mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-25 17:50:36 +00:00
dc787434bc
Adds a new plugin for ASIO devices. Although there is a standard low-level audio API, WASAPI, on Windows, ASIO is still being broadly used for audio devices which are aiming to professional use case. In case of such devices, ASIO API might be able to show better quality and latency performance depending on manufacturer's driver implementation. In order to build this plugin, user should provide path to ASIO SDK as a build option, "asio-sdk-path". Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/merge_requests/2309>
1875 lines
51 KiB
C++
1875 lines
51 KiB
C++
/* 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);
|
|
}
|