@blibliki/engine 0.3.7 → 0.3.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blibliki/engine",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "source": "src/index.ts",
6
6
  "main": "dist/index.cjs",
@@ -20,8 +20,8 @@
20
20
  "es-toolkit": "^1.41.0",
21
21
  "node-web-audio-api": "^1.0.3",
22
22
  "webmidi": "^3.1.14",
23
- "@blibliki/transport": "^0.3.7",
24
- "@blibliki/utils": "^0.3.7"
23
+ "@blibliki/transport": "^0.3.8",
24
+ "@blibliki/utils": "^0.3.8"
25
25
  },
26
26
  "scripts": {
27
27
  "build": "tsup",
package/src/Engine.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ BPM,
2
3
  ContextTime,
3
4
  Ticks,
4
5
  TimeSignature,
@@ -12,7 +13,14 @@ import {
12
13
  pick,
13
14
  uuidv4,
14
15
  } from "@blibliki/utils";
15
- import { IRoute, Routes, MidiDeviceManager, IModule, MidiEvent } from "@/core";
16
+ import {
17
+ IRoute,
18
+ Routes,
19
+ MidiDeviceManager,
20
+ IModule,
21
+ MidiEvent,
22
+ IModuleSerialize,
23
+ } from "@/core";
16
24
  import {
17
25
  ICreateModule,
18
26
  ModuleParams,
@@ -20,7 +28,11 @@ import {
20
28
  ModuleTypeToModuleMapping,
21
29
  createModule,
22
30
  } from "@/modules";
23
- import { IPolyModule, PolyModule } from "./core/module/PolyModule";
31
+ import {
32
+ IPolyModule,
33
+ IPolyModuleSerialize,
34
+ PolyModule,
35
+ } from "./core/module/PolyModule";
24
36
  import { loadProcessors } from "./processors";
25
37
 
26
38
  export type IUpdateModule<T extends ModuleType> = {
@@ -33,6 +45,13 @@ export type IUpdateModule<T extends ModuleType> = {
33
45
 
34
46
  export type ICreateRoute = Optional<IRoute, "id">;
35
47
 
48
+ export interface IEngineSerialize {
49
+ bpm: BPM;
50
+ timeSignature: TimeSignature;
51
+ modules: (IModuleSerialize<ModuleType> | IPolyModuleSerialize<ModuleType>)[];
52
+ routes: IRoute[];
53
+ }
54
+
36
55
  export class Engine {
37
56
  private static _engines = new Map<string, Engine>();
38
57
  private static _currentId: string | undefined;
@@ -65,6 +84,24 @@ export class Engine {
65
84
  return this.getById(this._currentId);
66
85
  }
67
86
 
87
+ static async load(data: IEngineSerialize): Promise<Engine> {
88
+ const { bpm, timeSignature, modules, routes } = data;
89
+ const context = new Context();
90
+ const engine = new Engine(context);
91
+ await engine.initialize();
92
+
93
+ engine.timeSignature = timeSignature;
94
+ engine.bpm = bpm;
95
+ modules.forEach((m) => {
96
+ engine.addModule(m);
97
+ });
98
+ routes.forEach((r) => {
99
+ engine.addRoute(r);
100
+ });
101
+
102
+ return engine;
103
+ }
104
+
68
105
  constructor(context: Context) {
69
106
  this.id = uuidv4();
70
107
 
@@ -201,6 +238,15 @@ export class Engine {
201
238
  this.modules.clear();
202
239
  }
203
240
 
241
+ serialize(): IEngineSerialize {
242
+ return {
243
+ bpm: this.bpm,
244
+ timeSignature: this.timeSignature,
245
+ modules: Array.from(this.modules.values()).map((m) => m.serialize()),
246
+ routes: this.routes.serialize(),
247
+ };
248
+ }
249
+
204
250
  findModule(
205
251
  id: string,
206
252
  ): ModuleTypeToModuleMapping[keyof ModuleTypeToModuleMapping] {
@@ -219,6 +265,10 @@ export class Engine {
219
265
  return this.midiDeviceManager.find(id);
220
266
  }
221
267
 
268
+ findMidiDeviceByName(name: string) {
269
+ return this.midiDeviceManager.findByName(name);
270
+ }
271
+
222
272
  onPropsUpdate(
223
273
  callback: <T extends ModuleType>(
224
274
  params: IModule<T> | IPolyModule<T>,
package/src/core/Route.ts CHANGED
@@ -37,9 +37,21 @@ export class Routes {
37
37
  }
38
38
 
39
39
  clear() {
40
- for (const id in this.routes) {
40
+ this.routes.forEach((_, id) => {
41
41
  this.removeRoute(id);
42
- }
42
+ });
43
+ }
44
+
45
+ replug() {
46
+ this.routes.forEach((_, id) => {
47
+ const { sourceIO, destinationIO } = this.getIOs(id);
48
+ sourceIO.rePlugAll();
49
+ destinationIO.rePlugAll();
50
+ });
51
+ }
52
+
53
+ serialize(): IRoute[] {
54
+ return Array.from(this.routes.values());
43
55
  }
44
56
 
45
57
  private plug(id: string) {
@@ -27,6 +27,10 @@ export default class MidiDeviceManager {
27
27
  return this.devices.get(id);
28
28
  }
29
29
 
30
+ findByName(name: string): MidiDevice | ComputerKeyboardDevice | undefined {
31
+ return Array.from(this.devices.values()).find((d) => d.name === name);
32
+ }
33
+
30
34
  addListener(callback: ListenerCallback) {
31
35
  this.listeners.push(callback);
32
36
  }
@@ -35,41 +35,6 @@ export type IModuleConstructor<T extends ModuleType> = Optional<
35
35
  audioNodeConstructor?: (context: Context) => AudioNode;
36
36
  };
37
37
 
38
- /**
39
- * Helper type for type-safe property lifecycle hooks.
40
- *
41
- * Hooks are completely optional - only define the ones you need.
42
- * Use explicit type annotation for automatic type inference.
43
- *
44
- * @example
45
- * ```typescript
46
- * export type IGainProps = {
47
- * gain: number;
48
- * muted: boolean;
49
- * };
50
- *
51
- * export class MonoGain extends Module<ModuleType.Gain> {
52
- * // ✅ Define only the hooks you need with type annotation
53
- * // value type is automatically inferred as number!
54
- * onSetGain: SetterHooks<IGainProps>["onSetGain"] = (value) => {
55
- * this.audioNode.gain.value = value;
56
- * return value; // optional: return modified value
57
- * };
58
- *
59
- * // ✅ onAfterSet is called after prop is set
60
- * onAfterSetMuted: SetterHooks<IGainProps>["onAfterSetMuted"] = (value) => {
61
- * if (value) this.audioNode.gain.value = 0;
62
- * };
63
- *
64
- * // ✅ You can omit hooks you don't need - they're optional!
65
- * // No need to define onSetMuted if you don't need it
66
- *
67
- * // ❌ This would cause a type error:
68
- * // onSetGain: SetterHooks<IGainProps>["onSetGain"] = (value: string) => value;
69
- * // ^^^^^^ Error!
70
- * }
71
- * ```
72
- */
73
38
  export type SetterHooks<P> = {
74
39
  [K in keyof P as `onSet${Capitalize<string & K>}`]: (value: P[K]) => P[K];
75
40
  } & {
@@ -88,7 +53,6 @@ export abstract class Module<T extends ModuleType> implements IModule<T> {
88
53
  inputs: InputCollection;
89
54
  outputs: OutputCollection;
90
55
  protected _props!: ModuleTypeToPropsMapping[T];
91
- protected superInitialized = false;
92
56
  protected activeNotes: Note[];
93
57
  private pendingUIUpdates = false;
94
58
 
@@ -108,8 +72,6 @@ export abstract class Module<T extends ModuleType> implements IModule<T> {
108
72
  this.inputs = new InputCollection(this);
109
73
  this.outputs = new OutputCollection(this);
110
74
 
111
- this.superInitialized = true;
112
-
113
75
  // Defer hook calls until after subclass is fully initialized
114
76
  queueMicrotask(() => {
115
77
  this.props = props;
@@ -44,7 +44,6 @@ export abstract class PolyModule<T extends ModuleType>
44
44
  outputs: OutputCollection;
45
45
  protected monoModuleConstructor: IPolyModuleConstructor<T>["monoModuleConstructor"];
46
46
  protected _props!: ModuleTypeToPropsMapping[T];
47
- protected superInitialized = false;
48
47
  private _voices!: number;
49
48
  private _name!: string;
50
49
  private pendingUIUpdates = false;
@@ -60,7 +59,6 @@ export abstract class PolyModule<T extends ModuleType>
60
59
  this.engineId = engineId;
61
60
  this.name = name;
62
61
  this.moduleType = moduleType;
63
- this.voices = voices || 1;
64
62
  this._props = props;
65
63
 
66
64
  this.inputs = new InputCollection(
@@ -70,10 +68,9 @@ export abstract class PolyModule<T extends ModuleType>
70
68
  this as unknown as PolyModule<ModuleType>,
71
69
  );
72
70
 
73
- this.superInitialized = true;
74
-
75
71
  // Defer hook calls until after subclass is fully initialized
76
72
  queueMicrotask(() => {
73
+ this.voices = voices || 1;
77
74
  this.props = props;
78
75
  });
79
76
  }
@@ -146,8 +143,6 @@ export abstract class PolyModule<T extends ModuleType>
146
143
  }
147
144
 
148
145
  rePlugAll(callback?: () => void) {
149
- if (!this.superInitialized) return;
150
-
151
146
  this.inputs.rePlugAll(callback);
152
147
  this.outputs.rePlugAll(callback);
153
148
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Engine } from "./Engine";
2
- export type { ICreateRoute, IUpdateModule } from "./Engine";
2
+ export type { ICreateRoute, IUpdateModule, IEngineSerialize } from "./Engine";
3
3
 
4
4
  export type {
5
5
  IRoute,
@@ -1,4 +1,5 @@
1
- import { IModule, Module, MidiOutput, SetterHooks } from "@/core";
1
+ import { IModule, Module, MidiOutput, SetterHooks, MidiDevice } from "@/core";
2
+ import ComputerKeyboardInput from "@/core/midi/ComputerKeyboardDevice";
2
3
  import MidiEvent from "@/core/midi/MidiEvent";
3
4
  import { ModulePropSchema } from "@/core/schema";
4
5
  import { ICreateModule, ModuleType } from ".";
@@ -6,6 +7,7 @@ import { ICreateModule, ModuleType } from ".";
6
7
  export type IMidiSelector = IModule<ModuleType.MidiSelector>;
7
8
  export type IMidiSelectorProps = {
8
9
  selectedId: string | undefined | null;
10
+ selectedName: string | undefined | null;
9
11
  };
10
12
 
11
13
  export const midiSelectorPropSchema: ModulePropSchema<IMidiSelectorProps> = {
@@ -13,9 +15,16 @@ export const midiSelectorPropSchema: ModulePropSchema<IMidiSelectorProps> = {
13
15
  kind: "string",
14
16
  label: "Midi device ID",
15
17
  },
18
+ selectedName: {
19
+ kind: "string",
20
+ label: "Midi device name",
21
+ },
16
22
  };
17
23
 
18
- const DEFAULT_PROPS: IMidiSelectorProps = { selectedId: undefined };
24
+ const DEFAULT_PROPS: IMidiSelectorProps = {
25
+ selectedId: undefined,
26
+ selectedName: undefined,
27
+ };
19
28
 
20
29
  export default class MidiSelector
21
30
  extends Module<ModuleType.MidiSelector>
@@ -36,7 +45,15 @@ export default class MidiSelector
36
45
  props,
37
46
  });
38
47
 
39
- this.addEventListener(this.props.selectedId);
48
+ const midiDevice =
49
+ (this.props.selectedId &&
50
+ this.engine.findMidiDevice(this.props.selectedId)) ??
51
+ (this.props.selectedName &&
52
+ this.engine.findMidiDeviceByName(this.props.selectedName));
53
+
54
+ if (midiDevice) {
55
+ this.addEventListener(midiDevice);
56
+ }
40
57
 
41
58
  this.registerOutputs();
42
59
  }
@@ -45,7 +62,13 @@ export default class MidiSelector
45
62
  value,
46
63
  ) => {
47
64
  this.removeEventListener();
48
- this.addEventListener(value);
65
+ if (!value) return value;
66
+
67
+ const midiDevice = this.engine.findMidiDevice(value);
68
+ if (!midiDevice) return value;
69
+
70
+ this.props = { selectedName: midiDevice.name };
71
+ this.addEventListener(midiDevice);
49
72
 
50
73
  return value;
51
74
  };
@@ -60,11 +83,8 @@ export default class MidiSelector
60
83
  return this._forwardMidiEvent;
61
84
  }
62
85
 
63
- private addEventListener(midiId: string | undefined | null) {
64
- if (!midiId) return;
65
-
66
- const midiDevice = this.engine.findMidiDevice(midiId);
67
- midiDevice?.addEventListener(this.forwardMidiEvent);
86
+ private addEventListener(midiDevice: MidiDevice | ComputerKeyboardInput) {
87
+ midiDevice.addEventListener(this.forwardMidiEvent);
68
88
  }
69
89
 
70
90
  private removeEventListener() {
@@ -108,7 +108,7 @@ export class MonoOscillator
108
108
  {
109
109
  declare audioNode: OscillatorNode;
110
110
  isStated = false;
111
- lowOutputGain: GainNode;
111
+ outputGain: GainNode;
112
112
  detuneGain!: GainNode;
113
113
 
114
114
  constructor(engineId: string, params: ICreateModule<ModuleType.Oscillator>) {
@@ -122,7 +122,7 @@ export class MonoOscillator
122
122
  audioNodeConstructor,
123
123
  });
124
124
 
125
- this.lowOutputGain = new GainNode(this.context.audioContext, {
125
+ this.outputGain = new GainNode(this.context.audioContext, {
126
126
  gain: dbToGain(LOW_GAIN),
127
127
  });
128
128
 
@@ -152,8 +152,8 @@ export class MonoOscillator
152
152
  this.updateFrequency();
153
153
  };
154
154
 
155
- onAfterSetLowGain: OscillatorSetterHooks["onAfterSetLowGain"] = () => {
156
- this.rePlugAll();
155
+ onAfterSetLowGain: OscillatorSetterHooks["onAfterSetLowGain"] = (lowGain) => {
156
+ this.outputGain.gain.value = lowGain ? dbToGain(LOW_GAIN) : 1;
157
157
  };
158
158
 
159
159
  start(time: ContextTime) {
@@ -218,7 +218,7 @@ export class MonoOscillator
218
218
  }
219
219
 
220
220
  private applyOutputGain() {
221
- this.audioNode.connect(this.lowOutputGain);
221
+ this.audioNode.connect(this.outputGain);
222
222
  }
223
223
 
224
224
  private initializeGainDetune() {
@@ -236,8 +236,7 @@ export class MonoOscillator
236
236
  private registerOutputs() {
237
237
  this.registerAudioOutput({
238
238
  name: "out",
239
- getAudioNode: () =>
240
- this.props.lowGain ? this.lowOutputGain : this.audioNode,
239
+ getAudioNode: () => this.outputGain,
241
240
  });
242
241
  }
243
242
  }