chore: restore product scaffold to rollback baseline

This commit is contained in:
Edison Jwa
2026-05-29 14:02:04 +09:00
parent 2896f14ec9
commit fe6e07353e
434 changed files with 27278 additions and 63230 deletions
@@ -34,54 +34,32 @@ import AVFoundation
let session = AVAudioSession.sharedInstance()
try session.setCategory(
.playAndRecord,
mode: .default,
// Mode rationale (re-revisited after the "low playback
// volume" investigation, May 2026):
mode: .voiceChat,
// Mode rationale (May 2026, .voiceChat reinstated):
//
// We've cycled through .voiceChat -> .default -> .voiceChat
// -> .default. Final answer is .default with
// .defaultToSpeaker, driven by these findings:
// We previously used .default mode after discovering that
// .voiceChat routed output through iOS's in-call audio
// channel, which made speaker output barely audible. That
// bug 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 re-binds
// on overrideOutputAudioPort.
//
// 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 re-binds on
// overrideOutputAudioPort. So route switching is no
// longer a deciding factor.
// .voiceChat advantages over .default:
// * Tells iOS this is a VoIP session other apps' audio
// is properly ducked/paused instead of competing.
// * Enables correct Bluetooth HFP negotiation without
// manual workarounds.
// * iOS treats the audio session as a "call" for priority
// purposes (won't be interrupted by notification sounds).
// * System-level CallKit integration (lock-screen controls).
//
// 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
//
// 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.
//
// 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.
// .defaultToSpeaker ensures output goes to the main speaker
// (not the earpiece) by default when no headphones are
// connected, compensating for the in-call channel's tendency
// to route to the earpiece.
//
// References:
// * https://github.com/twilio/video-quickstart-ios/issues/522
@@ -91,9 +69,6 @@ import AVFoundation
// .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.
@@ -245,7 +220,7 @@ import AVFoundation
let session = AVAudioSession.sharedInstance()
try session.setCategory(
.playAndRecord,
mode: .default,
mode: .voiceChat,
options: [.defaultToSpeaker, .allowBluetoothHFP, .allowBluetoothA2DP]
)
try session.setPreferredIOBufferDuration(0.02)