From 8f1e89ce50dbae058bc1740d08d0639dd161251a Mon Sep 17 00:00:00 2001 From: locphamtran Date: Tue, 11 Aug 2026 16:40:30 +0700 Subject: [PATCH] T12: native audio loop SPSC + WASAPI (exclusive/shared, zero-lock audio thread, SendNote -> SPSC) --- native_host/AudioEngine.cpp | 343 +++++++++++++++++++++++++++++++ native_host/AudioEngine.h | 109 ++++++++++ native_host/CMakeLists.txt | 15 ++ native_host/VST2AudioEngine.cpp | 138 +++++++++++++ native_host/vst3_host_bridge.cpp | 223 ++++++++++++++++++++ 5 files changed, 828 insertions(+) create mode 100644 native_host/AudioEngine.cpp create mode 100644 native_host/AudioEngine.h diff --git a/native_host/AudioEngine.cpp b/native_host/AudioEngine.cpp new file mode 100644 index 0000000..e0ac106 --- /dev/null +++ b/native_host/AudioEngine.cpp @@ -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 +#include + +#include +#include +#include + +namespace sonicforge { + +namespace { + +void set_err (char* err, int32 err_cap, const char* msg) +{ + if (err && err_cap > 0) + std::snprintf (err, static_cast (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 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 (&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 (sampleRate); + fmt.wBitsPerSample = 32; + fmt.nBlockAlign = 8; + fmt.nAvgBytesPerSec = static_cast (sampleRate) * 8; + + // 100ns units: 1e7 * seconds + REFERENCE_TIME hnsPeriod = + static_cast (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 (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 (err_cap), + "WASAPI Initialize failed: 0x%08X", static_cast (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 (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 (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 (frames), m_userdata) + : false; + + float* inter = reinterpret_cast (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 diff --git a/native_host/AudioEngine.h b/native_host/AudioEngine.h new file mode 100644 index 0000000..53c53cd --- /dev/null +++ b/native_host/AudioEngine.h @@ -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 + +#include +#include +#include + +#include +#include + +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 m_head {0}; // producer index (next write) + std::atomic m_tail {0}; // consumer index (next read) + std::mutex m_producerMutex; + MidiEvent m_drained[kMaxEventsPerBlock]; + + std::atomic m_running {false}; + std::atomic m_underruns {0}; + std::atomic m_latency {0}; + std::atomic 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 diff --git a/native_host/CMakeLists.txt b/native_host/CMakeLists.txt index 7906a98..1983a0e 100644 --- a/native_host/CMakeLists.txt +++ b/native_host/CMakeLists.txt @@ -32,6 +32,20 @@ target_compile_definitions(vst3_host_bridge PRIVATE NOMINMAX 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. add_library(vst2_host_bridge SHARED @@ -43,3 +57,4 @@ target_compile_definitions(vst2_host_bridge PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN ) +target_link_libraries(vst2_host_bridge PRIVATE audio_engine) diff --git a/native_host/VST2AudioEngine.cpp b/native_host/VST2AudioEngine.cpp index da0e3d0..fd4c3b8 100644 --- a/native_host/VST2AudioEngine.cpp +++ b/native_host/VST2AudioEngine.cpp @@ -23,6 +23,9 @@ #include #include "vestige.h" +#include "AudioEngine.h" + +#include namespace { @@ -45,6 +48,8 @@ struct Instance int32 handle = 0; SF_ParamChangedCallback paramCb = nullptr; void* paramCbUserdata = nullptr; + sonicforge::AudioEngine audio; // T12: native audio loop (SPSC + WASAPI) + std::atomic audioRunning {false}; }; 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 (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 (sizeof (VstMidiEvent)); + m.deltaFrames = 0; + m.flags = kVstMidiEventIsRealtime; + m.midiData[0] = static_cast ((events[i].noteOn ? 0x90 : 0x80) | + (events[i].channel & 0x0F)); + m.midiData[1] = static_cast (events[i].pitch & 0x7F); + m.midiData[2] = events[i].noteOn + ? static_cast (static_cast (events[i].velocity * 127.0f) & 0x7F) + : 0; + ptrs[i] = reinterpret_cast (&m); + buf.events[i] = ptrs[i]; + } + inst->effect->dispatcher (inst->effect, effProcessEvents, 0, 0, + reinterpret_cast (&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 extern "C" { @@ -244,6 +296,9 @@ __declspec (dllexport) int32 SF_VST2_Close (int32 handle, char* err, int32 err_c return -1; } 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) { { @@ -320,6 +375,18 @@ __declspec (dllexport) int32 SF_VST2_SendNoteOn (int32 handle, int32 channel, in if (!inst->effect) 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 (velocity) / 127.0f; + ev.noteOn = true; + inst->audio.pushMidi (ev); + return 0; + } + char midiData[4] = {0}; midiData[0] = static_cast (0x90 | (channel & 0x0F)); midiData[1] = static_cast (pitch & 0x7F); @@ -352,6 +419,18 @@ __declspec (dllexport) int32 SF_VST2_SendNoteOff (int32 handle, int32 channel, i if (!inst->effect) 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}; midiData[0] = static_cast (0x80 | (channel & 0x0F)); midiData[1] = static_cast (pitch & 0x7F); @@ -464,4 +543,63 @@ __declspec (dllexport) int32 SF_VST2_GetParam (int32 handle, int32 paramIndex, d 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 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 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 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 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 lock (g_mutex); + auto it = g_instances.find (handle); + if (it == g_instances.end ()) + return -1; + return it->second->audio.blocksRendered (); +} + } // extern "C" diff --git a/native_host/vst3_host_bridge.cpp b/native_host/vst3_host_bridge.cpp index 67eb151..1a32053 100644 --- a/native_host/vst3_host_bridge.cpp +++ b/native_host/vst3_host_bridge.cpp @@ -30,6 +30,11 @@ #include "pluginterfaces/vst/ivstaudioprocessor.h" #include "pluginterfaces/vst/ivstcomponent.h" #include "pluginterfaces/vst/ivsteditcontroller.h" +#include "pluginterfaces/vst/ivstevents.h" + +#include "AudioEngine.h" + +#include using namespace Steinberg; using namespace Steinberg::Vst; @@ -87,11 +92,106 @@ struct Instance { VST3::Hosting::Module::Ptr module; IPtr plugProvider; + IPtr component; + IPtr processor; IPtr controller; IPtr view; ComponentHandler handler; + sonicforge::AudioEngine audio; // T12: native audio loop (SPSC + WASAPI) + std::atomic 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 (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::map> g_instances; int32 g_next_handle = 1; @@ -160,6 +260,22 @@ __declspec (dllexport) int32 SF_VST3_Attach (const char* module_path_utf8, 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 (&rawProc)) != kResultOk || + !rawProc) + { + set_err (err, err_cap, "plugin has no audio processor"); + return 0; + } + inst->processor = IPtr (rawProc); + IEditController* rawController = inst->plugProvider->getController (); // +1 ref 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"); 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 (); @@ -324,4 +445,106 @@ __declspec (dllexport) int32 SF_VST3_GetParamInfo (int32 handle, int32 index, 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 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); + 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 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 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 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 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 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 (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 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"