diff --git a/scripts/src/osd/modules.lua b/scripts/src/osd/modules.lua index 5b19ed4fc44..9833bfb070f 100644 --- a/scripts/src/osd/modules.lua +++ b/scripts/src/osd/modules.lua @@ -132,6 +132,8 @@ function osdmodulesbuild() MAME_DIR .. "src/osd/modules/sound/coreaudio_sound.cpp", MAME_DIR .. "src/osd/modules/sound/direct_sound.cpp", MAME_DIR .. "src/osd/modules/sound/js_sound.cpp", + MAME_DIR .. "src/osd/modules/sound/mmdevice_helpers.cpp", + MAME_DIR .. "src/osd/modules/sound/mmdevice_helpers.h", MAME_DIR .. "src/osd/modules/sound/none.cpp", MAME_DIR .. "src/osd/modules/sound/pa_sound.cpp", MAME_DIR .. "src/osd/modules/sound/pulse_sound.cpp", diff --git a/src/osd/modules/monitor/monitor_win32.cpp b/src/osd/modules/monitor/monitor_win32.cpp index 1766ca358a1..7e4fb29c4ca 100644 --- a/src/osd/modules/monitor/monitor_win32.cpp +++ b/src/osd/modules/monitor/monitor_win32.cpp @@ -5,20 +5,28 @@ * */ -#include "modules/osdmodule.h" #include "monitor_module.h" +#include "modules/osdmodule.h" + #if defined(OSD_WINDOWS) +// local headers +#include "monitor_common.h" + +// OSD headers +#include "osdcore.h" +#include "strconv.h" +#include "window.h" +#include "windows/video.h" + // standard windows headers #include #undef interface -#include "osdcore.h" -#include "strconv.h" -#include "windows/video.h" -#include "window.h" -#include "monitor_common.h" +namespace osd { + +namespace { class win32_monitor_module; @@ -28,10 +36,10 @@ private: MONITORINFOEX m_info; public: - win32_monitor_info(monitor_module& module, const HMONITOR handle, const char* monitor_device, float aspect) - : osd_monitor_info(module, std::uintptr_t(handle), monitor_device, aspect) + win32_monitor_info(monitor_module& module, const HMONITOR handle, std::string &&monitor_device, float aspect) + : osd_monitor_info(module, std::uintptr_t(handle), std::move(monitor_device), aspect) { - win32_monitor_info::refresh(); + refresh(); } void refresh() override @@ -93,11 +101,9 @@ protected: EnumDisplayMonitors(nullptr, nullptr, monitor_enum_callback, reinterpret_cast(this)); // if we're verbose, print the list of monitors + for (const auto &monitor : list()) { - for (const auto &monitor : list()) - { - osd_printf_verbose("Video: Monitor %u = \"%s\" %s\n", monitor->oshandle(), monitor->devicename(), monitor->is_primary() ? "(primary)" : ""); - } + osd_printf_verbose("Video: Monitor %u = \"%s\" %s\n", monitor->oshandle(), monitor->devicename(), monitor->is_primary() ? "(primary)" : ""); } return 0; @@ -106,7 +112,7 @@ protected: private: static BOOL CALLBACK monitor_enum_callback(HMONITOR handle, HDC dc, LPRECT rect, LPARAM data) { - auto* self = reinterpret_cast(data); + auto *const self = reinterpret_cast(data); MONITORINFOEX info; BOOL result; @@ -116,14 +122,14 @@ private: assert(result); (void)result; // to silence gcc 4.6 - // guess the aspect ratio assuming square pixels - float aspect = static_cast(info.rcMonitor.right - info.rcMonitor.left) / static_cast(info.rcMonitor.bottom - info.rcMonitor.top); + // guess the aspect ratio assuming square pixels + float aspect = float(info.rcMonitor.right - info.rcMonitor.left) / float(info.rcMonitor.bottom - info.rcMonitor.top); // allocate a new monitor info auto temp = osd::text::from_tstring(info.szDevice); // copy in the data - auto monitor = std::make_shared(*self, handle, temp.c_str(), aspect); + auto monitor = std::make_shared(*self, handle, std::move(temp), aspect); // hook us into the list self->add_monitor(monitor); @@ -133,8 +139,14 @@ private: } }; +} // anonymous namespace + +} // namespace osd + #else -MODULE_NOT_SUPPORTED(win32_monitor_module, OSD_MONITOR_PROVIDER, "win32") + +namespace osd { namespace { MODULE_NOT_SUPPORTED(win32_monitor_module, OSD_MONITOR_PROVIDER, "win32") } } + #endif -MODULE_DEFINITION(MONITOR_WIN32, win32_monitor_module) +MODULE_DEFINITION(MONITOR_WIN32, osd::win32_monitor_module) diff --git a/src/osd/modules/sound/mmdevice_helpers.cpp b/src/osd/modules/sound/mmdevice_helpers.cpp new file mode 100644 index 00000000000..33d04c93f10 --- /dev/null +++ b/src/osd/modules/sound/mmdevice_helpers.cpp @@ -0,0 +1,337 @@ +// license:BSD-3-Clause +// copyright-holders:Vas Crabb +#include "mmdevice_helpers.h" + +#if defined(_WIN32) + +#include "osdcore.h" +#include "strconv.h" + +#include "util/coretmpl.h" + +#include +#include +#include +#include + +#include +#include + +#include + +#include + + +namespace osd { + +namespace { + +using mm_endpoint_ptr = Microsoft::WRL::ComPtr; + +char const *const f_speaker_names[] ={ + "FL", // SPEAKER_FRONT_LEFT + "FR", // SPEAKER_FRONT_RIGHT + "FC", // SPEAKER_FRONT_CENTER + "LFE", // SPEAKER_LOW_FREQUENCY + "BL", // SPEAKER_BACK_LEFT + "BR", // SPEAKER_BACK_RIGHT + "FCL", // SPEAKER_FRONT_LEFT_OF_CENTER + "FCR", // SPEAKER_FRONT_RIGHT_OF_CENTER + "BC", // SPEAKER_BACK_CENTER + "SL", // SPEAKER_SIDE_LEFT + "SR", // SPEAKER_SIDE_RIGHT + "TC", // SPEAKER_TOP_CENTER + "TFL", // SPEAKER_TOP_FRONT_LEFT + "TFC", // SPEAKER_TOP_FRONT_CENTER + "TFR", // SPEAKER_TOP_FRONT_RIGHT + "TBL", // SPEAKER_TOP_BACK_LEFT + "TBC", // SPEAKER_TOP_BACK_CENTER + "TBR" }; // SPEAKER_TOP_BACK_RIGHT + +std::array const f_speaker_positions[] = { + { -0.2, 0.0, 1.0 }, // SPEAKER_FRONT_LEFT + { 0.2, 0.0, 1.0 }, // SPEAKER_FRONT_RIGHT + { 0.0, 0.0, 1.0 }, // SPEAKER_FRONT_CENTER + { 0.0, -0.5, 1.0 }, // SPEAKER_LOW_FREQUENCY + { -0.2, 0.0, -0.5 }, // SPEAKER_BACK_LEFT + { 0.2, 0.0, -0.5 }, // SPEAKER_BACK_RIGHT + { -0.1, 0.0, 1.0 }, // SPEAKER_FRONT_LEFT_OF_CENTER + { 0.1, 0.0, 1.0 }, // SPEAKER_FRONT_RIGHT_OF_CENTER + { 0.0, 0.0, -0.5 }, // SPEAKER_BACK_CENTER + { -0.2, 0.0, 0.0 }, // SPEAKER_SIDE_LEFT + { 0.2, 0.0, 0.0 }, // SPEAKER_SIDE_RIGHT + { 0.0, 0.5, 0.0 }, // SPEAKER_TOP_CENTER + { -0.2, 0.5, 1.0 }, // SPEAKER_TOP_FRONT_LEFT + { 0.0, 0.5, 1.0 }, // SPEAKER_TOP_FRONT_CENTER + { 0.2, 0.5, 1.0 }, // SPEAKER_TOP_FRONT_RIGHT + { -0.2, 0.5, -0.5 }, // SPEAKER_TOP_BACK_LEFT + { 0.0, 0.5, -0.5 }, // SPEAKER_TOP_BACK_CENTER + { 0.2, 0.5, -0.5 } }; // SPEAKER_TOP_BACK_RIGHT + +} // anonymous namespace + + +HRESULT populate_audio_node_info( + IMMDevice &device, + std::wstring &device_id, + audio_info::node_info &info) +{ + HRESULT result; + + // get the device ID + co_task_wstr_ptr device_id_w; + std::string id_string; + { + LPWSTR id_raw = nullptr; + result = device.GetId(&id_raw); + if (FAILED(result) || !id_raw) + { + osd_printf_error( + "Error getting ID for audio device. Error: 0x%X\n", + static_cast(result)); + return FAILED(result) ? result : E_POINTER; + } + device_id_w.reset(std::exchange(id_raw, nullptr)); + try + { + osd::text::from_wstring(id_string, device_id_w.get()); + } + catch (std::bad_alloc const &) + { + return E_OUTOFMEMORY; + } + } + + // get the property store (needed for various important things) + property_store_ptr properties; + result = device.OpenPropertyStore(STGM_READ, properties.GetAddressOf()); + if (FAILED(result) || !properties) + { + osd_printf_error( + "Error opening property store for audio device %s. Error: 0x%X\n", + id_string, + static_cast(result)); + return FAILED(result) ? result : E_POINTER; + } + + // get the display name + std::string device_name; + { + std::optional name_string; + result = get_string_property_value(*properties.Get(), PKEY_Device_FriendlyName, name_string); + if (FAILED(result) || !name_string) + { + // fall back to using device ID + osd_printf_error( + "Error getting display name for audio device %s. Error: 0x%X\n", + id_string, + static_cast(result)); + try + { + device_name = id_string; + } + catch (std::bad_alloc const &) + { + return E_OUTOFMEMORY; + } + } + else + { + device_name = std::move(*name_string); + } + } + + // see whether it's an input or output + EDataFlow data_flow; + { + mm_endpoint_ptr endpoint; + result = device.QueryInterface(endpoint.GetAddressOf()); + if (FAILED(result) || !endpoint) + { + osd_printf_error( + "Error getting endpoint information for audio device %s. Error: 0x%X\n", + device_name, + static_cast(result)); + return FAILED(result) ? result : E_POINTER; + } + + result = endpoint->GetDataFlow(&data_flow); + if (FAILED(result)) + { + osd_printf_error( + "Error getting data flow direction for audio device %s. Error: 0x%X\n", + device_name, + static_cast(result)); + return result; + } + + if ((eRender != data_flow) && (eCapture != data_flow)) + { + osd_printf_error( + "Invalid data flow direction for audio device %s. Value: %u\n", + device_name, + std::underlying_type_t(data_flow)); + return E_INVALIDARG; + } + } + + // get format information + prop_variant_helper format_property; + result = properties->GetValue(PKEY_AudioEngine_DeviceFormat, &format_property.value); + if (FAILED(result)) + { + osd_printf_error( + "Error getting stream format for audio device %s. Error: 0x%X\n", + device_name, + static_cast(result)); + return result; + } + else if (VT_BLOB != format_property.value.vt) + { + // you can get VT_EMPTY when a device is initially added - don't warn about it + if (VT_EMPTY != format_property.value.vt) + { + osd_printf_error( + "Stream format has invalid data type for audio device %s. Type: %u\n", + device_name, + std::underlying_type_t(format_property.value.vt)); + } + return E_INVALIDARG; + } + auto const format = reinterpret_cast(format_property.value.blob.pBlobData); + + // get the channel mask for speaker positions + std::optional channel_mask; + { + prop_variant_helper speakers_property; + result = properties->GetValue(PKEY_AudioEndpoint_PhysicalSpeakers, &speakers_property.value); + if (FAILED(result)) + { + osd_printf_error( + "Error getting speaker arrangement for audio device %s. Error: 0x%X\n", + device_name, + static_cast(result)); + } + else switch (speakers_property.value.vt) + { + case VT_EMPTY: + break; + case VT_UI4: + channel_mask = speakers_property.value.ulVal; + break; + case VT_UINT: + channel_mask = speakers_property.value.uintVal; + break; + default: + osd_printf_error( + "Speaker arrangement has invalid data type for audio device %s. Type: %u\n", + device_name, + std::underlying_type_t(speakers_property.value.vt)); + } + } + if (!channel_mask && (WAVE_FORMAT_EXTENSIBLE == format->wFormatTag)) + { + auto const extensible_format = reinterpret_cast(format); + channel_mask = extensible_format->dwChannelMask; + } + + // set up channel info + std::vector channel_names; + std::vector > channel_positions; + try + { + channel_names.reserve(format->nChannels); + channel_positions.reserve(format->nChannels); + DWORD i = 0; + if ((eRender == data_flow) && channel_mask) + { + static_assert(std::size(f_speaker_names) == std::size(f_speaker_positions)); + DWORD b = 0; + while ((format->nChannels > i) && (std::size(f_speaker_names) > b)) + { + if (util::BIT(*channel_mask, b)) + { + channel_names.emplace_back(f_speaker_names[b]); + channel_positions.emplace_back(f_speaker_positions[b]); + ++i; + } + ++b; + } + } + while (format->nChannels > i) + { + channel_names.emplace_back(util::string_format("Channel %u", i + 1)); + ++i; + } + channel_positions.resize(format->nChannels, std::array{ 0.0, 0.0, 0.0 }); + } + catch (std::bad_alloc const &) + { + return E_OUTOFMEMORY; + } + + // set results + try + { + device_id = device_id_w.get(); + info.m_name = std::move(device_name); + info.m_rate.m_default_rate = format->nSamplesPerSec; + info.m_rate.m_min_rate = format->nSamplesPerSec; + info.m_rate.m_max_rate = format->nSamplesPerSec; + info.m_port_names = std::move(channel_names); + info.m_port_positions = std::move(channel_positions); + info.m_sinks = (eRender == data_flow) ? format->nChannels : 0; + info.m_sources = (eCapture == data_flow) ? format->nChannels : 0; + } + catch (std::bad_alloc const &) + { + return E_OUTOFMEMORY; + } + + return S_OK; +} + + +HRESULT get_string_property_value( + IPropertyStore &properties, + REFPROPERTYKEY key, + std::optional &value) +{ + prop_variant_helper property_value; + HRESULT const result = properties.GetValue(key, &property_value.value); + if (FAILED(result)) + return result; + + try + { + switch (property_value.value.vt) + { + case VT_EMPTY: + value = std::nullopt; + return result; + + case VT_BSTR: + value = osd::text::from_wstring(std::wstring_view(property_value.value.bstrVal, SysStringLen(property_value.value.bstrVal))); + return result; + + case VT_LPSTR: + value = osd::text::from_astring(property_value.value.pszVal); + return result; + + case VT_LPWSTR: + value = osd::text::from_wstring(property_value.value.pwszVal); + return result; + + default: + return E_INVALIDARG; + } + } + catch (std::bad_alloc const &) + { + return E_OUTOFMEMORY; + } +} + +} // namespace osd + +#endif // defined(_WIN32) diff --git a/src/osd/modules/sound/mmdevice_helpers.h b/src/osd/modules/sound/mmdevice_helpers.h new file mode 100644 index 00000000000..acabf2dcf00 --- /dev/null +++ b/src/osd/modules/sound/mmdevice_helpers.h @@ -0,0 +1,105 @@ +// license:BSD-3-Clause +// copyright-holders:Vas Crabb +#ifndef MAME_OSD_SOUND_MMDEVICE_HELPERS_H +#define MAME_OSD_SOUND_MMDEVICE_HELPERS_H + +#pragma once + +#if defined(_WIN32) + +#include "interface/audio.h" + +#include +#include +#include + +#include + +#include +#include +#include + +#include + +#include + + +namespace osd { + +struct prop_variant_helper +{ + PROPVARIANT value; + + prop_variant_helper() { PropVariantInit(&value); } + ~prop_variant_helper() { PropVariantClear(&value); } + prop_variant_helper(prop_variant_helper const &) = delete; + prop_variant_helper &operator=(prop_variant_helper const &) = delete; +}; + +struct co_task_mem_deleter +{ + template + void operator()(T *obj) const + { + if (obj) + CoTaskMemFree(obj); + } +}; + +using co_task_wstr_ptr = std::unique_ptr; + +using mm_device_ptr = Microsoft::WRL::ComPtr; +using mm_device_collection_ptr = Microsoft::WRL::ComPtr; +using mm_device_enumerator_ptr = Microsoft::WRL::ComPtr; +using property_store_ptr = Microsoft::WRL::ComPtr; + + +template +HRESULT enumerate_audio_endpoints( + IMMDeviceEnumerator &enumerator, + EDataFlow data_flow, + DWORD state_mask, + T &&action) +{ + HRESULT result; + + // get devices + mm_device_collection_ptr devices; + result = enumerator.EnumAudioEndpoints(data_flow, state_mask, devices.GetAddressOf()); + if (FAILED(result)) + return result; + + // count devices + UINT count; + result = devices->GetCount(&count); + if (FAILED(result)) + return result; + + // enumerate devices + for (UINT i = 0; count > i; ++i) + { + mm_device_ptr device; + result = devices->Item(i, device.GetAddressOf()); + if (!action(result, device)) + break; + } + + return S_OK; +} + +HRESULT populate_audio_node_info( + IMMDevice &device, + std::wstring &device_id, + audio_info::node_info &info); + +HRESULT get_string_property_value( + IPropertyStore &properties, + REFPROPERTYKEY key, + std::optional &value); + + +} // namespace osd + +#endif // defined(_WIN32) + +#endif // MAME_OSD_SOUND_MMDEVICE_HELPERS_H diff --git a/src/osd/modules/sound/xaudio2_sound.cpp b/src/osd/modules/sound/xaudio2_sound.cpp index ffd2db882d7..6ec17164875 100644 --- a/src/osd/modules/sound/xaudio2_sound.cpp +++ b/src/osd/modules/sound/xaudio2_sound.cpp @@ -1,5 +1,5 @@ // license:BSD-3-Clause -// copyright-holders:Brad Hughes +// copyright-holders:Vas Crabb, Brad Hughes //==================================================================== // // xaudio2_sound.cpp - XAudio2 implementation of MAME sound routines @@ -12,28 +12,46 @@ #if defined(OSD_WINDOWS) | defined(SDLMAME_WIN32) +// local headers +#include "mmdevice_helpers.h" + // OSD headers #include "modules/lib/osdlib.h" #include "modules/lib/osdobj_common.h" #include "osdcore.h" #include "osdepend.h" +#include "strconv.h" #include "windows/winutil.h" // stdlib includes #include +#include +#include #include +#include +#include #include #include +#include +#include #include +#include // standard windows headers #include +#include +#include + #include // XAudio2 include #include +// MMDevice API headers +#include +#include + namespace osd { @@ -44,7 +62,7 @@ namespace { //============================================================ #define SUBMIT_FREQUENCY_TARGET_MS 20 -#define RESAMPLE_TOLERANCE 1.20f +#define RESAMPLE_TOLERANCE 1.20F //============================================================ // Macros @@ -61,30 +79,9 @@ namespace { // Variant of HR_LOG to log using osd_printf_error #define HR_LOGE( CALL, ONFAIL ) HR_LOG(CALL, osd_printf_error, ONFAIL) -// Variant of HR_LOG to log using osd_printf_verbose -#define HR_LOGV( CALL, ONFAIL ) HR_LOG(CALL, osd_printf_verbose, ONFAIL) - // Macro to check for a failed HRESULT and if failed, goto a label called Error: #define HR_GOERR( CALL ) HR_LOGE( CALL, goto Error;) -// Macro to check for a failed HRESULT and if failed, return the specified value -#define HR_RET( CALL, ret ) HR_LOGE(CALL, return ret;) - -// Macro to check for a failed HRESULT and if failed, return nothing (void function) -#define HR_RETV( CALL ) HR_RET(CALL,) - -// Macro to check for a failed HRESULT and if failed, return 0 -#define HR_RET0( CALL ) HR_RET(CALL, 0) - -// Macro to check for a failed HRESULT and if failed, return the HRESULT -#define HR_RETHR( CALL ) HR_RET(CALL, result) - -// Macro to check for a failed HRESULT and if failed, return 1 -#define HR_RET1( CALL ) HR_RET(CALL, 1) - -// Macro to check for a failed HRESULT and if failed, log verbose, and proceed as normal -#define HR_IGNORE( CALL ) HR_LOGV(CALL,) - //============================================================ // Structs and typedefs //============================================================ @@ -117,9 +114,12 @@ public: } }; -// Typedefs for smart pointers used with customer deleters -typedef std::unique_ptr mastering_voice_ptr; -typedef std::unique_ptr src_voice_ptr; +// smart pointers used with customer deleters +using mastering_voice_ptr = std::unique_ptr; +using source_voice_ptr = std::unique_ptr; + +// smart pointers for things using COM ABI +using x_audio_2_ptr = Microsoft::WRL::ComPtr; //============================================================ // Helper classes @@ -131,7 +131,7 @@ class bufferpool private: int m_initial; int m_buffersize; - std::queue> m_queue; + std::queue> m_queue; public: // constructor @@ -141,36 +141,36 @@ public: { for (int i = 0; i < m_initial; i++) { - auto newBuffer = std::make_unique(m_buffersize); + auto newBuffer = std::make_unique(m_buffersize); memset(newBuffer.get(), 0, m_buffersize); m_queue.push(std::move(newBuffer)); } } // get next buffer element from the pool - BYTE* next() + std::unique_ptr next() { - BYTE* next_buffer; + std::unique_ptr next_buffer; if (!m_queue.empty()) { - next_buffer = m_queue.front().release(); + next_buffer = std::move(m_queue.front()); m_queue.pop(); } else { - next_buffer = new BYTE[m_buffersize]; - memset(next_buffer, 0, m_buffersize); + next_buffer.reset(new BYTE[m_buffersize]); + memset(next_buffer.get(), 0, m_buffersize); } return next_buffer; } // release element, make it available back in the pool - void return_to_pool(BYTE* buffer) + void return_to_pool(std::unique_ptr &&buffer) { - auto returned_buf = std::unique_ptr(buffer); - memset(returned_buf.get(), 0, m_buffersize); - m_queue.push(std::move(returned_buf)); + assert(buffer); + memset(buffer.get(), 0, m_buffersize); + m_queue.push(std::move(buffer)); } }; @@ -179,79 +179,149 @@ public: //============================================================ // The main class for the XAudio2 sound module implementation -class sound_xaudio2 : public osd_module, public sound_module, public IXAudio2VoiceCallback +class sound_xaudio2 : + public osd_module, + public sound_module, + public IMMNotificationClient { public: sound_xaudio2() : osd_module(OSD_SOUND_PROVIDER, "xaudio2"), sound_module(), - m_xAudio2(nullptr), - m_masterVoice(nullptr), - m_sourceVoice(nullptr), - m_sample_rate(0), - m_audio_latency(0.0f), - m_sample_bytes(0), - m_buffer(nullptr), - m_buffer_size(0), - m_buffer_count(0), - m_writepos(0), - m_hEventBufferCompleted(nullptr), - m_hEventDataAvailable(nullptr), + m_next_node_id(1), + m_default_sink(0), + m_endpoint_notifications(false), + m_next_voice_id(1), + m_generation(1), + m_exiting(false), + m_audio_latency(0.0F), + m_hEventNeedUpdate(nullptr), m_hEventExiting(nullptr), - m_buffer_pool(nullptr), m_overflows(0), - m_underflows(0), - m_in_underflow(FALSE), - m_initialized(FALSE) + m_underflows(0) { } - bool probe() override; - int init(osd_interface &osd, osd_options const &options) override; - void exit() override; + // osd_module implementation + virtual bool probe() override; + virtual int init(osd_interface &osd, osd_options const &options) override; + virtual void exit() override; // sound_module - void stream_sink_update(uint32_t, int16_t const *buffer, int samples_this_frame) override; + virtual uint32_t get_generation() override; + virtual audio_info get_information() override; + virtual bool external_per_channel_volume() { return true; } + virtual bool split_streams_per_source() override { return true; } + virtual uint32_t stream_sink_open(uint32_t node, std::string name, uint32_t rate) override; + virtual void stream_set_volumes(uint32_t id, const std::vector &db) override; + virtual void stream_close(uint32_t id) override; + virtual void stream_sink_update(uint32_t id, int16_t const *buffer, int samples_this_frame) override; private: - // Xaudio callbacks - void STDAPICALLTYPE OnVoiceProcessingPassStart(uint32_t bytes_required) noexcept override; - void STDAPICALLTYPE OnVoiceProcessingPassEnd() noexcept override {} - void STDAPICALLTYPE OnStreamEnd() noexcept override {} - void STDAPICALLTYPE OnBufferStart(void* pBufferContext) noexcept override {} - void STDAPICALLTYPE OnLoopEnd(void* pBufferContext) noexcept override {} - void STDAPICALLTYPE OnVoiceError(void* pBufferContext, HRESULT error) noexcept override {} - void STDAPICALLTYPE OnBufferEnd(void *pBufferContext) noexcept override; + struct device_info + { + device_info() = default; + device_info(device_info &&) = default; + device_info &operator=(device_info &&) = default; - void create_buffers(const WAVEFORMATEX &format); - HRESULT create_voices(const WAVEFORMATEX &format); - void process_audio(); - void submit_buffer(std::unique_ptr audioData, DWORD audioLength) const; - void submit_needed(); - void roll_buffer(); - BOOL submit_next_queued(); + static bool compare(device_info const &a, std::wstring_view const &b) noexcept { return a.device_id < b; } - Microsoft::WRL::ComPtr m_xAudio2; - mastering_voice_ptr m_masterVoice; - src_voice_ptr m_sourceVoice; - int m_sample_rate; - float m_audio_latency; - DWORD m_sample_bytes; - std::unique_ptr m_buffer; - DWORD m_buffer_size; - DWORD m_buffer_count; - DWORD m_writepos; - std::mutex m_buffer_lock; - HANDLE m_hEventBufferCompleted; - HANDLE m_hEventDataAvailable; - HANDLE m_hEventExiting; - std::thread m_audioThread; - std::queue m_queue; - std::unique_ptr m_buffer_pool; - uint32_t m_overflows; - uint32_t m_underflows; - BOOL m_in_underflow; - BOOL m_initialized; + std::wstring device_id; + mm_device_ptr device; + x_audio_2_ptr engine; + mastering_voice_ptr mastering_voice; + audio_info::node_info info; + }; + + class voice_info : public IXAudio2VoiceCallback + { + public: + voice_info(sound_xaudio2 &h, WAVEFORMATEX const &format); + ~voice_info(); + + void update(int16_t const *buffer, int samples_this_frame); + void submit_if_needed(); + + source_voice_ptr voice; + audio_info::stream_info info; + std::unique_ptr volume_matrix; + + private: + void roll_buffer(); + void submit_buffer(std::unique_ptr &&data, DWORD length) const; + + virtual void STDMETHODCALLTYPE OnVoiceProcessingPassStart(UINT32 BytesRequired) noexcept override; + virtual void STDMETHODCALLTYPE OnVoiceProcessingPassEnd() noexcept override { } + virtual void STDMETHODCALLTYPE OnStreamEnd() noexcept override { } + virtual void STDMETHODCALLTYPE OnBufferStart(void* pBufferContext) noexcept override { } + virtual void STDMETHODCALLTYPE OnBufferEnd(void *pBufferContext) noexcept override; + virtual void STDMETHODCALLTYPE OnLoopEnd(void* pBufferContext) noexcept override { } + virtual void STDMETHODCALLTYPE OnVoiceError(void* pBufferContext, HRESULT error) noexcept override { } + + sound_xaudio2 & m_host; + std::unique_ptr m_buffer_pool; + std::unique_ptr m_current_buffer; + std::queue m_buffer_queue; + uint32_t m_buffer_size; + unsigned m_sample_bytes; + unsigned m_buffer_count; + uint32_t m_write_position; + bool m_need_update; + bool m_underflowing; + }; + + using device_info_vector = std::vector; + using device_info_vector_iterator = device_info_vector::iterator; + using voice_info_ptr = std::unique_ptr; + using voice_info_vector = std::vector; + + // IMMNotificationClient callbacks + virtual HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDefaultDeviceId) override; + virtual HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) override; + virtual HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) override; + virtual HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) override; + virtual HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDefaultDeviceId, PROPERTYKEY const key) override; + + // stub IUnknown implementation + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override; + virtual ULONG STDMETHODCALLTYPE AddRef() override { return 1; } + virtual ULONG STDMETHODCALLTYPE Release() override { return 1; } + + device_info_vector_iterator find_device(std::wstring_view device_id); + HRESULT add_device(device_info_vector_iterator pos, LPCWSTR device_id); + HRESULT add_device(device_info_vector_iterator pos, std::wstring_view device_id, mm_device_ptr &&device); + HRESULT remove_device(device_info_vector_iterator pos); + + void audio_task(); + void cleanup_task(); + + // tracking audio endpoint devices + mm_device_enumerator_ptr m_device_enum; + device_info_vector m_device_info; + std::mutex m_device_mutex; + uint32_t m_next_node_id; + uint32_t m_default_sink; + bool m_endpoint_notifications; + + // managing cleanup + device_info_vector m_zombie_devices; + std::condition_variable m_cleanup_condition; + std::thread m_cleanup_thread; + std::mutex m_cleanup_mutex; + + voice_info_vector m_voice_info; + std::mutex m_voice_mutex; + uint32_t m_next_voice_id; + + uint32_t m_generation; + bool m_exiting; + + float m_audio_latency; + HANDLE m_hEventNeedUpdate; + HANDLE m_hEventExiting; + std::thread m_audioThread; + std::atomic m_overflows; + std::atomic m_underflows; OSD_DYNAMIC_API(xaudio2, "XAudio2_9.dll", "XAudio2_8.dll"); OSD_DYNAMIC_API_FN(xaudio2, HRESULT, WINAPI, XAudio2Create, IXAudio2 **, uint32_t, XAUDIO2_PROCESSOR); @@ -272,56 +342,103 @@ bool sound_xaudio2::probe() int sound_xaudio2::init(osd_interface &osd, osd_options const &options) { - auto const init_start = std::chrono::system_clock::now(); + HRESULT result; - // Make sure our XAudio2Create entrypoint is bound + // make sure our XAudio2Create entry point is bound if (!OSD_DYNAMIC_API_TEST(XAudio2Create)) { osd_printf_error("Could not find XAudio2. Please try to reinstall DirectX runtime package.\n"); return 1; } - HRESULT result; - std::chrono::milliseconds init_time; - WAVEFORMATEX format = { 0 }; - - m_sample_rate = options.sample_rate(); + // get relevant options m_audio_latency = options.audio_latency(); - if (m_audio_latency == 0.0f) - m_audio_latency = 0.1f; + if (m_audio_latency == 0.0F) + m_audio_latency = 0.1F; - // Create the IXAudio2 object - HR_GOERR(OSD_DYNAMIC_CALL(XAudio2Create, m_xAudio2.GetAddressOf(), 0, XAUDIO2_DEFAULT_PROCESSOR)); + // create a multimedia device enumerator and enumerate devices + HR_GOERR(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&m_device_enum))); + { + co_task_wstr_ptr default_id; + std::lock_guard device_lock(m_device_mutex); - // make a format description for what we want - format.wBitsPerSample = 16; - format.wFormatTag = WAVE_FORMAT_PCM; - format.nChannels = 2; - format.nSamplesPerSec = m_sample_rate; - format.nBlockAlign = format.wBitsPerSample * format.nChannels / 8; - format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; + HR_GOERR(m_device_enum->RegisterEndpointNotificationCallback(this)); + m_endpoint_notifications = true; - m_sample_bytes = format.nBlockAlign; + { + mm_device_ptr default_device; + LPWSTR id_raw = nullptr; + HR_GOERR(m_device_enum->GetDefaultAudioEndpoint(eRender, eMultimedia, default_device.GetAddressOf())); + HR_GOERR(default_device->GetId(&id_raw)); + default_id.reset(std::exchange(id_raw, nullptr)); + } - // Create the buffers - create_buffers(format); + result = enumerate_audio_endpoints( + *m_device_enum.Get(), + eAll, + DEVICE_STATE_ACTIVE | DEVICE_STATE_UNPLUGGED, + [this, &default_id] (HRESULT hr, mm_device_ptr &dev) -> bool + { + if (FAILED(hr) || !dev) + { + osd_printf_error("Error getting audio device. Error: 0x%X\n", static_cast(hr)); + return true; + } + + // skip devices that are disabled or not present + DWORD state; + hr = dev->GetState(&state); + if (FAILED(hr)) + { + osd_printf_error("Error getting audio device state. Error: 0x%X\n", static_cast(hr)); + return true; + } + if ((DEVICE_STATE_ACTIVE != state) && (DEVICE_STATE_UNPLUGGED != state)) + return true; + + // populate node info structure + std::wstring device_id; + audio_info::node_info info; + hr = populate_audio_node_info(*dev.Get(), device_id, info); + if (FAILED(hr)) + return true; + + // skip devices that have no outputs + if (0 >= info.m_sinks) + return true; + + // add a device ID mapping + auto const pos = find_device(device_id); + m_zombie_devices.reserve(m_zombie_devices.size() + m_device_info.size() + 1); + device_info &devinfo = *m_device_info.emplace(pos); + info.m_id = m_next_node_id++; + devinfo.device_id = std::move(device_id); + devinfo.device = std::move(dev); + devinfo.info = std::move(info); + if (!m_default_sink && default_id && (devinfo.device_id == default_id.get())) + m_default_sink = devinfo.info.m_id; + + return true; + }); + if (!m_default_sink && !m_device_info.empty()) + m_default_sink = m_device_info.front().info.m_id; + } + if (FAILED(result)) + { + osd_printf_error("Error enumerating audio endpoints. Error: 0x%X\n", static_cast(result)); + goto Error; + } + + // start a thread to clean up removed devices + m_cleanup_thread = std::thread([] (sound_xaudio2 *self) { self->cleanup_task(); }, this); // Initialize our events - m_hEventBufferCompleted = CreateEvent(nullptr, FALSE, FALSE, nullptr); - m_hEventDataAvailable = CreateEvent(nullptr, FALSE, FALSE, nullptr); + m_hEventNeedUpdate = CreateEvent(nullptr, FALSE, FALSE, nullptr); m_hEventExiting = CreateEvent(nullptr, FALSE, FALSE, nullptr); - // create the voices and start them - HR_GOERR(create_voices(format)); - HR_GOERR(m_sourceVoice->Start()); - // Start the thread listening - m_audioThread = std::thread([] (sound_xaudio2 *self) { self->process_audio(); }, this); + m_audioThread = std::thread([] (sound_xaudio2 *self) { self->audio_task(); }, this); - init_time = std::chrono::duration_cast(std::chrono::system_clock::now() - init_start); - osd_printf_verbose("Sound: XAudio2 initialized. %d ms.\n", init_time.count()); - - m_initialized = TRUE; return 0; Error: @@ -335,6 +452,11 @@ Error: void sound_xaudio2::exit() { + if (m_endpoint_notifications) + { + m_device_enum->UnregisterEndpointNotificationCallback(this); + } + // Wait on processing thread to end if (m_hEventExiting) SetEvent(m_hEventExiting); @@ -342,16 +464,10 @@ void sound_xaudio2::exit() if (m_audioThread.joinable()) m_audioThread.join(); - if (m_hEventBufferCompleted) + if (m_hEventNeedUpdate) { - CloseHandle(m_hEventBufferCompleted); - m_hEventBufferCompleted = nullptr; - } - - if (m_hEventDataAvailable) - { - CloseHandle(m_hEventDataAvailable); - m_hEventDataAvailable = nullptr; + CloseHandle(m_hEventNeedUpdate); + m_hEventNeedUpdate = nullptr; } if (m_hEventExiting) @@ -360,17 +476,281 @@ void sound_xaudio2::exit() m_hEventExiting = nullptr; } - m_sourceVoice.reset(); - m_masterVoice.reset(); - m_xAudio2 = nullptr; - m_buffer.reset(); - m_buffer_pool.reset(); + { + std::lock_guard voice_lock(m_voice_mutex); + m_voice_info.clear(); + } + + if (m_cleanup_thread.joinable()) + { + { + std::lock_guard cleanup_lock(m_cleanup_mutex); + m_exiting = true; + m_cleanup_condition.notify_all(); + } + m_cleanup_thread.join(); + } + + { + std::lock_guard device_lock(m_device_mutex); + m_device_info.clear(); + } + + m_device_enum = nullptr; if (m_overflows != 0 || m_underflows != 0) osd_printf_verbose("Sound: overflows=%u, underflows=%u\n", m_overflows, m_underflows); osd_printf_verbose("Sound: XAudio2 deinitialized\n"); - m_initialized = FALSE; +} + +//============================================================ +// get_generation +//============================================================ + +uint32_t sound_xaudio2::get_generation() +{ + uint32_t result; + { + std::lock_guard device_lock(m_device_mutex); + result = m_generation; + } + return result; +} + +//============================================================ +// get_information +//============================================================ + +audio_info sound_xaudio2::get_information() +{ + audio_info result; + { + std::lock_guard device_lock(m_device_mutex); + + result.m_generation = m_generation; + result.m_default_sink = m_default_sink; + result.m_default_source = 0; + + result.m_nodes.reserve(m_device_info.size()); + for (auto const &device : m_device_info) + result.m_nodes.emplace_back(device.info); + + std::lock_guard voice_lock(m_voice_mutex); + result.m_streams.reserve(m_voice_info.size()); + for (auto const &voice : m_voice_info) + result.m_streams.emplace_back(voice->info); + } + return result; +} + +//============================================================ +// stream_sink_open +//============================================================ + +uint32_t sound_xaudio2::stream_sink_open(uint32_t node, std::string name, uint32_t rate) +{ + std::lock_guard device_lock(m_device_mutex); + HRESULT result; + + // find the requested device + auto const device = std::find_if( + m_device_info.begin(), + m_device_info.end(), + [node] (device_info const &value) { return value.info.m_id == node; }); + if (m_device_info.end() == device) + { + osd_printf_error("Attempt to open audio stream %s for unknown node %u.\n", name, node); + return 0; + } + + // instantiate XAudio2 engine if necessary + if (!device->engine) + { + result = OSD_DYNAMIC_CALL(XAudio2Create, &device->engine, 0, XAUDIO2_DEFAULT_PROCESSOR); + if (FAILED(result) || !device->engine) + { + device->engine = nullptr; + osd_printf_error( + "Error creating XAudio2 engine for audio device %s. Error: 0x%X\n", + device->info.m_name, + static_cast(result)); + return 0; + } + } + + // create a mastering voice if we don't already have one for this device + if (!device->mastering_voice) + { + IXAudio2MasteringVoice *mastering_voice_raw = nullptr; + result = device->engine->CreateMasteringVoice( + &mastering_voice_raw, + device->info.m_sinks, + device->info.m_rate.m_default_rate, + 0, + device->device_id.c_str(), + nullptr, + AudioCategory_Other); + device->mastering_voice.reset(std::exchange(mastering_voice_raw, nullptr)); + if (FAILED(result) || !device->mastering_voice) + { + device->mastering_voice.reset(); + osd_printf_error( + "Error creating mastering voice for audio device %s. Error: 0x%X\n", + device->info.m_name, + static_cast(result)); + return 0; + } + } + + voice_info_ptr info; + WAVEFORMATEX format; + XAUDIO2_SEND_DESCRIPTOR destination; + XAUDIO2_VOICE_SENDS sends; + + try + { + // set up desired input format and destination + format.wFormatTag = WAVE_FORMAT_PCM; + format.nChannels = device->info.m_sinks; + format.nSamplesPerSec = rate; + format.nAvgBytesPerSec = 2 * format.nChannels * rate; + format.nBlockAlign = 2 * format.nChannels; + format.wBitsPerSample = 16; + format.cbSize = 0; + destination.Flags = 0; + destination.pOutputVoice = device->mastering_voice.get(); + sends.SendCount = 1; + sends.pSends = &destination; + + // create the voice info object + info = std::make_unique(*this, format); + info->info.m_node = node; + info->info.m_volumes.resize(device->info.m_sinks, 0.0F); + } + catch (std::bad_alloc const &) + { + return 0; + } + + // create a source voice for this stream + IXAudio2SourceVoice *source_voice_raw = nullptr; + result = device->engine->CreateSourceVoice( + &source_voice_raw, + &format, + XAUDIO2_VOICE_NOPITCH | XAUDIO2_VOICE_NOSRC, + 1.0F, + info.get(), + &sends, + nullptr); + info->voice.reset(std::exchange(source_voice_raw, nullptr)); + if (FAILED(result) || !info->voice) + { + osd_printf_error( + "Error creating source voice for audio device %s. Error: 0x%X\n", + device->info.m_name, + static_cast(result)); + return 0; + } + + // set the channel mapping + result = info->voice->SetOutputMatrix( + nullptr, + format.nChannels, + format.nChannels, + info->volume_matrix.get(), + XAUDIO2_COMMIT_NOW); + if (FAILED(result)) + { + osd_printf_error( + "Error setting source voice output matrix for audio device %s. Error: 0x%X\n", + device->info.m_name, + static_cast(result)); + return 0; + } + + // start the voice + result = info->voice->Start(); + if (FAILED(result)) + { + osd_printf_error( + "Error starting source voice for audio device %s. Error: 0x%X\n", + device->info.m_name, + static_cast(result)); + return 0; + } + + try + { + std::lock_guard voice_lock(m_voice_mutex); + m_voice_info.reserve(m_voice_info.size() + 1); + info->info.m_id = m_next_voice_id++; + assert(m_voice_info.empty() || (m_voice_info.back()->info.m_id < info->info.m_id)); + auto &pos = m_voice_info.emplace_back(std::move(info)); + return pos->info.m_id; + } + catch (std::bad_alloc const &) + { + return 0; + } +} + +//============================================================ +// stream_set_volumes +//============================================================ + +void sound_xaudio2::stream_set_volumes(uint32_t id, const std::vector &db) +{ + std::lock_guard voice_lock(m_voice_mutex); + auto const pos = std::lower_bound( + m_voice_info.begin(), + m_voice_info.end(), + id, + [] (voice_info_ptr const &a, uint32_t b) { return a->info.m_id < b; }); + if ((m_voice_info.end() == pos) || ((*pos)->info.m_id != id)) + return; + + auto const bound = std::min((*pos)->info.m_volumes.size(), db.size()); + for (unsigned i = 0; bound > i; ++i) + { + (*pos)->info.m_volumes[i] = db[i]; + (*pos)->volume_matrix[((*pos)->info.m_volumes.size() * i) + i] = db_to_linear(db[i]); + } + + HRESULT const result = (*pos)->voice->SetOutputMatrix( + nullptr, + (*pos)->info.m_volumes.size(), + (*pos)->info.m_volumes.size(), + (*pos)->volume_matrix.get(), + XAUDIO2_COMMIT_NOW); + if (FAILED(result)) + { + osd_printf_error( + "Error setting source voice output matrix for audio stream %u. Error: 0x%X\n", + (*pos)->info.m_id, + static_cast(result)); + } +} + +//============================================================ +// stream_close +//============================================================ + +void sound_xaudio2::stream_close(uint32_t id) +{ + std::lock_guard voice_lock(m_voice_mutex); + auto const pos = std::lower_bound( + m_voice_info.begin(), + m_voice_info.end(), + id, + [] (voice_info_ptr const &a, uint32_t b) { return a->info.m_id < b; }); + if ((m_voice_info.end() == pos) || ((*pos)->info.m_id != id)) + { + // the sound manager tries to close streams that have disappeared due to device disconnection + return; + } + + m_voice_info.erase(pos); } //============================================================ @@ -378,37 +758,191 @@ void sound_xaudio2::exit() //============================================================ void sound_xaudio2::stream_sink_update( - uint32_t, - int16_t const *buffer, - int samples_this_frame) + uint32_t id, + int16_t const *buffer, + int samples_this_frame) { - if (!m_initialized || m_sample_rate == 0 || !m_buffer) + std::lock_guard voice_lock(m_voice_mutex); + auto const pos = std::lower_bound( + m_voice_info.begin(), + m_voice_info.end(), + id, + [] (voice_info_ptr const &a, uint32_t b) { return a->info.m_id < b; }); + if ((m_voice_info.end() == pos) || ((*pos)->info.m_id != id)) return; - uint32_t const bytes_this_frame = samples_this_frame * m_sample_bytes; + (*pos)->update(buffer, samples_this_frame); +} - std::lock_guard lock(m_buffer_lock); +//============================================================ +// voice_info +//============================================================ - uint32_t bytes_left = bytes_this_frame; - - while (bytes_left > 0) +sound_xaudio2::voice_info::voice_info(sound_xaudio2 &h, WAVEFORMATEX const &format) : + volume_matrix(std::make_unique(format.nChannels * format.nChannels)), + m_host(h), + m_sample_bytes(format.nBlockAlign), + m_write_position(0), + m_need_update(false), + m_underflowing(false) +{ + // set default volume matrix + for (unsigned i = 0; format.nChannels > i; ++i) { - uint32_t chunk = std::min(uint32_t(m_buffer_size), bytes_left); + for (unsigned j = 0; format.nChannels > j; ++j) + volume_matrix[(format.nChannels * i) + j] = (i == j) ? 1.0F : 0.0F; + } - // Roll the buffer if needed - if (m_writepos + chunk >= m_buffer_size) - { + // calculate required buffer size + int const audio_latency_ms = std::max(int(m_host.m_audio_latency * 1000.0F + 0.5F), SUBMIT_FREQUENCY_TARGET_MS); + uint32_t const buffer_total = format.nSamplesPerSec * (audio_latency_ms / 1000.0F) * RESAMPLE_TOLERANCE; + m_buffer_count = audio_latency_ms / SUBMIT_FREQUENCY_TARGET_MS; + m_buffer_size = std::max(1024, buffer_total / m_buffer_count); + + // force to a whole number of samples + uint32_t const remainder = m_buffer_size % format.nBlockAlign; + if (remainder) + m_buffer_size += format.nBlockAlign - remainder; + + // allocate the initial buffers + m_buffer_pool = std::make_unique(m_buffer_count, m_buffer_size); + m_current_buffer = m_buffer_pool->next(); +} + +//============================================================ +// ~voice_info +//============================================================ + +sound_xaudio2::voice_info::~voice_info() +{ + // stop this before any buffers get destructed + voice.reset(); +} + +//============================================================ +// update +//============================================================ + +void sound_xaudio2::voice_info::update(int16_t const *buffer, int samples_this_frame) +{ + uint32_t const bytes_this_frame = samples_this_frame * m_sample_bytes; + uint32_t bytes_left = bytes_this_frame; + while (bytes_left) + { + uint32_t const chunk = std::min(uint32_t(m_buffer_size), bytes_left); + + // roll buffer if the chunk won't fit + if ((m_write_position + chunk) >= m_buffer_size) roll_buffer(); - } - // Copy in the data - memcpy(m_buffer.get() + m_writepos, buffer, chunk); - m_writepos += chunk; + // copy sample data + memcpy(&m_current_buffer[m_write_position], buffer, chunk); + m_write_position += chunk; + buffer += chunk / 2; bytes_left -= chunk; } - // Signal data available - SetEvent(m_hEventDataAvailable); + m_need_update = true; + SetEvent(m_host.m_hEventNeedUpdate); +} + +//============================================================ +// submit_if_needed +//============================================================ + +void sound_xaudio2::voice_info::submit_if_needed() +{ + if (!m_need_update) + return; + + XAUDIO2_VOICE_STATE state; + voice->GetState(&state, XAUDIO2_VOICE_NOSAMPLESPLAYED); + + // If we have buffers queued into XAudio and our current in-memory buffer + // isn't yet full, there's no need to submit it + if ((1 <= state.BuffersQueued) && m_buffer_queue.empty()) + return; + + // We do however want to achieve some kind of minimal latency, so if the queued buffers + // are greater than 2, flush them to re-sync the audio + if (2 < state.BuffersQueued) + { + voice->FlushSourceBuffers(); + m_host.m_overflows.fetch_add(1, std::memory_order_relaxed); + } + + // roll the buffer and submit whatever we have queued + roll_buffer(); + if (!m_buffer_queue.empty()) + { + auto &buf = m_buffer_queue.front(); + assert(0 < buf.AudioSize); + submit_buffer(std::move(buf.AudioData), buf.AudioSize); + m_buffer_queue.pop(); + } +} + +//============================================================ +// roll_buffer +//============================================================ + +void sound_xaudio2::voice_info::roll_buffer() +{ + // don't queue an empty buffer + if (!m_write_position) + return; + + // queue the current buffer + xaudio2_buffer buf; + buf.AudioData = std::move(m_current_buffer); + buf.AudioSize = m_write_position; + m_buffer_queue.push(std::move(buf)); + + // get a fresh buffer + m_current_buffer = m_buffer_pool->next(); + m_write_position = 0; + + // remove excess buffers from queue + if (m_buffer_queue.size() > m_buffer_count) + { + m_host.m_overflows.fetch_add(1, std::memory_order_relaxed); + while (m_buffer_queue.size() > m_buffer_count) + { + // return the oldest buffer to the pool, and remove it from queue + m_buffer_pool->return_to_pool(std::move(m_buffer_queue.front().AudioData)); + m_buffer_queue.pop(); + } + } +} + +//============================================================ +// submit_buffer +//============================================================ + +void sound_xaudio2::voice_info::submit_buffer(std::unique_ptr &&data, DWORD length) const +{ + assert(length); + + XAUDIO2_BUFFER buf = { 0 }; + buf.AudioBytes = length; + buf.pAudioData = data.get(); + buf.PlayBegin = 0; + buf.PlayLength = length / m_sample_bytes; + buf.Flags = XAUDIO2_END_OF_STREAM; + buf.pContext = data.get(); + + HRESULT result; + if (FAILED(result = voice->SubmitSourceBuffer(&buf))) + { + osd_printf_verbose("Sound: XAudio2 failed to submit source buffer (non-fatal). Error: 0x%X\n", static_cast(result)); + m_buffer_pool->return_to_pool(std::move(data)); + return; + } + + // If we succeeded, relinquish the buffer allocation to the XAudio2 runtime + // The buffer will be freed on the OnBufferCompleted callback + // FIXME: does this leak when the voice is destroyed? + data.release(); } //============================================================ @@ -416,16 +950,16 @@ void sound_xaudio2::stream_sink_update( //============================================================ // The XAudio2 voice callback triggered when a buffer finishes playing -void sound_xaudio2::OnBufferEnd(void *pBufferContext) noexcept +void sound_xaudio2::voice_info::OnBufferEnd(void *pBufferContext) noexcept { - BYTE* completed_buffer = static_cast(pBufferContext); - if (completed_buffer != nullptr) - { - std::lock_guard lock(m_buffer_lock); - m_buffer_pool->return_to_pool(completed_buffer); - } + BYTE *const completed_buffer = static_cast(pBufferContext); + std::lock_guard lock(m_host.m_voice_mutex); - SetEvent(m_hEventBufferCompleted); + if (completed_buffer) + m_buffer_pool->return_to_pool(std::unique_ptr(completed_buffer)); + + m_need_update = true; + SetEvent(m_host.m_hEventNeedUpdate); } //============================================================ @@ -433,243 +967,412 @@ void sound_xaudio2::OnBufferEnd(void *pBufferContext) noexcept //============================================================ // The XAudio2 voice callback triggered on every pass -void sound_xaudio2::OnVoiceProcessingPassStart(uint32_t bytes_required) noexcept +void sound_xaudio2::voice_info::OnVoiceProcessingPassStart(UINT32 BytesRequired) noexcept { - if (bytes_required == 0) + if (!BytesRequired) { - // Reset underflow indicator if we're caught up - if (m_in_underflow) m_in_underflow = FALSE; - - return; + m_underflowing = false; } - - // Since there are bytes required, we're going to be in underflow - if (!m_in_underflow) + else if (!m_underflowing) { - m_underflows++; - m_in_underflow = TRUE; + m_underflowing = true; + m_host.m_underflows.fetch_add(1, std::memory_order_relaxed); } } //============================================================ -// create_buffers +// IMMNotificationClient::OnDefaultDeviceChanged //============================================================ -void sound_xaudio2::create_buffers(const WAVEFORMATEX &format) +HRESULT sound_xaudio2::OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDefaultDeviceId) { - // Compute the buffer size - // buffer size is equal to the bytes we need to hold in memory per X thousands of a second where X is audio_latency - int audio_latency_ms = std::max(int(m_audio_latency * 1000.0f + 0.5f), SUBMIT_FREQUENCY_TARGET_MS); - uint32_t format_bytes_per_second = format.nSamplesPerSec * format.nBlockAlign; - uint32_t total_buffer_size = format_bytes_per_second * (audio_latency_ms / 1000.0f) * RESAMPLE_TOLERANCE; + try + { + if ((eRender == flow) && (eMultimedia == role)) + { + std::lock_guard device_lock(m_device_mutex); + std::wstring_view device_id(pwstrDefaultDeviceId); + auto const pos = find_device(device_id); + if ((m_device_info.end() != pos) && (pos->device_id == device_id) && (pos->info.m_id != m_default_sink)) + { + m_default_sink = pos->info.m_id; - // We want to be able to submit buffers every X milliseconds - // I want to divide these up into "packets" so figure out how many buffers we need - m_buffer_count = audio_latency_ms / SUBMIT_FREQUENCY_TARGET_MS; - - // Now record the size of the individual buffers - m_buffer_size = std::max(DWORD(1024), total_buffer_size / m_buffer_count); - - // Make the buffer a multiple of the format size bytes (rounding up) - uint32_t remainder = m_buffer_size % format.nBlockAlign; - if (remainder != 0) - m_buffer_size += format.nBlockAlign - remainder; - - // get our initial buffer pool and our first buffer - m_buffer_pool = std::make_unique(m_buffer_count + 1, m_buffer_size); - m_buffer = std::unique_ptr(m_buffer_pool->next()); - - osd_printf_verbose( - "Sound: XAudio2 created initial buffers. total size: %u, count %u, size each %u\n", - static_cast(total_buffer_size), - static_cast(m_buffer_count), - static_cast(m_buffer_size)); - - // reset buffer states - m_writepos = 0; - m_overflows = 0; - m_underflows = 0; + ++m_generation; + } + } + return S_OK; + } + catch (std::bad_alloc const &) + { + return E_OUTOFMEMORY; + } } //============================================================ -// create_voices +// IMMNotificationClient::OnDeviceAdded //============================================================ -HRESULT sound_xaudio2::create_voices(const WAVEFORMATEX &format) +HRESULT sound_xaudio2::OnDeviceAdded(LPCWSTR pwstrDeviceId) +{ + try + { + std::lock_guard device_lock(m_device_mutex); + std::wstring_view device_id(pwstrDeviceId); + HRESULT result; + auto const pos = find_device(device_id); + + // if the device is already there, something went wrong + if ((m_device_info.end() != pos) && (pos->device_id == device_id)) + { + osd_printf_error( + "Added sound device %s appears to already be present.\n", + osd::text::from_wstring(device_id)); + return S_OK; + } + + // add it if it's an available output device + result = add_device(pos, pwstrDeviceId); + if (FAILED(result)) + return result; + + return S_OK; + } + catch (std::bad_alloc const &) + { + return E_OUTOFMEMORY; + } +} + +//============================================================ +// IMMNotificationClient::OnDeviceRemoved +//============================================================ + +HRESULT sound_xaudio2::OnDeviceRemoved(LPCWSTR pwstrDeviceId) +{ + try + { + std::lock_guard device_lock(m_device_mutex); + std::wstring_view device_id(pwstrDeviceId); + auto const pos = find_device(device_id); + if ((m_device_info.end() != pos) && (pos->device_id == device_id)) + { + HRESULT const result = remove_device(pos); + if (FAILED(result)) + return result; + } + + return S_OK; + } + catch (std::bad_alloc const &) + { + return E_OUTOFMEMORY; + } +} + +//============================================================ +// IMMNotificationClient::OnDeviceStateChanged +//============================================================ + +HRESULT sound_xaudio2::OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) +{ + try + { + std::lock_guard device_lock(m_device_mutex); + std::wstring_view device_id(pwstrDeviceId); + auto const pos = find_device(device_id); + + if ((DEVICE_STATE_ACTIVE == dwNewState) || (DEVICE_STATE_UNPLUGGED == dwNewState)) + { + if ((m_device_info.end() == pos) || (pos->device_id != device_id)) + { + HRESULT result; + + mm_device_ptr device; + result = m_device_enum->GetDevice(pwstrDeviceId, device.GetAddressOf()); + if (FAILED(result)) + { + osd_printf_error( + "Error getting audio device %s. Error: 0x%X\n", + osd::text::from_wstring(device_id), + static_cast(result)); + return result; + } + + result = add_device(pos, device_id, std::move(device)); + if (FAILED(result)) + return result; + } + } + else + { + if ((m_device_info.end() != pos) && (pos->device_id == device_id)) + { + HRESULT const result = remove_device(pos); + if (FAILED(result)) + return result; + } + } + return S_OK; + } + catch (std::bad_alloc const &) + { + return E_OUTOFMEMORY; + } +} + +//============================================================ +// IMMNotificationClient::OnPropertyValueChanged +//============================================================ + +HRESULT sound_xaudio2::OnPropertyValueChanged(LPCWSTR pwstrDeviceId, PROPERTYKEY const key) +{ + try + { + std::lock_guard device_lock(m_device_mutex); + std::wstring_view device_id(pwstrDeviceId); + auto const pos = find_device(device_id); + + if (PKEY_Device_FriendlyName == key) + { + if ((m_device_info.end() != pos) && (pos->device_id == device_id)) + { + HRESULT result; + + property_store_ptr properties; + result = pos->device->OpenPropertyStore(STGM_READ, properties.GetAddressOf()); + if (FAILED(result)) + { + osd_printf_error( + "Error opening property store for audio device %s. Error: 0x%X\n", + pos->info.m_name, + static_cast(result)); + return result; + } + + std::optional name; + result = get_string_property_value(*properties.Get(), PKEY_Device_FriendlyName, name); + if (FAILED(result)) + { + osd_printf_error( + "Error getting updated display name for audio device %s. Error: 0x%X\n", + pos->info.m_name, + static_cast(result)); + return result; + } + + if (name) + { + pos->info.m_name = std::move(*name); + + ++m_generation; + } + } + } + else if (PKEY_AudioEngine_DeviceFormat == key) + { + if ((m_device_info.end() != pos) && (pos->device_id == device_id)) + { + // FIXME: update format + } + else + { + // deal with empty prop + HRESULT const result = add_device(pos, pwstrDeviceId); + if (FAILED(result)) + return result; + } + } + + return S_OK; + } + catch (std::bad_alloc const &) + { + return E_OUTOFMEMORY; + } +} + +//============================================================ +// IUnknown::QueryInterface +//============================================================ + +HRESULT sound_xaudio2::QueryInterface(REFIID riid, void **ppvObject) +{ + if (!ppvObject) + return E_POINTER; + else if (__uuidof(IUnknown) == riid) + *ppvObject = static_cast(this); + else if (__uuidof(IMMNotificationClient) == riid) + *ppvObject = static_cast(this); + else + *ppvObject = nullptr; + + return *ppvObject ? S_OK : E_NOINTERFACE; +} + + +inline sound_xaudio2::device_info_vector_iterator sound_xaudio2::find_device(std::wstring_view device_id) +{ + return std::lower_bound( + m_device_info.begin(), + m_device_info.end(), + device_id, + &device_info::compare); +} + +HRESULT sound_xaudio2::add_device(device_info_vector_iterator pos, LPCWSTR device_id) { - assert(m_xAudio2); - assert(!m_masterVoice); HRESULT result; - IXAudio2MasteringVoice *temp_master_voice = nullptr; - HR_RETHR( - m_xAudio2->CreateMasteringVoice( - &temp_master_voice, - format.nChannels, - m_sample_rate)); + // get the device + mm_device_ptr device; + result = m_device_enum->GetDevice(device_id, device.GetAddressOf()); + if (FAILED(result)) + { + osd_printf_error( + "Error getting audio device %s. Error: 0x%X\n", + osd::text::from_wstring(device_id), + static_cast(result)); + return result; + } - m_masterVoice = mastering_voice_ptr(temp_master_voice); + // ignore if it's disabled or not present + DWORD state; + result = device->GetState(&state); + if (FAILED(result)) + { + osd_printf_error( + "Error getting audio device %s state. Error: 0x%X\n", + osd::text::from_wstring(device_id), + static_cast(result)); + return result; + } + if ((DEVICE_STATE_ACTIVE != state) && (DEVICE_STATE_UNPLUGGED != state)) + return S_OK; - // create the source voice - IXAudio2SourceVoice *temp_source_voice = nullptr; - HR_RETHR(m_xAudio2->CreateSourceVoice( - &temp_source_voice, - &format, - XAUDIO2_VOICE_NOSRC | XAUDIO2_VOICE_NOPITCH, - 1.0, - this)); + // add it if it's an output device + return add_device(pos, device_id, std::move(device)); +} - m_sourceVoice = src_voice_ptr(temp_source_voice); +HRESULT sound_xaudio2::add_device(device_info_vector_iterator pos, std::wstring_view device_id, mm_device_ptr &&device) +{ + std::wstring device_id_str; + audio_info::node_info info; + HRESULT const result = populate_audio_node_info(*device.Get(), device_id_str, info); + if (FAILED(result)) + return result; + assert(device_id == device_id_str); + + if (0 < info.m_sinks) + { + try + { + { + std::lock_guard cleanup_lock(m_cleanup_mutex); + m_zombie_devices.reserve(m_zombie_devices.size() + m_device_info.size() + 1); + } + device_info &devinfo = *m_device_info.emplace(pos); + info.m_id = m_next_node_id++; + devinfo.device_id = std::move(device_id_str); + devinfo.device = std::move(device); + devinfo.info = std::move(info); + + ++m_generation; + } + catch (std::bad_alloc const &) + { + return E_OUTOFMEMORY; + } + } return S_OK; } -//============================================================ -// process_audio -//============================================================ - -// submits audio events on another thread in a loop -void sound_xaudio2::process_audio() +HRESULT sound_xaudio2::remove_device(device_info_vector_iterator pos) { - BOOL exiting = FALSE; - HANDLE hEvents[] = { m_hEventBufferCompleted, m_hEventDataAvailable, m_hEventExiting }; + // if this was the default device, choose a new default arbitrarily + if (pos->info.m_id == m_default_sink) + { + if (m_device_info.begin() != pos) + { + m_default_sink = m_device_info.front().info.m_id; + } + else + { + auto const next = std::next(pos); + if (m_device_info.end() != next) + m_default_sink = next->info.m_id; + else + m_default_sink = 0; + } + } + + // clean up any voices associated with this device + { + std::lock_guard voice_lock(m_voice_mutex); + auto it = m_voice_info.begin(); + while (m_voice_info.end() != it) + { + if ((*it)->info.m_node == pos->info.m_id) + { + it = m_voice_info.erase(it); + } + else + ++it; + } + } + + // flag the device for cleanup + { + std::lock_guard cleanup_lock(m_cleanup_mutex); + m_zombie_devices.emplace_back(std::move(*pos)); + m_cleanup_condition.notify_one(); + } + m_device_info.erase(pos); + + ++m_generation; + + return S_OK; +} + +void sound_xaudio2::audio_task() +{ + bool exiting = FALSE; + HANDLE hEvents[] = { m_hEventNeedUpdate, m_hEventExiting }; while (!exiting) { - DWORD wait_result = WaitForMultipleObjects(3, hEvents, FALSE, INFINITE); + DWORD wait_result = WaitForMultipleObjects(std::size(hEvents), hEvents, FALSE, INFINITE); switch (wait_result) { - // Buffer is complete or new data is available - case 0: - case 1: - submit_needed(); + case 0: // buffer is complete or new data is available + { + std::lock_guard voice_lock(m_voice_mutex); + for (auto const &voice : m_voice_info) + voice->submit_if_needed(); + } break; - case 2: - // exiting - exiting = TRUE; + case 1: // exiting + exiting = true; break; } } } -//============================================================ -// submit_needed -//============================================================ - -// Submits any buffers that have currently been queued, -// assuming they are needed based on current queue depth -void sound_xaudio2::submit_needed() +void sound_xaudio2::cleanup_task() { - XAUDIO2_VOICE_STATE state; - m_sourceVoice->GetState(&state, XAUDIO2_VOICE_NOSAMPLESPLAYED); - - std::lock_guard lock(m_buffer_lock); - - // If we have buffers queued into XAudio and our current in-memory buffer - // isn't yet full, there's no need to submit it - if (state.BuffersQueued >= 1 && m_queue.empty()) - return; - - // We do however want to achieve some kind of minimal latency, so if the queued buffers - // are greater than 2, flush them to re-sync the audio - if (state.BuffersQueued > 2) + // clean up removed devices on a separate thread to avoid deadlocks + std::unique_lock cleanup_lock(m_cleanup_mutex); + while (!m_exiting) { - m_sourceVoice->FlushSourceBuffers(); - m_overflows++; - } - - // Roll the buffer - roll_buffer(); - - // Submit the next buffer - submit_next_queued(); -} - -//============================================================ -// submit_buffer -//============================================================ - -void sound_xaudio2::submit_buffer(std::unique_ptr audioData, DWORD audioLength) const -{ - assert(audioLength != 0); - - XAUDIO2_BUFFER buf = { 0 }; - buf.AudioBytes = audioLength; - buf.pAudioData = audioData.get(); - buf.PlayBegin = 0; - buf.PlayLength = audioLength / m_sample_bytes; - buf.Flags = XAUDIO2_END_OF_STREAM; - buf.pContext = audioData.get(); - - HRESULT result; - if (FAILED(result = m_sourceVoice->SubmitSourceBuffer(&buf))) - { - osd_printf_verbose("Sound: XAudio2 failed to submit source buffer (non-fatal). Error: 0x%X\n", static_cast(result)); - m_buffer_pool->return_to_pool(audioData.release()); - return; - } - - // If we succeeded, relinquish the buffer allocation to the XAudio2 runtime - // The buffer will be freed on the OnBufferCompleted callback - audioData.release(); -} - -//============================================================ -// submit_next_queued -//============================================================ - -BOOL sound_xaudio2::submit_next_queued() -{ - if (!m_queue.empty()) - { - // Get a reference to the buffer - auto buf = &m_queue.front(); - - // submit the buffer data - submit_buffer(std::move(buf->AudioData), buf->AudioSize); - - // Remove it from the queue - assert(buf->AudioSize > 0); - m_queue.pop(); - - return !m_queue.empty(); - } - - // queue was already empty - return FALSE; -} - -//============================================================ -// roll_buffer -//============================================================ - -// Queues the current buffer, and gets a new write buffer -void sound_xaudio2::roll_buffer() -{ - // Don't queue a buffer if it is empty - if (m_writepos == 0) - return; - - // Queue the current buffer - xaudio2_buffer buf; - buf.AudioData = std::move(m_buffer); - buf.AudioSize = m_writepos; - m_queue.push(std::move(buf)); - - // Get a new buffer - m_buffer = std::unique_ptr(m_buffer_pool->next()); - m_writepos = 0; - - // We only want to keep a maximum number of buffers at any given time - // so remove any from queue greater than our target count - if (m_queue.size() > m_buffer_count) - { - xaudio2_buffer *next_buffer = &m_queue.front(); - - // return the oldest buffer to the pool, and remove it from queue - m_buffer_pool->return_to_pool(next_buffer->AudioData.release()); - m_queue.pop(); - - m_overflows++; + m_cleanup_condition.wait(cleanup_lock); + while (!m_zombie_devices.empty()) + { + device_info device(std::move(m_zombie_devices.back())); + m_zombie_devices.pop_back(); + cleanup_lock.unlock(); + device.mastering_voice.reset(); + device.engine = nullptr; + cleanup_lock.lock(); + } } }