Learning Goals 5 min
By the end of this lesson you will be able to:
- Declare an array with
int notes[] = {262, 294, 330, ...};, read individual values with the indexing syntaxnotes[0],notes[1], and walk every value with aforloop. - Use a length constant like
const int NUM_NOTES = 7;as the upper bound of your loops, so the same loop works for melodies of any size. - Use two parallel arrays — one for frequencies, one for durations — to store an entire tune as data, and a single small player function as code that can play any melody you hand it.
Warm-Up 10 min
L01-32 ended with a real pain point: every note of every tune had to be written out as its own playNote(...) line. Seven notes for Twinkle Twinkle. Forty-two for "Ode to Joy". Hundreds for any real piece of music. There's a better way — and it's the same idea that powers spreadsheets, playlists, video frames, and just about every other "list of things" inside a computer.
Quick-fire puzzle
Imagine you have to write down the shopping list for ten different recipes. Recipe 1 needs 5 items, Recipe 2 needs 8, Recipe 3 needs 12. You could either:
- Option A: Carry a separate notebook page for each recipe with the items written one per line.
- Option B: Carry one notebook of all the items numbered 1, 2, 3, 4 …, and for each recipe just write down which item-numbers you need ("Recipe 1 needs items 3, 7, 12, 18, 22").
Which option do you think saves more paper and writing — A or B? Why? And which option lets you easily reorder a recipe?
Reveal the answer
Option B is the array idea. The items are stored once in a numbered list; the recipes refer to them by their position number. Each recipe is now just a short sequence of numbers rather than a full ingredient list — much less writing. Reordering a recipe is also just rewriting its sequence; the master list never changes.
That's exactly what an array lets you do in code. Today's lesson takes the seven notes of Twinkle Twinkle (which you typed seven times yesterday) and stores them once as a list — then plays them with a tiny for loop that walks the list from start to finish. Changing the melody becomes editing one line of data. Adding a new song means adding one new list. Reordering, reversing, looping, doubling speed — all become tiny changes to the player code, with the data left untouched.
New Concept — arrays in three minutes 15 min
The big idea — one variable that holds many values
Up until now every variable held one thing. int n = 5; stores one number. char c = 'a'; stores one letter. An array is a variable that holds a fixed-size list of values, all the same type, addressed by their position number (called the index).
int notes[] = { 262, 294, 330, 349, 392 };
// ^idx 0 1 2 3 4That one line creates a list of five ints named notes. The values are 262 at position 0, 294 at position 1, and so on. The square brackets [] say "this is an array, not a single value", and the curly braces { … } hold the comma-separated initial values.
Reading a value — the index syntax
Get one value out with notes[i] — the array name followed by the index in square brackets. Indexes start at 0, not 1.
Serial.println(notes[0]); // prints 262 (the first note)
Serial.println(notes[4]); // prints 392 (the fifth and last)
int middle = notes[2]; // middle now holds 330Zero-indexed lists feel weird the first ten times. After that you stop noticing. Every modern programming language (Python, Java, JavaScript, C, C++, Go, Rust, Swift…) indexes from zero.
The length constant — write it once, use it everywhere
You always need to know how many items are in the array. The cleanest way is to declare a constant alongside the array and use it everywhere:
const int NUM_NOTES = 5;
int notes[NUM_NOTES] = { 262, 294, 330, 349, 392 };Now any code that walks notes uses NUM_NOTES as its upper bound. If you add or remove a note, you update the constant in one place; every loop using it automatically picks up the new length.
Walking the array — the canonical for loop
The pattern that handles 90% of array uses you'll ever write:
for (int i = 0; i < NUM_NOTES; i = i + 1) {
// do something with notes[i]
}Three things make this loop right:
- Start at 0, the first valid index.
- Stop before NUM_NOTES (note the
<, not<=) — the last valid index isNUM_NOTES - 1. - Add 1 each pass — so
ivisits every index in order.
Together these guarantee the loop touches every value exactly once, and never reads past the end of the array. Reading past the end is one of the most famous bugs in all of computing — at best you get garbage, at worst you crash. The < NUM_NOTES condition is what protects you.
Parallel arrays — two lists, same length, paired by index
A melody isn't just frequencies — each note also has a duration. The clean way to store that is two arrays of the same length, where frequencies[i] and durations[i] are the two pieces of information for note number i:
const int NUM_NOTES = 7;
int frequencies[NUM_NOTES] = { 262, 262, 392, 392, 440, 440, 392 };
int durations[NUM_NOTES] = { 400, 400, 400, 400, 400, 400, 800 };
// Twin- -kle, Twin- -kle, lit- -tle staaarNote 0 is the "Twin-" syllable at 262 Hz for 400 ms. Note 6 is the held "staaar" at 392 Hz for 800 ms. Same index → same note. Walk them with one for loop and you're playing music.
Separating data from code
Before today, every melody had to be hand-written as a sequence of tone + delay calls. The notes and the playing logic were tangled together. Arrays let you split them apart:
- The data is the two arrays — pure information about what to play.
- The code is a single
forloop and a small helper — pure logic about how to play it.
Swap in a different song? Edit only the data. Want to play every melody twice as fast? Edit only one line in the code. This split — data here, code there — is one of the single most important ideas in programming, and it shows up at every scale: from a five-note tune to a hundred-thousand-song streaming service.
Why it matters
Arrays are the most common data structure in the entire industry. Every list you've ever scrolled through — your phone's contacts, your texts, a Wikipedia article's references, the rows of a spreadsheet, the frames of a YouTube video — is some flavour of array. The note arrays you write today are the same shape as the frame arrays a video game uses, the channel arrays an oscilloscope uses, the password-hash array a login server uses. Once arrays click, you can read code in any language.
Worked Example — Twinkle as data 20 min
Take your L01-32 mini-challenge Twinkle Twinkle sketch — the one with seven playNote lines in setup() — and rewrite it using arrays. Same wiring (piezo on D8), same audible result, much less code per song, and a player that can handle any melody.
Step 1 — Store the melody as two arrays
// Twinkle Twinkle — stored as data
const int BUZZER_PIN = 8;
const int NUM_NOTES = 7;
int frequencies[NUM_NOTES] = { 262, 262, 392, 392, 440, 440, 392 };
int durations[NUM_NOTES] = { 400, 400, 400, 400, 400, 400, 800 };
void playNote(int freq, int duration) {
tone(BUZZER_PIN, freq, duration);
delay(duration + 50);
}
void setup() {
pinMode(BUZZER_PIN, OUTPUT);
for (int i = 0; i < NUM_NOTES; i = i + 1) {
playNote(frequencies[i], durations[i]);
}
}
void loop() { }Step 2 — Upload and listen
Upload. The Arduino plays the first line of Twinkle Twinkle exactly once, then stops — same audible output as the L01-32 version, but inside the sketch:
- The melody now lives on three lines (the constant + two arrays), not seven scattered
playNotecalls. - The player logic is one
forloop with one line inside it — short enough to read in a glance. - To change a note, edit one number. To add a note, append to both arrays and bump
NUM_NOTES.
Step 3 — Swap melodies without changing the player
This is the payoff. Replace just the data — the two arrays and the constant — with the Happy Birthday intro from yesterday's homework. The playNote helper, the for loop, the wiring: untouched.
// Same player, different song — Happy Birthday intro
const int NUM_NOTES = 6;
int frequencies[NUM_NOTES] = { 262, 262, 294, 262, 349, 330 };
int durations[NUM_NOTES] = { 300, 200, 500, 500, 500, 1000 };Upload again. Now you hear "Happy birthday to you" instead of Twinkle. Six numbers replaced, six numbers replaced — that's the whole change. The sketch's player logic doesn't know or care which song it's playing; it just walks the arrays. This is the practical superpower of separating data from code.
Step 4 — A general-purpose player function
To make the split even cleaner, extract the loop into its own function that takes both arrays plus the count:
void playMelody(int freqs[], int durs[], int n) {
for (int i = 0; i < n; i = i + 1) {
playNote(freqs[i], durs[i]);
}
}Now setup() just calls playMelody(frequencies, durations, NUM_NOTES);. Two songs, one player, two clean calls. (When you pass an array to a function, you write the type as int freqs[] in the parameter list — the array isn't copied; the function works on the original.) This player is fully reusable across every song-themed sketch you'll write from here on.
Trace the loop on paper
For the Twinkle arrays above, fill in what happens on each pass of the for loop.
| Pass | i | frequencies[i] | durations[i] | What plays |
|---|---|---|---|---|
| 1 | 0 | 262 | 400 | Twin- |
| 2 | 1 | ____ | ____ | ____ |
| 3 | 2 | ____ | ____ | ____ |
| … | … | … | … | … |
| 7 | 6 | ____ | ____ | ____ (held final note) |
| 8 | — | — | — | loop exits because i < NUM_NOTES is false |
If you can fill in passes 2, 3 and 7 without uploading, the array-and-loop combo has clicked.
Try It Yourself — three array tricks 20 min
Goal: Encode and play a short tune of your own — at least five notes. Use the worked example's player code; just change the data.
Plan: pick a tune you can hum (a video-game jingle, a TV theme, "Mary Had a Little Lamb", the chorus of a song). Look up which letter-named notes it uses, convert to frequencies using L01-32's table (or do octave maths), and pick a duration for each — most notes 300 ms, longer notes 600 ms.
// Your own tune — fill in the numbers
const int NUM_NOTES = 5; // or more
int frequencies[NUM_NOTES] = { ___, ___, ___, ___, ___ };
int durations[NUM_NOTES] = { ___, ___, ___, ___, ___ };Questions:
- How many lines of code did you change compared to writing the same tune as separate
playNotecalls? ____ - If you accidentally typed only 4 numbers inside
frequenciesbut keptNUM_NOTES = 5, what would happen on the last loop pass? ____ (Hint: re-read the watch-out about reading past the end.) - The Arduino C++ compiler can auto-fill the length if you leave the brackets empty:
int notes[] = {…};. Why does the lesson use the explicit[NUM_NOTES]form instead? ____ (Hint: the loop still needs a number, and having it once at the top is clearer than counting commas.)
Goal: A tempo control. Add a const float TEMPO = 1.0; at the top of your Twinkle sketch. Inside the player loop, multiply each duration by TEMPO before passing it to playNote. Set TEMPO = 2.0 for half-speed, 0.5 for double-speed.
Plan: change just the playNote call inside the loop:
playNote(frequencies[i], durations[i] * TEMPO);Try TEMPO = 0.5, 1.0, 2.0, 4.0 and listen to the song's pace change without touching the underlying note values.
Questions:
- Why is this so much easier than going through every duration in the array and multiplying it by hand? ____
- What happens at
TEMPO = 0.1? Is the tune still recognisable? ____ - How would you make pitch change instead of tempo — e.g. play the same tune one octave higher? ____ (Hint: octaves and the
* 2rule from L01-32.)
Goal: A reverse player. Add a second function that plays the same arrays backwards. Then in setup(), play the melody forward, then backward, then stop.
Plan: a normal player walks i from 0 to NUM_NOTES - 1. A reverse player walks i from NUM_NOTES - 1 down to 0. The trick is getting the loop bounds right.
void playForward(int freqs[], int durs[], int n) {
for (int i = 0; i < n; i = i + 1) {
playNote(freqs[i], durs[i]);
}
}
void playReverse(int freqs[], int durs[], int n) {
for (int i = n - 1; i >= 0; i = i - 1) {
playNote(freqs[i], durs[i]);
}
}Questions:
- Why does the reverse loop start at
n - 1and not atn? ____ - What happens if you accidentally wrote
i > 0instead ofi >= 0as the condition? Which note gets skipped? ____ - Twinkle Twinkle reversed sounds… weird. Is there a famous tune that sounds roughly the same forwards and backwards? ____ (Hint: think about repeating phrases — Row Row Row Your Boat is a famous one.)
Mini-Challenge — one scale, three plays 10 min
"Play the same data three different ways"
Store the C-major scale as a single array of just frequencies — no durations array this time, because every note will be the same length (300 ms). Then in setup(), play it three ways using the same array:
- Forward at normal tempo — C4 D4 E4 F4 G4 A4 B4 C5, 300 ms each.
- Backward at normal tempo — C5 B4 A4 G4 F4 E4 D4 C4, 300 ms each.
- Forward at double speed — C4…C5, but each note 150 ms.
The point: the data (eight numbers in one array) is written once. The player code is reused three different ways. This is the data/code split in its sharpest form.
Your task:
- Declare
const int NUM_NOTES = 8;and oneint scale[NUM_NOTES] = { 262, 294, 330, 349, 392, 440, 494, 523 };. - Write one
playScale(int freqs[], int n, int duration, bool reverse)helper.reverse = falsewalks 0 → n-1;truewalks n-1 → 0. - In
setup(): three calls — forward at 300 ms, reverse at 300 ms, forward at 150 ms. Put a 500 ms pause between each run.
It works if:
- You hear the scale climb, then descend, then climb again twice as fast — about 6 seconds of audio in total.
- The
scalearray is written down only once in your source code, and the three plays each reuse it.
Reveal one valid sketch
// One scale, three plays — data written once
const int BUZZER_PIN = 8;
const int NUM_NOTES = 8;
int scale[NUM_NOTES] = { 262, 294, 330, 349, 392, 440, 494, 523 };
void playScale(int freqs[], int n, int duration, bool reverse) {
if (reverse) {
for (int i = n - 1; i >= 0; i = i - 1) {
tone(BUZZER_PIN, freqs[i], duration);
delay(duration + 30);
}
}
else {
for (int i = 0; i < n; i = i + 1) {
tone(BUZZER_PIN, freqs[i], duration);
delay(duration + 30);
}
}
}
void setup() {
pinMode(BUZZER_PIN, OUTPUT);
playScale(scale, NUM_NOTES, 300, false); // forward, 300 ms
delay(500);
playScale(scale, NUM_NOTES, 300, true); // reverse, 300 ms
delay(500);
playScale(scale, NUM_NOTES, 150, false); // forward, 150 ms (2x speed)
}
void loop() { }The scale array is the single source of truth. The playScale helper is the single piece of playing logic. Direction and tempo are parameters, not new code. This is what programmers mean when they say "make data, not code" — and it's the seed of every sample-based audio system, sequencer, and music library on the planet.
Recap 5 min
An array is one variable that holds many values, all the same type, addressed by an index from 0 up. Declare with int notes[N] = {...}; read with notes[i]; walk with for (int i = 0; i < N; i++). Pair two arrays of the same length to store paired information — frequency + duration, x + y, name + age — and walk them in parallel. Most importantly, arrays let you separate data (the values) from code (the logic that reads them), so you can change one without touching the other. Today's seven-line tune became three lines of data + one line of code. Tomorrow's projects scale that pattern up to hundreds of items.
- Array
- A fixed-size list of values of the same type, stored under one name and addressed by position number. The most-used data structure in programming.
- Index
- The integer position of an item within an array. Indexes start at 0. An array of length N has valid indexes 0, 1, …, N − 1.
- Length constant
- A
const intdeclared next to the array (e.g.const int NUM_NOTES = 7;) and used as the size in the declaration and the upper bound of every walking loop. Write it once, use it everywhere. - Parallel arrays
- Two (or more) arrays of the same length where the items at the same index belong together — like
frequencies[i]anddurations[i]being the two parts of "note i". Walk them together with oneforloop. - Off-by-one bug
- The classic mistake of looping one too many or one too few times.
i <= NUM_NOTESinstead ofi < NUM_NOTESreads past the end of the array. Off-by-one bugs are so common they have their own Wikipedia article. - Data vs code
- The split between the values being processed (data) and the logic doing the processing (code). Arrays make this split clean: change the values without touching the logic, and vice versa.
Homework 5 min
Your own song, in arrays. Pick a tune you know by ear — a national anthem, a movie theme, the chorus of a favourite song, a video-game jingle. Encode at least twelve notes of it as two parallel arrays (frequencies + durations), and play them in setup() using the worked-example player.
- Look up note frequencies using L01-32's table; use octave doubling/halving for notes outside it.
- Match the rhythm of the tune by picking durations: short notes ~150 ms, normal ~300 ms, long ~600 ms, very long ~1000 ms.
- Use a
const int NUM_NOTESso you can edit easily. - Add a comment above each array entry naming the syllable or beat, like the worked example did for Twinkle.
Also: a design reflection on paper.
- How long did it take you to encode the tune? Where did most of the time go — looking up note names, looking up frequencies, or finding the rhythm? ____
- Imagine your tune is 200 notes long instead of 12. Roughly how big would your source file get? Are arrays still the right tool, or would you reach for something else (file storage? a different syntax)? ____ (Hint: arrays scale fine to hundreds; thousands and you'd want a file.)
- You wrote the same player code as the worked example. If you swap the frequency array for a duration array (and vice versa) by mistake — same data, wrong slots — what would the tune sound like? ____
- The chip's flash memory on a Uno holds about 32 kilobytes. Each
inttakes 2 bytes. Roughly how many notes' worth of data could you fit if your sketch were nothing but the arrays? ____
Bring back next class:
- The saved
.inofile (call itmy-song-in-arrays). - A 30-second phone video of the tune playing on power-on, with a sticky note showing what song it is.
- Your four written reflection answers, in your notebook.
Heads up for next class: L01-34 "Multi-LED Patterns" applies the same array trick to light. You'll wire five LEDs in a row, store animation patterns as arrays of HIGH/LOW values, and use one walking loop to play them — ripple, chase, sparkle, bouncing-ball. The data/code split you saw today carries over wholesale.