gstreamer/subprojects/gst-plugins-bad/sys/wasapi2/gstwasapi2ringbuffer.cpp
Seungha Yang cb7958e710 wasapi2: Add support for process loopback capture
Adding loopback capture mode for specified PID.

Note that this feature requires Windows 10 build 20348
(Windows 11/Windows Server 2022 or later),
and any process loopback related properties will not be exposed
if OS does not support it.

Example launch lines:
* wasapi2src loopback-mode=include-process-tree loopback-target-pid=<PID>
 Captures audio generated by an application (specified by PID)
 and its child process
* wasapi2src loopback-mode=exclude-process-tree loopback-target-pid=<PID>
 Captures desktop audio excluding PID and its child process

Fixes: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/1278
Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3195>
2022-10-17 23:28:48 +00:00

1475 lines
42 KiB
C++

/* GStreamer
* Copyright (C) 2021 Seungha Yang <seungha@centricular.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "gstwasapi2ringbuffer.h"
#include <string.h>
#include <mfapi.h>
#include <wrl.h>
GST_DEBUG_CATEGORY_STATIC (gst_wasapi2_ring_buffer_debug);
#define GST_CAT_DEFAULT gst_wasapi2_ring_buffer_debug
static HRESULT gst_wasapi2_ring_buffer_io_callback (GstWasapi2RingBuffer * buf);
static HRESULT
gst_wasapi2_ring_buffer_loopback_callback (GstWasapi2RingBuffer * buf);
/* *INDENT-OFF* */
using namespace Microsoft::WRL;
class GstWasapiAsyncCallback : public IMFAsyncCallback
{
public:
GstWasapiAsyncCallback(GstWasapi2RingBuffer *listener,
DWORD queue_id,
gboolean loopback)
: ref_count_(1)
, queue_id_(queue_id)
, loopback_(loopback)
{
g_weak_ref_init (&listener_, listener);
}
virtual ~GstWasapiAsyncCallback ()
{
g_weak_ref_set (&listener_, nullptr);
}
/* IUnknown */
STDMETHODIMP_ (ULONG)
AddRef (void)
{
GST_TRACE ("%p, %d", this, ref_count_);
return InterlockedIncrement (&ref_count_);
}
STDMETHODIMP_ (ULONG)
Release (void)
{
ULONG ref_count;
GST_TRACE ("%p, %d", this, ref_count_);
ref_count = InterlockedDecrement (&ref_count_);
if (ref_count == 0) {
GST_TRACE ("Delete instance %p", this);
delete this;
}
return ref_count;
}
STDMETHODIMP
QueryInterface (REFIID riid, void ** object)
{
if (!object)
return E_POINTER;
if (riid == IID_IUnknown) {
GST_TRACE ("query IUnknown interface %p", this);
*object = static_cast<IUnknown *> (static_cast<GstWasapiAsyncCallback *> (this));
} else if (riid == __uuidof (IMFAsyncCallback)) {
GST_TRACE ("query IUnknown interface %p", this);
*object = static_cast<IUnknown *> (static_cast<GstWasapiAsyncCallback *> (this));
} else {
*object = nullptr;
return E_NOINTERFACE;
}
AddRef ();
return S_OK;
}
/* IMFAsyncCallback */
STDMETHODIMP
GetParameters(DWORD * pdwFlags, DWORD * pdwQueue)
{
*pdwFlags = 0;
*pdwQueue = queue_id_;
return S_OK;
}
STDMETHODIMP
Invoke(IMFAsyncResult * pAsyncResult)
{
GstWasapi2RingBuffer *ringbuffer;
HRESULT hr;
ringbuffer = (GstWasapi2RingBuffer *) g_weak_ref_get (&listener_);
if (!ringbuffer) {
GST_WARNING ("Listener was removed");
return S_OK;
}
if (loopback_)
hr = gst_wasapi2_ring_buffer_loopback_callback (ringbuffer);
else
hr = gst_wasapi2_ring_buffer_io_callback (ringbuffer);
gst_object_unref (ringbuffer);
return hr;
}
private:
ULONG ref_count_;
DWORD queue_id_;
GWeakRef listener_;
gboolean loopback_;
};
/* *INDENT-ON* */
struct _GstWasapi2RingBuffer
{
GstAudioRingBuffer parent;
GstWasapi2ClientDeviceClass device_class;
gchar *device_id;
gboolean low_latency;
gboolean mute;
gdouble volume;
gpointer dispatcher;
gboolean can_auto_routing;
guint loopback_target_pid;
GstWasapi2Client *client;
GstWasapi2Client *loopback_client;
IAudioCaptureClient *capture_client;
IAudioRenderClient *render_client;
ISimpleAudioVolume *volume_object;
GstWasapiAsyncCallback *callback_object;
IMFAsyncResult *callback_result;
MFWORKITEM_KEY callback_key;
HANDLE event_handle;
GstWasapiAsyncCallback *loopback_callback_object;
IMFAsyncResult *loopback_callback_result;
MFWORKITEM_KEY loopback_callback_key;
HANDLE loopback_event_handle;
guint64 expected_position;
gboolean is_first;
gboolean running;
UINT32 buffer_size;
UINT32 loopback_buffer_size;
gint segoffset;
guint64 write_frame_offset;
GMutex volume_lock;
gboolean mute_changed;
gboolean volume_changed;
GstCaps *supported_caps;
};
static void gst_wasapi2_ring_buffer_constructed (GObject * object);
static void gst_wasapi2_ring_buffer_dispose (GObject * object);
static void gst_wasapi2_ring_buffer_finalize (GObject * object);
static gboolean gst_wasapi2_ring_buffer_open_device (GstAudioRingBuffer * buf);
static gboolean gst_wasapi2_ring_buffer_close_device (GstAudioRingBuffer * buf);
static gboolean gst_wasapi2_ring_buffer_acquire (GstAudioRingBuffer * buf,
GstAudioRingBufferSpec * spec);
static gboolean gst_wasapi2_ring_buffer_release (GstAudioRingBuffer * buf);
static gboolean gst_wasapi2_ring_buffer_start (GstAudioRingBuffer * buf);
static gboolean gst_wasapi2_ring_buffer_resume (GstAudioRingBuffer * buf);
static gboolean gst_wasapi2_ring_buffer_pause (GstAudioRingBuffer * buf);
static gboolean gst_wasapi2_ring_buffer_stop (GstAudioRingBuffer * buf);
static guint gst_wasapi2_ring_buffer_delay (GstAudioRingBuffer * buf);
#define gst_wasapi2_ring_buffer_parent_class parent_class
G_DEFINE_TYPE (GstWasapi2RingBuffer, gst_wasapi2_ring_buffer,
GST_TYPE_AUDIO_RING_BUFFER);
static void
gst_wasapi2_ring_buffer_class_init (GstWasapi2RingBufferClass * klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
GstAudioRingBufferClass *ring_buffer_class =
GST_AUDIO_RING_BUFFER_CLASS (klass);
gobject_class->constructed = gst_wasapi2_ring_buffer_constructed;
gobject_class->dispose = gst_wasapi2_ring_buffer_dispose;
gobject_class->finalize = gst_wasapi2_ring_buffer_finalize;
ring_buffer_class->open_device =
GST_DEBUG_FUNCPTR (gst_wasapi2_ring_buffer_open_device);
ring_buffer_class->close_device =
GST_DEBUG_FUNCPTR (gst_wasapi2_ring_buffer_close_device);
ring_buffer_class->acquire =
GST_DEBUG_FUNCPTR (gst_wasapi2_ring_buffer_acquire);
ring_buffer_class->release =
GST_DEBUG_FUNCPTR (gst_wasapi2_ring_buffer_release);
ring_buffer_class->start = GST_DEBUG_FUNCPTR (gst_wasapi2_ring_buffer_start);
ring_buffer_class->resume =
GST_DEBUG_FUNCPTR (gst_wasapi2_ring_buffer_resume);
ring_buffer_class->pause = GST_DEBUG_FUNCPTR (gst_wasapi2_ring_buffer_pause);
ring_buffer_class->stop = GST_DEBUG_FUNCPTR (gst_wasapi2_ring_buffer_stop);
ring_buffer_class->delay = GST_DEBUG_FUNCPTR (gst_wasapi2_ring_buffer_delay);
GST_DEBUG_CATEGORY_INIT (gst_wasapi2_ring_buffer_debug,
"wasapi2ringbuffer", 0, "wasapi2ringbuffer");
}
static void
gst_wasapi2_ring_buffer_init (GstWasapi2RingBuffer * self)
{
self->volume = 1.0f;
self->mute = FALSE;
self->event_handle = CreateEvent (nullptr, FALSE, FALSE, nullptr);
self->loopback_event_handle = CreateEvent (nullptr, FALSE, FALSE, nullptr);
g_mutex_init (&self->volume_lock);
}
static void
gst_wasapi2_ring_buffer_constructed (GObject * object)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (object);
HRESULT hr;
DWORD task_id = 0;
DWORD queue_id = 0;
hr = MFLockSharedWorkQueue (L"Pro Audio", 0, &task_id, &queue_id);
if (!gst_wasapi2_result (hr)) {
GST_WARNING_OBJECT (self, "Failed to get work queue id");
goto out;
}
self->callback_object = new GstWasapiAsyncCallback (self, queue_id, FALSE);
hr = MFCreateAsyncResult (nullptr, self->callback_object, nullptr,
&self->callback_result);
if (!gst_wasapi2_result (hr)) {
GST_WARNING_OBJECT (self, "Failed to create IAsyncResult");
GST_WASAPI2_CLEAR_COM (self->callback_object);
}
/* Create another callback object for loopback silence feed */
self->loopback_callback_object =
new GstWasapiAsyncCallback (self, queue_id, TRUE);
hr = MFCreateAsyncResult (nullptr, self->loopback_callback_object, nullptr,
&self->loopback_callback_result);
if (!gst_wasapi2_result (hr)) {
GST_WARNING_OBJECT (self, "Failed to create IAsyncResult");
GST_WASAPI2_CLEAR_COM (self->callback_object);
GST_WASAPI2_CLEAR_COM (self->callback_result);
GST_WASAPI2_CLEAR_COM (self->loopback_callback_object);
}
out:
G_OBJECT_CLASS (parent_class)->constructed (object);
}
static void
gst_wasapi2_ring_buffer_dispose (GObject * object)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (object);
GST_WASAPI2_CLEAR_COM (self->render_client);
GST_WASAPI2_CLEAR_COM (self->capture_client);
GST_WASAPI2_CLEAR_COM (self->volume_object);
GST_WASAPI2_CLEAR_COM (self->callback_result);
GST_WASAPI2_CLEAR_COM (self->callback_object);
GST_WASAPI2_CLEAR_COM (self->loopback_callback_result);
GST_WASAPI2_CLEAR_COM (self->loopback_callback_object);
gst_clear_object (&self->client);
gst_clear_object (&self->loopback_client);
gst_clear_caps (&self->supported_caps);
G_OBJECT_CLASS (parent_class)->dispose (object);
}
static void
gst_wasapi2_ring_buffer_finalize (GObject * object)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (object);
g_free (self->device_id);
CloseHandle (self->event_handle);
CloseHandle (self->loopback_event_handle);
g_mutex_clear (&self->volume_lock);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
gst_wasapi2_ring_buffer_post_open_error (GstWasapi2RingBuffer * self)
{
GstElement *parent = (GstElement *) GST_OBJECT_PARENT (self);
if (!parent) {
GST_WARNING_OBJECT (self, "Cannot find parent");
return;
}
if (self->device_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER) {
GST_ELEMENT_ERROR (parent, RESOURCE, OPEN_WRITE,
(nullptr), ("Failed to open device"));
} else {
GST_ELEMENT_ERROR (parent, RESOURCE, OPEN_READ,
(nullptr), ("Failed to open device"));
}
}
static void
gst_wasapi2_ring_buffer_post_scheduling_error (GstWasapi2RingBuffer * self)
{
GstElement *parent = (GstElement *) GST_OBJECT_PARENT (self);
if (!parent) {
GST_WARNING_OBJECT (self, "Cannot find parent");
return;
}
GST_ELEMENT_ERROR (parent, RESOURCE, FAILED,
(nullptr), ("Failed to schedule next I/O"));
}
static void
gst_wasapi2_ring_buffer_post_io_error (GstWasapi2RingBuffer * self, HRESULT hr)
{
GstElement *parent = (GstElement *) GST_OBJECT_PARENT (self);
gchar *error_msg;
if (!parent) {
GST_WARNING_OBJECT (self, "Cannot find parent");
return;
}
error_msg = gst_wasapi2_util_get_error_message (hr);
GST_ERROR_OBJECT (self, "Posting I/O error %s (hr: 0x%x)", error_msg, hr);
if (self->device_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER) {
GST_ELEMENT_ERROR (parent, RESOURCE, WRITE,
("Failed to write to device"), ("%s, hr: 0x%x", error_msg, hr));
} else {
GST_ELEMENT_ERROR (parent, RESOURCE, READ,
("Failed to read from device"), ("%s hr: 0x%x", error_msg, hr));
}
g_free (error_msg);
}
static gboolean
gst_wasapi2_ring_buffer_open_device (GstAudioRingBuffer * buf)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (buf);
GST_DEBUG_OBJECT (self, "Open");
if (self->client) {
GST_DEBUG_OBJECT (self, "Already opened");
return TRUE;
}
self->client = gst_wasapi2_client_new (self->device_class,
-1, self->device_id, self->loopback_target_pid, self->dispatcher);
if (!self->client) {
gst_wasapi2_ring_buffer_post_open_error (self);
return FALSE;
}
g_object_get (self->client, "auto-routing", &self->can_auto_routing, nullptr);
/* Open another render client to feed silence */
if (gst_wasapi2_device_class_is_loopback (self->device_class)) {
self->loopback_client =
gst_wasapi2_client_new (GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER,
-1, self->device_id, 0, self->dispatcher);
if (!self->loopback_client) {
gst_wasapi2_ring_buffer_post_open_error (self);
gst_clear_object (&self->client);
return FALSE;
}
}
return TRUE;
}
static gboolean
gst_wasapi2_ring_buffer_close_device_internal (GstAudioRingBuffer * buf)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (buf);
GST_DEBUG_OBJECT (self, "Close device");
if (self->running)
gst_wasapi2_ring_buffer_stop (buf);
GST_WASAPI2_CLEAR_COM (self->capture_client);
GST_WASAPI2_CLEAR_COM (self->render_client);
g_mutex_lock (&self->volume_lock);
if (self->volume_object)
self->volume_object->SetMute (FALSE, nullptr);
GST_WASAPI2_CLEAR_COM (self->volume_object);
g_mutex_unlock (&self->volume_lock);
gst_clear_object (&self->client);
gst_clear_object (&self->loopback_client);
return TRUE;
}
static gboolean
gst_wasapi2_ring_buffer_close_device (GstAudioRingBuffer * buf)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (buf);
GST_DEBUG_OBJECT (self, "Close");
gst_wasapi2_ring_buffer_close_device_internal (buf);
gst_clear_caps (&self->supported_caps);
return TRUE;
}
static HRESULT
gst_wasapi2_ring_buffer_read (GstWasapi2RingBuffer * self)
{
GstAudioRingBuffer *ringbuffer = GST_AUDIO_RING_BUFFER_CAST (self);
BYTE *data = nullptr;
UINT32 to_read = 0;
guint32 to_read_bytes;
DWORD flags = 0;
HRESULT hr;
guint64 position;
GstAudioInfo *info = &ringbuffer->spec.info;
IAudioCaptureClient *capture_client = self->capture_client;
guint gap_size = 0;
guint offset = 0;
gint segment;
guint8 *readptr;
gint len;
if (!capture_client) {
GST_ERROR_OBJECT (self, "IAudioCaptureClient is not available");
return E_FAIL;
}
hr = capture_client->GetBuffer (&data, &to_read, &flags, &position, nullptr);
if (hr == AUDCLNT_S_BUFFER_EMPTY || to_read == 0) {
GST_LOG_OBJECT (self, "Empty buffer");
to_read = 0;
goto out;
}
to_read_bytes = to_read * GST_AUDIO_INFO_BPF (info);
GST_LOG_OBJECT (self, "Reading %d frames offset at %" G_GUINT64_FORMAT
", expected position %" G_GUINT64_FORMAT, to_read, position,
self->expected_position);
/* XXX: position might not be increased in case of process loopback */
if (!gst_wasapi2_device_class_is_process_loopback (self->device_class)) {
if (self->is_first) {
self->expected_position = position + to_read;
self->is_first = FALSE;
} else {
if (position > self->expected_position) {
guint gap_frames;
gap_frames = (guint) (position - self->expected_position);
GST_WARNING_OBJECT (self, "Found %u frames gap", gap_frames);
gap_size = gap_frames * GST_AUDIO_INFO_BPF (info);
}
self->expected_position = position + to_read;
}
} else if (self->mute) {
/* volume clinet might not be available in case of process loopback */
flags |= AUDCLNT_BUFFERFLAGS_SILENT;
}
/* Fill gap data if any */
while (gap_size > 0) {
if (!gst_audio_ring_buffer_prepare_read (ringbuffer,
&segment, &readptr, &len)) {
GST_INFO_OBJECT (self, "No segment available");
goto out;
}
g_assert (self->segoffset >= 0);
len -= self->segoffset;
if (len > gap_size)
len = gap_size;
gst_audio_format_info_fill_silence (ringbuffer->spec.info.finfo,
readptr + self->segoffset, len);
self->segoffset += len;
gap_size -= len;
if (self->segoffset == ringbuffer->spec.segsize) {
gst_audio_ring_buffer_advance (ringbuffer, 1);
self->segoffset = 0;
}
}
while (to_read_bytes) {
if (!gst_audio_ring_buffer_prepare_read (ringbuffer,
&segment, &readptr, &len)) {
GST_INFO_OBJECT (self, "No segment available");
goto out;
}
len -= self->segoffset;
if (len > to_read_bytes)
len = to_read_bytes;
if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == AUDCLNT_BUFFERFLAGS_SILENT) {
gst_audio_format_info_fill_silence (ringbuffer->spec.info.finfo,
readptr + self->segoffset, len);
} else {
memcpy (readptr + self->segoffset, data + offset, len);
}
self->segoffset += len;
offset += len;
to_read_bytes -= len;
if (self->segoffset == ringbuffer->spec.segsize) {
gst_audio_ring_buffer_advance (ringbuffer, 1);
self->segoffset = 0;
}
}
out:
hr = capture_client->ReleaseBuffer (to_read);
/* For debugging */
gst_wasapi2_result (hr);
return hr;
}
static HRESULT
gst_wasapi2_ring_buffer_write (GstWasapi2RingBuffer * self, gboolean preroll)
{
GstAudioRingBuffer *ringbuffer = GST_AUDIO_RING_BUFFER_CAST (self);
HRESULT hr;
IAudioClient *client_handle;
IAudioRenderClient *render_client;
guint32 padding_frames = 0;
guint32 can_write;
guint32 can_write_bytes;
gint segment;
guint8 *readptr;
gint len;
BYTE *data = nullptr;
client_handle = gst_wasapi2_client_get_handle (self->client);
if (!client_handle) {
GST_ERROR_OBJECT (self, "IAudioClient is not available");
return E_FAIL;
}
render_client = self->render_client;
if (!render_client) {
GST_ERROR_OBJECT (self, "IAudioRenderClient is not available");
return E_FAIL;
}
hr = client_handle->GetCurrentPadding (&padding_frames);
if (!gst_wasapi2_result (hr))
return hr;
if (padding_frames >= self->buffer_size) {
GST_INFO_OBJECT (self,
"Padding size %d is larger than or equal to buffer size %d",
padding_frames, self->buffer_size);
return S_OK;
}
can_write = self->buffer_size - padding_frames;
can_write_bytes = can_write * GST_AUDIO_INFO_BPF (&ringbuffer->spec.info);
if (preroll) {
GST_INFO_OBJECT (self, "Pre-fill %d frames with silence", can_write);
hr = render_client->GetBuffer (can_write, &data);
if (!gst_wasapi2_result (hr))
return hr;
hr = render_client->ReleaseBuffer (can_write, AUDCLNT_BUFFERFLAGS_SILENT);
return gst_wasapi2_result (hr);
}
GST_LOG_OBJECT (self, "Writing %d frames offset at %" G_GUINT64_FORMAT,
can_write, self->write_frame_offset);
self->write_frame_offset += can_write;
while (can_write_bytes > 0) {
if (!gst_audio_ring_buffer_prepare_read (ringbuffer,
&segment, &readptr, &len)) {
GST_INFO_OBJECT (self, "No segment available, fill silence");
/* This would be case where in the middle of PAUSED state change.
* Just fill silent buffer to avoid immediate I/O callback after
* we return here */
hr = render_client->GetBuffer (can_write, &data);
if (!gst_wasapi2_result (hr))
return hr;
hr = render_client->ReleaseBuffer (can_write, AUDCLNT_BUFFERFLAGS_SILENT);
/* for debugging */
gst_wasapi2_result (hr);
return hr;
}
len -= self->segoffset;
if (len > can_write_bytes)
len = can_write_bytes;
can_write = len / GST_AUDIO_INFO_BPF (&ringbuffer->spec.info);
if (can_write == 0)
break;
hr = render_client->GetBuffer (can_write, &data);
if (!gst_wasapi2_result (hr))
return hr;
memcpy (data, readptr + self->segoffset, len);
hr = render_client->ReleaseBuffer (can_write, 0);
self->segoffset += len;
can_write_bytes -= len;
if (self->segoffset == ringbuffer->spec.segsize) {
gst_audio_ring_buffer_clear (ringbuffer, segment);
gst_audio_ring_buffer_advance (ringbuffer, 1);
self->segoffset = 0;
}
if (!gst_wasapi2_result (hr)) {
GST_WARNING_OBJECT (self, "Failed to release buffer");
break;
}
}
return S_OK;
}
static HRESULT
gst_wasapi2_ring_buffer_io_callback (GstWasapi2RingBuffer * self)
{
HRESULT hr = E_FAIL;
g_return_val_if_fail (GST_IS_WASAPI2_RING_BUFFER (self), E_FAIL);
if (!self->running) {
GST_INFO_OBJECT (self, "We are not running now");
return S_OK;
}
switch (self->device_class) {
case GST_WASAPI2_CLIENT_DEVICE_CLASS_CAPTURE:
case GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE:
case GST_WASAPI2_CLIENT_DEVICE_CLASS_INCLUDE_PROCESS_LOOPBACK_CAPTURE:
case GST_WASAPI2_CLIENT_DEVICE_CLASS_EXCLUDE_PROCESS_LOOPBACK_CAPTURE:
hr = gst_wasapi2_ring_buffer_read (self);
break;
case GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER:
hr = gst_wasapi2_ring_buffer_write (self, FALSE);
break;
default:
g_assert_not_reached ();
break;
}
/* We can ignore errors for device unplugged event if client can support
* automatic stream routing, but except for loopback capture.
* loopback capture client doesn't seem to be able to recover status from this
* situation */
if (self->can_auto_routing &&
!gst_wasapi2_device_class_is_loopback (self->device_class) &&
!gst_wasapi2_device_class_is_process_loopback (self->device_class) &&
(hr == AUDCLNT_E_ENDPOINT_CREATE_FAILED
|| hr == AUDCLNT_E_DEVICE_INVALIDATED)) {
GST_WARNING_OBJECT (self,
"Device was unplugged but client can support automatic routing");
hr = S_OK;
}
if (self->running) {
if (gst_wasapi2_result (hr)) {
hr = MFPutWaitingWorkItem (self->event_handle, 0, self->callback_result,
&self->callback_key);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to put item");
gst_wasapi2_ring_buffer_post_scheduling_error (self);
return hr;
}
}
} else {
GST_INFO_OBJECT (self, "We are not running now");
return S_OK;
}
if (FAILED (hr))
gst_wasapi2_ring_buffer_post_io_error (self, hr);
return hr;
}
static HRESULT
gst_wasapi2_ring_buffer_fill_loopback_silence (GstWasapi2RingBuffer * self)
{
HRESULT hr;
IAudioClient *client_handle;
IAudioRenderClient *render_client;
guint32 padding_frames = 0;
guint32 can_write;
BYTE *data = nullptr;
client_handle = gst_wasapi2_client_get_handle (self->loopback_client);
if (!client_handle) {
GST_ERROR_OBJECT (self, "IAudioClient is not available");
return E_FAIL;
}
render_client = self->render_client;
if (!render_client) {
GST_ERROR_OBJECT (self, "IAudioRenderClient is not available");
return E_FAIL;
}
hr = client_handle->GetCurrentPadding (&padding_frames);
if (!gst_wasapi2_result (hr))
return hr;
if (padding_frames >= self->buffer_size) {
GST_INFO_OBJECT (self,
"Padding size %d is larger than or equal to buffer size %d",
padding_frames, self->buffer_size);
return S_OK;
}
can_write = self->buffer_size - padding_frames;
GST_TRACE_OBJECT (self,
"Writing %d silent frames offset at %" G_GUINT64_FORMAT, can_write);
hr = render_client->GetBuffer (can_write, &data);
if (!gst_wasapi2_result (hr))
return hr;
hr = render_client->ReleaseBuffer (can_write, AUDCLNT_BUFFERFLAGS_SILENT);
return gst_wasapi2_result (hr);
}
static HRESULT
gst_wasapi2_ring_buffer_loopback_callback (GstWasapi2RingBuffer * self)
{
HRESULT hr = E_FAIL;
g_return_val_if_fail (GST_IS_WASAPI2_RING_BUFFER (self), E_FAIL);
g_return_val_if_fail (gst_wasapi2_device_class_is_loopback
(self->device_class), E_FAIL);
if (!self->running) {
GST_INFO_OBJECT (self, "We are not running now");
return S_OK;
}
hr = gst_wasapi2_ring_buffer_fill_loopback_silence (self);
if (self->running) {
if (gst_wasapi2_result (hr)) {
hr = MFPutWaitingWorkItem (self->loopback_event_handle, 0,
self->loopback_callback_result, &self->loopback_callback_key);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to put item");
gst_wasapi2_ring_buffer_post_scheduling_error (self);
return hr;
}
}
} else {
GST_INFO_OBJECT (self, "We are not running now");
return S_OK;
}
if (FAILED (hr))
gst_wasapi2_ring_buffer_post_io_error (self, hr);
return hr;
}
static HRESULT
gst_wasapi2_ring_buffer_initialize_audio_client3 (GstWasapi2RingBuffer * self,
IAudioClient * client_handle, WAVEFORMATEX * mix_format, guint * period)
{
HRESULT hr = S_OK;
UINT32 default_period, fundamental_period, min_period, max_period;
/* AUDCLNT_STREAMFLAGS_NOPERSIST is not allowed for
* InitializeSharedAudioStream */
DWORD stream_flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
ComPtr < IAudioClient3 > audio_client;
hr = client_handle->QueryInterface (IID_PPV_ARGS (&audio_client));
if (!gst_wasapi2_result (hr)) {
GST_INFO_OBJECT (self, "IAudioClient3 interface is unavailable");
return hr;
}
hr = audio_client->GetSharedModeEnginePeriod (mix_format,
&default_period, &fundamental_period, &min_period, &max_period);
if (!gst_wasapi2_result (hr)) {
GST_INFO_OBJECT (self, "Couldn't get period");
return hr;
}
GST_INFO_OBJECT (self, "Using IAudioClient3, default period %d frames, "
"fundamental period %d frames, minimum period %d frames, maximum period "
"%d frames", default_period, fundamental_period, min_period, max_period);
*period = min_period;
hr = audio_client->InitializeSharedAudioStream (stream_flags, min_period,
mix_format, nullptr);
if (!gst_wasapi2_result (hr))
GST_WARNING_OBJECT (self, "Failed to initialize IAudioClient3");
return hr;
}
static HRESULT
gst_wasapi2_ring_buffer_initialize_audio_client (GstWasapi2RingBuffer * self,
IAudioClient * client_handle, WAVEFORMATEX * mix_format, guint * period,
DWORD extra_flags, GstWasapi2ClientDeviceClass device_class)
{
GstAudioRingBuffer *ringbuffer = GST_AUDIO_RING_BUFFER_CAST (self);
REFERENCE_TIME default_period, min_period;
DWORD stream_flags =
AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST;
HRESULT hr;
stream_flags |= extra_flags;
if (!gst_wasapi2_device_class_is_process_loopback (device_class)) {
hr = client_handle->GetDevicePeriod (&default_period, &min_period);
if (!gst_wasapi2_result (hr)) {
GST_WARNING_OBJECT (self, "Couldn't get device period info");
return hr;
}
GST_INFO_OBJECT (self, "wasapi2 default period: %" G_GINT64_FORMAT
", min period: %" G_GINT64_FORMAT, default_period, min_period);
hr = client_handle->Initialize (AUDCLNT_SHAREMODE_SHARED, stream_flags,
/* hnsBufferDuration should be same as hnsPeriodicity
* when AUDCLNT_STREAMFLAGS_EVENTCALLBACK is used.
* And in case of shared mode, hnsPeriodicity should be zero, so
* this value should be zero as well */
0,
/* This must always be 0 in shared mode */
0, mix_format, nullptr);
} else {
/* XXX: virtual device will not report device period.
* Use hardcoded period 20ms, same as Microsoft sample code
* https://github.com/microsoft/windows-classic-samples/tree/main/Samples/ApplicationLoopback
*/
default_period = (20 * GST_MSECOND) / 100;
hr = client_handle->Initialize (AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
default_period,
AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, mix_format, nullptr);
}
if (!gst_wasapi2_result (hr)) {
GST_WARNING_OBJECT (self, "Couldn't initialize audioclient");
return hr;
}
*period = gst_util_uint64_scale_round (default_period * 100,
GST_AUDIO_INFO_RATE (&ringbuffer->spec.info), GST_SECOND);
return S_OK;
}
static gboolean
gst_wasapi2_ring_buffer_prepare_loopback_client (GstWasapi2RingBuffer * self)
{
IAudioClient *client_handle;
HRESULT hr;
WAVEFORMATEX *mix_format = nullptr;
guint period = 0;
ComPtr < IAudioRenderClient > render_client;
if (!self->loopback_client) {
GST_ERROR_OBJECT (self, "No configured client object");
return FALSE;
}
if (!gst_wasapi2_client_ensure_activation (self->loopback_client)) {
GST_ERROR_OBJECT (self, "Failed to activate audio client");
return FALSE;
}
client_handle = gst_wasapi2_client_get_handle (self->loopback_client);
if (!client_handle) {
GST_ERROR_OBJECT (self, "IAudioClient handle is not available");
return FALSE;
}
hr = client_handle->GetMixFormat (&mix_format);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to get mix format");
return FALSE;
}
hr = gst_wasapi2_ring_buffer_initialize_audio_client (self, client_handle,
mix_format, &period, 0, GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to initialize audio client");
return FALSE;
}
hr = client_handle->SetEventHandle (self->loopback_event_handle);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to set event handle");
return FALSE;
}
hr = client_handle->GetBufferSize (&self->loopback_buffer_size);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to query buffer size");
return FALSE;
}
hr = client_handle->GetService (IID_PPV_ARGS (&render_client));
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "IAudioRenderClient is unavailable");
return FALSE;
}
self->render_client = render_client.Detach ();
return TRUE;
}
static gboolean
gst_wasapi2_ring_buffer_acquire (GstAudioRingBuffer * buf,
GstAudioRingBufferSpec * spec)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (buf);
IAudioClient *client_handle;
HRESULT hr;
WAVEFORMATEX *mix_format = nullptr;
ComPtr < ISimpleAudioVolume > audio_volume;
GstAudioChannelPosition *position = nullptr;
guint period = 0;
GST_DEBUG_OBJECT (buf, "Acquire");
if (!self->client && !gst_wasapi2_ring_buffer_open_device (buf))
return FALSE;
if (gst_wasapi2_device_class_is_loopback (self->device_class)) {
if (!gst_wasapi2_ring_buffer_prepare_loopback_client (self)) {
GST_ERROR_OBJECT (self, "Failed to prepare loopback client");
goto error;
}
}
if (!gst_wasapi2_client_ensure_activation (self->client)) {
GST_ERROR_OBJECT (self, "Failed to activate audio client");
goto error;
}
client_handle = gst_wasapi2_client_get_handle (self->client);
if (!client_handle) {
GST_ERROR_OBJECT (self, "IAudioClient handle is not available");
goto error;
}
/* TODO: convert given caps to mix format */
hr = client_handle->GetMixFormat (&mix_format);
if (!gst_wasapi2_result (hr)) {
if (gst_wasapi2_device_class_is_process_loopback (self->device_class)) {
mix_format = gst_wasapi2_get_default_mix_format ();
} else {
GST_ERROR_OBJECT (self, "Failed to get mix format");
goto error;
}
}
/* Only use audioclient3 when low-latency is requested because otherwise
* very slow machines and VMs with 1 CPU allocated will get glitches:
* https://bugzilla.gnome.org/show_bug.cgi?id=794497 */
hr = E_FAIL;
if (self->low_latency &&
/* AUDCLNT_STREAMFLAGS_LOOPBACK is not allowed for
* InitializeSharedAudioStream */
!gst_wasapi2_device_class_is_loopback (self->device_class) &&
!gst_wasapi2_device_class_is_process_loopback (self->device_class)) {
hr = gst_wasapi2_ring_buffer_initialize_audio_client3 (self, client_handle,
mix_format, &period);
}
/* Try again if IAudioClinet3 API is unavailable.
* NOTE: IAudioClinet3:: methods might not be available for default device
* NOTE: The default device is a special device which is needed for supporting
* automatic stream routing
* https://docs.microsoft.com/en-us/windows/win32/coreaudio/automatic-stream-routing
*/
if (FAILED (hr)) {
DWORD extra_flags = 0;
if (gst_wasapi2_device_class_is_loopback (self->device_class))
extra_flags = AUDCLNT_STREAMFLAGS_LOOPBACK;
hr = gst_wasapi2_ring_buffer_initialize_audio_client (self, client_handle,
mix_format, &period, extra_flags, self->device_class);
}
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to initialize audio client");
goto error;
}
hr = client_handle->SetEventHandle (self->event_handle);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to set event handle");
goto error;
}
gst_wasapi2_util_waveformatex_to_channel_mask (mix_format, &position);
if (position)
gst_audio_ring_buffer_set_channel_positions (buf, position);
g_free (position);
CoTaskMemFree (mix_format);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to init audio client");
goto error;
}
hr = client_handle->GetBufferSize (&self->buffer_size);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to query buffer size");
goto error;
}
g_assert (period > 0);
if (self->buffer_size > period) {
GST_INFO_OBJECT (self, "Updating buffer size %d -> %d", self->buffer_size,
period);
self->buffer_size = period;
}
spec->segsize = period * GST_AUDIO_INFO_BPF (&buf->spec.info);
spec->segtotal = 2;
GST_INFO_OBJECT (self,
"Buffer size: %d frames, period: %d frames, segsize: %d bytes",
self->buffer_size, period, spec->segsize);
if (self->device_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER) {
ComPtr < IAudioRenderClient > render_client;
hr = client_handle->GetService (IID_PPV_ARGS (&render_client));
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "IAudioRenderClient is unavailable");
goto error;
}
self->render_client = render_client.Detach ();
} else {
ComPtr < IAudioCaptureClient > capture_client;
hr = client_handle->GetService (IID_PPV_ARGS (&capture_client));
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "IAudioCaptureClient is unavailable");
goto error;
}
self->capture_client = capture_client.Detach ();
}
hr = client_handle->GetService (IID_PPV_ARGS (&audio_volume));
if (!gst_wasapi2_result (hr)) {
GST_WARNING_OBJECT (self, "ISimpleAudioVolume is unavailable");
} else {
g_mutex_lock (&self->volume_lock);
self->volume_object = audio_volume.Detach ();
if (self->mute_changed) {
self->volume_object->SetMute (self->mute, nullptr);
self->mute_changed = FALSE;
} else {
self->volume_object->SetMute (FALSE, nullptr);
}
if (self->volume_changed) {
self->volume_object->SetMasterVolume (self->volume, nullptr);
self->volume_changed = FALSE;
}
g_mutex_unlock (&self->volume_lock);
}
buf->size = spec->segtotal * spec->segsize;
buf->memory = (guint8 *) g_malloc (buf->size);
gst_audio_format_info_fill_silence (buf->spec.info.finfo,
buf->memory, buf->size);
return TRUE;
error:
GST_WASAPI2_CLEAR_COM (self->render_client);
GST_WASAPI2_CLEAR_COM (self->capture_client);
GST_WASAPI2_CLEAR_COM (self->volume_object);
gst_wasapi2_ring_buffer_post_open_error (self);
return FALSE;
}
static gboolean
gst_wasapi2_ring_buffer_release (GstAudioRingBuffer * buf)
{
GST_DEBUG_OBJECT (buf, "Release");
g_clear_pointer (&buf->memory, g_free);
/* IAudioClient handle is not reusable once it's initialized */
gst_wasapi2_ring_buffer_close_device_internal (buf);
return TRUE;
}
static gboolean
gst_wasapi2_ring_buffer_start_internal (GstWasapi2RingBuffer * self)
{
IAudioClient *client_handle;
HRESULT hr;
if (self->running) {
GST_INFO_OBJECT (self, "We are running already");
return TRUE;
}
client_handle = gst_wasapi2_client_get_handle (self->client);
self->is_first = TRUE;
self->running = TRUE;
self->segoffset = 0;
self->write_frame_offset = 0;
switch (self->device_class) {
case GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER:
/* render client might read data from buffer immediately once it's prepared.
* Pre-fill with silence in order to start-up glitch */
hr = gst_wasapi2_ring_buffer_write (self, TRUE);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to pre-fill buffer with silence");
goto error;
}
break;
case GST_WASAPI2_CLIENT_DEVICE_CLASS_LOOPBACK_CAPTURE:
{
IAudioClient *loopback_client_handle;
/* Start silence feed client first */
loopback_client_handle =
gst_wasapi2_client_get_handle (self->loopback_client);
hr = loopback_client_handle->Start ();
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to start loopback client");
self->running = FALSE;
goto error;
}
hr = MFPutWaitingWorkItem (self->loopback_event_handle,
0, self->loopback_callback_result, &self->loopback_callback_key);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to put waiting item");
loopback_client_handle->Stop ();
self->running = FALSE;
goto error;
}
break;
}
default:
break;
}
hr = client_handle->Start ();
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to start client");
self->running = FALSE;
goto error;
}
hr = MFPutWaitingWorkItem (self->event_handle, 0, self->callback_result,
&self->callback_key);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to put waiting item");
client_handle->Stop ();
self->running = FALSE;
goto error;
}
return TRUE;
error:
gst_wasapi2_ring_buffer_post_open_error (self);
return FALSE;
}
static gboolean
gst_wasapi2_ring_buffer_start (GstAudioRingBuffer * buf)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (buf);
GST_DEBUG_OBJECT (self, "Start");
return gst_wasapi2_ring_buffer_start_internal (self);
}
static gboolean
gst_wasapi2_ring_buffer_resume (GstAudioRingBuffer * buf)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (buf);
GST_DEBUG_OBJECT (self, "Resume");
return gst_wasapi2_ring_buffer_start_internal (self);
}
static gboolean
gst_wasapi2_ring_buffer_stop_internal (GstWasapi2RingBuffer * self)
{
IAudioClient *client_handle;
HRESULT hr;
if (!self->client) {
GST_DEBUG_OBJECT (self, "No configured client");
return TRUE;
}
if (!self->running) {
GST_DEBUG_OBJECT (self, "We are not running");
return TRUE;
}
client_handle = gst_wasapi2_client_get_handle (self->client);
self->running = FALSE;
MFCancelWorkItem (self->callback_key);
hr = client_handle->Stop ();
gst_wasapi2_result (hr);
/* Call reset for later reuse case */
hr = client_handle->Reset ();
self->expected_position = 0;
self->write_frame_offset = 0;
if (self->loopback_client) {
client_handle = gst_wasapi2_client_get_handle (self->loopback_client);
MFCancelWorkItem (self->loopback_callback_key);
hr = client_handle->Stop ();
gst_wasapi2_result (hr);
client_handle->Reset ();
}
return TRUE;
}
static gboolean
gst_wasapi2_ring_buffer_stop (GstAudioRingBuffer * buf)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (buf);
GST_DEBUG_OBJECT (buf, "Stop");
return gst_wasapi2_ring_buffer_stop_internal (self);
}
static gboolean
gst_wasapi2_ring_buffer_pause (GstAudioRingBuffer * buf)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (buf);
GST_DEBUG_OBJECT (buf, "Pause");
return gst_wasapi2_ring_buffer_stop_internal (self);
}
static guint
gst_wasapi2_ring_buffer_delay (GstAudioRingBuffer * buf)
{
/* NOTE: WASAPI supports GetCurrentPadding() method for querying
* currently unread buffer size, but it doesn't seem to be quite useful
* here because:
*
* In case of capture client, GetCurrentPadding() will return the number of
* unread frames which will be identical to pNumFramesToRead value of
* IAudioCaptureClient::GetBuffer()'s return. Since we are running on
* event-driven mode and whenever available, WASAPI will notify signal
* so it's likely zero at this moment. And there is a chance to
* return incorrect value here because our IO callback happens from
* other thread.
*
* And render client's padding size will return the total size of buffer
* which is likely larger than twice of our period. Which doesn't represent
* the amount queued frame size in device correctly
*/
return 0;
}
GstAudioRingBuffer *
gst_wasapi2_ring_buffer_new (GstWasapi2ClientDeviceClass device_class,
gboolean low_latency, const gchar * device_id, gpointer dispatcher,
const gchar * name, guint loopback_target_pid)
{
GstWasapi2RingBuffer *self;
self = (GstWasapi2RingBuffer *)
g_object_new (GST_TYPE_WASAPI2_RING_BUFFER, "name", name, nullptr);
if (!self->callback_object) {
gst_object_unref (self);
return nullptr;
}
self->device_class = device_class;
self->low_latency = low_latency;
self->device_id = g_strdup (device_id);
self->dispatcher = dispatcher;
self->loopback_target_pid = loopback_target_pid;
return GST_AUDIO_RING_BUFFER_CAST (self);
}
GstCaps *
gst_wasapi2_ring_buffer_get_caps (GstWasapi2RingBuffer * buf)
{
g_return_val_if_fail (GST_IS_WASAPI2_RING_BUFFER (buf), nullptr);
if (buf->supported_caps)
return gst_caps_ref (buf->supported_caps);
if (!buf->client)
return nullptr;
if (!gst_wasapi2_client_ensure_activation (buf->client)) {
GST_ERROR_OBJECT (buf, "Failed to activate audio client");
return nullptr;
}
buf->supported_caps = gst_wasapi2_client_get_caps (buf->client);
if (buf->supported_caps)
return gst_caps_ref (buf->supported_caps);
return nullptr;
}
HRESULT
gst_wasapi2_ring_buffer_set_mute (GstWasapi2RingBuffer * buf, gboolean mute)
{
HRESULT hr = S_OK;
g_return_val_if_fail (GST_IS_WASAPI2_RING_BUFFER (buf), E_INVALIDARG);
g_mutex_lock (&buf->volume_lock);
buf->mute = mute;
if (buf->volume_object)
hr = buf->volume_object->SetMute (mute, nullptr);
else
buf->mute_changed = TRUE;
g_mutex_unlock (&buf->volume_lock);
return S_OK;
}
HRESULT
gst_wasapi2_ring_buffer_get_mute (GstWasapi2RingBuffer * buf, gboolean * mute)
{
BOOL mute_val;
HRESULT hr = S_OK;
g_return_val_if_fail (GST_IS_WASAPI2_RING_BUFFER (buf), E_INVALIDARG);
g_return_val_if_fail (mute != nullptr, E_INVALIDARG);
mute_val = buf->mute;
g_mutex_lock (&buf->volume_lock);
if (buf->volume_object)
hr = buf->volume_object->GetMute (&mute_val);
g_mutex_unlock (&buf->volume_lock);
*mute = mute_val ? TRUE : FALSE;
return hr;
}
HRESULT
gst_wasapi2_ring_buffer_set_volume (GstWasapi2RingBuffer * buf, gfloat volume)
{
HRESULT hr = S_OK;
g_return_val_if_fail (GST_IS_WASAPI2_RING_BUFFER (buf), E_INVALIDARG);
g_return_val_if_fail (volume >= 0 && volume <= 1.0, E_INVALIDARG);
g_mutex_lock (&buf->volume_lock);
buf->volume = volume;
if (buf->volume_object)
hr = buf->volume_object->SetMasterVolume (volume, nullptr);
else
buf->volume_changed = TRUE;
g_mutex_unlock (&buf->volume_lock);
return hr;
}
HRESULT
gst_wasapi2_ring_buffer_get_volume (GstWasapi2RingBuffer * buf, gfloat * volume)
{
gfloat volume_val;
HRESULT hr = S_OK;
g_return_val_if_fail (GST_IS_WASAPI2_RING_BUFFER (buf), E_INVALIDARG);
g_return_val_if_fail (volume != nullptr, E_INVALIDARG);
g_mutex_lock (&buf->volume_lock);
volume_val = buf->volume;
if (buf->volume_object)
hr = buf->volume_object->GetMasterVolume (&volume_val);
g_mutex_unlock (&buf->volume_lock);
*volume = volume_val;
return hr;
}