Tutorial 05. Volume control
In which we dive into bit depth, DACs and shifting and eventually arrive at a volume knob. |
Volume control is implemented in a slightly confusing way in the Mozzi library (which forms the core of MEAP's sound engine). It is very closely tied with bit depth and may seem somewhat arcane until we understand that.
The first thing we need to understand is that digital audio is made up of a series of numbers. If we were to trace a sine wave using a series of numbers, they would start at zero, increase up to a maximum, decrease, passing zero, to a negative minimum and then return to zero. This is exactly what the my_sine Oscil object we have been working with for the past few tutorials does. You may remember in our first tutorial that we imported specifically an "int8" sine wave table. This means that every one of these numbers describing the sine is an 8-bit signed integer, which could be a number between -128 and 127.
The next hint for us lies at the end of the
updateAudio()
function, in the following
line:
return StereoOutput::fromNBit(8, out_sample,
out_sample);
The first argument of the fromNBit()
function is
a number specifying the bit-rate of the sample we are giving
to it. We haven't changed this number but it is allowed!
The output is expecting an 8-bit sample, and our sine oscillator generates an 8-bit sine wave so they perfectly match! But lets imagine that they didn't match...
- Suppose we had a 7-bit oscillator instead of 8-bit. A 7-bit signed number can take any value in the range -64 to 63. If our output is expecting a number between -128 and 127 and we give it a number between -64 and 63, what will happen? Well, none of the numbers we give to the output would be outside of the range it was expecting so it will be able to accept them. What happens is that we have effectively given the output a sine wave at half the amplitude of the 8-bit sine wave.
- Now suppose the opposite was true and we tried to give a 9-bit sine wave to the output which was expecting an 8-bit sample. A 9-bit signed integer can represent numbers between -256 and 255. If we gave our output numbers this large, it won't know what to do with them and you will end up with digital clipping distortion; the kind that you almost certainly don't want. We would have essentially given the output a signal that is too loud for it to handle.
Now hopefully you see that the volume of a signal is tied to its bit depth. A higher bit-depth allows larger numbers, and larger numbers correspond to a louder signal. Let's apply this to a practical example now:
Suppose we had two sine oscillators we wanted to add together to start building a chord. They are both standard 8-bit oscillators like the ones we have been working with. If we added the outputs of these two oscillators together what kind of output could we expect, numerically speaking? Each oscillator's output at any point in time could be a number as high as 127, so if we add two together, well 127+127=254, which is approximately equal to the maximum value of a 9-bit integer. If we sent this to the 8-bit output, it would distort! Luckily there are two things we can do about this:
- Increase the bit-rate of our output.
The first argument of the
fromNBit()
function specifies the bit-rate that the output should expect, so we can just tell this function to expect a 9-bit sample rather than an 8-bit one!return StereoOutput::fromNBit(9, out_sample, out_sample);
- Decrease the volume of our
oscillators.
Alternatively, if we wanted to keep the output at 8-bits, we could decrease the volume of the sample we are sending to it. The following lines are two equivalent ways of doing this.
return StereoOutput::fromNBit(8, out_sample / 2, out_sample / 2);
return StereoOutput::fromNBit(8, out_sample >> 1, out_sample >> 1);
The first method uses a standard division operator to divide the output sample by 2 knocking the 9-bit output back down to 8-bit.
The second also divides the sample's volume by two but does it using bit-shifting rather than division. The downshift operator knocks one bit off of the sample which effectively divides it by two. Bit-shifting is a fast way of dividing or multiplying by powers of 2. You will sometimes see it in Mozzi or MEAP examples, where it is primarily used because it executes faster than division.
Let's use this new knowledge to code up some volume knobs. Our goal is to create two oscillators and mix them using the potentiometers as volume control for each. We'll be starting with MEAP_BASIC_TEMPLATE as our basis for this tutorial, so grab that code and make a new sketch.
- First lets make the two oscillators and set them
up.
Initialize them both in the global section.
#include <tables/sin8192_int8.h> // loads sine wavetable mOscil<8192, AUDIO_RATE> sine_0(SIN8192_DATA); mOscil<8192, AUDIO_RATE> sine_1(SIN8192_DATA);
And set their frequencies in
setup()
. One will play an A3, the other will play a C#4;sine_0.setFreq(220); sine_1.setFreq(277.18);
- Now in
updateAudio()
we need to control their volumes and mix them together.First we are going to multiply the sample from our sine wave (an 8-bit number) by the value of the potentiometer (from 0-4095, a 12-bit number). When an N-bit number is multiplied by an M-bit number, the result will be a (M+N)-bit number, in this case, 20 bits. When we add the second sine wave onto this, our result will be 21 bits, so we just need to tell the output to expect a 20 bit number!
int out_sample = sine_0.next() * meap.pot_vals[0] + sine_1.next() * meap.pot_vals[1]; return StereoOutput::fromNBit(21, out_sample, out_sample);
- Upload the code, you should now be able to control the volume of each sine with the potentiometers.
FULL CODE BELOW
/*
Control the volume of two sine waves with potentiometers
*/
#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> sine_0(SIN8192_DATA);
mOscil<8192, AUDIO_RATE> sine_1(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 ----------
sine_0.setFreq(220);
sine_1.setFreq(277.18);
}
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 = sine_0.next() * meap.pot_vals[0] + sine_1.next() * meap.pot_vals[1];
return StereoOutput::fromNBit(21, (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;
}
}