T14: SF host bridge native (FluidSynth runtime DLL) + golden test

This commit is contained in:
2026-08-11 17:11:29 +07:00
parent b9817e1aed
commit 500384a226
6 changed files with 862 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Test smoke SFHost bridge (T14): FluidSynth native qua ctypes.
Chạy: python tests/test_sf_host_bridge.py [path_sf2]
Yêu cầu: native_host/build/Release/sf_host_bridge.dll + fluidsynth_runtime/.
Verify: Create → LoadSF2 → SelectInstrument → AudioStart → NoteOn → chờ
(blocks tăng, underruns 0) → NoteOff → AudioStop → Close; mixer set OK.
"""
import ctypes
import ctypes.wintypes as wt
import os
import sys
import time
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DLL = os.path.join(ROOT, "build", "Release", "sf_host_bridge.dll")
SF2 = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
ROOT, "..", "app", "storage", "soundfonts",
"518e850f-a5d3-4790-b1f9-0c90c203c524.sf2")
assert os.path.exists(DLL), f"thiếu {DLL}"
assert os.path.exists(SF2), f"thiếu {SF2}"
dll = ctypes.WinDLL(DLL)
err = ctypes.create_string_buffer(256)
dll.SF_FS_Create.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.c_char_p, ctypes.c_int32]
dll.SF_FS_Create.restype = ctypes.c_int32
dll.SF_FS_LoadSF2.argtypes = [ctypes.c_int32, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int32]
dll.SF_FS_LoadSF2.restype = ctypes.c_int32
dll.SF_FS_SelectInstrument.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.c_int32, ctypes.c_int32, ctypes.c_int32]
dll.SF_FS_SelectInstrument.restype = ctypes.c_int32
dll.SF_FS_NoteOn.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.c_int32, ctypes.c_int32]
dll.SF_FS_NoteOn.restype = ctypes.c_int32
dll.SF_FS_NoteOff.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.c_int32]
dll.SF_FS_NoteOff.restype = ctypes.c_int32
dll.SF_FS_AudioStart.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.c_int32, ctypes.c_char_p, ctypes.c_int32]
dll.SF_FS_AudioStart.restype = ctypes.c_int32
dll.SF_FS_AudioStop.argtypes = [ctypes.c_int32]
dll.SF_FS_AudioStop.restype = ctypes.c_int32
dll.SF_FS_AudioUnderruns.argtypes = [ctypes.c_int32]
dll.SF_FS_AudioUnderruns.restype = ctypes.c_int32
dll.SF_FS_AudioBlocks.argtypes = [ctypes.c_int32]
dll.SF_FS_AudioBlocks.restype = ctypes.c_int32
dll.SF_FS_SetTrackGainPan.argtypes = [ctypes.c_int32, ctypes.c_float, ctypes.c_float]
dll.SF_FS_SetTrackGainPan.restype = ctypes.c_int32
dll.SF_FS_SetMasterLimiter.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.c_float]
dll.SF_FS_SetMasterLimiter.restype = ctypes.c_int32
dll.SF_FS_Close.argtypes = [ctypes.c_int32, ctypes.c_char_p, ctypes.c_int32]
dll.SF_FS_Close.restype = ctypes.c_int32
def check(name, rc, expect=0):
assert rc == expect, f"{name}: rc={rc} err={err.value.decode() if err.value else ''}"
print(f"OK {name}")
h = dll.SF_FS_Create(44100, 512, err, 256)
assert h > 0, f"Create trả {h}, err={err.value.decode() if err.value else ''}"
print(f"OK SF_FS_Create -> handle {h}")
check("SF_FS_LoadSF2", dll.SF_FS_LoadSF2(h, SF2.encode(), err, 256))
# Tìm preset đầu tiên tồn tại trong SF2 (SF2 tuỳ biến có thể không có 0/0).
sel = None
for bank in (0, 1, 128):
for prog in (0, 1, 12, 40, 80):
rc = dll.SF_FS_SelectInstrument(h, 0, -1, bank, prog)
if rc == 0:
sel = (bank, prog)
break
if sel:
break
assert sel, "không tìm được preset nào trong SF2"
print(f"OK SF_FS_SelectInstrument bank={sel[0]} program={sel[1]}")
check("SF_FS_SetTrackGainPan", dll.SF_FS_SetTrackGainPan(h, -6.0, 0.0))
check("SF_FS_SetMasterLimiter", dll.SF_FS_SetMasterLimiter(h, 1, -1.0))
check("SF_FS_AudioStart", dll.SF_FS_AudioStart(h, 44100, 512, err, 256))
b0 = dll.SF_FS_AudioBlocks(h)
check("SF_FS_NoteOn", dll.SF_FS_NoteOn(h, 0, 60, 100))
time.sleep(0.4)
b1 = dll.SF_FS_AudioBlocks(h)
u = dll.SF_FS_AudioUnderruns(h)
assert b1 > b0, f"blocks không tăng: {b0} -> {b1}"
assert u == 0, f"underruns={u}"
print(f"OK render blocks {b0} -> {b1}, underruns {u}")
check("SF_FS_NoteOff", dll.SF_FS_NoteOff(h, 0, 60))
time.sleep(0.2)
check("SF_FS_AudioStop", dll.SF_FS_AudioStop(h))
check("SF_FS_Close", dll.SF_FS_Close(h, err, 256))
print("PASS")