diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 0925240..e7c53e7 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -995,6 +995,8 @@ def _render_midi_notes_pedalboard(instrument_id: str, notes: list, bpm: float, preset_id=preset_id, preset_path=preset_path, preset_data_b64=preset_data_b64, + soundfont_bank=soundfont_bank, + soundfont_program=soundfont_program, ) if not HAS_PEDALBOARD: raise HTTPException(status_code=501, detail="pedalboard không khả dụng trên máy này") diff --git a/app/core/native_render.py b/app/core/native_render.py index c74a3b0..add5f71 100644 --- a/app/core/native_render.py +++ b/app/core/native_render.py @@ -77,7 +77,8 @@ def _preset_b64(preset_id, preset_path, preset_data_b64) -> str: def render_offline(instrument_id, notes, bpm, sample_rate, preset_id=None, - preset_path=None, preset_data_b64=None) -> tuple: + preset_path=None, preset_data_b64=None, + soundfont_bank=None, soundfont_program=None) -> tuple: """Render MIDI notes qua native_bridge --render → (out_path, duration_sec). Giữ response shape của _render_midi_notes_pedalboard. Lỗi → HTTPException @@ -103,7 +104,7 @@ def render_offline(instrument_id, notes, bpm, sample_rate, preset_id=None, "bpm": float(bpm), "notes": [{ "pitch": int(n.get("pitch", 60)), - "velocity": int(float(n.get("velocity", 0.8)) * 127), + "velocity": float(n.get("velocity", 0.8)), # bridge job: float 0..1 (RenderJob.cpp *127) "start_beat": float(n.get("start_beat", 0)), "duration_beats": float(n.get("duration_beats", 1)), } for n in notes], @@ -112,6 +113,12 @@ def render_offline(instrument_id, notes, bpm, sample_rate, preset_id=None, preset = _preset_b64(preset_id, preset_path, preset_data_b64) if preset: job["preset"] = preset + # bank/program (giữ parity với midi_events_to_messages cũ: key có mặt + # kể cả giá trị 0 — bridge xử lý theo presence, không theo truthiness) + if soundfont_bank is not None: + job["soundfont_bank"] = int(soundfont_bank) & 0x7F + if soundfont_program is not None: + job["soundfont_program"] = int(soundfont_program) & 0x7F os.makedirs(settings.PROCESSED_DIR, exist_ok=True) out_path = os.path.join(settings.PROCESSED_DIR, f"preview_{uuid.uuid4().hex[:10]}.wav") job_path = out_path + ".job.json" diff --git a/tests/test_native_render.py b/tests/test_native_render.py index 152d0b6..ecd82d1 100644 --- a/tests/test_native_render.py +++ b/tests/test_native_render.py @@ -44,7 +44,7 @@ def test_bridge_render_sf2_e2e(): 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": 101, "start_beat": 0.0, "duration_beats": 1.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: @@ -56,3 +56,37 @@ def test_bridge_render_sf2_e2e(): 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}"