How to generate music using code

A synthesizer that plays Tetris theme in 70 lines of C code

Antoine Champion
7 min readJul 5, 2022
Audio engineer working on his computer
Photo by Techivation on Unsplash

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 codeHere is the audio.

It’s all about numbers

An audio signal looks like this:

Representation of an audio signal through time

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:

Time is scaled to help visualization

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:

ADSR (Attack, Decay, Sustain, Release) envelope

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.

Musical sheet of Tetris Theme

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.

A sawtooth function

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:

Output of the synthesizer

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.

--

--