Tutorial 19. Randomness
In which we discuss how computers can make decisions and generate a naive random "melody". |
In the world of computer music, one of our most fundamental tools is randomness. We often want to give some form of autonomy to the computational portion of our program, essentially giving the computer the option to choose some musical parameter. This is often not purely random; we like to give the computer certain rules or weightings to follow, but at the end of the day, if we want this to evaluate in a non-deterministic way, we will need a random number generator of some sort running behind the scenes. Luckily, in MEAP we have a "true" random number generator.
In actuality, it is difficult for a computer to generate a truly random number. Computation is fundamentally a deterministic process and it turns out that without external intervention, there is no algorithm that a computer could use to generate a sequence of truly random numbers. Instead, what they have are pseudorandom number generators. These are generators that are able to quickly calculate a sequence of numbers that have the properties of a random sequence:
- No correlation between subsequent numbers
- An even distribution of all numberical values
The big problem that pseudorandom number generators have is that they will repeat exactly the same every time. If you turn on your computer and ask its pseudorandom number generator to generate a sequence of ten numbers, it will give you the same ten numbers every time, which seems like it would be a horrible problem. What we are able to do though, is give the generator a seed which will basically start the sequence at a different point, giving you a new set of numbers! You can set this seed number manually (which is how perfectly recreatable minecraft worlds are generated, for example), or you can have some physical process set the seed for you. Often times, a computer or microcontroller will have some sort of sensor that is set up to sense a physical noise process, read that sensor and use that true noise value as the seed to your pseudorandom number generator. Sometimes this is a heat sensor that tries to sense the ambient air temperature with a high degree of accuracy, inevitably resulting in a random value, as there are always slight fluctuations in air temperature. The ESP32-S3 uses one of its WiFi sensors for this purpose. It tries to detect the strength of WiFi signals it receives and, whether there is a router in range or not, the exact strength of this signal will have some random variance, so it makes a great seed our random number generator.
Fortunately, in MEAP this all happens behind the scenes and we have a few functions we can use to generate random numbers:
- meap.irand(int64_t howsmall, int64_t howbig): generates a random integer between howbig and howsmall, inclusive.
- meap.frand():generates a random floating point decimal number between 0 and 1.0, inclusive.
In this tutorial, we will be using meap.irand() to generate random melodies from a C major scale.
First let's create some global objects:
a 16-bit triangle wave oscillator whose volume is controlled by an ADSR envelope as well as an EventDelay to trigger a new note every 250ms.
EventDelay metro1;
int metro1_period = 250; // constant period of 250 ms
mOscil oscil1(tri8192_int16_DATA);
ADSR env1;
We also want to define a set of notes for our random number generator to choose from. In this case we want it to select one of eight notes in one octave of a C major scale, so let's create an array that contains the midi note numbers of a C major scale, starting on C2.
int notes[8] = {48, 50, 52, 53, 55, 57, 59, 60};
Let's get the envelope initialized in setup()
env1.setADLevels(255, 0); // set envelope sustain level to 0, effectively skipping sustain and release phases
env1.setTimes(1, 75, 1, 1);
Control the oscillator's volume with the envelope, and send the result to the DAC in updateAudio():
env1.update();
int64_t out_sample = oscil1.next() * env1.next(); // 16bit osc * 8 bit env = 24bits
return StereoOutput::fromNBit(24, (out_sample * meap.volume_val) >> 12, (out_sample * meap.volume_val) >> 12);
And finally, the randomness in updateControl()
if (metro1.ready()) {
metro1.start(metro1_period);
int note_index = meap.irand(0, 7);
int midi_note = notes[note_index];
oscil1.setFreq(mtof(midi_note));
env1.noteOn();
}
Whenever our metronome ticks (every 250ms), we do the following:
- Re-start the metronome
- Generate a random number between 0 and 7 to choose
which
note to play
from the
notes[]
array we defined above. - Grab that note from the
notes[]
array as a MIDI note number. - Convert that note number to a frequency in Hz with
mtof()
and set the oscillator to that frequency. - Trigger the envelope to start
Your program is now ready to upload. You should hear a constant stream of random notes from a C major scale. Of course, there is no structure to this randomness so it sounds somewhat aimless. In further tutorials we will explore some methods of adding structure, from leveraging random distributions to more complex state-based algorithms.
FULL CODE BELOW
/*
Two separate metronomes triggering two notes
The first consistently ticks with a period of 250 ms
The second has a period that changes with pot 0
*/
#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/tri8192_int16.h>
EventDelay metro1;
int metro1_period = 250; // constant period of 250 ms
mOscil<tri8192_int16_NUM_CELLS, AUDIO_RATE, int16_t> oscil1(tri8192_int16_DATA);
ADSR<AUDIO_RATE, AUDIO_RATE> env1;
int notes[8] = {48, 50, 52, 53, 55, 57, 59, 60};
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 ----------
env1.setADLevels(255, 0); // set envelope sustain level to 0, effectively skipping sustain and release phases
env1.setTimes(1, 75, 1, 1);
}
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 ----------
if (metro1.ready()) {
metro1.start(metro1_period);
int note_index = meap.irand(0, 7);
int midi_note = notes[note_index];
oscil1.setFreq(mtof(midi_note));
env1.noteOn();
}
}
/** 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() {
env1.update();
int64_t out_sample = oscil1.next() * env1.next(); // 16bit osc * 8 bit env = 24bits
return StereoOutput::fromNBit(24, (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;
}
}