@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/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -215
- package/dist/index.d.ts +218 -215
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/Engine.ts +52 -2
- package/src/core/Note/index.ts +1 -1
- package/src/core/Route.ts +14 -2
- package/src/core/midi/Message.ts +46 -0
- package/src/core/midi/MidiDevice.ts +22 -12
- package/src/core/midi/MidiDeviceManager.ts +68 -36
- package/src/core/midi/MidiEvent.ts +1 -1
- package/src/core/midi/jzz.types.ts +51 -0
- package/src/core/module/Module.ts +0 -38
- package/src/core/module/PolyModule.ts +1 -6
- package/src/index.ts +1 -1
- package/src/modules/MidiSelector.ts +29 -9
- package/src/modules/Oscillator.ts +6 -7
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.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 {
|
|
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 {
|
|
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>,
|
package/src/core/Note/index.ts
CHANGED
package/src/core/Route.ts
CHANGED
|
@@ -37,9 +37,21 @@ export class Routes {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
clear() {
|
|
40
|
-
|
|
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 {
|
|
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;
|
|
@@ -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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (this.
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
}
|
|
@@ -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,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 = {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
64
|
-
|
|
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() {
|