@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 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 (excess kurtosis of AR residual > 12)
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 CV > 0.6) → omlsa
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 — high kurtosis of AR residual → declick
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
- case 'dehum': out = dehum(new Float32Array(data), { ...opts, freq: plan.humFreq || 50 }); break
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: max AR-residual excess kurtosis over windows spanning the whole
115
- // signal. Clicks are sparse and impulsive — a fixed prefix window can miss them
116
- // entirely, so scan across and take the peak (the window holding a click spikes).
117
- let clickScore = 0
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 > 12) method = 'declick'
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
- /** peak excess kurtosis of AR residual across scanned windows */
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.1.9",
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.1.0",
22
- "@audio/denoise-omlsa": "^0.1.0",
23
- "@audio/denoise-dehum": "^0.1.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.1.0",
27
+ "@audio/denoise-dereverb": "^0.2.0",
28
28
  "@audio/lpc": "^1.0.0",
29
- "@audio/stft": "^1.0.3"
29
+ "@audio/stft": "^2.0.0"
30
30
  },
31
31
  "keywords": [
32
32
  "audio",