/** * syllabes.js * @version 1.0.0 * @link https://github.com/Komanaki/syllabesjs * @license MIT */ var Syllabes = (function () { function Syllabes() { /** * Default configuration values * @type {SyllabesConfig} */ this.default_config = { offset: 0, syllable_precision: false, start_as_previous_end: false }; } /** * Parse a subtitles file given a certain format and optionally some options * @param {string} format File format * @param {string} file File contents * @param {SyllabesConfig} user_config Options for the parsing */ Syllabes.prototype.parse = function (format, file, user_config) { if (user_config === void 0) { user_config = null; } var config = (user_config !== null) ? user_config : {}; // Add missing values in user config with the default config for (var key in this.default_config) { if (config[key] === undefined) { config[key] = this.default_config[key]; } } // Ensure that the offset is a number in any case if (config.offset !== null && !isNaN(parseInt(config.offset + ''))) { config.offset = parseInt(config.offset + ''); } else { config.offset = 0; } var parser = null; // Instanciate the right parser if (format.toLowerCase() == 'ultrastar') { parser = new UltrastarParser(config); } else if (['srt', 'subrip'].indexOf(format.toLowerCase()) > -1) { parser = new SubRipParser(config); } else if (['vtt', 'webvtt'].indexOf(format.toLowerCase()) > -1) { parser = new WebVTTParser(config); } // Stop if no parser was found for the asked file format if (parser == null) { console.error("Couldn't file any parser for the file format \"" + format + "\""); return null; } // Parse the file, destroy the parser and return the output var output = parser.parse(file); parser = null; return output; }; return Syllabes; }()); var SubRipParser = (function () { function SubRipParser(config) { this.config = config; this.track = []; } /** * Parse a SubRip file * @param {string} file SubRip file content */ SubRipParser.prototype.parse = function (file) { // Let's parse the file line by line var lines = file.replace(/\r+/g, '').split('\n'); var open = false; // If a sentence is being parsed or not var ID = 1; // Current sentence ID var text = ''; // Current sentence text var start = 0; var end = 0; var left = 0; var top = 0; var width = 0; var height = 0; // Parse each line of the file until the end for (var i = 0; i < lines.length; i++) { // Delete the trailing spaces var line = lines[i].replace(/^\s*/, ''); // Check if it's an ID var matchID = line.match(/^(\d+)$/); if (matchID != null && matchID.length > 0) { open = true; ID = parseInt(matchID[0]); continue; } // Check if it's a sentence duration var matchDuration = line.match(/^\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}/); if (matchDuration != null && matchDuration.length > 0) { if (!open) { open = true; } // Check if there's coordinates and add them to the sentence if present var matchPos = line.match(/X1:(\d+) X2:(\d+) Y1:(\d+) Y2:(\d+)$/); if (matchPos != null && matchPos.length > 0) { left = parseInt(matchPos[1]); width = parseInt(matchPos[2]) - left; top = parseInt(matchPos[3]); height = parseInt(matchPos[4]) - top; } matchDuration = matchDuration[0].split(' --> '); start = this.parseTimecode(matchDuration[0]) + this.config.offset; end = this.parseTimecode(matchDuration[1]) + this.config.offset; continue; } // Add a text line if it's not empty and a sentence is open if (line.length > 0 && open) { if (text.length > 0) { text += '
' + this.parseText(line); } else { text = this.parseText(line); } } // Close the current sentence if we encounter a blank line or if it's the end of the file if ((line.length == 0 || i == lines.length - 1) && open) { // Fix the first sentence start if the offset is negative if (this.config.offset < 0 && start < 0) { start = 0; } var sentence = { id: ID, start: start, end: end, duration: end - start, text: text }; // Add the sentence coordinates if they're not empty if (left != 0 && top != 0) { sentence.position = { left: left, top: top, width: width, height: height }; } // Add the sentence to the track this.track.push(sentence); // Reset the sentence variables open = false; ID++; text = ''; start = 0; end = 0; left = 0; top = 0; width = 0; height = 0; continue; } } return { track: this.track }; }; /** * Converts a SRT timecode into milliseconds * @param {string} timecode SRT timecode */ SubRipParser.prototype.parseTimecode = function (timecode) { var parts = timecode.split(':'); parts = parts.concat(parts.pop().split(',')); return parseInt(parts[0]) * 3600000 + parseInt(parts[1]) * 60000 + parseInt(parts[2]) * 1000 + parseInt(parts[3]); }; /** * Apply some modifications to a SRT text * @param {string} text SRT text */ SubRipParser.prototype.parseText = function (text) { text = text.replace(/{(\/?[biu]{1})}/g, '<$1>'); return text; }; return SubRipParser; }()); var UltrastarParser = (function () { function UltrastarParser(config) { this.config = config; this.meta = { title: null, artist: null, mp3: null, bpm: null, gap: 0, duet: false }; this.track = []; this.track_duet = []; } /** * Parse an Ultrastar text file * @param {string} file Ultrastar text file content */ UltrastarParser.prototype.parse = function (file) { // Let's parse the file line by line var lines = file.replace(/\r+/g, '').split('\n'); var sentenceID = 1; // Current sentence ID var trackID = 1; // Current track ID var relative = false; // Relative or absolute beats notation for the syllables var beatsCount = 0; // Count of the elapsed beats var beatDuration = null; // Duration of a beat (milliseconds) var currentStart = null; // Start of the current sentence (milliseconds) var previousEnd = null; // End of the previous sentence (milliseconds) var syllables = []; // Syllables list of the current sentence // Parse each line of the file until the end (or a "E" line) for (var i = 0; i < lines.length; i++) { // Delete the trailing spaces var line = lines[i].replace(/^\s*/, ''); // Ignore the line if it's empty if (line.replace(/\s*/g, '').length == 0) { continue; } // Metadata line if (line[0] == '#') { // Regex parsing of the line var matches = line.match(/(\w+):(.+)/); // Ignore the line if it's invalid if (matches == null || matches.length == 0) { continue; } // Split of the regex result matches = matches[0].split(':'); var keyword = matches[0].toLowerCase(); var value = matches[1]; // Float conversion of the BPM / GAP if (keyword == 'bpm' || keyword == 'gap') { value = parseFloat(value.replace(',', '.')); } // Beat duration calculation from the BPM if (keyword == 'bpm') { beatDuration = (60000) / (value * 4); } // Override the config offset if absent, and set the first sentence start as the GAP value if (keyword == 'gap') { if (this.config.offset !== null) { value += this.config.offset; } currentStart = value; } // Mark the syllables beat notation as relative if (keyword == 'relative' && value.toLowerCase() == 'yes') { relative = true; } // Save the data if it isn't empty if (value != '') { this.meta[keyword] = value; } continue; } // Player change or end of file when a sentence isn't finished if ((line == 'P2' || line == 'P 2' || line[0] == 'E') && syllables.length > 0) { // Create a new sentence var sentence = this.makeSentence(sentenceID, syllables, currentStart, null, previousEnd); if (currentStart != null) { currentStart = null; } // Add the sentence to the current track if (trackID == 1) { this.track.push(sentence); } else { this.track_duet.push(sentence); } // Increment the sentence ID, reset the current sentence syllables sentenceID++; syllables = []; } // Lyrics player change if (line == 'P2' || line == 'P 2') { trackID = 2; this.meta.duet = true; continue; } // Syllable line if ([':', '*', 'F'].indexOf(line[0]) > -1) { // Regex parsing of the line var matches = line.match(/^[:*F] (\d+) (\d+) (-?\d+) (.+)/); // Ignore the line if it's invalid if (matches == null || matches.length == 0) { continue; } var syllable = { type: 'normal' }; // Get the syllable text syllable.text = matches[0].split(' ').splice(4).join(' '); // Split of the regex result matches = matches[0].split(' ').splice(1, 3); // Add the start time of the syllable, with absolute or relative beats if (!relative) { syllable.start = Math.floor(this.meta.gap + parseInt(matches[0]) * beatDuration); } else { syllable.start = Math.floor(this.meta.gap + (beatsCount + parseInt(matches[0])) * beatDuration); } // Add the duration, end time, pitch syllable.duration = Math.floor(parseInt(matches[1]) * beatDuration); syllable.end = syllable.start + syllable.duration; syllable.pitch = parseInt(matches[2]); // Fix the first syllable start if the offset is negative if (this.meta.gap < 0 && syllable.start < 0) { syllable.start = 0; syllable.duration = syllable.end; } // Change the type for special syllables if (line[0] == '*') { syllable.type = 'golden'; } else if (line[0] == 'F') { syllable.type = 'freestyle'; } // Increment the total beats count with the syllable beats beatsCount += parseInt(matches[1]); // Add the syllable syllables.push(syllable); } // New line mark if (line[0] == '-') { // Regex parsing of the line var matches = line.match(/^- (\d+)\s?(\d+)/); // Ignore the line if it's invalid if (matches == null || matches.length == 0) { continue; } // Split of the regex result matches = matches[0].split(' ').splice(1); var currentEnd = null; // Add the end time of the sentence, with absolute or relative beats if (!relative) { currentEnd = Math.floor(this.meta.gap + parseInt(matches[0]) * beatDuration); } else { currentEnd = Math.floor(this.meta.gap + (beatsCount + parseInt(matches[0])) * beatDuration); } // Fix the first sentence start if the offset is negative if (this.meta.gap < 0 && currentStart < 0) { currentStart = 0; } // Create a new sentence var sentence = this.makeSentence(sentenceID, syllables, currentStart, currentEnd, previousEnd); if (currentStart != null) { currentStart = null; } // Save the sentence end time for possible use on the next sentence previousEnd = sentence.end; // Save the start time of the next sentence if it's present on the line, with absolute or relative beats if (matches[1] !== undefined) { if (!relative) { currentStart = Math.floor(this.config.offset + parseInt(matches[1]) * beatDuration); beatsCount = parseInt(matches[1]); } else { currentStart = Math.floor(this.config.offset + (beatsCount + parseInt(matches[1])) * beatDuration); beatsCount += parseInt(matches[1]); } } // Add the sentence to the current track if (trackID == 1) { this.track.push(sentence); } else { this.track_duet.push(sentence); } // Increment the sentence ID, reset the current sentence syllables sentenceID++; syllables = []; } // End of file if (line[0] == 'E') { break; } } return { meta: this.meta, track: this.track, track_duet: (this.track_duet.length > 0) ? this.track_duet : null }; }; /** * Make a new sentence * @param {number} id ID of the sentence * @param {any[]} syllables Syllables list of the sentence * @param {number} start Start time of the sentence * @param {number} end End time of the sentence * @param {number} previousEnd End time of the previous sentence */ UltrastarParser.prototype.makeSentence = function (id, syllables, start, end, previousEnd) { var sentence = { id: id, start: syllables[0].start, end: syllables[syllables.length - 1].end }; // Insert sentence syllables as objects or as a string if (this.config.syllable_precision) { sentence.syllables = syllables; } else { sentence.text = ''; for (var j = 0; j < syllables.length; j++) { sentence.text += syllables[j].text; } } // Add the start of the sentence if it was present on the last "sentence end" line if (start != null) { sentence.start = start; } else if (previousEnd != null && this.config.start_as_previous_end) { sentence.start = previousEnd; } // Add the end of the sentence if any if (end != null) { sentence.end = end; } // Set the duration with start and end sentence.duration = sentence.end - sentence.start; return sentence; }; return UltrastarParser; }()); var WebVTTParser = (function () { function WebVTTParser(config) { this.config = config; this.track = []; this.style = []; } /** * Parse a WebVTT file * @param {string} file WebVTT file content */ WebVTTParser.prototype.parse = function (file) { // Let's parse the file line by line var lines = file.replace(/\r+/g, '').split('\n'); var comment = false; // If a comment is being parsed or not var open = false; // If a sentence is being parsed or not var ID = 1; // Current sentence ID var text = ''; // Current sentence text var start = 0; var end = 0; var left = 0; var top = 0; var width = 0; var height = 0; if (lines[0] != 'WEBVTT') { console.error("The given file is not a WebVTT document."); return null; } // Parse each line of the file until the end for (var i = 0; i < lines.length; i++) { // Delete the trailing spaces var line = lines[i].replace(/^\s*/, ''); // Ignore the next lines if it's a comment if (line.substr(0, 4) == 'NOTE') { comment = true; continue; } // Close the comment if (line.length == 0 && comment) { comment = false; continue; } // Ignore if we're in comment mode if (comment) { continue; } // Save the CSS rule if (line.substr(0, 5) == '::cue') { this.style.push(this.parseStyle(line)); continue; } // If this isn't the last line of the file... if (i < lines.length - 1) { var matchNextDuration = lines[i + 1].match(/^\d{2}:\d{2}:\d{2}.\d{3} --> \d{2}:\d{2}:\d{2}.\d{3}/); // If we're not parsing a sentence and we get something before a duration line, then it's a sentence ID if (line.length > 0 && matchNextDuration != null && matchNextDuration.length > 0 && !open) { open = true; ID = line; continue; } } // Check if it's a sentence duration var matchDuration = line.match(/^\d{2}:\d{2}:\d{2}.\d{3} --> \d{2}:\d{2}:\d{2}.\d{3}/); if (matchDuration != null && matchDuration.length > 0) { if (!open) { open = true; } // Check if there's coordinates and add them to the sentence if present var matchPos = line.match(/X1:(\d+) X2:(\d+) Y1:(\d+) Y2:(\d+)$/); if (matchPos != null && matchPos.length > 0) { left = parseInt(matchPos[1]); width = parseInt(matchPos[2]) - left; top = parseInt(matchPos[3]); height = parseInt(matchPos[4]) - top; } matchDuration = matchDuration[0].split(' --> '); start = this.parseTimecode(matchDuration[0]) + this.config.offset; end = this.parseTimecode(matchDuration[1]) + this.config.offset; continue; } // Add a text line if it's not empty and a sentence is open if (line.length > 0 && open) { if (text.length > 0) { text += '
' + this.parseText(line); } else { text = this.parseText(line); } } // Close the current sentence if we encounter a blank line or if it's the end of the file if ((line.length == 0 || i == lines.length - 1) && open) { // Fix the first sentence start if the offset is negative if (this.config.offset < 0 && start < 0) { start = 0; } var sentence = { id: ID, start: start, end: end, duration: end - start, text: text }; // Add the sentence coordinates if they're not empty if (left != 0 && top != 0) { sentence.position = { left: left, top: top, width: width, height: height }; } // Add the sentence to the track this.track.push(sentence); // Reset the sentence variables open = false; ID = (typeof ID == 'number') ? ID++ : ID; text = ''; start = 0; end = 0; left = 0; top = 0; width = 0; height = 0; continue; } } return { track: this.track, style: this.style }; }; /** * Converts a SRT timecode into milliseconds * @param {string} timecode SRT timecode */ WebVTTParser.prototype.parseTimecode = function (timecode) { var parts = timecode.split(':'); parts = parts.concat(parts.pop().split('.')); return parseInt(parts[0]) * 3600000 + parseInt(parts[1]) * 60000 + parseInt(parts[2]) * 1000 + parseInt(parts[3]); }; /** * Apply some modifications to a SRT text * @param {string} text SRT text */ WebVTTParser.prototype.parseText = function (text) { text = text.replace(/{(\/?[biu]{1})}/g, '<$1>'); return text; }; /** * Parse a WebVTT-specific style to be used in a HTML document * @param {string} style CSS stylesheet content */ WebVTTParser.prototype.parseStyle = function (style) { // TODO return style; }; return WebVTTParser; }()); //# sourceMappingURL=maps/syllabes.js.map