From 521b490d0fc3f8ef0291f5d54b04e9de1212dfec Mon Sep 17 00:00:00 2001 From: Jack Andersen Date: Tue, 31 May 2016 18:53:57 -1000 Subject: [PATCH] Support for runtime changing of audio output endpoint under WASAPI --- include/boo/audiodev/IAudioSubmix.hpp | 3 + lib/audiodev/AudioSubmix.cpp | 10 + lib/audiodev/AudioSubmix.hpp | 3 + lib/audiodev/AudioVoice.hpp | 1 + lib/audiodev/WASAPI.cpp | 255 ++++++++++++++++++++------ 5 files changed, 218 insertions(+), 54 deletions(-) diff --git a/include/boo/audiodev/IAudioSubmix.hpp b/include/boo/audiodev/IAudioSubmix.hpp index 99bacc6..e66d247 100644 --- a/include/boo/audiodev/IAudioSubmix.hpp +++ b/include/boo/audiodev/IAudioSubmix.hpp @@ -58,6 +58,9 @@ struct IAudioSubmixCallback const ChannelMap& chanMap, double sampleRate) const=0; virtual void applyEffect(float* audio, size_t frameCount, const ChannelMap& chanMap, double sampleRate) const=0; + + /** Notify of output sample rate changes (for instance, changing the default audio device on Windows) */ + virtual void resetOutputSampleRate(double sampleRate)=0; }; } diff --git a/lib/audiodev/AudioSubmix.cpp b/lib/audiodev/AudioSubmix.cpp index 122c2cd..8c270d9 100644 --- a/lib/audiodev/AudioSubmix.cpp +++ b/lib/audiodev/AudioSubmix.cpp @@ -131,6 +131,16 @@ void AudioSubmix::_unbindFrom(std::list::iterator it) m_activeSubmixes.erase(it); } +void AudioSubmix::_resetOutputSampleRate() +{ + for (AudioVoice* vox : m_activeVoices) + vox->_resetSampleRate(vox->m_sampleRateIn); + for (AudioSubmix* smx : m_activeSubmixes) + smx->_resetOutputSampleRate(); + if (m_cb) + m_cb->resetOutputSampleRate(m_parent.mixInfo().m_sampleRate); +} + std::unique_ptr AudioSubmix::allocateNewMonoVoice(double sampleRate, IAudioVoiceCallback* cb, bool dynamicPitch) diff --git a/lib/audiodev/AudioSubmix.hpp b/lib/audiodev/AudioSubmix.hpp index a19c982..a9215d7 100644 --- a/lib/audiodev/AudioSubmix.hpp +++ b/lib/audiodev/AudioSubmix.hpp @@ -14,6 +14,7 @@ class AudioVoice; class AudioSubmix : public IAudioSubmix, public IAudioMix { friend class BaseAudioVoiceEngine; + friend struct WASAPIAudioVoiceEngine; /* Mixer-engine relationships */ BaseAudioVoiceEngine& m_root; @@ -48,6 +49,8 @@ class AudioSubmix : public IAudioSubmix, public IAudioMix void _unbindFrom(std::list::iterator it); void _unbindFrom(std::list::iterator it); + void _resetOutputSampleRate(); + public: ~AudioSubmix(); AudioSubmix(BaseAudioVoiceEngine& root, IAudioMix& parent, IAudioSubmixCallback* cb); diff --git a/lib/audiodev/AudioVoice.hpp b/lib/audiodev/AudioVoice.hpp index fb41c0c..2b75dcd 100644 --- a/lib/audiodev/AudioVoice.hpp +++ b/lib/audiodev/AudioVoice.hpp @@ -16,6 +16,7 @@ class AudioVoice : public IAudioVoice { friend class BaseAudioVoiceEngine; friend class AudioSubmix; + friend struct WASAPIAudioVoiceEngine; protected: /* Mixer-engine relationships */ diff --git a/lib/audiodev/WASAPI.cpp b/lib/audiodev/WASAPI.cpp index e8bc184..7b1e0e7 100644 --- a/lib/audiodev/WASAPI.cpp +++ b/lib/audiodev/WASAPI.cpp @@ -17,25 +17,112 @@ namespace boo { static logvisor::Module Log("boo::WASAPI"); +#define SAFE_RELEASE(punk) \ + if ((punk) != NULL) \ + { (punk)->Release(); (punk) = NULL; } + struct WASAPIAudioVoiceEngine : BaseAudioVoiceEngine { + ComPtr m_enumerator; ComPtr m_device; ComPtr m_audClient; ComPtr m_renderClient; - WASAPIAudioVoiceEngine() + struct NotificationClient : public IMMNotificationClient { - /* Enumerate default audio device */ - ComPtr pEnumerator; - if (FAILED(CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, - CLSCTX_ALL, IID_IMMDeviceEnumerator, - &pEnumerator))) + WASAPIAudioVoiceEngine& m_parent; + + LONG _cRef; + IMMDeviceEnumerator *_pEnumerator; + + NotificationClient(WASAPIAudioVoiceEngine& parent) + : m_parent(parent), + _cRef(1), + _pEnumerator(nullptr) + {} + + ~NotificationClient() { - Log.report(logvisor::Fatal, L"unable to create MMDeviceEnumerator instance"); - return; + SAFE_RELEASE(_pEnumerator) } - if (FAILED(pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &m_device))) + + // IUnknown methods -- AddRef, Release, and QueryInterface + + ULONG STDMETHODCALLTYPE AddRef() + { + return InterlockedIncrement(&_cRef); + } + + ULONG STDMETHODCALLTYPE Release() + { + ULONG ulRef = InterlockedDecrement(&_cRef); + if (0 == ulRef) + { + delete this; + } + return ulRef; + } + + HRESULT STDMETHODCALLTYPE QueryInterface( + REFIID riid, VOID **ppvInterface) + { + if (IID_IUnknown == riid) + { + AddRef(); + *ppvInterface = (IUnknown*)this; + } + else if (__uuidof(IMMNotificationClient) == riid) + { + AddRef(); + *ppvInterface = (IMMNotificationClient*)this; + } + else + { + *ppvInterface = NULL; + return E_NOINTERFACE; + } + return S_OK; + } + + // Callback methods for device-event notifications. + + HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged( + EDataFlow flow, ERole role, + LPCWSTR pwstrDeviceId) + { + m_parent.m_rebuild = true; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) + { + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) + { + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnDeviceStateChanged( + LPCWSTR pwstrDeviceId, + DWORD dwNewState) + { + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnPropertyValueChanged( + LPCWSTR pwstrDeviceId, + const PROPERTYKEY key) + { + return S_OK; + } + } m_notificationClient; + + void _buildAudioRenderClient() + { + if (FAILED(m_enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &m_device))) { Log.report(logvisor::Fatal, L"unable to obtain default audio device"); m_device.Reset(); @@ -198,60 +285,120 @@ struct WASAPIAudioVoiceEngine : BaseAudioVoiceEngine } } + WASAPIAudioVoiceEngine() + : m_notificationClient(*this) + { + /* Enumerate default audio device */ + if (FAILED(CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, + CLSCTX_ALL, IID_IMMDeviceEnumerator, + &m_enumerator))) + { + Log.report(logvisor::Fatal, L"unable to create MMDeviceEnumerator instance"); + return; + } + + if (FAILED(m_enumerator->RegisterEndpointNotificationCallback(&m_notificationClient))) + { + Log.report(logvisor::Fatal, L"unable to register multimedia event callback"); + m_device.Reset(); + return; + } + + _buildAudioRenderClient(); + } + bool m_started = false; + bool m_rebuild = false; + + void _rebuildAudioRenderClient() + { + soxr_datatype_t oldFmt = m_mixInfo.m_sampleFormat; + + _buildAudioRenderClient(); + m_rebuild = false; + m_started = false; + + if (m_mixInfo.m_sampleFormat != oldFmt) + Log.report(logvisor::Fatal, L"audio device sample format changed, boo doesn't support this!!"); + + for (AudioVoice* vox : m_activeVoices) + vox->_resetSampleRate(vox->m_sampleRateIn); + for (AudioSubmix* smx : m_activeSubmixes) + smx->_resetOutputSampleRate(); + } void pumpAndMixVoices() { - UINT32 numFramesPadding; - if (FAILED(m_audClient->GetCurrentPadding(&numFramesPadding))) + int attempt = 0; + while (true) { - Log.report(logvisor::Fatal, L"unable to get available buffer frames"); - return; - } + if (attempt >= 10) + Log.report(logvisor::Fatal, L"unable to setup AudioRenderClient"); - size_t frames = m_mixInfo.m_periodFrames - numFramesPadding; - if (frames <= 0) - return; + if (m_rebuild) + _rebuildAudioRenderClient(); - BYTE* bufOut; - if (FAILED(m_renderClient->GetBuffer(frames, &bufOut))) - { - Log.report(logvisor::Fatal, L"unable to map audio buffer"); - return; - } - - DWORD flags = 0; - switch (m_mixInfo.m_sampleFormat) - { - case SOXR_INT16_I: - _pumpAndMixVoices(frames, reinterpret_cast(bufOut)); - break; - case SOXR_INT32_I: - _pumpAndMixVoices(frames, reinterpret_cast(bufOut)); - break; - case SOXR_FLOAT32_I: - _pumpAndMixVoices(frames, reinterpret_cast(bufOut)); - break; - default: - flags = AUDCLNT_BUFFERFLAGS_SILENT; - break; - } - - if (FAILED(m_renderClient->ReleaseBuffer(frames, flags))) - { - Log.report(logvisor::Fatal, L"unable to unmap audio buffer"); - return; - } - - if (!m_started) - { - if (FAILED(m_audClient->Start())) + HRESULT res; + if (!m_started) { - Log.report(logvisor::Fatal, L"unable to start audio client"); - m_device.Reset(); - return; + res = m_audClient->Start(); + if (FAILED(res)) + { + m_rebuild = true; + ++attempt; + continue; + } + m_started = true; } - m_started = true; + + UINT32 numFramesPadding; + res = m_audClient->GetCurrentPadding(&numFramesPadding); + if (FAILED(res)) + { + m_rebuild = true; + ++attempt; + continue; + } + + size_t frames = m_mixInfo.m_periodFrames - numFramesPadding; + if (frames <= 0) + return; + + BYTE* bufOut; + res = m_renderClient->GetBuffer(frames, &bufOut); + if (FAILED(res)) + { + m_rebuild = true; + ++attempt; + continue; + } + + DWORD flags = 0; + switch (m_mixInfo.m_sampleFormat) + { + case SOXR_INT16_I: + _pumpAndMixVoices(frames, reinterpret_cast(bufOut)); + break; + case SOXR_INT32_I: + _pumpAndMixVoices(frames, reinterpret_cast(bufOut)); + break; + case SOXR_FLOAT32_I: + _pumpAndMixVoices(frames, reinterpret_cast(bufOut)); + break; + default: + flags = AUDCLNT_BUFFERFLAGS_SILENT; + break; + } + + res = m_renderClient->ReleaseBuffer(frames, flags); + if (FAILED(res)) + { + m_rebuild = true; + ++attempt; + continue; + } + + break; } }