From c16318c86b2fcafa0398cd69adce59cde7ee6944 Mon Sep 17 00:00:00 2001 From: EdisonJwa Date: Sun, 17 May 2026 01:54:07 +0800 Subject: [PATCH] fix(audio,ios): bypass VPIO voice processing for clean playback (rc.8+68) User report at +67 (.voiceChat mode): playback still 'broken'. Even with VPIO's Apple-documented session-mode pairing, its output-side gating chain (echo subtraction + adaptive noise suppression) chops quiet inter-phoneme content of human voice. Musicbot signal (loud, ~continuous) survives because it stays above the gating threshold; speech does not. Fix: set kAUVoiceIOProperty_BypassVoiceProcessing = 1 on the unit immediately after EnableIO (before stream format / callbacks / initialize). This disables ALL VPIO voice processing \u2014 the unit becomes effectively a vanilla RemoteIO with mic + speaker buses. Raw samples pass through both directions. Trade-off: * Lost: Apple's hardware AEC + AGC + NS on the mic path. User reports current capture is clean already, suggesting their test environment (headset? non-speakerphone?) doesn't need AEC. If echo loops back when speakerphone is engaged, we'll re-evaluate \u2014 either re-enable VPIO selectively for echo-prone routes or ship software AEC (DEC-007). * Gained: playback is no longer gated. Quiet inter-phoneme speech content reaches the speaker. Property setter: * Constant: kAUVoiceIOProperty_BypassVoiceProcessing = 2100 * Scope: Global, Element: Input (1) per WebRTC's reference iOS ADM (voice_processing_audio_unit.mm). * Value: u32 = 1 (= bypass). * Soft-fail with warn log if the property is rejected on an exotic iOS version (the unit still works, just with VPIO defaults). Build counter 67 -> 68. --- apps/chanora_flutter/pubspec.yaml | 2 +- crates/chanora_audio/src/ios_voice_unit.rs | 59 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/apps/chanora_flutter/pubspec.yaml b/apps/chanora_flutter/pubspec.yaml index a39965d..2fa75ea 100644 --- a/apps/chanora_flutter/pubspec.yaml +++ b/apps/chanora_flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0-rc.8+67 +version: 1.0.0-rc.8+68 environment: sdk: ^3.11.5 diff --git a/crates/chanora_audio/src/ios_voice_unit.rs b/crates/chanora_audio/src/ios_voice_unit.rs index 361c0e2..58b1772 100644 --- a/crates/chanora_audio/src/ios_voice_unit.rs +++ b/crates/chanora_audio/src/ios_voice_unit.rs @@ -387,6 +387,65 @@ impl IosVoiceUnit { ) .map_err(|e| AudioError::Backend(format!("vpio enable input I/O: {e}")))?; + // Bypass VPIO's built-in voice processing (AEC + AGC + NS). + // + // Rationale: with the AVAudioSession in mode .voiceChat + // (the documented VPIO pair), VPIO's output-side gating + // chain still aggressively gates quiet inter-phoneme + // content as "noise". User-reported symptom: musicbot + // audio plays cleanly (loud, near-continuous signal), + // but human voice with normal 20 dB peak-to-average + // ratio sounds broken and unintelligible because the + // average-level content gets chopped out between + // phonemes. + // + // Bypassing voice processing turns VPIO into effectively + // a vanilla I/O unit \u2014 raw mic samples in, raw + // playback out. The trade-offs: + // + // * Capture lost: Apple's AEC + AGC + NS on the mic + // input. The user's current setup reports clean + // capture without these (likely using a headset + // where AEC is unnecessary, or not generating + // echo loud enough to matter). If echo loops back + // when on speakerphone, we'll need to either + // re-enable VPIO selectively or ship software AEC + // (DEC-007). + // * Playback gained: signal passes through verbatim. + // Quiet inter-phoneme samples are no longer gated. + // + // Property constants from Apple's AudioUnitProperties.h + // (also exposed via objc2-audio-toolbox): + // kAUVoiceIOProperty_BypassVoiceProcessing = 2100 + // + // Set on (Scope::Global, Element::Input = 1) per WebRTC's + // reference iOS audio device manager + // (voice_processing_audio_unit.mm). The bypass flag is a + // UInt32 with 1 = bypass. + const K_AU_VOICE_IO_PROPERTY_BYPASS_VOICE_PROCESSING: u32 = 2100; + let bypass: u32 = 1; + if let Err(e) = unit.set_property( + K_AU_VOICE_IO_PROPERTY_BYPASS_VOICE_PROCESSING, + Scope::Global, + Element::Input, + Some(&bypass), + ) { + // Soft-fail: if the bypass property is rejected on + // an exotic iOS version, log and continue. The unit + // is still usable with default voice processing + // (with whatever quality issues that brings). + warn!( + target: "chanora_audio", + error = %e, + "vpio: BypassVoiceProcessing set failed; continuing with VPIO defaults" + ); + } else { + info!( + target: "chanora_audio", + "vpio: voice processing bypassed (no AEC/AGC/NS) for cleaner playback" + ); + } + // Stream format. Apple's iOS canonical format for // AudioUnits is Linear PCM, 16-bit signed integer samples // (see "Canonical formats" in the Audio Unit Hosting Guide