use std::path::Path;
use decibri_resampler::{PolyphaseResampler, Resampler};
use crate::error::DecibriError;
use crate::microphone::{DenoiseModel, HighpassFilter};
use crate::sample;
pub(crate) trait Stage: Send {
fn process(&mut self, input: &[f32], out: &mut Vec<f32>) -> Result<(), DecibriError>;
fn flush(&mut self, out: &mut Vec<f32>) -> Result<(), DecibriError> {
let _ = out;
Ok(())
}
fn latency_samples(&self) -> usize {
0
}
}
struct Downmix {
channels: u16,
}
impl Downmix {
fn new(channels: u16) -> Self {
Self { channels }
}
}
impl Stage for Downmix {
fn process(&mut self, input: &[f32], out: &mut Vec<f32>) -> Result<(), DecibriError> {
out.extend(sample::downmix_to_mono(input, self.channels));
Ok(())
}
}
struct ResampleStage(PolyphaseResampler);
impl Stage for ResampleStage {
fn process(&mut self, input: &[f32], out: &mut Vec<f32>) -> Result<(), DecibriError> {
self.0.process(input, out);
Ok(())
}
fn flush(&mut self, out: &mut Vec<f32>) -> Result<(), DecibriError> {
self.0.flush(out);
Ok(())
}
fn latency_samples(&self) -> usize {
self.0.latency_samples()
}
}
pub(crate) trait InPlaceDsp: Send {
fn process_in_place(&mut self, samples: &mut [f32]);
}
struct InPlace<T: InPlaceDsp>(T);
impl<T: InPlaceDsp> Stage for InPlace<T> {
fn process(&mut self, input: &[f32], out: &mut Vec<f32>) -> Result<(), DecibriError> {
out.extend_from_slice(input);
self.0.process_in_place(out);
Ok(())
}
}
struct DcBlocker {
x_prev: f32,
y_prev: f32,
}
impl DcBlocker {
const R: f32 = 0.995;
fn new() -> Self {
Self {
x_prev: 0.0,
y_prev: 0.0,
}
}
}
impl InPlaceDsp for DcBlocker {
fn process_in_place(&mut self, samples: &mut [f32]) {
for s in samples.iter_mut() {
let x = *s;
let y = x - self.x_prev + Self::R * self.y_prev;
self.x_prev = x;
self.y_prev = y;
*s = y;
}
}
}
struct Biquad {
b0: f32,
b1: f32,
b2: f32,
a1: f32,
a2: f32,
z1: f32,
z2: f32,
}
impl Biquad {
fn highpass(cutoff_hz: f32, sample_rate_hz: f32) -> Self {
use std::f32::consts::{FRAC_1_SQRT_2, PI};
let q = FRAC_1_SQRT_2;
let w0 = 2.0 * PI * cutoff_hz / sample_rate_hz;
let cos_w0 = w0.cos();
let alpha = w0.sin() / (2.0 * q);
let b0 = (1.0 + cos_w0) / 2.0;
let b1 = -(1.0 + cos_w0);
let b2 = (1.0 + cos_w0) / 2.0;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha;
Self {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: a1 / a0,
a2: a2 / a0,
z1: 0.0,
z2: 0.0,
}
}
}
impl InPlaceDsp for Biquad {
fn process_in_place(&mut self, samples: &mut [f32]) {
for s in samples.iter_mut() {
let x = *s;
let y = self.b0 * x + self.z1;
self.z1 = self.b1 * x - self.a1 * y + self.z2;
self.z2 = self.b2 * x - self.a2 * y;
*s = y;
}
}
}
pub(crate) struct CaptureStage {
normalize: Vec<Box<dyn Stage>>,
transform: Vec<Box<dyn Stage>>,
work: Vec<f32>,
scratch: Vec<f32>,
tap: Vec<f32>,
}
impl CaptureStage {
pub(crate) fn run(&mut self, input: &[f32]) -> Result<&[f32], DecibriError> {
self.work.clear();
self.work.extend_from_slice(input);
for sample in self.work.iter_mut() {
if !sample.is_finite() {
*sample = 0.0;
}
}
Self::run_segment(&mut self.work, &mut self.scratch, &mut self.normalize)?;
self.capture_tap();
Self::run_segment(&mut self.work, &mut self.scratch, &mut self.transform)?;
Ok(&self.work)
}
fn capture_tap(&mut self) {
if !self.transform.is_empty() {
self.tap.clear();
self.tap.extend_from_slice(&self.work);
}
}
fn run_segment(
work: &mut Vec<f32>,
scratch: &mut Vec<f32>,
stages: &mut [Box<dyn Stage>],
) -> Result<(), DecibriError> {
for stage in stages.iter_mut() {
scratch.clear();
stage.process(work, scratch)?;
std::mem::swap(work, scratch);
}
Ok(())
}
pub(crate) fn flush(&mut self, out: &mut Vec<f32>) -> Result<(), DecibriError> {
self.work.clear();
Self::flush_segment(&mut self.work, &mut self.scratch, &mut self.normalize)?;
self.capture_tap();
Self::flush_segment(&mut self.work, &mut self.scratch, &mut self.transform)?;
out.extend_from_slice(&self.work);
Ok(())
}
pub(crate) fn tap(&self) -> &[f32] {
&self.tap
}
pub(crate) fn has_transform(&self) -> bool {
!self.transform.is_empty()
}
pub(crate) fn transform_latency(&self) -> usize {
self.transform.iter().map(|s| s.latency_samples()).sum()
}
fn flush_segment(
work: &mut Vec<f32>,
scratch: &mut Vec<f32>,
stages: &mut [Box<dyn Stage>],
) -> Result<(), DecibriError> {
for stage in stages.iter_mut() {
scratch.clear();
if !work.is_empty() {
stage.process(work, scratch)?;
}
stage.flush(scratch)?;
std::mem::swap(work, scratch);
}
Ok(())
}
}
pub(crate) struct Transforms<'a> {
pub dc_removal: bool,
pub denoise: Option<(DenoiseModel, &'a Path, Option<&'a Path>)>,
pub highpass: Option<HighpassFilter>,
pub agc: Option<i8>,
pub limiter: Option<f32>,
}
pub(crate) fn build_capture_stage(
device_channels: u16,
target_channels: u16,
native_rate: u32,
target_rate: u32,
transforms: Transforms<'_>,
) -> Result<Option<CaptureStage>, DecibriError> {
let Transforms {
dc_removal,
denoise,
highpass,
agc,
limiter,
} = transforms;
let mut normalize: Vec<Box<dyn Stage>> = Vec::new();
if device_channels > target_channels {
normalize.push(Box::new(Downmix::new(device_channels)));
}
if native_rate != target_rate {
let resampler = PolyphaseResampler::new(native_rate, target_rate)?;
normalize.push(Box::new(ResampleStage(resampler)));
}
let mut transform: Vec<Box<dyn Stage>> = Vec::new();
if dc_removal {
transform.push(Box::new(InPlace(DcBlocker::new())));
}
#[cfg(feature = "denoise")]
if let Some((model, path, ort_library_path)) = denoise {
transform.push(Box::new(crate::denoise::Denoise::new(
model,
path,
ort_library_path,
)?));
}
#[cfg(not(feature = "denoise"))]
let _ = denoise;
if let Some(filter) = highpass {
transform.push(Box::new(InPlace(Biquad::highpass(
filter.cutoff_hz(),
target_rate as f32,
))));
}
#[cfg(feature = "gain")]
if let Some(target_db) = agc {
transform.push(Box::new(InPlace(crate::gain::LevelControl::agc(
target_db,
target_rate,
))));
}
#[cfg(not(feature = "gain"))]
let _ = agc;
#[cfg(feature = "gain")]
if let Some(ceiling_db) = limiter {
transform.push(Box::new(InPlace(crate::gain::Limiter::new(
ceiling_db,
target_rate,
))));
}
#[cfg(not(feature = "gain"))]
let _ = limiter;
Ok(if normalize.is_empty() && transform.is_empty() {
None
} else {
Some(CaptureStage {
normalize,
transform,
work: Vec::new(),
scratch: Vec::new(),
tap: Vec::new(),
})
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_returns_none_only_for_mono_at_target_rate() {
let off = false;
assert!(
build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: off,
denoise: None,
highpass: None,
agc: None,
limiter: None,
}
)
.unwrap()
.is_none(),
"mono device at the target rate needs no chain"
);
assert!(
build_capture_stage(
2,
1,
16_000,
16_000,
Transforms {
dc_removal: off,
denoise: None,
highpass: None,
agc: None,
limiter: None,
}
)
.unwrap()
.is_some(),
"stereo device gets a chain (downmix)"
);
assert!(
build_capture_stage(
6,
1,
16_000,
16_000,
Transforms {
dc_removal: off,
denoise: None,
highpass: None,
agc: None,
limiter: None,
}
)
.unwrap()
.is_some(),
"5.1 device gets a chain (downmix)"
);
assert!(
build_capture_stage(
1,
1,
48_000,
16_000,
Transforms {
dc_removal: off,
denoise: None,
highpass: None,
agc: None,
limiter: None,
}
)
.unwrap()
.is_some(),
"mono device above the target rate gets a chain (resample)"
);
assert!(
build_capture_stage(
2,
1,
48_000,
16_000,
Transforms {
dc_removal: off,
denoise: None,
highpass: None,
agc: None,
limiter: None,
}
)
.unwrap()
.is_some(),
"stereo device above the target rate gets a chain (downmix + resample)"
);
}
#[test]
fn build_with_dc_removal_adds_transform_even_with_empty_normalize() {
let on = true;
let chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: on,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("dc_removal builds a chain even with nothing to normalize");
assert!(
chain.normalize.is_empty(),
"a mono device at the target rate needs no normalize stage"
);
assert_eq!(
chain.transform.len(),
1,
"dc_removal pushes exactly one transform stage"
);
let full = build_capture_stage(
2,
1,
48_000,
16_000,
Transforms {
dc_removal: on,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("downmix + resample + DC chain");
assert_eq!(full.normalize.len(), 2, "downmix then resample");
assert_eq!(full.transform.len(), 1, "the DC-removal step");
}
#[test]
fn downmix_chain_averages_to_mono() {
let mut chain = build_capture_stage(
2,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("stereo -> downmix chain");
let out = chain.run(&[0.5, 0.3, 0.4, 0.6]).expect("downmix runs");
assert_eq!(out.len(), 2, "stereo input halves to mono");
assert!((out[0] - 0.4).abs() < 1e-6);
assert!((out[1] - 0.5).abs() < 1e-6);
assert_eq!(out, sample::downmix_to_mono(&[0.5, 0.3, 0.4, 0.6], 2));
}
#[test]
fn resample_chain_changes_rate_and_count() {
let mut chain = build_capture_stage(
1,
1,
48_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("48k mono -> resample chain");
let input: Vec<f32> = (0..24_000).map(|n| (n as f32 * 0.01).sin()).collect();
let out = chain.run(&input).expect("resample runs").to_vec();
assert!(
out.len() <= input.len() / 3,
"downsampling never exceeds the 1:3 count: {} > {}",
out.len(),
input.len() / 3
);
assert!(
out.len() > input.len() / 4,
"downsampled count {} should be near the 1:3 ratio",
out.len()
);
}
#[test]
fn resampler_error_bridges_to_decibri_error() {
use decibri_resampler::ResamplerError;
let mapped: DecibriError = ResamplerError::RatePairUnsupported {
in_rate: 7,
out_rate: 9,
}
.into();
assert!(matches!(
mapped,
DecibriError::ResampleConfigInvalid {
in_rate: 7,
out_rate: 9
}
));
let zero: DecibriError = ResamplerError::ZeroSampleRate.into();
assert!(matches!(zero, DecibriError::ResampleConfigInvalid { .. }));
}
#[test]
fn build_capture_stage_surfaces_unsupported_rate_pair() {
let result = build_capture_stage(
1,
1,
316_800_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
);
assert!(
matches!(result, Err(DecibriError::ResampleConfigInvalid { .. })),
"an enormous native rate exceeds the resampler's filter cap"
);
}
#[test]
fn flush_drains_resampler_tail_exactly() {
use decibri_resampler::{PolyphaseResampler, Resampler};
let input: Vec<f32> = (0..24_000).map(|n| (n as f32 * 0.01).sin()).collect();
let mut reference = PolyphaseResampler::new(48_000, 16_000).unwrap();
let mut expected = Vec::new();
reference.process(&input, &mut expected);
reference.flush(&mut expected);
let mut chain = build_capture_stage(
1,
1,
48_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("48k mono -> resample chain");
let mut got = chain.run(&input).expect("process runs").to_vec();
let process_only = got.len();
let mut tail = Vec::new();
chain.flush(&mut tail).expect("flush drains the tail");
assert!(
!tail.is_empty(),
"the resampler holds a group-delay tail to drain"
);
got.extend_from_slice(&tail);
assert_eq!(
got, expected,
"chain process+flush reproduces the full resampled signal, tail included, bit for bit"
);
assert_eq!(
got.len(),
process_only + tail.len(),
"the flushed tail is appended after the process output, nothing reordered"
);
}
#[test]
fn downmix_only_flush_is_empty() {
let mut chain = build_capture_stage(
2,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("stereo -> downmix chain");
let _ = chain.run(&[0.5, 0.3, 0.4, 0.6]).expect("downmix runs");
let mut tail = Vec::new();
chain
.flush(&mut tail)
.expect("flush on a downmix-only chain");
assert!(
tail.is_empty(),
"a stateless downmix chain has no flush tail"
);
}
#[test]
fn transform_off_leaves_segment_empty_and_output_unchanged() {
let mut chain = build_capture_stage(
2,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("stereo -> downmix chain");
assert!(
chain.transform.is_empty(),
"enhancement off leaves the transform segment empty"
);
let out = chain.run(&[0.5, 0.3, 0.4, 0.6]).expect("downmix runs");
assert_eq!(
out,
sample::downmix_to_mono(&[0.5, 0.3, 0.4, 0.6], 2),
"with no transform the output is exactly the downmix"
);
}
#[test]
fn dc_removal_removes_offset_preserves_length_and_is_continuous() {
let on = true;
let mut chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: on,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("dc-only chain");
let n = 16_000;
let input = vec![0.5_f32; n];
let out = chain.run(&input).expect("dc runs").to_vec();
assert_eq!(
out.len(),
n,
"the DC step preserves the sample count exactly"
);
let settled = &out[n - n / 4..];
let mean = settled.iter().sum::<f32>() / settled.len() as f32;
assert!(
mean.abs() < 1e-3,
"a known DC offset yields a near-zero-mean output (mean {mean})"
);
let mut split = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: on,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("dc-only chain");
let mut combined = split.run(&input[..8_000]).expect("first half").to_vec();
combined.extend_from_slice(split.run(&input[8_000..]).expect("second half"));
assert_eq!(
combined, out,
"filter state carries across chunks: split processing matches one-shot"
);
}
#[test]
fn two_segment_run_and_flush_order_and_tail() {
let on = true;
let frames = 12_000;
let mut input = Vec::with_capacity(frames * 2);
for k in 0..frames {
let s = (k as f32 * 0.01).sin() + 0.5;
input.push(s);
input.push(s);
}
let mono = sample::downmix_to_mono(&input, 2);
let mut resampler = PolyphaseResampler::new(48_000, 16_000).unwrap();
let mut resampled = Vec::new();
resampler.process(&mono, &mut resampled);
resampler.flush(&mut resampled);
let mut dc = DcBlocker::new();
let mut expected = resampled.clone();
dc.process_in_place(&mut expected);
let mut chain = build_capture_stage(
2,
1,
48_000,
16_000,
Transforms {
dc_removal: on,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("downmix + resample + DC chain");
let mut got = chain.run(&input).expect("run").to_vec();
let process_only = got.len();
let mut tail = Vec::new();
chain.flush(&mut tail).expect("flush");
assert!(
!tail.is_empty(),
"the resampler still holds a group-delay tail, now flushed through the DC step"
);
got.extend_from_slice(&tail);
assert_eq!(
got, expected,
"downmix -> resample -> DC order, DC state carried across run/flush, bit for bit"
);
assert_eq!(
got.len(),
process_only + tail.len(),
"the flushed tail is appended after the run output, nothing reordered"
);
}
#[test]
fn run_skips_tap_when_no_transform() {
let mut chain = build_capture_stage(
2,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("stereo -> downmix-only chain");
assert!(
!chain.has_transform(),
"a downmix-only chain has no transform"
);
let _ = chain.run(&[0.5, 0.3, 0.4, 0.6]).expect("downmix runs");
assert!(
chain.tap().is_empty(),
"no transform: nothing is captured into the tap"
);
}
#[test]
fn run_captures_post_normalize_tap() {
let on = true;
let mut chain = build_capture_stage(
2,
1,
48_000,
16_000,
Transforms {
dc_removal: on,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("downmix + resample + DC chain");
assert!(chain.has_transform());
let frames = 6_000;
let mut input = Vec::with_capacity(frames * 2);
for k in 0..frames {
let s = (k as f32 * 0.01).sin() + 0.5;
input.push(s);
input.push(s);
}
let out = chain.run(&input).expect("run").to_vec();
let tap = chain.tap().to_vec();
let mono = sample::downmix_to_mono(&input, 2);
let mut resampler = PolyphaseResampler::new(48_000, 16_000).unwrap();
let mut expected_norm = Vec::new();
resampler.process(&mono, &mut expected_norm);
assert_eq!(
tap, expected_norm,
"the tap is exactly the post-normalize (pre-transform) signal"
);
let mut dc = DcBlocker::new();
let mut expected_out = tap.clone();
dc.process_in_place(&mut expected_out);
assert_eq!(out, expected_out, "delivered output is DC(tap)");
assert_ne!(
tap, out,
"the DC step changes the signal, so tap != delivered"
);
assert_eq!(
tap.len(),
out.len(),
"the DC step preserves length, so aligned"
);
}
#[test]
fn flush_captures_post_normalize_tail() {
let on = true;
let mut chain = build_capture_stage(
1,
1,
48_000,
16_000,
Transforms {
dc_removal: on,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("resample + DC chain");
let input: Vec<f32> = (0..24_000).map(|n| (n as f32 * 0.01).sin()).collect();
let _ = chain.run(&input).expect("run");
let mut tail = Vec::new();
chain.flush(&mut tail).expect("flush");
let tap_tail = chain.tap().to_vec();
assert!(
!tap_tail.is_empty(),
"the resampler holds a group-delay tail, captured into the tap"
);
assert_eq!(
tap_tail.len(),
tail.len(),
"the tap tail and delivered tail are aligned (DC preserves length)"
);
assert_ne!(
tap_tail, tail,
"the delivered tail is post-transform, the tap tail is pre-transform"
);
}
#[test]
fn capture_stage_is_send() {
fn assert_send<T: Send>() {}
assert_send::<CaptureStage>();
}
#[test]
fn dc_stage_is_send() {
fn assert_send<T: Send>() {}
assert_send::<InPlace<DcBlocker>>();
}
#[cfg(feature = "denoise")]
fn denoise_model_path() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("models")
.join("fastenhancer_t.onnx")
}
#[cfg(feature = "denoise")]
fn mono_signal(n: usize) -> Vec<f32> {
(0..n)
.map(|i| 0.3 * (2.0 * std::f32::consts::PI * 180.0 * i as f32 / 16_000.0).sin())
.collect()
}
#[cfg(feature = "denoise")]
#[test]
fn build_orders_denoise_after_dc() {
let path = denoise_model_path();
let model = DenoiseModel::FastEnhancerT;
let both = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: true,
denoise: Some((model, path.as_path(), None)),
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("dc + denoise builds a transform chain");
assert_eq!(
both.transform.len(),
2,
"dc_removal then denoise: two transform stages"
);
assert!(
both.normalize.is_empty(),
"a mono device at the target rate needs no normalize stage"
);
let denoise_only = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: Some((model, path.as_path(), None)),
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("denoise alone builds a transform chain");
assert_eq!(
denoise_only.transform.len(),
1,
"denoise alone: one transform stage"
);
assert!(
build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
}
)
.unwrap()
.is_none(),
"denoise off and nothing else: no chain, byte-identical passthrough"
);
}
#[cfg(feature = "denoise")]
#[test]
fn denoise_chain_is_framed_with_latency_tail() {
let path = denoise_model_path();
let mut chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: Some((DenoiseModel::FastEnhancerT, path.as_path(), None)),
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("denoise chain");
let input = mono_signal(8000);
let out = chain.run(&input).expect("denoise runs").to_vec();
assert!(!out.is_empty(), "enough input produces enhanced hops");
assert!(
out.iter().all(|s| s.is_finite()),
"enhanced output is finite"
);
assert_eq!(out.len() % 256, 0, "output is whole 256-sample hops");
let mut tail = Vec::new();
chain.flush(&mut tail).expect("flush drains the tail");
assert!(
!tail.is_empty(),
"denoise holds a latency tail to flush at close (a same-length \
transform would have none)"
);
assert!(
out.len() + tail.len() > input.len(),
"delivered (process + flush) exceeds the input by the warm-up and \
latency: denoise is length-changing overall"
);
}
#[cfg(feature = "denoise")]
#[test]
fn denoise_tap_leads_delivered_by_bounded_amount() {
let path = denoise_model_path();
let mut chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: Some((DenoiseModel::FastEnhancerT, path.as_path(), None)),
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("denoise chain");
assert!(
chain.has_transform(),
"denoise is a transform, so the tap is active"
);
let input = mono_signal(8000);
let out = chain.run(&input).expect("denoise runs").to_vec();
let tap = chain.tap().to_vec();
assert_eq!(
tap.len(),
input.len(),
"the tap is the real-time post-normalize signal (input length here)"
);
assert!(
!tap.is_empty(),
"the tap is non-empty: the detector is still fed"
);
assert!(
out.len() < tap.len(),
"the tap leads the delivered enhanced output (delivered trails by the \
buffered partial frame)"
);
assert!(
tap.len() - out.len() <= 256,
"the per-call lead is bounded by one hop"
);
}
#[test]
fn transform_latency_is_zero_without_a_framed_stage() {
let dc_only = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: true,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("dc-only chain");
assert_eq!(
dc_only.transform_latency(),
0,
"the DC blocker is same-length, so it adds no latency"
);
let downmix_resample_dc = build_capture_stage(
2,
1,
48_000,
16_000,
Transforms {
dc_removal: true,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("downmix + resample + DC chain");
assert_eq!(
downmix_resample_dc.transform_latency(),
0,
"the resampler's group delay is a normalize delay, excluded from the transform latency"
);
}
#[test]
fn resample_latency_forwards_but_does_not_enter_transform_latency() {
let expected = PolyphaseResampler::new(48_000, 16_000)
.unwrap()
.latency_samples();
assert!(
expected > 0,
"downsampling 48k -> 16k has a nonzero group delay"
);
let stage = ResampleStage(PolyphaseResampler::new(48_000, 16_000).unwrap());
assert_eq!(
stage.latency_samples(),
expected,
"the stage forwards the resampler's group delay unchanged"
);
let identity = ResampleStage(PolyphaseResampler::new(16_000, 16_000).unwrap());
assert_eq!(
identity.latency_samples(),
0,
"an identity resampler adds no delay"
);
let chain = build_capture_stage(
1,
1,
48_000,
16_000,
Transforms {
dc_removal: true,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("resample + DC chain");
assert_eq!(
chain.transform_latency(),
0,
"the resampler's group delay does not enter the transform latency"
);
}
#[cfg(feature = "denoise")]
#[test]
fn transform_latency_reports_denoise_lead() {
let path = denoise_model_path();
let chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: Some((DenoiseModel::FastEnhancerT, path.as_path(), None)),
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("denoise chain");
assert_eq!(
chain.transform_latency(),
256,
"the framed denoise stage adds one half-window (512 - 256) of latency"
);
}
fn highpass_gain_at(freq_hz: f32, filter: HighpassFilter) -> f32 {
let fs = 16_000.0_f32;
let n = 16_000usize;
let input: Vec<f32> = (0..n)
.map(|i| (2.0 * std::f32::consts::PI * freq_hz * i as f32 / fs).sin())
.collect();
let mut chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: Some(filter),
agc: None,
limiter: None,
},
)
.unwrap()
.expect("high-pass-only chain");
let out = chain.run(&input).expect("high-pass runs").to_vec();
assert_eq!(
out.len(),
input.len(),
"the biquad is same-length: one output sample per input sample"
);
let rms = |s: &[f32]| (s.iter().map(|v| v * v).sum::<f32>() / s.len() as f32).sqrt();
rms(&out[n / 2..]) / rms(&input[n / 2..])
}
#[test]
fn highpass_attenuates_below_cutoff_and_preserves_passband() {
let sub = highpass_gain_at(20.0, HighpassFilter::Hz80);
assert!(
sub < 0.15,
"20 Hz (well below the 80 Hz corner) is strongly attenuated (gain {sub})"
);
let pass = highpass_gain_at(1000.0, HighpassFilter::Hz80);
assert!(
(0.95..=1.05).contains(&pass),
"1 kHz (passband) is preserved at near unity (gain {pass})"
);
let corner = highpass_gain_at(80.0, HighpassFilter::Hz80);
assert!(
(0.60..=0.80).contains(&corner),
"80 Hz (the corner) sits near the -3 dB Butterworth point of 0.707 (gain {corner})"
);
let mut chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: None,
limiter: None,
},
)
.unwrap()
.expect("high-pass-only chain");
let n = 16_000usize;
let out = chain
.run(&vec![0.5_f32; n])
.expect("high-pass runs")
.to_vec();
let settled = &out[n - n / 4..];
let mean = settled.iter().sum::<f32>() / settled.len() as f32;
assert!(
mean.abs() < 1e-3,
"a DC offset settles to a near-zero-mean output (mean {mean})"
);
}
#[test]
fn highpass_100hz_attenuates_below_cutoff_and_preserves_passband() {
let sub = highpass_gain_at(25.0, HighpassFilter::Hz100);
assert!(
sub < 0.15,
"25 Hz (well below the 100 Hz corner) is strongly attenuated (gain {sub})"
);
let pass = highpass_gain_at(1000.0, HighpassFilter::Hz100);
assert!(
(0.95..=1.05).contains(&pass),
"1 kHz (passband) is preserved at near unity (gain {pass})"
);
let corner = highpass_gain_at(100.0, HighpassFilter::Hz100);
assert!(
(0.60..=0.80).contains(&corner),
"100 Hz (the corner) sits near the -3 dB Butterworth point of 0.707 (gain {corner})"
);
let at_80_with_100 = highpass_gain_at(80.0, HighpassFilter::Hz100);
let at_80_with_80 = highpass_gain_at(80.0, HighpassFilter::Hz80);
assert!(
at_80_with_100 < at_80_with_80,
"the 100 Hz cutoff attenuates 80 Hz more than the 80 Hz cutoff does \
({at_80_with_100} < {at_80_with_80})"
);
}
#[test]
fn highpass_state_carries_across_blocks() {
let fs = 16_000.0_f32;
let n = 4096usize;
let input: Vec<f32> = (0..n)
.map(|i| 0.5 * (2.0 * std::f32::consts::PI * 120.0 * i as f32 / fs).sin() + 0.3)
.collect();
let one_shot = {
let mut chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: None,
limiter: None,
},
)
.unwrap()
.expect("high-pass-only chain");
chain.run(&input).expect("one-shot").to_vec()
};
let mut chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: None,
limiter: None,
},
)
.unwrap()
.expect("high-pass-only chain");
let mut chunked = Vec::with_capacity(n);
for chunk in [
&input[..1000],
&input[1000..1001],
&input[1001..3333],
&input[3333..],
] {
chunked.extend_from_slice(chain.run(chunk).expect("chunk"));
}
assert_eq!(
chunked, one_shot,
"biquad state carries across block boundaries: chunked equals one-shot"
);
}
#[test]
fn highpass_off_builds_no_stage() {
assert!(
build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
}
)
.unwrap()
.is_none(),
"high-pass off and nothing else: no chain, byte-identical passthrough"
);
let downmix_only = build_capture_stage(
2,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("stereo -> downmix chain");
assert!(
downmix_only.transform.is_empty(),
"high-pass off pushes no transform stage, not a transparent filter"
);
let hp = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: None,
limiter: None,
},
)
.unwrap()
.expect("high-pass-only chain");
assert!(
hp.normalize.is_empty(),
"a mono device at the target rate needs no normalize stage"
);
assert_eq!(
hp.transform.len(),
1,
"high-pass on pushes exactly one transform stage"
);
}
#[test]
fn build_orders_highpass_after_dc() {
let chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: true,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: None,
limiter: None,
},
)
.unwrap()
.expect("dc + high-pass chain");
assert_eq!(
chain.transform.len(),
2,
"dc_removal then high-pass: two transform stages, high-pass last"
);
}
#[test]
fn highpass_adds_no_transform_latency() {
let hp = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: None,
limiter: None,
},
)
.unwrap()
.expect("high-pass-only chain");
assert_eq!(
hp.transform_latency(),
0,
"the high-pass biquad is same-length, so it adds no latency"
);
let dc_and_hp = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: true,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: None,
limiter: None,
},
)
.unwrap()
.expect("dc + high-pass chain");
assert_eq!(
dc_and_hp.transform_latency(),
0,
"DC removal and high-pass are both same-length: zero transform latency"
);
}
#[test]
fn highpass_stage_is_send() {
fn assert_send<T: Send>() {}
assert_send::<InPlace<Biquad>>();
}
#[cfg(feature = "denoise")]
#[test]
fn build_orders_highpass_after_denoise() {
let path = denoise_model_path();
let chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: true,
denoise: Some((DenoiseModel::FastEnhancerT, path.as_path(), None)),
highpass: Some(HighpassFilter::Hz80),
agc: None,
limiter: None,
},
)
.unwrap()
.expect("dc + denoise + high-pass chain");
assert_eq!(
chain.transform.len(),
3,
"dc_removal, denoise, then high-pass: three transform stages"
);
assert_eq!(
chain.transform_latency(),
256,
"high-pass after denoise adds no latency: the 256 is the denoise lead alone"
);
}
#[cfg(feature = "gain")]
#[test]
fn agc_off_builds_no_stage() {
assert!(
build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
}
)
.unwrap()
.is_none(),
"AGC off and nothing else: no chain, byte-identical passthrough"
);
let downmix_only = build_capture_stage(
2,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("stereo -> downmix chain");
assert!(
downmix_only.transform.is_empty(),
"AGC off pushes no transform stage, not a transparent stage"
);
let agc = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: Some(-18),
limiter: None,
},
)
.unwrap()
.expect("AGC-only chain");
assert!(
agc.normalize.is_empty(),
"a mono device at the target rate needs no normalize stage"
);
assert_eq!(
agc.transform.len(),
1,
"AGC on pushes exactly one transform stage"
);
}
#[cfg(feature = "gain")]
#[test]
fn build_orders_agc_after_highpass() {
let chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: true,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: Some(-18),
limiter: None,
},
)
.unwrap()
.expect("dc + high-pass + AGC chain");
assert_eq!(
chain.transform.len(),
3,
"dc_removal, high-pass, then AGC: three transform stages, AGC last"
);
}
#[cfg(feature = "gain")]
#[test]
fn agc_adds_no_transform_latency() {
let agc = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: Some(-18),
limiter: None,
},
)
.unwrap()
.expect("AGC-only chain");
assert_eq!(
agc.transform_latency(),
0,
"the level-control engine is feedback with no look-ahead: zero latency"
);
let stacked = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: true,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: Some(-18),
limiter: None,
},
)
.unwrap()
.expect("dc + high-pass + AGC chain");
assert_eq!(
stacked.transform_latency(),
0,
"DC, high-pass, and AGC are all same-length: zero transform latency"
);
}
#[cfg(feature = "gain")]
#[test]
fn agc_conditions_audio_through_the_chain() {
let mut chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: Some(-18),
limiter: None,
},
)
.unwrap()
.expect("AGC-only chain");
let amp = 0.0316_f32 * std::f32::consts::SQRT_2; let input: Vec<f32> = (0..8000)
.map(|i| amp * (2.0 * std::f32::consts::PI * 220.0 * i as f32 / 16_000.0).sin())
.collect();
let out = chain.run(&input).expect("AGC runs").to_vec();
assert_eq!(out.len(), input.len(), "AGC is same-length");
let rms = |s: &[f32]| (s.iter().map(|v| v * v).sum::<f32>() / s.len() as f32).sqrt();
let in_rms = rms(&input[input.len() - 1600..]);
let out_rms = rms(&out[out.len() - 1600..]);
assert!(
out_rms > in_rms * 2.0,
"AGC boosts the quiet input toward target (in {in_rms}, out {out_rms})"
);
}
#[cfg(feature = "gain")]
#[test]
fn agc_stage_is_send() {
fn assert_send<T: Send>() {}
assert_send::<InPlace<crate::gain::LevelControl>>();
}
#[cfg(feature = "gain")]
#[test]
fn limiter_off_builds_no_stage() {
assert!(
build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None
}
)
.unwrap()
.is_none(),
"limiter off and nothing else: no chain, byte-identical passthrough"
);
let downmix_only = build_capture_stage(
2,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("stereo -> downmix chain");
assert!(
downmix_only.transform.is_empty(),
"limiter off pushes no transform stage, not a transparent stage"
);
let limiter = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: Some(-1.0),
},
)
.unwrap()
.expect("limiter-only chain");
assert!(
limiter.normalize.is_empty(),
"a mono device at the target rate needs no normalize stage"
);
assert_eq!(
limiter.transform.len(),
1,
"limiter on pushes exactly one transform stage, standalone (AGC off)"
);
}
#[cfg(feature = "gain")]
#[test]
fn build_orders_limiter_after_agc() {
let chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: true,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: Some(-18),
limiter: Some(-1.0),
},
)
.unwrap()
.expect("dc + high-pass + AGC + limiter chain");
assert_eq!(
chain.transform.len(),
4,
"dc_removal, high-pass, AGC, then limiter: four transform stages, limiter last"
);
}
#[cfg(feature = "gain")]
#[test]
fn limiter_adds_no_transform_latency() {
let limiter = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: Some(-1.0),
},
)
.unwrap()
.expect("limiter-only chain");
assert_eq!(
limiter.transform_latency(),
0,
"the limiter is feedback with no look-ahead: zero latency"
);
let stacked = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: true,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: Some(-18),
limiter: Some(-1.0),
},
)
.unwrap()
.expect("dc + high-pass + AGC + limiter chain");
assert_eq!(
stacked.transform_latency(),
0,
"DC, high-pass, AGC, and the limiter are all same-length: zero transform latency"
);
}
#[cfg(feature = "gain")]
#[test]
fn limiter_caps_audio_through_the_chain() {
let ceiling_db = -1.0_f32;
let ceiling = 10.0_f32.powf(ceiling_db / 20.0);
let mut chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: None,
highpass: None,
agc: None,
limiter: Some(ceiling_db),
},
)
.unwrap()
.expect("limiter-only chain");
let mut input: Vec<f32> = (0..8000)
.map(|i| 0.95 * (2.0 * std::f32::consts::PI * 1_000.0 * i as f32 / 16_000.0).sin())
.collect();
input[0] = 1.0;
input[4000] = 2.0;
input[4001] = -2.0;
let out = chain.run(&input).expect("limiter runs").to_vec();
assert_eq!(out.len(), input.len(), "the limiter is same-length");
let worst = out.iter().cloned().fold(0.0_f32, |m, s| m.max(s.abs()));
assert!(
worst <= ceiling,
"no delivered sample exceeds the ceiling end to end ({worst} > {ceiling})"
);
}
#[cfg(feature = "gain")]
#[test]
fn limiter_stage_is_send() {
fn assert_send<T: Send>() {}
assert_send::<InPlace<crate::gain::Limiter>>();
}
#[test]
fn non_finite_input_does_not_poison_the_chain() {
fn run_glitched(
dc: bool,
highpass: Option<HighpassFilter>,
agc: Option<i8>,
limiter: Option<f32>,
) -> Vec<f32> {
let sr = 16_000u32;
let mut input: Vec<f32> = (0..8_000)
.map(|i| 0.3 * (2.0 * std::f32::consts::PI * 220.0 * i as f32 / sr as f32).sin())
.collect();
input[100] = f32::NAN;
input[200] = f32::INFINITY;
input[300] = f32::NEG_INFINITY;
let mut chain = build_capture_stage(
1,
1,
sr,
sr,
Transforms {
dc_removal: dc,
denoise: None,
highpass,
agc,
limiter,
},
)
.unwrap()
.expect("a conditioned config builds a chain");
let mut out = chain.run(&input).expect("run").to_vec();
let mut tail = Vec::new();
chain.flush(&mut tail).expect("flush");
out.extend_from_slice(&tail);
out
}
fn assert_clean(label: &str, out: &[f32]) {
assert!(
out.iter().all(|s| s.is_finite()),
"{label}: a non-finite input sample leaked into the output"
);
let energy_after: f32 = out[4_000..].iter().map(|s| s * s).sum();
assert!(
energy_after > 0.0,
"{label}: the stream was silenced after the glitch"
);
}
assert_clean("dc-only", &run_glitched(true, None, None, None));
assert_clean(
"highpass-only",
&run_glitched(false, Some(HighpassFilter::Hz80), None, None),
);
#[cfg(feature = "gain")]
{
assert_clean(
"agc-only (no limiter)",
&run_glitched(false, None, Some(-18), None),
);
assert_clean(
"dc+hp+agc (no limiter)",
&run_glitched(true, Some(HighpassFilter::Hz80), Some(-18), None),
);
assert_clean(
"full chain",
&run_glitched(true, Some(HighpassFilter::Hz80), Some(-18), Some(-1.0)),
);
}
}
#[cfg(feature = "gain")]
#[test]
fn non_finite_guard_is_exact_on_finite_input() {
let sr = 16_000u32;
let input: Vec<f32> = (0..4_000)
.map(|i| 0.2 * (2.0 * std::f32::consts::PI * 200.0 * i as f32 / sr as f32).sin() + 0.05)
.collect();
let mut chain = build_capture_stage(
1,
1,
sr,
sr,
Transforms {
dc_removal: true,
denoise: None,
highpass: Some(HighpassFilter::Hz80),
agc: Some(-18),
limiter: Some(-1.0),
},
)
.unwrap()
.expect("conditioned chain");
let got = chain.run(&input).expect("run").to_vec();
let mut reference = input.clone();
DcBlocker::new().process_in_place(&mut reference);
Biquad::highpass(HighpassFilter::Hz80.cutoff_hz(), sr as f32)
.process_in_place(&mut reference);
crate::gain::LevelControl::agc(-18, sr).process_in_place(&mut reference);
crate::gain::Limiter::new(-1.0, sr).process_in_place(&mut reference);
assert_eq!(
got, reference,
"the guard must not alter finite input (bit-for-bit identity)"
);
}
#[cfg(feature = "denoise")]
#[test]
fn non_finite_input_does_not_poison_denoise() {
let path = denoise_model_path();
let mut chain = build_capture_stage(
1,
1,
16_000,
16_000,
Transforms {
dc_removal: false,
denoise: Some((DenoiseModel::FastEnhancerT, path.as_path(), None)),
highpass: None,
agc: None,
limiter: None,
},
)
.unwrap()
.expect("denoise chain");
let mut input = mono_signal(8_000);
input[100] = f32::NAN;
input[200] = f32::INFINITY;
input[300] = f32::NEG_INFINITY;
let mut out = chain.run(&input).expect("run").to_vec();
let mut tail = Vec::new();
chain.flush(&mut tail).expect("flush");
out.extend_from_slice(&tail);
assert!(
out.iter().all(|s| s.is_finite()),
"denoise leaked a non-finite output sample"
);
assert!(
out.iter().map(|s| s * s).sum::<f32>() > 0.0,
"the denoise stream was silenced after the glitch"
);
}
}