instruments.js

/**
 * Instruments provide a OOP style interface to the synths. Something with
 * `start` and `stop` methods, than can be called several times and know about
 * note frequencies and midi connections.
 *
 * @example
 * var simple = inst((fq) => conn(sine(fq), adsr()))
 * simple.start('C4')
 * simple.start('G5')
 * simple.stop() // => stop all notes
 * @module instruments
 */
import { isA, OPTS, toArr, slice } from './utils'
import { context, when, dest } from './context'
import { withDest } from './synths'

/**
 * A master output instrument. You can use it to start and stop nodes. All
 * started nodes will be connected to the AudioContext destination.
 *
 * @example
 * master.start(sine(300)) // connect to destination and start
 * master.start(sine(600), 0, 1) // connect to destination and start after 1 second
 * master.stop() // stop all
 */
export var master = inst(null, dest())

/**
 * Create an object-oriented-style instrument player. It wraps a synth function
 * (a function that create nodes) into in a convenient player API. It can
 * be used to limit the polyphony.
 *
 * The player object have the following methods:
 *
 * - `start(node, when, delay, duration)`: start a node
 * - `stop`: stop all nodes
 * - `on(name, callback)`: add an event callback
 * - `event(name, ...values)`: fire an event
 *
 *
 * @param {Function} synth - the synth function (a function that returns a node graph)
 * @param {AudioNode} destination - if present, all nodes will be connected to
 * this destination
 * @param {Object} options - (Optional) the options may include:
 *
 * - maxVoices: the maximum number of simultaneous voices. No value (by default)
 * means no limit.
 *
 * @return {Player} a player object
 *
 * @example
 * // an instrument with destination
 * var synth = inst((fq) => sine(fq), dest())
 * synth.start('A4')
 * synth.stopAll()
 * @example
 * // only the destination
 * var master = inst(null, conn(mix(0.2), reverb(), dest()))
 * master.start(sine(300))
 * master.start(sine(400))
 * master.stopAll()
 */
export function inst (synth, destination, options) {
  synth = withDest(synth, destination || dest())
  return tracker(synth, options || OPTS)
}

/**
 * tracker: (fn: (object) => Node, options: object) => interface { start: fn, stop: fn }
 * @private
 */
function tracker (synth, opts) {
  var ob = observable({})
  var limit = opts ? opts.maxVoices : 0
  var voices = { limit: limit, all: {}, nextId: 0, current: 0, pool: new Array(limit) }

  function track (node) {
    node.id = voices.nextId++
    voices.all[node.id] = node
    on(node, 'ended', function () {
      delete voices.all[node.id]
      ob.event('voices', Object.keys(voices.all), limit)
    })
    return node
  }

  ob.start = function (value, time, delay, duration) {
    var node = synth(value)
    if (node.start) {
      track(node)
      time = start(node, time, delay, duration).startedAt
      ob.event('start', node.id, time)
      ob.event('voices', Object.keys(voices.all), limit)
    }
    return node
  }
  ob.stop = function (ids, time, delay) {
    var t = 0
    ids = toArr(ids || ids === 0 ? ids : Object.keys(voices.all))
    ids.forEach(function (id) {
      if (voices.all[id]) {
        if (!t) t = when(time, delay, voices.all[id].context)
        voices.all[id].stop(t)
      }
    })
  }
  return ob
}

function start (node, time, delay, duration) {
  if (time && !isA('number', time)) throw Error('Invalid time (maybe forgot connect?): ', time, delay, duration, node)
  time = when(time, delay, context(node.context))
  node.start(time)
  node.startedAt = time
  var d = duration || node.duration
  if (d) node.stop(time + d)
  return node
}

// EVENTS
// ======
// decorate an objet to have `on` and `event` methods
function observable (obj) {
  obj.on = on.bind(null, obj)
  obj.event = event.bind(null, obj)
  return obj
}

// add a listener to a target
function on (target, name, callback) {
  if (!name || name === '*') name = 'event'
  var prev = target['on' + name]
  target['on' + name] = function () {
    if (prev) prev.apply(null, arguments)
    callback.apply(null, arguments)
  }
  return target
}

// fire an event
function event (target, name /*, ...values */) {
  var args = slice.call(arguments, 1)
  if (isA('function', target['on' + name])) target['on' + name].apply(null, args)
  if (isA('function', target.onevent)) target.onevent.apply(null, args)
  return target
}