Tutorial 18. Drum sequencing
An approach to sequencing drums (or other sounds!) |
There are a multitude of approaches one might take to generate a drum pattern. In this tutorial we will take a look at a simple and intuitive method that can be used to sequence basic drum patterns. We are going to be creating a traditional grid-based drum sequencer where:
- All drums play a sequence of 32 beats.
- When we reach the end of the sequence, we loop back to the beginning.
- On every beat, each given drum either plays or does not play.
fig 1. a typical user interface for a grid-based sequencer like the one we will be coding
The first thing we need to do is load all of the samples we want to use. Because the number of samples we are using is growing, there are a few things I like to do to stay organized.
- I like to keep my project samples in a subdirectory called "samples". I create this directory and place all of my ".h" files in there.
- Rather than creating a separate mSample variable for each drum, I will create an array of mSamples with a size equal to the total number of samples I want to load.
If you want to follow along with this tutorial, download the 8 drum samples I used. Unzip and place the "samples" folder in the same directory as your .ino file.
After loading the samples, I create an array called
sample_gains
which contain 8-bit gain control
for each drum.
Next, I define my drum pattern. I create an array of 8 arrays of 32 steps each. Each of these sub-arrays corresponds to the pattern that each drum sound will play: the first is the kick, second is snare, etc. If the value on a step is "1", the drum will play on that step; if it is "0", it won't play on that step. In this case, each step represents one sixteenth note and the whole array represents two bars of music.
Finally, I set up a metronome and a variable to keep track of what step of the pattern we are currently playing.
In setup, we need to initialize each mSample by tying it to a table, specifying how long that sample is and setting the frequency of the sample.
In updateControl(), we move through each step of the drum pattern in sequence. To make it a little more interesting, we add some randomness. Pot 1 controls the chance of an entropy event happening. When the entropy event happens, a random step from the pattern is played instead of the step that would normally be played.
FULL CODE BELOW
/*
32-step drum pattern with 8 drums
pot 0 controls tempo
pot 1 controls randomness; random steps will be played
dip switches bring in/out each drum; up is enabled, down is disabled
*/
#define CONTROL_RATE 128 // Hz, powers of 2 are most reliable
#include <Meap.h> // MEAP library, includes all dependent libraries, including all Mozzi modules
Meap meap; // creates MEAP object to handle inputs and other MEAP library functions
MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI); // defines MIDI in/out ports
// ---------- YOUR GLOBAL VARIABLES BELOW ----------
#include "samples/kick.h"
#include "samples/snare1.h"
#include "samples/hhc.h"
#include "samples/hho.h"
#include "samples/shaker.h"
#include "samples/claves.h"
#include "samples/snare2.h"
#include "samples/snare3.h"
#define NUM_DRUMS 8
mSample<16715, AUDIO_RATE> drum_bank[NUM_DRUMS]; //maybe change the MAX_SAMPLE_LENGTH? calc how it corresponds to seconds etc
int drum_gains[NUM_DRUMS] = { 150, 127, 80, 60, 80, 127, 240, 127 }; // manually setting volume of each drum ~8-bit maximum
bool drum_enable[NUM_DRUMS] = { true, true, true, true, true, true, true, true };
float default_freq;
#define PATTERN_LENGTH 32
int sample_pattern[NUM_DRUMS][PATTERN_LENGTH] = {
{ 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1 }, // kick
{ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 }, // snare
{ 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // hi-hat closed
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // hi-hat open
{ 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0 }, // shaker
{ 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0 }, // clave
{ 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // snare 2
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // snare 3
};
int pattern_index = 0;
EventDelay metro;
int metro_period = 100;
void setup() {
Serial.begin(115200); // begins Serial communication with computer
Serial1.begin(31250, SERIAL_8N1, 43, 44); // sets up MIDI: baud rate, serial mode, rx pin, tx pin
startMozzi(CONTROL_RATE); // starts Mozzi engine with control rate defined above
meap.begin(); // sets up MEAP object
// ---------- YOUR SETUP CODE BELOW ----------
default_freq = (float)AUDIO_RATE / (float)16715;
// because our sample bank uses all drums of length 16715, we need to manually tell mSample how long each drum will actually be with setEnd
drum_bank[0].setTable(kick_DATA);
drum_bank[0].setEnd(kick_NUM_CELLS);
drum_bank[1].setTable(snare1_DATA);
drum_bank[1].setEnd(snare1_NUM_CELLS);
drum_bank[2].setTable(hhc_DATA);
drum_bank[2].setEnd(hhc_NUM_CELLS);
drum_bank[3].setTable(hho_DATA);
drum_bank[3].setEnd(hho_NUM_CELLS);
drum_bank[4].setTable(shaker_DATA);
drum_bank[4].setEnd(shaker_NUM_CELLS);
drum_bank[5].setTable(claves_DATA);
drum_bank[5].setEnd(claves_NUM_CELLS);
drum_bank[6].setTable(snare2_DATA);
drum_bank[6].setEnd(snare2_NUM_CELLS);
drum_bank[7].setTable(snare3_DATA);
drum_bank[7].setEnd(snare3_NUM_CELLS);
int drum_glitch_pitches = true;
// while writing this patch I made a mistake initializing the pitch of my drums
// but I thought it sounded nice so I left it in
// if you want to play your drums at the pitch they were recorded at, set drum_glitch_pitches to false and look at the "else" block below
if (drum_glitch_pitches) {
drum_bank[0].setFreq(((float)kick_SAMPLERATE / (float)kick_NUM_CELLS) / 6);
drum_bank[1].setFreq(((float)snare1_SAMPLERATE / (float)snare1_NUM_CELLS) / 4);
drum_bank[2].setFreq(((float)hhc_SAMPLERATE / (float)hhc_NUM_CELLS) / 5);
drum_bank[3].setFreq((float)hho_SAMPLERATE / (float)hho_NUM_CELLS);
drum_bank[4].setFreq((float)shaker_SAMPLERATE / (float)shaker_NUM_CELLS);
drum_bank[5].setFreq((float)claves_SAMPLERATE / (float)claves_NUM_CELLS);
drum_bank[6].setFreq((float)snare2_SAMPLERATE / (float)snare2_NUM_CELLS);
drum_bank[7].setFreq((float)snare3_SAMPLERATE / (float)snare3_NUM_CELLS);
} else {
for (int i = 0; i < 8; i++) {
drum_bank[i].setFreq(default_freq);
}
}
}
void loop() {
audioHook(); // handles Mozzi audio generation behind the scenes
}
/** Called automatically at rate specified by CONTROL_RATE macro, most of your mode should live in here
*/
void updateControl() {
meap.readInputs();
// ---------- YOUR updateControl CODE BELOW ----------
if (metro.ready()) {
metro_period = map(meap.pot_vals[0], 0, 4095, 300, 50); // pot 0 controls tempo
metro.start(metro_period);
int curr_step = pattern_index; // what step to we play next?
if (meap.irand(0, 4095) < meap.pot_vals[1]) { // if entropy event occurs, we play a random step instead
curr_step = meap.irand(0, 31);
}
for (int i = 0; i < NUM_DRUMS; i++) { // check which drums play on current step and trigger them
if (sample_pattern[i][curr_step] == 1) {
if (drum_enable[i]) {
drum_bank[i].start();
}
}
}
pattern_index = (pattern_index + 1) % 32; // move to next step and cycle to beginning if at end
}
}
/** Called automatically at rate specified by AUDIO_RATE macro, for calculating samples sent to DAC, too much code in here can disrupt your output
*/
AudioOutput_t updateAudio() {
int64_t out_sample = 0;
for (int i = 0; i < NUM_DRUMS; i++) {
out_sample += drum_bank[i].next() * drum_gains[i];
}
return StereoOutput::fromNBit(17, (out_sample * meap.volume_val) >> 12, (out_sample * meap.volume_val) >> 12);
}
/**
* Runs whenever a touch pad is pressed or released
*
* int number: the number (0-7) of the pad that was pressed
* bool pressed: true indicates pad was pressed, false indicates it was released
*/
void updateTouch(int number, bool pressed) {
}
/**
* Runs whenever a DIP switch is toggled
*
* int number: the number (0-7) of the switch that was toggled
* bool up: true indicated switch was toggled up, false indicates switch was toggled
*/
void updateDip(int number, bool up) {
if (up) {
drum_enable[number] = true;
} else {
drum_enable[number] = false;
}
}