"""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 = 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) 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