Parsing SRT subtitles for a music player
- YT :: https://www.youtube.com/watch?v=7tCZrpB9iwA
- Original title :: Implemented Subtitles
Tsoding adds lyrics display to Sowon2, his Jai music player, by writing an SRT parser from scratch and wiring it into the Raylib UI. Along the way he invents a small string-view merging primitive, debugs a subtle bug caused by Jai's to_integer consuming its input, and shows off the declarative layout language ("castle") he built for positioning UI elements.
Motivation
He added Vocaloid tracks by beat_shobon, whose synthesized robotic vocals are deliberately incomprehensible, and wanted lyrics on screen. No official lyrics file existed, so he hand-wrote an .srt in about ten minutes. Each song in his database already carries an ID, thumbnail, audio file, artist, title and Bandcamp link; subtitles become one more optional field, a path relative to the executable.
Parsing SubRip
SRT is trivially simple — a numeric counter, a HH:MM:SS,mmm --> HH:MM:SS,mmm range, then one or more text lines, separated by blank lines. He stores timestamps as integer milliseconds rather than floats, a lesson from building markut: integer arithmetic avoids accumulated rounding error and is easier to reason about.
His stated philosophy is worth extracting: he is not implementing SRT, he is implementing his SRT file. Asked whether an ID can repeat, he answers "I have no idea and I don't care" — he'll fix corner cases as real files force him to, rather than burying himself in a spec he may never need. "People sometimes just overcomplicate software development."
Merging string views with holes
He wants the subtitle text to be a string view pointing into the original file buffer, not a freshly allocated concatenation. That means chopping line by line, then merging adjacent views. A naive merge_strings(a, b) asserts a.data + a.count = b.data= and returns a view spanning both, allocating nothing. But consecutive lines are separated by a newline, so the invariant fails. His fix: relax the assertion to <= (allowing holes) and compute the length as b.data + b.count - a.data, which swallows the separator. He half-jokes that chat just invented a new operation, and wonders aloud whether it has a standard name.
The to_integer bug
After porting the timestamp parser he gets wrong minute values. He reaches for GF2 and steps through: hours parse fine, minutes fail. The cause is that Jai's to_integer returns the unconsumed remainder, and he was reassigning content from both the split and to_integer, double-consuming the string. The fix is simply to ignore that return value — which prompts an aside on how pleasant it is that Jai lets you ignore trailing return values, and a tangent about Go's error handling: the semantics are fine, the syntax is the problem, since forcing braces around every if err ! nil= is what makes it feel intrusive.
Fonts, UI and the castle language
He wanted italic Iosevka but only had regular and bold, and couldn't remember where the originals came from — his system only had a 420 MB collection file. He downloads the separate-TTF package release and drops the italic in.
The subtitles element needs no new rendering code: his existing place_text_element already handles backdrops, emoji (exactly three — duck, dog, dolphin, because laamaa has three songs named after them), and multi-line text via Raylib. A missing font in the style crashes inside Raylib's draw_text, which he notes as a bug to report.
Positioning happens in "castle", the custom declarative layout language he's written in Jai. He anchors the subtitle's horizontal position to the sign element and its vertical position to the logo, so moving either drags the subtitles along. Padding units are percentages of width and height, so uniform padding needs an aspect-ratio correction. His verdict on the whole exercise: "Who needs CSS?" — centring things took one line of locking rather than voodoo. He plans to release castle as a Jai library once the language and compiler are publicly available.
Getting the current subtitle
The final piece is get_current_subtitle(song), deliberately placed at the song level so all the error handling collapses into one place: invalid song, no subtitles, or nothing playing right now all return an empty string. It reads get_music_time_played (seconds, per raylib.h), converts to milliseconds, and linearly scans the 43 subtitles each frame — trivially cheap at this scale. He is "kind of disappointed how easy it was."
He also notes the Jai release date: the Order of the Sinking Star ships around October 8, with the compiler binaries following, and open-sourcing after that.