fix: audio pipeline survives speaker toggle during LXST calls#3
Merged
Conversation
Toggling speaker during a voice call fires AVAudioEngineConfigurationChange. The handler queried inputNode.outputFormat(forBus: 0) after the engine was stopped, which returned the stale codec format (1ch 24kHz) instead of the real hardware format (1ch 48kHz). Installing a tap with that mismatched format crashed with "Failed to create tap due to format mismatch". Fix: use AVAudioSession.sharedInstance().sampleRate (the actual hardware rate) to create the tap format, instead of trusting the stale inputNode query. The drain loop resamples from the real HW rate to the codec rate. Also adds ColumbaAppTests target with AudioRingBuffer and AudioManager configuration change tests. Verified on-device: speaker toggle in both directions, rapid toggling, and full call lifecycle all work without crash. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… builds The local package reference pointed to ../reticulum-swift-lib but the GitHub repo was renamed to reticulum-swift. SPM derived identity "reticulum-swift-lib" from the directory name, conflicting with the "reticulum-swift" identity from LXMF/LXST transitive dependencies. Fix: point the local reference to ../reticulum-swift (which has the matching Package.swift on main). Also add shared xcscheme with test target configured so xcodebuild test works. All 17 tests pass on simulator (10 AudioManager + 7 AudioRingBuffer). Note: device builds may need Xcode signing re-auth after the package re-resolution invalidated DerivedData cached provisioning profiles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 tasks
torlando-tech
pushed a commit
that referenced
this pull request
May 26, 2026
Closes the loop on the lxst-wiring batch. With this commit a call placed
from sim1 to sim2 fully establishes through the canonical Mark Qvist
Python RNS+LXST embedded in BeeWare — the full Reticulum LXST signal
exchange (AVAILABLE → LINKIDENTIFY → RINGING → CONNECTING → ESTABLISHED
+ PREFERRED_PROFILE) crosses the bridge.
Bridge wiring completed in this commit:
- Compat Link: identifyHook (parallels sendBytesHook), wired by
AppServices to PythonRNSBackend.linkIdentify(linkId:) so the
caller's `link.identify(identity:)` call propagates through Python
and the remote sees the proper IDENTIFY signal.
- Compat Link.identifyCallbacks: was weakly held, became strong.
Telephone.handleIncomingLink constructs a
`TelephoneIdentifyHandler(telephone: self)` inline with no caller-
held reference; under weak storage it deallocated immediately and
the inbound call silently stalled at AVAILABLE, never receiving
the identify-callback invocation when Python's link_identified
event arrived. Strong storage fixes it.
- app/rns_bridge.py: announce_telephony() function so the LXST
telephony destination announces alongside the LXMF delivery one;
sendAllAnnounces() invokes both. Without this, sim2 couldn't see
sim1's voice destination in its path table.
- URL test triggers: lxma://test-call?to=HEX[&profile=...] places
an outbound call through CallManager.initiateCall; lxma://test-answer
auto-accepts the ringing call. Mirrors the existing lxma://test-* set.
- CallManager auto-answer escape hatch keyed off env var
COLUMBA_AUTO_ANSWER=1. Reason: simctl openurl only delivers URLs
to the frontmost simulator window; sim2 stays backgrounded during
the sim1 → sim2 test so we can't drive the answer via URL. The
env var lets the smoke test reliably push sim2 past RINGING.
Smoke test (iPhone 17 Pro / 17 Pro Max sims, fresh boots):
sim1 → sim2: [TEST-CALL] to=e6abaf41... profile=qualityMedium
sim1: [TEL_BRIDGE] opened outbound link 1 → e6abaf41
sim2: [PY] link 1 state=established inbound=true
sim2: [CALL] handleIncomingLink
sim2: [TEL] sendData #1: 4B (AVAILABLE)
sim1: [TEL] handlePacket first4=81009103 (AVAILABLE)
sim1: (link.identify → linkIdentifyHook → Python linkIdentify)
sim2: [PY] link 1 identified=cb54d9a4
sim2: [CALL] Ringing from: cb54d9a4...
sim2: [TEL] sendData #2: 4B (RINGING)
sim2: [TEL] PREFERRED_PROFILE: Medium Quality
sim1: [CALL] Ringing from: 4e4f5bcb...
sim2: [CALL] COLUMBA_AUTO_ANSWER=1 — auto-answering
sim2: [TEL] answer(): sending CONNECTING, profile=Medium Quality
sim2: [TEL] sendData #3: 4B (CONNECTING)
sim2: [TEL] answer(): pipeline started, codec=opus
sim2: [TEL] answer(): sent ESTABLISHED
sim2: [CALL] establishedCallback fired
sim2: [AUDIO] startAudio() creating AudioManager
sim1: [TEL] handlePacket first4=81009105 (CONNECTING)
sim1: [TEL] handlePacket first4=81009106 (ESTABLISHED)
sim1: [AUDIO] startAudio() creating AudioManager
Audio frame round-trip (what we'd ideally see next, e.g. opus
"frame #1: hdr=0x01 audioBytes=N") doesn't fire on the simulator:
both sides reach [AUDIO] startAudio() but then log "deferring engine
start until didActivateAudioSession" because CallKit on the simulator
doesn't drive its audio-session-activation delegate the way a physical
device does (the sim has no real audio hardware to bind to). The
signaling path is end-to-end across the Python bridge; verifying
actual audio bytes traversing requires installing on Torlando's USB-
attached iPhone, which is a separate workstream from this 5-commit
batch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Failed to create tap due to format mismatchbecause the config change handler queried a stale codec format (24kHz) instead of the real hardware format (48kHz)AVAudioSession.sharedInstance().sampleRatefor the tap format instead of the staleinputNode.outputFormatqueryhandleAudioSessionActivated()(same latent bug)ColumbaAppTeststarget withAudioRingBufferandAudioManagerconfig change tests (17 tests, all passing)reticulum-swift/reticulum-swift-libpackage identity conflict that blocked simulator buildsRoot cause
After
AVAudioEngineConfigurationChange, the engine is stopped by iOS. QueryinginputNode.outputFormat(forBus: 0)returns the negotiated codec rate (24kHz fromsetPreferredSampleRate), but the hardware has reverted to native 48kHz. The mismatch crashesinstallTap.Test plan
AudioRingBufferTests(7 tests) andAudioManagerConfigChangeTests(10 tests) — all pass on simulator🤖 Generated with Claude Code