fix(ios): VPIO silent playback + quiet speaker in stereo/studio presets
Two on-device bugs in the native iOS Voice-Processing path (Swift-only; no core/ABI change). 1. Voice Chat (VPIO) silent playback: doStartMicStream() called audioRestart() BEFORE startStream, so when the engine was already running (a remote stream had started it) it reopened with external_capture=false and opened a hardware miniaudio capture device. The announce-result restart then early-returns (engine already running) so that device was never dropped and fought the AVAudioEngine VPIO unit, silencing playback. Now: setExternalPlayback first, then startStream (stores external_feed synchronously), THEN audioRestart() — the core reopens in full external mode (no hardware devices). Added VPIO diagnostics: graph/route formats at start, ring written/read totals at teardown. 2. Stereo Mic / Studio quiet earpiece: the .builtInMicBtA2dp presets omit .defaultToSpeaker (it breaks A2DP) and skip forceSpeaker, so with no Bluetooth connected output pinned to the quiet receiver. New IOSAudioRouter.applyA2dpSpeakerFallback() overrides to the built-in speaker when no external (A2DP/wired/AirPlay) output is present and clears the override when one is — called after activation and on device-change route changes.
This commit is contained in:
@@ -75,6 +75,14 @@ final class PCMRing {
|
||||
|
||||
/// Discard everything buffered — call before (re)starting so stale pre-roll isn't played.
|
||||
func reset() { OSMemoryBarrier(); readIdx = writeIdx }
|
||||
|
||||
/// Diagnostics: monotonic total samples written / read since the ring was created. The
|
||||
/// indices are already cumulative, so these are free. Only read them when both threads are
|
||||
/// quiesced (e.g. at teardown after the engine + mixer sink are stopped) — they are not
|
||||
/// synchronized for live cross-thread reads. Lets us tell "core never delivered PCM" (Bug 1
|
||||
/// core path) apart from "PCM arrived but produced no sound" (AVAudioEngine output graph).
|
||||
var debugTotalWritten: UInt64 { writeIdx }
|
||||
var debugTotalRead: UInt64 { readIdx }
|
||||
}
|
||||
|
||||
/// Native iOS voice-processing audio path (docs/voice.md §8 "iOS voice processing").
|
||||
@@ -215,7 +223,17 @@ final class IOSVoiceProcessingEngine {
|
||||
do {
|
||||
try engine.start()
|
||||
isRunning = true
|
||||
logger.info("VPIO engine started — inFormat=\(inFormat), captureCh=\(targetCh)")
|
||||
// Diagnostics: capture the negotiated graph formats and the live output route so a
|
||||
// silent-playback report can be triaged (format/rate mismatch vs. routing vs. the
|
||||
// core not delivering PCM — see the ring stats logged in teardown()).
|
||||
let outFmt = engine.outputNode.outputFormat(forBus: 0)
|
||||
let mixFmt = engine.mainMixerNode.outputFormat(forBus: 0)
|
||||
let route = AVAudioSession.sharedInstance().currentRoute.outputs
|
||||
.map { "\($0.portName)[\($0.portType.rawValue)]" }.joined(separator: ", ")
|
||||
logger.info("""
|
||||
VPIO engine started — inFormat=\(inFormat), captureCh=\(targetCh), \
|
||||
outputNode=\(outFmt), mainMixer=\(mixFmt), outputRoute=[\(route)]
|
||||
""")
|
||||
} catch {
|
||||
logger.error("VPIO engine start failed: \(error.localizedDescription)")
|
||||
teardown()
|
||||
@@ -234,6 +252,10 @@ final class IOSVoiceProcessingEngine {
|
||||
client?.setMixedOutputSink(nil, user: nil)
|
||||
engine.inputNode.removeTap(onBus: 0)
|
||||
if engine.isRunning { engine.stop() }
|
||||
// Diagnostics (threads now quiesced): how much mixed PCM the core delivered into the ring
|
||||
// vs. how much the render thread consumed. written==0 ⇒ the core never delivered (Bug 1
|
||||
// core/lifecycle path); written>0 with no audible output ⇒ the AVAudioEngine output graph.
|
||||
logger.info("VPIO ring stats — written=\(self.ring.debugTotalWritten) read=\(self.ring.debugTotalRead) samples")
|
||||
try? engine.inputNode.setVoiceProcessingEnabled(false)
|
||||
if let src = sourceNode {
|
||||
engine.detach(src)
|
||||
|
||||
Reference in New Issue
Block a user