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.
iOS "voice chat" had echo and no noise suppression: real iOS AEC/NS/AGC
come only from Apple's Voice-Processing I/O unit (VPIO), but the core
plays/captures via miniaudio's plain RemoteIO units, so .voiceChat mode
alone never engaged AEC.
Core (ABI PATCH 1->2):
- vc_set_mixed_output_sink + vc_set_external_playback. In external mode the
AudioEngine opens no hardware playback device; a mixer-timer thread drives
on_playback (decode+mix) on a ~20ms cadence and ships the final mix to the
sink. start() also skips the hardware capture device when the MIC stream is
external_feed (AudioParams.external_capture).
- New white-box test test_external_playback (drives the timer with no hw).
iOS/Swift:
- StreamDescriptor.externalFeed; VoiceCatClient.setMixedOutputSink /
setExternalPlayback wrappers.
- IOSVoiceProcessingEngine: AVAudioEngine + setVoiceProcessingEnabled; mic
tap -> feedPcm, mixed-sink lock-free ring -> AVAudioSourceNode (both share
the VPIO unit so AEC has its reference signal).
- IOSAudioRouter.currentConfigUsesVoiceProcessing scopes VPIO to the AEC
presets; SessionState join/leave + reconcileVoicePath() switch paths;
Voice Chat defaults to speaker; Settings surfaces AEC/NS state.
Known: pending on-device verification; a few bugs to fix afterward.