@audio/neural-denoise 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,57 @@
1
+ Kṛṣnized
2
+
3
+ This is a reminder that, in the absolute sense, all results of work belong to the Supreme.
4
+
5
+ While software and intellectual property (IP) require a formal license to define authorship,
6
+ responsibilities, or distribution limitations, these are temporary material designations.
7
+ The origin and destination of all ideas, as well as the ultimate owner of all results,
8
+ is the Supreme. The most meaningful action is to dedicate these results to the Supreme.
9
+ This process is known as Karma-Yoga and is described in BG 3.9, BG 12.10 and BG 18.46.
10
+
11
+ "Work done as a sacrifice for the Supreme must be performed;
12
+ otherwise, work binds one to the material world."
13
+
14
+ This license does not override or alter the terms specified by the material-level license below.
15
+
16
+ ॐ
17
+
18
+ ---
19
+
20
+ BSD 3-Clause License
21
+
22
+ Copyright (c) 2026 Dmitry Iv. and audiojs contributors
23
+ Copyright (c) 2007-2017, 2024 Jean-Marc Valin
24
+ Copyright (c) 2023 Amazon
25
+ Copyright (c) 2017-2019 Mozilla
26
+ Copyright (c) 2005-2017 Xiph.Org Foundation
27
+ Copyright (c) 2008-2011 Octasic Inc.
28
+ Copyright (c) 2007-2008 CSIRO
29
+ Copyright (c) 2018 Gregor Richards
30
+ Copyright (c) 2003-2004 Mark Borgerding
31
+
32
+ Redistribution and use in source and binary forms, with or without
33
+ modification, are permitted provided that the following conditions
34
+ are met:
35
+
36
+ - Redistributions of source code must retain the above copyright
37
+ notice, this list of conditions and the following disclaimer.
38
+
39
+ - Redistributions in binary form must reproduce the above copyright
40
+ notice, this list of conditions and the following disclaimer in the
41
+ documentation and/or other materials provided with the distribution.
42
+
43
+ - Neither the name of the Xiph.Org Foundation nor the names of its
44
+ contributors may be used to endorse or promote products derived from
45
+ this software without specific prior written permission.
46
+
47
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
48
+ ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
49
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
50
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
51
+ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
52
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
53
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
54
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
55
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
56
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
57
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/NOTICE ADDED
@@ -0,0 +1,43 @@
1
+ @audio/neural-denoise
2
+ Copyright 2026 Dmitry Iv. and audiojs contributors
3
+
4
+ This product includes a JavaScript translation of RNNoise (https://gitlab.xiph.org/xiph/rnnoise,
5
+ mirrored at https://github.com/xiph/rnnoise), BSD-3-Clause, at commit 70f1d25. Translated files:
6
+ src/{denoise,rnn,nnet,pitch,celt_lpc,kiss_fft,parse_lpcnet_weights,dump_rnnoise_tables}.c and
7
+ src/{nnet_arch,vec,_kiss_fft_guts}.h, into rnnoise.js, fft.js and gemv.wat (vec.h's sparse_cgemv8x4 in
8
+ WebAssembly SIMD). The changes are listed in their headers. The copyright holders and the license are in LICENSE.
9
+
10
+ rnnoise.bin is RNNoise's default model (model_version 0a8755f8, rnnoise_data-0a8755f8….tar.gz from
11
+ media.xiph.org, trained by Jean-Marc Valin on the public datasets listed in upstream's datasets.txt)
12
+ in upstream's own weight-blob format, written by upstream's dump_weights_blob (src/write_weights.c)
13
+ built with the default configuration (DISABLE_DEBUG_FLOAT). It is distributed under the same
14
+ BSD-3-Clause terms as the RNNoise source it is compiled from.
15
+
16
+ This product includes a JavaScript translation of parts of DeepFilterNet
17
+ (https://github.com/Rikorose/DeepFilterNet) at commit d375b2d, dual-licensed MIT OR Apache-2.0,
18
+ used here under the MIT License: libDF/src/lib.rs and transforms.rs (STFT framing, ERB filterbank,
19
+ feature normalizations) and DeepFilterNet/df/{enhance,deepfilternet3,modules,multiframe}.py
20
+ (enhance(), the lookahead shift, the ERB mask, the deep filter), into deepfilter.js.
21
+
22
+ MIT License, Copyright (c) 2021 Hendrik Schröter
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
25
+ software and associated documentation files (the "Software"), to deal in the Software
26
+ without restriction, including without limitation the rights to use, copy, modify, merge,
27
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
28
+ to whom the Software is furnished to do so, subject to the following conditions:
29
+
30
+ The above copyright notice and this permission notice shall be included in all copies or
31
+ substantial portions of the Software.
32
+
33
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
34
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
35
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
36
+ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
37
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
38
+ DEALINGS IN THE SOFTWARE.
39
+
40
+ The DeepFilterNet3 model is not included. deepfilter.js downloads it on first use from the
41
+ DeepFilterNet repository at the same commit (models/DeepFilterNet3_onnx.tar.gz). Upstream's
42
+ README licenses "all code in this repository" as MIT OR Apache-2.0 and states no separate terms
43
+ for the model files; see README.md, "Licenses".
package/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # @audio/neural-denoise
2
+
3
+ > Neural speech enhancement: RNNoise ported to JS, bit-exact to its C, weights inside; DeepFilterNet3 through `@audio/neural-runtime`, matching the Python original.
4
+
5
+ The neural tier beside [`@audio/denoise`](https://github.com/audiojs/denoise)'s statistical denoisers (`omlsa`, `wiener`, `specsub`). A statistical denoiser tracks a noise floor it assumes to be steady; these models learned what speech is, so they also remove noise that moves: keyboards, traffic, a fan changing speed, a busy room.
6
+
7
+ ```js
8
+ import denoise from '@audio/neural-denoise'
9
+
10
+ let clean = await denoise(pcm, { sampleRate: 44100 }) // RNNoise: nothing to download
11
+ let better = await denoise(pcm, { sampleRate: 44100, model: 'deepfilternet3' }) // 8 MB model, fetched once
12
+ ```
13
+
14
+ `audio`: a mono `Float32Array` (with `sampleRate`), one array per channel (denoised independently), or a `decode()` result `{ channelData, sampleRate }`. The result has the same shape and length and lines up with the input: the models' delay is removed. Both models run at 48 kHz; other rates are resampled in and out by [`@audio/resample-sinc`](https://github.com/audiojs/resample).
15
+
16
+ | Option | Default | |
17
+ |---|---|---|
18
+ | `sampleRate` | | required unless `audio` carries it |
19
+ | `model` | `'rnnoise'` | `'rnnoise'`, `'deepfilternet3'`, or a handle from `load()` |
20
+ | `limit` | `20` RNNoise, `12` DeepFilterNet3 | attenuation limit, dB: the input is mixed back in at 10^(−limit/20), so noise drops by at most `limit`; `0` for none, upstream's output. Unlimited, RNNoise removes the voice on 12 of the 824 VoiceBank+DEMAND files (STOI down by more than 0.2), on none at 20 dB, where PESQ rises from 2.11 to 2.46; unlimited, DeepFilterNet3 takes the pauses of home narrations down to −152 dBFS, the digital silence ACX Check flags, and at 12 dB keeps them at −91 to −57 dBFS with the same DNSMOS, at a cost on noisy speech: VoiceBank+DEMAND PESQ 2.67 against 3.16 unlimited ([Accuracy](#accuracy)) |
21
+ | `weights` | | RNNoise: bytes of an upstream weight blob; DeepFilterNet3: URL or bytes of an upstream ONNX export |
22
+ | `device` | `'auto'` | DeepFilterNet3: `@audio/neural-runtime` backend, `'node'`, `'wasm'` or `'webgpu'` |
23
+ | `sessionOptions` | | DeepFilterNet3: ONNX Runtime session options, e.g. `{ intraOpNumThreads: 4 }` |
24
+ | `chunk` / `warmup` / `fade` | `1000` / `300` / `50` | DeepFilterNet3: 10 ms frames per model run, context run before each chunk, crossfade between chunks |
25
+
26
+ For many files, `load()` once, pass the handle as `model`, `free()` it after:
27
+
28
+ ```js
29
+ import denoise, { load } from '@audio/neural-denoise'
30
+
31
+ let model = await load('deepfilternet3')
32
+ for (let pcm of takes) out.push(await denoise(pcm, { sampleRate, model }))
33
+ model.free()
34
+ ```
35
+
36
+ ## Models
37
+
38
+ | `model` | Network | Weights | Runs on | Delay | Streaming |
39
+ |---|---|---|---|---|---|
40
+ | `'rnnoise'` | RNNoise (Valin 2018; upstream's current model, January 2025): 2 convolutions, 3 GRUs, 2.88M weights (2.80M int8); 32 band gains per 10 ms | in the package: 3.5 MB (2.8 MB gzipped), BSD-3-Clause | JS; int8 products in a 419-byte WebAssembly SIMD kernel where available | 20 ms (30 ms live) | frame API, AudioWorklet |
41
+ | `'deepfilternet3'` | DeepFilterNet3 (Schröter et al. 2023): ERB gains, then a deep filter on the lowest 96 bins; 2.13M parameters | fetched from upstream on first use (8 MB) and cached; weight terms unconfirmed, see Licenses | ONNX Runtime: Node, wasm, WebGPU | 30 ms (40 ms live) | offline, chunked |
42
+
43
+ Delay: algorithmic; live adds the 10 ms a frame takes to arrive.
44
+
45
+ **The RNNoise weights ship in the package.** `rnnoise.bin` is upstream's own loadable format (what `rnnoise_model_from_file` reads), byte for byte what upstream's `dump_weights_blob` writes from the pinned model tarball. Its terms are the BSD-3-Clause of the source it comes from, and RNNoise is worth having because it runs anywhere at once: in a worklet, offline, with no network and no runtime. A custom format would save about 10% after compression (the int32 block indices compress to almost nothing) and cost a converter and interop: any blob from upstream's tools, the little model or one you train, loads through `weights`.
46
+
47
+ ## Live: AudioWorklet
48
+
49
+ ```js
50
+ import { weights } from '@audio/neural-denoise'
51
+
52
+ let ctx = new AudioContext({ sampleRate: 48000 })
53
+ await ctx.audioWorklet.addModule(new URL('@audio/neural-denoise/worklet', import.meta.url))
54
+ let node = new AudioWorkletNode(ctx, 'neural-denoise', { processorOptions: { weights: await weights(), limit: 30 } })
55
+ mic.connect(node).connect(ctx.destination)
56
+ ```
57
+
58
+ The processor collects 128-sample render quanta into 10 ms frames. Its output queue starts with 480 − gcd(128, 480) = 448 samples of silence, the least that never runs dry, so the node's delay is constant: 1408 samples, 29.3 ms. It allocates nothing per frame. The context must run at 48 kHz; the browser resamples the device. `limit` works as in `denoise()`: 20 dB unless given, `0` for none.
59
+
60
+ Frame by frame, as `rnnoise_process_frame` works:
61
+
62
+ ```js
63
+ import { rnnoise, weights } from '@audio/neural-denoise'
64
+
65
+ let st = rnnoise(await weights())
66
+ let voice = st.process(input480, output480) // int16 scale in and out; voice probability 0–1; output 960 samples late
67
+ ```
68
+
69
+ ## In `audio`: the contract atom
70
+
71
+ `@audio/neural-denoise/audio` is the package's [contract](https://github.com/audiojs/compile/blob/main/CONTRACT.md) manifest: `rnnoise`, a streaming atom at any sample rate, with one parameter, `limit` (dB, default 20, 0 for none). [`audio`](https://github.com/audiojs/audio) hosts it as its `rnnoise` op, and runs DeepFilterNet3 as its `deepfilter` op (the whole input before rendering, `limit` 12 by default):
72
+
73
+ ```js
74
+ import audio from 'audio'
75
+
76
+ await audio('take.wav').rnnoise().save('clean.wav') // streams
77
+ await audio('take.wav').deepfilter().save('clean.wav') // DeepFilterNet3, room tone kept
78
+ ```
79
+
80
+ Blocks of any size collect into frames; the output queue starts with 479 samples of silence (FRAME − 1, the least that never runs dry for any block size), so the delay is constant: 1439 samples at 48 kHz (30 ms). Other rates go to 48 kHz and back through a streaming form of `@audio/resample-sinc`'s kernel, in its arithmetic, and the delay grows by its lookahead: 1354 samples at 44.1 kHz (30.7 ms), 271 at 8 kHz. The output is `denoise()`'s, delayed: at 48 kHz sample for sample; at other rates, with resample-sinc 1.2.0, sample for sample but in the last 20–30 ms, where the offline resampler sees the end and a stream the host's trailing silence. The default `limit`, 20 dB, is `denoise()`'s for RNNoise: unlimited, the model removes the voice on some files (Accuracy, below).
81
+
82
+ ## RNNoise
83
+
84
+ A translation of [xiph/rnnoise](https://gitlab.xiph.org/xiph/rnnoise) at 70f1d25, the network and all its DSP, following the portable C path (`vec.h` without SIMD) op for op in float32:
85
+
86
+ 1. **Frame**: 480 samples at 48 kHz through a DC-blocking biquad; a 960-point FFT of the last two frames under a Vorbis window.
87
+ 2. **Features** (65): the energies of 32 ERB-spaced bands up to 20 kHz as a log spectrum, its DCT; each band's correlation with a pitch-delayed copy of the signal, its DCT; the pitch period. The period comes from a 2:1 downsampled, LPC-whitened signal: a coarse search at 4:1, a fine one around the two best lags, octave errors removed.
88
+ 3. **Network**: two 1-D convolutions (65·3 → 128 → 384, tanh), three GRUs of 384, dense layers to 32 band gains and a voice probability. The GRU and second convolution weights are int8 with per-row scales, and their input activations are quantized to int8, as upstream does.
89
+ 4. **Pitch filter**: on the previous frame (one frame of lookahead), the pitch-delayed spectrum is added back per band where the gains would cut voiced harmonics, then the band energies are restored.
90
+ 5. **Gains**: they fall at most by a factor 0.6 per frame (an RT60 of 135 ms), are interpolated across each band and applied; inverse FFT, overlap-add. Bins above 20 kHz are zeroed.
91
+
92
+ The float code keeps C's float semantics: each float op is `Math.fround(a op b)`, exact for +, −, ×, ÷ and √ of float32 operands (Figueroa 1995), and values stay double where the C promotes to double. The int8 products are integers under 2²⁴, exact in any order, so they run in a WebAssembly SIMD kernel ([gemv.wat](./gemv.wat): widening to i16, `i32x4.dot_i16x8_s`) where the runtime has SIMD, 8.5 times faster than the JS loop that runs elsewhere; both give the same bits.
93
+
94
+ ## DeepFilterNet3
95
+
96
+ Upstream's three ONNX graphs (encoder, ERB-gain decoder, deep-filter decoder) run unchanged; the rest is ported from DeepFilterNet at d375b2d, `libDF` and `df.enhance.enhance()`:
97
+
98
+ 1. **STFT**: 960-point Vorbis window, 480 hop, the input padded by 960 zeros; the same FFT as RNNoise.
99
+ 2. **Features**: power in 32 ERB bands (libDF's `erb_fb`: 2 to 67 bins wide) in dB, mean-normalized with α = 0.99 (τ = 1 s); the lowest 96 bins, unit-normalized with the same α. Both run 2 frames ahead of the output (`conv_lookahead`).
100
+ 3. **Network**: 32 gains (sigmoid) and 96 × 5 complex filter taps per frame.
101
+ 4. **Enhance**: every bin takes its band's gain; below bin 96 (4.8 kHz) the output is instead Σₖ spec[t − 2 + k] · coef[t, k], k = 0–4, over the unmasked spectrum; `limit` mixes the noisy spectrum back in. Inverse FFT, overlap-add, the 480-sample STFT delay removed.
102
+
103
+ The parameters come from the export's `config.ini`, so upstream's low-latency export (`DeepFilterNet3_ll_onnx.tar.gz`: no lookahead, 36 MB) loads through `weights` too.
104
+
105
+ **Long inputs** run in chunks of `chunk` frames (10 s). Upstream feeds the whole file at once, about 8 MB of activations per second of audio (peak RSS 768 MB for 30 s, 1 GB for 60 s in onnxruntime-node); in 10 s chunks a 180 s file peaks at 563 MB, and any length stays there. The ONNX graphs are exactly causal (outputs on a prefix equal the whole run's) but take no GRU state, so each chunk after the first starts `warmup` frames (3 s) early from zero state, and its first `fade` frames (0.5 s) crossfade from the previous chunk's run, which continued that far. The GRUs remember how they started: 6 s after such a start the gains still differ from the whole-file run's by up to 0.1–0.5. Chunked output is another valid run, not the whole-file one; on VoiceBank+DEMAND in 1 s chunks (1 s warm-up, 0.2 s crossfade, 2 to 3 chunks per utterance) the scores do not move: PESQ 3.164 against 3.162 whole-file, STOI 0.9450 against 0.9452, SI-SDR 18.94 against 18.96 dB, though single files shift by up to 0.31 PESQ either way. The STFT and features stay exact across chunks. Exact chunking needs a stateful re-export (GRU states and convolution buffers as graph inputs and outputs), which is also what live DeepFilterNet3 needs.
106
+
107
+ ## Verification
108
+
109
+ | Check | Result |
110
+ |---|---|
111
+ | RNNoise vs upstream C, portable build (`cc -O2 -ffp-contract=off -DDISABLE_NEON`), default and little models | **bit-exact**, output and voice probability: lena (1127 frames), lena + noise (1127), digital silence and near-silence (300), a VoiceBank+DEMAND utterance (174) |
112
+ | RNNoise vs upstream C as `configure && make` builds it on arm64 (NEON, fused multiply-adds) | 38.8–62.4 dB SNR, voice probability within 0.087: upstream's builds differ from each other by this much (NEON's `vrecpeq_f32` reciprocal estimate inside tanh and sigmoid) |
113
+ | `rnnoise.bin` | byte for byte what upstream's `dump_weights_blob` writes from the model tarball (sha256 `1ad07b42…`) |
114
+ | FFT, window and DCT tables | equal to upstream's `rnnoise_tables.c`: 960 twiddles, 480 window values, 1024 DCT entries, the bit reversal; the FFT matches a direct DFT to 3.2e-9 |
115
+ | DeepFilterNet3 vs Python `df.enhance.enhance()` (deepfilternet 0.5.6, PyTorch checkpoint), same input, `limit: 0` | 133.5 dB SNR on a 1.7 s utterance, 128.2 dB on 11.3 s (max \|d\| 5.7e-7); ERB widths and α identical |
116
+ | DeepFilterNet3 plumbing, no model (`test.js`) | identity gains and taps give the input back to 1.8e-7, whole and chunked (chunks identical to the whole run); a tap one frame ahead advances the band below 4.8 kHz by exactly 480 samples (to 8.9e-8) |
117
+ | Worklet (`test.js`, AudioWorkletGlobalScope simulated) | the frame API's output, delayed by 448 samples, at `limit: 0`; at 12 and at the default 20, that output with the input mixed back, to 1e-7 |
118
+ | Worklet in Chromium 153 (headless, Playwright, `scripts/worklet.mjs`) | runs, no processor error, output non-silent; live underruns in Speed |
119
+ | Contract atom (`test.js`) against `denoise()`, delayed by its declared latency | 48 kHz: equal sample for sample in blocks of 1, 128, 1024 and 1 to 3000 samples, limits 20 and 0; 44.1 kHz (resample-sinc 1.2.0): equal but in the last 50 ms; with resample-sinc 1.1.2, whose arithmetic differs, 59.5 dB SNR |
120
+
121
+ To rerun: `node scripts/rnnoise-reference.mjs` builds upstream and rewrites `fixtures/rnnoise.json` (and caches the little model); `python scripts/deepfilter-reference.py` (DeepFilterNet 0.5.6 in a Python 3.11 venv) writes the reference `test.js` compares with.
122
+
123
+ ## Accuracy
124
+
125
+ The VoiceBank+DEMAND test set (Valentini-Botinhao 2017, CC BY 4.0): 824 utterances by 2 speakers at 48 kHz, 5 noises at 2.5 to 17.5 dB SNR. Scores at 16 kHz (`scipy.signal.resample_poly`, 1:3): wideband PESQ (ITU-T P.862.2, python-pesq 0.0.4), STOI (Taal 2011, pystoi 0.4.1), SI-SDR (Le Roux 2019), and the reference-free DNSMOS P.835 (Reddy 2022, `sig_bak_ovr.onnx` with the polynomial fits of `dnsmos_local.py`: SIG speech, BAK background, OVRL overall, 1 to 5). The classical rows are [`@audio/denoise`](https://github.com/audiojs/denoise)'s ops through their `audio` manifests after its September 2026 audit, measured by its `scripts/speech.mjs` and `scripts/speech.py` with this scoring; the 0.3.11 ops that `audio` 2.6 runs scored `omlsa` 1.88, `wiener` 2.19, `specsub` 2.11 PESQ.
126
+
127
+ | System | PESQ | STOI | SI-SDR dB | SIG | BAK | OVRL |
128
+ |---|---|---|---|---|---|---|
129
+ | noisy input | 1.97 | 0.921 | 8.45 | 3.32 | 3.11 | 2.68 |
130
+ | `omlsa()`, defaults | 2.40 | 0.915 | 14.55 | 3.37 | 3.48 | 2.85 |
131
+ | `wiener()`, defaults | 2.33 | 0.910 | 13.95 | 3.37 | 3.38 | 2.80 |
132
+ | `specsub()`, defaults | 2.24 | 0.920 | 12.52 | 3.35 | 3.33 | 2.77 |
133
+ | "Enhance speech", its denoising: `highpass(80).dehum().omlsa()` | 2.43 | 0.914 | 6.03 | 3.40 | 3.39 | 2.83 |
134
+ | "Enhance speech", the whole recipe, with 0.3.11's `omlsa` | 1.63 | 0.830 | 0.84 | 3.06 | 2.37 | 2.18 |
135
+ | RNNoise, `limit: 0` (this package = upstream C) | 2.11 | 0.890 | 12.28 | 3.28 | 3.85 | 2.94 |
136
+ | RNNoise, little model, `limit: 0` | 2.12 | 0.893 | 12.39 | 3.28 | 3.85 | 2.93 |
137
+ | RNNoise, `limit: 20`, the default | 2.46 | 0.918 | 12.86 | 3.40 | 3.71 | 2.97 |
138
+ | **DeepFilterNet3**, `limit: 0` (this package) | **3.16** | **0.945** | **18.96** | **3.44** | **4.08** | **3.18** |
139
+ | DeepFilterNet3, Python (deepfilternet 0.5.6) | 3.16 | 0.945 | 18.96 | 3.44 | 4.08 | 3.18 |
140
+ | DeepFilterNet3 in 1 s chunks, `limit: 0` | 3.16 | 0.945 | 18.94 | 3.44 | 4.08 | 3.18 |
141
+ | DeepFilterNet3, `limit: 12`, the default | 2.67 | 0.939 | 16.18 | 3.49 | 3.69 | 3.03 |
142
+
143
+ Over the noisy input, paired, with 95% intervals: DeepFilterNet3 adds 1.20 ± 0.03 PESQ, 0.024 ± 0.002 STOI and 10.5 ± 0.3 dB SI-SDR, and beats every classical row's PESQ on 804 of 824 files; RNNoise adds 0.14 ± 0.04 PESQ and 3.8 ± 0.3 dB SI-SDR but loses 0.031 ± 0.004 STOI; `omlsa`, the best classical row, adds 0.43 ± 0.03 PESQ and 6.1 ± 0.2 dB SI-SDR and loses 0.006 STOI. DeepFilterNet3's default 12 dB limit leaves noise in on this set: against `limit: 0` it costs 0.49 ± 0.02 PESQ, 2.8 ± 0.2 dB SI-SDR and 0.39 BAK, and gains 0.05 SIG; it still adds 0.70 ± 0.02 PESQ over the input, 0.27 ± 0.02 over `omlsa` and 0.21 over RNNoise at its default. Pass `limit: 0` where the most noise removal matters more than room tone. The same scoring gives upstream's published anchors: noisy 1.97 and 0.921 (as the DeepFilterNet2 paper's table), DeepFilterNet3 3.16 and 0.945 against the DeepFilterNet3 paper's 3.17 and 0.944. That DeepFilterNet2 table measured RNNoise's 2022 model at 2.33 and 0.922; the current model scores lower here.
144
+
145
+ - **RNNoise's current model sometimes removes the voice.** STOI drops by more than 0.1 on 68 of 824 files and by more than 0.2 on 12; on p232_354 it goes from 0.870 to 0.154, the output 24 dB under the clean level while RNNoise's own voice probability averages 0.84. Upstream's C does the same, the portable and the NEON build alike (0.154, 0.153). DeepFilterNet3 never loses more than 0.066 STOI, `wiener` 0.065, `omlsa` 0.059. A 20 dB `limit` (the input mixed back at 0.1) caps it: losses over 0.1 fall to 6 files and over 0.2 to none (p232_354 keeps 0.785), and PESQ rises to 2.46, +0.50 ± 0.03 over the noisy input and +0.07 ± 0.02 over `omlsa`, at a BAK of 3.71 instead of 3.85. `denoise()`, the worklet and the atom apply that limit unless given another.
146
+ - **The recipe's delivery stages are not denoisers.** Its compressor, EQ, de-esser and loudness target change the signal against the clean reference on purpose: the full recipe scores under its own denoising part. SI-SDR also falls with the 80 Hz high-pass: on its own it takes clean speech to 6.2 dB SI-SDR at PESQ 4.45 (21 test utterances). The table says what each stage does to speech in noise, not how finished a podcast sounds.
147
+
148
+ **Real rooms.** Ten amateur narrations from Spoken Wikipedia (volunteers at home, 2007 to 2026, the first 60 s of each; Ogg Vorbis at 44.1 and 48 kHz, decoded and resampled to 48 kHz by ffmpeg), scored without a reference: DNSMOS P.835 as above; the noise floor as the RMS of the quietest 500 ms (ACX Check's measure; one file with pauses cut to digital silence left out); speech level change over the loudest half of 50 ms frames.
149
+
150
+ | System | SIG | BAK | OVRL | noise floor, dBFS (median) | speech level |
151
+ |---|---|---|---|---|---|
152
+ | raw | 3.45 | 4.04 | 3.17 | −83 to −54 (−62) | |
153
+ | `omlsa()` | 3.46 | 4.09 | 3.19 | −98 to −69 (−75) | 0.0 dB |
154
+ | "Enhance speech" denoising | 3.45 | 4.07 | 3.17 | −101 to −71 (−85) | −0.2 dB |
155
+ | RNNoise, `limit: 0` | 3.42 | 4.14 | 3.19 | −127 to −106 (−112) | −0.5 dB |
156
+ | DeepFilterNet3, `limit: 0` | 3.47 | 4.12 | 3.21 | −152 to −61 (−89) | −0.5 dB |
157
+ | DeepFilterNet3, `limit: 12`, the default | 3.47 | 4.10 | 3.21 | −91 to −57 (−67) | −0.4 dB |
158
+
159
+ These rooms are mostly quiet (raw OVRL 3.17), so the gains are small. OM-LSA keeps SIG and raises BAK a little less than the models (0.3.11's lowered SIG by 0.20); both models raise BAK and keep SIG. Both can also take pauses to dead silence (RNNoise's median floor is −112 dBFS, DeepFilterNet3's lowest −152), which ACX Check flags; a `limit` of 12 dB keeps DeepFilterNet3's floors between −91 and −57 dBFS at the same OVRL, room tone left in. On lena (a film scene with music under the voice) RNNoise lowers OVRL from 2.76 to 2.47, DeepFilterNet3 raises it to 2.83, 2.90 with the limit.
160
+
161
+ To rerun: `node scripts/accuracy.mjs vbdemand SYSTEMS` and `node scripts/accuracy.mjs rooms` write the outputs (the data sources and checksums are in its header), `python scripts/accuracy.py vbdemand SYSTEMS` and `python scripts/accuracy.py rooms` score them, and `scripts/deepfilter-reference.py --vbdemand` writes the Python row; the classical rows come from `@audio/denoise`'s `node scripts/speech.mjs vbdemand|rooms SYSTEMS` and `python scripts/speech.py score vbdemand|rooms SYSTEMS`. Per-file scores stay in `~/.cache/audiojs/data/` (`vbdemand/scores/`, `spoken/scores.json`); the scripts regenerate outputs, skipping any already there.
162
+
163
+ ## Speed and memory
164
+
165
+ Apple M4 Max running other jobs (load average 28 to 50 on 14 cores), so wall times ran up to 10 times CPU times; CPU times are given.
166
+
167
+ **RNNoise**, per 10 ms frame at 48 kHz:
168
+
169
+ | Where | Per frame | |
170
+ |---|---|---|
171
+ | Chromium 153, page thread, SIMD kernel | 0.32 ms mean, 0.5 ms p99, 0.9 ms max | 1000 frames |
172
+ | Chromium 153, page thread, JS only | 1.46 ms mean, 1.6 ms p99, 2.9 ms max | |
173
+ | Chromium 153, the worklet in an OfflineAudioContext | 0.36 ms (30 s rendered in 1.09 s) | the whole node: FIFO, int16 scaling |
174
+ | Chromium 153, the worklet live: 48 kHz, 256-sample buffers, `AudioContext.playbackStats` | 5 runs of 19 s: no underrun in 3; 8 and 14 (43 and 75 ms) in 2. Controls without the worklet, at the same time: 0 and 2 (11 ms) | output checked non-silent; load average 21 to 50 on 14 cores |
175
+ | Node 25, SIMD kernel / JS only | 0.53 ms / 1.58 ms CPU | real-time factors 0.053 / 0.158 |
176
+ | Node 25, the contract atom, SIMD kernel, 1024-sample blocks | 0.92 ms CPU at 48 kHz; 1.32 ms a frame's worth at 44.1 kHz | real-time factors 0.092 / 0.132: the 44.1 kHz host resamples both ways; load average 65 on 14 cores |
177
+ | upstream C, portable / NEON build, same machine and load | 0.44 ms / 0.12 ms CPU | for scale |
178
+
179
+ A 128-sample render quantum lasts 2.67 ms and a frame is due every 3.75 quanta: the slowest SIMD frame measured (0.94 ms) fits a quantum 2.8 times over, the mean 8 times; the JS path's slowest (up to 4.0 ms) does not, and relies on device buffers of 256 samples or more, the browsers' usual size. The live underruns came with the machine oversubscribed, the control underran too; an idle machine is untested. Memory: 3.5 MB of weights (plus a 2.8 MB copy in the kernel's memory), about 70 KB of state per channel; the Node evaluation processes peaked at 83 MB RSS.
180
+
181
+ **DeepFilterNet3**, onnxruntime-node 1.30, 4 intra-op threads: real-time factor 0.047 in CPU on a 180 s file (8.4 s), 0.087 over the 824 short test utterances (2072 s of audio in 180 s; per-run overhead); Python DeepFilterNet (PyTorch 2.1, 4 threads) 0.067 on the same utterances. Peak RSS 436 MB over the test set, 563 MB for 180 s in 10 s chunks. The browser path (onnxruntime-web, wasm and WebGPU) is untested here.
182
+
183
+ ## Licenses
184
+
185
+ Audited 2026-09-27 against what each project states; a summary, not legal advice.
186
+
187
+ | Model | Code | Weights | Weight files and their terms | Here |
188
+ |---|---|---|---|---|
189
+ | **RNNoise**, model 0a8755f8 (January 2025) | BSD-3-Clause (`COPYING`: Valin, Amazon, Mozilla, Xiph.Org, Borgerding) | BSD-3-Clause | `rnnoise_data-0a8755f8….tar.gz` (media.xiph.org, sha256 pinned in upstream's `model_version`): `src/rnnoise_data.c` (default), `src/rnnoise_data_little.c`, two `.pth` checkpoints. No header of their own; upstream's `autogen.sh` downloads them into `src/` and compiles them into the BSD library, and the README calls them "the models distributed with RNNoise". Trained on the corpora in `datasets.txt`: OpenSLR 30–86 (CC BY-SA 4.0) and Hi-Fi TTS (OpenSLR 109, CC BY 4.0) for speech, Xiph's noise collections (donated noise "freely available", terms in that archive, not checked) | bundled: `rnnoise.bin`, the default model through upstream's `dump_weights_blob`; the little model (1.55 MB) loads through `weights` |
190
+ | **DeepFilterNet3**, d375b2d | MIT OR Apache-2.0 ("All code in this repository") | **unconfirmed** | `models/DeepFilterNet3_onnx.tar.gz`, `DeepFilterNet3.zip`, `DeepFilterNet3_ll_onnx.tar.gz`, in the same repository, no terms of their own. Four open issues ask whether "all code" covers them ([#697](https://github.com/Rikorose/DeepFilterNet/issues/697), [#700](https://github.com/Rikorose/DeepFilterNet/issues/700), [#709](https://github.com/Rikorose/DeepFilterNet/issues/709), [#712](https://github.com/Rikorose/DeepFilterNet/issues/712), July to September 2026), none answered; the last push was October 2024. The DeepFilterNet3 paper's abstract says "the framework as well as pretrained weights have been published under an open source license", naming none. Third parties republish them as MIT (Intel/deepfilternet-openvino on Hugging Face) or Apache-2.0 | fetched from upstream at the pinned commit, cached, never bundled or re-hosted |
191
+ | DeepFilterNet 1 and 2 | same | same as DeepFilterNet3 | `models/DeepFilterNet{,2}*.{zip,tar.gz}` | not used; libDF deprecates DeepFilterNet2 |
192
+ | GTCRN (Rong et al., ICASSP 2024) | MIT | MIT (checkpoints in the repository) | 48.2K parameters, trained on DNS3 or VCTK-DEMAND | not used: 16 kHz only, narration would lose everything above 8 kHz |
193
+ | Demucs speech denoiser (facebookresearch/denoiser) | CC BY-NC 4.0 | CC BY-NC 4.0 | | excluded: non-commercial; archived |
194
+ | DNSMOS P.835 (Microsoft) | CC BY 4.0 (microsoft/DNS-Challenge) | CC BY 4.0 | `DNSMOS/DNSMOS/sig_bak_ovr.onnx` at 82f1b17e77 | evaluation only |
195
+ | VoiceBank+DEMAND (Valentini-Botinhao 2017) | | CC BY 4.0 | 824 test utterances, DataShare checksums verified | evaluation only |
196
+ | Spoken Wikipedia narrations | | CC BY-SA 3.0/4.0, CC BY 4.0, CC0 | ten files from Wikimedia Commons | evaluation only, not redistributed |
197
+
198
+ In the browser, [shiguredo/rnnoise-wasm](https://github.com/shiguredo/rnnoise-wasm) (Apache-2.0) builds the same RNNoise commit with emscripten, and [sapphi-red/web-noise-suppressor](https://github.com/sapphi-red/web-noise-suppressor) (MIT) wraps its 2022 build next to Speex and GTCRN. This package needs no WebAssembly (it takes 419 bytes of it for speed where it can), is checked bit for bit against the C, and adds DeepFilterNet3.
199
+
200
+ The package is BSD-3-Clause (the RNNoise translation and weights); `deepfilter.js` carries DeepFilterNet's MIT notice ([NOTICE](./NOTICE)).
201
+
202
+ ## Reference
203
+
204
+ J.-M. Valin, "A Hybrid DSP/Deep Learning Approach to Real-Time Full-Band Speech Enhancement", MMSP 2018, [arXiv:1709.08243](https://arxiv.org/abs/1709.08243). · H. Schröter, T. Rosenkranz, A. N. Escalante-B., A. Maier, "DeepFilterNet: Perceptually Motivated Real-Time Speech Enhancement", Interspeech 2023, [arXiv:2305.08227](https://arxiv.org/abs/2305.08227). · C. Valentini-Botinhao, "Noisy speech database for training speech enhancement algorithms and TTS models", University of Edinburgh, 2017, [doi:10.7488/ds/2117](https://doi.org/10.7488/ds/2117). · ITU-T P.862.2 (2007), wideband PESQ. · C. H. Taal, R. C. Hendriks, R. Heusdens, J. Jensen, "An Algorithm for Intelligibility Prediction of Time-Frequency Weighted Noisy Speech", IEEE TASLP 19(7), 2011. · J. Le Roux, S. Wisdom, H. Erdogan, J. R. Hershey, "SDR: Half-baked or Well Done?", ICASSP 2019. · C. K. A. Reddy, V. Gopal, R. Cutler, "DNSMOS P.835: A Non-Intrusive Perceptual Objective Speech Quality Metric to Evaluate Noise Suppressors", ICASSP 2022. · S. A. Figueroa, "When is double rounding innocuous?", SIGNUM Newsletter 30(3), 1995. · [xiph/rnnoise](https://gitlab.xiph.org/xiph/rnnoise) at 70f1d25 · [Rikorose/DeepFilterNet](https://github.com/Rikorose/DeepFilterNet) at d375b2d.
205
+
206
+ **Use when:** speech with noise a statistical denoiser leaves or smears (moving noise, a busy room); live voice in a browser (RNNoise, 30 ms); files where quality is worth an 8 MB download (DeepFilterNet3).<br>
207
+ **Not for:** music or singing (both models keep speech and drop the rest); installs that must stay model-free ([`@audio/denoise`](https://github.com/audiojs/denoise)); reverb as such (`dereverb`); live DeepFilterNet3 (upstream's graphs take no state).
208
+
209
+ ---
210
+
211
+ Part of the [@audio/neural](https://github.com/audiojs/neural) lane.
212
+
213
+ BSD-3-Clause © 2026 audiojs (port), Jean-Marc Valin, Xiph.Org Foundation, Mozilla, Amazon (RNNoise); DeepFilterNet parts MIT © 2021 Hendrik Schröter
package/audio.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Generated from the audio.js manifest (params metadata is the source of truth).
2
+ // Regenerate: node tools/dts.js in @audio/compile. Do not edit by hand.
3
+
4
+ /** Automatable number — scalar, `t => value` fn, or breakpoint curve {t, v} */
5
+ type Auto = number | ((t: number) => number) | { t: number[], v: number[] }
6
+ /** Per-block param values as delivered by hosts (numbers arrive as 1-length Float32Array) */
7
+ type Live = Record<string, Float32Array | string | boolean>
8
+ type Ctx = { sampleRate: number, maxBlockSize: number, maxChannels: number, currentTime: number, duration?: number, events?: readonly any[], emit?: (name: string, ...args: any[]) => void, [k: string]: unknown }
9
+ type Process = (inputs: Float32Array[][], outputs: Float32Array[][], params: Live) => void
10
+
11
+ /** Chainable-host options for 'rnnoise' */
12
+ export interface RnnoiseOptions {
13
+ /** 0..100 dB (default 20) */
14
+ "limit"?: Auto
15
+ at?: number | string
16
+ duration?: number | string
17
+ }
18
+
19
+ export declare const rnnoise: {
20
+ (ctx: Ctx): Process
21
+ channels: "any"
22
+ latency: (ctx: { sampleRate: number, params: Live }) => number
23
+ tail: 0
24
+ params: {
25
+ /** 0..100 dB (default 20) */
26
+ "limit": { type: "number", default: 20 }
27
+ }
28
+ }
package/audio.js ADDED
@@ -0,0 +1,132 @@
1
+ // atom manifest: RNNoise per @audio/compile CONTRACT, streaming at any sample rate.
2
+ //
3
+ // RNNoise (rnnoise.js, bit-exact to upstream's C) takes 10 ms frames, 480 samples at 48 kHz, and returns each
4
+ // 960 samples late. Blocks of any size collect into frames; the output queue starts with FRAME − 1 = 479
5
+ // samples of silence, the least that never runs dry for any block size (the worklet's 448 serves 128-sample
6
+ // quanta only), so the delay is constant: 960 + 479 = 1439 samples at 48 kHz, 30 ms. Other rates go to
7
+ // 48 kHz and back through a streaming form of @audio/resample-sinc's Lanczos kernel, the resampler denoise()
8
+ // uses offline; its lookahead adds to the delay (1354 samples at 44.1 kHz, 30.7 ms). The output is
9
+ // denoise()'s: at 48 kHz sample for sample; at other rates, with resample-sinc 1.2.0, except within 50 ms of
10
+ // the end, where the offline resampler sees the end and a stream sees the host's trailing silence.
11
+ //
12
+ // limit (dB): the input is mixed back in at 10^(−limit/20), so noise drops by at most `limit`; 0 lifts it
13
+ // (denoise()'s option, DeepFilterNet's atten_lim_db). The default departs from denoise()'s (none) on purpose:
14
+ // unlimited, RNNoise's current model removes the voice on 12 of the 824 VoiceBank+DEMAND test files (STOI
15
+ // down by more than 0.2); limited to 20 dB, on none, and PESQ rises from 2.11 to 2.46 (README, Accuracy).
16
+ // The limit is read per block and applies from the next finished frame on.
17
+ //
18
+ // Channels are denoised independently; a channel's state (70 KB) is created on its first block, as the
19
+ // worklet does, since hosts declare up to 32 channels.
20
+
21
+ import { model, create, FRAME, LIMIT } from './rnnoise.js'
22
+ import { weights } from './denoise.js'
23
+
24
+ const RATE = 48000, DELAY = 2 * FRAME, PRIME = FRAME - 1, HALF = 16
25
+ const net = model(await weights())
26
+
27
+ const sinc = x => x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x)
28
+
29
+ /** Lanczos taps each side from rate `from` to `to`, as resample-sinc: 16, widened by the ratio on downsampling. */
30
+ function taps(from, to) {
31
+ let rate = from / to, scale = rate > 1 ? 1 / rate : 1
32
+ return Math.ceil(HALF / scale)
33
+ }
34
+
35
+ /** Delay at host rate sr, in samples. Output i of the converter back to sr reads 48 kHz samples up to
36
+ * floor(i·48000/sr) + T₂; each of those waits for the end of its frame, at most 1439 samples on; that 48 kHz
37
+ * sample reads host input up to floor(·sr/48000) + T₁. So output i needs input up to i + D, no further. */
38
+ const latency = sr => sr === RATE ? DELAY + PRIME : Math.floor((taps(RATE, sr) + DELAY + PRIME) * sr / RATE) + taps(sr, RATE)
39
+
40
+ /**
41
+ * @audio/resample-sinc's resample() (Lanczos, a = 16) as a stream, in its arithmetic, so its samples: `push(x)`
42
+ * calls `emit(v)` for every output whose taps have all arrived. Output i sits at input position i·from/to;
43
+ * taps before the start are left out and the weights renormalized, as resample() does at its edges.
44
+ */
45
+ function resampler(from, to, emit) {
46
+ let rate = from / to, scale = rate > 1 ? 1 / rate : 1, T = Math.ceil(HALF / scale), K = HALF / (Math.PI * Math.PI)
47
+ let A = new Float64Array(2 * T), B = new Float64Array(2 * T), C = new Float64Array(2 * T), D = new Float64Array(2 * T)
48
+ for (let j = 0, t = 1 - T; t <= T; t++, j++) {
49
+ let a = Math.PI * scale * t
50
+ A[j] = Math.sin(a); B[j] = Math.cos(a); C[j] = Math.sin(a / HALF); D[j] = Math.cos(a / HALF)
51
+ }
52
+ // input history: an output reads 2T samples, the newest of them just arrived
53
+ let mask = (1 << Math.ceil(Math.log2(2 * T + 1))) - 1, ring = new Float32Array(mask + 1), n = 0, i = 0
54
+ return x => {
55
+ for (let s = 0; s < x.length; s++) {
56
+ ring[n++ & mask] = x[s]
57
+ for (;;) {
58
+ let pos = i * rate, base = Math.floor(pos)
59
+ if (base + T >= n) break
60
+ let frac = pos - base
61
+ let f = Math.PI * scale * frac, sf = Math.sin(f), cf = Math.cos(f), sg = Math.sin(f / HALF), cg = Math.cos(f / HALF)
62
+ let sum = 0, w = 0
63
+ for (let j = 0, t = 1 - T; t <= T; t++, j++) {
64
+ if (base + t < 0) continue
65
+ let y = (t - frac) * scale
66
+ if (Math.abs(y) >= HALF) continue
67
+ let k = Math.abs(y) < 1e-3 ? sinc(y) * sinc(y / HALF) : (A[j] * cf - B[j] * sf) * (C[j] * cg - D[j] * sg) * K / (y * y)
68
+ sum += ring[(base + t) & mask] * k; w += k
69
+ }
70
+ emit(Math.fround(w !== 0 ? sum / w : 0))
71
+ i++
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ /** Output queue primed with `len` samples of silence (denoise-wiener's primed FIFO, filled a sample at a time). */
78
+ function queue(len) {
79
+ let q = { buf: new Float32Array(Math.max(1 << 12, 2 * len)), len }
80
+ q.push = v => {
81
+ if (q.len === q.buf.length) { let nb = new Float32Array(2 * q.buf.length); nb.set(q.buf); q.buf = nb }
82
+ q.buf[q.len++] = v
83
+ }
84
+ q.pull = out => {
85
+ let n = Math.min(out.length, q.len)
86
+ out.set(q.buf.subarray(0, n)); out.fill(0, n)
87
+ q.buf.copyWithin(0, n, q.len); q.len -= n
88
+ }
89
+ return q
90
+ }
91
+
92
+ /** One channel at host rate sr: `run(x, y, limit)` takes host samples, gives the denoised ones `latency(sr)` later. */
93
+ function channel(sr) {
94
+ let st = create(net), frame = new Float32Array(FRAME), den = new Float32Array(FRAME), z = new Float32Array(FRAME)
95
+ // this frame's input and the two before it: RNNoise's output for frame f is frame f − 2's time
96
+ let dry = new Float32Array(3 * FRAME), fill = 0, frames = 0, lim = 0, q = queue(latency(sr))
97
+ let back = sr === RATE ? x => { for (let k = 0; k < x.length; k++) q.push(x[k]) } : resampler(RATE, sr, q.push)
98
+ let push = v => {
99
+ frame[fill] = v * 32768; dry[(frames % 3) * FRAME + fill] = v
100
+ if (++fill < FRAME) return
101
+ st.process(frame, den)
102
+ // as denoise()'s offline path: the output 960 samples on, the input mixed back at `lim`
103
+ if (frames >= 2) {
104
+ let d = ((frames - 2) % 3) * FRAME
105
+ for (let k = 0; k < FRAME; k++) z[k] = (den[k] / 32768) * (1 - lim) + dry[d + k] * lim
106
+ back(z)
107
+ }
108
+ frames++; fill = 0
109
+ }
110
+ let into = sr === RATE ? x => { for (let i = 0; i < x.length; i++) push(x[i]) } : resampler(sr, RATE, push)
111
+ return (x, y, limit) => {
112
+ lim = limit ? 10 ** (-Math.abs(limit) / 20) : 0
113
+ into(x)
114
+ q.pull(y)
115
+ }
116
+ }
117
+
118
+ export const rnnoise = (ctx) => {
119
+ let chans = [], sr = ctx.sampleRate
120
+ return (inputs, outputs, params) => {
121
+ let inp = inputs[0], out = outputs[0]
122
+ if (!inp || !inp.length) return
123
+ let limit = params.limit[0]
124
+ for (let c = 0; c < inp.length; c++) (chans[c] ??= channel(sr))(inp[c], out[c], limit)
125
+ }
126
+ }
127
+ rnnoise.channels = 'any'
128
+ rnnoise.latency = ctx => latency(ctx.sampleRate)
129
+ rnnoise.tail = 0
130
+ rnnoise.params = {
131
+ limit: { type: 'number', min: 0, max: 100, default: LIMIT, unit: 'dB' },
132
+ }