Summary
There aren't many sources in the internet that show you how to read tags (metadata) from music files such as MP3, OGG or even M4A. Not only this, but they do use an external library. I've build an HTML5 player inspired by Foobar2000, called
Noisy, and I needed it to be as lightweight as possible, so everything inside I wanted to do by myself and optimize it in time. Unfortunately, this required a lot of specifications reading and many retries to get the things working. Here I'll show you how I did the reading and will try explaining the meaning of the code (by comments). It's far from perfect, but there wasn't much in the internet to help me with that task, so if you have any suggestions you are more than welcome to share them with me. Without further ado, lets begin!
Prequel
All tags we read by getting the file as ArrayBuffer using HTML5 FileReader, so here is a sample of how to do that:
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
| function onFilesSupplied( e )
{
// Get all the files from the input of type file
var files = e.files || e.dataTransfer.files,
file,
fr = new FileReader();
// Handle the typed array read by the FileReader,
// when it's finished loading
function _handleFileLoad()
{
// This is the point where the real tag
// read is happening - covered later on
var tags =readTags( this.result );
}
// Listen when FileReader finishes reading and
// pass result to the handler
fr.addEventListener( 'load', _handleFileLoad );
// This example uses only the first file to load,
// but you can loop them all and read them
file = files[0];
// Do the actual read, by saying to the reader
// we want the result as ArrayBuffer
fr.readAsArrayBuffer( file );
}
|
This code is made to run on both input of type file or drag and drop events. Noisy is cloud player, so it rarely reads the files from disk, meaning that code is not used a lot. What Noisy uses instead is to make the cloud service return the file directly as ArrayBuffer. So a simple:
1
| xhr.responseType = 'arraybuffer';
|
to the request does the trick, thus no FileReader is required. Unfortunately this works for Dropbox, but not for Google Drive (
update: now it works!). That's the reason Noisy doesn't support tag reading when user uses Google Drive. If he uses Mozilla Firefox, though, Noisy will read the tags using
audio.mozGetMetadata(), doesn't matter from where the file is coming. All that said, it really doesn't matter how you get your hands on the ArrayBuffer - what really matters is how we read the tags. I've split them in separate code snippets.
MP3 ID3v2
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
| readTags( buffer )
{
// We need only these tags for now. You can check specifications for other ones, if you need them
toGet = {
'TPE1': 'artist',
'TIT2': 'title',
'TALB': 'album',
'TYER': 'date'
};
len = Object.keys( toGet ).length;
i = 0;
// Loop first 1000 bytes. Here I couldn't find better solution to where the tag entries end, so I just loop the first 1KB of data
while( i < 1000 && len )
{
charCode = dv.getUint8( i++ );
// Loop through all the characters in the configuration object on top and compare letter by letter with current one
for( tag in toGet )
{
tagCode = tag.charCodeAt( 0 );
// Check for lower case match
if( charCode == tagCode )
{
match = true;
// loop through all letters to make sure we have found a match
for( k = 1; k < tag.length; k++ )
{
matchCharCode = dv.getUint8( ( i - 1 ) + k );
tagCode = tag.charCodeAt( k );
if( matchCharCode != tagCode )
{
match = false;
break;
}
}
// If a match is found get the tag
if( match )
{
// Pattern for tag: TALB 00 00 00 (HEX for length of tag in bytes, grouped
// by 2 for each char, the second being the first part in unicode, eg. 0415
// for cyrillic ? is written 15 04 here) 00 00 (unicode flag byte. If 00 then
// no unicode, else it's the first part of unicode char) (tag itself). All this
// I found out by myself. Don't know if it's always true
tagLength = i + 9 + dv.getUint8( i + 6 );
tagValue.length = 0;
// Check for *ÿþ symbols and read after them if found. Some tags have this funky
// charachers upfront and I don't know why and what they mean
i = ( 255 == dv.getUint8( i + 10 ) && 254 == dv.getUint8( i + 11 ) ) ? i + 12 : i + 9;
// First byte shows if the tag is encoded in Unicode or not
matchCharCode = dv.getUint8( i++ );
// If unicode
if( matchCharCode )
{
nextMatch = ( '00' + dv.getUint8( i - 1 ).toString( 16 ) ).slice( -2 );
while( i <= tagLength )
{
matchCharCode = ( '00' + dv.getUint8( i ).toString( 16 ) ).slice( -2 );
tagValue.push( '0x'.concat( matchCharCode, nextMatch ) );
nextMatch = ( '00' + dv.getUint8( i + 1 ).toString( 16 ) ).slice( -2 );
i += 2;
}
}
else
{
i++;
matchCharCode = ( '00' + dv.getUint8( i - 1 ).toString( 16 ) ).slice( -2 );
while( i <= tagLength )
{
tagValue.push( '0x00' + matchCharCode );
matchCharCode = ( '00' + dv.getUint8( i++ ).toString( 16 ) ).slice( -2 );
}
}
// Substract last step increase, as next tag comes right after this one
i--;
// Save tag's value as we are done reading it
metadata[toGet[tag]] = String.fromCharCode.apply( null, tagValue );
// Remove tag name from the object, so we don't loop it again
delete toGet[tag];
len = Object.keys( toGet ).length;
}
}
}
}
}
|
I do not do ID3v1, but it's very easily implementable if you understand this code. I just decided it's too old to be supported - all modern MP3 files should have ID3v2.
OGG
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
| readTags( buffer )
{
// We need only these tags for now. You can check specifications for other ones, if you need them
toGet = [
'artist',
'title',
'album',
'date'
];
len = toGet.length;
i = 0;
// Loop first 1000 bytes. Here I couldn't find better solution to where the tag entries end, so I just loop the first 1KB of data
while( i < 1000 && len )
{
charCode = dv.getUint8( i++ );
for( j = len; j--; )
{
tag = toGet[j];
tagCode = tag.charCodeAt( 0 );
// Check for lower case match
if( charCode == tagCode )
{
match = true;
// loop through all letters to make sure we have found a match
for( k = 1; k < tag.length; k++ )
{
matchCharCode = dv.getUint8( ( i - 1 ) + k );
tagCode = tag.charCodeAt( k );
if( matchCharCode != tagCode )
{
match = false;
break;
}
}
// If a match is found get the tag
if( match )
{
// Byte before the 00 00 00 shows how many bytes the tag will be, including the "artist=" part,
// so we read everything from "=" sign till the length is reached. All this
// I found out by myself. Don't know if it's always true
tagLength = i - 1 + dv.getUint8( i - 5 );
tagValue.length = 0;
i = i + tag.length;
matchCharCode = dv.getUint8( i++ );
while( i <= tagLength )
{
tagValue.push( matchCharCode );
matchCharCode = dv.getUint8( i++ );
}
str = '';
for( k = 0; k < tagValue.length; k++ )
{
str += '%' + ( '0' + tagValue[k].toString( 16 ) ).slice( -2 );
}
// Save tag's value as we are done reading it
metadata[tag] = decodeURIComponent( str );
// Remove tag name from the object, so we don't loop it again
toGet.splice( j, 1 );
len = toGet.length;
}
continue;
}
tagCode = tag.toUpperCase().charCodeAt( 0 );
// Check for uppercase match
if( charCode == tagCode )
{
match = true;
tag = tag.toUpperCase();
// loop through all letters to make sure we have found a match
for( k = 1; k < tag.length; k++ )
{
matchCharCode = dv.getUint8( ( i - 1 ) + k );
tagCode = tag.charCodeAt( k );
if( matchCharCode != tagCode )
{
match = false;
break;
}
}
// If a match is found get the tag
if( match )
{
// Byte before the 00 00 00 shows how many bytes the tag will be, including the "artist=" part,
//so we read everything from "=" sign till the length is reachedl
tagLength = i - 1 + dv.getUint8( i - 5 );
tagValue.length = 0;
i = i + tag.length;
matchCharCode = dv.getUint8( i++ );
while( i <= tagLength )
{
tagValue.push( matchCharCode );
matchCharCode = dv.getUint8( i++ );
}
str = '';
for( k = 0; k < tagValue.length; k++ )
{
str += '%' + ( '0' + tagValue[k].toString( 16 ) ).slice( -2 );
}
// Save tag's value as we are done reading it
metadata[tag.toLowerCase()] = decodeURIComponent( str );
// Remove tag name from the object, so we don't loop it again
toGet.splice( j, 1 );
len = toGet.length;
}
}
}
}
}
|
M4A
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
| readTags( buffer )
{
// We need only these tags for now. You can check specifications for other ones, if you need them
toGet = {
'©art': 'artist',
'©nam': 'title',
'©alb': 'album',
'©day': 'date'
};
len = Object.keys( toGet ).length;
i = 50000;
// Loop second 50000 bytes. Here I couldn't find better solution to where
// the tag entries end, so I just loop the characters between 50KB and 100KB
while( i < 100000 && len )
{
charCode = dv.getUint8( i++ );
for( tag in toGet )
{
tagCode = tag.charCodeAt( 0 );
// Check for lower case match
if( charCode == tagCode )
{
match = true;
// loop through all letters to make sure we have found a match
for( k = 1; k < tag.length; k++ )
{
matchCharCode = dv.getUint8( ( i - 1 ) + k );
tagCode = tag.charCodeAt( k );
if( matchCharCode != tagCode )
{
match = false;
break;
}
}
// If a match is found get the tag
if( match )
{
// Pattern: @nam 00 00 00 (byte showing length of tag, starting from next
// byte) data 00 00 00 (byte showing I don't know what) 00 00 00 00 (text
// for tag) 00 00 00 (byte showing I don't know what)
i += 6;
matchCharCode = dv.getUint8( i );
tagLength = i + matchCharCode;
i += 13;
matchCharCode = dv.getUint8( i );
tagValue.length = 0;
matchCharCode = dv.getUint8( i++ );
while( i <= tagLength && matchCharCode )
{
tagValue.push( matchCharCode );
matchCharCode = dv.getUint8( i++ );
}
str = '';
for( k = 0; k < tagValue.length; k++ )
{
str += '%' + ( '0' + tagValue[k].toString( 16 ) ).slice( -2 );
}
// Save tag's value as we are done reading it
metadata[toGet[tag]] = decodeURIComponent( str );
// Remove tag name from the object, so we don't loop it again
delete toGet[tag];
len = Object.keys( toGet ).length;
continue;
}
}
tagCode = tag.toUpperCase().charCodeAt( 0 );
// Check for lower case match
if( charCode == tagCode )
{
match = true;
tag = tag.toUpperCase();
// loop through all letters to make sure we have found a match
for( k = 1; k < tag.length; k++ )
{
matchCharCode = dv.getUint8( ( i - 1 ) + k );
tagCode = tag.charCodeAt( k );
if( matchCharCode != tagCode )
{
match = false;
break;
}
}
// If a match is found get the tag
if( match )
{
// Pattern: @nam 00 00 00 (byte showing length of tag, starting from next
// byte) data 00 00 00 (byte showing I don't know what) 00 00 00 00 (text
// for tag) 00 00 00 (byte showing I don't know what)
i += 6;
matchCharCode = dv.getUint8( i );
tagLength = i + matchCharCode;
i += 13;
matchCharCode = dv.getUint8( i );
tagValue.length = 0;
matchCharCode = dv.getUint8( i++ );
while( i <= tagLength && matchCharCode )
{
tagValue.push( matchCharCode );
matchCharCode = dv.getUint8( i++ );
}
str = '';
for( k = 0; k < tagValue.length; k++ )
{
str += '%' + ( '0' + tagValue[k].toString( 16 ) ).slice( -2 );
}
tag = tag.toLowerCase();
// Save tag's value as we are done reading it
metadata[toGet[tag]] = decodeURIComponent( str );
// Remove tag name from the object, so we don't loop it again
delete toGet[tag];
len = Object.keys( toGet ).length;
}
}
}
}
}
|
I'm sure this code can be optimized a lot, but I'm still keeping it as simple as it can be so it's clear how things work. When I understand all the little details, then I'll merge things and make the code more compact.
Final words
Don't know if this article can help you, but I hope it can add to the end result you are looking for. If you know something that I missed, please do say - I'll be very happy to any optimizations and suggestions.