# Part of the Fifteen-Thieves Project
# Chris Xiong 2022
# License: Expat (MIT)
import time
import math
import sys
from glob import glob
import xv.amap
import xv.util
import os
inst_sets = [(96,"classical"),(97,"contemporary"),(98,"solo"),(99,"enhanced"),(80,"special_1"),(81,"special_2")]
rhyt_sets = [(104,"classical"),(105,"contemporary"),(106,"solo"),(107,"enhanced")]
mfx_map = [ 0, 1, 35, 36, -1, # 0
-1, 7, 8, 21, -1, # 5
-1, 26, 27, 28, 23, # 10
24, 25, 43, 46, 47, # 15
48, 53, 62, 61, 64, # 20
65, 66, 67, 68, 69, # 25
70, 71, 72, 73, 74, # 30
75, 76, 77, -1, -1, # 35
-1, 11, -1, -1, 15, # 40
49, 50, 51, 52, -1, # 45
57, 56, 10, 37, 38, # 50
40, 41, 42, 20, 3, # 55
29, 30, 17, 18, -1, # 60
-1, 9, -1, -1, -1, # 65
-1, 22, -1, -1, -1, # 70
-1, -1, -1, 39, -1, # 75
-1, -1, -1, -1, -1, # 80
-1, -1, -1, 2, -1, # 85
-1] # 90
mfx_offsets = [
0x0000, 0x48c3, 0x48f3, 0x491f, 0x494b, # 0
0x4967, 0x499f, 0x4a07, 0x4a1f, 0x4a57, # 5
0x4a97, 0x4aaf, 0x4ae7, 0x4b2b, 0x4b5f, # 10
0x4b83, 0x4ba3, 0x4c07, 0x4c2b, 0x4c4f, # 15
0x4caf, 0x4d17, 0x4d47, 0x4d8b, 0x4dbf, # 20
0x4df7, 0x4e3b, 0x4e67, 0x4e9b, 0x4ec7, # 25
0x4eff, 0x4f3b, 0x4f83, 0x4fc3, 0x500b, # 30
0x506b, 0x5087, 0x50a3, 0x50c7, 0x50eb, # 35
0x5133, 0x514f, 0x516f, 0x518f, 0x51cf, # 40
0x51ff, 0x523f, 0x528b, 0x52d7, 0x5333, # 45
0x539f, 0x540f, 0x544f, 0x549f, 0x54cb, # 50
0x54fb, 0x5537, 0x557b, 0x559f, 0x55c7, # 55
0x55db, 0x5613, 0x563f, 0x5693, 0x570f, # 60
0x5733, 0x5753, 0x577b, 0x57a7, 0x57cf, # 65
0x57f7, 0x5823, 0x584b, 0x5873, 0x589f, # 70
0x58c7, 0x58ff, 0x593b, 0x5977, 0x59c3 # 75
]
# off chorus delay gm2 end
chorus_offsets = [0x0000, 0x59c3, 0x59e7, 0x5a1f, 0x5a3b]
# off reverb s.room s.hall s.plat gm2 end
reverb_offsets = [0x0000, 0x5a3b, 0x5a4b, 0x5a73, 0x5a9b, 0x5ac3, 0x5ad7]
#srx-09 is NEX04, srx-07 is NEX02
headers = [
b"",
b"",
b"KoaBankFile00003PG-NEX09", # Piano 1 / Concert Piano
b"KoaBankFile00003PG-NEX05", # Studio / Studio SRX
b"KoaBankFile00003PG-NEX07", # Strings / Symphonique Strings
b"KoaBankFile00003PG-NEX03", # Dance Trax / Supreme Dance + Platinum Trax
b"KoaBankFile00003PG-NEX01", # Orchestra / Complete Orchestra
b"KoaBankFile00003PG-NEX02", # Keyboards / Ultimate Keys
b"KoaBankFile00003PG-NEX03", # Dance Trax / Supreme Dance + Platinum Trax
b"KoaBankFile00003PG-NEX04", # World / World Collection
b"KoaBankFile00003PG-NEX08", # Brass / Big Brass Ensemble
b"KoaBankFile00003PG-NEX10", # Piano 2 / Complete Piano
b"KoaBankFile00003PG-NEX06", # Electric Piano / Classic EPs
]
header = b"KoaBankFile00003PG-NEX02"
template = b""
amap = xv.amap.AddrMapTemplate("amaps/sd-80.amap")
tree = xv.amap.create_addr_map_tree(amap)
tree.data[:] = [0xff for i in range(0, len(tree.data))]
def load_memregion(path):
mr = xv.util.load_memoryregion(path)
for addr, data in mr:
ra = xv.util.b7belist2int(addr)
tree.data[ra:ra + len(data)] = data
def clip_or_pad(b, l):
if len(b) < l:
return b + b'\00' * (l - len(b))
else: return b[0:l]
def convert_patch(iset, bk, pc):
ret = bytearray(template)
name = tree.value("sd_root.part[0].patch.pc_common.name")
ret[0:12] = name
common = tree.value("sd_root.part[0].patch.pc_common")
ret[0x54 : 0x54 + len(common)] = common
patch_common = tree.find_node("sd_root.part[0].patch.pc_common")
do_mod_lfo = patch_common.value("vibrato_pitch") != 64 or \
patch_common.value("vibrato_cutoff") != 64 or \
patch_common.value("vibrato_amp") != 64
tmt = tree.value("sd_root.part[0].patch.tmt")
ret[0x1dc : 0x1dc + len(tmt)] = tmt
tone_offsets = [0x205, 0x29f, 0x339, 0x3d3]
for t in range(0, 4):
tree.set_value(f"sd_root.part[0].patch.tone[{t}].tn_wave.wave_group_type", 1)
if do_mod_lfo:
lfo = []
lfo.append(tree.find_node(f"sd_root.part[0].patch.tone[{t}].tn_lfo[0]"))
lfo.append(tree.find_node(f"sd_root.part[0].patch.tone[{t}].tn_lfo[1]"))
dst_lfo = -1
for i in range(0, 2):
if lfo[i].value("pitch_depth") == 64 and \
lfo[i].value("filter_depth") == 64 and \
lfo[i].value("amp_depth") == 64 and \
lfo[i].value("pan_depth") == 64:
dst_lfo = i
break
if dst_lfo == -1:
print(f"WARNING ({iset}, {bk}, {pc}): cannot relocate modulation LFO for tone {t}")
else:
lfo[dst_lfo].set_value("waveform", patch_common.value("vibrato_lfo_waveform"))
lfo[dst_lfo].set_value("rate", patch_common.value("vibrato_rate"))
lfo[dst_lfo].set_value("offset", 2)
lfo[dst_lfo].set_value("rate_detune", 0)
lfo[dst_lfo].set_value("delay", patch_common.value("vibrato_delay"))
lfo[dst_lfo].set_value("delay_keyfollow", 64)
lfo[dst_lfo].set_value("fade_mode", 0)
lfo[dst_lfo].set_value("fade_time", patch_common.value("vibrato_attack"))
lfo[dst_lfo].set_value("key_trigger", 0)
lfo[dst_lfo].set_value("pitch_depth", patch_common.value("vibrato_pitch"))
lfo[dst_lfo].set_value("filter_depth", patch_common.value("vibrato_cutoff"))
lfo[dst_lfo].set_value("amp_depth", patch_common.value("vibrato_amp"))
lfo[dst_lfo].set_value("pan_depth", 64)
tone = tree.value(f"sd_root.part[0].patch.tone[{t}]")
ret[tone_offsets[t] : tone_offsets[t] + len(tone)] = tone
mfx = tree.value("sd_root.part[0].patch.mfx")
mfx_type = tree.value("sd_root.part[0].patch.mfx.type")
ret[0xa4 : 0xa4 + 13] = mfx[0:13]
mapped_mfx = mfx_map[tree.value("sd_root.part[0].patch.mfx.type")]
if mapped_mfx == -1:
print(f"WARNING ({iset}, {bk}, {pc}): no valid MFX mapped (XV MFX #{mfx_type})")
else:
ret[0xa4] = mapped_mfx
if mapped_mfx != 0:
ret[mfx_offsets[mapped_mfx] : mfx_offsets[mapped_mfx + 1]] = mfx[13 : 13 + mfx_offsets[mapped_mfx + 1] - mfx_offsets[mapped_mfx]]
chorus = tree.value("sd_root.part[0].patch.chorus")
chorus_type = tree.value("sd_root.part[0].patch.chorus.type")
ret[0x135 : 0x135 + 4] = chorus[0:4]
if chorus_type != 0:
ret[chorus_offsets[chorus_type] : chorus_offsets[chorus_type + 1]] = clip_or_pad(chorus[4:], chorus_offsets[chorus_type + 1] - chorus_offsets[chorus_type])
reverb = tree.value("sd_root.part[0].patch.reverb")
reverb_type = tree.value("sd_root.part[0].patch.reverb.type")
ret[0x189 : 0x189 + 3] = reverb[0:3]
if reverb_type != 0:
ret[reverb_offsets[reverb_type] : reverb_offsets[reverb_type + 1]] = reverb[3 : 3 + reverb_offsets[reverb_type + 1] - reverb_offsets[reverb_type]]
if len(ret) != 0x5ad7:
print(f"ERROR: ({iset}, {bk}, {pc}) messed up: {len(ret):x}!!! {chorus_type}")
return ret
def convert_rhythm(rset, pc):
ret = bytearray(template)
name = tree.value("sd_root.part[0].rhythm.ry_common.name")
ret[0:12] = name
ret[0x0c] = 1
common = tree.value("sd_root.part[0].rhythm.ry_common")
ret[0x46d : 0x46d + len(common)] = common
for key in range(0, 88):
for tone in range(0, 4):
tree.set_value(f"sd_root.part[0].rhythm.rtone[{key}].rt_wmt[{tone}].wave_group_type", 1)
rtone = tree.value(f"sd_root.part[0].rhythm.rtone[{key}]")
ret[0x5b7 + 0xc3 * key : 0x5b7 + 0xc3 * key + len(rtone)] = rtone
mfx = tree.value("sd_root.part[0].rhythm.mfx")
mfx_type = tree.value("sd_root.part[0].rhythm.mfx.type")
ret[0x47f : 0x47f + 13] = mfx[0:13]
mapped_mfx = mfx_map[tree.value("sd_root.part[0].rhythm.mfx.type")]
if mapped_mfx == -1:
print(f"WARNING ({rset}, {pc}): no valid MFX mapped (XV MFX #{mfx_type})")
else:
ret[0xa4] = mapped_mfx
if mapped_mfx != 0:
ret[mfx_offsets[mapped_mfx] : mfx_offsets[mapped_mfx + 1]] = mfx[13 : 13 + mfx_offsets[mapped_mfx + 1] - mfx_offsets[mapped_mfx]]
chorus = tree.value("sd_root.part[0].rhythm.chorus")
chorus_type = tree.value("sd_root.part[0].rhythm.chorus.type")
ret[0x510 : 0x510 + 4] = chorus[0:4]
if chorus_type != 0:
ret[chorus_offsets[chorus_type] : chorus_offsets[chorus_type + 1]] = clip_or_pad(chorus[4:], chorus_offsets[chorus_type + 1] - chorus_offsets[chorus_type])
reverb = tree.value("sd_root.part[0].rhythm.reverb")
reverb_type = tree.value("sd_root.part[0].rhythm.reverb.type")
ret[0x564 : 0x564 + 3] = reverb[0:3]
if reverb_type != 0:
ret[reverb_offsets[reverb_type] : reverb_offsets[reverb_type + 1]] = reverb[3 : 3 + reverb_offsets[reverb_type + 1] - reverb_offsets[reverb_type]]
if len(ret) != 0x5ad7:
print(f"ERROR: ({iset}, {bk}, {pc}) messed up: {len(ret):x}!!!")
return ret
def write_bank(fn, b):
if len(b) != 0x2d6b80:
print(f"ERROR: Invalid bank file: {len(b):x}")
return
with open(fn, "wb") as f:
f.write(header)
f.write(b)
if __name__ == "__main__":
try:
os.mkdir("data/srxsdpresets")
except Exception: pass
try:
os.mkdir("data/srxsdpresets/by_order")
except Exception: pass
try:
os.mkdir("data/srxsdpresets/by_set")
except Exception: pass
if len(sys.argv) > 1:
if int(sys.argv[1]) >= 2 and int(sys.argv[1]) <= 12:
header = headers[int(sys.argv[1])]
gm2pcb = []
gm2rhy = []
with open("data/gm2pcbanks", "r") as f:
gm2pcb = [int(x.strip(), 16) for x in f.readline().split(',')]
gm2rhy = [int(x.strip()) for x in f.readline().split(',')]
with open("data/srx_vsti_preset_template", "rb") as f:
template = f.read()
cur_prs = [bytearray() for i in range(0, 10)]
for iset in inst_sets:
patchdata = [bytearray(), bytearray()]
c = 0
for pc in range(0, 128):
for bk in range(0, 1 if iset[0] < 96 else gm2pcb[pc]):
fn = glob(f"data/patchparam/{iset[1]}/{bk:02d}-{pc:03d}-*.memoryregion")[0]
load_memregion(fn)
p = convert_patch(iset[0], bk, pc)
patchdata[int(bk > 0)] += p
if iset[0] < 96:
cur_prs[iset[0] - 80 + 8] += p
else:
cur_prs[c // 32] += p
c += 1
for bank in range(0, 1 + int(iset[0] >= 96)):
typ = "variation" if bank else "capital"
fn = f"data/srxsdpresets/by_set/{iset[1]}_{typ}.bin"
if iset[0] < 96:
fn = f"data/srxsdpresets/by_set/{iset[1]}.bin"
write_bank(fn, patchdata[bank])
for i, b in enumerate(cur_prs):
write_bank(f"data/srxsdpresets/by_order/PR-{chr(ord('A') + i)}.bin", b)
rhy_prs = bytearray()
for rset in rhyt_sets:
rhydata = bytearray()
for pc in range(0, 128):
if pc in gm2rhy:
fn = glob(f"data/rhythmparam/{rset[1]}/{pc:02d}-*.memoryregion")[0]
load_memregion(fn)
p = convert_rhythm(rset[0], pc)
rhydata += p
rhy_prs += p
else: rhydata += template
rhy_prs += 23 * template
fn = f"data/srxsdpresets/by_set/{rset[1]}_rhythm.bin"
write_bank(fn, rhydata)
write_bank(f"data/srxsdpresets/by_order/PR-K.bin", rhy_prs)