forked from johnnesky/beepbox
-
Notifications
You must be signed in to change notification settings - Fork 0
/
MidiInput.ts
114 lines (93 loc) · 3.24 KB
/
MidiInput.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// Copyright (c) 2012-2022 John Nesky and contributing authors, distributed under the MIT license, see accompanying the LICENSE.md file.
import {Config} from "../synth/SynthConfig";
import {SongDocument} from "./SongDocument";
import {AnalogousDrum, analogousDrumMap, MidiEventType} from "./Midi";
declare global {
interface Navigator {
requestMIDIAccess?(): Promise<any>;
}
}
interface MIDIInput extends EventTarget {
id: string;
type: "input" | "output";
state: "disconnected" | "connected";
}
interface MIDIConnectionEvent {
port: MIDIInput;
}
interface MIDIMessageEvent {
data: [type: number, key: number, velocity: number];
target: MIDIInput;
}
// A unique id for this tab.
const id: string = ((Math.random() * 0xffffffff) >>> 0).toString(16);
export class MidiInputHandler {
constructor(private _doc: SongDocument) {
this.registerMidiAccessHandler();
}
private async registerMidiAccessHandler() {
if (navigator.requestMIDIAccess == null) return;
try {
const midiAccess = await navigator.requestMIDIAccess();
midiAccess.inputs.forEach(this._registerMidiInput);
midiAccess.addEventListener("statechange", this._handleStateChange);
this._takeMidiHandlerFocus();
window.addEventListener("focus", this._takeMidiHandlerFocus);
} catch (e) {
console.error("Failed to get MIDI access", e);
}
}
private _takeMidiHandlerFocus = (event?: Event) => {
// Record that this browser tab is the one that should handle midi
// events and any other open tabs should ignore midi events for now.
localStorage.setItem("midiHandlerId", id);
}
private _handleStateChange = (event: MIDIConnectionEvent) => {
if (event.port.type !== "input") return;
switch (event.port.state) {
case "connected":
this._registerMidiInput(event.port);
break;
case "disconnected":
this._unregisterMidiInput(event.port);
break;
}
}
private _registerMidiInput = (midiInput: MIDIInput) => {
midiInput.addEventListener("midimessage", this._onMidiMessage as any);
}
private _unregisterMidiInput = (midiInput: MIDIInput) => {
midiInput.removeEventListener("midimessage", this._onMidiMessage as any);
this._doc.performance.clearAllPitches();
}
private _onMidiMessage = (event: MIDIMessageEvent) => {
// Ignore midi events if disabled or a different tab is handling them.
if (!this._doc.prefs.enableMidi || localStorage.getItem("midiHandlerId") != id) return;
const isDrum: boolean = this._doc.song.getChannelIsNoise(this._doc.channel);
let [eventType, key, velocity] = event.data;
eventType &= 0xF0;
if (isDrum) {
const drum: AnalogousDrum | undefined = analogousDrumMap[key];
if (drum != undefined) {
key = drum.frequency;
} else {
return;
}
} else {
key -= Config.keys[this._doc.song.key].basePitch; // The basePitch of the song key is implicit so don't include it.
if (key < 0 || key > Config.maxPitch) return;
}
if (eventType == MidiEventType.noteOn && velocity == 0) {
eventType = MidiEventType.noteOff;
}
switch (eventType) {
case MidiEventType.noteOn:
this._doc.synth.preferLowerLatency = true;
this._doc.performance.addPerformedPitch(key);
break;
case MidiEventType.noteOff:
this._doc.performance.removePerformedPitch(key);
break;
}
}
}