Skip to content
This repository has been archived by the owner on Nov 3, 2021. It is now read-only.

Commit

Permalink
Bug 1039639 - [music] Support parsing FLAC metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
Jim committed Dec 3, 2014
1 parent 8749f73 commit 17f58f7
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 4 deletions.
176 changes: 176 additions & 0 deletions apps/music/js/metadata/flac.js
@@ -0,0 +1,176 @@
/* exported FLACMetadata */
'use strict';

/**
* Parse metadata for FLAC files (this is very similar to Ogg files, but with a
* different container format).
*
* Format information:
* http://flac.sourceforge.net/format.html
*/
var FLACMetadata = (function() {
// Fields that should be stored as integers, not strings
var INTFIELDS = [
'tracknum', 'trackcount', 'discnum', 'disccount'
];

// Map FLAC field names to metadata property names
var FLACFIELDS = {
title: 'title',
artist: 'artist',
album: 'album',
tracknumber: 'tracknum',
tracktotal: 'trackcount',
discnumber: 'discnum',
disctotal: 'disccount'
};

/**
* 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, metadata) {
// First four bytes are "fLaC" or we wouldn't be here.
blobview.seek(4);

metadata.tag_format = 'flac';
return findVorbisCommentBlock(blobview).then(function(block) {
if (!block) {
return metadata;
}
return readAllComments(block.view, metadata);
});
}

/**
* Step over metadata blocks until we find the Vorbis comment block.
*
* @param {BlobView} blobview The BlobView for the file.
* @return {Promise} A promise resolving to an object describing the metadata
* block. See readMetadataBlockHeader for more details.
*/
function findVorbisCommentBlock(blobview) {
return readMetadataBlockHeader(blobview).then(function(block) {
// XXX: Support album art.
// See: http://flac.sourceforge.net/format.html#metadata_block_picture

// Did we find a Vorbis comment block yet?
if (block.block_type === 4) {
return block;
} else if (block.last) {
return null;
} else {
block.view.advance(block.length);
return findVorbisCommentBlock(block.view);
}
});
}

/**
* Read a metadata block header and fetch its contents from the Blob, plus
* enough extra data to read the next block's header.
*
* @param {BlobView} blobview The BlobView for the file.
* @param {Promise} A promise resolving to an object with the following
* fields:
* {Boolean} last True if this is the last metadata block.
* {Number} block_type The block's type, as an integer.
* {Number} length The size in bytes of the block's body (excluding the
* header).
* {BlobView} view The BlobView with the block's data, starting at the
* beginning of the metadata block's content (not the header).
*/
function readMetadataBlockHeader(blobview) {
return new Promise(function(resolve, reject) {
var header = blobview.readUnsignedByte();
var last = (header & 0x80) === 0x80;
var block_type = header & 0x7F;
var length = blobview.readUint24(false);

// Get the contents of this block, plus enough to read the next block's
// header if necessary.
blobview.getMore(blobview.viewOffset + blobview.index, length + 4,
function(more, err) {
if (err) {
reject(err);
return;
}

resolve({
last: last,
block_type: block_type,
length: length,
view: more
});
});
});
}

/**
* Read all the comments in a metadata header block.
*
* @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 vendor string

var num_comments = page.readUnsignedInt(true);
// |metadata| already has some of its values filled in (namely the title
// field). To make sure we overwrite the pre-filled metadata, but also
// append any repeated fields from the file, we keep track of the fields
// we've seen in the file separately.
var seen_fields = {};
for (var i = 0; i < num_comments; i++) {
try {
var comment = readComment(page);
if (comment) {
if (seen_fields.hasOwnProperty(comment.field)) {
// If we already have a value, append this new one.
metadata[comment.field] += ' / ' + comment.value;
} else {
// Otherwise, just save the single value.
metadata[comment.field] = comment.value;
seen_fields[comment.field] = true;
}
}
} catch (e) {
console.warn('Error parsing ogg metadata frame', e);
}
}
return metadata;
}

/**
* Read a single comment field.
*
* @param {BlobView} page The audio file being parsed.
*/
function readComment(page) {
var comment_length = page.readUnsignedInt(true);
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 = FLACFIELDS[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 null;
}

return {
parse: parse
};
})();
11 changes: 9 additions & 2 deletions apps/music/js/metadata/formats.js
@@ -1,5 +1,5 @@
/* global ForwardLockMetadata, ID3v1Metadata, ID3v2Metadata, LazyLoader,
MP4Metadata, OggMetadata */
/* global FLACMetadata, ForwardLockMetadata, ID3v1Metadata, ID3v2Metadata,
LazyLoader, MP4Metadata, OggMetadata */
/* exported MetadataFormats */
'use strict';

Expand Down Expand Up @@ -43,6 +43,13 @@ var MetadataFormats = (function() {
return header.getASCIIText(0, 4) === 'OggS';
}
},
{
file: 'js/metadata/flac.js',
get module() { return FLACMetadata; },
match: function(header) {
return header.getASCIIText(0, 4) === 'fLaC';
}
},
{
file: 'js/metadata/mp4.js',
get module() { return MP4Metadata; },
Expand Down
6 changes: 4 additions & 2 deletions apps/music/manifest.webapp
Expand Up @@ -20,15 +20,17 @@
"activities": {
"pick": {
"filters": {
"type": ["audio/*", "audio/mpeg", "audio/ogg", "audio/mp4"]
"type": ["audio/*", "audio/mpeg", "audio/ogg", "audio/mp4",
"audio/flac"]
},
"disposition": "inline",
"returnValue": true,
"href": "/index.html#pick"
},
"open": {
"filters": {
"type": ["audio/mpeg", "audio/ogg", "audio/mp4", "audio/amr"]
"type": ["audio/mpeg", "audio/ogg", "audio/mp4", "audio/amr",
"audio/flac"]
},
"disposition": "inline",
"returnValue": true,
Expand Down
Binary file added apps/music/test-data/vorbis-c.flac
Binary file not shown.
42 changes: 42 additions & 0 deletions apps/music/test/unit/metadata/flac_test.js
@@ -0,0 +1,42 @@
/* global parseMetadata, MockLazyLoader, MockGetDeviceStorage */
'use strict';

require('/test/unit/metadata/utils.js');
require('/js/metadata/flac.js');

suite('flac tags', function() {
var RealLazyLoader, RealGetDeviceStorage;

setup(function(done) {
this.timeout(1000);
RealLazyLoader = window.LazyLoader;
window.LazyLoader = MockLazyLoader;

RealGetDeviceStorage = navigator.getDeviceStorage;
navigator.getDeviceStorage = MockGetDeviceStorage;

require('/js/metadata_scripts.js', function() {
done();
});
});

teardown(function() {
window.LazyLoader = RealLazyLoader;
navigator.getDeviceStorage = RealGetDeviceStorage;
});

test('vorbis comment', function(done) {
parseMetadata('/test-data/vorbis-c.flac').then(function(metadata) {
done(function() {
assert.strictEqual(metadata.tag_format, 'flac');
assert.strictEqual(metadata.artist, 'Black Sabbath');
assert.strictEqual(metadata.album, 'Master of Reality');
assert.strictEqual(metadata.title, 'Children of the Grave');
assert.strictEqual(metadata.tracknum, 4);
assert.strictEqual(metadata.trackcount, 8);
assert.strictEqual(metadata.discnum, 1);
assert.strictEqual(metadata.disccount, 1);
});
});
});
});

0 comments on commit 17f58f7

Please sign in to comment.