@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.
- 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,347 @@
|
|
|
1
|
+
import { Division, divisionToMilliseconds } from "@blibliki/transport";
|
|
2
|
+
import { Context } from "@blibliki/utils";
|
|
3
|
+
import { EnumProp, ModulePropSchema } from "@/core";
|
|
4
|
+
import { Module, SetterHooks } from "@/core/module/Module";
|
|
5
|
+
import { WetDryMixer } from "@/utils";
|
|
6
|
+
import { ICreateModule, ModuleType } from ".";
|
|
7
|
+
|
|
8
|
+
export enum DelayTimeMode {
|
|
9
|
+
short = "short",
|
|
10
|
+
long = "long",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type IDelayProps = {
|
|
14
|
+
time: number; // 0-2000 ms (short) or 0-5000 ms (long)
|
|
15
|
+
timeMode: DelayTimeMode; // short (2s) or long (5s)
|
|
16
|
+
sync: boolean; // Enable BPM sync
|
|
17
|
+
division: Division; // Note division when sync is enabled
|
|
18
|
+
feedback: number; // 0-0.95 (feedback amount)
|
|
19
|
+
mix: number; // 0-1 (dry/wet)
|
|
20
|
+
stereo: boolean; // Enable ping-pong stereo mode
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type IDelay = Module<ModuleType.Delay>;
|
|
24
|
+
|
|
25
|
+
const NOTE_DIVISIONS: Division[] = [
|
|
26
|
+
"1/64",
|
|
27
|
+
"1/48",
|
|
28
|
+
"1/32",
|
|
29
|
+
"1/24",
|
|
30
|
+
"1/16",
|
|
31
|
+
"1/12",
|
|
32
|
+
"1/8",
|
|
33
|
+
"1/6",
|
|
34
|
+
"3/16",
|
|
35
|
+
"1/4",
|
|
36
|
+
"5/16",
|
|
37
|
+
"1/3",
|
|
38
|
+
"3/8",
|
|
39
|
+
"1/2",
|
|
40
|
+
"3/4",
|
|
41
|
+
"1",
|
|
42
|
+
"1.5",
|
|
43
|
+
"2",
|
|
44
|
+
"3",
|
|
45
|
+
"4",
|
|
46
|
+
"6",
|
|
47
|
+
"8",
|
|
48
|
+
"16",
|
|
49
|
+
"32",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export const delayPropSchema: ModulePropSchema<
|
|
53
|
+
IDelayProps,
|
|
54
|
+
{
|
|
55
|
+
timeMode: EnumProp<DelayTimeMode>;
|
|
56
|
+
division: EnumProp<Division>;
|
|
57
|
+
}
|
|
58
|
+
> = {
|
|
59
|
+
time: {
|
|
60
|
+
kind: "number",
|
|
61
|
+
min: 0,
|
|
62
|
+
max: 5000, // UI max for manual mode (long = 5s)
|
|
63
|
+
step: 1,
|
|
64
|
+
label: "Delay Time",
|
|
65
|
+
},
|
|
66
|
+
timeMode: {
|
|
67
|
+
kind: "enum",
|
|
68
|
+
options: [DelayTimeMode.short, DelayTimeMode.long],
|
|
69
|
+
label: "Time Mode",
|
|
70
|
+
},
|
|
71
|
+
sync: {
|
|
72
|
+
kind: "boolean",
|
|
73
|
+
label: "Sync",
|
|
74
|
+
},
|
|
75
|
+
division: {
|
|
76
|
+
kind: "enum",
|
|
77
|
+
options: NOTE_DIVISIONS,
|
|
78
|
+
label: "Division",
|
|
79
|
+
},
|
|
80
|
+
feedback: {
|
|
81
|
+
kind: "number",
|
|
82
|
+
min: 0,
|
|
83
|
+
max: 0.95,
|
|
84
|
+
step: 0.01,
|
|
85
|
+
label: "Feedback",
|
|
86
|
+
},
|
|
87
|
+
mix: {
|
|
88
|
+
kind: "number",
|
|
89
|
+
min: 0,
|
|
90
|
+
max: 1,
|
|
91
|
+
step: 0.01,
|
|
92
|
+
label: "Mix",
|
|
93
|
+
},
|
|
94
|
+
stereo: {
|
|
95
|
+
kind: "boolean",
|
|
96
|
+
label: "Stereo",
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const DEFAULT_DELAY_PROPS: IDelayProps = {
|
|
101
|
+
time: 250,
|
|
102
|
+
timeMode: DelayTimeMode.short,
|
|
103
|
+
sync: false,
|
|
104
|
+
division: "1/4",
|
|
105
|
+
feedback: 0.3,
|
|
106
|
+
mix: 0.3,
|
|
107
|
+
stereo: false,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Module Class
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
export default class Delay
|
|
115
|
+
extends Module<ModuleType.Delay>
|
|
116
|
+
implements
|
|
117
|
+
Pick<
|
|
118
|
+
SetterHooks<IDelayProps>,
|
|
119
|
+
| "onAfterSetTime"
|
|
120
|
+
| "onAfterSetTimeMode"
|
|
121
|
+
| "onAfterSetSync"
|
|
122
|
+
| "onAfterSetDivision"
|
|
123
|
+
| "onAfterSetFeedback"
|
|
124
|
+
| "onAfterSetMix"
|
|
125
|
+
| "onAfterSetStereo"
|
|
126
|
+
>
|
|
127
|
+
{
|
|
128
|
+
// Audio graph nodes
|
|
129
|
+
declare audioNode: GainNode; // Input node
|
|
130
|
+
private outputNode!: GainNode; // Final output node
|
|
131
|
+
private wetDryMixer: WetDryMixer;
|
|
132
|
+
|
|
133
|
+
// Mono delay nodes
|
|
134
|
+
private delayNode: DelayNode;
|
|
135
|
+
private feedbackGain: GainNode;
|
|
136
|
+
|
|
137
|
+
// Stereo ping-pong nodes (created when stereo=true)
|
|
138
|
+
private delayLeft: DelayNode | null = null;
|
|
139
|
+
private delayRight: DelayNode | null = null;
|
|
140
|
+
private feedbackLeft: GainNode | null = null;
|
|
141
|
+
private feedbackRight: GainNode | null = null;
|
|
142
|
+
private merger: ChannelMergerNode | null = null;
|
|
143
|
+
|
|
144
|
+
constructor(engineId: string, params: ICreateModule<ModuleType.Delay>) {
|
|
145
|
+
const props = { ...DEFAULT_DELAY_PROPS, ...params.props };
|
|
146
|
+
|
|
147
|
+
// Input node
|
|
148
|
+
const audioNodeConstructor = (context: Context) =>
|
|
149
|
+
context.audioContext.createGain();
|
|
150
|
+
|
|
151
|
+
super(engineId, {
|
|
152
|
+
...params,
|
|
153
|
+
props,
|
|
154
|
+
audioNodeConstructor,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Set input gain
|
|
158
|
+
this.audioNode.gain.value = 1;
|
|
159
|
+
|
|
160
|
+
// Create WetDryMixer
|
|
161
|
+
this.wetDryMixer = new WetDryMixer(this.context);
|
|
162
|
+
|
|
163
|
+
// Create mono delay graph (default)
|
|
164
|
+
// Max 179s (Web Audio API limit: must be < 180s / 3 minutes)
|
|
165
|
+
this.delayNode = this.context.audioContext.createDelay(179);
|
|
166
|
+
this.feedbackGain = this.context.audioContext.createGain();
|
|
167
|
+
|
|
168
|
+
// Connect mono graph initially
|
|
169
|
+
this.connectMonoGraph();
|
|
170
|
+
|
|
171
|
+
// Apply initial parameters
|
|
172
|
+
this.wetDryMixer.setMix(props.mix);
|
|
173
|
+
this.feedbackGain.gain.value = props.feedback;
|
|
174
|
+
this.updateDelayTime();
|
|
175
|
+
|
|
176
|
+
// Switch to stereo if needed
|
|
177
|
+
if (props.stereo) {
|
|
178
|
+
this.switchToStereo();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Setup BPM listener for sync mode
|
|
182
|
+
this.setupBPMListener();
|
|
183
|
+
|
|
184
|
+
this.registerDefaultIOs("in");
|
|
185
|
+
this.registerCustomOutput();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private setupBPMListener() {
|
|
189
|
+
this.engine.transport.addPropertyChangeCallback("bpm", () => {
|
|
190
|
+
if (!this.props.sync) return;
|
|
191
|
+
|
|
192
|
+
this.updateDelayTime();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private updateDelayTime() {
|
|
197
|
+
let timeInSeconds: number;
|
|
198
|
+
|
|
199
|
+
if (this.props.sync) {
|
|
200
|
+
// BPM-based timing
|
|
201
|
+
const bpm = this.engine.transport.bpm;
|
|
202
|
+
const timeMs = divisionToMilliseconds(this.props.division, bpm);
|
|
203
|
+
timeInSeconds = timeMs / 1000;
|
|
204
|
+
} else {
|
|
205
|
+
// Manual timing
|
|
206
|
+
timeInSeconds = this.props.time / 1000;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.delayNode.delayTime.value = timeInSeconds;
|
|
210
|
+
if (this.delayLeft) this.delayLeft.delayTime.value = timeInSeconds;
|
|
211
|
+
if (this.delayRight) this.delayRight.delayTime.value = timeInSeconds;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private connectMonoGraph() {
|
|
215
|
+
// Disconnect any existing connections
|
|
216
|
+
this.disconnectAll();
|
|
217
|
+
|
|
218
|
+
// Connect: Input -> WetDryMixer (dry)
|
|
219
|
+
this.wetDryMixer.connectInput(this.audioNode);
|
|
220
|
+
|
|
221
|
+
// Connect: Input -> Delay -> Feedback -> Delay (loop)
|
|
222
|
+
this.audioNode.connect(this.delayNode);
|
|
223
|
+
this.delayNode.connect(this.feedbackGain);
|
|
224
|
+
this.feedbackGain.connect(this.delayNode); // Feedback loop
|
|
225
|
+
|
|
226
|
+
// Connect: Delay -> WetDryMixer (wet)
|
|
227
|
+
this.delayNode.connect(this.wetDryMixer.getWetInput());
|
|
228
|
+
|
|
229
|
+
// Output
|
|
230
|
+
this.outputNode = this.wetDryMixer.getOutput();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private switchToStereo() {
|
|
234
|
+
if (!this.delayLeft) {
|
|
235
|
+
// Create stereo nodes (max 179s - Web Audio API limit: < 180s)
|
|
236
|
+
this.delayLeft = this.context.audioContext.createDelay(179);
|
|
237
|
+
this.delayRight = this.context.audioContext.createDelay(179);
|
|
238
|
+
this.feedbackLeft = this.context.audioContext.createGain();
|
|
239
|
+
this.feedbackRight = this.context.audioContext.createGain();
|
|
240
|
+
this.merger = this.context.audioContext.createChannelMerger(2);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Disconnect mono graph
|
|
244
|
+
this.disconnectAll();
|
|
245
|
+
|
|
246
|
+
// Connect stereo ping-pong graph
|
|
247
|
+
this.wetDryMixer.connectInput(this.audioNode);
|
|
248
|
+
|
|
249
|
+
// Input -> DelayLeft
|
|
250
|
+
this.audioNode.connect(this.delayLeft);
|
|
251
|
+
|
|
252
|
+
// DelayLeft -> Output to left channel
|
|
253
|
+
this.delayLeft.connect(this.merger!, 0, 0); // Left to channel 0
|
|
254
|
+
|
|
255
|
+
// DelayLeft -> FeedbackRight -> DelayRight (ping-pong to right)
|
|
256
|
+
this.delayLeft.connect(this.feedbackRight!);
|
|
257
|
+
this.feedbackRight!.connect(this.delayRight!);
|
|
258
|
+
|
|
259
|
+
// DelayRight -> Output to right channel
|
|
260
|
+
this.delayRight!.connect(this.merger!, 0, 1); // Right to channel 1
|
|
261
|
+
|
|
262
|
+
// DelayRight -> FeedbackLeft -> DelayLeft (ping-pong back to left)
|
|
263
|
+
this.delayRight!.connect(this.feedbackLeft!);
|
|
264
|
+
this.feedbackLeft!.connect(this.delayLeft);
|
|
265
|
+
|
|
266
|
+
// Merger -> WetDryMixer (wet)
|
|
267
|
+
this.merger!.connect(this.wetDryMixer.getWetInput());
|
|
268
|
+
|
|
269
|
+
this.outputNode = this.wetDryMixer.getOutput();
|
|
270
|
+
|
|
271
|
+
// Sync delay times and feedback gains
|
|
272
|
+
this.delayLeft.delayTime.value = this.delayNode.delayTime.value;
|
|
273
|
+
this.delayRight!.delayTime.value = this.delayNode.delayTime.value;
|
|
274
|
+
this.feedbackLeft!.gain.value = this.feedbackGain.gain.value;
|
|
275
|
+
this.feedbackRight!.gain.value = this.feedbackGain.gain.value;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private disconnectAll() {
|
|
279
|
+
try {
|
|
280
|
+
this.audioNode.disconnect();
|
|
281
|
+
this.delayNode.disconnect();
|
|
282
|
+
this.feedbackGain.disconnect();
|
|
283
|
+
if (this.delayLeft) this.delayLeft.disconnect();
|
|
284
|
+
if (this.delayRight) this.delayRight.disconnect();
|
|
285
|
+
if (this.feedbackLeft) this.feedbackLeft.disconnect();
|
|
286
|
+
if (this.feedbackRight) this.feedbackRight.disconnect();
|
|
287
|
+
if (this.merger) this.merger.disconnect();
|
|
288
|
+
} catch {
|
|
289
|
+
// Ignore disconnect errors
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private registerCustomOutput() {
|
|
294
|
+
this.registerAudioOutput({
|
|
295
|
+
name: "out",
|
|
296
|
+
getAudioNode: () => this.outputNode,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// SetterHooks
|
|
302
|
+
// ============================================================================
|
|
303
|
+
|
|
304
|
+
onAfterSetTime = (_value: number) => {
|
|
305
|
+
if (this.props.sync) return;
|
|
306
|
+
|
|
307
|
+
this.updateDelayTime();
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
onAfterSetTimeMode = (value: DelayTimeMode) => {
|
|
311
|
+
// Clamp time if it exceeds the new mode's maximum
|
|
312
|
+
const maxTime = value === DelayTimeMode.short ? 2000 : 5000;
|
|
313
|
+
if (this.props.time > maxTime) {
|
|
314
|
+
this.props = { time: maxTime };
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
onAfterSetSync = (_value: boolean) => {
|
|
319
|
+
this.updateDelayTime();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
onAfterSetDivision = (_value: Division) => {
|
|
323
|
+
if (this.props.sync) {
|
|
324
|
+
this.updateDelayTime();
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
onAfterSetFeedback = (value: number) => {
|
|
329
|
+
// Cap at 0.95 to prevent runaway feedback
|
|
330
|
+
const cappedValue = Math.min(value, 0.95);
|
|
331
|
+
this.feedbackGain.gain.value = cappedValue;
|
|
332
|
+
if (this.feedbackLeft) this.feedbackLeft.gain.value = cappedValue;
|
|
333
|
+
if (this.feedbackRight) this.feedbackRight.gain.value = cappedValue;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
onAfterSetMix = (value: number) => {
|
|
337
|
+
this.wetDryMixer.setMix(value);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
onAfterSetStereo = (value: boolean) => {
|
|
341
|
+
if (value) {
|
|
342
|
+
this.switchToStereo();
|
|
343
|
+
} else {
|
|
344
|
+
this.connectMonoGraph();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Context } from "@blibliki/utils";
|
|
2
|
+
import { GainNode } from "@blibliki/utils/web-audio-api";
|
|
3
|
+
import { ModulePropSchema } from "@/core";
|
|
4
|
+
import { IModuleConstructor, Module, SetterHooks } from "@/core/module/Module";
|
|
5
|
+
import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
|
|
6
|
+
import { WetDryMixer } from "@/utils";
|
|
7
|
+
import { ICreateModule, ModuleType } from ".";
|
|
8
|
+
|
|
9
|
+
export type IDistortionProps = {
|
|
10
|
+
drive: number; // 0-10 (controls distortion amount)
|
|
11
|
+
tone: number; // 200-20000 Hz (lowpass filter cutoff)
|
|
12
|
+
mix: number; // 0-1 (dry/wet blend)
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type IDistortion = Module<ModuleType.Distortion>;
|
|
16
|
+
|
|
17
|
+
export const distortionPropSchema: ModulePropSchema<IDistortionProps> = {
|
|
18
|
+
drive: {
|
|
19
|
+
kind: "number",
|
|
20
|
+
min: 0,
|
|
21
|
+
max: 10,
|
|
22
|
+
step: 0.1,
|
|
23
|
+
label: "Drive",
|
|
24
|
+
},
|
|
25
|
+
tone: {
|
|
26
|
+
kind: "number",
|
|
27
|
+
min: 200,
|
|
28
|
+
max: 20000,
|
|
29
|
+
step: 1,
|
|
30
|
+
exp: 3,
|
|
31
|
+
label: "Tone",
|
|
32
|
+
},
|
|
33
|
+
mix: {
|
|
34
|
+
kind: "number",
|
|
35
|
+
min: 0,
|
|
36
|
+
max: 1,
|
|
37
|
+
step: 0.01,
|
|
38
|
+
label: "Mix",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const DEFAULT_PROPS: IDistortionProps = {
|
|
43
|
+
drive: 2.0, // Moderate distortion
|
|
44
|
+
tone: 8000, // Mid-bright tone
|
|
45
|
+
mix: 1.0, // 100% wet (full effect)
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export class MonoDistortion
|
|
49
|
+
extends Module<ModuleType.Distortion>
|
|
50
|
+
implements
|
|
51
|
+
Pick<
|
|
52
|
+
SetterHooks<IDistortionProps>,
|
|
53
|
+
"onAfterSetDrive" | "onAfterSetTone" | "onAfterSetMix"
|
|
54
|
+
>
|
|
55
|
+
{
|
|
56
|
+
declare audioNode: GainNode; // Input node (inherited from Module)
|
|
57
|
+
private outputNode: GainNode; // Final output from mixer
|
|
58
|
+
private inputGain: GainNode; // Drive stage (pre-distortion gain)
|
|
59
|
+
private waveshaper: WaveShaperNode; // Tanh distortion
|
|
60
|
+
private filter: BiquadFilterNode; // Post-distortion lowpass filter
|
|
61
|
+
private wetDryMixer: WetDryMixer; // Wet/dry blending
|
|
62
|
+
|
|
63
|
+
constructor(engineId: string, params: ICreateModule<ModuleType.Distortion>) {
|
|
64
|
+
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
65
|
+
|
|
66
|
+
const audioNodeConstructor = (context: Context) =>
|
|
67
|
+
new GainNode(context.audioContext, { gain: 1 });
|
|
68
|
+
|
|
69
|
+
super(engineId, {
|
|
70
|
+
...params,
|
|
71
|
+
audioNodeConstructor,
|
|
72
|
+
props,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Create audio nodes
|
|
76
|
+
const audioContext = this.context.audioContext;
|
|
77
|
+
|
|
78
|
+
this.inputGain = audioContext.createGain();
|
|
79
|
+
this.waveshaper = audioContext.createWaveShaper();
|
|
80
|
+
this.filter = audioContext.createBiquadFilter();
|
|
81
|
+
this.wetDryMixer = new WetDryMixer(this.context);
|
|
82
|
+
|
|
83
|
+
// Configure filter
|
|
84
|
+
this.filter.type = "lowpass";
|
|
85
|
+
this.filter.Q.value = 0.707; // Butterworth response (no resonance peak)
|
|
86
|
+
|
|
87
|
+
// Connect audio graph
|
|
88
|
+
// Dry path: Input -> WetDryMixer
|
|
89
|
+
this.wetDryMixer.connectInput(this.audioNode);
|
|
90
|
+
|
|
91
|
+
// Wet path: Input -> InputGain -> WaveShaper -> Filter -> WetDryMixer
|
|
92
|
+
this.audioNode.connect(this.inputGain);
|
|
93
|
+
this.inputGain.connect(this.waveshaper);
|
|
94
|
+
this.waveshaper.connect(this.filter);
|
|
95
|
+
this.filter.connect(this.wetDryMixer.getWetInput());
|
|
96
|
+
|
|
97
|
+
// Output from mixer
|
|
98
|
+
this.outputNode = this.wetDryMixer.getOutput();
|
|
99
|
+
|
|
100
|
+
// Apply initial parameters
|
|
101
|
+
this.updateInputGain(props.drive);
|
|
102
|
+
this.updateDistortionCurve(props.drive);
|
|
103
|
+
this.filter.frequency.value = props.tone;
|
|
104
|
+
this.wetDryMixer.setMix(props.mix);
|
|
105
|
+
|
|
106
|
+
// Register IOs
|
|
107
|
+
this.registerDefaultIOs("in");
|
|
108
|
+
this.registerCustomOutput();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private registerCustomOutput() {
|
|
112
|
+
this.registerAudioOutput({
|
|
113
|
+
name: "out",
|
|
114
|
+
getAudioNode: () => this.outputNode,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private generateDistortionCurve(drive: number): Float32Array | null {
|
|
119
|
+
const samples = 65536; // High resolution for smooth distortion
|
|
120
|
+
const buffer = new ArrayBuffer(samples * Float32Array.BYTES_PER_ELEMENT);
|
|
121
|
+
const curve: Float32Array = new Float32Array(buffer) as Float32Array;
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < samples; i++) {
|
|
124
|
+
// Map sample index to input range [-1, 1]
|
|
125
|
+
const x = (i * 2) / samples - 1;
|
|
126
|
+
|
|
127
|
+
// Apply tanh waveshaping with drive scaling
|
|
128
|
+
// Higher drive values push more of the signal into saturation
|
|
129
|
+
const driven = x * drive;
|
|
130
|
+
curve[i] = Math.tanh(driven);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return curve;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private updateDistortionCurve(drive: number): void {
|
|
137
|
+
// @ts-expect-error - TypeScript strict mode issue with Float32Array<ArrayBufferLike> vs Float32Array<ArrayBuffer>
|
|
138
|
+
this.waveshaper.curve = this.generateDistortionCurve(drive);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private updateInputGain(drive: number): void {
|
|
142
|
+
// Exponential scaling: each increment doubles the gain
|
|
143
|
+
// drive=0 -> 1x, drive=1 -> 2x, drive=2 -> 4x, ..., drive=10 -> 1024x
|
|
144
|
+
this.inputGain.gain.value = Math.pow(2, drive);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
onAfterSetDrive: SetterHooks<IDistortionProps>["onAfterSetDrive"] = (
|
|
148
|
+
value,
|
|
149
|
+
) => {
|
|
150
|
+
this.updateInputGain(value);
|
|
151
|
+
this.updateDistortionCurve(value);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
onAfterSetTone: SetterHooks<IDistortionProps>["onAfterSetTone"] = (value) => {
|
|
155
|
+
this.filter.frequency.value = value;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
onAfterSetMix: SetterHooks<IDistortionProps>["onAfterSetMix"] = (value) => {
|
|
159
|
+
this.wetDryMixer.setMix(value);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default class PolyDistortion extends PolyModule<ModuleType.Distortion> {
|
|
164
|
+
constructor(
|
|
165
|
+
engineId: string,
|
|
166
|
+
params: IPolyModuleConstructor<ModuleType.Distortion>,
|
|
167
|
+
) {
|
|
168
|
+
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
169
|
+
const monoModuleConstructor = (
|
|
170
|
+
engineId: string,
|
|
171
|
+
params: IModuleConstructor<ModuleType.Distortion>,
|
|
172
|
+
) => Module.create(MonoDistortion, engineId, params);
|
|
173
|
+
|
|
174
|
+
super(engineId, {
|
|
175
|
+
...params,
|
|
176
|
+
props,
|
|
177
|
+
monoModuleConstructor,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
this.registerDefaultIOs();
|
|
181
|
+
}
|
|
182
|
+
}
|