T12: native audio loop SPSC + WASAPI (exclusive/shared, zero-lock audio thread, SendNote -> SPSC)
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
#include <vector>
|
||||
|
||||
#include "vestige.h"
|
||||
#include "AudioEngine.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
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<bool> 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<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
|
||||
|
||||
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<float> (velocity) / 127.0f;
|
||||
ev.noteOn = true;
|
||||
inst->audio.pushMidi (ev);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char midiData[4] = {0};
|
||||
midiData[0] = static_cast<char> (0x90 | (channel & 0x0F));
|
||||
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)
|
||||
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<char> (0x80 | (channel & 0x0F));
|
||||
midiData[1] = static_cast<char> (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<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"
|
||||
|
||||
@@ -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 <atomic>
|
||||
|
||||
using namespace Steinberg;
|
||||
using namespace Steinberg::Vst;
|
||||
@@ -87,11 +92,106 @@ 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};
|
||||
};
|
||||
|
||||
// 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::map<int32, std::unique_ptr<Instance>> 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<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)
|
||||
{
|
||||
@@ -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<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"
|
||||
|
||||
Reference in New Issue
Block a user