| |
| /* |
| * 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; |
| } |