-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmidi.js
More file actions
169 lines (140 loc) · 4.49 KB
/
midi.js
File metadata and controls
169 lines (140 loc) · 4.49 KB
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
const KeyMute0 = 0x10
const KeySolo0 = 0x08
const KeyRecord0 = 0x00
const KeySelect0 = 0x18
const KeyPlay = 0x5e
const KeyPause = 0x5d
const KeyRecord = 0x5f
const KeyFastBackwards = 0x5b
const KeyFastForward = 0x5c
const KeyLeftLeft = 0x2e
const KeyRightRight = 0x2f
const KeyUp = 0x60
const KeyDown = 0x61
const KeyLeft = 0x62
const KeyRight = 0x63
export class MIDIManager {
static #midiAccess = null
static #inputs = new Map()
static #outputs = new Map()
static #sliderState = new Map()
static #encoderState = new Map()
static #sequencerTimer = null
static #sequencerStep = 0
static async initialize() {
if (!navigator.requestMIDIAccess) {
console.error('WebMIDI is not supported in your browser')
return false
}
try {
MIDIManager.#midiAccess = await navigator.requestMIDIAccess({ sysex: false })
MIDIManager.#setupEventListeners()
MIDIManager.#updateDeviceLists()
console.info('MIDI initialized successfully')
return true
} catch (error) {
console.error('MIDI initialization failed:', error)
return false
}
}
static getSliderValue(slider) {
return MIDIManager.#sliderState.get(slider) || 0
}
static setSliderValue(slider, value) {
MIDIManager.#sliderState.set(slider, value)
}
static getEncoderValue(encoder) {
return MIDIManager.#encoderState.get(encoder) || 0
}
static setEncoderValue(encoder, value) {
return MIDIManager.#encoderState.set(encoder, value)
}
static #sendMIDIMessage(message) {
for (const output of MIDIManager.#outputs.values()) {
output.send(message)
}
}
static setLED(note, state) {
const channel = 1
const status = 0x90 | ((channel - 1) & 0x0F) // Note On, channel masked
const velocity = state ? 127 : 0
MIDIManager.#sendMIDIMessage(new Uint8Array([status, note, velocity]))
}
// =====================
// Private Methods
// =====================
static #setupEventListeners() {
if (!MIDIManager.#midiAccess) {
return
}
MIDIManager.#midiAccess.onstatechange = event => {
console.info(`Device ${event.port.name} ${event.port.state}`)
MIDIManager.#updateDeviceLists()
if (event.port.state === 'connected' && event.port.type === 'input') {
event.port.onmidimessage = msg => MIDIManager.#handleMessage(msg)
}
}
}
static #updateDeviceLists() {
MIDIManager.#inputs.clear()
MIDIManager.#outputs.clear()
if (MIDIManager.#midiAccess) {
MIDIManager.#midiAccess.inputs.forEach(input => {
MIDIManager.#inputs.set(input.id, input)
input.onmidimessage = msg => MIDIManager.#handleMessage(msg)
})
MIDIManager.#midiAccess.outputs.forEach(output => {
MIDIManager.#outputs.set(output.id, output)
})
}
}
static #handleMessage(message) {
const [command, data1, data2] = message.data
const commandType = command & 0xf0
const channel = (command & 0x0f) + 1
switch (commandType) {
case 0xB0: { // Control Change (Encoders)
const encoderId = data1 - 16
let value = MIDIManager.#encoderState.get(encoderId) || 0
value += data2 > 64 ? -data2 + 64 : data2
MIDIManager.#encoderState.set(encoderId, value)
break
}
case 0xE0: { // Pitch Bend (Sliders)
const value = ((data2 << 7) | data1) / 16256
console.debug(`Slider ${channel - 1}: ${value}`)
MIDIManager.#sliderState.set(channel - 1, value)
break
}
case 0x90: { // Note (Buttons)
if (data2 === 0) {
console.debug('MIDI key up', data1)
} else if (data2 === 127) {
console.debug('MIDI key down', data1)
}
break
}
default:
console.debug('ignoring midi command:', message.timeStamp, commandType, data1, data2, channel)
}
}
static startSequencer(bpm = 120) {
MIDIManager.stopSequencer()
const interval = (60 / bpm) * 1000 // ms per beat
MIDIManager.#sequencerTimer = setInterval(() => {
MIDIManager.setLED(0x18 + (MIDIManager.#sequencerStep + 7) % 8, false)
MIDIManager.setLED(0x18 + MIDIManager.#sequencerStep, true)
MIDIManager.#sequencerStep = (MIDIManager.#sequencerStep + 1) % 8
}, interval)
}
static stopSequencer() {
if (MIDIManager.#sequencerTimer) {
clearInterval(MIDIManager.#sequencerTimer)
MIDIManager.#sequencerTimer = null
}
// Optionally turn off all Select LEDs
for (let i = 0; i < 8; i++) {
MIDIManager.setLED(0x18 + i, false)
}
}
}