mp3
Version:
An MP3 decoder for Aurora.js
174 lines (139 loc) • 6.04 kB
JavaScript
var AV = require('av');
var ID3v23Stream = require('./id3').ID3v23Stream;
var ID3v22Stream = require('./id3').ID3v22Stream;
var MP3FrameHeader = require('./header');
var MP3Stream = require('./stream');
var MP3Demuxer = AV.Demuxer.extend(function() {
AV.Demuxer.register(this);
this.probe = function(stream) {
var off = stream.offset;
// skip id3 metadata if it exists
var id3header = MP3Demuxer.getID3v2Header(stream);
if (id3header)
stream.advance(10 + id3header.length);
// attempt to read the header of the first audio frame
var s = new MP3Stream(new AV.Bitstream(stream));
var header = null;
try {
header = MP3FrameHeader.decode(s);
} catch (e) {};
// go back to the beginning, for other probes
stream.seek(off);
return !!header;
};
this.getID3v2Header = function(stream) {
if (stream.peekString(0, 3) == 'ID3') {
stream = AV.Stream.fromBuffer(stream.peekBuffer(0, 10));
stream.advance(3); // 'ID3'
var major = stream.readUInt8();
var minor = stream.readUInt8();
var flags = stream.readUInt8();
var bytes = stream.readBuffer(4).data;
var length = (bytes[0] << 21) | (bytes[1] << 14) | (bytes[2] << 7) | bytes[3];
return {
version: '2.' + major + '.' + minor,
major: major,
minor: minor,
flags: flags,
length: length
};
}
return null;
};
const XING_OFFSETS = [[32, 17], [17, 9]];
this.prototype.parseDuration = function(header) {
var stream = this.stream;
var frames;
var offset = stream.offset;
if (!header || header.layer !== 3)
return false;
// Check for Xing/Info tag
stream.advance(XING_OFFSETS[header.flags & MP3FrameHeader.FLAGS.LSF_EXT ? 1 : 0][header.nchannels() === 1 ? 1 : 0]);
var tag = stream.readString(4);
if (tag === 'Xing' || tag === 'Info') {
var flags = stream.readUInt32();
if (flags & 1)
frames = stream.readUInt32();
if (flags & 2)
var size = stream.readUInt32();
if (flags & 4 && frames && size) {
for (var i = 0; i < 100; i++) {
var b = stream.readUInt8();
var pos = b / 256 * size | 0;
var time = i / 100 * (frames * header.nbsamples() * 32) | 0;
this.addSeekPoint(pos, time);
}
}
if (flags & 8)
stream.advance(4);
} else {
// Check for VBRI tag (always 32 bytes after end of mpegaudio header)
stream.seek(offset + 4 + 32);
tag = stream.readString(4);
if (tag == 'VBRI' && stream.readUInt16() === 1) { // Check tag version
stream.advance(4); // skip delay and quality
stream.advance(4); // skip size
frames = stream.readUInt32();
var entries = stream.readUInt16();
var scale = stream.readUInt16();
var bytesPerEntry = stream.readUInt16();
var framesPerEntry = stream.readUInt16();
var fn = 'readUInt' + (bytesPerEntry * 8);
var pos = 0;
for (var i = 0; i < entries; i++) {
this.addSeekPoint(pos, framesPerEntry * i);
pos += stream[fn]();
}
}
}
if (!frames)
return false;
this.emit('duration', (frames * header.nbsamples() * 32) / header.samplerate * 1000 | 0);
return true;
};
this.prototype.readChunk = function() {
var stream = this.stream;
if (!this.sentInfo) {
// read id3 metadata if it exists
var id3header = MP3Demuxer.getID3v2Header(stream);
if (id3header) {
stream.advance(10);
if (id3header.major > 2) {
var id3 = new ID3v23Stream(id3header, stream);
} else {
var id3 = new ID3v22Stream(id3header, stream);
}
this.emit('metadata', id3.read());
}
// read the header of the first audio frame
var off = stream.offset;
var s = new MP3Stream(new AV.Bitstream(stream));
var header = MP3FrameHeader.decode(s);
if (!header)
return this.emit('error', 'Could not find first frame.');
this.emit('format', {
formatID: 'mp3',
sampleRate: header.samplerate,
channelsPerFrame: header.nchannels(),
bitrate: header.bitrate,
floatingPoint: true
});
var sentDuration = this.parseDuration(header);
stream.advance(off - stream.offset);
// if there were no Xing/VBRI tags, guesstimate the duration based on data size and bitrate
this.dataSize = 0;
if (!sentDuration) {
this.on('end', function() {
this.emit('duration', this.dataSize * 8 / header.bitrate * 1000 | 0);
});
}
this.sentInfo = true;
}
while (stream.available(1)) {
var buffer = stream.readSingleBuffer(stream.remainingBytes());
this.dataSize += buffer.length;
this.emit('data', buffer);
}
};
});
module.exports = MP3Demuxer;