midiwire 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +845 -0
- package/dist/midiwire.es.js +1987 -0
- package/dist/midiwire.umd.js +1 -0
- package/package.json +58 -0
- package/src/bindings/DataAttributeBinder.js +198 -0
- package/src/bindings/DataAttributeBinder.test.js +825 -0
- package/src/core/EventEmitter.js +93 -0
- package/src/core/EventEmitter.test.js +357 -0
- package/src/core/MIDIConnection.js +364 -0
- package/src/core/MIDIConnection.test.js +783 -0
- package/src/core/MIDIController.js +756 -0
- package/src/core/MIDIController.test.js +1958 -0
- package/src/core/MIDIDeviceManager.js +204 -0
- package/src/core/MIDIDeviceManager.test.js +638 -0
- package/src/core/errors.js +99 -0
- package/src/index.js +181 -0
- package/src/utils/dx7.js +1294 -0
- package/src/utils/dx7.test.js +1208 -0
- package/src/utils/midi.js +244 -0
- package/src/utils/midi.test.js +260 -0
- package/src/utils/sysex.js +98 -0
- package/src/utils/sysex.test.js +222 -0
- package/src/utils/validators.js +88 -0
- package/src/utils/validators.test.js +300 -0
|
@@ -0,0 +1,1208 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest"
|
|
2
|
+
import { DX7Bank, DX7Voice } from "./dx7.js"
|
|
3
|
+
|
|
4
|
+
describe("DX7Voice", () => {
|
|
5
|
+
describe("constructor", () => {
|
|
6
|
+
it("should create a voice from 128 bytes of data", () => {
|
|
7
|
+
const data = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
8
|
+
const voice = new DX7Voice(data, 5)
|
|
9
|
+
|
|
10
|
+
expect(voice.index).toBe(5)
|
|
11
|
+
expect(voice.data.length).toBe(DX7Voice.PACKED_SIZE)
|
|
12
|
+
expect(voice.name).toBe("")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("should throw error for invalid data length", () => {
|
|
16
|
+
expect(() => new DX7Voice([1, 2, 3])).toThrow("Invalid voice data length")
|
|
17
|
+
expect(() => new DX7Voice(new Array(DX7Voice.PACKED_SIZE - 1).fill(0))).toThrow(
|
|
18
|
+
"Invalid voice data length",
|
|
19
|
+
)
|
|
20
|
+
expect(() => new DX7Voice(new Array(DX7Voice.PACKED_SIZE + 1).fill(0))).toThrow(
|
|
21
|
+
"Invalid voice data length",
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("should extract voice name correctly", () => {
|
|
26
|
+
const data = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
27
|
+
// Set name bytes (offset 118-127) to 'TEST VOICE'
|
|
28
|
+
const name = "TEST VOICE"
|
|
29
|
+
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
|
|
30
|
+
data[DX7Voice.PACKED_NAME_START + i] = name.charCodeAt(i) || DX7Voice.CHAR_SPACE
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const voice = new DX7Voice(data)
|
|
34
|
+
expect(voice.name).toBe("TEST VOICE")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("should normalize DX7 special characters", () => {
|
|
38
|
+
const data = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
39
|
+
// Set special characters
|
|
40
|
+
data[DX7Voice.PACKED_NAME_START] = DX7Voice.CHAR_YEN
|
|
41
|
+
data[DX7Voice.PACKED_NAME_START + 1] = DX7Voice.CHAR_ARROW_RIGHT
|
|
42
|
+
data[DX7Voice.PACKED_NAME_START + 2] = DX7Voice.CHAR_ARROW_LEFT
|
|
43
|
+
data[121] = DX7Voice.CHAR_SPACE
|
|
44
|
+
|
|
45
|
+
const voice = new DX7Voice(data)
|
|
46
|
+
expect(voice.name).toBe("Y><") // Position 121 is space, trim removes trailing spaces
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe("getParameter and setParameter", () => {
|
|
51
|
+
it("should get and set parameters correctly", () => {
|
|
52
|
+
const data = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
53
|
+
const voice = new DX7Voice(data)
|
|
54
|
+
|
|
55
|
+
// Set parameter at offset 10 to value 64
|
|
56
|
+
voice.setParameter(10, 64)
|
|
57
|
+
expect(voice.getParameter(10)).toBe(64)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("should mask values to 7-bit range", () => {
|
|
61
|
+
const data = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
62
|
+
const voice = new DX7Voice(data)
|
|
63
|
+
|
|
64
|
+
voice.setParameter(10, 200) // Should be masked to 72 (200 & DX7Voice.MASK_7BIT)
|
|
65
|
+
expect(voice.getParameter(10)).toBe(72)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("should update name when name bytes change", () => {
|
|
69
|
+
const data = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
70
|
+
const voice = new DX7Voice(data)
|
|
71
|
+
|
|
72
|
+
// Set name bytes
|
|
73
|
+
voice.setParameter(DX7Voice.PACKED_NAME_START, "A".charCodeAt(0))
|
|
74
|
+
voice.setParameter(DX7Voice.PACKED_NAME_START + 1, "B".charCodeAt(0))
|
|
75
|
+
voice.setParameter(DX7Voice.PACKED_NAME_START + 2, "C".charCodeAt(0))
|
|
76
|
+
|
|
77
|
+
expect(voice.name).toBe("ABC")
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe("getUnpackedParameter", () => {
|
|
82
|
+
it("should read algorithm from unpacked data", () => {
|
|
83
|
+
const data = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
84
|
+
// Algorithm is at packed[110], maps to unpacked[146]
|
|
85
|
+
data[DX7Voice.OFFSET_ALGORITHM] = 5 // Algorithm 6 (0-indexed)
|
|
86
|
+
|
|
87
|
+
const voice = new DX7Voice(data)
|
|
88
|
+
expect(voice.getUnpackedParameter(DX7Voice.UNPACKED_ALGORITHM)).toBe(5)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("should read feedback from unpacked data", () => {
|
|
92
|
+
const data = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
93
|
+
// Feedback is at packed[111], maps to unpacked[147]
|
|
94
|
+
data[DX7Voice.OFFSET_FEEDBACK] = 7 | 0 // Max feedback, OSC Sync is 0
|
|
95
|
+
// Bit 3 is OSC Sync, bits 0-2 are Feedback
|
|
96
|
+
|
|
97
|
+
const voice = new DX7Voice(data)
|
|
98
|
+
expect(voice.getUnpackedParameter(DX7Voice.UNPACKED_FEEDBACK)).toBe(7)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("should read LFO speed from unpacked data", () => {
|
|
102
|
+
const data = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
103
|
+
// LFO Speed is at packed[112], maps to unpacked[149]
|
|
104
|
+
data[DX7Voice.OFFSET_LFO_SPEED] = 50
|
|
105
|
+
|
|
106
|
+
const voice = new DX7Voice(data)
|
|
107
|
+
expect(voice.getUnpackedParameter(DX7Voice.UNPACKED_LFO_SPEED)).toBe(50)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe("unpack", () => {
|
|
112
|
+
it("should unpack 128-byte data to 165-byte format", () => {
|
|
113
|
+
const data = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
114
|
+
const voice = new DX7Voice(data)
|
|
115
|
+
const unpacked = voice.unpack()
|
|
116
|
+
|
|
117
|
+
expect(unpacked.length).toBe(DX7Voice.UNPACKED_SIZE)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("should unpack with correct operator structure", () => {
|
|
121
|
+
const data = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
122
|
+
// DX7 stores operators in reverse order: OP6 at packed[0], OP1 at packed[85-101]
|
|
123
|
+
// Set OP1 values at packed[85-101]
|
|
124
|
+
data[85] = 10 // OP1 EG Rate 1 at packed[85]
|
|
125
|
+
data[89] = 20 // OP1 EG Level 1 at packed[89] (85 + 4)
|
|
126
|
+
// Set OP2 EG Rate 4 at packed[71] (packed[68-84] is OP2, EG rates are bytes 0-3)
|
|
127
|
+
data[71] = 30 // OP2 EG Rate 4 at packed[68+3]
|
|
128
|
+
|
|
129
|
+
const voice = new DX7Voice(data)
|
|
130
|
+
const unpacked = voice.unpack()
|
|
131
|
+
|
|
132
|
+
expect(unpacked[0]).toBe(10) // OP1 EG Rate 1 <- packed[85]
|
|
133
|
+
expect(unpacked[4]).toBe(20) // OP1 EG Level 1 <- packed[89]
|
|
134
|
+
// OP2 EG Rate 1-4: unpacked[23-26] <- packed[68-71]
|
|
135
|
+
expect(unpacked[23]).toBe(data[68]) // OP2 EG Rate 1 <- packed[68]
|
|
136
|
+
expect(unpacked[26]).toBe(30) // OP2 EG Rate 4 <- packed[71]
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe("pack", () => {
|
|
141
|
+
it("should pack 169-byte data to 128-byte format", () => {
|
|
142
|
+
const unpacked = new Array(DX7Voice.UNPACKED_SIZE).fill(0)
|
|
143
|
+
const packed = DX7Voice.pack(unpacked)
|
|
144
|
+
|
|
145
|
+
expect(packed.length).toBe(DX7Voice.PACKED_SIZE)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("should throw error for invalid unpacked length", () => {
|
|
149
|
+
expect(() => DX7Voice.pack(new Array(100).fill(0))).toThrow("Invalid unpacked data length")
|
|
150
|
+
expect(() => DX7Voice.pack(new Array(200).fill(0))).toThrow("Invalid unpacked data length")
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("should pack and unpack correctly", () => {
|
|
154
|
+
const originalData = new Array(DX7Voice.PACKED_SIZE).fill(0)
|
|
155
|
+
originalData[0] = 50
|
|
156
|
+
originalData[10] = 100
|
|
157
|
+
originalData[DX7Voice.OFFSET_ALGORITHM] = 5 // Algorithm 6 (packed[110])
|
|
158
|
+
|
|
159
|
+
const voice = new DX7Voice(originalData)
|
|
160
|
+
const unpacked = voice.unpack()
|
|
161
|
+
const repacked = DX7Voice.pack(unpacked)
|
|
162
|
+
|
|
163
|
+
expect(repacked.length).toBe(DX7Voice.PACKED_SIZE)
|
|
164
|
+
expect(repacked[0]).toBe(50)
|
|
165
|
+
expect(repacked[10]).toBe(100)
|
|
166
|
+
expect(repacked[DX7Voice.OFFSET_ALGORITHM]).toBe(5) // Algorithm preserved
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe("fromUnpacked", () => {
|
|
171
|
+
it("should create a voice from unpacked data", () => {
|
|
172
|
+
const unpacked = new Array(DX7Voice.UNPACKED_SIZE).fill(0)
|
|
173
|
+
unpacked[0] = 99 // OP1 EG Rate 1
|
|
174
|
+
unpacked[4] = 50 // OP1 EG Level 1
|
|
175
|
+
|
|
176
|
+
const voice = DX7Voice.fromUnpacked(unpacked, 3)
|
|
177
|
+
|
|
178
|
+
expect(voice.index).toBe(3)
|
|
179
|
+
expect(voice.data.length).toBe(DX7Voice.PACKED_SIZE)
|
|
180
|
+
// OP1 data goes to packed[85-101] (operators are stored in reverse order)
|
|
181
|
+
expect(voice.getParameter(85)).toBe(99) // OP1 EG Rate 1 at packed[85]
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe("createDefault", () => {
|
|
186
|
+
it("should create a default voice", () => {
|
|
187
|
+
const voice = DX7Voice.createDefault(7)
|
|
188
|
+
|
|
189
|
+
expect(voice.index).toBe(7)
|
|
190
|
+
expect(voice.data.length).toBe(DX7Voice.PACKED_SIZE)
|
|
191
|
+
expect(voice.name).toBe("Init Voice")
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it("should create a valid voice that can be unpacked", () => {
|
|
195
|
+
const voice = DX7Voice.createDefault()
|
|
196
|
+
const unpacked = voice.unpack()
|
|
197
|
+
|
|
198
|
+
expect(unpacked.length).toBe(DX7Voice.UNPACKED_SIZE) // 159 parameters + 10 name bytes
|
|
199
|
+
expect(unpacked[0]).toBe(99) // Default EG rate
|
|
200
|
+
// Note: Algorithm position varies based on createDefault implementation
|
|
201
|
+
// The key test is that pack/unpack round-trip works correctly
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe("DX7Bank", () => {
|
|
207
|
+
describe("constructor", () => {
|
|
208
|
+
it("should create empty bank with default voices", () => {
|
|
209
|
+
const bank = new DX7Bank()
|
|
210
|
+
|
|
211
|
+
expect(bank.voices.length).toBe(DX7Bank.NUM_VOICES)
|
|
212
|
+
expect(bank.getVoice(0).name).toBe("Init Voice")
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it("should load data from SYX format", () => {
|
|
216
|
+
// Create minimal valid SYX data
|
|
217
|
+
const data = new Uint8Array(DX7Bank.SYSEX_SIZE)
|
|
218
|
+
data[0] = 0xf0
|
|
219
|
+
DX7Bank.SYSEX_HEADER.forEach((byte, i) => {
|
|
220
|
+
data[i] = byte
|
|
221
|
+
})
|
|
222
|
+
// Fill with valid voice data
|
|
223
|
+
for (
|
|
224
|
+
let i = DX7Bank.SYSEX_HEADER_SIZE;
|
|
225
|
+
i < DX7Bank.SYSEX_HEADER_SIZE + DX7Bank.VOICE_DATA_SIZE;
|
|
226
|
+
i++
|
|
227
|
+
) {
|
|
228
|
+
data[i] = 0
|
|
229
|
+
}
|
|
230
|
+
data[DX7Bank.SYSEX_SIZE - 1] = DX7Bank.MASK_7BIT
|
|
231
|
+
|
|
232
|
+
const bank = new DX7Bank(data)
|
|
233
|
+
expect(bank.voices.length).toBe(DX7Bank.NUM_VOICES)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it("should throw error for invalid header", () => {
|
|
237
|
+
const data = new Uint8Array(DX7Bank.SYSEX_SIZE)
|
|
238
|
+
data[0] = 0xf0
|
|
239
|
+
data[1] = 0x42 // Wrong manufacturer
|
|
240
|
+
|
|
241
|
+
expect(() => new DX7Bank(data)).toThrow("Invalid SysEx header")
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it("should throw error for invalid length", () => {
|
|
245
|
+
expect(() => new DX7Bank(new Uint8Array(100))).toThrow("Invalid data length")
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
describe("_calculateChecksum", () => {
|
|
250
|
+
it("should calculate checksum correctly", () => {
|
|
251
|
+
const data = new Uint8Array([1, 2, 3, 4, 5])
|
|
252
|
+
const checksum = DX7Bank._calculateChecksum(data, 5)
|
|
253
|
+
|
|
254
|
+
// Manual calculation: sum = 15, checksum = 128 - 15 = 113
|
|
255
|
+
expect(checksum).toBe(113)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it("should handle large data", () => {
|
|
259
|
+
const data = new Uint8Array(DX7Bank.VOICE_DATA_SIZE).fill(0) // All zeros
|
|
260
|
+
const checksum = DX7Bank._calculateChecksum(data, 4096)
|
|
261
|
+
|
|
262
|
+
expect(checksum).toBe(0) // 128 - (0 % 128) = 128, masked to 0
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
describe("replaceVoice", () => {
|
|
267
|
+
it("should replace a voice at specified index", () => {
|
|
268
|
+
const bank = new DX7Bank()
|
|
269
|
+
const newPatch = DX7Voice.createDefault()
|
|
270
|
+
|
|
271
|
+
// Set a parameter to mark it as different
|
|
272
|
+
newPatch.setParameter(0, 42)
|
|
273
|
+
|
|
274
|
+
bank.replaceVoice(5, newPatch)
|
|
275
|
+
|
|
276
|
+
expect(bank.getVoice(5).getParameter(0)).toBe(42)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("should throw error for invalid index", () => {
|
|
280
|
+
const bank = new DX7Bank()
|
|
281
|
+
const voice = DX7Voice.createDefault()
|
|
282
|
+
|
|
283
|
+
expect(() => bank.replaceVoice(-1, voice)).toThrow("Invalid voice index")
|
|
284
|
+
expect(() => bank.replaceVoice(32, voice)).toThrow("Invalid voice index")
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it("should create a copy of the voice", () => {
|
|
288
|
+
const bank = new DX7Bank()
|
|
289
|
+
const originalPatch = DX7Voice.createDefault()
|
|
290
|
+
originalPatch.setParameter(0, 99)
|
|
291
|
+
|
|
292
|
+
bank.replaceVoice(0, originalPatch)
|
|
293
|
+
|
|
294
|
+
// Modify original voice
|
|
295
|
+
originalPatch.setParameter(0, 55)
|
|
296
|
+
|
|
297
|
+
// Bank voice should be unchanged
|
|
298
|
+
expect(bank.getVoice(0).getParameter(0)).toBe(99)
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
describe("addVoice", () => {
|
|
303
|
+
it("should add voice to first empty slot", () => {
|
|
304
|
+
const bank = new DX7Bank()
|
|
305
|
+
const newPatch = DX7Voice.createDefault()
|
|
306
|
+
newPatch.setParameter(0, 77)
|
|
307
|
+
|
|
308
|
+
const index = bank.addVoice(newPatch)
|
|
309
|
+
|
|
310
|
+
expect(index).toBe(0) // First slot is empty
|
|
311
|
+
expect(bank.getVoice(index).getParameter(0)).toBe(77)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it("should return -1 when bank is full", () => {
|
|
315
|
+
const bank = new DX7Bank()
|
|
316
|
+
const newPatch = DX7Voice.createDefault()
|
|
317
|
+
|
|
318
|
+
// Replace all voices with a named voice
|
|
319
|
+
for (let i = 0; i < DX7Bank.NUM_VOICES; i++) {
|
|
320
|
+
const voice = DX7Voice.createDefault()
|
|
321
|
+
voice.setParameter(DX7Voice.PACKED_NAME_START, "A".charCodeAt(0)) // Set name to 'A'
|
|
322
|
+
bank.replaceVoice(i, voice)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const index = bank.addVoice(newPatch)
|
|
326
|
+
expect(index).toBe(-1)
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
describe("getVoice", () => {
|
|
331
|
+
it("should return voice at valid index", () => {
|
|
332
|
+
const bank = new DX7Bank()
|
|
333
|
+
const voice = bank.getVoice(5)
|
|
334
|
+
|
|
335
|
+
expect(voice).not.toBeNull()
|
|
336
|
+
expect(voice.index).toBe(5)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it("should return null for invalid index", () => {
|
|
340
|
+
const bank = new DX7Bank()
|
|
341
|
+
|
|
342
|
+
expect(bank.getVoice(-1)).toBeNull()
|
|
343
|
+
expect(bank.getVoice(32)).toBeNull()
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
describe("getVoiceNames", () => {
|
|
348
|
+
it("should return all voice names", () => {
|
|
349
|
+
const bank = new DX7Bank()
|
|
350
|
+
const names = bank.getVoiceNames()
|
|
351
|
+
|
|
352
|
+
expect(names.length).toBe(DX7Bank.NUM_VOICES)
|
|
353
|
+
expect(names[0]).toBe("Init Voice")
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe("findVoiceByName", () => {
|
|
358
|
+
it("should find voice by exact name", () => {
|
|
359
|
+
const bank = new DX7Bank()
|
|
360
|
+
|
|
361
|
+
// Replace a voice with a named one
|
|
362
|
+
const voice = DX7Voice.createDefault()
|
|
363
|
+
const nameData = "SUPER BASS".split("").map((c) => c.charCodeAt(0))
|
|
364
|
+
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
|
|
365
|
+
voice.setParameter(DX7Voice.PACKED_NAME_START + i, nameData[i] || 32)
|
|
366
|
+
}
|
|
367
|
+
bank.replaceVoice(10, voice)
|
|
368
|
+
|
|
369
|
+
const found = bank.findVoiceByName("SUPER BASS")
|
|
370
|
+
expect(found).not.toBeNull()
|
|
371
|
+
expect(found.name).toBe("SUPER BASS")
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it("should find voice by partial name (case-insensitive)", () => {
|
|
375
|
+
const bank = new DX7Bank()
|
|
376
|
+
|
|
377
|
+
const voice = DX7Voice.createDefault()
|
|
378
|
+
const nameData = "E.PIANO 1".split("").map((c) => c.charCodeAt(0))
|
|
379
|
+
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
|
|
380
|
+
voice.setParameter(DX7Voice.PACKED_NAME_START + i, nameData[i] || 32)
|
|
381
|
+
}
|
|
382
|
+
bank.replaceVoice(15, voice)
|
|
383
|
+
|
|
384
|
+
const found = bank.findVoiceByName("piano")
|
|
385
|
+
expect(found).not.toBeNull()
|
|
386
|
+
expect(found.name).toBe("E.PIANO 1")
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it("should return null when voice not found", () => {
|
|
390
|
+
const bank = new DX7Bank()
|
|
391
|
+
|
|
392
|
+
const found = bank.findVoiceByName("NONEXISTENT")
|
|
393
|
+
expect(found).toBeNull()
|
|
394
|
+
})
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
describe("toSysex", () => {
|
|
398
|
+
it("should export to SYSEX format", () => {
|
|
399
|
+
const bank = new DX7Bank()
|
|
400
|
+
const sysex = bank.toSysEx()
|
|
401
|
+
|
|
402
|
+
expect(sysex.length).toBe(DX7Bank.SYSEX_SIZE)
|
|
403
|
+
expect(sysex[0]).toBe(DX7Bank.SYSEX_START)
|
|
404
|
+
expect(sysex[DX7Bank.SYSEX_SIZE - 1]).toBe(DX7Bank.SYSEX_END)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
it("should include correct header", () => {
|
|
408
|
+
const bank = new DX7Bank()
|
|
409
|
+
const sysex = bank.toSysEx()
|
|
410
|
+
const header = sysex.slice(0, 6)
|
|
411
|
+
|
|
412
|
+
expect(Array.from(header)).toEqual(DX7Bank.SYSEX_HEADER)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it("should maintain data integrity", () => {
|
|
416
|
+
const originalBank = new DX7Bank()
|
|
417
|
+
|
|
418
|
+
// Modify a voice
|
|
419
|
+
const voice = DX7Voice.createDefault()
|
|
420
|
+
voice.setParameter(0, 123)
|
|
421
|
+
// Clear all name bytes first
|
|
422
|
+
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
|
|
423
|
+
voice.setParameter(DX7Voice.PACKED_NAME_START + i, 32) // Space
|
|
424
|
+
}
|
|
425
|
+
// Set only first 2 chars
|
|
426
|
+
voice.setParameter(DX7Voice.PACKED_NAME_START, "A".charCodeAt(0))
|
|
427
|
+
voice.setParameter(DX7Voice.PACKED_NAME_START + 1, "B".charCodeAt(0))
|
|
428
|
+
originalBank.replaceVoice(7, voice)
|
|
429
|
+
|
|
430
|
+
// Export and reimport
|
|
431
|
+
const sysex = originalBank.toSysEx()
|
|
432
|
+
const newBank = new DX7Bank(sysex)
|
|
433
|
+
|
|
434
|
+
expect(newBank.getVoice(7).getParameter(0)).toBe(123)
|
|
435
|
+
expect(newBank.getVoice(7).name).toBe("AB")
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
describe("fromFile", () => {
|
|
440
|
+
it("should load bank from file", async () => {
|
|
441
|
+
const data = new Uint8Array(DX7Bank.SYSEX_SIZE)
|
|
442
|
+
data[0] = 0xf0
|
|
443
|
+
DX7Bank.SYSEX_HEADER.forEach((byte, i) => {
|
|
444
|
+
data[i] = byte
|
|
445
|
+
})
|
|
446
|
+
// Fill with valid voice data
|
|
447
|
+
for (
|
|
448
|
+
let i = DX7Bank.SYSEX_HEADER_SIZE;
|
|
449
|
+
i < DX7Bank.SYSEX_HEADER_SIZE + DX7Bank.VOICE_DATA_SIZE;
|
|
450
|
+
i++
|
|
451
|
+
) {
|
|
452
|
+
data[i] = 0
|
|
453
|
+
}
|
|
454
|
+
data[DX7Bank.SYSEX_SIZE - 1] = DX7Voice.MASK_7BIT
|
|
455
|
+
|
|
456
|
+
const blob = new Blob([data])
|
|
457
|
+
const file = new File([blob], "test.syx")
|
|
458
|
+
|
|
459
|
+
const bank = await DX7Bank.fromFile(file)
|
|
460
|
+
expect(bank.voices.length).toBe(DX7Bank.NUM_VOICES)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
it("should reject on invalid file", async () => {
|
|
464
|
+
const data = new Uint8Array(100) // Too small
|
|
465
|
+
const blob = new Blob([data])
|
|
466
|
+
const file = new File([blob], "invalid.syx")
|
|
467
|
+
|
|
468
|
+
await expect(DX7Bank.fromFile(file)).rejects.toThrow()
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
describe("bank workflow", () => {
|
|
473
|
+
it("should handle complete create, modify, export workflow", () => {
|
|
474
|
+
// Create empty bank
|
|
475
|
+
const bank = new DX7Bank()
|
|
476
|
+
|
|
477
|
+
// Create a custom voice
|
|
478
|
+
const voice = DX7Voice.createDefault()
|
|
479
|
+
voice.setParameter(0, 50)
|
|
480
|
+
voice.setParameter(4, 75)
|
|
481
|
+
voice.setParameter(8, 60) // Break point
|
|
482
|
+
|
|
483
|
+
// Set voice name - clear all first then set
|
|
484
|
+
const name = "MY VOICE"
|
|
485
|
+
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
|
|
486
|
+
voice.setParameter(DX7Voice.PACKED_NAME_START + i, 32) // Space
|
|
487
|
+
}
|
|
488
|
+
for (let i = 0; i < name.length; i++) {
|
|
489
|
+
voice.setParameter(DX7Voice.PACKED_NAME_START + i, name.charCodeAt(i))
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Add to bank
|
|
493
|
+
bank.replaceVoice(0, voice)
|
|
494
|
+
|
|
495
|
+
// Verify
|
|
496
|
+
expect(bank.getVoice(0).name).toBe("MY VOICE")
|
|
497
|
+
expect(bank.getVoice(0).getParameter(0)).toBe(50)
|
|
498
|
+
expect(bank.getVoice(0).getParameter(4)).toBe(75)
|
|
499
|
+
|
|
500
|
+
// Export to SYSEX
|
|
501
|
+
const sysex = bank.toSysEx()
|
|
502
|
+
expect(sysex.length).toBe(DX7Bank.SYSEX_SIZE)
|
|
503
|
+
|
|
504
|
+
// Import back
|
|
505
|
+
const importedBank = new DX7Bank(sysex)
|
|
506
|
+
expect(importedBank.getVoice(0).name).toBe("MY VOICE")
|
|
507
|
+
})
|
|
508
|
+
})
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
describe("Real DX7 ROM Bank (ROM1A.syx)", () => {
|
|
512
|
+
let bank
|
|
513
|
+
|
|
514
|
+
beforeAll(async () => {
|
|
515
|
+
// Load the real DX7 ROM file from fixtures directory
|
|
516
|
+
const fs = await import("node:fs")
|
|
517
|
+
const path = await import("node:path")
|
|
518
|
+
const fixturesPath = path.join(__dirname, "../../fixtures/ROM1A.syx")
|
|
519
|
+
const data = fs.readFileSync(fixturesPath)
|
|
520
|
+
bank = new DX7Bank(data)
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
describe("basic bank properties", () => {
|
|
524
|
+
it("should load valid DX7 bank with 32 voices", () => {
|
|
525
|
+
expect(bank.voices.length).toBe(DX7Bank.NUM_VOICES)
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it("should have valid sysex structure", () => {
|
|
529
|
+
const sysex = bank.toSysEx()
|
|
530
|
+
expect(sysex[0]).toBe(DX7Bank.SYSEX_START) // SysEx start
|
|
531
|
+
expect(sysex[DX7Bank.SYSEX_SIZE - 1]).toBe(DX7Bank.SYSEX_END) // SysEx end
|
|
532
|
+
expect(sysex.length).toBe(DX7Bank.SYSEX_SIZE)
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
it("should have correct DX7 header", () => {
|
|
536
|
+
const sysex = bank.toSysEx()
|
|
537
|
+
const header = sysex.slice(0, DX7Bank.SYSEX_HEADER_SIZE)
|
|
538
|
+
expect(Array.from(header)).toEqual(DX7Bank.SYSEX_HEADER)
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
describe("voice validation", () => {
|
|
543
|
+
it("should have all non-empty voice names", () => {
|
|
544
|
+
const names = bank.getVoiceNames()
|
|
545
|
+
names.forEach((name) => {
|
|
546
|
+
expect(name.length).toBeGreaterThan(0)
|
|
547
|
+
expect(name).not.toBe("")
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
it("should find known DX7 ROM voices", () => {
|
|
552
|
+
// These are well-known voices from the DX7 ROM (with actual spacing from ROM)
|
|
553
|
+
expect(bank.findVoiceByName("BRASS")).not.toBeNull() // Partial match for "BRASS 1"
|
|
554
|
+
expect(bank.findVoiceByName("E.PIANO")).not.toBeNull() // Partial match for "E.PIANO 1"
|
|
555
|
+
expect(bank.findVoiceByName("BASS")).not.toBeNull() // Partial match for "BASS 1"
|
|
556
|
+
expect(bank.findVoiceByName("PIANO")).not.toBeNull() // Partial match for "PIANO 1"
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it("should have valid voice parameters", () => {
|
|
560
|
+
const voice = bank.getVoice(0)
|
|
561
|
+
expect(voice).not.toBeNull()
|
|
562
|
+
expect(voice.data.length).toBe(DX7Voice.PACKED_SIZE)
|
|
563
|
+
|
|
564
|
+
// All parameters should be in valid range (0-127)
|
|
565
|
+
for (let i = 0; i < DX7Voice.PACKED_SIZE; i++) {
|
|
566
|
+
const param = voice.getParameter(i)
|
|
567
|
+
expect(param).toBeGreaterThanOrEqual(0)
|
|
568
|
+
expect(param).toBeLessThanOrEqual(127)
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
it("should unpack real voices correctly", () => {
|
|
573
|
+
const voice = bank.getVoice(0)
|
|
574
|
+
const unpacked = voice.unpack()
|
|
575
|
+
|
|
576
|
+
expect(unpacked.length).toBe(DX7Voice.UNPACKED_SIZE) // 159 parameters + 10 name bytes
|
|
577
|
+
|
|
578
|
+
// Check EG rates are in valid range
|
|
579
|
+
for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
|
|
580
|
+
const opOffset = op * DX7Voice.UNPACKED_OP_SIZE
|
|
581
|
+
for (let i = 0; i < 4; i++) {
|
|
582
|
+
expect(unpacked[opOffset + i]).toBeGreaterThanOrEqual(0)
|
|
583
|
+
expect(unpacked[opOffset + i]).toBeLessThanOrEqual(99) // EG rates max 99
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
describe("data integrity tests", () => {
|
|
590
|
+
it("should maintain data integrity through pack/unpack cycle", () => {
|
|
591
|
+
const originalPatch = bank.getVoice(5)
|
|
592
|
+
const unpacked = originalPatch.unpack()
|
|
593
|
+
const repacked = DX7Voice.pack(unpacked)
|
|
594
|
+
const newPatch = new DX7Voice(repacked, 5)
|
|
595
|
+
|
|
596
|
+
// Compare all 128 bytes - pack/unpack normalizes the data
|
|
597
|
+
// so we check that packed values match (with 7-bit mask applied)
|
|
598
|
+
for (let i = 0; i < DX7Voice.PACKED_SIZE; i++) {
|
|
599
|
+
expect(newPatch.data[i]).toBe(originalPatch.data[i] & DX7Voice.MASK_7BIT)
|
|
600
|
+
}
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
it("should export and reimport without data loss", () => {
|
|
604
|
+
const originalSysex = bank.toSysEx()
|
|
605
|
+
const newBank = new DX7Bank(originalSysex)
|
|
606
|
+
|
|
607
|
+
// Compare all voices
|
|
608
|
+
for (let i = 0; i < DX7Bank.NUM_VOICES; i++) {
|
|
609
|
+
const originalPatch = bank.getVoice(i)
|
|
610
|
+
const newPatch = newBank.getVoice(i)
|
|
611
|
+
|
|
612
|
+
expect(newPatch.data.length).toBe(originalPatch.data.length)
|
|
613
|
+
for (let j = 0; j < DX7Voice.PACKED_SIZE; j++) {
|
|
614
|
+
expect(newPatch.data[j]).toBe(originalPatch.data[j])
|
|
615
|
+
}
|
|
616
|
+
expect(newPatch.name).toBe(originalPatch.name)
|
|
617
|
+
}
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
it("should validate specific known voice parameters", () => {
|
|
621
|
+
// Test a few known voices from the DX7 ROM
|
|
622
|
+
const brass1 = bank.findVoiceByName("BRASS")
|
|
623
|
+
expect(brass1).not.toBeNull()
|
|
624
|
+
|
|
625
|
+
// Should have some operators active
|
|
626
|
+
const unpacked = brass1.unpack()
|
|
627
|
+
let activeOperators = 0
|
|
628
|
+
for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
|
|
629
|
+
const opOffset = op * DX7Voice.UNPACKED_OP_SIZE
|
|
630
|
+
if (unpacked[opOffset + 16] > 0) {
|
|
631
|
+
activeOperators++
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
expect(activeOperators).toBeGreaterThan(0)
|
|
635
|
+
})
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
describe("edge cases", () => {
|
|
639
|
+
it("should handle voice extraction at all indices", () => {
|
|
640
|
+
// Verify we can access all 32 voices
|
|
641
|
+
for (let i = 0; i < DX7Bank.NUM_VOICES; i++) {
|
|
642
|
+
const voice = bank.getVoice(i)
|
|
643
|
+
expect(voice).not.toBeNull()
|
|
644
|
+
expect(voice.index).toBe(i)
|
|
645
|
+
expect(voice.data.length).toBe(DX7Voice.PACKED_SIZE)
|
|
646
|
+
}
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
it("should preserve voice names exactly", () => {
|
|
650
|
+
const originalNames = bank.getVoiceNames()
|
|
651
|
+
const sysex = bank.toSysEx()
|
|
652
|
+
const newBank = new DX7Bank(sysex)
|
|
653
|
+
const newNames = newBank.getVoiceNames()
|
|
654
|
+
|
|
655
|
+
expect(newNames).toEqual(originalNames)
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
it("should handle voice replacement and maintain order", () => {
|
|
659
|
+
const originalPatch4 = bank.getVoice(4)
|
|
660
|
+
const _originalPatch5 = bank.getVoice(5)
|
|
661
|
+
const originalPatch6 = bank.getVoice(6)
|
|
662
|
+
const newPatch = DX7Voice.createDefault()
|
|
663
|
+
|
|
664
|
+
// Set a unique name (pad with spaces like DX7 does)
|
|
665
|
+
const testName = "TESTPATCH"
|
|
666
|
+
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
|
|
667
|
+
newPatch.setParameter(
|
|
668
|
+
118 + i,
|
|
669
|
+
i < testName.length ? testName.charCodeAt(i) : DX7Voice.CHAR_SPACE,
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
bank.replaceVoice(5, newPatch)
|
|
674
|
+
|
|
675
|
+
expect(bank.getVoice(5).name).toBe("TESTPATCH")
|
|
676
|
+
expect(bank.getVoice(4).name).toBe(originalPatch4.name)
|
|
677
|
+
expect(bank.getVoice(6).name).toBe(originalPatch6.name)
|
|
678
|
+
})
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
describe("toJSON", () => {
|
|
682
|
+
it("should convert BASS 1 voice to correct JSON format", () => {
|
|
683
|
+
// Get voice 14 which is BASS 1 (based on ROM voice list)
|
|
684
|
+
const bassPatch = bank.getVoice(14)
|
|
685
|
+
expect(bassPatch).not.toBeNull()
|
|
686
|
+
expect(bassPatch.name).toBe("BASS 1")
|
|
687
|
+
|
|
688
|
+
const json = bassPatch.toJSON()
|
|
689
|
+
|
|
690
|
+
// Verify voice name (includes padding)
|
|
691
|
+
expect(json.name).toBe("BASS 1")
|
|
692
|
+
|
|
693
|
+
// Verify global parameters
|
|
694
|
+
expect(json.global.algorithm).toBe(16)
|
|
695
|
+
expect(json.global.feedback).toBe(7)
|
|
696
|
+
expect(json.global.oscKeySync).toBe(true)
|
|
697
|
+
expect(json.global.pitchModSens).toBe(3)
|
|
698
|
+
expect(json.global.transpose).toBe(-12)
|
|
699
|
+
|
|
700
|
+
// Verify LFO parameters
|
|
701
|
+
expect(json.lfo.speed).toBe(35)
|
|
702
|
+
expect(json.lfo.delay).toBe(0)
|
|
703
|
+
expect(json.lfo.pmDepth).toBe(0)
|
|
704
|
+
expect(json.lfo.amDepth).toBe(0)
|
|
705
|
+
expect(json.lfo.keySync).toBe(false)
|
|
706
|
+
expect(json.lfo.wave).toBe("TRIANGLE")
|
|
707
|
+
|
|
708
|
+
// Verify pitch EG
|
|
709
|
+
expect(json.pitchEG.rates).toEqual([94, 67, 95, 60])
|
|
710
|
+
expect(json.pitchEG.levels).toEqual([50, 50, 50, 50])
|
|
711
|
+
|
|
712
|
+
// Verify operators count
|
|
713
|
+
expect(json.operators).toHaveLength(6)
|
|
714
|
+
|
|
715
|
+
// Verify OP1
|
|
716
|
+
expect(json.operators[0]).toEqual({
|
|
717
|
+
id: 1,
|
|
718
|
+
osc: { detune: 0, freq: { coarse: 0, fine: 0, mode: "RATIO" } },
|
|
719
|
+
eg: { rates: [95, 62, 17, 58], levels: [99, 95, 32, 0] },
|
|
720
|
+
key: { velocity: 0, scaling: 7, breakPoint: "A2" },
|
|
721
|
+
output: { level: 99, ampModSens: 0 },
|
|
722
|
+
scale: { left: { depth: 57, curve: "+LN" }, right: { depth: 14, curve: "-LN" } },
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
// Verify OP2
|
|
726
|
+
expect(json.operators[1]).toEqual({
|
|
727
|
+
id: 2,
|
|
728
|
+
osc: { detune: 0, freq: { coarse: 0, fine: 0, mode: "RATIO" } },
|
|
729
|
+
eg: { rates: [99, 20, 0, 0], levels: [99, 0, 0, 0] },
|
|
730
|
+
key: { velocity: 0, scaling: 7, breakPoint: "D3" },
|
|
731
|
+
output: { level: 80, ampModSens: 0 },
|
|
732
|
+
scale: { left: { depth: 0, curve: "-LN" }, right: { depth: 0, curve: "-LN" } },
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
// Verify OP3
|
|
736
|
+
expect(json.operators[2]).toEqual({
|
|
737
|
+
id: 3,
|
|
738
|
+
osc: { detune: 0, freq: { coarse: 0, fine: 0, mode: "RATIO" } },
|
|
739
|
+
eg: { rates: [88, 96, 32, 30], levels: [79, 65, 0, 0] },
|
|
740
|
+
key: { velocity: 3, scaling: 6, breakPoint: "A-1" },
|
|
741
|
+
output: { level: 99, ampModSens: 0 },
|
|
742
|
+
scale: { left: { depth: 0, curve: "-LN" }, right: { depth: 0, curve: "-LN" } },
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
// Verify OP4
|
|
746
|
+
expect(json.operators[3]).toEqual({
|
|
747
|
+
id: 4,
|
|
748
|
+
osc: { detune: 0, freq: { coarse: 5, fine: 0, mode: "RATIO" } },
|
|
749
|
+
eg: { rates: [90, 42, 7, 55], levels: [90, 30, 0, 0] },
|
|
750
|
+
key: { velocity: 5, scaling: 5, breakPoint: "A-1" },
|
|
751
|
+
output: { level: 93, ampModSens: 0 },
|
|
752
|
+
scale: { left: { depth: 0, curve: "-LN" }, right: { depth: 0, curve: "-LN" } },
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
// Verify OP5
|
|
756
|
+
expect(json.operators[4]).toEqual({
|
|
757
|
+
id: 5,
|
|
758
|
+
osc: { detune: 0, freq: { coarse: 0, fine: 0, mode: "RATIO" } },
|
|
759
|
+
eg: { rates: [99, 0, 0, 0], levels: [99, 0, 0, 0] },
|
|
760
|
+
key: { velocity: 3, scaling: 7, breakPoint: "C#4" },
|
|
761
|
+
output: { level: 62, ampModSens: 0 },
|
|
762
|
+
scale: { left: { depth: 75, curve: "-LN" }, right: { depth: 0, curve: "-LN" } },
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
// Verify OP6
|
|
766
|
+
expect(json.operators[5]).toEqual({
|
|
767
|
+
id: 6,
|
|
768
|
+
osc: { detune: 0, freq: { coarse: 9, fine: 0, mode: "RATIO" } },
|
|
769
|
+
eg: { rates: [94, 56, 24, 55], levels: [93, 28, 0, 0] },
|
|
770
|
+
key: { velocity: 7, scaling: 1, breakPoint: "A-1" },
|
|
771
|
+
output: { level: 85, ampModSens: 0 },
|
|
772
|
+
scale: { left: { depth: 0, curve: "-LN" }, right: { depth: 0, curve: "-LN" } },
|
|
773
|
+
})
|
|
774
|
+
})
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
describe("bank toJSON", () => {
|
|
778
|
+
it("should convert entire bank to JSON format", () => {
|
|
779
|
+
const json = bank.toJSON()
|
|
780
|
+
|
|
781
|
+
// Verify bank structure
|
|
782
|
+
expect(json.name).toBe("")
|
|
783
|
+
expect(json.voices).toHaveLength(DX7Bank.NUM_VOICES)
|
|
784
|
+
|
|
785
|
+
// Check first few voices have correct structure
|
|
786
|
+
expect(json.voices[0].index).toBe(1)
|
|
787
|
+
expect(json.voices[0].name).toBe("BRASS 1")
|
|
788
|
+
expect(json.voices[0].operators).toHaveLength(6)
|
|
789
|
+
expect(json.voices[0].global).toBeDefined()
|
|
790
|
+
expect(json.voices[0].lfo).toBeDefined()
|
|
791
|
+
expect(json.voices[0].pitchEG).toBeDefined()
|
|
792
|
+
|
|
793
|
+
expect(json.voices[14].index).toBe(15) // BASS 1
|
|
794
|
+
expect(json.voices[14].name).toBe("BASS 1")
|
|
795
|
+
|
|
796
|
+
// All voices should have proper structure
|
|
797
|
+
json.voices.forEach((voice, idx) => {
|
|
798
|
+
expect(voice.index).toBe(idx + 1) // 1-based indexing
|
|
799
|
+
expect(typeof voice.name).toBe("string")
|
|
800
|
+
expect(voice.operators).toHaveLength(6)
|
|
801
|
+
expect(voice.global).toBeDefined()
|
|
802
|
+
expect(voice.lfo).toBeDefined()
|
|
803
|
+
expect(voice.pitchEG).toBeDefined()
|
|
804
|
+
})
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
it("should preserve bank name when set", () => {
|
|
808
|
+
const testBank = new DX7Bank()
|
|
809
|
+
testBank.name = "Test Bank"
|
|
810
|
+
const json = testBank.toJSON()
|
|
811
|
+
|
|
812
|
+
expect(json.name).toBe("Test Bank")
|
|
813
|
+
expect(json.voices).toHaveLength(DX7Bank.NUM_VOICES)
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
it("should include all voice data in JSON", () => {
|
|
817
|
+
const json = bank.toJSON()
|
|
818
|
+
|
|
819
|
+
// Pick a specific voice (BASS 1 at index 14)
|
|
820
|
+
const bass1Json = json.voices[14]
|
|
821
|
+
|
|
822
|
+
// Verify it matches the individual voice JSON
|
|
823
|
+
const bass1Voice = bank.getVoice(14)
|
|
824
|
+
const bass1IndividualJson = bass1Voice.toJSON()
|
|
825
|
+
|
|
826
|
+
expect(bass1Json.name).toBe(bass1IndividualJson.name)
|
|
827
|
+
expect(bass1Json.operators).toEqual(bass1IndividualJson.operators)
|
|
828
|
+
expect(bass1Json.global).toEqual(bass1IndividualJson.global)
|
|
829
|
+
expect(bass1Json.lfo).toEqual(bass1IndividualJson.lfo)
|
|
830
|
+
expect(bass1Json.pitchEG).toEqual(bass1IndividualJson.pitchEG)
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
it("should match structure of fixture JSON files", () => {
|
|
834
|
+
const json = bank.toJSON()
|
|
835
|
+
|
|
836
|
+
// Verify structure matches what we export in fixtures
|
|
837
|
+
expect(json).toHaveProperty("name")
|
|
838
|
+
expect(json).toHaveProperty("voices")
|
|
839
|
+
expect(Array.isArray(json.voices)).toBe(true)
|
|
840
|
+
|
|
841
|
+
// Verify each voice has index field
|
|
842
|
+
json.voices.forEach((voice) => {
|
|
843
|
+
expect(voice).toHaveProperty("index")
|
|
844
|
+
expect(typeof voice.index).toBe("number")
|
|
845
|
+
expect(voice.index).toBeGreaterThanOrEqual(1)
|
|
846
|
+
expect(voice.index).toBeLessThanOrEqual(32)
|
|
847
|
+
})
|
|
848
|
+
})
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
describe("export/import round-trip", () => {
|
|
852
|
+
it("should export BASS 1 and be able to re-import it", async () => {
|
|
853
|
+
// Get BASS 1 from ROM
|
|
854
|
+
const bass1 = bank.getVoice(14)
|
|
855
|
+
expect(bass1.name).toBe("BASS 1")
|
|
856
|
+
|
|
857
|
+
// Export to SysEx single voice format
|
|
858
|
+
const sysex = bass1.toSysEx()
|
|
859
|
+
expect(sysex.length).toBe(DX7Voice.VCED_SIZE)
|
|
860
|
+
|
|
861
|
+
// Load it back using DX7Voice.fromFile (single voice format)
|
|
862
|
+
const blob = new Blob([sysex])
|
|
863
|
+
const file = new File([blob], "BASS_1_EXPORT.syx")
|
|
864
|
+
const loadedVoice = await DX7Voice.fromFile(file)
|
|
865
|
+
|
|
866
|
+
// Verify the round-trip preserved the voice
|
|
867
|
+
expect(loadedVoice.name).toBe("BASS 1")
|
|
868
|
+
|
|
869
|
+
// Compare all packed bytes - they should be identical
|
|
870
|
+
for (let i = 0; i < DX7Voice.PACKED_SIZE; i++) {
|
|
871
|
+
expect(loadedVoice.data[i]).toBe(bass1.data[i])
|
|
872
|
+
}
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
it("should maintain voice integrity through complete round-trip", async () => {
|
|
876
|
+
// Get BASS 1 from ROM
|
|
877
|
+
const originalBass1 = bank.getVoice(14)
|
|
878
|
+
|
|
879
|
+
// Export to SysEx
|
|
880
|
+
const sysexData = originalBass1.toSysEx()
|
|
881
|
+
|
|
882
|
+
// Create a File object from the SysEx (simulating browser file upload)
|
|
883
|
+
const blob = new Blob([sysexData])
|
|
884
|
+
const file = new File([blob], "BASS_1_EXPORT.syx")
|
|
885
|
+
|
|
886
|
+
// Load it back using DX7Voice.fromFile (single voice format)
|
|
887
|
+
const loadedVoice = await DX7Voice.fromFile(file)
|
|
888
|
+
|
|
889
|
+
// Verify the round-trip preserved the voice
|
|
890
|
+
expect(loadedVoice.name).toBe("BASS 1")
|
|
891
|
+
|
|
892
|
+
// Compare unpacked parameters to verify no data loss
|
|
893
|
+
const originalUnpacked = originalBass1.unpack()
|
|
894
|
+
const loadedUnpacked = loadedVoice.unpack()
|
|
895
|
+
|
|
896
|
+
for (let i = 0; i < DX7Voice.UNPACKED_SIZE; i++) {
|
|
897
|
+
expect(loadedUnpacked[i]).toBe(originalUnpacked[i])
|
|
898
|
+
}
|
|
899
|
+
})
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
describe("toSysEx", () => {
|
|
903
|
+
it("should create correct single voice dump format", () => {
|
|
904
|
+
// Get voice 14 which is BASS 1 (based on ROM voice list)
|
|
905
|
+
const bassPatch = bank.getVoice(14)
|
|
906
|
+
expect(bassPatch).not.toBeNull()
|
|
907
|
+
expect(bassPatch.name).toBe("BASS 1")
|
|
908
|
+
|
|
909
|
+
const sysex = bassPatch.toSysEx()
|
|
910
|
+
|
|
911
|
+
// Verify total size (163 bytes)
|
|
912
|
+
expect(sysex.length).toBe(DX7Voice.VCED_SIZE)
|
|
913
|
+
|
|
914
|
+
// Verify header bytes
|
|
915
|
+
expect(sysex[0]).toBe(DX7Bank.SYSEX_START) // SysEx start
|
|
916
|
+
expect(sysex[1]).toBe(DX7Bank.SYSEX_YAMAHA_ID) // Yamaha ID
|
|
917
|
+
expect(sysex[2]).toBe(0x00) // Channel/format (nibblized)
|
|
918
|
+
expect(sysex[3]).toBe(0x00) // Substatus: bulk dump
|
|
919
|
+
expect(sysex[4]).toBe(0x01) // Format: single voice dump
|
|
920
|
+
expect(sysex[5]).toBe(0x1b) // Packet type
|
|
921
|
+
|
|
922
|
+
// Verify voice data (145 bytes of unpacked parameters)
|
|
923
|
+
const voiceData = sysex.slice(6, 151)
|
|
924
|
+
expect(voiceData.length).toBe(145)
|
|
925
|
+
|
|
926
|
+
// Voice name: 10 bytes at offset 151-160
|
|
927
|
+
const nameBytes = sysex.slice(151, 161)
|
|
928
|
+
const extractedName = String.fromCharCode(
|
|
929
|
+
...nameBytes.map((b) => b & DX7Voice.MASK_7BIT),
|
|
930
|
+
).trim()
|
|
931
|
+
expect(extractedName).toBe("BASS 1")
|
|
932
|
+
|
|
933
|
+
// Verify checksum (byte 161)
|
|
934
|
+
const checksum = sysex[161]
|
|
935
|
+
expect(checksum).toBeGreaterThanOrEqual(0)
|
|
936
|
+
expect(checksum).toBeLessThanOrEqual(127)
|
|
937
|
+
|
|
938
|
+
// Verify SysEx end byte
|
|
939
|
+
expect(sysex[DX7Voice.VCED_SIZE - 1]).toBe(DX7Voice.VCED_SYSEX_END)
|
|
940
|
+
|
|
941
|
+
// Verify checksum is correct
|
|
942
|
+
// Sum all 155 data bytes (bytes 6-160)
|
|
943
|
+
let sum = 0
|
|
944
|
+
for (let i = 6; i < 161; i++) {
|
|
945
|
+
sum += sysex[i]
|
|
946
|
+
}
|
|
947
|
+
const expectedChecksum =
|
|
948
|
+
(DX7Bank.CHECKSUM_MODULO - (sum % DX7Bank.CHECKSUM_MODULO)) & DX7Voice.MASK_7BIT
|
|
949
|
+
expect(checksum).toBe(expectedChecksum)
|
|
950
|
+
})
|
|
951
|
+
|
|
952
|
+
it("should produce consistent output for same voice", () => {
|
|
953
|
+
const bassPatch = bank.getVoice(14)
|
|
954
|
+
const sysex1 = bassPatch.toSysEx()
|
|
955
|
+
const sysex2 = bassPatch.toSysEx()
|
|
956
|
+
|
|
957
|
+
// Both outputs should be identical
|
|
958
|
+
expect(sysex1.length).toBe(sysex2.length)
|
|
959
|
+
for (let i = 0; i < sysex1.length; i++) {
|
|
960
|
+
expect(sysex1[i]).toBe(sysex2[i])
|
|
961
|
+
}
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
it("should have correct size for single voice dump format", () => {
|
|
965
|
+
const voice = bank.getVoice(0)
|
|
966
|
+
const sysex = voice.toSysEx()
|
|
967
|
+
|
|
968
|
+
// Single voice dump format:
|
|
969
|
+
// - 6 bytes header
|
|
970
|
+
// - 145 bytes voice data (unpacked format)
|
|
971
|
+
// - 10 bytes voice name
|
|
972
|
+
// - 1 byte checksum
|
|
973
|
+
// - 1 byte F7 footer
|
|
974
|
+
// Total: 163 bytes
|
|
975
|
+
expect(sysex.length).toBe(DX7Voice.VCED_SIZE)
|
|
976
|
+
})
|
|
977
|
+
|
|
978
|
+
it("should validate checksum calculation", () => {
|
|
979
|
+
const voice = bank.getVoice(0)
|
|
980
|
+
const sysex = voice.toSysEx()
|
|
981
|
+
|
|
982
|
+
// Manually calculate checksum
|
|
983
|
+
let calculatedSum = 0
|
|
984
|
+
for (let i = 6; i < 161; i++) {
|
|
985
|
+
calculatedSum += sysex[i]
|
|
986
|
+
}
|
|
987
|
+
const calculatedChecksum =
|
|
988
|
+
(DX7Bank.CHECKSUM_MODULO - (calculatedSum % DX7Bank.CHECKSUM_MODULO)) & DX7Voice.MASK_7BIT
|
|
989
|
+
|
|
990
|
+
// Compare with actual checksum in the SysEx (at index 161, before F7 at 162)
|
|
991
|
+
expect(sysex[161]).toBe(calculatedChecksum)
|
|
992
|
+
expect(sysex[DX7Voice.VCED_SIZE - 1]).toBe(DX7Voice.VCED_SYSEX_END) // Verify F7 is at the correct position
|
|
993
|
+
})
|
|
994
|
+
})
|
|
995
|
+
})
|
|
996
|
+
|
|
997
|
+
describe("Real DX7 Single Voice File (ROM1A_BASS____1.syx)", () => {
|
|
998
|
+
let bank
|
|
999
|
+
let voice
|
|
1000
|
+
let romBank
|
|
1001
|
+
let _romBass1
|
|
1002
|
+
|
|
1003
|
+
beforeAll(async () => {
|
|
1004
|
+
// Load the single voice file from fixtures directory using DX7Voice.fromFile
|
|
1005
|
+
const fs = await import("node:fs")
|
|
1006
|
+
const path = await import("node:path")
|
|
1007
|
+
|
|
1008
|
+
const fixturesPath = path.join(__dirname, "../../fixtures/ROM1A_BASS____1.syx")
|
|
1009
|
+
const data = fs.readFileSync(fixturesPath)
|
|
1010
|
+
const blob = new Blob([data])
|
|
1011
|
+
const file = new File([blob], "ROM1A_BASS____1.syx")
|
|
1012
|
+
voice = await DX7Voice.fromFile(file)
|
|
1013
|
+
|
|
1014
|
+
// Create a bank with the single voice for API compatibility
|
|
1015
|
+
bank = new DX7Bank()
|
|
1016
|
+
bank.voices.fill(DX7Voice.createDefault())
|
|
1017
|
+
bank.voices[0] = voice
|
|
1018
|
+
bank.name = "ROM1A_BASS____1.syx"
|
|
1019
|
+
|
|
1020
|
+
// Load the full ROM bank for comparison
|
|
1021
|
+
const romPath = path.join(__dirname, "../../fixtures/ROM1A.syx")
|
|
1022
|
+
const romData = fs.readFileSync(romPath)
|
|
1023
|
+
romBank = new DX7Bank(romData)
|
|
1024
|
+
_romBass1 = romBank.getVoice(14) // BASS 1 is at index 14 in ROM1A
|
|
1025
|
+
})
|
|
1026
|
+
|
|
1027
|
+
describe("basic file properties", () => {
|
|
1028
|
+
it("should load as a bank with one voice", () => {
|
|
1029
|
+
expect(bank.voices.length).toBe(DX7Bank.NUM_VOICES)
|
|
1030
|
+
expect(voice.name).toBe("BASS 1")
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
it("should have valid voice parameters", () => {
|
|
1034
|
+
expect(voice).not.toBeNull()
|
|
1035
|
+
expect(voice.data.length).toBe(DX7Voice.PACKED_SIZE)
|
|
1036
|
+
|
|
1037
|
+
// All parameters should be in valid range (0-127)
|
|
1038
|
+
for (let i = 0; i < DX7Voice.PACKED_SIZE; i++) {
|
|
1039
|
+
const param = voice.getParameter(i)
|
|
1040
|
+
expect(param).toBeGreaterThanOrEqual(0)
|
|
1041
|
+
expect(param).toBeLessThanOrEqual(127)
|
|
1042
|
+
}
|
|
1043
|
+
})
|
|
1044
|
+
|
|
1045
|
+
it("should have correct voice name", () => {
|
|
1046
|
+
expect(voice.name).toBe("BASS 1")
|
|
1047
|
+
})
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
describe("single voice conversion", () => {
|
|
1051
|
+
it("should convert to SysEx single voice format", () => {
|
|
1052
|
+
const sysex = voice.toSysEx()
|
|
1053
|
+
|
|
1054
|
+
expect(sysex.length).toBe(DX7Voice.VCED_SIZE)
|
|
1055
|
+
expect(sysex[0]).toBe(DX7Voice.VCED_SYSEX_START)
|
|
1056
|
+
expect(sysex[1]).toBe(DX7Voice.VCED_YAMAHA_ID)
|
|
1057
|
+
expect(sysex[2]).toBe(DX7Voice.VCED_SUB_STATUS)
|
|
1058
|
+
expect(sysex[3]).toBe(DX7Voice.VCED_FORMAT_SINGLE)
|
|
1059
|
+
expect(sysex[DX7Voice.VCED_SIZE - 1]).toBe(DX7Voice.VCED_SYSEX_END)
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
it("should maintain data integrity through toSysEx conversion", () => {
|
|
1063
|
+
const sysex = voice.toSysEx()
|
|
1064
|
+
|
|
1065
|
+
// Verify the SysEx structure
|
|
1066
|
+
expect(sysex.length).toBe(DX7Voice.VCED_SIZE)
|
|
1067
|
+
|
|
1068
|
+
// Verify the voice data section (bytes 6-160) contains valid parameters
|
|
1069
|
+
const voiceDataSection = sysex.slice(6, 161)
|
|
1070
|
+
expect(voiceDataSection.length).toBe(155)
|
|
1071
|
+
|
|
1072
|
+
// Each byte should be valid 7-bit MIDI data
|
|
1073
|
+
for (let i = 0; i < voiceDataSection.length; i++) {
|
|
1074
|
+
expect(voiceDataSection[i]).toBeGreaterThanOrEqual(0)
|
|
1075
|
+
expect(voiceDataSection[i]).toBeLessThanOrEqual(127)
|
|
1076
|
+
}
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
it("should create valid checksum", () => {
|
|
1080
|
+
const sysex = voice.toSysEx()
|
|
1081
|
+
const checksum = sysex[161]
|
|
1082
|
+
|
|
1083
|
+
// Manually calculate checksum
|
|
1084
|
+
let sum = 0
|
|
1085
|
+
for (let i = 6; i < 161; i++) {
|
|
1086
|
+
sum += sysex[i]
|
|
1087
|
+
}
|
|
1088
|
+
const expectedChecksum =
|
|
1089
|
+
(DX7Bank.CHECKSUM_MODULO - (sum % DX7Bank.CHECKSUM_MODULO)) & DX7Voice.MASK_7BIT
|
|
1090
|
+
|
|
1091
|
+
expect(checksum).toBe(expectedChecksum)
|
|
1092
|
+
})
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
describe("single voice pack/unpack", () => {
|
|
1096
|
+
it("should unpack and repack correctly", () => {
|
|
1097
|
+
const unpacked = voice.unpack()
|
|
1098
|
+
const repacked = DX7Voice.pack(unpacked)
|
|
1099
|
+
const newVoice = new DX7Voice(repacked, 0)
|
|
1100
|
+
|
|
1101
|
+
expect(newVoice.name).toBe(voice.name)
|
|
1102
|
+
|
|
1103
|
+
// Compare all packed bytes
|
|
1104
|
+
for (let i = 0; i < DX7Voice.PACKED_SIZE; i++) {
|
|
1105
|
+
expect(newVoice.data[i]).toBe(voice.data[i] & DX7Voice.MASK_7BIT)
|
|
1106
|
+
}
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
it("should produce correct JSON representation", () => {
|
|
1110
|
+
const json = voice.toJSON()
|
|
1111
|
+
expect(json.name).toBe("BASS 1")
|
|
1112
|
+
expect(json.operators).toHaveLength(6)
|
|
1113
|
+
expect(json.global.algorithm).toBe(16)
|
|
1114
|
+
expect(json.global.feedback).toBe(7)
|
|
1115
|
+
})
|
|
1116
|
+
})
|
|
1117
|
+
|
|
1118
|
+
describe("edge cases for single voice files", () => {
|
|
1119
|
+
it("should reject single voice files via DX7Bank.fromFile", async () => {
|
|
1120
|
+
const fs = await import("node:fs")
|
|
1121
|
+
const path = await import("node:path")
|
|
1122
|
+
|
|
1123
|
+
const fixturesPath = path.join(__dirname, "../../fixtures/ROM1A_BASS____1.syx")
|
|
1124
|
+
const data = fs.readFileSync(fixturesPath)
|
|
1125
|
+
const blob = new Blob([data])
|
|
1126
|
+
const file = new File([blob], "ROM1A_BASS____1.syx")
|
|
1127
|
+
|
|
1128
|
+
// DX7Bank.fromFile should reject single voice files
|
|
1129
|
+
await expect(DX7Bank.fromFile(file)).rejects.toThrow(
|
|
1130
|
+
"This is a single voice file. Use DX7Voice.fromFile() instead.",
|
|
1131
|
+
)
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
it("should handle single voice in voice array correctly", () => {
|
|
1135
|
+
// Single voice should be in slot 0
|
|
1136
|
+
expect(voice.index).toBe(0)
|
|
1137
|
+
|
|
1138
|
+
// All voices should be valid
|
|
1139
|
+
for (let i = 0; i < DX7Bank.NUM_VOICES; i++) {
|
|
1140
|
+
const v = bank.getVoice(i)
|
|
1141
|
+
expect(v).not.toBeNull()
|
|
1142
|
+
expect(v.data.length).toBe(DX7Voice.PACKED_SIZE)
|
|
1143
|
+
}
|
|
1144
|
+
})
|
|
1145
|
+
|
|
1146
|
+
it("should match ALL parameter values exactly", async () => {
|
|
1147
|
+
// Load the bank (ROM1A.syx) to get BASS 1 from slot 15 (index 14)
|
|
1148
|
+
const bankBass1 = romBank.getVoice(14) // BASS 1 in ROM1A
|
|
1149
|
+
const bankJson = bankBass1.toJSON()
|
|
1150
|
+
|
|
1151
|
+
// Load the single voice file (ROM1A_BASS____1.syx)
|
|
1152
|
+
const singleVoiceJson = voice.toJSON()
|
|
1153
|
+
|
|
1154
|
+
// Verify all operator parameters match
|
|
1155
|
+
for (let i = 0; i < DX7Voice.NUM_OPERATORS; i++) {
|
|
1156
|
+
const singleOp = singleVoiceJson.operators[i]
|
|
1157
|
+
const bankOp = bankJson.operators[i]
|
|
1158
|
+
|
|
1159
|
+
// OSC parameters
|
|
1160
|
+
expect(singleOp.osc.detune).toBe(bankOp.osc.detune)
|
|
1161
|
+
expect(singleOp.osc.freq.coarse).toBe(bankOp.osc.freq.coarse)
|
|
1162
|
+
expect(singleOp.osc.freq.fine).toBe(bankOp.osc.freq.fine)
|
|
1163
|
+
expect(singleOp.osc.freq.mode).toBe(bankOp.osc.freq.mode)
|
|
1164
|
+
|
|
1165
|
+
// EG rates and levels
|
|
1166
|
+
expect(singleOp.eg.rates).toEqual(bankOp.eg.rates)
|
|
1167
|
+
expect(singleOp.eg.levels).toEqual(bankOp.eg.levels)
|
|
1168
|
+
|
|
1169
|
+
// KEY parameters (including velocity)
|
|
1170
|
+
expect(singleOp.key.velocity).toBe(bankOp.key.velocity)
|
|
1171
|
+
expect(singleOp.key.scaling).toBe(bankOp.key.scaling)
|
|
1172
|
+
expect(singleOp.key.breakPoint).toBe(bankOp.key.breakPoint)
|
|
1173
|
+
|
|
1174
|
+
// OUTPUT parameters
|
|
1175
|
+
expect(singleOp.output.level).toBe(bankOp.output.level)
|
|
1176
|
+
expect(singleOp.output.ampModSens).toBe(bankOp.output.ampModSens)
|
|
1177
|
+
|
|
1178
|
+
// SCALE parameters
|
|
1179
|
+
expect(singleOp.scale.left.depth).toBe(bankOp.scale.left.depth)
|
|
1180
|
+
expect(singleOp.scale.left.curve).toBe(bankOp.scale.left.curve)
|
|
1181
|
+
expect(singleOp.scale.right.depth).toBe(bankOp.scale.right.depth)
|
|
1182
|
+
expect(singleOp.scale.right.curve).toBe(bankOp.scale.right.curve)
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Pitch EG
|
|
1186
|
+
expect(singleVoiceJson.pitchEG.rates).toEqual(bankJson.pitchEG.rates)
|
|
1187
|
+
expect(singleVoiceJson.pitchEG.levels).toEqual(bankJson.pitchEG.levels)
|
|
1188
|
+
|
|
1189
|
+
// LFO parameters
|
|
1190
|
+
expect(singleVoiceJson.lfo.speed).toBe(bankJson.lfo.speed)
|
|
1191
|
+
expect(singleVoiceJson.lfo.delay).toBe(bankJson.lfo.delay)
|
|
1192
|
+
expect(singleVoiceJson.lfo.pmDepth).toBe(bankJson.lfo.pmDepth)
|
|
1193
|
+
expect(singleVoiceJson.lfo.amDepth).toBe(bankJson.lfo.amDepth)
|
|
1194
|
+
expect(singleVoiceJson.lfo.keySync).toBe(bankJson.lfo.keySync)
|
|
1195
|
+
expect(singleVoiceJson.lfo.wave).toBe(bankJson.lfo.wave)
|
|
1196
|
+
|
|
1197
|
+
// Global parameters
|
|
1198
|
+
expect(singleVoiceJson.global.algorithm).toBe(bankJson.global.algorithm)
|
|
1199
|
+
expect(singleVoiceJson.global.feedback).toBe(bankJson.global.feedback)
|
|
1200
|
+
expect(singleVoiceJson.global.oscKeySync).toBe(bankJson.global.oscKeySync)
|
|
1201
|
+
expect(singleVoiceJson.global.pitchModSens).toBe(bankJson.global.pitchModSens)
|
|
1202
|
+
expect(singleVoiceJson.global.transpose).toBe(bankJson.global.transpose)
|
|
1203
|
+
|
|
1204
|
+
// Name should match
|
|
1205
|
+
expect(singleVoiceJson.name).toBe(bankJson.name)
|
|
1206
|
+
})
|
|
1207
|
+
})
|
|
1208
|
+
})
|