mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
Bug 1764574 - Update libcubeb to 708f52c. r=cubeb-reviewers,chunmin
Depends on D143612 Differential Revision: https://phabricator.services.mozilla.com/D143613
This commit is contained in:
parent
70126304ab
commit
a5b1ff084e
@ -2,6 +2,6 @@
|
||||
|
||||
See INSTALL.md for build instructions.
|
||||
|
||||
See [Backend Support](https://github.com/kinetiknz/cubeb/wiki/Backend-Support) in the wiki for the support level of each backend.
|
||||
See [Backend Support](https://github.com/mozilla/cubeb/wiki/Backend-Support) in the wiki for the support level of each backend.
|
||||
|
||||
Licensed under an ISC-style license. See LICENSE for details.
|
||||
|
@ -9,8 +9,8 @@ origin:
|
||||
description: "Cross platform audio library"
|
||||
url: https://github.com/mozilla/cubeb
|
||||
license: ISC
|
||||
release: commit b62d61bc661b49c7a7f5d97f4657189c630ac7a5 (2022-03-30T05:12:01Z).
|
||||
revision: b62d61bc661b49c7a7f5d97f4657189c630ac7a5
|
||||
release: commit 708f52cccffe69ed1d65b52903237c990db860a9 (2022-04-13T15:02:09Z).
|
||||
revision: 708f52cccffe69ed1d65b52903237c990db860a9
|
||||
|
||||
vendoring:
|
||||
url: https://github.com/mozilla/cubeb
|
||||
|
@ -240,8 +240,9 @@ wasapi_create_device(cubeb * ctx, cubeb_device_info & ret,
|
||||
void
|
||||
wasapi_destroy_device(cubeb_device_info * device_info);
|
||||
static int
|
||||
wasapi_enumerate_devices(cubeb * context, cubeb_device_type type,
|
||||
cubeb_device_collection * out);
|
||||
wasapi_enumerate_devices_internal(cubeb * context, cubeb_device_type type,
|
||||
cubeb_device_collection * out,
|
||||
DWORD state_mask);
|
||||
static int
|
||||
wasapi_device_collection_destroy(cubeb * ctx,
|
||||
cubeb_device_collection * collection);
|
||||
@ -409,12 +410,9 @@ struct cubeb_stream {
|
||||
float volume = 1.0;
|
||||
/* True if the stream is draining. */
|
||||
bool draining = false;
|
||||
/* True when we've destroyed the stream. This pointer is leaked on stream
|
||||
* destruction if we could not join the thread. */
|
||||
std::atomic<std::atomic<bool> *> emergency_bailout{nullptr};
|
||||
/* Synchronizes render thread start to ensure safe access to
|
||||
* emergency_bailout. */
|
||||
HANDLE thread_ready_event = 0;
|
||||
/* If the render thread fails to stop, this is set to true and ownership of
|
||||
* the stm is "leaked" to the render thread for later cleanup. */
|
||||
std::atomic<bool> emergency_bailout{false};
|
||||
/* This needs an active audio input stream to be known, and is updated in the
|
||||
* first audio input callback. */
|
||||
std::atomic<int64_t> input_latency_hns{LATENCY_NOT_AVAILABLE_YET};
|
||||
@ -753,6 +751,27 @@ private:
|
||||
|
||||
namespace {
|
||||
|
||||
long
|
||||
wasapi_data_callback(cubeb_stream * stm, void * user_ptr,
|
||||
void const * input_buffer, void * output_buffer,
|
||||
long nframes)
|
||||
{
|
||||
if (stm->emergency_bailout) {
|
||||
return CUBEB_ERROR;
|
||||
}
|
||||
return stm->data_callback(stm, user_ptr, input_buffer, output_buffer,
|
||||
nframes);
|
||||
}
|
||||
|
||||
void
|
||||
wasapi_state_callback(cubeb_stream * stm, void * user_ptr, cubeb_state state)
|
||||
{
|
||||
if (stm->emergency_bailout) {
|
||||
return;
|
||||
}
|
||||
return stm->state_callback(stm, user_ptr, state);
|
||||
}
|
||||
|
||||
char const *
|
||||
intern_device_id(cubeb * ctx, wchar_t const * id)
|
||||
{
|
||||
@ -875,7 +894,7 @@ refill(cubeb_stream * stm, void * input_buffer, long input_frames_count,
|
||||
&input_frames_count, dest, output_frames_needed);
|
||||
if (out_frames < 0) {
|
||||
ALOGV("Callback refill error: %d", out_frames);
|
||||
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
|
||||
wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
|
||||
return out_frames;
|
||||
}
|
||||
|
||||
@ -994,7 +1013,7 @@ get_input_buffer(cubeb_stream * stm)
|
||||
(stm->input_stream_params.prefs &
|
||||
CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) ||
|
||||
!trigger_async_reconfigure(stm)) {
|
||||
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
|
||||
wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -1108,7 +1127,7 @@ get_output_buffer(cubeb_stream * stm, void *& buffer, size_t & frame_count)
|
||||
(stm->output_stream_params.prefs &
|
||||
CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING) ||
|
||||
!trigger_async_reconfigure(stm)) {
|
||||
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
|
||||
wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -1124,7 +1143,7 @@ get_output_buffer(cubeb_stream * stm, void *& buffer, size_t & frame_count)
|
||||
if (stm->draining) {
|
||||
if (padding_out == 0) {
|
||||
LOG("Draining finished.");
|
||||
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
|
||||
wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
|
||||
return false;
|
||||
}
|
||||
LOG("Draining.");
|
||||
@ -1300,17 +1319,25 @@ refill_callback_output(cubeb_stream * stm)
|
||||
return size_t(got) == output_frames || stm->draining;
|
||||
}
|
||||
|
||||
void
|
||||
wasapi_stream_destroy(cubeb_stream * stm);
|
||||
|
||||
static void
|
||||
handle_emergency_bailout(cubeb_stream * stm)
|
||||
{
|
||||
if (stm->emergency_bailout) {
|
||||
CloseHandle(stm->thread);
|
||||
stm->thread = NULL;
|
||||
CloseHandle(stm->shutdown_event);
|
||||
stm->shutdown_event = 0;
|
||||
wasapi_stream_destroy(stm);
|
||||
_endthreadex(0);
|
||||
}
|
||||
}
|
||||
|
||||
static unsigned int __stdcall wasapi_stream_render_loop(LPVOID stream)
|
||||
{
|
||||
cubeb_stream * stm = static_cast<cubeb_stream *>(stream);
|
||||
std::atomic<bool> * emergency_bailout = stm->emergency_bailout;
|
||||
|
||||
// Signal wasapi_stream_start that we've copied emergency_bailout.
|
||||
BOOL ok = SetEvent(stm->thread_ready_event);
|
||||
if (!ok) {
|
||||
LOG("thread_ready SetEvent failed: %lx", GetLastError());
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool is_playing = true;
|
||||
HANDLE wait_array[4] = {stm->shutdown_event, stm->reconfigure_event,
|
||||
@ -1343,20 +1370,10 @@ static unsigned int __stdcall wasapi_stream_render_loop(LPVOID stream)
|
||||
unsigned timeout_count = 0;
|
||||
const unsigned timeout_limit = 3;
|
||||
while (is_playing) {
|
||||
// We want to check the emergency bailout variable before a
|
||||
// and after the WaitForMultipleObject, because the handles
|
||||
// WaitForMultipleObjects is going to wait on might have been closed
|
||||
// already.
|
||||
if (*emergency_bailout) {
|
||||
delete emergency_bailout;
|
||||
return 0;
|
||||
}
|
||||
handle_emergency_bailout(stm);
|
||||
DWORD waitResult = WaitForMultipleObjects(ARRAY_LENGTH(wait_array),
|
||||
wait_array, FALSE, 1000);
|
||||
if (*emergency_bailout) {
|
||||
delete emergency_bailout;
|
||||
return 0;
|
||||
}
|
||||
handle_emergency_bailout(stm);
|
||||
if (waitResult != WAIT_TIMEOUT) {
|
||||
timeout_count = 0;
|
||||
}
|
||||
@ -1366,7 +1383,7 @@ static unsigned int __stdcall wasapi_stream_render_loop(LPVOID stream)
|
||||
/* 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);
|
||||
wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@ -1428,7 +1445,8 @@ static unsigned int __stdcall wasapi_stream_render_loop(LPVOID stream)
|
||||
case WAIT_OBJECT_0 + 3: { /* input available */
|
||||
HRESULT rv = get_input_buffer(stm);
|
||||
if (FAILED(rv)) {
|
||||
return rv;
|
||||
is_playing = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!has_output(stm)) {
|
||||
@ -1447,18 +1465,29 @@ static unsigned int __stdcall wasapi_stream_render_loop(LPVOID stream)
|
||||
break;
|
||||
default:
|
||||
LOG("case %lu not handled in render loop.", waitResult);
|
||||
abort();
|
||||
XASSERT(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (FAILED(hr)) {
|
||||
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
|
||||
// Stop audio clients since this thread will no longer service
|
||||
// the events.
|
||||
if (stm->output_client) {
|
||||
stm->output_client->Stop();
|
||||
}
|
||||
if (stm->input_client) {
|
||||
stm->input_client->Stop();
|
||||
}
|
||||
|
||||
if (mmcss_handle) {
|
||||
AvRevertMmThreadCharacteristics(mmcss_handle);
|
||||
}
|
||||
|
||||
handle_emergency_bailout(stm);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -1707,54 +1736,58 @@ wasapi_init(cubeb ** context, char const * context_name)
|
||||
}
|
||||
|
||||
namespace {
|
||||
enum ShutdownPhase { OnStop, OnDestroy };
|
||||
|
||||
bool
|
||||
stop_and_join_render_thread(cubeb_stream * stm)
|
||||
stop_and_join_render_thread(cubeb_stream * stm, ShutdownPhase phase)
|
||||
{
|
||||
bool rv = true;
|
||||
LOG("Stop and join render thread.");
|
||||
// Only safe to transfer `stm` ownership to the render thread when
|
||||
// the stream is being destroyed by the caller.
|
||||
bool bailout = phase == OnDestroy;
|
||||
|
||||
LOG("%p: Stop and join render thread: %p (%d), phase=%d", stm, stm->thread,
|
||||
stm->emergency_bailout.load(), static_cast<int>(phase));
|
||||
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;
|
||||
}
|
||||
XASSERT(!stm->emergency_bailout);
|
||||
|
||||
BOOL ok = SetEvent(stm->shutdown_event);
|
||||
if (!ok) {
|
||||
LOG("Destroy SetEvent failed: %lx", GetLastError());
|
||||
LOG("stop_and_join_render_thread: SetEvent failed: %lx", GetLastError());
|
||||
stm->emergency_bailout = bailout;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
* check its event loop very often, five seconds is rather conservative.
|
||||
* Note: 5*1s loop to work around timer sleep issues on pre-Windows 8. */
|
||||
DWORD r;
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
r = WaitForSingleObject(stm->thread, 1000);
|
||||
if (r == WAIT_OBJECT_0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (r != WAIT_OBJECT_0) {
|
||||
/* Something weird happened, leak the thread and continue the shutdown
|
||||
* process. */
|
||||
*(stm->emergency_bailout) = true;
|
||||
// We give the ownership to the rendering thread.
|
||||
stm->emergency_bailout = nullptr;
|
||||
LOG("Destroy WaitForSingleObject on thread failed: %lx, %lx", r,
|
||||
GetLastError());
|
||||
rv = false;
|
||||
LOG("stop_and_join_render_thread: WaitForSingleObject on thread failed: "
|
||||
"%lx, %lx",
|
||||
r, GetLastError());
|
||||
stm->emergency_bailout = bailout;
|
||||
return 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;
|
||||
// Only attempt to close and null out the thread and event if the
|
||||
// WaitForSingleObject above succeeded.
|
||||
LOG("stop_and_join_render_thread: Closing thread.");
|
||||
CloseHandle(stm->thread);
|
||||
stm->thread = NULL;
|
||||
|
||||
CloseHandle(stm->shutdown_event);
|
||||
stm->shutdown_event = 0;
|
||||
}
|
||||
CloseHandle(stm->shutdown_event);
|
||||
stm->shutdown_event = 0;
|
||||
|
||||
return rv;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
@ -1892,9 +1925,6 @@ wasapi_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate)
|
||||
return CUBEB_OK;
|
||||
}
|
||||
|
||||
void
|
||||
wasapi_stream_destroy(cubeb_stream * stm);
|
||||
|
||||
static void
|
||||
waveformatex_update_derived_properties(WAVEFORMATEX * format)
|
||||
{
|
||||
@ -2270,8 +2300,12 @@ setup_wasapi_stream_one_side(cubeb_stream * stm,
|
||||
if (wasapi_create_device(stm->context, device_info,
|
||||
stm->device_enumerator.get(), device.get(),
|
||||
&default_devices) == CUBEB_OK) {
|
||||
if (device_info.latency_hi == 0) {
|
||||
LOG("Input: could not query latency_hi to guess safe latency");
|
||||
wasapi_destroy_device(&device_info);
|
||||
return CUBEB_ERROR;
|
||||
}
|
||||
// This multiplicator has been found empirically.
|
||||
XASSERT(device_info.latency_hi > 0);
|
||||
uint32_t latency_frames = device_info.latency_hi * 8;
|
||||
LOG("Input: latency increased to %u frames from a default of %u",
|
||||
latency_frames, device_info.latency_hi);
|
||||
@ -2373,10 +2407,10 @@ wasapi_find_bt_handsfree_output_device(cubeb_stream * stm)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int rv = wasapi_enumerate_devices(
|
||||
int rv = wasapi_enumerate_devices_internal(
|
||||
stm->context,
|
||||
(cubeb_device_type)(CUBEB_DEVICE_TYPE_INPUT | CUBEB_DEVICE_TYPE_OUTPUT),
|
||||
&collection);
|
||||
&collection, DEVICE_STATE_ACTIVE);
|
||||
if (rv != CUBEB_OK) {
|
||||
return nullptr;
|
||||
}
|
||||
@ -2572,7 +2606,7 @@ setup_wasapi_stream(cubeb_stream * stm)
|
||||
stm->resampler.reset(cubeb_resampler_create(
|
||||
stm, has_input(stm) ? &input_params : nullptr,
|
||||
has_output(stm) && !stm->has_dummy_output ? &output_params : nullptr,
|
||||
target_sample_rate, stm->data_callback, stm->user_ptr,
|
||||
target_sample_rate, wasapi_data_callback, stm->user_ptr,
|
||||
stm->voice ? CUBEB_RESAMPLER_QUALITY_VOIP
|
||||
: CUBEB_RESAMPLER_QUALITY_DESKTOP,
|
||||
CUBEB_RESAMPLER_RECLOCK_NONE));
|
||||
@ -2805,33 +2839,25 @@ wasapi_stream_destroy(cubeb_stream * stm)
|
||||
XASSERT(stm);
|
||||
LOG("Stream destroy (%p)", stm);
|
||||
|
||||
// Only free stm->emergency_bailout if we could join the thread.
|
||||
// If we could not join the thread, stm->emergency_bailout is true
|
||||
// and is still alive until the thread wakes up and exits cleanly.
|
||||
if (stop_and_join_render_thread(stm)) {
|
||||
delete stm->emergency_bailout.load();
|
||||
stm->emergency_bailout = nullptr;
|
||||
if (!stop_and_join_render_thread(stm, OnDestroy)) {
|
||||
// Emergency bailout: render thread becomes responsible for calling
|
||||
// wasapi_stream_destroy.
|
||||
return;
|
||||
}
|
||||
|
||||
if (stm->notification_client) {
|
||||
unregister_notification_client(stm);
|
||||
}
|
||||
|
||||
CloseHandle(stm->reconfigure_event);
|
||||
CloseHandle(stm->refill_event);
|
||||
CloseHandle(stm->input_available_event);
|
||||
|
||||
// The variables intialized in wasapi_stream_init,
|
||||
// must be destroyed in wasapi_stream_destroy.
|
||||
stm->linear_input_buffer.reset();
|
||||
|
||||
stm->device_enumerator = nullptr;
|
||||
|
||||
{
|
||||
auto_lock lock(stm->stream_reset_lock);
|
||||
close_wasapi_stream(stm);
|
||||
}
|
||||
|
||||
CloseHandle(stm->reconfigure_event);
|
||||
CloseHandle(stm->refill_event);
|
||||
CloseHandle(stm->input_available_event);
|
||||
|
||||
delete stm;
|
||||
}
|
||||
|
||||
@ -2886,8 +2912,6 @@ wasapi_stream_start(cubeb_stream * stm)
|
||||
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) {
|
||||
@ -2908,30 +2932,18 @@ wasapi_stream_start(cubeb_stream * stm)
|
||||
return CUBEB_ERROR;
|
||||
}
|
||||
|
||||
stm->thread_ready_event = CreateEvent(NULL, 0, 0, NULL);
|
||||
if (!stm->thread_ready_event) {
|
||||
LOG("Can't create the thread_ready event, error: %lx", GetLastError());
|
||||
return CUBEB_ERROR;
|
||||
}
|
||||
|
||||
cubeb_async_log_reset_threads();
|
||||
stm->thread =
|
||||
(HANDLE)_beginthreadex(NULL, 512 * 1024, wasapi_stream_render_loop, stm,
|
||||
STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
|
||||
if (stm->thread == NULL) {
|
||||
LOG("could not create WASAPI render thread.");
|
||||
CloseHandle(stm->shutdown_event);
|
||||
stm->shutdown_event = 0;
|
||||
return CUBEB_ERROR;
|
||||
}
|
||||
|
||||
// Wait for wasapi_stream_render_loop to signal that emergency_bailout has
|
||||
// been read, avoiding a bailout situation where we could free `stm`
|
||||
// before wasapi_stream_render_loop had a chance to run.
|
||||
HRESULT hr = WaitForSingleObject(stm->thread_ready_event, INFINITE);
|
||||
XASSERT(hr == WAIT_OBJECT_0);
|
||||
CloseHandle(stm->thread_ready_event);
|
||||
stm->thread_ready_event = 0;
|
||||
|
||||
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED);
|
||||
wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED);
|
||||
|
||||
return CUBEB_OK;
|
||||
}
|
||||
@ -2961,15 +2973,12 @@ wasapi_stream_stop(cubeb_stream * stm)
|
||||
}
|
||||
}
|
||||
|
||||
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED);
|
||||
wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED);
|
||||
}
|
||||
|
||||
if (stop_and_join_render_thread(stm)) {
|
||||
delete stm->emergency_bailout.load();
|
||||
stm->emergency_bailout = nullptr;
|
||||
} else {
|
||||
if (!stop_and_join_render_thread(stm, OnStop)) {
|
||||
// If we could not join the thread, put the stream in error.
|
||||
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
|
||||
wasapi_state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
|
||||
return CUBEB_ERROR;
|
||||
}
|
||||
|
||||
@ -3335,8 +3344,9 @@ wasapi_destroy_device(cubeb_device_info * device)
|
||||
}
|
||||
|
||||
static int
|
||||
wasapi_enumerate_devices(cubeb * context, cubeb_device_type type,
|
||||
cubeb_device_collection * out)
|
||||
wasapi_enumerate_devices_internal(cubeb * context, cubeb_device_type type,
|
||||
cubeb_device_collection * out,
|
||||
DWORD state_mask)
|
||||
{
|
||||
com_ptr<IMMDeviceEnumerator> enumerator;
|
||||
com_ptr<IMMDeviceCollection> collection;
|
||||
@ -3364,8 +3374,7 @@ wasapi_enumerate_devices(cubeb * context, cubeb_device_type type,
|
||||
return CUBEB_ERROR;
|
||||
}
|
||||
|
||||
hr = enumerator->EnumAudioEndpoints(flow, DEVICE_STATEMASK_ALL,
|
||||
collection.receive());
|
||||
hr = enumerator->EnumAudioEndpoints(flow, state_mask, collection.receive());
|
||||
if (FAILED(hr)) {
|
||||
LOG("Could not enumerate audio endpoints: %lx", hr);
|
||||
return CUBEB_ERROR;
|
||||
@ -3399,6 +3408,15 @@ wasapi_enumerate_devices(cubeb * context, cubeb_device_type type,
|
||||
return CUBEB_OK;
|
||||
}
|
||||
|
||||
static int
|
||||
wasapi_enumerate_devices(cubeb * context, cubeb_device_type type,
|
||||
cubeb_device_collection * out)
|
||||
{
|
||||
return wasapi_enumerate_devices_internal(
|
||||
context, type, out,
|
||||
DEVICE_STATE_ACTIVE | DEVICE_STATE_DISABLED | DEVICE_STATE_UNPLUGGED);
|
||||
}
|
||||
|
||||
static int
|
||||
wasapi_device_collection_destroy(cubeb * /*ctx*/,
|
||||
cubeb_device_collection * collection)
|
||||
|
Loading…
Reference in New Issue
Block a user