blob: c3975a3468490d116924d5a8134d833d7b23ecb4 [file] [log] [blame]
/*
* 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