Tutorial 01. Program structure and audio output
There are several functions that form the skeleton of any MEAP program. The first set of tutorials are designed to get you familiar with this structure that you will be building your program around.
Like many computer music programs, MEAP separates events into two categories:
- Audio rate events: Processes that actually generate or modify audio, such as an oscillator or a filter affecting that oscillator's output. These processes need to be calculated at a rate in the range of human hearing. MEAP's default sampling rate (which is the rate at which these audio rate processes are updated is 32768 Hz. This allows frequencies up to 16,384 to be generated without aliasing. This is lower than standard sampling rates but allows us to implement more complex sounds than if it was higher!
- Control rate events: Events that don't need to be updated as often as audio rate events, such as an LFO modulating the cutoff frequency of a filter. In most cases, our ears won't be able to tell the difference between that LFO being updated 32768 times per second or 128 times per second, so we can save some computational resources by updating it less often. MEAP's default control rate is 128, which should work fine for most use cases. Increasing this is always a tradeoff. You will get smoother controls as the control rate increases but you may reach a point where the processor doesn't have time to calculate all of these which will cause glitches in your output.
When working with microcontrollers, it is important to understand that it won't be able to handle everything you throw at it the was a modern laptop or desktop computer might and we need to do our best to not ask too much of it. A big part of this is not asking it to calculate things more often than we need them. Running as many events as possible at control rate rather than audio rate will help ease the load!
. . . . . . .
The following functions occur in every MEAP program and are the main places you will be adding code.
AudioOutput_t updateAudio()
updateAudio() runs at the sampling rate. It is in charge of generating new samples of audio and sending them to the output. This is where most of your audio generation code will live; such as grabbing samples from oscillators or processing them with filters or effects.
void updateControl()
updateControl() runs at the control rate. This is where the brains of your program go. Changing the frequency of oscillators, running compositional structures, etc.
void setup()
setup() is executed once each time your board is powered up or reset. Code placed in here is typically used for initialization; for example setting the initial frequency of an oscillator, the shape of an envelope or the starting state of a generative algorithm.
void loop()
loop() runs repeatedly at a high speed between calls to updateControl() and updateAudio(). It handles some behind the scenes parts of the audio engine, and generally you should avoid adding extra code to this section as too many calculations may disrupt your sampling rate.
. . . . . . .
So let's write our first MEAP program!
-
Start by opening
MEAP_BASIC_TEMPLATE
within theTesting and Templates
folder of the MEAP library. This template is a good starting point for any MEAP program as it includes all of the code blocks you will typically use. The template won't generate any sound on its own though so let's modify it to just output a constant sine wave.Create a new Arduino sketch by pressing CMD+N and copy all of the template code into it.
Most of the audio functions we will be using to begin with come from classes in the Mozzi library which are all extensively documented. For this tutorial, we will be using the mOscil class (which is based on Mozzi's Oscil class, with a few minor bug fixes) which implements a basic wavetable-based oscillator. Before we can use the class, however, we will need to choose a wavetable, which will determine the waveform of our oscillator. The mozzi library includes many wavetables which can be found in the
tables
directory of the Mozzi library within your Arduino libraries folder, but for this example we will be using theirsin8192_int8
wavetable.To include this wavetable, add the following line to your global variable section (beneath the line that says
// ---------- YOUR GLOBAL VARIABLES BELOW ----------
)#include<tables/sin8192_int8.h>
Now that we have included the wavetable in our sketch, we can create an oscillator that uses it. Add the following line to your global variable section as well:
mOscil<8192, AUDIO_RATE> my_sine(SIN8192_DATA);
This line creates a single oscillator, tells it how many samples are in the wavetable, specifies that it will be an audio rate oscillator (rather than a control rate LFO), names it "my_sine" and finally points it to the wavetable we included.
In the setup() function, we want to specify the frequency of this oscillator. Beneath the
// ---------- YOUR SETUP CODE BELOW ----------
line, add the following:my_sine.setFreq(220);
This will tell the oscillator to continuously cycle at 220Hz (an A3 in 12-tone equal temperament) until it is told to change to a different frequency.
Now we just need to connect this oscillator to the audio output.
In the updateAudio() function, rather than setting the variable
out_sample
equal to zero we want to set it to the output ofmy_sine
as follows.int out_sample = my_sine.next();
You can leave the rest of the function as is and upload the code to your MEAP! You should hear a constant sine on both output channels.
- Save this code as it will be the starting point of the next couple tutorials.
FULL CODE BELOW
/*
Constant sine wave output
*/
#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 <tables/sin8192_int8.h> // loads sine wavetable
mOscil<8192, AUDIO_RATE> my_sine(SIN8192_DATA);
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 ----------
my_sine.setFreq(220);
}
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 ----------
}
/** 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 = my_sine.next();
return StereoOutput::fromNBit(8, (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) {
if (pressed) { // Any pad pressed
} else { // Any pad released
}
switch (number) {
case 0:
if (pressed) { // Pad 0 pressed
Serial.println("t0 pressed ");
} else { // Pad 0 released
Serial.println("t0 released");
}
break;
case 1:
if (pressed) { // Pad 1 pressed
Serial.println("t1 pressed");
} else { // Pad 1 released
Serial.println("t1 released");
}
break;
case 2:
if (pressed) { // Pad 2 pressed
Serial.println("t2 pressed");
} else { // Pad 2 released
Serial.println("t2 released");
}
break;
case 3:
if (pressed) { // Pad 3 pressed
Serial.println("t3 pressed");
} else { // Pad 3 released
Serial.println("t3 released");
}
break;
case 4:
if (pressed) { // Pad 4 pressed
Serial.println("t4 pressed");
} else { // Pad 4 released
Serial.println("t4 released");
}
break;
case 5:
if (pressed) { // Pad 5 pressed
Serial.println("t5 pressed");
} else { // Pad 5 released
Serial.println("t5 released");
}
break;
case 6:
if (pressed) { // Pad 6 pressed
Serial.println("t6 pressed");
} else { // Pad 6 released
Serial.println("t6 released");
}
break;
case 7:
if (pressed) { // Pad 7 pressed
Serial.println("t7 pressed");
} else { // Pad 7 released
Serial.println("t7 released");
}
break;
}
}
/**
* 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) { // Any DIP toggled up
} else { //Any DIP toggled down
}
switch (number) {
case 0:
if (up) { // DIP 0 up
Serial.println("d0 up");
} else { // DIP 0 down
Serial.println("d0 down");
}
break;
case 1:
if (up) { // DIP 1 up
Serial.println("d1 up");
} else { // DIP 1 down
Serial.println("d1 down");
}
break;
case 2:
if (up) { // DIP 2 up
Serial.println("d2 up");
} else { // DIP 2 down
Serial.println("d2 down");
}
break;
case 3:
if (up) { // DIP 3 up
Serial.println("d3 up");
} else { // DIP 3 down
Serial.println("d3 down");
}
break;
case 4:
if (up) { // DIP 4 up
Serial.println("d4 up");
} else { // DIP 4 down
Serial.println("d4 down");
}
break;
case 5:
if (up) { // DIP 5 up
Serial.println("d5 up");
} else { // DIP 5 down
Serial.println("d5 down");
}
break;
case 6:
if (up) { // DIP 6 up
Serial.println("d6 up");
} else { // DIP 6 down
Serial.println("d6 down");
}
break;
case 7:
if (up) { // DIP 7 up
Serial.println("d7 up");
} else { // DIP 7 down
Serial.println("d7 down");
}
break;
}
}