diff --git a/src/silk/PLC.rs b/src/silk/PLC.rs index 4101aca..929da54 100644 --- a/src/silk/PLC.rs +++ b/src/silk/PLC.rs @@ -18,14 +18,15 @@ use crate::silk::bwexpander::silk_bwexpander; use crate::silk::define::{ LTP_ORDER, MAX_LPC_ORDER, MAX_NB_SUBFR, TYPE_NO_VOICE_ACTIVITY, TYPE_VOICED, }; -use crate::silk::macros::{silk_CLZ32, silk_SMULBB}; +use crate::silk::macros::{silk_CLZ32, silk_SMULBB, silk_SMULWW}; use crate::silk::structs::{silk_PLC_struct, silk_decoder_control, silk_decoder_state}; use crate::silk::sum_sqr_shift::silk_sum_sqr_shift; use crate::silk::Inlines::{silk_INVERSE32_varQ, silk_SQRT_APPROX}; use crate::silk::LPC_analysis_filter::silk_LPC_analysis_filter; use crate::silk::LPC_inv_pred_gain::silk_LPC_inverse_pred_gain_c; use crate::silk::SigProc_FIX::{ - silk_RAND, silk_max_16, silk_max_32, silk_max_int, silk_min_32, silk_min_int, SILK_FIX_CONST, + silk_RAND, silk_RSHIFT_ROUND, silk_SAT16, silk_max_16, silk_max_32, silk_max_int, silk_min_32, + silk_min_int, SILK_FIX_CONST, }; pub const NB_ATT: i32 = 2; @@ -566,169 +567,18 @@ unsafe fn silk_PLC_conceal( }) as u32) << 4) as i32 }; - *frame.offset(i as isize) = (if (if (if 8 == 1 { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 1) - + ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - & 1) - } else { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 8 - 1) - + 1 - >> 1 - }) > 0x7fff - { - 0x7fff - } else { - if (if 8 == 1 { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 1) - + ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - & 1) - } else { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 8 - 1) - + 1 - >> 1 - }) < 0x8000 - { - 0x8000 - } else { - if 8 == 1 { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 1) - + ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - & 1) - } else { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 8 - 1) - + 1 - >> 1 - } - } - }) > silk_int16_MAX - { - silk_int16_MAX - } else if (if (if 8 == 1 { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 1) - + ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - & 1) - } else { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 8 - 1) - + 1 - >> 1 - }) > 0x7fff - { - 0x7fff - } else { - if (if 8 == 1 { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 1) - + ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - & 1) - } else { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 8 - 1) - + 1 - >> 1 - }) < 0x8000 - { - 0x8000 - } else { - if 8 == 1 { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 1) - + ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - & 1) - } else { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 8 - 1) - + 1 - >> 1 - } - } - }) < silk_int16_MIN - { - silk_int16_MIN - } else if (if 8 == 1 { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 1) - + ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - & 1) - } else { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 8 - 1) - + 1 - >> 1 - }) > 0x7fff - { - 0x7fff - } else if (if 8 == 1 { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 1) - + ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - & 1) - } else { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 8 - 1) - + 1 - >> 1 - }) < 0x8000 - { - 0x8000 - } else if 8 == 1 { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 1) - + ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 - * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - & 1) - } else { - ((*sLPC_Q14_ptr.offset((16 + i) as isize) as i64 * prevGain_Q10[1 as usize] as i64 - >> 16) as i32 - >> 8 - 1) - + 1 - >> 1 - }) as i16; + // Upstream C (silk/PLC.c): + // frame[i] = (opus_int16)silk_SAT16(silk_SAT16( + // silk_RSHIFT_ROUND(silk_SMULWW(sLPC_Q14_ptr[MAX_LPC_ORDER + i], prevGain_Q10[1]), 8))); + // The previous c2rust expansion of this line translated silk_SAT16's + // LOWER bound ((opus_int16)0x8000 == -32768) as the bare literal + // 0x8000 (= +32768 as i32), so every sample that was not positively + // saturated took the "clamped low" arm and each concealed frame came + // out as full-scale garbage (RMS ~1.0) on SILK/hybrid streams. + *frame.offset(i as isize) = silk_SAT16(silk_SAT16(silk_RSHIFT_ROUND( + silk_SMULWW(*sLPC_Q14_ptr.offset((16 + i) as isize), prevGain_Q10[1 as usize]), + 8, + ))) as i16; i += 1; } memcpy( diff --git a/tests/silk_plc_regression.rs b/tests/silk_plc_regression.rs new file mode 100644 index 0000000..4da8614 --- /dev/null +++ b/tests/silk_plc_regression.rs @@ -0,0 +1,98 @@ +//! Regression test for the SILK PLC saturation bug. +//! +//! `silk_SAT16`'s LOWER bound in the C source is `(opus_int16)0x8000` +//! (= -32768); the c2rust expansion in `silk/PLC.rs` rendered it as the +//! bare literal `0x8000` (= +32768 as i32), so every non-positively- +//! saturated sample took the "clamped low" arm and every concealed frame +//! on a SILK/hybrid stream came out as full-scale garbage (RMS ~ 1.0, +//! where the reference libopus conceals at a decaying fraction of the +//! signal level). +//! +//! The test decodes a short SILK-only stream (VOIP application at a +//! bitrate that forces SILK mode), then requests packet-loss concealment +//! (NULL packet) and asserts the concealed audio is a plausible decaying +//! continuation instead of saturated garbage. + +use unsafe_libopus::{ + opus_decode_float, opus_decoder_create, opus_decoder_destroy, opus_encode_float, + opus_encoder_create, opus_encoder_ctl, opus_encoder_destroy, OPUS_APPLICATION_VOIP, + OPUS_SET_BITRATE_REQUEST, +}; + +fn rms(samples: &[f32]) -> f32 { + (samples.iter().map(|s| s * s).sum::() / samples.len() as f32).sqrt() +} + +#[test] +fn silk_plc_conceals_instead_of_saturating() { + const FRAME: usize = 960; // 20 ms @ 48 kHz mono + unsafe { + let mut err = 0i32; + let enc = opus_encoder_create(48_000, 1, OPUS_APPLICATION_VOIP, &mut err); + assert!(err == 0 && !enc.is_null(), "encoder create failed: {err}"); + // Force SILK mode via a speech-range bitrate. + let ret = opus_encoder_ctl!(enc, OPUS_SET_BITRATE_REQUEST, 12000); + assert_eq!(ret, 0, "OPUS_SET_BITRATE failed: {ret}"); + + let dec = opus_decoder_create(48_000, 1, &mut err); + assert!(err == 0 && !dec.is_null(), "decoder create failed: {err}"); + + // 25 frames of a continuous-phase tone at a speech-like level. + let mut phase = 0f64; + let mut pkt = vec![0u8; 4000]; + let mut out = vec![0f32; FRAME]; + let mut last_real_rms = 0f32; + for _ in 0..25 { + let frame: Vec = (0..FRAME) + .map(|_| { + phase += 2.0 * std::f64::consts::PI * 440.0 / 48_000.0; + (phase.sin() * 0.4) as f32 + }) + .collect(); + let n = opus_encode_float(enc, frame.as_ptr(), FRAME as i32, pkt.as_mut_ptr(), 4000); + assert!(n > 0, "encode failed: {n}"); + let d = opus_decode_float(&mut *dec, pkt.as_ptr(), n, out.as_mut_ptr(), FRAME as i32, 0); + assert_eq!(d as usize, FRAME, "decode failed: {d}"); + last_real_rms = rms(&out); + } + assert!( + last_real_rms > 0.05, + "precondition: the decoded stream must carry energy (got rms {last_real_rms})" + ); + + // Three consecutive PLC calls (NULL packet = concealment). + let mut plc_rms = [0f32; 3]; + for (step, slot) in plc_rms.iter_mut().enumerate() { + let d = opus_decode_float( + &mut *dec, + core::ptr::null(), + 0, + out.as_mut_ptr(), + FRAME as i32, + 0, + ); + assert_eq!(d as usize, FRAME, "plc decode failed at step {step}: {d}"); + *slot = rms(&out); + } + opus_decoder_destroy(dec); + opus_encoder_destroy(enc); + + // Broken SAT16 lower bound: every concealed frame saturates to + // ~full scale (rms ~= 1.0 regardless of signal level). Correct + // concealment continues the signal at (at most) a comparable level + // and decays. Reference libopus on this stream conceals at + // rms ~= 0.1-0.2 decaying; 0.6 is far above any legitimate + // concealment of a 0.4-amplitude tone and far below saturation. + for (step, &r) in plc_rms.iter().enumerate() { + assert!( + r < 0.6, + "PLC step {step} saturated: rms {r} (expected a decaying \ + continuation, not full-scale garbage)" + ); + } + assert!( + plc_rms[2] <= plc_rms[0] + 0.05, + "PLC energy must not grow across consecutive concealed frames: {plc_rms:?}" + ); + } +}