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