@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.
- package/denoise.js +143 -0
- 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
|
+
}
|