disassemble_sound.py (26320B)
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()