Built-in project · Arduino Uno R3

Ringtone player on an Arduino Uno

An Uno that reads RTTTL ringtones, the compact text format old phones used, and plays three songs on a piezo with a tempo knob, a next-song button and a pause button. The whole build, an Arduino Uno and 6 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 30 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
  • Controllers
  • Reading a button
  • Choosing and reading a resistor
  • Difficulty: Intermediate
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 8

How it works

An Uno that reads RTTTL ringtones, the compact text format old phones used, and plays three songs on a piezo with a tempo knob, a next-song button and a pause button.

Arduino Uno R3 runs the sketch below. The circuit around it: Pushbutton, Piezo speaker, Resistor, 220 Ω, LED, yellow, Potentiometer. pinMode sets NEXT_PIN as an input with its pull-up on. digitalWrite drives LED_PIN LOW. digitalRead reads whether pin is HIGH or LOW. analogRead measures the voltage on A0 as a number. Serial.begin opens the serial port at 9600 baud.

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 (1) pin 2aArduino Uno R3 pin GND
Pushbutton (2) pin 2aArduino Uno R3 pin GND
Piezo speaker pin 2Arduino Uno R3 pin GND
Piezo speaker pin 1Arduino Uno R3 pin 10
Resistor, 220 Ω pin 1Arduino Uno R3 pin 6
Pushbutton (2) pin 1aArduino Uno R3 pin 3
Pushbutton (1) pin 1aArduino Uno R3 pin 2
Potentiometer pin 3Arduino Uno R3 pin 5V
LED, yellow pin CArduino Uno R3 pin GND
Potentiometer pin 1Arduino Uno R3 pin GND
Potentiometer pin 2Arduino Uno R3 pin A0
LED, yellow pin AResistor, 220 Ω 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
// Ringtone player: songs written as text, read note by note.
//
// Wiring:
//   next song   pin 2 to GND
//   pause       pin 3 to GND
//   speed       potentiometer wiper -> A0, ends to 5 V and GND (half speed to double)
//   note light  pin 6 -> 220 ohms -> LED -> GND
//   piezo       pin 10 -> piezo -> GND
//
// RTTTL ("Ring Tone Text Transfer Language") is how old phones stored ringtones.
// A song is one line of text with three parts:
//
//   name : defaults : notes
//   Ode:d=4,o=5,b=125:e,e,f,g,g,f,e,d,...
//
//   d  the default length of a note (4 is a quarter note, 8 an eighth)
//   o  the default octave (the octave of middle C is 4, so 5 is one up)
//   b  beats per minute
//
// Each note is: an optional length, a letter (a to g, or p for a pause), an
// optional # for sharp, an optional dot (half again as long) and an optional
// octave. So "8d#6." is a dotted eighth D sharp in octave 6, and "e" is an E for
// the default length in the default octave.
//
// Pitch. Going up one octave doubles the frequency, so the sketch keeps only the
// twelve notes of octave 4 and doubles them as needed. Length. A whole note is 4
// beats, so a note of length L lasts 4 / L beats, 60000 / bpm milliseconds each.
//
// The player never waits: it works out when the current note ends, and when that
// time comes it reads the next one from the text.
//
// Things to try. Add your own song to SONGS. Turn the knob while it plays. Change
// b=125 to b=60.

const uint8_t NEXT_PIN = 2;
const uint8_t PAUSE_PIN = 3;
const uint8_t LED_PIN = 6;
const uint8_t PIEZO_PIN = 10;

const char *const SONGS[] = {
  "Ode to Joy:d=4,o=5,b=125:e,e,f,g,g,f,e,d,c,c,d,e,e.,8d,2d,e,e,f,g,g,f,e,d,c,c,d,e,d.,8c,2c",
  "Twinkle:d=4,o=5,b=120:c,c,g,g,a,a,2g,f,f,e,e,d,d,2c,g,g,f,f,e,e,2d,g,g,f,f,e,e,2d,c,c,g,g,a,a,2g,f,f,e,e,d,d,2c",
  "Birthday:d=4,o=5,b=140:8g,8g,a,g,c6,2b,8g,8g,a,g,d6,2c6,8g,8g,g6,e6,c6,b,a,8f6,8f6,e6,c6,d6,2c6",
};
const uint8_t SONG_COUNT = sizeof(SONGS) / sizeof(SONGS[0]);

// C4 to B4 in tenths of a hertz (C, C#, D, D#, E, F, F#, G, G#, A, A#, B)
const uint16_t OCTAVE4[12] = {2616, 2772, 2937, 3111, 3296, 3492, 3700, 3920, 4153, 4400, 4662, 4939};
// the semitone of each letter above C: a b c d e f g
const int8_t LETTER_SEMITONE[7] = {9, 11, 0, 2, 4, 5, 7};
const char *const NAMES[12] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};

uint8_t song = 0;
const char *cursor = nullptr;     // the next character of the notes
uint8_t defLength = 4, defOctave = 5;
uint16_t songBpm = 120;
uint32_t noteEndsAt = 0;
uint32_t soundEndsAt = 0;
bool paused = false;
bool sounding = false;
uint32_t pausedAt = 0;

bool nextWas = false, pauseWas = false;
uint32_t nextAt = 0, pauseAt = 0;

static bool pressed(uint8_t pin, bool &was, uint32_t &at)
{
  bool down = digitalRead(pin) == LOW;
  uint32_t now = millis();
  if (down != was && now - at > 20) {
    was = down;
    at = now;
    return down;
  }
  return false;
}

// Read a whole number from the text, moving the cursor past it. Returns 0 if there is none.
static uint8_t number()
{
  uint8_t n = 0;
  while (*cursor >= '0' && *cursor <= '9') {
    n = n * 10 + (*cursor - '0');
    cursor++;
  }
  return n;
}

static void loadSong(uint8_t index)
{
  const char *text = SONGS[index];
  // skip the name
  while (*text && *text != ':') {
    text++;
  }
  text++;
  defLength = 4;
  defOctave = 5;
  songBpm = 63;
  // the defaults: d=4,o=5,b=125
  while (*text && *text != ':') {
    char key = *text++;
    if (*text == '=') {
      text++;
    }
    cursor = text;
    uint16_t value = 0;
    while (*cursor >= '0' && *cursor <= '9') {
      value = value * 10 + (*cursor - '0');
      cursor++;
    }
    text = cursor;
    if (key == 'd') {
      defLength = value;
    } else if (key == 'o') {
      defOctave = value;
    } else if (key == 'b') {
      songBpm = value;
    }
    if (*text == ',') {
      text++;
    }
  }
  cursor = text + 1;  // the first note
  Serial.print("Song ");
  Serial.print(SONGS[index]);
  Serial.println();
}

// Read one note and start it.
static void playNext()
{
  if (*cursor == 0) {
    // the end of the song: start it again after a short rest
    loadSong(song);
    noteEndsAt = millis() + 800;
    return;
  }
  uint8_t length = number();
  if (length == 0) {
    length = defLength;
  }
  char letter = *cursor++;
  bool sharp = false;
  if (*cursor == '#') {
    sharp = true;
    cursor++;
  }
  bool dotted = false;
  if (*cursor == '.') {
    dotted = true;
    cursor++;
  }
  uint8_t octave = number();
  if (octave == 0) {
    octave = defOctave;
  }
  if (*cursor == '.') {  // the dot is sometimes written after the octave
    dotted = true;
    cursor++;
  }
  if (*cursor == ',') {
    cursor++;
  }

  // 100 % of the speed knob is the song's own tempo; the knob runs from half to double
  uint32_t speed = 50 + (analogRead(A0) * 150UL) / 1023;
  uint32_t beatMs = 60000UL * 100 / (songBpm * speed);
  uint32_t ms = beatMs * 4 / length;
  if (dotted) {
    ms += ms / 2;
  }
  noteEndsAt = millis() + ms;

  if (letter == 'p') {
    noTone(PIEZO_PIN);
    digitalWrite(LED_PIN, LOW);
    sounding = false;
    Serial.print("Rest ");
    Serial.print(ms);
    Serial.println(" ms");
    return;
  }
  int semitone = LETTER_SEMITONE[letter - 'a'] + (sharp ? 1 : 0);
  int octaveShift = octave - 4;
  if (semitone > 11) {
    semitone -= 12;
    octaveShift++;
  }
  uint32_t tenths = OCTAVE4[semitone];
  for (int8_t o = 0; o < octaveShift; o++) {
    tenths *= 2;
  }
  uint16_t hz = (tenths + 5) / 10;
  tone(PIEZO_PIN, hz);
  digitalWrite(LED_PIN, HIGH);
  sounding = true;
  // a little silence at the end of each note so repeated notes are heard separately
  soundEndsAt = millis() + ms * 85 / 100;
  Serial.print("Note ");
  Serial.print(NAMES[semitone]);
  Serial.print(octave);
  Serial.print(" ");
  Serial.print(hz);
  Serial.print(" Hz ");
  Serial.print(ms);
  Serial.println(" ms");
}

void setup()
{
  Serial.begin(9600);
  pinMode(NEXT_PIN, INPUT_PULLUP);
  pinMode(PAUSE_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  pinMode(PIEZO_PIN, OUTPUT);
  Serial.println("Ringtone player ready");
  loadSong(song);
  noteEndsAt = millis() + 300;
}

void loop()
{
  uint32_t now = millis();

  if (pressed(NEXT_PIN, nextWas, nextAt)) {
    song = (song + 1) % SONG_COUNT;
    noTone(PIEZO_PIN);
    digitalWrite(LED_PIN, LOW);
    sounding = false;
    paused = false;
    loadSong(song);
    noteEndsAt = now + 300;
  }
  if (pressed(PAUSE_PIN, pauseWas, pauseAt)) {
    paused = !paused;
    Serial.println(paused ? "Paused" : "Playing");
    if (paused) {
      noTone(PIEZO_PIN);
      digitalWrite(LED_PIN, LOW);
      sounding = false;
      pausedAt = now;
    } else {
      // shift the clock by the time we stood still
      noteEndsAt = now + 100;
    }
  }
  if (paused) {
    return;
  }

  if (sounding && now >= soundEndsAt) {
    noTone(PIEZO_PIN);
    digitalWrite(LED_PIN, LOW);
    sounding = false;
  }
  if (now >= noteEndsAt) {
    playNext();
  }
}

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(NEXT_PIN, INPUT_PULLUP);
  pinMode(PAUSE_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  pinMode(PIEZO_PIN, OUTPUT);
  Serial.println("Ringtone player ready");
  loadSong(song);
  noteEndsAt = millis() + 300;
}

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()
{
  uint32_t now = millis();

  if (pressed(NEXT_PIN, nextWas, nextAt)) {
    song = (song + 1) % SONG_COUNT;
    noTone(PIEZO_PIN);
    digitalWrite(LED_PIN, LOW);
    sounding = false;
    paused = false;
    loadSong(song);
    noteEndsAt = now + 300;
  }
  if (pressed(PAUSE_PIN, pauseWas, pauseAt)) {
    paused = !paused;
    Serial.println(paused ? "Paused" : "Playing");
    if (paused) {
      noTone(PIEZO_PIN);
      digitalWrite(LED_PIN, LOW);
      sounding = false;
      pausedAt = now;
    } else {
      // shift the clock by the time we stood still
      noteEndsAt = now + 100;
    }
  }
  if (paused) {
    return;
  // ... 11 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.