@audio/neural-denoise 0.6.0 → 0.7.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/README.md +29 -0
- package/deepfilter.js +1 -1
- package/denoise.js +24 -1
- package/guard.js +22 -11
- package/index.d.ts +6 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -121,6 +121,35 @@ Both models enhance speech, and to them everything else is noise: at their defau
|
|
|
121
121
|
|
|
122
122
|
It runs in JS: a stream's `process()` can't await ONNX Runtime, and RNNoise needs no runtime. The three upper convolutions (through im2col) and the dense layers run in a 1,074-byte WebAssembly SIMD kernel (`gemm.wat`), the first convolution once per frame and shared by the overlapping patches (the max pooling after it commutes with each patch's standardization); 1.1 s of CPU per minute of audio, 11 s on the JS path where WebAssembly SIMD is missing (same bits).
|
|
123
123
|
|
|
124
|
+
`segments(x, sampleRate)` gives the classifier's own reading of an input: what it is, stretch by stretch, `[{ kind, start, end }]` in seconds, `kind` one of `'speech'`, `'music'`, `'noise'`, in order and end to end over the input. It is step 2 without the prior for speech, which is the guard's: ina's decision as it stands, each 10 ms frame taking the patch nearest it. Digital silence takes the kind around it; an input of nothing but silence has no segments. A host names a recording by it (`audio`'s editor: speech, music, both).
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { segments } from '@audio/neural-denoise'
|
|
128
|
+
|
|
129
|
+
await segments(pcm, 44100) // [{ kind: 'music', start: 0, end: 2.5 }, { kind: 'speech', start: 2.5, end: 11.27 }]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
What it names the test sets below (`node scripts/accuracy.mjs kinds test`): the share of each set's active 10 ms frames named each kind, and its files by the kind most of their active frames have.
|
|
133
|
+
|
|
134
|
+
| Test set | Speech | Music | Noise | Files, by their most frames |
|
|
135
|
+
|---|---|---|---|---|
|
|
136
|
+
| VoiceBank+DEMAND, clean / noisy (824 each) | 99.9% / 99.8% | 0.0% / 0.0% | 0.1% / 0.2% | 823 / 822 speech, 1 / 2 noise |
|
|
137
|
+
| the same in white noise, 20 / 10 / 5 dB (275 each) | 99.6% / 99.6% / 98.9% | 0.0% | 0.4% / 0.4% / 1.1% | 274 / 274 / 273 speech, the rest noise |
|
|
138
|
+
| the same in pink noise, 20 / 10 / 5 dB | 99.6% / 99.4% / 99.4% | 0.0% | 0.4% / 0.6% / 0.6% | 274 speech each, 1 noise |
|
|
139
|
+
| over a music bed 20 / 15 / 10 dB down (275 each) | 99.6% / 99.3% / 98.6% | 0.4% / 0.7% / 1.4% | 0.0% | 274 / 274 / 272 speech, the rest music |
|
|
140
|
+
| ten home narrations (Rooms) | 99.9% | 0.0% | 0.1% | 10 speech |
|
|
141
|
+
| VocalSet, spoken excerpts (10) | 98.2% | 1.8% | 0.0% | 10 speech |
|
|
142
|
+
| MUSDB18 test previews, songs (50) | 0.0% | 100% | 0.0% | 50 music |
|
|
143
|
+
| the same, accompaniment only (50) | 0.0% | 100% | 0.0% | 50 music |
|
|
144
|
+
| Slakh2100 mixes 11 to 20 | 0.0% | 100% | 0.0% | 10 music |
|
|
145
|
+
| the four repair pieces | 0.0% | 95.7% | 4.3% | 3 music, 1 noise |
|
|
146
|
+
| the programmes (8), speech and music in turn | 53.3% | 46.7% | 0.0% | |
|
|
147
|
+
| MUSDB18 vocal stems (50) | 38.1% | 57.5% | 4.4% | 28 music, 19 speech, 3 noise |
|
|
148
|
+
| VocalSet sung phrases (20) / long tones (10) | 26.8% / 11.5% | 66.4% / 67.8% | 6.8% / 20.7% | 14 music, 6 speech / 8 music, 1 speech, 1 noise |
|
|
149
|
+
| DEMAND noise alone (noisy − clean, 824) | 13.6% | 7.8% | 78.6% | 637 noise, 120 speech, 67 music |
|
|
150
|
+
|
|
151
|
+
Speech is named speech and music music wherever the input is one of them (98.2% of the frames or more). A voice singing alone is either. What is neither is called noise four frames in five, speech or music otherwise: of DEMAND's noises alone, 187 of 824 files are named for a kind they are not, so its `'noise'` is the weak answer of the three.
|
|
152
|
+
|
|
124
153
|
**Accuracy**, on labelled sets (`python scripts/accuracy.py guard-sets`, `node scripts/accuracy.mjs guard train|test`): the share of active 10 ms frames passed (within 40 dB of the loudest). The constants (one patch per 100 ms, the streaming bound, prior and hysteresis) were chosen on the training sets: VoiceBank+DEMAND's training speakers, the MUSDB18 training previews, Slakh2100 mixes 1 to 10, VocalSet singers 1 to 5, the Spoken Wikipedia narrations not in Rooms; the test sets below were run once. Beds: clean utterances over a song's accompaniment (the MUSDB18 stems but the vocals) 20, 15 and 10 dB under the speech; hiss: white and pink Gaussian noise at 20, 10 and 5 dB SNR. Programmes: 8 sequences of six segments, speech (five utterances of one kind, or 15 s of narration) and music (up to 20 s of a song, its accompaniment or a Slakh mix) in turn, scored 0.5 s or more from each cut.
|
|
125
154
|
|
|
126
155
|
| Test set | Frames | Passed, offline (DeepFilterNet3) | Passed, streaming (RNNoise) |
|
package/deepfilter.js
CHANGED
|
@@ -58,7 +58,7 @@ export const LEVEL = -20
|
|
|
58
58
|
// the gain (README, Accuracy).
|
|
59
59
|
export const FILL = -20
|
|
60
60
|
|
|
61
|
-
const isNode =
|
|
61
|
+
const isNode = globalThis.process?.release?.name === 'node' // Node, Bun, Deno: not a process polyfill (esm.sh's, a bundler's), whose versions.node is set
|
|
62
62
|
|
|
63
63
|
// ------------------------------------------------ model archive
|
|
64
64
|
|
package/denoise.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import resample from '@audio/resample-sinc'
|
|
7
7
|
import { model as parseRNNoise, create, FRAME, LIMIT as RNNOISE_LIMIT } from './rnnoise.js'
|
|
8
8
|
import { loadDeepFilter, enhance, level, bandEdge, MODEL, LEVEL, LIMIT as DFN_LIMIT, FLOOR } from './deepfilter.js'
|
|
9
|
-
import { guardNet, offline, online, gains, blend } from './guard.js'
|
|
9
|
+
import { guardNet, states, patchOf, offline, online, gains, blend } from './guard.js'
|
|
10
10
|
|
|
11
11
|
export { create as rnnoise, FRAME } from './rnnoise.js'
|
|
12
12
|
export { MODEL } from './deepfilter.js'
|
|
@@ -115,6 +115,29 @@ export async function guard(x, sampleRate = RATE) {
|
|
|
115
115
|
return sampleRate === RATE ? g : fit(resample(g, { from: RATE, to: sampleRate }), x.length)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* segments(x, sampleRate) → what the input is, stretch by stretch: [{ kind, start, end }], kind 'speech', 'music' or
|
|
120
|
+
* 'noise', in seconds, in order and end to end from 0 to the input's length. Ina's segments over the whole input, as
|
|
121
|
+
* the classifier itself decides them: guard.js states() with no prior for speech (that is the guard's, for a voice
|
|
122
|
+
* under a bed). What counts as what follows its training, French radio and TV: speech over a music bed is speech, a
|
|
123
|
+
* song is music, its singer included. A change falls on the 10 ms frame whose nearest patch changes, a patch every
|
|
124
|
+
* 100 ms. Digital silence takes the kind around it; an input of nothing but silence, or shorter than a frame, has no
|
|
125
|
+
* segments.
|
|
126
|
+
*/
|
|
127
|
+
export const KINDS = ['speech', 'music', 'noise']
|
|
128
|
+
export async function segments(x, sampleRate = RATE) {
|
|
129
|
+
if (!x.some(v => v !== 0)) return []
|
|
130
|
+
let nn = await guardNet(), x48 = sampleRate === RATE ? x : resample(Float32Array.from(x), { from: sampleRate, to: RATE })
|
|
131
|
+
let st = states(x48, nn, 0), T = st.length, n = Math.ceil(x48.length / 480), out = []
|
|
132
|
+
for (let g = 0; T && g < n; g++) {
|
|
133
|
+
let kind = KINDS[st[patchOf(g, T)]], last = out.at(-1)
|
|
134
|
+
if (last?.kind === kind) continue
|
|
135
|
+
if (last) last.end = g / 100
|
|
136
|
+
out.push({ kind, start: g / 100, end: x.length / sampleRate })
|
|
137
|
+
}
|
|
138
|
+
return out
|
|
139
|
+
}
|
|
140
|
+
|
|
118
141
|
// the enhanced y where the gain g (at 48 kHz) is 1, the input x where it is 0: music passes untouched
|
|
119
142
|
function mix(y, x, g, rate) {
|
|
120
143
|
if (rate !== RATE) g = fit(resample(g, { from: RATE, to: rate }), x.length)
|
package/guard.js
CHANGED
|
@@ -279,22 +279,33 @@ export function analyzer(nn) {
|
|
|
279
279
|
|
|
280
280
|
const lp = (p, floor) => Math.log(Math.max(p, floor))
|
|
281
281
|
|
|
282
|
-
/**
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
/**
|
|
283
|
+
* states(x, nn, prior) → per patch of x (48 kHz), its state on the best path, 0 speech, 1 music, 2 noise: ina's Viterbi
|
|
284
|
+
* over the whole input, each patch `prior` nats in favour of speech (the guard's PRIOR; 0 is ina's own decision).
|
|
285
|
+
*/
|
|
286
|
+
export function states(x, nn, prior = PRIOR) {
|
|
287
|
+
let a = analyzer(nn), P = [...a(x), ...a.end()], T = P.length, st = new Uint8Array(T)
|
|
288
|
+
if (!T) return st
|
|
286
289
|
// V: the best path's log-likelihood ending in each state (speech, music, noise); B: the state it came from. Every
|
|
287
|
-
// switch costs COST, speech gains
|
|
288
|
-
// state, as numpy's argmax.
|
|
290
|
+
// switch costs COST, speech gains the prior a patch; a patch of silence scores all states alike. Ties go to the
|
|
291
|
+
// lower state, as numpy's argmax.
|
|
289
292
|
let V = [0, 0, 0], B = new Uint8Array(T * 3)
|
|
290
293
|
for (let t = 0; t < T; t++) {
|
|
291
|
-
let e = P[t] ? P[t].map((p, s) => lp(p, 1e-10) + (s ? 0 :
|
|
294
|
+
let e = P[t] ? P[t].map((p, s) => lp(p, 1e-10) + (s ? 0 : prior)) : [0, 0, 0], j = V.indexOf(Math.max(...V))
|
|
292
295
|
V = V.map((v, s) => { let b = V[j] - COST > v ? j : s; B[t * 3 + s] = b; return (b === s ? v : V[j] - COST) + e[s] })
|
|
293
296
|
}
|
|
294
|
-
let
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
297
|
+
for (let t = T - 1, s = V.indexOf(Math.max(...V)); t >= 0; s = B[t * 3 + s], t--) st[t] = s
|
|
298
|
+
return st
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** patchOf(g, T) → the patch, of T, a 48 kHz frame g (480 samples) takes: the one centered nearest it. */
|
|
302
|
+
export const patchOf = (g, T) => Math.min(T - 1, Math.max(0, Math.round((g + .5 - 34.716) / STEP)))
|
|
303
|
+
|
|
304
|
+
/** offline(x, nn) → per 48 kHz frame of x (480 samples), 1 where it is music: ina's Viterbi over the whole input. */
|
|
305
|
+
export function offline(x, nn) {
|
|
306
|
+
let st = states(x, nn), T = st.length, n = Math.ceil(x.length / 480), pass = new Uint8Array(n)
|
|
307
|
+
if (!T) return pass
|
|
308
|
+
for (let g = 0; g < n; g++) pass[g] = +(st[patchOf(g, T)] === 1)
|
|
298
309
|
return pass
|
|
299
310
|
}
|
|
300
311
|
|
package/index.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export function mixback(x: Float32Array, y: Float32Array, opts?: { limit?: numbe
|
|
|
51
51
|
/** Per sample of x, the gain an enhancement of it takes: 1 where it is speech or noise, 0 where it is music (inaSpeechSegmenter's segments over the whole input, as denoise() decides), ramped over 200 ms; blend g·y + (1 − g)·x. */
|
|
52
52
|
export function guard(x: Float32Array, sampleRate?: number): Promise<Float32Array>
|
|
53
53
|
|
|
54
|
+
/** What x is, stretch by stretch, as inaSpeechSegmenter's speech/music/noise CNN segments the whole input (no prior for speech, which is the guard's): in order, end to end from 0 to its length, seconds; none for an input of digital silence. */
|
|
55
|
+
export function segments(x: Float32Array, sampleRate?: number): Promise<{ kind: 'speech' | 'music' | 'noise', start: number, end: number }[]>
|
|
56
|
+
|
|
57
|
+
/** The kinds segments() names, as the classifier orders them. */
|
|
58
|
+
export const KINDS: ['speech', 'music', 'noise']
|
|
59
|
+
|
|
54
60
|
/** Load a model once to reuse it across denoise() calls. */
|
|
55
61
|
export function load(model?: 'rnnoise' | 'deepfilternet3', opts?: LoadOptions): Promise<Model>
|
|
56
62
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@audio/neural-denoise",
|
|
3
|
-
"version": "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; music passes through untouched (inaSpeechSegmenter's speech/music/noise CNN)",
|
|
3
|
+
"version": "0.7.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; music passes through untouched (inaSpeechSegmenter's speech/music/noise CNN), and the same classifier names an input's stretches (segments)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"main": "denoise.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"audio.d.ts"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@audio/neural-runtime": "^0.
|
|
41
|
+
"@audio/neural-runtime": "^0.2.0",
|
|
42
42
|
"@audio/resample-sinc": "^1.3.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|