Created
May 6, 2024 14:12
-
-
Save lf94/46d23417b76ad888c095e03f8a2a8cdf to your computer and use it in GitHub Desktop.
A time-based terminal subtitle player written in Zig.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const std = @import("std"); | |
const io = std.io; | |
const fs = std.fs; | |
const mem = std.mem; | |
const fmt = std.fmt; | |
const time = std.time; | |
const math = std.math; | |
const process = std.process; | |
pub fn main() !void { | |
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
var allocator = gpa.allocator(); | |
var args = try process.argsWithAllocator(allocator); | |
_ = args.next(); | |
const arg1 = args.next(); | |
if (arg1) |file_path| { | |
try SRT.play(allocator, file_path); | |
} else { | |
const stdout = io.getStdOut().writer(); | |
try stdout.print("Usage: fabyu <filename>\n", .{}); | |
} | |
} | |
const Syllable = []const u8; | |
const Word = struct { | |
syllables: []Syllable, | |
}; | |
const Sentence = struct { | |
words: []Word, | |
contentLength: usize, | |
}; | |
const Subtitle = struct { | |
sentence: Sentence = Sentence { | |
.words = &[0]Word{}, | |
.contentLength = 0 | |
}, | |
start: u64 = 0, | |
end: u64 = 0, | |
}; | |
const SRT = struct { | |
fn play(allocator: mem.Allocator, file_path: []const u8) !void { | |
const stdout = io.getStdOut().writer(); | |
var file: fs.File = try fs.cwd().openFile(file_path, .{}); | |
var buf: [1024]u8 = .{0} ** 1024; | |
var timer: i64 = 0; | |
while (SRT.getSubtitle(allocator, file, &buf)) |subtitle| { | |
std.time.sleep(@intCast(u64, math.max(0, @intCast(i64, subtitle.start) - timer)) * std.time.ns_per_ms); | |
timer = @intCast(i64, subtitle.start); | |
try SRT.playSyllables(stdout, subtitle); | |
std.time.sleep(@intCast(u64, math.max(0, @intCast(i64, subtitle.end) - timer)) * std.time.ns_per_ms); | |
timer = @intCast(i64, subtitle.end); | |
} | |
} | |
fn readTimestamp(timeBytes: []const u8) !u64 { | |
var splitHMSM = mem.split(u8, timeBytes, ","); | |
const hoursMinutesSeconds = splitHMSM.next().?; | |
var splitHMS = mem.split(u8, hoursMinutesSeconds, ":"); | |
const hoursText = splitHMS.next().?; | |
const minutesText = splitHMS.next().?; | |
const secondsText = splitHMS.next().?; | |
const hours = try fmt.parseInt(u64, hoursText, 10); | |
const minutes = try fmt.parseInt(u64, minutesText, 10); | |
const seconds = try fmt.parseInt(u64, secondsText, 10); | |
const msHMS = hours * 1000 * 60 * 60 + minutes * 1000 * 60 + seconds * 1000; | |
const msText = splitHMSM.next().?; | |
const ms = try fmt.parseInt(u64, msText, 10); | |
return msHMS + ms; | |
} | |
fn getSubtitle(allocator: mem.Allocator, file: fs.File, buf: []u8) ?Subtitle { | |
var reader = file.reader(); | |
// Ignore the subtitle ordering number. | |
_ = reader.readUntilDelimiter(buf, '\n') catch return null; | |
// Timing (XX:XX:XX,XXX --> XX:XX:XX,XXX), converted into time on screen. | |
const timeBytes = reader.readUntilDelimiter(buf, '\n') catch return null; | |
const timeTrimmed = mem.trimRight(u8, timeBytes, &.{'\r'}); | |
var splitTime = mem.split(u8, timeTrimmed, " --> "); | |
const start = SRT.readTimestamp(splitTime.next().?) catch return null; | |
const end = SRT.readTimestamp(splitTime.next().?) catch return null; | |
// The subtitle text, spread across multiple lines until an empty line. | |
var words = std.ArrayList(Word).init(allocator); | |
var length: usize = 0; | |
while (true) { | |
const lineBytes = reader.readUntilDelimiter(buf, '\n') catch return null; | |
const lineTrimmed = mem.trimRight(u8, lineBytes, &.{'\r'}); | |
if (lineTrimmed.len == 0) break; | |
var splitLine = mem.split(u8, lineTrimmed, " "); | |
while (splitLine.next()) |word| { | |
if (word.len == 0) continue; | |
var syllables = std.ArrayList(Syllable).init(allocator); | |
const bytes = allocator.alloc(u8, word.len) catch return null; | |
mem.copy(u8, bytes, word); | |
syllables.append(bytes) catch return null; | |
words.append(Word { | |
.syllables = syllables.toOwnedSlice() | |
}) catch return null; | |
length += word.len; | |
} | |
} | |
return Subtitle { | |
.sentence = Sentence { | |
.words = words.toOwnedSlice(), | |
.contentLength = length | |
}, | |
.start = start, | |
.end = end, | |
}; | |
} | |
fn playSyllables(writer: anytype, subtitle: Subtitle) !void { | |
var word_index: usize = 0; | |
var syllable_index: usize = 0; | |
for (subtitle.sentence.words) |word| { | |
for (word.syllables) |syllable| { | |
try writer.print("{0s}", .{syllable}); | |
const waitForMs = SRT.msWaitOnSyllable(word_index, syllable_index, subtitle); | |
// try writer.print(" ({})", .{ waitForMs }); | |
std.time.sleep(waitForMs * std.time.ns_per_ms); | |
syllable_index += 1; | |
} | |
try writer.print(" ", .{}); | |
syllable_index = 0; | |
word_index += 1; | |
} | |
try writer.print("\n", .{}); | |
} | |
fn msWaitOnSyllable(index_word: usize, index_syllable: usize, subtitle: Subtitle) u64 { | |
return (subtitle.end - subtitle.start) / (subtitle.sentence.contentLength / subtitle.sentence.words[index_word].syllables[index_syllable].len); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment