Creating a Stereo Image manipulation Plugin
- Introduction
- Creating the process and process variables
- Creating the controls
- Creating the service provider
- Testing the Plugin
- Enhancing the Plugin
- Things to note
Introduction
This tutorial shows you how to design and implement a basic audio effect plugin. Source code and UML class diagrams are provided.
Initially we'll just provide a left/right channel swap feature.
Creating the process and process variables
Every audio process implements AudioProcess, here StereoImageProcess extends SimpleAudioProcess which is an implementation of AudioProcess with null open() and close() methods. Typically a concrete process will use Constructor Dependency Injection (Inversion of Control type 3) to receive an instance of a class which implements the required process variable state accessors. We specify this requirement in the form of an interface, StereoImageProcessVariables.
package uk.org.toot.audio.stereoImage;
public interface StereoImageProcessVariables
{
boolean isLRSwapped(); // is Left/Right swapped?
}
package uk.org.toot.audio.stereoImage;
import uk.org.toot.audio.core.AudioBuffer;
import uk.org.toot.audio.core.SimpleAudioProcess;
import uk.org.toot.audio.core.ChannelFormat;
public class StereoImageProcess extends SimpleAudioProcess
{
private StereoImageProcessVariables vars;
public StereoImageProcess(StereoImageProcessVariables variables) {
vars = variables;
}
public void processAudio(AudioBuffer buffer) {
if ( buffer.getChannelCount() < 2 ) // mono in
buffer.convertTo(ChannelFormat.STEREO);
boolean swap = vars.isLRSwapped();
ChannelFormat format = buffer.getChannelFormat();
// get left/right pairs to handle quad and 5.1 properly
int[] leftChans = format.getLeft();
int[] rightChans = format.getRight();
for ( int pair = 0; pair < leftChans.length; pair++ ) {
// swap if necessary
if ( swap ) buffer.swap(leftChans[pair], rightChans[pair]);
}
}
}
The UML class diagram is show below with standard coloring for Toot Audio. Interfaces are yellow, processes are red.

Creating the controls
The controls for the plugin process extend AudioControls.
package uk.org.toot.audio.stereoImage;
import java.awt.Color;
import uk.org.toot.audio.core.AudioControls;
import uk.org.toot.control.BooleanControl;
public class StereoImageControls extends AudioControls
implements StereoImageProcessVariables
{
public static final Type STEREO_IMAGE = new Type("Image");
public static final Type LR_SWAP = new Type("Swap");
private BooleanControl swap;
public StereoImageControls() {
super(STEREO_IMAGE);
swap = new BooleanControl(LR_SWAP, false); // initially not swapped
swap.setStateColor(true, Color.red); // indicate red when swapped
add(swap);
}
public boolean isLRSwapped() {
return swap.getValue();
}
}
Essentially isLRSwapped(), which is the implementation of StereoImageProcessVariables, is a Facade over the specific control that maintains the associated state.
The UML class diagram is show below with standard coloring for Toot Audio. Interfaces are yellow, processes are red and controls are blue.

This is sufficient for direct usage but we're making a plugin so now we move on to providing this process and its controls as services.
Creating the service provider
The service provider extends AudioServiceProvider.
package uk.org.toot.audio.stereoImage;
import uk.org.toot.audio.spi.AudioServiceProvider;
import uk.org.toot.audio.core.AudioProcess;
import uk.org.toot.audio.core.AudioControls;
import static uk.org.toot.audio.id.ProviderId.TOOT_PROVIDER_ID;
public class StereoImageServiceProvider extends AudioServiceProvider
{
public StereoImageServiceProvider() {
super(TOOT_PROVIDER_ID, "Toot Software", "Stereo Image", "0.1");
addControls(StereoImageControls.class, "Stereo Image", "Basic", "0.1");
add(StereoImageProcess.class, "Stereo Image", "Basic", "0.1");
}
public AudioProcess createProcessor(AudioControls c) {
if ( c instanceof StereoImageProcessVariables ) {
return new StereoImageProcess((StereoImageProcesVariables)c);
}
return null; // caller then tries another provider
}
}
The constructor registers the provider ID, provider, description and version strings and adds our two main problem domain classes as services with their name, category and version strings. The provider ID must be unique to the provider and Toot Software will be pleased to create a unique ID for you on request.
The createProcessor factory method ensures that a StereoImageProcess is created from StereoImageProcessVariables.
The UML class diagram is show below with standard coloring for Toot Audio. Interfaces are yellow, processes are red and controls are blue. The service provider itself is not colored.

Testing the Plugin
A plugin is normally made available to the Java Service Provider Interface by bundling its components in a JAR and providing a text file in META-INF/services. In this case the text file would be named uk.org.toot.audio.spi.AudioServiceProvider and would contain the text uk.org.toot.audio.stereoImage.StereoImageServiceProvider.
To test the plugin without creating a JAR you simply create the text file in META-INF/services relative to your application classpath. Once this is done the plugin is available to applications such as the Toot Compact Mixer Demo.

Enhancing the Plugin
We'll now add left/right width control. The width will be variable between mono, stereo and wide. Since it isn't immediately obvious how to implement this feature in the AudioProcess we'll start by defining the width control in the AudioControls. We'll consider the value of the width control to represent the 'degree of stereoness', so mono will be 0, stereo will be 1 and wide will be 2. The width control will be a FloatControl with range variable between 0 and 2, default value 1.
package uk.org.toot.audio.stereoImage;
import java.awt.Color;
import uk.org.toot.audio.core.AudioControls;
import uk.org.toot.control.BooleanControl;
import uk.org.toot.control.FloatControl;
import uk.org.toot.control.ControlLaw;
import uk.org.toot.control.LinearLaw;
public class StereoImageControls extends AudioControls
implements StereoImageProcessVariables
{
public static final Type STEREO_IMAGE = new Type("Image");
public static final Type LR_SWAP = new Type("Swap");
public static final Type LR_WIDTH = new Type("Width");
private BooleanControl swap;
private FloatControl width;
private final static ControlLaw WIDTH_LAW = new LinearLaw(0f, 2f, "");
public StereoImageControls() {
super(STEREO_IMAGE);
width = new FloatControl(LR_WIDTH, WIDTH_LAW, 0.01f, 1f);
width.setInsertColor(Color.orange);
add(width);
swap = new BooleanControl(LR_SWAP, false); // initially not swapped
swap.setStateColor(true, Color.red);
add(swap);
}
public boolean isLRSwapped() {
return swap.getValue();
}
public boolean isAlwaysVertical() { return true; }
}
And it now looks like this:

The width control doesn't work of course because we haven't implemented the width processing yet. We haven't figured out how to do the processing yet, but as usual Google comes to the rescue. It turns out that a common electronic implementation typically just adds or subtracts a portion of the other channel to each channel. At mono you add the other channel, at stereo you add nothing and at wide you subtract the other channel. There's a bit more to it to maintain constant power but we'll gloss over that for now.
That means the 'other channel factor' is +1 at mono, 0 at stereo and -1 at wide. The 'user' control value is 0 at mono, 1 at stereo and 2 at wide. Hence we can derive the 'other channel factor' from the 'user' control value by subtracting 1 and negating the result.
So we'll add the following method to StereoImageProcessControls:
public float getWidthFactor() {
return -(width.getValue()-1);
}
... add the following method declaration to StereoImageProcessVariables:
float getWidthFactor(); // +1..-1 for Mono..Wide
... and revise StereoImageProcess.processAudio as follows:
public void processAudio(AudioBuffer buffer) {
int nsamples = buffer.getSampleCount();
if ( buffer.getChannelCount() < 2 ) // mono in
buffer.convertTo(ChannelFormat.STEREO);
float otherFactor = vars.getWidthFactor();
boolean swap = vars.isLRSwapped();
ChannelFormat format = buffer.getChannelFormat();
// get left/right pairs to handle quad and 5.1 properly
int[] leftChans = format.getLeft();
int[] rightChans = format.getRight();
float tmp;
for ( int pair = 0; pair < leftChans.length; pair++ ) {
float[] left = buffer.getChannel(leftChans[pair]);
float[] right = buffer.getChannel(rightChans[pair]);
// first we process the L/R width
for ( int i = 0; i < nsamples; i++ ) {
tmp = left[i];
left[i] += otherFactor * right[i];
right[i] += otherFactor * tmp;
}
// swap if necessary
if ( swap ) buffer.swap(leftChans[pair], rightChans[pair]);
}
}
The UML class diagram is show below.

That completes the plugin for now, although I may update this tutorial if I figure out how to maintain constant power.
If you've read this far I thank you and I hope you've found this tutorial useful in some way. I would estimate creating this tutorial took perhaps 10 times as long as actually writing the plugin so I'm not sure how many more I'll write.
Finally I list a few things to note, things which are important to proper or optimum use of the Toot Software audio domain.
Things to note
The user interface is generated automatically for typical controls.
The problem domain is represented by an AudioProcess and its associated AudioControls.
The AudioServiceProvider creates an AudioProcess for its associated AudioControls.
It is good practice to use a process variable interface to decouple an AudioProcess from its associated AudioControls.
The AudioControls decouple an AudioProcess from a user interface.
Multiple AudioProcesses can be controlled by a single AudioControls (sic).
