mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-24 17:20: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>
473 lines
13 KiB
C++
473 lines
13 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 "gstasioringbuffer.h"
|
|
#include <string.h>
|
|
#include "gstasioutils.h"
|
|
#include "gstasioobject.h"
|
|
|
|
GST_DEBUG_CATEGORY_STATIC (gst_asio_ring_buffer_debug);
|
|
#define GST_CAT_DEFAULT gst_asio_ring_buffer_debug
|
|
|
|
struct _GstAsioRingBuffer
|
|
{
|
|
GstAudioRingBuffer parent;
|
|
|
|
GstAsioDeviceClassType type;
|
|
|
|
GstAsioObject *asio_object;
|
|
guint *channel_indices;
|
|
guint num_channels;
|
|
ASIOBufferInfo **infos;
|
|
|
|
guint64 callback_id;
|
|
gboolean callback_installed;
|
|
|
|
gboolean running;
|
|
guint buffer_size;
|
|
|
|
/* Used to detect sample gap */
|
|
gboolean is_first;
|
|
guint64 expected_sample_position;
|
|
gboolean trace_sample_position;
|
|
};
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_DEVICE_INFO,
|
|
};
|
|
|
|
static void gst_asio_ring_buffer_dispose (GObject * object);
|
|
|
|
static gboolean gst_asio_ring_buffer_open_device (GstAudioRingBuffer * buf);
|
|
static gboolean gst_asio_ring_buffer_close_device (GstAudioRingBuffer * buf);
|
|
static gboolean gst_asio_ring_buffer_acquire (GstAudioRingBuffer * buf,
|
|
GstAudioRingBufferSpec * spec);
|
|
static gboolean gst_asio_ring_buffer_release (GstAudioRingBuffer * buf);
|
|
static gboolean gst_asio_ring_buffer_start (GstAudioRingBuffer * buf);
|
|
static gboolean gst_asio_ring_buffer_stop (GstAudioRingBuffer * buf);
|
|
static guint gst_asio_ring_buffer_delay (GstAudioRingBuffer * buf);
|
|
|
|
static gboolean gst_asio_buffer_switch_cb (GstAsioObject * obj,
|
|
glong index, ASIOBufferInfo * infos, guint num_infos,
|
|
ASIOChannelInfo * input_channel_infos,
|
|
ASIOChannelInfo * output_channel_infos,
|
|
ASIOSampleRate sample_rate, glong buffer_size, gpointer user_data);
|
|
|
|
#define gst_asio_ring_buffer_parent_class parent_class
|
|
G_DEFINE_TYPE (GstAsioRingBuffer, gst_asio_ring_buffer,
|
|
GST_TYPE_AUDIO_RING_BUFFER);
|
|
|
|
static void
|
|
gst_asio_ring_buffer_class_init (GstAsioRingBufferClass * klass)
|
|
{
|
|
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
|
GstAudioRingBufferClass *ring_buffer_class =
|
|
GST_AUDIO_RING_BUFFER_CLASS (klass);
|
|
|
|
gobject_class->dispose = gst_asio_ring_buffer_dispose;
|
|
|
|
ring_buffer_class->open_device =
|
|
GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_open_device);
|
|
ring_buffer_class->close_device =
|
|
GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_close_device);
|
|
ring_buffer_class->acquire = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_acquire);
|
|
ring_buffer_class->release = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_release);
|
|
ring_buffer_class->start = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_start);
|
|
ring_buffer_class->resume = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_start);
|
|
ring_buffer_class->stop = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_stop);
|
|
ring_buffer_class->delay = GST_DEBUG_FUNCPTR (gst_asio_ring_buffer_delay);
|
|
|
|
GST_DEBUG_CATEGORY_INIT (gst_asio_ring_buffer_debug,
|
|
"asioringbuffer", 0, "asioringbuffer");
|
|
}
|
|
|
|
static void
|
|
gst_asio_ring_buffer_init (GstAsioRingBuffer * self)
|
|
{
|
|
}
|
|
|
|
static void
|
|
gst_asio_ring_buffer_dispose (GObject * object)
|
|
{
|
|
GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (object);
|
|
|
|
gst_clear_object (&self->asio_object);
|
|
g_clear_pointer (&self->channel_indices, g_free);
|
|
|
|
G_OBJECT_CLASS (parent_class)->dispose (object);
|
|
}
|
|
|
|
static gboolean
|
|
gst_asio_ring_buffer_open_device (GstAudioRingBuffer * buf)
|
|
{
|
|
GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf);
|
|
|
|
GST_DEBUG_OBJECT (self, "Open");
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_asio_ring_buffer_close_device (GstAudioRingBuffer * buf)
|
|
{
|
|
GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf);
|
|
|
|
GST_DEBUG_OBJECT (self, "Close");
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
#define PACK_ASIO_64(v) ((v).lo | ((guint64)((v).hi) << 32))
|
|
|
|
static gboolean
|
|
gst_asio_buffer_switch_cb (GstAsioObject * obj, glong index,
|
|
ASIOBufferInfo * infos, guint num_infos,
|
|
ASIOChannelInfo * input_channel_infos,
|
|
ASIOChannelInfo * output_channel_infos,
|
|
ASIOSampleRate sample_rate, glong buffer_size,
|
|
ASIOTime * time_info, gpointer user_data)
|
|
{
|
|
GstAsioRingBuffer *self = (GstAsioRingBuffer *) user_data;
|
|
GstAudioRingBuffer *ringbuffer = GST_AUDIO_RING_BUFFER_CAST (self);
|
|
gint segment;
|
|
guint8 *readptr;
|
|
gint len;
|
|
guint i, j;
|
|
guint num_channels = 0;
|
|
guint bps = GST_AUDIO_INFO_WIDTH (&ringbuffer->spec.info) >> 3;
|
|
|
|
g_assert (index == 0 || index == 1);
|
|
g_assert (num_infos >= self->num_channels);
|
|
|
|
GST_TRACE_OBJECT (self, "Buffer Switch callback, index %ld", index);
|
|
|
|
if (!gst_audio_ring_buffer_prepare_read (ringbuffer,
|
|
&segment, &readptr, &len)) {
|
|
GST_WARNING_OBJECT (self, "No segment available");
|
|
return TRUE;
|
|
}
|
|
|
|
GST_TRACE_OBJECT (self, "segment %d, length %d", segment, len);
|
|
|
|
/* Check missing frames */
|
|
if (self->type == GST_ASIO_DEVICE_CLASS_CAPTURE) {
|
|
if (self->is_first) {
|
|
if (time_info) {
|
|
self->expected_sample_position =
|
|
PACK_ASIO_64 (time_info->timeInfo.samplePosition) + buffer_size;
|
|
self->trace_sample_position = TRUE;
|
|
} else {
|
|
GST_WARNING_OBJECT (self, "ASIOTime is not available");
|
|
self->trace_sample_position = FALSE;
|
|
}
|
|
|
|
self->is_first = FALSE;
|
|
} else if (self->trace_sample_position) {
|
|
if (!time_info) {
|
|
GST_WARNING_OBJECT (self, "ASIOTime is not available");
|
|
self->trace_sample_position = FALSE;
|
|
} else {
|
|
guint64 sample_position =
|
|
PACK_ASIO_64 (time_info->timeInfo.samplePosition);
|
|
if (self->expected_sample_position < sample_position) {
|
|
guint64 gap_frames = sample_position - self->expected_sample_position;
|
|
gint gap_size = gap_frames * bps;
|
|
|
|
GST_WARNING_OBJECT (self, "%" G_GUINT64_FORMAT " frames are missing");
|
|
|
|
while (gap_size >= len) {
|
|
gst_audio_format_info_fill_silence (ringbuffer->spec.info.finfo,
|
|
readptr, len);
|
|
gst_audio_ring_buffer_advance (ringbuffer, 1);
|
|
|
|
gst_audio_ring_buffer_prepare_read (ringbuffer,
|
|
&segment, &readptr, &len);
|
|
|
|
gap_size -= len;
|
|
}
|
|
}
|
|
|
|
self->expected_sample_position = sample_position + buffer_size;
|
|
GST_TRACE_OBJECT (self, "Sample Position %" G_GUINT64_FORMAT
|
|
", next: %" G_GUINT64_FORMAT, sample_position,
|
|
self->expected_sample_position);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Given @infos might contain more channel data, pick channels what we want to
|
|
* read */
|
|
for (i = 0; i < num_infos; i++) {
|
|
ASIOBufferInfo *info = &infos[i];
|
|
|
|
if (self->type == GST_ASIO_DEVICE_CLASS_CAPTURE) {
|
|
if (!info->isInput)
|
|
continue;
|
|
} else {
|
|
if (info->isInput)
|
|
continue;
|
|
}
|
|
|
|
for (j = 0; j < self->num_channels; j++) {
|
|
if (self->channel_indices[j] != info->channelNum)
|
|
continue;
|
|
|
|
g_assert (num_channels < self->num_channels);
|
|
self->infos[num_channels++] = info;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (num_channels < self->num_channels) {
|
|
GST_ERROR_OBJECT (self, "Too small number of channel %d (expected %d)",
|
|
num_channels, self->num_channels);
|
|
} else {
|
|
if (self->type == GST_ASIO_DEVICE_CLASS_CAPTURE ||
|
|
self->type == GST_ASIO_DEVICE_CLASS_LOOPBACK_CAPTURE) {
|
|
if (num_channels == 1) {
|
|
memcpy (readptr, self->infos[0]->buffers[index], len);
|
|
} else {
|
|
guint gst_offset = 0, asio_offset = 0;
|
|
|
|
/* Interleaves audio */
|
|
while (gst_offset < len) {
|
|
for (i = 0; i < num_channels; i++) {
|
|
ASIOBufferInfo *info = self->infos[i];
|
|
|
|
memcpy (readptr + gst_offset,
|
|
((guint8 *) info->buffers[index]) + asio_offset, bps);
|
|
|
|
gst_offset += bps;
|
|
}
|
|
asio_offset += bps;
|
|
}
|
|
}
|
|
} else {
|
|
if (num_channels == 1) {
|
|
memcpy (self->infos[0]->buffers[index], readptr, len);
|
|
} else {
|
|
guint gst_offset = 0, asio_offset = 0;
|
|
|
|
/* Interleaves audio */
|
|
while (gst_offset < len) {
|
|
for (i = 0; i < num_channels; i++) {
|
|
ASIOBufferInfo *info = self->infos[i];
|
|
|
|
memcpy (((guint8 *) info->buffers[index]) + asio_offset,
|
|
readptr + gst_offset, bps);
|
|
|
|
gst_offset += bps;
|
|
}
|
|
asio_offset += bps;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (self->type == GST_ASIO_DEVICE_CLASS_RENDER)
|
|
gst_audio_ring_buffer_clear (ringbuffer, segment);
|
|
gst_audio_ring_buffer_advance (ringbuffer, 1);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_asio_ring_buffer_acquire (GstAudioRingBuffer * buf,
|
|
GstAudioRingBufferSpec * spec)
|
|
{
|
|
GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf);
|
|
|
|
if (!self->asio_object) {
|
|
GST_ERROR_OBJECT (self, "No configured ASIO object");
|
|
return FALSE;
|
|
}
|
|
|
|
if (!self->channel_indices || self->num_channels == 0) {
|
|
GST_ERROR_OBJECT (self, "No configured channels");
|
|
return FALSE;
|
|
}
|
|
|
|
if (!gst_asio_object_set_sample_rate (self->asio_object,
|
|
GST_AUDIO_INFO_RATE (&spec->info))) {
|
|
GST_ERROR_OBJECT (self, "Failed to set sample rate");
|
|
return FALSE;
|
|
}
|
|
|
|
spec->segsize = self->buffer_size *
|
|
(GST_AUDIO_INFO_WIDTH (&spec->info) >> 3) *
|
|
GST_AUDIO_INFO_CHANNELS (&spec->info);
|
|
spec->segtotal = 2;
|
|
|
|
buf->size = spec->segtotal * spec->segsize;
|
|
buf->memory = (guint8 *) g_malloc (buf->size);
|
|
gst_audio_format_info_fill_silence (buf->spec.info.finfo,
|
|
buf->memory, buf->size);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_asio_ring_buffer_release (GstAudioRingBuffer * buf)
|
|
{
|
|
GST_DEBUG_OBJECT (buf, "Release");
|
|
|
|
g_clear_pointer (&buf->memory, g_free);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_asio_ring_buffer_start (GstAudioRingBuffer * buf)
|
|
{
|
|
GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf);
|
|
GstAsioObjectCallbacks callbacks;
|
|
|
|
GST_DEBUG_OBJECT (buf, "Start");
|
|
|
|
callbacks.buffer_switch = gst_asio_buffer_switch_cb;
|
|
callbacks.user_data = self;
|
|
|
|
self->is_first = TRUE;
|
|
self->expected_sample_position = 0;
|
|
|
|
if (!gst_asio_object_install_callback (self->asio_object, self->type,
|
|
&callbacks, &self->callback_id)) {
|
|
GST_ERROR_OBJECT (self, "Failed to install callback");
|
|
return FALSE;
|
|
}
|
|
|
|
self->callback_installed = TRUE;
|
|
|
|
if (!gst_asio_object_start (self->asio_object)) {
|
|
GST_ERROR_OBJECT (self, "Failed to start");
|
|
|
|
gst_asio_ring_buffer_stop (buf);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
self->running = TRUE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_asio_ring_buffer_stop (GstAudioRingBuffer * buf)
|
|
{
|
|
GstAsioRingBuffer *self = GST_ASIO_RING_BUFFER (buf);
|
|
|
|
GST_DEBUG_OBJECT (buf, "Stop");
|
|
|
|
self->running = FALSE;
|
|
|
|
if (!self->asio_object)
|
|
return TRUE;
|
|
|
|
if (self->callback_installed)
|
|
gst_asio_object_uninstall_callback (self->asio_object, self->callback_id);
|
|
|
|
self->callback_installed = FALSE;
|
|
self->callback_id = 0;
|
|
self->is_first = TRUE;
|
|
self->expected_sample_position = 0;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static guint
|
|
gst_asio_ring_buffer_delay (GstAudioRingBuffer * buf)
|
|
{
|
|
/* FIXME: impl. */
|
|
|
|
return 0;
|
|
}
|
|
|
|
GstAsioRingBuffer *
|
|
gst_asio_ring_buffer_new (GstAsioObject * object, GstAsioDeviceClassType type,
|
|
const gchar * name)
|
|
{
|
|
GstAsioRingBuffer *self;
|
|
|
|
g_return_val_if_fail (GST_IS_ASIO_OBJECT (object), nullptr);
|
|
|
|
self =
|
|
(GstAsioRingBuffer *) g_object_new (GST_TYPE_ASIO_RING_BUFFER,
|
|
"name", name, nullptr);
|
|
g_assert (self);
|
|
|
|
self->type = type;
|
|
self->asio_object = (GstAsioObject *) gst_object_ref (object);
|
|
|
|
return self;
|
|
}
|
|
|
|
gboolean
|
|
gst_asio_ring_buffer_configure (GstAsioRingBuffer * buf,
|
|
guint * channel_indices, guint num_channles, guint preferred_buffer_size)
|
|
{
|
|
g_return_val_if_fail (GST_IS_ASIO_RING_BUFFER (buf), FALSE);
|
|
g_return_val_if_fail (buf->asio_object != nullptr, FALSE);
|
|
g_return_val_if_fail (num_channles > 0, FALSE);
|
|
|
|
GST_DEBUG_OBJECT (buf, "Configure");
|
|
|
|
buf->buffer_size = preferred_buffer_size;
|
|
|
|
if (!gst_asio_object_create_buffers (buf->asio_object, buf->type,
|
|
channel_indices, num_channles, &buf->buffer_size)) {
|
|
GST_ERROR_OBJECT (buf, "Failed to configure");
|
|
|
|
g_clear_pointer (&buf->channel_indices, g_free);
|
|
buf->num_channels = 0;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (buf, "configured buffer size: %d", buf->buffer_size);
|
|
|
|
g_free (buf->channel_indices);
|
|
buf->channel_indices = g_new0 (guint, num_channles);
|
|
|
|
for (guint i = 0; i < num_channles; i++)
|
|
buf->channel_indices[i] = channel_indices[i];
|
|
|
|
buf->num_channels = num_channles;
|
|
|
|
g_clear_pointer (&buf->infos, g_free);
|
|
buf->infos = g_new0 (ASIOBufferInfo *, num_channles);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
GstCaps *
|
|
gst_asio_ring_buffer_get_caps (GstAsioRingBuffer * buf)
|
|
{
|
|
g_return_val_if_fail (GST_IS_ASIO_RING_BUFFER (buf), nullptr);
|
|
g_assert (buf->asio_object != nullptr);
|
|
|
|
return gst_asio_object_get_caps (buf->asio_object,
|
|
buf->type, buf->num_channels, buf->num_channels);
|
|
}
|