Skip to content

Instantly share code, notes, and snippets.

@CebolaBros64
Last active March 2, 2021 18:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CebolaBros64/3c809da8ea3381e07712f6670535dd02 to your computer and use it in GitHub Desktop.
Save CebolaBros64/3c809da8ea3381e07712f6670535dd02 to your computer and use it in GitHub Desktop.
Extract sound samples from a Rhythm Tengoku ROM file.

This script provides a CLI interface for exporting samples off of a Rhythm Tengoku (GBA) ROM file. It's still a work in progress. Requires Python >3.5

  -h, --help            show this help message and exit
  -v, --version         show program's version number and exit
  -xs ROM output, --export-samples ROM output
                        Export all samples referenced in the SFX table.
  -xt ROM output, --export-table ROM output
                        Export binary SFX table.
  -dt bin json, --decode-table bin json
                        Decode binary SFX table into a .json file. (NOT YET IMPLEMENTED)
  -et json bin, --encode-table json bin
                        Encode .json SFX table into a binary file.
import struct, json
from pathlib import Path
from functools import partial
struct_fmt = "<IIIIII" # little-endian, 6 integers
# friendly reminder: "integer" tipically means a 16-bit integer
# and a 16-bit integer is the same as a DWORD
def parse2list(_bin, unpack, length):
''' https://stackoverflow.com/a/14216741 '''
return [unpack(chunk) for chunk in iter(partial(_bin.read, length), b'')]
def banklist2dict(_list):
dictIndex = []
for item in _list:
itemDict = {}
itemDict['length'] = item[0]
itemDict['samplerate'] = item[1]
itemDict['pitch'] = item[2]
itemDict['looppoints'] = item[3]
itemDict['looppointl'] = item[4]
itemDict['start'] = item[5]
dictIndex.append(itemDict)
return dictIndex
def pointer_conv(p):
return int.from_bytes(b''.join(struct.unpack(">ccc", (struct.pack('>i', p)[1:]))), byteorder="big")
def decode_table(_bin):
struct_len = struct.calcsize(struct_fmt)
struct_unpack = struct.Struct(struct_fmt).unpack_from
parsedIndex = []
with open(_bin, 'rb') as f:
parsedIndex = parse2list(f, struct_unpack, struct_len)
return(banklist2dict(parsedIndex))
def decode_table_json(_bin):
return json.dumps(decode_table(_bin), indent=4)
def encode_table(f):
binIndex = bytearray()
dictIndex = json.loads(f.read()) # json -> dict
for item in dictIndex:
binItem = struct.pack(struct_fmt, item["length"], item["samplerate"], item["pitch"], item["looppoints"], item["looppointl"], item["start"])
binIndex += binItem
return binIndex
def export_samples(ROMIn, sampleTable, outputFolder): # samples is a list containing the id for the samples that will be exported
sample_count = 000
tengokuROM = ROMIn.read()
dictIndex = sampleTable
Path(outputFolder).mkdir(parents=True, exist_ok=True) # https://stackoverflow.com/a/273227
for item in dictIndex:
startOffset = pointer_conv(item['start'])
lengthOffset = startOffset + pointer_conv(item['length'])
with open(outputFolder+"/"+str(sample_count)+".raw", "wb") as outFile:
outFile.write(tengokuROM[startOffset:lengthOffset])
sample_count += 1
import argparse
import rtsoundbank
rtRomPath = 'rt_rom.gba'
output = 'RT_SFXIndex.bin'
sfxIndexOffset = 0xAA44FC
sfxIndexLength = 0x5A30
progName = 'TengokuTool.py'
progDesc = 'Hey baby, howzit going? This. Is. A work. In progress.'
def validate_extension(_str, ext, forceExt=True):
if forceExt:
if not _str[-len(ext):] == ext:
return _str + ext
else:
return _str
else:
if _str.find('.') != -1:
return _str
else:
return _str + ext
parser = argparse.ArgumentParser(prog=progName, description=progDesc)
parser.add_argument('-v', '--version', action='version',
version='%(prog)s 0.0')
parser.add_argument('-xs', '--export-samples',
metavar=('ROM', 'output'),
nargs=2,
help='Export all samples referenced in the SFX table.')
parser.add_argument('-xt', '--export-table',
metavar=('ROM', 'output'),
nargs=2,
help='Export binary SFX table.')
parser.add_argument('-dt', '--decode-table',
metavar=('bin', 'json'),
nargs=2,
help='Decode binary SFX table into a .json file. (NOT YET IMPLEMENTED)')
parser.add_argument('-et', '--encode-table',
metavar=('json', 'bin'),
nargs=2,
help='Encode .json SFX table into a binary file.')
args = parser.parse_args()
if args.decode_table:
pass
elif args.encode_table:
binIndex = bytearray()
with (open(args.encode_table[1], 'wb') as encodedOut,
open(args.encode_table[0], 'r') as jsonIn):
encodedOut.write(rtsoundbank.encode_table(jsonIn))
print('File successfully exported to', args.encode_table[1] + ".")
elif args.export_samples:
with (open(args.export_samples[0], 'rb') as ROMIn,
open("tempTable.bin", 'wb') as binTableOut):
tengokuROM = ROMIn.read()
binTableOut.write(tengokuROM[sfxIndexOffset:sfxIndexOffset+sfxIndexLength])
with open(args.export_samples[0], 'rb') as ROMIn:
outputFolder = args.export_samples[1]
rtsoundbank.export_samples(ROMIn,
rtsoundbank.decode_table("tempTable.bin"),
outputFolder)
elif args.export_table:
args.export_table[1] = validate_extension(args.export_table[1], ".bin", forceExt=False)
with (open(args.export_table[0], 'rb') as ROMIn,
open(args.export_table[1], 'wb') as binTableOut):
tengokuROM = ROMIn.read()
binTableOut.write(tengokuROM[sfxIndexOffset:sfxIndexOffset+sfxIndexLength])
else:
parser.parse_args('-h'.split())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment