447 lines
16 KiB
C++
447 lines
16 KiB
C++
// 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 <windows.h>
|
|
|
|
#include <atomic>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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<FS_SettingsNew> (GetProcAddress (m, "new_fluid_settings"));
|
|
fs.settings_setnum = reinterpret_cast<FS_SettingsSetnum> (GetProcAddress (m, "fluid_settings_setnum"));
|
|
fs.settings_setstr = reinterpret_cast<FS_SettingsSetstr> (GetProcAddress (m, "fluid_settings_setstr"));
|
|
fs.settings_delete = reinterpret_cast<FS_SettingsDelete> (GetProcAddress (m, "delete_fluid_settings"));
|
|
fs.synth_new = reinterpret_cast<FS_SynthNew> (GetProcAddress (m, "new_fluid_synth"));
|
|
fs.synth_delete = reinterpret_cast<FS_SynthDelete> (GetProcAddress (m, "delete_fluid_synth"));
|
|
fs.synth_sfload = reinterpret_cast<FS_SynthSfload> (GetProcAddress (m, "fluid_synth_sfload"));
|
|
fs.synth_noteon = reinterpret_cast<FS_SynthNoteon> (GetProcAddress (m, "fluid_synth_noteon"));
|
|
fs.synth_noteoff = reinterpret_cast<FS_SynthNoteoff> (GetProcAddress (m, "fluid_synth_noteoff"));
|
|
fs.synth_all_notes_off = reinterpret_cast<FS_SynthAllNotesOff> (GetProcAddress (m, "fluid_synth_all_notes_off"));
|
|
fs.synth_write_float = reinterpret_cast<FS_SynthWriteFloat> (GetProcAddress (m, "fluid_synth_write_float"));
|
|
fs.synth_set_gain = reinterpret_cast<FS_SynthSetGain> (GetProcAddress (m, "fluid_synth_set_gain"));
|
|
fs.synth_program_select = reinterpret_cast<FS_SynthProgramSelect> (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<bool> audioRunning {false};
|
|
sonicforge::NativeMixer mixer;
|
|
};
|
|
|
|
std::mutex g_mutex;
|
|
std::map<int32, std::unique_ptr<Instance>> 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<size_t> (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<Instance*> (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<int> (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<std::mutex> lock (g_mutex);
|
|
|
|
auto inst = std::make_unique<Instance> ();
|
|
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<double> (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<std::mutex> 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<std::mutex> 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<std::mutex> 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<float> (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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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;
|
|
}
|
|
|
|
// T15: master fader — linear, ap sau limiter+clip trong mixer (mirror
|
|
// masterBus.output.gain WebAudio).
|
|
__declspec (dllexport) int32 SF_FS_SetMasterGain (int32 handle, float gainLinear)
|
|
{
|
|
std::lock_guard<std::mutex> lock (g_mutex);
|
|
auto it = g_instances.find (handle);
|
|
if (it == g_instances.end ())
|
|
return -1;
|
|
it->second->mixer.setMasterGain (gainLinear);
|
|
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<std::mutex> 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"
|