@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blibliki/engine",
3
- "version": "0.5.2",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -15,15 +15,15 @@
15
15
  "dist"
16
16
  ],
17
17
  "devDependencies": {
18
- "@types/audioworklet": "0.0.92",
19
- "vite-tsconfig-paths": "^5.1.4",
20
- "vitest": "4.0.15"
18
+ "@types/audioworklet": "0.0.93",
19
+ "vite-tsconfig-paths": "6.0.5",
20
+ "vitest": "4.0.18"
21
21
  },
22
22
  "dependencies": {
23
23
  "@julusian/midi": "^3.6.1",
24
- "es-toolkit": "^1.41.0",
25
- "@blibliki/transport": "^0.5.2",
26
- "@blibliki/utils": "^0.5.2"
24
+ "es-toolkit": "1.44.0",
25
+ "@blibliki/transport": "^0.9.0",
26
+ "@blibliki/utils": "^0.9.0"
27
27
  },
28
28
  "scripts": {
29
29
  "build": "tsup",
package/src/Engine.ts CHANGED
@@ -1,11 +1,4 @@
1
- import {
2
- BPM,
3
- ContextTime,
4
- Ticks,
5
- TimeSignature,
6
- Transport,
7
- TransportEvent,
8
- } from "@blibliki/transport";
1
+ import { BPM, Ticks, TimeSignature, Transport } from "@blibliki/transport";
9
2
  import {
10
3
  assertDefined,
11
4
  Context,
@@ -26,6 +19,7 @@ import {
26
19
  ModuleParams,
27
20
  ModuleType,
28
21
  ModuleTypeToModuleMapping,
22
+ ModuleTypeToStateMapping,
29
23
  createModule,
30
24
  } from "@/modules";
31
25
  import {
@@ -63,7 +57,7 @@ export class Engine {
63
57
  context: Context;
64
58
  isInitialized = false;
65
59
  routes: Routes;
66
- transport: Transport<TransportEvent>;
60
+ transport: Transport;
67
61
  modules: Map<
68
62
  string,
69
63
  ModuleTypeToModuleMapping[keyof ModuleTypeToModuleMapping]
@@ -107,20 +101,8 @@ export class Engine {
107
101
 
108
102
  this.context = context;
109
103
  this.transport = new Transport(this.context, {
110
- generator: (_start: Ticks, _end: Ticks) => {
111
- return [] as TransportEvent[];
112
- },
113
- consumer: (_event: TransportEvent) => {
114
- return;
115
- },
116
- onJump: (_ticks: Ticks) => {
117
- return;
118
- },
119
104
  onStart: this.onStart,
120
105
  onStop: this.onStop,
121
- silence: (_actionAt: ContextTime) => {
122
- return;
123
- },
124
106
  });
125
107
  this.routes = new Routes(this);
126
108
  this.modules = new Map();
@@ -197,16 +179,19 @@ export class Engine {
197
179
 
198
180
  async start() {
199
181
  await this.resume();
200
- this.transport.start();
182
+ const actionAt = this.context.currentTime;
183
+ this.transport.start(actionAt);
201
184
  }
202
185
 
203
186
  stop() {
204
- this.transport.stop();
205
- this.transport.reset();
187
+ const actionAt = this.context.currentTime;
188
+ this.transport.stop(actionAt);
189
+ this.transport.reset(actionAt);
206
190
  }
207
191
 
208
192
  pause() {
209
- this.transport.stop();
193
+ const actionAt = this.context.currentTime;
194
+ this.transport.stop(actionAt);
210
195
  }
211
196
 
212
197
  get bpm() {
@@ -273,16 +258,44 @@ export class Engine {
273
258
  return this.midiDeviceManager.findByFuzzyName(name, threshold);
274
259
  }
275
260
 
261
+ findMidiInputDevice(id: string) {
262
+ return this.midiDeviceManager.findInput(id);
263
+ }
264
+
265
+ findMidiInputDeviceByName(name: string) {
266
+ return this.midiDeviceManager.findInputByName(name);
267
+ }
268
+
269
+ findMidiInputDeviceByFuzzyName(name: string, threshold?: number) {
270
+ return this.midiDeviceManager.findInputByFuzzyName(name, threshold);
271
+ }
272
+
273
+ findMidiOutputDevice(id: string) {
274
+ return this.midiDeviceManager.findOutput(id);
275
+ }
276
+
277
+ findMidiOutputDeviceByName(name: string) {
278
+ return this.midiDeviceManager.findOutputByName(name);
279
+ }
280
+
281
+ findMidiOutputDeviceByFuzzyName(name: string, threshold?: number) {
282
+ return this.midiDeviceManager.findOutputByFuzzyName(name, threshold);
283
+ }
284
+
276
285
  onPropsUpdate(
277
286
  callback: <T extends ModuleType>(
278
- params: IModule<T> | IPolyModule<T>,
287
+ params: (IModule<T> | IPolyModule<T>) & {
288
+ state?: ModuleTypeToStateMapping[T];
289
+ },
279
290
  ) => void,
280
291
  ) {
281
292
  this.propsUpdateCallbacks.push(callback);
282
293
  }
283
294
 
284
295
  _triggerPropsUpdate<T extends ModuleType>(
285
- params: IModule<T> | IPolyModule<T>,
296
+ params: (IModule<T> | IPolyModule<T>) & {
297
+ state?: ModuleTypeToStateMapping[T];
298
+ },
286
299
  ) {
287
300
  this.propsUpdateCallbacks.forEach((callback) => {
288
301
  callback(params);
@@ -301,14 +314,18 @@ export class Engine {
301
314
  }
302
315
 
303
316
  // actionAt is context time
304
- private onStart = (actionAt: ContextTime) => {
317
+ private onStart = (ticks: Ticks) => {
318
+ const actionAt = this.transport.getContextTimeAtTicks(ticks);
319
+
305
320
  this.modules.forEach((module) => {
306
321
  module.start(actionAt);
307
322
  });
308
323
  };
309
324
 
310
325
  // actionAt is context time
311
- private onStop = (actionAt: ContextTime) => {
326
+ private onStop = (ticks: Ticks) => {
327
+ const actionAt = this.transport.getContextTimeAtTicks(ticks);
328
+
312
329
  this.modules.forEach((module) => {
313
330
  module.stop(actionAt);
314
331
  });
package/src/core/index.ts CHANGED
@@ -5,6 +5,7 @@ export type {
5
5
  IPolyModuleSerialize,
6
6
  IAnyModuleSerialize,
7
7
  SetterHooks,
8
+ StateSetterHooks,
8
9
  } from "./module";
9
10
 
10
11
  export type IAnyAudioContext = AudioContext | OfflineAudioContext;
@@ -13,8 +14,16 @@ export { Routes } from "./Route";
13
14
  export type { IRoute } from "./Route";
14
15
 
15
16
  export { default as MidiDeviceManager } from "./midi/MidiDeviceManager";
16
- export { default as MidiDevice, MidiPortState } from "./midi/MidiDevice";
17
- export type { IMidiDevice } from "./midi/MidiDevice";
17
+ export {
18
+ default as BaseMidiDevice,
19
+ MidiPortState,
20
+ } from "./midi/BaseMidiDevice";
21
+ export type { IMidiDevice } from "./midi/BaseMidiDevice";
22
+ export { default as MidiInputDevice } from "./midi/MidiInputDevice";
23
+ export type { IMidiInput, EventListerCallback } from "./midi/MidiInputDevice";
24
+ export { default as MidiOutputDevice } from "./midi/MidiOutputDevice";
25
+ // Legacy export for backwards compatibility
26
+ export { default as MidiDevice } from "./midi/MidiInputDevice";
18
27
  export { default as MidiEvent, MidiEventType } from "./midi/MidiEvent";
19
28
  export {
20
29
  normalizeDeviceName,
@@ -0,0 +1,47 @@
1
+ import { IMidiPort } from "./adapters";
2
+
3
+ export enum MidiPortState {
4
+ connected = "connected",
5
+ disconnected = "disconnected",
6
+ }
7
+
8
+ export type IMidiDevice = {
9
+ id: string;
10
+ name: string;
11
+ state: MidiPortState;
12
+ };
13
+
14
+ export default abstract class BaseMidiDevice<
15
+ T extends IMidiPort,
16
+ > implements IMidiDevice {
17
+ protected midiPort: T;
18
+
19
+ constructor(props: T) {
20
+ this.midiPort = props;
21
+ this.connect();
22
+ }
23
+
24
+ abstract connect(): void;
25
+
26
+ abstract disconnect(): void;
27
+
28
+ get id() {
29
+ return this.midiPort.id;
30
+ }
31
+
32
+ get name() {
33
+ return this.midiPort.name;
34
+ }
35
+
36
+ get type() {
37
+ return this.midiPort.type;
38
+ }
39
+
40
+ get state() {
41
+ return this.midiPort.state as MidiPortState;
42
+ }
43
+
44
+ serialize() {
45
+ return { id: this.id, name: this.name, type: this.type, state: this.state };
46
+ }
47
+ }
@@ -1,7 +1,8 @@
1
1
  import { Context } from "@blibliki/utils";
2
2
  import Note from "../Note";
3
- import { EventListerCallback, IMidiInput, MidiPortState } from "./MidiDevice";
3
+ import { MidiPortState } from "./BaseMidiDevice";
4
4
  import MidiEvent from "./MidiEvent";
5
+ import { EventListerCallback, IMidiInput } from "./MidiInputDevice";
5
6
 
6
7
  const MAP_KEYS: Record<string, Note> = {
7
8
  a: new Note("C3"),
@@ -1,13 +1,15 @@
1
1
  import { Context } from "@blibliki/utils";
2
2
  import ComputerKeyboardDevice from "./ComputerKeyboardDevice";
3
- import MidiDevice from "./MidiDevice";
3
+ import MidiInputDevice from "./MidiInputDevice";
4
+ import MidiOutputDevice from "./MidiOutputDevice";
4
5
  import { createMidiAdapter, type IMidiAccess } from "./adapters";
5
6
  import { findBestMatch } from "./deviceMatcher";
6
7
 
7
- type ListenerCallback = (device: MidiDevice) => void;
8
+ type ListenerCallback = (device: MidiInputDevice | MidiOutputDevice) => void;
8
9
 
9
10
  export default class MidiDeviceManager {
10
- devices = new Map<string, MidiDevice | ComputerKeyboardDevice>();
11
+ inputDevices = new Map<string, MidiInputDevice | ComputerKeyboardDevice>();
12
+ outputDevices = new Map<string, MidiOutputDevice>();
11
13
  private initialized = false;
12
14
  private listeners: ListenerCallback[] = [];
13
15
  private context: Readonly<Context>;
@@ -26,12 +28,47 @@ export default class MidiDeviceManager {
26
28
  this.initialized = true;
27
29
  }
28
30
 
29
- find(id: string): MidiDevice | ComputerKeyboardDevice | undefined {
30
- return this.devices.get(id);
31
+ find(
32
+ id: string,
33
+ ): MidiInputDevice | ComputerKeyboardDevice | MidiOutputDevice | undefined {
34
+ return this.findInput(id) ?? this.findOutput(id);
31
35
  }
32
36
 
33
- findByName(name: string): MidiDevice | ComputerKeyboardDevice | undefined {
34
- return Array.from(this.devices.values()).find((d) => d.name === name);
37
+ findByName(
38
+ name: string,
39
+ ): MidiInputDevice | ComputerKeyboardDevice | MidiOutputDevice | undefined {
40
+ return this.findInputByName(name) ?? this.findOutputByName(name);
41
+ }
42
+
43
+ findByFuzzyName(
44
+ name: string,
45
+ threshold = 0.6,
46
+ ): MidiInputDevice | ComputerKeyboardDevice | MidiOutputDevice | undefined {
47
+ const input = this.findInputByFuzzyName(name, threshold);
48
+ const output = this.findOutputByFuzzyName(name, threshold);
49
+
50
+ if (!input) return output?.device;
51
+ if (!output) return input.device;
52
+
53
+ return input.score > output.score ? input.device : output.device;
54
+ }
55
+
56
+ findInput(id: string): MidiInputDevice | ComputerKeyboardDevice | undefined {
57
+ return this.inputDevices.get(id);
58
+ }
59
+
60
+ findInputByName(
61
+ name: string,
62
+ ): MidiInputDevice | ComputerKeyboardDevice | undefined {
63
+ return Array.from(this.inputDevices.values()).find((d) => d.name === name);
64
+ }
65
+
66
+ findOutput(id: string): MidiOutputDevice | undefined {
67
+ return this.outputDevices.get(id);
68
+ }
69
+
70
+ findOutputByName(name: string): MidiOutputDevice | undefined {
71
+ return Array.from(this.outputDevices.values()).find((d) => d.name === name);
35
72
  }
36
73
 
37
74
  /**
@@ -42,11 +79,30 @@ export default class MidiDeviceManager {
42
79
  * @param threshold - Minimum similarity score (0-1, default: 0.6)
43
80
  * @returns The best matching device and confidence score, or null
44
81
  */
45
- findByFuzzyName(
82
+ findInputByFuzzyName(
46
83
  targetName: string,
47
84
  threshold = 0.6,
48
- ): { device: MidiDevice | ComputerKeyboardDevice; score: number } | null {
49
- const deviceEntries = Array.from(this.devices.values());
85
+ ): {
86
+ device: MidiInputDevice | ComputerKeyboardDevice;
87
+ score: number;
88
+ } | null {
89
+ const deviceEntries = Array.from(this.inputDevices.values());
90
+ const candidateNames = deviceEntries.map((d) => d.name);
91
+
92
+ const match = findBestMatch(targetName, candidateNames, threshold);
93
+
94
+ if (!match) return null;
95
+
96
+ const device = deviceEntries.find((d) => d.name === match.name);
97
+
98
+ return device ? { device, score: match.score } : null;
99
+ }
100
+
101
+ findOutputByFuzzyName(
102
+ targetName: string,
103
+ threshold = 0.6,
104
+ ): { device: MidiOutputDevice; score: number } | null {
105
+ const deviceEntries = Array.from(this.outputDevices.values());
50
106
  const candidateNames = deviceEntries.map((d) => d.name);
51
107
 
52
108
  const match = findBestMatch(targetName, candidateNames, threshold);
@@ -79,8 +135,17 @@ export default class MidiDeviceManager {
79
135
  }
80
136
 
81
137
  for (const input of this.midiAccess.inputs()) {
82
- if (!this.devices.has(input.id)) {
83
- this.devices.set(input.id, new MidiDevice(input, this.context));
138
+ if (!this.inputDevices.has(input.id)) {
139
+ this.inputDevices.set(
140
+ input.id,
141
+ new MidiInputDevice(input, this.context),
142
+ );
143
+ }
144
+ }
145
+
146
+ for (const output of this.midiAccess.outputs()) {
147
+ if (!this.outputDevices.has(output.id)) {
148
+ this.outputDevices.set(output.id, new MidiOutputDevice(output));
84
149
  }
85
150
  }
86
151
  } catch (err) {
@@ -92,7 +157,7 @@ export default class MidiDeviceManager {
92
157
  if (typeof document === "undefined") return;
93
158
 
94
159
  const computerKeyboardDevice = new ComputerKeyboardDevice(this.context);
95
- this.devices.set(computerKeyboardDevice.id, computerKeyboardDevice);
160
+ this.inputDevices.set(computerKeyboardDevice.id, computerKeyboardDevice);
96
161
  }
97
162
 
98
163
  private listenChanges() {
@@ -101,26 +166,55 @@ export default class MidiDeviceManager {
101
166
  this.midiAccess.addEventListener("statechange", (port) => {
102
167
  if (port.state === "connected") {
103
168
  // Device connected
104
- if (this.devices.has(port.id)) return;
105
-
106
- const device = new MidiDevice(port, this.context);
107
- this.devices.set(device.id, device);
108
-
109
- this.listeners.forEach((listener) => {
110
- listener(device);
111
- });
169
+ if (port.type === "input") {
170
+ if (this.inputDevices.has(port.id)) return;
171
+
172
+ // Find the actual input port from midiAccess
173
+ for (const input of this.midiAccess!.inputs()) {
174
+ if (input.id === port.id) {
175
+ const device = new MidiInputDevice(input, this.context);
176
+ this.inputDevices.set(device.id, device);
177
+
178
+ this.listeners.forEach((listener) => {
179
+ listener(device);
180
+ });
181
+ break;
182
+ }
183
+ }
184
+ } else {
185
+ // Output device connected
186
+ if (this.outputDevices.has(port.id)) return;
187
+
188
+ // Find the actual output port from midiAccess
189
+ for (const output of this.midiAccess!.outputs()) {
190
+ if (output.id === port.id) {
191
+ const device = new MidiOutputDevice(output);
192
+ this.outputDevices.set(device.id, device);
193
+ break;
194
+ }
195
+ }
196
+ }
112
197
  } else {
113
198
  // Device disconnected
114
- const device = this.devices.get(port.id);
115
- if (!device) return;
116
- if (device instanceof ComputerKeyboardDevice) return;
117
-
118
- device.disconnect();
119
- this.devices.delete(device.id);
120
-
121
- this.listeners.forEach((listener) => {
122
- listener(device);
123
- });
199
+ if (port.type === "input") {
200
+ const device = this.inputDevices.get(port.id);
201
+ if (!device) return;
202
+ if (device instanceof ComputerKeyboardDevice) return;
203
+
204
+ device.disconnect();
205
+ this.inputDevices.delete(device.id);
206
+
207
+ this.listeners.forEach((listener) => {
208
+ listener(device);
209
+ });
210
+ } else {
211
+ // Output device disconnected
212
+ const device = this.outputDevices.get(port.id);
213
+ if (!device) return;
214
+
215
+ device.disconnect();
216
+ this.outputDevices.delete(device.id);
217
+ }
124
218
  }
125
219
  });
126
220
  }
@@ -1,67 +1,43 @@
1
1
  import { Context } from "@blibliki/utils";
2
+ import BaseMidiDevice, { MidiPortState } from "./BaseMidiDevice";
2
3
  import Message from "./Message";
3
4
  import MidiEvent, { MidiEventType } from "./MidiEvent";
4
5
  import type { IMidiInputPort, IMidiMessageEvent } from "./adapters";
5
6
 
6
- export enum MidiPortState {
7
- connected = "connected",
8
- disconnected = "disconnected",
9
- }
10
-
11
- export type IMidiDevice = {
7
+ export type IMidiInput = {
12
8
  id: string;
13
9
  name: string;
14
10
  state: MidiPortState;
15
- };
16
-
17
- export type IMidiInput = IMidiDevice & {
18
11
  eventListerCallbacks: EventListerCallback[];
19
12
  };
20
13
 
21
14
  export type EventListerCallback = (event: MidiEvent) => void;
22
15
 
23
- export default class MidiDevice implements IMidiDevice {
24
- id: string;
25
- name: string;
16
+ export default class MidiInputDevice extends BaseMidiDevice<IMidiInputPort> {
26
17
  eventListerCallbacks: EventListerCallback[] = [];
27
18
 
28
19
  private context: Readonly<Context>;
29
- private input: IMidiInputPort;
30
20
  private messageHandler: ((event: IMidiMessageEvent) => void) | null = null;
31
21
 
32
22
  constructor(input: IMidiInputPort, context: Context) {
33
- this.id = input.id;
34
- this.name = input.name;
35
- this.input = input;
23
+ super(input);
36
24
  this.context = context;
37
-
38
- this.connect();
39
- }
40
-
41
- get state() {
42
- return this.input.state as MidiPortState;
43
25
  }
44
26
 
45
27
  connect() {
46
28
  this.messageHandler = (e: IMidiMessageEvent) => {
47
29
  this.processEvent(e);
48
30
  };
49
- this.input.addEventListener(this.messageHandler);
31
+ this.midiPort.addEventListener(this.messageHandler);
50
32
  }
51
33
 
52
34
  disconnect() {
53
35
  if (this.messageHandler) {
54
- this.input.removeEventListener(this.messageHandler);
36
+ this.midiPort.removeEventListener(this.messageHandler);
55
37
  this.messageHandler = null;
56
38
  }
57
39
  }
58
40
 
59
- serialize() {
60
- const { id, name, state } = this;
61
-
62
- return { id, name, state };
63
- }
64
-
65
41
  addEventListener(callback: EventListerCallback) {
66
42
  this.eventListerCallbacks.push(callback);
67
43
  }
@@ -0,0 +1,23 @@
1
+ import BaseMidiDevice from "./BaseMidiDevice";
2
+ import type { IMidiOutputPort } from "./adapters";
3
+
4
+ export default class MidiOutputDevice extends BaseMidiDevice<IMidiOutputPort> {
5
+ constructor(output: IMidiOutputPort) {
6
+ super(output);
7
+ this.connect();
8
+ }
9
+
10
+ connect() {
11
+ // Output ports don't require connection setup like inputs
12
+ // This method exists to satisfy the BaseMidiDevice interface
13
+ }
14
+
15
+ disconnect() {
16
+ // Output ports don't require disconnection cleanup like inputs
17
+ // This method exists to satisfy the BaseMidiDevice interface
18
+ }
19
+
20
+ send(data: number[] | Uint8Array, timestamp?: number) {
21
+ this.midiPort.send(data, timestamp);
22
+ }
23
+ }