@audio/denoise-detect 0.1.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.
Files changed (2) hide show
  1. package/denoise.js +143 -0
  2. package/package.json +49 -0
package/denoise.js ADDED
@@ -0,0 +1,143 @@
1
+ // denoise — content-aware auto-selector that classifies the dominant noise type
2
+ // in `data` and dispatches to the most suitable single-pass method.
3
+ //
4
+ // Classification (single STFT sweep over the input):
5
+ // - tonalScore — strong narrow peaks at mains harmonics → dehum
6
+ // - clickScore — high kurtosis of AR residual → declick
7
+ // - lfScore — LF/MF energy ratio above 4 → dewind
8
+ // - sibilanceScore — 5–9 kHz peaks vs. mids → deesser
9
+ // - reverbScore — slow late-tail decay (cepstral peak) → dereverb
10
+ // - stationary → omlsa (best non-stationary general denoise)
11
+ // - otherwise → wiener (transparent broadband)
12
+ //
13
+ // Returns { out, plan } so callers can inspect which method ran.
14
+
15
+ import { stftAnalyse } from '@audio/denoise-core'
16
+ import { arFit } from '@audio/denoise-core'
17
+ import wiener from '@audio/denoise-wiener'
18
+ import omlsa from '@audio/denoise-omlsa'
19
+ import dehum from '@audio/denoise-dehum'
20
+ import declick from '@audio/denoise-declick'
21
+ import dewind from '@audio/denoise-dewind'
22
+ import deesser from '@audio/denoise-deesser'
23
+ import dereverb from '@audio/denoise-dereverb'
24
+
25
+ export default function denoise(data, params = {}) {
26
+ let fs = params.fs || 44100
27
+ let force = params.force // skip classification
28
+ let plan = force ? { method: force, scores: {} } : classify(data, fs)
29
+ let opts = { fs, ...params }
30
+ let out
31
+ switch (plan.method) {
32
+ case 'dehum': out = dehum(new Float32Array(data), { ...opts, freq: plan.humFreq || 50 }); break
33
+ case 'declick': out = declick(new Float32Array(data), opts); break
34
+ case 'dewind': out = dewind(new Float32Array(data), opts); break
35
+ case 'deesser': out = deesser(new Float32Array(data), opts); break
36
+ case 'dereverb':out = dereverb(data, opts); break
37
+ case 'omlsa': out = omlsa(data, opts); break
38
+ case 'wiener':
39
+ default: out = wiener(data, opts)
40
+ }
41
+ return params.returnPlan ? { out, plan } : out
42
+ }
43
+
44
+ export function classify(data, fs = 44100) {
45
+ let N = 2048, hop = 512, half = N >> 1
46
+ let bins = new Float64Array(half + 1)
47
+ let frames = 0
48
+ let lfSum = 0, mfSum = 0, hiSum = 0
49
+ let frameVar = [] // for stationarity
50
+
51
+ stftAnalyse(data, mag => {
52
+ let lf = 0, mf = 0, hi = 0
53
+ for (let k = 0; k <= half; k++) {
54
+ let p = mag[k] * mag[k]
55
+ bins[k] += p
56
+ let f = k * fs / N
57
+ if (f < 200) lf += p
58
+ else if (f < 2000) mf += p
59
+ else if (f < 9000) hi += p
60
+ }
61
+ lfSum += lf; mfSum += mf; hiSum += hi
62
+ frameVar.push(lf + mf + hi)
63
+ frames++
64
+ }, { frameSize: N, hopSize: hop })
65
+ if (!frames) return { method: 'wiener', scores: {} }
66
+
67
+ // Tonal hum detection via Goertzel: line power vs. off-line power at ±15 Hz.
68
+ // Avoids FFT-bin leakage at low frequencies (50/60 Hz fall between coarse bins).
69
+ // Counts harmonics where on/off ratio ≥ 50; threshold ≥2 means harmonic series
70
+ // is present (rules out arbitrary single tones).
71
+ let chunk = data.length > 16384 ? data.subarray(0, 16384) : data
72
+ let humScore = (f0) => {
73
+ let hits = 0
74
+ for (let h = 1; h <= 3; h++) {
75
+ let f = f0 * h
76
+ if (f > fs / 2 - 50 || f - 15 < 1) break
77
+ let on = goertzelE(chunk, f, fs)
78
+ let offL = goertzelE(chunk, f - 15, fs)
79
+ let offR = goertzelE(chunk, f + 15, fs)
80
+ let off = Math.max((offL + offR) / 2, 1e-30)
81
+ if (on / off > 50) hits++
82
+ }
83
+ return hits
84
+ }
85
+ let s50 = humScore(50), s60 = humScore(60)
86
+ let humBest = Math.max(s50, s60)
87
+ let humFreq = s50 >= s60 ? 50 : 60
88
+
89
+ // LF/MF ratio
90
+ let lfRatio = lfSum / Math.max(mfSum, 1e-30)
91
+ // HI/MF ratio
92
+ let hiRatio = hiSum / Math.max(mfSum, 1e-30)
93
+
94
+ // Click score: AR residual kurtosis on a short window
95
+ let clickScore = 0
96
+ if (data.length >= 4096) {
97
+ let win = data.subarray(0, 4096)
98
+ try {
99
+ let { a } = arFit(win, 30)
100
+ let resid = new Float64Array(4096), mean = 0, m2 = 0, m4 = 0
101
+ for (let i = 30; i < 4096; i++) {
102
+ let s = win[i]
103
+ for (let k = 1; k <= 30; k++) s += a[k] * win[i - k]
104
+ resid[i] = s; mean += s
105
+ }
106
+ mean /= (4096 - 30)
107
+ for (let i = 30; i < 4096; i++) { let d = resid[i] - mean; m2 += d * d; m4 += d * d * d * d }
108
+ m2 /= (4096 - 30); m4 /= (4096 - 30)
109
+ clickScore = m2 > 0 ? m4 / (m2 * m2) - 3 : 0 // excess kurtosis
110
+ } catch {}
111
+ }
112
+
113
+ // Stationarity: variance-of-variances of frame energy
114
+ let mean = 0; for (let v of frameVar) mean += v; mean /= frames
115
+ let varE = 0; for (let v of frameVar) varE += (v - mean) ** 2; varE /= frames
116
+ let cv = mean > 0 ? Math.sqrt(varE) / mean : 0 // higher = less stationary
117
+
118
+ let scores = {
119
+ hum: humBest, humFreq,
120
+ click: clickScore,
121
+ lf: lfRatio,
122
+ hi: hiRatio,
123
+ nonstationarity: cv
124
+ }
125
+
126
+ // Priority: tonal hum > impulses > sibilance > rumble > non-stationary > broadband.
127
+ // humBest is a hit count: ≥2 of the first 3 harmonics show 20× peak-to-median sharpness.
128
+ let method = 'wiener'
129
+ if (humBest >= 2) method = 'dehum'
130
+ else if (clickScore > 12) method = 'declick'
131
+ else if (hiRatio > 8) method = 'deesser' // white noise scores ~3.9 by bandwidth alone
132
+ else if (lfRatio > 3) method = 'dewind'
133
+ else if (cv > 0.6) method = 'omlsa'
134
+
135
+ return { method, scores, humFreq }
136
+ }
137
+
138
+ // Goertzel power at frequency f.
139
+ function goertzelE(data, f, fs) {
140
+ let w = 2 * Math.PI * f / fs, c = 2 * Math.cos(w), s1 = 0, s2 = 0
141
+ for (let i = 0; i < data.length; i++) { let s = data[i] + c * s1 - s2; s2 = s1; s1 = s }
142
+ return (s1 * s1 + s2 * s2 - c * s1 * s2) / data.length
143
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@audio/denoise-detect",
3
+ "version": "0.1.0",
4
+ "description": "denoise — content-aware auto-selector that classifies the dominant noise type",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "denoise.js",
8
+ "exports": {
9
+ ".": "./denoise.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "denoise.js"
14
+ ],
15
+ "dependencies": {
16
+ "@audio/denoise-core": "^0.1.0",
17
+ "@audio/denoise-wiener": "^0.1.0",
18
+ "@audio/denoise-omlsa": "^0.1.0",
19
+ "@audio/denoise-dehum": "^0.1.0",
20
+ "@audio/denoise-declick": "^0.1.0",
21
+ "@audio/denoise-dewind": "^0.1.0",
22
+ "@audio/denoise-deesser": "^0.1.0",
23
+ "@audio/denoise-dereverb": "^0.1.0"
24
+ },
25
+ "keywords": [
26
+ "audio",
27
+ "dsp",
28
+ "denoise",
29
+ "restoration",
30
+ "detect"
31
+ ],
32
+ "license": "MIT",
33
+ "author": "Dmitry Iv. <dfcreative@gmail.com>",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/audiojs/denoise.git",
37
+ "directory": "packages/denoise-detect"
38
+ },
39
+ "homepage": "https://github.com/audiojs/denoise/tree/main/packages/denoise-detect",
40
+ "bugs": {
41
+ "url": "https://github.com/audiojs/denoise/issues"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }