fix(ios): fix stereo mic + A2DP output silence
Three coordinated fixes for the bug where enabling stereo mic capture
caused all audio output (A2DP, speaker, wired) to go silent:
1. audio_engine.cpp — open playback before capture
On iOS, starting the stereo capture AudioUnit can trigger an audio
route reconfiguration that drops A2DP before the playback device has
a chance to claim the route. Opening and starting the playback device
first commits the output route (A2DP), so iOS is less likely to drop
it when stereo capture activates afterward.
2. client.cpp — decouple set_capture_channels from engine restart
Previously vc_set_capture_channels() stopped and restarted the engine
immediately, which opened capture first (old ordering) and raced
against the settling AVAudioSession route. Now it only stores the
channel count; the caller (Swift via vc_audio_restart) controls when
the engine restarts, after the route has settled.
3. IOSAudioRouter.swift — call audioRestart() after channel config
selectCaptureChannels() and applyPreset() now call audioRestart()
after applyConfiguration() + setCaptureChannels(). This is the
vc_audio_restart() path that was added to the ABI in fdcc84f but
never wired up in the Swift layer. The restart sees the stored
channel count and reopens devices in the correct order (playback
first, capture second).
The doStartMicStream path is unaffected: setCaptureChannels is called
before the server acknowledges the stream (engine not yet running), so
ensure_audio_running() picks up capture_channels=2 directly when the
stream is confirmed and opens with the right count from the start.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -577,13 +577,16 @@ final class IOSAudioRouter: ObservableObject {
|
||||
captureChannels = channels
|
||||
savePreferences()
|
||||
applyConfiguration()
|
||||
// Sync the core's capture channel count. The core's set_capture_channels handles
|
||||
// the engine restart internally (stop + ensure_audio_running) — no need for the
|
||||
// Swift layer to suspend/restart separately.
|
||||
// Update the core's stored capture channel count (does not restart the engine).
|
||||
if let streamId = AudioSessionManager.shared.activeMicStreamId {
|
||||
_ = AudioSessionManager.shared.client?.setCaptureChannels(
|
||||
streamId: streamId, channels: channels.channelCount)
|
||||
}
|
||||
// Restart the engine AFTER AVAudioSession routing has settled and the channel
|
||||
// count is stored. The engine reopens playback first (committing the A2DP/output
|
||||
// route), then capture — avoiding the race where stereo capture activation drops
|
||||
// A2DP before the playback device has a chance to claim the route.
|
||||
_ = AudioSessionManager.shared.client?.audioRestart()
|
||||
}
|
||||
|
||||
// MARK: - Presets
|
||||
@@ -620,12 +623,14 @@ final class IOSAudioRouter: ObservableObject {
|
||||
UserDefaults.standard.set(preset.rawValue, forKey: kPreset)
|
||||
savePreferences()
|
||||
applyConfiguration()
|
||||
// Sync the core's capture channel count. The core's set_capture_channels handles
|
||||
// the engine restart internally.
|
||||
// Update the core's stored capture channel count (does not restart the engine).
|
||||
if let streamId = AudioSessionManager.shared.activeMicStreamId {
|
||||
_ = AudioSessionManager.shared.client?.setCaptureChannels(
|
||||
streamId: streamId, channels: preset.captureChannels.channelCount)
|
||||
}
|
||||
// Restart the engine AFTER AVAudioSession routing has settled and the channel
|
||||
// count is stored. Playback opens first (commits A2DP route), then capture.
|
||||
_ = AudioSessionManager.shared.client?.audioRestart()
|
||||
refreshRoutes()
|
||||
logger.info("applyPreset — \(preset.rawValue)")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user