fix(ios): stereo mic + A2DP output, add vc_audio_restart ABI

Diagnosed by comparing against TeamTalk5 (Client/iTeamTalk), which
achieves stereo mic + A2DP output. Five fixes:

1. configureStereoCapture now calls setPreferredInput +
   setInputDataSource (mirroring TeamTalk5's SoundDevicesModel).
   Previously omitted based on incorrect diagnosis that
   setPreferredInput collapsed A2DP — the real culprit was
   setPreferredInputNumberOfChannels(2), which neither project uses.

2. New C ABI: vc_audio_restart (full stop + re-init, unlike
   suspend/resume which only stop/start). Swift wrapper added.
   The withAudioSuspend wrapper that used it was removed after
   on-device testing showed it killed all audio (including
   VoiceOver) when switching presets — the core's
   set_capture_channels handles engine restart internally.

3. Bluetooth options: Voice Chat preset now includes BOTH
   .allowBluetoothHFP AND .allowBluetoothA2DP (matching TeamTalk5's
   UtilSound.swift:228). Previously HFP-only blocked A2DP headphones.

4. Capture channels now reset when switching stereo→mono via
   selectCaptureChannels/applyPreset. AudioSessionManager tracks
   activeMicStreamId (set by SessionState on join/leave voice).

5. Docs synced: voice.md, tech-stack.md, architecture.md,
   PROGRESS.md. Removed stale setPreferredInputNumberOfChannels(2)
   references.

Verified: ctest --preset dev 21/21 green, iOS client builds.
Stereo mic + A2DP output still needs on-device debugging — the
core recipe is correct but iOS 26 route behavior requires
hands-on testing with a debugger.
This commit is contained in:
2026-06-19 16:58:21 +02:00
parent 1a1c8a1dfe
commit fdcc84fb42
14 changed files with 399 additions and 100 deletions

View File

@@ -159,6 +159,17 @@ final class AppState {
connectStatus = ""
showPasswordPrompt = false
self.session = newSession
// Activate the audio session now, while connected NOT lazily when the first
// remote stream arrives. The core opens its miniaudio playback device the moment
// a remote stream starts and only THEN emits .streamStarted; if we waited for
// that event to activate, the playback device would open against an inactive
// AVAudioSession and produce no sound (the "can't hear anyone" bug). Activating
// here guarantees the session is live before any device opens.
do {
try AudioSessionManager.shared.ensureSessionActive()
} catch {
print("Audio session activate on connect failed: \(error)")
}
} else {
connectStatus = "Auth failed: \(ev.result.description)"
showPasswordPrompt = true