gts-x / src /lib /gts /models /timecode.ts
JAMES HAN
Initial commit — GTS Modern subtitle converter
dff41a8
Raw
History Blame Contribute Delete
5.09 kB
import { FPS, fpsToNumber } from "./enums";
export interface TimeCode {
h: number;
m: number;
s: number;
f: number;
fps: FPS;
locked: boolean;
negative: boolean;
bypass: boolean;
autospot: boolean;
autospotApproved: boolean;
}
export function createTimeCode(partial?: Partial<TimeCode>): TimeCode {
return {
h: 0, m: 0, s: 0, f: 0,
fps: FPS.PAL,
locked: false, negative: false, bypass: false,
autospot: false, autospotApproved: false,
...partial,
};
}
export function isTimeCodeZero(tc: TimeCode): boolean {
return tc.h === 0 && tc.m === 0 && tc.s === 0 && tc.f === 0;
}
export function timeCodeToFrames(tc: TimeCode): number {
const fps = Math.round(fpsToNumber(tc.fps));
if (tc.fps === FPS.NTSC_DF) {
// Drop-frame calculation
const totalMinutes = tc.h * 60 + tc.m;
const dropFrames = 2;
const frames =
tc.h * 107892 + // 30 * 60 * 59.94 rounded
tc.m * 1798 +
tc.s * 30 +
tc.f -
dropFrames * (totalMinutes - Math.floor(totalMinutes / 10));
return tc.negative ? -frames : frames;
}
const frames = tc.h * fps * 3600 + tc.m * fps * 60 + tc.s * fps + tc.f;
return tc.negative ? -frames : frames;
}
export function framesToTimeCode(totalFrames: number, fps: FPS): TimeCode {
const negative = totalFrames < 0;
let frames = Math.abs(totalFrames);
const fpsNum = Math.round(fpsToNumber(fps));
if (fps === FPS.NTSC_DF) {
const dropFrames = 2;
const framesPerMin = 1798; // 30 * 60 - 2
const framesPer10Min = 17982; // 10 * 1798 + 2
const d = Math.floor(frames / framesPer10Min);
let m = frames % framesPer10Min;
if (m < 2) {
m = 0;
} else {
m = Math.floor((m - 2) / framesPerMin) + 1;
}
const totalMinutes = d * 10 + m;
const adjustedFrames = frames + dropFrames * (totalMinutes - Math.floor(totalMinutes / 10));
const h = Math.floor(adjustedFrames / (fpsNum * 3600));
const remainder1 = adjustedFrames % (fpsNum * 3600);
const min = Math.floor(remainder1 / (fpsNum * 60));
const remainder2 = remainder1 % (fpsNum * 60);
const s = Math.floor(remainder2 / fpsNum);
const f = remainder2 % fpsNum;
return createTimeCode({ h, m: min, s, f, fps, negative });
}
const h = Math.floor(frames / (fpsNum * 3600));
frames %= fpsNum * 3600;
const m = Math.floor(frames / (fpsNum * 60));
frames %= fpsNum * 60;
const s = Math.floor(frames / fpsNum);
const f = frames % fpsNum;
return createTimeCode({ h, m, s, f, fps, negative });
}
export function timeCodeToMilliseconds(tc: TimeCode): number {
const fpsNum = fpsToNumber(tc.fps);
const ms = (tc.h * 3600 + tc.m * 60 + tc.s) * 1000 + Math.round((tc.f / fpsNum) * 1000);
return tc.negative ? -ms : ms;
}
export function millisecondsToTimeCode(ms: number, fps: FPS): TimeCode {
const negative = ms < 0;
ms = Math.abs(ms);
const fpsNum = fpsToNumber(fps);
const totalSeconds = ms / 1000;
const h = Math.floor(totalSeconds / 3600);
const m = Math.floor((totalSeconds % 3600) / 60);
const s = Math.floor(totalSeconds % 60);
const remainingMs = ms % 1000;
const f = Math.round((remainingMs / 1000) * fpsNum);
return createTimeCode({ h, m, s, f: Math.min(f, Math.round(fpsNum) - 1), fps, negative });
}
export function timeCodeToString(tc: TimeCode, separator = ":"): string {
const pad2 = (n: number) => n.toString().padStart(2, "0");
const sign = tc.negative ? "-" : "";
const frameSep = tc.fps === FPS.NTSC_DF ? ";" : separator;
return `${sign}${pad2(tc.h)}:${pad2(tc.m)}:${pad2(tc.s)}${frameSep}${pad2(tc.f)}`;
}
export function timeCodeToSrtString(tc: TimeCode): string {
const pad2 = (n: number) => n.toString().padStart(2, "0");
const pad3 = (n: number) => n.toString().padStart(3, "0");
const ms = Math.round((tc.f / fpsToNumber(tc.fps)) * 1000);
return `${pad2(tc.h)}:${pad2(tc.m)}:${pad2(tc.s)},${pad3(ms)}`;
}
export function parseSrtTimeCode(str: string, fps: FPS): TimeCode {
// Format: HH:MM:SS,mmm
const match = str.trim().match(/^(\d{1,2}):(\d{2}):(\d{2})[,.](\d{3})$/);
if (!match) return createTimeCode({ fps });
const h = parseInt(match[1]);
const m = parseInt(match[2]);
const s = parseInt(match[3]);
const ms = parseInt(match[4]);
const f = Math.round((ms / 1000) * fpsToNumber(fps));
return createTimeCode({ h, m, s, f: Math.min(f, Math.round(fpsToNumber(fps)) - 1), fps });
}
export function addTimeCodes(a: TimeCode, b: TimeCode): TimeCode {
const framesA = timeCodeToFrames(a);
const framesB = timeCodeToFrames(b);
return framesToTimeCode(framesA + framesB, a.fps);
}
export function subtractTimeCodes(a: TimeCode, b: TimeCode): TimeCode {
const framesA = timeCodeToFrames(a);
const framesB = timeCodeToFrames(b);
return framesToTimeCode(framesA - framesB, a.fps);
}
export function compareTimeCodes(a: TimeCode, b: TimeCode): number {
return timeCodeToFrames(a) - timeCodeToFrames(b);
}
export function durationMs(tcIn: TimeCode, tcOut: TimeCode): number {
return timeCodeToMilliseconds(tcOut) - timeCodeToMilliseconds(tcIn);
}