Built-in project · Arduino Uno R3

Tuner on an Arduino Uno

An Uno that measures the pitch of a signal with a pin interrupt, names the nearest note, and lights flat, in tune or sharp. A reference button plays the note it is tuning to. The whole build, an Arduino Uno and 9 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 35 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $33. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Sound
  • Lights
  • Sensors
  • Reading a button
  • Choosing and reading a resistor
  • Difficulty: Intermediate
  • Time: About 35 minutes
  • Board: Arduino Uno R3
  • Parts: 10

How it works

An Uno that measures the pitch of a signal with a pin interrupt, names the nearest note, and lights flat, in tune or sharp. A reference button plays the note it is tuning to.

Arduino Uno R3 runs the sketch below. The circuit around it: Pushbutton, Piezo speaker, Resistor, 220 Ω, LED, blue, LED, green, LED, red. pinMode sets SIGNAL_PIN as an input. digitalRead reads whether REF_PIN is HIGH or LOW. Serial.begin opens the serial port at 9600 baud. tone plays a note on PIEZO_PIN. millis reads the time since power-up, so the sketch can time things without stopping.

Parts list

10 parts, plus the jumper wires and a bench instrument. The parts retail for about $33; on Mokxi they're free, and so are the instruments.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Pushbutton pin 2aArduino Uno R3 pin GND
Piezo speaker pin 2Arduino Uno R3 pin GND
Piezo speaker pin 1Arduino Uno R3 pin 10
Resistor, 220 Ω (3) pin 1Arduino Uno R3 pin 7
Resistor, 220 Ω (2) pin 1Arduino Uno R3 pin 6
Resistor, 220 Ω (1) pin 1Arduino Uno R3 pin 5
Pushbutton pin 1aArduino Uno R3 pin 3
Signal generator pin OUTArduino Uno R3 pin 2
LED, blue pin CArduino Uno R3 pin GND
LED, green pin CArduino Uno R3 pin GND
LED, red pin CArduino Uno R3 pin GND
Signal generator pin GNDArduino Uno R3 pin GND
LED, blue pin AResistor, 220 Ω (1) pin 2
LED, green pin AResistor, 220 Ω (2) pin 2
LED, red pin AResistor, 220 Ω (3) pin 2

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Tuner: measure a pitch, name the note, say flat or sharp.
//
// Wiring:
//   signal       the instrument's output (here a signal generator) -> pin 2, ground to GND
//   flat LED     pin 5 -> 220 ohms -> blue LED -> GND
//   in tune LED  pin 6 -> 220 ohms -> green LED -> GND
//   sharp LED    pin 7 -> 220 ohms -> red LED -> GND
//   reference    pin 3 to GND: plays the note it is tuning to
//   piezo        pin 10 -> piezo -> GND
//
// Pitch is how many times a second the sound repeats. A tuner counts that. Pin 2
// can raise an interrupt on every rising edge of the signal, and the interrupt
// routine writes down the time of the first edge and of the latest one, and how
// many there have been. Every quarter of a second the sketch turns those into a
// frequency:
//
//   frequency = (edges - 1) / (time of last edge - time of first edge)
//
// Dividing by the time between the first and last edge, not by the length of the
// window, is what makes it accurate: the error is one clock tick of 4
// microseconds in a quarter of a second.
//
// Then the note. Notes are spaced by ratios, not by steps: each semitone is the
// 12th root of 2 higher than the one below it, and A4 is 440 Hz. So
//
//   semitones from A4 = 12 * log2(frequency / 440)
//
// (A small chip does not want a floating-point logarithm, so the sketch finds the
// nearest note by comparing against a table of fourteen frequencies instead.)
//
// The nearest whole number of semitones is the note, and what is left over,
// times 100, is how many cents out you are (a cent is a hundredth of a semitone,
// and under about 5 cents most ears cannot tell). The sharp and flat lights
// come on past 10 cents.
//
// Things to try. Change the generator's frequency to 440, 330 and 82.4 (the low
// string of a guitar). Try 450 and 430. Press the reference button while a note
// is on to hear the pitch you are aiming for.

const uint8_t SIGNAL_PIN = 2;
const uint8_t REF_PIN = 3;
const uint8_t FLAT_LED = 5;
const uint8_t TUNE_LED = 6;
const uint8_t SHARP_LED = 7;
const uint8_t PIEZO_PIN = 10;

const char *const NAMES[12] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};

volatile uint32_t firstEdge = 0;
volatile uint32_t lastEdge = 0;
volatile uint16_t edges = 0;

void onEdge()
{
  uint32_t now = micros();
  if (edges == 0) {
    firstEdge = now;
  }
  lastEdge = now;
  edges++;
}

uint32_t windowStart = 0;
bool refWas = false;
int refMidi = -1;

static void lights(bool flat, bool tune, bool sharp)
{
  digitalWrite(FLAT_LED, flat);
  digitalWrite(TUNE_LED, tune);
  digitalWrite(SHARP_LED, sharp);
}

// The notes from B3 to C5, in hundredths of a hertz. Every octave up doubles
// the frequency and every octave down halves it, so these fourteen numbers are
// all the table there is. Index 1 is C4 (MIDI note 60).
const uint32_t NOTE_HUNDREDTHS[14] = {24694, 26163, 27718, 29366, 31113, 32963, 34923,
                                      36999, 39200, 41530, 44000, 46616, 49388, 52325};

// Frequency of a MIDI note number in whole hertz: 69 is A4 = 440 Hz.
static uint16_t midiToHz(int midi)
{
  uint32_t hundredths = NOTE_HUNDREDTHS[1 + (midi - 60 + 120) % 12];
  int octaves = (midi - 60 + 120) / 12 - 10;  // whole octaves away from octave 4
  while (octaves > 0) {
    hundredths *= 2;
    octaves--;
  }
  while (octaves < 0) {
    hundredths /= 2;
    octaves++;
  }
  return (uint16_t)((hundredths + 50) / 100);
}

// How far `f` is from `target`, in cents. Cents are 1200 * log2(f / target); for
// frequencies this close together that is very nearly 3462 * (f - t) / (f + t),
// which needs no logarithm, only integer arithmetic.
static int32_t centsBetween(uint32_t f, uint32_t target)
{
  return (int32_t)3462 * ((int32_t)f - (int32_t)target) / (int32_t)(f + target);
}

void setup()
{
  Serial.begin(9600);
  pinMode(SIGNAL_PIN, INPUT);
  pinMode(REF_PIN, INPUT_PULLUP);
  pinMode(FLAT_LED, OUTPUT);
  pinMode(TUNE_LED, OUTPUT);
  pinMode(SHARP_LED, OUTPUT);
  pinMode(PIEZO_PIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(SIGNAL_PIN), onEdge, RISING);
  windowStart = millis();
  Serial.println("Tuner ready");
}

void loop()
{
  // the reference button plays the note being tuned to, while it is held
  bool refDown = digitalRead(REF_PIN) == LOW;
  if (refDown && !refWas && refMidi >= 0) {
    tone(PIEZO_PIN, midiToHz(refMidi));
  } else if (!refDown && refWas) {
    noTone(PIEZO_PIN);
  }
  refWas = refDown;

  if (millis() - windowStart < 250) {
    return;
  }
  windowStart += 250;

  noInterrupts();
  uint16_t n = edges;
  uint32_t first = firstEdge;
  uint32_t last = lastEdge;
  edges = 0;
  interrupts();

  if (n < 4 || last == first) {
    lights(false, false, false);
    if (refMidi != -1) {
      Serial.println("No signal");
    }
    refMidi = -1;
    return;
  }

  // frequency in hundredths of a hertz, in two steps so nothing overflows
  uint32_t dt = last - first;
  uint32_t scaled = (uint32_t)(n - 1) * 10000UL;
  uint32_t hundredths = (scaled / dt) * 10000UL + ((scaled % dt) * 10000UL) / dt;

  // fold the pitch into the octave the table covers, remembering how far we moved
  int octaves = 0;
  uint32_t g = hundredths;
  while (g > 52000) {
    g /= 2;
    octaves++;
  }
  while (g < 24000) {
    g *= 2;
    octaves--;
  }

  // the nearest of the fourteen notes
  uint8_t best = 0;
  int32_t bestCents = centsBetween(g, NOTE_HUNDREDTHS[0]);
  for (uint8_t i = 1; i < 14; i++) {
    int32_t c = centsBetween(g, NOTE_HUNDREDTHS[i]);
    if (abs(c) < abs(bestCents)) {
      best = i;
      bestCents = c;
    }
  }
  int midi = 59 + best + 12 * octaves;  // index 1 is C4, MIDI 60
  refMidi = midi;
  int cents = (int)bestCents;

  lights(cents < -10, cents >= -10 && cents <= 10, cents > 10);

  Serial.print(NAMES[midi % 12]);
  Serial.print(midi / 12 - 1);
  Serial.print(" ");
  Serial.print(hundredths / 100);
  Serial.print(".");
  Serial.print((hundredths % 100) / 10);
  Serial.print(" Hz  ");
  Serial.print(cents >= 0 ? "+" : "");
  Serial.print(cents);
  Serial.print(" cents  ");
  Serial.println(cents < -10 ? "flat" : (cents > 10 ? "sharp" : "in tune"));
}

Code walkthrough

The key functions of the sketch, and what each one does.

setup sketch.ino

Runs once when the board starts. It gets the pins and libraries ready.

void setup()
{
  Serial.begin(9600);
  pinMode(SIGNAL_PIN, INPUT);
  pinMode(REF_PIN, INPUT_PULLUP);
  pinMode(FLAT_LED, OUTPUT);
  pinMode(TUNE_LED, OUTPUT);
  pinMode(SHARP_LED, OUTPUT);
  pinMode(PIEZO_PIN, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(SIGNAL_PIN), onEdge, RISING);
  windowStart = millis();
  Serial.println("Tuner ready");
}

loop sketch.ino

Runs over and over for as long as the board has power. This is where the project does its work.

void loop()
{
  // the reference button plays the note being tuned to, while it is held
  bool refDown = digitalRead(REF_PIN) == LOW;
  if (refDown && !refWas && refMidi >= 0) {
    tone(PIEZO_PIN, midiToHz(refMidi));
  } else if (!refDown && refWas) {
    noTone(PIEZO_PIN);
  }
  refWas = refDown;

  if (millis() - windowStart < 250) {
    return;
  }
  windowStart += 250;

  noInterrupts();
  uint16_t n = edges;
  uint32_t first = firstEdge;
  uint32_t last = lastEdge;
  edges = 0;
  interrupts();

  if (n < 4 || last == first) {
    lights(false, false, false);
    if (refMidi != -1) {
      Serial.println("No signal");
    }
  // ... 49 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The input reads random HIGH and LOW when the button is not pressed. Pushbutton
    A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW.
  • The button seems always pressed, or never changes. Pushbutton
    A 4-leg tactile switch has legs joined in pairs. Place it across the breadboard center gap so the pairs sit on different rows. Wired along one side, the legs are already connected.
  • A passive piezo stays silent when you just set the pin HIGH. Piezo speaker
    A passive piezo needs a changing signal. Use tone(pin, frequency) or a PWM wave. Around 2 to 4 kHz is loudest. A buzzer with a built in oscillator makes sound from plain DC instead.
  • Sound is very quiet or the pin seems overloaded. Piezo speaker
    Drive the piezo from a pin directly, or through a 100 ohm resistor. For a bigger buzzer, switch it with a transistor. Check that you wired the right two legs and joined grounds.
  • The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
    Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure.
  • A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
    Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground.
  • The LED does not light, or it lights once and then dies. LED
    LEDs have polarity. The longer leg (anode) goes toward the positive side. Always add a series resistor, about 220 ohms to 1 k ohm on a 5 V pin.
  • A blue or white LED stays dark on 3.3 V. LED
    Blue and white LEDs drop about 3 V, so 3.3 V leaves almost nothing for the resistor. Use a 5 V supply, or a smaller resistor and a red or green LED.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.