This repository has been archived by the owner on Nov 3, 2021. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bug 1039639 - [music] Support parsing FLAC metadata
- Loading branch information
Jim
committed
Dec 3, 2014
1 parent
8749f73
commit 17f58f7
Showing
5 changed files
with
231 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}; | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); | ||
}); | ||
}); |