Files
SonicForgeStudio/native_bridge/tests/test_offline_render.py
T
admin bbb11f8066 Phase 1: import .vstpreset in offline render (loadSerializedState)
daw_vst_bridge --render now applies a VST3 preset when job has 'preset'
(base64 of a .vstpreset file). Parse the SDK binary chunk format with
PresetFile (vst3sdk), rewrap component/controller state into the bridge
blob [4B BE compLen][comp][4B BE ctrlLen][ctrl] and apply via
loadSerializedState before rendering. SF2/SFZ ignore preset (warning).
Bad base64/magic/chunks fail the job with rc 1 and no partial clip.

Tests: golden Nexus .vstpreset fixture; VST3 with/without preset,
bad preset -> rc 1, non-VST3 preset ignored. 20/20 pass.
2026-08-17 10:44:13 +07:00

150 lines
7.4 KiB
Python

#!/usr/bin/env python3
# Phase 0 offline render test: `daw_vst_bridge --render <job.json> --out <clip.wav>`
# SF2 + SFZ through the same engines as realtime. Asserts:
# - exit 0, WAV exists, valid RIFF, exact duration (totalNeeded frames)
# - audio non-silent (RMS > 0.001)
# - error paths: missing --out -> rc 1; bad plugin_path -> rc 2
# Usage: python tests/test_offline_render.py
import json, math, os, struct, subprocess, sys, tempfile
BRIDGE = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "build", "Release", "daw_vst_bridge.exe"))
SF2 = r"C:\Users\locpham\SonicForgeStudio\app\storage\soundfonts\518e850f-a5d3-4790-b1f9-0c90c203c524.sf2"
SFZ = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "sfizz-src", "tests", "TestFiles", "g2_test.sfz"))
BPM, SR = 120.0, 44100
BEAT_SEC = 60.0 / BPM
TOTAL = int(max(1024, round((0.0 + 1.0) * BEAT_SEC * SR))) # 22050
def read_wav(path):
b = open(path, "rb").read()
assert b[:4] == b"RIFF" and b[8:12] == b"WAVE", "not a RIFF/WAVE file"
assert b[36:40] == b"data", "no data chunk"
data_size = struct.unpack_from("<I", b, 40)[0]
assert len(b) == 44 + data_size, "trailing bytes"
assert data_size % 4 == 0, "stereo 16-bit: data must be multiple of 4"
frames = data_size // 4
samples = struct.unpack("<%dh" % (frames * 2), b[44:])
rms = math.sqrt(sum(s * s for s in samples) / len(samples)) / 32767.0
return frames, rms
def make_job(itype, path, outdir):
return {
"instrument_type": itype, "plugin_path": path,
"sample_rate": SR, "bpm": BPM,
"notes": [{"pitch": 60, "velocity": 0.8, "start_beat": 0.0, "duration_beats": 1.0}],
}
def run_render(jobdict, out):
job = os.path.join(outdir_of(out), os.path.basename(out) + ".job.json")
with open(job, "w", encoding="utf-8") as f:
json.dump(jobdict, f)
if os.path.exists(out):
os.remove(out)
r = subprocess.run([BRIDGE, "--render", job, "--out", out],
capture_output=True, text=True, timeout=120)
return r.returncode, (r.stdout + r.stderr)
def outdir_of(p):
d = os.path.dirname(p)
return d if d else "."
def main():
results = []
def check(name, ok, detail):
results.append((name, ok, detail))
print(("PASS" if ok else "FAIL"), name, "-", detail)
with tempfile.TemporaryDirectory() as td:
# 1. SF2 render
job = os.path.join(td, "sf2.json"); out = os.path.join(td, "sf2.wav")
rc, log = run_render(make_job(2, SF2, td), out)
if rc == 0 and os.path.exists(out):
frames, rms = read_wav(out)
check("SF2 render exit 0", rc == 0, log.strip().splitlines()[-1:] or "")
check("SF2 exact duration", frames == TOTAL, f"{frames} == {TOTAL}")
check("SF2 non-silent", rms > 0.001, f"rms={rms:.5f}")
else:
check("SF2 render exit 0", False, f"rc={rc} {log[-400:]}")
# 2. SFZ render (g2_test.sfz one-shot decays ~200ms; 0.5s clip still has signal)
job = os.path.join(td, "sfz.json"); out = os.path.join(td, "sfz.wav")
rc, log = run_render(make_job(3, SFZ, td), out)
if rc == 0 and os.path.exists(out):
frames, rms = read_wav(out)
check("SFZ render exit 0", rc == 0, log.strip().splitlines()[-1:] or "")
check("SFZ exact duration", frames == TOTAL, f"{frames} == {TOTAL}")
check("SFZ non-silent", rms > 0.001, f"rms={rms:.5f}")
else:
check("SFZ render exit 0", False, f"rc={rc} {log[-400:]}")
# 3. Missing --out -> rc 1
job = os.path.join(td, "noout.json")
with open(job, "w", encoding="utf-8") as f:
json.dump(make_job(2, SF2, td), f)
r = subprocess.run([BRIDGE, "--render", job], capture_output=True, text=True, timeout=30)
check("missing --out -> rc 1", r.returncode == 1, f"rc={r.returncode}")
# 4. Bad plugin path -> rc 2
job = os.path.join(td, "bad.json"); out = os.path.join(td, "bad.wav")
rc, log = run_render(make_job(2, r"C:\nonexistent\no.sf2", td), out)
check("bad plugin_path -> rc 2", rc == 2, f"rc={rc}")
check("bad render removes partial out", not os.path.exists(out), "no clip left behind")
# 5. VST3 render (Nexus) - baseline without preset
NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3"
GOLDEN = os.path.abspath(os.path.join(os.path.dirname(__file__), "golden", "nexus_default.vstpreset"))
if os.path.exists(NEXUS) and os.path.exists(GOLDEN):
import base64
with open(GOLDEN, "rb") as f:
preset_b64 = base64.b64encode(f.read()).decode("ascii")
job = os.path.join(td, "vst3.json"); out = os.path.join(td, "vst3.wav")
rc, log = run_render(make_job(0, NEXUS, td), out)
if rc == 0 and os.path.exists(out):
frames, rms = read_wav(out)
check("VST3 render exit 0", rc == 0, log.strip().splitlines()[-1:] or "")
check("VST3 exact duration", frames == TOTAL, f"{frames} == {TOTAL}")
check("VST3 non-silent", rms > 0.001, f"rms={rms:.5f}")
else:
check("VST3 render exit 0", False, f"rc={rc} {log[-400:]}")
# 6. VST3 render WITH .vstpreset - same default state, must still
# render (proves the parse + loadSerializedState path runs).
job = os.path.join(td, "vst3p.json"); out = os.path.join(td, "vst3p.wav")
jobdict = make_job(0, NEXUS, td); jobdict["preset"] = preset_b64
rc, log = run_render(jobdict, out)
if rc == 0 and os.path.exists(out):
frames, rms = read_wav(out)
check("VST3+preset exit 0", rc == 0, log.strip().splitlines()[-1:] or "")
check("VST3+preset exact duration", frames == TOTAL, f"{frames} == {TOTAL}")
check("VST3+preset non-silent", rms > 0.001, f"rms={rms:.5f}")
check("VST3+preset applied (log)", "preset imported" in log, "log has preset imported")
else:
check("VST3+preset exit 0", False, f"rc={rc} {log[-400:]}")
# 7. Corrupt preset (not base64) -> rc 1, no partial clip
job = os.path.join(td, "vst3bad.json"); out = os.path.join(td, "vst3bad.wav")
jobdict = make_job(0, NEXUS, td); jobdict["preset"] = "!!!not-base64!!!"
rc, log = run_render(jobdict, out)
check("VST3 bad preset -> rc 1", rc == 1, f"rc={rc}")
check("bad preset removes partial out", not os.path.exists(out), "no clip left behind")
# 8. Non-VST3 with preset: ignored (warning), still renders fine
job = os.path.join(td, "sf2p.json"); out = os.path.join(td, "sf2p.wav")
jobdict = make_job(2, SF2, td); jobdict["preset"] = preset_b64
rc, log = run_render(jobdict, out)
if rc == 0 and os.path.exists(out):
frames, rms = read_wav(out)
check("SF2+preset exit 0 (ignored)", rc == 0, log.strip().splitlines()[-1:] or "")
check("SF2+preset non-silent", rms > 0.001, f"rms={rms:.5f}")
else:
check("SF2+preset exit 0 (ignored)", False, f"rc={rc} {log[-400:]}")
failed = [n for n, ok, _ in results if not ok]
print()
print(f"{len(results) - len(failed)}/{len(results)} passed")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())