#!/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")