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
+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