How Subomatic works
Subomatic re-times an out-of-sync subtitle automatically — no manual anchor points. It lines the subtitle up with when speech happens, either in a reference subtitle you already trust or in the audio of the video itself. Everything below runs locally, in your browser.
The pipeline
-
Decode (audio mode). Your media file is decoded by the
browser's native
WebAudioengine into a mono PCM signal. This is the same optimized (often hardware-accelerated) decoder the browser uses to play video, so any format it can play, Subomatic can read — and the file never leaves the page. - Detect speech. A voice-activity detector turns that waveform into a sparse set of speech intervals — the spans of time where someone is actually talking. (In reference mode this step is skipped: the reference subtitle's own cue timings are the intervals.)
- Align. The engine treats both sides — the reference intervals and your subtitle's lines — as sets of time intervals, and searches for the timing correction that makes them overlap as much as possible.
- Rewrite. The chosen correction is applied to the cue timestamps and the subtitle is serialized back to the same format it came in. Only the timing changes — text, styling, positioning and karaoke all round-trip untouched.
Written in Rust, compiled to WebAssembly, running in your browser
There is no server. The whole thing is a single pure-Rust
core — the subtitle parsers/serializers (SubRip
.srt, WebVTT .vtt, MicroDVD .sub,
ASS/SSA), the voice-activity detector, the alignment engine and the timing
warp — with zero platform or audio dependencies and
#![forbid(unsafe_code)]. Because it's dependency-free, the
exact same code compiles two ways: to a native command-line tool,
and to WebAssembly (a ~120 KB .wasm
module) for the web.
In the browser, a thin JavaScript shell wires up the file pickers and
calls two functions exported from the WASM module
(sync_to_reference and sync_to_audio); the PCM is
handed across as a Float32Array with no copying or
serialization. Decoding deliberately lives outside the Rust core —
the browser does it with WebAudio — which is exactly what keeps the core
small, portable and WASM-clean. The page is just static files
(HTML + CSS + JS + the .wasm)
served from GitHub Pages, so once it has loaded there is no backend to talk
to and nothing to upload.
Why it's reliable
- It never reads the words. Alignment is driven purely by when there is sound, not by text, OCR or transcription. That makes it completely language-agnostic — the script, the encoding and the translation can't trip it up, because none of them are involved.
- It optimizes over the whole timeline at once. The correction is chosen by a dynamic program that maximizes total overlap across every line simultaneously, not a greedy guess from a handful of points. A few mis-detected frames can't swing the result; the best fit across thousands of candidate shifts wins.
- It handles the things that actually break subtitles. A single global shift (the whole file is early or late), and frame-rate drift — it scans the common play-rate conversions (23.976 / 24 / 25 / 29.97, e.g. the PAL speed-up) and corrects the stretch as well as the offset. The same engine can also do piecewise shifts that absorb ad-breaks or a different cut (exposed through the command-line tool).
- It is conservative when there's no evidence. Ties break toward the smallest shift, so an ambiguous input lands on “barely moved” rather than inventing a large bogus offset.
-
It is hard to crash. The core is memory-safe Rust with
no
unsafe, uses saturating arithmetic throughout, and tolerates pathological input (NaN/∞ samples, absurd sample rates) without panicking. The algorithm is a clean-room implementation of the well-studied overlap-maximization approach, and the core ships with a full unit-test suite.
Why it's fast
- It works on intervals, not samples. A two-hour film is hundreds of millions of audio samples; voice-activity detection collapses that to a few thousand speech intervals. The aligner only ever touches those intervals and your subtitle's lines, so its cost scales with the number of subtitle lines, not the length of the audio.
- The detector is a single linear pass. Speech detection is just framing the signal into 20 ms windows, taking each window's RMS energy, and keeping the windows louder than 1.5× the average — plain arithmetic, one sweep over the samples, no FFT and no model to load.
- The search is linear, not combinatorial. The aligner tries a grid of candidate shifts (±60 s in 20 ms steps — about six thousand of them), scoring each by a cheap interval-overlap sum. A precomputed “best previous offset” turns each line into an O(1) stay-or-switch decision, so the work is lines × shifts rather than lines × shifts², even with piecewise alignment.
- It runs as compiled WebAssembly. The detector and the search execute at near-native speed inside the WASM sandbox — not interpreted JavaScript — so the entire alignment is typically a matter of milliseconds once the audio is decoded.
- Nothing is uploaded. The slowest part of every other online sync tool is sending a multi-hundred-megabyte video to a server and waiting in a queue. Subomatic decodes and aligns in place, so the only real cost is the browser's native decode — and there's no network round-trip at all.
The alignment, in a little more detail
Each input is a set of half-open time intervals in milliseconds. For a candidate shift δ, a line's score is the total overlap between the line moved by δ and the (merged, non-overlapping) reference intervals. A dynamic program assigns every line an offset from the grid to maximize the summed overlap, minus a split penalty charged whenever two neighbouring lines take different offsets. With the penalty set to infinity (the default in the web app) every line shares one offset — a pure global shift; lower it and the program is free to break the file into a few independently-shifted segments to absorb an ad-break or a re-cut. Wrapped around that is the frame-rate scan: the same solve is run for each common play-rate ratio, and the highest-scoring combination of stretch + offset wins.