TG SpeechBox with phoneme editor, NVDA Addon, SAPI5, Linux, Android, iOS, Mac OS, version 310 beta 6
Pre-releaseTGSpeechBox v3.10 Beta 6 — the real bottom of #100: two engine bugs under the /s/
Beta 5 gave you a post-mortem and said the /s/→/z/ leak was "understood
end to end." We owe you a correction. The device-processing half of
that story was real, and the Spanish /s/ brightening helped — but it
was compensating for two actual engine bugs we hadn't found yet, not
curing them. That's why some of you kept hearing the leak, and why the
maddening "wish"→"wiss" kept coming and going for no reason anyone
could pin down. Both bugs are now found and fixed at the source. This
is the beta where #100 closes at the engine level, on every platform.
Two things made this findable: the reports never stopped, and this
cycle we finally built proper on-device forensics (more on that below).
Both bugs were Android-only in effect, layout-dependent, and invisible
to desktop tooling — the kind of thing you can only corner on real
hardware.
Bug 1 — a voiced hum riding under every post-vocalic /s/
The engine's DC-blocking high-pass filters (the ones that keep the
waveform centered) never drained their state. When voicing stops, the
filter doesn't stop with it — it coasts on a low-frequency tail with a
~91 ms time constant. Because frication keeps the pre-formant gain up,
the existing silence-drain never fired during a voiceless fricative, so
that tail rode straight into the /s/ at full strength: F1-shaped,
sitting about 7 dB ABOVE the frication for the first ~50 ms of every
post-vocalic sibilant.
That hum IS the /s/→/z/ percept. Headphones flattered it — you could
still hear the /s/ over the hum. But a phone speaker plus the OEM
noise-suppression on the live screen-reader path deletes the marginal
high-frequency sibilance and leaves only the low hum, which reads to
the ear as a voiced /z/. This is exactly the mechanism Beta 5's
brightening was fighting from the outside; now the tail is drained at
the source (the filter empties in ~10 ms when the voiced source goes
quiet, gated on source amplitude so live voicing and breathy voice are
untouched).
Measured: "wish" final /ʃ/ centroid went from a muddy 1100 Hz to
2300 Hz solo (2800–3200 Hz in phrase context); Spanish post-vocalic
/s/ bleed markers gone. Ear-validated on a Samsung S22 in live
TalkBack. It's in the shared DSP core, so every platform benefits.
Bug 2 — the "random wiss": one uninitialized field, live since Beta 3
This is the one that made no sense. "wish" would read fine, then read
"wiss" on the next swipe, then fine again — different per word, per
build, and only on Android. Testers reported it since Beta 3 and we
couldn't reproduce it on desktop to save our lives. Here's why.
Since the fricationTiltDb tilt control shipped in Beta 3, the per-frame
frame factory built its 30-field frame struct on the stack and set 29
of the 30 fields. One — fricationTiltDb — was missed. Only the velar
stop-burst path ever wrote it, and it politely saved and restored the
garbage afterward. So every emitted frame carried whatever indeterminate
bytes happened to be on the stack in that slot.
The DSP consumes that field as an exponent — it scales the parallel
formant amplitudes above 1500 Hz. On desktop and iOS the stack slot
happened to hold roughly zero, so the bug was silent. On Android arm64,
certain call paths left huge negative values in it (one we caught:
-5.87e91), and that annihilated the 1.5–7 kHz energy that makes an /ʃ/
an /ʃ/ — collapsing it to a thin /s/-like residue while the fixed
presence resonators survived. Deterministic per execution path, moving
with the binary layout, invisible to AddressSanitizer because it's an
uninitialized-stack read, not an out-of-bounds one. That is the entire
"random, Android-only, per-word" character of the complaint, explained.
The fix value-initializes the frame struct (and a sibling defaults
struct with the same 12-field hole in the NVDA frontend), and guards
the tilt read the way the neighboring presence resonators are already
guarded: non-finite → 0, clamped to ±24 dB. So no future garbage source
can ever silently delete a fricative again. On the S22, the production
TTS path reading "wish" 20× on one handle went from 18/20 renders with
the fricative collapsed to 20/20 healthy. It lives in the shared
frontend factory — NVDA, Windows SAPI, iOS, macOS and Linux all inherit
it.
Three intelligibility fixes for band-limited and low-end devices
Alongside the two bug fixes, a DSP-core intelligibility pass, each piece
grounded in the formant-synthesis literature and validated by ear on
desktop and live on the S22 loudspeaker path:
-
Nyquist clamp (k = 0.475). A parallel resonator asked to sit at or
above the Nyquist frequency was disabling itself into passthrough —
which, in the differential parallel mix, is exact silence. That killed
the Spanish /s/ outright at an 11025 Hz sample rate (#100). It now
slides the pole just under Nyquist with proportional bandwidth, so the
peak survives as a wideband high-frequency shelf. Klatt (1980) ran his
synthesizer at 10 kHz and parked /s/ at F6 = 4900 for exactly this
reason. No-op at 16 kHz and above. -
Voiced-fricative pitch-synchronous AM. /z/ and /v/ frication now
tracks transglottal airflow — the buzzing hiss that tells your ear a
voiced fricative apart from a plain /s/ (Rabiner 1968; Stevens 1971).
It's mean-preserving, so the hiss buzzes without losing loudness, with
modulation depth kept near where radiated modulation saturates (Pincas
& Jackson 2005). Gated on voicing × frication, so vowels and voiceless
/s/ are untouched. -
Consonant clarity boost (~+4 dB on sustained fricatives). Band-
limiting and mobile noise-suppression destroy consonant PLACE cues and
duck quiet high-frequency frication as if it were noise (Miller &
Nicely 1955). Raising the consonant level relative to the vowel
restores audibility (+7–21% consonant recognition, Gordon-Salant
1986/87; strongest for fricatives, Sroka & Braida 1989 — stop bursts
are gated out so they don't get over-loud). On-device, S-sounds now cut
through the S22 speaker DSP where Beta 5 still ducked them toward /z/.
Walk-back — the Beta 5 "Español" known issue was a measurement artifact
Beta 5 flagged that words whose IPA begins with a secondary-stress mark
("Español" itself, "escuchar") didn't seem to pick up the brighter /s/,
and blamed a text-to-IPA alignment quirk. On re-investigation, that was
a measurement-window artifact, not a real bug — the /s/ on those words
was getting the fix all along; our on-device metric was sampling the
wrong slice. There's no frontend fix owed here. Sorry for the false
alarm, and thank you to whoever tried to A/B "Español" against the
promise and (correctly) heard no difference.
How we found them — on-device forensics
Both bugs were Android-only in effect and layout-dependent, so desktop
renders and ASan were never going to catch them. This cycle we built a
release-signed on-device instrumentation harness that drives a fresh
engine through the exact production layer — direct-IPA vs text+IPA A/B,
rapid rotor-hammering repeats, per-setting and per-pipeline-step bisect
toggles, raw PCM pull, and full engine-state dumps — so we could get
ground truth from the real AudioTrack path instead of guessing from a
desktop proxy. The render-time frame dumps are what caught the garbage
tilt field arriving from the frontend with every neighboring field
healthy.
One more bug fell out along the way: eSpeak's state is process-global,
but every engine instance was calling initialize on create and
terminate unconditionally on destroy — so tearing down one engine
(including a debug handle) tore eSpeak out from under whichever engine
was still live. It's now refcounted: first one in initializes, last one
out terminates. The instrumentation is behind test-only surfaces and
touches no production path.
What we need from you
If you reported #100: please A/B the words where you heard the leak —
Restablecer, Seleccionado, Español, suspendido, and the English
"wish" / "recent" / "Facebook" that went low and voiced — on the same
phone, same screen reader, same settings. Better, worse, or gone: tell
us on #100. That's the final gate.
Deferred (carried forward)
- endCb1/2/3 per-phoneme bandwidth evolution (Stevens 1998)
- Croatian #99 items; Spanish "siguiente" /ʝ/ split (#20)
- /o-ɣ-o/ Kingston perceptual integration trap (diálogo)
Testers, this was the payoff of your persistence
@gregodejesus2 — "it didn't occur in previous versions" was the sentence
that cracked Beta 5, and your s/z reports across the betas kept the
signal alive long enough for us to build the tools to find the real
bugs.
@29-Bloo — the recordings and the toggle tests. The scroll bug you
surfaced in b5 is closed; your ear on the fricatives helped confirm this
round.
@rmcpantoja, @dgomez42 — the accumulated #95/#100 reports across months.
Neither of these two bugs would have been found without a complaint that
refused to go away.
A note on the models this round
First time we ran part of a cycle on Fable 5, and it earned its seat.
The two bugs here — a filter that never drained and a single field left
uninitialized since b3 — are the kind that don't fall out of a fast,
clever pass. They need the slow, methodical "rule it out, one variable
at a time" grind on real hardware, and Fable 5 was genuinely good at it,
surfacing DSP problems that had been hiding under the surface for months.
Opus 4.8 still wins on speed at the same accuracy, and drove the
intelligibility triad and the on-device forensics tooling. Different
strengths, same table — this round was a good argument for keeping both
in the room.
Links
- Android (Google Play)
- iOS / macOS (App Store)
- GitHub
- License: MIT (sd_tgsb module: GPL-3.0)
Join the Test
— Tamas + Claudeo (Fable 5 + Opus 4.8, 1M context)