== Lesson 21: Subtitles image::learn/21-subtitles.png[The finished lesson, 480] Somebody who cannot hear is going to play your game. If your game has speech in it and no subtitles, they cannot. Not "it is harder for them" -- they will reach the first scene where a character says the thing that tells the player what to do, and they will stop. That is the whole of it, and it is the reason this lesson exists rather than being a paragraph in lesson nineteen. The other reason is practical. Retrofitting subtitles into a finished game is miserable. You have to find every line of speech, work out which frame it is on, and discover that half your timings are wrong because your video was re-encoded in the middle of the project. Done while you are writing the game, it is an afternoon. Singe gives you a system that does the hard part, and this lesson is how to use it. === It Is Not Ordinary Text Drawing You could do subtitles yourself. Keep a list of lines with start and end times, check the clock in `onOverlayUpdate`, and draw the current one with `fontToSprite`. People have. Here is why you should not. Subtitles are timed against *the disc*, not against a clock. When `srtLoad` reads your file it converts every timestamp into a disc frame number, there and then, using the frame rate of the disc that is loaded. From that moment the engine is not watching a clock at all; every drawn frame it asks which cue covers the frame the disc is on. That one decision buys you the thing a hand rolled system never gets right: *a subtitle survives a seek*. Send the disc anywhere -- `discSearch`, `discSkipForward`, a step, a branch into another segment -- and the right line is up on the next drawn frame, forwards or backwards, with nothing reloaded and no clock reset. In a branching game from lesson sixteen, where the player's choices jump the disc around constantly, that is the difference between working and almost working. It also draws in the right place. The subtitle document sits over the picture, above your overlay, below any screen space GUI, and it follows the picture wherever the player's settings put it -- a shrunken picture, a shifted one, a rotated cabinet, a hole in a bezel. It takes no mouse input, so a light gun shot passes straight through it. Getting all of that right yourself is a week you did not plan for. === Write the File Subtitles go in a SubRip file, which ends in `.srt` and is plain text you can type in any editor. It is one of the simplest formats there is. Make a file called `subtitles.srt` beside your script: ---- 1 00:00:01,000 --> 00:00:04,000 They told us the cave was empty. 2 00:00:04,200 --> 00:00:08,000 They were wrong about a great many things. 3 00:00:08,500 --> 00:00:13,000 Keep your light on the floor. ---- A cue is four things. A number on a line of its own. A line with two timestamps and an arrow made of two hyphens and a greater-than sign between them. One or more lines of text. A blank line to finish. A timestamp is `HH:MM:SS,mmm` -- hours, minutes, and seconds separated by colons, then a comma, then milliseconds. The comma is not a typo and a full stop will not do. `00:00:04,200` is four and a fifth seconds in. The cue numbers are read and thrown away, so they do not have to be in order or even correct. Number them anyway; you will be looking for cue forty-one at some point. Cue two shows what a two line cue looks like. The lines are shown as you wrote them, so *you* decide where a long line breaks, which matters more than you would think and is covered further down. The parser is deliberately forgiving about the things that go wrong in a hand edited file. Windows line endings load unchanged, and leading and trailing spaces are stripped from every line. A cue whose timestamps do not parse is skipped and the rest of the file goes on loading, which is friendly right up until you wonder where line twelve went. A few things are not supported: the `` and `` tags some tools write are shown as text rather than obeyed, and the positioning coordinates some tools append to the time line are ignored. The manual's Subtitles section is the full list. === Load It and Switch It On Three calls, and the order matters. [source,lua] ---- if srtLoad(DIR .. "subtitles.srt") then srtPosition(80) srtEnable(true) end ---- `srtLoad` reads the file and answers `true` or `false`. It answers `false`, having loaded nothing, in three cases: the file cannot be read, nothing in it parses, or *there is no disc*. In all three it prints a warning naming the file to the console. That is a good design and you should use it: a missing subtitle file is not worth killing a game over, but silently shipping a game with no subtitles is worth noticing. The "no disc" case is the one that will catch you. The timestamps have to become frame numbers at the disc's frame rate, and until a disc is open there is no frame rate to use. So load subtitles *after* the disc, which in an ordinary script means from the body of the script, where everything else you have written at the top level runs. It follows that a `.srt` cut for a video at one frame rate lands in the wrong place on a video at another. Time your file against the video you are actually going to ship. `srtEnable(true)` switches the loaded cues on. A script starts with subtitles off, always, so a game that loads them must switch them on. Switching them off again takes down whatever is showing, and switching them back on puts up whatever covers the frame the disc is on right now -- no hunting, no state to keep. Keeping subtitles on is a setting, not a decision you make for the player. Put it in your options page from lesson twenty, save it with `saveSet` from lesson twelve, and hand `srtEnable` whatever the player last chose. === Where They Sit [source,lua] ---- srtPosition(70) ---- `srtPosition` is a percentage down the picture, from `1` to `95`, and the default is `80`. It takes effect on the cue that is up right now and on every cue after it. A number outside the range is ignored rather than clamped, which means a typo leaves the position exactly where it was and says nothing, so check yours. Eighty per cent is low, and it is low on purpose: the bottom of the picture is usually the least interesting part of the shot. Move it up when the bottom is *not* boring -- when your game draws a scoreboard along the bottom edge, or when the player's light gun crosshair lives down there. === Taking One Down [source,lua] ---- srtClear() ---- `srtClear` takes whatever is on screen off at once, and leaves the loaded cues and the enabled state alone. The line that was showing stays off until the disc reaches the next cue. Use it when a scene ends early and you do not want the last line of dialogue hanging over the next shot. There is one more call that shares the subtitle machinery, and you may have used it already: [source,lua] ---- overlayBanner("Extra life!", 20) ---- `overlayBanner` shows a short message over the picture for about thirty drawn frames and then removes it. It is drawn by the same document as the subtitles, so a banner looks like a subtitle, and while one is up it takes the place of any subtitle, which comes back when the banner times out. Its own height argument is separate from `srtPosition` and defaults to `47`, the middle of the picture. Messages longer than sixty characters are ignored entirely, so a long one appears not to work at all. === Subtitles Already Inside the Video Some video files carry their own subtitle tracks. If yours does, you do not need a `.srt` at all: [source,lua] ---- if discGetSubtitleTracks() > 0 then srtLoadTrack(0) else srtLoad(DIR .. "subtitles.srt") end srtEnable(true) ---- `srtLoadTrack` takes a track number from `0` to `discGetSubtitleTracks() - 1` and everything after loading is identical. `discGetSubtitleLanguage(track)` tells you what language a track is labelled with, as a three letter code like `eng` or `fra`, so a game can offer the player whatever its video happens to carry. Two warnings. Reading a track walks the whole video file once, decoding no pictures, so a feature length disc takes a moment: do it while something else is on screen, not between two frames of gameplay. And subtitle tracks that hold *pictures* of the words rather than the words -- the kinds DVDs and Blu-rays use -- cannot be read. They are counted, so the numbering matches what other players show, but `srtLoadTrack` answers `false` for them. === The Whole Script This one needs a video. You have one: `Singe/menuBackground.mkv`, which the engine unpacked into your work folder the first time you ran anything. Tell Singe to use it as the disc with `--framefile`, which also turns the disc on: ---- Singe --framefile=Singe/menuBackground.mkv subtitles ---- [source,lua] ---- dofile("Singe/Framework.singe") overlaySetResolution(discGetWidth(), discGetHeight()) local HUD_X = 16 local STEP = 5 local TOP_LIMIT = 10 local LOW_LIMIT = 95 local hudFont = fontLoad("Singe/FreeSansBold.ttf", 18) local loaded = false local showing = false local height = 80 if discGetSubtitleTracks() > 0 then loaded = srtLoadTrack(0) end if not loaded then loaded = srtLoad(DIR .. "subtitles.srt") end if loaded then srtPosition(height) showing = true srtEnable(true) end discPlay() function onInputPressed(what) if not loaded then return end if what == SWITCH_BUTTON1 then showing = not showing srtEnable(showing) if showing then overlayBanner("Subtitles on", 20) else overlayBanner("Subtitles off", 20) end elseif what == SWITCH_UP then height = math.max(TOP_LIMIT, height - STEP) srtPosition(height) elseif what == SWITCH_DOWN then height = math.min(LOW_LIMIT, height + STEP) srtPosition(height) elseif what == SWITCH_LEFT then srtClear() discSearch(0) discPlay() end end function onOverlayUpdate() overlayClear() fontSelect(hudFont) colorForeground(255, 255, 255) if loaded then if showing then fontPrint(HUD_X, 16, "Frame " .. discGetFrame() .. " subtitles ON at " .. height .. "%") else fontPrint(HUD_X, 16, "Frame " .. discGetFrame() .. " subtitles OFF") end fontPrint(HUD_X, 40, "Fire toggles them, up and down move them, left rewinds") else fontPrint(HUD_X, 16, "No subtitles loaded.") fontPrint(HUD_X, 40, "Run with --framefile and check subtitles.srt is beside the script.") end return OVERLAY_UPDATED end function onShutdown() fontUnload(hudFont) end ---- Press fire to toggle, up and down to move the lines, and left to rewind to the start. The rewind is the one to watch: the disc jumps back to frame zero and the correct cue is up on the very next frame, without you doing anything about it. === What Just Happened [source,lua] ---- if discGetSubtitleTracks() > 0 then loaded = srtLoadTrack(0) end if not loaded then loaded = srtLoad(DIR .. "subtitles.srt") end ---- Take the video's own subtitles when it has them, fall back to the file beside the script when it does not, and end up knowing which -- if either -- worked. Both calls answer a boolean, so the fallback is two `if` statements and no special cases. `menuBackground.mkv` carries no subtitle track, so what you will actually get is the file you typed. [source,lua] ---- function onInputPressed(what) if not loaded then return end ---- `return` with nothing after it leaves the function immediately. With no subtitles loaded there is nothing for any of these keys to do, and leaving early is clearer than wrapping the whole body in an `if`. Calling `srtEnable` with nothing loaded is harmless, but writing code that only makes sense when it does nothing is a habit worth not forming. [source,lua] ---- elseif what == SWITCH_LEFT then srtClear() discSearch(0) discPlay() ---- The `srtClear` is not necessary. The engine will put up the right cue for frame zero on the next drawn frame regardless. It is there so that you can delete it and watch what happens: the old line stays up for a fraction of a second while the disc gets where it is going. On a slow seek that flash of the wrong line is visible, and clearing first is how you avoid it. === Making Text Readable Over a Moving Picture The system puts the words on screen. Whether anybody can read them is your problem, and it applies to every line of text you draw over video -- a prompt from lesson eighteen, a score, a branch choice -- not only to subtitles. *Stay out of the edges.* Television sets have been cutting the edges off pictures for seventy years, and arcade monitors are worse, not better. Work on the assumption that the outer five per cent of the picture on each side may not be there at all on somebody's screen. Keep anything a player must read inside that. This is why `srtPosition` stops at `95` and not `100`. *Contrast is not a colour choice, it is a guarantee.* White text over a picture is white text over whatever the picture happens to be doing, and in two seconds that may be a snowfield. The shipped subtitle style solves it the way film subtitles do: a dark panel that hugs the text, plus a one pixel shadow. If you draw your own text over video, do one or the other -- an `overlayBox` behind it or a dark copy of the text offset by a pixel. Do not rely on the picture. *Break your own lines.* Two short lines read faster than one long one, and a line that wraps where you did not choose usually breaks in the wrong place. Aim for about forty characters a line and no more than two lines at a time. Break at a natural pause: `They were wrong about / a great many things` reads; `They were wrong / about a great many things` stumbles. *Leave them up long enough.* A comfortable reading speed is somewhere around fifteen characters a second, so a forty character line wants roughly three seconds. Look at cue one in the file above: thirty-two characters, three seconds. If a line of dialogue is short and quick, keeping the cue up a beat past the end of the speech is kinder than snapping it away. *Put them where the action is not.* If the important thing in the shot is at the bottom of the frame, move the subtitles up for that scene with `srtPosition`. You can call it as often as you like, and the player will never notice that you did. === Styling Them The lines are drawn by a document, the same kind you met in lesson twenty, and two files in your `Singe` folder decide what they look like: * `Singe/subtitle.rml` -- the document. It holds one element, `#slot`, which the engine writes the current cue or banner into. * `Singe/subtitle.rcss` -- the style. `.cue` is the block that carries the height `srtPosition` asked for, and `.cue .text` is the bar of text inside it: white, bold, on a dark rounded panel that hugs the text, with a shadow. Open `subtitle.rcss` and read it -- it is about forty lines. Change the `font-size`, the `color`, the `background-color`, the `border-radius`, or the `font-effect`, save, and run again. The engine only ever sets `#slot`'s contents and the `.cue`'s height, so everything else in that file is yours and will survive an upgrade of the engine. Because `overlayBanner` uses the same document, restyling subtitles restyles your banners too, which is usually what you want and is worth knowing before you wonder why. One limit: the subtitle document is a document, and documents need a GPU. On a machine without one, `srtLoad` still reads your file and still answers truthfully, and nothing is drawn. A warning says so once. === Try It . *Add a fourth cue* to `subtitles.srt`, between eleven and thirteen seconds. Save and run. Then put it out of order, as cue `99`, first in the file, and confirm it still works. . *Give a cue a bad timestamp* -- write `00:00:05.500` with a full stop instead of a comma -- and run. Watch which line disappears, and notice that nothing at all is said about it. A file that loads is not a file that is right. . *Restyle them.* In `Singe/subtitle.rcss`, change `.cue .text` to yellow text on a fully transparent background, then add a heavier shadow. Then play it over the video and decide honestly whether it is still readable. . *Break the timing on purpose.* Change every timestamp in the file so it is two seconds late, and watch a correctly working system deliver useless subtitles. This is what a re-encoded video does to you. . *Put the toggle in a menu.* Take the pause page from lesson twenty, add a checkbox, and wire it to `srtEnable`. Then save the choice with `saveSet` so it is still on next time. This is the version your players actually want. === Break It on Purpose Run the script without the `--framefile` option: ---- Singe subtitles ---- The window opens, black, with your fallback message on it, and the console says: ---- Warning: No subtitles were loaded from subtitles.srt. ---- No crash and no error dialog, because `srtLoad` answered `false` and your script handled it. That is the most valuable failure in this lesson, because it is the one that happens without anybody noticing. There is no disc, so there is no frame rate, so there is nothing to turn the timestamps into, so nothing loads. If you had written `srtLoad(...)` on its own and thrown the answer away, your game would have run perfectly and shown no subtitles, and you would have found out when somebody told you. Use the answer. Every time. === What You Learned * A game with speech and no subtitles is a game some people cannot play at all. * Building subtitles in as you go is an afternoon; retrofitting them is not. * Subtitles are a `.srt` file: a number, two timestamps with `-->` between them, the text, and a blank line. * `srtLoad` turns the timestamps into disc frame numbers at load time, which is why a cue survives any seek. * It must be loaded after the disc, and it answers `false` when it cannot load, including when there is no disc. Check the answer. * `srtEnable` switches them on, and a script always starts with them off. * `srtPosition` places them as a percentage down the picture, `1` to `95`. * `srtClear` takes down whatever is showing, and `overlayBanner` shares the same document. * `srtLoadTrack` reads subtitles out of the video file itself when it has them. * The look lives in `Singe/subtitle.rcss` and is yours to replace. * Text over a moving picture needs a safe margin, its own contrast, lines you broke yourself, and time to be read. === Next Time That is the last of the text. Part four goes on without you having to read it in order, and the next lesson opens a door that has been shut for twenty lessons: `overlayGetWidth` and `overlayGetHeight` are two numbers, and a scene has three. Lesson twenty-two is the third one.