Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 15 additions & 165 deletions src/silk/PLC.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
98 changes: 98 additions & 0 deletions tests/silk_plc_regression.rs
Original file line number Diff line number Diff line change
@@ -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::<f32>() / 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<f32> = (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:?}"
);
}
}