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...

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:

  1. 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);

  2. 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.


FULL CODE HERE