Skip to content

Instantly share code, notes, and snippets.

@osyu
Last active October 25, 2024 21:45
Show Gist options
  • Select an option

  • Save osyu/c3390a4c1c32aaef0ea6d59c2fcb4ea0 to your computer and use it in GitHub Desktop.

Select an option

Save osyu/c3390a4c1c32aaef0ea6d59c2fcb4ea0 to your computer and use it in GitHub Desktop.
RE Engine ASRC encoder/decoder (for AJ:AA Trilogy)
import argparse
import shutil
import wave
DESCRIPTION = """Encode and decode RE Engine .asrc audio files.
Now with more confusion! For version 31 (AJ:AA Trilogy) only."""
SRCH_INFO = """srch (stub)
id: %d"""
SRCD_INFO = """srcd (data)
id: %d
unk0: %d
unk1: %d
urate: %d
soff: %s
strm: %s
loop: %s
lps: %d
lpe: %d
mark: %s
channels: %d
samples: %d
rate: %d
depth: %d"""
read_u32 = lambda f: int.from_bytes(f.read(4), 'little')
write_u32 = lambda f, x: f.write(x.to_bytes(4, 'little'))
def format_marker_list(ml):
if ml is None:
return
for i, m in enumerate(ml):
if m[0] == 0xffffffff:
ml[i] = (m[1],)
return ','.join(':'.join(str(y) for y in x) for x in ml)
def parse_marker_list(ms):
ml = []
if ms is not None:
for m in ms.split(','):
try:
mfields = tuple(int(x) for x in m.split(':', 2))
except ValueError as e:
raise ValueError("invalid marker list") from e
if len(mfields) == 1:
mfields = (0xffffffff, mfields[0])
ml.append(mfields)
return ml
def encode(args, pre=False):
with wave.open(args.file) as w:
params = w.getparams()
data_offset = args.file.tell()
args.file.seek(0, 2)
file_size = args.file.tell()
args.file.seek(0)
if pre and args.cpb:
loop = args.loop
else:
loop = not all(x is None for x in (args.lps, args.lpe))
if args.lps is None:
args.lps = 0
if args.lpe is None:
args.lpe = params.nframes - 1
if pre:
mark = args.mark or []
else:
mark = parse_marker_list(args.mark)
with open(args.out, 'wb') as of:
of.write(b'srcd')
write_u32(of, 0) # always 0
write_u32(of, file_size)
of.write(b'wav ')
write_u32(of, args.strm)
write_u32(of, args.id)
write_u32(of, args.unk0)
write_u32(of, params.nchannels)
write_u32(of, params.nframes * params.nchannels + args.soff)
write_u32(of, args.urate)
write_u32(of, params.framerate)
write_u32(of, params.sampwidth * 8)
write_u32(of, 1) # always 1
of.write(bytes((loop,)))
write_u32(of, args.lps)
write_u32(of, args.lpe)
write_u32(of, len(mark))
for m in mark:
write_u32(of, m[0])
write_u32(of, m[1])
of.write(b'\0' * 9) # always 0
write_u32(of, args.unk1)
write_u32(of, of.tell() + 8)
write_u32(of, data_offset)
shutil.copyfileobj(args.file, of)
args.file.close()
def replace(args):
mi = lambda: None
mi.file = args.base
mi = info(mi, prnt=False)
args.base.close()
with wave.open(args.file) as w:
params = w.getparams()
args.file.seek(0)
if not args.cpb and bool(args.mark) != bool(mi.mark):
raise ValueError("markers %srequired for this file" %
("not " if mi.mark is None else ""))
for a in dir(mi):
if not a.startswith('__'):
if a in args and not args.cpb:
continue
setattr(args, a, getattr(mi, a))
encode(args, pre=True)
def decode(args):
# calling info will validate and seek to end of header
info(args, prnt=False)
with open(args.out, 'wb') as of:
shutil.copyfileobj(args.file, of)
args.file.close()
def info(args, prnt=True):
f = args.file
magic = f.read(4)
if magic == b'srch':
assert read_u32(f) == 8 # always 8
sid = read_u32(f)
assert read_u32(f) == 1 # always 1
if prnt:
print(SRCH_INFO % sid)
else:
raise ValueError("srch files contain no audio data")
return
elif magic != b'srcd':
raise ValueError("not a valid asrc file")
# mock object to pass to other funcs
mi = lambda: None
assert read_u32(f) == 0 # always 0
file_size = read_u32(f)
assert f.read(4) == b'wav '
mi.strm = read_u32(f)
assert mi.strm <= 1
mi.strm = bool(mi.strm)
mi.id = read_u32(f)
mi.unk0 = read_u32(f)
mi.channels = read_u32(f)
mi.samples = read_u32(f)
mi.urate = read_u32(f)
mi.rate = read_u32(f)
mi.depth = read_u32(f)
assert read_u32(f) == 1 # always 1
mi.loop = ord(f.read(1))
assert mi.loop <= 1
mi.loop = bool(mi.loop)
mi.lps = read_u32(f)
mi.lpe = read_u32(f)
mark_count = read_u32(f)
if mark_count > 0:
mi.mark = []
for _ in range(mark_count):
mi.mark.append((read_u32(f), read_u32(f)))
else:
mi.mark = None
assert f.read(9) == b'\0' * 9 # always 0
mi.unk1 = read_u32(f)
header_size = read_u32(f)
data_offset = read_u32(f)
assert header_size == f.tell()
f.seek(0, 2)
assert file_size == f.tell() - header_size
f.seek(header_size)
with wave.open(f) as w:
params = w.getparams()
assert data_offset == f.tell() - header_size
f.seek(header_size)
mi.soff = mi.samples % mi.channels != 0
if mi.soff:
mi.samples -= 1
assert mi.channels == params.nchannels
assert mi.samples == params.nframes * params.nchannels
assert mi.rate == params.framerate
assert mi.depth == params.sampwidth * 8
if prnt:
mi.mark = format_marker_list(mi.mark)
print(SRCD_INFO % (mi.id, mi.unk0, mi.unk1, mi.urate,
mi.soff, mi.strm, mi.loop, mi.lps, mi.lpe, mi.mark,
mi.channels, mi.samples, mi.rate, mi.depth))
f.close()
return mi
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=DESCRIPTION,
formatter_class=argparse.RawTextHelpFormatter)
subparsers = parser.add_subparsers(dest='command',
help="command (encode/replace/decode/info)")
enc_parser = subparsers.add_parser('e')
enc_parser.add_argument('-soff', action='store_true',
help="offset sample count by one")
enc_parser.add_argument('-strm', action='store_true',
help="mark audio as streaming")
enc_parser.add_argument('-lps', type=int, metavar='POS',
help="loop start sample position")
enc_parser.add_argument('-lpe', type=int, metavar='POS',
help="loop end sample position")
enc_parser.add_argument('-mark', type=str, metavar='LIST',
help="comma-separated list of markers")
enc_parser.add_argument('id', type=int,
help="audio source id (check existing file using i command)")
enc_parser.add_argument('unk0', type=int,
help="unknown int (check existing file)")
enc_parser.add_argument('unk1', type=int,
help="unknown int (check existing file)")
enc_parser.add_argument('urate', type=int,
help="unknown sample rate (check existing file)")
enc_parser.add_argument('file', type=argparse.FileType('rb'),
help="path to input file")
enc_parser.add_argument('out', type=str,
help="path to output file")
rep_parser = subparsers.add_parser('r')
rep_parser.add_argument('-lps', type=int, metavar='POS',
help="loop start sample position")
rep_parser.add_argument('-lpe', type=int, metavar='POS',
help="loop end sample position")
rep_parser.add_argument('-mark', type=str, metavar='LIST',
help="comma-separated list of markers")
rep_parser.add_argument('-cpb', action='store_true',
help="copy loop and markers from base")
rep_parser.add_argument('file', type=argparse.FileType('rb'),
help="path to input file")
rep_parser.add_argument('base', type=argparse.FileType('rb'),
help="path to base file")
rep_parser.add_argument('out', type=str,
help="path to output file")
dec_parser = subparsers.add_parser('d')
dec_parser.add_argument('file', type=argparse.FileType('rb'),
help="path to input file")
dec_parser.add_argument('out', type=str,
help="path to output file")
inf_parser = subparsers.add_parser('i')
inf_parser.add_argument('file', type=argparse.FileType('rb'),
help="path to input file")
mappings = {'e': encode, 'r': replace, 'd': decode, 'i': info}
args = parser.parse_args()
if args.command in mappings:
mappings[args.command](args)
else:
parser.print_help()
@AnEnglishman
Copy link

Hello. I'm not good with .py files, could you explain how to use this to decode files?

@osyu
Copy link
Author

osyu commented Mar 23, 2024

Assuming you're on Windows:

  • Install Python
  • Shift+right-click on the .py file and select "Copy as path" to copy the file's full path
  • Navigate to the folder with the file you want to decode, type cmd into the address bar to open a command prompt there
  • Paste the path you copied earlier, add d <filename> to it (e.g. "C:\Users\whatever\Downloads\asrc31.py" d bgm_063.asrc.31) and hit enter

@KudoDerg
Copy link

KudoDerg commented Apr 23, 2024

Thank you for providing this amazing script, the only problem about this script is the information of loop points in asrc31 aren't correct including samples
https://cdn.discordapp.com/attachments/1232318120769618039/1232318177879527544/Capture.PNG?ex=6629053c&is=6627b3bc&hm=a0172fb8781979d01dbb4225da1968fb4129f987e8c83bd823b2af0074544fee&

@osyu
Copy link
Author

osyu commented Apr 24, 2024

Could you attach the image to your comment, the link you posted is dead

@KudoDerg
Copy link

Nevermind, the loop points are correct. I was using latest version of audio editing software called "Audacity" and it gives me wrong samples count so I had to use a certain old version instead and it worked perfectly, sorry about that

Could you attach the image to your comment, the link you posted is dead

@bemwy
Copy link

bemwy commented Oct 9, 2024

Hello! I am using the files from the Nintendo Switch version of this trilogy, which seems to use Ogg files instead of wav files, I have been able to manually get the audio out but is there any way to do this with this script?

@osyu
Copy link
Author

osyu commented Oct 9, 2024

@King-of-the-all-Cookies
Traceback (most recent call last):
  File "C:\Users\romas\Desktop\ajt scripts\asrc decoder encoder\main.py", line 309, in <module>
    mappings[args.command](args)
  File "C:\Users\romas\Desktop\ajt scripts\asrc decoder encoder\main.py", line 150, in decode
    info(args, prnt=False)
  File "C:\Users\romas\Desktop\ajt scripts\asrc decoder encoder\main.py", line 170, in info
    raise ValueError("srch files contain no audio data")
ValueError: srch files contain no audio data

occurs when trying to open asrc from AJT

@osyu
Copy link
Author

osyu commented Oct 25, 2024

.asrc files in natives/stm/sound only contain an id; the corresponding files in natives/stm/streaming/sound have the audio data

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment