Source: syngen/fn.js

/**
 * A collection of useful functions.
 * @namespace
 */
syngen.fn = {}

syngen.fn.accelerateValue = (current, target, acceleration, deceleration = undefined) => {
  if (typeof deceleration == 'undefined') {
    deceleration = acceleration
  }

  if (current == target) {
    return target
  }

  const deltaRate = syngen.loop.delta() * (
    Math.abs(target) >= Math.abs(current) && Math.sign(current) == Math.sign(target)
      ? acceleration
      : deceleration
  )

  if (syngen.fn.between(target, current - deltaRate, current + deltaRate)) {
    return target
  }

  return current > target
    ? current - deltaRate
    : current + deltaRate
}

syngen.fn.accelerateVector = (current, target, acceleration, deceleration = undefined) => {
  if (deceleration === undefined) {
    deceleration = acceleration
  }

  if (!syngen.tool.vector3d.prototype.isPrototypeOf(current)) {
    current = syngen.tool.vector3d.create(current)
  }

  if (!syngen.tool.vector3d.prototype.isPrototypeOf(target)) {
    target = syngen.tool.vector3d.create(target)
  }

  const next = current.clone()

  if (current.equals(target)) {
    return next
  }

  const normalized = target.subtract(current).normalize()

  for (const axis of ['x', 'y', 'z']) {
    next[axis] = syngen.fn.accelerateValue(
      current[axis],
      target[axis],
      acceleration * Math.abs(normalized[axis]),
      deceleration * Math.abs(normalized[axis])
    )
  }

  return next
}

/**
 * Adds a musical `interval` to a `frequency`, in Hertz.
 * @param {Number} frequency
 * @param {Number} interval
 *   Each integer multiple represents an octave.
 *   For example, `2` raises the frequency by two octaves.
 *   Likewise, `-1/12` lowers by one half-step, whereas `-1/100` lowers by one cent.
 * @static
 */
syngen.fn.addInterval = (frequency, interval) => frequency * (2 ** interval)

/**
 * Returns whether `value` is between `a` and `b` (inclusive).
 * @param {Number} value
 * @param {Number} a
 * @param {Number} b
 * @returns {Boolean}
 * @static
 */
syngen.fn.between = (value, a, b) => value >= Math.min(a, b) && value <= Math.max(a, b)

/**
 * Calculates the geometric center of variadic vectors or vector-likes.
 * @param {syngen.utility.vector3d[]|syngen.utility.vector2d[]|Object[]} ...ectors
 * @returns {syngen.utility.vector3d}
 * @static
 */
syngen.fn.centroid = (...vectors) => {
  if (!vectors.length) {
    return syngen.tool.vector3d.create()
  }

  let xSum = 0,
    ySum = 0,
    zSum = 0

  for (const vector of vectors) {
    xSum += vector.x || 0
    ySum += vector.y || 0
    zSum += vector.z || 0
  }

  return syngen.tool.vector3d.create({
    x: xSum / vectors.length,
    y: ySum / vectors.length,
    z: zSum / vectors.length,
  })
}

/**
 * Returns the element of `options` at the index determined by percentage `value`.
 * @param {Array} options
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @returns {*}
 * @static
 */
syngen.fn.choose = (options = [], value = 0) => {
  value = syngen.fn.clamp(value, 0, 1)

  const index = Math.round(value * (options.length - 1))
  return options[index]
}

/**
 * Splices and returns the element of `options` at the index determined by percentage `value`.
 * Beward that this mutates the passed array.
 * @param {Array} options
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @returns {*}
 * @static
 */
syngen.fn.chooseSplice = (options = [], value = 0) => {
  value = syngen.fn.clamp(value, 0, 1)

  const index = Math.round(value * (options.length - 1))
  return options.splice(index, 1)[0]
}

/**
 * Returns the element of `options` at the index determined by weighted percentage `value`.
 * @param {Array} options
 *   Each element is expected to have a `weight` key which is a positive number.
 *   Higher weights are more likely to be chosen.
 *   Beware that elements are not sorted by weight before selection.
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @returns {*}
 * @static
 */
syngen.fn.chooseWeighted = (options = [], value = 0) => {
  // SEE: https://medium.com/@peterkellyonline/weighted-random-selection-3ff222917eb6
  value = syngen.fn.clamp(value, 0, 1)

  const totalWeight = options.reduce((total, option) => {
    return total + (option.weight || 0)
  }, 0)

  let weight = value * totalWeight

  for (const option of options) {
    weight -= option.weight || 0

    if (weight <= 0) {
      return option
    }
  }
}

/**
 * Returns `value` clamped between `min` and `max`.
 * @param {Number} value
 * @param {Number} [min=0]
 * @param {Number} [max=1]
 * @returns {Number}
 * @static
 */
syngen.fn.clamp = (value, min = 0, max = 1) => {
  if (value > max) {
    return max
  }

  if (value < min) {
    return min
  }

  return value
}

/**
 * Returns whichever value, `a` or `b`, that is closer to `x`.
 * @param {Number} x
 * @param {Number} a
 * @param {Number} b
 * @returns {Number}
 * @static
 */
syngen.fn.closer = (x, a, b) => {
  return Math.abs(x - a) <= Math.abs(x - b) ? a : b
}

/**
 * Returns the closest value to `x` in the array `values`.
 * @param {Number} x
 * @param {Number[]} values
 * @returns {Number}
 * @static
 * @todo Improve performance with a version for pre-sorted arrays
 */
syngen.fn.closest = function (x, values = []) {
  return values.reduce((closest, value) => syngen.fn.closer(x, closest, value), values[0])
}

/**
 * Instantiates `octaves` noise generators of `type` with `seed` and returns a wrapper object that calculates their combined values.
 * @param {Object} options
 * @param {Number} [options.noScale=[]]
 *   Indices of arguments to not scale by frequency for higher octaves.
 * @param {Number} [options.octaves=1]
 * @param {*} options.seed
 * @param {String} options.type
 *   Valid values include `1d`, `perlin2d`, `perlin3d`, `perlin4d`, `simplex2d`, `simplex3d`, `simplex4d`.
 * @returns {Object}
 * @static
 * @todo noScale option needs a better name?
 */
syngen.fn.createNoise = ({
  noScale = [],
  seed,
  octaves = 1,
  type,
} = {}) => {
  const types = {
    '1d': syngen.tool.noise,
    perlin2d: syngen.tool.perlin2d,
    perlin3d: syngen.tool.perlin3d,
    perlin4d: syngen.tool.perlin4d,
    simplex2d: syngen.tool.simplex2d,
    simplex3d: syngen.tool.simplex3d,
    simplex4d: syngen.tool.simplex4d,
  }

  type = types[type]

  if (!type) {
    throw new Error('Incorrect type.')
  }

  octaves = Math.max(1, Math.round(octaves))

  if (octaves == 1) {
    return type.create(seed)
  }

  const compensation = 1 / (1 - (2 ** -octaves)),
    layers = []

  if (!Array.isArray(seed)) {
    seed = [seed]
  }

  for (let i = 0; i < octaves; i += 1) {
    layers.push(
      type.create(...seed, 'octave', i)
    )
  }

  // Optimize for up to 4d
  const noScale0 = noScale.includes(0),
    noScale1 = noScale.includes(1),
    noScale2 = noScale.includes(2),
    noScale3 = noScale.includes(3)

  return {
    layer: layers,
    reset: function () {
      for (let layer of this.layer) {
        layer.reset()
      }
      return this
    },
    value: function (...args) {
      let amplitude = 1/2,
        frequency = 1,
        sum = 0

      for (let i in this.layer) {
        const layer = this.layer[i]

        sum += layer.value(
          noScale0 ? args[0] : args[0] * frequency,
          noScale1 ? args[1] : args[1] * frequency,
          noScale2 ? args[2] : args[2] * frequency,
          noScale3 ? args[3] : args[3] * frequency,
        ) * amplitude

        amplitude /= 2
        frequency *= 2
      }

      sum *= compensation

      return sum
    },
  }
}

/**
 * Converts `degrees` to radians.
 * @param {Number} degrees
 * @returns {Number}
 * @static
 */
syngen.fn.deg2rad = (degrees) => degrees * Math.PI / 180

/**
 * Returns a debounced version of `fn` that executes `timeout` milliseconds after its last execution.
 * @param {Function} fn
 * @param {Number} [timeout=0]
 * @returns {Function}
 */
syngen.fn.debounced = function (fn, timeout = 0) {
  let handler

  return (...args) => {
    clearTimeout(handler)
    handler = setTimeout(() => fn(...args), timeout)
  }
}

/**
 * Adds a musical interval to `frequency` in `cents`.
 * @param {Number} frequency
 * @param {Number} [cents=0]
 *   Every 1200 represents an octave.
 *   For example, `2400` raises the frequency by two octaves.
 *   Likewise, `-100` lowers by one half-step, whereas `-1` lowers by one cent.
 * @returns {Number}
 * @static
 */
syngen.fn.detune = (frequency, cents = 0) => frequency * (2 ** (cents / 1200))

/**
 * Calculates the distance between two vectors or vector-likes.
 * @param {syngen.tool.vector2d|syngen.tool.vector3d|Object} a
 * @param {syngen.tool.vector2d|syngen.tool.vector3d|Object} b
 * @returns {Number}
 * @static
 */
syngen.fn.distance = (a, b) => Math.sqrt(syngen.fn.distance2(a, b))

/**
 * Calculates the squared distance between two vectors or vector-likes.
 * @param {syngen.tool.vector2d|syngen.tool.vector3d|Object} a
 * @param {syngen.tool.vector2d|syngen.tool.vector3d|Object} b
 * @returns {Number}
 * @static
 */
syngen.fn.distance2 = ({
  x: x1 = 0,
  y: y1 = 0,
  z: z1 = 0,
} = {}, {
  x: x2 = 0,
  y: y2 = 0,
  z: z2 = 0,
} = {}) => ((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)) + ((z2 - z1) * (z2 - z1))

/**
 * Creates a shallow copy of `definition` which has `prototype`.
 * If `definition` is a function, it will be executed with `prototype` as its argument.
 * @param {Object} prototype
 * @param {Object|Function} definition
 * @returns {Object}
 * @static
 */
syngen.fn.extend = function (prototype = {}, definition = {}) {
  if (typeof definition == 'function') {
    definition = definition(prototype)
  }

  return Object.setPrototypeOf({...definition}, prototype)
}

/**
 * Converts `decibels` to its equivalent gain value.
 * @param {Number} decibels
 * @returns {Number}
 * @static
 */
syngen.fn.fromDb = (decibels) => 10 ** (decibels / 10)

/**
 * Converts a MIDI `note` number to its frequency, in Hertz.
 * @param {Number} note
 * @returns {Number}
 * @see syngen.const.midiReferenceFrequency
 * @see syngen.const.midiReferenceNote
 * @static
 */
syngen.fn.fromMidi = (midiNote) => {
  return syngen.const.midiReferenceFrequency * Math.pow(2, (midiNote - syngen.const.midiReferenceNote) / 12)
}

/**
 * Converts `value` to an integer via the Jenkins hash function.
 * @param {String} value
 * @returns {Number}
 * @static
 */
syngen.fn.hash = (value) => {
  value = String(value)

  let hash = 0,
    i = value.length

  while (i--) {
    hash += value.charCodeAt(i)
    hash += hash << 10
    hash ^= hash >> 6
  }

  hash += (hash << 3)
  hash ^= (hash >> 11)
  hash += (hash << 15)

	return Math.abs(hash)
}

/**
 * Holds `audioParam` at its current time and cancels future values.
 * This is a polyfill for {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/cancelAndHoldAtTime|AudioParam.cancelAndHoldAtTime()}.
 * @param {AudioParam} audioParam
 * @static
 */
syngen.fn.holdParam = function (audioParam) {
  audioParam.value = audioParam.value
  audioParam.cancelScheduledValues(0)
  return this
}

/**
 * Adds a random value to `baseValue` within the range of negative to positive `amount`.
 * @param {Number} baseValue
 * @param {Number} amount
 * @returns {Number}
 * @static
 */
syngen.fn.humanize = (baseValue = 1, amount = 0) => {
  return baseValue + syngen.fn.randomFloat(-amount, amount)
}

/**
 * Adds a random gain to `baseGain` within the range of negative to positive `decibels`, first converted to gain.
 * @param {Number} baseGain
 * @param {Number} decibels
 * @returns {Number}
 * @static
 */
syngen.fn.humanizeDb = (baseGain = 1, decibels = 0) => {
  const amount = syngen.fn.fromDb(decibels)
  return baseGain * syngen.fn.randomFloat(1 - amount, 1 + amount)
}

/**
 * Returns whether rectangular prisms `a` and `b` intersect.
 * A rectangular prism has a bottom-left vertex with coordinates `(x, y, z)` and `width`, `height`, and `depth` along those axes respectively.
 * An intersection occurs if their faces intersect, they share vertices, or one is contained within the other.
 * This function works for one- and two-dimensional shapes as well.
 * @param {Object} a
 * @param {Object} b
 * @returns {Boolean}
 * @static
 * @todo Define a rectangular prism utility or type
 */
syngen.fn.intersects = ({
  depth: depth1 = 0,
  height: height1 = 0,
  width: width1 = 0,
  x: x1 = 0,
  y: y1 = 0,
  z: z1 = 0,
} = {}, {
  depth: depth2 = 0,
  height: height2 = 0,
  width: width2 = 0,
  x: x2 = 0,
  y: y2 = 0,
  z: z2 = 0,
} = {}) => {
  const between = syngen.fn.between

  const xOverlap = between(x1, x2, x2 + width2)
    || between(x2, x1, x1 + width1)

  const yOverlap = between(y1, y2, y2 + height2)
    || between(y2, y1, y1 + height1)

  const zOverlap = between(z1, z2, z2 + depth2)
    || between(z2, z1, z1 + depth1)

  return xOverlap && yOverlap && zOverlap
}

/**
 * Linearly interpolates between `min` and `max` with `value`.
 * @param {Number} min
 * @param {Number} max
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @returns {Number}
 * @static
 */
syngen.fn.lerp = (min, max, value = 0) => (min * (1 - value)) + (max * value)

/**
 * Linearly interpolates between `min` and `max` with `value` raised to `power`.
 * @param {Number} min
 * @param {Number} max
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @param {Number} [power=2]
 * @returns {Number}
 * @static
 */
syngen.fn.lerpExp = (min, max, value = 0, power = 2) => {
  return syngen.fn.lerp(min, max, value ** power)
}

/**
 * Returns a random value within the range where the lower bound is the interpolated value within `[lowMin, highMin]`, the upper bound is the interpolated value within `[lowMax, highMax]`.
 * Values are interpolated with {@link syngen.fn.lerpExpRandom|lerpExpRandom}.
 * @param {Number[]} lowRange
 *   Expects `[lowMin, lowMax]`.
 * @param {Number[]} highRange
 *   Expects `[highMin, highMax]`.
 * @param {Number} [value]
 * @param {Number} [power]
 * @returns {Number}
 * @see syngen.fn.lerpExp
 * @static
 */
syngen.fn.lerpExpRandom = ([lowMin, lowMax], [highMin, highMax], value, power) => {
  return syngen.fn.randomFloat(
    syngen.fn.lerpExp(lowMin, highMin, value, power),
    syngen.fn.lerpExp(lowMax, highMax, value, power),
  )
}

/**
 * Linearly interpolates between `min` and `max` with `value` logarithmically with `base`.
 * @param {Number} min
 * @param {Number} max
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @param {Number} [base=2]
 * @returns {Number}
 * @static
 */
syngen.fn.lerpLog = (min, max, value = 0, base = 2) => {
  value *= base - 1
  return syngen.fn.lerp(min, max, Math.log(1 + value) / Math.log(base))
}

/**
 * Linearly interpolates between `min` and `max` with `value` logarithmically with `base`.
 * This function is shorthand for `{@link syngen.fn.lerpLog|lerpLog}(min, max, 1 - value, 1 / base)` which results in curve that inversely favors larger values.
 * This is similar to but distinct from {@link syngen.fn.lerpExp|lerpExp}.
 * @param {Number} min
 * @param {Number} max
 * @param {Number} [value=0]
 *   Float within `[0, 1]`.
 * @param {Number} [base=2]
 * @returns {Number}
 * @see syngen.fn.lerpLog
 * @static
 */
syngen.fn.lerpLogi = (min, max, value, base) => {
  return syngen.fn.lerpLog(max, min, 1 - value, base)
}

/**
 * Returns a random value within the range where the lower bound is the interpolated value within `[lowMin, highMin]`, the upper bound is the interpolated value within `[lowMax, highMax]`.
 * Values are interpolated with {@link syngen.fn.lerpLogi|lerpLogi}.
 * @param {Number[]} lowRange
 *   Expects `[lowMin, lowMax]`.
 * @param {Number[]} highRange
 *   Expects `[highMin, highMax]`.
 * @param {Number} [value]
 * @param {Number} [power]
 * @returns {Number}
 * @see syngen.fn.lerpLogi
 * @static
 */
syngen.fn.lerpLogiRandom = ([lowMin, lowMax], [highMin, highMax], value) => {
  return syngen.fn.randomFloat(
    syngen.fn.lerpLogi(lowMin, highMin, value),
    syngen.fn.lerpLogi(lowMax, highMax, value),
  )
}

/**
 * Returns a random value within the range where the lower bound is the interpolated value within `[lowMin, highMin]`, the upper bound is the interpolated value within `[lowMax, highMax]`.
 * Values are interpolated with {@link syngen.fn.lerpLog|lerpLog}.
 * @param {Number[]} lowRange
 *   Expects `[lowMin, lowMax]`.
 * @param {Number[]} highRange
 *   Expects `[highMin, highMax]`.
 * @param {Number} [value]
 * @param {Number} [base]
 * @returns {Number}
 * @see syngen.fn.lerpLog
 * @static
 */
syngen.fn.lerpLogRandom = ([lowMin, lowMax], [highMin, highMax], value, base) => {
  return syngen.fn.randomFloat(
    syngen.fn.lerpLog(lowMin, highMin, value, base),
    syngen.fn.lerpLog(lowMax, highMax, value, base),
  )
}

/**
 * Returns a random value within the range where the lower bound is the interpolated value within `[lowMin, highMin]`, the upper bound is the interpolated value within `[lowMax, highMax]`.
 * Values are interpolated with {@link syngen.fn.lerp|lerp}.
 * @param {Number[]} lowRange
 *   Expects `[lowMin, lowMax]`.
 * @param {Number[]} highRange
 *   Expects `[highMin, highMax]`.
 * @param {Number} [value]
 * @param {Number} [base]
 * @returns {Number}
 * @see syngen.fn.lerp
 * @static
 */
syngen.fn.lerpRandom = ([lowMin, lowMax], [highMin, highMax], value) => {
  return syngen.fn.randomFloat(
    syngen.fn.lerp(lowMin, highMin, value),
    syngen.fn.lerp(lowMax, highMax, value),
  )
}

/**
 * Normalizes `angle` within therange of `[0, 2π]`.
 * @param {Number} angle
 * @returns {Number}
 * @static
 */
syngen.fn.normalizeAngle = (angle = 0) => {
  const tau = Math.PI * 2

  if (angle > tau) {
    angle %= tau
  } else if (angle < 0) {
    angle %= tau
    angle += tau
  }

  return angle
}

/**
 * Normalizes `angle` within the range of `[-π, +π]`.
 * @param {Number} angle
 * @returns {Number}
 * @static
 */
syngen.fn.normalizeAngleSigned = (angle) => {
  return syngen.fn.normalizeAngle(angle) - Math.PI
}

/**
 * Returns a cancelable promise that resolves after `duration` milliseconds.
 * @param {Number} [duration=0]
 * @returns {Promise}
 *   Has a `cancel` method that can reject itself prematurely.
 * @static
 */
syngen.fn.promise = (duration = 0) => {
  const scope = {}

  const promise = new Promise((resolve, reject) => {
    scope.reject = reject
    scope.resolve = resolve
  })

  const timeout = setTimeout(scope.resolve, duration)

  promise.reject = function (...args) {
    scope.reject(...args)
    clearTimeout(timeout)
    return this
  }

  promise.resolve = function (...args) {
    scope.resolve(...args)
    clearTimeout(timeout)
    return this
  }

  promise.catch(() => {})

  return promise
}

/**
 * Calculates the real solutions to the quadratic equation with coefficients `a`, `b`, and `c`.
 * @param {Number} a
 * @param {Number} b
 * @param {Number} c
 * @returns {Number[]}
 *   Contains only real solutions. May be empty.
 * @static
 */
syngen.fn.quadratic = (a, b, c) => {
  return [
    (-1 * b + Math.sqrt(Math.pow(b, 2) - (4 * a * c))) / (2 * a),
    (-1 * b - Math.sqrt(Math.pow(b, 2) - (4 * a * c))) / (2 * a),
  ].filter(isFinite)
}

/**
 * Converts `radians` to degrees.
 * @param {Number} radians
 * @returns {Number}
 * @static
 */
syngen.fn.rad2deg = (radians) => radians * 180 / Math.PI

/**
 * Ramps `audioParam` to the values in `curve` over `duration` seconds.
 * @param {AudioParam} audioParam
 * @param {Number[]} curve
 * @param {Number} [duration={@link syngen.const.zeroTime}]
 * @static
 */
syngen.fn.rampCurve = function (audioParam, curve, duration = syngen.const.zeroTime) {
  audioParam.cancelScheduledValues(0)
  audioParam.setValueCurveAtTime(curve, syngen.time(), syngen.time(duration))
  return this
}

/**
 * Exponentially ramps `audioParam` to `value` over `duration` seconds.
 * @param {AudioParam} audioParam
 * @param {Number} value
 * @param {Number} [duration={@link syngen.const.zeroTime}]
 * @static
 */
syngen.fn.rampExp = function (audioParam, value, duration = syngen.const.zeroTime) {
  syngen.fn.holdParam(audioParam)
  audioParam.exponentialRampToValueAtTime(value, syngen.time(duration))
  return this
}

/**
 * Linearly ramps `audioParam` to `value` over `duration` seconds.
 * @param {AudioParam} audioParam
 * @param {Number} value
 * @param {Number} [duration={@link syngen.const.zeroTime}]
 * @static
 */
syngen.fn.rampLinear = function (audioParam, value, duration = syngen.const.zeroTime) {
  syngen.fn.holdParam(audioParam)
  audioParam.linearRampToValueAtTime(value, syngen.time(duration))
  return this
}

/**
 * Returns a random float between `min` and `max`.
 * @param {Number} [min=0]
 * @param {Number} [max=1]
 * @returns {Number}
 * @static
 */
syngen.fn.randomFloat = (min = 0, max = 1) => {
  return min + (Math.random() * (max - min))
}

/**
 * Returns a random integer between `min` and `max`.
 * @param {Number} [min=0]
 * @param {Number} [max=1]
 * @returns {Number}
 * @static
 */
syngen.fn.randomInt = function (min = 0, max = 1) {
  return Math.round(
    this.randomFloat(min, max)
  )
}

/**
 * Returns a random sign as a positive or negative `1`.
 * @returns {Number}
 * @static
 */
syngen.fn.randomSign = () => Math.random() < 0.5 ? 1 : -1

/**
 * Returns a random key in `bag`.
 * @param {Array|Map|Object} bag
 * @returns {String}
 * @static
 */
syngen.fn.randomKey = function (bag) {
  const keys = bag instanceof Map
    ? [...bag.keys()]
    : Object.keys(bag)

  return keys[
    this.randomInt(0, keys.length - 1)
  ]
}

/**
 * Returns a random value in `bag`.
 * @param {Array|Map|Object|Set} bag
 * @returns {*}
 * @static
 */
syngen.fn.randomValue = function (bag) {
  if (bag instanceof Set) {
    bag = [...bag.values()]
  }

  const key = this.randomKey(bag)

  if (bag instanceof Map) {
    return bag.get(key)
  }

  return bag[key]
}

/**
 * Calculates the interior angle of a regular polygon with `sides`.
 * @param {Number} sides
 * @returns {Number}
 * @static
 */
syngen.fn.regularPolygonInteriorAngle = (sides) => (sides - 2) * Math.PI / sides

/**
 * Rounds `value` to `precision` places.
 * Beward that `precision` is an inverse power of ten.
 * For example, `3` rounds to the nearest thousandth, whereas `-3` rounds to the nearest thousand.
 * @param {Number} value
 * @param {Number} precision
 * @returns {Number}
 * @static
 */
syngen.fn.round = (value, precision = 0) => {
  precision = 10 ** precision
  return Math.round(value * precision) / precision
}

/**
 * Scales `value` within the range `[min, max]` to an equivalent value between `[a, b]`.
 * @param {Number} value
 * @param {Number} min
 * @param {Number} max
 * @param {Number} a
 * @param {Number} b
 * @returns {Number}
 * @static
 */
syngen.fn.scale = (value, min, max, a, b) => ((b - a) * (value - min) / (max - min)) + a

/**
 * Sets `audioParam` to `value` without pops or clicks.
 * The duration depends on the average frame rate.
 * @param {AudioParam} audioParam
 * @param {Number} value
 * @see syngen.performance.delta
 * @static
 */
syngen.fn.setParam = function (audioParam, value) {
  syngen.fn.rampLinear(audioParam, value, syngen.performance.delta())
  return this
}

/**
 * Returns a shuffled shallow copy of `array` using `random` algorithm.
 * For example, implementations could leverage {@link syngen.fn.srand|srand()} to produce the same results each time given the same seed value.
 * @param {Array} array
 * @param {Function} [random=Math.random]
 * @static
 */
syngen.fn.shuffle = (array, random = Math.random) => {
  array = [].slice.call(array)

  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]]
  }

  return array
}

/**
 * Implementation of the [generalized logistic function](https://en.wikipedia.org/wiki/Generalised_logistic_function) with configurable `slope`.
 * @param {Number} value
 * @param {Number} [slope=25]
 * @returns {Number}
 * @static
 */
syngen.fn.smooth = (value, slope = 10) => {
  // Generalized logistic function
  return 1 / (1 + (Math.E ** (-slope * (value - 0.5))))
}

/**
 * Returns a pseudo-random, linear congruential, seeded random number generator with variadic `seeds`.
 * Seeds are prepended with the global {@link syngen.seed} and concatenated with {@link syngen.const.seedSeparator}.
 * @param {...String} [...seeds]
 * @returns {syngen.fn.srandGenerator}
 * @static
 */
syngen.fn.srand = (...seeds) => {
  const increment = 1,
    modulus = 34359738337,
    multiplier = 185852,
    rotate = (seed) => ((seed * multiplier) + increment) % modulus

  let seed = syngen.fn.hash(
    syngen.seed.concat(...seeds)
  )

  seed = rotate(seed)

  /**
   * A pseudo-random, linear congruential, seeded random number generator that returns a value within `[min, max]`.
   * @param {Number} [min=0]
   * @param {Number} [max=1]
   * @returns {Number}
   * @see syngen.fn.srand
   * @type {Function}
   * @typedef syngen.fn.srandGenerator
   */
  const generator = (min = 0, max = 1) => {
    seed = rotate(seed)
    return min + ((seed / modulus) * (max - min))
  }

  return generator
}

/**
 * Calculates the musical interval between two frequencies, in cents.
 * @param {Number} a
 * @param {Number} b
 * @returns {Number}
 * @static
 */
syngen.fn.toCents = (a, b) => (b - a) / a * 1200

/**
 * Converts `gain` to its equivalent decibel value.
 * @param {Number} gain
 * @returns {Number}
 * @static
 */
syngen.fn.toDb = (gain) => 10 * Math.log10(gain)

/**
 * Converts `frequency`, in Hertz, to its corresponding MIDI note number.
 * The returned value is not rounded.
 * @param {Number} frequency
 * @returns {Number}
 * @see syngen.const.midiReferenceFrequency
 * @see syngen.const.midiReferenceNote
 * @static
 */
syngen.fn.toMidi = (frequency) => (Math.log2(frequency / syngen.const.midiReferenceFrequency) * 12) + syngen.const.midiReferenceNote

/**
 * Scales `frequency` by integer multiples so it's between `min` and `max`.
 * @param {Number} frequency
 * @param {Number} [min={@link syngen.const.minFrequency}]
 * @param {Number} [max={@link syngen.const.maxFrequency}]
 * @returns {Number}
 * @static
 */
syngen.fn.transpose = (frequency, min = syngen.const.minFrequency, max = syngen.const.maxFrequency) => {
  while (frequency > max) {
    frequency /= 2
  }

  while (frequency < min) {
    frequency *= 2
  }

  return frequency
}

/**
 * Generates a universally unique identifier.
 * @returns {String}
 * @static
 */
syngen.fn.uuid = () => {
  // SEE: https://stackoverflow.com/a/2117523
  return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
    (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
  )
}

/**
 * Wraps `value` around the range `[min, max)` with modular arithmetic.
 * Beware that `min` is congruent to `max`, so returned values will approach the limit of `max` before wrapping back to `min`.
 * A way to visualize this operation is that the range repeats along the number line.
 * @param {Number} value
 * @param {Number} [min=0]
 * @param {Number} [max=1]
 * @returns {Number}
 * @static
 */
syngen.fn.wrap = (value, min = 0, max = 1) => {
  const range = max - min

  if (value >= max) {
    return min + ((value - min) % range)
  }

  if (value < min) {
    return min + ((value + max) % range)
  }

  return value
}

/**
 * Maps `value` to an alternating oscillation of the range `[min, max]`.
 * A way to visualize this operation is that the range repeats alternately along the number line, such that `min` goes to `max` back to `min`.
 * @param {Number} value
 * @param {Number} [min=0]
 * @param {Number} [max=1]
 * @returns {Number}
 * @static
 */
syngen.fn.wrapAlternate = (value, min = 0, max = 1) => {
  const range = max - min
  const period = range * 2

  if (value > max) {
    value -= min

    if (value % period < range) {
      return min + (value % range)
    }

    return max - (value % range)
  }

  if (value < min) {
    if (Math.abs(value % period) < range) {
      return max - range + Math.abs(value % range)
    }

    return min + range - Math.abs(value % range)
  }

  return value
}