Tutorial 29. Buffering and storing audio
Recording audio into MEAP. |
In addition to passing audio through MEAP, we have the capability of storing small buffers of audio for playback or analysis. Currently only ~4 seconds can be stored, but I am working on finding a method for storing audio in other parts of memory so this can hopefully be expanded.
In this tutorial, we will tackle the relatively simple example of recording a short sample of audio and playing it back using mSample. Of course, once you have this audio sample in memory, you could analyze it or manipulate it however you want.
If we want to store audio within MEAP, we need to give it a place in memory to live, so we are going to create an empty buffer of ints. Specifically, we are going to use the datatype int16_t which specifies that these integers will be 16-bit integers (which is the format that our audio codec chip uses). The size of the array we create will determine how much audio we can record. Our sampling rate is 32768 Hz, so if we created a buffer of 32768 int16_t's, we would be able to record one second of audio. In our example we'll create and array of 131072 int16_t's, which corresponds to four seconds of audio. We can use this buffer to store less than four seconds but never more. If you try to create a buffer larger than the one we created, you will likely get an error because we don't have that much memory available when declaring arrays in this way.
In the global variable section we'll add the following code:
#define BUFFER_LENGTH 131072 // 32768 samples/second * 4 seconds
int16_t input_buffer[BUFFER_LENGTH] = {0};
mSample<BUFFER_LENGTH, AUDIO_RATE, int16_t> my_sample(input_buffer);
float default_freq;
int record_index = 0;
int sample_length = 0;
bool recording = false;
We create our audio input buffer, point the mSample to it similar to if we were loading the buffer in using a ".h" file and create a default_freq variable which we will use to play back the sample at the same speed it was recorded at.
Finally, we'll create a few variables to help us record our sample. Then way our system will work is as follows:
- When pad 0 is pressed, start recording a buffer.
- When pad 0 is released, or when four seconds have elapsed, stop recording and store the length of the recorded buffer.
- When pad 1 is pressed, play back the buffer.
The variable recording
just keeps track of if we are
currently recording or not, record_index
keeps track of
how far we are through the buffer while recording and
sample_length is used to store how long the sample is once we
are finished recording it.
In setup() we set up our mSample like usual, finding the default_freq by dividing the sample rate by the length of the sample buffer.
default_freq = (float)32768 / (float) BUFFER_LENGTH;
my_sample.setFreq(default_freq);
In updateTouch(), we use pads 0 and 1 to trigger recording and playback respectively.
case 0:
if (pressed) { // Pad 0 pressed
Serial.println("t0 pressed ");
record_index = 0;
recording = true;
} else { // Pad 0 released
Serial.println("t0 released");
sample_length = record_index;
my_sample.setEnd(sample_length);
recording = false;
}
break;
case 1:
if (pressed) { // Pad 1 pressed
Serial.println("t1 pressed");
my_sample.start();
} else { // Pad 1 released
Serial.println("t1 released");
}
break;
When pad 0 is pressed, we set record_index
to the start
of the buffer and enable recording. When it is released, we stop
recording, store the length of the sample we recorded, and pass that
information to our mSample object using the setEnd function.
When pad 1 is pressed, we simply trigger the mSample object to play our sample.
updateAudio is where the brunt of the code is for this tutorial.
if(recording){
input_buffer[record_index++] = meap_input_frame[0];
if(record_index >= BUFFER_LENGTH){
sample_length = BUFFER_LENGTH;
my_sample.setEnd(sample_length);
recording = false;
}
}
int64_t out_sample = my_sample.next();
return StereoOutput::fromNBit(16, (out_sample * meap.volume_val)>>12, (out_sample * meap.volume_val)>>12);
There are two separate processes going on here: recording and then playback.
If we are recording, we do the following:
- Grab a sample from our line input using
meap_input_frame[0]
and place it into our input buffer. We place it at indexrecord_index
and then increment that index. Note that we are only grabbing meap_input_frame[0], which is the left channel of the line input, and ignoring the right channel. If we wanted to record stereo, we would need two input buffers. - Check if we have filled up the input buffer:
record_index >= BUFFER_LENGTH
. If we have, we stop recording and store the length of the sample as if we had released pad 0.
Playback is simple. We just grab a sample from our mSample object and send it to the output. Remember that mSample by default only plays through the sample once, so we will only hear an output after we press pad 1 to trigger the sample.
FULL CODE BELOW
/*
Basic template for working with a stock MEAP board.
*/
#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 ----------
#define BUFFER_LENGTH 131072 // 32768 samples/second * 4 seconds
int16_t input_buffer[BUFFER_LENGTH] = {0};
mSample<BUFFER_LENGTH, AUDIO_RATE, int16_t> my_sample(input_buffer);
float default_freq;
int record_index = 0;
int sample_length = 0;
bool recording = false;
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)32768 / (float) BUFFER_LENGTH;
my_sample.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 ----------
}
/** 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_sample.next();
if(recording){
input_buffer[record_index++] = meap_input_frame[0];
if(record_index >= BUFFER_LENGTH){
sample_length = BUFFER_LENGTH;
my_sample.setEnd(sample_length);
recording = false;
}
}
return StereoOutput::fromNBit(16, (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 ");
record_index = 0;
recording = true;
} else { // Pad 0 released
Serial.println("t0 released");
sample_length = record_index;
my_sample.setEnd(sample_length);
recording = false;
}
break;
case 1:
if (pressed) { // Pad 1 pressed
Serial.println("t1 pressed");
my_sample.start();
} 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;
}
}