T11: IPC param sync 2 chieu native<->JS (setParamNormalized/setParameter, vst_param_changed event, guard chong loop)
This commit is contained in:
@@ -37,3 +37,9 @@ src-tauri/vc_redist.x64.exe
|
|||||||
app/storage/plugin_dirs.json
|
app/storage/plugin_dirs.json
|
||||||
app/storage/sf_scan_state.json.bak-root
|
app/storage/sf_scan_state.json.bak-root
|
||||||
app/storage/soundfonts/
|
app/storage/soundfonts/
|
||||||
|
|
||||||
|
# Native host build artifacts
|
||||||
|
native_host/tests/*.dll
|
||||||
|
native_host/tests/*.exp
|
||||||
|
native_host/tests/*.lib
|
||||||
|
native_host/tests/*.obj
|
||||||
|
|||||||
@@ -26,6 +26,10 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// Callback param (T11): plugin đổi param (audioMasterAutomate) → host.
|
||||||
|
typedef void (VSTCALLBACK* SF_ParamChangedCallback)(int32 handle, int32 paramIndex,
|
||||||
|
double valueNormalized, void* userdata);
|
||||||
|
|
||||||
struct Instance
|
struct Instance
|
||||||
{
|
{
|
||||||
HMODULE module = nullptr;
|
HMODULE module = nullptr;
|
||||||
@@ -38,9 +42,14 @@ struct Instance
|
|||||||
ERect* editorRect = nullptr;
|
ERect* editorRect = nullptr;
|
||||||
bool editorOpen = false;
|
bool editorOpen = false;
|
||||||
bool active = false;
|
bool active = false;
|
||||||
|
int32 handle = 0;
|
||||||
|
SF_ParamChangedCallback paramCb = nullptr;
|
||||||
|
void* paramCbUserdata = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::mutex g_mutex;
|
std::mutex g_mutex;
|
||||||
|
std::mutex g_effect_map_mutex; // riêng: audioMasterAutomate gọi từ audio thread
|
||||||
|
std::map<AEffect*, Instance*> g_effect_map;
|
||||||
std::map<int32, std::unique_ptr<Instance>> g_instances;
|
std::map<int32, std::unique_ptr<Instance>> g_instances;
|
||||||
int32 g_next_handle = 1;
|
int32 g_next_handle = 1;
|
||||||
|
|
||||||
@@ -59,10 +68,9 @@ thread_local Instance* tls_loading = nullptr;
|
|||||||
VstIntPtr VSTCALLBACK hostAudioMasterImpl (AEffect* effect, int32 opcode, int32 index,
|
VstIntPtr VSTCALLBACK hostAudioMasterImpl (AEffect* effect, int32 opcode, int32 index,
|
||||||
VstIntPtr value, void* ptr, float opt)
|
VstIntPtr value, void* ptr, float opt)
|
||||||
{
|
{
|
||||||
(void) effect;
|
|
||||||
(void) index;
|
(void) index;
|
||||||
(void) value;
|
(void) value;
|
||||||
(void) opt;
|
(void) ptr;
|
||||||
Instance* inst = tls_loading;
|
Instance* inst = tls_loading;
|
||||||
switch (opcode)
|
switch (opcode)
|
||||||
{
|
{
|
||||||
@@ -72,6 +80,22 @@ VstIntPtr VSTCALLBACK hostAudioMasterImpl (AEffect* effect, int32 opcode, int32
|
|||||||
return inst ? inst->sampleRate : 44100;
|
return inst ? inst->sampleRate : 44100;
|
||||||
case audioMasterGetBlockSize:
|
case audioMasterGetBlockSize:
|
||||||
return inst ? inst->blockSize : 512;
|
return inst ? inst->blockSize : 512;
|
||||||
|
case audioMasterAutomate:
|
||||||
|
{
|
||||||
|
// Plugin đổi param (user kéo knob trong editor) → callback lên host.
|
||||||
|
// Effect → instance qua map riêng (audio thread, không giữ g_mutex).
|
||||||
|
Instance* owner = nullptr;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_effect_map_mutex);
|
||||||
|
auto it = g_effect_map.find (effect);
|
||||||
|
if (it != g_effect_map.end ())
|
||||||
|
owner = it->second;
|
||||||
|
}
|
||||||
|
if (owner && owner->paramCb)
|
||||||
|
owner->paramCb (owner->handle, index, static_cast<double> (opt),
|
||||||
|
owner->paramCbUserdata);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
case audioMasterCurrentId:
|
case audioMasterCurrentId:
|
||||||
case audioMasterGetLanguage:
|
case audioMasterGetLanguage:
|
||||||
return 0;
|
return 0;
|
||||||
@@ -200,6 +224,11 @@ __declspec (dllexport) int32 SF_VST2_Load (const char* module_path_utf8,
|
|||||||
}
|
}
|
||||||
|
|
||||||
int32 handle = g_next_handle++;
|
int32 handle = g_next_handle++;
|
||||||
|
inst->handle = handle;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_effect_map_mutex);
|
||||||
|
g_effect_map[inst->effect] = inst.get ();
|
||||||
|
}
|
||||||
g_instances[handle] = std::move (inst);
|
g_instances[handle] = std::move (inst);
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
@@ -217,6 +246,10 @@ __declspec (dllexport) int32 SF_VST2_Close (int32 handle, char* err, int32 err_c
|
|||||||
Instance* inst = it->second.get ();
|
Instance* inst = it->second.get ();
|
||||||
if (inst->effect)
|
if (inst->effect)
|
||||||
{
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_effect_map_mutex);
|
||||||
|
g_effect_map.erase (inst->effect);
|
||||||
|
}
|
||||||
if (inst->active)
|
if (inst->active)
|
||||||
{
|
{
|
||||||
inst->effect->dispatcher (inst->effect, effStopProcess, 0, 0, nullptr, 0.0f);
|
inst->effect->dispatcher (inst->effect, effStopProcess, 0, 0, nullptr, 0.0f);
|
||||||
@@ -372,4 +405,63 @@ __declspec (dllexport) int32 SF_VST2_Process (int32 handle, float* buffers, int3
|
|||||||
return -4;
|
return -4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Đăng ký callback param (T11): plugin đổi param (audioMasterAutomate) →
|
||||||
|
// cb(handle, paramIndex, valueNormalized, userdata).
|
||||||
|
__declspec (dllexport) int32 SF_VST2_SetParamCallback (int32 handle,
|
||||||
|
SF_ParamChangedCallback cb,
|
||||||
|
void* userdata)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_mutex);
|
||||||
|
auto it = g_instances.find (handle);
|
||||||
|
if (it == g_instances.end ())
|
||||||
|
return -1;
|
||||||
|
it->second->paramCb = cb;
|
||||||
|
it->second->paramCbUserdata = userdata;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JS automation → setParameter (T11).
|
||||||
|
__declspec (dllexport) int32 SF_VST2_SetParam (int32 handle, int32 paramIndex,
|
||||||
|
double valueNormalized)
|
||||||
|
{
|
||||||
|
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->effect || !inst->effect->setParameter)
|
||||||
|
return -2;
|
||||||
|
inst->effect->setParameter (inst->effect, paramIndex,
|
||||||
|
static_cast<float> (valueNormalized));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Số tham số plugin.
|
||||||
|
__declspec (dllexport) int32 SF_VST2_GetParamCount (int32 handle)
|
||||||
|
{
|
||||||
|
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->effect || !inst->effect->numParams)
|
||||||
|
return -2;
|
||||||
|
return inst->effect->numParams (inst->effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Giá trị param (0..1) — JS đọc để dựng UI.
|
||||||
|
__declspec (dllexport) int32 SF_VST2_GetParam (int32 handle, int32 paramIndex, double* out_value)
|
||||||
|
{
|
||||||
|
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->effect || !inst->effect->getParameter)
|
||||||
|
return -2;
|
||||||
|
if (out_value)
|
||||||
|
*out_value = static_cast<double> (inst->effect->getParameter (inst->effect, paramIndex));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
@@ -13,9 +13,33 @@ struct FakeState
|
|||||||
{
|
{
|
||||||
float phase = 0.0f;
|
float phase = 0.0f;
|
||||||
int32 sampleRate = 44100;
|
int32 sampleRate = 44100;
|
||||||
|
float gain = 0.5f; // param 0 — "Gain" (0..1)
|
||||||
};
|
};
|
||||||
|
|
||||||
FakeState g_state;
|
FakeState g_state;
|
||||||
|
void* g_audioMaster = nullptr;
|
||||||
|
|
||||||
|
void VSTCALLBACK fakeSetParameter (AEffect* effect, int32 index, float value)
|
||||||
|
{
|
||||||
|
if (index == 0)
|
||||||
|
{
|
||||||
|
g_state.gain = value;
|
||||||
|
// Báo host: param đổi từ editor/plugin (T11) — chống loop host-side.
|
||||||
|
if (g_audioMaster)
|
||||||
|
{
|
||||||
|
typedef VstIntPtr (VSTCALLBACK* AMFn) (AEffect*, int32, int32, VstIntPtr, void*, float);
|
||||||
|
reinterpret_cast<AMFn> (g_audioMaster) (effect, audioMasterAutomate, index, 0, nullptr,
|
||||||
|
value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float VSTCALLBACK fakeGetParameter (AEffect*, int32 index)
|
||||||
|
{
|
||||||
|
if (index == 0)
|
||||||
|
return g_state.gain;
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
int32 VSTCALLBACK fakeDispatcher (AEffect* effect, int32 opcode, int32 index,
|
int32 VSTCALLBACK fakeDispatcher (AEffect* effect, int32 opcode, int32 index,
|
||||||
VstIntPtr value, void* ptr, float opt)
|
VstIntPtr value, void* ptr, float opt)
|
||||||
@@ -91,15 +115,18 @@ void VSTCALLBACK fakeProcessReplacing (AEffect*, float** inputs, float** outputs
|
|||||||
|
|
||||||
AEffect g_effect;
|
AEffect g_effect;
|
||||||
|
|
||||||
AEffect* VSTCALLBACK createInstance (void*)
|
AEffect* VSTCALLBACK createInstance (void* audioMaster)
|
||||||
{
|
{
|
||||||
|
g_audioMaster = audioMaster;
|
||||||
std::memset (&g_effect, 0, sizeof (g_effect));
|
std::memset (&g_effect, 0, sizeof (g_effect));
|
||||||
g_effect.magic = CCONST ('V', 's', 't', 'P');
|
g_effect.magic = CCONST ('V', 's', 't', 'P');
|
||||||
g_effect.dispatcher = &fakeDispatcher;
|
g_effect.dispatcher = &fakeDispatcher;
|
||||||
g_effect.processReplacing = &fakeProcessReplacing;
|
g_effect.processReplacing = &fakeProcessReplacing;
|
||||||
|
g_effect.setParameter = &fakeSetParameter;
|
||||||
|
g_effect.getParameter = &fakeGetParameter;
|
||||||
g_effect.numInputs = [] (AEffect*) -> int32 { return 0; };
|
g_effect.numInputs = [] (AEffect*) -> int32 { return 0; };
|
||||||
g_effect.numOutputs = [] (AEffect*) -> int32 { return 2; };
|
g_effect.numOutputs = [] (AEffect*) -> int32 { return 2; };
|
||||||
g_effect.numParams = [] (AEffect*) -> int32 { return 0; };
|
g_effect.numParams = [] (AEffect*) -> int32 { return 1; };
|
||||||
g_effect.numPrograms = [] (AEffect*) -> int32 { return 0; };
|
g_effect.numPrograms = [] (AEffect*) -> int32 { return 0; };
|
||||||
g_effect.flags = [] (AEffect*) -> int32 { return effFlagsCanReplacing | effFlagsIsSynth; };
|
g_effect.flags = [] (AEffect*) -> int32 { return effFlagsCanReplacing | effFlagsIsSynth; };
|
||||||
g_effect.uniqueID = CCONST ('F', 'k', '2', 'V');
|
g_effect.uniqueID = CCONST ('F', 'k', '2', 'V');
|
||||||
@@ -111,6 +138,5 @@ AEffect* VSTCALLBACK createInstance (void*)
|
|||||||
|
|
||||||
extern "C" __declspec (dllexport) AEffect* VSTPluginMain (void* audioMaster)
|
extern "C" __declspec (dllexport) AEffect* VSTPluginMain (void* audioMaster)
|
||||||
{
|
{
|
||||||
(void) audioMaster;
|
|
||||||
return createInstance (audioMaster);
|
return createInstance (audioMaster);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
#include "public.sdk/source/vst/hosting/module.h"
|
#include "public.sdk/source/vst/hosting/module.h"
|
||||||
#include "public.sdk/source/vst/hosting/plugprovider.h"
|
#include "public.sdk/source/vst/hosting/plugprovider.h"
|
||||||
|
#include "public.sdk/source/common/commonstringconvert.h"
|
||||||
#include "pluginterfaces/base/funknown.h"
|
#include "pluginterfaces/base/funknown.h"
|
||||||
#include "pluginterfaces/gui/iplugview.h"
|
#include "pluginterfaces/gui/iplugview.h"
|
||||||
#include "pluginterfaces/vst/ivstaudioprocessor.h"
|
#include "pluginterfaces/vst/ivstaudioprocessor.h"
|
||||||
@@ -35,7 +36,12 @@ using namespace Steinberg::Vst;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// ComponentHandler tối thiểu — T11: performEdit → JS param sync.
|
// Callback param: plugin đổi param trong editor → đẩy lên host (T11).
|
||||||
|
// handle = instance handle; paramId = ParamID; valueNormalized 0..1.
|
||||||
|
typedef void (__cdecl* SF_ParamChangedCallback)(int32 handle, int32 paramId,
|
||||||
|
double valueNormalized, void* userdata);
|
||||||
|
|
||||||
|
// ComponentHandler tối thiểu — T11: performEdit → callback param sync.
|
||||||
class ComponentHandler : public IComponentHandler
|
class ComponentHandler : public IComponentHandler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -55,12 +61,26 @@ public:
|
|||||||
uint32 PLUGIN_API addRef () override { return 1; }
|
uint32 PLUGIN_API addRef () override { return 1; }
|
||||||
uint32 PLUGIN_API release () override { return 1; }
|
uint32 PLUGIN_API release () override { return 1; }
|
||||||
tresult PLUGIN_API beginEdit (ParamID /*id*/) override { return kResultOk; }
|
tresult PLUGIN_API beginEdit (ParamID /*id*/) override { return kResultOk; }
|
||||||
tresult PLUGIN_API performEdit (ParamID /*id*/, ParamValue /*valueNormalized*/) override
|
tresult PLUGIN_API performEdit (ParamID id, ParamValue valueNormalized) override
|
||||||
{
|
{
|
||||||
|
if (cb)
|
||||||
|
cb (handle, static_cast<int32> (id), static_cast<double> (valueNormalized), userdata);
|
||||||
return kResultOk;
|
return kResultOk;
|
||||||
}
|
}
|
||||||
tresult PLUGIN_API endEdit (ParamID /*id*/) override { return kResultOk; }
|
tresult PLUGIN_API endEdit (ParamID /*id*/) override { return kResultOk; }
|
||||||
tresult PLUGIN_API restartComponent (int32 /*flags*/) override { return kResultOk; }
|
tresult PLUGIN_API restartComponent (int32 /*flags*/) override { return kResultOk; }
|
||||||
|
|
||||||
|
void setCallback (int32 h, SF_ParamChangedCallback c, void* u)
|
||||||
|
{
|
||||||
|
handle = h;
|
||||||
|
cb = c;
|
||||||
|
userdata = u;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
int32 handle = 0;
|
||||||
|
SF_ParamChangedCallback cb = nullptr;
|
||||||
|
void* userdata = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Instance
|
struct Instance
|
||||||
@@ -233,4 +253,75 @@ __declspec (dllexport) int32 SF_VST3_Resize (int32 handle, int32 w, int32 h)
|
|||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Đăng ký callback param (T11): plugin đổi param trong editor → cb(handle,
|
||||||
|
// paramId, valueNormalized, userdata). userdata là con trỏ do host giữ.
|
||||||
|
__declspec (dllexport) int32 SF_VST3_SetParamCallback (int32 handle,
|
||||||
|
SF_ParamChangedCallback cb,
|
||||||
|
void* userdata)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_mutex);
|
||||||
|
auto it = g_instances.find (handle);
|
||||||
|
if (it == g_instances.end ())
|
||||||
|
return -1;
|
||||||
|
it->second->handler.setCallback (handle, cb, userdata);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JS automation → setParamNormalized (T11).
|
||||||
|
__declspec (dllexport) int32 SF_VST3_SetParam (int32 handle, int32 paramId, double valueNormalized)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_mutex);
|
||||||
|
auto it = g_instances.find (handle);
|
||||||
|
if (it == g_instances.end ())
|
||||||
|
return -1;
|
||||||
|
if (!it->second->controller)
|
||||||
|
return -2;
|
||||||
|
it->second->controller->setParamNormalized (static_cast<ParamID> (paramId),
|
||||||
|
static_cast<ParamValue> (valueNormalized));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Số tham số plugin (để JS dựng UI param list).
|
||||||
|
__declspec (dllexport) int32 SF_VST3_GetParamCount (int32 handle)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_mutex);
|
||||||
|
auto it = g_instances.find (handle);
|
||||||
|
if (it == g_instances.end ())
|
||||||
|
return -1;
|
||||||
|
if (!it->second->controller)
|
||||||
|
return -2;
|
||||||
|
return static_cast<int32> (it->second->controller->getParameterCount ());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thông tin param thứ index (0-based): id + tên (title) + giá trị normalized.
|
||||||
|
// Trả 0 nếu OK; -1 bad handle; -2 no controller; -3 index ngoài phạm vi.
|
||||||
|
__declspec (dllexport) int32 SF_VST3_GetParamInfo (int32 handle, int32 index,
|
||||||
|
int32* out_id, char* out_title,
|
||||||
|
int32 title_cap, double* out_value)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_mutex);
|
||||||
|
auto it = g_instances.find (handle);
|
||||||
|
if (it == g_instances.end ())
|
||||||
|
return -1;
|
||||||
|
IEditController* ctrl = it->second->controller;
|
||||||
|
if (!ctrl)
|
||||||
|
return -2;
|
||||||
|
if (index < 0 || static_cast<uint32> (index) >= ctrl->getParameterCount ())
|
||||||
|
return -3;
|
||||||
|
ParameterInfo info = {};
|
||||||
|
if (ctrl->getParameterInfo (static_cast<int32> (index), info) != kResultTrue)
|
||||||
|
return -3;
|
||||||
|
if (out_id)
|
||||||
|
*out_id = static_cast<int32> (info.id);
|
||||||
|
if (out_title && title_cap > 0)
|
||||||
|
{
|
||||||
|
std::string titleUtf8 = StringConvert::convert (std::u16string (info.title));
|
||||||
|
std::strncpy (out_title, titleUtf8.c_str (), static_cast<size_t> (title_cap - 1));
|
||||||
|
out_title[title_cap - 1] = '\0';
|
||||||
|
}
|
||||||
|
if (out_value)
|
||||||
|
*out_value = static_cast<double> (ctrl->getParamNormalized (info.id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
@@ -75,8 +75,9 @@ pub fn run() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.invoke_handler(tauri::generate_handler![vst_gui::open_vst_gui])
|
.invoke_handler(tauri::generate_handler![vst_gui::open_vst_gui, vst_gui::set_vst_param, vst_gui::get_vst_params, vst_gui::close_vst_editor])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
vst_gui::init(app.handle());
|
||||||
let res_dir = app
|
let res_dir = app
|
||||||
.path()
|
.path()
|
||||||
.resource_dir()
|
.resource_dir()
|
||||||
|
|||||||
+332
-20
@@ -1,44 +1,356 @@
|
|||||||
// VST GUI windows (spec vsti_gui): floating child window per (track, plugin).
|
// VST GUI windows (spec vsti_gui): floating child window per (track, plugin).
|
||||||
//
|
//
|
||||||
// T8: command `open_vst_gui(plugin_id, track_id)` tao WebviewWindow noi
|
// T8: command `open_vst_gui(plugin_id, track_id)` tao WebviewWindow noi label
|
||||||
// label `vst_gui_{track}_{plugin}`, 800x600, always-on-top; mo lai thi focus.
|
// `vst_gui_{track}_{plugin}`, 800x600, always-on-top; mo lai thi focus.
|
||||||
// raw_window_handle() lay HWND (Windows) — T9 se attach native VST3/VST2
|
// raw_window_handle() lay HWND (Windows).
|
||||||
// editor vao HWND cua window nay. Chua load plugin GUI (T9), chi placeholder.
|
// T9/T10: native_host/*.dll attach VST3/VST2 editor vao HWND cua window.
|
||||||
|
// T11: param sync 2 chieu — native param doi -> event `vst_param_changed` ->
|
||||||
|
// JS; JS automation -> `set_vst_param` -> setParamNormalized/setParameter.
|
||||||
|
// Guard chong loop nam o JS side (bo qua event echo cua chinh minh).
|
||||||
use raw_window_handle::HasWindowHandle;
|
use raw_window_handle::HasWindowHandle;
|
||||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::{c_char, c_void, CString};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{LazyLock, Mutex};
|
||||||
|
use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||||
|
|
||||||
/// Mo (hoac focus neu da mo) cua so VST GUI cho 1 (track, plugin).
|
#[cfg(target_os = "windows")]
|
||||||
/// Tra ve chuoi trang thai cho JS log — khong throw.
|
mod native {
|
||||||
|
use super::*;
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
|
||||||
|
// Callback signature trung voi SF_ParamChangedCallback trong C++ bridge.
|
||||||
|
pub type ParamCb = unsafe extern "C" fn(i32, i32, f64, *mut c_void);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct Bridge {
|
||||||
|
pub module: *mut c_void,
|
||||||
|
pub attach: Option<unsafe extern "C" fn(*const c_char, *const c_char, *mut c_void, *mut i32, *mut i32, *mut c_char, i32) -> i32>,
|
||||||
|
pub load: Option<unsafe extern "C" fn(*const c_char, *mut c_void, *mut i32, *mut i32, *mut c_char, i32) -> i32>,
|
||||||
|
pub close: Option<unsafe extern "C" fn(i32, *mut c_char, i32) -> i32>,
|
||||||
|
pub set_param_cb: Option<unsafe extern "C" fn(i32, ParamCb, *mut c_void) -> i32>,
|
||||||
|
pub set_param: Option<unsafe extern "C" fn(i32, i32, f64) -> i32>,
|
||||||
|
pub get_param_count: Option<unsafe extern "C" fn(i32) -> i32>,
|
||||||
|
pub get_param: Option<unsafe extern "C" fn(i32, i32, *mut f64) -> i32>,
|
||||||
|
pub get_param_info: Option<unsafe extern "C" fn(i32, i32, *mut i32, *mut c_char, i32, *mut f64) -> i32>,
|
||||||
|
}
|
||||||
|
// Raw pointer `module` khong Send; bridge chi dung tu main thread, an toan.
|
||||||
|
unsafe impl Send for Bridge {}
|
||||||
|
|
||||||
|
static BRIDGES: LazyLock<Mutex<HashMap<String, Bridge>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
|
pub static APP: Mutex<Option<AppHandle>> = Mutex::new(None);
|
||||||
|
// handle native -> (track_id, plugin_id, kind)
|
||||||
|
pub static HANDLE_MAP: LazyLock<Mutex<HashMap<i32, (String, String, String)>>> =
|
||||||
|
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||||
|
// "{track}|{plugin}" -> handle native
|
||||||
|
pub static EDITOR_MAP: LazyLock<Mutex<HashMap<String, i32>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
|
unsafe fn load_bridge(kind: &str, app: &AppHandle) -> Option<Bridge> {
|
||||||
|
let name = if kind == "vst2" { "vst2_host_bridge.dll" } else { "vst3_host_bridge.dll" };
|
||||||
|
let path = find_bridge_path(name, app)?;
|
||||||
|
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||||
|
let module = unsafe { LoadLibraryW(wide.as_ptr()) };
|
||||||
|
if module.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let get = |sym: &str| -> Option<*mut c_void> {
|
||||||
|
let mut buf: Vec<u8> = sym.bytes().collect();
|
||||||
|
buf.push(0);
|
||||||
|
let p = unsafe { GetProcAddress(module, buf.as_ptr()) };
|
||||||
|
if p.is_null() { None } else { Some(p as *mut c_void) }
|
||||||
|
};
|
||||||
|
let mut bridge = Bridge {
|
||||||
|
module,
|
||||||
|
attach: None, load: None, close: None, set_param_cb: None,
|
||||||
|
set_param: None, get_param_count: None, get_param: None, get_param_info: None,
|
||||||
|
};
|
||||||
|
if kind == "vst2" {
|
||||||
|
bridge.load = get("SF_VST2_Load").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
bridge.close = get("SF_VST2_Close").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
bridge.set_param_cb = get("SF_VST2_SetParamCallback").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
bridge.set_param = get("SF_VST2_SetParam").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
bridge.get_param_count = get("SF_VST2_GetParamCount").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
bridge.get_param = get("SF_VST2_GetParam").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
} else {
|
||||||
|
bridge.attach = get("SF_VST3_Attach").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
bridge.close = get("SF_VST3_Close").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
bridge.set_param_cb = get("SF_VST3_SetParamCallback").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
bridge.set_param = get("SF_VST3_SetParam").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
bridge.get_param_count = get("SF_VST3_GetParamCount").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
bridge.get_param_info = get("SF_VST3_GetParamInfo").map(|p| unsafe { std::mem::transmute(p) });
|
||||||
|
}
|
||||||
|
Some(bridge)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_bridge_path(name: &str, app: &AppHandle) -> Option<PathBuf> {
|
||||||
|
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||||
|
if let Ok(dir) = std::env::var("SF_NATIVE_HOST_DIR") {
|
||||||
|
candidates.push(Path::new(&dir).join(name));
|
||||||
|
}
|
||||||
|
if let Ok(res) = app.path().resource_dir() {
|
||||||
|
candidates.push(res.join(name));
|
||||||
|
candidates.push(res.join("native").join(name));
|
||||||
|
}
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(dir) = exe.parent() {
|
||||||
|
candidates.push(dir.join(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(cwd) = std::env::current_dir() {
|
||||||
|
candidates.push(cwd.join(name));
|
||||||
|
candidates.push(cwd.join("native_host").join("build").join("Release").join(name));
|
||||||
|
}
|
||||||
|
candidates.into_iter().find(|p| p.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback native -> Rust: plugin doi param trong editor.
|
||||||
|
pub unsafe extern "C" fn on_param_changed(handle: i32, param_id: i32, value: f64, _userdata: *mut c_void) {
|
||||||
|
let ids = HANDLE_MAP.lock().unwrap().get(&handle).cloned();
|
||||||
|
let app = APP.lock().unwrap().clone();
|
||||||
|
if let (Some((track, plugin, _kind)), Some(app)) = (ids, app) {
|
||||||
|
let _ = app.emit_to(
|
||||||
|
&format!("vst_gui_{}_{}", track, plugin),
|
||||||
|
"vst_param_changed",
|
||||||
|
serde_json::json!({
|
||||||
|
"track_id": track,
|
||||||
|
"plugin_id": plugin,
|
||||||
|
"param_id": param_id,
|
||||||
|
"value": value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bridge(kind: &str, app: &AppHandle) -> Option<Bridge> {
|
||||||
|
let mut bridges = BRIDGES.lock().unwrap();
|
||||||
|
if let Some(b) = bridges.get(kind) {
|
||||||
|
// Clone fn pointers (module kept alive for app lifetime)
|
||||||
|
return Some(Bridge {
|
||||||
|
module: b.module,
|
||||||
|
attach: b.attach, load: b.load, close: b.close, set_param_cb: b.set_param_cb,
|
||||||
|
set_param: b.set_param, get_param_count: b.get_param_count,
|
||||||
|
get_param: b.get_param, get_param_info: b.get_param_info,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let b = unsafe { load_bridge(kind, app) }?;
|
||||||
|
bridges.insert(kind.to_string(), b);
|
||||||
|
Some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[link(name = "kernel32")]
|
||||||
|
extern "system" {
|
||||||
|
fn LoadLibraryW(name: *const u16) -> *mut c_void;
|
||||||
|
fn GetProcAddress(module: *mut c_void, name: *const u8) -> *mut c_void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ParamInfo {
|
||||||
|
pub param_id: i32,
|
||||||
|
pub title: String,
|
||||||
|
pub value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn urlencode(s: &str) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
for b in s.bytes() {
|
||||||
|
match b {
|
||||||
|
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char),
|
||||||
|
_ => out.push_str(&format!("%{:02X}", b)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(app: &AppHandle) {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
*native::APP.lock().unwrap() = Some(app.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mo (hoac focus neu da mo) cua so VST GUI cho 1 (track, plugin) va attach
|
||||||
|
/// native editor (VST3 .vst3 hoac VST2 .dll) vao HWND cua window.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_vst_gui(
|
pub fn open_vst_gui(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
plugin_id: String,
|
plugin_id: String,
|
||||||
track_id: String,
|
track_id: String,
|
||||||
|
plugin_path: String,
|
||||||
|
plugin_kind: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let label = format!("vst_gui_{}_{}", track_id, plugin_id);
|
let label = format!("vst_gui_{}_{}", track_id, plugin_id);
|
||||||
if let Some(win) = app.get_webview_window(&label) {
|
let win = if let Some(win) = app.get_webview_window(&label) {
|
||||||
win.set_focus().map_err(|e| e.to_string())?;
|
win.set_focus().map_err(|e| e.to_string())?;
|
||||||
return Ok(format!("focused: {}", label));
|
win
|
||||||
}
|
} else {
|
||||||
let win = WebviewWindowBuilder::new(&app, &label, WebviewUrl::App("vst_gui.html".into()))
|
let url = WebviewUrl::App(format!("vst_gui.html?track={}&plugin={}", urlencode(&track_id), urlencode(&plugin_id)).into());
|
||||||
|
let win = WebviewWindowBuilder::new(&app, &label, url)
|
||||||
.title(format!("VST GUI - {}", plugin_id))
|
.title(format!("VST GUI - {}", plugin_id))
|
||||||
.inner_size(800.0, 600.0)
|
.inner_size(800.0, 600.0)
|
||||||
.always_on_top(true)
|
.always_on_top(true)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let _ = win.set_focus();
|
let _ = win.set_focus();
|
||||||
// HWND cho native editor attach (T9). Log de verify — chua dung.
|
win
|
||||||
match win.window_handle() {
|
};
|
||||||
Ok(raw) => {
|
|
||||||
let raw = raw.as_raw();
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
if let raw_window_handle::RawWindowHandle::Win32(h) = raw {
|
{
|
||||||
println!("[vst_gui] {} hwnd={:?}", label, h.hwnd);
|
let hwnd = match win.window_handle() {
|
||||||
|
Ok(raw) => match raw.as_raw() {
|
||||||
|
raw_window_handle::RawWindowHandle::Win32(h) => h.hwnd.get() as *mut c_void,
|
||||||
|
_ => {
|
||||||
|
return Err("not a Win32 window".into());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => return Err(format!("raw_window_handle error: {}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let kind = if plugin_kind.eq_ignore_ascii_case("vst2") { "vst2" } else { "vst3" };
|
||||||
|
let b = native::bridge(kind, &app).ok_or("bridge DLL not found (set SF_NATIVE_HOST_DIR or bundle it)")?;
|
||||||
|
let mut err = [0i8; 512];
|
||||||
|
let mut w: i32 = 0;
|
||||||
|
let mut h: i32 = 0;
|
||||||
|
let c_path = CString::new(plugin_path.as_str()).map_err(|e| e.to_string())?;
|
||||||
|
let c_plugin = CString::new(plugin_id.as_str()).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let handle = unsafe {
|
||||||
|
if kind == "vst2" {
|
||||||
|
let f = b.load.ok_or("vst2 bridge missing SF_VST2_Load")?;
|
||||||
|
f(c_path.as_ptr(), hwnd, &mut w, &mut h, err.as_mut_ptr(), err.len() as i32)
|
||||||
|
} else {
|
||||||
|
let f = b.attach.ok_or("vst3 bridge missing SF_VST3_Attach")?;
|
||||||
|
f(c_path.as_ptr(), c_plugin.as_ptr(), hwnd, &mut w, &mut h, err.as_mut_ptr(), err.len() as i32)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if handle <= 0 {
|
||||||
|
let msg = unsafe { std::ffi::CStr::from_ptr(err.as_ptr()) }.to_string_lossy().into_owned();
|
||||||
|
return Err(format!("attach failed ({}): {}", kind, msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cb = b.set_param_cb.ok_or("bridge missing SetParamCallback")?;
|
||||||
|
let rc = unsafe { cb(handle, native::on_param_changed, std::ptr::null_mut()) };
|
||||||
|
if rc != 0 {
|
||||||
|
return Err(format!("set param callback failed: {}", rc));
|
||||||
|
}
|
||||||
|
|
||||||
|
native::HANDLE_MAP.lock().unwrap().insert(handle, (track_id.clone(), plugin_id.clone(), kind.to_string()));
|
||||||
|
native::EDITOR_MAP.lock().unwrap().insert(format!("{}|{}", track_id, plugin_id), handle);
|
||||||
|
Ok(format!("opened: {} (handle={}, {}x{})", label, handle, w, h))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
Ok(format!("opened (no native attach on this OS): {}", label))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JS automation -> setParamNormalized/setParameter.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_vst_param(
|
||||||
|
app: AppHandle,
|
||||||
|
track_id: String,
|
||||||
|
plugin_id: String,
|
||||||
|
param_id: i32,
|
||||||
|
value: f64,
|
||||||
|
) -> Result<i32, String> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let key = format!("{}|{}", track_id, plugin_id);
|
||||||
|
let handle = *native::EDITOR_MAP.lock().unwrap().get(&key).ok_or("editor not open")?;
|
||||||
|
let kind = {
|
||||||
|
let m = native::HANDLE_MAP.lock().unwrap();
|
||||||
|
m.get(&handle).map(|t| t.2.clone()).unwrap_or_else(|| "vst2".to_string())
|
||||||
|
};
|
||||||
|
let b = native::bridge(&kind, &app).ok_or("bridge DLL not found")?;
|
||||||
|
let f = b.set_param.ok_or("bridge missing SetParam")?;
|
||||||
|
let rc = unsafe { f(handle, param_id, value) };
|
||||||
|
if rc != 0 {
|
||||||
|
return Err(format!("set param failed: {}", rc));
|
||||||
|
}
|
||||||
|
Ok(0)
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
let _ = raw;
|
{
|
||||||
|
let _ = (app, track_id, plugin_id, param_id, value);
|
||||||
|
Ok(0)
|
||||||
}
|
}
|
||||||
Err(e) => println!("[vst_gui] {} raw_window_handle error: {}", label, e),
|
|
||||||
}
|
}
|
||||||
Ok(format!("opened: {}", label))
|
|
||||||
|
/// Lay danh sach param (id, title, value) de JS dung UI.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_vst_params(
|
||||||
|
app: AppHandle,
|
||||||
|
track_id: String,
|
||||||
|
plugin_id: String,
|
||||||
|
) -> Result<Vec<ParamInfo>, String> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let key = format!("{}|{}", track_id, plugin_id);
|
||||||
|
let handle = *native::EDITOR_MAP.lock().unwrap().get(&key).ok_or("editor not open")?;
|
||||||
|
let kind = {
|
||||||
|
let m = native::HANDLE_MAP.lock().unwrap();
|
||||||
|
m.get(&handle).map(|t| t.2.clone()).unwrap_or_else(|| "vst2".to_string())
|
||||||
|
};
|
||||||
|
let b = native::bridge(&kind, &app).ok_or("bridge DLL not found")?;
|
||||||
|
let count = {
|
||||||
|
let f = b.get_param_count.ok_or("bridge missing GetParamCount")?;
|
||||||
|
unsafe { f(handle) }
|
||||||
|
};
|
||||||
|
if count < 0 {
|
||||||
|
return Err(format!("get param count failed: {}", count));
|
||||||
|
}
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for i in 0..count {
|
||||||
|
if kind == "vst2" {
|
||||||
|
let f = b.get_param.ok_or("bridge missing GetParam")?;
|
||||||
|
let mut v: f64 = 0.0;
|
||||||
|
let rc = unsafe { f(handle, i, &mut v) };
|
||||||
|
if rc == 0 {
|
||||||
|
out.push(ParamInfo { param_id: i, title: format!("Param {}", i), value: v });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let f = b.get_param_info.ok_or("bridge missing GetParamInfo")?;
|
||||||
|
let mut id: i32 = 0;
|
||||||
|
let mut title = [0i8; 128];
|
||||||
|
let mut v: f64 = 0.0;
|
||||||
|
let rc = unsafe { f(handle, i, &mut id, title.as_mut_ptr(), title.len() as i32, &mut v) };
|
||||||
|
if rc == 0 {
|
||||||
|
let title_str = unsafe { std::ffi::CStr::from_ptr(title.as_ptr()) }.to_string_lossy().into_owned();
|
||||||
|
out.push(ParamInfo { param_id: id, title: title_str, value: v });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
let _ = (app, track_id, plugin_id);
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dong editor native + xoa map (goi khi cua so VST GUI dong).
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn close_vst_editor(app: AppHandle, track_id: String, plugin_id: String) -> Result<i32, String> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let key = format!("{}|{}", track_id, plugin_id);
|
||||||
|
let handle = native::EDITOR_MAP.lock().unwrap().remove(&key).ok_or("editor not open")?;
|
||||||
|
let kind = native::HANDLE_MAP.lock().unwrap().remove(&handle).map(|t| t.2).unwrap_or_else(|| "vst2".to_string());
|
||||||
|
let b = native::bridge(&kind, &app).ok_or("bridge DLL not found")?;
|
||||||
|
let f = b.close.ok_or("bridge missing Close")?;
|
||||||
|
let mut err = [0i8; 256];
|
||||||
|
let rc = unsafe { f(handle, err.as_mut_ptr(), err.len() as i32) };
|
||||||
|
Ok(rc)
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
let _ = (app, track_id, plugin_id);
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,97 @@
|
|||||||
<title>VST GUI</title>
|
<title>VST GUI</title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: system-ui, sans-serif; background: #1a1d24; color: #c8ccd4;
|
body { font-family: system-ui, sans-serif; background: #1a1d24; color: #c8ccd4;
|
||||||
display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
height: 100vh; margin: 0; display: flex; }
|
||||||
#msg { text-align: center; max-width: 520px; line-height: 1.6; }
|
#editor { flex: 1; position: relative; }
|
||||||
|
#panel { width: 260px; border-left: 1px solid #2c313b; padding: 10px; overflow-y: auto;
|
||||||
|
background: #171a20; }
|
||||||
|
#panel h3 { margin: 4px 0 10px; font-size: 13px; color: #9aa3b2; font-weight: 600;
|
||||||
|
text-transform: uppercase; letter-spacing: .04em; }
|
||||||
|
.row { margin-bottom: 10px; }
|
||||||
|
.row label { display: block; font-size: 12px; color: #c8ccd4; margin-bottom: 2px;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.row .val { float: right; color: #6ea8ff; font-variant-numeric: tabular-nums; }
|
||||||
|
.row input[type=range] { width: 100%; accent-color: #3b82f6; }
|
||||||
|
#msg { padding: 20px; text-align: center; color: #9aa3b2; line-height: 1.6; }
|
||||||
code { background: #262b34; padding: 2px 6px; border-radius: 4px; }
|
code { background: #262b34; padding: 2px 6px; border-radius: 4px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="msg">Cửa sổ nổi VST GUI (T8).<br>Plugin editor sẽ được attach vào HWND cửa sổ này ở T9.</div>
|
<div id="editor"><div id="msg">Plugin editor attach vào HWND (vùng này).<br>Nếu không thấy editor: cửa sổ này mở qua DAW, native DLL phải tìm thấy (SF_NATIVE_HOST_DIR).</div></div>
|
||||||
|
<div id="panel">
|
||||||
|
<h3>Parameters</h3>
|
||||||
|
<div id="params"><div id="msg">Đang tải params…</div></div>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// T8: placeholder — chưa có native editor. JS của DAW gọi invoke('open_vst_gui', ...)
|
// T11: param sync 2 chiều — native đổi param -> event vst_param_changed -> cập nhật UI;
|
||||||
// để mở/focus cửa sổ này. Thông tin (track, plugin) nằm ở title + label window.
|
// JS kéo slider -> invoke set_vst_param -> setParamNormalized/setParameter.
|
||||||
|
// Guard chống loop: pendingSet chứa param_id JS vừa set; event echo của chính mình bị bỏ qua.
|
||||||
|
const T = window.__TAURI__;
|
||||||
|
if (!T) { document.getElementById('msg').textContent = 'Không có __TAURI__ (chạy ngoài Tauri).'; throw new Error('no tauri'); }
|
||||||
|
const { invoke } = T.core;
|
||||||
|
const { listen } = T.event;
|
||||||
|
|
||||||
|
const qs = new URLSearchParams(location.search);
|
||||||
|
const trackId = qs.get('track') || '';
|
||||||
|
const pluginId = qs.get('plugin') || '';
|
||||||
|
const pendingSet = new Set();
|
||||||
|
const sliders = new Map(); // param_id -> {slider, val}
|
||||||
|
|
||||||
|
function setValue(paramId, value) {
|
||||||
|
const el = sliders.get(paramId);
|
||||||
|
if (!el) return;
|
||||||
|
el.slider.value = value;
|
||||||
|
el.val.textContent = Number(value).toFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
const box = document.getElementById('params');
|
||||||
|
let list;
|
||||||
|
try {
|
||||||
|
list = await invoke('get_vst_params', { trackId, pluginId });
|
||||||
|
} catch (e) {
|
||||||
|
box.innerHTML = '<div id="msg">Lỗi: ' + String(e) + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!list || list.length === 0) {
|
||||||
|
box.innerHTML = '<div id="msg">Plugin không có param nào.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
box.innerHTML = '';
|
||||||
|
for (const p of list) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'row';
|
||||||
|
const lbl = document.createElement('label');
|
||||||
|
lbl.textContent = p.title;
|
||||||
|
const val = document.createElement('span');
|
||||||
|
val.className = 'val';
|
||||||
|
val.textContent = Number(p.value).toFixed(3);
|
||||||
|
const slider = document.createElement('input');
|
||||||
|
slider.type = 'range'; slider.min = 0; slider.max = 1; slider.step = 0.001;
|
||||||
|
slider.value = p.value;
|
||||||
|
slider.addEventListener('input', () => { val.textContent = parseFloat(slider.value).toFixed(3); });
|
||||||
|
slider.addEventListener('change', () => {
|
||||||
|
const v = parseFloat(slider.value);
|
||||||
|
pendingSet.add(p.param_id);
|
||||||
|
invoke('set_vst_param', { trackId, pluginId, paramId: p.param_id, value: v })
|
||||||
|
.catch(err => { console.error('set_vst_param', err); })
|
||||||
|
.finally(() => setTimeout(() => pendingSet.delete(p.param_id), 500));
|
||||||
|
});
|
||||||
|
row.appendChild(lbl); row.appendChild(val); row.appendChild(slider);
|
||||||
|
box.appendChild(row);
|
||||||
|
sliders.set(p.param_id, { slider, val });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// native đổi param (từ editor/automation) -> cập nhật UI
|
||||||
|
listen('vst_param_changed', (e) => {
|
||||||
|
const d = e.payload || {};
|
||||||
|
if (d.track_id !== trackId || d.plugin_id !== pluginId) return;
|
||||||
|
if (pendingSet.has(d.param_id)) { pendingSet.delete(d.param_id); return; } // echo của chính mình
|
||||||
|
setValue(d.param_id, d.value);
|
||||||
|
}).catch(err => console.error('listen vst_param_changed', err));
|
||||||
|
|
||||||
|
refresh().catch(err => console.error('refresh', err));
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user