Development guide: Applying effects

Before we begin

If you've followed our Getting started and Capturing audio guides, you'll have an application receiving audio from the users microphone and looping it back through to the speakers with gain control via a slider.

We're going to add controllable effects to this incoming audio stream, so you'll need to continue from where the previous guide ended. If you'd like to pick up from this point, you can clone the guide code from here:

splice/superpowered-guideshttps://github.com/splice/superpowered-guides

Download the code examples for all the Superpowered guides in both Native and JS.


Creating the effects

We take the audio in, pass it through a Volume function to control input gain, then a reverb effect, then onto a filter. The effects will be applied in series, but because our gain stage is at the start of the audio graph, you should still hear reverb tails when the input gain is turned down.

Remember, we converted our input signal to stereo via the WebAudio API before we passed it to the AudioWorkletProcessor. The Reverb and Filter FX classes expect interleaved stereo input pointers.

First, create a new Superpowered Reverb and Filter in you processor's onReady method. Set the desired default properties here too.

// your audio processor file
onReady() {
this.reverb = new this.Superpowered.Reverb(
this.samplerate,
this.samplerate
);
this.reverb.enabled = true;
this.reverb.mix = 0.5;
this.filter = new this.Superpowered.Filter(
this.Superpowered.Filter.Resonant_Lowpass,
this.samplerate
);
this.filter.resonance = 0.2;
this.filter.frequency = 2000;
this.filter.enabled = true;
this.inputGain = 0.2;
this.previousInputGain = 0;
// Notify the main scope that we're prepared.
this.sendMessageToMainScope({ event: "ready" });
}

First, let's import the required library files we'll need in our ViewController.

...
#import "ViewController.h"
#import "Superpowered.h"
#import "SuperpoweredSimple.h"
#import "SuperpoweredOSXAudioIO.h"
+ #import "SuperpoweredFilter.h"
+ #import "SuperpoweredReverb.h"
...

Then let's define the local variables we'll be using to store the Superpowered class instances and editable values.

...
@implementation ViewController {
SuperpoweredOSXAudioIO *audioIO;
+ Superpowered::Filter *filter;
+ Superpowered::Reverb *reverb;
+ float previousInputGain;
}
...

In our viewDidLoad function, let's create the class instances we'll be using in our audio processing callback.

- (void)viewDidLoad {
[super viewDidLoad];
Superpowered::Initialize("ExampleLicenseKey-WillExpire-OnNextUpdate");
NSLog(@"Superpowered version: %u", Superpowered::Version());
reverb = new Superpowered::Reverb(48000, 48000);
filter = new Superpowered::Filter(Superpowered::Filter::FilterType::Resonant_Lowpass, 48000);
reverb->enabled = true;
filter->enabled = true;
[self paramChanged:nil];
previousInputGain = 0;
audioIO = [[SuperpoweredOSXAudioIO alloc] initWithDelegate:(id<SuperpoweredOSXAudioIODelegate>)self preferredBufferSizeMs:12 numberOfChannels:2 enableInput:true enableOutput:true];
[audioIO start];
}

Note that we're passing in the Resonant_Lowpass filter type into the filter. This is a read-only static property of the Superpowered Filter class.


Handling the parameter changes

We'll receive parameter changes for both our reverb and filter from the main scope, and should update our onMessageFromMainScope to include both of these new parameter changes. Trigger the param change in the same way as the existing inputGain slider.

We'll need to add some controls to the DOM that send the parameter changes to the main scope, which then forward those changes to the audio scope.

<!-- In our application's main HTML file, add the following -->
...
<button id="startButton" onclick="boot()">
Start
</button>
<div id="bootedControls">
<label>Input gain<span id="inpuGain">0.2</span></label>
<input
type="range"
min="0"
max="1"
step="0.01"
value="0.2"
oninput="onParamChange('inputGain', this.value)"
/>
<label>Reverb Size <span id="reverbSize">0.7</span></label>
<input
type="range"
min="0"
max="1"
step="0.01"
value="0.7"
oninput="onParamChange('reverbSize', this.value)"
/>
<label>Filter Freq <span id="filterFrequency">4000</span> Hz</label>
<input
type="range"
min="20"
max="10000"
step="0.1"
value="4000"
oninput="onParamChange('filterFrequency', this.value)"
/>
</div>
...

onParamChange in the main scope should forward the control messages to the processorNode:

...
onParamChange = (id, value) => {
// here we update the text label in the dom with the value.
document.getElementById(id).innerHTML = value;
// Here we send the new paramter change over to the audio thread to be applied, passing in the id and value to be parsed.
this.processorNode.sendMessageToAudioScope({
type: "parameterChange",
payload: {
id,
value: Number(value) // we should type cast here.
}
});
};
...

Then within our processor script, we need to respond to the incoming changes.

// Messages are received from the main scope through this method.
onMessageFromMainScope(message) {
if (message.type === "parameterChange") {
if (message.payload?.id === "inputGain") this.inputGain = message.payload.value;
else if (message.payload?.id === "reverbMix") this.reverb.mix = message.payload.value;
else if (message.payload?.id === "filterFrequency") this.filter.frequency = message.payload.value;
}
}

Create two new faders via Xcode's interface builder so we will have the following three sliders:

TypeNameMinMaxDefault
NSSliderinputGainSlider010.5
NSSliderreverbMixSlider010.5
NSSliderfilterFrequencySlider2040002000

You should bind the two new sliders to View Controller by CTRL dragging them across to where we are defining our properties at the top of the controller.

@interface ViewController ()
@property (weak) IBOutlet NSSlider *inputGainSlider;
@property (weak) IBOutlet NSSlider *reverbMixSlider;
@property (weak) IBOutlet NSSlider *filterFrequencySlider;
@end

You should also drag over the send action of the two new sliders to the existing paramChanged function:

...
- (IBAction)paramChanged:(id)sender {
// Set the current values of the effects.
// This function is called on the main thread and can concurrently happen with audioProcessingCallback, but the Superpowered effects are prepared to handle concurrency.
// Values are automatically smoothed as well, so no audio artifacts can be heard.
filter->frequency = self.filterFrequencySlider.floatValue;
reverb->mix = self.reverbMixSlider.floatValue;
}
...

Modifying the audio processing callback

We'll be applying our effects in series so we can take advantage of in-place processing. This refers to the concept of overwriting an input buffer rather than writing to a separate output.

In our processAudio method in the AudioWorkletProcessor script:

...
processAudio(inputBuffer, outputBuffer, buffersize, parameters) {
// Ensure the samplerate is in sync on every audio processing callback.
this.filter.samplerate = this.samplerate;
this.reverb.samplerate = this.samplerate;
// Apply volume while copy the input buffer to the output buffer.
// Gain is smoothed, starting from "previousInputGain" to "inputGain".
this.Superpowered.Volume(
inputBuffer.pointer,
outputBuffer.pointer,
this.previousInputGain,
this.inputGain,
buffersize
);
this.previousInputGain = this.inputGain; // Save the gain for the next round.
// Apply reverb to output (in-place).
this.reverb.process(outputBuffer.pointer, outputBuffer.pointer, buffersize);
// Apply the filter (in-place).
this.filter.process(outputBuffer.pointer, outputBuffer.pointer, buffersize);
}

Modify our audioProcessingCallback in the following way:

- (bool)audioProcessingCallback:(float *)inputBuffer outputBuffer:(float *)outputBuffer numberOfFrames:(unsigned int)numberOfFrames samplerate:(unsigned int)samplerate hostTime:(unsigned long long int)hostTime {
// Ensure the sample rate is in sync on every audio processing callback.
reverb->samplerate = samplerate;
filter->samplerate = samplerate;
// Apply volume while copy the input buffer to the output buffer.
// Gain is smoothed, starting from "previousInputGain" to "inputGain".
float inputGain = self.inputGainSlider.floatValue;
Superpowered::Volume(inputBuffer, outputBuffer, previousInputGain, inputGain, numberOfFrames);
previousInputGain = inputGain; // Save the gain for the next round.
// Apply reverb to output (in-place).
reverb->process(outputBuffer, outputBuffer, numberOfFrames);
// Apply the filter (in-place).
filter->process(outputBuffer, outputBuffer, numberOfFrames);
return true;
}

Cleaning up

Remember that we must also clear our class instances in the onDestruct method of our AudioWorkletProcessor.

...
// onDestruct is called when the parent AudioWorkletNode.destruct() method is called.
// You should clear up all Superpowered objects and allocated buffers here.
onDestruct() {
this.reverb.destruct();
this.filter.destruct();
}
...

End result


You can find the example code for this guide and all the others in both JS and native in one repository over at GitHub.

splice/superpowered-guideshttps://github.com/splice/superpowered-guides

Download the code examples for all the Superpowered guides in both Native and JS.


v1.0.6