From a0016998447717c0aa9f5adfda62280cbaf936ec Mon Sep 17 00:00:00 2001 From: whut Date: Tue, 14 May 2019 23:33:17 -0500 Subject: [PATCH] initial commit --- README.md | 31 ++++++++++++ hootvopm.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 README.md create mode 100644 hootvopm.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac03a80 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# hootopna + +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. + +## Usage + +`./hootopna.py -t chip_type input_file` + +Output is dumped straight to stdout; redirect it to a file if you want to save it. + +### Currently-supported chip types + +* OPM +* OPN +* OPNA (equivalent to OPN) + +## Capturing memory in hoot + +* Left- and right-clicking the "driver work" area flips between available memory pages. Scrolling the wheel in this area scrolls through the current page; you can also use Ctrl-(Up/Down/PgUp/PgDn) for this purpose. +* When you see the area you need to capture, use Ctrl-C to copy it to the clipboard. This copies the entire page to the clipboard in a human-readable format, with 16 comma-separated hex bytes per line (plus a comma at the end for whatever reason, but this is ignored). +* Paste this into a file, trimming it down to get the register area of the FM sound chip. Save it as whatever. + +### 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. + +## 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: + * LFO data + * Operator on/off state; all operators are considered to be in use (this *could* potentially be inferred, though) + * Panning, though VOPM doesn't seem to use this diff --git a/hootvopm.py b/hootvopm.py new file mode 100644 index 0000000..96ec810 --- /dev/null +++ b/hootvopm.py @@ -0,0 +1,153 @@ +#!/usr/bin/python3 +import sys, getopt + +def Error(*args): + print(sys.argv[0]+":", *args, file=sys.stderr) + sys.exit(1) + +if len(sys.argv) > 1: + Filename = sys.argv[1] + +Options, Args = getopt.getopt(sys.argv[1:], "ot:") + +def OptionExist(opt): + global Options + f = list(filter(lambda x: x[0] == opt, Options)) + return len(f) >= 1 + +def GetOption(opt): + global Options + f = list(filter(lambda x: x[0] == opt, Options)) + if len(f) == 0: return None + else: return f[0][1] + +if len(Args) >= 1: + Filename = Args[0] +else: + Error("need a file, genius") + + +DataFile = open(Filename,"r") +# 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 +Data = [] +OpNames = ["M1","C1","M2","C2"] +OpIdx = [0,2,1,3] + +for line in DataFile.readlines(): + for byte in line.strip().split(",")[:16]: + Data.append(int(byte,16)) + +def PrintOPM(data): + ne = (data[0xf] & 0b10000000) >> 7 + slot = 120 + pan = 64 + for i in range(8): + print("@:%d Ins%d" % (i, i)) + + print("LFO: 0 0 0 0 0") # no way to get this data... at least for now + + meta = data[0x20 + i : 0x3f + i : 8] + + fl = (meta[0] & 0b00111000) >> 3 + con = (meta[0] & 0b00000111) + + pms = (meta[3] & 0b01110000) >> 4 + ams = (meta[3] & 0b00000011) + + print("CH:", pan, fl, con, ams, pms, slot, ne) + + for op in range(4): + get = lambda x: data[x + i + OpIdx[op]*8] + + b = get(0x40) + dt1 = (b & 0b01110000) >> 4 + mul = (b & 0b00001111) + + tl = get(0x60) + + b = get(0x80) + ks = (b & 0b11000000) >> 6 + ar = (b & 0b00011111) + + b = get(0xa0) + amsen = (b & 0b10000000) >> 7 + d1r = (b & 0b00011111) + + b = get(0xc0) + dt2 = (b & 0b11000000) >> 6 + d2r = (b & 0b00011111) + + b = get(0xe0) + d1l = (b & 0b11110000) >> 4 + rr = (b & 0b00001111) + + print(OpNames[op] + ":", ar, d1r, d2r, rr, d1l, tl, ks, mul, dt1, dt2, amsen) + + print() + +def PrintOPN(data): + slot = 120 + pan = 64 + ne = 0 # either i'm blind or opn has no noise enable bit (not that i care either way) + # SR in opn parlance is equivalent to D2R in opm, thanks yamaha + # likewise, SL -> D1L and DR -> D1R + for i in range(3): + print("@:%d Ins%d" % (i, i)) + print("LFO: 0 0 0 0 0") # no way to get this data... at least for now + + fbcon = data[0xb0 + i] + fl = (fbcon & 0b00111000) >> 3 + con = (fbcon & 0b00000111) + + sens = data[0xb4 + i] + ams = (sens & 0b00110000) >> 4 + pms = (sens & 0b00000111) + + print("CH:", pan, fl, con, ams, pms, slot, ne) + + for op in range(4): + get = lambda x: data[x + i + OpIdx[op]*4] + + b = get(0x30) + dt1 = (b & 0b01110000) >> 4 + mul = (b & 0b00001111) + + tl = get(0x40) + + b = get(0x50) + ks = (b & 0b11000000) >> 6 + ar = (b & 0b00011111) + + b = get(0x60) + amsen = (b & 0b10000000) >> 7 + d1r = (b & 0b00011111) + + b = get(0x70) + dt2 = (b & 0b11000000) >> 6 + d2r = (b & 0b00011111) + + b = get(0x80) + d1l = (b & 0b11110000) >> 4 + rr = (b & 0b00001111) + + print(OpNames[op] + ":", ar, d1r, d2r, rr, d1l, tl, ks, mul, dt1, dt2, amsen) + +TypeFuncs = { + "opn": PrintOPN, + "opna": PrintOPN, # fm portion of opn and opna are functionally equivalent + "opm": PrintOPM +} + +Type = GetOption("-t").lower() +if Type==None: + Error("no chip type specified") +if Type not in TypeFuncs: + Error(Type+":","unknown chip type") + +print("// Exported by hootvopm : https://gitlab.com/whutt/hootvopm") +print("// Chip type:",Type) +TypeFuncs[Type](Data)