Playing DOOM on an Arduino
Or at least the music...
This is a short section of "At Doom's Gate", also known as the "E1M1 Hangar Theme". It is the very same tune used in the 2016 reboot for the Doomguy collectible action figures when you pick them up.
Playing a Tone with Arduino
The Arduino library has a built in method [1] for generating a specific frequency using a PWM square wave. This means the sound will not be as smooth as a perfect sine wave, but that just makes it sound more like an old chiptune. To use actual notes, I made a lookup table for the frequencies using the tempered scale [2], having 8 octaves.
int const Notes[8][12] = {
{ 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62 },
{ 65, 69, 73, 78, 82, 87, 92, 98, 104, 110, 117, 123 },
{ 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247 },
{ 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494 },
{ 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988 },
{ 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976 },
{ 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951 },
{ 4186, 4435, 4699, 4978, 5274, 5588, 5920, 6272, 6645, 7040, 7459, 7902 }
};
#define NOTE_C 0
#define NOTE_CS 1
#define NOTE_D 2
#define NOTE_DS 3
#define NOTE_E 4
#define NOTE_F 5
#define NOTE_FS 6
#define NOTE_G 7
#define NOTE_GS 8
#define NOTE_A 9
#define NOTE_AS 10
#define NOTE_B 11
The tone() function needs two input parameters: the I/O pin for the speaker, and a frequency. The playback itself works concurrently and goes on indefinitely, until a noTone() call is issued. In this specific implementation the execution is halted until a note is over.
#define IO_SPEAKER 10
void setup()
{
pinMode(IO_SPEAKER, OUTPUT);
}
void playNote(int octave, int note, int duration)
{
tone(IO_SPEAKER, Notes[octave][note]);
delay(duration);
noTone(IO_SPEAKER);
}
At Doom's Gate
This solution can only produce monophonic tunes. Because the fast parts in the original song are polyphonic, I had to recompose them to sound as close to the original as possible.
inline void noteDoomBase(int octave, int speed) {
playNote(octave - 1, NOTE_E, speed / 2);
delay(speed / 2);
playNote(octave - 1, NOTE_E, speed);
}
void loop()
{
int octave = 3;
// Fast part
int speed = 64;
playNote(octave, NOTE_B, speed);
playNote(octave, NOTE_G, speed);
playNote(octave, NOTE_E, speed);
playNote(octave, NOTE_C, speed);
playNote(octave, NOTE_E, speed);
playNote(octave, NOTE_G, speed);
playNote(octave, NOTE_B, speed);
playNote(octave, NOTE_G, speed);
playNote(octave, NOTE_B, speed);
playNote(octave, NOTE_G, speed);
playNote(octave, NOTE_E, speed);
playNote(octave, NOTE_G, speed);
playNote(octave, NOTE_B, speed);
playNote(octave, NOTE_G, speed);
playNote(octave, NOTE_B, speed);
playNote(octave + 1, NOTE_E, speed);
// Main theme
speed = 128;
noteDoomBase(octave, speed);
playNote(octave, NOTE_E, speed);
noteDoomBase(octave, speed);
playNote(octave, NOTE_D, speed);
noteDoomBase(octave, speed);
playNote(octave, NOTE_C, speed);
noteDoomBase(octave, speed);
playNote(octave, NOTE_AS, speed);
noteDoomBase(octave, speed);
playNote(octave, NOTE_B, speed);
playNote(octave, NOTE_C, speed);
noteDoomBase(octave, speed);
playNote(octave, NOTE_E, speed);
noteDoomBase(octave, speed);
playNote(octave, NOTE_D, speed);
noteDoomBase(octave, speed);
playNote(octave, NOTE_C, speed);
noteDoomBase(octave, speed);
playNote(octave - 1, NOTE_AS, speed * 2);
delay(4000); // Wait 4 seconds before repeating
}
Download Full Code
Improvements
On the video I'm actually using the toneAC() function instead, which can provide twice the volume by alternating the polarity. It's not part of the Arduino core library, but you can check it out here [3].