RhythmBox

by PepijnLu in Living > Video Games

104 Views, 1 Favorites, 0 Comments

RhythmBox

20240628_134638.jpg
Screenshot 2024-06-28 214254.png

For this project I made a simple rhythm game in a GameBoy-like handheld device. Lights come down from the top and when they reach the bottom, you have to press the corresponding button at the right time to make the correct sound and play a song. At the end of the song, lights light up depending on how well you did (more notes hit = more lights).

Supplies

Electronics:

  • 1x Arduino Uno
  • 4x Tactile Pushbutton Switch Momentary 4pin 12*12*7.3mm
  • 1x WS2812B Digitale 5050 RGB LED - Shield 40 LEDs - 8x5 Matrix (https://www.tinytronics.nl/en/lighting/matrix/ws2812b-digital-5050-rgb-led-shield-40-leds-8x5-matrix)
  • 2x Active Buzzer 5V
  • 16x Jumper wire

Casing:

  • Project box (https://www.amazon.co.uk/dp/B097R3YJKL?psc=1&ref=ppx_yo2ov_dt_b_product_details)
  • Screws
  • Glue
  • Acrylic
  • Hard plastic

Tools:

  • Soldering iron
  • Solder
  • Wire strippers (cable strippers)
  • Mini Rotary Tool (saw)

Proof of Concept

20240213_154154.jpg
Proof of Concept RhythmBox

The first thing I wanted to do was make a quick proof of concept of my idea. With a breadboard, a buzzer and some LEDs I made it so the light would go from the red to the green LEDs and when it reached the final LED, if you were pressing the button, a sound would play, playing a song.

Research

After making my proof of concept, I realised I wasn't too happy with how the LEDs looked and operated. I was gonna need lots of wires and pins if I wanted to connect 4 rows of LEDs. I did some research and found discovered LED matrixes. The one I ended up using, which can be found under supplies, only needs 1 pin and has 40 LEDs on it, which can all be individually controlled. This was perfect for my project, and fixed my problem entirely.

I also needed to figure out how to manage the casing. My idea was for it to kind of look like a retro handheld, like a gameboy for example. I wanted a case with 4 buttons and a glass panel in front of the led shield. I looked online and found out that a project case with a sheet of acrylic would probably be my best bet.

Design the Wiring

design 2.PNG

First, I wanted to see what the wiring would look like with the LED shield and the other components, so I made a diagram of how it would work. Given that the LED shield just sits on the arduino board, I didn't include it in the diagram. In my case, its using pin 6.

Design the Case

bc7669e2-d642-44fe-bf82-4b6716f48866.jpg
8ae64915-28cf-482d-9a98-b8732ff6f54b.jpg

When I started designing my case, I discovered that I was better off not using a breadboard for 2 reasons.

1) Both an arduino and a breadboard wouldn't fit in my project case;

2) There wasn't a lot of wiring to be done, so an entire breadboard felt pretty redundant.

I decided I was gonna use glue and screws to keep the individual parts in place in the case and solder wires directly to the components themselves.

Building (components in the Right Place)

20240625_161955.jpg
20240625_162312.jpg
20240625_162327.jpg

On the bottom part of the project case, cut out a small hole for the power supply and lie the arduino down. Drill 2 holes in the bottom using the rotary tool and glue the buzzers to the case so that the hole of the buzzers matches the drilled holes, so the sound can escape.

On the top half of the project case, cut out a rectangle the size of the LED shield. Glue a sheet of acrylic over it. Cut out another rectangle big enough to fit 4 buttons next to each other. Glue all the buttons next to each other to a strip of hard plastic, and glue the ends of that strip to your project case so the buttons pop out on the other side.

Building (soldering / Wires)

20240627_174506.jpg

Now that everything is in the right place, solder everything together using the diagram shown before, just without the breadboard. Cut the jumper cables to the necessary length and strip the ends using the wire strippers, so the copper is sticking out, and solder these to the components.

Building (finishing Case)

20240628_133945.jpg
20240628_134650.jpg
20240628_134638.jpg

Finally, use some screws to keep the arduino in place so it doesn't move around in the case. Put the two halves of the project case together and screw them together. The case is now finished.

Coding

This is the code I wrote for my project. With the comments it's pretty straightforward. Heres the libraries I used:


// NeoPixel Ring simple sketch (c) 2013 Shae Erisson
// Released under the GPLv3 license to match the rest of the
// Adafruit NeoPixel library

#include <List.hpp>
#include <Adafruit_NeoPixel.h>
#include <Arduino.h>
#include <AceRoutine.h>
using namespace ace_routine;
#ifdef __AVR__
 #include <avr/power.h> // Required for 16 MHz Adafruit Trinket
#endif

// Which pin on the Arduino is connected to the NeoPixels?
#define PIN    6 // On Trinket or Gemma, suggest changing this to 1

// How many NeoPixels are attached to the Arduino?
#define NUMPIXELS 40 // Popular NeoPixel ring size

// When setting up the NeoPixel library, we tell it how many pixels,
// and which pin to use to send signals. Note that for older NeoPixel
// strips you might need to change the third parameter -- see the
// strandtest example for more information on possible values.
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

#define DELAYVAL 250 // Time (in milliseconds) to pause between pixels

//Score ints
int notesHit;
int notesMissed;
int crScoreInt;

//Bools for if you hit a note
bool ls1Hit;
bool ls2Hit;
bool ls3Hit;
bool ls4Hit;

//Button pins
const int buttonPin1 = 8;
const int buttonPin2 = 4;
const int buttonPin3 = 2;
const int buttonPin4 = 3;

//Buzzer pins
const int buzzerPin1 = 13;
const int buzzerPin2 = 11;

//Button states
int buttonState1 = 0;
int buttonState2 = 0;
int buttonState3 = 0;
int buttonState4 = 0;

//The lists with the note frequencies and note durations
List<int> noteFreqs;
List<int> noteDurs;

//Bools for running the light strand coroutines
bool runCR1, runCR1x1;
bool runCR2, runCR2x1;
bool runCR3, runCR3x1;
bool runCR4, runCR4x1;

//To check if the light strand is on its last light (where you have to press the button)
bool onLastLight1, onLastLight2, onLastLight3, onLastLight4;

//Bools for running the other coroutinues
bool runPlaySound, runErrorSound, runShowScore, runSong;

//Bools for pressed buttons
bool button1Pressed, button2Pressed, button3Pressed, button4Pressed;

//Milliseconds it takes to show the next light on a strand
int strandSpeed = 250;

//Int to keep track of the note (DONT CHANGE)
int note = -1;

//Update a LED
void UpdateLight(int i, int R, int G, int B)
{
 //Serial.print(i);
 pixels.setPixelColor(i, pixels.Color(R, G, B));
 if ((i >= 1) && !runShowScore)
 {
  pixels.setPixelColor(i - 1, pixels.Color(0, 0, 0));
 }
 pixels.show();
}

//Show the score at the end of the game
COROUTINE(showScore)
{
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runShowScore);

  if(crScoreInt < notesHit - notesMissed)
  {
   UpdateLight(crScoreInt, 0, 75, 0);
   crScoreInt++;
  }
  else
  {
   runShowScore = false;
  }

  COROUTINE_DELAY(100);
 }
}

COROUTINE(playBuzzer)
{
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runPlaySound);
  int currentNote = note;
  tone(buzzerPin2, noteFreqs[currentNote]);
  COROUTINE_DELAY(noteDurs[currentNote]);
  noTone(buzzerPin2);
  runPlaySound = false;
  COROUTINE_AWAIT(runPlaySound);
 }
}

COROUTINE(errorSound)
{
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runErrorSound);
  int currentNote = note;
  tone(buzzerPin1, 50);
  //COROUTINE_DELAY(noteDurs[currentNote]);
  COROUTINE_DELAY(100);
  noTone(buzzerPin1);
  runErrorSound = false;
  COROUTINE_AWAIT(runErrorSound);
 }
}

//Check if the note has been hit and award points
void CheckForError(int strandInt)
{
 switch(strandInt)
 {
  case 1:
   if (ls1Hit)
   {
    notesHit++;
   }
   ls1Hit = false;
   break;
  case 2:
   if (ls2Hit)
   {
    notesHit++;
   }
   ls2Hit = false;
   break;  
  case 3:
   if (ls3Hit)
   {
    notesHit++;
   }
   ls3Hit = false;
   break;
  case 4:
   if (ls4Hit)
   {
    notesHit++;
   }
   else
   ls4Hit = false;
   break;
 }
}

//Light strand 1 coroutine
COROUTINE(lightStrand1CR)
{
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runCR1);
  //Light 1
  UpdateLight(0, 150, 0, 0);
  //Serial.print("light 1");
  COROUTINE_DELAY(strandSpeed);

  //Light 2
  UpdateLight(1, 150, 0, 0);
  //Serial.print("light 2");
  COROUTINE_DELAY(strandSpeed);

  //Light 3
  UpdateLight(2, 150, 0, 0);
  //Serial.print("light 3");
  COROUTINE_DELAY(strandSpeed);

  //Light 4
  UpdateLight(3, 150, 0, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 5
  UpdateLight(4, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 6
  UpdateLight(5, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 7
  UpdateLight(6, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 8
  UpdateLight(7, 0, 150, 0);
  onLastLight1 = true;
  note++;
  COROUTINE_DELAY(strandSpeed);
  UpdateLight(7, 0, 0, 0);
  onLastLight1 = false;
  runCR1 = false;
  CheckForError(1);
  COROUTINE_AWAIT(runCR1);
 }
}

//Light strand 2 coroutine
COROUTINE(lightStrand2CR)
{
 //COROUTINE_BEGIN();
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runCR2);
  //Light 1
  UpdateLight(8, 150, 0, 0);
  //Serial.print("light 1");
  COROUTINE_DELAY(strandSpeed);

  //Light 2
  UpdateLight(9, 150, 0, 0);
  //Serial.print("light 2");
  COROUTINE_DELAY(strandSpeed);

  //Light 3
  UpdateLight(10, 150, 0, 0);
  //Serial.print("light 3");
  COROUTINE_DELAY(strandSpeed);

  //Light 4
  UpdateLight(11, 150, 0, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 5
  UpdateLight(12, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 6
  UpdateLight(13, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 7
  UpdateLight(14, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 8
  UpdateLight(15, 0, 150, 0);
  onLastLight2 = true;
  note++;
  COROUTINE_DELAY(strandSpeed);
  onLastLight2 = false;
  UpdateLight(15, 0, 0, 0);
  runCR2 = false;
  CheckForError(2);
  COROUTINE_AWAIT(runCR2);
 }
}

//Light strand 3 coroutine
COROUTINE(lightStrand3CR)
{
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runCR3);
  //Light 1
  UpdateLight(16, 150, 0, 0);
  //Serial.print("light 1");
  COROUTINE_DELAY(strandSpeed);

  //Light 2
  UpdateLight(17, 150, 0, 0);
  //Serial.print("light 2");
  COROUTINE_DELAY(strandSpeed);

  //Light 3
  UpdateLight(18, 150, 0, 0);
  //Serial.print("light 3");
  COROUTINE_DELAY(strandSpeed);

  //Light 4
  UpdateLight(19, 150, 0, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 5
  UpdateLight(20, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 6
  UpdateLight(21, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 7
  UpdateLight(22, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 8
  UpdateLight(23, 0, 150, 0);
  onLastLight3 = true;
  note++;
  COROUTINE_DELAY(strandSpeed);
  onLastLight3 = false;
  UpdateLight(23, 0, 0, 0);
  runCR3 = false;
  CheckForError(3);
  COROUTINE_AWAIT(runCR3);
 }
}

//Light strand 4 coroutine
COROUTINE(lightStrand4CR)
{
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runCR4);
  //Light 1
  UpdateLight(24, 150, 0, 0);
  //Serial.print("light 1");
  COROUTINE_DELAY(strandSpeed);

  //Light 2
  UpdateLight(25, 150, 0, 0);
  //Serial.print("light 2");
  COROUTINE_DELAY(strandSpeed);

  //Light 3
  UpdateLight(26, 150, 0, 0);
  //Serial.print("light 3");
  COROUTINE_DELAY(strandSpeed);

  //Light 4
  UpdateLight(27, 150, 0, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 5
  UpdateLight(28, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 6
  UpdateLight(29, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 7
  UpdateLight(30, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 8
  UpdateLight(31, 0, 150, 0);
  onLastLight4 = true;
  note++;
  COROUTINE_DELAY(strandSpeed);
  onLastLight4 = false;
  UpdateLight(31, 0, 0, 0);
  runCR4 = false;
  CheckForError(4);
  COROUTINE_AWAIT(runCR4);
 }
}

//Duplicate light strand 1 coroutine (so I can have 2 running at the same time)
COROUTINE(lightStrand1CRx1)
{
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runCR1x1);
  //Light 1
  UpdateLight(0, 150, 0, 0);
  //Serial.print("light 1");
  COROUTINE_DELAY(strandSpeed);

  //Light 2
  UpdateLight(1, 150, 0, 0);
  //Serial.print("light 2");
  COROUTINE_DELAY(strandSpeed);

  //Light 3
  UpdateLight(2, 150, 0, 0);
  //Serial.print("light 3");
  COROUTINE_DELAY(strandSpeed);

  //Light 4
  UpdateLight(3, 150, 0, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 5
  UpdateLight(4, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 6
  UpdateLight(5, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 7
  UpdateLight(6, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 8
  UpdateLight(7, 0, 150, 0);
  onLastLight1 = true;
  note++;
  COROUTINE_DELAY(strandSpeed);
  onLastLight1 = false;
  UpdateLight(7, 0, 0, 0);
  runCR1x1 = false;
  CheckForError(1);
  COROUTINE_AWAIT(runCR1x1);
 }
}

//Duplicate light strand 2 coroutine (so I can have 2 running at the same time)
COROUTINE(lightStrand2CRx1)
{
 //COROUTINE_BEGIN();
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runCR2x1);
  //Light 1
  UpdateLight(8, 150, 0, 0);
  //Serial.print("light 1");
  COROUTINE_DELAY(strandSpeed);

  //Light 2
  UpdateLight(9, 150, 0, 0);
  //Serial.print("light 2");
  COROUTINE_DELAY(strandSpeed);

  //Light 3
  UpdateLight(10, 150, 0, 0);
  //Serial.print("light 3");
  COROUTINE_DELAY(strandSpeed);

  //Light 4
  UpdateLight(11, 150, 0, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 5
  UpdateLight(12, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 6
  UpdateLight(13, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 7
  UpdateLight(14, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 8
  UpdateLight(15, 0, 150, 0);
  onLastLight2 = true;
  note++;
  COROUTINE_DELAY(strandSpeed);
  onLastLight2 = false;
  UpdateLight(15, 0, 0, 0);
  runCR2x1 = false;
  CheckForError(2);
  COROUTINE_AWAIT(runCR2x1);

 }
}

//Duplicate light strand 3 coroutine (so I can have 2 running at the same time)
COROUTINE(lightStrand3CRx1)
{
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runCR3x1);
  //Light 1
  UpdateLight(16, 150, 0, 0);
  //Serial.print("light 1");
  COROUTINE_DELAY(strandSpeed);

  //Light 2
  UpdateLight(17, 150, 0, 0);
  //Serial.print("light 2");
  COROUTINE_DELAY(strandSpeed);

  //Light 3
  UpdateLight(18, 150, 0, 0);
  //Serial.print("light 3");
  COROUTINE_DELAY(strandSpeed);

  //Light 4
  UpdateLight(19, 150, 0, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 5
  UpdateLight(20, 75, 75, 0);
  //testButtonState3 = true;
  COROUTINE_DELAY(strandSpeed);

  //Light 6
  UpdateLight(21, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 7
  UpdateLight(22, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 8
  UpdateLight(23, 0, 150, 0);
  onLastLight3 = true;
  note++;
  COROUTINE_DELAY(strandSpeed);
  onLastLight3 = false;
  UpdateLight(23, 0, 0, 0);
  runCR3x1 = false;
  CheckForError(3);
  COROUTINE_AWAIT(runCR3x1);
 }
}

//Duplicate light strand 4 coroutine (so I can have 2 running at the same time)
COROUTINE(lightStrand4CRx1)
{
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runCR4x1);
  //Light 1
  UpdateLight(24, 150, 0, 0);
  //Serial.print("light 1");
  COROUTINE_DELAY(strandSpeed);

  //Light 2
  UpdateLight(25, 150, 0, 0);
  //Serial.print("light 2");
  COROUTINE_DELAY(strandSpeed);

  //Light 3
  UpdateLight(26, 150, 0, 0);
  //Serial.print("light 3");
  COROUTINE_DELAY(strandSpeed);

  //Light 4
  UpdateLight(27, 150, 0, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 5
  UpdateLight(28, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 6
  UpdateLight(29, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 7
  UpdateLight(30, 75, 75, 0);
  COROUTINE_DELAY(strandSpeed);

  //Light 8
  UpdateLight(31, 0, 150, 0);
  onLastLight4 = true;
  note++;
  COROUTINE_DELAY(strandSpeed);
  onLastLight4 = false;
  UpdateLight(31, 0, 0, 0);
  runCR4x1 = false;
  CheckForError(4);
  COROUTINE_AWAIT(runCR4x1);
 }
}

//The coroutine of the actual song
COROUTINE(songCR)
{
 COROUTINE_LOOP()
 {
  COROUTINE_AWAIT(runSong);
  runSong = false;
  //bar 1
  COROUTINE_AWAIT(!runCR1);
  runCR1 = true;
  COROUTINE_DELAY(noteDurs[0] * 2);
  COROUTINE_AWAIT(!runCR3);
  runCR3 = true;
  COROUTINE_DELAY(noteDurs[1] * 2);
  COROUTINE_AWAIT(!runCR2);
  runCR2 = true;
  COROUTINE_DELAY(noteDurs[2] * 2);
  COROUTINE_AWAIT(!runCR4);
  runCR4 = true;
  COROUTINE_DELAY(noteDurs[3] * 2);
  COROUTINE_AWAIT(!runCR1x1);
  runCR1x1 = true;
  COROUTINE_DELAY(noteDurs[4] * 2);
  COROUTINE_AWAIT(!runCR3x1);
  runCR3x1 = true;
  COROUTINE_DELAY(noteDurs[5] * 2);
  COROUTINE_AWAIT(!runCR2x1);
  runCR2x1 = true;
  COROUTINE_DELAY(noteDurs[6] * 2);
  COROUTINE_AWAIT(!runCR4x1);
  runCR4x1 = true;
  COROUTINE_DELAY(noteDurs[7] * 2);
  COROUTINE_AWAIT(!runCR1);
  runCR1 = true;
  COROUTINE_DELAY(noteDurs[8] * 2);
  COROUTINE_AWAIT(!runCR3);
  runCR3 = true;
  COROUTINE_DELAY(noteDurs[9] * 2);
  COROUTINE_AWAIT(!runCR2);
  runCR2 = true;
  COROUTINE_DELAY(noteDurs[10] * 2);

  Serial.print("end bar 1");

  //bar 2
  COROUTINE_AWAIT(!runCR1x1);
  runCR1x1 = true;
  COROUTINE_DELAY(noteDurs[11] * 2);
  COROUTINE_AWAIT(!runCR3x1);
  runCR3x1 = true;
  COROUTINE_DELAY(noteDurs[12] * 2);
  COROUTINE_AWAIT(!runCR2x1);
  runCR2x1 = true;
  COROUTINE_DELAY(noteDurs[13] * 2);
  COROUTINE_AWAIT(!runCR4);
  runCR4 = true;
  COROUTINE_DELAY(noteDurs[14] * 2);
  COROUTINE_AWAIT(!runCR1);
  runCR1 = true;
  COROUTINE_DELAY(noteDurs[15] * 2);
  COROUTINE_AWAIT(!runCR3);
  runCR3 = true;
  COROUTINE_DELAY(noteDurs[16] * 2);
  COROUTINE_AWAIT(!runCR2);
  runCR2 = true;
  COROUTINE_DELAY(noteDurs[17] * 2);
  COROUTINE_AWAIT(!runCR4x1);
  runCR4x1 = true;
  COROUTINE_DELAY(noteDurs[18] * 2);
  COROUTINE_AWAIT(!runCR1x1);
  runCR1x1 = true;
  COROUTINE_DELAY(noteDurs[19] * 2);
  COROUTINE_AWAIT(!runCR3x1);
  runCR3x1 = true;
  COROUTINE_DELAY(noteDurs[20] * 2);
  COROUTINE_AWAIT(!runCR2x1);
  runCR2x1 = true;
  COROUTINE_DELAY(noteDurs[21] * 2);

  //bar 3
  COROUTINE_AWAIT(!runCR1);
  runCR1 = true;
  COROUTINE_DELAY(noteDurs[22] * 2);
  COROUTINE_AWAIT(!runCR3);
  runCR3 = true;
  COROUTINE_DELAY(noteDurs[23] * 2);
  COROUTINE_AWAIT(!runCR2);
  runCR2 = true;
  COROUTINE_DELAY(noteDurs[24] * 2);
  COROUTINE_AWAIT(!runCR4);
  runCR4 = true;
  COROUTINE_DELAY(noteDurs[25] * 2);
  COROUTINE_AWAIT(!runCR1x1);
  runCR1x1 = true;
  COROUTINE_DELAY(noteDurs[26] * 2);
  COROUTINE_AWAIT(!runCR3x1);
  runCR3x1 = true;
  COROUTINE_DELAY(noteDurs[27] * 2);
  COROUTINE_AWAIT(!runCR2x1);
  runCR2x1 = true;
  COROUTINE_DELAY(noteDurs[28] * 2);
  COROUTINE_AWAIT(!runCR4x1);
  runCR4x1 = true;
  COROUTINE_DELAY(noteDurs[29] * 2);
  COROUTINE_AWAIT(!runCR1);
  runCR1 = true;
  COROUTINE_DELAY(noteDurs[30] * 2);
  COROUTINE_AWAIT(!runCR3);
  runCR3 = true;
  COROUTINE_DELAY(noteDurs[31] * 2);
  COROUTINE_AWAIT(!runCR2);
  runCR2 = true;
  COROUTINE_DELAY(noteDurs[32] * 2);

  //bar 4
  COROUTINE_AWAIT(!runCR1x1);
  runCR1x1 = true;
  COROUTINE_DELAY(noteDurs[33] * 2);
  COROUTINE_AWAIT(!runCR3x1);
  runCR3x1 = true;
  COROUTINE_DELAY(noteDurs[34] * 2);
  COROUTINE_AWAIT(!runCR2x1);
  runCR2x1 = true;
  COROUTINE_DELAY(noteDurs[35] * 2);
  COROUTINE_AWAIT(!runCR4);
  runCR4 = true;
  COROUTINE_DELAY(noteDurs[36] * 2);
  COROUTINE_AWAIT(!runCR1);
  runCR1 = true;
  COROUTINE_DELAY(noteDurs[37] * 2);
  COROUTINE_AWAIT(!runCR3);
  runCR3 = true;
  COROUTINE_DELAY(noteDurs[38] * 2);
  COROUTINE_AWAIT(!runCR2);
  runCR2 = true;
  COROUTINE_AWAIT(!runCR2);

  runShowScore = true;
 }
}

void setup() 
{
 // These lines are specifically to support the Adafruit Trinket 5V 16 MHz.
 // Any other board, you can remove this part (but no harm leaving it):
 #if defined(__AVR_ATtiny85__) && (F_CPU == 16000000)
  clock_prescale_set(clock_div_1);
 #endif
 // END of Trinket-specific code.

 Serial.begin(9600);
 Serial.print("start");
 pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)

 pinMode(buttonPin1, INPUT);
 pinMode(buttonPin2, INPUT);
 pinMode(buttonPin3, INPUT);
 pinMode(buttonPin4, INPUT);

 //Call the void to fill the lists with notes and freqs
 FillSong();

 //Start the song
 runSong = true;
}

void loop() 
{
 //READ THE BUTTON STATES
 buttonState1 = digitalRead(buttonPin1);
 buttonState2 = digitalRead(buttonPin2);
 buttonState3 = digitalRead(buttonPin3);
 buttonState4 = digitalRead(buttonPin4);

 //Run all the coroutines
 songCR.runCoroutine();
 playBuzzer.runCoroutine();
 errorSound.runCoroutine();

 lightStrand1CR.runCoroutine();
 lightStrand2CR.runCoroutine();
 lightStrand3CR.runCoroutine();
 lightStrand4CR.runCoroutine();

 lightStrand1CRx1.runCoroutine();
 lightStrand2CRx1.runCoroutine();
 lightStrand3CRx1.runCoroutine();
 lightStrand4CRx1.runCoroutine();

 showScore.runCoroutine();

 //Read button 1
 if (buttonState1 == HIGH)
 {
  if (!button1Pressed)
  {
   button1Pressed = true;

   if (onLastLight1)
   {
    onLastLight1 = false;
    runPlaySound = true;
    ls1Hit = true;
   }
   else
   {
    runErrorSound = true;
    notesMissed++;
   }
  }
 }
 else
 {
  button1Pressed = false;
 }

 //Read button 2
 if (buttonState2 == HIGH)
 {
  if (!button2Pressed)
  {
   button2Pressed = true;

   if (onLastLight2)
   {
    onLastLight2 = false;
    ls2Hit = true;
    runPlaySound = true;
   }
   else
   {
    runErrorSound = true;
    notesMissed++;
   }
  }
 }
 else
 {
  button2Pressed = false;
 }

 //Read button 3
 if (buttonState3 == HIGH)
 {
  if (!button3Pressed)
  {
   button3Pressed = true;

   if (onLastLight3)
   {
    onLastLight3 = false;
    runPlaySound = true;
    ls3Hit = true;
   }
   else
   {
    runErrorSound = true;
    notesMissed++;
   }
  }
 }
 else
 {
  button3Pressed = false;
 }

 //Read button 4
 if (buttonState4 == HIGH)
 {
  if (!button4Pressed)
  {
   button4Pressed = true;

   if (onLastLight4)
   {
    onLastLight4 = false;
    runPlaySound = true;
    ls4Hit = true;
   }
   else
   {
    runErrorSound = true;
    notesMissed++;
   }
  }
 }
 else
 {
  button4Pressed = false;
 }
}

//Fill the song with notes. You can change these values to whatever your heart desires.
//Right now it's playing the start of Toby Fox's Megalovania
void FillSong()
{
 //bar 1
 noteFreqs.add(293);
 noteDurs.add(140);

 noteFreqs.add(293);
 noteDurs.add(122);

 noteFreqs.add(587);
 noteDurs.add(248);

 noteFreqs.add(440);
 noteDurs.add(368);

 noteFreqs.add(415);
 noteDurs.add(256);

 noteFreqs.add(392);
 noteDurs.add(242);

 noteFreqs.add(349);
 noteDurs.add(251);

 noteFreqs.add(293);
 noteDurs.add(121);

 noteFreqs.add(349);
 noteDurs.add(121);

 noteFreqs.add(392);
 noteDurs.add(128);

 //bar 2
 noteFreqs.add(261);
 noteDurs.add(140);

 noteFreqs.add(261);
 noteDurs.add(122);

 noteFreqs.add(587);
 noteDurs.add(248);

 noteFreqs.add(440);
 noteDurs.add(368);

 noteFreqs.add(415);
 noteDurs.add(256);

 noteFreqs.add(392);
 noteDurs.add(242);

 noteFreqs.add(349);
 noteDurs.add(251);

 noteFreqs.add(293);
 noteDurs.add(121);

 noteFreqs.add(349);
 noteDurs.add(121);

 noteFreqs.add(392);
 noteDurs.add(128);

 //bar 3
 noteFreqs.add(246);
 noteDurs.add(140);

 noteFreqs.add(246);
 noteDurs.add(122);

 noteFreqs.add(587);
 noteDurs.add(248);

 noteFreqs.add(440);
 noteDurs.add(368);

 noteFreqs.add(415);
 noteDurs.add(256);

 noteFreqs.add(392);
 noteDurs.add(242);

 noteFreqs.add(349);
 noteDurs.add(251);

 noteFreqs.add(293);
 noteDurs.add(121);

 noteFreqs.add(349);
 noteDurs.add(121);

 noteFreqs.add(392);
 noteDurs.add(128);

 //bar 4
 noteFreqs.add(233);
 noteDurs.add(140);

 noteFreqs.add(233);
 noteDurs.add(122);

 noteFreqs.add(587);
 noteDurs.add(248);

 noteFreqs.add(440);
 noteDurs.add(368);

 noteFreqs.add(415);
 noteDurs.add(256);

 noteFreqs.add(392);
 noteDurs.add(242);

 noteFreqs.add(349);
 noteDurs.add(251);

 noteFreqs.add(293);
 noteDurs.add(121);

 noteFreqs.add(349);
 noteDurs.add(121);

 noteFreqs.add(392);
 noteDurs.add(128);
}

Showcase

Showcase video RhythmBox
Showcase video RhythmBox 100%

In the first video, I purpously mess up the first 4 notes to show what happens if you do miss the timing (an error noise playes and you get less points). If you hit the timing, the correct note plays. At the end you can see that the leftmost strip is still turned off, thats cause I only got 32 points: i hit 4 notes before they reached the bottom, and i missed those 4 notes as well, so thats -8 points.

In the second video, I did it perfectly, and scored all 40 points, which lights up the entire board at the end. Sometimes it looks like the final light isnt lighting up, but that seems to be the low framerate of the video.

Conclusion

Going into this project I was very new to a lot of it. I had never soldered or worked with an arduino or anything like it before. I had some coding experience, mainly C#, which did help tremendously with getting the code to work. Because I was completely new to a lot of it I wanted to keep things simple, and I did. I'm quite happy with how it turned out, the end result is very close to what I had in mind when I came up with the idea. The wiring and casing could be more polished, but I don't hate how the end result looks.

I learned quite a bit of new things while working on this project. Mainly soldering and electronic circuits, but even just the craftmanship of working with a rotary tool was new to me. I'd feel a lot more confident going into a similar project in the future then I was at the start of this project.