Jump To …

audioout.js

/*global webkitAudioContext */

having that set within detectApi() is sketchy (user would think they're all in one section), but not sure how to get around it.

workaround for Chrome bug http://code.google.com/p/chromium/issues/detail?id=82795 copied from sink.js.

Optionally initialize with a conf, but these settings may be overridden, eg Web Audio API does not allow setting of sample rate. Make sure to use the conf returned by init() in clients.

(function (global) {

global.fixChrome82795 = [];
var audioout = function (conf) {
  var that = {},
    apis = { 'audioData': false, 'webAudio': false },
    audioCallback,
    api,

audioData variables

    audio,
    tail = null,
    tailPosition,
    currentWritePosition,
    audioDataInterval = null,

webAudio variables

    context,
    jsNode,
    webAudioRunning = false,

file variables

    fileFormat,
    fileOutfile,
    fileRunning = false,
    fileFd,
    fileLines, fileBytesWritten,
    fs;

  conf = conf || {};
  conf.channels = conf.channels || 1;
  conf.sampleRate = conf.sampleRate || 44100;
  conf.bufferSize = conf.bufferSize || conf.sampleRate / 2;

  if (typeof Audio != 'undefined') {
    apis.audioData = !!new Audio().mozSetup; 
  } 
  if (typeof window != 'undefined') {
    apis.webAudio = !!(window.webkitAudioContext || window.AudioContext); 
  }

  function hasApi () {
    if (apis.audioData || apis.webAudio) {
      return true;
    }
  }

  function setAudioCallback (f) {
    audioCallback = f;
  }

////////////////////////////////////////////////////////////// Audio Data API functions

  function audioDataInit () {
    currentWritePosition = 0;
    tail = null;

    audio.mozSetup(conf.channels, conf.sampleRate);

hardcode 2 channels

    conf.channels = 2;

Audio Data API implementation needs a pretty big buffer, override conf.bufferSize here.

    conf.bufferSize = conf.channels * conf.sampleRate/2;
    return conf;
  }

  function audioDataIsRunning () {
    return (audioDataInterval !== null);
  }

undefined if already running, true if started

  function audioDataRun () {
    if (audioDataIsRunning()) {
      return; 
    }

    audioDataInterval = setInterval(function () {
      var written, currentPosition, available, soundData;

Check if some data was not written in previous attempts.

      if (tail) {
        written = audio.mozWriteAudio(tail.subarray(tailPosition));
        currentWritePosition += written;
        tailPosition += written;
        if(tailPosition < tail.length) {

Not all the data was written, saving the tail...

          return; // ... and exit the function.
        }
        tail = null;
      }

Check if we need add some data to the audio output.

      currentPosition = audio.mozCurrentSampleOffset();
      available = currentPosition + conf.bufferSize - currentWritePosition;
      if (available > 0) {

Request some sound data from the callback function.

        soundData = new Float32Array(available);
        audioCallback(soundData);

Writing the data.

        written = audio.mozWriteAudio(soundData);
        if(written < soundData.length) {

Not all the data was written, saving the tail.

          tail = soundData;
          tailPosition = written;
        }
        currentWritePosition += written;
      }
    }, 100);
    return true;
  }

undefined if already stopped, true if stopped here.

  function audioDataStop () {
    if (!audioDataIsRunning()) { 
      return; 
    }
    clearInterval(audioDataInterval);
    audioDataInterval = null;
    return true;
  }

////////////////////////////////////////////////////////////// Web Audio API functions

  function webAudioInit () {
    jsNode = context.createJavaScriptNode(conf.bufferSize, 0, 1);

Chrome bug workaround cribbed from sink.js

    global.fixChrome82795.push(jsNode);
    jsNode.onaudioprocess = function (e) {
      var buf, buf2,
        i, 
        i_max;
      buf = e.outputBuffer.getChannelData(0);
      buf2 = e.outputBuffer.getChannelData(1);

need to zero out data as it contains the last batch

      for (i=0, i_max=buf.length; i<i_max; i++) {
        buf[i] = 0;
        buf2[i] = 0;
      }
      audioCallback(buf, buf2);
    };

Sample rate is readonly for Web Audio API, so override conf.sampleRate with value from context.

    conf.sampleRate = context.sampleRate;

hardcode 2 channels.

    conf.channels = 2;
    return conf;
  }

  function webAudioRun () {
    jsNode.connect(context.destination);
    webAudioRunning = true;
    return true;
  }

  function webAudioStop () {
    jsNode.disconnect();
    webAudioRunning = false;
    return true;
  }

  function webAudioIsRunning () {
    return webAudioRunning;
  }

////////////////////////////////////////////////////////////// Node Filesystem API functions

Should maybe set path and format as extra args to setApi() instead.

  function fileInit (path, format) {
    fs = require('fs');
    fileOutfile = path;
    fileFormat = format;
    fileFd = fs.openSync(fileOutfile, 'w');

fix file buffer size to the sample rate

    conf.bufferSize = conf.sampleRate;
    conf.channels = 2;
    return conf;
  }

  function fileIsRunning () {
    return fileRunning;
  }

  function fileRun () {
    var samples, samples2, sampleString, sampleLength,
      i, iMax, buffer;

    fileBytesWritten = 0;
    fileLines = 0;
    fileRunning = true;

    while (fileRunning) { 
      sampleString = '';
      samples = new Float32Array(conf.bufferSize);
      samples2 = new Float32Array(conf.bufferSize);

kludge: don't write the last buffer if cb returns falsy, else since fileStop() was called and the fd closed this will throw an error

      if (audioCallback(samples, samples2)) {
        sampleLength = samples.length;

text file with lines sample_number value

        if (fileFormat == 'txt') {
          for (i=0; i<sampleLength; i++) {
            sampleString += fileLines;
            sampleString += ' ';
            sampleString += samples[i];
            sampleString += ' ';
            sampleString += samples2[i];
            sampleString += '\n';
            fileLines++;
          }
          fileBytesWritten += fs.writeSync(fileFd, sampleString);

binary file of interleaved 32-bit LE floats

        } else if (fileFormat == 'float') {
          buffer = new Buffer(8*samples.length);
          for (i=0; i<sampleLength; i++) {
            buffer.writeFloatLE(samples[i],i*8);
            buffer.writeFloatLE(samples2[i],i*8+4);
          }
          fileBytesWritten += fs.writeSync(fileFd, buffer, 
            0, buffer.length);
        }
      }
    }

    return true;
  }

  function fileStop () {
    fileRunning = false;
    fs.closeSync(fileFd);
    return true;
  }


  function setApi (theApi) {
    if (!theApi) {
      if (apis.audioData) {
        api = 'audioData';
      } else if (apis.webAudio) {
        api = 'webAudio';
      } else {
        return;
      }    
    } else {
      api = theApi;
    }

    if (api == 'audioData') {
      audio = new Audio();
      that.init = audioDataInit;
      that.run = audioDataRun;
      that.stop = audioDataStop;
      that.isRunning = audioDataIsRunning;
    } else if (api == 'webAudio') {

too many setApi's might result in "audio resources unavailable"

      context = new webkitAudioContext();
      that.init = webAudioInit;
      that.run = webAudioRun;
      that.stop = webAudioStop;
      that.isRunning = webAudioIsRunning;
    } else if (api == 'file') {
      that.init = fileInit;
      that.run = fileRun;
      that.stop = fileStop;
      that.isRunning = fileIsRunning;
    } else {
      return;
    }
    return api;
  }

  function getApi () {
    return api;
  }

  that.setAudioCallback = setAudioCallback;
  that.hasApi = hasApi;
  that.getApi = getApi;
  that.setApi = setApi;

  return that;
};

if (typeof module != 'undefined' && module.exports) {
  module.exports = audioout;
} else {
  global.trurl = global.trurl || {};
  global.trurl.audioout = audioout;
}

}(this));