Source: note.js

/** A musical note composed of one or more oscillators and an ADSR envelope. */
class Note {
  /** Create a note
  * @param {(AudioContext|AudioNode)} target output destination for audio
  * @param {Object} [noteParams] options that affect the note as a whole
  * @param {number} [noteParams.attack=0.02] attack time in seconds
  * @param {number} [noteParams.decay=0.02] decay time in seconds
  * @param {number} [noteParams.release=0.01] release time in seconds
  * @param {number} [noteParams.sustain=0.04] sustain level as proportion of peak level, 0 to 1 inclusive
  * @param {number} [noteParams.triggerTime=target.currentTime] time to schedule the note (see AudioContext.currentTime)
  * @param {number} [noteParams.velocity=1] note velocity, 0 to 1 inclusive
  */
  constructor (target, noteParams = {}) {
    this.context = target.context || target
    this.attack = noteParams.attack || 0.02
    this.decay = noteParams.decay || 0.02
    this.sustain = noteParams.sustain || 0.04
    this.release = noteParams.release || 0.01
    this.velocity = noteParams.velocity || 1
    this.triggerTime = noteParams.triggerTime || this.context.currentTime

    this.envGain = this.context.createGain()
    this.envGain.gain.setValueAtTime(0, this.triggerTime)
    this.envGain.gain.linearRampToValueAtTime(
      this.velocity,
      this.triggerTime + this.attack
    )
    this.envGain.gain.setTargetAtTime(
      this.sustain * this.velocity,
      this.triggerTime + this.attack,
      this.decay
    )
    this.envGain.connect(target)
    this.oscs = []
  }

  /** Trigger the release of the envelope */
  releaseNote () {
    this.stopNote(this.context.currentTime + this.release * 20)
    this.envGain.gain.setTargetAtTime(
      0,
      Math.max(this.context.currentTime, this.triggerTime + this.attack + this.decay),
      this.release
    )
  }

  /** Stop all oscillators
  * @param {number} [time=AudioContext.currentTime] When to stop playing the notes.
  */
  stopNote (time = this.context.currentTime) {
    for (const o of this.oscs) {
      o.stop(time)
    }
  }
}

/** A note whose sound is generated by multiple OscillatorNodes
* @param {Object[]} [oscParams] array of oscillator specific options
* @param {number} [oscParams[].detune=0] detune amount of the oscillator in cents
* @param {number} [oscParams[].frequency=440] frequency of the oscillator in Hertz
* @param {number} [oscParams[].gain=0.5] gain of the oscillator, 0 to 1 inclusive
* @param {string} [oscParams[].type='sine'] waveform shape, options are "sine", "square", "sawtooth", "triangle", "custom"
* @param {number[]} [oscParams[].real] the real part of the custom waveform
* @param {number[]} [oscParams[].imag] the imaginary part of the custom waveform
*/
class OscillatorNote extends Note {
  constructor (target, noteParams = {}, oscParams = [{}]) {
    super(target, noteParams)
    for (const p of oscParams) {
      let gainer = this.context.createGain()
      gainer.gain.value = p.gain || 0.5
      let osc = this.context.createOscillator()
      osc.detune.value = p.detune || 0,
      osc.frequency.value = p.frequency || 440.0
      if (p.type === 'custom') {
        osc.setPeriodicWave(this.context.createPeriodicWave(
          new Float32Array(p.real || [0, 1]),
          new Float32Array(p.imag || [0, 0]))
        )
      } else {
        osc.type = p.type || 'sine'
      }
      osc.connect(gainer).connect(this.envGain)
      osc.start()
      this.oscs.push(osc)
    }
  }
}