fix: Oboe SIGSEGV on OpenSL ES + speaker toggle for native stream#470
fix: Oboe SIGSEGV on OpenSL ES + speaker toggle for native stream#470torlando-tech merged 8 commits intomainfrom
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR updates the LXST-kt submodule to fix two audio bugs in the native Oboe playback engine:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
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()
Last reviewed commit: 77a4629 |
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>
a58e4c2 to
a4a4348
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
| if link is not None and self._tx_batch: | ||
| try: | ||
| self._flush_tx_batch(link) |
There was a problem hiding this comment.
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`.
…oggle fix: Oboe SIGSEGV on OpenSL ES + speaker toggle for native stream
Summary
stop()+close()+reset()teardown sequence left a race window where the callback could fire after the stream object was freed. Fix: remove redundantstop()(Oboe'sclose()handles it), adddestroyed_atomic guard inonAudioReady().setCommunicationDevice()routing changes. Fix: addrestartStream()to close/reopen the Oboe stream, called fromAudioDevice.setSpeakerphoneOn().Files changed (LXST-kt submodule)
oboe_playback_engine.hdestroyed_atomic,restartStream()declarationoboe_playback_engine.cppcloseStream(),destroy(),stopStream(); adddestroyed_guard in callback; addrestartStream()oboe_playback_jni.cppnativeRestartStreamJNI methodNativePlaybackEngine.ktrestartStream()+ external declarationAudioDevice.ktNativePlaybackEngine.restartStream()insetSpeakerphoneOn()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 togglerestartStream()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
compileDebugKotlinpassesTelephoneTest(65 tests) +ProfileTest(43 tests) passManual — 32-bit device (primary target)
closeStream()pathManual — 64-bit device (regression check)
LXST:OboeEngine— verify:Stream opened: API=AAudio(confirms AAudio path on 64-bit)Restarting stream for audio routing changeappears on speaker toggleStream closed/Stream startedpairs match (no orphaned streams)onErrorAfterCloserecovery still works (existing behavior, not new)🤖 Generated with Claude Code