Initial commit: soothe2 RE workspace + roadmap
- Инфраструктура: RTTI-дампы (rtti_dsp/full.json), декомпиляции DSP-классов (decomp_*.txt), Ghidra-скрипты (Dump*.java, ImportRtti*.java, Diag.java), depthcurve/curve_fits LUT. - Поведенческая модель sim_v5.py + sim.py (RMSE ~0.3dB), утилиты (measure, tt_sweep, patchparam, sweep, harness_*, verify_sim). - summary.md + roadmap.md (контракт из manual M1-M12, топология пайплайна из OCR, фазы A-C). - pipeline_ocr.txt: OCR диаграммы Appendix A (mid/side ручка, trim/mix/bypass порядок).
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64, re, sys, os
|
||||
|
||||
# Reads render_v5.rpp, changes plugin PARAM values, writes a new RPP targeting an output name.
|
||||
|
||||
TPL = "/home/m/soothe-bt/render_v5.rpp"
|
||||
|
||||
def load_state(rpp=TPL):
|
||||
txt = open(rpp).read().split('\n')
|
||||
vi = [i for i,l in enumerate(txt) if 'VST "VST3: soothe2' in l][0]
|
||||
blob = []
|
||||
end = vi
|
||||
for i in range(vi+1, vi+40):
|
||||
s = txt[i].strip()
|
||||
if s == '>' or not s:
|
||||
end = i
|
||||
break
|
||||
blob.append(s)
|
||||
out = b''
|
||||
for l in blob:
|
||||
out += base64.b64decode(l + '=' * ((-len(l)) % 4))
|
||||
return txt, vi, end, blob, out
|
||||
|
||||
def save_state(txt, vi, end, blob, out, binary_len, rpp_out):
|
||||
# re-encode: binary_len bytes header + rest is ascii xml
|
||||
header = out[:binary_len]
|
||||
xml = out[binary_len:]
|
||||
new = base64.b64encode(header + xml).decode()
|
||||
lines = [new[i:i+128] for i in range(0, len(new), 128)]
|
||||
newblk = [' ' + l for l in lines]
|
||||
txt2 = txt[:vi+1] + newblk + [txt[end]]
|
||||
open(rpp_out, 'w').write('\n'.join(txt2))
|
||||
|
||||
def set_params(rpp, out_rpp, out_wav, params):
|
||||
txt, vi, end, blob, out = load_state(rpp)
|
||||
# preserve everything: prefix before '<?xml', the xml, and the binary tail
|
||||
xml_idx = out.find(b'<?xml')
|
||||
if xml_idx < 0:
|
||||
raise ValueError("no XML in state")
|
||||
header = out[:xml_idx]
|
||||
xml_bytes = out[xml_idx:-1] # last byte is padding
|
||||
xml = xml_bytes.decode('utf8', 'replace')
|
||||
for pid, val in params.items():
|
||||
m = re.search(rf'<PARAM id="{re.escape(pid)}" value="([^"]*)"', xml)
|
||||
if not m:
|
||||
raise ValueError(f"param not found: {pid}")
|
||||
# IMPORTANT: plugin honors the value only if serialized in the same
|
||||
# float format the original state used. depth uses full precision;
|
||||
# selectivity/mix/mode/oversample store "X.0" short form.
|
||||
# Try to keep the original format's decimal count.
|
||||
orig = m.group(1)
|
||||
if '.' in orig and len(orig.split('.')[1]) > 4:
|
||||
# full-precision param (e.g. depth) -> needs 16 decimals
|
||||
full = f"{float(val):.16f}"
|
||||
elif '.' in orig:
|
||||
# short-form param (e.g. selectivity "10.0") -> keep decimal count
|
||||
full = f"{float(val):.{len(orig.split('.')[1])}f}"
|
||||
else:
|
||||
full = val
|
||||
xml = xml[:m.start()] + f'<PARAM id="{pid}" value="{full}"' + xml[m.end():]
|
||||
if out_rpp:
|
||||
newb = base64.b64encode(header + xml.encode('utf8') + out[-1:]).decode()
|
||||
lines = [newb[i:i+128] for i in range(0, len(newb), 128)]
|
||||
txt2 = txt[:vi+1] + [' ' + l for l in lines]
|
||||
if end < len(txt) - 1:
|
||||
txt2 = txt2 + txt[end:]
|
||||
txt2 = [l if not l.strip().startswith('RENDER_FILE')
|
||||
else f' RENDER_FILE "{out_wav}"' for l in txt2]
|
||||
open(out_rpp, 'w').write('\n'.join(txt2))
|
||||
return txt
|
||||
|
||||
if __name__ == '__main__':
|
||||
# usage: sweep.py <rpp_out> <out_wav> <id=val> [id=val ...]
|
||||
rpp_out = sys.argv[1]
|
||||
out_wav = sys.argv[2]
|
||||
params = dict(a.split('=', 1) for a in sys.argv[3:])
|
||||
set_params(TPL, rpp_out, out_wav, params)
|
||||
print("wrote", rpp_out, params)
|
||||
Reference in New Issue
Block a user