linx-simulator2/node_modules/lame/lib/encoder.js
2019-09-18 11:11:16 +03:00

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('_', '');
});
}