@audio/defeedback-analyzer 1.0.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/analyzer.js +59 -0
  2. package/package.json +33 -0
package/analyzer.js ADDED
@@ -0,0 +1,59 @@
1
+ // Feedback candidate detection — spectral peaks scored by the classical howl criteria
2
+ // (van Waterschoot & Moonen 2011): PNPR (peak-to-neighboring-power ratio — feedback is
3
+ // far narrower than music) and PHPR (peak-to-harmonic-power ratio — feedback is a bare
4
+ // sinusoid, musical tones carry harmonics).
5
+
6
+ import { fft } from 'fourier-transform'
7
+
8
+ /**
9
+ * Analyze one window. @returns Array<{freq, level (dB), pnpr, phpr}>
10
+ * @param {Float32Array} data — analysis window (power of 2)
11
+ * @param {object} opts — { fs=44100, maxCandidates=6, floor=-70 (dB) }
12
+ */
13
+ export default function analyze (data, { fs = 44100, maxCandidates = 6, floor = -70 } = {}) {
14
+ let n = data.length, half = n / 2
15
+ let buf = new Float64Array(n)
16
+ for (let i = 0; i < n; i++) buf[i] = data[i] * (0.5 - 0.5 * Math.cos(2 * Math.PI * i / n))
17
+ let [re, im] = fft(buf)
18
+ let mag = new Float64Array(half + 1)
19
+ for (let k = 0; k <= half; k++) mag[k] = re[k] * re[k] + im[k] * im[k]
20
+
21
+ let db = p => 10 * Math.log10(p + 1e-20)
22
+ let at = kc => { // max power around a (possibly fractional) bin
23
+ let b = Math.round(kc)
24
+ let m = 0
25
+ for (let j = Math.max(0, b - 1); j <= Math.min(half, b + 1); j++) if (mag[j] > m) m = mag[j]
26
+ return m
27
+ }
28
+ let out = []
29
+ let kMin = Math.max(4, Math.round(100 * n / fs)) // ignore <100 Hz rumble
30
+ for (let k = kMin; k < half - 4; k++) {
31
+ if (mag[k] <= mag[k - 1] || mag[k] < mag[k + 1]) continue
32
+ let level = db(mag[k] * 4 / (n * n)) // rough dBFS calibration
33
+ if (level < floor) continue
34
+ // parabolic freq refinement (dB domain)
35
+ let la = Math.log(mag[k - 1] + 1e-20), lb = Math.log(mag[k] + 1e-20), lc = Math.log(mag[k + 1] + 1e-20)
36
+ let off = 0.5 * (la - lc) / (la - 2 * lb + lc || 1e-12)
37
+ if (!(off > -1 && off < 1)) off = 0
38
+ let kc = k + off
39
+ // PNPR: peak vs mean power a few bins away (±4..8) — feedback is far narrower than music
40
+ let nb = (mag[k - 4] + mag[k - 5] + mag[k - 6] + mag[k + 4] + mag[k + 5] + mag[k + 6]) / 6
41
+ let pnpr = db(mag[k]) - db(nb)
42
+ // PHPR: peak vs its own 2nd/3rd harmonics — a bare howl has none
43
+ let phpr = db(mag[k]) - db(Math.max(2 * kc <= half ? at(2 * kc) : 0, 3 * kc <= half ? at(3 * kc) : 0))
44
+ out.push({ freq: kc * fs / n, level, pnpr, phpr, kc })
45
+ }
46
+ // relational harmonic gate: a peak that sits within 1% of an integer multiple of a
47
+ // strong lower peak is a musical overtone, not feedback (parabolic freqs ≈ 0.1% accurate)
48
+ let fundamentals = out.filter(c => c.level > -45).sort((a, b) => a.kc - b.kc).slice(0, 8)
49
+ out = out.filter(c => {
50
+ for (let f of fundamentals) {
51
+ if (f.kc >= c.kc * 0.75) break
52
+ let m = Math.round(c.kc / f.kc)
53
+ if (m >= 2 && Math.abs(c.kc - m * f.kc) / (m * f.kc) < 0.01) return false
54
+ }
55
+ return true
56
+ })
57
+ out.sort((a, b) => b.level - a.level)
58
+ return out.slice(0, maxCandidates)
59
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@audio/defeedback-analyzer",
3
+ "version": "1.0.0",
4
+ "description": "Feedback candidate detection — spectral peaks with PNPR/PHPR howl criteria (Waterschoot & Moonen 2011)",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "analyzer.js",
8
+ "exports": {
9
+ ".": "./analyzer.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "analyzer.js"
14
+ ],
15
+ "dependencies": {
16
+ "fourier-transform": "^2.2.0"
17
+ },
18
+ "keywords": [
19
+ "audio",
20
+ "dsp",
21
+ "feedback",
22
+ "howling",
23
+ "analyzer"
24
+ ],
25
+ "license": "MIT",
26
+ "author": "Dmitry Iv. <dfcreative@gmail.com>",
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ }
33
+ }