/* GStreamer * Copyright (C) 2016 Centricular Ltd. * Author: Arun Raghavan * * 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. */ /** * SECTION:element-tinyalsasink * @title: tinyalsasink * @see_also: alsasink * * This element renders raw audio samples using the ALSA audio API via the * tinyalsa library. * * ## Example pipelines * |[ * gst-launch-1.0 -v uridecodebin uri=file:///path/to/audio.ogg ! audioconvert ! audioresample ! tinyalsasink * ]| Play an Ogg/Vorbis file and output audio via ALSA using the tinyalsa * library. * */ #include #include #include "tinyalsasink.h" /* Hardcoding these bitmask values rather than including a kernel header */ #define SNDRV_PCM_FORMAT_S8 0 #define SNDRV_PCM_FORMAT_S16_LE 2 #define SNDRV_PCM_FORMAT_S24_LE 6 #define SNDRV_PCM_FORMAT_S32_LE 10 #define SNDRV_PCM_FORMAT_ANY \ ((1 << SNDRV_PCM_FORMAT_S8) | \ (1 << SNDRV_PCM_FORMAT_S16_LE) | \ (1 << SNDRV_PCM_FORMAT_S24_LE) | \ (1 << SNDRV_PCM_FORMAT_S32_LE)) GST_DEBUG_CATEGORY_STATIC (tinyalsa_sink_debug); #define GST_CAT_DEFAULT tinyalsa_sink_debug #define parent_class gst_tinyalsa_sink_parent_class G_DEFINE_TYPE (GstTinyalsaSink, gst_tinyalsa_sink, GST_TYPE_AUDIO_SINK); enum { PROP_0, PROP_CARD, PROP_DEVICE, PROP_LAST }; #define DEFAULT_CARD 0 #define DEFAULT_DEVICE 0 static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS ("audio/x-raw, " "format = (string) { S16LE, S32LE, S24_32LE, S8 }, " "channels = (int) [ 1, MAX ], " "rate = (int) [ 1, MAX ], " "layout = (string) interleaved")); static void gst_tinyalsa_sink_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GstTinyalsaSink *sink = GST_TINYALSA_SINK (object); switch (prop_id) { case PROP_CARD: g_value_set_uint (value, sink->card); break; case PROP_DEVICE: g_value_set_uint (value, sink->device); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gst_tinyalsa_sink_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GstTinyalsaSink *sink = GST_TINYALSA_SINK (object); switch (prop_id) { case PROP_CARD: sink->card = g_value_get_uint (value); break; case PROP_DEVICE: sink->device = g_value_get_uint (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static GstCaps * gst_tinyalsa_sink_getcaps (GstBaseSink * bsink, GstCaps * filter) { GstTinyalsaSink *sink = GST_TINYALSA_SINK (bsink); GstCaps *caps = NULL; GValue formats = { 0, }; GValue format = { 0, }; struct pcm_params *params = NULL; struct pcm_mask *mask; int rate_min, rate_max, channels_min, channels_max; guint16 m; GST_DEBUG_OBJECT (sink, "Querying caps"); GST_OBJECT_LOCK (sink); if (sink->cached_caps) { GST_DEBUG_OBJECT (sink, "Returning cached caps"); caps = gst_caps_ref (sink->cached_caps); goto done; } if (sink->pcm) { /* We can't query the device while it's open, so return current caps */ caps = gst_pad_get_current_caps (GST_BASE_SINK_PAD (bsink)); goto done; } params = pcm_params_get (sink->card, sink->device, PCM_OUT); if (!params) { GST_ERROR_OBJECT (sink, "Could not get PCM params"); goto done; } mask = pcm_params_get_mask (params, PCM_PARAM_FORMAT); m = (mask->bits[1] << 8) | mask->bits[0]; if (!(m & SNDRV_PCM_FORMAT_ANY)) { GST_ERROR_OBJECT (sink, "Could not find any supported format"); goto done; } caps = gst_caps_new_empty_simple ("audio/x-raw"); g_value_init (&formats, GST_TYPE_LIST); g_value_init (&format, G_TYPE_STRING); if (m & (1 << SNDRV_PCM_FORMAT_S8)) { g_value_set_static_string (&format, "S8"); gst_value_list_prepend_value (&formats, &format); } if (m & (1 << SNDRV_PCM_FORMAT_S16_LE)) { g_value_set_static_string (&format, "S16LE"); gst_value_list_prepend_value (&formats, &format); } if (m & (1 << SNDRV_PCM_FORMAT_S24_LE)) { g_value_set_static_string (&format, "S24_32LE"); gst_value_list_prepend_value (&formats, &format); } if (m & (1 << SNDRV_PCM_FORMAT_S32_LE)) { g_value_set_static_string (&format, "S32LE"); gst_value_list_prepend_value (&formats, &format); } gst_caps_set_value (caps, "format", &formats); g_value_unset (&format); g_value_unset (&formats); /* This is a bit of a lie, since the device likely only supports some * standard rates in this range. We should probably filter the range to * those, standard audio rates but even that isn't guaranteed to be accurate. */ rate_min = pcm_params_get_min (params, PCM_PARAM_RATE); rate_max = pcm_params_get_max (params, PCM_PARAM_RATE); if (rate_min == rate_max) gst_caps_set_simple (caps, "rate", G_TYPE_INT, rate_min, NULL); else gst_caps_set_simple (caps, "rate", GST_TYPE_INT_RANGE, rate_min, rate_max, NULL); channels_min = pcm_params_get_min (params, PCM_PARAM_CHANNELS); channels_max = pcm_params_get_max (params, PCM_PARAM_CHANNELS); if (channels_min == channels_max) gst_caps_set_simple (caps, "channels", G_TYPE_INT, channels_min, NULL); else gst_caps_set_simple (caps, "channels", GST_TYPE_INT_RANGE, channels_min, channels_max, NULL); gst_caps_replace (&sink->cached_caps, caps); done: GST_OBJECT_UNLOCK (sink); GST_DEBUG_OBJECT (sink, "Got caps %" GST_PTR_FORMAT, caps); if (caps && filter) { GstCaps *intersection = gst_caps_intersect_full (filter, caps, GST_CAPS_INTERSECT_FIRST); gst_caps_unref (caps); caps = intersection; } if (params) pcm_params_free (params); return caps; } static gboolean gst_tinyalsa_sink_open (GstAudioSink * asink) { /* Nothing to do here, we can't call pcm_open() till we have stream * parameters available */ return TRUE; } static enum pcm_format pcm_format_from_gst (GstAudioFormat format) { switch (format) { case GST_AUDIO_FORMAT_S8: return PCM_FORMAT_S8; case GST_AUDIO_FORMAT_S16LE: return PCM_FORMAT_S16_LE; case GST_AUDIO_FORMAT_S24_32LE: return PCM_FORMAT_S24_LE; case GST_AUDIO_FORMAT_S32LE: return PCM_FORMAT_S32_LE; default: g_assert_not_reached (); } } static void pcm_config_from_spec (struct pcm_config *config, const GstAudioRingBufferSpec * spec) { gint64 frames; config->format = pcm_format_from_gst (GST_AUDIO_INFO_FORMAT (&spec->info)); config->channels = GST_AUDIO_INFO_CHANNELS (&spec->info); config->rate = GST_AUDIO_INFO_RATE (&spec->info); gst_audio_info_convert (&spec->info, GST_FORMAT_TIME, spec->latency_time * GST_USECOND, GST_FORMAT_DEFAULT /* frames */ , &frames); config->period_size = frames; config->period_count = spec->buffer_time / spec->latency_time; } static gboolean gst_tinyalsa_sink_prepare (GstAudioSink * asink, GstAudioRingBufferSpec * spec) { GstTinyalsaSink *sink = GST_TINYALSA_SINK (asink); struct pcm_config config = { 0, }; struct pcm_params *params = NULL; int period_size_min, period_size_max; int periods_min, periods_max; pcm_config_from_spec (&config, spec); GST_DEBUG_OBJECT (sink, "Requesting %u periods of %u frames", config.period_count, config.period_size); params = pcm_params_get (sink->card, sink->device, PCM_OUT); if (!params) GST_ERROR_OBJECT (sink, "Could not get PCM params"); period_size_min = pcm_params_get_min (params, PCM_PARAM_PERIOD_SIZE); period_size_max = pcm_params_get_max (params, PCM_PARAM_PERIOD_SIZE); periods_min = pcm_params_get_min (params, PCM_PARAM_PERIODS); periods_max = pcm_params_get_max (params, PCM_PARAM_PERIODS); pcm_params_free (params); /* Snap period size/count to the permitted range */ config.period_size = CLAMP (config.period_size, period_size_min, period_size_max); config.period_count = CLAMP (config.period_count, periods_min, periods_max); /* mutex with getcaps */ GST_OBJECT_LOCK (sink); sink->pcm = pcm_open (sink->card, sink->device, PCM_OUT | PCM_NORESTART, &config); GST_OBJECT_UNLOCK (sink); if (!sink->pcm || !pcm_is_ready (sink->pcm)) { GST_ERROR_OBJECT (sink, "Could not open device: %s", pcm_get_error (sink->pcm)); goto fail; } if (pcm_prepare (sink->pcm) < 0) { GST_ERROR_OBJECT (sink, "Could not prepare device: %s", pcm_get_error (sink->pcm)); goto fail; } spec->segsize = pcm_frames_to_bytes (sink->pcm, config.period_size); spec->segtotal = config.period_count; GST_DEBUG_OBJECT (sink, "Configured for %u periods of %u frames", config.period_count, config.period_size); return TRUE; fail: if (sink->pcm) pcm_close (sink->pcm); return FALSE; } static gboolean gst_tinyalsa_sink_unprepare (GstAudioSink * asink) { GstTinyalsaSink *sink = GST_TINYALSA_SINK (asink); if (pcm_stop (sink->pcm) < 0) { GST_ERROR_OBJECT (sink, "Could not stop device: %s", pcm_get_error (sink->pcm)); } /* mutex with getcaps */ GST_OBJECT_LOCK (sink); if (pcm_close (sink->pcm)) { GST_ERROR_OBJECT (sink, "Could not close device: %s", pcm_get_error (sink->pcm)); return FALSE; } sink->pcm = NULL; gst_caps_replace (&sink->cached_caps, NULL); GST_OBJECT_UNLOCK (sink); GST_DEBUG_OBJECT (sink, "Device unprepared"); return TRUE; } static gboolean gst_tinyalsa_sink_close (GstAudioSink * asink) { /* Nothing to do here, see gst_tinyalsa_sink_open() */ return TRUE; } static gint gst_tinyalsa_sink_write (GstAudioSink * asink, gpointer data, guint length) { GstTinyalsaSink *sink = GST_TINYALSA_SINK (asink); int ret; again: GST_DEBUG_OBJECT (sink, "Starting write"); ret = pcm_write (sink->pcm, data, length); if (ret == -EPIPE) { GST_WARNING_OBJECT (sink, "Got an underrun"); if (pcm_prepare (sink->pcm) < 0) { GST_ERROR_OBJECT (sink, "Could not prepare device: %s", pcm_get_error (sink->pcm)); return -1; } goto again; } else if (ret < 0) { GST_ERROR_OBJECT (sink, "Could not write data to device: %s", pcm_get_error (sink->pcm)); return -1; } GST_DEBUG_OBJECT (sink, "Wrote %u bytes", length); return length; } static void gst_tinyalsa_sink_reset (GstAudioSink * asink) { GstTinyalsaSink *sink = GST_TINYALSA_SINK (asink); if (pcm_stop (sink->pcm) < 0) { GST_ERROR_OBJECT (sink, "Could not stop device: %s", pcm_get_error (sink->pcm)); } if (pcm_prepare (sink->pcm) < 0) { GST_ERROR_OBJECT (sink, "Could not prepare device: %s", pcm_get_error (sink->pcm)); } } static guint gst_tinyalsa_sink_delay (GstAudioSink * asink) { GstTinyalsaSink *sink = GST_TINYALSA_SINK (asink); int delay; delay = pcm_get_delay (sink->pcm); if (delay < 0) { /* This might happen before the stream has started */ GST_DEBUG_OBJECT (sink, "Got negative delay"); delay = 0; } else GST_DEBUG_OBJECT (sink, "Got delay of %u", delay); return delay; } static void gst_tinyalsa_sink_class_init (GstTinyalsaSinkClass * klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); GstElementClass *element_class = GST_ELEMENT_CLASS (klass); GstBaseSinkClass *basesink_class = GST_BASE_SINK_CLASS (klass); GstAudioSinkClass *audiosink_class = GST_AUDIO_SINK_CLASS (klass); gobject_class->get_property = GST_DEBUG_FUNCPTR (gst_tinyalsa_sink_get_property); gobject_class->set_property = GST_DEBUG_FUNCPTR (gst_tinyalsa_sink_set_property); basesink_class->get_caps = GST_DEBUG_FUNCPTR (gst_tinyalsa_sink_getcaps); audiosink_class->open = GST_DEBUG_FUNCPTR (gst_tinyalsa_sink_open); audiosink_class->prepare = GST_DEBUG_FUNCPTR (gst_tinyalsa_sink_prepare); audiosink_class->unprepare = GST_DEBUG_FUNCPTR (gst_tinyalsa_sink_unprepare); audiosink_class->close = GST_DEBUG_FUNCPTR (gst_tinyalsa_sink_close); audiosink_class->write = GST_DEBUG_FUNCPTR (gst_tinyalsa_sink_write); audiosink_class->reset = GST_DEBUG_FUNCPTR (gst_tinyalsa_sink_reset); audiosink_class->delay = GST_DEBUG_FUNCPTR (gst_tinyalsa_sink_delay); gst_element_class_set_static_metadata (element_class, "tinyalsa Audio Sink", "Sink/Audio", "Plays audio to an ALSA device", "Arun Raghavan "); gst_element_class_add_static_pad_template (element_class, &sink_template); g_object_class_install_property (gobject_class, PROP_CARD, g_param_spec_uint ("card", "Card", "The ALSA card to use", 0, G_MAXUINT, DEFAULT_CARD, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_DEVICE, g_param_spec_uint ("device", "Device", "The ALSA device to use", 0, G_MAXUINT, DEFAULT_CARD, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); GST_DEBUG_CATEGORY_INIT (tinyalsa_sink_debug, "tinyalsasink", 0, "tinyalsa Sink"); } static void gst_tinyalsa_sink_init (GstTinyalsaSink * sink) { sink->card = DEFAULT_CARD; sink->device = DEFAULT_DEVICE; sink->cached_caps = NULL; }