@blibliki/engine 0.3.8 → 0.3.10
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 +38 -2
- package/dist/index.d.ts +38 -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 +17 -10
- package/src/core/midi/MidiDeviceManager.ts +43 -33
- package/src/core/midi/MidiEvent.ts +1 -1
- package/src/core/midi/adapters/NodeMidiAdapter.ts +174 -0
- package/src/core/midi/adapters/WebMidiAdapter.ts +120 -0
- package/src/core/midi/adapters/index.ts +23 -0
- package/src/core/midi/adapters/types.ts +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blibliki/engine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"source": "src/index.ts",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
"vitest": "^4.0.6"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@julusian/midi": "^3.6.1",
|
|
20
21
|
"es-toolkit": "^1.41.0",
|
|
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.10",
|
|
24
|
+
"@blibliki/transport": "^0.3.10"
|
|
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 Message from "./Message";
|
|
3
3
|
import MidiEvent, { MidiEventType } from "./MidiEvent";
|
|
4
|
+
import type { IMidiInputPort, IMidiMessageEvent } from "./adapters";
|
|
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: IMidiInputPort;
|
|
30
|
+
private messageHandler: ((event: IMidiMessageEvent) => void) | null = null;
|
|
29
31
|
|
|
30
|
-
constructor(input:
|
|
32
|
+
constructor(input: IMidiInputPort, context: Context) {
|
|
31
33
|
this.id = input.id;
|
|
32
|
-
this.name = input.name
|
|
34
|
+
this.name = input.name;
|
|
33
35
|
this.input = input;
|
|
34
36
|
this.context = context;
|
|
35
37
|
|
|
@@ -41,13 +43,17 @@ export default class MidiDevice implements IMidiDevice {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
connect() {
|
|
44
|
-
this.
|
|
46
|
+
this.messageHandler = (e: IMidiMessageEvent) => {
|
|
45
47
|
this.processEvent(e);
|
|
46
|
-
}
|
|
48
|
+
};
|
|
49
|
+
this.input.addEventListener(this.messageHandler);
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
disconnect() {
|
|
50
|
-
this.
|
|
53
|
+
if (this.messageHandler) {
|
|
54
|
+
this.input.removeEventListener(this.messageHandler);
|
|
55
|
+
this.messageHandler = null;
|
|
56
|
+
}
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
serialize() {
|
|
@@ -66,10 +72,11 @@ export default class MidiDevice implements IMidiDevice {
|
|
|
66
72
|
);
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
private processEvent(event:
|
|
75
|
+
private processEvent(event: IMidiMessageEvent) {
|
|
76
|
+
const message = new Message(event.data);
|
|
70
77
|
const midiEvent = new MidiEvent(
|
|
71
|
-
|
|
72
|
-
this.context.browserToContextTime(event.
|
|
78
|
+
message,
|
|
79
|
+
this.context.browserToContextTime(event.timeStamp),
|
|
73
80
|
);
|
|
74
81
|
|
|
75
82
|
switch (midiEvent.type) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Context } from "@blibliki/utils";
|
|
2
|
-
import { Input, Output, WebMidi } from "webmidi";
|
|
3
2
|
import ComputerKeyboardDevice from "./ComputerKeyboardDevice";
|
|
4
3
|
import MidiDevice from "./MidiDevice";
|
|
4
|
+
import { createMidiAdapter, type IMidiAccess } from "./adapters";
|
|
5
5
|
|
|
6
6
|
type ListenerCallback = (device: MidiDevice) => void;
|
|
7
7
|
|
|
@@ -10,6 +10,8 @@ export default class MidiDeviceManager {
|
|
|
10
10
|
private initialized = false;
|
|
11
11
|
private listeners: ListenerCallback[] = [];
|
|
12
12
|
private context: Readonly<Context>;
|
|
13
|
+
private midiAccess: IMidiAccess | null = null;
|
|
14
|
+
private adapter = createMidiAdapter();
|
|
13
15
|
|
|
14
16
|
constructor(context: Context) {
|
|
15
17
|
this.context = context;
|
|
@@ -39,15 +41,25 @@ export default class MidiDeviceManager {
|
|
|
39
41
|
if (this.initialized) return;
|
|
40
42
|
|
|
41
43
|
try {
|
|
42
|
-
|
|
44
|
+
if (!this.adapter.isSupported()) {
|
|
45
|
+
console.warn("MIDI is not supported on this platform");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
this.midiAccess = await this.adapter.requestMIDIAccess();
|
|
50
|
+
|
|
51
|
+
if (!this.midiAccess) {
|
|
52
|
+
console.error("Failed to get MIDI access");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const input of this.midiAccess.inputs()) {
|
|
45
57
|
if (!this.devices.has(input.id)) {
|
|
46
58
|
this.devices.set(input.id, new MidiDevice(input, this.context));
|
|
47
59
|
}
|
|
48
|
-
}
|
|
60
|
+
}
|
|
49
61
|
} catch (err) {
|
|
50
|
-
console.error("Error enabling
|
|
62
|
+
console.error("Error enabling MIDI:", err);
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
65
|
|
|
@@ -59,34 +71,32 @@ export default class MidiDeviceManager {
|
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
private listenChanges() {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
listener
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
listener(device);
|
|
89
|
-
});
|
|
74
|
+
if (!this.midiAccess) return;
|
|
75
|
+
|
|
76
|
+
this.midiAccess.addEventListener("statechange", (port) => {
|
|
77
|
+
if (port.state === "connected") {
|
|
78
|
+
// Device connected
|
|
79
|
+
if (this.devices.has(port.id)) return;
|
|
80
|
+
|
|
81
|
+
const device = new MidiDevice(port, this.context);
|
|
82
|
+
this.devices.set(device.id, device);
|
|
83
|
+
|
|
84
|
+
this.listeners.forEach((listener) => {
|
|
85
|
+
listener(device);
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
// Device disconnected
|
|
89
|
+
const device = this.devices.get(port.id);
|
|
90
|
+
if (!device) return;
|
|
91
|
+
if (device instanceof ComputerKeyboardDevice) return;
|
|
92
|
+
|
|
93
|
+
device.disconnect();
|
|
94
|
+
this.devices.delete(device.id);
|
|
95
|
+
|
|
96
|
+
this.listeners.forEach((listener) => {
|
|
97
|
+
listener(device);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
90
100
|
});
|
|
91
101
|
}
|
|
92
102
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node-midi adapter for Node.js
|
|
3
|
+
*/
|
|
4
|
+
import { isNode } from "es-toolkit";
|
|
5
|
+
import type {
|
|
6
|
+
IMidiAccess,
|
|
7
|
+
IMidiAdapter,
|
|
8
|
+
IMidiInputPort,
|
|
9
|
+
MidiMessageCallback,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
// Dynamic import type for node-midi
|
|
13
|
+
type NodeMidiInput = {
|
|
14
|
+
getPortCount(): number;
|
|
15
|
+
getPortName(port: number): string;
|
|
16
|
+
openPort(port: number): void;
|
|
17
|
+
closePort(): void;
|
|
18
|
+
on(
|
|
19
|
+
event: "message",
|
|
20
|
+
callback: (deltaTime: number, message: number[]) => void,
|
|
21
|
+
): void;
|
|
22
|
+
off(
|
|
23
|
+
event: "message",
|
|
24
|
+
callback: (deltaTime: number, message: number[]) => void,
|
|
25
|
+
): void;
|
|
26
|
+
isPortOpen(): boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type NodeMidiModule = {
|
|
30
|
+
Input: new () => NodeMidiInput;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
class NodeMidiInputPort implements IMidiInputPort {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly name: string;
|
|
36
|
+
private portIndex: number;
|
|
37
|
+
private input: NodeMidiInput;
|
|
38
|
+
private callbacks = new Set<MidiMessageCallback>();
|
|
39
|
+
private handler: ((deltaTime: number, message: number[]) => void) | null =
|
|
40
|
+
null;
|
|
41
|
+
private _state: "connected" | "disconnected" = "disconnected";
|
|
42
|
+
|
|
43
|
+
constructor(portIndex: number, name: string, input: NodeMidiInput) {
|
|
44
|
+
this.portIndex = portIndex;
|
|
45
|
+
this.id = `node-midi-${portIndex}`;
|
|
46
|
+
this.name = name;
|
|
47
|
+
this.input = input;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get state(): "connected" | "disconnected" {
|
|
51
|
+
return this._state;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setState(state: "connected" | "disconnected"): void {
|
|
55
|
+
this._state = state;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
addEventListener(callback: MidiMessageCallback): void {
|
|
59
|
+
if (this.callbacks.size === 0) {
|
|
60
|
+
this.handler = (_deltaTime: number, message: number[]) => {
|
|
61
|
+
const event = {
|
|
62
|
+
data: new Uint8Array(message),
|
|
63
|
+
timeStamp: performance.now(),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.callbacks.forEach((cb) => {
|
|
67
|
+
cb(event);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
if (!this.input.isPortOpen()) {
|
|
73
|
+
this.input.openPort(this.portIndex);
|
|
74
|
+
this._state = "connected";
|
|
75
|
+
}
|
|
76
|
+
this.input.on("message", this.handler);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(`Error opening MIDI port ${this.portIndex}:`, err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
this.callbacks.add(callback);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
removeEventListener(callback: MidiMessageCallback): void {
|
|
85
|
+
this.callbacks.delete(callback);
|
|
86
|
+
|
|
87
|
+
if (this.callbacks.size === 0 && this.handler) {
|
|
88
|
+
try {
|
|
89
|
+
this.input.off("message", this.handler);
|
|
90
|
+
if (this.input.isPortOpen()) {
|
|
91
|
+
this.input.closePort();
|
|
92
|
+
this._state = "disconnected";
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(`Error closing MIDI port ${this.portIndex}:`, err);
|
|
96
|
+
}
|
|
97
|
+
this.handler = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class NodeMidiAccess implements IMidiAccess {
|
|
103
|
+
private ports = new Map<string, NodeMidiInputPort>();
|
|
104
|
+
private MidiModule: NodeMidiModule;
|
|
105
|
+
|
|
106
|
+
constructor(MidiModule: NodeMidiModule) {
|
|
107
|
+
this.MidiModule = MidiModule;
|
|
108
|
+
this.scanPorts();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private scanPorts(): void {
|
|
112
|
+
try {
|
|
113
|
+
const input = new this.MidiModule.Input();
|
|
114
|
+
const portCount = input.getPortCount();
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < portCount; i++) {
|
|
117
|
+
const portName = input.getPortName(i);
|
|
118
|
+
const id = `node-midi-${i}`;
|
|
119
|
+
|
|
120
|
+
if (!this.ports.has(id)) {
|
|
121
|
+
// Create a new input instance for each port
|
|
122
|
+
const portInput = new this.MidiModule.Input();
|
|
123
|
+
const port = new NodeMidiInputPort(i, portName, portInput);
|
|
124
|
+
this.ports.set(id, port);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Clean up the scanning input
|
|
129
|
+
if (input.isPortOpen()) {
|
|
130
|
+
input.closePort();
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error("Error scanning MIDI ports:", err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
*inputs(): IterableIterator<IMidiInputPort> {
|
|
138
|
+
for (const [, port] of this.ports) {
|
|
139
|
+
yield port;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
addEventListener(
|
|
144
|
+
_event: "statechange",
|
|
145
|
+
_callback: (port: IMidiInputPort) => void,
|
|
146
|
+
): void {
|
|
147
|
+
// node-midi doesn't support hot-plugging detection
|
|
148
|
+
// This could be implemented with polling if needed
|
|
149
|
+
console.warn(
|
|
150
|
+
"Hot-plug detection not supported with node-midi adapter. Restart required for new devices.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default class NodeMidiAdapter implements IMidiAdapter {
|
|
156
|
+
async requestMIDIAccess(): Promise<IMidiAccess | null> {
|
|
157
|
+
try {
|
|
158
|
+
// Dynamic import to avoid bundling in browser builds
|
|
159
|
+
const midi = (await import("@julusian/midi")) as
|
|
160
|
+
| NodeMidiModule
|
|
161
|
+
| { default: NodeMidiModule };
|
|
162
|
+
const midiModule = "default" in midi ? midi.default : midi;
|
|
163
|
+
return new NodeMidiAccess(midiModule);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error("Error loading node-midi:", err);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
isSupported(): boolean {
|
|
171
|
+
// Check if we're in Node.js environment
|
|
172
|
+
return isNode();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web MIDI API adapter for browsers
|
|
3
|
+
*/
|
|
4
|
+
import type {
|
|
5
|
+
IMidiAccess,
|
|
6
|
+
IMidiAdapter,
|
|
7
|
+
IMidiInputPort,
|
|
8
|
+
MidiMessageCallback,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
class WebMidiInputPort implements IMidiInputPort {
|
|
12
|
+
private input: MIDIInput;
|
|
13
|
+
private callbacks = new Set<MidiMessageCallback>();
|
|
14
|
+
private handler: ((e: MIDIMessageEvent) => void) | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(input: MIDIInput) {
|
|
17
|
+
this.input = input;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get id(): string {
|
|
21
|
+
return this.input.id;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get name(): string {
|
|
25
|
+
return this.input.name ?? `Device ${this.input.id}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get state(): "connected" | "disconnected" {
|
|
29
|
+
return this.input.state as "connected" | "disconnected";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
addEventListener(callback: MidiMessageCallback): void {
|
|
33
|
+
if (this.callbacks.size === 0) {
|
|
34
|
+
this.handler = (e: MIDIMessageEvent) => {
|
|
35
|
+
if (!e.data) return;
|
|
36
|
+
|
|
37
|
+
const event = {
|
|
38
|
+
data: e.data,
|
|
39
|
+
timeStamp: e.timeStamp,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.callbacks.forEach((cb) => {
|
|
43
|
+
cb(event);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
this.input.addEventListener("midimessage", this.handler);
|
|
47
|
+
}
|
|
48
|
+
this.callbacks.add(callback);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
removeEventListener(callback: MidiMessageCallback): void {
|
|
52
|
+
this.callbacks.delete(callback);
|
|
53
|
+
|
|
54
|
+
if (this.callbacks.size === 0 && this.handler) {
|
|
55
|
+
this.input.removeEventListener("midimessage", this.handler);
|
|
56
|
+
this.handler = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class WebMidiAccess implements IMidiAccess {
|
|
62
|
+
private midiAccess: MIDIAccess;
|
|
63
|
+
private portCache = new Map<string, WebMidiInputPort>();
|
|
64
|
+
|
|
65
|
+
constructor(midiAccess: MIDIAccess) {
|
|
66
|
+
this.midiAccess = midiAccess;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
*inputs(): IterableIterator<IMidiInputPort> {
|
|
70
|
+
for (const [, input] of this.midiAccess.inputs) {
|
|
71
|
+
if (!this.portCache.has(input.id)) {
|
|
72
|
+
this.portCache.set(input.id, new WebMidiInputPort(input));
|
|
73
|
+
}
|
|
74
|
+
yield this.portCache.get(input.id)!;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
addEventListener(
|
|
79
|
+
event: "statechange",
|
|
80
|
+
callback: (port: IMidiInputPort) => void,
|
|
81
|
+
): void {
|
|
82
|
+
this.midiAccess.addEventListener(event, (e) => {
|
|
83
|
+
const port = e.port;
|
|
84
|
+
if (!port || port.type !== "input") return;
|
|
85
|
+
|
|
86
|
+
const input = port as MIDIInput;
|
|
87
|
+
if (!this.portCache.has(input.id)) {
|
|
88
|
+
this.portCache.set(input.id, new WebMidiInputPort(input));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
callback(this.portCache.get(input.id)!);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default class WebMidiAdapter implements IMidiAdapter {
|
|
97
|
+
async requestMIDIAccess(): Promise<IMidiAccess | null> {
|
|
98
|
+
try {
|
|
99
|
+
if (
|
|
100
|
+
typeof navigator === "undefined" ||
|
|
101
|
+
typeof navigator.requestMIDIAccess !== "function"
|
|
102
|
+
) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const midiAccess = await navigator.requestMIDIAccess();
|
|
107
|
+
return new WebMidiAccess(midiAccess);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error("Error enabling Web MIDI API:", err);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
isSupported(): boolean {
|
|
115
|
+
return (
|
|
116
|
+
typeof navigator !== "undefined" &&
|
|
117
|
+
typeof navigator.requestMIDIAccess === "function"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIDI adapter factory
|
|
3
|
+
* Automatically selects the correct MIDI implementation based on the platform
|
|
4
|
+
*/
|
|
5
|
+
import { isNode } from "es-toolkit";
|
|
6
|
+
import NodeMidiAdapter from "./NodeMidiAdapter";
|
|
7
|
+
import WebMidiAdapter from "./WebMidiAdapter";
|
|
8
|
+
import type { IMidiAdapter } from "./types";
|
|
9
|
+
|
|
10
|
+
export * from "./types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates the appropriate MIDI adapter for the current platform
|
|
14
|
+
* @returns The MIDI adapter (Web MIDI API for browsers, node-midi for Node.js)
|
|
15
|
+
*/
|
|
16
|
+
export function createMidiAdapter(): IMidiAdapter {
|
|
17
|
+
if (isNode()) {
|
|
18
|
+
return new NodeMidiAdapter();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Default to Web MIDI API for browsers
|
|
22
|
+
return new WebMidiAdapter();
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic MIDI interfaces
|
|
3
|
+
* Allows switching between Web MIDI API and node-midi without refactoring
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface IMidiMessageEvent {
|
|
7
|
+
data: Uint8Array;
|
|
8
|
+
timeStamp: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type MidiMessageCallback = (event: IMidiMessageEvent) => void;
|
|
12
|
+
|
|
13
|
+
export interface IMidiInputPort {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly state: "connected" | "disconnected";
|
|
17
|
+
addEventListener(callback: MidiMessageCallback): void;
|
|
18
|
+
removeEventListener(callback: MidiMessageCallback): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IMidiAccess {
|
|
22
|
+
inputs(): IterableIterator<IMidiInputPort>;
|
|
23
|
+
addEventListener(
|
|
24
|
+
event: "statechange",
|
|
25
|
+
callback: (port: IMidiInputPort) => void,
|
|
26
|
+
): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IMidiAdapter {
|
|
30
|
+
requestMIDIAccess(): Promise<IMidiAccess | null>;
|
|
31
|
+
isSupported(): boolean;
|
|
32
|
+
}
|