Trigger Sounds, Tone.js
Four instruments from Tone.js, each animating a shape while it plays. Press the keys A, S, D and F, or click the shapes. After Luisa Pereira's p5.js sketch for The Code of Music.
js
const width = 640;
const height = 240;
const step = width / 4;
const instruments = [
{key: "a", synth: new Tone.MembraneSynth().toDestination(), note: "C2"},
{key: "s", synth: new Tone.PluckSynth().toDestination(), note: "E4"},
{key: "d", synth: new Tone.FMSynth().toDestination(), note: "G3"},
{key: "f", synth: new Tone.MetalSynth({volume: -12}).toDestination(), note: "C4"},
].map((d) => ({...d, start: -Infinity}));
const renderer = cm.canvas({width, height});
const root = cm.flow().renderer(renderer);
const node = renderer.node();
async function trigger(instrument) {
await Tone.start();
instrument.synth.triggerAttackRelease(instrument.note, "8n");
instrument.start = performance.now();
}
const onkeydown = (event) => {
const instrument = instruments.find((d) => d.key === event.key);
if (instrument) trigger(instrument);
};
window.addEventListener("keydown", onkeydown);
node.addEventListener("click", (event) => trigger(instruments[Math.floor((event.offsetX / node.clientWidth) * 4)]));
node.style.cursor = "pointer";
invalidation.then(() => {
window.removeEventListener("keydown", onkeydown);
for (const {synth} of instruments) synth.dispose();
});
requestAnimationFrame(function frame(time) {
// The progress of each sound over one second, 0 when it's not playing.
const progress = instruments.map((d) => Math.min(1, (time - d.start) / 1000) % 1);
const ease = progress.map((t) => d3.easeCubicOut(t));
root.append(cm.clear, {fill: "black"});
const cells = root.data(instruments).append(cm.group, {x: (_, i) => i * step + step / 2, y: height / 2});
cells.append(cm.text, {
text: (d) => d.key.toUpperCase(),
x: 0,
y: height / 2 - 16,
fill: "#666",
fontSize: 14,
fontFamily: "sans-serif",
textAlign: "center",
});
root.append(cm.circle, {
x: step / 2 + (ease[0] - 0.5) * step * 0.8 * Math.sign(progress[0]),
y: height / 2,
r: 30 - ease[0] * 15,
fill: "white",
});
root.append(cm.circle, {
x: step * 1.5,
y: height / 2 - 60 + ease[1] * 120 * Math.sign(progress[1]),
r: 20,
fill: "white",
});
root.append(cm.rect, {
x: step * 2.5,
y: height / 2,
width: 60,
height: 60,
anchor: "center",
rotate: ease[2] * Math.PI,
fill: "white",
});
root.append(cm.circle, {
x: step * 3.5,
y: height / 2,
r: 10 + ease[3] * 50,
fill: "transparent",
stroke: "white",
strokeWidth: 4 * (1 - progress[3]),
});
root.render();
requestAnimationFrame(frame);
});
document.body.append(node);