fix(audio,ios): route playback via media channel (.default + .defaultToSpeaker) (rc.8+70)
User report after the 8x boost commit (e85a6d3): playback STILL broken, but now the diagnostic clearly shows the actual problem. Render-callback peak_out_i16 SATURATES at 32767 on speech peaks (cb=600, 1100, 1200, 2400) because the 8x boost amplifies an already-loud signal into hard clipping. Quiet content reaches audible level but loud peaks are catastrophically distorted. The 8x boost was treating the wrong cause. Real root cause (researched online after user prompted: 'this is iOS a popular platform, there must be solutions'): iOS has TWO independent audio channels: In-call channel (.voiceChat / .videoChat modes) * Routes through the phone-call audio path. * Aggressively ducks non-voice content to the earpiece. * Volume controlled by a separate in-call hardware register, not the side buttons when not actively on a phone call. Media channel (.default mode) * Routes through the standard media playback path. * No automatic ducking. * Volume controlled by the side volume buttons normally. With AVAudioSession mode .voiceChat, iOS sends our output through the in-call channel which plays at 'earpiece-level' loudness on the speaker too. Signal is technically present but buried under the speaker's noise floor. With mode .default + .defaultToSpeaker option, output routes via media channel and plays at normal loudness. Both Twilio (video-quickstart-ios) and Daily.co (patched WebRTC module) document the same workaround and use VPIO for AEC while keeping the session mode at .default for loud playback: github.com/twilio/video-quickstart-ios/issues/522 stackoverflow.com/questions/79834998 (Daily.co) The user also noticed 'tx/rx almost no changes even receiving packages' \u2014 likely a misinterpretation of the frames counter not advancing as fast as expected during quiet voice; AudioHandler returns silence when its jitter buffer is in buffering_samples state which doesn't fire 'decode failed' but also doesn't increment frames_received. The real issue is still the playback ducking; the counter behaviour is a downstream symptom. Changes: 1. AppDelegate.swift: AVAudioSession mode .voiceChat -> .default with options [.defaultToSpeaker, .allowBluetoothHFP, .allowBluetoothA2DP]. VPIO continues to do its job (AEC, NS, AGC on the mic side); only the playback routing changes. The earlier 'speaker selector silent under .default' bug does NOT apply because we no longer use cpal RemoteIO \u2014 VPIO honours overrideOutputAudioPort under any mode. 2. ios_voice_unit.rs: revert the 8x output boost frome85a6d3. With media-channel routing, signal levels are correct and no software amplification is needed. Render callback restored to plain (l+r)*0.5*gain downmix. 3. ios_voice_unit.rs: revert the BypassVoiceProcessing toggle fromc16318c. The VPIO chain stays enabled so we keep capture-side AEC/AGC/NS for free \u2014 the playback breakage it was trying to fix was the wrong layer all along. 4. ios_voice_unit.rs: drop the diagnostic render-callback log line. Production-clean code; can be re-enabled by reverting the diff in the closure if future debugging needs it. Build counter 69 -> 70.
This commit is contained in:
@@ -31,43 +31,74 @@ import AVFoundation
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
try session.setCategory(
|
||||
.playAndRecord,
|
||||
mode: .voiceChat,
|
||||
// Mode rationale (revisited after the iOS VPIO migration):
|
||||
mode: .default,
|
||||
// Mode rationale (re-revisited after the "low playback
|
||||
// volume" investigation, May 2026):
|
||||
//
|
||||
// We previously tried .default mode to fix the
|
||||
// "speaker/receiver toggle is silent" bug \u2014 that bug was
|
||||
// ultimately caused by cpal's iOS RemoteIO unit binding to
|
||||
// a stale physical transducer, not by .voiceChat itself.
|
||||
// After migrating to coreaudio-rs + kAudioUnitSubType_VoiceProcessingIO
|
||||
// We've cycled through .voiceChat -> .default -> .voiceChat
|
||||
// -> .default. Final answer is .default with
|
||||
// .defaultToSpeaker, driven by these findings:
|
||||
//
|
||||
// The earlier "speaker selector silent" bug under
|
||||
// .voiceChat was caused by cpal's RemoteIO unit binding
|
||||
// to a stale physical transducer. After migrating to
|
||||
// coreaudio-rs + kAudioUnitSubType_VoiceProcessingIO
|
||||
// (see crates/chanora_audio/src/ios_voice_unit.rs) the
|
||||
// route binding is correct under either mode because VPIO
|
||||
// is the canonical voice unit and natively re-binds on
|
||||
// overrideOutputAudioPort. So .default lost its only
|
||||
// benefit.
|
||||
// route binding is correct under either mode because
|
||||
// VPIO is the canonical voice unit and re-binds on
|
||||
// overrideOutputAudioPort. So route switching is no
|
||||
// longer a deciding factor.
|
||||
//
|
||||
// Under .default mode, VPIO's output-side voice processing
|
||||
// chain (echo subtraction, noise gating) interprets low-
|
||||
// amplitude playback signal as "no farend audio" and
|
||||
// aggressively gates inter-phoneme content. Symptom in
|
||||
// testing: musicbot (loud, continuous signal) plays fine,
|
||||
// human voice (peaks ~-6 dB, average ~-40 dB, classic
|
||||
// 20 dB peak-to-average ratio) sounds broken and
|
||||
// unintelligible \u2014 the quiet samples between phonemes
|
||||
// get gated out, destroying intelligibility.
|
||||
// The "broken playback quality" bug under either
|
||||
// .voiceChat or .default (with VPIO) was actually NOT
|
||||
// a VPIO problem at all. iOS has TWO independent audio
|
||||
// channels: the in-call channel (used by .voiceChat /
|
||||
// .videoChat modes) and the media channel (used by
|
||||
// .default). The in-call channel:
|
||||
// * Routes through the phone-call audio path
|
||||
// * Aggressively ducks non-voice content to the
|
||||
// earpiece (Apple's "speakerphone vs ear" UX)
|
||||
// * Volume controlled by separate in-call volume
|
||||
// hardware, not the side buttons (when not in a
|
||||
// phone call)
|
||||
// The media channel:
|
||||
// * Routes through the standard media playback path
|
||||
// * No automatic ducking
|
||||
// * Volume controlled by the side volume buttons
|
||||
//
|
||||
// Apple's documentation explicitly pairs VPIO with
|
||||
// AVAudioSessionModeVoiceChat. WebRTC's reference iOS
|
||||
// ADM implementation uses the same pair. Under
|
||||
// .voiceChat mode VPIO's internal AGC/AEC/NS thresholds
|
||||
// are tuned for telephony-style speech and pass quiet
|
||||
// inter-phoneme content through cleanly.
|
||||
// Even with VPIO + .voiceChat producing a perfectly
|
||||
// good signal, iOS's in-call channel routing made it
|
||||
// play at "earpiece" loudness on the speaker too \u2014
|
||||
// user-perceived as "broken and poor" because the
|
||||
// signal is technically there but barely audible against
|
||||
// the loud iPhone speaker's noise floor.
|
||||
//
|
||||
// Category options unchanged \u2014 .allowBluetoothHFP +
|
||||
// .allowBluetoothA2DP permit BT headsets in both
|
||||
// directions regardless of mode.
|
||||
options: [.allowBluetoothHFP, .allowBluetoothA2DP]
|
||||
// Twilio's video-quickstart-ios and Daily.co's patched
|
||||
// WebRTC both document the same workaround: use .default
|
||||
// mode with .defaultToSpeaker option even when using
|
||||
// VPIO for AEC. The VPIO unit itself still does its job
|
||||
// (echo cancellation, noise suppression, AGC on the mic
|
||||
// path) \u2014 only the playback routing changes.
|
||||
//
|
||||
// References:
|
||||
// * https://github.com/twilio/video-quickstart-ios/issues/522
|
||||
// * https://stackoverflow.com/questions/79834998 (Daily.co)
|
||||
//
|
||||
// Options:
|
||||
// .defaultToSpeaker : route output to the main speaker
|
||||
// (not the earpiece) by default
|
||||
// when no headphones are connected.
|
||||
// This is what makes the audio
|
||||
// actually audible at normal
|
||||
// loudness.
|
||||
// .allowBluetoothHFP : permit Bluetooth Hands-Free
|
||||
// Profile headsets as both input
|
||||
// and output.
|
||||
// .allowBluetoothA2DP : permit higher-quality A2DP
|
||||
// output-only Bluetooth devices.
|
||||
options: [.defaultToSpeaker, .allowBluetoothHFP, .allowBluetoothA2DP]
|
||||
)
|
||||
NSLog("chanora_flutter: AVAudioSession category set (playAndRecord/voiceChat)")
|
||||
NSLog("chanora_flutter: AVAudioSession category set (playAndRecord/default + defaultToSpeaker)")
|
||||
} catch {
|
||||
NSLog("chanora_flutter: AVAudioSession setCategory failed: \(error)")
|
||||
}
|
||||
|
||||
@@ -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+69
|
||||
version: 1.0.0-rc.8+70
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.5
|
||||
|
||||
Reference in New Issue
Block a user