Playing DOOM on an Arduino

Or at least the music...

Playing Doom E1M1 Hangar Theme on Arduino

This is a short section of "At Doom's Gate", also known as the "E1M1 Hangar Theme". It's the very same tune used in the 2016 reboot for the collectible Doomguy dolls 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 won't 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
}

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].

References

  1. tone() function - Arduino reference
  2. Equal Temperament - Wikipedia
  3. toneAC Library for Arduino by Tim Eckel