diff --git a/README.md b/README.md index dca4faf..cae490d 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Once you parse a header, these are the properties you can access from the packag | -------------------------- | -------- | ------------ | --------- | | `parsed` | Boolean | Both | `true` | | `is_valid` | Boolean | Both | `true` | +| `id3v2_offset` | Integer | Both | `42005` | | `mpeg_version` | Integer | Both | `2` | | `mpeg_layer` | Integer | Both | `3` | | `mpeg_has_padding` | Boolean | Both | `true` | diff --git a/fixtures/file_metadata.js b/fixtures/file_metadata.js index 3a16c57..fcd558d 100644 --- a/fixtures/file_metadata.js +++ b/fixtures/file_metadata.js @@ -9,7 +9,8 @@ module.exports = [ xing_offset: 21, xing_frames: 223, xing_bytes: 43008, - num_samples: 576 + num_samples: 576, + id3v2_offset: 0 },{ filename: "mp3-cbr-stereo-32000khz-64kbps.mp3", type: "cbr", @@ -20,7 +21,8 @@ module.exports = [ xing_offset: 36, xing_frames: 146, xing_bytes: 42336, - num_samples: 1152 + num_samples: 1152, + id3v2_offset: 0 },{ filename: "mp3-cbr-mono-44100khz-64kbps.mp3", type: "cbr", @@ -31,7 +33,8 @@ module.exports = [ xing_offset: 21, xing_frames: 222, xing_bytes: 46601, - num_samples: 1152 + num_samples: 1152, + id3v2_offset: 0 },{ filename: "mp3-cbr-mono-44100khz-128kbps.mp3", type: "cbr", @@ -42,7 +45,8 @@ module.exports = [ xing_offset: 21, xing_frames: 238, xing_bytes: 99891, - num_samples: 1152 + num_samples: 1152, + id3v2_offset: 0 },{ filename: "mp3-cbr-stereo-44100khz-128kbps.mp3", type: "cbr", @@ -53,7 +57,8 @@ module.exports = [ xing_offset: 36, xing_frames: 243, xing_bytes: 101981, - num_samples: 1152 + num_samples: 1152, + id3v2_offset: 0 },{ filename: "mp3-cbr-stereo-44100khz-192kbps.mp3", type: "cbr", @@ -64,7 +69,20 @@ module.exports = [ xing_offset: 36, xing_frames: 247, xing_bytes: 155479, - num_samples: 1152 + num_samples: 1152, + id3v2_offset: 0 + },{ + filename: "mp3-cbr-stereo-44100khz-192kbps-id3v2.mp3", + type: "cbr", + channels: 2, + samplerate: 44100, + bitrate: 192, + frame_length: 626, + xing_offset: 42041, + xing_frames: 247, + xing_bytes: 155479, + num_samples: 1152, + id3v2_offset: 42005 },{ filename: "mp3-cbr-mono-48000khz-64kbps.mp3", type: "cbr", @@ -75,7 +93,8 @@ module.exports = [ xing_offset: 21, xing_frames: 215, xing_bytes: 41472, - num_samples: 1152 + num_samples: 1152, + id3v2_offset: 0 },{ filename: "mp3-cbr-stereo-48000khz-128kbps.mp3", type: "cbr", @@ -86,7 +105,8 @@ module.exports = [ xing_offset: 36, xing_frames: 237, xing_bytes: 91392, - num_samples: 1152 + num_samples: 1152, + id3v2_offset: 0 },{ filename: "mp3-vbr-mono-22050khz-64kbps.mp3", type: "vbr", @@ -97,7 +117,8 @@ module.exports = [ xing_offset: 13, xing_frames: 205, xing_bytes: 22813, - num_samples: 576 + num_samples: 576, + id3v2_offset: 0 },{ filename: "mp3-vbr-stereo-22050khz-64kbps.mp3", type: "vbr", @@ -108,7 +129,8 @@ module.exports = [ xing_offset: 21, xing_frames: 215, xing_bytes: 50342, - num_samples: 576 + num_samples: 576, + id3v2_offset: 0 },{ filename: "mp3-vbr-mono-44100khz-128kbps.mp3", type: "vbr", @@ -119,7 +141,8 @@ module.exports = [ xing_offset: 21, xing_frames: 235, xing_bytes: 98295, - num_samples: 1152 + num_samples: 1152, + id3v2_offset: 0 },{ filename: "mp3-vbr-stereo-44100khz-128kbps.mp3", type: "vbr", @@ -130,6 +153,7 @@ module.exports = [ xing_offset: 36, xing_frames: 241, xing_bytes: 77751, - num_samples: 1152 + num_samples: 1152, + id3v2_offset: 0 } ]; \ No newline at end of file diff --git a/fixtures/mp3-cbr-stereo-44100khz-192kbps-id3v2.mp3 b/fixtures/mp3-cbr-stereo-44100khz-192kbps-id3v2.mp3 new file mode 100644 index 0000000..41c16cb Binary files /dev/null and b/fixtures/mp3-cbr-stereo-44100khz-192kbps-id3v2.mp3 differ diff --git a/src/Mp3Header.js b/src/Mp3Header.js index 34a7090..2bb08a4 100644 --- a/src/Mp3Header.js +++ b/src/Mp3Header.js @@ -35,6 +35,24 @@ const SAMPLES_PER_FRAME = { 2: {0: 0, 1: 384, 2: 1152, 3: 576} } +// https://en.wikipedia.org/wiki/Synchsafe +function _unsynchsafe(int) { + var out = 0, mask = 0x7F000000; + while (mask) { + out >>= 1; + out |= int & mask; + mask >>= 8; + } + return out; +} + +function _isKthBitSet(n, k) { + if (n & (1 << (k - 1))) { + return true; + } + return false; +} + module.exports = class Mp3Header { constructor(buffer) { @@ -43,6 +61,7 @@ module.exports = class Mp3Header { this.parsed = false; this.header = null; this.is_valid = false; + this.id3v2_offset = 0; this._parse(); } @@ -53,15 +72,33 @@ module.exports = class Mp3Header { return; } + // Not enough data to check for id3v2 + if (this.buffer.length < 3) { + return; + } + + // http://fileformats.archiveteam.org/wiki/ID3#How_to_skip_past_an_ID3v2_segment + var maybe_id3 = this.buffer.toString("ascii", 0, 3); + if (maybe_id3 == "ID3") { + // Decode bytes 6-9 as a 32-bit "synchsafe int" (refer to any ID3v2 spec). + var synchsafe = this.buffer.readInt32BE(6); + var synchsafe_decoded = _unsynchsafe(synchsafe); + this.id3v2_offset = 10 + synchsafe_decoded; + // If the 0x10 bit of byte 5 is set, let OFFSET = OFFSET + 10 (for the footer). + if (_isKthBitSet(this.buffer.readUInt8(5), 2)) { + this.id3v2_offset += 10; + } + } + // Not enough data to read the header - if (this.buffer.length < 4) { + if (this.buffer.length < this.id3v2_offset + 4) { return; } this.parsed = true; // Read the first 4 bytes - var header = [this.buffer.readUInt8(0), this.buffer.readUInt8(1), this.buffer.readUInt8(2), this.buffer.readUInt8(3)]; + var header = [this.buffer.readUInt8(this.id3v2_offset + 0), this.buffer.readUInt8(this.id3v2_offset + 1), this.buffer.readUInt8(this.id3v2_offset + 2), this.buffer.readUInt8(this.id3v2_offset + 3)]; this.is_valid = this._isMpegHeader(header); if (!this.is_valid) { return; diff --git a/src/Mp3Header_spec.js b/src/Mp3Header_spec.js index 20ab900..3273b46 100644 --- a/src/Mp3Header_spec.js +++ b/src/Mp3Header_spec.js @@ -6,6 +6,7 @@ describe("MP3Header Parsing", function() { const fixtures_path = "/../fixtures"; const expected_data = require(`./${fixtures_path}/file_metadata`); const MAX_FRAME_LENGTH = 2881; // Theoretical max mp3 frame length + const NUMBER_OF_BYTES = 65536 + MAX_FRAME_LENGTH; // Include 64k bytes for id3v2 header for (const data of expected_data) { @@ -20,10 +21,10 @@ describe("MP3Header Parsing", function() { return; } - var buffer = new Buffer(MAX_FRAME_LENGTH); + var buffer = new Buffer(NUMBER_OF_BYTES); var offset = 0; - fs.read(fd, buffer, 0, MAX_FRAME_LENGTH, offset, function(err, bytesRead, buffer) { + fs.read(fd, buffer, 0, NUMBER_OF_BYTES, offset, function(err, bytesRead, buffer) { if (err) { return fs.close(fd, function() { @@ -46,7 +47,7 @@ describe("MP3Header Parsing", function() { expect(header.mpeg_has_padding).toBeFalsy(); expect(header.mpeg_num_samples).toBe(data.num_samples); expect(header.mpeg_bitrate).toBe(data.bitrate*1000); - + expect(header.id3v2_offset).toBe(data.id3v2_offset); fs.close(fd, function() { done(); }); diff --git a/src/XingHeader.js b/src/XingHeader.js index 47b9f32..8eaa5b4 100644 --- a/src/XingHeader.js +++ b/src/XingHeader.js @@ -46,7 +46,7 @@ module.exports = class XingHeader { this.is_valid = false; // If this header don't contains a Xing/Info tag, nothing to do - this.xing_offset = this.mp3.header.length + XING_OFFSETS[this.mp3.mpeg_version][this.mp3.mpeg_channels]; + this.xing_offset = this.mp3.id3v2_offset + this.mp3.header.length + XING_OFFSETS[this.mp3.mpeg_version][this.mp3.mpeg_channels]; this.xing_keyword = this.mp3.buffer.toString("ascii", this.xing_offset, this.xing_offset + 4); if (this.xing_keyword !== "Xing" && this.xing_keyword != "Info") { return; diff --git a/src/XingHeader_spec.js b/src/XingHeader_spec.js index 74fa609..2067c78 100644 --- a/src/XingHeader_spec.js +++ b/src/XingHeader_spec.js @@ -6,6 +6,7 @@ describe("XingHeader Parsing", function() { const fixtures_path = "/../fixtures"; const expected_data = require(`./${fixtures_path}/file_metadata`); const MAX_FRAME_LENGTH = 2881; // Theoretical max mp3 frame length + const NUMBER_OF_BYTES = 65536 + MAX_FRAME_LENGTH; // Include 64k bytes for id3v2 header for (const data of expected_data) { @@ -22,10 +23,10 @@ describe("XingHeader Parsing", function() { return; } - var buffer = new Buffer(MAX_FRAME_LENGTH); + var buffer = new Buffer(NUMBER_OF_BYTES); var offset = 0; - fs.read(fd, buffer, 0, MAX_FRAME_LENGTH, offset, function(err, bytesRead, buffer) { + fs.read(fd, buffer, 0, NUMBER_OF_BYTES, offset, function(err, bytesRead, buffer) { if (err) { return fs.close(fd, function() {