Files
SonicForgeStudio/tests/test_native_render.py
T
admin 3d963eb8dd phase2: fix velocity double-scaling in bridge job, bank/program parity, golden RMS test
- render_offline: send velocity as float 0..1 (RenderJob.cpp scales *127);
  old int(0.8*127) was scaled again -> clamp 127, velocity ignored
- forward soundfont_bank/program into bridge job (presence-based, matches
  midi_events_to_messages)
- new tests: bridge branch dispatch (SF_RENDER_ENGINE=bridge -> 404),
  bridge vs pedalboard golden RMS comparison (plan section 6)
2026-08-17 11:04:01 +07:00

93 lines
4.0 KiB
Python

"""Phase 2: native_render client (thay pedalboard bằng native_bridge --render).
Unit: phân loại plugin, tìm bridge, instrument lạ → 404. E2E nhỏ: bridge render
SF2 thật ra WAV đúng duration (giống native_bridge/tests/test_offline_render.py).
Skip nếu máy thiếu bridge exe hoặc file SF2 fixture."""
import json
import os
import subprocess
import tempfile
import pytest
import soundfile as sf
from fastapi import HTTPException
from app.core import native_render
def test_classify_instrument():
assert native_render.classify_instrument(r"C:\VST\Vital.vst3") == 0
assert native_render.classify_instrument(r"C:\VST\foo.dll") == 1
assert native_render.classify_instrument(r"C:\SF\a.sf2") == 2
assert native_render.classify_instrument(r"C:\SF\a.sf3") == 2
assert native_render.classify_instrument(r"C:\SF\a.sfz") == 3
def test_find_bridge_exe_dev():
exe = native_render.find_bridge_exe()
assert exe, "không tìm thấy bridge exe (install/ hoặc SF_BRIDGE_PATH)"
assert os.path.isfile(exe)
def test_render_offline_unknown_instrument_404():
with pytest.raises(HTTPException) as ei:
native_render.render_offline("__no_such_plugin__", [], bpm=120.0, sample_rate=44100)
assert ei.value.status_code == 404
def test_bridge_render_sf2_e2e():
exe = native_render.find_bridge_exe()
if not exe:
pytest.skip("bridge exe missing")
sf2 = r"C:\Users\locpham\SonicForgeStudio\app\storage\soundfonts\518e850f-a5d3-4790-b1f9-0c90c203c524.sf2"
if not os.path.isfile(sf2):
pytest.skip("SF2 fixture missing")
with tempfile.TemporaryDirectory() as td:
job = {"instrument_type": 2, "plugin_path": sf2, "sample_rate": 44100, "bpm": 120.0,
"notes": [{"pitch": 60, "velocity": 0.8, "start_beat": 0.0, "duration_beats": 1.0}]}
jp = os.path.join(td, "job.json")
out = os.path.join(td, "out.wav")
with open(jp, "w", encoding="utf-8") as fh:
json.dump(job, fh)
r = subprocess.run([exe, "--render", jp, "--out", out],
capture_output=True, text=True, timeout=120)
assert r.returncode == 0, ((r.stdout or "") + (r.stderr or ""))[-500:]
assert os.path.isfile(out)
info = sf.info(out)
assert info.samplerate == 44100
assert info.frames == 22050
def test_plugins_bridge_branch_dispatch(monkeypatch):
"""SF_RENDER_ENGINE=bridge: _render_midi_notes_pedalboard phải đi
vào native_render (instrument lạ -> 404), không cần pedalboard."""
from app.api.v1 import plugins as pl
monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "bridge")
with pytest.raises(HTTPException) as ei:
pl._render_midi_notes_pedalboard("__no_such__", [], bpm=120.0, sample_rate=44100)
assert ei.value.status_code == 404
def test_bridge_matches_pedalboard_rms():
"""Golden so sánh (plan §6): bridge vs pedalboard cùng job Nexus → RMS
trong tolerance. Skip nếu máy thiếu pedalboard hoặc Nexus VST3."""
from app.core.vst_engine import HAS_PEDALBOARD, PluginManager
if not HAS_PEDALBOARD:
pytest.skip("pedalboard not installed")
itype, path = native_render._resolve_instrument("Nexus")
if not path:
pytest.skip("Nexus VST3 not installed")
notes = [{"pitch": 60, "velocity": 0.8, "start_beat": 0.0, "duration_beats": 1.0}]
out_b, dur_b = native_render.render_offline("Nexus", notes, bpm=120.0, sample_rate=44100)
vst = PluginManager().load_vst("Nexus")
if vst is None:
pytest.skip("pedalboard cannot load Nexus")
ev = [{"note": 60, "start_beat": 0.0, "duration_beats": 1.0, "velocity": 101}]
msgs = PluginManager.midi_events_to_messages(ev, 120.0, 44100)
buf = vst(msgs, sample_rate=44100, duration=dur_b, num_channels=2)
import numpy as np
audio = sf.read(out_b)[0]
rms_b = float(np.sqrt((audio ** 2).mean()))
rms_p = float(np.sqrt((np.asarray(buf.T) ** 2).mean()))
assert abs(rms_b - rms_p) < 0.01, f"bridge={rms_b:.5f} pedalboard={rms_p:.5f}"