#![allow(dead_code)]
use crate::celt::math::{celt_sqrt, frac_div32};
use crate::celt::types::{CeltSig, OpusVal16, OpusVal32};
use crate::celt::{celt_autocorr, celt_lpc, celt_udiv};
#[cfg(feature = "fixed_point")]
use crate::celt::{celt_autocorr_fixed, celt_lpc_fixed};
use alloc::vec;
use core::cmp::min;
#[cfg(test)]
extern crate std;
#[cfg(feature = "fixed_point")]
use core::cmp::{max, min as min_i32};
#[cfg(feature = "fixed_point")]
use crate::celt::fixed_arch::{Q15_ONE, SIG_SHIFT};
#[cfg(feature = "fixed_point")]
use crate::celt::fixed_ops::{
extract16, mac16_16, mult16_16, mult16_16_q15, mult16_32_q15, pshr32, qconst16, shl32, shr32,
vshr32,
};
#[cfg(feature = "fixed_point")]
use crate::celt::math::celt_ilog2;
#[cfg(feature = "fixed_point")]
use crate::celt::math_fixed::frac_div32 as frac_div32_fixed;
#[cfg(feature = "fixed_point")]
use crate::celt::math_fixed::{celt_maxabs16, celt_maxabs32, celt_rsqrt_norm};
#[cfg(feature = "fixed_point")]
use crate::celt::types::{FixedCeltSig, FixedOpusVal16, FixedOpusVal32};
#[cfg(test)]
mod remove_doubling_trace {
extern crate std;
use core::sync::atomic::{AtomicUsize, Ordering};
use std::env;
use std::sync::OnceLock;
static FRAME_INDEX: AtomicUsize = AtomicUsize::new(usize::MAX);
static TARGET_CALL: OnceLock<Option<usize>> = OnceLock::new();
static ENABLED: OnceLock<bool> = OnceLock::new();
pub(crate) fn set_frame(frame_idx: Option<usize>) {
FRAME_INDEX.store(frame_idx.unwrap_or(usize::MAX), Ordering::Relaxed);
}
pub(crate) fn current_frame() -> Option<usize> {
let value = FRAME_INDEX.load(Ordering::Relaxed);
if value == usize::MAX {
None
} else {
Some(value)
}
}
pub(crate) fn should_trace() -> bool {
let enabled = *ENABLED.get_or_init(|| {
env::var("CELT_TRACE_REMOVE_DOUBLING")
.map(|value| !value.is_empty() && value != "0")
.unwrap_or(false)
});
let target = *TARGET_CALL.get_or_init(|| {
env::var("CELT_TRACE_REMOVE_DOUBLING_FRAME")
.ok()
.and_then(|value| value.parse::<usize>().ok())
});
if let Some(target) = target {
FRAME_INDEX.load(Ordering::Relaxed) == target
} else {
enabled
}
}
}
#[cfg(test)]
pub(crate) fn remove_doubling_trace_set_frame(frame_idx: Option<usize>) {
remove_doubling_trace::set_frame(frame_idx);
}
#[cfg(not(test))]
pub(crate) fn remove_doubling_trace_set_frame(_frame_idx: Option<usize>) {}
pub(crate) fn find_best_pitch(
xcorr: &[OpusVal32],
y: &[OpusVal16],
len: usize,
max_pitch: usize,
best_pitch: &mut [i32; 2],
) {
assert!(
xcorr.len() >= max_pitch,
"xcorr must contain max_pitch elements"
);
assert!(
y.len() >= len + max_pitch,
"y must contain len + max_pitch samples to slide the energy window",
);
let mut syy: OpusVal32 = 1.0;
for &sample in &y[..len] {
syy += sample * sample;
}
let mut best_num = [-1.0, -1.0];
let mut best_den = [0.0, 0.0];
best_pitch[0] = 0;
best_pitch[1] = if max_pitch > 1 { 1 } else { 0 };
for (i, &corr) in xcorr.iter().enumerate().take(max_pitch) {
if corr > 0.0 {
let mut corr16 = corr;
corr16 *= 1e-12;
let num = corr16 * corr16;
if num * best_den[1] > best_num[1] * syy {
if num * best_den[0] > best_num[0] * syy {
best_num[1] = best_num[0];
best_den[1] = best_den[0];
best_pitch[1] = best_pitch[0];
best_num[0] = num;
best_den[0] = syy;
best_pitch[0] = i as i32;
} else {
best_num[1] = num;
best_den[1] = syy;
best_pitch[1] = i as i32;
}
}
}
let entering = y[i + len];
let leaving = y[i];
syy += entering * entering - leaving * leaving;
if syy < 1.0 {
syy = 1.0;
}
}
}
pub(crate) fn celt_inner_prod(x: &[OpusVal16], y: &[OpusVal16]) -> OpusVal32 {
assert_eq!(
x.len(),
y.len(),
"vectors provided to celt_inner_prod must have the same length",
);
let mut sum = 0.0;
for (&a, &b) in x.iter().zip(y.iter()) {
sum += a * b;
}
sum
}
#[cfg(feature = "fixed_point")]
pub(crate) fn celt_inner_prod_fixed(x: &[FixedOpusVal16], y: &[FixedOpusVal16]) -> FixedOpusVal32 {
assert_eq!(
x.len(),
y.len(),
"vectors provided to celt_inner_prod_fixed must have the same length",
);
let mut sum = 0i32;
for (&a, &b) in x.iter().zip(y.iter()) {
sum = mac16_16(sum, a, b);
}
sum
}
pub(crate) fn dual_inner_prod(
x: &[OpusVal16],
y0: &[OpusVal16],
y1: &[OpusVal16],
) -> (OpusVal32, OpusVal32) {
assert!(
x.len() == y0.len() && x.len() == y1.len(),
"dual_inner_prod inputs must have the same length"
);
let mut xy0 = 0.0;
let mut xy1 = 0.0;
for ((&a, &b0), &b1) in x.iter().zip(y0.iter()).zip(y1.iter()) {
xy0 += a * b0;
xy1 += a * b1;
}
(xy0, xy1)
}
#[cfg(feature = "fixed_point")]
pub(crate) fn dual_inner_prod_fixed(
x: &[FixedOpusVal16],
y0: &[FixedOpusVal16],
y1: &[FixedOpusVal16],
) -> (FixedOpusVal32, FixedOpusVal32) {
assert!(
x.len() == y0.len() && x.len() == y1.len(),
"dual_inner_prod_fixed inputs must have the same length"
);
let mut xy0 = 0i32;
let mut xy1 = 0i32;
for ((&a, &b0), &b1) in x.iter().zip(y0.iter()).zip(y1.iter()) {
xy0 = mac16_16(xy0, a, b0);
xy1 = mac16_16(xy1, a, b1);
}
(xy0, xy1)
}
pub(crate) fn xcorr_kernel(x: &[OpusVal16], y: &[OpusVal16], sum: &mut [OpusVal32; 4], len: usize) {
assert!(len >= 3, "xcorr_kernel requires at least three samples");
assert!(x.len() >= len, "xcorr_kernel needs len samples from x");
assert!(
y.len() >= len + 3,
"xcorr_kernel needs len + 3 samples from y"
);
let mut y_index = 0usize;
let mut y0 = y[y_index];
y_index += 1;
let mut y1 = y[y_index];
y_index += 1;
let mut y2 = y[y_index];
y_index += 1;
let mut y3 = 0.0;
let mut j = 0usize;
while j + 3 < len {
let tmp0 = x[j];
y3 = y[y_index];
y_index += 1;
sum[0] += tmp0 * y0;
sum[1] += tmp0 * y1;
sum[2] += tmp0 * y2;
sum[3] += tmp0 * y3;
let tmp1 = x[j + 1];
y0 = y[y_index];
y_index += 1;
sum[0] += tmp1 * y1;
sum[1] += tmp1 * y2;
sum[2] += tmp1 * y3;
sum[3] += tmp1 * y0;
let tmp2 = x[j + 2];
y1 = y[y_index];
y_index += 1;
sum[0] += tmp2 * y2;
sum[1] += tmp2 * y3;
sum[2] += tmp2 * y0;
sum[3] += tmp2 * y1;
let tmp3 = x[j + 3];
y2 = y[y_index];
y_index += 1;
sum[0] += tmp3 * y3;
sum[1] += tmp3 * y0;
sum[2] += tmp3 * y1;
sum[3] += tmp3 * y2;
j += 4;
}
if j < len {
let tmp = x[j];
y3 = y[y_index];
y_index += 1;
sum[0] += tmp * y0;
sum[1] += tmp * y1;
sum[2] += tmp * y2;
sum[3] += tmp * y3;
j += 1;
}
if j < len {
let tmp = x[j];
y0 = y[y_index];
y_index += 1;
sum[0] += tmp * y1;
sum[1] += tmp * y2;
sum[2] += tmp * y3;
sum[3] += tmp * y0;
j += 1;
}
if j < len {
let tmp = x[j];
y1 = y[y_index];
sum[0] += tmp * y2;
sum[1] += tmp * y3;
sum[2] += tmp * y0;
sum[3] += tmp * y1;
}
}
#[inline]
pub(crate) fn compute_pitch_gain(xy: OpusVal32, xx: OpusVal32, yy: OpusVal32) -> OpusVal16 {
(xy / celt_sqrt(1.0 + xx * yy)) as OpusVal16
}
pub(crate) fn celt_pitch_xcorr(
x: &[OpusVal16],
y: &[OpusVal16],
len: usize,
max_pitch: usize,
xcorr: &mut [OpusVal32],
) {
assert!(x.len() >= len, "input x must provide at least len samples");
assert!(
y.len() >= len + max_pitch.saturating_sub(1),
"input y must provide len + max_pitch - 1 samples"
);
assert!(
xcorr.len() >= max_pitch,
"output buffer must store max_pitch correlation values"
);
let x_head = &x[..len];
let mut i = 0usize;
while i + 3 < max_pitch {
let mut sum = [0.0; 4];
xcorr_kernel(x_head, &y[i..], &mut sum, len);
xcorr[i] = sum[0];
xcorr[i + 1] = sum[1];
xcorr[i + 2] = sum[2];
xcorr[i + 3] = sum[3];
i += 4;
}
for delay in i..max_pitch {
let y_window = &y[delay..delay + len];
xcorr[delay] = celt_inner_prod(x_head, y_window);
}
}
#[cfg(feature = "fixed_point")]
pub(crate) fn celt_pitch_xcorr_fixed(
x: &[FixedOpusVal16],
y: &[FixedOpusVal16],
len: usize,
max_pitch: usize,
xcorr: &mut [FixedOpusVal32],
) -> FixedOpusVal32 {
assert!(x.len() >= len, "input x must provide at least len samples");
assert!(
y.len() >= len + max_pitch.saturating_sub(1),
"input y must provide len + max_pitch - 1 samples"
);
assert!(
xcorr.len() >= max_pitch,
"output buffer must store max_pitch correlation values"
);
let mut maxcorr = 1i32;
for (delay, slot) in xcorr.iter_mut().enumerate().take(max_pitch) {
let mut sum = 0i32;
for j in 0..len {
sum = mac16_16(sum, x[j], y[delay + j]);
}
*slot = sum;
maxcorr = maxcorr.max(sum);
}
maxcorr
}
#[cfg(feature = "fixed_point")]
pub(crate) fn find_best_pitch_fixed(
xcorr: &[FixedOpusVal32],
y: &[FixedOpusVal16],
len: usize,
max_pitch: usize,
best_pitch: &mut [i32; 2],
yshift: i32,
maxcorr: FixedOpusVal32,
) {
assert!(
xcorr.len() >= max_pitch,
"xcorr must contain max_pitch elements"
);
assert!(
y.len() >= len + max_pitch,
"y must contain len + max_pitch samples to slide the energy window",
);
let xshift = celt_ilog2(maxcorr) - 14;
let mut syy: FixedOpusVal32 = 1;
for &sample in &y[..len] {
syy = syy.wrapping_add(shr32(mult16_16(sample, sample), yshift as u32));
}
let mut best_num = [-1i16, -1i16];
let mut best_den = [0i32, 0i32];
best_pitch[0] = 0;
best_pitch[1] = if max_pitch > 1 { 1 } else { 0 };
for (i, &corr) in xcorr.iter().enumerate().take(max_pitch) {
if corr > 0 {
let xcorr16 = extract16(vshr32(corr, xshift));
let num = mult16_16_q15(xcorr16, xcorr16);
if mult16_32_q15(num, best_den[1]) > mult16_32_q15(best_num[1], syy) {
if mult16_32_q15(num, best_den[0]) > mult16_32_q15(best_num[0], syy) {
best_num[1] = best_num[0];
best_den[1] = best_den[0];
best_pitch[1] = best_pitch[0];
best_num[0] = num;
best_den[0] = syy;
best_pitch[0] = i as i32;
} else {
best_num[1] = num;
best_den[1] = syy;
best_pitch[1] = i as i32;
}
}
}
let entering = y[i + len];
let leaving = y[i];
syy = syy
.wrapping_add(shr32(mult16_16(entering, entering), yshift as u32))
.wrapping_sub(shr32(mult16_16(leaving, leaving), yshift as u32));
syy = max(1, syy);
}
}
#[cfg(feature = "fixed_point")]
pub(crate) fn compute_pitch_gain_fixed(
xy: FixedOpusVal32,
xx: FixedOpusVal32,
yy: FixedOpusVal32,
) -> FixedOpusVal16 {
if xy == 0 || xx == 0 || yy == 0 {
return 0;
}
let sx = celt_ilog2(xx) - 14;
let sy = celt_ilog2(yy) - 14;
let mut shift = sx + sy;
let mut x2y2 = shr32(
mult16_16(extract16(vshr32(xx, sx)), extract16(vshr32(yy, sy))),
14,
);
if shift & 1 != 0 {
if x2y2 < 32_768 {
x2y2 <<= 1;
shift -= 1;
} else {
x2y2 >>= 1;
shift += 1;
}
}
let den = celt_rsqrt_norm(x2y2);
let mut g = mult16_32_q15(den, xy);
g = vshr32(g, (shift >> 1) - 1);
extract16(max(-i32::from(Q15_ONE), min_i32(g, i32::from(Q15_ONE))))
}
pub(crate) fn pitch_search(
x_lp: &[OpusVal16],
y: &[OpusVal16],
len: usize,
max_pitch: usize,
_arch: i32,
) -> i32 {
assert!(len > 0, "pitch_search requires a non-empty target length");
assert!(
max_pitch > 0,
"pitch_search requires a positive search span"
);
let len_half = len >> 1;
assert!(
x_lp.len() >= len_half,
"x_lp must provide at least len / 2 samples",
);
let lag = len + max_pitch;
let max_pitch_half = max_pitch >> 1;
assert!(
y.len() >= len_half + max_pitch_half,
"y must contain at least len / 2 + max_pitch / 2 samples",
);
let len_quarter = len >> 2;
let lag_quarter = lag >> 2;
let max_pitch_quarter = max_pitch >> 2;
let mut best_pitch = [0i32, 0i32];
if len_quarter > 0 && max_pitch_quarter > 0 {
let mut x_lp4 = vec![0.0; len_quarter];
for (j, slot) in x_lp4.iter_mut().enumerate() {
*slot = x_lp[2 * j];
}
let mut y_lp4 = vec![0.0; lag_quarter];
for (j, slot) in y_lp4.iter_mut().enumerate() {
*slot = y[2 * j];
}
let mut xcorr = vec![0.0; max_pitch_quarter];
celt_pitch_xcorr(&x_lp4, &y_lp4, len_quarter, max_pitch_quarter, &mut xcorr);
let y_needed = min(y_lp4.len(), len_quarter + max_pitch_quarter);
find_best_pitch(
&xcorr,
&y_lp4[..y_needed],
len_quarter,
max_pitch_quarter,
&mut best_pitch,
);
}
let mut xcorr = vec![0.0; max_pitch_half.max(1)];
if max_pitch_half > 0 {
let len_half = len >> 1;
if len_half > 0 {
for (i, slot) in xcorr.iter_mut().enumerate().take(max_pitch_half) {
if (i as i32 - 2 * best_pitch[0]).abs() > 2
&& (i as i32 - 2 * best_pitch[1]).abs() > 2
{
continue;
}
let start = i;
let end = start + len_half;
if end > y.len() {
break;
}
let sum: OpusVal32 = x_lp[..len_half]
.iter()
.zip(&y[start..end])
.map(|(&a, &b)| a * b)
.sum();
*slot = sum.max(-1.0);
}
let y_needed = min(y.len(), len_half + max_pitch_half);
find_best_pitch(
&xcorr[..max_pitch_half],
&y[..y_needed],
len_half,
max_pitch_half,
&mut best_pitch,
);
if best_pitch[0] > 0 && (best_pitch[0] as usize) < max_pitch_half - 1 {
let a = xcorr[(best_pitch[0] - 1) as usize];
let b = xcorr[best_pitch[0] as usize];
let c = xcorr[(best_pitch[0] + 1) as usize];
let mut offset = 0;
if (c - a) > 0.7 * (b - a) {
offset = 1;
} else if (a - c) > 0.7 * (b - c) {
offset = -1;
}
return 2 * best_pitch[0] - offset;
}
}
}
2 * best_pitch[0]
}
#[cfg(feature = "fixed_point")]
pub(crate) fn pitch_search_fixed(
x_lp: &[FixedOpusVal16],
y: &[FixedOpusVal16],
len: usize,
max_pitch: usize,
_arch: i32,
) -> i32 {
assert!(
len > 0,
"pitch_search_fixed requires a non-empty target length"
);
assert!(
max_pitch > 0,
"pitch_search_fixed requires a positive search span"
);
let lag = len + max_pitch;
let len_quarter = len >> 2;
let lag_quarter = lag >> 2;
let max_pitch_quarter = max_pitch >> 2;
let mut best_pitch = [0i32, 0i32];
let mut shift = 0i32;
if len_quarter > 0 && max_pitch_quarter > 0 {
let mut x_lp4 = vec![0i16; len_quarter];
for (j, slot) in x_lp4.iter_mut().enumerate() {
*slot = x_lp[2 * j];
}
let mut y_lp4 = vec![0i16; lag_quarter];
for (j, slot) in y_lp4.iter_mut().enumerate() {
*slot = y[2 * j];
}
let xmax = celt_maxabs16(&x_lp4);
let ymax = celt_maxabs16(&y_lp4);
shift = celt_ilog2(max(1, max(xmax, ymax))) - 11;
if shift > 0 {
let shift_u = shift as u32;
for sample in x_lp4.iter_mut() {
*sample = (*sample >> shift_u) as i16;
}
for sample in y_lp4.iter_mut() {
*sample = (*sample >> shift_u) as i16;
}
shift *= 2;
} else {
shift = 0;
}
let mut xcorr = vec![0i32; max_pitch_quarter];
let maxcorr =
celt_pitch_xcorr_fixed(&x_lp4, &y_lp4, len_quarter, max_pitch_quarter, &mut xcorr);
find_best_pitch_fixed(
&xcorr,
&y_lp4,
len_quarter,
max_pitch_quarter,
&mut best_pitch,
0,
maxcorr,
);
}
let max_pitch_half = max_pitch >> 1;
if max_pitch_half == 0 {
return 2 * best_pitch[0];
}
let len_half = len >> 1;
let mut xcorr = vec![0i32; max_pitch_half];
if len_half > 0 {
let mut maxcorr = 1i32;
for (i, slot) in xcorr.iter_mut().enumerate() {
if (i as i32 - 2 * best_pitch[0]).abs() > 2 && (i as i32 - 2 * best_pitch[1]).abs() > 2
{
continue;
}
let mut sum = 0i32;
for j in 0..len_half {
sum = sum.wrapping_add(shr32(mult16_16(x_lp[j], y[i + j]), shift as u32));
}
if sum < -1 {
sum = -1;
}
*slot = sum;
maxcorr = max(maxcorr, sum);
}
find_best_pitch_fixed(
&xcorr,
y,
len_half,
max_pitch_half,
&mut best_pitch,
shift + 1,
maxcorr,
);
}
let mut offset = 0;
if best_pitch[0] > 0 && (best_pitch[0] as usize) < max_pitch_half - 1 {
let a = xcorr[(best_pitch[0] - 1) as usize];
let b = xcorr[best_pitch[0] as usize];
let c = xcorr[(best_pitch[0] + 1) as usize];
if (c - a) > mult16_32_q15(qconst16(0.7, 15), b - a) {
offset = 1;
} else if (a - c) > mult16_32_q15(qconst16(0.7, 15), b - c) {
offset = -1;
}
}
2 * best_pitch[0] - offset
}
const SECOND_CHECK: [i32; 16] = [0, 0, 3, 2, 3, 2, 5, 2, 3, 2, 3, 2, 5, 2, 3, 2];
fn window(data: &[OpusVal16], center: usize, offset: isize, len: usize) -> &[OpusVal16] {
let start = center as isize + offset;
assert!(start >= 0, "window would start before the buffer");
let start = start as usize;
let end = start + len;
assert!(end <= data.len(), "window extends beyond the buffer");
&data[start..end]
}
#[cfg(feature = "fixed_point")]
fn window_fixed(
data: &[FixedOpusVal16],
center: usize,
offset: isize,
len: usize,
) -> &[FixedOpusVal16] {
let start = center as isize + offset;
assert!(start >= 0, "window would start before the buffer");
let start = start as usize;
let end = start + len;
assert!(end <= data.len(), "window extends beyond the buffer");
&data[start..end]
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn remove_doubling(
x: &[OpusVal16],
maxperiod: usize,
minperiod: usize,
n: usize,
t0: &mut i32,
prev_period: i32,
prev_gain: OpusVal16,
_arch: i32,
) -> OpusVal16 {
assert!(maxperiod > 0, "maxperiod must be positive");
assert!(minperiod > 0, "minperiod must be positive");
assert!(n > 0, "window size must be positive");
#[cfg(test)]
let trace = remove_doubling_trace::should_trace();
let maxperiod_half = maxperiod >> 1;
let n_half = n >> 1;
assert!(
x.len() >= maxperiod_half + n_half,
"x must contain at least maxperiod / 2 + n / 2 samples",
);
let minperiod0 = minperiod as i32;
let minperiod_half = minperiod >> 1;
let t0_half = (*t0 >> 1).clamp(0, maxperiod_half.saturating_sub(1) as i32);
let prev_period_half = prev_period >> 1;
#[cfg(test)]
if trace {
crate::test_trace::trace_println!(
"celt_remove_doubling.t0_half={t0_half} minperiod_half={minperiod_half} maxperiod_half={maxperiod_half} n_half={n_half} prev_period_half={prev_period_half}"
);
}
if maxperiod_half <= 1 || n_half == 0 {
*t0 = (*t0).max(minperiod0);
return prev_gain;
}
let center = maxperiod_half;
assert!(
center + n_half <= x.len(),
"insufficient samples for windowed view"
);
let x_center = window(x, center, 0, n_half);
let x_t0 = window(x, center, -(t0_half as isize), n_half);
let (xx, xy) = dual_inner_prod(x_center, x_center, x_t0);
let mut yy_lookup = vec![0.0; maxperiod_half + 1];
yy_lookup[0] = xx;
let mut yy = xx;
for i in 1..=maxperiod_half {
let prev_sample = x[center - i];
let enter_sample = x[center + n_half - i];
yy += prev_sample * prev_sample;
yy -= enter_sample * enter_sample;
yy_lookup[i] = yy.max(0.0);
}
yy = yy_lookup[t0_half as usize];
let mut best_xy = xy;
let mut best_yy = yy;
let mut g = compute_pitch_gain(xy, xx, yy);
let g0 = g;
#[cfg(test)]
if trace {
crate::test_trace::trace_println!(
"celt_remove_doubling.xx={:.9e} xy={:.9e} yy={:.9e} g0={:.9e}",
xx,
xy,
yy,
g0
);
if std::env::var("CELT_TRACE_REMOVE_DOUBLING_BITS")
.map(|value| !value.is_empty() && value != "0")
.unwrap_or(false)
{
crate::test_trace::trace_println!(
"celt_remove_doubling.xx_bits=0x{:08x}",
xx.to_bits()
);
crate::test_trace::trace_println!(
"celt_remove_doubling.xy_bits=0x{:08x}",
xy.to_bits()
);
crate::test_trace::trace_println!(
"celt_remove_doubling.yy_bits=0x{:08x}",
yy.to_bits()
);
crate::test_trace::trace_println!(
"celt_remove_doubling.g0_bits=0x{:08x}",
g0.to_bits()
);
}
}
let max_allowed = maxperiod_half.saturating_sub(1) as i32;
let mut t = if max_allowed >= 1 {
t0_half.clamp(1, max_allowed)
} else {
0
};
for k in 2..=15 {
let t1 = celt_udiv((2 * t0_half + k) as u32, (2 * k) as u32) as i32;
if t1 < minperiod_half as i32 {
break;
}
if t1 as usize > maxperiod_half {
continue;
}
let t1b = if k == 2 {
if t1 + t0_half > maxperiod_half as i32 {
t0_half
} else {
t0_half + t1
}
} else {
let check = SECOND_CHECK[k as usize];
celt_udiv((2 * check * t0_half + k) as u32, (2 * k) as u32) as i32
};
if t1b as usize > maxperiod_half {
continue;
}
let x_t1 = window(x, center, -(t1 as isize), n_half);
let x_t1b = window(x, center, -(t1b as isize), n_half);
let (mut xy1, xy2) = dual_inner_prod(x_center, x_t1, x_t1b);
xy1 = 0.5 * (xy1 + xy2);
let yy1 = 0.5 * (yy_lookup[t1 as usize] + yy_lookup[t1b as usize]);
let g1 = compute_pitch_gain(xy1, xx, yy1);
let diff = (t1 - prev_period_half).abs();
let cont = if diff <= 1 {
prev_gain
} else if diff <= 2 && 5 * (k * k) < t0_half {
0.5 * prev_gain
} else {
0.0
};
let mut thresh = (0.7 * g0 - cont).max(0.3);
if t1 < 3 * minperiod_half as i32 {
thresh = (0.85 * g0 - cont).max(0.4);
} else if t1 < 2 * minperiod_half as i32 {
thresh = (0.9 * g0 - cont).max(0.5);
}
if g1 > thresh {
best_xy = xy1;
best_yy = yy1;
if max_allowed >= 1 {
t = t1.clamp(1, max_allowed);
} else {
t = 0;
}
g = g1;
}
}
#[cfg(test)]
if trace {
crate::test_trace::trace_println!(
"celt_remove_doubling.best_xy={:.9e} best_yy={:.9e} g={:.9e} t={t}",
best_xy,
best_yy,
g
);
if std::env::var("CELT_TRACE_REMOVE_DOUBLING_BITS")
.map(|value| !value.is_empty() && value != "0")
.unwrap_or(false)
{
crate::test_trace::trace_println!(
"celt_remove_doubling.best_xy_bits=0x{:08x}",
best_xy.to_bits()
);
crate::test_trace::trace_println!(
"celt_remove_doubling.best_yy_bits=0x{:08x}",
best_yy.to_bits()
);
crate::test_trace::trace_println!("celt_remove_doubling.g_bits=0x{:08x}", g.to_bits());
}
}
best_xy = best_xy.max(0.0);
let mut pg = if best_yy <= best_xy {
1.0
} else {
frac_div32(best_xy, best_yy + 1.0)
};
let mut xcorr = [0.0; 3];
for (k, slot) in xcorr.iter_mut().enumerate() {
let lag = t + k as i32 - 1;
let windowed = window(x, center, -(lag as isize), n_half);
*slot = celt_inner_prod(x_center, windowed);
}
let mut offset = 0;
if (xcorr[2] - xcorr[0]) > 0.7 * (xcorr[1] - xcorr[0]) {
offset = 1;
} else if (xcorr[0] - xcorr[2]) > 0.7 * (xcorr[1] - xcorr[2]) {
offset = -1;
}
if pg > g {
pg = g;
}
let updated = 2 * t + offset;
*t0 = updated.max(minperiod0);
#[cfg(test)]
if trace {
crate::test_trace::trace_println!(
"celt_remove_doubling.xcorr0={:.9e} xcorr1={:.9e} xcorr2={:.9e} offset={offset} pg={:.9e} t0={}",
xcorr[0],
xcorr[1],
xcorr[2],
pg,
*t0
);
}
pg
}
#[cfg(feature = "fixed_point")]
#[allow(clippy::too_many_arguments)]
pub(crate) fn remove_doubling_fixed(
x: &[FixedOpusVal16],
maxperiod: usize,
minperiod: usize,
n: usize,
t0: &mut i32,
prev_period: i32,
prev_gain: FixedOpusVal16,
_arch: i32,
) -> FixedOpusVal16 {
assert!(maxperiod > 0, "maxperiod must be positive");
assert!(minperiod > 0, "minperiod must be positive");
assert!(n > 0, "window size must be positive");
let maxperiod_half = maxperiod >> 1;
let minperiod_half = minperiod >> 1;
let n_half = n >> 1;
assert!(
x.len() >= maxperiod_half + n_half,
"x must contain at least maxperiod / 2 + n / 2 samples",
);
let minperiod0 = minperiod as i32;
let t0_half = (*t0 >> 1).clamp(0, maxperiod_half.saturating_sub(1) as i32);
let prev_period_half = prev_period >> 1;
if maxperiod_half <= 1 || n_half == 0 {
*t0 = (*t0).max(minperiod0);
return prev_gain;
}
let center = maxperiod_half;
let x_center = window_fixed(x, center, 0, n_half);
let x_t0 = window_fixed(x, center, -(t0_half as isize), n_half);
let (xx, xy) = dual_inner_prod_fixed(x_center, x_center, x_t0);
let mut yy_lookup = vec![0i32; maxperiod_half + 1];
yy_lookup[0] = xx;
let mut yy = xx;
for i in 1..=maxperiod_half {
let prev_sample = x[center - i];
let enter_sample = x[center + n_half - i];
yy = yy
.wrapping_add(mult16_16(prev_sample, prev_sample))
.wrapping_sub(mult16_16(enter_sample, enter_sample));
yy_lookup[i] = max(0, yy);
}
yy = yy_lookup[t0_half as usize];
let mut best_xy = xy;
let mut best_yy = yy;
let mut g = compute_pitch_gain_fixed(xy, xx, yy);
let g0 = g;
let max_allowed = maxperiod_half.saturating_sub(1) as i32;
let mut t = if max_allowed >= 1 {
t0_half.clamp(1, max_allowed)
} else {
0
};
for k in 2..=15 {
let t1 = celt_udiv((2 * t0_half + k) as u32, (2 * k) as u32) as i32;
if t1 < minperiod_half as i32 {
break;
}
if t1 as usize > maxperiod_half {
continue;
}
let t1b = if k == 2 {
if t1 + t0_half > maxperiod_half as i32 {
t0_half
} else {
t0_half + t1
}
} else {
let check = SECOND_CHECK[k as usize];
celt_udiv((2 * check * t0_half + k) as u32, (2 * k) as u32) as i32
};
if t1b as usize > maxperiod_half {
continue;
}
let x_t1 = window_fixed(x, center, -(t1 as isize), n_half);
let x_t1b = window_fixed(x, center, -(t1b as isize), n_half);
let (xy1, xy2) = dual_inner_prod_fixed(x_center, x_t1, x_t1b);
let xy1 = shr32(xy1.wrapping_add(xy2), 1);
let yy1 = shr32(
yy_lookup[t1 as usize].wrapping_add(yy_lookup[t1b as usize]),
1,
);
let g1 = compute_pitch_gain_fixed(xy1, xx, yy1);
let diff = (t1 - prev_period_half).abs();
let cont = if diff <= 1 {
prev_gain
} else if diff <= 2 && 5 * (k * k) < t0_half {
prev_gain >> 1
} else {
0
};
let mut thresh = max(
qconst16(0.3, 15),
mult16_16_q15(qconst16(0.7, 15), g0).wrapping_sub(cont),
);
if t1 < 3 * minperiod_half as i32 {
thresh = max(
qconst16(0.4, 15),
mult16_16_q15(qconst16(0.85, 15), g0).wrapping_sub(cont),
);
} else if t1 < 2 * minperiod_half as i32 {
thresh = max(
qconst16(0.5, 15),
mult16_16_q15(qconst16(0.9, 15), g0).wrapping_sub(cont),
);
}
if g1 > thresh {
best_xy = xy1;
best_yy = yy1;
if max_allowed >= 1 {
t = t1.clamp(1, max_allowed);
} else {
t = 0;
}
g = g1;
}
}
best_xy = max(0, best_xy);
let mut pg = if best_yy <= best_xy {
Q15_ONE
} else {
extract16(shr32(frac_div32_fixed(best_xy, best_yy + 1), 16))
};
let mut xcorr = [0i32; 3];
for (k, slot) in xcorr.iter_mut().enumerate() {
let lag = t + k as i32 - 1;
let x_lag = window_fixed(x, center, -(lag as isize), n_half);
*slot = celt_inner_prod_fixed(x_center, x_lag);
}
let mut offset = 0;
if (xcorr[2] - xcorr[0]) > mult16_32_q15(qconst16(0.7, 15), xcorr[1] - xcorr[0]) {
offset = 1;
} else if (xcorr[0] - xcorr[2]) > mult16_32_q15(qconst16(0.7, 15), xcorr[1] - xcorr[2]) {
offset = -1;
}
if pg > g {
pg = g;
}
let updated = 2 * t + offset;
*t0 = updated.max(minperiod0);
pg
}
fn celt_fir5(x: &mut [OpusVal16], num: &[OpusVal16; 5]) {
let [num0, num1, num2, num3, num4] = *num;
let mut mem0 = 0.0;
let mut mem1 = 0.0;
let mut mem2 = 0.0;
let mut mem3 = 0.0;
let mut mem4 = 0.0;
for sample in x.iter_mut() {
let current = *sample;
let mut sum = current;
sum = crate::celt::math::mul_add_f32(num0, mem0, sum);
sum = crate::celt::math::mul_add_f32(num1, mem1, sum);
sum = crate::celt::math::mul_add_f32(num2, mem2, sum);
sum = crate::celt::math::mul_add_f32(num3, mem3, sum);
sum = crate::celt::math::mul_add_f32(num4, mem4, sum);
mem4 = mem3;
mem3 = mem2;
mem2 = mem1;
mem1 = mem0;
mem0 = current;
*sample = sum;
}
}
#[cfg(feature = "fixed_point")]
fn celt_fir5_fixed(x: &mut [FixedOpusVal16], num: &[FixedOpusVal16; 5]) {
let [num0, num1, num2, num3, num4] = *num;
let mut mem0: FixedOpusVal16 = 0;
let mut mem1: FixedOpusVal16 = 0;
let mut mem2: FixedOpusVal16 = 0;
let mut mem3: FixedOpusVal16 = 0;
let mut mem4: FixedOpusVal16 = 0;
for sample in x.iter_mut() {
let current = *sample;
let mut sum = shl32(i32::from(current), SIG_SHIFT);
sum = mac16_16(sum, num0, mem0);
sum = mac16_16(sum, num1, mem1);
sum = mac16_16(sum, num2, mem2);
sum = mac16_16(sum, num3, mem3);
sum = mac16_16(sum, num4, mem4);
mem4 = mem3;
mem3 = mem2;
mem2 = mem1;
mem1 = mem0;
mem0 = current;
*sample = extract16(pshr32(sum, SIG_SHIFT));
}
}
pub(crate) fn pitch_downsample(x: &[&[CeltSig]], x_lp: &mut [OpusVal16], len: usize, arch: i32) {
assert!(!x.is_empty(), "at least one channel is required");
assert!(
x.len() <= 2,
"pitch_downsample supports at most two channels"
);
assert!(
len >= 2,
"pitch_downsample requires at least two input samples"
);
for (idx, channel) in x.iter().enumerate() {
assert!(
channel.len() >= len,
"channel {idx} must provide at least len samples",
);
}
let half_len = len / 2;
assert!(
x_lp.len() >= half_len,
"output buffer must contain len / 2 samples"
);
if half_len == 0 {
return;
}
let x_lp = &mut x_lp[..half_len];
x_lp.fill(0.0);
for channel in x {
let mut acc0 = 0.25 * channel[1];
acc0 += 0.5 * channel[0];
x_lp[0] += acc0;
for (i, slot) in x_lp.iter_mut().enumerate().take(half_len).skip(1) {
let base = 2 * i;
let mut acc = 0.25 * channel[base - 1];
acc += 0.25 * channel[base + 1];
acc += 0.5 * channel[base];
*slot += acc;
}
}
let mut ac = [0.0; 5];
celt_autocorr(x_lp, &mut ac, None, 0, 4, arch);
ac[0] *= 1.0001;
for (i, value) in ac.iter_mut().enumerate().skip(1) {
let coeff = 0.008 * i as f32;
*value -= *value * coeff * coeff;
}
let mut lpc = [0.0; 4];
celt_lpc(&mut lpc, &ac);
let mut tmp = 1.0;
for coeff in &mut lpc {
tmp *= 0.9;
*coeff *= tmp;
}
let c1 = 0.8;
let lpc2 = [
lpc[0] + 0.8,
crate::celt::math::mul_add_f32(c1, lpc[0], lpc[1]),
crate::celt::math::mul_add_f32(c1, lpc[1], lpc[2]),
crate::celt::math::mul_add_f32(c1, lpc[2], lpc[3]),
c1 * lpc[3],
];
#[cfg(test)]
if std::env::var("CELT_TRACE_PITCH_LPC")
.map(|value| !value.is_empty() && value != "0")
.unwrap_or(false)
{
if let Some(frame_idx) = remove_doubling_trace::current_frame() {
let target = std::env::var("CELT_TRACE_PITCH_LPC_FRAME")
.ok()
.and_then(|value| value.parse::<usize>().ok());
let want_bits = std::env::var("CELT_TRACE_PITCH_LPC_BITS")
.map(|value| !value.is_empty() && value != "0")
.unwrap_or(false);
if target.map_or(true, |value| value == frame_idx) {
for (i, value) in ac.iter().enumerate() {
crate::test_trace::trace_println!(
"celt_pitch_lpc[{frame_idx}].ac[{i}]={:.9e}",
value
);
if want_bits {
crate::test_trace::trace_println!(
"celt_pitch_lpc[{frame_idx}].ac_bits[{i}]=0x{:08x}",
value.to_bits()
);
}
}
for (i, value) in lpc.iter().enumerate() {
crate::test_trace::trace_println!(
"celt_pitch_lpc[{frame_idx}].lpc[{i}]={:.9e}",
value
);
if want_bits {
crate::test_trace::trace_println!(
"celt_pitch_lpc[{frame_idx}].lpc_bits[{i}]=0x{:08x}",
value.to_bits()
);
}
}
for (i, value) in lpc2.iter().enumerate() {
crate::test_trace::trace_println!(
"celt_pitch_lpc[{frame_idx}].lpc2[{i}]={:.9e}",
value
);
if want_bits {
crate::test_trace::trace_println!(
"celt_pitch_lpc[{frame_idx}].lpc2_bits[{i}]=0x{:08x}",
value.to_bits()
);
}
}
}
}
}
celt_fir5(x_lp, &lpc2);
}
#[cfg(feature = "fixed_point")]
pub(crate) fn pitch_downsample_fixed(
x: &[&[FixedCeltSig]],
x_lp: &mut [FixedOpusVal16],
len: usize,
arch: i32,
) {
assert!(!x.is_empty(), "at least one channel is required");
assert!(
x.len() <= 2,
"pitch_downsample_fixed supports at most two channels"
);
assert!(
len >= 2,
"pitch_downsample_fixed requires at least two input samples"
);
for (idx, channel) in x.iter().enumerate() {
assert!(
channel.len() >= len,
"channel {idx} must provide at least len samples",
);
}
let half_len = len / 2;
assert!(
x_lp.len() >= half_len,
"output buffer must contain len / 2 samples"
);
if half_len == 0 {
return;
}
let x_lp = &mut x_lp[..half_len];
x_lp.fill(0);
let mut maxabs = celt_maxabs32(&x[0][..len]);
if x.len() == 2 {
let maxabs_1 = celt_maxabs32(&x[1][..len]);
maxabs = max(maxabs, maxabs_1);
}
if maxabs < 1 {
maxabs = 1;
}
let mut shift = celt_ilog2(maxabs) - 10;
if shift < 0 {
shift = 0;
}
if x.len() == 2 {
shift += 1;
}
for i in 1..half_len {
let base = 2 * i;
let mut sum = shr32(x[0][base - 1], (shift + 2) as u32)
.wrapping_add(shr32(x[0][base + 1], (shift + 2) as u32))
.wrapping_add(shr32(x[0][base], (shift + 1) as u32));
if x.len() == 2 {
sum = sum
.wrapping_add(shr32(x[1][base - 1], (shift + 2) as u32))
.wrapping_add(shr32(x[1][base + 1], (shift + 2) as u32))
.wrapping_add(shr32(x[1][base], (shift + 1) as u32));
}
x_lp[i] = sum as FixedOpusVal16;
}
let mut head =
shr32(x[0][1], (shift + 2) as u32).wrapping_add(shr32(x[0][0], (shift + 1) as u32));
if x.len() == 2 {
head = head
.wrapping_add(shr32(x[1][1], (shift + 2) as u32))
.wrapping_add(shr32(x[1][0], (shift + 1) as u32));
}
x_lp[0] = head as FixedOpusVal16;
let mut ac = [0i32; 5];
celt_autocorr_fixed(x_lp, &mut ac, None, 0, 4, arch);
ac[0] = ac[0].wrapping_add(shr32(ac[0], 13));
for i in 1..=4 {
let coeff = (2 * i * i) as FixedOpusVal16;
ac[i] = ac[i].wrapping_sub(mult16_32_q15(coeff, ac[i]));
}
let mut lpc = [0i16; 4];
celt_lpc_fixed(&mut lpc, &ac);
let mut tmp = Q15_ONE;
let mut lpc_scaled = [0i16; 4];
for (idx, slot) in lpc_scaled.iter_mut().enumerate() {
tmp = mult16_16_q15(qconst16(0.9, 15), tmp);
*slot = mult16_16_q15(lpc[idx], tmp);
}
let c1 = qconst16(0.8, 15);
let lpc2 = [
lpc_scaled[0].wrapping_add(qconst16(0.8, SIG_SHIFT)),
lpc_scaled[1].wrapping_add(mult16_16_q15(c1, lpc_scaled[0])),
lpc_scaled[2].wrapping_add(mult16_16_q15(c1, lpc_scaled[1])),
lpc_scaled[3].wrapping_add(mult16_16_q15(c1, lpc_scaled[2])),
mult16_16_q15(c1, lpc_scaled[3]),
];
celt_fir5_fixed(x_lp, &lpc2);
}
#[cfg(test)]
mod tests {
use super::{
SECOND_CHECK, celt_fir5, celt_inner_prod, celt_pitch_xcorr, compute_pitch_gain,
dual_inner_prod, find_best_pitch, pitch_downsample, pitch_search, remove_doubling,
xcorr_kernel,
};
#[cfg(feature = "fixed_point")]
use super::{
celt_pitch_xcorr_fixed, find_best_pitch_fixed, pitch_downsample_fixed, pitch_search_fixed,
remove_doubling_fixed,
};
#[cfg(feature = "fixed_point")]
use crate::celt::fixed_arch::float2sig;
use crate::celt::math::celt_sqrt;
use crate::celt::types::{CeltSig, OpusVal16, OpusVal32};
#[cfg(feature = "fixed_point")]
use crate::celt::types::{FixedCeltSig, FixedOpusVal16};
use crate::celt::{celt_autocorr, celt_lpc, celt_udiv, frac_div32};
use alloc::vec;
use alloc::vec::Vec;
use core::f32::consts::PI;
fn generate_sequence(len: usize, seed: u32) -> Vec<OpusVal16> {
let mut state = seed;
let mut data = Vec::with_capacity(len);
for _ in 0..len {
state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
let val = ((state >> 8) as f32 / u32::MAX as f32) * 2.0 - 1.0;
data.push(val as OpusVal16);
}
data
}
#[test]
fn inner_product_matches_reference() {
let x = generate_sequence(64, 0x1234_5678);
let y = generate_sequence(64, 0x8765_4321);
let expected: f32 = x.iter().zip(y.iter()).map(|(&a, &b)| a * b).sum();
let result = celt_inner_prod(&x, &y);
assert!((expected - result).abs() < 1e-6);
}
#[test]
fn dual_inner_product_matches_individual_computations() {
let x = generate_sequence(48, 0x4242_4242);
let y0 = generate_sequence(48, 0x1357_9bdf);
let y1 = generate_sequence(48, 0x0246_8ace);
let (dot0, dot1) = dual_inner_prod(&x, &y0, &y1);
let expected0: f32 = x.iter().zip(y0.iter()).map(|(&a, &b)| a * b).sum();
let expected1: f32 = x.iter().zip(y1.iter()).map(|(&a, &b)| a * b).sum();
assert!((dot0 - expected0).abs() < 1e-6);
assert!((dot1 - expected1).abs() < 1e-6);
}
#[test]
fn xcorr_kernel_matches_four_delays() {
let len = 12usize;
let x = generate_sequence(len, 0xfeed_face);
let y = generate_sequence(len + 3, 0xdead_beef);
let mut sums = [0.0f32; 4];
xcorr_kernel(&x, &y, &mut sums, len);
for delay in 0..4 {
let expected: f32 = (0..len).map(|i| x[i] * y[i + delay]).sum();
assert!(
(sums[delay] - expected).abs() < 1e-6,
"delay {delay} mismatch: expected {expected}, got {}",
sums[delay]
);
}
}
#[test]
fn pitch_gain_matches_reference_formula() {
let xy = 0.75f32;
let xx = 0.5f32;
let yy = 1.25f32;
let expected = (xy / celt_sqrt(1.0 + xx * yy)) as OpusVal16;
let gain = compute_pitch_gain(xy, xx, yy);
assert!((expected - gain).abs() < 1e-6);
}
#[test]
fn pitch_xcorr_matches_naive_cross_correlation() {
let len = 16usize;
let max_pitch = 8usize;
let x = generate_sequence(len, 0x0f0f_0f0f);
let y = generate_sequence(len + max_pitch, 0x1337_4242);
let mut xcorr = vec![0.0f32; max_pitch];
celt_pitch_xcorr(&x, &y, len, max_pitch, &mut xcorr);
for delay in 0..max_pitch {
let expected: f32 = x
.iter()
.zip(y[delay..delay + len].iter())
.map(|(&a, &b)| a * b)
.sum();
assert!(
(expected - xcorr[delay]).abs() < 1e-6,
"mismatch at delay {delay}: expected {expected}, got {}",
xcorr[delay]
);
}
}
fn reference_find_best_pitch(
xcorr: &[OpusVal32],
y: &[OpusVal16],
len: usize,
max_pitch: usize,
) -> [i32; 2] {
let mut syy = 1.0;
for &sample in &y[..len] {
syy += sample * sample;
}
let mut best_num = [-1.0, -1.0];
let mut best_den = [0.0, 0.0];
let mut best_pitch = [0, if max_pitch > 1 { 1 } else { 0 }];
for i in 0..max_pitch {
let corr = xcorr[i];
if corr > 0.0 {
let num = corr * corr;
if num * best_den[1] > best_num[1] * syy {
if num * best_den[0] > best_num[0] * syy {
best_num[1] = best_num[0];
best_den[1] = best_den[0];
best_pitch[1] = best_pitch[0];
best_num[0] = num;
best_den[0] = syy;
best_pitch[0] = i as i32;
} else {
best_num[1] = num;
best_den[1] = syy;
best_pitch[1] = i as i32;
}
}
}
let entering = y[i + len];
let leaving = y[i];
syy += entering * entering - leaving * leaving;
if syy < 1.0 {
syy = 1.0;
}
}
best_pitch
}
#[test]
fn find_best_pitch_matches_reference() {
let len = 48usize;
let max_pitch = 24usize;
let x = generate_sequence(len, 0x1111_2222);
let mut y = vec![0.0; len + max_pitch];
let primary_lag = 7usize;
let secondary_lag = 15usize;
for i in 0..len {
y[i + primary_lag] += x[i];
y[i + secondary_lag] += 0.6 * x[i];
}
let mut xcorr = vec![0.0; max_pitch];
celt_pitch_xcorr(&x, &y, len, max_pitch, &mut xcorr);
let mut best = [0i32; 2];
find_best_pitch(&xcorr, &y, len, max_pitch, &mut best);
let expected = reference_find_best_pitch(&xcorr, &y, len, max_pitch);
assert_eq!(best, expected);
}
fn reference_pitch_downsample(x: &[&[CeltSig]], len: usize, arch: i32) -> Vec<OpusVal16> {
let half_len = len / 2;
let mut downsampled = vec![0.0; half_len];
if half_len == 0 {
return downsampled;
}
for channel in x {
downsampled[0] += 0.25 * channel[1] + 0.5 * channel[0];
for (i, slot) in downsampled.iter_mut().enumerate().take(half_len).skip(1) {
let base = 2 * i;
*slot += 0.25 * channel[base - 1] + 0.5 * channel[base] + 0.25 * channel[base + 1];
}
}
let mut ac = [0.0; 5];
celt_autocorr(&downsampled, &mut ac, None, 0, 4, arch);
ac[0] *= 1.0001;
for (i, value) in ac.iter_mut().enumerate().skip(1) {
let coeff = 0.008 * i as f32;
*value -= *value * coeff * coeff;
}
let mut lpc = [0.0; 4];
celt_lpc(&mut lpc, &ac);
let mut tmp = 1.0;
for coeff in &mut lpc {
tmp *= 0.9;
*coeff *= tmp;
}
let c1 = 0.8;
let lpc2 = [
lpc[0] + 0.8,
lpc[1] + c1 * lpc[0],
lpc[2] + c1 * lpc[1],
lpc[3] + c1 * lpc[2],
c1 * lpc[3],
];
let mut filtered = downsampled;
celt_fir5(&mut filtered, &lpc2);
filtered
}
#[test]
fn pitch_downsample_matches_reference_for_mono() {
let len = 64;
let input = generate_sequence(len, 0x5555_aaaa);
let channels: [&[CeltSig]; 1] = [&input];
let mut output = vec![0.0; len / 2];
pitch_downsample(&channels, &mut output, len, 0);
let expected = reference_pitch_downsample(&channels, len, 0);
for (result, reference) in output.iter().zip(expected.iter()) {
assert!((result - reference).abs() < 1e-6);
}
}
#[test]
fn pitch_downsample_matches_reference_for_stereo() {
let len = 48;
let left = generate_sequence(len, 0x1234_ffff);
let right = generate_sequence(len, 0xabcd_0001);
let channels: [&[CeltSig]; 2] = [&left, &right];
let mut output = vec![0.0; len / 2];
pitch_downsample(&channels, &mut output, len, 0);
let expected = reference_pitch_downsample(&channels, len, 0);
for (result, reference) in output.iter().zip(expected.iter()) {
assert!((result - reference).abs() < 1e-6);
}
}
fn reference_pitch_search(
x_lp: &[OpusVal16],
y: &[OpusVal16],
len: usize,
max_pitch: usize,
) -> i32 {
let lag = len + max_pitch;
let len_quarter = len >> 2;
let lag_quarter = lag >> 2;
let max_pitch_quarter = max_pitch >> 2;
let mut best = [0, if max_pitch_quarter > 1 { 1 } else { 0 }];
if len_quarter > 0 && max_pitch_quarter > 0 {
let mut x_lp4 = vec![0.0; len_quarter];
for (j, slot) in x_lp4.iter_mut().enumerate() {
*slot = x_lp[2 * j];
}
let mut y_lp4 = vec![0.0; lag_quarter];
for (j, slot) in y_lp4.iter_mut().enumerate() {
*slot = y[2 * j];
}
let mut xcorr = vec![0.0; max_pitch_quarter];
for i in 0..max_pitch_quarter {
let mut sum = 0.0;
for j in 0..len_quarter {
sum += x_lp4[j] * y_lp4[i + j];
}
xcorr[i] = sum;
}
find_best_pitch(
&xcorr,
&y_lp4[..len_quarter + max_pitch_quarter],
len_quarter,
max_pitch_quarter,
&mut best,
);
}
let max_pitch_half = max_pitch >> 1;
if max_pitch_half == 0 {
return 2 * best[0];
}
let len_half = len >> 1;
let mut xcorr = vec![0.0; max_pitch_half];
if len_half > 0 {
for i in 0..max_pitch_half {
if (i as i32 - 2 * best[0]).abs() > 2 && (i as i32 - 2 * best[1]).abs() > 2 {
continue;
}
let mut sum = 0.0;
for j in 0..len_half {
sum += x_lp[j] * y[i + j];
}
xcorr[i] = sum.max(-1.0);
}
find_best_pitch(
&xcorr,
&y[..len_half + max_pitch_half],
len_half,
max_pitch_half,
&mut best,
);
}
let mut offset = 0;
if best[0] > 0 && (best[0] as usize) < max_pitch_half - 1 {
let a = xcorr[(best[0] - 1) as usize];
let b = xcorr[best[0] as usize];
let c = xcorr[(best[0] + 1) as usize];
if (c - a) > 0.7 * (b - a) {
offset = 1;
} else if (a - c) > 0.7 * (b - c) {
offset = -1;
}
}
2 * best[0] - offset
}
fn reference_remove_doubling(
x: &[OpusVal16],
maxperiod: usize,
minperiod: usize,
n: usize,
t0: &mut i32,
prev_period: i32,
prev_gain: OpusVal16,
) -> OpusVal16 {
let minperiod0 = minperiod as i32;
let maxperiod_half = maxperiod >> 1;
let minperiod_half = minperiod >> 1;
let t0_half = (*t0 >> 1).clamp(0, maxperiod_half.saturating_sub(1) as i32);
let prev_period_half = prev_period >> 1;
let n_half = n >> 1;
if maxperiod_half <= 1 || n_half == 0 {
*t0 = (*t0).max(minperiod0);
return prev_gain;
}
let center = maxperiod_half;
let x_center = &x[center..center + n_half];
let x_t0 = &x[center - t0_half as usize..center - t0_half as usize + n_half];
let mut xx = 0.0;
let mut xy = 0.0;
for j in 0..n_half {
xx += x_center[j] * x_center[j];
xy += x_center[j] * x_t0[j];
}
let mut yy_lookup = vec![0.0; maxperiod_half + 1];
yy_lookup[0] = xx;
let mut yy = xx;
for i in 1..=maxperiod_half {
let prev_sample = x[center - i];
let enter_sample = x[center + n_half - i];
yy += prev_sample * prev_sample - enter_sample * enter_sample;
yy_lookup[i] = yy.max(0.0);
}
yy = yy_lookup[t0_half as usize];
let mut best_xy = xy;
let mut best_yy = yy;
let g0 = compute_pitch_gain(xy, xx, yy);
let mut g = g0;
let max_allowed = maxperiod_half.saturating_sub(1) as i32;
let mut t = if max_allowed >= 1 {
t0_half.clamp(1, max_allowed)
} else {
0
};
for k in 2..=15 {
let t1 = celt_udiv((2 * t0_half + k) as u32, (2 * k) as u32) as i32;
if t1 < minperiod_half as i32 {
break;
}
if t1 as usize > maxperiod_half {
continue;
}
let t1b = if k == 2 {
if t1 + t0_half > maxperiod_half as i32 {
t0_half
} else {
t0_half + t1
}
} else {
let check = SECOND_CHECK[k as usize];
celt_udiv((2 * check * t0_half + k) as u32, (2 * k) as u32) as i32
};
if t1b as usize > maxperiod_half {
continue;
}
let x_t1 = &x[center - t1 as usize..center - t1 as usize + n_half];
let x_t1b = &x[center - t1b as usize..center - t1b as usize + n_half];
let mut xy1 = 0.0;
let mut xy2 = 0.0;
for j in 0..n_half {
xy1 += x_center[j] * x_t1[j];
xy2 += x_center[j] * x_t1b[j];
}
xy1 = 0.5 * (xy1 + xy2);
let yy1 = 0.5 * (yy_lookup[t1 as usize] + yy_lookup[t1b as usize]);
let g1 = compute_pitch_gain(xy1, xx, yy1);
let diff = (t1 - prev_period_half).abs();
let cont = if diff <= 1 {
prev_gain
} else if diff <= 2 && 5 * (k * k) < t0_half {
0.5 * prev_gain
} else {
0.0
};
let mut thresh = (0.7 * g0 - cont).max(0.3);
if t1 < 3 * minperiod_half as i32 {
thresh = (0.85 * g0 - cont).max(0.4);
} else if t1 < 2 * minperiod_half as i32 {
thresh = (0.9 * g0 - cont).max(0.5);
}
if g1 > thresh {
best_xy = xy1;
best_yy = yy1;
if max_allowed >= 1 {
t = t1.clamp(1, max_allowed);
} else {
t = 0;
}
g = g1;
}
}
best_xy = best_xy.max(0.0);
let mut pg = if best_yy <= best_xy {
1.0
} else {
frac_div32(best_xy, best_yy + 1.0)
};
let mut xcorr = [0.0; 3];
for (k, slot) in xcorr.iter_mut().enumerate() {
let lag = t + k as i32 - 1;
let lag_usize = lag as usize;
let x_lag = &x[center - lag_usize..center - lag_usize + n_half];
let mut sum = 0.0;
for j in 0..n_half {
sum += x_center[j] * x_lag[j];
}
*slot = sum;
}
let mut offset = 0;
if (xcorr[2] - xcorr[0]) > 0.7 * (xcorr[1] - xcorr[0]) {
offset = 1;
} else if (xcorr[0] - xcorr[2]) > 0.7 * (xcorr[1] - xcorr[2]) {
offset = -1;
}
if pg > g {
pg = g;
}
let updated = 2 * t + offset;
*t0 = updated.max(minperiod0);
pg
}
#[test]
fn pitch_search_matches_reference() {
let len = 96usize;
let max_pitch = 48usize;
let fundamental = 34usize;
let mut x_lp = vec![0.0; len];
for (i, sample) in x_lp.iter_mut().enumerate() {
let phase = 2.0 * PI * (i as f32) / fundamental as f32;
*sample = phase.sin();
}
let mut y = vec![0.0; len + max_pitch];
for i in 0..len {
y[i + fundamental] += x_lp[i];
}
for i in 0..len {
y[i + 20] += 0.4 * x_lp[i];
}
let reference = reference_pitch_search(&x_lp, &y, len, max_pitch);
let result = pitch_search(&x_lp, &y, len, max_pitch, 0);
assert_eq!(result, reference);
}
#[test]
fn remove_doubling_matches_reference() {
let maxperiod = 120usize;
let minperiod = 40usize;
let n = 80usize;
let fundamental = 60usize;
let mut x = vec![0.0; maxperiod + n];
for (i, sample) in x.iter_mut().enumerate() {
let phase = 2.0 * PI * (i as f32) / fundamental as f32;
*sample = phase.sin();
}
let mut t0_reference = (2 * fundamental) as i32;
let mut t0_result = t0_reference;
let expected = reference_remove_doubling(
&x,
maxperiod,
minperiod,
n,
&mut t0_reference,
fundamental as i32,
0.8,
);
let gain = remove_doubling(
&x,
maxperiod,
minperiod,
n,
&mut t0_result,
fundamental as i32,
0.8,
0,
);
assert_eq!(t0_result, t0_reference);
assert!((gain - expected).abs() < 1e-6);
}
#[cfg(feature = "fixed_point")]
#[test]
fn fixed_pitch_xcorr_matches_float_reference() {
let len = 16usize;
let max_pitch = 6usize;
let mut x = vec![0.0f32; len];
let mut y = vec![0.0f32; len + max_pitch];
for (i, sample) in x.iter_mut().enumerate() {
*sample = (i as f32 * 0.17).sin();
}
for (i, sample) in y.iter_mut().enumerate() {
*sample = (i as f32 * 0.23).cos();
}
let scale = 8192.0;
let x_fixed: Vec<FixedOpusVal16> = x.iter().map(|&v| (v * scale).round() as i16).collect();
let y_fixed: Vec<FixedOpusVal16> = y.iter().map(|&v| (v * scale).round() as i16).collect();
let mut xcorr_fixed = vec![0i32; max_pitch];
let maxcorr = celt_pitch_xcorr_fixed(&x_fixed, &y_fixed, len, max_pitch, &mut xcorr_fixed);
assert!(maxcorr > 0);
let x_float: Vec<f32> = x_fixed.iter().map(|&v| v as f32 / scale).collect();
let y_float: Vec<f32> = y_fixed.iter().map(|&v| v as f32 / scale).collect();
let mut xcorr_float = vec![0.0f32; max_pitch];
celt_pitch_xcorr(&x_float, &y_float, len, max_pitch, &mut xcorr_float);
let denom = scale * scale;
for (fixed, float) in xcorr_fixed.iter().zip(xcorr_float.iter()) {
let fixed_float = *fixed as f32 / denom;
assert!(
(fixed_float - float).abs() < 1e-3,
"{fixed_float} vs {float}"
);
}
}
#[cfg(feature = "fixed_point")]
#[test]
fn fixed_find_best_pitch_matches_float_choice() {
let len = 12usize;
let max_pitch = 5usize;
let mut y = vec![0.0f32; len + max_pitch];
for (i, sample) in y.iter_mut().enumerate() {
*sample = (i as f32 * 0.19).sin();
}
let scale = 8192.0;
let y_fixed: Vec<FixedOpusVal16> = y.iter().map(|&v| (v * scale).round() as i16).collect();
let mut xcorr_fixed = vec![0i32; max_pitch];
let maxcorr =
celt_pitch_xcorr_fixed(&y_fixed[..len], &y_fixed, len, max_pitch, &mut xcorr_fixed);
let mut best_fixed = [0i32; 2];
find_best_pitch_fixed(
&xcorr_fixed,
&y_fixed,
len,
max_pitch,
&mut best_fixed,
0,
maxcorr,
);
let y_float: Vec<f32> = y_fixed.iter().map(|&v| v as f32 / scale).collect();
let mut xcorr_float = vec![0.0f32; max_pitch];
celt_pitch_xcorr(&y_float[..len], &y_float, len, max_pitch, &mut xcorr_float);
let mut best_float = [0i32; 2];
find_best_pitch(&xcorr_float, &y_float, len, max_pitch, &mut best_float);
assert_eq!(best_fixed[0], best_float[0]);
}
#[cfg(feature = "fixed_point")]
#[test]
fn fixed_pitch_search_tracks_float_reference() {
let len = 96usize;
let max_pitch = 48usize;
let fundamental = 34usize;
let mut x_lp = vec![0.0f32; len];
for (i, sample) in x_lp.iter_mut().enumerate() {
let phase = 2.0 * PI * (i as f32) / fundamental as f32;
*sample = phase.sin();
}
let mut y = vec![0.0f32; len + max_pitch];
for i in 0..len {
y[i + fundamental] += x_lp[i];
}
for i in 0..len {
y[i + 20] += 0.4 * x_lp[i];
}
let scale = 32_768.0;
let x_fixed: Vec<FixedOpusVal16> =
x_lp.iter().map(|&v| (v * scale).round() as i16).collect();
let y_fixed: Vec<FixedOpusVal16> = y.iter().map(|&v| (v * scale).round() as i16).collect();
let fixed = pitch_search_fixed(&x_fixed, &y_fixed, len, max_pitch, 0);
let x_float: Vec<f32> = x_fixed.iter().map(|&v| v as f32 / scale).collect();
let y_float: Vec<f32> = y_fixed.iter().map(|&v| v as f32 / scale).collect();
let reference = pitch_search(&x_float, &y_float, len, max_pitch, 0);
assert!((fixed - reference).abs() <= 1);
}
#[cfg(feature = "fixed_point")]
#[test]
fn fixed_remove_doubling_tracks_float_reference() {
let maxperiod = 120usize;
let minperiod = 40usize;
let n = 80usize;
let fundamental = 60usize;
let mut x = vec![0.0f32; maxperiod + n];
for (i, sample) in x.iter_mut().enumerate() {
let phase = 2.0 * PI * (i as f32) / fundamental as f32;
*sample = phase.sin();
}
let mut t0_float = (2 * fundamental) as i32;
let _float_gain = remove_doubling(
&x,
maxperiod,
minperiod,
n,
&mut t0_float,
fundamental as i32,
0.8,
0,
);
let scale = 32_768.0;
let x_fixed: Vec<FixedOpusVal16> = x.iter().map(|&v| (v * scale).round() as i16).collect();
let mut t0_fixed = (2 * fundamental) as i32;
let fixed_gain = remove_doubling_fixed(
&x_fixed,
maxperiod,
minperiod,
n,
&mut t0_fixed,
fundamental as i32,
(0.8 * scale).round() as i16,
0,
);
assert!((t0_fixed - t0_float).abs() <= 2);
let fixed_gain_float = fixed_gain as f32 / scale;
assert!((0.0..=1.0).contains(&fixed_gain_float));
}
#[cfg(feature = "fixed_point")]
#[test]
fn fixed_pitch_downsample_tracks_float_shape() {
let len = 64usize;
let mut input = vec![0.0f32; len];
for (i, sample) in input.iter_mut().enumerate() {
let phase = 2.0 * PI * (i as f32) / 16.0;
*sample = 0.4 * phase.sin();
}
let input_fixed: Vec<FixedCeltSig> = input.iter().map(|&v| float2sig(v)).collect();
let channels_fixed: [&[FixedCeltSig]; 1] = [&input_fixed];
let channels_float: [&[CeltSig]; 1] = [&input];
let mut fixed_out = vec![0i16; len / 2];
pitch_downsample_fixed(&channels_fixed, &mut fixed_out, len, 0);
let mut float_out = vec![0.0f32; len / 2];
pitch_downsample(&channels_float, &mut float_out, len, 0);
let fixed_float: Vec<f32> = fixed_out.iter().map(|&v| v as f32 / 32_768.0).collect();
let mut dot = 0.0;
let mut dot_fixed = 0.0;
for (f, r) in fixed_float.iter().zip(float_out.iter()) {
dot += f * r;
dot_fixed += f * f;
}
let scale = if dot_fixed > 0.0 {
dot / dot_fixed
} else {
0.0
};
let mut err = 0.0;
let mut ref_energy = 0.0;
for (f, r) in fixed_float.iter().zip(float_out.iter()) {
let diff = f * scale - r;
err += diff * diff;
ref_energy += r * r;
}
let nmse = if ref_energy > 0.0 {
err / ref_energy
} else {
0.0
};
assert!(nmse < 1e-2);
}
}