3d963eb8dd
- 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)
93 lines
4.0 KiB
Python
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}"
|