UNPKG

kar-to-ass

Version:

Converts midi/kar karaoke files to ass format

165 lines (136 loc) 5.46 kB
#!/usr/bin/env node import { asyncExists, asyncReadFile, asyncWriteFile, clone, usToAss } from './utils'; import stringify from 'ass-stringify'; import { v4 as uuid } from 'uuid'; import cli from 'commander'; import { KarData, Options, KarTrack, KarEvent } from './types'; import { parseKar as _parseKar } from './kar'; const ass = require('./assTemplate'); const pjson = require('../package.json'); function generateASSLine(line: KarEvent[], sectionId:string, options: Options) { const offset: number = options.offset || 0; const syncType: string = options.progressive ? 'kf' : 'k'; const dialogue = clone(ass.dialogue); const comment = clone(ass.dialogue); // keep times as µs const lineStart = line[0].timeUs + 1e4 * (offset - ass.previewTime); const lineStartComment = line[0].timeUs + 1e4 * offset; const lineEnd = line[line.length-1].timeUs + 1e4 * (offset + ass.postviewTime); const correction = lineStart < 0 ? lineStart : 0; dialogue.value.Style = sectionId; dialogue.value.Start = usToAss(lineStart - correction); dialogue.value.End = usToAss(lineEnd - correction); dialogue.value.Text = writeLineAsAss(line, syncType); dialogue.value.Effect = 'fx'; comment.value.Style = sectionId; comment.value.Start = usToAss(lineStartComment - correction); comment.value.End = usToAss(lineEnd - correction); comment.value.Text = writeLineAsAss(line, syncType, true); comment.value.Effect = 'karaoke'; comment.key = 'Comment'; return { dialogue, comment }; } export function parseKar(fileContent: Buffer): KarData { // Proxy Method return _parseKar(fileContent); } export function convertKarStringToAss(fileContent: Buffer, options: Options): string { return convertKarToAss(parseKar(fileContent), options); } export function convertKarToAss(kar: KarData, options: Options): string { const defaultStyle = ass.styles.body.filter(style => style.key == 'Style')[0]; const dialogues = []; const comments = []; const styles = clone(ass.styles); const script = clone(ass.dialogue); script.value.Effect = ass.scriptFX; script.value.Text = ass.script; script.key = 'Comment'; comments.push(script); for(const track of kar.tracks) { const sectionId: string = uuid().replace(/-/g, '').substring(0,16); const sectionStyle = clone(defaultStyle); sectionStyle.value.Name = sectionId; styles.body.push(sectionStyle); for(const line of splitLines(track)) { const ASSLines = generateASSLine(line, sectionId, options); comments.push(clone(ASSLines.comment)); dialogues.push(clone(ASSLines.dialogue)); } } const scriptInfo = clone(ass.scriptInfo); scriptInfo.body[0].value = scriptInfo.body[0].value.replace('%version', pjson.version); scriptInfo.body[1].value = scriptInfo.body[1].value.replace('%options', JSON.stringify(options)); const events = clone(ass.events); events.body = events.body.concat(comments, dialogues); return stringify([scriptInfo, styles, events]); } function splitLines(track: KarTrack): KarEvent[][] { const lines: KarEvent[][] = []; let tempLine: KarEvent[] = []; for(const event of track) { if(event.type === 'EndOfLine' && tempLine.length > 0) { tempLine.push(event); lines.push(tempLine); tempLine = []; } else if(event.type === 'Lyrics') { tempLine.push(event); // } else { // console.debug(`Ignored ${JSON.stringify(event)}`) } } return lines; } function writeLineAsAss(line: KarEvent[], syncType: string, comment?:boolean) { if(line.length == 0) return ''; const realPreview = Math.max(Math.min(ass.previewTime, line[0].timeUs * 1e-4), 0); const resArray: string[] = []; if (!comment) resArray.push(`{\\k${realPreview}\\fad(300,200)}`); for(let i = 0; i < line.length-1; i++) { // console.error(`At ${usToAss(line[i].timeUs)} for ${Math.round(1e-4 * (line[i+1].timeUs-line[i].timeUs))} cs: '${line[i].text}'`); resArray.push(`${line[i].text.startsWith(' ') ? ' ' : ''}{\\${syncType}${Math.round(1e-4 * (line[i+1].timeUs-line[i].timeUs))}}${line[i].text.trim()}${(line[i].text.endsWith(' ') ? ' ' : '')}`); } return resArray.join(''); } async function mainCLI() { const argv = parseArgs(); if(!argv.in) throw '--in argument is required'; const karFile: string = argv.in; const correctionString: string = argv.correction; const outFile: string = argv.out; const correction = (correctionString && !isNaN(+correctionString)) ? +correctionString : 0; const options: Options = { offset: correction, progressive: argv.progressive || false, }; if(!await asyncExists(karFile)) throw `File ${karFile} does not exist`; const fileContent = await asyncReadFile(karFile); const kar: KarData = parseKar(fileContent); const assString = convertKarToAss(kar, options); if(!outFile) { return assString; } else { await asyncWriteFile(outFile, assString); return ''; } } function parseArgs() { const argv = process.argv.filter(e => e !== '--'); return cli .command('kar-to-ass') .description('Converts midi/kar files to ass format') .version(pjson.version) .option('-i, --in <karFile> (required)', 'the file to read data from') .option('-o, --out <assFile>', 'if set, saves the converted data there instead of printing it on stdout') .option('-c, --correction <cs>', 'offsets said amount of cs on each line', 0) .option('-p, --progressive', 'use kf instead of k ') .parse(argv); } if (require.main === module) mainCLI() .then(data => console.log(data)) .catch(err => console.error(err));