craft-audio: review fixes, round 9 (#442)

Refuse a linear predictor whose prediction leaves the i32 range. Such a
stream is valid, but symphonia adds the prediction to the residual as an
i32, which panics in a debug build. The round 8 residual limit made this
reachable at 24 bits; without the override, 13 of 240 clipped 32-bit test
streams failed in a debug build and none do now, for 0.09% more bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alehel
2026-11-04 10:35:44 +01:00
co-authored by Claude Opus 5.5
parent 82f63f3a4f
commit 5617261fdc
+14 -1
View File
@@ -141,7 +141,10 @@ pub fn lpc_residual(x: &[i32], coefs: &[i32], shift: u8) -> Option<Vec<i64>> {
let hist = x.get(i - order..i)?;
// coefs[0] weighs the previous sample
let pred: i64 = coefs.iter().zip(hist.iter().rev()).map(|(&c, &s)| i64::from(c) * i64::from(s)).sum();
let r = i64::from(*x.get(i)?) - (pred >> shift);
// Valid past the i32 range too, but symphonia adds it to the residual as an i32, which
// panics in a debug build: its overflow checks are on in apps without our profile override.
let pred = i32::try_from(pred >> shift).ok()?;
let r = i64::from(*x.get(i)?) - i64::from(pred);
if r.abs() > MAX_RESIDUAL {
return None;
}
@@ -322,6 +325,16 @@ mod tests {
assert!(lpc_residual(&[0, i32::MAX], &[1], 0).is_some());
}
#[test]
fn linear_predictions_stay_in_the_range_of_a_32_bit_integer() {
let x = [1 << 30, 1 << 30];
assert!(lpc_residual(&x, &[1], 0).is_some());
// predicts 2^31, and the residual would be small
assert!(lpc_residual(&x, &[2], 0).is_none());
assert!(lpc_residual(&x, &[4], 1).is_none());
assert!(lpc_residual(&x, &[3], 1).is_some());
}
#[test]
fn wasted_bits_are_not_coded() {
let x = tones(4096);