@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
package/src/modules/Envelope.ts
CHANGED
|
@@ -1,69 +1,104 @@
|
|
|
1
1
|
import { ContextTime } from "@blibliki/transport";
|
|
2
|
-
import { Context
|
|
2
|
+
import { Context } from "@blibliki/utils";
|
|
3
3
|
import { GainNode } from "@blibliki/utils/web-audio-api";
|
|
4
4
|
import { Module } from "@/core";
|
|
5
5
|
import Note from "@/core/Note";
|
|
6
|
-
import { IModuleConstructor } from "@/core/module/Module";
|
|
6
|
+
import { IModuleConstructor, SetterHooks } from "@/core/module/Module";
|
|
7
7
|
import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
|
|
8
8
|
import { ModulePropSchema } from "@/core/schema";
|
|
9
|
+
import { CustomWorklet, newAudioWorklet } from "@/processors";
|
|
9
10
|
import { ICreateModule, ModuleType } from ".";
|
|
10
11
|
|
|
11
|
-
export type
|
|
12
|
+
export type ICustomEnvelopeProps = {
|
|
12
13
|
attack: number;
|
|
14
|
+
attackCurve: number;
|
|
13
15
|
decay: number;
|
|
14
16
|
sustain: number;
|
|
15
17
|
release: number;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
|
-
const DEFAULT_PROPS:
|
|
19
|
-
attack: 0.
|
|
20
|
-
|
|
20
|
+
const DEFAULT_PROPS: ICustomEnvelopeProps = {
|
|
21
|
+
attack: 0.1,
|
|
22
|
+
attackCurve: 0.5,
|
|
23
|
+
decay: 0.1,
|
|
21
24
|
sustain: 1,
|
|
22
|
-
release: 0,
|
|
25
|
+
release: 0.1,
|
|
23
26
|
};
|
|
24
27
|
|
|
25
|
-
export const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
28
|
+
export const customEnvelopePropSchema: ModulePropSchema<ICustomEnvelopeProps> =
|
|
29
|
+
{
|
|
30
|
+
attack: {
|
|
31
|
+
kind: "number",
|
|
32
|
+
min: 0,
|
|
33
|
+
max: 10,
|
|
34
|
+
step: 0.01,
|
|
35
|
+
exp: 7,
|
|
36
|
+
label: "Attack",
|
|
37
|
+
},
|
|
38
|
+
attackCurve: {
|
|
39
|
+
kind: "number",
|
|
40
|
+
min: 0,
|
|
41
|
+
max: 1,
|
|
42
|
+
step: 0.01,
|
|
43
|
+
label: "Attack Curve",
|
|
44
|
+
},
|
|
45
|
+
decay: {
|
|
46
|
+
kind: "number",
|
|
47
|
+
min: 0,
|
|
48
|
+
max: 10,
|
|
49
|
+
step: 0.01,
|
|
50
|
+
exp: 6.6,
|
|
51
|
+
label: "Decay",
|
|
52
|
+
},
|
|
53
|
+
sustain: {
|
|
54
|
+
kind: "number",
|
|
55
|
+
min: 0,
|
|
56
|
+
max: 1,
|
|
57
|
+
step: 0.01,
|
|
58
|
+
label: "Sustain",
|
|
59
|
+
},
|
|
60
|
+
release: {
|
|
61
|
+
kind: "number",
|
|
62
|
+
min: 0,
|
|
63
|
+
max: 10,
|
|
64
|
+
step: 0.01,
|
|
65
|
+
exp: 5,
|
|
66
|
+
label: "Release",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type CustomEnvelopeSetterHooks = SetterHooks<ICustomEnvelopeProps>;
|
|
71
|
+
|
|
72
|
+
class MonoCustomEnvelope
|
|
73
|
+
extends Module<ModuleType.Envelope>
|
|
74
|
+
implements
|
|
75
|
+
Pick<
|
|
76
|
+
CustomEnvelopeSetterHooks,
|
|
77
|
+
| "onAfterSetAttack"
|
|
78
|
+
| "onAfterSetAttackCurve"
|
|
79
|
+
| "onAfterSetDecay"
|
|
80
|
+
| "onAfterSetSustain"
|
|
81
|
+
| "onAfterSetRelease"
|
|
82
|
+
>
|
|
83
|
+
{
|
|
84
|
+
declare audioNode: ReturnType<typeof newAudioWorklet>;
|
|
85
|
+
private gainNode!: GainNode;
|
|
61
86
|
|
|
62
87
|
constructor(engineId: string, params: ICreateModule<ModuleType.Envelope>) {
|
|
63
88
|
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
64
89
|
const audioNodeConstructor = (context: Context) => {
|
|
65
|
-
const audioNode =
|
|
66
|
-
|
|
90
|
+
const audioNode = newAudioWorklet(
|
|
91
|
+
context,
|
|
92
|
+
CustomWorklet.CustomEnvelopeProcessor,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Set initial parameter values
|
|
96
|
+
audioNode.parameters.get("attack")!.value = props.attack;
|
|
97
|
+
audioNode.parameters.get("attackcurve")!.value = props.attackCurve;
|
|
98
|
+
audioNode.parameters.get("decay")!.value = props.decay;
|
|
99
|
+
audioNode.parameters.get("sustain")!.value = props.sustain;
|
|
100
|
+
audioNode.parameters.get("release")!.value = props.release;
|
|
101
|
+
|
|
67
102
|
return audioNode;
|
|
68
103
|
};
|
|
69
104
|
|
|
@@ -73,69 +108,100 @@ class MonoEnvelope extends Module<ModuleType.Envelope> {
|
|
|
73
108
|
audioNodeConstructor,
|
|
74
109
|
});
|
|
75
110
|
|
|
76
|
-
this.
|
|
111
|
+
this.gainNode = new GainNode(this.context.audioContext, {
|
|
112
|
+
gain: 0,
|
|
113
|
+
});
|
|
114
|
+
this.audioNode.connect(this.gainNode.gain);
|
|
115
|
+
|
|
116
|
+
this.registerIOs();
|
|
77
117
|
}
|
|
78
118
|
|
|
79
|
-
|
|
80
|
-
|
|
119
|
+
// AudioParam getters
|
|
120
|
+
get attackParam() {
|
|
121
|
+
return this.audioNode.parameters.get("attack")!;
|
|
122
|
+
}
|
|
81
123
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
124
|
+
get attackCurveParam() {
|
|
125
|
+
return this.audioNode.parameters.get("attackcurve")!;
|
|
126
|
+
}
|
|
85
127
|
|
|
86
|
-
|
|
128
|
+
get decayParam() {
|
|
129
|
+
return this.audioNode.parameters.get("decay")!;
|
|
130
|
+
}
|
|
87
131
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
132
|
+
get sustainParam() {
|
|
133
|
+
return this.audioNode.parameters.get("sustain")!;
|
|
134
|
+
}
|
|
92
135
|
|
|
93
|
-
|
|
94
|
-
this.audioNode.
|
|
136
|
+
get releaseParam() {
|
|
137
|
+
return this.audioNode.parameters.get("release")!;
|
|
138
|
+
}
|
|
95
139
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
get triggerParam() {
|
|
141
|
+
return this.audioNode.parameters.get("trigger")!;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Setter hooks that update AudioParams
|
|
145
|
+
onAfterSetAttack: CustomEnvelopeSetterHooks["onAfterSetAttack"] = (value) => {
|
|
146
|
+
this.attackParam.value = value;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
onAfterSetAttackCurve: CustomEnvelopeSetterHooks["onAfterSetAttackCurve"] = (
|
|
150
|
+
value,
|
|
151
|
+
) => {
|
|
152
|
+
this.attackCurveParam.value = value;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
onAfterSetDecay: CustomEnvelopeSetterHooks["onAfterSetDecay"] = (value) => {
|
|
156
|
+
this.decayParam.value = value;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
onAfterSetSustain: CustomEnvelopeSetterHooks["onAfterSetSustain"] = (
|
|
160
|
+
value,
|
|
161
|
+
) => {
|
|
162
|
+
this.sustainParam.value = value;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
onAfterSetRelease: CustomEnvelopeSetterHooks["onAfterSetRelease"] = (
|
|
166
|
+
value,
|
|
167
|
+
) => {
|
|
168
|
+
this.releaseParam.value = value;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
triggerAttack(note: Note, triggeredAt: ContextTime) {
|
|
172
|
+
super.triggerAttack(note, triggeredAt);
|
|
173
|
+
|
|
174
|
+
this.triggerParam.setValueAtTime(1, triggeredAt);
|
|
109
175
|
}
|
|
110
176
|
|
|
111
177
|
triggerRelease(note: Note, triggeredAt: ContextTime) {
|
|
112
178
|
super.triggerRelease(note, triggeredAt);
|
|
179
|
+
|
|
180
|
+
// Only release if this is the last active note
|
|
113
181
|
if (this.activeNotes.length > 0) return;
|
|
114
182
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (currentGainValue >= 0.0001) {
|
|
124
|
-
// Always set the value at the release time to ensure a smooth ramp from here
|
|
125
|
-
this.audioNode.gain.setValueAtTime(currentGainValue, triggeredAt);
|
|
126
|
-
// Exponential ramp to a tiny value
|
|
127
|
-
this.audioNode.gain.exponentialRampToValueAtTime(
|
|
128
|
-
0.0001,
|
|
129
|
-
triggeredAt + release - 0.0001,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
183
|
+
this.triggerParam.setValueAtTime(0, triggeredAt);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
dispose() {
|
|
187
|
+
this.gainNode.disconnect();
|
|
188
|
+
super.dispose();
|
|
189
|
+
}
|
|
132
190
|
|
|
133
|
-
|
|
134
|
-
this.
|
|
191
|
+
private registerIOs() {
|
|
192
|
+
this.registerAudioInput({
|
|
193
|
+
name: "in",
|
|
194
|
+
getAudioNode: () => this.gainNode,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
this.registerAudioOutput({
|
|
198
|
+
name: "out",
|
|
199
|
+
getAudioNode: () => this.gainNode,
|
|
200
|
+
});
|
|
135
201
|
}
|
|
136
202
|
}
|
|
137
203
|
|
|
138
|
-
export default class
|
|
204
|
+
export default class CustomEnvelope extends PolyModule<ModuleType.Envelope> {
|
|
139
205
|
constructor(
|
|
140
206
|
engineId: string,
|
|
141
207
|
params: IPolyModuleConstructor<ModuleType.Envelope>,
|
|
@@ -144,7 +210,7 @@ export default class Envelope extends PolyModule<ModuleType.Envelope> {
|
|
|
144
210
|
const monoModuleConstructor = (
|
|
145
211
|
engineId: string,
|
|
146
212
|
params: IModuleConstructor<ModuleType.Envelope>,
|
|
147
|
-
) =>
|
|
213
|
+
) => Module.create(MonoCustomEnvelope, engineId, params);
|
|
148
214
|
|
|
149
215
|
super(engineId, {
|
|
150
216
|
...params,
|
package/src/modules/Filter.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { BiquadFilterNode } from "@blibliki/utils/web-audio-api";
|
|
|
3
3
|
import { EnumProp, ModulePropSchema } from "@/core";
|
|
4
4
|
import { IModuleConstructor, Module, SetterHooks } from "@/core/module/Module";
|
|
5
5
|
import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
|
|
6
|
-
import {
|
|
6
|
+
import { ICreateModule, ModuleType } from ".";
|
|
7
7
|
import { MonoGain } from "./Gain";
|
|
8
|
-
import
|
|
8
|
+
import { MonoScale } from "./Scale";
|
|
9
9
|
|
|
10
10
|
export type IFilterProps = {
|
|
11
11
|
cutoff: number;
|
|
@@ -72,7 +72,7 @@ class MonoFilter
|
|
|
72
72
|
>
|
|
73
73
|
{
|
|
74
74
|
declare audioNode: BiquadFilterNode;
|
|
75
|
-
private scale:
|
|
75
|
+
private scale: MonoScale;
|
|
76
76
|
private amount: MonoGain;
|
|
77
77
|
|
|
78
78
|
constructor(engineId: string, params: ICreateModule<ModuleType.Filter>) {
|
|
@@ -91,17 +91,17 @@ class MonoFilter
|
|
|
91
91
|
audioNodeConstructor,
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
this.amount =
|
|
94
|
+
this.amount = Module.create(MonoGain, engineId, {
|
|
95
95
|
name: "amount",
|
|
96
96
|
moduleType: ModuleType.Gain,
|
|
97
97
|
props: { gain: props.envelopeAmount },
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
this.scale =
|
|
100
|
+
this.scale = Module.create(MonoScale, engineId, {
|
|
101
101
|
name: "scale",
|
|
102
102
|
moduleType: ModuleType.Scale,
|
|
103
103
|
props: { min: MIN_FREQ, max: MAX_FREQ, current: this.props.cutoff },
|
|
104
|
-
})
|
|
104
|
+
});
|
|
105
105
|
|
|
106
106
|
this.amount.plug({ audioModule: this.scale, from: "out", to: "in" });
|
|
107
107
|
this.scale.audioNode.connect(this.audioNode.frequency);
|
|
@@ -154,7 +154,7 @@ export default class Filter extends PolyModule<ModuleType.Filter> {
|
|
|
154
154
|
const monoModuleConstructor = (
|
|
155
155
|
engineId: string,
|
|
156
156
|
params: IModuleConstructor<ModuleType.Filter>,
|
|
157
|
-
) =>
|
|
157
|
+
) => Module.create(MonoFilter, engineId, params);
|
|
158
158
|
|
|
159
159
|
super(engineId, {
|
|
160
160
|
...params,
|
package/src/modules/Gain.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const gainPropSchema: ModulePropSchema<IGainProps> = {
|
|
|
14
14
|
gain: {
|
|
15
15
|
kind: "number",
|
|
16
16
|
min: 0,
|
|
17
|
-
max:
|
|
17
|
+
max: 2,
|
|
18
18
|
step: 0.01,
|
|
19
19
|
label: "Gain",
|
|
20
20
|
},
|
|
@@ -64,7 +64,7 @@ export default class Gain extends PolyModule<ModuleType.Gain> {
|
|
|
64
64
|
const monoModuleConstructor = (
|
|
65
65
|
engineId: string,
|
|
66
66
|
params: IModuleConstructor<ModuleType.Gain>,
|
|
67
|
-
) =>
|
|
67
|
+
) => Module.create(MonoGain, engineId, params);
|
|
68
68
|
|
|
69
69
|
super(engineId, {
|
|
70
70
|
...params,
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { Division, divisionToFrequency } from "@blibliki/transport";
|
|
2
|
+
import { Context } from "@blibliki/utils";
|
|
3
|
+
import { ConstantSourceNode, GainNode } from "@blibliki/utils/web-audio-api";
|
|
4
|
+
import { IModule, Module } from "@/core";
|
|
5
|
+
import { IModuleConstructor, SetterHooks } from "@/core/module/Module";
|
|
6
|
+
import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
|
|
7
|
+
import { EnumProp, ModulePropSchema } from "@/core/schema";
|
|
8
|
+
import { CustomWorklet, newAudioWorklet } from "@/processors";
|
|
9
|
+
import { ICreateModule, ModuleType } from ".";
|
|
10
|
+
|
|
11
|
+
export type ILFO = IModule<ModuleType.LFO>;
|
|
12
|
+
|
|
13
|
+
export enum LFOWaveform {
|
|
14
|
+
sine = "sine",
|
|
15
|
+
triangle = "triangle",
|
|
16
|
+
square = "square",
|
|
17
|
+
sawtooth = "sawtooth",
|
|
18
|
+
rampDown = "rampDown",
|
|
19
|
+
random = "random",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DIVISIONS: Division[] = [
|
|
23
|
+
"1/64",
|
|
24
|
+
"1/48",
|
|
25
|
+
"1/32",
|
|
26
|
+
"1/24",
|
|
27
|
+
"1/16",
|
|
28
|
+
"1/12",
|
|
29
|
+
"1/8",
|
|
30
|
+
"1/6",
|
|
31
|
+
"3/16",
|
|
32
|
+
"1/4",
|
|
33
|
+
"5/16",
|
|
34
|
+
"1/3",
|
|
35
|
+
"3/8",
|
|
36
|
+
"1/2",
|
|
37
|
+
"3/4",
|
|
38
|
+
"1",
|
|
39
|
+
"1.5",
|
|
40
|
+
"2",
|
|
41
|
+
"3",
|
|
42
|
+
"4",
|
|
43
|
+
"6",
|
|
44
|
+
"8",
|
|
45
|
+
"16",
|
|
46
|
+
"32",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export type ILFOProps = {
|
|
50
|
+
sync: boolean;
|
|
51
|
+
frequency: number;
|
|
52
|
+
division: Division;
|
|
53
|
+
waveform: LFOWaveform;
|
|
54
|
+
offset: number;
|
|
55
|
+
amount: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const DEFAULT_PROPS: ILFOProps = {
|
|
59
|
+
sync: false,
|
|
60
|
+
frequency: 1.0,
|
|
61
|
+
division: "1/4",
|
|
62
|
+
waveform: LFOWaveform.sine,
|
|
63
|
+
offset: 0,
|
|
64
|
+
amount: 1,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const lfoPropSchema: ModulePropSchema<
|
|
68
|
+
ILFOProps,
|
|
69
|
+
{
|
|
70
|
+
division: EnumProp<Division>;
|
|
71
|
+
waveform: EnumProp<LFOWaveform>;
|
|
72
|
+
}
|
|
73
|
+
> = {
|
|
74
|
+
sync: {
|
|
75
|
+
kind: "boolean",
|
|
76
|
+
label: "Sync",
|
|
77
|
+
},
|
|
78
|
+
frequency: {
|
|
79
|
+
kind: "number",
|
|
80
|
+
min: 0.01,
|
|
81
|
+
max: 40,
|
|
82
|
+
step: 0.01,
|
|
83
|
+
exp: 3,
|
|
84
|
+
label: "Frequency (Hz)",
|
|
85
|
+
},
|
|
86
|
+
division: {
|
|
87
|
+
kind: "enum",
|
|
88
|
+
options: DIVISIONS,
|
|
89
|
+
label: "Division",
|
|
90
|
+
},
|
|
91
|
+
waveform: {
|
|
92
|
+
kind: "enum",
|
|
93
|
+
options: Object.values(LFOWaveform),
|
|
94
|
+
label: "Waveform",
|
|
95
|
+
},
|
|
96
|
+
offset: {
|
|
97
|
+
kind: "number",
|
|
98
|
+
min: -1,
|
|
99
|
+
max: 1,
|
|
100
|
+
step: 0.01,
|
|
101
|
+
label: "Offset",
|
|
102
|
+
},
|
|
103
|
+
amount: {
|
|
104
|
+
kind: "number",
|
|
105
|
+
min: 0,
|
|
106
|
+
max: 1,
|
|
107
|
+
step: 0.01,
|
|
108
|
+
label: "Amount",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
type LFOSetterHooks = SetterHooks<ILFOProps>;
|
|
113
|
+
|
|
114
|
+
export class MonoLFO
|
|
115
|
+
extends Module<ModuleType.LFO>
|
|
116
|
+
implements
|
|
117
|
+
Pick<
|
|
118
|
+
LFOSetterHooks,
|
|
119
|
+
| "onAfterSetSync"
|
|
120
|
+
| "onAfterSetFrequency"
|
|
121
|
+
| "onAfterSetDivision"
|
|
122
|
+
| "onAfterSetWaveform"
|
|
123
|
+
| "onAfterSetOffset"
|
|
124
|
+
| "onAfterSetAmount"
|
|
125
|
+
>
|
|
126
|
+
{
|
|
127
|
+
declare audioNode: AudioWorkletNode;
|
|
128
|
+
private offsetConstant!: ConstantSourceNode;
|
|
129
|
+
private offsetGain!: GainNode;
|
|
130
|
+
private amplitudeGain!: GainNode;
|
|
131
|
+
private mixerGain!: GainNode;
|
|
132
|
+
private amountGain!: GainNode;
|
|
133
|
+
|
|
134
|
+
constructor(engineId: string, params: ICreateModule<ModuleType.LFO>) {
|
|
135
|
+
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
136
|
+
const audioNodeConstructor = (context: Context) =>
|
|
137
|
+
newAudioWorklet(context, CustomWorklet.LFOProcessor);
|
|
138
|
+
|
|
139
|
+
super(engineId, {
|
|
140
|
+
...params,
|
|
141
|
+
props,
|
|
142
|
+
audioNodeConstructor,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
this.setupAudioGraph();
|
|
146
|
+
this.setupBPMListener();
|
|
147
|
+
this.registerOutputs();
|
|
148
|
+
this.updateFrequency();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private setupAudioGraph() {
|
|
152
|
+
const ctx = this.context.audioContext;
|
|
153
|
+
|
|
154
|
+
// Create nodes
|
|
155
|
+
this.offsetConstant = new ConstantSourceNode(ctx, { offset: 1 });
|
|
156
|
+
this.offsetGain = new GainNode(ctx, { gain: 0 });
|
|
157
|
+
this.amplitudeGain = new GainNode(ctx, { gain: 1 });
|
|
158
|
+
this.mixerGain = new GainNode(ctx, { gain: 1 });
|
|
159
|
+
this.amountGain = new GainNode(ctx, { gain: this.props.amount });
|
|
160
|
+
|
|
161
|
+
// Set initial waveform
|
|
162
|
+
const waveformIndex = Object.values(LFOWaveform).indexOf(
|
|
163
|
+
this.props.waveform,
|
|
164
|
+
);
|
|
165
|
+
this.waveformParam.value = waveformIndex;
|
|
166
|
+
|
|
167
|
+
// Apply offset calculation
|
|
168
|
+
this.updateOffsetGains();
|
|
169
|
+
|
|
170
|
+
// Connect audio graph:
|
|
171
|
+
// LFO → amplitudeGain → mixerGain
|
|
172
|
+
this.audioNode.connect(this.amplitudeGain);
|
|
173
|
+
this.amplitudeGain.connect(this.mixerGain);
|
|
174
|
+
|
|
175
|
+
// ConstantSource(1) → offsetGain → mixerGain
|
|
176
|
+
this.offsetConstant.connect(this.offsetGain);
|
|
177
|
+
this.offsetGain.connect(this.mixerGain);
|
|
178
|
+
|
|
179
|
+
// mixerGain → amountGain → (output registered separately)
|
|
180
|
+
this.mixerGain.connect(this.amountGain);
|
|
181
|
+
|
|
182
|
+
// Start constant source
|
|
183
|
+
this.offsetConstant.start();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private setupBPMListener() {
|
|
187
|
+
this.engine.transport.addPropertyChangeCallback("bpm", () => {
|
|
188
|
+
if (!this.props.sync) return;
|
|
189
|
+
|
|
190
|
+
this.updateFrequencyFromBPM();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private registerOutputs() {
|
|
195
|
+
this.registerAudioOutput({
|
|
196
|
+
name: "out",
|
|
197
|
+
getAudioNode: () => this.amountGain,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private updateFrequency() {
|
|
202
|
+
if (this.props.sync) {
|
|
203
|
+
this.updateFrequencyFromBPM();
|
|
204
|
+
} else {
|
|
205
|
+
this.frequencyParam.value = this.props.frequency;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private updateFrequencyFromBPM() {
|
|
210
|
+
const bpm = this.engine.transport.bpm;
|
|
211
|
+
const frequency = divisionToFrequency(this.props.division, bpm);
|
|
212
|
+
this.frequencyParam.value = frequency;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private updateOffsetGains() {
|
|
216
|
+
const offset = this.props.offset;
|
|
217
|
+
// Formula: output = lfo_signal * (1 - |offset|/2) + offset/2
|
|
218
|
+
const amplitude = 1 - Math.abs(offset) / 2;
|
|
219
|
+
const dcOffset = offset / 2;
|
|
220
|
+
|
|
221
|
+
this.amplitudeGain.gain.value = amplitude;
|
|
222
|
+
this.offsetGain.gain.value = dcOffset;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
get frequencyParam() {
|
|
226
|
+
return this.audioNode.parameters.get("frequency")!;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
get waveformParam() {
|
|
230
|
+
return this.audioNode.parameters.get("waveform")!;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
get phaseParam() {
|
|
234
|
+
return this.audioNode.parameters.get("phase")!;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
onAfterSetSync: LFOSetterHooks["onAfterSetSync"] = () => {
|
|
238
|
+
this.updateFrequency();
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
onAfterSetFrequency: LFOSetterHooks["onAfterSetFrequency"] = (value) => {
|
|
242
|
+
this.frequencyParam.value = value;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
onAfterSetDivision: LFOSetterHooks["onAfterSetDivision"] = () => {
|
|
246
|
+
this.updateFrequencyFromBPM();
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
onAfterSetWaveform: LFOSetterHooks["onAfterSetWaveform"] = (value) => {
|
|
250
|
+
const waveformIndex = Object.values(LFOWaveform).indexOf(value);
|
|
251
|
+
this.waveformParam.value = waveformIndex;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
onAfterSetOffset: LFOSetterHooks["onAfterSetOffset"] = () => {
|
|
255
|
+
this.updateOffsetGains();
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
onAfterSetAmount: LFOSetterHooks["onAfterSetAmount"] = (value) => {
|
|
259
|
+
this.amountGain.gain.value = value;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
dispose() {
|
|
263
|
+
this.offsetConstant.stop();
|
|
264
|
+
super.dispose();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export default class LFO extends PolyModule<ModuleType.LFO> {
|
|
269
|
+
constructor(
|
|
270
|
+
engineId: string,
|
|
271
|
+
params: IPolyModuleConstructor<ModuleType.LFO>,
|
|
272
|
+
) {
|
|
273
|
+
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
274
|
+
const monoModuleConstructor = (
|
|
275
|
+
engineId: string,
|
|
276
|
+
params: IModuleConstructor<ModuleType.LFO>,
|
|
277
|
+
) => Module.create(MonoLFO, engineId, params);
|
|
278
|
+
|
|
279
|
+
super(engineId, {
|
|
280
|
+
...params,
|
|
281
|
+
props,
|
|
282
|
+
monoModuleConstructor,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
this.registerDefaultIOs("out");
|
|
286
|
+
}
|
|
287
|
+
}
|