diff options
author | Thibault Saunier <tsaunier@igalia.com> | 2021-09-24 16:14:03 -0300 |
---|---|---|
committer | Thibault Saunier <tsaunier@igalia.com> | 2021-09-24 16:14:04 -0300 |
commit | 07ad3439ce3859244d67bfd19b4c578400a2274c (patch) | |
tree | cd47f60172d8b31ed04124e75cc56b0bc70ebc47 /sys/wasapi | |
parent | 5ff769d731e97fa05796f3ea32e039d267ad4f2f (diff) | |
parent | ca8068c6d793d7aaa6f2e2cc6324fdedfe2f33fa (diff) |
Merging gst-plugins-bad
Diffstat (limited to 'sys/wasapi')
-rw-r--r-- | sys/wasapi/gstaudioclient3.h | 236 | ||||
-rw-r--r-- | sys/wasapi/gstmmdeviceenumerator.cpp | 472 | ||||
-rw-r--r-- | sys/wasapi/gstmmdeviceenumerator.h | 69 | ||||
-rw-r--r-- | sys/wasapi/gstwasapi.c | 57 | ||||
-rw-r--r-- | sys/wasapi/gstwasapidevice.c | 362 | ||||
-rw-r--r-- | sys/wasapi/gstwasapidevice.h | 81 | ||||
-rw-r--r-- | sys/wasapi/gstwasapisink.c | 763 | ||||
-rw-r--r-- | sys/wasapi/gstwasapisink.h | 81 | ||||
-rw-r--r-- | sys/wasapi/gstwasapisrc.c | 934 | ||||
-rw-r--r-- | sys/wasapi/gstwasapisrc.h | 90 | ||||
-rw-r--r-- | sys/wasapi/gstwasapiutil.c | 964 | ||||
-rw-r--r-- | sys/wasapi/gstwasapiutil.h | 139 | ||||
-rw-r--r-- | sys/wasapi/meson.build | 41 |
13 files changed, 4289 insertions, 0 deletions
diff --git a/sys/wasapi/gstaudioclient3.h b/sys/wasapi/gstaudioclient3.h new file mode 100644 index 0000000000..784f478d6b --- /dev/null +++ b/sys/wasapi/gstaudioclient3.h @@ -0,0 +1,236 @@ +/* + * Structure and enum definitions are from audioclient.h in the Windows 10 SDK + * + * These should be defined by MinGW, but they aren't yet since they're very new + * so we keep a copy in our tree. All definitions are guarded, so it should be + * fine to always include this even when building with MSVC. + */ +#pragma once + +#ifndef __IAudioClient3_FWD_DEFINED__ +#define __IAudioClient3_FWD_DEFINED__ +typedef interface IAudioClient3 IAudioClient3; + +#endif /* __IAudioClient3_FWD_DEFINED__ */ + +#ifndef __IAudioClient3_INTERFACE_DEFINED__ +#define __IAudioClient3_INTERFACE_DEFINED__ + +#ifndef HAVE_AUDCLNT_STREAMOPTIONS +typedef enum AUDCLNT_STREAMOPTIONS +{ + AUDCLNT_STREAMOPTIONS_NONE = 0, + AUDCLNT_STREAMOPTIONS_RAW = 0x1, + AUDCLNT_STREAMOPTIONS_MATCH_FORMAT = 0x2 +} AUDCLNT_STREAMOPTIONS; +#endif + +/* These should be available when the IAudioClient2 interface is defined */ +#ifndef __IAudioClient2_FWD_DEFINED__ +typedef enum _AUDIO_STREAM_CATEGORY { + AudioCategory_Other = 0, + AudioCategory_ForegroundOnlyMedia, + AudioCategory_BackgroundCapableMedia, + AudioCategory_Communications, + AudioCategory_Alerts, + AudioCategory_SoundEffects, + AudioCategory_GameEffects, + AudioCategory_GameMedia, + AudioCategory_GameChat, + AudioCategory_Speech, + AudioCategory_Movie, + AudioCategory_Media +} AUDIO_STREAM_CATEGORY; + +typedef struct AudioClientProperties +{ + UINT32 cbSize; + BOOL bIsOffload; + AUDIO_STREAM_CATEGORY eCategory; + AUDCLNT_STREAMOPTIONS Options; +} AudioClientProperties; +#endif /* __IAudioClient2_FWD_DEFINED__ */ + +EXTERN_C const IID IID_IAudioClient3; + +typedef struct IAudioClient3Vtbl +{ + BEGIN_INTERFACE + + HRESULT ( STDMETHODCALLTYPE *QueryInterface )( + IAudioClient3 * This, + REFIID riid, + void **ppvObject); + + ULONG ( STDMETHODCALLTYPE *AddRef )( + IAudioClient3 * This); + + ULONG ( STDMETHODCALLTYPE *Release )( + IAudioClient3 * This); + + HRESULT ( STDMETHODCALLTYPE *Initialize )( + IAudioClient3 * This, + AUDCLNT_SHAREMODE ShareMode, + DWORD StreamFlags, + REFERENCE_TIME hnsBufferDuration, + REFERENCE_TIME hnsPeriodicity, + const WAVEFORMATEX *pFormat, + LPCGUID AudioSessionGuid); + + HRESULT ( STDMETHODCALLTYPE *GetBufferSize )( + IAudioClient3 * This, + UINT32 *pNumBufferFrames); + + HRESULT ( STDMETHODCALLTYPE *GetStreamLatency )( + IAudioClient3 * This, + REFERENCE_TIME *phnsLatency); + + HRESULT ( STDMETHODCALLTYPE *GetCurrentPadding )( + IAudioClient3 * This, + UINT32 *pNumPaddingFrames); + + HRESULT ( STDMETHODCALLTYPE *IsFormatSupported )( + IAudioClient3 * This, + AUDCLNT_SHAREMODE ShareMode, + const WAVEFORMATEX *pFormat, + WAVEFORMATEX **ppClosestMatch); + + HRESULT ( STDMETHODCALLTYPE *GetMixFormat )( + IAudioClient3 * This, + WAVEFORMATEX **ppDeviceFormat); + + HRESULT ( STDMETHODCALLTYPE *GetDevicePeriod )( + IAudioClient3 * This, + REFERENCE_TIME *phnsDefaultDevicePeriod, + REFERENCE_TIME *phnsMinimumDevicePeriod); + + HRESULT ( STDMETHODCALLTYPE *Start )( + IAudioClient3 * This); + + HRESULT ( STDMETHODCALLTYPE *Stop )( + IAudioClient3 * This); + + HRESULT ( STDMETHODCALLTYPE *Reset )( + IAudioClient3 * This); + + HRESULT ( STDMETHODCALLTYPE *SetEventHandle )( + IAudioClient3 * This, + HANDLE eventHandle); + + HRESULT ( STDMETHODCALLTYPE *GetService )( + IAudioClient3 * This, + REFIID riid, + void **ppv); + + HRESULT ( STDMETHODCALLTYPE *IsOffloadCapable )( + IAudioClient3 * This, + AUDIO_STREAM_CATEGORY Category, + BOOL *pbOffloadCapable); + + HRESULT ( STDMETHODCALLTYPE *SetClientProperties )( + IAudioClient3 * This, + const AudioClientProperties *pProperties); + + HRESULT ( STDMETHODCALLTYPE *GetBufferSizeLimits )( + IAudioClient3 * This, + const WAVEFORMATEX *pFormat, + BOOL bEventDriven, + REFERENCE_TIME *phnsMinBufferDuration, + REFERENCE_TIME *phnsMaxBufferDuration); + + HRESULT ( STDMETHODCALLTYPE *GetSharedModeEnginePeriod )( + IAudioClient3 * This, + const WAVEFORMATEX *pFormat, + UINT32 *pDefaultPeriodInFrames, + UINT32 *pFundamentalPeriodInFrames, + UINT32 *pMinPeriodInFrames, + UINT32 *pMaxPeriodInFrames); + + HRESULT ( STDMETHODCALLTYPE *GetCurrentSharedModeEnginePeriod )( + IAudioClient3 * This, + WAVEFORMATEX **ppFormat, + UINT32 *pCurrentPeriodInFrames); + + HRESULT ( STDMETHODCALLTYPE *InitializeSharedAudioStream )( + IAudioClient3 * This, + DWORD StreamFlags, + UINT32 PeriodInFrames, + const WAVEFORMATEX *pFormat, + LPCGUID AudioSessionGuid); + + END_INTERFACE +} IAudioClient3Vtbl; + +interface IAudioClient3 +{ + CONST_VTBL struct IAudioClient3Vtbl *lpVtbl; +}; + +#define IAudioClient3_QueryInterface(This,riid,ppvObject) \ + ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) + +#define IAudioClient3_AddRef(This) \ + ( (This)->lpVtbl -> AddRef(This) ) + +#define IAudioClient3_Release(This) \ + ( (This)->lpVtbl -> Release(This) ) + + +#define IAudioClient3_Initialize(This,ShareMode,StreamFlags,hnsBufferDuration,hnsPeriodicity,pFormat,AudioSessionGuid) \ + ( (This)->lpVtbl -> Initialize(This,ShareMode,StreamFlags,hnsBufferDuration,hnsPeriodicity,pFormat,AudioSessionGuid) ) + +#define IAudioClient3_GetBufferSize(This,pNumBufferFrames) \ + ( (This)->lpVtbl -> GetBufferSize(This,pNumBufferFrames) ) + +#define IAudioClient3_GetStreamLatency(This,phnsLatency) \ + ( (This)->lpVtbl -> GetStreamLatency(This,phnsLatency) ) + +#define IAudioClient3_GetCurrentPadding(This,pNumPaddingFrames) \ + ( (This)->lpVtbl -> GetCurrentPadding(This,pNumPaddingFrames) ) + +#define IAudioClient3_IsFormatSupported(This,ShareMode,pFormat,ppClosestMatch) \ + ( (This)->lpVtbl -> IsFormatSupported(This,ShareMode,pFormat,ppClosestMatch) ) + +#define IAudioClient3_GetMixFormat(This,ppDeviceFormat) \ + ( (This)->lpVtbl -> GetMixFormat(This,ppDeviceFormat) ) + +#define IAudioClient3_GetDevicePeriod(This,phnsDefaultDevicePeriod,phnsMinimumDevicePeriod) \ + ( (This)->lpVtbl -> GetDevicePeriod(This,phnsDefaultDevicePeriod,phnsMinimumDevicePeriod) ) + +#define IAudioClient3_Start(This) \ + ( (This)->lpVtbl -> Start(This) ) + +#define IAudioClient3_Stop(This) \ + ( (This)->lpVtbl -> Stop(This) ) + +#define IAudioClient3_Reset(This) \ + ( (This)->lpVtbl -> Reset(This) ) + +#define IAudioClient3_SetEventHandle(This,eventHandle) \ + ( (This)->lpVtbl -> SetEventHandle(This,eventHandle) ) + +#define IAudioClient3_GetService(This,riid,ppv) \ + ( (This)->lpVtbl -> GetService(This,riid,ppv) ) + + +#define IAudioClient3_IsOffloadCapable(This,Category,pbOffloadCapable) \ + ( (This)->lpVtbl -> IsOffloadCapable(This,Category,pbOffloadCapable) ) + +#define IAudioClient3_SetClientProperties(This,pProperties) \ + ( (This)->lpVtbl -> SetClientProperties(This,pProperties) ) + +#define IAudioClient3_GetBufferSizeLimits(This,pFormat,bEventDriven,phnsMinBufferDuration,phnsMaxBufferDuration) \ + ( (This)->lpVtbl -> GetBufferSizeLimits(This,pFormat,bEventDriven,phnsMinBufferDuration,phnsMaxBufferDuration) ) + + +#define IAudioClient3_GetSharedModeEnginePeriod(This,pFormat,pDefaultPeriodInFrames,pFundamentalPeriodInFrames,pMinPeriodInFrames,pMaxPeriodInFrames) \ + ( (This)->lpVtbl -> GetSharedModeEnginePeriod(This,pFormat,pDefaultPeriodInFrames,pFundamentalPeriodInFrames,pMinPeriodInFrames,pMaxPeriodInFrames) ) + +#define IAudioClient3_GetCurrentSharedModeEnginePeriod(This,ppFormat,pCurrentPeriodInFrames) \ + ( (This)->lpVtbl -> GetCurrentSharedModeEnginePeriod(This,ppFormat,pCurrentPeriodInFrames) ) + +#define IAudioClient3_InitializeSharedAudioStream(This,StreamFlags,PeriodInFrames,pFormat,AudioSessionGuid) \ + ( (This)->lpVtbl -> InitializeSharedAudioStream(This,StreamFlags,PeriodInFrames,pFormat,AudioSessionGuid) ) + + +#endif /* __IAudioClient3_INTERFACE_DEFINED__ */ diff --git a/sys/wasapi/gstmmdeviceenumerator.cpp b/sys/wasapi/gstmmdeviceenumerator.cpp new file mode 100644 index 0000000000..1826b7fe1b --- /dev/null +++ b/sys/wasapi/gstmmdeviceenumerator.cpp @@ -0,0 +1,472 @@ +/* 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "gstmmdeviceenumerator.h" + +#ifndef INITGUID +#include <initguid.h> +#endif + +/* *INDENT-OFF* */ +G_BEGIN_DECLS + +GST_DEBUG_CATEGORY_EXTERN (gst_wasapi_debug); +#define GST_CAT_DEFAULT gst_wasapi_debug + +G_END_DECLS + +/* IMMNotificationClient implementation */ +class GstIMMNotificationClient : public IMMNotificationClient +{ +public: + static HRESULT + CreateInstance (GstMMDeviceEnumerator * enumerator, + const GstMMNotificationClientCallbacks * callbacks, + gpointer user_data, + IMMNotificationClient ** client) + { + GstIMMNotificationClient *self; + + self = new GstIMMNotificationClient (); + + self->callbacks_ = *callbacks; + self->user_data_ = user_data; + g_weak_ref_set (&self->enumerator_, enumerator); + + *client = (IMMNotificationClient *) self; + + return S_OK; + } + + /* IUnknown */ + STDMETHODIMP + QueryInterface (REFIID riid, void ** object) + { + if (!object) + return E_POINTER; + + if (riid == IID_IUnknown) { + *object = static_cast<IUnknown *> (this); + } else if (riid == __uuidof(IMMNotificationClient)) { + *object = static_cast<IMMNotificationClient *> (this); + } else { + *object = nullptr; + return E_NOINTERFACE; + } + + AddRef (); + + return S_OK; + } + + STDMETHODIMP_ (ULONG) + AddRef (void) + { + GST_TRACE ("%p, %d", this, (guint) ref_count_); + return InterlockedIncrement (&ref_count_); + } + + STDMETHODIMP_ (ULONG) + Release (void) + { + ULONG ref_count; + + GST_TRACE ("%p, %d", this, (guint) ref_count_); + ref_count = InterlockedDecrement (&ref_count_); + + if (ref_count == 0) { + GST_TRACE ("Delete instance %p", this); + delete this; + } + + return ref_count; + } + + /* IMMNotificationClient */ + STDMETHODIMP + OnDeviceStateChanged (LPCWSTR device_id, DWORD new_state) + { + GstMMDeviceEnumerator *listener; + HRESULT hr; + + if (!callbacks_.device_state_changed) + return S_OK; + + listener = (GstMMDeviceEnumerator *) g_weak_ref_get (&enumerator_); + if (!listener) + return S_OK; + + hr = callbacks_.device_state_changed (listener, device_id, new_state, + user_data_); + gst_object_unref (listener); + + return hr; + } + + STDMETHODIMP + OnDeviceAdded (LPCWSTR device_id) + { + GstMMDeviceEnumerator *listener; + HRESULT hr; + + if (!callbacks_.device_added) + return S_OK; + + listener = (GstMMDeviceEnumerator *) g_weak_ref_get (&enumerator_); + if (!listener) + return S_OK; + + hr = callbacks_.device_added (listener, device_id, user_data_); + gst_object_unref (listener); + + return hr; + } + + STDMETHODIMP + OnDeviceRemoved (LPCWSTR device_id) + { + GstMMDeviceEnumerator *listener; + HRESULT hr; + + if (!callbacks_.device_removed) + return S_OK; + + listener = (GstMMDeviceEnumerator *) g_weak_ref_get (&enumerator_); + if (!listener) + return S_OK; + + hr = callbacks_.device_removed (listener, device_id, user_data_); + gst_object_unref (listener); + + return hr; + } + + STDMETHODIMP + OnDefaultDeviceChanged (EDataFlow flow, ERole role, LPCWSTR default_device_id) + { + GstMMDeviceEnumerator *listener; + HRESULT hr; + + if (!callbacks_.default_device_changed) + return S_OK; + + listener = (GstMMDeviceEnumerator *) g_weak_ref_get (&enumerator_); + if (!listener) + return S_OK; + + hr = callbacks_.default_device_changed (listener, + flow, role, default_device_id, user_data_); + gst_object_unref (listener); + + return hr; + } + + STDMETHODIMP + OnPropertyValueChanged (LPCWSTR device_id, const PROPERTYKEY key) + { + GstMMDeviceEnumerator *listener; + HRESULT hr; + + if (!callbacks_.property_value_changed) + return S_OK; + + listener = (GstMMDeviceEnumerator *) g_weak_ref_get (&enumerator_); + if (!device_id) + return S_OK; + + hr = callbacks_.property_value_changed (listener, + device_id, key, user_data_); + gst_object_unref (listener); + + return hr; + } + +private: + GstIMMNotificationClient () + : ref_count_ (1) + { + g_weak_ref_init (&enumerator_, nullptr); + } + + virtual ~GstIMMNotificationClient () + { + g_weak_ref_clear (&enumerator_); + } + +private: + ULONG ref_count_; + GstMMNotificationClientCallbacks callbacks_; + gpointer user_data_; + GWeakRef enumerator_; +}; +/* *INDENT-ON* */ + +struct _GstMMDeviceEnumerator +{ + GstObject parent; + + IMMDeviceEnumerator *handle; + IMMNotificationClient *client; + + GMutex lock; + GCond cond; + + GThread *thread; + GMainContext *context; + GMainLoop *loop; + + gboolean running; +}; + +static void gst_mm_device_enumerator_constructed (GObject * object); +static void gst_mm_device_enumerator_finalize (GObject * object); + +static gpointer +gst_mm_device_enumerator_thread_func (GstMMDeviceEnumerator * self); + +#define gst_mm_device_enumerator_parent_class parent_class +G_DEFINE_TYPE (GstMMDeviceEnumerator, + gst_mm_device_enumerator, GST_TYPE_OBJECT); + +static void +gst_mm_device_enumerator_class_init (GstMMDeviceEnumeratorClass * klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->constructed = gst_mm_device_enumerator_constructed; + gobject_class->finalize = gst_mm_device_enumerator_finalize; +} + +static void +gst_mm_device_enumerator_init (GstMMDeviceEnumerator * self) +{ + g_mutex_init (&self->lock); + g_cond_init (&self->cond); + self->context = g_main_context_new (); + self->loop = g_main_loop_new (self->context, FALSE); +} + +static void +gst_mm_device_enumerator_constructed (GObject * object) +{ + GstMMDeviceEnumerator *self = GST_MM_DEVICE_ENUMERATOR (object); + + g_mutex_lock (&self->lock); + self->thread = g_thread_new ("GstMMDeviceEnumerator", + (GThreadFunc) gst_mm_device_enumerator_thread_func, self); + while (!g_main_loop_is_running (self->loop)) + g_cond_wait (&self->cond, &self->lock); + g_mutex_unlock (&self->lock); +} + +static void +gst_mm_device_enumerator_finalize (GObject * object) +{ + GstMMDeviceEnumerator *self = GST_MM_DEVICE_ENUMERATOR (object); + + g_main_loop_quit (self->loop); + g_thread_join (self->thread); + g_main_loop_unref (self->loop); + g_main_context_unref (self->context); + + g_mutex_clear (&self->lock); + g_cond_clear (&self->cond); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static gboolean +loop_running_cb (GstMMDeviceEnumerator * self) +{ + g_mutex_lock (&self->lock); + g_cond_signal (&self->cond); + g_mutex_unlock (&self->lock); + + return G_SOURCE_REMOVE; +} + +static gpointer +gst_mm_device_enumerator_thread_func (GstMMDeviceEnumerator * self) +{ + GSource *idle_source; + IMMDeviceEnumerator *enumerator = nullptr; + HRESULT hr; + + CoInitializeEx (NULL, COINIT_MULTITHREADED); + g_main_context_push_thread_default (self->context); + + idle_source = g_idle_source_new (); + g_source_set_callback (idle_source, + (GSourceFunc) loop_running_cb, self, nullptr); + g_source_attach (idle_source, self->context); + g_source_unref (idle_source); + + hr = CoCreateInstance (__uuidof (MMDeviceEnumerator), + nullptr, CLSCTX_ALL, IID_PPV_ARGS (&enumerator)); + if (FAILED (hr)) { + GST_ERROR_OBJECT (self, "Failed to create IMMDeviceEnumerator instance"); + goto run_loop; + } + + self->handle = enumerator; + +run_loop: + GST_INFO_OBJECT (self, "Starting loop"); + g_main_loop_run (self->loop); + GST_INFO_OBJECT (self, "Stopped loop"); + + if (self->client && self->handle) { + self->handle->UnregisterEndpointNotificationCallback (self->client); + + self->client->Release (); + } + + if (self->handle) + self->handle->Release (); + + g_main_context_pop_thread_default (self->context); + CoUninitialize (); + + return nullptr; +} + +GstMMDeviceEnumerator * +gst_mm_device_enumerator_new (void) +{ + GstMMDeviceEnumerator *self; + + self = (GstMMDeviceEnumerator *) g_object_new (GST_TYPE_MM_DEVICE_ENUMERATOR, + nullptr); + + if (!self->handle) { + gst_object_unref (self); + return nullptr; + } + + gst_object_ref_sink (self); + + return self; +} + +IMMDeviceEnumerator * +gst_mm_device_enumerator_get_handle (GstMMDeviceEnumerator * enumerator) +{ + g_return_val_if_fail (GST_IS_MM_DEVICE_ENUMERATOR (enumerator), nullptr); + + return enumerator->handle; +} + +typedef struct +{ + GstMMDeviceEnumerator *self; + GstMMNotificationClientCallbacks *callbacks; + gpointer user_data; + + gboolean handled; + GMutex lock; + GCond cond; + + gboolean ret; +} SetNotificationCallbackData; + +static gboolean +set_notification_callback (SetNotificationCallbackData * data) +{ + GstMMDeviceEnumerator *self = data->self; + HRESULT hr; + + g_mutex_lock (&data->lock); + g_mutex_lock (&self->lock); + + data->ret = TRUE; + + if (self->client) { + self->handle->UnregisterEndpointNotificationCallback (self->client); + self->client->Release (); + self->client = nullptr; + } + + if (data->callbacks) { + IMMNotificationClient *client; + + hr = GstIMMNotificationClient::CreateInstance (self, data->callbacks, + data->user_data, &client); + if (FAILED (hr)) { + GST_ERROR_OBJECT (self, + "Failed to create IMMNotificationClient instance"); + data->ret = FALSE; + goto out; + } + + hr = self->handle->RegisterEndpointNotificationCallback (client); + if (FAILED (hr)) { + GST_ERROR_OBJECT (self, "Failed to register callback"); + client->Release (); + data->ret = FALSE; + goto out; + } + + self->client = client; + } + +out: + data->handled = TRUE; + g_cond_signal (&data->cond); + g_mutex_unlock (&self->lock); + g_mutex_unlock (&data->lock); + + return G_SOURCE_REMOVE; +} + +gboolean +gst_mm_device_enumerator_set_notification_callback (GstMMDeviceEnumerator * + enumerator, GstMMNotificationClientCallbacks * callbacks, + gpointer user_data) +{ + SetNotificationCallbackData data; + gboolean ret; + + g_return_val_if_fail (GST_IS_MM_DEVICE_ENUMERATOR (enumerator), FALSE); + + data.self = enumerator; + data.callbacks = callbacks; + data.user_data = user_data; + data.handled = FALSE; + + g_mutex_init (&data.lock); + g_cond_init (&data.cond); + + g_main_context_invoke (enumerator->context, + (GSourceFunc) set_notification_callback, &data); + g_mutex_lock (&data.lock); + while (!data.handled) + g_cond_wait (&data.cond, &data.lock); + g_mutex_unlock (&data.lock); + + ret = data.ret; + + g_mutex_clear (&data.lock); + g_cond_clear (&data.cond); + + return ret; +} diff --git a/sys/wasapi/gstmmdeviceenumerator.h b/sys/wasapi/gstmmdeviceenumerator.h new file mode 100644 index 0000000000..d3f7b07a9c --- /dev/null +++ b/sys/wasapi/gstmmdeviceenumerator.h @@ -0,0 +1,69 @@ +/* + * 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. + */ + +#ifndef __GST_MM_DEVICE_ENUMERATOR_H__ +#define __GST_MM_DEVICE_ENUMERATOR_H__ + +#include <gst/gst.h> +#include <mmdeviceapi.h> + +G_BEGIN_DECLS + +#define GST_TYPE_MM_DEVICE_ENUMERATOR (gst_mm_device_enumerator_get_type ()) +G_DECLARE_FINAL_TYPE (GstMMDeviceEnumerator, gst_mm_device_enumerator, + GST, MM_DEVICE_ENUMERATOR, GstObject); + +typedef struct +{ + HRESULT (*device_state_changed) (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, + DWORD new_state, + gpointer user_data); + + HRESULT (*device_added) (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, + gpointer user_data); + + HRESULT (*device_removed) (GstMMDeviceEnumerator * provider, + LPCWSTR device_id, + gpointer user_data); + + HRESULT (*default_device_changed) (GstMMDeviceEnumerator * provider, + EDataFlow flow, + ERole role, + LPCWSTR default_device_id, + gpointer user_data); + + HRESULT (*property_value_changed) (GstMMDeviceEnumerator * provider, + LPCWSTR device_id, + const PROPERTYKEY key, + gpointer user_data); +} GstMMNotificationClientCallbacks; + +GstMMDeviceEnumerator * gst_mm_device_enumerator_new (void); + +IMMDeviceEnumerator * gst_mm_device_enumerator_get_handle (GstMMDeviceEnumerator * enumerator); + +gboolean gst_mm_device_enumerator_set_notification_callback (GstMMDeviceEnumerator * enumerator, + GstMMNotificationClientCallbacks * callbacks, + gpointer user_data); + +G_END_DECLS + +#endif /* __GST_MM_DEVICE_ENUMERATOR_H__ */ diff --git a/sys/wasapi/gstwasapi.c b/sys/wasapi/gstwasapi.c new file mode 100644 index 0000000000..72ebd53221 --- /dev/null +++ b/sys/wasapi/gstwasapi.c @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2008 Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.com> + * Copyright (C) 2018 Centricular Ltd. + * Author: Nirbheek Chauhan <nirbheek@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. + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "gstwasapisink.h" +#include "gstwasapisrc.h" +#include "gstwasapidevice.h" + +GST_DEBUG_CATEGORY (gst_wasapi_debug); + +static gboolean +plugin_init (GstPlugin * plugin) +{ + if (!gst_element_register (plugin, "wasapisink", GST_RANK_PRIMARY, + GST_TYPE_WASAPI_SINK)) + return FALSE; + + if (!gst_element_register (plugin, "wasapisrc", GST_RANK_PRIMARY, + GST_TYPE_WASAPI_SRC)) + return FALSE; + + if (!gst_device_provider_register (plugin, "wasapideviceprovider", + GST_RANK_PRIMARY, GST_TYPE_WASAPI_DEVICE_PROVIDER)) + return FALSE; + + GST_DEBUG_CATEGORY_INIT (gst_wasapi_debug, "wasapi", + 0, "Windows audio session API generic"); + + return TRUE; +} + +GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, + GST_VERSION_MINOR, + wasapi, + "Windows audio session API plugin", + plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN) diff --git a/sys/wasapi/gstwasapidevice.c b/sys/wasapi/gstwasapidevice.c new file mode 100644 index 0000000000..c13fc6a8aa --- /dev/null +++ b/sys/wasapi/gstwasapidevice.c @@ -0,0 +1,362 @@ +/* GStreamer + * Copyright (C) 2018 Nirbheek Chauhan <nirbheek@centricular.com> + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "gstwasapidevice.h" + +GST_DEBUG_CATEGORY_EXTERN (gst_wasapi_debug); +#define GST_CAT_DEFAULT gst_wasapi_debug + +G_DEFINE_TYPE (GstWasapiDeviceProvider, gst_wasapi_device_provider, + GST_TYPE_DEVICE_PROVIDER); + +static void gst_wasapi_device_provider_finalize (GObject * object); +static GList *gst_wasapi_device_provider_probe (GstDeviceProvider * provider); +static gboolean gst_wasapi_device_provider_start (GstDeviceProvider * provider); +static void gst_wasapi_device_provider_stop (GstDeviceProvider * provider); + +static HRESULT +gst_wasapi_device_provider_device_added (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, gpointer user_data); +static HRESULT +gst_wasapi_device_provider_device_removed (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, gpointer user_data); +static HRESULT +gst_wasapi_device_provider_default_device_changed (GstMMDeviceEnumerator * + enumerator, EDataFlow flow, ERole role, LPCWSTR device_id, + gpointer user_data); + +static void +gst_wasapi_device_provider_class_init (GstWasapiDeviceProviderClass * klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GstDeviceProviderClass *dm_class = GST_DEVICE_PROVIDER_CLASS (klass); + + gobject_class->finalize = gst_wasapi_device_provider_finalize; + + dm_class->probe = gst_wasapi_device_provider_probe; + dm_class->start = gst_wasapi_device_provider_start; + dm_class->stop = gst_wasapi_device_provider_stop; + + gst_device_provider_class_set_static_metadata (dm_class, + "WASAPI (Windows Audio Session API) Device Provider", + "Source/Sink/Audio", "List WASAPI source and sink devices", + "Nirbheek Chauhan <nirbheek@centricular.com>"); +} + +static void +gst_wasapi_device_provider_init (GstWasapiDeviceProvider * self) +{ + self->enumerator = gst_mm_device_enumerator_new (); +} + +static gboolean +gst_wasapi_device_provider_start (GstDeviceProvider * provider) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (provider); + GstMMNotificationClientCallbacks callbacks = { NULL, }; + GList *devices = NULL; + GList *iter; + + if (!self->enumerator) { + GST_WARNING_OBJECT (self, "Enumerator wasn't configured"); + return FALSE; + } + + callbacks.device_added = gst_wasapi_device_provider_device_added; + callbacks.device_removed = gst_wasapi_device_provider_device_removed; + callbacks.default_device_changed = + gst_wasapi_device_provider_default_device_changed; + + if (!gst_mm_device_enumerator_set_notification_callback (self->enumerator, + &callbacks, self)) { + GST_WARNING_OBJECT (self, "Failed to set callbacks"); + return FALSE; + } + + /* baseclass will not call probe() once it's started, but we can get + * notification only add/remove or change case. To this manually */ + devices = gst_wasapi_device_provider_probe (provider); + if (devices) { + for (iter = devices; iter; iter = g_list_next (iter)) { + gst_device_provider_device_add (provider, GST_DEVICE (iter->data)); + } + + g_list_free (devices); + } + + return TRUE; +} + +static void +gst_wasapi_device_provider_stop (GstDeviceProvider * provider) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (provider); + + if (self->enumerator) { + gst_mm_device_enumerator_set_notification_callback (self->enumerator, + NULL, NULL); + } +} + +static void +gst_wasapi_device_provider_finalize (GObject * object) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (object); + + gst_clear_object (&self->enumerator); + + G_OBJECT_CLASS (gst_wasapi_device_provider_parent_class)->finalize (object); +} + +static GList * +gst_wasapi_device_provider_probe (GstDeviceProvider * provider) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (provider); + GList *devices = NULL; + + if (!gst_wasapi_util_get_devices (self->enumerator, TRUE, &devices)) + GST_ERROR_OBJECT (self, "Failed to enumerate devices"); + + return devices; +} + +static gboolean +gst_wasapi_device_is_in_list (GList * list, GstDevice * device) +{ + GList *iter; + GstStructure *s; + const gchar *device_id; + gboolean found = FALSE; + + s = gst_device_get_properties (device); + g_assert (s); + + device_id = gst_structure_get_string (s, "device.strid"); + g_assert (device_id); + + for (iter = list; iter; iter = g_list_next (iter)) { + GstStructure *other_s; + const gchar *other_id; + + other_s = gst_device_get_properties (GST_DEVICE (iter->data)); + g_assert (other_s); + + other_id = gst_structure_get_string (other_s, "device.strid"); + g_assert (other_id); + + if (g_ascii_strcasecmp (device_id, other_id) == 0) { + found = TRUE; + } + + gst_structure_free (other_s); + if (found) + break; + } + + gst_structure_free (s); + + return found; +} + +static void +gst_wasapi_device_provider_update_devices (GstWasapiDeviceProvider * self) +{ + GstDeviceProvider *provider = GST_DEVICE_PROVIDER_CAST (self); + GList *prev_devices = NULL; + GList *new_devices = NULL; + GList *to_add = NULL; + GList *to_remove = NULL; + GList *iter; + + GST_OBJECT_LOCK (self); + prev_devices = g_list_copy_deep (provider->devices, + (GCopyFunc) gst_object_ref, NULL); + GST_OBJECT_UNLOCK (self); + + new_devices = gst_wasapi_device_provider_probe (provider); + + /* Ownership of GstDevice for gst_device_provider_device_add() + * and gst_device_provider_device_remove() is a bit complicated. + * Remove floating reference here for things to be clear */ + for (iter = new_devices; iter; iter = g_list_next (iter)) + gst_object_ref_sink (iter->data); + + /* Check newly added devices */ + for (iter = new_devices; iter; iter = g_list_next (iter)) { + if (!gst_wasapi_device_is_in_list (prev_devices, GST_DEVICE (iter->data))) { + to_add = g_list_prepend (to_add, gst_object_ref (iter->data)); + } + } + + /* Check removed device */ + for (iter = prev_devices; iter; iter = g_list_next (iter)) { + if (!gst_wasapi_device_is_in_list (new_devices, GST_DEVICE (iter->data))) { + to_remove = g_list_prepend (to_remove, gst_object_ref (iter->data)); + } + } + + for (iter = to_remove; iter; iter = g_list_next (iter)) + gst_device_provider_device_remove (provider, GST_DEVICE (iter->data)); + + for (iter = to_add; iter; iter = g_list_next (iter)) + gst_device_provider_device_add (provider, GST_DEVICE (iter->data)); + + if (prev_devices) + g_list_free_full (prev_devices, (GDestroyNotify) gst_object_unref); + + if (to_add) + g_list_free_full (to_add, (GDestroyNotify) gst_object_unref); + + if (to_remove) + g_list_free_full (to_remove, (GDestroyNotify) gst_object_unref); +} + +static HRESULT +gst_wasapi_device_provider_device_added (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, gpointer user_data) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (user_data); + + gst_wasapi_device_provider_update_devices (self); + + return S_OK; +} + +static HRESULT +gst_wasapi_device_provider_device_removed (GstMMDeviceEnumerator * enumerator, + LPCWSTR device_id, gpointer user_data) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (user_data); + + gst_wasapi_device_provider_update_devices (self); + + return S_OK; +} + +static HRESULT +gst_wasapi_device_provider_default_device_changed (GstMMDeviceEnumerator * + enumerator, EDataFlow flow, ERole role, LPCWSTR device_id, + gpointer user_data) +{ + GstWasapiDeviceProvider *self = GST_WASAPI_DEVICE_PROVIDER (user_data); + + gst_wasapi_device_provider_update_devices (self); + + return S_OK; +} + +/* GstWasapiDevice begins */ + +enum +{ + PROP_DEVICE_STRID = 1, +}; + +G_DEFINE_TYPE (GstWasapiDevice, gst_wasapi_device, GST_TYPE_DEVICE); + +static void gst_wasapi_device_get_property (GObject * object, + guint prop_id, GValue * value, GParamSpec * pspec); +static void gst_wasapi_device_set_property (GObject * object, + guint prop_id, const GValue * value, GParamSpec * pspec); +static void gst_wasapi_device_finalize (GObject * object); +static GstElement *gst_wasapi_device_create_element (GstDevice * device, + const gchar * name); + +static void +gst_wasapi_device_class_init (GstWasapiDeviceClass * klass) +{ + GstDeviceClass *dev_class = GST_DEVICE_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + dev_class->create_element = gst_wasapi_device_create_element; + + object_class->get_property = gst_wasapi_device_get_property; + object_class->set_property = gst_wasapi_device_set_property; + object_class->finalize = gst_wasapi_device_finalize; + + g_object_class_install_property (object_class, PROP_DEVICE_STRID, + g_param_spec_string ("device", "Device string ID", + "Device strId", NULL, + G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} + +static void +gst_wasapi_device_init (GstWasapiDevice * device) +{ +} + +static void +gst_wasapi_device_finalize (GObject * object) +{ + GstWasapiDevice *device = GST_WASAPI_DEVICE (object); + + g_free (device->strid); + + G_OBJECT_CLASS (gst_wasapi_device_parent_class)->finalize (object); +} + +static GstElement * +gst_wasapi_device_create_element (GstDevice * device, const gchar * name) +{ + GstWasapiDevice *wasapi_dev = GST_WASAPI_DEVICE (device); + GstElement *elem; + + elem = gst_element_factory_make (wasapi_dev->element, name); + + g_object_set (elem, "device", wasapi_dev->strid, NULL); + + return elem; +} + +static void +gst_wasapi_device_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstWasapiDevice *device = GST_WASAPI_DEVICE_CAST (object); + + switch (prop_id) { + case PROP_DEVICE_STRID: + g_value_set_string (value, device->strid); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_wasapi_device_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstWasapiDevice *device = GST_WASAPI_DEVICE_CAST (object); + + switch (prop_id) { + case PROP_DEVICE_STRID: + device->strid = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} diff --git a/sys/wasapi/gstwasapidevice.h b/sys/wasapi/gstwasapidevice.h new file mode 100644 index 0000000000..55eddd06b5 --- /dev/null +++ b/sys/wasapi/gstwasapidevice.h @@ -0,0 +1,81 @@ +/* GStreamer + * Copyright (C) 2018 Nirbheek Chauhan <nirbheek@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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GST_WASAPI_DEVICE_H__ +#define __GST_WASAPI_DEVICE_H__ + +#include "gstwasapiutil.h" + +G_BEGIN_DECLS + +typedef struct _GstWasapiDeviceProvider GstWasapiDeviceProvider; +typedef struct _GstWasapiDeviceProviderClass GstWasapiDeviceProviderClass; + +#define GST_TYPE_WASAPI_DEVICE_PROVIDER (gst_wasapi_device_provider_get_type()) +#define GST_IS_WASAPI_DEVICE_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_WASAPI_DEVICE_PROVIDER)) +#define GST_IS_WASAPI_DEVICE_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_WASAPI_DEVICE_PROVIDER)) +#define GST_WASAPI_DEVICE_PROVIDER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_WASAPI_DEVICE_PROVIDER, GstWasapiDeviceProviderClass)) +#define GST_WASAPI_DEVICE_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_WASAPI_DEVICE_PROVIDER, GstWasapiDeviceProvider)) +#define GST_WASAPI_DEVICE_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_DEVICE_PROVIDER, GstWasapiDeviceProviderClass)) +#define GST_WASAPI_DEVICE_PROVIDER_CAST(obj) ((GstWasapiDeviceProvider *)(obj)) + +struct _GstWasapiDeviceProvider +{ + GstDeviceProvider parent; + + GstMMDeviceEnumerator *enumerator; +}; + +struct _GstWasapiDeviceProviderClass +{ + GstDeviceProviderClass parent_class; +}; + +GType gst_wasapi_device_provider_get_type (void); + + +typedef struct _GstWasapiDevice GstWasapiDevice; +typedef struct _GstWasapiDeviceClass GstWasapiDeviceClass; + +#define GST_TYPE_WASAPI_DEVICE (gst_wasapi_device_get_type()) +#define GST_IS_WASAPI_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_WASAPI_DEVICE)) +#define GST_IS_WASAPI_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_WASAPI_DEVICE)) +#define GST_WASAPI_DEVICE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_WASAPI_DEVICE, GstWasapiDeviceClass)) +#define GST_WASAPI_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_WASAPI_DEVICE, GstWasapiDevice)) +#define GST_WASAPI_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_DEVICE, GstWasapiDeviceClass)) +#define GST_WASAPI_DEVICE_CAST(obj) ((GstWasapiDevice *)(obj)) + +struct _GstWasapiDevice +{ + GstDevice parent; + + gchar *strid; + const gchar *element; +}; + +struct _GstWasapiDeviceClass +{ + GstDeviceClass parent_class; +}; + +GType gst_wasapi_device_get_type (void); + +G_END_DECLS + +#endif /* __GST_WASAPI_DEVICE_H__ */ diff --git a/sys/wasapi/gstwasapisink.c b/sys/wasapi/gstwasapisink.c new file mode 100644 index 0000000000..b10d391104 --- /dev/null +++ b/sys/wasapi/gstwasapisink.c @@ -0,0 +1,763 @@ +/* + * Copyright (C) 2008 Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.com> + * Copyright (C) 2013 Collabora Ltd. + * Author: Sebastian Dröge <sebastian.droege@collabora.co.uk> + * Copyright (C) 2018 Centricular Ltd. + * Author: Nirbheek Chauhan <nirbheek@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. + */ + +/** + * SECTION:element-wasapisink + * @title: wasapisink + * + * Provides audio playback using the Windows Audio Session API available with + * Vista and newer. + * + * ## Example pipelines + * |[ + * gst-launch-1.0 -v audiotestsrc samplesperbuffer=160 ! wasapisink + * ]| Generate 20 ms buffers and render to the default audio device. + * + * |[ + * gst-launch-1.0 -v audiotestsrc samplesperbuffer=160 ! wasapisink low-latency=true + * ]| Same as above, but with the minimum possible latency + * + */ +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "gstwasapisink.h" + +#include <avrt.h> + +GST_DEBUG_CATEGORY_STATIC (gst_wasapi_sink_debug); +#define GST_CAT_DEFAULT gst_wasapi_sink_debug + +static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink", + GST_PAD_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_LOW_LATENCY FALSE +#define DEFAULT_AUDIOCLIENT3 TRUE + +enum +{ + PROP_0, + PROP_ROLE, + PROP_MUTE, + PROP_DEVICE, + PROP_EXCLUSIVE, + PROP_LOW_LATENCY, + PROP_AUDIOCLIENT3 +}; + +static void gst_wasapi_sink_dispose (GObject * object); +static void gst_wasapi_sink_finalize (GObject * object); +static void gst_wasapi_sink_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_wasapi_sink_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); + +static GstCaps *gst_wasapi_sink_get_caps (GstBaseSink * bsink, + GstCaps * filter); + +static gboolean gst_wasapi_sink_prepare (GstAudioSink * asink, + GstAudioRingBufferSpec * spec); +static gboolean gst_wasapi_sink_unprepare (GstAudioSink * asink); +static gboolean gst_wasapi_sink_open (GstAudioSink * asink); +static gboolean gst_wasapi_sink_close (GstAudioSink * asink); +static gint gst_wasapi_sink_write (GstAudioSink * asink, + gpointer data, guint length); +static guint gst_wasapi_sink_delay (GstAudioSink * asink); +static void gst_wasapi_sink_reset (GstAudioSink * asink); + +#define gst_wasapi_sink_parent_class parent_class +G_DEFINE_TYPE (GstWasapiSink, gst_wasapi_sink, GST_TYPE_AUDIO_SINK); + +static void +gst_wasapi_sink_class_init (GstWasapiSinkClass * klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GstElementClass *gstelement_class = GST_ELEMENT_CLASS (klass); + GstBaseSinkClass *gstbasesink_class = GST_BASE_SINK_CLASS (klass); + GstAudioSinkClass *gstaudiosink_class = GST_AUDIO_SINK_CLASS (klass); + + gobject_class->dispose = gst_wasapi_sink_dispose; + gobject_class->finalize = gst_wasapi_sink_finalize; + gobject_class->set_property = gst_wasapi_sink_set_property; + gobject_class->get_property = gst_wasapi_sink_get_property; + + g_object_class_install_property (gobject_class, + PROP_ROLE, + g_param_spec_enum ("role", "Role", + "Role of the device: communications, multimedia, etc", + GST_WASAPI_DEVICE_TYPE_ROLE, DEFAULT_ROLE, G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | GST_PARAM_MUTABLE_READY)); + + g_object_class_install_property (gobject_class, + PROP_MUTE, + g_param_spec_boolean ("mute", "Mute", "Mute state of this stream", + DEFAULT_MUTE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | + GST_PARAM_MUTABLE_PLAYING)); + + g_object_class_install_property (gobject_class, + PROP_DEVICE, + g_param_spec_string ("device", "Device", + "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)); + + g_object_class_install_property (gobject_class, + PROP_LOW_LATENCY, + g_param_spec_boolean ("low-latency", "Low latency", + "Optimize all settings for lowest latency. Always safe to enable.", + DEFAULT_LOW_LATENCY, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, + PROP_AUDIOCLIENT3, + g_param_spec_boolean ("use-audioclient3", "Use the AudioClient3 API", + "Use the Windows 10 AudioClient3 API when available and if the " + "low-latency property is set to TRUE", + DEFAULT_AUDIOCLIENT3, 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/Hardware", + "Stream audio to an audio capture device through WASAPI", + "Nirbheek Chauhan <nirbheek@centricular.com>, " + "Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.com>"); + + gstbasesink_class->get_caps = GST_DEBUG_FUNCPTR (gst_wasapi_sink_get_caps); + + gstaudiosink_class->prepare = GST_DEBUG_FUNCPTR (gst_wasapi_sink_prepare); + gstaudiosink_class->unprepare = GST_DEBUG_FUNCPTR (gst_wasapi_sink_unprepare); + gstaudiosink_class->open = GST_DEBUG_FUNCPTR (gst_wasapi_sink_open); + gstaudiosink_class->close = GST_DEBUG_FUNCPTR (gst_wasapi_sink_close); + gstaudiosink_class->write = GST_DEBUG_FUNCPTR (gst_wasapi_sink_write); + gstaudiosink_class->delay = GST_DEBUG_FUNCPTR (gst_wasapi_sink_delay); + gstaudiosink_class->reset = GST_DEBUG_FUNCPTR (gst_wasapi_sink_reset); + + GST_DEBUG_CATEGORY_INIT (gst_wasapi_sink_debug, "wasapisink", + 0, "Windows audio session API sink"); + + gst_type_mark_as_plugin_api (GST_WASAPI_DEVICE_TYPE_ROLE, 0); +} + +static void +gst_wasapi_sink_init (GstWasapiSink * self) +{ + self->role = DEFAULT_ROLE; + self->mute = DEFAULT_MUTE; + self->sharemode = AUDCLNT_SHAREMODE_SHARED; + self->low_latency = DEFAULT_LOW_LATENCY; + self->try_audioclient3 = DEFAULT_AUDIOCLIENT3; + self->event_handle = CreateEvent (NULL, FALSE, FALSE, NULL); + self->cancellable = CreateEvent (NULL, TRUE, FALSE, NULL); + self->client_needs_restart = FALSE; + + self->enumerator = gst_mm_device_enumerator_new (); +} + +static void +gst_wasapi_sink_dispose (GObject * object) +{ + GstWasapiSink *self = GST_WASAPI_SINK (object); + + if (self->event_handle != NULL) { + CloseHandle (self->event_handle); + self->event_handle = NULL; + } + + if (self->cancellable != NULL) { + CloseHandle (self->cancellable); + self->cancellable = NULL; + } + + if (self->client != NULL) { + IUnknown_Release (self->client); + self->client = NULL; + } + + if (self->render_client != NULL) { + IUnknown_Release (self->render_client); + self->render_client = NULL; + } + + gst_clear_object (&self->enumerator); + + G_OBJECT_CLASS (gst_wasapi_sink_parent_class)->dispose (object); +} + +static void +gst_wasapi_sink_finalize (GObject * object) +{ + GstWasapiSink *self = GST_WASAPI_SINK (object); + + CoTaskMemFree (self->mix_format); + self->mix_format = NULL; + + if (self->cached_caps != NULL) { + gst_caps_unref (self->cached_caps); + self->cached_caps = NULL; + } + + g_clear_pointer (&self->positions, g_free); + g_clear_pointer (&self->device_strid, g_free); + self->mute = FALSE; + + G_OBJECT_CLASS (gst_wasapi_sink_parent_class)->finalize (object); +} + +static void +gst_wasapi_sink_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstWasapiSink *self = GST_WASAPI_SINK (object); + + switch (prop_id) { + case PROP_ROLE: + self->role = gst_wasapi_device_role_to_erole (g_value_get_enum (value)); + break; + case PROP_MUTE: + self->mute = g_value_get_boolean (value); + break; + case PROP_DEVICE: + { + const gchar *device = g_value_get_string (value); + g_free (self->device_strid); + self->device_strid = + 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; + case PROP_LOW_LATENCY: + self->low_latency = g_value_get_boolean (value); + break; + case PROP_AUDIOCLIENT3: + self->try_audioclient3 = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_wasapi_sink_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstWasapiSink *self = GST_WASAPI_SINK (object); + + switch (prop_id) { + case PROP_ROLE: + g_value_set_enum (value, gst_wasapi_erole_to_device_role (self->role)); + break; + case PROP_MUTE: + g_value_set_boolean (value, self->mute); + break; + case PROP_DEVICE: + 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; + case PROP_LOW_LATENCY: + g_value_set_boolean (value, self->low_latency); + break; + case PROP_AUDIOCLIENT3: + g_value_set_boolean (value, self->try_audioclient3); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static gboolean +gst_wasapi_sink_can_audioclient3 (GstWasapiSink * self) +{ + /* AudioClient3 API only makes sense in shared mode */ + if (self->sharemode != AUDCLNT_SHAREMODE_SHARED) + return FALSE; + + if (!self->try_audioclient3) { + GST_INFO_OBJECT (self, "AudioClient3 disabled by user"); + return FALSE; + } + + if (!gst_wasapi_util_have_audioclient3 ()) { + GST_INFO_OBJECT (self, "AudioClient3 not available on this OS"); + 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 */ + if (!self->low_latency) { + GST_INFO_OBJECT (self, "AudioClient3 disabled because low-latency mode " + "was not requested"); + return FALSE; + } + + return TRUE; +} + +static GstCaps * +gst_wasapi_sink_get_caps (GstBaseSink * bsink, GstCaps * filter) +{ + GstWasapiSink *self = GST_WASAPI_SINK (bsink); + WAVEFORMATEX *format = NULL; + GstCaps *caps = NULL; + + GST_DEBUG_OBJECT (self, "entering get caps"); + + if (self->cached_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) { + caps = template_caps; + goto out; + } + + 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), + ("failed to detect format")); + gst_caps_unref (template_caps); + return NULL; + } + + gst_wasapi_util_parse_waveformatex ((WAVEFORMATEXTENSIBLE *) format, + template_caps, &caps, &self->positions); + if (caps == NULL) { + GST_ELEMENT_ERROR (self, STREAM, FORMAT, (NULL), ("unknown format")); + gst_caps_unref (template_caps); + return NULL; + } + + { + gchar *pos_str = gst_audio_channel_positions_to_string (self->positions, + format->nChannels); + GST_INFO_OBJECT (self, "positions are: %s", pos_str); + g_free (pos_str); + } + + self->mix_format = format; + gst_caps_replace (&self->cached_caps, caps); + gst_caps_unref (template_caps); + } + + if (filter) { + GstCaps *filtered = + gst_caps_intersect_full (filter, caps, GST_CAPS_INTERSECT_FIRST); + gst_caps_unref (caps); + caps = filtered; + } + +out: + GST_DEBUG_OBJECT (self, "returning caps %" GST_PTR_FORMAT, caps); + return caps; +} + +static gboolean +gst_wasapi_sink_open (GstAudioSink * asink) +{ + GstWasapiSink *self = GST_WASAPI_SINK (asink); + gboolean res = FALSE; + IMMDevice *device = NULL; + IAudioClient *client = NULL; + + GST_DEBUG_OBJECT (self, "opening device"); + + if (self->client) + return TRUE; + + /* FIXME: Switching the default device does not switch the stream to it, + * even if the old device was unplugged. We need to handle this somehow. + * 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 (self->enumerator, eRender, + self->role, self->device_strid, &device) + || !gst_wasapi_util_get_audio_client (GST_ELEMENT (self), + device, &client)) { + if (!self->device_strid) + GST_ELEMENT_ERROR (self, RESOURCE, OPEN_WRITE, (NULL), + ("Failed to get default device")); + else + GST_ELEMENT_ERROR (self, RESOURCE, OPEN_WRITE, (NULL), + ("Failed to open device %S", self->device_strid)); + goto beach; + } + + self->client = client; + self->device = device; + res = TRUE; + +beach: + + return res; +} + +static gboolean +gst_wasapi_sink_close (GstAudioSink * asink) +{ + 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; + } + + return TRUE; +} + +/* Get the empty space in the buffer that we have to write to */ +static gint +gst_wasapi_sink_get_can_frames (GstWasapiSink * self) +{ + HRESULT hr; + guint n_frames_padding; + + /* There is no padding in exclusive mode since there is no ringbuffer */ + if (self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) { + GST_DEBUG_OBJECT (self, "exclusive mode, can write: %i", + self->buffer_frame_count); + return self->buffer_frame_count; + } + + /* Frames the card hasn't rendered yet */ + hr = IAudioClient_GetCurrentPadding (self->client, &n_frames_padding); + HR_FAILED_ELEMENT_ERROR_RET (hr, IAudioClient::GetCurrentPadding, self, -1); + + GST_DEBUG_OBJECT (self, "%i unread frames (padding)", n_frames_padding); + + /* We can write out these many frames */ + return self->buffer_frame_count - n_frames_padding; +} + +static gboolean +gst_wasapi_sink_prepare (GstAudioSink * asink, GstAudioRingBufferSpec * spec) +{ + GstWasapiSink *self = GST_WASAPI_SINK (asink); + gboolean res = FALSE; + REFERENCE_TIME latency_rt; + guint bpf, rate, devicep_frames; + HRESULT hr; + + if (!self->client) { + GST_DEBUG_OBJECT (self, "no IAudioClient, creating a new one"); + if (!gst_wasapi_util_get_audio_client (GST_ELEMENT (self), + self->device, &self->client)) + goto beach; + } + + if (gst_wasapi_sink_can_audioclient3 (self)) { + if (!gst_wasapi_util_initialize_audioclient3 (GST_ELEMENT (self), spec, + (IAudioClient3 *) self->client, self->mix_format, self->low_latency, + FALSE, &devicep_frames)) + goto beach; + } else { + if (!gst_wasapi_util_initialize_audioclient (GST_ELEMENT (self), spec, + self->client, self->mix_format, self->sharemode, self->low_latency, + FALSE, &devicep_frames)) + goto beach; + } + + bpf = GST_AUDIO_INFO_BPF (&spec->info); + rate = GST_AUDIO_INFO_RATE (&spec->info); + + /* Total size of the allocated buffer that we will write to */ + hr = IAudioClient_GetBufferSize (self->client, &self->buffer_frame_count); + HR_FAILED_GOTO (hr, IAudioClient::GetBufferSize, beach); + + GST_INFO_OBJECT (self, "buffer size is %i frames, device period is %i " + "frames, bpf is %i bytes, rate is %i Hz", self->buffer_frame_count, + devicep_frames, bpf, rate); + + /* Actual latency-time/buffer-time will be different now */ + spec->segsize = devicep_frames * bpf; + + /* 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); + + /* Get latency for logging */ + hr = IAudioClient_GetStreamLatency (self->client, &latency_rt); + HR_FAILED_GOTO (hr, IAudioClient::GetStreamLatency, beach); + + GST_INFO_OBJECT (self, "wasapi stream latency: %" G_GINT64_FORMAT " (%" + G_GINT64_FORMAT "ms)", latency_rt, latency_rt / 10000); + + /* Set the event handler which will trigger writes */ + hr = IAudioClient_SetEventHandle (self->client, self->event_handle); + HR_FAILED_GOTO (hr, IAudioClient::SetEventHandle, beach); + + /* Get render sink client and start it up */ + if (!gst_wasapi_util_get_render_client (GST_ELEMENT (self), self->client, + &self->render_client)) { + goto beach; + } + + GST_INFO_OBJECT (self, "got render client"); + + /* To avoid start-up glitches, before starting the streaming, we fill the + * buffer with silence as recommended by the documentation: + * https://msdn.microsoft.com/en-us/library/windows/desktop/dd370879%28v=vs.85%29.aspx */ + { + gint n_frames, len; + gint16 *dst = NULL; + + n_frames = gst_wasapi_sink_get_can_frames (self); + if (n_frames < 1) { + GST_ELEMENT_ERROR (self, RESOURCE, WRITE, (NULL), + ("should have more than %i frames to write", n_frames)); + goto beach; + } + + len = n_frames * self->mix_format->nBlockAlign; + + hr = IAudioRenderClient_GetBuffer (self->render_client, n_frames, + (BYTE **) & dst); + HR_FAILED_GOTO (hr, IAudioRenderClient::GetBuffer, beach); + + GST_DEBUG_OBJECT (self, "pre-wrote %i bytes of silence", len); + + hr = IAudioRenderClient_ReleaseBuffer (self->render_client, n_frames, + AUDCLNT_BUFFERFLAGS_SILENT); + HR_FAILED_GOTO (hr, IAudioRenderClient::ReleaseBuffer, beach); + } + + hr = IAudioClient_Start (self->client); + HR_FAILED_GOTO (hr, IAudioClient::Start, beach); + self->client_needs_restart = FALSE; + + gst_audio_ring_buffer_set_channel_positions (GST_AUDIO_BASE_SINK + (self)->ringbuffer, self->positions); + + res = TRUE; + + /* reset cancellable event handle */ + ResetEvent (self->cancellable); + +beach: + /* unprepare() is not called if prepare() fails, but we want it to be, so call + * it manually when needed */ + if (!res) + gst_wasapi_sink_unprepare (asink); + + return res; +} + +static gboolean +gst_wasapi_sink_unprepare (GstAudioSink * asink) +{ + GstWasapiSink *self = GST_WASAPI_SINK (asink); + + if (self->client != NULL) { + IUnknown_Release (self->client); + self->client = NULL; + } + + if (self->render_client != NULL) { + IUnknown_Release (self->render_client); + self->render_client = NULL; + } + + return TRUE; +} + +static gint +gst_wasapi_sink_write (GstAudioSink * asink, gpointer data, guint length) +{ + GstWasapiSink *self = GST_WASAPI_SINK (asink); + HRESULT hr; + gint16 *dst = NULL; + DWORD dwWaitResult; + guint can_frames, have_frames, n_frames, write_len, written_len = 0; + HANDLE event_handle[2]; + + event_handle[0] = self->event_handle; + event_handle[1] = self->cancellable; + + GST_OBJECT_LOCK (self); + if (self->client_needs_restart) { + hr = IAudioClient_Start (self->client); + HR_FAILED_ELEMENT_ERROR_AND (hr, IAudioClient::Start, self, + GST_OBJECT_UNLOCK (self); goto err); + self->client_needs_restart = FALSE; + ResetEvent (self->cancellable); + } + GST_OBJECT_UNLOCK (self); + + /* We have N frames to be written out */ + have_frames = length / (self->mix_format->nBlockAlign); + + if (self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) { + /* In exclusive mode we have to wait always */ + dwWaitResult = WaitForMultipleObjects (2, event_handle, FALSE, INFINITE); + if (dwWaitResult != WAIT_OBJECT_0 && dwWaitResult != WAIT_OBJECT_0 + 1) { + GST_ERROR_OBJECT (self, "Error waiting for event handle: %x", + (guint) dwWaitResult); + goto err; + } + + /* ::reset was requested */ + if (dwWaitResult == WAIT_OBJECT_0 + 1) { + GST_DEBUG_OBJECT (self, "operation was cancelled"); + return -1; + } + + can_frames = gst_wasapi_sink_get_can_frames (self); + if (can_frames < 0) { + GST_ERROR_OBJECT (self, "Error getting frames to write to"); + goto err; + } + /* In exclusive mode we need to fill the whole buffer in one go or + * GetBuffer will error out */ + if (can_frames != have_frames) { + GST_ERROR_OBJECT (self, + "Need at %i frames to write for exclusive mode, but got %i", + can_frames, have_frames); + goto err; + } + } else { + /* In shared mode we can write parts of the buffer, so only wait + * in case we can't write anything */ + can_frames = gst_wasapi_sink_get_can_frames (self); + if (can_frames < 0) { + GST_ERROR_OBJECT (self, "Error getting frames to write to"); + goto err; + } + + if (can_frames == 0) { + dwWaitResult = WaitForMultipleObjects (2, event_handle, FALSE, INFINITE); + if (dwWaitResult != WAIT_OBJECT_0 && dwWaitResult != WAIT_OBJECT_0 + 1) { + GST_ERROR_OBJECT (self, "Error waiting for event handle: %x", + (guint) dwWaitResult); + goto err; + } + + /* ::reset was requested */ + if (dwWaitResult == WAIT_OBJECT_0 + 1) { + GST_DEBUG_OBJECT (self, "operation was cancelled"); + return -1; + } + + can_frames = gst_wasapi_sink_get_can_frames (self); + if (can_frames < 0) { + GST_ERROR_OBJECT (self, "Error getting frames to write to"); + goto err; + } + } + } + + /* 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_DEBUG_OBJECT (self, "total: %i, have_frames: %i (%i bytes), " + "can_frames: %i, will write: %i (%i bytes)", self->buffer_frame_count, + have_frames, length, can_frames, n_frames, write_len); + + hr = IAudioRenderClient_GetBuffer (self->render_client, n_frames, + (BYTE **) & dst); + HR_FAILED_ELEMENT_ERROR_AND (hr, IAudioRenderClient::GetBuffer, self, + goto err); + + memcpy (dst, data, write_len); + + hr = IAudioRenderClient_ReleaseBuffer (self->render_client, n_frames, + self->mute ? AUDCLNT_BUFFERFLAGS_SILENT : 0); + HR_FAILED_ELEMENT_ERROR_AND (hr, IAudioRenderClient::ReleaseBuffer, self, + goto err); + + written_len = write_len; + +out: + return written_len; + +err: + written_len = -1; + goto out; +} + +static guint +gst_wasapi_sink_delay (GstAudioSink * asink) +{ + GstWasapiSink *self = GST_WASAPI_SINK (asink); + guint delay = 0; + HRESULT hr; + + hr = IAudioClient_GetCurrentPadding (self->client, &delay); + HR_FAILED_RET (hr, IAudioClient::GetCurrentPadding, 0); + + return delay; +} + +static void +gst_wasapi_sink_reset (GstAudioSink * asink) +{ + GstWasapiSink *self = GST_WASAPI_SINK (asink); + HRESULT hr; + + GST_INFO_OBJECT (self, "reset called"); + + if (!self->client) + return; + + SetEvent (self->cancellable); + + GST_OBJECT_LOCK (self); + hr = IAudioClient_Stop (self->client); + HR_FAILED_AND (hr, IAudioClient::Stop, goto err); + + hr = IAudioClient_Reset (self->client); + HR_FAILED_AND (hr, IAudioClient::Reset, goto err); + +err: + self->client_needs_restart = TRUE; + GST_OBJECT_UNLOCK (self); +} diff --git a/sys/wasapi/gstwasapisink.h b/sys/wasapi/gstwasapisink.h new file mode 100644 index 0000000000..4aac69efc7 --- /dev/null +++ b/sys/wasapi/gstwasapisink.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2008 Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.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. + */ + +#ifndef __GST_WASAPI_SINK_H__ +#define __GST_WASAPI_SINK_H__ + +#include "gstwasapiutil.h" + +G_BEGIN_DECLS +#define GST_TYPE_WASAPI_SINK \ + (gst_wasapi_sink_get_type ()) +#define GST_WASAPI_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_WASAPI_SINK, GstWasapiSink)) +#define GST_WASAPI_SINK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_WASAPI_SINK, GstWasapiSinkClass)) +#define GST_IS_WASAPI_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_WASAPI_SINK)) +#define GST_IS_WASAPI_SINK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_WASAPI_SINK)) +typedef struct _GstWasapiSink GstWasapiSink; +typedef struct _GstWasapiSinkClass GstWasapiSinkClass; + +struct _GstWasapiSink +{ + GstAudioSink parent; + + GstMMDeviceEnumerator *enumerator; + + IMMDevice *device; + IAudioClient *client; + IAudioRenderClient *render_client; + HANDLE event_handle; + HANDLE cancellable; + /* Client was reset, so it needs to be started again */ + gboolean client_needs_restart; + + /* Actual size of the allocated buffer */ + guint buffer_frame_count; + /* The mix format that wasapi prefers in shared mode */ + WAVEFORMATEX *mix_format; + /* The probed caps that we can accept */ + GstCaps *cached_caps; + /* The channel positions in the data to be written to the device we + * will pass this to GstAudioRingbuffer so it can to it translate + * from the native GStreamer channel layout. */ + GstAudioChannelPosition *positions; + + /* properties */ + gint role; + gint sharemode; + gboolean mute; + gboolean low_latency; + gboolean try_audioclient3; + wchar_t *device_strid; +}; + +struct _GstWasapiSinkClass +{ + GstAudioSinkClass parent_class; +}; + +GType gst_wasapi_sink_get_type (void); + +G_END_DECLS +#endif /* __GST_WASAPI_SINK_H__ */ diff --git a/sys/wasapi/gstwasapisrc.c b/sys/wasapi/gstwasapisrc.c new file mode 100644 index 0000000000..8bdff3be08 --- /dev/null +++ b/sys/wasapi/gstwasapisrc.c @@ -0,0 +1,934 @@ +/* + * Copyright (C) 2008 Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.com> + * Copyright (C) 2018 Centricular Ltd. + * Author: Nirbheek Chauhan <nirbheek@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. + */ + +/** + * SECTION:element-wasapisrc + * @title: wasapisrc + * + * Provides audio capture from the Windows Audio Session API available with + * Vista and newer. + * + * ## Example pipelines + * |[ + * gst-launch-1.0 -v wasapisrc ! fakesink + * ]| Capture from the default audio device and render to fakesink. + * + * |[ + * gst-launch-1.0 -v wasapisrc low-latency=true ! fakesink + * ]| Capture from the default audio device with the minimum possible latency and render to fakesink. + * + */ +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "gstwasapisrc.h" + +#include <avrt.h> + +GST_DEBUG_CATEGORY_STATIC (gst_wasapi_src_debug); +#define GST_CAT_DEFAULT gst_wasapi_src_debug + +static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS (GST_WASAPI_STATIC_CAPS)); + +#define DEFAULT_ROLE GST_WASAPI_DEVICE_ROLE_CONSOLE +#define DEFAULT_LOOPBACK FALSE +#define DEFAULT_EXCLUSIVE FALSE +#define DEFAULT_LOW_LATENCY FALSE +#define DEFAULT_AUDIOCLIENT3 FALSE +/* The clock provided by WASAPI is always off and causes buffers to be late + * very quickly on the sink. Disable pending further investigation. */ +#define DEFAULT_PROVIDE_CLOCK FALSE + +enum +{ + PROP_0, + PROP_ROLE, + PROP_DEVICE, + PROP_LOOPBACK, + PROP_EXCLUSIVE, + PROP_LOW_LATENCY, + PROP_AUDIOCLIENT3 +}; + +static void gst_wasapi_src_dispose (GObject * object); +static void gst_wasapi_src_finalize (GObject * object); +static void gst_wasapi_src_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_wasapi_src_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); + +static GstCaps *gst_wasapi_src_get_caps (GstBaseSrc * bsrc, GstCaps * filter); + +static gboolean gst_wasapi_src_open (GstAudioSrc * asrc); +static gboolean gst_wasapi_src_close (GstAudioSrc * asrc); +static gboolean gst_wasapi_src_prepare (GstAudioSrc * asrc, + GstAudioRingBufferSpec * spec); +static gboolean gst_wasapi_src_unprepare (GstAudioSrc * asrc); +static guint gst_wasapi_src_read (GstAudioSrc * asrc, gpointer data, + guint length, GstClockTime * timestamp); +static guint gst_wasapi_src_delay (GstAudioSrc * asrc); +static void gst_wasapi_src_reset (GstAudioSrc * asrc); + +#if DEFAULT_PROVIDE_CLOCK +static GstClockTime gst_wasapi_src_get_time (GstClock * clock, + gpointer user_data); +#endif + +#define gst_wasapi_src_parent_class parent_class +G_DEFINE_TYPE (GstWasapiSrc, gst_wasapi_src, GST_TYPE_AUDIO_SRC); + +static void +gst_wasapi_src_class_init (GstWasapiSrcClass * klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GstElementClass *gstelement_class = GST_ELEMENT_CLASS (klass); + GstBaseSrcClass *gstbasesrc_class = GST_BASE_SRC_CLASS (klass); + GstAudioSrcClass *gstaudiosrc_class = GST_AUDIO_SRC_CLASS (klass); + + gobject_class->dispose = gst_wasapi_src_dispose; + gobject_class->finalize = gst_wasapi_src_finalize; + gobject_class->set_property = gst_wasapi_src_set_property; + gobject_class->get_property = gst_wasapi_src_get_property; + + g_object_class_install_property (gobject_class, + PROP_ROLE, + g_param_spec_enum ("role", "Role", + "Role of the device: communications, multimedia, etc", + GST_WASAPI_DEVICE_TYPE_ROLE, DEFAULT_ROLE, G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | GST_PARAM_MUTABLE_READY)); + + g_object_class_install_property (gobject_class, + PROP_DEVICE, + g_param_spec_string ("device", "Device", + "WASAPI playback device as a GUID string", + NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, + PROP_LOOPBACK, + g_param_spec_boolean ("loopback", "Loopback recording", + "Open the sink device for loopback recording", + DEFAULT_LOOPBACK, 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)); + + g_object_class_install_property (gobject_class, + PROP_LOW_LATENCY, + g_param_spec_boolean ("low-latency", "Low latency", + "Optimize all settings for lowest latency. Always safe to enable.", + DEFAULT_LOW_LATENCY, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, + PROP_AUDIOCLIENT3, + g_param_spec_boolean ("use-audioclient3", "Use the AudioClient3 API", + "Whether to use the Windows 10 AudioClient3 API when available", + DEFAULT_AUDIOCLIENT3, 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/Hardware", + "Stream audio from an audio capture device through WASAPI", + "Nirbheek Chauhan <nirbheek@centricular.com>, " + "Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.com>"); + + gstbasesrc_class->get_caps = GST_DEBUG_FUNCPTR (gst_wasapi_src_get_caps); + + gstaudiosrc_class->open = GST_DEBUG_FUNCPTR (gst_wasapi_src_open); + gstaudiosrc_class->close = GST_DEBUG_FUNCPTR (gst_wasapi_src_close); + gstaudiosrc_class->read = GST_DEBUG_FUNCPTR (gst_wasapi_src_read); + gstaudiosrc_class->prepare = GST_DEBUG_FUNCPTR (gst_wasapi_src_prepare); + gstaudiosrc_class->unprepare = GST_DEBUG_FUNCPTR (gst_wasapi_src_unprepare); + gstaudiosrc_class->delay = GST_DEBUG_FUNCPTR (gst_wasapi_src_delay); + gstaudiosrc_class->reset = GST_DEBUG_FUNCPTR (gst_wasapi_src_reset); + + GST_DEBUG_CATEGORY_INIT (gst_wasapi_src_debug, "wasapisrc", + 0, "Windows audio session API source"); + + gst_type_mark_as_plugin_api (GST_WASAPI_DEVICE_TYPE_ROLE, 0); +} + +static void +gst_wasapi_src_init (GstWasapiSrc * self) +{ +#if DEFAULT_PROVIDE_CLOCK + /* override with a custom clock */ + if (GST_AUDIO_BASE_SRC (self)->clock) + gst_object_unref (GST_AUDIO_BASE_SRC (self)->clock); + + GST_AUDIO_BASE_SRC (self)->clock = gst_audio_clock_new ("GstWasapiSrcClock", + gst_wasapi_src_get_time, gst_object_ref (self), + (GDestroyNotify) gst_object_unref); +#endif + + self->role = DEFAULT_ROLE; + self->sharemode = AUDCLNT_SHAREMODE_SHARED; + self->loopback = DEFAULT_LOOPBACK; + self->low_latency = DEFAULT_LOW_LATENCY; + self->try_audioclient3 = DEFAULT_AUDIOCLIENT3; + self->event_handle = CreateEvent (NULL, FALSE, FALSE, NULL); + self->cancellable = CreateEvent (NULL, TRUE, FALSE, NULL); + self->client_needs_restart = FALSE; + self->adapter = gst_adapter_new (); + + /* Extra event handles used for loopback */ + self->loopback_event_handle = CreateEvent (NULL, FALSE, FALSE, NULL); + self->loopback_cancellable = CreateEvent (NULL, TRUE, FALSE, NULL); + + self->enumerator = gst_mm_device_enumerator_new (); +} + +static void +gst_wasapi_src_dispose (GObject * object) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (object); + + if (self->event_handle != NULL) { + CloseHandle (self->event_handle); + self->event_handle = NULL; + } + + if (self->cancellable != NULL) { + CloseHandle (self->cancellable); + self->cancellable = NULL; + } + + if (self->client_clock != NULL) { + IUnknown_Release (self->client_clock); + self->client_clock = NULL; + } + + if (self->client != NULL) { + IUnknown_Release (self->client); + self->client = NULL; + } + + if (self->capture_client != NULL) { + IUnknown_Release (self->capture_client); + self->capture_client = NULL; + } + + if (self->loopback_client != NULL) { + IUnknown_Release (self->loopback_client); + self->loopback_client = NULL; + } + + if (self->loopback_event_handle != NULL) { + CloseHandle (self->loopback_event_handle); + self->loopback_event_handle = NULL; + } + + if (self->loopback_cancellable != NULL) { + CloseHandle (self->loopback_cancellable); + self->loopback_cancellable = NULL; + } + + gst_clear_object (&self->enumerator); + + G_OBJECT_CLASS (parent_class)->dispose (object); +} + +static void +gst_wasapi_src_finalize (GObject * object) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (object); + + CoTaskMemFree (self->mix_format); + self->mix_format = NULL; + + g_clear_pointer (&self->cached_caps, gst_caps_unref); + g_clear_pointer (&self->positions, g_free); + g_clear_pointer (&self->device_strid, g_free); + + g_object_unref (self->adapter); + self->adapter = NULL; + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +gst_wasapi_src_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (object); + + switch (prop_id) { + case PROP_ROLE: + self->role = gst_wasapi_device_role_to_erole (g_value_get_enum (value)); + break; + case PROP_DEVICE: + { + const gchar *device = g_value_get_string (value); + g_free (self->device_strid); + self->device_strid = + device ? g_utf8_to_utf16 (device, -1, NULL, NULL, NULL) : NULL; + break; + } + case PROP_LOOPBACK: + self->loopback = g_value_get_boolean (value); + break; + case PROP_EXCLUSIVE: + 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; + case PROP_AUDIOCLIENT3: + self->try_audioclient3 = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_wasapi_src_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (object); + + switch (prop_id) { + case PROP_ROLE: + g_value_set_enum (value, gst_wasapi_erole_to_device_role (self->role)); + break; + case PROP_DEVICE: + g_value_take_string (value, self->device_strid ? + g_utf16_to_utf8 (self->device_strid, -1, NULL, NULL, NULL) : NULL); + break; + case PROP_LOOPBACK: + g_value_set_boolean (value, self->loopback); + break; + case PROP_EXCLUSIVE: + g_value_set_boolean (value, + self->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE); + break; + case PROP_LOW_LATENCY: + g_value_set_boolean (value, self->low_latency); + break; + case PROP_AUDIOCLIENT3: + g_value_set_boolean (value, self->try_audioclient3); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static gboolean +gst_wasapi_src_can_audioclient3 (GstWasapiSrc * self) +{ + return (self->sharemode == AUDCLNT_SHAREMODE_SHARED && + self->try_audioclient3 && gst_wasapi_util_have_audioclient3 ()); +} + +static GstCaps * +gst_wasapi_src_get_caps (GstBaseSrc * bsrc, GstCaps * filter) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (bsrc); + WAVEFORMATEX *format = NULL; + GstCaps *caps = NULL; + + GST_DEBUG_OBJECT (self, "entering get caps"); + + if (self->cached_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) { + caps = template_caps; + goto out; + } + + 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), + ("failed to detect format")); + gst_caps_unref (template_caps); + return NULL; + } + + gst_wasapi_util_parse_waveformatex ((WAVEFORMATEXTENSIBLE *) format, + template_caps, &caps, &self->positions); + if (caps == NULL) { + GST_ELEMENT_ERROR (self, STREAM, FORMAT, (NULL), ("unknown format")); + gst_caps_unref (template_caps); + return NULL; + } + + { + gchar *pos_str = gst_audio_channel_positions_to_string (self->positions, + format->nChannels); + GST_INFO_OBJECT (self, "positions are: %s", pos_str); + g_free (pos_str); + } + + self->mix_format = format; + gst_caps_replace (&self->cached_caps, caps); + gst_caps_unref (template_caps); + } + + if (filter) { + GstCaps *filtered = + gst_caps_intersect_full (filter, caps, GST_CAPS_INTERSECT_FIRST); + gst_caps_unref (caps); + caps = filtered; + } + +out: + GST_DEBUG_OBJECT (self, "returning caps %" GST_PTR_FORMAT, caps); + return caps; +} + +static gboolean +gst_wasapi_src_open (GstAudioSrc * asrc) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (asrc); + gboolean res = FALSE; + IAudioClient *client = NULL; + IMMDevice *device = NULL; + IMMDevice *loopback_device = NULL; + + if (self->client) + return TRUE; + + /* FIXME: Switching the default device does not switch the stream to it, + * even if the old device was unplugged. We need to handle this somehow. + * 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 (self->enumerator, + self->loopback ? eRender : eCapture, self->role, self->device_strid, + &device) + || !gst_wasapi_util_get_audio_client (GST_ELEMENT (self), + device, &client)) { + if (!self->device_strid) + GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ, (NULL), + ("Failed to get default device")); + else + GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ, (NULL), + ("Failed to open device %S", self->device_strid)); + goto beach; + } + + /* An oddness of wasapi loopback feature is that capture client will not + * provide any audio data if there is no outputting sound. + * To workaround this problem, probably we can add timeout around loop + * in this case but it's glitch prone. So, instead of timeout, + * we will keep pusing silence data to into wasapi client so that make audio + * client report audio data in any case + */ + if (!gst_wasapi_util_get_device (self->enumerator, + eRender, self->role, self->device_strid, &loopback_device) + || !gst_wasapi_util_get_audio_client (GST_ELEMENT (self), + loopback_device, &self->loopback_client)) { + if (!self->device_strid) + GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ, (NULL), + ("Failed to get default device for loopback")); + else + GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ, (NULL), + ("Failed to open device %S", self->device_strid)); + goto beach; + + /* no need to hold this object */ + IUnknown_Release (loopback_device); + } + + self->client = client; + self->device = device; + res = TRUE; + +beach: + + return res; +} + +static gboolean +gst_wasapi_src_close (GstAudioSrc * asrc) +{ + 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; + } + + if (self->loopback_client != NULL) { + IUnknown_Release (self->loopback_client); + self->loopback_client = NULL; + } + + return TRUE; +} + +static gpointer +gst_wasapi_src_loopback_silence_feeding_thread (GstWasapiSrc * self) +{ + HRESULT hr; + UINT32 buffer_frames; + gboolean res G_GNUC_UNUSED = FALSE; + BYTE *data; + DWORD dwWaitResult; + HANDLE event_handle[2]; + UINT32 padding; + UINT32 n_frames; + + /* NOTE: if this task cause glitch, we need to consider thread priority + * adjusing. See gstaudioutilsprivate.c (e.g., AvSetMmThreadCharacteristics) + * for this context */ + + GST_INFO_OBJECT (self, "Run loopback silence feeding thread"); + + event_handle[0] = self->loopback_event_handle; + event_handle[1] = self->loopback_cancellable; + + hr = IAudioClient_GetBufferSize (self->loopback_client, &buffer_frames); + HR_FAILED_GOTO (hr, IAudioClient::GetBufferSize, beach); + + hr = IAudioClient_SetEventHandle (self->loopback_client, + self->loopback_event_handle); + HR_FAILED_GOTO (hr, IAudioClient::SetEventHandle, beach); + + /* To avoid start-up glitches, before starting the streaming, we fill the + * buffer with silence as recommended by the documentation: + * https://msdn.microsoft.com/en-us/library/windows/desktop/dd370879%28v=vs.85%29.aspx */ + hr = IAudioRenderClient_GetBuffer (self->loopback_render_client, + buffer_frames, &data); + HR_FAILED_GOTO (hr, IAudioRenderClient::GetBuffer, beach); + + hr = IAudioRenderClient_ReleaseBuffer (self->loopback_render_client, + buffer_frames, AUDCLNT_BUFFERFLAGS_SILENT); + HR_FAILED_GOTO (hr, IAudioRenderClient::ReleaseBuffer, beach); + + hr = IAudioClient_Start (self->loopback_client); + HR_FAILED_GOTO (hr, IAudioClock::Start, beach); + + /* There is an OS bug prior to Windows 10, that is loopback capture client + * will not receive event (in case of event-driven mode). + * A guide for workaround this case is that signal it whenever render client + * writes data. + * See https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudioclient-initialize + */ + + /* Signal for read thread to wakeup */ + SetEvent (self->event_handle); + + /* Ok, now we are ready for running for feeding silence data */ + while (1) { + dwWaitResult = WaitForMultipleObjects (2, event_handle, FALSE, INFINITE); + if (dwWaitResult != WAIT_OBJECT_0 && dwWaitResult != WAIT_OBJECT_0 + 1) { + GST_ERROR_OBJECT (self, "Error waiting for event handle: %x", + (guint) dwWaitResult); + goto stop; + } + + /* Stopping was requested from unprepare() */ + if (dwWaitResult == WAIT_OBJECT_0 + 1) { + GST_DEBUG_OBJECT (self, "operation was cancelled"); + goto stop; + } + + hr = IAudioClient_GetCurrentPadding (self->loopback_client, &padding); + HR_FAILED_GOTO (hr, IAudioClock::Start, stop); + + if (buffer_frames < padding) { + GST_WARNING_OBJECT (self, + "Current padding %d is too large (buffer size %d)", + padding, buffer_frames); + n_frames = 0; + } else { + n_frames = buffer_frames - padding; + } + + hr = IAudioRenderClient_GetBuffer (self->loopback_render_client, n_frames, + &data); + HR_FAILED_GOTO (hr, IAudioRenderClient::GetBuffer, stop); + + hr = IAudioRenderClient_ReleaseBuffer (self->loopback_render_client, + n_frames, AUDCLNT_BUFFERFLAGS_SILENT); + HR_FAILED_GOTO (hr, IAudioRenderClient::ReleaseBuffer, stop); + + /* Signal for read thread to wakeup */ + SetEvent (self->event_handle); + } + +stop: + IAudioClient_Stop (self->loopback_client); + +beach: + GST_INFO_OBJECT (self, "Terminate loopback silence feeding thread"); + + return NULL; +} + +static gboolean +gst_wasapi_src_prepare (GstAudioSrc * asrc, GstAudioRingBufferSpec * spec) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (asrc); + gboolean res = FALSE; + REFERENCE_TIME latency_rt; + guint bpf, rate, devicep_frames, buffer_frames; + HRESULT hr; + + if (gst_wasapi_src_can_audioclient3 (self)) { + if (!gst_wasapi_util_initialize_audioclient3 (GST_ELEMENT (self), spec, + (IAudioClient3 *) self->client, self->mix_format, self->low_latency, + self->loopback, &devicep_frames)) + goto beach; + } else { + if (!gst_wasapi_util_initialize_audioclient (GST_ELEMENT (self), spec, + self->client, self->mix_format, self->sharemode, self->low_latency, + self->loopback, &devicep_frames)) + goto beach; + } + + bpf = GST_AUDIO_INFO_BPF (&spec->info); + rate = GST_AUDIO_INFO_RATE (&spec->info); + + /* Total size in frames of the allocated buffer that we will read from */ + hr = IAudioClient_GetBufferSize (self->client, &buffer_frames); + HR_FAILED_GOTO (hr, IAudioClient::GetBufferSize, beach); + + GST_INFO_OBJECT (self, "buffer size is %i frames, device period is %i " + "frames, bpf is %i bytes, rate is %i Hz", buffer_frames, + devicep_frames, bpf, rate); + + /* Actual latency-time/buffer-time will be different now */ + spec->segsize = devicep_frames * bpf; + + /* We need a minimum of 2 segments to ensure glitch-free playback */ + spec->segtotal = MAX (buffer_frames * bpf / spec->segsize, 2); + + GST_INFO_OBJECT (self, "segsize is %i, segtotal is %i", spec->segsize, + spec->segtotal); + + /* Get WASAPI latency for logging */ + hr = IAudioClient_GetStreamLatency (self->client, &latency_rt); + HR_FAILED_GOTO (hr, IAudioClient::GetStreamLatency, beach); + + GST_INFO_OBJECT (self, "wasapi stream latency: %" G_GINT64_FORMAT " (%" + G_GINT64_FORMAT " ms)", latency_rt, latency_rt / 10000); + + /* Set the event handler which will trigger reads */ + hr = IAudioClient_SetEventHandle (self->client, self->event_handle); + HR_FAILED_GOTO (hr, IAudioClient::SetEventHandle, beach); + + /* Get the clock and the clock freq */ + if (!gst_wasapi_util_get_clock (GST_ELEMENT (self), self->client, + &self->client_clock)) + goto beach; + + hr = IAudioClock_GetFrequency (self->client_clock, &self->client_clock_freq); + HR_FAILED_GOTO (hr, IAudioClock::GetFrequency, beach); + + GST_INFO_OBJECT (self, "wasapi clock freq is %" G_GUINT64_FORMAT, + self->client_clock_freq); + + /* Get capture source client and start it up */ + if (!gst_wasapi_util_get_capture_client (GST_ELEMENT (self), self->client, + &self->capture_client)) { + goto beach; + } + + /* In case loopback, spawn another dedicated thread for feeding silence data + * into wasapi render client */ + if (self->loopback) { + /* don't need to be audioclient3 or low-latency since we will keep pushing + * silence data which is not varying over entire playback */ + if (!gst_wasapi_util_initialize_audioclient (GST_ELEMENT (self), spec, + self->loopback_client, self->mix_format, self->sharemode, + FALSE, FALSE, &devicep_frames)) + goto beach; + + if (!gst_wasapi_util_get_render_client (GST_ELEMENT (self), + self->loopback_client, &self->loopback_render_client)) { + goto beach; + } + + self->loopback_thread = g_thread_new ("wasapi-loopback", + (GThreadFunc) gst_wasapi_src_loopback_silence_feeding_thread, self); + } + + hr = IAudioClient_Start (self->client); + HR_FAILED_GOTO (hr, IAudioClock::Start, beach); + self->client_needs_restart = FALSE; + + gst_audio_ring_buffer_set_channel_positions (GST_AUDIO_BASE_SRC + (self)->ringbuffer, self->positions); + + res = TRUE; + + /* reset cancellable event handle */ + ResetEvent (self->cancellable); + +beach: + + /* unprepare() is not called if prepare() fails, but we want it to be, so call + * it manually when needed */ + if (!res) + gst_wasapi_src_unprepare (asrc); + + return res; +} + +static gboolean +gst_wasapi_src_unprepare (GstAudioSrc * asrc) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (asrc); + + if (self->client != NULL) { + IAudioClient_Stop (self->client); + } + + if (self->capture_client != NULL) { + IUnknown_Release (self->capture_client); + self->capture_client = NULL; + } + + if (self->client_clock != NULL) { + IUnknown_Release (self->client_clock); + self->client_clock = NULL; + } + + if (self->loopback_thread) { + GST_DEBUG_OBJECT (self, "loopback task thread is stopping"); + + SetEvent (self->loopback_cancellable); + + g_thread_join (self->loopback_thread); + self->loopback_thread = NULL; + ResetEvent (self->loopback_cancellable); + GST_DEBUG_OBJECT (self, "loopback task thread has been stopped"); + } + + if (self->loopback_render_client != NULL) { + IUnknown_Release (self->loopback_render_client); + self->loopback_render_client = NULL; + } + + self->client_clock_freq = 0; + + return TRUE; +} + +static guint +gst_wasapi_src_read (GstAudioSrc * asrc, gpointer data, guint length, + GstClockTime * timestamp) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (asrc); + HRESULT hr; + gint16 *from = NULL; + guint wanted = length; + guint bpf; + DWORD flags; + + GST_OBJECT_LOCK (self); + if (self->client_needs_restart) { + hr = IAudioClient_Start (self->client); + HR_FAILED_ELEMENT_ERROR_AND (hr, IAudioClient::Start, self, + GST_OBJECT_UNLOCK (self); goto err); + self->client_needs_restart = FALSE; + ResetEvent (self->cancellable); + gst_adapter_clear (self->adapter); + } + + bpf = self->mix_format->nBlockAlign; + GST_OBJECT_UNLOCK (self); + + /* If we've accumulated enough data, return it immediately */ + if (gst_adapter_available (self->adapter) >= wanted) { + memcpy (data, gst_adapter_map (self->adapter, wanted), wanted); + gst_adapter_flush (self->adapter, wanted); + GST_DEBUG_OBJECT (self, "Adapter has enough data, returning %i", wanted); + goto out; + } + + while (wanted > 0) { + DWORD dwWaitResult; + guint got_frames, avail_frames, n_frames, want_frames, read_len; + HANDLE event_handle[2]; + + event_handle[0] = self->event_handle; + event_handle[1] = self->cancellable; + + /* Wait for data to become available */ + dwWaitResult = WaitForMultipleObjects (2, event_handle, FALSE, INFINITE); + if (dwWaitResult != WAIT_OBJECT_0 && dwWaitResult != WAIT_OBJECT_0 + 1) { + GST_ERROR_OBJECT (self, "Error waiting for event handle: %x", + (guint) dwWaitResult); + goto err; + } + + /* ::reset was requested */ + if (dwWaitResult == WAIT_OBJECT_0 + 1) { + GST_DEBUG_OBJECT (self, "operation was cancelled"); + return -1; + } + + hr = IAudioCaptureClient_GetBuffer (self->capture_client, + (BYTE **) & from, &got_frames, &flags, NULL, NULL); + if (hr != S_OK) { + if (hr == AUDCLNT_S_BUFFER_EMPTY) { + gchar *msg = gst_wasapi_util_hresult_to_string (hr); + GST_WARNING_OBJECT (self, "IAudioCaptureClient::GetBuffer failed: %s" + ", retrying", msg); + g_free (msg); + length = 0; + goto out; + } + HR_FAILED_ELEMENT_ERROR_AND (hr, IAudioCaptureClient::GetBuffer, self, + goto err); + } + + if (G_UNLIKELY (flags != 0)) { + /* https://docs.microsoft.com/en-us/windows/win32/api/audioclient/ne-audioclient-_audclnt_bufferflags */ + if (flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) + GST_DEBUG_OBJECT (self, "WASAPI reported discontinuity (glitch?)"); + if (flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) + GST_DEBUG_OBJECT (self, "WASAPI reported a timestamp error"); + } + + /* Copy all the frames we got into the adapter, and then extract at most + * @wanted size of frames from it. This helps when ::GetBuffer returns more + * data than we can handle right now. */ + { + GstBuffer *tmp = gst_buffer_new_allocate (NULL, got_frames * bpf, NULL); + /* If flags has AUDCLNT_BUFFERFLAGS_SILENT, we will ignore the actual + * data and write out silence, see: + * https://docs.microsoft.com/en-us/windows/win32/api/audioclient/ne-audioclient-_audclnt_bufferflags */ + if (flags & AUDCLNT_BUFFERFLAGS_SILENT) + memset (from, 0, got_frames * bpf); + gst_buffer_fill (tmp, 0, from, got_frames * bpf); + gst_adapter_push (self->adapter, tmp); + } + + /* Release all captured buffers; we copied them above */ + hr = IAudioCaptureClient_ReleaseBuffer (self->capture_client, got_frames); + from = NULL; + HR_FAILED_ELEMENT_ERROR_AND (hr, IAudioCaptureClient::ReleaseBuffer, self, + goto err); + + want_frames = wanted / bpf; + avail_frames = gst_adapter_available (self->adapter) / bpf; + + /* Only copy data that will fit into the allocated buffer of size @length */ + n_frames = MIN (avail_frames, want_frames); + read_len = n_frames * bpf; + + GST_DEBUG_OBJECT (self, "frames captured: %i (%i bytes), " + "can read: %i (%i bytes), will read: %i (%i bytes), " + "adapter has: %i (%i bytes)", got_frames, got_frames * bpf, want_frames, + wanted, n_frames, read_len, avail_frames, avail_frames * bpf); + + memcpy (data, gst_adapter_map (self->adapter, read_len), read_len); + gst_adapter_flush (self->adapter, read_len); + wanted -= read_len; + } + + +out: + return length; + +err: + length = -1; + goto out; +} + +static guint +gst_wasapi_src_delay (GstAudioSrc * asrc) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (asrc); + guint delay = 0; + HRESULT hr; + + hr = IAudioClient_GetCurrentPadding (self->client, &delay); + HR_FAILED_RET (hr, IAudioClock::GetCurrentPadding, 0); + + return delay; +} + +static void +gst_wasapi_src_reset (GstAudioSrc * asrc) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (asrc); + HRESULT hr; + + if (!self->client) + return; + + SetEvent (self->cancellable); + + GST_OBJECT_LOCK (self); + hr = IAudioClient_Stop (self->client); + HR_FAILED_AND (hr, IAudioClock::Stop, goto err); + + hr = IAudioClient_Reset (self->client); + HR_FAILED_AND (hr, IAudioClock::Reset, goto err); + +err: + self->client_needs_restart = TRUE; + GST_OBJECT_UNLOCK (self); +} + +#if DEFAULT_PROVIDE_CLOCK +static GstClockTime +gst_wasapi_src_get_time (GstClock * clock, gpointer user_data) +{ + GstWasapiSrc *self = GST_WASAPI_SRC (user_data); + HRESULT hr; + guint64 devpos; + GstClockTime result; + + if (G_UNLIKELY (self->client_clock == NULL)) + return GST_CLOCK_TIME_NONE; + + hr = IAudioClock_GetPosition (self->client_clock, &devpos, NULL); + HR_FAILED_RET (hr, IAudioClock::GetPosition, GST_CLOCK_TIME_NONE); + + result = gst_util_uint64_scale_int (devpos, GST_SECOND, + self->client_clock_freq); + + /* + GST_DEBUG_OBJECT (self, "devpos = %" G_GUINT64_FORMAT + " frequency = %" G_GUINT64_FORMAT + " result = %" G_GUINT64_FORMAT " ms", + devpos, self->client_clock_freq, GST_TIME_AS_MSECONDS (result)); + */ + + return result; +} +#endif diff --git a/sys/wasapi/gstwasapisrc.h b/sys/wasapi/gstwasapisrc.h new file mode 100644 index 0000000000..8b78b7f173 --- /dev/null +++ b/sys/wasapi/gstwasapisrc.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2008 Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.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. + */ + +#ifndef __GST_WASAPI_SRC_H__ +#define __GST_WASAPI_SRC_H__ + +#include "gstwasapiutil.h" + +G_BEGIN_DECLS +#define GST_TYPE_WASAPI_SRC \ + (gst_wasapi_src_get_type ()) +#define GST_WASAPI_SRC(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_WASAPI_SRC, GstWasapiSrc)) +#define GST_WASAPI_SRC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_WASAPI_SRC, GstWasapiSrcClass)) +#define GST_IS_WASAPI_SRC(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_WASAPI_SRC)) +#define GST_IS_WASAPI_SRC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_WASAPI_SRC)) +typedef struct _GstWasapiSrc GstWasapiSrc; +typedef struct _GstWasapiSrcClass GstWasapiSrcClass; + +struct _GstWasapiSrc +{ + GstAudioSrc parent; + + GstMMDeviceEnumerator *enumerator; + + IMMDevice *device; + IAudioClient *client; + IAudioClock *client_clock; + guint64 client_clock_freq; + IAudioCaptureClient *capture_client; + HANDLE event_handle; + HANDLE cancellable; + /* Smooth frames captured from WASAPI, which can be irregular sometimes */ + GstAdapter *adapter; + /* Client was reset, so it needs to be started again */ + gboolean client_needs_restart; + + /* The mix format that wasapi prefers in shared mode */ + WAVEFORMATEX *mix_format; + /* The probed caps that we can accept */ + GstCaps *cached_caps; + /* The channel positions in the data read from the device + * we will pass this to GstAudioRingbuffer so it can + * translate it to the native GStreamer channel layout. */ + GstAudioChannelPosition *positions; + + /* Used for loopback use case in order to keep feeding silence into client */ + IAudioClient *loopback_client; + IAudioRenderClient *loopback_render_client; + GThread *loopback_thread; + HANDLE loopback_event_handle; + HANDLE loopback_cancellable; + + /* properties */ + gint role; + gint sharemode; + gboolean loopback; + gboolean low_latency; + gboolean try_audioclient3; + wchar_t *device_strid; +}; + +struct _GstWasapiSrcClass +{ + GstAudioSrcClass parent_class; +}; + +GType gst_wasapi_src_get_type (void); + +G_END_DECLS +#endif /* __GST_WASAPI_SRC_H__ */ diff --git a/sys/wasapi/gstwasapiutil.c b/sys/wasapi/gstwasapiutil.c new file mode 100644 index 0000000000..b59f5f1936 --- /dev/null +++ b/sys/wasapi/gstwasapiutil.c @@ -0,0 +1,964 @@ +/* + * Copyright (C) 2008 Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.com> + * Copyright (C) 2018 Centricular Ltd. + * Author: Nirbheek Chauhan <nirbheek@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. + */ +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +/* Note: initguid.h can not be included in gstwasapiutil.h, otherwise a + * symbol redefinition error will be raised. + * initguid.h must be included in the C file before mmdeviceapi.h + * which is included in gstwasapiutil.h. + */ +#ifdef _MSC_VER +#include <initguid.h> +#endif +#include "gstwasapiutil.h" +#include "gstwasapidevice.h" + +GST_DEBUG_CATEGORY_EXTERN (gst_wasapi_debug); +#define GST_CAT_DEFAULT gst_wasapi_debug + +/* This was only added to MinGW in ~2015 and our Cerbero toolchain is too old */ +#if defined(_MSC_VER) +#include <functiondiscoverykeys_devpkey.h> +#elif !defined(PKEY_Device_FriendlyName) +#include <initguid.h> +#include <propkey.h> +DEFINE_PROPERTYKEY (PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, + 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); +DEFINE_PROPERTYKEY (PKEY_AudioEngine_DeviceFormat, 0xf19f064d, 0x82c, 0x4e27, + 0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c, 0); +#endif + +/* __uuidof is only available in C++, so we hard-code the GUID values for all + * these. This is ok because these are ABI. */ +const CLSID CLSID_MMDeviceEnumerator = { 0xbcde0395, 0xe52f, 0x467c, + {0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e} +}; + +const IID IID_IMMDeviceEnumerator = { 0xa95664d2, 0x9614, 0x4f35, + {0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6} +}; + +const IID IID_IMMEndpoint = { 0x1be09788, 0x6894, 0x4089, + {0x85, 0x86, 0x9a, 0x2a, 0x6c, 0x26, 0x5a, 0xc5} +}; + +const IID IID_IAudioClient = { 0x1cb9ad4c, 0xdbfa, 0x4c32, + {0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2} +}; + +const IID IID_IAudioClient3 = { 0x7ed4ee07, 0x8e67, 0x4cd4, + {0x8c, 0x1a, 0x2b, 0x7a, 0x59, 0x87, 0xad, 0x42} +}; + +const IID IID_IAudioClock = { 0xcd63314f, 0x3fba, 0x4a1b, + {0x81, 0x2c, 0xef, 0x96, 0x35, 0x87, 0x28, 0xe7} +}; + +const IID IID_IAudioCaptureClient = { 0xc8adbd64, 0xe71e, 0x48a0, + {0xa4, 0xde, 0x18, 0x5c, 0x39, 0x5c, 0xd3, 0x17} +}; + +const IID IID_IAudioRenderClient = { 0xf294acfc, 0x3146, 0x4483, + {0xa7, 0xbf, 0xad, 0xdc, 0xa7, 0xc2, 0x60, 0xe2} +}; + +/* *INDENT-OFF* */ +static struct +{ + guint64 wasapi_pos; + GstAudioChannelPosition gst_pos; +} wasapi_to_gst_pos[] = { + {SPEAKER_FRONT_LEFT, GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT}, + {SPEAKER_FRONT_RIGHT, GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT}, + {SPEAKER_FRONT_CENTER, GST_AUDIO_CHANNEL_POSITION_FRONT_CENTER}, + {SPEAKER_LOW_FREQUENCY, GST_AUDIO_CHANNEL_POSITION_LFE1}, + {SPEAKER_BACK_LEFT, GST_AUDIO_CHANNEL_POSITION_REAR_LEFT}, + {SPEAKER_BACK_RIGHT, GST_AUDIO_CHANNEL_POSITION_REAR_RIGHT}, + {SPEAKER_FRONT_LEFT_OF_CENTER, + GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER}, + {SPEAKER_FRONT_RIGHT_OF_CENTER, + GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER}, + {SPEAKER_BACK_CENTER, GST_AUDIO_CHANNEL_POSITION_REAR_CENTER}, + /* Enum values diverge from this point onwards */ + {SPEAKER_SIDE_LEFT, GST_AUDIO_CHANNEL_POSITION_SIDE_LEFT}, + {SPEAKER_SIDE_RIGHT, GST_AUDIO_CHANNEL_POSITION_SIDE_RIGHT}, + {SPEAKER_TOP_CENTER, GST_AUDIO_CHANNEL_POSITION_TOP_CENTER}, + {SPEAKER_TOP_FRONT_LEFT, GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_LEFT}, + {SPEAKER_TOP_FRONT_CENTER, GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_CENTER}, + {SPEAKER_TOP_FRONT_RIGHT, GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_RIGHT}, + {SPEAKER_TOP_BACK_LEFT, GST_AUDIO_CHANNEL_POSITION_TOP_REAR_LEFT}, + {SPEAKER_TOP_BACK_CENTER, GST_AUDIO_CHANNEL_POSITION_TOP_REAR_CENTER}, + {SPEAKER_TOP_BACK_RIGHT, GST_AUDIO_CHANNEL_POSITION_TOP_REAR_RIGHT} +}; +/* *INDENT-ON* */ + +static int windows_major_version = 0; + +gboolean +gst_wasapi_util_have_audioclient3 (void) +{ + if (windows_major_version > 0) + return windows_major_version == 10; + + if (g_getenv ("GST_WASAPI_DISABLE_AUDIOCLIENT3") != NULL) { + windows_major_version = 6; + return FALSE; + } + + /* https://msdn.microsoft.com/en-us/library/windows/desktop/ms724834(v=vs.85).aspx */ + windows_major_version = 6; + if (g_win32_check_windows_version (10, 0, 0, G_WIN32_OS_ANY)) + windows_major_version = 10; + + return windows_major_version == 10; +} + +GType +gst_wasapi_device_role_get_type (void) +{ + static const GEnumValue values[] = { + {GST_WASAPI_DEVICE_ROLE_CONSOLE, + "Games, system notifications, voice commands", "console"}, + {GST_WASAPI_DEVICE_ROLE_MULTIMEDIA, "Music, movies, recorded media", + "multimedia"}, + {GST_WASAPI_DEVICE_ROLE_COMMS, "Voice communications", "comms"}, + {0, NULL, NULL} + }; + static GType id = 0; + + if (g_once_init_enter ((gsize *) & id)) { + GType _id; + + _id = g_enum_register_static ("GstWasapiDeviceRole", values); + + g_once_init_leave ((gsize *) & id, _id); + } + + return id; +} + +gint +gst_wasapi_device_role_to_erole (gint role) +{ + switch (role) { + case GST_WASAPI_DEVICE_ROLE_CONSOLE: + return eConsole; + case GST_WASAPI_DEVICE_ROLE_MULTIMEDIA: + return eMultimedia; + case GST_WASAPI_DEVICE_ROLE_COMMS: + return eCommunications; + default: + g_assert_not_reached (); + } + + return -1; +} + +gint +gst_wasapi_erole_to_device_role (gint erole) +{ + switch (erole) { + case eConsole: + return GST_WASAPI_DEVICE_ROLE_CONSOLE; + case eMultimedia: + return GST_WASAPI_DEVICE_ROLE_MULTIMEDIA; + case eCommunications: + return GST_WASAPI_DEVICE_ROLE_COMMS; + default: + g_assert_not_reached (); + } + + return -1; +} + +static const gchar * +hresult_to_string_fallback (HRESULT hr) +{ + const gchar *s = "unknown error"; + + switch (hr) { + case AUDCLNT_E_NOT_INITIALIZED: + s = "AUDCLNT_E_NOT_INITIALIZED"; + break; + case AUDCLNT_E_ALREADY_INITIALIZED: + s = "AUDCLNT_E_ALREADY_INITIALIZED"; + break; + case AUDCLNT_E_WRONG_ENDPOINT_TYPE: + s = "AUDCLNT_E_WRONG_ENDPOINT_TYPE"; + break; + case AUDCLNT_E_DEVICE_INVALIDATED: + s = "AUDCLNT_E_DEVICE_INVALIDATED"; + break; + case AUDCLNT_E_NOT_STOPPED: + s = "AUDCLNT_E_NOT_STOPPED"; + break; + case AUDCLNT_E_BUFFER_TOO_LARGE: + s = "AUDCLNT_E_BUFFER_TOO_LARGE"; + break; + case AUDCLNT_E_OUT_OF_ORDER: + s = "AUDCLNT_E_OUT_OF_ORDER"; + break; + case AUDCLNT_E_UNSUPPORTED_FORMAT: + s = "AUDCLNT_E_UNSUPPORTED_FORMAT"; + break; + case AUDCLNT_E_INVALID_DEVICE_PERIOD: + s = "AUDCLNT_E_INVALID_DEVICE_PERIOD"; + break; + case AUDCLNT_E_INVALID_SIZE: + s = "AUDCLNT_E_INVALID_SIZE"; + break; + case AUDCLNT_E_DEVICE_IN_USE: + s = "AUDCLNT_E_DEVICE_IN_USE"; + break; + case AUDCLNT_E_BUFFER_OPERATION_PENDING: + s = "AUDCLNT_E_BUFFER_OPERATION_PENDING"; + break; + case AUDCLNT_E_BUFFER_SIZE_ERROR: + s = "AUDCLNT_E_BUFFER_SIZE_ERROR"; + break; + case AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED: + s = "AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED"; + break; + case AUDCLNT_E_THREAD_NOT_REGISTERED: + s = "AUDCLNT_E_THREAD_NOT_REGISTERED"; + break; + case AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED: + s = "AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED"; + break; + case AUDCLNT_E_ENDPOINT_CREATE_FAILED: + s = "AUDCLNT_E_ENDPOINT_CREATE_FAILED"; + break; + case AUDCLNT_E_SERVICE_NOT_RUNNING: + s = "AUDCLNT_E_SERVICE_NOT_RUNNING"; + break; + case AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED: + s = "AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED"; + break; + case AUDCLNT_E_EXCLUSIVE_MODE_ONLY: + s = "AUDCLNT_E_EXCLUSIVE_MODE_ONLY"; + break; + case AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL: + s = "AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL"; + break; + case AUDCLNT_E_EVENTHANDLE_NOT_SET: + s = "AUDCLNT_E_EVENTHANDLE_NOT_SET"; + break; + case AUDCLNT_E_INCORRECT_BUFFER_SIZE: + s = "AUDCLNT_E_INCORRECT_BUFFER_SIZE"; + break; + case AUDCLNT_E_CPUUSAGE_EXCEEDED: + s = "AUDCLNT_E_CPUUSAGE_EXCEEDED"; + break; + case AUDCLNT_S_BUFFER_EMPTY: + s = "AUDCLNT_S_BUFFER_EMPTY"; + break; + case AUDCLNT_S_THREAD_ALREADY_REGISTERED: + s = "AUDCLNT_S_THREAD_ALREADY_REGISTERED"; + break; + case AUDCLNT_S_POSITION_STALLED: + s = "AUDCLNT_S_POSITION_STALLED"; + break; + case E_POINTER: + s = "E_POINTER"; + break; + case E_INVALIDARG: + s = "E_INVALIDARG"; + break; + } + + return s; +} + +gchar * +gst_wasapi_util_hresult_to_string (HRESULT hr) +{ + gchar *error_text = NULL; + + error_text = g_win32_error_message ((gint) hr); + /* g_win32_error_message() seems to be returning empty string for + * AUDCLNT_* cases */ + if (!error_text || strlen (error_text) == 0) { + g_free (error_text); + error_text = g_strdup (hresult_to_string_fallback (hr)); + } + + return error_text; +} + +gboolean +gst_wasapi_util_get_devices (GstMMDeviceEnumerator * self, + gboolean active, GList ** devices) +{ + gboolean res = FALSE; + static GstStaticCaps scaps = GST_STATIC_CAPS (GST_WASAPI_STATIC_CAPS); + DWORD dwStateMask = active ? DEVICE_STATE_ACTIVE : DEVICE_STATEMASK_ALL; + IMMDeviceCollection *device_collection = NULL; + IMMDeviceEnumerator *enum_handle = NULL; + const gchar *device_class, *element_name; + guint ii, count; + HRESULT hr; + + *devices = NULL; + + if (!self) + return FALSE; + + enum_handle = gst_mm_device_enumerator_get_handle (self); + if (!enum_handle) + return FALSE; + + hr = IMMDeviceEnumerator_EnumAudioEndpoints (enum_handle, eAll, dwStateMask, + &device_collection); + HR_FAILED_GOTO (hr, IMMDeviceEnumerator::EnumAudioEndpoints, err); + + hr = IMMDeviceCollection_GetCount (device_collection, &count); + HR_FAILED_GOTO (hr, IMMDeviceCollection::GetCount, err); + + /* Create a GList of GstDevices* to return */ + for (ii = 0; ii < count; ii++) { + IMMDevice *item = NULL; + IMMEndpoint *endpoint = NULL; + IAudioClient *client = NULL; + IPropertyStore *prop_store = NULL; + WAVEFORMATEX *format = NULL; + gchar *description = NULL; + gchar *strid = NULL; + EDataFlow dataflow; + PROPVARIANT var; + wchar_t *wstrid; + GstDevice *device; + GstStructure *props; + GstCaps *caps; + + hr = IMMDeviceCollection_Item (device_collection, ii, &item); + if (hr != S_OK) + continue; + + hr = IMMDevice_QueryInterface (item, &IID_IMMEndpoint, (void **) &endpoint); + if (hr != S_OK) + goto next; + + hr = IMMEndpoint_GetDataFlow (endpoint, &dataflow); + if (hr != S_OK) + goto next; + + if (dataflow == eRender) { + device_class = "Audio/Sink"; + element_name = "wasapisink"; + } else { + device_class = "Audio/Source"; + element_name = "wasapisrc"; + } + + PropVariantInit (&var); + + hr = IMMDevice_GetId (item, &wstrid); + if (hr != S_OK) + goto next; + strid = g_utf16_to_utf8 (wstrid, -1, NULL, NULL, NULL); + CoTaskMemFree (wstrid); + + hr = IMMDevice_OpenPropertyStore (item, STGM_READ, &prop_store); + if (hr != S_OK) + goto next; + + /* NOTE: More properties can be added as needed from here: + * https://msdn.microsoft.com/en-us/library/windows/desktop/dd370794(v=vs.85).aspx */ + hr = IPropertyStore_GetValue (prop_store, &PKEY_Device_FriendlyName, &var); + if (hr != S_OK) + goto next; + description = g_utf16_to_utf8 (var.pwszVal, -1, NULL, NULL, NULL); + PropVariantClear (&var); + + /* Get the audio client so we can fetch the mix format for shared mode + * to get the device format for exclusive mode (or something close to that) + * fetch PKEY_AudioEngine_DeviceFormat from the property store. */ + hr = IMMDevice_Activate (item, &IID_IAudioClient, CLSCTX_ALL, NULL, + (void **) &client); + if (hr != S_OK) { + gchar *msg = gst_wasapi_util_hresult_to_string (hr); + GST_ERROR_OBJECT (self, "IMMDevice::Activate (IID_IAudioClient) failed" + "on %s: %s", strid, msg); + g_free (msg); + goto next; + } + + hr = IAudioClient_GetMixFormat (client, &format); + if (hr != S_OK || format == NULL) { + gchar *msg = gst_wasapi_util_hresult_to_string (hr); + GST_ERROR_OBJECT (self, "GetMixFormat failed on %s: %s", strid, msg); + g_free (msg); + goto next; + } + + if (!gst_wasapi_util_parse_waveformatex ((WAVEFORMATEXTENSIBLE *) format, + gst_static_caps_get (&scaps), &caps, NULL)) + goto next; + + /* Set some useful properties */ + props = gst_structure_new ("wasapi-proplist", + "device.api", G_TYPE_STRING, "wasapi", + "device.strid", G_TYPE_STRING, GST_STR_NULL (strid), + "wasapi.device.description", G_TYPE_STRING, description, NULL); + + device = g_object_new (GST_TYPE_WASAPI_DEVICE, "device", strid, + "display-name", description, "caps", caps, + "device-class", device_class, "properties", props, NULL); + GST_WASAPI_DEVICE (device)->element = element_name; + + gst_structure_free (props); + gst_caps_unref (caps); + *devices = g_list_prepend (*devices, device); + + next: + PropVariantClear (&var); + if (prop_store) + IUnknown_Release (prop_store); + if (endpoint) + IUnknown_Release (endpoint); + if (client) + IUnknown_Release (client); + if (item) + IUnknown_Release (item); + if (description) + g_free (description); + if (strid) + g_free (strid); + } + + res = TRUE; + +err: + if (device_collection) + IUnknown_Release (device_collection); + return res; +} + +gboolean +gst_wasapi_util_get_device_format (GstElement * self, + gint device_mode, IMMDevice * device, IAudioClient * client, + WAVEFORMATEX ** ret_format) +{ + WAVEFORMATEX *format; + HRESULT hr; + + *ret_format = NULL; + + hr = IAudioClient_GetMixFormat (client, &format); + HR_FAILED_RET (hr, IAudioClient::GetMixFormat, 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); + HR_FAILED_RET (hr, IMMDevice::OpenPropertyStore, FALSE); + + hr = IPropertyStore_GetValue (prop_store, &PKEY_AudioEngine_DeviceFormat, + &var); + if (hr != S_OK) { + gchar *msg = gst_wasapi_util_hresult_to_string (hr); + GST_ERROR_OBJECT (self, "GetValue failed: %s", msg); + g_free (msg); + 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 (self, "AudioEngine DeviceFormat not supported"); + free (format); + return FALSE; + +out: + *ret_format = format; + return TRUE; +} + +gboolean +gst_wasapi_util_get_device (GstMMDeviceEnumerator * self, + gint data_flow, gint role, const wchar_t * device_strid, + IMMDevice ** ret_device) +{ + gboolean res = FALSE; + HRESULT hr; + IMMDeviceEnumerator *enum_handle = NULL; + IMMDevice *device = NULL; + + if (!self) + return FALSE; + + enum_handle = gst_mm_device_enumerator_get_handle (self); + if (!enum_handle) + return FALSE; + + if (!device_strid) { + hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint (enum_handle, data_flow, + role, &device); + HR_FAILED_GOTO (hr, IMMDeviceEnumerator::GetDefaultAudioEndpoint, beach); + } else { + hr = IMMDeviceEnumerator_GetDevice (enum_handle, device_strid, &device); + if (hr != S_OK) { + gchar *msg = gst_wasapi_util_hresult_to_string (hr); + GST_ERROR_OBJECT (self, "IMMDeviceEnumerator::GetDevice (%S) failed" + ": %s", device_strid, msg); + g_free (msg); + goto beach; + } + } + + IUnknown_AddRef (device); + *ret_device = device; + + res = TRUE; + +beach: + if (device != NULL) + IUnknown_Release (device); + + return res; +} + +gboolean +gst_wasapi_util_get_audio_client (GstElement * self, + IMMDevice * device, IAudioClient ** ret_client) +{ + IAudioClient *client = NULL; + gboolean res = FALSE; + HRESULT hr; + + if (gst_wasapi_util_have_audioclient3 ()) + hr = IMMDevice_Activate (device, &IID_IAudioClient3, CLSCTX_ALL, NULL, + (void **) &client); + else + hr = IMMDevice_Activate (device, &IID_IAudioClient, CLSCTX_ALL, NULL, + (void **) &client); + HR_FAILED_GOTO (hr, IMMDevice::Activate (IID_IAudioClient), beach); + + IUnknown_AddRef (client); + *ret_client = client; + + res = TRUE; + +beach: + if (client != NULL) + IUnknown_Release (client); + + return res; +} + +gboolean +gst_wasapi_util_get_render_client (GstElement * self, IAudioClient * client, + IAudioRenderClient ** ret_render_client) +{ + gboolean res = FALSE; + HRESULT hr; + IAudioRenderClient *render_client = NULL; + + hr = IAudioClient_GetService (client, &IID_IAudioRenderClient, + (void **) &render_client); + HR_FAILED_GOTO (hr, IAudioClient::GetService, beach); + + *ret_render_client = render_client; + res = TRUE; + +beach: + return res; +} + +gboolean +gst_wasapi_util_get_capture_client (GstElement * self, IAudioClient * client, + IAudioCaptureClient ** ret_capture_client) +{ + gboolean res = FALSE; + HRESULT hr; + IAudioCaptureClient *capture_client = NULL; + + hr = IAudioClient_GetService (client, &IID_IAudioCaptureClient, + (void **) &capture_client); + HR_FAILED_GOTO (hr, IAudioClient::GetService, beach); + + *ret_capture_client = capture_client; + res = TRUE; + +beach: + return res; +} + +gboolean +gst_wasapi_util_get_clock (GstElement * self, IAudioClient * client, + IAudioClock ** ret_clock) +{ + gboolean res = FALSE; + HRESULT hr; + IAudioClock *clock = NULL; + + hr = IAudioClient_GetService (client, &IID_IAudioClock, (void **) &clock); + HR_FAILED_GOTO (hr, IAudioClient::GetService, beach); + + *ret_clock = clock; + res = TRUE; + +beach: + return res; +} + +static const gchar * +gst_waveformatex_to_audio_format (WAVEFORMATEXTENSIBLE * format) +{ + const gchar *fmt_str = NULL; + GstAudioFormat fmt = GST_AUDIO_FORMAT_UNKNOWN; + + if (format->Format.wFormatTag == WAVE_FORMAT_PCM) { + fmt = gst_audio_format_build_integer (TRUE, G_LITTLE_ENDIAN, + format->Format.wBitsPerSample, format->Format.wBitsPerSample); + } else if (format->Format.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) { + if (format->Format.wBitsPerSample == 32) + fmt = GST_AUDIO_FORMAT_F32LE; + else if (format->Format.wBitsPerSample == 64) + fmt = GST_AUDIO_FORMAT_F64LE; + } else if (format->Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE) { + if (IsEqualGUID (&format->SubFormat, &KSDATAFORMAT_SUBTYPE_PCM)) { + fmt = gst_audio_format_build_integer (TRUE, G_LITTLE_ENDIAN, + format->Format.wBitsPerSample, format->Samples.wValidBitsPerSample); + } else if (IsEqualGUID (&format->SubFormat, + &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) { + if (format->Format.wBitsPerSample == 32 + && format->Samples.wValidBitsPerSample == 32) + fmt = GST_AUDIO_FORMAT_F32LE; + else if (format->Format.wBitsPerSample == 64 && + format->Samples.wValidBitsPerSample == 64) + fmt = GST_AUDIO_FORMAT_F64LE; + } + } + + if (fmt != GST_AUDIO_FORMAT_UNKNOWN) + fmt_str = gst_audio_format_to_string (fmt); + + return fmt_str; +} + +static void +gst_wasapi_util_channel_position_all_none (guint channels, + GstAudioChannelPosition * position) +{ + int ii; + for (ii = 0; ii < channels; ii++) + position[ii] = GST_AUDIO_CHANNEL_POSITION_NONE; +} + +/* Parse WAVEFORMATEX to get the gstreamer channel mask, and the wasapi channel + * positions so GstAudioRingbuffer can reorder the audio data to match the + * gstreamer channel order. */ +static guint64 +gst_wasapi_util_waveformatex_to_channel_mask (WAVEFORMATEXTENSIBLE * format, + GstAudioChannelPosition ** out_position) +{ + int ii, ch; + guint64 mask = 0; + WORD nChannels = format->Format.nChannels; + DWORD dwChannelMask = format->dwChannelMask; + GstAudioChannelPosition *pos = NULL; + + pos = g_new (GstAudioChannelPosition, nChannels); + gst_wasapi_util_channel_position_all_none (nChannels, pos); + + /* Too many channels, have to assume that they are all non-positional */ + if (nChannels > G_N_ELEMENTS (wasapi_to_gst_pos)) { + GST_INFO ("Got too many (%i) channels, assuming non-positional", nChannels); + goto out; + } + + /* Too many bits in the channel mask, and the bits don't match nChannels */ + if (dwChannelMask >> (G_N_ELEMENTS (wasapi_to_gst_pos) + 1) != 0) { + GST_WARNING ("Too many bits in channel mask (%lu), assuming " + "non-positional", dwChannelMask); + goto out; + } + + /* Map WASAPI's channel mask to Gstreamer's channel mask and positions. + * If the no. of bits in the mask > nChannels, we will ignore the extra. */ + for (ii = 0, ch = 0; ii < G_N_ELEMENTS (wasapi_to_gst_pos) && ch < nChannels; + ii++) { + if (!(dwChannelMask & wasapi_to_gst_pos[ii].wasapi_pos)) + /* no match, try next */ + continue; + mask |= G_GUINT64_CONSTANT (1) << wasapi_to_gst_pos[ii].gst_pos; + pos[ch++] = wasapi_to_gst_pos[ii].gst_pos; + } + + /* XXX: Warn if some channel masks couldn't be mapped? */ + + GST_DEBUG ("Converted WASAPI mask 0x%" G_GINT64_MODIFIER "x -> 0x%" + G_GINT64_MODIFIER "x", (guint64) dwChannelMask, (guint64) mask); + +out: + if (out_position) + *out_position = pos; + return mask; +} + +gboolean +gst_wasapi_util_parse_waveformatex (WAVEFORMATEXTENSIBLE * format, + GstCaps * template_caps, GstCaps ** out_caps, + GstAudioChannelPosition ** out_positions) +{ + int ii; + const gchar *afmt; + guint64 channel_mask; + + *out_caps = NULL; + + /* TODO: handle SPDIF and other encoded formats */ + + /* 1 or 2 channels <= 16 bits sample size OR + * 1 or 2 channels > 16 bits sample size or >2 channels */ + if (format->Format.wFormatTag != WAVE_FORMAT_PCM && + format->Format.wFormatTag != WAVE_FORMAT_IEEE_FLOAT && + format->Format.wFormatTag != WAVE_FORMAT_EXTENSIBLE) + /* Unhandled format tag */ + return FALSE; + + /* WASAPI can only tell us one canonical mix format that it will accept. The + * alternative is calling IsFormatSupported on all combinations of formats. + * Instead, it's simpler and faster to require conversion inside gstreamer */ + afmt = gst_waveformatex_to_audio_format (format); + if (afmt == NULL) + return FALSE; + + *out_caps = gst_caps_copy (template_caps); + + /* This will always return something that might be usable */ + channel_mask = + gst_wasapi_util_waveformatex_to_channel_mask (format, out_positions); + + for (ii = 0; ii < gst_caps_get_size (*out_caps); ii++) { + GstStructure *s = gst_caps_get_structure (*out_caps, ii); + + gst_structure_set (s, + "format", G_TYPE_STRING, afmt, + "channels", G_TYPE_INT, format->Format.nChannels, + "rate", G_TYPE_INT, format->Format.nSamplesPerSec, NULL); + + if (channel_mask) { + gst_structure_set (s, + "channel-mask", GST_TYPE_BITMASK, channel_mask, NULL); + } + } + + 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; +} + +gboolean +gst_wasapi_util_initialize_audioclient (GstElement * self, + GstAudioRingBufferSpec * spec, IAudioClient * client, + WAVEFORMATEX * format, guint sharemode, gboolean low_latency, + gboolean loopback, guint * ret_devicep_frames) +{ + REFERENCE_TIME default_period, min_period; + REFERENCE_TIME device_period, device_buffer_duration; + guint rate, stream_flags; + guint32 n_frames; + HRESULT hr; + + hr = IAudioClient_GetDevicePeriod (client, &default_period, &min_period); + HR_FAILED_RET (hr, IAudioClient::GetDevicePeriod, FALSE); + + GST_INFO_OBJECT (self, "wasapi default period: %" G_GINT64_FORMAT + ", min period: %" G_GINT64_FORMAT, default_period, min_period); + + rate = GST_AUDIO_INFO_RATE (&spec->info); + + if (low_latency) { + if (sharemode == AUDCLNT_SHAREMODE_SHARED) { + device_period = default_period; + device_buffer_duration = 0; + } else { + device_period = min_period; + device_buffer_duration = min_period; + } + } else { + /* Clamp values to integral multiples of an appropriate period */ + gst_wasapi_util_get_best_buffer_sizes (spec, + sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE, default_period, + min_period, &device_period, &device_buffer_duration); + } + + stream_flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK; + if (loopback) + stream_flags |= AUDCLNT_STREAMFLAGS_LOOPBACK; + + hr = IAudioClient_Initialize (client, sharemode, stream_flags, + device_buffer_duration, + /* This must always be 0 in shared mode */ + sharemode == AUDCLNT_SHAREMODE_SHARED ? 0 : device_period, format, NULL); + + if (hr == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED && + sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) { + GST_WARNING_OBJECT (self, "initialize failed due to unaligned period %i", + (int) device_period); + + /* Calculate a new aligned period. First get the aligned buffer size. */ + hr = IAudioClient_GetBufferSize (client, &n_frames); + HR_FAILED_RET (hr, IAudioClient::GetBufferSize, FALSE); + + device_period = (GST_SECOND / 100) * n_frames / rate; + + GST_WARNING_OBJECT (self, "trying to re-initialize with period %i " + "(%i frames, %i rate)", (int) device_period, n_frames, rate); + + hr = IAudioClient_Initialize (client, sharemode, stream_flags, + device_period, device_period, format, NULL); + } + HR_FAILED_RET (hr, IAudioClient::Initialize, FALSE); + + if (sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) { + /* We use the device period for the segment size and that needs to match + * the buffer size exactly when we write into it */ + hr = IAudioClient_GetBufferSize (client, &n_frames); + HR_FAILED_RET (hr, IAudioClient::GetBufferSize, FALSE); + + *ret_devicep_frames = n_frames; + } else { + /* device_period can be a non-power-of-10 value so round while converting */ + *ret_devicep_frames = + gst_util_uint64_scale_round (device_period, rate * 100, GST_SECOND); + } + + return TRUE; +} + +gboolean +gst_wasapi_util_initialize_audioclient3 (GstElement * self, + GstAudioRingBufferSpec * spec, IAudioClient3 * client, + WAVEFORMATEX * format, gboolean low_latency, gboolean loopback, + guint * ret_devicep_frames) +{ + HRESULT hr; + gint stream_flags; + guint devicep_frames; + guint defaultp_frames, fundp_frames, minp_frames, maxp_frames; + WAVEFORMATEX *tmpf; + + hr = IAudioClient3_GetSharedModeEnginePeriod (client, format, + &defaultp_frames, &fundp_frames, &minp_frames, &maxp_frames); + HR_FAILED_RET (hr, IAudioClient3::GetSharedModeEnginePeriod, FALSE); + + GST_INFO_OBJECT (self, "Using IAudioClient3, default period %i frames, " + "fundamental period %i frames, minimum period %i frames, maximum period " + "%i frames", defaultp_frames, fundp_frames, minp_frames, maxp_frames); + + if (low_latency) + devicep_frames = minp_frames; + else + /* Just pick the max period, because lower values can cause glitches + * https://bugzilla.gnome.org/show_bug.cgi?id=794497 */ + devicep_frames = maxp_frames; + + stream_flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK; + if (loopback) + stream_flags |= AUDCLNT_STREAMFLAGS_LOOPBACK; + + hr = IAudioClient3_InitializeSharedAudioStream (client, stream_flags, + devicep_frames, format, NULL); + HR_FAILED_RET (hr, IAudioClient3::InitializeSharedAudioStream, FALSE); + + hr = IAudioClient3_GetCurrentSharedModeEnginePeriod (client, &tmpf, + &devicep_frames); + CoTaskMemFree (tmpf); + HR_FAILED_RET (hr, IAudioClient3::GetCurrentSharedModeEnginePeriod, FALSE); + + *ret_devicep_frames = devicep_frames; + return TRUE; +} diff --git a/sys/wasapi/gstwasapiutil.h b/sys/wasapi/gstwasapiutil.h new file mode 100644 index 0000000000..70f241980c --- /dev/null +++ b/sys/wasapi/gstwasapiutil.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2008 Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.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. + */ + +#ifndef __GST_WASAPI_UTIL_H__ +#define __GST_WASAPI_UTIL_H__ + +#include <gst/gst.h> +#include <gst/audio/audio.h> +#include <gst/audio/gstaudiosrc.h> +#include <gst/audio/gstaudiosink.h> + +#include <mmdeviceapi.h> +#include <audioclient.h> + +#include "gstaudioclient3.h" +#include "gstmmdeviceenumerator.h" + +/* Static Caps shared between source, sink, and device provider */ +#define GST_WASAPI_STATIC_CAPS "audio/x-raw, " \ + "format = (string) " GST_AUDIO_FORMATS_ALL ", " \ + "layout = (string) interleaved, " \ + "rate = " GST_AUDIO_RATE_RANGE ", " \ + "channels = " GST_AUDIO_CHANNELS_RANGE + +/* Standard error path */ +#define HR_FAILED_AND(hr,func,and) \ + G_STMT_START { \ + if (FAILED (hr)) { \ + gchar *msg = gst_wasapi_util_hresult_to_string (hr); \ + GST_ERROR_OBJECT (self, #func " failed (%x): %s", (guint) hr, msg); \ + g_free (msg); \ + and; \ + } \ + } G_STMT_END + +#define HR_FAILED_RET(hr,func,ret) HR_FAILED_AND(hr,func,return ret) + +#define HR_FAILED_GOTO(hr,func,where) HR_FAILED_AND(hr,func,res = FALSE; goto where) + +#define HR_FAILED_ELEMENT_ERROR_AND(hr,func,el,and) \ + G_STMT_START { \ + if (FAILED (hr)) { \ + gchar *msg = gst_wasapi_util_hresult_to_string (hr); \ + GST_ERROR_OBJECT (el, #func " failed (%x): %s", (guint) hr, msg); \ + if (GST_IS_AUDIO_SRC (el)) \ + GST_ELEMENT_ERROR(el, RESOURCE, READ, \ + (#func " failed (%x): %s", (guint) hr, msg), (NULL)); \ + else \ + GST_ELEMENT_ERROR(el, RESOURCE, WRITE, \ + (#func " failed (%x): %s", (guint) hr, msg), (NULL)); \ + g_free (msg); \ + and; \ + } \ + } G_STMT_END + +#define HR_FAILED_ELEMENT_ERROR_RET(hr,func,el,ret) \ + HR_FAILED_ELEMENT_ERROR_AND(hr,func,el,return ret) + + +/* Device role enum property */ +typedef enum +{ + GST_WASAPI_DEVICE_ROLE_CONSOLE, + GST_WASAPI_DEVICE_ROLE_MULTIMEDIA, + GST_WASAPI_DEVICE_ROLE_COMMS +} GstWasapiDeviceRole; +#define GST_WASAPI_DEVICE_TYPE_ROLE (gst_wasapi_device_role_get_type()) +GType gst_wasapi_device_role_get_type (void); + +/* Utilities */ + +gboolean gst_wasapi_util_have_audioclient3 (void); + +gint gst_wasapi_device_role_to_erole (gint role); + +gint gst_wasapi_erole_to_device_role (gint erole); + +gchar *gst_wasapi_util_hresult_to_string (HRESULT hr); + +gboolean gst_wasapi_util_get_devices (GstMMDeviceEnumerator * enumerator, + gboolean active, + GList ** devices); + +gboolean gst_wasapi_util_get_device (GstMMDeviceEnumerator * enumerator, + gint data_flow, gint role, const wchar_t * device_strid, + IMMDevice ** ret_device); + +gboolean gst_wasapi_util_get_audio_client (GstElement * self, + IMMDevice * 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); + +gboolean gst_wasapi_util_get_capture_client (GstElement * element, + IAudioClient * client, IAudioCaptureClient ** ret_capture_client); + +gboolean gst_wasapi_util_get_clock (GstElement * element, + IAudioClient * client, IAudioClock ** ret_clock); + +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); + +gboolean gst_wasapi_util_initialize_audioclient (GstElement * element, + GstAudioRingBufferSpec * spec, IAudioClient * client, + WAVEFORMATEX * format, guint sharemode, gboolean low_latency, + gboolean loopback, guint * ret_devicep_frames); + +gboolean gst_wasapi_util_initialize_audioclient3 (GstElement * element, + GstAudioRingBufferSpec * spec, IAudioClient3 * client, + WAVEFORMATEX * format, gboolean low_latency, gboolean loopback, + guint * ret_devicep_frames); + +#endif /* __GST_WASAPI_UTIL_H__ */ diff --git a/sys/wasapi/meson.build b/sys/wasapi/meson.build new file mode 100644 index 0000000000..3fdd73a2fb --- /dev/null +++ b/sys/wasapi/meson.build @@ -0,0 +1,41 @@ +wasapi_sources = [ + 'gstmmdeviceenumerator.cpp', + 'gstwasapi.c', + 'gstwasapisrc.c', + 'gstwasapisink.c', + 'gstwasapiutil.c', + 'gstwasapidevice.c', +] + +if host_system != 'windows' + if get_option('wasapi').disabled() + subdir_done() + elif get_option('wasapi').enabled() + error('Cannot build wasapi plugin when not building for Windows') + endif +endif + +ole32_dep = cc.find_library('ole32', required : get_option('wasapi')) +ksuser_dep = cc.find_library('ksuser', required : get_option('wasapi')) +have_audioclient_h = cc.has_header('audioclient.h') +if not have_audioclient_h and get_option('wasapi').enabled() + error('wasapi plugin enabled but audioclient.h not found') +endif + +if ole32_dep.found() and ksuser_dep.found() and have_audioclient_h + wasapi_args = ['-DCOBJMACROS'] + if cc.has_header_symbol('audioclient.h', 'AUDCLNT_STREAMOPTIONS_NONE') + wasapi_args += ['-DHAVE_AUDCLNT_STREAMOPTIONS'] + endif + + gstwasapi = library('gstwasapi', + wasapi_sources, + c_args : gst_plugins_bad_args + wasapi_args, + cpp_args: gst_plugins_bad_args, + include_directories : [configinc], + dependencies : [gstaudio_dep, ole32_dep, ksuser_dep], + install : true, + install_dir : plugins_install_dir) + pkgconfig.generate(gstwasapi, install_dir : plugins_pkgconfig_install_dir) + plugins += [gstwasapi] +endif |