Compare commits

...

25 commits

Author SHA1 Message Date
donovanmalisch23-alt
f266faaff7 espeak: escape XML-special characters in single-character mode
When speakup echoes a single character, espeakup wraps it in SSML:

    <say-as interpret-as="characters">%c</say-as>

If the character is one of XML's five special characters (< > & ' "),
the raw byte is injected straight into the markup, producing ill-formed
SSML.  espeak-ng then misparses the <say-as> element, which surfaces as
a spurious high-pitched "ringing" whenever one of these characters is
spoken.

Escape each special character to its corresponding XML entity
(&lt; &gt; &amp; &apos; &quot;) so the markup stays well-formed.
Non-printable characters (< 0x20 or > 0x7e) are no longer wrapped in
SSML either; they fall through to the existing raw-synthesis fallback
so they cannot corrupt the surrounding element either.

The space and ordinary-printable-character paths are unchanged.
2026-08-03 20:52:35 +02:00
Achill Gilgenast
c3c7c6a007 services: add fallback systemd unitdir if systemd is not available
Allows building the service files without a systemd dependency by
defining a default systemd user unit directory.

Relevant in Alpine, where we can package the service files in a
subpackage without having systemd in Alpine.

Addresses feedback of https://github.com/linux-speakup/espeakup/pull/64,
therefore superseeds it.
2026-06-29 18:55:16 +02:00
Alexander Epaneshnikov
cc1447b16a fixup! espeak: detect a wedged engine, restart it, and eventually give up 2026-06-29 10:29:36 +02:00
Alexander Epaneshnikov
76a2af5008 fixup! espeak: back off before retrying after any espeak error 2026-06-29 10:29:36 +02:00
Alexander Epaneshnikov
6835449642 fixup! softsynth: bound the wait for espeak to acknowledge a stop request 2026-06-29 10:29:36 +02:00
Alexander Epaneshnikov
71d0e1fa2a espeak: detect a wedged engine, restart it, and eventually give up
Issue #62 reports espeakup going permanently silent after libespeak-ng
prints "error: Device or resource busy" (EBUSY from the ALSA device,
via pcaudiolib).  Once the audio output is wedged, espeak's internal
command queue never drains, every espeak_Synth call fails with
EE_BUFFER_FULL forever, and espeakup just kept retrying silently.

EE_BUFFER_FULL is also perfectly normal while a long backlog is being
played back, so persistent failure alone is not a reliable signal.  To
tell a backlogged engine from a wedged one, note progress whenever the
synth callback fires (it is invoked for every chunk espeak
synthesizes, and synthesis is paced by audio playback): if entries
keep failing for ~10 seconds with no callback activity at all, declare
the engine wedged.

Recovery is layered:
- restart the engine in-process (espeak_Cancel + espeak_Terminate +
  reinitialize), which recovers transient device problems;
- if the engine has not been healthy for at least a minute between
  such restarts, after 3 restarts give up and exit, letting the init
  system (Restart=always in our systemd unit) respawn espeakup in a
  completely clean state;
- if the restart itself blocks on the wedged device, the
  stop-acknowledgement timeout in the softsynth thread eventually
  terminates the process as a last resort.

A quick espeak_Synth success right after a restart does not count as
healthy on purpose: espeak's freshly emptied internal queue accepts
entries even while the device is still wedged.

Helps: https://github.com/linux-speakup/espeakup/issues/45
Helps: https://github.com/linux-speakup/espeakup/issues/62

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 10:29:36 +02:00
Alexander Epaneshnikov
a504688f3d espeak: do not call espeak functions after a failed reinitialization
When resuming from CMD_PAUSE, queue_process_entry ignored the result
of reinitialize_espeak: if espeak_Initialize failed, paused_espeak
remained set, yet the entry was processed anyway, calling
espeak_Synth & co on a terminated engine.  Combined with the
busy-retry loop, this produced an endless stream of failing calls
against a dead engine.

Make reinitialize_espeak report failure, and when espeak is
unavailable, leave the entry queued and back off before trying to
reinitialize again.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 10:29:36 +02:00
Alexander Epaneshnikov
7405455baf espeak: back off before retrying after any espeak error
When processing an entry failed, only EE_BUFFER_FULL throttled before
the retry; any other persistent error (e.g. EE_INTERNAL_ERROR after
espeak was terminated) made queue_process_entry retry the same entry
in a tight loop with no sleep, burning a whole CPU while printing to a
stderr that points to /dev/null in daemon mode.

Factor the one-second throttle out into espeak_wait_retry() and apply
it to every failed entry.  The wake_stop condition variable still
interrupts the wait immediately when a flush comes in.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 10:29:36 +02:00
Alexander Epaneshnikov
e92edf96dc softsynth: bound the wait for espeak to acknowledge a stop request
request_espeak_stop() waited forever for the espeak thread to
acknowledge the stop.  If espeak-ng is wedged inside the audio output
(e.g. an ALSA device blocked or stuck returning EBUSY, as reported in
issue #62), the acknowledgement never comes, and the softsynth thread
stops draining /dev/softsynth forever.  Speakup's kernel buffer then
fills up and console output stalls, which matches the "blocks dmesg
output after a couple of pages" observation in issue #45.  The process
goes silent and only SIGKILL gets rid of it.

Wait at most 10 seconds for the acknowledgement (a normal cancellation
takes milliseconds; the timeout can only trigger when espeak is truly
stuck).  On timeout, exit with a clear message so that the init system
respawns espeakup in a clean state: our systemd unit already has
Restart=always.  A one-second restart beats an unkillable silent
daemon, and was explicitly requested by the reporter of issue #62.

Helps: https://github.com/linux-speakup/espeakup/issues/45
Helps: https://github.com/linux-speakup/espeakup/issues/62

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 10:29:36 +02:00
Alexander Epaneshnikov
2b959e874e signal: wake all condition-variable waiters on shutdown
On SIGINT/SIGTERM, the signal thread only set should_run to 0 and
relied on a wake-up chain to propagate the shutdown: the self-pipe
wakes the softsynth thread out of select(), which on exit signals
runner_awake to wake the espeak thread.

That chain breaks whenever the softsynth thread is not sitting in
select() but waiting on stop_acknowledged in request_espeak_stop():
nobody ever signals that condition variable on shutdown, so the thread
never re-evaluates should_run and the process never exits.

Broadcast all three condition variables after clearing should_run, so
that every parked thread re-checks its predicate, whichever wait it is
blocked in.

Helps: https://github.com/linux-speakup/espeakup/issues/45
Helps: https://github.com/linux-speakup/espeakup/issues/62

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 10:29:36 +02:00
Alexander Epaneshnikov
d1f7e2384b espeak: do not call espeak_Cancel() while holding queue_guard
When a flush is requested, the espeak thread called stop_speech() ->
espeak_Cancel() with queue_guard held.  espeak_Cancel() waits for
espeak-ng's internal say thread to acknowledge the cancellation, and
that thread can be blocked indefinitely inside a blocking ALSA call
(snd_pcm_writei/snd_pcm_drain on a wedged device, as seen with EBUSY
errors).  In that case queue_guard was held forever, which in turn:

- blocked the signal thread on pthread_mutex_lock(), so SIGINT/SIGTERM
  appeared to be ignored and only SIGKILL could end the process;
- left the softsynth thread stuck in request_espeak_stop(), so
  /dev/softsynth was no longer drained, speakup's kernel buffer filled
  up, and console output (e.g. dmesg) stalled.

Release queue_guard around the espeak_Cancel() call.  This is safe
because the only queue producer, the softsynth thread, is blocked
waiting for stop_acknowledged for as long as stop_requested is set, so
the queue cannot be mutated concurrently.

Helps: https://github.com/linux-speakup/espeakup/issues/45
Helps: https://github.com/linux-speakup/espeakup/issues/62

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 10:29:36 +02:00
Copilot
dc1f1b9efa
Fix deprecated Meson setup command warning in README (#61)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: alex19EP <4889846+alex19EP@users.noreply.github.com>
2025-08-22 14:12:32 +03:00
Samuel Thibault
fa848f509e set_punctuation: Fix punctuation levels values
They do not actually follow the espeak values (and have no reason to,
anyway).
2025-05-29 01:00:18 +03:00
Samuel Thibault
98dc103749 queue_process_entry: Avoid leaving error uninitialized
CMD_PAUSE was not actually setting error to EE_OK, and the default case,
even if it is not supposed to happen, should set error to something sane.
2025-05-29 00:59:51 +03:00
Alexander Epaneshnikov
d7c06a6f9f
meson: switch from deprecated functions (#49) 2022-03-11 14:51:28 +01:00
Samuel Thibault
108c28ae08 Throttle on EE_BUFFER_FULL errors
When espeak returns EE_BUFFER_FULL we should just wait a bit before
retrying. Also, we don't want to lose the current entry. We however want
to wake up as soon as possible on stop request.
2022-03-03 05:53:04 +03:00
Brandon McGinty
78e561fae9 use correct case for --pid-file option
This permits --pid-file to be specified.
The getopt_long option was looking for a lowercase p,
and the pid-file option used an uppercase P.
This meant that pid-file was never settable.
2022-02-14 16:14:23 +03:00
Samuel Thibault
c99bfb8e45 Request systemd to protect espeakup from OOM
Otherwise it might get killed on memory pressure.
2022-02-10 18:37:23 -06:00
Samuel Thibault
ca234f2122 Request systemd to prioritize espeakup
So speech doesn't get choppy on a loaded system.

As suggested by Nick Gawronski.

This fixes #46.
2022-02-10 18:30:08 -06:00
Samuel Thibault
316e4fc51d softsynth: on error, be clear we are talking about /dev/softsynth 2021-08-21 16:30:32 +03:00
William Hubbs
e858481c0a use dupeString in command line processing 2021-07-07 22:16:25 -05:00
William Hubbs
8f18ad7f88 add dupeString wrapper function
This function calls strdup and exits on failure.
2021-07-07 22:16:25 -05:00
William Hubbs
b7282c1a90 use systemd modprobe service for speakup_soft 2021-07-03 23:37:00 +03:00
William Hubbs
f077d0c042 format build files consistently 2021-07-03 03:53:58 +03:00
Christopher Brannon
3794e8e74b Update my email address in the docs. 2021-06-20 15:58:20 +03:00
17 changed files with 292 additions and 57 deletions

View file

@ -22,7 +22,7 @@ espeakup yet, espeakup just uses meson, so you should be able to
change to the source directory, then type: change to the source directory, then type:
```bash ```bash
meson . ./build meson setup . ./build
cd ./build cd ./build
ninja ninja
sudo ninja install sudo ninja install

View file

@ -71,7 +71,7 @@ espeak-ng can be found at [github](https://github.com/espeak-ng/espeak-ng)
William Hubbs <w.d.hubbs@gmail.com> is the author of espeakup. William Hubbs <w.d.hubbs@gmail.com> is the author of espeakup.
This manual page was written by Chris Brannon <cmbrannon79@gmail.com>. This manual page was written by Chris Brannon <chris@the-brannons.com>.
current authors and maintainers can be found at current authors and maintainers can be found at
[github](https://github.com/linux-speakup/espeakup/graphs/contributors) [github](https://github.com/linux-speakup/espeakup/graphs/contributors)

View file

@ -1,8 +1,8 @@
ronn = find_program('ronn', required: get_option('man')) ronn = find_program('ronn', required: get_option('man'))
if ronn.found() if ronn.found()
custom_target('man', input:files('espeakup.8.ronn'), custom_target('man',
output:'espeakup.8', input : files('espeakup.8.ronn'),
command:[ronn, '--output-dir', '@OUTDIR@', '--roff', '@INPUT@'], output : 'espeakup.8',
install: true, install_dir: join_paths(get_option('mandir'),'man8')) command : [ronn, '--output-dir', '@OUTDIR@', '--roff', '@INPUT@'],
install : true, install_dir: join_paths(get_option('mandir'),'man8'))
endif endif

View file

@ -1,8 +1,12 @@
project('espeakup', 'c', 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', license : 'GPL-3.0-or-later',
version : '0.90', version : '0.90',
meson_version : '>=0.47.0') meson_version : '>=0.51.0')
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
thread_dep = dependency('threads') thread_dep = dependency('threads')
@ -15,7 +19,7 @@ subdir('services')
subdir('src') subdir('src')
executable('espeakup', executable('espeakup',
espeakup_version, espeakup_version,
espeakup_sources, espeakup_sources,
dependencies : [thread_dep, espeak_dep, alsa_dep, math_dep], dependencies : [thread_dep, espeak_dep, alsa_dep, math_dep],
install : true) install : true)

View file

@ -1,4 +1,4 @@
option('systemd', type : 'feature', value : 'auto',
description :'enable systemd support')
option('man', type : 'feature', value : 'auto', option('man', type : 'feature', value : 'auto',
description : 'build manpage with ronn') description : 'build manpage with ronn')
option('systemd', type : 'feature', value : 'auto',
description :'enable systemd support')

View file

@ -1,4 +1,4 @@
systemd = dependency('systemd', required: get_option('systemd')) systemd = dependency('systemd')
if systemd.found() if (systemd.found() and get_option('systemd').allowed()) or get_option('systemd').enabled()
subdir('systemd') subdir('systemd')
endif endif

View file

@ -1,17 +1,18 @@
[Unit] [Unit]
Description=Software speech output for Speakup Description=Software speech output for Speakup
Documentation=man:espeakup(8) Documentation=man:espeakup(8)
Wants=systemd-udev-settle.service Wants=modprobe@speakup_soft.service
After=systemd-udev-settle.service sound.target After=modprobe@speakup_soft.service sound.target
[Service] [Service]
Type=forking Type=forking
PIDFile=/run/espeakup.pid PIDFile=/run/espeakup.pid
Environment="default_voice=" Environment="default_voice="
ExecStartPre=+modprobe speakup_soft
ExecStart=@bindir@/espeakup --default-voice=${default_voice} ExecStart=@bindir@/espeakup --default-voice=${default_voice}
ExecReload=kill -HUP $MAINPID ExecReload=kill -HUP $MAINPID
Restart=always Restart=always
Nice=-10
OOMScoreAdjust=-900
[Install] [Install]
WantedBy=sound.target WantedBy=sound.target

View file

@ -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') prefixdir = get_option('prefix')
bindir = join_paths(prefixdir, get_option('bindir')) bindir = join_paths(prefixdir, get_option('bindir'))
unit_conf = configuration_data() unit_conf = configuration_data()
unit_conf.set('bindir', bindir) unit_conf.set('bindir', bindir)
service_file = configure_file( service_file = configure_file(
input : 'espeakup.service.in', input : 'espeakup.service.in',
output : 'espeakup.service', output : 'espeakup.service',
configuration : unit_conf configuration : unit_conf
) )
install_data(service_file, install_data(service_file,
install_dir : unitdir install_dir : unitdir
) )

View file

@ -23,6 +23,7 @@
#include <string.h> #include <string.h>
#include "espeakup.h" #include "espeakup.h"
#include "stringhandling.h"
#include "version.h" #include "version.h"
/* pid path */ /* pid path */
@ -72,18 +73,15 @@ static void show_version(void)
void process_cli(int argc, char **argv) void process_cli(int argc, char **argv)
{ {
int opt; int opt;
char *cp;
do { do {
opt = getopt_long(argc, argv, shortOptions, longOptions, NULL); opt = getopt_long(argc, argv, shortOptions, longOptions, NULL);
switch (opt) { switch (opt) {
case 'p': case 'P':
cp = strdup(optarg); pidPath = dupeString(optarg);
if (cp != NULL)
pidPath = cp;
break; break;
case 'V': case 'V':
defaultVoice = strdup(optarg); defaultVoice = dupeString(optarg);
break; break;
case 'a': case 'a':
espeakup_mode = ESPEAKUP_MODE_ACSINT; espeakup_mode = ESPEAKUP_MODE_ACSINT;

View file

@ -21,9 +21,12 @@
#include <alsa/asoundlib.h> #include <alsa/asoundlib.h>
#include <assert.h> #include <assert.h>
#include <math.h> #include <math.h>
#include <stdatomic.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h>
#include <unistd.h>
#include "espeakup.h" #include "espeakup.h"
@ -47,9 +50,31 @@ const int volumeMultiplier = 22;
volatile int stop_requested = 0; volatile int stop_requested = 0;
int paused_espeak = 1; 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) static int callback(short *wav, int numsamples, espeak_EVENT *events)
{ {
int i; int i;
atomic_store(&synth_progressed, 1);
for (i = 0; events[i].type != espeakEVENT_LIST_TERMINATED; i++) { for (i = 0; events[i].type != espeakEVENT_LIST_TERMINATED; i++) {
if (events[i].type == espeakEVENT_MARK) { if (events[i].type == espeakEVENT_MARK) {
int mark = atoi(events[i].id.name); 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) enum adjust_t adj)
{ {
espeak_ERROR rc; espeak_ERROR rc;
espeak_PUNCT_TYPE espeak_punct;
if (adj == ADJ_DEC) if (adj == ADJ_DEC)
punct = -punct; punct = -punct;
if (adj != ADJ_SET) if (adj != ADJ_SET)
punct += s->punct; 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) if (rc == EE_OK)
s->punct = punct; s->punct = punct;
return rc; return rc;
@ -258,15 +302,39 @@ static espeak_ERROR speak_text(struct synth_t *s)
synth_mode |= espeakSSML; synth_mode |= espeakSSML;
if (espeakup_mode == ESPEAKUP_MODE_SPEAKUP && (s->len == 1)) { if (espeakup_mode == ESPEAKUP_MODE_SPEAKUP && (s->len == 1)) {
char *buf; char *buf = NULL;
int n; int n;
if (s->buf[0] == ' ') unsigned char c = s->buf[0];
if (c == ' ')
n = asprintf(&buf, n = asprintf(&buf,
"<say-as interpret-as=\"tts:char\">&#32;</say-as>"); "<say-as interpret-as=\"tts:char\">&#32;</say-as>");
else else if (c < 0x20 || c > 0x7e) {
n = asprintf(&buf, /* Not a printable character; do not embed it in SSML, as
"<say-as interpret-as=\"characters\">%c</say-as>", * that would produce invalid markup. Fall through to the
s->buf[0]); * 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 <say-as> element, which is the source of
* the spurious high-pitched "ringing" on these characters. */
const char *entity = NULL;
switch (c) {
case '<': entity = "&lt;"; break;
case '>': entity = "&gt;"; break;
case '&': entity = "&amp;"; break;
case '\'': entity = "&apos;"; break;
case '"': entity = "&quot;"; break;
}
if (entity)
n = asprintf(&buf,
"<say-as interpret-as=\"characters\">%s</say-as>",
entity);
else
n = asprintf(&buf,
"<say-as interpret-as=\"characters\">%c</say-as>",
c);
}
if (n == -1) { if (n == -1) {
/* D'oh. Not much to do on allocation failure. /* D'oh. Not much to do on allocation failure.
* Perhaps espeak will happen to say the character */ * 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; int rate;
@ -309,7 +377,7 @@ static void reinitialize_espeak(struct synth_t *s)
rate = espeak_Initialize(AUDIO_OUTPUT_PLAYBACK, 0, NULL, 0); rate = espeak_Initialize(AUDIO_OUTPUT_PLAYBACK, 0, NULL, 0);
if (rate < 0) { if (rate < 0) {
fprintf(stderr, "Unable to initialize espeak.\n"); fprintf(stderr, "Unable to initialize espeak.\n");
return; return -1;
} }
espeak_SetSynthCallback(callback); 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(espeakVOLUME, (s->volume + 1) * volumeMultiplier, 0);
espeak_SetParameter(espeakCAPITALS, 0, 0); espeak_SetParameter(espeakCAPITALS, 0, 0);
paused_espeak = 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) static void queue_process_entry(struct synth_t *s)
{ {
espeak_ERROR error; espeak_ERROR error = EE_OK;
char markbuff[50]; char markbuff[50];
static struct espeak_entry_t *current = NULL;
if (current != queue_peek(synth_queue)) { if (current != queue_peek(synth_queue)) {
if (current) if (current)
free_espeak_entry(current); free_espeak_entry(current);
current = (struct espeak_entry_t *) queue_remove(synth_queue); current = queue_peek(synth_queue);
} }
pthread_mutex_unlock(&queue_guard); pthread_mutex_unlock(&queue_guard);
if (current->cmd != CMD_PAUSE && paused_espeak) { 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) { switch (current->cmd) {
@ -377,18 +509,48 @@ static void queue_process_entry(struct synth_t *s)
break; break;
case CMD_PAUSE: case CMD_PAUSE:
if (!paused_espeak) { if (!paused_espeak) {
espeak_Cancel(); error = espeak_Cancel();
espeak_Terminate(); if (error == EE_OK)
paused_espeak = 1; error = espeak_Terminate();
if (error == EE_OK)
paused_espeak = 1;
} else {
error = EE_OK;
} }
break; break;
default: default:
/* Uh? */
error = EE_OK;
break; break;
} }
pthread_mutex_lock(&queue_guard);
if (error == EE_OK) { if (error == EE_OK) {
/* Processed, drop it */
struct espeak_entry_t *unqueued = queue_remove(synth_queue);
assert(unqueued == current);
free_espeak_entry(current); free_espeak_entry(current);
current = NULL; 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); pthread_cond_wait(&runner_awake, &queue_guard);
if (stop_requested) { 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(); stop_speech();
pthread_mutex_lock(&queue_guard);
synth_queue_clear(); synth_queue_clear();
stop_requested = 0; stop_requested = 0;
pthread_cond_signal(&stop_acknowledged); pthread_cond_signal(&stop_acknowledged);
@ -458,7 +630,6 @@ void *espeak_thread(void *arg)
while (should_run && queue_peek(synth_queue) && !stop_requested) { while (should_run && queue_peek(synth_queue) && !stop_requested) {
queue_process_entry(s); queue_process_entry(s);
pthread_mutex_lock(&queue_guard);
} }
} }
pthread_cond_signal(&stop_acknowledged); pthread_cond_signal(&stop_acknowledged);

View file

@ -24,6 +24,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/file.h> #include <sys/file.h>
#include <time.h>
#include <unistd.h> #include <unistd.h>
#include "espeakup.h" #include "espeakup.h"
@ -40,7 +41,9 @@ volatile int should_run = 1;
espeak_AUDIO_OUTPUT audio_mode; espeak_AUDIO_OUTPUT audio_mode;
pthread_cond_t runner_awake = PTHREAD_COND_INITIALIZER; 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; pthread_mutex_t queue_guard = PTHREAD_MUTEX_INITIALIZER;
int espeakup_start_daemon(void) int espeakup_start_daemon(void)
@ -146,6 +149,18 @@ int main(int argc, char **argv)
struct synth_t s = { struct synth_t s = {
.voice = "", .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(); synth_queue = new_queue();
if (!synth_queue) { if (!synth_queue) {

View file

@ -99,6 +99,7 @@ extern int self_pipe_fds[2];
#define PIPE_WRITE_FD (self_pipe_fds[1]) #define PIPE_WRITE_FD (self_pipe_fds[1])
extern pthread_cond_t runner_awake; extern pthread_cond_t runner_awake;
extern pthread_cond_t wake_stop;
extern pthread_cond_t stop_acknowledged; extern pthread_cond_t stop_acknowledged;
extern pthread_mutex_t queue_guard; extern pthread_mutex_t queue_guard;

View file

@ -1,6 +1,3 @@
espeakup_version = vcs_tag(input : 'version.h.in', output : 'version.h')
espeakup_sources = files([ espeakup_sources = files([
'cli.c', 'cli.c',
'espeak.c', 'espeak.c',
@ -10,3 +7,4 @@ espeakup_sources = files([
'softsynth.c', 'softsynth.c',
'stringhandling.c' 'stringhandling.c'
]) ])
espeakup_version = vcs_tag(input : 'version.h.in', output : 'version.h')

View file

@ -58,6 +58,14 @@ void *signal_thread(void *arg)
case SIGTERM: case SIGTERM:
pthread_mutex_lock(&queue_guard); pthread_mutex_lock(&queue_guard);
should_run = 0; 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); pthread_mutex_unlock(&queue_guard);
break; break;
default: default:

View file

@ -24,6 +24,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/select.h> #include <sys/select.h>
#include <time.h>
#include <unistd.h> #include <unistd.h>
#include "espeakup.h" #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) static void request_espeak_stop(void)
{ {
struct timespec timeout;
int err = 0;
pthread_mutex_lock(&queue_guard); pthread_mutex_lock(&queue_guard);
stop_requested = 1; stop_requested = 1;
pthread_cond_signal(&runner_awake); // Wake runner, if necessary. 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. // 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); pthread_mutex_unlock(&queue_guard);
} }
@ -249,7 +273,7 @@ int open_softsynth(void)
// Kernel without unicode support? Try without unicode. // Kernel without unicode support? Try without unicode.
softFD = open("/dev/softsynth", O_RDWR | O_NONBLOCK); softFD = open("/dev/softsynth", O_RDWR | O_NONBLOCK);
if (softFD < 0) { if (softFD < 0) {
perror("Unable to open the softsynth device"); perror("Unable to open the /dev/softsynth device");
rc = -1; rc = -1;
} }
return rc; return rc;

View file

@ -62,6 +62,17 @@ void *reallocMem(void *p, size_t n)
return s; 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) char *initString(int *l)
{ {
*l = 0; *l = 0;

View file

@ -26,6 +26,7 @@ extern char *EMPTYSTRING;
void *allocMem(size_t n); void *allocMem(size_t n);
void *reallocMem(void *p, size_t n); void *reallocMem(void *p, size_t n);
char *dupeString(char *s);
char *initString(int *l); char *initString(int *l);
void stringAndString(char **s, int *l, const char *t); void stringAndString(char **s, int *l, const char *t);
void stringAndBytes(char **s, int *l, const char *t, int cnt); void stringAndBytes(char **s, int *l, const char *t, int cnt);