T11: IPC param sync 2 chieu native<->JS (setParamNormalized/setParameter, vst_param_changed event, guard chong loop)

This commit is contained in:
2026-08-11 16:27:30 +07:00
parent aaf21ddf89
commit 9eec33cc79
7 changed files with 650 additions and 41 deletions
+6
View File
@@ -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
+94 -2
View File
@@ -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"
+29 -3
View File
@@ -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);
} }
+93 -2
View File
@@ -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"
+2 -1
View File
@@ -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
View File
@@ -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)
}
} }
+86 -5
View File
@@ -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 mqua 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>