Export Hoot memory dumps to VOPM instruments
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

235 lines
6.8KB

  1. #!/usr/bin/python3
  2. import argparse, collections, sys
  3. # MiOPMdrv sound bank Paramer Ver2002.04.22
  4. # LFO: LFRQ AMD PMD WF NFRQ
  5. # @:[Num] [Name]
  6. # CH: PAN FL CON AMS PMS SLOT NE
  7. # [OPname]: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AMS-EN
  8. OpNames = ["M1","C1","M2","C2"]
  9. OpIdx = [0,2,1,3]
  10. def Error(*args):
  11. print(sys.argv[0]+":", *args, file=sys.stderr)
  12. sys.exit(1)
  13. def PrintChannel(pan, fl, con, ams, pms, slot, ne):
  14. print("CH:", pan, fl, con, ams, pms, slot, ne)
  15. def PrintOp(op, ar, d1r, d2r, rr, d1l, tl, ks, mul, dt1, dt2, amsen):
  16. print(OpNames[op] + ":", ar, d1r, d2r, rr, d1l, tl, ks, mul, dt1, dt2, amsen)
  17. class Operator:
  18. def __init__(self, num):
  19. self.num = num
  20. for n in "ar d1r d2r rr d1l tl ks mul dt1 dt2 amsen".split():
  21. setattr(self, n, 0)
  22. def setDT1Mul(self, b):
  23. self.dt1, self.mul = (b & 0b01110000) >> 4, (b & 0b00001111)
  24. def setKSAR(self, b):
  25. self.ks, self.ar = (b & 0b11000000) >> 6, (b & 0b00011111)
  26. def setAmsenD1R(self, b):
  27. self.amsen, self.d1r = (b & 0b10000000) >> 7, (b & 0b00011111)
  28. def setDT2D2R(self, b):
  29. self.dt2, self.d2r = (b & 0b11000000) >> 6, (b & 0b00011111)
  30. def setD1LRR(self, b):
  31. self.d1l, self.rr = (b & 0b11110000) >> 4, (b & 0b00001111)
  32. class Instrument:
  33. def __init__(self, num, name):
  34. self.num = num
  35. self.name = name
  36. self.slot = 120
  37. self.pan = 64
  38. self.ne = 0
  39. for n in "fl con ams pms".split():
  40. setattr(self, n, 0)
  41. self.ops = [Operator(i) for i in range(4)]
  42. def render(self):
  43. print("@:%d %s" % (self.num, self.name))
  44. print("LFO: 0 0 0 0 0")
  45. PrintChannel(self.pan, self.fl, self.con, self.ams, self.pms, self.slot, self.ne)
  46. for o in self.ops:
  47. 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)
  48. def setFlCon(self, b):
  49. self.fl, self.con = (b & 0b00111000) >> 3, (b & 0b00000111)
  50. def setAMSPMS(self, b):
  51. self.ams, self.pms = (b & 0b00110000) >> 4, (b & 0b00000111)
  52. # """
  53. def __hash__(self):
  54. get = lambda n, d = 1: [getattr(self.ops[x], n) // d for x in range(4)]
  55. data = bytes([self.con] + get("tl", 4))
  56. #print(data, data.__hash__())
  57. return data.__hash__()
  58. def __eq__(self, other):
  59. return self.__hash__() == other.__hash__()
  60. # """
  61. class RecordGrabber:
  62. def __init__(self, fields, recordSize, **options):
  63. self.fields = fields
  64. self.recordSize = recordSize
  65. self.instStep = 1
  66. for k, v in options:
  67. setattr(self, k, v)
  68. def __call__(self,data):
  69. result = []
  70. f = self.fields
  71. for n in range(0, len(data) // self.recordSize):
  72. inst = Instrument(n, ("Ins%d" % n) if "name" not in f else
  73. f["name"](data, ofs))
  74. ofs = n * self.recordSize
  75. get = lambda s: data[ofs + f[s]] if s in f else 0
  76. inst.setFlCon(get("flcon"))
  77. inst.setAMSPMS(get("amspms"))
  78. for o in range(4):
  79. getOp = lambda s: data[ofs + f[s] + OpIdx[o]] if s in f else 0
  80. op = inst.ops[o]
  81. op.setDT1Mul(getOp("dt1mul"))
  82. op.tl = getOp("tl")
  83. op.setKSAR(getOp("ksar"))
  84. op.setAmsenD1R(getOp("amsend1r"))
  85. op.setDT2D2R(getOp("dt2d2r"))
  86. op.setD1LRR(getOp("d1lrr"))
  87. #inst.render()
  88. result.append(inst)
  89. #print(inst.__hash__())
  90. return result
  91. def PrintOPM(data):
  92. result = []
  93. for i in range(8):
  94. inst = Instrument(i, ("Ins%d" % i))
  95. inst.ne = (data[0xf] & 0b10000000) >> 7
  96. meta = data[0x20 + i : 0x3f + i : 8]
  97. inst.setFlCon(meta[0])
  98. inst.setAMSPMS(meta[3])
  99. for o in range(4):
  100. op = inst.ops[o]
  101. get = lambda x: data[x + i + OpIdx[o]*8]
  102. op.setDT1Mul(get(0x40))
  103. op.tl = get(0x60)
  104. op.setKSAR(get(0x80))
  105. op.setAmsenD1R(get(0xa0))
  106. op.setDT2D2R(get(0xc0))
  107. op.setD1LRR(get(0xe0))
  108. return result
  109. def PrintOPNBasic(data, opna = False):
  110. result = []
  111. # SR in opn parlance is equivalent to D2R in opm, thanks yamaha
  112. # likewise, SL -> D1L and DR -> D1R
  113. ofs = 0x100 if opna else 0
  114. for i in range(3):
  115. n = i + (3 if opna else 0)
  116. inst = Instrument(n, ("Ins%d" % n))
  117. inst.setFlCon(data[ofs + 0xb0 + i])
  118. inst.setAMSPMS(data[ofs + 0xb4 + i])
  119. for o in range(4):
  120. op = inst.ops[o]
  121. get = lambda x: data[ofs + x + i + OpIdx[o]*4]
  122. op.getDT1Mul(get(0x30))
  123. op.tl = get(0x40)
  124. op.getKSAR(get(0x50))
  125. op.getAmsenD1R(get(0x60))
  126. op.getDT2D2R(get(0x70))
  127. op.getD1LRR(get(0x80))
  128. result.append(inst)
  129. return result
  130. def PrintOPN(data):
  131. PrintOPNBasic(data)
  132. def PrintOPNA(data):
  133. PrintOPNBasic(data)
  134. PrintOPNBasic(data, True)
  135. TypeFuncs = {
  136. "opn": PrintOPN,
  137. "opna": PrintOPNA,
  138. "opm": PrintOPM,
  139. "raw": RecordGrabber({
  140. "flcon": 0x18,
  141. "amspms": 0x19,
  142. "dt1mul": 0x0,
  143. "tl": 0x04,
  144. "ksar": 0x08,
  145. "amsend1r": 0x0c,
  146. "dt2d2r": 0x10,
  147. "d1lrr": 0x14
  148. }, 0x20),
  149. "solfeace": RecordGrabber({
  150. "flcon": 0x00,
  151. "dt1mul": 0x02,
  152. "tl": 0x06,
  153. "ksar": 0x0a,
  154. "amsend1r": 0x0e,
  155. "dt2d2r": 0x12,
  156. "d1lrr": 0x16,
  157. "name": lambda data, ofs: data[ofs + 0x1a : ofs + 0x20].decode("shift-jis")
  158. }, 0x20)
  159. }
  160. Parser = argparse.ArgumentParser(
  161. prog="hootvopm",
  162. description="Convert Yamaha OPM/OPN data to VOPM instruments")
  163. Parser.add_argument("file", help="file to read (- for stdin)")
  164. Parser.add_argument("-b", "--binary", dest="binary", action="store_true",
  165. help="read binary output instead of comma-separated hex")
  166. Parser.add_argument("-t", "--type", dest="type", action="store",
  167. default="raw", choices=TypeFuncs.keys(),
  168. help="chip/format type (default: raw)")
  169. Args = Parser.parse_args()
  170. Filename = Args.file
  171. DataFile = open(Filename,"r" + ("b" if Args.binary else "")) if Filename != "-" else sys.stdin
  172. Data = [] if not Args.binary else DataFile.read()
  173. if not Args.binary:
  174. for line in DataFile.readlines():
  175. for byte in line.strip().split(",")[:16]:
  176. Data.append(int(byte,16))
  177. print("// Exported by hootvopm : https://git.lain.church/whut/hootvopm")
  178. print("// Chip type:", Args.type)
  179. Instruments = set(TypeFuncs[Args.type.lower()](Data))
  180. #print(len(Instruments))
  181. Instruments = sorted(list(Instruments), key = lambda x: x.num)
  182. count = 0
  183. for i in Instruments:
  184. i.num = count
  185. i.render()
  186. count += 1