Tutorial 17. A melody sequencer

In which we play a melody.


It's a common misconception that computer music is all about writing algorithms to generate music from scratch. There's no reason we can't just use a computer to play pre-programmed melodies! In this tutorial we'll look at an approach to playing a melody using EventDelay.

A melody can be conceptualized as a sequence of events; at certain points in time, a pitch will be chosen, a note will start, and a note will end. We wait an amount of time between these events that depends on the rhythm of our notes. EventDelay is a perfect tool for implementing a melody.

To implement a basic melody, we'll need two main things: an array of the sequence of notes in our melody, and an array of the time between note onsets. For simplicity's sake we'll be using MIDI note numbers to choose pitches, and the length of each note in sixteenth notes to handle rhythm.


For the purposes of this tutorial, we'll be recreating the intro melody from Untrust Us by Crystal Castles. The first step is to transcribe the melody which I have done in Ableton's piano roll below.

Once we know what notes we have, we want to write them in a way our MEAP can understand. We have 20 notes in this melody so we want to make an array containing the pitches of these 20 notes and an array containing how many sixteenth notes there are between note onsets. To make MIDI note entry easier, we can use MEAP's MIDI note definitions header file. This will allow us to write notes in terms of their musical pitch value rather than looking them up in a MIDI note table every time.

Writing our rhythms in terms of sixteenth notes similarly allows us to simplify note input. We can just calculate the length of a sixteenth note at our desired tempo once, and then convert all of our note lengths to milliseconds by multiplying by that number. Untrust Us is at 126 BPM so we can find the length of a sixteenth note as follows:

If we calculate that out, we'll find that there are 119 milliseconds per sixteenth note at 126 BPM. This approach simplifies note entry, but it also lets us change tempo easily. All we need to do is calculate a new sixteenth length and all the rhythms will follow our new tempo.

The final variable we want to create is an indexing variable which we will call sequence_index. We will use this variable to keep track of which note from the melody we need to play next. We'll initialize it to 0 to start at the beginning of our sequence.

int sequence_length = 20;
int sequence_pitches[20] = {mCs3, mFs3, mCs3, mA2, mCs3, mE3, mD3, mCs3, mB2, mA2, mCs3, mE3, mCs3, mA2, mCs3, mE3, mD3, mCs3, mB2, mA2};
int sequence_rhythms[20] = {2, 2, 2, 2, 2, 1, 1, 2, 1, 1, 2, 2, 2, 2, 2, 1, 1, 2, 1, 1};
int sequence_index = 0;
int sixteenth_length = 119.

Beyond that, we'll need to create our EventDelay object and a synth voice. We aren't going to go too deep into sound design in this tutorial but the synth in the song sounds like a filtered square wave so we'll just use a bandlimited square wave table for our oscillator.

#include <tables/sq8192_4harm_int8.h>
EventDelay metro1;
int metro1_period;  // period will be set by sequence below
mOscil<sq8192_4harm_int8_NUM_CELLS, AUDIO_RATE> oscil1(sq8192_4harm_int8_DATA);
ADSR<AUDIO_RATE, AUDIO_RATE> env1;

In setup() we'll set up our envelope:

env1.setADLevels(255, 0);  // set envelope sustain level to 0, effectively skipping sustain and release phases
env1.setTimes(1, 75, 1, 1);

And in updateAudio() we'll connect our synth objects together:

 env1.update();
int64_t out_sample = oscil1.next() * env1.next(); // 16bit osc * 8 bit env = 24bits
return StereoOutput::fromNBit(16, (out_sample * meap.volume_val) >> 12, (out_sample * meap.volume_val) >> 12);

Logic in updateControl()

As is often the case, the interesting part of the code comes in updateControl().

if (metro1.ready()) {
    metro1.start(sequence_rhythms[sequence_index] * sixteenth_length);
    oscil1.setFreq(mtof(sequence_pitches[sequence_index]));
    env1.noteOn();
    sequence_index = (sequence_index + 1) % sequence_length;
}

Inside of our metronome if statement:


Go ahead and upload the code. It should play our melody over and over again, unchanging forever.

Defining our melody as a sequence of numbers with an indexing variable allows a lot of flexibility in how the melody is played back (it also seems suspiciously similar to how samples and wavetables are stored and used). As an exercise, try playing the melody backwards, or skipping every other note and see how differnt possibilities can emerge from a single melody.


FULL CODE HERE