
/*
 * alsa.c -- The Advanced Linux Sound System backend for Speech Dispatcher
 *
 * Copyright (C) 2005,2006 Brailcom, o.p.s.
 * Copyright (C) 2019-2024 Samuel Thibault <samuel.thibault@ens-lyon.org>
 *
 * This is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1, or (at your option) any later
 * version.
 *
 * This software 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 Lesser General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * $Id: alsa.c,v 1.30 2008-10-15 17:27:32 hanke Exp $
 */

/* NOTE: This module uses the non-blocking write() / poll() approach to
    alsa-lib functions.*/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <sys/time.h>
#include <time.h>
#include <pthread.h>
#include <glib.h>

#include <alsa/asoundlib.h>
#include <alsa/pcm.h>

#ifdef USE_DLOPEN
#define SPD_AUDIO_PLUGIN_ENTRY spd_audio_plugin_get
#else
#define SPD_AUDIO_PLUGIN_ENTRY spd_alsa_LTX_spd_audio_plugin_get
#endif
#include <spd_audio_plugin.h>

#include "../common/common.h"

typedef struct {
	AudioID id;
	snd_pcm_t *alsa_pcm;	/* identifier of the ALSA device */
	snd_pcm_hw_params_t *alsa_hw_params;	/* parameters of sound */
	snd_pcm_sw_params_t *alsa_sw_params;	/* parameters of playback */
	snd_pcm_uframes_t alsa_buffer_size;
	pthread_mutex_t alsa_pcm_mutex;	/* mutex to guard the state of the device */
	pthread_mutex_t alsa_pipe_mutex;	/* mutex to guard the stop pipes */
	pthread_cond_t alsa_pipe_cond;	/* mutex to guard the stop pipes */
	int alsa_stop_pipe[2];	/* Pipe for communication about stop requests */
	int stop_requested;	/* Whether we want to stop */
	int alsa_fd_count;	/* Counter of descriptors to poll */
	struct pollfd *alsa_poll_fds;	/* Descriptors to poll */
	int alsa_opened;	/* 1 between snd_pcm_open and _close, 0 otherwise */
	char *alsa_device_name;	/* the name of the device to open */
} spd_alsa_id_t;

static int _alsa_close(spd_alsa_id_t * id);
static int _alsa_open(spd_alsa_id_t * id);

static int xrun(spd_alsa_id_t * id);
static int suspend(spd_alsa_id_t * id);

static int wait_for_poll(spd_alsa_id_t * id, struct pollfd *alsa_poll_fds,
			 unsigned int count, int draining);

#ifndef timersub
#define	timersub(a, b, result) \
	do { \
		(result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \
		(result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \
		if ((result)->tv_usec < 0) { \
			--(result)->tv_sec; \
			(result)->tv_usec += 1000000; \
		} \
	} while (0)
#endif

/* Put a message into the logfile (stderr) */
#define MSG(level, arg, ...) if (level <= alsa_log_level) { MSG(0, "ALSA: " arg, ##__VA_ARGS__); }
#define ERR(arg, ...) MSG(0, "ALSA ERROR: " arg, ##__VA_ARGS__)

static int alsa_log_level;
static char const *alsa_play_cmd = "aplay";

/* I/O error handler */
static int xrun(spd_alsa_id_t * id)
{
	snd_pcm_status_t *status;
	int res;

	if (id == NULL)
		return -1;

	MSG(1, "WARNING: Entering XRUN handler");

	snd_pcm_status_alloca(&status);
	if ((res = snd_pcm_status(id->alsa_pcm, status)) < 0) {
		ERR("status error: %s", snd_strerror(res));

		return -1;
	}
	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);
		MSG(1, "underrun!!! (at least %.3f ms long)",
		    diff.tv_sec * 1000 + diff.tv_usec / 1000.0);
		if ((res = snd_pcm_prepare(id->alsa_pcm)) < 0) {
			ERR("xrun: prepare error: %s", snd_strerror(res));

			return -1;
		}

		return 0;	/* ok, data should be accepted again */
	}
	ERR("read/write error, state = %s",
	    snd_pcm_state_name(snd_pcm_status_get_state(status)));

	return -1;
}

/* I/O suspend handler */
static int suspend(spd_alsa_id_t * id)
{
	int res;

	MSG(1, "WARNING: Entering SUSPEND handler.");

	if (id == NULL)
		return -1;

	while ((res = snd_pcm_resume(id->alsa_pcm)) == -EAGAIN)
		sleep(1);	/* wait until suspend flag is released */

	if (res < 0) {
		if ((res = snd_pcm_prepare(id->alsa_pcm)) < 0) {
			ERR("suspend: prepare error: %s", snd_strerror(res));

			return -1;
		}
	}

	return 0;
}

/* Open the device so that it's ready for playing on the default
   device. Internal function used by the public alsa_open. */
static int _alsa_open(spd_alsa_id_t * id)
{
	int err;

	MSG(1, "Opening ALSA device");
	fflush(stderr);

	/* Open the device */
	if ((err = snd_pcm_open(&id->alsa_pcm, id->alsa_device_name,
				SND_PCM_STREAM_PLAYBACK,
				SND_PCM_NONBLOCK)) < 0) {
		ERR("Cannot open audio device %s (%s)", id->alsa_device_name,
		    snd_strerror(err));
		return -1;
	}

	/* Allocate space for hw_params (description of the sound parameters) */
	/* Allocate space for sw_params (description of the sound parameters) */
	MSG(2, "Allocating new sw_params structure");
	if ((err = snd_pcm_sw_params_malloc(&id->alsa_sw_params)) < 0) {
		ERR("Cannot allocate hardware parameter structure (%s)",
		    snd_strerror(err));
		return -1;
	}

	MSG(1, "Opening ALSA device ... success");

	return 0;
}

/*
   Close the device. Internal function used by public alsa_close.
*/

static int _alsa_close(spd_alsa_id_t * id)
{
	int err;

	MSG(1, "Closing ALSA device");

	pthread_mutex_lock(&id->alsa_pipe_mutex);

	if (id->alsa_opened == 0) {
		pthread_mutex_unlock(&id->alsa_pipe_mutex);
		return 0;
	}

	id->alsa_opened = 0;

	if ((err = snd_pcm_close(id->alsa_pcm)) < 0) {
		MSG(2, "Cannot close ALSA device (%s)", snd_strerror(err));
		pthread_mutex_unlock(&id->alsa_pipe_mutex);
		return -1;
	}

	snd_pcm_sw_params_free(id->alsa_sw_params);

	g_free(id->alsa_poll_fds);
	pthread_mutex_unlock(&id->alsa_pipe_mutex);

	MSG(1, "Closing ALSA device ... success");

	return 0;
}

/* Open ALSA for playback.

  These parameters are passed in pars:
  (char*) pars[0] ... null-terminated string containing the name
                      of the device to be used for sound output
                      on ALSA
  (void*) pars[1] ... =NULL
*/
static AudioID *alsa_open(void **pars)
{
	spd_alsa_id_t *alsa_id;
	int ret;

	if (pars[1] == NULL) {
		ERR("Can't open ALSA sound output, missing parameters in argument.");
		return NULL;
	}

	alsa_id = (spd_alsa_id_t *) g_malloc(sizeof(spd_alsa_id_t));

	pthread_mutex_init(&alsa_id->alsa_pipe_mutex, NULL);
	pthread_cond_init(&alsa_id->alsa_pipe_cond, NULL);

	alsa_id->alsa_opened = 0;

	MSG(1, "Opening ALSA sound output");

	alsa_id->alsa_device_name = g_strdup(pars[1]);

	ret = _alsa_open(alsa_id);
	if (ret) {
		ERR("Cannot initialize Alsa device '%s': Can't open.",
		    alsa_id->alsa_device_name);
		g_free(alsa_id);
		return NULL;
	}

	MSG(1, "Device '%s' initialized successfully.",
	    alsa_id->alsa_device_name);

	return (AudioID *) alsa_id;
}

/* Close ALSA */
static int alsa_close(AudioID * id)
{
	int err;
	spd_alsa_id_t *alsa_id = (spd_alsa_id_t *) id;

	/* Close device */
	if ((err = _alsa_close(alsa_id)) < 0) {
		ERR("Cannot close audio device");
		return -1;
	}
	MSG(1, "ALSA closed.");

	g_free(alsa_id->alsa_device_name);
	g_free(alsa_id);
	id = NULL;

	return 0;
}

/* Wait until ALSA is readdy for more samples or alsa_stop() was called.

Returns 0 if ALSA is ready for more input, +1 if a request to stop
the sound output was received and a negative value on error.  */

int wait_for_poll(spd_alsa_id_t * id, struct pollfd *alsa_poll_fds,
		  unsigned int count, int draining)
{
	unsigned short revents;
	snd_pcm_state_t state;
	int ret;

	//      MSG("Waiting for poll");

	/* Wait for certain events */
	while (1) {
		ret = poll(id->alsa_poll_fds, count, -1);
		//      MSG("wait_for_poll: activity on %d descriptors", ret);

		/* Check for stop request from alsa_stop on the last file
		   descriptors */
		revents = id->alsa_poll_fds[count - 1].revents;
		if (0 != revents) {
			if (revents & POLLIN) {
				MSG(4, "wait_for_poll: stop requested");
				return 1;
			}
		}

		/* Check the first count-1 descriptors for ALSA events */
		snd_pcm_poll_descriptors_revents(id->alsa_pcm,
						 id->alsa_poll_fds, count - 1,
						 &revents);

		/* Ensure we are in the right state */
		state = snd_pcm_state(id->alsa_pcm);
		//      MSG("State after poll returned is %s", snd_pcm_state_name(state));

		if (SND_PCM_STATE_XRUN == state) {
			if (!draining) {
				MSG(1, "WARNING: Buffer underrun detected!");
				if (xrun(id) != 0)
					return -1;
				return 0;
			} else {
				MSG(4, "Poll: Playback terminated");
				return 0;
			}
		}

		if (SND_PCM_STATE_SUSPENDED == state) {
			MSG(1, "WARNING: Suspend detected!");
			if (suspend(id) != 0)
				return -1;
			return 0;
		}

		/* Check for errors */
		if (revents & POLLERR) {
			MSG(4, "wait_for_poll: poll revents says POLLERR");
			return -EIO;
		}

		/* Is ALSA ready for more input? */
		if ((revents & POLLOUT)) {
			MSG(5, "Poll: Ready for more input");
			return 0;
		}
	}
}

#define ERROR_EXIT() do {\
	g_free(track_volume.samples); \
	ERR("alsa_play() abnormal exit"); \
	_alsa_close(alsa_id); \
	return -1; \
} while (0)

/* Configure ALSA playback for the given configuration of track
   But do not play anything yet */
static int alsa_begin(AudioID * id, AudioTrack track)
{
	snd_pcm_format_t format;
	spd_alsa_id_t *alsa_id = (spd_alsa_id_t *) id;

	int err;

	snd_pcm_uframes_t period_size;
	unsigned int sr;

	snd_pcm_state_t state;

	struct pollfd alsa_stop_pipe_pfd;

	if (alsa_id == NULL) {
		ERR("Invalid device passed to alsa_play()");
		return -1;
	}

	pthread_mutex_lock(&alsa_id->alsa_pipe_mutex);

	MSG(2, "Start of playback on ALSA");

	/* Is it not an empty track? */
	/* Passing an empty track is not an error */
	if (track.samples == NULL) {
		pthread_mutex_unlock(&alsa_id->alsa_pipe_mutex);
		return 0;
	}
	/* Allocate space for hw_params (description of the sound parameters) */
	MSG(2, "Allocating new hw_params structure");
	if ((err = snd_pcm_hw_params_malloc(&alsa_id->alsa_hw_params)) < 0) {
		ERR("Cannot allocate hardware parameter structure (%s)",
		    snd_strerror(err));
		pthread_mutex_unlock(&alsa_id->alsa_pipe_mutex);
		return -1;
	}

	/* Initialize hw_params on our pcm */
	if ((err =
	     snd_pcm_hw_params_any(alsa_id->alsa_pcm,
				   alsa_id->alsa_hw_params)) < 0) {
		ERR("Cannot initialize hardware parameter structure (%s)",
		    snd_strerror(err));
		pthread_mutex_unlock(&alsa_id->alsa_pipe_mutex);
		return -1;
	}

	/* Create the pipe for communication about stop requests */
	if (pipe(alsa_id->alsa_stop_pipe)) {
		ERR("Stop pipe creation failed (%s)", strerror(errno));
		pthread_mutex_unlock(&alsa_id->alsa_pipe_mutex);
		return -1;
	}
	alsa_id->stop_requested = 0;

	/* Find how many descriptors we will get for poll() */
	alsa_id->alsa_fd_count =
	    snd_pcm_poll_descriptors_count(alsa_id->alsa_pcm);
	if (alsa_id->alsa_fd_count <= 0) {
		ERR("Invalid poll descriptors count returned from ALSA.");
		pthread_mutex_unlock(&alsa_id->alsa_pipe_mutex);
		return -1;
	}

	/* Create and fill in struct pollfd *alsa_poll_fds with ALSA descriptors */
	alsa_id->alsa_poll_fds =
	    g_malloc((alsa_id->alsa_fd_count + 1) * sizeof(struct pollfd));
	assert(alsa_id->alsa_poll_fds);
	if ((err =
	     snd_pcm_poll_descriptors(alsa_id->alsa_pcm, alsa_id->alsa_poll_fds,
				      alsa_id->alsa_fd_count)) < 0) {
		ERR("Unable to obtain poll descriptors for playback: %s\n",
		    snd_strerror(err));
		pthread_mutex_unlock(&alsa_id->alsa_pipe_mutex);
		return -1;
	}

	/* Create a new pollfd structure for requests by alsa_stop() */
	alsa_stop_pipe_pfd.fd = alsa_id->alsa_stop_pipe[0];
	alsa_stop_pipe_pfd.events = POLLIN;
	alsa_stop_pipe_pfd.revents = 0;

	/* Join this our own pollfd to the ALSAs ones */
	alsa_id->alsa_poll_fds[alsa_id->alsa_fd_count] = alsa_stop_pipe_pfd;
	alsa_id->alsa_fd_count++;

	alsa_id->alsa_opened = 1;
	pthread_mutex_unlock(&alsa_id->alsa_pipe_mutex);

	/* Report current state */
	state = snd_pcm_state(alsa_id->alsa_pcm);
	MSG(4, "PCM state before setting audio parameters: %s",
	    snd_pcm_state_name(state));

	/* Choose the correct format */
	if (track.bits == 16) {
		switch (alsa_id->id.format) {
		case SPD_AUDIO_LE:
			format = SND_PCM_FORMAT_S16_LE;
			break;
		case SPD_AUDIO_BE:
			format = SND_PCM_FORMAT_S16_BE;
			break;
		default:
			ERR("unknown audio format (%d)", alsa_id->id.format);
			return -1;
		}
	} else if (track.bits == 8) {
		format = SND_PCM_FORMAT_S8;
	} else {
		ERR("Unsupported sound data format, track.bits = %d",
		    track.bits);
		return -1;
	}

	/* Set access mode, bitrate, sample rate and channels */
	MSG(4, "Setting access type to INTERLEAVED");
	if ((err = snd_pcm_hw_params_set_access(alsa_id->alsa_pcm,
						alsa_id->alsa_hw_params,
						SND_PCM_ACCESS_RW_INTERLEAVED)
	    ) < 0) {
		ERR("Cannot set access type (%s)", snd_strerror(err));
		return -1;
	}

	MSG(4, "Setting sample format to %s", snd_pcm_format_name(format));
	if ((err =
	     snd_pcm_hw_params_set_format(alsa_id->alsa_pcm,
					  alsa_id->alsa_hw_params,
					  format)) < 0) {
		ERR("Cannot set sample format (%s)", snd_strerror(err));
		return -1;
	}

	MSG(4, "Setting sample rate to %i", track.sample_rate);
	sr = track.sample_rate;
	if ((err =
	     snd_pcm_hw_params_set_rate_near(alsa_id->alsa_pcm,
					     alsa_id->alsa_hw_params, &sr,
					     0)) < 0) {
		ERR("Cannot set sample rate (%s)", snd_strerror(err));

		return -1;
	}

	MSG(4, "Setting channel count to %i", track.num_channels);
	if ((err =
	     snd_pcm_hw_params_set_channels(alsa_id->alsa_pcm,
					    alsa_id->alsa_hw_params,
					    track.num_channels)) < 0) {
		MSG(4, "cannot set channel count (%s)", snd_strerror(err));
		return -1;
	}

	MSG(4, "Setting hardware parameters on the ALSA device");
	if ((err =
	     snd_pcm_hw_params(alsa_id->alsa_pcm,
			       alsa_id->alsa_hw_params)) < 0) {
		MSG(4, "cannot set parameters (%s) state=%s", snd_strerror(err),
		    snd_pcm_state_name(snd_pcm_state(alsa_id->alsa_pcm)));
		return -1;
	}

	/* Get the current swparams */
	if ((err =
	     snd_pcm_sw_params_current(alsa_id->alsa_pcm,
				       alsa_id->alsa_sw_params)) < 0) {
		ERR("Unable to determine current swparams for playback: %s\n",
		    snd_strerror(err));
		return -1;
	}
	//    MSG("Checking buffer size");
	if ((err =
	     snd_pcm_hw_params_get_buffer_size(alsa_id->alsa_hw_params,
					       &(alsa_id->alsa_buffer_size))) <
	    0) {
		ERR("Unable to get buffer size for playback: %s\n",
		    snd_strerror(err));
		return -1;
	}
	MSG(4, "Buffer size on ALSA device is %d frames",
	    (int)alsa_id->alsa_buffer_size);

	/* This is probably better left for the device driver to decide */
	/* allow the transfer when at least period_size samples can be processed */
	/*    err = snd_pcm_sw_params_set_avail_min(id->alsa_pcm, id->alsa_sw_params, id->alsa_buffer_size/4);
	   if (err < 0) {
	   ERR("Unable to set avail min for playback: %s\n", snd_strerror(err));
	   return err;
	   } */

	/* Get period size. */
	snd_pcm_hw_params_get_period_size(alsa_id->alsa_hw_params, &period_size,
	                                  0);
	MSG(4, "Period size on ALSA device is %lu frames", (unsigned long) period_size);

	MSG(4, "Preparing device for playback");
	if ((err = snd_pcm_prepare(alsa_id->alsa_pcm)) < 0) {
		ERR("Cannot prepare audio interface for playback (%s)",
		    snd_strerror(err));

		return -1;
	}

	return 0;
}

/* Push audio track to ALSA playback */
static int alsa_feed(AudioID * id, AudioTrack track)
{
	int bytes_per_sample;
	int num_bytes;
	spd_alsa_id_t *alsa_id = (spd_alsa_id_t *) id;

	AudioTrack track_volume;
	float real_volume;
	int i;

	signed short *output_samples;

	int err;
	int ret;

	snd_pcm_state_t state;

	snd_pcm_uframes_t framecount;
	size_t volume_size;

	bytes_per_sample = track.bits / 8;

	/* Calculate space needed to round up to nearest period size. */
	volume_size = bytes_per_sample * track.num_samples;
	MSG(4, "volume size = %i", (int)volume_size);

	/* Create a copy of track with adjusted volume. */
	MSG(4, "Making copy of track and adjusting volume");
	track_volume = track;
	track_volume.samples = (short *)g_malloc(volume_size);
	real_volume = ((float)alsa_id->id.volume + 100) / (float)200;
	for (i = 0; i <= track.num_samples - 1; i++)
		track_volume.samples[i] = track.samples[i] * real_volume;

	/* Loop until all samples are played on the device. */
	output_samples = track_volume.samples;
	num_bytes = volume_size;
	MSG(4, "%d bytes to be played", num_bytes);
	while (num_bytes > 0) {

		/* Write as much samples as possible */
		framecount = num_bytes / bytes_per_sample / track.num_channels;

		/* Report current state state */
		state = snd_pcm_state(alsa_id->alsa_pcm);
		//      MSG("PCM state before writei: %s",
		//          snd_pcm_state_name(state));

		/* MSG("snd_pcm_writei() called") */
		ret =
		    snd_pcm_writei(alsa_id->alsa_pcm, output_samples,
				   framecount);
		if (ret >= 0)
			MSG(5, "Sent %d of %d remaining bytes", ret*bytes_per_sample*track.num_channels, num_bytes);

		if (ret == -EAGAIN) {
			MSG(4, "Warning: Forced wait!");
			snd_pcm_wait(alsa_id->alsa_pcm, 100);
		} else if (ret == -EPIPE) {
			MSG(4, "Warning: returned EPIPE!");
			if (xrun(alsa_id) != 0)
				ERROR_EXIT();
#ifdef ESTRPIPE
		} else if (ret == -ESTRPIPE) {
			MSG(4, "Warning: returned ESTRPIPE!");
			if (suspend(alsa_id) != 0)
				ERROR_EXIT();
#endif
		} else if (ret == -EBUSY) {
			MSG(4, "WARNING: sleeping while PCM BUSY");
			usleep(100);
			continue;
		} else if (ret < 0) {
			ERR("Write to audio interface failed (%s)",
			    snd_strerror(ret));
			ERROR_EXIT();
		}

		if (ret > 0) {
			/* Update counter of bytes left and move the data pointer */
			num_bytes -=
			    ret * bytes_per_sample * track.num_channels;
			output_samples +=
			    ret * bytes_per_sample * track.num_channels / 2;
		}

		/* Report current state */
		state = snd_pcm_state(alsa_id->alsa_pcm);
		//      MSG("PCM state before polling: %s",
		//          snd_pcm_state_name(state));

		err =
		    wait_for_poll(alsa_id, alsa_id->alsa_poll_fds,
				  alsa_id->alsa_fd_count, 0);
		if (err < 0) {
			ERR("Wait for poll() failed\n");
			ERROR_EXIT();
		} else if (err == 1) {
			MSG(4, "Playback stopped");

			/* Drop the playback on the sound device (probably
			   still in progress up till now) */
			err = snd_pcm_drop(alsa_id->alsa_pcm);
			if (err < 0) {
				ERR("snd_pcm_drop() failed: %s",
				    snd_strerror(err));
				return -1;
			}

			/* Terminating (successfully or after a stop) */
			goto terminate;
		}

		if (num_bytes <= 0)
			break;
//      MSG("ALSA ready for more samples");

		/* Stop requests can be issued again */
	}

terminate:
	if (track_volume.samples != NULL)
		g_free(track_volume.samples);

	return 0;
}

/* Drain ALSA playback until only `left' samples are left in the buffer */
static int alsa_drain_left(AudioID * id, snd_pcm_uframes_t left)
{
	spd_alsa_id_t *alsa_id = (spd_alsa_id_t *) id;

	int err;

	MSG(4, "Draining until %lu frames left...", (unsigned long) left);

	/* We want to get next "device ready" notification only after the buffer
	   is already empty */
	err =
	    snd_pcm_sw_params_set_avail_min(alsa_id->alsa_pcm,
					    alsa_id->alsa_sw_params,
					    alsa_id->alsa_buffer_size - left);
	if (err < 0) {
		ERR("Unable to set avail min for playback: %s\n",
		    snd_strerror(err));
		return err;
	}
	/* write the parameters to the playback device */
	err = snd_pcm_sw_params(alsa_id->alsa_pcm, alsa_id->alsa_sw_params);
	if (err < 0) {
		ERR("Unable to set sw params for playback: %s\n",
		    snd_strerror(err));
		return -1;
	}

	err =
	    wait_for_poll(alsa_id, alsa_id->alsa_poll_fds,
			  alsa_id->alsa_fd_count, 1);
	if (err < 0) {
		ERR("Wait for poll() failed\n");
		return -1;
	} else if (err == 1) {
		MSG(4, "Playback stopped while draining");

		/* Drop the playback on the sound device (probably
		   still in progress up till now) */
		err = snd_pcm_drop(alsa_id->alsa_pcm);
		if (err < 0) {
			ERR("snd_pcm_drop() failed: %s", snd_strerror(err));
			return -1;
		}
	}

	/* When ALSA is going through Pulseaudio, wait_for_poll returns too
	   early because the file descriptor is always available for writing
	   :/ */
	while (!alsa_id->stop_requested)
	{
		snd_pcm_sframes_t frames;
		snd_pcm_state_t state;
		struct timeval tv;
		struct timespec ts;

		/* Poll server */
		frames = snd_pcm_avail(alsa_id->alsa_pcm);
		if (frames < 0) {
			MSG(4, "Drain: Buffer clear");
			break;
		}

		MSG(5, "Drain: %lu frames left in buffer",
			(unsigned long) alsa_id->alsa_buffer_size - frames);
		if (alsa_id->alsa_buffer_size - frames <= left) {
			MSG(4, "Drain: Buffer clear enough");
			break;
		}

		state = snd_pcm_state(alsa_id->alsa_pcm);
		if (err != 0) {
			MSG(4, "Drain: Status error %d", err);
			break;
		}
		if (state == SND_PCM_STATE_XRUN) {
			MSG(4, "Drain: Playback terminated");
			break;
		}

		/* Poll every 10ms */
		gettimeofday(&tv, NULL);
		ts.tv_sec = tv.tv_sec;
		ts.tv_nsec = tv.tv_usec * 1000 + 10000000;
		if (ts.tv_nsec >= 1000000000) {
			ts.tv_sec += 1;
			ts.tv_nsec -= 1000000000;
		}

		pthread_mutex_lock(&alsa_id->alsa_pipe_mutex);
		if (!alsa_id->stop_requested)
			pthread_cond_timedwait(&alsa_id->alsa_pipe_cond,
					       &alsa_id->alsa_pipe_mutex, &ts);
		pthread_mutex_unlock(&alsa_id->alsa_pipe_mutex);
	}

	MSG(4, "Draining terminated");

	return 0;
}

/* Drain until the amount of samples left in the buffer is big enough to make
   sure we will have time to push the rest of the audio */
static int alsa_drain_overlap(AudioID * id, AudioTrack track)
{
	/* We typically want 20ms overlap: usually about a period size, and
	   small enough to be unnoticeable */
	unsigned min_ms = 20;

	spd_alsa_id_t *alsa_id = (spd_alsa_id_t *) id;
	snd_pcm_uframes_t min;
	snd_pcm_uframes_t period_size;
	snd_pcm_uframes_t min2;
	snd_pcm_uframes_t left;

	min = (min_ms * track.sample_rate) / 1000;

	/* Get period size. */
	snd_pcm_hw_params_get_period_size(alsa_id->alsa_hw_params, &period_size,
	                                  0);

	/* Round minimum to two period size */
	if (2*period_size >= min)
		min2 = 2*period_size;
	else
		min2 = (min + period_size - 1) / period_size;

	/* Usually the buffer size will be far enough */
	if (alsa_id->alsa_buffer_size >= min2)
		left = min2;
	else
		/* That's very odd. Just wait for some room to be available */
		left = alsa_id->alsa_buffer_size - 1;

	MSG(4, "Draining with at least %ums left, i.e. %lu frames, with period %lu frames, thus %lu frames, i.e. %lu left of %lu",
		min_ms, (unsigned long) min, (unsigned long) period_size,
		(unsigned long) min2, (unsigned long) left, (unsigned long)
		alsa_id->alsa_buffer_size);

	return alsa_drain_left(id, left);
}

static int alsa_drain(AudioID * id)
{
	return alsa_drain_left(id, 0);
}

static int alsa_feed_sync(AudioID * id, AudioTrack track)
{
	int ret;

	ret = alsa_feed(id, track);
	if (ret)
		return ret;

	return alsa_drain(id);
}

static int alsa_feed_sync_overlap(AudioID * id, AudioTrack track)
{
	int ret;

	ret = alsa_feed(id, track);
	if (ret)
		return ret;

	return alsa_drain_overlap(id, track);
}

static int alsa_end(AudioID * id)
{
	spd_alsa_id_t *alsa_id = (spd_alsa_id_t *) id;
	int err;

	if (!alsa_id->stop_requested)
		alsa_drain(id);

	err = snd_pcm_drop(alsa_id->alsa_pcm);
	if (err < 0) {
		ERR("snd_pcm_drop() failed: %s", snd_strerror(err));
		return -1;
	}

	MSG(2, "Freeing HW parameters");
	snd_pcm_hw_params_free(alsa_id->alsa_hw_params);

	pthread_mutex_lock(&alsa_id->alsa_pipe_mutex);
	alsa_id->alsa_opened = 0;
	close(alsa_id->alsa_stop_pipe[0]);
	close(alsa_id->alsa_stop_pipe[1]);

	g_free(alsa_id->alsa_poll_fds);
	pthread_mutex_unlock(&alsa_id->alsa_pipe_mutex);

	MSG(1, "End of playback on ALSA");

	return 0;
}

/* Play the track _track_ (see spd_audio.h) using the id->alsa_pcm device and
 id-hw_params parameters. This is a blocking function, however, it's possible
 to interrupt playing from a different thread with alsa_stop(). alsa_play
 returns after and immediately after the whole sound was played on the
 speakers.

 The idea is that we get the ALSA file descriptors and we will poll() to see
 when alsa is ready for more input while sleeping in the meantime. We will
 additionally poll() for one more descriptor used by alsa_stop() to notify the
 thread with alsa_play() that the stop of the playback is requested. The
 variable can_be_stopped is used for very simple synchronization between the
 two threads. */
static int alsa_play(AudioID * id, AudioTrack track)
{
	int ret;

	ret = alsa_begin(id, track);
	if (ret)
		return ret;

	ret = alsa_feed_sync(id, track);
	if (ret)
		return ret;

	return alsa_end(id);
}

#undef ERROR_EXIT

/*
 Stop the playback on the device and interrupt alsa_play()
*/
static int alsa_stop(AudioID * id)
{
	char buf;
	int ret;
	spd_alsa_id_t *alsa_id = (spd_alsa_id_t *) id;

	MSG(1, "STOP!");

	if (alsa_id == NULL)
		return 0;

	pthread_mutex_lock(&alsa_id->alsa_pipe_mutex);
	if (alsa_id->alsa_opened) {
		alsa_id->stop_requested = 1;

		/* This constant is arbitrary */
		buf = 42;

		ret = write(alsa_id->alsa_stop_pipe[1], &buf, 1);
		if (ret <= 0) {
			ERR("Can't write stop request to pipe, err %d: %s",
			    errno, strerror(errno));
		}
		pthread_cond_broadcast(&alsa_id->alsa_pipe_cond);
	}
	pthread_mutex_unlock(&alsa_id->alsa_pipe_mutex);

	return 0;
}

/*
  Set volume

  Comments: It's not possible to set individual track volume with Alsa, so we
   handle volume in alsa_play() by multiplication of each sample.
*/
static int alsa_set_volume(AudioID * id, int volume)
{
	return 0;
}

static void alsa_set_loglevel(int level)
{
	if (level) {
		alsa_log_level = level;
	}
}

static char const *alsa_get_playcmd(void)
{
	return alsa_play_cmd;
}

/* Provide the Alsa backend. */
static spd_audio_plugin_t alsa_functions = {
	"alsa",
	alsa_open,
	alsa_play,
	alsa_stop,
	alsa_close,
	alsa_set_volume,
	alsa_set_loglevel,
	alsa_get_playcmd,
	alsa_begin,
	alsa_feed_sync,
	alsa_feed_sync_overlap,
	alsa_end,
};

spd_audio_plugin_t *alsa_plugin_get(void)
{
	return &alsa_functions;
}

spd_audio_plugin_t *
    __attribute__ ((weak))
    SPD_AUDIO_PLUGIN_ENTRY(void)
{
	return &alsa_functions;
}
#undef MSG
#undef ERR
