A Minimalist MIDI Synth with Sine Waves

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:

  1. Get the frequency f associated to the note;
  2. Compute the amplitude A = velocity ÷ 127, i.e. normalize the velocity to a 0-1 range;
  3. Let s and e denote start- and end-time, respectively;
  4. Compute the sine wave A · sin(2π · f · t), for each t such that ste.
  5. 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:

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


© 2024 Massimo Nazaria

RSS

Licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.