Easy-Peasy Piano

I welcome comments and suggestions.

In this project, I learned how to utilize pull-up resistors, and how to drive the passive buzzer to play specific notes/tones. I did not use the tone() module that everyone is fond of, as I prefer to figure out how things work on my own. I also dove into drawing my first schematic for this project. It may not be perfect and complete, but I think it gets the point across.

The project is built on the Mega2560 board, but should work on the UNO as well. The code supports the regular CDEFGAB notes, as well as their sharp versions. However, due to lack of buttons in my inventory, the sharp functionality is disabled in the code. A person just needs to uncomment all code related to the sharps, and assign proper pins, then add buttons to support those notes. * Note: I don't think the UNO has enough input pins to support sharp notes.

Other features:

  • Octave up and down, via 2 buttons.
  • Records as you play, allowing you to play back your recording. Supports up to 512 notes array, though you can adjust this to play more if you wish.
  • Tempo can be changed via the tempoMs variable in code.

Code:

// notes
int cPin = 2; //int cSharpPin = 3;
int dPin = 3; //int dSharpPin = 5;
int ePin = 4;
int fPin = 5; //int fSharpPin = 8;
int gPin = 6; //int gSharpPin = 10;
int aPin = 7; //int aSharpPin = 12;
int bPin = 8;

// octave
int octaveDownPin = 9;
int octaveUpPin = 10;
int octave = 4;
int minOctave = 2;
int maxOctave = 6;

// speaker/buzzer
int spkPin = 11;

// playback
int playbackPin = 12;
String recordedNote[512];
int recordedNoteIndex = 0;

int tempoMs = 250; // 1/4 beat tempo, milliseconds

void setup() {
  pinMode(cPin, INPUT); //pinMode(cSharpPin, INPUT);
  pinMode(dPin, INPUT); //pinMode(dSharpPin, INPUT);
  pinMode(ePin, INPUT);
  pinMode(fPin, INPUT); //pinMode(fSharpPin, INPUT);
  pinMode(gPin, INPUT); //pinMode(gSharpPin, INPUT);
  pinMode(aPin, INPUT); //pinMode(aSharpPin, INPUT);
  pinMode(bPin, INPUT);
  
  pinMode(octaveDownPin, INPUT);
  pinMode(octaveUpPin, INPUT);

  pinMode(spkPin, OUTPUT);

  pinMode(playbackPin, INPUT);

  Serial.begin(9600);
  delay(500);
  Serial.println("Ready!  Press RESET to clear recording.");
}

void loop() {
  // octave down
  if (digitalRead(octaveDownPin) == LOW) {
    setOctave(octave - 1);
    addToRecordedNotes("O-");
    while (digitalRead(octaveDownPin) == LOW) {}
  }
  // octave up
  if (digitalRead(octaveUpPin) == LOW) {
    setOctave(octave + 1);
    addToRecordedNotes("O+");
    while (digitalRead(octaveUpPin) == LOW);
  }

  // notes
  int freq = 0;
  String note = "";
  if (digitalRead(cPin) == LOW) { note = "C"; }
  //if (digitalRead(cSharpPin) == LOW) { note = "C#"; }
  if (digitalRead(dPin) == LOW) { note = "D"; }
  //if (digitalRead(dSharpPin) == LOW) { note = "D#"; }
  if (digitalRead(ePin) == LOW) { note = "E"; }
  if (digitalRead(fPin) == LOW) { note = "F"; }
  //if (digitalRead(fSharpPin) == LOW) { note = "F#"; }
  if (digitalRead(gPin) == LOW) { note = "G"; }
  //if (digitalRead(gSharpPin) == LOW) { note = "G#"; }
  if (digitalRead(aPin) == LOW) { note = "A"; }
  //if (digitalRead(aSharpPin) == LOW) { note = "A#"; }
  if (digitalRead(bPin) == LOW) { note = "B"; }
  if (note != "") {
    playNote(note);
    addToRecordedNotes(note);
  }

  // playback recording
  if (digitalRead(playbackPin) == LOW) {
    while (digitalRead(playbackPin) == LOW) {}
    playRecorded();
  }
}

// play back recorded notes
void playRecorded() {
  if (recordedNoteIndex == 0) return;

  // print all notes from array
  Serial.print("\nPlaying: ");
  for(int i = 0; i < recordedNoteIndex; i++) { Serial.print(recordedNote[i] + " "); }
  Serial.println();
  
  octave = 4;

  // play each note from array, display note currently being played
  for (int i = 0; i < recordedNoteIndex; i++) {
    String note = recordedNote[i];
    if (note == "O+") { setOctave(octave + 1); Serial.print(note + " "); }
    else if (note == "O-") { setOctave(octave - 1); Serial.print(note + " "); }
    else { playNote(note); }
  }

  octave = 4;

  Serial.println("\nEnd Playback");
}

// play a specific note
void playNote(String note, int durationMs) {
  note.toUpperCase();

  Serial.print(note + " ");

  float freq;
  if (note == "C") freq = 16.35;
  if (note == "C#") freq = 17.32;
  if (note == "D") freq = 18.35;
  if (note == "D#") freq = 19.45;
  if (note == "E") freq = 20.60;
  if (note == "F") freq = 21.83;
  if (note == "F#") freq = 23.12;
  if (note == "G") freq = 24.50;
  if (note == "G#") freq = 25.96;
  if (note == "A") freq = 27.50;
  if (note == "A#") freq = 29.14;
  if (note == "B") freq = 30.87;
  
  // update frequency for current octave
  freq = freq * pow(2, octave + 1); // calc freq for octave

  // convert note frequency to speaker cycle value (microseconds (us))
  int cycleUs = (1000 / freq) * 1000;

  // get number of cycles
  long cycles = durationMs * 1000L / cycleUs;

  // cycle speaker
  for (long i = 0; i < cycles; i++) {
    digitalWrite(spkPin, HIGH);
    delayMicroseconds(cycleUs);
    digitalWrite(spkPin, LOW);
    delayMicroseconds(cycleUs);
  }
}
void playNote(String note) { playNote(note, tempoMs); } // play at default tempo

// set octave, within bounds
void setOctave(int value) {
  octave = value;
  if (octave < minOctave) octave = minOctave;
  if (octave > maxOctave) octave = maxOctave;
}

// add a note to recording array
void addToRecordedNotes(String note) {
  recordedNote[recordedNoteIndex] = note;
  recordedNoteIndex++;
}

Physical build:

Schematic:

Edited: I accidentally switched the connections on the buzzer. Fixed.

You could have used INPUT_PULLUP in pinMode() and save yourself the bother of using external pullup resistors

Consider using C style strings (zero terminated arrays of chars) instead of Strings which have a bad reputation in small memory spaces such as a microcontroller

You could shorten the code that reads the note buttons by using arrays or an array of structs. Perhaps they should be the next thing on your learning plan

I'd be interested in an example of what you mean in my code? I'm using strings because the symbols representing the notes are multi-character.
I'm thinking if I were to use structs, more working RAM would be required, as I'd need to represent at minimum 3 values: symbol (like a char note), bool isSharp, and if the symbol is octave, bool isOctaveUp? And wouldn't that also require more code to process that data?

pinMode(cPin, INPUT); //pinMode(cSharpPin, INPUT);

You can do that with C style strings which are arrays of chars

If an array has fixed values then you can put into PROGMEM and the code will, in practice, be shorter because it will not be repeated

Yep, I made a conscious decision to do my own, because I want to ingrain it in my memory.