@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
@@ -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<IScaleProps> = {
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 = { min: 0, max: 1, current: 0.5 };
49
+ const DEFAULT_PROPS: IScaleProps = {
50
+ min: 0,
51
+ max: 1,
52
+ current: 0.5,
53
+ mode: "exponential",
54
+ };
39
55
 
40
- export default class Scale
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 { INote, Module, IModule, MidiOutput, ModulePropSchema } from "@/core";
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 type ISequence = {
7
- active: boolean;
8
- // time: BarsBeatsSixteenths;
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
- bars: number;
15
- steps: number;
16
- sequences: ISequence[][];
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
- Omit<IStepSequencerProps, "sequences">
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
- steps: {
71
+ activePatternNo: {
23
72
  kind: "number",
24
- min: 1,
25
- max: 16,
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
- bars: {
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
- sequences: [],
40
- steps: 16,
41
- bars: 1,
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
- // Not implemented yet, just the data modeling
45
- export default class StepSequencer extends Module<ModuleType.StepSequencer> {
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
- ) => new MonoStereoPanner(engineId, params);
72
+ ) => Module.create(MonoStereoPanner, engineId, params);
73
73
 
74
74
  super(engineId, {
75
75
  ...params,