Last episode I left you with a door I'd been itching to open for ages: what if the code didn't just draw, what if it could sing? We spent 117 episodes using your eyes and only your eyes. Pixels, ink, light, shape. And the whole time there was this other sense sitting right there, one that computers are freakishly good at, and we brushed past it exactly once, way back in episode 19 when we made visuals that jump to music. Today we turn that around completely. Today we make the sound. Allez, put your headphones on, we're going to make some noise :-).
I want to be honest about why this excites me so much. Drawing is wonderful, but sound hits a different part of you. A colour is nice; a chord can give you goosebumps. And the beautiful thing is that generative sound is the exact same mindset we've practised this whole series - rules, randomness, time, feedback - just pointed at your ears instead of a canvas. If you can write a particle system, you can write a piece of music. Genuinly. Let me show you.
The browser has had a sound engine baked into it for years - the Web Audio API. It's powerful, and it's also a bit of a pain to use directly. It thinks in raw oscillator nodes and gain nodes and sample rates, which is a lot of ceremony just to play a beep. So instead we use Tone.js, a framework built on top of Web Audio that speaks like a musician instead of an electrical engineer. It handles synths, effects, and - crucially - musical timing, which the raw API makes genuinly miserable.
You pull it in exactly like p5. One script tag from a CDN and you're off.
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
That's it. No build step, no npm, nothing to configure. Now Tone is a global object, the same way p5 gave us ellipse and noise. Everything below lives inside that one object.
Remember the very first thing we ever did in this series? We drew an ellipse. One line, one shape on screen, and something clicked - "oh, I made that". Sound has an exact equivalent, and it's just as small.
// the ellipse() of audio. one synth, one note, done.
const synth = new Tone.Synth().toDestination();
synth.triggerAttackRelease("C4", "8n");
Two lines. new Tone.Synth() builds a little synthesizer - a thing that makes tones. .toDestination() plugs it into your speakers (the "destination" is your audio output, like toDestination is the sound version of the canvas). Then triggerAttackRelease("C4", "8n") plays middle C for an eighth-note's worth of time. You just made a computer sing a note from scratch. That "C4" is a note name and "8n" is a musical duration - hold that thought, we'll unpack both.
There's one catch I have to mention right now or you'll be confused for an hour: browsers refuse to make sound until the user clicks something. It's an anti-annoyance rule so websites can't blast audio at you on load. So in practice, your very first line of audio code always lives inside a click handler. We'll come back to this properly, but know it's coming.
Under every synth is an oscillator - a thing that vibrates at a frequency. The shape of that vibration is what gives a sound its character, its timbre. Four classic waveforms cover most of the ground, and they each sound distinct enough that you'll recognise them instantly once you've heard them.
// the four classic waveforms. each has a totally different "colour" of sound.
const waveforms = {
sine: "pure, round, flute-like. a single clean frequency, nothing else.",
square: "hollow and buzzy, clarinet-ish. rich in odd harmonics.",
sawtooth: "bright and brassy, string-like. every harmonic, full and sharp.",
triangle: "soft and muted, like a gentle square. mellow, easy on the ear.",
};
You pick the waveform when you build the synth. Same synth code, one property changed, wildly different feel - it's very much like swapping a fill colour and watching the whole mood of a sketch change.
// switch the oscillator type and the SAME note sounds like a new instrument.
const buzzy = new Tone.Synth({
oscillator: { type: "sawtooth" } // try "sine", "square", "triangle" too
}).toDestination();
buzzy.triggerAttackRelease("A3", "4n"); // a bright, brassy A
Why do they sound different if it's the same pitch? Because timbre comes from harmonics - the quieter frequencies stacked above the main one. A sine has none, so it sounds pure and a little dull. A sawtooth has loads, so it sounds rich and biting. This is the audio cousin of texture from episode 27 - same note, different surface. Makes sense, right?
Here's a thing that took me embarrassingly long to really get. A note isn't a flat block of sound. It has a life: it starts, it swells or stabs, it holds, it fades. That shape over time is called an envelope, and it's controlled by four numbers known as ADSR.
// ADSR - the four numbers that shape a note's life over time.
const adsr = {
attack: "how long to reach full volume. 0 = instant stab, high = slow swell.",
decay: "how long to drop from the peak down to the sustain level.",
sustain: "the level it HOLDS at while the note is held (0..1, a level not a time).",
release: "how long to fade to silence AFTER you let the note go.",
};
Those four dials are astonishingly expressive. The exact same oscillator becomes a plucky harp or a slow warm pad depending purely on the envelope. Fast attack and no sustain gives you a pluck. Slow attack and a long sustain gives you a pad that breathes in. Let me show both from the identical synth.
// a PLUCK: snaps in instantly, dies fast. good for melodies & arps.
const pluck = new Tone.Synth({
envelope: { attack: 0.005, decay: 0.2, sustain: 0.0, release: 0.2 }
}).toDestination();
// a PAD: swells in slowly, holds forever, fades out gently. good for atmosphere.
const pad = new Tone.Synth({
envelope: { attack: 0.8, decay: 0.3, sustain: 0.8, release: 1.5 }
}).toDestination();
pluck.triggerAttackRelease("E4", "8n");
pad.triggerAttackRelease("E4", "2n");
Feel the difference? Same pitch, same waveform, completely different gesture. This is one of those ideas that quietly runs through all of music production, and now it's yours. If you ever wondered why a piano and an organ playing the same note feel so unlike each other, a huge part of the answer is the envelope.
Once you have a raw tone, you usually want to carve it. A filter removes some frequencies and keeps others, and it's probably the single most-used tool in electronic sound. The workhorse is the lowpass filter: it lets low frequencies through and cuts the highs, which makes a sound darker and warmer. Its opposite, the highpass, cuts the lows and leaves it thin and bright.
// a lowpass filter: keep the lows, cut the highs above the cutoff frequency.
// low cutoff = dark & muffled. high cutoff = bright & open.
const filter = new Tone.Filter(800, "lowpass").toDestination();
const rich = new Tone.Synth({ oscillator: { type: "sawtooth" } });
rich.connect(filter); // synth -> filter -> speakers
rich.triggerAttackRelease("C3", "2n");
Notice we're now chaining things: the synth goes into the filter, and the filter goes to the speakers. That's the whole mental model of audio - a signal flowing through a pipeline of boxes, each one changing it a bit. It's honestly the same thinking as the post-processing pipeline we built for visuals back in episode 43, just for sound.
And here's where it gets fun. Any of these values can be automated - swept over time - and a filter sweep is one of the most satisfying sounds in all of electronic music. You know that "wooOOOoow" opening-up sound in dance tracks? That's a cutoff frequency being ramped.
// automate the cutoff: ramp it from dark to bright over 2 seconds. that classic sweep.
const sweepFilter = new Tone.Filter(200, "lowpass").toDestination();
const drone = new Tone.Synth({ oscillator: { type: "sawtooth" } });
drone.connect(sweepFilter);
drone.triggerAttackRelease("C2", "2n");
// glide the cutoff from 200Hz up to 4000Hz over 2 seconds. the sound "opens up".
sweepFilter.frequency.rampTo(4000, 2);
That rampTo is the audio version of lerp - we're easing a value smoothly from here to there over time, exactly like we eased a circle across the canvas in episode 16. Same idea, forever useful.
Raw synths sound dry and a bit lonely. Effects are what make them sound like they live somewhere. Reverb puts the sound in a room. Delay bounces it back as echoes. Distortion adds dirt and aggression. Chorus thickens a thin sound into something lush. You add them the same way as the filter - by chaining.
// a little pipeline of effects. reverb for space, delay for echoes.
const reverb = new Tone.Reverb(3); // 3-second-ish room
const delay = new Tone.FeedbackDelay("8n", 0.4); // echoes an 8th-note apart
const lead = new Tone.Synth({ oscillator: { type: "triangle" } });
// chain: synth -> delay -> reverb -> speakers
lead.chain(delay, reverb, Tone.Destination);
lead.triggerAttackRelease("G4", "8n");
chain() is a lovely convenience - it wires a whole line of boxes together in one call, ending at Tone.Destination (the speakers). Now our little triangle note echoes and sits in a big airy room instead of buzzing dryly in your face. Reverb especially is magic; it's the difference between a note recorded in a cupboard and one sung in a cathedral. Go easy though - too much reverb and everything turns to mush, same as too much blur on a visual.
Let's clear up "C4" properly, because pitch has three ways of being written and they trip everyone up at first. Tone.js happily accepts all three.
// three ways to say the SAME pitch. tone.js understands all of them.
const pitch = {
noteName: "C4", // human-friendly. letter + octave. middle C.
midi: 60, // MIDI number. every semitone up = +1. C4 is 60.
frequency: 261.63, // raw hertz. the actual vibrations per second.
};
Note names are easiest to read. MIDI numbers are easiest to do maths on - want the note three semitones up? Add 3. That makes them perfect for generative melodies, where you're calculating pitches rather than typing them. And under it all is frequency in hertz, the physical truth. There's a tidy formula connecting MIDI to frequency, and it's worth seeing because it explains why octaves feel the way they do.
// MIDI note -> frequency in Hz. the "2^(n/12)" is why music is exponential:
// every 12 semitones (one octave) DOUBLES the frequency.
function midiToFreq(midi) {
return 440 * Math.pow(2, (midi - 69) / 12); // 69 = A4 = 440Hz, the anchor
}
midiToFreq(60); // 261.63 -> middle C
midiToFreq(69); // 440 -> concert A
midiToFreq(72); // 523.25 -> C an octave above middle C (exactly double 261.63)
That doubling-per-octave is exponential, which is why this is really the trigonometry-and-maths brain from episode 13 showing up in your ears. Pitch is geometry. I find that genuinly beautiful - the same maths that placed points on a circle now places notes in a scale.
A basic Tone.Synth plays exactly one note at a time - it's monophonic. Ask it for a chord and later notes just interrupt earlier ones. For chords and harmony we need polyphony, and Tone.js gives us PolySynth, which is basically "a synth that can spin up several voices at once".
// PolySynth: many notes at the same time. chords, harmony, lush stacks.
const poly = new Tone.PolySynth(Tone.Synth).toDestination();
// play a C major chord - three notes, all at once.
poly.triggerAttackRelease(["C4", "E4", "G4"], "2n");
Just pass an array of notes instead of one, and they all sound together. This is the door to harmony, which we'll walk through properly soon - for now, just notice how a single note feels lonely and a chord feels like a decision, an emotion. Three sine waves stacked into a major triad and suddenly there's a mood in the room.
Everything so far has been one-shot notes fired by hand. Music is about time - notes arranged in a pattern, repeating, locked to a tempo. Tone.js handles this with the Transport, a master musical clock that the whole library syncs to. You set a tempo in BPM (beats per minute), you schedule events in musical time, and you press start.
// the Transport is a global musical clock. set the tempo once.
Tone.Transport.bpm.value = 120; // 120 beats per minute, a comfy dance tempo
// "8n" = eighth note, "4n" = quarter, "1m" = one measure. MUSICAL time, not seconds.
// so the same pattern stays in time even if you change the BPM later.
The genius here is that you schedule in musical units - eighth notes, bars - not raw seconds. So if you speed up the tempo, everything still lines up perfectly. To actually repeat a pattern, the friendliest tool is Tone.Loop, which fires a callback over and over at a musical interval.
// a Loop fires its callback every "8n" (eighth note), forever, in perfect time.
const bass = new Tone.Synth({ oscillator: { type: "square" } }).toDestination();
const loop = new Tone.Loop((time) => {
// `time` is the EXACT scheduled moment - always pass it through for tight timing.
bass.triggerAttackRelease("C2", "16n", time);
}, "8n");
loop.start(0); // start the loop at the beginning of the timeline
Tone.Transport.start(); // and start the master clock rolling
That time argument matters more than it looks. Your JavaScript is a bit jittery - it can't fire at the precise microsecond a beat lands. So Tone hands you the exact scheduled time and you pass it straight to the synth, which schedules the note at that perfect moment under the hood. Ignore it and your rhythm wobbles; use it and it's tight as a drum machine. Little detail, huge difference.
For an actual pattern of different notes, Tone.Sequence is even nicer - you give it an array and it steps through, one entry per interval, looping round.
// a Sequence steps through an array, one note per "8n", looping forever.
const synth = new Tone.Synth().toDestination();
const seq = new Tone.Sequence((time, note) => {
synth.triggerAttackRelease(note, "16n", time);
}, ["C4", "E4", "G4", "E4", "A4", "G4", "E4", "C4"], "8n");
seq.start(0);
Tone.Transport.start();
There it is - eight notes marching round in a loop, a little riff generated entirely by code. Swap the array, change the tune. Fill the array from Math.random or from Perlin noise (episode 12!) and congratulations, you're writing generative music.
Right, that catch I flagged earlier. Because browsers block audio until a user interacts, you have to call Tone.start() inside a click or keypress, once, to wake the audio engine up. Everything else stays broken and silent until you do. So the standard shape of every Tone.js sketch is: build your synths up top, but only start anything from inside a click handler.
// the ALWAYS pattern: nothing sounds until the user clicks. wake audio first.
document.querySelector("#startButton").addEventListener("click", async () => {
await Tone.start(); // unlock the browser's audio engine
console.log("audio is live");
Tone.Transport.start(); // now it's safe to make noise
});
Forget this and you'll swear your code is broken when it's actually just the browser being polite. I have lost genuinly embarrassing amounts of time to a missing Tone.start(). Put a big friendly "click to begin" button on everything and save yourself the pain.
Here's the part that makes this our kind of tool and not just a music library. Back in episode 19 we read audio and drove visuals with it. Now we're generating the audio ourselves - so we can analyse our own sound and feed it straight back into a p5 sketch. The synth we wrote drives the picture we draw. Fully self-contained audiovisual, no microphone, no external track.
// tap the output with an Analyser to read our OWN synth's waveform in real time.
const analyser = new Tone.Analyser("waveform", 256); // 256 samples of the wave
Tone.Destination.connect(analyser); // listen to the master output
function draw() { // inside your p5 draw loop
const wave = analyser.getValue(); // 256 numbers, roughly -1..1, the live waveform
beginShape();
for (let i = 0; i < wave.length; i++) {
const x = map(i, 0, wave.length, 0, width);
const y = map(wave[i], -1, 1, 0, height); // the sound becomes a shape
vertex(x, y);
}
endShape();
}
This is the whole thesis of the next stretch of episodes in one snippet: the sound you generate becomes the visual you render, in perfect sync, because they share a single clock. No more chasing an external track hoping the beat lines up - you own both sides now. That's a genuinly powerful place to be.
Time to build something real, and it's the perfect first audiovisual instrument: a step sequencer. Eight steps across, a handful of notes from a pentatonic scale (which is the cheat code of music - every note sounds nice together, you literally cannot play a wrong one). You click cells to toggle them on, a Tone.Sequence walks through the columns in time, and p5 lights up the current step. Here's the sound core.
// an 8-step sequencer core. a grid of on/off cells, played in a loop.
// pentatonic scale = "no wrong notes", perfect for a first instrument.
const notes = ["C4", "D4", "E4", "G4", "A4"]; // C major pentatonic
const STEPS = 8;
// grid[row][col] = is this note on at this step? start all off.
const grid = notes.map(() => new Array(STEPS).fill(false));
const synth = new Tone.PolySynth(Tone.Synth).toDestination();
let currentStep = 0;
const seq = new Tone.Sequence((time, step) => {
currentStep = step; // remember it so p5 can draw the playhead
notes.forEach((note, row) => {
if (grid[row][step]) { // if this cell is toggled on...
synth.triggerAttackRelease(note, "16n", time); // ...play it, in perfect time
}
});
}, [0, 1, 2, 3, 4, 5, 6, 7], "8n");
And the p5 side - draw the grid, toggle a cell on click, and highlight whichever column is currently playing so you can see the music move.
// p5 side: draw the grid, click to toggle, highlight the playing column.
const CELL = 60;
function draw() {
background(20);
for (let row = 0; row < notes.length; row++) {
for (let col = 0; col < STEPS; col++) {
// the current step gets a faint highlight column
if (col === currentStep) fill(40, 60, 80);
else fill(30);
if (grid[row][col]) fill(80, 200, 255); // a lit, active note
rect(col * CELL, row * CELL, CELL - 2, CELL - 2);
}
}
}
function mousePressed() {
const col = floor(mouseX / CELL);
const row = floor(mouseY / CELL);
if (grid[row] && grid[row][col] !== undefined) {
grid[row][col] = !grid[row][col]; // flip the cell on/off
}
}
Wire it together behind a "click to start" button (remember Tone.start()), call seq.start(0) and Tone.Transport.start(), and you have a playable instrument. Click some cells, watch the playhead sweep across, hear your pattern loop. Congratulations - that is genuinly a musical toy you built from nothing, and it's the same grid-and-loop skeleton behind half the drum machines on earth.
Stretch it, occassionally, when you're bored: add a second row of a different synth for drums, let the tempo react to the mouse, or feed currentStep into a bigger p5 visual so the whole screen pulses with the beat. It's your instrument now.
Tone is a global, just like p5new Tone.Synth().toDestination() then triggerAttackRelease("C4", "8n"). It's the ellipse() of soundrampTo (which is just lerp from episode 16) and you get that classic dance-music sweepchain() them into a pipeline ending at the speakers, just like post-processing for visuals in episode 43Tone.Loop repeats a callback, Tone.Sequence steps through an array - and always pass the time argument through for tight rhythmTone.start() once inside a user gesture. Forget it and you'll think your code is broken when it's just being politeTone.Analyser lets us read it back and drive p5 visuals from it - fully self-contained audiovisual sharing one clock, no external track neededSo that's the whole foundation - oscillators, envelopes, filters, effects, timing, and the bridge back to visuals. We've gone from making things you look at to making things you hear, and the wild part is how little the thinking changed. Rules over time, randomness with taste, a signal flowing through a pipeline - it's every instinct we've built for 118 episodes, just aimed at your ears.
But right now we're mostly playing pre-made Tone.Synths and trusting them to sound good. What if you wanted to build a sound from the raw ingredients yourself - stack the oscillators, shape the harmonics, decide exactly what it is from the ground up? That's a deeper, more hands-on kind of making, and it's where we go next. Build the step sequencer first - genuinly, play with it until you've got a loop you actually like - and then come back, because next time we stop using instruments and start building them :-).
Sallukes! Thanks for reading.
X