diff --git a/sys/wasapi/gstwasapisink.c b/sys/wasapi/gstwasapisink.c index 9fe393a06f..81436cfa08 100644 --- a/sys/wasapi/gstwasapisink.c +++ b/sys/wasapi/gstwasapisink.c @@ -50,9 +50,10 @@ static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_ALWAYS, GST_STATIC_CAPS (GST_WASAPI_STATIC_CAPS)); -#define DEFAULT_ROLE GST_WASAPI_DEVICE_ROLE_CONSOLE -#define DEFAULT_MUTE FALSE -#define DEFAULT_EXCLUSIVE FALSE +#define DEFAULT_ROLE GST_WASAPI_DEVICE_ROLE_CONSOLE +#define DEFAULT_MUTE FALSE +#define DEFAULT_EXCLUSIVE FALSE +#define DEFAULT_LOW_LATENCY FALSE enum { @@ -60,7 +61,8 @@ enum PROP_ROLE, PROP_MUTE, PROP_DEVICE, - PROP_EXCLUSIVE + PROP_EXCLUSIVE, + PROP_LOW_LATENCY }; static void gst_wasapi_sink_dispose (GObject * object); @@ -124,6 +126,12 @@ gst_wasapi_sink_class_init (GstWasapiSinkClass * klass) "Open the device in exclusive mode", DEFAULT_EXCLUSIVE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, + PROP_LOW_LATENCY, + g_param_spec_boolean ("low-latency", "Low latency", + "Optimize all settings for lowest latency", + DEFAULT_LOW_LATENCY, 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", @@ -221,6 +229,9 @@ gst_wasapi_sink_set_property (GObject * object, guint prop_id, self->sharemode = g_value_get_boolean (value) ? AUDCLNT_SHAREMODE_EXCLUSIVE : AUDCLNT_SHAREMODE_SHARED; break; + case PROP_LOW_LATENCY: + self->low_latency = g_value_get_boolean (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -248,6 +259,9 @@ gst_wasapi_sink_get_property (GObject * object, guint prop_id, g_value_set_boolean (value, self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE); break; + case PROP_LOW_LATENCY: + g_value_set_boolean (value, self->low_latency); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -376,42 +390,48 @@ gst_wasapi_sink_prepare (GstAudioSink * asink, GstAudioRingBufferSpec * spec) gboolean res = FALSE; REFERENCE_TIME latency_rt; IAudioRenderClient *render_client = NULL; - gint64 default_period, min_period, use_period; + REFERENCE_TIME default_period, min_period; + REFERENCE_TIME device_period, device_buffer_duration; guint bpf, rate; HRESULT hr; - hr = IAudioClient_GetDevicePeriod (self->client, &default_period, &min_period); + hr = IAudioClient_GetDevicePeriod (self->client, &default_period, + &min_period); if (hr != S_OK) { GST_ERROR_OBJECT (self, "IAudioClient::GetDevicePeriod failed"); - goto beach; + return FALSE; } + GST_INFO_OBJECT (self, "wasapi default period: %" G_GINT64_FORMAT ", min period: %" G_GINT64_FORMAT, default_period, min_period); - 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); + if (self->low_latency) { + if (self->sharemode == AUDCLNT_SHAREMODE_SHARED) { + device_period = default_period; + device_buffer_duration = 0; + } else { + device_period = min_period; + device_buffer_duration = min_period; + } } 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); + /* Clamp values to integral multiples of an appropriate period */ + gst_wasapi_util_get_best_buffer_sizes (spec, + self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE, default_period, + min_period, &device_period, &device_buffer_duration); } + + /* For some reason, we need to call this a second time for exclusive mode */ + if (self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) + CoInitialize (NULL); + + hr = IAudioClient_Initialize (self->client, self->sharemode, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, device_buffer_duration, + /* This must always be 0 in shared mode */ + self->sharemode == AUDCLNT_SHAREMODE_SHARED ? 0 : device_period, + self->mix_format, NULL); if (hr != S_OK) { gchar *msg = gst_wasapi_util_hresult_to_string (hr); - GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ, (NULL), + GST_ELEMENT_ERROR (self, RESOURCE, OPEN_WRITE, (NULL), ("IAudioClient::Initialize () failed: %s", msg)); g_free (msg); goto beach; @@ -431,7 +451,7 @@ gst_wasapi_sink_prepare (GstAudioSink * asink, GstAudioRingBufferSpec * spec) /* Actual latency-time/buffer-time are different now */ spec->segsize = gst_util_uint64_scale_int_round (rate * bpf, - use_period * 100, GST_SECOND); + device_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); diff --git a/sys/wasapi/gstwasapisink.h b/sys/wasapi/gstwasapisink.h index 588d5807db..e445ce2908 100644 --- a/sys/wasapi/gstwasapisink.h +++ b/sys/wasapi/gstwasapisink.h @@ -61,6 +61,7 @@ struct _GstWasapiSink gint role; gint sharemode; gboolean mute; + gboolean low_latency; wchar_t *device_strid; }; diff --git a/sys/wasapi/gstwasapisrc.c b/sys/wasapi/gstwasapisrc.c index dcb196c869..375400914a 100644 --- a/sys/wasapi/gstwasapisrc.c +++ b/sys/wasapi/gstwasapisrc.c @@ -48,15 +48,17 @@ static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_ALWAYS, GST_STATIC_CAPS (GST_WASAPI_STATIC_CAPS)); -#define DEFAULT_ROLE GST_WASAPI_DEVICE_ROLE_CONSOLE -#define DEFAULT_EXCLUSIVE FALSE +#define DEFAULT_ROLE GST_WASAPI_DEVICE_ROLE_CONSOLE +#define DEFAULT_EXCLUSIVE FALSE +#define DEFAULT_LOW_LATENCY FALSE enum { PROP_0, PROP_ROLE, PROP_DEVICE, - PROP_EXCLUSIVE + PROP_EXCLUSIVE, + PROP_LOW_LATENCY }; static void gst_wasapi_src_dispose (GObject * object); @@ -116,6 +118,12 @@ gst_wasapi_src_class_init (GstWasapiSrcClass * klass) "Open the device in exclusive mode", DEFAULT_EXCLUSIVE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, + PROP_LOW_LATENCY, + g_param_spec_boolean ("low-latency", "Low latency", + "Optimize all settings for lowest latency", + DEFAULT_LOW_LATENCY, 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", @@ -218,6 +226,9 @@ gst_wasapi_src_set_property (GObject * object, guint prop_id, self->sharemode = g_value_get_boolean (value) ? AUDCLNT_SHAREMODE_EXCLUSIVE : AUDCLNT_SHAREMODE_SHARED; break; + case PROP_LOW_LATENCY: + self->low_latency = g_value_get_boolean (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -242,6 +253,9 @@ gst_wasapi_src_get_property (GObject * object, guint prop_id, g_value_set_boolean (value, self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE); break; + case PROP_LOW_LATENCY: + g_value_set_boolean (value, self->low_latency); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -369,8 +383,8 @@ gst_wasapi_src_prepare (GstAudioSrc * asrc, GstAudioRingBufferSpec * spec) IAudioClock *client_clock = NULL; guint64 client_clock_freq = 0; IAudioCaptureClient *capture_client = NULL; - REFERENCE_TIME latency_rt; - gint64 default_period, min_period, use_period; + REFERENCE_TIME latency_rt, default_period, min_period; + REFERENCE_TIME device_period, device_buffer_duration; guint bpf, rate, buffer_frames; HRESULT hr; @@ -383,27 +397,30 @@ gst_wasapi_src_prepare (GstAudioSrc * asrc, GstAudioRingBufferSpec * spec) GST_INFO_OBJECT (self, "wasapi default period: %" G_GINT64_FORMAT ", min period: %" G_GINT64_FORMAT, default_period, min_period); - 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); + if (self->low_latency) { + if (self->sharemode == AUDCLNT_SHAREMODE_SHARED) { + device_period = default_period; + device_buffer_duration = 0; + } else { + device_period = min_period; + device_buffer_duration = min_period; + } } 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); + /* Clamp values to integral multiples of an appropriate period */ + gst_wasapi_util_get_best_buffer_sizes (spec, + self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE, default_period, + min_period, &device_period, &device_buffer_duration); } + + /* For some reason, we need to call this a second time for exclusive mode */ + if (self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) + CoInitialize (NULL); + + hr = IAudioClient_Initialize (self->client, self->sharemode, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, device_buffer_duration, + /* This must always be 0 in shared mode */ + self->sharemode == AUDCLNT_SHAREMODE_SHARED ? 0 : device_period, + self->mix_format, NULL); if (hr != S_OK) { gchar *msg = gst_wasapi_util_hresult_to_string (hr); GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ, (NULL), @@ -425,7 +442,7 @@ gst_wasapi_src_prepare (GstAudioSrc * asrc, GstAudioRingBufferSpec * spec) "rate is %i Hz", buffer_frames, bpf, rate); spec->segsize = gst_util_uint64_scale_int_round (rate * bpf, - use_period * 100, GST_SECOND); + device_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); diff --git a/sys/wasapi/gstwasapisrc.h b/sys/wasapi/gstwasapisrc.h index 530fb2b927..ca3f641464 100644 --- a/sys/wasapi/gstwasapisrc.h +++ b/sys/wasapi/gstwasapisrc.h @@ -62,6 +62,7 @@ struct _GstWasapiSrc /* properties */ gint role; gint sharemode; + gboolean low_latency; wchar_t *device_strid; }; diff --git a/sys/wasapi/gstwasapiutil.c b/sys/wasapi/gstwasapiutil.c index be25de558c..402880aaf1 100644 --- a/sys/wasapi/gstwasapiutil.c +++ b/sys/wasapi/gstwasapiutil.c @@ -814,3 +814,49 @@ gst_wasapi_util_parse_waveformatex (WAVEFORMATEXTENSIBLE * format, return TRUE; } + +void +gst_wasapi_util_get_best_buffer_sizes (GstAudioRingBufferSpec * spec, + gboolean exclusive, REFERENCE_TIME default_period, + REFERENCE_TIME min_period, REFERENCE_TIME * ret_period, + REFERENCE_TIME * ret_buffer_duration) +{ + REFERENCE_TIME use_period, use_buffer; + + /* Figure out what integral device period to use as the base */ + if (exclusive) { + /* Exclusive mode can run at multiples of either the minimum period or the + * default period; these are on the hardware ringbuffer */ + if (spec->latency_time * 10 > default_period) + use_period = default_period; + else + use_period = min_period; + } else { + /* Shared mode always runs at the default period, so if we want a larger + * period (for lower CPU usage), we do it as a multiple of that */ + use_period = default_period; + } + + /* Ensure that the period (latency_time) used is an integral multiple of + * either the default period or the minimum period */ + use_period = use_period * MAX ((spec->latency_time * 10) / use_period, 1); + + if (exclusive) { + /* Buffer duration is the same as the period in exclusive mode. The + * hardware is always writing out one buffer (of size *ret_period), and + * we're writing to the other one. */ + use_buffer = use_period; + } else { + /* Ask WASAPI to create a software ringbuffer of at least this size; it may + * be larger so the actual buffer time may be different, which is why after + * initialization we read the buffer duration actually in-use and set + * segsize/segtotal from that. */ + use_buffer = spec->buffer_time * 10; + /* Has to be at least twice the period */ + if (use_buffer < 2 * use_period) + use_buffer = 2 * use_period; + } + + *ret_period = use_period; + *ret_buffer_duration = use_buffer; +} diff --git a/sys/wasapi/gstwasapiutil.h b/sys/wasapi/gstwasapiutil.h index 1584a649ef..3ca96ecebe 100644 --- a/sys/wasapi/gstwasapiutil.h +++ b/sys/wasapi/gstwasapiutil.h @@ -77,4 +77,9 @@ gboolean gst_wasapi_util_parse_waveformatex (WAVEFORMATEXTENSIBLE * format, GstCaps * template_caps, GstCaps ** out_caps, GstAudioChannelPosition ** out_positions); +void gst_wasapi_util_get_best_buffer_sizes (GstAudioRingBufferSpec * spec, + gboolean exclusive, REFERENCE_TIME default_period, + REFERENCE_TIME min_period, REFERENCE_TIME * ret_period, + REFERENCE_TIME * ret_buffer_duration); + #endif /* __GST_WASAPI_UTIL_H__ */