97 lines
3.9 KiB
Python
97 lines
3.9 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_render_fx_chain_builtin_gain_e2e():
|
|
"""render_fx_chain (PLAN_MASTERBUS_FX_RACK_VST Phase 1): builtin gain
|
|
+6dB qua bridge --render-fx, không cần VST. Output tồn tại, đúng sr,
|
|
RMS gấp đôi input."""
|
|
import math
|
|
import numpy as np
|
|
import soundfile as sf
|
|
exe = native_render.find_bridge_exe()
|
|
if not exe:
|
|
pytest.skip("bridge exe missing")
|
|
sr = 44100
|
|
t = np.linspace(0, 1.0, sr, endpoint=False)
|
|
data = (0.1 * np.sin(2 * np.pi * 440 * t)).astype(np.float32)
|
|
with tempfile.TemporaryDirectory() as td:
|
|
inp = os.path.join(td, "in.wav")
|
|
sf.write(inp, data, sr, subtype="FLOAT")
|
|
out, _lat = native_render.render_fx_chain(
|
|
input_wav=inp,
|
|
fx_chain=[{"type": "builtin", "id": "gain", "params": {"db": 6.0}}],
|
|
sample_rate=sr,
|
|
)
|
|
assert os.path.isfile(out)
|
|
assert _lat == [0] # builtin gain: latency 0
|
|
d, _ = sf.read(out, dtype="float32", always_2d=True)
|
|
rms = float(np.sqrt(np.mean(d ** 2)))
|
|
assert rms > 0.09 # ~2x 0.05
|
|
info = sf.info(out)
|
|
assert info.samplerate == sr
|