/* * 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 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., 675 Mass Ave, Cambridge, MA 02139, USA. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "gstalsasink.h" #include "gstalsaclock.h" /* elementfactory information */ static GstElementDetails gst_alsa_sink_details = GST_ELEMENT_DETAILS ("Alsa Sink", "Sink/Audio", "Output to a sound card via ALSA", "Thomas Nyberg , " "Andy Wingo , " "Benjamin Otte "); static GstPadTemplate *gst_alsa_sink_pad_factory (void); static GstPadTemplate *gst_alsa_sink_request_pad_factory (void); static void gst_alsa_sink_base_init (gpointer g_class); static void gst_alsa_sink_class_init (gpointer g_klass, gpointer class_data); static void gst_alsa_sink_init (GstAlsaSink * this); static inline void gst_alsa_sink_flush_one_pad (GstAlsaSink * sink, gint i); static void gst_alsa_sink_flush_pads (GstAlsaSink * sink); static int gst_alsa_sink_mmap (GstAlsa * this, snd_pcm_sframes_t * avail); static int gst_alsa_sink_write (GstAlsa * this, snd_pcm_sframes_t * avail); static void gst_alsa_sink_loop (GstElement * element); static gboolean gst_alsa_sink_check_event (GstAlsaSink * sink, gint pad_nr); static GstElementStateReturn gst_alsa_sink_change_state (GstElement * element); static GstClockTime gst_alsa_sink_get_time (GstAlsa * this); static GstAlsa *sink_parent_class = NULL; 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)); 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)); return template; } GType gst_alsa_sink_get_type (void) { static GType alsa_sink_type = 0; if (!alsa_sink_type) { static const GTypeInfo alsa_sink_info = { sizeof (GstAlsaSinkClass), gst_alsa_sink_base_init, NULL, gst_alsa_sink_class_init, NULL, NULL, sizeof (GstAlsaSink), 0, (GInstanceInitFunc) gst_alsa_sink_init, }; alsa_sink_type = g_type_register_static (GST_TYPE_ALSA_MIXER, "GstAlsaSink", &alsa_sink_info, 0); } return alsa_sink_type; } static void gst_alsa_sink_base_init (gpointer g_class) { GstElementClass *element_class = GST_ELEMENT_CLASS (g_class); gst_element_class_add_pad_template (element_class, gst_alsa_sink_pad_factory ()); gst_element_class_add_pad_template (element_class, gst_alsa_sink_request_pad_factory ()); gst_element_class_set_details (element_class, &gst_alsa_sink_details); } static void gst_alsa_sink_class_init (gpointer g_class, gpointer class_data) { GObjectClass *object_class; GstElementClass *element_class; GstAlsaClass *alsa_class; GstAlsaSinkClass *klass; klass = (GstAlsaSinkClass *) g_class; object_class = (GObjectClass *) klass; element_class = (GstElementClass *) klass; alsa_class = (GstAlsaClass *) klass; if (sink_parent_class == NULL) sink_parent_class = g_type_class_ref (GST_TYPE_ALSA_MIXER); alsa_class->stream = SND_PCM_STREAM_PLAYBACK; alsa_class->transmit_mmap = gst_alsa_sink_mmap; alsa_class->transmit_rw = gst_alsa_sink_write; element_class->change_state = gst_alsa_sink_change_state; } static void gst_alsa_sink_init (GstAlsaSink * sink) { GstAlsa *this = GST_ALSA (sink); this->pad[0] = gst_pad_new_from_template (gst_alsa_sink_pad_factory (), "sink"); gst_pad_set_link_function (this->pad[0], gst_alsa_link); gst_pad_set_getcaps_function (this->pad[0], gst_alsa_get_caps); gst_pad_set_fixate_function (this->pad[0], gst_alsa_fixate); gst_element_add_pad (GST_ELEMENT (this), this->pad[0]); this->clock = gst_alsa_clock_new ("alsasinkclock", gst_alsa_sink_get_time, this); /* we hold a ref to our clock until we're disposed */ gst_object_ref (GST_OBJECT (this->clock)); gst_object_sink (GST_OBJECT (this->clock)); gst_element_set_loop_function (GST_ELEMENT (this), gst_alsa_sink_loop); } static inline void gst_alsa_sink_flush_one_pad (GstAlsaSink * sink, gint i) { GST_DEBUG_OBJECT (sink, "flushing pad %d", i); switch (sink->behaviour[i]) { case 0: if (sink->gst_data[i]) { GST_DEBUG_OBJECT (sink, "unreffing gst data %p", sink->gst_data[i]); gst_data_unref (GST_DATA (sink->gst_data[i])); } sink->gst_data[i] = NULL; sink->buf_data[i] = NULL; sink->behaviour[i] = 0; sink->size[i] = 0; break; case 1: g_free (sink->buf_data[i]); sink->buf_data[i] = NULL; sink->behaviour[i] = 0; sink->size[i] = 0; break; default: g_assert_not_reached (); } } static void gst_alsa_sink_flush_pads (GstAlsaSink * sink) { gint i; for (i = 0; i < GST_ELEMENT (sink)->numpads; i++) { /* flush twice to unref buffer when behaviour == 1 */ gst_alsa_sink_flush_one_pad (sink, i); gst_alsa_sink_flush_one_pad (sink, i); } } /* TRUE, if everything should continue */ static gboolean gst_alsa_sink_check_event (GstAlsaSink * sink, gint pad_nr) { gboolean cont = TRUE; GstEvent *event; GstAlsa *this = GST_ALSA (sink); /* we get the event from our internal buffer and clear the internal one */ event = GST_EVENT (sink->gst_data[pad_nr]); sink->gst_data[pad_nr] = 0; GST_LOG_OBJECT (sink, "checking event %p of type %d on sink pad %d", event, GST_EVENT_TYPE (event), pad_nr); if (event) { switch (GST_EVENT_TYPE (event)) { case GST_EVENT_EOS: gst_alsa_set_eos (this); cont = FALSE; break; case GST_EVENT_INTERRUPT: cont = FALSE; break; case GST_EVENT_DISCONTINUOUS: { GstClockTime value, delay; /* only the first pad may seek */ if (pad_nr != 0) { break; } delay = (this->format == NULL) ? 0 : GST_SECOND * this->played / this->format->rate - gst_alsa_sink_get_time (this); if (gst_event_discont_get_value (event, GST_FORMAT_TIME, &value)) { gst_element_set_time_delay (GST_ELEMENT (this), value, MIN (value, delay)); } else if (this->format && (gst_event_discont_get_value (event, GST_FORMAT_DEFAULT, &value))) { value = gst_alsa_samples_to_timestamp (this, value); gst_element_set_time_delay (GST_ELEMENT (this), value, MIN (value, delay)); } else if (this->format && (gst_event_discont_get_value (event, GST_FORMAT_BYTES, &value))) { value = gst_alsa_bytes_to_timestamp (this, value); gst_element_set_time_delay (GST_ELEMENT (this), value, MIN (value, delay)); } else { GST_WARNING_OBJECT (this, "couldn't extract time from discont event. Bad things might happen!"); } break; } default: GST_INFO_OBJECT (this, "got an unknown event (Type: %d)", GST_EVENT_TYPE (event)); break; } GST_LOG_OBJECT (sink, "unreffing event %p of type %d with refcount %d", event, GST_EVENT_TYPE (event), GST_DATA_REFCOUNT (event)); gst_event_unref (event); sink->gst_data[pad_nr] = NULL; } else { /* the element at the top of the chain did not emit an event. */ g_assert_not_reached (); } return cont; } static int gst_alsa_sink_mmap (GstAlsa * this, snd_pcm_sframes_t * avail) { snd_pcm_uframes_t offset; const snd_pcm_channel_area_t *dst; snd_pcm_channel_area_t *src; GstAlsaSink *sink = GST_ALSA_SINK (this); int i; int err = -1; int width = snd_pcm_format_physical_width (this->format->format); /* areas points to the memory areas that belong to gstreamer. */ src = g_malloc0 (this->format->channels * sizeof (snd_pcm_channel_area_t)); if (((GstElement *) this)->numpads == 1) { /* interleaved */ for (i = 0; i < this->format->channels; i++) { src[i].addr = sink->buf_data[0]; src[i].first = i * width; src[i].step = this->format->channels * width; } } else { /* noninterleaved */ for (i = 0; i < this->format->channels; i++) { src[i].addr = sink->buf_data[i]; src[i].first = 0; src[i].step = width; } } if ((err = snd_pcm_mmap_begin (this->handle, &dst, &offset, avail)) < 0) { GST_ERROR_OBJECT (this, "mmap failed: %s", snd_strerror (err)); goto out; } if ((err = snd_pcm_areas_copy (dst, offset, src, 0, this->format->channels, *avail, this->format->format)) < 0) { snd_pcm_mmap_commit (this->handle, offset, 0); GST_ERROR_OBJECT (this, "data copy failed: %s", snd_strerror (err)); goto out; } if ((err = snd_pcm_mmap_commit (this->handle, offset, *avail)) < 0) { if (err == -EPIPE) { gst_alsa_xrun_recovery (GST_ALSA (this)); } else { GST_ERROR_OBJECT (this, "mmap commit failed: %s", snd_strerror (err)); } goto out; } out: g_free (src); return err; } static int gst_alsa_sink_write (GstAlsa * this, snd_pcm_sframes_t * avail) { GstAlsaSink *sink = GST_ALSA_SINK (this); void *channels[this->format->channels]; int err, i; if (((GstElement *) this)->numpads == 1) { /* interleaved */ err = snd_pcm_writei (this->handle, sink->buf_data[0], *avail); } else { /* noninterleaved */ for (i = 0; i < this->format->channels; i++) { channels[i] = sink->buf_data[i]; } err = snd_pcm_writen (this->handle, channels, *avail); } /* error handling */ if (err < 0) { if (err == -EPIPE) { gst_alsa_xrun_recovery (this); return 0; } GST_ERROR_OBJECT (this, "error on data access: %s", snd_strerror (err)); } return err; } static void gst_alsa_sink_loop (GstElement * element) { snd_pcm_sframes_t avail, avail2, copied, sample_diff, max_discont; snd_pcm_uframes_t samplestamp, expected; gint i; guint bytes; /* per channel */ GstAlsa *this = GST_ALSA (element); GstAlsaSink *sink = GST_ALSA_SINK (element); g_return_if_fail (sink != NULL); sink_restart: avail = gst_alsa_update_avail (this); if (avail == -EPIPE) goto sink_restart; if (avail < 0) return; if (avail > 0 || (avail == 0 && !this->format)) { bytes = G_MAXINT; for (i = 0; i < element->numpads; i++) { GstBuffer *buf; g_assert (this->pad[i] != NULL); while (sink->size[i] == 0) { if (!sink->gst_data[i]) { sink->gst_data[i] = gst_pad_pull (this->pad[i]); GST_LOG_OBJECT (sink, "pulled data %p from pad %d", sink->gst_data[i], i); } if (GST_IS_EVENT (sink->gst_data[i])) { GST_LOG_OBJECT (sink, "pulled data %p is an event, checking", sink->gst_data[i]); if (gst_alsa_sink_check_event (sink, i)) continue; return; } /* it's a buffer */ g_return_if_fail (GST_IS_BUFFER (sink->gst_data[i])); buf = GST_BUFFER (sink->gst_data[i]); /* check if caps nego failed somewhere */ if (this->format == NULL) { GST_ELEMENT_ERROR (this, CORE, NEGOTIATION, (NULL), ("ALSA format not negotiated")); } samplestamp = gst_alsa_timestamp_to_samples (this, GST_BUFFER_TIMESTAMP (buf)); max_discont = gst_alsa_timestamp_to_samples (this, this->max_discont); /* optimization: check if we're using our own clock * This optimization is important because if we're using our own clock * gst_element_get_time calls snd_pcm_delay and the following code * assumes that both calls return the same value. However they can be * wildly different, since snd_pcm_delay goes deep into the kernel. */ if (this->ext_clock == GST_CLOCK (this->clock)) { /* FIXME: this is ugly because of the variables it uses but I * don't know a better way to get this info */ if (element->base_time > this->clock->start_time) { expected = this->played - gst_alsa_timestamp_to_samples (this, element->base_time - this->clock->start_time); } else { expected = this->played + gst_alsa_timestamp_to_samples (this, this->clock->start_time - element->base_time); } } else { if (snd_pcm_delay (this->handle, &sample_diff) != 0) { sample_diff = 0; } expected = gst_alsa_timestamp_to_samples (this, gst_element_get_time (GST_ELEMENT (this))) + sample_diff; /* actual diff = buffer samplestamp - played - to_play */ } sample_diff = samplestamp - expected; if ((!GST_BUFFER_TIMESTAMP_IS_VALID (buf)) || (-max_discont <= sample_diff && sample_diff <= max_discont)) { /* difference between expected and current is < GST_ALSA_DEVIATION */ no_difference: sink->size[i] = GST_BUFFER_SIZE (buf); sink->buf_data[i] = GST_BUFFER_DATA (buf); sink->behaviour[i] = 0; } else if (sample_diff > 0) { /* there are empty samples in front of us, fill them with silence */ int samples = MIN (bytes, sample_diff) * (element->numpads == 1 ? this->format->channels : 1); int size = samples * snd_pcm_format_physical_width (this->format->format) / 8; GST_INFO_OBJECT (this, "Allocating %d bytes (%ld samples) now to resync: sample %lu expected, but got %ld", size, MIN (bytes, sample_diff), expected, samplestamp); sink->buf_data[i] = g_try_malloc (size); if (!sink->buf_data[i]) { GST_WARNING_OBJECT (this, "error allocating %d bytes, buffers unsynced now.", size); goto no_difference; } sink->size[i] = size; if (0 != snd_pcm_format_set_silence (this->format->format, sink->buf_data[i], samples)) { GST_WARNING_OBJECT (this, "error silencing buffer, enjoy the noise."); } sink->behaviour[i] = 1; } else if (gst_alsa_samples_to_bytes (this, -sample_diff) >= GST_BUFFER_SIZE (buf)) { GST_INFO_OBJECT (this, "Skipping %lu samples to resync (complete buffer): sample %lu expected, but got %ld", gst_alsa_bytes_to_samples (this, GST_BUFFER_SIZE (buf)), expected, samplestamp); /* this buffer is way behind */ gst_buffer_unref (buf); sink->gst_data[i] = NULL; continue; } else if (sample_diff < 0) { gint difference = gst_alsa_samples_to_bytes (this, -sample_diff); GST_INFO_OBJECT (this, "Skipping %lu samples to resync: sample %lu expected, but got %ld", (gulong) - sample_diff, expected, samplestamp); /* this buffer is only a bit behind */ sink->size[i] = GST_BUFFER_SIZE (buf) - difference; sink->buf_data[i] = GST_BUFFER_DATA (buf) + difference; sink->behaviour[i] = 0; } else { g_assert_not_reached (); } } bytes = MIN (bytes, sink->size[i]); } /* check how many bytes we still have in all our bytestreams */ /* initialize this value to a somewhat sane state, we might alloc * this much data below (which would be a bug, but who knows)... */ bytes = MIN (bytes, this->period_size * this->period_count * element->numpads * 8); /* must be > max sample size in bytes */ /* Not enough space. We grab data nonetheless and sleep afterwards */ if (avail < this->period_size) avail = this->period_size; avail = MIN (avail, gst_alsa_bytes_to_samples (this, bytes)); /* wait until the hw buffer has enough space */ while (gst_element_get_state (element) == GST_STATE_PLAYING && (avail2 = gst_alsa_update_avail (this)) < avail) { if (avail2 <= -EPIPE) goto sink_restart; if (avail2 < 0) return; if (avail2 < avail && snd_pcm_state (this->handle) != SND_PCM_STATE_RUNNING) if (!gst_alsa_start (this)) return; if (gst_alsa_pcm_wait (this) == FALSE) return; } /* FIXME: lotsa stuff can have happened while fetching data. * Do we need to check something? */ /* put this data into alsa */ if ((copied = this->transmit (this, &avail)) < 0) return; /* update our clock */ this->played += copied; /* flush the data */ bytes = gst_alsa_samples_to_bytes (this, copied); for (i = 0; i < element->numpads; i++) { if ((sink->size[i] -= bytes) == 0) { gst_alsa_sink_flush_one_pad (sink, i); continue; } g_assert (sink->size[i] > 0); if (sink->behaviour[i] != 1) sink->buf_data[i] += bytes; } } else if (avail == 0 && gst_element_get_state (element) == GST_STATE_PLAYING) { if (gst_alsa_pcm_wait (this) == FALSE) return; } if (snd_pcm_state (this->handle) != SND_PCM_STATE_RUNNING && snd_pcm_avail_update (this->handle) == 0) { gst_alsa_start (this); } } static GstElementStateReturn gst_alsa_sink_change_state (GstElement * element) { GstAlsaSink *sink; g_return_val_if_fail (element != NULL, FALSE); sink = GST_ALSA_SINK (element); switch (GST_STATE_TRANSITION (element)) { case GST_STATE_NULL_TO_READY: case GST_STATE_READY_TO_PAUSED: case GST_STATE_PAUSED_TO_PLAYING: case GST_STATE_PLAYING_TO_PAUSED: break; case GST_STATE_PAUSED_TO_READY: gst_alsa_sink_flush_pads (sink); break; case GST_STATE_READY_TO_NULL: break; default: break; } if (GST_ELEMENT_CLASS (sink_parent_class)->change_state) return GST_ELEMENT_CLASS (sink_parent_class)->change_state (element); return GST_STATE_SUCCESS; } static GstClockTime gst_alsa_sink_get_time (GstAlsa * this) { snd_pcm_sframes_t delay; if (!this->format) return 0; if (!GST_FLAG_IS_SET (this, GST_ALSA_RUNNING)) { delay = 0; } else if (snd_pcm_delay (this->handle, &delay) != 0) { delay = 0; } if (this->played <= delay) { return 0; } return GST_SECOND * (this->played - delay) / this->format->rate; }