Created
November 10, 2020 06:21
-
-
Save depp/20d20ba2cc568ce8dd1de4ad5e90369d to your computer and use it in GitHub Desktop.
Nintendo 64 CTL Dumper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Dump a Nintendo 64 .ctl file to text. | |
Usage: python3 ctl.py <file.ctl> | |
""" | |
import argparse | |
import struct | |
import sys | |
SPACE = ' ' * 80 | |
def pr(indent, *msg): | |
space = SPACE[:indent] | |
for arg in msg: | |
sys.stdout.write(space) | |
sys.stdout.write(str(arg)) | |
space = ' ' | |
sys.stdout.write('\n') | |
ENV_FIELDS = ["attackTime", "decayTime", "releaseTime", "attackVolume", | |
"decayVolume"] | |
def read_envelope(data, ptr, n): | |
fields = struct.unpack('>iiiBB', data[ptr:ptr+14]) | |
assert len(fields) == len(ENV_FIELDS) | |
for name, value in zip(ENV_FIELDS, fields): | |
pr(n, '{} = {}'.format(name, value)) | |
KMAP_FIELDS = ["velocityMin", "velocityMax", "keyMin", "keyMax", "keyBase", | |
"detune"] | |
def read_kmap(data, ptr, n): | |
fields = struct.unpack('>BBBBBb', data[ptr:ptr+6]) | |
assert len(fields) == len(KMAP_FIELDS) | |
for name, value in zip(KMAP_FIELDS, fields): | |
pr(n, '{} = {}'.format(name, value)) | |
ADPCM_LOOP_FIELDS = ["start", "end", "count"] | |
def read_adpcm_loop(data, ptr, n): | |
fields = struct.unpack('>III', data[ptr:ptr+12]) | |
for (name, value) in zip(ADPCM_LOOP_FIELDS, fields): | |
pr(n, name, '=', value) | |
state = struct.unpack('>' + 'h' * 16, data[ptr+12:ptr+12+32]) | |
pr(n, 'state =', state) | |
def read_adpcm_book(data, ptr, n): | |
pr(n, '<unimplemented>') | |
def read_adpcm_info(data, ptr, n): | |
loop, book = struct.unpack('>II', data[ptr:ptr+8]) | |
if loop: | |
pr(n, 'loop = {') | |
read_adpcm_loop(data, loop, n + 2) | |
pr(n, '}') | |
else: | |
pr(n, 'loop = NULL') | |
pr(n, 'book = {') | |
read_adpcm_book(data, book, n + 2) | |
pr(n, '}') | |
RAW_LOOP_FIELDS = ["start", "end", "count"] | |
def read_raw_loop(data, ptr, n): | |
fields = struct.unpack('>III', data[ptr:ptr+12]) | |
for (name, value) in zip(RAW_LOOP_FIELDS, fields): | |
pr(n, name, '=', value) | |
def read_raw_info(data, ptr, n): | |
loop, = struct.unpack('>I', data[ptr:ptr+4]) | |
if loop: | |
pr(n, 'loop = {') | |
read_raw_loop(data, loop, n + 2) | |
pr(n, '}') | |
else: | |
pr(n, 'loop = NULL') | |
def read_wavetable(data, ptr, n): | |
base, wlen, wtype, flags = struct.unpack('>IIBB', data[ptr:ptr+10]) | |
pr(n, 'base =', base) | |
pr(n, 'len =', wlen) | |
pr(n, 'wtype =', wtype) | |
pr(n, 'flags =', flags) | |
if wtype == 0: | |
pr(n, 'waveInfo.adpcmWave = {') | |
read_adpcm_info(data, ptr + 12, n + 2) | |
pr(n, '}') | |
elif wtype == 1: | |
pr(n, 'waveInfo.rawWave = {') | |
read_raw_info(data, ptr + 12, n + 2) | |
pr(n, '}') | |
def read_sound(data, ptr, n): | |
env, kmap, wtable, pan, volume, flags = struct.unpack('>IIIBBB', data[ptr:ptr+15]) | |
m = n + 2 | |
pr(n, 'envelope = {') | |
read_envelope(data, env, m) | |
pr(n, '}') | |
pr(n, 'kmap = {') | |
read_kmap(data, kmap, m) | |
pr(n, '}') | |
pr(n, 'wtable = {') | |
read_wavetable(data, wtable, m) | |
pr(n, '}') | |
pr(n, 'pan =', pan) | |
pr(n, 'volume =', volume) | |
pr(n, 'flags =', flags) | |
INSTRUMENT_FNAMES = ["volume", "pan", "priority", "flags", "tremType", | |
"tremRate", "tremDepth", "tremDelay", "vibType", "vibRate", "vibDepth", | |
"vibDelay", "bendRange", "soundCount"] | |
def read_instrument(data, ptr, n): | |
fields = struct.unpack('>BBBBBBBBBBBBhh', data[ptr:ptr+16]) | |
assert len(fields) == len(INSTRUMENT_FNAMES) | |
for name, value in zip(INSTRUMENT_FNAMES, fields): | |
pr(n, '{} = {}'.format(name, value)) | |
count = fields[-1] | |
m = n + 2 | |
for i in range(count): | |
pr(n, 'soundArray[{}] = {{'.format(i)) | |
sptr, = struct.unpack('>I', data[ptr+16+4*i:ptr+20+4*i]) | |
read_sound(data, sptr, m) | |
pr(n, '}') | |
def read_bank(data, ptr, n): | |
count, flags, pad, rate, perc = struct.unpack('>hBBiI', data[ptr:ptr+12]) | |
pr(n, 'instCount =', count) | |
pr(n, 'flags = 0x{:02x}'.format(flags)) | |
pr(n, 'pad =', pad) | |
pr(n, 'sampleRate =', rate) | |
m = n + 2 | |
if not perc: | |
pr(n, 'percussion = NULL') | |
else: | |
pr(n, 'percussion = {') | |
read_instrument(data, perc, m) | |
pr(n, '}') | |
for i in range(count): | |
iptr, = struct.unpack('>I', data[ptr+12+4*i:ptr+16+4*i]) | |
pr(n, 'instArray[{}] = {{'.format(i)) | |
read_instrument(data, iptr, m) | |
pr(n, '}') | |
def read_bankfile(data, ptr, n): | |
rev, count = struct.unpack('>hh', data[ptr:ptr+4]) | |
pr(n, 'revision =', rev) | |
pr(n, 'bankCount =', count) | |
m = n + 2 | |
for i in range(count): | |
pr(n, 'bankArray[{}] = {{'.format(i)) | |
bptr, = struct.unpack('>I', data[ptr+4+4*i:ptr+8+4*i]) | |
read_bank(data, bptr, m) | |
pr(n, '}') | |
def main(argv): | |
p = argparse.ArgumentParser('ctl.py') | |
p.add_argument('ctl') | |
args = p.parse_args(argv) | |
with open(args.ctl, 'rb') as fp: | |
data = fp.read() | |
read_bankfile(data, 0, 0) | |
if __name__ == '__main__': | |
main(sys.argv[1:]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment