Tutorial: Processing audio input

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

Level: Beginner

Platforms: Windows, Mac OS X, Linux

Classes: Random, BigInteger

Getting started

Download the demo project for this tutorial here: tutorial_processing_audio_input.zip. Unzip the project and open it in your IDE.

If you need help with this step, see Tutorial: 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: Synthesis with level control 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: Simple synthesis (noise), Tutorial: Synthesis with level control, and Tutorial: Sine synthesis. 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
{
AudioIODevice* device = deviceManager.getCurrentAudioDevice();
const BigInteger activeInputChannels = device->getActiveInputChannels();
const BigInteger 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.

//...
const int maxInputChannels = activeInputChannels.getHighestBit() + 1;
const int 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:

//...
const float level = (float) levelSlider.getValue();
for (int 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
{
const int actualInputChannel = channel % maxInputChannels; // [1]
if (! activeInputChannels[channel]) // [2]
{
bufferToFill.buffer->clear (channel, bufferToFill.startSample, bufferToFill.numSamples);
}
else // [3]
{
const float* inBuffer = bufferToFill.buffer->getReadPointer (actualInputChannel,
bufferToFill.startSample);
float* outBuffer = bufferToFill.buffer->getWritePointer (channel, bufferToFill.startSample);
for (int 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:

  • [1]: We may have requested more output channels than input channels, so our app needs to make a decision about what to do about these extra outputs. In this example we simply repeat the input channels if we have more outputs than inputs. (To do this we use the modulo operator to "wrap" our access to the input channels based on the number of input channels available.) In other applications it may be more appropriate to output silence for higher numbered channels where there are more output channels than input channels.
  • [2]: Individual input channels may be inactive so we output silence in this case too.
  • [3]: This final block actually does the processing! Here we get pointers to the input and output buffer samples and output the input samples multiplied by our scaled white noise.
Exercise
In this example we don't smooth the amplitude changes as we do in Tutorial: Sine synthesis. 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:

  • How to set up an audio application to be able to access audio from the computer's audio input hardware.
  • That input and output buffers are shared.
  • How to deal with active and inactive channels.
  • How to reason about what to do where there are different numbers of input and output channels.

See also