UNPKG

kar-to-ass

Version:

Converts midi/kar karaoke files to ass format

125 lines (118 loc) 3.94 kB
import { KarData, KarTimebase, KarTrack, KarTimebaseSection } from './types'; import { parseMidi } from 'midi-file'; import { clone } from './utils'; import debug from 'debug'; const debugTimebase = debug('timebase'); const debugTracks = debug('tracks'); const debugLyrics = debug('lyrics'); const isEventLyrics = event => (['text', 'lyrics'].includes(event.type) && !event.text.startsWith('@')); const DEFAULT_EOL = 200; export function parseKar(fileContent: Buffer): KarData { const midi = parseMidi(fileContent); const timebase: KarTimebase = getTimebase(midi); const rawTracks = getTracks(midi); rawTracks.forEach(track => applyTimebase(track, timebase)); return { timebase: timebase, tracks: rawTracks, }; } function applyTimebase(track: KarTrack, timebase: KarTimebase): void { const tmpTimebase: KarTimebase = clone(timebase); let time: KarTimebaseSection = tmpTimebase.shift(); let baseUs: number = 0; for(const event of track) { while(event.ticks >= time.tickEnd) { baseUs += time.microSecondsPerTick * (time.tickEnd - time.tickStart); time = tmpTimebase.shift(); } event.timeUs = baseUs + time.microSecondsPerTick * (event.ticks - time.tickStart); // console.error(`Mapped ${JSON.stringify({timeMillis: Math.round(event.timeUs/1000), type: event.type, text: event.text})}`) } debugTracks('Applied timebase to ', track); } function getTimebase(midi: any): KarTimebase { const tempoTrack: any = midi.tracks.filter(track => track.some(event => event.type == 'setTempo'))[0]; debugTimebase('Tempo track is ', tempoTrack); const timebase: KarTimebase = []; let curr: number = 0; const tempoEvents: any = tempoTrack.filter(e => e.type === 'setTempo'); for(let i = 0; i < tempoEvents.length; i++) { const tempoEvent: any = tempoEvents[i]; const duration = (i+1<tempoEvents.length) ? tempoEvents[i+1].deltaTime : Number.MAX_SAFE_INTEGER; timebase.push({ tickStart: curr, tickEnd: curr + duration, microSecondsPerTick: tempoEvent.microsecondsPerBeat / midi.header.ticksPerBeat, }); curr += duration; debugTimebase('Added timebase event', timebase[timebase.length - 1]); } return timebase; } function getTracks(midi: any, defaultEOL: number = DEFAULT_EOL): KarTrack[] { const response: KarTrack[] = []; debugTracks('All tracks from midi', midi.tracks); const tracksWithLyrics: any[] = midi.tracks .filter(events => events.some(isEventLyrics)); debugTracks('Filtered tracks with lyric events', tracksWithLyrics); for(const rawTrack of tracksWithLyrics) { let timeCursor: number = 0; let track: KarTrack = []; for(const rawEvent of rawTrack) { if(isEventLyrics(rawEvent)) { const cleanText: string = rawEvent.text.replace(/[\/\\]/g, ''); debugLyrics('Lyrics line: ', cleanText, rawEvent); if(['/', '\r', '\n', '\r\n'].includes(rawEvent.text)) { timeCursor += rawEvent.deltaTime; track.push({ ticks: timeCursor, type: 'EndOfLine', }); } else if(rawEvent.text.match(/^[\/\\]/)) { track.push({ ticks: timeCursor + defaultEOL, type: 'EndOfLine', }); timeCursor += rawEvent.deltaTime; track.push({ ticks: timeCursor, type: 'Lyrics', text: cleanText, }); } else if(rawEvent.text.match(/[\/\\]$/)) { timeCursor += rawEvent.deltaTime; track.push({ ticks: timeCursor, type: 'Lyrics', text: cleanText, }); track.push({ ticks: timeCursor + defaultEOL, type: 'EndOfLine', }); } else { timeCursor += rawEvent.deltaTime; track.push({ ticks: timeCursor, type: 'Lyrics', text: cleanText, }); } } else { timeCursor += rawEvent.deltaTime; } } if(track[track.length-1].type === 'Lyrics') { track.push({ ticks: timeCursor + defaultEOL, type: 'EndOfLine', }); } debugTracks('Finalized track: ', track); response.push(track); } return response; }