synths.js

/**
 * This module provides some typical syntetizers components, like vca, vcf
 * and some syntetizers
 * @example
 * // a dual osc synth sound
 * synth(
 *  frequency: 440,
 *  oscillators: { ratios: [1, 5/7, 9/8], types: 'saw' },
 *  attack: 0.1, release: 0.1,
 *  filter: { frequency: 'follow' }
  ).connect(dest()).start()
 * @module synths
 */
import { isA, OPTS, assign, toArr } from './utils'
import { dBToGain } from './units'
import { conn, add } from './routing'
import { gain, mult, signal } from './signals'
import { osc } from './oscillators'
import { filter } from './filters'
import { adsr } from './envelopes'

/**
 * Decorate a function to have default configuration
 */
export function withDefaults (synth, defaults) {
  return function (options) {
    return synth(assign({}, defaults, options))
  }
}

/**
 * Create a VCA: an amplifier controlled by an ADSR envelope
 * @function
 * @param {Object} options - may include:
 * @return {AudioNode} a GainNode
 *
 * - db: the gain in decibels
 * - gain: the gain (will override dB param)
 * - attack, decay, sustain, release: the ADSR envelope parameters
 * - context: the audio context
 */
export const vca = withDefaults(function (opts) {
  if (!isA('number', opts.gain)) opts.gain = dBToGain(opts.dB) || 1
  return conn(gain(opts), adsr(opts))
}, {
  dB: 0, // in dB
  attack: 0.01, // attack time in seconds
  decay: 0.1, // decay time in seconds
  sustain: 0,
  release: 0.1
})

/**
 * Create a VCF: a filter controlled by an ADSR envelope
 * @function
 * @param {Object} config - may include:
 * @return {AudioNode} the BiquadFilterNode
 *
 * - type: filter type
 * - frequency: filter frequency (can be a signal)
 * - octaves: amplitude of the modulation in octaves (defaults to 2)
 * - attack, decay, sustain, release: the adsr parameters
 * - context: the audio context

 * @example
 * conn(square({ frequency: 800 }), vcf({ frequency: 400, attack: 0.1 }))
 */
export const vcf = withDefaults(function (opts) {
  var mod = conn(signal(mult(opts.octaves, opts.frequency)), adsr(opts))
  return filter(opts.type, add(signal(opts.frequency), mod), opts)
}, {
  type: 'lowpass',
  frequency: 22000,
  octaves: 2,
  attack: 0.01,
  decay: 0.1,
  sustain: 0,
  release: 0.1
})

/**
 * A subtractive synthetizer (oscillator bank -> VCF -> VCA)
 *
 * @param {AudioNode|Number|String} source - The subtractive synth source
 * @param {Object} options - (Optional) the configuration may include:
 *
 * - gain: the gain (defaults to 1)
 * - adsr: the parameters of the gain envelop in the form [attack, decay, sustain, release]
 * - attack, decay, sustain, release: the parameters of the gain envelope. It
 * will override the one provided by adsr parameter if any
 * - filter: an object with the filter properties
 * - filter.type: the filter type ('lowpass' by default)
 * - filter.frequency: the filter frequency (can be follow to use the options.frequency value)
 * - filter.adsr: the filter envelope
 * - filter.attack, filter.decay, filter.sustain, filter.release: the individual
 * filter envelope parameters
 * - filter.octaves: the number of the octaves of the filter envelope (1 by default)
 *
 * @function
 * @example
 * // a 300Hz sawtooth with adsr and lowass filter at 600
 * subtractive({ frequency: 300, type: 'sawtooth', adsr: [0.01, 0.1, 0.8, 1], filter: { type: 'lowpass', frequency: 600 } })
 * // a custom source node
 * substractive({ source: add(white(), square(500)), attack: 0.1, duration: 1 })
 * // connected in a chain
 * connect(sample('snare.wav', subtractive({ attack: 0.1 }))
 */
export function subtractive (opts) {
  opts = opts || OPTS
  return conn(bank(opts), vcf(opts.filter), vca(opts))
}

/**
 * Create a bank of oscillators.
 *
 * @param {Array<Float>} frequencies - an array with the frequencies
 * @param {Object} options - (Optional) options can include:
 *
 * - frequencies: an array with the frequencies of the oscillators (will
 * override ratios)
 * - ratios: an array of relative freqnecies of the oscillators (in combination
 * iwth frequency paramter)
 * - frequency: the base frequency of the oscillators (only used if frequencies)
 * - types: a value or an array of oscillator types. If the array is shorter
 * than the frequencies array, it's assumed to be circular.
 * - gains: a value or an array of gain values. If the array is shorter
 * than the frequencies array, it's assumed to be circular.
 * - compensate: if true, the gain of the bank will be reduced by the number
 * of oscillators (true by default)
 *
 * @return {AudioNode}
 *
 * @example
 * // create three sines with unrelated frequencies:
 * bank({ frequencies: [1345.387, 435.392, 899.432] })
 * // create three sawtooth with relative frequencies:
 * bank({ frequency: 440, ratios: [1, 2, 2.4] , types: 'sawtooth' })
 * // create two squares of 400 and 800 and two sawtooths of 600 and 1200
 * // (the types are cyclic)
 * bank({ frequencies: [400, 600, 800, 1200], types: ['square', 'sawtooth'] })
 * // specify gains
 * bank({ frequencies: [440, 660], gains: [0.6, 0.2] })
 */
export function bank (opts) {
  opts = opts || OPTS
  var base = opts.ratios ? opts.frequency || 440 : 1
  var freqs = toArr(opts.frequencies || opts.ratios || 440)
  var gains = toArr(opts.gains || 1)
  var types = toArr(opts.types || 'sine')
  var N = opts.compensate === false ? 1 : freqs.length

  var tl = types.length
  var gl = gains.length
  return conn(add(freqs.map(function (freq, i) {
    var src = osc(types[i % tl], base * freq)
    var g = gains[i % gl]
    return g === 1 ? src : conn(src, gain(g))
  })), gain(1 / N))
}

/**
 * Decorate a synth to use a destination. A synth is any function that
 * returns an audio node
 * @param {Function} synth - the audio node builder
 * @param {AudioNode} destination - (Optional) destination (or the default destination)
 * @return {Function} the decorated synth function
 * @example
 * // TODO: write a more realistic example
 * var synth = withDest(sine, dest())
 * synth(300).start()
 * // TODO: make this function public
 * @private
 */
export function withDest (synth, dest) {
  return function (value) {
    var node = synth ? synth(value) : value
    if (dest) node.connect(dest)
    return node
  }
}