| /* GStreamer |
| * Copyright (C) 2005 Wim Taymans <wim@fluendo.com> |
| * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net> |
| * |
| * gstalsasink.c: |
| * |
| * 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-alsasink |
| * @title: alsasink |
| * @see_also: alsasrc |
| * |
| * This element renders audio samples using the ALSA audio API. |
| * |
| * ## Example pipelines |
| * |[ |
| * gst-launch-1.0 -v uridecodebin uri=file:///path/to/audio.ogg ! audioconvert ! audioresample ! autoaudiosink |
| * ]| |
| * |
| * Play an Ogg/Vorbis file and output audio via ALSA. |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include "config.h" |
| #endif |
| #include <sys/ioctl.h> |
| #include <fcntl.h> |
| #include <errno.h> |
| #include <unistd.h> |
| #include <string.h> |
| #include <getopt.h> |
| #include <alsa/asoundlib.h> |
| |
| #include "gstalsa.h" |
| #include "gstalsasink.h" |
| #include "gstalsadeviceprobe.h" |
| |
| #include <gst/audio/gstaudioiec61937.h> |
| #include <gst/gst-i18n-plugin.h> |
| |
| #ifndef ESTRPIPE |
| #define ESTRPIPE EPIPE |
| #endif |
| |
| #define DEFAULT_DEVICE "default" |
| #define DEFAULT_DEVICE_NAME "" |
| #define DEFAULT_CARD_NAME "" |
| #define SPDIF_PERIOD_SIZE 1536 |
| #define SPDIF_BUFFER_SIZE 15360 |
| |
| enum |
| { |
| PROP_0, |
| PROP_DEVICE, |
| PROP_DEVICE_NAME, |
| PROP_CARD_NAME, |
| PROP_PASSTHROUGH, |
| PROP_LAST |
| }; |
| |
| static void gst_alsasink_init_interfaces (GType type); |
| #define gst_alsasink_parent_class parent_class |
| G_DEFINE_TYPE_WITH_CODE (GstAlsaSink, gst_alsasink, |
| GST_TYPE_AUDIO_SINK, gst_alsasink_init_interfaces (g_define_type_id)); |
| |
| static void gst_alsasink_finalise (GObject * object); |
| static void gst_alsasink_set_property (GObject * object, |
| guint prop_id, const GValue * value, GParamSpec * pspec); |
| static void gst_alsasink_get_property (GObject * object, |
| guint prop_id, GValue * value, GParamSpec * pspec); |
| |
| static GstCaps *gst_alsasink_getcaps (GstBaseSink * bsink, GstCaps * filter); |
| static gboolean gst_alsasink_query (GstBaseSink * bsink, GstQuery * query); |
| |
| static gboolean gst_alsasink_open (GstAudioSink * asink); |
| static gboolean gst_alsasink_prepare (GstAudioSink * asink, |
| GstAudioRingBufferSpec * spec); |
| static gboolean gst_alsasink_unprepare (GstAudioSink * asink); |
| static gboolean gst_alsasink_close (GstAudioSink * asink); |
| static gint gst_alsasink_write (GstAudioSink * asink, gpointer data, |
| guint length); |
| static guint gst_alsasink_delay (GstAudioSink * asink); |
| static void gst_alsasink_reset (GstAudioSink * asink); |
| static gboolean gst_alsasink_acceptcaps (GstAlsaSink * alsa, GstCaps * caps); |
| static GstBuffer *gst_alsasink_payload (GstAudioBaseSink * sink, |
| GstBuffer * buf); |
| |
| static gint output_ref; /* 0 */ |
| static snd_output_t *output; /* NULL */ |
| static GMutex output_mutex; |
| |
| static GstStaticPadTemplate alsasink_sink_factory = |
| GST_STATIC_PAD_TEMPLATE ("sink", |
| GST_PAD_SINK, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS ("audio/x-raw, " |
| "format = (string) " GST_AUDIO_FORMATS_ALL ", " |
| "layout = (string) interleaved, " |
| "rate = (int) [ 1, MAX ], " "channels = (int) [ 1, MAX ]; " |
| PASSTHROUGH_CAPS) |
| ); |
| |
| static void |
| gst_alsasink_finalise (GObject * object) |
| { |
| GstAlsaSink *sink = GST_ALSA_SINK (object); |
| |
| g_free (sink->device); |
| g_mutex_clear (&sink->alsa_lock); |
| g_mutex_clear (&sink->delay_lock); |
| |
| g_mutex_lock (&output_mutex); |
| --output_ref; |
| if (output_ref == 0) { |
| snd_output_close (output); |
| output = NULL; |
| } |
| g_mutex_unlock (&output_mutex); |
| |
| G_OBJECT_CLASS (parent_class)->finalize (object); |
| } |
| |
| static void |
| gst_alsasink_init_interfaces (GType type) |
| { |
| #if 0 |
| gst_alsa_type_add_device_property_probe_interface (type); |
| #endif |
| } |
| |
| static void |
| gst_alsasink_class_init (GstAlsaSinkClass * klass) |
| { |
| GObjectClass *gobject_class; |
| GstElementClass *gstelement_class; |
| GstBaseSinkClass *gstbasesink_class; |
| GstAudioBaseSinkClass *gstbaseaudiosink_class; |
| GstAudioSinkClass *gstaudiosink_class; |
| |
| gobject_class = (GObjectClass *) klass; |
| gstelement_class = (GstElementClass *) klass; |
| gstbasesink_class = (GstBaseSinkClass *) klass; |
| gstbaseaudiosink_class = (GstAudioBaseSinkClass *) klass; |
| gstaudiosink_class = (GstAudioSinkClass *) klass; |
| |
| parent_class = g_type_class_peek_parent (klass); |
| |
| gobject_class->finalize = gst_alsasink_finalise; |
| gobject_class->get_property = gst_alsasink_get_property; |
| gobject_class->set_property = gst_alsasink_set_property; |
| |
| gst_element_class_set_static_metadata (gstelement_class, |
| "Audio sink (ALSA)", "Sink/Audio", |
| "Output to a sound card via ALSA", "Wim Taymans <wim@fluendo.com>"); |
| |
| gst_element_class_add_static_pad_template (gstelement_class, |
| &alsasink_sink_factory); |
| |
| gstbasesink_class->get_caps = GST_DEBUG_FUNCPTR (gst_alsasink_getcaps); |
| gstbasesink_class->query = GST_DEBUG_FUNCPTR (gst_alsasink_query); |
| |
| gstbaseaudiosink_class->payload = GST_DEBUG_FUNCPTR (gst_alsasink_payload); |
| |
| gstaudiosink_class->open = GST_DEBUG_FUNCPTR (gst_alsasink_open); |
| gstaudiosink_class->prepare = GST_DEBUG_FUNCPTR (gst_alsasink_prepare); |
| gstaudiosink_class->unprepare = GST_DEBUG_FUNCPTR (gst_alsasink_unprepare); |
| gstaudiosink_class->close = GST_DEBUG_FUNCPTR (gst_alsasink_close); |
| gstaudiosink_class->write = GST_DEBUG_FUNCPTR (gst_alsasink_write); |
| gstaudiosink_class->delay = GST_DEBUG_FUNCPTR (gst_alsasink_delay); |
| gstaudiosink_class->reset = GST_DEBUG_FUNCPTR (gst_alsasink_reset); |
| |
| g_object_class_install_property (gobject_class, PROP_DEVICE, |
| g_param_spec_string ("device", "Device", |
| "ALSA device, as defined in an asound configuration file", |
| DEFAULT_DEVICE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); |
| |
| g_object_class_install_property (gobject_class, PROP_DEVICE_NAME, |
| g_param_spec_string ("device-name", "Device name", |
| "Human-readable name of the sound device", DEFAULT_DEVICE_NAME, |
| G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); |
| |
| g_object_class_install_property (gobject_class, PROP_CARD_NAME, |
| g_param_spec_string ("card-name", "Card name", |
| "Human-readable name of the sound card", DEFAULT_CARD_NAME, |
| G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); |
| |
| g_object_class_install_property (gobject_class, PROP_PASSTHROUGH, |
| g_param_spec_boolean ("pass-through", "Pass through", |
| "enable pass through mode", |
| FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); |
| } |
| |
| static void |
| gst_alsasink_set_property (GObject * object, guint prop_id, |
| const GValue * value, GParamSpec * pspec) |
| { |
| GstAlsaSink *sink; |
| |
| sink = GST_ALSA_SINK (object); |
| |
| switch (prop_id) { |
| case PROP_DEVICE: |
| g_free (sink->device); |
| sink->device = g_value_dup_string (value); |
| /* setting NULL restores the default device */ |
| if (sink->device == NULL) { |
| sink->device = g_strdup (DEFAULT_DEVICE); |
| } |
| break; |
| case PROP_PASSTHROUGH: |
| sink->passthrough = g_value_get_boolean(value); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| } |
| |
| static void |
| gst_alsasink_get_property (GObject * object, guint prop_id, |
| GValue * value, GParamSpec * pspec) |
| { |
| GstAlsaSink *sink; |
| |
| sink = GST_ALSA_SINK (object); |
| |
| switch (prop_id) { |
| case PROP_DEVICE: |
| g_value_set_string (value, sink->device); |
| break; |
| case PROP_DEVICE_NAME: |
| g_value_take_string (value, |
| gst_alsa_find_device_name (GST_OBJECT_CAST (sink), |
| sink->device, sink->handle, SND_PCM_STREAM_PLAYBACK)); |
| break; |
| case PROP_CARD_NAME: |
| g_value_take_string (value, |
| gst_alsa_find_card_name (GST_OBJECT_CAST (sink), |
| sink->device, SND_PCM_STREAM_PLAYBACK)); |
| break; |
| case PROP_PASSTHROUGH: |
| g_value_set_boolean (value, sink->passthrough); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| } |
| |
| static void |
| gst_alsasink_init (GstAlsaSink * alsasink) |
| { |
| GST_DEBUG_OBJECT (alsasink, "initializing alsasink"); |
| |
| alsasink->device = g_strdup (DEFAULT_DEVICE); |
| alsasink->handle = NULL; |
| alsasink->cached_caps = NULL; |
| alsasink->passthrough = FALSE; |
| g_mutex_init (&alsasink->alsa_lock); |
| g_mutex_init (&alsasink->delay_lock); |
| |
| g_mutex_lock (&output_mutex); |
| if (output_ref == 0) { |
| snd_output_stdio_attach (&output, stdout, 0); |
| ++output_ref; |
| } |
| g_mutex_unlock (&output_mutex); |
| } |
| |
| #define CHECK(call, error) \ |
| G_STMT_START { \ |
| if ((err = call) < 0) { \ |
| GST_WARNING_OBJECT (alsa, "Error %d (%s) calling " #call, err, snd_strerror (err)); \ |
| goto error; \ |
| } \ |
| } G_STMT_END; |
| |
| static GstCaps * |
| gst_alsasink_getcaps (GstBaseSink * bsink, GstCaps * filter) |
| { |
| GstElementClass *element_class; |
| GstPadTemplate *pad_template; |
| GstAlsaSink *sink = GST_ALSA_SINK (bsink); |
| GstCaps *caps, *templ_caps; |
| snd_pcm_stream_t stream_type; |
| snd_pcm_t *pcm; |
| int res; |
| |
| GST_OBJECT_LOCK (sink); |
| if (sink->handle == NULL) { |
| GST_OBJECT_UNLOCK (sink); |
| GST_DEBUG_OBJECT (sink, "device not open, using template caps"); |
| return NULL; /* base class will get template caps for us */ |
| } |
| |
| if (sink->cached_caps) { |
| if (filter) { |
| caps = gst_caps_intersect_full (filter, sink->cached_caps, |
| GST_CAPS_INTERSECT_FIRST); |
| GST_OBJECT_UNLOCK (sink); |
| GST_LOG_OBJECT (sink, "Returning cached caps %" GST_PTR_FORMAT " with " |
| "filter %" GST_PTR_FORMAT " applied: %" GST_PTR_FORMAT, |
| sink->cached_caps, filter, caps); |
| return caps; |
| } else { |
| caps = gst_caps_ref (sink->cached_caps); |
| GST_OBJECT_UNLOCK (sink); |
| GST_LOG_OBJECT (sink, "Returning cached caps %" GST_PTR_FORMAT, caps); |
| return caps; |
| } |
| } |
| |
| element_class = GST_ELEMENT_GET_CLASS (sink); |
| pad_template = gst_element_class_get_pad_template (element_class, "sink"); |
| if (pad_template == NULL) { |
| GST_OBJECT_UNLOCK (sink); |
| g_assert_not_reached (); |
| return NULL; |
| } |
| |
| templ_caps = gst_pad_template_get_caps (pad_template); |
| caps = gst_alsa_probe_supported_formats (GST_OBJECT (sink), sink->device, |
| sink->handle, templ_caps); |
| gst_caps_unref (templ_caps); |
| |
| /* Try opening IEC958 device to see if we can support that format (playback |
| * only for now but we could add SPDIF capture later) |
| */ |
| |
| if (gst_alsa_iec958_formats_supported(GST_OBJECT (sink), sink->device, |
| &sink->handle) == TRUE) { |
| GST_LOG_OBJECT (sink, "Add pass-through capabilities"); |
| if (TRUE == sink->passthrough) |
| gst_caps_append (caps, gst_caps_from_string (PASSTHROUGH_CAPS)); |
| } |
| |
| if (caps) { |
| sink->cached_caps = gst_caps_ref (caps); |
| } |
| GST_OBJECT_UNLOCK (sink); |
| |
| GST_INFO_OBJECT (sink, "returning caps %" GST_PTR_FORMAT, caps); |
| |
| if (filter) { |
| GstCaps *intersection; |
| |
| intersection = |
| gst_caps_intersect_full (filter, caps, GST_CAPS_INTERSECT_FIRST); |
| gst_caps_unref (caps); |
| return intersection; |
| } else { |
| return caps; |
| } |
| } |
| |
| static gboolean |
| gst_alsasink_acceptcaps (GstAlsaSink * alsa, GstCaps * caps) |
| { |
| GstPad *pad = GST_BASE_SINK (alsa)->sinkpad; |
| GstCaps *pad_caps; |
| GstStructure *st; |
| gboolean ret = FALSE; |
| GstAudioRingBufferSpec spec = { 0 }; |
| |
| pad_caps = gst_pad_query_caps (pad, caps); |
| if (!pad_caps || gst_caps_is_empty (pad_caps)) { |
| if (pad_caps) |
| gst_caps_unref (pad_caps); |
| ret = FALSE; |
| goto done; |
| } |
| gst_caps_unref (pad_caps); |
| |
| /* If we've not got fixed caps, creating a stream might fail, so let's just |
| * return from here with default acceptcaps behaviour */ |
| if (!gst_caps_is_fixed (caps)) |
| goto done; |
| |
| /* parse helper expects this set, so avoid nasty warning |
| * will be set properly later on anyway */ |
| spec.latency_time = GST_SECOND; |
| if (!gst_audio_ring_buffer_parse_caps (&spec, caps)) |
| goto done; |
| |
| /* Make sure input is framed (one frame per buffer) and can be payloaded */ |
| switch (spec.type) { |
| case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_AC3: |
| case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_EAC3: |
| case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_DTS: |
| case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_MPEG: |
| { |
| gboolean framed = FALSE, parsed = FALSE; |
| st = gst_caps_get_structure (caps, 0); |
| |
| gst_structure_get_boolean (st, "framed", &framed); |
| gst_structure_get_boolean (st, "parsed", &parsed); |
| if ((!framed && !parsed) || gst_audio_iec61937_frame_size (&spec) <= 0) |
| goto done; |
| } |
| default:{ |
| } |
| } |
| ret = TRUE; |
| |
| done: |
| gst_caps_replace (&spec.caps, NULL); |
| return ret; |
| } |
| |
| static gboolean |
| gst_alsasink_query (GstBaseSink * sink, GstQuery * query) |
| { |
| GstAlsaSink *alsa = GST_ALSA_SINK (sink); |
| gboolean ret; |
| |
| switch (GST_QUERY_TYPE (query)) { |
| case GST_QUERY_ACCEPT_CAPS: |
| { |
| GstCaps *caps; |
| |
| gst_query_parse_accept_caps (query, &caps); |
| ret = gst_alsasink_acceptcaps (alsa, caps); |
| gst_query_set_accept_caps_result (query, ret); |
| ret = TRUE; |
| break; |
| } |
| default: |
| ret = GST_BASE_SINK_CLASS (parent_class)->query (sink, query); |
| break; |
| } |
| return ret; |
| } |
| |
| static int |
| set_hwparams (GstAlsaSink * alsa) |
| { |
| guint rrate; |
| gint err; |
| snd_pcm_hw_params_t *params; |
| guint period_time, buffer_time; |
| |
| snd_pcm_hw_params_malloc (¶ms); |
| |
| GST_DEBUG_OBJECT (alsa, "Negotiating to %d channels @ %d Hz (format = %s) " |
| "SPDIF (%d)", alsa->channels, alsa->rate, |
| snd_pcm_format_name (alsa->format), alsa->iec958); |
| |
| /* start with requested values, if we cannot configure alsa for those values, |
| * we set these values to -1, which will leave the default alsa values */ |
| buffer_time = alsa->buffer_time; |
| period_time = alsa->period_time; |
| |
| retry: |
| /* choose all parameters */ |
| CHECK (snd_pcm_hw_params_any (alsa->handle, params), no_config); |
| /* set the interleaved read/write format */ |
| CHECK (snd_pcm_hw_params_set_access (alsa->handle, params, alsa->access), |
| wrong_access); |
| /* set the sample format */ |
| if (alsa->iec958) { |
| /* Try to use big endian first else fallback to le and swap bytes */ |
| if (snd_pcm_hw_params_set_format (alsa->handle, params, alsa->format) < 0) { |
| alsa->format = SND_PCM_FORMAT_S16_LE; |
| /* swap needed if system is big endian */ |
| alsa->need_swap = (G_BYTE_ORDER == G_BIG_ENDIAN); |
| GST_DEBUG_OBJECT (alsa, "falling back to little endian with swapping"); |
| } else { |
| /* swap needed if system is little endian */ |
| alsa->need_swap = (G_BYTE_ORDER != G_BIG_ENDIAN); |
| } |
| } |
| CHECK (snd_pcm_hw_params_set_format (alsa->handle, params, alsa->format), |
| no_sample_format); |
| /* set the count of channels */ |
| CHECK (snd_pcm_hw_params_set_channels (alsa->handle, params, alsa->channels), |
| no_channels); |
| /* set the stream rate */ |
| rrate = alsa->rate; |
| if ((TRUE== alsa->passthrough) && alsa->iec958 && (alsa->type == GST_AUDIO_RING_BUFFER_FORMAT_TYPE_EAC3)) |
| rrate = 192000; |
| CHECK (snd_pcm_hw_params_set_rate_near (alsa->handle, params, &rrate, NULL), |
| no_rate); |
| |
| #ifndef GST_DISABLE_GST_DEBUG |
| /* get and dump some limits */ |
| { |
| guint min, max; |
| |
| snd_pcm_hw_params_get_buffer_time_min (params, &min, NULL); |
| snd_pcm_hw_params_get_buffer_time_max (params, &max, NULL); |
| |
| GST_DEBUG_OBJECT (alsa, "buffer time %u, min %u, max %u", |
| alsa->buffer_time, min, max); |
| |
| snd_pcm_hw_params_get_period_time_min (params, &min, NULL); |
| snd_pcm_hw_params_get_period_time_max (params, &max, NULL); |
| |
| GST_DEBUG_OBJECT (alsa, "period time %u, min %u, max %u", |
| alsa->period_time, min, max); |
| |
| snd_pcm_hw_params_get_periods_min (params, &min, NULL); |
| snd_pcm_hw_params_get_periods_max (params, &max, NULL); |
| |
| GST_DEBUG_OBJECT (alsa, "periods min %u, max %u", min, max); |
| } |
| #endif |
| |
| /* now try to configure the buffer time and period time, if one |
| * of those fail, we fall back to the defaults and emit a warning. */ |
| if (buffer_time != -1 && !alsa->iec958) { |
| /* set the buffer time */ |
| if ((err = snd_pcm_hw_params_set_buffer_time_near (alsa->handle, params, |
| &buffer_time, NULL)) < 0) { |
| GST_ELEMENT_WARNING (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Unable to set buffer time %i for playback: %s", |
| buffer_time, snd_strerror (err))); |
| /* disable buffer_time the next round */ |
| buffer_time = -1; |
| goto retry; |
| } |
| GST_DEBUG_OBJECT (alsa, "buffer time %u", buffer_time); |
| alsa->buffer_time = buffer_time; |
| } |
| if (period_time != -1 && !alsa->iec958) { |
| /* set the period time */ |
| if ((err = snd_pcm_hw_params_set_period_time_near (alsa->handle, params, |
| &period_time, NULL)) < 0) { |
| GST_ELEMENT_WARNING (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Unable to set period time %i for playback: %s", |
| period_time, snd_strerror (err))); |
| /* disable period_time the next round */ |
| period_time = -1; |
| goto retry; |
| } |
| GST_DEBUG_OBJECT (alsa, "period time %u", period_time); |
| alsa->period_time = period_time; |
| } |
| |
| /* Set buffer size and period size manually for SPDIF */ |
| if (G_UNLIKELY (alsa->iec958)) { |
| snd_pcm_uframes_t buffer_size = SPDIF_BUFFER_SIZE; |
| snd_pcm_uframes_t period_size = SPDIF_PERIOD_SIZE; |
| |
| CHECK (snd_pcm_hw_params_set_buffer_size_near (alsa->handle, params, |
| &buffer_size), buffer_size); |
| CHECK (snd_pcm_hw_params_set_period_size_near (alsa->handle, params, |
| &period_size, NULL), period_size); |
| } |
| |
| /* write the parameters to device */ |
| CHECK (snd_pcm_hw_params (alsa->handle, params), set_hw_params); |
| |
| /* now get the configured values */ |
| CHECK (snd_pcm_hw_params_get_buffer_size (params, &alsa->buffer_size), |
| buffer_size); |
| CHECK (snd_pcm_hw_params_get_period_size (params, &alsa->period_size, NULL), |
| period_size); |
| |
| GST_DEBUG_OBJECT (alsa, "buffer size %lu, period size %lu", alsa->buffer_size, |
| alsa->period_size); |
| |
| snd_pcm_hw_params_free (params); |
| return 0; |
| |
| /* ERRORS */ |
| no_config: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Broken configuration for playback: no configurations available: %s", |
| snd_strerror (err))); |
| snd_pcm_hw_params_free (params); |
| return err; |
| } |
| wrong_access: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Access type not available for playback: %s", snd_strerror (err))); |
| snd_pcm_hw_params_free (params); |
| return err; |
| } |
| no_sample_format: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Sample format not available for playback: %s", snd_strerror (err))); |
| snd_pcm_hw_params_free (params); |
| return err; |
| } |
| no_channels: |
| { |
| gchar *msg = NULL; |
| |
| if ((alsa->channels) == 1) |
| msg = g_strdup (_("Could not open device for playback in mono mode.")); |
| if ((alsa->channels) == 2) |
| msg = g_strdup (_("Could not open device for playback in stereo mode.")); |
| if ((alsa->channels) > 2) |
| msg = |
| g_strdup_printf (_ |
| ("Could not open device for playback in %d-channel mode."), |
| alsa->channels); |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, ("%s", msg), |
| ("%s", snd_strerror (err))); |
| g_free (msg); |
| snd_pcm_hw_params_free (params); |
| return err; |
| } |
| no_rate: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Rate %iHz not available for playback: %s", |
| alsa->rate, snd_strerror (err))); |
| return err; |
| } |
| buffer_size: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Unable to get buffer size for playback: %s", snd_strerror (err))); |
| snd_pcm_hw_params_free (params); |
| return err; |
| } |
| period_size: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Unable to get period size for playback: %s", snd_strerror (err))); |
| snd_pcm_hw_params_free (params); |
| return err; |
| } |
| set_hw_params: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Unable to set hw params for playback: %s", snd_strerror (err))); |
| snd_pcm_hw_params_free (params); |
| return err; |
| } |
| } |
| |
| static int |
| set_swparams (GstAlsaSink * alsa) |
| { |
| int err; |
| snd_pcm_sw_params_t *params; |
| |
| snd_pcm_sw_params_malloc (¶ms); |
| |
| /* get the current swparams */ |
| CHECK (snd_pcm_sw_params_current (alsa->handle, params), no_config); |
| /* start the transfer when the buffer is almost full: */ |
| /* (buffer_size / avail_min) * avail_min */ |
| CHECK (snd_pcm_sw_params_set_start_threshold (alsa->handle, params, |
| (alsa->buffer_size / alsa->period_size) * alsa->period_size), |
| start_threshold); |
| |
| /* allow the transfer when at least period_size samples can be processed */ |
| CHECK (snd_pcm_sw_params_set_avail_min (alsa->handle, params, |
| alsa->period_size), set_avail); |
| |
| #if GST_CHECK_ALSA_VERSION(1,0,16) |
| /* snd_pcm_sw_params_set_xfer_align() is deprecated, alignment is always 1 */ |
| #else |
| /* align all transfers to 1 sample */ |
| CHECK (snd_pcm_sw_params_set_xfer_align (alsa->handle, params, 1), set_align); |
| #endif |
| |
| /* write the parameters to the playback device */ |
| CHECK (snd_pcm_sw_params (alsa->handle, params), set_sw_params); |
| |
| snd_pcm_sw_params_free (params); |
| return 0; |
| |
| /* ERRORS */ |
| no_config: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Unable to determine current swparams for playback: %s", |
| snd_strerror (err))); |
| snd_pcm_sw_params_free (params); |
| return err; |
| } |
| start_threshold: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Unable to set start threshold mode for playback: %s", |
| snd_strerror (err))); |
| snd_pcm_sw_params_free (params); |
| return err; |
| } |
| set_avail: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Unable to set avail min for playback: %s", snd_strerror (err))); |
| snd_pcm_sw_params_free (params); |
| return err; |
| } |
| #if !GST_CHECK_ALSA_VERSION(1,0,16) |
| set_align: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Unable to set transfer align for playback: %s", snd_strerror (err))); |
| snd_pcm_sw_params_free (params); |
| return err; |
| } |
| #endif |
| set_sw_params: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Unable to set sw params for playback: %s", snd_strerror (err))); |
| snd_pcm_sw_params_free (params); |
| return err; |
| } |
| } |
| |
| static gboolean |
| alsasink_parse_spec (GstAlsaSink * alsa, GstAudioRingBufferSpec * spec) |
| { |
| /* Initialize our boolean */ |
| alsa->iec958 = FALSE; |
| |
| switch (spec->type) { |
| case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_RAW: |
| switch (GST_AUDIO_INFO_FORMAT (&spec->info)) { |
| case GST_AUDIO_FORMAT_U8: |
| alsa->format = SND_PCM_FORMAT_U8; |
| break; |
| case GST_AUDIO_FORMAT_S8: |
| alsa->format = SND_PCM_FORMAT_S8; |
| break; |
| case GST_AUDIO_FORMAT_S16LE: |
| alsa->format = SND_PCM_FORMAT_S16_LE; |
| break; |
| case GST_AUDIO_FORMAT_S16BE: |
| alsa->format = SND_PCM_FORMAT_S16_BE; |
| break; |
| case GST_AUDIO_FORMAT_U16LE: |
| alsa->format = SND_PCM_FORMAT_U16_LE; |
| break; |
| case GST_AUDIO_FORMAT_U16BE: |
| alsa->format = SND_PCM_FORMAT_U16_BE; |
| break; |
| case GST_AUDIO_FORMAT_S24_32LE: |
| alsa->format = SND_PCM_FORMAT_S24_LE; |
| break; |
| case GST_AUDIO_FORMAT_S24_32BE: |
| alsa->format = SND_PCM_FORMAT_S24_BE; |
| break; |
| case GST_AUDIO_FORMAT_U24_32LE: |
| alsa->format = SND_PCM_FORMAT_U24_LE; |
| break; |
| case GST_AUDIO_FORMAT_U24_32BE: |
| alsa->format = SND_PCM_FORMAT_U24_BE; |
| break; |
| case GST_AUDIO_FORMAT_S32LE: |
| alsa->format = SND_PCM_FORMAT_S32_LE; |
| break; |
| case GST_AUDIO_FORMAT_S32BE: |
| alsa->format = SND_PCM_FORMAT_S32_BE; |
| break; |
| case GST_AUDIO_FORMAT_U32LE: |
| alsa->format = SND_PCM_FORMAT_U32_LE; |
| break; |
| case GST_AUDIO_FORMAT_U32BE: |
| alsa->format = SND_PCM_FORMAT_U32_BE; |
| break; |
| case GST_AUDIO_FORMAT_S24LE: |
| alsa->format = SND_PCM_FORMAT_S24_3LE; |
| break; |
| case GST_AUDIO_FORMAT_S24BE: |
| alsa->format = SND_PCM_FORMAT_S24_3BE; |
| break; |
| case GST_AUDIO_FORMAT_U24LE: |
| alsa->format = SND_PCM_FORMAT_U24_3LE; |
| break; |
| case GST_AUDIO_FORMAT_U24BE: |
| alsa->format = SND_PCM_FORMAT_U24_3BE; |
| break; |
| case GST_AUDIO_FORMAT_S20LE: |
| alsa->format = SND_PCM_FORMAT_S20_3LE; |
| break; |
| case GST_AUDIO_FORMAT_S20BE: |
| alsa->format = SND_PCM_FORMAT_S20_3BE; |
| break; |
| case GST_AUDIO_FORMAT_U20LE: |
| alsa->format = SND_PCM_FORMAT_U20_3LE; |
| break; |
| case GST_AUDIO_FORMAT_U20BE: |
| alsa->format = SND_PCM_FORMAT_U20_3BE; |
| break; |
| case GST_AUDIO_FORMAT_S18LE: |
| alsa->format = SND_PCM_FORMAT_S18_3LE; |
| break; |
| case GST_AUDIO_FORMAT_S18BE: |
| alsa->format = SND_PCM_FORMAT_S18_3BE; |
| break; |
| case GST_AUDIO_FORMAT_U18LE: |
| alsa->format = SND_PCM_FORMAT_U18_3LE; |
| break; |
| case GST_AUDIO_FORMAT_U18BE: |
| alsa->format = SND_PCM_FORMAT_U18_3BE; |
| break; |
| case GST_AUDIO_FORMAT_F32LE: |
| alsa->format = SND_PCM_FORMAT_FLOAT_LE; |
| break; |
| case GST_AUDIO_FORMAT_F32BE: |
| alsa->format = SND_PCM_FORMAT_FLOAT_BE; |
| break; |
| case GST_AUDIO_FORMAT_F64LE: |
| alsa->format = SND_PCM_FORMAT_FLOAT64_LE; |
| break; |
| case GST_AUDIO_FORMAT_F64BE: |
| alsa->format = SND_PCM_FORMAT_FLOAT64_BE; |
| break; |
| default: |
| goto error; |
| } |
| alsa->channels = GST_AUDIO_INFO_CHANNELS (&spec->info); |
| break; |
| case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_A_LAW: |
| alsa->format = SND_PCM_FORMAT_A_LAW; |
| alsa->channels = GST_AUDIO_INFO_CHANNELS (&spec->info); |
| break; |
| case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_MU_LAW: |
| alsa->format = SND_PCM_FORMAT_MU_LAW; |
| alsa->channels = GST_AUDIO_INFO_CHANNELS (&spec->info); |
| break; |
| case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_AC3: |
| case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_EAC3: |
| case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_DTS: |
| case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_MPEG: |
| alsa->format = SND_PCM_FORMAT_S16_BE; |
| alsa->iec958 = TRUE; |
| /* Set channels to stereo as iec playload are rendered on a stereo stream */ |
| alsa->channels = 2; |
| break; |
| default: |
| goto error; |
| |
| } |
| alsa->rate = GST_AUDIO_INFO_RATE (&spec->info); |
| alsa->buffer_time = spec->buffer_time; |
| alsa->period_time = spec->latency_time; |
| alsa->access = SND_PCM_ACCESS_RW_INTERLEAVED; |
| alsa->type = spec->type; |
| |
| if (spec->type == GST_AUDIO_RING_BUFFER_FORMAT_TYPE_RAW && alsa->channels < 9) |
| gst_audio_ring_buffer_set_channel_positions (GST_AUDIO_BASE_SINK |
| (alsa)->ringbuffer, alsa_position[alsa->channels - 1]); |
| |
| return TRUE; |
| |
| /* ERRORS */ |
| error: |
| { |
| return FALSE; |
| } |
| } |
| |
| static gboolean |
| gst_alsasink_open (GstAudioSink * asink) |
| { |
| GstAlsaSink *alsa; |
| gint err; |
| |
| alsa = GST_ALSA_SINK (asink); |
| |
| /* open in non-blocking mode, we'll use snd_pcm_wait() for space to become |
| * available. */ |
| CHECK (snd_pcm_open (&alsa->handle, alsa->device, SND_PCM_STREAM_PLAYBACK, |
| SND_PCM_NONBLOCK), open_error); |
| GST_LOG_OBJECT (alsa, "Opened device %s", alsa->device); |
| |
| return TRUE; |
| |
| /* ERRORS */ |
| open_error: |
| { |
| if (err == -EBUSY) { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, BUSY, |
| (_("Could not open audio device for playback. " |
| "Device is being used by another application.")), |
| ("Device '%s' is busy", alsa->device)); |
| } else { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, OPEN_WRITE, |
| (_("Could not open audio device for playback.")), |
| ("Playback open error on device '%s': %s", alsa->device, |
| snd_strerror (err))); |
| } |
| return FALSE; |
| } |
| } |
| |
| static gboolean |
| gst_alsasink_prepare (GstAudioSink * asink, GstAudioRingBufferSpec * spec) |
| { |
| GstAlsaSink *alsa; |
| gint err; |
| |
| alsa = GST_ALSA_SINK (asink); |
| |
| if (!alsasink_parse_spec (alsa, spec)) |
| goto spec_parse; |
| |
| if (alsa->iec958) { |
| snd_pcm_close (alsa->handle); |
| alsa->handle = gst_alsa_open_iec958_pcm (GST_OBJECT (alsa), alsa->device); |
| if (G_UNLIKELY (!alsa->handle)) { |
| goto no_iec958; |
| } |
| } |
| |
| CHECK (set_hwparams (alsa), hw_params_failed); |
| CHECK (set_swparams (alsa), sw_params_failed); |
| |
| alsa->bpf = GST_AUDIO_INFO_BPF (&spec->info); |
| spec->segsize = alsa->period_size * alsa->bpf; |
| spec->segtotal = alsa->buffer_size / alsa->period_size; |
| |
| { |
| snd_output_t *out_buf = NULL; |
| char *msg = NULL; |
| |
| snd_output_buffer_open (&out_buf); |
| snd_pcm_dump_hw_setup (alsa->handle, out_buf); |
| snd_output_buffer_string (out_buf, &msg); |
| GST_DEBUG_OBJECT (alsa, "Hardware setup: \n%s", msg); |
| snd_output_close (out_buf); |
| snd_output_buffer_open (&out_buf); |
| snd_pcm_dump_sw_setup (alsa->handle, out_buf); |
| snd_output_buffer_string (out_buf, &msg); |
| GST_DEBUG_OBJECT (alsa, "Software setup: \n%s", msg); |
| snd_output_close (out_buf); |
| } |
| |
| #ifdef SND_CHMAP_API_VERSION |
| alsa_detect_channels_mapping (GST_OBJECT (alsa), alsa->handle, spec, |
| alsa->channels, GST_AUDIO_BASE_SINK (alsa)->ringbuffer); |
| #endif /* SND_CHMAP_API_VERSION */ |
| |
| return TRUE; |
| |
| /* ERRORS */ |
| no_iec958: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, OPEN_WRITE, (NULL), |
| ("Could not open IEC958 (SPDIF) device for playback")); |
| return FALSE; |
| } |
| spec_parse: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Error parsing spec")); |
| return FALSE; |
| } |
| hw_params_failed: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Setting of hwparams failed: %s", snd_strerror (err))); |
| return FALSE; |
| } |
| sw_params_failed: |
| { |
| GST_ELEMENT_ERROR (alsa, RESOURCE, SETTINGS, (NULL), |
| ("Setting of swparams failed: %s", snd_strerror (err))); |
| return FALSE; |
| } |
| } |
| |
| static gboolean |
| gst_alsasink_unprepare (GstAudioSink * asink) |
| { |
| GstAlsaSink *alsa; |
| |
| alsa = GST_ALSA_SINK (asink); |
| |
| snd_pcm_drop (alsa->handle); |
| snd_pcm_hw_free (alsa->handle); |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| gst_alsasink_close (GstAudioSink * asink) |
| { |
| GstAlsaSink *alsa = GST_ALSA_SINK (asink); |
| |
| GST_OBJECT_LOCK (asink); |
| if (alsa->handle) { |
| snd_pcm_close (alsa->handle); |
| alsa->handle = NULL; |
| } |
| gst_caps_replace (&alsa->cached_caps, NULL); |
| GST_OBJECT_UNLOCK (asink); |
| |
| return TRUE; |
| } |
| |
| |
| /* |
| * Underrun and suspend recovery |
| */ |
| static gint |
| xrun_recovery (GstAlsaSink * alsa, snd_pcm_t * handle, gint err) |
| { |
| GST_WARNING_OBJECT (alsa, "xrun recovery %d: %s", err, g_strerror (-err)); |
| |
| if (err == -EPIPE) { /* under-run */ |
| err = snd_pcm_prepare (handle); |
| if (err < 0) |
| GST_WARNING_OBJECT (alsa, |
| "Can't recover from underrun, prepare failed: %s", |
| snd_strerror (err)); |
| gst_audio_base_sink_report_device_failure (GST_AUDIO_BASE_SINK (alsa)); |
| return 0; |
| } else if (err == -ESTRPIPE) { |
| while ((err = snd_pcm_resume (handle)) == -EAGAIN) |
| g_usleep (100); /* wait until the suspend flag is released */ |
| |
| if (err < 0) { |
| err = snd_pcm_prepare (handle); |
| if (err < 0) |
| GST_WARNING_OBJECT (alsa, |
| "Can't recover from suspend, prepare failed: %s", |
| snd_strerror (err)); |
| } |
| if (err == 0) |
| gst_audio_base_sink_report_device_failure (GST_AUDIO_BASE_SINK (alsa)); |
| return 0; |
| } |
| return err; |
| } |
| |
| static gint |
| gst_alsasink_write (GstAudioSink * asink, gpointer data, guint length) |
| { |
| GstAlsaSink *alsa; |
| gint err; |
| gint cptr; |
| guint8 *ptr = data; |
| |
| alsa = GST_ALSA_SINK (asink); |
| |
| if (alsa->iec958 && alsa->need_swap) { |
| guint i; |
| guint16 *ptr_tmp = (guint16 *) ptr; |
| |
| GST_DEBUG_OBJECT (asink, "swapping bytes"); |
| for (i = 0; i < length / 2; i++) { |
| ptr_tmp[i] = GUINT16_SWAP_LE_BE (ptr_tmp[i]); |
| } |
| } |
| |
| GST_LOG_OBJECT (asink, "received audio samples buffer of %u bytes", length); |
| |
| if (alsa->iec958 && (TRUE == alsa->passthrough) ) { |
| cptr = length / 4; // treated as 2ch with S16 in pass-through mode |
| } |
| else { |
| cptr = length / alsa->bpf; |
| } |
| GST_ALSA_SINK_LOCK (asink); |
| while (cptr > 0) { |
| /* start by doing a blocking wait for free space. Set the timeout |
| * to 4 times the period time */ |
| err = snd_pcm_wait (alsa->handle, (4 * alsa->period_time / 1000)); |
| if (err < 0) { |
| GST_DEBUG_OBJECT (asink, "wait error, %d", err); |
| } else { |
| GST_DELAY_SINK_LOCK (asink); |
| err = snd_pcm_writei (alsa->handle, ptr, cptr); |
| GST_DELAY_SINK_UNLOCK (asink); |
| } |
| |
| GST_DEBUG_OBJECT (asink, "written %d frames out of %d", err, cptr); |
| if (err < 0) { |
| GST_DEBUG_OBJECT (asink, "Write error: %s", snd_strerror (err)); |
| if (err == -EAGAIN) { |
| continue; |
| } else if (err == -ENODEV) { |
| goto device_disappeared; |
| } else if (xrun_recovery (alsa, alsa->handle, err) < 0) { |
| goto write_error; |
| } |
| continue; |
| } |
| |
| ptr += snd_pcm_frames_to_bytes (alsa->handle, err); |
| cptr -= err; |
| } |
| GST_ALSA_SINK_UNLOCK (asink); |
| |
| return length - (cptr * alsa->bpf); |
| |
| write_error: |
| { |
| GST_ALSA_SINK_UNLOCK (asink); |
| return length; /* skip one period */ |
| } |
| device_disappeared: |
| { |
| GST_ELEMENT_ERROR (asink, RESOURCE, WRITE, |
| (_("Error outputting to audio device. " |
| "The device has been disconnected.")), (NULL)); |
| goto write_error; |
| } |
| } |
| |
| static guint |
| gst_alsasink_delay (GstAudioSink * asink) |
| { |
| GstAlsaSink *alsa; |
| snd_pcm_sframes_t delay; |
| int res; |
| |
| alsa = GST_ALSA_SINK (asink); |
| |
| GST_DELAY_SINK_LOCK (asink); |
| res = snd_pcm_delay (alsa->handle, &delay); |
| GST_DELAY_SINK_UNLOCK (asink); |
| if (G_UNLIKELY (res < 0)) { |
| /* on errors, report 0 delay */ |
| GST_DEBUG_OBJECT (alsa, "snd_pcm_delay returned %d", res); |
| delay = 0; |
| } |
| if (G_UNLIKELY (delay < 0)) { |
| /* make sure we never return a negative delay */ |
| GST_WARNING_OBJECT (alsa, "snd_pcm_delay returned negative delay"); |
| delay = 0; |
| } |
| |
| return delay; |
| } |
| |
| static void |
| gst_alsasink_reset (GstAudioSink * asink) |
| { |
| GstAlsaSink *alsa; |
| gint err; |
| |
| alsa = GST_ALSA_SINK (asink); |
| |
| GST_ALSA_SINK_LOCK (asink); |
| GST_DEBUG_OBJECT (alsa, "drop"); |
| CHECK (snd_pcm_drop (alsa->handle), drop_error); |
| GST_DEBUG_OBJECT (alsa, "prepare"); |
| CHECK (snd_pcm_prepare (alsa->handle), prepare_error); |
| GST_DEBUG_OBJECT (alsa, "reset done"); |
| GST_ALSA_SINK_UNLOCK (asink); |
| |
| return; |
| |
| /* ERRORS */ |
| drop_error: |
| { |
| GST_ERROR_OBJECT (alsa, "alsa-reset: pcm drop error: %s", |
| snd_strerror (err)); |
| GST_ALSA_SINK_UNLOCK (asink); |
| return; |
| } |
| prepare_error: |
| { |
| GST_ERROR_OBJECT (alsa, "alsa-reset: pcm prepare error: %s", |
| snd_strerror (err)); |
| GST_ALSA_SINK_UNLOCK (asink); |
| return; |
| } |
| } |
| |
| static GstBuffer * |
| gst_alsasink_payload (GstAudioBaseSink * sink, GstBuffer * buf) |
| { |
| GstAlsaSink *alsa; |
| |
| alsa = GST_ALSA_SINK (sink); |
| |
| if (alsa->iec958) { |
| GstBuffer *out; |
| gint framesize; |
| GstMapInfo iinfo, oinfo; |
| |
| framesize = gst_audio_iec61937_frame_size (&sink->ringbuffer->spec); |
| if (framesize <= 0) |
| return NULL; |
| |
| out = gst_buffer_new_and_alloc (framesize); |
| |
| gst_buffer_map (buf, &iinfo, GST_MAP_READ); |
| gst_buffer_map (out, &oinfo, GST_MAP_WRITE); |
| |
| if (!gst_audio_iec61937_payload (iinfo.data, iinfo.size, |
| oinfo.data, oinfo.size, &sink->ringbuffer->spec, G_BIG_ENDIAN)) { |
| gst_buffer_unmap (buf, &iinfo); |
| gst_buffer_unmap (out, &oinfo); |
| gst_buffer_unref (out); |
| return NULL; |
| } |
| |
| gst_buffer_unmap (buf, &iinfo); |
| gst_buffer_unmap (out, &oinfo); |
| |
| gst_buffer_copy_into (out, buf, GST_BUFFER_COPY_METADATA, 0, -1); |
| return out; |
| } |
| |
| return gst_buffer_ref (buf); |
| } |