Skip to content

Instantly share code, notes, and snippets.

@mmimigaa
Last active September 29, 2023 17:28
Show Gist options
  • Save mmimigaa/308109b6e595c990832c8fe5cbf20806 to your computer and use it in GitHub Desktop.
Save mmimigaa/308109b6e595c990832c8fe5cbf20806 to your computer and use it in GitHub Desktop.
from argparse import ArgumentParser
import subprocess
import shutil
import gzip
import sys
import os
# build list of offsets to check for chip clock rates based on the VGM version.
# if a chip's clock rate is not null, then it's (probably) used during playback,
# and stems for it should be rendered
def getVGMChips(header):
version = header[8]
print(f"VGM format version 1.{version:02X}\n")
chisps = []
# version 1.00
clocks = {0x0C: ("SN76496" , 4), 0x10: ("YM2413" , 14)}
if version >= 0x10: # version 1.10
clocks.update({0x2C: ("YM2612" , 7), 0x30: ("YM2151" , 8)})
if version >= 0x51: # version 1.51
clocks.update({0x38: ("SegaPCM" , 16), 0x40: ("RF5C68" , 8), 0x44: ("YM2203" , 6), 0x48: ("YM2608" , 16), 0x4C: ("YM2610" , 16),
0x50: ("YM3812" , 14), 0x54: ("YM3526" , 14), 0x58: ("Y8950" , 15), 0x5C: ("YMF262" , 23), 0x60: ("YMF278B" , 47),
0x64: ("YMF271" , 12), 0x68: ("YMZ280B", 8), 0x6C: ("RF5C68" , 8), 0x70: ("PWM" , 1), 0x74: ("AY8910" , 3)})
if version >= 0x61: # version 1.61
clocks.update({0x80: ("GameBoy" , 4), 0x84: ("NES APU", 6), 0x88: ("YMW258" , 28), 0x8C: ("uPD7759", 1), 0x90: ("OKIM6258", 1),
0x98: ("OKIM6295", 4), 0x9C: ("K051649", 5), 0xA0: ("K054539", 8), 0xA4: ("HuC6280", 6), 0xAC: ("K053260" , 4),
0xB0: ("Pokey" , 4), 0xB4: ("QSound" , 16)})
# the C140 and C219 share the same offset, so a separate flag must be checked
# to determine which of the two is actually used
if header[0xA8:0xAC] != b'\0' * 4:
if header[0x96] == 0x02:
chisps.append(("C219", 16))
else:
chisps.append(("C140", 24))
if version >= 0x71: # version 1.71
clocks.update({0xB8: ("SCSP" , 32), 0xC0: ("WSwan" , 4), 0xC4: ("VSU" , 6), 0xC8: ("SAA1099", 6), 0xCC: ("ES5503" , 32),
0xD8: ("X1-010" , 16), 0xDC: ("C352" , 32), 0xE0: ("GA20" , 4)})
# ^ ES5506 at 0xD0, but doesn't seem to be implemented ^ #
# check to see which chips are used
for o, c in clocks.items():
if (header[o:o + 4] != b'\0' * 4) and (c not in chisps):
chisps.append(c)
return chisps
# similar thing for S98 files
# TODO: support versions earlier than 3
def getS98Chips(header):
print("S98 format version 3\n")
chisps = []
chipIDs = {1: ("AY8910", 3), 2: ("YM2203", 6), 3: ("YM2612", 7), 4: ("YM2608", 16), 5: ("YM2151", 8), 6: ("YM2413", 14),
7: ("YM3526", 14), 8: ("YM3812", 14), 9: ("YMF262", 23), 15: ("AY8910", 3), 16: ("SN76496", 4)}
nChips = header[0x1C] # uint32, but max value is 64, so eh
# assume single YM2608 if no chips listed, for back compat with older format versions
if not nChips:
chisps.append(chipIDs[4])
else:
seek = 0x20
for _ in range(nChips):
cID = header[seek]
if cID:
chisps.append(chipIDs[cID])
seek += 0xF
return chisps
def main():
parser = ArgumentParser(description="Generate master and stem tracks from VGM/VGZ/S98 files for use with corrscope")
parser.add_argument("file", help="Path to input file")
parser.add_argument("-l", "--loops", type=int)
args = parser.parse_args()
# check if file is compressed
with open(args.file, "rb") as f:
header = f.read(2)
if header == b"\x1f\x8b": # gzip magic number. most likely a vgz. decompress it
header = gzip.decompress(header + f.read())[:256]
else: # something else
header += f.read(254)
# check file format
if header.startswith(b"Vgm "):
chisps = getVGMChips(header)
elif header.startswith(b"S983"):
chisps = getS98Chips(header)
else:
sys.exit(f"'{args.file}' is not a valid input file")
################################################################################
# additional configuration parameters to pass to VGMPlay. useful for setting things like chip core
# e.g. "YM2612.Core=NUKE"
# consult VGMPlay.ini for more info
additionalConfig = []
ps = []
basecmd = ["VGMPlay64", "-w"]
if args.loops:
basecmd += ["-c", f"General.MaxLoops={args.loops}"]
for a in additionalConfig:
basecmd += ["-c", a]
# VGMPlay does not allow the user to specify a custom output filename,
# so copies of the input file must be made with the filenames we want
# (these will be cleaned up afterwards)
# generate master track
shutil.copy(args.file, "master")
print("starting subprocess for master track...\n")
ps.append(subprocess.Popen(basecmd + ["master"], stdout=subprocess.DEVNULL))
for c in chisps:
mask = (2 ** c[1]) - 1
cmd = basecmd.copy()
# disable all other chips
for d in chisps:
if d[0] != c[0]:
cmd += ["-c", f"{d[0]}.Disabled=True"]
if d[0] == "YM2203" or d[0] == "YM2608" or d[0] == "YM2610":
cmd += ["-c", f"{d[0]}.DisableSSG=True"]
elif d[0] == "YMF278B":
cmd += ["-c", "YMF278B.DisableFM=True"]
for e in range(c[1]):
filename = f"{c[0]}-ch{e + 1}"
shutil.copy(args.file, filename)
mm = mask - (2 ** e)
# handle special MuteMask args for special chips
# TODO: figure out a less gross way of doing this
if c[0] == "YM2203":
if e <= 2: # first 3 FM channels
mm = mm & 0x7
nc = cmd + ["-c", "YM2203.DisableSSG=True", "-c", f"YM2203.MuteMask_FM=0x{mm:X}"]
else: # last 3 SSG channels
mm = mm >> 3
nc = cmd + ["-c", "YM2203.Disabled=True", "-c", f"YM2203.MuteMask_SSG=0x{mm:X}"]
elif c[0] == "YM2608" or c[0] == "YM2610":
if e <= 5: # first 6 FM channels
mm = mm & 0x3F
nc = cmd + ["-c", f"{c[0]}.DisableSSG=True", "-c", f"{c[0]}.MuteMask_PCM=0x7F", "-c", f"{c[0]}.MuteMask_FM=0x{mm:X}"]
elif e <= 12: # next 6 ADPCM channels + 1 Delta-T channel
mm = (mm >> 6) & 0x7F
nc = cmd + ["-c", f"{c[0]}.DisableSSG=True", "-c", f"{c[0]}.MuteMask_FM=0x3F", "-c", f"{c[0]}.MuteMask_PCM=0x{mm:X}"]
else:
mm = mm >> 13 # last 3 SSG channels
nc = cmd + ["-c", f"{c[0]}.Disabled=True", "-c", f"{c[0]}.MuteMask_SSG=0x{mm:X}"]
elif c[0] == "YMF278B":
if e <= 22: # first 23 FM channels
mm = mm & 0x7FFFFF
nc = cmd + ["-c", "YMF278B.Disabled=True", "-c", f"YMF278B.MuteMask_FM=0x{mm:X}"]
else:
mm = mm >> 23 # last 24 wavetable channels
nc = cmd + ["-c", "YMF278B.DisableFM=True", "-c", f"YMF278B.MuteMask_WT=0x{mm:X}"]
else:
nc = cmd + ["-c", f"{c[0]}.MuteMask=0x{mm:X}"]
nc.append(filename)
print(f"starting subprocess for {c[0]} channel {e + 1}...")
#print(nc) # debug
ps.append(subprocess.Popen(nc, stdout=subprocess.DEVNULL))
# wait for all subprocesses to terminate
for p in ps:
p.wait()
print("\ndone. cleaning up...")
# delete copies of input file
os.remove("master")
for c in chisps:
for e in range(c[1]):
os.remove(f"{c[0]}-ch{e + 1}")
if __name__ == "__main__":
main()
@RiedleroD
Copy link

thanks ^^

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