| /* GStreamer | |
| * Copyright (C) 2016 Centricular Ltd. | |
| * Author: Arun Raghavan <arun@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. | |
| */ | |
| /** | |
| * 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 <gst/audio/gstaudiobasesink.h> | |
| #include <tinyalsa/asoundlib.h> | |
| #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 <arun@centricular.com>"); | |
| 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; | |
| } |