gstreamer/sys/wasapi2/gstwasapi2ringbuffer.cpp

1016 lines
28 KiB
C++
Raw Normal View History

wasapi2: Rewrite plugin and implement audioringbuffer subclass ... based on MediaFoundation work queue API. By this commit, wasapi2 plugin will make use of pull mode scheduling with audioringbuffer subclass. There are several drawbacks of audiosrc/audiosink subclassing (not audiobasesrc/audiobasesink) for WASAPI API, which are: * audiosrc/audiosink classes try to set high priority to read/write thread via MMCSS (Multimedia Class Scheduler Service) but it's not allowed in case of UWP application. In order to use MMCSS in UWP, application should use MediaFoundation work queue indirectly. Since audiosrc/audiosink scheduling model is not compatible with MediaFoundation's work queue model, audioringbuffer subclassing is required. * WASAPI capture device might report larger packet size than expected (i.e., larger frames we can read than expected frame size per period). Meanwhile, in any case, application should drain all packets at that moment. In order to handle the case, wasapi/wasapi2 plugins were making use of GstAdapter which is obviously sub-optimal because it requires additional memory allocation and copy. By implementing audioringbuffer subclassing, we can avoid such inefficiency. In this commit, all the device read/write operations will be moved to newly implemented wasapi2ringbuffer class and existing wasapi2client class will take care of device enumeration and activation parts only. Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/merge_requests/2306>
2021-05-10 11:45:28 +00:00
/* 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);
/* *INDENT-OFF* */
using namespace Microsoft::WRL;
class GstWasapiAsyncCallback : public IMFAsyncCallback
{
public:
GstWasapiAsyncCallback(GstWasapi2RingBuffer *listener, DWORD queue_id)
: ref_count_(1)
, queue_id_(queue_id)
{
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;
}
hr = gst_wasapi2_ring_buffer_io_callback (ringbuffer);
gst_object_unref (ringbuffer);
return hr;
}
private:
ULONG ref_count_;
DWORD queue_id_;
GWeakRef listener_;
};
/* *INDENT-ON* */
struct _GstWasapi2RingBuffer
{
GstAudioRingBuffer parent;
GstWasapi2ClientDeviceClass device_class;
gchar *device_id;
gboolean low_latency;
gboolean mute;
gdouble volume;
gpointer dispatcher;
GstWasapi2Client *client;
IAudioCaptureClient *capture_client;
IAudioRenderClient *render_client;
ISimpleAudioVolume *volume_object;
GstWasapiAsyncCallback *callback_object;
IMFAsyncResult *callback_result;
MFWORKITEM_KEY callback_key;
HANDLE event_handle;
guint64 expected_position;
gboolean is_first;
gboolean running;
UINT32 buffer_size;
gint segoffset;
guint64 write_frame_offset;
GMutex volume_lock;
gboolean mute_changed;
gboolean volume_changed;
};
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_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_start);
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);
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);
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);
}
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_clear_object (&self->client);
GST_WASAPI2_CLEAR_COM (self->callback_result);
GST_WASAPI2_CLEAR_COM (self->callback_object);
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);
g_mutex_clear (&self->volume_lock);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static gboolean
gst_wasapi2_ring_buffer_open_device (GstAudioRingBuffer * buf)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (buf);
GST_DEBUG_OBJECT (self, "Open");
self->client = gst_wasapi2_client_new (self->device_class,
-1, self->device_id, self->dispatcher);
if (!self->client) {
GST_ERROR_OBJECT (self, "Failed to open device");
return FALSE;
}
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_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);
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_TRACE_OBJECT (self, "Reading at %d frames offset %" G_GUINT64_FORMAT
", expected position %" G_GUINT64_FORMAT, to_read, position,
self->expected_position);
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;
}
/* 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;
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 S_OK;
}
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_TRACE_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");
return S_OK;
}
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:
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;
}
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)) {
GstElement *parent =
(GstElement *) gst_object_get_parent (GST_OBJECT_CAST (self));
GST_ERROR_OBJECT (self, "Failed to put item");
if (parent) {
GST_ELEMENT_ERROR (parent, RESOURCE, FAILED,
(nullptr), ("Failed to schedule next I/O"));
gst_object_unref (parent);
}
}
}
} else {
GST_INFO_OBJECT (self, "We are not running now");
return S_OK;
}
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)
{
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;
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);
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_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_ERROR_OBJECT (self, "No configured client object");
return FALSE;
}
if (!gst_wasapi2_client_ensure_activation (self->client)) {
GST_ERROR_OBJECT (self, "Failed to activate audio client");
return FALSE;
}
client_handle = gst_wasapi2_client_get_handle (self->client);
if (!client_handle) {
GST_ERROR_OBJECT (self, "IAudioClient handle is not available");
return FALSE;
}
/* TODO: convert given caps to mix format */
hr = client_handle->GetMixFormat (&mix_format);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to get mix format");
return FALSE;
}
/* 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) {
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)) {
hr = gst_wasapi2_ring_buffer_initialize_audio_client (self, client_handle,
mix_format, &period);
}
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to initialize audio client");
return FALSE;
}
hr = client_handle->SetEventHandle (self->event_handle);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to set event handle");
return FALSE;
}
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");
return FALSE;
}
hr = client_handle->GetBufferSize (&self->buffer_size);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to query buffer size");
return FALSE;
}
g_assert (period > 0);
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");
return FALSE;
}
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");
return FALSE;
}
self->capture_client = capture_client.Detach ();
}
hr = client_handle->GetService (IID_PPV_ARGS (&audio_volume));
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "ISimpleAudioVolume is unavailable");
goto error;
}
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);
return FALSE;
}
static gboolean
gst_wasapi2_ring_buffer_release (GstAudioRingBuffer * buf)
{
GST_DEBUG_OBJECT (buf, "Release");
g_clear_pointer (&buf->memory, g_free);
return TRUE;
}
static gboolean
gst_wasapi2_ring_buffer_start (GstAudioRingBuffer * buf)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (buf);
IAudioClient *client_handle;
HRESULT hr;
GST_DEBUG_OBJECT (self, "Start");
client_handle = gst_wasapi2_client_get_handle (self->client);
self->is_first = TRUE;
self->running = TRUE;
self->segoffset = 0;
self->write_frame_offset = 0;
/* render client might read data from buffer immediately once it's prepared.
* Pre-fill with silence in order to start-up glitch */
if (self->device_class == GST_WASAPI2_CLIENT_DEVICE_CLASS_RENDER) {
hr = gst_wasapi2_ring_buffer_write (self, TRUE);
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to pre-fill buffer with silence");
return FALSE;
}
}
hr = client_handle->Start ();
if (!gst_wasapi2_result (hr)) {
GST_ERROR_OBJECT (self, "Failed to start client");
self->running = FALSE;
return FALSE;
}
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;
return FALSE;
}
return TRUE;
}
static gboolean
gst_wasapi2_ring_buffer_stop (GstAudioRingBuffer * buf)
{
GstWasapi2RingBuffer *self = GST_WASAPI2_RING_BUFFER (buf);
IAudioClient *client_handle;
HRESULT hr;
GST_DEBUG_OBJECT (buf, "Stop");
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;
return TRUE;
}
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)
{
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;
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->client)
return nullptr;
if (!gst_wasapi2_client_ensure_activation (buf->client)) {
GST_ERROR_OBJECT (buf, "Failed to activate audio client");
return nullptr;
}
return gst_wasapi2_client_get_caps (buf->client);
}
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->volume_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->mute_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;
}