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