diff --git a/engine/include/XCEngine/Audio/IAudioBackend.h b/engine/include/XCEngine/Audio/IAudioBackend.h index 6f616c71..68240534 100644 --- a/engine/include/XCEngine/Audio/IAudioBackend.h +++ b/engine/include/XCEngine/Audio/IAudioBackend.h @@ -15,6 +15,8 @@ public: virtual std::string GetDeviceName() const = 0; virtual void GetAvailableDevices(std::vector& devices) = 0; + // Backends may treat SetDevice as a pre-initialize device preference. + // Runtime hot-switch is optional and unsupported backends should return false. virtual bool SetDevice(const std::string& deviceName) = 0; virtual float GetMasterVolume() const = 0; @@ -28,7 +30,7 @@ public: virtual void Suspend() = 0; virtual void Resume() = 0; - virtual void ProcessAudio(float* buffer, uint32 bufferSize, + virtual void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels, uint32 sampleRate) = 0; virtual bool IsRunning() const = 0; diff --git a/engine/include/XCEngine/Audio/WindowsAudioBackend.h b/engine/include/XCEngine/Audio/WindowsAudioBackend.h index 39ee7afa..bdb26876 100644 --- a/engine/include/XCEngine/Audio/WindowsAudioBackend.h +++ b/engine/include/XCEngine/Audio/WindowsAudioBackend.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -17,12 +18,12 @@ namespace XCEngine { namespace Audio { -namespace WASAPI { +namespace WaveOut { -class WASAPIBackend : public IAudioBackend { +class WaveOutBackend : public IAudioBackend { public: - WASAPIBackend(); - ~WASAPIBackend() override; + WaveOutBackend(); + ~WaveOutBackend() override; bool Initialize(const AudioConfig& config) override; void Shutdown() override; @@ -42,15 +43,30 @@ public: void Suspend() override; void Resume() override; - void ProcessAudio(float* buffer, uint32 bufferSize, + void ProcessAudio(float* buffer, uint32 frameCount, uint32 channels, uint32 sampleRate) override; bool IsRunning() const override { return m_isRunning.load(); } AudioConfig GetConfig() const override { return m_config; } private: + static void FillPcm16Buffer(std::vector& output, + const std::vector& input, + float masterVolume, + bool muted) { + const float volume = muted ? 0.0f : std::clamp(masterVolume, 0.0f, 1.0f); + + std::fill(output.begin(), output.end(), 0); + for (size_t i = 0; i < output.size() && i < input.size(); ++i) { + float sample = input[i] * volume; + sample = std::clamp(sample, -1.0f, 1.0f); + output[i] = static_cast(sample * 32767.0f); + } + } + MMRESULT InitDevice(); MMRESULT InitBuffer(); + MMRESULT SubmitBuffer(WAVEHDR& header, std::vector& samples); static DWORD WINAPI AudioThreadProc(LPVOID lpParameter); void AudioThread(); @@ -61,10 +77,6 @@ private: DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2); - MMRESULT PlayFrontData(); - void PrepareBackData(); - void SwapBuffer(); - private: std::atomic m_isRunning{false}; std::thread m_audioThread; @@ -79,6 +91,7 @@ private: std::vector m_waveOutCaps; std::string m_deviceName; + std::string m_requestedDeviceName; std::atomic m_masterVolume{1.0f}; std::atomic m_muted{false}; @@ -86,9 +99,10 @@ private: static constexpr size_t BufferSize = 8192; std::vector m_audioBuffer1; std::vector m_audioBuffer2; - bool m_isBuffer1Front = true; - bool m_isBufferPrepared = false; - bool m_dataReady = false; + std::vector m_pendingMixBuffer; + bool m_hasPendingMix = false; + bool m_buffer1Available = true; + bool m_buffer2Available = true; std::mutex m_bufferMutex; std::condition_variable m_dataReadyCond; @@ -97,6 +111,6 @@ private: WAVEHDR m_waveHeader2 = {}; }; -} // namespace WASAPI +} // namespace WaveOut } // namespace Audio } // namespace XCEngine diff --git a/engine/src/Audio/WindowsAudioBackend.cpp b/engine/src/Audio/WindowsAudioBackend.cpp index 72f8d015..a4ac2753 100644 --- a/engine/src/Audio/WindowsAudioBackend.cpp +++ b/engine/src/Audio/WindowsAudioBackend.cpp @@ -8,7 +8,7 @@ namespace XCEngine { namespace Audio { -namespace WASAPI { +namespace WaveOut { namespace { @@ -49,18 +49,25 @@ std::string ConvertWaveOutDeviceName(const TCHAR* deviceName) { } // namespace -WASAPIBackend::WASAPIBackend() +WaveOutBackend::WaveOutBackend() : m_audioBuffer1(BufferSize * 2) , m_audioBuffer2(BufferSize * 2) { } -WASAPIBackend::~WASAPIBackend() { +WaveOutBackend::~WaveOutBackend() { Shutdown(); } -bool WASAPIBackend::Initialize(const AudioConfig& config) { +bool WaveOutBackend::Initialize(const AudioConfig& config) { m_config = config; + const size_t bufferSampleCount = static_cast(config.bufferSize) * config.channels; + m_audioBuffer1.assign(bufferSampleCount, 0); + m_audioBuffer2.assign(bufferSampleCount, 0); + m_pendingMixBuffer.assign(bufferSampleCount, 0.0f); + m_hasPendingMix = false; + m_buffer1Available = true; + m_buffer2Available = true; m_waveFormat.wFormatTag = WAVE_FORMAT_PCM; m_waveFormat.nChannels = static_cast(config.channels); @@ -84,7 +91,7 @@ bool WASAPIBackend::Initialize(const AudioConfig& config) { return true; } -void WASAPIBackend::Shutdown() { +void WaveOutBackend::Shutdown() { if (m_isRunning.load()) { Stop(); } @@ -97,22 +104,36 @@ void WASAPIBackend::Shutdown() { } } -std::string WASAPIBackend::GetDeviceName() const { +std::string WaveOutBackend::GetDeviceName() const { return m_deviceName; } -void WASAPIBackend::GetAvailableDevices(std::vector& devices) { +void WaveOutBackend::GetAvailableDevices(std::vector& devices) { devices.clear(); for (const auto& caps : m_waveOutCaps) { devices.push_back(ConvertWaveOutDeviceName(caps.szPname)); } } -bool WASAPIBackend::SetDevice(const std::string& deviceName) { +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; + } + for (UINT i = 0; i < waveOutGetNumDevs(); ++i) { WAVEOUTCAPS caps; waveOutGetDevCaps(i, &caps, sizeof(WAVEOUTCAPS)); if (deviceName == ConvertWaveOutDeviceName(caps.szPname)) { + m_requestedDeviceName = deviceName; m_deviceName = deviceName; return true; } @@ -120,46 +141,51 @@ bool WASAPIBackend::SetDevice(const std::string& deviceName) { return false; } -float WASAPIBackend::GetMasterVolume() const { +float WaveOutBackend::GetMasterVolume() const { return m_masterVolume.load(); } -void WASAPIBackend::SetMasterVolume(float volume) { +void WaveOutBackend::SetMasterVolume(float volume) { m_masterVolume.store(std::max(0.0f, std::min(1.0f, volume))); } -bool WASAPIBackend::IsMuted() const { +bool WaveOutBackend::IsMuted() const { return m_muted.load(); } -void WASAPIBackend::SetMuted(bool muted) { +void WaveOutBackend::SetMuted(bool muted) { m_muted.store(muted); } -void WASAPIBackend::Start() { +void WaveOutBackend::Start() { if (m_isRunning.load()) { return; } m_isRunning = true; - m_audioThread = std::thread(&WASAPIBackend::AudioThreadProc, this); + m_audioThread = std::thread(&WaveOutBackend::AudioThreadProc, this); } -void WASAPIBackend::Stop() { +void WaveOutBackend::Stop() { m_isRunning = false; + m_dataReadyCond.notify_all(); + + if (m_hWaveOut != nullptr) { + waveOutReset(m_hWaveOut); + } if (m_audioThread.joinable()) { m_audioThread.join(); } } -void WASAPIBackend::Suspend() { +void WaveOutBackend::Suspend() { if (m_hWaveOut != nullptr) { waveOutReset(m_hWaveOut); } } -void WASAPIBackend::Resume() { +void WaveOutBackend::Resume() { if (m_hWaveOut != nullptr) { MMRESULT result = waveOutRestart(m_hWaveOut); if (result != MMSYSERR_NOERROR) { @@ -168,52 +194,62 @@ void WASAPIBackend::Resume() { } } -void WASAPIBackend::ProcessAudio(float* buffer, uint32 bufferSize, +void WaveOutBackend::ProcessAudio(float* buffer, uint32 frameCount, uint32 channels, uint32 sampleRate) { - if (m_muted.load() || buffer == nullptr) { + (void)sampleRate; + + if (buffer == nullptr) { return; } - float volume = m_masterVolume.load(); - if (volume < 0.001f) { - return; - } - - uint32 sampleCount = bufferSize / sizeof(float); - int16_t* backBuffer = m_isBuffer1Front ? m_audioBuffer2.data() : m_audioBuffer1.data(); - uint32 bufferSamples = static_cast(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(sample * 32767.0f); - } - + const uint32 sampleCount = frameCount * channels; std::lock_guard lock(m_bufferMutex); - m_dataReady = true; + m_pendingMixBuffer.assign(m_pendingMixBuffer.size(), 0.0f); + for (uint32 i = 0; i < sampleCount && i < m_pendingMixBuffer.size(); ++i) { + m_pendingMixBuffer[i] = buffer[i]; + } + m_hasPendingMix = true; m_dataReadyCond.notify_one(); } -MMRESULT WASAPIBackend::InitDevice() { +MMRESULT WaveOutBackend::InitDevice() { + m_waveOutCaps.clear(); 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, + 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, reinterpret_cast(this), CALLBACK_FUNCTION); if (result != MMSYSERR_NOERROR) { std::cout << "Failed to open audio device" << std::endl; return result; } - m_deviceName = "Default Device"; + m_deviceName = m_requestedDeviceName.empty() ? "Default Device" : m_requestedDeviceName; return MMSYSERR_NOERROR; } -MMRESULT WASAPIBackend::InitBuffer() { +MMRESULT WaveOutBackend::InitBuffer() { m_waveHeader1.lpData = (LPSTR)m_audioBuffer1.data(); m_waveHeader1.dwBufferLength = static_cast(m_audioBuffer1.size() * sizeof(int16_t)); m_waveHeader1.dwFlags = 0; @@ -238,66 +274,95 @@ MMRESULT WASAPIBackend::InitBuffer() { return MMSYSERR_NOERROR; } -DWORD WINAPI WASAPIBackend::AudioThreadProc(LPVOID lpParameter) { - WASAPIBackend* backend = static_cast(lpParameter); +DWORD WINAPI WaveOutBackend::AudioThreadProc(LPVOID lpParameter) { + WaveOutBackend* backend = static_cast(lpParameter); if (backend) { backend->AudioThread(); } return 0; } -void WASAPIBackend::AudioThread() { - PlayFrontData(); - SwapBuffer(); - PlayFrontData(); - +void WaveOutBackend::AudioThread() { while (m_isRunning.load()) { std::unique_lock lock(m_bufferMutex); - m_dataReadyCond.wait_for(lock, std::chrono::milliseconds(10), [this] { return m_dataReady || !m_isRunning.load(); }); + m_dataReadyCond.wait(lock, [this] { + return !m_isRunning.load() || + (m_hasPendingMix && (m_buffer1Available || m_buffer2Available)); + }); - if (m_dataReady) { - PrepareBackData(); - SwapBuffer(); - PlayFrontData(); - m_dataReady = false; + if (!m_isRunning.load()) { + break; + } + + WAVEHDR* targetHeader = nullptr; + std::vector* 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; + } + + FillPcm16Buffer(*targetBuffer, m_pendingMixBuffer, m_masterVolume.load(), m_muted.load()); + m_hasPendingMix = false; + lock.unlock(); + + if (SubmitBuffer(*targetHeader, *targetBuffer) != MMSYSERR_NOERROR) { + std::lock_guard restoreLock(m_bufferMutex); + if (targetHeader == &m_waveHeader1) { + m_buffer1Available = true; + } else { + m_buffer2Available = true; + } } } } -void WASAPIBackend::OnAudioCallback(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, +void WaveOutBackend::OnAudioCallback(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { + (void)hwo; + (void)dwInstance; + (void)dwParam2; + if (uMsg == WOM_DONE) { - PrepareBackData(); - SwapBuffer(); - PlayFrontData(); + std::lock_guard lock(m_bufferMutex); + WAVEHDR* completedHeader = reinterpret_cast(dwParam1); + if (completedHeader == &m_waveHeader1) { + m_buffer1Available = true; + } else if (completedHeader == &m_waveHeader2) { + m_buffer2Available = true; + } + m_dataReadyCond.notify_one(); } } -void CALLBACK WASAPIBackend::StaticAudioCallback(HWAVEOUT hwo, UINT uMsg, +void CALLBACK WaveOutBackend::StaticAudioCallback(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { - WASAPIBackend* backend = reinterpret_cast(dwInstance); + WaveOutBackend* backend = reinterpret_cast(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)); +MMRESULT WaveOutBackend::SubmitBuffer(WAVEHDR& header, std::vector& samples) { + header.lpData = reinterpret_cast(samples.data()); + header.dwBufferLength = static_cast(samples.size() * sizeof(int16_t)); + header.dwFlags &= ~WHDR_DONE; + + MMRESULT result = waveOutWrite(m_hWaveOut, &header, 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 WaveOut } // namespace Audio } // namespace XCEngine diff --git a/tests/Components/test_windows_audio_backend.cpp b/tests/Components/test_windows_audio_backend.cpp new file mode 100644 index 00000000..1a4b1263 --- /dev/null +++ b/tests/Components/test_windows_audio_backend.cpp @@ -0,0 +1,96 @@ +#include + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define NOMINMAX +#include +#include +#endif + +#define private public +#include +#undef private + +using namespace XCEngine::Audio; + +namespace { + +#ifdef _WIN32 + +TEST(WaveOutBackend, MasterVolumeIsClampedToValidRange) { + WaveOut::WaveOutBackend backend; + + backend.SetMasterVolume(1.5f); + EXPECT_FLOAT_EQ(backend.GetMasterVolume(), 1.0f); + + backend.SetMasterVolume(-0.5f); + EXPECT_FLOAT_EQ(backend.GetMasterVolume(), 0.0f); +} + +TEST(WaveOutBackend, ProcessAudioCopiesOnlyRequestedSamplesIntoPendingMixBuffer) { + WaveOut::WaveOutBackend backend; + backend.m_pendingMixBuffer.assign(8, 1.0f); + + float input[] = {0.25f, -0.5f, 0.75f, -1.0f}; + backend.ProcessAudio(input, 2, 2, 48000); + + ASSERT_EQ(backend.m_pendingMixBuffer.size(), 8u); + EXPECT_TRUE(backend.m_hasPendingMix); + EXPECT_FLOAT_EQ(backend.m_pendingMixBuffer[0], 0.25f); + EXPECT_FLOAT_EQ(backend.m_pendingMixBuffer[1], -0.5f); + EXPECT_FLOAT_EQ(backend.m_pendingMixBuffer[2], 0.75f); + EXPECT_FLOAT_EQ(backend.m_pendingMixBuffer[3], -1.0f); + EXPECT_FLOAT_EQ(backend.m_pendingMixBuffer[4], 0.0f); + EXPECT_FLOAT_EQ(backend.m_pendingMixBuffer[5], 0.0f); + EXPECT_FLOAT_EQ(backend.m_pendingMixBuffer[6], 0.0f); + EXPECT_FLOAT_EQ(backend.m_pendingMixBuffer[7], 0.0f); +} + +TEST(WaveOutBackend, SetDeviceAcceptsDefaultDeviceBeforeInitialize) { + WaveOut::WaveOutBackend backend; + + EXPECT_TRUE(backend.SetDevice("Default Device")); + EXPECT_EQ(backend.m_deviceName, "Default Device"); + EXPECT_TRUE(backend.m_requestedDeviceName.empty()); +} + +TEST(WaveOutBackend, SetDeviceRejectsHotSwitchAfterOpen) { + WaveOut::WaveOutBackend backend; + backend.m_deviceName = "Default Device"; + backend.m_hWaveOut = reinterpret_cast(1); + + EXPECT_FALSE(backend.SetDevice("Some Other Device")); + EXPECT_EQ(backend.m_deviceName, "Default Device"); + + backend.m_hWaveOut = nullptr; +} + +TEST(WaveOutBackend, FillPcm16BufferAppliesVolumeClampAndMute) { + std::vector input = {-2.0f, -0.5f, 0.5f, 2.0f}; + std::vector output(4, 123); + + WaveOut::WaveOutBackend::FillPcm16Buffer(output, input, 0.5f, false); + + ASSERT_EQ(output.size(), 4u); + EXPECT_EQ(output[0], -32767); + EXPECT_EQ(output[1], -8191); + EXPECT_EQ(output[2], 8191); + EXPECT_EQ(output[3], 32767); + + WaveOut::WaveOutBackend::FillPcm16Buffer(output, input, 1.0f, true); + + EXPECT_EQ(output[0], 0); + EXPECT_EQ(output[1], 0); + EXPECT_EQ(output[2], 0); + EXPECT_EQ(output[3], 0); +} + +#endif + +} // namespace