fix(ios): stop core opening a second mic device — dual capture/crackle
On iOS the remote end heard the mic twice and crackly (BT headset + internal mic in Voice Chat; mono + stereo copies of the internal mic in Stereo Mic). Root cause is an io-thread ordering race. `ensure_audio_running()` only set `external_capture` when a MIC stream already existed, but it also runs from `sync_remote_streams` on the post-auth `ServerStateSnapshot` — before the user joins voice. With `external_playback_` still false and no MIC stream, `AudioEngine::start()` opened a real miniaudio capture device that stayed open all session (later calls early-return on running()), racing the AVAudioEngine input tap fed via `vc_stream_feed_pcm`. `on_capture_frame` then encoded+sent both paths — the mic transmitted twice, the two unsynchronized capture clocks producing the crackle. - core (ensure_audio_running): force `external_capture = true` whenever `external_playback_` is set, so iOS unified mode never opens a hardware capture device. No-op on desktop. - ios (AppState): move `setExternalPlayback(true)` before `connect()`, so the flag is set before the io thread processes any message — closing the race. Verified on device: remote end hears the iOS mic once and clean in both Voice Chat (+ BT) and Stereo Mic.
This commit is contained in:
@@ -1184,6 +1184,16 @@ void vc_client::ensure_audio_running() {
|
||||
p.external_capture = it->second.external_feed;
|
||||
}
|
||||
}
|
||||
// iOS unified mode (external_playback): capture is ALWAYS external — the Swift AVAudioEngine
|
||||
// owns the only mic path and feeds via vc_stream_feed_pcm. Force external_capture so start()
|
||||
// never opens a hardware mic device, even when ensure_audio_running runs before a MIC stream
|
||||
// exists (a remote stream arriving first — e.g. the post-auth ServerStateSnapshot — starts
|
||||
// the engine for playback). Without this the core opens a miniaudio capture device that races
|
||||
// the AVAudioEngine tap → dual mic capture → duplicated, crackly audio on the remote end.
|
||||
// No-op on desktop, where external_playback_ is never set.
|
||||
if (external_playback_.load(std::memory_order_acquire)) {
|
||||
p.external_capture = true;
|
||||
}
|
||||
// iOS VPIO: skip the hardware playback device and drive the mixer on a timer, delivering the
|
||||
// final mix to the mixed-output sink for the Swift VPIO renderer (vc_set_external_playback).
|
||||
audio_engine_.set_external_playback(external_playback_.load(std::memory_order_acquire));
|
||||
|
||||
Reference in New Issue
Block a user