summaryrefslogtreecommitdiffstats
path: root/media/libcubeb/src/cubeb_wasapi.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /media/libcubeb/src/cubeb_wasapi.cpp
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'media/libcubeb/src/cubeb_wasapi.cpp')
-rw-r--r--media/libcubeb/src/cubeb_wasapi.cpp3274
1 files changed, 3274 insertions, 0 deletions
diff --git a/media/libcubeb/src/cubeb_wasapi.cpp b/media/libcubeb/src/cubeb_wasapi.cpp
new file mode 100644
index 0000000000..b7b337ce78
--- /dev/null
+++ b/media/libcubeb/src/cubeb_wasapi.cpp
@@ -0,0 +1,3274 @@
+/*
+ * Copyright © 2013 Mozilla Foundation
+ *
+ * This program is made available under an ISC-style license. See the
+ * accompanying file LICENSE for details.
+ */
+#define _WIN32_WINNT 0x0603
+#define NOMINMAX
+
+#include <initguid.h>
+#include <windows.h>
+#include <mmdeviceapi.h>
+#include <windef.h>
+#include <audioclient.h>
+#include <devicetopology.h>
+#include <process.h>
+#include <avrt.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <cmath>
+#include <algorithm>
+#include <memory>
+#include <limits>
+#include <atomic>
+#include <vector>
+
+#include "cubeb/cubeb.h"
+#include "cubeb-internal.h"
+#include "cubeb_mixer.h"
+#include "cubeb_resampler.h"
+#include "cubeb_strings.h"
+#include "cubeb_utils.h"
+
+// Windows 10 exposes the IAudioClient3 interface to create low-latency streams.
+// Copy the interface definition from audioclient.h here to make the code simpler
+// and so that we can still access IAudioClient3 via COM if cubeb was compiled
+// against an older SDK.
+#ifndef __IAudioClient3_INTERFACE_DEFINED__
+#define __IAudioClient3_INTERFACE_DEFINED__
+MIDL_INTERFACE("7ED4EE07-8E67-4CD4-8C1A-2B7A5987AD42")
+IAudioClient3 : public IAudioClient
+{
+public:
+ virtual HRESULT STDMETHODCALLTYPE GetSharedModeEnginePeriod(
+ /* [annotation][in] */
+ _In_ const WAVEFORMATEX *pFormat,
+ /* [annotation][out] */
+ _Out_ UINT32 *pDefaultPeriodInFrames,
+ /* [annotation][out] */
+ _Out_ UINT32 *pFundamentalPeriodInFrames,
+ /* [annotation][out] */
+ _Out_ UINT32 *pMinPeriodInFrames,
+ /* [annotation][out] */
+ _Out_ UINT32 *pMaxPeriodInFrames) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetCurrentSharedModeEnginePeriod(
+ /* [unique][annotation][out] */
+ _Out_ WAVEFORMATEX **ppFormat,
+ /* [annotation][out] */
+ _Out_ UINT32 *pCurrentPeriodInFrames) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE InitializeSharedAudioStream(
+ /* [annotation][in] */
+ _In_ DWORD StreamFlags,
+ /* [annotation][in] */
+ _In_ UINT32 PeriodInFrames,
+ /* [annotation][in] */
+ _In_ const WAVEFORMATEX *pFormat,
+ /* [annotation][in] */
+ _In_opt_ LPCGUID AudioSessionGuid) = 0;
+};
+#ifdef __CRT_UUID_DECL
+// Required for MinGW
+__CRT_UUID_DECL(IAudioClient3, 0x7ED4EE07, 0x8E67, 0x4CD4, 0x8C, 0x1A, 0x2B, 0x7A, 0x59, 0x87, 0xAD, 0x42)
+#endif
+#endif
+// Copied from audioclient.h in the Windows 10 SDK
+#ifndef AUDCLNT_E_ENGINE_PERIODICITY_LOCKED
+#define AUDCLNT_E_ENGINE_PERIODICITY_LOCKED AUDCLNT_ERR(0x028)
+#endif
+
+#ifndef PKEY_Device_FriendlyName
+DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); // DEVPROP_TYPE_STRING
+#endif
+#ifndef PKEY_Device_InstanceId
+DEFINE_PROPERTYKEY(PKEY_Device_InstanceId, 0x78c34fc8, 0x104a, 0x4aca, 0x9e, 0xa4, 0x52, 0x4d, 0x52, 0x99, 0x6e, 0x57, 0x00000100); // VT_LPWSTR
+#endif
+
+namespace {
+
+const int64_t LATENCY_NOT_AVAILABLE_YET = -1;
+
+struct com_heap_ptr_deleter {
+ void operator()(void * ptr) const noexcept {
+ CoTaskMemFree(ptr);
+ }
+};
+
+template <typename T>
+using com_heap_ptr = std::unique_ptr<T, com_heap_ptr_deleter>;
+
+template<typename T, size_t N>
+constexpr size_t
+ARRAY_LENGTH(T(&)[N])
+{
+ return N;
+}
+
+template <typename T>
+class no_addref_release : public T {
+ ULONG STDMETHODCALLTYPE AddRef() = 0;
+ ULONG STDMETHODCALLTYPE Release() = 0;
+};
+
+template <typename T>
+class com_ptr {
+public:
+ com_ptr() noexcept = default;
+
+ com_ptr(com_ptr const & other) noexcept = delete;
+ com_ptr & operator=(com_ptr const & other) noexcept = delete;
+ T ** operator&() const noexcept = delete;
+
+ ~com_ptr() noexcept {
+ release();
+ }
+
+ com_ptr(com_ptr && other) noexcept
+ : ptr(other.ptr)
+ {
+ other.ptr = nullptr;
+ }
+
+ com_ptr & operator=(com_ptr && other) noexcept {
+ if (ptr != other.ptr) {
+ release();
+ ptr = other.ptr;
+ other.ptr = nullptr;
+ }
+ return *this;
+ }
+
+ explicit operator bool() const noexcept {
+ return nullptr != ptr;
+ }
+
+ no_addref_release<T> * operator->() const noexcept {
+ return static_cast<no_addref_release<T> *>(ptr);
+ }
+
+ T * get() const noexcept {
+ return ptr;
+ }
+
+ T ** receive() noexcept {
+ XASSERT(ptr == nullptr);
+ return &ptr;
+ }
+
+ void ** receive_vpp() noexcept {
+ return reinterpret_cast<void **>(receive());
+ }
+
+ com_ptr & operator=(std::nullptr_t) noexcept {
+ release();
+ return *this;
+ }
+
+ void reset(T * p = nullptr) noexcept {
+ release();
+ ptr = p;
+ }
+
+private:
+ void release() noexcept {
+ T * temp = ptr;
+
+ if (temp) {
+ ptr = nullptr;
+ temp->Release();
+ }
+ }
+
+ T * ptr = nullptr;
+};
+
+extern cubeb_ops const wasapi_ops;
+
+int wasapi_stream_stop(cubeb_stream * stm);
+int wasapi_stream_start(cubeb_stream * stm);
+void close_wasapi_stream(cubeb_stream * stm);
+int setup_wasapi_stream(cubeb_stream * stm);
+ERole pref_to_role(cubeb_stream_prefs param);
+int wasapi_create_device(cubeb * ctx, cubeb_device_info& ret, IMMDeviceEnumerator * enumerator, IMMDevice * dev);
+void wasapi_destroy_device(cubeb_device_info * device_info);
+static int wasapi_enumerate_devices(cubeb * context, cubeb_device_type type, cubeb_device_collection * out);
+static int wasapi_device_collection_destroy(cubeb * ctx, cubeb_device_collection * collection);
+static char const * wstr_to_utf8(wchar_t const * str);
+static std::unique_ptr<wchar_t const []> utf8_to_wstr(char const * str);
+
+}
+
+class wasapi_collection_notification_client;
+class monitor_device_notifications;
+
+struct cubeb {
+ cubeb_ops const * ops = &wasapi_ops;
+ cubeb_strings * device_ids;
+ /* Device enumerator to get notifications when the
+ device collection change. */
+ com_ptr<IMMDeviceEnumerator> device_collection_enumerator;
+ com_ptr<wasapi_collection_notification_client> collection_notification_client;
+ /* Collection changed for input (capture) devices. */
+ cubeb_device_collection_changed_callback input_collection_changed_callback = nullptr;
+ void * input_collection_changed_user_ptr = nullptr;
+ /* Collection changed for output (render) devices. */
+ cubeb_device_collection_changed_callback output_collection_changed_callback = nullptr;
+ void * output_collection_changed_user_ptr = nullptr;
+ UINT64 performance_counter_frequency;
+};
+
+class wasapi_endpoint_notification_client;
+
+/* We have three possible callbacks we can use with a stream:
+ * - input only
+ * - output only
+ * - synchronized input and output
+ *
+ * Returns true when we should continue to play, false otherwise.
+ */
+typedef bool (*wasapi_refill_callback)(cubeb_stream * stm);
+
+struct cubeb_stream {
+ /* Note: Must match cubeb_stream layout in cubeb.c. */
+ cubeb * context = nullptr;
+ void * user_ptr = nullptr;
+ /**/
+
+ /* Mixer pameters. We need to convert the input stream to this
+ samplerate/channel layout, as WASAPI does not resample nor upmix
+ itself. */
+ cubeb_stream_params input_mix_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE };
+ cubeb_stream_params output_mix_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE };
+ /* Stream parameters. This is what the client requested,
+ * and what will be presented in the callback. */
+ cubeb_stream_params input_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE };
+ cubeb_stream_params output_stream_params = { CUBEB_SAMPLE_FLOAT32NE, 0, 0, CUBEB_LAYOUT_UNDEFINED, CUBEB_STREAM_PREF_NONE };
+ /* A MMDevice role for this stream: either communication or console here. */
+ ERole role;
+ /* True if this stream will transport voice-data. */
+ bool voice;
+ /* True if the input device of this stream is using bluetooth handsfree. */
+ bool input_bluetooth_handsfree;
+ /* The input and output device, or NULL for default. */
+ std::unique_ptr<const wchar_t[]> input_device_id;
+ std::unique_ptr<const wchar_t[]> output_device_id;
+ com_ptr<IMMDevice> input_device;
+ com_ptr<IMMDevice> output_device;
+ /* The latency initially requested for this stream, in frames. */
+ unsigned latency = 0;
+ cubeb_state_callback state_callback = nullptr;
+ cubeb_data_callback data_callback = nullptr;
+ wasapi_refill_callback refill_callback = nullptr;
+ /* True when a loopback device is requested with no output device. In this
+ case a dummy output device is opened to drive the loopback, but should not
+ be exposed. */
+ bool has_dummy_output = false;
+ /* Lifetime considerations:
+ - client, render_client, audio_clock and audio_stream_volume are interface
+ pointer to the IAudioClient.
+ - The lifetime for device_enumerator and notification_client, resampler,
+ mix_buffer are the same as the cubeb_stream instance. */
+
+ /* Main handle on the WASAPI stream. */
+ com_ptr<IAudioClient> output_client;
+ /* Interface pointer to use the event-driven interface. */
+ com_ptr<IAudioRenderClient> render_client;
+ /* Interface pointer to use the volume facilities. */
+ com_ptr<IAudioStreamVolume> audio_stream_volume;
+ /* Interface pointer to use the stream audio clock. */
+ com_ptr<IAudioClock> audio_clock;
+ /* Frames written to the stream since it was opened. Reset on device
+ change. Uses mix_params.rate. */
+ UINT64 frames_written = 0;
+ /* Frames written to the (logical) stream since it was first
+ created. Updated on device change. Uses stream_params.rate. */
+ UINT64 total_frames_written = 0;
+ /* Last valid reported stream position. Used to ensure the position
+ reported by stream_get_position increases monotonically. */
+ UINT64 prev_position = 0;
+ /* Device enumerator to be able to be notified when the default
+ device change. */
+ com_ptr<IMMDeviceEnumerator> device_enumerator;
+ /* Device notification client, to be able to be notified when the default
+ audio device changes and route the audio to the new default audio output
+ device */
+ com_ptr<wasapi_endpoint_notification_client> notification_client;
+ /* Main andle to the WASAPI capture stream. */
+ com_ptr<IAudioClient> input_client;
+ /* Interface to use the event driven capture interface */
+ com_ptr<IAudioCaptureClient> capture_client;
+ /* This event is set by the stream_stop and stream_destroy
+ function, so the render loop can exit properly. */
+ HANDLE shutdown_event = 0;
+ /* Set by OnDefaultDeviceChanged when a stream reconfiguration is required.
+ The reconfiguration is handled by the render loop thread. */
+ HANDLE reconfigure_event = 0;
+ /* This is set by WASAPI when we should refill the stream. */
+ HANDLE refill_event = 0;
+ /* This is set by WASAPI when we should read from the input stream. In
+ * practice, we read from the input stream in the output callback, so
+ * this is not used, but it is necessary to start getting input data. */
+ HANDLE input_available_event = 0;
+ /* Each cubeb_stream has its own thread. */
+ HANDLE thread = 0;
+ /* The lock protects all members that are touched by the render thread or
+ change during a device reset, including: audio_clock, audio_stream_volume,
+ client, frames_written, mix_params, total_frames_written, prev_position. */
+ owned_critical_section stream_reset_lock;
+ /* Maximum number of frames that can be passed down in a callback. */
+ uint32_t input_buffer_frame_count = 0;
+ /* Maximum number of frames that can be requested in a callback. */
+ uint32_t output_buffer_frame_count = 0;
+ /* Resampler instance. Resampling will only happen if necessary. */
+ std::unique_ptr<cubeb_resampler, decltype(&cubeb_resampler_destroy)> resampler = { nullptr, cubeb_resampler_destroy };
+ /* Mixer interfaces */
+ std::unique_ptr<cubeb_mixer, decltype(&cubeb_mixer_destroy)> output_mixer = { nullptr, cubeb_mixer_destroy };
+ std::unique_ptr<cubeb_mixer, decltype(&cubeb_mixer_destroy)> input_mixer = { nullptr, cubeb_mixer_destroy };
+ /* A buffer for up/down mixing multi-channel audio output. */
+ std::vector<BYTE> mix_buffer;
+ /* WASAPI input works in "packets". We re-linearize the audio packets
+ * into this buffer before handing it to the resampler. */
+ std::unique_ptr<auto_array_wrapper> linear_input_buffer;
+ /* Bytes per sample. This multiplied by the number of channels is the number
+ * of bytes per frame. */
+ size_t bytes_per_sample = 0;
+ /* WAVEFORMATEXTENSIBLE sub-format: either PCM or float. */
+ GUID waveformatextensible_sub_format = GUID_NULL;
+ /* Stream volume. Set via stream_set_volume and used to reset volume on
+ device changes. */
+ float volume = 1.0;
+ /* True if the stream is draining. */
+ bool draining = false;
+ /* True when we've destroyed the stream. This pointer is leaked on stream
+ * destruction if we could not join the thread. */
+ std::atomic<std::atomic<bool>*> emergency_bailout { nullptr };
+ /* Synchronizes render thread start to ensure safe access to emergency_bailout. */
+ HANDLE thread_ready_event = 0;
+ /* This needs an active audio input stream to be known, and is updated in the
+ * first audio input callback. */
+ std::atomic<int64_t> input_latency_hns { LATENCY_NOT_AVAILABLE_YET };
+};
+
+class monitor_device_notifications {
+public:
+ monitor_device_notifications(cubeb * context)
+ : cubeb_context(context)
+ {
+ create_thread();
+ }
+
+ ~monitor_device_notifications()
+ {
+ SetEvent(begin_shutdown);
+ WaitForSingleObject(shutdown_complete, INFINITE);
+ CloseHandle(thread);
+
+ CloseHandle(input_changed);
+ CloseHandle(output_changed);
+ CloseHandle(begin_shutdown);
+ CloseHandle(shutdown_complete);
+ }
+
+ void notify(EDataFlow flow)
+ {
+ XASSERT(cubeb_context);
+ if (flow == eCapture && cubeb_context->input_collection_changed_callback) {
+ bool res = SetEvent(input_changed);
+ if (!res) {
+ LOG("Failed to set input changed event");
+ }
+ return;
+ }
+ if (flow == eRender && cubeb_context->output_collection_changed_callback) {
+ bool res = SetEvent(output_changed);
+ if (!res) {
+ LOG("Failed to set output changed event");
+ }
+ }
+ }
+private:
+ static unsigned int __stdcall
+ thread_proc(LPVOID args)
+ {
+ XASSERT(args);
+ auto mdn = static_cast<monitor_device_notifications*>(args);
+ mdn->notification_thread_loop();
+ SetEvent(mdn->shutdown_complete);
+ return 0;
+ }
+
+ void notification_thread_loop()
+ {
+ struct auto_com {
+ auto_com() {
+ HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+ XASSERT(SUCCEEDED(hr));
+ }
+ ~auto_com() {
+ CoUninitialize();
+ }
+ } com;
+
+ HANDLE wait_array[3] = {
+ input_changed,
+ output_changed,
+ begin_shutdown,
+ };
+
+ while (true) {
+ Sleep(200);
+
+ DWORD wait_result = WaitForMultipleObjects(ARRAY_LENGTH(wait_array),
+ wait_array,
+ FALSE,
+ INFINITE);
+ if (wait_result == WAIT_OBJECT_0) { // input changed
+ cubeb_context->input_collection_changed_callback(cubeb_context,
+ cubeb_context->input_collection_changed_user_ptr);
+ } else if (wait_result == WAIT_OBJECT_0 + 1) { // output changed
+ cubeb_context->output_collection_changed_callback(cubeb_context,
+ cubeb_context->output_collection_changed_user_ptr);
+ } else if (wait_result == WAIT_OBJECT_0 + 2) { // shutdown
+ break;
+ } else {
+ LOG("Unexpected result %lu", wait_result);
+ }
+ } // loop
+ }
+
+ void create_thread()
+ {
+ output_changed = CreateEvent(nullptr, 0, 0, nullptr);
+ if (!output_changed) {
+ LOG("Failed to create output changed event.");
+ return;
+ }
+
+ input_changed = CreateEvent(nullptr, 0, 0, nullptr);
+ if (!input_changed) {
+ LOG("Failed to create input changed event.");
+ return;
+ }
+
+ begin_shutdown = CreateEvent(nullptr, 0, 0, nullptr);
+ if (!begin_shutdown) {
+ LOG("Failed to create begin_shutdown event.");
+ return;
+ }
+
+ shutdown_complete = CreateEvent(nullptr, 0, 0, nullptr);
+ if (!shutdown_complete) {
+ LOG("Failed to create shutdown_complete event.");
+ return;
+ }
+
+ thread = (HANDLE) _beginthreadex(nullptr,
+ 256 * 1024,
+ thread_proc,
+ this,
+ STACK_SIZE_PARAM_IS_A_RESERVATION,
+ nullptr);
+ if (!thread) {
+ LOG("Failed to create thread.");
+ return;
+ }
+ }
+
+ HANDLE thread = INVALID_HANDLE_VALUE;
+ HANDLE output_changed = INVALID_HANDLE_VALUE;
+ HANDLE input_changed = INVALID_HANDLE_VALUE;
+ HANDLE begin_shutdown = INVALID_HANDLE_VALUE;
+ HANDLE shutdown_complete = INVALID_HANDLE_VALUE;
+
+ cubeb * cubeb_context = nullptr;
+};
+
+class wasapi_collection_notification_client : public IMMNotificationClient
+{
+public:
+ /* The implementation of MSCOM was copied from MSDN. */
+ ULONG STDMETHODCALLTYPE
+ AddRef()
+ {
+ return InterlockedIncrement(&ref_count);
+ }
+
+ ULONG STDMETHODCALLTYPE
+ Release()
+ {
+ ULONG ulRef = InterlockedDecrement(&ref_count);
+ if (0 == ulRef) {
+ delete this;
+ }
+ return ulRef;
+ }
+
+ HRESULT STDMETHODCALLTYPE
+ QueryInterface(REFIID riid, VOID **ppvInterface)
+ {
+ if (__uuidof(IUnknown) == riid) {
+ AddRef();
+ *ppvInterface = (IUnknown*)this;
+ } else if (__uuidof(IMMNotificationClient) == riid) {
+ AddRef();
+ *ppvInterface = (IMMNotificationClient*)this;
+ } else {
+ *ppvInterface = NULL;
+ return E_NOINTERFACE;
+ }
+ return S_OK;
+ }
+
+ wasapi_collection_notification_client(cubeb * context)
+ : ref_count(1)
+ , cubeb_context(context)
+ , monitor_notifications(context)
+ {
+ XASSERT(cubeb_context);
+ }
+
+ virtual ~wasapi_collection_notification_client()
+ { }
+
+ HRESULT STDMETHODCALLTYPE
+ OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR device_id)
+ {
+ LOG("collection: Audio device default changed, id = %S.", device_id);
+ return S_OK;
+ }
+
+ /* The remaining methods are not implemented, they simply log when called (if
+ log is enabled), for debugging. */
+ HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR device_id)
+ {
+ LOG("collection: Audio device added.");
+ return S_OK;
+ };
+
+ HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR device_id)
+ {
+ LOG("collection: Audio device removed.");
+ return S_OK;
+ }
+
+ HRESULT STDMETHODCALLTYPE
+ OnDeviceStateChanged(LPCWSTR device_id, DWORD new_state)
+ {
+ XASSERT(cubeb_context->output_collection_changed_callback ||
+ cubeb_context->input_collection_changed_callback);
+ LOG("collection: Audio device state changed, id = %S, state = %lu.", device_id, new_state);
+ EDataFlow flow;
+ HRESULT hr = GetDataFlow(device_id, &flow);
+ if (FAILED(hr)) {
+ return hr;
+ }
+ monitor_notifications.notify(flow);
+ return S_OK;
+ }
+
+ HRESULT STDMETHODCALLTYPE
+ OnPropertyValueChanged(LPCWSTR device_id, const PROPERTYKEY key)
+ {
+ //Audio device property value changed.
+ return S_OK;
+ }
+
+private:
+ HRESULT GetDataFlow(LPCWSTR device_id, EDataFlow * flow)
+ {
+ com_ptr<IMMDevice> device;
+ com_ptr<IMMEndpoint> endpoint;
+
+ HRESULT hr = cubeb_context->device_collection_enumerator
+ ->GetDevice(device_id, device.receive());
+ if (FAILED(hr)) {
+ LOG("collection: Could not get device: %lx", hr);
+ return hr;
+ }
+
+ hr = device->QueryInterface(IID_PPV_ARGS(endpoint.receive()));
+ if (FAILED(hr)) {
+ LOG("collection: Could not get endpoint: %lx", hr);
+ return hr;
+ }
+
+ return endpoint->GetDataFlow(flow);
+ }
+
+ /* refcount for this instance, necessary to implement MSCOM semantics. */
+ LONG ref_count;
+
+ cubeb * cubeb_context = nullptr;
+ monitor_device_notifications monitor_notifications;
+};
+
+class wasapi_endpoint_notification_client : public IMMNotificationClient
+{
+public:
+ /* The implementation of MSCOM was copied from MSDN. */
+ ULONG STDMETHODCALLTYPE
+ AddRef()
+ {
+ return InterlockedIncrement(&ref_count);
+ }
+
+ ULONG STDMETHODCALLTYPE
+ Release()
+ {
+ ULONG ulRef = InterlockedDecrement(&ref_count);
+ if (0 == ulRef) {
+ delete this;
+ }
+ return ulRef;
+ }
+
+ HRESULT STDMETHODCALLTYPE
+ QueryInterface(REFIID riid, VOID **ppvInterface)
+ {
+ if (__uuidof(IUnknown) == riid) {
+ AddRef();
+ *ppvInterface = (IUnknown*)this;
+ } else if (__uuidof(IMMNotificationClient) == riid) {
+ AddRef();
+ *ppvInterface = (IMMNotificationClient*)this;
+ } else {
+ *ppvInterface = NULL;
+ return E_NOINTERFACE;
+ }
+ return S_OK;
+ }
+
+ wasapi_endpoint_notification_client(HANDLE event, ERole role)
+ : ref_count(1)
+ , reconfigure_event(event)
+ , role(role)
+ { }
+
+ virtual ~wasapi_endpoint_notification_client()
+ { }
+
+ HRESULT STDMETHODCALLTYPE
+ OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR device_id)
+ {
+ LOG("endpoint: Audio device default changed.");
+
+ /* we only support a single stream type for now. */
+ if (flow != eRender && role != this->role) {
+ return S_OK;
+ }
+
+ BOOL ok = SetEvent(reconfigure_event);
+ if (!ok) {
+ LOG("endpoint: SetEvent on reconfigure_event failed: %lx", GetLastError());
+ }
+
+ return S_OK;
+ }
+
+ /* The remaining methods are not implemented, they simply log when called (if
+ log is enabled), for debugging. */
+ HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR device_id)
+ {
+ LOG("endpoint: Audio device added.");
+ return S_OK;
+ };
+
+ HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR device_id)
+ {
+ LOG("endpoint: Audio device removed.");
+ return S_OK;
+ }
+
+ HRESULT STDMETHODCALLTYPE
+ OnDeviceStateChanged(LPCWSTR device_id, DWORD new_state)
+ {
+ LOG("endpoint: Audio device state changed.");
+ return S_OK;
+ }
+
+ HRESULT STDMETHODCALLTYPE
+ OnPropertyValueChanged(LPCWSTR device_id, const PROPERTYKEY key)
+ {
+ //Audio device property value changed.
+ return S_OK;
+ }
+private:
+ /* refcount for this instance, necessary to implement MSCOM semantics. */
+ LONG ref_count;
+ HANDLE reconfigure_event;
+ ERole role;
+};
+
+namespace {
+
+char const *
+intern_device_id(cubeb * ctx, wchar_t const * id)
+{
+ XASSERT(id);
+
+ char const * tmp = wstr_to_utf8(id);
+ if (!tmp) {
+ return nullptr;
+ }
+
+ char const * interned = cubeb_strings_intern(ctx->device_ids, tmp);
+
+ free((void *) tmp);
+
+ return interned;
+}
+
+bool has_input(cubeb_stream * stm)
+{
+ return stm->input_stream_params.rate != 0;
+}
+
+bool has_output(cubeb_stream * stm)
+{
+ return stm->output_stream_params.rate != 0;
+}
+
+double stream_to_mix_samplerate_ratio(cubeb_stream_params & stream, cubeb_stream_params & mixer)
+{
+ return double(stream.rate) / mixer.rate;
+}
+
+/* Convert the channel layout into the corresponding KSAUDIO_CHANNEL_CONFIG.
+ See more: https://msdn.microsoft.com/en-us/library/windows/hardware/ff537083(v=vs.85).aspx */
+
+cubeb_channel_layout
+mask_to_channel_layout(WAVEFORMATEX const * fmt)
+{
+ cubeb_channel_layout mask = 0;
+
+ if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
+ WAVEFORMATEXTENSIBLE const * ext = reinterpret_cast<WAVEFORMATEXTENSIBLE const *>(fmt);
+ mask = ext->dwChannelMask;
+ } else if (fmt->wFormatTag == WAVE_FORMAT_PCM ||
+ fmt->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
+ if (fmt->nChannels == 1) {
+ mask = CHANNEL_FRONT_CENTER;
+ } else if (fmt->nChannels == 2) {
+ mask = CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT;
+ }
+ }
+ return mask;
+}
+
+uint32_t
+get_rate(cubeb_stream * stm)
+{
+ return has_input(stm) ? stm->input_stream_params.rate
+ : stm->output_stream_params.rate;
+}
+
+uint32_t
+hns_to_frames(uint32_t rate, REFERENCE_TIME hns)
+{
+ return std::ceil((hns - 1) / 10000000.0 * rate);
+}
+
+uint32_t
+hns_to_frames(cubeb_stream * stm, REFERENCE_TIME hns)
+{
+ return hns_to_frames(get_rate(stm), hns);
+}
+
+REFERENCE_TIME
+frames_to_hns(uint32_t rate, uint32_t frames)
+{
+ return std::ceil(frames * 10000000.0 / rate);
+}
+
+/* This returns the size of a frame in the stream, before the eventual upmix
+ occurs. */
+static size_t
+frames_to_bytes_before_mix(cubeb_stream * stm, size_t frames)
+{
+ // This is called only when we has a output client.
+ XASSERT(has_output(stm));
+ return stm->output_stream_params.channels * stm->bytes_per_sample * frames;
+}
+
+/* This function handles the processing of the input and output audio,
+ * converting it to rate and channel layout specified at initialization.
+ * It then calls the data callback, via the resampler. */
+long
+refill(cubeb_stream * stm, void * input_buffer, long input_frames_count,
+ void * output_buffer, long output_frames_needed)
+{
+ XASSERT(!stm->draining);
+ /* If we need to upmix after resampling, resample into the mix buffer to
+ avoid a copy. Avoid exposing output if it is a dummy stream. */
+ void * dest = nullptr;
+ if (has_output(stm) && !stm->has_dummy_output) {
+ if (stm->output_mixer) {
+ dest = stm->mix_buffer.data();
+ } else {
+ dest = output_buffer;
+ }
+ }
+
+ long out_frames = cubeb_resampler_fill(stm->resampler.get(),
+ input_buffer,
+ &input_frames_count,
+ dest,
+ output_frames_needed);
+ /* TODO: Report out_frames < 0 as an error via the API. */
+ XASSERT(out_frames >= 0);
+
+ {
+ auto_lock lock(stm->stream_reset_lock);
+ stm->frames_written += out_frames;
+ }
+
+ /* Go in draining mode if we got fewer frames than requested. If the stream
+ has no output we still expect the callback to return number of frames read
+ from input, otherwise we stop. */
+ if ((out_frames < output_frames_needed) ||
+ (!has_output(stm) && out_frames < input_frames_count)) {
+ LOG("start draining.");
+ stm->draining = true;
+ }
+
+ /* If this is not true, there will be glitches.
+ It is alright to have produced less frames if we are draining, though. */
+ XASSERT(out_frames == output_frames_needed || stm->draining || !has_output(stm) || stm->has_dummy_output);
+
+ // We don't bother mixing dummy output as it will be silenced, otherwise mix output if needed
+ if (!stm->has_dummy_output && has_output(stm) && stm->output_mixer) {
+ XASSERT(dest == stm->mix_buffer.data());
+ size_t dest_size =
+ out_frames * stm->output_stream_params.channels * stm->bytes_per_sample;
+ XASSERT(dest_size <= stm->mix_buffer.size());
+ size_t output_buffer_size =
+ out_frames * stm->output_mix_params.channels * stm->bytes_per_sample;
+ int ret = cubeb_mixer_mix(stm->output_mixer.get(),
+ out_frames,
+ dest,
+ dest_size,
+ output_buffer,
+ output_buffer_size);
+ if (ret < 0) {
+ LOG("Error remixing content (%d)", ret);
+ }
+ }
+
+ return out_frames;
+}
+
+int wasapi_stream_reset_default_device(cubeb_stream * stm);
+
+/* This helper grabs all the frames available from a capture client, put them in
+ * linear_input_buffer. linear_input_buffer should be cleared before the
+ * callback exits. This helper does not work with exclusive mode streams. */
+bool get_input_buffer(cubeb_stream * stm)
+{
+ XASSERT(has_input(stm));
+
+ HRESULT hr;
+ BYTE * input_packet = NULL;
+ DWORD flags;
+ UINT64 dev_pos;
+ UINT64 pc_position;
+ UINT32 next;
+ /* Get input packets until we have captured enough frames, and put them in a
+ * contiguous buffer. */
+ uint32_t offset = 0;
+ // If the input stream is event driven we should only ever expect to read a
+ // single packet each time. However, if we're pulling from the stream we may
+ // need to grab multiple packets worth of frames that have accumulated (so
+ // need a loop).
+ for (hr = stm->capture_client->GetNextPacketSize(&next);
+ next > 0;
+ hr = stm->capture_client->GetNextPacketSize(&next)) {
+ if (hr == AUDCLNT_E_DEVICE_INVALIDATED) {
+ // Application can recover from this error. More info
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd316605(v=vs.85).aspx
+ LOG("Device invalidated error, reset default device");
+ wasapi_stream_reset_default_device(stm);
+ return true;
+ }
+
+ if (FAILED(hr)) {
+ LOG("cannot get next packet size: %lx", hr);
+ return false;
+ }
+
+ UINT32 frames;
+ hr = stm->capture_client->GetBuffer(&input_packet,
+ &frames,
+ &flags,
+ &dev_pos,
+ &pc_position);
+
+ if (FAILED(hr)) {
+ LOG("GetBuffer failed for capture: %lx", hr);
+ return false;
+ }
+ XASSERT(frames == next);
+
+ if (stm->context->performance_counter_frequency) {
+ LARGE_INTEGER now;
+ UINT64 now_hns;
+ // See https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudiocaptureclient-getbuffer, section "Remarks".
+ QueryPerformanceCounter(&now);
+ now_hns = 10000000 * now.QuadPart / stm->context->performance_counter_frequency;
+ if (now_hns >= pc_position) {
+ stm->input_latency_hns = now_hns - pc_position;
+ }
+ }
+
+ UINT32 input_stream_samples = frames * stm->input_stream_params.channels;
+ // We do not explicitly handle the AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY
+ // flag. There a two primary (non exhaustive) scenarios we anticipate this
+ // flag being set in:
+ // - The first GetBuffer after Start has this flag undefined. In this
+ // case the flag may be set but is meaningless and can be ignored.
+ // - If a glitch is introduced into the input. This should not happen
+ // for event based inputs, and should be mitigated by using a dummy
+ // stream to drive input in the case of input only loopback. Without
+ // a dummy output, input only loopback would glitch on silence. However,
+ // the dummy input should push silence to the loopback and prevent
+ // discontinuities. See https://blogs.msdn.microsoft.com/matthew_van_eerde/2008/12/16/sample-wasapi-loopback-capture-record-what-you-hear/
+ // As the first scenario can be ignored, and we anticipate the second
+ // scenario is mitigated, we ignore the flag.
+ // For more info: https://msdn.microsoft.com/en-us/library/windows/desktop/dd370859(v=vs.85).aspx,
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd371458(v=vs.85).aspx
+ if (flags & AUDCLNT_BUFFERFLAGS_SILENT) {
+ LOG("insert silence: ps=%u", frames);
+ stm->linear_input_buffer->push_silence(input_stream_samples);
+ } else {
+ if (stm->input_mixer) {
+ bool ok = stm->linear_input_buffer->reserve(
+ stm->linear_input_buffer->length() + input_stream_samples);
+ XASSERT(ok);
+ size_t input_packet_size =
+ frames * stm->input_mix_params.channels *
+ cubeb_sample_size(stm->input_mix_params.format);
+ size_t linear_input_buffer_size =
+ input_stream_samples *
+ cubeb_sample_size(stm->input_stream_params.format);
+ cubeb_mixer_mix(stm->input_mixer.get(),
+ frames,
+ input_packet,
+ input_packet_size,
+ stm->linear_input_buffer->end(),
+ linear_input_buffer_size);
+ stm->linear_input_buffer->set_length(
+ stm->linear_input_buffer->length() + input_stream_samples);
+ } else {
+ stm->linear_input_buffer->push(
+ input_packet, input_stream_samples);
+ }
+ }
+ hr = stm->capture_client->ReleaseBuffer(frames);
+ if (FAILED(hr)) {
+ LOG("FAILED to release intput buffer");
+ return false;
+ }
+ offset += input_stream_samples;
+ }
+
+ XASSERT(stm->linear_input_buffer->length() >= offset);
+
+ return true;
+}
+
+/* Get an output buffer from the render_client. It has to be released before
+ * exiting the callback. */
+bool get_output_buffer(cubeb_stream * stm, void *& buffer, size_t & frame_count)
+{
+ UINT32 padding_out;
+ HRESULT hr;
+
+ XASSERT(has_output(stm));
+
+ hr = stm->output_client->GetCurrentPadding(&padding_out);
+ if (hr == AUDCLNT_E_DEVICE_INVALIDATED) {
+ // Application can recover from this error. More info
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd316605(v=vs.85).aspx
+ LOG("Device invalidated error, reset default device");
+ wasapi_stream_reset_default_device(stm);
+ return true;
+ }
+
+ if (FAILED(hr)) {
+ LOG("Failed to get padding: %lx", hr);
+ return false;
+ }
+
+ XASSERT(padding_out <= stm->output_buffer_frame_count);
+
+ if (stm->draining) {
+ if (padding_out == 0) {
+ LOG("Draining finished.");
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
+ return false;
+ }
+ LOG("Draining.");
+ return true;
+ }
+
+ frame_count = stm->output_buffer_frame_count - padding_out;
+ BYTE * output_buffer;
+
+ hr = stm->render_client->GetBuffer(frame_count, &output_buffer);
+ if (FAILED(hr)) {
+ LOG("cannot get render buffer");
+ return false;
+ }
+
+ buffer = output_buffer;
+
+ return true;
+}
+
+/**
+ * This function gets input data from a input device, and pass it along with an
+ * output buffer to the resamplers. */
+bool
+refill_callback_duplex(cubeb_stream * stm)
+{
+ HRESULT hr;
+ void * output_buffer = nullptr;
+ size_t output_frames = 0;
+ size_t input_frames;
+ bool rv;
+
+ XASSERT(has_input(stm) && has_output(stm));
+
+ rv = get_input_buffer(stm);
+ if (!rv) {
+ return rv;
+ }
+
+ input_frames = stm->linear_input_buffer->length() / stm->input_stream_params.channels;
+
+ rv = get_output_buffer(stm, output_buffer, output_frames);
+ if (!rv) {
+ hr = stm->render_client->ReleaseBuffer(output_frames, 0);
+ return rv;
+ }
+
+ /* This can only happen when debugging, and having breakpoints set in the
+ * callback in a way that it makes the stream underrun. */
+ if (output_frames == 0) {
+ return true;
+ }
+
+ /* Wait for draining is not important on duplex. */
+ if (stm->draining) {
+ return false;
+ }
+
+ if (stm->has_dummy_output) {
+ ALOGV("Duplex callback (dummy output): input frames: %Iu, output frames: %Iu",
+ input_frames, output_frames);
+
+ // We don't want to expose the dummy output to the callback so don't pass
+ // the output buffer (it will be released later with silence in it)
+ refill(stm,
+ stm->linear_input_buffer->data(),
+ input_frames,
+ nullptr,
+ 0);
+ } else {
+ ALOGV("Duplex callback: input frames: %Iu, output frames: %Iu",
+ input_frames, output_frames);
+
+ refill(stm,
+ stm->linear_input_buffer->data(),
+ input_frames,
+ output_buffer,
+ output_frames);
+ }
+
+ stm->linear_input_buffer->clear();
+
+ if (stm->has_dummy_output) {
+ // If output is a dummy output, make sure it's silent
+ hr = stm->render_client->ReleaseBuffer(output_frames, AUDCLNT_BUFFERFLAGS_SILENT);
+ } else {
+ hr = stm->render_client->ReleaseBuffer(output_frames, 0);
+ }
+ if (FAILED(hr)) {
+ LOG("failed to release buffer: %lx", hr);
+ return false;
+ }
+ return true;
+}
+
+bool
+refill_callback_input(cubeb_stream * stm)
+{
+ bool rv;
+ size_t input_frames;
+
+ XASSERT(has_input(stm) && !has_output(stm));
+
+ rv = get_input_buffer(stm);
+ if (!rv) {
+ return rv;
+ }
+
+ input_frames = stm->linear_input_buffer->length() / stm->input_stream_params.channels;
+ if (!input_frames) {
+ return true;
+ }
+
+ ALOGV("Input callback: input frames: %Iu", input_frames);
+
+ long read = refill(stm,
+ stm->linear_input_buffer->data(),
+ input_frames,
+ nullptr,
+ 0);
+
+ XASSERT(read >= 0);
+
+ stm->linear_input_buffer->clear();
+
+ return !stm->draining;
+}
+
+bool
+refill_callback_output(cubeb_stream * stm)
+{
+ bool rv;
+ HRESULT hr;
+ void * output_buffer = nullptr;
+ size_t output_frames = 0;
+
+ XASSERT(!has_input(stm) && has_output(stm));
+
+ rv = get_output_buffer(stm, output_buffer, output_frames);
+ if (!rv) {
+ return rv;
+ }
+
+ if (stm->draining || output_frames == 0) {
+ return true;
+ }
+
+ long got = refill(stm,
+ nullptr,
+ 0,
+ output_buffer,
+ output_frames);
+
+ ALOGV("Output callback: output frames requested: %Iu, got %ld",
+ output_frames, got);
+
+ XASSERT(got >= 0);
+ XASSERT(size_t(got) == output_frames || stm->draining);
+
+ hr = stm->render_client->ReleaseBuffer(got, 0);
+ if (FAILED(hr)) {
+ LOG("failed to release buffer: %lx", hr);
+ return false;
+ }
+
+ return size_t(got) == output_frames || stm->draining;
+}
+
+static unsigned int __stdcall
+wasapi_stream_render_loop(LPVOID stream)
+{
+ cubeb_stream * stm = static_cast<cubeb_stream *>(stream);
+ std::atomic<bool> * emergency_bailout = stm->emergency_bailout;
+
+ // Signal wasapi_stream_start that we've copied emergency_bailout.
+ BOOL ok = SetEvent(stm->thread_ready_event);
+ if (!ok) {
+ LOG("thread_ready SetEvent failed: %lx", GetLastError());
+ return 0;
+ }
+
+ bool is_playing = true;
+ HANDLE wait_array[4] = {
+ stm->shutdown_event,
+ stm->reconfigure_event,
+ stm->refill_event,
+ stm->input_available_event
+ };
+ HANDLE mmcss_handle = NULL;
+ HRESULT hr = 0;
+ DWORD mmcss_task_index = 0;
+ struct auto_com {
+ auto_com() {
+ HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+ XASSERT(SUCCEEDED(hr));
+ }
+ ~auto_com() {
+ CoUninitialize();
+ }
+ } com;
+
+ /* We could consider using "Pro Audio" here for WebAudio and
+ maybe WebRTC. */
+ mmcss_handle = AvSetMmThreadCharacteristicsA("Audio", &mmcss_task_index);
+ if (!mmcss_handle) {
+ /* This is not fatal, but we might glitch under heavy load. */
+ LOG("Unable to use mmcss to bump the render thread priority: %lx", GetLastError());
+ }
+
+ /* WaitForMultipleObjects timeout can trigger in cases where we don't want to
+ treat it as a timeout, such as across a system sleep/wake cycle. Trigger
+ the timeout error handling only when the timeout_limit is reached, which is
+ reset on each successful loop. */
+ unsigned timeout_count = 0;
+ const unsigned timeout_limit = 3;
+ while (is_playing) {
+ // We want to check the emergency bailout variable before a
+ // and after the WaitForMultipleObject, because the handles WaitForMultipleObjects
+ // is going to wait on might have been closed already.
+ if (*emergency_bailout) {
+ delete emergency_bailout;
+ return 0;
+ }
+ DWORD waitResult = WaitForMultipleObjects(ARRAY_LENGTH(wait_array),
+ wait_array,
+ FALSE,
+ 1000);
+ if (*emergency_bailout) {
+ delete emergency_bailout;
+ return 0;
+ }
+ if (waitResult != WAIT_TIMEOUT) {
+ timeout_count = 0;
+ }
+ switch (waitResult) {
+ case WAIT_OBJECT_0: { /* shutdown */
+ is_playing = false;
+ /* We don't check if the drain is actually finished here, we just want to
+ shutdown. */
+ if (stm->draining) {
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
+ }
+ continue;
+ }
+ case WAIT_OBJECT_0 + 1: { /* reconfigure */
+ XASSERT(stm->output_client || stm->input_client);
+ LOG("Reconfiguring the stream");
+ /* Close the stream */
+ if (stm->output_client) {
+ stm->output_client->Stop();
+ LOG("Output stopped.");
+ }
+ if (stm->input_client) {
+ stm->input_client->Stop();
+ LOG("Input stopped.");
+ }
+ {
+ auto_lock lock(stm->stream_reset_lock);
+ close_wasapi_stream(stm);
+ LOG("Stream closed.");
+ /* Reopen a stream and start it immediately. This will automatically pick the
+ new default device for this role. */
+ int r = setup_wasapi_stream(stm);
+ if (r != CUBEB_OK) {
+ LOG("Error setting up the stream during reconfigure.");
+ /* Don't destroy the stream here, since we expect the caller to do
+ so after the error has propagated via the state callback. */
+ is_playing = false;
+ hr = E_FAIL;
+ continue;
+ }
+ LOG("Stream setup successfuly.");
+ }
+ XASSERT(stm->output_client || stm->input_client);
+ if (stm->output_client) {
+ hr = stm->output_client->Start();
+ if (FAILED(hr)) {
+ LOG("Error starting output after reconfigure, error: %lx", hr);
+ is_playing = false;
+ continue;
+ }
+ LOG("Output started after reconfigure.");
+ }
+ if (stm->input_client) {
+ hr = stm->input_client->Start();
+ if (FAILED(hr)) {
+ LOG("Error starting input after reconfiguring, error: %lx", hr);
+ is_playing = false;
+ continue;
+ }
+ LOG("Input started after reconfigure.");
+ }
+ break;
+ }
+ case WAIT_OBJECT_0 + 2: /* refill */
+ XASSERT((has_input(stm) && has_output(stm)) ||
+ (!has_input(stm) && has_output(stm)));
+ is_playing = stm->refill_callback(stm);
+ break;
+ case WAIT_OBJECT_0 + 3: /* input available */
+ if (has_input(stm) && has_output(stm)) { continue; }
+ is_playing = stm->refill_callback(stm);
+ break;
+ case WAIT_TIMEOUT:
+ XASSERT(stm->shutdown_event == wait_array[0]);
+ if (++timeout_count >= timeout_limit) {
+ LOG("Render loop reached the timeout limit.");
+ is_playing = false;
+ hr = E_FAIL;
+ }
+ break;
+ default:
+ LOG("case %lu not handled in render loop.", waitResult);
+ abort();
+ }
+ }
+
+ if (FAILED(hr)) {
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+ }
+
+ if (mmcss_handle) {
+ AvRevertMmThreadCharacteristics(mmcss_handle);
+ }
+
+ return 0;
+}
+
+void wasapi_destroy(cubeb * context);
+
+HRESULT register_notification_client(cubeb_stream * stm)
+{
+ assert(stm->device_enumerator);
+
+ stm->notification_client.reset(new wasapi_endpoint_notification_client(stm->reconfigure_event, stm->role));
+
+ HRESULT hr = stm->device_enumerator->RegisterEndpointNotificationCallback(stm->notification_client.get());
+ if (FAILED(hr)) {
+ LOG("Could not register endpoint notification callback: %lx", hr);
+ stm->notification_client = nullptr;
+ stm->device_enumerator = nullptr;
+ }
+
+ return hr;
+}
+
+HRESULT unregister_notification_client(cubeb_stream * stm)
+{
+ XASSERT(stm);
+ HRESULT hr;
+
+ if (!stm->device_enumerator) {
+ return S_OK;
+ }
+
+ hr = stm->device_enumerator->UnregisterEndpointNotificationCallback(stm->notification_client.get());
+ if (FAILED(hr)) {
+ // We can't really do anything here, we'll probably leak the
+ // notification client, but we can at least release the enumerator.
+ stm->device_enumerator = nullptr;
+ return S_OK;
+ }
+
+ stm->notification_client = nullptr;
+
+ return S_OK;
+}
+
+HRESULT get_endpoint(com_ptr<IMMDevice> & device, LPCWSTR devid)
+{
+ com_ptr<IMMDeviceEnumerator> enumerator;
+ HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
+ NULL, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(enumerator.receive()));
+ if (FAILED(hr)) {
+ LOG("Could not get device enumerator: %lx", hr);
+ return hr;
+ }
+
+ hr = enumerator->GetDevice(devid, device.receive());
+ if (FAILED(hr)) {
+ LOG("Could not get device: %lx", hr);
+ return hr;
+ }
+
+ return S_OK;
+}
+
+HRESULT register_collection_notification_client(cubeb * context)
+{
+ HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
+ NULL, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(context->device_collection_enumerator.receive()));
+ if (FAILED(hr)) {
+ LOG("Could not get device enumerator: %lx", hr);
+ return hr;
+ }
+
+ context->collection_notification_client.reset(new wasapi_collection_notification_client(context));
+
+ hr = context->device_collection_enumerator->RegisterEndpointNotificationCallback(
+ context->collection_notification_client.get());
+ if (FAILED(hr)) {
+ LOG("Could not register endpoint notification callback: %lx", hr);
+ context->collection_notification_client.reset();
+ context->device_collection_enumerator.reset();
+ }
+
+ return hr;
+}
+
+HRESULT unregister_collection_notification_client(cubeb * context)
+{
+ HRESULT hr = context->device_collection_enumerator->
+ UnregisterEndpointNotificationCallback(context->collection_notification_client.get());
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ context->collection_notification_client = nullptr;
+ context->device_collection_enumerator = nullptr;
+
+ return hr;
+}
+
+HRESULT get_default_endpoint(com_ptr<IMMDevice> & device, EDataFlow direction, ERole role)
+{
+ com_ptr<IMMDeviceEnumerator> enumerator;
+ HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
+ NULL, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(enumerator.receive()));
+ if (FAILED(hr)) {
+ LOG("Could not get device enumerator: %lx", hr);
+ return hr;
+ }
+ hr = enumerator->GetDefaultAudioEndpoint(direction, role, device.receive());
+ if (FAILED(hr)) {
+ LOG("Could not get default audio endpoint: %lx", hr);
+ return hr;
+ }
+
+ return ERROR_SUCCESS;
+}
+
+double
+current_stream_delay(cubeb_stream * stm)
+{
+ stm->stream_reset_lock.assert_current_thread_owns();
+
+ /* If the default audio endpoint went away during playback and we weren't
+ able to configure a new one, it's possible the caller may call this
+ before the error callback has propogated back. */
+ if (!stm->audio_clock) {
+ return 0;
+ }
+
+ UINT64 freq;
+ HRESULT hr = stm->audio_clock->GetFrequency(&freq);
+ if (FAILED(hr)) {
+ LOG("GetFrequency failed: %lx", hr);
+ return 0;
+ }
+
+ UINT64 pos;
+ hr = stm->audio_clock->GetPosition(&pos, NULL);
+ if (FAILED(hr)) {
+ LOG("GetPosition failed: %lx", hr);
+ return 0;
+ }
+
+ double cur_pos = static_cast<double>(pos) / freq;
+ double max_pos = static_cast<double>(stm->frames_written) / stm->output_mix_params.rate;
+ double delay = std::max(max_pos - cur_pos, 0.0);
+
+ return delay;
+}
+
+int
+stream_set_volume(cubeb_stream * stm, float volume)
+{
+ stm->stream_reset_lock.assert_current_thread_owns();
+
+ if (!stm->audio_stream_volume) {
+ return CUBEB_ERROR;
+ }
+
+ uint32_t channels;
+ HRESULT hr = stm->audio_stream_volume->GetChannelCount(&channels);
+ if (FAILED(hr)) {
+ LOG("could not get the channel count: %lx", hr);
+ return CUBEB_ERROR;
+ }
+
+ /* up to 9.1 for now */
+ if (channels > 10) {
+ return CUBEB_ERROR_NOT_SUPPORTED;
+ }
+
+ float volumes[10];
+ for (uint32_t i = 0; i < channels; i++) {
+ volumes[i] = volume;
+ }
+
+ hr = stm->audio_stream_volume->SetAllVolumes(channels, volumes);
+ if (FAILED(hr)) {
+ LOG("could not set the channels volume: %lx", hr);
+ return CUBEB_ERROR;
+ }
+
+ return CUBEB_OK;
+}
+} // namespace anonymous
+
+extern "C" {
+int wasapi_init(cubeb ** context, char const * context_name)
+{
+ /* We don't use the device yet, but need to make sure we can initialize one
+ so that this backend is not incorrectly enabled on platforms that don't
+ support WASAPI. */
+ com_ptr<IMMDevice> device;
+ HRESULT hr = get_default_endpoint(device, eRender, eConsole);
+ if (FAILED(hr)) {
+ XASSERT(hr != CO_E_NOTINITIALIZED);
+ LOG("It wasn't able to find a default rendering device: %lx", hr);
+ hr = get_default_endpoint(device, eCapture, eConsole);
+ if (FAILED(hr)) {
+ LOG("It wasn't able to find a default capture device: %lx", hr);
+ return CUBEB_ERROR;
+ }
+ }
+
+ cubeb * ctx = new cubeb();
+
+ ctx->ops = &wasapi_ops;
+ if (cubeb_strings_init(&ctx->device_ids) != CUBEB_OK) {
+ delete ctx;
+ return CUBEB_ERROR;
+ }
+
+ LARGE_INTEGER frequency;
+ if (QueryPerformanceFrequency(&frequency)) {
+ ctx->performance_counter_frequency = frequency.QuadPart;
+ } else {
+ LOG("Failed getting performance counter frequency, latency reporting will be inacurate");
+ ctx->performance_counter_frequency = 0;
+ }
+
+ *context = ctx;
+
+ return CUBEB_OK;
+}
+}
+
+namespace {
+bool stop_and_join_render_thread(cubeb_stream * stm)
+{
+ bool rv = true;
+ LOG("Stop and join render thread.");
+ if (!stm->thread) {
+ LOG("No thread present.");
+ return true;
+ }
+
+ // If we've already leaked the thread, just return,
+ // there is not much we can do.
+ if (!stm->emergency_bailout.load()) {
+ return false;
+ }
+
+ BOOL ok = SetEvent(stm->shutdown_event);
+ if (!ok) {
+ LOG("Destroy SetEvent failed: %lx", GetLastError());
+ }
+
+ /* Wait five seconds for the rendering thread to return. It's supposed to
+ * check its event loop very often, five seconds is rather conservative. */
+ DWORD r = WaitForSingleObject(stm->thread, 5000);
+ if (r != WAIT_OBJECT_0) {
+ /* Something weird happened, leak the thread and continue the shutdown
+ * process. */
+ *(stm->emergency_bailout) = true;
+ // We give the ownership to the rendering thread.
+ stm->emergency_bailout = nullptr;
+ LOG("Destroy WaitForSingleObject on thread failed: %lx, %lx", r, GetLastError());
+ rv = false;
+ }
+
+ // Only attempts to close and null out the thread and event if the
+ // WaitForSingleObject above succeeded, so that calling this function again
+ // attemps to clean up the thread and event each time.
+ if (rv) {
+ LOG("Closing thread.");
+ CloseHandle(stm->thread);
+ stm->thread = NULL;
+
+ CloseHandle(stm->shutdown_event);
+ stm->shutdown_event = 0;
+ }
+
+ return rv;
+}
+
+void wasapi_destroy(cubeb * context)
+{
+ if (context->device_ids) {
+ cubeb_strings_destroy(context->device_ids);
+ }
+
+ delete context;
+}
+
+char const * wasapi_get_backend_id(cubeb * context)
+{
+ return "wasapi";
+}
+
+int
+wasapi_get_max_channel_count(cubeb * ctx, uint32_t * max_channels)
+{
+ XASSERT(ctx && max_channels);
+
+ com_ptr<IMMDevice> device;
+ HRESULT hr = get_default_endpoint(device, eRender, eConsole);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+
+ com_ptr<IAudioClient> client;
+ hr = device->Activate(__uuidof(IAudioClient),
+ CLSCTX_INPROC_SERVER,
+ NULL, client.receive_vpp());
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+
+ WAVEFORMATEX * tmp = nullptr;
+ hr = client->GetMixFormat(&tmp);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+ com_heap_ptr<WAVEFORMATEX> mix_format(tmp);
+
+ *max_channels = mix_format->nChannels;
+
+ return CUBEB_OK;
+}
+
+int
+wasapi_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency_frames)
+{
+ if (params.format != CUBEB_SAMPLE_FLOAT32NE && params.format != CUBEB_SAMPLE_S16NE) {
+ return CUBEB_ERROR_INVALID_FORMAT;
+ }
+
+ ERole role = pref_to_role(params.prefs);
+
+ com_ptr<IMMDevice> device;
+ HRESULT hr = get_default_endpoint(device, eRender, role);
+ if (FAILED(hr)) {
+ LOG("Could not get default endpoint: %lx", hr);
+ return CUBEB_ERROR;
+ }
+
+ com_ptr<IAudioClient> client;
+ hr = device->Activate(__uuidof(IAudioClient),
+ CLSCTX_INPROC_SERVER,
+ NULL, client.receive_vpp());
+ if (FAILED(hr)) {
+ LOG("Could not activate device for latency: %lx", hr);
+ return CUBEB_ERROR;
+ }
+
+ REFERENCE_TIME minimum_period;
+ REFERENCE_TIME default_period;
+ hr = client->GetDevicePeriod(&default_period, &minimum_period);
+ if (FAILED(hr)) {
+ LOG("Could not get device period: %lx", hr);
+ return CUBEB_ERROR;
+ }
+
+ LOG("default device period: %I64d, minimum device period: %I64d", default_period, minimum_period);
+
+ /* If we're on Windows 10, we can use IAudioClient3 to get minimal latency.
+ Otherwise, according to the docs, the best latency we can achieve is by
+ synchronizing the stream and the engine.
+ http://msdn.microsoft.com/en-us/library/windows/desktop/dd370871%28v=vs.85%29.aspx */
+
+ #ifdef _WIN32_WINNT_WIN10
+ *latency_frames = hns_to_frames(params.rate, minimum_period);
+ #else
+ *latency_frames = hns_to_frames(params.rate, default_period);
+ #endif
+
+ LOG("Minimum latency in frames: %u", *latency_frames);
+
+ return CUBEB_OK;
+}
+
+int
+wasapi_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate)
+{
+ com_ptr<IMMDevice> device;
+ HRESULT hr = get_default_endpoint(device, eRender, eConsole);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+
+ com_ptr<IAudioClient> client;
+ hr = device->Activate(__uuidof(IAudioClient),
+ CLSCTX_INPROC_SERVER,
+ NULL, client.receive_vpp());
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+
+ WAVEFORMATEX * tmp = nullptr;
+ hr = client->GetMixFormat(&tmp);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+ com_heap_ptr<WAVEFORMATEX> mix_format(tmp);
+
+ *rate = mix_format->nSamplesPerSec;
+
+ LOG("Preferred sample rate for output: %u", *rate);
+
+ return CUBEB_OK;
+}
+
+void wasapi_stream_destroy(cubeb_stream * stm);
+
+static void
+waveformatex_update_derived_properties(WAVEFORMATEX * format)
+{
+ format->nBlockAlign = format->wBitsPerSample * format->nChannels / 8;
+ format->nAvgBytesPerSec = format->nSamplesPerSec * format->nBlockAlign;
+ if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
+ WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(format);
+ format_pcm->Samples.wValidBitsPerSample = format->wBitsPerSample;
+ }
+}
+
+/* Based on the mix format and the stream format, try to find a way to play
+ what the user requested. */
+static void
+handle_channel_layout(cubeb_stream * stm, EDataFlow direction, com_heap_ptr<WAVEFORMATEX> & mix_format, const cubeb_stream_params * stream_params)
+{
+ com_ptr<IAudioClient> & audio_client = (direction == eRender) ? stm->output_client : stm->input_client;
+ XASSERT(audio_client);
+ /* The docs say that GetMixFormat is always of type WAVEFORMATEXTENSIBLE [1],
+ so the reinterpret_cast below should be safe. In practice, this is not
+ true, and we just want to bail out and let the rest of the code find a good
+ conversion path instead of trying to make WASAPI do it by itself.
+ [1]: http://msdn.microsoft.com/en-us/library/windows/desktop/dd370811%28v=vs.85%29.aspx*/
+ if (mix_format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) {
+ return;
+ }
+
+ WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.get());
+
+ /* Stash a copy of the original mix format in case we need to restore it later. */
+ WAVEFORMATEXTENSIBLE hw_mix_format = *format_pcm;
+
+ /* Get the channel mask by the channel layout.
+ If the layout is not supported, we will get a closest settings below. */
+ format_pcm->dwChannelMask = stream_params->layout;
+ mix_format->nChannels = stream_params->channels;
+ waveformatex_update_derived_properties(mix_format.get());
+
+ /* Check if wasapi will accept our channel layout request. */
+ WAVEFORMATEX * tmp = nullptr;
+ HRESULT hr = audio_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED,
+ mix_format.get(),
+ &tmp);
+ com_heap_ptr<WAVEFORMATEX> closest(tmp);
+ if (hr == S_FALSE) {
+ /* Channel layout not supported, but WASAPI gives us a suggestion. Use it,
+ and handle the eventual upmix/downmix ourselves. Ignore the subformat of
+ the suggestion, since it seems to always be IEEE_FLOAT. */
+ LOG("Using WASAPI suggested format: channels: %d", closest->nChannels);
+ XASSERT(closest->wFormatTag == WAVE_FORMAT_EXTENSIBLE);
+ WAVEFORMATEXTENSIBLE * closest_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(closest.get());
+ format_pcm->dwChannelMask = closest_pcm->dwChannelMask;
+ mix_format->nChannels = closest->nChannels;
+ waveformatex_update_derived_properties(mix_format.get());
+ } else if (hr == AUDCLNT_E_UNSUPPORTED_FORMAT) {
+ /* Not supported, no suggestion. This should not happen, but it does in the
+ field with some sound cards. We restore the mix format, and let the rest
+ of the code figure out the right conversion path. */
+ XASSERT(mix_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE);
+ *reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.get()) = hw_mix_format;
+ } else if (hr == S_OK) {
+ LOG("Requested format accepted by WASAPI.");
+ } else {
+ LOG("IsFormatSupported unhandled error: %lx", hr);
+ }
+}
+
+static bool
+initialize_iaudioclient2(com_ptr<IAudioClient> & audio_client)
+{
+ com_ptr<IAudioClient2> audio_client2;
+ audio_client->QueryInterface<IAudioClient2>(audio_client2.receive());
+ if (!audio_client2) {
+ LOG("Could not get IAudioClient2 interface, not setting AUDCLNT_STREAMOPTIONS_RAW.");
+ return CUBEB_OK;
+ }
+ AudioClientProperties properties = { 0 };
+ properties.cbSize = sizeof(AudioClientProperties);
+#ifndef __MINGW32__
+ properties.Options |= AUDCLNT_STREAMOPTIONS_RAW;
+#endif
+ HRESULT hr = audio_client2->SetClientProperties(&properties);
+ if (FAILED(hr)) {
+ LOG("IAudioClient2::SetClientProperties error: %lx", GetLastError());
+ return CUBEB_ERROR;
+ }
+ return CUBEB_OK;
+}
+
+// Not static to suppress a warning.
+/* static */ bool
+initialize_iaudioclient3(com_ptr<IAudioClient> & audio_client,
+ cubeb_stream * stm,
+ const com_heap_ptr<WAVEFORMATEX> & mix_format,
+ DWORD flags,
+ EDataFlow direction)
+{
+ com_ptr<IAudioClient3> audio_client3;
+ audio_client->QueryInterface<IAudioClient3>(audio_client3.receive());
+ if (!audio_client3) {
+ LOG("Could not get IAudioClient3 interface");
+ return false;
+ }
+
+ if (flags & AUDCLNT_STREAMFLAGS_LOOPBACK) {
+ // IAudioClient3 doesn't work with loopback streams, and will return error
+ // 88890021: AUDCLNT_E_INVALID_STREAM_FLAG
+ LOG("Audio stream is loopback, not using IAudioClient3");
+ return false;
+ }
+
+ // IAudioClient3 doesn't support AUDCLNT_STREAMFLAGS_NOPERSIST, and will return
+ // AUDCLNT_E_INVALID_STREAM_FLAG. This is undocumented.
+ flags = flags & ~AUDCLNT_STREAMFLAGS_NOPERSIST;
+
+ // Some people have reported glitches with capture streams:
+ // http://blog.nirbheek.in/2018/03/low-latency-audio-on-windows-with.html
+ if (direction == eCapture) {
+ LOG("Audio stream is capture, not using IAudioClient3");
+ return false;
+ }
+
+ // Possibly initialize a shared-mode stream using IAudioClient3. Initializing
+ // a stream this way lets you request lower latencies, but also locks the global
+ // WASAPI engine at that latency.
+ // - If we request a shared-mode stream, streams created with IAudioClient will
+ // have their latency adjusted to match. When the shared-mode stream is
+ // closed, they'll go back to normal.
+ // - If there's already a shared-mode stream running, then we cannot request
+ // the engine change to a different latency - we have to match it.
+ // - It's antisocial to lock the WASAPI engine at its default latency. If we
+ // would do this, then stop and use IAudioClient instead.
+
+ HRESULT hr;
+ uint32_t default_period = 0, fundamental_period = 0, min_period = 0, max_period = 0;
+ hr = audio_client3->GetSharedModeEnginePeriod(mix_format.get(), &default_period, &fundamental_period, &min_period, &max_period);
+ if (FAILED(hr)) {
+ LOG("Could not get shared mode engine period: error: %lx", hr);
+ return false;
+ }
+ uint32_t requested_latency = stm->latency;
+ if (requested_latency >= default_period) {
+ LOG("Requested latency %i greater than default latency %i, not using IAudioClient3", requested_latency, default_period);
+ return false;
+ }
+ LOG("Got shared mode engine period: default=%i fundamental=%i min=%i max=%i", default_period, fundamental_period, min_period, max_period);
+ // Snap requested latency to a valid value
+ uint32_t old_requested_latency = requested_latency;
+ if (requested_latency < min_period) {
+ requested_latency = min_period;
+ }
+ requested_latency -= (requested_latency - min_period) % fundamental_period;
+ if (requested_latency != old_requested_latency) {
+ LOG("Requested latency %i was adjusted to %i", old_requested_latency, requested_latency);
+ }
+
+ hr = audio_client3->InitializeSharedAudioStream(flags, requested_latency, mix_format.get(), NULL);
+ if (SUCCEEDED(hr)) {
+ return true;
+ }
+ else if (hr == AUDCLNT_E_ENGINE_PERIODICITY_LOCKED) {
+ LOG("Got AUDCLNT_E_ENGINE_PERIODICITY_LOCKED, adjusting latency request");
+ } else {
+ LOG("Could not initialize shared stream with IAudioClient3: error: %lx", hr);
+ return false;
+ }
+
+ uint32_t current_period = 0;
+ WAVEFORMATEX* current_format = nullptr;
+ // We have to pass a valid WAVEFORMATEX** and not nullptr, otherwise
+ // GetCurrentSharedModeEnginePeriod will return E_POINTER
+ hr = audio_client3->GetCurrentSharedModeEnginePeriod(&current_format, &current_period);
+ CoTaskMemFree(current_format);
+ if (FAILED(hr)) {
+ LOG("Could not get current shared mode engine period: error: %lx", hr);
+ return false;
+ }
+
+ if (current_period >= default_period) {
+ LOG("Current shared mode engine period %i too high, not using IAudioClient", current_period);
+ return false;
+ }
+
+ hr = audio_client3->InitializeSharedAudioStream(flags, current_period, mix_format.get(), NULL);
+ if (SUCCEEDED(hr)) {
+ LOG("Current shared mode engine period is %i instead of requested %i", current_period, requested_latency);
+ return true;
+ }
+
+ LOG("Could not initialize shared stream with IAudioClient3: error: %lx", hr);
+ return false;
+}
+
+#define DIRECTION_NAME (direction == eCapture ? "capture" : "render")
+
+template<typename T>
+int setup_wasapi_stream_one_side(cubeb_stream * stm,
+ cubeb_stream_params * stream_params,
+ wchar_t const * devid,
+ EDataFlow direction,
+ REFIID riid,
+ com_ptr<IAudioClient> & audio_client,
+ uint32_t * buffer_frame_count,
+ HANDLE & event,
+ T & render_or_capture_client,
+ cubeb_stream_params * mix_params,
+ com_ptr<IMMDevice>& device)
+{
+ HRESULT hr;
+ bool is_loopback = stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK;
+ if (is_loopback && direction != eCapture) {
+ LOG("Loopback pref can only be used with capture streams!\n");
+ return CUBEB_ERROR;
+ }
+
+ stm->stream_reset_lock.assert_current_thread_owns();
+ bool try_again = false;
+ // This loops until we find a device that works, or we've exhausted all
+ // possibilities.
+ do {
+ if (devid) {
+ hr = get_endpoint(device, devid);
+ if (FAILED(hr)) {
+ LOG("Could not get %s endpoint, error: %lx\n", DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+ } else {
+ // If caller has requested loopback but not specified a device, look for
+ // the default render device. Otherwise look for the default device
+ // appropriate to the direction.
+ hr = get_default_endpoint(device, is_loopback ? eRender : direction, pref_to_role(stream_params->prefs));
+ if (FAILED(hr)) {
+ if (is_loopback) {
+ LOG("Could not get default render endpoint for loopback, error: %lx\n", hr);
+ } else {
+ LOG("Could not get default %s endpoint, error: %lx\n", DIRECTION_NAME, hr);
+ }
+ return CUBEB_ERROR;
+ }
+ }
+
+ /* Get a client. We will get all other interfaces we need from
+ * this pointer. */
+#if 0 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1590902
+ hr = device->Activate(__uuidof(IAudioClient3),
+ CLSCTX_INPROC_SERVER,
+ NULL, audio_client.receive_vpp());
+ if (hr == E_NOINTERFACE) {
+#endif
+ hr = device->Activate(__uuidof(IAudioClient),
+ CLSCTX_INPROC_SERVER,
+ NULL, audio_client.receive_vpp());
+#if 0
+ }
+#endif
+
+ if (FAILED(hr)) {
+ LOG("Could not activate the device to get an audio"
+ " client for %s: error: %lx\n", DIRECTION_NAME, hr);
+ // A particular device can't be activated because it has been
+ // unplugged, try fall back to the default audio device.
+ if (devid && hr == AUDCLNT_E_DEVICE_INVALIDATED) {
+ LOG("Trying again with the default %s audio device.", DIRECTION_NAME);
+ devid = nullptr;
+ device = nullptr;
+ try_again = true;
+ } else {
+ return CUBEB_ERROR;
+ }
+ } else {
+ try_again = false;
+ }
+ } while (try_again);
+
+ /* We have to distinguish between the format the mixer uses,
+ * and the format the stream we want to play uses. */
+ WAVEFORMATEX * tmp = nullptr;
+ hr = audio_client->GetMixFormat(&tmp);
+ if (FAILED(hr)) {
+ LOG("Could not fetch current mix format from the audio"
+ " client for %s: error: %lx", DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+ com_heap_ptr<WAVEFORMATEX> mix_format(tmp);
+
+ mix_format->wBitsPerSample = stm->bytes_per_sample * 8;
+ if (mix_format->wFormatTag == WAVE_FORMAT_PCM ||
+ mix_format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
+ switch (mix_format->wBitsPerSample) {
+ case 8:
+ case 16:
+ mix_format->wFormatTag = WAVE_FORMAT_PCM;
+ break;
+ case 32:
+ mix_format->wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
+ break;
+ default:
+ LOG("%u bits per sample is incompatible with PCM wave formats",
+ mix_format->wBitsPerSample);
+ return CUBEB_ERROR;
+ }
+ }
+
+ if (mix_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
+ WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.get());
+ format_pcm->SubFormat = stm->waveformatextensible_sub_format;
+ }
+ waveformatex_update_derived_properties(mix_format.get());
+
+ /* Set channel layout only when there're more than two channels. Otherwise,
+ * use the default setting retrieved from the stream format of the audio
+ * engine's internal processing by GetMixFormat. */
+ if (mix_format->nChannels > 2) {
+ handle_channel_layout(stm, direction, mix_format, stream_params);
+ }
+
+ mix_params->format = stream_params->format;
+ mix_params->rate = mix_format->nSamplesPerSec;
+ mix_params->channels = mix_format->nChannels;
+ mix_params->layout = mask_to_channel_layout(mix_format.get());
+
+ LOG("Setup requested=[f=%d r=%u c=%u l=%u] mix=[f=%d r=%u c=%u l=%u]",
+ stream_params->format, stream_params->rate, stream_params->channels,
+ stream_params->layout,
+ mix_params->format, mix_params->rate, mix_params->channels,
+ mix_params->layout);
+
+
+ DWORD flags = 0;
+
+ bool is_persist = stream_params->prefs & CUBEB_STREAM_PREF_PERSIST;
+ if (!is_persist) {
+ flags |= AUDCLNT_STREAMFLAGS_NOPERSIST;
+ }
+
+ // Check if a loopback device should be requested. Note that event callbacks
+ // do not work with loopback devices, so only request these if not looping.
+ if (is_loopback) {
+ flags |= AUDCLNT_STREAMFLAGS_LOOPBACK;
+ } else {
+ flags |= AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
+ }
+
+ // Sanity check the latency, it may be that the device doesn't support it.
+ REFERENCE_TIME minimum_period;
+ REFERENCE_TIME default_period;
+ hr = audio_client->GetDevicePeriod(&default_period, &minimum_period);
+ if (FAILED(hr)) {
+ LOG("Could not get device period: %lx", hr);
+ return CUBEB_ERROR;
+ }
+
+ REFERENCE_TIME latency_hns;
+
+ uint32_t latency_frames = stm->latency;
+ cubeb_device_info device_info;
+ int rv = wasapi_create_device(stm->context, device_info, stm->device_enumerator.get(), device.get());
+ if (rv == CUBEB_OK) {
+ const char* HANDSFREE_TAG = "BTHHFENUM";
+ size_t len = sizeof(HANDSFREE_TAG);
+ if (direction == eCapture &&
+ strlen(device_info.group_id) >= len &&
+ strncmp(device_info.group_id, HANDSFREE_TAG, len) == 0) {
+ // Rather high-latency to prevent constant under-runs in this particular
+ // case of an input device using bluetooth handsfree.
+ uint32_t default_period_frames = hns_to_frames(device_info.default_rate, default_period);
+ latency_frames = default_period_frames * 4;
+ stm->input_bluetooth_handsfree = true;
+ LOG("Input is a bluetooth device in handsfree, latency increased to %u frames from a default of %u", latency_frames, default_period_frames);
+ } else {
+ uint32_t minimum_period_frames = hns_to_frames(device_info.default_rate, minimum_period);
+ latency_frames = std::max(latency_frames, minimum_period_frames);
+ stm->input_bluetooth_handsfree = false;
+ LOG("Input is a not bluetooth handsfree, latency %s to %u frames (minimum %u)", latency_frames < minimum_period_frames ? "increased" : "set", latency_frames, minimum_period_frames);
+ }
+
+ latency_hns = frames_to_hns(device_info.default_rate, latency_frames);
+
+ wasapi_destroy_device(&device_info);
+ } else {
+ stm->input_bluetooth_handsfree = false;
+ latency_hns = frames_to_hns(mix_params->rate, latency_frames);
+ LOG("Could not get cubeb_device_info.");
+ }
+
+ if (stream_params->prefs & CUBEB_STREAM_PREF_RAW) {
+ if (initialize_iaudioclient2(audio_client) != CUBEB_OK) {
+ LOG("Can't initialize an IAudioClient2, error: %lx", GetLastError());
+ // This is not fatal.
+ }
+ }
+
+#if 0 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1590902
+ if (initialize_iaudioclient3(audio_client, stm, mix_format, flags, direction)) {
+ LOG("Initialized with IAudioClient3");
+ } else {
+#endif
+ hr = audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED,
+ flags,
+ latency_hns,
+ 0,
+ mix_format.get(),
+ NULL);
+#if 0
+ }
+#endif
+ if (FAILED(hr)) {
+ LOG("Unable to initialize audio client for %s: %lx.", DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+
+ hr = audio_client->GetBufferSize(buffer_frame_count);
+ if (FAILED(hr)) {
+ LOG("Could not get the buffer size from the client"
+ " for %s %lx.", DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+
+ // Events are used if not looping back
+ if (!is_loopback) {
+ hr = audio_client->SetEventHandle(event);
+ if (FAILED(hr)) {
+ LOG("Could set the event handle for the %s client %lx.",
+ DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+ }
+
+ hr = audio_client->GetService(riid, render_or_capture_client.receive_vpp());
+ if (FAILED(hr)) {
+ LOG("Could not get the %s client %lx.", DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+
+ return CUBEB_OK;
+}
+
+#undef DIRECTION_NAME
+
+void wasapi_find_matching_output_device(cubeb_stream * stm) {
+ HRESULT hr;
+ cubeb_device_info * input_device = nullptr;
+ cubeb_device_collection collection;
+
+ // Only try to match to an output device if the input device is a bluetooth
+ // device that is using the handsfree protocol
+ if (!stm->input_bluetooth_handsfree) {
+ return;
+ }
+
+ wchar_t * tmp = nullptr;
+ hr = stm->input_device->GetId(&tmp);
+ if (FAILED(hr)) {
+ LOG("Couldn't get input device id in wasapi_find_matching_output_device");
+ return;
+ }
+ com_heap_ptr<wchar_t> device_id(tmp);
+ cubeb_devid input_device_id = intern_device_id(stm->context, device_id.get());
+ if (!input_device_id) {
+ return;
+ }
+
+ int rv = wasapi_enumerate_devices(stm->context, (cubeb_device_type)(CUBEB_DEVICE_TYPE_INPUT|CUBEB_DEVICE_TYPE_OUTPUT), &collection);
+ if (rv != CUBEB_OK) {
+ return;
+ }
+
+ // Find the input device, and then find the output device with the same group
+ // id and the same rate.
+ for (uint32_t i = 0; i < collection.count; i++) {
+ cubeb_device_info dev = collection.device[i];
+ if (dev.devid == input_device_id) {
+ input_device = &dev;
+ break;
+ }
+ }
+
+ for (uint32_t i = 0; i < collection.count; i++) {
+ cubeb_device_info dev = collection.device[i];
+ if (dev.type == CUBEB_DEVICE_TYPE_OUTPUT && dev.group_id && input_device &&
+ !strcmp(dev.group_id, input_device->group_id) &&
+ dev.default_rate == input_device->default_rate) {
+ LOG("Found matching device for %s: %s", input_device->friendly_name,
+ dev.friendly_name);
+ stm->output_device_id =
+ utf8_to_wstr(reinterpret_cast<char const *>(dev.devid));
+ }
+ }
+
+ wasapi_device_collection_destroy(stm->context, &collection);
+}
+
+int setup_wasapi_stream(cubeb_stream * stm)
+{
+ int rv;
+
+ stm->stream_reset_lock.assert_current_thread_owns();
+
+ XASSERT((!stm->output_client || !stm->input_client) && "WASAPI stream already setup, close it first.");
+
+ if (has_input(stm)) {
+ LOG("(%p) Setup capture: device=%p", stm, stm->input_device_id.get());
+ rv = setup_wasapi_stream_one_side(stm,
+ &stm->input_stream_params,
+ stm->input_device_id.get(),
+ eCapture,
+ __uuidof(IAudioCaptureClient),
+ stm->input_client,
+ &stm->input_buffer_frame_count,
+ stm->input_available_event,
+ stm->capture_client,
+ &stm->input_mix_params,
+ stm->input_device);
+ if (rv != CUBEB_OK) {
+ LOG("Failure to open the input side.");
+ return rv;
+ }
+
+ // We initializing an input stream, buffer ahead two buffers worth of silence.
+ // This delays the input side slightly, but allow to not glitch when no input
+ // is available when calling into the resampler to call the callback: the input
+ // refill event will be set shortly after to compensate for this lack of data.
+ // In debug, four buffers are used, to avoid tripping up assertions down the line.
+#if !defined(DEBUG)
+ const int silent_buffer_count = 2;
+#else
+ const int silent_buffer_count = 6;
+#endif
+ stm->linear_input_buffer->push_silence(stm->input_buffer_frame_count *
+ stm->input_stream_params.channels *
+ silent_buffer_count);
+
+ // If this is a bluetooth device, and the output device is the default
+ // device, and the default device is the same bluetooth device, pick the
+ // right output device, running at the same rate and with the same protocol
+ // as the input.
+ if (!stm->output_device_id) {
+ wasapi_find_matching_output_device(stm);
+ }
+ }
+
+ // If we don't have an output device but are requesting a loopback device,
+ // we attempt to open that same device in output mode in order to drive the
+ // loopback via the output events.
+ stm->has_dummy_output = false;
+ if (!has_output(stm) && stm->input_stream_params.prefs & CUBEB_STREAM_PREF_LOOPBACK) {
+ stm->output_stream_params.rate = stm->input_stream_params.rate;
+ stm->output_stream_params.channels = stm->input_stream_params.channels;
+ stm->output_stream_params.layout = stm->input_stream_params.layout;
+ if (stm->input_device_id) {
+ size_t len = wcslen(stm->input_device_id.get());
+ std::unique_ptr<wchar_t[]> tmp(new wchar_t[len + 1]);
+ if (wcsncpy_s(tmp.get(), len + 1, stm->input_device_id.get(), len) != 0) {
+ LOG("Failed to copy device identifier while copying input stream"
+ " configuration to output stream configuration to drive loopback.");
+ return CUBEB_ERROR;
+ }
+ stm->output_device_id = move(tmp);
+ }
+ stm->has_dummy_output = true;
+ }
+
+ if (has_output(stm)) {
+ LOG("(%p) Setup render: device=%p", stm, stm->output_device_id.get());
+ rv = setup_wasapi_stream_one_side(stm,
+ &stm->output_stream_params,
+ stm->output_device_id.get(),
+ eRender,
+ __uuidof(IAudioRenderClient),
+ stm->output_client,
+ &stm->output_buffer_frame_count,
+ stm->refill_event,
+ stm->render_client,
+ &stm->output_mix_params,
+ stm->output_device);
+ if (rv != CUBEB_OK) {
+ LOG("Failure to open the output side.");
+ return rv;
+ }
+
+ HRESULT hr = stm->output_client->GetService(__uuidof(IAudioStreamVolume),
+ stm->audio_stream_volume.receive_vpp());
+ if (FAILED(hr)) {
+ LOG("Could not get the IAudioStreamVolume: %lx", hr);
+ return CUBEB_ERROR;
+ }
+
+ XASSERT(stm->frames_written == 0);
+ hr = stm->output_client->GetService(__uuidof(IAudioClock),
+ stm->audio_clock.receive_vpp());
+ if (FAILED(hr)) {
+ LOG("Could not get the IAudioClock: %lx", hr);
+ return CUBEB_ERROR;
+ }
+
+ /* Restore the stream volume over a device change. */
+ if (stream_set_volume(stm, stm->volume) != CUBEB_OK) {
+ LOG("Could not set the volume.");
+ return CUBEB_ERROR;
+ }
+ }
+
+ /* If we have both input and output, we resample to
+ * the highest sample rate available. */
+ int32_t target_sample_rate;
+ if (has_input(stm) && has_output(stm)) {
+ XASSERT(stm->input_stream_params.rate == stm->output_stream_params.rate);
+ target_sample_rate = stm->input_stream_params.rate;
+ } else if (has_input(stm)) {
+ target_sample_rate = stm->input_stream_params.rate;
+ } else {
+ XASSERT(has_output(stm));
+ target_sample_rate = stm->output_stream_params.rate;
+ }
+
+ LOG("Target sample rate: %d", target_sample_rate);
+
+ /* If we are playing/capturing a mono stream, we only resample one channel,
+ and copy it over, so we are always resampling the number
+ of channels of the stream, not the number of channels
+ that WASAPI wants. */
+ cubeb_stream_params input_params = stm->input_mix_params;
+ input_params.channels = stm->input_stream_params.channels;
+ cubeb_stream_params output_params = stm->output_mix_params;
+ output_params.channels = stm->output_stream_params.channels;
+
+ stm->resampler.reset(
+ cubeb_resampler_create(stm,
+ has_input(stm) ? &input_params : nullptr,
+ has_output(stm) ? &output_params : nullptr,
+ target_sample_rate,
+ stm->data_callback,
+ stm->user_ptr,
+ stm->voice ? CUBEB_RESAMPLER_QUALITY_VOIP : CUBEB_RESAMPLER_QUALITY_DESKTOP));
+ if (!stm->resampler) {
+ LOG("Could not get a resampler");
+ return CUBEB_ERROR;
+ }
+
+ XASSERT(has_input(stm) || has_output(stm));
+
+ if (has_input(stm) && has_output(stm)) {
+ stm->refill_callback = refill_callback_duplex;
+ } else if (has_input(stm)) {
+ stm->refill_callback = refill_callback_input;
+ } else if (has_output(stm)) {
+ stm->refill_callback = refill_callback_output;
+ }
+
+ // Create input mixer.
+ if (has_input(stm) &&
+ ((stm->input_mix_params.layout != CUBEB_LAYOUT_UNDEFINED &&
+ stm->input_mix_params.layout != stm->input_stream_params.layout) ||
+ (stm->input_mix_params.channels != stm->input_stream_params.channels))) {
+ if (stm->input_mix_params.layout == CUBEB_LAYOUT_UNDEFINED) {
+ LOG("Input stream using undefined layout! Any mixing may be "
+ "unpredictable!\n");
+ }
+ stm->input_mixer.reset(cubeb_mixer_create(stm->input_stream_params.format,
+ stm->input_mix_params.channels,
+ stm->input_mix_params.layout,
+ stm->input_stream_params.channels,
+ stm->input_stream_params.layout));
+ assert(stm->input_mixer);
+ }
+
+ // Create output mixer.
+ if (has_output(stm) && stm->output_mix_params.layout != stm->output_stream_params.layout) {
+ if (stm->output_mix_params.layout == CUBEB_LAYOUT_UNDEFINED) {
+ LOG("Output stream using undefined layout! Any mixing may be unpredictable!\n");
+ }
+ stm->output_mixer.reset(cubeb_mixer_create(stm->output_stream_params.format,
+ stm->output_stream_params.channels,
+ stm->output_stream_params.layout,
+ stm->output_mix_params.channels,
+ stm->output_mix_params.layout));
+ assert(stm->output_mixer);
+ // Input is up/down mixed when depacketized in get_input_buffer.
+ stm->mix_buffer.resize(
+ frames_to_bytes_before_mix(stm, stm->output_buffer_frame_count));
+ }
+
+ return CUBEB_OK;
+}
+
+ERole
+pref_to_role(cubeb_stream_prefs prefs)
+{
+ if (prefs & CUBEB_STREAM_PREF_VOICE) {
+ return eCommunications;
+ }
+
+ return eConsole;
+}
+
+int
+wasapi_stream_init(cubeb * context, cubeb_stream ** stream,
+ char const * stream_name,
+ cubeb_devid input_device,
+ cubeb_stream_params * input_stream_params,
+ cubeb_devid output_device,
+ cubeb_stream_params * output_stream_params,
+ unsigned int latency_frames, cubeb_data_callback data_callback,
+ cubeb_state_callback state_callback, void * user_ptr)
+{
+ int rv;
+
+ XASSERT(context && stream && (input_stream_params || output_stream_params));
+
+ if (output_stream_params && input_stream_params &&
+ output_stream_params->format != input_stream_params->format) {
+ return CUBEB_ERROR_INVALID_FORMAT;
+ }
+
+ std::unique_ptr<cubeb_stream, decltype(&wasapi_stream_destroy)> stm(new cubeb_stream(), wasapi_stream_destroy);
+
+ stm->context = context;
+ stm->data_callback = data_callback;
+ stm->state_callback = state_callback;
+ stm->user_ptr = user_ptr;
+ stm->role = eConsole;
+ stm->input_bluetooth_handsfree = false;
+
+ HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
+ NULL, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(stm->device_enumerator.receive()));
+ if (FAILED(hr)) {
+ LOG("Could not get device enumerator: %lx", hr);
+ return hr;
+ }
+
+ if (input_stream_params) {
+ stm->input_stream_params = *input_stream_params;
+ stm->input_device_id = utf8_to_wstr(reinterpret_cast<char const *>(input_device));
+ }
+ if (output_stream_params) {
+ stm->output_stream_params = *output_stream_params;
+ stm->output_device_id = utf8_to_wstr(reinterpret_cast<char const *>(output_device));
+ }
+
+ if (stm->output_stream_params.prefs & CUBEB_STREAM_PREF_VOICE ||
+ stm->input_stream_params.prefs & CUBEB_STREAM_PREF_VOICE) {
+ stm->voice = true;
+ } else {
+ stm->voice = false;
+ }
+
+ switch (output_stream_params ? output_stream_params->format : input_stream_params->format) {
+ case CUBEB_SAMPLE_S16NE:
+ stm->bytes_per_sample = sizeof(short);
+ stm->waveformatextensible_sub_format = KSDATAFORMAT_SUBTYPE_PCM;
+ stm->linear_input_buffer.reset(new auto_array_wrapper_impl<short>);
+ break;
+ case CUBEB_SAMPLE_FLOAT32NE:
+ stm->bytes_per_sample = sizeof(float);
+ stm->waveformatextensible_sub_format = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+ stm->linear_input_buffer.reset(new auto_array_wrapper_impl<float>);
+ break;
+ default:
+ return CUBEB_ERROR_INVALID_FORMAT;
+ }
+
+ stm->latency = latency_frames;
+
+ stm->reconfigure_event = CreateEvent(NULL, 0, 0, NULL);
+ if (!stm->reconfigure_event) {
+ LOG("Can't create the reconfigure event, error: %lx", GetLastError());
+ return CUBEB_ERROR;
+ }
+
+ /* Unconditionally create the two events so that the wait logic is simpler. */
+ stm->refill_event = CreateEvent(NULL, 0, 0, NULL);
+ if (!stm->refill_event) {
+ LOG("Can't create the refill event, error: %lx", GetLastError());
+ return CUBEB_ERROR;
+ }
+
+ stm->input_available_event = CreateEvent(NULL, 0, 0, NULL);
+ if (!stm->input_available_event) {
+ LOG("Can't create the input available event , error: %lx", GetLastError());
+ return CUBEB_ERROR;
+ }
+
+ {
+ /* Locking here is not strictly necessary, because we don't have a
+ notification client that can reset the stream yet, but it lets us
+ assert that the lock is held in the function. */
+ auto_lock lock(stm->stream_reset_lock);
+ rv = setup_wasapi_stream(stm.get());
+ }
+ if (rv != CUBEB_OK) {
+ return rv;
+ }
+
+ if (!((input_stream_params ?
+ (input_stream_params->prefs & CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) : 0) ||
+ (output_stream_params ?
+ (output_stream_params->prefs & CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) : 0))) {
+ HRESULT hr = register_notification_client(stm.get());
+ if (FAILED(hr)) {
+ /* this is not fatal, we can still play audio, but we won't be able
+ to keep using the default audio endpoint if it changes. */
+ LOG("failed to register notification client, %lx", hr);
+ }
+ }
+
+ *stream = stm.release();
+
+ LOG("Stream init succesfull (%p)", *stream);
+ return CUBEB_OK;
+}
+
+void close_wasapi_stream(cubeb_stream * stm)
+{
+ XASSERT(stm);
+
+ stm->stream_reset_lock.assert_current_thread_owns();
+
+ stm->output_client = nullptr;
+ stm->render_client = nullptr;
+
+ stm->input_client = nullptr;
+ stm->capture_client = nullptr;
+
+ stm->output_device = nullptr;
+ stm->input_device = nullptr;
+
+ stm->audio_stream_volume = nullptr;
+
+ stm->audio_clock = nullptr;
+ stm->total_frames_written += static_cast<UINT64>(round(stm->frames_written * stream_to_mix_samplerate_ratio(stm->output_stream_params, stm->output_mix_params)));
+ stm->frames_written = 0;
+
+ stm->resampler.reset();
+ stm->output_mixer.reset();
+ stm->input_mixer.reset();
+ stm->mix_buffer.clear();
+}
+
+void wasapi_stream_destroy(cubeb_stream * stm)
+{
+ XASSERT(stm);
+ LOG("Stream destroy (%p)", stm);
+
+ // Only free stm->emergency_bailout if we could join the thread.
+ // If we could not join the thread, stm->emergency_bailout is true
+ // and is still alive until the thread wakes up and exits cleanly.
+ if (stop_and_join_render_thread(stm)) {
+ delete stm->emergency_bailout.load();
+ stm->emergency_bailout = nullptr;
+ }
+
+ if (stm->notification_client) {
+ unregister_notification_client(stm);
+ }
+
+ CloseHandle(stm->reconfigure_event);
+ CloseHandle(stm->refill_event);
+ CloseHandle(stm->input_available_event);
+
+ // The variables intialized in wasapi_stream_init,
+ // must be destroyed in wasapi_stream_destroy.
+ stm->linear_input_buffer.reset();
+
+ stm->device_enumerator = nullptr;
+
+ {
+ auto_lock lock(stm->stream_reset_lock);
+ close_wasapi_stream(stm);
+ }
+
+ delete stm;
+}
+
+enum StreamDirection {
+ OUTPUT,
+ INPUT
+};
+
+int stream_start_one_side(cubeb_stream * stm, StreamDirection dir)
+{
+ XASSERT((dir == OUTPUT && stm->output_client) ||
+ (dir == INPUT && stm->input_client));
+
+ HRESULT hr = dir == OUTPUT ? stm->output_client->Start() : stm->input_client->Start();
+ if (hr == AUDCLNT_E_DEVICE_INVALIDATED) {
+ LOG("audioclient invalidated for %s device, reconfiguring",
+ dir == OUTPUT ? "output" : "input");
+
+ BOOL ok = ResetEvent(stm->reconfigure_event);
+ if (!ok) {
+ LOG("resetting reconfig event failed for %s stream: %lx",
+ dir == OUTPUT ? "output" : "input", GetLastError());
+ }
+
+ close_wasapi_stream(stm);
+ int r = setup_wasapi_stream(stm);
+ if (r != CUBEB_OK) {
+ LOG("reconfigure failed");
+ return r;
+ }
+
+ HRESULT hr2 = dir == OUTPUT ? stm->output_client->Start() : stm->input_client->Start();
+ if (FAILED(hr2)) {
+ LOG("could not start the %s stream after reconfig: %lx",
+ dir == OUTPUT ? "output" : "input", hr);
+ return CUBEB_ERROR;
+ }
+ } else if (FAILED(hr)) {
+ LOG("could not start the %s stream: %lx.",
+ dir == OUTPUT ? "output" : "input", hr);
+ return CUBEB_ERROR;
+ }
+
+ return CUBEB_OK;
+}
+
+int wasapi_stream_start(cubeb_stream * stm)
+{
+ auto_lock lock(stm->stream_reset_lock);
+
+ XASSERT(stm && !stm->thread && !stm->shutdown_event);
+ XASSERT(stm->output_client || stm->input_client);
+
+ stm->emergency_bailout = new std::atomic<bool>(false);
+
+ if (stm->output_client) {
+ int rv = stream_start_one_side(stm, OUTPUT);
+ if (rv != CUBEB_OK) {
+ return rv;
+ }
+ }
+
+ if (stm->input_client) {
+ int rv = stream_start_one_side(stm, INPUT);
+ if (rv != CUBEB_OK) {
+ return rv;
+ }
+ }
+
+ stm->shutdown_event = CreateEvent(NULL, 0, 0, NULL);
+ if (!stm->shutdown_event) {
+ LOG("Can't create the shutdown event, error: %lx", GetLastError());
+ return CUBEB_ERROR;
+ }
+
+ stm->thread_ready_event = CreateEvent(NULL, 0, 0, NULL);
+ if (!stm->thread_ready_event) {
+ LOG("Can't create the thread_ready event, error: %lx", GetLastError());
+ return CUBEB_ERROR;
+ }
+
+ cubeb_async_log_reset_threads();
+ stm->thread = (HANDLE) _beginthreadex(NULL, 512 * 1024, wasapi_stream_render_loop, stm, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
+ if (stm->thread == NULL) {
+ LOG("could not create WASAPI render thread.");
+ return CUBEB_ERROR;
+ }
+
+ // Wait for wasapi_stream_render_loop to signal that emergency_bailout has
+ // been read, avoiding a bailout situation where we could free `stm`
+ // before wasapi_stream_render_loop had a chance to run.
+ HRESULT hr = WaitForSingleObject(stm->thread_ready_event, INFINITE);
+ XASSERT(hr == WAIT_OBJECT_0);
+ CloseHandle(stm->thread_ready_event);
+ stm->thread_ready_event = 0;
+
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED);
+
+ return CUBEB_OK;
+}
+
+int wasapi_stream_stop(cubeb_stream * stm)
+{
+ XASSERT(stm);
+ HRESULT hr;
+
+ {
+ auto_lock lock(stm->stream_reset_lock);
+
+ if (stm->output_client) {
+ hr = stm->output_client->Stop();
+ if (FAILED(hr)) {
+ LOG("could not stop AudioClient (output)");
+ return CUBEB_ERROR;
+ }
+ }
+
+ if (stm->input_client) {
+ hr = stm->input_client->Stop();
+ if (FAILED(hr)) {
+ LOG("could not stop AudioClient (input)");
+ return CUBEB_ERROR;
+ }
+ }
+
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED);
+ }
+
+ if (stop_and_join_render_thread(stm)) {
+ delete stm->emergency_bailout.load();
+ stm->emergency_bailout = nullptr;
+ } else {
+ // If we could not join the thread, put the stream in error.
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+ return CUBEB_ERROR;
+ }
+
+ return CUBEB_OK;
+}
+
+int wasapi_stream_reset_default_device(cubeb_stream * stm)
+{
+ XASSERT(stm && stm->reconfigure_event);
+ BOOL ok = SetEvent(stm->reconfigure_event);
+ if (!ok) {
+ LOG("SetEvent on reconfigure_event failed: %lx", GetLastError());
+ return CUBEB_ERROR;
+ }
+ return CUBEB_OK;
+}
+
+int wasapi_stream_get_position(cubeb_stream * stm, uint64_t * position)
+{
+ XASSERT(stm && position);
+ auto_lock lock(stm->stream_reset_lock);
+
+ if (!has_output(stm)) {
+ return CUBEB_ERROR;
+ }
+
+ /* Calculate how far behind the current stream head the playback cursor is. */
+ uint64_t stream_delay = static_cast<uint64_t>(current_stream_delay(stm) * stm->output_stream_params.rate);
+
+ /* Calculate the logical stream head in frames at the stream sample rate. */
+ uint64_t max_pos = stm->total_frames_written +
+ static_cast<uint64_t>(round(stm->frames_written * stream_to_mix_samplerate_ratio(stm->output_stream_params, stm->output_mix_params)));
+
+ *position = max_pos;
+ if (stream_delay <= *position) {
+ *position -= stream_delay;
+ }
+
+ if (*position < stm->prev_position) {
+ *position = stm->prev_position;
+ }
+ stm->prev_position = *position;
+
+ return CUBEB_OK;
+}
+
+int wasapi_stream_get_latency(cubeb_stream * stm, uint32_t * latency)
+{
+ XASSERT(stm && latency);
+
+ if (!has_output(stm)) {
+ return CUBEB_ERROR;
+ }
+
+ auto_lock lock(stm->stream_reset_lock);
+
+ /* The GetStreamLatency method only works if the
+ AudioClient has been initialized. */
+ if (!stm->output_client) {
+ LOG("get_latency: No output_client.");
+ return CUBEB_ERROR;
+ }
+
+ REFERENCE_TIME latency_hns;
+ HRESULT hr = stm->output_client->GetStreamLatency(&latency_hns);
+ if (FAILED(hr)) {
+ LOG("GetStreamLatency failed %lx.", hr);
+ return CUBEB_ERROR;
+ }
+ // This happens on windows 10: no error, but always 0 for latency.
+ if (latency_hns == 0) {
+ LOG("GetStreamLatency returned 0, using workaround.");
+ double delay_s = current_stream_delay(stm);
+ // convert to sample-frames
+ *latency = delay_s * stm->output_stream_params.rate;
+ } else {
+ *latency = hns_to_frames(stm, latency_hns);
+ }
+
+ LOG("Output latency %u frames.", *latency);
+
+ return CUBEB_OK;
+}
+
+
+int wasapi_stream_get_input_latency(cubeb_stream * stm, uint32_t * latency)
+{
+ XASSERT(stm && latency);
+
+ if (!has_input(stm)) {
+ LOG("Input latency queried on an output-only stream.");
+ return CUBEB_ERROR;
+ }
+
+ auto_lock lock(stm->stream_reset_lock);
+
+ if (stm->input_latency_hns == LATENCY_NOT_AVAILABLE_YET) {
+ LOG("Input latency not available yet.");
+ return CUBEB_ERROR;
+ }
+
+ *latency = hns_to_frames(stm, stm->input_latency_hns);
+
+ return CUBEB_OK;
+}
+
+int wasapi_stream_set_volume(cubeb_stream * stm, float volume)
+{
+ auto_lock lock(stm->stream_reset_lock);
+
+ if (!has_output(stm)) {
+ return CUBEB_ERROR;
+ }
+
+ if (stream_set_volume(stm, volume) != CUBEB_OK) {
+ return CUBEB_ERROR;
+ }
+
+ stm->volume = volume;
+
+ return CUBEB_OK;
+}
+
+static char const *
+wstr_to_utf8(LPCWSTR str)
+{
+ int size = ::WideCharToMultiByte(CP_UTF8, 0, str, -1, nullptr, 0, NULL, NULL);
+ if (size <= 0) {
+ return nullptr;
+ }
+
+ char * ret = static_cast<char *>(malloc(size));
+ ::WideCharToMultiByte(CP_UTF8, 0, str, -1, ret, size, NULL, NULL);
+ return ret;
+}
+
+static std::unique_ptr<wchar_t const []>
+utf8_to_wstr(char const * str)
+{
+ int size = ::MultiByteToWideChar(CP_UTF8, 0, str, -1, nullptr, 0);
+ if (size <= 0) {
+ return nullptr;
+ }
+
+ std::unique_ptr<wchar_t []> ret(new wchar_t[size]);
+ ::MultiByteToWideChar(CP_UTF8, 0, str, -1, ret.get(), size);
+ return ret;
+}
+
+static com_ptr<IMMDevice>
+wasapi_get_device_node(IMMDeviceEnumerator * enumerator, IMMDevice * dev)
+{
+ com_ptr<IMMDevice> ret;
+ com_ptr<IDeviceTopology> devtopo;
+ com_ptr<IConnector> connector;
+
+ if (SUCCEEDED(dev->Activate(__uuidof(IDeviceTopology), CLSCTX_ALL, NULL, devtopo.receive_vpp())) &&
+ SUCCEEDED(devtopo->GetConnector(0, connector.receive()))) {
+ wchar_t * tmp = nullptr;
+ if (SUCCEEDED(connector->GetDeviceIdConnectedTo(&tmp))) {
+ com_heap_ptr<wchar_t> filterid(tmp);
+ if (FAILED(enumerator->GetDevice(filterid.get(), ret.receive())))
+ ret = NULL;
+ }
+ }
+
+ return ret;
+}
+
+static BOOL
+wasapi_is_default_device(EDataFlow flow, ERole role, LPCWSTR device_id,
+ IMMDeviceEnumerator * enumerator)
+{
+ BOOL ret = FALSE;
+ com_ptr<IMMDevice> dev;
+ HRESULT hr;
+
+ hr = enumerator->GetDefaultAudioEndpoint(flow, role, dev.receive());
+ if (SUCCEEDED(hr)) {
+ wchar_t * tmp = nullptr;
+ if (SUCCEEDED(dev->GetId(&tmp))) {
+ com_heap_ptr<wchar_t> defdevid(tmp);
+ ret = (wcscmp(defdevid.get(), device_id) == 0);
+ }
+ }
+
+ return ret;
+}
+
+/* `ret` must be deallocated with `wasapi_destroy_device`, iff the return value
+ * of this function is `CUBEB_OK`. */
+int
+wasapi_create_device(cubeb * ctx, cubeb_device_info& ret, IMMDeviceEnumerator * enumerator, IMMDevice * dev)
+{
+ com_ptr<IMMEndpoint> endpoint;
+ com_ptr<IMMDevice> devnode;
+ com_ptr<IAudioClient> client;
+ EDataFlow flow;
+ DWORD state = DEVICE_STATE_NOTPRESENT;
+ com_ptr<IPropertyStore> propstore;
+ REFERENCE_TIME def_period, min_period;
+ HRESULT hr;
+
+ // zero-out to be able to safely delete the pointers to friendly_name and
+ // group_id at all time in this function.
+ PodZero(&ret, 1);
+
+ struct prop_variant : public PROPVARIANT {
+ prop_variant() { PropVariantInit(this); }
+ ~prop_variant() { PropVariantClear(this); }
+ prop_variant(prop_variant const &) = delete;
+ prop_variant & operator=(prop_variant const &) = delete;
+ };
+
+ hr = dev->QueryInterface(IID_PPV_ARGS(endpoint.receive()));
+ if (FAILED(hr)) {
+ wasapi_destroy_device(&ret);
+ return CUBEB_ERROR;
+ }
+
+ hr = endpoint->GetDataFlow(&flow);
+ if (FAILED(hr)) {
+ wasapi_destroy_device(&ret);
+ return CUBEB_ERROR;
+ }
+
+ wchar_t * tmp = nullptr;
+ hr = dev->GetId(&tmp);
+ if (FAILED(hr)) {
+ wasapi_destroy_device(&ret);
+ return CUBEB_ERROR;
+ }
+ com_heap_ptr<wchar_t> device_id(tmp);
+
+ char const * device_id_intern = intern_device_id(ctx, device_id.get());
+ if (!device_id_intern) {
+ wasapi_destroy_device(&ret);
+ return CUBEB_ERROR;
+ }
+
+ hr = dev->OpenPropertyStore(STGM_READ, propstore.receive());
+ if (FAILED(hr)) {
+ wasapi_destroy_device(&ret);
+ return CUBEB_ERROR;
+ }
+
+ hr = dev->GetState(&state);
+ if (FAILED(hr)) {
+ wasapi_destroy_device(&ret);
+ return CUBEB_ERROR;
+ }
+
+ ret.device_id = device_id_intern;
+ ret.devid = reinterpret_cast<cubeb_devid>(ret.device_id);
+ prop_variant namevar;
+ hr = propstore->GetValue(PKEY_Device_FriendlyName, &namevar);
+ if (SUCCEEDED(hr) && namevar.vt == VT_LPWSTR) {
+ ret.friendly_name = wstr_to_utf8(namevar.pwszVal);
+ }
+ if (!ret.friendly_name) {
+ // This is not fatal, but a valid string is expected in all cases.
+ char* empty = new char[1];
+ empty[0] = '\0';
+ ret.friendly_name = empty;
+ }
+
+ devnode = wasapi_get_device_node(enumerator, dev);
+ if (devnode) {
+ com_ptr<IPropertyStore> ps;
+ hr = devnode->OpenPropertyStore(STGM_READ, ps.receive());
+ if (FAILED(hr)) {
+ wasapi_destroy_device(&ret);
+ return CUBEB_ERROR;
+ }
+
+ prop_variant instancevar;
+ hr = ps->GetValue(PKEY_Device_InstanceId, &instancevar);
+ if (SUCCEEDED(hr) && instancevar.vt == VT_LPWSTR) {
+ ret.group_id = wstr_to_utf8(instancevar.pwszVal);
+ }
+ }
+
+ if (!ret.group_id) {
+ // This is not fatal, but a valid string is expected in all cases.
+ char* empty = new char[1];
+ empty[0] = '\0';
+ ret.group_id = empty;
+ }
+
+ ret.preferred = CUBEB_DEVICE_PREF_NONE;
+ if (wasapi_is_default_device(flow, eConsole, device_id.get(), enumerator)) {
+ ret.preferred = (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_MULTIMEDIA);
+ }
+ if (wasapi_is_default_device(flow, eCommunications, device_id.get(), enumerator)) {
+ ret.preferred = (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_VOICE);
+ }
+ if (wasapi_is_default_device(flow, eConsole, device_id.get(), enumerator)) {
+ ret.preferred = (cubeb_device_pref)(ret.preferred | CUBEB_DEVICE_PREF_NOTIFICATION);
+ }
+
+ if (flow == eRender) {
+ ret.type = CUBEB_DEVICE_TYPE_OUTPUT;
+ } else if (flow == eCapture) {
+ ret.type = CUBEB_DEVICE_TYPE_INPUT;
+ }
+
+ switch (state) {
+ case DEVICE_STATE_ACTIVE:
+ ret.state = CUBEB_DEVICE_STATE_ENABLED;
+ break;
+ case DEVICE_STATE_UNPLUGGED:
+ ret.state = CUBEB_DEVICE_STATE_UNPLUGGED;
+ break;
+ default:
+ ret.state = CUBEB_DEVICE_STATE_DISABLED;
+ break;
+ };
+
+ ret.format = static_cast<cubeb_device_fmt>(CUBEB_DEVICE_FMT_F32NE | CUBEB_DEVICE_FMT_S16NE);
+ ret.default_format = CUBEB_DEVICE_FMT_F32NE;
+ prop_variant fmtvar;
+ hr = propstore->GetValue(PKEY_AudioEngine_DeviceFormat, &fmtvar);
+ if (SUCCEEDED(hr) && fmtvar.vt == VT_BLOB) {
+ if (fmtvar.blob.cbSize == sizeof(PCMWAVEFORMAT)) {
+ const PCMWAVEFORMAT * pcm = reinterpret_cast<const PCMWAVEFORMAT *>(fmtvar.blob.pBlobData);
+
+ ret.max_rate = ret.min_rate = ret.default_rate = pcm->wf.nSamplesPerSec;
+ ret.max_channels = pcm->wf.nChannels;
+ } else if (fmtvar.blob.cbSize >= sizeof(WAVEFORMATEX)) {
+ WAVEFORMATEX* wfx = reinterpret_cast<WAVEFORMATEX*>(fmtvar.blob.pBlobData);
+
+ if (fmtvar.blob.cbSize >= sizeof(WAVEFORMATEX) + wfx->cbSize ||
+ wfx->wFormatTag == WAVE_FORMAT_PCM) {
+ ret.max_rate = ret.min_rate = ret.default_rate = wfx->nSamplesPerSec;
+ ret.max_channels = wfx->nChannels;
+ }
+ }
+ }
+
+ if (SUCCEEDED(dev->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL, client.receive_vpp())) &&
+ SUCCEEDED(client->GetDevicePeriod(&def_period, &min_period))) {
+ ret.latency_lo = hns_to_frames(ret.default_rate, min_period);
+ ret.latency_hi = hns_to_frames(ret.default_rate, def_period);
+ } else {
+ ret.latency_lo = 0;
+ ret.latency_hi = 0;
+ }
+
+ XASSERT(ret.friendly_name && ret.group_id);
+
+ return CUBEB_OK;
+}
+
+void
+wasapi_destroy_device(cubeb_device_info * device)
+{
+ delete [] device->friendly_name;
+ delete [] device->group_id;
+}
+
+static int
+wasapi_enumerate_devices(cubeb * context, cubeb_device_type type,
+ cubeb_device_collection * out)
+{
+ com_ptr<IMMDeviceEnumerator> enumerator;
+ com_ptr<IMMDeviceCollection> collection;
+ HRESULT hr;
+ UINT cc, i;
+ EDataFlow flow;
+
+ hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL,
+ CLSCTX_INPROC_SERVER, IID_PPV_ARGS(enumerator.receive()));
+ if (FAILED(hr)) {
+ LOG("Could not get device enumerator: %lx", hr);
+ return CUBEB_ERROR;
+ }
+
+ if (type == CUBEB_DEVICE_TYPE_OUTPUT) flow = eRender;
+ else if (type == CUBEB_DEVICE_TYPE_INPUT) flow = eCapture;
+ else if (type & (CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT)) flow = eAll;
+ else return CUBEB_ERROR;
+
+ hr = enumerator->EnumAudioEndpoints(flow, DEVICE_STATEMASK_ALL, collection.receive());
+ if (FAILED(hr)) {
+ LOG("Could not enumerate audio endpoints: %lx", hr);
+ return CUBEB_ERROR;
+ }
+
+ hr = collection->GetCount(&cc);
+ if (FAILED(hr)) {
+ LOG("IMMDeviceCollection::GetCount() failed: %lx", hr);
+ return CUBEB_ERROR;
+ }
+ cubeb_device_info * devices = new cubeb_device_info[cc];
+ if (!devices)
+ return CUBEB_ERROR;
+
+ PodZero(devices, cc);
+ out->count = 0;
+ for (i = 0; i < cc; i++) {
+ com_ptr<IMMDevice> dev;
+ hr = collection->Item(i, dev.receive());
+ if (FAILED(hr)) {
+ LOG("IMMDeviceCollection::Item(%u) failed: %lx", i-1, hr);
+ continue;
+ }
+ if (wasapi_create_device(context, devices[out->count],
+ enumerator.get(), dev.get()) == CUBEB_OK) {
+ out->count += 1;
+ }
+ }
+
+ out->device = devices;
+ return CUBEB_OK;
+}
+
+static int
+wasapi_device_collection_destroy(cubeb * /*ctx*/, cubeb_device_collection * collection)
+{
+ XASSERT(collection);
+
+ for (size_t n = 0; n < collection->count; n++) {
+ cubeb_device_info& dev = collection->device[n];
+ wasapi_destroy_device(&dev);
+ }
+
+ delete [] collection->device;
+ return CUBEB_OK;
+}
+
+static int
+wasapi_register_device_collection_changed(cubeb * context,
+ cubeb_device_type devtype,
+ cubeb_device_collection_changed_callback collection_changed_callback,
+ void * user_ptr)
+{
+ if (devtype == CUBEB_DEVICE_TYPE_UNKNOWN) {
+ return CUBEB_ERROR_INVALID_PARAMETER;
+ }
+
+ if (collection_changed_callback) {
+ // Make sure it has been unregistered first.
+ XASSERT(((devtype & CUBEB_DEVICE_TYPE_INPUT) &&
+ !context->input_collection_changed_callback) ||
+ ((devtype & CUBEB_DEVICE_TYPE_OUTPUT) &&
+ !context->output_collection_changed_callback));
+
+ // Stop the notification client. Notifications arrive on
+ // a separate thread. We stop them here to avoid
+ // synchronization issues during the update.
+ if (context->device_collection_enumerator.get()) {
+ HRESULT hr = unregister_collection_notification_client(context);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+ }
+
+ if (devtype & CUBEB_DEVICE_TYPE_INPUT) {
+ context->input_collection_changed_callback = collection_changed_callback;
+ context->input_collection_changed_user_ptr = user_ptr;
+ }
+ if (devtype & CUBEB_DEVICE_TYPE_OUTPUT) {
+ context->output_collection_changed_callback = collection_changed_callback;
+ context->output_collection_changed_user_ptr = user_ptr;
+ }
+
+ HRESULT hr = register_collection_notification_client(context);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+ } else {
+ if (!context->device_collection_enumerator.get()) {
+ // Already unregistered, ignore it.
+ return CUBEB_OK;
+ }
+
+ HRESULT hr = unregister_collection_notification_client(context);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+ if (devtype & CUBEB_DEVICE_TYPE_INPUT) {
+ context->input_collection_changed_callback = nullptr;
+ context->input_collection_changed_user_ptr = nullptr;
+ }
+ if (devtype & CUBEB_DEVICE_TYPE_OUTPUT) {
+ context->output_collection_changed_callback = nullptr;
+ context->output_collection_changed_user_ptr = nullptr;
+ }
+
+ // If after the updates we still have registered
+ // callbacks restart the notification client.
+ if (context->input_collection_changed_callback ||
+ context->output_collection_changed_callback) {
+ hr = register_collection_notification_client(context);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+ }
+ }
+
+ return CUBEB_OK;
+}
+
+cubeb_ops const wasapi_ops = {
+ /*.init =*/ wasapi_init,
+ /*.get_backend_id =*/ wasapi_get_backend_id,
+ /*.get_max_channel_count =*/ wasapi_get_max_channel_count,
+ /*.get_min_latency =*/ wasapi_get_min_latency,
+ /*.get_preferred_sample_rate =*/ wasapi_get_preferred_sample_rate,
+ /*.enumerate_devices =*/ wasapi_enumerate_devices,
+ /*.device_collection_destroy =*/ wasapi_device_collection_destroy,
+ /*.destroy =*/ wasapi_destroy,
+ /*.stream_init =*/ wasapi_stream_init,
+ /*.stream_destroy =*/ wasapi_stream_destroy,
+ /*.stream_start =*/ wasapi_stream_start,
+ /*.stream_stop =*/ wasapi_stream_stop,
+ /*.stream_reset_default_device =*/ wasapi_stream_reset_default_device,
+ /*.stream_get_position =*/ wasapi_stream_get_position,
+ /*.stream_get_latency =*/ wasapi_stream_get_latency,
+ /*.stream_get_input_latency =*/ wasapi_stream_get_input_latency,
+ /*.stream_set_volume =*/ wasapi_stream_set_volume,
+ /*.stream_set_name =*/ NULL,
+ /*.stream_get_current_device =*/ NULL,
+ /*.stream_device_destroy =*/ NULL,
+ /*.stream_register_device_changed_callback =*/ NULL,
+ /*.register_device_collection_changed =*/ wasapi_register_device_collection_changed,
+};
+} // namespace anonymous