diff --git a/README.md b/README.md index 9028d0b..e1395b0 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ espeakup yet, espeakup just uses meson, so you should be able to change to the source directory, then type: ```bash -meson . ./build +meson setup . ./build cd ./build ninja sudo ninja install diff --git a/doc/espeakup.8.ronn b/doc/espeakup.8.ronn index 40b9d9c..598611c 100644 --- a/doc/espeakup.8.ronn +++ b/doc/espeakup.8.ronn @@ -71,7 +71,7 @@ espeak-ng can be found at [github](https://github.com/espeak-ng/espeak-ng) William Hubbs is the author of espeakup. -This manual page was written by Chris Brannon . +This manual page was written by Chris Brannon . current authors and maintainers can be found at [github](https://github.com/linux-speakup/espeakup/graphs/contributors) diff --git a/doc/meson.build b/doc/meson.build index 9645649..f055bb4 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -1,8 +1,8 @@ ronn = find_program('ronn', required: get_option('man')) - if ronn.found() - custom_target('man', input:files('espeakup.8.ronn'), - output:'espeakup.8', - command:[ronn, '--output-dir', '@OUTDIR@', '--roff', '@INPUT@'], - install: true, install_dir: join_paths(get_option('mandir'),'man8')) + custom_target('man', + input : files('espeakup.8.ronn'), + output : 'espeakup.8', + command : [ronn, '--output-dir', '@OUTDIR@', '--roff', '@INPUT@'], + install : true, install_dir: join_paths(get_option('mandir'),'man8')) endif diff --git a/meson.build b/meson.build index 2ba7c86..38788d2 100644 --- a/meson.build +++ b/meson.build @@ -1,8 +1,12 @@ project('espeakup', 'c', - default_options : ['buildtype=debugoptimized', 'c_std=gnu11', 'warning_level=3'], + default_options : [ + 'buildtype=debugoptimized', + 'c_std=gnu11', + 'warning_level=3' + ], license : 'GPL-3.0-or-later', version : '0.90', - meson_version : '>=0.47.0') + meson_version : '>=0.51.0') cc = meson.get_compiler('c') thread_dep = dependency('threads') @@ -15,7 +19,7 @@ subdir('services') subdir('src') executable('espeakup', - espeakup_version, - espeakup_sources, - dependencies : [thread_dep, espeak_dep, alsa_dep, math_dep], - install : true) + espeakup_version, + espeakup_sources, + dependencies : [thread_dep, espeak_dep, alsa_dep, math_dep], + install : true) diff --git a/meson_options.txt b/meson_options.txt index c7e35d9..c8560b6 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,4 +1,4 @@ -option('systemd', type : 'feature', value : 'auto', - description :'enable systemd support') option('man', type : 'feature', value : 'auto', description : 'build manpage with ronn') +option('systemd', type : 'feature', value : 'auto', + description :'enable systemd support') diff --git a/services/meson.build b/services/meson.build index 0be40f1..5eadcac 100644 --- a/services/meson.build +++ b/services/meson.build @@ -1,4 +1,4 @@ -systemd = dependency('systemd', required: get_option('systemd')) -if systemd.found() +systemd = dependency('systemd') +if (systemd.found() and get_option('systemd').allowed()) or get_option('systemd').enabled() subdir('systemd') endif diff --git a/services/systemd/espeakup.service.in b/services/systemd/espeakup.service.in index ec5f4da..075a717 100644 --- a/services/systemd/espeakup.service.in +++ b/services/systemd/espeakup.service.in @@ -1,17 +1,18 @@ [Unit] Description=Software speech output for Speakup Documentation=man:espeakup(8) -Wants=systemd-udev-settle.service -After=systemd-udev-settle.service sound.target +Wants=modprobe@speakup_soft.service +After=modprobe@speakup_soft.service sound.target [Service] Type=forking PIDFile=/run/espeakup.pid Environment="default_voice=" -ExecStartPre=+modprobe speakup_soft ExecStart=@bindir@/espeakup --default-voice=${default_voice} ExecReload=kill -HUP $MAINPID Restart=always +Nice=-10 +OOMScoreAdjust=-900 [Install] WantedBy=sound.target diff --git a/services/systemd/meson.build b/services/systemd/meson.build index 4a693ed..1a15303 100644 --- a/services/systemd/meson.build +++ b/services/systemd/meson.build @@ -1,17 +1,20 @@ -unitdir = systemd.get_pkgconfig_variable('systemdsystemunitdir') +if systemd.found() + unitdir = systemd.get_variable(pkgconfig: 'systemdsystemunitdir') +else + unitdir = join_paths(prefixdir, get_option('libdir'), 'systemd', 'system') +endif prefixdir = get_option('prefix') bindir = join_paths(prefixdir, get_option('bindir')) unit_conf = configuration_data() - unit_conf.set('bindir', bindir) service_file = configure_file( - input : 'espeakup.service.in', - output : 'espeakup.service', - configuration : unit_conf + input : 'espeakup.service.in', + output : 'espeakup.service', + configuration : unit_conf ) install_data(service_file, - install_dir : unitdir + install_dir : unitdir ) diff --git a/src/cli.c b/src/cli.c index a4cf48f..8453587 100644 --- a/src/cli.c +++ b/src/cli.c @@ -23,6 +23,7 @@ #include #include "espeakup.h" +#include "stringhandling.h" #include "version.h" /* pid path */ @@ -72,18 +73,15 @@ static void show_version(void) void process_cli(int argc, char **argv) { int opt; - char *cp; do { opt = getopt_long(argc, argv, shortOptions, longOptions, NULL); switch (opt) { - case 'p': - cp = strdup(optarg); - if (cp != NULL) - pidPath = cp; + case 'P': + pidPath = dupeString(optarg); break; case 'V': - defaultVoice = strdup(optarg); + defaultVoice = dupeString(optarg); break; case 'a': espeakup_mode = ESPEAKUP_MODE_ACSINT; diff --git a/src/espeak.c b/src/espeak.c index 135c6b5..c9ac908 100644 --- a/src/espeak.c +++ b/src/espeak.c @@ -21,9 +21,12 @@ #include #include #include +#include #include #include #include +#include +#include #include "espeakup.h" @@ -47,9 +50,31 @@ const int volumeMultiplier = 22; volatile int stop_requested = 0; int paused_espeak = 1; +/* Wedged-engine detection. Espeak may legitimately refuse entries for a + * while (EE_BUFFER_FULL while a long backlog is being played back), so a + * failing entry is normally just retried. But when entries keep failing + * while the synth callback reports no progress at all, the audio output + * is most likely wedged (e.g. an ALSA device stuck returning EBUSY). + * After ESPEAK_STALL_RETRIES consecutive such retries (about one second + * each), restart the engine; after ESPEAK_MAX_RESTARTS restarts without + * the engine having been healthy for ESPEAK_HEALTHY_SECS in between, + * give up and exit, so that the init system can respawn us. */ +#define ESPEAK_STALL_RETRIES 10 +#define ESPEAK_MAX_RESTARTS 3 +#define ESPEAK_HEALTHY_SECS 60 + +/* Set by the synth callback whenever espeak makes synthesis progress; + * used to tell a merely backlogged engine from a wedged one. The + * callback runs in espeak's own thread, so the flag is atomic. */ +static atomic_int synth_progressed = 0; +static int stalled_retries = 0; +static int restart_attempts = 0; +static struct timespec last_restart; + static int callback(short *wav, int numsamples, espeak_EVENT *events) { int i; + atomic_store(&synth_progressed, 1); for (i = 0; events[i].type != espeakEVENT_LIST_TERMINATED; i++) { if (events[i].type == espeakEVENT_MARK) { int mark = atoi(events[i].id.name); @@ -108,12 +133,31 @@ static espeak_ERROR set_punctuation(struct synth_t *s, int punct, enum adjust_t adj) { espeak_ERROR rc; + espeak_PUNCT_TYPE espeak_punct; if (adj == ADJ_DEC) punct = -punct; if (adj != ADJ_SET) punct += s->punct; - rc = espeak_SetParameter(espeakPUNCTUATION, punct, 0); + + switch (punct) { + case 0: + espeak_punct = espeakPUNCT_NONE; + break; + case 1: + espeak_punct = espeakPUNCT_SOME; + break; + case 2: + /* XXX: approximation */ + espeak_punct = espeakPUNCT_SOME; + break; + case 3: + default: + espeak_punct = espeakPUNCT_ALL; + break; + } + + rc = espeak_SetParameter(espeakPUNCTUATION, espeak_punct, 0); if (rc == EE_OK) s->punct = punct; return rc; @@ -258,15 +302,39 @@ static espeak_ERROR speak_text(struct synth_t *s) synth_mode |= espeakSSML; if (espeakup_mode == ESPEAKUP_MODE_SPEAKUP && (s->len == 1)) { - char *buf; + char *buf = NULL; int n; - if (s->buf[0] == ' ') + unsigned char c = s->buf[0]; + if (c == ' ') n = asprintf(&buf, " "); - else - n = asprintf(&buf, - "%c", - s->buf[0]); + else if (c < 0x20 || c > 0x7e) { + /* Not a printable character; do not embed it in SSML, as + * that would produce invalid markup. Fall through to the + * raw-synthesis path below. */ + n = -1; + } else { + /* Escape characters that are special in XML/SSML so the + * resulting markup stays well-formed; otherwise espeak-ng + * misparses the element, which is the source of + * the spurious high-pitched "ringing" on these characters. */ + const char *entity = NULL; + switch (c) { + case '<': entity = "<"; break; + case '>': entity = ">"; break; + case '&': entity = "&"; break; + case '\'': entity = "'"; break; + case '"': entity = """; break; + } + if (entity) + n = asprintf(&buf, + "%s", + entity); + else + n = asprintf(&buf, + "%c", + c); + } if (n == -1) { /* D'oh. Not much to do on allocation failure. * Perhaps espeak will happen to say the character */ @@ -301,7 +369,7 @@ static void synth_queue_clear() } } -static void reinitialize_espeak(struct synth_t *s) +static int reinitialize_espeak(struct synth_t *s) { int rate; @@ -309,7 +377,7 @@ static void reinitialize_espeak(struct synth_t *s) rate = espeak_Initialize(AUDIO_OUTPUT_PLAYBACK, 0, NULL, 0); if (rate < 0) { fprintf(stderr, "Unable to initialize espeak.\n"); - return; + return -1; } espeak_SetSynthCallback(callback); @@ -322,24 +390,88 @@ static void reinitialize_espeak(struct synth_t *s) espeak_SetParameter(espeakVOLUME, (s->volume + 1) * volumeMultiplier, 0); espeak_SetParameter(espeakCAPITALS, 0, 0); paused_espeak = 0; - return; + return 0; } +/* Wait for up to a second before retrying an entry which could not be + * processed, so that we do not busy-loop on a persistent error. Called + * and returns with queue_guard held. Wakes up immediately if a stop is + * requested. */ +static void espeak_wait_retry(void) +{ + struct timespec timeout; + + clock_gettime(CLOCK_MONOTONIC, &timeout); + timeout.tv_sec++; + pthread_cond_timedwait(&wake_stop, &queue_guard, &timeout); +} + +/* Handle an entry which could not be processed. Called and returns + * with queue_guard held. + * Normally just back off before the retry, but watch out for a wedged + * engine: if entries keep failing while the synth callback shows no + * progress at all, restart the engine, and if restarting does not help + * either, exit so that the init system respawns us in a clean state. */ +static void espeak_handle_failure(struct synth_t *s) +{ + if (atomic_exchange(&synth_progressed, 0)) { + /* Espeak is making progress, it is merely backlogged. */ + stalled_retries = 0; + } else if (++stalled_retries >= ESPEAK_STALL_RETRIES) { + stalled_retries = 0; + if (++restart_attempts > ESPEAK_MAX_RESTARTS) { + fprintf(stderr, "espeakup: espeak keeps failing without " + "making progress and restarting it did not help, " + "aborting\n"); + /* Use _exit because exit could hang in library destructors + * while the audio device is wedged. */ + _exit(3); + } + fprintf(stderr, "espeakup: espeak has been failing without " + "making progress for %d seconds, restarting it\n", + ESPEAK_STALL_RETRIES); + /* Call into espeak with queue_guard released: these calls can + * take time, or block on a wedged audio device. If they do + * block forever, the stop-acknowledgement timeout in the + * softsynth thread is our last resort. */ + pthread_mutex_unlock(&queue_guard); + if (!paused_espeak) { + espeak_Cancel(); + espeak_Terminate(); + paused_espeak = 1; + } + reinitialize_espeak(s); + clock_gettime(CLOCK_MONOTONIC, &last_restart); + pthread_mutex_lock(&queue_guard); + return; + } + + espeak_wait_retry(); +} + +static struct espeak_entry_t *current = NULL; static void queue_process_entry(struct synth_t *s) { - espeak_ERROR error; + espeak_ERROR error = EE_OK; char markbuff[50]; - static struct espeak_entry_t *current = NULL; if (current != queue_peek(synth_queue)) { if (current) free_espeak_entry(current); - current = (struct espeak_entry_t *) queue_remove(synth_queue); + current = queue_peek(synth_queue); } pthread_mutex_unlock(&queue_guard); if (current->cmd != CMD_PAUSE && paused_espeak) { - reinitialize_espeak(s); + if (reinitialize_espeak(s) < 0) { + /* Espeak is unavailable, so the entry cannot be processed. + * Calling espeak functions on a terminated engine would + * just fail (or worse). Leave the entry queued and retry + * after a small pause. */ + pthread_mutex_lock(&queue_guard); + espeak_handle_failure(s); + return; + } } switch (current->cmd) { @@ -377,18 +509,48 @@ static void queue_process_entry(struct synth_t *s) break; case CMD_PAUSE: if (!paused_espeak) { - espeak_Cancel(); - espeak_Terminate(); - paused_espeak = 1; + error = espeak_Cancel(); + if (error == EE_OK) + error = espeak_Terminate(); + if (error == EE_OK) + paused_espeak = 1; + } else { + error = EE_OK; } break; default: + /* Uh? */ + error = EE_OK; break; } + pthread_mutex_lock(&queue_guard); if (error == EE_OK) { + /* Processed, drop it */ + struct espeak_entry_t *unqueued = queue_remove(synth_queue); + assert(unqueued == current); free_espeak_entry(current); current = NULL; + stalled_retries = 0; + if (restart_attempts) { + /* Forget about past restarts once the engine has been + * healthy for a while. Entries can spuriously succeed + * right after a restart while the audio output is still + * wedged (espeak's internal queue is empty again), so a + * quick success must not reset the counter. */ + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if (now.tv_sec - last_restart.tv_sec >= ESPEAK_HEALTHY_SECS) + restart_attempts = 0; + } + } else { + if (error != EE_BUFFER_FULL) + fprintf(stderr, "espeak error: %d\n", error); + /* The entry stays queued and will be retried. Give espeak a + * little break before that, whatever the error: previously only + * EE_BUFFER_FULL throttled, and any other persistent error made + * us retry the same entry in a tight loop, burning a whole CPU. */ + espeak_handle_failure(s); } } @@ -450,7 +612,17 @@ void *espeak_thread(void *arg) pthread_cond_wait(&runner_awake, &queue_guard); if (stop_requested) { + current = NULL; + /* Call into espeak with queue_guard released: espeak_Cancel + * can take time, or even block indefinitely when the audio + * output is wedged, and holding the lock here would prevent + * the other threads from ever making progress again. The + * queue cannot change concurrently: the only producer (the + * softsynth thread) is blocked waiting for stop_acknowledged + * as long as stop_requested is set. */ + pthread_mutex_unlock(&queue_guard); stop_speech(); + pthread_mutex_lock(&queue_guard); synth_queue_clear(); stop_requested = 0; pthread_cond_signal(&stop_acknowledged); @@ -458,7 +630,6 @@ void *espeak_thread(void *arg) while (should_run && queue_peek(synth_queue) && !stop_requested) { queue_process_entry(s); - pthread_mutex_lock(&queue_guard); } } pthread_cond_signal(&stop_acknowledged); diff --git a/src/espeakup.c b/src/espeakup.c index 86b3a88..4e077f5 100644 --- a/src/espeakup.c +++ b/src/espeakup.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include "espeakup.h" @@ -40,7 +41,9 @@ volatile int should_run = 1; espeak_AUDIO_OUTPUT audio_mode; pthread_cond_t runner_awake = PTHREAD_COND_INITIALIZER; -pthread_cond_t stop_acknowledged = PTHREAD_COND_INITIALIZER; +/* Initialized in main: use the monotonic clock for timed waits. */ +pthread_cond_t wake_stop; +pthread_cond_t stop_acknowledged; pthread_mutex_t queue_guard = PTHREAD_MUTEX_INITIALIZER; int espeakup_start_daemon(void) @@ -146,6 +149,18 @@ int main(int argc, char **argv) struct synth_t s = { .voice = "", }; + pthread_condattr_t monotonic_attr; + + /* Condition variables used with pthread_cond_timedwait must use the + * monotonic clock, so that wall-clock adjustments (NTP, an + * installer setting the system time) cannot make the timeouts fire + * too early or far too late. */ + pthread_condattr_init(&monotonic_attr); + pthread_condattr_setclock(&monotonic_attr, CLOCK_MONOTONIC); + pthread_cond_init(&wake_stop, &monotonic_attr); + pthread_cond_init(&stop_acknowledged, &monotonic_attr); + pthread_condattr_destroy(&monotonic_attr); + synth_queue = new_queue(); if (!synth_queue) { diff --git a/src/espeakup.h b/src/espeakup.h index 58d0f6e..62a5ca8 100644 --- a/src/espeakup.h +++ b/src/espeakup.h @@ -99,6 +99,7 @@ extern int self_pipe_fds[2]; #define PIPE_WRITE_FD (self_pipe_fds[1]) extern pthread_cond_t runner_awake; +extern pthread_cond_t wake_stop; extern pthread_cond_t stop_acknowledged; extern pthread_mutex_t queue_guard; diff --git a/src/meson.build b/src/meson.build index f1c4610..6221208 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,6 +1,3 @@ -espeakup_version = vcs_tag(input : 'version.h.in', output : 'version.h') - - espeakup_sources = files([ 'cli.c', 'espeak.c', @@ -10,3 +7,4 @@ espeakup_sources = files([ 'softsynth.c', 'stringhandling.c' ]) +espeakup_version = vcs_tag(input : 'version.h.in', output : 'version.h') diff --git a/src/signal.c b/src/signal.c index f0b4d57..b6ddefc 100644 --- a/src/signal.c +++ b/src/signal.c @@ -58,6 +58,14 @@ void *signal_thread(void *arg) case SIGTERM: pthread_mutex_lock(&queue_guard); should_run = 0; + /* Wake up any thread waiting on a condition variable so + * that it notices the shutdown request: the softsynth + * thread may be waiting for a stop acknowledgement, and + * the espeak thread may be waiting for work or throttling + * before a retry. */ + pthread_cond_broadcast(&runner_awake); + pthread_cond_broadcast(&wake_stop); + pthread_cond_broadcast(&stop_acknowledged); pthread_mutex_unlock(&queue_guard); break; default: diff --git a/src/softsynth.c b/src/softsynth.c index 7d14708..e382b32 100644 --- a/src/softsynth.c +++ b/src/softsynth.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include "espeakup.h" @@ -223,14 +224,37 @@ static void process_buffer_acsint(struct synth_t *s, char *buf, ssize_t length) } } +/* How long to wait for the espeak thread to acknowledge a stop request + * before concluding that espeak is wedged beyond in-process recovery. */ +static const int stopAckTimeout = 10; + static void request_espeak_stop(void) { + struct timespec timeout; + int err = 0; + pthread_mutex_lock(&queue_guard); stop_requested = 1; pthread_cond_signal(&runner_awake); // Wake runner, if necessary. - while (should_run && stop_requested) + pthread_cond_signal(&wake_stop); // Wake runner, if necessary. + clock_gettime(CLOCK_MONOTONIC, &timeout); + timeout.tv_sec += stopAckTimeout; + while (should_run && stop_requested && err != ETIMEDOUT) // wait for acknowledgement. - pthread_cond_wait(&stop_acknowledged, &queue_guard); + err = pthread_cond_timedwait(&stop_acknowledged, &queue_guard, + &timeout); + if (should_run && stop_requested) { + /* The espeak thread is stuck in a call into espeak, most likely + * on a wedged audio device. There is no way to recover from + * within the process: exit so that the init system can respawn + * us in a clean state, rather than staying silent, ignoring + * SIGTERM, and stalling the whole console by not draining + * /dev/softsynth anymore. Use _exit because exit could hang in + * library destructors while the audio device is wedged. */ + fprintf(stderr, "espeakup: espeak did not acknowledge a stop " + "request within %d seconds, aborting\n", stopAckTimeout); + _exit(3); + } pthread_mutex_unlock(&queue_guard); } @@ -249,7 +273,7 @@ int open_softsynth(void) // Kernel without unicode support? Try without unicode. softFD = open("/dev/softsynth", O_RDWR | O_NONBLOCK); if (softFD < 0) { - perror("Unable to open the softsynth device"); + perror("Unable to open the /dev/softsynth device"); rc = -1; } return rc; diff --git a/src/stringhandling.c b/src/stringhandling.c index 86ed965..096b870 100644 --- a/src/stringhandling.c +++ b/src/stringhandling.c @@ -62,6 +62,17 @@ void *reallocMem(void *p, size_t n) return s; } +char *dupeString(char *s) +{ + char *c; + + if (!(c = strdup(s))) { + fprintf(stderr, "Out of memory!\n"); + exit(1); + } + return c; +} + char *initString(int *l) { *l = 0; diff --git a/src/stringhandling.h b/src/stringhandling.h index d138933..7f104ef 100644 --- a/src/stringhandling.h +++ b/src/stringhandling.h @@ -26,6 +26,7 @@ extern char *EMPTYSTRING; void *allocMem(size_t n); void *reallocMem(void *p, size_t n); +char *dupeString(char *s); char *initString(int *l); void stringAndString(char **s, int *l, const char *t); void stringAndBytes(char **s, int *l, const char *t, int cnt);