midiwire 0.1.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/LICENSE +21 -0
- package/README.md +845 -0
- package/dist/midiwire.es.js +1987 -0
- package/dist/midiwire.umd.js +1 -0
- package/package.json +58 -0
- package/src/bindings/DataAttributeBinder.js +198 -0
- package/src/bindings/DataAttributeBinder.test.js +825 -0
- package/src/core/EventEmitter.js +93 -0
- package/src/core/EventEmitter.test.js +357 -0
- package/src/core/MIDIConnection.js +364 -0
- package/src/core/MIDIConnection.test.js +783 -0
- package/src/core/MIDIController.js +756 -0
- package/src/core/MIDIController.test.js +1958 -0
- package/src/core/MIDIDeviceManager.js +204 -0
- package/src/core/MIDIDeviceManager.test.js +638 -0
- package/src/core/errors.js +99 -0
- package/src/index.js +181 -0
- package/src/utils/dx7.js +1294 -0
- package/src/utils/dx7.test.js +1208 -0
- package/src/utils/midi.js +244 -0
- package/src/utils/midi.test.js +260 -0
- package/src/utils/sysex.js +98 -0
- package/src/utils/sysex.test.js +222 -0
- package/src/utils/validators.js +88 -0
- package/src/utils/validators.test.js +300 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { CONNECTION_EVENTS } from "./MIDIConnection.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* High-level MIDI device manager for web UIs. Simplifies device
|
|
5
|
+
* management with helpers for:
|
|
6
|
+
* - Populating device select dropdowns
|
|
7
|
+
* - Handling device connections/disconnections
|
|
8
|
+
* - Tracking connection status
|
|
9
|
+
* - Updating UI on device changes
|
|
10
|
+
*
|
|
11
|
+
* NOTE: Typically used with createMIDIDeviceManager(). For direct
|
|
12
|
+
* MIDI I/O, use MIDIController instead.
|
|
13
|
+
*/
|
|
14
|
+
export class MIDIDeviceManager {
|
|
15
|
+
/**
|
|
16
|
+
* @param {Object} options
|
|
17
|
+
* @param {MIDIController} options.midiController - The MIDIController instance
|
|
18
|
+
* @param {Function} options.onStatusUpdate - Callback for status updates (message, state)
|
|
19
|
+
* @param {Function} options.onConnectionUpdate - Callback when connection status changes
|
|
20
|
+
* @param {number} [options.channel=1] - Default MIDI channel
|
|
21
|
+
*/
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.midi = options.midiController || null
|
|
24
|
+
this.onStatusUpdate = options.onStatusUpdate || (() => {})
|
|
25
|
+
this.onConnectionUpdate = options.onConnectionUpdate || (() => {})
|
|
26
|
+
this.channel = options.channel || 1
|
|
27
|
+
this.currentDevice = null
|
|
28
|
+
this.isConnecting = false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the device manager with a MIDIController
|
|
33
|
+
* @param {MIDIController} midi
|
|
34
|
+
*/
|
|
35
|
+
setMIDI(midi) {
|
|
36
|
+
this.midi = midi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set up device change event listeners
|
|
41
|
+
* @param {Function} [onDeviceListChange] - Optional callback when device list should be refreshed
|
|
42
|
+
*/
|
|
43
|
+
setupDeviceListeners(onDeviceListChange) {
|
|
44
|
+
if (!this.midi?.connection) return
|
|
45
|
+
|
|
46
|
+
this.midi.connection.on(CONNECTION_EVENTS.OUTPUT_DEVICE_CONNECTED, ({ device }) => {
|
|
47
|
+
this.updateStatus(`Device connected: ${device.name}`, "connected")
|
|
48
|
+
if (onDeviceListChange) {
|
|
49
|
+
onDeviceListChange()
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
this.midi.connection.on(CONNECTION_EVENTS.OUTPUT_DEVICE_DISCONNECTED, ({ device }) => {
|
|
54
|
+
this.updateStatus(`Device disconnected: ${device.name}`, "error")
|
|
55
|
+
|
|
56
|
+
const wasCurrentDevice = this.currentDevice && device.name === this.currentDevice.name
|
|
57
|
+
|
|
58
|
+
if (wasCurrentDevice) {
|
|
59
|
+
this.currentDevice = null
|
|
60
|
+
this.updateConnectionStatus()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (onDeviceListChange) {
|
|
64
|
+
onDeviceListChange()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Update status message
|
|
71
|
+
* @param {string} message
|
|
72
|
+
* @param {string} state
|
|
73
|
+
*/
|
|
74
|
+
updateStatus(message, state = "") {
|
|
75
|
+
this.onStatusUpdate(message, state)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Update connection status
|
|
80
|
+
*/
|
|
81
|
+
updateConnectionStatus() {
|
|
82
|
+
this.onConnectionUpdate(this.currentDevice, this.midi)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the current list of MIDI output devices
|
|
87
|
+
* @returns {Array<Object>} Array of device objects with id, name, manufacturer
|
|
88
|
+
*/
|
|
89
|
+
getOutputDevices() {
|
|
90
|
+
if (!this.midi?.connection) return []
|
|
91
|
+
return this.midi.connection.getOutputs()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if a device is still connected
|
|
96
|
+
* @param {string} deviceName
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
isDeviceConnected(deviceName) {
|
|
100
|
+
if (!this.midi?.connection) return false
|
|
101
|
+
const outputs = this.midi.connection.getOutputs()
|
|
102
|
+
return outputs.some((o) => o.name === deviceName)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Connect device selection events to the device manager
|
|
107
|
+
* @param {HTMLSelectElement} deviceSelectElement
|
|
108
|
+
* @param {Function} onConnect - Callback when device is connected (midi, device)
|
|
109
|
+
*/
|
|
110
|
+
connectDeviceSelection(deviceSelectElement, onConnect) {
|
|
111
|
+
if (!deviceSelectElement || !this.midi) return
|
|
112
|
+
|
|
113
|
+
deviceSelectElement.addEventListener("change", async (e) => {
|
|
114
|
+
const deviceIndex = e.target.value
|
|
115
|
+
|
|
116
|
+
if (!deviceIndex) {
|
|
117
|
+
if (this.currentDevice && this.midi) {
|
|
118
|
+
this.midi.connection.disconnect()
|
|
119
|
+
this.currentDevice = null
|
|
120
|
+
this.updateStatus("Disconnected")
|
|
121
|
+
this.updateConnectionStatus()
|
|
122
|
+
}
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (this.isConnecting) return
|
|
127
|
+
this.isConnecting = true
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await this.midi.setOutput(parseInt(deviceIndex, 10))
|
|
131
|
+
this.currentDevice = this.midi.getCurrentOutput()
|
|
132
|
+
this.updateConnectionStatus()
|
|
133
|
+
|
|
134
|
+
if (onConnect) {
|
|
135
|
+
await onConnect(this.midi, this.currentDevice)
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
this.updateStatus(`Connection failed: ${err.message}`, "error")
|
|
139
|
+
} finally {
|
|
140
|
+
this.isConnecting = false
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Connect channel selection events
|
|
147
|
+
* @param {HTMLSelectElement} channelSelectElement
|
|
148
|
+
*/
|
|
149
|
+
connectChannelSelection(channelSelectElement) {
|
|
150
|
+
if (!channelSelectElement || !this.midi) return
|
|
151
|
+
|
|
152
|
+
channelSelectElement.addEventListener("change", (e) => {
|
|
153
|
+
if (this.midi) {
|
|
154
|
+
this.midi.options.channel = parseInt(e.target.value, 10)
|
|
155
|
+
this.updateConnectionStatus()
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Populate a device select element with available MIDI output devices
|
|
162
|
+
* @param {HTMLSelectElement} selectElement
|
|
163
|
+
* @param {Function} [onChange] - Optional callback when selection should change
|
|
164
|
+
*/
|
|
165
|
+
populateDeviceList(selectElement, onChange) {
|
|
166
|
+
if (!selectElement) return
|
|
167
|
+
|
|
168
|
+
const outputs = this.getOutputDevices()
|
|
169
|
+
|
|
170
|
+
if (outputs.length > 0) {
|
|
171
|
+
selectElement.innerHTML =
|
|
172
|
+
'<option value="">Select a device</option>' +
|
|
173
|
+
outputs.map((output, i) => `<option value="${i}">${output.name}</option>`).join("")
|
|
174
|
+
|
|
175
|
+
// Check if the currently connected device is still available
|
|
176
|
+
if (this.currentDevice) {
|
|
177
|
+
const deviceIndex = outputs.findIndex((o) => o.name === this.currentDevice.name)
|
|
178
|
+
if (deviceIndex !== -1) {
|
|
179
|
+
// Device is still connected, keep it selected
|
|
180
|
+
selectElement.value = deviceIndex.toString()
|
|
181
|
+
} else {
|
|
182
|
+
// Current device was disconnected
|
|
183
|
+
selectElement.value = ""
|
|
184
|
+
this.currentDevice = null
|
|
185
|
+
this.updateConnectionStatus()
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
// No device connected, show "Select a device"
|
|
189
|
+
selectElement.value = ""
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!this.currentDevice) {
|
|
193
|
+
this.updateStatus("Select a MIDI device")
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
selectElement.innerHTML = '<option value="">No MIDI devices found</option>'
|
|
197
|
+
this.updateStatus("No MIDI devices available", "error")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (onChange) {
|
|
201
|
+
onChange()
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|