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 "int16" sine wave table. This means that every one of these numbers describing the sine is a 16-bit signed integer, which could be a number between -32,768 and 32,767.
The next conundrum for us lies at the end of the
updateAudio() function, in the following
line:
return StereoOutput::fromNBit(16, 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 so far, but in many programs we
will need to.
The output is expecting a 16-bit sample, and our sine oscillator generates a 16-bit sine wave so they perfectly match! But lets imagine that they didn't match...
- Suppose we had a 15-bit oscillator instead of 16-bit. A 15-bit signed number can take any value in the range -16,384 to 16,383. If our output is expecting a number between -32,768 and 32767 and we give it a number between -16,384 and 16,383, 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 16-bit sine wave.
- Now suppose we instead tried to give a 17-bit sine wave to the output which was expecting a 16-bit sample. A 17-bit signed integer can represent numbers between -65,536 and 65,535. These numbers are greater than what the 16-bit output is willing to take so it will chop them off at -32,768 and 32,767. What you will hear is a digitally clipped version of your sine wave, a kind of distortion you probably don't want to hear.
Now hopefully you can 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. For a more detailed discussion of the subject of using integers to represent audio, refer to the C++ tips page in the MEAP library documentation.
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 16-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 32,767, so if we add two together, well 32,768+32,768 = 65,534, which is approximately equal to the maximum value of a 17-bit integer. If we sent this to the 16-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 17-bit sample rather than an 16-bit one! Each bit we increase by will raise the ceiling of our output, giving us more headroom (6dB per bit).return StereoOutput::fromNBit(17, out_sample, out_sample); - Decrease the volume of our
oscillators.
Alternatively, if we wanted to keep the output at 16 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(16, out_sample / 2, out_sample / 2);return StereoOutput::fromNBit(16, out_sample >> 1, out_sample >> 1);The first method uses a standard division operator to divide the output sample by 2 knocking the 17-bit output back down to 16-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.
Using floats to control volume.
This is all a little complex, potentially a little confusing. It should be noted that you can also control volume by multiplying your signal by a float, which is more intuitive for normal volume control cases.
For example, if you wanted the output of an oscillator to be a little quieter, you could do the following:
out_sample = my_osc.next() * 0.7;
And if you wanted the output to be a little louder, you could do the following:
out_sample = my_osc.next() * 1.3;
Let's use this new knowledge to code up some volume knobs. Our goal is to create two oscillators and mix them using the one potentiometer 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_int16.h> // loads sine wavetable mOscil<sin8192_int16_NUM_CELLS, AUDIO_RATE, int16_t> sine_0(sin8192_int16_DATA); mOscil<sin8192_int16_NUM_CELLS, AUDIO_RATE, int16_t> sine_1(sin8192_int16_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 (a 16-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; 28 bits in this case.
When we add the second sine wave onto this, our result will be 29 bits, so we need to tell the output to expect a 29-bit number. Additionally, we need to be sure to declare
out_sampleas a 64 bit integer to give us enough headroom to deal with the high bit-rate numbers we are dealing with.int64_t out_sample = sine_0.next() * meap.pot_vals[0] + sine_1.next() * meap.pot_vals[1]; return StereoOutput::fromNBit(29, (out_sample * meap.volume_val)>>12, (out_sample * meap.volume_val)>>12); - Upload the code, you should now be able to control the volume of each sine with the potentiometers.
