@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/rnnoise.js ADDED
@@ -0,0 +1,615 @@
1
+ // RNNoise: J.-M. Valin, "A Hybrid DSP/Deep Learning Approach to Real-Time Full-Band Speech
2
+ // Enhancement", MMSP 2018 (arXiv:1709.08243). A network of 1.8M weights predicts 32 ERB-band gains
3
+ // and a voice probability per 10 ms frame at 48 kHz; DSP does the rest: 32 bands, pitch search, a
4
+ // pitch filter restoring voiced harmonics between the bands, gain smoothing.
5
+ //
6
+ // Copyright (c) 2007-2017, 2024 Jean-Marc Valin; 2023 Amazon; 2017-2019 Mozilla; 2005-2017 Xiph.Org
7
+ // Foundation; 2008-2011 Octasic Inc.; 2007-2008 CSIRO; 2018 Gregor Richards. BSD-3-Clause: the
8
+ // conditions and the disclaimer are in LICENSE.
9
+ //
10
+ // Changes, 2026, audiojs: translated to JavaScript from xiph/rnnoise at 70f1d25 (src/denoise.c, rnn.c,
11
+ // nnet.c, nnet_arch.h, vec.h, pitch.c, celt_lpc.c, parse_lpcnet_weights.c); the portable C path
12
+ // (vec.h without SIMD) op for op in float32, so the output is bit-exact to that build compiled
13
+ // without FMA contraction (-ffp-contract=off). One file; state in typed arrays, no allocation per
14
+ // frame; weights parsed from upstream's blob (rnnoise_model_from_buffer, parse_weights, linear_init
15
+ // with the same checks). The int8 products run in a 419-byte WebAssembly SIMD kernel (gemv.wat) where
16
+ // available, else in JS: integer sums, identical either way. Not ported: x86 SU biases and unsigned
17
+ // activations (vec_avx.h), training.
18
+ //
19
+ // Float semantics: each C float op is Math.fround(a op b). For +, −, ×, ÷ and sqrt of float32
20
+ // operands the double result rounded to float32 equals the float32 operation (S. A. Figueroa,
21
+ // "When is double rounding innocuous?", SIGNUM Newsletter 30(3), 1995: 53 ≥ 2·24 + 2). Where the C
22
+ // promotes to double (double literals such as 1e-3, libm log10 and sqrt) the JS stays in double and
23
+ // rounds on the store, as the C does.
24
+
25
+ import { WINDOW, rfft, irfft } from './fft.js'
26
+
27
+ const f = Math.fround
28
+
29
+ // C float literals (the f suffix), rounded once as the compiler does
30
+ const F = { c06: f(.6), c07: f(.7), c085: f(.85), c09: f(.9), c03: f(.3), c04: f(.4), c05: f(.5), c08: f(.8), c008: f(.008),
31
+ c10001: f(1.0001), c0001: f(.001), c1e12: f(1e-12), hpA0: f(-1.99599), hpA1: f(0.99600),
32
+ N0: f(952.52801514), N1: f(96.39235687), N2: f(0.60863042), D0: f(952.72399902), D1: f(413.36801147), D2: f(11.88600922) }
33
+
34
+ export const FRAME = 480
35
+ // The attenuation limit, dB, that denoise(), the worklet and the audio atom apply unless given one (0:
36
+ // none, upstream's output). Unlimited, this model removes the voice on 12 of the 824 VoiceBank+DEMAND
37
+ // test files (STOI down by more than 0.2); limited to 20 dB, on none (README, Accuracy).
38
+ export const LIMIT = 20
39
+ const WIN = 960, FREQ = 481, NB = 32, NF = 65
40
+ const PMIN = 60, PMAX = 768, PFRAME = 960, PBUF = PMAX + PFRAME
41
+
42
+ // denoise.c eband20ms: ERB-spaced band edges in 50 Hz bins (0 Hz to 20 kHz)
43
+ const EBAND = [0, 2, 4, 6, 8, 10, 12, 15, 18, 21, 24, 28, 32, 36, 41, 47, 53, 60, 68, 77, 87, 98, 110, 124, 140, 157, 176, 198, 223, 251, 282, 317, 356, 400]
44
+
45
+ // ------------------------------------------------ tables (dump_rnnoise_tables.c)
46
+
47
+ const DCT = new Float32Array(NB * NB)
48
+ for (let i = 0; i < NB; i++) for (let j = 0; j < NB; j++) {
49
+ DCT[i * NB + j] = Math.cos((i + .5) * j * Math.PI / NB)
50
+ if (j === 0) DCT[i * NB + j] = DCT[i * NB + j] * Math.sqrt(.5)
51
+ }
52
+ const DCT_NORM = Math.sqrt(2 / 22)
53
+
54
+ // ------------------------------------------------ weights (parse_lpcnet_weights.c)
55
+
56
+ const HEAD = 64, TYPES = { 0: Float32Array, 1: Int32Array, 3: Int8Array }
57
+
58
+ // upstream's weight blob: 64-byte records { 'DNNw', version, type, size, block_size, name[44] }
59
+ function parseBlob(bytes) {
60
+ let u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
61
+ let dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength), arrays = new Map(), o = 0
62
+ while (o < u8.length) {
63
+ if (u8.length - o < HEAD) throw new Error('rnnoise: truncated weight record')
64
+ let type = dv.getInt32(o + 8, true), size = dv.getInt32(o + 12, true), block = dv.getInt32(o + 16, true)
65
+ let name = '' // ASCII, NUL-terminated; no TextDecoder: AudioWorkletGlobalScope has none
66
+ for (let i = o + 20; i < o + 64 && u8[i]; i++) name += String.fromCharCode(u8[i])
67
+ if (block < size || block > u8.length - o - HEAD || size < 0 || u8[o + 63] !== 0) throw new Error(`rnnoise: bad weight record '${name}'`)
68
+ let T = TYPES[type]
69
+ if (!T) throw new Error(`rnnoise: unknown weight type ${type} for '${name}'`)
70
+ // a copy (ArrayBuffer.slice): the bytes may sit at any offset of a shared buffer (a Node Buffer's pool)
71
+ let data = new T(u8.buffer.slice(u8.byteOffset + o + HEAD, u8.byteOffset + o + HEAD + size))
72
+ arrays.set(name, data)
73
+ o += HEAD + block
74
+ }
75
+ return arrays
76
+ }
77
+
78
+ // linear_init: same presence and size checks; float weights win over int8 when both exist (compute_linear)
79
+ function linear(arrays, name, nIn, nOut, { int8 = false, sparse = false, diag = false } = {}) {
80
+ let need = (key, len) => {
81
+ let a = arrays.get(key)
82
+ if (!a || a.length !== len) throw new Error(`rnnoise: weight '${key}' missing or not ${len} long`)
83
+ return a
84
+ }
85
+ let opt = (key, len) => {
86
+ let a = arrays.get(key)
87
+ if (a && a.length !== len) throw new Error(`rnnoise: weight '${key}' is ${a.length} long, want ${len}`)
88
+ return a || null
89
+ }
90
+ let L = { nIn, nOut, bias: need(name + '_bias', nOut), idx: null, weights: null, floats: null, scale: null, diag: null }
91
+ let len = nIn * nOut
92
+ if (sparse) {
93
+ let idx = need(name + '_weights_idx', arrays.get(name + '_weights_idx')?.length ?? -1), blocks = 0, p = 0, out = nOut
94
+ while (p < idx.length) {
95
+ let n = idx[p++]
96
+ if (idx.length - p < n) throw new Error(`rnnoise: '${name}_weights_idx' overruns`)
97
+ for (let i = 0; i < n; i++) { let pos = idx[p++]; if (pos + 3 >= nIn || (pos & 3)) throw new Error(`rnnoise: '${name}_weights_idx' column ${pos} out of range`) }
98
+ blocks += n; out -= 8
99
+ }
100
+ if (out !== 0) throw new Error(`rnnoise: '${name}_weights_idx' covers the wrong number of rows`)
101
+ L.idx = idx; len = 32 * blocks
102
+ }
103
+ if (int8) L.weights = need(name + '_weights_int8', len)
104
+ L.floats = opt(name + '_weights_float', len)
105
+ if (!int8 && !L.floats) throw new Error(`rnnoise: weight '${name}_weights_float' missing`)
106
+ if (int8) L.scale = need(name + '_scale', nOut)
107
+ if (diag) L.diag = need(name + '_weights_diag', nOut)
108
+ opt(name + '_subias', nOut)
109
+ return L
110
+ }
111
+
112
+ // gemv.wat, compiled by wat2wasm (419 bytes): the int8 products of sparse_cgemv8x4 with WebAssembly SIMD
113
+ // (i16 widening, i32x4.dot_i16x8_s). The same integer sums as the JS loop, 8.5 times faster; the JS loop
114
+ // runs where WebAssembly SIMD does not (Safari before 16.4, Node before 16.4).
115
+ const GEMV = new Uint8Array([
116
+ 0,97,115,109,1,0,0,0,1,9,1,96,5,127,127,127,127,127,0,3,2,1,0,5,3,1,0,1,7,17,2,6,109,101,109,111,114,121,2,0,
117
+ 4,103,101,109,118,0,0,10,241,2,1,238,2,2,2,127,6,123,3,64,32,1,40,2,0,33,6,32,1,65,4,106,33,1,253,12,0,0,0,0,
118
+ 0,0,0,0,0,0,0,0,0,0,0,0,33,9,253,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,33,10,253,12,0,0,0,0,
119
+ 0,0,0,0,0,0,0,0,0,0,0,0,33,11,253,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,33,12,2,64,3,64,32,6,
120
+ 69,13,1,32,2,32,1,40,2,0,106,40,2,0,253,17,253,135,1,33,7,32,0,253,0,4,0,33,8,32,9,32,8,253,135,1,32,7,253,186,
121
+ 1,253,174,1,33,9,32,10,32,8,253,136,1,32,7,253,186,1,253,174,1,33,10,32,0,253,0,4,16,33,8,32,11,32,8,253,135,1,32,7,
122
+ 253,186,1,253,174,1,33,11,32,12,32,8,253,136,1,32,7,253,186,1,253,174,1,33,12,32,0,65,32,106,33,0,32,1,65,4,106,33,1,32,
123
+ 6,65,1,107,33,6,12,0,11,11,32,3,32,9,32,10,253,13,0,1,2,3,8,9,10,11,16,17,18,19,24,25,26,27,32,9,32,10,253,13,
124
+ 4,5,6,7,12,13,14,15,20,21,22,23,28,29,30,31,253,174,1,253,11,4,0,32,3,32,11,32,12,253,13,0,1,2,3,8,9,10,11,16,
125
+ 17,18,19,24,25,26,27,32,11,32,12,253,13,4,5,6,7,12,13,14,15,20,21,22,23,28,29,30,31,253,174,1,253,11,4,16,32,3,65,32,
126
+ 106,33,3,32,5,65,8,106,33,5,32,5,32,4,73,13,0,11,11,
127
+ ])
128
+
129
+ // the int8 layers' weights and block indices in one wasm memory: x (int8) at 0, sums (int32) at 512, then the layers
130
+ function attachSimd(net) {
131
+ let mod
132
+ try { mod = new WebAssembly.Module(GEMV) } catch { return }
133
+ let layers = [net.conv2, net.gru1.input, net.gru1.recurrent, net.gru2.input, net.gru2.recurrent, net.gru3.input, net.gru3.recurrent].filter(L => L.weights && !L.floats)
134
+ let idxs = layers.map(L => L.idx ?? denseIdx(L.nIn, L.nOut)), size = 5120
135
+ for (let i = 0; i < layers.length; i++) size += idxs[i].length * 4 + layers[i].weights.length + 16
136
+ let inst = new WebAssembly.Instance(mod), mem = inst.exports.memory
137
+ mem.grow(Math.ceil(size / 65536))
138
+ let k = { gemv: inst.exports.gemv, x: new Int8Array(mem.buffer, 0, 512), sums: new Int32Array(mem.buffer, 512, 1152) }, o = 5120
139
+ layers.forEach((L, i) => {
140
+ new Int32Array(mem.buffer, o, idxs[i].length).set(idxs[i])
141
+ let idx = o; o = (o + idxs[i].length * 4 + 15) & ~15
142
+ new Int8Array(mem.buffer, o, L.weights.length).set(L.weights)
143
+ L.simd = { k, idx, w: o }; o += L.weights.length
144
+ })
145
+ }
146
+
147
+ // the block index of a dense 8×4-blocked matrix: every row block uses every column block
148
+ function denseIdx(nIn, nOut) {
149
+ let idx = []
150
+ for (let i = 0; i < nOut; i += 8) { idx.push(nIn >> 2); for (let j = 0; j < nIn; j += 4) idx.push(j) }
151
+ return Int32Array.from(idx)
152
+ }
153
+
154
+ // init_rnnoise (rnnoise_data.c): conv 65·3 → 128, conv 128·3 → 384, three GRUs of 384, dense 1536 → 32 + 1
155
+ export function model(bytes, { simd = true } = {}) {
156
+ let a = parseBlob(bytes)
157
+ let gru = k => ({
158
+ input: linear(a, `gru${k}_input`, 384, 1152, { int8: true, sparse: true }),
159
+ recurrent: linear(a, `gru${k}_recurrent`, 384, 1152, { int8: true, sparse: true, diag: true }),
160
+ })
161
+ let net = {
162
+ conv1: linear(a, 'conv1', 195, 128),
163
+ conv2: linear(a, 'conv2', 384, 384, { int8: true }),
164
+ gru1: gru(1), gru2: gru(2), gru3: gru(3),
165
+ dense: linear(a, 'dense_out', 1536, 32),
166
+ vad: linear(a, 'vad_dense', 1536, 1),
167
+ }
168
+ if (simd && typeof WebAssembly === 'object') attachSimd(net)
169
+ return net
170
+ }
171
+
172
+ // ------------------------------------------------ network (nnet.c, nnet_arch.h, vec.h)
173
+
174
+ const XQ = new Int8Array(1536)
175
+
176
+ // compute_linear_c: out = W·in + bias (+ diag ⊙ in, GRU recurrent only)
177
+ function computeLinear(L, out, x) {
178
+ let N = L.nOut, M = L.nIn
179
+ if (L.floats) {
180
+ if (L.idx) sparseSgemv(out, L.floats, L.idx, N, x)
181
+ else sgemv(out, L.floats, N, M, x)
182
+ } else if (L.simd) {
183
+ let { k, idx, w } = L.simd, s = L.scale
184
+ for (let i = 0; i < M; i++) k.x[i] = Math.floor(.5 + f(127 * x[i]))
185
+ k.gemv(w, idx, 0, 512, N)
186
+ for (let i = 0; i < N; i++) out[i] = f(k.sums[i] * s[i])
187
+ } else {
188
+ // cgemv8x4 / sparse_cgemv8x4: activations quantized to int8, floor(.5 + 127x); the dot
189
+ // products are integers under 2^24, exact in the C's float accumulator as in doubles here
190
+ for (let i = 0; i < M; i++) XQ[i] = Math.floor(.5 + f(127 * x[i]))
191
+ let w = L.weights, s = L.scale, idx = L.idx, p = 0, q = 0
192
+ for (let i = 0; i < N; i += 8) {
193
+ let y0 = 0, y1 = 0, y2 = 0, y3 = 0, y4 = 0, y5 = 0, y6 = 0, y7 = 0
194
+ let n = idx ? idx[q++] : M >> 2
195
+ for (let j = 0; j < n; j++, p += 32) {
196
+ let pos = idx ? idx[q++] : j << 2
197
+ let x0 = XQ[pos], x1 = XQ[pos + 1], x2 = XQ[pos + 2], x3 = XQ[pos + 3]
198
+ y0 += w[p] * x0 + w[p + 1] * x1 + w[p + 2] * x2 + w[p + 3] * x3
199
+ y1 += w[p + 4] * x0 + w[p + 5] * x1 + w[p + 6] * x2 + w[p + 7] * x3
200
+ y2 += w[p + 8] * x0 + w[p + 9] * x1 + w[p + 10] * x2 + w[p + 11] * x3
201
+ y3 += w[p + 12] * x0 + w[p + 13] * x1 + w[p + 14] * x2 + w[p + 15] * x3
202
+ y4 += w[p + 16] * x0 + w[p + 17] * x1 + w[p + 18] * x2 + w[p + 19] * x3
203
+ y5 += w[p + 20] * x0 + w[p + 21] * x1 + w[p + 22] * x2 + w[p + 23] * x3
204
+ y6 += w[p + 24] * x0 + w[p + 25] * x1 + w[p + 26] * x2 + w[p + 27] * x3
205
+ y7 += w[p + 28] * x0 + w[p + 29] * x1 + w[p + 30] * x2 + w[p + 31] * x3
206
+ }
207
+ out[i] = f(y0 * s[i]); out[i + 1] = f(y1 * s[i + 1]); out[i + 2] = f(y2 * s[i + 2]); out[i + 3] = f(y3 * s[i + 3])
208
+ out[i + 4] = f(y4 * s[i + 4]); out[i + 5] = f(y5 * s[i + 5]); out[i + 6] = f(y6 * s[i + 6]); out[i + 7] = f(y7 * s[i + 7])
209
+ }
210
+ }
211
+ let b = L.bias
212
+ for (let i = 0; i < N; i++) out[i] = f(out[i] + b[i])
213
+ if (L.diag) {
214
+ let d = L.diag
215
+ for (let i = 0; i < M; i++) {
216
+ out[i] = f(out[i] + f(d[i] * x[i]))
217
+ out[i + M] = f(out[i + M] + f(d[i + M] * x[i]))
218
+ out[i + 2 * M] = f(out[i + 2 * M] + f(d[i + 2 * M] * x[i]))
219
+ }
220
+ }
221
+ }
222
+
223
+ // sgemv (sgemv16x1 / sgemv8x1 / plain loop): column-major, accumulated in input order
224
+ function sgemv(out, w, N, M, x) {
225
+ out.fill(0, 0, N)
226
+ if (N % 8 === 0) {
227
+ let B = N % 16 === 0 ? 16 : 8
228
+ for (let i = 0; i < N; i += B)
229
+ for (let j = 0; j < M; j++) {
230
+ let xj = x[j], o = j * N + i
231
+ for (let k = 0; k < B; k++) out[i + k] = f(out[i + k] + f(w[o + k] * xj))
232
+ }
233
+ }
234
+ else for (let i = 0; i < N; i++) {
235
+ let y = 0
236
+ for (let j = 0; j < M; j++) y = f(y + f(w[j * N + i] * x[j]))
237
+ out[i] = y
238
+ }
239
+ }
240
+
241
+ // sparse_sgemv8x4: only reached with a blob carrying float copies of the sparse int8 matrices
242
+ function sparseSgemv(out, w, idx, N, x) {
243
+ out.fill(0, 0, N)
244
+ let p = 0, q = 0
245
+ for (let i = 0; i < N; i += 8) {
246
+ let n = idx[q++]
247
+ for (let j = 0; j < n; j++) {
248
+ let pos = idx[q++]
249
+ for (let c = 0; c < 4; c++, p += 8) {
250
+ let xj = x[pos + c]
251
+ for (let k = 0; k < 8; k++) out[i + k] = f(out[i + k] + f(w[p + k] * xj))
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ // vec.h tanh_approx: (N0 + N1x² + N2x⁴)x / (D0 + D1x² + D2x⁴), clamped to ±1
258
+ function tanh(x) {
259
+ let x2 = f(x * x)
260
+ let num = f(f(f(f(F.N2 * x2) + F.N1) * x2) + F.N0)
261
+ let den = f(f(f(f(F.D2 * x2) + F.D1) * x2) + F.D0)
262
+ num = f(f(num * x) / den)
263
+ return num > 1 ? 1 : num < -1 ? -1 : num
264
+ }
265
+ const sigmoid = x => f(.5 + f(.5 * tanh(f(.5 * x))))
266
+
267
+ function convLayer(L, out, mem, x, nIn, act, tmp) {
268
+ let keep = L.nIn - nIn
269
+ tmp.set(mem.subarray(0, keep)); tmp.set(x.subarray(0, nIn), keep)
270
+ computeLinear(L, out, tmp)
271
+ for (let i = 0; i < L.nOut; i++) out[i] = act(out[i])
272
+ mem.set(tmp.subarray(nIn, nIn + keep))
273
+ }
274
+
275
+ function gruLayer(G, state, x, zrh, recur) {
276
+ let N = 384
277
+ computeLinear(G.input, zrh, x)
278
+ computeLinear(G.recurrent, recur, state)
279
+ for (let i = 0; i < 2 * N; i++) zrh[i] = sigmoid(f(zrh[i] + recur[i]))
280
+ for (let i = 0; i < N; i++) {
281
+ let h = tanh(f(zrh[2 * N + i] + f(recur[2 * N + i] * zrh[N + i]))), z = zrh[i]
282
+ state[i] = f(f(z * state[i]) + f(f(1 - z) * h))
283
+ }
284
+ }
285
+
286
+ // ------------------------------------------------ DSP (denoise.c, pitch.c, celt_lpc.c)
287
+
288
+ const SUM = new Float32Array(NB + 2)
289
+ function bandEnergy(E, Xr, Xi, Pr, Pi) {
290
+ let sum = SUM.fill(0)
291
+ for (let i = 0; i < NB + 1; i++) {
292
+ let n = EBAND[i + 1] - EBAND[i]
293
+ for (let j = 0; j < n; j++) {
294
+ let fr = f(j / n), k = EBAND[i] + j
295
+ let t = Pr ? f(f(Xr[k] * Pr[k]) + f(Xi[k] * Pi[k])) : f(f(Xr[k] * Xr[k]) + f(Xi[k] * Xi[k]))
296
+ sum[i] = f(sum[i] + f(f(1 - fr) * t))
297
+ sum[i + 1] = f(sum[i + 1] + f(fr * t))
298
+ }
299
+ }
300
+ sum[1] = f(f(f(sum[0] + sum[1]) * 2) / 3)
301
+ sum[NB] = f(f(f(sum[NB] + sum[NB + 1]) * 2) / 3)
302
+ for (let i = 0; i < NB; i++) E[i] = sum[i + 1]
303
+ }
304
+
305
+ // interp_band_gain; bins 400–480 (above 20 kHz) keep what g held: zeros in every caller
306
+ function interpGain(g, b) {
307
+ for (let i = 1; i < NB; i++) {
308
+ let n = EBAND[i + 1] - EBAND[i]
309
+ for (let j = 0; j < n; j++) { let fr = f(j / n); g[EBAND[i] + j] = f(f(f(1 - fr) * b[i - 1]) + f(fr * b[i])) }
310
+ }
311
+ for (let j = 0; j < EBAND[1]; j++) g[j] = b[0]
312
+ for (let j = EBAND[NB]; j < EBAND[NB + 1]; j++) g[j] = b[NB - 1]
313
+ }
314
+
315
+ function dct(out, o, x) {
316
+ for (let i = 0; i < NB; i++) {
317
+ let s = 0
318
+ for (let j = 0; j < NB; j++) s = f(s + f(x[j] * DCT[j * NB + i]))
319
+ out[o + i] = s * DCT_NORM
320
+ }
321
+ }
322
+
323
+ function innerProd(x, xo, y, yo, n) {
324
+ let s = 0
325
+ for (let i = 0; i < n; i++) s = f(s + f(x[xo + i] * y[yo + i]))
326
+ return s
327
+ }
328
+
329
+ // xcorr_kernel: four lags at once, the C's rotation of y_0…y_3 kept for the same rounding order
330
+ function xcorrKernel(x, xo, y, yo, s, len) {
331
+ let y0 = y[yo++], y1 = y[yo++], y2 = y[yo++], y3 = 0, s0 = s[0], s1 = s[1], s2 = s[2], s3 = s[3], j = 0, t
332
+ for (; j < len - 3; j += 4) {
333
+ t = x[xo++]; y3 = y[yo++]
334
+ s0 = f(s0 + f(t * y0)); s1 = f(s1 + f(t * y1)); s2 = f(s2 + f(t * y2)); s3 = f(s3 + f(t * y3))
335
+ t = x[xo++]; y0 = y[yo++]
336
+ s0 = f(s0 + f(t * y1)); s1 = f(s1 + f(t * y2)); s2 = f(s2 + f(t * y3)); s3 = f(s3 + f(t * y0))
337
+ t = x[xo++]; y1 = y[yo++]
338
+ s0 = f(s0 + f(t * y2)); s1 = f(s1 + f(t * y3)); s2 = f(s2 + f(t * y0)); s3 = f(s3 + f(t * y1))
339
+ t = x[xo++]; y2 = y[yo++]
340
+ s0 = f(s0 + f(t * y3)); s1 = f(s1 + f(t * y0)); s2 = f(s2 + f(t * y1)); s3 = f(s3 + f(t * y2))
341
+ }
342
+ if (j++ < len) { t = x[xo++]; y3 = y[yo++]; s0 = f(s0 + f(t * y0)); s1 = f(s1 + f(t * y1)); s2 = f(s2 + f(t * y2)); s3 = f(s3 + f(t * y3)) }
343
+ if (j++ < len) { t = x[xo++]; y0 = y[yo++]; s0 = f(s0 + f(t * y1)); s1 = f(s1 + f(t * y2)); s2 = f(s2 + f(t * y3)); s3 = f(s3 + f(t * y0)) }
344
+ if (j < len) { t = x[xo++]; y1 = y[yo++]; s0 = f(s0 + f(t * y2)); s1 = f(s1 + f(t * y3)); s2 = f(s2 + f(t * y0)); s3 = f(s3 + f(t * y1)) }
345
+ s[0] = s0; s[1] = s1; s[2] = s2; s[3] = s3
346
+ }
347
+
348
+ const SUM4 = new Float32Array(4)
349
+ function pitchXcorr(x, xo, y, yo, xcorr, len, maxPitch) {
350
+ let i = 0
351
+ for (; i < maxPitch - 3; i += 4) {
352
+ SUM4.fill(0)
353
+ xcorrKernel(x, xo, y, yo + i, SUM4, len)
354
+ xcorr[i] = SUM4[0]; xcorr[i + 1] = SUM4[1]; xcorr[i + 2] = SUM4[2]; xcorr[i + 3] = SUM4[3]
355
+ }
356
+ for (; i < maxPitch; i++) xcorr[i] = innerProd(x, xo, y, yo + i, len)
357
+ }
358
+
359
+ // rnn_lpc: Levinson–Durbin, order 4 here, stopping at 30 dB prediction gain
360
+ function lpc(out, ac, p) {
361
+ let err = ac[0]
362
+ out.fill(0, 0, p)
363
+ if (ac[0] === 0) return
364
+ for (let i = 0; i < p; i++) {
365
+ let rr = 0
366
+ for (let j = 0; j < i; j++) rr = f(rr + f(out[j] * ac[i - j]))
367
+ rr = f(rr + ac[i + 1])
368
+ let r = f(-rr / err)
369
+ out[i] = r
370
+ for (let j = 0; j < (i + 1) >> 1; j++) {
371
+ let t1 = out[j], t2 = out[i - 1 - j]
372
+ out[j] = f(t1 + f(r * t2)); out[i - 1 - j] = f(t2 + f(r * t1))
373
+ }
374
+ err = f(err - f(f(r * r) * err))
375
+ if (err < f(F.c0001 * ac[0])) break
376
+ }
377
+ }
378
+
379
+ const AC = new Float32Array(5), LPC = new Float32Array(4)
380
+
381
+ // rnn_pitch_downsample (one channel): 2:1 by [.25 .5 .25], then a 4th-order LPC whitening with a zero
382
+ function pitchDownsample(x, xlp, len) {
383
+ let n = len >> 1
384
+ for (let i = 1; i < n; i++) xlp[i] = f(.5 * f(f(.5 * f(x[2 * i - 1] + x[2 * i + 1])) + x[2 * i]))
385
+ xlp[0] = f(.5 * f(f(.5 * x[1]) + x[0]))
386
+ let ac = AC
387
+ pitchXcorr(xlp, 0, xlp, 0, ac, n - 4, 5)
388
+ for (let k = 0; k <= 4; k++) {
389
+ let d = 0
390
+ for (let i = k + n - 4; i < n; i++) d = f(d + f(xlp[i] * xlp[i - k]))
391
+ ac[k] = f(ac[k] + d)
392
+ }
393
+ ac[0] = f(ac[0] * F.c10001)
394
+ for (let i = 1; i <= 4; i++) ac[i] = f(ac[i] - f(f(ac[i] * f(F.c008 * i)) * f(F.c008 * i)))
395
+ let a = LPC, tmp = 1
396
+ lpc(a, ac, 4)
397
+ for (let i = 0; i < 4; i++) { tmp = f(F.c09 * tmp); a[i] = f(a[i] * tmp) }
398
+ let c1 = F.c08
399
+ let n0 = f(a[0] + c1), n1 = f(a[1] + f(c1 * a[0])), n2 = f(a[2] + f(c1 * a[1])), n3 = f(a[3] + f(c1 * a[2])), n4 = f(c1 * a[3])
400
+ let m0 = 0, m1 = 0, m2 = 0, m3 = 0, m4 = 0
401
+ for (let i = 0; i < n; i++) {
402
+ let xi = xlp[i]
403
+ let s = f(xi + f(n0 * m0)); s = f(s + f(n1 * m1)); s = f(s + f(n2 * m2)); s = f(s + f(n3 * m3)); s = f(s + f(n4 * m4))
404
+ m4 = m3; m3 = m2; m2 = m1; m1 = m0; m0 = xi
405
+ xlp[i] = s
406
+ }
407
+ }
408
+
409
+ function findBestPitch(xcorr, y, yo, len, maxPitch, best) {
410
+ let Syy = 1, bn0 = -1, bn1 = -1, bd0 = 0, bd1 = 0
411
+ best[0] = 0; best[1] = 1
412
+ for (let j = 0; j < len; j++) Syy = f(Syy + f(y[yo + j] * y[yo + j]))
413
+ for (let i = 0; i < maxPitch; i++) {
414
+ if (xcorr[i] > 0) {
415
+ let c = f(xcorr[i] * F.c1e12), num = f(c * c)
416
+ if (f(num * bd1) > f(bn1 * Syy)) {
417
+ if (f(num * bd0) > f(bn0 * Syy)) { bn1 = bn0; bd1 = bd0; best[1] = best[0]; bn0 = num; bd0 = Syy; best[0] = i }
418
+ else { bn1 = num; bd1 = Syy; best[1] = i }
419
+ }
420
+ }
421
+ Syy = f(Syy + f(f(y[yo + i + len] * y[yo + i + len]) - f(y[yo + i] * y[yo + i])))
422
+ Syy = 1 > Syy ? 1 : Syy
423
+ }
424
+ }
425
+
426
+ // rnn_pitch_search: coarse at 4:1, refined at 2:1 around the two best, pseudo-interpolated
427
+ const X4 = new Float32Array(PFRAME >> 2), Y4 = new Float32Array((PFRAME + PMAX) >> 2), XCORR = new Float32Array(PMAX >> 1), BEST = new Int32Array(2)
428
+ function pitchSearch(xlp, xo, y, len, maxPitch) {
429
+ let lag = len + maxPitch, x4 = X4, y4 = Y4, xcorr = XCORR.fill(0), best = BEST
430
+ for (let j = 0; j < len >> 2; j++) x4[j] = xlp[xo + 2 * j]
431
+ for (let j = 0; j < lag >> 2; j++) y4[j] = y[2 * j]
432
+ pitchXcorr(x4, 0, y4, 0, xcorr, len >> 2, maxPitch >> 2)
433
+ findBestPitch(xcorr, y4, 0, len >> 2, maxPitch >> 2, best)
434
+ for (let i = 0; i < maxPitch >> 1; i++) {
435
+ xcorr[i] = 0
436
+ if (Math.abs(i - 2 * best[0]) > 2 && Math.abs(i - 2 * best[1]) > 2) continue
437
+ let s = innerProd(xlp, xo, y, i, len >> 1)
438
+ xcorr[i] = -1 > s ? -1 : s
439
+ }
440
+ findBestPitch(xcorr, y, 0, len >> 1, maxPitch >> 1, best)
441
+ let offset = 0
442
+ if (best[0] > 0 && best[0] < (maxPitch >> 1) - 1) {
443
+ let a = xcorr[best[0] - 1], b = xcorr[best[0]], c = xcorr[best[0] + 1]
444
+ if (f(c - a) > f(F.c07 * f(b - a))) offset = 1
445
+ else if (f(a - c) > f(F.c07 * f(b - c))) offset = -1
446
+ }
447
+ return 2 * best[0] - offset
448
+ }
449
+
450
+ const SECOND_CHECK = [0, 0, 3, 2, 3, 2, 5, 2, 3, 2, 3, 2, 5, 2, 3, 2]
451
+ const pitchGain = (xy, xx, yy) => f(xy / Math.sqrt(f(1 + f(xx * yy))))
452
+
453
+ // rnn_remove_doubling: prefer T/k when its correlation holds up; the gain goes to PG[0]
454
+ const YYL = new Float32Array((PMAX >> 1) + 1), PG = new Float32Array(1)
455
+ function removeDoubling(x, T0, prevPeriod, prevGain) {
456
+ let maxp = PMAX >> 1, minp = PMIN >> 1, N = PFRAME >> 1, o = maxp
457
+ T0 = (T0 / 2) | 0; prevPeriod = (prevPeriod / 2) | 0
458
+ if (T0 >= maxp) T0 = maxp - 1
459
+ let T = T0, xx = 0, xy = 0
460
+ for (let i = 0; i < N; i++) { xx = f(xx + f(x[o + i] * x[o + i])); xy = f(xy + f(x[o + i] * x[o + i - T0])) }
461
+ let yyl = YYL, yy = xx
462
+ yyl[0] = xx
463
+ for (let i = 1; i <= maxp; i++) {
464
+ yy = f(f(yy + f(x[o - i] * x[o - i])) - f(x[o + N - i] * x[o + N - i]))
465
+ yyl[i] = 0 > yy ? 0 : yy
466
+ }
467
+ yy = yyl[T0]
468
+ let bestXy = xy, bestYy = yy, g0 = pitchGain(xy, xx, yy), g = g0
469
+ for (let k = 2; k <= 15; k++) {
470
+ let T1 = ((2 * T0 + k) / (2 * k)) | 0
471
+ if (T1 < minp) break
472
+ let T1b = k === 2 ? (T1 + T0 > maxp ? T0 : T0 + T1) : ((2 * SECOND_CHECK[k] * T0 + k) / (2 * k)) | 0
473
+ let a = 0, b = 0
474
+ for (let i = 0; i < N; i++) { a = f(a + f(x[o + i] * x[o + i - T1])); b = f(b + f(x[o + i] * x[o + i - T1b])) }
475
+ let xy1 = f(.5 * f(a + b)), yy1 = f(.5 * f(yyl[T1] + yyl[T1b])), g1 = pitchGain(xy1, xx, yy1)
476
+ let cont = Math.abs(T1 - prevPeriod) <= 1 ? prevGain : Math.abs(T1 - prevPeriod) <= 2 && 5 * k * k < T0 ? f(.5 * prevGain) : 0
477
+ let th = f(f(F.c07 * g0) - cont); th = F.c03 > th ? F.c03 : th
478
+ if (T1 < 3 * minp) { th = f(f(F.c085 * g0) - cont); th = F.c04 > th ? F.c04 : th }
479
+ else if (T1 < 2 * minp) { th = f(f(F.c09 * g0) - cont); th = F.c05 > th ? F.c05 : th }
480
+ if (g1 > th) { bestXy = xy1; bestYy = yy1; T = T1; g = g1 }
481
+ }
482
+ bestXy = 0 > bestXy ? 0 : bestXy
483
+ let pg = bestYy <= bestXy ? 1 : f(bestXy / f(bestYy + 1))
484
+ let c0 = innerProd(x, o, x, o - (T - 1), N), c1 = innerProd(x, o, x, o - T, N), c2 = innerProd(x, o, x, o - (T + 1), N)
485
+ let offset = f(c2 - c0) > f(F.c07 * f(c1 - c0)) ? 1 : f(c0 - c2) > f(F.c07 * f(c1 - c2)) ? -1 : 0
486
+ if (pg > g) pg = g
487
+ T0 = 2 * T + offset
488
+ PG[0] = pg
489
+ return T0 < PMIN ? PMIN : T0
490
+ }
491
+
492
+ // ------------------------------------------------ denoiser state (DenoiseState)
493
+
494
+ // create(weights) → { process(input480, output480) → VAD probability }: one 10 ms frame at 48 kHz,
495
+ // samples at int16 scale (±32768), as rnnoise_process_frame. Output lags input by 960 samples.
496
+ export function create(net) {
497
+ if (!net || !net.gru1) net = model(net)
498
+ let analysisMem = new Float32Array(FRAME), synthesisMem = new Float32Array(FRAME)
499
+ let pitchBuf = new Float32Array(PBUF), memHp = new Float64Array(2), lastg = new Float32Array(NB)
500
+ let lastPeriod = 0, lastGain = 0
501
+ let dXr = new Float32Array(FREQ), dXi = new Float32Array(FREQ), dPr = new Float32Array(FREQ), dPi = new Float32Array(FREQ)
502
+ let dEx = new Float32Array(NB), dEp = new Float32Array(NB), dExp = new Float32Array(NB)
503
+ // RNNState
504
+ let conv1 = new Float32Array(130), conv2 = new Float32Array(256)
505
+ let h1 = new Float32Array(384), h2 = new Float32Array(384), h3 = new Float32Array(384)
506
+ // scratch
507
+ let x = new Float32Array(FRAME), buf = new Float32Array(WIN)
508
+ let Xr = new Float32Array(FREQ), Xi = new Float32Array(FREQ), Pr = new Float32Array(FREQ), Pi = new Float32Array(FREQ)
509
+ let Ex = new Float32Array(NB), Ep = new Float32Array(NB), Exp = new Float32Array(NB), Ly = new Float32Array(NB)
510
+ let feat = new Float32Array(NF), g = new Float32Array(NB), gf = new Float32Array(FREQ)
511
+ let xlp = new Float32Array(PBUF >> 1), vad = new Float32Array(1)
512
+ let t1 = new Float32Array(128), tmp = new Float32Array(1536), cat = new Float32Array(1536)
513
+ let zrh = new Float32Array(1152), recur = new Float32Array(1152)
514
+
515
+ // apply_window: the 960-point Vorbis window, symmetric
516
+ function window(v) {
517
+ for (let i = 0; i < WIN; i++) v[i] = f(v[i] * WINDOW[i])
518
+ }
519
+
520
+ // rnn_compute_frame_features → true when the frame is silent (E < 0.04)
521
+ function features(x) {
522
+ buf.set(analysisMem); buf.set(x, FRAME); analysisMem.set(x)
523
+ window(buf); rfft(buf, Xr, Xi); bandEnergy(Ex, Xr, Xi)
524
+ pitchBuf.copyWithin(0, FRAME); pitchBuf.set(x, PBUF - FRAME)
525
+ pitchDownsample(pitchBuf, xlp, PBUF)
526
+ let idx = removeDoubling(xlp, PMAX - pitchSearch(xlp, PMAX >> 1, xlp, PFRAME, PMAX - 3 * PMIN), lastPeriod, lastGain)
527
+ lastPeriod = idx; lastGain = PG[0]
528
+ for (let i = 0; i < WIN; i++) buf[i] = pitchBuf[PBUF - WIN - idx + i]
529
+ window(buf); rfft(buf, Pr, Pi)
530
+ bandEnergy(Ep, Pr, Pi); bandEnergy(Exp, Xr, Xi, Pr, Pi)
531
+ for (let i = 0; i < NB; i++) Exp[i] = Exp[i] / Math.sqrt(.001 + f(Ex[i] * Ep[i]))
532
+ dct(feat, NB, Exp)
533
+ feat[2 * NB] = .01 * (idx - 300)
534
+ let logMax = -2, follow = -2, E = 0
535
+ for (let i = 0; i < NB; i++) {
536
+ let ly = f(Math.log10(1e-2 + Ex[i])), a = follow - 1.5, b = f(logMax - 7)
537
+ ly = a > ly ? a : ly
538
+ Ly[i] = ly = f(b > ly ? b : ly)
539
+ logMax = logMax > ly ? logMax : ly
540
+ follow = f(a > ly ? a : ly)
541
+ E = f(E + Ex[i])
542
+ }
543
+ if (E < .04) { feat.fill(0); return true }
544
+ dct(feat, 0, Ly)
545
+ feat[0] = f(feat[0] - 12); feat[1] = f(feat[1] - 4)
546
+ return false
547
+ }
548
+
549
+ function network() {
550
+ convLayer(net.conv1, t1, conv1, feat, NF, tanh, tmp)
551
+ convLayer(net.conv2, cat, conv2, t1, 128, tanh, tmp)
552
+ gruLayer(net.gru1, h1, cat, zrh, recur)
553
+ gruLayer(net.gru2, h2, h1, zrh, recur)
554
+ gruLayer(net.gru3, h3, h2, zrh, recur)
555
+ cat.set(h1, 384); cat.set(h2, 768); cat.set(h3, 1152)
556
+ computeLinear(net.dense, g, cat)
557
+ for (let i = 0; i < NB; i++) g[i] = sigmoid(g[i])
558
+ computeLinear(net.vad, vad, cat)
559
+ return sigmoid(vad[0])
560
+ }
561
+
562
+ // rnn_pitch_filter on the delayed frame: add back the pitch-periodic part the gains would remove
563
+ let r = new Float32Array(NB), rf = new Float32Array(FREQ), newE = new Float32Array(NB), norm = new Float32Array(NB), nf = new Float32Array(FREQ)
564
+ function pitchFilter() {
565
+ rf.fill(0); nf.fill(0)
566
+ for (let i = 0; i < NB; i++) {
567
+ let e = dExp[i], gi = g[i], v
568
+ if (e > gi) v = 1
569
+ else v = f(f(e * e) * f(1 - f(gi * gi))) / (.001 + f(f(gi * gi) * f(1 - f(e * e))))
570
+ v = f(v)
571
+ v = 0 > v ? 0 : v; v = 1 < v ? 1 : v
572
+ r[i] = f(Math.sqrt(v)) * Math.sqrt(dEx[i] / (1e-8 + dEp[i]))
573
+ }
574
+ interpGain(rf, r)
575
+ for (let i = 0; i < FREQ; i++) { dXr[i] = f(dXr[i] + f(rf[i] * dPr[i])); dXi[i] = f(dXi[i] + f(rf[i] * dPi[i])) }
576
+ bandEnergy(newE, dXr, dXi)
577
+ for (let i = 0; i < NB; i++) norm[i] = Math.sqrt(dEx[i] / (1e-8 + newE[i]))
578
+ interpGain(nf, norm)
579
+ for (let i = 0; i < FREQ; i++) { dXr[i] = f(dXr[i] * nf[i]); dXi[i] = f(dXi[i] * nf[i]) }
580
+ }
581
+
582
+ // rnnoise_process_frame
583
+ function process(input, output) {
584
+ // rnn_biquad: DC-blocking high-pass, state updated in double, stored as float
585
+ let m0 = memHp[0], m1 = memHp[1]
586
+ for (let i = 0; i < FRAME; i++) {
587
+ let xi = f(input[i]), yi = f(xi + m0)
588
+ m0 = f(m1 + (-2 * xi - F.hpA0 * yi))
589
+ m1 = f(1 * xi - F.hpA1 * yi)
590
+ x[i] = yi
591
+ }
592
+ memHp[0] = m0; memHp[1] = m1
593
+ let p = 0
594
+ if (!features(x)) {
595
+ p = network()
596
+ pitchFilter()
597
+ for (let i = 0; i < NB; i++) {
598
+ let gi = g[i] > f(F.c06 * lastg[i]) ? g[i] : f(F.c06 * lastg[i])
599
+ g[i] = gi
600
+ let v = gi * (dEx[i] + 1e-3) / (Ex[i] + 1e-3)
601
+ lastg[i] = 1 < v ? 1 : v
602
+ }
603
+ gf.fill(0); interpGain(gf, g)
604
+ for (let i = 0; i < FREQ; i++) { dXr[i] = f(dXr[i] * gf[i]); dXi[i] = f(dXi[i] * gf[i]) }
605
+ }
606
+ irfft(dXr, dXi, buf)
607
+ window(buf)
608
+ for (let i = 0; i < FRAME; i++) output[i] = f(buf[i] + synthesisMem[i])
609
+ synthesisMem.set(buf.subarray(FRAME))
610
+ dXr.set(Xr); dXi.set(Xi); dPr.set(Pr); dPi.set(Pi); dEx.set(Ex); dEp.set(Ep); dExp.set(Exp)
611
+ return p
612
+ }
613
+
614
+ return { process, latency: 2 * FRAME }
615
+ }
package/worklet.js ADDED
@@ -0,0 +1,63 @@
1
+ // AudioWorkletProcessor 'neural-denoise': RNNoise on each channel of a 48 kHz AudioContext.
2
+ //
3
+ // await ctx.audioWorklet.addModule(new URL('@audio/neural-denoise/worklet', import.meta.url))
4
+ // let node = new AudioWorkletNode(ctx, 'neural-denoise', { processorOptions: { weights: await weights() } })
5
+ //
6
+ // Render quanta (128 samples) are collected into 10 ms frames (480); the output queue starts with
7
+ // 480 − gcd(quantum, 480) samples of silence, the least that never runs dry, so the node's latency is
8
+ // constant: that plus RNNoise's 960 samples (1408 samples, 29.3 ms, at the 128-sample quantum).
9
+ // processorOptions.limit (dB) caps the attenuation by mixing the input back in: 20 unless given, 0 for
10
+ // none (upstream's output).
11
+
12
+ import { model, create, FRAME, LIMIT } from './rnnoise.js'
13
+
14
+ const gcd = (a, b) => b ? gcd(b, a % b) : a
15
+
16
+ class NeuralDenoise extends AudioWorkletProcessor {
17
+ constructor({ processorOptions: { weights, limit } = {} } = {}) {
18
+ super()
19
+ if (sampleRate !== 48000) throw new Error(`neural-denoise: RNNoise runs at 48 kHz, this context runs at ${sampleRate} Hz; create it with { sampleRate: 48000 }`)
20
+ if (!weights) throw new Error('neural-denoise: pass processorOptions.weights (await weights())')
21
+ this.net = model(weights)
22
+ limit ??= LIMIT
23
+ this.lim = limit ? 10 ** (-Math.abs(limit) / 20) : 0
24
+ this.chans = []
25
+ }
26
+
27
+ channel(c, quantum) {
28
+ let prime = FRAME - gcd(quantum, FRAME), size = FRAME + prime + quantum
29
+ return this.chans[c] ??= {
30
+ st: create(this.net), frame: new Float32Array(FRAME), out: new Float32Array(FRAME), fill: 0,
31
+ // output ring: queued samples between `read` and `read + queued`
32
+ ring: new Float32Array(size), read: 0, queued: prime,
33
+ // dry ring for the attenuation limit, delayed by the node's latency
34
+ dry: new Float32Array(2 * FRAME + prime + quantum), dw: 2 * FRAME + prime, dr: 0,
35
+ }
36
+ }
37
+
38
+ process(inputs, outputs) {
39
+ let input = inputs[0], output = outputs[0]
40
+ for (let c = 0; c < output.length; c++) {
41
+ let x = input[c] ?? input[0], y = output[c], n = y.length, s = this.channel(c, n)
42
+ for (let i = 0; i < n; i++) {
43
+ let v = x ? x[i] : 0
44
+ s.frame[s.fill++] = v * 32768
45
+ if (this.lim) { s.dry[s.dw] = v; s.dw = (s.dw + 1) % s.dry.length }
46
+ if (s.fill === FRAME) {
47
+ s.st.process(s.frame, s.out)
48
+ for (let k = 0, w = (s.read + s.queued) % s.ring.length; k < FRAME; k++, w = (w + 1) % s.ring.length) s.ring[w] = s.out[k] / 32768
49
+ s.queued += FRAME; s.fill = 0
50
+ }
51
+ }
52
+ for (let i = 0; i < n; i++) {
53
+ let v = s.queued > 0 ? s.ring[s.read] : 0
54
+ if (s.queued > 0) { s.read = (s.read + 1) % s.ring.length; s.queued-- }
55
+ if (this.lim) { v = v * (1 - this.lim) + s.dry[s.dr] * this.lim; s.dr = (s.dr + 1) % s.dry.length }
56
+ y[i] = v
57
+ }
58
+ }
59
+ return true
60
+ }
61
+ }
62
+
63
+ registerProcessor('neural-denoise', NeuralDenoise)