oscillators.js

/**
 * This module provides some syntactic sugar over the AudioContext.createOscillator
 * function
 *
 * @example
 * import { sine, square, master } from 'synth-kit'
 *
 * master.start(sine('A4'))
 * master.start(square({ note: 'c3', detune: -10 }))
 * @module oscillators
 */
import { noteToFreq, midiToFreq, tempoToFreq } from './units'
import { context } from './context'
import { mult } from './signals'
import { plug, lifecycle } from './routing'
import { OPTS, isA } from './utils'

// Create a OscillatorNode of the given type and configuration
// this is a private function intended to be partially applied
function create (type, opts) {
  opts = opts || OPTS
  type = type || opts.type
  var osc = context(opts.context).createOscillator()
  if (type) osc.type = type
  return lifecycle(osc, [
    plug('frequency', getFreq(opts), osc),
    plug('detune', opts.detune, osc)
  ])
}

// given a configuration object, get the frequency
export function getFreq (obj) {
  return obj.frequency ? obj.frequency
    : obj.freq ? obj.freq
    : obj.midi ? midiToFreq(obj.midi)
    : obj.note ? noteToFreq(obj.note)
    : isA('string', obj) ? noteToFreq(obj)
    : isA('number', obj) ? obj
    : null
}

/**
 * Create an oscillator (an OscillatorNode)
 *
 * @function
 * @param {Object} config - may include:
 *
 * - type: one of the OscillatorNode types
 * - frequency (or freq): the oscillator frequency (can be a signal)
 * - detune: the detune in cents (can be a signal)
 * - note: the note name to get the frequency from (if frequency is not present)
 * - midi: the note midi number to get the frequency from (if frequency is not present)
 * - context: the audio context to use
 *
 * Notice that instead of a config object, this function also accepts a note
 * name or a frequency value (see examples)
 *
 * @return {AudioNode} the oscillator
 * @example
 * // basic usage
 * osc({ type: 'sine', frequency: 880 })
 * osc({ note: 'C4', type: 'square', detune: -10 })
 * // parameter modulation
 * osc({ freq: 1500, detune: osc({ freq: 20}) })
 * osc({ freq: envelope(...), type: 'square' })
 * // without configuration object
 * osc('C4')
 * osc(1200)
 */
export const osc = create.bind(null, null)

/**
 * Create a sine oscillator. An alias for `osc({ type: 'sine', ... })`
 * @function
 * @see osc
 * @param {Object} config - Same as `osc` function, but without 'type'
 * @return {AudioNode} the oscillator
 * @example
 * sine('C4')
 * sine({ midi: 70, detune: -50 })
 */
export const sine = create.bind(null, 'sine')

/**
 * Create a sawtooth oscillator. An alias for `osc({ type: 'sawtooth', ... })`
 * @function
 * @see osc
 * @param {Object} config - Same as `osc` function, but without 'type'
 * @return {AudioNode} the oscillator
 * @example
 * saw('A3')
 * saw({ freq: 440, detune: lfo(5, 10) })
 */
export const saw = create.bind(null, 'sawtooth')
/**
 * Create a square oscillator. An alias for `osc({ type: 'square', ... })`
 * @function
 * @see osc
 * @param {Object} config - Same as `osc` function, but without 'type'
 * @return {AudioNode} the oscillator
 * @example
 * square({ note: 'c#3', context: offline() })
 */
export const square = create.bind(null, 'square')

/**
 * Create a triangle oscillator. An alias for `osc({ type: 'triangle', ... })`
 * @function
 * @see osc
 * @param {Object} config - Same as `osc` function, but without 'type'
 * @return {AudioNode} the oscillator
 * @example
 * triangle({ note: 'Bb4', detune: -10 })
 */
export const triangle = osc.bind(null, 'triangle')

/**
 * Create an LFO (low frequency oscillator). It's a standard oscillator with
 * some goodies to reduce the boilerplate code when used as signal modulator.
 *
 * @see osc
 * @param {Options} config - May include any of the `osc` function plus:
 *
 * - tempo: the tempo used to calculate the frequency (it overrides the frequency parameter)
 * - division: the number of subdivisions of the tempo (defaults to 1)
 * - amplitude: the amplitude of the oscillator
 *
 * @example
 * sine({ note: 'C4', detune: lfo({ freq: 5, amplitude: 50 }) })
 * sine({ note: 'A4', detune: lfo({ amplitude: 10, tempo: 120, division: 3 })
 */
export function lfo (opts) {
  opts = opts || OPTS
  var node = osc(opts)
  if (opts.tempo) node.frequency.value = tempoToFreq(opts.tempo, opts.division)
  return mult(opts.amplitude || 1, node)
}