@blibliki/engine 0.1.26 → 0.3.1
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/README.md +252 -76
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +765 -0
- package/dist/index.d.ts +765 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -31
- package/src/Engine.ts +158 -177
- package/src/core/IO/AudioIO.ts +72 -0
- package/src/core/IO/Base.ts +118 -0
- package/src/core/IO/Collection.ts +123 -47
- package/src/core/IO/MidiIO.ts +43 -0
- package/src/core/IO/PolyAudioIO.ts +115 -0
- package/src/core/IO/index.ts +7 -61
- package/src/core/Note/frequencyTable.ts +144 -144
- package/src/core/Note/index.ts +49 -59
- package/src/core/Route.ts +79 -0
- package/src/core/Timing/Scheduler.ts +37 -0
- package/src/core/Timing/Time.ts +103 -0
- package/src/core/Timing/Transport.ts +104 -0
- package/src/core/Timing/index.ts +16 -0
- package/src/core/index.ts +36 -0
- package/src/core/midi/{ComputerKeyboardInput.ts → ComputerKeyboardDevice.ts} +31 -11
- package/src/core/midi/MidiDevice.ts +38 -31
- package/src/core/midi/MidiDeviceManager.ts +54 -55
- package/src/core/midi/MidiEvent.ts +36 -60
- package/src/core/module/Module.ts +233 -0
- package/src/core/module/PolyModule.ts +246 -0
- package/src/core/module/VoiceScheduler.ts +121 -0
- package/src/core/module/index.ts +3 -0
- package/src/core/schema.ts +41 -0
- package/src/index.ts +31 -9
- package/src/modules/BiquadFilter.ts +162 -0
- package/src/modules/Constant.ts +72 -0
- package/src/modules/Envelope.ts +178 -0
- package/src/modules/Filter.ts +109 -104
- package/src/modules/Gain.ts +78 -0
- package/src/modules/Inspector.ts +59 -0
- package/src/modules/Master.ts +18 -21
- package/src/modules/MidiSelector.ts +50 -50
- package/src/modules/Oscillator.ts +202 -148
- package/src/modules/Scale.ts +79 -0
- package/src/modules/StepSequencer.ts +61 -0
- package/src/modules/VirtualMidi.ts +33 -49
- package/src/modules/index.ts +159 -74
- package/src/nodePolyfill.ts +25 -0
- package/src/processors/filter-processor.ts +82 -0
- package/src/processors/index.ts +28 -0
- package/src/processors/scale-processor.ts +81 -0
- package/dist/build/Engine.d.ts +0 -82
- package/dist/build/core/IO/AudioNode.d.ts +0 -35
- package/dist/build/core/IO/Collection.d.ts +0 -13
- package/dist/build/core/IO/ForwardNode/Base.d.ts +0 -18
- package/dist/build/core/IO/ForwardNode/index.d.ts +0 -27
- package/dist/build/core/IO/ForwardNode.d.ts +0 -38
- package/dist/build/core/IO/MidiNode.d.ts +0 -30
- package/dist/build/core/IO/Node.d.ts +0 -40
- package/dist/build/core/IO/index.d.ts +0 -21
- package/dist/build/core/Module/MonoModule.d.ts +0 -67
- package/dist/build/core/Module/PolyModule.d.ts +0 -61
- package/dist/build/core/Module/index.d.ts +0 -6
- package/dist/build/core/Note/frequencyTable.d.ts +0 -4
- package/dist/build/core/Note/index.d.ts +0 -28
- package/dist/build/core/midi/ComputerKeyboardInput.d.ts +0 -11
- package/dist/build/core/midi/MidiDevice.d.ts +0 -29
- package/dist/build/core/midi/MidiDeviceManager.d.ts +0 -14
- package/dist/build/core/midi/MidiEvent.d.ts +0 -21
- package/dist/build/core/midi/index.d.ts +0 -5
- package/dist/build/index.d.ts +0 -9
- package/dist/build/modules/BitCrusher.d.ts +0 -16
- package/dist/build/modules/Delay.d.ts +0 -19
- package/dist/build/modules/Distortion.d.ts +0 -16
- package/dist/build/modules/Effect.d.ts +0 -19
- package/dist/build/modules/Envelope/AmpEnvelope.d.ts +0 -18
- package/dist/build/modules/Envelope/Base.d.ts +0 -66
- package/dist/build/modules/Envelope/FreqEnvelope.d.ts +0 -25
- package/dist/build/modules/Envelope/index.d.ts +0 -3
- package/dist/build/modules/Filter.d.ts +0 -42
- package/dist/build/modules/LFO.d.ts +0 -44
- package/dist/build/modules/Master.d.ts +0 -11
- package/dist/build/modules/MidiSelector.d.ts +0 -21
- package/dist/build/modules/Oscillator.d.ts +0 -55
- package/dist/build/modules/Reverb.d.ts +0 -19
- package/dist/build/modules/Sequencer.d.ts +0 -42
- package/dist/build/modules/VirtualMidi.d.ts +0 -32
- package/dist/build/modules/VoiceScheduler.d.ts +0 -44
- package/dist/build/modules/Volume.d.ts +0 -26
- package/dist/build/modules/index.d.ts +0 -17
- package/dist/build/routes.d.ts +0 -18
- package/dist/build/types.d.ts +0 -5
- package/dist/build/utils.d.ts +0 -1
- package/dist/main.cjs.js +0 -25
- package/dist/main.cjs.js.map +0 -1
- package/dist/main.esm.js +0 -25
- package/dist/main.esm.js.map +0 -1
- package/src/core/IO/AudioNode.ts +0 -82
- package/src/core/IO/ForwardNode/Base.ts +0 -99
- package/src/core/IO/ForwardNode/index.ts +0 -60
- package/src/core/IO/MidiNode.ts +0 -67
- package/src/core/IO/Node.ts +0 -118
- package/src/core/Module/MonoModule.ts +0 -219
- package/src/core/Module/PolyModule.ts +0 -218
- package/src/core/Module/index.ts +0 -15
- package/src/core/midi/index.ts +0 -5
- package/src/modules/BitCrusher.ts +0 -45
- package/src/modules/Delay.ts +0 -53
- package/src/modules/Distortion.ts +0 -45
- package/src/modules/Effect.ts +0 -46
- package/src/modules/Envelope/AmpEnvelope.ts +0 -23
- package/src/modules/Envelope/Base.ts +0 -176
- package/src/modules/Envelope/FreqEnvelope.ts +0 -64
- package/src/modules/Envelope/index.ts +0 -3
- package/src/modules/LFO.ts +0 -149
- package/src/modules/Reverb.ts +0 -53
- package/src/modules/Sequencer.ts +0 -178
- package/src/modules/VoiceScheduler.ts +0 -145
- package/src/modules/Volume.ts +0 -72
- package/src/routes.ts +0 -49
- package/src/types.ts +0 -3
- package/src/utils.ts +0 -18
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { now } from ".";
|
|
2
|
+
import Scheduler from "./Scheduler";
|
|
3
|
+
import Time, { t, TTime } from "./Time";
|
|
4
|
+
|
|
5
|
+
export enum TransportState {
|
|
6
|
+
playing = "playing",
|
|
7
|
+
stopped = "stopped",
|
|
8
|
+
paused = "paused",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type TransportEvents = {
|
|
12
|
+
start: { actionAt: TTime; offset: TTime };
|
|
13
|
+
stop: { actionAt: TTime };
|
|
14
|
+
pause: { actionAt: TTime };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type TPlaybackCallback = (actionAt: TTime) => void;
|
|
18
|
+
|
|
19
|
+
type TransportProps = {
|
|
20
|
+
onStart?: TPlaybackCallback;
|
|
21
|
+
onStop?: TPlaybackCallback;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default class Transport {
|
|
25
|
+
bpm = 120;
|
|
26
|
+
timeSignature: [number, number] = [4, 4];
|
|
27
|
+
loopStart: TTime;
|
|
28
|
+
loopEnd?: TTime;
|
|
29
|
+
|
|
30
|
+
state: TransportState = TransportState.stopped;
|
|
31
|
+
offset: TTime = 0;
|
|
32
|
+
|
|
33
|
+
private startTime: TTime = 0;
|
|
34
|
+
private onStart: TransportProps["onStart"];
|
|
35
|
+
private onStop: TransportProps["onStop"];
|
|
36
|
+
private scheduler: Scheduler;
|
|
37
|
+
|
|
38
|
+
constructor(props: TransportProps) {
|
|
39
|
+
this.onStart = props.onStart;
|
|
40
|
+
this.onStop = props.onStop;
|
|
41
|
+
this.loopStart = t("0:0:0");
|
|
42
|
+
this.scheduler = new Scheduler(this);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
start({
|
|
46
|
+
offset = this.offset,
|
|
47
|
+
actionAt = now(),
|
|
48
|
+
}: {
|
|
49
|
+
offset?: TTime;
|
|
50
|
+
actionAt?: TTime;
|
|
51
|
+
}) {
|
|
52
|
+
if (this.state === TransportState.playing) return;
|
|
53
|
+
|
|
54
|
+
this.validateFutureTime(actionAt);
|
|
55
|
+
|
|
56
|
+
this.scheduler.start(actionAt, () => {
|
|
57
|
+
this.state = TransportState.playing;
|
|
58
|
+
this.offset = offset;
|
|
59
|
+
this.startTime = t(actionAt).subtrack(this.offset);
|
|
60
|
+
});
|
|
61
|
+
this.onStart?.(actionAt);
|
|
62
|
+
|
|
63
|
+
return actionAt;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
stop({ actionAt: actionAt = now() }: { actionAt?: TTime }) {
|
|
67
|
+
if (this.state === TransportState.stopped) return;
|
|
68
|
+
|
|
69
|
+
this.validateFutureTime(actionAt);
|
|
70
|
+
|
|
71
|
+
this.scheduler.stop(actionAt, () => {
|
|
72
|
+
this.state = TransportState.stopped;
|
|
73
|
+
this.offset = 0;
|
|
74
|
+
});
|
|
75
|
+
this.onStop?.(actionAt);
|
|
76
|
+
|
|
77
|
+
return actionAt;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pause({ actionAt: actionAt = now() }: { actionAt?: TTime }) {
|
|
81
|
+
if (this.state === TransportState.paused) return;
|
|
82
|
+
|
|
83
|
+
this.validateFutureTime(actionAt);
|
|
84
|
+
|
|
85
|
+
this.scheduler.stop(actionAt, () => {
|
|
86
|
+
this.state = TransportState.paused;
|
|
87
|
+
this.offset = t(actionAt).subtrack(this.startTime);
|
|
88
|
+
});
|
|
89
|
+
this.onStop?.(actionAt);
|
|
90
|
+
|
|
91
|
+
return actionAt;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get playhead(): Time {
|
|
95
|
+
if (this.state === TransportState.stopped) return t(0);
|
|
96
|
+
if (this.state === TransportState.paused) return t(this.offset);
|
|
97
|
+
|
|
98
|
+
return t(now()).subtrack(this.startTime);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private validateFutureTime(time: TTime) {
|
|
102
|
+
if (t(time).isBefore(now())) throw Error("Past time not allowed");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Engine } from "@/Engine";
|
|
2
|
+
|
|
3
|
+
export { default as Transport, TransportState } from "./Transport";
|
|
4
|
+
export { default as Time } from "./Time";
|
|
5
|
+
|
|
6
|
+
export type { TransportEvents } from "./Transport";
|
|
7
|
+
export type { TTime } from "./Time";
|
|
8
|
+
|
|
9
|
+
export function now() {
|
|
10
|
+
return Engine.current.context.currentTime;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function browserToContextTime(time: number): number {
|
|
14
|
+
const differenceBetweenClocks = performance.now() / 1000 - now();
|
|
15
|
+
return time / 1000 - differenceBetweenClocks;
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export { Module } from "./module";
|
|
2
|
+
export type { IModule, IModuleSerialize, IPolyModuleSerialize } from "./module";
|
|
3
|
+
|
|
4
|
+
export type IAnyAudioContext = AudioContext | OfflineAudioContext;
|
|
5
|
+
|
|
6
|
+
export { Routes } from "./Route";
|
|
7
|
+
export type { IRoute } from "./Route";
|
|
8
|
+
|
|
9
|
+
export { default as MidiDeviceManager } from "./midi/MidiDeviceManager";
|
|
10
|
+
export { default as MidiDevice, MidiPortState } from "./midi/MidiDevice";
|
|
11
|
+
export type { IMidiDevice } from "./midi/MidiDevice";
|
|
12
|
+
export { default as MidiEvent, MidiEventType } from "./midi/MidiEvent";
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
MidiOutput,
|
|
16
|
+
MidiInput,
|
|
17
|
+
AudioInput,
|
|
18
|
+
AudioOutput,
|
|
19
|
+
IIOSerialize,
|
|
20
|
+
} from "./IO";
|
|
21
|
+
|
|
22
|
+
export { Time, TransportState, Transport } from "./Timing";
|
|
23
|
+
export type { TTime } from "./Timing";
|
|
24
|
+
|
|
25
|
+
export type {
|
|
26
|
+
PropDefinition,
|
|
27
|
+
PropSchema,
|
|
28
|
+
NumberProp,
|
|
29
|
+
StringProp,
|
|
30
|
+
EnumProp,
|
|
31
|
+
BooleanProp,
|
|
32
|
+
ArrayProp,
|
|
33
|
+
} from "./schema";
|
|
34
|
+
|
|
35
|
+
export { default as Note } from "./Note";
|
|
36
|
+
export type { INote } from "./Note";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import Note from "../Note";
|
|
2
|
-
import { IMidiInput,
|
|
2
|
+
import { EventListerCallback, IMidiInput, MidiPortState } from "./MidiDevice";
|
|
3
3
|
import MidiEvent from "./MidiEvent";
|
|
4
4
|
|
|
5
|
-
const MAP_KEYS:
|
|
5
|
+
const MAP_KEYS: Record<string, Note> = {
|
|
6
6
|
a: new Note("C3"),
|
|
7
7
|
s: new Note("D3"),
|
|
8
8
|
d: new Note("E3"),
|
|
@@ -21,32 +21,52 @@ const MAP_KEYS: { [key: string]: Note } = {
|
|
|
21
21
|
p: new Note("D#4"),
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const computerKeyboardData = () => ({
|
|
25
25
|
id: "computer_keyboard",
|
|
26
26
|
name: "Computer Keyboard",
|
|
27
|
-
state:
|
|
28
|
-
};
|
|
27
|
+
state: MidiPortState.connected,
|
|
28
|
+
});
|
|
29
29
|
|
|
30
30
|
export default class ComputerKeyboardInput implements IMidiInput {
|
|
31
31
|
id: string;
|
|
32
32
|
name: string;
|
|
33
|
-
state:
|
|
34
|
-
|
|
33
|
+
state: MidiPortState;
|
|
34
|
+
eventListerCallbacks: EventListerCallback[] = [];
|
|
35
35
|
|
|
36
36
|
constructor() {
|
|
37
|
-
|
|
37
|
+
const { id, name, state } = computerKeyboardData();
|
|
38
|
+
this.id = id;
|
|
39
|
+
this.name = name;
|
|
40
|
+
this.state = state;
|
|
41
|
+
|
|
38
42
|
document.addEventListener("keydown", this.onKeyTrigger(true));
|
|
39
43
|
document.addEventListener("keyup", this.onKeyTrigger(false));
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
addEventListener(callback: EventListerCallback) {
|
|
47
|
+
this.eventListerCallbacks.push(callback);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
removeEventListener(callback: EventListerCallback) {
|
|
51
|
+
this.eventListerCallbacks = this.eventListerCallbacks.filter(
|
|
52
|
+
(c) => c !== callback,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
serialize() {
|
|
57
|
+
const { id, name, state } = this;
|
|
44
58
|
|
|
59
|
+
return { id, name, state };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onKeyTrigger = (noteOn: boolean) => (event: KeyboardEvent) => {
|
|
45
63
|
const note = this.extractNote(event);
|
|
46
64
|
if (!note) return;
|
|
47
65
|
|
|
48
66
|
const midiEvent = MidiEvent.fromNote(note, noteOn);
|
|
49
|
-
this.
|
|
67
|
+
this.eventListerCallbacks.forEach((callback) => {
|
|
68
|
+
callback(midiEvent);
|
|
69
|
+
});
|
|
50
70
|
};
|
|
51
71
|
|
|
52
72
|
private extractNote(event: KeyboardEvent): Note | undefined {
|
|
@@ -1,49 +1,51 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { MessageEvent, Input } from "webmidi";
|
|
2
|
+
import { browserToContextTime } from "../Timing";
|
|
3
|
+
import MidiEvent, { MidiEventType } from "./MidiEvent";
|
|
2
4
|
|
|
3
|
-
export
|
|
5
|
+
export enum MidiPortState {
|
|
6
|
+
connected = "connected",
|
|
7
|
+
disconnected = "disconnected",
|
|
8
|
+
}
|
|
4
9
|
|
|
5
|
-
export
|
|
10
|
+
export type IMidiDevice = {
|
|
6
11
|
id: string;
|
|
7
12
|
name: string;
|
|
8
|
-
state:
|
|
9
|
-
}
|
|
13
|
+
state: MidiPortState;
|
|
14
|
+
};
|
|
10
15
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
}
|
|
16
|
+
export type IMidiInput = IMidiDevice & {
|
|
17
|
+
eventListerCallbacks: EventListerCallback[];
|
|
18
|
+
};
|
|
14
19
|
|
|
15
20
|
export type EventListerCallback = (event: MidiEvent) => void;
|
|
16
21
|
|
|
17
|
-
export default class MidiDevice implements
|
|
22
|
+
export default class MidiDevice implements IMidiDevice {
|
|
18
23
|
id: string;
|
|
19
24
|
name: string;
|
|
20
|
-
state: TMidiPortState;
|
|
21
25
|
eventListerCallbacks: EventListerCallback[] = [];
|
|
22
26
|
|
|
23
|
-
private
|
|
27
|
+
private input: Input;
|
|
24
28
|
|
|
25
|
-
constructor(
|
|
26
|
-
this.id =
|
|
27
|
-
this.name =
|
|
28
|
-
this.
|
|
29
|
-
this._midi = midi;
|
|
29
|
+
constructor(input: Input) {
|
|
30
|
+
this.id = input.id;
|
|
31
|
+
this.name = input.name || `Device ${input.id}`;
|
|
32
|
+
this.input = input;
|
|
30
33
|
|
|
31
34
|
this.connect();
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
e instanceof MIDIMessageEvent || e instanceof MidiEvent;
|
|
38
|
-
|
|
39
|
-
if (!isMidiEvent) return;
|
|
37
|
+
get state() {
|
|
38
|
+
return this.input.state as MidiPortState;
|
|
39
|
+
}
|
|
40
40
|
|
|
41
|
+
connect() {
|
|
42
|
+
this.input.addListener("midimessage", (e: MessageEvent) => {
|
|
41
43
|
this.processEvent(e);
|
|
42
|
-
};
|
|
44
|
+
});
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
disconnect() {
|
|
46
|
-
this.
|
|
48
|
+
this.input.removeListener();
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
serialize() {
|
|
@@ -58,17 +60,22 @@ export default class MidiDevice implements MidiDeviceInterface {
|
|
|
58
60
|
|
|
59
61
|
removeEventListener(callback: EventListerCallback) {
|
|
60
62
|
this.eventListerCallbacks = this.eventListerCallbacks.filter(
|
|
61
|
-
(c) => c !== callback
|
|
63
|
+
(c) => c !== callback,
|
|
62
64
|
);
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
private processEvent(
|
|
66
|
-
const
|
|
67
|
+
private processEvent(event: MessageEvent) {
|
|
68
|
+
const midiEvent = new MidiEvent(
|
|
69
|
+
event.message,
|
|
70
|
+
browserToContextTime(event.timestamp),
|
|
71
|
+
);
|
|
67
72
|
|
|
68
|
-
switch (
|
|
69
|
-
case
|
|
70
|
-
case
|
|
71
|
-
this.eventListerCallbacks.forEach((callback) =>
|
|
73
|
+
switch (midiEvent.type) {
|
|
74
|
+
case MidiEventType.noteOn:
|
|
75
|
+
case MidiEventType.noteOff:
|
|
76
|
+
this.eventListerCallbacks.forEach((callback) => {
|
|
77
|
+
callback(midiEvent);
|
|
78
|
+
});
|
|
72
79
|
}
|
|
73
80
|
}
|
|
74
81
|
}
|
|
@@ -1,80 +1,79 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { Input, Output, WebMidi } from "webmidi";
|
|
2
|
+
import ComputerKeyboardDevice from "./ComputerKeyboardDevice";
|
|
3
|
+
import MidiDevice from "./MidiDevice";
|
|
4
|
+
|
|
5
|
+
type ListenerCallback = (device: MidiDevice) => void;
|
|
3
6
|
|
|
4
7
|
export default class MidiDeviceManager {
|
|
5
|
-
devices
|
|
8
|
+
devices = new Map<string, MidiDevice | ComputerKeyboardDevice>();
|
|
6
9
|
private initialized = false;
|
|
7
|
-
private
|
|
10
|
+
private listeners: ListenerCallback[] = [];
|
|
8
11
|
|
|
9
12
|
constructor() {
|
|
10
|
-
|
|
11
|
-
this.
|
|
12
|
-
.then(() => {
|
|
13
|
-
this.listenChanges();
|
|
14
|
-
this.initialized = true;
|
|
15
|
-
})
|
|
16
|
-
.catch(() => {});
|
|
13
|
+
const computerKeyboardDevice = new ComputerKeyboardDevice();
|
|
14
|
+
this.devices.set(computerKeyboardDevice.id, computerKeyboardDevice);
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!device) return null;
|
|
17
|
+
async initialize() {
|
|
18
|
+
await this.initializeDevices();
|
|
23
19
|
|
|
24
|
-
|
|
20
|
+
this.listenChanges();
|
|
21
|
+
this.initialized = true;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.then((access: MIDIAccess) => {
|
|
31
|
-
access.onstatechange = (e) => {
|
|
32
|
-
const isMidiEvent = e instanceof MIDIConnectionEvent;
|
|
33
|
-
|
|
34
|
-
if (!isMidiEvent) return;
|
|
35
|
-
if (e.port instanceof MIDIOutput) return;
|
|
36
|
-
|
|
37
|
-
const input = e.port as unknown as IMidiInput;
|
|
24
|
+
find(id: string): MidiDevice | ComputerKeyboardDevice | undefined {
|
|
25
|
+
return this.devices.get(id);
|
|
26
|
+
}
|
|
38
27
|
|
|
39
|
-
|
|
28
|
+
addListener(callback: ListenerCallback) {
|
|
29
|
+
this.listeners.push(callback);
|
|
30
|
+
}
|
|
40
31
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
private async initializeDevices() {
|
|
33
|
+
if (this.initialized) return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await WebMidi.enable();
|
|
37
|
+
|
|
38
|
+
WebMidi.inputs.forEach((input) => {
|
|
39
|
+
if (!this.devices.has(input.id)) {
|
|
40
|
+
this.devices.set(input.id, new MidiDevice(input));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error("Error enabling WebMidi:", err);
|
|
45
|
+
}
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
private listenChanges() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
delete this.devices[device.id];
|
|
52
|
-
} else {
|
|
53
|
-
this.devices[device.id] = device;
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
49
|
+
WebMidi.addListener("connected", (event) => {
|
|
50
|
+
const port = event.port as Input | Output;
|
|
51
|
+
if (port instanceof Output) return;
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
if (this.initialized) return Object.values(this.devices);
|
|
53
|
+
if (this.devices.has(port.id)) return;
|
|
60
54
|
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
const device = new MidiDevice(port);
|
|
56
|
+
this.devices.set(device.id, device);
|
|
63
57
|
|
|
64
|
-
this.
|
|
58
|
+
this.listeners.forEach((listener) => {
|
|
59
|
+
listener(device);
|
|
60
|
+
});
|
|
65
61
|
});
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
WebMidi.addListener("disconnected", (event) => {
|
|
64
|
+
const port = event.port as Input | Output;
|
|
65
|
+
if (port instanceof Output) return;
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
];
|
|
67
|
+
const device = this.devices.get(port.id);
|
|
68
|
+
if (!device) return;
|
|
69
|
+
if (device instanceof ComputerKeyboardDevice) return;
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
device.disconnect();
|
|
72
|
+
this.devices.delete(device.id);
|
|
77
73
|
|
|
78
|
-
|
|
74
|
+
this.listeners.forEach((listener) => {
|
|
75
|
+
listener(device);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
79
78
|
}
|
|
80
79
|
}
|
|
@@ -1,91 +1,67 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ISequence } from "../../modules";
|
|
1
|
+
import { Message } from "webmidi";
|
|
3
2
|
import Note, { INote } from "../Note";
|
|
3
|
+
import { t, TTime } from "../Timing/Time";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type MidiEventType = "noteOn" | "noteOff" | "cc";
|
|
5
|
+
export enum MidiEventType {
|
|
6
|
+
noteOn = "noteon",
|
|
7
|
+
noteOff = "noteoff",
|
|
8
|
+
cc = "cc",
|
|
9
|
+
}
|
|
12
10
|
|
|
13
11
|
export default class MidiEvent {
|
|
14
|
-
note
|
|
12
|
+
note?: Note;
|
|
15
13
|
voiceNo?: number;
|
|
16
|
-
readonly triggeredAt:
|
|
17
|
-
|
|
18
|
-
private data: Uint8Array;
|
|
19
|
-
private event: MIDIMessageEvent;
|
|
20
|
-
|
|
21
|
-
static fromSequence(sequence: ISequence, triggeredAt: number): MidiEvent[] {
|
|
22
|
-
return sequence.notes.map((noteName) =>
|
|
23
|
-
this.fromNote(noteName, true, triggeredAt)
|
|
24
|
-
);
|
|
25
|
-
}
|
|
14
|
+
readonly triggeredAt: TTime;
|
|
15
|
+
private message: Message;
|
|
26
16
|
|
|
27
17
|
static fromNote(
|
|
28
|
-
noteName: string | Note | INote,
|
|
29
|
-
noteOn
|
|
30
|
-
triggeredAt?:
|
|
18
|
+
noteName: string | Note | Omit<INote, "frequency">,
|
|
19
|
+
noteOn = true,
|
|
20
|
+
triggeredAt?: TTime,
|
|
31
21
|
): MidiEvent {
|
|
32
22
|
const note = noteName instanceof Note ? noteName : new Note(noteName);
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
new MIDIMessageEvent("", { data: note.midiData(noteOn) }),
|
|
36
|
-
triggeredAt
|
|
37
|
-
);
|
|
38
|
-
event.note = note;
|
|
39
|
-
|
|
40
|
-
return event;
|
|
24
|
+
return new MidiEvent(new Message(note.midiData(noteOn)), triggeredAt);
|
|
41
25
|
}
|
|
42
26
|
|
|
43
|
-
static fromCC(cc: number, value: number, triggeredAt?:
|
|
27
|
+
static fromCC(cc: number, value: number, triggeredAt?: TTime): MidiEvent {
|
|
44
28
|
return new MidiEvent(
|
|
45
|
-
new
|
|
46
|
-
triggeredAt
|
|
29
|
+
new Message(new Uint8Array([0xb0, cc, value])),
|
|
30
|
+
triggeredAt,
|
|
47
31
|
);
|
|
48
32
|
}
|
|
49
33
|
|
|
50
|
-
constructor(
|
|
51
|
-
this.
|
|
52
|
-
this.triggeredAt = triggeredAt
|
|
53
|
-
this.data = event.data;
|
|
34
|
+
constructor(message: Message, triggeredAt?: TTime) {
|
|
35
|
+
this.message = message;
|
|
36
|
+
this.triggeredAt = triggeredAt ?? t();
|
|
54
37
|
this.defineNotes();
|
|
55
38
|
}
|
|
56
39
|
|
|
57
|
-
get statusByte(): number {
|
|
58
|
-
return this.data[0];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
get firstData(): number {
|
|
62
|
-
return this.data[1];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
get secondData(): number {
|
|
66
|
-
return this.data[2];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
40
|
get type() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let type = EventType[this.statusByte & 0xf0];
|
|
73
|
-
|
|
74
|
-
if (type === "noteOn" && this.secondData === 0) {
|
|
75
|
-
type = "noteOff";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return (this._type = type);
|
|
41
|
+
return this.message.type as MidiEventType;
|
|
79
42
|
}
|
|
80
43
|
|
|
81
44
|
get isNote() {
|
|
82
|
-
return
|
|
45
|
+
return (
|
|
46
|
+
this.type === MidiEventType.noteOn || this.type === MidiEventType.noteOff
|
|
47
|
+
);
|
|
83
48
|
}
|
|
84
49
|
|
|
85
50
|
defineNotes() {
|
|
86
51
|
if (!this.isNote) return;
|
|
87
52
|
if (this.note) return;
|
|
88
53
|
|
|
89
|
-
this.note =
|
|
54
|
+
this.note = Note.fromEvent(this.message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get rawMessage() {
|
|
58
|
+
return this.message;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clone(voiceNo?: number) {
|
|
62
|
+
const newEvent = new MidiEvent(this.message, this.triggeredAt);
|
|
63
|
+
newEvent.voiceNo = voiceNo;
|
|
64
|
+
|
|
65
|
+
return newEvent;
|
|
90
66
|
}
|
|
91
67
|
}
|