How to generate music using code
A synthesizer that plays Tetris theme in 70 lines of C code
Digital sound processing is the art of manipulating audio using a computer. This article will dive into the underlooked world of sound programming, as we will create a real-time synthesizer and automate it to play the Tetris theme song.
Want to see the final result? Here is the code • Here is the audio.
It’s all about numbers
An audio signal looks like this:
When zooming a bit, you can see that this signal is a collection of points, whose values are between -1 and 1 and that are separated using a fixed time step:
For instance, the first point is 0.68 at time 0. The next point is 0.69 at time 10 milliseconds. The next one is 0.73 at time 20 milliseconds.
But this complex and squiggly signal is actually a sum of multiple harmonics. A single harmonic is a pure sine signal which represents a single note:
- The time period on the horizontal axis determines the note of the harmonic. In practice, we work with frequencies which are the inverse of time periods. For instance, a diapason generates the note A with a sine period of 2.27 milliseconds (440 Hertz).
- The amplitude on the vertical axis between the highest and the lowest point will determine the volume of the note. For instance, an harmonic with an amplitude of ±0.71 will have a volume of -3 decibels.
By adding harmonics together with different frequencies and volumes, you can create any audio signal.
Using Formula
Our goal is simple: programming a synthesizer that generates harmonics for a given note and automate it to play the Tetris theme.
We will use an open-source tool/mini-IDE for audio development in C/C++ called Formula which takes care of routing the sound to the operating system.
You can download Formula here (~20 MB).
This is the minimal working code in Formula:
The code block in formula_main must return a value between -1 and 1. This code is called multiple times to generate an audio signal from its successive output values.
Creating the synthesizer
First, let’s create a C function for the simplest possible synthesizer, which generates a single harmonic for a given frequency:
float synth(float frequency) {
float timePeriod = 1 / frequency;
float output = sin(2 * M_PI / timePeriod * TIME);
return output;
}
As we saw before, a harmonic is a sine function that progresses over time. We can access the current time in second using the Formula macro TIME. This time is multiplied by 2π because a sine is 2π-periodic. It is also divided to scale to our time period.
When Formula will call this synth() function over time, it will generate a sine signal:
Now, let’s write a very simple code inside the formula_main block to test this synthesizer function:
- Generate a A4 (La 4) using synth() for half a second. Frequency for A4 is 440 Hz.
- Pause for half a second
And repeat! Here is the corresponding code:
formula_main {
float output = 0;
float noteLength = 0.5; // A note lasts for half a second
float totalTime = noteLength * 2; // 1 note & 1 silence
float currentTime = fmod(TIME, totalTime); // Repeat
if (currentTime < noteLength) {
output = synth(440); // Generate A4 note
}
return output;
}
As we want this to repeat indefinitly, we use the fmod function (float modulo) so currentTime variable is set back to 0 when it exceeds totalTime.
After hitting the play button in the sidebar, you should hear a repeating note after running this code. You can mute the output with the speaker button. Here is what our code generates:
Creating an envelope
Still, our note has that annoying “buzzer” feeling. It starts and stop baldly without any nuance. In a real synthesizer, the notes have a smoothing envelope that controls their volume:
This envelope gives a “pluck” effect to each note, as they start hard and decay over time. To calculate the envelope value between the beginning and the end of each of its sections, we need to create a lerp() function (a.k.a. linear interpolation) which scales a value between a start and a destination interval.
float lerp(float x, float x1, float x2, float y1, float y2) {
return y1 + (x-x1) * (y2-y1) / (x2-x1) ;
}
Using this lerp()function, we can find the value of the envelope inside each of its 4 sections at any point in time. We can then create a function that takes as arguments an input point (sample), the time position of this point inside a note (t), and the total duration of the note (duration). This function will return that sample scaled by the current envelope value.
float adsr(float sample, float t, float duration) {
float attackTime = 0.01;
float decayTime = 0.1;
float sustainGain = 0.7;
float releaseTime = 0.1;
if (t < attackTime) {
sample *= lerp(t, 0, attackTime, 0, 1);
}
else if (t < attackTime + decayTime) {
sample *= lerp(t, attackTime, attackTime + decayTime, 1, sustainGain);
}
else if (t < duration - releaseTime) {
sample *= sustainGain;
}
else {
sample *= lerp(t, duration - releaseTime, duration, sustainGain, 0);
}
return sample;
}
This envelope function can be applied to our note right before returning it from the main block:
output = adsr(output, noteTime, noteLength);
return output;
The resulting notes should have the same pitch, but their start and end should be much smoother.
Creating the melody
As of now, our code repeats the same note over and over. How boring! Let’s setup an array of notes for the Tetris theme music.
Every note from the above sheet must be mapped to a harmonic frequency (look-up tables can be found online). Each of those 41 notes will be stored in a 2-dimensional array with its frequency and its duration:
const float notes[41][2] = {
{659.25, 0.5}, {493.88, 0.25}, {523.25, 0.25}, {587.33, 0.5}, {523.25, 0.25}, {493.88, 0.25},
{440.00, 0.5}, {440.00, 0.25}, {523.25, 0.25}, {659.25, 0.5}, {587.33, 0.25}, {523.25, 0.25},
{493.88, 0.5}, {493.88, 0.25}, {523.25, 0.25}, {587.33, 0.5}, {659.25, 0.5},
{523.25, 0.5}, {440.00, 0.5}, {440.00, 0.5}, {0, 0.5},
{0, 0.25}, {587.33, 0.5}, {698.46, 0.25}, {880.00, 0.5}, {783.99, 0.25}, {698.46, 0.25},
{659.25, 0.75},{523.25, 0.25}, {659.25, 0.5}, {587.33, 0.25}, {523.25, 0.25},
{493.88, 0.5}, {493.88, 0.25}, {523.25, 0.25}, {587.33, 0.5}, {659.25, 0.5},
{523.25, 0.5}, {440.00, 0.5}, {440.00, 0.5}, {0, 0.5},
};
const float totalDuration = 16;
Now that we have all the notes, the formula_main block has to be rewritten to pick the right note from the above array depending on the current time. We are going through the above array note after note by summing their durations. We know that our note is the current one when the sum exceeds the current time.
formula_main {
float output = 0;
float musicTime = fmod(TIME, totalDuration);
float noteLength, noteFreq;
for (int i = 0; i < 41; i++) {
noteFreq = notes[i][0];
noteLength = notes[i][1];
if (musicTime < noteLength) {
break;
}
else {
musicTime -= noteLength;
}
}
output = synth(noteFreq);
output = adsr(output, musicTime, noteLength);
return output;
}
Here we go! You should hear the Tetris theme after clicking play.
Improving the synthesizer
Our synth is a bit basic as it is a single sine function. We will use a sawtooth function instead which has a richer sound color.
Generating a sawtooth function is simple. It is a straight line that increases over time and goes back to -1 every time period. Hence, we need to modulo our straight line every period, and scale it back to our frequency: Sawtooth = Frequency * (Time % Period).
We will layer multiple sawtooths together. Each will have a frequency offset of 0.5%: not enough to be out of tune but enough to add thickness to our instrument.
Finally, let’s reuse our first sine synthesizer as a bass: we will divide its input frequency by 4 so it will be 2 octaves lower. In addition, its ouput will be raised to the power of 5 to add grittiness.
float synth(float freq) {
float output = (freq*fmod(TIME, 1./freq)-0.5);
output += (freq*fmod(TIME, 1./(freq*1.005))-0.5);
output += (freq*fmod(TIME, 1./(freq*0.995))-0.5);
output += pow(sin(2*M_PI*freq / 4 * TIME), 5);
return output / 4;
}
The audio signal with the 3 sawtooth voices and the sine added together looks like this:
This is all it takes to have a working synthesizer playing the Tetris theme song! Complete code can be found here and here is the audio result.
In case you are interested to learn more about audio programming, here are a couple of resources.
- Audio signal processing on Coursera (beginner)
- WolfSound Youtube channel (intermediate)
- Audio Effects by Reiss and McPherson (advanced)
- Formula Cloud audio effects (advanced)