0) $out .= '{\k' . $lead . '}'; $n = count($words); foreach ($words as $i => $w) { $wStart = (float) $w['start']; $wEnd = (float) $w['end']; $dur = max(1, (int) round(($wEnd - $wStart) * 100)); $out .= '{\k' . $dur . '}' . self::escape((string) $w['text']); if ($i < $n - 1) { $gap = (int) round((((float) $words[$i + 1]['start']) - $wEnd) * 100); $out .= ($gap > 0 ? '{\k' . $gap . '}' : '') . ' '; } } return $out; } /** ASS timestamp: H:MM:SS.cc (centiseconds). */ private static function ts(float $t): string { if ($t < 0) $t = 0; $cs = (int) round($t * 100); $h = intdiv($cs, 360000); $m = intdiv($cs % 360000, 6000); $s = intdiv($cs % 6000, 100); $c = $cs % 100; return sprintf('%d:%02d:%02d.%02d', $h, $m, $s, $c); } private static function escape(string $s): string { // Strip ASS override delimiters and collapse newlines. $s = str_replace(['{', '}', '\\'], ['(', ')', '/'], $s); return str_replace(["\r\n", "\n", "\r"], ' ', $s); } private static function header(): string { // Colours are &HAABBGGRR. Primary = sung (white), Secondary = unsung // (translucent grey), heavy outline + shadow for legibility over artwork. return "[Script Info]\n" . "ScriptType: v4.00+\n" . 'PlayResX: ' . self::W . "\n" . 'PlayResY: ' . self::H . "\n" . "WrapStyle: 0\n" . "ScaledBorderAndShadow: yes\n\n" . "[V4+ Styles]\n" . "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n" . "Style: Lyrics,Sans,54,&H00FFFFFF,&H64C8C8C8,&H00101010,&H80000000,-1,0,0,0,100,100,0,0,1,3,2,2,80,80,70,1\n\n" . "[Events]\n" . "Format: Layer, Start, End, Style, Name, MarginL, MarginR, Effect, Text\n"; } }