fix(ios-audio): unify iOS audio onto one always-external AVAudioEngine
The iOS audio path was a hybrid: Voice-Chat-class presets ran a native VPIO AVAudioEngine (core external) while Stereo/Studio/A2DP presets ran the core's miniaudio devices. Nearly every "no input / no output / both" bug lived in the seam between the two paths — the lingering miniaudio capture unit fighting VPIO, the audioRestart ordering dance, the route-change "glitching" loop, stereo<->mono stickiness, and "can't hear anyone". Switching presets/routes mid-call routinely dropped a direction. Drive ALL iOS audio through one AVAudioEngine with the core fully external at all times: setExternalPlayback(1) once at connect, every MIC stream external_feed=1, mic via vc_stream_feed_pcm, playback via vc_set_mixed_output_sink (drained by an always-on AVAudioSourceNode so remote audio plays before joining voice). VPIO + AGC toggle per preset. Every preset/route/interruption change funnels through one deterministic Swift-only reconfigure (stop -> apply session config -> rebuild -> start) — no second path to hand off to, so a change can't drop a direction. - IOSVoiceProcessingEngine.swift -> IOSAudioEngine: always-on source-node playback, conditional mic tap, VPIO/AGC; one rebuild() backing startListening/stop/startMic/stopMic/reconfigure/setCaptureChannels. - IOSAudioRouter: 7 presets -> 4 (Voice Chat / Stereo Mic / Mono Mic / Advanced); persisted voiceProcessingEnabled + agcEnabled; setters call IOSAudioEngine.reconfigure() instead of audioRestart/reconcileVoicePath. - AudioSessionManager slimmed; SessionState mic lifecycle collapsed; AppState wires external playback + listening at connect, stop at disconnect; SettingsView shows 4 presets + Advanced VPIO/AGC toggles. No core/ABI/test changes — relies on the already-shipped external API (test_external_pcm, test_external_playback). xcodebuild iOS device Debug BUILD SUCCEEDED. Updates docs/voice.md §8 and PROGRESS.md.
This commit is contained in:
@@ -38,9 +38,9 @@ struct SettingsView: View {
|
||||
.accessibilityLabel("Speaker output")
|
||||
.accessibilityHint("Routes audio to the speaker instead of the earpiece when no headphones are connected.")
|
||||
|
||||
// Surface the voice-processing state. On the AEC presets the native iOS
|
||||
// Surface the voice-processing state. On Voice Chat the native iOS
|
||||
// Voice-Processing unit (VPIO) does echo cancellation, noise suppression and
|
||||
// automatic gain control; the other presets (stereo/studio/A2DP) can't use it.
|
||||
// automatic gain control; the stereo / mono-mic / A2DP configs can't use it.
|
||||
if router.currentConfigUsesVoiceProcessing {
|
||||
Label("Echo cancellation & noise suppression on (iOS voice processing)",
|
||||
systemImage: "waveform.badge.mic")
|
||||
@@ -48,18 +48,11 @@ struct SettingsView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityLabel("Echo cancellation and noise suppression are on")
|
||||
} else {
|
||||
Label("No echo cancellation in this preset (stereo / studio / A2DP)",
|
||||
Label("No echo cancellation in this configuration (stereo / A2DP / off)",
|
||||
systemImage: "waveform.slash")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityLabel("Echo cancellation is off in this preset")
|
||||
}
|
||||
|
||||
if !router.hasBluetoothDevice && !router.hasWiredHeadset {
|
||||
Text("Connect Bluetooth headphones or a wired headset for more presets.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityLabel("No external audio device connected")
|
||||
.accessibilityLabel("Echo cancellation is off in this configuration")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +120,27 @@ struct SettingsView: View {
|
||||
}
|
||||
.accessibilityLabel("Microphone processing mode")
|
||||
|
||||
// Voice-processing (VPIO) controls — only meaningful on a VPIO-capable
|
||||
// config (mono + standard + non-A2DP). iOS bundles echo cancellation and
|
||||
// noise suppression into one master switch (no per-stage toggle); AGC is
|
||||
// the one sub-stage it lets us control independently.
|
||||
if router.voiceProcessingAvailable {
|
||||
Toggle("Voice Processing (AEC + noise suppression)", isOn: Binding(
|
||||
get: { router.voiceProcessingEnabled },
|
||||
set: { router.setVoiceProcessingEnabled($0) }
|
||||
))
|
||||
.accessibilityLabel("Voice processing")
|
||||
.accessibilityHint("Echo cancellation and noise suppression, bundled together by iOS.")
|
||||
|
||||
if router.voiceProcessingEnabled {
|
||||
Toggle("Automatic Gain Control", isOn: Binding(
|
||||
get: { router.agcEnabled },
|
||||
set: { router.setAgcEnabled($0) }
|
||||
))
|
||||
.accessibilityLabel("Automatic gain control")
|
||||
}
|
||||
}
|
||||
|
||||
if router.showsRawModeSpeakerWarning {
|
||||
Label(
|
||||
"Raw mode on speaker — echo risk (no AEC)",
|
||||
|
||||
Reference in New Issue
Block a user