#![warn(missing_docs)]
use std::collections::VecDeque;
use nnnoiseless::DenoiseState;
pub const FRAME_SIZE: usize = DenoiseState::FRAME_SIZE;
const I16_SCALE: f32 = 32768.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DenoiseError {
InvalidChannels(u16),
InvalidLength {
len: usize,
channels: u16,
},
}
impl std::fmt::Display for DenoiseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DenoiseError::InvalidChannels(c) => {
write!(f, "invalid channel count {c} (expected 1 or 2)")
}
DenoiseError::InvalidLength { len, channels } => {
write!(
f,
"interleaved length {len} is not a multiple of channel count {channels}"
)
}
}
}
}
impl std::error::Error for DenoiseError {}
pub struct Denoiser {
channels: usize,
states: Vec<Box<DenoiseState<'static>>>,
in_buf: Vec<Vec<f32>>,
out_buf: VecDeque<f32>,
frame_in: Vec<f32>,
frame_out: Vec<Vec<f32>>,
}
impl Denoiser {
pub fn new(channels: u16) -> Result<Denoiser, DenoiseError> {
if !(1..=2).contains(&channels) {
return Err(DenoiseError::InvalidChannels(channels));
}
let ch = channels as usize;
let mut dn = Denoiser {
channels: ch,
states: (0..ch).map(|_| DenoiseState::new()).collect(),
in_buf: vec![Vec::new(); ch],
out_buf: VecDeque::new(),
frame_in: vec![0.0; FRAME_SIZE],
frame_out: vec![vec![0.0; FRAME_SIZE]; ch],
};
dn.prime_delay();
Ok(dn)
}
pub fn process(&mut self, interleaved: &mut [f32]) -> Result<(), DenoiseError> {
if interleaved.len() % self.channels != 0 {
return Err(DenoiseError::InvalidLength {
len: interleaved.len(),
channels: self.channels as u16,
});
}
for frame in interleaved.chunks_exact(self.channels) {
for (ch, &s) in frame.iter().enumerate() {
self.in_buf[ch].push(s);
}
}
self.process_ready_frames();
for s in interleaved.iter_mut() {
*s = self
.out_buf
.pop_front()
.expect("delay line must hold enough processed samples");
}
Ok(())
}
pub fn flush(&mut self) -> Vec<f32> {
if !self.in_buf[0].is_empty() {
for buf in &mut self.in_buf {
buf.resize(FRAME_SIZE, 0.0);
}
self.process_ready_frames();
}
let take = FRAME_SIZE * self.channels;
let mut out = Vec::with_capacity(take);
for _ in 0..take {
out.push(self.out_buf.pop_front().unwrap_or(0.0));
}
self.reset();
out
}
pub fn reset(&mut self) {
for st in &mut self.states {
*st = DenoiseState::new();
}
for buf in &mut self.in_buf {
buf.clear();
}
self.out_buf.clear();
self.prime_delay();
}
pub fn channels(&self) -> u16 {
self.channels as u16
}
fn prime_delay(&mut self) {
for buf in &mut self.in_buf {
buf.resize(FRAME_SIZE, 0.0);
}
}
fn process_ready_frames(&mut self) {
while self.in_buf[0].len() >= FRAME_SIZE {
for ch in 0..self.channels {
for (dst, &src) in self.frame_in.iter_mut().zip(&self.in_buf[ch][..FRAME_SIZE]) {
*dst = src * I16_SCALE;
}
self.states[ch].process_frame(&mut self.frame_out[ch], &self.frame_in);
self.in_buf[ch].drain(..FRAME_SIZE);
}
for i in 0..FRAME_SIZE {
for out_ch in &self.frame_out {
self.out_buf
.push_back((out_ch[i] / I16_SCALE).clamp(-1.0, 1.0));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Lcg(u64);
impl Lcg {
fn next_unit(&mut self) -> f32 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((self.0 >> 40) as f32) / (1u32 << 24) as f32
}
}
fn white_noise(n: usize, amp: f32) -> Vec<f32> {
let mut lcg = Lcg(0x5EED_1234_5678_9ABC);
(0..n)
.map(|_| (lcg.next_unit() * 2.0 - 1.0) * amp)
.collect()
}
fn lowpass(xs: &[f32], a: f32) -> Vec<f32> {
let mut y = 0.0f32;
xs.iter()
.map(|&x| {
y += a * (x - y);
y
})
.collect()
}
fn sine(n: usize, freq: f32, amp: f32) -> Vec<f32> {
(0..n)
.map(|i| (2.0 * std::f32::consts::PI * freq * i as f32 / 48_000.0).sin() * amp)
.collect()
}
fn rms(xs: &[f32]) -> f64 {
let sum: f64 = xs.iter().map(|&x| (x as f64) * (x as f64)).sum();
(sum / xs.len() as f64).sqrt()
}
fn run_aligned(channels: u16, input: &[f32]) -> Vec<f32> {
let mut dn = Denoiser::new(channels).unwrap();
let mut buf = input.to_vec();
dn.process(&mut buf).unwrap();
buf.extend_from_slice(&dn.flush());
buf.split_off(FRAME_SIZE * channels as usize)
}
#[test]
#[ignore]
fn measure_rms_ratios() {
let white = white_noise(96_000, 0.3);
let fan = lowpass(&white, 0.1);
let tone = sine(96_000, 440.0, 0.5);
for (name, input) in [
("white noise amp=0.3", &white),
("lowpass(a=0.1) noise", &fan),
("440Hz sine amp=0.5", &tone),
] {
let output = run_aligned(1, input);
println!(
"{name}: in_rms={:.6} out_rms={:.6} ratio={:.6}",
rms(input),
rms(&output),
rms(&output) / rms(input)
);
}
}
#[test]
fn stationary_noise_rms_strongly_reduced() {
let input = lowpass(&white_noise(96_000, 0.3), 0.1);
let output = run_aligned(1, &input);
let (in_rms, out_rms) = (rms(&input), rms(&output));
assert!(
out_rms < in_rms * 0.25,
"stationary noise must be strongly attenuated: \
in_rms={in_rms:.4} out_rms={out_rms:.4}"
);
}
#[test]
fn white_noise_rms_reduced() {
let input = white_noise(96_000, 0.3);
let output = run_aligned(1, &input);
let (in_rms, out_rms) = (rms(&input), rms(&output));
assert!(
out_rms < in_rms * 0.90,
"white noise must not be amplified: in_rms={in_rms:.4} out_rms={out_rms:.4}"
);
}
#[test]
fn sine_output_sane() {
let input = sine(96_000, 440.0, 0.5);
let output = run_aligned(1, &input);
assert!(
output.iter().all(|x| x.is_finite()),
"output must not contain NaN/inf"
);
assert!(
output.iter().all(|&x| (-1.0..=1.0).contains(&x)),
"output must stay within +/-1.0"
);
let (in_rms, out_rms) = (rms(&input), rms(&output));
assert!(
out_rms > in_rms * 0.50,
"sine must pass through mostly intact: in_rms={in_rms:.4} out_rms={out_rms:.4}"
);
}
#[test]
fn first_delay_block_is_silence() {
let mut dn = Denoiser::new(1).unwrap();
let mut buf = white_noise(FRAME_SIZE * 2, 0.3);
dn.process(&mut buf).unwrap();
assert!(
buf[..FRAME_SIZE].iter().all(|&x| x == 0.0),
"first FRAME_SIZE output samples must be exactly zero"
);
}
#[test]
fn chunked_equals_oneshot() {
let input = white_noise(96_000, 0.3);
let mut oneshot = input.clone();
let mut dn1 = Denoiser::new(1).unwrap();
dn1.process(&mut oneshot).unwrap();
let tail1 = dn1.flush();
let mut chunked = Vec::with_capacity(input.len());
let mut dn2 = Denoiser::new(1).unwrap();
for chunk in input.chunks(1000) {
let mut buf = chunk.to_vec();
dn2.process(&mut buf).unwrap();
assert_eq!(
buf.len(),
chunk.len(),
"process must emit in place, same length"
);
chunked.extend_from_slice(&buf);
}
let tail2 = dn2.flush();
assert_eq!(
chunked.len(),
input.len(),
"total process output == total input"
);
assert_eq!(
tail1.len(),
FRAME_SIZE,
"flush must emit exactly FRAME_SIZE per channel"
);
assert_eq!(
oneshot, chunked,
"chunk granularity must not change the output"
);
assert_eq!(tail1, tail2, "flush residue must also match");
}
#[test]
fn stereo_keeps_channels_independent_and_interleaved() {
let n = 48_000; let left = white_noise(n, 0.3);
let mut stereo = Vec::with_capacity(n * 2);
for &l in &left {
stereo.push(l);
stereo.push(0.0);
}
let mut dn = Denoiser::new(2).unwrap();
let mut stereo_out = Vec::with_capacity(stereo.len());
for chunk in stereo.chunks(1000) {
let mut buf = chunk.to_vec();
dn.process(&mut buf).unwrap();
stereo_out.extend_from_slice(&buf);
}
let tail = dn.flush();
assert_eq!(
tail.len(),
FRAME_SIZE * 2,
"stereo flush is FRAME_SIZE per channel"
);
stereo_out.extend_from_slice(&tail);
let left_out: Vec<f32> = stereo_out.iter().step_by(2).copied().collect();
let right_out: Vec<f32> = stereo_out.iter().skip(1).step_by(2).copied().collect();
assert!(
right_out.iter().all(|&x| x == 0.0),
"silent right channel must stay exactly zero (no crosstalk)"
);
let mut mono_ref = left.clone();
let mut dn_mono = Denoiser::new(1).unwrap();
dn_mono.process(&mut mono_ref).unwrap();
mono_ref.extend_from_slice(&dn_mono.flush());
assert_eq!(
left_out, mono_ref,
"stereo left must equal the mono reference"
);
}
#[test]
fn reset_restores_initial_state() {
let input = white_noise(10_000, 0.3);
let mut dn = Denoiser::new(1).unwrap();
let mut first = input.clone();
dn.process(&mut first).unwrap();
dn.reset();
let mut second = input.clone();
dn.process(&mut second).unwrap();
assert_eq!(first, second, "reset must restore the initial state");
dn.flush();
let mut third = input.clone();
dn.process(&mut third).unwrap();
assert_eq!(first, third, "flush must leave the denoiser reusable");
}
#[test]
fn rejects_invalid_channels() {
assert_eq!(
Denoiser::new(0).err(),
Some(DenoiseError::InvalidChannels(0))
);
assert_eq!(
Denoiser::new(3).err(),
Some(DenoiseError::InvalidChannels(3))
);
}
#[test]
fn rejects_misaligned_length() {
let mut dn = Denoiser::new(2).unwrap();
let mut buf = vec![0.0f32; 999]; let err = dn.process(&mut buf).unwrap_err();
assert_eq!(
err,
DenoiseError::InvalidLength {
len: 999,
channels: 2
}
);
assert!(buf.iter().all(|&x| x == 0.0));
}
#[test]
fn empty_process_and_bare_flush() {
let mut dn = Denoiser::new(1).unwrap();
let mut empty: [f32; 0] = [];
dn.process(&mut empty).unwrap(); assert_eq!(dn.channels(), 1);
let tail = dn.flush();
assert_eq!(tail.len(), FRAME_SIZE);
assert!(tail.iter().all(|&x| x == 0.0));
}
}