Tutorial 06. Envelopes and an improved keyboard
Now that we have the basics of volume control out of the way, let's use it to improve the keyboard we made in tutorial 3 with an envelope. |
An ADSR envelope is one of the most common envelopes in the world of synthesizers. It describes a sound that starts at silence, fades in to a maximum volume, decays down to a sustained volume and then fades out to silence. Mozzi has a great ADSR envelope implemented for us to use. It is an 8-bit envelope that can be used to control gain in the same way we did in tutorial 6.
- Start by making a new sketch and copying code from tutorial 3
- Let's create an envelope that we can use to control the volume of our notes.
- In
setup()
we want to set the attack and sustain levels of our envelope. These values can be any number between 0 and 255. We'll set the attack level to a maximum volume of 255 and the decay level to 200 (a bit quieter than max volume).my_envelope.setADLevels(255, 200);
- Now let's use the potentiometers to control the attack
and release times so we can adjust the envelope on the
fly.
my_envelope.setTimes(meap.pot_vals[0], 100, 1000000, meap.pot_vals[1]);
The setTimes function has four inputs that will set the amount of time in milliseconds the envelope spends in the attack, decay, sustain and release phases respectively. We'll use the raw potentiometer readings to set the attack and release values to vary between instantaneous and ~4 seconds long each, and set the decay and sustain times to stay constant. We set the decay to a fairly quick value of 100ms, and the sustain time to an arbitrarily large value of 1000 seconds so we can sustain the note as long as we want to, and only move to the release phase when we explicitly tell it to.
- Now let's do just that; tell the envelope when to turn
on or off!
We'll want to start the envelope whenever a pad is pressed and end the envelope whenever the pad is released. At the top of the
updateTouch()
function is a section that will execute whenever any pad is pressed or released so we can avoid adding the same code to thecase
statement for each individual pad. Edit it as follows:if (pressed) { // Any pad pressed my_envelope.noteOn(); } else { // Any pad released my_envelope.noteOff(); }
- Now we just need to actually use the envelope to control
the volume of our oscillator. Add the following line to
updateControl()
my_envelope.update();
This will tell the envelope to calculate its next value and store it internally.
Next we need to grab that value in
updateAudio()
and use it!int out_sample = my_sine.next() * my_envelope.next() >> 8; return StereoOutput::fromNBit(8, out_sample, out_sample);
Multiplying our sine by this 8-bit envelope will give us a 16-bit output so we bit-shift it back down by 8 bits to give us an 8-bit output.
- Upload it and try playing it, a note should hold as long as you keep your finger on a pad, and the fade in/out of the note should respond to the position of the two potentiometers.This keyboard is still a little glitchy. If you try to play notes that overlap with each other, they can trigger each other's envelopes to turn off. The next step would probably be to make this keyboard polyphonic. The simplest approach to this would be to create a separate oscillator and envelope for each touch pad. This is left as an excercise for the reader!
ADSR<CONTROL_RATE, AUDIO_RATE> my_envelope;
The Mozzi ADSR has a bit of configurability in terms of
how often it is updated (ie. as a control rate signal or
an
audio rate signal). The first argument of the ADSR template
specifies how often we want the envelope to calculate a
new gain value. This is typically done in
updateControl()
so we set this to
CONTROL_RATE
. The second argument allows us
to interpolate between the envelope values at a higher
speed to smooth out the envelope a little. We are going
this in updateAudio()
so we set this to
AUDIO_RATE. If you wanted, you could do both of these in
updateAudio()
which would result in a
smoother envelope at the expense of some computational
efficiency.
FULL CODE BELOW
/*
Capacitive touch keyboard, playing A major scale.
*/
#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);
ADSR<CONTROL_RATE, AUDIO_RATE> my_envelope;
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);
my_envelope.setADLevels(255, 200);
}
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 ----------
my_envelope.setTimes(meap.pot_vals[0], 100, 1000000, meap.pot_vals[1]);
my_envelope.update();
}
/** 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() * my_envelope.next() >> 8;
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
my_envelope.noteOn();
} else { // Any pad released
my_envelope.noteOff();
}
switch (number) {
case 0:
if (pressed) { // Pad 0 pressed
Serial.println("t0 pressed ");
my_sine.setFreq(220); // A3
} else { // Pad 0 released
Serial.println("t0 released");
}
break;
case 1:
if (pressed) { // Pad 1 pressed
Serial.println("t1 pressed");
my_sine.setFreq(246.94); // B3
} else { // Pad 1 released
Serial.println("t1 released");
}
break;
case 2:
if (pressed) { // Pad 2 pressed
Serial.println("t2 pressed");
my_sine.setFreq(277.18); // C#4
} else { // Pad 2 released
Serial.println("t2 released");
}
break;
case 3:
if (pressed) { // Pad 3 pressed
Serial.println("t3 pressed");
my_sine.setFreq(293.66); // D4
} else { // Pad 3 released
Serial.println("t3 released");
}
break;
case 4:
if (pressed) { // Pad 4 pressed
Serial.println("t4 pressed");
my_sine.setFreq(329.63); // E4
} else { // Pad 4 released
Serial.println("t4 released");
}
break;
case 5:
if (pressed) { // Pad 5 pressed
Serial.println("t5 pressed");
my_sine.setFreq(369.99); // F#4
} else { // Pad 5 released
Serial.println("t5 released");
}
break;
case 6:
if (pressed) { // Pad 6 pressed
Serial.println("t6 pressed");
my_sine.setFreq(415.3); // G#4
} else { // Pad 6 released
Serial.println("t6 released");
}
break;
case 7:
if (pressed) { // Pad 7 pressed
Serial.println("t7 pressed");
my_sine.setFreq(440); // A4
} 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;
}
}