#include "gstwasapisink.h"
-#include <mmdeviceapi.h>
-
GST_DEBUG_CATEGORY_STATIC (gst_wasapi_sink_debug);
#define GST_CAT_DEFAULT gst_wasapi_sink_debug
GST_PAD_ALWAYS,
GST_STATIC_CAPS (GST_WASAPI_STATIC_CAPS));
-#define DEFAULT_ROLE GST_WASAPI_DEVICE_ROLE_CONSOLE
-#define DEFAULT_MUTE FALSE
+#define DEFAULT_ROLE GST_WASAPI_DEVICE_ROLE_CONSOLE
+#define DEFAULT_MUTE FALSE
+#define DEFAULT_EXCLUSIVE FALSE
enum
{
PROP_0,
PROP_ROLE,
PROP_MUTE,
- PROP_DEVICE
+ PROP_DEVICE,
+ PROP_EXCLUSIVE
};
static void gst_wasapi_sink_dispose (GObject * object);
"WASAPI playback device as a GUID string",
NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+ g_object_class_install_property (gobject_class,
+ PROP_EXCLUSIVE,
+ g_param_spec_boolean ("exclusive", "Exclusive mode",
+ "Open the device in exclusive mode",
+ DEFAULT_EXCLUSIVE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
gst_element_class_add_static_pad_template (gstelement_class, &sink_template);
gst_element_class_set_static_metadata (gstelement_class, "WasapiSrc",
"Sink/Audio",
device ? g_utf8_to_utf16 (device, -1, NULL, NULL, NULL) : NULL;
break;
}
+ case PROP_EXCLUSIVE:
+ self->sharemode = g_value_get_boolean (value)
+ ? AUDCLNT_SHAREMODE_EXCLUSIVE : AUDCLNT_SHAREMODE_SHARED;
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
g_value_take_string (value, self->device_strid ?
g_utf16_to_utf8 (self->device_strid, -1, NULL, NULL, NULL) : NULL);
break;
+ case PROP_EXCLUSIVE:
+ g_value_set_boolean (value,
+ self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
GstWasapiSink *self = GST_WASAPI_SINK (bsink);
WAVEFORMATEX *format = NULL;
GstCaps *caps = NULL;
- HRESULT hr;
GST_DEBUG_OBJECT (self, "entering get caps");
caps = gst_caps_ref (self->cached_caps);
} else {
GstCaps *template_caps;
+ gboolean ret;
template_caps = gst_pad_get_pad_template_caps (bsink->sinkpad);
if (!self->client)
gst_wasapi_sink_open (GST_AUDIO_SINK (bsink));
- hr = IAudioClient_GetMixFormat (self->client, &format);
- if (hr != S_OK || format == NULL) {
+ ret = gst_wasapi_util_get_device_format (GST_ELEMENT (self),
+ self->sharemode, self->device, self->client, &format);
+ if (!ret) {
GST_ELEMENT_ERROR (self, STREAM, FORMAT, (NULL),
- ("GetMixFormat failed: %s", gst_wasapi_util_hresult_to_string (hr)));
+ ("failed to detect format"));
goto out;
}
{
GstWasapiSink *self = GST_WASAPI_SINK (asink);
gboolean res = FALSE;
+ IMMDevice *device = NULL;
IAudioClient *client = NULL;
GST_DEBUG_OBJECT (self, "opening device");
* For example, perhaps we should automatically switch to the new device if
* the default device is changed and a device isn't explicitly selected. */
if (!gst_wasapi_util_get_device_client (GST_ELEMENT (self), FALSE,
- self->role, self->device_strid, &client)) {
+ self->role, self->device_strid, &device, &client)) {
if (!self->device_strid)
GST_ELEMENT_ERROR (self, RESOURCE, OPEN_WRITE, (NULL),
("Failed to get default device"));
}
self->client = client;
+ self->device = device;
res = TRUE;
beach:
{
GstWasapiSink *self = GST_WASAPI_SINK (asink);
+ if (self->device != NULL) {
+ IUnknown_Release (self->device);
+ self->device = NULL;
+ }
+
if (self->client != NULL) {
IUnknown_Release (self->client);
self->client = NULL;
gboolean res = FALSE;
REFERENCE_TIME latency_rt;
IAudioRenderClient *render_client = NULL;
- gint64 default_period, min_period;
+ gint64 default_period, min_period, use_period;
guint bpf, rate;
HRESULT hr;
GST_INFO_OBJECT (self, "wasapi default period: %" G_GINT64_FORMAT
", min period: %" G_GINT64_FORMAT, default_period, min_period);
- /* Set hnsBufferDuration to 0, which should, in theory, tell the device to
- * create a buffer with the smallest latency possible. In practice, this is
- * usually 2 * default_period. See:
- * https://msdn.microsoft.com/en-us/library/windows/desktop/dd370871(v=vs.85).aspx
- *
- * NOTE: min_period is a lie, and I have never seen WASAPI use it as the
- * current period */
- hr = IAudioClient_Initialize (self->client, AUDCLNT_SHAREMODE_SHARED,
- AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 0, 0, self->mix_format, NULL);
+ if (self->sharemode == AUDCLNT_SHAREMODE_SHARED) {
+ use_period = default_period;
+ /* Set hnsBufferDuration to 0, which should, in theory, tell the device to
+ * create a buffer with the smallest latency possible. In practice, this is
+ * usually 2 * default_period. See:
+ * https://msdn.microsoft.com/en-us/library/windows/desktop/dd370871(v=vs.85).aspx
+ *
+ * NOTE: min_period is a lie, and I have never seen WASAPI use it as the
+ * current period */
+ hr = IAudioClient_Initialize (self->client, AUDCLNT_SHAREMODE_SHARED,
+ AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 0, 0, self->mix_format, NULL);
+ } else {
+ use_period = min_period;
+ /* For some reason, we need to call this another time for exclusive mode */
+ CoInitialize (NULL);
+ /* FIXME: We should be able to use min_period as the device buffer size,
+ * but I'm hitting a problem in GStreamer. */
+ hr = IAudioClient_Initialize (self->client, AUDCLNT_SHAREMODE_EXCLUSIVE,
+ AUDCLNT_STREAMFLAGS_EVENTCALLBACK, use_period, use_period,
+ self->mix_format, NULL);
+ }
if (hr != S_OK) {
GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ, (NULL),
("IAudioClient::Initialize () failed: %s",
/* Actual latency-time/buffer-time are different now */
spec->segsize = gst_util_uint64_scale_int_round (rate * bpf,
- default_period * 100, GST_SECOND);
- spec->segtotal = (self->buffer_frame_count * bpf) / spec->segsize;
+ use_period * 100, GST_SECOND);
+
+ /* We need a minimum of 2 segments to ensure glitch-free playback */
+ spec->segtotal = MAX (self->buffer_frame_count * bpf / spec->segsize, 2);
GST_INFO_OBJECT (self, "segsize is %i, segtotal is %i", spec->segsize,
spec->segtotal);
{
GstWasapiSink *self = GST_WASAPI_SINK (asink);
+ if (self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE)
+ CoUninitialize ();
+
if (self->client != NULL) {
IAudioClient_Stop (self->client);
}
guint pending = length;
while (pending > 0) {
- guint have_frames, can_frames, n_frames, n_frames_padding, write_len;
-
- /* We have N frames to be written out */
- have_frames = pending / (self->mix_format->nBlockAlign);
+ guint n_frames, write_len;
WaitForSingleObject (self->event_handle, INFINITE);
- /* Frames the card hasn't rendered yet */
- hr = IAudioClient_GetCurrentPadding (self->client, &n_frames_padding);
- if (hr != S_OK) {
- GST_ERROR_OBJECT (self, "IAudioClient::GetCurrentPadding failed: %s",
- gst_wasapi_util_hresult_to_string (hr));
- length = 0;
- goto beach;
+ if (self->sharemode == AUDCLNT_SHAREMODE_SHARED) {
+ guint have_frames, can_frames, n_frames_padding;
+
+ /* Frames the card hasn't rendered yet */
+ hr = IAudioClient_GetCurrentPadding (self->client, &n_frames_padding);
+ if (hr != S_OK) {
+ GST_ERROR_OBJECT (self, "IAudioClient::GetCurrentPadding failed: %s",
+ gst_wasapi_util_hresult_to_string (hr));
+ length = 0;
+ goto beach;
+ }
+ /* We have N frames to be written out */
+ have_frames = pending / (self->mix_format->nBlockAlign);
+ /* We can write out these many frames */
+ can_frames = self->buffer_frame_count - n_frames_padding;
+ /* We will write out these many frames, and this much length */
+ n_frames = MIN (can_frames, have_frames);
+
+ GST_TRACE_OBJECT (self, "total: %i, unread: %i, have: %i (%i bytes), "
+ "will write: %i", self->buffer_frame_count, n_frames_padding,
+ have_frames, pending, n_frames);
+ } else {
+ n_frames = self->buffer_frame_count;
}
- /* We can write out these many frames */
- can_frames = self->buffer_frame_count - n_frames_padding;
-
- /* We will write out these many frames, and this much length */
- n_frames = MIN (can_frames, have_frames);
write_len = n_frames * self->mix_format->nBlockAlign;
- GST_TRACE_OBJECT (self, "total: %i, unread: %i, have: %i (%i bytes), "
- "will write: %i (%i bytes)", self->buffer_frame_count, n_frames_padding,
- have_frames, pending, n_frames, write_len);
-
hr = IAudioRenderClient_GetBuffer (self->render_client, n_frames,
(BYTE **) & dst);
if (hr != S_OK) {
{
GstAudioSink parent;
+ IMMDevice *device;
IAudioClient *client;
IAudioRenderClient *render_client;
HANDLE event_handle;
/* properties */
gint role;
+ gint sharemode;
gboolean mute;
wchar_t *device_strid;
};
#include "gstwasapisrc.h"
-#include <mmdeviceapi.h>
-
GST_DEBUG_CATEGORY_STATIC (gst_wasapi_src_debug);
#define GST_CAT_DEFAULT gst_wasapi_src_debug
GST_PAD_ALWAYS,
GST_STATIC_CAPS (GST_WASAPI_STATIC_CAPS));
-#define DEFAULT_ROLE GST_WASAPI_DEVICE_ROLE_CONSOLE
+#define DEFAULT_ROLE GST_WASAPI_DEVICE_ROLE_CONSOLE
+#define DEFAULT_EXCLUSIVE FALSE
enum
{
PROP_0,
PROP_ROLE,
- PROP_DEVICE
+ PROP_DEVICE,
+ PROP_EXCLUSIVE
};
static void gst_wasapi_src_dispose (GObject * object);
"WASAPI playback device as a GUID string",
NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+ g_object_class_install_property (gobject_class,
+ PROP_EXCLUSIVE,
+ g_param_spec_boolean ("exclusive", "Exclusive mode",
+ "Open the device in exclusive mode",
+ DEFAULT_EXCLUSIVE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
gst_element_class_add_static_pad_template (gstelement_class, &src_template);
gst_element_class_set_static_metadata (gstelement_class, "WasapiSrc",
"Source/Audio",
device ? g_utf8_to_utf16 (device, -1, NULL, NULL, NULL) : NULL;
break;
}
+ case PROP_EXCLUSIVE:
+ self->sharemode = g_value_get_boolean (value)
+ ? AUDCLNT_SHAREMODE_EXCLUSIVE : AUDCLNT_SHAREMODE_SHARED;
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
g_value_take_string (value, self->device_strid ?
g_utf16_to_utf8 (self->device_strid, -1, NULL, NULL, NULL) : NULL);
break;
+ case PROP_EXCLUSIVE:
+ g_value_set_boolean (value,
+ self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
GstWasapiSrc *self = GST_WASAPI_SRC (bsrc);
WAVEFORMATEX *format = NULL;
GstCaps *caps = NULL;
- HRESULT hr;
GST_DEBUG_OBJECT (self, "entering get caps");
caps = gst_caps_ref (self->cached_caps);
} else {
GstCaps *template_caps;
+ gboolean ret;
template_caps = gst_pad_get_pad_template_caps (bsrc->srcpad);
if (!self->client)
gst_wasapi_src_open (GST_AUDIO_SRC (bsrc));
- hr = IAudioClient_GetMixFormat (self->client, &format);
- if (hr != S_OK || format == NULL) {
+ ret = gst_wasapi_util_get_device_format (GST_ELEMENT (self),
+ self->sharemode, self->device, self->client, &format);
+ if (!ret) {
GST_ELEMENT_ERROR (self, STREAM, FORMAT, (NULL),
- ("GetMixFormat failed: %s", gst_wasapi_util_hresult_to_string (hr)));
+ ("failed to detect format"));
goto out;
}
GstWasapiSrc *self = GST_WASAPI_SRC (asrc);
gboolean res = FALSE;
IAudioClient *client = NULL;
+ IMMDevice *device = NULL;
if (self->client)
return TRUE;
* For example, perhaps we should automatically switch to the new device if
* the default device is changed and a device isn't explicitly selected. */
if (!gst_wasapi_util_get_device_client (GST_ELEMENT (self), TRUE,
- self->role, self->device_strid, &client)) {
+ self->role, self->device_strid, &device, &client)) {
if (!self->device_strid)
GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ, (NULL),
("Failed to get default device"));
}
self->client = client;
+ self->device = device;
res = TRUE;
beach:
{
GstWasapiSrc *self = GST_WASAPI_SRC (asrc);
+ if (self->device != NULL) {
+ IUnknown_Release (self->device);
+ self->device = NULL;
+ }
+
if (self->client != NULL) {
IUnknown_Release (self->client);
self->client = NULL;
guint64 client_clock_freq = 0;
IAudioCaptureClient *capture_client = NULL;
REFERENCE_TIME latency_rt;
- gint64 default_period, min_period;
+ gint64 default_period, min_period, use_period;
guint bpf, rate, buffer_frames;
HRESULT hr;
GST_INFO_OBJECT (self, "wasapi default period: %" G_GINT64_FORMAT
", min period: %" G_GINT64_FORMAT, default_period, min_period);
- /* Set hnsBufferDuration to 0, which should, in theory, tell the device to
- * create a buffer with the smallest latency possible. In practice, this is
- * usually 2 * default_period. See:
- * https://msdn.microsoft.com/en-us/library/windows/desktop/dd370871(v=vs.85).aspx
- *
- * NOTE: min_period is a lie, and I have never seen WASAPI use it as the
- * current period */
- hr = IAudioClient_Initialize (self->client, AUDCLNT_SHAREMODE_SHARED,
- AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 0, 0, self->mix_format, NULL);
+ if (self->sharemode == AUDCLNT_SHAREMODE_SHARED) {
+ use_period = default_period;
+ /* Set hnsBufferDuration to 0, which should, in theory, tell the device to
+ * create a buffer with the smallest latency possible. In practice, this is
+ * usually 2 * default_period. See:
+ * https://msdn.microsoft.com/en-us/library/windows/desktop/dd370871(v=vs.85).aspx
+ *
+ * NOTE: min_period is a lie, and I have never seen WASAPI use it as the
+ * current period */
+ hr = IAudioClient_Initialize (self->client, AUDCLNT_SHAREMODE_SHARED,
+ AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 0, 0, self->mix_format, NULL);
+ } else {
+ use_period = default_period;
+ /* For some reason, we need to call this another time for exclusive mode */
+ CoInitialize (NULL);
+ /* FIXME: We should be able to use min_period as the device buffer size,
+ * but I'm hitting a problem in GStreamer. */
+ hr = IAudioClient_Initialize (self->client, AUDCLNT_SHAREMODE_EXCLUSIVE,
+ AUDCLNT_STREAMFLAGS_EVENTCALLBACK, use_period, use_period,
+ self->mix_format, NULL);
+ }
if (hr != S_OK) {
GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ, (NULL),
- ("IAudioClient::Initialize failed: %s",
+ ("IAudioClient::Initialize () failed: %s",
gst_wasapi_util_hresult_to_string (hr)));
goto beach;
}
"rate is %i Hz", buffer_frames, bpf, rate);
spec->segsize = gst_util_uint64_scale_int_round (rate * bpf,
- default_period * 100, GST_SECOND);
- spec->segtotal = (buffer_frames * bpf) / spec->segsize;
+ use_period * 100, GST_SECOND);
+
+ /* We need a minimum of 2 segments to ensure glitch-free playback */
+ spec->segtotal = MAX (self->buffer_frame_count * bpf / spec->segsize, 2);
GST_INFO_OBJECT (self, "segsize is %i, segtotal is %i", spec->segsize,
spec->segtotal);
{
GstWasapiSrc *self = GST_WASAPI_SRC (asrc);
+ if (self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE)
+ CoUninitialize ();
+
if (self->client != NULL) {
IAudioClient_Stop (self->client);
}
{
GstAudioSrc parent;
+ IMMDevice *device;
IAudioClient *client;
IAudioClock *client_clock;
guint64 client_clock_freq;
/* properties */
gint role;
+ gint sharemode;
wchar_t *device_strid;
};
#include "gstwasapiutil.h"
#include "gstwasapidevice.h"
-#include <mmdeviceapi.h>
-
/* This was only added to MinGW in ~2015 and our Cerbero toolchain is too old */
#if defined(_MSC_VER)
#include <functiondiscoverykeys_devpkey.h>
}
gboolean
+gst_wasapi_util_get_device_format (GstElement * element,
+ gint device_mode, IMMDevice * device, IAudioClient * client,
+ WAVEFORMATEX ** ret_format)
+{
+ WAVEFORMATEX *format;
+ HRESULT hr;
+
+ *ret_format = NULL;
+
+ hr = IAudioClient_GetMixFormat (client, &format);
+ if (hr != S_OK || format == NULL) {
+ GST_ERROR_OBJECT (element, "GetMixFormat failed: %s",
+ gst_wasapi_util_hresult_to_string (hr));
+ return FALSE;
+ }
+
+ /* WASAPI always accepts the format returned by GetMixFormat in shared mode */
+ if (device_mode == AUDCLNT_SHAREMODE_SHARED)
+ goto out;
+
+ /* WASAPI may or may not support this format in exclusive mode */
+ hr = IAudioClient_IsFormatSupported (client, AUDCLNT_SHAREMODE_EXCLUSIVE,
+ format, NULL);
+ if (hr == S_OK)
+ goto out;
+
+ CoTaskMemFree (format);
+
+ /* Open the device property store, and get the format that WASAPI has been
+ * using for sending data to the device */
+ {
+ PROPVARIANT var;
+ IPropertyStore *prop_store = NULL;
+
+ hr = IMMDevice_OpenPropertyStore (device, STGM_READ, &prop_store);
+ if (hr != S_OK) {
+ GST_ERROR_OBJECT (element, "OpenPropertyStore failed: %s",
+ gst_wasapi_util_hresult_to_string (hr));
+ return FALSE;
+ }
+
+ hr = IPropertyStore_GetValue (prop_store, &PKEY_AudioEngine_DeviceFormat,
+ &var);
+ if (hr != S_OK) {
+ GST_ERROR_OBJECT (element, "GetValue failed: %s",
+ gst_wasapi_util_hresult_to_string (hr));
+ IUnknown_Release (prop_store);
+ return FALSE;
+ }
+
+ format = malloc (var.blob.cbSize);
+ memcpy (format, var.blob.pBlobData, var.blob.cbSize);
+
+ PropVariantClear (&var);
+ IUnknown_Release (prop_store);
+ }
+
+ /* WASAPI may or may not support this format in exclusive mode */
+ hr = IAudioClient_IsFormatSupported (client, AUDCLNT_SHAREMODE_EXCLUSIVE,
+ format, NULL);
+ if (hr == S_OK)
+ goto out;
+
+ GST_ERROR_OBJECT (element, "AudioEngine DeviceFormat not supported");
+ free (format);
+ return FALSE;
+
+out:
+ *ret_format = format;
+ return TRUE;
+}
+
+gboolean
gst_wasapi_util_get_device_client (GstElement * element,
gboolean capture, gint role, const wchar_t * device_strid,
- IAudioClient ** ret_client)
+ IMMDevice ** ret_device, IAudioClient ** ret_client)
{
gboolean res = FALSE;
HRESULT hr;
}
IUnknown_AddRef (client);
+ IUnknown_AddRef (device);
*ret_client = client;
+ *ret_device = device;
res = TRUE;
#include <gst/audio/gstaudiosrc.h>
#include <gst/audio/gstaudiosink.h>
+#include <mmdeviceapi.h>
#include <audioclient.h>
/* Static Caps shared between source, sink, and device provider */
gboolean gst_wasapi_util_get_device_client (GstElement * element,
gboolean capture, gint role, const wchar_t * device_strid,
- IAudioClient ** ret_client);
+ IMMDevice ** ret_device, IAudioClient ** ret_client);
+
+gboolean gst_wasapi_util_get_device_format (GstElement * element,
+ gint device_mode, IMMDevice * device, IAudioClient * client,
+ WAVEFORMATEX ** ret_format);
gboolean gst_wasapi_util_get_render_client (GstElement * element,
IAudioClient * client, IAudioRenderClient ** ret_render_client);