Skip to content

fix: Oboe SIGSEGV on OpenSL ES + speaker toggle for native stream#470

Merged
torlando-tech merged 8 commits intomainfrom
fix/oboe-sigsegv-speaker-toggle
Feb 16, 2026
Merged

fix: Oboe SIGSEGV on OpenSL ES + speaker toggle for native stream#470
torlando-tech merged 8 commits intomainfrom
fix/oboe-sigsegv-speaker-toggle

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

  • Bug 1 (COLUMBA-3A): Fix fatal SIGSEGV in Oboe audio callback on 32-bit OpenSL ES devices (Galaxy A13). The stop() + close() + reset() teardown sequence left a race window where the callback could fire after the stream object was freed. Fix: remove redundant stop() (Oboe's close() handles it), add destroyed_ atomic guard in onAudioReady().
  • Bug 2: Fix speaker toggle having no effect on Oboe playback path. Native Oboe streams bind to a hardware device at open time and don't follow setCommunicationDevice() routing changes. Fix: add restartStream() to close/reopen the Oboe stream, called from AudioDevice.setSpeakerphoneOn().

Files changed (LXST-kt submodule)

File Changes
oboe_playback_engine.h Add destroyed_ atomic, restartStream() declaration
oboe_playback_engine.cpp Fix closeStream(), destroy(), stopStream(); add destroyed_ guard in callback; add restartStream()
oboe_playback_jni.cpp Add nativeRestartStream JNI method
NativePlaybackEngine.kt Add restartStream() + external declaration
AudioDevice.kt Call NativePlaybackEngine.restartStream() in setSpeakerphoneOn()

Impact scope

These changes affect all ABIs (arm64-v8a, armeabi-v7a, x86_64), not just 32-bit. The SIGSEGV was only observed on OpenSL ES, but the teardown fix and destroyed_ guard apply to all Oboe streams. The speaker toggle restartStream() is also architecture-agnostic. Risk on 64-bit AAudio devices is low — close() already synchronizes callbacks reliably, and the atomic guard adds ~1ns per callback.

Test plan

Automated

  • compileDebugKotlin passes
  • TelephoneTest (65 tests) + ProfileTest (43 tests) pass

Manual — 32-bit device (primary target)

  • Deploy to Galaxy A13 or equivalent armeabi-v7a device
  • Make a voice call, verify audio plays through earpiece
  • Toggle speaker — audio should switch to speaker at normal volume
  • Toggle back to earpiece — audio should return to earpiece
  • End call — no crash on teardown
  • Repeat 3x to stress the closeStream() path
  • Check Sentry for COLUMBA-3A recurrence after release

Manual — 64-bit device (regression check)

  • Deploy to a 64-bit device (Pixel, Samsung S-series, etc.)
  • Make a voice call, verify audio plays through earpiece at normal volume
  • Toggle speaker mid-call — audio should switch with only a brief (~50ms) gap
  • Toggle back to earpiece — audio should return cleanly
  • End call normally — verify no crash, clean shutdown in logcat
  • Start a second call immediately after ending the first — verify no lingering state issues
  • During a call, check logcat for LXST:OboeEngine — verify:
    • Stream opened: API=AAudio (confirms AAudio path on 64-bit)
    • Restarting stream for audio routing change appears on speaker toggle
    • Stream closed / Stream started pairs match (no orphaned streams)
  • If device supports headphones/BT: plug in during call, verify onErrorAfterClose recovery still works (existing behavior, not new)

🤖 Generated with Claude Code

@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Feb 15, 2026

Codecov Report

❌ Patch coverage is 84.31373% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
python/lxst_modules/call_manager.py 84.31% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 15, 2026

Greptile Summary

This PR updates the LXST-kt submodule to fix two audio bugs in the native Oboe playback engine:

  • SIGSEGV fix (COLUMBA-3A): Removes the redundant stop() call before close() in stream teardown and adds a destroyed_ atomic guard in the onAudioReady callback to prevent use-after-free on 32-bit OpenSL ES devices (Galaxy A13). The close() call already stops the stream internally, and the atomic guard ensures any late-firing callback returns immediately.
  • Speaker toggle fix: Adds restartStream() which closes and reopens the Oboe stream when the speaker/earpiece toggle changes. Oboe streams bind to a hardware device at open time and don't follow setCommunicationDevice() routing changes, so a full close/reopen cycle is needed. The Kotlin side catches UnsatisfiedLinkError to gracefully handle cases where the native library isn't loaded.
  • Race fix (follow-up commit): Sets isPlaying_ = false in restartStream() before calling closeStream() to prevent onErrorAfterClose() from concurrently reopening the stream during a speaker toggle restart.

Confidence Score: 4/5

  • This PR is safe to merge — it fixes a real SIGSEGV crash and adds necessary speaker toggle support with sound synchronization patterns.
  • The SIGSEGV fix is well-reasoned: removing the redundant stop() call and adding a destroyed_ atomic guard is the correct approach for OpenSL ES's less deterministic callback lifecycle. The restartStream race fix (setting isPlaying_=false before closeStream) properly prevents onErrorAfterClose from concurrent re-opening. The Kotlin-side UnsatisfiedLinkError catch is a defensive guard for when native code isn't loaded. The one deduction is for the theoretical concurrent access to the stream_ shared_ptr between restartStream and onErrorAfterClose without a mutex, though this is low-probability in practice.
  • oboe_playback_engine.cpp — the stream lifecycle methods (restartStream, openStream, closeStream, onErrorAfterClose) share stream_ without a mutex, relying only on atomic flag ordering. Manual testing on both 32-bit and 64-bit devices is critical.

Important Files Changed

Filename Overview
LXST-kt Submodule pointer update (4af529c → 8859783) bringing in two fixes: SIGSEGV guard for Oboe audio callback on OpenSL ES teardown, and speaker toggle restart for native Oboe streams. One minor thread-safety concern in restartStream but low practical risk.

Sequence Diagram

sequenceDiagram
    participant KT as AudioDevice.kt (Kotlin)
    participant NPE as NativePlaybackEngine.kt
    participant JNI as oboe_playback_jni.cpp
    participant OPE as OboePlaybackEngine
    participant Oboe as Oboe AudioStream
    participant CB as SCHED_FIFO Callback

    Note over KT: User toggles speaker
    KT->>NPE: NativePlaybackEngine.restartStream()
    NPE->>JNI: nativeRestartStream()
    JNI->>OPE: restartStream()
    OPE->>OPE: isPlaying_ = false
    OPE->>Oboe: stream_->close()
    Note over CB: Late callback sees isPlaying_=false → Stop
    Oboe-->>OPE: close() returns
    OPE->>OPE: stream_.reset()
    OPE->>Oboe: builder.openStream(stream_)
    OPE->>OPE: isPlaying_ = true
    OPE->>Oboe: stream_->requestStart()
    Oboe->>CB: onAudioReady() resumes

    Note over OPE: destroy() path (teardown)
    OPE->>OPE: destroyed_ = true
    OPE->>Oboe: stream_->close()
    Note over CB: Late callback sees destroyed_=true → memset(0) + Stop
    OPE->>OPE: stream_.reset()
Loading

Last reviewed commit: 77a4629

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

torlando-tech and others added 3 commits February 15, 2026 22:40
Update LXST-kt submodule with fixes for:
- SIGSEGV in Oboe callback on 32-bit OpenSL ES devices (COLUMBA-3A)
- Speaker toggle having no effect on native Oboe playback stream

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech torlando-tech force-pushed the fix/oboe-sigsegv-speaker-toggle branch from a58e4c2 to a4a4348 Compare February 16, 2026 03:41
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
torlando-tech and others added 4 commits February 15, 2026 23:46
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
__link_closed() was calling Kotlin callbacks (signal + onCallEnded)
while holding _call_handler_lock. The signal triggered Kotlin hangup()
synchronously, running NativePlaybackEngine.destroy() which blocks on
Oboe stream close — keeping the Python lock held for the entire
duration and preventing subsequent call() from acquiring it.

Fix: move Kotlin callbacks outside the lock (same pattern as hangup())
and dispatch hangup() async in Kotlin's STATUS_AVAILABLE handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Voice calls degraded progressively from 60ms to 130ms per-frame arrival
rate over 20 seconds, even on fast 1-hop local WiFi links. Root cause:
each RNS.Packet.send() holds the Python GIL for encryption (AES-256-CBC
+ HMAC-SHA256) and transport dispatch, creating a feedback loop where
both devices' TX/RX paths compete for GIL time.

Batch 3 audio frames per RNS.Packet.send() call, reducing crypto
overhead from ~16.7 calls/sec to ~5.6 calls/sec (67% reduction).
The LXST wire format already supports frame lists ({0x01: [f1, f2, f3]})
and the receiver already handles both single frames and lists.

Results: packet arrival rate stable at ~60ms/frame for 40+ seconds,
zero silence callbacks, zero PLC, buffer steady at 6-9 frames.

Also updates LXST-kt submodule with adaptive playout drain for ring
buffer latency bounding during packet bursts and speaker toggles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +436 to +438
if link is not None and self._tx_batch:
try:
self._flush_tx_batch(link)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The _tx_batch list is accessed by hangup() and receive_audio_packet() without a lock, creating a race condition that can cause lost audio frames during call teardown.
Severity: HIGH

Suggested Fix

Wrap all accesses and modifications to _tx_batch and its related state variables within both the hangup() and receive_audio_packet() methods using the self._call_handler_lock. This will ensure that operations on the transmit buffer are atomic and prevent concurrent modification from different threads.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: python/lxst_modules/call_manager.py#L436-L438

Potential issue: A race condition exists due to unsynchronized access to the `_tx_batch`
list from multiple threads. The `hangup()` method can read, modify, and clear
`_tx_batch` without a lock, while the `receive_audio_packet()` method concurrently
appends data to it, also without a lock. This can lead to lost audio frames, as one
thread might replace the list object (`_tx_batch = []`) while another thread is still
operating on a stale reference. This can also cause state corruption for diagnostic
counters like `_tx_batch_count`.

@torlando-tech torlando-tech merged commit 64b5767 into main Feb 16, 2026
10 checks passed
@torlando-tech torlando-tech deleted the fix/oboe-sigsegv-speaker-toggle branch February 16, 2026 20:21
torlando-tech added a commit that referenced this pull request Feb 17, 2026
…oggle

fix: Oboe SIGSEGV on OpenSL ES + speaker toggle for native stream
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant