#define _WIN32_WINNT 0x0600
#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"
#ifndef PKEY_Device_FriendlyName
DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); #endif
#ifndef PKEY_Device_InstanceId
DEFINE_PROPERTYKEY(PKEY_Device_InstanceId, 0x78c34fc8, 0x104a, 0x4aca, 0x9e, 0xa4, 0x52, 0x4d, 0x52, 0x99, 0x6e, 0x57, 0x00000100); #endif
namespace {
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;
};
struct auto_com {
auto_com() {
result = CoInitializeEx(NULL, COINIT_MULTITHREADED);
}
~auto_com() {
if (result == RPC_E_CHANGED_MODE) {
LOG("COM was already initialized in STA.");
} else if (result == S_FALSE) {
LOG("COM was already initialized in MTA");
}
if (SUCCEEDED(result)) {
CoUninitialize();
}
}
bool ok() {
return result == RPC_E_CHANGED_MODE || SUCCEEDED(result);
}
private:
HRESULT result;
};
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 const * wstr_to_utf8(wchar_t const * str);
static std::unique_ptr<wchar_t const []> utf8_to_wstr(char const * str);
}
struct cubeb {
cubeb_ops const * ops = &wasapi_ops;
cubeb_strings * device_ids;
};
class wasapi_endpoint_notification_client;
typedef bool (*wasapi_refill_callback)(cubeb_stream * stm);
struct cubeb_stream {
cubeb * context = nullptr;
void * user_ptr = nullptr;
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 };
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 };
std::unique_ptr<const wchar_t[]> input_device;
std::unique_ptr<const wchar_t[]> output_device;
unsigned latency = 0;
cubeb_state_callback state_callback = nullptr;
cubeb_data_callback data_callback = nullptr;
wasapi_refill_callback refill_callback = nullptr;
bool has_dummy_output = false;
com_ptr<IAudioClient> output_client;
com_ptr<IAudioRenderClient> render_client;
com_ptr<IAudioStreamVolume> audio_stream_volume;
com_ptr<IAudioClock> audio_clock;
UINT64 frames_written = 0;
UINT64 total_frames_written = 0;
UINT64 prev_position = 0;
com_ptr<IMMDeviceEnumerator> device_enumerator;
com_ptr<wasapi_endpoint_notification_client> notification_client;
com_ptr<IAudioClient> input_client;
com_ptr<IAudioCaptureClient> capture_client;
HANDLE shutdown_event = 0;
HANDLE reconfigure_event = 0;
HANDLE refill_event = 0;
HANDLE input_available_event = 0;
HANDLE thread = 0;
owned_critical_section stream_reset_lock;
uint32_t input_buffer_frame_count = 0;
uint32_t output_buffer_frame_count = 0;
std::unique_ptr<cubeb_resampler, decltype(&cubeb_resampler_destroy)> resampler = { nullptr, cubeb_resampler_destroy };
std::unique_ptr<cubeb_mixer, decltype(&cubeb_mixer_destroy)> mixer = { nullptr, cubeb_mixer_destroy };
std::vector<BYTE> mix_buffer;
std::unique_ptr<auto_array_wrapper> linear_input_buffer;
size_t bytes_per_sample = 0;
GUID waveformatextensible_sub_format = GUID_NULL;
float volume = 1.0;
bool draining = false;
std::atomic<std::atomic<bool>*> emergency_bailout;
};
class wasapi_endpoint_notification_client : public IMMNotificationClient
{
public:
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.");
if (flow != eRender && role != eConsole) {
return S_OK;
}
BOOL ok = SetEvent(reconfigure_event);
if (!ok) {
LOG("SetEvent on reconfigure_event failed: %lx", GetLastError());
}
return S_OK;
}
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:
LONG ref_count;
HANDLE reconfigure_event;
};
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;
}
#define MASK_DUAL_MONO (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT)
#define MASK_DUAL_MONO_LFE (MASK_DUAL_MONO | SPEAKER_LOW_FREQUENCY)
#define MASK_MONO (KSAUDIO_SPEAKER_MONO)
#define MASK_MONO_LFE (MASK_MONO | SPEAKER_LOW_FREQUENCY)
#define MASK_STEREO (KSAUDIO_SPEAKER_STEREO)
#define MASK_STEREO_LFE (MASK_STEREO | SPEAKER_LOW_FREQUENCY)
#define MASK_3F (MASK_STEREO | SPEAKER_FRONT_CENTER)
#define MASK_3F_LFE (MASK_3F | SPEAKER_LOW_FREQUENCY)
#define MASK_2F1 (MASK_STEREO | SPEAKER_BACK_CENTER)
#define MASK_2F1_LFE (MASK_2F1 | SPEAKER_LOW_FREQUENCY)
#define MASK_3F1 (KSAUDIO_SPEAKER_SURROUND)
#define MASK_3F1_LFE (MASK_3F1 | SPEAKER_LOW_FREQUENCY)
#define MASK_2F2 (MASK_STEREO | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT)
#define MASK_2F2_LFE (MASK_2F2 | SPEAKER_LOW_FREQUENCY)
#define MASK_3F2 (MASK_3F | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT)
#define MASK_3F2_LFE (KSAUDIO_SPEAKER_5POINT1_SURROUND)
#define MASK_3F3R_LFE (MASK_3F2_LFE | SPEAKER_BACK_CENTER)
#define MASK_3F4_LFE (KSAUDIO_SPEAKER_7POINT1_SURROUND)
static DWORD
channel_layout_to_mask(cubeb_channel_layout layout)
{
XASSERT(layout < CUBEB_LAYOUT_MAX && "invalid conversion.");
static DWORD map[CUBEB_LAYOUT_MAX] = {
KSAUDIO_SPEAKER_DIRECTOUT, MASK_DUAL_MONO, MASK_DUAL_MONO_LFE, MASK_MONO, MASK_MONO_LFE, MASK_STEREO, MASK_STEREO_LFE, MASK_3F, MASK_3F_LFE, MASK_2F1, MASK_2F1_LFE, MASK_3F1, MASK_3F1_LFE, MASK_2F2, MASK_2F2_LFE, MASK_3F2, MASK_3F2_LFE, MASK_3F3R_LFE, MASK_3F4_LFE, };
return map[layout];
}
cubeb_channel_layout
mask_to_channel_layout(WAVEFORMATEX const * fmt)
{
DWORD 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 = MASK_MONO;
} else if (fmt->nChannels == 2) {
mask = MASK_STEREO;
}
}
switch (mask) {
case MASK_MONO: return CUBEB_LAYOUT_MONO;
case MASK_MONO_LFE: return CUBEB_LAYOUT_MONO_LFE;
case MASK_STEREO: return CUBEB_LAYOUT_STEREO;
case MASK_STEREO_LFE: return CUBEB_LAYOUT_STEREO_LFE;
case MASK_3F: return CUBEB_LAYOUT_3F;
case MASK_3F_LFE: return CUBEB_LAYOUT_3F_LFE;
case MASK_2F1: return CUBEB_LAYOUT_2F1;
case MASK_2F1_LFE: return CUBEB_LAYOUT_2F1_LFE;
case MASK_3F1: return CUBEB_LAYOUT_3F1;
case MASK_3F1_LFE: return CUBEB_LAYOUT_3F1_LFE;
case MASK_2F2: return CUBEB_LAYOUT_2F2;
case KSAUDIO_SPEAKER_QUAD: return CUBEB_LAYOUT_2F2;
case MASK_2F2_LFE: return CUBEB_LAYOUT_2F2_LFE;
case MASK_3F2: return CUBEB_LAYOUT_3F2;
case MASK_3F2_LFE: return CUBEB_LAYOUT_3F2_LFE;
case KSAUDIO_SPEAKER_5POINT1: return CUBEB_LAYOUT_3F2_LFE;
case MASK_3F3R_LFE: return CUBEB_LAYOUT_3F3R_LFE;
case MASK_3F4_LFE: return CUBEB_LAYOUT_3F4_LFE;
default: return CUBEB_LAYOUT_UNDEFINED;
}
}
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 / 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(cubeb_stream * stm, uint32_t frames)
{
return std::ceil(frames * 10000000.0 / get_rate(stm));
}
static size_t
frames_to_bytes_before_mix(cubeb_stream * stm, size_t frames)
{
XASSERT(has_output(stm));
return stm->output_stream_params.channels * stm->bytes_per_sample * frames;
}
long
refill(cubeb_stream * stm, void * input_buffer, long input_frames_count,
void * output_buffer, long output_frames_needed)
{
XASSERT(!stm->draining);
void * dest = nullptr;
if (has_output(stm) && !stm->has_dummy_output) {
if (cubeb_should_mix(&stm->output_stream_params, &stm->output_mix_params)) {
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);
XASSERT(out_frames >= 0);
{
auto_lock lock(stm->stream_reset_lock);
stm->frames_written += out_frames;
}
if (out_frames < output_frames_needed) {
LOG("start draining.");
stm->draining = true;
}
XASSERT(out_frames == output_frames_needed || stm->draining || !has_output(stm) || stm->has_dummy_output);
if (!stm->has_dummy_output && has_output(stm) && cubeb_should_mix(&stm->output_stream_params, &stm->output_mix_params)) {
XASSERT(dest == stm->mix_buffer.data());
unsigned long dest_len = out_frames * stm->output_stream_params.channels;
XASSERT(dest_len <= stm->mix_buffer.size() / stm->bytes_per_sample);
unsigned long output_buffer_len = out_frames * stm->output_mix_params.channels;
cubeb_mixer_mix(stm->mixer.get(), out_frames,
dest, dest_len, output_buffer, output_buffer_len,
&stm->output_stream_params, &stm->output_mix_params);
}
return out_frames;
}
int wasapi_stream_reset_default_device(cubeb_stream * stm);
bool get_input_buffer(cubeb_stream * stm)
{
XASSERT(has_input(stm));
HRESULT hr;
BYTE * input_packet = NULL;
DWORD flags;
UINT64 dev_pos;
UINT32 next;
uint32_t offset = 0;
for (hr = stm->capture_client->GetNextPacketSize(&next);
next > 0;
hr = stm->capture_client->GetNextPacketSize(&next)) {
if (hr == AUDCLNT_E_DEVICE_INVALIDATED) {
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 packet_size;
hr = stm->capture_client->GetBuffer(&input_packet,
&packet_size,
&flags,
&dev_pos,
NULL);
if (FAILED(hr)) {
LOG("GetBuffer failed for capture: %lx", 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 (cubeb_should_mix(&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);
XASSERT(ok);
unsigned long input_packet_length = packet_size * stm->input_mix_params.channels;
unsigned long linear_input_buffer_length = packet_size * stm->input_stream_params.channels;
cubeb_mixer_mix(stm->mixer.get(), packet_size,
input_packet, input_packet_length,
stm->linear_input_buffer->end(), linear_input_buffer_length,
&stm->input_mix_params,
&stm->input_stream_params);
stm->linear_input_buffer->set_length(stm->linear_input_buffer->length() + linear_input_buffer_length);
} else {
stm->linear_input_buffer->push(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;
}
XASSERT(stm->linear_input_buffer->length() >= offset);
return true;
}
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) {
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;
}
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;
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;
}
if (output_frames == 0) {
return true;
}
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);
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) {
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((unsigned long) 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 (unsigned long) 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;
}
mmcss_handle = AvSetMmThreadCharacteristicsA("Audio", &mmcss_task_index);
if (!mmcss_handle) {
LOG("Unable to use mmcss to bump the render thread priority: %lx", GetLastError());
}
if (!emergency_bailout) {
is_playing = false;
}
unsigned timeout_count = 0;
const unsigned timeout_limit = 5;
while (is_playing) {
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: {
is_playing = false;
if (stm->draining) {
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
}
continue;
}
case WAIT_OBJECT_0 + 1: {
XASSERT(stm->output_client || stm->input_client);
LOG("Reconfiguring 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.");
int r = setup_wasapi_stream(stm);
if (r != CUBEB_OK) {
LOG("Error setting up the stream during reconfigure.");
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:
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:
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)
{
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;
}
stm->notification_client.reset(new wasapi_endpoint_notification_client(stm->reconfigure_event));
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)) {
stm->device_enumerator = nullptr;
return S_OK;
}
stm->notification_client = nullptr;
stm->device_enumerator = 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 get_default_endpoint(com_ptr<IMMDevice> & device, EDataFlow direction)
{
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, eConsole, 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 (!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 = 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 (FAILED(hr)) {
LOG("could not get the channel count: %lx", hr);
return CUBEB_ERROR;
}
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;
}
}
extern "C" {
int wasapi_init(cubeb ** context, char const * context_name)
{
HRESULT hr;
auto_com com;
if (!com.ok()) {
return CUBEB_ERROR;
}
com_ptr<IMMDevice> device;
hr = get_default_endpoint(device, eRender);
if (FAILED(hr)) {
LOG("Could not get 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;
}
*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 (!stm->emergency_bailout.load()) {
return false;
}
BOOL ok = SetEvent(stm->shutdown_event);
if (!ok) {
LOG("Destroy SetEvent failed: %lx", GetLastError());
}
DWORD r = WaitForSingleObject(stm->thread, 5000);
if (r == WAIT_TIMEOUT) {
*(stm->emergency_bailout) = true;
stm->emergency_bailout = nullptr;
LOG("Destroy WaitForSingleObject on thread timed out,"
" leaking the thread: %lx", GetLastError());
rv = false;
}
if (r == WAIT_FAILED) {
*(stm->emergency_bailout) = true;
stm->emergency_bailout = nullptr;
LOG("Destroy WaitForSingleObject on thread failed: %lx", GetLastError());
rv = false;
}
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)
{
HRESULT hr;
auto_com com;
if (!com.ok()) {
return CUBEB_ERROR;
}
XASSERT(ctx && max_channels);
com_ptr<IMMDevice> device;
hr = get_default_endpoint(device, eRender);
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)
{
HRESULT hr;
REFERENCE_TIME default_period;
auto_com com;
if (!com.ok()) {
return CUBEB_ERROR;
}
if (params.format != CUBEB_SAMPLE_FLOAT32NE && params.format != CUBEB_SAMPLE_S16NE) {
return CUBEB_ERROR_INVALID_FORMAT;
}
com_ptr<IMMDevice> device;
hr = get_default_endpoint(device, eRender);
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;
}
hr = client->GetDevicePeriod(&default_period, NULL);
if (FAILED(hr)) {
LOG("Could not get device period: %lx", hr);
return CUBEB_ERROR;
}
LOG("default device period: %I64d", default_period);
*latency_frames = hns_to_frames(params.rate, default_period);
LOG("Minimum latency in frames: %u", *latency_frames);
return CUBEB_OK;
}
int
wasapi_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate)
{
HRESULT hr;
auto_com com;
if (!com.ok()) {
return CUBEB_ERROR;
}
com_ptr<IMMDevice> device;
hr = get_default_endpoint(device, eRender);
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;
}
int
wasapi_get_preferred_channel_layout(cubeb * context, cubeb_channel_layout * layout)
{
HRESULT hr;
auto_com com;
if (!com.ok()) {
return CUBEB_ERROR;
}
com_ptr<IMMDevice> device;
hr = get_default_endpoint(device, eRender);
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);
*layout = mask_to_channel_layout(mix_format.get());
LOG("Preferred channel layout: %s", CUBEB_CHANNEL_LAYOUT_MAPS[*layout].name);
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;
}
}
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);
if (mix_format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) {
return;
}
WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.get());
WAVEFORMATEXTENSIBLE hw_mix_format = *format_pcm;
format_pcm->dwChannelMask = channel_layout_to_mask(stream_params->layout);
mix_format->nChannels = stream_params->channels;
waveformatex_update_derived_properties(mix_format.get());
WAVEFORMATEX * closest;
HRESULT hr = audio_client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED,
mix_format.get(),
&closest);
if (hr == S_FALSE) {
LOG("Using WASAPI suggested format: channels: %d", closest->nChannels);
XASSERT(closest->wFormatTag == WAVE_FORMAT_EXTENSIBLE);
WAVEFORMATEXTENSIBLE * closest_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(closest);
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) {
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);
}
}
#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;
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 {
hr = get_default_endpoint(device, is_loopback ? eRender : direction);
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;
}
}
hr = device->Activate(__uuidof(IAudioClient),
CLSCTX_INPROC_SERVER,
NULL, audio_client.receive_vpp());
if (FAILED(hr)) {
LOG("Could not activate the device to get an audio"
" client for %s: error: %lx\n", DIRECTION_NAME, hr);
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);
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_EXTENSIBLE) {
WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.get());
format_pcm->SubFormat = stm->waveformatextensible_sub_format;
}
waveformatex_update_derived_properties(mix_format.get());
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());
if (mix_params->layout == CUBEB_LAYOUT_UNDEFINED) {
LOG("Stream using undefined layout! Any mixing may be unpredictable!\n");
} else if (mix_format->nChannels != CUBEB_CHANNEL_LAYOUT_MAPS[mix_params->layout].channels) {
LOG("Channel count is different from the layout standard!\n");
}
LOG("Setup requested=[f=%d r=%u c=%u l=%s] mix=[f=%d r=%u c=%u l=%s]",
stream_params->format, stream_params->rate, stream_params->channels,
CUBEB_CHANNEL_LAYOUT_MAPS[stream_params->layout].name,
mix_params->format, mix_params->rate, mix_params->channels,
CUBEB_CHANNEL_LAYOUT_MAPS[mix_params->layout].name);
DWORD flags = AUDCLNT_STREAMFLAGS_NOPERSIST;
if (is_loopback) {
flags |= AUDCLNT_STREAMFLAGS_LOOPBACK;
} else {
flags |= AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
}
hr = audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED,
flags,
frames_to_hns(stm, stm->latency),
0,
mix_format.get(),
NULL);
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;
}
if (has_output(stm) && cubeb_should_mix(stream_params, mix_params)) {
stm->mix_buffer.resize(frames_to_bytes_before_mix(stm, *buffer_frame_count));
}
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
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("(%p) Setup capture: device=%p", stm, stm->input_device.get());
rv = setup_wasapi_stream_one_side(stm,
&stm->input_stream_params,
stm->input_device.get(),
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 !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);
}
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) {
size_t len = wcslen(stm->input_device.get());
std::unique_ptr<wchar_t[]> tmp(new wchar_t[len + 1]);
if (wcsncpy_s(tmp.get(), len + 1, stm->input_device.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 = move(tmp);
}
stm->has_dummy_output = true;
}
if (has_output(stm)) {
LOG("(%p) Setup render: device=%p", stm, stm->output_device.get());
rv = setup_wasapi_stream_one_side(stm,
&stm->output_stream_params,
stm->output_device.get(),
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),
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;
}
if (stream_set_volume(stm, stm->volume) != CUBEB_OK) {
LOG("Could not set the volume.");
return CUBEB_ERROR;
}
}
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);
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,
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 && 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;
if (input_stream_params) {
stm->input_stream_params = *input_stream_params;
stm->input_device = utf8_to_wstr(reinterpret_cast<char const *>(input_device));
XASSERT(stm->input_stream_params.layout == CUBEB_LAYOUT_UNDEFINED ||
stm->input_stream_params.channels == CUBEB_CHANNEL_LAYOUT_MAPS[stm->input_stream_params.layout].channels);
}
if (output_stream_params) {
stm->output_stream_params = *output_stream_params;
stm->output_device = utf8_to_wstr(reinterpret_cast<char const *>(output_device));
XASSERT(stm->output_stream_params.layout == CUBEB_LAYOUT_UNDEFINED ||
stm->output_stream_params.channels == CUBEB_CHANNEL_LAYOUT_MAPS[stm->output_stream_params.layout].channels);
}
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->mixer.reset(cubeb_mixer_create(output_stream_params ? output_stream_params->format :
input_stream_params->format,
CUBEB_MIXER_DIRECTION_DOWNMIX | CUBEB_MIXER_DIRECTION_UPMIX));
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;
}
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;
}
{
auto_lock lock(stm->stream_reset_lock);
rv = setup_wasapi_stream(stm.get());
}
if (rv != CUBEB_OK) {
return rv;
}
hr = register_notification_client(stm.get());
if (FAILED(hr)) {
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->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->mix_buffer.clear();
}
void wasapi_stream_destroy(cubeb_stream * stm)
{
XASSERT(stm);
LOG("Stream destroy (%p)", stm);
if (stop_and_join_render_thread(stm)) {
delete stm->emergency_bailout.load();
stm->emergency_bailout = nullptr;
}
unregister_notification_client(stm);
CloseHandle(stm->reconfigure_event);
CloseHandle(stm->refill_event);
CloseHandle(stm->input_available_event);
stm->mixer.reset();
stm->linear_input_buffer.reset();
{
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;
}
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;
}
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)) {
if (stm->emergency_bailout.load()) {
delete stm->emergency_bailout.load();
stm->emergency_bailout = nullptr;
}
} else {
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;
}
uint64_t stream_delay = static_cast<uint64_t>(current_stream_delay(stm) * stm->output_stream_params.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);
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 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 std::move(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;
}
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;
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)) return CUBEB_ERROR;
hr = endpoint->GetDataFlow(&flow);
if (FAILED(hr)) return CUBEB_ERROR;
wchar_t * tmp = nullptr;
hr = dev->GetId(&tmp);
if (FAILED(hr)) 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) {
return CUBEB_ERROR;
}
hr = dev->OpenPropertyStore(STGM_READ, propstore.receive());
if (FAILED(hr)) return CUBEB_ERROR;
hr = dev->GetState(&state);
if (FAILED(hr)) 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))
ret.friendly_name = wstr_to_utf8(namevar.pwszVal);
devnode = wasapi_get_device_node(enumerator, dev);
if (devnode) {
com_ptr<IPropertyStore> ps;
hr = devnode->OpenPropertyStore(STGM_READ, ps.receive());
if (FAILED(hr)) return CUBEB_ERROR;
prop_variant instancevar;
hr = ps->GetValue(PKEY_Device_InstanceId, &instancevar);
if (SUCCEEDED(hr)) {
ret.group_id = wstr_to_utf8(instancevar.pwszVal);
}
}
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;
}
return CUBEB_OK;
}
static int
wasapi_enumerate_devices(cubeb * context, cubeb_device_type type,
cubeb_device_collection * out)
{
auto_com com;
com_ptr<IMMDeviceEnumerator> enumerator;
com_ptr<IMMDeviceCollection> collection;
HRESULT hr;
UINT cc, i;
EDataFlow flow;
if (!com.ok())
return CUBEB_ERROR;
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 * , cubeb_device_collection * collection)
{
XASSERT(collection);
for (size_t n = 0; n < collection->count; n++) {
cubeb_device_info& dev = collection->device[n];
delete [] dev.friendly_name;
delete [] dev.group_id;
}
delete [] collection->device;
return CUBEB_OK;
}
cubeb_ops const wasapi_ops = {
wasapi_init,
wasapi_get_backend_id,
wasapi_get_max_channel_count,
wasapi_get_min_latency,
wasapi_get_preferred_sample_rate,
wasapi_get_preferred_channel_layout,
wasapi_enumerate_devices,
wasapi_device_collection_destroy,
wasapi_destroy,
wasapi_stream_init,
wasapi_stream_destroy,
wasapi_stream_start,
wasapi_stream_stop,
wasapi_stream_reset_default_device,
wasapi_stream_get_position,
wasapi_stream_get_latency,
wasapi_stream_set_volume,
NULL,
NULL,
NULL,
NULL,
NULL
};
}