Build an Auduino Step Sequencer

Pages
Contributors: Nick Poole
Favorited Favorite 6

Modifying the Framework

Now that we have a basic understanding of how the standard Auduino sketch works, we can start adding some of our own procedures to the main loop. First let's lay out what it is that we're trying to accomplish.

This tutorial is all about building a "step sequencer," a synthesizer that will hold a number of different parameters and play them back in sequence. This means that, along with more analog inputs, we'll also need:

  • Global Variables to hold all of the parameters for each step
  • Controls for selecting and changing steps in the sequence (step editor)
  • A Mechanism for advancing through all steps in the sequence at a set rate (playback)
  • A Control for changing the step rate (tempo)

I threw in another cool feature: the ability to shift the parameters on all steps in the sequence at once. That way I can setup a sequence and run it while I sweep the filters around. Also, I can pitch-shift the entire sequence.

You can download the complete altered sketch here, if you're not into copy/paste.

Let's go through the new code and figure out where all of these changes are gonna live:

First, we'll need to establish a heaping handful of global variables...

language:c
int tempo = 100000;
int pattern = 0;
int counter = 0;

int a1 = 0; int a2 = 0; int a3 = 0; int a4 = 0; int a5 = 0; 
int b1 = 0; int b2 = 0; int b3 = 0; int b4 = 0; int b5 = 0; 
int c1 = 0; int c2 = 0; int c3 = 0; int c4 = 0; int c5 = 0; 
int d1 = 0; int d2 = 0; int d3 = 0; int d4 = 0; int d5 = 0; 
int e1 = 0; int e2 = 0; int e3 = 0; int e4 = 0; int e5 = 0; 
int f1 = 0; int f2 = 0; int f3 = 0; int f4 = 0; int f5 = 0; 
int g1 = 0; int g2 = 0; int g3 = 0; int g4 = 0; int g5 = 0; 
int h1 = 0; int h2 = 0; int h3 = 0; int h4 = 0; int h5 = 0; 

int live_sync_phase = 0;
int live_grain_phase = 0;
int live_grain_decay = 0;
int live_grain2_phase = 0;
int live_grain2_decay = 0;

The "tempo" variable will dictate how many loop cycles can elapse before we move to the next step in the sequence. The user will access this variable directly using an analog input. Next, we setup a variable called "pattern," which would be better thought of as "step." It controls which step in the 8-step sequence we're currently playing. We also need to create a counter, which will keep track of how many loop cycles we've executed. This will be compared to "tempo" during each iteration, so we know when to advance one step in the sequence.

The big block of alphanumerically labeled integer variables will be used to store the five grain synthesizer parameters (1-5) for each of the eight steps (a-h). This could be accomplished using a table as well. This way, it's obvious to the beginner what's going on later, because we won't be calling stuff from tables.

Finally, there is a set of five variables that contain what I call the "live" parameters -- the adjustments that are made to the entire 8-step sequence during play.

Now that we've created all of our global variables, we can start configuring our hardware. I added these chunks to the setup procedure:

language:c
  pinMode(39, OUTPUT); digitalWrite(39, LOW);
  pinMode(41, OUTPUT); digitalWrite(41, LOW);
  pinMode(43, OUTPUT); digitalWrite(43, LOW);
  pinMode(45, OUTPUT); digitalWrite(45, LOW);
  pinMode(47, OUTPUT); digitalWrite(47, LOW);
  pinMode(49, OUTPUT); digitalWrite(49, LOW);
  pinMode(51, OUTPUT); digitalWrite(51, LOW);
  pinMode(53, OUTPUT); digitalWrite(53, LOW);

  pinMode(24, INPUT); digitalWrite(24, HIGH);
  pinMode(26, INPUT); digitalWrite(26, HIGH);
  pinMode(28, INPUT); digitalWrite(28, HIGH);
  pinMode(30, INPUT); digitalWrite(30, HIGH);
  pinMode(32, INPUT); digitalWrite(32, HIGH);
  pinMode(34, INPUT); digitalWrite(34, HIGH);
  pinMode(36, INPUT); digitalWrite(36, HIGH);
  pinMode(38, INPUT); digitalWrite(38, HIGH);

The first eight lines correspond to the eight status LEDs that indicate the user's position in the sequence. The second set of eight are the eight buttons used to select a step in the sequence for editing. All of the LEDs are declared as outputs and default to the LOW position. Whereas, all of the buttons are declared as inputs, and the internal pull-up resistors are set for each.

And now, the main loop. I'll explain as much as I can in the comments:

language:c
void loop() {


  counter++; 

/* Most of the time, the main loop will just advance the counter while we continue generating noise. Each iteration, we check the counter against our "tempo" parameter to find out if it's time yet to jump to the next step. */

  if(counter>tempo){ //If it's time to advance to the next step:      



//Housecleaning: Just a few things to get out of the way since the counter is "full"

  counter=0; //Reset the counter variable      
  if(pattern==8){pattern=0;} //Make sure we're not about to go to imaginary step 9.
  pattern++; //Let all of the following code know that we're setting up for the next step      
  //Turn off all of the step indicator lights in preparation for lighting the correct one.
  digitalWrite(39, LOW);digitalWrite(41, LOW);digitalWrite(43, LOW);digitalWrite(45, LOW);
  digitalWrite(47, LOW);digitalWrite(49, LOW);digitalWrite(51, LOW);digitalWrite(53, LOW);



 //Live Tweaks: Read the analog inputs associated with each "live" parameter.

  live_sync_phase = map(analogRead(14),0,1023,-500,500);
  live_grain_phase = map(analogRead(10),0,1023,-200,200);
  live_grain_decay = map(analogRead(9),0,1023,-20,20);
  live_grain2_phase = map(analogRead(8),0,1023,-200,200);
  live_grain2_decay = map(analogRead(11),0,1023,-50,50);



//Tempo Control: Read the analog inputs associated with the "tempo" parameter.
  tempo = map(analogRead(15),0,1023,1000,32000);



//Grab the parameters for the step that we're now in. We'll use a series of case
//statements switched on the "pattern" variable that we incremented earlier.

/* In each of the case routines below you'll notice that we're addressing 
each of the existing Auduino parameters and making them equal to the stored 
parameter plus the associated "live" parameter. */

  switch(pattern){

    case 1:
    syncPhaseInc = a1 + live_sync_phase; grainPhaseInc = a2 + live_grain_phase; grainDecay = a3 + live_grain_decay; grain2PhaseInc = a4 + live_grain2_phase; grain2Decay = a5 + live_grain2_decay; digitalWrite(53, HIGH); break;
    case 2:
    syncPhaseInc = b1 + live_sync_phase; grainPhaseInc = b2 + live_grain_phase; grainDecay = b3 + live_grain_decay; grain2PhaseInc = b4 + live_grain2_phase; grain2Decay = b5 + live_grain2_decay; digitalWrite(51, HIGH); break;
    case 3:
    syncPhaseInc = c1 + live_sync_phase; grainPhaseInc = c2 + live_grain_phase; grainDecay = c3 + live_grain_decay; grain2PhaseInc = c4 + live_grain2_phase; grain2Decay = c5 + live_grain2_decay; digitalWrite(49, HIGH); break;
    case 4:
    syncPhaseInc = d1 + live_sync_phase; grainPhaseInc = d2 + live_grain_phase; grainDecay = d3 + live_grain_decay; grain2PhaseInc = d4 + live_grain2_phase; grain2Decay = d5 + live_grain2_decay; digitalWrite(47, HIGH); break;
    case 5:
    syncPhaseInc = e1 + live_sync_phase; grainPhaseInc = e2 + live_grain_phase; grainDecay = e3 + live_grain_decay; grain2PhaseInc = e4 + live_grain2_phase; grain2Decay = e5 + live_grain2_decay; digitalWrite(45, HIGH); break;
    case 6:
    syncPhaseInc = f1 + live_sync_phase; grainPhaseInc = f2 + live_grain_phase; grainDecay = f3 + live_grain_decay; grain2PhaseInc = f4 + live_grain2_phase; grain2Decay = f5 + live_grain2_decay; digitalWrite(43, HIGH); break;
    case 7:
    syncPhaseInc = g1 + live_sync_phase; grainPhaseInc = g2 + live_grain_phase; grainDecay = g3 + live_grain_decay; grain2PhaseInc = g4 + live_grain2_phase; grain2Decay = g5 + live_grain2_decay; digitalWrite(41, HIGH); break; 
    case 8:
    syncPhaseInc = h1 + live_sync_phase; grainPhaseInc = h2 + live_grain_phase; grainDecay = h3 + live_grain_decay; grain2PhaseInc = h4 + live_grain2_phase; grain2Decay = h5 + live_grain2_decay; digitalWrite(39, HIGH); break;
  }

//Check to see if the user is trying to change the step parameters.
//This series of statements simply check for a button press from each of
//the step buttons and call a function to change the indicated step.

    if(digitalRead(24)==LOW){changeStep(1);}
    if(digitalRead(26)==LOW){changeStep(2);}
    if(digitalRead(28)==LOW){changeStep(3);}
    if(digitalRead(30)==LOW){changeStep(4);}
    if(digitalRead(32)==LOW){changeStep(5);}
    if(digitalRead(34)==LOW){changeStep(6);}
    if(digitalRead(38)==LOW){changeStep(7);}
    if(digitalRead(36)==LOW){changeStep(8);}
}}

Hopefully that wasn't too confusing. Basically the main loop just counts up until the counter hits the "tempo" value, then it pops over to the next step in the sequence, loading up the parameters for that step and being sure to reset to step 1 when it passes step 8. Finally, it checks for user input from the step change buttons. If there's a button press (meaning that the user wants to change the step for that button) we call a function to alter the appropriate parameters and pass it the number of the step that we want to change:

language:c
void changeStep(int step_num){

/* The first thing we do is to turn off all indicator lights so that we can properly indicate which step we're currently editing. */

  digitalWrite(39, LOW);digitalWrite(41, LOW);digitalWrite(43, LOW);digitalWrite(45, LOW);
  digitalWrite(47, LOW);digitalWrite(49, LOW);digitalWrite(51, LOW);digitalWrite(53, LOW);  

// Then indicate the appropriate step.  

    switch(step_num){

    case 1:
    digitalWrite(53, HIGH); break;
    case 2:
    digitalWrite(51, HIGH); break;
    case 3:
    digitalWrite(49, HIGH); break;
    case 4:
    digitalWrite(47, HIGH); break;
    case 5:
    digitalWrite(45, HIGH); break;
    case 6:
    digitalWrite(43, HIGH); break;
    case 7:
    digitalWrite(41, HIGH); break; 
    case 8:
    digitalWrite(39, HIGH); break;
  }


/* This next chunk of code is fairly similar to the unaltered Auduino sketch. This allows 
us to continue updating the synth parameters to the user input. That way, you can dial in 
the sound of a particular step. The while-loop traps the program flow here until the user 
pushes button 1. As the code currently stands, "live" parameters aren't applied while in 
the step editor but you could easily add the live parameters below. */


while(1){  

  counter++;
  if(counter>tempo){

  counter=0;
  syncPhaseInc = mapPentatonic(analogRead(SYNC_CONTROL));

  grainPhaseInc  = mapPhaseInc(analogRead(GRAIN_FREQ_CONTROL)) / 2;
  grainDecay     = analogRead(GRAIN_DECAY_CONTROL) / 8;
  grain2PhaseInc = mapPhaseInc(analogRead(GRAIN2_FREQ_CONTROL)) / 2;
  grain2Decay    = analogRead(GRAIN2_DECAY_CONTROL) / 4; 

//Here we read the button 1 input and commit the step changes to the appropriate parameters.

  if(digitalRead(24)==LOW && step_num==1){
    a1 = syncPhaseInc; a2 = grainPhaseInc; a3 = grainDecay; a4 = grain2PhaseInc; a5 = grain2Decay; 
    return;}

  else if(digitalRead(24)==LOW && step_num==2){
    b1 = syncPhaseInc; b2 = grainPhaseInc; b3 = grainDecay; b4 = grain2PhaseInc; b5 = grain2Decay; 
    return;}

  else if(digitalRead(24)==LOW && step_num==3){
    c1 = syncPhaseInc; c2 = grainPhaseInc; c3 = grainDecay; c4 = grain2PhaseInc; c5 = grain2Decay; 
    return;}

  else if(digitalRead(24)==LOW && step_num==4){
    d1 = syncPhaseInc; d2 = grainPhaseInc; d3 = grainDecay; d4 = grain2PhaseInc; d5 = grain2Decay; 
    return;}

  else if(digitalRead(24)==LOW && step_num==5){
    e1 = syncPhaseInc; e2 = grainPhaseInc; e3 = grainDecay; e4 = grain2PhaseInc; e5 = grain2Decay; 
    return;}

  else if(digitalRead(24)==LOW && step_num==6){
    f1 = syncPhaseInc; f2 = grainPhaseInc; f3 = grainDecay; f4 = grain2PhaseInc; f5 = grain2Decay; 
    return;}

  else if(digitalRead(24)==LOW && step_num==7){
    g1 = syncPhaseInc; g2 = grainPhaseInc; g3 = grainDecay; g4 = grain2PhaseInc; g5 = grain2Decay; 
    return;}

  else if(digitalRead(24)==LOW && step_num==8){
    h1 = syncPhaseInc; h2 = grainPhaseInc; h3 = grainDecay; h4 = grain2PhaseInc; h5 = grain2Decay; 
    return;}

    }
  }
}

With these pieces of code added, the Auduino sketch should now act as an 8-step sequencer.

Now that we have the firmware nailed down, let's get our hardware sorted. After all, a solid 32% of the fun is purely in turning the knobs and pushing the buttons.