Skip to main content

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}