@audio/reverb-convolution 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.
- package/convolution.js +82 -0
- package/package.json +32 -0
package/convolution.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Convolution reverb — convolve with an impulse response (room, plate, spring, cabinet…).
|
|
2
|
+
// Small IRs run direct-form; long IRs run uniform-partitioned FFT overlap-add with a
|
|
3
|
+
// frequency-domain delay line (Wefers/Gardner class). Both paths are differential-tested
|
|
4
|
+
// against each other. Wet tail truncated at the input length.
|
|
5
|
+
|
|
6
|
+
import { fft, ifft } from 'fourier-transform'
|
|
7
|
+
|
|
8
|
+
const PART = 2048 // partition size; FFT size = 2·PART
|
|
9
|
+
const DIRECT_MAX = 1024 // direct-form below this IR length
|
|
10
|
+
|
|
11
|
+
function direct (data, h) {
|
|
12
|
+
let out = new Float64Array(data.length)
|
|
13
|
+
for (let j = 0; j < h.length; j++) {
|
|
14
|
+
let k = h[j]
|
|
15
|
+
if (k === 0) continue
|
|
16
|
+
for (let i = j; i < data.length; i++) out[i] += k * data[i - j]
|
|
17
|
+
}
|
|
18
|
+
return out
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function partitioned (data, h) {
|
|
22
|
+
let nParts = Math.ceil(h.length / PART)
|
|
23
|
+
let F = 2 * PART
|
|
24
|
+
// IR partition spectra
|
|
25
|
+
let H = []
|
|
26
|
+
let seg = new Float64Array(F)
|
|
27
|
+
for (let p = 0; p < nParts; p++) {
|
|
28
|
+
seg.fill(0)
|
|
29
|
+
for (let i = 0; i < PART; i++) seg[i] = h[p * PART + i] || 0
|
|
30
|
+
let [re, im] = fft(seg)
|
|
31
|
+
H.push([Float64Array.from(re), Float64Array.from(im)])
|
|
32
|
+
}
|
|
33
|
+
// frequency-domain delay line of past input spectra
|
|
34
|
+
let fdl = Array.from({ length: nParts }, () => [new Float64Array(PART + 1), new Float64Array(PART + 1)])
|
|
35
|
+
let fdlPos = 0
|
|
36
|
+
let out = new Float64Array(data.length + F)
|
|
37
|
+
let yRe = new Float64Array(PART + 1), yIm = new Float64Array(PART + 1)
|
|
38
|
+
let block = new Float64Array(F)
|
|
39
|
+
|
|
40
|
+
for (let pos = 0; pos < data.length; pos += PART) {
|
|
41
|
+
block.fill(0)
|
|
42
|
+
for (let i = 0; i < PART && pos + i < data.length; i++) block[i] = data[pos + i]
|
|
43
|
+
let [re, im] = fft(block)
|
|
44
|
+
fdl[fdlPos][0].set(re.subarray(0, PART + 1))
|
|
45
|
+
fdl[fdlPos][1].set(im.subarray(0, PART + 1))
|
|
46
|
+
|
|
47
|
+
yRe.fill(0); yIm.fill(0)
|
|
48
|
+
for (let p = 0; p < nParts; p++) {
|
|
49
|
+
let [xr, xi] = fdl[(fdlPos - p + nParts) % nParts]
|
|
50
|
+
let [hr, hi] = H[p]
|
|
51
|
+
for (let k = 0; k <= PART; k++) {
|
|
52
|
+
yRe[k] += xr[k] * hr[k] - xi[k] * hi[k]
|
|
53
|
+
yIm[k] += xr[k] * hi[k] + xi[k] * hr[k]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
let y = ifft(yRe, yIm)
|
|
57
|
+
for (let i = 0; i < F && pos + i < out.length; i++) out[pos + i] += y[i]
|
|
58
|
+
fdlPos = (fdlPos + 1) % nParts
|
|
59
|
+
}
|
|
60
|
+
return out.subarray(0, data.length)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {Float32Array} data — mono PCM, processed in place
|
|
65
|
+
* @param {object} opts — { ir: Float32Array, mix=1, normalize=false,
|
|
66
|
+
* method: 'auto'|'direct'|'fft' }
|
|
67
|
+
* normalize scales the IR to unit energy so output level ≈ input level
|
|
68
|
+
*/
|
|
69
|
+
export default function convolve (data, { ir, mix = 1, normalize = false, method = 'auto' } = {}) {
|
|
70
|
+
if (!ir || !ir.length) throw new RangeError('convolve: opts.ir required')
|
|
71
|
+
let h = ir
|
|
72
|
+
if (normalize) {
|
|
73
|
+
let e = 0
|
|
74
|
+
for (let j = 0; j < ir.length; j++) e += ir[j] * ir[j]
|
|
75
|
+
let g = e > 0 ? 1 / Math.sqrt(e) : 1
|
|
76
|
+
h = Float32Array.from(ir, v => v * g)
|
|
77
|
+
}
|
|
78
|
+
let useFft = method === 'fft' || (method === 'auto' && h.length > DIRECT_MAX)
|
|
79
|
+
let wet = useFft ? partitioned(data, h) : direct(data, h)
|
|
80
|
+
for (let i = 0; i < data.length; i++) data[i] = data[i] * (1 - mix) + wet[i] * mix
|
|
81
|
+
return data
|
|
82
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/reverb-convolution",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Convolution reverb — impulse-response convolution (rooms, plates, cabinets)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "convolution.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./convolution.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"convolution.js"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"audio",
|
|
17
|
+
"dsp",
|
|
18
|
+
"reverb",
|
|
19
|
+
"convolution"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"fourier-transform": "^2.2.0"
|
|
31
|
+
}
|
|
32
|
+
}
|