856 lines
30 KiB
JavaScript
856 lines
30 KiB
JavaScript
(function ( w, d, PKAE ) {
|
|
'use strict';
|
|
|
|
|
|
var StringUtils = {
|
|
readUTF16String: function(bytes, bigEndian, maxBytes) {
|
|
var ix = 0;
|
|
var offset1 = 1, offset2 = 0;
|
|
maxBytes = Math.min(maxBytes||bytes.length, bytes.length);
|
|
|
|
if( bytes[0] == 0xFE && bytes[1] == 0xFF ) {
|
|
bigEndian = true;
|
|
ix = 2;
|
|
} else if( bytes[0] == 0xFF && bytes[1] == 0xFE ) {
|
|
bigEndian = false;
|
|
ix = 2;
|
|
}
|
|
if( bigEndian ) {
|
|
offset1 = 0;
|
|
offset2 = 1;
|
|
}
|
|
|
|
var arr = [];
|
|
for( var j = 0; ix < maxBytes; j++ ) {
|
|
var byte1 = bytes[ix+offset1];
|
|
var byte2 = bytes[ix+offset2];
|
|
var word1 = (byte1<<8)+byte2;
|
|
ix += 2;
|
|
if( word1 == 0x0000 ) {
|
|
break;
|
|
} else if( byte1 < 0xD8 || byte1 >= 0xE0 ) {
|
|
arr[j] = String.fromCharCode(word1);
|
|
} else {
|
|
var byte3 = bytes[ix+offset1];
|
|
var byte4 = bytes[ix+offset2];
|
|
var word2 = (byte3<<8)+byte4;
|
|
ix += 2;
|
|
arr[j] = String.fromCharCode(word1, word2);
|
|
}
|
|
}
|
|
var string = new String(arr.join(""));
|
|
string.bytesReadCount = ix;
|
|
return string;
|
|
},
|
|
readUTF8String: function(bytes, maxBytes) {
|
|
var ix = 0;
|
|
maxBytes = Math.min(maxBytes||bytes.length, bytes.length);
|
|
|
|
if( bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF ) {
|
|
ix = 3;
|
|
}
|
|
|
|
var arr = [];
|
|
for( var j = 0; ix < maxBytes; j++ ) {
|
|
var byte1 = bytes[ix++];
|
|
if( byte1 == 0x00 ) {
|
|
break;
|
|
} else if( byte1 < 0x80 ) {
|
|
arr[j] = String.fromCharCode(byte1);
|
|
} else if( byte1 >= 0xC2 && byte1 < 0xE0 ) {
|
|
var byte2 = bytes[ix++];
|
|
arr[j] = String.fromCharCode(((byte1&0x1F)<<6) + (byte2&0x3F));
|
|
} else if( byte1 >= 0xE0 && byte1 < 0xF0 ) {
|
|
var byte2 = bytes[ix++];
|
|
var byte3 = bytes[ix++];
|
|
arr[j] = String.fromCharCode(((byte1&0xFF)<<12) + ((byte2&0x3F)<<6) + (byte3&0x3F));
|
|
} else if( byte1 >= 0xF0 && byte1 < 0xF5) {
|
|
var byte2 = bytes[ix++];
|
|
var byte3 = bytes[ix++];
|
|
var byte4 = bytes[ix++];
|
|
var codepoint = ((byte1&0x07)<<18) + ((byte2&0x3F)<<12)+ ((byte3&0x3F)<<6) + (byte4&0x3F) - 0x10000;
|
|
arr[j] = String.fromCharCode(
|
|
(codepoint>>10) + 0xD800,
|
|
(codepoint&0x3FF) + 0xDC00
|
|
);
|
|
}
|
|
}
|
|
var string = new String(arr.join(""));
|
|
string.bytesReadCount = ix;
|
|
return string;
|
|
},
|
|
readNullTerminatedString: function(bytes, maxBytes) {
|
|
var arr = [];
|
|
maxBytes = maxBytes || bytes.length;
|
|
for ( var i = 0; i < maxBytes; ) {
|
|
var byte1 = bytes[i++];
|
|
if( byte1 == 0x00 ) break;
|
|
arr[i-1] = String.fromCharCode(byte1);
|
|
}
|
|
var string = new String(arr.join(""));
|
|
string.bytesReadCount = i;
|
|
return string;
|
|
}
|
|
};
|
|
|
|
var getBytesAt = function(data, iOffset, iLength) {
|
|
var bytes = new Array(iLength);
|
|
for( var i = 0; i < iLength; i++ ) {
|
|
bytes[i] = data.getUint8(iOffset+i);
|
|
}
|
|
return bytes;
|
|
};
|
|
var getStringWithCharsetAt = function(data, iOffset, iLength, iCharset) {
|
|
var bytes = getBytesAt(data, iOffset, iLength);
|
|
var sString;
|
|
|
|
switch( iCharset.toLowerCase() ) {
|
|
case 'utf-16':
|
|
case 'utf-16le':
|
|
case 'utf-16be':
|
|
sString = StringUtils.readUTF16String(bytes, iCharset);
|
|
break;
|
|
|
|
case 'utf-8':
|
|
sString = StringUtils.readUTF8String(bytes);
|
|
break;
|
|
|
|
default:
|
|
sString = StringUtils.readNullTerminatedString(bytes);
|
|
break;
|
|
}
|
|
|
|
return sString;
|
|
};
|
|
|
|
var ID3v2 = {
|
|
readFrameData: {}
|
|
};
|
|
|
|
ID3v2.frames = {
|
|
// v2.2
|
|
"BUF" : "Recommended buffer size",
|
|
"CNT" : "Play counter",
|
|
"COM" : "Comments",
|
|
"CRA" : "Audio encryption",
|
|
"CRM" : "Encrypted meta frame",
|
|
"ETC" : "Event timing codes",
|
|
"EQU" : "Equalization",
|
|
"GEO" : "General encapsulated object",
|
|
"IPL" : "Involved people list",
|
|
"LNK" : "Linked information",
|
|
"MCI" : "Music CD Identifier",
|
|
"MLL" : "MPEG location lookup table",
|
|
"PIC" : "Attached picture",
|
|
"POP" : "Popularimeter",
|
|
"REV" : "Reverb",
|
|
"RVA" : "Relative volume adjustment",
|
|
"SLT" : "Synchronized lyric/text",
|
|
"STC" : "Synced tempo codes",
|
|
"TAL" : "Album/Movie/Show title",
|
|
"TBP" : "BPM (Beats Per Minute)",
|
|
"TCM" : "Composer",
|
|
"TCO" : "Content type",
|
|
"TCR" : "Copyright message",
|
|
"TDA" : "Date",
|
|
"TDY" : "Playlist delay",
|
|
"TEN" : "Encoded by",
|
|
"TFT" : "File type",
|
|
"TIM" : "Time",
|
|
"TKE" : "Initial key",
|
|
"TLA" : "Language(s)",
|
|
"TLE" : "Length",
|
|
"TMT" : "Media type",
|
|
"TOA" : "Original artist(s)/performer(s)",
|
|
"TOF" : "Original filename",
|
|
"TOL" : "Original Lyricist(s)/text writer(s)",
|
|
"TOR" : "Original release year",
|
|
"TOT" : "Original album/Movie/Show title",
|
|
"TP1" : "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group",
|
|
"TP2" : "Band/Orchestra/Accompaniment",
|
|
"TP3" : "Conductor/Performer refinement",
|
|
"TP4" : "Interpreted, remixed, or otherwise modified by",
|
|
"TPA" : "Part of a set",
|
|
"TPB" : "Publisher",
|
|
"TRC" : "ISRC (International Standard Recording Code)",
|
|
"TRD" : "Recording dates",
|
|
"TRK" : "Track number/Position in set",
|
|
"TSI" : "Size",
|
|
"TSS" : "Software/hardware and settings used for encoding",
|
|
"TT1" : "Content group description",
|
|
"TT2" : "Title/Songname/Content description",
|
|
"TT3" : "Subtitle/Description refinement",
|
|
"TXT" : "Lyricist/text writer",
|
|
"TXX" : "User defined text information frame",
|
|
"TYE" : "Year",
|
|
"UFI" : "Unique file identifier",
|
|
"ULT" : "Unsychronized lyric/text transcription",
|
|
"WAF" : "Official audio file webpage",
|
|
"WAR" : "Official artist/performer webpage",
|
|
"WAS" : "Official audio source webpage",
|
|
"WCM" : "Commercial information",
|
|
"WCP" : "Copyright/Legal information",
|
|
"WPB" : "Publishers official webpage",
|
|
"WXX" : "User defined URL link frame",
|
|
// v2.3
|
|
"AENC" : "Audio encryption",
|
|
"APIC" : "Attached picture",
|
|
"COMM" : "Comments",
|
|
"COMR" : "Commercial frame",
|
|
"ENCR" : "Encryption method registration",
|
|
"EQUA" : "Equalization",
|
|
"ETCO" : "Event timing codes",
|
|
"GEOB" : "General encapsulated object",
|
|
"GRID" : "Group identification registration",
|
|
"IPLS" : "Involved people list",
|
|
"LINK" : "Linked information",
|
|
"MCDI" : "Music CD identifier",
|
|
"MLLT" : "MPEG location lookup table",
|
|
"OWNE" : "Ownership frame",
|
|
"PRIV" : "Private frame",
|
|
"PCNT" : "Play counter",
|
|
"POPM" : "Popularimeter",
|
|
"POSS" : "Position synchronisation frame",
|
|
"RBUF" : "Recommended buffer size",
|
|
"RVAD" : "Relative volume adjustment",
|
|
"RVRB" : "Reverb",
|
|
"SYLT" : "Synchronized lyric/text",
|
|
"SYTC" : "Synchronized tempo codes",
|
|
"TALB" : "Album/Movie/Show title",
|
|
"TBPM" : "BPM (beats per minute)",
|
|
"TCOM" : "Composer",
|
|
"TCON" : "Content type",
|
|
"TCOP" : "Copyright message",
|
|
"TDAT" : "Date",
|
|
"TDLY" : "Playlist delay",
|
|
"TENC" : "Encoded by",
|
|
"TEXT" : "Lyricist/Text writer",
|
|
"TFLT" : "File type",
|
|
"TIME" : "Time",
|
|
"TIT1" : "Content group description",
|
|
"TIT2" : "Title/songname/content description",
|
|
"TIT3" : "Subtitle/Description refinement",
|
|
"TKEY" : "Initial key",
|
|
"TLAN" : "Language(s)",
|
|
"TLEN" : "Length",
|
|
"TMED" : "Media type",
|
|
"TOAL" : "Original album/movie/show title",
|
|
"TOFN" : "Original filename",
|
|
"TOLY" : "Original lyricist(s)/text writer(s)",
|
|
"TOPE" : "Original artist(s)/performer(s)",
|
|
"TORY" : "Original release year",
|
|
"TOWN" : "File owner/licensee",
|
|
"TPE1" : "Lead performer(s)/Soloist(s)",
|
|
"TPE2" : "Band/orchestra/accompaniment",
|
|
"TPE3" : "Conductor/performer refinement",
|
|
"TPE4" : "Interpreted, remixed, or otherwise modified by",
|
|
"TPOS" : "Part of a set",
|
|
"TPUB" : "Publisher",
|
|
"TRCK" : "Track number/Position in set",
|
|
"TRDA" : "Recording dates",
|
|
"TRSN" : "Internet radio station name",
|
|
"TRSO" : "Internet radio station owner",
|
|
"TSIZ" : "Size",
|
|
"TSRC" : "ISRC (international standard recording code)",
|
|
"TSSE" : "Software/Hardware and settings used for encoding",
|
|
"TYER" : "Year",
|
|
"TXXX" : "User defined text information frame",
|
|
"UFID" : "Unique file identifier",
|
|
"USER" : "Terms of use",
|
|
"USLT" : "Unsychronized lyric/text transcription",
|
|
"WCOM" : "Commercial information",
|
|
"WCOP" : "Copyright/Legal information",
|
|
"WOAF" : "Official audio file webpage",
|
|
"WOAR" : "Official artist/performer webpage",
|
|
"WOAS" : "Official audio source webpage",
|
|
"WORS" : "Official internet radio station homepage",
|
|
"WPAY" : "Payment",
|
|
"WPUB" : "Publishers official webpage",
|
|
"WXXX" : "User defined URL link frame"
|
|
};
|
|
|
|
var pictureType = [
|
|
"32x32 pixels 'file icon' (PNG only)",
|
|
"Other file icon",
|
|
"Cover (front)",
|
|
"Cover (back)",
|
|
"Leaflet page",
|
|
"Media (e.g. lable side of CD)",
|
|
"Lead artist/lead performer/soloist",
|
|
"Artist/performer",
|
|
"Conductor",
|
|
"Band/Orchestra",
|
|
"Composer",
|
|
"Lyricist/text writer",
|
|
"Recording Location",
|
|
"During recording",
|
|
"During performance",
|
|
"Movie/video screen capture",
|
|
"A bright coloured fish",
|
|
"Illustration",
|
|
"Band/artist logotype",
|
|
"Publisher/Studio logotype"
|
|
];
|
|
|
|
var getStringAt = function(data, iOffset, iLength) {
|
|
var aStr = [];
|
|
for (var i=iOffset,j=0;i<iOffset+iLength;i++,j++) {
|
|
aStr[j] = String.fromCharCode(data.getUint8(i));
|
|
}
|
|
return aStr.join("");
|
|
};
|
|
var getLongAt = function(data, iOffset, bBigEndian) {
|
|
var iByte1 = data.getUint8(iOffset),
|
|
iByte2 = data.getUint8(iOffset + 1),
|
|
iByte3 = data.getUint8(iOffset + 2),
|
|
iByte4 = data.getUint8(iOffset + 3);
|
|
|
|
var iLong = bBigEndian ?
|
|
(((((iByte1 << 8) + iByte2) << 8) + iByte3) << 8) + iByte4
|
|
: (((((iByte4 << 8) + iByte3) << 8) + iByte2) << 8) + iByte1;
|
|
if (iLong < 0) iLong += 4294967296;
|
|
return iLong;
|
|
};
|
|
var getSLongAt = function(data, iOffset, bBigEndian) {
|
|
var iULong = getLongAt(data, iOffset, bBigEndian);
|
|
if (iULong > 2147483647)
|
|
return iULong - 4294967296;
|
|
else
|
|
return iULong;
|
|
};
|
|
var getShortAt = function(data, iOffset, bBigEndian) {
|
|
var iShort = bBigEndian ?
|
|
(data.getUint8(iOffset) << 8) + data.getUint8(iOffset + 1)
|
|
: (data.getUint8(iOffset + 1) << 8) + data.getUint8(iOffset);
|
|
if (iShort < 0) iShort += 65536;
|
|
return iShort;
|
|
};
|
|
var getInteger24At = function(data, iOffset, bBigEndian) {
|
|
var iByte1 = data.getUint8(iOffset),
|
|
iByte2 = data.getUint8(iOffset + 1),
|
|
iByte3 = data.getUint8(iOffset + 2);
|
|
|
|
var iInteger = bBigEndian ?
|
|
((((iByte1 << 8) + iByte2) << 8) + iByte3)
|
|
: ((((iByte3 << 8) + iByte2) << 8) + iByte1);
|
|
if (iInteger < 0) iInteger += 16777216;
|
|
return iInteger;
|
|
};
|
|
var isBitSetAt = function ( dataview, offset, bit ) {
|
|
var ibyte = dataview.getUint8(offset);
|
|
return (ibyte & (1 << bit)) != 0;
|
|
};
|
|
var readSynchsafeInteger32At = function (offset, data) {
|
|
var size1 = data.getUint8(offset);
|
|
var size2 = data.getUint8(offset+1);
|
|
var size3 = data.getUint8(offset+2);
|
|
var size4 = data.getUint8(offset+3);
|
|
// 0x7f = 0b01111111
|
|
var size = size4 & 0x7f
|
|
| ((size3 & 0x7f) << 7)
|
|
| ((size2 & 0x7f) << 14)
|
|
| ((size1 & 0x7f) << 21);
|
|
|
|
return size;
|
|
};
|
|
var readFrameFlags = function(data, offset) {
|
|
var flags =
|
|
{
|
|
message:
|
|
{
|
|
tag_alter_preservation : isBitSetAt(data, offset, 6),
|
|
file_alter_preservation : isBitSetAt(data, offset, 5),
|
|
read_only : isBitSetAt(data, offset, 4)
|
|
},
|
|
format:
|
|
{
|
|
grouping_identity : isBitSetAt(data, offset+1, 7),
|
|
compression : isBitSetAt(data, offset+1, 3),
|
|
encription : isBitSetAt(data, offset+1, 2),
|
|
unsynchronisation : isBitSetAt(data, offset+1, 1),
|
|
data_length_indicator : isBitSetAt(data, offset+1, 0)
|
|
}
|
|
};
|
|
|
|
return flags;
|
|
};
|
|
var _shortcuts = {
|
|
"title" : ["TIT2", "TT2"],
|
|
"artist" : ["TPE1", "TP1"],
|
|
"album" : ["TALB", "TAL"],
|
|
"year" : ["TYER", "TYE"],
|
|
"comment" : ["COMM", "COM"],
|
|
"track" : ["TRCK", "TRK"],
|
|
"genre" : ["TCON", "TCO"],
|
|
"picture" : ["APIC", "PIC"],
|
|
"lyrics" : ["USLT", "ULT"]
|
|
};
|
|
var _defaultShortcuts = ["title", "artist", "album", "track"];
|
|
|
|
var getTagsFromShortcuts = function(shortcuts) {
|
|
var tags = [];
|
|
for( var i = 0, shortcut; shortcut = shortcuts[i]; i++ ) {
|
|
tags = tags.concat(_shortcuts[shortcut]||[shortcut]);
|
|
}
|
|
return tags;
|
|
};
|
|
var getFrameData = function( frames, ids ) {
|
|
if( typeof ids == 'string' ) { ids = [ids]; }
|
|
|
|
for( var i = 0, id; id = ids[i]; i++ ) {
|
|
if( id in frames ) { return frames[id].data; }
|
|
}
|
|
};
|
|
var readFrames = function (offset, end, data, id3header, tags) {
|
|
var frames = {};
|
|
var frameDataSize;
|
|
var major = id3header["major"];
|
|
|
|
tags = getTagsFromShortcuts(tags || _defaultShortcuts);
|
|
|
|
while( offset < end ) {
|
|
var readFrameFunc = null;
|
|
var frameData = data;
|
|
var frameDataOffset = offset;
|
|
var flags = null;
|
|
|
|
switch( major ) {
|
|
case 2:
|
|
var frameID = getStringAt(frameData, frameDataOffset, 3);
|
|
var frameSize = getInteger24At(frameData, frameDataOffset+3, true);
|
|
var frameHeaderSize = 6;
|
|
break;
|
|
|
|
case 3:
|
|
var frameID = getStringAt(frameData, frameDataOffset, 4);
|
|
var frameSize = getLongAt(frameData, frameDataOffset+4, true);
|
|
var frameHeaderSize = 10;
|
|
break;
|
|
|
|
case 4:
|
|
var frameID = getStringAt(frameData, frameDataOffset, 4);
|
|
var frameSize = readSynchsafeInteger32At(frameDataOffset+4, frameData);
|
|
var frameHeaderSize = 10;
|
|
break;
|
|
}
|
|
// if last frame GTFO
|
|
if( frameID == "" ) { break; }
|
|
|
|
// advance data offset to the next frame data
|
|
offset += frameHeaderSize + frameSize;
|
|
// skip unwanted tags
|
|
if( tags.indexOf( frameID ) < 0 ) { continue; }
|
|
|
|
// read frame message and format flags
|
|
if( major > 2 )
|
|
{
|
|
flags = readFrameFlags(frameData, frameDataOffset+8);
|
|
}
|
|
|
|
frameDataOffset += frameHeaderSize;
|
|
|
|
// the first 4 bytes are the real data size
|
|
// (after unsynchronisation && encryption)
|
|
if( flags && flags.format.data_length_indicator )
|
|
{
|
|
frameDataSize = readSynchsafeInteger32At(frameDataOffset, frameData);
|
|
frameDataOffset += 4;
|
|
frameSize -= 4;
|
|
}
|
|
|
|
// TODO: support unsynchronisation
|
|
if( flags && flags.format.unsynchronisation )
|
|
{
|
|
//frameData = removeUnsynchronisation(frameData, frameSize);
|
|
continue;
|
|
}
|
|
|
|
// find frame parsing function
|
|
|
|
if( frameID in ID3v2.readFrameData ) {
|
|
readFrameFunc = ID3v2.readFrameData[frameID];
|
|
} else if( frameID[0] == "T" ) {
|
|
readFrameFunc = ID3v2.readFrameData["T*"];
|
|
}
|
|
|
|
var parsedData = readFrameFunc ? readFrameFunc(frameDataOffset, frameSize, frameData, flags) : undefined;
|
|
var desc = frameID in ID3v2.frames ? ID3v2.frames[frameID] : 'Unknown';
|
|
|
|
var frame = {
|
|
id : frameID,
|
|
size : frameSize,
|
|
description : desc,
|
|
data : parsedData
|
|
};
|
|
|
|
if( frameID in frames ) {
|
|
if( frames[frameID].id ) {
|
|
frames[frameID] = [frames[frameID]];
|
|
}
|
|
frames[frameID].push(frame);
|
|
} else {
|
|
frames[frameID] = frame;
|
|
}
|
|
}
|
|
|
|
return frames;
|
|
};
|
|
|
|
function getTextEncoding( bite ) {
|
|
var charset;
|
|
switch( bite )
|
|
{
|
|
case 0x00:
|
|
charset = 'iso-8859-1';
|
|
break;
|
|
|
|
case 0x01:
|
|
charset = 'utf-16';
|
|
break;
|
|
|
|
case 0x02:
|
|
charset = 'utf-16be';
|
|
break;
|
|
|
|
case 0x03:
|
|
charset = 'utf-8';
|
|
break;
|
|
}
|
|
|
|
return charset;
|
|
}
|
|
|
|
function getTime( duration )
|
|
{
|
|
var duration = duration/1000,
|
|
seconds = Math.floor( duration ) % 60,
|
|
minutes = Math.floor( duration/60 ) % 60,
|
|
hours = Math.floor( duration/3600 );
|
|
|
|
return {
|
|
seconds : seconds,
|
|
minutes : minutes,
|
|
hours : hours
|
|
};
|
|
}
|
|
|
|
function formatTime( time )
|
|
{
|
|
var seconds = time.seconds < 10 ? '0'+time.seconds : time.seconds;
|
|
var minutes = (time.hours > 0 && time.minutes < 10) ? '0'+time.minutes : time.minutes;
|
|
|
|
return (time.hours>0?time.hours+':':'') + minutes + ':' + seconds;
|
|
}
|
|
|
|
ID3v2.readFrameData['APIC'] = function readPictureFrame(offset, length, data, flags, v) {
|
|
v = v || '3';
|
|
|
|
var start = offset;
|
|
var charset = getTextEncoding( data.getUint8(offset) );
|
|
switch( v ) {
|
|
case '2':
|
|
var format = getStringAt(data, offset+1, 3);
|
|
offset += 4;
|
|
break;
|
|
|
|
case '3':
|
|
case '4':
|
|
var format = getStringWithCharsetAt(data, offset+1, length - (offset-start), '');
|
|
offset += 1 + format.bytesReadCount;
|
|
break;
|
|
}
|
|
var bite = data.getUint8(offset, 1);
|
|
var type = pictureType[bite];
|
|
var desc = getStringWithCharsetAt(data, offset+1, length - (offset-start), charset);
|
|
|
|
offset += 1 + desc.bytesReadCount;
|
|
|
|
return {
|
|
"format" : format.toString(),
|
|
"type" : type,
|
|
"description" : desc.toString(),
|
|
"data" : getBytesAt(data, offset, (start+length) - offset)
|
|
};
|
|
};
|
|
|
|
ID3v2.readFrameData['COMM'] = function readCommentsFrame(offset, length, data) {
|
|
var start = offset;
|
|
var charset = getTextEncoding( data.getUint8(offset) );
|
|
var language = getStringAt(data, offset+1, 3 );
|
|
var shortdesc = getStringWithCharsetAt(data, offset+4, length-4, charset);
|
|
|
|
offset += 4 + shortdesc.bytesReadCount;
|
|
var text = getStringWithCharsetAt(data, offset, (start+length) - offset, charset );
|
|
|
|
return {
|
|
language : language,
|
|
short_description : shortdesc.toString(),
|
|
text : text.toString()
|
|
};
|
|
};
|
|
|
|
ID3v2.readFrameData['COM'] = ID3v2.readFrameData['COMM'];
|
|
|
|
ID3v2.readFrameData['PIC'] = function(offset, length, data, flags) {
|
|
return ID3v2.readFrameData['APIC'](offset, length, data, flags, '2');
|
|
};
|
|
|
|
ID3v2.readFrameData['PCNT'] = function readCounterFrame(offset, length, data) {
|
|
// FIXME: implement the rest of the spec
|
|
return data.getInteger32At(offset);
|
|
};
|
|
|
|
ID3v2.readFrameData['CNT'] = ID3v2.readFrameData['PCNT'];
|
|
|
|
ID3v2.readFrameData['T*'] = function readTextFrame(offset, length, data) {
|
|
var charset = getTextEncoding( data.getUint8(offset) );
|
|
|
|
return getStringWithCharsetAt(data, offset+1, length-1, charset).toString();
|
|
};
|
|
|
|
ID3v2.readFrameData['TCON'] = function readGenreFrame(offset, length, data) {
|
|
var text = ID3v2.readFrameData['T*'].apply( this, arguments );
|
|
return text.replace(/^\(\d+\)/, '');
|
|
};
|
|
|
|
ID3v2.readFrameData['TCO'] = ID3v2.readFrameData['TCON'];
|
|
|
|
//ID3v2.readFrameData['TLEN'] = function readLengthFrame(offset, length, data) {
|
|
// var text = ID3v2.readFrameData['T*'].apply( this, arguments );
|
|
//
|
|
// return {
|
|
// text : text,
|
|
// parsed : formatTime( getTime(parseInt(text)) )
|
|
// };
|
|
//};
|
|
|
|
ID3v2.readFrameData['USLT'] = function readLyricsFrame(offset, length, data) {
|
|
var start = offset;
|
|
var charset = getTextEncoding( data.getUint8(offset) );
|
|
var language = getStringAt(data, offset+1, 3 );
|
|
var descriptor = getStringWithCharsetAt(data, offset+4, length-4, charset );
|
|
|
|
offset += 4 + descriptor.bytesReadCount;
|
|
var lyrics = getStringWithCharsetAt(data, offset, (start+length) - offset, charset );
|
|
|
|
return {
|
|
language : language,
|
|
descriptor : descriptor.toString(),
|
|
lyrics : lyrics.toString()
|
|
};
|
|
};
|
|
|
|
ID3v2.readFrameData['ULT'] = ID3v2.readFrameData['USLT'];
|
|
|
|
|
|
ID3v2.ReadTags = function ( arraybuffer ) {
|
|
var data = new DataView ( arraybuffer );
|
|
var offset = 0;
|
|
|
|
|
|
var major = data.getUint8(offset+3);
|
|
if( major > 4 ) { return {version: '>2.4'}; }
|
|
var revision = data.getUint8(offset+4);
|
|
var unsynch = isBitSetAt(data, offset+5, 7);
|
|
var xheader = isBitSetAt(data, offset+5, 6);
|
|
var xindicator = isBitSetAt(data, offset+5, 5);
|
|
var size = readSynchsafeInteger32At(offset+6, data);
|
|
offset += 10;
|
|
|
|
if( xheader ) {
|
|
var xheadersize = data.getInt32( offset, true ); //data.getLongAt(offset, true);
|
|
// The 'Extended header size', currently 6 or 10 bytes, excludes itself.
|
|
offset += xheadersize + 4;
|
|
}
|
|
|
|
var id3 = {
|
|
"version" : '2.' + major + '.' + revision,
|
|
"major" : major,
|
|
"revision" : revision,
|
|
"flags" : {
|
|
"unsynchronisation" : unsynch,
|
|
"extended_header" : xheader,
|
|
"experimental_indicator" : xindicator
|
|
},
|
|
"size" : size
|
|
};
|
|
|
|
var frames = unsynch ? {} : readFrames(offset, size-10, data, id3);
|
|
// create shortcuts for most common data
|
|
for( var name in _shortcuts ) if(_shortcuts.hasOwnProperty(name)) {
|
|
var data = getFrameData( frames, _shortcuts[name] );
|
|
if( data ) id3[name] = data;
|
|
}
|
|
|
|
for( var frame in frames ) {
|
|
if( frames.hasOwnProperty(frame) ) {
|
|
id3[frame] = frames[frame];
|
|
}
|
|
}
|
|
|
|
return id3;
|
|
};
|
|
|
|
w.ID3v2 = ID3v2;
|
|
|
|
|
|
/// -------
|
|
|
|
var ID4 = {};
|
|
|
|
ID4.types = {
|
|
'0' : 'uint8',
|
|
'1' : 'text',
|
|
'13' : 'jpeg',
|
|
'14' : 'png',
|
|
'21' : 'uint8'
|
|
};
|
|
ID4.atom = {
|
|
'©alb': ['album'],
|
|
'©art': ['artist'],
|
|
'©ART': ['artist'],
|
|
'aART': ['artist'],
|
|
'©day': ['year'],
|
|
'©nam': ['title'],
|
|
'©gen': ['genre'],
|
|
'trkn': ['track'],
|
|
'©wrt': ['composer'],
|
|
'©too': ['encoder'],
|
|
'cprt': ['copyright'],
|
|
'covr': ['picture'],
|
|
'©grp': ['grouping'],
|
|
'keyw': ['keyword'],
|
|
'©lyr': ['lyrics'],
|
|
'©cmt': ['comment'],
|
|
'tmpo': ['tempo'],
|
|
'cpil': ['compilation'],
|
|
'disk': ['disc']
|
|
};
|
|
|
|
ID4.loadData = function(arraybuffer, callback) {
|
|
var data = new DataView ( arraybuffer );
|
|
// load the header of the first block
|
|
loadAtom(data, 0, data.byteLength, callback);
|
|
};
|
|
|
|
/**
|
|
* Make sure that the [offset, offset+7] bytes (the block header) are
|
|
* already loaded before calling this function.
|
|
*/
|
|
function loadAtom(data, offset, length, callback) {
|
|
// 8 is the size of the atomSize and atomName fields.
|
|
// When reading the current block we always read 8 more bytes in order
|
|
// to also read the header of the next block.
|
|
var atomSize = getLongAt(data, offset, true);
|
|
if (atomSize == 0) return callback();
|
|
var atomName = getStringAt(data, offset + 4, 4);
|
|
|
|
// Container atoms
|
|
if (['moov', 'udta', 'meta', 'ilst'].indexOf(atomName) > -1)
|
|
{
|
|
if (atomName == 'meta') offset += 4; // next_item_id (uint32)
|
|
// data.loadRange([offset+8, offset+8 + 8], function() {
|
|
loadAtom(data, offset + 8, atomSize - 8, callback);
|
|
// });
|
|
} else {
|
|
// Value atoms
|
|
var readAtom = atomName in ID4.atom;
|
|
// data.loadRange([offset+(readAtom?0:atomSize), offset+atomSize + 8], function() {
|
|
loadAtom(data, offset+atomSize, length, callback);
|
|
// });
|
|
}
|
|
};
|
|
|
|
ID4.ReadTags = function(arraybuffer) {
|
|
var data = new DataView ( arraybuffer );
|
|
var tag = {};
|
|
readAtom(tag, data, 0, data.byteLength);
|
|
return tag;
|
|
};
|
|
|
|
function readAtom(tag, data, offset, length, indent)
|
|
{
|
|
// debugger;
|
|
|
|
indent = indent === undefined ? "" : indent + " ";
|
|
var seek = offset;
|
|
while (seek < offset + length)
|
|
{
|
|
var atomSize = data.getInt32(seek); // getLongAt(data, seek, true);
|
|
if (atomSize == 0) return;
|
|
var atomName = getStringAt(data, seek + 4, 4);
|
|
// Container atoms
|
|
if (atomName === 'meta')
|
|
{
|
|
seek += 4; // next_item_id (uint32)
|
|
readAtom(tag, data, seek + 8, atomSize - 8, indent);
|
|
return;
|
|
}
|
|
if (atomName === 'moov' || atomName === 'udta' || atomName === 'ilst' ) // ['moov', 'udta', 'meta', 'ilst'].indexOf(atomName) > -1)
|
|
{
|
|
readAtom(tag, data, seek + 8, atomSize - 8, indent);
|
|
return;
|
|
}
|
|
|
|
/*
|
|
if (['moov', 'udta', 'meta', 'ilst'].indexOf(atomName) > -1)
|
|
{
|
|
if (atomName === 'meta') seek += 4; // next_item_id (uint32)
|
|
readAtom(tag, data, seek + 8, atomSize - 8, indent);
|
|
return;
|
|
}
|
|
*/
|
|
|
|
// Value atoms
|
|
if (ID4.atom[atomName])
|
|
{
|
|
var klass = getInteger24At(data, seek + 16 + 1, true);
|
|
var atom = ID4.atom[atomName];
|
|
var type = ID4.types[klass];
|
|
if (atomName === 'trkn')
|
|
{
|
|
tag[atom[0]] = data.getUint8(seek + 16 + 11);
|
|
tag['count'] = data.getUint8(seek + 16 + 13);
|
|
}
|
|
else
|
|
{
|
|
// 16: name + size + "data" + size (4 bytes each)
|
|
// 4: atom version (1 byte) + atom flags (3 bytes)
|
|
// 4: NULL (usually locale indicator)
|
|
var dataStart = seek + 16 + 4 + 4;
|
|
var dataEnd = atomSize - 16 - 4 - 4;
|
|
var atomData;
|
|
switch( type ) {
|
|
case 'text':
|
|
atomData = getStringWithCharsetAt(data, dataStart, dataEnd, "UTF-8");
|
|
break;
|
|
|
|
case 'uint8':
|
|
atomData = getShortAt(data, dataStart);
|
|
break;
|
|
|
|
case 'jpeg':
|
|
case 'png':
|
|
atomData = {
|
|
format : "image/" + type,
|
|
data : getBytesAt(data, dataStart, dataEnd)
|
|
};
|
|
break;
|
|
}
|
|
|
|
if (atom[0] === "comment") {
|
|
tag[atom[0]] = {
|
|
"text": atomData
|
|
};
|
|
} else {
|
|
tag[atom[0]] = atomData;
|
|
}
|
|
}
|
|
}
|
|
seek += atomSize;
|
|
}
|
|
}
|
|
|
|
w.ID4 = ID4;
|
|
|
|
})( window, document, PKAudioEditor ); |