diag(audio,ios): comprehensive render-callback metrics per external review (rc.8+72)
External code review pushed back on the 'iPhone speaker hardware
distortion' hypothesis and pointed out we need more than just
peak measurements. The reviewer's checklist:
* peak_i16
* rms_i16
* num_clipped_samples (abs >= 32767)
* zero_fill_count / underrun_count
* callback_frame_count variability
* decoded_packet_duration_ms
* input_was_zero
* actual ASBD / actual sample rate
The format diagnostics at +71 already showed iOS honoured
48 kHz Int16 mono on both buses and that .default mode +
.defaultToSpeaker routed to the speaker correctly with
outputVolume=0.45. So format + route are confirmed correct.
The remaining mystery is WHY 'loud but distorted' \u2014 we need
sample-level metrics to isolate where in the pipeline the
breakage occurs.
This commit instruments the VPIO render callback with:
* num_frames + frames_changes : detects iOS re-negotiating
buffer size between callbacks
(which would imply jitter the
fixed scratch_stereo Vec can't
absorb cleanly).
* peak_stereo + rms_stereo : characterises AudioHandler's
output BEFORE our downmix.
Distinguishes 'real audio
arriving' from 'silence'.
* peak_out_i16 + clip_count : measures what we hand VPIO.
clip_count > 0 means we're
clipping at our boundary even
with gain=1.0 \u2014 indicates
upstream is over-driven.
* input_was_zero : explicit silence/no-talker
indicator separate from peak=0
which could mean tiny content
rounded to 0.
Reviewer's preferred diagnostic path is to dump PCM to file
and play with ffplay externally; that's iOS-impractical
without a shared filesystem path the user can extract via
Files.app. Instead we sample the same metrics in-callback at
~2 Hz which gives us the same information at run time.
Pure diagnostic. No behavioural change. Counters live in the
FnMut closure so the audio thread cost is one branch +
counter increment per callback, plus a one-pass RMS sum +
peak scan every 100 callbacks.
Build counter 71 -> 72.
Reviewer also recommended a headphone test in parallel \u2014
that will be done by the user (out-of-band) at the next
test cycle to determine whether the symptom changes when
audio leaves the speaker path.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<data::Interleaved<i16>>| {
|
||||
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}")))?;
|
||||
|
||||
Reference in New Issue
Block a user