@audio/measure-ir 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/ir.js +63 -0
  2. package/package.json +32 -0
package/ir.js ADDED
@@ -0,0 +1,63 @@
1
+ // Impulse-response capture via exponential sine sweep deconvolution (Farina, AES 2000).
2
+ // Inverse filter = time-reversed sweep with exp(−t/L) amplitude compensation; convolving
3
+ // the recording with it collapses the sweep to an impulse at t=0 while harmonic
4
+ // distortion products land at negative time and are trimmed away — the ESS advantage.
5
+ // Calibrated by self-deconvolution of the sweep, so an identity system yields δ = 1.0.
6
+
7
+ import { fft, ifft } from 'fourier-transform'
8
+
9
+ export function convFFT (a, b) {
10
+ let n = a.length + b.length - 1
11
+ let N = 1
12
+ while (N < n) N <<= 1
13
+ let A = new Float64Array(N); A.set(a)
14
+ let B = new Float64Array(N); B.set(b)
15
+ let fa = fft(A)
16
+ let ar = Float64Array.from(fa[0]), ai = Float64Array.from(fa[1]) // fft reuses one scratch buffer — copy before the next call
17
+ let [br, bi] = fft(B)
18
+ let re = new Float64Array(N / 2 + 1), im = new Float64Array(N / 2 + 1)
19
+ for (let k = 0; k <= N / 2; k++) {
20
+ re[k] = ar[k] * br[k] - ai[k] * bi[k]
21
+ im[k] = ar[k] * bi[k] + ai[k] * br[k]
22
+ }
23
+ return Float64Array.from(ifft(re, im)) // copy — ifft may also reuse scratch
24
+ }
25
+
26
+ /** Farina inverse filter for an exponential sweep */
27
+ export function inverseSweep (sweep, { f0 = 20, f1, fs = 44100 } = {}) {
28
+ let n = sweep.length
29
+ f1 ||= fs / 2 * 0.95
30
+ let L = (n / fs) / Math.log(f1 / f0)
31
+ let inv = new Float64Array(n)
32
+ for (let i = 0; i < n; i++) {
33
+ // envelope decays over the inverse filter's own time axis: HF (first) at unity,
34
+ // LF (last) at f0/f1 — compensates the sweep's 1/f energy density to a flat pulse
35
+ inv[i] = sweep[n - 1 - i] * Math.exp(-(i / fs) / L)
36
+ }
37
+ return inv
38
+ }
39
+
40
+ /**
41
+ * @param {Float32Array} recorded — system output for the played sweep
42
+ * @param {object} opts — { sweep: Float32Array (the played signal), f0=20, f1, fs=44100,
43
+ * length (samples of IR to return, default recorded−sweep+1) }
44
+ * @returns {Float32Array} impulse response, direct path at index 0, calibrated to unit gain
45
+ */
46
+ export default function ir (recorded, { sweep, f0 = 20, f1, fs = 44100, length } = {}) {
47
+ if (!sweep) throw new RangeError('ir: opts.sweep (the played sweep) is required')
48
+ let inv = inverseSweep(sweep, { f0, f1, fs })
49
+
50
+ let ref = convFFT(sweep, inv)
51
+ let peakIdx = 0, peak = 0
52
+ for (let i = 0; i < ref.length; i++) {
53
+ let a = Math.abs(ref[i])
54
+ if (a > peak) { peak = a; peakIdx = i }
55
+ }
56
+ let scale = peak > 0 ? 1 / ref[peakIdx] : 1
57
+
58
+ let raw = convFFT(recorded, inv)
59
+ let n = length ?? Math.max(1, recorded.length - sweep.length + 1)
60
+ let out = new Float32Array(n)
61
+ for (let i = 0; i < n && peakIdx + i < raw.length; i++) out[i] = raw[peakIdx + i] * scale
62
+ return out
63
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@audio/measure-ir",
3
+ "version": "1.0.0",
4
+ "description": "Impulse-response capture — exponential sine sweep deconvolution (Farina 2000), self-calibrated",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "ir.js",
8
+ "exports": {
9
+ ".": "./ir.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "ir.js"
14
+ ],
15
+ "dependencies": {
16
+ "fourier-transform": "^2.2.0"
17
+ },
18
+ "keywords": [
19
+ "audio",
20
+ "dsp",
21
+ "measurement",
22
+ "ir"
23
+ ],
24
+ "license": "MIT",
25
+ "author": "Dmitry Iv. <dfcreative@gmail.com>",
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }