sm64

A Super Mario 64 decompilation
Log | Files | Refs | README | LICENSE

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()