Tutorial: Processing audio input

Table of Contents


This tutorial shows how to process audio input and pass it to the audio output.

Level: Beginner

Platforms: Windows, macOS, Linux

Classes: Random, BigInteger, AudioBuffer

Getting started

Download the demo project for this tutorial here: PIP | ZIP. Unzip the project and open it in your IDE.

If you need help with this step, see Tutorial: Projucer Part 1: Getting started with the Projucer.

The demo project

The demo project modulates an incoming signal with white noise. The level of the white noise can be changed which affects the level of the overall output (see Tutorial: Control audio levels for the technique used to generate the white noise). The result is a very "fuzzy" version of the input signal.

Warning
Be careful to avoid feedback when running the application (although the effect can be quite interesting!).

It's probably best to use a separate microphone and headphones. Of course, you will need some kind of audio input device for the project to work correctly.

Audio input

This tutorial uses the AudioAppComponent class as the basis for the demo project application. In other tutorials we generate audio within the getNextAudioBlock() function — see Tutorial: Build a white noise generator, Tutorial: Control audio levels, and Tutorial: Build a sine wave synthesiser. In this tutorial we read the audio input and output some audio too. In the MainContentComponent constructor we request two audio inputs and two audio outputs:

setAudioChannels (2, 2);
Note
The actual number of available inputs or outputs may be fewer than the number we request.

Reusing buffers

It is important to know that the input and output buffers are not completely separate. The same buffer is used for the input and output. You can test this by temporarily commenting out all of the code in the getNextAudioBlock() function. If you then run the application, the audio input will be passed directly to the output. In the getNextAudioBlock() function, the number of channels in the AudioSampleBuffer object within the bufferToFill struct may be larger than the number input channels, the number of output channels, or both. It is important to access only the data that refers to the number of input and output channels that you have requested, and that are available. In particular, if you have more input channels than output channels you must not modify the channels that should contain read-only data.

Getting active channels

In the getNextAudioBlock() function we obtain BigInteger objects that represent the list of active input and output channels as a bitmask (this is similar to the std::bitset class or using a std::vector<bool> object). In these BigInteger objects the channels are represented by a 0 (inactive) or 1 (active) in the bits comprising the BigInteger value.

Note
See Tutorial: The BigInteger class for other operations that can be performed on BigInteger objects.
void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
{
auto* device = deviceManager.getCurrentAudioDevice();
auto activeInputChannels = device->getActiveInputChannels();
auto activeOutputChannels = device->getActiveOutputChannels();
//...

To work out the maximum number of channels over which we need to iterate, we can inspect the bits in the BigInteger objects to find the highest numbered bit. The maximum number of channels will be one more than this.

//...
auto maxInputChannels = activeInputChannels .getHighestBit() + 1;
auto maxOutputChannels = activeOutputChannels.getHighestBit() + 1;
//...

Then obtain the desired level from our level slider and move on to process the audio by processing each output channel, one at a time. If the maximum number of input channels is zero (which could happen even though we requested two channels if our hardware has no audio inputs) then we must not try to process the audio. In this case we simply zero the output channel buffer (to output silence). Individual output channels may also be inactive, so we check the state of the channel and also output silence for that channel if it is inactive:

//...
auto level = (float) levelSlider.getValue();
for (auto channel = 0; channel < maxOutputChannels; ++channel)
{
if ((! activeOutputChannels[channel]) || maxInputChannels == 0)
{
bufferToFill.buffer->clear (channel, bufferToFill.startSample, bufferToFill.numSamples);
}
else
//...

Then we go on to process the input data through to the output:

//...
else
{
auto actualInputChannel = channel % maxInputChannels; // [1]
if (! activeInputChannels[channel]) // [2]
{
bufferToFill.buffer->clear (channel, bufferToFill.startSample, bufferToFill.numSamples);
}
else // [3]
{
auto* inBuffer = bufferToFill.buffer->getReadPointer (actualInputChannel,
bufferToFill.startSample);
auto* outBuffer = bufferToFill.buffer->getWritePointer (channel, bufferToFill.startSample);
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
outBuffer[sample] = inBuffer[sample] * random.nextFloat() * level;
}
}
}
}

The code should be reasonably self-explanatory but here are a few highlights:

Exercise
In this example we don't smooth the amplitude changes as we do in Tutorial: Build a sine wave synthesiser. This is partly to keep the example simple, but you probably wouldn't hear any additional glitches due to the crackling effect anyway. Modify the code to simply output the input channels, scaled by the level slider value, but also smooth out the level changes so that there are no glitches.

Summary

In this tutorial we have introduced processing audio from an audio input in a JUCE application. In particular, you should now know:

See also