@audio/denoise-detect 0.1.2 → 0.1.4
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/denoise.js +59 -34
- package/package.json +1 -1
package/denoise.js
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
// in `data` and dispatches to the most suitable single-pass method.
|
|
3
3
|
//
|
|
4
4
|
// Classification (single STFT sweep over the input):
|
|
5
|
-
// -
|
|
6
|
-
// -
|
|
7
|
-
// -
|
|
8
|
-
// -
|
|
9
|
-
// -
|
|
10
|
-
// -
|
|
11
|
-
// - otherwise → wiener (transparent broadband)
|
|
5
|
+
// - hum — narrow peaks at mains harmonics (Goertzel) → dehum
|
|
6
|
+
// - click — high kurtosis of AR residual → declick
|
|
7
|
+
// - hi — 5–9 kHz / mid energy ratio → deesser
|
|
8
|
+
// - lf — LF/mid energy ratio → dewind
|
|
9
|
+
// - stationarity — frame-energy floor CV: stable → wiener, wandering → omlsa
|
|
10
|
+
// - otherwise → wiener (transparent broadband)
|
|
12
11
|
//
|
|
12
|
+
// dereverb has no reliable single-pass signature, so auto-mode never selects it —
|
|
13
|
+
// reach it explicitly via `denoise(data, { force: 'dereverb' })` or `dereverb()`.
|
|
13
14
|
// Returns { out, plan } so callers can inspect which method ran.
|
|
14
15
|
|
|
15
16
|
import { stftAnalyse } from '@audio/stft'
|
|
@@ -49,18 +50,19 @@ export function classify(data, fs = 44100) {
|
|
|
49
50
|
let frameVar = [] // for stationarity
|
|
50
51
|
|
|
51
52
|
stftAnalyse(data, mag => {
|
|
52
|
-
let lf = 0, mf = 0, hi = 0
|
|
53
|
+
let lf = 0, mf = 0, hi = 0, tot = 0
|
|
53
54
|
for (let k = 0; k <= half; k++) {
|
|
54
55
|
let p = mag[k] * mag[k]
|
|
55
56
|
bins[k] += p
|
|
57
|
+
tot += p
|
|
56
58
|
let f = k * fs / N
|
|
57
59
|
if (f < 200) lf += p
|
|
58
60
|
else if (f < 2000) mf += p
|
|
59
|
-
else if (f < 9000) hi += p
|
|
61
|
+
else if (f >= 5000 && f < 9000) hi += p // sibilance band, kept specific (not 2–9 kHz)
|
|
60
62
|
}
|
|
61
63
|
lfSum += lf; mfSum += mf; hiSum += hi
|
|
62
|
-
frameVar.push(
|
|
63
|
-
frames++
|
|
64
|
+
frameVar.push(tot) // full-spectrum energy — band-restricted
|
|
65
|
+
frames++ // sums under-average the floor statistic
|
|
64
66
|
}, { frameSize: N, hopSize: hop })
|
|
65
67
|
if (!frames) return { method: 'wiener', scores: {} }
|
|
66
68
|
|
|
@@ -91,46 +93,69 @@ export function classify(data, fs = 44100) {
|
|
|
91
93
|
// HI/MF ratio
|
|
92
94
|
let hiRatio = hiSum / Math.max(mfSum, 1e-30)
|
|
93
95
|
|
|
94
|
-
// Click score: AR
|
|
96
|
+
// Click score: max AR-residual excess kurtosis over windows spanning the whole
|
|
97
|
+
// signal. Clicks are sparse and impulsive — a fixed prefix window can miss them
|
|
98
|
+
// entirely, so scan across and take the peak (the window holding a click spikes).
|
|
95
99
|
let clickScore = 0
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
let
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
let cw = 4096
|
|
101
|
+
if (data.length >= cw) {
|
|
102
|
+
let stride = Math.max(cw, Math.floor((data.length - cw) / 8) || cw)
|
|
103
|
+
for (let start = 0; start + cw <= data.length; start += stride) {
|
|
104
|
+
let seg = data.subarray(start, start + cw)
|
|
105
|
+
try {
|
|
106
|
+
let { a } = arFit(seg, 30)
|
|
107
|
+
let resid = new Float64Array(cw), mean = 0, m2 = 0, m4 = 0
|
|
108
|
+
for (let i = 30; i < cw; i++) {
|
|
109
|
+
let s = seg[i]
|
|
110
|
+
for (let k = 1; k <= 30; k++) s += a[k] * seg[i - k]
|
|
111
|
+
resid[i] = s; mean += s
|
|
112
|
+
}
|
|
113
|
+
mean /= (cw - 30)
|
|
114
|
+
for (let i = 30; i < cw; i++) { let d = resid[i] - mean; m2 += d * d; m4 += d * d * d * d }
|
|
115
|
+
m2 /= (cw - 30); m4 /= (cw - 30)
|
|
116
|
+
let ex = m2 > 0 ? m4 / (m2 * m2) - 3 : 0 // excess kurtosis
|
|
117
|
+
if (ex > clickScore) clickScore = ex
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
// Noise stationarity: CV of the frame-energy FLOOR (rolling minimum over ~0.75 s).
|
|
123
|
+
// Speech dynamics ride above the floor, so the floor tracks the *noise bed*:
|
|
124
|
+
// stationary noise → stable floor (CV ≈ 0.06 measured on speech+white), babble /
|
|
125
|
+
// wandering beds → drifting floor (CV ≈ 0.5). Raw frame-energy CV can't make this
|
|
126
|
+
// call — speech's own variance trips it regardless of the noise.
|
|
127
|
+
let floorCV = 0
|
|
128
|
+
{
|
|
129
|
+
let D = 64, step = 16, floors = []
|
|
130
|
+
for (let i = D; i < frames; i += step) {
|
|
131
|
+
let mn = Infinity
|
|
132
|
+
for (let j = i - D; j < i; j++) if (frameVar[j] < mn) mn = frameVar[j]
|
|
133
|
+
floors.push(mn)
|
|
134
|
+
}
|
|
135
|
+
if (floors.length >= 3) {
|
|
136
|
+
let m = 0; for (let f of floors) m += f; m /= floors.length
|
|
137
|
+
let v = 0; for (let f of floors) v += (f - m) ** 2; v /= floors.length
|
|
138
|
+
floorCV = m > 0 ? Math.sqrt(v) / m : 0
|
|
139
|
+
}
|
|
140
|
+
}
|
|
117
141
|
|
|
118
142
|
let scores = {
|
|
119
143
|
hum: humBest, humFreq,
|
|
120
144
|
click: clickScore,
|
|
121
145
|
lf: lfRatio,
|
|
122
146
|
hi: hiRatio,
|
|
123
|
-
|
|
147
|
+
stationarity: floorCV // low = stationary noise bed
|
|
124
148
|
}
|
|
125
149
|
|
|
126
|
-
// Priority: tonal hum > impulses > sibilance > rumble >
|
|
150
|
+
// Priority: tonal hum > impulses > sibilance > rumble > stationary → wiener,
|
|
151
|
+
// non-stationary → omlsa (IMCRA keeps adapting where a frozen profile can't).
|
|
127
152
|
// humBest is a hit count: ≥2 of the first 3 harmonics show 20× peak-to-median sharpness.
|
|
128
153
|
let method = 'wiener'
|
|
129
154
|
if (humBest >= 2) method = 'dehum'
|
|
130
155
|
else if (clickScore > 12) method = 'declick'
|
|
131
156
|
else if (hiRatio > 8) method = 'deesser' // white noise scores ~3.9 by bandwidth alone
|
|
132
157
|
else if (lfRatio > 3) method = 'dewind'
|
|
133
|
-
else if (
|
|
158
|
+
else if (floorCV > 0.3) method = 'omlsa' // white ~0.06 · rumble ~0.2 · babble ~0.5
|
|
134
159
|
|
|
135
160
|
return { method, scores, humFreq }
|
|
136
161
|
}
|