Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
MorsGames
GitHub Repository: MorsGames/sm64plus
Path: blob/master/tools/disassemble_sound.py
7854 views
1
#!/usr/bin/env python3
2
from collections import namedtuple, defaultdict
3
import tempfile
4
import subprocess
5
import uuid
6
import json
7
import os
8
import re
9
import struct
10
import sys
11
12
TYPE_CTL = 1
13
TYPE_TBL = 2
14
15
16
class AifcEntry:
17
def __init__(self, data, book, loop):
18
self.name = None
19
self.data = data
20
self.book = book
21
self.loop = loop
22
self.tunings = []
23
24
25
class SampleBank:
26
def __init__(self, name, data, offset):
27
self.offset = offset
28
self.name = name
29
self.data = data
30
self.entries = {}
31
32
def add_sample(self, offset, sample_size, book, loop):
33
assert sample_size % 2 == 0
34
if sample_size % 9 != 0:
35
assert sample_size % 9 == 1
36
sample_size -= 1
37
38
if offset in self.entries:
39
entry = self.entries[offset]
40
assert entry.book == book
41
assert entry.loop == loop
42
assert len(entry.data) == sample_size
43
else:
44
entry = AifcEntry(self.data[offset : offset + sample_size], book, loop)
45
self.entries[offset] = entry
46
47
return entry
48
49
50
Sound = namedtuple("Sound", ["sample_addr", "tuning"])
51
Drum = namedtuple("Drum", ["name", "addr", "release_rate", "pan", "envelope", "sound"])
52
Inst = namedtuple(
53
"Inst",
54
[
55
"name",
56
"addr",
57
"release_rate",
58
"normal_range_lo",
59
"normal_range_hi",
60
"envelope",
61
"sound_lo",
62
"sound_med",
63
"sound_hi",
64
],
65
)
66
Book = namedtuple("Book", ["order", "npredictors", "table"])
67
Loop = namedtuple("Loop", ["start", "end", "count", "state"])
68
Envelope = namedtuple("Envelope", ["name", "entries"])
69
Bank = namedtuple(
70
"Bank",
71
[
72
"name",
73
"iso_date",
74
"sample_bank",
75
"insts",
76
"drums",
77
"all_insts",
78
"inst_list",
79
"envelopes",
80
"samples",
81
],
82
)
83
84
85
def align(val, al):
86
return (val + (al - 1)) & -al
87
88
89
name_tbl = {}
90
91
92
def gen_name(prefix, name_table=[]):
93
if prefix not in name_tbl:
94
name_tbl[prefix] = 0
95
ind = name_tbl[prefix]
96
name_tbl[prefix] += 1
97
if ind < len(name_table):
98
return name_table[ind]
99
return prefix + str(ind)
100
101
102
def parse_bcd(data):
103
ret = 0
104
for c in data:
105
ret *= 10
106
ret += c >> 4
107
ret *= 10
108
ret += c & 15
109
return ret
110
111
112
def serialize_f80(num):
113
num = float(num)
114
(f64,) = struct.unpack(">Q", struct.pack(">d", num))
115
f64_sign_bit = f64 & 2 ** 63
116
if num == 0.0:
117
if f64_sign_bit:
118
return b"\x80" + b"\0" * 9
119
else:
120
return b"\0" * 10
121
exponent = (f64 ^ f64_sign_bit) >> 52
122
assert exponent != 0, "can't handle denormals"
123
assert exponent != 0x7FF, "can't handle infinity/nan"
124
exponent -= 1023
125
f64_mantissa_bits = f64 & (2 ** 52 - 1)
126
f80_sign_bit = f64_sign_bit << (80 - 64)
127
f80_exponent = (exponent + 0x3FFF) << 64
128
f80_mantissa_bits = 2 ** 63 | (f64_mantissa_bits << (63 - 52))
129
f80 = f80_sign_bit | f80_exponent | f80_mantissa_bits
130
return struct.pack(">HQ", f80 >> 64, f80 & (2 ** 64 - 1))
131
132
133
def round_f32(num):
134
enc = struct.pack(">f", num)
135
for decimals in range(5, 20):
136
num2 = round(num, decimals)
137
if struct.pack(">f", num2) == enc:
138
return num2
139
return num
140
141
142
def parse_sound(data):
143
sample_addr, tuning = struct.unpack(">If", data)
144
if sample_addr == 0:
145
assert tuning == 0
146
return None
147
return Sound(sample_addr, tuning)
148
149
150
def parse_drum(data, addr):
151
name = gen_name("drum")
152
release_rate, pan, loaded, pad = struct.unpack(">BBBB", data[:4])
153
assert loaded == 0
154
assert pad == 0
155
sound = parse_sound(data[4:12])
156
(env_addr,) = struct.unpack(">I", data[12:])
157
assert env_addr != 0
158
return Drum(name, addr, release_rate, pan, env_addr, sound)
159
160
161
def parse_inst(data, addr):
162
name = gen_name("inst")
163
loaded, normal_range_lo, normal_range_hi, release_rate, env_addr = struct.unpack(
164
">BBBBI", data[:8]
165
)
166
assert env_addr != 0
167
sound_lo = parse_sound(data[8:16])
168
sound_med = parse_sound(data[16:24])
169
sound_hi = parse_sound(data[24:])
170
if sound_lo is None:
171
assert normal_range_lo == 0
172
if sound_hi is None:
173
assert normal_range_hi == 127
174
return Inst(
175
name,
176
addr,
177
release_rate,
178
normal_range_lo,
179
normal_range_hi,
180
env_addr,
181
sound_lo,
182
sound_med,
183
sound_hi,
184
)
185
186
187
def parse_loop(addr, bank_data):
188
start, end, count, pad = struct.unpack(">IIiI", bank_data[addr : addr + 16])
189
assert pad == 0
190
if count != 0:
191
state = struct.unpack(">16h", bank_data[addr + 16 : addr + 48])
192
else:
193
state = None
194
return Loop(start, end, count, state)
195
196
197
def parse_book(addr, bank_data):
198
order, npredictors = struct.unpack(">ii", bank_data[addr : addr + 8])
199
assert order == 2
200
assert npredictors == 2
201
table_data = bank_data[addr + 8 : addr + 8 + 16 * order * npredictors]
202
table = []
203
for i in range(0, 16 * order * npredictors, 2):
204
table.append(struct.unpack(">h", table_data[i : i + 2])[0])
205
return Book(order, npredictors, table)
206
207
208
def parse_sample(data, bank_data, sample_bank, is_shindou):
209
if is_shindou:
210
sample_size, addr, loop, book = struct.unpack(">IIII", data)
211
else:
212
zero, addr, loop, book, sample_size = struct.unpack(">IIIII", data)
213
assert zero == 0
214
assert loop != 0
215
assert book != 0
216
loop = parse_loop(loop, bank_data)
217
book = parse_book(book, bank_data)
218
return sample_bank.add_sample(addr, sample_size, book, loop)
219
220
221
def parse_envelope(addr, data_bank):
222
entries = []
223
while True:
224
delay, arg = struct.unpack(">HH", data_bank[addr : addr + 4])
225
entries.append((delay, arg))
226
addr += 4
227
if 1 <= (-delay) % 2 ** 16 <= 3:
228
break
229
return entries
230
231
232
def parse_ctl_header(header):
233
num_instruments, num_drums, shared = struct.unpack(">III", header[:12])
234
date = parse_bcd(header[12:])
235
y = date // 10000
236
m = date // 100 % 100
237
d = date % 100
238
iso_date = "{:02}-{:02}-{:02}".format(y, m, d)
239
assert shared in [0, 1]
240
return num_instruments, num_drums, iso_date
241
242
243
def parse_ctl(parsed_header, data, sample_bank, index, is_shindou):
244
name_tbl.clear()
245
name = "{:02X}".format(index)
246
num_instruments, num_drums, iso_date = parsed_header
247
# print("{}: {}, {} + {}".format(name, iso_date, num_instruments, num_drums))
248
249
(drum_base_addr,) = struct.unpack(">I", data[:4])
250
drum_addrs = []
251
if num_drums != 0:
252
assert drum_base_addr != 0
253
for i in range(num_drums):
254
(drum_addr,) = struct.unpack(
255
">I", data[drum_base_addr + i * 4 : drum_base_addr + i * 4 + 4]
256
)
257
assert drum_addr != 0
258
drum_addrs.append(drum_addr)
259
else:
260
assert drum_base_addr == 0
261
262
inst_base_addr = 4
263
inst_addrs = []
264
inst_list = []
265
for i in range(num_instruments):
266
(inst_addr,) = struct.unpack(
267
">I", data[inst_base_addr + i * 4 : inst_base_addr + i * 4 + 4]
268
)
269
if inst_addr == 0:
270
inst_list.append(None)
271
else:
272
inst_list.append(inst_addr)
273
inst_addrs.append(inst_addr)
274
275
inst_addrs.sort()
276
assert drum_addrs == sorted(drum_addrs)
277
if drum_addrs and inst_addrs:
278
assert max(inst_addrs) < min(drum_addrs)
279
280
assert len(set(inst_addrs)) == len(inst_addrs)
281
assert len(set(drum_addrs)) == len(drum_addrs)
282
283
insts = []
284
for inst_addr in inst_addrs:
285
insts.append(parse_inst(data[inst_addr : inst_addr + 32], inst_addr))
286
287
drums = []
288
for drum_addr in drum_addrs:
289
drums.append(parse_drum(data[drum_addr : drum_addr + 16], drum_addr))
290
291
env_addrs = set()
292
sample_addrs = set()
293
tunings = defaultdict(lambda: [])
294
for inst in insts:
295
for sound in [inst.sound_lo, inst.sound_med, inst.sound_hi]:
296
if sound is not None:
297
sample_addrs.add(sound.sample_addr)
298
tunings[sound.sample_addr].append(sound.tuning)
299
env_addrs.add(inst.envelope)
300
for drum in drums:
301
sample_addrs.add(drum.sound.sample_addr)
302
tunings[drum.sound.sample_addr].append(drum.sound.tuning)
303
env_addrs.add(drum.envelope)
304
305
# Put drums somewhere in the middle of the instruments to make sample
306
# addresses come in increasing order. (This logic isn't totally right,
307
# but it works for our purposes.)
308
all_insts = []
309
need_drums = len(drums) > 0
310
for inst in insts:
311
if need_drums and any(
312
s.sample_addr > drums[0].sound.sample_addr
313
for s in [inst.sound_lo, inst.sound_med, inst.sound_hi]
314
if s is not None
315
):
316
all_insts.append(drums)
317
need_drums = False
318
all_insts.append(inst)
319
320
if need_drums:
321
all_insts.append(drums)
322
323
samples = {}
324
for addr in sorted(sample_addrs):
325
sample_size = 16 if is_shindou else 20
326
sample_data = data[addr : addr + sample_size]
327
samples[addr] = parse_sample(sample_data, data, sample_bank, is_shindou)
328
samples[addr].tunings.extend(tunings[addr])
329
330
env_data = {}
331
used_env_addrs = set()
332
for addr in sorted(env_addrs):
333
env = parse_envelope(addr, data)
334
env_data[addr] = env
335
for i in range(align(len(env), 4)):
336
used_env_addrs.add(addr + i * 4)
337
338
# Unused envelopes
339
unused_envs = set()
340
if used_env_addrs:
341
for addr in range(min(used_env_addrs) + 4, max(used_env_addrs), 4):
342
if addr not in used_env_addrs:
343
unused_envs.add(addr)
344
(stub_marker,) = struct.unpack(">I", data[addr : addr + 4])
345
assert stub_marker == 0
346
env = parse_envelope(addr, data)
347
env_data[addr] = env
348
for i in range(align(len(env), 4)):
349
used_env_addrs.add(addr + i * 4)
350
351
envelopes = {}
352
for addr in sorted(env_data.keys()):
353
env_name = gen_name("envelope")
354
if addr in unused_envs:
355
env_name += "_unused"
356
envelopes[addr] = Envelope(env_name, env_data[addr])
357
358
return Bank(
359
name,
360
iso_date,
361
sample_bank,
362
insts,
363
drums,
364
all_insts,
365
inst_list,
366
envelopes,
367
samples,
368
)
369
370
371
def parse_seqfile(data, filetype):
372
magic, num_entries = struct.unpack(">HH", data[:4])
373
assert magic == filetype
374
prev = align(4 + num_entries * 8, 16)
375
entries = []
376
for i in range(num_entries):
377
offset, length = struct.unpack(">II", data[4 + i * 8 : 4 + i * 8 + 8])
378
if filetype == TYPE_CTL:
379
assert offset == prev
380
else:
381
assert offset <= prev
382
prev = max(prev, offset + length)
383
entries.append((offset, length))
384
assert all(x == 0 for x in data[prev:])
385
return entries
386
387
388
def parse_sh_header(data, filetype):
389
(num_entries,) = struct.unpack(">H", data[:2])
390
assert data[2:16] == b"\0" * 14
391
prev = 0
392
entries = []
393
for i in range(num_entries):
394
subdata = data[16 + 16 * i : 32 + 16 * i]
395
offset, length, magic = struct.unpack(">IIH", subdata[:10])
396
assert offset == prev
397
assert magic == (0x0204 if filetype == TYPE_TBL else 0x0203)
398
prev = offset + length
399
if filetype == TYPE_CTL:
400
assert subdata[14:16] == b"\0" * 2
401
sample_bank_index, magic2, num_instruments, num_drums = struct.unpack(
402
">BBBB", subdata[10:14]
403
)
404
assert magic2 == 0xFF
405
entries.append(
406
(offset, length, (sample_bank_index, num_instruments, num_drums))
407
)
408
else:
409
assert subdata[10:16] == b"\0" * 6
410
entries.append((offset, length))
411
return entries
412
413
414
def parse_tbl(data, entries):
415
seen = {}
416
tbls = []
417
sample_banks = []
418
sample_bank_map = {}
419
for (offset, length) in entries:
420
if offset not in seen:
421
name = gen_name("sample_bank")
422
seen[offset] = name
423
sample_bank = SampleBank(name, data[offset : offset + length], offset)
424
sample_banks.append(sample_bank)
425
sample_bank_map[name] = sample_bank
426
tbls.append(seen[offset])
427
return tbls, sample_banks, sample_bank_map
428
429
430
class AifcWriter:
431
def __init__(self, out):
432
self.out = out
433
self.sections = []
434
self.total_size = 0
435
436
def add_section(self, tp, data):
437
assert isinstance(tp, bytes)
438
assert isinstance(data, bytes)
439
self.sections.append((tp, data))
440
self.total_size += align(len(data), 2) + 8
441
442
def add_custom_section(self, tp, data):
443
self.add_section(b"APPL", b"stoc" + self.pstring(tp) + data)
444
445
def pstring(self, data):
446
return bytes([len(data)]) + data + (b"" if len(data) % 2 else b"\0")
447
448
def finish(self):
449
# total_size isn't used, and is regularly wrong. In particular, vadpcm_enc
450
# preserves the size of the input file...
451
self.total_size += 4
452
self.out.write(b"FORM" + struct.pack(">I", self.total_size) + b"AIFC")
453
for (tp, data) in self.sections:
454
self.out.write(tp + struct.pack(">I", len(data)))
455
self.out.write(data)
456
if len(data) % 2:
457
self.out.write(b"\0")
458
459
460
def write_aifc(entry, out):
461
writer = AifcWriter(out)
462
num_channels = 1
463
data = entry.data
464
assert len(data) % 9 == 0
465
if len(data) % 2 == 1:
466
data += b"\0"
467
# (Computing num_frames this way makes it off by one when the data length
468
# is odd. It matches vadpcm_enc, though.)
469
num_frames = len(data) * 16 // 9
470
sample_size = 16 # bits per sample
471
472
if len(set(entry.tunings)) == 1:
473
sample_rate = 32000 * entry.tunings[0]
474
else:
475
# Some drum sounds in sample bank B don't have unique sample rates, so
476
# we have to guess. This doesn't matter for matching, it's just to make
477
# the sounds easy to listen to.
478
if min(entry.tunings) <= 0.5 <= max(entry.tunings):
479
sample_rate = 16000
480
elif min(entry.tunings) <= 1.0 <= max(entry.tunings):
481
sample_rate = 32000
482
elif min(entry.tunings) <= 1.5 <= max(entry.tunings):
483
sample_rate = 48000
484
elif min(entry.tunings) <= 2.5 <= max(entry.tunings):
485
sample_rate = 80000
486
else:
487
sample_rate = 16000 * (min(entry.tunings) + max(entry.tunings))
488
489
writer.add_section(
490
b"COMM",
491
struct.pack(">hIh", num_channels, num_frames, sample_size)
492
+ serialize_f80(sample_rate)
493
+ b"VAPC"
494
+ writer.pstring(b"VADPCM ~4-1"),
495
)
496
writer.add_section(b"INST", b"\0" * 20)
497
table_data = b"".join(struct.pack(">h", x) for x in entry.book.table)
498
writer.add_custom_section(
499
b"VADPCMCODES",
500
struct.pack(">hhh", 1, entry.book.order, entry.book.npredictors) + table_data,
501
)
502
writer.add_section(b"SSND", struct.pack(">II", 0, 0) + data)
503
if entry.loop.count != 0:
504
writer.add_custom_section(
505
b"VADPCMLOOPS",
506
struct.pack(
507
">HHIIi16h",
508
1,
509
1,
510
entry.loop.start,
511
entry.loop.end,
512
entry.loop.count,
513
*entry.loop.state
514
),
515
)
516
writer.finish()
517
518
519
def write_aiff(entry, filename):
520
temp = tempfile.NamedTemporaryFile(suffix=".aifc", delete=False)
521
try:
522
write_aifc(entry, temp)
523
temp.flush()
524
temp.close()
525
aifc_decode = os.path.join(os.path.dirname(__file__), "aifc_decode")
526
subprocess.run([aifc_decode, temp.name, filename], check=True)
527
finally:
528
temp.close()
529
os.remove(temp.name)
530
531
532
# Modified from https://stackoverflow.com/a/25935321/1359139, cc by-sa 3.0
533
class NoIndent(object):
534
def __init__(self, value):
535
self.value = value
536
537
538
class NoIndentEncoder(json.JSONEncoder):
539
def __init__(self, *args, **kwargs):
540
super(NoIndentEncoder, self).__init__(*args, **kwargs)
541
self._replacement_map = {}
542
543
def default(self, o):
544
def ignore_noindent(o):
545
if isinstance(o, NoIndent):
546
return o.value
547
return self.default(o)
548
549
if isinstance(o, NoIndent):
550
key = uuid.uuid4().hex
551
self._replacement_map[key] = json.dumps(o.value, default=ignore_noindent)
552
return "@@%s@@" % (key,)
553
else:
554
return super(NoIndentEncoder, self).default(o)
555
556
def encode(self, o):
557
result = super(NoIndentEncoder, self).encode(o)
558
repl_map = self._replacement_map
559
560
def repl(m):
561
key = m.group()[3:-3]
562
return repl_map[key]
563
564
return re.sub(r"\"@@[0-9a-f]*?@@\"", repl, result)
565
566
567
def inst_ifdef_json(bank_index, inst_index):
568
if bank_index == 7 and inst_index >= 13:
569
return NoIndent(["VERSION_US", "VERSION_EU"])
570
if bank_index == 8 and inst_index >= 16:
571
return NoIndent(["VERSION_US", "VERSION_EU"])
572
if bank_index == 10 and inst_index >= 14:
573
return NoIndent(["VERSION_US", "VERSION_EU"])
574
return None
575
576
577
def main():
578
args = []
579
need_help = False
580
only_samples = False
581
only_samples_list = []
582
shindou_headers = None
583
skip_next = 0
584
for i, a in enumerate(sys.argv[1:], 1):
585
if skip_next > 0:
586
skip_next -= 1
587
continue
588
if a == "--help" or a == "-h":
589
need_help = True
590
elif a == "--only-samples":
591
only_samples = True
592
elif a == "--shindou-headers":
593
shindou_headers = sys.argv[i + 1 : i + 5]
594
skip_next = 4
595
elif a.startswith("-"):
596
print("Unrecognized option " + a)
597
sys.exit(1)
598
elif only_samples:
599
only_samples_list.append(a)
600
else:
601
args.append(a)
602
603
expected_num_args = 5 + (0 if only_samples else 2)
604
if (
605
need_help
606
or len(args) != expected_num_args
607
or (shindou_headers and len(shindou_headers) != 4)
608
):
609
print(
610
"Usage: {}"
611
" <.z64 rom> <ctl offset> <ctl size> <tbl offset> <tbl size>"
612
" [--shindou-headers <ctl header offset> <ctl header size>"
613
" <tbl header offset> <tbl header size>]"
614
" (<samples outdir> <sound bank outdir> |"
615
" --only-samples file:index ...)".format(sys.argv[0])
616
)
617
sys.exit(0 if need_help else 1)
618
619
rom_file = open(args[0], "rb")
620
621
def read_at(offset, size):
622
rom_file.seek(int(offset))
623
return rom_file.read(int(size))
624
625
ctl_data = read_at(args[1], args[2])
626
tbl_data = read_at(args[3], args[4])
627
628
ctl_header_data = None
629
tbl_header_data = None
630
if shindou_headers:
631
ctl_header_data = read_at(shindou_headers[0], shindou_headers[1])
632
tbl_header_data = read_at(shindou_headers[2], shindou_headers[3])
633
634
if not only_samples:
635
samples_out_dir = args[5]
636
banks_out_dir = args[6]
637
638
banks = []
639
640
if shindou_headers:
641
ctl_entries = parse_sh_header(ctl_header_data, TYPE_CTL)
642
tbl_entries = parse_sh_header(tbl_header_data, TYPE_TBL)
643
644
sample_banks = parse_tbl(tbl_data, tbl_entries)[1]
645
646
for index, (offset, length, sh_meta) in enumerate(ctl_entries):
647
sample_bank = sample_banks[sh_meta[0]]
648
entry = ctl_data[offset : offset + length]
649
header = (sh_meta[1], sh_meta[2], "0000-00-00")
650
banks.append(parse_ctl(header, entry, sample_bank, index, True))
651
else:
652
ctl_entries = parse_seqfile(ctl_data, TYPE_CTL)
653
tbl_entries = parse_seqfile(tbl_data, TYPE_TBL)
654
assert len(ctl_entries) == len(tbl_entries)
655
656
tbls, sample_banks, sample_bank_map = parse_tbl(tbl_data, tbl_entries)
657
658
for index, (offset, length), sample_bank_name in zip(
659
range(len(ctl_entries)), ctl_entries, tbls
660
):
661
sample_bank = sample_bank_map[sample_bank_name]
662
entry = ctl_data[offset : offset + length]
663
header = parse_ctl_header(entry[:16])
664
banks.append(parse_ctl(header, entry[16:], sample_bank, index, False))
665
666
# Special mode used for asset extraction: generate aifc files, with paths
667
# given by command line arguments
668
if only_samples:
669
index_to_filename = {}
670
created_dirs = set()
671
for arg in only_samples_list:
672
filename, index = arg.rsplit(":", 1)
673
index_to_filename[int(index)] = filename
674
index = -1
675
for sample_bank in sample_banks:
676
offsets = sorted(set(sample_bank.entries.keys()))
677
for offset in offsets:
678
entry = sample_bank.entries[offset]
679
index += 1
680
if index in index_to_filename:
681
filename = index_to_filename[index]
682
dir = os.path.dirname(filename)
683
if dir not in created_dirs:
684
os.makedirs(dir, exist_ok=True)
685
created_dirs.add(dir)
686
write_aiff(entry, filename)
687
return
688
689
# Generate aiff files
690
for sample_bank in sample_banks:
691
dir = os.path.join(samples_out_dir, sample_bank.name)
692
os.makedirs(dir, exist_ok=True)
693
694
offsets = sorted(set(sample_bank.entries.keys()))
695
# print(sample_bank.name, len(offsets), 'entries')
696
offsets.append(len(sample_bank.data))
697
698
assert 0 in offsets
699
for offset, next_offset, index in zip(
700
offsets, offsets[1:], range(len(offsets))
701
):
702
entry = sample_bank.entries[offset]
703
entry.name = "{:02X}".format(index)
704
size = next_offset - offset
705
assert size % 16 == 0
706
assert size - 15 <= len(entry.data) <= size
707
garbage = sample_bank.data[offset + len(entry.data) : offset + size]
708
if len(entry.data) % 2 == 1:
709
assert garbage[0] == 0
710
if next_offset != offsets[-1]:
711
# (The last chunk follows a more complex garbage pattern)
712
assert all(x == 0 for x in garbage)
713
filename = os.path.join(dir, entry.name + ".aiff")
714
write_aiff(entry, filename)
715
716
# Generate sound bank .json files
717
os.makedirs(banks_out_dir, exist_ok=True)
718
for bank_index, bank in enumerate(banks):
719
filename = os.path.join(banks_out_dir, bank.name + ".json")
720
with open(filename, "w") as out:
721
722
def sound_to_json(sound):
723
entry = bank.samples[sound.sample_addr]
724
if len(set(entry.tunings)) == 1:
725
return entry.name
726
return {"sample": entry.name, "tuning": round_f32(sound.tuning)}
727
728
bank_json = {
729
"date": bank.iso_date,
730
"sample_bank": bank.sample_bank.name,
731
"envelopes": {},
732
"instruments": {},
733
"instrument_list": [],
734
}
735
addr_to_name = {}
736
737
# Envelopes
738
for env in bank.envelopes.values():
739
env_json = []
740
for (delay, arg) in env.entries:
741
if delay == 0:
742
ins = "stop"
743
assert arg == 0
744
elif delay == 2 ** 16 - 1:
745
ins = "hang"
746
assert arg == 0
747
elif delay == 2 ** 16 - 2:
748
ins = ["goto", arg]
749
elif delay == 2 ** 16 - 3:
750
ins = "restart"
751
assert arg == 0
752
else:
753
ins = [delay, arg]
754
env_json.append(NoIndent(ins))
755
bank_json["envelopes"][env.name] = env_json
756
757
# Instruments/drums
758
for inst_index, inst in enumerate(bank.all_insts):
759
if isinstance(inst, Inst):
760
inst_json = {
761
"ifdef": inst_ifdef_json(bank_index, inst_index),
762
"release_rate": inst.release_rate,
763
"normal_range_lo": inst.normal_range_lo,
764
"normal_range_hi": inst.normal_range_hi,
765
"envelope": bank.envelopes[inst.envelope].name,
766
}
767
768
if inst_json["ifdef"] is None:
769
del inst_json["ifdef"]
770
771
if inst.sound_lo is not None:
772
inst_json["sound_lo"] = NoIndent(sound_to_json(inst.sound_lo))
773
else:
774
del inst_json["normal_range_lo"]
775
776
inst_json["sound"] = NoIndent(sound_to_json(inst.sound_med))
777
778
if inst.sound_hi is not None:
779
inst_json["sound_hi"] = NoIndent(sound_to_json(inst.sound_hi))
780
else:
781
del inst_json["normal_range_hi"]
782
783
bank_json["instruments"][inst.name] = inst_json
784
addr_to_name[inst.addr] = inst.name
785
786
else:
787
assert isinstance(inst, list)
788
drums_list_json = []
789
for drum in inst:
790
drum_json = {
791
"release_rate": drum.release_rate,
792
"pan": drum.pan,
793
"envelope": bank.envelopes[drum.envelope].name,
794
"sound": sound_to_json(drum.sound),
795
}
796
drums_list_json.append(NoIndent(drum_json))
797
bank_json["instruments"]["percussion"] = drums_list_json
798
799
# Instrument lists
800
for addr in bank.inst_list:
801
if addr is None:
802
bank_json["instrument_list"].append(None)
803
else:
804
bank_json["instrument_list"].append(addr_to_name[addr])
805
806
out.write(json.dumps(bank_json, indent=4, cls=NoIndentEncoder))
807
out.write("\n")
808
809
810
if __name__ == "__main__":
811
main()
812
813