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,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
|
+
};
|