import os import json import pytest from app.core.render_engine import PythonRenderEngine import numpy as np def test_render_engine_init(): engine = PythonRenderEngine() assert engine.sample_rate == 44100 def test_bars_to_samples(): engine = PythonRenderEngine() samples = engine.bars_to_samples(4.0, 120.0, 4) assert samples == 352800 def test_render_session_container_empty(): engine = PythonRenderEngine() session = {"tracks": []} buf = engine.render_session_container(session, {}, 120.0, 4, 1000) assert buf.shape == (2, 1000) assert (buf == 0.0).all() def _midi_track(instrument_id, fx_type=None): return { "id": "t1", "type": "MIDI", "instrument_id": instrument_id, "instrument_source": "vsti", "fx_type": fx_type, "volume_db": 0.0, "pan": 0.0, "mute": False, "items": [{ "type": "MIDI_ITEM", "start_bar": 0.0, "duration_bars": 1.0, "clip_start_offset_bars": 0.0, "source_data": { "notes": [ {"pitch": 60, "velocity": 0.8, "start_beat": 0.0, "duration_beats": 1.0} ] }, }], } def test_render_session_vsti_bridge_unknown_instrument_falls_back(monkeypatch): """SF_RENDER_ENGINE=bridge: track VSTi đi vào native_render; instrument lạ (404) → fallback oscillator, không crash, không cần pedalboard.""" from app.api.v1 import plugins as pl monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "bridge") engine = PythonRenderEngine() session = {"tracks": [_midi_track("__no_such_plugin__")]} buf = engine.render_session_container(session, {}, 120.0, 4, 44100) assert buf.shape == (2, 44100) # oscillator sinh âm thật (non-silent) assert (buf != 0.0).any() def test_render_session_vsti_non_bridge_engine_falls_back(monkeypatch): """Engine không phải bridge (vd 'pedalboard' cũ): VSTi → fallback oscillator (không crash).""" from app.api.v1 import plugins as pl monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "pedalboard") engine = PythonRenderEngine() session = {"tracks": [_midi_track("__no_such_plugin__")]} buf = engine.render_session_container(session, {}, 120.0, 4, 44100) assert buf.shape == (2, 44100) def test_render_session_fx_chorus_scipy_fallback(): """FX chorus: fallback scipy (không pedalboard) không crash, thay đổi âm.""" from app.api.v1 import plugins as pl pl.settings.RENDER_ENGINE = "bridge" engine = PythonRenderEngine() session = {"tracks": [_midi_track("__no_such_plugin__", fx_type="chorus")]} buf = engine.render_session_container(session, {}, 120.0, 4, 44100) assert buf.shape == (2, 44100) assert (buf != 0.0).any() def test_render_session_fx_reverb_scipy_fallback(): """FX reverb: fallback scipy (không pedalboard) không crash, thay đổi âm.""" from app.api.v1 import plugins as pl pl.settings.RENDER_ENGINE = "bridge" engine = PythonRenderEngine() session = {"tracks": [_midi_track("__no_such_plugin__", fx_type="reverb")]} buf = engine.render_session_container(session, {}, 120.0, 4, 44100) assert buf.shape == (2, 44100) assert (buf != 0.0).any() _BRIDGE_TEST_PATH = r"C:/Users/locpham/SonicForgeStudio/native_bridge/build/Release/daw_vst_bridge.exe" def _peak(buf): return float(np.max(np.abs(buf))) def test_render_track_fx_chain_gain_via_bridge(monkeypatch): """Track fx_chain (builtin gain +6dB) qua native_bridge → biên độ x2.""" import numpy as np if not os.path.isfile(_BRIDGE_TEST_PATH): pytest.skip("bridge chưa build") monkeypatch.setenv("SF_BRIDGE_PATH", _BRIDGE_TEST_PATH) from app.api.v1 import plugins as pl monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "bridge") engine = PythonRenderEngine() base = _midi_track("__no_such_plugin__") fx = _midi_track("__no_such_plugin__") fx["vst_fx_chain"] = [{"type": "builtin", "id": "gain", "params": {"db": 6.0}}] b0 = engine.render_session_container({"tracks": [base]}, {}, 120.0, 4, 44100) b1 = engine.render_session_container({"tracks": [fx]}, {}, 120.0, 4, 44100) assert _peak(b1) > 1.5 * _peak(b0) def test_render_project_master_fx_chain_gain_via_bridge(monkeypatch, tmp_path): """Masterbus master.fx_chain (gain +6dB) qua native_bridge → peak x2.""" import numpy as np if not os.path.isfile(_BRIDGE_TEST_PATH): pytest.skip("bridge chưa build") monkeypatch.setenv("SF_BRIDGE_PATH", _BRIDGE_TEST_PATH) from app.api.v1 import plugins as pl monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "bridge") engine = PythonRenderEngine() def project(with_master): track = _midi_track("__no_such_plugin__") track["volume_db"] = -20.0 # tránh normalize (>1.0) làm hỏng tỉ lệ 2x proj = { "metadata": {"bpm": 120.0, "time_signature_numerator": 4}, "main_session": {"length_bars": 1.0, "tracks": [track]}, "section_store": {}, } if with_master: proj["main_session"]["master"] = { "fx_chain": [{"type": "builtin", "id": "gain", "params": {"db": 6.0}}], "bypass": False, } return proj out0 = str(tmp_path / "no_master.wav") out1 = str(tmp_path / "with_master.wav") engine.render_project(project(False), out0) engine.render_project(project(True), out1) _, d0 = sf_read_peaks(out0) _, d1 = sf_read_peaks(out1) assert d1 > 1.5 * d0 import soundfile as _sf def sf_read_peaks(path): import numpy as np data, _ = _sf.read(path, dtype="float32", always_2d=True) return data, float(np.max(np.abs(data))) _FX_BRIDGE_TEST_PATH = r"C:/Users/locpham/SonicForgeStudio/native_bridge/build/Release/fx_vst_bridge.exe" def _master_project(chain, pan=0.6, **flat): """Project với mastering_settings.chain + flat mastering knobs (giống UI).""" track = _midi_track("__no_such_plugin__") track["volume_db"] = -20.0 # tránh normalize (>1.0) làm hỏng tỉ lệ track["pan"] = pan # constant-power pan → L≠R: master có side content proj = { "metadata": {"bpm": 120.0, "time_signature_numerator": 4}, "main_session": {"length_bars": 1.0, "tracks": [track]}, "section_store": {}, } if chain is not None: ms = {"chain": chain} ms.update(flat) proj["mastering_settings"] = ms return proj def _side_rms(path): data, _ = _sf.read(path, dtype="float32", always_2d=True) side = data[:, 0] - data[:, 1] return float(np.sqrt(np.mean(side ** 2))) def _render_rms(path): data, _ = _sf.read(path, dtype="float32", always_2d=True) return float(np.sqrt(np.mean(data ** 2))) def test_render_master_chain_imager_maximizer_via_bridge(monkeypatch, tmp_path): """Phase 3.2: master dùng mastering_settings.chain (hết bất đối xứng) — imager w=0 giết side, w=200 tăng side, maximizer boost RMS — qua bridge.""" if not os.path.isfile(_FX_BRIDGE_TEST_PATH): pytest.skip("fx bridge chưa build") monkeypatch.setenv("SF_FX_BRIDGE_PATH", _FX_BRIDGE_TEST_PATH) monkeypatch.setenv("SF_BRIDGE_PATH", _BRIDGE_TEST_PATH) from app.api.v1 import plugins as pl monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "bridge") engine = PythonRenderEngine() def render(proj, name): out = str(tmp_path / name) engine.render_project(proj, out) return out base = _side_rms(render(_master_project(None), "base.wav")) s0 = _side_rms(render(_master_project( [{"id": "imager", "type": "imager", "name": "Imager", "active": True}], w1=0, w2=0, w3=0, w4=0), "imager0.wav")) s2 = _side_rms(render(_master_project( [{"id": "imager", "type": "imager", "name": "Imager", "active": True}], w1=200, w2=200, w3=200, w4=200), "imager200.wav")) assert base > 0.001 # pan tạo side content assert s0 < 0.05 * base # w=0 → side≈0 (imager đã được áp qua bridge) assert s2 > 1.5 * base # w=200 → side tăng rm0 = _render_rms(render(_master_project( [{"id": "maximizer", "type": "maximizer", "name": "Max", "active": True}], maxGain=0.0), "max0.wav")) rm6 = _render_rms(render(_master_project( [{"id": "maximizer", "type": "maximizer", "name": "Max", "active": True}], maxGain=6.0), "max6.wav")) assert rm6 > 1.3 * rm0 # boost 6dB → RMS tăng def test_probe_track_latency_no_vst3_returns_zero(monkeypatch): """Phase 3.3: probe latency chỉ spawn bridge khi chain có vst3 — chain builtin-only → 0, không spawn exe.""" from app.api.v1 import plugins as pl monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "bridge") track = _midi_track("__no_such_plugin__") track["fx_chain"] = [{"type": "builtin", "id": "gain", "params": {"db": 6.0}}] from app.core.render_engine import _probe_track_latency assert _probe_track_latency(track, 44100) == 0