@audio/denoise-detect 0.1.9 → 0.3.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 +2 -2
- package/denoise.js +42 -28
- package/index.d.ts +4 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -26,10 +26,10 @@ denoise(data, { force: 'wiener' }) // skip classifie
|
|
|
26
26
|
|
|
27
27
|
**Routing (in priority order):**
|
|
28
28
|
1. tonal hum (Goertzel — ≥2 of first 3 harmonics show 50× line/off-line ratio at 50 or 60 Hz)
|
|
29
|
-
2. impulses (
|
|
29
|
+
2. impulses: AR(30) residual samples over 12× the residual RMS within ±10 ms and 2× every residual 2.5–15 ms away (a glottal pulse has its like one pitch period off), more than 1 a second (`CLICK_RATE`)
|
|
30
30
|
3. sibilance (high/mid band power ratio > 8)
|
|
31
31
|
4. LF rumble (low/mid band power ratio > 3)
|
|
32
|
-
5. non-stationary noise (frame-energy
|
|
32
|
+
5. non-stationary noise (CV of the rolling-minimum frame-energy floor > 0.3) → omlsa
|
|
33
33
|
6. otherwise → wiener
|
|
34
34
|
|
|
35
35
|
---
|
package/denoise.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Classification (single STFT sweep over the input):
|
|
5
5
|
// - hum — narrow peaks at mains harmonics (Goertzel) → dehum
|
|
6
|
-
// - click —
|
|
6
|
+
// - click — impulses standing out of the AR residual, over 1 a second → declick
|
|
7
7
|
// - hi — 5–9 kHz / mid energy ratio → deesser
|
|
8
8
|
// - lf — LF/mid energy ratio → dewind
|
|
9
9
|
// - stationarity — frame-energy floor CV: stable → wiener, wandering → omlsa
|
|
@@ -48,7 +48,9 @@ export default function denoise(data, params = {}) {
|
|
|
48
48
|
let opts = { fs, ...params }
|
|
49
49
|
let out
|
|
50
50
|
switch (plan.method) {
|
|
51
|
-
|
|
51
|
+
// the series classify heard; dehum measures its exact frequency and harmonics. Forced without `freq`,
|
|
52
|
+
// dehum measures 50 and 60 Hz itself (a fixed 50 missed 60 Hz mains)
|
|
53
|
+
case 'dehum': out = dehum(new Float32Array(data), { ...opts, freq: params.freq ?? plan.humFreq }); break
|
|
52
54
|
case 'declick': out = declick(new Float32Array(data), opts); break
|
|
53
55
|
case 'dewind': out = dewind(new Float32Array(data), opts); break
|
|
54
56
|
case 'deesser': out = deesser(new Float32Array(data), opts); break
|
|
@@ -111,31 +113,10 @@ export function classify(data, fs = 44100) {
|
|
|
111
113
|
// HI/MF ratio
|
|
112
114
|
let hiRatio = hiSum / Math.max(mfSum, 1e-30)
|
|
113
115
|
|
|
114
|
-
// Click score:
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
let clickScore =
|
|
118
|
-
let cw = 4096
|
|
119
|
-
if (data.length >= cw) {
|
|
120
|
-
let stride = Math.max(cw, Math.floor((data.length - cw) / 8) || cw)
|
|
121
|
-
for (let start = 0; start + cw <= data.length; start += stride) {
|
|
122
|
-
let seg = data.subarray(start, start + cw)
|
|
123
|
-
try {
|
|
124
|
-
let { a } = arFit(seg, 30)
|
|
125
|
-
let resid = new Float64Array(cw), mean = 0, m2 = 0, m4 = 0
|
|
126
|
-
for (let i = 30; i < cw; i++) {
|
|
127
|
-
let s = seg[i]
|
|
128
|
-
for (let k = 1; k <= 30; k++) s += a[k] * seg[i - k]
|
|
129
|
-
resid[i] = s; mean += s
|
|
130
|
-
}
|
|
131
|
-
mean /= (cw - 30)
|
|
132
|
-
for (let i = 30; i < cw; i++) { let d = resid[i] - mean; m2 += d * d; m4 += d * d * d * d }
|
|
133
|
-
m2 /= (cw - 30); m4 /= (cw - 30)
|
|
134
|
-
let ex = m2 > 0 ? m4 / (m2 * m2) - 3 : 0 // excess kurtosis
|
|
135
|
-
if (ex > clickScore) clickScore = ex
|
|
136
|
-
} catch {}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
116
|
+
// Click score: impulses per second. A click stands far out of the AR(30) prediction error around it; a glottal
|
|
117
|
+
// pulse doesn't, its neighbours 2.5–12 ms away being pulses too (the error's kurtosis can't tell them apart:
|
|
118
|
+
// clean narration read 2.5–401 against a trigger of 12).
|
|
119
|
+
let clickScore = impulseRate(data, fs)
|
|
139
120
|
|
|
140
121
|
// Noise stationarity: CV of the frame-energy FLOOR (rolling minimum over ~0.75 s).
|
|
141
122
|
// Speech dynamics ride above the floor, so the floor tracks the *noise bed*:
|
|
@@ -170,7 +151,7 @@ export function classify(data, fs = 44100) {
|
|
|
170
151
|
// humBest is a hit count: ≥2 of the first 3 harmonics show 20× peak-to-median sharpness.
|
|
171
152
|
let method = 'wiener'
|
|
172
153
|
if (humBest >= 2) method = 'dehum'
|
|
173
|
-
else if (clickScore >
|
|
154
|
+
else if (clickScore > CLICK_RATE) method = 'declick'
|
|
174
155
|
else if (hiRatio > 8) method = 'deesser' // white noise scores ~3.9 by bandwidth alone
|
|
175
156
|
else if (lfRatio > 3) method = 'dewind'
|
|
176
157
|
else if (floorCV > 0.3) method = 'omlsa' // white ~0.06 · rumble ~0.2 · babble ~0.5
|
|
@@ -179,6 +160,39 @@ export function classify(data, fs = 44100) {
|
|
|
179
160
|
}
|
|
180
161
|
|
|
181
162
|
// Goertzel power at frequency f.
|
|
163
|
+
/** Impulses per second a recording must carry for declick. Measured: clean narration, music and a sung vowel 0–0.87;
|
|
164
|
+
* the same with clicks at 2.5 a second 1.9 and up, faint ones (0.05) 1.3 and up (Spoken Wikipedia takes, lena). */
|
|
165
|
+
export const CLICK_RATE = 1
|
|
166
|
+
|
|
167
|
+
/** Impulses per second. An impulse is an AR(30) residual sample over 12× the residual RMS within ±10 ms that also
|
|
168
|
+
* towers (2×) over every residual 2.5–15 ms away: a glottal pulse has its like one pitch period off, a click doesn't.
|
|
169
|
+
* Events 5 ms apart, over up to 64 windows of 4096 samples spread across the signal (≈ 6 s at 44.1 kHz). */
|
|
170
|
+
function impulseRate(data, fs) {
|
|
171
|
+
const W = 4096, P = 30, K = 12, M = 2
|
|
172
|
+
const h = Math.round(0.01 * fs), gap = Math.round(0.005 * fs), near = Math.round(0.0025 * fs), far = Math.round(0.015 * fs)
|
|
173
|
+
if (data.length < W || W - P <= 2 * h) return 0
|
|
174
|
+
let count = Math.min(64, Math.floor(data.length / W)), stride = count > 1 ? (data.length - W) / (count - 1) : 0
|
|
175
|
+
let r = new Float64Array(W), events = 0, seconds = 0
|
|
176
|
+
for (let w = 0; w < count; w++) {
|
|
177
|
+
let seg = data.subarray(Math.round(w * stride), Math.round(w * stride) + W), a
|
|
178
|
+
try { ({ a } = arFit(seg, P)) } catch { continue }
|
|
179
|
+
for (let i = P; i < W; i++) { let e = seg[i]; for (let k = 1; k <= P; k++) e += a[k] * seg[i - k]; r[i] = e }
|
|
180
|
+
let e2 = 0, last = -gap
|
|
181
|
+
for (let i = P; i < P + 2 * h; i++) e2 += r[i] * r[i]
|
|
182
|
+
for (let i = P + h; i < W - h; i++) {
|
|
183
|
+
let v = Math.abs(r[i]), rms = Math.sqrt(e2 / (2 * h))
|
|
184
|
+
if (rms > 0 && v > K * rms && i - last > gap) {
|
|
185
|
+
let m = 0
|
|
186
|
+
for (let j = Math.max(P, i - far); j <= Math.min(W - 1, i + far); j++) if (Math.abs(j - i) >= near && Math.abs(r[j]) > m) m = Math.abs(r[j])
|
|
187
|
+
if (v > M * m) { events++; last = i }
|
|
188
|
+
}
|
|
189
|
+
e2 += r[i + h] * r[i + h] - r[i - h] * r[i - h]
|
|
190
|
+
}
|
|
191
|
+
seconds += (W - P - 2 * h) / fs
|
|
192
|
+
}
|
|
193
|
+
return seconds ? events / seconds : 0
|
|
194
|
+
}
|
|
195
|
+
|
|
182
196
|
function goertzelE(data, f, fs) {
|
|
183
197
|
let w = 2 * Math.PI * f / fs, c = 2 * Math.cos(w), s1 = 0, s2 = 0
|
|
184
198
|
for (let i = 0; i < data.length; i++) { let s = data[i] + c * s1 - s2; s2 = s1; s1 = s }
|
package/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface ClassifyScores {
|
|
|
6
6
|
hum: number
|
|
7
7
|
/** 50 or 60 */
|
|
8
8
|
humFreq: number
|
|
9
|
-
/**
|
|
9
|
+
/** impulses per second standing out of the AR(30) residual; declick above `CLICK_RATE` */
|
|
10
10
|
click: number
|
|
11
11
|
/** low/mid band power ratio */
|
|
12
12
|
lf: number
|
|
@@ -47,6 +47,9 @@ export default function denoise(data: Float32Array, params: DenoiseOptions & { r
|
|
|
47
47
|
/** Run the classification sweep only (no cleaning). */
|
|
48
48
|
export function classify(data: Float32Array, fs?: number): Plan
|
|
49
49
|
|
|
50
|
+
/** Impulses per second above which classify() routes to declick. */
|
|
51
|
+
export const CLICK_RATE: number
|
|
52
|
+
|
|
50
53
|
export interface DeesserOptions {
|
|
51
54
|
/** sample rate (Hz), default 44100 */
|
|
52
55
|
fs?: number
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@audio/denoise-detect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "denoise — content-aware auto-selector that classifies the dominant noise type",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
],
|
|
19
19
|
"types": "index.d.ts",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@audio/denoise-wiener": "^0.
|
|
22
|
-
"@audio/denoise-omlsa": "^0.
|
|
23
|
-
"@audio/denoise-dehum": "^0.
|
|
21
|
+
"@audio/denoise-wiener": "^0.2.0",
|
|
22
|
+
"@audio/denoise-omlsa": "^0.2.0",
|
|
23
|
+
"@audio/denoise-dehum": "^0.2.0",
|
|
24
24
|
"@audio/denoise-declick": "^0.1.0",
|
|
25
25
|
"@audio/denoise-dewind": "^0.1.0",
|
|
26
26
|
"@audio/dynamics-deesser": "^0.2.7",
|
|
27
|
-
"@audio/denoise-dereverb": "^0.
|
|
27
|
+
"@audio/denoise-dereverb": "^0.2.0",
|
|
28
28
|
"@audio/lpc": "^1.0.0",
|
|
29
|
-
"@audio/stft": "^
|
|
29
|
+
"@audio/stft": "^2.0.0"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"audio",
|