577 lines
19 KiB
C++
577 lines
19 KiB
C++
// vst3_host_bridge.cpp — VST3 GUI bridge (T9, spec vsti_gui)
|
|
//
|
|
// DLL export C API để attach VST3 editor vào HWND cha (cửa sổ nổi `vst_gui_*`
|
|
// từ T8). Flow chuẩn hosting: Module::create(path) → PlugProvider →
|
|
// getController → createView(kEditor) → getSize → attached(hwnd,
|
|
// kPlatformTypeHWND). Đóng: view->removed() + release (IPtr).
|
|
//
|
|
// QUYẾT ĐỊNH (T9): VST3 SDK THUẦN (không JUCE) — nhẹ, license BSD-3 sạch;
|
|
// T10 VST2 dùng vestige.h clean-room riêng; T12 native audio loop cũng SDK
|
|
// thuần. Nâng cấp nếu cần: gộp engine bằng JUCE khi có yêu cầu rõ ràng.
|
|
//
|
|
// T11 sẽ mở rộng ComponentHandler::performEdit → đẩy lên JS (vst_param_changed);
|
|
// hiện tại no-op để plugin không crash khi user chỉnh param trong editor.
|
|
#define NOMINMAX
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
|
|
#include "public.sdk/source/vst/hosting/module.h"
|
|
#include "public.sdk/source/vst/hosting/plugprovider.h"
|
|
#include "public.sdk/source/common/commonstringconvert.h"
|
|
#include "pluginterfaces/base/funknown.h"
|
|
#include "pluginterfaces/gui/iplugview.h"
|
|
#include "pluginterfaces/vst/ivstaudioprocessor.h"
|
|
#include "pluginterfaces/vst/ivstcomponent.h"
|
|
#include "pluginterfaces/vst/ivsteditcontroller.h"
|
|
#include "pluginterfaces/vst/ivstevents.h"
|
|
|
|
#include "AudioEngine.h"
|
|
#include "NativeMixer.h"
|
|
|
|
#include <atomic>
|
|
|
|
using namespace Steinberg;
|
|
using namespace Steinberg::Vst;
|
|
|
|
namespace {
|
|
|
|
// 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
|
|
{
|
|
public:
|
|
ComponentHandler () = default;
|
|
tresult PLUGIN_API queryInterface (const TUID _iid, void** obj) override
|
|
{
|
|
if (FUnknownPrivate::iidEqual (_iid, IComponentHandler::iid) ||
|
|
FUnknownPrivate::iidEqual (_iid, FUnknown::iid))
|
|
{
|
|
*obj = this;
|
|
addRef ();
|
|
return kResultOk;
|
|
}
|
|
*obj = nullptr;
|
|
return kNoInterface;
|
|
}
|
|
uint32 PLUGIN_API addRef () override { return 1; }
|
|
uint32 PLUGIN_API release () override { return 1; }
|
|
tresult PLUGIN_API beginEdit (ParamID /*id*/) override { return kResultOk; }
|
|
tresult PLUGIN_API performEdit (ParamID id, ParamValue valueNormalized) override
|
|
{
|
|
if (cb)
|
|
cb (handle, static_cast<int32> (id), static_cast<double> (valueNormalized), userdata);
|
|
return kResultOk;
|
|
}
|
|
tresult PLUGIN_API endEdit (ParamID /*id*/) 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
|
|
{
|
|
VST3::Hosting::Module::Ptr module;
|
|
IPtr<PlugProvider> plugProvider;
|
|
IPtr<IComponent> component;
|
|
IPtr<IAudioProcessor> processor;
|
|
IPtr<IEditController> controller;
|
|
IPtr<IPlugView> view;
|
|
ComponentHandler handler;
|
|
sonicforge::AudioEngine audio; // T12: native audio loop (SPSC + WASAPI)
|
|
std::atomic<bool> audioRunning {false};
|
|
sonicforge::NativeMixer mixer; // T13: track gain/pan + master limiter
|
|
};
|
|
|
|
// T12: IEventList cố định (không cấp phát trên audio thread).
|
|
class FixedEventList : public IEventList
|
|
{
|
|
public:
|
|
FixedEventList () = default;
|
|
tresult PLUGIN_API queryInterface (const TUID _iid, void** obj) override
|
|
{
|
|
if (FUnknownPrivate::iidEqual (_iid, IEventList::iid) ||
|
|
FUnknownPrivate::iidEqual (_iid, FUnknown::iid))
|
|
{
|
|
*obj = this;
|
|
addRef ();
|
|
return kResultOk;
|
|
}
|
|
*obj = nullptr;
|
|
return kNoInterface;
|
|
}
|
|
uint32 PLUGIN_API addRef () override { return 1; }
|
|
uint32 PLUGIN_API release () override { return 1; }
|
|
int32 PLUGIN_API getEventCount () override { return count; }
|
|
tresult PLUGIN_API getEvent (int32 index, Event& e) override
|
|
{
|
|
if (index < 0 || index >= count)
|
|
return kInvalidArgument;
|
|
e = events[index];
|
|
return kResultOk;
|
|
}
|
|
tresult PLUGIN_API addEvent (Event& e) override
|
|
{
|
|
if (count >= 512)
|
|
return kOutOfMemory;
|
|
events[count++] = e;
|
|
return kResultOk;
|
|
}
|
|
|
|
Event events[512];
|
|
int32 count = 0;
|
|
};
|
|
|
|
// T12: audio thread callback — build EventList + process(). Render thread:
|
|
// KHÔNG lock, KHÔNG cấp phát (EventList cố định, buffer pre-alloc).
|
|
bool vst3AudioProcess (const sonicforge::MidiEvent* events, int32 eventCount,
|
|
float* outL, float* outR, int32 frames, void* userdata)
|
|
{
|
|
Instance* inst = static_cast<Instance*> (userdata);
|
|
IAudioProcessor* proc = inst ? inst->processor.get () : nullptr;
|
|
if (!proc)
|
|
return false;
|
|
|
|
FixedEventList list;
|
|
for (int32 i = 0; i < eventCount; ++i)
|
|
{
|
|
Event e = {};
|
|
e.sampleOffset = 0;
|
|
e.ppqPosition = 0.0;
|
|
e.flags = 0;
|
|
if (events[i].noteOn)
|
|
{
|
|
e.type = Event::kNoteOnEvent;
|
|
e.noteOn.channel = events[i].channel;
|
|
e.noteOn.pitch = events[i].pitch;
|
|
e.noteOn.velocity = events[i].velocity;
|
|
e.noteOn.noteId = -1;
|
|
}
|
|
else
|
|
{
|
|
e.type = Event::kNoteOffEvent;
|
|
e.noteOff.channel = events[i].channel;
|
|
e.noteOff.pitch = events[i].pitch;
|
|
e.noteOff.velocity = 0.0f;
|
|
e.noteOff.noteId = -1;
|
|
}
|
|
list.addEvent (e);
|
|
}
|
|
|
|
ProcessData data = {};
|
|
data.processMode = kRealtime;
|
|
data.symbolicSampleSize = kSample32;
|
|
data.numSamples = frames;
|
|
data.numInputs = 0;
|
|
data.numOutputs = 1;
|
|
AudioBusBuffers outBus = {};
|
|
float* chans[2] = { outL, outR };
|
|
outBus.numChannels = 2;
|
|
outBus.channelBuffers32 = chans;
|
|
data.outputs = &outBus;
|
|
data.inputEvents = &list;
|
|
|
|
const tresult res = proc->process (data);
|
|
inst->mixer.processInPlace (outL, outR, frames); // T13: gain/pan + limiter
|
|
return res == kResultOk;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" {
|
|
|
|
// Attach VST3 editor vào parent_hwnd. module_path_utf8: đường dẫn tới .vst3
|
|
// (UTF-8). plugin_name: tên class VST3 (ClassInfo::name — khớp plugin_id JS).
|
|
// Trả handle (>= 1) hoặc 0 + err. out_w/out_h: kích thước ưa thích của editor.
|
|
__declspec (dllexport) int32 SF_VST3_Attach (const char* module_path_utf8,
|
|
const char* plugin_name,
|
|
HWND parent_hwnd,
|
|
int32* out_w,
|
|
int32* out_h,
|
|
char* err,
|
|
int32 err_cap)
|
|
{
|
|
std::lock_guard<std::mutex> lock (g_mutex);
|
|
if (!module_path_utf8 || !plugin_name || !parent_hwnd)
|
|
{
|
|
set_err (err, err_cap, "null arg");
|
|
return 0;
|
|
}
|
|
auto inst = std::make_unique<Instance> ();
|
|
|
|
std::string loadError;
|
|
inst->module = VST3::Hosting::Module::create (module_path_utf8, loadError);
|
|
if (!inst->module)
|
|
{
|
|
set_err (err, err_cap, "cannot load VST3 module");
|
|
return 0;
|
|
}
|
|
|
|
VST3::Hosting::ClassInfo classInfo;
|
|
bool found = false;
|
|
auto factory = inst->module->getFactory ();
|
|
for (auto& ci : factory.classInfos ())
|
|
{
|
|
if (ci.category () == kVstAudioEffectClass && ci.name () == plugin_name)
|
|
{
|
|
classInfo = ci;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
{
|
|
set_err (err, err_cap, "no VST3 audio effect class with that name");
|
|
return 0;
|
|
}
|
|
|
|
inst->plugProvider = IPtr<PlugProvider> (new PlugProvider (factory, classInfo, true));
|
|
if (!inst->plugProvider->initialize ())
|
|
{
|
|
set_err (err, err_cap, "plugin initialize failed");
|
|
return 0;
|
|
}
|
|
|
|
inst->component = inst->plugProvider->getComponent (); // giữ ref
|
|
if (!inst->component)
|
|
{
|
|
set_err (err, err_cap, "plugin has no component");
|
|
return 0;
|
|
}
|
|
IAudioProcessor* rawProc = nullptr;
|
|
if (inst->component->queryInterface (IAudioProcessor::iid,
|
|
reinterpret_cast<void**> (&rawProc)) != kResultOk ||
|
|
!rawProc)
|
|
{
|
|
set_err (err, err_cap, "plugin has no audio processor");
|
|
return 0;
|
|
}
|
|
inst->processor = IPtr<IAudioProcessor> (rawProc);
|
|
|
|
IEditController* rawController = inst->plugProvider->getController (); // +1 ref
|
|
if (!rawController)
|
|
{
|
|
set_err (err, err_cap, "plugin has no edit controller");
|
|
return 0;
|
|
}
|
|
inst->controller = IPtr<IEditController> (rawController); // nhận +1
|
|
|
|
inst->controller->setComponentHandler (&inst->handler);
|
|
|
|
inst->view = inst->controller->createView (ViewType::kEditor);
|
|
if (!inst->view)
|
|
{
|
|
set_err (err, err_cap, "plugin has no editor view");
|
|
return 0;
|
|
}
|
|
|
|
ViewRect r {};
|
|
if (inst->view->getSize (&r) == kResultTrue)
|
|
{
|
|
if (out_w)
|
|
*out_w = r.getWidth ();
|
|
if (out_h)
|
|
*out_h = r.getHeight ();
|
|
}
|
|
|
|
if (inst->view->attached ((void*) parent_hwnd, kPlatformTypeHWND) != kResultTrue)
|
|
{
|
|
set_err (err, err_cap, "view attach failed");
|
|
return 0;
|
|
}
|
|
|
|
int32 handle = g_next_handle++;
|
|
g_instances[handle] = std::move (inst);
|
|
return handle;
|
|
}
|
|
|
|
// Đóng editor: removed() + release (IPtr tự release khi xóa Instance).
|
|
__declspec (dllexport) int32 SF_VST3_Close (int32 handle, 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;
|
|
}
|
|
// T12: dung audio loop (join render thread) truoc khi pha huy plugin.
|
|
it->second->audioRunning.store (false, std::memory_order_release);
|
|
it->second->audio.stop ();
|
|
if (it->second->processor)
|
|
it->second->processor->setProcessing (false);
|
|
if (it->second->view)
|
|
{
|
|
it->second->view->removed ();
|
|
it->second->view = nullptr;
|
|
}
|
|
g_instances.erase (it);
|
|
return 0;
|
|
}
|
|
|
|
// Kích thước ưa thích hiện tại của editor.
|
|
__declspec (dllexport) int32 SF_VST3_GetSize (int32 handle, int32* out_w, int32* out_h)
|
|
{
|
|
std::lock_guard<std::mutex> lock (g_mutex);
|
|
auto it = g_instances.find (handle);
|
|
if (it == g_instances.end ())
|
|
return -1;
|
|
ViewRect r {};
|
|
if (it->second->view && it->second->view->getSize (&r) == kResultTrue)
|
|
{
|
|
if (out_w)
|
|
*out_w = r.getWidth ();
|
|
if (out_h)
|
|
*out_h = r.getHeight ();
|
|
return 0;
|
|
}
|
|
return -2;
|
|
}
|
|
|
|
// Báo view: parent HWND đã được resize (w,h) — plugin cập nhật nội dung.
|
|
__declspec (dllexport) int32 SF_VST3_Resize (int32 handle, int32 w, int32 h)
|
|
{
|
|
std::lock_guard<std::mutex> lock (g_mutex);
|
|
auto it = g_instances.find (handle);
|
|
if (it == g_instances.end ())
|
|
return -1;
|
|
if (it->second->view)
|
|
{
|
|
ViewRect r = {};
|
|
r.right = w;
|
|
r.bottom = h;
|
|
if (it->second->view->onSize (&r) == kResultTrue)
|
|
return 0;
|
|
}
|
|
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;
|
|
}
|
|
|
|
// T12: bat native audio loop (SPSC + WASAPI) cho instance nay. Audio thread
|
|
// goi vst3AudioProcess (EventList + process) moi block.
|
|
__declspec (dllexport) int32 SF_VST3_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->processor)
|
|
return -2;
|
|
|
|
ProcessSetup setup = {};
|
|
setup.processMode = kRealtime;
|
|
setup.symbolicSampleSize = kSample32;
|
|
setup.maxSamplesPerBlock = blockSize;
|
|
setup.sampleRate = static_cast<SampleRate> (sampleRate);
|
|
inst->processor->setupProcessing (setup);
|
|
inst->processor->setProcessing (true);
|
|
|
|
inst->audioRunning.store (true, std::memory_order_release);
|
|
if (!inst->audio.start (sampleRate, blockSize, &vst3AudioProcess, inst, err, err_cap))
|
|
{
|
|
inst->audioRunning.store (false, std::memory_order_release);
|
|
inst->processor->setProcessing (false);
|
|
return -3;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
__declspec (dllexport) int32 SF_VST3_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 ();
|
|
if (it->second->processor)
|
|
it->second->processor->setProcessing (false);
|
|
return 0;
|
|
}
|
|
|
|
__declspec (dllexport) int32 SF_VST3_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_VST3_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_VST3_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 ();
|
|
}
|
|
|
|
// T12: MIDI → SPSC (audio thread xu ly). Khong co duong direct nhu VST2 —
|
|
// offline render (T16) se drain SPSC rieng.
|
|
__declspec (dllexport) int32 SF_VST3_SendNoteOn (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;
|
|
sonicforge::MidiEvent ev = {};
|
|
ev.channel = channel;
|
|
ev.pitch = pitch;
|
|
ev.velocity = static_cast<float> (velocity) / 127.0f;
|
|
ev.noteOn = true;
|
|
return it->second->audio.pushMidi (ev) ? 0 : -2;
|
|
}
|
|
|
|
__declspec (dllexport) int32 SF_VST3_SendNoteOff (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;
|
|
sonicforge::MidiEvent ev = {};
|
|
ev.channel = channel;
|
|
ev.pitch = pitch;
|
|
ev.velocity = 0.0f;
|
|
ev.noteOn = false;
|
|
return it->second->audio.pushMidi (ev) ? 0 : -2;
|
|
}
|
|
|
|
// T13: track gain/pan + master limiter (NativeMixer). Set truoc AudioStart;
|
|
// audio thread doc state nay (khong lock).
|
|
__declspec (dllexport) int32 SF_VST3_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_VST3_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;
|
|
}
|
|
|
|
} // extern "C"
|