/* * Copyright (C) 2001 CodeFactory AB * Copyright (C) 2001 Thomas Nyberg * Copyright (C) 2001-2002 Andy Wingo * Copyright (C) 2003 Benjamin Otte * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU 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 * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this library; if not, write to the Free * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include #include #include "gstalsa.h" /* error checking for standard alsa functions */ #ifdef G_HAVE_ISO_VARARGS #define ERROR_CHECK(value, ...) G_STMT_START{ \ int err = (value); \ if (err < 0) { \ g_warning ( __VA_ARGS__, snd_strerror (err)); \ return FALSE; \ } \ }G_STMT_END #elif defined(G_HAVE_GNUC_VARARGS) #define ERROR_CHECK(value, args...) G_STMT_START{ \ int err = (value); \ if (err < 0) { \ g_warning ( ## args, snd_strerror (err)); \ return FALSE; \ } \ }G_STMT_END #else #define ERROR_CHECK(value, args...) G_STMT_START{ \ int err = (value); \ if (err < 0) { \ g_warning (snd_strerror (err)); \ return FALSE; \ } \ }G_STMT_END #endif /* elementfactory information */ static GstElementDetails gst_alsa_sink_details = { "Alsa Sink", "Sink/Audio", "LGPL", "Output to a sound card via ALSA", VERSION, "Thomas Nyberg , " "Andy Wingo " "Benjamin Otte ", "(C) 2001-2003" }; /* elementfactory information */ static GstElementDetails gst_alsa_src_details = { "Alsa Src", "Source/Audio", "LGPL", "Read from a sound card via ALSA", VERSION, "Thomas Nyberg , " "Andy Wingo " "Benjamin Otte ", "(C) 2001-2003" }; /* GObject functions */ static void gst_alsa_class_init (GstAlsaClass *klass); static void gst_alsa_init (GstAlsa *this); static void gst_alsa_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); static void gst_alsa_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); /* GStreamer functions for pads and state changing */ static GstPadTemplate *gst_alsa_src_pad_factory (); static GstPadTemplate *gst_alsa_src_request_pad_factory (); static GstPadTemplate *gst_alsa_sink_pad_factory (); static GstPadTemplate *gst_alsa_sink_request_pad_factory (); static GstPad *gst_alsa_request_new_pad (GstElement *element, GstPadTemplate *templ, const gchar *name); static GstPadLinkReturn gst_alsa_link (GstPad *pad, GstCaps *caps); static GstCaps *gst_alsa_caps (snd_pcm_format_t format, gint rate, gint channels); static GstElementStateReturn gst_alsa_change_state (GstElement *element); /* audio processing functions */ static void gst_alsa_sink_loop (GstElement *element); static void gst_alsa_xrun_recovery (GstAlsa *this); static gboolean gst_alsa_sink_check_event (GstAlsa *this, gint pad_nr); /* alsa setup / start / stop functions */ static gboolean gst_alsa_set_hw_params (GstAlsa *this); static gboolean gst_alsa_set_sw_params (GstAlsa *this); static gboolean gst_alsa_open_audio (GstAlsa *this); static gboolean gst_alsa_start_audio (GstAlsa *this); static gboolean gst_alsa_drain_audio (GstAlsa *this); static gboolean gst_alsa_stop_audio (GstAlsa *this); static gboolean gst_alsa_close_audio (GstAlsa *this); /*** TYPE FUNCTIONS ***********************************************************/ #define GST_TYPE_ALSA_FORMAT (gst_alsa_format_get_type()) static GType gst_alsa_format_get_type (void) { static GType type = 0; static GEnumValue *values = NULL; gint i; if (values == NULL) { /* the three: for -1, 0, and the terminating NULL */ values = g_new0 (GEnumValue, SND_PCM_FORMAT_LAST + 1); for (i = -1; i <= SND_PCM_FORMAT_LAST; i++) { values[i + 1].value = i; /* UNKNOWN is -1 */ values[i + 1].value_name = g_strdup_printf ("%d", i); values[i + 1].value_nick = g_strdup (snd_pcm_format_name ((snd_pcm_format_t) i)); } } if (!type) type = g_enum_register_static ("GstAlsaFormat", values); return type; } GType gst_alsa_get_type (void) { static GType alsa_type = 0; if (!alsa_type) { static const GTypeInfo alsa_info = { sizeof (GstAlsaClass), NULL, NULL, NULL, NULL, NULL, sizeof (GstAlsa), 0, NULL, }; alsa_type = g_type_register_static (GST_TYPE_ELEMENT, "GstAlsa", &alsa_info, 0); } return alsa_type; } GType gst_alsa_sink_get_type (void) { static GType alsa_type = 0; if (!alsa_type) { static const GTypeInfo alsa_info = { sizeof (GstAlsaClass), NULL, NULL, (GClassInitFunc) gst_alsa_class_init, NULL, NULL, sizeof (GstAlsa), 0, (GInstanceInitFunc) gst_alsa_init, }; alsa_type = g_type_register_static (GST_TYPE_ALSA, "GstAlsaSink", &alsa_info, 0); } return alsa_type; } GType gst_alsa_src_get_type (void) { static GType alsa_type = 0; if (!alsa_type) { static const GTypeInfo alsa_info = { sizeof (GstAlsaClass), NULL, NULL, (GClassInitFunc) gst_alsa_class_init, NULL, NULL, sizeof (GstAlsa), 0, (GInstanceInitFunc) gst_alsa_init, }; alsa_type = g_type_register_static (GST_TYPE_ALSA, "GstAlsaSrc", &alsa_info, 0); } return alsa_type; } /*** GOBJECT FUNCTIONS ********************************************************/ enum { ARG_0, ARG_DEVICE, ARG_FORMAT, ARG_CHANNELS, ARG_RATE, ARG_PERIODCOUNT, ARG_PERIODSIZE, ARG_BUFFERSIZE, ARG_DEBUG, ARG_AUTORECOVER }; static GstElement *parent_class = NULL; static void gst_alsa_class_init (GstAlsaClass *klass) { GObjectClass *object_class; GstElementClass *element_class; object_class = (GObjectClass *) klass; element_class = (GstElementClass *) klass; if (parent_class == NULL) parent_class = g_type_class_ref (GST_TYPE_ELEMENT); object_class->get_property = gst_alsa_get_property; object_class->set_property = gst_alsa_set_property; g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_DEVICE, g_param_spec_string ("device", "Device", "Alsa device, as defined in an asoundrc", "default", G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); /* The next 3 should only be settable on srcs, should it? */ g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_FORMAT, g_param_spec_enum ("format", "Format", "PCM audio format", GST_TYPE_ALSA_FORMAT, -1, G_PARAM_READABLE)); g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_CHANNELS, g_param_spec_int ("channels", "Channels", "Number of channels", 1, GST_ALSA_MAX_CHANNELS, 2, G_PARAM_READABLE)); g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_RATE, g_param_spec_int ("rate", "Rate", "Sample rate, in Hz", 8000, 192000, 44100, G_PARAM_READABLE)); g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_PERIODCOUNT, g_param_spec_int ("period-count", "Period count", "Number of hardware buffers to use", 2, 64, 2, G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_PERIODSIZE, g_param_spec_int ("period-size", "Period size", "Number of frames (samples on each channel) in one hardware period", 64, 8192, 8192, G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_BUFFERSIZE, g_param_spec_int ("buffer-size", "Buffer size", "Number of frames the hardware buffer can hold", 128, 65536, 16384, G_PARAM_READWRITE)); g_object_class_install_property (G_OBJECT_CLASS (klass), ARG_AUTORECOVER, g_param_spec_boolean ("autorecover", "Automatic xrun recovery", "When TRUE tries to reduce processor load on xruns", TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); element_class->change_state = gst_alsa_change_state; element_class->request_new_pad = gst_alsa_request_new_pad; } static void gst_alsa_init (GstAlsa *this) { gint i; /* init values */ this->handle = NULL; this->channels = 1; GST_FLAG_SET (this, GST_ELEMENT_THREAD_SUGGESTED); if (G_OBJECT_TYPE (this) == GST_TYPE_ALSA_SRC) { this->stream = SND_PCM_STREAM_CAPTURE; this->format = SND_PCM_FORMAT_S16; /* native endian */ this->pads[0].pad = gst_pad_new_from_template (gst_alsa_sink_pad_factory (), "src"); } else if (G_OBJECT_TYPE (this) == GST_TYPE_ALSA_SINK) { this->stream = SND_PCM_STREAM_PLAYBACK; this->format = SND_PCM_FORMAT_UNKNOWN; /* we don't know until caps are set */ gst_element_set_loop_function (GST_ELEMENT (this), gst_alsa_sink_loop); this->pads[0].pad = gst_pad_new_from_template (gst_alsa_sink_pad_factory (), "sink"); this->pads[0].bs = gst_bytestream_new (this->pads[0].pad); } gst_element_add_pad (GST_ELEMENT (this), this->pads[0].pad); for (i = 1; i < GST_ALSA_MAX_CHANNELS; i++) { this->pads[i].pad = NULL; } gst_pad_set_link_function (this->pads[0].pad, gst_alsa_link); } static void gst_alsa_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GstAlsa *this; gint buffer_size; this = (GstAlsa *) object; switch (prop_id) { case ARG_DEVICE: if (this->device) g_free (this->device); this->device = g_strdup (g_value_get_string (value)); break; /* case ARG_FORMAT: this->format = g_value_get_enum (value); break; case ARG_CHANNELS: this->channels = g_value_get_int (value); break; case ARG_RATE: this->rate = g_value_get_int (value); break;*/ case ARG_PERIODCOUNT: g_return_if_fail (!GST_FLAG_IS_SET (this, GST_ALSA_RUNNING)); this->period_count = g_value_get_int (value); break; case ARG_PERIODSIZE: g_return_if_fail (!GST_FLAG_IS_SET (this, GST_ALSA_RUNNING)); this->period_size = g_value_get_int (value); break; case ARG_BUFFERSIZE: g_return_if_fail (!GST_FLAG_IS_SET (this, GST_ALSA_RUNNING)); buffer_size = g_value_get_int (value); this->period_count = buffer_size / this->period_size; break; case ARG_AUTORECOVER: this->autorecover = g_value_get_boolean (value); return; default: GST_DEBUG (0, "Unknown arg"); return; } if (GST_STATE (this) == GST_STATE_NULL) return; if (GST_FLAG_IS_SET (this, GST_ALSA_RUNNING)) { gst_alsa_stop_audio (this); gst_alsa_start_audio (this); } } static void gst_alsa_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GstAlsa *this; this = (GstAlsa *) object; switch (prop_id) { case ARG_DEVICE: g_value_set_string (value, this->device); break; /* case ARG_FORMAT: g_value_set_enum (value, this->format); break; case ARG_CHANNELS: g_value_set_int (value, this->channels); break; case ARG_RATE: g_value_set_int (value, this->rate); break; */ case ARG_PERIODCOUNT: g_value_set_int (value, this->period_count); break; case ARG_PERIODSIZE: g_value_set_int (value, this->period_size); break; case ARG_BUFFERSIZE: g_value_set_int (value, this->period_size * this->period_count); break; case ARG_AUTORECOVER: g_value_set_boolean (value, this->autorecover); break; default: GST_DEBUG (0, "Unknown arg"); break; } } /*** GSTREAMER PAD / STATE FUNCTIONS*******************************************/ static GstPadTemplate * gst_alsa_src_pad_factory (void) { static GstPadTemplate *template = NULL; if (!template) template = gst_pad_template_new ("src", GST_PAD_SRC, GST_PAD_ALWAYS, gst_alsa_caps (SND_PCM_FORMAT_UNKNOWN, -1, -1), NULL); return template; } static GstPadTemplate * gst_alsa_src_request_pad_factory (void) { static GstPadTemplate *template = NULL; if (!template) template = gst_pad_template_new ("src", GST_PAD_SRC, GST_PAD_ALWAYS, gst_alsa_caps (SND_PCM_FORMAT_UNKNOWN, -1, 1), NULL); return template; } static GstPadTemplate * gst_alsa_sink_pad_factory (void) { static GstPadTemplate *template = NULL; if (!template) template = gst_pad_template_new ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, gst_alsa_caps (SND_PCM_FORMAT_UNKNOWN, -1, -1), NULL); return template; } static GstPadTemplate * gst_alsa_sink_request_pad_factory (void) { static GstPadTemplate *template = NULL; if (!template) template = gst_pad_template_new ("sink%d", GST_PAD_SINK, GST_PAD_REQUEST, gst_alsa_caps (SND_PCM_FORMAT_UNKNOWN, -1, 1), NULL); return template; } static GstPad * gst_alsa_request_new_pad (GstElement *element, GstPadTemplate *templ, const gchar *name) { GstAlsa *this; gint channel = 0; g_return_val_if_fail ((this = GST_ALSA (element)), NULL); g_return_val_if_fail (GST_FLAG_IS_SET (element, GST_ALSA_RUNNING), NULL); /* you can't request a pad if the non-request pad already has more than 1 channel */ g_return_val_if_fail (this->channels > GST_ELEMENT(this)->numpads, NULL); if (name) { /* locate the channel number in the requested pad name. to do so look at where the % (which begins the %d) is in the template name. */ channel = (gint) strtol (name + (strchr (templ->name_template, '%') - templ->name_template), NULL, 0); if (channel < 1 || channel >= GST_ALSA_MAX_CHANNELS) { g_warning ("invalid channel requested. (%d)", channel); return NULL; } } /* make sure the requested channel is free. */ if (channel > 0 || this->pads[channel].pad != NULL) { g_warning ("requested channel %d already in use.", channel); return NULL; } /* if the user doesn't care which channel, find the lowest channel number that's free. */ if (channel == 0) { for (channel = 1; channel < GST_ALSA_MAX_CHANNELS; channel++) { if (this->pads[channel].pad != NULL) goto found_channel; } return NULL; } found_channel: this->pads[channel].pad = gst_pad_new_from_template (templ, name); gst_pad_set_link_function (this->pads[channel].pad, gst_alsa_link); gst_element_add_pad (GST_ELEMENT (this), this->pads[channel].pad); if (G_OBJECT_TYPE (this) == GST_TYPE_ALSA_SINK) this->pads[channel].bs = gst_bytestream_new (this->pads[channel].pad); return this->pads[channel].pad; } /* gets the matching alsa format or SND_PCM_FORMAT_UNKNOWN if none matches */ static snd_pcm_format_t gst_alsa_get_format (GstCaps *caps) { const gchar *format_name; /** * We have to differentiate between int and float, so lets find out which one we have. * Somebody fix the float caps spec please. */ if (!gst_caps_get_string (caps, "format", &format_name)) return SND_PCM_FORMAT_UNKNOWN; if (strncmp (format_name, "int", 3) == 0) { gboolean sign; int width, depth, endianness, law; /* extract the needed information from the caps */ if (!gst_caps_get (caps, "width", &width, "depth", &depth, "law", &law, "endianness", &endianness, "signed", &sign, NULL)) return SND_PCM_FORMAT_UNKNOWN; /* find corresponding alsa format */ switch (law) { case 0: return snd_pcm_build_linear_format (depth, width, sign ? 0 : 1, endianness == G_LITTLE_ENDIAN ? 0 : 1); case 1: return SND_PCM_FORMAT_A_LAW; case 2: return SND_PCM_FORMAT_MU_LAW; default: return SND_PCM_FORMAT_UNKNOWN; } } else if (strncmp (format_name, "float", 5) == 0) { gchar *layout; /* get layout */ if (!gst_caps_get_string (caps, "layout", (const gchar **) &layout)) return SND_PCM_FORMAT_UNKNOWN; /* match layout to format wrt to endianness */ if (strncmp (layout, "gfloat", 6) == 0) { return SND_PCM_FORMAT_FLOAT; } else if (strncmp (layout, "gdouble", 7) == 0) { return SND_PCM_FORMAT_FLOAT64; } else { return SND_PCM_FORMAT_UNKNOWN; } } return SND_PCM_FORMAT_UNKNOWN; } /* get props for a spec */ static GstProps * gst_alsa_get_props (snd_pcm_format_t format) { /* int or float */ if (snd_pcm_format_linear (format)) { GstProps *props = gst_props_new ("format", GST_PROPS_STRING ("int"), "width", GST_PROPS_INT(snd_pcm_format_physical_width (format)), "depth", GST_PROPS_INT(snd_pcm_format_width (format)), "signed", GST_PROPS_BOOLEAN (snd_pcm_format_signed (format) == 1 ? TRUE : FALSE), NULL); /* endianness */ switch (snd_pcm_format_little_endian (format)) { case 0: gst_props_add_entry (props, gst_props_entry_new ("endianness", GST_PROPS_INT (G_BIG_ENDIAN))); break; case 1: gst_props_add_entry (props, gst_props_entry_new ("endianness", GST_PROPS_INT (G_LITTLE_ENDIAN))); break; default: break; } /* law */ switch (format) { case SND_PCM_FORMAT_A_LAW: gst_props_add_entry (props, gst_props_entry_new ("law", GST_PROPS_INT (1))); break; case SND_PCM_FORMAT_MU_LAW: gst_props_add_entry (props, gst_props_entry_new ("law", GST_PROPS_INT (2))); break; default: gst_props_add_entry (props, gst_props_entry_new ("law", GST_PROPS_INT (0))); break; } return props; } else if (snd_pcm_format_float (format)) { /* no float with non-platform endianness */ if (!snd_pcm_format_cpu_endian (format)) return NULL; return gst_props_new ("format", GST_PROPS_STRING ("float"), "layout", GST_PROPS_STRING (snd_pcm_format_width (format) == 64 ? "gdouble" : "gfloat"), NULL); } return NULL; } static inline void add_channels (GstProps *props, gint rate, gint channels) { if (rate < 0) { gst_props_add_entry (props, gst_props_entry_new ("rate", GST_PROPS_INT_RANGE (8000, 192000))); } else { gst_props_add_entry (props, gst_props_entry_new ("rate", GST_PROPS_INT (rate))); } if (channels < 0) { gst_props_add_entry (props, gst_props_entry_new ("channels", GST_PROPS_INT_RANGE (1, GST_ALSA_MAX_CHANNELS))); } else { gst_props_add_entry (props, gst_props_entry_new ("channels", GST_PROPS_INT (channels))); } } /** * Get all available caps. * @format: SND_PCM_FORMAT_UNKNOWN for all formats, desired format else * @rate: allowed rates if < 0, else desired rate * @channels: all allowed values for channels if < 0, else desired channels */ static GstCaps * gst_alsa_caps (snd_pcm_format_t format, gint rate, gint channels) { GstCaps *ret_caps = NULL; if (format != SND_PCM_FORMAT_UNKNOWN) { /* there are some caps set already */ GstProps *props = gst_alsa_get_props (format); /* we can never use a format we can't set caps for */ g_assert (props != NULL); add_channels (props, rate, channels); ret_caps = gst_caps_new ("alsacaps", "audio/raw", props); } else { int i; GstProps *props; for (i = 0; i <= SND_PCM_FORMAT_LAST; i++) { props = gst_alsa_get_props (i); /* can be NULL, because not all alsa formats can be specified as caps */ if (props != NULL) { add_channels (props, rate, channels); ret_caps = gst_caps_append (ret_caps, gst_caps_new (g_strdup (snd_pcm_format_name (i)), "audio/raw", props)); } } } return ret_caps; } /* Negotiates the caps */ GstPadLinkReturn gst_alsa_link (GstPad *pad, GstCaps *caps) { GstAlsa *this; snd_pcm_format_t format; gint rate, channels; g_return_val_if_fail (caps != NULL, GST_PAD_LINK_REFUSED); g_return_val_if_fail (pad != NULL, GST_PAD_LINK_REFUSED); this = GST_ALSA (gst_pad_get_parent (pad)); if (GST_CAPS_IS_FIXED (caps)) { if (this->handle == NULL) if (!gst_alsa_open_audio (this)) return GST_PAD_LINK_REFUSED; /* FIXME: allow changing the format here, even by retrying caps on other pads */ format = gst_alsa_get_format (caps); GST_DEBUG (GST_CAT_CAPS, "found format %s\n", snd_pcm_format_name (format)); if (this->format != SND_PCM_FORMAT_UNKNOWN && this->format != format) return GST_PAD_LINK_REFUSED; if (!gst_caps_get (caps, "rate", &rate, "channels", &channels, NULL)) return GST_PAD_LINK_REFUSED; if (this->format != SND_PCM_FORMAT_UNKNOWN && this->rate != rate) return GST_PAD_LINK_REFUSED; if (this->format != SND_PCM_FORMAT_UNKNOWN && ((this->channels > 1 || channels > 1))) return GST_PAD_LINK_REFUSED; if (this->format == SND_PCM_FORMAT_UNKNOWN) { this->channels = channels; } else { this->channels++; } this->format = format; this->rate = rate; /* sync the params */ if (GST_FLAG_IS_SET (this, GST_ALSA_RUNNING)) gst_alsa_stop_audio (this); if (GST_FLAG_IS_SET (this, GST_ALSA_OPEN)) gst_alsa_close_audio (this); if (!gst_alsa_open_audio (this)) return GST_PAD_LINK_REFUSED; if (!gst_alsa_start_audio (this)) return GST_PAD_LINK_REFUSED; return GST_PAD_LINK_OK; } return GST_PAD_LINK_DELAYED; } static GstElementStateReturn gst_alsa_change_state (GstElement *element) { GstAlsa *this; g_return_val_if_fail (element != NULL, FALSE); this = GST_ALSA (element); switch (GST_STATE_TRANSITION (element)) { case GST_STATE_NULL_TO_READY: if (GST_FLAG_IS_SET (element, GST_ALSA_OPEN) == FALSE) if (!gst_alsa_open_audio (this)) return GST_STATE_FAILURE; break; case GST_STATE_READY_TO_PAUSED: break; case GST_STATE_PAUSED_TO_PLAYING: if (GST_FLAG_IS_SET (element, GST_ALSA_OPEN) == FALSE) if (!gst_alsa_open_audio (this)) return GST_STATE_FAILURE; case GST_STATE_PLAYING_TO_PAUSED: if (GST_FLAG_IS_SET (element, GST_ALSA_RUNNING)) gst_alsa_drain_audio (this); break; case GST_STATE_PAUSED_TO_READY: break; case GST_STATE_READY_TO_NULL: if (GST_FLAG_IS_SET (element, GST_ALSA_OPEN)) gst_alsa_close_audio (this); break; default: g_assert_not_reached(); } if (GST_ELEMENT_CLASS (parent_class)->change_state) return GST_ELEMENT_CLASS (parent_class)->change_state (element); return GST_STATE_SUCCESS; } /*** AUDIO PROCESSING *********************************************************/ static void gst_alsa_sink_loop (GstElement *element) { snd_pcm_sframes_t avail; gint i; gint bytes, num_bytes; /* per channel */ GstAlsa *this = GST_ALSA (element); g_return_if_fail (this != NULL); /* caps nego: fetch 1 byte from every pad */ if (this->format == SND_PCM_FORMAT_UNKNOWN) { GST_DEBUG (GST_CAT_NEGOTIATION, "starting caps negotiation"); for (i = 0; i < element->numpads; i++) { g_assert (this->pads[i].pad != NULL); do { num_bytes = gst_bytestream_peek_bytes (this->pads[i].bs, &this->pads[i].data, 1); } while (num_bytes == 0 && gst_alsa_sink_check_event (this, i)); if (num_bytes == 0) return; } if (this->format == SND_PCM_FORMAT_UNKNOWN) { gst_element_error (GST_ELEMENT (this), "alsasink: No caps available"); } } while (1) { avail = snd_pcm_avail_update (this->handle); if (avail < 0) { if (avail == -EPIPE) { gst_alsa_xrun_recovery (this); continue; } else { g_warning ("unknown ALSA avail_update return value (%d)", (int) avail); return; } } if (avail > 0) { /* check how many bytes we still have in all our bytestreams */ bytes = avail * ( snd_pcm_format_physical_width (this->format) / 8 ) * (element->numpads == 1 ? this->channels : 1); for (i = 0; i < element->numpads; i++) { g_assert (this->pads[i].pad != NULL); do { num_bytes = gst_bytestream_peek_bytes (this->pads[i].bs, &this->pads[i].data, bytes); } while (num_bytes == 0 && gst_alsa_sink_check_event (this, i)); if (num_bytes == 0) return; bytes = MIN (bytes, num_bytes); } /* FIXME: lotsa stuff can have happened while fetching data. Do we need to check something? */ /* put this data into alsa */ { snd_pcm_uframes_t offset; snd_pcm_channel_area_t *dst; int err; int width = snd_pcm_format_physical_width (this->format); snd_pcm_channel_area_t *src = calloc(this->channels, sizeof(snd_pcm_channel_area_t)); avail = bytes / (width / 8 ) / (element->numpads == 1 ? this->channels : 1); if (element->numpads == 1) { /* interleaved */ for (i = 0; i < this->channels; i++) { src[i].addr = this->pads[0].data; src[i].first = i * width; src[i].step = this->channels * width; } } else { /* noninterleaved */ for (i = 0; i < this->channels; i++) { src[i].addr = this->pads[i].data; src[i].first = 0; src[i].step = width; } } if ((err = snd_pcm_mmap_begin (this->handle, (const snd_pcm_channel_area_t **) &dst, &offset, &avail)) < 0) { g_warning ("gstalsa: mmap failed: %s", snd_strerror (err)); return; } if ((err = snd_pcm_areas_copy (dst, offset, src, 0, this->channels, avail, this->format)) < 0) { snd_pcm_mmap_commit (this->handle, offset, 0); g_warning ("gstalsa: data copy failed: %s", snd_strerror (err)); return; } if ((err = snd_pcm_mmap_commit (this->handle, offset, avail)) < 0) { g_warning ("gstalsa: mmap commit failed: %s", snd_strerror (err)); return; } /* flush the data */ bytes = err * ( width / 8 ) * (element->numpads == 1 ? this->channels : 1); for (i = 0; i < element->numpads; i++) { gst_bytestream_flush (this->pads[i].bs, bytes); } } /* BUG: we start the stream explicitly, autostart doesn't work correctly (alsa 0.9.0rc7) */ if (snd_pcm_state(this->handle) == SND_PCM_STATE_PREPARED && snd_pcm_avail_update (this->handle) == 0) { GST_DEBUG (GST_CAT_PLUGIN_INFO, "Explicitly starting playback"); snd_pcm_start(this->handle); } } /* wait */ if (snd_pcm_state(this->handle) == SND_PCM_STATE_RUNNING) { if (snd_pcm_wait (this->handle, 1000) < 0) { if (errno == EINTR) { /* this happens mostly when run * under gdb, or when exiting due to a signal */ GST_DEBUG (GST_CAT_PLUGIN_INFO, "got interrupted while waiting"); if (gst_element_interrupt (element)) break; else continue; } g_warning ("error waiting for alsa pcm: (%d: %s)", errno, strerror (errno)); return; } } } } static void gst_alsa_xrun_recovery (GstAlsa *this) { snd_pcm_status_t *status; gint err; snd_pcm_status_alloca (&status); if ((err = snd_pcm_status (this->handle, status)) < 0) g_warning ("status error: %s", snd_strerror (err)); if (snd_pcm_status_get_state (status) == SND_PCM_STATE_XRUN) { struct timeval now, diff, tstamp; gettimeofday (&now, 0); snd_pcm_status_get_trigger_tstamp (status, &tstamp); timersub (&now, &tstamp, &diff); g_warning ("alsa: xrun of at least %.3f msecs", diff.tv_sec * 1000 + diff.tv_usec / 1000.0); if (this->autorecover) { /* increase the period size or buffer size / period count to prevent further xruns (at the cost of increased latency and memory usage). Only do this if it's allowed. */ if (this->period_count >= 4) { this->period_size *= 2; this->period_count /= 2; } else { this->period_count *= 2; } } } if (!(gst_alsa_stop_audio (this) && gst_alsa_start_audio (this))) { gst_element_error (GST_ELEMENT (this), "alsasink: Error restarting audio after xrun"); } } #if 0 /* FIXME: DO NOT RECORD WITH ALSA, IT DOESN'T WORK, IT ONLY COMPILES */ static gboolean gst_alsa_src_process (GstAlsa *this, snd_pcm_uframes_t frames) { GstAlsaPad *pad = NULL; GstBuffer *buf; GstCaps *caps; GList *l; guint32 channel; gint unit; static gboolean caps_set = FALSE; /* let's get on the caps-setting merry-go-round! */ if (!caps_set) { caps = gst_alsa_caps (this->format, this->rate, this->channels); l = this->pads; while (l) { if (gst_pad_try_set_caps (GST_ALSA_PAD (l)->pad, caps) <= 0) { DEBUG (this, "setting caps (%p) in source (%p) failed\n", caps, this); sleep (1); return FALSE; } l = l->next; } caps_set = TRUE; } unit = this->sample_bytes * (this->data_interleaved ? this->channels : 1); /* pull the same amount of data into each pad. create an outgoing buffer with the data, and push the buffer. lather, rinse, repeat. */ while (frames) { l = this->pads; while (l) { pad = GST_ALSA_PAD (l); if (!pad->buf) pad->buf = g_malloc (this->period_frames * unit); channel = (pad->channel > 0) ? pad->channel - 1 : 0; memcpy (pad->buf + pad->offset * unit, this->access_addr[channel], MIN (frames, this->period_frames - pad->offset) * unit); pad->offset += MIN (frames, this->period_frames - pad->offset); if (pad->offset >= this->period_frames) { g_assert (pad->offset <= this->period_frames); buf = gst_buffer_new (); GST_BUFFER_DATA (buf) = pad->buf; GST_BUFFER_SIZE (buf) = this->period_frames * unit; GST_BUFFER_MAXSIZE (buf) = this->period_frames * unit; gst_pad_push (pad->pad, buf); pad->buf = NULL; pad->offset = 0; } l = l->next; } /* all pads should have the same amount of data, shouldn't matter which one we use for updating output frames ... */ frames -= MIN (frames, this->period_frames - pad->offset); } return TRUE; } #endif /* TRUE, if everything should continue */ static gboolean gst_alsa_sink_check_event (GstAlsa *this, gint pad_nr) { GstEvent *event = NULL; guint32 avail; gboolean cont = TRUE; gst_bytestream_get_status (this->pads[pad_nr].bs, &avail, &event); if (event) { if (GST_EVENT_TYPE (event) == GST_EVENT_EOS) { gst_element_set_eos (GST_ELEMENT (this)); cont = FALSE; } else { g_warning ("GstAlsaSink: got an unknown event (Type: %d)", GST_EVENT_TYPE (event)); } gst_event_unref (event); } else { /* the element at the top of the chain did not emit an event. */ g_assert_not_reached (); } return cont; } /*** AUDIO SETUP / START / STOP ***********************************************/ static gboolean gst_alsa_open_audio (GstAlsa *this) { g_assert (this != NULL); g_assert (this->handle == NULL); GST_INFO (GST_CAT_PLUGIN_INFO, "Opening alsa device \"%s\" for %s...\n", this->device, this->stream == SND_PCM_STREAM_PLAYBACK ? "playback" : "capture"); ERROR_CHECK (snd_output_stdio_attach (&this->out, stdout, 0), "error opening log output: %s"); /* blocking i/o */ ERROR_CHECK (snd_pcm_open (&this->handle, this->device, this->stream, 0), "error opening pcm device %s: %s\n", this->device); GST_FLAG_SET (this, GST_ALSA_OPEN); return TRUE; } /* you must set all hw parameters at once - thx ALSA for not documenting this */ static gboolean gst_alsa_set_hw_params (GstAlsa *this) { snd_pcm_hw_params_t *hw_params; snd_pcm_access_mask_t *mask; snd_pcm_uframes_t size_min, size_max; unsigned int count_min, count_max; /* whether to use default values when setting params */ gboolean def = (this->format == SND_PCM_FORMAT_UNKNOWN); g_return_val_if_fail (this != NULL, FALSE); g_return_val_if_fail (this->handle != NULL, FALSE); GST_INFO (GST_CAT_PLUGIN_INFO, "Preparing channel: %s %dHz, %d channels\n", snd_pcm_format_name (this->format), this->rate, this->channels); snd_pcm_hw_params_alloca (&hw_params); ERROR_CHECK (snd_pcm_hw_params_any (this->handle, hw_params), "Broken configuration for this PCM: %s"); ERROR_CHECK (snd_pcm_hw_params_set_periods_integer (this->handle, hw_params), "cannot restrict period size to integral value: %s"); mask = alloca (snd_pcm_access_mask_sizeof ()); snd_pcm_access_mask_none (mask); if (GST_ELEMENT (this)->numpads == 1) { snd_pcm_access_mask_set (mask, SND_PCM_ACCESS_MMAP_INTERLEAVED); } else { snd_pcm_access_mask_set (mask, SND_PCM_ACCESS_MMAP_NONINTERLEAVED); } ERROR_CHECK (snd_pcm_hw_params_set_access_mask (this->handle, hw_params, mask), "The Gstreamer ALSA plugin does not support your hardware. Error: %s"); ERROR_CHECK (snd_pcm_hw_params_set_format (this->handle, hw_params, def ? SND_PCM_FORMAT_S16 : this->format), "Sample format (%s) not available: %s", snd_pcm_format_name (def ? SND_PCM_FORMAT_S16 : this->format)); ERROR_CHECK (snd_pcm_hw_params_set_channels (this->handle, hw_params, this->channels), "Channels count (%d) not available: %s", this->channels); ERROR_CHECK (snd_pcm_hw_params_set_rate (this->handle, hw_params, def ? 44100 : this->rate, 0), "error setting rate (%d): %s", def ? 44100 : this->rate); if (snd_pcm_hw_params_get_period_size_min (hw_params, &size_min, 0) < 0) size_min = this->period_size; if (snd_pcm_hw_params_get_period_size_max (hw_params, &size_max, 0) < 0) size_max = this->period_size; g_assert (size_max >= size_min); if (size_min > this->period_size) this->period_size = size_min; if (size_max < this->period_size) this->period_size = size_max; ERROR_CHECK (snd_pcm_hw_params_set_period_size (this->handle, hw_params, this->period_size, 0), "error setting period size to %u frames: %s", (guint) this->period_size); if (snd_pcm_hw_params_get_periods_min (hw_params, &count_min, 0) < 0) count_min = this->period_count; if (snd_pcm_hw_params_get_periods_max (hw_params, &count_max, 0) < 0) count_max = this->period_count; g_assert (count_max >= count_min); if (count_min > this->period_count) this->period_count = count_min; if (count_max < this->period_count) this->period_count = count_max; ERROR_CHECK (snd_pcm_hw_params_set_buffer_size (this->handle, hw_params, this->period_size * this->period_count), "error setting buffer size to %u: %s", (guint) (this->period_size * this->period_count)); ERROR_CHECK (snd_pcm_hw_params (this->handle, hw_params), "Could not set hardware parameters: %s"); return TRUE; } static gboolean gst_alsa_set_sw_params (GstAlsa *this) { snd_pcm_sw_params_t *sw_params; snd_pcm_sw_params_alloca (&sw_params); ERROR_CHECK (snd_pcm_sw_params_current (this->handle, sw_params), "Could not get current software parameters: %s"); ERROR_CHECK (snd_pcm_sw_params_set_silence_size (this->handle, sw_params, 0), "could not set silence size: %s"); ERROR_CHECK (snd_pcm_sw_params_set_silence_threshold (this->handle, sw_params, 0), "could not set silence threshold: %s"); ERROR_CHECK (snd_pcm_sw_params_set_avail_min (this->handle, sw_params, this->period_size), "could not set avail min: %s"); ERROR_CHECK (snd_pcm_sw_params_set_start_threshold (this->handle, sw_params, 1), "could not set start mode: %s"); ERROR_CHECK (snd_pcm_sw_params_set_stop_threshold (this->handle, sw_params, this->period_size * this->period_count), "could not set stop mode: %s"); ERROR_CHECK (snd_pcm_sw_params_set_xfer_align(this->handle, sw_params, 1), "Unable to set transfer align for playback: %s"); ERROR_CHECK (snd_pcm_sw_params (this->handle, sw_params), "could not set sw_params: %s"); return TRUE; } static gboolean gst_alsa_start_audio (GstAlsa *this) { if (!gst_alsa_set_hw_params (this)) return FALSE; if (!gst_alsa_set_sw_params (this)) return FALSE; GST_FLAG_SET (this, GST_ALSA_RUNNING); return TRUE; } static gboolean gst_alsa_drain_audio (GstAlsa *this) { g_assert (this != NULL); g_return_val_if_fail (this != NULL, FALSE); g_return_val_if_fail (this->handle != NULL, FALSE); GST_DEBUG (GST_CAT_PLUGIN_INFO, "stopping alsa"); if (this->stream == SND_PCM_STREAM_PLAYBACK) { ERROR_CHECK (snd_pcm_drain (this->handle), "couldn't stop and drain buffer: %s"); } GST_FLAG_UNSET (this, GST_ALSA_RUNNING); return TRUE; } static gboolean gst_alsa_stop_audio (GstAlsa *this) { g_assert (this != NULL); g_return_val_if_fail (this != NULL, FALSE); g_return_val_if_fail (this->handle != NULL, FALSE); GST_DEBUG (GST_CAT_PLUGIN_INFO, "stopping alsa, skipping pending frames"); if (this->stream == SND_PCM_STREAM_PLAYBACK) { ERROR_CHECK (snd_pcm_drop (this->handle), "couldn't stop (dropping frames): %s"); } GST_FLAG_UNSET (this, GST_ALSA_RUNNING); return TRUE; } static gboolean gst_alsa_close_audio (GstAlsa *this) { g_return_val_if_fail (this != NULL, FALSE); g_return_val_if_fail (this->handle != NULL, FALSE); ERROR_CHECK (snd_pcm_close (this->handle), "Error closing device: %s"); this->handle = NULL; GST_FLAG_UNSET (this, GST_ALSA_OPEN); return TRUE; } /*** GSTREAMER PLUGIN *********************************************************/ static gboolean plugin_init (GModule * module, GstPlugin * plugin) { GstElementFactory *factory; if (!gst_library_load ("gstbytestream")) return FALSE; /* FIXME: doesn't work */ if (0) { factory = gst_element_factory_new ("alsasrc", GST_TYPE_ALSA_SRC, &gst_alsa_src_details); g_return_val_if_fail (factory != NULL, FALSE); gst_element_factory_add_pad_template (factory, gst_alsa_src_pad_factory ()); gst_element_factory_add_pad_template (factory, gst_alsa_src_request_pad_factory ()); gst_plugin_add_feature (plugin, GST_PLUGIN_FEATURE (factory)); } factory = gst_element_factory_new ("alsasink", GST_TYPE_ALSA_SINK, &gst_alsa_sink_details); g_return_val_if_fail (factory != NULL, FALSE); gst_element_factory_add_pad_template (factory, gst_alsa_sink_pad_factory ()); gst_element_factory_add_pad_template (factory, gst_alsa_sink_request_pad_factory ()); gst_plugin_add_feature (plugin, GST_PLUGIN_FEATURE (factory)); gst_plugin_set_longname (plugin, "ALSA plugin library"); return TRUE; } GstPluginDesc plugin_desc = { GST_VERSION_MAJOR, GST_VERSION_MINOR, "alsa", plugin_init };