diff --git a/apps/chanora_flutter/pubspec.yaml b/apps/chanora_flutter/pubspec.yaml index 2ca8cf0..b519afc 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+71 +version: 1.0.0-rc.8+72 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 d95397e..3705641 100644 --- a/crates/chanora_audio/src/ios_voice_unit.rs +++ b/crates/chanora_audio/src/ios_voice_unit.rs @@ -488,6 +488,13 @@ 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 counters (per external review pointing out + // that "peak alone is insufficient" \u2014 we also need + // RMS, clip count, underrun count, callback frame size + // variability). Sampled every 100 callbacks (~2 s). + let mut cb_count: u64 = 0; + let mut last_num_frames: usize = 0; + let mut num_frames_changes: u32 = 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 @@ -555,6 +562,75 @@ impl IosVoiceUnit { let clamped = mono_f32.clamp(-1.0, 1.0); *dst = (clamped * i16::MAX as f32) as i16; } + + // Comprehensive callback diagnostic (per external + // review). Reports: + // * num_frames : VPIO buffer size this call. + // Should be stable; transitions + // indicate iOS re-negotiating. + // * frames_change_cnt : count of times num_frames + // changed across callbacks. + // Non-trivial = iOS jitter. + // * peak_stereo_f32 : peak |sample| of AudioHandler + // output before downmix. + // * rms_stereo_f32 : RMS over the scratch buffer. + // Gives loudness perception not + // just transient peaks. + // * peak_out_i16 : peak |sample| handed to VPIO. + // * clip_count_i16 : samples at \u00b132767 (digital + // clipping). Non-zero with our + // gain=1.0 indicates upstream + // is already at full scale. + // * input_was_zero : true if AudioHandler returned + // silence (jitter underrun or + // no talker). Distinguishes + // "no audio to play" from + // "audio reached us but + // broken". + // * gain : current master output gain. + if last_num_frames != 0 && last_num_frames != num_frames { + num_frames_changes = num_frames_changes.wrapping_add(1); + } + last_num_frames = num_frames; + cb_count = cb_count.wrapping_add(1); + if cb_count.is_multiple_of(100) { + // Scratch peak + RMS. + let mut peak_stereo: f32 = 0.0; + let mut sumsq: f64 = 0.0; + for &s in scratch_stereo[..needed].iter() { + let a = s.abs(); + if a > peak_stereo { + peak_stereo = a; + } + sumsq += (s as f64) * (s as f64); + } + let rms_stereo = (sumsq / needed as f64).sqrt() as f32; + // Output peak + clip count. + let mut peak_out: i16 = 0; + let mut clipped: u32 = 0; + for &s in out.iter() { + let a = s.unsigned_abs() as i16; + if a > peak_out { + peak_out = a; + } + if s == i16::MAX || s == i16::MIN || s == -i16::MAX { + clipped += 1; + } + } + info!( + target: "chanora_audio", + cb = cb_count, + num_frames, + frames_changes = num_frames_changes, + peak_stereo = peak_stereo, + rms_stereo = rms_stereo, + peak_out_i16 = peak_out, + clip_count_i16 = clipped, + input_was_zero = peak_stereo == 0.0, + gain, + "ios VPIO render callback diagnostic sample" + ); + } Ok(()) }) .map_err(|e| AudioError::Backend(format!("vpio set render callback: {e}")))?;