| * Build the code with a C99 compiler, instead of a C++ compiler. | * Build the code with a C99 compiler, instead of a C++ compiler. | ||||
| * Provide a pkg-config file (patch by Luke Yelavich). | * Provide a pkg-config file (patch by Luke Yelavich). | ||||
| * Use -fPIC to support sparc/sparc64 architectures. | * Use -fPIC to support sparc/sparc64 architectures. | ||||
| * Use the system's portaudio header files. | |||||
| * Removed the local portaudio header files. | |||||
| * Use the system's sonic library and header files. | * Use the system's sonic library and header files. | ||||
| * Output phoneme compilation errors to stderr. | * Output phoneme compilation errors to stderr. | ||||
| * Generate build failures if building phoneme, intonation or dictionary files | * Generate build failures if building phoneme, intonation or dictionary files | ||||
| * Converted the documentation to markdown. | * Converted the documentation to markdown. | ||||
| * Group the Windows and POSIX mbrowrap code to provide the `mbrowrap.h` | * Group the Windows and POSIX mbrowrap code to provide the `mbrowrap.h` | ||||
| implementation in a single place. | implementation in a single place. | ||||
| * Use the `wave_*` APIs in synchronous playback mode. | |||||
| * Replaced the audio APIs with PCAudioLib to improve portability of the audio | |||||
| and to share that across different projects. | |||||
| * Reworked the synchronous audio to share the code paths with asynchronous | |||||
| audio. | |||||
| cleanup: | cleanup: | ||||
| lib_LTLIBRARIES += src/libespeak-ng.la | lib_LTLIBRARIES += src/libespeak-ng.la | ||||
| src_libespeak_ng_la_LDFLAGS = -version-info $(SHARED_VERSION) -lpthread -lm | |||||
| src_libespeak_ng_la_LDFLAGS = -version-info $(SHARED_VERSION) -lpthread -lm \ | |||||
| ${PCAUDIOLIB_LIBS} | |||||
| src_libespeak_ng_la_CFLAGS = -Isrc/include -Isrc/include/compat \ | src_libespeak_ng_la_CFLAGS = -Isrc/include -Isrc/include/compat \ | ||||
| -fPIC -fvisibility=hidden -D _BSD_SOURCE -D_DEFAULT_SOURCE -D _POSIX_C_SOURCE=200112L \ | -fPIC -fvisibility=hidden -D _BSD_SOURCE -D_DEFAULT_SOURCE -D _POSIX_C_SOURCE=200112L \ | ||||
| -pedantic -fno-exceptions -D PATH_ESPEAK_DATA=\"$(DATADIR)\" -DLIBESPEAK_NG_EXPORT | |||||
| -pedantic -fno-exceptions -D PATH_ESPEAK_DATA=\"$(DATADIR)\" -DLIBESPEAK_NG_EXPORT \ | |||||
| ${PCAUDIOLIB_CFLAGS} | |||||
| src_libespeak_ng_la_SOURCES = \ | src_libespeak_ng_la_SOURCES = \ | ||||
| src/libespeak-ng/compiledata.c \ | src/libespeak-ng/compiledata.c \ | ||||
| src/libespeak-ng/compiledict.c \ | src/libespeak-ng/compiledict.c \ | ||||
| src/libespeak-ng/fifo.c | src/libespeak-ng/fifo.c | ||||
| endif | endif | ||||
| if AUDIO_RUNTIME | |||||
| src_libespeak_ng_la_LDFLAGS += -lpulse -lpulse-simple -lportaudio | |||||
| src_libespeak_ng_la_CFLAGS += -DUSE_PULSEAUDIO -DUSE_PORTAUDIO | |||||
| src_libespeak_ng_la_SOURCES += \ | |||||
| src/libespeak-ng/wave.c \ | |||||
| src/libespeak-ng/wave_pulse.c | |||||
| else | |||||
| if AUDIO_PULSEAUDIO | |||||
| src_libespeak_ng_la_LDFLAGS += -lpulse | |||||
| src_libespeak_ng_la_CFLAGS += -DUSE_PULSEAUDIO | |||||
| src_libespeak_ng_la_SOURCES += src/libespeak-ng/wave_pulse.c | |||||
| else | |||||
| if AUDIO_PORTAUDIO | |||||
| src_libespeak_ng_la_LDFLAGS += -lportaudio | |||||
| src_libespeak_ng_la_CFLAGS += -DUSE_PORTAUDIO | |||||
| src_libespeak_ng_la_SOURCES += src/libespeak-ng/wave.c | |||||
| else | |||||
| if AUDIO_SADA | |||||
| src_libespeak_ng_la_CFLAGS += -DUSE_SADA | |||||
| src_libespeak_ng_la_SOURCES += src/libespeak-ng/wave_sada.c | |||||
| else | |||||
| src_libespeak_ng_la_SOURCES += src/libespeak-ng/wave.c | |||||
| endif | |||||
| endif | |||||
| endif | |||||
| endif | |||||
| bin_PROGRAMS += src/speak-ng | bin_PROGRAMS += src/speak-ng | ||||
| if HAVE_RONN | if HAVE_RONN | ||||
| endif | endif | ||||
| src_speak_ng_LDADD = src/libespeak-ng.la | src_speak_ng_LDADD = src/libespeak-ng.la | ||||
| src_speak_ng_LDFLAGS = -static -lm | |||||
| src_speak_ng_LDFLAGS = -static -lm ${PCAUDIOLIB_LIBS} | |||||
| src_speak_ng_CFLAGS = -Isrc/libespeak-ng -Isrc/include -D _POSIX_C_SOURCE=200112L | src_speak_ng_CFLAGS = -Isrc/libespeak-ng -Isrc/include -D _POSIX_C_SOURCE=200112L | ||||
| src_speak_ng_SOURCES = src/speak-ng.c | src_speak_ng_SOURCES = src/speak-ng.c | ||||
| man1_MANS += src/espeak-ng.1 | man1_MANS += src/espeak-ng.1 | ||||
| endif | endif | ||||
| src_espeak_ng_LDADD = src/libespeak-ng.la | |||||
| src_espeak_ng_LDADD = src/libespeak-ng.la ${PCAUDIOLIB_LIBS} | |||||
| src_espeak_ng_CFLAGS = -Isrc/include | src_espeak_ng_CFLAGS = -Isrc/include | ||||
| src_espeak_ng_SOURCES = src/espeak-ng.c | src_espeak_ng_SOURCES = src/espeak-ng.c | ||||
| Optionally, you need: | Optionally, you need: | ||||
| 1. the pulseaudio development library to enable pulseaudio output; | |||||
| 2. the portaudio development library to enable portaudio output; | |||||
| 1. the [pcaudiolib](https://github.com/rhdunn/pcaudiolib) development library | |||||
| to enable audio output; | |||||
| 3. the [sonic](https://github.com/waywardgeek/sonic) development library to | 3. the [sonic](https://github.com/waywardgeek/sonic) development library to | ||||
| enable sonic audio speed up support; | enable sonic audio speed up support; | ||||
| 4. the `ronn` man-page markdown processor to build the man pages. | 4. the `ronn` man-page markdown processor to build the man pages. | ||||
| | Dependency | Install | | | Dependency | Install | | ||||
| |--------------|-----------------------------------------| | |--------------|-----------------------------------------| | ||||
| | pulseaudio | `sudo apt-get install libpulse-dev` | | |||||
| | portaudio 18 | `sudo apt-get install libportaudio-dev` | | |||||
| | portaudio 19 | `sudo apt-get install portaudio19-dev` | | |||||
| | sonic | `sudo apt-get install libsonic-dev` | | | sonic | `sudo apt-get install libsonic-dev` | | ||||
| | ronn | `sudo apt-get install ruby-ronn` | | | ronn | `sudo apt-get install ruby-ronn` | | ||||
| AC_CHECK_FUNCS([strstr]) | AC_CHECK_FUNCS([strstr]) | ||||
| dnl ================================================================ | dnl ================================================================ | ||||
| dnl PulseAudio checks. | |||||
| dnl PCAudioLib checks. | |||||
| dnl ================================================================ | dnl ================================================================ | ||||
| AC_ARG_WITH([pulseaudio], | |||||
| [AS_HELP_STRING([--with-pulseaudio], [use the pulseaudio library for audio output @<:@default=yes@:>@])], | |||||
| AC_ARG_WITH([pcaudiolib], | |||||
| [AS_HELP_STRING([--with-pcaudiolib], [use the pcaudiolib library for audio output @<:@default=yes@:>@])], | |||||
| []) | []) | ||||
| if test "$with_pulseaudio" = "no"; then | |||||
| echo "Disabling pulseaudio output support via pulseaudio" | |||||
| have_pulseaudio=no | |||||
| if test "$with_pcaudiolib" = "no"; then | |||||
| echo "Disabling audio output support via pcaudiolib" | |||||
| have_pcaudiolib=no | |||||
| else | else | ||||
| PKG_CHECK_MODULES(PULSEAUDIO, [libpulse >= 0.9], | |||||
| AC_CHECK_HEADERS([pcaudiolib/audio.h], | |||||
| [ | [ | ||||
| have_pulseaudio=yes | |||||
| have_pcaudiolib=yes | |||||
| PCAUDIOLIB_CFLAGS= | |||||
| PCAUDIOLIB_LIBS=-lpcaudio | |||||
| ],[ | ],[ | ||||
| have_pulseaudio=no | |||||
| have_pcaudiolib=no | |||||
| ]) | ]) | ||||
| fi | fi | ||||
| dnl ================================================================ | |||||
| dnl PortAudio checks. | |||||
| dnl ================================================================ | |||||
| AC_ARG_WITH([portaudio], | |||||
| [AS_HELP_STRING([--with-portaudio], [use the portaudio library for audio output @<:@default=yes@:>@])], | |||||
| []) | |||||
| if test "$with_portaudio" = "no"; then | |||||
| echo "Disabling portaudio output support via portaudio" | |||||
| have_portaudio=no | |||||
| else | |||||
| AC_CHECK_HEADERS([portaudio.h], | |||||
| [ | |||||
| # Check the portaudio library. | |||||
| AC_CHECK_LIB([portaudio], [Pa_IsStreamActive]) # portaudio 19 | |||||
| AC_CHECK_LIB([portaudio], [Pa_StreamActive]) # portaudio 18 | |||||
| # Then use the headers to determine the portaudio version. | |||||
| # This is because on some systems with both libportaudio0 and | |||||
| # libportaudio2 installed, portaudio.h and -lportaudio refer | |||||
| # to different versions. | |||||
| AC_CHECK_FUNC([Pa_IsStreamActive], | |||||
| [ | |||||
| have_portaudio=19 | |||||
| ],[ | |||||
| AC_CHECK_FUNC([Pa_StreamActive], | |||||
| [ | |||||
| have_portaudio=18 | |||||
| ],[ | |||||
| have_portaudio=no | |||||
| ]) | |||||
| ]) | |||||
| ],[ | |||||
| have_portaudio=no | |||||
| ]) | |||||
| fi | |||||
| dnl ================================================================ | |||||
| dnl Audio checks. | |||||
| dnl ================================================================ | |||||
| AC_ARG_WITH([sada], | |||||
| [AS_HELP_STRING([--with-sada], [use the Solaris SADA audio API @<:@default=no@:>@])], | |||||
| []) | |||||
| if test "$with_sada" = "yes" ; then | |||||
| have_sada=yes | |||||
| else | |||||
| have_sada=no | |||||
| fi | |||||
| if test "$have_portaudio" = 18 -o "$have_portaudio" = 19 ; then | |||||
| if test "$have_pulseaudio" = yes ; then | |||||
| PKG_CHECK_MODULES(PULSEAUDIO_SIMPLE, [libpulse-simple >= 0.9], | |||||
| [ | |||||
| have_pulseaudio=yes | |||||
| AUDIO=runtime | |||||
| ],[ | |||||
| have_pulseaudio=no | |||||
| AUDIO=portaudio | |||||
| ]) | |||||
| else | |||||
| AUDIO=portaudio | |||||
| fi | |||||
| elif test "$have_pulseaudio" = yes ; then | |||||
| AUDIO=pulseaudio | |||||
| elif test "$have_sada" = yes ; then | |||||
| AUDIO=sada | |||||
| else | |||||
| AUDIO=disabled | |||||
| fi | |||||
| AC_SUBST(AUDIO) | |||||
| AM_CONDITIONAL(AUDIO_RUNTIME, [test x"$AUDIO" = xruntime]) | |||||
| AM_CONDITIONAL(AUDIO_PULSEAUDIO, [test x"$AUDIO" = xpulseaudio]) | |||||
| AM_CONDITIONAL(AUDIO_PORTAUDIO, [test x"$AUDIO" = xportaudio]) | |||||
| AM_CONDITIONAL(AUDIO_SADA, [test x"$AUDIO" = xsada]) | |||||
| AC_SUBST(PCAUDIOLIB_CFLAGS) | |||||
| AC_SUBST(PCAUDIOLIB_LIBS) | |||||
| dnl ================================================================ | dnl ================================================================ | ||||
| dnl Optional compilation checks. | dnl Optional compilation checks. | ||||
| C99 Compiler: ${CC} | C99 Compiler: ${CC} | ||||
| C99 Compiler flags: ${CFLAGS} | C99 Compiler flags: ${CFLAGS} | ||||
| pulseaudio: ${have_pulseaudio} | |||||
| portaudio: ${have_portaudio} | |||||
| sada: ${have_sada} | |||||
| audio configuration: ${AUDIO} | |||||
| Sonic: ${have_sonic} | |||||
| PCAudioLib: ${have_pcaudiolib} | |||||
| Klatt: ${have_klatt} | Klatt: ${have_klatt} | ||||
| MBROLA: ${have_mbrola} | MBROLA: ${have_mbrola} | ||||
| Sonic: ${have_sonic} | |||||
| Async: ${have_async} | Async: ${have_async} | ||||
| Extended Dictionaries: | Extended Dictionaries: |
| #include <unistd.h> | #include <unistd.h> | ||||
| #include <wchar.h> | #include <wchar.h> | ||||
| #ifdef HAVE_PCAUDIOLIB_AUDIO_H | |||||
| #include <pcaudiolib/audio.h> | |||||
| #endif | |||||
| #if defined(_WIN32) || defined(_WIN64) | #if defined(_WIN32) || defined(_WIN64) | ||||
| #include <fcntl.h> | #include <fcntl.h> | ||||
| #include <io.h> | #include <io.h> | ||||
| #include "espeak_command.h" | #include "espeak_command.h" | ||||
| #include "fifo.h" | #include "fifo.h" | ||||
| #include "event.h" | #include "event.h" | ||||
| #include "wave.h" | |||||
| #ifndef S_ISDIR | #ifndef S_ISDIR | ||||
| #define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) | #define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) | ||||
| int event_list_ix = 0; | int event_list_ix = 0; | ||||
| int n_event_list; | int n_event_list; | ||||
| long count_samples; | long count_samples; | ||||
| void *my_audio = NULL; | |||||
| #ifdef HAVE_PCAUDIOLIB_AUDIO_H | |||||
| struct audio_object *my_audio = NULL; | |||||
| #endif | |||||
| static const char *option_device = NULL; | static const char *option_device = NULL; | ||||
| static unsigned int my_unique_identifier = 0; | static unsigned int my_unique_identifier = 0; | ||||
| voice_samplerate = event->id.number; | voice_samplerate = event->id.number; | ||||
| if (out_samplerate != voice_samplerate) { | if (out_samplerate != voice_samplerate) { | ||||
| #ifdef HAVE_PCAUDIOLIB_AUDIO_H | |||||
| if (out_samplerate != 0) { | if (out_samplerate != 0) { | ||||
| // sound was previously open with a different sample rate | // sound was previously open with a different sample rate | ||||
| wave_close(my_audio); | |||||
| audio_object_close(my_audio); | |||||
| sleep(1); | sleep(1); | ||||
| } | } | ||||
| #endif | |||||
| out_samplerate = voice_samplerate; | out_samplerate = voice_samplerate; | ||||
| my_audio = wave_open(voice_samplerate, option_device); | |||||
| #ifdef HAVE_PCAUDIOLIB_AUDIO_H | |||||
| audio_object_open(my_audio, AUDIO_OBJECT_FORMAT_S16LE, voice_samplerate, 1); | |||||
| if (!my_audio) { | if (!my_audio) { | ||||
| err = ENS_AUDIO_ERROR; | err = ENS_AUDIO_ERROR; | ||||
| return -1; | return -1; | ||||
| } | } | ||||
| #endif | |||||
| #ifdef USE_ASYNC | #ifdef USE_ASYNC | ||||
| if ((my_mode & ENOUTPUT_MODE_SYNCHRONOUS) == 0) | if ((my_mode & ENOUTPUT_MODE_SYNCHRONOUS) == 0) | ||||
| event_init(); | event_init(); | ||||
| } | } | ||||
| } | } | ||||
| if (outbuf && length && a_wave_can_be_played) { | |||||
| wave_write(my_audio, (char *)outbuf, 2*length); | |||||
| } | |||||
| #ifdef HAVE_PCAUDIOLIB_AUDIO_H | |||||
| if (outbuf && length && a_wave_can_be_played) | |||||
| audio_object_write(my_audio, (char *)outbuf, 2*length); | |||||
| #endif | |||||
| #ifdef USE_ASYNC | #ifdef USE_ASYNC | ||||
| while (event && a_wave_can_be_played) { | while (event && a_wave_can_be_played) { | ||||
| { | { | ||||
| option_device = device; | option_device = device; | ||||
| my_mode = output_mode; | my_mode = output_mode; | ||||
| my_audio = NULL; | |||||
| out_samplerate = 0; | out_samplerate = 0; | ||||
| #ifdef HAVE_PCAUDIOLIB_AUDIO_H | |||||
| my_audio = create_audio_device_object(device, "eSpeak", "Text-to-Speech"); | |||||
| #endif | |||||
| // buflength is in mS, allocate 2 bytes per sample | // buflength is in mS, allocate 2 bytes per sample | ||||
| if ((buffer_length == 0) || (output_mode & ENOUTPUT_MODE_SPEAK_AUDIO)) | if ((buffer_length == 0) || (output_mode & ENOUTPUT_MODE_SPEAK_AUDIO)) | ||||
| buffer_length = 200; | buffer_length = 200; | ||||
| end_character_position = end_position; | end_character_position = end_position; | ||||
| espeak_ng_STATUS aStatus = Synthesize(unique_identifier, text, flags); | espeak_ng_STATUS aStatus = Synthesize(unique_identifier, text, flags); | ||||
| #ifdef HAVE_PCAUDIOLIB_AUDIO_H | |||||
| if ((my_mode & ENOUTPUT_MODE_SPEAK_AUDIO) == ENOUTPUT_MODE_SPEAK_AUDIO) | if ((my_mode & ENOUTPUT_MODE_SPEAK_AUDIO) == ENOUTPUT_MODE_SPEAK_AUDIO) | ||||
| wave_flush(my_audio); | |||||
| audio_object_drain(my_audio); | |||||
| #endif | |||||
| return aStatus; | return aStatus; | ||||
| } | } | ||||
| event_clear_all(); | event_clear_all(); | ||||
| #endif | #endif | ||||
| #ifdef HAVE_PCAUDIOLIB_AUDIO_H | |||||
| if ((my_mode & ENOUTPUT_MODE_SPEAK_AUDIO) == ENOUTPUT_MODE_SPEAK_AUDIO) | if ((my_mode & ENOUTPUT_MODE_SPEAK_AUDIO) == ENOUTPUT_MODE_SPEAK_AUDIO) | ||||
| wave_close(my_audio); | |||||
| audio_object_close(my_audio); | |||||
| #endif | |||||
| embedded_value[EMBED_T] = 0; // reset echo for pronunciation announcements | embedded_value[EMBED_T] = 0; // reset echo for pronunciation announcements | ||||
| for (int i = 0; i < N_SPEECH_PARAM; i++) | for (int i = 0; i < N_SPEECH_PARAM; i++) | ||||
| #endif | #endif | ||||
| if ((my_mode & ENOUTPUT_MODE_SPEAK_AUDIO) == ENOUTPUT_MODE_SPEAK_AUDIO) { | if ((my_mode & ENOUTPUT_MODE_SPEAK_AUDIO) == ENOUTPUT_MODE_SPEAK_AUDIO) { | ||||
| wave_close(my_audio); | |||||
| wave_terminate(); | |||||
| #ifdef HAVE_PCAUDIOLIB_AUDIO_H | |||||
| audio_object_close(my_audio); | |||||
| audio_object_destroy(my_audio); | |||||
| #endif | |||||
| out_samplerate = 0; | out_samplerate = 0; | ||||
| } | } | ||||
| /* | |||||
| * Copyright (C) 2007, Gilles Casse <[email protected]> | |||||
| * Copyright (C) 2015-2016 Reece H. Dunn | |||||
| * based on AudioIO.cc (Audacity-1.2.4b) and wavegen.cpp | |||||
| * | |||||
| * This program is free software; you can redistribute it and/or modify | |||||
| * it under the terms of the GNU General Public License as published by | |||||
| * the Free Software Foundation; either version 3 of the License, or | |||||
| * (at your option) any later version. | |||||
| * | |||||
| * This program 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 General Public License | |||||
| * along with this program; if not, see: <http://www.gnu.org/licenses/>. | |||||
| */ | |||||
| #include "config.h" | |||||
| #include <assert.h> | |||||
| #include <math.h> | |||||
| #include <stdbool.h> | |||||
| #include <stdint.h> | |||||
| #include <stdio.h> | |||||
| #include <stdlib.h> | |||||
| #include <string.h> | |||||
| #include <sys/time.h> | |||||
| #include <time.h> | |||||
| #include <unistd.h> | |||||
| #ifdef PLATFORM_WINDOWS | |||||
| #include <windows.h> | |||||
| #endif | |||||
| #include <espeak-ng/espeak_ng.h> | |||||
| #include "speech.h" | |||||
| #include "wave.h" | |||||
| #ifdef USE_PORTAUDIO | |||||
| #include "portaudio.h" | |||||
| #undef USE_PORTAUDIO | |||||
| // determine portaudio version by looking for a #define which is not in V18 | |||||
| #ifdef paNeverDropInput | |||||
| #define USE_PORTAUDIO 19 | |||||
| #else | |||||
| #define USE_PORTAUDIO 18 | |||||
| #endif | |||||
| #ifdef USE_PULSEAUDIO | |||||
| // create some wrappers for runtime detection | |||||
| // checked on wave_open | |||||
| static int pulse_running; | |||||
| // wave.cpp (this file) | |||||
| void *wave_port_open(int, const char *); | |||||
| size_t wave_port_write(void *theHandler, char *theMono16BitsWaveBuffer, size_t theSize); | |||||
| int wave_port_close(void *theHandler); | |||||
| void wave_port_terminate(); | |||||
| void wave_port_flush(void *theHandler); | |||||
| void *wave_port_test_get_write_buffer(); | |||||
| // wave_pulse.cpp | |||||
| int is_pulse_running(); | |||||
| void *wave_pulse_open(int, const char *); | |||||
| size_t wave_pulse_write(void *theHandler, char *theMono16BitsWaveBuffer, size_t theSize); | |||||
| int wave_pulse_close(void *theHandler); | |||||
| void wave_pulse_terminate(); | |||||
| void wave_pulse_flush(void *theHandler); | |||||
| void *wave_pulse_test_get_write_buffer(); | |||||
| // wrappers | |||||
| void *wave_open(int srate, const char *device) | |||||
| { | |||||
| pulse_running = is_pulse_running(); | |||||
| if (pulse_running) | |||||
| return wave_pulse_open(srate, device); | |||||
| else | |||||
| return wave_port_open(srate, device); | |||||
| } | |||||
| size_t wave_write(void *theHandler, char *theMono16BitsWaveBuffer, size_t theSize) | |||||
| { | |||||
| if (pulse_running) | |||||
| return wave_pulse_write(theHandler, theMono16BitsWaveBuffer, theSize); | |||||
| else | |||||
| return wave_port_write(theHandler, theMono16BitsWaveBuffer, theSize); | |||||
| } | |||||
| int wave_close(void *theHandler) | |||||
| { | |||||
| if (pulse_running) | |||||
| return wave_pulse_close(theHandler); | |||||
| else | |||||
| return wave_port_close(theHandler); | |||||
| } | |||||
| void wave_terminate() | |||||
| { | |||||
| if (pulse_running) | |||||
| wave_pulse_terminate(); | |||||
| else | |||||
| wave_port_terminate(); | |||||
| } | |||||
| void wave_flush(void *theHandler) | |||||
| { | |||||
| if (pulse_running) | |||||
| wave_pulse_flush(theHandler); | |||||
| else | |||||
| wave_port_flush(theHandler); | |||||
| } | |||||
| // rename functions to be wrapped | |||||
| #define wave_open wave_port_open | |||||
| #define wave_write wave_port_write | |||||
| #define wave_close wave_port_close | |||||
| #define wave_terminate wave_port_terminate | |||||
| #define wave_flush wave_port_flush | |||||
| #endif | |||||
| #define MAX_SAMPLE_RATE 22050 | |||||
| #define FRAMES_PER_BUFFER 512 | |||||
| #define BUFFER_LENGTH (MAX_SAMPLE_RATE*2*sizeof(uint16_t)) | |||||
| static char myBuffer[BUFFER_LENGTH]; | |||||
| static char *myRead = NULL; | |||||
| static char *myWrite = NULL; | |||||
| static int out_channels = 1; | |||||
| static int my_stream_could_start = 0; | |||||
| static int wave_samplerate; | |||||
| static int mInCallbackFinishedState = false; | |||||
| #if (USE_PORTAUDIO == 18) | |||||
| static PaDeviceID myOutputDevice = 0; | |||||
| static PortAudioStream *pa_stream = NULL; | |||||
| #endif | |||||
| #if (USE_PORTAUDIO == 19) | |||||
| static struct PaStreamParameters myOutputParameters; | |||||
| static PaStream *pa_stream = NULL; | |||||
| #endif | |||||
| static int userdata[4]; | |||||
| static PaError pa_init_err = 0; | |||||
| // time measurement | |||||
| // The read and write position audio stream in the audio stream are measured in ms. | |||||
| // | |||||
| // * When the stream is opened, myReadPosition and myWritePosition are cleared. | |||||
| // * myWritePosition is updated in wave_write. | |||||
| // * myReadPosition is updated in pa_callback (+ sample delay). | |||||
| static uint32_t myReadPosition = 0; // in ms | |||||
| static uint32_t myWritePosition = 0; | |||||
| static void init_buffer() | |||||
| { | |||||
| myWrite = myBuffer; | |||||
| myRead = myBuffer; | |||||
| memset(myBuffer, 0, BUFFER_LENGTH); | |||||
| myReadPosition = myWritePosition = 0; | |||||
| } | |||||
| static unsigned int get_used_mem() | |||||
| { | |||||
| char *aRead = myRead; | |||||
| char *aWrite = myWrite; | |||||
| unsigned int used = 0; | |||||
| assert((aRead >= myBuffer) | |||||
| && (aRead <= myBuffer + BUFFER_LENGTH) | |||||
| && (aWrite >= myBuffer) | |||||
| && (aWrite <= myBuffer + BUFFER_LENGTH)); | |||||
| if (aRead < aWrite) | |||||
| used = aWrite - aRead; | |||||
| else | |||||
| used = aWrite + BUFFER_LENGTH - aRead; | |||||
| return used; | |||||
| } | |||||
| static PaError start_stream() | |||||
| { | |||||
| PaError err; | |||||
| my_stream_could_start = 0; | |||||
| mInCallbackFinishedState = false; | |||||
| err = Pa_StartStream(pa_stream); | |||||
| #if USE_PORTAUDIO == 19 | |||||
| if (err == paStreamIsNotStopped) { | |||||
| // not sure why we need this, but PA v19 seems to need it | |||||
| err = Pa_StopStream(pa_stream); | |||||
| err = Pa_StartStream(pa_stream); | |||||
| } | |||||
| #endif | |||||
| return err; | |||||
| } | |||||
| /* This routine will be called by the PortAudio engine when audio is needed. | |||||
| ** It may called at interrupt level on some machines so don't do anything | |||||
| ** that could mess up the system like calling malloc() or free(). | |||||
| */ | |||||
| #if USE_PORTAUDIO == 18 | |||||
| static int pa_callback(void *inputBuffer, void *outputBuffer, | |||||
| unsigned long framesPerBuffer, PaTimestamp outTime, void *userData) | |||||
| #else | |||||
| static int pa_callback(const void *inputBuffer, void *outputBuffer, | |||||
| long unsigned int framesPerBuffer, const PaStreamCallbackTimeInfo *outTime, | |||||
| PaStreamCallbackFlags flags, void *userData) | |||||
| #endif | |||||
| { | |||||
| (void)inputBuffer; // unused | |||||
| (void)outTime; // unused | |||||
| (void)userData; // unused | |||||
| int aResult = 0; // paContinue | |||||
| char *aWrite = myWrite; | |||||
| size_t n = out_channels*sizeof(uint16_t)*framesPerBuffer; | |||||
| myReadPosition += framesPerBuffer; | |||||
| if (aWrite >= myRead) { | |||||
| if ((size_t)(aWrite - myRead) >= n) { | |||||
| memcpy(outputBuffer, myRead, n); | |||||
| myRead += n; | |||||
| } else { | |||||
| // underflow | |||||
| aResult = 1; // paComplete; | |||||
| mInCallbackFinishedState = true; | |||||
| size_t aUsedMem = 0; | |||||
| aUsedMem = (size_t)(aWrite - myRead); | |||||
| if (aUsedMem) | |||||
| memcpy(outputBuffer, myRead, aUsedMem); | |||||
| char *p = (char *)outputBuffer + aUsedMem; | |||||
| memset(p, 0, n - aUsedMem); | |||||
| myRead = aWrite; | |||||
| } | |||||
| } else { | |||||
| if ((size_t)(myBuffer + BUFFER_LENGTH - myRead) >= n) { | |||||
| memcpy(outputBuffer, myRead, n); | |||||
| myRead += n; | |||||
| } else if ((size_t)(aWrite + BUFFER_LENGTH - myRead) >= n) { | |||||
| int aTopMem = myBuffer + BUFFER_LENGTH - myRead; | |||||
| if (aTopMem) | |||||
| memcpy(outputBuffer, myRead, aTopMem); | |||||
| int aRest = n - aTopMem; | |||||
| if (aRest) { | |||||
| char *p = (char *)outputBuffer + aTopMem; | |||||
| memcpy(p, myBuffer, aRest); | |||||
| } | |||||
| myRead = myBuffer + aRest; | |||||
| } else { | |||||
| // underflow | |||||
| aResult = 1; // paComplete; | |||||
| int aTopMem = myBuffer + BUFFER_LENGTH - myRead; | |||||
| if (aTopMem) | |||||
| memcpy(outputBuffer, myRead, aTopMem); | |||||
| int aRest = aWrite - myBuffer; | |||||
| if (aRest) { | |||||
| char *p = (char *)outputBuffer + aTopMem; | |||||
| memcpy(p, myBuffer, aRest); | |||||
| } | |||||
| size_t aUsedMem = aTopMem + aRest; | |||||
| char *p = (char *)outputBuffer + aUsedMem; | |||||
| memset(p, 0, n - aUsedMem); | |||||
| myRead = aWrite; | |||||
| } | |||||
| } | |||||
| #ifdef ARCH_BIG | |||||
| // BIG-ENDIAN, swap the order of bytes in each sound sample in the portaudio buffer | |||||
| int c; | |||||
| unsigned char *out_ptr; | |||||
| unsigned char *out_end; | |||||
| out_ptr = (unsigned char *)outputBuffer; | |||||
| out_end = out_ptr + framesPerBuffer*2 * out_channels; | |||||
| while (out_ptr < out_end) { | |||||
| c = out_ptr[0]; | |||||
| out_ptr[0] = out_ptr[1]; | |||||
| out_ptr[1] = c; | |||||
| out_ptr += 2; | |||||
| } | |||||
| #endif | |||||
| return aResult; | |||||
| } | |||||
| void wave_flush(void *theHandler) | |||||
| { | |||||
| (void)theHandler; // unused | |||||
| if (my_stream_could_start) | |||||
| start_stream(); | |||||
| } | |||||
| static int wave_open_sound() | |||||
| { | |||||
| PaError err = paNoError; | |||||
| PaError active; | |||||
| #if USE_PORTAUDIO == 18 | |||||
| active = Pa_StreamActive(pa_stream); | |||||
| #else | |||||
| active = Pa_IsStreamActive(pa_stream); | |||||
| #endif | |||||
| if (active == 1) | |||||
| return 0; | |||||
| if (active < 0) { | |||||
| out_channels = 1; | |||||
| #if USE_PORTAUDIO == 18 | |||||
| err = Pa_OpenStream(&pa_stream, | |||||
| // capture parameters | |||||
| paNoDevice, | |||||
| 0, | |||||
| paInt16, | |||||
| NULL, | |||||
| // playback parameters | |||||
| myOutputDevice, | |||||
| out_channels, | |||||
| paInt16, | |||||
| NULL, | |||||
| // general parameters | |||||
| wave_samplerate, FRAMES_PER_BUFFER, 0, | |||||
| paNoFlag, | |||||
| pa_callback, (void *)userdata); | |||||
| if (err == paInvalidChannelCount) { | |||||
| // failed to open with mono, try stereo | |||||
| out_channels = 2; | |||||
| err = Pa_OpenStream(&pa_stream, | |||||
| // capture parameters | |||||
| paNoDevice, | |||||
| 0, | |||||
| paInt16, | |||||
| NULL, | |||||
| // playback parameters | |||||
| myOutputDevice, | |||||
| out_channels, | |||||
| paInt16, | |||||
| NULL, | |||||
| // general parameters | |||||
| wave_samplerate, FRAMES_PER_BUFFER, 0, | |||||
| paNoFlag, | |||||
| pa_callback, (void *)userdata); | |||||
| } | |||||
| mInCallbackFinishedState = false; // v18 only | |||||
| #else | |||||
| myOutputParameters.channelCount = out_channels; | |||||
| unsigned long framesPerBuffer = paFramesPerBufferUnspecified; | |||||
| err = Pa_OpenStream(&pa_stream, | |||||
| NULL, // no input | |||||
| &myOutputParameters, | |||||
| wave_samplerate, | |||||
| framesPerBuffer, | |||||
| paNoFlag, | |||||
| pa_callback, | |||||
| (void *)userdata); | |||||
| if ((err != paNoError) | |||||
| && (err != paInvalidChannelCount)) { | |||||
| fprintf(stderr, "wave_open_sound > Pa_OpenStream : err=%d (%s)\n", err, Pa_GetErrorText(err)); | |||||
| framesPerBuffer = FRAMES_PER_BUFFER; | |||||
| err = Pa_OpenStream(&pa_stream, | |||||
| NULL, // no input | |||||
| &myOutputParameters, | |||||
| wave_samplerate, | |||||
| framesPerBuffer, | |||||
| paNoFlag, | |||||
| pa_callback, | |||||
| (void *)userdata); | |||||
| } | |||||
| if (err == paInvalidChannelCount) { | |||||
| // failed to open with mono, try stereo | |||||
| out_channels = 2; | |||||
| myOutputParameters.channelCount = out_channels; | |||||
| err = Pa_OpenStream(&pa_stream, | |||||
| NULL, // no input | |||||
| &myOutputParameters, | |||||
| wave_samplerate, | |||||
| framesPerBuffer, | |||||
| paNoFlag, | |||||
| pa_callback, | |||||
| (void *)userdata); | |||||
| } | |||||
| mInCallbackFinishedState = false; | |||||
| #endif | |||||
| } | |||||
| return err != paNoError; | |||||
| } | |||||
| static void update_output_parameters(int selectedDevice, const PaDeviceInfo *deviceInfo) | |||||
| { | |||||
| #if (USE_PORTAUDIO == 19) | |||||
| myOutputParameters.device = selectedDevice; | |||||
| myOutputParameters.channelCount = 1; | |||||
| myOutputParameters.sampleFormat = paInt16; | |||||
| // Latency greater than 100ms for avoiding glitches | |||||
| // (e.g. when moving a window in a graphical desktop) | |||||
| // deviceInfo = Pa_GetDeviceInfo(selectedDevice); | |||||
| if (deviceInfo) { | |||||
| double aLatency = deviceInfo->defaultLowOutputLatency; | |||||
| myOutputParameters.suggestedLatency = aLatency; // for faster response ? | |||||
| } else | |||||
| myOutputParameters.suggestedLatency = (double)0.1; // 100ms | |||||
| myOutputParameters.hostApiSpecificStreamInfo = NULL; | |||||
| #else | |||||
| myOutputDevice = selectedDevice; | |||||
| #endif | |||||
| } | |||||
| static const PaDeviceInfo *select_device(const char *device) | |||||
| { | |||||
| #if (USE_PORTAUDIO == 19) | |||||
| int numDevices = Pa_GetDeviceCount(); | |||||
| #else | |||||
| int numDevices = Pa_CountDevices(); | |||||
| #endif | |||||
| if (numDevices < 0) | |||||
| return NULL; | |||||
| #if (USE_PORTAUDIO == 19) | |||||
| PaDeviceIndex i = 0, selectedIndex = 0; | |||||
| #else | |||||
| PaDeviceID i = 0, selectedIndex = 0; | |||||
| #endif | |||||
| const PaDeviceInfo *deviceInfo = NULL; | |||||
| const PaDeviceInfo *selectedDeviceInfo = NULL; | |||||
| if (device == NULL) { | |||||
| #if (USE_PORTAUDIO == 19) | |||||
| selectedIndex = Pa_GetDefaultOutputDevice(); | |||||
| #else | |||||
| selectedIndex = Pa_GetDefaultOutputDeviceID(); | |||||
| #endif | |||||
| selectedDeviceInfo = Pa_GetDeviceInfo(selectedIndex); | |||||
| } | |||||
| if (selectedDeviceInfo == NULL) { | |||||
| for (i = 0; i < numDevices; i++) { | |||||
| deviceInfo = Pa_GetDeviceInfo(i); | |||||
| if (deviceInfo != NULL && !strcmp(device, deviceInfo->name)) { | |||||
| selectedIndex = i; | |||||
| selectedDeviceInfo = deviceInfo; | |||||
| } | |||||
| } | |||||
| } | |||||
| if (selectedDeviceInfo) | |||||
| update_output_parameters(selectedIndex, selectedDeviceInfo); | |||||
| return selectedDeviceInfo; | |||||
| } | |||||
| void *wave_open(int srate, const char *device) | |||||
| { | |||||
| PaError err; | |||||
| pa_stream = NULL; | |||||
| wave_samplerate = srate; | |||||
| mInCallbackFinishedState = false; | |||||
| init_buffer(); | |||||
| // PortAudio sound output library | |||||
| err = Pa_Initialize(); | |||||
| pa_init_err = err; | |||||
| if (err != paNoError) | |||||
| return NULL; | |||||
| static int once = 0; | |||||
| if (!once) { | |||||
| if (!select_device(device)) | |||||
| return NULL; | |||||
| once = 1; | |||||
| } | |||||
| return (void *)1; | |||||
| } | |||||
| static size_t copyBuffer(char *dest, char *src, const size_t theSizeInBytes) | |||||
| { | |||||
| size_t bytes_written = 0; | |||||
| unsigned int i = 0; | |||||
| uint16_t *a_dest = NULL; | |||||
| uint16_t *a_src = NULL; | |||||
| if ((src != NULL) && dest != NULL) { | |||||
| // copy for one channel (mono)? | |||||
| if (out_channels == 1) { | |||||
| memcpy(dest, src, theSizeInBytes); | |||||
| bytes_written = theSizeInBytes; | |||||
| } else { // copy for 2 channels (stereo) | |||||
| a_dest = (uint16_t *)dest; | |||||
| a_src = (uint16_t *)src; | |||||
| for (i = 0; i < theSizeInBytes/2; i++) { | |||||
| a_dest[2*i] = a_src[i]; | |||||
| a_dest[2*i+1] = a_src[i]; | |||||
| } | |||||
| bytes_written = 2*theSizeInBytes; | |||||
| } | |||||
| } | |||||
| return bytes_written; | |||||
| } | |||||
| size_t wave_write(void *theHandler, char *theMono16BitsWaveBuffer, size_t theSize) | |||||
| { | |||||
| (void)theHandler; // unused | |||||
| size_t bytes_written = 0; | |||||
| // space in ringbuffer for the sample needed: 1x mono channel but 2x for 1 stereo channel | |||||
| size_t bytes_to_write = (out_channels == 1) ? theSize : theSize*2; | |||||
| my_stream_could_start = 0; | |||||
| if (pa_stream == NULL) { | |||||
| if (0 != wave_open_sound()) | |||||
| return 0; | |||||
| my_stream_could_start = 1; | |||||
| } | |||||
| assert(BUFFER_LENGTH >= bytes_to_write); | |||||
| if (myWrite >= myBuffer + BUFFER_LENGTH) | |||||
| myWrite = myBuffer; | |||||
| size_t aTotalFreeMem = 0; | |||||
| char *aRead; | |||||
| while (1) { | |||||
| aRead = myRead; | |||||
| // write pointer is before read pointer? | |||||
| if (myWrite >= aRead) | |||||
| aTotalFreeMem = aRead + BUFFER_LENGTH - myWrite; | |||||
| else // read pointer is before write pointer! | |||||
| aTotalFreeMem = aRead - myWrite; | |||||
| if (aTotalFreeMem > 1) { | |||||
| // -1 because myWrite must be different of aRead | |||||
| // otherwise buffer would be considered as empty | |||||
| aTotalFreeMem -= 1; | |||||
| } | |||||
| if (aTotalFreeMem >= bytes_to_write) | |||||
| break; | |||||
| usleep(10000); | |||||
| } | |||||
| aRead = myRead; | |||||
| // write pointer is ahead the read pointer? | |||||
| if (myWrite >= aRead) { | |||||
| // determine remaining free memory to the end of the ringbuffer | |||||
| size_t aFreeMem = myBuffer + BUFFER_LENGTH - myWrite; | |||||
| // is enough linear space available (regardless 1 or 2 channels)? | |||||
| if (aFreeMem >= bytes_to_write) { | |||||
| // copy direct - no wrap around at end of ringbuffer needed | |||||
| myWrite += copyBuffer(myWrite, theMono16BitsWaveBuffer, theSize); | |||||
| } else { // not enough linear space available | |||||
| // 2 channels (stereo)? | |||||
| if (out_channels == 2) { | |||||
| // copy with wrap around at the end of ringbuffer | |||||
| copyBuffer(myWrite, theMono16BitsWaveBuffer, aFreeMem/2); | |||||
| myWrite = myBuffer; | |||||
| myWrite += copyBuffer(myWrite, theMono16BitsWaveBuffer+aFreeMem/2, theSize - aFreeMem/2); | |||||
| } else { // 1 channel (mono) | |||||
| // copy with wrap around at the end of ringbuffer | |||||
| copyBuffer(myWrite, theMono16BitsWaveBuffer, aFreeMem); | |||||
| myWrite = myBuffer; | |||||
| myWrite += copyBuffer(myWrite, theMono16BitsWaveBuffer+aFreeMem, theSize - aFreeMem); | |||||
| } | |||||
| } | |||||
| } else // read pointer is ahead the write pointer | |||||
| myWrite += copyBuffer(myWrite, theMono16BitsWaveBuffer, theSize); | |||||
| bytes_written = bytes_to_write; | |||||
| myWritePosition += theSize/sizeof(uint16_t); // add number of samples | |||||
| if (my_stream_could_start && (get_used_mem() >= out_channels * sizeof(uint16_t) * FRAMES_PER_BUFFER)) | |||||
| start_stream(); | |||||
| return bytes_written; | |||||
| } | |||||
| int wave_close(void *theHandler) | |||||
| { | |||||
| (void)theHandler; // unused | |||||
| static int aStopStreamCount = 0; | |||||
| #if (USE_PORTAUDIO == 19) | |||||
| if (pa_stream == NULL) | |||||
| return 0; | |||||
| if (Pa_IsStreamStopped(pa_stream)) | |||||
| return 0; | |||||
| #else | |||||
| if (pa_stream == NULL) | |||||
| return 0; | |||||
| if (Pa_StreamActive(pa_stream) == false && mInCallbackFinishedState == false) | |||||
| return 0; | |||||
| #endif | |||||
| // Avoid race condition by making sure this function only | |||||
| // gets called once at a time | |||||
| aStopStreamCount++; | |||||
| if (aStopStreamCount != 1) | |||||
| return 0; | |||||
| // Comment from Audacity-1.2.4b adapted to the eSpeak context. | |||||
| // | |||||
| // We got here in one of two ways: | |||||
| // | |||||
| // 1. The calling program calls the espeak_Cancel function and we | |||||
| // therefore want to stop as quickly as possible. | |||||
| // So we use AbortStream(). If this is | |||||
| // the case the portaudio stream is still in the Running state | |||||
| // (see PortAudio state machine docs). | |||||
| // | |||||
| // 2. The callback told PortAudio to stop the stream since it had | |||||
| // reached the end of the selection. | |||||
| // The event polling thread discovered this by noticing that | |||||
| // wave_is_busy() returned false. | |||||
| // wave_is_busy() (which calls Pa_GetStreamActive()) will not return | |||||
| // false until all buffers have finished playing, so we can call | |||||
| // AbortStream without losing any samples. If this is the case | |||||
| // we are in the "callback finished state" (see PortAudio state | |||||
| // machine docs). | |||||
| // | |||||
| // The moral of the story: We can call AbortStream safely, without | |||||
| // losing samples. | |||||
| // | |||||
| // DMM: This doesn't seem to be true; it seems to be necessary to | |||||
| // call StopStream if the callback brought us here, and AbortStream | |||||
| // if the user brought us here. | |||||
| #if (USE_PORTAUDIO == 19) | |||||
| if (pa_stream) { | |||||
| Pa_AbortStream(pa_stream); | |||||
| Pa_CloseStream(pa_stream); | |||||
| pa_stream = NULL; | |||||
| mInCallbackFinishedState = false; | |||||
| } | |||||
| #else | |||||
| if (pa_stream) { | |||||
| if (mInCallbackFinishedState) { | |||||
| Pa_StopStream(pa_stream); | |||||
| } else { | |||||
| Pa_AbortStream(pa_stream); | |||||
| } | |||||
| Pa_CloseStream(pa_stream); | |||||
| pa_stream = NULL; | |||||
| mInCallbackFinishedState = false; | |||||
| } | |||||
| #endif | |||||
| init_buffer(); | |||||
| aStopStreamCount = 0; // last action | |||||
| return 0; | |||||
| } | |||||
| void wave_terminate() | |||||
| { | |||||
| Pa_Terminate(); | |||||
| } | |||||
| #else | |||||
| void *wave_open(int srate, const char *device) | |||||
| { | |||||
| (void)srate; // unused | |||||
| (void)device; // unused | |||||
| return (void *)1; | |||||
| } | |||||
| size_t wave_write(void *theHandler, char *theMono16BitsWaveBuffer, size_t theSize) | |||||
| { | |||||
| (void)theHandler; // unused | |||||
| (void)theMono16BitsWaveBuffer; // unused | |||||
| return theSize; | |||||
| } | |||||
| int wave_close(void *theHandler) | |||||
| { | |||||
| (void)theHandler; // unused | |||||
| return 0; | |||||
| } | |||||
| void wave_terminate() | |||||
| { | |||||
| } | |||||
| void wave_flush(void *theHandler) | |||||
| { | |||||
| (void)theHandler; // unused | |||||
| } | |||||
| #endif |
| /* | |||||
| * Copyright (C) 2007, Gilles Casse <[email protected]> | |||||
| * Copyright (C) 2015-2016 Reece H. Dunn | |||||
| * based on AudioIO.cc (Audacity-1.2.4b) and wavegen.cpp | |||||
| * | |||||
| * This program is free software; you can redistribute it and/or modify | |||||
| * it under the terms of the GNU General Public License as published by | |||||
| * the Free Software Foundation; either version 3 of the License, or | |||||
| * (at your option) any later version. | |||||
| * | |||||
| * This program 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 General Public License | |||||
| * along with this program; if not, see: <http://www.gnu.org/licenses/>. | |||||
| */ | |||||
| #ifndef WAVE_H | |||||
| #define WAVE_H | |||||
| #ifdef __cplusplus | |||||
| extern "C" | |||||
| { | |||||
| #endif | |||||
| extern void *wave_open(int samplerate, const char *device); | |||||
| extern size_t wave_write(void *theHandler, char *theMono16BitsWaveBuffer, size_t theSize); | |||||
| extern int wave_close(void *theHandler); | |||||
| extern void wave_flush(void *theHandler); | |||||
| extern void wave_terminate(); | |||||
| #ifdef __cplusplus | |||||
| } | |||||
| #endif | |||||
| #endif |
| /* | |||||
| * Copyright (C) 2007, Gilles Casse <[email protected]> | |||||
| * Copyright (C) 2015-2016 Reece H. Dunn | |||||
| * eSpeak driver for PulseAudio | |||||
| * based on the XMMS PulseAudio Plugin | |||||
| * | |||||
| * This program is free software; you can redistribute it and/or modify | |||||
| * it under the terms of the GNU General Public License as published by | |||||
| * the Free Software Foundation; either version 3 of the License, or | |||||
| * (at your option) any later version. | |||||
| * | |||||
| * This program 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 General Public License | |||||
| * along with this program; if not, see: <http://www.gnu.org/licenses/>. | |||||
| */ | |||||
| // TBD: | |||||
| // * ARCH_BIG | |||||
| // * uint64? a_timing_info.read_index | |||||
| // * prebuf,... size? | |||||
| // * 0.9.6: pb pulse_free using tlength=8820 (max size never returned -> tlength=10000 ok, but higher drain). | |||||
| // | |||||
| #include "config.h" | |||||
| #include <assert.h> | |||||
| #include <math.h> | |||||
| #include <pthread.h> | |||||
| #include <pulse/pulseaudio.h> | |||||
| #include <stdbool.h> | |||||
| #include <stdio.h> | |||||
| #include <stdlib.h> | |||||
| #include <string.h> | |||||
| #include <sys/time.h> | |||||
| #include <time.h> | |||||
| #include <unistd.h> | |||||
| #include <espeak-ng/espeak_ng.h> | |||||
| #include "speech.h" | |||||
| #include "wave.h" | |||||
| enum { | |||||
| /* return value */ | |||||
| PULSE_OK = 0, | |||||
| PULSE_ERROR = -1, | |||||
| PULSE_NO_CONNECTION = -2 | |||||
| }; | |||||
| #ifdef USE_PULSEAUDIO | |||||
| #define SAMPLE_RATE 22050 | |||||
| #define ESPEAK_FORMAT PA_SAMPLE_S16LE | |||||
| #define ESPEAK_CHANNEL 1 | |||||
| #define MAXLENGTH 132300 | |||||
| #define TLENGTH 4410 | |||||
| #define PREBUF 2200 | |||||
| #define MINREQ 880 | |||||
| #ifdef USE_PORTAUDIO | |||||
| // rename functions to be wrapped | |||||
| #define wave_open wave_pulse_open | |||||
| #define wave_write wave_pulse_write | |||||
| #define wave_close wave_pulse_close | |||||
| #define wave_terminate wave_pulse_terminate | |||||
| #define wave_flush wave_pulse_flush | |||||
| // check whether we can connect to PulseAudio | |||||
| #include <pulse/simple.h> | |||||
| int is_pulse_running() | |||||
| { | |||||
| pa_sample_spec ss; | |||||
| ss.format = ESPEAK_FORMAT; | |||||
| ss.rate = SAMPLE_RATE; | |||||
| ss.channels = ESPEAK_CHANNEL; | |||||
| pa_simple *s = pa_simple_new(NULL, "eSpeak", PA_STREAM_PLAYBACK, NULL, "is_pulse_running", &ss, NULL, NULL, NULL); | |||||
| if (s) { | |||||
| pa_simple_free(s); | |||||
| return 1; | |||||
| } else | |||||
| return 0; | |||||
| } | |||||
| #endif | |||||
| static pthread_mutex_t pulse_mutex; | |||||
| static pa_context *context = NULL; | |||||
| static pa_stream *stream = NULL; | |||||
| static pa_threaded_mainloop *mainloop = NULL; | |||||
| static int do_trigger = 0; | |||||
| static uint64_t written = 0; | |||||
| static int time_offset_msec = 0; | |||||
| static int just_flushed = 0; | |||||
| static int connected = 0; | |||||
| static int wave_samplerate; | |||||
| #define CHECK_DEAD_GOTO(label, warn) do { \ | |||||
| if (!mainloop || \ | |||||
| !context || pa_context_get_state(context) != PA_CONTEXT_READY || \ | |||||
| !stream || pa_stream_get_state(stream) != PA_STREAM_READY) { \ | |||||
| if (warn) \ | |||||
| fprintf(stderr, "Connection died: %s\n", context ? pa_strerror(pa_context_errno(context)) : "NULL"); \ | |||||
| goto label; \ | |||||
| } \ | |||||
| } while (0); | |||||
| #define CHECK_CONNECTED(retval) \ | |||||
| do { \ | |||||
| if (!connected) return retval; \ | |||||
| } while (0); | |||||
| #define CHECK_CONNECTED_NO_RETVAL(id) \ | |||||
| do { \ | |||||
| if (!connected) { return; } \ | |||||
| } while (0); | |||||
| static void subscribe_cb(struct pa_context *c, enum pa_subscription_event_type t, uint32_t index, void *userdata) | |||||
| { | |||||
| (void)userdata; // unused | |||||
| assert(c); | |||||
| if (!stream || | |||||
| index != pa_stream_get_index(stream) || | |||||
| (t != (PA_SUBSCRIPTION_EVENT_SINK_INPUT|PA_SUBSCRIPTION_EVENT_CHANGE) && | |||||
| t != (PA_SUBSCRIPTION_EVENT_SINK_INPUT|PA_SUBSCRIPTION_EVENT_NEW))) | |||||
| return; | |||||
| } | |||||
| static void context_state_cb(pa_context *c, void *userdata) | |||||
| { | |||||
| (void)userdata; // unused | |||||
| assert(c); | |||||
| switch (pa_context_get_state(c)) | |||||
| { | |||||
| case PA_CONTEXT_READY: | |||||
| case PA_CONTEXT_TERMINATED: | |||||
| case PA_CONTEXT_FAILED: | |||||
| pa_threaded_mainloop_signal(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) | |||||
| { | |||||
| (void)userdata; // unused | |||||
| assert(s); | |||||
| switch (pa_stream_get_state(s)) | |||||
| { | |||||
| case PA_STREAM_READY: | |||||
| case PA_STREAM_FAILED: | |||||
| case PA_STREAM_TERMINATED: | |||||
| pa_threaded_mainloop_signal(mainloop, 0); | |||||
| break; | |||||
| case PA_STREAM_UNCONNECTED: | |||||
| case PA_STREAM_CREATING: | |||||
| break; | |||||
| } | |||||
| } | |||||
| static void stream_success_cb(pa_stream *s, int success, void *userdata) | |||||
| { | |||||
| assert(s); | |||||
| if (userdata) | |||||
| *(int *)userdata = success; | |||||
| pa_threaded_mainloop_signal(mainloop, 0); | |||||
| } | |||||
| static void context_success_cb(pa_context *c, int success, void *userdata) | |||||
| { | |||||
| assert(c); | |||||
| if (userdata) | |||||
| *(int *)userdata = success; | |||||
| pa_threaded_mainloop_signal(mainloop, 0); | |||||
| } | |||||
| static void stream_request_cb(pa_stream *s, size_t length, void *userdata) | |||||
| { | |||||
| (void)length; // unused | |||||
| (void)userdata; // unused | |||||
| assert(s); | |||||
| pa_threaded_mainloop_signal(mainloop, 0); | |||||
| } | |||||
| static void stream_latency_update_cb(pa_stream *s, void *userdata) | |||||
| { | |||||
| (void)userdata; // unused | |||||
| assert(s); | |||||
| pa_threaded_mainloop_signal(mainloop, 0); | |||||
| } | |||||
| static int pulse_free(void) | |||||
| { | |||||
| size_t l = 0; | |||||
| pa_operation *o = NULL; | |||||
| CHECK_CONNECTED(0); | |||||
| pa_threaded_mainloop_lock(mainloop); | |||||
| CHECK_DEAD_GOTO(fail, 1); | |||||
| if ((l = pa_stream_writable_size(stream)) == (size_t)-1) { | |||||
| fprintf(stderr, "pa_stream_writable_size() failed: %s", pa_strerror(pa_context_errno(context))); | |||||
| l = 0; | |||||
| goto fail; | |||||
| } | |||||
| /* If this function is called twice with no pulse_write() call in | |||||
| * between this means we should trigger the playback */ | |||||
| if (do_trigger) { | |||||
| int success = 0; | |||||
| if (!(o = pa_stream_trigger(stream, stream_success_cb, &success))) { | |||||
| fprintf(stderr, "pa_stream_trigger() failed: %s", pa_strerror(pa_context_errno(context))); | |||||
| goto fail; | |||||
| } | |||||
| while (pa_operation_get_state(o) != PA_OPERATION_DONE) { | |||||
| CHECK_DEAD_GOTO(fail, 1); | |||||
| pa_threaded_mainloop_wait(mainloop); | |||||
| } | |||||
| if (!success) | |||||
| fprintf(stderr, "pa_stream_trigger() failed: %s", pa_strerror(pa_context_errno(context))); | |||||
| } | |||||
| fail: | |||||
| if (o) | |||||
| pa_operation_unref(o); | |||||
| pa_threaded_mainloop_unlock(mainloop); | |||||
| do_trigger = !!l; | |||||
| return (int)l; | |||||
| } | |||||
| static int pulse_playing(const pa_timing_info *the_timing_info) | |||||
| { | |||||
| int r = 0; | |||||
| const pa_timing_info *i; | |||||
| assert(the_timing_info); | |||||
| CHECK_CONNECTED(0); | |||||
| pa_threaded_mainloop_lock(mainloop); | |||||
| for (;;) { | |||||
| CHECK_DEAD_GOTO(fail, 1); | |||||
| if ((i = pa_stream_get_timing_info(stream))) | |||||
| break; | |||||
| if (pa_context_errno(context) != PA_ERR_NODATA) { | |||||
| fprintf(stderr, "pa_stream_get_timing_info() failed: %s", pa_strerror(pa_context_errno(context))); | |||||
| goto fail; | |||||
| } | |||||
| pa_threaded_mainloop_wait(mainloop); | |||||
| } | |||||
| r = i->playing; | |||||
| memcpy((void *)the_timing_info, (void *)i, sizeof(pa_timing_info)); | |||||
| fail: | |||||
| pa_threaded_mainloop_unlock(mainloop); | |||||
| return r; | |||||
| } | |||||
| static void pulse_write(void *ptr, int length) | |||||
| { | |||||
| CHECK_CONNECTED_NO_RETVAL(); | |||||
| pa_threaded_mainloop_lock(mainloop); | |||||
| CHECK_DEAD_GOTO(fail, 1); | |||||
| if (pa_stream_write(stream, ptr, length, NULL, PA_SEEK_RELATIVE, (pa_seek_mode_t)0) < 0) { | |||||
| fprintf(stderr, "pa_stream_write() failed: %s", pa_strerror(pa_context_errno(context))); | |||||
| goto fail; | |||||
| } | |||||
| do_trigger = 0; | |||||
| written += length; | |||||
| fail: | |||||
| pa_threaded_mainloop_unlock(mainloop); | |||||
| } | |||||
| static int drain(void) | |||||
| { | |||||
| pa_operation *o = NULL; | |||||
| int success = 0; | |||||
| int ret = PULSE_ERROR; | |||||
| CHECK_CONNECTED(ret); | |||||
| pa_threaded_mainloop_lock(mainloop); | |||||
| CHECK_DEAD_GOTO(fail, 0); | |||||
| if (!(o = pa_stream_drain(stream, stream_success_cb, &success))) { | |||||
| fprintf(stderr, "pa_stream_drain() failed: %s\n", pa_strerror(pa_context_errno(context))); | |||||
| goto fail; | |||||
| } | |||||
| while (pa_operation_get_state(o) != PA_OPERATION_DONE) { | |||||
| CHECK_DEAD_GOTO(fail, 1); | |||||
| pa_threaded_mainloop_wait(mainloop); | |||||
| } | |||||
| if (!success) | |||||
| fprintf(stderr, "pa_stream_drain() failed: %s\n", pa_strerror(pa_context_errno(context))); | |||||
| else | |||||
| ret = PULSE_OK; | |||||
| fail: | |||||
| if (o) | |||||
| pa_operation_unref(o); | |||||
| pa_threaded_mainloop_unlock(mainloop); | |||||
| return ret; | |||||
| } | |||||
| static void pulse_close(void) | |||||
| { | |||||
| drain(); | |||||
| connected = 0; | |||||
| if (mainloop) | |||||
| pa_threaded_mainloop_stop(mainloop); | |||||
| connected = 0; | |||||
| if (context) { | |||||
| pa_context_disconnect(context); | |||||
| pa_context_unref(context); | |||||
| context = NULL; | |||||
| } | |||||
| if (mainloop) { | |||||
| pa_threaded_mainloop_free(mainloop); | |||||
| mainloop = NULL; | |||||
| } | |||||
| } | |||||
| static int pulse_open(const char *device) | |||||
| { | |||||
| pa_sample_spec ss; | |||||
| pa_operation *o = NULL; | |||||
| int success; | |||||
| int ret = PULSE_ERROR; | |||||
| assert(!mainloop); | |||||
| assert(!context); | |||||
| assert(!stream); | |||||
| assert(!connected); | |||||
| pthread_mutex_init(&pulse_mutex, (const pthread_mutexattr_t *)NULL); | |||||
| ss.format = ESPEAK_FORMAT; | |||||
| ss.rate = wave_samplerate; | |||||
| ss.channels = ESPEAK_CHANNEL; | |||||
| if (!pa_sample_spec_valid(&ss)) | |||||
| return false; | |||||
| if (!(mainloop = pa_threaded_mainloop_new())) | |||||
| goto fail; | |||||
| pa_threaded_mainloop_lock(mainloop); | |||||
| if (!(context = pa_context_new(pa_threaded_mainloop_get_api(mainloop), "eSpeak"))) | |||||
| goto fail; | |||||
| pa_context_set_state_callback(context, context_state_cb, NULL); | |||||
| pa_context_set_subscribe_callback(context, subscribe_cb, NULL); | |||||
| if (pa_context_connect(context, NULL, (pa_context_flags_t)0, NULL) < 0) { | |||||
| fprintf(stderr, "Failed to connect to server: %s", pa_strerror(pa_context_errno(context))); | |||||
| ret = PULSE_NO_CONNECTION; | |||||
| goto fail; | |||||
| } | |||||
| if (pa_threaded_mainloop_start(mainloop) < 0) | |||||
| goto fail; | |||||
| // Wait until the context is ready | |||||
| pa_threaded_mainloop_wait(mainloop); | |||||
| if (pa_context_get_state(context) != PA_CONTEXT_READY) { | |||||
| fprintf(stderr, "Failed to connect to server: %s", pa_strerror(pa_context_errno(context))); | |||||
| ret = PULSE_NO_CONNECTION; | |||||
| if (mainloop) | |||||
| pa_threaded_mainloop_stop(mainloop); | |||||
| goto fail; | |||||
| } | |||||
| if (!(stream = pa_stream_new(context, "unknown", &ss, NULL))) { | |||||
| fprintf(stderr, "Failed to create stream: %s", pa_strerror(pa_context_errno(context))); | |||||
| goto fail; | |||||
| } | |||||
| pa_stream_set_state_callback(stream, stream_state_cb, NULL); | |||||
| pa_stream_set_write_callback(stream, stream_request_cb, NULL); | |||||
| pa_stream_set_latency_update_callback(stream, stream_latency_update_cb, NULL); | |||||
| pa_buffer_attr a_attr; | |||||
| a_attr.maxlength = MAXLENGTH; | |||||
| a_attr.tlength = TLENGTH; | |||||
| a_attr.prebuf = PREBUF; | |||||
| a_attr.minreq = MINREQ; | |||||
| a_attr.fragsize = 0; | |||||
| if (pa_stream_connect_playback(stream, device, &a_attr, (pa_stream_flags_t)(PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_AUTO_TIMING_UPDATE), NULL, NULL) < 0) { | |||||
| fprintf(stderr, "Failed to connect stream: %s", pa_strerror(pa_context_errno(context))); | |||||
| goto fail; | |||||
| } | |||||
| // Wait until the stream is ready | |||||
| pa_threaded_mainloop_wait(mainloop); | |||||
| if (pa_stream_get_state(stream) != PA_STREAM_READY) { | |||||
| fprintf(stderr, "Failed to connect stream: %s", pa_strerror(pa_context_errno(context))); | |||||
| goto fail; | |||||
| } | |||||
| // Now subscribe to events | |||||
| if (!(o = pa_context_subscribe(context, PA_SUBSCRIPTION_MASK_SINK_INPUT, context_success_cb, &success))) { | |||||
| fprintf(stderr, "pa_context_subscribe() failed: %s", pa_strerror(pa_context_errno(context))); | |||||
| goto fail; | |||||
| } | |||||
| while (pa_operation_get_state(o) != PA_OPERATION_DONE) { | |||||
| CHECK_DEAD_GOTO(fail, 1); | |||||
| pa_threaded_mainloop_wait(mainloop); | |||||
| } | |||||
| pa_operation_unref(o); | |||||
| do_trigger = 0; | |||||
| written = 0; | |||||
| time_offset_msec = 0; | |||||
| just_flushed = 0; | |||||
| connected = 1; | |||||
| pa_threaded_mainloop_unlock(mainloop); | |||||
| return PULSE_OK; | |||||
| fail: | |||||
| if (mainloop) | |||||
| pa_threaded_mainloop_unlock(mainloop); | |||||
| if (ret == PULSE_NO_CONNECTION) { | |||||
| if (context) { | |||||
| pa_context_disconnect(context); | |||||
| pa_context_unref(context); | |||||
| context = NULL; | |||||
| } | |||||
| if (mainloop) { | |||||
| pa_threaded_mainloop_free(mainloop); | |||||
| mainloop = NULL; | |||||
| } | |||||
| } else | |||||
| pulse_close(); | |||||
| return ret; | |||||
| } | |||||
| void wave_flush(void *theHandler) | |||||
| { | |||||
| (void)theHandler; // unused | |||||
| } | |||||
| void *wave_open(int srate, const char *device) | |||||
| { | |||||
| stream = NULL; | |||||
| wave_samplerate = srate; | |||||
| if (pulse_open(device) != PULSE_OK) | |||||
| return NULL; | |||||
| return (void *)1; | |||||
| } | |||||
| size_t wave_write(void *theHandler, char *theMono16BitsWaveBuffer, size_t theSize) | |||||
| { | |||||
| (void)theHandler; // unused | |||||
| size_t bytes_to_write = theSize; | |||||
| char *aBuffer = theMono16BitsWaveBuffer; | |||||
| assert(stream); | |||||
| size_t aTotalFreeMem = 0; | |||||
| pthread_mutex_lock(&pulse_mutex); | |||||
| while (1) { | |||||
| aTotalFreeMem = pulse_free(); | |||||
| if (aTotalFreeMem >= bytes_to_write) | |||||
| break; | |||||
| // TBD: check if really helpful | |||||
| if (aTotalFreeMem >= MAXLENGTH*2) | |||||
| aTotalFreeMem = MAXLENGTH*2; | |||||
| // 500: threshold for avoiding too many calls to pulse_write | |||||
| if (aTotalFreeMem > 500) { | |||||
| pulse_write(aBuffer, aTotalFreeMem); | |||||
| bytes_to_write -= aTotalFreeMem; | |||||
| aBuffer += aTotalFreeMem; | |||||
| } | |||||
| usleep(10000); | |||||
| } | |||||
| pulse_write(aBuffer, bytes_to_write); | |||||
| terminate: | |||||
| pthread_mutex_unlock(&pulse_mutex); | |||||
| return theSize; | |||||
| } | |||||
| int wave_close(void *theHandler) | |||||
| { | |||||
| (void)theHandler; // unused | |||||
| static int aStopStreamCount = 0; | |||||
| // Avoid race condition by making sure this function only | |||||
| // gets called once at a time | |||||
| aStopStreamCount++; | |||||
| if (aStopStreamCount != 1) | |||||
| return 0; | |||||
| int a_status = pthread_mutex_lock(&pulse_mutex); | |||||
| if (a_status) { | |||||
| aStopStreamCount = 0; // last action | |||||
| return PULSE_ERROR; | |||||
| } | |||||
| drain(); | |||||
| pthread_mutex_unlock(&pulse_mutex); | |||||
| aStopStreamCount = 0; // last action | |||||
| return PULSE_OK; | |||||
| } | |||||
| void wave_terminate() | |||||
| { | |||||
| pthread_mutex_t *a_mutex = NULL; | |||||
| a_mutex = &pulse_mutex; | |||||
| pthread_mutex_lock(a_mutex); | |||||
| pulse_close(); | |||||
| pthread_mutex_unlock(a_mutex); | |||||
| pthread_mutex_destroy(a_mutex); | |||||
| } | |||||
| #endif |
| /* | |||||
| * Copyright (C) 2008, Sun Microsystems, Inc. | |||||
| * Copyright (C) 2015-2016 Reece H. Dunn | |||||
| * eSpeak driver for Solaris Audio Device Architecture (SADA) | |||||
| * Written by Willie Walker, based on the eSpeak PulseAudio driver | |||||
| * from Gilles Casse | |||||
| * | |||||
| * This program is free software; you can redistribute it and/or modify | |||||
| * it under the terms of the GNU General Public License as published by | |||||
| * the Free Software Foundation; either version 3 of the License, or | |||||
| * (at your option) any later version. | |||||
| * | |||||
| * This program 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 General Public License | |||||
| * along with this program; if not, see: <http://www.gnu.org/licenses/>. | |||||
| */ | |||||
| #include "config.h" | |||||
| #include <assert.h> | |||||
| #include <errno.h> | |||||
| #include <fcntl.h> | |||||
| #include <stdlib.h> | |||||
| #include <string.h> | |||||
| #include <stropts.h> | |||||
| #include <sys/audioio.h> | |||||
| #include <unistd.h> | |||||
| #include <espeak-ng/espeak_ng.h> | |||||
| #include "speech.h" | |||||
| #include "wave.h" | |||||
| #define SAMPLE_RATE 22050 | |||||
| #define SAMPLE_SIZE 16 | |||||
| #ifdef USE_SADA | |||||
| static const char *sun_audio_device = "/dev/audio"; | |||||
| static int sun_audio_fd = -1; | |||||
| // The total number of 16-bit samples sent to be played via the | |||||
| // wave_write method. | |||||
| // | |||||
| static uint32_t total_samples_sent; | |||||
| // The total number of samples sent to be played via the wave_write | |||||
| // method, but which were never played because of a call to | |||||
| // wave_close. | |||||
| // | |||||
| static uint32_t total_samples_skipped; | |||||
| // The last known playing index after a call to wave_close. | |||||
| // | |||||
| static uint32_t last_play_position = 0; | |||||
| static uint32_t wave_samplerate; | |||||
| static int wave_get_remaining_time(uint32_t sample, uint32_t *time); | |||||
| // wave_open | |||||
| // | |||||
| // DESCRIPTION: | |||||
| // | |||||
| // initializes the audio subsytem. | |||||
| // | |||||
| // GLOBALS USED/MODIFIED: | |||||
| // | |||||
| // sun_audio_fd: modified to hold the file descriptor of the opened | |||||
| // audio device. | |||||
| // | |||||
| void *wave_open(int srate, const char *device) | |||||
| { | |||||
| if (device == NULL) | |||||
| device = sun_audio_device; | |||||
| audio_info_t ainfo; | |||||
| wave_samplerate = srate; | |||||
| if ((sun_audio_fd = open(device, O_WRONLY)) < 0) { | |||||
| fprintf(stderr, "wave_open() could not open: %s (%d)\n", device, sun_audio_fd); | |||||
| return NULL; | |||||
| } | |||||
| ioctl(sun_audio_fd, AUDIO_GETINFO, &ainfo); | |||||
| ainfo.play.encoding = AUDIO_ENCODING_LINEAR; | |||||
| ainfo.play.channels = 1; | |||||
| ainfo.play.sample_rate = wave_samplerate; | |||||
| ainfo.play.precision = SAMPLE_SIZE; | |||||
| if (ioctl(sun_audio_fd, AUDIO_SETINFO, &ainfo) == -1) { | |||||
| fprintf(stderr, "wave_open() failed to set audio params: %s\n", strerror(errno)); | |||||
| close(sun_audio_fd); | |||||
| return NULL; | |||||
| } | |||||
| return (void *)sun_audio_fd; | |||||
| } | |||||
| // wave_write | |||||
| // | |||||
| // DESCRIPTION: | |||||
| // | |||||
| // Meant to be asynchronous, it supplies the wave sample to the lower | |||||
| // audio layer and returns. The sample is played later on. [[[WDW - | |||||
| // we purposely do not open the audio device as non-blocking because | |||||
| // managing that would be a pain. So, we rely a lot upon fifo.cpp and | |||||
| // event.cpp to not overload us, allowing us to get away with a | |||||
| // blocking write. event.cpp:polling_thread in particular appears to | |||||
| // use get_remaining_time to prevent flooding.]]] | |||||
| // | |||||
| // PARAMETERS: | |||||
| // | |||||
| // theHandler: the audio device file descriptor | |||||
| // theMono16BitsWaveBuffer: the audio data | |||||
| // theSize: the number of bytes (not 16-bit samples) | |||||
| // | |||||
| // GLOBALS USED/MODIFIED: | |||||
| // | |||||
| // total_samples_sent: modified based upon 16-bit samples sent | |||||
| // | |||||
| // RETURNS: | |||||
| // | |||||
| // the number of bytes (not 16-bit samples) sent | |||||
| // | |||||
| size_t wave_write(void *theHandler, | |||||
| char *theMono16BitsWaveBuffer, | |||||
| size_t theSize) | |||||
| { | |||||
| size_t num; | |||||
| #if defined(BYTE_ORDER) && BYTE_ORDER == BIG_ENDIAN | |||||
| // BIG-ENDIAN, swap the order of bytes in each sound sample | |||||
| int c; | |||||
| char *out_ptr; | |||||
| char *out_end; | |||||
| out_ptr = (char *)theMono16BitsWaveBuffer; | |||||
| out_end = out_ptr + theSize; | |||||
| while (out_ptr < out_end) { | |||||
| c = out_ptr[0]; | |||||
| out_ptr[0] = out_ptr[1]; | |||||
| out_ptr[1] = c; | |||||
| out_ptr += 2; | |||||
| } | |||||
| #endif | |||||
| num = write((int)theHandler, theMono16BitsWaveBuffer, theSize); | |||||
| // Keep track of the total number of samples sent -- we use this in | |||||
| // wave_get_read_position and also use it to help calculate the | |||||
| // total_samples_skipped in wave_close. | |||||
| // | |||||
| total_samples_sent += num / 2; | |||||
| return num; | |||||
| } | |||||
| // wave_close | |||||
| // | |||||
| // DESCRIPTION: | |||||
| // | |||||
| // Does what SADA normally would call a flush, which means to cease | |||||
| // all audio production in progress and throw any remaining audio | |||||
| // away. [[[WDW - see comment in wave_flush.]]] | |||||
| // | |||||
| // PARAMETERS: | |||||
| // | |||||
| // theHandler: the audio device file descriptor | |||||
| // | |||||
| // GLOBALS USED/MODIFIED: | |||||
| // | |||||
| // last_play_position: modified to reflect play position the last time | |||||
| // this method was called | |||||
| // total_samples_sent: used to help calculate total_samples_skipped | |||||
| // total_samples_skipped: modified to hold the total number of 16-bit | |||||
| // samples sent to wave_write, but which were | |||||
| // never played | |||||
| // sun_audio_fd: used because some calls to wave_close seem to | |||||
| // pass a NULL for theHandler for some odd reason | |||||
| // | |||||
| // RETURNS: | |||||
| // | |||||
| // The result of the ioctl call (non-0 means failure) | |||||
| // | |||||
| int wave_close(void *theHandler) | |||||
| { | |||||
| int ret; | |||||
| audio_info_t ainfo; | |||||
| int audio_fd = (int)theHandler; | |||||
| if (!audio_fd) | |||||
| audio_fd = sun_audio_fd; | |||||
| // [[[WDW: maybe do a pause/resume ioctl???]]] | |||||
| ret = ioctl(audio_fd, I_FLUSH, FLUSHRW); | |||||
| ioctl(audio_fd, AUDIO_GETINFO, &ainfo); | |||||
| // Calculate the number of samples that won't get | |||||
| // played. We also keep track of the last_play_position | |||||
| // because wave_close can be called multiple times | |||||
| // before another call to wave_write. | |||||
| // | |||||
| if (last_play_position != ainfo.play.samples) { | |||||
| last_play_position = ainfo.play.samples; | |||||
| total_samples_skipped = total_samples_sent - last_play_position; | |||||
| } | |||||
| return ret; | |||||
| } | |||||
| // wave_is_busy | |||||
| // | |||||
| // DESCRIPTION: | |||||
| // | |||||
| // Returns a non-0 value if audio is being played. | |||||
| // | |||||
| // PARAMETERS: | |||||
| // | |||||
| // theHandler: the audio device file descriptor | |||||
| // | |||||
| // GLOBALS USED/MODIFIED: | |||||
| // | |||||
| // sun_audio_fd: used because some calls to wave_is_busy seem to | |||||
| // pass a NULL for theHandler for some odd reason | |||||
| // | |||||
| // RETURNS: | |||||
| // | |||||
| // A non-0 value if audio is being played | |||||
| // | |||||
| int wave_is_busy(void *theHandler) | |||||
| { | |||||
| (void)theHandler; // unused | |||||
| uint32_t time; | |||||
| if (total_samples_sent >= 1) | |||||
| wave_get_remaining_time(total_samples_sent - 1, &time); | |||||
| else | |||||
| time = 0; | |||||
| return time != 0; | |||||
| } | |||||
| // wave_terminate | |||||
| // | |||||
| // DESCRIPTION: | |||||
| // | |||||
| // Used to end our session with eSpeak. | |||||
| // | |||||
| // GLOBALS USED/MODIFIED: | |||||
| // | |||||
| // sun_audio_fd: modified - closed and set to -1 | |||||
| // | |||||
| void wave_terminate() | |||||
| { | |||||
| close(sun_audio_fd); | |||||
| sun_audio_fd = -1; | |||||
| } | |||||
| // wave_flush | |||||
| // | |||||
| // DESCRIPTION: | |||||
| // | |||||
| // Appears to want to tell the audio subsystem to make sure it plays | |||||
| // the audio. In our case, the system is already doing this, so this | |||||
| // is basically a no-op. [[[WDW - if you do a drain, you block, so | |||||
| // don't do that. In addition the typical SADA notion of flush is | |||||
| // currently handled by wave_close. I think this is most likely just | |||||
| // terminology conflict between eSpeak and SADA.]]] | |||||
| // | |||||
| // PARAMETERS: | |||||
| // | |||||
| // theHandler: the audio device file descriptor | |||||
| // | |||||
| void wave_flush(void *theHandler) | |||||
| { | |||||
| (void)theHandler; // unused | |||||
| } | |||||
| // wave_get_remaining_time | |||||
| // | |||||
| // DESCRIPTION: | |||||
| // | |||||
| // Returns the remaining time (in ms) before the sample is played. | |||||
| // The sample in this case is a return value from a previous call to | |||||
| // wave_get_write_position. | |||||
| // | |||||
| // PARAMETERS: | |||||
| // | |||||
| // sample: an index returned from wave_get_write_position representing | |||||
| // an index into the long continuous stream of 16-bit samples | |||||
| // time: a return value representing the delay in milliseconds until | |||||
| // sample is played. A value of 0 means the sample is either | |||||
| // currently being played or it has already been played. | |||||
| // | |||||
| // GLOBALS USED/MODIFIED: | |||||
| // | |||||
| // sun_audio_fd: used to determine total number of samples played by | |||||
| // the audio system | |||||
| // total_samples_skipped: used in remaining time calculation | |||||
| // | |||||
| // RETURNS: | |||||
| // | |||||
| // Time in milliseconds before the sample is played or 0 if the sample | |||||
| // is currently playing or has already been played. | |||||
| // | |||||
| int wave_get_remaining_time(uint32_t sample, uint32_t *time) | |||||
| { | |||||
| uint32_t a_time = 0; | |||||
| uint32_t actual_index; | |||||
| audio_info_t ainfo; | |||||
| if (!time) | |||||
| return -1; | |||||
| ioctl(sun_audio_fd, AUDIO_GETINFO, &ainfo); | |||||
| // See if this sample has already been played or is currently | |||||
| // playing. | |||||
| // | |||||
| actual_index = sample - total_samples_skipped; | |||||
| if ((sample < total_samples_skipped) || | |||||
| (actual_index <= ainfo.play.samples)) | |||||
| *time = 0; | |||||
| else { | |||||
| a_time = ((actual_index - ainfo.play.samples) * 1000) / wave_samplerate; | |||||
| *time = (uint32_t)a_time; | |||||
| } | |||||
| return 0; | |||||
| } | |||||
| #endif |