@audio/denoise-detect 0.1.9 → 0.2.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 +1 -1
- package/denoise.js +39 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ 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
32
|
5. non-stationary noise (frame-energy CV > 0.6) → omlsa
|
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
|
|
@@ -111,31 +111,10 @@ export function classify(data, fs = 44100) {
|
|
|
111
111
|
// HI/MF ratio
|
|
112
112
|
let hiRatio = hiSum / Math.max(mfSum, 1e-30)
|
|
113
113
|
|
|
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
|
-
}
|
|
114
|
+
// Click score: impulses per second. A click stands far out of the AR(30) prediction error around it; a glottal
|
|
115
|
+
// pulse doesn't, its neighbours 2.5–12 ms away being pulses too (the error's kurtosis can't tell them apart:
|
|
116
|
+
// clean narration read 2.5–401 against a trigger of 12).
|
|
117
|
+
let clickScore = impulseRate(data, fs)
|
|
139
118
|
|
|
140
119
|
// Noise stationarity: CV of the frame-energy FLOOR (rolling minimum over ~0.75 s).
|
|
141
120
|
// Speech dynamics ride above the floor, so the floor tracks the *noise bed*:
|
|
@@ -170,7 +149,7 @@ export function classify(data, fs = 44100) {
|
|
|
170
149
|
// humBest is a hit count: ≥2 of the first 3 harmonics show 20× peak-to-median sharpness.
|
|
171
150
|
let method = 'wiener'
|
|
172
151
|
if (humBest >= 2) method = 'dehum'
|
|
173
|
-
else if (clickScore >
|
|
152
|
+
else if (clickScore > CLICK_RATE) method = 'declick'
|
|
174
153
|
else if (hiRatio > 8) method = 'deesser' // white noise scores ~3.9 by bandwidth alone
|
|
175
154
|
else if (lfRatio > 3) method = 'dewind'
|
|
176
155
|
else if (floorCV > 0.3) method = 'omlsa' // white ~0.06 · rumble ~0.2 · babble ~0.5
|
|
@@ -179,6 +158,39 @@ export function classify(data, fs = 44100) {
|
|
|
179
158
|
}
|
|
180
159
|
|
|
181
160
|
// Goertzel power at frequency f.
|
|
161
|
+
/** Impulses per second a recording must carry for declick. Measured: clean narration, music and a sung vowel 0–0.87;
|
|
162
|
+
* 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). */
|
|
163
|
+
export const CLICK_RATE = 1
|
|
164
|
+
|
|
165
|
+
/** Impulses per second. An impulse is an AR(30) residual sample over 12× the residual RMS within ±10 ms that also
|
|
166
|
+
* towers (2×) over every residual 2.5–15 ms away: a glottal pulse has its like one pitch period off, a click doesn't.
|
|
167
|
+
* Events 5 ms apart, over up to 64 windows of 4096 samples spread across the signal (≈ 6 s at 44.1 kHz). */
|
|
168
|
+
function impulseRate(data, fs) {
|
|
169
|
+
const W = 4096, P = 30, K = 12, M = 2
|
|
170
|
+
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)
|
|
171
|
+
if (data.length < W || W - P <= 2 * h) return 0
|
|
172
|
+
let count = Math.min(64, Math.floor(data.length / W)), stride = count > 1 ? (data.length - W) / (count - 1) : 0
|
|
173
|
+
let r = new Float64Array(W), events = 0, seconds = 0
|
|
174
|
+
for (let w = 0; w < count; w++) {
|
|
175
|
+
let seg = data.subarray(Math.round(w * stride), Math.round(w * stride) + W), a
|
|
176
|
+
try { ({ a } = arFit(seg, P)) } catch { continue }
|
|
177
|
+
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 }
|
|
178
|
+
let e2 = 0, last = -gap
|
|
179
|
+
for (let i = P; i < P + 2 * h; i++) e2 += r[i] * r[i]
|
|
180
|
+
for (let i = P + h; i < W - h; i++) {
|
|
181
|
+
let v = Math.abs(r[i]), rms = Math.sqrt(e2 / (2 * h))
|
|
182
|
+
if (rms > 0 && v > K * rms && i - last > gap) {
|
|
183
|
+
let m = 0
|
|
184
|
+
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])
|
|
185
|
+
if (v > M * m) { events++; last = i }
|
|
186
|
+
}
|
|
187
|
+
e2 += r[i + h] * r[i + h] - r[i - h] * r[i - h]
|
|
188
|
+
}
|
|
189
|
+
seconds += (W - P - 2 * h) / fs
|
|
190
|
+
}
|
|
191
|
+
return seconds ? events / seconds : 0
|
|
192
|
+
}
|
|
193
|
+
|
|
182
194
|
function goertzelE(data, f, fs) {
|
|
183
195
|
let w = 2 * Math.PI * f / fs, c = 2 * Math.cos(w), s1 = 0, s2 = 0
|
|
184
196
|
for (let i = 0; i < data.length; i++) { let s = data[i] + c * s1 - s2; s2 = s1; s1 = s }
|