gstreamer/ext/alsa/gstalsasink.c
Ronald S. Bultje 618e9b77f9 ext/alsa/gstalsasink.c: Fix for negotiation order problem. This would show when the
Original commit message from CVS:
* ext/alsa/gstalsasink.c: (gst_alsa_sink_loop):
Fix for negotiation order problem. This would show when the
ALSA loopfuction was called before any other function. ALSA
wouldn't do anything because we're not negotiated yet, leading
to an infinite loop. Showed in e.g. Rhythmbox. Fixes #158006.
2004-11-12 10:07:46 +00:00

590 lines
19 KiB
C

/*
* Copyright (C) 2001 CodeFactory AB
* Copyright (C) 2001 Thomas Nyberg <thomas@codefactory.se>
* Copyright (C) 2001-2002 Andy Wingo <apwingo@eos.ncsu.edu>
* Copyright (C) 2003 Benjamin Otte <in7y118@public.uni-hamburg.de>
*
* 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 <thomas@codefactory.se>, "
"Andy Wingo <apwingo@eos.ncsu.edu>, "
"Benjamin Otte <in7y118@public.uni-hamburg.de>");
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 (gst_element_get_clock (element) == 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;
}
}
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;
}