blob: c04af5aa7c0d7785599efbc72b94d3931680ac43 [file]
/*
* spd_audio.c -- Spd Audio Output Library
*
* Copyright (C) 2004, 2006 Brailcom, o.p.s.
* Copyright (C) 2019, 2021, 2025 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: spd_audio.c,v 1.21 2008-06-09 10:29:12 hanke Exp $
*/
/*
* spd_audio is a simple realtime audio output library with the capability of
* playing 8 or 16 bit data, immediate stop and synchronization. This library
* currently provides OSS, NAS, ALSA and PulseAudio backend. The available backends are
* specified at compile-time using the directives WITH_OSS, WITH_NAS, WITH_ALSA,
* WITH_PULSE, WITH_LIBAO but the user program is allowed to switch between them at run-time.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "spd_audio.h"
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <glib.h>
#ifdef USE_DLOPEN
#include <dlfcn.h>
#else
#include <ltdl.h>
#endif
static int spd_audio_log_level;
#ifdef USE_DLOPEN
static void *dlhandle;
#else
static lt_dlhandle lt_h;
/* Dynamically load a library with RTLD_GLOBAL set.
This is needed when a dynamically-loaded library has its own plugins
that call into the parent library.
Most of the credit for this function goes to Gary Vaughan.
*/
static lt_dlhandle my_dlopenextglobal(const char *filename)
{
lt_dlhandle handle = NULL;
lt_dladvise advise;
if (lt_dladvise_init(&advise))
return handle;
if (!lt_dladvise_ext(&advise) && !lt_dladvise_global(&advise))
handle = lt_dlopenadvise(filename, advise);
lt_dladvise_destroy(&advise);
return handle;
}
#endif
/* Open the audio device.
Arguments:
type -- The requested device. Currently AudioOSS or AudioNAS.
pars -- and array of pointers to parameters to pass to
the device backend, terminated by a NULL pointer.
See the source/documentation of each specific backend.
error -- a pointer to the string where error description is
stored in case of failure (returned AudioID == NULL).
Otherwise will contain NULL.
Return value:
Newly allocated AudioID structure that can be passed to
all other spd_audio functions, or NULL in case of failure.
*/
AudioID *spd_audio_open(const char *name, void **pars, char **error)
{
AudioID *id;
spd_audio_plugin_t const *p;
spd_audio_plugin_t *(*fn) (void);
gchar *libname;
char *plugin_dir;
#ifndef USE_DLOPEN
int ret;
/* now check whether dynamic plugin is available */
ret = lt_dlinit();
if (ret != 0) {
*error = (char *)g_strdup_printf("lt_dlinit() failed");
return (AudioID *) NULL;
}
#endif
plugin_dir = getenv("SPEECHD_PLUGIN_DIR");
if (!plugin_dir)
plugin_dir = PLUGIN_DIR;
#ifdef USE_DLOPEN
libname = g_strdup_printf("%s/" SPD_AUDIO_LIB_PREFIX "%s.so", plugin_dir, name);
dlhandle = dlopen(libname, RTLD_NOW | RTLD_GLOBAL);
if (NULL == dlhandle) {
*error =
(char *)g_strdup_printf("Cannot open plugin %s at %s. error: %s",
name, libname, dlerror());
g_free(libname);
return (AudioID *) NULL;
}
g_free(libname);
fn = dlsym(dlhandle, SPD_AUDIO_PLUGIN_ENTRY_STR);
#else
ret = lt_dlsetsearchpath(plugin_dir);
if (ret != 0) {
*error = (char *)g_strdup_printf("lt_dlsetsearchpath() failed");
return (AudioID *) NULL;
}
libname = g_strdup_printf(SPD_AUDIO_LIB_PREFIX "%s", name);
lt_h = my_dlopenextglobal(libname);
if (NULL == lt_h) {
*error =
(char *)g_strdup_printf("Cannot open plugin %s in %s/%s. error: %s",
name, plugin_dir, libname, lt_dlerror());
g_free(libname);
return (AudioID *) NULL;
}
g_free(libname);
fn = lt_dlsym(lt_h, SPD_AUDIO_PLUGIN_ENTRY_STR);
#endif
if (NULL == fn) {
*error = (char *)g_strdup_printf("Cannot find symbol %s",
SPD_AUDIO_PLUGIN_ENTRY_STR);
return (AudioID *) NULL;
}
p = fn();
if (p == NULL || p->name == NULL) {
*error = (char *)g_strdup_printf("plugin %s not found", name);
return (AudioID *) NULL;
}
id = p->open(pars);
if (id == NULL) {
*error =
(char *)g_strdup_printf("Couldn't open %s plugin", name);
return (AudioID *) NULL;
}
id->function = p;
#if defined(BYTE_ORDER) && (BYTE_ORDER == BIG_ENDIAN)
id->format = SPD_AUDIO_BE;
#else
id->format = SPD_AUDIO_LE;
#endif
*error = NULL;
return id;
}
/* Initialize for playing a track on the audio device.
Arguments:
id -- the AudioID* of the device returned by spd_audio_open
track -- a track to play (see spd_audio.h)
Return value:
0 if everything is ok, a non-zero value in case of failure.
See the particular backend documentation or source for the
meaning of these non-zero values.
*/
int spd_audio_begin(AudioID * id, AudioTrack track, AudioFormat format)
{
if (!id) {
fprintf(stderr, "No audio open\n");
return -1;
}
if (!id->function->begin) {
/* Too bad */
return 0;
}
return id->function->begin(id, track);
}
/* Perform byte-swapping if needed */
static void spd_audio_convert(AudioID * id, AudioTrack track, AudioFormat format)
{
/* Only perform byte swapping if the driver in use has given us audio in
an endian format other than what the running CPU supports. */
if (format != id->format && track.bits == 16) {
unsigned char *out_ptr, *out_end, c;
out_ptr = (unsigned char *)track.samples;
out_end =
out_ptr +
track.num_samples * 2 * track.num_channels;
while (out_ptr < out_end) {
c = out_ptr[0];
out_ptr[0] = out_ptr[1];
out_ptr[1] = c;
out_ptr += 2;
}
}
}
/* Feed a track to the audio device (blocking).
This can be called several times to feed more audio samples with the same
configuration, but since this version is blocking, the audio card will
underrun. Using spd_audio_feed_overlap is thus preferred in that case.
Arguments:
id -- the AudioID* of the device returned by spd_audio_open
track -- a track to play (see spd_audio.h)
Return value:
0 if everything is ok, a non-zero value in case of failure.
See the particular backend documentation or source for the
meaning of these non-zero values.
Comment:
spd_audio_feed() is a blocking function. It returns exactly
when the given piece of track stopped playing. However, it's possible
to safely interrupt it using spd_audio_stop() described below.
(spd_audio_stop() needs to be called from another thread, obviously.)
*/
int spd_audio_feed_sync(AudioID * id, AudioTrack track, AudioFormat format)
{
if (!id) {
fprintf(stderr, "No audio open\n");
return -1;
}
spd_audio_convert(id, track, format);
if (id->function->feed_sync) {
return id->function->feed_sync(id, track);
}
if (id->function->play) {
return id->function->play(id, track);
}
fprintf(stderr,"Play not supported on this device\n");
return -1;
}
/* Feed a track to the audio device (blocking, with overlapping).
This can be called several times to feed more audio samples with the same
configuration, this version is blocking, but with some overlap, so that it
can be called within the overlapping period without audio card overrun.
Arguments:
id -- the AudioID* of the device returned by spd_audio_open
track -- a track to play (see spd_audio.h)
Return value:
0 if everything is ok, a non-zero value in case of failure.
See the particular backend documentation or source for the
meaning of these non-zero values.
Comment:
spd_audio_feed() is a blocking function. It returns exactly
when the given piece of track stopped playing. However, it's possible
to safely interrupt it using spd_audio_stop() described below.
(spd_audio_stop() needs to be called from another thread, obviously.)
*/
int spd_audio_feed_sync_overlap(AudioID * id, AudioTrack track, AudioFormat format)
{
if (!id) {
fprintf(stderr, "No audio open\n");
return -1;
}
spd_audio_convert(id, track, format);
if (id->function->feed_sync_overlap) {
return id->function->feed_sync_overlap(id, track);
}
if (id->function->feed_sync) {
return id->function->feed_sync(id, track);
}
if (id->function->play) {
return id->function->play(id, track);
}
fprintf(stderr,"Play not supported on this device\n");
return -1;
}
/* Finish playing a track on the audio device.
Arguments:
id -- the AudioID* of the device returned by spd_audio_open
Return value:
0 if everything is ok, a non-zero value in case of failure.
See the particular backend documentation or source for the
meaning of these non-zero values.
*/
int spd_audio_end(AudioID * id)
{
if (!id) {
fprintf(stderr, "No audio open\n");
return -1;
}
if (!id->function->end) {
/* Too bad */
return 0;
}
return id->function->end(id);
}
/* Play a track on the audio device (blocking).
Arguments:
id -- the AudioID* of the device returned by spd_audio_open
track -- a track to play (see spd_audio.h)
Return value:
0 if everything is ok, a non-zero value in case of failure.
See the particular backend documentation or source for the
meaning of these non-zero values.
Comment:
spd_audio_play() is a blocking function. It returns exactly
when the given track stopped playing. However, it's possible
to safely interrupt it using spd_audio_stop() described below.
(spd_audio_stop() needs to be called from another thread, obviously.)
*/
int spd_audio_play(AudioID * id, AudioTrack track, AudioFormat format)
{
int ret;
ret = spd_audio_begin(id, track, format);
if (ret)
return ret;
ret = spd_audio_feed_sync(id, track, format);
if (ret)
return ret;
return spd_audio_end(id);
}
/* Stop playing the current track on device id
Arguments:
id -- the AudioID* of the device returned by spd_audio_open
Return value:
0 if everything is ok, a non-zero value in case of failure.
See the particular backend documentation or source for the
meaning of these non-zero values.
Comment:
spd_audio_stop() safely interrupts spd_audio_play() when called
from another thread. It shouldn't cause any clicks or unwanted
effects in the sound output.
It's safe to call spd_audio_stop() even if the device isn't playing
any track. In that case, it does nothing. However, there is a danger
when using spd_audio_stop(). Since you must obviously do it from
another thread than where spd_audio_play is running, you must make
yourself sure that the device is still open and the id you pass it
is valid and will be valid until spd_audio_stop returns. In other words,
you should use some mutex or other synchronization device to be sure
spd_audio_close isn't called before or during spd_audio_stop execution.
*/
int spd_audio_stop(AudioID * id)
{
int ret;
if (id && id->function->stop) {
ret = id->function->stop(id);
} else {
fprintf(stderr, "Stop not supported on this device\n");
return -1;
}
return ret;
}
/* Close the audio device id
Arguments:
id -- the AudioID* of the device returned by spd_audio_open
Return value:
0 if everything is ok, a non-zero value in case of failure.
Comments:
Please make sure no other spd_audio function with this device id
is running in another threads. See spd_audio_stop() for detailed
description of possible problems.
*/
int spd_audio_close(AudioID * id)
{
int ret = 0;
if (id && id->function->close) {
ret = (id->function->close(id));
}
#ifdef USE_DLOPEN
if (NULL != dlhandle) {
dlclose(dlhandle);
dlhandle = NULL;
}
#else
if (NULL != lt_h) {
lt_dlclose(lt_h);
lt_h = NULL;
lt_dlexit();
}
#endif
return ret;
}
/* Set volume for playing tracks on the device id
Arguments:
id -- the AudioID* of the device returned by spd_audio_open
volume -- a value in the range <-100:100> where -100 means the
least volume (probably silence), 0 the default volume
and +100 the highest volume possible to make on that
device for a single flow (i.e. not using mixer).
Return value:
0 if everything is ok, a non-zero value in case of failure.
See the particular backend documentation or source for the
meaning of these non-zero values.
Comments:
In case of /dev/dsp, it's not possible to set volume for
the particular flow. For that reason, the value 0 means
the volume the track was recorded on and each smaller value
means less volume (since this works by dividing the samples
in the track by a constant).
*/
int spd_audio_set_volume(AudioID * id, int volume)
{
if ((volume > 100) || (volume < -100)) {
fprintf(stderr, "Requested volume out of range");
return -1;
}
if (id == NULL) {
fprintf(stderr, "audio id is NULL in spd_audio_set_volume\n");
return -1;
}
id->volume = volume;
return 0;
}
void spd_audio_set_loglevel(AudioID * id, int level)
{
if (level) {
spd_audio_log_level = level;
if (id != 0 && id->function != 0)
id->function->set_loglevel(level);
}
}
char const *spd_audio_get_playcmd(AudioID * id)
{
if (id != 0 && id->function != 0) {
return id->function->get_playcmd();
}
return NULL;
}