Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support files with id3v2 tags #4

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
48 changes: 36 additions & 12 deletions fixtures/file_metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -130,6 +153,7 @@ module.exports = [
xing_offset: 36,
xing_frames: 241,
xing_bytes: 77751,
num_samples: 1152
num_samples: 1152,
id3v2_offset: 0
}
];
Binary file not shown.
41 changes: 39 additions & 2 deletions src/Mp3Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -43,6 +61,7 @@ module.exports = class Mp3Header {
this.parsed = false;
this.header = null;
this.is_valid = false;
this.id3v2_offset = 0;

this._parse();
}
Expand All @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions src/Mp3Header_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand All @@ -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() {
Expand All @@ -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();
});
Expand Down
2 changes: 1 addition & 1 deletion src/XingHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions src/XingHeader_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand All @@ -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() {
Expand Down