Learning Goals 5 min
By the end of this lesson you will be able to:
- Combine the RGB LED (L01-30 wiring), the piezo buzzer (L01-14 wiring), and a single shared GND bus onto one breadboard so that one sketch can drive both the lights and the sound.
- Store an entire device's "personality" as two pieces of pure data — a melody (two parallel arrays from L01-33) and a colour palette (a 2D array from L01-34) — so that swapping themes is a data edit, not a code edit.
- Write a smooth colour transition that fades the RGB LED from one palette entry to the next by interpolating each of the three channels independently, and a startup jingle that plays once before the mood lamp kicks in.
Warm-Up 10 min
Cluster E gave you light (L01-30, L01-31) and sound (L01-32), then taught you to store sequences of either as data (L01-33, L01-34). Today you bolt them together into one small finished thing — a desk lamp that plays a tune when you power it on and then gently cycles through a colour palette of your choice. The whole point: every detail of how it looks and sounds is in your data, not in the code.
Quick-fire puzzle
Every device you own has a startup sequence. Your laptop plays a chime and shows a logo. A car beeps and lights up the dashboard. A microwave shows "88:88" and ticks. A games console plays a 5-second jingle while its logo zooms in.
- Why do device-makers bother with these — what do they tell the user?
- Pick any startup sequence you can recall. How many things happen at once during it — sound, image, animation, status lights?
- If you had to recreate the same startup with just one RGB LED and one piezo, what would you keep? What would you drop?
Reveal the answer
- They confirm that the device is alive (something is happening on power-on), establish identity (the chime is recognisable), and give the hardware time to come up before the user is asked to do anything.
- Often four or five: a colour, a sound, an animation, a logo, a status indicator. Real products coordinate them carefully so they feel like one experience.
- You'd keep one signature colour fade and one short jingle. The whole project today is exactly that minimum-viable startup sequence — but you build it from scratch.
The two-data-blocks structure (melody data + palette data) you're about to write is the same one Nintendo, Apple and Tesla all use for theirs. They run on bigger chips with millions of pixels and stereo audio; today your version is one LED and one buzzer; but the shape of the code is the same.
New Concept — two data blocks, two players 20 min
The big idea — the "theme" is data
Today's project has two halves — sound and light — and each half follows the same structure you've been practising: a chunk of data describing the experience, plus a small player function that reads it.
- Melody = two parallel 1D arrays (frequencies + durations), played by
playMelody(…). The shape from L01-33. - Palette = one 2D array (N rows × 3 columns of R, G, B), played by
cyclePalette(…). The shape from L01-34, only the columns are colour channels instead of LEDs.
Together: change the melody arrays for a new tune. Change the palette array for a new mood. The player code never moves.
The palette as a 2D array
Each row of the palette is one colour, stored as three numbers from 0–255 (the format from L01-31). The number of rows is up to you — 4 is calm, 6 is fun, 12 is "rainbow".
const int NUM_COLOURS = 6;
int palette[NUM_COLOURS][3] = {
{ 255, 0, 0 }, // red
{ 255, 100, 0 }, // orange
{ 255, 255, 0 }, // yellow
{ 0, 255, 0 }, // green
{ 0, 100, 255 }, // sky blue
{ 128, 0, 200 } // purple
};Reading: palette[2][0] is the red component of the third colour (which is 255 — yellow has full red); palette[2][1] is the green component (also 255); palette[2][2] is the blue (0). The convention is "row picks the colour, column picks the channel".
The fade — linear interpolation, one channel at a time
L01-31 showed you how to cross-fade between two colours by walking v from 0 to 255 in a for loop. Today you do the same trick but more generally: given any starting colour and any ending colour, ramp each channel smoothly between its two values over N steps.
The formula for one channel:
// At step s out of total steps, channel value is:
value = from + (to - from) * s / steps;When s = 0, the formula gives from. When s = steps, it gives to. In between, it walks evenly from one to the other. Do this for R, G and B at the same value of s and you get a smooth colour transition. This is called linear interpolation — the foundation of every colour fade, motion tween and slider animation in software.
The fade-between-two-colours helper
void fadeBetween(int from[3], int to[3], int steps, int stepMs) {
for (int s = 0; s <= steps; s = s + 1) {
int r = from[0] + (to[0] - from[0]) * s / steps;
int g = from[1] + (to[1] - from[1]) * s / steps;
int b = from[2] + (to[2] - from[2]) * s / steps;
setColour(r, g, b);
delay(stepMs);
}
}One function, three channels, any two endpoints. With steps = 100 and stepMs = 10, each fade takes about a second; with steps = 50, stepMs = 40, it's slower and chunkier. Total time of any fade ≈ steps × stepMs.
The palette cycle — walk the palette, fading consecutive pairs
void cyclePalette() {
for (int c = 0; c < NUM_COLOURS; c = c + 1) {
int next = (c + 1) % NUM_COLOURS;
fadeBetween(palette[c], palette[next], 100, 10);
}
}Two ideas tucked in here: palette[c] is a whole row of the 2D array — a 3-element list — passed to fadeBetween as a 1D array. And (c + 1) % NUM_COLOURS uses modulo (the L01-29 trick) so that after the last colour, next wraps back to colour 0 — closing the loop into a never-ending cycle.
Sound + light at the same time — avoid the timer clash
L01-32 warned that tone() on the Uno breaks analogWrite on pins 3 and 11 because they share a timer. Pin 11 is exactly where the worked example wires the blue channel. The simplest fix is to not run them at the same time: the jingle plays once in setup() (with the LED dark), then the colour cycle takes over in loop() (with the buzzer silent). No overlap, no glitches. If you want them concurrent, the Mini-Challenge below shows how to handle the small flicker honestly.
Why it matters
The pattern you'll write today — "two pieces of data describing the device's personality, plus two small players that read them" — is the basic shape of every connected device's "theme" code, from your phone's ringtone-and-wallpaper system to the LED bar on a wireless router. Industrial-scale versions of this same code structure ship in tens of millions of products. Yours is the smallest interesting one.
Worked Example — wire it, write it, watch it 25 min
The wiring
Two components on one breadboard: the RGB LED across the trough (L01-30 layout) and the piezo buzzer (L01-14 layout) at the right-hand end. Both their GND legs jumper to the − rail, which carries one shared GND wire back to the Arduino. Total Arduino connections: D8 (buzzer), D9 (R), D10 (G), D11 (B), GND.
The full sketch
// Mood lamp + theme tune — Cluster E project
const int RED_PIN = 9;
const int GREEN_PIN = 10;
const int BLUE_PIN = 11;
const int BUZZER_PIN = 8;
// ===== JINGLE DATA (edit me!) =====
const int NUM_NOTES = 7;
int jingleFreq[NUM_NOTES] = { 523, 659, 784, 659, 523, 659, 784 };
int jingleDur[NUM_NOTES] = { 200, 200, 200, 200, 200, 200, 600 };
// ===== PALETTE DATA (edit me!) =====
const int NUM_COLOURS = 6;
int palette[NUM_COLOURS][3] = {
{ 255, 0, 0 }, // red
{ 255, 100, 0 }, // orange
{ 255, 255, 0 }, // yellow
{ 0, 255, 0 }, // green
{ 0, 100, 255 }, // sky blue
{ 128, 0, 200 } // purple
};
// ===== HELPERS =====
void setColour(int r, int g, int b) {
analogWrite(RED_PIN, r);
analogWrite(GREEN_PIN, g);
analogWrite(BLUE_PIN, b);
}
void playMelody(int freqs[], int durs[], int n) {
for (int i = 0; i < n; i = i + 1) {
tone(BUZZER_PIN, freqs[i], durs[i]);
delay(durs[i] + 50);
}
}
void fadeBetween(int from[3], int to[3], int steps, int stepMs) {
for (int s = 0; s <= steps; s = s + 1) {
int r = from[0] + (to[0] - from[0]) * s / steps;
int g = from[1] + (to[1] - from[1]) * s / steps;
int b = from[2] + (to[2] - from[2]) * s / steps;
setColour(r, g, b);
delay(stepMs);
}
}
void cyclePalette(int stepsPerFade, int stepMs) {
for (int c = 0; c < NUM_COLOURS; c = c + 1) {
int next = (c + 1) % NUM_COLOURS;
fadeBetween(palette[c], palette[next], stepsPerFade, stepMs);
}
}
// ===== TOP LEVEL =====
void setup() {
pinMode(RED_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
pinMode(BLUE_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
setColour(0, 0, 0); // LED dark during jingle
playMelody(jingleFreq, jingleDur, NUM_NOTES); // startup tune
}
void loop() {
cyclePalette(100, 15); // each fade ~1.5 s; full cycle ~9 s
}Walk through what each part does
- Pin constants name the four wires.
- Jingle data — seven frequencies and seven durations. The shape from L01-33. Today's jingle is a tiny "rising fanfare" — high C, E, G, then back and finish on G. Total runtime about 1.7 seconds.
- Palette data — six colours laid out as a 2D array, each row an RGB triple. The shape from L01-34, only the columns now mean channels not LEDs.
- Four helper functions in a tight stack:
setColour(lowest),playMelodyandfadeBetween(one up),cyclePalette(highest). Each calls the layer below. setup()sets pin modes, blanks the LED, plays the jingle once. By the time it returns, the buzzer has gone quiet.loop()is one line: cycle the palette forever. With 100 steps per fade × 15 ms per step = 1500 ms per fade, and 6 fades per cycle, the LED takes ~9 seconds to walk through the whole palette.
Upload and watch
- Upload. As soon as the chip resets, the seven-note jingle plays. The LED stays dark.
- About 1.7 seconds later, the jingle finishes and the LED springs to life on the palette's first colour (red). It then smoothly fades to orange, then yellow, then green, then sky blue, then purple, then back round to red — and keeps going indefinitely.
- Press the Arduino's reset button to restart the whole sequence — jingle + palette cycle — from the top.
Why the structure is what it is
Three things make this sketch easy to personalise without writing any new logic:
- Two clearly labelled data blocks at the top. A future-you (or a friend) can change the jingle or palette without having to read or understand any helper.
- One helper per concept, named after what it does.
fadeBetweennever knows about palettes;cyclePalettenever knows about interpolation. Each piece is small enough to fit on one screen. - Sequential phasing — jingle then lamp — sidesteps the timer clash described in the watch-out. No PWM happens during the jingle; no
tonehappens during the lamp. Each subsystem has the chip's full attention when it's running.
Trace one fade on paper
Suppose cyclePalette is fading between colour 0 (red, 255, 0, 0) and colour 1 (orange, 255, 100, 0). Fill in the channel values at the moment s = 50 (halfway through), with steps = 100.
| Channel | from | to | Formula | Value at s = 50 |
|---|---|---|---|---|
| R | 255 | 255 | 255 + (255 − 255) × 50 / 100 | 255 |
| G | 0 | 100 | 0 + (100 − 0) × 50 / 100 | ____ |
| B | 0 | 0 | ____ | ____ |
If you can fill in G and B without uploading, you understand the interpolation formula — and you can derive the colour at any point of any fade in the same way.
Try It Yourself — make it yours 15 min
Goal: Replace the default jingle with one of your own. At least five notes, with at least two different durations. Use the L01-32 frequency table.
Plan: only the two jingle arrays and the NUM_NOTES constant change. The helpers, the palette, and setup()/loop() stay exactly as they are.
const int NUM_NOTES = 5; // or more
int jingleFreq[NUM_NOTES] = { ___, ___, ___, ___, ___ };
int jingleDur[NUM_NOTES] = { ___, ___, ___, ___, ___ };Questions:
- How many lines of code did you change to make the device "yours" musically? ____
- If your jingle is 12 seconds long, when does the LED actually start fading? ____
- What change would make the jingle play twice at startup instead of once? ____ (Hint:
setup()already has the call once.)
Goal: Build a new palette of your own. Pick a theme — beach (sand, sea, sky), forest (greens and browns), team colours, a flag — and encode at least four palette entries that feel like that theme. Use any RGB colour picker (most browsers, image editors and even Wikipedia "infobox" colour swatches show them).
const int NUM_COLOURS = 4; // or more
int palette[NUM_COLOURS][3] = {
{ ___, ___, ___ }, // name your colour 1
{ ___, ___, ___ }, // name your colour 2
{ ___, ___, ___ }, // name your colour 3
{ ___, ___, ___ } // name your colour 4
};Questions:
- Some palette pairs look better fading than others. Pick the pair that produces the worst looking transition in your palette. Why is it ugly? ____ (Hint: passing through "muddy" colours.)
- If you wanted a palette that looped more naturally — i.e. the last colour fades back to the first one nicely — what should you make the first and last entries? ____
- The fade time per pair is
stepsPerFade × stepMs. The total cycle time is that timesNUM_COLOURS. What two-number tweak would halve your total cycle time? ____
Goal: Add a mood-only switch. Some mornings you don't want the jingle; you just want the lamp. Replace the constant call in setup() with a small input read: if a button is held during boot, skip the jingle.
Plan: wire one button to D7 with INPUT_PULLUP (the L01-17 circuit). In setup(), after setting pin modes, read the button once. If it's HIGH (not pressed), play the jingle as normal. If it's LOW (pressed), skip straight to the lamp. The user holds the button while plugging in the USB.
pinMode(7, INPUT_PULLUP);
if (digitalRead(7) == HIGH) {
playMelody(jingleFreq, jingleDur, NUM_NOTES);
}
// loop() then starts the palette cycle as beforeQuestions:
- Why once in
setup()and not every loop pass? ____ (Hint: the user is only going to "boot quietly" by holding the button at startup time, not later.) - How could you replace the silent skip with a different jingle stored as a second pair of arrays? List the new data and the new branch you'd add. ____
- What's a third behaviour the same button could trigger in a future version? ____
Mini-Challenge — sync sound and colour 10 min
"Each note picks a colour"
Bigger version: instead of the jingle and the lamp being separate phases, every note also sets the LED to the next palette colour. The lamp's transitions are no longer smooth — they snap to a new colour with each note — but the whole thing feels much more like one designed experience.
Your task:
- Make sure
NUM_COLOURS≥NUM_NOTES(or just use modulo so colours wrap if you have fewer). - Replace
playMelodywith a newplaySyncedMelodythat takes the melody data plus the palette, and for each notei: sets the LED topalette[i % NUM_COLOURS]withsetColour, plays the note withtone, and delays for the note's duration plus a small gap. - Call it in
setup()instead ofplayMelody. After it finishes,loop()kicks in with the smoothcyclePaletteas before — so you get a sharp synced opening followed by a calm continuous mood phase.
It works if:
- Every note arrives with a new colour, perfectly in sync — they feel like one event, not two.
- After the last note, the LED holds on the final palette colour for a beat, then the smooth cycle takes over.
- You're not changing the helpers
fadeBetween,cyclePaletteorsetColourat all — just the player.
Heads-up about the timer clash: while a tone is sounding, the blue channel (D11) may visibly stutter — that's the L01-32 watch-out, not a bug in your code. The notes are short (200 ms each) so the flicker is mostly imperceptible. For a glitch-free version you'd move the blue wire to D5 or D6 (free PWM pins) and update BLUE_PIN — try it if the flicker bothers you.
Reveal one valid playSyncedMelody
void playSyncedMelody(int freqs[], int durs[], int n,
int pal[][3], int palSize) {
for (int i = 0; i < n; i = i + 1) {
int c = i % palSize;
setColour(pal[c][0], pal[c][1], pal[c][2]);
tone(BUZZER_PIN, freqs[i], durs[i]);
delay(durs[i] + 50);
}
}
// In setup(), replace the playMelody call with:
playSyncedMelody(jingleFreq, jingleDur, NUM_NOTES, palette, NUM_COLOURS);The function signature shows the 2D array parameter int pal[][3] — first dimension blank (because we pass palSize separately), second dimension 3 because every palette row has three channels. Inside, i % palSize wraps the note index round the palette so a 7-note jingle on a 6-colour palette repeats the first colour for the seventh note rather than reading off the end of the array. This is the same modulo wrap-around trick from cyclePalette. Nothing else in the sketch changes — the four helpers, the data blocks, and loop() all stay put.
Recap 5 min
Cluster E's project is a tiny "device with a personality": one RGB LED and one piezo, two data blocks describing the look and sound, and four small helpers that play them. The melody arrays are the L01-33 shape; the palette 2D array is the L01-34 shape adapted to colour channels. The fade helper uses the linear-interpolation formula from + (to − from) × s / steps to walk smoothly between any two colours. Phasing the jingle in setup() and the lamp in loop() sidesteps the buzzer/PWM timer clash. Swap either data block for a fresh theme — the helper code never moves. That's the whole shape of every device-personality system on the market.
- Theme / personality
- A coordinated set of look and sound that makes a device feel like itself. In code: two data blocks — a melody and a palette — that fully describe the device's signature. Swap the data, swap the personality.
- Palette
- A 2D array of colour values — rows are colours, columns are R/G/B channels. The shape of every colour picker, gradient editor, and design system on a computer.
- Linear interpolation
- The formula
from + (to − from) × s / stepsthat walks evenly between two values assgoes from 0 tosteps. Apply per channel for smooth colour fades; apply per pixel for the same idea in image processing. - Cross-channel fade
- A colour transition that interpolates R, G and B independently between the same two colours at the same value of
s. Today'sfadeBetweenin one function call. - Sequential phasing
- Letting one subsystem finish before another starts, to avoid hardware conflicts (here: jingle in
setup()before lamp inloop()to avoid thetone/PWM timer clash). - Wrap-around (modulo)
- Using
(c + 1) % NUM_COLOURSto make a counter loop back to 0 after the last entry. The same trick from L01-29's animation engine; here it closes the palette cycle into a continuous loop.
Homework 5 min
Your signature device. Personalise the worked-example sketch with both a custom jingle (at least 8 notes, two different durations) and a custom palette (at least 5 colours, themed around something meaningful — your favourite season, a sports team, a video game, a country flag). The structure of the code shouldn't change; only the two data blocks should.
- Pick a theme and write it down at the top of the file as a comment, like
// "Sunset over the sea" — Aljay's signature. - Encode the jingle. The 8 notes don't have to be a famous tune; you can compose your own — just sequence frequencies you find pleasing.
- Encode the palette. Use a free online colour picker (search "rgb color picker") to find good values. Add a one-word comment after each palette row naming the colour.
- Upload and listen/watch. Make small adjustments (a different duration here, a more saturated colour there) until it feels like your device.
Also: a design reflection on paper.
- Why are jingles in real products almost always under 5 seconds? ____ (Hint: think about the second time you boot.)
- You wrote two data blocks (jingle + palette). What's a third theme-able thing your device could have? Describe in one sentence what its data would look like. ____
- Your
fadeBetweenuses 100 steps × 15 ms = 1500 ms per pair. A 60-colour palette would mean 90 seconds per cycle. At what palette size do you think a viewer would no longer recognise it's "cycling" vs. "drifting"? ____ - Look at one device near you that has a startup sequence. What does its sequence tell you about who designed it — the company's personality, their target audience, the era it was made in? ____
Bring back next class:
- The saved
.inofile (call itmy-signature-device). - A 30-second phone video showing one full sequence — your jingle, the LED waking up, and at least one full palette cycle.
- Your four written reflection answers, in your notebook.
Heads up for next class: Cluster E is done. L01-36 "Light-Dependent Resistors" opens Cluster F — the first lesson on sensors. Until now your sketches have only been able to output (LEDs, buzzer, serial print) or take simple button input. With an LDR and analogRead, the Arduino will finally start sensing the world — measuring brightness as a number from 0 to 1023 and reacting to it.