From cb2cfddc68cde0941ad14b29cc18721604704711 Mon Sep 17 00:00:00 2001 From: whut Date: Sun, 29 Jan 2023 23:17:31 -0600 Subject: [PATCH] add support for sol-feace/"wolfteam" format, generalize things a bit --- README.md | 20 +++++++++----- hootvopm.py | 89 ++++++++++++++++++++++++++++++++++++++++++------------------- 2 files changed, 74 insertions(+), 35 deletions(-) mode change 100644 => 100755 hootvopm.py diff --git a/README.md b/README.md index bbb3580..bf920f8 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # hootvopm -This script converts a [Hoot](http://snesmusic.org/hoot/v2/) memory dump into an .opm instrument pack, suitable for the VOPM VST plugin. It's an extremely rudimentary way of extracting FM instruments from games, but it works. +This script was initially made to convert a [Hoot](http://snesmusic.org/hoot/v2/) memory dump into an .opm instrument pack, suitable for the VOPM VST plugin. It's an extremely rudimentary way of extracting FM instruments from games, but it works. + +Over time, though, I've found myself slowly turning this into a general Yamaha 4op rip-o-tron. ## Usage -`./hootvopm.py [-b] -t chip_type input_file` +`./hootvopm.py [-b] -t format input_file` Input file can also be `-` to read from stdin. @@ -12,12 +14,15 @@ Output is dumped straight to stdout; redirect it to a file if you want to save i Pass -b to instead read straight binary data instead of comma-separated values. -### Currently-supported chip types +### Currently-supported chip types and formats -* OPM -* OPN -* OPNA (equivalent to OPN) -* Raw data in 32-byte groups (see below) +| ID | Description | +|------------|----------------------------------------------------------------| +| `opm` | OPM register area | +| `opn` | OPN register area | +| `opna` | OPNA register area (similar to OPN but has 3 more instruments) | +| `raw` | Raw data in 32-byte groups; see table below | +| `solfeace` | Format used in Sol-Feace for X68000 ("SOL.VCE") | #### Raw data format | Offset | Value | @@ -42,6 +47,7 @@ Rows marked with * are groups of 4 bytes, one per operator. ### Tips * Most chips (read: all of the ones currently supported by this script) have a 256-byte register area, meaning you'll need to trim it down to 16 lines. * From what I've seen, the chip's register area almost always resides on the first page, starting either at 0x0000 or 0x0100. +* This should be obvious, but for the formats that don't involve capturing the register area, you should pass `-b`. ## Known issues * Since the script looks at a simple static memory dump, it has no way of capturing certain data, instead replacing it with placeholders. These include: diff --git a/hootvopm.py b/hootvopm.py old mode 100644 new mode 100755 index 7e2e36b..374731f --- a/hootvopm.py +++ b/hootvopm.py @@ -31,8 +31,8 @@ DataFile = open(Filename,"r" + ("b" if BinMode else "")) if Filename != "-" else # MiOPMdrv sound bank Paramer Ver2002.04.22 # LFO: LFRQ AMD PMD WF NFRQ # @:[Num] [Name] -# CH: PAN FL CON AMS PMS SLOT NE -# [OPname]: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AMS-EN +# CH: PAN FL CON AMS PMS SLOT NE +# [OPname]: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AMS-EN Data = [] if not BinMode else DataFile.read() OpNames = ["M1","C1","M2","C2"] OpIdx = [0,2,1,3] @@ -69,6 +69,45 @@ def GetDT2D2R(b): def GetD1LRR(b): return (b & 0b11110000) >> 4, (b & 0b00001111) +class RecordGrabber: + def __init__(self, fields, recordSize, **options): + self.fields = fields + self.recordSize = recordSize + self.instStep = 1 + for k, v in options: + setattr(self, k, v) + + def __call__(self,data): + f = self.fields + slot = 120 + pan = 64 + ne = 0 + + for n in range(0, len(data) // self.recordSize): + ofs = n * self.recordSize + print("@:%d %s" % (n, ("Ins%d" % n) if "name" not in f else + f["name"](data, ofs))) + print("LFO: 0 0 0 0 0") # the DRY violations will end! ...soon + + get = lambda s: data[ofs + f[s]] if s in f else 0 + + fl, con = GetFlCon(get("flcon")) + ams, pms = GetAMSPMS(get("amspms")) + + PrintChannel(pan, fl, con, ams, pms, slot, ne) + + for op in range(4): + getOp = lambda s: data[ofs + f[s] + OpIdx[op]] if s in f else 0 + dt1, mul = GetDT1Mul(getOp("dt1mul")) + tl = getOp("tl") + ks, ar = GetKSAR(getOp("ksar")) + amsen, d1r = GetAmsenD1R(getOp("amsend1r")) + dt2, d2r = GetDT2D2R(getOp("dt2d2r")) + d1l, rr = GetD1LRR(getOp("d1lrr")) + + PrintOp(op, ar, d1r, d2r, rr, d1l, tl, ks, mul, dt1, dt2, amsen) + + def PrintOPM(data): ne = (data[0xf] & 0b10000000) >> 7 slot = 120 @@ -137,36 +176,30 @@ def PrintOPNA(data): PrintOPNBasic(data) PrintOPNBasic(data, True) -def PrintRaw(data): - slot = 120 - pan = 64 - ne = 0 - for n in range(0, len(data) // 32): - ofs = n * 32 - print("@:%d Ins%d" % (n, n)) - print("LFO: 0 0 0 0 0") # no way to get this data... at least for now - - fl, con = GetFlCon(data[ofs+0x18]) - ams, pms = GetAMSPMS(data[ofs+0x19]) - - PrintChannel(pan, fl, con, ams, pms, slot, ne) - - for op in range(4): - get = lambda x: data[ofs + x + OpIdx[op]] - dt1, mul = GetDT1Mul(get(0x00)) - tl = get(0x04) - ks, ar = GetKSAR(get(0x08)) - amsen, d1r = GetAmsenD1R(get(0x0C)) - dt2, d2r = GetDT2D2R(get(0x10)) - d1l, rr = GetD1LRR(get(0x14)) - - PrintOp(op, ar, d1r, d2r, rr, d1l, tl, ks, mul, dt1, dt2, amsen) - TypeFuncs = { "opn": PrintOPN, "opna": PrintOPNA, "opm": PrintOPM, - "raw": PrintRaw + "raw": RecordGrabber({ + "flcon": 0x18, + "amspms": 0x19, + "dt1mul": 0x0, + "tl": 0x04, + "ksar": 0x08, + "amsend1r": 0x0c, + "dt2d2r": 0x10, + "d1lrr": 0x14 + }, 0x20), + "solfeace": RecordGrabber({ + "flcon": 0x00, + "dt1mul": 0x02, + "tl": 0x06, + "ksar": 0x0a, + "amsend1r": 0x0e, + "dt2d2r": 0x12, + "d1lrr": 0x16, + "name": lambda data, ofs: data[ofs + 0x1a : ofs + 0x20].decode("shift-jis") + }, 0x20) } Type = GetOption("-t")