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] 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 */