diff --git a/apps/chanora_flutter/pubspec.yaml b/apps/chanora_flutter/pubspec.yaml index 2fa75ea..3d28bfe 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+68 +version: 1.0.0-rc.8+69 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 58b1772..fe6892a 100644 --- a/crates/chanora_audio/src/ios_voice_unit.rs +++ b/crates/chanora_audio/src/ios_voice_unit.rs @@ -598,18 +598,46 @@ impl IosVoiceUnit { // would otherwise clip). Multiplying by gain after // the downmix saves one multiplication per sample. // + // iOS-specific fixed output boost: AudioHandler delivers + // f32 stereo content matching what the remote client + // encoded. Most desktop TS3 clients ship audio at -30 + // to -45 dB FS (peak ~0.005-0.03), well below speaker- + // ready levels. On Linux/macOS/Windows our cpal+SDL + // paths play that level through OS audio mixers that + // apply additional system-volume amplification, so it + // reaches the user's ears at sensible loudness. iOS's + // VPIO output is NOT amplified by the system mixer — + // it goes nearly raw to the speaker, so the same -40 + // dB signal is barely audible. + // + // We compensate with a fixed 8x boost (= +18 dB) on + // top of the user-controllable output_gain. Brings a + // -40 dB signal up to -22 dB (normal speakerphone + // level) while keeping the user's UI gain slider + // functional in a useful range. Hard-clip at 1.0 + // prevents the boost from clipping legitimate loud + // signals (musicbot at peak 0.5 -> 4.0 -> clamped to + // 1.0, audible distortion only on extremely loud + // sustained content). + // + // This is consistent with how Discord / Zoom / FaceTime + // iOS clients apply an internal output normalization + // on top of the user-facing volume slider. + const IOS_OUTPUT_BOOST: f32 = 8.0; + let effective_gain = gain * IOS_OUTPUT_BOOST; + // Cast to i16 with saturate-on-overflow. Hard-clip is // acceptable here because the upstream signal is // already in [-1.0, 1.0] from the f32 stereo mix; - // gain values >1.0 are the only path to clipping and - // hard-clip at the engine boundary is what every other - // backend's i16 path does (see the cpal-side - // FromF32 for i16 impl in engine.rs). + // the IOS_OUTPUT_BOOST multiplier above is the + // primary path to clipping pressure and saturating + // at ±1.0 is the standard answer (matches the + // cpal-side FromF32 for i16 impl in engine.rs). let mut peak_out_i16: i16 = 0; for (i, dst) in out.iter_mut().enumerate() { let l = scratch_stereo[i * 2]; let r = scratch_stereo[i * 2 + 1]; - let mono_f32 = (l + r) * 0.5 * gain; + let mono_f32 = (l + r) * 0.5 * effective_gain; let clamped = mono_f32.clamp(-1.0, 1.0); let sample = (clamped * i16::MAX as f32) as i16; *dst = sample;