@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/deepfilter.js ADDED
@@ -0,0 +1,209 @@
1
+ // DeepFilterNet3: H. Schröter, T. Rosenkranz, A. N. Escalante-B., A. Maier, "DeepFilterNet: Perceptually
2
+ // Motivated Real-Time Speech Enhancement", Interspeech 2023 (arXiv:2305.08227). Two stages on a 10 ms
3
+ // STFT at 48 kHz: gains on 32 ERB bands for the envelope, then a complex 5-tap filter over time on each
4
+ // of the lowest 96 bins (up to 4.8 kHz) for the periodic structure. Upstream's three ONNX graphs
5
+ // (encoder, ERB decoder, deep-filter decoder) run through @audio/neural-runtime.
6
+ //
7
+ // Copyright (c) 2021 Hendrik Schröter. MIT License (upstream is MIT OR Apache-2.0); the notice is in NOTICE.
8
+ //
9
+ // Changes, 2026, audiojs: translated to JavaScript from Rikorose/DeepFilterNet at d375b2d: libDF's STFT,
10
+ // ERB filterbank and feature normalizations (libDF/src/lib.rs, transforms.rs) and df.enhance.enhance()
11
+ // with DfNet.forward (DeepFilterNet/df/enhance.py, deepfilternet3.py, modules.py, multiframe.py): the
12
+ // conv_lookahead shift, the ERB mask, the multi-frame deep filter, the attenuation limit. Model
13
+ // parameters are read from the export's config.ini. The FFT is RNNoise's (fft.js) in float32.
14
+ // Inputs longer than `chunk` frames run in chunks: upstream feeds the whole file, about 8 MB of
15
+ // activations per second of audio. Each chunk's run starts `warmup` frames early from zero GRU state
16
+ // (those outputs dropped) and continues `fade` frames past its end, where the next chunk crossfades in
17
+ // (raised cosine). The GRUs keep a long memory of their start, so chunked output is another valid
18
+ // trajectory, not the whole-file one; the STFT and the features stay exact across chunks.
19
+
20
+ import { load, tensor, fetchModel } from '@audio/neural-runtime'
21
+ import { WINDOW, rfft, irfft, N as FFT_N, HOP as FFT_HOP, BINS } from './fft.js'
22
+
23
+ const f = Math.fround
24
+
25
+ // models/DeepFilterNet3_onnx.tar.gz at d375b2d (7,983,136 bytes, sha256 c94d91f7…): enc, erb_dec, df_dec, config.ini
26
+ export const MODEL = 'https://raw.githubusercontent.com/Rikorose/DeepFilterNet/d375b2d8309e0935d165700c91da9de862a99c31/models/DeepFilterNet3_onnx.tar.gz'
27
+
28
+ // The attenuation limit, dB, that denoise() applies to this model unless given one (0: none, upstream's
29
+ // output). Unlimited, it takes the pauses of ten home narrations as low as -152 dBFS, the digital silence
30
+ // ACX Check flags; limited to 12 dB, they keep -91 to -57 dBFS at the same DNSMOS (README, Accuracy).
31
+ export const LIMIT = 12
32
+
33
+ const isNode = typeof process !== 'undefined' && !!process.versions?.node
34
+
35
+ // ------------------------------------------------ model archive
36
+
37
+ async function gunzip(bytes) {
38
+ if (isNode) return new Uint8Array((await import('node:zlib')).gunzipSync(bytes))
39
+ let stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'))
40
+ return new Uint8Array(await new Response(stream).arrayBuffer())
41
+ }
42
+
43
+ // ustar: 512-byte headers, octal sizes, data padded to 512
44
+ function untar(bytes) {
45
+ let files = {}, o = 0, str = (at, n) => new TextDecoder().decode(bytes.subarray(at, at + n)).replace(/\0.*$/s, '')
46
+ while (o + 512 <= bytes.length && bytes[o]) {
47
+ let name = str(o, 100), size = parseInt(str(o + 124, 12).trim() || '0', 8), prefix = str(o + 345, 155)
48
+ if (bytes[o + 156] === 48 || bytes[o + 156] === 0) files[(prefix ? prefix + '/' : '') + name] = bytes.subarray(o + 512, o + 512 + size)
49
+ o += 512 + Math.ceil(size / 512) * 512
50
+ }
51
+ return files
52
+ }
53
+
54
+ function ini(text) {
55
+ let out = {}, sec = out
56
+ for (let line of text.split(/\r?\n/)) {
57
+ let s = line.match(/^\s*\[(.+)\]\s*$/), kv = line.match(/^\s*([^=#;]+?)\s*=\s*(.*?)\s*$/)
58
+ if (s) sec = out[s[1]] = {}
59
+ else if (kv) sec[kv[1]] = kv[2]
60
+ }
61
+ return out
62
+ }
63
+
64
+ // load(model?) → { enc, erb, df, params }: the upstream export (tar.gz: enc.onnx, erb_dec.onnx, df_dec.onnx, config.ini)
65
+ export async function loadDeepFilter(model = MODEL, opts = {}) {
66
+ let bytes = typeof model === 'string' ? await fetchModel(model, opts) : new Uint8Array(model)
67
+ if (bytes[0] === 0x1f && bytes[1] === 0x8b) bytes = await gunzip(bytes)
68
+ let files = untar(bytes), pick = n => Object.entries(files).find(([k]) => k.endsWith('/' + n) || k === n)?.[1]
69
+ let [enc, erb, df, cfg] = ['enc.onnx', 'erb_dec.onnx', 'df_dec.onnx', 'config.ini'].map(pick)
70
+ if (!enc || !erb || !df || !cfg) throw new Error('neural-denoise: a DeepFilterNet export needs enc.onnx, erb_dec.onnx, df_dec.onnx and config.ini')
71
+ let c = ini(new TextDecoder().decode(cfg)), d = c.df, m = c.deepfilternet
72
+ if (c.train?.model !== 'deepfilternet3') throw new Error(`neural-denoise: model type '${c.train?.model}' is not deepfilternet3`)
73
+ let params = {
74
+ sr: +d.sr, n: +d.fft_size, hop: +d.hop_size, nbErb: +d.nb_erb, nbDf: +d.nb_df, order: +(d.df_order ?? m.df_order),
75
+ dfLa: +(d.df_lookahead ?? m.df_lookahead), convLa: +m.conv_lookahead, tau: +d.norm_tau, minErb: +d.min_nb_erb_freqs,
76
+ }
77
+ if (params.n !== FFT_N || params.hop !== FFT_HOP || params.sr !== 48000) throw new Error('neural-denoise: only 48 kHz models with 960/480 framing are supported')
78
+ params.widths = erbWidths(params.sr, params.n, params.nbErb, params.minErb)
79
+ params.alpha = normAlpha(params.sr, params.hop, params.tau)
80
+ let ld = opts.session ?? (b => load(b, { backend: opts.device, sessionOptions: opts.sessionOptions }))
81
+ let e = await ld(enc), r = await ld(erb), g = await ld(df)
82
+ return { enc: e, erb: r, df: g, params, free() { e.free(); r.free(); g.free() } }
83
+ }
84
+
85
+ // ------------------------------------------------ libDF features
86
+
87
+ const freq2erb = hz => f(f(9.265) * f(Math.log1p(f(hz / f(f(24.7) * f(9.265))))))
88
+ const erb2freq = e => f(f(f(24.7) * f(9.265)) * f(f(Math.exp(f(e / f(9.265)))) - 1))
89
+
90
+ // erb_fb: bins per band, equal ERB steps, at least `minNb` bins per band, all bins covered
91
+ export function erbWidths(sr, n, bands, minNb) {
92
+ let width = f(sr / n), lo = freq2erb(0), hi = freq2erb(sr >> 1), step = f(f(hi - lo) / bands)
93
+ let erb = new Array(bands).fill(0), prev = 0, over = 0
94
+ for (let i = 1; i <= bands; i++) {
95
+ let fb = Math.round(f(erb2freq(f(lo + f(i * step))) / width)), nb = fb - prev - over
96
+ if (nb < minNb) { over = minNb - nb; nb = minNb } else over = 0
97
+ erb[i - 1] = nb; prev = fb
98
+ }
99
+ erb[bands - 1] += 1
100
+ let extra = erb.reduce((a, b) => a + b, 0) - (n / 2 + 1)
101
+ if (extra > 0) erb[bands - 1] -= extra
102
+ return erb
103
+ }
104
+
105
+ // df.utils.get_norm_alpha: exp(−hop/sr/τ), rounded to the fewest decimals (from 3) that keep it below 1
106
+ function normAlpha(sr, hop, tau) {
107
+ let a = Math.exp(-hop / sr / tau), out = 1
108
+ for (let p = 3; out >= 1; p++) out = Math.round(a * 10 ** p) / 10 ** p
109
+ return f(out)
110
+ }
111
+
112
+ // ndarray's linspace, in f32 as libDF builds its initial normalization states
113
+ const linspace = (a, b, n) => { let step = f(f(f(b) - f(a)) / (n - 1)); return Float32Array.from({ length: n }, (_, i) => f(f(a) + f(step * i))) }
114
+
115
+ // ------------------------------------------------ enhance
116
+
117
+ // enhance(x, net, opts) → Float32Array: one channel at 48 kHz, as df.enhance.enhance(model, state, x, pad=True)
118
+ export async function enhance(x, net, { limit, chunk = 1000, warmup = 300, fade = 50 } = {}) {
119
+ let { hop, n, nbErb, nbDf, order, dfLa, convLa, widths, alpha } = net.params
120
+ let L = x.length, T = Math.floor((L + n) / hop), lead = order - 1 - dfLa, look = Math.max(convLa, dfLa)
121
+ let lim = limit != null && Math.abs(limit) > 0 ? 10 ** (-Math.abs(limit) / 20) : 0
122
+ // per-frame store, a ring big enough for one chunk with its context
123
+ chunk = Math.max(1, Math.min(chunk, T)); fade = Math.min(fade, chunk)
124
+ let cap = chunk + fade + warmup + look + lead + 2
125
+ let Sr = new Float32Array(cap * BINS), Si = new Float32Array(cap * BINS), FE = new Float32Array(cap * nbErb), FC = new Float32Array(cap * nbDf * 2)
126
+ let mean = linspace(-60, -90, nbErb), unit = linspace(0.001, 0.0001, nbDf), a1 = f(1 - alpha)
127
+ let mem = new Float32Array(hop), buf = new Float32Array(n), made = 0
128
+ let erbStart = new Int32Array(nbErb), erbK = new Float32Array(nbErb)
129
+ for (let b = 0, s = 0; b < nbErb; s += widths[b++]) { erbStart[b] = s; erbK[b] = f(1 / widths[b]) }
130
+
131
+ // frame_analysis + feat_erb + feat_cplx for frames up to t (sequential: the normalizations carry state)
132
+ function make(upto) {
133
+ for (; made <= upto && made < T; made++) {
134
+ let t = made, at = t * hop, slot = t % cap, o = slot * BINS
135
+ for (let i = 0; i < hop; i++) buf[i] = f(mem[i] * WINDOW[i])
136
+ for (let i = 0; i < hop; i++) { let v = at + i < L ? x[at + i] : 0; buf[hop + i] = f(v * WINDOW[hop + i]); mem[i] = v }
137
+ rfft(buf, Sr.subarray(o, o + BINS), Si.subarray(o, o + BINS))
138
+ for (let b = 0; b < nbErb; b++) {
139
+ let s = 0, k = erbK[b]
140
+ for (let j = erbStart[b], e = j + widths[b]; j < e; j++) s = f(s + f(f(f(Sr[o + j] * Sr[o + j]) + f(Si[o + j] * Si[o + j])) * k))
141
+ let v = f(f(Math.log10(f(s + f(1e-10)))) * 10)
142
+ mean[b] = f(f(v * a1) + f(mean[b] * alpha))
143
+ FE[slot * nbErb + b] = f(f(v - mean[b]) / 40)
144
+ }
145
+ for (let j = 0; j < nbDf; j++) {
146
+ let re = Sr[o + j], im = Si[o + j]
147
+ unit[j] = f(f(f(Math.hypot(re, im)) * a1) + f(unit[j] * alpha))
148
+ let d = f(Math.sqrt(unit[j]))
149
+ FC[slot * nbDf * 2 + j] = f(re / d); FC[slot * nbDf * 2 + nbDf + j] = f(im / d)
150
+ }
151
+ }
152
+ }
153
+
154
+ let out = new Float32Array(L), syn = new Float32Array(hop), y = new Float32Array(n), d = n - hop
155
+ let Er = new Float32Array(BINS), Ei = new Float32Array(BINS)
156
+ // a chunk's run continues `fade` frames past its end; the next chunk crossfades from those spectra
157
+ let tailR = new Float32Array(fade * BINS), tailI = new Float32Array(fade * BINS)
158
+ for (let a = 0, b; a < T; a = b) {
159
+ b = Math.min(T, a + chunk)
160
+ let e = Math.min(T, b + fade), a0 = Math.max(0, a - warmup), S = e - a0
161
+ make(Math.min(T - 1, e - 1 + look))
162
+ // network input: features shifted by conv_lookahead (DfNet.pad_feat), zeros past the end
163
+ let fe = new Float32Array(S * nbErb), fc = new Float32Array(2 * S * nbDf)
164
+ for (let s = 0; s < S; s++) {
165
+ let t = a0 + s + convLa
166
+ if (t >= T) continue
167
+ let slot = t % cap
168
+ fe.set(FE.subarray(slot * nbErb, slot * nbErb + nbErb), s * nbErb)
169
+ fc.set(FC.subarray(slot * nbDf * 2, slot * nbDf * 2 + nbDf), s * nbDf)
170
+ fc.set(FC.subarray(slot * nbDf * 2 + nbDf, slot * nbDf * 2 + 2 * nbDf), S * nbDf + s * nbDf)
171
+ }
172
+ let enc = await net.enc.run({ feat_erb: tensor(fe, [1, 1, S, nbErb]), feat_spec: tensor(fc, [1, 2, S, nbDf]) })
173
+ let M = (await net.erb.run({ emb: enc.emb, e3: enc.e3, e2: enc.e2, e1: enc.e1, e0: enc.e0 })).m.data
174
+ let C = (await net.df.run({ emb: enc.emb, c0: enc.c0 }, ['coefs'])).coefs.data
175
+ for (let t = a; t < e; t++) {
176
+ let s = t - a0, o = (t % cap) * BINS
177
+ // ERB gains (Mask with the unnormalized inverse filterbank: each bin takes its band's gain)
178
+ for (let band = 0; band < nbErb; band++) {
179
+ let g = M[s * nbErb + band]
180
+ for (let j = erbStart[band], end = j + widths[band]; j < end; j++) { Er[j] = f(Sr[o + j] * g); Ei[j] = f(Si[o + j] * g) }
181
+ }
182
+ // deep filter on the unmasked spectrum: Σₖ spec[t − lead + k] · coef[t, k], complex, below nbDf
183
+ for (let j = 0; j < nbDf; j++) { Er[j] = 0; Ei[j] = 0 }
184
+ for (let k = 0; k < order; k++) {
185
+ let tt = t - lead + k
186
+ if (tt < 0 || tt >= T) continue
187
+ let q = (tt % cap) * BINS, c = s * nbDf * order * 2
188
+ for (let j = 0; j < nbDf; j++) {
189
+ let cr = C[c + j * order * 2 + 2 * k], ci = C[c + j * order * 2 + 2 * k + 1], sr = Sr[q + j], si = Si[q + j]
190
+ Er[j] = f(Er[j] + f(f(sr * cr) - f(si * ci))); Ei[j] = f(Ei[j] + f(f(sr * ci) + f(si * cr)))
191
+ }
192
+ }
193
+ if (lim) for (let j = 0; j < BINS; j++) { Er[j] = f(f(Sr[o + j] * lim) + f(Er[j] * (1 - lim))); Ei[j] = f(f(Si[o + j] * lim) + f(Ei[j] * (1 - lim))) }
194
+ if (t >= b) { tailR.set(Er, (t - b) * BINS); tailI.set(Ei, (t - b) * BINS); continue }
195
+ if (a > 0 && t - a < fade) {
196
+ let w = .5 - .5 * Math.cos(Math.PI * (t - a + .5) / fade), q = (t - a) * BINS
197
+ for (let j = 0; j < BINS; j++) { Er[j] = f(tailR[q + j] + w * (Er[j] - tailR[q + j])); Ei[j] = f(tailI[q + j] + w * (Ei[j] - tailI[q + j])) }
198
+ }
199
+ // frame_synthesis: unnormalized inverse, window, overlap-add
200
+ irfft(Er, Ei, y)
201
+ for (let i = 0; i < hop; i++) {
202
+ let v = f(f(y[i] * WINDOW[i]) + syn[i]), p = t * hop + i - d
203
+ if (p >= 0 && p < L) out[p] = v
204
+ syn[i] = f(y[hop + i] * WINDOW[hop + i])
205
+ }
206
+ }
207
+ }
208
+ return out
209
+ }
package/denoise.js ADDED
@@ -0,0 +1,74 @@
1
+ // Neural speech enhancement: RNNoise (pure JS, weights bundled) and DeepFilterNet3 (ONNX through
2
+ // @audio/neural-runtime, weights fetched from upstream and cached). Both run at 48 kHz on 10 ms hops;
3
+ // other rates are resampled in and out with @audio/resample-sinc. Offline results are aligned with the
4
+ // input: the models' delay is compensated.
5
+
6
+ import resample from '@audio/resample-sinc'
7
+ import { model as parseRNNoise, create, FRAME, LIMIT as RNNOISE_LIMIT } from './rnnoise.js'
8
+ import { loadDeepFilter, enhance, MODEL, LIMIT as DFN_LIMIT } from './deepfilter.js'
9
+
10
+ export { create as rnnoise, FRAME } from './rnnoise.js'
11
+ export { MODEL } from './deepfilter.js'
12
+
13
+ const RATE = 48000
14
+ // the attenuation limit (dB) each model gets when opts.limit is not given; 0 is upstream's output
15
+ const LIMITS = { rnnoise: RNNOISE_LIMIT, deepfilternet3: DFN_LIMIT }
16
+
17
+ // weights() → the bundled RNNoise weights (upstream's weight blob, 3,544,320 bytes), for the worklet
18
+ export async function weights() {
19
+ let url = new URL('./rnnoise.bin', import.meta.url)
20
+ if (url.protocol === 'file:') return new Uint8Array((await import('node:fs')).readFileSync(url))
21
+ let res = await fetch(url)
22
+ if (!res.ok) throw new Error(`neural-denoise: can't fetch ${url}: ${res.status}`)
23
+ return new Uint8Array(await res.arrayBuffer())
24
+ }
25
+
26
+ let bundled
27
+ // load(model?, opts?) → a handle denoise() reuses: 'rnnoise' (default) or 'deepfilternet3'
28
+ export async function load(model = 'rnnoise', opts = {}) {
29
+ if (model === 'rnnoise') {
30
+ let net = opts.weights ? parseRNNoise(opts.weights) : await (bundled ??= weights().then(parseRNNoise))
31
+ return { model, net, latency: 2 * FRAME, free() {} }
32
+ }
33
+ if (model === 'deepfilternet3') {
34
+ // ORT's arena keeps the largest chunk's activations: off, a 180 s file peaks at 563 MB instead of 665
35
+ let net = await loadDeepFilter(opts.weights ?? MODEL, { ...opts, sessionOptions: { enableCpuMemArena: false, enableMemPattern: false, ...opts.sessionOptions } })
36
+ return { model, net, latency: (net.params.n - net.params.hop) + net.params.hop * Math.max(net.params.convLa, net.params.dfLa), free: () => net.free() }
37
+ }
38
+ throw new Error(`neural-denoise: unknown model '${model}' (want 'rnnoise' or 'deepfilternet3')`)
39
+ }
40
+
41
+ // denoise(audio, opts) → the same shape back: Float32Array, Float32Array[] or { channelData, sampleRate }
42
+ export default async function denoise(audio, opts = {}) {
43
+ let channels, rate = opts.sampleRate
44
+ if (audio instanceof Float32Array) channels = [audio]
45
+ else if (Array.isArray(audio)) channels = audio
46
+ else if (audio?.channelData) { channels = audio.channelData; rate = audio.sampleRate ?? rate }
47
+ else throw new TypeError('neural-denoise: audio must be a Float32Array, Float32Array[] or { channelData, sampleRate }')
48
+ if (!(rate > 0)) throw new TypeError('neural-denoise: sampleRate is required (opts.sampleRate, or audio.sampleRate)')
49
+ let handle = typeof opts.model === 'object' && opts.model ? opts.model : await load(opts.model, opts)
50
+ try {
51
+ let out = [], limit = opts.limit ?? LIMITS[handle.model]
52
+ for (let ch of channels) {
53
+ let x = rate === RATE ? Float32Array.from(ch) : resample(Float32Array.from(ch), { from: rate, to: RATE })
54
+ let y = handle.model === 'rnnoise' ? rnnoiseOffline(x, handle.net, limit) : await enhance(x, handle.net, { ...opts, limit })
55
+ out.push(rate === RATE ? y : fit(resample(y, { from: RATE, to: rate }), ch.length))
56
+ }
57
+ return audio instanceof Float32Array ? out[0] : Array.isArray(audio) ? out : { ...audio, channelData: out, sampleRate: rate }
58
+ } finally {
59
+ if (handle !== opts.model) handle.free()
60
+ }
61
+ }
62
+
63
+ const fit = (y, n) => y.length === n ? y : (() => { let o = new Float32Array(n); o.set(y.subarray(0, n)); return o })()
64
+
65
+ // One channel through RNNoise at int16 scale, delay removed: out[i] lines up with x[i]
66
+ function rnnoiseOffline(x, net, limit) {
67
+ let st = create(net), delay = 2 * FRAME, L = x.length, n = Math.ceil((L + delay) / FRAME) * FRAME
68
+ let pad = new Float32Array(n), y = new Float32Array(n)
69
+ for (let i = 0; i < L; i++) pad[i] = x[i] * 32768
70
+ for (let i = 0; i < n; i += FRAME) st.process(pad.subarray(i, i + FRAME), y.subarray(i, i + FRAME))
71
+ let lim = limit ? 10 ** (-Math.abs(limit) / 20) : 0, out = new Float32Array(L)
72
+ for (let i = 0; i < L; i++) out[i] = (y[i + delay] / 32768) * (1 - lim) + x[i] * lim
73
+ return out
74
+ }
package/fft.js ADDED
@@ -0,0 +1,135 @@
1
+ // The 960-point FFT and 20 ms Vorbis window both models frame speech with (10 ms hop at 48 kHz).
2
+ //
3
+ // Copyright (c) 2003-2004 Mark Borgerding; 2005-2009 Xiph.Org Foundation; 2008 CSIRO; 2017-2018 Mozilla;
4
+ // 2023 Amazon. BSD-3-Clause: the conditions and the disclaimer are in LICENSE.
5
+ //
6
+ // Changes, 2026, audiojs: translated to JavaScript from xiph/rnnoise at 70f1d25 (src/kiss_fft.c,
7
+ // _kiss_fft_guts.h, dump_rnnoise_tables.c): the float build of kiss_fft fixed at 960 points, radices
8
+ // 5·3·4·4·4 as kf_factor orders them, twiddles and bit reversal generated as the tables generator does
9
+ // (equal to rnnoise_tables.c), every float op rounded as float32.
10
+
11
+ const f = Math.fround
12
+
13
+ export const N = 960, HOP = 480, BINS = 481
14
+
15
+ // sin(π/2 · sin²(π(i + ½)/960)): Vorbis power-complementary window (rnn_half_window, libDF DFState::new)
16
+ export const WINDOW = new Float32Array(N)
17
+ for (let i = 0; i < HOP; i++) {
18
+ let s = Math.sin(.5 * Math.PI * (i + .5) / HOP)
19
+ WINDOW[i] = WINDOW[N - 1 - i] = Math.sin(.5 * Math.PI * s * s)
20
+ }
21
+
22
+ const FACTORS = [5, 192, 3, 64, 4, 16, 4, 4, 4, 1]
23
+ const SCALE = f(0.0010416667) // kiss_fft_state.scale as rnnoise_tables.c prints it: 1/960 in float32
24
+ const TW_R = new Float32Array(N), TW_I = new Float32Array(N)
25
+ for (let i = 0; i < N; i++) {
26
+ let phase = (-2 * 3.14159265358979323846264338327 / N) * i
27
+ TW_R[i] = Math.cos(phase); TW_I[i] = Math.sin(phase)
28
+ }
29
+ const BITREV = new Int32Array(N)
30
+ ;(function bitrev(fout, at, fstride, k) {
31
+ let p = FACTORS[k], m = FACTORS[k + 1]
32
+ if (m === 1) for (let j = 0; j < p; j++, at += fstride) BITREV[at] = fout + j
33
+ else for (let j = 0; j < p; j++, at += fstride, fout += m) bitrev(fout, at, fstride * p, k + 2)
34
+ })(0, 0, 1, 0)
35
+
36
+ // rnn_fft_c: out = FFT(in)/960, complex values as separate re/im arrays of 960
37
+ export function fft(ir, ii, or, oi) {
38
+ for (let i = 0; i < N; i++) { or[BITREV[i]] = f(SCALE * ir[i]); oi[BITREV[i]] = f(SCALE * ii[i]) }
39
+ bfly4d(or, oi, 240)
40
+ bfly4(or, oi, 60, 4, 60, 16)
41
+ bfly4(or, oi, 15, 16, 15, 64)
42
+ bfly3(or, oi, 5, 64, 5, 192)
43
+ bfly5(or, oi, 1, 192)
44
+ }
45
+
46
+ const ZERO = new Float32Array(N), FR = new Float32Array(N), FI = new Float32Array(N), IR = new Float32Array(N), II = new Float32Array(N)
47
+
48
+ // forward_transform: 481 bins of a real 960-sample frame, scaled by 1/960 (libDF's wnorm is the same 1/960)
49
+ export function rfft(x, Xr, Xi) {
50
+ fft(x, ZERO, FR, FI)
51
+ for (let i = 0; i < BINS; i++) { Xr[i] = FR[i]; Xi[i] = FI[i] }
52
+ }
53
+
54
+ // inverse_transform: unnormalized inverse of 481 bins (Hermitian extension; imaginary parts of the
55
+ // DC and Nyquist bins ignored, as realfft's ComplexToReal does)
56
+ export function irfft(Xr, Xi, x) {
57
+ for (let i = 0; i < BINS; i++) { IR[i] = Xr[i]; II[i] = Xi[i] }
58
+ for (let i = BINS; i < N; i++) { IR[i] = IR[N - i]; II[i] = -II[N - i] }
59
+ fft(IR, II, FR, FI)
60
+ x[0] = f(N * FR[0])
61
+ for (let i = 1; i < N; i++) x[i] = f(N * FR[N - i])
62
+ }
63
+
64
+ function bfly4d(r, im, n) {
65
+ for (let i = 0, o = 0; i < n; i++, o += 4) {
66
+ let s0r = f(r[o] - r[o + 2]), s0i = f(im[o] - im[o + 2])
67
+ r[o] = f(r[o] + r[o + 2]); im[o] = f(im[o] + im[o + 2])
68
+ let s1r = f(r[o + 1] + r[o + 3]), s1i = f(im[o + 1] + im[o + 3])
69
+ r[o + 2] = f(r[o] - s1r); im[o + 2] = f(im[o] - s1i)
70
+ r[o] = f(r[o] + s1r); im[o] = f(im[o] + s1i)
71
+ s1r = f(r[o + 1] - r[o + 3]); s1i = f(im[o + 1] - im[o + 3])
72
+ r[o + 1] = f(s0r + s1i); im[o + 1] = f(s0i - s1r)
73
+ r[o + 3] = f(s0r - s1i); im[o + 3] = f(s0i + s1r)
74
+ }
75
+ }
76
+
77
+ function bfly4(r, im, fstride, m, n, mm) {
78
+ let m2 = 2 * m, m3 = 3 * m
79
+ for (let i = 0; i < n; i++) {
80
+ for (let j = 0, o = i * mm; j < m; j++, o++) {
81
+ let t1 = j * fstride, t2 = 2 * t1, t3 = 3 * t1, a = o + m, b = o + m2, c = o + m3
82
+ let s0r = f(f(r[a] * TW_R[t1]) - f(im[a] * TW_I[t1])), s0i = f(f(r[a] * TW_I[t1]) + f(im[a] * TW_R[t1]))
83
+ let s1r = f(f(r[b] * TW_R[t2]) - f(im[b] * TW_I[t2])), s1i = f(f(r[b] * TW_I[t2]) + f(im[b] * TW_R[t2]))
84
+ let s2r = f(f(r[c] * TW_R[t3]) - f(im[c] * TW_I[t3])), s2i = f(f(r[c] * TW_I[t3]) + f(im[c] * TW_R[t3]))
85
+ let s5r = f(r[o] - s1r), s5i = f(im[o] - s1i)
86
+ r[o] = f(r[o] + s1r); im[o] = f(im[o] + s1i)
87
+ let s3r = f(s0r + s2r), s3i = f(s0i + s2i), s4r = f(s0r - s2r), s4i = f(s0i - s2i)
88
+ r[b] = f(r[o] - s3r); im[b] = f(im[o] - s3i)
89
+ r[o] = f(r[o] + s3r); im[o] = f(im[o] + s3i)
90
+ r[a] = f(s5r + s4i); im[a] = f(s5i - s4r)
91
+ r[c] = f(s5r - s4i); im[c] = f(s5i + s4r)
92
+ }
93
+ }
94
+ }
95
+
96
+ function bfly3(r, im, fstride, m, n, mm) {
97
+ let m2 = 2 * m, epi = TW_I[fstride * m]
98
+ for (let i = 0; i < n; i++) {
99
+ for (let k = 0, o = i * mm; k < m; k++, o++) {
100
+ let t1 = k * fstride, t2 = 2 * t1, a = o + m, b = o + m2
101
+ let s1r = f(f(r[a] * TW_R[t1]) - f(im[a] * TW_I[t1])), s1i = f(f(r[a] * TW_I[t1]) + f(im[a] * TW_R[t1]))
102
+ let s2r = f(f(r[b] * TW_R[t2]) - f(im[b] * TW_I[t2])), s2i = f(f(r[b] * TW_I[t2]) + f(im[b] * TW_R[t2]))
103
+ let s3r = f(s1r + s2r), s3i = f(s1i + s2i), s0r = f(s1r - s2r), s0i = f(s1i - s2i)
104
+ r[a] = f(r[o] - f(s3r * .5)); im[a] = f(im[o] - f(s3i * .5))
105
+ s0r = f(s0r * epi); s0i = f(s0i * epi)
106
+ r[o] = f(r[o] + s3r); im[o] = f(im[o] + s3i)
107
+ r[b] = f(r[a] + s0i); im[b] = f(im[a] - s0r)
108
+ r[a] = f(r[a] - s0i); im[a] = f(im[a] + s0r)
109
+ }
110
+ }
111
+ }
112
+
113
+ function bfly5(r, im, fstride, m) {
114
+ let yar = TW_R[fstride * m], yai = TW_I[fstride * m], ybr = TW_R[2 * fstride * m], ybi = TW_I[2 * fstride * m]
115
+ for (let u = 0; u < m; u++) {
116
+ let o0 = u, o1 = u + m, o2 = u + 2 * m, o3 = u + 3 * m, o4 = u + 4 * m
117
+ let t1 = u * fstride, t2 = 2 * t1, t3 = 3 * t1, t4 = 4 * t1
118
+ let s0r = r[o0], s0i = im[o0]
119
+ let s1r = f(f(r[o1] * TW_R[t1]) - f(im[o1] * TW_I[t1])), s1i = f(f(r[o1] * TW_I[t1]) + f(im[o1] * TW_R[t1]))
120
+ let s2r = f(f(r[o2] * TW_R[t2]) - f(im[o2] * TW_I[t2])), s2i = f(f(r[o2] * TW_I[t2]) + f(im[o2] * TW_R[t2]))
121
+ let s3r = f(f(r[o3] * TW_R[t3]) - f(im[o3] * TW_I[t3])), s3i = f(f(r[o3] * TW_I[t3]) + f(im[o3] * TW_R[t3]))
122
+ let s4r = f(f(r[o4] * TW_R[t4]) - f(im[o4] * TW_I[t4])), s4i = f(f(r[o4] * TW_I[t4]) + f(im[o4] * TW_R[t4]))
123
+ let s7r = f(s1r + s4r), s7i = f(s1i + s4i), s10r = f(s1r - s4r), s10i = f(s1i - s4i)
124
+ let s8r = f(s2r + s3r), s8i = f(s2i + s3i), s9r = f(s2r - s3r), s9i = f(s2i - s3i)
125
+ r[o0] = f(r[o0] + f(s7r + s8r)); im[o0] = f(im[o0] + f(s7i + s8i))
126
+ let s5r = f(s0r + f(f(s7r * yar) + f(s8r * ybr))), s5i = f(s0i + f(f(s7i * yar) + f(s8i * ybr)))
127
+ let s6r = f(f(s10i * yai) + f(s9i * ybi)), s6i = -f(f(s10r * yai) + f(s9r * ybi))
128
+ r[o1] = f(s5r - s6r); im[o1] = f(s5i - s6i)
129
+ r[o4] = f(s5r + s6r); im[o4] = f(s5i + s6i)
130
+ let s11r = f(s0r + f(f(s7r * ybr) + f(s8r * yar))), s11i = f(s0i + f(f(s7i * ybr) + f(s8i * yar)))
131
+ let s12r = f(f(s9i * yai) - f(s10i * ybi)), s12i = f(f(s10r * ybi) - f(s9r * yai))
132
+ r[o2] = f(s11r + s12r); im[o2] = f(s11i + s12i)
133
+ r[o3] = f(s11r - s12r); im[o3] = f(s11i - s12i)
134
+ }
135
+ }
package/gemv.wat ADDED
@@ -0,0 +1,38 @@
1
+ ;; RNNoise's int8 matrix-vector products (sparse_cgemv8x4 in xiph/rnnoise vec.h, without the scale) in
2
+ ;; WebAssembly SIMD, embedded in rnnoise.js as bytes. Rebuild: wat2wasm gemv.wat -o gemv.wasm (WABT).
3
+ (module
4
+ (memory (export "memory") 1)
5
+ ;; gemv(w, idx, x, out, rows): int32 out[r] = Σ w[r, c] · x[c] over upstream's 8×4-blocked int8 layout
6
+ ;; (sparse_cgemv8x4 without the scale): per 8-row block, idx holds a count, then the column of each 4-wide block.
7
+ (func (export "gemv") (param $w i32) (param $idx i32) (param $x i32) (param $out i32) (param $rows i32)
8
+ (local $i i32) (local $n i32) (local $x16 v128) (local $wv v128)
9
+ (local $a v128) (local $b v128) (local $c v128) (local $d v128)
10
+ (loop $block
11
+ (local.set $n (i32.load (local.get $idx)))
12
+ (local.set $idx (i32.add (local.get $idx) (i32.const 4)))
13
+ (local.set $a (v128.const i32x4 0 0 0 0)) (local.set $b (v128.const i32x4 0 0 0 0))
14
+ (local.set $c (v128.const i32x4 0 0 0 0)) (local.set $d (v128.const i32x4 0 0 0 0))
15
+ (block $done
16
+ (loop $cols
17
+ (br_if $done (i32.eqz (local.get $n)))
18
+ (local.set $x16 (i16x8.extend_low_i8x16_s (i32x4.splat (i32.load (i32.add (local.get $x) (i32.load (local.get $idx)))))))
19
+ (local.set $wv (v128.load (local.get $w)))
20
+ (local.set $a (i32x4.add (local.get $a) (i32x4.dot_i16x8_s (i16x8.extend_low_i8x16_s (local.get $wv)) (local.get $x16))))
21
+ (local.set $b (i32x4.add (local.get $b) (i32x4.dot_i16x8_s (i16x8.extend_high_i8x16_s (local.get $wv)) (local.get $x16))))
22
+ (local.set $wv (v128.load offset=16 (local.get $w)))
23
+ (local.set $c (i32x4.add (local.get $c) (i32x4.dot_i16x8_s (i16x8.extend_low_i8x16_s (local.get $wv)) (local.get $x16))))
24
+ (local.set $d (i32x4.add (local.get $d) (i32x4.dot_i16x8_s (i16x8.extend_high_i8x16_s (local.get $wv)) (local.get $x16))))
25
+ (local.set $w (i32.add (local.get $w) (i32.const 32)))
26
+ (local.set $idx (i32.add (local.get $idx) (i32.const 4)))
27
+ (local.set $n (i32.sub (local.get $n) (i32.const 1)))
28
+ (br $cols)))
29
+ ;; lanes hold half-rows: row = lane pair sum
30
+ (v128.store (local.get $out) (i32x4.add
31
+ (i8x16.shuffle 0 1 2 3 8 9 10 11 16 17 18 19 24 25 26 27 (local.get $a) (local.get $b))
32
+ (i8x16.shuffle 4 5 6 7 12 13 14 15 20 21 22 23 28 29 30 31 (local.get $a) (local.get $b))))
33
+ (v128.store offset=16 (local.get $out) (i32x4.add
34
+ (i8x16.shuffle 0 1 2 3 8 9 10 11 16 17 18 19 24 25 26 27 (local.get $c) (local.get $d))
35
+ (i8x16.shuffle 4 5 6 7 12 13 14 15 20 21 22 23 28 29 30 31 (local.get $c) (local.get $d))))
36
+ (local.set $out (i32.add (local.get $out) (i32.const 32)))
37
+ (local.set $i (i32.add (local.get $i) (i32.const 8)))
38
+ (br_if $block (i32.lt_u (local.get $i) (local.get $rows))))))
package/index.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ /** Neural speech enhancement: RNNoise (pure JS, weights bundled) and DeepFilterNet3 (ONNX, fetched and cached). */
2
+
3
+ export type Audio = Float32Array | Float32Array[] | { channelData: Float32Array[], sampleRate: number }
4
+
5
+ export interface Model {
6
+ model: 'rnnoise' | 'deepfilternet3'
7
+ /** algorithmic delay in samples at 48 kHz: 960 (RNNoise), 1440 (DeepFilterNet3); offline results are aligned */
8
+ latency: number
9
+ /** releases the ONNX sessions (DeepFilterNet3); no-op for RNNoise */
10
+ free(): void
11
+ }
12
+
13
+ export interface LoadOptions {
14
+ /** RNNoise: bytes of an upstream weight blob (dump_weights_blob), default the bundled model. DeepFilterNet3: URL or bytes of an upstream ONNX export (.tar.gz), default MODEL */
15
+ weights?: string | Uint8Array | ArrayBuffer
16
+ /** DeepFilterNet3: @audio/neural-runtime backend, 'node' | 'wasm' | 'webgpu'; default 'auto' */
17
+ device?: 'auto' | 'node' | 'wasm' | 'webgpu'
18
+ /** DeepFilterNet3: ort.InferenceSession options passed through, e.g. { intraOpNumThreads: 4 } */
19
+ sessionOptions?: Record<string, unknown>
20
+ /** DeepFilterNet3: replaces @audio/neural-runtime's load() for each graph (a custom ORT setup, a test double) */
21
+ session?: (bytes: Uint8Array) => Promise<unknown>
22
+ /** fetch progress, as @audio/neural-runtime reports it */
23
+ progress?: (p: { loaded: number, total: number | null }) => void
24
+ }
25
+
26
+ export interface DenoiseOptions extends LoadOptions {
27
+ /** Hz; required unless audio is { channelData, sampleRate } */
28
+ sampleRate?: number
29
+ /** 'rnnoise' (default), 'deepfilternet3', or a handle from load() to reuse across calls */
30
+ model?: 'rnnoise' | 'deepfilternet3' | Model
31
+ /** attenuation limit in dB: mixes the input back in at 10^(−limit/20); default 20 for RNNoise, 12 for DeepFilterNet3 (README, API); 0 for none, upstream's output */
32
+ limit?: number
33
+ /** DeepFilterNet3: frames (10 ms) per model run, default 1000 */
34
+ chunk?: number
35
+ /** DeepFilterNet3: frames of context run before each chunk after the first, default 300 */
36
+ warmup?: number
37
+ /** DeepFilterNet3: frames over which consecutive chunks crossfade, default 50 */
38
+ fade?: number
39
+ }
40
+
41
+ /** Denoise speech; returns the same shape as `audio`, same length, aligned with it. */
42
+ export default function denoise<T extends Audio>(audio: T, opts?: DenoiseOptions): Promise<T>
43
+
44
+ /** Load a model once to reuse it across denoise() calls. */
45
+ export function load(model?: 'rnnoise' | 'deepfilternet3', opts?: LoadOptions): Promise<Model>
46
+
47
+ /** The bundled RNNoise weights (3,544,320 bytes), e.g. for the worklet's processorOptions.weights. */
48
+ export function weights(): Promise<Uint8Array>
49
+
50
+ /** RNNoise frame by frame, as rnnoise_process_frame: 480 samples at 48 kHz and int16 scale in and out; returns the frame's voice probability. Output lags input by 960 samples. */
51
+ export function rnnoise(weights: Uint8Array | ArrayBuffer | object): { process(input: Float32Array, output: Float32Array): number, latency: number }
52
+
53
+ /** RNNoise frame length: 480 samples, 10 ms at 48 kHz. */
54
+ export const FRAME: 480
55
+
56
+ /** Upstream's DeepFilterNet3 ONNX export at Rikorose/DeepFilterNet d375b2d, fetched on first use and cached. */
57
+ export const MODEL: string
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@audio/neural-denoise",
3
+ "version": "0.1.0",
4
+ "description": "Neural speech enhancement: RNNoise (Valin 2018) in pure JS, bit-exact to its C, weights bundled; DeepFilterNet3 through ONNX Runtime, matching the Python original",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "denoise.js",
8
+ "types": "./index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./index.d.ts",
12
+ "default": "./denoise.js"
13
+ },
14
+ "./audio": {
15
+ "types": "./audio.d.ts",
16
+ "default": "./audio.js"
17
+ },
18
+ "./worklet": "./worklet.js",
19
+ "./rnnoise.bin": "./rnnoise.bin",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "files": [
23
+ "denoise.js",
24
+ "audio.js",
25
+ "rnnoise.js",
26
+ "deepfilter.js",
27
+ "fft.js",
28
+ "worklet.js",
29
+ "rnnoise.bin",
30
+ "gemv.wat",
31
+ "index.d.ts",
32
+ "LICENSE",
33
+ "NOTICE",
34
+ "audio.d.ts"
35
+ ],
36
+ "dependencies": {
37
+ "@audio/neural-runtime": "^0.1.0",
38
+ "@audio/resample-sinc": "^1.2.0"
39
+ },
40
+ "devDependencies": {
41
+ "audio-lena": "^3.0.1",
42
+ "onnxruntime-node": "^1.29.0",
43
+ "tst": "^9.4.0"
44
+ },
45
+ "scripts": {
46
+ "test": "node test.js"
47
+ },
48
+ "keywords": [
49
+ "audio",
50
+ "ml",
51
+ "neural",
52
+ "denoise",
53
+ "noise-suppression",
54
+ "speech-enhancement",
55
+ "rnnoise",
56
+ "deepfilternet",
57
+ "audioworklet",
58
+ "onnx"
59
+ ],
60
+ "license": "BSD-3-Clause AND MIT",
61
+ "author": "Dmitry Iv. <dfcreative@gmail.com>",
62
+ "contributors": [
63
+ "Jean-Marc Valin, Xiph.Org Foundation, Mozilla, Amazon and the RNNoise authors (RNNoise)",
64
+ "Hendrik Schröter (DeepFilterNet)"
65
+ ],
66
+ "audio": "./audio.js",
67
+ "engines": {
68
+ "node": ">=18"
69
+ },
70
+ "repository": {
71
+ "type": "git",
72
+ "url": "git+https://github.com/audiojs/neural.git",
73
+ "directory": "packages/neural-denoise"
74
+ },
75
+ "funding": "https://github.com/sponsors/audiojs",
76
+ "publishConfig": {
77
+ "access": "public"
78
+ }
79
+ }
package/rnnoise.bin ADDED
Binary file