Tutorial 07. Audio filters
With the basics of controlling your MEAP out of the way, it's time to start making some more interesting sounds. |
Filters are fundamental and powerful tools in the world of synthesis. They are the easiest way in which we can shape raw sounds into something more focused. We will start by looking at a filter closely matches those found on analog synthesizers.
Mozzi's MultiResonantFilter implements a state variable filter, a classic filter design in the analog synthesizer world. It's a nice filter to implement digitally because it is relatively easy for a computer to calculate and it gives us Lowpass, Highpass, Bandpass and Notch filter outputs for no additional computational cost which makes it a very versatile filter.
fig 1. plot of frequency (x-axis) vs amplitude (y-axis) of the four outputs of a state variable filter
Simple filters like this typically have two parameters you can adjust.
- The cutoff frequency (fc): This is the frequency where the filter begins doing whatever it is doing. For a lowpass filter, the filter will allow frequencies below fc to pass, and will attenuate frequencies above fc. A highpass will do the inverse, cutting out frequencies below fc and passing through frequencies above fc. A bandpass will allow frequencies near fc through, cutting out all else and a notch will do the opposite, allowing all frequencies through except those near fc.
- Resonance (Q): Filters with resonance will cause
a peak in frequency at fc. This is a large part of what
makes filters sound so "powerful" in synthesizers; a
filter without resonance can go unnoticed, but a filter
with resonance will make itself known with powerful
filter sweeps. Q allows us to adjust how
much of this resonance is present, with high values
corresponding to a very audible and narrow resonant
tone. A high Q on a bandpass or notch will make the
range of frequencies effected narrower but more
prominent.
Since a white noise signal contains all frequencies, if we put one through a filter, we can easily hear the character of that filter (ie. what frequencies it attenuates or amplifies)
Let's start with MEAP_BASIC_TEMPLATE and create a white noise generator by using a table of white noise as an mOscil's table.
#include <tables/whitenoise8192_int8.h> // loads white noise wavetable
mOscil<WHITENOISE8192_NUM_CELLS, AUDIO_RATE> white_noise(WHITENOISE8192_DATA);
Then create a filter:
MultiResonantFilter filter;
Since white noise isn't a pitched signal per se, we'll jest set the frequency of this wavetable to 1Hz in setup.
white_noise.setFreq(1);
Before filtering the noise signal, let's set up a way to control the cutoff and resonance of the filter. A quirk of Mozzi's implementation of a state-variable filter is that we always need to set both cutoff and resonance. If you set only one of them, the filter's parameters won't update properly. The function setCutoffFreqAndResonance(uint8_t cutoff, uint8_t resonance) will do this for us. Its arguments both have a range of 0-255. For cutoff, this corresponds to 0Hz-16386Hz (half the sampling rate of 32768Hz) and for resonance 255 is most resonant.
We'll read the position of our potentiometers and use them to control these parameters in updateControl() as follows.
int cutoff = map(meap.pot_vals[0], 0, 4095, 0, 255);
int resonance = map(meap.pot_vals[1], 0, 4095, 0, 255);
filter.setCutoffFreqAndResonance(cutoff, resonance);
Now we just need to put together the audio chain in updateAudio().
First we'll grab a sample from our oscillator like normal.
int64_t out_sample = white_noise.next();
Now, instead of sending that straight to the output, let's send it through the filter first. The first line will send our signal into the filter, but notice that we don't immediately grab an output from the filter in this line. We need to choose which of the four filter types mentioned above we want to use. Our options are:
- filter.low(); // lowpass output
- filter.high(); // highpass output
- filter.band(); // bandpass output
- filter.notch(); // output output
We'll go for a classic synthesizer sound and grab the lowpass ouput, but feel free to try out the other ones, or check out the MEAP_Resonant_Filter.ino example for a method of switching between outputs with the touch keyboard.
out_sample = filter.low(); // lowpassed sample
return StereoOutput::fromNBit(11, (out_sample * meap.volume_val) >> 12, (out_sample * meap.volume_val) >> 12);
Notice that when we send out_sample to the output in the final line, we send it as an 11-bit value rather than 8-bit. This is because when the filter is highly resonant, it can greatly increase the volume of our signal and clip the output, so we're giving it a few bits of headroom.
Upload the code and turn the pots around; pot 0 will control cutoff frequency and pot 1 will control resonance.
FULL CODE BELOW
/*
Demonstrates filtering of white noise
Pot 0 controls cutoff
Pot 1 controls resonance
*/
#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/whitenoise8192_int8.h> // loads white noise wavetable
mOscil<WHITENOISE8192_NUM_CELLS, AUDIO_RATE> white_noise(WHITENOISE8192_DATA);
MultiResonantFilter filter;
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();
white_noise.setFreq(1);
}
void loop() {
audioHook();
}
void updateControl() {
meap.readInputs();
// ---------- YOUR updateControl CODE BELOW ----------
int cutoff = map(meap.pot_vals[0], 0, 4095, 0, 255);
int resonance = map(meap.pot_vals[1], 0, 4095, 0, 255);
filter.setCutoffFreqAndResonance(cutoff, resonance);
}
AudioOutput_t updateAudio() {
int64_t out_sample = white_noise.next();
filter.next(out_sample);
out_sample = filter.low(); // lowpassed sample
return StereoOutput::fromNBit(11, (out_sample * meap.volume_val) >> 12, (out_sample * meap.volume_val) >> 12);
}
/**
* 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
} else { // DIP 0 down
}
break;
case 1:
if (up) { // DIP 1 up
} else { // DIP 1 down
}
break;
case 2:
if (up) { // DIP 2 up
} else { // DIP 2 down
}
break;
case 3:
if (up) { // DIP 3 up
} else { // DIP 3 down
}
break;
case 4:
if (up) { // DIP 4 up
} else { // DIP 4 down
}
break;
case 5:
if (up) { // DIP 5 up
} else { // DIP 5 down
}
break;
case 6:
if (up) { // DIP 6 up
} else { // DIP 6 down
}
break;
case 7:
if (up) { // DIP 7 up
} else { // DIP 7 down
}
break;
}
}
/**
* Runs whenever a touch pad is pressed or released
*
* int number: the number (0-7) of the pad that was pressed
* bool pressed: true indicated 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
} else { // Pad 0 released
}
break;
case 1:
if (pressed) { // Pad 1 pressed
} else { // Pad 1 released
}
break;
case 2:
if (pressed) { // Pad 2 pressed
} else { // Pad 2 released
}
break;
case 3:
if (pressed) { // Pad 3 pressed
} else { // Pad 3 released
}
break;
case 4:
if (pressed) { // Pad 4 pressed
} else { // Pad 4 released
}
break;
case 5:
if (pressed) { // Pad 5 pressed
} else { // Pad 5 released
}
break;
case 6:
if (pressed) { // Pad 6 pressed
} else { // Pad 6 released
}
break;
case 7:
if (pressed) { // Pad 7 pressed
} else { // Pad 7 released
}
break;
}
}