Skip to main content

Custom Audio Nodes

The building blocks of the AudioGraph are the three kinds of audio nodes: source nodes, processor nodes and sink nodes.

You can find the list of the available audio nodes in our API reference and Extensions page.

Can you create your own audio nodes? Of course, but currently you need to use C++ to do so. You can subclass the AudioSourceNode, AudioProcessorNode and AudioSinkNode classes to create your own nodes. To interact with your node from Swift or Kotlin you need to wrap the C++ class.

Creating a Simple Low-Pass Filter

We can create a simple low-pass filter by averaging every two consecutive samples. In the time domain, the output is given as:

y[n] = (x[n] + x[n-1])/2

Now let's see how we could implement this. The header file should have the following structure:

// LowPassFilterNode.hpp
class LowPassFilterNode : public AudioProcessorNode {
public:
LowPassFilterNode();

bool process(AudioBus& inBus, AudioBus& outBus) override;

bool setNumberOfBuses(const uint numberOfInputBuses, const uint numberOfOutputBuses) override;

bool setBusFormats(AudioBusFormatList& inputBusFormats, AudioBusFormatList& outputBusFormats) override;

private:
// Let's suppose that the max nr of channels is two
float filterMemory[2] = {0.0f, 0.0f};
};

A possible implementation looks like this:

// LowPassFilterNode.cpp
#include "LowPassFilterNode.hpp"

using namespace switchboard;

LowPassFilterNode::LowPassFilterNode() {
type = "LowPassFilterNode";
}

bool LowPassFilterNode::process(AudioBus& inBus, AudioBus& outBus) {
AudioBuffer<float>& inBuffer = *inBus.buffer;
AudioBuffer<float>& outBuffer = *outBus.buffer;

const uint numFrames = inBuffer.getNumberOfFrames();
const uint numChannels = inBuffer.getNumberOfChannels();

// Calculate the first value of the output buffer
for (uint channelIndex = 0; channelIndex < numChannels; channelIndex++) {
const float newValue = (inBuffer.getSample(channelIndex, 0) + filterMemory[channelIndex]) / 2.0f;
outBuffer.setSample(channelIndex, 0, newValue);
}

// Calculate the rest of the values
for (uint frameIndex = 1; frameIndex < numFrames; frameIndex++) {
for (uint channelIndex = 0; channelIndex < numChannels; channelIndex++) {
const float newValue = (inBuffer.getSample(channelIndex, frameIndex) +
inBuffer.getSample(channelIndex, frameIndex - 1))
/ 2.0f;
outBuffer.setSample(channelIndex, frameIndex, newValue);
}
}

// Set filter memory
for (uint channelIndex = 0; channelIndex < numChannels; channelIndex++) {
filterMemory[channelIndex] = inBuffer.getSample(channelIndex, numFrames - 1);
}

return true;
}

bool LowPassFilterNode::setNumberOfBuses(const uint numberOfInputBuses, const uint numberOfOutputBuses) {
return (numberOfInputBuses == 1) && (numberOfOutputBuses == 1);
}

bool LowPassFilterNode::setBusFormats(
AudioBusFormatList& inputBusFormats,
AudioBusFormatList& outputBusFormats
) {
AudioBusFormat& inputBusFormat = inputBusFormats.getBusFormat(0);
AudioBusFormat& outputBusFormat = outputBusFormats.getBusFormat(0);

const bool matchResult = AudioBusFormat::matchBusFormats(inputBusFormat, outputBusFormat);

return matchResult;
}
note

By subclassing SingleBusAudioProcessor instead of AudioProcessorNode the setNumberOfBuses method can be omitted since the number of buses is limited to one.

Read more about the C++ classes in our API reference.