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:
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user