Permalink
Browse files

encoder: initial kinda working Encoder class

Now to make it play nice with node-ogg...
  • Loading branch information...
TooTallNate committed Jan 27, 2013
1 parent 2351fab commit 759368d1d69282729113cef9446c286776af159c
Showing with 395 additions and 32 deletions.
  1. +165 −19 lib/encoder.js
  2. +200 −13 src/binding.cc
  3. +30 −0 src/binding.h
View
@@ -33,23 +33,31 @@ function Encoder (opts) {
if (!(this instanceof Encoder)) return new Encoder(opts);
Transform.call(this, opts);
- this._writableState.objectMode = true;
- this._writableState.lowWaterMark = 0;
- this._writableState.highWaterMark = 0;
+ // the readable side (the output end) should output regular objects
+ this._readableState.objectMode = true;
+ this._readableState.lowWaterMark = 0;
+ this._readableState.highWaterMark = 0;
// set to `true` after the headerout() call
this._headerWritten = false;
// range from -0.1 to 1.0
this._quality = 0.6;
+ // PCM formatting options
+ this.channels = 2;
+ this.sampleRate = 44100;
+ this.float = true;
+ this.signed = true;
+ this.bitDepth = 32; // sizeof(float) * 8
+
this.vi = new Buffer(binding.sizeof_vorbis_info);
this.vc = new Buffer(binding.sizeof_vorbis_comment);
binding.vorbis_info_init(this.vi);
binding.vorbis_comment_init(this.vc);
// the `vorbis_dsp_state` and `vorbis_block` stucts get allocated when the
- // initial 3 packets are being written
+ // initial 3 header packets are being written
this.vd = null;
this.vb = null;
}
@@ -79,11 +87,27 @@ Encoder.prototype.addComment = function (tag, contents) {
* @api private
*/
-Encoder.prototype._transform = function (packet, output, fn) {
- debug('_transform(%d bytes)', packet.length);
- if (!this._headerWritten) {
- var err = this._writeHeader(output);
+Encoder.prototype._transform = function (buf, output, fn) {
+ debug('_transform(%d bytes)', buf.length);
+
+ // ensure the vorbis header has been output first
+ var self = this;
+ if (this._headerWritten) {
+ process();
+ } else {
+ this._writeHeader(output, process);
+ }
+
+ // time to write the PCM buffer to the vorbis encoder
+ function process (err) {
if (err) return fn(err);
+ self._writepcm(buf, written);
+ }
+
+ // after the PCM buffer has been written, read out the encoded "block"s
+ function written (err) {
+ if (err) return fn(err);
+ self._blockout(output, fn);
}
};
@@ -94,40 +118,162 @@ Encoder.prototype._transform = function (packet, output, fn) {
* @api private
*/
-Encoder.prototype._writeHeader = function (output) {
+Encoder.prototype._writeHeader = function (output, fn) {
debug('_writeHeader()');
// encoder init (only VBR currently supported)
- var channels = 2;
- var sampleRate = 44100;
+ var channels = this.channels;
+ var sampleRate = this.sampleRate;
// TODO: async maybe?
r = binding.vorbis_encode_init_vbr(this.vi, channels, sampleRate, this._quality);
- if (0 !== r) {
- return new Error(r);
- }
+ if (0 !== r) return fn(new Error(r));
+
+ // constant: number of 'bytes per sample'
+ this.blockAlign = this.bitDepth / 8 * this.channels;
// synthesis init
this.vd = new Buffer(binding.sizeof_vorbis_dsp_state);
this.vb = new Buffer(binding.sizeof_vorbis_block);
var r = binding.vorbis_analysis_init(this.vd, this.vi);
- if (0 !== r) return new Error(r);
+ if (0 !== r) return fn(new Error(r));
r = binding.vorbis_block_init(this.vd, this.vb);
- if (0 !== r) return new Error(r);
+ if (0 !== r) return fn(new Error(r));
// create first 3 packets
// TODO: async
var op_header = new Buffer(binding.sizeof_ogg_packet);
var op_comments = new Buffer(binding.sizeof_ogg_packet);
var op_code = new Buffer(binding.sizeof_ogg_packet);
r = binding.vorbis_analysis_headerout(this.vd, this.vc, op_header, op_comments, op_code);
- if (0 !== r) return new Error(r);
+ if (0 !== r) return fn(new Error(r));
output(op_header); // automatically gets placed in its own page
output(op_comments);
output(op_code);
- // imply that a page flush() call is required
- this.emit('flush');
+ // specify that a page flush() call is required at this point
+ output({ flush: true });
this._headerWritten = true;
+
+ process.nextTick(fn);
+};
+
+/**
+ * Writes the given Buffer `buf` to the vorbis backend encoder.
+ *
+ * @api private
+ */
+
+Encoder.prototype._writepcm = function (buf, fn) {
+ debug('_writepcm(%d bytes)', buf.length);
+
+ var channels = this.channels;
+ var blockAlign = this.bitDepth / 8 * channels;
+ var samples = buf.length / blockAlign | 0;
+ var leftover = (samples * blockAlign) - buf.length;
+ if (leftover > 0) {
+ console.error('%d bytes leftover!', leftover);
+ throw new Error('implement "leftover"!');
+ }
+
+ binding.vorbis_analysis_write(this.vd, buf, channels, samples, function (rtn) {
+ buf = buf; // keep ref to "buf" for the async call...
+
+ if (0 === rtn) {
+ // success
+ fn();
+ } else {
+ // error code
+ fn(new Error('vorbis_analysis_write() error: ' + rtn));
+ }
+ });
+};
+
+/**
+ * Calls `vorbis_analysis_blockout()` continuously until no more blocks are
+ * returned. For each "block" that gets returned, _flushpacket() is called to
+ * extract any possible `ogg_packet` instances from the block.
+ *
+ * @api private
+ */
+
+Encoder.prototype._blockout = function (output, fn) {
+ debug('_blockout');
+ var vd = this.vd;
+ var vb = this.vb;
+ var self = this;
+ binding.vorbis_analysis_blockout(vd, vb, function (rtn) {
+ if (1 === rtn) {
+ // got a "block"
+
+ // analysis, assume we want to use bitrate management
+ // TODO: async?
+ // TODO: check return values
+ binding.vorbis_analysis(vb, null);
+ binding.vorbis_bitrate_addblock(vb);
+
+ self._flushpacket(output, afterFlush);
+ } else if (0 === rtn) {
+ // need more PCM data...
+ fn();
+ } else {
+ // error code
+ fn(new Error('vorbis_analysis_blockout() error: ' + rtn));
+ }
+ });
+ function afterFlush (err) {
+ if (err) return fn(err);
+ // now attempt to read another "block"
+ self._blockout(output, fn);
+ }
+};
+
+/**
+ * Calls `vorbis_bitrate_flushpacket()` continuously until no more `ogg_packet`s
+ * are returned.
+ *
+ * @api private
+ */
+
+Encoder.prototype._flushpacket = function (output, fn) {
+ debug('_flushpacket()');
+ var self = this;
+ var packet = new Buffer(binding.sizeof_ogg_packet);
+ binding.vorbis_bitrate_flushpacket(this.vd, packet, function (rtn) {
+ if (1 === rtn) {
+ // got a packet, output it
+ output(packet);
+ // the consumer should call `pageout()` at this point
+ output({ pageout: true });
+
+ // attempt to get another `ogg_packet`...
+ self._flushpacket(output, fn);
+ } else if (0 === rtn) {
+ // need more "block" data
+ fn();
+ } else {
+ // error code
+ fn(new Error('vorbis_bitrate_flushpacket() error: ' + rtn));
+ }
+ });
+};
+
+/**
+ * This function calls the `vorbis_analysis_wrote(this.vd, 0)` function, which
+ * implies to libvorbis that the end of the audio PCM stream has been reached,
+ * and that it's time to close up the ogg stream.
+ *
+ * @api private
+ */
+
+Encoder.prototype._flush = function (output, fn) {
+ debug('_onflush()');
+ var r = binding.vorbis_analysis_eos(this.vd, 0);
+ if (0 === r) {
+ this._blockout(output, fn);
+ } else {
+ // error code
+ fn(new Error('vorbis_analysis_eos() error: ' + r));
+ }
};
Oops, something went wrong.

0 comments on commit 759368d

Please sign in to comment.