/* * 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 GstBufferPool *gst_alsa_src_get_buffer_pool (GstPad *pad); static GstElementStateReturn gst_alsa_change_state (GstElement *element); /* audio processing functions */ static int gst_alsa_do_mmap (GstAlsa *this, guint numpads, snd_pcm_sframes_t *avail); static void gst_alsa_sink_loop (GstElement *element); static void gst_alsa_src_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 are only settable on srcs */ 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_READWRITE)); 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_READWRITE)); 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_READWRITE)); 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 */ gst_element_set_loop_function (GST_ELEMENT (this), gst_alsa_src_loop); this->pads[0].pad = gst_pad_new_from_template (gst_alsa_src_pad_factory (), "src"); gst_pad_set_bufferpool_function(this->pads[0].pad, gst_alsa_src_get_buffer_pool); /* set the rate to a sensible value. we can't have gobject construct this manually since it only really makes sense on src elements. the rate can be changed later through the gobject set property function. */ this->rate = 44100; } 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: /* setting this property only makes sense on sources */ if (G_OBJECT_TYPE (this) == GST_TYPE_ALSA_SRC) this->format = g_value_get_enum (value); break; case ARG_CHANNELS: /* setting this property only makes sense on sources */ if (G_OBJECT_TYPE (this) == GST_TYPE_ALSA_SRC) this->channels = g_value_get_int (value); break; case ARG_RATE: /* setting this property only makes sense on sources */ if (G_OBJECT_TYPE (this) == GST_TYPE_ALSA_SRC) 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 <= element->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); else gst_pad_set_bufferpool_function(this->pads[channel].pad, gst_alsa_src_get_buffer_pool); 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 formats */ if (!gst_caps_get_string (caps, "format", &format_name)) return SND_PCM_FORMAT_UNKNOWN; if (strncmp (format_name, "int", 3) == 0) { gboolean sign; gint width, depth, endianness, law; /* extract the needed information from the caps */ if (!gst_caps_get (caps, "width", &width, "depth", &depth, "law", &law, "signed", &sign, NULL)) return SND_PCM_FORMAT_UNKNOWN; /* extract endianness if needed */ if (width > 8) { if (!gst_caps_get (caps, "endianness", &endianness, NULL)) return SND_PCM_FORMAT_UNKNOWN; } else { endianness = G_BYTE_ORDER; } /* 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: if (width == 8 && depth == 8 && sign == FALSE) { return SND_PCM_FORMAT_MU_LAW; } else { return SND_PCM_FORMAT_UNKNOWN; } case 2: if (width == 8 && depth == 8 && sign == FALSE) { return SND_PCM_FORMAT_A_LAW; } else { return SND_PCM_FORMAT_UNKNOWN; } default: return SND_PCM_FORMAT_UNKNOWN; } } else if (strncmp (format_name, "float", 5) == 0) { gchar *layout; gfloat intercept, slope; /* get layout */ if (!gst_caps_get (caps, "layout", &layout, "intercept", &intercept, "slope", &slope, NULL)) return SND_PCM_FORMAT_UNKNOWN; if (intercept != 0.0f || slope != 1.0f) { return SND_PCM_FORMAT_UNKNOWN; } /* match layout to format wrt to endianness */ if (strncmp (layout, "gfloat", 6) == 0) { if (G_BYTE_ORDER == G_LITTLE_ENDIAN) return SND_PCM_FORMAT_FLOAT_LE; if (G_BYTE_ORDER == G_BIG_ENDIAN) return SND_PCM_FORMAT_FLOAT_BE; return SND_PCM_FORMAT_FLOAT; } else if (strncmp (layout, "gdouble", 7) == 0) { if (G_BYTE_ORDER == G_LITTLE_ENDIAN) return SND_PCM_FORMAT_FLOAT64_LE; if (G_BYTE_ORDER == G_BIG_ENDIAN) return SND_PCM_FORMAT_FLOAT64_BE; 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) { if (format == SND_PCM_FORMAT_A_LAW) { return gst_props_new ("format", GST_PROPS_STRING ("int"), "law", GST_PROPS_INT(2), "width", GST_PROPS_INT(8), "depth", GST_PROPS_INT(8), "signed", GST_PROPS_BOOLEAN (FALSE), NULL); } else if (format == SND_PCM_FORMAT_MU_LAW) { return gst_props_new ("format", GST_PROPS_STRING ("int"), "law", GST_PROPS_INT(1), "width", GST_PROPS_INT(8), "depth", GST_PROPS_INT(8), "signed", GST_PROPS_BOOLEAN (FALSE), NULL); } else if (snd_pcm_format_linear (format)) { /* int */ 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)), "law", GST_PROPS_INT(0), "signed", GST_PROPS_BOOLEAN (snd_pcm_format_signed (format) == 1 ? TRUE : FALSE), NULL); /* endianness */ if (snd_pcm_format_physical_width (format) > 8) { 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: g_warning("ALSA: Unknown byte order in sound driver. Continuing by assuming system byte order."); gst_props_add_entry (props, gst_props_entry_new ("endianness", GST_PROPS_INT (G_BYTE_ORDER))); 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"), "intercept", GST_PROPS_FLOAT (0), "slope", GST_PROPS_FLOAT (1), 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; format = gst_alsa_get_format (caps); GST_DEBUG (GST_CAT_CAPS, "found format %s\n", snd_pcm_format_name (format)); /* FIXME: allow changing the format here, even by retrying caps on other pads */ if (format == SND_PCM_FORMAT_UNKNOWN || (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 GstBufferPool * gst_alsa_src_get_buffer_pool (GstPad *pad) { int width, bytes_per_frame; GstAlsa *this = GST_ALSA (gst_pad_get_parent (pad)); width = snd_pcm_format_physical_width (this->format); bytes_per_frame = ( width / 8 ) * (GST_ELEMENT (this)->numpads == 1 ? this->channels : 1); /* FIXME : is this right ? constant size buffers are probably a good thing, but what if the size changes (e.g. during xrun autorecovery) ? */ return gst_buffer_pool_get_default (this->period_size * bytes_per_frame, this->period_count); } 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_RUNNING) == FALSE) if (!gst_alsa_start_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 int gst_alsa_do_mmap (GstAlsa *this, guint numpads, snd_pcm_sframes_t *avail) { snd_pcm_uframes_t offset; snd_pcm_channel_area_t *dst, *src, *areas; int i, err, width = snd_pcm_format_physical_width (this->format); /* areas points to the memory areas that belong to gstreamer. */ if (G_OBJECT_TYPE (this) == GST_TYPE_ALSA_SRC) { areas = dst = calloc(this->channels, sizeof(snd_pcm_channel_area_t)); } else { areas = src = calloc(this->channels, sizeof(snd_pcm_channel_area_t)); } if (numpads == 1) { /* interleaved */ for (i = 0; i < this->channels; i++) { areas[i].addr = this->pads[0].data; areas[i].first = i * width; areas[i].step = this->channels * width; } } else { /* noninterleaved */ for (i = 0; i < this->channels; i++) { areas[i].addr = this->pads[i].data; areas[i].first = 0; areas[i].step = width; } } /* might want to try to do some pointer sneakery in the first if block to get rid of this second if block ... but right now i'm too sleepy */ if (G_OBJECT_TYPE (this) == GST_TYPE_ALSA_SRC) { if ((err = snd_pcm_mmap_begin (this->handle, (const snd_pcm_channel_area_t **) &src, &offset, avail)) < 0) { g_warning ("gstalsa: mmap failed: %s", snd_strerror (err)); return -1; } } else { 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 -1; } } 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 -1; } if ((err = snd_pcm_mmap_commit (this->handle, offset, *avail)) < 0) { g_warning ("gstalsa: mmap commit failed: %s", snd_strerror (err)); return -1; } return err; } inline static snd_pcm_sframes_t gst_alsa_update_avail (GstAlsa *this) { snd_pcm_sframes_t avail = snd_pcm_avail_update (this->handle); if (avail < 0) { if (avail == -EPIPE) { gst_alsa_xrun_recovery (this); return -EPIPE; } else { g_warning ("unknown ALSA avail_update return value (%d)", (int) avail); return -1; } } return avail; } inline static gint gst_alsa_pcm_wait (GstAlsa *this) { if (snd_pcm_state (this->handle) == SND_PCM_STATE_RUNNING) { if (snd_pcm_wait (this->handle, 1000) < 0) { if (errno == EINTR) { /* 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 (GST_ELEMENT (this))) return -1; } g_warning ("error waiting for alsa pcm: (%d: %s)", errno, strerror (errno)); return -1; } } return 0; } static void gst_alsa_sink_loop (GstElement *element) { snd_pcm_sframes_t avail, copied; 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"); } } sink_restart: while (1) { avail = gst_alsa_update_avail (this); if (avail == -EPIPE) goto sink_restart; if (avail == -1) break; if (avail > 0) { int width = snd_pcm_format_physical_width (this->format); /* check how many bytes we still have in all our bytestreams */ bytes = avail * ( width / 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) break; 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 */ avail = bytes / (width / 8 ) / (element->numpads == 1 ? this->channels : 1); if ((copied = gst_alsa_do_mmap (this, element->numpads, &avail)) < 0) break; /* flush the data */ bytes = copied * ( 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 (gst_alsa_pcm_wait (this) < 0) break; } } static void gst_alsa_src_loop (GstElement *element) { snd_pcm_sframes_t avail, copied; GstBufferPool *pool = NULL; GstBuffer *buf; GstCaps *caps; gint i; GstAlsaPad *pad; GstAlsa *this = GST_ALSA (element); g_return_if_fail (this != NULL); static gboolean caps_set = FALSE; /* set the caps on all pads */ if (!caps_set) { GST_DEBUG (GST_CAT_NEGOTIATION, "starting caps negotiation"); caps = gst_alsa_caps (this->format, this->rate, this->channels); for (i = 0; i < element->numpads; i++) { if (gst_pad_try_set_caps (this->pads[i].pad, caps) <= 0) { GST_DEBUG (GST_CAT_NEGOTIATION, "setting caps (%p) in alsasrc (%p) on pad %d failed", caps, this, i); return; } } caps_set = TRUE; } src_restart: while (1) { avail = gst_alsa_update_avail (this); if (avail == -EPIPE) goto src_restart; if (avail == -1) break; if (avail > 0) { int width = snd_pcm_format_physical_width (this->format); int bytes_per_frame = ( width / 8 ) * (element->numpads == 1 ? this->channels : 1); if ((copied = gst_alsa_do_mmap (this, element->numpads, &avail)) < 0) break; /* we get the buffer pool once per go round */ if (! pool) pool = gst_alsa_src_get_buffer_pool (this->pads[0].pad); /* push the data to gstreamer if it's big enough to fill up a buffer. */ for (i = 0; i < element->numpads; i++) { pad = &this->pads[i]; pad->offset += MIN (copied, this->period_size - pad->offset); if (pad->offset >= this->period_size) { g_assert (pad->offset <= this->period_size); buf = gst_buffer_new_from_pool (pool, 0, 0); GST_BUFFER_DATA (buf) = pad->data; GST_BUFFER_SIZE (buf) = this->period_size * bytes_per_frame; GST_BUFFER_MAXSIZE (buf) = this->period_size * bytes_per_frame; gst_pad_push (pad->pad, buf); pad->data = NULL; pad->offset = 0; } } pool = NULL; /* 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 capture"); snd_pcm_start(this->handle); } } /* wait */ if (gst_alsa_pcm_wait (this) < 0) break; } } 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 we're allowed to recover, ... */ if (this->autorecover) { /* ... then increase the period size or buffer size / period count to prevent further xruns (at the cost of increased latency and memory usage). */ 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"); } } /* 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; 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 };