flexaudio_denoise/lib.rs
1//! flexaudio-denoise — An offline noise-suppression addon using RNNoise (nnnoiseless).
2//!
3//! It does not depend on `flexaudio-core`; it accepts only normalized ±1.0, 48 kHz interleaved
4//! `&[f32]`. Model weights are embedded in the nnnoiseless crate (BSD-3-Clause), so no runtime
5//! model file or network access is needed. It targets steady noise in microphone recordings
6//! (fans, air conditioning, keyboard clicks, and similar sounds).
7//!
8//! # Latency and carry-over semantics
9//!
10//! RNNoise processes only fixed 480-sample frames (10 ms at 48 kHz), so [`Denoiser::process`]
11//! splits input into frames internally and carries any remainder into the next call. The fixed
12//! latency does not depend on call size. Output is always the input delayed by exactly
13//! [`FRAME_SIZE`] samples per channel:
14//!
15//! - `process` returns the same-length buffer in place. The first [`FRAME_SIZE`] samples per
16//! channel are latency padding (silence, 0.0).
17//! - [`Denoiser::flush`] returns the final [`FRAME_SIZE`] samples per channel and closes the
18//! stream. Total output = total input + [`FRAME_SIZE`] samples per channel.
19//!
20//! The output is bit-identical regardless of how the input is chunked.
21//!
22//! # Example
23//! ```
24//! use flexaudio_denoise::{Denoiser, FRAME_SIZE};
25//!
26//! let mut dn = Denoiser::new(1).unwrap();
27//! let mut chunk = vec![0.0f32; 1000]; // normalized ±1.0, mono, 48 kHz
28//! dn.process(&mut chunk).unwrap(); // In place (the first 480 samples are latency silence)
29//! let tail = dn.flush(); // Remaining 480 samples per channel
30//! assert_eq!(tail.len(), FRAME_SIZE);
31//! ```
32
33#![warn(missing_docs)]
34
35use std::collections::VecDeque;
36
37use nnnoiseless::DenoiseState;
38
39/// Samples per RNNoise frame per channel (10 ms at 48 kHz). Processing latency is also this fixed value.
40pub const FRAME_SIZE: usize = DenoiseState::FRAME_SIZE;
41
42/// nnnoiseless expects f32 scaled to the i16 range (±32768), so use this factor to convert to and
43/// from flexaudio's normalized ±1.0 range.
44const I16_SCALE: f32 = 32768.0;
45
46/// Noise-suppression errors.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum DenoiseError {
49 /// Channel count is out of range (only 1..=2 are supported).
50 InvalidChannels(u16),
51 /// Interleaved length is not a multiple of the channel count.
52 InvalidLength {
53 /// Length of the supplied slice.
54 len: usize,
55 /// Channel count at construction.
56 channels: u16,
57 },
58}
59
60impl std::fmt::Display for DenoiseError {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 DenoiseError::InvalidChannels(c) => {
64 write!(f, "invalid channel count {c} (expected 1 or 2)")
65 }
66 DenoiseError::InvalidLength { len, channels } => {
67 write!(
68 f,
69 "interleaved length {len} is not a multiple of channel count {channels}"
70 )
71 }
72 }
73 }
74}
75
76impl std::error::Error for DenoiseError {}
77
78/// Streaming noise suppressor. Stereo uses two independent RNNoise states, one per channel
79/// (no crosstalk between channels).
80///
81/// See the [crate-level documentation](crate) for details on latency and carry-over.
82pub struct Denoiser {
83 channels: usize,
84 /// Per-channel RNNoise state. nnnoiseless has no reset, so reset by recreating the instance
85 /// (the default embedded model is deterministic).
86 states: Vec<Box<DenoiseState<'static>>>,
87 /// Unprocessed input per channel (kept at ±1.0 scale). Prepending FRAME_SIZE of silence at
88 /// construction lets the delay line always emit as many samples as it receives.
89 in_buf: Vec<Vec<f32>>,
90 /// Processed interleaved output awaiting emission (±1.0 scale, clamped).
91 out_buf: VecDeque<f32>,
92 /// Frame-processing input scratch buffer (i16 scale, 480 samples).
93 frame_in: Vec<f32>,
94 /// Frame-processing output scratch buffer (480 samples per channel).
95 frame_out: Vec<Vec<f32>>,
96}
97
98impl Denoiser {
99 /// Create with a channel count (1 = mono, 2 = interleaved stereo).
100 pub fn new(channels: u16) -> Result<Denoiser, DenoiseError> {
101 if !(1..=2).contains(&channels) {
102 return Err(DenoiseError::InvalidChannels(channels));
103 }
104 let ch = channels as usize;
105 let mut dn = Denoiser {
106 channels: ch,
107 states: (0..ch).map(|_| DenoiseState::new()).collect(),
108 in_buf: vec![Vec::new(); ch],
109 out_buf: VecDeque::new(),
110 frame_in: vec![0.0; FRAME_SIZE],
111 frame_out: vec![vec![0.0; FRAME_SIZE]; ch],
112 };
113 dn.prime_delay();
114 Ok(dn)
115 }
116
117 /// Apply noise suppression in place to any-length interleaved samples (normalized ±1.0, 48 kHz).
118 /// Length must be a multiple of the channel count (an empty slice is a no-op).
119 ///
120 /// Internally splits input into frames of [`FRAME_SIZE`] samples per channel and carries any
121 /// remainder to the next call. Output is delayed by [`FRAME_SIZE`] samples per channel, with
122 /// silence (0.0) at the start of the stream. Chunk boundaries do not change the output sequence.
123 /// Output samples are clamped to ±1.0.
124 pub fn process(&mut self, interleaved: &mut [f32]) -> Result<(), DenoiseError> {
125 if interleaved.len() % self.channels != 0 {
126 return Err(DenoiseError::InvalidLength {
127 len: interleaved.len(),
128 channels: self.channels as u16,
129 });
130 }
131
132 // Deinterleave and append to the carry-over buffers.
133 for frame in interleaved.chunks_exact(self.channels) {
134 for (ch, &s) in frame.iter().enumerate() {
135 self.in_buf[ch].push(s);
136 }
137 }
138
139 self.process_ready_frames();
140
141 // Emit as many samples as input from the delay line. The prepended FRAME_SIZE of silence
142 // ensures "processed >= emitted + current input" always holds (the initial silence is latency).
143 for s in interleaved.iter_mut() {
144 *s = self
145 .out_buf
146 .pop_front()
147 .expect("delay line must hold enough processed samples");
148 }
149 Ok(())
150 }
151
152 /// Zero-pad the carried remainder to one frame and process it, then return the final delayed
153 /// [`FRAME_SIZE`] samples per channel as interleaved output and close the stream.
154 ///
155 /// Total output is now total input + [`FRAME_SIZE`] samples per channel (initial silence padding).
156 /// After this call, the denoiser returns to the same initial state as [`Denoiser::reset`] and
157 /// can process a new stream.
158 pub fn flush(&mut self) -> Vec<f32> {
159 if !self.in_buf[0].is_empty() {
160 for buf in &mut self.in_buf {
161 buf.resize(FRAME_SIZE, 0.0);
162 }
163 self.process_ready_frames();
164 }
165 let take = FRAME_SIZE * self.channels;
166 let mut out = Vec::with_capacity(take);
167 for _ in 0..take {
168 // After the zero-padding above, the delay line always contains at least `take` samples.
169 out.push(self.out_buf.pop_front().unwrap_or(0.0));
170 }
171 self.reset();
172 out
173 }
174
175 /// Reset the RNN state, carry-over buffers, and delay line.
176 ///
177 /// After reset, the state matches a newly created denoiser, so the same input produces the same output.
178 pub fn reset(&mut self) {
179 for st in &mut self.states {
180 *st = DenoiseState::new();
181 }
182 for buf in &mut self.in_buf {
183 buf.clear();
184 }
185 self.out_buf.clear();
186 self.prime_delay();
187 }
188
189 /// Channel count specified at construction.
190 pub fn channels(&self) -> u16 {
191 self.channels as u16
192 }
193
194 /// Preload the delay line by adding FRAME_SIZE of silence to each channel's input buffer.
195 /// Processing this silent frame yields exactly 0.0, so the first FRAME_SIZE output samples per
196 /// channel are silence padding.
197 fn prime_delay(&mut self) {
198 for buf in &mut self.in_buf {
199 buf.resize(FRAME_SIZE, 0.0);
200 }
201 }
202
203 /// Process all complete frames and append them to the delay line (out_buf).
204 fn process_ready_frames(&mut self) {
205 // All channels contain the same number of samples, so checking the first channel is enough.
206 while self.in_buf[0].len() >= FRAME_SIZE {
207 for ch in 0..self.channels {
208 // Scale from ±1.0 to the i16 range and process one frame.
209 for (dst, &src) in self.frame_in.iter_mut().zip(&self.in_buf[ch][..FRAME_SIZE]) {
210 *dst = src * I16_SCALE;
211 }
212 self.states[ch].process_frame(&mut self.frame_out[ch], &self.frame_in);
213 self.in_buf[ch].drain(..FRAME_SIZE);
214 }
215 // Scale back from the i16 range to ±1.0, interleave, and queue for output.
216 for i in 0..FRAME_SIZE {
217 for out_ch in &self.frame_out {
218 self.out_buf
219 .push_back((out_ch[i] / I16_SCALE).clamp(-1.0, 1.0));
220 }
221 }
222 }
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 /// Deterministic pseudorandom generator (LCG with Knuth's MMIX constants). Avoids a rand dependency.
231 struct Lcg(u64);
232
233 impl Lcg {
234 /// Uniform random value in [0, 1), using the top 24 bits.
235 fn next_unit(&mut self) -> f32 {
236 self.0 = self
237 .0
238 .wrapping_mul(6364136223846793005)
239 .wrapping_add(1442695040888963407);
240 ((self.0 >> 40) as f32) / (1u32 << 24) as f32
241 }
242 }
243
244 /// Uniform white noise with amplitude ±amp (fixed seed, deterministic).
245 fn white_noise(n: usize, amp: f32) -> Vec<f32> {
246 let mut lcg = Lcg(0x5EED_1234_5678_9ABC);
247 (0..n)
248 .map(|_| (lcg.next_unit() * 2.0 - 1.0) * amp)
249 .collect()
250 }
251
252 /// First-order IIR low-pass filter (y += a * (x - y)). Used to simulate steady, low-frequency
253 /// noise such as fans or air conditioning (a=0.1 corresponds to a cutoff near 800 Hz; deterministic).
254 fn lowpass(xs: &[f32], a: f32) -> Vec<f32> {
255 let mut y = 0.0f32;
256 xs.iter()
257 .map(|&x| {
258 y += a * (x - y);
259 y
260 })
261 .collect()
262 }
263
264 fn sine(n: usize, freq: f32, amp: f32) -> Vec<f32> {
265 (0..n)
266 .map(|i| (2.0 * std::f32::consts::PI * freq * i as f32 / 48_000.0).sin() * amp)
267 .collect()
268 }
269
270 fn rms(xs: &[f32]) -> f64 {
271 let sum: f64 = xs.iter().map(|&x| (x as f64) * (x as f64)).sum();
272 (sum / xs.len() as f64).sqrt()
273 }
274
275 /// Process the whole input and return output aligned to the input after removing latency (same length).
276 fn run_aligned(channels: u16, input: &[f32]) -> Vec<f32> {
277 let mut dn = Denoiser::new(channels).unwrap();
278 let mut buf = input.to_vec();
279 dn.process(&mut buf).unwrap();
280 buf.extend_from_slice(&dn.flush());
281 // The first FRAME_SIZE samples per channel (FRAME_SIZE*ch interleaved) are latency silence.
282 buf.split_off(FRAME_SIZE * channels as usize)
283 }
284
285 /// Measurement helper for choosing thresholds (not normally run): prints the RMS ratio for
286 /// each test signal. Run with `cargo test -p flexaudio-denoise -- --ignored --nocapture measure`.
287 #[test]
288 #[ignore]
289 fn measure_rms_ratios() {
290 let white = white_noise(96_000, 0.3);
291 let fan = lowpass(&white, 0.1);
292 let tone = sine(96_000, 440.0, 0.5);
293 for (name, input) in [
294 ("white noise amp=0.3", &white),
295 ("lowpass(a=0.1) noise", &fan),
296 ("440Hz sine amp=0.5", &tone),
297 ] {
298 let output = run_aligned(1, input);
299 println!(
300 "{name}: in_rms={:.6} out_rms={:.6} ratio={:.6}",
301 rms(input),
302 rms(&output),
303 rms(&output) / rms(input)
304 );
305 }
306 }
307
308 #[test]
309 fn stationary_noise_rms_strongly_reduced() {
310 // Two seconds of low-frequency steady noise at 48 kHz mono (low-pass filtered LCG white noise).
311 // This is the crate's target, similar to fan or air-conditioning noise. Measurement with
312 // measure_rms_ratios gave ratio = 0.0556 (in_rms 0.0399 → out_rms 0.0022; 0.006 for the last second).
313 // Set the threshold to 25%, about 4.5 times the measured ratio.
314 let input = lowpass(&white_noise(96_000, 0.3), 0.1);
315 let output = run_aligned(1, &input);
316 let (in_rms, out_rms) = (rms(&input), rms(&output));
317 assert!(
318 out_rms < in_rms * 0.25,
319 "stationary noise must be strongly attenuated: \
320 in_rms={in_rms:.4} out_rms={out_rms:.4}"
321 );
322 }
323
324 #[test]
325 fn white_noise_rms_reduced() {
326 // Two seconds of full-band white noise at 48 kHz mono (amplitude ±0.3). This synthetic
327 // noise is far from the training distribution, so RNNoise suppression is weak; the measured
328 // ratio is 0.7883 (stationary_noise_rms_strongly_reduced covers steady-noise effectiveness).
329 // Here, verify only that it is not amplified and is reduced by at least 10% from measurement.
330 let input = white_noise(96_000, 0.3);
331 let output = run_aligned(1, &input);
332 let (in_rms, out_rms) = (rms(&input), rms(&output));
333 assert!(
334 out_rms < in_rms * 0.90,
335 "white noise must not be amplified: in_rms={in_rms:.4} out_rms={out_rms:.4}"
336 );
337 }
338
339 #[test]
340 fn sine_output_sane() {
341 // Two seconds of a 440 Hz sine wave (amplitude 0.5). Measurement gave ratio = 0.9999,
342 // nearly unchanged (RNNoise treats periodic signals as voiced). Since pure-tone handling
343 // depends on the model, assert only valid output (no NaN, within ±1.0) and that the tone
344 // is not erased (a 50% floor, half the measured level).
345 let input = sine(96_000, 440.0, 0.5);
346 let output = run_aligned(1, &input);
347 assert!(
348 output.iter().all(|x| x.is_finite()),
349 "output must not contain NaN/inf"
350 );
351 assert!(
352 output.iter().all(|&x| (-1.0..=1.0).contains(&x)),
353 "output must stay within +/-1.0"
354 );
355 let (in_rms, out_rms) = (rms(&input), rms(&output));
356 assert!(
357 out_rms > in_rms * 0.50,
358 "sine must pass through mostly intact: in_rms={in_rms:.4} out_rms={out_rms:.4}"
359 );
360 }
361
362 #[test]
363 fn first_delay_block_is_silence() {
364 // The first FRAME_SIZE samples per channel of output are exactly 0.0 latency padding.
365 let mut dn = Denoiser::new(1).unwrap();
366 let mut buf = white_noise(FRAME_SIZE * 2, 0.3);
367 dn.process(&mut buf).unwrap();
368 assert!(
369 buf[..FRAME_SIZE].iter().all(|&x| x == 0.0),
370 "first FRAME_SIZE output samples must be exactly zero"
371 );
372 }
373
374 #[test]
375 fn chunked_equals_oneshot() {
376 // Streaming in 1000-sample chunks (not a multiple of 480) must match batch output bit for
377 // bit, regardless of call size. Also verify the total sample count.
378 let input = white_noise(96_000, 0.3);
379
380 let mut oneshot = input.clone();
381 let mut dn1 = Denoiser::new(1).unwrap();
382 dn1.process(&mut oneshot).unwrap();
383 let tail1 = dn1.flush();
384
385 let mut chunked = Vec::with_capacity(input.len());
386 let mut dn2 = Denoiser::new(1).unwrap();
387 for chunk in input.chunks(1000) {
388 let mut buf = chunk.to_vec();
389 dn2.process(&mut buf).unwrap();
390 assert_eq!(
391 buf.len(),
392 chunk.len(),
393 "process must emit in place, same length"
394 );
395 chunked.extend_from_slice(&buf);
396 }
397 let tail2 = dn2.flush();
398
399 assert_eq!(
400 chunked.len(),
401 input.len(),
402 "total process output == total input"
403 );
404 assert_eq!(
405 tail1.len(),
406 FRAME_SIZE,
407 "flush must emit exactly FRAME_SIZE per channel"
408 );
409 assert_eq!(
410 oneshot, chunked,
411 "chunk granularity must not change the output"
412 );
413 assert_eq!(tail1, tail2, "flush residue must also match");
414 }
415
416 #[test]
417 fn stereo_keeps_channels_independent_and_interleaved() {
418 // Interleaved stereo with noise on L and silence on R. R output remains exactly 0, while
419 // L output matches processing the same signal as mono bit for bit (checks interleaving and
420 // channel independence).
421 let n = 48_000; // 1 second per channel, streamed in 1000-sample chunks (=500/ch, not a multiple of 480).
422 let left = white_noise(n, 0.3);
423 let mut stereo = Vec::with_capacity(n * 2);
424 for &l in &left {
425 stereo.push(l);
426 stereo.push(0.0);
427 }
428
429 let mut dn = Denoiser::new(2).unwrap();
430 let mut stereo_out = Vec::with_capacity(stereo.len());
431 for chunk in stereo.chunks(1000) {
432 let mut buf = chunk.to_vec();
433 dn.process(&mut buf).unwrap();
434 stereo_out.extend_from_slice(&buf);
435 }
436 let tail = dn.flush();
437 assert_eq!(
438 tail.len(),
439 FRAME_SIZE * 2,
440 "stereo flush is FRAME_SIZE per channel"
441 );
442 stereo_out.extend_from_slice(&tail);
443
444 let left_out: Vec<f32> = stereo_out.iter().step_by(2).copied().collect();
445 let right_out: Vec<f32> = stereo_out.iter().skip(1).step_by(2).copied().collect();
446 assert!(
447 right_out.iter().all(|&x| x == 0.0),
448 "silent right channel must stay exactly zero (no crosstalk)"
449 );
450
451 let mut mono_ref = left.clone();
452 let mut dn_mono = Denoiser::new(1).unwrap();
453 dn_mono.process(&mut mono_ref).unwrap();
454 mono_ref.extend_from_slice(&dn_mono.flush());
455 assert_eq!(
456 left_out, mono_ref,
457 "stereo left must equal the mono reference"
458 );
459 }
460
461 #[test]
462 fn reset_restores_initial_state() {
463 // The same input after reset produces the same output bit for bit. flush also resets automatically.
464 let input = white_noise(10_000, 0.3);
465
466 let mut dn = Denoiser::new(1).unwrap();
467 let mut first = input.clone();
468 dn.process(&mut first).unwrap();
469
470 dn.reset();
471 let mut second = input.clone();
472 dn.process(&mut second).unwrap();
473 assert_eq!(first, second, "reset must restore the initial state");
474
475 // After closing the stream, flush returns to the same initial state as reset.
476 dn.flush();
477 let mut third = input.clone();
478 dn.process(&mut third).unwrap();
479 assert_eq!(first, third, "flush must leave the denoiser reusable");
480 }
481
482 #[test]
483 fn rejects_invalid_channels() {
484 assert_eq!(
485 Denoiser::new(0).err(),
486 Some(DenoiseError::InvalidChannels(0))
487 );
488 assert_eq!(
489 Denoiser::new(3).err(),
490 Some(DenoiseError::InvalidChannels(3))
491 );
492 }
493
494 #[test]
495 fn rejects_misaligned_length() {
496 let mut dn = Denoiser::new(2).unwrap();
497 let mut buf = vec![0.0f32; 999]; // Not a multiple of 2 channels.
498 let err = dn.process(&mut buf).unwrap_err();
499 assert_eq!(
500 err,
501 DenoiseError::InvalidLength {
502 len: 999,
503 channels: 2
504 }
505 );
506 // Do not modify the buffer on error.
507 assert!(buf.iter().all(|&x| x == 0.0));
508 }
509
510 #[test]
511 fn empty_process_and_bare_flush() {
512 let mut dn = Denoiser::new(1).unwrap();
513 let mut empty: [f32; 0] = [];
514 dn.process(&mut empty).unwrap(); // Empty input is a no-op.
515 assert_eq!(dn.channels(), 1);
516
517 // Flushing without input still returns exactly the latency padding (silence).
518 let tail = dn.flush();
519 assert_eq!(tail.len(), FRAME_SIZE);
520 assert!(tail.iter().all(|&x| x == 0.0));
521 }
522}