diag(audio,ios): instrument VPIO render callback to isolate playback breakage (rc.8+66)

User reports capture-side audio (mic -> remote) is clean but
local playback (remote -> speaker via VPIO render callback) is
'broken and poor' at +65. The pipeline appears correct on paper:
fill_buffer -> downmix (L+R)*0.5 -> gain -> clamp -> i16 -> VPIO.
No errors logged. To stop guessing, add structured logging
inside the render callback so the next test cycle yields data
about what's actually flowing through.

Diagnostic emitted every 100th callback (~2 s at iOS's typical
20-50 Hz callback rate):

  ios VPIO render callback diagnostic sample
    cb=<counter>
    num_frames=<N>           VPIO buffer size in mono samples.
                             Expected ~960 (20ms) or ~1104 (23ms).
                             Outliers point at format mismatch.
    peak_stereo_f32=<f32>    Peak |sample| of AudioHandler's
                             output BEFORE gain + downmix.
                             0.0 = handler is producing silence
                                   (jitter underrun, no audio).
                             ~1.0 = full-scale content reaching
                                    the callback as expected.
    peak_out_i16=<i16>       Peak |sample| of the downmixed mono
                             i16 we write to VPIO. Zero with
                             non-zero peak_stereo = downmix bug.
                             Near 32767 = clipping pressure.
    gain=<f32>               Current master output gain.

What we'll be able to diagnose from a 5-second talker session:

* peak_stereo_f32 = 0 throughout
    -> AudioHandler isn't producing samples. Inbound forwarder
       may not be feeding it, or jitter buffer is stuck in
       buffering_samples state. NOT a render-callback bug.

* peak_stereo_f32 oscillating, peak_out_i16 = 0
    -> Downmix or i16 cast is broken. Math bug in the loop.

* num_frames wildly different from ~960-1104
    -> StreamFormat got rejected and VPIO is delivering a
       different rate. Format-pinning fight with the session.

* peak_stereo_f32 normal AND peak_out_i16 normal AND user
  still says 'broken'
    -> The signal reaches the device cleanly but iOS's VPIO
       output processing (AEC residual subtraction, AGC
       compression, NS gate) is mangling it after our callback
       returns. That's a VPIO-config problem, not a render-
       callback problem; fix is to disable specific VPIO
       voice-processing properties on the unit before
       initialize().

Pure diagnostic commit. No behavioural change beyond a
warn-rate-limited info log line every ~2 seconds. Cost in the
audio thread is one branch + counter increment + (every 100th)
a tracing macro invocation.

Build counter 65 -> 66.
This commit is contained in:
EdisonJwa
2026-05-17 01:26:15 +08:00
parent ecf9370db1
commit 63cbab901e
2 changed files with 62 additions and 2 deletions
+1 -1
View File
@@ -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
+61 -1
View File
@@ -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<data::Interleaved<i16>>| {
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(())
})