
/*
 * pulse.c -- The simple pulseaudio backend for the spd_audio library.
 *
 * Copyright 2007-2009 Gilles Casse <gcasse@oralux.org>
 * Copyright 2008-2010 Brailcom, o.p.s
 * Copyright 2008-2015 Luke Yelavich <luke.yelavich@canonical.com>
 * Copyright 2009 Rui Batista <ruiandrebatista@gmail.com>
 * Copyright 2009 Marco Skambraks <marco@openblinux.de>
 * Copyright 2010 Andrei Kholodnyi <Andrei.Kholodnyi@gmail.com>
 * Copyright 2010 Christopher Brannon <cmbrannon79@gmail.com>
 * Copyright 2010-2011 William Hubbs <w.d.hubbs@gmail.com>
 * Copyright 2015 Jeremy Whiting <jpwhiting@kde.org>
 * Copyright 2018-2025 Samuel Thibault <samuel.thibault@ens-lyon.org>
 * Copyright 2004-2006 Lennart Poettering
 *
 * Copied from Luke Yelavich's libao.c driver, and merged with code from
 * Marco's ao_pulse.c driver, by Bill Cox, Dec 21, 2009.
 *
 * Minor changes be Rui Batista <rui.batista@ist.utl.pt> to configure settings through speech-dispatcher configuration files
 * Date: Dec 22, 2009
 *
 * 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/>.
 *
 */

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

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <string.h>
#include <stdarg.h>
#include <glib.h>

#include <pulse/pulseaudio.h>
#include <pulse/thread-mainloop.h>
#include <pulse/error.h>

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

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

typedef struct spd_pa_simple spd_pa_simple;

typedef struct {
	AudioID id;
	spd_pa_simple *pa_simple;
	char *pa_server;
	char *pa_device;
	char *pa_name;
	int pa_min_audio_length;	// in ms
	int pa_current_rate;	// Sample rate for currently PA connection
	int pa_current_bps;	// Bits per sample rate for currently PA connection
	int pa_current_channels;	// Number of channels for currently PA connection
} spd_pulse_id_t;

/* Initial values, most often what synths will requests */
#define DEF_RATE 44100
#define DEF_CHANNELS 1
#define DEF_BYTES_PER_SAMPLE 2

/* This is the smallest audio sound we are expected to play immediately without buffering. */
/* Changed to define on config file. Default is the same. */
/* Default to 10 ms of latency */
#define DEFAULT_PA_MIN_AUDIO_LENGTH 10

static int pulse_log_level;
static char const *pulse_play_cmd = "paplay -n speech-dispatcher-generic";

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

/* The following is a copy of pulseaudio's src/pulse/simple.c
 * that was modified to fit our needs: very low-latency playback start and stop,
 * but still ample buffering to avoid underruns. */
struct spd_pa_simple {
	pa_threaded_mainloop *mainloop;
	pa_context *context;
	pa_stream *stream;

	int operation_success;

	int32_t bytes_per_s;

	int playing;

	pthread_mutex_t drain_lock;
	pthread_cond_t drain_cond;
};

#define CHECK_SUCCESS_GOTO(p, rerror, expression, label)			\
	do {									\
		if (!(expression)) {						\
			if (rerror)						\
				*(rerror) = pa_context_errno((p)->context);	\
			goto label;						\
		}								\
	} while(0);

#define CHECK_DEAD_GOTO(p, rerror, label)					\
	do {									\
		if (!(p)->context || !PA_CONTEXT_IS_GOOD(pa_context_get_state((p)->context)) || \
			!(p)->stream || !PA_STREAM_IS_GOOD(pa_stream_get_state((p)->stream))) { \
			if (((p)->context && pa_context_get_state((p)->context) == PA_CONTEXT_FAILED) || \
				((p)->stream && pa_stream_get_state((p)->stream) == PA_STREAM_FAILED)) { \
				if (rerror)					\
					*(rerror) = pa_context_errno((p)->context);	\
			} else							\
				if (rerror)					\
					*(rerror) = PA_ERR_BADSTATE;		\
			goto label;						\
		}								\
	} while(0);

static void context_state_cb(pa_context *c, void *userdata) {
	spd_pa_simple *p = userdata;

	switch (pa_context_get_state(c)) {
		case PA_CONTEXT_READY:
		case PA_CONTEXT_TERMINATED:
		case PA_CONTEXT_FAILED:
			pa_threaded_mainloop_signal(p->mainloop, 0);
			break;

		case PA_CONTEXT_UNCONNECTED:
		case PA_CONTEXT_CONNECTING:
		case PA_CONTEXT_AUTHORIZING:
		case PA_CONTEXT_SETTING_NAME:
			break;
	}
}

static void stream_state_cb(pa_stream *s, void * userdata) {
	spd_pa_simple *p = userdata;

	switch (pa_stream_get_state(s)) {

		case PA_STREAM_READY:
		case PA_STREAM_FAILED:
		case PA_STREAM_TERMINATED:
			pa_threaded_mainloop_signal(p->mainloop, 0);
			break;

		case PA_STREAM_UNCONNECTED:
		case PA_STREAM_CREATING:
			break;
	}
}

static void stream_request_cb(pa_stream *s, size_t length, void *userdata) {
	spd_pa_simple *p = userdata;

	pa_threaded_mainloop_signal(p->mainloop, 0);
}

static void stream_latency_update_cb(pa_stream *s, void *userdata) {
	spd_pa_simple *p = userdata;

	pa_threaded_mainloop_signal(p->mainloop, 0);
}

static void spd_pa_simple_free(spd_pa_simple *s);

static spd_pa_simple* spd_pa_simple_new(
		const char *server,
		const char *name,
		const char *dev,
		const char *stream_name,
		const pa_sample_spec *ss,
		const pa_buffer_attr *attr,
		int *rerror) {

	spd_pa_simple *p;
	int error = PA_ERR_INTERNAL, r;

	p = pa_xnew0(spd_pa_simple, 1);

	if (!(p->mainloop = pa_threaded_mainloop_new()))
		goto fail;

	if (!(p->context = pa_context_new(pa_threaded_mainloop_get_api(p->mainloop), name)))
		goto fail;

	pa_context_set_state_callback(p->context, context_state_cb, p);

	if (pa_context_connect(p->context, server, 0, NULL) < 0) {
		error = pa_context_errno(p->context);
		goto fail;
	}

	pa_threaded_mainloop_lock(p->mainloop);

	if (pa_threaded_mainloop_start(p->mainloop) < 0)
		goto unlock_and_fail;

	for (;;) {
		pa_context_state_t state;

		state = pa_context_get_state(p->context);

		if (state == PA_CONTEXT_READY)
			break;

		if (!PA_CONTEXT_IS_GOOD(state)) {
			error = pa_context_errno(p->context);
			goto unlock_and_fail;
		}

		/* Wait until the context is ready */
		pa_threaded_mainloop_wait(p->mainloop);
	}

	if (!(p->stream = pa_stream_new(p->context, stream_name, ss, NULL))) {
		error = pa_context_errno(p->context);
		goto unlock_and_fail;
	}

	pa_stream_set_state_callback(p->stream, stream_state_cb, p);
	pa_stream_set_read_callback(p->stream, stream_request_cb, p);
	pa_stream_set_write_callback(p->stream, stream_request_cb, p);
	pa_stream_set_latency_update_callback(p->stream, stream_latency_update_cb, p);

	r = pa_stream_connect_playback(p->stream, dev, attr,
				       PA_STREAM_INTERPOLATE_TIMING
				       |PA_STREAM_ADJUST_LATENCY
				       |PA_STREAM_AUTO_TIMING_UPDATE, NULL, NULL);

	if (r < 0) {
		error = pa_context_errno(p->context);
		goto unlock_and_fail;
	}

	for (;;) {
		pa_stream_state_t state;

		state = pa_stream_get_state(p->stream);

		if (state == PA_STREAM_READY)
			break;

		if (!PA_STREAM_IS_GOOD(state)) {
			error = pa_context_errno(p->context);
			goto unlock_and_fail;
		}

		/* Wait until the stream is ready */
		pa_threaded_mainloop_wait(p->mainloop);
	}

	pa_threaded_mainloop_unlock(p->mainloop);

	p->playing = 0;
	p->bytes_per_s = ss->rate * ss->channels * (ss->format == PA_SAMPLE_U8 ? 1 : 2);
	pthread_mutex_init(&p->drain_lock, NULL);
	pthread_cond_init(&p->drain_cond, NULL);

	return p;

unlock_and_fail:
	pa_threaded_mainloop_unlock(p->mainloop);

fail:
	if (rerror)
		*rerror = error;
	spd_pa_simple_free(p);
	return NULL;
}

static void spd_pa_simple_free(spd_pa_simple *s) {
	if (s->mainloop)
		pa_threaded_mainloop_stop(s->mainloop);

	if (s->stream)
		pa_stream_unref(s->stream);

	if (s->context) {
		pa_context_disconnect(s->context);
		pa_context_unref(s->context);
	}

	if (s->mainloop)
		pa_threaded_mainloop_free(s->mainloop);

	pa_xfree(s);
}

static void success_cb(pa_stream *s, int success, void *userdata) {
	spd_pa_simple *p = userdata;

	p->operation_success = success;
	pa_threaded_mainloop_signal(p->mainloop, 0);
}

/* Assumes p is locked */
static int spd_pa_simple_do_flush(spd_pa_simple *p, int *rerror) {
	pa_operation *o = NULL;

	p->playing = 0;
	CHECK_DEAD_GOTO(p, rerror, unlock_and_fail);

	o = pa_stream_flush(p->stream, success_cb, p);
	CHECK_SUCCESS_GOTO(p, rerror, o, unlock_and_fail);

	p->operation_success = 0;
	while (pa_operation_get_state(o) == PA_OPERATION_RUNNING) {
		pa_threaded_mainloop_wait(p->mainloop);
		CHECK_DEAD_GOTO(p, rerror, unlock_and_fail);
	}
	CHECK_SUCCESS_GOTO(p, rerror, p->operation_success, unlock_and_fail);

	pa_operation_unref(o);

	return 0;

unlock_and_fail:

	if (o) {
		pa_operation_cancel(o);
		pa_operation_unref(o);
	}

	return -1;
}

static int spd_pa_simple_write(spd_pa_simple *p, const void*data, size_t length, int *rerror) {
	pa_threaded_mainloop_lock(p->mainloop);

	CHECK_DEAD_GOTO(p, rerror, unlock_and_fail);

	p->playing = 1;

	while (length > 0) {
		size_t l;
		int r;

		while (p->playing && !(l = pa_stream_writable_size(p->stream))) {
			pa_threaded_mainloop_wait(p->mainloop);
			CHECK_DEAD_GOTO(p, rerror, unlock_and_fail);
		}

		if (!p->playing)
			break;

		CHECK_SUCCESS_GOTO(p, rerror, l != (size_t) -1, unlock_and_fail);

		if (l > length)
			l = length;

		r = pa_stream_write(p->stream, data, l, NULL, 0LL, PA_SEEK_RELATIVE);
		CHECK_SUCCESS_GOTO(p, rerror, r >= 0, unlock_and_fail);

		data = (const uint8_t*) data + l;
		length -= l;
	}

	if (pulse_log_level >= 4) {
		pa_usec_t t = 0;

		if (pa_stream_get_latency(p->stream, &t, NULL) >= 0) {
			MSG(4, "Wrote data, playing with latency %lldµs\n", (long long) t);
		}
	}

	if (!p->playing) {
		/* We got interrupted, flush.  */
		spd_pa_simple_do_flush(p, NULL);
	}

	pa_threaded_mainloop_unlock(p->mainloop);
	return 0;

unlock_and_fail:
	pa_threaded_mainloop_unlock(p->mainloop);
	return -1;
}

static int spd_pa_simple_drain(spd_pa_simple *p, pa_usec_t overlap, int *rerror) {
	pa_operation *o = NULL;

	pa_threaded_mainloop_lock(p->mainloop);

	CHECK_DEAD_GOTO(p, rerror, unlock_and_fail);

	MSG(4, "draining to overlap %llu\n", (unsigned long long) overlap);

	/* Drain until requested overlap */
	while(1) {
		if (!p->playing) {
			/* We got interrupted, flush.  */
			spd_pa_simple_do_flush(p, NULL);
			break;
		}

		o = pa_stream_update_timing_info(p->stream, success_cb, p);
		CHECK_SUCCESS_GOTO(p, rerror, o, unlock_and_fail);

		p->operation_success = 0;
		while (pa_operation_get_state(o) == PA_OPERATION_RUNNING) {
			pa_threaded_mainloop_wait(p->mainloop);
			CHECK_DEAD_GOTO(p, rerror, unlock_and_fail);
		}
		CHECK_SUCCESS_GOTO(p, rerror, p->operation_success, unlock_and_fail);

		pa_operation_unref(o);
		o = NULL;

		const pa_timing_info *info = pa_stream_get_timing_info(p->stream);
		int64_t left_us;

		if (info) {
			int64_t left = info->write_index - info->read_index;
			left_us = left * PA_USEC_PER_SEC / p->bytes_per_s - info->transport_usec - info->sink_usec;

			//MSG(5, "left %lld bytes, %lldµs\n", (long long) left, (long long) left_us);

			if (left_us <= (int64_t) overlap)
				break;
			left_us -= overlap;
		} else {
			/* Wait a bit for information */
			left_us = 1000;
		}

		/* Do not sleep too much to avoid miss-ups */
		if (left_us > 10000)
			left_us = 10000;

		pa_threaded_mainloop_unlock(p->mainloop);

		/* Ideally pulseaudio would provide a pa_threaded_mainloop_timedwait */
		pthread_mutex_lock(&p->drain_lock);
		if (p->playing)
		{
			struct timespec ts;

			clock_gettime(CLOCK_REALTIME, &ts);
			ts.tv_nsec += left_us * 1000;
			while (ts.tv_nsec >= 1000000000)
			{
				ts.tv_nsec -= 1000000000;
				ts.tv_sec++;
			}

			pthread_cond_timedwait(&p->drain_cond, &p->drain_lock, &ts);
		}
		pthread_mutex_unlock(&p->drain_lock);
		pa_threaded_mainloop_lock(p->mainloop);
	}

	pa_threaded_mainloop_unlock(p->mainloop);
	return 0;

unlock_and_fail:
	if (o) {
		pa_operation_cancel(o);
		pa_operation_unref(o);
	}

	pa_threaded_mainloop_unlock(p->mainloop);
	return -1;
}

static int spd_pa_simple_flush(spd_pa_simple *p) {
	pa_threaded_mainloop_lock(p->mainloop);

	pthread_mutex_lock(&p->drain_lock);
	if (p->playing) {
		/* Still writing or draining, stop it and let it flush.  */
		p->playing = 0;
		pa_threaded_mainloop_signal(p->mainloop, 0);
		pthread_cond_signal(&p->drain_cond);
	} else {
		/* Not writing, playback is ongoing, flush it. */
		spd_pa_simple_do_flush(p, NULL);
	}
	pthread_mutex_unlock(&p->drain_lock);

	pa_threaded_mainloop_unlock(p->mainloop);
	return -1;
}

/* Now pure-spd code */

static int _pulse_open(spd_pulse_id_t * id, int sample_rate,
		       int num_channels, int bytes_per_sample)
{
	pa_buffer_attr buffAttr;
	pa_sample_spec ss;
	int error;
	char *client_name;

	ss.rate = sample_rate;
	ss.channels = num_channels;
	if (bytes_per_sample == 2) {
		switch (id->id.format) {
		case SPD_AUDIO_LE:
			ss.format = PA_SAMPLE_S16LE;
			break;
		case SPD_AUDIO_BE:
			ss.format = PA_SAMPLE_S16BE;
			break;
		}
	} else {
		ss.format = PA_SAMPLE_U8;
	}

	/* Set prebuf to one sample so that keys are spoken as soon as typed rather than delayed until the next key pressed */
	buffAttr.maxlength = (uint32_t) - 1;
	//buffAttr.tlength = (uint32_t)-1; - this is the default, which causes key echo to not work properly.
	buffAttr.tlength = id->pa_min_audio_length * sample_rate * num_channels * bytes_per_sample / 1000;
	buffAttr.prebuf = (uint32_t) - 1;
	buffAttr.minreq = (uint32_t) - 1;
	buffAttr.fragsize = (uint32_t) - 1;

	if (!id->pa_name ||
	    asprintf(&client_name, "speech-dispatcher-%s", id->pa_name) < 0)
		client_name = strdup("speech-dispatcher");

	/* Open new connection */
	if (!
	    (id->pa_simple =
	     spd_pa_simple_new(id->pa_server, client_name,
			   id->pa_device, "playback",
			   &ss, &buffAttr, &error))) {
		fprintf(stderr, __FILE__ ": pa_simple_new() failed: %s\n",
			pa_strerror(error));
		free(client_name);
		return 1;
	}
	free(client_name);
	return 0;
}

/* Close the connection to the server.  Does not free the AudioID struct. */
/* Usable in pulse_play, which closes connections on failure or */
/* changes in audio parameters. */
static void pulse_connection_close(spd_pulse_id_t * pulse_id)
{
	if (pulse_id->pa_simple != NULL) {
		spd_pa_simple_free(pulse_id->pa_simple);
		pulse_id->pa_simple = NULL;
	}
}

static AudioID *pulse_open(void **pars)
{
	spd_pulse_id_t *pulse_id;
	int ret;

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

	pulse_id = (spd_pulse_id_t *) g_malloc(sizeof(spd_pulse_id_t));

	/* Select an Endianness for the initial connection. */
#if defined(BYTE_ORDER) && (BYTE_ORDER == BIG_ENDIAN)
	pulse_id->id.format = SPD_AUDIO_BE;
#else
	pulse_id->id.format = SPD_AUDIO_LE;
#endif
	pulse_id->pa_simple = NULL;
	pulse_id->pa_server = NULL;
	pulse_id->pa_device = (char *)pars[3];
	pulse_id->pa_name = (char *)pars[5];
	pulse_id->pa_min_audio_length = DEFAULT_PA_MIN_AUDIO_LENGTH;

	pulse_id->pa_current_rate = -1;
	pulse_id->pa_current_bps = -1;
	pulse_id->pa_current_channels = -1;

	if (!strcmp(pulse_id->pa_device, "default")) {
		pulse_id->pa_device = NULL;
	}

	if (pars[4] != NULL && atoi(pars[4]) != 0)
		pulse_id->pa_min_audio_length = atoi(pars[4]);

	ret = _pulse_open(pulse_id, DEF_RATE, DEF_CHANNELS, DEF_BYTES_PER_SAMPLE);
	if (ret) {
		g_free(pulse_id);
		pulse_id = NULL;
	}

	return (AudioID *) pulse_id;
}

/* Configure pulse playback for the given configuration of track
   But do not play anything yet */
static int pulse_begin(AudioID * id, AudioTrack track)
{
	int bytes_per_sample;
	int error;
	spd_pulse_id_t *pulse_id = (spd_pulse_id_t *) id;

	if (id == NULL) {
		return -1;
	}
	if (track.samples == NULL || track.num_samples <= 0) {
		return 0;
	}
	MSG(4, "Starting playback\n");
	/* Choose the correct format */
	if (track.bits == 16) {
		bytes_per_sample = 2;
	} else if (track.bits == 8) {
		bytes_per_sample = 1;
	} else {
		ERR("ERROR: Unsupported sound data format, track.bits = %d\n",
		    track.bits);
		return -1;
	}

	/* Check if the current connection has suitable parameters for this track */
	if (pulse_id->pa_current_rate != track.sample_rate
	    || pulse_id->pa_current_bps != track.bits
	    || pulse_id->pa_current_channels != track.num_channels) {
		MSG(4, "Reopening connection due to change in track parameters sample_rate:%d bps:%d channels:%d\n", track.sample_rate, track.bits, track.num_channels);
		/* Close old connection if any */
		pulse_connection_close(pulse_id);
		/* Open a new connection */
		error = _pulse_open(pulse_id, track.sample_rate, track.num_channels,
			    bytes_per_sample);
		if (error) {
			pulse_id->pa_current_rate = -1;
			pulse_id->pa_current_bps = -1;
			pulse_id->pa_current_channels = -1;
			return -1;
		}
		/* Keep track of current connection parameters */
		pulse_id->pa_current_rate = track.sample_rate;
		pulse_id->pa_current_bps = track.bits;
		pulse_id->pa_current_channels = track.num_channels;
	}

	return 0;
}

static int pulse_feed(AudioID * id, AudioTrack track)
{
	int num_bytes;
	int bytes_per_sample;
	int error;
	spd_pulse_id_t *pulse_id = (spd_pulse_id_t *) id;

	if (track.bits == 16) {
		bytes_per_sample = 2;
	} else if (track.bits == 8) {
		bytes_per_sample = 1;
	} else {
		ERR("ERROR: Unsupported sound data format, track.bits = %d\n",
		    track.bits);
		return -1;
	}
	num_bytes = track.num_samples * bytes_per_sample;

	MSG(4, "bytes to play: %d, (%f secs)\n", num_bytes,
	    (((float)(num_bytes) / 2) / (float)track.sample_rate));

	if (spd_pa_simple_write
	    (pulse_id->pa_simple, track.samples, num_bytes,
	     &error) < 0) {
		spd_pa_simple_flush(pulse_id->pa_simple);
		pulse_connection_close(pulse_id);
		MSG(4, "ERROR: Audio: pulse_play(): %s - closing device - re-open it in next run\n", pa_strerror(error));
		return -1;
	}

	/* TODO: pa_stream_get_time() or pa_stream_get_latency(). The former
	will return the current playback time of the hardware since the stream
	has been started. */

	return 0;
}

static int pulse_feed_sync(AudioID * id, AudioTrack track)
{
	spd_pulse_id_t *pulse_id = (spd_pulse_id_t *) id;
	int ret;

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

	return spd_pa_simple_drain(pulse_id->pa_simple, 0, NULL);
}

static int pulse_feed_sync_overlap(AudioID * id, AudioTrack track)
{
	spd_pulse_id_t *pulse_id = (spd_pulse_id_t *) id;
	int ret;

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

	/* We typically want 20ms overlap: usually about a period size, and
	   small enough to be unnoticeable */
	return spd_pa_simple_drain(pulse_id->pa_simple, 20000, NULL);
}

static int pulse_end(AudioID * id)
{
	spd_pulse_id_t *pulse_id = (spd_pulse_id_t *) id;
	spd_pa_simple_drain(pulse_id->pa_simple, 0, NULL);
	return 0;
}

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

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

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

	return pulse_end(id);
}

/* stop the pulse_play() loop */
static int pulse_stop(AudioID * id)
{
	spd_pulse_id_t *pulse_id = (spd_pulse_id_t *) id;

	spd_pa_simple_flush(pulse_id->pa_simple);

	return 0;
}

static int pulse_close(AudioID * id)
{
	spd_pulse_id_t *pulse_id = (spd_pulse_id_t *) id;
	pulse_connection_close(pulse_id);
	g_free(pulse_id);
	id = NULL;

	return 0;
}

static int pulse_set_volume(AudioID * id, int volume)
{
	/* TODO */
	return 0;
}

static void pulse_set_loglevel(int level)
{
	if (level) {
		pulse_log_level = level;
	}
}

static char const *pulse_get_playcmd(void)
{
	return pulse_play_cmd;
}

/* Provide the pulse backend. */
static spd_audio_plugin_t pulse_functions = {
	"pulse",
	pulse_open,
	pulse_play,
	pulse_stop,
	pulse_close,
	pulse_set_volume,
	pulse_set_loglevel,
	pulse_get_playcmd,
	pulse_begin,
	pulse_feed_sync,
	pulse_feed_sync_overlap,
	pulse_end,
};

spd_audio_plugin_t *pulse_plugin_get(void)
{
	return &pulse_functions;
}

spd_audio_plugin_t *
    __attribute__ ((weak))
    SPD_AUDIO_PLUGIN_ENTRY(void)
{
	return &pulse_functions;
}

#undef MSG
#undef ERR
