Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
205 lines
5.8 KiB
JavaScript
205 lines
5.8 KiB
JavaScript
/* global LazyLoader, VorbisPictureComment */
|
|
/* exported OggMetadata */
|
|
'use strict';
|
|
|
|
/**
|
|
* Parse files with Ogg metadata (e.g. Vorbis comments or Opus tags).
|
|
*
|
|
* Format information:
|
|
* http://wiki.xiph.org/VorbisComment
|
|
*/
|
|
var OggMetadata = (function() {
|
|
// Fields that should be stored as integers, not strings
|
|
var INTFIELDS = [
|
|
'tracknum', 'trackcount', 'discnum', 'disccount'
|
|
];
|
|
|
|
// Map ogg field names to metadata property names
|
|
var OGGFIELDS = {
|
|
title: 'title',
|
|
artist: 'artist',
|
|
album: 'album',
|
|
tracknumber: 'tracknum',
|
|
tracktotal: 'trackcount',
|
|
discnumber: 'discnum',
|
|
disctotal: 'disccount',
|
|
metadata_block_picture: 'picture'
|
|
};
|
|
|
|
/**
|
|
* A type thrown if we walk off the end of an Ogg page.
|
|
*/
|
|
function EndOfPageError() {}
|
|
|
|
/**
|
|
* Parse a file and return a Promise with the metadata.
|
|
*
|
|
* @param {BlobView} blobview The audio file to parse.
|
|
* @param {Metadata} metadata The (partially filled-in) metadata object.
|
|
* @return {Promise} A Promise returning the parsed metadata object.
|
|
*/
|
|
function parse(blobview) {
|
|
readIdentificationHeader(blobview);
|
|
return readCommentHeader(blobview);
|
|
}
|
|
|
|
/**
|
|
* Read the identification header of an Ogg container. It's always the first
|
|
* page.
|
|
*
|
|
* @param {BlobView} page The audio file being parsed.
|
|
*/
|
|
function readIdentificationHeader(page) {
|
|
var header = readPageHeader(page);
|
|
if (header.segment_table.length !== 1) {
|
|
throw new Error(
|
|
'ogg identification header expected as only packet of first page'
|
|
);
|
|
}
|
|
// Skip over the identification header.
|
|
page.advance(header.segment_table[0]);
|
|
}
|
|
|
|
/**
|
|
* Read the comment header of an Ogg container.
|
|
*
|
|
* @param {BlobView} page The audio file being parsed.
|
|
* @return {Promise} A Promise that resolves with the completed metadata.
|
|
*/
|
|
function readCommentHeader(page) {
|
|
var metadata = {};
|
|
var header = readPageHeader(page);
|
|
|
|
var sum = function(a, b) { return a + b; };
|
|
var comment_length = Array.reduce(header.segment_table, sum, 0);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
page.getMore(page.index, comment_length, (fullpage, err) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Look for a comment header from a supported codec
|
|
var first_byte = fullpage.readByte();
|
|
var valid = false;
|
|
switch (first_byte) {
|
|
case 3:
|
|
valid = fullpage.readBinaryText(6) === 'vorbis';
|
|
metadata.tag_format = 'vorbis';
|
|
break;
|
|
case 79:
|
|
valid = fullpage.readBinaryText(7) === 'pusTags';
|
|
metadata.tag_format = 'opus';
|
|
break;
|
|
}
|
|
if (!valid) {
|
|
reject('malformed ogg comment packet');
|
|
return;
|
|
}
|
|
|
|
readAllComments(fullpage, metadata);
|
|
|
|
LazyLoader.load('js/metadata/vorbis_picture.js').then(() => {
|
|
return VorbisPictureComment.parsePictureComment(metadata);
|
|
}).then(resolve);
|
|
} catch(e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Read the header for an Ogg page.
|
|
*
|
|
* @param {BlobView} page The audio file being parsed.
|
|
* @return {Object} An object containing the page's segment table.
|
|
*/
|
|
function readPageHeader(page) {
|
|
var capture_pattern = page.readBinaryText(4);
|
|
if (capture_pattern !== 'OggS') {
|
|
throw new Error('malformed ogg page header');
|
|
}
|
|
|
|
// Skip over some header fields until we reach the page segments.
|
|
page.advance(22);
|
|
|
|
var page_segments = page.readUnsignedByte();
|
|
var segment_table = page.readUnsignedByteArray(page_segments);
|
|
|
|
return {
|
|
segment_table: segment_table
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Read all the comments in an Ogg container.
|
|
*
|
|
* @param {BlobView} page The audio file being parsed.
|
|
* @param {Metadata} metadata The (partially filled-in) metadata object.
|
|
*/
|
|
function readAllComments(page, metadata) {
|
|
var vendor_string_length = page.readUnsignedInt(true);
|
|
page.advance(vendor_string_length); // skip libvorbis vendor string
|
|
|
|
var num_comments = page.readUnsignedInt(true);
|
|
for (var i = 0; i < num_comments; i++) {
|
|
try {
|
|
var comment = readComment(page);
|
|
if (comment) {
|
|
if (!(comment.field in metadata)) {
|
|
metadata[comment.field] = comment.value;
|
|
} else if (comment.field !== 'picture') {
|
|
// We already have a value, so append this new one.
|
|
metadata[comment.field] += ' / ' + comment.value;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (e instanceof EndOfPageError) {
|
|
return;
|
|
}
|
|
console.warn('Error parsing vorbis comment', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read a single comment field.
|
|
*
|
|
* @param {BlobView} page The audio file being parsed.
|
|
*/
|
|
function readComment(page) {
|
|
if (page.remaining() < 4) { // 4 bytes for comment-length variable
|
|
// TODO: handle metadata that uses multiple pages
|
|
throw new EndOfPageError();
|
|
}
|
|
var comment_length = page.readUnsignedInt(true);
|
|
if (comment_length > page.remaining()) {
|
|
// TODO: handle metadata that uses multiple pages
|
|
throw new EndOfPageError();
|
|
}
|
|
|
|
var comment = page.readUTF8Text(comment_length);
|
|
var equal = comment.indexOf('=');
|
|
if (equal === -1) {
|
|
throw new Error('missing delimiter in comment');
|
|
}
|
|
|
|
var fieldname = comment.substring(0, equal).toLowerCase().replace(' ', '');
|
|
var propname = OGGFIELDS[fieldname];
|
|
if (propname) { // Do we care about this field?
|
|
var value = comment.substring(equal + 1);
|
|
if (INTFIELDS.indexOf(propname) !== -1) {
|
|
value = parseInt(value, 10);
|
|
}
|
|
return {field: propname, value: value};
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
|
parse: parse
|
|
};
|
|
})();
|