Merge pull request #573 from DCubix/fix/resampler-block-edge

Fix stale sample read at the end of each mix block when device and session rates differ
This commit is contained in:
Brandon Thomas
2026-10-24 01:00:42 -04:00
committed by GitHub
+22 -1
View File
@@ -362,7 +362,8 @@ impl AudioState {
let sample = |c: usize| {
let ch = self.pending.get(c);
let a = ch.and_then(|v| v.get(i0)).copied().unwrap_or(0.0);
let b = ch.and_then(|v| v.get(i0 + 1)).copied().unwrap_or(a);
// `pending` is BLOCK long; only `pending_len` frames are valid. Hold the last one at the edge.
let b = if i0 + 1 < self.pending_len { ch.and_then(|v| v.get(i0 + 1)).copied().unwrap_or(a) } else { a };
a + (b - a) * fr
};
if channels < nch {
@@ -1223,6 +1224,26 @@ mod tests {
}
}
#[test]
fn resampling_at_a_short_block_edge_does_not_mix_in_stale_frames() {
use soundcraft_model::ChannelFormat;
// DC for the first long block, then silence. The device runs slower than the session, so
// reads land between frames; a short block leaves the long block's DC past `pending_len`.
let mut st = audio_output_state(ChannelFormat::Stereo, vec![vec![0.8; BLOCK]; 2]);
st.device_rate = 44_100.0;
let mut long = vec![0.0; 2 * 470];
st.fill(&mut long, 2);
assert_eq!(st.pending_len, BLOCK);
let mut short = [0.0; 2 * 8];
for call in 0..20 {
st.fill(&mut short, 2);
assert!(st.pending_len < BLOCK);
// The first frame of the first short call still reads the DC block's last frame.
let from = if call == 0 { 2 } else { 0 };
assert!(short[from..].iter().all(|x| x.abs() < 1e-6), "call {call}: {short:?}");
}
}
#[test]
fn mono_device_preserves_centered_mono_level_and_includes_surround_right_channels() {
use soundcraft_model::ChannelFormat;