<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
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.