Skip to content

Instantly share code, notes, and snippets.

@lf94
Created May 6, 2024 14:12
Show Gist options
  • Save lf94/46d23417b76ad888c095e03f8a2a8cdf to your computer and use it in GitHub Desktop.
Save lf94/46d23417b76ad888c095e03f8a2a8cdf to your computer and use it in GitHub Desktop.
A time-based terminal subtitle player written in Zig.
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