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 {
|
|
|
|
|
namespace WASAPI {
|
|
|
|
|
|
|
|
|
|
WASAPIBackend::WASAPIBackend()
|
|
|
|
|
: m_audioBuffer1(BufferSize * 2)
|
|
|
|
|
, m_audioBuffer2(BufferSize * 2)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WASAPIBackend::~WASAPIBackend() {
|
|
|
|
|
Shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool WASAPIBackend::Initialize(const AudioConfig& config) {
|
|
|
|
|
m_config = config;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::Shutdown() {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string WASAPIBackend::GetDeviceName() const {
|
|
|
|
|
return m_deviceName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::GetAvailableDevices(std::vector<std::string>& devices) {
|
|
|
|
|
devices.clear();
|
|
|
|
|
for (const auto& caps : m_waveOutCaps) {
|
|
|
|
|
devices.push_back(caps.szPname);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool WASAPIBackend::SetDevice(const std::string& deviceName) {
|
|
|
|
|
for (UINT i = 0; i < waveOutGetNumDevs(); ++i) {
|
|
|
|
|
WAVEOUTCAPS caps;
|
|
|
|
|
waveOutGetDevCaps(i, &caps, sizeof(WAVEOUTCAPS));
|
|
|
|
|
if (deviceName == caps.szPname) {
|
|
|
|
|
m_deviceName = deviceName;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float WASAPIBackend::GetMasterVolume() const {
|
|
|
|
|
return m_masterVolume.load();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::SetMasterVolume(float volume) {
|
|
|
|
|
m_masterVolume.store(std::max(0.0f, std::min(1.0f, volume)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool WASAPIBackend::IsMuted() const {
|
|
|
|
|
return m_muted.load();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::SetMuted(bool muted) {
|
|
|
|
|
m_muted.store(muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::Start() {
|
|
|
|
|
if (m_isRunning.load()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_isRunning = true;
|
|
|
|
|
m_audioThread = std::thread(&WASAPIBackend::AudioThreadProc, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::Stop() {
|
|
|
|
|
m_isRunning = false;
|
|
|
|
|
|
|
|
|
|
if (m_audioThread.joinable()) {
|
|
|
|
|
m_audioThread.join();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::Suspend() {
|
|
|
|
|
if (m_hWaveOut != nullptr) {
|
|
|
|
|
waveOutReset(m_hWaveOut);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::Resume() {
|
|
|
|
|
if (m_hWaveOut != nullptr) {
|
|
|
|
|
MMRESULT result = waveOutRestart(m_hWaveOut);
|
|
|
|
|
if (result != MMSYSERR_NOERROR) {
|
|
|
|
|
std::cout << "Failed to resume audio playback" << std::endl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::ProcessAudio(float* buffer, uint32 bufferSize,
|
|
|
|
|
uint32 channels, uint32 sampleRate) {
|
|
|
|
|
if (m_muted.load() || buffer == nullptr) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float volume = m_masterVolume.load();
|
|
|
|
|
if (volume < 0.001f) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32 sampleCount = bufferSize / sizeof(float);
|
2026-03-22 13:00:10 +08:00
|
|
|
int16_t* backBuffer = m_isBuffer1Front ? m_audioBuffer2.data() : m_audioBuffer1.data();
|
|
|
|
|
uint32 bufferSamples = static_cast<uint32>(m_audioBuffer1.size());
|
|
|
|
|
|
|
|
|
|
for (uint32 i = 0; i < sampleCount && i < bufferSamples; ++i) {
|
|
|
|
|
float sample = buffer[i] * volume;
|
|
|
|
|
sample = std::max(-1.0f, std::min(1.0f, sample));
|
|
|
|
|
backBuffer[i] = static_cast<int16_t>(sample * 32767.0f);
|
2026-03-20 20:31:24 +08:00
|
|
|
}
|
2026-03-22 13:00:10 +08:00
|
|
|
|
|
|
|
|
std::lock_guard<std::mutex> lock(m_bufferMutex);
|
|
|
|
|
m_dataReady = true;
|
|
|
|
|
m_dataReadyCond.notify_one();
|
2026-03-20 20:31:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MMRESULT WASAPIBackend::InitDevice() {
|
|
|
|
|
WAVEOUTCAPS waveOutCapsTemp;
|
|
|
|
|
for (UINT i = 0; i < waveOutGetNumDevs(); ++i) {
|
|
|
|
|
waveOutGetDevCaps(i, &waveOutCapsTemp, sizeof(WAVEOUTCAPS));
|
|
|
|
|
m_waveOutCaps.push_back(waveOutCapsTemp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MMRESULT result = waveOutOpen(&m_hWaveOut, WAVE_MAPPER, &m_waveFormat,
|
|
|
|
|
(DWORD_PTR)&WASAPIBackend::StaticAudioCallback,
|
|
|
|
|
reinterpret_cast<DWORD_PTR>(this), CALLBACK_FUNCTION);
|
|
|
|
|
if (result != MMSYSERR_NOERROR) {
|
|
|
|
|
std::cout << "Failed to open audio device" << std::endl;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_deviceName = "Default Device";
|
|
|
|
|
return MMSYSERR_NOERROR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MMRESULT WASAPIBackend::InitBuffer() {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DWORD WINAPI WASAPIBackend::AudioThreadProc(LPVOID lpParameter) {
|
|
|
|
|
WASAPIBackend* backend = static_cast<WASAPIBackend*>(lpParameter);
|
|
|
|
|
if (backend) {
|
|
|
|
|
backend->AudioThread();
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::AudioThread() {
|
|
|
|
|
PlayFrontData();
|
|
|
|
|
SwapBuffer();
|
|
|
|
|
PlayFrontData();
|
|
|
|
|
|
|
|
|
|
while (m_isRunning.load()) {
|
2026-03-22 13:00:10 +08:00
|
|
|
std::unique_lock<std::mutex> lock(m_bufferMutex);
|
|
|
|
|
m_dataReadyCond.wait_for(lock, std::chrono::milliseconds(10), [this] { return m_dataReady || !m_isRunning.load(); });
|
|
|
|
|
|
|
|
|
|
if (m_dataReady) {
|
|
|
|
|
PrepareBackData();
|
|
|
|
|
SwapBuffer();
|
|
|
|
|
PlayFrontData();
|
|
|
|
|
m_dataReady = false;
|
|
|
|
|
}
|
2026-03-20 20:31:24 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::OnAudioCallback(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance,
|
|
|
|
|
DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
|
|
|
|
|
if (uMsg == WOM_DONE) {
|
|
|
|
|
PrepareBackData();
|
|
|
|
|
SwapBuffer();
|
|
|
|
|
PlayFrontData();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CALLBACK WASAPIBackend::StaticAudioCallback(HWAVEOUT hwo, UINT uMsg,
|
|
|
|
|
DWORD_PTR dwInstance,
|
|
|
|
|
DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
|
|
|
|
|
WASAPIBackend* backend = reinterpret_cast<WASAPIBackend*>(dwInstance);
|
|
|
|
|
if (backend != nullptr) {
|
|
|
|
|
backend->OnAudioCallback(hwo, uMsg, dwInstance, dwParam1, dwParam2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MMRESULT WASAPIBackend::PlayFrontData() {
|
|
|
|
|
WAVEHDR* frontHeader = m_isBuffer1Front ? &m_waveHeader1 : &m_waveHeader2;
|
|
|
|
|
MMRESULT result = waveOutWrite(m_hWaveOut, frontHeader, sizeof(WAVEHDR));
|
|
|
|
|
if (result != MMSYSERR_NOERROR) {
|
|
|
|
|
std::cout << "Failed to write audio data" << std::endl;
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::PrepareBackData() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WASAPIBackend::SwapBuffer() {
|
|
|
|
|
m_isBuffer1Front = !m_isBuffer1Front;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace WASAPI
|
|
|
|
|
} // namespace Audio
|
|
|
|
|
} // namespace XCEngine
|