@marmooo/midy 0.4.9 → 0.5.1

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.
@@ -0,0 +1,58 @@
1
+ export function createConvolutionReverbImpulse(audioContext: any, decay: any, preDecay: any): any;
2
+ export function createConvolutionReverb(audioContext: any, impulse: any): {
3
+ input: any;
4
+ output: any;
5
+ };
6
+ export function createCombFilter(audioContext: any, input: any, delay: any, feedback: any): any;
7
+ export function createAllpassFilter(audioContext: any, input: any, delay: any, feedback: any): any;
8
+ export function createLPFCombFilter(audioContext: any, input: any, delayTime: any, feedback: any, damping: any): any;
9
+ export function createSchroederReverb(audioContext: any, combFeedbacks: any, combDelays: any, allpassFeedbacks: any, allpassDelays: any): {
10
+ input: any;
11
+ output: any;
12
+ };
13
+ export function createMoorerReverb(audioContext: any, earlyTaps: any, earlyGains: any, combDelays: any, combFeedbacks: any, damping: any, allpassDelays: any, allpassFeedbacks: any): {
14
+ input: any;
15
+ output: any;
16
+ };
17
+ export function createMoorerReverbDefault(audioContext: any, { rt60, damping, }?: {
18
+ rt60?: number | undefined;
19
+ damping?: number | undefined;
20
+ }): {
21
+ input: any;
22
+ output: any;
23
+ };
24
+ export function createFDN(audioContext: any, delayTimes: any, gains: any, damping?: number, modulation?: number): {
25
+ input: any;
26
+ output: any;
27
+ };
28
+ export function createFDNDefault(audioContext: any, { rt60, damping, modulation }?: {
29
+ rt60?: number | undefined;
30
+ damping?: number | undefined;
31
+ modulation?: number | undefined;
32
+ }): {
33
+ input: any;
34
+ output: any;
35
+ };
36
+ export function createDattorroReverb(audioContext: any, { decay, damping, bandwidth, }?: {
37
+ decay?: number | undefined;
38
+ damping?: number | undefined;
39
+ bandwidth?: number | undefined;
40
+ }): {
41
+ input: any;
42
+ output: any;
43
+ };
44
+ export function createFreeverb(audioContext: any, { roomSize, damping }?: {
45
+ roomSize?: number | undefined;
46
+ damping?: number | undefined;
47
+ }): {
48
+ inputL: any;
49
+ inputR: any;
50
+ outputL: any;
51
+ outputR: any;
52
+ };
53
+ export function createVelvetNoiseImpulse(audioContext: any, decay: any, density?: number): any;
54
+ export function createVelvetNoiseReverb(audioContext: any, decay: any, density: any): {
55
+ input: any;
56
+ output: any;
57
+ };
58
+ //# sourceMappingURL=reverb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reverb.d.ts","sourceRoot":"","sources":["../src/reverb.js"],"names":[],"mappings":"AAWA,kGAiBC;AAED;;;EAGC;AAED,gGAUC;AAMD,mGAYC;AAKD,qHAuBC;AAOD;;;EA8BC;AAWD;;;EAmDC;AAGD;;;;;;EA0BC;AAcD;;;EA+EC;AAGD;;;;;;;EAWC;AAcD;;;;;;;EAoFC;AAoBD;;;;;;;;EA2CC;AAOD,+FAkBC;AAED;;;EAGC"}
@@ -0,0 +1,405 @@
1
+ "use strict";
2
+ // Reverb algorithms for Web Audio API
3
+ // - Convolution Reverb
4
+ // - Schroeder (1962)
5
+ // - Moorer (1979)
6
+ // - FDN (1992)
7
+ // - Dattorro (1997)
8
+ // - Freeverb (1999)
9
+ // - Velvet Noise Reverb (2012)
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.createConvolutionReverbImpulse = createConvolutionReverbImpulse;
12
+ exports.createConvolutionReverb = createConvolutionReverb;
13
+ exports.createCombFilter = createCombFilter;
14
+ exports.createAllpassFilter = createAllpassFilter;
15
+ exports.createLPFCombFilter = createLPFCombFilter;
16
+ exports.createSchroederReverb = createSchroederReverb;
17
+ exports.createMoorerReverb = createMoorerReverb;
18
+ exports.createMoorerReverbDefault = createMoorerReverbDefault;
19
+ exports.createFDN = createFDN;
20
+ exports.createFDNDefault = createFDNDefault;
21
+ exports.createDattorroReverb = createDattorroReverb;
22
+ exports.createFreeverb = createFreeverb;
23
+ exports.createVelvetNoiseImpulse = createVelvetNoiseImpulse;
24
+ exports.createVelvetNoiseReverb = createVelvetNoiseReverb;
25
+ // Convolution Reverb
26
+ function createConvolutionReverbImpulse(audioContext, decay, preDecay) {
27
+ const sampleRate = audioContext.sampleRate;
28
+ const length = sampleRate * decay;
29
+ const impulse = new AudioBuffer({ numberOfChannels: 2, length, sampleRate });
30
+ const preDecayLength = Math.min(sampleRate * preDecay, length);
31
+ for (let channel = 0; channel < impulse.numberOfChannels; channel++) {
32
+ const channelData = impulse.getChannelData(channel);
33
+ for (let i = 0; i < preDecayLength; i++) {
34
+ channelData[i] = Math.random() * 2 - 1;
35
+ }
36
+ const attenuationFactor = 1 / (sampleRate * decay);
37
+ for (let i = preDecayLength; i < length; i++) {
38
+ const attenuation = Math.exp(-(i - preDecayLength) * attenuationFactor);
39
+ channelData[i] = (Math.random() * 2 - 1) * attenuation;
40
+ }
41
+ }
42
+ return impulse;
43
+ }
44
+ function createConvolutionReverb(audioContext, impulse) {
45
+ const convolverNode = new ConvolverNode(audioContext, { buffer: impulse });
46
+ return { input: convolverNode, output: convolverNode };
47
+ }
48
+ function createCombFilter(audioContext, input, delay, feedback) {
49
+ const delayNode = new DelayNode(audioContext, {
50
+ maxDelayTime: delay,
51
+ delayTime: delay,
52
+ });
53
+ const feedbackGain = new GainNode(audioContext, { gain: feedback });
54
+ input.connect(delayNode);
55
+ delayNode.connect(feedbackGain);
56
+ feedbackGain.connect(delayNode);
57
+ return delayNode;
58
+ }
59
+ // Schroeder allpass approximation for Web Audio API.
60
+ // Exact H(z) = (-g + z^-d) / (1 - g·z^-d) requires a feedforward path
61
+ // that causes zero-delay loops in the Web Audio API graph, which is unstable.
62
+ // This approximation omits the feedforward term and returns only the delay output.
63
+ function createAllpassFilter(audioContext, input, delay, feedback) {
64
+ const delayNode = new DelayNode(audioContext, {
65
+ maxDelayTime: delay,
66
+ delayTime: delay,
67
+ });
68
+ const feedbackGain = new GainNode(audioContext, { gain: feedback });
69
+ const passGain = new GainNode(audioContext, { gain: 1 - feedback });
70
+ input.connect(delayNode);
71
+ delayNode.connect(feedbackGain);
72
+ feedbackGain.connect(delayNode);
73
+ delayNode.connect(passGain);
74
+ return passGain;
75
+ }
76
+ // LPF comb filter (Freeverb / Moorer):
77
+ // feedback loop contains a one-pole lowpass to simulate air absorption.
78
+ // damping=0: bright tail, damping=1: dark tail
79
+ function createLPFCombFilter(audioContext, input, delayTime, feedback, damping) {
80
+ const delayNode = new DelayNode(audioContext, {
81
+ maxDelayTime: delayTime,
82
+ delayTime,
83
+ });
84
+ const feedbackGain = new GainNode(audioContext, { gain: feedback });
85
+ const damp = Math.max(0, Math.min(1, damping));
86
+ // y[n] = (1-d)*x[n] + d*y[n-1]
87
+ const lpf = new IIRFilterNode(audioContext, {
88
+ feedforward: [1 - damp],
89
+ feedback: [1, -damp],
90
+ });
91
+ input.connect(delayNode);
92
+ delayNode.connect(lpf);
93
+ lpf.connect(feedbackGain);
94
+ feedbackGain.connect(delayNode);
95
+ return delayNode;
96
+ }
97
+ // Schroeder Reverb (1962)
98
+ // https://hajim.rochester.edu/ece/sites/zduan/teaching/ece472/reading/Schroeder_1962.pdf
99
+ // M.R.Schroeder, "Natural Sounding Artificial Reverberation",
100
+ // J. Audio Eng. Soc., vol.10, p.219, 1962
101
+ function createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays) {
102
+ const input = new GainNode(audioContext);
103
+ const mergerGain = new GainNode(audioContext);
104
+ for (let i = 0; i < combDelays.length; i++) {
105
+ const comb = createCombFilter(audioContext, input, combDelays[i], combFeedbacks[i]);
106
+ comb.connect(mergerGain);
107
+ }
108
+ const allpasses = [];
109
+ for (let i = 0; i < allpassDelays.length; i++) {
110
+ const src = i === 0 ? mergerGain : allpasses.at(-1);
111
+ const allpass = createAllpassFilter(audioContext, src, allpassDelays[i], allpassFeedbacks[i]);
112
+ allpasses.push(allpass);
113
+ }
114
+ return { input, output: allpasses.at(-1) };
115
+ }
116
+ // Moorer Reverb (1979)
117
+ // http://articles.ircam.fr/textes/Moorer78b/
118
+ // J.A.Moorer, "About this Reverberation Business",
119
+ // Computer Music Journal, vol.3, no.2, 1979
120
+ //
121
+ // Adds two things over Schroeder:
122
+ // 1. Early reflections as a tapped delay line (FIR)
123
+ // 2. LPF-comb filters instead of plain combs
124
+ function createMoorerReverb(audioContext, earlyTaps, earlyGains, combDelays, combFeedbacks, damping, allpassDelays, allpassFeedbacks) {
125
+ const input = new GainNode(audioContext);
126
+ const earlySum = new GainNode(audioContext);
127
+ for (let i = 0; i < earlyTaps.length; i++) {
128
+ const tapDelay = new DelayNode(audioContext, {
129
+ maxDelayTime: earlyTaps[i],
130
+ delayTime: earlyTaps[i],
131
+ });
132
+ const tapGain = new GainNode(audioContext, { gain: earlyGains[i] });
133
+ input.connect(tapDelay);
134
+ tapDelay.connect(tapGain);
135
+ tapGain.connect(earlySum);
136
+ }
137
+ // Late reverberation: LPF-comb filters
138
+ const lateSum = new GainNode(audioContext);
139
+ for (let i = 0; i < combDelays.length; i++) {
140
+ const comb = createLPFCombFilter(audioContext, earlySum, combDelays[i], combFeedbacks[i], damping);
141
+ comb.connect(lateSum);
142
+ }
143
+ // Allpass diffusers
144
+ const allpasses = [];
145
+ for (let i = 0; i < allpassDelays.length; i++) {
146
+ const src = i === 0 ? lateSum : allpasses.at(-1);
147
+ const allpass = createAllpassFilter(audioContext, src, allpassDelays[i], allpassFeedbacks[i]);
148
+ allpasses.push(allpass);
149
+ }
150
+ // Mix early + late to output
151
+ const output = new GainNode(audioContext);
152
+ earlySum.connect(output);
153
+ allpasses.at(-1).connect(output);
154
+ return { input, output };
155
+ }
156
+ // Sensible defaults for Moorer at 44100 Hz
157
+ function createMoorerReverbDefault(audioContext, { rt60 = 2.0, damping = 0.3, } = {}) {
158
+ const sr = audioContext.sampleRate;
159
+ // Early reflection taps (ms -> seconds), gains chosen for natural sounding
160
+ const earlyTaps = [0.0043, 0.0215, 0.0225, 0.0268, 0.0270, 0.0298, 0.0458];
161
+ const earlyGains = [0.841, 0.504, 0.491, 0.379, 0.380, 0.346, 0.289];
162
+ // RT60 -> comb feedback: g = 10^(-3 * delay / rt60)
163
+ const combSamples = [1309, 1635, 1811, 1926, 2053, 2667];
164
+ const combDelays = combSamples.map((s) => s / sr);
165
+ const combFeedbacks = combDelays.map((d) => Math.pow(10, -3 * d / rt60));
166
+ const allpassDelays = [0.005, 0.0017];
167
+ const allpassFeedbacks = [0.7, 0.7];
168
+ return createMoorerReverb(audioContext, earlyTaps, earlyGains, combDelays, combFeedbacks, damping, allpassDelays, allpassFeedbacks);
169
+ }
170
+ // FDN - Feedback Delay Network (1992)
171
+ // https://ccrma.stanford.edu/~jos/Reverb/Reverb.pdf
172
+ // J.-M.Jot, A.Chaigne, "Digital Delay Networks for Designing
173
+ // Artificial Reverberators", AES 90th Convention, 1991
174
+ //
175
+ // N delay lines connected via a unitary feedback matrix.
176
+ // Using the normalized 4×4 Hadamard matrix for energy preservation.
177
+ //
178
+ // modulation: each delay line is driven by a low-frequency oscillator at a
179
+ // slightly different frequency and phase, breaking up the periodic ringing
180
+ // that fixed delay lengths produce. Set to 0 to disable.
181
+ function createFDN(audioContext, delayTimes, gains, damping = 0.2, modulation = 0.0005) {
182
+ const N = delayTimes.length;
183
+ // Normalized 4×4 Hadamard matrix (only N=4 supported here)
184
+ // H4 = (1/2) * [[1,1,1,1],[1,-1,1,-1],[1,1,-1,-1],[1,-1,-1,1]]
185
+ // Any unitary matrix works; Hadamard is convenient and mixes all lines equally.
186
+ if (N !== 4) {
187
+ throw new Error("createFDN: only N=4 is supported (4x4 Hadamard)");
188
+ }
189
+ const H = [
190
+ [0.5, 0.5, 0.5, 0.5],
191
+ [0.5, -0.5, 0.5, -0.5],
192
+ [0.5, 0.5, -0.5, -0.5],
193
+ [0.5, -0.5, -0.5, 0.5],
194
+ ];
195
+ const input = new GainNode(audioContext);
196
+ const output = new GainNode(audioContext);
197
+ // Create delay lines with headroom for modulation depth
198
+ const delays = delayTimes.map((t) => new DelayNode(audioContext, {
199
+ maxDelayTime: t + modulation,
200
+ delayTime: t,
201
+ }));
202
+ // Per-line LPF for damping (air absorption)
203
+ const lpfs = delays.map(() => {
204
+ const damp = Math.max(0, Math.min(1, damping));
205
+ return new IIRFilterNode(audioContext, {
206
+ feedforward: [1 - damp],
207
+ feedback: [1, -damp],
208
+ });
209
+ });
210
+ // Per-line attenuation gains (RT60 control)
211
+ const attenuations = gains.map((g) => new GainNode(audioContext, { gain: g }));
212
+ // Delay modulation: slightly different LFO per line to avoid coherent artifacts
213
+ if (modulation > 0) {
214
+ delays.forEach((delayNode, i) => {
215
+ const osc = new OscillatorNode(audioContext, {
216
+ frequency: 0.3 + i * 0.07, // 0.30, 0.37, 0.44, 0.51 Hz
217
+ });
218
+ const oscGain = new GainNode(audioContext, { gain: modulation });
219
+ osc.connect(oscGain);
220
+ oscGain.connect(delayNode.delayTime);
221
+ osc.start();
222
+ });
223
+ }
224
+ // Input injection: feed input into all delay lines equally
225
+ const inputScale = new GainNode(audioContext, { gain: 1 / N });
226
+ input.connect(inputScale);
227
+ delays.forEach((d) => inputScale.connect(d));
228
+ // Feedback matrix: for each output delay line j,
229
+ // sum over all input lines i: H[j][i] * attenuation[i] * lpf[i]
230
+ // Signal flow per line i:
231
+ // delay[i] -> lpf[i] -> attenuation[i] -> (distributed via H to all delay[j] inputs)
232
+ // We implement H as N×N individual GainNodes (N^2 = 16 for N=4).
233
+ for (let i = 0; i < N; i++) {
234
+ delays[i].connect(lpfs[i]);
235
+ lpfs[i].connect(attenuations[i]);
236
+ }
237
+ for (let j = 0; j < N; j++) {
238
+ for (let i = 0; i < N; i++) {
239
+ if (H[j][i] === 0)
240
+ continue;
241
+ const matrixGain = new GainNode(audioContext, { gain: H[j][i] });
242
+ attenuations[i].connect(matrixGain);
243
+ matrixGain.connect(delays[j]);
244
+ }
245
+ delays[j].connect(output);
246
+ }
247
+ return { input, output };
248
+ }
249
+ // Sensible defaults for FDN
250
+ function createFDNDefault(audioContext, { rt60 = 2.0, damping = 0.2, modulation = 0.0005 } = {}) {
251
+ const sr = audioContext.sampleRate;
252
+ // Mutually prime delay lengths (samples) — avoids periodicity artifacts
253
+ const delaySamples = [1049, 1327, 1601, 1873];
254
+ const delayTimes = delaySamples.map((s) => s / sr);
255
+ // Attenuation from RT60: g = 10^(-3 * delayTime / rt60)
256
+ const gains = delayTimes.map((d) => Math.pow(10, -3 * d / rt60));
257
+ return createFDN(audioContext, delayTimes, gains, damping, modulation);
258
+ }
259
+ // Dattorro Reverb (1997)
260
+ // https://ccrma.stanford.edu/~dattorro/EffectDesignPart1.pdf
261
+ // J.Dattorro, "Effect Design Part 1: Reverberator and Other Filters",
262
+ // J. Audio Eng. Soc., vol.45, no.9, 1997
263
+ //
264
+ // Figure-of-eight allpass loop with pre-diffusion stage.
265
+ // Topology:
266
+ // input -> pre-LPF -> 4×allpass (pre-diffusion)
267
+ // -> split into two "tank" loops (left / right)
268
+ // each loop: allpass -> delay1 -> LPF -> delay2 -> decayGain -> cross-feed
269
+ // output tapped at multiple points from both loops
270
+ function createDattorroReverb(audioContext, { decay = 0.7, damping = 0.0005, bandwidth = 0.9995, } = {}) {
271
+ const sr = audioContext.sampleRate;
272
+ // Pre-filter (bandwidth)
273
+ // One-pole LPF on input: y[n] = (1-bw)*x[n] + bw*y[n-1]
274
+ const bw = Math.max(0, Math.min(1, bandwidth));
275
+ const preLPF = new IIRFilterNode(audioContext, {
276
+ feedforward: [1 - bw],
277
+ feedback: [1, -bw],
278
+ });
279
+ // Pre-diffusion: 4 allpass filters in series
280
+ // Delay lengths from Dattorro Table 1 (normalized to 29761 Hz, rescaled)
281
+ const scale = sr / 29761;
282
+ const preDiffSamples = [142, 107, 379, 277];
283
+ const preDiffFeedbacks = [0.75, 0.75, 0.625, 0.625];
284
+ const input = new GainNode(audioContext);
285
+ input.connect(preLPF);
286
+ const preDiffs = [];
287
+ for (let i = 0; i < preDiffSamples.length; i++) {
288
+ const src = i === 0 ? preLPF : preDiffs.at(-1);
289
+ const allpass = createAllpassFilter(audioContext, src, (preDiffSamples[i] * scale) / sr, preDiffFeedbacks[i]);
290
+ preDiffs.push(allpass);
291
+ }
292
+ const preDiffOut = preDiffs.at(-1);
293
+ // Tank: two cross-coupled loops
294
+ // Each tank: allpass(modulated) -> delay1 -> LPF -> allpass -> delay2 -> decayGain -> cross-feed
295
+ // Sample counts from Dattorro Table 1
296
+ const tankAllpassSamples = [672, 908];
297
+ const tankAllpassFeedbacks = [0.5, 0.5];
298
+ const tankDelay1Samples = [4453, 4217];
299
+ const tankDelay2Samples = [3720, 3163];
300
+ const damp = Math.max(0, Math.min(1, damping));
301
+ const loopInput = [new GainNode(audioContext), new GainNode(audioContext)];
302
+ preDiffOut.connect(loopInput[0]);
303
+ preDiffOut.connect(loopInput[1]);
304
+ const loopOutput = [];
305
+ for (let t = 0; t < 2; t++) {
306
+ // allpass -> delay1 -> LPF -> allpass -> delay2 -> decayGain
307
+ const allpass1 = createAllpassFilter(audioContext, loopInput[t], (tankAllpassSamples[t] * scale) / sr, tankAllpassFeedbacks[t]);
308
+ const delay1 = new DelayNode(audioContext, {
309
+ maxDelayTime: (tankDelay1Samples[t] * scale) / sr,
310
+ delayTime: (tankDelay1Samples[t] * scale) / sr,
311
+ });
312
+ const tankLPF = new IIRFilterNode(audioContext, {
313
+ feedforward: [1 - damp],
314
+ feedback: [1, -damp],
315
+ });
316
+ const delay2 = new DelayNode(audioContext, {
317
+ maxDelayTime: (tankDelay2Samples[t] * scale) / sr,
318
+ delayTime: (tankDelay2Samples[t] * scale) / sr,
319
+ });
320
+ const decayGain = new GainNode(audioContext, { gain: decay });
321
+ allpass1.connect(delay1);
322
+ delay1.connect(tankLPF);
323
+ tankLPF.connect(delay2);
324
+ delay2.connect(decayGain);
325
+ loopOutput.push(decayGain);
326
+ }
327
+ // Cross-feed: decayGain of each tank feeds the other tank's loopInput
328
+ loopOutput[0].connect(loopInput[1]);
329
+ loopOutput[1].connect(loopInput[0]);
330
+ // Output: mix both tanks
331
+ const output = new GainNode(audioContext, { gain: 0.5 });
332
+ loopOutput[0].connect(output);
333
+ loopOutput[1].connect(output);
334
+ return { input, output };
335
+ }
336
+ // Freeverb (1999)
337
+ // https://github.com/sinshu/freeverb
338
+ // Jezar at Dreampoint, 1999
339
+ const FREEVERB_COMB_SAMPLES_L = [
340
+ 1116,
341
+ 1188,
342
+ 1277,
343
+ 1356,
344
+ 1422,
345
+ 1491,
346
+ 1557,
347
+ 1617,
348
+ ];
349
+ const FREEVERB_STEREO_SPREAD = 23; // samples
350
+ const FREEVERB_ALLPASS_SAMPLES = [225, 341, 441, 556];
351
+ const FREEVERB_ALLPASS_FEEDBACK = 0.5;
352
+ function createFreeverb(audioContext, { roomSize = 0.84, damping = 0.2 } = {}) {
353
+ const sr = audioContext.sampleRate;
354
+ const feedback = roomSize * 0.28 + 0.7; // maps [0,1] -> [0.7, 0.98]
355
+ const buildChannel = (sampleOffsetPerComb) => {
356
+ const inputGain = new GainNode(audioContext);
357
+ const sumGain = new GainNode(audioContext);
358
+ for (const samples of FREEVERB_COMB_SAMPLES_L) {
359
+ const delayTime = (samples + sampleOffsetPerComb) / sr;
360
+ const comb = createLPFCombFilter(audioContext, inputGain, delayTime, feedback, damping);
361
+ comb.connect(sumGain);
362
+ }
363
+ const allpasses = [];
364
+ for (let i = 0; i < FREEVERB_ALLPASS_SAMPLES.length; i++) {
365
+ const src = i === 0 ? sumGain : allpasses.at(-1);
366
+ const allpass = createAllpassFilter(audioContext, src, FREEVERB_ALLPASS_SAMPLES[i] / sr, FREEVERB_ALLPASS_FEEDBACK);
367
+ allpasses.push(allpass);
368
+ }
369
+ return { input: inputGain, output: allpasses.at(-1) };
370
+ };
371
+ const L = buildChannel(0);
372
+ const R = buildChannel(FREEVERB_STEREO_SPREAD);
373
+ return {
374
+ inputL: L.input,
375
+ inputR: R.input,
376
+ outputL: L.output,
377
+ outputR: R.output,
378
+ };
379
+ }
380
+ // Velvet Noise Reverb (2012)
381
+ // https://aaltodoc.aalto.fi/server/api/core/bitstreams/97ed04a8-cb88-461f-b1a3-e72da5129256/content
382
+ // V.Välimäki et al., "Fifty Years of Artificial Reverberation",
383
+ // IEEE Trans. Audio Speech Lang. Process., vol.20, no.5, 2012
384
+ function createVelvetNoiseImpulse(audioContext, decay, density = 2000) {
385
+ const sampleRate = audioContext.sampleRate;
386
+ const length = Math.ceil(sampleRate * decay);
387
+ const impulse = new AudioBuffer({ numberOfChannels: 2, length, sampleRate });
388
+ const interval = Math.max(1, Math.round(sampleRate / density));
389
+ for (let ch = 0; ch < 2; ch++) {
390
+ const data = impulse.getChannelData(ch);
391
+ for (let i = 0; i < length; i += interval) {
392
+ // Randomize position within the interval (velvet noise definition)
393
+ const idx = i + Math.floor(Math.random() * interval);
394
+ if (idx < length) {
395
+ const env = Math.exp(-idx / (sampleRate * decay * 0.3));
396
+ data[idx] = (Math.random() > 0.5 ? 1 : -1) * env;
397
+ }
398
+ }
399
+ }
400
+ return impulse;
401
+ }
402
+ function createVelvetNoiseReverb(audioContext, decay, density) {
403
+ const impulse = createVelvetNoiseImpulse(audioContext, decay, density);
404
+ return createConvolutionReverb(audioContext, impulse);
405
+ }