Build a Breadboard Music Keyboard with Arduino and the switch/case Statement

  • #arduino
  • #sound
  • #tone
  • #switch-case
  • #beginner
  • #project
  • #tutorial

This is one of those projects that takes about an hour, makes a satisfying noise when it works, and teaches a handful of things that come up again and again in real Arduino sketches. Eight pushbuttons wired to eight digital inputs, a speaker on a PWM pin, and the switch/case statement tying it all together. Press a button, play a note.

The whole thing fits on a single breadboard and uses parts from any basic Arduino kit. The code is short enough to walk through completely, and the one bug that showed up during development — a mysterious erratic pin — turns out to be a useful lesson about the hardware serial interface.

What you’ll need

  • Arduino Uno or compatible
  • 8 momentary pushbuttons
  • 8× 10K resistors (pulldown, one per button)
  • 1 piezo speaker or small speaker (the speaker used here is from a Snap Circuits kit and has an internal current-limiting resistor; if yours doesn’t, add a 100–220Ω series resistor)
  • Breadboard and jumper wire

The wiring

Buttons: Each button bridges across two breadboard rows. One leg goes to the Arduino digital input through a yellow signal wire. The other leg connects to 5V. A 10K pulldown resistor connects the signal leg to GND. When the button is open, the pin reads LOW (pulled down to ground). When pressed, 5V flows through the button to the input, reading HIGH.

This is the standard active-high button circuit. Wire all eight buttons the same way, with signal lines going to digital pins D3 through D10.

Why D3–D10 and not D0–D7? The short answer: D0 and D1 are the Arduino’s hardware serial RX and TX pins. During development one of the buttons was originally wired to D1, and it was erratic — flickering HIGH even when not pressed, impossible to stabilize with different resistors or different wiring. Moved to D3 and the problem vanished. The likely cause is that D1 is driven by the USB serial interface while a sketch is running with Serial.begin() active, causing interference on the pin. D2 is generally safe but this layout skips it to keep the button group contiguous on D3–D10.

Speaker: The speaker’s positive terminal connects to D11 (a PWM-capable pin — required for tone()). The other terminal goes to GND. If your speaker doesn’t have an internal resistor, add a 100–220Ω resistor in series on the positive lead to limit current.

The code

const int speakerPin = 11;
const int delayValue = 100;  // ms to hold note after button release

void setup() {
  for (int i = 3; i <= 10; i++) {
    pinMode(i, INPUT);
    digitalWrite(i, LOW);  // explicit LOW — belt-and-suspenders
  }
  pinMode(speakerPin, OUTPUT);
}

void loop() {
  int currentTone = 0;  // reset every loop — critical

  // Read all buttons; last one pressed wins
  for (int i = 3; i <= 10; i++) {
    if (digitalRead(i) == HIGH) {
      currentTone = i;
    }
  }

  switch (currentTone) {
    case 3:  tone(speakerPin, 262); delay(delayValue); break;  // C4
    case 4:  tone(speakerPin, 294); delay(delayValue); break;  // D4
    case 5:  tone(speakerPin, 330); delay(delayValue); break;  // E4
    case 6:  tone(speakerPin, 349); delay(delayValue); break;  // F4
    case 7:  tone(speakerPin, 392); delay(delayValue); break;  // G4
    case 8:  tone(speakerPin, 440); delay(delayValue); break;  // A4
    case 9:  tone(speakerPin, 494); delay(delayValue); break;  // B4
    case 10: tone(speakerPin, 523); delay(delayValue); break;  // C5
    default: noTone(speakerPin); break;
  }
}

How switch/case works

switch evaluates currentTone and jumps to the matching case. The break at the end of each case exits the switch block — without it, execution would fall through to the next case. The default case runs when no button is pressed (currentTone stayed at 0), which calls noTone() to silence the speaker.

This structure is more readable than a chain of if/else if statements when you have multiple discrete values to handle, and it compiles to more efficient machine code on AVR processors.

Why currentTone must reset to 0 at the top of loop()

The first draft of this sketch didn’t reset currentTone. It would work — barely — but had a noticeable bug: after pressing a button and releasing it, the speaker would keep playing the last note instead of going silent. The issue is that currentTone is declared inside loop(), which means it’s recreated on every iteration, but if you move the declaration outside loop() for any reason, the bug shows up clearly: the variable retains its previous value across loop iterations, so the last button pressed never stops playing.

Resetting to 0 at the top of every loop guarantees that if no button is currently pressed, currentTone hits the default case and the speaker stops.

Note frequencies

The values passed to tone() are frequencies in Hz. These are the standard equal-temperament values for one octave starting at middle C:

ButtonPinNoteFrequency
1D3C4262 Hz
2D4D4294 Hz
3D5E4330 Hz
4D6F4349 Hz
5D7G4392 Hz
6D8A4440 Hz
7D9B4494 Hz
8D10C5523 Hz

A4 at 440 Hz is the standard tuning reference (the A above middle C). The rest follow from equal temperament, where each semitone is a factor of 2^(1/12) ≈ 1.0595 higher than the previous.

The delayValue variable

One hundred milliseconds was added as a hold time after a button is read. Without it, releasing a button and moving to the next one cuts the previous note very abruptly. The 100ms carry-over smooths the transition between notes.

Setting delayValue as a named variable at the top rather than hardcoding 100 in all eight delay() calls is worth doing even in a short sketch. It’s the same argument as using a named constant for any value that appears more than once: change it once at the top, it changes everywhere.

Where to take it next

The natural extensions from here: add sharp/flat notes by adding more buttons (D2 and D11–D13 are available), play pre-programmed songs by reading frequencies and durations from an array, or replace the polling loop with interrupts so notes respond immediately rather than waiting for loop() to come around. The Arduino tone() reference has the full API including simultaneous tones on multiple pins.

References and further reading