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
package/src/utils/dx7.js
ADDED
|
@@ -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
|
+
}
|