summaryrefslogtreecommitdiffstats
path: root/media/libcubeb/src/cubeb_wasapi.cpp
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /media/libcubeb/src/cubeb_wasapi.cpp
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'media/libcubeb/src/cubeb_wasapi.cpp')
-rw-r--r--media/libcubeb/src/cubeb_wasapi.cpp2311
1 files changed, 2311 insertions, 0 deletions
diff --git a/media/libcubeb/src/cubeb_wasapi.cpp b/media/libcubeb/src/cubeb_wasapi.cpp
new file mode 100644
index 000000000..e88d6becd
--- /dev/null
+++ b/media/libcubeb/src/cubeb_wasapi.cpp
@@ -0,0 +1,2311 @@
+/*
+ * Copyright © 2013 Mozilla Foundation
+ *
+ * This program is made available under an ISC-style license. See the
+ * accompanying file LICENSE for details.
+ */
+#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 "cubeb/cubeb.h"
+#include "cubeb-internal.h"
+#include "cubeb_resampler.h"
+#include "cubeb_utils.h"
+
+/* devicetopology.h missing in MinGW. */
+#ifndef __devicetopology_h__
+#include "cubeb_devicetopology.h"
+#endif
+
+/* Taken from winbase.h, Not in MinGW. */
+#ifndef STACK_SIZE_PARAM_IS_A_RESERVATION
+#define STACK_SIZE_PARAM_IS_A_RESERVATION 0x00010000 // Threads only
+#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 {
+template<typename T, size_t N>
+constexpr size_t
+ARRAY_LENGTH(T(&)[N])
+{
+ return N;
+}
+
+void
+SafeRelease(HANDLE handle)
+{
+ if (handle) {
+ CloseHandle(handle);
+ }
+}
+
+template <typename T>
+void SafeRelease(T * ptr)
+{
+ if (ptr) {
+ ptr->Release();
+ }
+}
+
+struct auto_com {
+ auto_com() {
+ result = CoInitializeEx(NULL, COINIT_MULTITHREADED);
+ }
+ ~auto_com() {
+ if (result == RPC_E_CHANGED_MODE) {
+ // This is not an error, COM was not initialized by this function, so it is
+ // not necessary to uninit it.
+ LOG("COM was already initialized in STA.");
+ } else if (result == S_FALSE) {
+ // This is not an error. We are allowed to call CoInitializeEx more than
+ // once, as long as it is matches by an CoUninitialize call.
+ // We do that in the dtor which is guaranteed to be called.
+ LOG("COM was already initialized in MTA");
+ }
+ if (SUCCEEDED(result)) {
+ CoUninitialize();
+ }
+ }
+ bool ok() {
+ return result == RPC_E_CHANGED_MODE || SUCCEEDED(result);
+ }
+private:
+ HRESULT result;
+};
+
+typedef HANDLE (WINAPI *set_mm_thread_characteristics_function)(
+ const char * TaskName, LPDWORD TaskIndex);
+typedef BOOL (WINAPI *revert_mm_thread_characteristics_function)(HANDLE handle);
+
+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);
+static char * wstr_to_utf8(const wchar_t * str);
+static std::unique_ptr<const wchar_t[]> utf8_to_wstr(char* str);
+
+}
+
+struct cubeb
+{
+ cubeb_ops const * ops;
+ /* Library dynamically opened to increase the render thread priority, and
+ the two function pointers we need. */
+ HMODULE mmcss_module;
+ set_mm_thread_characteristics_function set_mm_thread_characteristics;
+ revert_mm_thread_characteristics_function revert_mm_thread_characteristics;
+};
+
+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
+{
+ cubeb * context;
+ /* 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_stream_params output_mix_params;
+ /* Stream parameters. This is what the client requested,
+ * and what will be presented in the callback. */
+ cubeb_stream_params input_stream_params;
+ cubeb_stream_params output_stream_params;
+ /* The input and output device, or NULL for default. */
+ cubeb_devid input_device;
+ cubeb_devid output_device;
+ /* The latency initially requested for this stream, in frames. */
+ unsigned latency;
+ cubeb_state_callback state_callback;
+ cubeb_data_callback data_callback;
+ wasapi_refill_callback refill_callback;
+ void * user_ptr;
+ /* 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. */
+ IAudioClient * output_client;
+ /* Interface pointer to use the event-driven interface. */
+ IAudioRenderClient * render_client;
+ /* Interface pointer to use the volume facilities. */
+ IAudioStreamVolume * audio_stream_volume;
+ /* Interface pointer to use the stream audio clock. */
+ IAudioClock * audio_clock;
+ /* Frames written to the stream since it was opened. Reset on device
+ change. Uses mix_params.rate. */
+ UINT64 frames_written;
+ /* Frames written to the (logical) stream since it was first
+ created. Updated on device change. Uses stream_params.rate. */
+ UINT64 total_frames_written;
+ /* Last valid reported stream position. Used to ensure the position
+ reported by stream_get_position increases monotonically. */
+ UINT64 prev_position;
+ /* Device enumerator to be able to be notified when the default
+ device change. */
+ 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 */
+ wasapi_endpoint_notification_client * notification_client;
+ /* Main andle to the WASAPI capture stream. */
+ IAudioClient * input_client;
+ /* Interface to use the event driven capture interface */
+ 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;
+ /* Set by OnDefaultDeviceChanged when a stream reconfiguration is required.
+ The reconfiguration is handled by the render loop thread. */
+ HANDLE reconfigure_event;
+ /* This is set by WASAPI when we should refill the stream. */
+ HANDLE refill_event;
+ /* 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;
+ /* Each cubeb_stream has its own thread. */
+ HANDLE thread;
+ /* 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;
+ /* Maximum number of frames that can be requested in a callback. */
+ uint32_t output_buffer_frame_count;
+ /* Resampler instance. Resampling will only happen if necessary. */
+ cubeb_resampler * resampler;
+ /* A buffer for up/down mixing multi-channel audio. */
+ float * mix_buffer;
+ /* WASAPI input works in "packets". We re-linearize the audio packets
+ * into this buffer before handing it to the resampler. */
+ auto_array<float> linear_input_buffer;
+ /* Stream volume. Set via stream_set_volume and used to reset volume on
+ device changes. */
+ float volume;
+ /* True if the stream is draining. */
+ bool draining;
+ /* 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;
+};
+
+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)
+ : ref_count(1)
+ , reconfigure_event(event)
+ { }
+
+ virtual ~wasapi_endpoint_notification_client()
+ { }
+
+ HRESULT STDMETHODCALLTYPE
+ OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR device_id)
+ {
+ LOG("Audio device default changed.");
+
+ /* we only support a single stream type for now. */
+ if (flow != eRender && role != eConsole) {
+ return S_OK;
+ }
+
+ BOOL ok = SetEvent(reconfigure_event);
+ if (!ok) {
+ LOG("SetEvent on reconfigure_event failed: %x", 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("Audio device added.");
+ return S_OK;
+ };
+
+ HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR device_id)
+ {
+ LOG("Audio device removed.");
+ return S_OK;
+ }
+
+ HRESULT STDMETHODCALLTYPE
+ OnDeviceStateChanged(LPCWSTR device_id, DWORD new_state)
+ {
+ LOG("Audio device state changed.");
+ return S_OK;
+ }
+
+ HRESULT STDMETHODCALLTYPE
+ OnPropertyValueChanged(LPCWSTR device_id, const PROPERTYKEY key)
+ {
+ LOG("Audio device property value changed.");
+ return S_OK;
+ }
+private:
+ /* refcount for this instance, necessary to implement MSCOM semantics. */
+ LONG ref_count;
+ HANDLE reconfigure_event;
+};
+
+namespace {
+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;
+}
+
+bool should_upmix(cubeb_stream_params & stream, cubeb_stream_params & mixer)
+{
+ return mixer.channels > stream.channels;
+}
+
+bool should_downmix(cubeb_stream_params & stream, cubeb_stream_params & mixer)
+{
+ return mixer.channels < stream.channels;
+}
+
+double stream_to_mix_samplerate_ratio(cubeb_stream_params & stream, cubeb_stream_params & mixer)
+{
+ return double(stream.rate) / mixer.rate;
+}
+
+
+uint32_t
+get_rate(cubeb_stream * stm)
+{
+ return has_input(stm) ? stm->input_stream_params.rate
+ : stm->output_stream_params.rate;
+}
+
+uint32_t
+ms_to_hns(uint32_t ms)
+{
+ return ms * 10000;
+}
+
+uint32_t
+hns_to_ms(REFERENCE_TIME hns)
+{
+ return static_cast<uint32_t>(hns / 10000);
+}
+
+double
+hns_to_s(REFERENCE_TIME hns)
+{
+ return static_cast<double>(hns) / 10000000;
+}
+
+uint32_t
+hns_to_frames(cubeb_stream * stm, REFERENCE_TIME hns)
+{
+ return hns_to_ms(hns * get_rate(stm)) / 1000;
+}
+
+uint32_t
+hns_to_frames(uint32_t rate, REFERENCE_TIME hns)
+{
+ return hns_to_ms(hns * rate) / 1000;
+}
+
+REFERENCE_TIME
+frames_to_hns(cubeb_stream * stm, uint32_t frames)
+{
+ return frames * 1000 / get_rate(stm);
+}
+
+/* Upmix function, copies a mono channel into L and R */
+template<typename T>
+void
+mono_to_stereo(T * in, long insamples, T * out, int32_t out_channels)
+{
+ for (int i = 0, j = 0; i < insamples; ++i, j += out_channels) {
+ out[j] = out[j + 1] = in[i];
+ }
+}
+
+template<typename T>
+void
+upmix(T * in, long inframes, T * out, int32_t in_channels, int32_t out_channels)
+{
+ XASSERT(out_channels >= in_channels && in_channels > 0);
+
+ /* Either way, if we have 2 or more channels, the first two are L and R. */
+ /* If we are playing a mono stream over stereo speakers, copy the data over. */
+ if (in_channels == 1 && out_channels >= 2) {
+ mono_to_stereo(in, inframes, out, out_channels);
+ } else {
+ /* Copy through. */
+ for (int i = 0, o = 0; i < inframes * in_channels;
+ i += in_channels, o += out_channels) {
+ for (int j = 0; j < in_channels; ++j) {
+ out[o + j] = in[i + j];
+ }
+ }
+ }
+
+ /* Check if more channels. */
+ if (out_channels <= 2) {
+ return;
+ }
+
+ /* Put silence in remaining channels. */
+ for (long i = 0, o = 0; i < inframes; ++i, o += out_channels) {
+ for (int j = 2; j < out_channels; ++j) {
+ out[o + j] = 0.0;
+ }
+ }
+}
+
+template<typename T>
+void
+downmix(T * in, long inframes, T * out, int32_t in_channels, int32_t out_channels)
+{
+ XASSERT(in_channels >= out_channels);
+ /* We could use a downmix matrix here, applying mixing weight based on the
+ channel, but directsound and winmm simply drop the channels that cannot be
+ rendered by the hardware, so we do the same for consistency. */
+ long out_index = 0;
+ for (long i = 0; i < inframes * in_channels; i += in_channels) {
+ for (int j = 0; j < out_channels; ++j) {
+ out[out_index + j] = in[i + j];
+ }
+ out_index += out_channels;
+ }
+}
+
+/* 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)
+{
+ size_t stream_frame_size = stm->output_stream_params.channels * sizeof(float);
+ return stream_frame_size * 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, float * input_buffer, long input_frames_count,
+ float * output_buffer, long output_frames_needed)
+{
+ /* If we need to upmix after resampling, resample into the mix buffer to
+ avoid a copy. */
+ float * dest = nullptr;
+ if (has_output(stm)) {
+ if (should_upmix(stm->output_stream_params, stm->output_mix_params) ||
+ should_downmix(stm->output_stream_params, stm->output_mix_params)) {
+ dest = stm->mix_buffer;
+ } else {
+ dest = output_buffer;
+ }
+ }
+
+ long out_frames = cubeb_resampler_fill(stm->resampler,
+ 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 (out_frames < output_frames_needed) {
+ 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));
+
+ if (has_output(stm)) {
+ if (should_upmix(stm->output_stream_params, stm->output_mix_params)) {
+ upmix(dest, out_frames, output_buffer,
+ stm->output_stream_params.channels, stm->output_mix_params.channels);
+ } else if (should_downmix(stm->output_stream_params, stm->output_mix_params)) {
+ downmix(dest, out_frames, output_buffer,
+ stm->output_stream_params.channels, stm->output_mix_params.channels);
+ }
+ }
+
+ return out_frames;
+}
+
+/* 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. */
+bool get_input_buffer(cubeb_stream * stm)
+{
+ HRESULT hr;
+ UINT32 padding_in;
+
+ XASSERT(has_input(stm));
+
+ hr = stm->input_client->GetCurrentPadding(&padding_in);
+ if (FAILED(hr)) {
+ LOG("Failed to get padding");
+ return false;
+ }
+ XASSERT(padding_in <= stm->input_buffer_frame_count);
+ UINT32 total_available_input = padding_in;
+
+ BYTE * input_packet = NULL;
+ DWORD flags;
+ UINT64 dev_pos;
+ UINT32 next;
+ /* Get input packets until we have captured enough frames, and put them in a
+ * contiguous buffer. */
+ uint32_t offset = 0;
+ while (offset != total_available_input) {
+ hr = stm->capture_client->GetNextPacketSize(&next);
+ if (FAILED(hr)) {
+ LOG("cannot get next packet size: %x", hr);
+ return false;
+ }
+ /* This can happen if the capture stream has stopped. Just return in this
+ * case. */
+ if (!next) {
+ break;
+ }
+
+ UINT32 packet_size;
+ hr = stm->capture_client->GetBuffer(&input_packet,
+ &packet_size,
+ &flags,
+ &dev_pos,
+ NULL);
+ if (FAILED(hr)) {
+ LOG("GetBuffer failed for capture: %x", hr);
+ return false;
+ }
+ XASSERT(packet_size == next);
+ if (flags & AUDCLNT_BUFFERFLAGS_SILENT) {
+ LOG("insert silence: ps=%u", packet_size);
+ stm->linear_input_buffer.push_silence(packet_size * stm->input_stream_params.channels);
+ } else {
+ if (should_upmix(stm->input_mix_params, stm->input_stream_params)) {
+ bool ok = stm->linear_input_buffer.reserve(stm->linear_input_buffer.length() +
+ packet_size * stm->input_stream_params.channels);
+ assert(ok);
+ upmix(reinterpret_cast<float*>(input_packet), packet_size,
+ stm->linear_input_buffer.data() + stm->linear_input_buffer.length(),
+ stm->input_mix_params.channels,
+ stm->input_stream_params.channels);
+ stm->linear_input_buffer.set_length(stm->linear_input_buffer.length() + packet_size * stm->input_stream_params.channels);
+ } else if (should_downmix(stm->input_mix_params, stm->input_stream_params)) {
+ bool ok = stm->linear_input_buffer.reserve(stm->linear_input_buffer.length() +
+ packet_size * stm->input_stream_params.channels);
+ assert(ok);
+ downmix(reinterpret_cast<float*>(input_packet), packet_size,
+ stm->linear_input_buffer.data() + stm->linear_input_buffer.length(),
+ stm->input_mix_params.channels,
+ stm->input_stream_params.channels);
+ stm->linear_input_buffer.set_length(stm->linear_input_buffer.length() + packet_size * stm->input_stream_params.channels);
+ } else {
+ stm->linear_input_buffer.push(reinterpret_cast<float*>(input_packet),
+ packet_size * stm->input_stream_params.channels);
+ }
+ }
+ hr = stm->capture_client->ReleaseBuffer(packet_size);
+ if (FAILED(hr)) {
+ LOG("FAILED to release intput buffer");
+ return false;
+ }
+ offset += packet_size;
+ }
+
+ assert(stm->linear_input_buffer.length() >= total_available_input &&
+ offset == total_available_input);
+
+ 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, float *& buffer, size_t & frame_count)
+{
+ UINT32 padding_out;
+ HRESULT hr;
+
+ XASSERT(has_output(stm));
+
+ hr = stm->output_client->GetCurrentPadding(&padding_out);
+ if (FAILED(hr)) {
+ LOG("Failed to get padding: %x", 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 = reinterpret_cast<float*>(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;
+ float * 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;
+ if (!input_frames) {
+ return true;
+ }
+
+ 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;
+ }
+
+ // When WASAPI has not filled the input buffer yet, send silence.
+ double output_duration = double(output_frames) / stm->output_mix_params.rate;
+ double input_duration = double(stm->linear_input_buffer.length() / stm->input_stream_params.channels) / stm->input_mix_params.rate;
+ if (input_duration < output_duration) {
+ size_t padding = size_t(round((output_duration - input_duration) * stm->input_mix_params.rate));
+ LOG("padding silence: out=%f in=%f pad=%u", output_duration, input_duration, padding);
+ stm->linear_input_buffer.push_front_silence(padding * stm->input_stream_params.channels);
+ }
+
+ LOGV("Duplex callback: input frames: %zu, output frames: %zu",
+ stm->linear_input_buffer.length(), output_frames);
+
+ refill(stm,
+ stm->linear_input_buffer.data(),
+ stm->linear_input_buffer.length(),
+ output_buffer,
+ output_frames);
+
+ stm->linear_input_buffer.clear();
+
+ hr = stm->render_client->ReleaseBuffer(output_frames, 0);
+ if (FAILED(hr)) {
+ LOG("failed to release buffer: %x", hr);
+ return false;
+ }
+ return true;
+}
+
+bool
+refill_callback_input(cubeb_stream * stm)
+{
+ bool rv, consumed_all_buffer;
+
+ XASSERT(has_input(stm) && !has_output(stm));
+
+ rv = get_input_buffer(stm);
+ if (!rv) {
+ return rv;
+ }
+
+ // This can happen at the very beginning of the stream.
+ if (!stm->linear_input_buffer.length()) {
+ return true;
+ }
+
+ LOGV("Input callback: input frames: %zu", stm->linear_input_buffer.length());
+
+ long read = refill(stm,
+ stm->linear_input_buffer.data(),
+ stm->linear_input_buffer.length(),
+ nullptr,
+ 0);
+
+ consumed_all_buffer = read == stm->linear_input_buffer.length();
+
+ stm->linear_input_buffer.clear();
+
+ return consumed_all_buffer;
+}
+
+bool
+refill_callback_output(cubeb_stream * stm)
+{
+ bool rv;
+ HRESULT hr;
+ float * 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);
+
+ LOGV("Output callback: output frames requested: %zu, got %ld",
+ output_frames, got);
+
+ XASSERT(got >= 0);
+ XASSERT(got == output_frames || stm->draining);
+
+ hr = stm->render_client->ReleaseBuffer(got, 0);
+ if (FAILED(hr)) {
+ LOG("failed to release buffer: %x", hr);
+ return false;
+ }
+
+ return 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;
+
+ 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;
+ auto_com com;
+ if (!com.ok()) {
+ LOG("COM initialization failed on render_loop thread.");
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+ return 0;
+ }
+
+ /* We could consider using "Pro Audio" here for WebAudio and
+ maybe WebRTC. */
+ mmcss_handle =
+ stm->context->set_mm_thread_characteristics("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: %x", GetLastError());
+ }
+
+ // This has already been nulled out, simply exit.
+ if (!emergency_bailout) {
+ is_playing = false;
+ }
+
+ /* 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 = 5;
+ 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) {
+ stm->output_client->Start();
+ LOG("Output started after reconfigure.");
+ }
+ if (stm->input_client) {
+ stm->input_client->Start();
+ 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 %d not handled in render loop.", waitResult);
+ abort();
+ }
+ }
+
+ if (FAILED(hr)) {
+ stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
+ }
+
+ stm->context->revert_mm_thread_characteristics(mmcss_handle);
+
+ return 0;
+}
+
+void wasapi_destroy(cubeb * context);
+
+HANDLE WINAPI set_mm_thread_characteristics_noop(const char *, LPDWORD mmcss_task_index)
+{
+ return (HANDLE)1;
+}
+
+BOOL WINAPI revert_mm_thread_characteristics_noop(HANDLE mmcss_handle)
+{
+ return true;
+}
+
+HRESULT register_notification_client(cubeb_stream * stm)
+{
+ HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
+ NULL, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(&stm->device_enumerator));
+ if (FAILED(hr)) {
+ LOG("Could not get device enumerator: %x", hr);
+ return hr;
+ }
+
+ stm->notification_client = new wasapi_endpoint_notification_client(stm->reconfigure_event);
+
+ hr = stm->device_enumerator->RegisterEndpointNotificationCallback(stm->notification_client);
+ if (FAILED(hr)) {
+ LOG("Could not register endpoint notification callback: %x", hr);
+ SafeRelease(stm->notification_client);
+ stm->notification_client = nullptr;
+ SafeRelease(stm->device_enumerator);
+ 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);
+ 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.
+ SafeRelease(stm->device_enumerator);
+ return S_OK;
+ }
+
+ SafeRelease(stm->notification_client);
+ SafeRelease(stm->device_enumerator);
+
+ return S_OK;
+}
+
+HRESULT get_endpoint(IMMDevice ** device, LPCWSTR devid)
+{
+ IMMDeviceEnumerator * enumerator;
+ HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
+ NULL, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(&enumerator));
+ if (FAILED(hr)) {
+ LOG("Could not get device enumerator: %x", hr);
+ return hr;
+ }
+
+ hr = enumerator->GetDevice(devid, device);
+ if (FAILED(hr)) {
+ LOG("Could not get device: %x", hr);
+ SafeRelease(enumerator);
+ return hr;
+ }
+
+ SafeRelease(enumerator);
+
+ return S_OK;
+}
+
+HRESULT get_default_endpoint(IMMDevice ** device, EDataFlow direction)
+{
+ IMMDeviceEnumerator * enumerator;
+ HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
+ NULL, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(&enumerator));
+ if (FAILED(hr)) {
+ LOG("Could not get device enumerator: %x", hr);
+ return hr;
+ }
+ hr = enumerator->GetDefaultAudioEndpoint(direction, eConsole, device);
+ if (FAILED(hr)) {
+ LOG("Could not get default audio endpoint: %x", hr);
+ SafeRelease(enumerator);
+ return hr;
+ }
+
+ SafeRelease(enumerator);
+
+ 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: %x", hr);
+ return 0;
+ }
+
+ UINT64 pos;
+ hr = stm->audio_clock->GetPosition(&pos, NULL);
+ if (FAILED(hr)) {
+ LOG("GetPosition failed: %x", 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 = max_pos - cur_pos;
+ XASSERT(delay >= 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 (hr != S_OK) {
+ LOG("could not get the channel count: %x", 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 (hr != S_OK) {
+ LOG("could not set the channels volume: %x", hr);
+ return CUBEB_ERROR;
+ }
+
+ return CUBEB_OK;
+}
+} // namespace anonymous
+
+extern "C" {
+int wasapi_init(cubeb ** context, char const * context_name)
+{
+ HRESULT hr;
+ auto_com com;
+ if (!com.ok()) {
+ return CUBEB_ERROR;
+ }
+
+ /* 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. */
+ IMMDevice * device;
+ hr = get_default_endpoint(&device, eRender);
+ if (FAILED(hr)) {
+ LOG("Could not get device: %x", hr);
+ return CUBEB_ERROR;
+ }
+ SafeRelease(device);
+
+ cubeb * ctx = (cubeb *)calloc(1, sizeof(cubeb));
+ if (!ctx) {
+ return CUBEB_ERROR;
+ }
+
+ ctx->ops = &wasapi_ops;
+
+ ctx->mmcss_module = LoadLibraryA("Avrt.dll");
+
+ if (ctx->mmcss_module) {
+ ctx->set_mm_thread_characteristics =
+ (set_mm_thread_characteristics_function) GetProcAddress(
+ ctx->mmcss_module, "AvSetMmThreadCharacteristicsA");
+ ctx->revert_mm_thread_characteristics =
+ (revert_mm_thread_characteristics_function) GetProcAddress(
+ ctx->mmcss_module, "AvRevertMmThreadCharacteristics");
+ if (!(ctx->set_mm_thread_characteristics && ctx->revert_mm_thread_characteristics)) {
+ LOG("Could not load AvSetMmThreadCharacteristics or AvRevertMmThreadCharacteristics: %x", GetLastError());
+ FreeLibrary(ctx->mmcss_module);
+ }
+ } else {
+ // This is not a fatal error, but we might end up glitching when
+ // the system is under high load.
+ LOG("Could not load Avrt.dll");
+ ctx->set_mm_thread_characteristics = &set_mm_thread_characteristics_noop;
+ ctx->revert_mm_thread_characteristics = &revert_mm_thread_characteristics_noop;
+ }
+
+ *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: %d", 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_TIMEOUT) {
+ /* 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 timed out,"
+ " leaking the thread: %d", GetLastError());
+ rv = false;
+ }
+ if (r == WAIT_FAILED) {
+ *(stm->emergency_bailout) = true;
+ // We give the ownership to the rendering thread.
+ stm->emergency_bailout = nullptr;
+ LOG("Destroy WaitForSingleObject on thread failed: %d", 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->mmcss_module) {
+ FreeLibrary(context->mmcss_module);
+ }
+ free(context);
+}
+
+char const * wasapi_get_backend_id(cubeb * context)
+{
+ return "wasapi";
+}
+
+int
+wasapi_get_max_channel_count(cubeb * ctx, uint32_t * max_channels)
+{
+ HRESULT hr;
+ IAudioClient * client;
+ WAVEFORMATEX * mix_format;
+ auto_com com;
+ if (!com.ok()) {
+ return CUBEB_ERROR;
+ }
+
+ XASSERT(ctx && max_channels);
+
+ IMMDevice * device;
+ hr = get_default_endpoint(&device, eRender);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+
+ hr = device->Activate(__uuidof(IAudioClient),
+ CLSCTX_INPROC_SERVER,
+ NULL, (void **)&client);
+ SafeRelease(device);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+
+ hr = client->GetMixFormat(&mix_format);
+ if (FAILED(hr)) {
+ SafeRelease(client);
+ return CUBEB_ERROR;
+ }
+
+ *max_channels = mix_format->nChannels;
+
+ CoTaskMemFree(mix_format);
+ SafeRelease(client);
+
+ return CUBEB_OK;
+}
+
+int
+wasapi_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency_frames)
+{
+ HRESULT hr;
+ IAudioClient * client;
+ REFERENCE_TIME default_period;
+ auto_com com;
+ if (!com.ok()) {
+ return CUBEB_ERROR;
+ }
+
+ if (params.format != CUBEB_SAMPLE_FLOAT32NE) {
+ return CUBEB_ERROR_INVALID_FORMAT;
+ }
+
+ IMMDevice * device;
+ hr = get_default_endpoint(&device, eRender);
+ if (FAILED(hr)) {
+ LOG("Could not get default endpoint: %x", hr);
+ return CUBEB_ERROR;
+ }
+
+ hr = device->Activate(__uuidof(IAudioClient),
+ CLSCTX_INPROC_SERVER,
+ NULL, (void **)&client);
+ SafeRelease(device);
+ if (FAILED(hr)) {
+ LOG("Could not activate device for latency: %x", hr);
+ return CUBEB_ERROR;
+ }
+
+ /* The second parameter is for exclusive mode, that we don't use. */
+ hr = client->GetDevicePeriod(&default_period, NULL);
+ if (FAILED(hr)) {
+ SafeRelease(client);
+ LOG("Could not get device period: %x", hr);
+ return CUBEB_ERROR;
+ }
+
+ LOG("default device period: %lld", default_period);
+
+ /* 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 */
+
+ *latency_frames = hns_to_frames(params.rate, default_period);
+
+ LOG("Minimum latency in frames: %u", *latency_frames);
+
+ SafeRelease(client);
+
+ return CUBEB_OK;
+}
+
+int
+wasapi_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate)
+{
+ HRESULT hr;
+ IAudioClient * client;
+ WAVEFORMATEX * mix_format;
+ auto_com com;
+ if (!com.ok()) {
+ return CUBEB_ERROR;
+ }
+
+ IMMDevice * device;
+ hr = get_default_endpoint(&device, eRender);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+
+ hr = device->Activate(__uuidof(IAudioClient),
+ CLSCTX_INPROC_SERVER,
+ NULL, (void **)&client);
+ SafeRelease(device);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+
+ hr = client->GetMixFormat(&mix_format);
+ if (FAILED(hr)) {
+ SafeRelease(client);
+ return CUBEB_ERROR;
+ }
+
+ *rate = mix_format->nSamplesPerSec;
+
+ LOG("Preferred sample rate for output: %u", *rate);
+
+ CoTaskMemFree(mix_format);
+ SafeRelease(client);
+
+ return CUBEB_OK;
+}
+
+void wasapi_stream_destroy(cubeb_stream * stm);
+
+/* 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, WAVEFORMATEX ** mix_format, const cubeb_stream_params * stream_params)
+{
+ /* Common case: the hardware is stereo. Up-mixing and down-mixing will be
+ handled in the callback. */
+ if ((*mix_format)->nChannels <= 2) {
+ return;
+ }
+
+ /* 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);
+
+ /* Stash a copy of the original mix format in case we need to restore it later. */
+ WAVEFORMATEXTENSIBLE hw_mix_format = *format_pcm;
+
+ /* The hardware is in surround mode, we want to only use front left and front
+ right. Try that, and check if it works. */
+ switch (stream_params->channels) {
+ case 1: /* Mono */
+ format_pcm->dwChannelMask = KSAUDIO_SPEAKER_MONO;
+ break;
+ case 2: /* Stereo */
+ format_pcm->dwChannelMask = KSAUDIO_SPEAKER_STEREO;
+ break;
+ default:
+ XASSERT(false && "Channel layout not supported.");
+ break;
+ }
+ (*mix_format)->nChannels = stream_params->channels;
+ (*mix_format)->nBlockAlign = ((*mix_format)->wBitsPerSample * (*mix_format)->nChannels) / 8;
+ (*mix_format)->nAvgBytesPerSec = (*mix_format)->nSamplesPerSec * (*mix_format)->nBlockAlign;
+ format_pcm->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+ (*mix_format)->wBitsPerSample = 32;
+ format_pcm->Samples.wValidBitsPerSample = (*mix_format)->wBitsPerSample;
+
+ /* Check if wasapi will accept our channel layout request. */
+ WAVEFORMATEX * closest;
+ HRESULT hr = stm->output_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED,
+ *mix_format,
+ &closest);
+ if (hr == S_FALSE) {
+ /* Not supported, but WASAPI gives us a suggestion. Use it, and handle the
+ eventual upmix/downmix ourselves */
+ LOG("Using WASAPI suggested format: channels: %d", closest->nChannels);
+ WAVEFORMATEXTENSIBLE * closest_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(closest);
+ XASSERT(closest_pcm->SubFormat == format_pcm->SubFormat);
+ CoTaskMemFree(*mix_format);
+ *mix_format = closest;
+ } 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. */
+ *reinterpret_cast<WAVEFORMATEXTENSIBLE *>(*mix_format) = hw_mix_format;
+ } else if (hr == S_OK) {
+ LOG("Requested format accepted by WASAPI.");
+ } else {
+ LOG("IsFormatSupported unhandled error: %x", hr);
+ }
+}
+
+#define DIRECTION_NAME (direction == eCapture ? "capture" : "render")
+
+template<typename T>
+int setup_wasapi_stream_one_side(cubeb_stream * stm,
+ cubeb_stream_params * stream_params,
+ cubeb_devid devid,
+ EDataFlow direction,
+ REFIID riid,
+ IAudioClient ** audio_client,
+ uint32_t * buffer_frame_count,
+ HANDLE & event,
+ T ** render_or_capture_client,
+ cubeb_stream_params * mix_params)
+{
+ IMMDevice * device;
+ WAVEFORMATEX * mix_format;
+ HRESULT hr;
+
+ 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) {
+ std::unique_ptr<const wchar_t[]> id(utf8_to_wstr(reinterpret_cast<char*>(devid)));
+ hr = get_endpoint(&device, id.get());
+ if (FAILED(hr)) {
+ LOG("Could not get %s endpoint, error: %x\n", DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+ }
+ else {
+ hr = get_default_endpoint(&device, direction);
+ if (FAILED(hr)) {
+ LOG("Could not get default %s endpoint, error: %x\n", DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+ }
+
+ /* Get a client. We will get all other interfaces we need from
+ * this pointer. */
+ hr = device->Activate(__uuidof(IAudioClient),
+ CLSCTX_INPROC_SERVER,
+ NULL, (void **)audio_client);
+ SafeRelease(device);
+ if (FAILED(hr)) {
+ LOG("Could not activate the device to get an audio"
+ " client for %s: error: %x\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;
+ 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. */
+ hr = (*audio_client)->GetMixFormat(&mix_format);
+ if (FAILED(hr)) {
+ LOG("Could not fetch current mix format from the audio"
+ " client for %s: error: %x", DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+
+ handle_channel_layout(stm, &mix_format, stream_params);
+
+ /* Shared mode WASAPI always supports float32 sample format, so this
+ * is safe. */
+ mix_params->format = CUBEB_SAMPLE_FLOAT32NE;
+ mix_params->rate = mix_format->nSamplesPerSec;
+ mix_params->channels = mix_format->nChannels;
+ LOG("Setup requested=[f=%d r=%u c=%u] mix=[f=%d r=%u c=%u]",
+ stream_params->format, stream_params->rate, stream_params->channels,
+ mix_params->format, mix_params->rate, mix_params->channels);
+
+ hr = (*audio_client)->Initialize(AUDCLNT_SHAREMODE_SHARED,
+ AUDCLNT_STREAMFLAGS_EVENTCALLBACK |
+ AUDCLNT_STREAMFLAGS_NOPERSIST,
+ frames_to_hns(stm, stm->latency),
+ 0,
+ mix_format,
+ NULL);
+ if (FAILED(hr)) {
+ LOG("Unable to initialize audio client for %s: %x.", DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+
+ CoTaskMemFree(mix_format);
+
+ hr = (*audio_client)->GetBufferSize(buffer_frame_count);
+ if (FAILED(hr)) {
+ LOG("Could not get the buffer size from the client"
+ " for %s %x.", DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+
+ // Input is up/down mixed when depacketized in get_input_buffer.
+ if (has_output(stm) &&
+ (should_upmix(*stream_params, *mix_params) ||
+ should_downmix(*stream_params, *mix_params))) {
+ stm->mix_buffer = (float *)malloc(frames_to_bytes_before_mix(stm, *buffer_frame_count));
+ }
+
+ hr = (*audio_client)->SetEventHandle(event);
+ if (FAILED(hr)) {
+ LOG("Could set the event handle for the %s client %x.",
+ DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+
+ hr = (*audio_client)->GetService(riid, (void **)render_or_capture_client);
+ if (FAILED(hr)) {
+ LOG("Could not get the %s client %x.", DIRECTION_NAME, hr);
+ return CUBEB_ERROR;
+ }
+
+ return CUBEB_OK;
+}
+
+#undef DIRECTION_NAME
+
+int setup_wasapi_stream(cubeb_stream * stm)
+{
+ HRESULT hr;
+ int rv;
+
+ stm->stream_reset_lock.assert_current_thread_owns();
+
+ auto_com com;
+ if (!com.ok()) {
+ LOG("Failure to initialize COM.");
+ return CUBEB_ERROR;
+ }
+
+ XASSERT((!stm->output_client || !stm->input_client) && "WASAPI stream already setup, close it first.");
+
+ if (has_input(stm)) {
+ LOG("Setup capture: device=%x", (int)stm->input_device);
+ rv = setup_wasapi_stream_one_side(stm,
+ &stm->input_stream_params,
+ stm->input_device,
+ eCapture,
+ __uuidof(IAudioCaptureClient),
+ &stm->input_client,
+ &stm->input_buffer_frame_count,
+ stm->input_available_event,
+ &stm->capture_client,
+ &stm->input_mix_params);
+ if (rv != CUBEB_OK) {
+ LOG("Failure to open the input side.");
+ return rv;
+ }
+ }
+
+ if (has_output(stm)) {
+ LOG("Setup render: device=%x", (int)stm->output_device);
+ rv = setup_wasapi_stream_one_side(stm,
+ &stm->output_stream_params,
+ stm->output_device,
+ eRender,
+ __uuidof(IAudioRenderClient),
+ &stm->output_client,
+ &stm->output_buffer_frame_count,
+ stm->refill_event,
+ &stm->render_client,
+ &stm->output_mix_params);
+ if (rv != CUBEB_OK) {
+ LOG("Failure to open the output side.");
+ return rv;
+ }
+
+ hr = stm->output_client->GetService(__uuidof(IAudioStreamVolume),
+ (void **)&stm->audio_stream_volume);
+ if (FAILED(hr)) {
+ LOG("Could not get the IAudioStreamVolume: %x", hr);
+ return CUBEB_ERROR;
+ }
+
+ XASSERT(stm->frames_written == 0);
+ hr = stm->output_client->GetService(__uuidof(IAudioClock),
+ (void **)&stm->audio_clock);
+ if (FAILED(hr)) {
+ LOG("Could not get the IAudioClock: %x", 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)) {
+ assert(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 =
+ 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,
+ 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;
+ }
+
+ return CUBEB_OK;
+}
+
+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)
+{
+ HRESULT hr;
+ int rv;
+ auto_com com;
+ if (!com.ok()) {
+ return CUBEB_ERROR;
+ }
+
+ XASSERT(context && stream && (input_stream_params || output_stream_params));
+
+ if (output_stream_params && output_stream_params->format != CUBEB_SAMPLE_FLOAT32NE ||
+ input_stream_params && input_stream_params->format != CUBEB_SAMPLE_FLOAT32NE) {
+ LOG("Invalid format, %p %p %d %d",
+ output_stream_params, input_stream_params,
+ output_stream_params && output_stream_params->format,
+ input_stream_params && input_stream_params->format);
+ return CUBEB_ERROR_INVALID_FORMAT;
+ }
+
+ cubeb_stream * stm = (cubeb_stream *)calloc(1, sizeof(cubeb_stream));
+
+ XASSERT(stm);
+
+ stm->context = context;
+ stm->data_callback = data_callback;
+ stm->state_callback = state_callback;
+ stm->user_ptr = user_ptr;
+ stm->draining = false;
+ if (input_stream_params) {
+ stm->input_stream_params = *input_stream_params;
+ stm->input_device = input_device;
+ }
+ if (output_stream_params) {
+ stm->output_stream_params = *output_stream_params;
+ stm->output_device = output_device;
+ }
+
+ stm->latency = latency_frames;
+ stm->volume = 1.0;
+
+ // Placement new to call ctor.
+ new (&stm->stream_reset_lock) owned_critical_section();
+
+ stm->reconfigure_event = CreateEvent(NULL, 0, 0, NULL);
+ if (!stm->reconfigure_event) {
+ LOG("Can't create the reconfigure event, error: %x", GetLastError());
+ wasapi_stream_destroy(stm);
+ 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: %x", GetLastError());
+ wasapi_stream_destroy(stm);
+ 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: %x", GetLastError());
+ wasapi_stream_destroy(stm);
+ 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);
+ }
+ if (rv != CUBEB_OK) {
+ wasapi_stream_destroy(stm);
+ return rv;
+ }
+
+ hr = register_notification_client(stm);
+ 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, %x", hr);
+ }
+
+ *stream = stm;
+
+ return CUBEB_OK;
+}
+
+void close_wasapi_stream(cubeb_stream * stm)
+{
+ XASSERT(stm);
+
+ stm->stream_reset_lock.assert_current_thread_owns();
+
+ SafeRelease(stm->output_client);
+ stm->output_client = NULL;
+ SafeRelease(stm->input_client);
+ stm->input_client = NULL;
+
+ SafeRelease(stm->render_client);
+ stm->render_client = NULL;
+
+ SafeRelease(stm->capture_client);
+ stm->capture_client = NULL;
+
+ SafeRelease(stm->audio_stream_volume);
+ stm->audio_stream_volume = NULL;
+
+ SafeRelease(stm->audio_clock);
+ stm->audio_clock = NULL;
+ 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;
+
+ if (stm->resampler) {
+ cubeb_resampler_destroy(stm->resampler);
+ stm->resampler = NULL;
+ }
+
+ free(stm->mix_buffer);
+ stm->mix_buffer = NULL;
+}
+
+void wasapi_stream_destroy(cubeb_stream * stm)
+{
+ XASSERT(stm);
+
+ // Only free stm->emergency_bailout if we could not 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;
+ }
+
+ unregister_notification_client(stm);
+
+ SafeRelease(stm->reconfigure_event);
+ SafeRelease(stm->refill_event);
+ SafeRelease(stm->input_available_event);
+
+ {
+ auto_lock lock(stm->stream_reset_lock);
+ close_wasapi_stream(stm);
+ }
+
+ // Need to call dtor to free the resource in owned_critical_section.
+ stm->stream_reset_lock.~owned_critical_section();
+
+ free(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: %x",
+ 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: %x",
+ dir == OUTPUT ? "output" : "input", hr);
+ return CUBEB_ERROR;
+ }
+ } else if (FAILED(hr)) {
+ LOG("could not start the %s stream: %x.",
+ 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: %x", GetLastError());
+ return CUBEB_ERROR;
+ }
+
+ 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;
+ }
+
+ 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)) {
+ // This is null if we've given the pointer to the other thread
+ if (stm->emergency_bailout.load()) {
+ delete stm->emergency_bailout.load();
+ stm->emergency_bailout = nullptr;
+ }
+ }
+
+ 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) {
+ return CUBEB_ERROR;
+ }
+
+ REFERENCE_TIME latency_hns;
+ HRESULT hr = stm->output_client->GetStreamLatency(&latency_hns);
+ if (FAILED(hr)) {
+ return CUBEB_ERROR;
+ }
+ *latency = hns_to_frames(stm, 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 *
+wstr_to_utf8(LPCWSTR str)
+{
+ char * ret = NULL;
+ int size;
+
+ size = ::WideCharToMultiByte(CP_UTF8, 0, str, -1, ret, 0, NULL, NULL);
+ if (size > 0) {
+ ret = static_cast<char *>(malloc(size));
+ ::WideCharToMultiByte(CP_UTF8, 0, str, -1, ret, size, NULL, NULL);
+ }
+
+ return ret;
+}
+
+static std::unique_ptr<const wchar_t[]>
+utf8_to_wstr(char* str)
+{
+ std::unique_ptr<wchar_t[]> ret;
+ int size;
+
+ size = ::MultiByteToWideChar(CP_UTF8, 0, str, -1, nullptr, 0);
+ if (size > 0) {
+ ret.reset(new wchar_t[size]);
+ ::MultiByteToWideChar(CP_UTF8, 0, str, -1, ret.get(), size);
+ }
+
+ return std::move(ret);
+}
+
+static IMMDevice *
+wasapi_get_device_node(IMMDeviceEnumerator * enumerator, IMMDevice * dev)
+{
+ IMMDevice * ret = NULL;
+ IDeviceTopology * devtopo = NULL;
+ IConnector * connector = NULL;
+
+ if (SUCCEEDED(dev->Activate(__uuidof(IDeviceTopology), CLSCTX_ALL, NULL, (void**)&devtopo)) &&
+ SUCCEEDED(devtopo->GetConnector(0, &connector))) {
+ LPWSTR filterid;
+ if (SUCCEEDED(connector->GetDeviceIdConnectedTo(&filterid))) {
+ if (FAILED(enumerator->GetDevice(filterid, &ret)))
+ ret = NULL;
+ CoTaskMemFree(filterid);
+ }
+ }
+
+ SafeRelease(connector);
+ SafeRelease(devtopo);
+ return ret;
+}
+
+static BOOL
+wasapi_is_default_device(EDataFlow flow, ERole role, LPCWSTR device_id,
+ IMMDeviceEnumerator * enumerator)
+{
+ BOOL ret = FALSE;
+ IMMDevice * dev;
+ HRESULT hr;
+
+ hr = enumerator->GetDefaultAudioEndpoint(flow, role, &dev);
+ if (SUCCEEDED(hr)) {
+ LPWSTR defdevid = NULL;
+ if (SUCCEEDED(dev->GetId(&defdevid)))
+ ret = (wcscmp(defdevid, device_id) == 0);
+ if (defdevid != NULL)
+ CoTaskMemFree(defdevid);
+ SafeRelease(dev);
+ }
+
+ return ret;
+}
+
+static cubeb_device_info *
+wasapi_create_device(IMMDeviceEnumerator * enumerator, IMMDevice * dev)
+{
+ IMMEndpoint * endpoint = NULL;
+ IMMDevice * devnode = NULL;
+ IAudioClient * client = NULL;
+ cubeb_device_info * ret = NULL;
+ EDataFlow flow;
+ LPWSTR device_id = NULL;
+ DWORD state = DEVICE_STATE_NOTPRESENT;
+ IPropertyStore * propstore = NULL;
+ PROPVARIANT propvar;
+ REFERENCE_TIME def_period, min_period;
+ HRESULT hr;
+
+ PropVariantInit(&propvar);
+
+ hr = dev->QueryInterface(IID_PPV_ARGS(&endpoint));
+ if (FAILED(hr)) goto done;
+
+ hr = endpoint->GetDataFlow(&flow);
+ if (FAILED(hr)) goto done;
+
+ hr = dev->GetId(&device_id);
+ if (FAILED(hr)) goto done;
+
+ hr = dev->OpenPropertyStore(STGM_READ, &propstore);
+ if (FAILED(hr)) goto done;
+
+ hr = dev->GetState(&state);
+ if (FAILED(hr)) goto done;
+
+ ret = (cubeb_device_info *)calloc(1, sizeof(cubeb_device_info));
+
+ ret->devid = ret->device_id = wstr_to_utf8(device_id);
+ hr = propstore->GetValue(PKEY_Device_FriendlyName, &propvar);
+ if (SUCCEEDED(hr))
+ ret->friendly_name = wstr_to_utf8(propvar.pwszVal);
+
+ devnode = wasapi_get_device_node(enumerator, dev);
+ if (devnode != NULL) {
+ IPropertyStore * ps = NULL;
+ hr = devnode->OpenPropertyStore(STGM_READ, &ps);
+ if (FAILED(hr)) goto done;
+
+ PropVariantClear(&propvar);
+ hr = ps->GetValue(PKEY_Device_InstanceId, &propvar);
+ if (SUCCEEDED(hr)) {
+ ret->group_id = wstr_to_utf8(propvar.pwszVal);
+ }
+ SafeRelease(ps);
+ }
+
+ ret->preferred = CUBEB_DEVICE_PREF_NONE;
+ if (wasapi_is_default_device(flow, eConsole, device_id, enumerator))
+ ret->preferred = (cubeb_device_pref)(ret->preferred | CUBEB_DEVICE_PREF_MULTIMEDIA);
+ if (wasapi_is_default_device(flow, eCommunications, device_id, enumerator))
+ ret->preferred = (cubeb_device_pref)(ret->preferred | CUBEB_DEVICE_PREF_VOICE);
+ if (wasapi_is_default_device(flow, eConsole, device_id, 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 = CUBEB_DEVICE_FMT_F32NE; /* cubeb only supports 32bit float at the moment */
+ ret->default_format = CUBEB_DEVICE_FMT_F32NE;
+ PropVariantClear(&propvar);
+ hr = propstore->GetValue(PKEY_AudioEngine_DeviceFormat, &propvar);
+ if (SUCCEEDED(hr) && propvar.vt == VT_BLOB) {
+ if (propvar.blob.cbSize == sizeof(PCMWAVEFORMAT)) {
+ const PCMWAVEFORMAT * pcm = reinterpret_cast<const PCMWAVEFORMAT *>(propvar.blob.pBlobData);
+
+ ret->max_rate = ret->min_rate = ret->default_rate = pcm->wf.nSamplesPerSec;
+ ret->max_channels = pcm->wf.nChannels;
+ } else if (propvar.blob.cbSize >= sizeof(WAVEFORMATEX)) {
+ WAVEFORMATEX* wfx = reinterpret_cast<WAVEFORMATEX*>(propvar.blob.pBlobData);
+
+ if (propvar.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, (void**)&client)) &&
+ 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;
+ }
+ SafeRelease(client);
+
+done:
+ SafeRelease(devnode);
+ SafeRelease(endpoint);
+ SafeRelease(propstore);
+ if (device_id != NULL)
+ CoTaskMemFree(device_id);
+ PropVariantClear(&propvar);
+ return ret;
+}
+
+static int
+wasapi_enumerate_devices(cubeb * context, cubeb_device_type type,
+ cubeb_device_collection ** out)
+{
+ auto_com com;
+ IMMDeviceEnumerator * enumerator;
+ IMMDeviceCollection * collection;
+ IMMDevice * dev;
+ cubeb_device_info * cur;
+ HRESULT hr;
+ UINT cc, i;
+ EDataFlow flow;
+
+ *out = NULL;
+
+ if (!com.ok())
+ return CUBEB_ERROR;
+
+ hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL,
+ CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&enumerator));
+ if (FAILED(hr)) {
+ LOG("Could not get device enumerator: %x", 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_INPUT)) flow = eAll;
+ else return CUBEB_ERROR;
+
+ hr = enumerator->EnumAudioEndpoints(flow, DEVICE_STATEMASK_ALL, &collection);
+ if (FAILED(hr)) {
+ LOG("Could not enumerate audio endpoints: %x", hr);
+ return CUBEB_ERROR;
+ }
+
+ hr = collection->GetCount(&cc);
+ if (FAILED(hr)) {
+ LOG("IMMDeviceCollection::GetCount() failed: %x", hr);
+ return CUBEB_ERROR;
+ }
+ *out = (cubeb_device_collection *) malloc(sizeof(cubeb_device_collection) +
+ sizeof(cubeb_device_info*) * (cc > 0 ? cc - 1 : 0));
+ if (!*out) {
+ return CUBEB_ERROR;
+ }
+ (*out)->count = 0;
+ for (i = 0; i < cc; i++) {
+ hr = collection->Item(i, &dev);
+ if (FAILED(hr)) {
+ LOG("IMMDeviceCollection::Item(%u) failed: %x", i-1, hr);
+ } else if ((cur = wasapi_create_device(enumerator, dev)) != NULL) {
+ (*out)->device[(*out)->count++] = cur;
+ }
+ }
+
+ SafeRelease(collection);
+ SafeRelease(enumerator);
+ 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,
+ /*.destroy =*/ wasapi_destroy,
+ /*.stream_init =*/ wasapi_stream_init,
+ /*.stream_destroy =*/ wasapi_stream_destroy,
+ /*.stream_start =*/ wasapi_stream_start,
+ /*.stream_stop =*/ wasapi_stream_stop,
+ /*.stream_get_position =*/ wasapi_stream_get_position,
+ /*.stream_get_latency =*/ wasapi_stream_get_latency,
+ /*.stream_set_volume =*/ wasapi_stream_set_volume,
+ /*.stream_set_panning =*/ NULL,
+ /*.stream_get_current_device =*/ NULL,
+ /*.stream_device_destroy =*/ NULL,
+ /*.stream_register_device_changed_callback =*/ NULL,
+ /*.register_device_collection_changed =*/ NULL
+};
+} // namespace anonymous