use crate::mbe_params::{MbeParams, SAMPLES_PER_FRAME};
include!(concat!(env!("OUT_DIR"), "/annex_b_analysis_window.rs"));
include!(concat!(env!("OUT_DIR"), "/annex_c_refinement_window.rs"));
include!(concat!(env!("OUT_DIR"), "/annex_d_lpf.rs"));
#[inline]
pub fn analysis_window(n: i32) -> f32 {
if !(-150..=150).contains(&n) {
return 0.0;
}
IMBE_ANALYSIS_WINDOW[(n + 150) as usize]
}
#[inline]
pub fn refinement_window(n: i32) -> f32 {
if !(-110..=110).contains(&n) {
return 0.0;
}
IMBE_REFINEMENT_WINDOW[(n + 110) as usize]
}
#[inline]
pub fn lpf_tap(n: i32) -> f32 {
if !(-10..=10).contains(&n) {
return 0.0;
}
IMBE_ANNEX_D_LPF[(n + 10) as usize]
}
pub mod basis;
pub use basis::{
Complex64, DFT_SIZE, HarmonicBasis, WINDOW_DFT_SIZE, W_R_HALF, packed_index, signal_spectrum,
};
pub mod pitch;
pub use pitch::{
H_LPF_HALF, LookBackContext, PITCH_COLD_START, PITCH_GRID_LEN, PITCH_GRID_MAX,
PITCH_GRID_MIN, PITCH_GRID_STEP, PITCH_INPUT_HALF, PITCH_INPUT_LEN, PitchSearch,
SHARED_LPF_LEN, W_I_HALF, compute_s_lpf, compute_s_lpf_shared, decide_initial_pitch,
look_ahead, look_back, slot_s_lpf, snap_to_pitch_grid,
};
pub mod refine;
pub use refine::{
B0_MAX, L_HAT_MAX, L_HAT_MIN, N_REFINE_CANDIDATES, PitchRefinement,
REFINE_OFFSETS, harmonic_count_for, quantize_pitch_index, refine_pitch,
};
pub mod vuv;
pub use vuv::{K_HAT_MAX, VuvResult, VuvState, XI_MAX_FLOOR, band_count_for, determine_vuv};
pub mod amplitude;
pub use amplitude::{SpectralAmplitudes, band_for_harmonic, estimate_spectral_amplitudes};
pub mod predictor;
pub use predictor::{
HalfratePredictorState, L_TILDE_COLD_START, L_TILDE_COLD_START_HALFRATE,
LAMBDA_HAT_UNVOICED_BIAS, PredictionResidual, PredictorState, RHO_HALFRATE,
compute_prediction_residual, compute_prediction_residual_ambe_plus2, imbe_rho_f64,
lambda_hat_from_m_hat,
};
pub mod hpf;
pub use hpf::{HPF_POLE, HpfState};
pub mod silence;
pub use silence::SilenceDetector;
pub mod tone_detect;
pub use tone_detect::{ToneDetection, detect_dtmf, detect_single_tone, detect_tone};
pub mod profile;
pub mod pyin;
pub use pyin::{PyinHmmState, run_pyin, run_pyin_smoothed};
pub mod denoise;
pub use denoise::{NoiseSpectrum, apply_subtraction};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AnalysisError {
WrongFrameLength,
PitchOutOfRange,
SpecGap,
}
#[derive(Clone, Debug, PartialEq)]
pub enum AnalysisOutput {
Silence,
Voice(MbeParams),
}
#[derive(Clone, Debug)]
pub struct EncodeTrace {
pub p_hat_b: f64,
pub ce_b: f64,
pub p_hat_f: f64,
pub ce_f: f64,
pub p_hat_i: f64,
pub e_p_hat_i: f64,
pub pitch_search_current: PitchSearch,
pub vuv_result: Option<VuvResult>,
pub frame_energy: f64,
pub silence_eta: f64,
pub silence_decision: bool,
}
fn shared_basis() -> &'static HarmonicBasis {
use std::sync::OnceLock;
static BASIS: OnceLock<HarmonicBasis> = OnceLock::new();
BASIS.get_or_init(HarmonicBasis::new)
}
fn matched_decoder_roundtrip(
m_hat: &SpectralAmplitudes,
refinement: &PitchRefinement,
vuv: &VuvResult,
predictor: &PredictorState,
) -> Result<[f64; L_HAT_MAX as usize + 1], AnalysisError> {
use crate::imbe_wire::dequantize::{
quantize as imbe_quantize, reconstruct_amplitudes_from_bits, DecoderState,
};
let l_hat = refinement.l_hat;
let omega_0_f32 = refinement.omega_hat as f32;
let mut amplitudes = Vec::with_capacity(l_hat as usize);
let mut voiced = Vec::with_capacity(l_hat as usize);
for l in 1..=u32::from(l_hat) {
amplitudes.push(m_hat[l as usize] as f32);
let k = band_for_harmonic(l, vuv.k_hat);
let v = k >= 1 && vuv.vuv[k as usize] == 1;
voiced.push(v);
}
let params = MbeParams::new(omega_0_f32, l_hat, &voiced, &litudes)
.map_err(|_| AnalysisError::PitchOutOfRange)?;
let prev_slice = predictor.m_tilde_prev_slice();
let ds_snapshot = DecoderState::from_amplitudes(&prev_slice, predictor.l_tilde_prev());
let mut ds_for_quantize = ds_snapshot.clone();
let bits = imbe_quantize(¶ms, &mut ds_for_quantize)
.map_err(|_| AnalysisError::PitchOutOfRange)?;
let reconstructed = reconstruct_amplitudes_from_bits(&bits, l_hat, &ds_snapshot);
let mut m_tilde = [0.0f64; L_HAT_MAX as usize + 1];
m_tilde[0] = 1.0;
for i in 0..l_hat as usize {
m_tilde[i + 1] = f64::from(reconstructed[i]);
}
Ok(m_tilde)
}
#[derive(Clone, Debug)]
struct HalfrateRoundtrip {
lambda_tilde_zero: [f64; L_HAT_MAX as usize + 1],
l_tilde_zero: u8,
gamma_tilde_zero: f64,
}
fn matched_decoder_roundtrip_ambe_plus2(
m_hat: &SpectralAmplitudes,
refinement: &PitchRefinement,
vuv: &VuvResult,
predictor: &HalfratePredictorState,
) -> Result<HalfrateRoundtrip, AnalysisError> {
use crate::ambe_plus2_wire::dequantize::{
dequantize as ambe_plus2_dequantize, quantize as ambe_plus2_quantize,
DecoderState as HalfrateDecoderState,
};
let l_hat = refinement.l_hat;
let omega_0_f32 = refinement.omega_hat as f32;
let mut amplitudes = Vec::with_capacity(l_hat as usize);
let mut voiced = Vec::with_capacity(l_hat as usize);
for l in 1..=u32::from(l_hat) {
amplitudes.push(m_hat[l as usize] as f32);
let k = band_for_harmonic(l, vuv.k_hat);
let v = k >= 1 && vuv.vuv[k as usize] == 1;
voiced.push(v);
}
let params = MbeParams::new(omega_0_f32, l_hat, &voiced, &litudes)
.map_err(|_| AnalysisError::PitchOutOfRange)?;
let lambda_prev_slice = {
let lambda_arr = predictor.lambda_tilde_prev();
let n = predictor.l_tilde_prev() as usize;
let mut v = Vec::with_capacity(n + 1);
v.push(0.0);
for l in 1..=n {
v.push(lambda_arr[l]);
}
v
};
let ds_snapshot = HalfrateDecoderState::from_lambda_state(
&lambda_prev_slice,
predictor.l_tilde_prev(),
predictor.gamma_tilde_prev(),
);
let mut ds_for_quantize = ds_snapshot.clone();
let u = ambe_plus2_quantize(¶ms, &mut ds_for_quantize)
.map_err(|_| AnalysisError::PitchOutOfRange)?;
let mut ds_for_dequantize = ds_snapshot;
let _ = ambe_plus2_dequantize(&u, &mut ds_for_dequantize)
.map_err(|_| AnalysisError::PitchOutOfRange)?;
let committed_l = ds_for_dequantize.previous_l();
let lambda_snapshot = ds_for_dequantize.lambda_tilde_snapshot();
let mut lambda_tilde_zero = [0.0f64; L_HAT_MAX as usize + 1];
for l in 1..=committed_l as usize {
if l < lambda_snapshot.len() {
lambda_tilde_zero[l] = lambda_snapshot[l];
}
}
Ok(HalfrateRoundtrip {
lambda_tilde_zero,
l_tilde_zero: committed_l,
gamma_tilde_zero: ds_for_dequantize.previous_gamma(),
})
}
pub const LOOKAHEAD_LEN: usize = 3 * SAMPLES_PER_FRAME as usize;
#[derive(Clone, Debug)]
pub struct AnalysisState {
hpf: HpfState,
lookahead: [f64; LOOKAHEAD_LEN],
lookahead_fill: u8,
pitch_history: LookBackContext,
vuv: VuvState,
predictor: PredictorState,
predictor_ambe_plus2: HalfratePredictorState,
preroll: u8,
silence: SilenceDetector,
silence_detection_enabled: bool,
pitch_silence_override_enabled: bool,
default_pitch_on_silence_enabled: bool,
pyin_pitch_enabled: bool,
pyin_hmm: PyinHmmState,
spectral_subtraction_enabled: bool,
noise_spectrum: NoiseSpectrum,
amp_ema_alpha: f64,
prev_m_hat: amplitude::SpectralAmplitudes,
prev_l_hat: u8,
prev_omega_hat: f64,
prev_vuv_bits: [u8; (vuv::K_HAT_MAX + 1) as usize],
prev_k_hat: u8,
}
pub const PITCH_SILENCE_EPI_THRESH: f64 = 0.4;
pub const PITCH_SILENCE_RATIO_THRESH: f64 = 2.0;
pub const DEFAULT_PITCH_ON_SILENCE_PERIOD: f64 = 25.0;
pub const DEFAULT_PITCH_PHANTOM_THRESHOLD: f64 = 80.0;
pub const DEFAULT_PITCH_ON_SILENCE_E: f64 = 1.0;
pub const PREROLL_FRAMES: u8 = 2;
const AMP_EMA_PITCH_GATE: f64 = 0.25;
impl AnalysisState {
pub const fn new() -> Self {
Self {
hpf: HpfState::new(),
lookahead: [0.0; LOOKAHEAD_LEN],
lookahead_fill: 0,
pitch_history: LookBackContext::cold_start(),
vuv: VuvState::cold_start(),
predictor: PredictorState::cold_start(),
predictor_ambe_plus2: HalfratePredictorState::cold_start(),
preroll: 0,
silence: SilenceDetector::cold_start(),
silence_detection_enabled: false,
pitch_silence_override_enabled: false,
default_pitch_on_silence_enabled: false,
pyin_pitch_enabled: false,
pyin_hmm: PyinHmmState::new(),
spectral_subtraction_enabled: true,
noise_spectrum: NoiseSpectrum::new(),
amp_ema_alpha: 0.0,
prev_m_hat: [0.0; L_HAT_MAX as usize + 1],
prev_l_hat: 0,
prev_omega_hat: 0.0,
prev_vuv_bits: [0; (vuv::K_HAT_MAX + 1) as usize],
prev_k_hat: 0,
}
}
pub fn set_silence_detection(&mut self, enabled: bool) {
self.silence_detection_enabled = enabled;
}
#[inline]
pub fn silence_detection_enabled(&self) -> bool {
self.silence_detection_enabled
}
pub fn set_pitch_silence_override(&mut self, enabled: bool) {
self.pitch_silence_override_enabled = enabled;
}
#[inline]
pub fn pitch_silence_override_enabled(&self) -> bool {
self.pitch_silence_override_enabled
}
pub fn set_default_pitch_on_silence(&mut self, enabled: bool) {
self.default_pitch_on_silence_enabled = enabled;
}
#[inline]
pub fn default_pitch_on_silence_enabled(&self) -> bool {
self.default_pitch_on_silence_enabled
}
pub fn set_pyin_pitch(&mut self, enabled: bool) {
self.pyin_pitch_enabled = enabled;
}
#[inline]
pub fn pyin_pitch_enabled(&self) -> bool {
self.pyin_pitch_enabled
}
pub fn set_spectral_subtraction(&mut self, enabled: bool) {
self.spectral_subtraction_enabled = enabled;
}
#[inline]
pub fn spectral_subtraction_enabled(&self) -> bool {
self.spectral_subtraction_enabled
}
pub fn set_amp_ema_alpha(&mut self, alpha: f64) {
let a = if alpha.is_finite() { alpha.clamp(0.0, 1.0) } else { 0.0 };
self.amp_ema_alpha = a;
self.prev_l_hat = 0;
self.prev_omega_hat = 0.0;
}
#[inline]
pub fn amp_ema_alpha(&self) -> f64 {
self.amp_ema_alpha
}
#[inline]
pub fn silence_detector(&self) -> &SilenceDetector {
&self.silence
}
#[inline]
pub fn vuv_state_mut(&mut self) -> &mut VuvState {
&mut self.vuv
}
fn ingest_frame(&mut self, new_frame: &[f64; SAMPLES_PER_FRAME as usize]) {
let frame = SAMPLES_PER_FRAME as usize;
if (self.lookahead_fill as usize) < LOOKAHEAD_LEN / frame {
let start = (self.lookahead_fill as usize) * frame;
self.lookahead[start..start + frame].copy_from_slice(new_frame);
self.lookahead_fill += 1;
} else {
self.lookahead.copy_within(frame.., 0);
let start = LOOKAHEAD_LEN - frame;
self.lookahead[start..].copy_from_slice(new_frame);
}
}
#[inline]
fn lookahead_buf(&self) -> &[f64; LOOKAHEAD_LEN] {
&self.lookahead
}
fn extract_refinement_window(&self) -> [f64; (2 * W_R_HALF + 1) as usize] {
let center = (SAMPLES_PER_FRAME as usize) / 2;
let mut out = [0.0f64; (2 * W_R_HALF + 1) as usize];
let len = (2 * W_R_HALF + 1) as usize;
for i in 0..len {
let offset = i as i32 - W_R_HALF;
let buf_idx = center as i32 + offset;
if (0..LOOKAHEAD_LEN as i32).contains(&buf_idx) {
out[i] = self.lookahead[buf_idx as usize];
}
}
out
}
#[inline]
pub fn in_preroll(&self) -> bool {
self.preroll < PREROLL_FRAMES
}
#[inline]
pub fn preroll_counter(&self) -> u8 {
self.preroll
}
pub fn advance_preroll(&mut self) {
if self.preroll < PREROLL_FRAMES {
self.preroll += 1;
}
}
#[inline]
pub fn hpf_mut(&mut self) -> &mut HpfState {
&mut self.hpf
}
#[inline]
pub fn pitch_history(&self) -> LookBackContext {
self.pitch_history
}
pub fn commit_pitch(&mut self, p_hat_i: f64, e_p_hat_i: f64) {
self.pitch_history.prev_err_2 = self.pitch_history.prev_err_1;
self.pitch_history.prev_err_1 = e_p_hat_i;
self.pitch_history.prev_pitch = p_hat_i;
}
#[inline]
pub fn vuv_mut(&mut self) -> &mut VuvState {
&mut self.vuv
}
#[inline]
pub fn vuv(&self) -> &VuvState {
&self.vuv
}
fn apply_amp_ema(
&mut self,
m_hat: &mut amplitude::SpectralAmplitudes,
l_hat: u8,
omega_hat: f64,
vuv: &VuvResult,
) {
let alpha = self.amp_ema_alpha;
if alpha > 0.0 && alpha < 1.0 && self.prev_l_hat > 0 && self.prev_omega_hat > 0.0 {
let rel = (omega_hat - self.prev_omega_hat).abs() / self.prev_omega_hat;
if rel < AMP_EMA_PITCH_GATE {
let overlap = u32::from(l_hat.min(self.prev_l_hat));
let k_hat = vuv.k_hat;
let prev_k_hat = self.prev_k_hat;
for l in 1..=overlap {
let k_now = amplitude::band_for_harmonic(l, k_hat);
let k_prev = amplitude::band_for_harmonic(l, prev_k_hat);
if k_now == 0 || k_prev == 0 {
continue;
}
if vuv.vuv[k_now as usize] != self.prev_vuv_bits[k_prev as usize] {
continue;
}
let raw = m_hat[l as usize];
let prev = self.prev_m_hat[l as usize];
m_hat[l as usize] = alpha * raw + (1.0 - alpha) * prev;
}
}
}
self.prev_m_hat = *m_hat;
self.prev_l_hat = l_hat;
self.prev_omega_hat = omega_hat;
self.prev_vuv_bits = vuv.vuv;
self.prev_k_hat = vuv.k_hat;
}
#[inline]
fn reset_amp_ema_history(&mut self) {
self.prev_l_hat = 0;
self.prev_omega_hat = 0.0;
self.prev_k_hat = 0;
}
#[inline]
pub fn predictor_mut(&mut self) -> &mut PredictorState {
&mut self.predictor
}
#[inline]
pub fn predictor(&self) -> &PredictorState {
&self.predictor
}
pub fn freeze(&self) {
}
}
impl Default for AnalysisState {
fn default() -> Self {
Self::new()
}
}
pub fn encode(
pcm: &[i16],
state: &mut AnalysisState,
) -> Result<AnalysisOutput, AnalysisError> {
encode_with_trace(pcm, state).map(|(out, _)| out)
}
pub fn encode_with_trace(
pcm: &[i16],
state: &mut AnalysisState,
) -> Result<(AnalysisOutput, Option<EncodeTrace>), AnalysisError> {
if pcm.len() != SAMPLES_PER_FRAME as usize {
return Err(AnalysisError::WrongFrameLength);
}
let mut hpf_out = [0.0f64; SAMPLES_PER_FRAME as usize];
profile::time(profile::Stage::Hpf, || state.hpf.run_pcm(pcm, &mut hpf_out));
let (frame_energy, silent) = profile::time(profile::Stage::SilenceDetect, || {
let e: f64 = hpf_out.iter().map(|s| s * s).sum();
let s = state.silence.update(e);
(e, s)
});
profile::time(profile::Stage::Ingest, || state.ingest_frame(&hpf_out));
if state.in_preroll() {
state.advance_preroll();
return Ok((AnalysisOutput::Silence, None));
}
let (search_cur, search_f1, search_f2) =
profile::time(profile::Stage::PitchSearch, || {
let shared = compute_s_lpf_shared(state.lookahead_buf());
let mut search_f1 = PitchSearch::from_lpf_slice(slot_s_lpf(&shared, 1));
let mut search_f2 = PitchSearch::from_lpf_slice(slot_s_lpf(&shared, 2));
search_f1.enable_argmin_fast();
search_f2.enable_argmin_fast();
(
PitchSearch::from_lpf_slice(slot_s_lpf(&shared, 0)),
search_f1,
search_f2,
)
});
let (p_b, ce_b, p_f, ce_f, p_hat_i, e_p_hat_i) =
profile::time(profile::Stage::PitchTrack, || {
let (p_b, ce_b) = look_back(&search_cur, state.pitch_history);
let (p_f, ce_f) = look_ahead(&search_cur, &search_f1, &search_f2);
if state.pyin_pitch_enabled {
let buf = *state.lookahead_buf();
let (p, e) = pyin::run_pyin_smoothed(&buf, &mut state.pyin_hmm);
(p_b, ce_b, p_f, ce_f, p, e)
} else {
let p_hat_i = decide_initial_pitch(p_b, ce_b, p_f, ce_f);
let e_p_hat_i = search_cur.e_of_p(p_hat_i);
(p_b, ce_b, p_f, ce_f, p_hat_i, e_p_hat_i)
}
});
let mut trace = EncodeTrace {
p_hat_b: p_b,
ce_b,
p_hat_f: p_f,
ce_f,
p_hat_i,
e_p_hat_i,
pitch_search_current: search_cur.clone(),
vuv_result: None,
frame_energy,
silence_eta: state.silence.noise_floor(),
silence_decision: silent,
};
let basis = shared_basis();
let sw = profile::time(profile::Stage::SignalSpectrum, || {
let sig_win = state.extract_refinement_window();
signal_spectrum(&sig_win)
});
state
.noise_spectrum
.update(&sw, silent, frame_energy, state.silence.noise_floor());
let refinement = profile::time(profile::Stage::RefinePitch, || {
refine_pitch(&sw, basis, p_hat_i)
});
let vuv_result = profile::time(profile::Stage::Vuv, || {
determine_vuv(&sw, basis, &refinement, e_p_hat_i, &mut state.vuv)
});
trace.vuv_result = Some(vuv_result.clone());
let sw_for_amplitude = if state.spectral_subtraction_enabled {
apply_subtraction(&sw, &mut state.noise_spectrum)
} else {
sw
};
let mut m_hat = profile::time(profile::Stage::Amplitude, || {
estimate_spectral_amplitudes(&sw_for_amplitude, basis, &refinement, &vuv_result)
});
state.apply_amp_ema(&mut m_hat, refinement.l_hat, refinement.omega_hat, &vuv_result);
let override_silent = state.silence_detection_enabled
&& state.pitch_silence_override_enabled
&& e_p_hat_i >= PITCH_SILENCE_EPI_THRESH
&& frame_energy < PITCH_SILENCE_RATIO_THRESH * state.silence.noise_floor();
let phantom_pitch =
p_hat_i > DEFAULT_PITCH_PHANTOM_THRESHOLD && e_p_hat_i >= PITCH_SILENCE_EPI_THRESH;
let (commit_p, commit_e) = if state.default_pitch_on_silence_enabled
&& silent
&& phantom_pitch
{
(DEFAULT_PITCH_ON_SILENCE_PERIOD, DEFAULT_PITCH_ON_SILENCE_E)
} else {
(p_hat_i, e_p_hat_i)
};
if (state.silence_detection_enabled && silent) || override_silent {
state.reset_amp_ema_history();
state.commit_pitch(commit_p, commit_e);
state.advance_preroll();
return Ok((AnalysisOutput::Silence, Some(trace)));
}
let m_tilde = profile::time(profile::Stage::MatchedDecoder, || {
matched_decoder_roundtrip(&m_hat, &refinement, &vuv_result, &state.predictor)
})?;
let params = profile::time(profile::Stage::Tail, || {
state.predictor.commit(&m_tilde, refinement.l_hat);
state.commit_pitch(commit_p, commit_e);
let mut amplitudes = Vec::with_capacity(refinement.l_hat as usize);
let mut voiced = Vec::with_capacity(refinement.l_hat as usize);
for l in 1..=u32::from(refinement.l_hat) {
amplitudes.push(m_hat[l as usize] as f32);
let k = band_for_harmonic(l, vuv_result.k_hat);
voiced.push(k >= 1 && vuv_result.vuv[k as usize] == 1);
}
let params = MbeParams::new(
refinement.omega_hat as f32,
refinement.l_hat,
&voiced,
&litudes,
)?;
state.advance_preroll();
Ok::<MbeParams, crate::mbe_params::MbeParamsError>(params)
})
.map_err(|_| AnalysisError::PitchOutOfRange)?;
Ok((AnalysisOutput::Voice(params), Some(trace)))
}
pub fn encode_ambe_plus2(
pcm: &[i16],
state: &mut AnalysisState,
) -> Result<AnalysisOutput, AnalysisError> {
if pcm.len() != SAMPLES_PER_FRAME as usize {
return Err(AnalysisError::WrongFrameLength);
}
let mut hpf_out = [0.0f64; SAMPLES_PER_FRAME as usize];
profile::time(profile::Stage::Hpf, || state.hpf.run_pcm(pcm, &mut hpf_out));
let (frame_energy, silent) = profile::time(profile::Stage::SilenceDetect, || {
let e: f64 = hpf_out.iter().map(|s| s * s).sum();
let s = state.silence.update(e);
(e, s)
});
profile::time(profile::Stage::Ingest, || state.ingest_frame(&hpf_out));
if state.in_preroll() {
state.advance_preroll();
return Ok(AnalysisOutput::Silence);
}
let (search_cur, search_f1, search_f2) =
profile::time(profile::Stage::PitchSearch, || {
let shared = compute_s_lpf_shared(state.lookahead_buf());
let mut search_f1 = PitchSearch::from_lpf_slice(slot_s_lpf(&shared, 1));
let mut search_f2 = PitchSearch::from_lpf_slice(slot_s_lpf(&shared, 2));
search_f1.enable_argmin_fast();
search_f2.enable_argmin_fast();
(
PitchSearch::from_lpf_slice(slot_s_lpf(&shared, 0)),
search_f1,
search_f2,
)
});
let (p_hat_i, e_p_hat_i) = profile::time(profile::Stage::PitchTrack, || {
if state.pyin_pitch_enabled {
let buf = *state.lookahead_buf();
pyin::run_pyin_smoothed(&buf, &mut state.pyin_hmm)
} else {
let (p_b, ce_b) = look_back(&search_cur, state.pitch_history);
let (p_f, ce_f) = look_ahead(&search_cur, &search_f1, &search_f2);
let p_hat_i = decide_initial_pitch(p_b, ce_b, p_f, ce_f);
let e_p_hat_i = search_cur.e_of_p(p_hat_i);
(p_hat_i, e_p_hat_i)
}
});
let phantom_pitch =
p_hat_i > DEFAULT_PITCH_PHANTOM_THRESHOLD && e_p_hat_i >= PITCH_SILENCE_EPI_THRESH;
let (commit_p, commit_e) = if state.default_pitch_on_silence_enabled
&& silent
&& phantom_pitch
{
(DEFAULT_PITCH_ON_SILENCE_PERIOD, DEFAULT_PITCH_ON_SILENCE_E)
} else {
(p_hat_i, e_p_hat_i)
};
let basis = shared_basis();
let sw = profile::time(profile::Stage::SignalSpectrum, || {
let sig_win = state.extract_refinement_window();
signal_spectrum(&sig_win)
});
state
.noise_spectrum
.update(&sw, silent, frame_energy, state.silence.noise_floor());
let refinement_result =
profile::time(profile::Stage::RefinePitch, || -> Result<Option<refine::PitchRefinement>, AnalysisError> {
let mut refinement = refine_pitch(&sw, basis, p_hat_i);
use crate::ambe_plus2_wire::dequantize::{decode_pitch, encode_pitch};
let Some(b0) = encode_pitch(refinement.omega_hat as f32) else {
return Ok(None);
};
refinement.l_hat = decode_pitch(b0).ok_or(AnalysisError::PitchOutOfRange)?.l;
Ok(Some(refinement))
});
let refinement = match refinement_result? {
Some(r) => r,
None => {
state.reset_amp_ema_history();
state.commit_pitch(commit_p, commit_e);
state.advance_preroll();
return Ok(AnalysisOutput::Silence);
}
};
let vuv_result = profile::time(profile::Stage::Vuv, || {
determine_vuv(&sw, basis, &refinement, e_p_hat_i, &mut state.vuv)
});
let sw_for_amplitude = if state.spectral_subtraction_enabled {
apply_subtraction(&sw, &mut state.noise_spectrum)
} else {
sw
};
let mut m_hat = profile::time(profile::Stage::Amplitude, || {
estimate_spectral_amplitudes(&sw_for_amplitude, basis, &refinement, &vuv_result)
});
state.apply_amp_ema(&mut m_hat, refinement.l_hat, refinement.omega_hat, &vuv_result);
let override_silent = state.silence_detection_enabled
&& state.pitch_silence_override_enabled
&& e_p_hat_i >= PITCH_SILENCE_EPI_THRESH
&& frame_energy < PITCH_SILENCE_RATIO_THRESH * state.silence.noise_floor();
if (state.silence_detection_enabled && silent) || override_silent {
state.reset_amp_ema_history();
state.commit_pitch(commit_p, commit_e);
state.advance_preroll();
return Ok(AnalysisOutput::Silence);
}
let rt = profile::time(profile::Stage::MatchedDecoder, || {
matched_decoder_roundtrip_ambe_plus2(
&m_hat,
&refinement,
&vuv_result,
&state.predictor_ambe_plus2,
)
})?;
let params = profile::time(profile::Stage::Tail, || -> Result<MbeParams, AnalysisError> {
state.predictor_ambe_plus2.commit(
&rt.lambda_tilde_zero,
rt.l_tilde_zero,
rt.gamma_tilde_zero,
);
state.commit_pitch(commit_p, commit_e);
let mut amplitudes = Vec::with_capacity(refinement.l_hat as usize);
let mut voiced = Vec::with_capacity(refinement.l_hat as usize);
for l in 1..=u32::from(refinement.l_hat) {
amplitudes.push(m_hat[l as usize] as f32);
let k = band_for_harmonic(l, vuv_result.k_hat);
voiced.push(k >= 1 && vuv_result.vuv[k as usize] == 1);
}
MbeParams::new(
refinement.omega_hat as f32,
refinement.l_hat,
&voiced,
&litudes,
)
.map_err(|_| AnalysisError::PitchOutOfRange)
})?;
state.advance_preroll();
Ok(AnalysisOutput::Voice(params))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn analysis_window_symmetry_and_endpoints() {
assert_eq!(ANALYSIS_WINDOW_LEN, 301);
assert_eq!(analysis_window(-151), 0.0);
assert_eq!(analysis_window(151), 0.0);
for n in 1..=150 {
assert!(
(analysis_window(-n) - analysis_window(n)).abs() < 1e-5,
"wI(-{n}) != wI({n})"
);
}
}
#[test]
fn refinement_window_peak_and_symmetry() {
assert_eq!(REFINEMENT_WINDOW_LEN, 221);
assert!((refinement_window(0) - 1.0).abs() < 1e-6);
assert_eq!(refinement_window(-111), 0.0);
assert_eq!(refinement_window(111), 0.0);
for n in 1..=110 {
assert!(
(refinement_window(-n) - refinement_window(n)).abs() < 1e-5,
"wR(-{n}) != wR({n})"
);
}
}
#[test]
fn lpf_symmetry_and_support() {
assert_eq!(ANNEX_D_LPF_LEN, 21);
for n in 1..=10 {
assert!((lpf_tap(-n) - lpf_tap(n)).abs() < 1e-6, "h_LPF(-{n}) != h_LPF({n})");
}
assert_eq!(lpf_tap(-11), 0.0);
assert_eq!(lpf_tap(11), 0.0);
}
#[test]
fn encode_rejects_wrong_length() {
let mut s = AnalysisState::new();
assert_eq!(
encode(&[0i16; 159], &mut s),
Err(AnalysisError::WrongFrameLength)
);
assert_eq!(
encode(&[0i16; 161], &mut s),
Err(AnalysisError::WrongFrameLength)
);
}
#[test]
fn encode_emits_silence_during_preroll() {
let mut s = AnalysisState::new();
let pcm = [0i16; SAMPLES_PER_FRAME as usize];
assert_eq!(encode(&pcm, &mut s), Ok(AnalysisOutput::Silence));
assert_eq!(encode(&pcm, &mut s), Ok(AnalysisOutput::Silence));
}
fn periodic_pcm_frame(period: f64, max_h: u32, phase_offset: f64) -> [i16; SAMPLES_PER_FRAME as usize] {
let mut out = [0i16; SAMPLES_PER_FRAME as usize];
let omega = 2.0 * core::f64::consts::PI / period;
for (idx, slot) in out.iter_mut().enumerate() {
let nf = idx as f64 + phase_offset;
let mut s = 0.0;
for h in 1..=max_h {
s += (f64::from(h) * omega * nf).cos() / f64::from(h);
}
*slot = (s * 8000.0).clamp(i16::MIN as f64, i16::MAX as f64) as i16;
}
out
}
#[test]
fn encode_third_frame_emits_voice_on_periodic_input() {
let mut s = AnalysisState::new();
let p0 = periodic_pcm_frame(50.0, 10, 0.0);
let p1 = periodic_pcm_frame(50.0, 10, 160.0);
let p2 = periodic_pcm_frame(50.0, 10, 320.0);
assert_eq!(encode(&p0, &mut s), Ok(AnalysisOutput::Silence));
assert_eq!(encode(&p1, &mut s), Ok(AnalysisOutput::Silence));
match encode(&p2, &mut s).unwrap() {
AnalysisOutput::Voice(params) => {
let omega = params.omega_0();
assert!(omega.is_finite() && omega > 0.0);
let p = 2.0 * core::f64::consts::PI as f32 / omega;
assert!(
(19.875..=123.125).contains(&p),
"P = {p} out of admissible range"
);
let l = params.harmonic_count();
assert!((L_HAT_MIN..=L_HAT_MAX).contains(&l));
let any_nonzero = (0..l as usize)
.any(|i| params.amplitudes_slice()[i].abs() > 1e-6);
assert!(any_nonzero, "all amplitudes zero on periodic input");
}
other => panic!("expected Voice, got {other:?}"),
}
}
#[test]
fn encode_ambe_plus2_third_frame_emits_voice_on_periodic_input() {
let mut s = AnalysisState::new();
let p0 = periodic_pcm_frame(50.0, 10, 0.0);
let p1 = periodic_pcm_frame(50.0, 10, 160.0);
let p2 = periodic_pcm_frame(50.0, 10, 320.0);
assert_eq!(encode_ambe_plus2(&p0, &mut s), Ok(AnalysisOutput::Silence));
assert_eq!(encode_ambe_plus2(&p1, &mut s), Ok(AnalysisOutput::Silence));
match encode_ambe_plus2(&p2, &mut s).unwrap() {
AnalysisOutput::Voice(params) => {
let omega = params.omega_0();
assert!(omega.is_finite() && omega > 0.0);
let l = params.harmonic_count();
assert!((L_HAT_MIN..=L_HAT_MAX).contains(&l));
let any_nonzero = (0..l as usize)
.any(|i| params.amplitudes_slice()[i].abs() > 1e-6);
assert!(any_nonzero, "all amplitudes zero on periodic input");
}
other => panic!("expected Voice, got {other:?}"),
}
}
#[test]
fn encode_ambe_plus2_predictor_advances_across_frames() {
let mut s = AnalysisState::new();
assert_eq!(s.predictor_ambe_plus2.l_tilde_prev(), 15);
assert_eq!(s.predictor_ambe_plus2.gamma_tilde_prev(), 0.0);
for i in 0..5 {
let pcm = periodic_pcm_frame(50.0, 10, 160.0 * i as f64);
let _ = encode_ambe_plus2(&pcm, &mut s).unwrap();
}
let advanced = s.predictor_ambe_plus2.l_tilde_prev() != 15
|| s.predictor_ambe_plus2.gamma_tilde_prev() != 0.0
|| s.predictor_ambe_plus2.lambda_tilde_prev()[1] != 1.0;
assert!(advanced, "half-rate predictor did not advance past cold-start");
}
#[test]
fn encode_ambe_plus2_matches_encode_on_frontend_outputs() {
let mut s_full = AnalysisState::new();
let mut s_half = AnalysisState::new();
for i in 0..2 {
let pcm = periodic_pcm_frame(50.0, 10, 160.0 * i as f64);
let _ = encode(&pcm, &mut s_full).unwrap();
let _ = encode_ambe_plus2(&pcm, &mut s_half).unwrap();
}
let p2 = periodic_pcm_frame(50.0, 10, 320.0);
let out_full = encode(&p2, &mut s_full).unwrap();
let out_half = encode_ambe_plus2(&p2, &mut s_half).unwrap();
let (pf, ph) = match (&out_full, &out_half) {
(AnalysisOutput::Voice(pf), AnalysisOutput::Voice(ph)) => (pf, ph),
_ => panic!("expected Voice from both, got full={out_full:?} half={out_half:?}"),
};
assert_eq!(pf.omega_0(), ph.omega_0(), "ω̂_0 drift across encoders");
assert_eq!(
pf.harmonic_count(),
ph.harmonic_count(),
"L̂ drift across encoders"
);
for i in 0..pf.harmonic_count() as usize {
assert_eq!(
pf.voiced_slice()[i],
ph.voiced_slice()[i],
"voicing mismatch at l={}",
i + 1
);
assert_eq!(
pf.amplitudes_slice()[i],
ph.amplitudes_slice()[i],
"M̂_l mismatch at l={}",
i + 1
);
}
}
#[test]
fn encode_updates_predictor_state_after_first_real_frame() {
let mut s = AnalysisState::new();
let p0 = periodic_pcm_frame(50.0, 10, 0.0);
let p1 = periodic_pcm_frame(50.0, 10, 160.0);
let p2 = periodic_pcm_frame(50.0, 10, 320.0);
let _ = encode(&p0, &mut s);
let _ = encode(&p1, &mut s);
let cold_m_tilde_0: f64 = 1.0; let cold_m_tilde_1: f64 = s.predictor().read(1);
assert_eq!(cold_m_tilde_1, 1.0, "pre-voice state should be cold");
let _ = encode(&p2, &mut s).unwrap();
let any_moved = (1..=s.predictor().l_tilde_prev())
.any(|l| (s.predictor().read(u32::from(l)) - 1.0).abs() > 1e-3);
assert!(any_moved, "predictor state did not evolve after voice emission");
assert_eq!(s.predictor().read(0), cold_m_tilde_0); }
#[test]
fn encode_preroll_is_exactly_two_frames() {
let mut s = AnalysisState::new();
let pcm = [0i16; SAMPLES_PER_FRAME as usize];
for i in 0..PREROLL_FRAMES {
assert_eq!(
encode(&pcm, &mut s),
Ok(AnalysisOutput::Silence),
"frame {i} should be silence during preroll"
);
}
let result = encode(&pcm, &mut s);
match result {
Ok(AnalysisOutput::Voice(_)) | Ok(AnalysisOutput::Silence) | Err(_) => {}
}
}
#[test]
fn encode_with_silence_detection_disabled_never_emits_non_preroll_silence() {
let mut s = AnalysisState::new();
assert!(!s.silence_detection_enabled());
let zero_pcm = [0i16; SAMPLES_PER_FRAME as usize];
let _ = encode(&zero_pcm, &mut s);
let _ = encode(&zero_pcm, &mut s);
let mut silence_count = 0u32;
let mut other_count = 0u32;
for _ in 0..20 {
match encode(&zero_pcm, &mut s) {
Ok(AnalysisOutput::Silence) => silence_count += 1,
_ => other_count += 1,
}
}
assert_eq!(silence_count, 0);
assert_eq!(other_count, 20);
}
#[test]
fn encode_pitch_silence_override_fires_without_hysteresis() {
let mut s_on = AnalysisState::new();
s_on.set_silence_detection(true);
s_on.set_pitch_silence_override(true);
let loud = periodic_pcm_frame(50.0, 10, 0.0);
for _ in 0..20 {
let _ = encode(&loud, &mut s_on);
}
let zero_pcm = [0i16; SAMPLES_PER_FRAME as usize];
let mut first_silence: Option<usize> = None;
for i in 0..5 {
let out = encode(&zero_pcm, &mut s_on).unwrap();
if matches!(out, AnalysisOutput::Silence) {
first_silence = Some(i);
break;
}
}
let Some(idx) = first_silence else {
panic!("override never dispatched silence across 5 zero frames");
};
assert!(
idx <= 4,
"override fired at zero-frame #{idx}; expected ≤ 4"
);
}
#[test]
fn default_pitch_on_silence_does_not_change_current_frame_output() {
let mut s_off = AnalysisState::new();
let mut s_on = AnalysisState::new();
s_on.set_default_pitch_on_silence(true);
let loud = periodic_pcm_frame(50.0, 10, 0.0);
let quiet = [5i16; SAMPLES_PER_FRAME as usize];
for _ in 0..3 {
let _ = encode(&loud, &mut s_off);
let _ = encode(&loud, &mut s_on);
}
for i in 0..30 {
let frame = if i % 7 < 5 { &quiet } else { &loud };
let out_off = encode(frame, &mut s_off).unwrap();
let out_on = encode(frame, &mut s_on).unwrap();
assert_eq!(
format!("{out_off:?}"),
format!("{out_on:?}"),
"frame {i} output diverged between flag-off and flag-on \
— the override must only affect pitch_history side-effects, \
not the current frame's encoded output"
);
}
}
#[test]
fn default_pitch_on_silence_does_not_fire_on_single_quiet_frame() {
let mut s = AnalysisState::new();
s.set_default_pitch_on_silence(true);
let loud = periodic_pcm_frame(50.0, 10, 0.0);
for _ in 0..20 {
let _ = encode(&loud, &mut s);
}
let quiet = [5i16; SAMPLES_PER_FRAME as usize];
let _ = encode(&quiet, &mut s).unwrap();
assert_ne!(
s.pitch_history().prev_pitch,
DEFAULT_PITCH_ON_SILENCE_PERIOD,
"single quiet frame must not fire the override before \
the hysteresis gate trips"
);
}
#[test]
fn pitch_silence_override_requires_silence_detection() {
let mut s = AnalysisState::new();
assert!(!s.pitch_silence_override_enabled());
s.set_pitch_silence_override(true);
assert!(s.pitch_silence_override_enabled());
assert!(!s.silence_detection_enabled());
let pcm = periodic_pcm_frame(50.0, 10, 0.0);
let _ = encode(&pcm, &mut s);
let _ = encode(&pcm, &mut s);
let quiet = [5i16; SAMPLES_PER_FRAME as usize];
let out = encode(&quiet, &mut s).unwrap();
assert!(
matches!(out, AnalysisOutput::Voice(_)),
"override alone must not dispatch silence when silence \
detection is disabled"
);
}
#[test]
fn encode_with_silence_detection_enabled_gates_silent_input() {
let mut s = AnalysisState::new();
s.set_silence_detection(true);
assert!(s.silence_detection_enabled());
let zero_pcm = [0i16; SAMPLES_PER_FRAME as usize];
let mut saw_silence_after_preroll = false;
for i in 0..30 {
let out = encode(&zero_pcm, &mut s);
if i >= PREROLL_FRAMES as usize && matches!(out, Ok(AnalysisOutput::Silence)) {
saw_silence_after_preroll = true;
break;
}
}
assert!(
saw_silence_after_preroll,
"silence detector should trigger on sustained zero input"
);
}
#[test]
fn encode_silence_does_not_update_predictor_state() {
let mut s = AnalysisState::new();
s.set_silence_detection(true);
let zero_pcm = [0i16; SAMPLES_PER_FRAME as usize];
for _ in 0..30 {
let _ = encode(&zero_pcm, &mut s);
}
let l_before = s.predictor().l_tilde_prev();
let m1_before = s.predictor().read(1);
for _ in 0..5 {
match encode(&zero_pcm, &mut s) {
Ok(AnalysisOutput::Silence) => {}
Ok(AnalysisOutput::Voice(_)) => {
return;
}
Err(_) => return,
}
}
let l_after = s.predictor().l_tilde_prev();
let m1_after = s.predictor().read(1);
assert_eq!(l_before, l_after, "L̃(−1) changed across silent frames");
assert_eq!(m1_before, m1_after, "M̃_1(−1) changed across silent frames");
}
#[test]
fn encode_rejects_wrong_length_in_preroll() {
let mut s = AnalysisState::new();
assert_eq!(
encode(&[0i16; 159], &mut s),
Err(AnalysisError::WrongFrameLength)
);
assert_eq!(
encode(&[0i16; 161], &mut s),
Err(AnalysisError::WrongFrameLength)
);
}
#[test]
fn analysis_state_cold_start_is_in_preroll() {
let s = AnalysisState::new();
assert!(s.in_preroll());
assert_eq!(s.preroll_counter(), 0);
assert_eq!(s.predictor().l_tilde_prev(), L_TILDE_COLD_START);
assert_eq!(s.vuv().xi_max(), XI_MAX_FLOOR);
assert_eq!(s.pitch_history().prev_pitch, PITCH_COLD_START);
}
#[test]
fn analysis_state_preroll_saturates_at_two() {
let mut s = AnalysisState::new();
s.advance_preroll();
assert_eq!(s.preroll_counter(), 1);
assert!(s.in_preroll());
s.advance_preroll();
assert_eq!(s.preroll_counter(), PREROLL_FRAMES);
assert!(!s.in_preroll());
s.advance_preroll();
assert_eq!(s.preroll_counter(), PREROLL_FRAMES);
s.advance_preroll();
assert_eq!(s.preroll_counter(), PREROLL_FRAMES);
}
#[test]
fn analysis_state_commit_pitch_shifts_history() {
let mut s = AnalysisState::new();
s.commit_pitch(55.0, 0.25);
let h1 = s.pitch_history();
assert_eq!(h1.prev_pitch, 55.0);
assert_eq!(h1.prev_err_1, 0.25);
assert_eq!(h1.prev_err_2, 0.0); s.commit_pitch(60.0, 0.30);
let h2 = s.pitch_history();
assert_eq!(h2.prev_pitch, 60.0);
assert_eq!(h2.prev_err_1, 0.30);
assert_eq!(h2.prev_err_2, 0.25);
}
#[test]
fn analysis_state_hpf_mut_filters_samples() {
let mut s = AnalysisState::new();
let y0 = s.hpf_mut().step(1.0);
let y1 = s.hpf_mut().step(0.0);
assert!((y0 - 1.0).abs() < 1e-12);
assert!((y1 - (-0.01)).abs() < 1e-12);
}
#[test]
fn analysis_state_predictor_mut_allows_matched_decoder_commit() {
let mut s = AnalysisState::new();
let mut m_tilde = [0.0f64; L_HAT_MAX as usize + 1];
for l in 1..=16u32 {
m_tilde[l as usize] = f64::from(l) * 0.5;
}
s.predictor_mut().commit(&m_tilde, 16);
assert_eq!(s.predictor().l_tilde_prev(), 16);
}
fn vuv_voiced(k_hat: u8) -> VuvResult {
let mut bits = [0u8; (K_HAT_MAX + 1) as usize];
for k in 1..=k_hat {
bits[k as usize] = 1;
}
VuvResult {
k_hat,
vuv: bits,
d_k: [0.0; (K_HAT_MAX + 1) as usize],
theta_k: [0.0; (K_HAT_MAX + 1) as usize],
m_xi: 0.0,
xi_0: 0.0,
xi_max_after: XI_MAX_FLOOR,
}
}
fn vuv_unvoiced(k_hat: u8) -> VuvResult {
VuvResult {
k_hat,
vuv: [0u8; (K_HAT_MAX + 1) as usize],
d_k: [1.0; (K_HAT_MAX + 1) as usize],
theta_k: [0.0; (K_HAT_MAX + 1) as usize],
m_xi: 0.0,
xi_0: 0.0,
xi_max_after: XI_MAX_FLOOR,
}
}
#[test]
fn amp_ema_off_is_passthrough() {
let mut s = AnalysisState::new();
let mut m = [0.0f64; L_HAT_MAX as usize + 1];
m[1] = 1.0;
m[2] = 2.0;
let v = vuv_voiced(1);
s.apply_amp_ema(&mut m, 2, 0.1, &v);
assert_eq!(m[1], 1.0);
assert_eq!(m[2], 2.0);
}
#[test]
fn amp_ema_first_frame_is_passthrough() {
let mut s = AnalysisState::new();
s.set_amp_ema_alpha(0.5);
let mut m = [0.0f64; L_HAT_MAX as usize + 1];
m[1] = 4.0;
let v = vuv_voiced(1);
s.apply_amp_ema(&mut m, 1, 0.1, &v);
assert!((m[1] - 4.0).abs() < 1e-12);
}
#[test]
fn amp_ema_blends_when_pitch_and_vuv_stable() {
let mut s = AnalysisState::new();
s.set_amp_ema_alpha(0.5);
let v = vuv_voiced(1);
let mut frame_a = [0.0f64; L_HAT_MAX as usize + 1];
frame_a[1] = 10.0;
s.apply_amp_ema(&mut frame_a, 1, 0.10, &v);
let mut frame_b = [0.0f64; L_HAT_MAX as usize + 1];
frame_b[1] = 20.0;
s.apply_amp_ema(&mut frame_b, 1, 0.10, &v);
assert!((frame_b[1] - 15.0).abs() < 1e-12);
}
#[test]
fn amp_ema_resets_on_pitch_jump() {
let mut s = AnalysisState::new();
s.set_amp_ema_alpha(0.5);
let v = vuv_voiced(1);
let mut frame_a = [0.0f64; L_HAT_MAX as usize + 1];
frame_a[1] = 10.0;
s.apply_amp_ema(&mut frame_a, 1, 0.10, &v);
let mut frame_b = [0.0f64; L_HAT_MAX as usize + 1];
frame_b[1] = 20.0;
s.apply_amp_ema(&mut frame_b, 1, 0.20, &v);
assert!((frame_b[1] - 20.0).abs() < 1e-12);
}
#[test]
fn amp_ema_skips_blend_on_vuv_flip() {
let mut s = AnalysisState::new();
s.set_amp_ema_alpha(0.5);
let voiced = vuv_voiced(1);
let unvoiced = vuv_unvoiced(1);
let mut frame_a = [0.0f64; L_HAT_MAX as usize + 1];
frame_a[1] = 10.0;
s.apply_amp_ema(&mut frame_a, 1, 0.10, &voiced);
let mut frame_b = [0.0f64; L_HAT_MAX as usize + 1];
frame_b[1] = 20.0;
s.apply_amp_ema(&mut frame_b, 1, 0.10, &unvoiced);
assert!((frame_b[1] - 20.0).abs() < 1e-12);
}
#[test]
fn amp_ema_history_cleared_by_reset() {
let mut s = AnalysisState::new();
s.set_amp_ema_alpha(0.5);
let v = vuv_voiced(1);
let mut frame_a = [0.0f64; L_HAT_MAX as usize + 1];
frame_a[1] = 10.0;
s.apply_amp_ema(&mut frame_a, 1, 0.10, &v);
s.reset_amp_ema_history();
let mut frame_b = [0.0f64; L_HAT_MAX as usize + 1];
frame_b[1] = 20.0;
s.apply_amp_ema(&mut frame_b, 1, 0.10, &v);
assert!((frame_b[1] - 20.0).abs() < 1e-12);
}
}