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.
This commit is contained in:
EdisonJwa
2026-05-17 01:54:07 +08:00
parent c89acacc74
commit c16318c86b
2 changed files with 60 additions and 1 deletions
+1 -1
View File
@@ -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 # 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 # 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. # 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: environment:
sdk: ^3.11.5 sdk: ^3.11.5
@@ -387,6 +387,65 @@ impl IosVoiceUnit {
) )
.map_err(|e| AudioError::Backend(format!("vpio enable input I/O: {e}")))?; .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 // Stream format. Apple's iOS canonical format for
// AudioUnits is Linear PCM, 16-bit signed integer samples // AudioUnits is Linear PCM, 16-bit signed integer samples
// (see "Canonical formats" in the Audio Unit Hosting Guide // (see "Canonical formats" in the Audio Unit Hosting Guide