diff --git a/lib/asf.js b/lib/asf.js index ec85b90..27b3053 100644 --- a/lib/asf.js +++ b/lib/asf.js @@ -2,46 +2,46 @@ var fs = require('fs'); var util = require('util'); var events = require('events'); -var strtok = require('strtok2'); +var Calippo = require('Calippo'); var common = require('./common'); var equal = require('deep-equal'); var decodeString = common.decodeString; -module.exports = function (stream, callback, done) { +module.exports = function () { var currentState = startState; - strtok.parse(stream, function (v, cb) { - currentState = currentState.parse(callback, v, done); - return currentState.getExpectedType(); + return new Calippo({'objectMode': true}, function (v) { + currentState = currentState.parse(this, v); + return currentState.getExpectedType(this); }) }; var startState = { - parse: function (callback) { + parse: function (parser) { return idState; }, } var finishedState = { - parse: function (callback) { + parse: function (parser) { return this; }, - getExpectedType: function () { - return strtok.DONE; + getExpectedType: function (parser) { + parser.push(null); } } var idState = { - parse: function (callback, data, done) { + parse: function (parser, data) { if (!equal(common.asfGuidBuf, data)) { - done(new Error('expected asf header but was not found')); + parser.emit('error', new Error('expected asf header but was not found')); return finishedState; } return headerDataState; }, - getExpectedType: function () { - return new strtok.BufferType(common.asfGuidBuf.length); + getExpectedType: function (parser) { + return parser.Buffer(common.asfGuidBuf.length); } }; @@ -50,7 +50,7 @@ function ReadObjectState(size, objectCount) { this.objectCount = objectCount; } -ReadObjectState.prototype.parse = function(callback, data, done) { +ReadObjectState.prototype.parse = function(parser, data) { var guid = data.slice(0, 16); var size = readUInt64LE(data, 16) var State = stateByGuid(guid) || IgnoreObjectState; @@ -60,21 +60,21 @@ ReadObjectState.prototype.parse = function(callback, data, done) { return new State(nextState, size - 24); } -ReadObjectState.prototype.getExpectedType = function() { - return new strtok.BufferType(24); +ReadObjectState.prototype.getExpectedType = function(parser) { + return parser.Buffer(24); } var headerDataState = { - parse: function (callback, data, done) { + parse: function (parser, data) { var size = readUInt64LE(data, 0); var objectCount = data.readUInt32LE(8); return new ReadObjectState(size, objectCount); }, - getExpectedType: function () { + getExpectedType: function (parser) { // 8 bytes size // 4 bytes object count // 2 bytes ignore - return new strtok.BufferType(14); + return parser.Buffer(14); } }; @@ -83,13 +83,13 @@ function IgnoreObjectState(nextState, size) { this.size = size; } -IgnoreObjectState.prototype.parse = function(callback, data, done) { - if (this.nextState === finishedState) done(); +IgnoreObjectState.prototype.parse = function(parser, data) { + if (this.nextState === finishedState) parser.push(null); return this.nextState; } -IgnoreObjectState.prototype.getExpectedType = function() { - return new strtok.IgnoreType(this.size); +IgnoreObjectState.prototype.getExpectedType = function(parser) { + return parser.Skip(this.size) } function ContentDescriptionObjectState(nextState, size) { @@ -98,7 +98,7 @@ function ContentDescriptionObjectState(nextState, size) { } var contentDescTags = ['Title', 'Author', 'Copyright', 'Description', 'Rating']; -ContentDescriptionObjectState.prototype.parse = function(callback, data, done) { +ContentDescriptionObjectState.prototype.parse = function(parser, data) { var lengths = [ data.readUInt16LE(0), data.readUInt16LE(2), @@ -113,16 +113,17 @@ ContentDescriptionObjectState.prototype.parse = function(callback, data, done) { var end = pos + length; if (length > 0) { var value = parseUnicodeAttr(data.slice(pos, end)); - callback(tagName, value); + parser.push([tagName, value]) } pos = end; } - if (this.nextState === finishedState) done(); + // if (this.nextState === finishedState) done(); + // TODO: what to do about this return this.nextState; } -ContentDescriptionObjectState.prototype.getExpectedType = function() { - return new strtok.BufferType(this.size); +ContentDescriptionObjectState.prototype.getExpectedType = function(parser) { + return parser.Buffer(this.size); } ContentDescriptionObjectState.guid = new Buffer([ @@ -145,7 +146,7 @@ var attributeParsers = [ parseByteArrayAttr, ]; -ExtendedContentDescriptionObjectState.prototype.parse = function(callback, data, done) { +ExtendedContentDescriptionObjectState.prototype.parse = function(parser, data) { var attrCount = data.readUInt16LE(0); var pos = 2; for (var i = 0; i < attrCount; i += 1) { @@ -161,18 +162,19 @@ ExtendedContentDescriptionObjectState.prototype.parse = function(callback, data, pos += valueLen; var parseAttr = attributeParsers[valueType]; if (!parseAttr) { - done(new Error('unexpected value type: ' + valueType)); + parser.emit(['error', new Error('unexpected value type: ' + valueType)]); + // TODO: startstate? return finishedState; } var attr = parseAttr(value); - callback(name, attr); + parser.push([name, attr]) } - if (this.nextState === finishedState) done(); + if (this.nextState === finishedState) parser.push(null); return this.nextState; } -ExtendedContentDescriptionObjectState.prototype.getExpectedType = function() { - return new strtok.BufferType(this.size); +ExtendedContentDescriptionObjectState.prototype.getExpectedType = function(parser) { + return parser.Buffer(this.size); } ExtendedContentDescriptionObjectState.guid = new Buffer([ @@ -185,17 +187,17 @@ function FilePropertiesObject(nextState, size) { this.size = size; } -FilePropertiesObject.prototype.parse = function (callback, data, done) { +FilePropertiesObject.prototype.parse = function (parser, data) { // in miliseconds var playDuration = parseQWordAttr(data.slice(40, 48)) / 10000 - callback('duration', Math.round(playDuration / 1000)) + parser.push(['duration', Math.round(playDuration / 1000)]) - if (this.nextState === finishedState) done(); + if (this.nextState === finishedState) parser.push(null); return this.nextState; } -FilePropertiesObject.prototype.getExpectedType = function() { - return new strtok.BufferType(this.size); +FilePropertiesObject.prototype.getExpectedType = function(parser) { + return parser.Buffer(this.size); } FilePropertiesObject.guid = new Buffer([ diff --git a/lib/flac.js b/lib/flac.js index 4b29f5c..5f57b47 100644 --- a/lib/flac.js +++ b/lib/flac.js @@ -1,13 +1,13 @@ 'use strict'; -var strtok = require('strtok2'); +var Calippo = require('calippo'); var common = require('./common'); -module.exports = function (stream, callback, done) { +module.exports = function () { var currentState = startState; - strtok.parse(stream, function (v, cb) { - currentState = currentState.parse(callback, v, done); - return currentState.getExpectedType(); + return new Calippo({'objectMode': true}, function (v) { + currentState = currentState.parse(this, v); + return currentState.getExpectedType(this); }) } @@ -17,7 +17,7 @@ var DataDecoder = function (data) { } DataDecoder.prototype.readInt32 = function () { - var value = strtok.UINT32_LE.get(this.data, this.offset); + var value = this.data.readUInt32LE(this.offset); this.offset += 4; return value; } @@ -30,11 +30,11 @@ DataDecoder.prototype.readStringUtf8 = function () { }; var finishedState = { - parse: function (callback) { + parse: function (parser, data) { return this; }, - getExpectedType: function () { - return strtok.DONE; + getExpectedType: function (parser) { + parser.push(null) } } @@ -44,7 +44,7 @@ var BlockDataState = function (type, length, nextStateFactory) { this.nextStateFactory = nextStateFactory; } -BlockDataState.prototype.parse = function (callback, data) { +BlockDataState.prototype.parse = function (parser, data) { if (this.type === 4) { var decoder = new DataDecoder(data); var vendorString = decoder.readStringUtf8(); @@ -56,35 +56,33 @@ BlockDataState.prototype.parse = function (callback, data) { for (i = 0; i < commentListLength; i++) { comment = decoder.readStringUtf8(); split = comment.split('='); - callback(split[0].toUpperCase(), split[1]); + parser.push([split[0].toUpperCase(), split[1]]) } } else if (this.type === 6) { var picture = common.readVorbisPicture(data); - callback('METADATA_BLOCK_PICTURE', picture); + parser.push(['METADATA_BLOCK_PICTURE', picture]) } else if (this.type === 0) { // METADATA_BLOCK_STREAMINFO if (data.length < 34) return; // invalid streaminfo var sampleRate = common.strtokUINT24_BE.get(data, 10) >> 4; var totalSamples = data.readUInt32BE(14); var duration = totalSamples / sampleRate; - callback('duration', Math.round(duration)); + parser.push(['duration', Math.round(duration)]); } - return this.nextStateFactory(); } -BlockDataState.prototype.getExpectedType = function () { - return new strtok.BufferType(this.length); +BlockDataState.prototype.getExpectedType = function (parser) { + return parser.Buffer(this.length); } var blockHeaderState = { - parse: function (callback, data, done) { + parse: function (parser, data) { var header = { lastBlock: (data[0] & 0x80) == 0x80, type: data[0] & 0x7f, length: common.strtokUINT24_BE.get(data, 1) } var followingStateFactory = header.lastBlock ? function() { - done(); return finishedState; } : function() { return blockHeaderState; @@ -92,28 +90,28 @@ var blockHeaderState = { return new BlockDataState(header.type, header.length, followingStateFactory); }, - getExpectedType: function () { - return new strtok.BufferType(4); + getExpectedType: function (parser) { + return parser.Buffer(4); } } var idState = { - parse: function (callback, data, done) { + parse: function (parser, data) { if (data !== 'fLaC') { - done(new Error('expected flac header but was not found')); + // TODO: shouldn't the param be wrapped in an array?? [] + parser.emit('error', new Error('expected flac header but was not found')) + return startState; } return blockHeaderState; }, - getExpectedType: function () { - return new strtok.StringType(4); + getExpectedType: function (parser) { + return parser.String(4); } }; var startState = { - parse: function (callback) { + parse: function (parser, data) { return idState; }, - getExpectedType: function () { - return strtok.DONE; - } + getExpectedType: function (parser) {} } diff --git a/lib/id3v1.js b/lib/id3v1.js index 5e60d90..142e0e7 100644 --- a/lib/id3v1.js +++ b/lib/id3v1.js @@ -1,41 +1,51 @@ 'use strict'; -var util = require('util'); +var through2 = require('through2'); var common = require('./common'); -module.exports = function (stream, callback, done) { - var endData = null; - stream.on('data', function (data) { - endData = data; - }); - common.streamOnRealEnd(stream, function () { - var offset = endData.length - 128; - var header = endData.toString('ascii', offset, offset += 3); - if (header !== 'TAG') { - return done(new Error('Could not find metadata header')); - } +module.exports = function (readDuration) { + + var bufs = [] + + return through2({ objectMode: true}, + function (data, encoding, callback) { + bufs.push(data) + callback() + }, + function (callback) { + var buffer = Buffer.concat(bufs) + var offset = buffer.length - 128; + + var header = buffer.toString('ascii', offset, offset += 3); + if (header !== 'TAG') { + this.emit('error', new Error('Could not find metadata header')); + // this.push(null) + return; + } - var title = endData.toString('ascii', offset, offset += 30); - callback('title', title.trim().replace(/\x00/g, '')); + var title = buffer.toString('ascii', offset, offset += 30); + this.push(['title', title.trim().replace(/\x00/g, '')]); - var artist = endData.toString('ascii', offset, offset += 30); - callback('artist', artist.trim().replace(/\x00/g, '')); + var artist = buffer.toString('ascii', offset, offset += 30); + this.push(['artist', artist.trim().replace(/\x00/g, '')]); - var album = endData.toString('ascii', offset, offset += 30); - callback('album', album.trim().replace(/\x00/g, '')); + var album = buffer.toString('ascii', offset, offset += 30); + this.push(['album', album.trim().replace(/\x00/g, '')]); - var year = endData.toString('ascii', offset, offset += 4); - callback('year', year.trim().replace(/\x00/g, '')); + var year = buffer.toString('ascii', offset, offset += 4); + this.push(['year', year.trim().replace(/\x00/g, '')]); - var comment = endData.toString('ascii', offset, offset += 28); - callback('comment', comment.trim().replace(/\x00/g, '')); + var comment = buffer.toString('ascii', offset, offset += 28); + this.push(['comment', comment.trim().replace(/\x00/g, '')]); - var track = endData[endData.length - 2]; - callback('track', track); + var track = buffer[buffer.length - 2]; + this.push(['track', track]); - if (endData[endData.length - 1] in common.GENRES) { - var genre = common.GENRES[endData[endData.length - 1]]; - callback('genre', genre); + if (buffer[buffer.length - 1] in common.GENRES) { + var genre = common.GENRES[buffer[buffer.length - 1]]; + this.push(['genre', genre]); + } + this.push(null) + callback() } - return done(); - }); + ) } diff --git a/lib/id3v2.js b/lib/id3v2.js index e3c80b3..079e8fc 100644 --- a/lib/id3v2.js +++ b/lib/id3v2.js @@ -1,29 +1,35 @@ 'use strict'; -var strtok = require('strtok2'); +var Calippo = require('calippo'); var parser = require('./id3v2_frames'); var common = require('./common'); var BitArray = require('node-bitarray'); var equal = require('deep-equal'); var sum = require('sum-component'); -module.exports = function (stream, callback, done, readDuration, fileSize) { +module.exports = function (readDuration, fileSize) { var frameCount = 0; var audioFrameHeader; var bitrates = []; - strtok.parse(stream, function (v, cb) { + return new Calippo({'objectMode': true}, function (v) { + + // var seekFirstAudioFrame = _seekFirstAudioFrame.bind(this) + var self = this; + if (!v) { - cb.state = 0; - return new strtok.BufferType(10); + this.state = 0; + return this.Buffer(10); } - switch (cb.state) { + switch (this.state) { case 0: // header if (v.toString('ascii', 0, 3) !== 'ID3') { - return done(new Error('expected id3 header but was not found')); + this.emit('error', new Error('expected id3 header but was not found')) + this.push(null); + return; } - cb.id3Header = { + this.id3Header = { version: '2.' + v[3] + '.' + v[4], major: v[3], unsync: common.strtokBITSET.get(v, 5, 7), @@ -32,32 +38,34 @@ module.exports = function (stream, callback, done, readDuration, fileSize) { footer: common.strtokBITSET.get(v, 5, 4), size: common.strtokINT32SYNCSAFE.get(v, 6) } - cb.state = 1; - return new strtok.BufferType(cb.id3Header.size); + this.state = 1; + return this.Buffer(this.id3Header.size); case 1: // id3 data - parseMetadata(v, cb.id3Header, done).map(function (obj) { - callback.apply(this, obj) + parseMetadata(v, this.id3Header).map(function (obj) { + self.push(obj) }) if (readDuration) { - cb.state = 2; - return new strtok.BufferType(4); + this.state = 2; + return this.Buffer(4); } - return done(); + this.push(null); + return; case 1.5: var shiftedBuffer = new Buffer(4); - cb.frameFragment.copy(shiftedBuffer, 0, 1); + this.frameFragment.copy(shiftedBuffer, 0, 1); v.copy(shiftedBuffer, 3); v = shiftedBuffer; - cb.state = 2; + this.state = 2; /* falls through */ case 2: // audio frame header // we have found the id3 tag at the end of the file, ignore if (v.slice(0, 3).toString() === 'TAG') { - return strtok.DONE; + this.push(null); + return; } var bts = BitArray.fromBuffer(v); @@ -66,7 +74,7 @@ module.exports = function (stream, callback, done, readDuration, fileSize) { if (sum(syncWordBits) != 11) { // keep scanning for frame header, id3 tag may // have some padding (0x00) at the end - return seekFirstAudioFrame(done); + return seekFirstAudioFrame(this); } var header = { @@ -78,12 +86,12 @@ module.exports = function (stream, callback, done, readDuration, fileSize) { } if (isNaN(header.version) || isNaN(header.layer)) { - return seekFirstAudioFrame(done); + return seekFirstAudioFrame(this); } // mp3 files are only found in MPEG1/2 Layer 3 if ((header.version !== 1 && header.version !== 2) || header.layer !== 3) { - return seekFirstAudioFrame(done); + return seekFirstAudioFrame(this); } header.samples_per_frame = calcSamplesPerFrame( @@ -92,13 +100,13 @@ module.exports = function (stream, callback, done, readDuration, fileSize) { header.bitrate = common.id3BitrateCalculator( bts.slice(16, 20), header.version, header.layer); if (isNaN(header.bitrate)) { - return seekFirstAudioFrame(done); + return seekFirstAudioFrame(this); } header.sample_rate = common.samplingRateCalculator( bts.slice(20, 22), header.version); if (isNaN(header.sample_rate)) { - return seekFirstAudioFrame(done); + return seekFirstAudioFrame(this); } header.slot_size = calcSlotSize(header.layer); @@ -117,75 +125,83 @@ module.exports = function (stream, callback, done, readDuration, fileSize) { // xtra header only exists in first frame if (frameCount === 1) { - cb.offset = header.sideinfo_length; - cb.state = 3; - return new strtok.BufferType(header.sideinfo_length); + this.offset = header.sideinfo_length; + this.state = 3; + return this.Buffer(header.sideinfo_length); } // the stream is CBR if the first 3 frame bitrates are the same if (readDuration && fileSize && frameCount === 3 && areAllSame(bitrates)) { fileSize(function (size) { // subtract non audio stream data from duration calculation - size = size - cb.id3Header.size; + size = size - self.id3Header.size; var kbps = (header.bitrate * 1000) / 8; - callback('duration', Math.round(size / kbps)); - cb(done()); - }) - return strtok.DEFER; - } - - // once we know the file is VBR attach listener to end of - // stream so we can do the duration calculation when we - // have counted all the frames - if (readDuration && frameCount === 4) { - stream.once('end', function () { - callback('duration', calcDuration(frameCount, - header.samples_per_frame, header.sample_rate)) - done() + self.push(['duration', Math.round(size / kbps)]) + self.push(null) + // // // cb(done()); + // // // TODO: might fail + // self.defer() }) + return this.DEFER; } - cb.state = 5; - return new strtok.BufferType(header.frame_size - 4); + this.state = 5; + return this.Buffer(header.frame_size - 4); + + // // once we know the file is VBR attach listener to end of + // // stream so we can do the duration calculation when we + // // have counted all the frames + // if (readDuration && frameCount === 4) { + // // TODO: stream doesn't exist anymore + // stream.once('end', function () { + // self.push(['duration', calcDuration(frameCount, + // header.samples_per_frame, header.sample_rate)]) + // done() + // self.push(null) + // }) + // } case 3: // side information - cb.offset += 12; - cb.state = 4; - return new strtok.BufferType(12); + this.offset += 12; + this.state = 4; + return this.Buffer(12); case 4: // xtra / info header - cb.state = 5; - var frameDataLeft = audioFrameHeader.frame_size - 4 - cb.offset; + this.state = 5; + var frameDataLeft = audioFrameHeader.frame_size - 4 - this.offset; var id = v.toString('ascii', 0, 4); if (id !== 'Xtra' && id !== 'Info' && id !== 'Xing') { - return new strtok.BufferType(frameDataLeft); + return this.Buffer(frameDataLeft); } var bits = BitArray.fromBuffer(v.slice(4, 8)); // frames field is not present if (bits.__bits[bits.__bits.length-1] !== 1) { - return new strtok.BufferType(frameDataLeft); + return this.Buffer(frameDataLeft); } var numFrames = v.readUInt32BE(8); var ah = audioFrameHeader; - callback('duration', calcDuration(numFrames, ah.samples_per_frame, ah.sample_rate)); - return done(); + this.push(['duration', calcDuration(numFrames, ah.samples_per_frame, ah.sample_rate)]) + this.push(null) + return; case 5: // skip frame data - cb.state = 2; - return new strtok.BufferType(4); + this.state = 2; + return this.Buffer(4); } - function seekFirstAudioFrame(done) { + function seekFirstAudioFrame(ctx) { if (frameCount) { - return done(new Error('expected frame header but was not found')); + ctx.emit('error', new Error('expected frame header but was not found')) + ctx.push(null) + return undefined; } - cb.frameFragment = v; - cb.state = 1.5; - return new strtok.BufferType(1); + ctx.frameFragment = v; + ctx.state = 1.5; + return ctx.Buffer(1); } }); }; @@ -201,7 +217,7 @@ function calcDuration (numFrames, samplesPerFrame, sampleRate) { return Math.round(numFrames * (samplesPerFrame / sampleRate)); } -function parseMetadata (data, header, done) { +function parseMetadata (data, header) { var offset = 0; var frames = []; @@ -211,7 +227,7 @@ function parseMetadata (data, header, done) { while (true) { if (offset === data.length) break; - var frameHeaderBytes = data.slice(offset, offset += getFrameHeaderLength(header.major, done)); + var frameHeaderBytes = data.slice(offset, offset += getFrameHeaderLength(header.major)); var frameHeader = readFrameHeader(frameHeaderBytes, header.major); // Last frame. Check first char is a letter, bit of defensive programming @@ -256,7 +272,7 @@ function readFrameHeader (v, majorVer) { break; case 3: header.id = v.toString('ascii', 0, 4); - header.length = strtok.UINT32_BE.get(v, 4, 8); + header.length = v.readUInt32BE(4, 8); header.flags = readFrameFlags(v.slice(8, 10)); break; case 4: @@ -268,7 +284,7 @@ function readFrameHeader (v, majorVer) { return header; } -function getFrameHeaderLength (majorVer, done) { +function getFrameHeaderLength (majorVer) { switch (majorVer) { case 2: return 6; @@ -276,7 +292,7 @@ function getFrameHeaderLength (majorVer, done) { case 4: return 10; default: - return done(new Error('header version is incorrect')); + throw new Error('header version is incorrect') // TODO: need to emit header upstream } } diff --git a/lib/id4.js b/lib/id4.js index bfc1725..2c97952 100644 --- a/lib/id4.js +++ b/lib/id4.js @@ -1,84 +1,86 @@ 'use strict'; -var strtok = require('strtok2'); +var Calippo = require('calippo'); var common = require('./common'); -module.exports = function (stream, callback, done, readDuration) { +module.exports = function (readDuration) { - strtok.parse(stream, function (v, cb) { + return new Calippo({'objectMode': true}, function (v) { // the very first thing we expect to see is the first atom's length if (!v) { - cb.metaAtomsTotalLength = 0; - cb.state = 0; - return strtok.UINT32_BE; + this.metaAtomsTotalLength = 0; + this.state = 0; + return this.readUInt32BE; } - switch (cb.state) { + switch (this.state) { case -1: // skip - cb.state = 0; - return strtok.UINT32_BE; + this.state = 0; + return this.readUInt32BE; case 0: // atom length - cb.atomLength = v; - cb.state++; - return new strtok.StringType(4, 'binary'); + this.atomLength = v; + this.state++; + return this.String(4, 'binary') case 1: // atom name - cb.atomName = v; + this.atomName = v; // meta has 4 bytes padding at the start (skip) if (v === 'meta') { - cb.state = -1; // what to do for skip? - return new strtok.BufferType(4); + this.state = -1; // what to do for skip? + return this.Buffer(4); } if (readDuration) { if (v === 'mdhd') { - cb.state = 3; - return new strtok.BufferType(cb.atomLength - 8); + this.state = 3; + return this.Buffer(this.atomLength - 8); } } if (!~CONTAINER_ATOMS.indexOf(v)) { // whats the num for ilst? - cb.state = (cb.atomContainer === 'ilst') ? 2 : -1; - return new strtok.BufferType(cb.atomLength - 8); + this.state = (this.atomContainer === 'ilst') ? 2 : -1; + return this.Buffer(this.atomLength - 8); } // dig into container atoms - cb.atomContainer = v; - cb.atomContainerLength = cb.atomLength; - cb.state--; - return strtok.UINT32_BE; + this.atomContainer = v; + this.atomContainerLength = this.atomLength; + this.state--; + return this.readUInt32BE; case 2: // ilst atom - cb.metaAtomsTotalLength += cb.atomLength; - var result = processMetaAtom(v, cb.atomName, cb.atomLength - 8); + this.metaAtomsTotalLength += this.atomLength; + var result = processMetaAtom(v, this.atomName, this.atomLength - 8); if (result.length > 0) { for (var i = 0; i < result.length; i++) { - callback(cb.atomName, result[i]); + this.push([this.atomName, result[i]]) } } // we can stop processing atoms once we get to the end of the ilst atom - if (cb.metaAtomsTotalLength >= cb.atomContainerLength - 8) { - return done(); + if (this.metaAtomsTotalLength >= this.atomContainerLength - 8) { + this.push(null); + return; } - cb.state = 0; - return strtok.UINT32_BE; + this.state = 0; + return this.readUInt32BE; case 3: // mdhd atom // TODO: support version 1 var sampleRate = v.readUInt32BE(12); var duration = v.readUInt32BE(16); - callback('duration', Math.floor(duration / sampleRate)); - cb.state = 0; - return strtok.UINT32_BE; + this.push(['duration', Math.floor(duration / sampleRate)]) + this.state = 0; + return this.readUInt32BE; } // if we ever get this this point something bad has happened - return done(new Error('error parsing')); + this.emit('error', new Error('error parsing')) + return }) } @@ -90,8 +92,8 @@ function processMetaAtom (data, atomName, atomLength) { if (atomName === '----') return result; while (offset < atomLength) { - var length = strtok.UINT32_BE.get(data, offset); - var type = TYPES[strtok.UINT32_BE.get(data, offset + 8)]; + var length = data.readUInt32BE(offset); + var type = TYPES[data.readUInt32BE(offset + 8)]; var content = processMetaDataAtom(data.slice(offset + 12, offset + length), type, atomName); @@ -108,14 +110,13 @@ function processMetaAtom (data, atomName, atomLength) { case 'uint8': if (atomName === 'gnre') { - var genreInt = strtok.UINT8.get(data, 5); + var genreInt = data.readUInt8(5); return common.GENRES[genreInt - 1]; } if (atomName === 'trkn' || atomName === 'disk') { return data[7] + '/' + data[9]; } - - return strtok.UINT8.get(data, 4); + return data.readUInt8(4); case 'jpeg': case 'png': diff --git a/lib/index.js b/lib/index.js index 09f2d84..160f856 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,8 +1,7 @@ 'use strict'; var events = require('events'); var common = require('./common'); -var strtok = require('strtok2'); -var through = require('through') +var domain = require('domain') var fs = require('fs') var MusicMetadata = module.exports = function (stream, opts, callback) { @@ -32,9 +31,9 @@ var MusicMetadata = module.exports = function (stream, opts, callback) { } } - // pipe to an internal stream so we aren't fucking + // pipe to an internal stream so we aren't messing // with the stream passed to us by our users - var istream = stream.pipe(through(null, null, {autoDestroy: false})); + var istream = stream; var metadata = { title: '', @@ -52,22 +51,54 @@ var MusicMetadata = module.exports = function (stream, opts, callback) { var aliased = {}; var hasReadData = false; - istream.once('data', function (result) { + istream.once('readable', function () { + var data = this.read(20); // TODO: what if null?? + if (!data) { + hasReadData = false; + return; + } + this.unshift(data); + var parser = common.getParserForMediaType(headerTypes, data); + var errd = false; + + // TODO: hate this variable name + var closeMode = (parser !== require('./id3v2')) + + // TODO: explain why destination pipe has to stay open.. basically in id3v2 + // we calculate the duration in filesize callback which MAY be called AFTER + // the source filestream is closed. + this.pipe(parser(opts.hasOwnProperty('duration'), fsize), { end: closeMode }) + .on('readable', function () { + var val; + while (true) { + // while ((val = this.read()) !== null) { + val = this.read() + if (val === null) { + return; + } + var event = val[0] + var value = val[1] + if (value === null) return; + var alias = lookupAlias(event); + // emit original event & value + if (event !== alias) { + emitter.emit(event, value); + } + buildAliases(alias, event, value, aliased) + } + }) + .on('end', function () { + if (!errd) { + done() + } + }) + .on('error', function (err) { + done(err) + errd = true; + }) + hasReadData = true; - var parser = common.getParserForMediaType(headerTypes, result); - parser(istream, function (event, value) { - if (value === null) return; - var alias = lookupAlias(event); - // emit original event & value - if (event !== alias) { - emitter.emit(event, value); - } - buildAliases(alias, event, value, aliased) - }, done, opts.hasOwnProperty('duration'), fsize); - // re-emitting the first data chunk so the - // parser picks the stream up from the start - istream.emit('data', result); - }); + }) istream.on('end', function () { if (!hasReadData) { @@ -108,7 +139,6 @@ var MusicMetadata = module.exports = function (stream, opts, callback) { if (callback) { callback(exception, metadata) } - return strtok.DONE; } return emitter; diff --git a/lib/monkeysaudio.js b/lib/monkeysaudio.js index 055f595..05ee14c 100644 --- a/lib/monkeysaudio.js +++ b/lib/monkeysaudio.js @@ -1,68 +1,74 @@ 'use strict'; var common = require('./common'); -var strtok = require('strtok2'); +var through2 = require('through2'); -module.exports = function (stream, callback, done) { - var bufs = []; +module.exports = function (readDuration) { - // TODO: need to be able to parse the tag if its at the start of the file - stream.on('data', function (data) { - bufs.push(data); - }) + var bufs = [] - common.streamOnRealEnd(stream, function () { - var buffer = Buffer.concat(bufs); - var offset = buffer.length - 32; + return through2({ objectMode: true}, + function (data, encoding, callback) { + bufs.push(data) + callback() + }, + function (callback) { + var buffer = Buffer.concat(bufs) + var offset = buffer.length - 32; - if ('APETAGEX' !== buffer.toString('utf8', offset, offset += 8)) { - done(new Error('expected APE header but wasn\'t found')); - } + if ('APETAGEX' !== buffer.toString('utf8', offset, offset += 8)) { + this.emit('error', new Error('expected APE header but wasn\'t found')) + this.push(null) + return; + } - var footer = { - version: strtok.UINT32_LE.get(buffer, offset, offset + 4), - size: strtok.UINT32_LE.get(buffer, offset + 4, offset + 8), - count: strtok.UINT32_LE.get(buffer, offset + 8, offset + 12) - } + var footer = { + version: buffer.readUInt32LE(offset, offset + 4), + size: buffer.readUInt32LE(offset + 4, offset + 8), + count: buffer.readUInt32LE(offset + 8, offset + 12) + } - //go 'back' to where the 'tags' start - offset = buffer.length - footer.size; + //go 'back' to where the 'tags' start + offset = buffer.length - footer.size; - for (var i = 0; i < footer.count; i++) { - var size = strtok.UINT32_LE.get(buffer, offset, offset += 4); - var flags = strtok.UINT32_LE.get(buffer, offset, offset += 4); - var kind = (flags & 6) >> 1; + for (var i = 0; i < footer.count; i++) { + var size = buffer.readUInt32LE(offset, offset += 4); + var flags = buffer.readUInt32LE(offset, offset += 4); + var kind = (flags & 6) >> 1; - var zero = common.findZero(buffer, offset, buffer.length); - var key = buffer.toString('ascii', offset, zero); - offset = zero + 1; + var zero = common.findZero(buffer, offset, buffer.length); + var key = buffer.toString('ascii', offset, zero); + offset = zero + 1; - if (kind === 0) { // utf-8 textstring - var value = buffer.toString('utf8', offset, offset += size); - var values = value.split(/\x00/g); + if (kind === 0) { // utf-8 textstring + var value = buffer.toString('utf8', offset, offset += size); + var values = value.split(/\x00/g); - /*jshint loopfunc:true */ - values.forEach(function (val) { - callback(key, val); - }) - } else if (kind === 1) { //binary (probably artwork) - if (key === 'Cover Art (Front)' || key === 'Cover Art (Back)') { - var picData = buffer.slice(offset, offset + size); + var self = this; + /*jshint loopfunc:true */ + values.forEach(function (val) { + self.push([key, val]) + }) + } else if (kind === 1) { //binary (probably artwork) + if (key === 'Cover Art (Front)' || key === 'Cover Art (Back)') { + var picData = buffer.slice(offset, offset + size); - var off = 0; - zero = common.findZero(picData, off, picData.length); - var description = picData.toString('utf8', off, zero); - off = zero + 1; + var off = 0; + zero = common.findZero(picData, off, picData.length); + var description = picData.toString('utf8', off, zero); + off = zero + 1; - var picture = { - description: description, - data: picData.slice(off) - }; + var picture = { + description: description, + data: picData.slice(off) + }; - offset += size; - callback(key, picture); + offset += size; + this.push([key, picture]) + } } } + this.push(null) + callback() } - return done(); - }) + ) } diff --git a/lib/ogg.js b/lib/ogg.js index 0e641ac..f94cd64 100644 --- a/lib/ogg.js +++ b/lib/ogg.js @@ -1,113 +1,104 @@ 'use strict'; var fs = require('fs'); var util = require('util'); -var events = require('events'); -var strtok = require('strtok2'); +var Calippo = require('calippo'); var common = require('./common'); var sum = require('sum-component'); +var bun = require('bun'); -module.exports = function (stream, callback, done, readDuration) { - var innerStream = new events.EventEmitter(); - var pageLength = 0; +module.exports = function (readDuration) { var sampleRate = 0; - var header; - var stop = false; - - stream.on('end', function () { - if (readDuration) { - callback('duration', Math.floor(header.pcm_sample_pos / sampleRate)); - done(); - } - }) // top level parser that handles the parsing of pages - strtok.parse(stream, function (v, cb) { + var top = new Calippo(function (v) { if (!v) { - cb.state = 0; - return new strtok.BufferType(27); + this.state = 0; + return this.Buffer(27); } - if (stop) { - return done(); - } - - switch (cb.state) { + switch (this.state) { case 0: // header - header = { + this.header = { type: v.toString('ascii', 0, 4), version: v[4], packet_flag: v[5], pcm_sample_pos: (v.readUInt32LE(10) << 32) + v.readUInt32LE(6), - stream_serial_num: strtok.UINT32_LE.get(v, 14), - page_number: strtok.UINT32_LE.get(v, 18), - check_sum: strtok.UINT32_LE.get(v, 22), + stream_serial_num: v.readUInt32LE(14), + page_number: v.readUInt32LE(18), + check_sum: v.readUInt32LE(22), segments: v[26] } - if (header.type !== 'OggS') { - return done(new Error('expected ogg header but was not found')); + if (this.header.type !== 'OggS') { + this.emit('error', new Error('expected ogg header but was not found')) + return } - cb.pageNumber = header.page_number; - cb.state++; - return new strtok.BufferType(header.segments); + this.state++; + return this.Buffer(this.header.segments); case 1: // segments - pageLength = sum(v); - cb.state++; - return new strtok.BufferType(pageLength); + this.state++; + return this.Buffer(sum(v)); case 2: // page data - innerStream.emit('data', new Buffer(v)); - cb.state = 0; - return new strtok.BufferType(27); + this.push(new Buffer(v)) + this.state = 0; + return this.Buffer(27); + } + }) + .on('end', function () { + if (readDuration) { + bottom.push(['duration', Math.floor(this.header.pcm_sample_pos / sampleRate)]) } }) - // Second level parser that handles the parsing of metadata. // The top level parser emits data that this parser should // handle. - strtok.parse(innerStream, function (v, cb) { + var bottom = new Calippo({'objectMode': true}, function (v) { if (!v) { - cb.commentsRead = 0; - cb.state = 0; - return new strtok.BufferType(7); + this.commentsRead = 0; + this.state = 0; + return this.Buffer(7); } - switch (cb.state) { + switch (this.state) { case 0: // type if (v.toString() === '\x01vorbis') { - cb.state = 6; - return new strtok.BufferType(23); + this.state = 6; + return this.Buffer(23); } else if (v.toString() === '\x03vorbis') { - cb.state++; - return strtok.UINT32_LE; + this.state++; + return this.readUInt32LE; } else { - return done(new Error('expected vorbis header but found something else')); + this.emit('error', + new Error('expected vorbis header but found something else: ' + v.toString())) + return; } break; case 1: // vendor length - cb.state++; - return new strtok.StringType(v); + this.state++; + return this.String(v); case 2: // vendor string - cb.state++; - return new strtok.BufferType(4); + this.state++; + return this.readUInt32LE; case 3: // user comment list length - cb.commentsLength = v.readUInt32LE(0); + this.commentsLength = v; // no metadata, stop parsing - if (cb.commentsLength === 0) return strtok.DONE; - cb.state++; - return strtok.UINT32_LE; + if (this.commentsLength === 0) { + return; + } + this.state++; + return this.readUInt32LE; case 4: // comment length - cb.state++; - return new strtok.StringType(v); + this.state++; + return this.String(v); case 5: // comment - cb.commentsRead++; - + this.commentsRead++; var idx = v.indexOf('='); var key = v.slice(0, idx).toUpperCase(); var value = v.slice(idx+1); @@ -115,18 +106,14 @@ module.exports = function (stream, callback, done, readDuration) { if (key === 'METADATA_BLOCK_PICTURE') { value = common.readVorbisPicture(new Buffer(value, 'base64')); } + this.push([key, value]) - callback(key, value); - - if (cb.commentsRead === cb.commentsLength) { - // if we don't want to read the duration - // then tell the parent stream to stop - stop = !readDuration; - return strtok.DONE; + if (this.commentsRead === this.commentsLength) { + return } - cb.state--; // back to comment length - return strtok.UINT32_LE; + this.state--; // back to comment length + return this.readUInt32LE; case 6: // vorbis info var info = { @@ -136,8 +123,12 @@ module.exports = function (stream, callback, done, readDuration) { 'bitrate_nominal': v.readUInt32LE(13) } sampleRate = info.sample_rate; - cb.state = 0; - return new strtok.BufferType(7); + this.state = 0; + return this.Buffer(7); } }) + + return bun([top, bottom]) + + // return top.pipe(bottom, {end: false}) } diff --git a/package.json b/package.json index 6057ee7..009baa8 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,17 @@ "version": "1.0.0", "author": "Lee Treveil", "dependencies": { + "buffer-equal": "0.0.0", + "bun": "0.0.11", + "calippo": "0.0.2", "deep-equal": "0.2.1", "filereader-stream": "^0.2.0", "iconv-lite": "^0.4.4", "node-bitarray": "0.0.2", "strtok2": "~1.0.0", "sum-component": "^0.1.1", - "through": "~2.3.4" + "through": "~2.3.4", + "through2": "^0.6.3" }, "keywords": [ "id3", diff --git a/test/test-audio-frame-header-bug.js b/test/test-audio-frame-header-bug.js index c505436..2af7dfd 100644 --- a/test/test-audio-frame-header-bug.js +++ b/test/test-audio-frame-header-bug.js @@ -5,14 +5,15 @@ var fs = require('fs'); var test = require('prova'); test('audio-frame-header-bug', function (t) { - t.plan(1); + t.plan(2); var sample = (process.browser) ? new Blob([fs.readFileSync(__dirname + '/samples/audio-frame-header-bug.mp3')]) : fs.createReadStream(path.join(__dirname, '/samples/audio-frame-header-bug.mp3')) new mm(sample, { duration: true }, function (err, result) { + t.error(err) t.strictEqual(result.duration, 201); - t.end(); + t.end() }) }); diff --git a/test/test-ogg-multipagemetadatabug.js b/test/test-ogg-multipagemetadatabug.js index 3b40b42..794963c 100644 --- a/test/test-ogg-multipagemetadatabug.js +++ b/test/test-ogg-multipagemetadatabug.js @@ -4,13 +4,13 @@ var fs = require('fs'); var test = require('prova'); test('ogg-multipage-metadata-bug', function (t) { - t.plan(13); + t.plan(14); var sample = (process.browser) ? new Blob([fs.readFileSync(__dirname + '/samples/ogg-multipagemetadata-bug.ogg')]) : fs.createReadStream(path.join(__dirname, '/samples/ogg-multipagemetadata-bug.ogg')) - new mm(sample, function (err, result) { + new mm(sample, { duration: true }, function (err, result) { t.error(err); t.strictEqual(result.title, 'Modestep - To The Stars (Break the Noize & The Autobots Remix)', 'title'); @@ -25,6 +25,6 @@ test('ogg-multipage-metadata-bug', function (t) { t.strictEqual(result.genre[0], 'Dubstep', 'genre'); t.strictEqual(result.picture[0].format, 'jpg', 'picture format'); t.strictEqual(result.picture[0].data.length, 207439, 'picture length'); - t.end(); + t.strictEqual(result.duration, 6, 'duration'); }) }); \ No newline at end of file