2026-03-20 20:48:09 +08:00
|
|
|
#ifdef _WIN32
|
|
|
|
|
#define NOMINMAX
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-03-24 16:14:05 +08:00
|
|
|
#include <XCEngine/Audio/WindowsAudioBackend.h>
|
2026-03-20 20:31:24 +08:00
|
|
|
#include <iostream>
|
2026-03-20 20:48:09 +08:00
|
|
|
#include <algorithm>
|
2026-03-20 20:31:24 +08:00
|
|
|
|
|
|
|
|
namespace XCEngine {
|
|
|
|
|
namespace Audio {
|
2026-04-14 16:30:02 +08:00
|
|
|
namespace WaveOut {
|
2026-03-20 20:31:24 +08:00
|
|
|
|
2026-04-08 16:09:15 +08:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
std::string ConvertWaveOutDeviceName(const TCHAR* deviceName) {
|
|
|
|
|
if (deviceName == nullptr) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if defined(UNICODE) || defined(_UNICODE)
|
|
|
|
|
const int requiredBytes = WideCharToMultiByte(
|
|
|
|
|
CP_UTF8,
|
|
|
|
|
0,
|
|
|
|
|
deviceName,
|
|
|
|
|
-1,
|
|
|
|
|
nullptr,
|
|
|
|
|
0,
|
|
|
|
|
nullptr,
|
|
|
|
|
nullptr);
|
|
|
|
|
if (requiredBytes <= 1) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string utf8Name(static_cast<std::size_t>(requiredBytes - 1), '\0');
|
|
|
|
|
WideCharToMultiByte(
|
|
|
|
|
CP_UTF8,
|
|
|
|
|
0,
|
|
|
|
|
deviceName,
|
|
|
|
|
-1,
|
|
|
|
|
utf8Name.data(),
|
|
|
|
|
requiredBytes,
|
|
|
|
|
nullptr,
|
|
|
|
|
nullptr);
|
|
|
|
|
return utf8Name;
|
|
|
|
|
#else
|
|
|
|
|
return std::string(deviceName);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
WaveOutBackend::WaveOutBackend()
|
2026-03-20 20:31:24 +08:00
|
|
|
: m_audioBuffer1(BufferSize * 2)
|
|
|
|
|
, m_audioBuffer2(BufferSize * 2)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
WaveOutBackend::~WaveOutBackend() {
|
2026-03-20 20:31:24 +08:00
|
|
|
Shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
bool WaveOutBackend::Initialize(const AudioConfig& config) {
|
2026-03-20 20:31:24 +08:00
|
|
|
m_config = config;
|
2026-04-14 16:30:02 +08:00
|
|
|
const size_t bufferSampleCount = static_cast<size_t>(config.bufferSize) * config.channels;
|
|
|
|
|
m_audioBuffer1.assign(bufferSampleCount, 0);
|
|
|
|
|
m_audioBuffer2.assign(bufferSampleCount, 0);
|
2026-04-14 19:04:18 +08:00
|
|
|
m_renderBuffer.assign(bufferSampleCount, 0.0f);
|
2026-04-14 16:30:02 +08:00
|
|
|
m_buffer1Available = true;
|
|
|
|
|
m_buffer2Available = true;
|
2026-03-20 20:31:24 +08:00
|
|
|
|
|
|
|
|
m_waveFormat.wFormatTag = WAVE_FORMAT_PCM;
|
|
|
|
|
m_waveFormat.nChannels = static_cast<WORD>(config.channels);
|
|
|
|
|
m_waveFormat.nSamplesPerSec = config.sampleRate;
|
|
|
|
|
m_waveFormat.nAvgBytesPerSec = config.sampleRate * config.channels * (config.bitsPerSample / 8);
|
|
|
|
|
m_waveFormat.nBlockAlign = static_cast<WORD>(config.channels * (config.bitsPerSample / 8));
|
|
|
|
|
m_waveFormat.wBitsPerSample = config.bitsPerSample;
|
|
|
|
|
m_waveFormat.cbSize = 0;
|
|
|
|
|
|
|
|
|
|
MMRESULT result = InitDevice();
|
|
|
|
|
if (result != MMSYSERR_NOERROR) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result = InitBuffer();
|
|
|
|
|
if (result != MMSYSERR_NOERROR) {
|
|
|
|
|
waveOutClose(m_hWaveOut);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
void WaveOutBackend::Shutdown() {
|
2026-03-20 20:31:24 +08:00
|
|
|
if (m_isRunning.load()) {
|
|
|
|
|
Stop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_hWaveOut != nullptr) {
|
|
|
|
|
waveOutUnprepareHeader(m_hWaveOut, &m_waveHeader1, sizeof(WAVEHDR));
|
|
|
|
|
waveOutUnprepareHeader(m_hWaveOut, &m_waveHeader2, sizeof(WAVEHDR));
|
|
|
|
|
waveOutClose(m_hWaveOut);
|
|
|
|
|
m_hWaveOut = nullptr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
std::string WaveOutBackend::GetDeviceName() const {
|
2026-03-20 20:31:24 +08:00
|
|
|
return m_deviceName;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
void WaveOutBackend::GetAvailableDevices(std::vector<std::string>& devices) {
|
2026-03-20 20:31:24 +08:00
|
|
|
devices.clear();
|
|
|
|
|
for (const auto& caps : m_waveOutCaps) {
|
2026-04-08 16:09:15 +08:00
|
|
|
devices.push_back(ConvertWaveOutDeviceName(caps.szPname));
|
2026-03-20 20:31:24 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
bool WaveOutBackend::SetDevice(const std::string& deviceName) {
|
|
|
|
|
const bool useDefaultDevice = deviceName.empty() || deviceName == "Default Device";
|
|
|
|
|
const std::string requestedName = useDefaultDevice ? "Default Device" : deviceName;
|
|
|
|
|
|
|
|
|
|
if (m_hWaveOut != nullptr || m_isRunning.load()) {
|
|
|
|
|
return requestedName == m_deviceName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (useDefaultDevice) {
|
|
|
|
|
m_requestedDeviceName.clear();
|
|
|
|
|
m_deviceName = "Default Device";
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 20:31:24 +08:00
|
|
|
for (UINT i = 0; i < waveOutGetNumDevs(); ++i) {
|
|
|
|
|
WAVEOUTCAPS caps;
|
|
|
|
|
waveOutGetDevCaps(i, &caps, sizeof(WAVEOUTCAPS));
|
2026-04-08 16:09:15 +08:00
|
|
|
if (deviceName == ConvertWaveOutDeviceName(caps.szPname)) {
|
2026-04-14 16:30:02 +08:00
|
|
|
m_requestedDeviceName = deviceName;
|
2026-03-20 20:31:24 +08:00
|
|
|
m_deviceName = deviceName;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
void WaveOutBackend::Start() {
|
2026-03-20 20:31:24 +08:00
|
|
|
if (m_isRunning.load()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_isRunning = true;
|
2026-04-14 16:30:02 +08:00
|
|
|
m_audioThread = std::thread(&WaveOutBackend::AudioThreadProc, this);
|
2026-03-20 20:31:24 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
void WaveOutBackend::Stop() {
|
2026-03-20 20:31:24 +08:00
|
|
|
m_isRunning = false;
|
2026-04-14 19:04:18 +08:00
|
|
|
m_bufferReadyCond.notify_all();
|
2026-04-14 16:30:02 +08:00
|
|
|
|
|
|
|
|
if (m_hWaveOut != nullptr) {
|
|
|
|
|
waveOutReset(m_hWaveOut);
|
|
|
|
|
}
|
2026-03-20 20:31:24 +08:00
|
|
|
|
|
|
|
|
if (m_audioThread.joinable()) {
|
|
|
|
|
m_audioThread.join();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
void WaveOutBackend::Suspend() {
|
2026-03-20 20:31:24 +08:00
|
|
|
if (m_hWaveOut != nullptr) {
|
|
|
|
|
waveOutReset(m_hWaveOut);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
void WaveOutBackend::Resume() {
|
2026-03-20 20:31:24 +08:00
|
|
|
if (m_hWaveOut != nullptr) {
|
|
|
|
|
MMRESULT result = waveOutRestart(m_hWaveOut);
|
|
|
|
|
if (result != MMSYSERR_NOERROR) {
|
|
|
|
|
std::cout << "Failed to resume audio playback" << std::endl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
void WaveOutBackend::ProcessAudio(float* buffer, uint32 frameCount,
|
2026-03-20 20:31:24 +08:00
|
|
|
uint32 channels, uint32 sampleRate) {
|
2026-04-14 19:04:18 +08:00
|
|
|
(void)buffer;
|
|
|
|
|
(void)frameCount;
|
|
|
|
|
(void)channels;
|
2026-04-14 16:30:02 +08:00
|
|
|
(void)sampleRate;
|
2026-04-14 19:04:18 +08:00
|
|
|
}
|
2026-03-20 20:31:24 +08:00
|
|
|
|
2026-04-14 19:04:18 +08:00
|
|
|
void WaveOutBackend::SetRenderCallback(RenderCallback callback) {
|
2026-03-22 13:00:10 +08:00
|
|
|
std::lock_guard<std::mutex> lock(m_bufferMutex);
|
2026-04-14 19:04:18 +08:00
|
|
|
m_renderCallback = std::move(callback);
|
2026-03-20 20:31:24 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
MMRESULT WaveOutBackend::InitDevice() {
|
|
|
|
|
m_waveOutCaps.clear();
|
2026-03-20 20:31:24 +08:00
|
|
|
WAVEOUTCAPS waveOutCapsTemp;
|
|
|
|
|
for (UINT i = 0; i < waveOutGetNumDevs(); ++i) {
|
|
|
|
|
waveOutGetDevCaps(i, &waveOutCapsTemp, sizeof(WAVEOUTCAPS));
|
|
|
|
|
m_waveOutCaps.push_back(waveOutCapsTemp);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
UINT deviceId = WAVE_MAPPER;
|
|
|
|
|
if (!m_requestedDeviceName.empty()) {
|
|
|
|
|
bool foundRequestedDevice = false;
|
|
|
|
|
for (UINT i = 0; i < m_waveOutCaps.size(); ++i) {
|
|
|
|
|
if (m_requestedDeviceName == ConvertWaveOutDeviceName(m_waveOutCaps[i].szPname)) {
|
|
|
|
|
deviceId = i;
|
|
|
|
|
foundRequestedDevice = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!foundRequestedDevice) {
|
|
|
|
|
std::cout << "Requested audio device is no longer available" << std::endl;
|
|
|
|
|
return MMSYSERR_BADDEVICEID;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MMRESULT result = waveOutOpen(&m_hWaveOut, deviceId, &m_waveFormat,
|
|
|
|
|
(DWORD_PTR)&WaveOutBackend::StaticAudioCallback,
|
2026-03-20 20:31:24 +08:00
|
|
|
reinterpret_cast<DWORD_PTR>(this), CALLBACK_FUNCTION);
|
|
|
|
|
if (result != MMSYSERR_NOERROR) {
|
|
|
|
|
std::cout << "Failed to open audio device" << std::endl;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
m_deviceName = m_requestedDeviceName.empty() ? "Default Device" : m_requestedDeviceName;
|
2026-03-20 20:31:24 +08:00
|
|
|
return MMSYSERR_NOERROR;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
MMRESULT WaveOutBackend::InitBuffer() {
|
2026-03-20 20:31:24 +08:00
|
|
|
m_waveHeader1.lpData = (LPSTR)m_audioBuffer1.data();
|
|
|
|
|
m_waveHeader1.dwBufferLength = static_cast<DWORD>(m_audioBuffer1.size() * sizeof(int16_t));
|
|
|
|
|
m_waveHeader1.dwFlags = 0;
|
|
|
|
|
|
|
|
|
|
MMRESULT result = waveOutPrepareHeader(m_hWaveOut, &m_waveHeader1, sizeof(WAVEHDR));
|
|
|
|
|
if (result != MMSYSERR_NOERROR) {
|
|
|
|
|
std::cout << "Failed to prepare audio header1" << std::endl;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_waveHeader2.lpData = (LPSTR)m_audioBuffer2.data();
|
|
|
|
|
m_waveHeader2.dwBufferLength = static_cast<DWORD>(m_audioBuffer2.size() * sizeof(int16_t));
|
|
|
|
|
m_waveHeader2.dwFlags = 0;
|
|
|
|
|
|
|
|
|
|
result = waveOutPrepareHeader(m_hWaveOut, &m_waveHeader2, sizeof(WAVEHDR));
|
|
|
|
|
if (result != MMSYSERR_NOERROR) {
|
|
|
|
|
std::cout << "Failed to prepare audio header2" << std::endl;
|
|
|
|
|
waveOutUnprepareHeader(m_hWaveOut, &m_waveHeader1, sizeof(WAVEHDR));
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return MMSYSERR_NOERROR;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
DWORD WINAPI WaveOutBackend::AudioThreadProc(LPVOID lpParameter) {
|
|
|
|
|
WaveOutBackend* backend = static_cast<WaveOutBackend*>(lpParameter);
|
2026-03-20 20:31:24 +08:00
|
|
|
if (backend) {
|
|
|
|
|
backend->AudioThread();
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
void WaveOutBackend::AudioThread() {
|
2026-03-20 20:31:24 +08:00
|
|
|
while (m_isRunning.load()) {
|
2026-03-22 13:00:10 +08:00
|
|
|
std::unique_lock<std::mutex> lock(m_bufferMutex);
|
2026-04-14 19:04:18 +08:00
|
|
|
m_bufferReadyCond.wait(lock, [this] {
|
|
|
|
|
return !m_isRunning.load() || m_buffer1Available || m_buffer2Available;
|
2026-04-14 16:30:02 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!m_isRunning.load()) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WAVEHDR* targetHeader = nullptr;
|
|
|
|
|
std::vector<int16_t>* targetBuffer = nullptr;
|
|
|
|
|
|
|
|
|
|
if (m_buffer1Available) {
|
|
|
|
|
targetHeader = &m_waveHeader1;
|
|
|
|
|
targetBuffer = &m_audioBuffer1;
|
|
|
|
|
m_buffer1Available = false;
|
|
|
|
|
} else if (m_buffer2Available) {
|
|
|
|
|
targetHeader = &m_waveHeader2;
|
|
|
|
|
targetBuffer = &m_audioBuffer2;
|
|
|
|
|
m_buffer2Available = false;
|
|
|
|
|
} else {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-03-22 13:00:10 +08:00
|
|
|
|
2026-04-14 19:04:18 +08:00
|
|
|
RenderCallback renderCallback;
|
|
|
|
|
const uint32 frameCount = m_config.bufferSize;
|
|
|
|
|
const uint32 channels = m_config.channels;
|
|
|
|
|
const uint32 sampleRate = m_config.sampleRate;
|
|
|
|
|
const size_t sampleCount = static_cast<size_t>(frameCount) * channels;
|
|
|
|
|
|
|
|
|
|
if (targetBuffer->size() != sampleCount) {
|
|
|
|
|
targetBuffer->assign(sampleCount, 0);
|
|
|
|
|
} else {
|
|
|
|
|
std::fill(targetBuffer->begin(), targetBuffer->end(), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_renderBuffer.size() != sampleCount) {
|
|
|
|
|
m_renderBuffer.assign(sampleCount, 0.0f);
|
|
|
|
|
} else {
|
|
|
|
|
std::fill(m_renderBuffer.begin(), m_renderBuffer.end(), 0.0f);
|
|
|
|
|
}
|
|
|
|
|
renderCallback = m_renderCallback;
|
2026-04-14 16:30:02 +08:00
|
|
|
lock.unlock();
|
|
|
|
|
|
2026-04-14 19:04:18 +08:00
|
|
|
if (renderCallback && !m_renderBuffer.empty()) {
|
|
|
|
|
renderCallback(m_renderBuffer.data(), frameCount, channels, sampleRate);
|
|
|
|
|
}
|
|
|
|
|
FillPcm16Buffer(*targetBuffer, m_renderBuffer);
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
if (SubmitBuffer(*targetHeader, *targetBuffer) != MMSYSERR_NOERROR) {
|
|
|
|
|
std::lock_guard<std::mutex> restoreLock(m_bufferMutex);
|
|
|
|
|
if (targetHeader == &m_waveHeader1) {
|
|
|
|
|
m_buffer1Available = true;
|
|
|
|
|
} else {
|
|
|
|
|
m_buffer2Available = true;
|
|
|
|
|
}
|
2026-04-14 19:04:18 +08:00
|
|
|
m_bufferReadyCond.notify_one();
|
2026-03-22 13:00:10 +08:00
|
|
|
}
|
2026-03-20 20:31:24 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
void WaveOutBackend::OnAudioCallback(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance,
|
2026-03-20 20:31:24 +08:00
|
|
|
DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
|
2026-04-14 16:30:02 +08:00
|
|
|
(void)hwo;
|
|
|
|
|
(void)dwInstance;
|
|
|
|
|
(void)dwParam2;
|
|
|
|
|
|
2026-03-20 20:31:24 +08:00
|
|
|
if (uMsg == WOM_DONE) {
|
2026-04-14 16:30:02 +08:00
|
|
|
std::lock_guard<std::mutex> lock(m_bufferMutex);
|
|
|
|
|
WAVEHDR* completedHeader = reinterpret_cast<WAVEHDR*>(dwParam1);
|
|
|
|
|
if (completedHeader == &m_waveHeader1) {
|
|
|
|
|
m_buffer1Available = true;
|
|
|
|
|
} else if (completedHeader == &m_waveHeader2) {
|
|
|
|
|
m_buffer2Available = true;
|
|
|
|
|
}
|
2026-04-14 19:04:18 +08:00
|
|
|
m_bufferReadyCond.notify_one();
|
2026-03-20 20:31:24 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
void CALLBACK WaveOutBackend::StaticAudioCallback(HWAVEOUT hwo, UINT uMsg,
|
2026-03-20 20:31:24 +08:00
|
|
|
DWORD_PTR dwInstance,
|
|
|
|
|
DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
|
2026-04-14 16:30:02 +08:00
|
|
|
WaveOutBackend* backend = reinterpret_cast<WaveOutBackend*>(dwInstance);
|
2026-03-20 20:31:24 +08:00
|
|
|
if (backend != nullptr) {
|
|
|
|
|
backend->OnAudioCallback(hwo, uMsg, dwInstance, dwParam1, dwParam2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
MMRESULT WaveOutBackend::SubmitBuffer(WAVEHDR& header, std::vector<int16_t>& samples) {
|
|
|
|
|
header.lpData = reinterpret_cast<LPSTR>(samples.data());
|
|
|
|
|
header.dwBufferLength = static_cast<DWORD>(samples.size() * sizeof(int16_t));
|
|
|
|
|
header.dwFlags &= ~WHDR_DONE;
|
|
|
|
|
|
|
|
|
|
MMRESULT result = waveOutWrite(m_hWaveOut, &header, sizeof(WAVEHDR));
|
2026-03-20 20:31:24 +08:00
|
|
|
if (result != MMSYSERR_NOERROR) {
|
|
|
|
|
std::cout << "Failed to write audio data" << std::endl;
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:30:02 +08:00
|
|
|
} // namespace WaveOut
|
2026-03-20 20:31:24 +08:00
|
|
|
} // namespace Audio
|
|
|
|
|
} // namespace XCEngine
|