FFT, Tone.js
The spectrum of a Tone.js synth from a fast Fourier transform, on a log frequency scale, with the overall level from a meter on the right. Click the canvas to play or stop. After Luisa Pereira's p5.js sketches for The Code of Music.
js
const width = 640;
const height = 320;
const meterWidth = 24;
const fft = new Tone.FFT(512);
const meter = new Tone.Meter({normalRange: true, smoothing: 0.8});
const synth = new Tone.PolySynth(Tone.Synth, {oscillator: {type: "sawtooth"}, volume: -12})
.fan(fft, meter)
.toDestination();
const sequence = new Tone.Sequence(
(time, chord) => synth.triggerAttackRelease(chord, "4n", time),
[
["C3", "E4", "G4"],
["A2", "C4", "E4"],
["F2", "A3", "C4"],
["G2", "B3", "D4"],
],
"2n",
);
const x = d3.scaleLog([1, 512], [0, width - meterWidth - 8]);
const y = d3.scaleLinear([-100, 0], [height, 0]).clamp(true);
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();
fft.dispose();
meter.dispose();
});
requestAnimationFrame(function frame() {
const values = Array.from(fft.getValue()).slice(1);
const level = meter.getValue();
const points = [[x(1), height], ...values.map((d, i) => [x(i + 1), y(d)]), [x(values.length), height]];
root.append(cm.clear, {fill: "black"});
root.datum(points).append(cm.polygon, {x: (d) => d.map((p) => p[0]), y: (d) => d.map((p) => p[1]), fill: "white"});
root.append(cm.rect, {
x: width - meterWidth,
y: height * (1 - Math.min(1, level * 2)),
width: meterWidth,
height: height * Math.min(1, level * 2),
fill: "#00adb5",
});
if (!playing) {
root.append(cm.text, {
text: "Click to play",
x: width / 2,
y: height / 2,
fill: "#999",
fontSize: 14,
fontFamily: "sans-serif",
textAlign: "center",
});
}
root.render();
requestAnimationFrame(frame);
});
document.body.append(node);