diff --git a/README.md b/README.md index bf920f8..b8138e4 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ Pass -b to instead read straight binary data instead of comma-separated values. ### Currently-supported chip types and formats -| 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") | +| ID | Description | +|-----------------|----------------------------------------------------------------| +| `raw` (Default) | Raw data in 32-byte groups; see table below | +| `opm` | OPM register area | +| `opn` | OPN register area | +| `opna` | OPNA register area (similar to OPN but has 3 more instruments) | +| `solfeace` | Format used in Sol-Feace for X68000 ("SOL.VCE") | #### Raw data format | Offset | Value | @@ -38,6 +38,8 @@ Pass -b to instead read straight binary data instead of comma-separated values. Rows marked with * are groups of 4 bytes, one per operator. +If you're curious, this is the format found in the PC-88VA version of Sorcerian, plus some padding bytes. + ## 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. diff --git a/hootvopm.py b/hootvopm.py index 374731f..fe015d6 100755 --- a/hootvopm.py +++ b/hootvopm.py @@ -1,46 +1,18 @@ #!/usr/bin/python3 -import sys, getopt +import argparse, collections, sys -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:], "bot:") - -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("no input file") - -BinMode = OptionExist("-b") -DataFile = open(Filename,"r" + ("b" if BinMode else "")) if Filename != "-" else sys.stdin -# MiOPMdrv sound bank Paramer Ver2002.04.22 +# 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 = [] if not BinMode else DataFile.read() OpNames = ["M1","C1","M2","C2"] OpIdx = [0,2,1,3] -if not BinMode: - for line in DataFile.readlines(): - for byte in line.strip().split(",")[:16]: - Data.append(int(byte,16)) +def Error(*args): + print(sys.argv[0]+":", *args, file=sys.stderr) + sys.exit(1) + def PrintChannel(pan, fl, con, ams, pms, slot, ne): print("CH:", pan, fl, con, ams, pms, slot, ne) @@ -48,27 +20,61 @@ def PrintChannel(pan, fl, con, ams, pms, slot, ne): def PrintOp(op, ar, d1r, d2r, rr, d1l, tl, ks, mul, dt1, dt2, amsen): print(OpNames[op] + ":", ar, d1r, d2r, rr, d1l, tl, ks, mul, dt1, dt2, amsen) -def GetFlCon(b): - return (b & 0b00111000) >> 3, (b & 0b00000111) +class Operator: + def __init__(self, num): + self.num = num + for n in "ar d1r d2r rr d1l tl ks mul dt1 dt2 amsen".split(): + setattr(self, n, 0) + + def setDT1Mul(self, b): + self.dt1, self.mul = (b & 0b01110000) >> 4, (b & 0b00001111) -def GetAMSPMS(b): - return (b & 0b00110000) >> 4, (b & 0b00000111) + def setKSAR(self, b): + self.ks, self.ar = (b & 0b11000000) >> 6, (b & 0b00011111) -def GetDT1Mul(b): - return (b & 0b01110000) >> 4, (b & 0b00001111) + def setAmsenD1R(self, b): + self.amsen, self.d1r = (b & 0b10000000) >> 7, (b & 0b00011111) -def GetKSAR(b): - return (b & 0b11000000) >> 6, (b & 0b00011111) + def setDT2D2R(self, b): + self.dt2, self.d2r = (b & 0b11000000) >> 6, (b & 0b00011111) -def GetAmsenD1R(b): - return (b & 0b10000000) >> 7, (b & 0b00011111) + def setD1LRR(self, b): + self.d1l, self.rr = (b & 0b11110000) >> 4, (b & 0b00001111) -def GetDT2D2R(b): - return (b & 0b11000000) >> 6, (b & 0b00011111) +class Instrument: + def __init__(self, num, name): + self.num = num + self.name = name + self.slot = 120 + self.pan = 64 + self.ne = 0 + for n in "fl con ams pms".split(): + setattr(self, n, 0) -def GetD1LRR(b): - return (b & 0b11110000) >> 4, (b & 0b00001111) + self.ops = [Operator(i) for i in range(4)] + def render(self): + print("@:%d %s" % (self.num, self.name)) + print("LFO: 0 0 0 0 0") + PrintChannel(self.pan, self.fl, self.con, self.ams, self.pms, self.slot, self.ne) + for o in self.ops: + PrintOp(o.num, o.ar, o.d1r, o.d2r, o.rr, o.d1l, o.tl, o.ks, o.mul, o.dt1, o.dt2, o.amsen) + + def setFlCon(self, b): + self.fl, self.con = (b & 0b00111000) >> 3, (b & 0b00000111) + + def setAMSPMS(self, b): + self.ams, self.pms = (b & 0b00110000) >> 4, (b & 0b00000111) +# """ + def __hash__(self): + get = lambda n, d = 1: [getattr(self.ops[x], n) // d for x in range(4)] + data = bytes([self.con] + get("tl", 4)) + #print(data, data.__hash__()) + return data.__hash__() + + def __eq__(self, other): + return self.__hash__() == other.__hash__() +# """ class RecordGrabber: def __init__(self, fields, recordSize, **options): self.fields = fields @@ -78,96 +84,84 @@ class RecordGrabber: setattr(self, k, v) def __call__(self,data): + result = [] f = self.fields - slot = 120 - pan = 64 - ne = 0 - for n in range(0, len(data) // self.recordSize): + inst = Instrument(n, ("Ins%d" % n) if "name" not in f else + f["name"](data, ofs)) + 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")) + inst.setFlCon(get("flcon")) + inst.setAMSPMS(get("amspms")) - PrintChannel(pan, fl, con, ams, pms, slot, ne) + for o in range(4): + getOp = lambda s: data[ofs + f[s] + OpIdx[o]] if s in f else 0 + op = inst.ops[o] - 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")) + op.setDT1Mul(getOp("dt1mul")) + op.tl = getOp("tl") + op.setKSAR(getOp("ksar")) + op.setAmsenD1R(getOp("amsend1r")) + op.setDT2D2R(getOp("dt2d2r")) + op.setD1LRR(getOp("d1lrr")) - PrintOp(op, ar, d1r, d2r, rr, d1l, tl, ks, mul, dt1, dt2, amsen) + #inst.render() + result.append(inst) + #print(inst.__hash__()) + return result def PrintOPM(data): - ne = (data[0xf] & 0b10000000) >> 7 - slot = 120 - pan = 64 + result = [] 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 + inst = Instrument(i, ("Ins%d" % i)) + inst.ne = (data[0xf] & 0b10000000) >> 7 meta = data[0x20 + i : 0x3f + i : 8] - fl, con = GetFlCon(meta[0]) + inst.setFlCon(meta[0]) + inst.setAMSPMS(meta[3]) - pms, ams = GetAMSPMS(meta[3]) - - PrintChannel(pan, fl, con, ams, pms, slot, ne) - - for op in range(4): - get = lambda x: data[x + i + OpIdx[op]*8] + for o in range(4): + op = inst.ops[o] + get = lambda x: data[x + i + OpIdx[o]*8] - dt1, mul = GetDT1Mul(get(0x40)) - tl = get(0x60) - ks, ar = GetKSAR(get(0x80)) - amsen, d1r = GetAmsenD1R(get(0xa0)) - dt2, d2r = GetDT2D2R(get(0xc0)) - d1l, rr = GetD1LRR(get(0xe0)) - - PrintOp(op, ar, d1r, d2r, rr, d1l, tl, ks, mul, dt1, dt2, amsen) - - print() + op.setDT1Mul(get(0x40)) + op.tl = get(0x60) + op.setKSAR(get(0x80)) + op.setAmsenD1R(get(0xa0)) + op.setDT2D2R(get(0xc0)) + op.setD1LRR(get(0xe0)) + return result def PrintOPNBasic(data, opna = False): - slot = 120 - pan = 64 - ne = 0 # either i'm blind or opn has no noise enable bit (not that i care either way) + result = [] # SR in opn parlance is equivalent to D2R in opm, thanks yamaha # likewise, SL -> D1L and DR -> D1R ofs = 0x100 if opna else 0 for i in range(3): n = i + (3 if opna else 0) - 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 + 0xb0 + i]) - - ams, pms = GetAMSPMS(data[ofs + 0xb4 + i]) - - PrintChannel(pan, fl, con, ams, pms, slot, ne) - - for op in range(4): - get = lambda x: data[ofs + x + i + OpIdx[op]*4] - - dt1, mul = GetDT1Mul(get(0x30)) - tl = get(0x40) - ks, ar = GetKSAR(get(0x50)) - amsen, d1r = GetAmsenD1R(get(0x60)) - dt2, d2r = GetDT2D2R(get(0x70)) - d1l, rr = GetD1LRR(get(0x80)) + inst = Instrument(n, ("Ins%d" % n)) + + inst.setFlCon(data[ofs + 0xb0 + i]) + inst.setAMSPMS(data[ofs + 0xb4 + i]) + + for o in range(4): + op = inst.ops[o] + get = lambda x: data[ofs + x + i + OpIdx[o]*4] - PrintOp(op, ar, d1r, d2r, rr, d1l, tl, ks, mul, dt1, dt2, amsen) + op.getDT1Mul(get(0x30)) + op.tl = get(0x40) + op.getKSAR(get(0x50)) + op.getAmsenD1R(get(0x60)) + op.getDT2D2R(get(0x70)) + op.getD1LRR(get(0x80)) + + result.append(inst) + return result def PrintOPN(data): PrintOPNBasic(data) @@ -202,12 +196,39 @@ TypeFuncs = { }, 0x20) } -Type = GetOption("-t") -if Type==None: - Error("no chip type specified") -if Type not in TypeFuncs: - Error(Type+":","unknown chip type") +Parser = argparse.ArgumentParser( + prog="hootvopm", + description="Convert Yamaha OPM/OPN data to VOPM instruments") + +Parser.add_argument("file", help="file to read (- for stdin)") +Parser.add_argument("-b", "--binary", dest="binary", action="store_true", + help="read binary output instead of comma-separated hex") +Parser.add_argument("-t", "--type", dest="type", action="store", + default="raw", choices=TypeFuncs.keys(), + help="chip/format type (default: raw)") + +Args = Parser.parse_args() + +Filename = Args.file + +DataFile = open(Filename,"r" + ("b" if Args.binary else "")) if Filename != "-" else sys.stdin +Data = [] if not Args.binary else DataFile.read() + +if not Args.binary: + for line in DataFile.readlines(): + for byte in line.strip().split(",")[:16]: + Data.append(int(byte,16)) + print("// Exported by hootvopm : https://git.lain.church/whut/hootvopm") -print("// Chip type:",Type) -TypeFuncs[Type.lower()](Data) +print("// Chip type:", Args.type) + +Instruments = set(TypeFuncs[Args.type.lower()](Data)) +#print(len(Instruments)) +Instruments = sorted(list(Instruments), key = lambda x: x.num) + +count = 0 +for i in Instruments: + i.num = count + i.render() + count += 1