@blibliki/engine 0.5.2 → 0.9.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/README.md +22 -2
- package/dist/index.d.ts +501 -107
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/Engine.ts +46 -29
- package/src/core/index.ts +11 -2
- package/src/core/midi/BaseMidiDevice.ts +47 -0
- package/src/core/midi/ComputerKeyboardDevice.ts +2 -1
- package/src/core/midi/MidiDeviceManager.ts +125 -31
- package/src/core/midi/{MidiDevice.ts → MidiInputDevice.ts} +6 -30
- package/src/core/midi/MidiOutputDevice.ts +23 -0
- package/src/core/midi/adapters/NodeMidiAdapter.ts +99 -13
- package/src/core/midi/adapters/WebMidiAdapter.ts +68 -10
- package/src/core/midi/adapters/types.ts +13 -4
- package/src/core/midi/controllers/BaseController.ts +14 -0
- package/src/core/module/Module.ts +121 -13
- package/src/core/module/PolyModule.ts +36 -0
- package/src/core/module/VoiceScheduler.ts +150 -10
- package/src/core/module/index.ts +9 -4
- package/src/index.ts +27 -3
- package/src/modules/Chorus.ts +222 -0
- package/src/modules/Constant.ts +2 -2
- package/src/modules/Delay.ts +347 -0
- package/src/modules/Distortion.ts +182 -0
- package/src/modules/Envelope.ts +158 -92
- package/src/modules/Filter.ts +7 -7
- package/src/modules/Gain.ts +2 -2
- package/src/modules/LFO.ts +287 -0
- package/src/modules/LegacyEnvelope.ts +146 -0
- package/src/modules/{MidiSelector.ts → MidiInput.ts} +26 -19
- package/src/modules/MidiMapper.ts +59 -4
- package/src/modules/MidiOutput.ts +121 -0
- package/src/modules/Noise.ts +259 -0
- package/src/modules/Oscillator.ts +9 -3
- package/src/modules/Reverb.ts +379 -0
- package/src/modules/Scale.ts +49 -4
- package/src/modules/StepSequencer.ts +410 -22
- package/src/modules/StereoPanner.ts +1 -1
- package/src/modules/index.ts +142 -29
- package/src/processors/custom-envelope-processor.ts +125 -0
- package/src/processors/index.ts +10 -0
- package/src/processors/lfo-processor.ts +123 -0
- package/src/processors/scale-processor.ts +42 -5
- package/src/utils/WetDryMixer.ts +123 -0
- package/src/utils/expandPatternSequence.ts +18 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { ContextTime } from "@blibliki/transport";
|
|
2
|
+
import { Context } from "@blibliki/utils";
|
|
3
|
+
import { AudioBufferSourceNode } from "@blibliki/utils/web-audio-api";
|
|
4
|
+
import { IModule, Module } from "@/core";
|
|
5
|
+
import { SetterHooks } from "@/core/module/Module";
|
|
6
|
+
import { EnumProp, ModulePropSchema } from "@/core/schema";
|
|
7
|
+
import { ICreateModule, ModuleType } from ".";
|
|
8
|
+
|
|
9
|
+
export type INoise = IModule<ModuleType.Noise>;
|
|
10
|
+
|
|
11
|
+
export enum NoiseType {
|
|
12
|
+
white = "white",
|
|
13
|
+
pink = "pink",
|
|
14
|
+
brown = "brown",
|
|
15
|
+
blue = "blue",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Props for the Noise module.
|
|
20
|
+
*
|
|
21
|
+
* @property type - Type of noise to generate.
|
|
22
|
+
* One of: "white", "pink", "brown", or "blue".
|
|
23
|
+
*/
|
|
24
|
+
export type INoiseProps = {
|
|
25
|
+
type: NoiseType;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const noisePropSchema: ModulePropSchema<
|
|
29
|
+
INoiseProps,
|
|
30
|
+
{
|
|
31
|
+
type: EnumProp<NoiseType>;
|
|
32
|
+
}
|
|
33
|
+
> = {
|
|
34
|
+
type: {
|
|
35
|
+
kind: "enum",
|
|
36
|
+
options: Object.values(NoiseType),
|
|
37
|
+
label: "Type",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const DEFAULT_PROPS: INoiseProps = {
|
|
42
|
+
type: NoiseType.white,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type NoiseSetterHooks = Pick<SetterHooks<INoiseProps>, "onAfterSetType">;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generates a buffer containing white noise.
|
|
49
|
+
*/
|
|
50
|
+
function generateWhiteNoise(
|
|
51
|
+
context: BaseAudioContext,
|
|
52
|
+
duration: number,
|
|
53
|
+
): AudioBuffer {
|
|
54
|
+
const sampleRate = context.sampleRate;
|
|
55
|
+
const length = sampleRate * duration;
|
|
56
|
+
const buffer = context.createBuffer(2, length, sampleRate);
|
|
57
|
+
|
|
58
|
+
for (let channel = 0; channel < 2; channel++) {
|
|
59
|
+
const data = buffer.getChannelData(channel);
|
|
60
|
+
for (let i = 0; i < length; i++) {
|
|
61
|
+
data[i] = Math.random() * 2 - 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return buffer;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generates a buffer containing pink noise using the Voss-McCartney algorithm.
|
|
70
|
+
* Pink noise has equal energy per octave (1/f spectrum).
|
|
71
|
+
*/
|
|
72
|
+
function generatePinkNoise(
|
|
73
|
+
context: BaseAudioContext,
|
|
74
|
+
duration: number,
|
|
75
|
+
): AudioBuffer {
|
|
76
|
+
const sampleRate = context.sampleRate;
|
|
77
|
+
const length = sampleRate * duration;
|
|
78
|
+
const buffer = context.createBuffer(2, length, sampleRate);
|
|
79
|
+
|
|
80
|
+
for (let channel = 0; channel < 2; channel++) {
|
|
81
|
+
const data = buffer.getChannelData(channel);
|
|
82
|
+
|
|
83
|
+
// Paul Kellet pink noise generator state
|
|
84
|
+
let b0 = 0,
|
|
85
|
+
b1 = 0,
|
|
86
|
+
b2 = 0,
|
|
87
|
+
b3 = 0,
|
|
88
|
+
b4 = 0,
|
|
89
|
+
b5 = 0,
|
|
90
|
+
b6 = 0;
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < length; i++) {
|
|
93
|
+
const white = Math.random() * 2 - 1;
|
|
94
|
+
|
|
95
|
+
b0 = 0.99886 * b0 + white * 0.0555179;
|
|
96
|
+
b1 = 0.99332 * b1 + white * 0.0750759;
|
|
97
|
+
b2 = 0.969 * b2 + white * 0.153852;
|
|
98
|
+
b3 = 0.8665 * b3 + white * 0.3104856;
|
|
99
|
+
b4 = 0.55 * b4 + white * 0.5329522;
|
|
100
|
+
b5 = -0.7616 * b5 - white * 0.016898;
|
|
101
|
+
|
|
102
|
+
const pink = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
|
|
103
|
+
b6 = white * 0.115926;
|
|
104
|
+
|
|
105
|
+
data[i] = pink * 0.11; // Scale to approximately -1 to 1
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return buffer;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generates a buffer containing brown noise (Brownian/red noise).
|
|
114
|
+
* Brown noise has a 1/f² spectrum with heavy low-end emphasis.
|
|
115
|
+
*/
|
|
116
|
+
function generateBrownNoise(
|
|
117
|
+
context: BaseAudioContext,
|
|
118
|
+
duration: number,
|
|
119
|
+
): AudioBuffer {
|
|
120
|
+
const sampleRate = context.sampleRate;
|
|
121
|
+
const length = sampleRate * duration;
|
|
122
|
+
const buffer = context.createBuffer(2, length, sampleRate);
|
|
123
|
+
|
|
124
|
+
for (let channel = 0; channel < 2; channel++) {
|
|
125
|
+
const data = buffer.getChannelData(channel);
|
|
126
|
+
let lastOut = 0;
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < length; i++) {
|
|
129
|
+
const white = Math.random() * 2 - 1;
|
|
130
|
+
lastOut = (lastOut + white * 0.02) * 0.99;
|
|
131
|
+
data[i] = lastOut * 3.5; // Scale to approximately -1 to 1
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return buffer;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Generates a buffer containing blue noise.
|
|
140
|
+
* Blue noise emphasizes high frequencies (opposite of pink noise).
|
|
141
|
+
*/
|
|
142
|
+
function generateBlueNoise(
|
|
143
|
+
context: BaseAudioContext,
|
|
144
|
+
duration: number,
|
|
145
|
+
): AudioBuffer {
|
|
146
|
+
const sampleRate = context.sampleRate;
|
|
147
|
+
const length = sampleRate * duration;
|
|
148
|
+
const buffer = context.createBuffer(2, length, sampleRate);
|
|
149
|
+
|
|
150
|
+
for (let channel = 0; channel < 2; channel++) {
|
|
151
|
+
const data = buffer.getChannelData(channel);
|
|
152
|
+
let lastWhite = 0;
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < length; i++) {
|
|
155
|
+
const white = Math.random() * 2 - 1;
|
|
156
|
+
// Blue noise is the derivative of white noise
|
|
157
|
+
data[i] = (white - lastWhite) * 0.5;
|
|
158
|
+
lastWhite = white;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return buffer;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Noise generator module supporting white, pink, brown, and blue noise types.
|
|
167
|
+
*/
|
|
168
|
+
export default class Noise
|
|
169
|
+
extends Module<ModuleType.Noise>
|
|
170
|
+
implements NoiseSetterHooks
|
|
171
|
+
{
|
|
172
|
+
declare audioNode: AudioBufferSourceNode;
|
|
173
|
+
isStated = false;
|
|
174
|
+
private noiseBuffers: Map<NoiseType, AudioBuffer>;
|
|
175
|
+
|
|
176
|
+
constructor(engineId: string, params: ICreateModule<ModuleType.Noise>) {
|
|
177
|
+
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
178
|
+
|
|
179
|
+
const audioNodeConstructor = (context: Context) => {
|
|
180
|
+
const node = new AudioBufferSourceNode(context.audioContext);
|
|
181
|
+
return node;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
super(engineId, {
|
|
185
|
+
...params,
|
|
186
|
+
props,
|
|
187
|
+
audioNodeConstructor,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Pre-generate all noise buffers (2 seconds of noise, looped)
|
|
191
|
+
const bufferDuration = 2;
|
|
192
|
+
this.noiseBuffers = new Map<NoiseType, AudioBuffer>([
|
|
193
|
+
[
|
|
194
|
+
NoiseType.white,
|
|
195
|
+
generateWhiteNoise(this.context.audioContext, bufferDuration),
|
|
196
|
+
],
|
|
197
|
+
[
|
|
198
|
+
NoiseType.pink,
|
|
199
|
+
generatePinkNoise(this.context.audioContext, bufferDuration),
|
|
200
|
+
],
|
|
201
|
+
[
|
|
202
|
+
NoiseType.brown,
|
|
203
|
+
generateBrownNoise(this.context.audioContext, bufferDuration),
|
|
204
|
+
],
|
|
205
|
+
[
|
|
206
|
+
NoiseType.blue,
|
|
207
|
+
generateBlueNoise(this.context.audioContext, bufferDuration),
|
|
208
|
+
],
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
// Set the initial buffer
|
|
212
|
+
this.audioNode.buffer = this.noiseBuffers.get(props.type)!;
|
|
213
|
+
this.audioNode.loop = true;
|
|
214
|
+
|
|
215
|
+
this.registerDefaultIOs("out");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
onAfterSetType: NoiseSetterHooks["onAfterSetType"] = (type) => {
|
|
219
|
+
const wasStarted = this.isStated;
|
|
220
|
+
const currentTime = this.context.audioContext.currentTime;
|
|
221
|
+
|
|
222
|
+
if (wasStarted) {
|
|
223
|
+
this.stop(currentTime);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Replace the audio node with a new one using the selected buffer
|
|
227
|
+
this.rePlugAll(() => {
|
|
228
|
+
this.audioNode = new AudioBufferSourceNode(this.context.audioContext, {
|
|
229
|
+
buffer: this.noiseBuffers.get(type)!,
|
|
230
|
+
loop: true,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (wasStarted) {
|
|
235
|
+
this.start(currentTime);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
start(time: ContextTime) {
|
|
240
|
+
if (this.isStated) return;
|
|
241
|
+
|
|
242
|
+
this.isStated = true;
|
|
243
|
+
this.audioNode.start(time);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
stop(time: ContextTime) {
|
|
247
|
+
if (!this.isStated) return;
|
|
248
|
+
|
|
249
|
+
this.audioNode.stop(time);
|
|
250
|
+
this.rePlugAll(() => {
|
|
251
|
+
this.audioNode = new AudioBufferSourceNode(this.context.audioContext, {
|
|
252
|
+
buffer: this.noiseBuffers.get(this.props.type)!,
|
|
253
|
+
loop: true,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
this.isStated = false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -183,7 +183,7 @@ export class MonoOscillator
|
|
|
183
183
|
triggerAttack = (note: Note, triggeredAt: ContextTime) => {
|
|
184
184
|
super.triggerAttack(note, triggeredAt);
|
|
185
185
|
|
|
186
|
-
this.
|
|
186
|
+
this._props.frequency = note.frequency;
|
|
187
187
|
this.updateFrequency(triggeredAt);
|
|
188
188
|
this.start(triggeredAt);
|
|
189
189
|
};
|
|
@@ -196,7 +196,7 @@ export class MonoOscillator
|
|
|
196
196
|
: null;
|
|
197
197
|
if (!lastNote) return;
|
|
198
198
|
|
|
199
|
-
this.
|
|
199
|
+
this._props.frequency = lastNote.frequency;
|
|
200
200
|
this.updateFrequency(triggeredAt);
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -232,6 +232,11 @@ export class MonoOscillator
|
|
|
232
232
|
name: "detune",
|
|
233
233
|
getAudioNode: () => this.detuneGain,
|
|
234
234
|
});
|
|
235
|
+
|
|
236
|
+
this.registerAudioInput({
|
|
237
|
+
name: "fm",
|
|
238
|
+
getAudioNode: () => this.audioNode.frequency,
|
|
239
|
+
});
|
|
235
240
|
}
|
|
236
241
|
|
|
237
242
|
private registerOutputs() {
|
|
@@ -251,7 +256,7 @@ export default class Oscillator extends PolyModule<ModuleType.Oscillator> {
|
|
|
251
256
|
const monoModuleConstructor = (
|
|
252
257
|
engineId: string,
|
|
253
258
|
params: IModuleConstructor<ModuleType.Oscillator>,
|
|
254
|
-
) =>
|
|
259
|
+
) => Module.create(MonoOscillator, engineId, params);
|
|
255
260
|
|
|
256
261
|
super(engineId, {
|
|
257
262
|
...params,
|
|
@@ -277,5 +282,6 @@ export default class Oscillator extends PolyModule<ModuleType.Oscillator> {
|
|
|
277
282
|
|
|
278
283
|
private registerInputs() {
|
|
279
284
|
this.registerAudioInput({ name: "detune" });
|
|
285
|
+
this.registerAudioInput({ name: "fm" });
|
|
280
286
|
}
|
|
281
287
|
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { Context } from "@blibliki/utils";
|
|
2
|
+
import { ModulePropSchema } from "@/core";
|
|
3
|
+
import { Module, SetterHooks } from "@/core/module/Module";
|
|
4
|
+
import { WetDryMixer } from "@/utils";
|
|
5
|
+
import { ICreateModule, ModuleType } from ".";
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export enum ReverbType {
|
|
12
|
+
room = "room",
|
|
13
|
+
hall = "hall",
|
|
14
|
+
plate = "plate",
|
|
15
|
+
spring = "spring",
|
|
16
|
+
chamber = "chamber",
|
|
17
|
+
reflections = "reflections",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type IReverbProps = {
|
|
21
|
+
mix: number; // 0-1 (dry/wet)
|
|
22
|
+
decayTime: number; // 0.1-10 seconds
|
|
23
|
+
preDelay: number; // 0-100 ms
|
|
24
|
+
type: ReverbType;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type IReverb = Module<ModuleType.Reverb>;
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Schema
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
export const reverbPropSchema: ModulePropSchema<
|
|
34
|
+
IReverbProps,
|
|
35
|
+
{ type: { kind: "enum"; options: ReverbType[] } }
|
|
36
|
+
> = {
|
|
37
|
+
mix: {
|
|
38
|
+
kind: "number",
|
|
39
|
+
min: 0,
|
|
40
|
+
max: 1,
|
|
41
|
+
step: 0.01,
|
|
42
|
+
label: "Mix",
|
|
43
|
+
},
|
|
44
|
+
decayTime: {
|
|
45
|
+
kind: "number",
|
|
46
|
+
min: 0.1,
|
|
47
|
+
max: 10,
|
|
48
|
+
step: 0.1,
|
|
49
|
+
exp: 2,
|
|
50
|
+
label: "Decay Time",
|
|
51
|
+
},
|
|
52
|
+
preDelay: {
|
|
53
|
+
kind: "number",
|
|
54
|
+
min: 0,
|
|
55
|
+
max: 100,
|
|
56
|
+
step: 1,
|
|
57
|
+
label: "Pre-delay",
|
|
58
|
+
},
|
|
59
|
+
type: {
|
|
60
|
+
kind: "enum",
|
|
61
|
+
options: [
|
|
62
|
+
ReverbType.room,
|
|
63
|
+
ReverbType.hall,
|
|
64
|
+
ReverbType.plate,
|
|
65
|
+
ReverbType.spring,
|
|
66
|
+
ReverbType.chamber,
|
|
67
|
+
ReverbType.reflections,
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const DEFAULT_REVERB_PROPS: IReverbProps = {
|
|
73
|
+
mix: 0.3,
|
|
74
|
+
decayTime: 1.5,
|
|
75
|
+
preDelay: 0,
|
|
76
|
+
type: ReverbType.room,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Impulse Response Generation
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
function generateImpulseResponse(
|
|
84
|
+
context: Context,
|
|
85
|
+
type: ReverbType,
|
|
86
|
+
decayTime: number,
|
|
87
|
+
): AudioBuffer {
|
|
88
|
+
const sampleRate = context.audioContext.sampleRate;
|
|
89
|
+
|
|
90
|
+
// Special handling for reflections - use short buffer
|
|
91
|
+
const effectiveDecayTime =
|
|
92
|
+
type === ReverbType.reflections
|
|
93
|
+
? Math.min(decayTime, 0.2) // Max 200ms for reflections
|
|
94
|
+
: decayTime;
|
|
95
|
+
|
|
96
|
+
const length = Math.floor(sampleRate * effectiveDecayTime);
|
|
97
|
+
const buffer = context.audioContext.createBuffer(2, length, sampleRate);
|
|
98
|
+
|
|
99
|
+
// Room type tuning parameters
|
|
100
|
+
const tuning = getRoomTuning(type);
|
|
101
|
+
|
|
102
|
+
// Reflections type uses discrete early reflections
|
|
103
|
+
if (type === ReverbType.reflections) {
|
|
104
|
+
generateEarlyReflections(buffer, sampleRate, tuning);
|
|
105
|
+
} else {
|
|
106
|
+
// Standard diffuse reverb tail
|
|
107
|
+
generateDiffuseTail(buffer, sampleRate, length, tuning);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Normalize
|
|
111
|
+
normalizeBuffer(buffer);
|
|
112
|
+
|
|
113
|
+
return buffer;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Generate discrete early reflections for small spaces
|
|
117
|
+
function generateEarlyReflections(
|
|
118
|
+
buffer: AudioBuffer,
|
|
119
|
+
sampleRate: number,
|
|
120
|
+
tuning: { decayFactor: number; damping: number; cutoff: number },
|
|
121
|
+
) {
|
|
122
|
+
// Reflection times in milliseconds (psychoacoustic early reflection pattern)
|
|
123
|
+
const reflectionTimes = [
|
|
124
|
+
0, 7, 11, 17, 23, 31, 41, 47, 59, 67, 79, 89, 103, 127,
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
for (let channel = 0; channel < 2; channel++) {
|
|
128
|
+
const data = buffer.getChannelData(channel);
|
|
129
|
+
|
|
130
|
+
// Add slight variation between channels for stereo width
|
|
131
|
+
const channelOffset = channel * 2.3;
|
|
132
|
+
|
|
133
|
+
for (const reflectionMs of reflectionTimes) {
|
|
134
|
+
const time = (reflectionMs + channelOffset) / 1000;
|
|
135
|
+
const position = Math.floor(time * sampleRate);
|
|
136
|
+
|
|
137
|
+
if (position >= data.length) break;
|
|
138
|
+
|
|
139
|
+
// Each reflection has a short burst
|
|
140
|
+
const burstLength = Math.floor(sampleRate * 0.003); // 3ms burst
|
|
141
|
+
const amplitude = Math.exp(-time * tuning.decayFactor);
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < burstLength && position + i < data.length; i++) {
|
|
144
|
+
const noise = Math.random() * 2 - 1;
|
|
145
|
+
const envelope = Math.exp(-i / (burstLength * 0.3)); // Quick decay within burst
|
|
146
|
+
data[position + i] = data[position + i]! + noise * amplitude * envelope;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Apply lowpass filter
|
|
151
|
+
applyLowpass(data, tuning.cutoff);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Generate standard diffuse reverb tail
|
|
156
|
+
function generateDiffuseTail(
|
|
157
|
+
buffer: AudioBuffer,
|
|
158
|
+
sampleRate: number,
|
|
159
|
+
length: number,
|
|
160
|
+
tuning: { decayFactor: number; damping: number; cutoff: number },
|
|
161
|
+
) {
|
|
162
|
+
for (let channel = 0; channel < 2; channel++) {
|
|
163
|
+
const data = buffer.getChannelData(channel);
|
|
164
|
+
|
|
165
|
+
for (let i = 0; i < length; i++) {
|
|
166
|
+
// White noise
|
|
167
|
+
const noise = Math.random() * 2 - 1;
|
|
168
|
+
|
|
169
|
+
// Exponential decay
|
|
170
|
+
const time = i / sampleRate;
|
|
171
|
+
const decay = Math.exp(-time * tuning.decayFactor);
|
|
172
|
+
|
|
173
|
+
// High-frequency damping (simple one-pole lowpass)
|
|
174
|
+
const damping = 1 - tuning.damping * (i / length);
|
|
175
|
+
|
|
176
|
+
data[i] = noise * decay * damping;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Apply simple lowpass filter for damping
|
|
180
|
+
applyLowpass(data, tuning.cutoff);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getRoomTuning(type: ReverbType) {
|
|
185
|
+
switch (type) {
|
|
186
|
+
case ReverbType.room:
|
|
187
|
+
// Small to medium space - intimate, clear
|
|
188
|
+
return {
|
|
189
|
+
decayFactor: 3, // Moderate decay (~1-2s)
|
|
190
|
+
damping: 0.5, // Moderate high-freq loss
|
|
191
|
+
cutoff: 0.7, // Moderate brightness
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
case ReverbType.hall:
|
|
195
|
+
// Large concert hall - spacious, smooth, long decay
|
|
196
|
+
return {
|
|
197
|
+
decayFactor: 1.5, // Slower decay (longer reverb tail)
|
|
198
|
+
damping: 0.3, // Less damping (preserve highs longer)
|
|
199
|
+
cutoff: 0.85, // Brighter (less filtering)
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
case ReverbType.plate:
|
|
203
|
+
// Vintage plate reverb - bright, dense, metallic
|
|
204
|
+
return {
|
|
205
|
+
decayFactor: 2.5, // Medium decay
|
|
206
|
+
damping: 0.2, // Very little damping (bright character)
|
|
207
|
+
cutoff: 0.9, // Very bright (minimal filtering)
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
case ReverbType.spring:
|
|
211
|
+
// Vintage spring reverb - metallic, bright, resonant
|
|
212
|
+
return {
|
|
213
|
+
decayFactor: 4, // Fast decay (spring tanks have quick decay)
|
|
214
|
+
damping: 0.1, // Very little damping (bright, metallic)
|
|
215
|
+
cutoff: 0.95, // Very bright (minimal filtering, metallic character)
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
case ReverbType.chamber:
|
|
219
|
+
// Echo chamber - medium-large space, smooth, diffuse
|
|
220
|
+
return {
|
|
221
|
+
decayFactor: 2.0, // Medium decay
|
|
222
|
+
damping: 0.35, // Moderate damping
|
|
223
|
+
cutoff: 0.75, // Moderate brightness
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
case ReverbType.reflections:
|
|
227
|
+
// Early reflections only - small space acoustics
|
|
228
|
+
return {
|
|
229
|
+
decayFactor: 5, // Fast decay for discrete reflections
|
|
230
|
+
damping: 0.4, // Moderate damping
|
|
231
|
+
cutoff: 0.8, // Fairly bright
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
default:
|
|
235
|
+
return {
|
|
236
|
+
decayFactor: 3,
|
|
237
|
+
damping: 0.5,
|
|
238
|
+
cutoff: 0.7,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function applyLowpass(data: Float32Array, cutoff: number) {
|
|
244
|
+
let y1 = 0;
|
|
245
|
+
const a = 1 - cutoff; // Simple coefficient
|
|
246
|
+
|
|
247
|
+
for (let i = 0; i < data.length; i++) {
|
|
248
|
+
const sample = data[i]!;
|
|
249
|
+
y1 = a * sample + (1 - a) * y1;
|
|
250
|
+
data[i] = y1;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizeBuffer(buffer: AudioBuffer) {
|
|
255
|
+
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
|
256
|
+
const data = buffer.getChannelData(channel);
|
|
257
|
+
let max = 0;
|
|
258
|
+
|
|
259
|
+
// Find peak
|
|
260
|
+
for (const sample of data) {
|
|
261
|
+
max = Math.max(max, Math.abs(sample));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Normalize to 0.5 (avoid clipping)
|
|
265
|
+
if (max > 0) {
|
|
266
|
+
const scale = 0.5 / max;
|
|
267
|
+
for (let i = 0; i < data.length; i++) {
|
|
268
|
+
data[i] = data[i]! * scale;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// Module Class
|
|
276
|
+
// ============================================================================
|
|
277
|
+
|
|
278
|
+
export default class Reverb
|
|
279
|
+
extends Module<ModuleType.Reverb>
|
|
280
|
+
implements
|
|
281
|
+
Pick<
|
|
282
|
+
SetterHooks<IReverbProps>,
|
|
283
|
+
| "onAfterSetMix"
|
|
284
|
+
| "onAfterSetDecayTime"
|
|
285
|
+
| "onAfterSetPreDelay"
|
|
286
|
+
| "onAfterSetType"
|
|
287
|
+
>
|
|
288
|
+
{
|
|
289
|
+
// Audio graph nodes
|
|
290
|
+
declare audioNode: GainNode; // Input node
|
|
291
|
+
private outputNode: GainNode; // Final output node
|
|
292
|
+
private convolverNode: ConvolverNode;
|
|
293
|
+
private preDelayNode: DelayNode;
|
|
294
|
+
private wetDryMixer: WetDryMixer;
|
|
295
|
+
|
|
296
|
+
constructor(engineId: string, params: ICreateModule<ModuleType.Reverb>) {
|
|
297
|
+
const props = { ...DEFAULT_REVERB_PROPS, ...params.props };
|
|
298
|
+
|
|
299
|
+
// Input node (this will be audioNode for Module interface)
|
|
300
|
+
const audioNodeConstructor = (context: Context) =>
|
|
301
|
+
context.audioContext.createGain();
|
|
302
|
+
|
|
303
|
+
super(engineId, {
|
|
304
|
+
...params,
|
|
305
|
+
props,
|
|
306
|
+
audioNodeConstructor,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Set input gain
|
|
310
|
+
this.audioNode.gain.value = 1;
|
|
311
|
+
|
|
312
|
+
// Create wet/dry mixer
|
|
313
|
+
this.wetDryMixer = new WetDryMixer(this.context);
|
|
314
|
+
|
|
315
|
+
// Create audio processing nodes
|
|
316
|
+
this.convolverNode = this.context.audioContext.createConvolver();
|
|
317
|
+
this.preDelayNode = this.context.audioContext.createDelay(0.1); // 100ms max
|
|
318
|
+
|
|
319
|
+
// Connect graph:
|
|
320
|
+
// audioNode (input) -> wetDryMixer (dry path)
|
|
321
|
+
// -> preDelay -> convolver -> wetDryMixer (wet path)
|
|
322
|
+
// wetDryMixer -> outputNode
|
|
323
|
+
this.wetDryMixer.connectInput(this.audioNode);
|
|
324
|
+
this.audioNode.connect(this.preDelayNode);
|
|
325
|
+
this.preDelayNode.connect(this.convolverNode);
|
|
326
|
+
this.convolverNode.connect(this.wetDryMixer.getWetInput());
|
|
327
|
+
this.outputNode = this.wetDryMixer.getOutput();
|
|
328
|
+
|
|
329
|
+
// Generate initial impulse response
|
|
330
|
+
this.regenerateImpulseResponse();
|
|
331
|
+
|
|
332
|
+
// Set initial parameters
|
|
333
|
+
this.wetDryMixer.setMix(props.mix);
|
|
334
|
+
this.preDelayNode.delayTime.value = props.preDelay / 1000;
|
|
335
|
+
|
|
336
|
+
this.registerDefaultIOs("in");
|
|
337
|
+
this.registerCustomOutput();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private registerCustomOutput() {
|
|
341
|
+
this.registerAudioOutput({
|
|
342
|
+
name: "out",
|
|
343
|
+
getAudioNode: () => this.outputNode,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ============================================================================
|
|
348
|
+
// SetterHooks
|
|
349
|
+
// ============================================================================
|
|
350
|
+
|
|
351
|
+
onAfterSetMix = (value: number) => {
|
|
352
|
+
this.wetDryMixer.setMix(value);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
onAfterSetDecayTime = () => {
|
|
356
|
+
this.regenerateImpulseResponse();
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
onAfterSetPreDelay = (value: number) => {
|
|
360
|
+
this.preDelayNode.delayTime.value = value / 1000; // ms to seconds
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
onAfterSetType = () => {
|
|
364
|
+
this.regenerateImpulseResponse();
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// ============================================================================
|
|
368
|
+
// Private Methods
|
|
369
|
+
// ============================================================================
|
|
370
|
+
|
|
371
|
+
private regenerateImpulseResponse() {
|
|
372
|
+
const impulse = generateImpulseResponse(
|
|
373
|
+
this.context,
|
|
374
|
+
this.props.type,
|
|
375
|
+
this.props.decayTime,
|
|
376
|
+
);
|
|
377
|
+
this.convolverNode.buffer = impulse;
|
|
378
|
+
}
|
|
379
|
+
}
|