Read Along with the Fox
Category: SDL Adventure Game
When the fox learned to move her mouth with the words, the same spec promised a second half: the words themselves, on screen, with the one being spoken highlighted karaoke-style. Not as a debugging aid and not only as subtitles — as a learn-to-read feature. The game’s audience is a child who can’t read yet; a line of text where a bright box hops from word to word in time with the voice is how she starts connecting the sounds she knows to the shapes she doesn’t. This post ships that half.

The timing was already there
The hard part of a karaoke highlight is knowing when each word is spoken —
and that problem was already solved offline, in the
first speech post.
Every dialogue WAV has a committed .words sidecar estimating each word’s
start and end from the mouth cues (when speech happens) and the transcript
(what is said). The estimate distributes words over the speech spans, so the
highlight starts with the voice, pauses when the voice pauses, and lands
within a word of the truth — plenty for follow-along reading. The engine just
asks, every frame, “which word is active at this millisecond?” — a
cursor-cached lookup that was sitting in lipsync.c waiting for a caller.
Text, finally
This is the game’s first on-screen text, which meant picking how to render text at all. Pre-rendered PNGs per line — the pattern the localized buttons use — would mean ~40 images per locale, regenerated on every script edit. A bitmap-font blitter means owning glyph coverage forever. So the engine gained its third SDL satellite library: SDL2_ttf, integrated exactly like SDL2_image and SDL2_mixer before it (pkg-config natively, an Emscripten port flag for the web, a forwarding header for the include style). The bundled face is Atkinson Hyperlegible — designed for maximum legibility, covers Italian diacritics, weighs 56 KB, and is OFL-licensed so it lives in the repo next to its license file.
The overlay itself does its work once per line, not per frame. When a line
starts, subtitle_show splits it on spaces and renders every word twice
into tiny cached textures — white for normal, near-black for when it sits on
the yellow highlight — then wraps greedily at 700 px and centres the lines,
bottom-anchored. Per frame, rendering is just: black backing box, yellow box
under the active word, blit the words (the active one in its dark version).
The highlight pauses in the voice’s pauses for free, because word windows
only ever cover speech.
One robustness rule earned its keep immediately in testing: the timing file
pairs with the displayed words by index, so if a transcript is edited and
its .words sidecar goes stale, the counts disagree — and the overlay
disables just the highlight for that line, logs once, and shows the text
anyway. Degrade the garnish, keep the meal.
The lines you could never see
My favourite consequence is the fallback. Gina’s dialogue has always been a placeholder — her Italian lines live in the code and play as a shared silent WAV, so in the game she just… stands there, beak moving. The overlay rule is: subtitles show per the setting, but a line with no audio always shows. Combined with subtitles being on by default, every one of Gina’s lines is now readable in the game itself, wrapped and centred like any other — just without a highlight until her real recordings land and bring timing data with them.

On by default is a deliberate stance: this is an accessibility and
learn-to-read feature, not a debugging overlay, so it ships enabled and the
opt-out is the setting — --subtitles=0 on desktop, ?subtitles=0 on the
web, plumbed exactly like the language selection. And since the overlay is
screen-space UI, it draws after the
camera offset is
reset — a line reads the same whether the fox speaks it in a static room or
at the far end of a scrolling field.
What remains of the speech plan is art and audio, not code: real seven-frame mouth sheets to replace the remapped three, and Gina’s actual voice. When those recordings arrive, the pipeline from the first post turns them into cues and word timings, and everything in this post — the mouth, the text, the bouncing highlight — simply starts being true for her too. The fox, for now, reads along with herself.