pub const TAB_NEW_CHIRP: [[f32; 4]; 4] = [
[0.0, 0.6, 0.9, 0.98],
[0.6, 0.75, 0.9, 0.98],
[0.0, 0.75, 0.9, 0.98],
[0.0, 0.75, 0.9, 0.98],
];
pub const TS_OFFSET_HFADJ: usize = 4;
pub const EPSILON_INV: f64 = 1.0_f64 / (1u32 << 20) as f64;
#[derive(Debug, Clone, Default)]
pub struct AspxTnsState {
pub tna_mode_prev: Vec<u8>,
pub chirp_prev: Vec<f32>,
}
impl AspxTnsState {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.tna_mode_prev.clear();
self.chirp_prev.clear();
}
}
pub fn compute_preflat_gains(
q_low: &[Vec<(f32, f32)>],
sbx: u32,
atsg_sig: &[u32],
num_ts_in_ats: u32,
) -> Vec<f32> {
let num_qmf_subbands = sbx as usize;
let mut gain_vec = vec![1.0_f32; num_qmf_subbands];
if num_qmf_subbands == 0 || atsg_sig.len() < 2 {
return gain_vec;
}
let num_atsg_sig = atsg_sig.len() - 1;
let ts_lo = (atsg_sig[0] * num_ts_in_ats) as usize;
let ts_hi = (atsg_sig[num_atsg_sig] * num_ts_in_ats) as usize;
if ts_hi <= ts_lo {
return gain_vec;
}
let denom = (ts_hi - ts_lo) as f64;
let mut pow_env = vec![0.0_f64; num_qmf_subbands];
let mut mean_energy = 0.0_f64;
#[allow(clippy::needless_range_loop)] for sb in 0..num_qmf_subbands {
if sb >= q_low.len() {
continue;
}
let row = &q_low[sb];
let mut acc = 0.0_f64;
let lo = ts_lo.min(row.len());
let hi = ts_hi.min(row.len());
for sample in row.iter().take(hi).skip(lo) {
let re = sample.0 as f64;
let im = sample.1 as f64;
acc += re * re + im * im;
}
let mean_pow = acc / denom;
pow_env[sb] = 10.0 * (mean_pow + 1.0).log10();
mean_energy += pow_env[sb];
}
mean_energy /= num_qmf_subbands as f64;
let poly = polynomial_fit_3(num_qmf_subbands, &pow_env);
#[allow(clippy::needless_range_loop)] for sb in 0..num_qmf_subbands {
let x = sb as f64;
let slope = poly[3] + poly[2] * x + poly[1] * x * x + poly[0] * x * x * x;
let g = 10.0_f64.powf((mean_energy - slope) / 20.0);
gain_vec[sb] = g as f32;
}
gain_vec
}
fn polynomial_fit_3(n: usize, y: &[f64]) -> [f64; 4] {
if n < 4 {
return [0.0; 4];
}
let mut s = [0.0_f64; 7];
let mut b = [0.0_f64; 4];
for (i, &yi) in y.iter().enumerate().take(n) {
let x = i as f64;
let mut xk = 1.0_f64;
for sk in s.iter_mut() {
*sk += xk;
xk *= x;
}
let mut xk = 1.0_f64;
for bk in b.iter_mut() {
*bk += yi * xk;
xk *= x;
}
}
let mut m = [[0.0_f64; 5]; 4];
for i in 0..4 {
m[i][..4].copy_from_slice(&s[i..(4 + i)]);
m[i][4] = b[i];
}
for col in 0..4 {
let mut piv = col;
for r in (col + 1)..4 {
if m[r][col].abs() > m[piv][col].abs() {
piv = r;
}
}
if m[piv][col].abs() < 1e-30 {
return [0.0; 4];
}
if piv != col {
m.swap(col, piv);
}
let pivot = m[col][col];
for r in (col + 1)..4 {
let f = m[r][col] / pivot;
#[allow(clippy::needless_range_loop)] for c in col..5 {
m[r][c] -= f * m[col][c];
}
}
}
let mut a = [0.0_f64; 4];
for r in (0..4).rev() {
let mut sum = m[r][4];
for c in (r + 1)..4 {
sum -= m[r][c] * a[c];
}
a[r] = sum / m[r][r];
}
[a[3], a[2], a[1], a[0]]
}
pub fn build_q_low_ext(
q_low: &[Vec<(f32, f32)>],
q_low_prev: &[Vec<(f32, f32)>],
sba: u32,
) -> Vec<Vec<(f32, f32)>> {
let n_cur = q_low.iter().map(|r| r.len()).max().unwrap_or(0);
let n_ext = n_cur + TS_OFFSET_HFADJ;
let mut ext: Vec<Vec<(f32, f32)>> = (0..(sba as usize))
.map(|_| vec![(0.0_f32, 0.0_f32); n_ext])
.collect();
for sb in 0..(sba as usize) {
if sb < q_low_prev.len() && !q_low_prev[sb].is_empty() {
let prev = &q_low_prev[sb];
let off = prev.len().saturating_sub(TS_OFFSET_HFADJ);
let take = (prev.len() - off).min(TS_OFFSET_HFADJ);
ext[sb][..take].copy_from_slice(&prev[off..off + take]);
}
if sb < q_low.len() {
let cur = &q_low[sb];
let copy_len = cur.len().min(n_ext - TS_OFFSET_HFADJ);
ext[sb][TS_OFFSET_HFADJ..TS_OFFSET_HFADJ + copy_len].copy_from_slice(&cur[..copy_len]);
}
}
ext
}
pub type CovMatrix = [[(f64, f64); 3]; 3];
pub type Alphas = (Vec<(f32, f32)>, Vec<(f32, f32)>);
pub fn compute_covariance(q_low_ext: &[Vec<(f32, f32)>], sba: u32) -> Vec<CovMatrix> {
let mut cov: Vec<CovMatrix> = vec![[[(0.0, 0.0); 3]; 3]; sba as usize];
#[allow(clippy::needless_range_loop)] for sb in 0..(sba as usize) {
if sb >= q_low_ext.len() {
continue;
}
let row = &q_low_ext[sb];
let n = row.len();
for i in 0..3 {
for j in 1..3 {
let mut acc = (0.0_f64, 0.0_f64);
let mut ts = TS_OFFSET_HFADJ;
while ts < n {
let a_idx = ts.wrapping_sub(2 * i);
let b_idx = ts.wrapping_sub(2 * j);
if a_idx < n && b_idx < n {
let a = row[a_idx];
let b = row[b_idx];
let re = a.0 as f64 * b.0 as f64 + a.1 as f64 * b.1 as f64;
let im = a.1 as f64 * b.0 as f64 - a.0 as f64 * b.1 as f64;
acc.0 += re;
acc.1 += im;
}
ts += 2;
}
cov[sb][i][j] = acc;
}
}
}
cov
}
pub fn compute_alphas(cov: &[CovMatrix]) -> Alphas {
let n = cov.len();
let mut alpha0: Vec<(f32, f32)> = vec![(0.0, 0.0); n];
let mut alpha1: Vec<(f32, f32)> = vec![(0.0, 0.0); n];
let one_over_eps = 1.0_f64 / (1.0 + EPSILON_INV);
for (sb, c) in cov.iter().enumerate() {
let c11 = c[1][1];
let c22 = c[2][2];
let c12 = c[1][2];
let c01 = c[0][1];
let c02 = c[0][2];
let denom_re = c22.0 * c11.0 - c22.1 * c11.1;
let mag12_sq = c12.0 * c12.0 + c12.1 * c12.1;
let denom = denom_re - mag12_sq * one_over_eps;
let mut a1 = (0.0_f64, 0.0_f64);
if denom != 0.0 {
let p1 = cmul(c01, c12);
let p2 = cmul(c02, c11);
a1.0 = (p1.0 - p2.0) / denom;
a1.1 = (p1.1 - p2.1) / denom;
}
let mut a0 = (0.0_f64, 0.0_f64);
if c11.0 != 0.0 || c11.1 != 0.0 {
let conj12 = (c12.0, -c12.1);
let prod = cmul(a1, conj12);
let numer = (-c01.0 + prod.0, -c01.1 + prod.1);
let denom11 = c11.0 * c11.0 + c11.1 * c11.1;
if denom11 != 0.0 {
a0.0 = (numer.0 * c11.0 + numer.1 * c11.1) / denom11;
a0.1 = (numer.1 * c11.0 - numer.0 * c11.1) / denom11;
}
}
let mag0 = (a0.0 * a0.0 + a0.1 * a0.1).sqrt();
let mag1 = (a1.0 * a1.0 + a1.1 * a1.1).sqrt();
if mag0 >= 4.0 || mag1 >= 4.0 {
alpha0[sb] = (0.0, 0.0);
alpha1[sb] = (0.0, 0.0);
} else {
alpha0[sb] = (a0.0 as f32, a0.1 as f32);
alpha1[sb] = (a1.0 as f32, a1.1 as f32);
}
}
(alpha0, alpha1)
}
fn cmul(a: (f64, f64), b: (f64, f64)) -> (f64, f64) {
(a.0 * b.0 - a.1 * b.1, a.0 * b.1 + a.1 * b.0)
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChirpResult {
pub chirp_arr: Vec<f32>,
pub tna_mode: Vec<u8>,
}
pub fn chirp_factors(tna_mode: &[u8], state: &AspxTnsState) -> ChirpResult {
let n = tna_mode.len();
let mut chirp_arr = vec![0.0_f32; n];
for sbg in 0..n {
let curr = (tna_mode[sbg] & 0x03) as usize;
let prev = state.tna_mode_prev.get(sbg).copied().unwrap_or(0) as usize & 0x03;
let mut new_chirp = TAB_NEW_CHIRP[prev][curr];
let prev_chirp = state.chirp_prev.get(sbg).copied().unwrap_or(0.0_f32);
if new_chirp < prev_chirp {
new_chirp = 0.75_f32 * new_chirp + 0.25_f32 * prev_chirp;
} else {
new_chirp = 0.90625_f32 * new_chirp + 0.09375_f32 * prev_chirp;
}
chirp_arr[sbg] = if new_chirp < 0.015625_f32 {
0.0
} else {
new_chirp
};
}
ChirpResult {
chirp_arr,
tna_mode: tna_mode.to_vec(),
}
}
pub fn advance_tns_state(state: &mut AspxTnsState, result: &ChirpResult) {
state.tna_mode_prev = result.tna_mode.clone();
state.chirp_prev = result.chirp_arr.clone();
}
#[allow(clippy::too_many_arguments)]
pub fn hf_tile_tns(
q_low_ext: &[Vec<(f32, f32)>],
patches: &crate::aspx::AspxPatchTables,
sbg_noise: &[u32],
chirp_arr: &[f32],
alpha0: &[(f32, f32)],
alpha1: &[(f32, f32)],
gain_vec: Option<&[f32]>,
sbx: u32,
num_qmf_subbands: u32,
atsg_sig: &[u32],
num_ts_in_ats: u32,
) -> Vec<Vec<(f32, f32)>> {
if atsg_sig.len() < 2 {
return (0..num_qmf_subbands).map(|_| Vec::new()).collect();
}
let n_ts_ext = q_low_ext.iter().map(|r| r.len()).max().unwrap_or(0);
let ts_lo = (atsg_sig[0] * num_ts_in_ats) as usize;
let ts_hi = (atsg_sig[atsg_sig.len() - 1] * num_ts_in_ats) as usize;
let mut q_high: Vec<Vec<(f32, f32)>> = (0..num_qmf_subbands)
.map(|_| vec![(0.0_f32, 0.0_f32); ts_hi])
.collect();
#[allow(clippy::needless_range_loop)] for ts in ts_lo..ts_hi {
let mut sum_sb_patches: u32 = 0;
let mut g: usize = 0;
for i in 0..(patches.num_sbg_patches as usize) {
for sb_off in 0..patches.sbg_patch_num_sb[i] {
let sb_high = sbx + sum_sb_patches + sb_off;
if sb_high >= num_qmf_subbands {
continue;
}
while g + 1 < sbg_noise.len() && sbg_noise[g + 1] == sb_high {
g += 1;
}
let p = (patches.sbg_patch_start_sb[i] + sb_off) as usize;
let n = ts + TS_OFFSET_HFADJ;
if p >= q_low_ext.len() || n >= n_ts_ext {
continue;
}
let row = &q_low_ext[p];
let mut sample = row[n];
if g < chirp_arr.len() && p < alpha0.len() {
let c = chirp_arr[g];
if c > 0.0 && n >= 2 {
let a = alpha0[p];
let z = row[n - 2];
let prod = (c * (a.0 * z.0 - a.1 * z.1), c * (a.0 * z.1 + a.1 * z.0));
sample.0 += prod.0;
sample.1 += prod.1;
}
if c > 0.0 && n >= 4 && p < alpha1.len() {
let c2 = c * c;
let a = alpha1[p];
let z = row[n - 4];
let prod = (c2 * (a.0 * z.0 - a.1 * z.1), c2 * (a.0 * z.1 + a.1 * z.0));
sample.0 += prod.0;
sample.1 += prod.1;
}
}
if let Some(gv) = gain_vec {
if let Some(&g_p) = gv.get(p) {
if g_p != 0.0 {
let inv = 1.0 / g_p;
sample.0 *= inv;
sample.1 *= inv;
}
}
}
q_high[sb_high as usize][ts] = sample;
}
sum_sb_patches += patches.sbg_patch_num_sb[i];
}
}
q_high
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aspx::AspxPatchTables;
#[test]
fn tab_new_chirp_layout() {
assert_eq!(TAB_NEW_CHIRP[0][0], 0.0);
assert_eq!(TAB_NEW_CHIRP[0][3], 0.98);
assert_eq!(TAB_NEW_CHIRP[1][1], 0.75);
assert_eq!(TAB_NEW_CHIRP[2][3], 0.98);
assert_eq!(TAB_NEW_CHIRP[3][3], 0.98);
assert_eq!(TAB_NEW_CHIRP[0][1], 0.6);
assert_eq!(TAB_NEW_CHIRP[1][0], 0.6);
assert_eq!(TAB_NEW_CHIRP[2][0], 0.0);
assert_eq!(TAB_NEW_CHIRP[3][0], 0.0);
}
#[test]
fn chirp_first_interval_attack() {
let state = AspxTnsState::new();
let modes = vec![0_u8, 1, 2, 3];
let r = chirp_factors(&modes, &state);
assert_eq!(r.chirp_arr[0], 0.0);
assert!((r.chirp_arr[1] - 0.54375).abs() < 1e-6);
assert!((r.chirp_arr[2] - 0.815625).abs() < 1e-6);
assert!((r.chirp_arr[3] - 0.888_125).abs() < 1e-6);
}
#[test]
fn chirp_decay_branch_uses_75_25() {
let mut state = AspxTnsState::new();
state.tna_mode_prev = vec![3]; state.chirp_prev = vec![0.9]; let r = chirp_factors(&[0_u8], &state);
assert!((r.chirp_arr[0] - 0.225).abs() < 1e-6);
}
#[test]
fn chirp_zero_gate_at_under_one_64th() {
let mut state = AspxTnsState::new();
state.tna_mode_prev = vec![0];
state.chirp_prev = vec![0.01]; let r = chirp_factors(&[0_u8], &state);
assert_eq!(r.chirp_arr[0], 0.0);
}
#[test]
fn chirp_state_advances_across_intervals() {
let mut state = AspxTnsState::new();
let modes1 = vec![3_u8];
let r1 = chirp_factors(&modes1, &state);
advance_tns_state(&mut state, &r1);
assert_eq!(state.tna_mode_prev, vec![3]);
assert_eq!(state.chirp_prev, r1.chirp_arr);
let modes2 = vec![3_u8];
let r2 = chirp_factors(&modes2, &state);
assert!(r2.chirp_arr[0] > r1.chirp_arr[0]);
assert!(r2.chirp_arr[0] < 0.98);
}
#[test]
fn covariance_zero_input_yields_zero_alphas() {
let sba = 4_u32;
let q_low = (0..sba)
.map(|_| vec![(0.0_f32, 0.0); 32])
.collect::<Vec<_>>();
let q_low_ext = build_q_low_ext(&q_low, &[], sba);
let cov = compute_covariance(&q_low_ext, sba);
let (a0, a1) = compute_alphas(&cov);
for sb in 0..(sba as usize) {
assert_eq!(a0[sb], (0.0, 0.0));
assert_eq!(a1[sb], (0.0, 0.0));
}
}
#[test]
fn covariance_complex_exponential_predicts_correctly() {
let sba = 1_u32;
let n_ts = 64;
let w = std::f32::consts::PI / 8.0;
let mut row = Vec::with_capacity(n_ts);
for ts in 0..n_ts {
let phi = w * ts as f32;
row.push((phi.cos(), phi.sin()));
}
let q_low = vec![row];
let q_low_ext = build_q_low_ext(&q_low, &[], sba);
let cov = compute_covariance(&q_low_ext, sba);
assert!(cov[0][1][1].0 > 0.0);
let (a0, a1) = compute_alphas(&cov);
let mag0 = (a0[0].0 * a0[0].0 + a0[0].1 * a0[0].1).sqrt();
let mag1 = (a1[0].0 * a1[0].0 + a1[0].1 * a1[0].1).sqrt();
assert!(mag0.is_finite() && mag0 < 4.0);
assert!(mag1.is_finite() && mag1 < 4.0);
}
#[test]
fn build_q_low_ext_prepends_prev_tail_and_zero_pads_first_frame() {
let q_low = vec![vec![(1.0_f32, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0)]];
let ext = build_q_low_ext(&q_low, &[], 1);
assert_eq!(ext[0][0..4], [(0.0, 0.0); 4]);
assert_eq!(ext[0][4], (1.0, 0.0));
assert_eq!(ext[0][5], (2.0, 0.0));
assert_eq!(ext[0][7], (4.0, 0.0));
let q_low_prev = vec![vec![(10.0, 0.0), (20.0, 0.0), (30.0, 0.0), (40.0, 0.0)]];
let ext2 = build_q_low_ext(&q_low, &q_low_prev, 1);
assert_eq!(ext2[0][0], (10.0, 0.0));
assert_eq!(ext2[0][3], (40.0, 0.0));
assert_eq!(ext2[0][4], (1.0, 0.0));
}
#[test]
fn preflat_gains_match_for_flat_envelope() {
let sbx = 8_u32;
let mut q_low: Vec<Vec<(f32, f32)>> = Vec::with_capacity(sbx as usize);
for _ in 0..sbx {
q_low.push(vec![(1.0_f32, 0.0); 32]);
}
let atsg_sig = vec![0_u32, 16];
let g = compute_preflat_gains(&q_low, sbx, &atsg_sig, 1);
for v in g {
assert!((v - 1.0).abs() < 1e-3, "flat gain != 1: {v}");
}
}
#[test]
fn hf_tile_tns_zero_alphas_equals_plain_tile_copy() {
let sba = 8_u32;
let sbx = sba;
let num_qmf = 16_u32;
let mut q_low: Vec<Vec<(f32, f32)>> = Vec::with_capacity(num_qmf as usize);
for sb in 0..num_qmf {
if sb < sba {
let mut row = Vec::with_capacity(8);
for ts in 0..8 {
row.push(((sb as f32 + 1.0) * (ts as f32 + 1.0), 0.0));
}
q_low.push(row);
} else {
q_low.push(vec![(0.0, 0.0); 8]);
}
}
let q_low_ext = build_q_low_ext(&q_low, &[], sba);
let patches = AspxPatchTables {
sbg_patches: vec![sbx, sbx + 4],
num_sbg_patches: 1,
sbg_patch_num_sb: vec![4],
sbg_patch_start_sb: vec![0],
};
let sbg_noise = vec![sbx, sbx + 4];
let chirp = vec![0.0_f32];
let alpha0 = vec![(0.0_f32, 0.0); sba as usize];
let alpha1 = vec![(0.0_f32, 0.0); sba as usize];
let atsg_sig = vec![0_u32, 8];
let q_high = hf_tile_tns(
&q_low_ext, &patches, &sbg_noise, &chirp, &alpha0, &alpha1, None, sbx, num_qmf,
&atsg_sig, 1,
);
for sb_off in 0u32..4 {
let sb_high = (sbx + sb_off) as usize;
for (ts, dst) in q_high[sb_high].iter().enumerate().take(8) {
let n = ts + TS_OFFSET_HFADJ;
let expected = q_low_ext[sb_off as usize][n];
assert_eq!(
*dst, expected,
"tile copy mismatch at sb_high={sb_high} ts={ts}"
);
}
}
}
#[test]
fn hf_tile_tns_with_alphas_modifies_output() {
let sba = 4_u32;
let sbx = sba;
let num_qmf = 8_u32;
let mut q_low: Vec<Vec<(f32, f32)>> = Vec::with_capacity(num_qmf as usize);
for sb in 0..num_qmf {
if sb < sba {
let mut row = Vec::with_capacity(16);
for ts in 0..16 {
let phi = std::f32::consts::PI * 0.25 * ts as f32;
row.push(((sb as f32 + 1.0) * phi.cos(), (sb as f32 + 1.0) * phi.sin()));
}
q_low.push(row);
} else {
q_low.push(vec![(0.0, 0.0); 16]);
}
}
let q_low_ext = build_q_low_ext(&q_low, &[], sba);
let patches = AspxPatchTables {
sbg_patches: vec![sbx, sbx + 4],
num_sbg_patches: 1,
sbg_patch_num_sb: vec![4],
sbg_patch_start_sb: vec![0],
};
let sbg_noise = vec![sbx, sbx + 4];
let chirp = vec![0.5_f32];
let alpha0 = vec![(0.5_f32, 0.0); sba as usize];
let alpha1 = vec![(0.0_f32, 0.0); sba as usize];
let atsg_sig = vec![0_u32, 16];
let q_with = hf_tile_tns(
&q_low_ext, &patches, &sbg_noise, &chirp, &alpha0, &alpha1, None, sbx, num_qmf,
&atsg_sig, 1,
);
let chirp0 = vec![0.0_f32];
let q_without = hf_tile_tns(
&q_low_ext, &patches, &sbg_noise, &chirp0, &alpha0, &alpha1, None, sbx, num_qmf,
&atsg_sig, 1,
);
let mut diffs = 0;
for (a_row, b_row) in q_with
.iter()
.zip(q_without.iter())
.take((sbx + 4) as usize)
.skip(sbx as usize)
{
for (a, b) in a_row.iter().zip(b_row.iter()).take(16) {
if (a.0 - b.0).abs() > 1e-6 || (a.1 - b.1).abs() > 1e-6 {
diffs += 1;
}
}
}
assert!(
diffs > 0,
"TNS with chirp=0.5 made no difference vs chirp=0"
);
}
#[test]
fn hf_tile_tns_preflat_divides_by_gain() {
let sba = 4_u32;
let sbx = sba;
let num_qmf = 8_u32;
let mut q_low: Vec<Vec<(f32, f32)>> = Vec::with_capacity(num_qmf as usize);
for sb in 0..num_qmf {
if sb < sba {
q_low.push(vec![(2.0_f32, 0.0); 8]);
} else {
q_low.push(vec![(0.0, 0.0); 8]);
}
}
let q_low_ext = build_q_low_ext(&q_low, &[], sba);
let patches = AspxPatchTables {
sbg_patches: vec![sbx, sbx + 4],
num_sbg_patches: 1,
sbg_patch_num_sb: vec![4],
sbg_patch_start_sb: vec![0],
};
let sbg_noise = vec![sbx, sbx + 4];
let chirp = vec![0.0_f32];
let alpha0 = vec![(0.0_f32, 0.0); sba as usize];
let alpha1 = vec![(0.0_f32, 0.0); sba as usize];
let gain_vec = vec![2.0_f32; sba as usize];
let atsg_sig = vec![0_u32, 8];
let q_high = hf_tile_tns(
&q_low_ext,
&patches,
&sbg_noise,
&chirp,
&alpha0,
&alpha1,
Some(&gain_vec),
sbx,
num_qmf,
&atsg_sig,
1,
);
for row in q_high.iter().take((sbx + 4) as usize).skip(sbx as usize) {
for sample in row.iter().take(8) {
assert!((sample.0 - 1.0).abs() < 1e-6);
assert!(sample.1.abs() < 1e-6);
}
}
}
#[test]
fn alphas_clamped_when_magnitude_ge_4() {
let mut cov: Vec<CovMatrix> = vec![[[(0.0, 0.0); 3]; 3]; 1];
cov[0][0][1] = (10.0, 0.0);
cov[0][0][2] = (0.0, 0.0);
cov[0][1][1] = (1.0, 0.0);
cov[0][1][2] = (1.0, 0.0);
cov[0][2][2] = (1.0, 0.0);
let (a0, a1) = compute_alphas(&cov);
assert_eq!(a0[0], (0.0, 0.0), "alpha0 should be clamped to 0");
assert_eq!(a1[0], (0.0, 0.0), "alpha1 should be clamped to 0");
}
#[test]
fn polynomial_fit_3_recovers_known_polynomial() {
let n = 16;
let mut y = vec![0.0_f64; n];
for (i, yi) in y.iter_mut().enumerate() {
let x = i as f64;
*yi = 1.0 + 2.0 * x + 3.0 * x * x + 4.0 * x * x * x;
}
let p = polynomial_fit_3(n, &y);
assert!((p[0] - 4.0).abs() < 1e-6);
assert!((p[1] - 3.0).abs() < 1e-6);
assert!((p[2] - 2.0).abs() < 1e-6);
assert!((p[3] - 1.0).abs() < 1e-6);
}
#[test]
fn tns_state_reset_clears_history() {
let mut state = AspxTnsState::new();
state.tna_mode_prev = vec![3, 1];
state.chirp_prev = vec![0.9, 0.6];
state.reset();
assert!(state.tna_mode_prev.is_empty());
assert!(state.chirp_prev.is_empty());
}
}