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
(< > & ' ") 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.
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.
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>
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>
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>
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>
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>
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>
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.
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.