From 2823aa31979d456b802f08d8cf7c141a6853de76 Mon Sep 17 00:00:00 2001 From: Attila Uygun Date: Wed, 4 Oct 2023 13:08:26 +0200 Subject: [PATCH] Implement AudioDeviceWASAPI --- build/BUILD.gn | 1 + src/engine/audio/BUILD.gn | 5 +- src/engine/audio/audio_device_null.h | 23 --- src/engine/audio/audio_device_wasapi.cc | 210 ++++++++++++++++++++++++ src/engine/audio/audio_device_wasapi.h | 49 ++++++ src/engine/audio/audio_mixer.cc | 5 +- src/engine/platform/BUILD.gn | 1 + src/engine/platform/platform_win.cc | 5 + 8 files changed, 272 insertions(+), 27 deletions(-) delete mode 100644 src/engine/audio/audio_device_null.h create mode 100644 src/engine/audio/audio_device_wasapi.cc create mode 100644 src/engine/audio/audio_device_wasapi.h diff --git a/build/BUILD.gn b/build/BUILD.gn index 1af90ae..75e2b62 100644 --- a/build/BUILD.gn +++ b/build/BUILD.gn @@ -184,6 +184,7 @@ config("warnings") { "/wd4820", # padding added after data member. "/wd5045", # compiler will insert Spectre mitigation for memory load if # switch specified. + "/wd4355", # 'this': used in base member initializer list # TODO: Not sure how I feel about these conversion warnings. "/Wv:18", diff --git a/src/engine/audio/BUILD.gn b/src/engine/audio/BUILD.gn index 6ab1d03..b22cebb 100644 --- a/src/engine/audio/BUILD.gn +++ b/src/engine/audio/BUILD.gn @@ -21,7 +21,10 @@ source_set("audio") { ] libs += [ "asound" ] } else if (target_os == "win") { - sources += [ "audio_device_null.h" ] + sources += [ + "audio_device_wasapi.cc", + "audio_device_wasapi.h", + ] } else if (target_os == "android") { sources += [ "audio_device_oboe.cc", diff --git a/src/engine/audio/audio_device_null.h b/src/engine/audio/audio_device_null.h deleted file mode 100644 index 6a5df90..0000000 --- a/src/engine/audio/audio_device_null.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef ENGINE_AUDIO_AUDIO_DEVICE_NULL_H -#define ENGINE_AUDIO_AUDIO_DEVICE_NULL_H - -#include "engine/audio/audio_device.h" - -namespace eng { - -class AudioDeviceNull final : public AudioDevice { - public: - AudioDeviceNull() = default; - ~AudioDeviceNull() final = default; - - bool Initialize() final { return true; } - - void Suspend() final {} - void Resume() final {} - - size_t GetHardwareSampleRate() final { return 0; } -}; - -} // namespace eng - -#endif // ENGINE_AUDIO_AUDIO_DEVICE_NULL_H diff --git a/src/engine/audio/audio_device_wasapi.cc b/src/engine/audio/audio_device_wasapi.cc new file mode 100644 index 0000000..9c8d597 --- /dev/null +++ b/src/engine/audio/audio_device_wasapi.cc @@ -0,0 +1,210 @@ +#include "engine/audio/audio_device_wasapi.h" + +#include "base/log.h" + +const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); +const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); +const IID IID_IAudioClient = __uuidof(IAudioClient); +const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient); + +using namespace base; + +namespace eng { + +AudioDeviceWASAPI::AudioDeviceWASAPI(AudioDevice::Delegate* delegate) + : delegate_(delegate) {} + +AudioDeviceWASAPI::~AudioDeviceWASAPI() { + LOG(0) << "Shutting down audio."; + + TerminateAudioThread(); + + if (shutdown_event_) + CloseHandle(shutdown_event_); + if (ready_event_) + CloseHandle(ready_event_); + if (device_) + device_->Release(); + if (audio_client_) + audio_client_->Release(); + if (render_client_) + render_client_->Release(); + if (device_enumerator_) + device_enumerator_->Release(); +} + +bool AudioDeviceWASAPI::Initialize() { + LOG(0) << "Initializing audio."; + + HRESULT hr; + do { + hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, + IID_IMMDeviceEnumerator, (void**)&device_enumerator_); + if (FAILED(hr)) { + LOG(0) << "Unable to instantiate device enumerator: " << hr; + break; + } + + hr = device_enumerator_->GetDefaultAudioEndpoint(eRender, eConsole, + &device_); + if (FAILED(hr)) { + LOG(0) << "Unable to get default audio endpoint: " << hr; + break; + } + + hr = device_->Activate(IID_IAudioClient, CLSCTX_ALL, NULL, + (void**)&audio_client_); + if (FAILED(hr)) { + LOG(0) << "Unable to activate audio client: " << hr; + break; + } + + // Use float format. + WAVEFORMATEX* closest_match = nullptr; + WAVEFORMATEXTENSIBLE wfxex = {0}; + wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); + wfxex.dwChannelMask = KSAUDIO_SPEAKER_STEREO; + wfxex.Format.nChannels = 2; + wfxex.Format.nSamplesPerSec = 48000; + wfxex.Format.wBitsPerSample = 32; + wfxex.Samples.wValidBitsPerSample = 32; + wfxex.Format.nBlockAlign = + wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3); + wfxex.Format.nAvgBytesPerSec = + wfxex.Format.nBlockAlign * wfxex.Format.nSamplesPerSec; + wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + hr = audio_client_->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, + &wfxex.Format, &closest_match); + if (FAILED(hr)) { + LOG(0) << "Unsupported sample format."; + break; + } + + WAVEFORMATEX* format = closest_match ? closest_match : &wfxex.Format; + if ((format->wFormatTag != WAVE_FORMAT_IEEE_FLOAT && + (format->wFormatTag != WAVE_FORMAT_EXTENSIBLE || + reinterpret_cast(format)->SubFormat != + KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) || + format->nChannels != 2) { + LOG(0) << "Unsupported sample format."; + break; + } + + sample_rate_ = format->nSamplesPerSec; + + HRESULT hr = audio_client_->Initialize( + AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST, 0, 0, + format, NULL); + if (closest_match) + CoTaskMemFree(closest_match); + if (FAILED(hr)) { + LOG(0) << "Unable to initialize audio client: " << hr; + break; + } + + // Get the actual size of the allocated buffer. + hr = audio_client_->GetBufferSize(&buffer_size_); + if (FAILED(hr)) { + LOG(0) << "Unable to get audio client buffer size: " << hr; + break; + } + + hr = audio_client_->GetService(IID_IAudioRenderClient, + (void**)&render_client_); + if (FAILED(hr)) { + LOG(0) << "Unable to get audio render client: " << hr; + break; + } + + shutdown_event_ = + CreateEventEx(NULL, NULL, 0, EVENT_MODIFY_STATE | SYNCHRONIZE); + if (shutdown_event_ == NULL) { + LOG(0) << "Unable to create shutdown event: " << hr; + break; + } + ready_event_ = + CreateEventEx(NULL, NULL, 0, EVENT_MODIFY_STATE | SYNCHRONIZE); + if (ready_event_ == NULL) { + LOG(0) << "Unable to create samples ready event: " << hr; + break; + } + + hr = audio_client_->SetEventHandle(ready_event_); + if (FAILED(hr)) { + LOG(0) << "Unable to set ready event: " << hr; + break; + } + + StartAudioThread(); + + hr = audio_client_->Start(); + if (FAILED(hr)) { + break; + } + + return true; + } while (false); + + return false; +} + +void AudioDeviceWASAPI::Suspend() { + audio_client_->Stop(); +} + +void AudioDeviceWASAPI::Resume() { + audio_client_->Start(); +} + +size_t AudioDeviceWASAPI::GetHardwareSampleRate() { + return sample_rate_; +} + +void AudioDeviceWASAPI::StartAudioThread() { + DCHECK(!audio_thread_.joinable()); + + LOG(0) << "Starting audio thread."; + audio_thread_ = std::thread(&AudioDeviceWASAPI::AudioThreadMain, this); +} + +void AudioDeviceWASAPI::TerminateAudioThread() { + LOG(0) << "Terminating audio thread"; + SetEvent(shutdown_event_); + audio_thread_.join(); +} + +void AudioDeviceWASAPI::AudioThreadMain() { + DCHECK(delegate_); + + HANDLE wait_array[] = {shutdown_event_, ready_event_}; + + for (;;) { + switch (WaitForMultipleObjects(2, wait_array, FALSE, INFINITE)) { + case WAIT_OBJECT_0 + 0: // shutdown_event_ + return; + + case WAIT_OBJECT_0 + 1: { // ready_event_ + UINT32 padding; + HRESULT hr = audio_client_->GetCurrentPadding(&padding); + if (SUCCEEDED(hr)) { + BYTE* pData; + UINT32 frames_available = buffer_size_ - padding; + hr = render_client_->GetBuffer(frames_available, &pData); + if (SUCCEEDED(hr)) { + delegate_->RenderAudio(reinterpret_cast(pData), + frames_available); + hr = render_client_->ReleaseBuffer(frames_available, 0); + if (!SUCCEEDED(hr)) + DLOG(0) << "Unable to release buffer: " << hr; + } else { + DLOG(0) << "Unable to get buffer: " << hr; + } + } + } break; + } + } +} + +} // namespace eng diff --git a/src/engine/audio/audio_device_wasapi.h b/src/engine/audio/audio_device_wasapi.h new file mode 100644 index 0000000..7a63d14 --- /dev/null +++ b/src/engine/audio/audio_device_wasapi.h @@ -0,0 +1,49 @@ +#ifndef ENGINE_AUDIO_AUDIO_DEVICE_WASAPI_H +#define ENGINE_AUDIO_AUDIO_DEVICE_WASAPI_H + +#include + +#include +#include + +#include "engine/audio/audio_device.h" + +namespace eng { + +class AudioDeviceWASAPI final : public AudioDevice { + public: + AudioDeviceWASAPI(AudioDevice::Delegate* delegate); + ~AudioDeviceWASAPI() final; + + bool Initialize() final; + + void Suspend() final; + void Resume() final; + + size_t GetHardwareSampleRate() final; + + private: + IMMDevice* device_ = nullptr; + IAudioClient* audio_client_ = nullptr; + IAudioRenderClient* render_client_ = nullptr; + IMMDeviceEnumerator* device_enumerator_ = nullptr; + + HANDLE shutdown_event_ = nullptr; + HANDLE ready_event_ = nullptr; + + std::thread audio_thread_; + + UINT32 buffer_size_ = 0; + size_t sample_rate_ = 0; + + AudioDevice::Delegate* delegate_ = nullptr; + + void StartAudioThread(); + void TerminateAudioThread(); + + void AudioThreadMain(); +}; + +} // namespace eng + +#endif // ENGINE_AUDIO_AUDIO_DEVICE_WASAPI_H diff --git a/src/engine/audio/audio_mixer.cc b/src/engine/audio/audio_mixer.cc index 1eb82db..c8d4501 100644 --- a/src/engine/audio/audio_mixer.cc +++ b/src/engine/audio/audio_mixer.cc @@ -12,7 +12,7 @@ #elif defined(__linux__) #include "engine/audio/audio_device_alsa.h" #elif defined(_WIN32) -#include "engine/audio/audio_device_null.h" +#include "engine/audio/audio_device_wasapi.h" #endif using namespace base; @@ -26,8 +26,7 @@ AudioMixer::AudioMixer() #elif defined(__linux__) audio_device_{std::make_unique(this)} { #elif defined(_WIN32) - // TODO: Implement AudioDeviceWindows - audio_device_{std::make_unique()} { + audio_device_{std::make_unique(this)} { #endif bool res = audio_device_->Initialize(); CHECK(res) << "Failed to initialize audio device."; diff --git a/src/engine/platform/BUILD.gn b/src/engine/platform/BUILD.gn index 81172c2..8cff165 100644 --- a/src/engine/platform/BUILD.gn +++ b/src/engine/platform/BUILD.gn @@ -24,6 +24,7 @@ source_set("platform") { libs = [ "gdi32.lib", # Graphics "user32.lib", # Win32 API core functionality. + "Ole32.lib", # COM "opengl32.lib", ] } else if (target_os == "android") { diff --git a/src/engine/platform/platform_win.cc b/src/engine/platform/platform_win.cc index 83c8354..379ba50 100644 --- a/src/engine/platform/platform_win.cc +++ b/src/engine/platform/platform_win.cc @@ -1,5 +1,7 @@ #include "engine/platform/platform.h" +#include + #include "base/log.h" #include "base/vecmath.h" #include "engine/input_event.h" @@ -36,6 +38,9 @@ Platform::Platform(HINSTANCE instance, int cmd_show) LOG(0) << "Data path: " << data_path_.c_str(); LOG(0) << "Shared data path: " << shared_data_path_.c_str(); + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + CHECK(SUCCEEDED(hr)) << "Unable to initialize COM: " << hr; + WNDCLASSEXW wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW;