Skip to content

Instantly share code, notes, and snippets.

@darkyen
Last active January 13, 2023 02:36
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save darkyen/4450502 to your computer and use it in GitHub Desktop.
Save darkyen/4450502 to your computer and use it in GitHub Desktop.
A javascript based decoder for mp3 frame parsing supports id3
/*
var net = require('net');
var http = require('http');
var listeners = [];
var Meta = {};
var streamer = http.createServer(function(req,res){
res.write('ICY 200 OK\r\nicy-notice1:<BR>FUCK OFF <BR>icy-notice2:SHOUTcast Distributed Network Audio Server/posix v1.2.3<BR>icy-name:'+Meta.name+'\r\nicy-genre:'+Meta.genre+'\r\nicy-url:'+Meta.url+'\r\nContent-Type:audio/mpeg\r\nicy-pub:1\r\nicy-br:'+Meta.br+'\r\nicy-metaint:8192\r\n\r\n');
listeners.push(res);
});
streamer.listen(7200);
function handleMeta(data){
if(data.indexOf('icy')> -1){
//Media Meta Data
var arr = data.split('\n');
for( item in arr ){
var str = arr[item];
str = str.split(':',2);
str[0] = str[0].replace('icy-','');
if(str[0] !== '' && str.length > 1)
Meta[str[0]] = str[1];
}
console.dir(Meta);
}
}
function handleStream(data){
handleMeta(data);
console.log(data);
for (var i = listeners.length - 1; i > -1 ; i-- ) {
listeners[i].write(data);
}
}
var server = net.createServer(function(c) { //'connection' listener
console.log('Connection');
c.setEncoding('utf8');
c.once('data',function(data){
handleMeta(data);
c.on('data',function(data){
handleStream(data);
});
if(data.indexOf('cgcarls5')>-1){
c.write('OK2\r\nicy-caps:11\r\n\r\n');
}else{
c.write('Invalid Password \r\n\r\n');
}
});
c.on('end', function() {
console.log('server disconnected');
});
});
server.listen(8124, function() { //'listening' listener
console.log('server bound');
});
*/
var fs = require('fs');
var Buffer = require('buffer').Buffer;
// Mp3Id3Reader
// Supports Id3v2.3.0 fully
// TODO: Add id3v2.2.0
// TODO: Add id3v2.4.0
var id3Reader = function() {
var self = this;
function id3Size( buffer ) {
var integer = ( ( buffer[0] & 0x7F ) << 21 ) |
( ( buffer[1] & 0x7F ) << 14 ) |
( ( buffer[2] & 0x7F ) << 7 ) |
( buffer[3] & 0x7F );
return integer;
}
var callback = null;
var PIC_TYPE = ["Other","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","Movie/video screen capture",
"A bright coloured fish", //<--- Wait what the f ?
"Illustration","Band/artist logotype","Publisher/Studio logotype","Band/Orchestra","Composer","Lyricist/text writer","Recording Location","During recording","During performance"];
var GENRES = ["Blues","Classic Rock","Country","Dance","Disco","Funk","Grunge","Hip-Hop","Jazz","Metal","New Age","Oldies","Other","Pop","R&B","Rap","Reggae","Rock","Techno","Industrial","Alternative","Ska","Death Metal","Pranks","Soundtrack","Euro-Techno","Ambient","Trip-Hop","Vocal","Jazz+Funk","Fusion","Trance","Classical","Instrumental","Acid","House","Game","Sound Clip","Gospel","Noise","AlternRock","Bass","Soul","Punk","Space","Meditative","Instrumental Pop","Instrumental Rock","Ethnic","Gothic","Darkwave","Techno-Industrial","Electronic","Pop-Folk","Eurodance","Dream","Southern Rock","Comedy","Cult","Gangsta","Top 40","Christian Rap","Pop/Funk","Jungle","Native American","Cabaret","New Wave","Psychadelic","Rave","Showtunes","Trailer","Lo-Fi","Tribal","Acid Punk","Acid Jazz","Polka","Retro","Musical","Rock & Roll","Hard Rock","Folk","Folk-Rock","National Folk","Swing","Fast Fusion","Bebob","Latin","Revival","Celtic","Bluegrass","Avantgarde","Gothic Rock","Progressive Rock","Psychedelic Rock","Symphonic Rock","Slow Rock","Big Band","Chorus","Easy Listening","Acoustic","Humour","Speech","Chanson","Opera","Chamber Music","Sonata","Symphony","Booty Bass","Primus","Porn Groove","Satire","Slow Jam","Club","Tango","Samba","Folklore","Ballad","Power Ballad","Rhythmic Soul","Freestyle","Duet","Punk Rock","Drum Solo","A capella","Euro-House","Dance Hall"];
var TAGS = {
"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",
"TBPM": "BPM",
"TCOM": "Composer",
"TCON": "Genre",
"TCOP": "Copyright message",
"TDAT": "Date",
"TDLY": "Playlist delay",
"TENC": "Encoded by",
"TEXT": "Lyricist",
"TFLT": "File type",
"TIME": "Time",
"TIT1": "Content group description",
"TIT2": "Title",
"TIT3": "Subtitle",
"TKEY": "Initial key",
"TLAN": "Language(s)",
"TLEN": "Length",
"TMED": "Media type",
"TOAL": "Original album",
"TOFN": "Original filename",
"TOLY": "Original lyricist",
"TOPE": "Original artist",
"TORY": "Original release year",
"TOWN": "File owner",
"TPE1": "Artist",
"TPE2": "Band",
"TPE3": "Conductor",
"TPE4": "Interpreted, remixed, or otherwise modified by",
"TPOS": "Part of a set",
"TPUB": "Publisher",
"TRCK": "Track number",
"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 special_tags = {
'APIC': function(raw) {
var frame = {
txt_enc : raw.readUInt8(0)
}
var pos = raw.toString('ascii',1,(raw.length < 24)?raw.length:24).indexOf('\0');
frame.mime = raw.toString('ascii',1,pos+1);
pos += 2;
frame.type = PIC_TYPE[raw.readUInt8(pos++)] || 'unknown';
var desc = raw.toString('ascii',pos,pos+64); // Max 64 char comment
var desc_pos = desc.indexOf('\0');
frame.desc = desc.substr(0,desc_pos);
pos += desc_pos + 1 ;// /0 is the last character which wont be counted xP
frame.img = fs.writeFileSync('art2.'+frame.mime.split('/')[1],raw.slice(pos,raw.length),'binary'); // Replace the art with unique ID .
return frame;
},
'TRCK': function(raw) {
return raw.toString('ascii').replace(/\u0000/g,'') * 1;
},
'TYER': function(raw) {
return raw.toString('ascii').replace(/\u0000/g,'') *1;
}
}
function parseTags (raw_tags,callback) {
var max = raw_tags.length;
var pos = 0;
var parsed_tags = [];
while( pos < max-10) {
var TAG = {
NAME : raw_tags.toString('ascii',pos,pos+4),
SIZE : raw_tags.readUInt32BE(pos+4)
};
if( special_tags[TAG.NAME] !== undefined) {
TAG.content = special_tags[TAG.NAME](raw_tags.slice(pos+10,pos+10+TAG.SIZE)) || 'FUCK IN COMPLETE THE FUCKING FUNCTION';
} else {
TAG.content = raw_tags.toString('utf8',pos+10,pos+10+TAG.SIZE).replace(/\u0000/g,'');
}
if( TAGS[TAG.NAME] !== undefined && TAG.NAME !== 'PRIV') {
parsed_tags.push(TAG);
}
pos += (10+TAG.SIZE);
}
callback(parsed_tags);
};
function beginRead(err,fd,extern) {
if(err) {
console.dir(err);
return;
}
var id3 = {};
var _ext = extern;
var header = new Buffer(10);
fs.read(fd,header,0,10,0, function(err,bytesRead,buff) {
if( buff.toString('ascii',0,3) != 'ID3') {
console.log("Not an id3v2 ");
return;
}
id3.head = {
size:id3Size(buff.slice(6,10)),
ver:'2.'+buff.readUInt8(3)+'.'+buff.readUInt8(4)
};
var raw_tags = new Buffer(id3.head.size);
fs.read(fd,raw_tags,0,id3.head.size,null, function() {
if( _ext === false ) {
fs.close(fd, function() {
parseTags( raw_tags, function(parsed_tags) {
id3.tags = parsed_tags;
callback(id3);
});
});
} else {
parseTags( raw_tags , function( parsed_tags ) {
id3.tags = parsed_tags;
callback(id3);
});
}
});
});
}
/*
* @API - PUBLIC
*/
self.read = function (file,_callback,fd) {
callback = _callback;
if(fd === undefined) {
fs.open(file,'r',beginRead);
} else {
beginRead(null,fd,true);
}
}
};
/* Singular Mp3 Reader */
// Loads of constants for binary reading
var ext = {
_single_bit:[0xff,0x7f,0x3f,0x1f,0x0f,0x07,0x03,0x01],
ver: {
frame: {
0:'MPEG 2.5',
1:'Reserved',
2:'MPEG 2',
3:'MPEG 1'
},
layer: {
0:'reserved',
1:'Layer III',
2:'Layer II',
3:'Layer I'
},
bitrate: {
/* Defines in Mpeg version */
/* Mpeg 1 */
3: {
/* defines the mpeg layer in the version */
/* Layer 3 */
1:[0,32,40,48,56,64,80,96,112,128,160,192,224,256,320,-1],
/* Layer 2 */
2:[0,32,48,56,64,80,96,112,128,160,192,224,256,320,384,-1],
/* Layer 1 */
3:[0,32,64,96,128,160,192,224,256,288,320,352,384,416,488,-1]
},
/* Mpeg version 2 */
2: {
/* Layer 1 */
3:[0,32,48,56,64,80,96,112,128,144,160,176,192,224,256,-1],
/* Layer 2 */
2:[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,-1],
/* Layer 3 */
1:[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,-1]
},
/* Mpeg version 2.5 */
0: {
/* Layer 1 */
3:[0,32,48,56,64,80,96,112,128,144,160,176,192,224,256,-1],
/* Layer 2 */
2:[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,-1],
/* Layer 3 */
1:[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,-1]
}
},
samplerate: {
3:[44100,48000,32000,-1], /* Mpeg 1 */
2:[22050,24000,16000,-1], /* Mpeg 2 */
0:[11025,12000,8000] /* Mpeg 2.5 */
},
channel:[
'Stereo',
'Joint Stereo',
'Dual Channel',
'Single Channel'
],
jsMode: {
1:[['off','off'],['on','off'],['off','on'],['on','on']],
2:[[4,31],[8,31],[12,31],[16,31]],
3:[[4,31],[8,31],[12,31],[16,31]]
},
Emphasis:[
'none',
'50/15ms',
'reserved',
'CCIT J.17'
],
slot: {
3:4,
2:1,
1:1
},
samplePerFrame: {
3: {
3:384,
2:1152,
1:1152
},
2: {
3:384,
2:1152,
1:576,
},
0: {
3:384,
2:1152,
1:576
}
}
}
}
Buffer.prototype.ptr = 0; /* The position of read head in bits */
Buffer.prototype.getBits = function (bits) {
var ptr = this.ptr;
this.ptr += bits;
var start = Math.floor(ptr/8);
var end = Math.floor(this.ptr/8);
var No_of_Bytes = Math.floor(bits/8);
var _offset_start = ( ptr % 8);
var _offset_end = 8 - (this.ptr % 8);
if( _offset_end === 8) {
end --;
}
var integer = 0;
integer = (this[start] & ext._single_bit[_offset_start]);
var i = start + 1;
do {
if(i < end )
integer = ( (integer << 8) | this[i++] );
} while(i < end-1 );
if( No_of_Bytes > 0) {
integer = (integer <<((8 - _offset_end%8))) | (this[end]>>(_offset_end%8));
} else {
integer = integer >> (_offset_end%8);
}
return integer;
};
Buffer.prototype.skip = function(bits) {
// done :-) // why wrapper , i dont know :P
this.ptr += bits;
}
function mp3Reader () {
'use_strict';
var file = '';
var meta = null;
var fd = null;
var self = this;
var frames = 0;
var Length = 0;
var buffer = {
head:new Buffer(4),
body:null
}
var iter = 0;
function readStream() {
fs.read(fd,buffer.head,0,4,null, function(err,bytesRead) {
if(err || iter > 5) {
return;
}
iter++;
var parsed = {};
/* 0000 0000 0000 0000 0000 0000 0000 0000 *.
* Sample
* F F F
* Mpeg Version = 20th and 19th bit from right;
* 0000 0000 0001 1000 0000 0000 0000 0000
*
* MPEG LAYER = 17th and 18th bit from right
* 0000 0000 0000 0110 0000 0000 0000 0000
* ----------0000 0110
*
* MPEG CRC
* 0000 0000 0000 0001 0000 0000 0000 0000
* 0000 0001 - 0x1;
*
* MPEG BITRATE INFORMATION
* 0000 0000 0000 0000 1111 0000 0000 0000
* ------------------- 1111 0000 = F0
*
* Sampler information
*
* 0000 0000 0000 0000 0000 1100 0000 0000
* ------------------- 0000 1100 - C
*
* Padding bit
* 0000 0000 0000 0000 0000 0010 0000 0000
* --------------------0000 0010 - 2 ;
*
* Priv - 8th bit;
*
* Channel Mode
*
* 0000 0000 0000 0000 0000 0000 1100 0000
* ------------------------------1100 0000 - C0
*
* JS ext
* 0011 0000 - 30
*
* (C)
* 0000 1000 - 8
* (Origninal)
* 0000 0100 - 4
*/
//
// 1000 - 8 , 1001 - 9 , 1010 - A , 1011 - B , 1100 - 12
if ( (buffer.head[0] & 0xff) !== 255 && (buffer.head[1] & 0xf0 >>> 4) !== 15 ) {
console.log(Length);
return; /* End of mp3 */
}
var ver = ( buffer.head[1] & 0x18)>>>3,
layer = ( buffer.head[1]& 0x6 )>>>1,
crc = ( buffer.head[1]&0x1 ),
bitrate = ( buffer.head[2] & 0xF0 )>>>4,
samplerate = ( buffer.head[2] & 0xC)>>>2,
padding = ( buffer.head[2] & 0x2)>>>1,
priv = ( buffer.head[2]& 0x1 ),
channel = ( buffer.head[3] & 0xC0 )>>>6,
jsExt = ( buffer.head[3]& 0x30)>>> 4, /* joing stereo ext */
cpyrite = ( buffer.head[3]& 0x9)>>> 3,
orig = ( buffer.head[3] & 0x4 )>>>2,
Emph = ( buffer.head[3] & 0x3 ),
sample_per_frame = ext.ver.samplePerFrame[ver][layer],
frameSize = 144 * ( ext.ver.bitrate[ver][layer][bitrate]*1000 / ext.ver.samplerate[ver][samplerate] ) ;
frameSize = Math.floor(frameSize) + ( (!!padding)?ext.ver.slot[layer]:0 );
Length += sample_per_frame/ext.ver.samplerate[ver][samplerate];
/*
console.log('Version : ',ext.ver.frame[ver]);
console.log('Layer : ',ext.ver.layer[layer]);
console.log('Bitrate : ',ext.ver.bitrate[ver][layer][bitrate],'kbps');
console.log('CRC : ',!!crc);
console.log('SampleRate : ',ext.ver.samplerate[ver][samplerate]);
console.log('Channel : ',ext.ver.channel[channel]);
console.log('Mode Ext : ',(channel===1)? ext.ver.jsMode[layer][jsExt] :null);
console.log('Copyrighted : ',!!cpyrite);
console.log('Original : ',!!orig);
console.log('Emph : ',ext.ver.Emphasis[Emph]);
console.log('Padding : ',!!padding);
console.log('FrameSize :',frameSize);
// */
var body = new Buffer( frameSize - 4 ); // we already read the 4 bytes :P
fs.read(fd,body,0,frameSize - 4 ,null, function(err,bytesRead) {
if(err) {
console.dir(err);
return;
}
if(crc) {
var checksum = body.getBits(16);
}
if(channel < 2) {
/* Not single */
/* Mpeg Layer III */
if( layer === 1) {
var main_data_end = body.getBits(9),
priv_bits = body.getBits(3),
scfsi = [[
body.getBits(1),
body.getBits(1),
body.getBits(1),
body.getBits(1),/* shifting this will be an overkill son */
],[
body.getBits(1),
body.getBits(1),
body.getBits(1),
body.getBits(1),/* shifting this will be an overkill son */
]],
grn = [
[{},{}],[{},{}]
];
for(var gr = 0 ; gr < 2 ; gr++ ) {
for( var ch = 0 ; ch < 2 ; ch++) {
grn[gr][ch].part2_3_length = body.getBits(12);
grn[gr][ch].bigvalues = body.getBits(9);
grn[gr][ch].global_gain = body.getBits(8);
grn[gr][ch].scalefac_comp = body.getBits(4);
grn[gr][ch].blocksplit_flag = !!(body.getBits(1));
if(grn[gr][ch].blocksplit_flag) {
grn[gr][ch].blocktype = body.getBits(2);
grn[gr][ch].switch_point = body.getBits(1);
grn[gr][ch].region = [];
grn[gr][ch].sub_block_gain = [];
for(var region = 0 ; region < 2 ; region++) {
grn[gr][ch].region[region] = body.getBits(5);
}
for(var win = 0 ; win < 3 ; win ++) {
grn[gr][ch].sub_block_gain[win] = body.getBits(3);
}
} else {
grn[gr][ch].region = [];
grn[gr][ch].reg = [];
for(var region = 0 ; region < 3 ; region++) {
grn[gr][ch].region[region] = body.getBits(5);
}
grn[gr][ch].reg = {
region_address1:body.getBits(4),
region_address2:body.getBits(3)
};
}
}
}
for(var gr = 0; gr < 2 ; gr++ ) {
for(var ch =0; ch < 2 ;ch++) {
if( grn[gr][ch].blocksplit_flag === 1 && grn[gr][ch].blocktype == 2) {
grn[gr][ch].scalefac = [];
for(cb = 0; cb < grn[gr][ch].switch_point ; cb++) {
grn[gr][ch].scalefac[cb] = body.getBits(1);
}
}
}
}
}
console.log('Main Data Length',main_data_end);
console.log('scfsi',scfsi);
console.dir(grn[0][0]);
} else {
/* Yeah now its single :-( */
}
readStream();
})
});
}
self.load = function ( _file ) {
file = _file;
fs.open(file,'r', function(err,_fd) {
if(err) {
return;
}
fd = _fd;
new id3Reader().read(file, function(tags) {
meta = tags;
console.dir(tags);
readStream();
},fd);
});
}
};
var f1 = new mp3Reader();
f1.load('dah.mp3');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment