@blibliki/engine 0.5.1 → 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.
Files changed (47) hide show
  1. package/README.md +22 -2
  2. package/dist/index.d.ts +501 -107
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/package.json +7 -7
  6. package/src/Engine.ts +46 -29
  7. package/src/core/index.ts +11 -2
  8. package/src/core/midi/BaseMidiDevice.ts +47 -0
  9. package/src/core/midi/ComputerKeyboardDevice.ts +2 -1
  10. package/src/core/midi/MidiDeviceManager.ts +125 -31
  11. package/src/core/midi/{MidiDevice.ts → MidiInputDevice.ts} +6 -30
  12. package/src/core/midi/MidiOutputDevice.ts +23 -0
  13. package/src/core/midi/adapters/NodeMidiAdapter.ts +99 -13
  14. package/src/core/midi/adapters/WebMidiAdapter.ts +68 -10
  15. package/src/core/midi/adapters/types.ts +13 -4
  16. package/src/core/midi/controllers/BaseController.ts +14 -0
  17. package/src/core/module/Module.ts +121 -13
  18. package/src/core/module/PolyModule.ts +36 -0
  19. package/src/core/module/VoiceScheduler.ts +150 -10
  20. package/src/core/module/index.ts +9 -4
  21. package/src/index.ts +27 -3
  22. package/src/modules/Chorus.ts +222 -0
  23. package/src/modules/Constant.ts +2 -2
  24. package/src/modules/Delay.ts +347 -0
  25. package/src/modules/Distortion.ts +182 -0
  26. package/src/modules/Envelope.ts +158 -92
  27. package/src/modules/Filter.ts +7 -7
  28. package/src/modules/Gain.ts +2 -2
  29. package/src/modules/LFO.ts +287 -0
  30. package/src/modules/LegacyEnvelope.ts +146 -0
  31. package/src/modules/{MidiSelector.ts → MidiInput.ts} +26 -19
  32. package/src/modules/MidiMapper.ts +59 -4
  33. package/src/modules/MidiOutput.ts +121 -0
  34. package/src/modules/Noise.ts +259 -0
  35. package/src/modules/Oscillator.ts +9 -3
  36. package/src/modules/Reverb.ts +379 -0
  37. package/src/modules/Scale.ts +49 -4
  38. package/src/modules/StepSequencer.ts +410 -22
  39. package/src/modules/StereoPanner.ts +1 -1
  40. package/src/modules/index.ts +142 -29
  41. package/src/processors/custom-envelope-processor.ts +125 -0
  42. package/src/processors/index.ts +10 -0
  43. package/src/processors/lfo-processor.ts +123 -0
  44. package/src/processors/scale-processor.ts +42 -5
  45. package/src/utils/WetDryMixer.ts +123 -0
  46. package/src/utils/expandPatternSequence.ts +18 -0
  47. 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.props = { frequency: note.frequency };
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.props = { frequency: lastNote.frequency };
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
- ) => new MonoOscillator(engineId, params);
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
+ }