@blibliki/engine 0.3.9 → 0.4.0
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.d.ts +21 -12
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +11 -9
- package/src/core/IO/AudioIO.ts +1 -0
- package/src/core/midi/MidiDevice.ts +19 -22
- package/src/core/midi/MidiDeviceManager.ts +44 -62
- 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/src/modules/Constant.ts +1 -0
- package/src/modules/Envelope.ts +1 -0
- package/src/modules/Filter.ts +1 -0
- package/src/modules/Gain.ts +1 -0
- package/src/modules/Inspector.ts +1 -0
- package/src/modules/Oscillator.ts +1 -0
- package/src/modules/StereoPanner.ts +1 -0
- package/dist/index.cjs +0 -2
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -851
- package/src/core/midi/jzz.types.ts +0 -51
- package/src/nodePolyfill.ts +0 -25
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blibliki/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"import": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
9
12
|
"files": [
|
|
10
13
|
"README.md",
|
|
11
14
|
"src",
|
|
@@ -17,11 +20,10 @@
|
|
|
17
20
|
"vitest": "^4.0.6"
|
|
18
21
|
},
|
|
19
22
|
"dependencies": {
|
|
23
|
+
"@julusian/midi": "^3.6.1",
|
|
20
24
|
"es-toolkit": "^1.41.0",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"@blibliki/utils": "^0.3.9",
|
|
24
|
-
"@blibliki/transport": "^0.3.9"
|
|
25
|
+
"@blibliki/transport": "^0.4.0",
|
|
26
|
+
"@blibliki/utils": "^0.4.0"
|
|
25
27
|
},
|
|
26
28
|
"scripts": {
|
|
27
29
|
"build": "tsup",
|
package/src/core/IO/AudioIO.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Context } from "@blibliki/utils";
|
|
2
|
-
import type { JZZMidiMessage, JZZPort } from "./jzz.types";
|
|
3
|
-
import MidiEvent, { MidiEventType } from "./MidiEvent";
|
|
4
2
|
import Message from "./Message";
|
|
3
|
+
import MidiEvent, { MidiEventType } from "./MidiEvent";
|
|
4
|
+
import type { IMidiInputPort, IMidiMessageEvent } from "./adapters";
|
|
5
5
|
|
|
6
6
|
export enum MidiPortState {
|
|
7
7
|
connected = "connected",
|
|
@@ -26,12 +26,12 @@ export default class MidiDevice implements IMidiDevice {
|
|
|
26
26
|
eventListerCallbacks: EventListerCallback[] = [];
|
|
27
27
|
|
|
28
28
|
private context: Readonly<Context>;
|
|
29
|
-
private input:
|
|
30
|
-
private
|
|
29
|
+
private input: IMidiInputPort;
|
|
30
|
+
private messageHandler: ((event: IMidiMessageEvent) => void) | null = null;
|
|
31
31
|
|
|
32
|
-
constructor(input:
|
|
33
|
-
this.id = id;
|
|
34
|
-
this.name = name
|
|
32
|
+
constructor(input: IMidiInputPort, context: Context) {
|
|
33
|
+
this.id = input.id;
|
|
34
|
+
this.name = input.name;
|
|
35
35
|
this.input = input;
|
|
36
36
|
this.context = context;
|
|
37
37
|
|
|
@@ -39,19 +39,21 @@ export default class MidiDevice implements IMidiDevice {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
get state() {
|
|
42
|
-
return this.
|
|
42
|
+
return this.input.state as MidiPortState;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
connect() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
this.messageHandler = (e: IMidiMessageEvent) => {
|
|
47
|
+
this.processEvent(e);
|
|
48
|
+
};
|
|
49
|
+
this.input.addEventListener(this.messageHandler);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
disconnect() {
|
|
53
|
-
this.
|
|
54
|
-
|
|
53
|
+
if (this.messageHandler) {
|
|
54
|
+
this.input.removeEventListener(this.messageHandler);
|
|
55
|
+
this.messageHandler = null;
|
|
56
|
+
}
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
serialize() {
|
|
@@ -70,16 +72,11 @@ export default class MidiDevice implements IMidiDevice {
|
|
|
70
72
|
);
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
private processEvent(
|
|
74
|
-
|
|
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();
|
|
75
|
+
private processEvent(event: IMidiMessageEvent) {
|
|
76
|
+
const message = new Message(event.data);
|
|
80
77
|
const midiEvent = new MidiEvent(
|
|
81
78
|
message,
|
|
82
|
-
this.context.browserToContextTime(
|
|
79
|
+
this.context.browserToContextTime(event.timeStamp),
|
|
83
80
|
);
|
|
84
81
|
|
|
85
82
|
switch (midiEvent.type) {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Context } from "@blibliki/utils";
|
|
2
|
-
import JZZ from "jzz";
|
|
3
|
-
import type { JZZ as JZZType, JZZInputInfo, JZZPort } from "./jzz.types";
|
|
4
2
|
import ComputerKeyboardDevice from "./ComputerKeyboardDevice";
|
|
5
3
|
import MidiDevice from "./MidiDevice";
|
|
4
|
+
import { createMidiAdapter, type IMidiAccess } from "./adapters";
|
|
6
5
|
|
|
7
6
|
type ListenerCallback = (device: MidiDevice) => void;
|
|
8
7
|
|
|
@@ -11,7 +10,8 @@ export default class MidiDeviceManager {
|
|
|
11
10
|
private initialized = false;
|
|
12
11
|
private listeners: ListenerCallback[] = [];
|
|
13
12
|
private context: Readonly<Context>;
|
|
14
|
-
private
|
|
13
|
+
private midiAccess: IMidiAccess | null = null;
|
|
14
|
+
private adapter = createMidiAdapter();
|
|
15
15
|
|
|
16
16
|
constructor(context: Context) {
|
|
17
17
|
this.context = context;
|
|
@@ -41,21 +41,25 @@ export default class MidiDeviceManager {
|
|
|
41
41
|
if (this.initialized) return;
|
|
42
42
|
|
|
43
43
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
44
|
+
if (!this.adapter.isSupported()) {
|
|
45
|
+
console.warn("MIDI is not supported on this platform");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
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()) {
|
|
57
|
+
if (!this.devices.has(input.id)) {
|
|
58
|
+
this.devices.set(input.id, new MidiDevice(input, this.context));
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
} catch (err) {
|
|
58
|
-
console.error("Error enabling
|
|
62
|
+
console.error("Error enabling MIDI:", err);
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -67,53 +71,31 @@ export default class MidiDeviceManager {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
private listenChanges() {
|
|
70
|
-
if (!this.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
}
|
|
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
|
+
});
|
|
117
99
|
}
|
|
118
100
|
});
|
|
119
101
|
}
|
|
@@ -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
|
+
}
|
package/src/modules/Constant.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ContextTime } from "@blibliki/transport";
|
|
2
2
|
import { Context } from "@blibliki/utils";
|
|
3
|
+
import { ConstantSourceNode } from "@blibliki/utils/web-audio-api";
|
|
3
4
|
import { IModule, Module, ModulePropSchema, SetterHooks } from "@/core";
|
|
4
5
|
import Note from "@/core/Note";
|
|
5
6
|
import { ICreateModule, ModuleType } from ".";
|
package/src/modules/Envelope.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ContextTime } from "@blibliki/transport";
|
|
2
2
|
import { Context, cancelAndHoldAtTime } from "@blibliki/utils";
|
|
3
|
+
import { GainNode } from "@blibliki/utils/web-audio-api";
|
|
3
4
|
import { Module } from "@/core";
|
|
4
5
|
import Note from "@/core/Note";
|
|
5
6
|
import { IModuleConstructor } from "@/core/module/Module";
|
package/src/modules/Filter.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context } from "@blibliki/utils";
|
|
2
|
+
import { BiquadFilterNode } from "@blibliki/utils/web-audio-api";
|
|
2
3
|
import { EnumProp, ModulePropSchema } from "@/core";
|
|
3
4
|
import { IModuleConstructor, Module, SetterHooks } from "@/core/module/Module";
|
|
4
5
|
import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
|
package/src/modules/Gain.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context } from "@blibliki/utils";
|
|
2
|
+
import { GainNode } from "@blibliki/utils/web-audio-api";
|
|
2
3
|
import { IModule, Module, ModulePropSchema, SetterHooks } from "@/core";
|
|
3
4
|
import { IModuleConstructor } from "@/core/module/Module";
|
|
4
5
|
import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
|
package/src/modules/Inspector.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context } from "@blibliki/utils";
|
|
2
|
+
import { AnalyserNode } from "@blibliki/utils/web-audio-api";
|
|
2
3
|
import { IModule, Module, SetterHooks } from "@/core";
|
|
3
4
|
import { EnumProp, ModulePropSchema } from "@/core/schema";
|
|
4
5
|
import { ICreateModule, ModuleType } from ".";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ContextTime } from "@blibliki/transport";
|
|
2
2
|
import { Context, dbToGain } from "@blibliki/utils";
|
|
3
|
+
import { GainNode, OscillatorNode } from "@blibliki/utils/web-audio-api";
|
|
3
4
|
import { IModule, Module } from "@/core";
|
|
4
5
|
import Note from "@/core/Note";
|
|
5
6
|
import { IModuleConstructor, SetterHooks } from "@/core/module/Module";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context } from "@blibliki/utils";
|
|
2
|
+
import { StereoPannerNode } from "@blibliki/utils/web-audio-api";
|
|
2
3
|
import { IModule, Module, ModulePropSchema } from "@/core";
|
|
3
4
|
import { IModuleConstructor, SetterHooks } from "@/core/module/Module";
|
|
4
5
|
import { IPolyModuleConstructor, PolyModule } from "@/core/module/PolyModule";
|