From 3794e8e74bdb3786c35aa1de05037faa888ebd2a Mon Sep 17 00:00:00 2001 From: Christopher Brannon Date: Sun, 20 Jun 2021 02:25:05 -0700 Subject: [PATCH 01/25] Update my email address in the docs. --- doc/espeakup.8.ronn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From f077d0c042974043daaef50f853770deebad5711 Mon Sep 17 00:00:00 2001 From: William Hubbs Date: Fri, 2 Jul 2021 13:24:22 -0500 Subject: [PATCH 02/25] format build files consistently --- doc/meson.build | 10 +++++----- meson.build | 14 +++++++++----- meson_options.txt | 4 ++-- services/systemd/meson.build | 9 ++++----- src/meson.build | 4 +--- 5 files changed, 21 insertions(+), 20 deletions(-) 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..0002729 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,9 @@ 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') @@ -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/systemd/meson.build b/services/systemd/meson.build index 4a693ed..9ae652e 100644 --- a/services/systemd/meson.build +++ b/services/systemd/meson.build @@ -3,15 +3,14 @@ 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/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') From b7282c1a90c8d1bf87388396113321d09c665809 Mon Sep 17 00:00:00 2001 From: William Hubbs Date: Tue, 29 Jun 2021 11:12:26 -0500 Subject: [PATCH 03/25] use systemd modprobe service for speakup_soft --- services/systemd/espeakup.service.in | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/systemd/espeakup.service.in b/services/systemd/espeakup.service.in index ec5f4da..da97c2a 100644 --- a/services/systemd/espeakup.service.in +++ b/services/systemd/espeakup.service.in @@ -1,14 +1,13 @@ [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 From 8f18ad7f887c64f307b1a8ccc0df99a44a041a25 Mon Sep 17 00:00:00 2001 From: William Hubbs Date: Mon, 5 Jul 2021 19:32:37 -0500 Subject: [PATCH 04/25] add dupeString wrapper function This function calls strdup and exits on failure. --- src/stringhandling.c | 11 +++++++++++ src/stringhandling.h | 1 + 2 files changed, 12 insertions(+) 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); From e858481c0a158514e383c9282641a436a07e6de2 Mon Sep 17 00:00:00 2001 From: William Hubbs Date: Mon, 5 Jul 2021 19:35:24 -0500 Subject: [PATCH 05/25] use dupeString in command line processing --- src/cli.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/cli.c b/src/cli.c index a4cf48f..bc25c79 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; + pidPath = dupeString(optarg); break; case 'V': - defaultVoice = strdup(optarg); + defaultVoice = dupeString(optarg); break; case 'a': espeakup_mode = ESPEAKUP_MODE_ACSINT; From 316e4fc51d7e9db322feaf84e202e9243023671e Mon Sep 17 00:00:00 2001 From: Samuel Thibault Date: Sat, 21 Aug 2021 14:53:27 +0200 Subject: [PATCH 06/25] softsynth: on error, be clear we are talking about /dev/softsynth --- src/softsynth.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/softsynth.c b/src/softsynth.c index 7d14708..9eb75a4 100644 --- a/src/softsynth.c +++ b/src/softsynth.c @@ -249,7 +249,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; From ca234f2122a9460a7290e0ea6a3dad3fdd8182df Mon Sep 17 00:00:00 2001 From: Samuel Thibault Date: Fri, 11 Feb 2022 00:30:26 +0100 Subject: [PATCH 07/25] Request systemd to prioritize espeakup So speech doesn't get choppy on a loaded system. As suggested by Nick Gawronski. This fixes #46. --- services/systemd/espeakup.service.in | 1 + 1 file changed, 1 insertion(+) diff --git a/services/systemd/espeakup.service.in b/services/systemd/espeakup.service.in index da97c2a..ca5b2d7 100644 --- a/services/systemd/espeakup.service.in +++ b/services/systemd/espeakup.service.in @@ -11,6 +11,7 @@ Environment="default_voice=" ExecStart=@bindir@/espeakup --default-voice=${default_voice} ExecReload=kill -HUP $MAINPID Restart=always +Nice=-10 [Install] WantedBy=sound.target From c99bfb8e4519196b8338f0b06adc632e09482cf7 Mon Sep 17 00:00:00 2001 From: Samuel Thibault Date: Fri, 11 Feb 2022 00:31:05 +0100 Subject: [PATCH 08/25] Request systemd to protect espeakup from OOM Otherwise it might get killed on memory pressure. --- services/systemd/espeakup.service.in | 1 + 1 file changed, 1 insertion(+) diff --git a/services/systemd/espeakup.service.in b/services/systemd/espeakup.service.in index ca5b2d7..075a717 100644 --- a/services/systemd/espeakup.service.in +++ b/services/systemd/espeakup.service.in @@ -12,6 +12,7 @@ ExecStart=@bindir@/espeakup --default-voice=${default_voice} ExecReload=kill -HUP $MAINPID Restart=always Nice=-10 +OOMScoreAdjust=-900 [Install] WantedBy=sound.target From 78e561fae952a1230e0534f4e461a31a6d7a51e0 Mon Sep 17 00:00:00 2001 From: Brandon McGinty Date: Sat, 12 Feb 2022 15:29:23 -0600 Subject: [PATCH 09/25] 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. --- src/cli.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.c b/src/cli.c index bc25c79..8453587 100644 --- a/src/cli.c +++ b/src/cli.c @@ -77,7 +77,7 @@ void process_cli(int argc, char **argv) do { opt = getopt_long(argc, argv, shortOptions, longOptions, NULL); switch (opt) { - case 'p': + case 'P': pidPath = dupeString(optarg); break; case 'V': From 108c28ae08bdffd4861f42cdf42374bb00c77bb7 Mon Sep 17 00:00:00 2001 From: Samuel Thibault Date: Tue, 1 Mar 2022 01:28:38 +0100 Subject: [PATCH 10/25] 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. --- src/espeak.c | 24 ++++++++++++++++++++---- src/espeakup.c | 1 + src/espeakup.h | 1 + src/softsynth.c | 1 + 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/espeak.c b/src/espeak.c index 135c6b5..b4b58ca 100644 --- a/src/espeak.c +++ b/src/espeak.c @@ -325,16 +325,16 @@ static void reinitialize_espeak(struct synth_t *s) return; } +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); @@ -386,9 +386,25 @@ static void queue_process_entry(struct synth_t *s) 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; + } else { + if (error == EE_BUFFER_FULL) + { + /* Give speak a little break before retrying */ + struct timespec timeout; + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec++; + /* But wake up immediately if we have to stop */ + pthread_cond_timedwait(&wake_stop, &queue_guard, &timeout); + } + else + fprintf(stderr, "espeak error: %d\n", error); } } @@ -450,6 +466,7 @@ void *espeak_thread(void *arg) pthread_cond_wait(&runner_awake, &queue_guard); if (stop_requested) { + current = NULL; stop_speech(); synth_queue_clear(); stop_requested = 0; @@ -458,7 +475,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..baeee11 100644 --- a/src/espeakup.c +++ b/src/espeakup.c @@ -40,6 +40,7 @@ volatile int should_run = 1; espeak_AUDIO_OUTPUT audio_mode; pthread_cond_t runner_awake = PTHREAD_COND_INITIALIZER; +pthread_cond_t wake_stop = PTHREAD_COND_INITIALIZER; pthread_cond_t stop_acknowledged = PTHREAD_COND_INITIALIZER; pthread_mutex_t queue_guard = PTHREAD_MUTEX_INITIALIZER; 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/softsynth.c b/src/softsynth.c index 9eb75a4..ca2e47e 100644 --- a/src/softsynth.c +++ b/src/softsynth.c @@ -228,6 +228,7 @@ static void request_espeak_stop(void) pthread_mutex_lock(&queue_guard); stop_requested = 1; pthread_cond_signal(&runner_awake); // Wake runner, if necessary. + pthread_cond_signal(&wake_stop); // Wake runner, if necessary. while (should_run && stop_requested) // wait for acknowledgement. pthread_cond_wait(&stop_acknowledged, &queue_guard); From d7c06a6f9f9c2d00801d14314eaf55bfdb328442 Mon Sep 17 00:00:00 2001 From: Alexander Epaneshnikov Date: Fri, 11 Mar 2022 16:51:28 +0300 Subject: [PATCH 11/25] meson: switch from deprecated functions (#49) --- meson.build | 2 +- services/systemd/meson.build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 0002729..38788d2 100644 --- a/meson.build +++ b/meson.build @@ -6,7 +6,7 @@ project('espeakup', 'c', ], 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') diff --git a/services/systemd/meson.build b/services/systemd/meson.build index 9ae652e..eccbde7 100644 --- a/services/systemd/meson.build +++ b/services/systemd/meson.build @@ -1,4 +1,4 @@ -unitdir = systemd.get_pkgconfig_variable('systemdsystemunitdir') +unitdir = systemd.get_variable(pkgconfig: 'systemdsystemunitdir') prefixdir = get_option('prefix') bindir = join_paths(prefixdir, get_option('bindir')) From 98dc10374915c087c3a7368ccdb28bbb3df701b6 Mon Sep 17 00:00:00 2001 From: Samuel Thibault Date: Mon, 15 Aug 2022 23:48:26 +0200 Subject: [PATCH 12/25] 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. --- src/espeak.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/espeak.c b/src/espeak.c index b4b58ca..73876b4 100644 --- a/src/espeak.c +++ b/src/espeak.c @@ -377,12 +377,18 @@ 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; } From fa848f509e83bc8dff23e6fb1f2425e2f3ea7393 Mon Sep 17 00:00:00 2001 From: Samuel Thibault Date: Mon, 15 Aug 2022 23:41:52 +0200 Subject: [PATCH 13/25] set_punctuation: Fix punctuation levels values They do not actually follow the espeak values (and have no reason to, anyway). --- src/espeak.c | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/espeak.c b/src/espeak.c index 73876b4..90bdfc6 100644 --- a/src/espeak.c +++ b/src/espeak.c @@ -108,12 +108,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; From dc1f1b9efa2df9dc7e7ea7267b7a1cb0c1caf568 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:12:32 +0300 Subject: [PATCH 14/25] 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> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From d1f7e2384ba6c29e76f570df45301c6b6df22d44 Mon Sep 17 00:00:00 2001 From: Alexander Epaneshnikov Date: Thu, 11 Jun 2026 11:05:10 +0300 Subject: [PATCH 15/25] 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 --- src/espeak.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/espeak.c b/src/espeak.c index 90bdfc6..41a7e99 100644 --- a/src/espeak.c +++ b/src/espeak.c @@ -492,7 +492,16 @@ void *espeak_thread(void *arg) 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); From 2b959e874e26729b7d508a7d4d88eb33f331c24c Mon Sep 17 00:00:00 2001 From: Alexander Epaneshnikov Date: Thu, 11 Jun 2026 11:05:56 +0300 Subject: [PATCH 16/25] 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 --- src/signal.c | 8 ++++++++ 1 file changed, 8 insertions(+) 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: From e92edf96dc86ee4973bba468f35cfb173cd60a49 Mon Sep 17 00:00:00 2001 From: Alexander Epaneshnikov Date: Thu, 11 Jun 2026 11:06:35 +0300 Subject: [PATCH 17/25] 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 --- src/softsynth.c | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/softsynth.c b/src/softsynth.c index ca2e47e..d2fe585 100644 --- a/src/softsynth.c +++ b/src/softsynth.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include "espeakup.h" @@ -223,15 +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. pthread_cond_signal(&wake_stop); // Wake runner, if necessary. - while (should_run && stop_requested) + clock_gettime(CLOCK_REALTIME, &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); } From 7405455baf4a20f0f8e31a057ecbb59f64f636e8 Mon Sep 17 00:00:00 2001 From: Alexander Epaneshnikov Date: Thu, 11 Jun 2026 11:07:43 +0300 Subject: [PATCH 18/25] 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 --- src/espeak.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/espeak.c b/src/espeak.c index 41a7e99..f6b4d1e 100644 --- a/src/espeak.c +++ b/src/espeak.c @@ -24,6 +24,7 @@ #include #include #include +#include #include "espeakup.h" @@ -344,6 +345,19 @@ static void reinitialize_espeak(struct synth_t *s) return; } +/* 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_REALTIME, &timeout); + timeout.tv_sec++; + pthread_cond_timedwait(&wake_stop, &queue_guard, &timeout); +} + static struct espeak_entry_t *current = NULL; static void queue_process_entry(struct synth_t *s) { @@ -419,17 +433,13 @@ static void queue_process_entry(struct synth_t *s) free_espeak_entry(current); current = NULL; } else { - if (error == EE_BUFFER_FULL) - { - /* Give speak a little break before retrying */ - struct timespec timeout; - clock_gettime(CLOCK_REALTIME, &timeout); - timeout.tv_sec++; - /* But wake up immediately if we have to stop */ - pthread_cond_timedwait(&wake_stop, &queue_guard, &timeout); - } - 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_wait_retry(); } } From a504688f3d3bc1bcdefd89c015c33507d1b4d411 Mon Sep 17 00:00:00 2001 From: Alexander Epaneshnikov Date: Thu, 11 Jun 2026 11:08:16 +0300 Subject: [PATCH 19/25] 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 --- src/espeak.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/espeak.c b/src/espeak.c index f6b4d1e..a77fcf3 100644 --- a/src/espeak.c +++ b/src/espeak.c @@ -321,7 +321,7 @@ static void synth_queue_clear() } } -static void reinitialize_espeak(struct synth_t *s) +static int reinitialize_espeak(struct synth_t *s) { int rate; @@ -329,7 +329,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); @@ -342,7 +342,7 @@ 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 @@ -372,7 +372,15 @@ static void queue_process_entry(struct synth_t *s) 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_wait_retry(); + return; + } } switch (current->cmd) { From 71d0e1fa2a4f90ef2ed89ba807737374ecba1968 Mon Sep 17 00:00:00 2001 From: Alexander Epaneshnikov Date: Thu, 11 Jun 2026 11:09:29 +0300 Subject: [PATCH 20/25] 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 --- src/espeak.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/src/espeak.c b/src/espeak.c index a77fcf3..c701f74 100644 --- a/src/espeak.c +++ b/src/espeak.c @@ -25,6 +25,7 @@ #include #include #include +#include #include "espeakup.h" @@ -48,9 +49,30 @@ 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. */ +static volatile 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; + 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); @@ -358,6 +380,50 @@ static void espeak_wait_retry(void) 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 (synth_progressed) { + /* Espeak is making progress, it is merely backlogged. */ + synth_progressed = 0; + 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) { @@ -378,7 +444,7 @@ static void queue_process_entry(struct synth_t *s) * just fail (or worse). Leave the entry queued and retry * after a small pause. */ pthread_mutex_lock(&queue_guard); - espeak_wait_retry(); + espeak_handle_failure(s); return; } } @@ -440,6 +506,18 @@ static void queue_process_entry(struct synth_t *s) 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); @@ -447,7 +525,7 @@ static void queue_process_entry(struct synth_t *s) * 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_wait_retry(); + espeak_handle_failure(s); } } From 683544964229388933d5f9a7722a2327312ad5d3 Mon Sep 17 00:00:00 2001 From: Alexander Epaneshnikov Date: Thu, 11 Jun 2026 11:34:40 +0300 Subject: [PATCH 21/25] fixup! softsynth: bound the wait for espeak to acknowledge a stop request --- src/espeakup.c | 15 ++++++++++++++- src/softsynth.c | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/espeakup.c b/src/espeakup.c index baeee11..1e39aaf 100644 --- a/src/espeakup.c +++ b/src/espeakup.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include "espeakup.h" @@ -41,7 +42,8 @@ espeak_AUDIO_OUTPUT audio_mode; pthread_cond_t runner_awake = PTHREAD_COND_INITIALIZER; pthread_cond_t wake_stop = PTHREAD_COND_INITIALIZER; -pthread_cond_t stop_acknowledged = PTHREAD_COND_INITIALIZER; +/* Initialized in main: uses the monotonic clock for timed waits. */ +pthread_cond_t stop_acknowledged; pthread_mutex_t queue_guard = PTHREAD_MUTEX_INITIALIZER; int espeakup_start_daemon(void) @@ -147,6 +149,17 @@ 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(&stop_acknowledged, &monotonic_attr); + pthread_condattr_destroy(&monotonic_attr); + synth_queue = new_queue(); if (!synth_queue) { diff --git a/src/softsynth.c b/src/softsynth.c index d2fe585..e382b32 100644 --- a/src/softsynth.c +++ b/src/softsynth.c @@ -237,7 +237,7 @@ static void request_espeak_stop(void) stop_requested = 1; pthread_cond_signal(&runner_awake); // Wake runner, if necessary. pthread_cond_signal(&wake_stop); // Wake runner, if necessary. - clock_gettime(CLOCK_REALTIME, &timeout); + clock_gettime(CLOCK_MONOTONIC, &timeout); timeout.tv_sec += stopAckTimeout; while (should_run && stop_requested && err != ETIMEDOUT) // wait for acknowledgement. From 76a2af5008794fa87330c2649ca13c6705327077 Mon Sep 17 00:00:00 2001 From: Alexander Epaneshnikov Date: Thu, 11 Jun 2026 11:35:26 +0300 Subject: [PATCH 22/25] fixup! espeak: back off before retrying after any espeak error --- src/espeak.c | 2 +- src/espeakup.c | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/espeak.c b/src/espeak.c index c701f74..996d574 100644 --- a/src/espeak.c +++ b/src/espeak.c @@ -375,7 +375,7 @@ static void espeak_wait_retry(void) { struct timespec timeout; - clock_gettime(CLOCK_REALTIME, &timeout); + clock_gettime(CLOCK_MONOTONIC, &timeout); timeout.tv_sec++; pthread_cond_timedwait(&wake_stop, &queue_guard, &timeout); } diff --git a/src/espeakup.c b/src/espeakup.c index 1e39aaf..4e077f5 100644 --- a/src/espeakup.c +++ b/src/espeakup.c @@ -41,8 +41,8 @@ volatile int should_run = 1; espeak_AUDIO_OUTPUT audio_mode; pthread_cond_t runner_awake = PTHREAD_COND_INITIALIZER; -pthread_cond_t wake_stop = PTHREAD_COND_INITIALIZER; -/* Initialized in main: uses the monotonic clock for timed waits. */ +/* 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; @@ -157,6 +157,7 @@ int main(int argc, char **argv) * 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); From cc1447b16aba1da2ab004c13922941b38d480b7f Mon Sep 17 00:00:00 2001 From: Alexander Epaneshnikov Date: Thu, 11 Jun 2026 11:35:52 +0300 Subject: [PATCH 23/25] fixup! espeak: detect a wedged engine, restart it, and eventually give up --- src/espeak.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/espeak.c b/src/espeak.c index 996d574..60cc996 100644 --- a/src/espeak.c +++ b/src/espeak.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -63,8 +64,9 @@ int paused_espeak = 1; #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. */ -static volatile int synth_progressed = 0; + * 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; @@ -72,7 +74,7 @@ static struct timespec last_restart; static int callback(short *wav, int numsamples, espeak_EVENT *events) { int i; - synth_progressed = 1; + 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); @@ -388,9 +390,8 @@ static void espeak_wait_retry(void) * either, exit so that the init system respawns us in a clean state. */ static void espeak_handle_failure(struct synth_t *s) { - if (synth_progressed) { + if (atomic_exchange(&synth_progressed, 0)) { /* Espeak is making progress, it is merely backlogged. */ - synth_progressed = 0; stalled_retries = 0; } else if (++stalled_retries >= ESPEAK_STALL_RETRIES) { stalled_retries = 0; From c3c7c6a007f1c758ec6c9fe2b9ecd73bb7be6129 Mon Sep 17 00:00:00 2001 From: Achill Gilgenast Date: Mon, 29 Jun 2026 15:56:42 +0200 Subject: [PATCH 24/25] 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. --- services/meson.build | 4 ++-- services/systemd/meson.build | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) 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/meson.build b/services/systemd/meson.build index eccbde7..1a15303 100644 --- a/services/systemd/meson.build +++ b/services/systemd/meson.build @@ -1,4 +1,8 @@ -unitdir = systemd.get_variable(pkgconfig: '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')) From f266faaff78b9f71a739ffead91c09ec5f8afc07 Mon Sep 17 00:00:00 2001 From: donovanmalisch23-alt <228235407+donovanmalisch23-alt@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:04:45 +0000 Subject: [PATCH 25/25] espeak: escape XML-special characters in single-character mode When speakup echoes a single character, espeakup wraps it in SSML: %c 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 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 (< > & ' ") 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. --- src/espeak.c | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/espeak.c b/src/espeak.c index 60cc996..c9ac908 100644 --- a/src/espeak.c +++ b/src/espeak.c @@ -302,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 */