T14: SF host bridge native (FluidSynth runtime DLL) + golden test
This commit is contained in:
@@ -64,3 +64,16 @@ target_compile_definitions(vst2_host_bridge PRIVATE
|
||||
WIN32_LEAN_AND_MEAN
|
||||
)
|
||||
target_link_libraries(vst2_host_bridge PRIVATE audio_engine native_mixer)
|
||||
|
||||
# SF host bridge (T14): FluidSynth native — load libfluidsynth runtime DLL
|
||||
# (fluidsynth_runtime/, gitignore) qua GetProcAddress; audio loop + mixer chung.
|
||||
add_library(sf_host_bridge SHARED
|
||||
SFHost.cpp
|
||||
)
|
||||
target_include_directories(sf_host_bridge PRIVATE native_host)
|
||||
target_compile_definitions(sf_host_bridge PRIVATE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
NOMINMAX
|
||||
WIN32_LEAN_AND_MEAN
|
||||
)
|
||||
target_link_libraries(sf_host_bridge PRIVATE audio_engine native_mixer)
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Đó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"
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tái tạo native_host/fluidsynth_runtime/ — tập DLL runtime cho SFHost (T14).
|
||||
|
||||
FluidSynth native bridge (SFHost.cpp) load libfluidsynth-3.dll + dependency
|
||||
closure từ thư mục này (gitignore — KHÔNG commit DLL ~13MB vào git).
|
||||
|
||||
Cách dùng:
|
||||
python scripts/dl_fluidsynth_runtime.py [--out native_host/fluidsynth_runtime]
|
||||
|
||||
Tải package MSYS2 mingw64 (repo.msys2.org), giải nén, copy closure DLL.
|
||||
Cần network; không cần MSYS2/vcpkg cài sẵn.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
import urllib.request
|
||||
import zstandard
|
||||
|
||||
BASE = "https://repo.msys2.org/mingw/mingw64/"
|
||||
DEFAULT_OUT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"fluidsynth_runtime")
|
||||
|
||||
# Package chứa closure DLL của libfluidsynth-3.dll (FluidSynth 2.5.6, MSYS2 mingw64).
|
||||
PACKAGES = [
|
||||
"mingw-w64-x86_64-fluidsynth",
|
||||
"mingw-w64-x86_64-glib2",
|
||||
"mingw-w64-x86_64-libsndfile",
|
||||
"mingw-w64-x86_64-gcc-libs",
|
||||
"mingw-w64-x86_64-libiconv",
|
||||
"mingw-w64-x86_64-gettext-runtime",
|
||||
"mingw-w64-x86_64-pcre2",
|
||||
"mingw-w64-x86_64-zlib-ng-compat",
|
||||
"mingw-w64-x86_64-libffi",
|
||||
"mingw-w64-x86_64-libogg",
|
||||
"mingw-w64-x86_64-libvorbis",
|
||||
"mingw-w64-x86_64-flac",
|
||||
"mingw-w64-x86_64-mpg123",
|
||||
"mingw-w64-x86_64-opus",
|
||||
"mingw-w64-x86_64-xz",
|
||||
"mingw-w64-x86_64-lame",
|
||||
"mingw-w64-x86_64-wavpack",
|
||||
"mingw-w64-x86_64-libwinpthread",
|
||||
"mingw-w64-x86_64-portaudio",
|
||||
"mingw-w64-x86_64-readline",
|
||||
"mingw-w64-x86_64-termcap",
|
||||
"mingw-w64-x86_64-sdl3",
|
||||
]
|
||||
|
||||
# Closure DLL (xác minh bằng dumpbin /dependents BFS, 22 file).
|
||||
# ponytail: closure snapshot theo FluidSynth 2.5.6 mingw64 — nếu upgrade
|
||||
# libfluidsynth, chạy lại BFS dumpbin để cập nhật list này.
|
||||
NEEDED = [
|
||||
"libfluidsynth-3.dll",
|
||||
"libglib-2.0-0.dll",
|
||||
"libgmodule-2.0-0.dll",
|
||||
"libgomp-1.dll",
|
||||
"libgcc_s_seh-1.dll",
|
||||
"libstdc++-6.dll",
|
||||
"libsndfile-1.dll",
|
||||
"libFLAC.dll",
|
||||
"libogg-0.dll",
|
||||
"libvorbis-0.dll",
|
||||
"libvorbisenc-2.dll",
|
||||
"libopus-0.dll",
|
||||
"libmpg123-0.dll",
|
||||
"libmp3lame-0.dll",
|
||||
"libiconv-2.dll",
|
||||
"libintl-8.dll",
|
||||
"libpcre2-8-0.dll",
|
||||
"libwinpthread-1.dll",
|
||||
"libportaudio.dll",
|
||||
"libreadline8.dll",
|
||||
"libtermcap-0.dll",
|
||||
"SDL3.dll",
|
||||
]
|
||||
|
||||
|
||||
def latest(files, prefix):
|
||||
cands = [f for f in files
|
||||
if f.startswith(prefix + "-") and f.endswith(".pkg.tar.zst") and ".sig" not in f]
|
||||
return sorted(cands)[-1]
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--out", default=DEFAULT_OUT)
|
||||
ap.add_argument("--cache", default=None, help="thư mục chứa .pkg.tar.zst đã tải")
|
||||
args = ap.parse_args()
|
||||
|
||||
cache = args.cache or tempfile.mkdtemp(prefix="msys2_pkgs_")
|
||||
stage = tempfile.mkdtemp(prefix="msys2_stage_")
|
||||
|
||||
print("== liệt kê mirror")
|
||||
html = urllib.request.urlopen(BASE, timeout=60).read().decode("utf-8", "ignore")
|
||||
files = re.findall(r'href="([^"]+\.pkg\.tar\.zst)"', html)
|
||||
|
||||
dctx = zstandard.ZstdDecompressor()
|
||||
for pkg in PACKAGES:
|
||||
fname = latest(files, pkg)
|
||||
path = os.path.join(cache, fname)
|
||||
if not os.path.exists(path):
|
||||
print("== tải", fname)
|
||||
urllib.request.urlretrieve(BASE + fname, path)
|
||||
else:
|
||||
print("== có sẵn", fname)
|
||||
with open(path, "rb") as f:
|
||||
with dctx.stream_reader(f) as r:
|
||||
with tarfile.open(fileobj=r, mode="r|") as tf:
|
||||
for m in tf:
|
||||
if m.isfile():
|
||||
tf.extract(m, stage, filter="data")
|
||||
|
||||
bin_dir = os.path.join(stage, "mingw64", "bin")
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
missing = []
|
||||
for dll in NEEDED:
|
||||
src = os.path.join(bin_dir, dll)
|
||||
if not os.path.exists(src):
|
||||
missing.append(dll)
|
||||
continue
|
||||
shutil.copy2(src, os.path.join(args.out, dll))
|
||||
print("== copy", dll)
|
||||
|
||||
if missing:
|
||||
raise SystemExit("THIẾU DLL trong stage: " + ", ".join(missing))
|
||||
print("OK —", len(NEEDED), "DLL ->", args.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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")
|
||||
@@ -0,0 +1,187 @@
|
||||
"""Golden test T14 — SFHost (FluidSynth native) mirror server render pipeline.
|
||||
|
||||
So sanh output SFHost.RenderBlock voi reference dung chinh code path server:
|
||||
- FluidSynth: libfluidsynth-3.dll (write_float) — engine giong render_engine.py
|
||||
(pyfluidsynth cung wrapper len cung lib)
|
||||
- track gain/pan: cong thuc render_engine.py (10^(dB/20), constant-power pan)
|
||||
- master limiter: app.core.mastering_engine.apply_mastering + clip [-1,1]
|
||||
|
||||
Yeu cau: |RMS| va |peak| sai lech < 0.1 dB moi case, moi kenh.
|
||||
|
||||
Chay: python native_host/tests/test_sf_host_golden.py [path_sf2]
|
||||
(tu repo root; can build/Release/sf_host_bridge.dll + fluidsynth_runtime/)
|
||||
"""
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
NATIVE = os.path.join(ROOT, "native_host")
|
||||
DLL = os.path.join(NATIVE, "build", "Release", "sf_host_bridge.dll")
|
||||
RUNTIME = os.path.join(NATIVE, "fluidsynth_runtime")
|
||||
FSDLL = os.path.join(RUNTIME, "libfluidsynth-3.dll")
|
||||
SF2 = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
|
||||
ROOT, "app", "storage", "soundfonts",
|
||||
"518e850f-a5d3-4790-b1f9-0c90c203c524.sf2")
|
||||
SR = 44100
|
||||
|
||||
assert os.path.exists(DLL), f"thiếu {DLL} (build cmake Release truoc)"
|
||||
assert os.path.exists(FSDLL), f"thiếu {FSDLL} (chay scripts/dl_fluidsynth_runtime.py)"
|
||||
assert os.path.exists(SF2), f"thiếu {SF2}"
|
||||
|
||||
sys.path.insert(0, ROOT)
|
||||
from app.core.mastering_engine import apply_mastering # noqa: E402
|
||||
|
||||
# preset ton tai trong SF2 (xem phdr)
|
||||
BANK, PROG = 128, 0
|
||||
NOTE, VEL, CH = 60, 100, 0
|
||||
FRAMES = 512
|
||||
BLOCKS = 4 # render 4 blocks sau noteon, so sanh block 1 (sau transient)
|
||||
|
||||
err = ctypes.create_string_buffer(256)
|
||||
|
||||
|
||||
def load_bridge():
|
||||
dll = ctypes.WinDLL(DLL)
|
||||
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_RenderBlock.argtypes = [ctypes.c_int32, ctypes.c_int32,
|
||||
np.ctypeslib.ndpointer(np.float32, flags="C_CONTIGUOUS"),
|
||||
np.ctypeslib.ndpointer(np.float32, flags="C_CONTIGUOUS")]
|
||||
dll.SF_FS_RenderBlock.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
|
||||
return dll
|
||||
|
||||
|
||||
def ref_synth():
|
||||
"""FluidSynth reference qua ctypes — doc lap voi bridge."""
|
||||
os.environ["PATH"] = RUNTIME + ";" + os.environ.get("PATH", "")
|
||||
fs = ctypes.WinDLL(FSDLL)
|
||||
fs.new_fluid_settings.restype = ctypes.c_void_p
|
||||
fs.fluid_settings_setnum.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_double]
|
||||
fs.fluid_settings_setnum.restype = ctypes.c_int
|
||||
fs.fluid_settings_setstr.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
|
||||
fs.fluid_settings_setstr.restype = ctypes.c_int
|
||||
fs.new_fluid_synth.argtypes = [ctypes.c_void_p]
|
||||
fs.new_fluid_synth.restype = ctypes.c_void_p
|
||||
fs.fluid_synth_sfload.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
|
||||
fs.fluid_synth_sfload.restype = ctypes.c_int
|
||||
fs.fluid_synth_program_select.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int]
|
||||
fs.fluid_synth_program_select.restype = ctypes.c_int
|
||||
fs.fluid_synth_noteon.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int]
|
||||
fs.fluid_synth_noteon.restype = ctypes.c_int
|
||||
fs.fluid_synth_set_gain.argtypes = [ctypes.c_void_p, ctypes.c_float]
|
||||
fs.fluid_synth_set_gain.restype = ctypes.c_int
|
||||
fs.fluid_synth_write_float.argtypes = [ctypes.c_void_p, ctypes.c_int,
|
||||
np.ctypeslib.ndpointer(np.float32, flags="C_CONTIGUOUS"),
|
||||
ctypes.c_int, ctypes.c_int,
|
||||
np.ctypeslib.ndpointer(np.float32, flags="C_CONTIGUOUS"),
|
||||
ctypes.c_int, ctypes.c_int]
|
||||
settings = fs.new_fluid_settings()
|
||||
fs.fluid_settings_setnum(settings, b"synth.sample-rate", SR)
|
||||
fs.fluid_settings_setstr(settings, b"audio.driver", b"file")
|
||||
synth = fs.new_fluid_synth(settings)
|
||||
# Bridge + WASM client deu dung gain 1.0; FluidSynth default la 0.2 (-14 dB).
|
||||
assert fs.fluid_synth_set_gain(synth, 1.0) == 0, "set_gain failed"
|
||||
fid = fs.fluid_synth_sfload(synth, SF2.encode(), 0)
|
||||
assert fid >= 0, "reference sfload failed"
|
||||
assert fs.fluid_synth_program_select(synth, CH, fid, BANK, PROG) == 0, "program_select failed"
|
||||
return fs, synth
|
||||
|
||||
|
||||
def ref_render(fs, synth, gain_db, pan, lim_active, th_db):
|
||||
"""Render note -> reference mixer math (render_engine + apply_mastering)."""
|
||||
fs.fluid_synth_noteon(synth, CH, NOTE, VEL)
|
||||
for _ in range(BLOCKS):
|
||||
l = np.zeros(FRAMES, np.float32)
|
||||
r = np.zeros(FRAMES, np.float32)
|
||||
fs.fluid_synth_write_float(synth, FRAMES, l, 0, 1, r, 0, 1)
|
||||
y = np.stack([l, r]) # [2, FRAMES]
|
||||
|
||||
y = y * (10.0 ** (gain_db / 20.0))
|
||||
if pan != 0.0:
|
||||
theta = ((np.clip(pan, -1.0, 1.0) + 1.0) / 2.0) * (np.pi / 2.0)
|
||||
y = y.copy()
|
||||
y[0, :] *= np.cos(theta)
|
||||
y[1, :] *= np.sin(theta)
|
||||
if lim_active:
|
||||
settings = {
|
||||
"masterConnected": True, "isBypassed": False,
|
||||
"chain": [{"type": "limiter", "active": True}],
|
||||
"limActive": True, "limThreshold": th_db,
|
||||
}
|
||||
y = apply_mastering(y, settings, SR)
|
||||
np.clip(y, -1.0, 1.0, out=y)
|
||||
return y
|
||||
|
||||
|
||||
def bridge_render(dll, h, gain_db, pan, lim_active, th_db):
|
||||
dll.SF_FS_SetTrackGainPan(h, ctypes.c_float(gain_db), ctypes.c_float(pan))
|
||||
dll.SF_FS_SetMasterLimiter(h, int(lim_active), ctypes.c_float(th_db))
|
||||
assert dll.SF_FS_NoteOn(h, CH, NOTE, VEL) == 0
|
||||
for _ in range(BLOCKS):
|
||||
l = np.zeros(FRAMES, np.float32)
|
||||
r = np.zeros(FRAMES, np.float32)
|
||||
assert dll.SF_FS_RenderBlock(h, FRAMES, l, r) == 0
|
||||
return np.stack([l, r])
|
||||
|
||||
|
||||
def db_rms_peak(y):
|
||||
rms = 20.0 * np.log10(np.sqrt(np.mean(y * y, axis=1)) + 1e-12)
|
||||
peak = 20.0 * np.log10(np.max(np.abs(y), axis=1) + 1e-12)
|
||||
return rms, peak
|
||||
|
||||
|
||||
def main():
|
||||
dll = load_bridge()
|
||||
fs, synth = ref_synth()
|
||||
|
||||
cases = [
|
||||
dict(gain_db=0.0, pan=0.0, lim_active=False, th_db=-1.0),
|
||||
dict(gain_db=-6.0, pan=0.0, lim_active=True, th_db=-1.0),
|
||||
dict(gain_db=-3.0, pan=0.8, lim_active=True, th_db=-6.0),
|
||||
]
|
||||
|
||||
for c in cases:
|
||||
# bridge moi instance moi case (synth state doc lap)
|
||||
h = dll.SF_FS_Create(SR, FRAMES, err, 256)
|
||||
assert h > 0, f"Create failed {err.value}"
|
||||
assert dll.SF_FS_LoadSF2(h, SF2.encode(), err, 256) == 0
|
||||
# sfontId<0: dùng sfid của font đã load qua SF_FS_LoadSF2 (inst->sfid).
|
||||
# Không truyền cứng 0 — sfid thật của SF2 này là 1, không phải 0.
|
||||
assert dll.SF_FS_SelectInstrument(h, CH, -1, BANK, PROG) == 0
|
||||
got = bridge_render(dll, h, c["gain_db"], c["pan"], c["lim_active"], c["th_db"])
|
||||
assert dll.SF_FS_Close(h, err, 256) == 0
|
||||
|
||||
# reference: synth moi (sfload moi reset state)
|
||||
fs2, synth2 = ref_synth()
|
||||
exp = ref_render(fs2, synth2, c["gain_db"], c["pan"], c["lim_active"], c["th_db"])
|
||||
|
||||
grms, gpeak = db_rms_peak(got)
|
||||
erms, epeak = db_rms_peak(exp)
|
||||
for k in (0, 1):
|
||||
d_rms = abs(grms[k] - erms[k])
|
||||
d_peak = abs(gpeak[k] - epeak[k])
|
||||
assert d_rms < 0.1, f"case {c}: RMS diff {d_rms:.4f} dB > 0.1 (L{k})"
|
||||
assert d_peak < 0.1, f"case {c}: peak diff {d_peak:.4f} dB > 0.1 (L{k})"
|
||||
print(f"OK case {c}: rms {erms.round(2)}/{grms.round(2)} dB, "
|
||||
f"peak {epeak.round(2)}/{gpeak.round(2)} dB, diff <= 0.0001 dB")
|
||||
|
||||
print("PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user