@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
package/src/modules/Scale.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Context } from "@blibliki/utils";
|
|
2
2
|
import { IModule, Module, SetterHooks } from "@/core";
|
|
3
|
+
import { IModuleConstructor } from "@/core/module/Module";
|
|
4
|
+
import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
|
|
3
5
|
import { ModulePropSchema } from "@/core/schema";
|
|
4
6
|
import { CustomWorklet, newAudioWorklet } from "@/processors";
|
|
5
7
|
import { ICreateModule, ModuleType } from ".";
|
|
@@ -9,9 +11,13 @@ export type IScaleProps = {
|
|
|
9
11
|
min: number;
|
|
10
12
|
max: number;
|
|
11
13
|
current: number;
|
|
14
|
+
mode: "exponential" | "linear";
|
|
12
15
|
};
|
|
13
16
|
|
|
14
|
-
export const scalePropSchema: ModulePropSchema<
|
|
17
|
+
export const scalePropSchema: ModulePropSchema<
|
|
18
|
+
IScaleProps,
|
|
19
|
+
{ mode: import("@/core/schema").EnumProp<"exponential" | "linear"> }
|
|
20
|
+
> = {
|
|
15
21
|
min: {
|
|
16
22
|
kind: "number",
|
|
17
23
|
min: -Infinity,
|
|
@@ -33,16 +39,26 @@ export const scalePropSchema: ModulePropSchema<IScaleProps> = {
|
|
|
33
39
|
step: 0.01,
|
|
34
40
|
label: "Current",
|
|
35
41
|
},
|
|
42
|
+
mode: {
|
|
43
|
+
kind: "enum",
|
|
44
|
+
label: "Mode",
|
|
45
|
+
options: ["exponential", "linear"],
|
|
46
|
+
},
|
|
36
47
|
};
|
|
37
48
|
|
|
38
|
-
const DEFAULT_PROPS: IScaleProps = {
|
|
49
|
+
const DEFAULT_PROPS: IScaleProps = {
|
|
50
|
+
min: 0,
|
|
51
|
+
max: 1,
|
|
52
|
+
current: 0.5,
|
|
53
|
+
mode: "exponential",
|
|
54
|
+
};
|
|
39
55
|
|
|
40
|
-
export
|
|
56
|
+
export class MonoScale
|
|
41
57
|
extends Module<ModuleType.Scale>
|
|
42
58
|
implements
|
|
43
59
|
Pick<
|
|
44
60
|
SetterHooks<IScaleProps>,
|
|
45
|
-
"onAfterSetMin" | "onAfterSetMax" | "onAfterSetCurrent"
|
|
61
|
+
"onAfterSetMin" | "onAfterSetMax" | "onAfterSetCurrent" | "onAfterSetMode"
|
|
46
62
|
>
|
|
47
63
|
{
|
|
48
64
|
declare audioNode: AudioWorkletNode;
|
|
@@ -73,6 +89,10 @@ export default class Scale
|
|
|
73
89
|
return this.audioNode.parameters.get("max")!;
|
|
74
90
|
}
|
|
75
91
|
|
|
92
|
+
get mode() {
|
|
93
|
+
return this.audioNode.parameters.get("mode")!;
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
onAfterSetMin: SetterHooks<IScaleProps>["onAfterSetMin"] = (value) => {
|
|
77
97
|
this.min.value = value;
|
|
78
98
|
};
|
|
@@ -86,4 +106,29 @@ export default class Scale
|
|
|
86
106
|
) => {
|
|
87
107
|
this.current.value = value;
|
|
88
108
|
};
|
|
109
|
+
|
|
110
|
+
onAfterSetMode: SetterHooks<IScaleProps>["onAfterSetMode"] = (value) => {
|
|
111
|
+
this.mode.value = value === "exponential" ? 0 : 1;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default class Scale extends PolyModule<ModuleType.Scale> {
|
|
116
|
+
constructor(
|
|
117
|
+
engineId: string,
|
|
118
|
+
params: IPolyModuleConstructor<ModuleType.Scale>,
|
|
119
|
+
) {
|
|
120
|
+
const props = { ...DEFAULT_PROPS, ...params.props };
|
|
121
|
+
const monoModuleConstructor = (
|
|
122
|
+
engineId: string,
|
|
123
|
+
params: IModuleConstructor<ModuleType.Scale>,
|
|
124
|
+
) => Module.create(MonoScale, engineId, params);
|
|
125
|
+
|
|
126
|
+
super(engineId, {
|
|
127
|
+
...params,
|
|
128
|
+
props,
|
|
129
|
+
monoModuleConstructor,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
this.registerDefaultIOs();
|
|
133
|
+
}
|
|
89
134
|
}
|
|
@@ -1,51 +1,227 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ContextTime,
|
|
3
|
+
Division,
|
|
4
|
+
divisionToMilliseconds,
|
|
5
|
+
TPB,
|
|
6
|
+
StepSequencerSource,
|
|
7
|
+
StepSequencerSourceEvent,
|
|
8
|
+
Resolution,
|
|
9
|
+
PlaybackMode,
|
|
10
|
+
IStep,
|
|
11
|
+
IStepNote,
|
|
12
|
+
IStepCC,
|
|
13
|
+
IPage,
|
|
14
|
+
IPattern,
|
|
15
|
+
} from "@blibliki/transport";
|
|
16
|
+
import {
|
|
17
|
+
Module,
|
|
18
|
+
IModule,
|
|
19
|
+
MidiOutput,
|
|
20
|
+
Note,
|
|
21
|
+
ModulePropSchema,
|
|
22
|
+
EnumProp,
|
|
23
|
+
SetterHooks,
|
|
24
|
+
} from "@/core";
|
|
25
|
+
import MidiEvent from "@/core/midi/MidiEvent";
|
|
2
26
|
import { ICreateModule, ModuleType } from ".";
|
|
3
27
|
|
|
4
28
|
export type IStepSequencer = IModule<ModuleType.StepSequencer>;
|
|
5
29
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
duration: string;
|
|
10
|
-
notes: INote[];
|
|
11
|
-
};
|
|
30
|
+
// Re-export types from transport for backward compatibility
|
|
31
|
+
export type { IStep, IStepNote, IStepCC, IPage, IPattern };
|
|
32
|
+
export { Resolution, PlaybackMode };
|
|
12
33
|
|
|
34
|
+
// Module props (serialized)
|
|
13
35
|
export type IStepSequencerProps = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
patterns: IPattern[];
|
|
37
|
+
activePatternNo: number; // Currently selected pattern index
|
|
38
|
+
activePageNo: number; // Currently selected page within pattern
|
|
39
|
+
stepsPerPage: number; // 1-16 steps per page
|
|
40
|
+
resolution: Resolution; // Step resolution (16th, 8th, etc.)
|
|
41
|
+
playbackMode: PlaybackMode; // loop or oneShot
|
|
42
|
+
patternSequence: string; // Pattern sequence notation (e.g., "2A4B2AC")
|
|
43
|
+
enableSequence: boolean; // Toggle to enable/disable sequence mode
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Module state (temporal/runtime only, not serialized)
|
|
47
|
+
export type IStepSequencerState = {
|
|
48
|
+
isRunning: boolean;
|
|
49
|
+
currentStep: number; // For UI indicator
|
|
50
|
+
sequencePosition?: string; // UI display: "A (2/2)"
|
|
17
51
|
};
|
|
18
52
|
|
|
53
|
+
const MICROTIMING_STEP = TPB / 4 / 10;
|
|
54
|
+
|
|
19
55
|
export const stepSequencerPropSchema: ModulePropSchema<
|
|
20
|
-
|
|
56
|
+
Pick<
|
|
57
|
+
IStepSequencerProps,
|
|
58
|
+
| "activePatternNo"
|
|
59
|
+
| "activePageNo"
|
|
60
|
+
| "stepsPerPage"
|
|
61
|
+
| "resolution"
|
|
62
|
+
| "playbackMode"
|
|
63
|
+
| "patternSequence"
|
|
64
|
+
| "enableSequence"
|
|
65
|
+
>,
|
|
66
|
+
{
|
|
67
|
+
resolution: EnumProp<Resolution>;
|
|
68
|
+
playbackMode: EnumProp<PlaybackMode>;
|
|
69
|
+
}
|
|
21
70
|
> = {
|
|
22
|
-
|
|
71
|
+
activePatternNo: {
|
|
23
72
|
kind: "number",
|
|
24
|
-
|
|
25
|
-
|
|
73
|
+
label: "Active pattern",
|
|
74
|
+
min: 0,
|
|
75
|
+
max: 100,
|
|
76
|
+
step: 1,
|
|
77
|
+
},
|
|
78
|
+
activePageNo: {
|
|
79
|
+
kind: "number",
|
|
80
|
+
label: "Active page",
|
|
81
|
+
min: 0,
|
|
82
|
+
max: 100,
|
|
26
83
|
step: 1,
|
|
27
|
-
label: "Steps",
|
|
28
84
|
},
|
|
29
|
-
|
|
85
|
+
stepsPerPage: {
|
|
30
86
|
kind: "number",
|
|
31
87
|
min: 1,
|
|
32
88
|
max: 16,
|
|
33
89
|
step: 1,
|
|
34
|
-
label: "Steps",
|
|
90
|
+
label: "Steps per Page",
|
|
91
|
+
},
|
|
92
|
+
resolution: {
|
|
93
|
+
kind: "enum",
|
|
94
|
+
options: Object.values(Resolution),
|
|
95
|
+
label: "Resolution",
|
|
96
|
+
},
|
|
97
|
+
playbackMode: {
|
|
98
|
+
kind: "enum",
|
|
99
|
+
options: Object.values(PlaybackMode),
|
|
100
|
+
label: "Playback Mode",
|
|
101
|
+
},
|
|
102
|
+
patternSequence: {
|
|
103
|
+
kind: "string",
|
|
104
|
+
label: "Pattern Sequence",
|
|
105
|
+
},
|
|
106
|
+
enableSequence: {
|
|
107
|
+
kind: "boolean",
|
|
108
|
+
label: "Enable Sequence",
|
|
35
109
|
},
|
|
36
110
|
};
|
|
37
111
|
|
|
112
|
+
const NOTE_DIVISIONS: Division[] = [
|
|
113
|
+
"1/64",
|
|
114
|
+
"1/48",
|
|
115
|
+
"1/32",
|
|
116
|
+
"1/24",
|
|
117
|
+
"1/16",
|
|
118
|
+
"1/12",
|
|
119
|
+
"1/8",
|
|
120
|
+
"1/6",
|
|
121
|
+
"3/16",
|
|
122
|
+
"1/4",
|
|
123
|
+
"5/16",
|
|
124
|
+
"1/3",
|
|
125
|
+
"3/8",
|
|
126
|
+
"1/2",
|
|
127
|
+
"3/4",
|
|
128
|
+
"1",
|
|
129
|
+
"1.5",
|
|
130
|
+
"2",
|
|
131
|
+
"3",
|
|
132
|
+
"4",
|
|
133
|
+
"6",
|
|
134
|
+
"8",
|
|
135
|
+
"16",
|
|
136
|
+
"32",
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
export const stepPropSchema: ModulePropSchema<
|
|
140
|
+
Pick<IStep, "probability" | "duration" | "microtimeOffset">,
|
|
141
|
+
{
|
|
142
|
+
duration: EnumProp<Division>;
|
|
143
|
+
}
|
|
144
|
+
> = {
|
|
145
|
+
probability: {
|
|
146
|
+
kind: "number",
|
|
147
|
+
label: "Probability",
|
|
148
|
+
min: 0,
|
|
149
|
+
max: 100,
|
|
150
|
+
step: 1,
|
|
151
|
+
},
|
|
152
|
+
duration: {
|
|
153
|
+
kind: "enum",
|
|
154
|
+
label: "Duration",
|
|
155
|
+
options: NOTE_DIVISIONS,
|
|
156
|
+
},
|
|
157
|
+
microtimeOffset: {
|
|
158
|
+
kind: "number",
|
|
159
|
+
label: "Microtiming",
|
|
160
|
+
min: -100,
|
|
161
|
+
max: 100,
|
|
162
|
+
step: 1,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Create a default empty step
|
|
167
|
+
const createDefaultStep = (): IStep => ({
|
|
168
|
+
active: false,
|
|
169
|
+
notes: [],
|
|
170
|
+
ccMessages: [],
|
|
171
|
+
probability: 100,
|
|
172
|
+
microtimeOffset: 0,
|
|
173
|
+
duration: "1/16",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Create a default page with 16 empty steps
|
|
177
|
+
const createDefaultPage = (name: string): IPage => ({
|
|
178
|
+
name,
|
|
179
|
+
steps: Array.from({ length: 16 }, () => createDefaultStep()),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Create a default pattern with one page
|
|
183
|
+
const createDefaultPattern = (name: string): IPattern => ({
|
|
184
|
+
name,
|
|
185
|
+
pages: [createDefaultPage("Page 1")],
|
|
186
|
+
});
|
|
187
|
+
|
|
38
188
|
const DEFAULT_PROPS: IStepSequencerProps = {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
189
|
+
patterns: [createDefaultPattern("A")],
|
|
190
|
+
activePatternNo: 0,
|
|
191
|
+
activePageNo: 0,
|
|
192
|
+
stepsPerPage: 16,
|
|
193
|
+
resolution: Resolution.sixteenth,
|
|
194
|
+
playbackMode: PlaybackMode.loop,
|
|
195
|
+
patternSequence: "",
|
|
196
|
+
enableSequence: false,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const DEFAULT_STATE: IStepSequencerState = {
|
|
200
|
+
isRunning: false,
|
|
201
|
+
currentStep: 0,
|
|
202
|
+
sequencePosition: undefined,
|
|
42
203
|
};
|
|
43
204
|
|
|
44
|
-
|
|
45
|
-
|
|
205
|
+
type StepSequencerSetterHooks = Pick<
|
|
206
|
+
SetterHooks<IStepSequencerProps>,
|
|
207
|
+
| "onSetActivePatternNo"
|
|
208
|
+
| "onAfterSetPatternSequence"
|
|
209
|
+
| "onAfterSetPatterns"
|
|
210
|
+
| "onAfterSetResolution"
|
|
211
|
+
| "onAfterSetPlaybackMode"
|
|
212
|
+
| "onAfterSetEnableSequence"
|
|
213
|
+
>;
|
|
214
|
+
|
|
215
|
+
export default class StepSequencer
|
|
216
|
+
extends Module<ModuleType.StepSequencer>
|
|
217
|
+
implements StepSequencerSetterHooks
|
|
218
|
+
{
|
|
46
219
|
declare audioNode: undefined;
|
|
47
220
|
midiOutput!: MidiOutput;
|
|
48
221
|
|
|
222
|
+
private scheduledNotes = new Map<string, ContextTime>(); // Track scheduled note-offs
|
|
223
|
+
private source?: StepSequencerSource;
|
|
224
|
+
|
|
49
225
|
constructor(
|
|
50
226
|
engineId: string,
|
|
51
227
|
params: ICreateModule<ModuleType.StepSequencer>,
|
|
@@ -56,5 +232,217 @@ export default class StepSequencer extends Module<ModuleType.StepSequencer> {
|
|
|
56
232
|
...params,
|
|
57
233
|
props,
|
|
58
234
|
});
|
|
235
|
+
|
|
236
|
+
// Initialize state
|
|
237
|
+
this._state = { ...DEFAULT_STATE };
|
|
238
|
+
|
|
239
|
+
this.registerOutputs();
|
|
240
|
+
this.initializeSource();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
onSetActivePatternNo: StepSequencerSetterHooks["onSetActivePatternNo"] = (
|
|
244
|
+
value,
|
|
245
|
+
) => {
|
|
246
|
+
return Math.max(Math.min(value, this.props.patterns.length - 1), 0);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
onAfterSetPatternSequence: StepSequencerSetterHooks["onAfterSetPatternSequence"] =
|
|
250
|
+
(value) => {
|
|
251
|
+
if (!this.source) return;
|
|
252
|
+
|
|
253
|
+
this.source.props = {
|
|
254
|
+
...this.source.props,
|
|
255
|
+
patternSequence: value,
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
onAfterSetPatterns: StepSequencerSetterHooks["onAfterSetPatterns"] = (
|
|
260
|
+
value,
|
|
261
|
+
) => {
|
|
262
|
+
if (!this.source) return;
|
|
263
|
+
|
|
264
|
+
this.source.props = {
|
|
265
|
+
...this.source.props,
|
|
266
|
+
patterns: value,
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
onAfterSetResolution: StepSequencerSetterHooks["onAfterSetResolution"] = (
|
|
271
|
+
value,
|
|
272
|
+
) => {
|
|
273
|
+
if (!this.source) return;
|
|
274
|
+
|
|
275
|
+
this.source.props = {
|
|
276
|
+
...this.source.props,
|
|
277
|
+
resolution: value,
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
onAfterSetPlaybackMode: StepSequencerSetterHooks["onAfterSetPlaybackMode"] = (
|
|
282
|
+
value,
|
|
283
|
+
) => {
|
|
284
|
+
if (!this.source) return;
|
|
285
|
+
|
|
286
|
+
this.source.props = {
|
|
287
|
+
...this.source.props,
|
|
288
|
+
playbackMode: value,
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
onAfterSetEnableSequence: StepSequencerSetterHooks["onAfterSetEnableSequence"] =
|
|
293
|
+
(value) => {
|
|
294
|
+
if (!this.source) return;
|
|
295
|
+
|
|
296
|
+
this.source.props = {
|
|
297
|
+
...this.source.props,
|
|
298
|
+
enableSequence: value,
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
private initializeSource() {
|
|
303
|
+
this.source = new StepSequencerSource(this.engine.transport, {
|
|
304
|
+
onEvent: this.handleStepEvent,
|
|
305
|
+
patterns: this.props.patterns,
|
|
306
|
+
stepsPerPage: this.props.stepsPerPage,
|
|
307
|
+
resolution: this.props.resolution,
|
|
308
|
+
playbackMode: this.props.playbackMode,
|
|
309
|
+
patternSequence: this.props.patternSequence,
|
|
310
|
+
enableSequence: this.props.enableSequence,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
this.engine.transport.addSource(this.source);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private handleStepEvent = (event: StepSequencerSourceEvent) => {
|
|
317
|
+
// Update state for UI
|
|
318
|
+
this.state = {
|
|
319
|
+
...this.state,
|
|
320
|
+
currentStep: event.stepNo,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Update active page if changed
|
|
324
|
+
if (event.pageNo !== this.props.activePageNo) {
|
|
325
|
+
this.props = {
|
|
326
|
+
...this.props,
|
|
327
|
+
activePageNo: event.pageNo,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Update active pattern if changed (for sequence mode)
|
|
332
|
+
if (event.patternNo !== this.props.activePatternNo) {
|
|
333
|
+
this.props = {
|
|
334
|
+
...this.props,
|
|
335
|
+
activePatternNo: event.patternNo,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Trigger the step
|
|
340
|
+
this.triggerStep(event.step, event.contextTime);
|
|
341
|
+
|
|
342
|
+
// Trigger UI update
|
|
343
|
+
this.triggerPropsUpdate();
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
private registerOutputs() {
|
|
347
|
+
this.midiOutput = this.registerMidiOutput({ name: "midi" });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private triggerStep(step: IStep, contextTime: ContextTime) {
|
|
351
|
+
if (!step.active) return;
|
|
352
|
+
|
|
353
|
+
// Check if step has notes or CC messages
|
|
354
|
+
if (step.notes.length === 0 && step.ccMessages.length === 0) return;
|
|
355
|
+
|
|
356
|
+
// Check probability
|
|
357
|
+
if (Math.random() * 100 > step.probability) return;
|
|
358
|
+
|
|
359
|
+
const bpm = this.engine.bpm;
|
|
360
|
+
|
|
361
|
+
// Send CC messages immediately
|
|
362
|
+
step.ccMessages.forEach((ccMessage) => {
|
|
363
|
+
this.sendCC(ccMessage, contextTime);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Send notes
|
|
367
|
+
const microtimeOffsetSeconds =
|
|
368
|
+
(step.microtimeOffset / MICROTIMING_STEP) * (60 / bpm);
|
|
369
|
+
const noteDurationSeconds =
|
|
370
|
+
divisionToMilliseconds(step.duration, bpm) / 1000;
|
|
371
|
+
|
|
372
|
+
const noteTime = contextTime + microtimeOffsetSeconds;
|
|
373
|
+
|
|
374
|
+
step.notes.forEach((stepNote) => {
|
|
375
|
+
this.sendNoteOn(stepNote, noteTime);
|
|
376
|
+
if (noteDurationSeconds === Infinity) return;
|
|
377
|
+
|
|
378
|
+
this.sendNoteOff(stepNote, noteTime + noteDurationSeconds);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private sendNoteOn(stepNote: IStepNote, triggeredAt: ContextTime) {
|
|
383
|
+
const note = new Note(stepNote.note);
|
|
384
|
+
note.velocity = stepNote.velocity / 127; // Normalize to 0-1
|
|
385
|
+
|
|
386
|
+
const midiEvent = MidiEvent.fromNote(note, true, triggeredAt);
|
|
387
|
+
this.midiOutput.onMidiEvent(midiEvent);
|
|
388
|
+
|
|
389
|
+
// Track scheduled note
|
|
390
|
+
this.scheduledNotes.set(stepNote.note, triggeredAt);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private sendNoteOff(stepNote: IStepNote, triggeredAt: ContextTime) {
|
|
394
|
+
const midiEvent = MidiEvent.fromNote(stepNote.note, false, triggeredAt);
|
|
395
|
+
this.midiOutput.onMidiEvent(midiEvent);
|
|
396
|
+
|
|
397
|
+
// Remove from scheduled notes
|
|
398
|
+
this.scheduledNotes.delete(stepNote.note);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private sendCC(stepCC: IStepCC, triggeredAt: ContextTime) {
|
|
402
|
+
const midiEvent = MidiEvent.fromCC(stepCC.cc, stepCC.value, triggeredAt);
|
|
403
|
+
this.midiOutput.onMidiEvent(midiEvent);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Called when transport starts
|
|
407
|
+
start(contextTime: ContextTime): void {
|
|
408
|
+
super.start(contextTime);
|
|
409
|
+
|
|
410
|
+
this.state = { isRunning: true };
|
|
411
|
+
this.scheduledNotes.clear();
|
|
412
|
+
|
|
413
|
+
const ticks = this.engine.transport.getTicksAtContextTime(contextTime);
|
|
414
|
+
this.source!.onStart(ticks);
|
|
415
|
+
|
|
416
|
+
this.triggerPropsUpdate();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Called when transport stops
|
|
420
|
+
stop(contextTime: ContextTime): void {
|
|
421
|
+
super.stop(contextTime);
|
|
422
|
+
|
|
423
|
+
this.state = { isRunning: false };
|
|
424
|
+
|
|
425
|
+
// Send all note-offs immediately
|
|
426
|
+
this.scheduledNotes.forEach((_offTime, noteName) => {
|
|
427
|
+
const midiEvent = MidiEvent.fromNote(noteName, false, contextTime);
|
|
428
|
+
this.midiOutput.onMidiEvent(midiEvent);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
this.scheduledNotes.clear();
|
|
432
|
+
|
|
433
|
+
// Stop the source
|
|
434
|
+
const ticks = this.engine.transport.getTicksAtContextTime(contextTime);
|
|
435
|
+
this.source!.onStop(ticks);
|
|
436
|
+
this.source!.onJump(0);
|
|
437
|
+
|
|
438
|
+
// Reset UI indicator
|
|
439
|
+
this.state = { currentStep: 0 };
|
|
440
|
+
this.triggerPropsUpdate();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
dispose() {
|
|
444
|
+
if (!this.source) return;
|
|
445
|
+
|
|
446
|
+
this.engine.transport.removeSource(this.source.id);
|
|
59
447
|
}
|
|
60
448
|
}
|
|
@@ -69,7 +69,7 @@ export default class StereoPanner extends PolyModule<ModuleType.StereoPanner> {
|
|
|
69
69
|
const monoModuleConstructor = (
|
|
70
70
|
engineId: string,
|
|
71
71
|
params: IModuleConstructor<ModuleType.StereoPanner>,
|
|
72
|
-
) =>
|
|
72
|
+
) => Module.create(MonoStereoPanner, engineId, params);
|
|
73
73
|
|
|
74
74
|
super(engineId, {
|
|
75
75
|
...params,
|