T12: native audio loop SPSC + WASAPI (exclusive/shared, zero-lock audio thread, SendNote -> SPSC)

This commit is contained in:
2026-08-11 16:40:30 +07:00
parent 9eec33cc79
commit 8f1e89ce50
5 changed files with 828 additions and 0 deletions
+343
View File
@@ -0,0 +1,343 @@
// AudioEngine.cpp — native audio loop (T12, spec vsti_native_audio_pipeline_spec)
//
// SPSC ring buffer (MIDI UI thread → audio thread) + WASAPI render thread.
// Audio thread: không mutex, không cấp phát, không I/O. MMCSS "Audio" để
// giảm underrun.
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include "AudioEngine.h"
#include <cstdio>
#include <cstring>
#include <avrt.h>
#include <functiondiscoverykeys.h>
#include <objbase.h>
namespace sonicforge {
namespace {
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
AudioEngine::AudioEngine ()
{
std::memset (m_events, 0, sizeof (m_events));
}
AudioEngine::~AudioEngine ()
{
stop ();
delete[] m_outL;
delete[] m_outR;
}
bool AudioEngine::pushMidi (const MidiEvent& ev)
{
std::lock_guard<std::mutex> lock (m_producerMutex);
uint32 head = m_head.load (std::memory_order_relaxed);
uint32 next = nextIdx (head);
if (next == m_tail.load (std::memory_order_acquire))
return false; // full
m_events[head] = ev;
m_head.store (next, std::memory_order_release);
return true;
}
// T12: activate IAudioClient cho 1 device + Initialize exclusive→shared +
// event handle + render service. Khong cấp phát m_outL/m_outR (start() lam).
bool AudioEngine::initOnDevice (IMMDevice* device, int32 sampleRate, int32 blockSize,
char* err, int32 err_cap)
{
if (!device)
return false;
if (m_client)
{
// device truoc do dang giu — stop/release de thu device moi
stop ();
}
HRESULT hr = device->Activate (__uuidof (IAudioClient), CLSCTX_ALL, nullptr,
reinterpret_cast<void**> (&m_client));
if (FAILED (hr) || !m_client)
{
m_client = nullptr;
set_err (err, err_cap, "IAudioClient activate failed");
return false;
}
WAVEFORMATEX fmt = {};
fmt.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
fmt.nChannels = 2;
fmt.nSamplesPerSec = static_cast<DWORD> (sampleRate);
fmt.wBitsPerSample = 32;
fmt.nBlockAlign = 8;
fmt.nAvgBytesPerSec = static_cast<DWORD> (sampleRate) * 8;
// 100ns units: 1e7 * seconds
REFERENCE_TIME hnsPeriod =
static_cast<REFERENCE_TIME> (10000000.0 * blockSize / sampleRate);
// Exclusive trước (latency đúng period), fallback shared. Shared mode
// yêu cầu đúng mix format của device (GetMixFormat) — format tự build
// IEEE_FLOAT 2ch thường bị AUDCLNT_E_UNSUPPORTED_FORMAT (0x88890008).
hr = m_client->Initialize (AUDCLNT_SHAREMODE_EXCLUSIVE,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
hnsPeriod, hnsPeriod, &fmt, nullptr);
if (FAILED (hr))
{
WAVEFORMATEX* mix = nullptr;
if (SUCCEEDED (m_client->GetMixFormat (&mix)) && mix)
{
m_sampleRate = static_cast<int32> (mix->nSamplesPerSec);
hr = m_client->Initialize (AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
hnsPeriod, 0, mix, nullptr);
CoTaskMemFree (mix);
}
if (FAILED (hr))
{
std::snprintf (err, static_cast<size_t> (err_cap),
"WASAPI Initialize failed: 0x%08X", static_cast<unsigned> (hr));
m_client->Release ();
m_client = nullptr;
return false;
}
}
hr = m_client->GetBufferSize (&m_bufferFrames);
if (FAILED (hr))
{
stop ();
return false;
}
m_event = CreateEventW (nullptr, FALSE, FALSE, nullptr);
if (!m_event)
{
stop ();
return false;
}
hr = m_client->SetEventHandle (m_event);
if (FAILED (hr))
{
stop ();
return false;
}
hr = m_client->GetService (IID_PPV_ARGS (&m_render));
if (FAILED (hr) || !m_render)
{
set_err (err, err_cap, "IAudioRenderClient failed");
stop ();
return false;
}
return true;
}
bool AudioEngine::start (int32 sampleRate, int32 blockSize, ProcessFn fn, void* userdata,
char* err, int32 err_cap)
{
if (m_running.load (std::memory_order_acquire))
return true;
if (sampleRate <= 0 || blockSize <= 0)
{
set_err (err, err_cap, "bad sampleRate/blockSize");
return false;
}
m_fn = fn;
m_userdata = userdata;
m_sampleRate = sampleRate;
m_blockSize = blockSize;
HRESULT hr = CoInitializeEx (nullptr, COINIT_MULTITHREADED);
if (FAILED (hr) && hr != RPC_E_CHANGED_MODE)
{
set_err (err, err_cap, "CoInitializeEx failed");
return false;
}
m_comInit = (hr != RPC_E_CHANGED_MODE);
IMMDeviceEnumerator* enumerator = nullptr;
hr = CoCreateInstance (__uuidof (MMDeviceEnumerator), nullptr, CLSCTX_ALL,
IID_PPV_ARGS (&enumerator));
if (FAILED (hr))
{
set_err (err, err_cap, "no MMDeviceEnumerator");
goto fail;
}
// Default endpoint trước; fallback: enumerate các endpoint ACTIVE và thử
// từng cái (device default có thể bị invalidated — vd HDMI không connect).
IMMDevice* device = nullptr;
if (SUCCEEDED (enumerator->GetDefaultAudioEndpoint (eRender, eConsole, &device)) && device)
{
if (initOnDevice (device, sampleRate, blockSize, err, err_cap))
{
device->Release ();
enumerator->Release ();
goto ready;
}
device->Release ();
}
IMMDeviceCollection* coll = nullptr;
hr = enumerator->EnumAudioEndpoints (eRender, DEVICE_STATE_ACTIVE, &coll);
if (FAILED (hr) || !coll)
{
enumerator->Release ();
set_err (err, err_cap, "no active render device");
goto fail;
}
UINT nDev = 0;
coll->GetCount (&nDev);
bool ok = false;
for (UINT i = 0; i < nDev; ++i)
{
IMMDevice* d = nullptr;
if (FAILED (coll->Item (i, &d)) || !d)
continue;
if (initOnDevice (d, sampleRate, blockSize, err, err_cap))
ok = true;
d->Release ();
if (ok)
break;
}
coll->Release ();
enumerator->Release ();
if (!ok)
goto fail;
ready:
m_latency.store (static_cast<int32> (m_bufferFrames), std::memory_order_relaxed);
delete[] m_outL;
delete[] m_outR;
m_outL = new float[m_bufferFrames];
m_outR = new float[m_bufferFrames];
m_thread = CreateThread (nullptr, 0, &AudioEngine::renderThreadEntry, this, 0, nullptr);
if (!m_thread)
{
set_err (err, err_cap, "CreateThread failed");
goto fail;
}
m_running.store (true, std::memory_order_release);
hr = m_client->Start ();
if (FAILED (hr))
{
m_running.store (false, std::memory_order_release);
set_err (err, err_cap, "IAudioClient Start failed");
goto fail;
}
return true;
fail:
stop ();
return false;
}
void AudioEngine::stop ()
{
m_running.store (false, std::memory_order_release);
if (m_client)
m_client->Stop ();
if (m_thread)
{
WaitForSingleObject (m_thread, 3000);
CloseHandle (m_thread);
m_thread = nullptr;
}
if (m_event)
{
CloseHandle (m_event);
m_event = nullptr;
}
if (m_render)
{
m_render->Release ();
m_render = nullptr;
}
if (m_client)
{
m_client->Release ();
m_client = nullptr;
}
if (m_comInit)
{
CoUninitialize ();
m_comInit = false;
}
m_head.store (0, std::memory_order_relaxed);
m_tail.store (0, std::memory_order_relaxed);
}
DWORD WINAPI AudioEngine::renderThreadEntry (LPVOID self)
{
static_cast<AudioEngine*> (self)->renderLoop ();
return 0;
}
void AudioEngine::renderLoop ()
{
CoInitializeEx (nullptr, COINIT_MULTITHREADED);
DWORD taskIndex = 0;
HANDLE mmcss = AvSetMmThreadCharacteristicsW (L"Audio", &taskIndex);
while (m_running.load (std::memory_order_acquire))
{
DWORD wait = WaitForSingleObject (m_event, 200);
if (wait != WAIT_OBJECT_0)
continue;
UINT32 pad = 0;
m_client->GetCurrentPadding (&pad);
UINT32 frames = m_bufferFrames - pad;
if (frames == 0)
continue;
BYTE* data = nullptr;
HRESULT hr = m_render->GetBuffer (frames, &data);
if (FAILED (hr))
{
m_underruns.fetch_add (1, std::memory_order_relaxed);
continue;
}
// Drain SPSC (single consumer — không lock)
int32 count = 0;
uint32 tail = m_tail.load (std::memory_order_relaxed);
uint32 head = m_head.load (std::memory_order_acquire);
while (tail != head && count < kMaxEventsPerBlock)
{
m_drained[count] = m_events[tail];
tail = nextIdx (tail);
++count;
}
m_tail.store (tail, std::memory_order_release);
bool wrote = m_fn
? m_fn (m_drained, count, m_outL, m_outR, static_cast<int32> (frames), m_userdata)
: false;
float* inter = reinterpret_cast<float*> (data);
for (UINT32 i = 0; i < frames; ++i)
{
inter[2 * i] = wrote ? m_outL[i] : 0.0f;
inter[2 * i + 1] = wrote ? m_outR[i] : 0.0f;
}
m_render->ReleaseBuffer (frames, 0);
m_blocks.fetch_add (1, std::memory_order_relaxed);
}
if (mmcss)
AvRevertMmThreadCharacteristics (mmcss);
CoUninitialize ();
}
} // namespace sonicforge
+109
View File
@@ -0,0 +1,109 @@
// AudioEngine.h — native audio loop (T12, spec vsti_native_audio_pipeline_spec)
//
// Lõi chung cho cả 2 bridge DLL (vst2_host_bridge, vst3_host_bridge): SPSC
// ring buffer chuyển MIDI từ UI thread → audio thread, render thread WASAPI
// (exclusive trước, fallback shared) gọi ProcessFn mỗi block. Audio thread
// TUYỆT ĐỐI không lock / không cấp phát (golden rule spec III).
//
// QUYẾT ĐỊNH (T12): gộp "VST3AudioEngine.cpp" thành AudioEngine.cpp dùng
// chung 2 plugin kind (spec cho phép "hoặc gộp với T9/T10"); WASAPI đủ cho
// milestone, ASIO bỏ qua (ghi chú, thêm khi có yêu cầu rõ ràng).
#pragma once
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <atomic>
#include <cstdint>
#include <mutex>
#include <audioclient.h>
#include <mmdeviceapi.h>
namespace sonicforge {
using int32 = int32_t;
using uint32 = uint32_t;
// MIDI event từ UI thread → audio thread (SPSC). velocity 0..1.
struct MidiEvent
{
int32 channel;
int32 pitch;
float velocity;
bool noteOn;
};
constexpr int32 kEventCapacity = 4096;
constexpr int32 kMaxEventsPerBlock = 256;
// ProcessFn chạy trên audio thread (render thread). events/count là các MIDI
// event đã drain từ SPSC trong block này. outL/outR: frames mẫu mỗi kênh,
// pre-alloc (audio thread không cấp phát). Trả true nếu đã ghi output (false
// → engine zero-fill).
typedef bool (*ProcessFn)(const MidiEvent* events, int32 eventCount,
float* outL, float* outR, int32 frames, void* userdata);
class AudioEngine
{
public:
AudioEngine ();
~AudioEngine ();
// Khởi tạo WASAPI + render thread. Exclusive trước, fallback shared.
// err/err_cap để trả message lỗi (có thể null). Idempotent.
bool start (int32 sampleRate, int32 blockSize, ProcessFn fn, void* userdata,
char* err, int32 err_cap);
void stop (); // join thread + giải phóng WASAPI
bool running () const { return m_running.load (std::memory_order_acquire); }
// Producer (UI thread) — có mutex riêng để an toàn với nhiều producer
// (Tauri command thread pool); audio thread KHÔNG lock.
bool pushMidi (const MidiEvent& ev);
int32 underruns () const { return m_underruns.load (std::memory_order_relaxed); }
int32 latencySamples () const { return m_latency.load (std::memory_order_relaxed); }
int32 blocksRendered () const { return m_blocks.load (std::memory_order_relaxed); }
int32 sampleRate () const { return m_sampleRate; }
private:
static DWORD WINAPI renderThreadEntry (LPVOID self);
void renderLoop ();
static uint32 nextIdx (uint32 i) { return (i + 1) % kEventCapacity; }
// T12: activate IAudioClient cho 1 device + Initialize exclusive→shared.
// Trả true nếu OK (m_client/m_render/m_event/m_bufferFrames set).
bool initOnDevice (IMMDevice* device, int32 sampleRate, int32 blockSize,
char* err, int32 err_cap);
// SPSC ring (single consumer = audio thread; producer guarded by mutex)
MidiEvent m_events[kEventCapacity];
std::atomic<uint32> m_head {0}; // producer index (next write)
std::atomic<uint32> m_tail {0}; // consumer index (next read)
std::mutex m_producerMutex;
MidiEvent m_drained[kMaxEventsPerBlock];
std::atomic<bool> m_running {false};
std::atomic<int32> m_underruns {0};
std::atomic<int32> m_latency {0};
std::atomic<int32> m_blocks {0};
HANDLE m_thread = nullptr;
HANDLE m_event = nullptr;
IAudioClient* m_client = nullptr;
IAudioRenderClient* m_render = nullptr;
UINT32 m_bufferFrames = 0;
int32 m_sampleRate = 44100;
int32 m_blockSize = 128;
bool m_comInit = false;
ProcessFn m_fn = nullptr;
void* m_userdata = nullptr;
float* m_outL = nullptr;
float* m_outR = nullptr;
};
} // namespace sonicforge
+15
View File
@@ -32,6 +32,20 @@ target_compile_definitions(vst3_host_bridge PRIVATE
NOMINMAX NOMINMAX
WIN32_LEAN_AND_MEAN WIN32_LEAN_AND_MEAN
) )
target_link_libraries(vst3_host_bridge PRIVATE audio_engine)
# Native audio loop (T12): SPSC + WASAPI — dùng chung cho cả 2 bridge.
add_library(audio_engine STATIC
AudioEngine.cpp
)
target_include_directories(audio_engine PUBLIC native_host)
target_compile_definitions(audio_engine PRIVATE
_CRT_SECURE_NO_WARNINGS
NOMINMAX
WIN32_LEAN_AND_MEAN
_WIN32_WINNT=0x0601
)
target_link_libraries(audio_engine PUBLIC ole32 avrt)
# VST2 host bridge — chỉ cần vestige.h clean-room, không cần SDK. # VST2 host bridge — chỉ cần vestige.h clean-room, không cần SDK.
add_library(vst2_host_bridge SHARED add_library(vst2_host_bridge SHARED
@@ -43,3 +57,4 @@ target_compile_definitions(vst2_host_bridge PRIVATE
NOMINMAX NOMINMAX
WIN32_LEAN_AND_MEAN WIN32_LEAN_AND_MEAN
) )
target_link_libraries(vst2_host_bridge PRIVATE audio_engine)
+138
View File
@@ -23,6 +23,9 @@
#include <vector> #include <vector>
#include "vestige.h" #include "vestige.h"
#include "AudioEngine.h"
#include <atomic>
namespace { namespace {
@@ -45,6 +48,8 @@ struct Instance
int32 handle = 0; int32 handle = 0;
SF_ParamChangedCallback paramCb = nullptr; SF_ParamChangedCallback paramCb = nullptr;
void* paramCbUserdata = nullptr; void* paramCbUserdata = nullptr;
sonicforge::AudioEngine audio; // T12: native audio loop (SPSC + WASAPI)
std::atomic<bool> audioRunning {false};
}; };
std::mutex g_mutex; std::mutex g_mutex;
@@ -116,6 +121,53 @@ VstIntPtr VSTCALLBACK hostAudioMasterImpl (AEffect* effect, int32 opcode, int32
} }
} }
// T12: audio thread callback — effProcessEvents (MIDI drain tu SPSC) +
// processReplacing. Chay tren WASAPI render thread: KHONG lock, KHONG cap phat.
bool vst2AudioProcess (const sonicforge::MidiEvent* events, int32 eventCount,
float* outL, float* outR, int32 frames, void* userdata)
{
Instance* inst = static_cast<Instance*> (userdata);
if (!inst || !inst->effect)
return false;
if (eventCount > 0)
{
int32 n = eventCount < 256 ? eventCount : 256;
VstMidiEvent midi[256];
VstEvent* ptrs[256];
struct VstEventsBuf { int32 numEvents; intptr_t reserved; VstEvent* events[256]; };
VstEventsBuf buf = {};
buf.numEvents = n;
for (int32 i = 0; i < n; ++i)
{
VstMidiEvent& m = midi[i];
std::memset (&m, 0, sizeof (m));
m.type = kVstMidiType;
m.byteSize = static_cast<int32> (sizeof (VstMidiEvent));
m.deltaFrames = 0;
m.flags = kVstMidiEventIsRealtime;
m.midiData[0] = static_cast<char> ((events[i].noteOn ? 0x90 : 0x80) |
(events[i].channel & 0x0F));
m.midiData[1] = static_cast<char> (events[i].pitch & 0x7F);
m.midiData[2] = events[i].noteOn
? static_cast<char> (static_cast<int> (events[i].velocity * 127.0f) & 0x7F)
: 0;
ptrs[i] = reinterpret_cast<VstEvent*> (&m);
buf.events[i] = ptrs[i];
}
inst->effect->dispatcher (inst->effect, effProcessEvents, 0, 0,
reinterpret_cast<VstEvents*> (&buf), 0.0f);
}
if (inst->effect->processReplacing)
{
float* outs[2] = { outL, outR };
inst->effect->processReplacing (inst->effect, nullptr, outs, frames);
return true;
}
return false;
}
} // namespace } // namespace
extern "C" { extern "C" {
@@ -244,6 +296,9 @@ __declspec (dllexport) int32 SF_VST2_Close (int32 handle, char* err, int32 err_c
return -1; return -1;
} }
Instance* inst = it->second.get (); Instance* inst = it->second.get ();
// T12: dung audio loop (join render thread) truoc khi pha huy plugin.
inst->audioRunning.store (false, std::memory_order_release);
inst->audio.stop ();
if (inst->effect) if (inst->effect)
{ {
{ {
@@ -320,6 +375,18 @@ __declspec (dllexport) int32 SF_VST2_SendNoteOn (int32 handle, int32 channel, in
if (!inst->effect) if (!inst->effect)
return -2; return -2;
// T12: audio loop dang chay -> day vao SPSC (audio thread xu ly).
if (inst->audioRunning.load (std::memory_order_acquire))
{
sonicforge::MidiEvent ev = {};
ev.channel = channel;
ev.pitch = pitch;
ev.velocity = static_cast<float> (velocity) / 127.0f;
ev.noteOn = true;
inst->audio.pushMidi (ev);
return 0;
}
char midiData[4] = {0}; char midiData[4] = {0};
midiData[0] = static_cast<char> (0x90 | (channel & 0x0F)); midiData[0] = static_cast<char> (0x90 | (channel & 0x0F));
midiData[1] = static_cast<char> (pitch & 0x7F); midiData[1] = static_cast<char> (pitch & 0x7F);
@@ -352,6 +419,18 @@ __declspec (dllexport) int32 SF_VST2_SendNoteOff (int32 handle, int32 channel, i
if (!inst->effect) if (!inst->effect)
return -2; return -2;
// T12: audio loop dang chay -> day vao SPSC.
if (inst->audioRunning.load (std::memory_order_acquire))
{
sonicforge::MidiEvent ev = {};
ev.channel = channel;
ev.pitch = pitch;
ev.velocity = 0.0f;
ev.noteOn = false;
inst->audio.pushMidi (ev);
return 0;
}
char midiData[4] = {0}; char midiData[4] = {0};
midiData[0] = static_cast<char> (0x80 | (channel & 0x0F)); midiData[0] = static_cast<char> (0x80 | (channel & 0x0F));
midiData[1] = static_cast<char> (pitch & 0x7F); midiData[1] = static_cast<char> (pitch & 0x7F);
@@ -464,4 +543,63 @@ __declspec (dllexport) int32 SF_VST2_GetParam (int32 handle, int32 paramIndex, d
return 0; return 0;
} }
// T12: bat native audio loop (SPSC + WASAPI) cho instance nay. Audio thread
// goi vst2AudioProcess (effProcessEvents + processReplacing) moi block.
__declspec (dllexport) int32 SF_VST2_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->effect)
return -2;
inst->audioRunning.store (true, std::memory_order_release); // truoc khi thread chay
if (!inst->audio.start (sampleRate, blockSize, &vst2AudioProcess, inst, err, err_cap))
{
inst->audioRunning.store (false, std::memory_order_release);
return -3;
}
return 0;
}
__declspec (dllexport) int32 SF_VST2_AudioStop (int32 handle)
{
std::lock_guard<std::mutex> lock (g_mutex);
auto it = g_instances.find (handle);
if (it == g_instances.end ())
return -1;
it->second->audioRunning.store (false, std::memory_order_release);
it->second->audio.stop ();
return 0;
}
__declspec (dllexport) int32 SF_VST2_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_VST2_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_VST2_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 ();
}
} // extern "C" } // extern "C"
+223
View File
@@ -30,6 +30,11 @@
#include "pluginterfaces/vst/ivstaudioprocessor.h" #include "pluginterfaces/vst/ivstaudioprocessor.h"
#include "pluginterfaces/vst/ivstcomponent.h" #include "pluginterfaces/vst/ivstcomponent.h"
#include "pluginterfaces/vst/ivsteditcontroller.h" #include "pluginterfaces/vst/ivsteditcontroller.h"
#include "pluginterfaces/vst/ivstevents.h"
#include "AudioEngine.h"
#include <atomic>
using namespace Steinberg; using namespace Steinberg;
using namespace Steinberg::Vst; using namespace Steinberg::Vst;
@@ -87,11 +92,106 @@ struct Instance
{ {
VST3::Hosting::Module::Ptr module; VST3::Hosting::Module::Ptr module;
IPtr<PlugProvider> plugProvider; IPtr<PlugProvider> plugProvider;
IPtr<IComponent> component;
IPtr<IAudioProcessor> processor;
IPtr<IEditController> controller; IPtr<IEditController> controller;
IPtr<IPlugView> view; IPtr<IPlugView> view;
ComponentHandler handler; ComponentHandler handler;
sonicforge::AudioEngine audio; // T12: native audio loop (SPSC + WASAPI)
std::atomic<bool> audioRunning {false};
}; };
// 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;
return proc->process (data) == kResultOk;
}
std::mutex g_mutex; std::mutex g_mutex;
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;
@@ -160,6 +260,22 @@ __declspec (dllexport) int32 SF_VST3_Attach (const char* module_path_utf8,
return 0; 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 IEditController* rawController = inst->plugProvider->getController (); // +1 ref
if (!rawController) if (!rawController)
{ {
@@ -207,6 +323,11 @@ __declspec (dllexport) int32 SF_VST3_Close (int32 handle, char* err, int32 err_c
set_err (err, err_cap, "bad handle"); set_err (err, err_cap, "bad handle");
return -1; 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) if (it->second->view)
{ {
it->second->view->removed (); it->second->view->removed ();
@@ -324,4 +445,106 @@ __declspec (dllexport) int32 SF_VST3_GetParamInfo (int32 handle, int32 index,
return 0; 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;
}
} // extern "C" } // extern "C"