Source: webaudio-wav-stream.js

'use strict';
var Transform = require('stream').Transform;
var util = require('util');

function WebAudioWavStream(opts) {

  Transform.call(this, opts);

  this.sourceSampleRate = 4800;
  this.sampleRate = 16000;

  this.bufferUnusedSamples = new Float32Array(0);

  var self = this;
  this.on('pipe', function (src) {
    src.on('format', function (format) {
      self.sourceSampleRate = format.sampleRate;
    });
  });

  self.writeHeader();
}
util.inherits(WebAudioWavStream, Transform);

/**
 * Converts WebAudio to 'audio/l16' (raw wav) and downsamples to 16 kHz.
 *
 * Explanation for the math: The raw values captured from the Web Audio API are
 * in 32-bit Floating Point, between -1 and 1 (per the specification).
 * The values for 16-bit PCM range between -32768 and +32767 (16-bit signed integer).
 * Multiply to control the volume of the output. We store in little endian.
 *
 * @param  {Object} buffer Microphone/MediaElement audio chunk
 * @return {Buffer} 'audio/l16' chunk
 * @deprecated This method is deprecated
 */
WebAudioWavStream.prototype._exportDataBufferTo16Khz = function (nodebuffer) {
  var bufferNewSamples = new Float32Array(nodebuffer.buffer),
    buffer = null,
    newSamples = bufferNewSamples.length,
    unusedSamples = this.bufferUnusedSamples.length;


  if (unusedSamples > 0) {
    buffer = new Float32Array(unusedSamples + newSamples);
    for (var i = 0; i < unusedSamples; ++i) {
      buffer[i] = this.bufferUnusedSamples[i];
    }
    for (i = 0; i < newSamples; ++i) {
      buffer[unusedSamples + i] = bufferNewSamples[i];
    }
  } else {
    buffer = bufferNewSamples;
  }

  // downsampling variables
  var filter = [
      -0.037935, -0.00089024, 0.040173, 0.019989, 0.0047792, -0.058675, -0.056487,
      -0.0040653, 0.14527, 0.26927, 0.33913, 0.26927, 0.14527, -0.0040653, -0.056487,
      -0.058675, 0.0047792, 0.019989, 0.040173, -0.00089024, -0.037935
    ],
    samplingRateRatio = this.sourceSampleRate / 16000,
    nOutputSamples = Math.floor((buffer.length - filter.length) / (samplingRateRatio)) + 1,
    pcmEncodedBuffer16k = new ArrayBuffer(nOutputSamples * 2),
    dataView16k = new DataView(pcmEncodedBuffer16k),
    index = 0,
    volume = 0x7FFF, //range from 0 to 0x7FFF to control the volume
    nOut = 0;

  for (i = 0; i + filter.length - 1 < buffer.length; i = Math.round(samplingRateRatio * nOut)) {
    var sample = 0;
    for (var j = 0; j < filter.length; ++j) {
      sample += buffer[i + j] * filter[j];
    }
    sample *= volume;
    try {
      dataView16k.setInt16(index, sample, true); // 'true' -> means little endian
    } catch (ex) {
      // chrome occasionally throws RangeError: Offset is outside the bounds of the DataView
      // todo: actually fix it instead of just ignoring the error..
    }
    index += 2;
    nOut++;
  }

  var indexSampleAfterLastUsed = Math.round(samplingRateRatio * nOut);
  var remaining = buffer.length - indexSampleAfterLastUsed;
  if (remaining > 0) {
    this.bufferUnusedSamples = new Float32Array(remaining);
    for (i = 0; i < remaining; ++i) {
      this.bufferUnusedSamples[i] = buffer[indexSampleAfterLastUsed + i];
    }
  } else {
    this.bufferUnusedSamples = new Float32Array(0);
  }

  return new Buffer(dataView16k.buffer);
};


/**
 * The max size of the "data" chunk of a WAVE file. This is the max unsigned
 * 32-bit int value, minus 100 bytes (overkill, 44 would be safe) for the header.
 *
 * From https://github.com/TooTallNate/node-wav/blob/master/lib/writer.js
 *
 * @api private
 */

var MAX_WAV = 4294967295 - 100;

function writeString(view, offset, string) {
  for (var i = 0; i < string.length; i++){
    view.setUint8(offset + i, string.charCodeAt(i));
  }
}

WebAudioWavStream.prototype.writeHeader = function writeHeader() {
  var buffer = new ArrayBuffer(44);
  var view = new DataView(buffer);
  var length = MAX_WAV; // can't use the actuall length because we don't know it yet
  var numChannels = 1;
  var sampleRate = 16000;

  /* RIFF identifier */
  writeString(view, 0, 'RIFF');
  /* RIFF chunk length */
  view.setUint32(4, 36 + length, true);
  /* RIFF type */
  writeString(view, 8, 'WAVE');
  /* format chunk identifier */
  writeString(view, 12, 'fmt ');
  /* format chunk length */
  view.setUint32(16, 16, true);
  /* sample format (raw PCM) */
  view.setUint16(20, 1, true);
  /* channel count */
  view.setUint16(22, numChannels, true);
  /* sample rate */
  view.setUint32(24, sampleRate, true);
  /* byte rate (sample rate * block align) */
  view.setUint32(28, sampleRate * 4, true);
  /* block align (channel count * bytes per sample) */
  view.setUint16(32, numChannels * 2, true);
  /* bits per sample */
  view.setUint16(34, 16, true);
  /* data chunk identifier */
  writeString(view, 36, 'data');
  /* data chunk length */
  view.setUint32(40, length, true);

  this.push(new Buffer(buffer));
};

WebAudioWavStream.prototype._transform = function (chunk, encoding, next) {
  this.push(this._exportDataBufferTo16Khz(chunk));
  next();
};

WebAudioWavStream.prototype._flush = function (next) {
  // todo: handle anything left in this.bufferUnusedSamples here...
  next();
};

module.exports = WebAudioWavStream;