Today is May 4th, Star Wars Day! And we are going to celebrate it with a fun project, playing the Imperial March with a processor like Arduino.
But just playing the imperial march on a speaker or buzzer would be too simple, so instead we are going to use a stepper motor as a “musical instrument”.
Actually, you can use not only a stepper motor. Almost anything that makes noise and can be controlled (servo, DC motors…) can work for you.
In fact, on YouTube you will find amazing creations, with floppy drives, with hard drives, with cans… seriously, search for it, there are amazing things.
We are not going to spend so much time on it, and we are satisfied with a motor. But if you want to expand it to more instruments, the base is the same.
Imperial March on Arduino
Let’s start with the simplest case, which is playing the Imperial March with a buzzer or passive speaker, as we saw in this post.
The core of our program is the ImperialMarch class, which contains the melody of the Imperial March. It looks like this.
typedef void(*PlayNoteCallback)(int note, int duration);
class ImperialMarch
{
public:
PlayNoteCallback playNoteCallback;
void playSong()
{
firstSection();
secondSection();
firstVariant();
secondSection();
secondVariant();
}
void playNote(int note, int duration)
{
playNoteCallback(note, duration);
delay(20);
}
private:
const int c = 261;
const int d = 294;
const int e = 329;
const int f = 349;
const int g = 391;
const int gS = 415;
const int a = 440;
const int aS = 455;
const int b = 466;
const int cH = 523;
const int cSH = 554;
const int dH = 587;
const int dSH = 622;
const int eH = 659;
const int fH = 698;
const int fSH = 740;
const int gH = 784;
const int gSH = 830;
const int aH = 880;
void firstSection()
{
playNote(a, 500);
playNote(a, 500);
playNote(a, 500);
playNote(f, 350);
playNote(cH, 150);
playNote(a, 500);
playNote(f, 350);
playNote(cH, 150);
playNote(a, 650);
delay(500);
playNote(eH, 500);
playNote(eH, 500);
playNote(eH, 500);
playNote(fH, 350);
playNote(cH, 150);
playNote(gS, 500);
playNote(f, 350);
playNote(cH, 150);
playNote(a, 650);
delay(500);
}
void secondSection()
{
playNote(aH, 500);
playNote(a, 300);
playNote(a, 150);
playNote(aH, 500);
playNote(gSH, 325);
playNote(gH, 175);
playNote(fSH, 125);
playNote(fH, 125);
playNote(fSH, 250);
delay(325);
playNote(aS, 250);
playNote(dSH, 500);
playNote(dH, 325);
playNote(cSH, 175);
playNote(cH, 125);
playNote(b, 125);
playNote(cH, 250);
delay(350);
}
void firstVariant()
{
playNote(f, 250);
playNote(gS, 500);
playNote(f, 350);
playNote(a, 125);
playNote(cH, 500);
playNote(a, 375);
playNote(cH, 125);
playNote(eH, 650);
delay(500);
}
void secondVariant()
{
playNote(f, 250);
playNote(gS, 500);
playNote(f, 375);
playNote(cH, 125);
playNote(a, 500);
playNote(f, 375);
playNote(cH, 125);
playNote(a, 650);
delay(650);
}
};
As we can see, it first has the definition of the frequencies for the notes. Next, we have the “music sheet” itself, in the form of note and its duration.
To get the melody, the usual approach would be to start with a Midi file, and export an instrument. Another very good source are the old ringtones (yes, those from old mobile phones). But this is worth a separate post.
The other important point of our class is the callback function playNoteCallback(note, duration), which contains the function that the class will use to play a note.
Now, to use our class in the main program, we would do the following.
#include <./imperialMarch.hpp>
ImperialMarch imperialMarch;
void setup()
{
imperialMarch.playNoteCallback = beep;
}
void loop()
{
imperialMarch.playSong();
delay(500);
}
Where, as you can see, we have associated the beep() function of Arduino with our callback function. Doesn’t it look like a short and cute code? That’s it, that easy.
Why the callback function?
At this point, someone might ask, “Why on earth are you using a class and a callback function? It’s so easy to make a buzzer sound.”
Well, apart from trying to keep our code as clean as possible, mainly because we can reuse the ImperialMarch class for other sound sources.
For example, if we want to take this code to an M5StickC based on the ESP32, the code would be as follows:
#include <M5StickC.h>
#include <./imperialMarch.hpp>
ImperialMarch imperialMarch;
void playNote(int note, int duration)
{
ledcWriteTone(ledChannel, note);
delay(duration);
ledcWriteTone(ledChannel, 0);
}
void setup()
{
M5.begin();
ledcSetup(ledChannel, freq, resolution);
ledcAttachPin(servo_pin, ledChannel);
ledcWrite(ledChannel, 256);
imperialMarch.playNoteCallback = playNote;
}
void loop()
{
imperialMarch.playSong();
delay(500);
}
In other words, we can basically use the same code, simply changing the action to execute to play a note.
Here is the result.
Make that step by step sound!
By now, the end should be in sight. We can put anything that makes a sound in our playNoteCallback function, if we can control its frequency and duration.
For example, a motor makes a higher-pitched noise the faster it goes (until it becomes inaudible). So we can play with its speed to make a melody.
The case of controlling a stepper motor is one of the simplest, especially if we use a driver like the A4988 or similar, as we saw in this post.
#include <./imperialMarch.hpp>
const int STEPPER1_STEP = 0;
const int STEPPER1_DIR = 0;
ImperialMarch imperialMarch;
void playNote(int note, int duration)
{
auto start_time = millis();
auto step_time = 1000000 / note;
while (millis() - start_time < duration)
{
digitalWrite(STEPPER1_STEP, HIGH);
delayMicroseconds(100);
digitalWrite(STEPPER1_STEP, LOW);
delayMicroseconds(step_time - 100);
}
}
void setup()
{
pinMode(STEPPER1_DIR, OUTPUT);
pinMode(STEPPER1_STEP, OUTPUT);
digitalWrite(STEPPER1_DIR, HIGH);
digitalWrite(STEPPER1_STEP, LOW);
imperialMarch.playNoteCallback = playNote;
}
void loop()
{
imperialMarch.playSong();
delay(500);
}
And if we try it, it looks something like this.
I can tell you that live it’s much better than in the video, and I never get tired of watching the little motor spin faster and slower as it makes the different notes of the melody.
As we can see, a simple project that will make Darth Vader fans and any self-respecting geek happy, and that, in addition, we can do with the kids at home.
Do you dare to do something similar with a servo? Or with a DC motor? Or with more instruments? If you do and want to leave a comment, you are invited.
I leave you all the code on Github. May the force be with you, see you in the next post!
Download the code
All the code from this post is available for download on Github.