@blibliki/engine 0.3.7 → 0.3.9

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.9",
4
4
  "type": "module",
5
5
  "source": "src/index.ts",
6
6
  "main": "dist/index.cjs",
@@ -18,10 +18,10 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "es-toolkit": "^1.41.0",
21
+ "jzz": "^1.9.6",
21
22
  "node-web-audio-api": "^1.0.3",
22
- "webmidi": "^3.1.14",
23
- "@blibliki/transport": "^0.3.7",
24
- "@blibliki/utils": "^0.3.7"
23
+ "@blibliki/utils": "^0.3.9",
24
+ "@blibliki/transport": "^0.3.9"
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>,
@@ -1,5 +1,5 @@
1
1
  import { Seconds } from "@blibliki/transport";
2
- import { Message } from "webmidi";
2
+ import Message from "../midi/Message";
3
3
  import frequencyTable from "./frequencyTable";
4
4
 
5
5
  const Notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
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) {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Simple wrapper around MIDI message data (Uint8Array)
3
+ * Replaces the webmidi Message class with native Web MIDI API data
4
+ */
5
+ export default class Message {
6
+ public readonly data: Uint8Array;
7
+
8
+ constructor(data: Uint8Array) {
9
+ this.data = data;
10
+ }
11
+
12
+ /**
13
+ * Returns the data bytes (excluding the status byte)
14
+ */
15
+ get dataBytes(): number[] {
16
+ return Array.from(this.data.slice(1));
17
+ }
18
+
19
+ /**
20
+ * Returns the MIDI message type based on the status byte
21
+ */
22
+ get type(): string {
23
+ const statusByte = this.data[0];
24
+ const messageType = statusByte & 0xf0;
25
+
26
+ switch (messageType) {
27
+ case 0x90: // Note On
28
+ // Check if velocity is 0 (which is actually Note Off)
29
+ return this.data[2] === 0 ? "noteoff" : "noteon";
30
+ case 0x80: // Note Off
31
+ return "noteoff";
32
+ case 0xb0: // Control Change
33
+ return "controlchange";
34
+ case 0xe0: // Pitch Bend
35
+ return "pitchbend";
36
+ case 0xd0: // Channel Pressure (Aftertouch)
37
+ return "channelaftertouch";
38
+ case 0xa0: // Polyphonic Key Pressure
39
+ return "keyaftertouch";
40
+ case 0xc0: // Program Change
41
+ return "programchange";
42
+ default:
43
+ return "unknown";
44
+ }
45
+ }
46
+ }
@@ -1,6 +1,7 @@
1
1
  import { Context } from "@blibliki/utils";
2
- import { MessageEvent, Input } from "webmidi";
2
+ import type { JZZMidiMessage, JZZPort } from "./jzz.types";
3
3
  import MidiEvent, { MidiEventType } from "./MidiEvent";
4
+ import Message from "./Message";
4
5
 
5
6
  export enum MidiPortState {
6
7
  connected = "connected",
@@ -25,11 +26,12 @@ export default class MidiDevice implements IMidiDevice {
25
26
  eventListerCallbacks: EventListerCallback[] = [];
26
27
 
27
28
  private context: Readonly<Context>;
28
- private input: Input;
29
+ private input: JZZPort;
30
+ private _state: MidiPortState = MidiPortState.connected;
29
31
 
30
- constructor(input: Input, context: Context) {
31
- this.id = input.id;
32
- this.name = input.name || `Device ${input.id}`;
32
+ constructor(input: JZZPort, id: string, name: string, context: Context) {
33
+ this.id = id;
34
+ this.name = name || `Device ${id}`;
33
35
  this.input = input;
34
36
  this.context = context;
35
37
 
@@ -37,17 +39,19 @@ export default class MidiDevice implements IMidiDevice {
37
39
  }
38
40
 
39
41
  get state() {
40
- return this.input.state as MidiPortState;
42
+ return this._state;
41
43
  }
42
44
 
43
45
  connect() {
44
- this.input.addListener("midimessage", (e: MessageEvent) => {
45
- this.processEvent(e);
46
+ // JZZ uses a callback function to receive MIDI messages
47
+ this.input.connect((msg: JZZMidiMessage) => {
48
+ this.processEvent(msg);
46
49
  });
47
50
  }
48
51
 
49
52
  disconnect() {
50
- this.input.removeListener();
53
+ this.input.close();
54
+ this._state = MidiPortState.disconnected;
51
55
  }
52
56
 
53
57
  serialize() {
@@ -66,10 +70,16 @@ export default class MidiDevice implements IMidiDevice {
66
70
  );
67
71
  }
68
72
 
69
- private processEvent(event: MessageEvent) {
73
+ private processEvent(msg: JZZMidiMessage) {
74
+ // Convert JZZ MIDI message to Uint8Array
75
+ const data = new Uint8Array(msg.slice());
76
+ const message = new Message(data);
77
+
78
+ // Use current time as timestamp since JZZ doesn't provide precise timestamps
79
+ const timestamp = performance.now();
70
80
  const midiEvent = new MidiEvent(
71
- event.message,
72
- this.context.browserToContextTime(event.timestamp),
81
+ message,
82
+ this.context.browserToContextTime(timestamp),
73
83
  );
74
84
 
75
85
  switch (midiEvent.type) {
@@ -1,5 +1,6 @@
1
1
  import { Context } from "@blibliki/utils";
2
- import { Input, Output, WebMidi } from "webmidi";
2
+ import JZZ from "jzz";
3
+ import type { JZZ as JZZType, JZZInputInfo, JZZPort } from "./jzz.types";
3
4
  import ComputerKeyboardDevice from "./ComputerKeyboardDevice";
4
5
  import MidiDevice from "./MidiDevice";
5
6
 
@@ -10,6 +11,7 @@ export default class MidiDeviceManager {
10
11
  private initialized = false;
11
12
  private listeners: ListenerCallback[] = [];
12
13
  private context: Readonly<Context>;
14
+ private jzz: JZZType | null = null;
13
15
 
14
16
  constructor(context: Context) {
15
17
  this.context = context;
@@ -27,6 +29,10 @@ export default class MidiDeviceManager {
27
29
  return this.devices.get(id);
28
30
  }
29
31
 
32
+ findByName(name: string): MidiDevice | ComputerKeyboardDevice | undefined {
33
+ return Array.from(this.devices.values()).find((d) => d.name === name);
34
+ }
35
+
30
36
  addListener(callback: ListenerCallback) {
31
37
  this.listeners.push(callback);
32
38
  }
@@ -35,15 +41,21 @@ export default class MidiDeviceManager {
35
41
  if (this.initialized) return;
36
42
 
37
43
  try {
38
- await WebMidi.enable();
39
-
40
- WebMidi.inputs.forEach((input) => {
41
- if (!this.devices.has(input.id)) {
42
- this.devices.set(input.id, new MidiDevice(input, this.context));
44
+ const jzz: JZZType = (await JZZ()) as unknown as JZZType;
45
+ this.jzz = jzz;
46
+ const info = jzz.info();
47
+
48
+ // Get all MIDI input devices
49
+ for (const inputInfo of info.inputs ?? []) {
50
+ const id: string = inputInfo.id ?? inputInfo.name;
51
+ if (!this.devices.has(id)) {
52
+ const port = (await jzz.openMidiIn(inputInfo.name)) as unknown as JZZPort;
53
+ const device = new MidiDevice(port, id, inputInfo.name, this.context);
54
+ this.devices.set(id, device);
43
55
  }
44
- });
56
+ }
45
57
  } catch (err) {
46
- console.error("Error enabling WebMidi:", err);
58
+ console.error("Error enabling JZZ MIDI:", err);
47
59
  }
48
60
  }
49
61
 
@@ -55,34 +67,54 @@ export default class MidiDeviceManager {
55
67
  }
56
68
 
57
69
  private listenChanges() {
58
- WebMidi.addListener("connected", (event) => {
59
- const port = event.port as Input | Output;
60
- if (port instanceof Output) return;
61
-
62
- if (this.devices.has(port.id)) return;
63
-
64
- const device = new MidiDevice(port, this.context);
65
- this.devices.set(device.id, device);
66
-
67
- this.listeners.forEach((listener) => {
68
- listener(device);
69
- });
70
- });
71
-
72
- WebMidi.addListener("disconnected", (event) => {
73
- const port = event.port as Input | Output;
74
- if (port instanceof Output) return;
75
-
76
- const device = this.devices.get(port.id);
77
- if (!device) return;
78
- if (device instanceof ComputerKeyboardDevice) return;
79
-
80
- device.disconnect();
81
- this.devices.delete(device.id);
82
-
83
- this.listeners.forEach((listener) => {
84
- listener(device);
85
- });
70
+ if (!this.jzz) return;
71
+
72
+ // JZZ watch for MIDI device changes
73
+ this.jzz.onChange(() => {
74
+ if (!this.jzz) return;
75
+
76
+ const info = this.jzz.info();
77
+ const currentInputIds = new Set<string>(
78
+ (info.inputs ?? []).map((i: JZZInputInfo) => i.id ?? i.name),
79
+ );
80
+
81
+ // Check for new devices
82
+ for (const inputInfo of info.inputs ?? []) {
83
+ const id: string = inputInfo.id ?? inputInfo.name;
84
+ if (!this.devices.has(id)) {
85
+ // New device connected
86
+ void this.jzz
87
+ .openMidiIn(inputInfo.name)
88
+ .then((port: unknown) => {
89
+ const jzzPort = port as JZZPort;
90
+ const device = new MidiDevice(
91
+ jzzPort,
92
+ id,
93
+ inputInfo.name,
94
+ this.context,
95
+ );
96
+ this.devices.set(id, device);
97
+
98
+ this.listeners.forEach((listener) => {
99
+ listener(device);
100
+ });
101
+ });
102
+ }
103
+ }
104
+
105
+ // Check for removed devices
106
+ for (const [id, device] of this.devices) {
107
+ if (device instanceof ComputerKeyboardDevice) continue;
108
+ if (!currentInputIds.has(id)) {
109
+ // Device disconnected
110
+ device.disconnect();
111
+ this.devices.delete(id);
112
+
113
+ this.listeners.forEach((listener) => {
114
+ listener(device);
115
+ });
116
+ }
117
+ }
86
118
  });
87
119
  }
88
120
  }
@@ -1,5 +1,5 @@
1
1
  import { ContextTime } from "@blibliki/transport";
2
- import { Message } from "webmidi";
2
+ import Message from "./Message";
3
3
  import Note, { INote } from "../Note";
4
4
 
5
5
  export enum MidiEventType {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Type definitions for JZZ MIDI library
3
+ */
4
+
5
+ export interface JZZInputInfo {
6
+ id?: string;
7
+ name: string;
8
+ manufacturer?: string;
9
+ version?: string;
10
+ engine?: string;
11
+ }
12
+
13
+ export interface JZZOutputInfo {
14
+ id?: string;
15
+ name: string;
16
+ manufacturer?: string;
17
+ version?: string;
18
+ engine?: string;
19
+ }
20
+
21
+ export interface JZZInfo {
22
+ inputs?: JZZInputInfo[];
23
+ outputs?: JZZOutputInfo[];
24
+ version?: string;
25
+ engine?: string;
26
+ }
27
+
28
+ export interface JZZMidiMessage extends Array<number> {
29
+ slice(): number[];
30
+ }
31
+
32
+ export interface JZZPort {
33
+ connect(callback: (msg: JZZMidiMessage) => void): JZZPort;
34
+ close(): void;
35
+ disconnect(): void;
36
+ }
37
+
38
+ // JZZ uses a custom Promise-like type called Async
39
+ export interface JZZAsync<T> {
40
+ then<TResult1 = T, TResult2 = never>(
41
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
42
+ onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
43
+ ): JZZAsync<TResult1 | TResult2>;
44
+ }
45
+
46
+ export interface JZZ {
47
+ info(): JZZInfo;
48
+ openMidiIn(name: string): JZZAsync<JZZPort>;
49
+ openMidiOut(name: string): JZZAsync<JZZPort>;
50
+ onChange(callback: () => void): void;
51
+ }
@@ -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() {