@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/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -2
- package/dist/index.d.ts +29 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/core/Note/index.ts +1 -1
- package/src/core/midi/Message.ts +46 -0
- package/src/core/midi/MidiDevice.ts +22 -12
- package/src/core/midi/MidiDeviceManager.ts +64 -36
- package/src/core/midi/MidiEvent.ts +1 -1
- package/src/core/midi/jzz.types.ts +51 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blibliki/engine",
|
|
3
|
-
"version": "0.3.
|
|
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
|
-
"
|
|
23
|
-
"@blibliki/transport": "^0.3.
|
|
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",
|
package/src/core/Note/index.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
29
|
+
private input: JZZPort;
|
|
30
|
+
private _state: MidiPortState = MidiPortState.connected;
|
|
29
31
|
|
|
30
|
-
constructor(input:
|
|
31
|
-
this.id =
|
|
32
|
-
this.name =
|
|
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.
|
|
42
|
+
return this._state;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
connect() {
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
72
|
-
this.context.browserToContextTime(
|
|
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
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (this.
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|