305 lines
7.3 KiB
JavaScript
305 lines
7.3 KiB
JavaScript
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var assert = require('assert');
|
|
var binding = require('./bindings');
|
|
var inherits = require('util').inherits;
|
|
var Transform = require('readable-stream/transform');
|
|
var debug = require('debug')('lame:encoder');
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = Encoder;
|
|
|
|
/**
|
|
* Constants.
|
|
*/
|
|
|
|
var LAME_OKAY = binding.LAME_OKAY;
|
|
var LAME_BADBITRATE = binding.LAME_BADBITRATE;
|
|
var LAME_BADSAMPFREQ = binding.LAME_BADSAMPFREQ;
|
|
var LAME_INTERNALERROR = binding.LAME_INTERNALERROR;
|
|
|
|
var PCM_TYPE_SHORT_INT = binding.PCM_TYPE_SHORT_INT;
|
|
var PCM_TYPE_FLOAT = binding.PCM_TYPE_FLOAT;
|
|
var PCM_TYPE_DOUBLE = binding.PCM_TYPE_DOUBLE;
|
|
|
|
/**
|
|
* Messages for error codes returned from the lame C encoding functions.
|
|
*/
|
|
|
|
var ERRORS = {
|
|
'-1': 'output buffer too small',
|
|
'-2': 'malloc() problems',
|
|
'-3': 'lame_init_params() not called',
|
|
'-4': 'psycho acoustic problems'
|
|
};
|
|
|
|
/**
|
|
* Map of libmp3lame functions to node-lame property names.
|
|
*/
|
|
|
|
var PROPS = {
|
|
'brate': 'bitRate',
|
|
'num_channels': 'channels',
|
|
'bWriteVbrTag': 'writeVbrTag',
|
|
'in_samplerate': 'sampleRate',
|
|
'out_samplerate': 'outSampleRate'
|
|
};
|
|
|
|
/**
|
|
* The valid bit depths that lame supports encoding in.
|
|
*/
|
|
|
|
var INT_BITS = binding.sizeof_int * 8;
|
|
var SHORT_BITS = binding.sizeof_short * 8;
|
|
var FLOAT_BITS = binding.sizeof_float * 8;
|
|
var DOUBLE_BITS = binding.sizeof_double * 8;
|
|
|
|
/**
|
|
* The `Encoder` class is a Transform stream class.
|
|
* Write raw PCM data, out comes an MP3 file.
|
|
*
|
|
* @param {Object} opts PCM stream format info and stream options
|
|
* @api public
|
|
*/
|
|
|
|
function Encoder (opts) {
|
|
if (!(this instanceof Encoder)) {
|
|
return new Encoder(opts);
|
|
}
|
|
Transform.call(this, opts);
|
|
|
|
// lame malloc()s the "gfp" buffer
|
|
this.gfp = binding.lame_init();
|
|
|
|
// set default options
|
|
if (!opts) opts = {};
|
|
if (null == opts.channels) opts.channels = 2;
|
|
if (null == opts.bitDepth) opts.bitDepth = 16;
|
|
if (null == opts.sampleRate) opts.sampleRate = 44100;
|
|
if (null == opts.signed) opts.signed = opts.bitDepth != 8;
|
|
|
|
if (opts.float && opts.bitDepth == DOUBLE_BITS) {
|
|
this.inputType = PCM_TYPE_DOUBLE;
|
|
} else if (opts.float && opts.bitDepth == FLOAT_BITS) {
|
|
this.inputType = PCM_TYPE_FLOAT;
|
|
} else if (!opts.float && opts.bitDepth == SHORT_BITS) {
|
|
this.inputType = PCM_TYPE_SHORT_INT;
|
|
} else {
|
|
throw new Error('unsupported PCM format!');
|
|
}
|
|
|
|
// copy over opts to the encoder instance
|
|
Object.keys(opts).forEach(function(key){
|
|
if (key[0] != '_' && Encoder.prototype.hasOwnProperty(key)) {
|
|
debug('setting opt %j', key);
|
|
this[key] = opts[key];
|
|
}
|
|
}, this);
|
|
}
|
|
inherits(Encoder, Transform);
|
|
|
|
/**
|
|
* Default PCM format: signed 16-bit little endian integer samples.
|
|
*/
|
|
|
|
Encoder.prototype.bitDepth = 16;
|
|
|
|
/**
|
|
* Called one time at the beginning of the first `_transform()` call.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Encoder.prototype._init = function () {
|
|
debug('_init()');
|
|
|
|
var r = binding.lame_init_params(this.gfp);
|
|
if (LAME_OKAY !== r) {
|
|
throw new Error('error initializing params: ' + r);
|
|
}
|
|
|
|
// constant: number of 'bytes per sample'
|
|
this.blockAlign = this.bitDepth / 8 * this.channels;
|
|
};
|
|
|
|
/**
|
|
* Calls `lame_encode_buffer_interleaved()` on the given "chunk.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Encoder.prototype._transform = function (chunk, encoding, done) {
|
|
debug('_transform (%d bytes)', chunk.length);
|
|
|
|
var self = this;
|
|
if (!this._initCalled) {
|
|
try { this._init(); } catch (e) { return done(e); }
|
|
this._initCalled = true;
|
|
}
|
|
|
|
// first handle any _remainder
|
|
if (this._remainder) {
|
|
debug('concating remainder');
|
|
chunk = Buffer.concat([ this._remainder, chunk ]);
|
|
this._remainder = null;
|
|
}
|
|
// set any necessary _remainder (we can only send whole samples at a time)
|
|
var remainder = chunk.length % this.blockAlign;
|
|
if (remainder > 0) {
|
|
debug('setting remainder of %d bytes', remainder);
|
|
var slice = chunk.length - remainder;
|
|
this._remainder = chunk.slice(slice);
|
|
chunk = chunk.slice(0, slice);
|
|
}
|
|
assert.equal(chunk.length % this.blockAlign, 0);
|
|
|
|
var num_samples = chunk.length / this.blockAlign;
|
|
// TODO: Use better calculation logic from lame.h here
|
|
var estimated_size = 1.25 * num_samples + 7200;
|
|
var output = new Buffer(estimated_size);
|
|
debug('encoding %d byte chunk with %d byte output buffer (%d samples)', chunk.length, output.length, num_samples);
|
|
|
|
|
|
binding.lame_encode_buffer(
|
|
this.gfp,
|
|
chunk,
|
|
this.inputType,
|
|
this.channels,
|
|
num_samples,
|
|
output,
|
|
0,
|
|
output.length,
|
|
cb
|
|
);
|
|
|
|
function cb (bytesWritten) {
|
|
debug('after lame_encode_buffer() (rtn: %d)', bytesWritten);
|
|
if (bytesWritten < 0) {
|
|
var err = new Error(ERRORS[bytesWritten]);
|
|
err.code = bytesWritten;
|
|
done(err);
|
|
} else if (bytesWritten > 0) {
|
|
output = output.slice(0, bytesWritten);
|
|
debug('writing %d MP3 bytes', output.length);
|
|
self.push(output);
|
|
done();
|
|
} else { // bytesWritten == 0
|
|
done();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Calls `lame_encode_flush_nogap()` on the thread pool.
|
|
*/
|
|
|
|
Encoder.prototype._flush = function (done) {
|
|
debug('_flush');
|
|
|
|
var self = this;
|
|
var estimated_size = 7200; // value specified in lame.h
|
|
var output = new Buffer(estimated_size);
|
|
|
|
if (!this._initCalled) {
|
|
try { this._init(); } catch (e) { return done(e); }
|
|
this._initCalled = true;
|
|
}
|
|
|
|
binding.lame_encode_flush_nogap(
|
|
this.gfp,
|
|
output,
|
|
0,
|
|
output.length,
|
|
cb
|
|
);
|
|
|
|
function cb (bytesWritten) {
|
|
debug('after lame_encode_flush_nogap() (rtn: %d)', bytesWritten);
|
|
|
|
binding.lame_close(self.gfp);
|
|
self.gfp = null;
|
|
|
|
if (bytesWritten < 0) {
|
|
var err = new Error(ERRORS[bytesWritten]);
|
|
err.code = bytesWritten;
|
|
done(err);
|
|
} else if (bytesWritten > 0) {
|
|
output = output.slice(0, bytesWritten);
|
|
self.push(output);
|
|
done();
|
|
} else { // bytesWritten == 0
|
|
done();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Define the getter/setters for the lame encoder settings.
|
|
*/
|
|
|
|
Object.keys(binding).forEach(function (key) {
|
|
if (!/^lame_[gs]et/.test(key)) return;
|
|
var name = key.substring(9);
|
|
var prop = PROPS[name] || toCamelCase(name);
|
|
debug('processing prop %j as %j', key, prop);
|
|
var getter = 'g' == key[5];
|
|
/*
|
|
console.error({
|
|
key: key,
|
|
name: name,
|
|
prop: prop,
|
|
getter: getter
|
|
});
|
|
*/
|
|
var desc = Object.getOwnPropertyDescriptor(Encoder.prototype, prop);
|
|
if (!desc) desc = { enumerable: true, configurable: true };
|
|
if (getter) {
|
|
desc.get = function () {
|
|
debug('%s()', key);
|
|
return binding[key](this.gfp);
|
|
};
|
|
} else {
|
|
desc.set = function (v) {
|
|
debug('%s(%j)', key, v);
|
|
var r = binding[key](this.gfp, v);
|
|
if (LAME_OKAY !== r) {
|
|
throw new Error('error setting prop "' + prop + '": ' + r);
|
|
}
|
|
return r;
|
|
};
|
|
}
|
|
Object.defineProperty(Encoder.prototype, prop, desc);
|
|
});
|
|
|
|
/**
|
|
* The lame encoder only supports "signed" data types.
|
|
*/
|
|
|
|
Object.defineProperty(Encoder.prototype, 'signed', {
|
|
enumerable: true,
|
|
configurable: true,
|
|
get: function () { return true; },
|
|
set: function (v) { if (!v) throw new Error('"signed" must be `true`'); }
|
|
});
|
|
|
|
|
|
/**
|
|
* Converts a string_with_underscores to camelCase.
|
|
*
|
|
* @param {String} name The name to convert.
|
|
* @return {String} The camel case'd name.
|
|
* @api private
|
|
*/
|
|
|
|
function toCamelCase (name) {
|
|
return name.replace(/(\_[a-zA-Z])/g, function ($1) {
|
|
return $1.toUpperCase().replace('_', '');
|
|
});
|
|
}
|