Waveform, Tone.js
The waveform of a Tone.js synth, read with an analyser every frame and drawn as a stroke, a fill or a radial shape. Click the canvas to play or stop. After Luisa Pereira's p5.js sketches for The Code of Music.
js
const style = Inputs.radio(["Stroke", "Fill", "Radial"], {label: "Style", value: "Stroke"});js
const width = 640;
const height = 320;
const analyser = new Tone.Waveform(256);
const synth = new Tone.PolySynth(Tone.Synth, {oscillator: {type: "triangle"}}).connect(analyser).toDestination();
const sequence = new Tone.Sequence(
(time, note) => synth.triggerAttackRelease(note, "8n", time),
["C4", "E4", "G4", "B4", "C5", "B4", "G4", "E4"],
"8n",
);
const renderer = cm.canvas({width, height});
const root = cm.flow().renderer(renderer);
const node = renderer.node();
let playing = false;
node.style.cursor = "pointer";
node.addEventListener("click", async () => {
await Tone.start();
playing = !playing;
if (playing) (sequence.start(0), Tone.getTransport().start());
else (sequence.stop(), Tone.getTransport().stop());
});
invalidation.then(() => {
Tone.getTransport().stop();
sequence.dispose();
synth.dispose();
analyser.dispose();
});
requestAnimationFrame(function frame() {
const values = Array.from(analyser.getValue());
const n = values.length;
root.append(cm.clear, {fill: "black"});
if (style === "Stroke") {
root.data(values).append(cm.line, {
x: (_, i) => (i / (n - 1)) * width,
y: (d) => height / 2 - (d * height) / 2,
stroke: "white",
strokeWidth: 4,
});
} else if (style === "Fill") {
// The area between the wave and the center line.
const points = [
...values.map((d, i) => [(i / (n - 1)) * width, height / 2 - (d * height) / 2]),
[width, height / 2],
[0, height / 2],
];
root.datum(points).append(cm.polygon, {x: (d) => d.map((p) => p[0]), y: (d) => d.map((p) => p[1]), fill: "white"});
} else {
const r = (d) => height / 4 + (d * height) / 4;
const a = (i) => (i / n) * Math.PI * 2;
root.datum(values).append(cm.polygon, {
x: (V) => V.map((d, i) => width / 2 + Math.cos(a(i)) * r(d)),
y: (V) => V.map((d, i) => height / 2 + Math.sin(a(i)) * r(d)),
fill: "white",
});
}
if (!playing) {
root.append(cm.text, {
text: "Click to play",
x: width / 2,
y: height - 20,
fill: "#999",
fontSize: 14,
fontFamily: "sans-serif",
textAlign: "center",
});
}
root.render();
requestAnimationFrame(frame);
});
document.body.append(node);