Gap 12: soundfont render qua native bridge — render_soundfont_offline (FluidSynth C API) thay pyfluidsynth khi thiếu lib; render_engine soundfont branch fallback bridge (test_64, 176 passed)

This commit is contained in:
2026-08-23 08:52:57 +07:00
parent 6b2c9a039b
commit 2b5f396133
3 changed files with 186 additions and 58 deletions
+65
View File
@@ -32,6 +32,8 @@ FX_BRIDGE = os.path.join(REPO, "native_bridge", "build", "Release", "fx_vst_brid
DAW_BRIDGE = os.path.join(REPO, "native_bridge", "build", "Release", "daw_vst_bridge.exe")
ADELAY = os.path.join(REPO, "native_bridge", "build", "VST3", "Release", "adelay.vst3")
SR = 44100
REAL_SF2 = os.path.join(REPO, "app", "storage", "soundfonts", "518e850f-a5d3-4790-b1f9-0c90c203c524.sf2")
@pytest.fixture(scope="module")
@@ -741,3 +743,66 @@ def test_63_fxrt_ws_latency_push(monkeypatch):
assert d.get("cmd") == "latency", d
assert d.get("total") == 1024, d
assert d.get("latencies", {}).get("0") == 1024, d
# ── 5.10 Gap 12: SoundFont thật qua native bridge (thay pyfluidsynth) ─────
def test_64_soundfont_render_via_bridge(monkeypatch, tmp_path):
"""Gap 12: pyfluidsynth thiếu FluidSynth lib → native bridge render SF2 thật
(bridge nhúng FluidSynth C API, NativeInstrumentEngine.cpp).
(1) render_soundfont_offline trực tiếp: non-silent, đúng duration, program
khác → audio khác (program_select áp).
(2) render_project với HAS_PYFLUIDSYNTH=False: track MIDI soundfont ra âm
thật — không fallback oscillator synth."""
if not os.path.isfile(DAW_BRIDGE):
pytest.skip("daw_vst_bridge.exe chưa build (native_bridge/build/Release)")
if not os.path.isfile(REAL_SF2):
pytest.skip("không có SF2 mẫu trong app/storage/soundfonts")
monkeypatch.setenv("SF_BRIDGE_PATH", DAW_BRIDGE)
from app.core import native_render
notes = [{"pitch": 60, "velocity": 0.8, "start_beat": 0.0, "duration_beats": 2.0}]
out0, dur0 = native_render.render_soundfont_offline(REAL_SF2, notes, 120.0, SR, bank=0, program=0)
try:
d0, sr0 = _read_wav(out0)
assert sr0 == SR, f"sr {sr0}"
assert d0.shape[1] >= 44000, f"duration ngắn: {d0.shape[1]} (2 beats @120 = 44100)"
assert float(np.max(np.abs(d0))) > 0.01, "SF2 render im lặng"
finally:
os.remove(out0)
out40, _ = native_render.render_soundfont_offline(REAL_SF2, notes, 120.0, SR, bank=0, program=40)
try:
d40, _ = _read_wav(out40)
n = min(d0.shape[1], d40.shape[1])
assert float(np.max(np.abs(d40[:, :n] - d0[:, :n]))) > 1e-3, "program_select không áp — program 0 == 40"
finally:
os.remove(out40)
# (2) engine-level: bắt buộc branch bridge (pyfluidsynth vắng mặt)
import app.core.vst_engine as ve
import app.core.render_engine as re_mod
monkeypatch.setattr(ve, "HAS_PYFLUIDSYNTH", False)
monkeypatch.setattr(re_mod, "HAS_PYFLUIDSYNTH", False)
monkeypatch.setattr(re_mod, "_find_sf2_path", lambda sf_id: REAL_SF2)
from app.api.v1 import plugins as pl
monkeypatch.setattr(pl.settings, "RENDER_ENGINE", "bridge")
from app.core.render_engine import PythonRenderEngine
engine = PythonRenderEngine()
track = _midi_track()
track["instrument_id"] = ""
track["instrument_source"] = "soundfont"
track["soundfont_id"] = "518e850f-a5d3-4790-b1f9-0c90c203c524"
track["volume_db"] = -12.0
project = {
"metadata": {"bpm": 120.0, "time_signature_numerator": 4},
"main_session": {
"length_bars": 1.0,
"tracks": [track],
"master": {"fx_chain": [], "bypass": False, "volume_db": 0.0},
},
"section_store": {},
}
out = str(tmp_path / "sf2_engine.wav")
engine.render_project(project, out)
data, _ = _read_wav(out)
assert float(np.max(np.abs(data))) > 0.01, "engine render soundfont im lặng — fallback oscillator?"