Music Sequencing

Posted on May 18, 2020

GameMaker Studio 2 Tutorials

Overview

Before I continue, I want to point out that I wrote this in the new 2.3 update, but this can be changed around to work in previous versions. With that out of the way, we’re going to make a simple music sequencer. This is really primitive, only allowing basic waveforms and every instrument is monophonic.

Algorithm

  1. Define a song’s properties (tempo, subdivisions, length)
  2. Define a song’s instruments, with including volume and waveform properties.
  3. Define the instuments’ notes.
  4. Define a sample rate (I’ll use 32k)
  5. Create a buffer to hold the audio data.
  6. Generate samples based on instrument data. (This step is way too long to explain here; check the code)
  7. Turn that audio buffer into an audio ID to play.

Code

// Song Properties (BPM = 100, 1 "beat" = 8th note, length = 32 beats, sample rate = 32k)
tempo = 100
subdivision = 2
length = 32
sampleRate = 32000

// To hold the audio data
audioBuffer = -1
audioID = -1
    
// Instrument Definition 
instruments = [
    // Variable Pulse Wave Bass
    {
        sample: function(_info) { 
            // frac(_x) will always be in range [0, 1) for single cycle waveform use
            // floor(_x) can be used for changing the waveform
            var _x = _info[1] /_info[0];
            var _phase = (frac((_x div 10)/10))/4 + 0.375;
            return ((_x % 1) > _phase ? -1 : 1)
        },

      // Attack and decay are in seconds
      // The max volume we can have is [-32768, 32767], so choose your volumeBase wisely
        attack: 0,
        decay: 0.5,
        sustain: 0.75,
        volumeBase: 1600,
        volume: function(_time) {
            if ((_time) < attack) { return _time/attack; }
            if ((_time) < attack+decay) { return lerp(1, sustain, (_time-attack)/decay); }
            return sustain;
        },

        
        pattern: [ // 0 = A4, -1 = G#4, 1 = A#4, etc.; "" = don't play
            -17, "", "", "", -13, "", "", "",
            -12, "", "", "", -15, "", "", "",
            -13, "", "", "", -17, "", "", "",
            -12, "", "", "", -15, "", "", ""
        ]
        
    },
    // 25% Pulse Wave
    {
        sample: function(_info) {
            var _x = _info[1] /_info[0];
            return frac(_x) < 0.25 ? -1 : 1;
        },
        
        attack: 0,
        decay: 0.25,
        sustain: 0.25,
        volumeBase: 2000,
        volume: function(_time) {
            if ((_time) < attack) { return _time/attack; }
            if ((_time) < attack+decay) { return lerp(1, sustain, (_time-attack)/decay); }
            return sustain;
        },

        
        pattern: [
             7, 11, 23, 19, 11, 19, 23, 14,
             4,  9, 24, 21,  9, 21, 24, 16,
             4, 11, 23, 19, 11, 19, 23, 16,
             4, 12, 24, 19,  9, 21, 26, 18
        ]	
    }
]
    
GenerateAudio = function() {
    // Get total samples by the ratio of sample rate and beats per second, multiplied by total length
    var _samples = (sampleRate / ((tempo*subdivision)/60)) * length;
    // Multiply it by 2 here because we're gonna be dealing with 2-byte values
    audioBuffer = buffer_create(_samples*2, buffer_fixed, 1);
    buffer_seek(audioBuffer, buffer_seek_start, 0);

    // Store the last beat
    // Store info for each of the instruments (current note, elapsed samples, and elapsed seconds)
    var _beatLast = -1;
    var _noteInfo = array_create(array_length(instruments), array_create(3));

    for (var _i = 0; _i < _samples; _i++) {
        // Keep track of current position in song
        var _beat = floor((_i/sampleRate)*((tempo*subdivision)/60));
        var _newBeat = false;
        // If new beat, update beat and set flag for later
        if (_beatLast != floor(_beat)) {
            _newBeat = true;
            _beatLast = _beat;
        }

        // Keep track of final value	
        var _v = 0;
        // Loop through instruments
        for (var _j = 0; _j < array_length(instruments); _j++) {
            // Get instrument
            var _inst = instruments[_j];
            if (_newBeat) {
                // If a new beat has occurred, get current note, and trigger it 
                var _note = _inst.pattern[_beat];
                if is_real(_note) {
                    // Dividing the sample rate by the note's value in Hz
                    // This finds the number of samples needed to generate the note at its frequency
                    // Also, reset timer values
                    _noteInfo[_j][0] = sampleRate / (440 * power(2, (_note-12)/12)); 
                    _noteInfo[_j][1] = 0;
                    _noteInfo[_j][2] = 0;
                }
            }
            // Get instrument's sample and volume, and add it to the current value
            // Add 1 to the elapsed samples, and 1/sampleRate for elapsed seconds
            _v += _inst.sample(_noteInfo[_j]) * _inst.volume(_noteInfo[_j][2]) * _inst.volumeBase;
            _noteInfo[_j][1] += 1;
            _noteInfo[_j][2] += 1/sampleRate;
        }
        // Write the final value to the buffer
        buffer_write(audioBuffer, buffer_s16, _v);
    }
    // Create sound ID out of the buffer
    audioID = audio_create_buffer_sound(audioBuffer, 
                                        buffer_s16, 
                                        sampleRate, 
                                        0, 
                                        buffer_get_size(audioBuffer), 
                                        audio_mono);	
}

GenerateAudio();
audio_play_sound(audioID, 1, false);

Example

Here is the little tune I used to test this, and this is what the exact code above will give you.

Final Thoughts

Although this little sequencer is prefectly find for the project I’m working on, I can possibly add things like effects and PCM samples in the future. I can also possibly just turn this into a full-on .MOD player, but I don’t want to overwork myself. I’m sorry this post is more of a devlog, but I hope this “tutorial” was of any use to you.

Thanks for reading!

🠬 Previous Post: Poisson Disk Sampling