// SFHost.cpp — FluidSynth native bridge (T14) // // DLL export C API để chạy libfluidsynth (native, không WASM) trong audio // loop chung (SPSC + WASAPI, giống VST2/VST3 bridge). Mục tiêu T14: track // SF chơi bằng FluidSynth native cùng master với track VSTi; gain/pan/master // limiter (NativeMixer) áp cho cả 2; preview/items/offline nhất quán. // // QUYẾT ĐỊNH (T14): KHÔNG link tĩnh libfluidsynth (runtime DLL 12MB + phụ // thuộc) — load động qua GetProcAddress từ native_host/fluidsynth_runtime/ // (gitignore; tái tạo bằng scripts/dl_fluidsynth_runtime.py). Load bằng // LoadLibraryExW(LOAD_WITH_ALTERED_SEARCH_PATH) để các DLL phụ thuộc cùng // thư mục được tìm thấy. // // Thread-safety: fluid_synth_* KHÔNG thread-safe — mọi gọi synth (sfload, // noteon/off, write_float, set_gain) chỉ trên audio thread; sfload CHỈ trước // AudioStart. MIDI từ UI thread → SPSC (AudioEngine) → audio thread drain. #include #include #include #include #include #include #include #include #include #include "AudioEngine.h" #include "NativeMixer.h" using int32 = int32_t; namespace { // FluidSynth C API (runtime load — xem fluidsynth/synth.h, settings.h) typedef void* fluid_settings_t; typedef void* fluid_synth_t; typedef fluid_settings_t (*FS_SettingsNew) (void); typedef int (*FS_SettingsSetnum) (fluid_settings_t, const char*, double); typedef int (*FS_SettingsSetstr) (fluid_settings_t, const char*, const char*); typedef void (*FS_SettingsDelete) (fluid_settings_t); typedef fluid_synth_t (*FS_SynthNew) (fluid_settings_t); typedef int (*FS_SynthDelete) (fluid_synth_t); typedef int (*FS_SynthSfload) (fluid_synth_t, const char*, int); typedef int (*FS_SynthNoteon) (fluid_synth_t, int, int, int); typedef int (*FS_SynthNoteoff) (fluid_synth_t, int, int); typedef int (*FS_SynthAllNotesOff) (fluid_synth_t, int); typedef void (*FS_SynthWriteFloat) (fluid_synth_t, int, float*, int, int, float*, int, int); typedef int (*FS_SynthSetGain) (fluid_synth_t, float); typedef int (*FS_SynthProgramSelect) (fluid_synth_t, int, int, int, int); struct FS { FS_SettingsNew settings_new = nullptr; FS_SettingsSetnum settings_setnum = nullptr; FS_SettingsSetstr settings_setstr = nullptr; FS_SettingsDelete settings_delete = nullptr; FS_SynthNew synth_new = nullptr; FS_SynthDelete synth_delete = nullptr; FS_SynthSfload synth_sfload = nullptr; FS_SynthNoteon synth_noteon = nullptr; FS_SynthNoteoff synth_noteoff = nullptr; FS_SynthAllNotesOff synth_all_notes_off = nullptr; FS_SynthWriteFloat synth_write_float = nullptr; FS_SynthSetGain synth_set_gain = nullptr; FS_SynthProgramSelect synth_program_select = nullptr; }; bool load_fs (HMODULE m, FS& fs) { // FluidSynth 2.5.x exports: new_fluid_*/delete_fluid_* (prefix), còn lại // fluid_* — xác minh bằng dumpbin /exports trên libfluidsynth-3.dll. fs.settings_new = reinterpret_cast (GetProcAddress (m, "new_fluid_settings")); fs.settings_setnum = reinterpret_cast (GetProcAddress (m, "fluid_settings_setnum")); fs.settings_setstr = reinterpret_cast (GetProcAddress (m, "fluid_settings_setstr")); fs.settings_delete = reinterpret_cast (GetProcAddress (m, "delete_fluid_settings")); fs.synth_new = reinterpret_cast (GetProcAddress (m, "new_fluid_synth")); fs.synth_delete = reinterpret_cast (GetProcAddress (m, "delete_fluid_synth")); fs.synth_sfload = reinterpret_cast (GetProcAddress (m, "fluid_synth_sfload")); fs.synth_noteon = reinterpret_cast (GetProcAddress (m, "fluid_synth_noteon")); fs.synth_noteoff = reinterpret_cast (GetProcAddress (m, "fluid_synth_noteoff")); fs.synth_all_notes_off = reinterpret_cast (GetProcAddress (m, "fluid_synth_all_notes_off")); fs.synth_write_float = reinterpret_cast (GetProcAddress (m, "fluid_synth_write_float")); fs.synth_set_gain = reinterpret_cast (GetProcAddress (m, "fluid_synth_set_gain")); fs.synth_program_select = reinterpret_cast (GetProcAddress (m, "fluid_synth_program_select")); return fs.settings_new && fs.settings_setnum && fs.synth_new && fs.synth_sfload && fs.synth_noteon && fs.synth_noteoff && fs.synth_write_float && fs.synth_set_gain; } struct Instance { HMODULE fsLib = nullptr; FS fs; fluid_settings_t settings = nullptr; fluid_synth_t synth = nullptr; int sfid = -1; // sfont id tra ve tu fluid_synth_sfload int32 sampleRate = 44100; int32 blockSize = 512; int32 handle = 0; sonicforge::AudioEngine audio; std::atomic audioRunning {false}; sonicforge::NativeMixer mixer; }; std::mutex g_mutex; std::map> g_instances; int32 g_next_handle = 1; void set_err (char* err, int32 err_cap, const char* msg) { if (err && err_cap > 0) std::snprintf (err, static_cast (err_cap), "%s", msg); } // Audio thread callback (WASAPI render thread): drain MIDI SPSC → synth // noteon/noteoff → fluid_synth_write_float → mixer (gain/pan/limiter). bool fsAudioProcess (const sonicforge::MidiEvent* events, int32 eventCount, float* outL, float* outR, int32 frames, void* userdata) { Instance* inst = static_cast (userdata); if (!inst || !inst->synth) return false; for (int32 i = 0; i < eventCount; ++i) { const sonicforge::MidiEvent& e = events[i]; if (e.noteOn) inst->fs.synth_noteon (inst->synth, e.channel, e.pitch, static_cast (e.velocity * 127.0f)); else inst->fs.synth_noteoff (inst->synth, e.channel, e.pitch); } inst->fs.synth_write_float (inst->synth, frames, outL, 0, 1, outR, 0, 1); inst->mixer.processInPlace (outL, outR, frames); return true; } } // namespace extern "C" { // Tạo synth + load runtime DLL. sfload CHỈ sau Create (trước AudioStart). // Trả handle (>= 1) hoặc 0 + err. __declspec (dllexport) int32 SF_FS_Create (int32 sampleRate, int32 blockSize, char* err, int32 err_cap) { std::lock_guard lock (g_mutex); auto inst = std::make_unique (); inst->sampleRate = sampleRate > 0 ? sampleRate : 44100; inst->blockSize = blockSize > 0 ? blockSize : 512; // Runtime DLL trong native_host/fluidsynth_runtime/ (gitignore) — tìm // tương đối với chính sf_host_bridge.dll (GetModuleHandleW) chứ không // phải exe host: bridge có thể được load từ python test / Tauri app. HMODULE self = GetModuleHandleW (L"sf_host_bridge"); wchar_t selfPath[MAX_PATH] = {}; DWORD selfLen = self ? GetModuleFileNameW (self, selfPath, MAX_PATH) : 0; std::wstring dir; if (selfLen > 0) { dir = selfPath; auto slash = dir.find_last_of (L'\\'); if (slash != std::wstring::npos) dir.resize (slash + 1); } std::wstring fsDll = dir + L"fluidsynth_runtime\\libfluidsynth-3.dll"; inst->fsLib = LoadLibraryExW (fsDll.c_str (), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); if (!inst->fsLib) { set_err (err, err_cap, "LoadLibraryExW libfluidsynth failed"); return 0; } if (!load_fs (inst->fsLib, inst->fs)) { FreeLibrary (inst->fsLib); set_err (err, err_cap, "libfluidsynth exports missing"); return 0; } inst->settings = inst->fs.settings_new (); inst->fs.settings_setnum (inst->settings, "synth.sample-rate", static_cast (inst->sampleRate)); // T14: render thẳng ra float stereo, không qua audio driver của fluidsynth. inst->fs.settings_setstr (inst->settings, "audio.driver", "file"); inst->synth = inst->fs.synth_new (inst->settings); if (!inst->synth) { inst->fs.settings_delete (inst->settings); FreeLibrary (inst->fsLib); set_err (err, err_cap, "fluid_synth_new failed"); return 0; } // WASM client dùng gain 1.0 — mirror. inst->fs.synth_set_gain (inst->synth, 1.0f); int32 handle = g_next_handle++; inst->handle = handle; g_instances[handle] = std::move (inst); return handle; } // Load SF2/SF3 vào synth. reset=0: giữ preset đã chọn. CHỈ trước AudioStart. __declspec (dllexport) int32 SF_FS_LoadSF2 (int32 handle, const char* path_utf8, char* err, int32 err_cap) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) { set_err (err, err_cap, "bad handle"); return -1; } Instance* inst = it->second.get (); if (!inst->synth || !path_utf8) { set_err (err, err_cap, "not created or null path"); return -2; } if (inst->audioRunning.load (std::memory_order_acquire)) { set_err (err, err_cap, "sfload only before AudioStart"); return -3; } int sfid = inst->fs.synth_sfload (inst->synth, path_utf8, 0); if (sfid < 0) { set_err (err, err_cap, "fluid_synth_sfload failed"); return -4; } inst->sfid = sfid; return 0; } // Chọn instrument channel (bank/program) — mirror WASM selectInstrument. // sfont_id: id trả từ SF_FS_LoadSF2 (0..n-1); fluid_synth_program_select // signature: (synth, chan, sfont_id, bank_num, preset_num). __declspec (dllexport) int32 SF_FS_SelectInstrument (int32 handle, int32 channel, int32 sfontId, int32 bank, int32 program) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; Instance* inst = it->second.get (); if (!inst->synth) return -2; int fid = (sfontId < 0) ? inst->sfid : sfontId; return inst->fs.synth_program_select (inst->synth, channel, fid, bank, program); } __declspec (dllexport) int32 SF_FS_NoteOn (int32 handle, int32 channel, int32 pitch, int32 velocity) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; Instance* inst = it->second.get (); if (!inst->synth) return -2; if (inst->audioRunning.load (std::memory_order_acquire)) { sonicforge::MidiEvent ev = {}; ev.channel = channel; ev.pitch = pitch; ev.velocity = static_cast (velocity) / 127.0f; ev.noteOn = true; inst->audio.pushMidi (ev); return 0; } return inst->fs.synth_noteon (inst->synth, channel, pitch, velocity); } __declspec (dllexport) int32 SF_FS_NoteOff (int32 handle, int32 channel, int32 pitch) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; Instance* inst = it->second.get (); if (!inst->synth) return -2; if (inst->audioRunning.load (std::memory_order_acquire)) { sonicforge::MidiEvent ev = {}; ev.channel = channel; ev.pitch = pitch; ev.velocity = 0.0f; ev.noteOn = false; inst->audio.pushMidi (ev); return 0; } return inst->fs.synth_noteoff (inst->synth, channel, pitch); } __declspec (dllexport) int32 SF_FS_AllNotesOff (int32 handle, int32 channel) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; Instance* inst = it->second.get (); if (!inst->synth) return -2; return inst->fs.synth_all_notes_off (inst->synth, channel); } // Render offline 1 block thẳng ra buffer (không qua WASAPI): fluid_synth_write_float // + mixer. Dùng cho golden test T14 (so với server reference) và offline path. // outL/outR: frames mẫu mỗi kênh. Trả 0 nếu OK. __declspec (dllexport) int32 SF_FS_RenderBlock (int32 handle, int32 frames, float* outL, float* outR) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; Instance* inst = it->second.get (); if (!inst->synth || !outL || !outR || frames <= 0) return -2; inst->fs.synth_write_float (inst->synth, frames, outL, 0, 1, outR, 0, 1); inst->mixer.processInPlace (outL, outR, frames); return 0; } __declspec (dllexport) int32 SF_FS_AudioStart (int32 handle, int32 sampleRate, int32 blockSize, char* err, int32 err_cap) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; Instance* inst = it->second.get (); if (!inst->synth) return -2; inst->audioRunning.store (true, std::memory_order_release); if (!inst->audio.start (sampleRate > 0 ? sampleRate : inst->sampleRate, blockSize > 0 ? blockSize : inst->blockSize, &fsAudioProcess, inst, err, err_cap)) { inst->audioRunning.store (false, std::memory_order_release); return -3; } return 0; } __declspec (dllexport) int32 SF_FS_AudioStop (int32 handle) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; it->second->audioRunning.store (false, std::memory_order_release); it->second->audio.stop (); return 0; } __declspec (dllexport) int32 SF_FS_AudioUnderruns (int32 handle) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; return it->second->audio.underruns (); } __declspec (dllexport) int32 SF_FS_AudioLatency (int32 handle) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; return it->second->audio.latencySamples (); } __declspec (dllexport) int32 SF_FS_AudioBlocks (int32 handle) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; return it->second->audio.blocksRendered (); } // T13 chung: track gain/pan + master limiter qua NativeMixer. __declspec (dllexport) int32 SF_FS_SetTrackGainPan (int32 handle, float gainDb, float pan) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; it->second->mixer.setTrack (sonicforge::TrackGainPan::fromDbPan (gainDb, pan)); return 0; } __declspec (dllexport) int32 SF_FS_SetMasterLimiter (int32 handle, int32 active, float thresholdDb) { std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; it->second->mixer.setLimiter (active != 0, thresholdDb); return 0; } // Đóng: stop audio loop, xóa synth, giải phóng DLL. __declspec (dllexport) int32 SF_FS_Close (int32 handle, char* err, int32 err_cap) { (void) err; (void) err_cap; std::lock_guard lock (g_mutex); auto it = g_instances.find (handle); if (it == g_instances.end ()) return -1; Instance* inst = it->second.get (); inst->audioRunning.store (false, std::memory_order_release); inst->audio.stop (); if (inst->synth) inst->fs.synth_delete (inst->synth); if (inst->settings) inst->fs.settings_delete (inst->settings); if (inst->fsLib) FreeLibrary (inst->fsLib); g_instances.erase (it); return 0; } } // extern "C"