<button type="button">Play Glockenspiel</button>
<canvas></canvas>
html {
  height: 100%;
  width: 100%;
}

body {
  align-items: center;
  box-sizing: border-box;
  display: flex;
  justify-content: center;
  height: 100%;
  margin: 0;
  padding: 1em;
  width: 100%;
}

button {
  background-color: #FFFFFF;
  border: none;
  color: #000000;
  font-size: 1em;
  padding: 0.5em;
  position: relative;
  z-index: 2;
}

canvas {
  background-color: #FF0069;
  height: 100%;
  left: 0;
  position: absolute;
  top: 0;
  width: 100%;
  z-index: 1;
}

@media (prefers-reduced-motion: no-preference) {
  body {
    align-items: flex-end;
  }
}
const audioContext = new AudioContext(),
      canvas = document.querySelector('canvas'),
      canvasContext = canvas.getContext('2d'),
      prefersReducedMotion = !window.matchMedia('(prefers-reduced-motion: no-preference)').matches

const analyzer = audioContext.createAnalyser(),
      analyzerData = new Uint8Array(analyzer.frequencyBinCount),
      mixer = audioContext.createDynamicsCompressor()

document.addEventListener('click', onClick)

mixer.connect(analyzer)
mixer.connect(audioContext.destination)

if (!prefersReducedMotion) {
  requestAnimationFrame(onFrame)

  window.addEventListener('resize', onResize)
  onResize()
}

function createFmSynth({
  carrierDetune = 0,
  carrierFrequency = 440,
  carrierType = 'sine',
  context,
  gain = 0,
  modulatorDepth = 0,
  modulatorDetune = 0,
  modulatorFrequency = 0,
  modulatorType = 'sine',
  when = 0,
} = {}) {
  if (!(context instanceof AudioContext)) {
    throw new Error('Please provide an AudioContext')
  }

  const carrier = context.createOscillator(),
        depth = context.createGain(),
        modulator = context.createOscillator(),
        output = context.createGain()

  // Set up circuit
  carrier.connect(output)
  modulator.connect(depth)
  depth.connect(carrier.frequency)

  // Set parameters
  carrier.detune.value = carrierDetune
  carrier.frequency.value = carrierFrequency
  carrier.type = carrierType
  depth.gain.value = modulatorDepth
  modulator.detune.value = modulatorDetune
  modulator.frequency.value = modulatorFrequency
  modulator.type = modulatorType
  output.gain.value = gain

  // Start oscillators
  carrier.start(when)
  modulator.start(when)

  return {
    connect: (...args) => output.connect(...args),
    disconnect: (...args) => output.disconnect(...args),
    param: {
      carrierDetune: carrier.detune,
      carrierFrequency: carrier.frequency,
      gain: output.gain,
      modulatorDepth: depth.gain,
      modulatorDetune: modulator.detune,
      modulatorFrequency: modulator.frequency,
    },
    stop: (when = context.currentTime) => {
      carrier.stop(when)
      modulator.stop(when)
    },
  }
}

function onClick() {
  audioContext.resume()

  ;[440, 554, 660, 880].forEach((frequency, index) => {
    playGlockenspiel({
      context: audioContext,
      destination: mixer,
      frequency,
      gain: 1/2,
      when: audioContext.currentTime + (index / 3),
    })
  })
}

function onFrame() {
  const xScale = canvas.width / analyzerData.length,
        yOffset = canvas.height / 2

  analyzer.getByteTimeDomainData(analyzerData)

  canvasContext.clearRect(0, 0, canvas.width, canvas.height)

  canvasContext.lineWidth = 2
  canvasContext.strokeStyle = '#FFFFFF'

  canvasContext.beginPath()
  analyzerData.forEach((sample, index) => {
    const x = index * xScale,
          y = sample * yOffset / 128

    if (index) {
      canvasContext.lineTo(x, y)
    } else {
      canvasContext.moveTo(x, y)
    }
  })
  canvasContext.stroke()

  requestAnimationFrame(onFrame)
}

function onResize() {
  canvas.height = window.innerHeight
  canvas.width = window.innerWidth
}

function playGlockenspiel({
  context,
  destination,
  frequency = 440,
  gain = 0,
  when = 0,
} = {}) {
  // Ensure future times
  when = Math.max(when, context.currentTime)

  // Instantiate synth
  const synth = createFmSynth({
    carrierFrequency: frequency,
    context,
    modulatorDepth: frequency / 2,
    modulatorDetune: 215,
    modulatorFrequency: frequency * 3,
    modulatorType: 'sawtooth',
    when,
  })

  // Filter the synth
  const filter = context.createBiquadFilter()
  filter.frequency.value = frequency * 6

  synth.connect(filter)
  filter.connect(destination || context.destination)

  // Schedule gain envelope
  const attack = when + 1/64,
        decay = attack + 1,
        release = decay + 2

  const gainParam = synth.param.gain

  gainParam.setValueAtTime(0, when)
  gainParam.exponentialRampToValueAtTime(gain, attack)
  gainParam.exponentialRampToValueAtTime(gain / 8, decay)
  gainParam.linearRampToValueAtTime(0, release)

  synth.stop(release)

  return synth
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.