Plugin Factory

JSAP defines the plugin host specification to ensure that all plugins have access to a common framework and interaction. To ease integration and reduce development time to apply, JSAP supplies a middle-man object to handle all the host requirements called PluginFactory. This object can build the plugins, manage individual effects chains and manage notifications and events.

The constructor of the factory is as follows:

var context = new AudioContext();
var Factory = new PluginFactory(context);

It only requires an active, Web Audio API context to be passed to it, this context is used by all child nodes.

JSAP Layout

The PluginFactory is a self-contained environment for JSAP, handling all of the JSAP objects underneath.

Adding Plugin Prototypes

A plugin prototype is an object holding a plugins' constructor and any prototype functions or objects. A plugin prototype can be added to the factory in two ways:

Loading from a URL

To load from URL, more information is needed than just the URL. This is because it is possible to load multiple plugins from the same source file. Equally plugins may have extra resources it needs (such as another JavaScript library). Therefore we must pass the PluginFactory an object giving it the information to ensure the plugin is loaded properly.

// Build our Factory
var context = new AudioContext();
var Factory = new PluginFactory(context);

// Now lets go get our plugin
var resource = {
    url: "http://dmtlab.bcu.ac.uk/nickjillings/jsap-plugins/plugins/gain.js",
    test: function () {
        return typeof window["GainPlugin"] === "function";
    },
    type: "JavaScript",
    returnObject: "GainPlugin"
}
Factory.loadPluginScript(resource);
// This returns a promise which you can bind .then() to if need be

The resource object above has the following four properties:

  1. Run the test in resource.test
  2. Add the plugin to the Factory prototype list for building
  3. Return the promise with the object from resource.returnObject.

This is all performed asynchronously with a Promise returned allowing for easy callbacks to be attached to the loading process.

Loading from a pre-loaded object

If your plugin prototypes are already loaded into the execution environment, then the asynchronous addition is not necessary.

// Build our Factory
var context = new AudioContext();
var Factory = new PluginFactory(context);

// Now lets go get our plugin
var PluginProto = function(factory, owner) {...}; // Assume this is our plugin

Factory.addPrototype(PluginProto);

This will place the plugin prototype into the Factory if the plugin is valid.

Getting the Prototypes

The prototypes are stored in the PluginFactory and can be accessed using .getPrototypes();. This returns an array of the prototype objects, including the plugin name, code and version numbers.

SubFactory

The SubFactory is an object representing a processing chain. It can host multiple plugins in a linear chain, allowing for complex processing chains to be built with ease. The SubFactory will manage the injection, connection, moving and ejection of plugin objects from this chain. It can also be used to give the plugins meta-data about the chain (such as what the track it is on is called, if applicable).

SubFactory Constructor and Destructor

For the effects chain to work, the SubFactory needs to know which audio nodes will define the input and output points of the chain. The easiest way to achieve this is to use two gain nodes.

// Build our Factory
var context = new AudioContext();
var Factory = new PluginFactory(context);

// Build our global chain start and stop points
var chainStart = context.createGain();
var chainStop = context.createGain();
// Send to the audio destination
chainStop.connect(context.destination);

var effectChain = Factory.createSubFactory(chainStart, chainStop);

When building the factory, the input node (chainStart above) is disconnected from any outputs and then added to the output (chainStop above) to complete the chain.

The SubFactory can be destroyed by calling this.destroy() from the SubFactory scope, which completely empties the SubFactory chain but the object itself is still registered with the PluginFactory and can therefore keep rebuilding effects.

To completely destory the object the PluginFactory.destroySubFactory(instance) function must be called. This invokes the SubFactory.destroy() to clean out the plugins but then also deletes the object references for Garbage collection.

The chain start and stop nodes are connected together upon deletion.

Adding an effect

The effect prototypes are accessed through the PluginFactory member function getPrototypes(). From this list one of the plugin prototype objects must be used on the SubFactory member function createPlugin().

// Build our Factory
var context = new AudioContext();
var Factory = new PluginFactory(context);

// Now lets go get our plugin
var PluginProto = function() {...}; // Assume this is our plugin

Factory.addPrototype(PluginProto);

// Build our global chain start and stop points
var chainStart = context.createGain();
var chainStop = context.createGain();
// Send to the audio destination
chainStop.connect(context.destination);

var effectChain = Factory.createSubFactory(chainStart, chainStop);

// Add a plugin to the chain
effectChain.createPlugin(Factory.getPrototypes()[0]);

An effect is added to the end of the chain by default.

Moving an effect

The SubFactory can handle the movement of plugins within the chain through the movePlugin() member function. In the example below, our chain has three plugins and we will move the 2nd plugin to the head of the chain (1st position).

var effectChain; // Our SubFactory, already loaded
var plugins = effectChain.getPlugins(); // Return an array of the plugins in the SubFactory
// Currently have [1,2,3]

// Get the 2nd plugin
var pluginToMove = plugins[1];

// Move it to the 1st position
effectChain.movePlugin(pluginToMove, 0);
// Now we have [2,1,3]

If we want to move a plugin but we don't know where its current position is, we can use the function getPluginIndex.

var pluginToMove; // Our plugin instance, already obtained
var index = effectChain.getPluginIndex(pluginToMove);
// index now holds the plugin position
// Let's say we want to move it up 1 position
if (index == 0) {
    return; // We are already at the first position here!
} else {
    effectChain.movePlugin(pluginToMove, index-1);
}

Bypassing an effect

An effect can be muted when needed without destroying it. The plugin still behaves as normal except the input audio is routed around the plugin.

// Get the first plugin in the chain as an example
var p = effectChain.getPlugins()[0];

// Bypass the plugin
effectChain.bypassPlugin(p, true);

// Get a true/false on the current bypass state
var state = p.isBypassed()

// Restore the plugin
effectChain.bypassPlugin(p, false);

Deleting an effect

An effect is deleted by the SubFactory when needed by calling deletePlugin. A plugin is removed from the chain and destroyed, all subsequent plugins move up one position and the chain is reconnected.

var effectChain; // Our SubFactory, already loaded
var plugins = effectChain.getPlugins(); // Return an array of the plugins in the SubFactory
// Currently have [1,2,3]

// Get the 2nd plugin
var pluginToDelete = plugins[1];

effectChain.deletePlugin(pluginToDelete);
// Now have [1,3]