phase2: fix velocity double-scaling in bridge job, bank/program parity, golden RMS test

- 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)
This commit is contained in:
2026-08-17 11:04:01 +07:00
parent 235dee103d
commit 3d963eb8dd
3 changed files with 46 additions and 3 deletions
+2
View File
@@ -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")
+9 -2
View File
@@ -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"
+35 -1
View File
@@ -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}"