Skip to main content

flexaudio_denoise/
lib.rs

1//! flexaudio-denoise — RNNoise (nnnoiseless) によるオフラインのノイズ抑制アドオン。
2//!
3//! `flexaudio-core` には依存せず、±1.0 正規化・48kHz interleaved の `&[f32]` だけを
4//! 受け取る。モデル重みは nnnoiseless クレート (BSD-3-Clause) に埋め込まれているので、
5//! 実行時のモデルファイルもネットワークも要らない。マイク録音の定常ノイズ
6//! (ファン・空調・キーボード打鍵など) の低減を想定している。
7//!
8//! # 遅延と持ち越しのセマンティクス
9//!
10//! RNNoise は 480 サンプル (48kHz で 10ms) 固定のフレームでしか処理できないため、
11//! [`Denoiser::process`] は内部でフレームに切り、端数を次回呼び出しへ持ち越す。
12//! 呼び出し粒度に依存しない固定遅延の設計で、出力は常に「入力をちょうど
13//! [`FRAME_SIZE`] サンプル/ch 遅らせた列」になる:
14//!
15//! - `process` は渡されたバッファと同じ長さをインプレースで返す。ストリーム先頭の
16//!   [`FRAME_SIZE`] サンプル/ch は遅延の詰め物 (無音 0.0)。
17//! - [`Denoiser::flush`] が末尾の [`FRAME_SIZE`] サンプル/ch を返してストリームを
18//!   閉じる。つまり総出力 = 総入力 + [`FRAME_SIZE`] サンプル/ch。
19//!
20//! チャンクの切り方を変えても出力列はビット単位で一致する。
21//!
22//! # 例
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]; // ±1.0 正規化 mono 48kHz
28//! dn.process(&mut chunk).unwrap();    // インプレース (先頭 480 サンプルは遅延の無音)
29//! let tail = dn.flush();              // 残りの 480 サンプル/ch
30//! assert_eq!(tail.len(), FRAME_SIZE);
31//! ```
32
33#![warn(missing_docs)]
34
35use std::collections::VecDeque;
36
37use nnnoiseless::DenoiseState;
38
39/// RNNoise の 1 フレームのサンプル数/ch (48kHz で 10ms)。処理遅延もこの固定値。
40pub const FRAME_SIZE: usize = DenoiseState::FRAME_SIZE;
41
42/// nnnoiseless は i16 レンジ (±32768) スケールの f32 を想定するので、flexaudio の
43/// ±1.0 正規化とはこの係数で相互変換する。
44const I16_SCALE: f32 = 32768.0;
45
46/// ノイズ抑制のエラー型。
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum DenoiseError {
49    /// チャンネル数が範囲外 (1..=2 のみ対応)。
50    InvalidChannels(u16),
51    /// interleaved 長がチャンネル数の倍数でない。
52    InvalidLength {
53        /// 渡されたスライス長。
54        len: usize,
55        /// 構築時のチャンネル数。
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/// ストリーミングのノイズ抑制器。ステレオはチャンネル独立に 2 インスタンスの
79/// RNNoise 状態で処理する (チャンネル間のクロストークなし)。
80///
81/// 遅延と持ち越しの詳細は [crate レベルのドキュメント](crate) を参照。
82pub struct Denoiser {
83    channels: usize,
84    /// チャンネルごとの RNNoise 状態。nnnoiseless に reset が無いので、リセットは
85    /// インスタンスの作り直しで行う (モデルはデフォルト埋め込み・決定論)。
86    states: Vec<Box<DenoiseState<'static>>>,
87    /// チャンネル別の未処理入力 (±1.0 スケールのまま保持)。構築時に FRAME_SIZE 分の
88    /// 無音を先詰めしておくことで、遅延線が常に入力と同数を排出できる。
89    in_buf: Vec<Vec<f32>>,
90    /// 処理済み・未排出の interleaved 出力 (±1.0 スケール・クランプ済み)。
91    out_buf: VecDeque<f32>,
92    /// フレーム処理の入力スクラッチ (i16 スケール・480 サンプル)。
93    frame_in: Vec<f32>,
94    /// フレーム処理の出力スクラッチ (チャンネル別・480 サンプル)。
95    frame_out: Vec<Vec<f32>>,
96}
97
98impl Denoiser {
99    /// チャンネル数 (1 = mono, 2 = stereo interleaved) を指定して構築する。
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    /// 任意長の interleaved (±1.0 正規化・48kHz) サンプルをインプレースで
118    /// ノイズ抑制する。長さはチャンネル数の倍数であること (空スライスは no-op)。
119    ///
120    /// 内部で [`FRAME_SIZE`] サンプル/ch のフレームに切って処理し、端数は次回へ
121    /// 持ち越す。出力は入力を [`FRAME_SIZE`] サンプル/ch 遅らせた列で、ストリーム
122    /// 先頭の遅延分は無音 (0.0)。呼び出しの切り方を変えても出力列は変わらない。
123    /// 出力サンプルは ±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 して持ち越しバッファへ積む。
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        // 遅延線から入力と同数を排出する。先詰めした FRAME_SIZE 分の無音のおかげで
142        // 「処理済み ≥ 排出済み + 今回分」が常に成り立つ (先頭無音が遅延になる)。
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    /// 持ち越し中の端数をゼロ詰めで 1 フレームに整えて処理し、遅延分の末尾
153    /// [`FRAME_SIZE`] サンプル/ch を interleaved で返してストリームを閉じる。
154    ///
155    /// これで総出力 = 総入力 + [`FRAME_SIZE`] サンプル/ch (先頭の無音詰め物) になる。
156    /// 呼び出し後は [`Denoiser::reset`] と同じ初期状態に戻るので、続けて新しい
157    /// ストリームを処理できる。
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            // 上記のゼロ詰め処理後、遅延線には必ず take 以上が残っている。
169            out.push(self.out_buf.pop_front().unwrap_or(0.0));
170        }
171        self.reset();
172        out
173    }
174
175    /// RNN 状態・持ち越しバッファ・遅延線をすべて初期化する。
176    ///
177    /// reset 後は生成直後と同じ状態で、同一入力からは同一出力が得られる。
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    /// 構築時に指定したチャンネル数。
190    pub fn channels(&self) -> u16 {
191        self.channels as u16
192    }
193
194    /// 遅延線の先詰め。各チャンネルの入力バッファに FRAME_SIZE 分の無音を積む。
195    /// この無音フレームの処理結果は厳密に 0.0 なので、出力ストリームの先頭
196    /// FRAME_SIZE サンプル/ch が「無音の詰め物」になる。
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    /// 揃っている分のフレームをすべて処理して遅延線 (out_buf) に積む。
204    fn process_ready_frames(&mut self) {
205        // 全チャンネル同数積まれているので先頭チャンネルの長さだけ見ればよい。
206        while self.in_buf[0].len() >= FRAME_SIZE {
207            for ch in 0..self.channels {
208                // ±1.0 → i16 レンジへ拡大して 1 フレーム処理。
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            // i16 レンジ → ±1.0 に戻し、interleave して排出待ちに積む。
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    /// 決定論の擬似乱数 (LCG・Knuth の MMIX 定数)。rand 依存を避ける。
231    struct Lcg(u64);
232
233    impl Lcg {
234        /// [0, 1) の一様乱数。上位 24bit を使う。
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    /// 振幅 ±amp の一様ホワイトノイズ (シード固定・決定論)。
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    /// 一次 IIR ローパス (y += a * (x - y))。ファン・空調のような低域寄りの定常ノイズを
253    /// 模すために使う (a=0.1 でカットオフ ~800Hz 相当・決定論)。
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    /// 一括処理して「遅延分を除いて入力に整列した」出力列 (入力と同じ長さ) を返す。
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        // 先頭 FRAME_SIZE サンプル/ch (interleaved で FRAME_SIZE*ch) が遅延の無音。
282        buf.split_off(FRAME_SIZE * channels as usize)
283    }
284
285    /// 閾値決めの実測用 (通常は走らせない): 各テスト信号の RMS 比を出力する。
286    /// `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        // 2 秒 @48k mono の低域寄り定常ノイズ (LCG ホワイトノイズの一次ローパス)。
311        // ファン・空調に近い、このクレートの本来の対象。measure_rms_ratios の実測で
312        // ratio = 0.0556 (in_rms 0.0399 → out_rms 0.0022、後半 1 秒に限れば 0.006)。
313        // 実測の約 4.5 倍のマージンを取って閾値 25% とする。
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        // 2 秒 @48k mono のフルバンドホワイトノイズ (振幅 ±0.3)。学習分布から遠い
327        // 合成ノイズなので RNNoise の抑制は弱く、実測 ratio = 0.7883 に留まる
328        // (定常ノイズへの実効性は stationary_noise_rms_strongly_reduced が担う)。
329        // ここでは「増幅せず一定の低減はある」ことだけを実測 + 余裕の 90% で確認する。
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        // 440Hz 正弦 (振幅 0.5) 2 秒。実測では ratio = 0.9999 とほぼ素通し
342        // (RNNoise は周期信号を有声とみなす)。とはいえ純音の扱いはモデル依存なので、
343        // アサーションは健全性 (NaN なし・±1.0 内) と「消えないこと」= 実測の半分の
344        // 50% 床に留める。
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        // 出力ストリームの先頭 FRAME_SIZE サンプル/ch は遅延の詰め物で厳密に 0.0。
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        // 480 の倍数でない 1000 サンプル刻みで流しても、一括処理と出力がビット単位で
377        // 一致する (呼び出し粒度非依存の検証)。総サンプル数の整合も同時に確認。
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        // L = ノイズ, R = 無音の interleaved ステレオ。R 出力は厳密に 0 のまま、
419        // L 出力は同じ信号を mono で処理した結果とビット単位で一致する
420        // (interleave の維持とチャンネル独立性の両方を検証)。
421        let n = 48_000; // 1 秒/ch。1000 サンプル刻み (=500/ch, 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        // reset 後に同一入力を流すと同一出力 (ビット単位)。flush の自動リセットも同様。
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        // flush はストリームを閉じたあと 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]; // 2ch の倍数でない
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        // エラー時はバッファに触れない。
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(); // 空は no-op
515        assert_eq!(dn.channels(), 1);
516
517        // 入力ゼロのまま flush しても遅延分 (無音) がちょうど返る。
518        let tail = dn.flush();
519        assert_eq!(tail.len(), FRAME_SIZE);
520        assert!(tail.iter().all(|&x| x == 0.0));
521    }
522}