midiwire 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1294 @@
1
+ /**
2
+ * DX7 Bank loader for parsing Yamaha DX7 SYX files
3
+ *
4
+ * Based on Dexed implementation by Pascal Gauthier
5
+ * @see https://github.com/asb2m10/dexed
6
+ */
7
+
8
+ import { DX7ParseError, DX7ValidationError } from "../core/errors.js"
9
+
10
+ /**
11
+ * @typedef {Object} DX7OperatorJSON - JSON representation of a DX7 operator
12
+ * @property {number} id - Operator number (1-6)
13
+ * @property {Object} osc - Oscillator parameters
14
+ * @property {Object} eg - Envelope parameters
15
+ * @property {Object} key - Key scaling parameters
16
+ * @property {Object} output - Output parameters
17
+ * @property {Object} scale - Keyboard scaling parameters
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} DX7VoiceJSON - JSON representation of a DX7 voice
22
+ * @property {string} name - Voice/patch name
23
+ * @property {DX7OperatorJSON[]} operators - Array of 6 operators
24
+ * @property {Object} pitchEG - Pitch envelope parameters
25
+ * @property {Object} lfo - LFO parameters
26
+ * @property {Object} global - Global voice parameters
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} DX7VoiceIndexJSON - JSON representation of a DX7 voice with index
31
+ * @property {number} index - Voice index (1-32)
32
+ * @property {string} name - Voice/patch name
33
+ * @property {DX7OperatorJSON[]} operators - Array of 6 operators
34
+ * @property {Object} pitchEG - Pitch envelope parameters
35
+ * @property {Object} lfo - LFO parameters
36
+ * @property {Object} global - Global voice parameters
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} DX7BankJSON - JSON representation of a DX7 bank
41
+ * @property {string} version - Version string (e.g., "1.0")
42
+ * @property {string} name - Bank name (e.g., filename)
43
+ * @property {DX7VoiceIndexJSON[]} voices - Array of 32 voices
44
+ */
45
+
46
+ /**
47
+ * DX7 Voice (patch) structure
48
+ * Each voice is 128 bytes in packed format
49
+ */
50
+ export class DX7Voice {
51
+ // Packed format (128 bytes)
52
+ // See: DX7 Service Manual, Voice Memory Format
53
+ static PACKED_SIZE = 128
54
+ static PACKED_OP_SIZE = 17 // 17 bytes per operator in packed format
55
+ static NUM_OPERATORS = 6
56
+
57
+ // Packed operator parameter offsets (within each 17-byte operator block)
58
+ static PACKED_OP_EG_RATE_1 = 0
59
+ static PACKED_OP_EG_RATE_2 = 1
60
+ static PACKED_OP_EG_RATE_3 = 2
61
+ static PACKED_OP_EG_RATE_4 = 3
62
+ static PACKED_OP_EG_LEVEL_1 = 4
63
+ static PACKED_OP_EG_LEVEL_2 = 5
64
+ static PACKED_OP_EG_LEVEL_3 = 6
65
+ static PACKED_OP_EG_LEVEL_4 = 7
66
+ static PACKED_OP_BREAK_POINT = 8
67
+ static PACKED_OP_L_SCALE_DEPTH = 9
68
+ static PACKED_OP_R_SCALE_DEPTH = 10
69
+ static PACKED_OP_CURVES = 11 // LC and RC packed
70
+ static PACKED_OP_RATE_SCALING = 12 // RS and DET packed
71
+ static PACKED_OP_MOD_SENS = 13 // AMS and KVS packed
72
+ static PACKED_OP_OUTPUT_LEVEL = 14
73
+ static PACKED_OP_MODE_FREQ = 15 // Mode and Freq Coarse packed
74
+ static PACKED_OP_DETUNE_FINE = 16 // OSC Detune and Freq Fine packed
75
+
76
+ // Packed voice offsets (after 6 operators = bytes 102+)
77
+ static PACKED_PITCH_EG_RATE_1 = 102
78
+ static PACKED_PITCH_EG_RATE_2 = 103
79
+ static PACKED_PITCH_EG_RATE_3 = 104
80
+ static PACKED_PITCH_EG_RATE_4 = 105
81
+ static PACKED_PITCH_EG_LEVEL_1 = 106
82
+ static PACKED_PITCH_EG_LEVEL_2 = 107
83
+ static PACKED_PITCH_EG_LEVEL_3 = 108
84
+ static PACKED_PITCH_EG_LEVEL_4 = 109
85
+ static OFFSET_ALGORITHM = 110
86
+ static OFFSET_FEEDBACK = 111 // Also contains OSC Sync
87
+ static OFFSET_LFO_SPEED = 112
88
+ static OFFSET_LFO_DELAY = 113
89
+ static OFFSET_LFO_PM_DEPTH = 114
90
+ static OFFSET_LFO_AM_DEPTH = 115
91
+ static OFFSET_LFO_SYNC_WAVE = 116 // LFO sync, wave, and PM sensitivity packed
92
+ static OFFSET_TRANSPOSE = 117
93
+ static OFFSET_AMP_MOD_SENS = 118
94
+ static OFFSET_EG_BIAS_SENS = 119
95
+
96
+ // Voice name (bytes 118-127)
97
+ // IMPORTANT: Byte 118 serves dual-purpose in DX7 hardware:
98
+ // 1. First character of voice name (as ASCII)
99
+ // 2. Amp Mod Sensitivity parameter (as numeric value 0-127)
100
+ // Both interpretations are used when converting to unpacked format
101
+ static PACKED_NAME_START = 118
102
+ static NAME_LENGTH = 10
103
+
104
+ // Unpacked format (169 bytes)
105
+ static UNPACKED_SIZE = 169 // Total unpacked size (159 params + 10 name)
106
+ static UNPACKED_OP_SIZE = 23 // 23 bytes per operator in unpacked format
107
+
108
+ // Unpacked operator parameter offsets (within each 23-byte operator block)
109
+ static UNPACKED_OP_EG_RATE_1 = 0
110
+ static UNPACKED_OP_EG_RATE_2 = 1
111
+ static UNPACKED_OP_EG_RATE_3 = 2
112
+ static UNPACKED_OP_EG_RATE_4 = 3
113
+ static UNPACKED_OP_EG_LEVEL_1 = 4
114
+ static UNPACKED_OP_EG_LEVEL_2 = 5
115
+ static UNPACKED_OP_EG_LEVEL_3 = 6
116
+ static UNPACKED_OP_EG_LEVEL_4 = 7
117
+ static UNPACKED_OP_BREAK_POINT = 8
118
+ static UNPACKED_OP_L_SCALE_DEPTH = 9
119
+ static UNPACKED_OP_R_SCALE_DEPTH = 10
120
+ static UNPACKED_OP_L_CURVE = 11
121
+ static UNPACKED_OP_R_CURVE = 12
122
+ static UNPACKED_OP_RATE_SCALING = 13
123
+ static UNPACKED_OP_DETUNE = 14
124
+ static UNPACKED_OP_AMP_MOD_SENS = 15
125
+ static UNPACKED_OP_OUTPUT_LEVEL = 16
126
+ static UNPACKED_OP_MODE = 17 // Mode (0=ratio, 1=fixed)
127
+ static UNPACKED_OP_KEY_VEL_SENS = 18
128
+ static UNPACKED_OP_FREQ_COARSE = 19
129
+ static UNPACKED_OP_OSC_DETUNE = 20
130
+ static UNPACKED_OP_FREQ_FINE = 21
131
+
132
+ // Unpacked pitch EG offsets (after 6 operators = index 138+)
133
+ static UNPACKED_PITCH_EG_RATE_1 = 138
134
+ static UNPACKED_PITCH_EG_RATE_2 = 139
135
+ static UNPACKED_PITCH_EG_RATE_3 = 140
136
+ static UNPACKED_PITCH_EG_RATE_4 = 141
137
+ static UNPACKED_PITCH_EG_LEVEL_1 = 142
138
+ static UNPACKED_PITCH_EG_LEVEL_2 = 143
139
+ static UNPACKED_PITCH_EG_LEVEL_3 = 144
140
+ static UNPACKED_PITCH_EG_LEVEL_4 = 145
141
+
142
+ // Unpacked global parameters (after pitch EG = index 146+)
143
+ static UNPACKED_ALGORITHM = 146
144
+ static UNPACKED_FEEDBACK = 147
145
+ static UNPACKED_OSC_SYNC = 148
146
+ static UNPACKED_LFO_SPEED = 149
147
+ static UNPACKED_LFO_DELAY = 150
148
+ static UNPACKED_LFO_PM_DEPTH = 151
149
+ static UNPACKED_LFO_AM_DEPTH = 152
150
+ static UNPACKED_LFO_KEY_SYNC = 153
151
+ static UNPACKED_LFO_WAVE = 154
152
+ static UNPACKED_LFO_PM_SENS = 155
153
+ static UNPACKED_AMP_MOD_SENS = 156
154
+ static UNPACKED_TRANSPOSE = 157
155
+ static UNPACKED_EG_BIAS_SENS = 158
156
+ static UNPACKED_NAME_START = 159
157
+
158
+ // VCED (single voice SysEx) format - for DX7 single patch dumps
159
+ static VCED_SIZE = 163 // Total VCED sysex size (6 header + 155 data + 1 checksum + 1 end)
160
+ static VCED_HEADER_SIZE = 6
161
+ static VCED_DATA_SIZE = 155 // Voice data bytes (6 operators × 21 bytes + 8 pitch EG + 11 global + 10 name)
162
+
163
+ // VCED header bytes - DX7 single voice dump format
164
+ static VCED_SYSEX_START = 0xf0 // SysEx Message Start
165
+ static VCED_YAMAHA_ID = 0x43 // Yamaha manufacturer ID
166
+ static VCED_SUB_STATUS = 0x00
167
+ static VCED_FORMAT_SINGLE = 0x00 // Single voice format identifier
168
+ static VCED_BYTE_COUNT_MSB = 0x01 // High byte of data length (1)
169
+ static VCED_BYTE_COUNT_LSB = 0x1b // Low byte of data length (27 in decimal = 155 bytes)
170
+ static VCED_SYSEX_END = 0xf7 // SysEx Message End
171
+
172
+ // Bit masks
173
+ static MASK_7BIT = 0x7f // Standard 7-bit MIDI data mask
174
+ static MASK_2BIT = 0x03 // For 2-bit values (curves)
175
+ static MASK_3BIT = 0x07 // For 3-bit values (RS, detune)
176
+ static MASK_4BIT = 0x0f // For 4-bit values (detune, fine freq)
177
+ static MASK_5BIT = 0x1f // For 5-bit values (algorithm, freq coarse)
178
+ static MASK_1BIT = 0x01 // For 1-bit values (mode, sync)
179
+
180
+ // Parameter value ranges
181
+ static TRANSPOSE_CENTER = 24 // MIDI note 24 = C0 (center of DX7 transpose range: -24 to +24 semitones)
182
+
183
+ // Special character mappings - for Japanese DX7 character set compatibility
184
+ static CHAR_YEN = 92 // Japanese Yen symbol (¥) maps to ASCII backslash
185
+ static CHAR_ARROW_RIGHT = 126 // Right arrow (→) maps to ASCII tilde
186
+ static CHAR_ARROW_LEFT = 127 // Left arrow (←) maps to ASCII DEL
187
+ static CHAR_REPLACEMENT_Y = 89 // Replace Yen symbol with 'Y'
188
+ static CHAR_REPLACEMENT_GT = 62 // Right arrow with '>'
189
+ static CHAR_REPLACEMENT_LT = 60 // Left arrow with '<'
190
+ static CHAR_SPACE = 32 // Standard space character
191
+ static CHAR_MIN_PRINTABLE = 32 // Minimum ASCII printable character
192
+ static CHAR_MAX_PRINTABLE = 126 // Maximum ASCII printable character
193
+
194
+ // Default voice values
195
+ static DEFAULT_EG_RATE = 99
196
+ static DEFAULT_EG_LEVEL_MAX = 99
197
+ static DEFAULT_EG_LEVEL_MIN = 0
198
+ static DEFAULT_BREAK_POINT = 60 // MIDI note 60 = C3
199
+ static DEFAULT_OUTPUT_LEVEL = 99
200
+ static DEFAULT_PITCH_EG_LEVEL = 50
201
+ static DEFAULT_LFO_SPEED = 35
202
+ static DEFAULT_LFO_PM_SENS = 3
203
+ static DEFAULT_ALGORITHM = 0
204
+ static DEFAULT_FEEDBACK = 0
205
+
206
+ // MIDI notes
207
+ static MIDI_OCTAVE_OFFSET = -2 // For displaying MIDI notes (MIDI 0 = C-2)
208
+ static MIDI_BREAK_POINT_OFFSET = 21 // Offset for breakpoint display
209
+
210
+ /**
211
+ * Create a DX7Voice from raw 128-byte data
212
+ * @param {Array<number>|Uint8Array} data - 128 bytes of voice data
213
+ * @param {number} index - Voice index (0-31)
214
+ * @throws {DX7ValidationError} If data length is not exactly 128 bytes
215
+ */
216
+ constructor(data, index = 0) {
217
+ if (data.length !== DX7Voice.PACKED_SIZE) {
218
+ throw new DX7ValidationError(
219
+ `Invalid voice data length: expected ${DX7Voice.PACKED_SIZE} bytes, got ${data.length}`,
220
+ "length",
221
+ data.length,
222
+ )
223
+ }
224
+
225
+ this.index = index
226
+ this.data = new Uint8Array(data)
227
+ this.name = this._extractName()
228
+ }
229
+
230
+ /**
231
+ * Extract the voice name from the data (10 characters at offset 118)
232
+ * @private
233
+ */
234
+ _extractName() {
235
+ const nameBytes = this.data.subarray(
236
+ DX7Voice.PACKED_NAME_START,
237
+ DX7Voice.PACKED_NAME_START + DX7Voice.NAME_LENGTH,
238
+ )
239
+ // Normalize DX7 special characters
240
+ const normalized = Array.from(nameBytes).map((byte) => {
241
+ let c = byte & DX7Voice.MASK_7BIT
242
+ // Dexed special character mappings
243
+ if (c === DX7Voice.CHAR_YEN) c = DX7Voice.CHAR_REPLACEMENT_Y
244
+ if (c === DX7Voice.CHAR_ARROW_RIGHT) c = DX7Voice.CHAR_REPLACEMENT_GT
245
+ if (c === DX7Voice.CHAR_ARROW_LEFT) c = DX7Voice.CHAR_REPLACEMENT_LT
246
+ if (c < DX7Voice.CHAR_MIN_PRINTABLE || c > DX7Voice.CHAR_MAX_PRINTABLE)
247
+ c = DX7Voice.CHAR_SPACE
248
+ return String.fromCharCode(c)
249
+ })
250
+ return normalized.join("").trim()
251
+ }
252
+
253
+ /**
254
+ * Get a raw parameter value from the packed data
255
+ * @param {number} offset - Byte offset in the voice data (0-127)
256
+ * @returns {number} Parameter value (0-127)
257
+ * @throws {DX7ValidationError} If offset is out of range
258
+ */
259
+ getParameter(offset) {
260
+ if (offset < 0 || offset >= DX7Voice.PACKED_SIZE) {
261
+ throw new DX7ValidationError(
262
+ `Parameter offset out of range: ${offset} (must be 0-${DX7Voice.PACKED_SIZE - 1})`,
263
+ "offset",
264
+ offset,
265
+ )
266
+ }
267
+ return this.data[offset] & DX7Voice.MASK_7BIT
268
+ }
269
+
270
+ /**
271
+ * Get a parameter value from the unpacked 169-byte format
272
+ * @param {number} offset - Byte offset in the unpacked data (0-168)
273
+ * @returns {number} Parameter value (0-127)
274
+ * @throws {DX7ValidationError} If offset is out of range
275
+ */
276
+ getUnpackedParameter(offset) {
277
+ if (offset < 0 || offset >= DX7Voice.UNPACKED_SIZE) {
278
+ throw new DX7ValidationError(
279
+ `Unpacked parameter offset out of range: ${offset} (must be 0-${DX7Voice.UNPACKED_SIZE - 1})`,
280
+ "offset",
281
+ offset,
282
+ )
283
+ }
284
+ const unpacked = this.unpack()
285
+ return unpacked[offset] & DX7Voice.MASK_7BIT
286
+ }
287
+
288
+ /**
289
+ * Set a raw parameter value in the packed data
290
+ * @param {number} offset - Byte offset in the voice data
291
+ * @param {number} value - Parameter value (0-127)
292
+ */
293
+ setParameter(offset, value) {
294
+ if (offset < 0 || offset >= DX7Voice.PACKED_SIZE) {
295
+ throw new DX7ValidationError(
296
+ `Parameter offset out of range: ${offset} (must be 0-${DX7Voice.PACKED_SIZE - 1})`,
297
+ "offset",
298
+ offset,
299
+ )
300
+ }
301
+ this.data[offset] = value & DX7Voice.MASK_7BIT
302
+ // Update name if name bytes changed
303
+ if (
304
+ offset >= DX7Voice.PACKED_NAME_START &&
305
+ offset < DX7Voice.PACKED_NAME_START + DX7Voice.NAME_LENGTH
306
+ ) {
307
+ this.name = this._extractName()
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Unpack the voice data to 169-byte unpacked format
313
+ * This converts the packed 128-byte format to the full DX7 parameter set
314
+ * @returns {Uint8Array} 169 bytes of unpacked voice data (138 operator + 8 pitch EG + 13 global + 10 name = 169 bytes)
315
+ */
316
+ unpack() {
317
+ const packed = this.data
318
+ const unpacked = new Uint8Array(DX7Voice.UNPACKED_SIZE)
319
+
320
+ // Operators (6 operators × 17 bytes each in packed format)
321
+ // Note: DX7 stores operators in reverse order in packed format
322
+ // OP1 data is at the end (packed offset 85-101), OP6 data is at the beginning (packed offset 0-16)
323
+ for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
324
+ // Calculate source and destination offsets
325
+ // op=0 is OP1, which is at packed offset 85, unpacked offset 0
326
+ // op=5 is OP6, which is at packed offset 0, unpacked offset 115
327
+ const src = (DX7Voice.NUM_OPERATORS - 1 - op) * DX7Voice.PACKED_OP_SIZE // Source offset in packed data
328
+ const dst = op * DX7Voice.UNPACKED_OP_SIZE // Destination offset in unpacked data
329
+
330
+ // EG rates and levels (4 bytes each) - bytes 0-7
331
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_1] =
332
+ packed[src + DX7Voice.PACKED_OP_EG_RATE_1] & DX7Voice.MASK_7BIT
333
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_2] =
334
+ packed[src + DX7Voice.PACKED_OP_EG_RATE_2] & DX7Voice.MASK_7BIT
335
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_3] =
336
+ packed[src + DX7Voice.PACKED_OP_EG_RATE_3] & DX7Voice.MASK_7BIT
337
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_4] =
338
+ packed[src + DX7Voice.PACKED_OP_EG_RATE_4] & DX7Voice.MASK_7BIT
339
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_1] =
340
+ packed[src + DX7Voice.PACKED_OP_EG_LEVEL_1] & DX7Voice.MASK_7BIT
341
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_2] =
342
+ packed[src + DX7Voice.PACKED_OP_EG_LEVEL_2] & DX7Voice.MASK_7BIT
343
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_3] =
344
+ packed[src + DX7Voice.PACKED_OP_EG_LEVEL_3] & DX7Voice.MASK_7BIT
345
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_4] =
346
+ packed[src + DX7Voice.PACKED_OP_EG_LEVEL_4] & DX7Voice.MASK_7BIT
347
+
348
+ // Break point and scaling depths - bytes 8-10
349
+ unpacked[dst + DX7Voice.UNPACKED_OP_BREAK_POINT] =
350
+ packed[src + DX7Voice.PACKED_OP_BREAK_POINT] & DX7Voice.MASK_7BIT
351
+ unpacked[dst + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH] =
352
+ packed[src + DX7Voice.PACKED_OP_L_SCALE_DEPTH] & DX7Voice.MASK_7BIT
353
+ unpacked[dst + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH] =
354
+ packed[src + DX7Voice.PACKED_OP_R_SCALE_DEPTH] & DX7Voice.MASK_7BIT
355
+
356
+ // Key scales (bits 0-1 = LC, bits 2-3 = RC)
357
+ const curves = packed[src + DX7Voice.PACKED_OP_CURVES] & DX7Voice.MASK_7BIT
358
+ unpacked[dst + DX7Voice.UNPACKED_OP_L_CURVE] = curves & DX7Voice.MASK_2BIT
359
+ unpacked[dst + DX7Voice.UNPACKED_OP_R_CURVE] = (curves >> 2) & DX7Voice.MASK_2BIT
360
+
361
+ // Rate scaling and detune (bits 0-2 = RS, bits 3-6 = DET)
362
+ const rateScaling = packed[src + DX7Voice.PACKED_OP_RATE_SCALING] & DX7Voice.MASK_7BIT
363
+ unpacked[dst + DX7Voice.UNPACKED_OP_RATE_SCALING] = rateScaling & DX7Voice.MASK_3BIT
364
+ unpacked[dst + DX7Voice.UNPACKED_OP_DETUNE] = (rateScaling >> 3) & DX7Voice.MASK_4BIT
365
+
366
+ // Amp mod sensitivity and key velocity sensitivity (bits 0-1 = AMS, bits 2-4 = KVS)
367
+ const modSens = packed[src + DX7Voice.PACKED_OP_MOD_SENS] & DX7Voice.MASK_7BIT
368
+ unpacked[dst + DX7Voice.UNPACKED_OP_AMP_MOD_SENS] = modSens & DX7Voice.MASK_2BIT
369
+ unpacked[dst + DX7Voice.UNPACKED_OP_KEY_VEL_SENS] = (modSens >> 2) & DX7Voice.MASK_3BIT
370
+
371
+ // Output level
372
+ unpacked[dst + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL] =
373
+ packed[src + DX7Voice.PACKED_OP_OUTPUT_LEVEL] & DX7Voice.MASK_7BIT
374
+
375
+ // Mode, frequency (bits 0 = MODE, bits 1-5 = FREQ)
376
+ const modeFreq = packed[src + DX7Voice.PACKED_OP_MODE_FREQ] & DX7Voice.MASK_7BIT
377
+ unpacked[dst + DX7Voice.UNPACKED_OP_MODE] = modeFreq & DX7Voice.MASK_1BIT
378
+ unpacked[dst + DX7Voice.UNPACKED_OP_FREQ_COARSE] = (modeFreq >> 1) & DX7Voice.MASK_5BIT
379
+
380
+ // OSC detune and frequency fine (bits 0-2 = OSC DET, bits 3-6 = FREQ FINE)
381
+ const detuneFine = packed[src + DX7Voice.PACKED_OP_DETUNE_FINE] & DX7Voice.MASK_7BIT
382
+ unpacked[dst + DX7Voice.UNPACKED_OP_OSC_DETUNE] = detuneFine & DX7Voice.MASK_3BIT
383
+ unpacked[dst + DX7Voice.UNPACKED_OP_FREQ_FINE] = (detuneFine >> 3) & DX7Voice.MASK_4BIT
384
+ }
385
+
386
+ // Pitch EG rates and levels
387
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1] =
388
+ packed[DX7Voice.PACKED_PITCH_EG_RATE_1] & DX7Voice.MASK_7BIT
389
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2] =
390
+ packed[DX7Voice.PACKED_PITCH_EG_RATE_2] & DX7Voice.MASK_7BIT
391
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3] =
392
+ packed[DX7Voice.PACKED_PITCH_EG_RATE_3] & DX7Voice.MASK_7BIT
393
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4] =
394
+ packed[DX7Voice.PACKED_PITCH_EG_RATE_4] & DX7Voice.MASK_7BIT
395
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1] =
396
+ packed[DX7Voice.PACKED_PITCH_EG_LEVEL_1] & DX7Voice.MASK_7BIT
397
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2] =
398
+ packed[DX7Voice.PACKED_PITCH_EG_LEVEL_2] & DX7Voice.MASK_7BIT
399
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3] =
400
+ packed[DX7Voice.PACKED_PITCH_EG_LEVEL_3] & DX7Voice.MASK_7BIT
401
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4] =
402
+ packed[DX7Voice.PACKED_PITCH_EG_LEVEL_4] & DX7Voice.MASK_7BIT
403
+
404
+ // Global parameters
405
+ unpacked[DX7Voice.UNPACKED_ALGORITHM] = packed[DX7Voice.OFFSET_ALGORITHM] & DX7Voice.MASK_5BIT
406
+
407
+ // Feedback and OSC Sync combined (bits 0-2 = Feedback, bit 3 = OSC Sync)
408
+ const feedbackOscSync = packed[DX7Voice.OFFSET_FEEDBACK] & DX7Voice.MASK_7BIT
409
+ unpacked[DX7Voice.UNPACKED_FEEDBACK] = feedbackOscSync & DX7Voice.MASK_3BIT
410
+ unpacked[DX7Voice.UNPACKED_OSC_SYNC] = (feedbackOscSync >> 3) & DX7Voice.MASK_1BIT
411
+
412
+ unpacked[DX7Voice.UNPACKED_LFO_SPEED] = packed[DX7Voice.OFFSET_LFO_SPEED] & DX7Voice.MASK_7BIT
413
+ unpacked[DX7Voice.UNPACKED_LFO_DELAY] = packed[DX7Voice.OFFSET_LFO_DELAY] & DX7Voice.MASK_7BIT
414
+ unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH] =
415
+ packed[DX7Voice.OFFSET_LFO_PM_DEPTH] & DX7Voice.MASK_7BIT
416
+ unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH] =
417
+ packed[DX7Voice.OFFSET_LFO_AM_DEPTH] & DX7Voice.MASK_7BIT
418
+
419
+ // LFO Key Sync, Wave, Pitch Mod Sensitivity packed
420
+ const lfoParams = packed[DX7Voice.OFFSET_LFO_SYNC_WAVE] & DX7Voice.MASK_7BIT
421
+ unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] = lfoParams & DX7Voice.MASK_1BIT
422
+ unpacked[DX7Voice.UNPACKED_LFO_WAVE] = (lfoParams >> 1) & DX7Voice.MASK_3BIT
423
+ unpacked[DX7Voice.UNPACKED_LFO_PM_SENS] = (lfoParams >> 4) & DX7Voice.MASK_3BIT
424
+
425
+ unpacked[DX7Voice.UNPACKED_AMP_MOD_SENS] =
426
+ packed[DX7Voice.OFFSET_AMP_MOD_SENS] & DX7Voice.MASK_7BIT
427
+ unpacked[DX7Voice.UNPACKED_TRANSPOSE] = packed[DX7Voice.OFFSET_TRANSPOSE] & DX7Voice.MASK_7BIT
428
+ unpacked[DX7Voice.UNPACKED_EG_BIAS_SENS] =
429
+ packed[DX7Voice.OFFSET_EG_BIAS_SENS] & DX7Voice.MASK_7BIT
430
+
431
+ // Copy voice name
432
+ for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
433
+ unpacked[DX7Voice.UNPACKED_NAME_START + i] =
434
+ packed[DX7Voice.PACKED_NAME_START + i] & DX7Voice.MASK_7BIT
435
+ }
436
+
437
+ return unpacked
438
+ }
439
+
440
+ /**
441
+ * Pack 169-byte unpacked data to 128-byte format
442
+ * @param {Array<number>|Uint8Array} unpacked - 169 bytes of unpacked data (159 parameters + 10 name bytes)
443
+ * @returns {Uint8Array} 128 bytes of packed data
444
+ */
445
+ static pack(unpacked) {
446
+ if (unpacked.length !== DX7Voice.UNPACKED_SIZE) {
447
+ throw new DX7ValidationError(
448
+ `Invalid unpacked data length: expected ${DX7Voice.UNPACKED_SIZE} bytes, got ${unpacked.length}`,
449
+ "length",
450
+ unpacked.length,
451
+ )
452
+ }
453
+
454
+ const packed = new Uint8Array(DX7Voice.PACKED_SIZE)
455
+
456
+ // Pack operators (6 operators × 17 bytes each in packed format)
457
+ // DX7 stores operators in reverse order: OP1 data goes to packed[85-101], OP6 to packed[0-16]
458
+ for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
459
+ const opSrc = op * DX7Voice.UNPACKED_OP_SIZE // Read OP1-OP6 sequentially from unpacked
460
+ const opDst = (DX7Voice.NUM_OPERATORS - 1 - op) * DX7Voice.PACKED_OP_SIZE // Write in reverse
461
+
462
+ // EG rates and levels - bytes 0-7
463
+ packed[opDst + DX7Voice.PACKED_OP_EG_RATE_1] =
464
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_EG_RATE_1]
465
+ packed[opDst + DX7Voice.PACKED_OP_EG_RATE_2] =
466
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_EG_RATE_2]
467
+ packed[opDst + DX7Voice.PACKED_OP_EG_RATE_3] =
468
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_EG_RATE_3]
469
+ packed[opDst + DX7Voice.PACKED_OP_EG_RATE_4] =
470
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_EG_RATE_4]
471
+ packed[opDst + DX7Voice.PACKED_OP_EG_LEVEL_1] =
472
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_EG_LEVEL_1]
473
+ packed[opDst + DX7Voice.PACKED_OP_EG_LEVEL_2] =
474
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_EG_LEVEL_2]
475
+ packed[opDst + DX7Voice.PACKED_OP_EG_LEVEL_3] =
476
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_EG_LEVEL_3]
477
+ packed[opDst + DX7Voice.PACKED_OP_EG_LEVEL_4] =
478
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_EG_LEVEL_4]
479
+
480
+ // Break point and scaling depths
481
+ packed[opDst + DX7Voice.PACKED_OP_BREAK_POINT] =
482
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_BREAK_POINT]
483
+ packed[opDst + DX7Voice.PACKED_OP_L_SCALE_DEPTH] =
484
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH]
485
+ packed[opDst + DX7Voice.PACKED_OP_R_SCALE_DEPTH] =
486
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH]
487
+
488
+ // Key scales (LC and RC) combined
489
+ const lc = unpacked[opSrc + DX7Voice.UNPACKED_OP_L_CURVE] & DX7Voice.MASK_2BIT
490
+ const rc = unpacked[opSrc + DX7Voice.UNPACKED_OP_R_CURVE] & DX7Voice.MASK_2BIT
491
+ packed[opDst + DX7Voice.PACKED_OP_CURVES] = lc | (rc << 2)
492
+
493
+ // Rate scaling and detune combined
494
+ const rs = unpacked[opSrc + DX7Voice.UNPACKED_OP_RATE_SCALING] & DX7Voice.MASK_3BIT
495
+ const det = unpacked[opSrc + DX7Voice.UNPACKED_OP_DETUNE] & DX7Voice.MASK_4BIT
496
+ packed[opDst + DX7Voice.PACKED_OP_RATE_SCALING] = rs | (det << 3)
497
+
498
+ // Amp mod sensitivity and key velocity sensitivity combined
499
+ const ams = unpacked[opSrc + DX7Voice.UNPACKED_OP_AMP_MOD_SENS] & DX7Voice.MASK_2BIT
500
+ const kvs = unpacked[opSrc + DX7Voice.UNPACKED_OP_KEY_VEL_SENS] & DX7Voice.MASK_3BIT
501
+ packed[opDst + DX7Voice.PACKED_OP_MOD_SENS] = ams | (kvs << 2)
502
+
503
+ // Output level
504
+ packed[opDst + DX7Voice.PACKED_OP_OUTPUT_LEVEL] =
505
+ unpacked[opSrc + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL]
506
+
507
+ // Mode and frequency combined
508
+ const mode = unpacked[opSrc + DX7Voice.UNPACKED_OP_MODE] & DX7Voice.MASK_1BIT
509
+ const freq = unpacked[opSrc + DX7Voice.UNPACKED_OP_FREQ_COARSE] & DX7Voice.MASK_5BIT
510
+ packed[opDst + DX7Voice.PACKED_OP_MODE_FREQ] = mode | (freq << 1)
511
+
512
+ // OSC detune and frequency fine combined
513
+ const oscDetune = unpacked[opSrc + DX7Voice.UNPACKED_OP_OSC_DETUNE] & DX7Voice.MASK_3BIT
514
+ const freqFine = unpacked[opSrc + DX7Voice.UNPACKED_OP_FREQ_FINE] & DX7Voice.MASK_4BIT
515
+ packed[opDst + DX7Voice.PACKED_OP_DETUNE_FINE] = oscDetune | (freqFine << 3)
516
+ }
517
+
518
+ // Pitch EG rates and levels
519
+ packed[DX7Voice.PACKED_PITCH_EG_RATE_1] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1]
520
+ packed[DX7Voice.PACKED_PITCH_EG_RATE_2] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2]
521
+ packed[DX7Voice.PACKED_PITCH_EG_RATE_3] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3]
522
+ packed[DX7Voice.PACKED_PITCH_EG_RATE_4] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4]
523
+ packed[DX7Voice.PACKED_PITCH_EG_LEVEL_1] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1]
524
+ packed[DX7Voice.PACKED_PITCH_EG_LEVEL_2] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2]
525
+ packed[DX7Voice.PACKED_PITCH_EG_LEVEL_3] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3]
526
+ packed[DX7Voice.PACKED_PITCH_EG_LEVEL_4] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4]
527
+ packed[DX7Voice.OFFSET_ALGORITHM] = unpacked[DX7Voice.UNPACKED_ALGORITHM]
528
+
529
+ // Feedback and OSC Sync combined
530
+ const feedback = unpacked[DX7Voice.UNPACKED_FEEDBACK] & DX7Voice.MASK_3BIT
531
+ const oscSync = unpacked[DX7Voice.UNPACKED_OSC_SYNC] & DX7Voice.MASK_1BIT
532
+ packed[DX7Voice.OFFSET_FEEDBACK] = feedback | (oscSync << 3)
533
+
534
+ packed[DX7Voice.OFFSET_LFO_SPEED] = unpacked[DX7Voice.UNPACKED_LFO_SPEED]
535
+ packed[DX7Voice.OFFSET_LFO_DELAY] = unpacked[DX7Voice.UNPACKED_LFO_DELAY]
536
+ packed[DX7Voice.OFFSET_LFO_PM_DEPTH] = unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH]
537
+ packed[DX7Voice.OFFSET_LFO_AM_DEPTH] = unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH]
538
+
539
+ // LFO Key Sync, Wave, Pitch Mod Sensitivity combined
540
+ const lfoKeySync = unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] & DX7Voice.MASK_1BIT
541
+ const lfoWave = unpacked[DX7Voice.UNPACKED_LFO_WAVE] & DX7Voice.MASK_3BIT
542
+ const lfoPitchSens = unpacked[DX7Voice.UNPACKED_LFO_PM_SENS] & DX7Voice.MASK_3BIT
543
+ packed[DX7Voice.OFFSET_LFO_SYNC_WAVE] = lfoKeySync | (lfoWave << 1) | (lfoPitchSens << 4)
544
+
545
+ packed[DX7Voice.OFFSET_AMP_MOD_SENS] = unpacked[DX7Voice.UNPACKED_AMP_MOD_SENS]
546
+ packed[DX7Voice.OFFSET_TRANSPOSE] = unpacked[DX7Voice.UNPACKED_TRANSPOSE]
547
+ packed[DX7Voice.OFFSET_EG_BIAS_SENS] = unpacked[DX7Voice.UNPACKED_EG_BIAS_SENS]
548
+
549
+ // Write voice name
550
+ for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
551
+ packed[DX7Voice.PACKED_NAME_START + i] = unpacked[DX7Voice.UNPACKED_NAME_START + i]
552
+ }
553
+
554
+ return packed
555
+ }
556
+
557
+ /**
558
+ * Create a default/empty voice
559
+ * @param {number} index - Voice index
560
+ * @returns {DX7Voice}
561
+ */
562
+ static createDefault(index = 0) {
563
+ const unpacked = new Uint8Array(DX7Voice.UNPACKED_SIZE)
564
+
565
+ // Default operator settings
566
+ for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
567
+ const opOffset = op * DX7Voice.UNPACKED_OP_SIZE
568
+
569
+ // EG rates
570
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_1] = DX7Voice.DEFAULT_EG_RATE
571
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_2] = DX7Voice.DEFAULT_EG_RATE
572
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_3] = DX7Voice.DEFAULT_EG_RATE
573
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_4] = DX7Voice.DEFAULT_EG_RATE
574
+
575
+ // EG levels
576
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_1] = DX7Voice.DEFAULT_EG_LEVEL_MAX
577
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_2] = DX7Voice.DEFAULT_EG_LEVEL_MAX
578
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_3] = DX7Voice.DEFAULT_EG_LEVEL_MAX
579
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_4] = DX7Voice.DEFAULT_EG_LEVEL_MIN
580
+
581
+ // Break point, scaling, curves
582
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_BREAK_POINT] = DX7Voice.DEFAULT_BREAK_POINT
583
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH] = 0
584
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH] = 0
585
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_L_CURVE] = 0
586
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_R_CURVE] = 0
587
+
588
+ // Rate scaling, detune, sensitivities
589
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_RATE_SCALING] = 0
590
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_AMP_MOD_SENS] = 0
591
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_KEY_VEL_SENS] = 0
592
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL] = DX7Voice.DEFAULT_OUTPUT_LEVEL
593
+
594
+ // Oscillator parameters
595
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_MODE] = 0 // Ratio mode
596
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_FREQ_COARSE] = 0
597
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_OSC_DETUNE] = 0
598
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_FREQ_FINE] = 0
599
+ }
600
+
601
+ // Pitch EG rates
602
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1] = DX7Voice.DEFAULT_EG_RATE
603
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2] = DX7Voice.DEFAULT_EG_RATE
604
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3] = DX7Voice.DEFAULT_EG_RATE
605
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4] = DX7Voice.DEFAULT_EG_RATE
606
+
607
+ // Pitch EG levels
608
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1] = DX7Voice.DEFAULT_PITCH_EG_LEVEL
609
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2] = DX7Voice.DEFAULT_PITCH_EG_LEVEL
610
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3] = DX7Voice.DEFAULT_PITCH_EG_LEVEL
611
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4] = DX7Voice.DEFAULT_PITCH_EG_LEVEL
612
+
613
+ // Global params
614
+ unpacked[DX7Voice.UNPACKED_ALGORITHM] = DX7Voice.DEFAULT_ALGORITHM
615
+ unpacked[DX7Voice.UNPACKED_FEEDBACK] = DX7Voice.DEFAULT_FEEDBACK
616
+ unpacked[DX7Voice.UNPACKED_OSC_SYNC] = 0
617
+ unpacked[DX7Voice.UNPACKED_LFO_SPEED] = DX7Voice.DEFAULT_LFO_SPEED
618
+ unpacked[DX7Voice.UNPACKED_LFO_DELAY] = 0
619
+ unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH] = 0
620
+ unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH] = 0
621
+ unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] = 0
622
+ unpacked[DX7Voice.UNPACKED_LFO_WAVE] = 0
623
+ unpacked[DX7Voice.UNPACKED_LFO_PM_SENS] = DX7Voice.DEFAULT_LFO_PM_SENS
624
+ unpacked[DX7Voice.UNPACKED_AMP_MOD_SENS] = 0
625
+ unpacked[DX7Voice.UNPACKED_TRANSPOSE] = DX7Voice.TRANSPOSE_CENTER
626
+ unpacked[DX7Voice.UNPACKED_EG_BIAS_SENS] = 0
627
+
628
+ // Set name to "Init Voice"
629
+ const name = "Init Voice"
630
+ for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
631
+ unpacked[DX7Voice.UNPACKED_NAME_START + i] =
632
+ i < name.length ? name.charCodeAt(i) : DX7Voice.CHAR_SPACE
633
+ }
634
+
635
+ const packed = DX7Voice.pack(unpacked)
636
+ return new DX7Voice(packed, index)
637
+ }
638
+
639
+ /**
640
+ * Create a voice from unpacked 169-byte data
641
+ * @param {Array<number>|Uint8Array} unpacked - 169 bytes of unpacked data (159 parameters + 10 name bytes)
642
+ * @param {number} index - Voice index
643
+ * @returns {DX7Voice}
644
+ */
645
+ static fromUnpacked(unpacked, index = 0) {
646
+ const packed = DX7Voice.pack(unpacked)
647
+ return new DX7Voice(packed, index)
648
+ }
649
+
650
+ /**
651
+ * Load a DX7 voice from a single voice SYX file
652
+ * @param {File|Blob} file - SYX file (single voice in VCED format)
653
+ * @returns {Promise<DX7Voice>}
654
+ * @throws {DX7ParseError} If file has invalid VCED header
655
+ * @throws {Error} If file cannot be read (FileReader error)
656
+ */
657
+ static async fromFile(file) {
658
+ return new Promise((resolve, reject) => {
659
+ const reader = new FileReader()
660
+ reader.onload = (e) => {
661
+ try {
662
+ const bytes = new Uint8Array(e.target.result)
663
+
664
+ // Verify VCED header
665
+ if (
666
+ bytes[0] !== DX7Voice.VCED_SYSEX_START ||
667
+ bytes[1] !== DX7Voice.VCED_YAMAHA_ID ||
668
+ bytes[2] !== DX7Voice.VCED_SUB_STATUS ||
669
+ bytes[3] !== DX7Voice.VCED_FORMAT_SINGLE ||
670
+ bytes[4] !== DX7Voice.VCED_BYTE_COUNT_MSB ||
671
+ bytes[5] !== DX7Voice.VCED_BYTE_COUNT_LSB
672
+ ) {
673
+ throw new DX7ParseError("Invalid VCED header", "header", 0)
674
+ }
675
+
676
+ // Extract the 155 bytes of voice data
677
+ const voiceData = bytes.subarray(
678
+ DX7Voice.VCED_HEADER_SIZE,
679
+ DX7Voice.VCED_HEADER_SIZE + DX7Voice.VCED_DATA_SIZE,
680
+ )
681
+
682
+ // Verify checksum
683
+ const checksum = bytes[DX7Voice.VCED_HEADER_SIZE + DX7Voice.VCED_DATA_SIZE]
684
+ const calculatedChecksum = DX7Bank._calculateChecksum(voiceData, DX7Voice.VCED_DATA_SIZE)
685
+
686
+ if (checksum !== calculatedChecksum) {
687
+ console.warn(
688
+ `DX7 VCED checksum mismatch (expected ${calculatedChecksum.toString(16)}, got ${checksum.toString(16)}). This is common with vintage SysEx files.`,
689
+ )
690
+ }
691
+
692
+ // Convert VCED data to unpacked format (169 bytes)
693
+ const unpacked = new Uint8Array(DX7Voice.UNPACKED_SIZE)
694
+
695
+ let offset = 0
696
+
697
+ // Operators: 6 × 21 bytes = 126 bytes
698
+ // VCED stores operators in reverse order: OP6, OP5, OP4, OP3, OP2, OP1
699
+ for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
700
+ const dst = (DX7Voice.NUM_OPERATORS - 1 - op) * DX7Voice.UNPACKED_OP_SIZE
701
+
702
+ // Copy operator parameters
703
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_1] = voiceData[offset++]
704
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_2] = voiceData[offset++]
705
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_3] = voiceData[offset++]
706
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_4] = voiceData[offset++]
707
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_1] = voiceData[offset++]
708
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_2] = voiceData[offset++]
709
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_3] = voiceData[offset++]
710
+ unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_4] = voiceData[offset++]
711
+ unpacked[dst + DX7Voice.UNPACKED_OP_BREAK_POINT] = voiceData[offset++]
712
+ unpacked[dst + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH] = voiceData[offset++]
713
+ unpacked[dst + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH] = voiceData[offset++]
714
+ unpacked[dst + DX7Voice.UNPACKED_OP_L_CURVE] = voiceData[offset++]
715
+ unpacked[dst + DX7Voice.UNPACKED_OP_R_CURVE] = voiceData[offset++]
716
+ unpacked[dst + DX7Voice.UNPACKED_OP_RATE_SCALING] = voiceData[offset++]
717
+ unpacked[dst + DX7Voice.UNPACKED_OP_DETUNE] = voiceData[offset++]
718
+ // Amp mod sensitivity and key velocity sensitivity are packed in VCED
719
+ const modSens = voiceData[offset++]
720
+ unpacked[dst + DX7Voice.UNPACKED_OP_AMP_MOD_SENS] = modSens & DX7Voice.MASK_2BIT
721
+ unpacked[dst + DX7Voice.UNPACKED_OP_KEY_VEL_SENS] = (modSens >> 2) & DX7Voice.MASK_3BIT
722
+ unpacked[dst + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL] = voiceData[offset++]
723
+ unpacked[dst + DX7Voice.UNPACKED_OP_MODE] = voiceData[offset++]
724
+ unpacked[dst + DX7Voice.UNPACKED_OP_FREQ_COARSE] = voiceData[offset++]
725
+ // FREQ_FINE and OSC_DETUNE order swapped from unpacked format
726
+ unpacked[dst + DX7Voice.UNPACKED_OP_FREQ_FINE] = voiceData[offset++]
727
+ unpacked[dst + DX7Voice.UNPACKED_OP_OSC_DETUNE] = voiceData[offset++]
728
+ }
729
+
730
+ // Pitch EG: 8 bytes
731
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1] = voiceData[offset++]
732
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2] = voiceData[offset++]
733
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3] = voiceData[offset++]
734
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4] = voiceData[offset++]
735
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1] = voiceData[offset++]
736
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2] = voiceData[offset++]
737
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3] = voiceData[offset++]
738
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4] = voiceData[offset++]
739
+
740
+ // Algorithm and global parameters: 11 bytes
741
+ unpacked[DX7Voice.UNPACKED_ALGORITHM] = voiceData[offset++]
742
+ unpacked[DX7Voice.UNPACKED_FEEDBACK] = voiceData[offset++]
743
+ unpacked[DX7Voice.UNPACKED_OSC_SYNC] = voiceData[offset++]
744
+ unpacked[DX7Voice.UNPACKED_LFO_SPEED] = voiceData[offset++]
745
+ unpacked[DX7Voice.UNPACKED_LFO_DELAY] = voiceData[offset++]
746
+ unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH] = voiceData[offset++]
747
+ unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH] = voiceData[offset++]
748
+ unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] = voiceData[offset++]
749
+ unpacked[DX7Voice.UNPACKED_LFO_WAVE] = voiceData[offset++]
750
+ unpacked[DX7Voice.UNPACKED_LFO_PM_SENS] = voiceData[offset++]
751
+ unpacked[DX7Voice.UNPACKED_TRANSPOSE] = voiceData[offset++]
752
+
753
+ // Voice name: 10 bytes
754
+ for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
755
+ unpacked[DX7Voice.UNPACKED_NAME_START + i] = voiceData[offset++]
756
+ }
757
+
758
+ // Pack to 128-byte format
759
+ const packed = DX7Voice.pack(unpacked)
760
+ resolve(new DX7Voice(packed, 0))
761
+ } catch (err) {
762
+ reject(err)
763
+ }
764
+ }
765
+ reader.onerror = () => reject(new Error("Failed to read file"))
766
+ reader.readAsArrayBuffer(file)
767
+ })
768
+ }
769
+
770
+ /**
771
+ * Export voice to DX7 single voice SysEx format (VCED format)
772
+ * This is useful for synths that only support single voice dumps (e.g., KORG Volca FM)
773
+ * Converts from 169-byte unpacked format to 155-byte VCED format
774
+ * @returns {Uint8Array} Single voice SysEx data (163 bytes)
775
+ */
776
+ toSysEx() {
777
+ const unpacked = this.unpack()
778
+ const result = new Uint8Array(DX7Voice.VCED_SIZE)
779
+ let offset = 0
780
+
781
+ // DX7 single voice dump header
782
+ result[offset++] = DX7Voice.VCED_SYSEX_START
783
+ result[offset++] = DX7Voice.VCED_YAMAHA_ID
784
+ result[offset++] = DX7Voice.VCED_SUB_STATUS
785
+ result[offset++] = DX7Voice.VCED_FORMAT_SINGLE
786
+ result[offset++] = DX7Voice.VCED_BYTE_COUNT_MSB
787
+ result[offset++] = DX7Voice.VCED_BYTE_COUNT_LSB
788
+
789
+ // Convert operators: 6 × 21 bytes = 126 bytes
790
+ // VCED expects operators in reverse order: OP6, OP5, OP4, OP3, OP2, OP1
791
+ for (let op = DX7Voice.NUM_OPERATORS - 1; op >= 0; op--) {
792
+ const src = op * DX7Voice.UNPACKED_OP_SIZE
793
+
794
+ // Copy 21 bytes per operator, skipping bytes 18 and 22 in our format
795
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_1]
796
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_2]
797
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_3]
798
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_4]
799
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_1]
800
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_2]
801
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_3]
802
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_4]
803
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_BREAK_POINT]
804
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH]
805
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH]
806
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_L_CURVE]
807
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_R_CURVE]
808
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_RATE_SCALING]
809
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_DETUNE]
810
+ // Pack amp mod sensitivity and key velocity sensitivity for VCED
811
+ const ams = unpacked[src + DX7Voice.UNPACKED_OP_AMP_MOD_SENS] & DX7Voice.MASK_2BIT
812
+ const kvs = unpacked[src + DX7Voice.UNPACKED_OP_KEY_VEL_SENS] & DX7Voice.MASK_3BIT
813
+ result[offset++] = ams | (kvs << 2)
814
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL]
815
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_MODE]
816
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_FREQ_COARSE]
817
+ // VCED has OSC_DETUNE before FREQ_FINE (opposite of unpacked format)
818
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_OSC_DETUNE]
819
+ result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_FREQ_FINE]
820
+ }
821
+
822
+ // Pitch EG: 8 bytes (Rates 1-4, Levels 1-4)
823
+ result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1]
824
+ result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2]
825
+ result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3]
826
+ result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4]
827
+ result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1]
828
+ result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2]
829
+ result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3]
830
+ result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4]
831
+
832
+ // Algorithm and global parameters: 11 bytes
833
+ result[offset++] = unpacked[DX7Voice.UNPACKED_ALGORITHM]
834
+ result[offset++] = unpacked[DX7Voice.UNPACKED_FEEDBACK]
835
+ result[offset++] = unpacked[DX7Voice.UNPACKED_OSC_SYNC]
836
+ result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_SPEED]
837
+ result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_DELAY]
838
+ result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH]
839
+ result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH]
840
+ result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC]
841
+ result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_WAVE]
842
+ result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_PM_SENS]
843
+ result[offset++] = unpacked[DX7Voice.UNPACKED_TRANSPOSE]
844
+
845
+ // Voice name: 10 bytes
846
+ for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
847
+ result[offset++] = unpacked[DX7Voice.UNPACKED_NAME_START + i]
848
+ }
849
+
850
+ // Calculate checksum on 155 bytes of data
851
+ const dataForChecksum = result.subarray(
852
+ DX7Voice.VCED_HEADER_SIZE,
853
+ DX7Voice.VCED_HEADER_SIZE + DX7Voice.VCED_DATA_SIZE,
854
+ )
855
+ result[offset++] = DX7Bank._calculateChecksum(dataForChecksum, DX7Voice.VCED_DATA_SIZE)
856
+
857
+ // SysEx end
858
+ result[offset++] = DX7Voice.VCED_SYSEX_END
859
+
860
+ return result
861
+ }
862
+
863
+ /**
864
+ * Convert voice to JSON format
865
+ * @returns {object} Voice data in JSON format
866
+ */
867
+ toJSON() {
868
+ const unpacked = this.unpack()
869
+ const operators = []
870
+
871
+ // Helper function to get key scale curve string
872
+ const getKeyScaleCurve = (value) => {
873
+ const curves = ["-LN", "-EX", "+EX", "+LN"]
874
+ return curves[value] || "UNKNOWN"
875
+ }
876
+
877
+ // Helper function to get LFO wave string
878
+ const getLFOWave = (value) => {
879
+ const waves = ["TRIANGLE", "SAW DOWN", "SAW UP", "SQUARE", "SINE", "SAMPLE & HOLD"]
880
+ return waves[value] || "UNKNOWN"
881
+ }
882
+
883
+ // Helper function to convert MIDI note to note name
884
+ const getNoteName = (midiNote) => {
885
+ const notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
886
+ const octave = Math.floor(midiNote / 12) + DX7Voice.MIDI_OCTAVE_OFFSET
887
+ const note = notes[midiNote % 12]
888
+ return `${note}${octave}`
889
+ }
890
+
891
+ // Extract operator data
892
+ for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
893
+ const opOffset = op * DX7Voice.UNPACKED_OP_SIZE
894
+ const mode = unpacked[opOffset + DX7Voice.UNPACKED_OP_MODE] === 0 ? "RATIO" : "FIXED"
895
+
896
+ operators.push({
897
+ id: op + 1,
898
+ osc: {
899
+ detune: unpacked[opOffset + DX7Voice.UNPACKED_OP_OSC_DETUNE],
900
+ freq: {
901
+ coarse: unpacked[opOffset + DX7Voice.UNPACKED_OP_FREQ_COARSE],
902
+ fine: unpacked[opOffset + DX7Voice.UNPACKED_OP_FREQ_FINE],
903
+ mode: mode,
904
+ },
905
+ },
906
+ eg: {
907
+ rates: [
908
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_1],
909
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_2],
910
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_3],
911
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_4],
912
+ ],
913
+ levels: [
914
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_1],
915
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_2],
916
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_3],
917
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_4],
918
+ ],
919
+ },
920
+ key: {
921
+ velocity: unpacked[opOffset + DX7Voice.UNPACKED_OP_KEY_VEL_SENS],
922
+ scaling: unpacked[opOffset + DX7Voice.UNPACKED_OP_RATE_SCALING],
923
+ breakPoint: getNoteName(
924
+ unpacked[opOffset + DX7Voice.UNPACKED_OP_BREAK_POINT] +
925
+ DX7Voice.MIDI_BREAK_POINT_OFFSET,
926
+ ),
927
+ },
928
+ output: {
929
+ level: unpacked[opOffset + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL],
930
+ ampModSens: unpacked[opOffset + DX7Voice.UNPACKED_OP_AMP_MOD_SENS],
931
+ },
932
+ scale: {
933
+ left: {
934
+ depth: unpacked[opOffset + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH],
935
+ curve: getKeyScaleCurve(unpacked[opOffset + DX7Voice.UNPACKED_OP_L_CURVE]),
936
+ },
937
+ right: {
938
+ depth: unpacked[opOffset + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH],
939
+ curve: getKeyScaleCurve(unpacked[opOffset + DX7Voice.UNPACKED_OP_R_CURVE]),
940
+ },
941
+ },
942
+ })
943
+ }
944
+
945
+ return {
946
+ name: this.name || "(Empty)",
947
+ operators: operators,
948
+ pitchEG: {
949
+ rates: [
950
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1],
951
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2],
952
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3],
953
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4],
954
+ ],
955
+ levels: [
956
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1],
957
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2],
958
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3],
959
+ unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4],
960
+ ],
961
+ },
962
+ lfo: {
963
+ speed: unpacked[DX7Voice.UNPACKED_LFO_SPEED],
964
+ delay: unpacked[DX7Voice.UNPACKED_LFO_DELAY],
965
+ pmDepth: unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH],
966
+ amDepth: unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH],
967
+ keySync: unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] === 1,
968
+ wave: getLFOWave(unpacked[DX7Voice.UNPACKED_LFO_WAVE]),
969
+ },
970
+ global: {
971
+ algorithm: unpacked[DX7Voice.UNPACKED_ALGORITHM] + 1,
972
+ feedback: unpacked[DX7Voice.UNPACKED_FEEDBACK],
973
+ oscKeySync: unpacked[DX7Voice.UNPACKED_OSC_SYNC] === 1,
974
+ pitchModSens: unpacked[DX7Voice.UNPACKED_LFO_PM_SENS],
975
+ transpose: unpacked[DX7Voice.UNPACKED_TRANSPOSE] - DX7Voice.TRANSPOSE_CENTER,
976
+ },
977
+ }
978
+ }
979
+ }
980
+
981
+ /**
982
+ * DX7Bank - Represents a DX7 bank loaded from a SYX file
983
+ * Contains 32 voices in the packed 128-byte format
984
+ */
985
+ export class DX7Bank {
986
+ // SysEx header
987
+ static SYSEX_START = 0xf0
988
+ static SYSEX_END = 0xf7
989
+ static SYSEX_YAMAHA_ID = 0x43
990
+ static SYSEX_SUB_STATUS = 0x00
991
+ static SYSEX_FORMAT_32_VOICES = 0x09
992
+ static SYSEX_BYTE_COUNT_MSB = 0x20
993
+ static SYSEX_BYTE_COUNT_LSB = 0x00
994
+ static SYSEX_HEADER = [
995
+ DX7Bank.SYSEX_START,
996
+ DX7Bank.SYSEX_YAMAHA_ID,
997
+ DX7Bank.SYSEX_SUB_STATUS,
998
+ DX7Bank.SYSEX_FORMAT_32_VOICES,
999
+ DX7Bank.SYSEX_BYTE_COUNT_MSB,
1000
+ DX7Bank.SYSEX_BYTE_COUNT_LSB,
1001
+ ]
1002
+ static SYSEX_HEADER_SIZE = 6
1003
+
1004
+ // Bank structure
1005
+ static VOICE_DATA_SIZE = 4096 // 32 voices × 128 bytes
1006
+ static SYSEX_SIZE = 4104 // Header(6) + Data(4096) + Checksum(1) + End(1)
1007
+ static VOICE_SIZE = 128 // Bytes per voice in packed format
1008
+ static NUM_VOICES = 32
1009
+
1010
+ // Checksum
1011
+ static CHECKSUM_MODULO = 128
1012
+ static MASK_7BIT = 0x7f
1013
+
1014
+ /**
1015
+ * Create a DX7Bank
1016
+ * @param {Array<number>|ArrayBuffer|Uint8Array} data - Bank SYX data (optional)
1017
+ * @param {string} name - Optional bank name (e.g., filename)
1018
+ */
1019
+ constructor(data, name = "") {
1020
+ this.voices = new Array(DX7Bank.NUM_VOICES)
1021
+ this.name = name
1022
+
1023
+ if (data) {
1024
+ // Load existing data
1025
+ this._load(data)
1026
+ } else {
1027
+ // Create empty bank with default voices
1028
+ for (let i = 0; i < DX7Bank.NUM_VOICES; i++) {
1029
+ this.voices[i] = DX7Voice.createDefault(i)
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ /**
1035
+ * Calculate DX7 SysEx checksum
1036
+ * @private
1037
+ * @param {Uint8Array} data - Data to checksum
1038
+ * @param {number} size - Number of bytes
1039
+ * @returns {number} Checksum byte
1040
+ */
1041
+ static _calculateChecksum(data, size) {
1042
+ let sum = 0
1043
+ for (let i = 0; i < size; i++) {
1044
+ sum += data[i]
1045
+ }
1046
+ return (DX7Bank.CHECKSUM_MODULO - (sum % DX7Bank.CHECKSUM_MODULO)) & DX7Bank.MASK_7BIT
1047
+ }
1048
+
1049
+ /**
1050
+ * Load and validate bank data
1051
+ * @private
1052
+ * @param {Array<number>|ArrayBuffer|Uint8Array} data
1053
+ */
1054
+ _load(data) {
1055
+ // Convert to Uint8Array if needed
1056
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data)
1057
+
1058
+ // Check if we have raw voice data or full SysEx
1059
+ let voiceData
1060
+ let offset = 0
1061
+
1062
+ // Remove SysEx wrapper if present
1063
+ if (bytes[0] === DX7Bank.SYSEX_START) {
1064
+ // Verify header
1065
+ const header = bytes.subarray(0, DX7Bank.SYSEX_HEADER_SIZE)
1066
+ const expectedHeader = DX7Bank.SYSEX_HEADER
1067
+
1068
+ for (let i = 0; i < DX7Bank.SYSEX_HEADER_SIZE; i++) {
1069
+ if (header[i] !== expectedHeader[i]) {
1070
+ throw new DX7ParseError(
1071
+ `Invalid SysEx header at position ${i}: expected ${expectedHeader[i].toString(16)}, got ${header[i].toString(16)}`,
1072
+ "header",
1073
+ i,
1074
+ )
1075
+ }
1076
+ }
1077
+
1078
+ // Extract voice data (skip header and footer)
1079
+ voiceData = bytes.subarray(
1080
+ DX7Bank.SYSEX_HEADER_SIZE,
1081
+ DX7Bank.SYSEX_HEADER_SIZE + DX7Bank.VOICE_DATA_SIZE,
1082
+ )
1083
+ offset = DX7Bank.SYSEX_HEADER_SIZE
1084
+ } else if (bytes.length === DX7Bank.VOICE_DATA_SIZE) {
1085
+ // Raw voice data, no SysEx wrapper
1086
+ voiceData = bytes
1087
+ } else {
1088
+ throw new DX7ValidationError(
1089
+ `Invalid data length: expected ${DX7Bank.VOICE_DATA_SIZE} or ${DX7Bank.SYSEX_SIZE} bytes, got ${bytes.length}`,
1090
+ "length",
1091
+ bytes.length,
1092
+ )
1093
+ }
1094
+
1095
+ // Verify total size
1096
+ if (voiceData.length !== DX7Bank.VOICE_DATA_SIZE) {
1097
+ throw new DX7ValidationError(
1098
+ `Invalid voice data length: expected ${DX7Bank.VOICE_DATA_SIZE} bytes, got ${voiceData.length}`,
1099
+ "length",
1100
+ voiceData.length,
1101
+ )
1102
+ }
1103
+
1104
+ // Validate checksum if we have SysEx wrapper
1105
+ const checksumOffset = DX7Bank.SYSEX_HEADER_SIZE + DX7Bank.VOICE_DATA_SIZE
1106
+ if (offset > 0 && bytes.length >= checksumOffset + 1) {
1107
+ const checksum = bytes[checksumOffset]
1108
+ const calculatedChecksum = DX7Bank._calculateChecksum(voiceData, DX7Bank.VOICE_DATA_SIZE)
1109
+
1110
+ if (checksum !== calculatedChecksum) {
1111
+ console.warn(
1112
+ `DX7 checksum mismatch (expected ${calculatedChecksum.toString(16)}, got ${checksum.toString(16)}). ` +
1113
+ `This is common with vintage SysEx files and the data is likely still valid.`,
1114
+ )
1115
+ }
1116
+ }
1117
+
1118
+ // Extract voices
1119
+ this.voices = new Array(DX7Bank.NUM_VOICES)
1120
+ for (let i = 0; i < DX7Bank.NUM_VOICES; i++) {
1121
+ const voiceStart = i * DX7Bank.VOICE_SIZE
1122
+ const singleVoiceData = voiceData.subarray(voiceStart, voiceStart + DX7Bank.VOICE_SIZE)
1123
+ this.voices[i] = new DX7Voice(singleVoiceData, i)
1124
+ }
1125
+ }
1126
+
1127
+ /**
1128
+ * Replace a voice at the specified index
1129
+ * @param {number} index - Voice index (0-31)
1130
+ * @param {DX7Voice} voice - Voice to insert
1131
+ * @throws {DX7ValidationError} If index is out of range
1132
+ */
1133
+ replaceVoice(index, voice) {
1134
+ if (index < 0 || index >= DX7Bank.NUM_VOICES) {
1135
+ throw new DX7ValidationError(`Invalid voice index: ${index}`, "index", index)
1136
+ }
1137
+
1138
+ // Create a copy of the voice with the correct index
1139
+ const voiceData = new Uint8Array(voice.data)
1140
+ this.voices[index] = new DX7Voice(voiceData, index)
1141
+ }
1142
+
1143
+ /**
1144
+ * Add a voice to the first empty slot
1145
+ * @param {DX7Voice} voice - Voice to add
1146
+ * @returns {number} Index where voice was added, or -1 if bank is full
1147
+ */
1148
+ addVoice(voice) {
1149
+ for (let i = 0; i < this.voices.length; i++) {
1150
+ const currentPatch = this.voices[i]
1151
+ // Check if slot is empty (all zeros or default voice)
1152
+ const isEmpty = currentPatch.name === "" || currentPatch.name === "Init Voice"
1153
+ if (isEmpty) {
1154
+ this.replaceVoice(i, voice)
1155
+ return i
1156
+ }
1157
+ }
1158
+ return -1
1159
+ }
1160
+
1161
+ /**
1162
+ * Get all voices in the bank
1163
+ * @returns {DX7Voice[]}
1164
+ */
1165
+ getVoices() {
1166
+ return this.voices
1167
+ }
1168
+
1169
+ /**
1170
+ * Get a specific voice by index
1171
+ * @param {number} index - Voice index (0-31)
1172
+ * @returns {DX7Voice|null}
1173
+ */
1174
+ getVoice(index) {
1175
+ if (index < 0 || index >= this.voices.length) {
1176
+ return null
1177
+ }
1178
+ return this.voices[index]
1179
+ }
1180
+
1181
+ /**
1182
+ * Get all voice names
1183
+ * @returns {string[]}
1184
+ */
1185
+ getVoiceNames() {
1186
+ return this.voices.map((voice) => voice.name)
1187
+ }
1188
+
1189
+ /**
1190
+ * Find a voice by name (case-insensitive, partial match)
1191
+ * @param {string} name - Voice name to search for
1192
+ * @returns {DX7Voice|null}
1193
+ */
1194
+ findVoiceByName(name) {
1195
+ const lowerName = name.toLowerCase()
1196
+ return this.voices.find((voice) => voice.name.toLowerCase().includes(lowerName)) || null
1197
+ }
1198
+
1199
+ /**
1200
+ * Load a DX7 bank from a file
1201
+ * @param {File|Blob} file - SYX file to load
1202
+ * @returns {Promise<DX7Bank>}
1203
+ * @throws {DX7ParseError} If file is a single voice file
1204
+ * @throws {DX7ValidationError} If data is not valid DX7 SYX format
1205
+ * @throws {Error} If file cannot be read (FileReader error)
1206
+ */
1207
+ static async fromFile(file) {
1208
+ return new Promise((resolve, reject) => {
1209
+ const reader = new FileReader()
1210
+ reader.onload = async (e) => {
1211
+ try {
1212
+ const fileName = file.name || ""
1213
+ const bytes = new Uint8Array(e.target.result)
1214
+
1215
+ // Check if it's a single voice file (VCED format)
1216
+ // Single voice files have format byte 0x00, banks have 0x09
1217
+ if (bytes[0] === DX7Bank.SYSEX_START && bytes[3] === DX7Voice.VCED_FORMAT_SINGLE) {
1218
+ // This is a single voice file - DX7Bank is for banks only
1219
+ reject(
1220
+ new DX7ParseError(
1221
+ "This is a single voice file. Use DX7Voice.fromFile() instead.",
1222
+ "format",
1223
+ 3,
1224
+ ),
1225
+ )
1226
+ } else {
1227
+ // This is a bank file - strip file extension from name
1228
+ const bankName = fileName.replace(/\.[^/.]+$/, "")
1229
+ const bank = new DX7Bank(e.target.result, bankName)
1230
+ resolve(bank)
1231
+ }
1232
+ } catch (err) {
1233
+ reject(err)
1234
+ }
1235
+ }
1236
+ reader.onerror = () => reject(new Error("Failed to read file"))
1237
+ reader.readAsArrayBuffer(file)
1238
+ })
1239
+ }
1240
+
1241
+ /**
1242
+ * Export bank to SysEx format
1243
+ * @returns {Uint8Array} Full SysEx data (4104 bytes)
1244
+ */
1245
+ toSysEx() {
1246
+ const result = new Uint8Array(DX7Bank.SYSEX_SIZE)
1247
+ let offset = 0
1248
+
1249
+ // Header
1250
+ DX7Bank.SYSEX_HEADER.forEach((byte) => {
1251
+ result[offset++] = byte
1252
+ })
1253
+
1254
+ // Voice data (all voices)
1255
+ for (const voice of this.voices) {
1256
+ for (let i = 0; i < DX7Bank.VOICE_SIZE; i++) {
1257
+ result[offset++] = voice.data[i]
1258
+ }
1259
+ }
1260
+
1261
+ // Checksum
1262
+ const voiceData = result.subarray(
1263
+ DX7Bank.SYSEX_HEADER_SIZE,
1264
+ DX7Bank.SYSEX_HEADER_SIZE + DX7Bank.VOICE_DATA_SIZE,
1265
+ )
1266
+ result[offset++] = DX7Bank._calculateChecksum(voiceData, DX7Bank.VOICE_DATA_SIZE)
1267
+
1268
+ // SysEx end
1269
+ result[offset++] = DX7Bank.SYSEX_END
1270
+
1271
+ return result
1272
+ }
1273
+
1274
+ /**
1275
+ * Convert bank to JSON format
1276
+ * @returns {object} Bank data in JSON format
1277
+ */
1278
+ toJSON() {
1279
+ const voices = this.voices.map((voice, index) => {
1280
+ const jsonPatch = voice.toJSON()
1281
+ // Voice indices are 0-based internally, but show as 1-32 to users
1282
+ return {
1283
+ index: index + 1,
1284
+ ...jsonPatch,
1285
+ }
1286
+ })
1287
+
1288
+ return {
1289
+ version: "1.0",
1290
+ name: this.name || "",
1291
+ voices: voices,
1292
+ }
1293
+ }
1294
+ }