@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.
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
@@ -1,69 +1,104 @@
1
1
  import { ContextTime } from "@blibliki/transport";
2
- import { Context, cancelAndHoldAtTime } from "@blibliki/utils";
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 IEnvelopeProps = {
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: IEnvelopeProps = {
19
- attack: 0.01,
20
- decay: 0,
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 envelopePropSchema: ModulePropSchema<IEnvelopeProps> = {
26
- attack: {
27
- kind: "number",
28
- min: 0.0001,
29
- max: 20,
30
- step: 0.01,
31
- exp: 3,
32
- label: "Attack",
33
- },
34
- decay: {
35
- kind: "number",
36
- min: 0,
37
- max: 20,
38
- step: 0.01,
39
- exp: 3,
40
- label: "Decay",
41
- },
42
- sustain: {
43
- kind: "number",
44
- min: 0,
45
- max: 1,
46
- step: 0.01,
47
- label: "Sustain",
48
- },
49
- release: {
50
- kind: "number",
51
- min: 0,
52
- max: 20,
53
- step: 0.01,
54
- exp: 3,
55
- label: "Release",
56
- },
57
- };
58
-
59
- class MonoEnvelope extends Module<ModuleType.Envelope> {
60
- declare audioNode: GainNode;
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 = new GainNode(context.audioContext);
66
- audioNode.gain.value = 0;
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.registerDefaultIOs();
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
- triggerAttack(note: Note, triggeredAt: ContextTime) {
80
- super.triggerAttack(note, triggeredAt);
119
+ // AudioParam getters
120
+ get attackParam() {
121
+ return this.audioNode.parameters.get("attack")!;
122
+ }
81
123
 
82
- const attack = this.props.attack;
83
- const decay = this.props.decay;
84
- const sustain = this.props.sustain;
124
+ get attackCurveParam() {
125
+ return this.audioNode.parameters.get("attackcurve")!;
126
+ }
85
127
 
86
- cancelAndHoldAtTime(this.audioNode.gain, triggeredAt);
128
+ get decayParam() {
129
+ return this.audioNode.parameters.get("decay")!;
130
+ }
87
131
 
88
- // Always start from a tiny value, can't ramp from 0
89
- if (this.audioNode.gain.value === 0) {
90
- this.audioNode.gain.setValueAtTime(0.001, triggeredAt);
91
- }
132
+ get sustainParam() {
133
+ return this.audioNode.parameters.get("sustain")!;
134
+ }
92
135
 
93
- // Attack
94
- this.audioNode.gain.exponentialRampToValueAtTime(1.0, triggeredAt + attack);
136
+ get releaseParam() {
137
+ return this.audioNode.parameters.get("release")!;
138
+ }
95
139
 
96
- // Decay
97
- if (sustain > 0) {
98
- this.audioNode.gain.exponentialRampToValueAtTime(
99
- sustain,
100
- triggeredAt + attack + decay,
101
- );
102
- // Do not set to zero or anything else!
103
- } else {
104
- this.audioNode.gain.exponentialRampToValueAtTime(
105
- 0.001,
106
- triggeredAt + attack + decay,
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
- const release = this.props.release;
116
-
117
- // Cancel scheduled automations and set gain to the ACTUAL value at this moment
118
- const currentGainValue = cancelAndHoldAtTime(
119
- this.audioNode.gain,
120
- triggeredAt,
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
- // Set to zero at the very end
134
- this.audioNode.gain.setValueAtTime(0, triggeredAt + release);
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 Envelope extends PolyModule<ModuleType.Envelope> {
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
- ) => new MonoEnvelope(engineId, params);
213
+ ) => Module.create(MonoCustomEnvelope, engineId, params);
148
214
 
149
215
  super(engineId, {
150
216
  ...params,
@@ -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 { createModule, ICreateModule, ModuleType } from ".";
6
+ import { ICreateModule, ModuleType } from ".";
7
7
  import { MonoGain } from "./Gain";
8
- import Scale from "./Scale";
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: 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 = new MonoGain(engineId, {
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 = createModule(engineId, {
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
- }) as Scale;
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
- ) => new MonoFilter(engineId, params);
157
+ ) => Module.create(MonoFilter, engineId, params);
158
158
 
159
159
  super(engineId, {
160
160
  ...params,
@@ -14,7 +14,7 @@ export const gainPropSchema: ModulePropSchema<IGainProps> = {
14
14
  gain: {
15
15
  kind: "number",
16
16
  min: 0,
17
- max: Infinity,
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
- ) => new MonoGain(engineId, params);
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
+ }