mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-27 10:40:34 +00:00
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);
|
|
}
|