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.
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.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Full-size breadboard
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 1 × Piezo speakerIn Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 3 × Resistor, 220 ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × LED, blueIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × LED, greenIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × LED, redIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × Signal generator
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 pin | Connects to |
|---|---|
| Pushbutton pin 2a | Arduino Uno R3 pin GND |
| Piezo speaker pin 2 | Arduino Uno R3 pin GND |
| Piezo speaker pin 1 | Arduino Uno R3 pin 10 |
| Resistor, 220 Ω (3) pin 1 | Arduino Uno R3 pin 7 |
| Resistor, 220 Ω (2) pin 1 | Arduino Uno R3 pin 6 |
| Resistor, 220 Ω (1) pin 1 | Arduino Uno R3 pin 5 |
| Pushbutton pin 1a | Arduino Uno R3 pin 3 |
| Signal generator pin OUT | Arduino Uno R3 pin 2 |
| LED, blue pin C | Arduino Uno R3 pin GND |
| LED, green pin C | Arduino Uno R3 pin GND |
| LED, red pin C | Arduino Uno R3 pin GND |
| Signal generator pin GND | Arduino Uno R3 pin GND |
| LED, blue pin A | Resistor, 220 Ω (1) pin 2 |
| LED, green pin A | Resistor, 220 Ω (2) pin 2 |
| LED, red pin A | Resistor, 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.
// 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 editorTroubleshooting
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.