Skip to content
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
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
language: node_js
node_js:
- '0.12'
- '8'
- 'node'
42 changes: 25 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,59 @@

> Calculate duration of an MP3


## Install

```
$ npm install --save mp3-duration
```sh
npm install mp3-duration
```

## Usage

1. JavaScript

```javascript
var mp3Duration = require('mp3-duration');

mp3Duration('file.mp3', function (err, duration) {
if (err) return console.log(err.message);
console.log('Your file is ' + duration + ' seconds long');
mp3Duration('file.mp3').then(duration => {
console.log(`Your file is ${duration} seconds long`);
}).catch(e => {
console.err(e);
});
```

2. TypeScript

```typescript
import mp3Duration = require('mp3-duration');

mp3Duration('file.mp3').then(duration => {
console.log(`Your file is ${duration} seconds long`);
}).catch(e => {
console.err(e);
});
```

## API

## mp3Duration(filePathOrBuffer [, cbrEstimate] [, callback])
## mp3Duration(filePathOrBuffer [, cbrEstimate])

### filePathOrBuffer

Type: `string` | `Buffer`
Type: `string | Buffer`

Path to the file or a buffer with the file's contents

### cbrEstimate

Type: `boolean`

Defaults to `false`. When set to `true`, will estimate the length of a
constant-bitrate mp3. This speeds up the calculation a lot but isn't
guaranteed to be accurate.

### callback(error, duration)

Type: `function`

Callback to be called once duration is calculated. It's also possible to
instead use the returned `Promise`. `duration` is the duration of the
mp3 in `ms`.

### Return value

`mp3Duration` returns a Promise that resolves to the duration of the mp3 in `ms` or rejects with some error.
`mp3Duration` returns a Promise that resolves to the duration of the mp3 in `second` or rejects with some error.

## License

Expand Down
4 changes: 4 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare function mp3Duration(filePathOrBuffer: string | Buffer , cbrEstimate?: boolean): Promise<number>;
declare function mp3Duration(filePathOrBuffer: string | Buffer): Promise<number>;

export = mp3Duration;
126 changes: 59 additions & 67 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
'use strict';

var fs = require('fs')
, Promise = require('bluebird')
, co = require('bluebird-co').co
const fs = require('fs');
const { promisify } = require('util');

, open = Promise.promisify(fs.open) //(filename, flags [, mode])
, read = Promise.promisify(fs.read).bind(fs) //(fd, buffer, bufferOffset, length, position)
, fstat = Promise.promisify(fs.fstat).bind(fs) //(fs)
, close = Promise.promisify(fs.close).bind(fs);
const open = promisify(fs.open);
const read = promisify(fs.read);
const fstat = promisify(fs.fstat);
const close = promisify(fs.close);

function skipId3(buffer) {
var id3v2Flags
Expand Down Expand Up @@ -36,7 +35,7 @@ function skipId3(buffer) {
return 0;
}

var versions = ['2.5', 'x', '2', '1']
const versions = ['2.5', 'x', '2', '1']
, layers = ['x', '3', '2', '1']
, bitRates = {
'V1Lx' : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
Expand Down Expand Up @@ -142,83 +141,76 @@ function round(duration) {
return Math.round(duration * 1000) / 1000; //round to nearest ms
}

function mp3Duration(filename, cbrEstimate, callback) {
if (typeof cbrEstimate === 'function') {
callback = cbrEstimate;
cbrEstimate = false;
async function mp3Duration(filename, cbrEstimate = false) {
var duration = 0
, fd
, buffer
, bytesRead
, offset
, stat
, info
, srcBuffer
, isBuffer = false;

if (typeof filename === 'string') {
fd = await open(filename, 'r');
} else if (filename instanceof Buffer) {
srcBuffer = filename;
isBuffer = true;
}
return co(function* () {
var duration = 0
, fd
, buffer
, bytesRead
, offset
, stat
, info
, srcBuffer
, isBuffer = false;

if (typeof filename === 'string') {
fd = yield open(filename, 'r');
} else if (filename instanceof Buffer) {
srcBuffer = filename;
isBuffer = true;

try {
if (!isBuffer) {
stat = await fstat(fd);
}

try {
if (!isBuffer) {
stat = yield fstat(fd);
}
buffer = Buffer.alloc(100);

if (!isBuffer) {
bytesRead = await read(fd, buffer, 0, 100, 0);
} else {
bytesRead = srcBuffer.copy(buffer, 0, 0, 100);
}
if (bytesRead < 100) return 0;

buffer = new Buffer(100);
offset = skipId3(buffer);

while (offset < (isBuffer ? srcBuffer.length : stat.size)) {
if (!isBuffer) {
bytesRead = yield read(fd, buffer, 0, 100, 0);
bytesRead = await read(fd, buffer, 0, 10, offset);
} else {
bytesRead = srcBuffer.copy(buffer, 0, 0, 100);
bytesRead = srcBuffer.copy(buffer, 0, offset, offset + 10);
}
if (bytesRead < 100) return 0;

offset = skipId3(buffer);

while (offset < (isBuffer ? srcBuffer.length : stat.size)) {
if (!isBuffer) {
bytesRead = yield read(fd, buffer, 0, 10, offset);
} else {
bytesRead = srcBuffer.copy(buffer, 0, offset, offset + 10);
}
if (bytesRead < 10) return round(duration);

if (bytesRead < 10) return round(duration);

//Looking for 1111 1111 111 (frame synchronization bits)
if (buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0) {
info = parseFrameHeader(buffer);
if (info.frameSize && info.samples) {
offset += info.frameSize;
duration += ( info.samples / info.sampleRate );
} else {
offset++; //Corrupt file?
}
} else if (buffer[0] === 0x54 && buffer[1] === 0x41 && buffer[2] === 0x47) {//'TAG'
offset += 128; //Skip over id3v1 tag size
//Looking for 1111 1111 111 (frame synchronization bits)
if (buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0) {
info = parseFrameHeader(buffer);
if (info.frameSize && info.samples) {
offset += info.frameSize;
duration += ( info.samples / info.sampleRate );
} else {
offset++; //Corrupt file?
}

if (cbrEstimate && info) {
return round(estimateDuration(info.bitRate, offset, stat.size));
}
} else if (buffer[0] === 0x54 && buffer[1] === 0x41 && buffer[2] === 0x47) {//'TAG'
offset += 128; //Skip over id3v1 tag size
} else {
offset++; //Corrupt file?
}

return round(duration);

} finally {
if (!isBuffer) {
yield close(fd);
if (cbrEstimate && info) {
return round(estimateDuration(info.bitRate, offset, stat.size));
}
}

}).asCallback(callback);
return round(duration);

} finally {
if (!isBuffer) {
await close(fd);
}
}
}

module.exports = mp3Duration;
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"url": "git://github.com/ddsol/mp3-duration"
},
"engines": {
"node": ">=0.11.0"
"node": ">=8.0.0"
},
"keywords": [
"mp3",
Expand All @@ -27,11 +27,10 @@
},
"homepage": "https://github.com/ddsol/mp3-duration",
"dependencies": {
"bluebird": "^3.5.1",
"bluebird-co": "^2.2.0"
"@types/node": ">=8.0.0"
},
"devDependencies": {
"mocha": "^2.2.4",
"mocha": "^5.2.0",
"nyc": "^11.2.1"
}
}
51 changes: 17 additions & 34 deletions tests/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,35 @@ var assert = require('assert'),
mp3Duration = require('../index.js'),
fs = require('fs');

describe('mp3Duration', function() {

it('returns a correct value for VBR duration', function(done) {
mp3Duration('./tests/demo - vbr.mp3', function(err, length) {
if (err) console.log(err.stack);
assert.equal(err, null, (err || {}).message);
assert.equal(length, 285.727, 'Length not as expected');
done();
});
describe('mp3Duration', function () {

it('returns a correct value for VBR duration', async () => {
const length = await mp3Duration('./tests/demo - vbr.mp3');
assert.equal(length, 285.727, 'Length not as expected');
});

it('returns a correct value for CBR duration with estimate', function(done) {
mp3Duration('./tests/demo - cbr.mp3', true, function(err, length) {
assert.equal(err, null, (err || {}).message);
assert.equal(length, 285.727, 'Length not as expected');
done();
});
it('returns a correct value for CBR duration with estimate', async () => {
const length = await mp3Duration('./tests/demo - cbr.mp3', true);
assert.equal(length, 285.727, 'Length not as expected');
});

it('returns a correct value for CBR duration without estimate', function(done) {
mp3Duration('./tests/demo - cbr.mp3', function(err, length) {
assert.equal(err, null, (err || {}).message);
assert.equal(length, 285.78, 'Length not as expected');
done();
});
it('returns a correct value for CBR duration without estimate', async () => {
const length = await mp3Duration('./tests/demo - cbr.mp3');
assert.equal(length, 285.78, 'Length not as expected');
});

it('returns a correct value for VBR duration when passing a buffer instead of a filename', function(done) {
it('returns a correct value for VBR duration when passing a buffer instead of a filename', async () => {
const buffer = fs.readFileSync('./tests/demo - vbr.mp3');

mp3Duration(buffer, function(err, length) {
if (err) console.log(err.stack);
assert.equal(err, null, (err || {}).message);
assert.equal(length, 285.727, 'Length not as expected');
done();
});
const length = await mp3Duration(buffer);
assert.equal(length, 285.727, 'Length not as expected');
});

it('return a correct value for CBR duration without estimate when passing a buffer instead of a filename', function(done) {
it('return a correct value for CBR duration without estimate when passing a buffer instead of a filename', async () => {
const buffer = fs.readFileSync('./tests/demo - cbr.mp3');

mp3Duration(buffer, function(err, length) {
assert.equal(err, null, (err || {}).message);
assert.equal(length, 285.78, 'Length not as expected');
done();
});
const length = await mp3Duration(buffer);
assert.equal(length, 285.78, 'Length not as expected');
});

});