diff options
-rw-r--r-- | meson_options.txt | 2 | ||||
-rw-r--r-- | sys/asio/gstasiodeviceprovider.cpp | 273 | ||||
-rw-r--r-- | sys/asio/gstasiodeviceprovider.h | 37 | ||||
-rw-r--r-- | sys/asio/gstasioobject.cpp | 1875 | ||||
-rw-r--r-- | sys/asio/gstasioobject.h | 104 | ||||
-rw-r--r-- | sys/asio/gstasioringbuffer.cpp | 473 | ||||
-rw-r--r-- | sys/asio/gstasioringbuffer.h | 47 | ||||
-rw-r--r-- | sys/asio/gstasiosink.cpp | 361 | ||||
-rw-r--r-- | sys/asio/gstasiosink.h | 34 | ||||
-rw-r--r-- | sys/asio/gstasiosrc.cpp | 375 | ||||
-rw-r--r-- | sys/asio/gstasiosrc.h | 34 | ||||
-rw-r--r-- | sys/asio/gstasioutils.cpp | 282 | ||||
-rw-r--r-- | sys/asio/gstasioutils.h | 55 | ||||
-rw-r--r-- | sys/asio/meson.build | 84 | ||||
-rw-r--r-- | sys/asio/plugin.c | 48 | ||||
-rw-r--r-- | sys/meson.build | 1 |
16 files changed, 4085 insertions, 0 deletions
diff --git a/meson_options.txt b/meson_options.txt index 4a9778073..c24b3b3b0 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -81,6 +81,8 @@ option('aom', type : 'feature', value : 'auto', description : 'AOM AV1 video cod option('avtp', type : 'feature', value : 'auto', description : 'Audio/Video Transport Protocol (AVTP) plugin') option('androidmedia', type : 'feature', value : 'auto', description : 'Video capture and codec plugins for Android') option('applemedia', type : 'feature', value : 'auto', description : 'Video capture and codec access plugins for macOS and iOS') +option('asio', type : 'feature', value : 'auto', description : 'Steinberg Audio Streaming Input Output (ASIO) plugin') +option('asio-sdk-path', type : 'string', value : '', description : 'Full path to Steinberg Audio Streaming Input Output (ASIO) SDK') option('assrender', type : 'feature', value : 'auto', description : 'ASS/SSA subtitle renderer plugin') option('bluez', type : 'feature', value : 'auto', description : 'Bluetooth audio A2DP/AVDTP sink, AVDTP source plugin') option('bs2b', type : 'feature', value : 'auto', description : 'Bauer stereophonic-to-binaural audio plugin') diff --git a/sys/asio/gstasiodeviceprovider.cpp b/sys/asio/gstasiodeviceprovider.cpp new file mode 100644 index 000000000..4d1e9b0d0 --- /dev/null +++ b/sys/asio/gstasiodeviceprovider.cpp @@ -0,0 +1,273 @@ +/* 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 "gstasiodeviceprovider.h" +#include "gstasioutils.h" +#include "gstasioobject.h" +#include <atlconv.h> + +enum +{ + PROP_0, + PROP_DEVICE_CLSID, +}; + +struct _GstAsioDevice +{ + GstDevice parent; + + gchar *device_clsid; + const gchar *factory_name; +}; + +G_DEFINE_TYPE (GstAsioDevice, gst_asio_device, GST_TYPE_DEVICE); + +static void gst_asio_device_get_property (GObject * object, + guint prop_id, GValue * value, GParamSpec * pspec); +static void gst_asio_device_set_property (GObject * object, + guint prop_id, const GValue * value, GParamSpec * pspec); +static void gst_asio_device_finalize (GObject * object); +static GstElement *gst_asio_device_create_element (GstDevice * device, + const gchar * name); + +static void +gst_asio_device_class_init (GstAsioDeviceClass * klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GstDeviceClass *dev_class = GST_DEVICE_CLASS (klass); + + dev_class->create_element = gst_asio_device_create_element; + + gobject_class->get_property = gst_asio_device_get_property; + gobject_class->set_property = gst_asio_device_set_property; + gobject_class->finalize = gst_asio_device_finalize; + + g_object_class_install_property (gobject_class, PROP_DEVICE_CLSID, + g_param_spec_string ("device-clsid", "Device CLSID", + "ASIO device CLSID as string including curly brackets", NULL, + (GParamFlags) (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS))); +} + +static void +gst_asio_device_init (GstAsioDevice * self) +{ +} + +static void +gst_asio_device_finalize (GObject * object) +{ + GstAsioDevice *self = GST_ASIO_DEVICE (object); + + g_free (self->device_clsid); + + G_OBJECT_CLASS (gst_asio_device_parent_class)->finalize (object); +} + +static GstElement * +gst_asio_device_create_element (GstDevice * device, const gchar * name) +{ + GstAsioDevice *self = GST_ASIO_DEVICE (device); + GstElement *elem; + + elem = gst_element_factory_make (self->factory_name, name); + + g_object_set (elem, "device-clsid", self->device_clsid, NULL); + + return elem; +} + +static void +gst_asio_device_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstAsioDevice *self = GST_ASIO_DEVICE (object); + + switch (prop_id) { + case PROP_DEVICE_CLSID: + g_value_set_string (value, self->device_clsid); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_asio_device_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstAsioDevice *self = GST_ASIO_DEVICE (object); + + switch (prop_id) { + case PROP_DEVICE_CLSID: + g_free (self->device_clsid); + self->device_clsid = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +struct _GstAsioDeviceProvider +{ + GstDeviceProvider parent; +}; + +G_DEFINE_TYPE (GstAsioDeviceProvider, gst_asio_device_provider, + GST_TYPE_DEVICE_PROVIDER); + +static GList *gst_asio_device_provider_probe (GstDeviceProvider * provider); + +static void +gst_asio_device_provider_class_init (GstAsioDeviceProviderClass * klass) +{ + GstDeviceProviderClass *provider_class = GST_DEVICE_PROVIDER_CLASS (klass); + + provider_class->probe = GST_DEBUG_FUNCPTR (gst_asio_device_provider_probe); + + gst_device_provider_class_set_static_metadata (provider_class, + "ASIO Device Provider", + "Source/Sink/Audio", "List ASIO source and sink devices", + "Seungha Yang <seungha@centricular.com>"); +} + +static void +gst_asio_device_provider_init (GstAsioDeviceProvider * provider) +{ +} + +static void +gst_asio_device_provider_probe_internal (GstAsioDeviceProvider * self, + gboolean is_src, GList * asio_device_list, GList ** devices) +{ + const gchar *device_class, *factory_name; + GList *iter; + + USES_CONVERSION; + + if (is_src) { + device_class = "Audio/Source"; + factory_name = "asiosrc"; + } else { + device_class = "Audio/Sink"; + factory_name = "asiosink"; + } + + for (iter = asio_device_list; iter; iter = g_list_next (iter)) { + GstDevice *device; + GstAsioDeviceInfo *info = (GstAsioDeviceInfo *) iter->data; + GstAsioObject *obj; + GstCaps *caps = nullptr; + GstStructure *props = nullptr; + long max_in_ch = 0; + long max_out_ch = 0; + HRESULT hr; + LPOLESTR clsid_str = nullptr; + glong min_buf_size = 0; + glong max_buf_size = 0; + glong preferred_buf_size = 0; + glong buf_size_granularity = 0; + + obj = gst_asio_object_new (info, FALSE); + if (!obj) + continue; + + if (!gst_asio_object_get_max_num_channels (obj, &max_in_ch, &max_out_ch)) + goto done; + + if (is_src && max_in_ch <= 0) + goto done; + else if (!is_src && max_out_ch <= 0) + goto done; + + if (is_src) { + caps = gst_asio_object_get_caps (obj, + GST_ASIO_DEVICE_CLASS_CAPTURE, 1, max_in_ch); + } else { + caps = gst_asio_object_get_caps (obj, + GST_ASIO_DEVICE_CLASS_RENDER, 1, max_out_ch); + } + if (!caps) + goto done; + + hr = StringFromIID (info->clsid, &clsid_str); + if (FAILED (hr)) + goto done; + + if (!gst_asio_object_get_buffer_size (obj, &min_buf_size, &max_buf_size, + &preferred_buf_size, &buf_size_granularity)) + goto done; + + props = gst_structure_new ("asio-proplist", + "device.api", G_TYPE_STRING, "asio", + "device.clsid", G_TYPE_STRING, OLE2A (clsid_str), + "asio.device.description", G_TYPE_STRING, info->driver_desc, + "asio.device.min-buf-size", G_TYPE_LONG, min_buf_size, + "asio.device.max-buf-size", G_TYPE_LONG, max_buf_size, + "asio.device.preferred-buf-size", G_TYPE_LONG, preferred_buf_size, + "asio.device.buf-size-granularity", G_TYPE_LONG, buf_size_granularity, + nullptr); + + device = (GstDevice *) g_object_new (GST_TYPE_ASIO_DEVICE, + "device-clsid", OLE2A (clsid_str), + "display-name", info->driver_desc, "caps", caps, + "device-class", device_class, "properties", props, nullptr); + GST_ASIO_DEVICE (device)->factory_name = factory_name; + + *devices = g_list_append (*devices, device); + + done: + gst_clear_caps (&caps); + gst_clear_object (&obj); + if (props) + gst_structure_free (props); + } + + return; +} + +static GList * +gst_asio_device_provider_probe (GstDeviceProvider * provider) +{ + GstAsioDeviceProvider *self = GST_ASIO_DEVICE_PROVIDER (provider); + GList *devices = nullptr; + guint num_device; + GList *asio_device_list = nullptr; + + num_device = gst_asio_enum (&asio_device_list); + + if (num_device == 0) + return nullptr; + + gst_asio_device_provider_probe_internal (self, + TRUE, asio_device_list, &devices); + gst_asio_device_provider_probe_internal (self, + FALSE, asio_device_list, &devices); + + g_list_free_full (asio_device_list, + (GDestroyNotify) gst_asio_device_info_free); + + return devices; +} diff --git a/sys/asio/gstasiodeviceprovider.h b/sys/asio/gstasiodeviceprovider.h new file mode 100644 index 000000000..24126e808 --- /dev/null +++ b/sys/asio/gstasiodeviceprovider.h @@ -0,0 +1,37 @@ +/* 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. + */ + +#ifndef __GST_ASIO_DEVICE_PROVIDER_H__ +#define __GST_ASIO_DEVICE_PROVIDER_H__ + +#include <gst/gst.h> + +G_BEGIN_DECLS + +#define GST_TYPE_ASIO_DEVICE (gst_asio_device_get_type()) +#define GST_TYPE_ASIO_DEVICE_PROVIDER (gst_asio_device_provider_get_type()) + +G_DECLARE_FINAL_TYPE (GstAsioDevice, gst_asio_device, + GST, ASIO_DEVICE, GstDevice); +G_DECLARE_FINAL_TYPE (GstAsioDeviceProvider, gst_asio_device_provider, + GST, ASIO_DEVICE_PROVIDER, GstDeviceProvider); + +G_END_DECLS + +#endif /* __GST_ASIO_DEVICE_PROVIDER_H__ */
\ No newline at end of file diff --git a/sys/asio/gstasioobject.cpp b/sys/asio/gstasioobject.cpp new file mode 100644 index 000000000..675ffc89a --- /dev/null +++ b/sys/asio/gstasioobject.cpp @@ -0,0 +1,1875 @@ +/* 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 "gstasioobject.h" +#include <string.h> +#include <avrt.h> +#include <string> +#include <functional> +#include <vector> +#include <mutex> +#include <iasiodrv.h> + +GST_DEBUG_CATEGORY_STATIC (gst_asio_object_debug); +#define GST_CAT_DEFAULT gst_asio_object_debug + +/* List of GstAsioObject */ +static GList *asio_object_list = nullptr; + +/* *INDENT-OFF* */ +/* Protect asio_object_list and other global values */ +std::mutex global_lock; + +/* Protect callback slots */ +std::mutex slot_lock; +/* *INDENT-ON* */ + +static void gst_asio_object_buffer_switch (GstAsioObject * self, + glong index, ASIOBool process_now); +static void gst_asio_object_sample_rate_changed (GstAsioObject * self, + ASIOSampleRate rate); +static glong gst_asio_object_messages (GstAsioObject * self, glong selector, + glong value, gpointer message, gdouble * opt); +static ASIOTime *gst_asio_object_buffer_switch_time_info (GstAsioObject * self, + ASIOTime * time_info, glong index, ASIOBool process_now); + +/* *INDENT-OFF* */ +/* Object to delegate ASIO callbacks to dedicated GstAsioObject */ +class GstAsioCallbacks +{ +public: + GstAsioCallbacks (GstAsioObject * object) + { + g_weak_ref_init (&object_, object); + } + + virtual ~GstAsioCallbacks () + { + g_weak_ref_clear (&object_); + } + + void BufferSwitch (glong index, ASIOBool process_now) + { + GstAsioObject *obj = (GstAsioObject *) g_weak_ref_get (&object_); + if (!obj) + return; + + gst_asio_object_buffer_switch (obj, index, process_now); + gst_object_unref (obj); + } + + void SampleRateChanged (ASIOSampleRate rate) + { + GstAsioObject *obj = (GstAsioObject *) g_weak_ref_get (&object_); + if (!obj) + return; + + gst_asio_object_sample_rate_changed (obj, rate); + gst_object_unref (obj); + } + + glong Messages (glong selector, glong value, gpointer message, gdouble *opt) + { + GstAsioObject *obj = (GstAsioObject *) g_weak_ref_get (&object_); + if (!obj) + return 0; + + glong ret = gst_asio_object_messages (obj, selector, value, message, opt); + gst_object_unref (obj); + + return ret; + } + + ASIOTime * BufferSwitchTimeInfo (ASIOTime * time_info, + glong index, ASIOBool process_now) + { + GstAsioObject *obj = (GstAsioObject *) g_weak_ref_get (&object_); + if (!obj) + return nullptr; + + ASIOTime * ret = gst_asio_object_buffer_switch_time_info (obj, + time_info, index, process_now); + gst_object_unref (obj); + + return ret; + } + +private: + GWeakRef object_; +}; + +template <int instance_id> +class GstAsioCallbacksSlot +{ +public: + static void + BufferSwitchStatic(glong index, ASIOBool process_now) + { + buffer_switch(index, process_now); + } + + static void + SampleRateChangedStatic (ASIOSampleRate rate) + { + sample_rate_changed(rate); + } + + static glong + MessagesStatic(glong selector, glong value, gpointer message, gdouble *opt) + { + return messages(selector, value, message, opt); + } + + static ASIOTime * + BufferSwitchTimeInfoStatic(ASIOTime * time_info, glong index, + ASIOBool process_now) + { + return buffer_switch_time_info(time_info, index, process_now); + } + + static std::function<void(glong, ASIOBool)> buffer_switch; + static std::function<void(ASIOSampleRate)> sample_rate_changed; + static std::function<glong(glong, glong, gpointer, gdouble *)> messages; + static std::function<ASIOTime *(ASIOTime *, glong, ASIOBool)> buffer_switch_time_info; + + static bool bound; + + static void Init () + { + buffer_switch = nullptr; + sample_rate_changed = nullptr; + messages = nullptr; + buffer_switch_time_info = nullptr; + bound = false; + } + + static bool IsBound () + { + return bound; + } + + static void Bind (GstAsioCallbacks * cb, ASIOCallbacks * driver_cb) + { + buffer_switch = std::bind(&GstAsioCallbacks::BufferSwitch, cb, + std::placeholders::_1, std::placeholders::_2); + sample_rate_changed = std::bind(&GstAsioCallbacks::SampleRateChanged, cb, + std::placeholders::_1); + messages = std::bind(&GstAsioCallbacks::Messages, cb, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4); + buffer_switch_time_info = std::bind(&GstAsioCallbacks::BufferSwitchTimeInfo, + cb, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + driver_cb->bufferSwitch = BufferSwitchStatic; + driver_cb->sampleRateDidChange = SampleRateChangedStatic; + driver_cb->asioMessage = MessagesStatic; + driver_cb->bufferSwitchTimeInfo = BufferSwitchTimeInfoStatic; + + bound = true; + } +}; + +template <int instance_id> +std::function<void(glong, ASIOBool)> GstAsioCallbacksSlot<instance_id>::buffer_switch; +template <int instance_id> +std::function<void(ASIOSampleRate)> GstAsioCallbacksSlot<instance_id>::sample_rate_changed; +template <int instance_id> +std::function<glong(glong, glong, gpointer, gdouble *)> GstAsioCallbacksSlot<instance_id>::messages; +template <int instance_id> +std::function<ASIOTime *(ASIOTime *, glong, ASIOBool)> GstAsioCallbacksSlot<instance_id>::buffer_switch_time_info; +template <int instance_id> +bool GstAsioCallbacksSlot<instance_id>::bound; + +/* XXX: Create global slot objects, + * because ASIO callback doesn't support user data, hum.... */ +GstAsioCallbacksSlot<0> cb_slot_0; +GstAsioCallbacksSlot<1> cb_slot_1; +GstAsioCallbacksSlot<2> cb_slot_2; +GstAsioCallbacksSlot<3> cb_slot_3; +GstAsioCallbacksSlot<4> cb_slot_4; +GstAsioCallbacksSlot<5> cb_slot_5; +GstAsioCallbacksSlot<6> cb_slot_6; +GstAsioCallbacksSlot<7> cb_slot_7; + +/* *INDENT-ON* */ + +typedef struct +{ + GstAsioObjectCallbacks callbacks; + guint64 callback_id; +} GstAsioObjectCallbacksPrivate; + +enum +{ + PROP_0, + PROP_DEVICE_INFO, +}; + +typedef enum +{ + GST_ASIO_OBJECT_STATE_LOADED, + GST_ASIO_OBJECT_STATE_INITIALIZED, + GST_ASIO_OBJECT_STATE_PREPARED, + GST_ASIO_OBJECT_STATE_RUNNING, +} GstAsioObjectState; + +/* Protect singletone object */ +struct _GstAsioObject +{ + GstObject parent; + + GstAsioDeviceInfo *device_info; + + GstAsioObjectState state; + + IASIO *asio_handle; + + GThread *thread; + GMutex lock; + GCond cond; + GMainContext *context; + GMainLoop *loop; + + GMutex thread_lock; + GCond thread_cond; + + GMutex api_lock; + + /* called after init() done */ + glong max_num_input_channels; + glong max_num_output_channels; + + glong min_buffer_size; + glong max_buffer_size; + glong preferred_buffer_size; + glong buffer_size_granularity; + + glong selected_buffer_size; + + /* List of supported sample rate */ + GArray *supported_sample_rates; + + /* List of ASIOChannelInfo */ + ASIOChannelInfo *input_channel_infos; + ASIOChannelInfo *output_channel_infos; + + /* Selected sample rate */ + ASIOSampleRate sample_rate; + + /* Input/Output buffer infors */ + ASIOBufferInfo *buffer_infos; + + /* Store requested channel before createbuffer */ + gboolean *input_channel_requested; + gboolean *output_channel_requested; + + glong num_requested_input_channels; + glong num_requested_output_channels; + guint num_allocated_buffers; + + GList *src_client_callbacks; + GList *sink_client_callbacks; + GList *loopback_client_callbacks; + guint64 next_callback_id; + + GstAsioCallbacks *callbacks; + ASIOCallbacks driver_callbacks; + int slot_id; + + gboolean occupy_all_channels; +}; + +static void gst_asio_object_constructed (GObject * object); +static void gst_asio_object_finalize (GObject * object); +static void gst_asio_object_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); + +static gpointer gst_asio_object_thread_func (GstAsioObject * self); + +#define gst_asio_object_parent_class parent_class +G_DEFINE_TYPE (GstAsioObject, gst_asio_object, GST_TYPE_OBJECT); + +static void +gst_asio_object_class_init (GstAsioObjectClass * klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->constructed = gst_asio_object_constructed; + gobject_class->finalize = gst_asio_object_finalize; + gobject_class->set_property = gst_asio_object_set_property; + + g_object_class_install_property (gobject_class, PROP_DEVICE_INFO, + g_param_spec_pointer ("device-info", "Device Info", + "A pointer to GstAsioDeviceInfo struct", + (GParamFlags) (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS))); + + GST_DEBUG_CATEGORY_INIT (gst_asio_object_debug, + "asioobject", 0, "asioobject"); +} + +static void +gst_asio_object_init (GstAsioObject * self) +{ + g_mutex_init (&self->lock); + g_cond_init (&self->cond); + + g_mutex_init (&self->thread_lock); + g_cond_init (&self->thread_cond); + + g_mutex_init (&self->api_lock); + + self->supported_sample_rates = g_array_new (FALSE, + FALSE, sizeof (ASIOSampleRate)); + + self->slot_id = -1; +} + +static void +gst_asio_object_constructed (GObject * object) +{ + GstAsioObject *self = GST_ASIO_OBJECT (object); + + if (!self->device_info) { + GST_ERROR_OBJECT (self, "Device info was not configured"); + return; + } + + self->context = g_main_context_new (); + self->loop = g_main_loop_new (self->context, FALSE); + + g_mutex_lock (&self->lock); + self->thread = g_thread_new ("GstAsioObject", + (GThreadFunc) gst_asio_object_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_asio_object_finalize (GObject * object) +{ + GstAsioObject *self = GST_ASIO_OBJECT (object); + + if (self->loop) { + 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_mutex_clear (&self->thread_lock); + g_cond_clear (&self->thread_cond); + + g_mutex_clear (&self->api_lock); + + g_array_unref (self->supported_sample_rates); + + gst_asio_device_info_free (self->device_info); + g_free (self->input_channel_infos); + g_free (self->output_channel_infos); + g_free (self->input_channel_requested); + g_free (self->output_channel_requested); + + if (self->src_client_callbacks) + g_list_free_full (self->src_client_callbacks, (GDestroyNotify) g_free); + if (self->sink_client_callbacks) + g_list_free_full (self->sink_client_callbacks, (GDestroyNotify) g_free); + if (self->loopback_client_callbacks) + g_list_free_full (self->loopback_client_callbacks, (GDestroyNotify) g_free); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +gst_asio_object_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstAsioObject *self = GST_ASIO_OBJECT (object); + + switch (prop_id) { + case PROP_DEVICE_INFO: + g_clear_pointer (&self->device_info, gst_asio_device_info_free); + self->device_info = gst_asio_device_info_copy ((GstAsioDeviceInfo *) + g_value_get_pointer (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static HWND +gst_asio_object_create_internal_hwnd (GstAsioObject * self) +{ + WNDCLASSEXW wc; + ATOM atom = 0; + HINSTANCE hinstance = GetModuleHandle (NULL); + + atom = GetClassInfoExW (hinstance, L"GstAsioInternalWindow", &wc); + if (atom == 0) { + GST_LOG_OBJECT (self, "Register internal window class"); + ZeroMemory (&wc, sizeof (WNDCLASSEX)); + + wc.cbSize = sizeof (WNDCLASSEX); + wc.lpfnWndProc = DefWindowProc; + wc.hInstance = GetModuleHandle (nullptr); + wc.style = CS_OWNDC; + wc.lpszClassName = L"GstAsioInternalWindow"; + + atom = RegisterClassExW (&wc); + + if (atom == 0) { + GST_ERROR_OBJECT (self, "Failed to register window class 0x%x", + (unsigned int) GetLastError ()); + return nullptr; + } + } + + return CreateWindowExW (0, L"GstAsioInternalWindow", L"GstAsioInternal", + WS_POPUP, 0, 0, 1, 1, nullptr, nullptr, GetModuleHandle (nullptr), + nullptr); +} + +static gboolean +hwnd_msg_cb (GIOChannel * source, GIOCondition condition, gpointer data) +{ + MSG msg; + + if (!PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) + return G_SOURCE_CONTINUE; + + TranslateMessage (&msg); + DispatchMessage (&msg); + + return G_SOURCE_CONTINUE; +} + +static gboolean +gst_asio_object_main_loop_running_cb (GstAsioObject * self) +{ + GST_INFO_OBJECT (self, "Main loop running now"); + + g_mutex_lock (&self->lock); + g_cond_signal (&self->cond); + g_mutex_unlock (&self->lock); + + return G_SOURCE_REMOVE; +} + +static gboolean +gst_asio_object_bind_callbacks (GstAsioObject * self) +{ + std::lock_guard < std::mutex > lk (slot_lock); + gboolean ret = TRUE; + + if (!cb_slot_0.IsBound ()) { + cb_slot_0.Bind (self->callbacks, &self->driver_callbacks); + self->slot_id = 0; + } else if (!cb_slot_1.IsBound ()) { + cb_slot_1.Bind (self->callbacks, &self->driver_callbacks); + self->slot_id = 1; + } else if (!cb_slot_2.IsBound ()) { + cb_slot_2.Bind (self->callbacks, &self->driver_callbacks); + self->slot_id = 2; + } else if (!cb_slot_3.IsBound ()) { + cb_slot_3.Bind (self->callbacks, &self->driver_callbacks); + self->slot_id = 3; + } else if (!cb_slot_4.IsBound ()) { + cb_slot_4.Bind (self->callbacks, &self->driver_callbacks); + self->slot_id = 4; + } else if (!cb_slot_5.IsBound ()) { + cb_slot_5.Bind (self->callbacks, &self->driver_callbacks); + self->slot_id = 5; + } else if (!cb_slot_6.IsBound ()) { + cb_slot_6.Bind (self->callbacks, &self->driver_callbacks); + self->slot_id = 6; + } else if (!cb_slot_7.IsBound ()) { + cb_slot_7.Bind (self->callbacks, &self->driver_callbacks); + self->slot_id = 7; + } else { + self->slot_id = -1; + ret = FALSE; + } + + return ret; +} + +static void +gst_asio_object_unbind_callbacks (GstAsioObject * self) +{ + std::lock_guard < std::mutex > lk (slot_lock); + + if (!self->callbacks || self->slot_id < 0) + return; + + switch (self->slot_id) { + case 0: + cb_slot_0.Init (); + break; + case 1: + cb_slot_1.Init (); + break; + case 2: + cb_slot_2.Init (); + break; + case 3: + cb_slot_3.Init (); + break; + case 4: + cb_slot_4.Init (); + break; + case 5: + cb_slot_5.Init (); + break; + case 6: + cb_slot_6.Init (); + break; + case 7: + cb_slot_7.Init (); + break; + default: + g_assert_not_reached (); + break; + } + + return; +} + +static gpointer +gst_asio_object_thread_func (GstAsioObject * self) +{ + HANDLE avrt_handle = nullptr; + static DWORD task_idx = 0; + HWND hwnd; + GSource *source = nullptr; + GSource *hwnd_msg_source = nullptr; + GIOChannel *msg_io_channel = nullptr; + HRESULT hr; + ASIOError asio_rst; + IASIO *asio_handle = nullptr; + GstAsioDeviceInfo *device_info = self->device_info; + /* FIXME: check more sample rate */ + static ASIOSampleRate sample_rate_to_check[] = { + 48000.0, 44100.0, 192000.0, 96000.0, 88200.0, + }; + + g_assert (device_info); + + GST_INFO_OBJECT (self, + "Enter loop, ThreadingModel: %s, driver-name: %s, driver-desc: %s", + device_info->sta_model ? "STA" : "MTA", + GST_STR_NULL (device_info->driver_name), + GST_STR_NULL (device_info->driver_desc)); + + if (device_info->sta_model) + CoInitializeEx (NULL, COINIT_APARTMENTTHREADED); + else + CoInitializeEx (NULL, COINIT_MULTITHREADED); + + /* Our thread is unlikely different from driver's working thread though, + * let's do this. It should not cause any problem */ + AvSetMmThreadCharacteristicsW (L"Pro Audio", &task_idx); + g_main_context_push_thread_default (self->context); + + source = g_idle_source_new (); + g_source_set_callback (source, + (GSourceFunc) gst_asio_object_main_loop_running_cb, self, nullptr); + g_source_attach (source, self->context); + g_source_unref (source); + + /* XXX: not sure why ASIO API wants Windows handle for init(). + * Possibly it might be used for STA COM threading + * but it's undocummented... */ + hwnd = gst_asio_object_create_internal_hwnd (self); + if (!hwnd) + goto run_loop; + + hr = CoCreateInstance (device_info->clsid, nullptr, CLSCTX_INPROC_SERVER, + device_info->clsid, (gpointer *) & asio_handle); + if (FAILED (hr)) { + GST_WARNING_OBJECT (self, "Failed to create IASIO instance, hr: 0x%x", + (guint) hr); + goto run_loop; + } + + if (!asio_handle->init (hwnd)) { + GST_WARNING_OBJECT (self, "Failed to init IASIO instance"); + asio_handle->Release (); + asio_handle = nullptr; + goto run_loop; + } + + /* Query device information */ + asio_rst = asio_handle->getChannels (&self->max_num_input_channels, + &self->max_num_output_channels); + if (asio_rst != 0) { + GST_WARNING_OBJECT (self, "Failed to query in/out channels, ret %ld", + asio_rst); + asio_handle->Release (); + asio_handle = nullptr; + goto run_loop; + } + + GST_INFO_OBJECT (self, "Input/Output channles: %ld/%ld", + self->max_num_input_channels, self->max_num_output_channels); + + asio_rst = asio_handle->getBufferSize (&self->min_buffer_size, + &self->max_buffer_size, &self->preferred_buffer_size, + &self->buffer_size_granularity); + if (asio_rst != 0) { + GST_WARNING_OBJECT (self, "Failed to get buffer size, ret %ld", asio_rst); + asio_handle->Release (); + asio_handle = nullptr; + goto run_loop; + } + + /* Use preferreed buffer size by default */ + self->selected_buffer_size = self->preferred_buffer_size; + + GST_INFO_OBJECT (self, "min-buffer-size %ld, max-buffer-size %ld, " + "preferred-buffer-size %ld, buffer-size-granularity %ld", + self->min_buffer_size, self->max_buffer_size, + self->preferred_buffer_size, self->buffer_size_granularity); + + for (guint i = 0; i < G_N_ELEMENTS (sample_rate_to_check); i++) { + asio_rst = asio_handle->canSampleRate (sample_rate_to_check[i]); + if (asio_rst != 0) + continue; + + GST_INFO_OBJECT (self, "SampleRate %.1lf is supported", + sample_rate_to_check[i]); + g_array_append_val (self->supported_sample_rates, sample_rate_to_check[i]); + } + + if (self->supported_sample_rates->len == 0) { + GST_WARNING_OBJECT (self, "Failed to query supported sample rate"); + asio_handle->Release (); + asio_handle = nullptr; + goto run_loop; + } + + /* Pick the first supported samplerate */ + self->sample_rate = + g_array_index (self->supported_sample_rates, ASIOSampleRate, 0); + if (asio_handle->setSampleRate (self->sample_rate) != 0) { + GST_WARNING_OBJECT (self, "Failed to set samplerate %.1lf", + self->sample_rate); + asio_handle->Release (); + asio_handle = nullptr; + goto run_loop; + } + + if (self->max_num_input_channels > 0) { + self->input_channel_infos = g_new0 (ASIOChannelInfo, + self->max_num_input_channels); + for (glong i = 0; i < self->max_num_input_channels; i++) { + ASIOChannelInfo *info = &self->input_channel_infos[i]; + info->channel = i; + info->isInput = TRUE; + + asio_rst = asio_handle->getChannelInfo (info); + if (asio_rst != 0) { + GST_WARNING_OBJECT (self, "Failed to %ld input channel info, ret %ld", + i, asio_rst); + asio_handle->Release (); + asio_handle = nullptr; + goto run_loop; + } + + GST_INFO_OBJECT (self, + "InputChannelInfo %ld: isActive %s, channelGroup %ld, " + "ASIOSampleType %ld, name %s", i, info->isActive ? "true" : "false", + info->channelGroup, info->type, GST_STR_NULL (info->name)); + } + + self->input_channel_requested = + g_new0 (gboolean, self->max_num_input_channels); + } + + if (self->max_num_output_channels > 0) { + self->output_channel_infos = g_new0 (ASIOChannelInfo, + self->max_num_output_channels); + for (glong i = 0; i < self->max_num_output_channels; i++) { + ASIOChannelInfo *info = &self->output_channel_infos[i]; + info->channel = i; + info->isInput = FALSE; + + asio_rst = asio_handle->getChannelInfo (info); + if (asio_rst != 0) { + GST_WARNING_OBJECT (self, "Failed to %ld output channel info, ret %ld", + i, asio_rst); + asio_handle->Release (); + asio_handle = nullptr; + goto run_loop; + } + + GST_INFO_OBJECT (self, + "OutputChannelInfo %ld: isActive %s, channelGroup %ld, " + "ASIOSampleType %ld, name %s", i, info->isActive ? "true" : "false", + info->channelGroup, info->type, GST_STR_NULL (info->name)); + } + + self->output_channel_requested = + g_new0 (gboolean, self->max_num_input_channels); + } + + asio_rst = asio_handle->getSampleRate (&self->sample_rate); + if (asio_rst != 0) { + GST_WARNING_OBJECT (self, + "Failed to get current samplerate, ret %ld", asio_rst); + asio_handle->Release (); + asio_handle = nullptr; + goto run_loop; + } + + GST_INFO_OBJECT (self, "Current samplerate %.1lf", self->sample_rate); + + self->callbacks = new GstAsioCallbacks (self); + if (!gst_asio_object_bind_callbacks (self)) { + GST_ERROR_OBJECT (self, "Failed to bind callback to slot"); + delete self->callbacks; + self->callbacks = nullptr; + + asio_handle->Release (); + asio_handle = nullptr; + goto run_loop; + } + + msg_io_channel = g_io_channel_win32_new_messages ((guintptr) hwnd); + hwnd_msg_source = g_io_create_watch (msg_io_channel, G_IO_IN); + g_source_set_callback (hwnd_msg_source, (GSourceFunc) hwnd_msg_cb, + self->context, nullptr); + g_source_attach (hwnd_msg_source, self->context); + + self->state = GST_ASIO_OBJECT_STATE_INITIALIZED; + self->asio_handle = asio_handle; + +run_loop: + g_main_loop_run (self->loop); + + if (self->asio_handle) { + if (self->state > GST_ASIO_OBJECT_STATE_PREPARED) + self->asio_handle->stop (); + + if (self->state > GST_ASIO_OBJECT_STATE_INITIALIZED) + self->asio_handle->disposeBuffers (); + } + + gst_asio_object_unbind_callbacks (self); + if (self->callbacks) { + delete self->callbacks; + self->callbacks = nullptr; + } + + if (hwnd_msg_source) { + g_source_destroy (hwnd_msg_source); + g_source_unref (hwnd_msg_source); + } + + if (msg_io_channel) + g_io_channel_unref (msg_io_channel); + + if (hwnd) + DestroyWindow (hwnd); + + g_main_context_pop_thread_default (self->context); + + if (avrt_handle) + AvRevertMmThreadCharacteristics (avrt_handle); + + if (asio_handle) { + asio_handle->Release (); + asio_handle = nullptr; + } + + CoUninitialize (); + + GST_INFO_OBJECT (self, "Exit loop"); + + return nullptr; +} + +static void +gst_asio_object_weak_ref_notify (gpointer data, GstAsioObject * object) +{ + std::lock_guard < std::mutex > lk (global_lock); + asio_object_list = g_list_remove (asio_object_list, object); +} + +GstAsioObject * +gst_asio_object_new (const GstAsioDeviceInfo * info, + gboolean occupy_all_channels) +{ + GstAsioObject *self = nullptr; + GList *iter; + std::lock_guard < std::mutex > lk (global_lock); + + g_return_val_if_fail (info != nullptr, nullptr); + + /* Check if we have object corresponding to CLSID, and if so return + * already existing object instead of allocating new one */ + for (iter = asio_object_list; iter; iter = g_list_next (iter)) { + GstAsioObject *object = (GstAsioObject *) iter->data; + + if (object->device_info->clsid == info->clsid) { + GST_DEBUG_OBJECT (object, "Found configured ASIO object"); + self = (GstAsioObject *) gst_object_ref (object); + break; + } + } + + if (self) + return self; + + self = (GstAsioObject *) g_object_new (GST_TYPE_ASIO_OBJECT, + "device-info", info, nullptr); + + if (!self->asio_handle) { + GST_WARNING_OBJECT (self, "ASIO handle is not available"); + gst_object_unref (self); + + return nullptr; + } + + self->occupy_all_channels = occupy_all_channels; + + gst_object_ref_sink (self); + + g_object_weak_ref (G_OBJECT (self), + (GWeakNotify) gst_asio_object_weak_ref_notify, nullptr); + asio_object_list = g_list_append (asio_object_list, self); + + return self; +} + +static GstCaps * +gst_asio_object_create_caps_from_channel_info (GstAsioObject * self, + ASIOChannelInfo * info, guint min_num_channels, guint max_num_channels) +{ + GstCaps *caps; + std::string caps_str; + GstAudioFormat fmt; + const gchar *fmt_str; + + g_assert (info); + g_assert (max_num_channels >= min_num_channels); + + fmt = gst_asio_sample_type_to_gst (info->type); + if (fmt == GST_AUDIO_FORMAT_UNKNOWN) { + GST_ERROR_OBJECT (self, "Unknown format"); + return nullptr; + } + + fmt_str = gst_audio_format_to_string (fmt); + + /* Actually we are non-interleaved, but element will interlave data */ + caps_str = "audio/x-raw, layout = (string) interleaved, "; + caps_str += "format = (string) " + std::string (fmt_str) + ", "; + /* use fixated sample rate, otherwise get_caps/set_sample_rate() might + * be racy in case that multiple sink/src are used */ + caps_str += + "rate = (int) " + std::to_string ((gint) self->sample_rate) + ", "; + + if (max_num_channels == min_num_channels) + caps_str += "channels = (int) " + std::to_string (max_num_channels); + else + caps_str += "channels = (int) [ " + std::to_string (min_num_channels) + + ", " + std::to_string (max_num_channels) + " ]"; + + caps = gst_caps_from_string (caps_str.c_str ()); + if (!caps) { + GST_ERROR_OBJECT (self, "Failed to create caps"); + return nullptr; + } + + GST_DEBUG_OBJECT (self, "Create caps %" GST_PTR_FORMAT, caps); + + return caps; +} + +/* FIXME: assuming all channels has the same format but it might not be true? */ +GstCaps * +gst_asio_object_get_caps (GstAsioObject * obj, GstAsioDeviceClassType type, + guint min_num_channels, guint max_num_channels) +{ + ASIOChannelInfo *infos; + + g_return_val_if_fail (GST_IS_ASIO_OBJECT (obj), nullptr); + + if (type == GST_ASIO_DEVICE_CLASS_CAPTURE) { + if (obj->max_num_input_channels == 0) { + GST_WARNING_OBJECT (obj, "Device doesn't support input"); + return nullptr; + } + + /* max_num_channels == 0 means [1, max-allowed-channles] */ + if (max_num_channels > 0) { + if (max_num_channels > obj->max_num_input_channels) { + GST_WARNING_OBJECT (obj, "Too many max channels"); + return nullptr; + } + } else { + max_num_channels = obj->max_num_input_channels; + } + + if (min_num_channels > 0) { + if (min_num_channels > obj->max_num_input_channels) { + GST_WARNING_OBJECT (obj, "Too many min channels"); + return nullptr; + } + } else { + min_num_channels = 1; + } + + infos = obj->input_channel_infos; + } else { + if (obj->max_num_output_channels == 0) { + GST_WARNING_OBJECT (obj, "Device doesn't support output"); + return nullptr; + } + + /* max_num_channels == 0 means [1, max-allowed-channles] */ + if (max_num_channels > 0) { + if (max_num_channels > obj->max_num_output_channels) { + GST_WARNING_OBJECT (obj, "Too many max channels"); + return nullptr; + } + } else { + max_num_channels = obj->max_num_output_channels; + } + + if (min_num_channels > 0) { + if (min_num_channels > obj->max_num_output_channels) { + GST_WARNING_OBJECT (obj, "Too many min channels"); + return nullptr; + } + } else { + min_num_channels = 1; + } + + infos = obj->output_channel_infos; + } + + return gst_asio_object_create_caps_from_channel_info (obj, + infos, min_num_channels, max_num_channels); +} + +gboolean +gst_asio_object_get_max_num_channels (GstAsioObject * obj, glong * num_input_ch, + glong * num_output_ch) +{ + g_return_val_if_fail (GST_IS_ASIO_OBJECT (obj), FALSE); + + if (num_input_ch) + *num_input_ch = obj->max_num_input_channels; + if (num_output_ch) + *num_output_ch = obj->max_num_output_channels; + + return TRUE; +} + +gboolean +gst_asio_object_get_buffer_size (GstAsioObject * obj, glong * min_size, + glong * max_size, glong * preferred_size, glong * granularity) +{ + g_return_val_if_fail (GST_IS_ASIO_OBJECT (obj), FALSE); + + if (min_size) + *min_size = obj->min_buffer_size; + if (max_size) + *max_size = obj->max_buffer_size; + if (preferred_size) + *preferred_size = obj->preferred_buffer_size; + if (granularity) + *granularity = obj->buffer_size_granularity; + + return TRUE; +} + +typedef void (*GstAsioObjectThreadFunc) (GstAsioObject * obj, gpointer data); + +typedef struct +{ + GstAsioObject *self; + GstAsioObjectThreadFunc func; + gpointer data; + gboolean fired; +} GstAsioObjectThreadRunData; + +static gboolean +gst_asio_object_thread_run_func (GstAsioObjectThreadRunData * data) +{ + GstAsioObject *self = data->self; + + if (data->func) + data->func (self, data->data); + + g_mutex_lock (&self->thread_lock); + data->fired = TRUE; + g_cond_broadcast (&self->thread_cond); + g_mutex_unlock (&self->thread_lock); + + return G_SOURCE_REMOVE; +} + +static void +gst_asio_object_thread_add (GstAsioObject * self, GstAsioObjectThreadFunc func, + gpointer data) +{ + GstAsioObjectThreadRunData thread_data; + + g_return_if_fail (GST_IS_ASIO_OBJECT (self)); + + thread_data.self = self; + thread_data.func = func; + thread_data.data = data; + thread_data.fired = FALSE; + + g_main_context_invoke (self->context, + (GSourceFunc) gst_asio_object_thread_run_func, &thread_data); + + g_mutex_lock (&self->thread_lock); + while (!thread_data.fired) + g_cond_wait (&self->thread_cond, &self->thread_lock); + g_mutex_unlock (&self->thread_lock); +} + +static gboolean +gst_asio_object_validate_channels (GstAsioObject * self, gboolean is_input, + guint * channel_indices, guint num_channels) +{ + if (is_input) { + if (self->max_num_input_channels < num_channels) { + GST_WARNING_OBJECT (self, "%d exceeds max input channels %ld", + num_channels, self->max_num_input_channels); + return FALSE; + } + + for (guint i = 0; i < num_channels; i++) { + guint ch = channel_indices[i]; + if (self->max_num_input_channels <= ch) { + GST_WARNING_OBJECT (self, "%d exceeds max input channels %ld", + ch, self->max_num_input_channels); + + return FALSE; + } + } + } else { + if (self->max_num_output_channels < num_channels) { + GST_WARNING_OBJECT (self, "%d exceeds max output channels %ld", + num_channels, self->max_num_output_channels); + + return FALSE; + } + + for (guint i = 0; i < num_channels; i++) { + guint ch = channel_indices[i]; + if (self->max_num_output_channels <= ch) { + GST_WARNING_OBJECT (self, "%d exceeds max output channels %ld", + ch, self->max_num_output_channels); + + return FALSE; + } + } + } + + return TRUE; +} + +static gboolean +gst_asio_object_check_buffer_reuse (GstAsioObject * self, ASIOBool is_input, + guint * channel_indices, guint num_channels) +{ + guint num_found = 0; + + g_assert (self->buffer_infos); + g_assert (self->num_allocated_buffers > 0); + + for (guint i = 0; i < self->num_allocated_buffers; i++) { + ASIOBufferInfo *info = &self->buffer_infos[i]; + + if (info->isInput != is_input) + continue; + + for (guint j = 0; j < num_channels; j++) { + if (info->channelNum == channel_indices[j]) { + num_found++; + + break; + } + } + } + + return num_found == num_channels; +} + +static void +gst_asio_object_dispose_buffers_async (GstAsioObject * self, ASIOError * rst) +{ + g_assert (self->asio_handle); + g_assert (rst); + + *rst = self->asio_handle->disposeBuffers (); +} + +static gboolean +gst_asio_object_dispose_buffers (GstAsioObject * self) +{ + ASIOError rst; + g_assert (self->asio_handle); + + if (!self->buffer_infos) + return TRUE; + + if (!self->device_info->sta_model) { + rst = self->asio_handle->disposeBuffers (); + } else { + gst_asio_object_thread_add (self, + (GstAsioObjectThreadFunc) gst_asio_object_dispose_buffers_async, &rst); + } + + g_clear_pointer (&self->buffer_infos, g_free); + self->num_allocated_buffers = 0; + + return rst == 0; +} + +static ASIOError +gst_asio_object_create_buffers_real (GstAsioObject * self, glong * buffer_size) +{ + ASIOError err; + + g_assert (buffer_size); + + err = self->asio_handle->createBuffers (self->buffer_infos, + self->num_requested_input_channels + self->num_requested_output_channels, + *buffer_size, &self->driver_callbacks); + + /* It failed and buffer size is not equal to preferred size, + * try again with preferred size */ + if (err != 0 && *buffer_size != self->preferred_buffer_size) { + GST_WARNING_OBJECT (self, + "Failed to create buffer with buffer size %ld, try again with %ld", + *buffer_size, self->preferred_buffer_size); + + err = self->asio_handle->createBuffers (self->buffer_infos, + self->num_requested_input_channels + + self->num_requested_output_channels, self->preferred_buffer_size, + &self->driver_callbacks); + + if (!err) { + *buffer_size = self->preferred_buffer_size; + } + } + + return err; +} + +typedef struct +{ + glong buffer_size; + ASIOError err; +} CreateBuffersAsyncData; + +static void +gst_asio_object_create_buffers_async (GstAsioObject * self, + CreateBuffersAsyncData * data) +{ + data->err = gst_asio_object_create_buffers_real (self, &data->buffer_size); +} + +static gboolean +gst_asio_object_create_buffers_internal (GstAsioObject * self, + glong * buffer_size) +{ + ASIOError err; + g_assert (self->asio_handle); + + if (!self->device_info->sta_model) { + err = gst_asio_object_create_buffers_real (self, buffer_size); + } else { + CreateBuffersAsyncData data; + data.buffer_size = *buffer_size; + + gst_asio_object_thread_add (self, + (GstAsioObjectThreadFunc) gst_asio_object_create_buffers_async, &data); + + err = data.err; + *buffer_size = data.buffer_size; + } + + return !err; +} + +gboolean +gst_asio_object_create_buffers (GstAsioObject * obj, + GstAsioDeviceClassType type, + guint * channel_indices, guint num_channels, guint * buffer_size) +{ + gboolean can_reuse = FALSE; + guint i, j; + glong buf_size; + glong prev_buf_size = 0; + gboolean is_src; + + g_return_val_if_fail (GST_IS_ASIO_OBJECT (obj), FALSE); + g_return_val_if_fail (channel_indices != nullptr, FALSE); + g_return_val_if_fail (num_channels > 0, FALSE); + + GST_DEBUG_OBJECT (obj, "Create buffers"); + + if (type == GST_ASIO_DEVICE_CLASS_CAPTURE) + is_src = TRUE; + else + is_src = FALSE; + + g_mutex_lock (&obj->api_lock); + if (!gst_asio_object_validate_channels (obj, is_src, channel_indices, + num_channels)) { + GST_ERROR_OBJECT (obj, "Invalid request"); + g_mutex_unlock (&obj->api_lock); + + return FALSE; + } + + if (obj->buffer_infos) { + GST_DEBUG_OBJECT (obj, + "Have configured buffer infors, checking whether we can reuse it"); + can_reuse = gst_asio_object_check_buffer_reuse (obj, + is_src ? TRUE : FALSE, channel_indices, num_channels); + } + + if (can_reuse) { + GST_DEBUG_OBJECT (obj, "We can reuse already allocated buffers"); + if (buffer_size) + *buffer_size = obj->selected_buffer_size; + + g_mutex_unlock (&obj->api_lock); + + return TRUE; + } + + /* Cannot re-allocated buffers once started... */ + if (obj->state > GST_ASIO_OBJECT_STATE_PREPARED) { + GST_WARNING_OBJECT (obj, "We are running already"); + g_mutex_unlock (&obj->api_lock); + + return FALSE; + } + + /* Use already configured buffer size */ + if (obj->buffer_infos) + prev_buf_size = obj->selected_buffer_size; + + /* If we have configured buffers, dispose and re-allocate */ + if (!gst_asio_object_dispose_buffers (obj)) { + GST_ERROR_OBJECT (obj, "Failed to dispose buffers"); + + obj->state = GST_ASIO_OBJECT_STATE_INITIALIZED; + + g_mutex_unlock (&obj->api_lock); + return FALSE; + } + + if (obj->occupy_all_channels) { + GST_INFO_OBJECT (obj, + "occupy-all-channels mode, will allocate buffers for all channels"); + /* In this case, we will allocate buffer for all available input/output + * channles, regardless of what requested here */ + for (guint i = 0; i < (guint) obj->max_num_input_channels; i++) + obj->input_channel_requested[i] = TRUE; + for (guint i = 0; i < (guint) obj->max_num_output_channels; i++) + obj->output_channel_requested[i] = TRUE; + + obj->num_requested_input_channels = obj->max_num_input_channels; + obj->num_requested_output_channels = obj->max_num_output_channels; + } else { + if (is_src) { + for (guint i = 0; i < num_channels; i++) { + guint ch = channel_indices[i]; + + obj->input_channel_requested[ch] = TRUE; + } + + obj->num_requested_input_channels = 0; + for (guint i = 0; i < obj->max_num_input_channels; i++) { + if (obj->input_channel_requested[i]) + obj->num_requested_input_channels++; + } + } else { + for (guint i = 0; i < num_channels; i++) { + guint ch = channel_indices[i]; + + obj->output_channel_requested[ch] = TRUE; + } + + obj->num_requested_output_channels = 0; + for (guint i = 0; i < obj->max_num_output_channels; i++) { + if (obj->output_channel_requested[i]) + obj->num_requested_output_channels++; + } + } + } + + obj->num_allocated_buffers = obj->num_requested_input_channels + + obj->num_requested_output_channels; + + obj->buffer_infos = g_new0 (ASIOBufferInfo, obj->num_allocated_buffers); + for (i = 0, j = 0; i < obj->num_requested_input_channels; i++) { + ASIOBufferInfo *info = &obj->buffer_infos[i]; + + info->isInput = TRUE; + while (!obj->input_channel_requested[j]) + j++; + + info->channelNum = j; + j++; + } + + for (i = obj->num_requested_input_channels, j = 0; + i < + obj->num_requested_input_channels + obj->num_requested_output_channels; + i++) { + ASIOBufferInfo *info = &obj->buffer_infos[i]; + + info->isInput = FALSE; + while (!obj->output_channel_requested[j]) + j++; + + info->channelNum = j; + j++; + } + + if (prev_buf_size > 0) { + buf_size = prev_buf_size; + } else if (buffer_size && *buffer_size > 0) { + buf_size = *buffer_size; + } else { + buf_size = obj->preferred_buffer_size; + } + + GST_INFO_OBJECT (obj, "Creating buffer with size %ld", buf_size); + + if (!gst_asio_object_create_buffers_internal (obj, &buf_size)) { + GST_ERROR_OBJECT (obj, "Failed to create buffers"); + g_clear_pointer (&obj->buffer_infos, g_free); + obj->num_allocated_buffers = 0; + + obj->state = GST_ASIO_OBJECT_STATE_INITIALIZED; + + g_mutex_unlock (&obj->api_lock); + + return FALSE; + } + + GST_INFO_OBJECT (obj, "Selected buffer size %ld", buf_size); + + obj->selected_buffer_size = buf_size; + if (buffer_size) + *buffer_size = buf_size; + + obj->state = GST_ASIO_OBJECT_STATE_PREPARED; + + g_mutex_unlock (&obj->api_lock); + + return TRUE; +} + +typedef struct +{ + glong arg[4]; + ASIOError ret; +} RunAsyncData; + +static void +gst_asio_object_get_latencies_async (GstAsioObject * self, RunAsyncData * data) +{ + data->ret = self->asio_handle->getLatencies (&data->arg[0], &data->arg[1]); +} + +gboolean +gst_asio_object_get_latencies (GstAsioObject * obj, glong * input_latency, + glong * output_latency) +{ + RunAsyncData data = { 0 }; + ASIOError err; + + g_return_val_if_fail (GST_IS_ASIO_OBJECT (obj), FALSE); + g_assert (obj->asio_handle); + + if (!obj->device_info->sta_model) { + err = obj->asio_handle->getLatencies (input_latency, output_latency); + } else { + gst_asio_object_thread_add (obj, + (GstAsioObjectThreadFunc) gst_asio_object_get_latencies_async, &data); + + *input_latency = data.arg[0]; + *output_latency = data.arg[1]; + err = data.ret; + } + + return !err; +} + +typedef struct +{ + ASIOSampleRate sample_rate; + ASIOError err; +} SampleRateAsyncData; + +static void +gst_asio_object_can_sample_rate_async (GstAsioObject * self, + SampleRateAsyncData * data) +{ + data->err = self->asio_handle->canSampleRate (data->sample_rate); +} + +gboolean +gst_asio_object_can_sample_rate (GstAsioObject * obj, + ASIOSampleRate sample_rate) +{ + SampleRateAsyncData data = { 0 }; + ASIOError err = 0; + + g_return_val_if_fail (GST_IS_ASIO_OBJECT (obj), FALSE); + g_assert (obj->asio_handle); + + g_mutex_lock (&obj->api_lock); + for (guint i = 0; i < obj->supported_sample_rates->len; i++) { + ASIOSampleRate val = g_array_index (obj->supported_sample_rates, + ASIOSampleRate, i); + if (val == sample_rate) { + g_mutex_unlock (&obj->api_lock); + return TRUE; + } + } + + if (!obj->device_info->sta_model) { + err = obj->asio_handle->canSampleRate (sample_rate); + + if (!err) + g_array_append_val (obj->supported_sample_rates, sample_rate); + + g_mutex_unlock (&obj->api_lock); + return !err; + } + + data.sample_rate = sample_rate; + gst_asio_object_thread_add (obj, + (GstAsioObjectThreadFunc) gst_asio_object_can_sample_rate_async, &data); + + if (!data.err) + g_array_append_val (obj->supported_sample_rates, sample_rate); + + g_mutex_unlock (&obj->api_lock); + + return !data.err; +} + +gboolean +gst_asio_object_get_sample_rate (GstAsioObject * obj, + ASIOSampleRate * sample_rate) +{ + g_return_val_if_fail (GST_IS_ASIO_OBJECT (obj), FALSE); + + *sample_rate = obj->sample_rate; + + return 0; +} + +static void +gst_asio_object_set_sample_rate_async (GstAsioObject * self, + SampleRateAsyncData * data) +{ + data->err = self->asio_handle->setSampleRate (data->sample_rate); + if (!data->err) + self->sample_rate = data->sample_rate; +} + +gboolean +gst_asio_object_set_sample_rate (GstAsioObject * obj, + ASIOSampleRate sample_rate) +{ + SampleRateAsyncData data = { 0 }; + ASIOError err = 0; + + g_return_val_if_fail (GST_IS_ASIO_OBJECT (obj), FALSE); + g_assert (obj->asio_handle); + + g_mutex_lock (&obj->api_lock); + if (sample_rate == obj->sample_rate) { + g_mutex_unlock (&obj->api_lock); + return TRUE; + } + + if (!obj->device_info->sta_model) { + err = obj->asio_handle->setSampleRate (sample_rate); + if (!err) + obj->sample_rate = sample_rate; + + g_mutex_unlock (&obj->api_lock); + return !err; + } + + data.sample_rate = sample_rate; + gst_asio_object_thread_add (obj, + (GstAsioObjectThreadFunc) gst_asio_object_set_sample_rate_async, &data); + g_mutex_unlock (&obj->api_lock); + + return !data.err; +} + +static void +gst_asio_object_buffer_switch (GstAsioObject * self, + glong index, ASIOBool process_now) +{ + ASIOTime time_info; + ASIOTime *our_time_info = nullptr; + ASIOError err = 0; + + memset (&time_info, 0, sizeof (ASIOTime)); + + err = + self->asio_handle->getSamplePosition (&time_info.timeInfo.samplePosition, + &time_info.timeInfo.systemTime); + if (!err) + our_time_info = &time_info; + + gst_asio_object_buffer_switch_time_info (self, + our_time_info, index, process_now); +} + +static void +gst_asio_object_sample_rate_changed (GstAsioObject * self, ASIOSampleRate rate) +{ + GST_INFO_OBJECT (self, "SampleRate changed to %lf", rate); +} + +static glong +gst_asio_object_messages (GstAsioObject * self, + glong selector, glong value, gpointer message, gdouble * opt) +{ + GST_DEBUG_OBJECT (self, "ASIO message: %ld, %ld", selector, value); + + switch (selector) { + case kAsioSelectorSupported: + if (value == kAsioResetRequest || value == kAsioEngineVersion || + value == kAsioResyncRequest || value == kAsioLatenciesChanged || + value == kAsioSupportsTimeCode || value == kAsioSupportsInputMonitor) + return 0; + else if (value == kAsioSupportsTimeInfo) + return 1; + GST_WARNING_OBJECT (self, "Unsupported ASIO selector: %li", value); + break; + case kAsioBufferSizeChange: + GST_WARNING_OBJECT (self, + "Unsupported ASIO message: kAsioBufferSizeChange"); + break; + case kAsioResetRequest: + GST_WARNING_OBJECT (self, "Unsupported ASIO message: kAsioResetRequest"); + break; + case kAsioResyncRequest: + GST_WARNING_OBJECT (self, "Unsupported ASIO message: kAsioResyncRequest"); + break; + case kAsioLatenciesChanged: + GST_WARNING_OBJECT (self, + "Unsupported ASIO message: kAsioLatenciesChanged"); + break; + case kAsioEngineVersion: + /* We target the ASIO v2 API, which includes ASIOOutputReady() */ + return 2; + case kAsioSupportsTimeInfo: + /* We use the new time info buffer switch callback */ + return 1; + case kAsioSupportsTimeCode: + /* We don't use the time code info right now */ + return 0; + default: + GST_WARNING_OBJECT (self, "Unsupported ASIO message: %li, %li", selector, + value); + break; + } + + return 0; +} + +#define PACK_ASIO_64(v) ((v).lo | ((guint64)((v).hi) << 32)) + +static ASIOTime * +gst_asio_object_buffer_switch_time_info (GstAsioObject * self, + ASIOTime * time_info, glong index, ASIOBool process_now) +{ + GList *iter; + + if (time_info) { + guint64 pos; + guint64 system_time; + + pos = PACK_ASIO_64 (time_info->timeInfo.samplePosition); + system_time = PACK_ASIO_64 (time_info->timeInfo.systemTime); + + GST_TRACE_OBJECT (self, "Sample Position: %" G_GUINT64_FORMAT + ", System Time: %" GST_TIME_FORMAT, pos, GST_TIME_ARGS (system_time)); + } + + g_mutex_lock (&self->api_lock); + if (!self->src_client_callbacks && !self->sink_client_callbacks && + !self->loopback_client_callbacks) { + GST_WARNING_OBJECT (self, "No installed client callback"); + goto out; + } + + for (iter = self->src_client_callbacks; iter;) { + GstAsioObjectCallbacksPrivate *cb = + (GstAsioObjectCallbacksPrivate *) iter->data; + gboolean ret; + + ret = cb->callbacks.buffer_switch (self, index, self->buffer_infos, + self->num_allocated_buffers, self->input_channel_infos, + self->output_channel_infos, self->sample_rate, + self->selected_buffer_size, time_info, cb->callbacks.user_data); + if (!ret) { + GST_INFO_OBJECT (self, "Remove callback for id %" G_GUINT64_FORMAT, + cb->callback_id); + GList *to_remove = iter; + iter = g_list_next (iter); + + g_free (to_remove->data); + g_list_free (to_remove); + } + + iter = g_list_next (iter); + } + + for (iter = self->sink_client_callbacks; iter;) { + GstAsioObjectCallbacksPrivate *cb = + (GstAsioObjectCallbacksPrivate *) iter->data; + gboolean ret; + + ret = cb->callbacks.buffer_switch (self, index, self->buffer_infos, + self->num_allocated_buffers, self->input_channel_infos, + self->output_channel_infos, self->sample_rate, + self->selected_buffer_size, time_info, cb->callbacks.user_data); + if (!ret) { + GST_INFO_OBJECT (self, "Remove callback for id %" G_GUINT64_FORMAT, + cb->callback_id); + GList *to_remove = iter; + iter = g_list_next (iter); + + g_free (to_remove->data); + g_list_free (to_remove); + } + + iter = g_list_next (iter); + } + + for (iter = self->loopback_client_callbacks; iter;) { + GstAsioObjectCallbacksPrivate *cb = + (GstAsioObjectCallbacksPrivate *) iter->data; + gboolean ret; + + ret = cb->callbacks.buffer_switch (self, index, self->buffer_infos, + self->num_allocated_buffers, self->input_channel_infos, + self->output_channel_infos, self->sample_rate, + self->selected_buffer_size, time_info, cb->callbacks.user_data); + if (!ret) { + GST_INFO_OBJECT (self, "Remove callback for id %" G_GUINT64_FORMAT, + cb->callback_id); + GList *to_remove = iter; + iter = g_list_next (iter); + + g_free (to_remove->data); + g_list_free (to_remove); + } + + iter = g_list_next (iter); + } + + self->asio_handle->outputReady (); + +out: + g_mutex_unlock (&self->api_lock); + + return nullptr; +} + +static void +gst_asio_object_start_async (GstAsioObject * self, ASIOError * rst) +{ + *rst = self->asio_handle->start (); +} + +gboolean +gst_asio_object_start (GstAsioObject * obj) +{ + ASIOError ret; + + g_return_val_if_fail (GST_IS_ASIO_OBJECT (obj), FALSE); + + g_mutex_lock (&obj->api_lock); + if (obj->state > GST_ASIO_OBJECT_STATE_PREPARED) { + GST_DEBUG_OBJECT (obj, "We are running already"); + g_mutex_unlock (&obj->api_lock); + + return TRUE; + } else if (obj->state < GST_ASIO_OBJECT_STATE_PREPARED) { + GST_ERROR_OBJECT (obj, "We are not prepared"); + g_mutex_unlock (&obj->api_lock); + + return FALSE; + } + + /* Then start */ + if (!obj->device_info->sta_model) { + ret = obj->asio_handle->start (); + } else { + gst_asio_object_thread_add (obj, + (GstAsioObjectThreadFunc) gst_asio_object_start_async, &ret); + } + + if (ret != 0) { + GST_ERROR_OBJECT (obj, "Failed to start object"); + g_mutex_unlock (&obj->api_lock); + + return FALSE; + } + + obj->state = GST_ASIO_OBJECT_STATE_RUNNING; + g_mutex_unlock (&obj->api_lock); + + return TRUE; +} + +gboolean +gst_asio_object_install_callback (GstAsioObject * obj, + GstAsioDeviceClassType type, + GstAsioObjectCallbacks * callbacks, guint64 * callback_id) +{ + GstAsioObjectCallbacksPrivate *cb; + + g_return_val_if_fail (GST_IS_ASIO_OBJECT (obj), FALSE); + g_return_val_if_fail (callbacks != nullptr, FALSE); + g_return_val_if_fail (callback_id != nullptr, FALSE); + + g_mutex_lock (&obj->api_lock); + cb = g_new0 (GstAsioObjectCallbacksPrivate, 1); + cb->callbacks = *callbacks; + cb->callback_id = obj->next_callback_id; + + switch (type) { + case GST_ASIO_DEVICE_CLASS_CAPTURE: + obj->src_client_callbacks = g_list_append (obj->src_client_callbacks, cb); + break; + case GST_ASIO_DEVICE_CLASS_RENDER: + obj->sink_client_callbacks = + g_list_append (obj->sink_client_callbacks, cb); + break; + case GST_ASIO_DEVICE_CLASS_LOOPBACK_CAPTURE: + obj->loopback_client_callbacks = + g_list_append (obj->loopback_client_callbacks, cb); + break; + default: + g_assert_not_reached (); + g_free (cb); + return FALSE; + } + + *callback_id = cb->callback_id; + g_mutex_unlock (&obj->api_lock); + + return TRUE; +} + +void +gst_asio_object_uninstall_callback (GstAsioObject * obj, guint64 callback_id) +{ + GList *iter; + + g_return_if_fail (GST_IS_ASIO_OBJECT (obj)); + + g_mutex_lock (&obj->api_lock); + + GST_DEBUG_OBJECT (obj, "Removing callback id %" G_GUINT64_FORMAT, + callback_id); + + for (iter = obj->src_client_callbacks; iter; iter = g_list_next (iter)) { + GstAsioObjectCallbacksPrivate *cb = + (GstAsioObjectCallbacksPrivate *) iter->data; + + if (cb->callback_id != callback_id) + continue; + + GST_DEBUG_OBJECT (obj, "Found src callback for id %" G_GUINT64_FORMAT, + callback_id); + + obj->src_client_callbacks = + g_list_remove_link (obj->src_client_callbacks, iter); + g_free (iter->data); + g_list_free (iter); + g_mutex_unlock (&obj->api_lock); + + return; + } + + for (iter = obj->sink_client_callbacks; iter; iter = g_list_next (iter)) { + GstAsioObjectCallbacksPrivate *cb = + (GstAsioObjectCallbacksPrivate *) iter->data; + + if (cb->callback_id != callback_id) + continue; + + GST_DEBUG_OBJECT (obj, "Found sink callback for id %" G_GUINT64_FORMAT, + callback_id); + + obj->sink_client_callbacks = + g_list_remove_link (obj->sink_client_callbacks, iter); + g_free (iter->data); + g_list_free (iter); + g_mutex_unlock (&obj->api_lock); + + return; + } + + for (iter = obj->loopback_client_callbacks; iter; iter = g_list_next (iter)) { + GstAsioObjectCallbacksPrivate *cb = + (GstAsioObjectCallbacksPrivate *) iter->data; + + if (cb->callback_id != callback_id) + continue; + + GST_DEBUG_OBJECT (obj, "Found loopback callback for id %" G_GUINT64_FORMAT, + callback_id); + + obj->loopback_client_callbacks = + g_list_remove_link (obj->loopback_client_callbacks, iter); + g_free (iter->data); + g_list_free (iter); + break; + } + + g_mutex_unlock (&obj->api_lock); +} diff --git a/sys/asio/gstasioobject.h b/sys/asio/gstasioobject.h new file mode 100644 index 000000000..7d80b7b99 --- /dev/null +++ b/sys/asio/gstasioobject.h @@ -0,0 +1,104 @@ +/* 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 aglong with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __GST_ASIO_OBJECT_H__ +#define __GST_ASIO_OBJECT_H__ + +#include <gst/gst.h> +#include <windows.h> +#include "gstasioutils.h" + +G_BEGIN_DECLS + +#define GST_TYPE_ASIO_OBJECT (gst_asio_object_get_type()) +G_DECLARE_FINAL_TYPE (GstAsioObject, gst_asio_object, + GST, ASIO_OBJECT, GstObject); + +typedef struct { + gboolean (*buffer_switch) (GstAsioObject * obj, + glong index, + ASIOBufferInfo * infos, + guint num_infos, + ASIOChannelInfo * input_channel_infos, + ASIOChannelInfo * output_channel_infos, + ASIOSampleRate sample_rate, + glong buffer_size, + ASIOTime * time_info, + gpointer user_data); + + gpointer user_data; +} GstAsioObjectCallbacks; + +typedef enum +{ + GST_ASIO_DEVICE_CLASS_CAPTURE, + GST_ASIO_DEVICE_CLASS_RENDER, + GST_ASIO_DEVICE_CLASS_LOOPBACK_CAPTURE, +} GstAsioDeviceClassType; + +GstAsioObject * gst_asio_object_new (const GstAsioDeviceInfo * info, + gboolean occupy_all_channels); + +GstCaps * gst_asio_object_get_caps (GstAsioObject * obj, + GstAsioDeviceClassType type, + guint num_min_channels, + guint num_max_channels); + +gboolean gst_asio_object_create_buffers (GstAsioObject * obj, + GstAsioDeviceClassType type, + guint * channel_indices, + guint num_channels, + guint * buffer_size); + +gboolean gst_asio_object_start (GstAsioObject * obj); + +gboolean gst_asio_object_install_callback (GstAsioObject * obj, + GstAsioDeviceClassType type, + GstAsioObjectCallbacks * callbacks, + guint64 * callback_id); + +void gst_asio_object_uninstall_callback (GstAsioObject * obj, + guint64 callback_id); + +gboolean gst_asio_object_get_max_num_channels (GstAsioObject * obj, + glong * num_input_ch, + glong * num_output_ch); + +gboolean gst_asio_object_get_buffer_size (GstAsioObject * obj, + glong * min_size, + glong * max_size, + glong * preferred_size, + glong * granularity); + +gboolean gst_asio_object_get_latencies (GstAsioObject * obj, + glong * input_latency, + glong * output_latency); + +gboolean gst_asio_object_can_sample_rate (GstAsioObject * obj, + ASIOSampleRate sample_rate); + +gboolean gst_asio_object_get_sample_rate (GstAsioObject * obj, + ASIOSampleRate * sample_rate); + +gboolean gst_asio_object_set_sample_rate (GstAsioObject * obj, + ASIOSampleRate sample_rate); + +G_END_DECLS + +#endif /* __GST_ASIO_OBJECT_H__ */
\ No newline at end of file diff --git a/sys/asio/gstasioringbuffer.cpp b/sys/asio/gstasioringbuffer.cpp new file mode 100644 index 000000000..449d54bb4 --- /dev/null +++ b/sys/asio/gstasioringbuffer.cpp @@ -0,0 +1,473 @@ +/* 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 "gstasioringbuffer.h" +#include <string.h> +#include "gstasioutils.h" +#include "gstasioobject.h" + +GST_DEBUG_CATEGORY_STATIC (gst_asio_ring_buffer_debug); +#define GST_CAT_DEFAULT gst_asio_ring_buffer_debug + +struct _GstAsioRingBuffer +{ + GstAudioRingBuffer parent; + + GstAsioDeviceClassType type; + + GstAsioObject *asio_object; + guint *channel_indices; + guint num_channels; + ASIOBufferInfo **infos; + + guint64 callback_id; + gboolean callback_installed; + + gboolean running; + guint buffer_size; + + /* Used to detect sample gap */ + gboolean is_first; + guint64 expected_sample_position; + gboolean trace_sample_position; +}; + +enum +{ + PROP_0, + PROP_DEVICE_INFO, +}; + +static void gst_asio_ring_buffer_dispose (GObject * object); + +static gboolean gst_asio_ring_buffer_open_device (GstAudioRingBuffer * buf); +static gboolean gst_asio_ring_buffer_close_device (GstAudioRingBuffer * buf); +static gboolean gst_asio_ring_buffer_acquire (GstAudioRingBuffer * buf, + GstAudioRingBufferSpec * spec); +static gboolean gst_asio_ring_buffer_release (GstAudioRingBuffer * buf); +static gboolean gst_asio_ring_buffer_start (GstAudioRingBuffer * buf); +static gboolean gst_asio_ring_buffer_stop (GstAudioRingBuffer * buf); +static guint gst_asio_ring_buffer_delay (GstAudioRingBuffer * buf); + +static gboolean gst_asio_buffer_switch_cb (GstAsioObject * obj, + glong index, ASIOBufferInfo * infos, guint num_infos, + ASIOChannelInfo * input_channel_infos, + ASIOChannelInfo * output_channel_infos, + ASIOSampleRate sample_rate, glong buffer_size, gpointer user_data); + +#define gst_asio_ring_buffer_parent_class parent_class +G_DEFINE_TYPE (GstAsioRingBuffer, gst_asio_ring_buffer, + GST_TYPE_AUDIO_RING_BUFFER); + +static void +gst_asio_ring_buffer_class_init (GstAsioRingBufferClass * klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GstAudioRingBufferClass *ring_buffer_class = + GST_AUDIO_RING_BUFFER_CLASS (klass); + + gobject_class->dispose = gst_asio_ring_buffer_dispose; + + ring_buffer_class->open_device = + GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_open_device); + ring_buffer_class->close_device = + GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_close_device); + ring_buffer_class->acquire = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_acquire); + ring_buffer_class->release = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_release); + ring_buffer_class->start = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_start); + ring_buffer_class->resume = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_start); + ring_buffer_class->stop = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_stop); + ring_buffer_class->delay = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_delay); + + GST_DEBUG_CATEGORY_INIT (gst_asio_ring_buffer_debug, + "asioringbuffer", 0, "asioringbuffer"); +} + +static void +gst_asio_ring_buffer_init (GstAsioRingBuffer * self) +{ +} + +static void +gst_asio_ring_buffer_dispose (GObject * object) +{ + GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (object); + + gst_clear_object (&self->asio_object); + g_clear_pointer (&self->channel_indices, g_free); + + G_OBJECT_CLASS (parent_class)->dispose (object); +} + +static gboolean +gst_asio_ring_buffer_open_device (GstAudioRingBuffer * buf) +{ + GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf); + + GST_DEBUG_OBJECT (self, "Open"); + + return TRUE; +} + +static gboolean +gst_asio_ring_buffer_close_device (GstAudioRingBuffer * buf) +{ + GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf); + + GST_DEBUG_OBJECT (self, "Close"); + + return TRUE; +} + +#define PACK_ASIO_64(v) ((v).lo | ((guint64)((v).hi) << 32)) + +static gboolean +gst_asio_buffer_switch_cb (GstAsioObject * obj, glong index, + ASIOBufferInfo * infos, guint num_infos, + ASIOChannelInfo * input_channel_infos, + ASIOChannelInfo * output_channel_infos, + ASIOSampleRate sample_rate, glong buffer_size, + ASIOTime * time_info, gpointer user_data) +{ + GstAsioRingBuffer *self = (GstAsioRingBuffer *) user_data; + GstAudioRingBuffer *ringbuffer = GST_AUDIO_RING_BUFFER_CAST (self); + gint segment; + guint8 *readptr; + gint len; + guint i, j; + guint num_channels = 0; + guint bps = GST_AUDIO_INFO_WIDTH (&ringbuffer->spec.info) >> 3; + + g_assert (index == 0 || index == 1); + g_assert (num_infos >= self->num_channels); + + GST_TRACE_OBJECT (self, "Buffer Switch callback, index %ld", index); + + if (!gst_audio_ring_buffer_prepare_read (ringbuffer, + &segment, &readptr, &len)) { + GST_WARNING_OBJECT (self, "No segment available"); + return TRUE; + } + + GST_TRACE_OBJECT (self, "segment %d, length %d", segment, len); + + /* Check missing frames */ + if (self->type == GST_ASIO_DEVICE_CLASS_CAPTURE) { + if (self->is_first) { + if (time_info) { + self->expected_sample_position = + PACK_ASIO_64 (time_info->timeInfo.samplePosition) + buffer_size; + self->trace_sample_position = TRUE; + } else { + GST_WARNING_OBJECT (self, "ASIOTime is not available"); + self->trace_sample_position = FALSE; + } + + self->is_first = FALSE; + } else if (self->trace_sample_position) { + if (!time_info) { + GST_WARNING_OBJECT (self, "ASIOTime is not available"); + self->trace_sample_position = FALSE; + } else { + guint64 sample_position = + PACK_ASIO_64 (time_info->timeInfo.samplePosition); + if (self->expected_sample_position < sample_position) { + guint64 gap_frames = sample_position - self->expected_sample_position; + gint gap_size = gap_frames * bps; + + GST_WARNING_OBJECT (self, "%" G_GUINT64_FORMAT " frames are missing"); + + while (gap_size >= len) { + gst_audio_format_info_fill_silence (ringbuffer->spec.info.finfo, + readptr, len); + gst_audio_ring_buffer_advance (ringbuffer, 1); + + gst_audio_ring_buffer_prepare_read (ringbuffer, + &segment, &readptr, &len); + + gap_size -= len; + } + } + + self->expected_sample_position = sample_position + buffer_size; + GST_TRACE_OBJECT (self, "Sample Position %" G_GUINT64_FORMAT + ", next: %" G_GUINT64_FORMAT, sample_position, + self->expected_sample_position); + } + } + } + + /* Given @infos might contain more channel data, pick channels what we want to + * read */ + for (i = 0; i < num_infos; i++) { + ASIOBufferInfo *info = &infos[i]; + + if (self->type == GST_ASIO_DEVICE_CLASS_CAPTURE) { + if (!info->isInput) + continue; + } else { + if (info->isInput) + continue; + } + + for (j = 0; j < self->num_channels; j++) { + if (self->channel_indices[j] != info->channelNum) + continue; + + g_assert (num_channels < self->num_channels); + self->infos[num_channels++] = info; + break; + } + } + + if (num_channels < self->num_channels) { + GST_ERROR_OBJECT (self, "Too small number of channel %d (expected %d)", + num_channels, self->num_channels); + } else { + if (self->type == GST_ASIO_DEVICE_CLASS_CAPTURE || + self->type == GST_ASIO_DEVICE_CLASS_LOOPBACK_CAPTURE) { + if (num_channels == 1) { + memcpy (readptr, self->infos[0]->buffers[index], len); + } else { + guint gst_offset = 0, asio_offset = 0; + + /* Interleaves audio */ + while (gst_offset < len) { + for (i = 0; i < num_channels; i++) { + ASIOBufferInfo *info = self->infos[i]; + + memcpy (readptr + gst_offset, + ((guint8 *) info->buffers[index]) + asio_offset, bps); + + gst_offset += bps; + } + asio_offset += bps; + } + } + } else { + if (num_channels == 1) { + memcpy (self->infos[0]->buffers[index], readptr, len); + } else { + guint gst_offset = 0, asio_offset = 0; + + /* Interleaves audio */ + while (gst_offset < len) { + for (i = 0; i < num_channels; i++) { + ASIOBufferInfo *info = self->infos[i]; + + memcpy (((guint8 *) info->buffers[index]) + asio_offset, + readptr + gst_offset, bps); + + gst_offset += bps; + } + asio_offset += bps; + } + } + } + } + + if (self->type == GST_ASIO_DEVICE_CLASS_RENDER) + gst_audio_ring_buffer_clear (ringbuffer, segment); + gst_audio_ring_buffer_advance (ringbuffer, 1); + + return TRUE; +} + +static gboolean +gst_asio_ring_buffer_acquire (GstAudioRingBuffer * buf, + GstAudioRingBufferSpec * spec) +{ + GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf); + + if (!self->asio_object) { + GST_ERROR_OBJECT (self, "No configured ASIO object"); + return FALSE; + } + + if (!self->channel_indices || self->num_channels == 0) { + GST_ERROR_OBJECT (self, "No configured channels"); + return FALSE; + } + + if (!gst_asio_object_set_sample_rate (self->asio_object, + GST_AUDIO_INFO_RATE (&spec->info))) { + GST_ERROR_OBJECT (self, "Failed to set sample rate"); + return FALSE; + } + + spec->segsize = self->buffer_size * + (GST_AUDIO_INFO_WIDTH (&spec->info) >> 3) * + GST_AUDIO_INFO_CHANNELS (&spec->info); + spec->segtotal = 2; + + buf->size = spec->segtotal * spec->segsize; + buf->memory = (guint8 *) g_malloc (buf->size); + gst_audio_format_info_fill_silence (buf->spec.info.finfo, + buf->memory, buf->size); + + return TRUE; +} + +static gboolean +gst_asio_ring_buffer_release (GstAudioRingBuffer * buf) +{ + GST_DEBUG_OBJECT (buf, "Release"); + + g_clear_pointer (&buf->memory, g_free); + + return TRUE; +} + +static gboolean +gst_asio_ring_buffer_start (GstAudioRingBuffer * buf) +{ + GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf); + GstAsioObjectCallbacks callbacks; + + GST_DEBUG_OBJECT (buf, "Start"); + + callbacks.buffer_switch = gst_asio_buffer_switch_cb; + callbacks.user_data = self; + + self->is_first = TRUE; + self->expected_sample_position = 0; + + if (!gst_asio_object_install_callback (self->asio_object, self->type, + &callbacks, &self->callback_id)) { + GST_ERROR_OBJECT (self, "Failed to install callback"); + return FALSE; + } + + self->callback_installed = TRUE; + + if (!gst_asio_object_start (self->asio_object)) { + GST_ERROR_OBJECT (self, "Failed to start"); + + gst_asio_ring_buffer_stop (buf); + + return FALSE; + } + + self->running = TRUE; + + return TRUE; +} + +static gboolean +gst_asio_ring_buffer_stop (GstAudioRingBuffer * buf) +{ + GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf); + + GST_DEBUG_OBJECT (buf, "Stop"); + + self->running = FALSE; + + if (!self->asio_object) + return TRUE; + + if (self->callback_installed) + gst_asio_object_uninstall_callback (self->asio_object, self->callback_id); + + self->callback_installed = FALSE; + self->callback_id = 0; + self->is_first = TRUE; + self->expected_sample_position = 0; + + return TRUE; +} + +static guint +gst_asio_ring_buffer_delay (GstAudioRingBuffer * buf) +{ + /* FIXME: impl. */ + + return 0; +} + +GstAsioRingBuffer * +gst_asio_ring_buffer_new (GstAsioObject * object, GstAsioDeviceClassType type, + const gchar * name) +{ + GstAsioRingBuffer *self; + + g_return_val_if_fail (GST_IS_ASIO_OBJECT (object), nullptr); + + self = + (GstAsioRingBuffer *) g_object_new (GST_TYPE_ASIO_RING_BUFFER, + "name", name, nullptr); + g_assert (self); + + self->type = type; + self->asio_object = (GstAsioObject *) gst_object_ref (object); + + return self; +} + +gboolean +gst_asio_ring_buffer_configure (GstAsioRingBuffer * buf, + guint * channel_indices, guint num_channles, guint preferred_buffer_size) +{ + g_return_val_if_fail (GST_IS_ASIO_RING_BUFFER (buf), FALSE); + g_return_val_if_fail (buf->asio_object != nullptr, FALSE); + g_return_val_if_fail (num_channles > 0, FALSE); + + GST_DEBUG_OBJECT (buf, "Configure"); + + buf->buffer_size = preferred_buffer_size; + + if (!gst_asio_object_create_buffers (buf->asio_object, buf->type, + channel_indices, num_channles, &buf->buffer_size)) { + GST_ERROR_OBJECT (buf, "Failed to configure"); + + g_clear_pointer (&buf->channel_indices, g_free); + buf->num_channels = 0; + + return FALSE; + } + + GST_DEBUG_OBJECT (buf, "configured buffer size: %d", buf->buffer_size); + + g_free (buf->channel_indices); + buf->channel_indices = g_new0 (guint, num_channles); + + for (guint i = 0; i < num_channles; i++) + buf->channel_indices[i] = channel_indices[i]; + + buf->num_channels = num_channles; + + g_clear_pointer (&buf->infos, g_free); + buf->infos = g_new0 (ASIOBufferInfo *, num_channles); + + return TRUE; +} + +GstCaps * +gst_asio_ring_buffer_get_caps (GstAsioRingBuffer * buf) +{ + g_return_val_if_fail (GST_IS_ASIO_RING_BUFFER (buf), nullptr); + g_assert (buf->asio_object != nullptr); + + return gst_asio_object_get_caps (buf->asio_object, + buf->type, buf->num_channels, buf->num_channels); +} diff --git a/sys/asio/gstasioringbuffer.h b/sys/asio/gstasioringbuffer.h new file mode 100644 index 000000000..338791c6f --- /dev/null +++ b/sys/asio/gstasioringbuffer.h @@ -0,0 +1,47 @@ +/* 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. + */ + +#ifndef __GST_ASIO_RING_BUFFER_H__ +#define __GST_ASIO_RING_BUFFER_H__ + +#include <gst/gst.h> +#include <gst/audio/audio.h> +#include "gstasioutils.h" +#include "gstasioobject.h" + +G_BEGIN_DECLS + +#define GST_TYPE_ASIO_RING_BUFFER (gst_asio_ring_buffer_get_type()) +G_DECLARE_FINAL_TYPE (GstAsioRingBuffer, gst_asio_ring_buffer, + GST, ASIO_RING_BUFFER, GstAudioRingBuffer); + +GstAsioRingBuffer * gst_asio_ring_buffer_new (GstAsioObject * object, + GstAsioDeviceClassType type, + const gchar * name); + +gboolean gst_asio_ring_buffer_configure (GstAsioRingBuffer * buf, + guint * channel_indices, + guint num_channles, + guint preferred_buffer_size); + +GstCaps * gst_asio_ring_buffer_get_caps (GstAsioRingBuffer * buf); + +G_END_DECLS + +#endif /* __GST_ASIO_RING_BUFFER_H__ */
\ No newline at end of file diff --git a/sys/asio/gstasiosink.cpp b/sys/asio/gstasiosink.cpp new file mode 100644 index 000000000..492e8fbb4 --- /dev/null +++ b/sys/asio/gstasiosink.cpp @@ -0,0 +1,361 @@ +/* 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 "gstasiosink.h" +#include "gstasioobject.h" +#include "gstasioringbuffer.h" +#include <atlconv.h> +#include <string.h> +#include <set> + +GST_DEBUG_CATEGORY_STATIC (gst_asio_sink_debug); +#define GST_CAT_DEFAULT gst_asio_sink_debug + +static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS (GST_ASIO_STATIC_CAPS)); + +enum +{ + PROP_0, + PROP_DEVICE_CLSID, + PROP_OUTPUT_CHANNELS, + PROP_BUFFER_SIZE, + PROP_OCCUPY_ALL_CHANNELS, +}; + +#define DEFAULT_BUFFER_SIZE 0 +#define DEFAULT_OCCUPY_ALL_CHANNELS TRUE + +struct _GstAsioSink +{ + GstAudioSink parent; + + /* properties */ + gchar *device_clsid; + gchar *output_channels; + guint buffer_size; + gboolean occupy_all_channels; +}; + +static void gst_asio_sink_finalize (GObject * object); +static void gst_asio_sink_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_asio_sink_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); + +static GstCaps *gst_asio_sink_get_caps (GstBaseSink * sink, GstCaps * filter); + +static GstAudioRingBuffer *gst_asio_sink_create_ringbuffer (GstAudioBaseSink * + sink); + +#define gst_asio_sink_parent_class parent_class +G_DEFINE_TYPE (GstAsioSink, gst_asio_sink, GST_TYPE_AUDIO_BASE_SINK); + +static void +gst_asio_sink_class_init (GstAsioSinkClass * klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GstElementClass *element_class = GST_ELEMENT_CLASS (klass); + GstBaseSinkClass *basesink_class = GST_BASE_SINK_CLASS (klass); + GstAudioBaseSinkClass *audiobasesink_class = + GST_AUDIO_BASE_SINK_CLASS (klass); + + gobject_class->finalize = gst_asio_sink_finalize; + gobject_class->set_property = gst_asio_sink_set_property; + gobject_class->get_property = gst_asio_sink_get_property; + + g_object_class_install_property (gobject_class, PROP_DEVICE_CLSID, + g_param_spec_string ("device-clsid", "Device CLSID", + "ASIO device CLSID as a string", NULL, + (GParamFlags) (GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); + g_object_class_install_property (gobject_class, PROP_OUTPUT_CHANNELS, + g_param_spec_string ("output-channels", "Output Channels", + "Comma-separated list of ASIO channels to output", NULL, + (GParamFlags) (GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); + g_object_class_install_property (gobject_class, PROP_BUFFER_SIZE, + g_param_spec_uint ("buffer-size", "Buffer Size", + "Preferred buffer size (0 for default)", + 0, G_MAXINT32, DEFAULT_BUFFER_SIZE, + (GParamFlags) (GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); + g_object_class_install_property (gobject_class, PROP_OCCUPY_ALL_CHANNELS, + g_param_spec_boolean ("occupy-all-channels", + "Occupy All Channles", + "When enabled, ASIO device will allocate resources for all in/output " + "channles", + DEFAULT_OCCUPY_ALL_CHANNELS, + (GParamFlags) (GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); + + gst_element_class_add_static_pad_template (element_class, &sink_template); + gst_element_class_set_static_metadata (element_class, "AsioSink", + "Source/Audio/Hardware", + "Stream audio from an audio capture device through ASIO", + "Seungha Yang <seungha@centricular.com>"); + + basesink_class->get_caps = GST_DEBUG_FUNCPTR (gst_asio_sink_get_caps); + + audiobasesink_class->create_ringbuffer = + GST_DEBUG_FUNCPTR (gst_asio_sink_create_ringbuffer); + + GST_DEBUG_CATEGORY_INIT (gst_asio_sink_debug, "asiosink", 0, "asiosink"); +} + +static void +gst_asio_sink_init (GstAsioSink * self) +{ + self->buffer_size = DEFAULT_BUFFER_SIZE; + self->occupy_all_channels = DEFAULT_OCCUPY_ALL_CHANNELS; +} + +static void +gst_asio_sink_finalize (GObject * object) +{ + GstAsioSink *self = GST_ASIO_SINK (object); + + g_free (self->device_clsid); + g_free (self->output_channels); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +gst_asio_sink_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstAsioSink *self = GST_ASIO_SINK (object); + + switch (prop_id) { + case PROP_DEVICE_CLSID: + g_free (self->device_clsid); + self->device_clsid = g_value_dup_string (value); + break; + case PROP_OUTPUT_CHANNELS: + g_free (self->output_channels); + self->output_channels = g_value_dup_string (value); + break; + case PROP_BUFFER_SIZE: + self->buffer_size = g_value_get_uint (value); + break; + case PROP_OCCUPY_ALL_CHANNELS: + self->occupy_all_channels = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_asio_sink_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstAsioSink *self = GST_ASIO_SINK (object); + + switch (prop_id) { + case PROP_DEVICE_CLSID: + g_value_set_string (value, self->device_clsid); + break; + case PROP_OUTPUT_CHANNELS: + g_value_set_string (value, self->output_channels); + break; + case PROP_BUFFER_SIZE: + g_value_set_uint (value, self->buffer_size); + break; + case PROP_OCCUPY_ALL_CHANNELS: + g_value_set_boolean (value, self->occupy_all_channels); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GstCaps * +gst_asio_sink_get_caps (GstBaseSink * sink, GstCaps * filter) +{ + GstAudioBaseSink *asink = GST_AUDIO_BASE_SINK (sink); + GstAsioSink *self = GST_ASIO_SINK (sink); + GstCaps *caps = nullptr; + + if (asink->ringbuffer) + caps = + gst_asio_ring_buffer_get_caps (GST_ASIO_RING_BUFFER + (asink->ringbuffer)); + + if (!caps) + caps = gst_pad_get_pad_template_caps (GST_BASE_SINK_PAD (sink)); + + if (filter) { + GstCaps *filtered = + gst_caps_intersect_full (filter, caps, GST_CAPS_INTERSECT_FIRST); + gst_caps_unref (caps); + caps = filtered; + } + + GST_DEBUG_OBJECT (self, "returning caps %" GST_PTR_FORMAT, caps); + + return caps; +} + +static GstAudioRingBuffer * +gst_asio_sink_create_ringbuffer (GstAudioBaseSink * sink) +{ + GstAsioSink *self = GST_ASIO_SINK (sink); + GstAsioRingBuffer *ringbuffer = nullptr; + HRESULT hr; + CLSID clsid = GUID_NULL; + GList *device_infos = nullptr; + GstAsioDeviceInfo *info = nullptr; + GstAsioObject *asio_object = nullptr; + glong max_input_ch = 0; + glong max_output_ch = 0; + guint *channel_indices = nullptr; + guint num_capture_channels = 0; + std::set < guint > channel_list; + guint i; + gchar *ringbuffer_name; + + USES_CONVERSION; + + GST_DEBUG_OBJECT (self, "Create ringbuffer"); + + if (gst_asio_enum (&device_infos) == 0) { + GST_WARNING_OBJECT (self, "No available ASIO devices"); + return nullptr; + } + + if (self->device_clsid) { + hr = CLSIDFromString (A2COLE (self->device_clsid), &clsid); + if (FAILED (hr)) { + GST_WARNING_OBJECT (self, "Failed to convert %s to CLSID", + self->device_clsid); + clsid = GUID_NULL; + } + } + + /* Pick the first device */ + if (clsid == GUID_NULL) { + info = (GstAsioDeviceInfo *) device_infos->data; + } else { + /* Find matching device */ + GList *iter; + for (iter = device_infos; iter; iter = g_list_next (iter)) { + GstAsioDeviceInfo *tmp = (GstAsioDeviceInfo *) iter->data; + if (tmp->clsid == clsid) { + info = tmp; + break; + } + } + } + + if (!info) { + GST_WARNING_OBJECT (self, "Failed to find matching device"); + goto out; + } + + asio_object = gst_asio_object_new (info, self->occupy_all_channels); + if (!asio_object) { + GST_WARNING_OBJECT (self, "Failed to create ASIO object"); + goto out; + } + + /* Configure channels to use */ + if (!gst_asio_object_get_max_num_channels (asio_object, &max_input_ch, + &max_output_ch) || max_input_ch <= 0) { + GST_WARNING_OBJECT (self, "No available input channels"); + goto out; + } + + /* Check if user requested specific channle(s) */ + if (self->output_channels) { + gchar **ch; + + ch = g_strsplit (self->output_channels, ",", 0); + + num_capture_channels = g_strv_length (ch); + if (num_capture_channels > max_input_ch) { + GST_WARNING_OBJECT (self, "To many channels %d were requested", + num_capture_channels); + } else { + for (i = 0; i < num_capture_channels; i++) { + guint64 c = g_ascii_strtoull (ch[i], nullptr, 0); + if (c >= (guint64) max_input_ch) { + GST_WARNING_OBJECT (self, "Invalid channel index"); + num_capture_channels = 0; + break; + } + + channel_list.insert ((guint) c); + } + } + + g_strfreev (ch); + } + + channel_indices = (guint *) g_alloca (sizeof (guint) * max_input_ch); + if (channel_list.size () == 0) { + for (i = 0; i < max_input_ch; i++) + channel_indices[i] = i; + + num_capture_channels = max_input_ch; + } else { + num_capture_channels = (guint) channel_list.size (); + i = 0; + for (auto iter:channel_list) { + channel_indices[i++] = iter; + } + } + + ringbuffer_name = g_strdup_printf ("%s-asioringbuffer", + GST_OBJECT_NAME (sink)); + + ringbuffer = + (GstAsioRingBuffer *) gst_asio_ring_buffer_new (asio_object, + GST_ASIO_DEVICE_CLASS_RENDER, ringbuffer_name); + g_free (ringbuffer_name); + + if (!ringbuffer) { + GST_WARNING_OBJECT (self, "Couldn't create ringbuffer object"); + goto out; + } + + if (!gst_asio_ring_buffer_configure (ringbuffer, channel_indices, + num_capture_channels, self->buffer_size)) { + GST_WARNING_OBJECT (self, "Failed to configure ringbuffer"); + gst_clear_object (&ringbuffer); + goto out; + } + +out: + if (device_infos) + g_list_free_full (device_infos, (GDestroyNotify) gst_asio_device_info_free); + + gst_clear_object (&asio_object); + + return GST_AUDIO_RING_BUFFER_CAST (ringbuffer); +} diff --git a/sys/asio/gstasiosink.h b/sys/asio/gstasiosink.h new file mode 100644 index 000000000..bffafb12a --- /dev/null +++ b/sys/asio/gstasiosink.h @@ -0,0 +1,34 @@ +/* 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. + */ + +#ifndef __GST_ASIO_SINK_H__ +#define __GST_ASIO_SINK_H__ + +#include <gst/gst.h> +#include <gst/audio/audio.h> + +G_BEGIN_DECLS + +#define GST_TYPE_ASIO_SINK (gst_asio_sink_get_type ()) +G_DECLARE_FINAL_TYPE (GstAsioSink, + gst_asio_sink, GST, ASIO_SINK, GstAudioBaseSink); + +G_END_DECLS + +#endif /* __GST_ASIO_SINK_H__ */ diff --git a/sys/asio/gstasiosrc.cpp b/sys/asio/gstasiosrc.cpp new file mode 100644 index 000000000..47bbad5f0 --- /dev/null +++ b/sys/asio/gstasiosrc.cpp @@ -0,0 +1,375 @@ +/* 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 "gstasiosrc.h" +#include "gstasioobject.h" +#include "gstasioringbuffer.h" +#include <atlconv.h> +#include <string.h> +#include <set> + +GST_DEBUG_CATEGORY_STATIC (gst_asio_src_debug); +#define GST_CAT_DEFAULT gst_asio_src_debug + +static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS (GST_ASIO_STATIC_CAPS)); + +enum +{ + PROP_0, + PROP_DEVICE_CLSID, + PROP_CAPTURE_CHANNELS, + PROP_BUFFER_SIZE, + PROP_OCCUPY_ALL_CHANNELS, + PROP_LOOPBACK, +}; + +#define DEFAULT_BUFFER_SIZE 0 +#define DEFAULT_OCCUPY_ALL_CHANNELS TRUE +#define DEFAULT_LOOPBACK FALSE + +struct _GstAsioSrc +{ + GstAudioSrc parent; + + /* properties */ + gchar *device_clsid; + gchar *capture_channles; + guint buffer_size; + gboolean occupy_all_channels; + gboolean loopback; +}; + +static void gst_asio_src_finalize (GObject * object); +static void gst_asio_src_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_asio_src_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); + +static GstCaps *gst_asio_src_get_caps (GstBaseSrc * src, GstCaps * filter); + +static GstAudioRingBuffer *gst_asio_src_create_ringbuffer (GstAudioBaseSrc * + src); + +#define gst_asio_src_parent_class parent_class +G_DEFINE_TYPE (GstAsioSrc, gst_asio_src, GST_TYPE_AUDIO_BASE_SRC); + +static void +gst_asio_src_class_init (GstAsioSrcClass * klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GstElementClass *element_class = GST_ELEMENT_CLASS (klass); + GstBaseSrcClass *basesrc_class = GST_BASE_SRC_CLASS (klass); + GstAudioBaseSrcClass *audiobasesrc_class = GST_AUDIO_BASE_SRC_CLASS (klass); + + gobject_class->finalize = gst_asio_src_finalize; + gobject_class->set_property = gst_asio_src_set_property; + gobject_class->get_property = gst_asio_src_get_property; + + g_object_class_install_property (gobject_class, PROP_DEVICE_CLSID, + g_param_spec_string ("device-clsid", "Device CLSID", + "ASIO device CLSID as a string", NULL, + (GParamFlags) (GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); + g_object_class_install_property (gobject_class, PROP_CAPTURE_CHANNELS, + g_param_spec_string ("input-channels", "Input Channels", + "Comma-separated list of ASIO channels to capture", NULL, + (GParamFlags) (GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); + g_object_class_install_property (gobject_class, PROP_BUFFER_SIZE, + g_param_spec_uint ("buffer-size", "Buffer Size", + "Preferred buffer size (0 for default)", + 0, G_MAXINT32, DEFAULT_BUFFER_SIZE, + (GParamFlags) (GST_PARAM_MUTABLE_READY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS))); + g_object_class_install_property (gobject_class, PROP_OCCUPY_ALL_CHANNELS, + g_param_spec_boolean ("occupy-all-channels", + "Occupy All Channles", + "When enabled, ASIO device will allocate resources for all in/output " + "channles", + DEFAULT_OCCUPY_ALL_CHANNELS, + (GParamFlags) (GST_PARAM_MUTABLE_READY | 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, + (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + + gst_element_class_add_static_pad_template (element_class, &src_template); + gst_element_class_set_static_metadata (element_class, "AsioSrc", + "Source/Audio/Hardware", + "Stream audio from an audio capture device through ASIO", + "Seungha Yang <seungha@centricular.com>"); + + basesrc_class->get_caps = GST_DEBUG_FUNCPTR (gst_asio_src_get_caps); + + audiobasesrc_class->create_ringbuffer = + GST_DEBUG_FUNCPTR (gst_asio_src_create_ringbuffer); + + GST_DEBUG_CATEGORY_INIT (gst_asio_src_debug, "asiosrc", 0, "asiosrc"); +} + +static void +gst_asio_src_init (GstAsioSrc * self) +{ + self->buffer_size = DEFAULT_BUFFER_SIZE; + self->occupy_all_channels = DEFAULT_OCCUPY_ALL_CHANNELS; + self->loopback = DEFAULT_LOOPBACK; +} + +static void +gst_asio_src_finalize (GObject * object) +{ + GstAsioSrc *self = GST_ASIO_SRC (object); + + g_free (self->device_clsid); + g_free (self->capture_channles); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +gst_asio_src_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstAsioSrc *self = GST_ASIO_SRC (object); + + switch (prop_id) { + case PROP_DEVICE_CLSID: + g_free (self->device_clsid); + self->device_clsid = g_value_dup_string (value); + break; + case PROP_CAPTURE_CHANNELS: + g_free (self->capture_channles); + self->capture_channles = g_value_dup_string (value); + break; + case PROP_BUFFER_SIZE: + self->buffer_size = g_value_get_uint (value); + break; + case PROP_OCCUPY_ALL_CHANNELS: + self->occupy_all_channels = g_value_get_boolean (value); + break; + case PROP_LOOPBACK: + self->loopback = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_asio_src_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstAsioSrc *self = GST_ASIO_SRC (object); + + switch (prop_id) { + case PROP_DEVICE_CLSID: + g_value_set_string (value, self->device_clsid); + break; + case PROP_CAPTURE_CHANNELS: + g_value_set_string (value, self->capture_channles); + break; + case PROP_BUFFER_SIZE: + g_value_set_uint (value, self->buffer_size); + break; + case PROP_OCCUPY_ALL_CHANNELS: + g_value_set_boolean (value, self->occupy_all_channels); + break; + case PROP_LOOPBACK: + g_value_set_boolean (value, self->loopback); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GstCaps * +gst_asio_src_get_caps (GstBaseSrc * src, GstCaps * filter) +{ + GstAudioBaseSrc *asrc = GST_AUDIO_BASE_SRC (src); + GstAsioSrc *self = GST_ASIO_SRC (src); + GstCaps *caps = nullptr; + + if (asrc->ringbuffer) + caps = + gst_asio_ring_buffer_get_caps (GST_ASIO_RING_BUFFER (asrc->ringbuffer)); + + if (!caps) + caps = gst_pad_get_pad_template_caps (GST_BASE_SRC_PAD (src)); + + if (filter) { + GstCaps *filtered = + gst_caps_intersect_full (filter, caps, GST_CAPS_INTERSECT_FIRST); + gst_caps_unref (caps); + caps = filtered; + } + + GST_DEBUG_OBJECT (self, "returning caps %" GST_PTR_FORMAT, caps); + + return caps; +} + +static GstAudioRingBuffer * +gst_asio_src_create_ringbuffer (GstAudioBaseSrc * src) +{ + GstAsioSrc *self = GST_ASIO_SRC (src); + GstAsioRingBuffer *ringbuffer = nullptr; + HRESULT hr; + CLSID clsid = GUID_NULL; + GList *device_infos = nullptr; + GstAsioDeviceInfo *info = nullptr; + GstAsioObject *asio_object = nullptr; + glong max_input_ch = 0; + glong max_output_ch = 0; + guint *channel_indices = nullptr; + guint num_capture_channels = 0; + std::set < guint > channel_list; + guint i; + gchar *ringbuffer_name; + + USES_CONVERSION; + + GST_DEBUG_OBJECT (self, "Create ringbuffer"); + + if (gst_asio_enum (&device_infos) == 0) { + GST_WARNING_OBJECT (self, "No available ASIO devices"); + return nullptr; + } + + if (self->device_clsid) { + hr = CLSIDFromString (A2COLE (self->device_clsid), &clsid); + if (FAILED (hr)) { + GST_WARNING_OBJECT (self, "Failed to convert %s to CLSID", + self->device_clsid); + clsid = GUID_NULL; + } + } + + /* Pick the first device */ + if (clsid == GUID_NULL) { + info = (GstAsioDeviceInfo *) device_infos->data; + } else { + /* Find matching device */ + GList *iter; + for (iter = device_infos; iter; iter = g_list_next (iter)) { + GstAsioDeviceInfo *tmp = (GstAsioDeviceInfo *) iter->data; + if (tmp->clsid == clsid) { + info = tmp; + break; + } + } + } + + if (!info) { + GST_WARNING_OBJECT (self, "Failed to find matching device"); + goto out; + } + + asio_object = gst_asio_object_new (info, self->occupy_all_channels); + if (!asio_object) { + GST_WARNING_OBJECT (self, "Failed to create ASIO object"); + goto out; + } + + /* Configure channels to use */ + if (!gst_asio_object_get_max_num_channels (asio_object, &max_input_ch, + &max_output_ch) || max_input_ch <= 0) { + GST_WARNING_OBJECT (self, "No available input channels"); + goto out; + } + + /* Check if user requested specific channel(s) */ + if (self->capture_channles) { + gchar **ch; + + ch = g_strsplit (self->capture_channles, ",", 0); + + num_capture_channels = g_strv_length (ch); + if (num_capture_channels > max_input_ch) { + GST_WARNING_OBJECT (self, "To many channels %d were requested", + num_capture_channels); + } else { + for (i = 0; i < num_capture_channels; i++) { + guint64 c = g_ascii_strtoull (ch[i], nullptr, 0); + if (c >= (guint64) max_input_ch) { + GST_WARNING_OBJECT (self, "Invalid channel index"); + num_capture_channels = 0; + break; + } + + channel_list.insert ((guint) c); + } + } + + g_strfreev (ch); + } + + channel_indices = (guint *) g_alloca (sizeof (guint) * max_input_ch); + if (channel_list.size () == 0) { + for (i = 0; i < max_input_ch; i++) + channel_indices[i] = i; + + num_capture_channels = max_input_ch; + } else { + num_capture_channels = (guint) channel_list.size (); + i = 0; + for (auto iter:channel_list) { + channel_indices[i++] = iter; + } + } + + ringbuffer_name = g_strdup_printf ("%s-asioringbuffer", + GST_OBJECT_NAME (src)); + + ringbuffer = + (GstAsioRingBuffer *) gst_asio_ring_buffer_new (asio_object, + self->loopback ? GST_ASIO_DEVICE_CLASS_LOOPBACK_CAPTURE : + GST_ASIO_DEVICE_CLASS_CAPTURE, ringbuffer_name); + g_free (ringbuffer_name); + + if (!ringbuffer) { + GST_WARNING_OBJECT (self, "Couldn't create ringbuffer object"); + goto out; + } + + if (!gst_asio_ring_buffer_configure (ringbuffer, channel_indices, + num_capture_channels, self->buffer_size)) { + GST_WARNING_OBJECT (self, "Failed to configure ringbuffer"); + gst_clear_object (&ringbuffer); + goto out; + } + +out: + if (device_infos) + g_list_free_full (device_infos, (GDestroyNotify) gst_asio_device_info_free); + + gst_clear_object (&asio_object); + + return GST_AUDIO_RING_BUFFER_CAST (ringbuffer); +} diff --git a/sys/asio/gstasiosrc.h b/sys/asio/gstasiosrc.h new file mode 100644 index 000000000..704ddcb7d --- /dev/null +++ b/sys/asio/gstasiosrc.h @@ -0,0 +1,34 @@ +/* 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. + */ + +#ifndef __GST_ASIO_SRC_H__ +#define __GST_ASIO_SRC_H__ + +#include <gst/gst.h> +#include <gst/audio/audio.h> + +G_BEGIN_DECLS + +#define GST_TYPE_ASIO_SRC (gst_asio_src_get_type ()) +G_DECLARE_FINAL_TYPE (GstAsioSrc, + gst_asio_src, GST, ASIO_SRC, GstAudioBaseSrc); + +G_END_DECLS + +#endif /* __GST_ASIO_SRC_H__ */ diff --git a/sys/asio/gstasioutils.cpp b/sys/asio/gstasioutils.cpp new file mode 100644 index 000000000..687af9c94 --- /dev/null +++ b/sys/asio/gstasioutils.cpp @@ -0,0 +1,282 @@ +/* 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 "gstasioutils.h" +#include <windows.h> +#include <string.h> +#include <atlconv.h> + +static gboolean +gst_asio_enum_check_class_root (GstAsioDeviceInfo * info, LPCWSTR clsid) +{ + LSTATUS status; + HKEY root_key = nullptr; + HKEY device_key = nullptr; + HKEY proc_server_key = nullptr; + DWORD type = REG_SZ; + CHAR data[256]; + DWORD size = sizeof (data); + gboolean ret = FALSE; + + status = RegOpenKeyExW (HKEY_CLASSES_ROOT, L"clsid", 0, KEY_READ, &root_key); + if (status != ERROR_SUCCESS) + return FALSE; + + /* Read registry HKEY_CLASS_ROOT/CLSID/{device-clsid} */ + status = RegOpenKeyExW (root_key, clsid, 0, KEY_READ, &device_key); + if (status != ERROR_SUCCESS) + goto done; + + /* ThreadingModel describes COM apartment */ + status = RegOpenKeyExW (device_key, + L"InprocServer32", 0, KEY_READ, &proc_server_key); + if (status != ERROR_SUCCESS) + goto done; + + status = RegQueryValueExA (proc_server_key, + "ThreadingModel", nullptr, &type, (LPBYTE) data, &size); + if (status != ERROR_SUCCESS) + goto done; + + if (g_ascii_strcasecmp (data, "Both") == 0 || + g_ascii_strcasecmp (data, "Free") == 0) { + info->sta_model = FALSE; + } else { + info->sta_model = TRUE; + } + + ret = TRUE; + +done: + if (proc_server_key) + RegCloseKey (proc_server_key); + + if (device_key) + RegCloseKey (device_key); + + if (root_key) + RegCloseKey (root_key); + + return ret; +} + +static GstAsioDeviceInfo * +gst_asio_enum_new_device_info_from_reg (HKEY reg_key, LPWSTR key_name) +{ + LSTATUS status; + HKEY sub_key = nullptr; + WCHAR clsid_data[256]; + WCHAR desc_data[256]; + DWORD type = REG_SZ; + DWORD size = sizeof (clsid_data); + GstAsioDeviceInfo *ret = nullptr; + CLSID id; + HRESULT hr; + + USES_CONVERSION; + + status = RegOpenKeyExW (reg_key, key_name, 0, KEY_READ, &sub_key); + if (status != ERROR_SUCCESS) + return nullptr; + + /* find CLSID value, used for CoCreateInstance */ + status = RegQueryValueExW (sub_key, + L"clsid", 0, &type, (LPBYTE) clsid_data, &size); + if (status != ERROR_SUCCESS) + goto done; + + hr = CLSIDFromString (W2COLE (clsid_data), &id); + if (FAILED (hr)) + goto done; + + ret = g_new0 (GstAsioDeviceInfo, 1); + ret->clsid = id; + ret->driver_name = g_utf16_to_utf8 ((gunichar2 *) key_name, -1, + nullptr, nullptr, nullptr); + + /* human readable device description */ + status = RegQueryValueExW (sub_key, + L"description", 0, &type, (LPBYTE) desc_data, &size); + if (status != ERROR_SUCCESS) { + GST_WARNING ("no description"); + ret->driver_desc = g_strdup (ret->driver_name); + } else { + ret->driver_desc = g_utf16_to_utf8 ((gunichar2 *) desc_data, -1, + nullptr, nullptr, nullptr); + } + + /* Check COM threading model */ + if (!gst_asio_enum_check_class_root (ret, clsid_data)) { + gst_asio_device_info_free (ret); + ret = nullptr; + } + +done: + if (sub_key) + RegCloseKey (sub_key); + + return ret; +} + +guint +gst_asio_enum (GList ** infos) +{ + GList *info_list = nullptr; + DWORD index = 0; + guint num_device = 0; + LSTATUS status; + HKEY reg_key = nullptr; + WCHAR key_name[512]; + + g_return_val_if_fail (infos != nullptr, 0); + + status = RegOpenKeyExW (HKEY_LOCAL_MACHINE, L"software\\asio", 0, + KEY_READ, ®_key); + while (status == ERROR_SUCCESS) { + GstAsioDeviceInfo *info; + + status = RegEnumKeyW (reg_key, index, key_name, 512); + if (status != ERROR_SUCCESS) + break; + + index++; + info = gst_asio_enum_new_device_info_from_reg (reg_key, key_name); + if (!info) + continue; + + info_list = g_list_append (info_list, info); + num_device++; + } + + if (reg_key) + RegCloseKey (reg_key); + + *infos = info_list; + + return num_device; +} + +GstAsioDeviceInfo * +gst_asio_device_info_copy (const GstAsioDeviceInfo * info) +{ + GstAsioDeviceInfo *new_info; + + if (!info) + return nullptr; + + new_info = g_new0 (GstAsioDeviceInfo, 1); + + new_info->clsid = info->clsid; + new_info->sta_model = info->sta_model; + new_info->driver_name = g_strdup (info->driver_name); + new_info->driver_desc = g_strdup (info->driver_desc); + + return new_info; +} + +void +gst_asio_device_info_free (GstAsioDeviceInfo * info) +{ + if (!info) + return; + + g_free (info->driver_name); + g_free (info->driver_desc); + + g_free (info); +} + +GstAudioFormat +gst_asio_sample_type_to_gst (ASIOSampleType type) +{ + GstAudioFormat fmt; + + switch (type) { + /*~~ MSB means big endian ~~ */ + case ASIOSTInt16MSB: + fmt = GST_AUDIO_FORMAT_S16BE; + break; + /* FIXME: also used for 20 bits packed in 24 bits, how do we detect that? */ + case ASIOSTInt24MSB: + fmt = GST_AUDIO_FORMAT_S24BE; + break; + case ASIOSTInt32MSB: + fmt = GST_AUDIO_FORMAT_S32BE; + break; + case ASIOSTFloat32MSB: + fmt = GST_AUDIO_FORMAT_F32BE; + break; + case ASIOSTFloat64MSB: + fmt = GST_AUDIO_FORMAT_F64BE; + break; + /* All these are aligned to a different boundary than the packing, not sure + * how to handle it, let's try the normal S32BE format */ + case ASIOSTInt32MSB16: + case ASIOSTInt32MSB18: + case ASIOSTInt32MSB20: + case ASIOSTInt32MSB24: + fmt = GST_AUDIO_FORMAT_S32BE; + break; + + /*~~ LSB means little endian ~~ */ + case ASIOSTInt16LSB: + fmt = GST_AUDIO_FORMAT_S16LE; + break; + /* FIXME: also used for 20 bits packed in 24 bits, how do we detect that? */ + case ASIOSTInt24LSB: + fmt = GST_AUDIO_FORMAT_S24LE; + break; + case ASIOSTInt32LSB: + fmt = GST_AUDIO_FORMAT_S32LE; + break; + case ASIOSTFloat32LSB: + fmt = GST_AUDIO_FORMAT_F32LE; + break; + case ASIOSTFloat64LSB: + fmt = GST_AUDIO_FORMAT_F64LE; + break; + /* All these are aligned to a different boundary than the packing, not sure + * how to handle it, let's try the normal S32LE format */ + case ASIOSTInt32LSB16: + case ASIOSTInt32LSB18: + case ASIOSTInt32LSB20: + case ASIOSTInt32LSB24: + GST_WARNING ("weird alignment %ld, trying S32LE", type); + fmt = GST_AUDIO_FORMAT_S32LE; + break; + + /*~~ ASIO DSD formats are don't have gstreamer mappings ~~ */ + case ASIOSTDSDInt8LSB1: + case ASIOSTDSDInt8MSB1: + case ASIOSTDSDInt8NER8: + GST_ERROR ("ASIO DSD formats are not supported"); + fmt = GST_AUDIO_FORMAT_UNKNOWN; + break; + default: + GST_ERROR ("Unknown asio sample type %ld", type); + fmt = GST_AUDIO_FORMAT_UNKNOWN; + break; + } + + return fmt; +} diff --git a/sys/asio/gstasioutils.h b/sys/asio/gstasioutils.h new file mode 100644 index 000000000..ef159c7e0 --- /dev/null +++ b/sys/asio/gstasioutils.h @@ -0,0 +1,55 @@ +/* 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. + */ + +#ifndef __GST_ASIO_DEVICE_ENUM_H__ +#define __GST_ASIO_DEVICE_ENUM_H__ + +#include <gst/gst.h> +#include <gst/audio/audio.h> +#include <windows.h> +#include <asiosys.h> +#include <asio.h> + +G_BEGIN_DECLS + +#define GST_ASIO_STATIC_CAPS "audio/x-raw, " \ + "format = (string) " GST_AUDIO_FORMATS_ALL ", " \ + "layout = (string) interleaved, " \ + "rate = " GST_AUDIO_RATE_RANGE ", " \ + "channels = " GST_AUDIO_CHANNELS_RANGE + +typedef struct +{ + CLSID clsid; + gboolean sta_model; + gchar *driver_name; + gchar *driver_desc; +} GstAsioDeviceInfo; + +guint gst_asio_enum (GList ** infos); + +GstAsioDeviceInfo * gst_asio_device_info_copy (const GstAsioDeviceInfo * info); + +void gst_asio_device_info_free (GstAsioDeviceInfo * info); + +GstAudioFormat gst_asio_sample_type_to_gst (ASIOSampleType type); + +G_END_DECLS + +#endif /* __GST_ASIO_DEVICE_ENUM_H__ */
\ No newline at end of file diff --git a/sys/asio/meson.build b/sys/asio/meson.build new file mode 100644 index 000000000..3006d26ce --- /dev/null +++ b/sys/asio/meson.build @@ -0,0 +1,84 @@ +asio_sources = [ + 'gstasiodeviceprovider.cpp', + 'gstasioobject.cpp', + 'gstasioringbuffer.cpp', + 'gstasiosink.cpp', + 'gstasiosrc.cpp', + 'gstasioutils.cpp', + 'plugin.c', +] + +asio_option = get_option('asio') +if asio_option.disabled() or host_system != 'windows' + subdir_done() +endif + +# FIXME: non-msvc is not tested, and unlikely supported yet because of +# tool-chain issue +if cxx.get_id() != 'msvc' + if asio_option.enabled() + error('asio plugin can only be built with MSVC') + else + subdir_done () + endif +endif + +winapi_desktop = cxx.compiles('''#include <winapifamily.h> + #if !WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) + #error "not win32" + #endif''', + name: 'building for Win32') + +if not winapi_desktop + if asio_option.enabled() + error('asio plugin requires WINAPI_PARTITION_DESKTOP') + else + subdir_done () + endif +endif + +avrt_lib = cc.find_library('avrt', required: asio_option) +if not avrt_lib.found() + subdir_done () +endif + +winmm_lib = cc.find_library('winmm', required: asio_option) +if not winmm_lib.found() + subdir_done () +endif + +# Checking SDK headers. User should install ASIO sdk on system, and +# this plugin requires asio.h, asiosys.h and iasiodrv.h headers +asio_sdk_root = get_option ('asio-sdk-path') +if asio_sdk_root == '' + if asio_option.enabled() + error('asio sdk path is needed, pass with -Dasio-sdk-path=C:/path/to/sdk') + else + subdir_done () + endif +endif + +asio_inc_dir = include_directories(join_paths(asio_sdk_root, 'common'), is_system : true) +has_asio_header = cxx.has_header('asio.h', include_directories: asio_inc_dir) +has_asiosys_header = cxx.has_header('asiosys.h', include_directories: asio_inc_dir) +has_iasiodrv_header = cxx.has_header('iasiodrv.h', include_directories: asio_inc_dir) +if not has_asio_header or not has_asiosys_header or not has_iasiodrv_header + if asio_option.enabled() + error('Failed to find required SDK header(s)') + else + subdir_done () + endif +endif + +asio_deps = [gstaudio_dep, avrt_lib, winmm_lib] + +gstasio = library('gstasio', + asio_sources, + include_directories : [configinc, asio_inc_dir], + dependencies : asio_deps, + c_args : gst_plugins_bad_args, + cpp_args : gst_plugins_bad_args, + install : true, + install_dir : plugins_install_dir) +pkgconfig.generate(gstasio, install_dir : plugins_pkgconfig_install_dir) +plugins += [gstasio]
\ No newline at end of file diff --git a/sys/asio/plugin.c b/sys/asio/plugin.c new file mode 100644 index 000000000..55811372c --- /dev/null +++ b/sys/asio/plugin.c @@ -0,0 +1,48 @@ +/* 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 "gstasiodeviceprovider.h" +#include "gstasiosrc.h" +#include "gstasiosink.h" + +static gboolean +plugin_init (GstPlugin * plugin) +{ + GstRank rank = GST_RANK_SECONDARY; + + if (!gst_element_register (plugin, "asiosrc", rank, GST_TYPE_ASIO_SRC)) + return FALSE; + if (!gst_element_register (plugin, "asiosink", rank, GST_TYPE_ASIO_SINK)) + return FALSE; + if (!gst_device_provider_register (plugin, "asiodeviceprovider", + rank, GST_TYPE_ASIO_DEVICE_PROVIDER)) + return FALSE; + + return TRUE; +} + +GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, + GST_VERSION_MINOR, + asio, + "Steinberg ASIO plugin", + plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN) diff --git a/sys/meson.build b/sys/meson.build index cf39e5770..f63507737 100644 --- a/sys/meson.build +++ b/sys/meson.build @@ -1,5 +1,6 @@ subdir('androidmedia') subdir('applemedia') +subdir('asio') subdir('bluez') subdir('d3d11') subdir('d3dvideosink') |