This repository has been archived by the owner on Nov 3, 2021. It is now read-only.
/
flac.js
176 lines (162 loc) · 5.48 KB
/
flac.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
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
};
})();