Let’s implement a minimalist MIDI software synthesizer by using sine waves!
Sample output:
About MIDI Synthesis
Software synthesizers convert MIDI data into audio signals to be either played through an audio device or stored to a file.
MIDI Data Examples
- Note On Event: a note starts playing with a given velocity at a given time.
- Note Off Event: a note stops playing at a given time.
Based on MIDI data, software synthesizers implement techniques in order to emulate the sound of real instruments.
A Minimal MIDI Synthesizer
Our synth aims at being as simple and fun to implement as possible.
Basically, for each playing note in the MIDI file we compute the corresponding sine wave and add it to the final waveform we’ll listen to.
Step 1: MIDI Data Collection
We collect the following MIDI data for each note to be played.
- note, which is the MIDI note number;
- velocity, which says how hard the note is played (from 0 to 127);
- start- and end-time, which say when the note starts and stops playing (in seconds).
Let S denote the total number of seconds of the MIDI tune (i.e. the maximum end-time).
Step 2: Waveform Initialization + Time Array
We zero-initialize the waveform array W of length ⌈44100×S⌉ that we’ll populate with the amplitude values (i.e. the samples) of the final waveform.
Namely, W will contain a number of 44100 samples for each second in the MIDI tune. (Notice that 44100 is a standard sampling rate for audio.)
We also create the time array T related to W by computing all the ⌈44100×S⌉ equidistant units of time (i.e. time steps) from 0 to S, namely
- The i-th time step T[i] is the time of the i-th sample W[i]; in other words
- The i-th sample W[i] stores the amplitude of the final waveform W at time t = T[i].
Step 3: Sine Waves Computation + Additive Synthesis
For each collected tuple (note, velocity, start-time, end-time) we compute the corresponding sine wave and add it to W:
- Get the frequency f associated to the note;
- Compute the amplitude A = velocity ÷ 127, i.e. normalize the velocity to a 0-1 range;
- Let s and e denote start- and end-time, respectively;
- Compute the sine wave A · sin(2π · f · t), for each t such that s ≤ t ≤ e.
- Add the sine wave to the waveform array W.
Summing up, we add the computed sine wave to W at the corresponding t-values as follows:
- For each i from ⌊44100×s⌋ to ⌊44100×e⌋, do
- t = T[i];
- W[i] = W[i] + A · sin(2π · f · t).
To understand Step 3 better, read:
- “Note names, MIDI numbers and frequencies” by Joe Wolfe, on how to compute a note frequency w.r.t. its distance to the note A4 (440 Hz), namely f = 2(note distance from A4)÷12 × 440;
- “Sinusoidal Waves as Sound” by Jonathan Rogness, on how to play multiple notes by adding their sine waves together, which includes nice plots and examples.
Final Step: Waveform Playback
By default we automatically play the computed waveform W through the speakers. On user request, we store it on a specified WAV file without playing it.
The Implementation
See the example and get the code!
This work was initially inspired by Robert Elder’s article “Compose Music From Entropy in /dev/urandom”.