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.
@@ -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
+ }