Last active
January 13, 2023 02:36
-
-
Save darkyen/4450502 to your computer and use it in GitHub Desktop.
A javascript based decoder for mp3 frame parsing supports id3
This file contains hidden or 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
/* | |
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