From 06576957b93dda4c23645427b660bed9e843c2f6 Mon Sep 17 00:00:00 2001 From: Lee Treveil Date: Wed, 3 Dec 2014 14:19:10 +0000 Subject: [PATCH] WIP --- lib/asf.js | 82 +++++++------ lib/flac.js | 56 +++++---- lib/id3v1.js | 66 ++++++----- lib/id3v2.js | 137 ++++++++++++--------- lib/id4.js | 83 ++++++------- lib/index.js | 59 +++++++-- lib/monkeysaudio.js | 104 ++++++++-------- lib/ogg.js | 164 ++++++++++++++------------ package.json | 1 + test/test-audio-frame-header-bug.js | 7 +- test/test-flac.js | 1 + test/test-id4.js | 3 +- test/test-no-metadata.js | 2 + test/test-ogg-multipagemetadatabug.js | 5 +- 14 files changed, 433 insertions(+), 337 deletions(-) diff --git a/lib/asf.js b/lib/asf.js index 828584c..4afd2cb 100644 --- a/lib/asf.js +++ b/lib/asf.js @@ -1,46 +1,43 @@ -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 Calippo(function (v) { + currentState = currentState.parse(this, v); + return currentState.getExpectedType(this); + }, {'objectMode': true}) }; 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); } }; @@ -49,7 +46,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; @@ -59,21 +56,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); } }; @@ -82,13 +79,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) { @@ -97,7 +94,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), @@ -112,7 +109,7 @@ 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; } @@ -120,8 +117,8 @@ ContentDescriptionObjectState.prototype.parse = function(callback, data, done) { 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([ @@ -144,7 +141,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) { @@ -160,18 +157,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([ @@ -184,17 +182,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 dad633d..d8829fb 100644 --- a/lib/flac.js +++ b/lib/flac.js @@ -1,13 +1,13 @@ -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 Calippo(function (v) { + currentState = currentState.parse(this, v); + return currentState.getExpectedType(this); + }, {'objectMode': true}) } var DataDecoder = function (data) { @@ -16,7 +16,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; } @@ -29,11 +29,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) } } @@ -43,7 +43,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(); @@ -55,35 +55,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; @@ -91,28 +89,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 b76471c..0694160 100644 --- a/lib/id3v1.js +++ b/lib/id3v1.js @@ -1,40 +1,48 @@ -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')); + } - 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 8b601b3..98151ef 100644 --- a/lib/id3v2.js +++ b/lib/id3v2.js @@ -1,27 +1,32 @@ -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'); -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 Calippo(function (v) { + + var seekFirstAudioFrame = _seekFirstAudioFrame.bind(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), @@ -30,30 +35,33 @@ 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, callback); + parseMetadata.apply(this, [v]); if (readDuration) { - cb.state = 2; - return new strtok.BufferType(4); + this.state = 2; + return this.Buffer(4); } - return done(); + console.log('pushing null...') + 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); @@ -113,77 +121,90 @@ 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); } + var self = this; + // 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; - } + console.log(Math.round(size / kbps)) + self.push(['duration', Math.round(size / kbps)]) + // // cb(done()); + // // TODO: might fail - // 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() + console.log('defering!...') + // self.defer() }) + return this.DEFER; } - cb.state = 5; - return new strtok.BufferType(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) + // }) + // } + + this.state = 5; + return this.Buffer(header.frame_size - 4); 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)]) + console.log('pushing nullzzzz...') + 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() { + function _seekFirstAudioFrame() { if (frameCount) { - return done(new Error('expected frame header but was not found')); + this.emit('error', new Error('expected frame header but was not found')) + console.log('pushing null...') + this.push(null) } - cb.frameFragment = v; - cb.state = 1.5; - return new strtok.BufferType(1); + this.frameFragment = v; + this.state = 1.5; + return this.Buffer(1); } - }); + }, {'objectMode': true}); }; function areAllSame (array) { @@ -197,9 +218,12 @@ function calcDuration (numFrames, samplesPerFrame, sampleRate) { return Math.round(numFrames * (samplesPerFrame / sampleRate)); } -function parseMetadata (data, header, callback) { +function parseMetadata (data) { var offset = 0; + var header = this.id3Header; + + // console.log(this) if (header.xheader) { offset += data.readUInt32BE(0); } @@ -219,8 +243,9 @@ function parseMetadata (data, header, callback) { var frameData = readFrameData(frameDataBytes, frameHeader, header.major); /*jshint loopfunc:true */ + var self = this; frameData.forEach(function (val) { - callback(frameHeader.id, val); + self.push([frameHeader.id, val]); }) } } @@ -250,7 +275,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: diff --git a/lib/id4.js b/lib/id4.js index 0e746a2..836b09b 100644 --- a/lib/id4.js +++ b/lib/id4.js @@ -1,81 +1,85 @@ -var strtok = require('strtok2'); +var Calippo = require('calippo'); var common = require('./common'); -module.exports = function (stream, callback, done, readDuration) { - strtok.parse(stream, function (v, cb) { +module.exports = function (readDuration) { + + return Calippo(function (v) { // 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 + // TODO: do we send eof before we've read the duration or after?? } // 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]]) } } - 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 + }, {'objectMode': true}) } function processMetaAtom (data, atomName, atomLength) { @@ -86,8 +90,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); @@ -104,14 +108,13 @@ function processMetaAtom (data, atomName, atomLength) { case 'uint8': if (atomName === 'gnre') { - var genreInt = strtok.UINT16_BE.get(data, 4); + var genreInt = data.readUInt8(5); return common.GENRES[genreInt - 1]; } if (atomName === 'trkn' || atomName === 'disk') { return data[7] + '/' + data[9]; } - - return strtok.UINT16_BE.get(data, 4); + return data.readUInt8(4); case 'jpeg': case 'png': diff --git a/lib/index.js b/lib/index.js index d00ebb3..4a59145 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,10 +1,12 @@ var util = require('util'); var events = require('events'); var common = require('./common'); -var strtok = require('strtok2'); -var through = require('through') +var PassThrough = require('stream').PassThrough; +var domain = require('domain') var fs = require('fs') +var cl = console.log + var MusicMetadata = module.exports = function (stream, opts) { if (!(this instanceof MusicMetadata)) return new MusicMetadata(stream, opts); opts = opts || {}; @@ -31,7 +33,7 @@ var MusicMetadata = module.exports = function (stream, opts) { // pipe to an internal stream so we aren't fucking // with the stream passed to us by our users - this.stream = stream.pipe(through(null, null, {autoDestroy: false})); + this.stream = stream; this.metadata = { title: '', @@ -48,20 +50,41 @@ var MusicMetadata = module.exports = function (stream, opts) { this.aliased = {}; - var hasReadData = false; - this.stream.once('data', function (result) { + this.stream.once('readable', function () { + var data = self.stream.read(20); // TODO: what if null?? + self.stream.unshift(data); + var parser = common.getParserForMediaType(headerTypes, data); + + var errd = false; + + self.stream.pipe(parser(opts.hasOwnProperty('duration'), fsize)) + .on('readable', function () { + var val; + while ((val = this.read()) !== null) { + self.readEvent.apply(self, val) + } + }) + .on('end', function () { + if (!errd) { + done() + } + }) + .on('error', function (err) { + // console.log('errz', err) + // self.readEvent('done', err.message) + // TODO: why err.message and not just err? won't we lose the stacktrace?? + done(err.message) + errd = true; + }) + hasReadData = true; - var parser = common.getParserForMediaType(headerTypes, result); - parser(self.stream, self.readEvent.bind(self), done, - opts.hasOwnProperty('duration'), fsize); - // re-emitting the first data chunk so the - // parser picks the stream up from the start - self.stream.emit('data', result); - }); + }) this.stream.on('end', function () { + console.log('in end') if (!hasReadData) { + console.log('calling done in end') done(new Error('Could not read any data from this stream')) } }) @@ -69,13 +92,14 @@ var MusicMetadata = module.exports = function (stream, opts) { this.stream.on('close', onClose); function onClose () { + console.log('in close') done(new Error('Unexpected end of stream')); } function done (exception) { + console.log('in done') self.stream.removeListener('close', onClose); self.readEvent('done', exception); - return strtok.DONE; } events.EventEmitter.call(this); @@ -84,6 +108,7 @@ var MusicMetadata = module.exports = function (stream, opts) { util.inherits(MusicMetadata, events.EventEmitter); MusicMetadata.prototype.readEvent = function (event, value) { + console.log(event, '---', value) // We only emit aliased events once the 'done' event has been raised, // this is because an alias like 'artist' could have values split // over many data chunks. @@ -174,6 +199,7 @@ MusicMetadata.prototype.readEvent = function (event, value) { } function lookupAlias (event) { + // console.log(event) var alias; for (var i = 0; i < MAPPINGS.length; i++) { for (var j = 0; j < MAPPINGS[i].length; j++) { @@ -210,6 +236,13 @@ function cleanupPicture (picture) { return { format: newFormat, data: picture.data } } +var headerTypes = [ + { + buf: new Buffer('OggS'), + tag: require('./ogg'), + } +]; + var headerTypes = [ { buf: common.asfGuidBuf, diff --git a/lib/monkeysaudio.js b/lib/monkeysaudio.js index 46b8c9c..09739e0 100644 --- a/lib/monkeysaudio.js +++ b/lib/monkeysaudio.js @@ -1,67 +1,73 @@ 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 f808fb9..e51b867 100644 --- a/lib/ogg.js +++ b/lib/ogg.js @@ -1,112 +1,126 @@ 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'); -module.exports = function (stream, callback, done, readDuration) { - var innerStream = new events.EventEmitter(); - - var pageLength = 0; +var cl = console.log + + + // domain.create() + // .on('error', function (err) { + // console.log(err) + // self.readEvent('done', err.message); + // }) + // .run(function () { + // parser(self.stream, opts.hasOwnProperty('duration'), fsize) + // .on('readable', function () { + // // need while loop... + // var val; + // while ((val = this.read()) !== null) { + // self.readEvent.apply(self, val) + // } + + // }) + // .on('end', function () { + // self.readEvent('done') + // }) + // }) + // cl(parser.toString()) + +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 = Calippo(function (v) { if (!v) { - cb.state = 0; - return new strtok.BufferType(27); - } - - if (stop) { - return done(); + this.state = 0; + return this.Buffer(27); } - 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)]) + bottom.push(null) + this.push(null) } }) - // 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 = Calippo(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')) + 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); @@ -114,18 +128,18 @@ module.exports = function (stream, callback, done, readDuration) { if (key === 'METADATA_BLOCK_PICTURE') { value = common.readVorbisPicture(new Buffer(value, 'base64')); } - - 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; + cl(key, value) + this.push([key, value]) + + if (this.commentsRead === this.commentsLength) { + if (!readDuration) { + this.push(null) + } + 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 = { @@ -135,8 +149,10 @@ 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); } - }) + }, {'objectMode': true}) + + return top.pipe(bottom) } diff --git a/package.json b/package.json index 0d5316e..23be1d5 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "version": "0.6.0", "author": "Lee Treveil", "dependencies": { + "buffer-equal": "0.0.0", "deep-equal": "0.2.1", "filereader-stream": "0.0.1", "iconv-lite": "^0.4.4", diff --git a/test/test-audio-frame-header-bug.js b/test/test-audio-frame-header-bug.js index ff3bea5..5ea5672 100644 --- a/test/test-audio-frame-header-bug.js +++ b/test/test-audio-frame-header-bug.js @@ -5,7 +5,7 @@ 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')]) @@ -14,6 +14,9 @@ test('audio-frame-header-bug', function (t) { new mm(sample, { duration: true }) .on('metadata', function (result) { t.strictEqual(result.duration, 201); - t.end(); + }) + .on('done', function (err) { + t.error(err) + t.end() }) }); diff --git a/test/test-flac.js b/test/test-flac.js index 7324363..a79de86 100644 --- a/test/test-flac.js +++ b/test/test-flac.js @@ -12,6 +12,7 @@ test('flac', function (t) { new mm(sample) .on('metadata', function (result) { + console.log(result) t.strictEqual(result.title, 'Brian Eno', 'title'); t.strictEqual(result.artist[0], 'MGMT', 'artist'); t.strictEqual(result.albumartist.length, 0, 'albumartist length'); diff --git a/test/test-id4.js b/test/test-id4.js index e0f27e3..7e6435f 100644 --- a/test/test-id4.js +++ b/test/test-id4.js @@ -12,6 +12,7 @@ test('id4', function (t) { new id3(sample, { duration: true }) .on('metadata', function (result) { + // console.log(result) t.strictEqual(result.title, 'Voodoo People (Pendulum Remix)', 'title'); t.strictEqual(result.artist[0], 'The Prodigy', 'artist'); t.strictEqual(result.albumartist[0], 'Pendulum', 'albumartist'); @@ -81,7 +82,7 @@ test('id4', function (t) { t.strictEqual(result, 'Electronic', 'raw gnre'); }) .on('stik', function (result) { - t.strictEqual(result, 256, 'raw stik'); + t.strictEqual(result, 1, 'raw stik'); }) .on('©alb', function (result) { t.strictEqual(result, 'Voodoo People', 'raw ©alb'); diff --git a/test/test-no-metadata.js b/test/test-no-metadata.js index 36cc06e..4d10270 100644 --- a/test/test-no-metadata.js +++ b/test/test-no-metadata.js @@ -6,6 +6,8 @@ var test = require('prova'); test('shouldn\'t raise metadata event for files that can\'t be parsed', function (t) { t.plan(1); + console.log('ZEFILE', __filename) + var sample = (process.browser) ? new Blob([fs.readFileSync(__filename)]) : fs.createReadStream(path.join(__filename)) diff --git a/test/test-ogg-multipagemetadatabug.js b/test/test-ogg-multipagemetadatabug.js index 0511d10..29a2a36 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) + new mm(sample, { duration: true }) .on('metadata', function (result) { t.strictEqual(result.title, 'Modestep - To The Stars (Break the Noize & The Autobots Remix)', 'title'); @@ -25,6 +25,7 @@ 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.strictEqual(result.duration, 6, 'duration'); }) .once('done', function (err) { t.error(err);