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,1987 @@
1
+ class B {
2
+ /**
3
+ * @param {import("../core/MIDIController.js").MIDIController} controller
4
+ * @param {string} selector - CSS selector for elements to bind
5
+ */
6
+ constructor(E, s = "[data-midi-cc]") {
7
+ this.controller = E, this.selector = s, this.observer = null;
8
+ }
9
+ /**
10
+ * Bind all matching elements in the document
11
+ */
12
+ bindAll() {
13
+ document.querySelectorAll(
14
+ this.selector === "[data-midi-cc]" ? "[data-midi-cc], [data-midi-msb][data-midi-lsb]" : this.selector
15
+ ).forEach((s) => {
16
+ if (s.hasAttribute("data-midi-bound")) return;
17
+ const n = this._parseAttributes(s);
18
+ n && (this.controller.bind(s, n), s.setAttribute("data-midi-bound", "true"));
19
+ });
20
+ }
21
+ /**
22
+ * Watch for dynamically added elements and auto-bind them
23
+ */
24
+ enableAutoBinding() {
25
+ if (this.observer) return;
26
+ const E = this.selector === "[data-midi-cc]" ? "[data-midi-cc], [data-midi-msb][data-midi-lsb]" : this.selector;
27
+ this.observer = new MutationObserver((s) => {
28
+ s.forEach((n) => {
29
+ n.addedNodes.forEach((_) => {
30
+ if (_.nodeType === Node.ELEMENT_NODE) {
31
+ if (_.matches?.(E)) {
32
+ const A = this._parseAttributes(_);
33
+ A && !_.hasAttribute("data-midi-bound") && (this.controller.bind(_, A), _.setAttribute("data-midi-bound", "true"));
34
+ }
35
+ _.querySelectorAll && _.querySelectorAll(E).forEach((e) => {
36
+ if (!e.hasAttribute("data-midi-bound")) {
37
+ const r = this._parseAttributes(e);
38
+ r && (this.controller.bind(e, r), e.setAttribute("data-midi-bound", "true"));
39
+ }
40
+ });
41
+ }
42
+ }), n.removedNodes.forEach((_) => {
43
+ _.nodeType === Node.ELEMENT_NODE && (_.hasAttribute?.("data-midi-bound") && this.controller.unbind(_), _.querySelectorAll && _.querySelectorAll("[data-midi-bound]").forEach((e) => {
44
+ this.controller.unbind(e);
45
+ }));
46
+ });
47
+ });
48
+ }), this.observer.observe(document.body, {
49
+ childList: !0,
50
+ subtree: !0
51
+ });
52
+ }
53
+ /**
54
+ * Stop watching for new elements
55
+ */
56
+ disableAutoBinding() {
57
+ this.observer && (this.observer.disconnect(), this.observer = null);
58
+ }
59
+ /**
60
+ * Parse MIDI config from data attributes
61
+ * @param {HTMLElement} element
62
+ * @returns {Object|null}
63
+ * @private
64
+ */
65
+ _parseAttributes(E) {
66
+ const s = parseInt(E.dataset.midiMsb, 10), n = parseInt(E.dataset.midiLsb, 10);
67
+ if (!Number.isNaN(s) && !Number.isNaN(n) && s >= 0 && s <= 127 && n >= 0 && n <= 127) {
68
+ const A = parseInt(E.dataset.midiCc, 10);
69
+ return !Number.isNaN(A) && A >= 0 && A <= 127 && console.warn(
70
+ `Element has both 7-bit (data-midi-cc="${A}") and 14-bit (data-midi-msb="${s}" data-midi-lsb="${n}") CC attributes. 14-bit takes precedence.`,
71
+ E
72
+ ), {
73
+ msb: s,
74
+ lsb: n,
75
+ is14Bit: !0,
76
+ channel: parseInt(E.dataset.midiChannel, 10) || void 0,
77
+ min: parseFloat(E.getAttribute("min")) || 0,
78
+ max: parseFloat(E.getAttribute("max")) || 127,
79
+ invert: E.dataset.midiInvert === "true",
80
+ label: E.dataset.midiLabel
81
+ };
82
+ }
83
+ const _ = parseInt(E.dataset.midiCc, 10);
84
+ return !Number.isNaN(_) && _ >= 0 && _ <= 127 ? {
85
+ cc: _,
86
+ channel: parseInt(E.dataset.midiChannel, 10) || void 0,
87
+ min: parseFloat(E.getAttribute("min")) || 0,
88
+ max: parseFloat(E.getAttribute("max")) || 127,
89
+ invert: E.dataset.midiInvert === "true",
90
+ label: E.dataset.midiLabel
91
+ } : ((E.dataset.midiCc !== void 0 || E.dataset.midiMsb !== void 0 && E.dataset.midiLsb !== void 0) && console.warn("Invalid MIDI configuration on element:", E), null);
92
+ }
93
+ /**
94
+ * Clean up
95
+ */
96
+ destroy() {
97
+ this.disableAutoBinding();
98
+ }
99
+ }
100
+ class M extends Error {
101
+ constructor(E, s) {
102
+ super(E), this.name = "MIDIError", this.code = s, Error.captureStackTrace && Error.captureStackTrace(this, this.constructor);
103
+ }
104
+ }
105
+ class p extends M {
106
+ constructor(E, s) {
107
+ super(E, "MIDI_ACCESS_ERROR"), this.name = "MIDIAccessError", this.reason = s;
108
+ }
109
+ }
110
+ class g extends M {
111
+ constructor(E) {
112
+ super(E, "MIDI_CONNECTION_ERROR"), this.name = "MIDIConnectionError";
113
+ }
114
+ }
115
+ class f extends M {
116
+ constructor(E, s, n) {
117
+ super(E, "MIDI_DEVICE_ERROR"), this.name = "MIDIDeviceError", this.deviceType = s, this.deviceId = n;
118
+ }
119
+ }
120
+ class m extends M {
121
+ constructor(E, s) {
122
+ super(E, "MIDI_VALIDATION_ERROR"), this.name = "MIDIValidationError", this.validationType = s;
123
+ }
124
+ }
125
+ class H extends Error {
126
+ constructor(E, s) {
127
+ super(E), this.name = "DX7Error", this.code = s, Error.captureStackTrace && Error.captureStackTrace(this, this.constructor);
128
+ }
129
+ }
130
+ class D extends H {
131
+ constructor(E, s, n) {
132
+ super(E, "DX7_PARSE_ERROR"), this.name = "DX7ParseError", this.parseType = s, this.offset = n;
133
+ }
134
+ }
135
+ class c extends H {
136
+ constructor(E, s, n) {
137
+ super(E, "DX7_VALIDATION_ERROR"), this.name = "DX7ValidationError", this.validationType = s, this.value = n;
138
+ }
139
+ }
140
+ function O(a, E, s) {
141
+ return Math.max(E, Math.min(s, a));
142
+ }
143
+ function w(a, E, s, n = !1) {
144
+ const _ = (a - E) / (s - E), e = (n ? 1 - _ : _) * 127;
145
+ return O(Math.round(e), 0, 127);
146
+ }
147
+ function Q(a, E, s, n = !1) {
148
+ let _ = O(a, 0, 127) / 127;
149
+ return n && (_ = 1 - _), E + _ * (s - E);
150
+ }
151
+ function j(a) {
152
+ const E = {
153
+ C: 0,
154
+ "C#": 1,
155
+ DB: 1,
156
+ D: 2,
157
+ "D#": 3,
158
+ EB: 3,
159
+ E: 4,
160
+ F: 5,
161
+ "F#": 6,
162
+ GB: 6,
163
+ G: 7,
164
+ "G#": 8,
165
+ AB: 8,
166
+ A: 9,
167
+ "A#": 10,
168
+ BB: 10,
169
+ B: 11
170
+ }, s = a.match(/^([A-G][#b]?)(-?\d+)$/i);
171
+ if (!s)
172
+ throw new m(`Invalid note name: ${a}`, "note", a);
173
+ const [, n, _] = s, A = E[n.toUpperCase()];
174
+ if (A === void 0)
175
+ throw new m(`Invalid note: ${n}`, "note", n);
176
+ const e = (parseInt(_, 10) + 1) * 12 + A;
177
+ return O(e, 0, 127);
178
+ }
179
+ function J(a, E = !1) {
180
+ const _ = E ? ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"] : ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"], A = Math.floor(a / 12) - 1;
181
+ return `${_[a % 12]}${A}`;
182
+ }
183
+ function k(a) {
184
+ const E = 69 + 12 * Math.log2(a / 440);
185
+ return O(Math.round(E), 0, 127);
186
+ }
187
+ function V(a) {
188
+ return 440 * 2 ** ((a - 69) / 12);
189
+ }
190
+ function X(a) {
191
+ return {
192
+ 0: "Bank Select",
193
+ 1: "Modulation",
194
+ 2: "Breath Controller",
195
+ 4: "Foot Controller",
196
+ 5: "Portamento Time",
197
+ 7: "Volume",
198
+ 8: "Balance",
199
+ 10: "Pan",
200
+ 11: "Expression",
201
+ 64: "Sustain Pedal",
202
+ 65: "Portamento",
203
+ 66: "Sostenuto",
204
+ 67: "Soft Pedal",
205
+ 68: "Legato",
206
+ 71: "Resonance",
207
+ 72: "Release Time",
208
+ 73: "Attack Time",
209
+ 74: "Cutoff",
210
+ 75: "Decay Time",
211
+ 76: "Vibrato Rate",
212
+ 77: "Vibrato Depth",
213
+ 78: "Vibrato Delay",
214
+ 84: "Portamento Control",
215
+ 91: "Reverb",
216
+ 92: "Tremolo",
217
+ 93: "Chorus",
218
+ 94: "Detune",
219
+ 95: "Phaser",
220
+ 120: "All Sound Off",
221
+ 121: "Reset All Controllers",
222
+ 123: "All Notes Off"
223
+ }[a] || `CC ${a}`;
224
+ }
225
+ function x(a) {
226
+ const E = O(Math.round(a), 0, 16383);
227
+ return {
228
+ msb: E >> 7 & 127,
229
+ lsb: E & 127
230
+ };
231
+ }
232
+ function Y(a, E) {
233
+ return O(a, 0, 127) << 7 | O(E, 0, 127);
234
+ }
235
+ function Z(a, E, s, n = !1) {
236
+ const _ = (a - E) / (s - E), e = (n ? 1 - _ : _) * 16383;
237
+ return x(e);
238
+ }
239
+ function tt(a, E, s, n, _ = !1) {
240
+ let e = Y(a, E) / 16383;
241
+ return _ && (e = 1 - e), s + e * (n - s);
242
+ }
243
+ class b {
244
+ constructor() {
245
+ this.events = /* @__PURE__ */ new Map();
246
+ }
247
+ /**
248
+ * Register an event listener
249
+ * @param {string} event - Event name
250
+ * @param {Function} handler - Event handler function
251
+ * @returns {Function} Unsubscribe function
252
+ */
253
+ on(E, s) {
254
+ return this.events.has(E) || this.events.set(E, []), this.events.get(E).push(s), () => this.off(E, s);
255
+ }
256
+ /**
257
+ * Register a one-time event listener
258
+ * @param {string} event - Event name
259
+ * @param {Function} handler - Event handler function
260
+ */
261
+ once(E, s) {
262
+ const n = (..._) => {
263
+ s(..._), this.off(E, n);
264
+ };
265
+ this.on(E, n);
266
+ }
267
+ /**
268
+ * Remove an event listener
269
+ * @param {string} event - Event name
270
+ * @param {Function} handler - Event handler function
271
+ */
272
+ off(E, s) {
273
+ if (!this.events.has(E)) return;
274
+ const n = this.events.get(E), _ = n.indexOf(s);
275
+ _ > -1 && n.splice(_, 1), n.length === 0 && this.events.delete(E);
276
+ }
277
+ /**
278
+ * Emit an event
279
+ * @param {string} event - Event name
280
+ * @param {*} data - Event data
281
+ */
282
+ emit(E, s) {
283
+ if (!this.events.has(E)) return;
284
+ [...this.events.get(E)].forEach((_) => {
285
+ try {
286
+ _(s);
287
+ } catch (A) {
288
+ console.error(`Error in event handler for "${E}":`, A);
289
+ }
290
+ });
291
+ }
292
+ /**
293
+ * Remove all event listeners
294
+ * @param {string} [event] - Optional event name to clear specific event
295
+ */
296
+ removeAllListeners(E) {
297
+ E ? this.events.delete(E) : this.events.clear();
298
+ }
299
+ }
300
+ const d = {
301
+ DEVICE_CHANGE: "device-change",
302
+ INPUT_DEVICE_CONNECTED: "input-device-connected",
303
+ INPUT_DEVICE_DISCONNECTED: "input-device-disconnected",
304
+ OUTPUT_DEVICE_CONNECTED: "output-device-connected",
305
+ OUTPUT_DEVICE_DISCONNECTED: "output-device-disconnected"
306
+ };
307
+ class $ extends b {
308
+ /**
309
+ * @param {Object} options
310
+ * @param {boolean} [options.sysex=false] - Request SysEx access
311
+ */
312
+ constructor(E = {}) {
313
+ super(), this.options = {
314
+ sysex: !1,
315
+ ...E
316
+ }, this.midiAccess = null, this.output = null, this.input = null;
317
+ }
318
+ /**
319
+ * Request MIDI access from the browser
320
+ * @returns {Promise<void>}
321
+ * @throws {MIDIAccessError} If MIDI is not supported or access is denied
322
+ */
323
+ async requestAccess() {
324
+ if (!navigator.requestMIDIAccess)
325
+ throw new p("Web MIDI API is not supported in this browser", "unsupported");
326
+ try {
327
+ this.midiAccess = await navigator.requestMIDIAccess({
328
+ sysex: this.options.sysex
329
+ }), this.midiAccess.onstatechange = (E) => {
330
+ const s = E.port, n = E.port.state;
331
+ this.emit(d.DEVICE_CHANGE, {
332
+ port: s,
333
+ state: n,
334
+ type: s.type,
335
+ device: {
336
+ id: s.id,
337
+ name: s.name,
338
+ manufacturer: s.manufacturer || "Unknown"
339
+ }
340
+ }), n === "disconnected" ? s.type === "input" ? (this.emit(d.INPUT_DEVICE_DISCONNECTED, { device: s }), this.input && this.input.id === s.id && (this.input = null)) : s.type === "output" && (this.emit(d.OUTPUT_DEVICE_DISCONNECTED, { device: s }), this.output && this.output.id === s.id && (this.output = null)) : n === "connected" && (s.type === "input" ? this.emit(d.INPUT_DEVICE_CONNECTED, { device: s }) : s.type === "output" && this.emit(d.OUTPUT_DEVICE_CONNECTED, { device: s }));
341
+ };
342
+ } catch (E) {
343
+ throw E.name === "SecurityError" ? new p("MIDI access denied. SysEx requires user permission.", "denied") : new p(`Failed to get MIDI access: ${E.message}`, "failed");
344
+ }
345
+ }
346
+ /**
347
+ * Get all available MIDI outputs
348
+ * @returns {Array<{id: string, name: string, manufacturer: string}>}
349
+ */
350
+ getOutputs() {
351
+ if (!this.midiAccess) return [];
352
+ const E = [];
353
+ return this.midiAccess.outputs.forEach((s) => {
354
+ s.state === "connected" && E.push({
355
+ id: s.id,
356
+ name: s.name,
357
+ manufacturer: s.manufacturer || "Unknown"
358
+ });
359
+ }), E;
360
+ }
361
+ /**
362
+ * Get all available MIDI inputs
363
+ * @returns {Array<{id: string, name: string, manufacturer: string}>}
364
+ */
365
+ getInputs() {
366
+ if (!this.midiAccess) return [];
367
+ const E = [];
368
+ return this.midiAccess.inputs.forEach((s) => {
369
+ s.state === "connected" && E.push({
370
+ id: s.id,
371
+ name: s.name,
372
+ manufacturer: s.manufacturer || "Unknown"
373
+ });
374
+ }), E;
375
+ }
376
+ /**
377
+ * Connect to a MIDI output device
378
+ * @param {string|number} [device] - Device name, ID, or index (defaults to first available)
379
+ * @returns {Promise<void>}
380
+ * @throws {MIDIConnectionError} If MIDI access not initialized
381
+ * @throws {MIDIDeviceError} If device not found or index out of range
382
+ */
383
+ async connect(E) {
384
+ if (!this.midiAccess)
385
+ throw new g("MIDI access not initialized. Call requestAccess() first.");
386
+ const s = Array.from(this.midiAccess.outputs.values());
387
+ if (s.length === 0)
388
+ throw new f("No MIDI output devices available", "output");
389
+ if (E === void 0) {
390
+ this.output = s[0];
391
+ return;
392
+ }
393
+ if (typeof E == "number") {
394
+ if (E < 0 || E >= s.length)
395
+ throw new f(
396
+ `Output index ${E} out of range (0-${s.length - 1})`,
397
+ "output",
398
+ E
399
+ );
400
+ this.output = s[E];
401
+ return;
402
+ }
403
+ if (this.output = s.find((n) => n.name === E || n.id === E), !this.output) {
404
+ const n = s.map((_) => _.name).join(", ");
405
+ throw new f(
406
+ `MIDI output "${E}" not found. Available: ${n}`,
407
+ "output",
408
+ E
409
+ );
410
+ }
411
+ }
412
+ /**
413
+ * Connect to a MIDI input device for receiving messages
414
+ * @param {string|number} [device] - Device name, ID, or index (defaults to first available)
415
+ * @param {Function} onMessage - Callback for incoming MIDI messages
416
+ * @returns {Promise<void>}
417
+ * @throws {MIDIConnectionError} If MIDI access not initialized
418
+ * @throws {MIDIValidationError} If onMessage is not a function
419
+ * @throws {MIDIDeviceError} If device not found or index out of range
420
+ */
421
+ async connectInput(E, s) {
422
+ if (!this.midiAccess)
423
+ throw new g("MIDI access not initialized. Call requestAccess() first.");
424
+ if (typeof s != "function")
425
+ throw new m("onMessage callback must be a function", "callback");
426
+ const n = Array.from(this.midiAccess.inputs.values());
427
+ if (n.length === 0)
428
+ throw new f("No MIDI input devices available", "input");
429
+ if (this.input && (this.input.onmidimessage = null), E === void 0)
430
+ this.input = n[0];
431
+ else if (typeof E == "number") {
432
+ if (E < 0 || E >= n.length)
433
+ throw new f(
434
+ `Input index ${E} out of range (0-${n.length - 1})`,
435
+ "input",
436
+ E
437
+ );
438
+ this.input = n[E];
439
+ } else if (this.input = n.find((_) => _.name === E || _.id === E), !this.input) {
440
+ const _ = n.map((A) => A.name).join(", ");
441
+ throw new f(
442
+ `MIDI input "${E}" not found. Available: ${_}`,
443
+ "input",
444
+ E
445
+ );
446
+ }
447
+ this.input.onmidimessage = (_) => {
448
+ s(_);
449
+ };
450
+ }
451
+ /**
452
+ * Send a MIDI message
453
+ * @param {Uint8Array|Array<number>} message - MIDI message bytes
454
+ * @param {number} [timestamp=performance.now()] - Optional timestamp
455
+ */
456
+ send(E, s = null) {
457
+ if (!this.output) {
458
+ console.warn("No MIDI output connected. Call connect() first.");
459
+ return;
460
+ }
461
+ try {
462
+ const n = new Uint8Array(E);
463
+ s === null ? this.output.send(n) : this.output.send(n, s);
464
+ } catch (n) {
465
+ console.error("Failed to send MIDI message:", n);
466
+ }
467
+ }
468
+ /**
469
+ * Send a SysEx message
470
+ * @param {Array<number>} data - SysEx data bytes (without F0/F7 wrapper)
471
+ * @param {boolean} [includeWrapper=false] - If true, data already includes F0/F7
472
+ */
473
+ sendSysEx(E, s = !1) {
474
+ if (!this.options.sysex) {
475
+ console.warn("SysEx not enabled. Initialize with sysex: true");
476
+ return;
477
+ }
478
+ let n;
479
+ s ? n = [240, ...E, 247] : n = E, this.send(n);
480
+ }
481
+ /**
482
+ * Disconnect from current output and input
483
+ */
484
+ disconnect() {
485
+ this.input && (this.input.onmidimessage = null, this.input = null), this.output = null;
486
+ }
487
+ /**
488
+ * Check if currently connected to an output
489
+ * @returns {boolean}
490
+ */
491
+ isConnected() {
492
+ return this.output !== null;
493
+ }
494
+ /**
495
+ * Get current output device info
496
+ * @returns {Object|null}
497
+ */
498
+ getCurrentOutput() {
499
+ return this.output ? {
500
+ id: this.output.id,
501
+ name: this.output.name,
502
+ manufacturer: this.output.manufacturer || "Unknown"
503
+ } : null;
504
+ }
505
+ /**
506
+ * Get current input device info
507
+ * @returns {Object|null}
508
+ */
509
+ getCurrentInput() {
510
+ return this.input ? {
511
+ id: this.input.id,
512
+ name: this.input.name,
513
+ manufacturer: this.input.manufacturer || "Unknown"
514
+ } : null;
515
+ }
516
+ }
517
+ const o = {
518
+ READY: "ready",
519
+ ERROR: "error",
520
+ CC_SEND: "cc-send",
521
+ CC_RECV: "cc-recv",
522
+ NOTE_ON_SEND: "note-on-send",
523
+ NOTE_ON_RECV: "note-on-recv",
524
+ NOTE_OFF_SEND: "note-off-send",
525
+ NOTE_OFF_RECV: "note-off-recv",
526
+ SYSEX_SEND: "sysex-send",
527
+ SYSEX_RECV: "sysex-recv",
528
+ OUTPUT_CHANGED: "output-changed",
529
+ INPUT_CONNECTED: "input-connected",
530
+ DESTROYED: "destroyed",
531
+ MIDI_MSG: "midi-msg",
532
+ PATCH_SAVED: "patch-saved",
533
+ PATCH_LOADED: "patch-loaded",
534
+ PATCH_DELETED: "patch-deleted"
535
+ };
536
+ class z extends b {
537
+ /**
538
+ * @param {Object} options
539
+ * @param {number} [options.channel=1] - Default MIDI channel (1-16)
540
+ * @param {string|number} [options.output] - MIDI output device
541
+ * @param {string|number} [options.input] - MIDI input device
542
+ * @param {boolean} [options.sysex=false] - Request SysEx access
543
+ * @param {boolean} [options.autoConnect=true] - Auto-connect to first available output
544
+ * @param {Function} [options.onReady] - Callback when MIDI is ready
545
+ * @param {Function} [options.onError] - Error handler
546
+ */
547
+ constructor(E = {}) {
548
+ super(), this.options = {
549
+ channel: 1,
550
+ autoConnect: !0,
551
+ sysex: !1,
552
+ ...E
553
+ }, this.connection = null, this.bindings = /* @__PURE__ */ new Map(), this.state = /* @__PURE__ */ new Map(), this.initialized = !1;
554
+ }
555
+ /**
556
+ * Initialize MIDI access
557
+ * @returns {Promise<void>}
558
+ */
559
+ async initialize() {
560
+ if (this.initialized) {
561
+ console.warn("MIDI Controller already initialized");
562
+ return;
563
+ }
564
+ try {
565
+ this.connection = new $({
566
+ sysex: this.options.sysex
567
+ }), await this.connection.requestAccess(), this.options.autoConnect && await this.connection.connect(this.options.output), this.options.input !== void 0 && await this.connectInput(this.options.input), this.initialized = !0, this.emit(o.READY, this), this.options.onReady?.(this);
568
+ } catch (E) {
569
+ throw this.emit(o.ERROR, E), this.options.onError?.(E), E;
570
+ }
571
+ }
572
+ /**
573
+ * Connect to a MIDI input device for receiving messages
574
+ * @param {string|number} device - Device name, ID, or index
575
+ * @returns {Promise<void>}
576
+ */
577
+ async connectInput(E) {
578
+ await this.connection.connectInput(E, (s) => {
579
+ this._handleMIDIMessage(s);
580
+ }), this.emit(o.INPUT_CONNECTED, this.connection.getCurrentInput());
581
+ }
582
+ /**
583
+ * Send a control change message
584
+ * @param {number} cc - CC number (0-127)
585
+ * @param {number} value - CC value (0-127)
586
+ * @param {number} [channel] - MIDI channel (defaults to controller channel)
587
+ */
588
+ sendCC(E, s, n = this.options.channel) {
589
+ if (!this.initialized) {
590
+ console.warn("MIDI not initialized. Call initialize() first.");
591
+ return;
592
+ }
593
+ E = O(Math.round(E), 0, 127), s = O(Math.round(s), 0, 127), n = O(Math.round(n), 1, 16);
594
+ const _ = 176 + (n - 1);
595
+ this.connection.send([_, E, s]);
596
+ const A = `${n}:${E}`;
597
+ this.state.set(A, s), this.emit(o.CC_SEND, { cc: E, value: s, channel: n });
598
+ }
599
+ /**
600
+ * Send a SysEx message
601
+ * @param {Array<number>} data - SysEx data bytes (without F0/F7 wrapper)
602
+ * @param {boolean} [includeWrapper=false] - If true, data already includes F0/F7
603
+ *
604
+ * @example
605
+ * // Send with wrapper included
606
+ * midi.sendSysEx([0xF0, 0x42, 0x30, 0x00, 0x01, 0x2F, 0x12, 0xF7], true)
607
+ */
608
+ sendSysEx(E, s = !1) {
609
+ if (!this.initialized) {
610
+ console.warn("MIDI not initialized. Call initialize() first.");
611
+ return;
612
+ }
613
+ if (!this.options.sysex) {
614
+ console.warn("SysEx not enabled. Initialize with sysex: true");
615
+ return;
616
+ }
617
+ this.connection.sendSysEx(E, s), this.emit(o.SYSEX_SEND, { data: E, includeWrapper: s });
618
+ }
619
+ /**
620
+ * Send a note on message
621
+ * @param {number} note - Note number (0-127)
622
+ * @param {number} [velocity=64] - Note velocity (0-127)
623
+ * @param {number} [channel] - MIDI channel
624
+ */
625
+ sendNoteOn(E, s = 64, n = this.options.channel) {
626
+ if (!this.initialized) return;
627
+ E = O(Math.round(E), 0, 127), s = O(Math.round(s), 0, 127), n = O(Math.round(n), 1, 16);
628
+ const _ = 144 + (n - 1);
629
+ this.connection.send([_, E, s]), this.emit(o.NOTE_ON_SEND, { note: E, velocity: s, channel: n });
630
+ }
631
+ /**
632
+ * Send a note off message
633
+ * @param {number} note - Note number (0-127)
634
+ * @param {number} [channel] - MIDI channel
635
+ * @param {number} [velocity=0] - Release velocity (0-127)
636
+ */
637
+ sendNoteOff(E, s = this.options.channel, n = 0) {
638
+ if (!this.initialized) return;
639
+ E = O(Math.round(E), 0, 127), n = O(Math.round(n), 0, 127), s = O(Math.round(s), 1, 16);
640
+ const _ = 144 + (s - 1);
641
+ this.connection.send([_, E, n]), this.emit(o.NOTE_OFF_SEND, { note: E, channel: s, velocity: n });
642
+ }
643
+ /**
644
+ * Bind a control programmatically
645
+ * @param {HTMLElement} element - DOM element
646
+ * @param {Object} config - Binding configuration
647
+ * @param {number} config.cc - CC number
648
+ * @param {number} [config.min=0] - Minimum input value
649
+ * @param {number} [config.max=127] - Maximum input value
650
+ * @param {number} [config.channel] - Override channel
651
+ * @param {boolean} [config.invert=false] - Invert the value
652
+ * @param {Function} [config.onInput] - Optional callback for value updates (receives normalized element value)
653
+ * @param {Object} [options={}] - Additional options
654
+ * @param {number} [options.debounce=0] - Debounce delay in ms for high-frequency updates
655
+ * @returns {Function} Unbind function
656
+ */
657
+ bind(E, s, n = {}) {
658
+ if (!E)
659
+ return console.warn("Cannot bind: element is null or undefined"), () => {
660
+ };
661
+ const _ = this._createBinding(E, s, n);
662
+ return this.bindings.set(E, _), this.initialized && this.connection?.isConnected() && _.handler({ target: E }), () => this.unbind(E);
663
+ }
664
+ /**
665
+ * Unbind a control
666
+ * @param {HTMLElement} element
667
+ */
668
+ unbind(E) {
669
+ const s = this.bindings.get(E);
670
+ s && (s.destroy(), this.bindings.delete(E));
671
+ }
672
+ /**
673
+ * Get current value of a CC
674
+ * @param {number} cc - CC number
675
+ * @param {number} [channel] - MIDI channel
676
+ * @returns {number|undefined}
677
+ */
678
+ getCC(E, s = this.options.channel) {
679
+ const n = `${s}:${E}`;
680
+ return this.state.get(n);
681
+ }
682
+ /**
683
+ * Get all available MIDI outputs
684
+ * @returns {Array<{id: string, name: string, manufacturer: string}>}
685
+ */
686
+ getOutputs() {
687
+ return this.connection?.getOutputs() || [];
688
+ }
689
+ /**
690
+ * Get all available MIDI inputs
691
+ * @returns {Array<{id: string, name: string, manufacturer: string}>}
692
+ */
693
+ getInputs() {
694
+ return this.connection?.getInputs() || [];
695
+ }
696
+ /**
697
+ * Switch to a different output device
698
+ * @param {string|number} output - Device name, ID, or index
699
+ * @returns {Promise<void>}
700
+ */
701
+ async setOutput(E) {
702
+ await this.connection.connect(E), this.emit(o.OUTPUT_CHANGED, this.connection.getCurrentOutput());
703
+ }
704
+ /**
705
+ * Get current output device
706
+ * @returns {Object|null}
707
+ */
708
+ getCurrentOutput() {
709
+ return this.connection?.getCurrentOutput() || null;
710
+ }
711
+ /**
712
+ * Get current input device
713
+ * @returns {Object|null}
714
+ */
715
+ getCurrentInput() {
716
+ return this.connection?.getCurrentInput() || null;
717
+ }
718
+ /**
719
+ * Clean up resources
720
+ */
721
+ destroy() {
722
+ for (const E of this.bindings.values())
723
+ E.destroy();
724
+ this.bindings.clear(), this.state.clear(), this.connection?.disconnect(), this.initialized = !1, this.emit(o.DESTROYED), this.removeAllListeners();
725
+ }
726
+ /**
727
+ * Handle incoming MIDI messages
728
+ * @private
729
+ */
730
+ _handleMIDIMessage(E) {
731
+ const [s, n, _] = E.data, A = s & 240, e = (s & 15) + 1;
732
+ if (s === 240) {
733
+ this.emit(o.SYSEX_RECV, {
734
+ data: Array.from(E.data),
735
+ timestamp: E.midiwire
736
+ });
737
+ return;
738
+ }
739
+ if (A === 176) {
740
+ const r = `${e}:${n}`;
741
+ this.state.set(r, _), this.emit(o.CC_RECV, {
742
+ cc: n,
743
+ value: _,
744
+ channel: e
745
+ });
746
+ return;
747
+ }
748
+ if (A === 144 && _ > 0) {
749
+ this.emit(o.NOTE_ON_RECV, {
750
+ note: n,
751
+ velocity: _,
752
+ channel: e
753
+ });
754
+ return;
755
+ }
756
+ if (A === 128 || A === 144 && _ === 0) {
757
+ this.emit(o.NOTE_OFF_RECV, {
758
+ note: n,
759
+ channel: e
760
+ });
761
+ return;
762
+ }
763
+ this.emit(o.MIDI_MSG, {
764
+ status: s,
765
+ data: [n, _],
766
+ channel: e,
767
+ timestamp: E.midiwire
768
+ });
769
+ }
770
+ /**
771
+ * Create a binding between an element and MIDI CC
772
+ * @private
773
+ */
774
+ _createBinding(E, s, n = {}) {
775
+ const {
776
+ min: _ = parseFloat(E.getAttribute("min")) || 0,
777
+ max: A = parseFloat(E.getAttribute("max")) || 127,
778
+ channel: e,
779
+ invert: r = !1,
780
+ onInput: T = void 0
781
+ } = s, { debounce: u = 0 } = n, i = {
782
+ ...s,
783
+ min: _,
784
+ max: A,
785
+ invert: r,
786
+ onInput: T
787
+ };
788
+ if (e !== void 0 && (i.channel = e), s.is14Bit) {
789
+ const { msb: h, lsb: U } = s, I = (R) => {
790
+ const F = parseFloat(R.target.value);
791
+ if (Number.isNaN(F)) return;
792
+ const { msb: y, lsb: v } = Z(F, _, A, r), G = e || this.options.channel;
793
+ this.sendCC(h, y, G), this.sendCC(U, v, G);
794
+ };
795
+ let K = null;
796
+ const L = u > 0 ? (R) => {
797
+ K && clearTimeout(K), K = setTimeout(() => {
798
+ I(R), K = null;
799
+ }, u);
800
+ } : I;
801
+ return E.addEventListener("input", L), E.addEventListener("change", L), {
802
+ element: E,
803
+ config: i,
804
+ handler: I,
805
+ destroy: () => {
806
+ K && clearTimeout(K), E.removeEventListener("input", L), E.removeEventListener("change", L);
807
+ }
808
+ };
809
+ }
810
+ const { cc: P } = s, l = (h) => {
811
+ const U = parseFloat(h.target.value);
812
+ if (Number.isNaN(U)) return;
813
+ const I = w(U, _, A, r), K = e === void 0 ? this.options.channel : e;
814
+ this.sendCC(P, I, K);
815
+ };
816
+ let S = null;
817
+ const N = u > 0 ? (h) => {
818
+ S && clearTimeout(S), S = setTimeout(() => {
819
+ l(h), S = null;
820
+ }, u);
821
+ } : l;
822
+ return E.addEventListener("input", N), E.addEventListener("change", N), {
823
+ element: E,
824
+ config: i,
825
+ handler: l,
826
+ destroy: () => {
827
+ S && clearTimeout(S), E.removeEventListener("input", N), E.removeEventListener("change", N);
828
+ }
829
+ };
830
+ }
831
+ /**
832
+ * Get current state as a patch object
833
+ * @param {string} [name] - Optional patch name
834
+ * @returns {Object} Patch object
835
+ */
836
+ getPatch(E = "Unnamed Patch") {
837
+ const s = {
838
+ name: E,
839
+ device: this.getCurrentOutput()?.name || null,
840
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
841
+ version: "1.0",
842
+ channels: {},
843
+ settings: {}
844
+ };
845
+ for (const [n, _] of this.state.entries()) {
846
+ const [A, e] = n.split(":").map(Number);
847
+ s.channels[A] || (s.channels[A] = { ccs: {}, notes: {} }), s.channels[A].ccs[e] = _;
848
+ }
849
+ for (const [n, _] of this.bindings.entries()) {
850
+ const { config: A } = _;
851
+ if (A.cc) {
852
+ const e = `cc${A.cc}`;
853
+ s.settings[e] = {
854
+ min: A.min,
855
+ max: A.max,
856
+ invert: A.invert || !1,
857
+ is14Bit: A.is14Bit || !1,
858
+ label: n.getAttribute?.("data-midi-label") || null,
859
+ elementId: n.id || null
860
+ };
861
+ }
862
+ }
863
+ return s;
864
+ }
865
+ /**
866
+ * Apply a patch to the controller
867
+ * @param {PatchData} patch - Patch object
868
+ * @returns {Promise<void>}
869
+ */
870
+ async setPatch(E) {
871
+ if (!E || !E.channels)
872
+ throw new m("Invalid patch format", "patch");
873
+ const s = E.version || "1.0";
874
+ s === "1.0" ? await this._applyPatchV1(E) : (console.warn(`Unknown patch version: ${s}. Attempting to apply as v1.0`), await this._applyPatchV1(E)), this.emit(o.PATCH_LOADED, { patch: E });
875
+ }
876
+ /**
877
+ * Apply v1.0 patch format
878
+ * @private
879
+ * @param {PatchData} patch
880
+ */
881
+ async _applyPatchV1(E) {
882
+ for (const [s, n] of Object.entries(E.channels)) {
883
+ const _ = parseInt(s, 10);
884
+ if (n.ccs)
885
+ for (const [A, e] of Object.entries(n.ccs)) {
886
+ const r = parseInt(A, 10);
887
+ this.sendCC(r, e, _);
888
+ }
889
+ if (n.notes)
890
+ for (const [A, e] of Object.entries(n.notes)) {
891
+ const r = parseInt(A, 10);
892
+ e > 0 ? this.sendNoteOn(r, e, _) : this.sendNoteOff(r, _);
893
+ }
894
+ }
895
+ if (E.settings)
896
+ for (const [s, n] of Object.entries(E.settings))
897
+ for (const [_, A] of this.bindings.entries())
898
+ A.config.cc?.toString() === s.replace("cc", "") && (_.min !== void 0 && n.min !== void 0 && (_.min = String(n.min)), _.max !== void 0 && n.max !== void 0 && (_.max = String(n.max)));
899
+ for (const [s, n] of this.bindings.entries()) {
900
+ const { config: _ } = n;
901
+ if (_.cc !== void 0) {
902
+ const A = _.channel || this.options.channel, e = E.channels[A];
903
+ if (e?.ccs) {
904
+ const r = e.ccs[_.cc];
905
+ if (r !== void 0) {
906
+ const T = _.min !== void 0 ? _.min : parseFloat(s.getAttribute?.("min")) || 0, u = _.max !== void 0 ? _.max : parseFloat(s.getAttribute?.("max")) || 127, i = _.invert || !1;
907
+ let P;
908
+ i ? P = u - r / 127 * (u - T) : P = T + r / 127 * (u - T), _.onInput && typeof _.onInput == "function" ? _.onInput(P) : (s.value = P, s.dispatchEvent(new Event("input", { bubbles: !0 })));
909
+ }
910
+ }
911
+ }
912
+ }
913
+ }
914
+ /**
915
+ * Save a patch to localStorage
916
+ * @param {string} name - Patch name
917
+ * @param {Object} [patch] - Optional patch object (will use getPatch() if not provided)
918
+ * @returns {string} Storage key used
919
+ */
920
+ savePatch(E, s = null) {
921
+ const n = s || this.getPatch(E), _ = `midiwire_patch_${E}`;
922
+ try {
923
+ return localStorage.setItem(_, JSON.stringify(n)), this.emit(o.PATCH_SAVED, { name: E, patch: n }), _;
924
+ } catch (A) {
925
+ throw console.error("Failed to save patch:", A), A;
926
+ }
927
+ }
928
+ /**
929
+ * Load a patch from localStorage
930
+ * @param {string} name - Patch name
931
+ * @returns {Object|null} Patch object or null if not found
932
+ */
933
+ loadPatch(E) {
934
+ const s = `midiwire_patch_${E}`;
935
+ try {
936
+ const n = localStorage.getItem(s);
937
+ if (!n)
938
+ return null;
939
+ const _ = JSON.parse(n);
940
+ return this.emit(o.PATCH_LOADED, { name: E, patch: _ }), _;
941
+ } catch (n) {
942
+ return console.error("Failed to load patch:", n), null;
943
+ }
944
+ }
945
+ /**
946
+ * Delete a patch from localStorage
947
+ * @param {string} name - Patch name
948
+ * @returns {boolean} Success
949
+ */
950
+ deletePatch(E) {
951
+ const s = `midiwire_patch_${E}`;
952
+ try {
953
+ return localStorage.removeItem(s), this.emit(o.PATCH_DELETED, { name: E }), !0;
954
+ } catch (n) {
955
+ return console.error("Failed to delete patch:", n), !1;
956
+ }
957
+ }
958
+ /**
959
+ * List all saved patches
960
+ * @returns {Array<Object>} Array of { name, patch }
961
+ */
962
+ listPatches() {
963
+ const E = [];
964
+ try {
965
+ for (let s = 0; s < localStorage.length; s++) {
966
+ const n = localStorage.key(s);
967
+ if (n?.startsWith("midiwire_patch_")) {
968
+ const _ = n.replace("midiwire_patch_", ""), A = this.loadPatch(_);
969
+ A && E.push({ name: _, patch: A });
970
+ }
971
+ }
972
+ } catch (s) {
973
+ console.error("Failed to list patches:", s);
974
+ }
975
+ return E.sort((s, n) => s.name.localeCompare(n.name));
976
+ }
977
+ }
978
+ class W {
979
+ /**
980
+ * @param {Object} options
981
+ * @param {MIDIController} options.midiController - The MIDIController instance
982
+ * @param {Function} options.onStatusUpdate - Callback for status updates (message, state)
983
+ * @param {Function} options.onConnectionUpdate - Callback when connection status changes
984
+ * @param {number} [options.channel=1] - Default MIDI channel
985
+ */
986
+ constructor(E = {}) {
987
+ this.midi = E.midiController || null, this.onStatusUpdate = E.onStatusUpdate || (() => {
988
+ }), this.onConnectionUpdate = E.onConnectionUpdate || (() => {
989
+ }), this.channel = E.channel || 1, this.currentDevice = null, this.isConnecting = !1;
990
+ }
991
+ /**
992
+ * Initialize the device manager with a MIDIController
993
+ * @param {MIDIController} midi
994
+ */
995
+ setMIDI(E) {
996
+ this.midi = E;
997
+ }
998
+ /**
999
+ * Set up device change event listeners
1000
+ * @param {Function} [onDeviceListChange] - Optional callback when device list should be refreshed
1001
+ */
1002
+ setupDeviceListeners(E) {
1003
+ this.midi?.connection && (this.midi.connection.on(d.OUTPUT_DEVICE_CONNECTED, ({ device: s }) => {
1004
+ this.updateStatus(`Device connected: ${s.name}`, "connected"), E && E();
1005
+ }), this.midi.connection.on(d.OUTPUT_DEVICE_DISCONNECTED, ({ device: s }) => {
1006
+ this.updateStatus(`Device disconnected: ${s.name}`, "error"), this.currentDevice && s.name === this.currentDevice.name && (this.currentDevice = null, this.updateConnectionStatus()), E && E();
1007
+ }));
1008
+ }
1009
+ /**
1010
+ * Update status message
1011
+ * @param {string} message
1012
+ * @param {string} state
1013
+ */
1014
+ updateStatus(E, s = "") {
1015
+ this.onStatusUpdate(E, s);
1016
+ }
1017
+ /**
1018
+ * Update connection status
1019
+ */
1020
+ updateConnectionStatus() {
1021
+ this.onConnectionUpdate(this.currentDevice, this.midi);
1022
+ }
1023
+ /**
1024
+ * Get the current list of MIDI output devices
1025
+ * @returns {Array<Object>} Array of device objects with id, name, manufacturer
1026
+ */
1027
+ getOutputDevices() {
1028
+ return this.midi?.connection ? this.midi.connection.getOutputs() : [];
1029
+ }
1030
+ /**
1031
+ * Check if a device is still connected
1032
+ * @param {string} deviceName
1033
+ * @returns {boolean}
1034
+ */
1035
+ isDeviceConnected(E) {
1036
+ return this.midi?.connection ? this.midi.connection.getOutputs().some((n) => n.name === E) : !1;
1037
+ }
1038
+ /**
1039
+ * Connect device selection events to the device manager
1040
+ * @param {HTMLSelectElement} deviceSelectElement
1041
+ * @param {Function} onConnect - Callback when device is connected (midi, device)
1042
+ */
1043
+ connectDeviceSelection(E, s) {
1044
+ !E || !this.midi || E.addEventListener("change", async (n) => {
1045
+ const _ = n.target.value;
1046
+ if (!_) {
1047
+ this.currentDevice && this.midi && (this.midi.connection.disconnect(), this.currentDevice = null, this.updateStatus("Disconnected"), this.updateConnectionStatus());
1048
+ return;
1049
+ }
1050
+ if (!this.isConnecting) {
1051
+ this.isConnecting = !0;
1052
+ try {
1053
+ await this.midi.setOutput(parseInt(_, 10)), this.currentDevice = this.midi.getCurrentOutput(), this.updateConnectionStatus(), s && await s(this.midi, this.currentDevice);
1054
+ } catch (A) {
1055
+ this.updateStatus(`Connection failed: ${A.message}`, "error");
1056
+ } finally {
1057
+ this.isConnecting = !1;
1058
+ }
1059
+ }
1060
+ });
1061
+ }
1062
+ /**
1063
+ * Connect channel selection events
1064
+ * @param {HTMLSelectElement} channelSelectElement
1065
+ */
1066
+ connectChannelSelection(E) {
1067
+ !E || !this.midi || E.addEventListener("change", (s) => {
1068
+ this.midi && (this.midi.options.channel = parseInt(s.target.value, 10), this.updateConnectionStatus());
1069
+ });
1070
+ }
1071
+ /**
1072
+ * Populate a device select element with available MIDI output devices
1073
+ * @param {HTMLSelectElement} selectElement
1074
+ * @param {Function} [onChange] - Optional callback when selection should change
1075
+ */
1076
+ populateDeviceList(E, s) {
1077
+ if (!E) return;
1078
+ const n = this.getOutputDevices();
1079
+ if (n.length > 0) {
1080
+ if (E.innerHTML = '<option value="">Select a device</option>' + n.map((_, A) => `<option value="${A}">${_.name}</option>`).join(""), this.currentDevice) {
1081
+ const _ = n.findIndex((A) => A.name === this.currentDevice.name);
1082
+ _ !== -1 ? E.value = _.toString() : (E.value = "", this.currentDevice = null, this.updateConnectionStatus());
1083
+ } else
1084
+ E.value = "";
1085
+ this.currentDevice || this.updateStatus("Select a MIDI device");
1086
+ } else
1087
+ E.innerHTML = '<option value="">No MIDI devices found</option>', this.updateStatus("No MIDI devices available", "error");
1088
+ s && s();
1089
+ }
1090
+ }
1091
+ class t {
1092
+ // Packed format (128 bytes)
1093
+ // See: DX7 Service Manual, Voice Memory Format
1094
+ static PACKED_SIZE = 128;
1095
+ static PACKED_OP_SIZE = 17;
1096
+ // 17 bytes per operator in packed format
1097
+ static NUM_OPERATORS = 6;
1098
+ // Packed operator parameter offsets (within each 17-byte operator block)
1099
+ static PACKED_OP_EG_RATE_1 = 0;
1100
+ static PACKED_OP_EG_RATE_2 = 1;
1101
+ static PACKED_OP_EG_RATE_3 = 2;
1102
+ static PACKED_OP_EG_RATE_4 = 3;
1103
+ static PACKED_OP_EG_LEVEL_1 = 4;
1104
+ static PACKED_OP_EG_LEVEL_2 = 5;
1105
+ static PACKED_OP_EG_LEVEL_3 = 6;
1106
+ static PACKED_OP_EG_LEVEL_4 = 7;
1107
+ static PACKED_OP_BREAK_POINT = 8;
1108
+ static PACKED_OP_L_SCALE_DEPTH = 9;
1109
+ static PACKED_OP_R_SCALE_DEPTH = 10;
1110
+ static PACKED_OP_CURVES = 11;
1111
+ // LC and RC packed
1112
+ static PACKED_OP_RATE_SCALING = 12;
1113
+ // RS and DET packed
1114
+ static PACKED_OP_MOD_SENS = 13;
1115
+ // AMS and KVS packed
1116
+ static PACKED_OP_OUTPUT_LEVEL = 14;
1117
+ static PACKED_OP_MODE_FREQ = 15;
1118
+ // Mode and Freq Coarse packed
1119
+ static PACKED_OP_DETUNE_FINE = 16;
1120
+ // OSC Detune and Freq Fine packed
1121
+ // Packed voice offsets (after 6 operators = bytes 102+)
1122
+ static PACKED_PITCH_EG_RATE_1 = 102;
1123
+ static PACKED_PITCH_EG_RATE_2 = 103;
1124
+ static PACKED_PITCH_EG_RATE_3 = 104;
1125
+ static PACKED_PITCH_EG_RATE_4 = 105;
1126
+ static PACKED_PITCH_EG_LEVEL_1 = 106;
1127
+ static PACKED_PITCH_EG_LEVEL_2 = 107;
1128
+ static PACKED_PITCH_EG_LEVEL_3 = 108;
1129
+ static PACKED_PITCH_EG_LEVEL_4 = 109;
1130
+ static OFFSET_ALGORITHM = 110;
1131
+ static OFFSET_FEEDBACK = 111;
1132
+ // Also contains OSC Sync
1133
+ static OFFSET_LFO_SPEED = 112;
1134
+ static OFFSET_LFO_DELAY = 113;
1135
+ static OFFSET_LFO_PM_DEPTH = 114;
1136
+ static OFFSET_LFO_AM_DEPTH = 115;
1137
+ static OFFSET_LFO_SYNC_WAVE = 116;
1138
+ // LFO sync, wave, and PM sensitivity packed
1139
+ static OFFSET_TRANSPOSE = 117;
1140
+ static OFFSET_AMP_MOD_SENS = 118;
1141
+ static OFFSET_EG_BIAS_SENS = 119;
1142
+ // Voice name (bytes 118-127)
1143
+ // IMPORTANT: Byte 118 serves dual-purpose in DX7 hardware:
1144
+ // 1. First character of voice name (as ASCII)
1145
+ // 2. Amp Mod Sensitivity parameter (as numeric value 0-127)
1146
+ // Both interpretations are used when converting to unpacked format
1147
+ static PACKED_NAME_START = 118;
1148
+ static NAME_LENGTH = 10;
1149
+ // Unpacked format (169 bytes)
1150
+ static UNPACKED_SIZE = 169;
1151
+ // Total unpacked size (159 params + 10 name)
1152
+ static UNPACKED_OP_SIZE = 23;
1153
+ // 23 bytes per operator in unpacked format
1154
+ // Unpacked operator parameter offsets (within each 23-byte operator block)
1155
+ static UNPACKED_OP_EG_RATE_1 = 0;
1156
+ static UNPACKED_OP_EG_RATE_2 = 1;
1157
+ static UNPACKED_OP_EG_RATE_3 = 2;
1158
+ static UNPACKED_OP_EG_RATE_4 = 3;
1159
+ static UNPACKED_OP_EG_LEVEL_1 = 4;
1160
+ static UNPACKED_OP_EG_LEVEL_2 = 5;
1161
+ static UNPACKED_OP_EG_LEVEL_3 = 6;
1162
+ static UNPACKED_OP_EG_LEVEL_4 = 7;
1163
+ static UNPACKED_OP_BREAK_POINT = 8;
1164
+ static UNPACKED_OP_L_SCALE_DEPTH = 9;
1165
+ static UNPACKED_OP_R_SCALE_DEPTH = 10;
1166
+ static UNPACKED_OP_L_CURVE = 11;
1167
+ static UNPACKED_OP_R_CURVE = 12;
1168
+ static UNPACKED_OP_RATE_SCALING = 13;
1169
+ static UNPACKED_OP_DETUNE = 14;
1170
+ static UNPACKED_OP_AMP_MOD_SENS = 15;
1171
+ static UNPACKED_OP_OUTPUT_LEVEL = 16;
1172
+ static UNPACKED_OP_MODE = 17;
1173
+ // Mode (0=ratio, 1=fixed)
1174
+ static UNPACKED_OP_KEY_VEL_SENS = 18;
1175
+ static UNPACKED_OP_FREQ_COARSE = 19;
1176
+ static UNPACKED_OP_OSC_DETUNE = 20;
1177
+ static UNPACKED_OP_FREQ_FINE = 21;
1178
+ // Unpacked pitch EG offsets (after 6 operators = index 138+)
1179
+ static UNPACKED_PITCH_EG_RATE_1 = 138;
1180
+ static UNPACKED_PITCH_EG_RATE_2 = 139;
1181
+ static UNPACKED_PITCH_EG_RATE_3 = 140;
1182
+ static UNPACKED_PITCH_EG_RATE_4 = 141;
1183
+ static UNPACKED_PITCH_EG_LEVEL_1 = 142;
1184
+ static UNPACKED_PITCH_EG_LEVEL_2 = 143;
1185
+ static UNPACKED_PITCH_EG_LEVEL_3 = 144;
1186
+ static UNPACKED_PITCH_EG_LEVEL_4 = 145;
1187
+ // Unpacked global parameters (after pitch EG = index 146+)
1188
+ static UNPACKED_ALGORITHM = 146;
1189
+ static UNPACKED_FEEDBACK = 147;
1190
+ static UNPACKED_OSC_SYNC = 148;
1191
+ static UNPACKED_LFO_SPEED = 149;
1192
+ static UNPACKED_LFO_DELAY = 150;
1193
+ static UNPACKED_LFO_PM_DEPTH = 151;
1194
+ static UNPACKED_LFO_AM_DEPTH = 152;
1195
+ static UNPACKED_LFO_KEY_SYNC = 153;
1196
+ static UNPACKED_LFO_WAVE = 154;
1197
+ static UNPACKED_LFO_PM_SENS = 155;
1198
+ static UNPACKED_AMP_MOD_SENS = 156;
1199
+ static UNPACKED_TRANSPOSE = 157;
1200
+ static UNPACKED_EG_BIAS_SENS = 158;
1201
+ static UNPACKED_NAME_START = 159;
1202
+ // VCED (single voice SysEx) format - for DX7 single patch dumps
1203
+ static VCED_SIZE = 163;
1204
+ // Total VCED sysex size (6 header + 155 data + 1 checksum + 1 end)
1205
+ static VCED_HEADER_SIZE = 6;
1206
+ static VCED_DATA_SIZE = 155;
1207
+ // Voice data bytes (6 operators × 21 bytes + 8 pitch EG + 11 global + 10 name)
1208
+ // VCED header bytes - DX7 single voice dump format
1209
+ static VCED_SYSEX_START = 240;
1210
+ // SysEx Message Start
1211
+ static VCED_YAMAHA_ID = 67;
1212
+ // Yamaha manufacturer ID
1213
+ static VCED_SUB_STATUS = 0;
1214
+ static VCED_FORMAT_SINGLE = 0;
1215
+ // Single voice format identifier
1216
+ static VCED_BYTE_COUNT_MSB = 1;
1217
+ // High byte of data length (1)
1218
+ static VCED_BYTE_COUNT_LSB = 27;
1219
+ // Low byte of data length (27 in decimal = 155 bytes)
1220
+ static VCED_SYSEX_END = 247;
1221
+ // SysEx Message End
1222
+ // Bit masks
1223
+ static MASK_7BIT = 127;
1224
+ // Standard 7-bit MIDI data mask
1225
+ static MASK_2BIT = 3;
1226
+ // For 2-bit values (curves)
1227
+ static MASK_3BIT = 7;
1228
+ // For 3-bit values (RS, detune)
1229
+ static MASK_4BIT = 15;
1230
+ // For 4-bit values (detune, fine freq)
1231
+ static MASK_5BIT = 31;
1232
+ // For 5-bit values (algorithm, freq coarse)
1233
+ static MASK_1BIT = 1;
1234
+ // For 1-bit values (mode, sync)
1235
+ // Parameter value ranges
1236
+ static TRANSPOSE_CENTER = 24;
1237
+ // MIDI note 24 = C0 (center of DX7 transpose range: -24 to +24 semitones)
1238
+ // Special character mappings - for Japanese DX7 character set compatibility
1239
+ static CHAR_YEN = 92;
1240
+ // Japanese Yen symbol (¥) maps to ASCII backslash
1241
+ static CHAR_ARROW_RIGHT = 126;
1242
+ // Right arrow (→) maps to ASCII tilde
1243
+ static CHAR_ARROW_LEFT = 127;
1244
+ // Left arrow (←) maps to ASCII DEL
1245
+ static CHAR_REPLACEMENT_Y = 89;
1246
+ // Replace Yen symbol with 'Y'
1247
+ static CHAR_REPLACEMENT_GT = 62;
1248
+ // Right arrow with '>'
1249
+ static CHAR_REPLACEMENT_LT = 60;
1250
+ // Left arrow with '<'
1251
+ static CHAR_SPACE = 32;
1252
+ // Standard space character
1253
+ static CHAR_MIN_PRINTABLE = 32;
1254
+ // Minimum ASCII printable character
1255
+ static CHAR_MAX_PRINTABLE = 126;
1256
+ // Maximum ASCII printable character
1257
+ // Default voice values
1258
+ static DEFAULT_EG_RATE = 99;
1259
+ static DEFAULT_EG_LEVEL_MAX = 99;
1260
+ static DEFAULT_EG_LEVEL_MIN = 0;
1261
+ static DEFAULT_BREAK_POINT = 60;
1262
+ // MIDI note 60 = C3
1263
+ static DEFAULT_OUTPUT_LEVEL = 99;
1264
+ static DEFAULT_PITCH_EG_LEVEL = 50;
1265
+ static DEFAULT_LFO_SPEED = 35;
1266
+ static DEFAULT_LFO_PM_SENS = 3;
1267
+ static DEFAULT_ALGORITHM = 0;
1268
+ static DEFAULT_FEEDBACK = 0;
1269
+ // MIDI notes
1270
+ static MIDI_OCTAVE_OFFSET = -2;
1271
+ // For displaying MIDI notes (MIDI 0 = C-2)
1272
+ static MIDI_BREAK_POINT_OFFSET = 21;
1273
+ // Offset for breakpoint display
1274
+ /**
1275
+ * Create a DX7Voice from raw 128-byte data
1276
+ * @param {Array<number>|Uint8Array} data - 128 bytes of voice data
1277
+ * @param {number} index - Voice index (0-31)
1278
+ * @throws {DX7ValidationError} If data length is not exactly 128 bytes
1279
+ */
1280
+ constructor(E, s = 0) {
1281
+ if (E.length !== t.PACKED_SIZE)
1282
+ throw new c(
1283
+ `Invalid voice data length: expected ${t.PACKED_SIZE} bytes, got ${E.length}`,
1284
+ "length",
1285
+ E.length
1286
+ );
1287
+ this.index = s, this.data = new Uint8Array(E), this.name = this._extractName();
1288
+ }
1289
+ /**
1290
+ * Extract the voice name from the data (10 characters at offset 118)
1291
+ * @private
1292
+ */
1293
+ _extractName() {
1294
+ const E = this.data.subarray(
1295
+ t.PACKED_NAME_START,
1296
+ t.PACKED_NAME_START + t.NAME_LENGTH
1297
+ );
1298
+ return Array.from(E).map((n) => {
1299
+ let _ = n & t.MASK_7BIT;
1300
+ return _ === t.CHAR_YEN && (_ = t.CHAR_REPLACEMENT_Y), _ === t.CHAR_ARROW_RIGHT && (_ = t.CHAR_REPLACEMENT_GT), _ === t.CHAR_ARROW_LEFT && (_ = t.CHAR_REPLACEMENT_LT), (_ < t.CHAR_MIN_PRINTABLE || _ > t.CHAR_MAX_PRINTABLE) && (_ = t.CHAR_SPACE), String.fromCharCode(_);
1301
+ }).join("").trim();
1302
+ }
1303
+ /**
1304
+ * Get a raw parameter value from the packed data
1305
+ * @param {number} offset - Byte offset in the voice data (0-127)
1306
+ * @returns {number} Parameter value (0-127)
1307
+ * @throws {DX7ValidationError} If offset is out of range
1308
+ */
1309
+ getParameter(E) {
1310
+ if (E < 0 || E >= t.PACKED_SIZE)
1311
+ throw new c(
1312
+ `Parameter offset out of range: ${E} (must be 0-${t.PACKED_SIZE - 1})`,
1313
+ "offset",
1314
+ E
1315
+ );
1316
+ return this.data[E] & t.MASK_7BIT;
1317
+ }
1318
+ /**
1319
+ * Get a parameter value from the unpacked 169-byte format
1320
+ * @param {number} offset - Byte offset in the unpacked data (0-168)
1321
+ * @returns {number} Parameter value (0-127)
1322
+ * @throws {DX7ValidationError} If offset is out of range
1323
+ */
1324
+ getUnpackedParameter(E) {
1325
+ if (E < 0 || E >= t.UNPACKED_SIZE)
1326
+ throw new c(
1327
+ `Unpacked parameter offset out of range: ${E} (must be 0-${t.UNPACKED_SIZE - 1})`,
1328
+ "offset",
1329
+ E
1330
+ );
1331
+ return this.unpack()[E] & t.MASK_7BIT;
1332
+ }
1333
+ /**
1334
+ * Set a raw parameter value in the packed data
1335
+ * @param {number} offset - Byte offset in the voice data
1336
+ * @param {number} value - Parameter value (0-127)
1337
+ */
1338
+ setParameter(E, s) {
1339
+ if (E < 0 || E >= t.PACKED_SIZE)
1340
+ throw new c(
1341
+ `Parameter offset out of range: ${E} (must be 0-${t.PACKED_SIZE - 1})`,
1342
+ "offset",
1343
+ E
1344
+ );
1345
+ this.data[E] = s & t.MASK_7BIT, E >= t.PACKED_NAME_START && E < t.PACKED_NAME_START + t.NAME_LENGTH && (this.name = this._extractName());
1346
+ }
1347
+ /**
1348
+ * Unpack the voice data to 169-byte unpacked format
1349
+ * This converts the packed 128-byte format to the full DX7 parameter set
1350
+ * @returns {Uint8Array} 169 bytes of unpacked voice data (138 operator + 8 pitch EG + 13 global + 10 name = 169 bytes)
1351
+ */
1352
+ unpack() {
1353
+ const E = this.data, s = new Uint8Array(t.UNPACKED_SIZE);
1354
+ for (let A = 0; A < t.NUM_OPERATORS; A++) {
1355
+ const e = (t.NUM_OPERATORS - 1 - A) * t.PACKED_OP_SIZE, r = A * t.UNPACKED_OP_SIZE;
1356
+ s[r + t.UNPACKED_OP_EG_RATE_1] = E[e + t.PACKED_OP_EG_RATE_1] & t.MASK_7BIT, s[r + t.UNPACKED_OP_EG_RATE_2] = E[e + t.PACKED_OP_EG_RATE_2] & t.MASK_7BIT, s[r + t.UNPACKED_OP_EG_RATE_3] = E[e + t.PACKED_OP_EG_RATE_3] & t.MASK_7BIT, s[r + t.UNPACKED_OP_EG_RATE_4] = E[e + t.PACKED_OP_EG_RATE_4] & t.MASK_7BIT, s[r + t.UNPACKED_OP_EG_LEVEL_1] = E[e + t.PACKED_OP_EG_LEVEL_1] & t.MASK_7BIT, s[r + t.UNPACKED_OP_EG_LEVEL_2] = E[e + t.PACKED_OP_EG_LEVEL_2] & t.MASK_7BIT, s[r + t.UNPACKED_OP_EG_LEVEL_3] = E[e + t.PACKED_OP_EG_LEVEL_3] & t.MASK_7BIT, s[r + t.UNPACKED_OP_EG_LEVEL_4] = E[e + t.PACKED_OP_EG_LEVEL_4] & t.MASK_7BIT, s[r + t.UNPACKED_OP_BREAK_POINT] = E[e + t.PACKED_OP_BREAK_POINT] & t.MASK_7BIT, s[r + t.UNPACKED_OP_L_SCALE_DEPTH] = E[e + t.PACKED_OP_L_SCALE_DEPTH] & t.MASK_7BIT, s[r + t.UNPACKED_OP_R_SCALE_DEPTH] = E[e + t.PACKED_OP_R_SCALE_DEPTH] & t.MASK_7BIT;
1357
+ const T = E[e + t.PACKED_OP_CURVES] & t.MASK_7BIT;
1358
+ s[r + t.UNPACKED_OP_L_CURVE] = T & t.MASK_2BIT, s[r + t.UNPACKED_OP_R_CURVE] = T >> 2 & t.MASK_2BIT;
1359
+ const u = E[e + t.PACKED_OP_RATE_SCALING] & t.MASK_7BIT;
1360
+ s[r + t.UNPACKED_OP_RATE_SCALING] = u & t.MASK_3BIT, s[r + t.UNPACKED_OP_DETUNE] = u >> 3 & t.MASK_4BIT;
1361
+ const i = E[e + t.PACKED_OP_MOD_SENS] & t.MASK_7BIT;
1362
+ s[r + t.UNPACKED_OP_AMP_MOD_SENS] = i & t.MASK_2BIT, s[r + t.UNPACKED_OP_KEY_VEL_SENS] = i >> 2 & t.MASK_3BIT, s[r + t.UNPACKED_OP_OUTPUT_LEVEL] = E[e + t.PACKED_OP_OUTPUT_LEVEL] & t.MASK_7BIT;
1363
+ const P = E[e + t.PACKED_OP_MODE_FREQ] & t.MASK_7BIT;
1364
+ s[r + t.UNPACKED_OP_MODE] = P & t.MASK_1BIT, s[r + t.UNPACKED_OP_FREQ_COARSE] = P >> 1 & t.MASK_5BIT;
1365
+ const l = E[e + t.PACKED_OP_DETUNE_FINE] & t.MASK_7BIT;
1366
+ s[r + t.UNPACKED_OP_OSC_DETUNE] = l & t.MASK_3BIT, s[r + t.UNPACKED_OP_FREQ_FINE] = l >> 3 & t.MASK_4BIT;
1367
+ }
1368
+ s[t.UNPACKED_PITCH_EG_RATE_1] = E[t.PACKED_PITCH_EG_RATE_1] & t.MASK_7BIT, s[t.UNPACKED_PITCH_EG_RATE_2] = E[t.PACKED_PITCH_EG_RATE_2] & t.MASK_7BIT, s[t.UNPACKED_PITCH_EG_RATE_3] = E[t.PACKED_PITCH_EG_RATE_3] & t.MASK_7BIT, s[t.UNPACKED_PITCH_EG_RATE_4] = E[t.PACKED_PITCH_EG_RATE_4] & t.MASK_7BIT, s[t.UNPACKED_PITCH_EG_LEVEL_1] = E[t.PACKED_PITCH_EG_LEVEL_1] & t.MASK_7BIT, s[t.UNPACKED_PITCH_EG_LEVEL_2] = E[t.PACKED_PITCH_EG_LEVEL_2] & t.MASK_7BIT, s[t.UNPACKED_PITCH_EG_LEVEL_3] = E[t.PACKED_PITCH_EG_LEVEL_3] & t.MASK_7BIT, s[t.UNPACKED_PITCH_EG_LEVEL_4] = E[t.PACKED_PITCH_EG_LEVEL_4] & t.MASK_7BIT, s[t.UNPACKED_ALGORITHM] = E[t.OFFSET_ALGORITHM] & t.MASK_5BIT;
1369
+ const n = E[t.OFFSET_FEEDBACK] & t.MASK_7BIT;
1370
+ s[t.UNPACKED_FEEDBACK] = n & t.MASK_3BIT, s[t.UNPACKED_OSC_SYNC] = n >> 3 & t.MASK_1BIT, s[t.UNPACKED_LFO_SPEED] = E[t.OFFSET_LFO_SPEED] & t.MASK_7BIT, s[t.UNPACKED_LFO_DELAY] = E[t.OFFSET_LFO_DELAY] & t.MASK_7BIT, s[t.UNPACKED_LFO_PM_DEPTH] = E[t.OFFSET_LFO_PM_DEPTH] & t.MASK_7BIT, s[t.UNPACKED_LFO_AM_DEPTH] = E[t.OFFSET_LFO_AM_DEPTH] & t.MASK_7BIT;
1371
+ const _ = E[t.OFFSET_LFO_SYNC_WAVE] & t.MASK_7BIT;
1372
+ s[t.UNPACKED_LFO_KEY_SYNC] = _ & t.MASK_1BIT, s[t.UNPACKED_LFO_WAVE] = _ >> 1 & t.MASK_3BIT, s[t.UNPACKED_LFO_PM_SENS] = _ >> 4 & t.MASK_3BIT, s[t.UNPACKED_AMP_MOD_SENS] = E[t.OFFSET_AMP_MOD_SENS] & t.MASK_7BIT, s[t.UNPACKED_TRANSPOSE] = E[t.OFFSET_TRANSPOSE] & t.MASK_7BIT, s[t.UNPACKED_EG_BIAS_SENS] = E[t.OFFSET_EG_BIAS_SENS] & t.MASK_7BIT;
1373
+ for (let A = 0; A < t.NAME_LENGTH; A++)
1374
+ s[t.UNPACKED_NAME_START + A] = E[t.PACKED_NAME_START + A] & t.MASK_7BIT;
1375
+ return s;
1376
+ }
1377
+ /**
1378
+ * Pack 169-byte unpacked data to 128-byte format
1379
+ * @param {Array<number>|Uint8Array} unpacked - 169 bytes of unpacked data (159 parameters + 10 name bytes)
1380
+ * @returns {Uint8Array} 128 bytes of packed data
1381
+ */
1382
+ static pack(E) {
1383
+ if (E.length !== t.UNPACKED_SIZE)
1384
+ throw new c(
1385
+ `Invalid unpacked data length: expected ${t.UNPACKED_SIZE} bytes, got ${E.length}`,
1386
+ "length",
1387
+ E.length
1388
+ );
1389
+ const s = new Uint8Array(t.PACKED_SIZE);
1390
+ for (let T = 0; T < t.NUM_OPERATORS; T++) {
1391
+ const u = T * t.UNPACKED_OP_SIZE, i = (t.NUM_OPERATORS - 1 - T) * t.PACKED_OP_SIZE;
1392
+ s[i + t.PACKED_OP_EG_RATE_1] = E[u + t.UNPACKED_OP_EG_RATE_1], s[i + t.PACKED_OP_EG_RATE_2] = E[u + t.UNPACKED_OP_EG_RATE_2], s[i + t.PACKED_OP_EG_RATE_3] = E[u + t.UNPACKED_OP_EG_RATE_3], s[i + t.PACKED_OP_EG_RATE_4] = E[u + t.UNPACKED_OP_EG_RATE_4], s[i + t.PACKED_OP_EG_LEVEL_1] = E[u + t.UNPACKED_OP_EG_LEVEL_1], s[i + t.PACKED_OP_EG_LEVEL_2] = E[u + t.UNPACKED_OP_EG_LEVEL_2], s[i + t.PACKED_OP_EG_LEVEL_3] = E[u + t.UNPACKED_OP_EG_LEVEL_3], s[i + t.PACKED_OP_EG_LEVEL_4] = E[u + t.UNPACKED_OP_EG_LEVEL_4], s[i + t.PACKED_OP_BREAK_POINT] = E[u + t.UNPACKED_OP_BREAK_POINT], s[i + t.PACKED_OP_L_SCALE_DEPTH] = E[u + t.UNPACKED_OP_L_SCALE_DEPTH], s[i + t.PACKED_OP_R_SCALE_DEPTH] = E[u + t.UNPACKED_OP_R_SCALE_DEPTH];
1393
+ const P = E[u + t.UNPACKED_OP_L_CURVE] & t.MASK_2BIT, l = E[u + t.UNPACKED_OP_R_CURVE] & t.MASK_2BIT;
1394
+ s[i + t.PACKED_OP_CURVES] = P | l << 2;
1395
+ const S = E[u + t.UNPACKED_OP_RATE_SCALING] & t.MASK_3BIT, N = E[u + t.UNPACKED_OP_DETUNE] & t.MASK_4BIT;
1396
+ s[i + t.PACKED_OP_RATE_SCALING] = S | N << 3;
1397
+ const h = E[u + t.UNPACKED_OP_AMP_MOD_SENS] & t.MASK_2BIT, U = E[u + t.UNPACKED_OP_KEY_VEL_SENS] & t.MASK_3BIT;
1398
+ s[i + t.PACKED_OP_MOD_SENS] = h | U << 2, s[i + t.PACKED_OP_OUTPUT_LEVEL] = E[u + t.UNPACKED_OP_OUTPUT_LEVEL];
1399
+ const I = E[u + t.UNPACKED_OP_MODE] & t.MASK_1BIT, K = E[u + t.UNPACKED_OP_FREQ_COARSE] & t.MASK_5BIT;
1400
+ s[i + t.PACKED_OP_MODE_FREQ] = I | K << 1;
1401
+ const L = E[u + t.UNPACKED_OP_OSC_DETUNE] & t.MASK_3BIT, R = E[u + t.UNPACKED_OP_FREQ_FINE] & t.MASK_4BIT;
1402
+ s[i + t.PACKED_OP_DETUNE_FINE] = L | R << 3;
1403
+ }
1404
+ s[t.PACKED_PITCH_EG_RATE_1] = E[t.UNPACKED_PITCH_EG_RATE_1], s[t.PACKED_PITCH_EG_RATE_2] = E[t.UNPACKED_PITCH_EG_RATE_2], s[t.PACKED_PITCH_EG_RATE_3] = E[t.UNPACKED_PITCH_EG_RATE_3], s[t.PACKED_PITCH_EG_RATE_4] = E[t.UNPACKED_PITCH_EG_RATE_4], s[t.PACKED_PITCH_EG_LEVEL_1] = E[t.UNPACKED_PITCH_EG_LEVEL_1], s[t.PACKED_PITCH_EG_LEVEL_2] = E[t.UNPACKED_PITCH_EG_LEVEL_2], s[t.PACKED_PITCH_EG_LEVEL_3] = E[t.UNPACKED_PITCH_EG_LEVEL_3], s[t.PACKED_PITCH_EG_LEVEL_4] = E[t.UNPACKED_PITCH_EG_LEVEL_4], s[t.OFFSET_ALGORITHM] = E[t.UNPACKED_ALGORITHM];
1405
+ const n = E[t.UNPACKED_FEEDBACK] & t.MASK_3BIT, _ = E[t.UNPACKED_OSC_SYNC] & t.MASK_1BIT;
1406
+ s[t.OFFSET_FEEDBACK] = n | _ << 3, s[t.OFFSET_LFO_SPEED] = E[t.UNPACKED_LFO_SPEED], s[t.OFFSET_LFO_DELAY] = E[t.UNPACKED_LFO_DELAY], s[t.OFFSET_LFO_PM_DEPTH] = E[t.UNPACKED_LFO_PM_DEPTH], s[t.OFFSET_LFO_AM_DEPTH] = E[t.UNPACKED_LFO_AM_DEPTH];
1407
+ const A = E[t.UNPACKED_LFO_KEY_SYNC] & t.MASK_1BIT, e = E[t.UNPACKED_LFO_WAVE] & t.MASK_3BIT, r = E[t.UNPACKED_LFO_PM_SENS] & t.MASK_3BIT;
1408
+ s[t.OFFSET_LFO_SYNC_WAVE] = A | e << 1 | r << 4, s[t.OFFSET_AMP_MOD_SENS] = E[t.UNPACKED_AMP_MOD_SENS], s[t.OFFSET_TRANSPOSE] = E[t.UNPACKED_TRANSPOSE], s[t.OFFSET_EG_BIAS_SENS] = E[t.UNPACKED_EG_BIAS_SENS];
1409
+ for (let T = 0; T < t.NAME_LENGTH; T++)
1410
+ s[t.PACKED_NAME_START + T] = E[t.UNPACKED_NAME_START + T];
1411
+ return s;
1412
+ }
1413
+ /**
1414
+ * Create a default/empty voice
1415
+ * @param {number} index - Voice index
1416
+ * @returns {DX7Voice}
1417
+ */
1418
+ static createDefault(E = 0) {
1419
+ const s = new Uint8Array(t.UNPACKED_SIZE);
1420
+ for (let A = 0; A < t.NUM_OPERATORS; A++) {
1421
+ const e = A * t.UNPACKED_OP_SIZE;
1422
+ s[e + t.UNPACKED_OP_EG_RATE_1] = t.DEFAULT_EG_RATE, s[e + t.UNPACKED_OP_EG_RATE_2] = t.DEFAULT_EG_RATE, s[e + t.UNPACKED_OP_EG_RATE_3] = t.DEFAULT_EG_RATE, s[e + t.UNPACKED_OP_EG_RATE_4] = t.DEFAULT_EG_RATE, s[e + t.UNPACKED_OP_EG_LEVEL_1] = t.DEFAULT_EG_LEVEL_MAX, s[e + t.UNPACKED_OP_EG_LEVEL_2] = t.DEFAULT_EG_LEVEL_MAX, s[e + t.UNPACKED_OP_EG_LEVEL_3] = t.DEFAULT_EG_LEVEL_MAX, s[e + t.UNPACKED_OP_EG_LEVEL_4] = t.DEFAULT_EG_LEVEL_MIN, s[e + t.UNPACKED_OP_BREAK_POINT] = t.DEFAULT_BREAK_POINT, s[e + t.UNPACKED_OP_L_SCALE_DEPTH] = 0, s[e + t.UNPACKED_OP_R_SCALE_DEPTH] = 0, s[e + t.UNPACKED_OP_L_CURVE] = 0, s[e + t.UNPACKED_OP_R_CURVE] = 0, s[e + t.UNPACKED_OP_RATE_SCALING] = 0, s[e + t.UNPACKED_OP_AMP_MOD_SENS] = 0, s[e + t.UNPACKED_OP_KEY_VEL_SENS] = 0, s[e + t.UNPACKED_OP_OUTPUT_LEVEL] = t.DEFAULT_OUTPUT_LEVEL, s[e + t.UNPACKED_OP_MODE] = 0, s[e + t.UNPACKED_OP_FREQ_COARSE] = 0, s[e + t.UNPACKED_OP_OSC_DETUNE] = 0, s[e + t.UNPACKED_OP_FREQ_FINE] = 0;
1423
+ }
1424
+ s[t.UNPACKED_PITCH_EG_RATE_1] = t.DEFAULT_EG_RATE, s[t.UNPACKED_PITCH_EG_RATE_2] = t.DEFAULT_EG_RATE, s[t.UNPACKED_PITCH_EG_RATE_3] = t.DEFAULT_EG_RATE, s[t.UNPACKED_PITCH_EG_RATE_4] = t.DEFAULT_EG_RATE, s[t.UNPACKED_PITCH_EG_LEVEL_1] = t.DEFAULT_PITCH_EG_LEVEL, s[t.UNPACKED_PITCH_EG_LEVEL_2] = t.DEFAULT_PITCH_EG_LEVEL, s[t.UNPACKED_PITCH_EG_LEVEL_3] = t.DEFAULT_PITCH_EG_LEVEL, s[t.UNPACKED_PITCH_EG_LEVEL_4] = t.DEFAULT_PITCH_EG_LEVEL, s[t.UNPACKED_ALGORITHM] = t.DEFAULT_ALGORITHM, s[t.UNPACKED_FEEDBACK] = t.DEFAULT_FEEDBACK, s[t.UNPACKED_OSC_SYNC] = 0, s[t.UNPACKED_LFO_SPEED] = t.DEFAULT_LFO_SPEED, s[t.UNPACKED_LFO_DELAY] = 0, s[t.UNPACKED_LFO_PM_DEPTH] = 0, s[t.UNPACKED_LFO_AM_DEPTH] = 0, s[t.UNPACKED_LFO_KEY_SYNC] = 0, s[t.UNPACKED_LFO_WAVE] = 0, s[t.UNPACKED_LFO_PM_SENS] = t.DEFAULT_LFO_PM_SENS, s[t.UNPACKED_AMP_MOD_SENS] = 0, s[t.UNPACKED_TRANSPOSE] = t.TRANSPOSE_CENTER, s[t.UNPACKED_EG_BIAS_SENS] = 0;
1425
+ const n = "Init Voice";
1426
+ for (let A = 0; A < t.NAME_LENGTH; A++)
1427
+ s[t.UNPACKED_NAME_START + A] = A < n.length ? n.charCodeAt(A) : t.CHAR_SPACE;
1428
+ const _ = t.pack(s);
1429
+ return new t(_, E);
1430
+ }
1431
+ /**
1432
+ * Create a voice from unpacked 169-byte data
1433
+ * @param {Array<number>|Uint8Array} unpacked - 169 bytes of unpacked data (159 parameters + 10 name bytes)
1434
+ * @param {number} index - Voice index
1435
+ * @returns {DX7Voice}
1436
+ */
1437
+ static fromUnpacked(E, s = 0) {
1438
+ const n = t.pack(E);
1439
+ return new t(n, s);
1440
+ }
1441
+ /**
1442
+ * Load a DX7 voice from a single voice SYX file
1443
+ * @param {File|Blob} file - SYX file (single voice in VCED format)
1444
+ * @returns {Promise<DX7Voice>}
1445
+ * @throws {DX7ParseError} If file has invalid VCED header
1446
+ * @throws {Error} If file cannot be read (FileReader error)
1447
+ */
1448
+ static async fromFile(E) {
1449
+ return new Promise((s, n) => {
1450
+ const _ = new FileReader();
1451
+ _.onload = (A) => {
1452
+ try {
1453
+ const e = new Uint8Array(A.target.result);
1454
+ if (e[0] !== t.VCED_SYSEX_START || e[1] !== t.VCED_YAMAHA_ID || e[2] !== t.VCED_SUB_STATUS || e[3] !== t.VCED_FORMAT_SINGLE || e[4] !== t.VCED_BYTE_COUNT_MSB || e[5] !== t.VCED_BYTE_COUNT_LSB)
1455
+ throw new D("Invalid VCED header", "header", 0);
1456
+ const r = e.subarray(
1457
+ t.VCED_HEADER_SIZE,
1458
+ t.VCED_HEADER_SIZE + t.VCED_DATA_SIZE
1459
+ ), T = e[t.VCED_HEADER_SIZE + t.VCED_DATA_SIZE], u = C._calculateChecksum(r, t.VCED_DATA_SIZE);
1460
+ T !== u && console.warn(
1461
+ `DX7 VCED checksum mismatch (expected ${u.toString(16)}, got ${T.toString(16)}). This is common with vintage SysEx files.`
1462
+ );
1463
+ const i = new Uint8Array(t.UNPACKED_SIZE);
1464
+ let P = 0;
1465
+ for (let S = 0; S < t.NUM_OPERATORS; S++) {
1466
+ const N = (t.NUM_OPERATORS - 1 - S) * t.UNPACKED_OP_SIZE;
1467
+ i[N + t.UNPACKED_OP_EG_RATE_1] = r[P++], i[N + t.UNPACKED_OP_EG_RATE_2] = r[P++], i[N + t.UNPACKED_OP_EG_RATE_3] = r[P++], i[N + t.UNPACKED_OP_EG_RATE_4] = r[P++], i[N + t.UNPACKED_OP_EG_LEVEL_1] = r[P++], i[N + t.UNPACKED_OP_EG_LEVEL_2] = r[P++], i[N + t.UNPACKED_OP_EG_LEVEL_3] = r[P++], i[N + t.UNPACKED_OP_EG_LEVEL_4] = r[P++], i[N + t.UNPACKED_OP_BREAK_POINT] = r[P++], i[N + t.UNPACKED_OP_L_SCALE_DEPTH] = r[P++], i[N + t.UNPACKED_OP_R_SCALE_DEPTH] = r[P++], i[N + t.UNPACKED_OP_L_CURVE] = r[P++], i[N + t.UNPACKED_OP_R_CURVE] = r[P++], i[N + t.UNPACKED_OP_RATE_SCALING] = r[P++], i[N + t.UNPACKED_OP_DETUNE] = r[P++];
1468
+ const h = r[P++];
1469
+ i[N + t.UNPACKED_OP_AMP_MOD_SENS] = h & t.MASK_2BIT, i[N + t.UNPACKED_OP_KEY_VEL_SENS] = h >> 2 & t.MASK_3BIT, i[N + t.UNPACKED_OP_OUTPUT_LEVEL] = r[P++], i[N + t.UNPACKED_OP_MODE] = r[P++], i[N + t.UNPACKED_OP_FREQ_COARSE] = r[P++], i[N + t.UNPACKED_OP_FREQ_FINE] = r[P++], i[N + t.UNPACKED_OP_OSC_DETUNE] = r[P++];
1470
+ }
1471
+ i[t.UNPACKED_PITCH_EG_RATE_1] = r[P++], i[t.UNPACKED_PITCH_EG_RATE_2] = r[P++], i[t.UNPACKED_PITCH_EG_RATE_3] = r[P++], i[t.UNPACKED_PITCH_EG_RATE_4] = r[P++], i[t.UNPACKED_PITCH_EG_LEVEL_1] = r[P++], i[t.UNPACKED_PITCH_EG_LEVEL_2] = r[P++], i[t.UNPACKED_PITCH_EG_LEVEL_3] = r[P++], i[t.UNPACKED_PITCH_EG_LEVEL_4] = r[P++], i[t.UNPACKED_ALGORITHM] = r[P++], i[t.UNPACKED_FEEDBACK] = r[P++], i[t.UNPACKED_OSC_SYNC] = r[P++], i[t.UNPACKED_LFO_SPEED] = r[P++], i[t.UNPACKED_LFO_DELAY] = r[P++], i[t.UNPACKED_LFO_PM_DEPTH] = r[P++], i[t.UNPACKED_LFO_AM_DEPTH] = r[P++], i[t.UNPACKED_LFO_KEY_SYNC] = r[P++], i[t.UNPACKED_LFO_WAVE] = r[P++], i[t.UNPACKED_LFO_PM_SENS] = r[P++], i[t.UNPACKED_TRANSPOSE] = r[P++];
1472
+ for (let S = 0; S < t.NAME_LENGTH; S++)
1473
+ i[t.UNPACKED_NAME_START + S] = r[P++];
1474
+ const l = t.pack(i);
1475
+ s(new t(l, 0));
1476
+ } catch (e) {
1477
+ n(e);
1478
+ }
1479
+ }, _.onerror = () => n(new Error("Failed to read file")), _.readAsArrayBuffer(E);
1480
+ });
1481
+ }
1482
+ /**
1483
+ * Export voice to DX7 single voice SysEx format (VCED format)
1484
+ * This is useful for synths that only support single voice dumps (e.g., KORG Volca FM)
1485
+ * Converts from 169-byte unpacked format to 155-byte VCED format
1486
+ * @returns {Uint8Array} Single voice SysEx data (163 bytes)
1487
+ */
1488
+ toSysEx() {
1489
+ const E = this.unpack(), s = new Uint8Array(t.VCED_SIZE);
1490
+ let n = 0;
1491
+ s[n++] = t.VCED_SYSEX_START, s[n++] = t.VCED_YAMAHA_ID, s[n++] = t.VCED_SUB_STATUS, s[n++] = t.VCED_FORMAT_SINGLE, s[n++] = t.VCED_BYTE_COUNT_MSB, s[n++] = t.VCED_BYTE_COUNT_LSB;
1492
+ for (let A = t.NUM_OPERATORS - 1; A >= 0; A--) {
1493
+ const e = A * t.UNPACKED_OP_SIZE;
1494
+ s[n++] = E[e + t.UNPACKED_OP_EG_RATE_1], s[n++] = E[e + t.UNPACKED_OP_EG_RATE_2], s[n++] = E[e + t.UNPACKED_OP_EG_RATE_3], s[n++] = E[e + t.UNPACKED_OP_EG_RATE_4], s[n++] = E[e + t.UNPACKED_OP_EG_LEVEL_1], s[n++] = E[e + t.UNPACKED_OP_EG_LEVEL_2], s[n++] = E[e + t.UNPACKED_OP_EG_LEVEL_3], s[n++] = E[e + t.UNPACKED_OP_EG_LEVEL_4], s[n++] = E[e + t.UNPACKED_OP_BREAK_POINT], s[n++] = E[e + t.UNPACKED_OP_L_SCALE_DEPTH], s[n++] = E[e + t.UNPACKED_OP_R_SCALE_DEPTH], s[n++] = E[e + t.UNPACKED_OP_L_CURVE], s[n++] = E[e + t.UNPACKED_OP_R_CURVE], s[n++] = E[e + t.UNPACKED_OP_RATE_SCALING], s[n++] = E[e + t.UNPACKED_OP_DETUNE];
1495
+ const r = E[e + t.UNPACKED_OP_AMP_MOD_SENS] & t.MASK_2BIT, T = E[e + t.UNPACKED_OP_KEY_VEL_SENS] & t.MASK_3BIT;
1496
+ s[n++] = r | T << 2, s[n++] = E[e + t.UNPACKED_OP_OUTPUT_LEVEL], s[n++] = E[e + t.UNPACKED_OP_MODE], s[n++] = E[e + t.UNPACKED_OP_FREQ_COARSE], s[n++] = E[e + t.UNPACKED_OP_OSC_DETUNE], s[n++] = E[e + t.UNPACKED_OP_FREQ_FINE];
1497
+ }
1498
+ s[n++] = E[t.UNPACKED_PITCH_EG_RATE_1], s[n++] = E[t.UNPACKED_PITCH_EG_RATE_2], s[n++] = E[t.UNPACKED_PITCH_EG_RATE_3], s[n++] = E[t.UNPACKED_PITCH_EG_RATE_4], s[n++] = E[t.UNPACKED_PITCH_EG_LEVEL_1], s[n++] = E[t.UNPACKED_PITCH_EG_LEVEL_2], s[n++] = E[t.UNPACKED_PITCH_EG_LEVEL_3], s[n++] = E[t.UNPACKED_PITCH_EG_LEVEL_4], s[n++] = E[t.UNPACKED_ALGORITHM], s[n++] = E[t.UNPACKED_FEEDBACK], s[n++] = E[t.UNPACKED_OSC_SYNC], s[n++] = E[t.UNPACKED_LFO_SPEED], s[n++] = E[t.UNPACKED_LFO_DELAY], s[n++] = E[t.UNPACKED_LFO_PM_DEPTH], s[n++] = E[t.UNPACKED_LFO_AM_DEPTH], s[n++] = E[t.UNPACKED_LFO_KEY_SYNC], s[n++] = E[t.UNPACKED_LFO_WAVE], s[n++] = E[t.UNPACKED_LFO_PM_SENS], s[n++] = E[t.UNPACKED_TRANSPOSE];
1499
+ for (let A = 0; A < t.NAME_LENGTH; A++)
1500
+ s[n++] = E[t.UNPACKED_NAME_START + A];
1501
+ const _ = s.subarray(
1502
+ t.VCED_HEADER_SIZE,
1503
+ t.VCED_HEADER_SIZE + t.VCED_DATA_SIZE
1504
+ );
1505
+ return s[n++] = C._calculateChecksum(_, t.VCED_DATA_SIZE), s[n++] = t.VCED_SYSEX_END, s;
1506
+ }
1507
+ /**
1508
+ * Convert voice to JSON format
1509
+ * @returns {object} Voice data in JSON format
1510
+ */
1511
+ toJSON() {
1512
+ const E = this.unpack(), s = [], n = (e) => ["-LN", "-EX", "+EX", "+LN"][e] || "UNKNOWN", _ = (e) => ["TRIANGLE", "SAW DOWN", "SAW UP", "SQUARE", "SINE", "SAMPLE & HOLD"][e] || "UNKNOWN", A = (e) => {
1513
+ const r = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"], T = Math.floor(e / 12) + t.MIDI_OCTAVE_OFFSET;
1514
+ return `${r[e % 12]}${T}`;
1515
+ };
1516
+ for (let e = 0; e < t.NUM_OPERATORS; e++) {
1517
+ const r = e * t.UNPACKED_OP_SIZE, T = E[r + t.UNPACKED_OP_MODE] === 0 ? "RATIO" : "FIXED";
1518
+ s.push({
1519
+ id: e + 1,
1520
+ osc: {
1521
+ detune: E[r + t.UNPACKED_OP_OSC_DETUNE],
1522
+ freq: {
1523
+ coarse: E[r + t.UNPACKED_OP_FREQ_COARSE],
1524
+ fine: E[r + t.UNPACKED_OP_FREQ_FINE],
1525
+ mode: T
1526
+ }
1527
+ },
1528
+ eg: {
1529
+ rates: [
1530
+ E[r + t.UNPACKED_OP_EG_RATE_1],
1531
+ E[r + t.UNPACKED_OP_EG_RATE_2],
1532
+ E[r + t.UNPACKED_OP_EG_RATE_3],
1533
+ E[r + t.UNPACKED_OP_EG_RATE_4]
1534
+ ],
1535
+ levels: [
1536
+ E[r + t.UNPACKED_OP_EG_LEVEL_1],
1537
+ E[r + t.UNPACKED_OP_EG_LEVEL_2],
1538
+ E[r + t.UNPACKED_OP_EG_LEVEL_3],
1539
+ E[r + t.UNPACKED_OP_EG_LEVEL_4]
1540
+ ]
1541
+ },
1542
+ key: {
1543
+ velocity: E[r + t.UNPACKED_OP_KEY_VEL_SENS],
1544
+ scaling: E[r + t.UNPACKED_OP_RATE_SCALING],
1545
+ breakPoint: A(
1546
+ E[r + t.UNPACKED_OP_BREAK_POINT] + t.MIDI_BREAK_POINT_OFFSET
1547
+ )
1548
+ },
1549
+ output: {
1550
+ level: E[r + t.UNPACKED_OP_OUTPUT_LEVEL],
1551
+ ampModSens: E[r + t.UNPACKED_OP_AMP_MOD_SENS]
1552
+ },
1553
+ scale: {
1554
+ left: {
1555
+ depth: E[r + t.UNPACKED_OP_L_SCALE_DEPTH],
1556
+ curve: n(E[r + t.UNPACKED_OP_L_CURVE])
1557
+ },
1558
+ right: {
1559
+ depth: E[r + t.UNPACKED_OP_R_SCALE_DEPTH],
1560
+ curve: n(E[r + t.UNPACKED_OP_R_CURVE])
1561
+ }
1562
+ }
1563
+ });
1564
+ }
1565
+ return {
1566
+ name: this.name || "(Empty)",
1567
+ operators: s,
1568
+ pitchEG: {
1569
+ rates: [
1570
+ E[t.UNPACKED_PITCH_EG_RATE_1],
1571
+ E[t.UNPACKED_PITCH_EG_RATE_2],
1572
+ E[t.UNPACKED_PITCH_EG_RATE_3],
1573
+ E[t.UNPACKED_PITCH_EG_RATE_4]
1574
+ ],
1575
+ levels: [
1576
+ E[t.UNPACKED_PITCH_EG_LEVEL_1],
1577
+ E[t.UNPACKED_PITCH_EG_LEVEL_2],
1578
+ E[t.UNPACKED_PITCH_EG_LEVEL_3],
1579
+ E[t.UNPACKED_PITCH_EG_LEVEL_4]
1580
+ ]
1581
+ },
1582
+ lfo: {
1583
+ speed: E[t.UNPACKED_LFO_SPEED],
1584
+ delay: E[t.UNPACKED_LFO_DELAY],
1585
+ pmDepth: E[t.UNPACKED_LFO_PM_DEPTH],
1586
+ amDepth: E[t.UNPACKED_LFO_AM_DEPTH],
1587
+ keySync: E[t.UNPACKED_LFO_KEY_SYNC] === 1,
1588
+ wave: _(E[t.UNPACKED_LFO_WAVE])
1589
+ },
1590
+ global: {
1591
+ algorithm: E[t.UNPACKED_ALGORITHM] + 1,
1592
+ feedback: E[t.UNPACKED_FEEDBACK],
1593
+ oscKeySync: E[t.UNPACKED_OSC_SYNC] === 1,
1594
+ pitchModSens: E[t.UNPACKED_LFO_PM_SENS],
1595
+ transpose: E[t.UNPACKED_TRANSPOSE] - t.TRANSPOSE_CENTER
1596
+ }
1597
+ };
1598
+ }
1599
+ }
1600
+ class C {
1601
+ // SysEx header
1602
+ static SYSEX_START = 240;
1603
+ static SYSEX_END = 247;
1604
+ static SYSEX_YAMAHA_ID = 67;
1605
+ static SYSEX_SUB_STATUS = 0;
1606
+ static SYSEX_FORMAT_32_VOICES = 9;
1607
+ static SYSEX_BYTE_COUNT_MSB = 32;
1608
+ static SYSEX_BYTE_COUNT_LSB = 0;
1609
+ static SYSEX_HEADER = [
1610
+ C.SYSEX_START,
1611
+ C.SYSEX_YAMAHA_ID,
1612
+ C.SYSEX_SUB_STATUS,
1613
+ C.SYSEX_FORMAT_32_VOICES,
1614
+ C.SYSEX_BYTE_COUNT_MSB,
1615
+ C.SYSEX_BYTE_COUNT_LSB
1616
+ ];
1617
+ static SYSEX_HEADER_SIZE = 6;
1618
+ // Bank structure
1619
+ static VOICE_DATA_SIZE = 4096;
1620
+ // 32 voices × 128 bytes
1621
+ static SYSEX_SIZE = 4104;
1622
+ // Header(6) + Data(4096) + Checksum(1) + End(1)
1623
+ static VOICE_SIZE = 128;
1624
+ // Bytes per voice in packed format
1625
+ static NUM_VOICES = 32;
1626
+ // Checksum
1627
+ static CHECKSUM_MODULO = 128;
1628
+ static MASK_7BIT = 127;
1629
+ /**
1630
+ * Create a DX7Bank
1631
+ * @param {Array<number>|ArrayBuffer|Uint8Array} data - Bank SYX data (optional)
1632
+ * @param {string} name - Optional bank name (e.g., filename)
1633
+ */
1634
+ constructor(E, s = "") {
1635
+ if (this.voices = new Array(C.NUM_VOICES), this.name = s, E)
1636
+ this._load(E);
1637
+ else
1638
+ for (let n = 0; n < C.NUM_VOICES; n++)
1639
+ this.voices[n] = t.createDefault(n);
1640
+ }
1641
+ /**
1642
+ * Calculate DX7 SysEx checksum
1643
+ * @private
1644
+ * @param {Uint8Array} data - Data to checksum
1645
+ * @param {number} size - Number of bytes
1646
+ * @returns {number} Checksum byte
1647
+ */
1648
+ static _calculateChecksum(E, s) {
1649
+ let n = 0;
1650
+ for (let _ = 0; _ < s; _++)
1651
+ n += E[_];
1652
+ return C.CHECKSUM_MODULO - n % C.CHECKSUM_MODULO & C.MASK_7BIT;
1653
+ }
1654
+ /**
1655
+ * Load and validate bank data
1656
+ * @private
1657
+ * @param {Array<number>|ArrayBuffer|Uint8Array} data
1658
+ */
1659
+ _load(E) {
1660
+ const s = E instanceof Uint8Array ? E : new Uint8Array(E);
1661
+ let n, _ = 0;
1662
+ if (s[0] === C.SYSEX_START) {
1663
+ const e = s.subarray(0, C.SYSEX_HEADER_SIZE), r = C.SYSEX_HEADER;
1664
+ for (let T = 0; T < C.SYSEX_HEADER_SIZE; T++)
1665
+ if (e[T] !== r[T])
1666
+ throw new D(
1667
+ `Invalid SysEx header at position ${T}: expected ${r[T].toString(16)}, got ${e[T].toString(16)}`,
1668
+ "header",
1669
+ T
1670
+ );
1671
+ n = s.subarray(
1672
+ C.SYSEX_HEADER_SIZE,
1673
+ C.SYSEX_HEADER_SIZE + C.VOICE_DATA_SIZE
1674
+ ), _ = C.SYSEX_HEADER_SIZE;
1675
+ } else if (s.length === C.VOICE_DATA_SIZE)
1676
+ n = s;
1677
+ else
1678
+ throw new c(
1679
+ `Invalid data length: expected ${C.VOICE_DATA_SIZE} or ${C.SYSEX_SIZE} bytes, got ${s.length}`,
1680
+ "length",
1681
+ s.length
1682
+ );
1683
+ if (n.length !== C.VOICE_DATA_SIZE)
1684
+ throw new c(
1685
+ `Invalid voice data length: expected ${C.VOICE_DATA_SIZE} bytes, got ${n.length}`,
1686
+ "length",
1687
+ n.length
1688
+ );
1689
+ const A = C.SYSEX_HEADER_SIZE + C.VOICE_DATA_SIZE;
1690
+ if (_ > 0 && s.length >= A + 1) {
1691
+ const e = s[A], r = C._calculateChecksum(n, C.VOICE_DATA_SIZE);
1692
+ e !== r && console.warn(
1693
+ `DX7 checksum mismatch (expected ${r.toString(16)}, got ${e.toString(16)}). This is common with vintage SysEx files and the data is likely still valid.`
1694
+ );
1695
+ }
1696
+ this.voices = new Array(C.NUM_VOICES);
1697
+ for (let e = 0; e < C.NUM_VOICES; e++) {
1698
+ const r = e * C.VOICE_SIZE, T = n.subarray(r, r + C.VOICE_SIZE);
1699
+ this.voices[e] = new t(T, e);
1700
+ }
1701
+ }
1702
+ /**
1703
+ * Replace a voice at the specified index
1704
+ * @param {number} index - Voice index (0-31)
1705
+ * @param {DX7Voice} voice - Voice to insert
1706
+ * @throws {DX7ValidationError} If index is out of range
1707
+ */
1708
+ replaceVoice(E, s) {
1709
+ if (E < 0 || E >= C.NUM_VOICES)
1710
+ throw new c(`Invalid voice index: ${E}`, "index", E);
1711
+ const n = new Uint8Array(s.data);
1712
+ this.voices[E] = new t(n, E);
1713
+ }
1714
+ /**
1715
+ * Add a voice to the first empty slot
1716
+ * @param {DX7Voice} voice - Voice to add
1717
+ * @returns {number} Index where voice was added, or -1 if bank is full
1718
+ */
1719
+ addVoice(E) {
1720
+ for (let s = 0; s < this.voices.length; s++) {
1721
+ const n = this.voices[s];
1722
+ if (n.name === "" || n.name === "Init Voice")
1723
+ return this.replaceVoice(s, E), s;
1724
+ }
1725
+ return -1;
1726
+ }
1727
+ /**
1728
+ * Get all voices in the bank
1729
+ * @returns {DX7Voice[]}
1730
+ */
1731
+ getVoices() {
1732
+ return this.voices;
1733
+ }
1734
+ /**
1735
+ * Get a specific voice by index
1736
+ * @param {number} index - Voice index (0-31)
1737
+ * @returns {DX7Voice|null}
1738
+ */
1739
+ getVoice(E) {
1740
+ return E < 0 || E >= this.voices.length ? null : this.voices[E];
1741
+ }
1742
+ /**
1743
+ * Get all voice names
1744
+ * @returns {string[]}
1745
+ */
1746
+ getVoiceNames() {
1747
+ return this.voices.map((E) => E.name);
1748
+ }
1749
+ /**
1750
+ * Find a voice by name (case-insensitive, partial match)
1751
+ * @param {string} name - Voice name to search for
1752
+ * @returns {DX7Voice|null}
1753
+ */
1754
+ findVoiceByName(E) {
1755
+ const s = E.toLowerCase();
1756
+ return this.voices.find((n) => n.name.toLowerCase().includes(s)) || null;
1757
+ }
1758
+ /**
1759
+ * Load a DX7 bank from a file
1760
+ * @param {File|Blob} file - SYX file to load
1761
+ * @returns {Promise<DX7Bank>}
1762
+ * @throws {DX7ParseError} If file is a single voice file
1763
+ * @throws {DX7ValidationError} If data is not valid DX7 SYX format
1764
+ * @throws {Error} If file cannot be read (FileReader error)
1765
+ */
1766
+ static async fromFile(E) {
1767
+ return new Promise((s, n) => {
1768
+ const _ = new FileReader();
1769
+ _.onload = async (A) => {
1770
+ try {
1771
+ const e = E.name || "", r = new Uint8Array(A.target.result);
1772
+ if (r[0] === C.SYSEX_START && r[3] === t.VCED_FORMAT_SINGLE)
1773
+ n(
1774
+ new D(
1775
+ "This is a single voice file. Use DX7Voice.fromFile() instead.",
1776
+ "format",
1777
+ 3
1778
+ )
1779
+ );
1780
+ else {
1781
+ const T = e.replace(/\.[^/.]+$/, ""), u = new C(A.target.result, T);
1782
+ s(u);
1783
+ }
1784
+ } catch (e) {
1785
+ n(e);
1786
+ }
1787
+ }, _.onerror = () => n(new Error("Failed to read file")), _.readAsArrayBuffer(E);
1788
+ });
1789
+ }
1790
+ /**
1791
+ * Export bank to SysEx format
1792
+ * @returns {Uint8Array} Full SysEx data (4104 bytes)
1793
+ */
1794
+ toSysEx() {
1795
+ const E = new Uint8Array(C.SYSEX_SIZE);
1796
+ let s = 0;
1797
+ C.SYSEX_HEADER.forEach((_) => {
1798
+ E[s++] = _;
1799
+ });
1800
+ for (const _ of this.voices)
1801
+ for (let A = 0; A < C.VOICE_SIZE; A++)
1802
+ E[s++] = _.data[A];
1803
+ const n = E.subarray(
1804
+ C.SYSEX_HEADER_SIZE,
1805
+ C.SYSEX_HEADER_SIZE + C.VOICE_DATA_SIZE
1806
+ );
1807
+ return E[s++] = C._calculateChecksum(n, C.VOICE_DATA_SIZE), E[s++] = C.SYSEX_END, E;
1808
+ }
1809
+ /**
1810
+ * Convert bank to JSON format
1811
+ * @returns {object} Bank data in JSON format
1812
+ */
1813
+ toJSON() {
1814
+ const E = this.voices.map((s, n) => {
1815
+ const _ = s.toJSON();
1816
+ return {
1817
+ index: n + 1,
1818
+ ..._
1819
+ };
1820
+ });
1821
+ return {
1822
+ version: "1.0",
1823
+ name: this.name || "",
1824
+ voices: E
1825
+ };
1826
+ }
1827
+ }
1828
+ function Et(a) {
1829
+ return a[0] !== 240 || a[a.length - 1] !== 247 ? null : {
1830
+ manufacturerId: a[1],
1831
+ payload: a.slice(2, -1),
1832
+ raw: a
1833
+ };
1834
+ }
1835
+ function st(a, E) {
1836
+ return [240, a, ...E, 247];
1837
+ }
1838
+ function nt(a) {
1839
+ return a.length >= 2 && a[0] === 240 && a[a.length - 1] === 247;
1840
+ }
1841
+ function _t(a) {
1842
+ const E = [];
1843
+ for (let s = 0; s < a.length; s += 7) {
1844
+ const n = a.slice(s, s + 7);
1845
+ let _ = 0;
1846
+ const A = [];
1847
+ for (let e = 0; e < n.length; e++) {
1848
+ const r = n[e];
1849
+ r & 128 && (_ |= 1 << e), A.push(r & 127);
1850
+ }
1851
+ E.push(_, ...A);
1852
+ }
1853
+ return E;
1854
+ }
1855
+ function et(a) {
1856
+ const E = [];
1857
+ for (let s = 0; s < a.length; s += 8) {
1858
+ const n = a[s], _ = Math.min(7, a.length - s - 1);
1859
+ for (let A = 0; A < _; A++) {
1860
+ let e = a[s + 1 + A];
1861
+ n & 1 << A && (e |= 128), E.push(e);
1862
+ }
1863
+ }
1864
+ return E;
1865
+ }
1866
+ function rt(a) {
1867
+ return Number.isInteger(a) && a >= 1 && a <= 16;
1868
+ }
1869
+ function At(a) {
1870
+ return Number.isInteger(a) && a >= 0 && a <= 127;
1871
+ }
1872
+ function at(a) {
1873
+ return Number.isInteger(a) && a >= 0 && a <= 31;
1874
+ }
1875
+ function it(a) {
1876
+ return Number.isInteger(a) && a >= 0 && a <= 127;
1877
+ }
1878
+ function Pt(a) {
1879
+ return Number.isInteger(a) && a >= 0 && a <= 127;
1880
+ }
1881
+ function Ct(a) {
1882
+ return Number.isInteger(a) && a >= 0 && a <= 127;
1883
+ }
1884
+ function Tt(a) {
1885
+ return Number.isInteger(a) && a >= 0 && a <= 127;
1886
+ }
1887
+ function ut(a) {
1888
+ return Number.isInteger(a) && a >= 0 && a <= 16383;
1889
+ }
1890
+ function Nt(a, E) {
1891
+ return Number.isInteger(a) && a >= 0 && a <= 127 && Number.isInteger(E) && E >= 0 && E <= 127;
1892
+ }
1893
+ async function q(a = {}) {
1894
+ const E = new z(a);
1895
+ await E.initialize();
1896
+ const s = a.selector || "[data-midi-cc]";
1897
+ {
1898
+ const n = new B(E, s);
1899
+ n.bindAll(), a.watchDOM && n.enableAutoBinding(), E._binder = n;
1900
+ }
1901
+ return E;
1902
+ }
1903
+ async function St(a = {}) {
1904
+ const {
1905
+ onStatusUpdate: E,
1906
+ onConnectionUpdate: s,
1907
+ channel: n,
1908
+ output: _,
1909
+ sysex: A,
1910
+ onReady: e,
1911
+ onError: r,
1912
+ selector: T,
1913
+ watchDOM: u,
1914
+ ...i
1915
+ } = a, l = await q({
1916
+ autoConnect: !1,
1917
+ sysex: A,
1918
+ channel: n || 1,
1919
+ selector: T || "[data-midi-cc]",
1920
+ watchDOM: u,
1921
+ onError: r,
1922
+ ...i
1923
+ }), S = new W({
1924
+ midiController: l,
1925
+ onStatusUpdate: E || (() => {
1926
+ }),
1927
+ onConnectionUpdate: s || (() => {
1928
+ }),
1929
+ channel: n || 1
1930
+ });
1931
+ if (_)
1932
+ try {
1933
+ await l.setOutput(_), S.currentDevice = l.getCurrentOutput(), S.updateConnectionStatus();
1934
+ } catch (N) {
1935
+ r ? r(N) : console.error("Failed to connect to MIDI device:", N.message);
1936
+ }
1937
+ return e && e(l, S), S;
1938
+ }
1939
+ export {
1940
+ d as CONN,
1941
+ d as CONNECTION_EVENTS,
1942
+ o as CONTROLLER_EVENTS,
1943
+ o as CTRL,
1944
+ C as DX7Bank,
1945
+ H as DX7Error,
1946
+ D as DX7ParseError,
1947
+ c as DX7ValidationError,
1948
+ t as DX7Voice,
1949
+ B as DataAttributeBinder,
1950
+ b as EventEmitter,
1951
+ p as MIDIAccessError,
1952
+ $ as MIDIConnection,
1953
+ g as MIDIConnectionError,
1954
+ z as MIDIController,
1955
+ f as MIDIDeviceError,
1956
+ W as MIDIDeviceManager,
1957
+ M as MIDIError,
1958
+ m as MIDIValidationError,
1959
+ O as clamp,
1960
+ q as createMIDIController,
1961
+ St as createMIDIDeviceManager,
1962
+ st as createSysEx,
1963
+ Y as decode14BitValue,
1964
+ et as decode7Bit,
1965
+ tt as denormalize14BitValue,
1966
+ Q as denormalizeValue,
1967
+ x as encode14BitValue,
1968
+ _t as encode7Bit,
1969
+ k as frequencyToNote,
1970
+ X as getCCName,
1971
+ nt as isSysEx,
1972
+ at as isValid14BitCC,
1973
+ At as isValidCC,
1974
+ rt as isValidChannel,
1975
+ it as isValidMIDIValue,
1976
+ Pt as isValidNote,
1977
+ ut as isValidPitchBend,
1978
+ Nt as isValidPitchBendBytes,
1979
+ Tt as isValidProgramChange,
1980
+ Ct as isValidVelocity,
1981
+ Z as normalize14BitValue,
1982
+ w as normalizeValue,
1983
+ j as noteNameToNumber,
1984
+ J as noteNumberToName,
1985
+ V as noteToFrequency,
1986
+ Et as parseSysEx
1987
+ };