gstreamer/sys/asio/gstasioringbuffer.cpp
Seungha Yang dc787434bc Introduce Steinberg ASIO (Audio Streaming Input/Output) plugin
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>
2021-07-26 14:58:16 +00:00

474 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);
}