@blibliki/engine 0.3.8 → 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.8",
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.8",
24
- "@blibliki/utils": "^0.3.8"
23
+ "@blibliki/utils": "^0.3.9",
24
+ "@blibliki/transport": "^0.3.9"
25
25
  },
26
26
  "scripts": {
27
27
  "build": "tsup",
@@ -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"];
@@ -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;
@@ -39,15 +41,21 @@ export default class MidiDeviceManager {
39
41
  if (this.initialized) return;
40
42
 
41
43
  try {
42
- await WebMidi.enable();
43
-
44
- WebMidi.inputs.forEach((input) => {
45
- if (!this.devices.has(input.id)) {
46
- 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);
47
55
  }
48
- });
56
+ }
49
57
  } catch (err) {
50
- console.error("Error enabling WebMidi:", err);
58
+ console.error("Error enabling JZZ MIDI:", err);
51
59
  }
52
60
  }
53
61
 
@@ -59,34 +67,54 @@ export default class MidiDeviceManager {
59
67
  }
60
68
 
61
69
  private listenChanges() {
62
- WebMidi.addListener("connected", (event) => {
63
- const port = event.port as Input | Output;
64
- if (port instanceof Output) return;
65
-
66
- if (this.devices.has(port.id)) return;
67
-
68
- const device = new MidiDevice(port, this.context);
69
- this.devices.set(device.id, device);
70
-
71
- this.listeners.forEach((listener) => {
72
- listener(device);
73
- });
74
- });
75
-
76
- WebMidi.addListener("disconnected", (event) => {
77
- const port = event.port as Input | Output;
78
- if (port instanceof Output) return;
79
-
80
- const device = this.devices.get(port.id);
81
- if (!device) return;
82
- if (device instanceof ComputerKeyboardDevice) return;
83
-
84
- device.disconnect();
85
- this.devices.delete(device.id);
86
-
87
- this.listeners.forEach((listener) => {
88
- listener(device);
89
- });
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
+ }
90
118
  });
91
119
  }
92
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
+ }