diff --git a/apps/chanora_flutter/pubspec.yaml b/apps/chanora_flutter/pubspec.yaml index 924086f..682ee51 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+65 +version: 1.0.0-rc.8+66 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 931bd58..361c0e2 100644 --- a/crates/chanora_audio/src/ios_voice_unit.rs +++ b/crates/chanora_audio/src/ios_voice_unit.rs @@ -478,6 +478,15 @@ impl IosVoiceUnit { let handler_for_render = handler.clone(); let output_gain_for_render = output_gain.clone(); let output_muted_for_render = output_muted.clone(); + // Diagnostic: count render-callback invocations and log + // a sampled line every ~100 callbacks (= ~2 seconds @ a + // typical iOS 20-50 Hz callback rate). Lets us correlate + // the user-perceived audio quality with what's actually + // flowing through the callback in realtime: how big the + // VPIO buffer is, whether AudioHandler is returning + // actual content or silence, what the peak / RMS of the + // signal looks like at each pipeline stage. + let mut cb_count: u64 = 0; unit.set_render_callback(move |args: render_callback::Args>| { let out: &mut [i16] = args.data.buffer; // VPIO with our pinned stream format gives us a mono @@ -505,6 +514,17 @@ impl IosVoiceUnit { // cpal output behaviour). } + // Compute scratch-stereo peak BEFORE applying gain or + // downmix so the diagnostic line reflects what + // AudioHandler actually produced. + let mut peak_stereo_f32: f32 = 0.0; + for &s in scratch_stereo[..needed].iter() { + let a = s.abs(); + if a > peak_stereo_f32 { + peak_stereo_f32 = a; + } + } + if output_muted_for_render.load(Ordering::Relaxed) { for s in out.iter_mut() { *s = 0; @@ -526,12 +546,52 @@ impl IosVoiceUnit { // 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). + 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 clamped = mono_f32.clamp(-1.0, 1.0); - *dst = (clamped * i16::MAX as f32) as i16; + let sample = (clamped * i16::MAX as f32) as i16; + *dst = sample; + let a = sample.unsigned_abs() as i16; + if a > peak_out_i16 { + peak_out_i16 = a; + } + } + + // Diagnostic sample: log once every 100 callbacks + // (~2 s) so we can see what the pipeline is producing + // when the user reports "broken" audio. The fields + // tell us: + // * num_frames — how big each VPIO callback is. + // Should be ~960 (20 ms) or ~1104 + // (23 ms) at 48 kHz; wildly off + // values point at format mismatch. + // * peak_stereo — peak f32 absolute value of the + // AudioHandler output. Zero = + // handler is producing silence + // (jitter-buffer underrun or no + // remote audio). Non-zero = + // decoded content is reaching the + // callback. + // * peak_out — peak i16 absolute value of the + // downmixed mono signal we hand + // VPIO. Zero with non-zero + // peak_stereo would indicate the + // downmix is broken. + // * gain — current master output gain. + cb_count = cb_count.wrapping_add(1); + if cb_count.is_multiple_of(100) { + info!( + target: "chanora_audio", + cb = cb_count, + num_frames, + peak_stereo_f32 = peak_stereo_f32, + peak_out_i16, + gain, + "ios VPIO render callback diagnostic sample" + ); } Ok(()) })