JavaScript library for audio synthesis and composition.
Core Audiolet classes
The base audiolet object. Contains an output node which pulls data from connected nodes.
var Audiolet = function(sampleRate, numberOfChannels, bufferSize) {
this.output = new AudioletDestination(this, sampleRate,
numberOfChannels, bufferSize);
};
The basic building block of Audiolet applications. Nodes are connected together to create a processing graph which governs the flow of audio data. AudioletNodes can contain any number of inputs and outputs which send and receive one or more channels of audio data. Audio data is created and processed using the generate function, which is called whenever new data is needed.
var AudioletNode = function(audiolet, numberOfInputs, numberOfOutputs,
generate) {
this.audiolet = audiolet;
this.inputs = [];
for (var i = 0; i < numberOfInputs; i++) {
this.inputs.push(new AudioletInput(this, i));
}
this.outputs = [];
for (var i = 0; i < numberOfOutputs; i++) {
this.outputs.push(new AudioletOutput(this, i));
}
if (generate) {
this.generate = generate;
}
};
Connect the node to another node or group.
AudioletNode.prototype.connect = function(node, output, input) {
if (node instanceof AudioletGroup) {
// Connect to the pass-through node rather than the group
node = node.inputs[input || 0];
input = 0;
}
var outputPin = this.outputs[output || 0];
var inputPin = node.inputs[input || 0];
outputPin.connect(inputPin);
inputPin.connect(outputPin);
this.audiolet.device.needTraverse = true;
};
Disconnect the node from another node or group
AudioletNode.prototype.disconnect = function(node, output, input) {
if (node instanceof AudioletGroup) {
node = node.inputs[input || 0];
input = 0;
}
var outputPin = this.outputs[output || 0];
var inputPin = node.inputs[input || 0];
inputPin.disconnect(outputPin);
outputPin.disconnect(inputPin);
this.audiolet.device.needTraverse = true;
};
Force an output to contain a fixed number of channels.
AudioletNode.prototype.setNumberOfOutputChannels = function(output,
numberOfChannels) {
this.outputs[output].numberOfChannels = numberOfChannels;
};
Link an output to an input, forcing the output to always contain the same number of channels as the input.
AudioletNode.prototype.linkNumberOfOutputChannels = function(output, input) {
this.outputs[output].linkNumberOfChannels(this.inputs[input]);
};
Process samples a from each channel. This function should not be called manually by users, who should instead rely on automatic ticking from connections to the AudioletDevice.
AudioletNode.prototype.tick = function() {
this.createInputSamples();
this.createOutputSamples();
this.generate();
};
Traverse the audio graph, adding this and any parent nodes to the nodes array.
AudioletNode.prototype.traverse = function(nodes) {
if (nodes.indexOf(this) == -1) {
nodes.push(this);
nodes = this.traverseParents(nodes);
}
return nodes;
};
Call the traverse function on nodes which are connected to the inputs.
AudioletNode.prototype.traverseParents = function(nodes) {
var numberOfInputs = this.inputs.length;
for (var i = 0; i < numberOfInputs; i++) {
var input = this.inputs[i];
var numberOfStreams = input.connectedFrom.length;
for (var j = 0; j < numberOfStreams; j++) {
nodes = input.connectedFrom[j].node.traverse(nodes);
}
}
return nodes;
};
Process a sample for each channel, reading from the inputs and putting new values into the outputs. Override me!
AudioletNode.prototype.generate = function() {
};
Create the input samples by grabbing data from the outputs of connected nodes and summing it. If no nodes are connected to an input, then give an empty array
AudioletNode.prototype.createInputSamples = function() {
var numberOfInputs = this.inputs.length;
for (var i = 0; i < numberOfInputs; i++) {
var input = this.inputs[i];
var numberOfInputChannels = 0;
for (var j = 0; j < input.connectedFrom.length; j++) {
var output = input.connectedFrom[j];
for (var k = 0; k < output.samples.length; k++) {
var sample = output.samples[k];
if (k < numberOfInputChannels) {
input.samples[k] += sample;
}
else {
input.samples[k] = sample;
numberOfInputChannels += 1;
}
}
}
if (input.samples.length > numberOfInputChannels) {
input.samples = input.samples.slice(0, numberOfInputChannels);
}
}
};
Create output samples for each channel.
AudioletNode.prototype.createOutputSamples = function() {
var numberOfOutputs = this.outputs.length;
for (var i = 0; i < numberOfOutputs; i++) {
var output = this.outputs[i];
var numberOfChannels = output.getNumberOfChannels();
if (output.samples.length == numberOfChannels) {
continue;
}
else if (output.samples.length > numberOfChannels) {
output.samples = output.samples.slice(0, numberOfChannels);
continue;
}
for (var j = output.samples.length; j < numberOfChannels; j++) {
output.samples[j] = 0;
}
}
};
Remove the node completely from the processing graph, disconnecting all of its inputs and outputs.
AudioletNode.prototype.remove = function() {
// Disconnect inputs
var numberOfInputs = this.inputs.length;
for (var i = 0; i < numberOfInputs; i++) {
var input = this.inputs[i];
var numberOfStreams = input.connectedFrom.length;
for (var j = 0; j < numberOfStreams; j++) {
var outputPin = input.connectedFrom[j];
var output = outputPin.node;
output.disconnect(this, outputPin.index, i);
}
}
// Disconnect outputs
var numberOfOutputs = this.outputs.length;
for (var i = 0; i < numberOfOutputs; i++) {
var output = this.outputs[i];
var numberOfStreams = output.connectedTo.length;
for (var j = 0; j < numberOfStreams; j++) {
var inputPin = output.connectedTo[j];
var input = inputPin.node;
this.disconnect(input, i, inputPin.index);
}
}
};
A container for collections of connected AudioletNodes. Groups make it possible to create multiple copies of predefined networks of nodes, without having to manually create and connect up each individual node.
From the outside groups look and behave exactly the same as nodes. Internally you can connect nodes directly to the group's inputs and outputs, allowing connection to nodes outside of the group.
var AudioletGroup = function(audiolet, numberOfInputs, numberOfOutputs) {
this.audiolet = audiolet;
this.inputs = [];
for (var i = 0; i < numberOfInputs; i++) {
this.inputs.push(new PassThroughNode(this.audiolet, 1, 1));
}
this.outputs = [];
for (var i = 0; i < numberOfOutputs; i++) {
this.outputs.push(new PassThroughNode(this.audiolet, 1, 1));
}
};
Connect the group to another node or group
AudioletGroup.prototype.connect = function(node, output, input) {
this.outputs[output || 0].connect(node, 0, input);
};
Disconnect the group from another node or group
AudioletGroup.prototype.disconnect = function(node, output, input) {
this.outputs[output || 0].disconnect(node, 0, input);
};
Remove the group completely from the processing graph, disconnecting all of its inputs and outputs
AudioletGroup.prototype.remove = function() {
var numberOfInputs = this.inputs.length;
for (var i = 0; i < numberOfInputs; i++) {
this.inputs[i].remove();
}
var numberOfOutputs = this.outputs.length;
for (var i = 0; i < numberOfOutputs; i++) {
this.outputs[i].remove();
}
};
A variable size multi-channel audio buffer.
var AudioletBuffer = function(numberOfChannels, length) {
this.numberOfChannels = numberOfChannels;
this.length = length;
this.channels = [];
for (var i = 0; i < this.numberOfChannels; i++) {
this.channels.push(new Float32Array(length));
}
this.unslicedChannels = [];
for (var i = 0; i < this.numberOfChannels; i++) {
this.unslicedChannels.push(this.channels[i]);
}
this.isEmpty = false;
this.channelOffset = 0;
};
Get a single channel of data
AudioletBuffer.prototype.getChannelData = function(channel) {
return (this.channels[channel]);
};
Set the data in the buffer by copying data from a second buffer
AudioletBuffer.prototype.set = function(buffer) {
var numberOfChannels = buffer.numberOfChannels;
for (var i = 0; i < numberOfChannels; i++) {
this.channels[i].set(buffer.getChannelData(i));
}
};
Set the data in a section of the buffer by copying data from a second buffer
AudioletBuffer.prototype.setSection = function(buffer, length, inputOffset,
outputOffset) {
inputOffset = inputOffset || 0;
outputOffset = outputOffset || 0;
var numberOfChannels = buffer.numberOfChannels;
for (var i = 0; i < numberOfChannels; i++) {
// Begin subarray-of-subarray fix
inputOffset += buffer.channelOffset;
outputOffset += this.channelOffset;
var channel1 = this.unslicedChannels[i].subarray(outputOffset,
outputOffset +
length);
var channel2 = buffer.unslicedChannels[i].subarray(inputOffset,
inputOffset +
length);
// End subarray-of-subarray fix
// Uncomment the following lines when subarray-of-subarray is fixed
channel1.set(channel2);
}
};
Add the data from a second buffer to the data in this buffer
AudioletBuffer.prototype.add = function(buffer) {
var length = this.length;
var numberOfChannels = buffer.numberOfChannels;
for (var i = 0; i < numberOfChannels; i++) {
var channel1 = this.getChannelData(i);
var channel2 = buffer.getChannelData(i);
for (var j = 0; j < length; j++) {
channel1[j] += channel2[j];
}
}
};
Add the data from a section of a second buffer to the data in this buffer
AudioletBuffer.prototype.addSection = function(buffer, length, inputOffset,
outputOffset) {
inputOffset = inputOffset || 0;
outputOffset = outputOffset || 0;
var numberOfChannels = buffer.numberOfChannels;
for (var i = 0; i < numberOfChannels; i++) {
var channel1 = this.getChannelData(i);
var channel2 = buffer.getChannelData(i);
for (var j = 0; j < length; j++) {
channel1[j + outputOffset] += channel2[j + inputOffset];
}
}
};
Resize the buffer. This operation can optionally be lazy, which is generally faster but doesn't necessarily result in an empty buffer.
AudioletBuffer.prototype.resize = function(numberOfChannels, length, lazy,
offset) {
offset = offset || 0;
// Local variables
var channels = this.channels;
var unslicedChannels = this.unslicedChannels;
var oldLength = this.length;
var channelOffset = this.channelOffset + offset;
for (var i = 0; i < numberOfChannels; i++) {
// Get the current channels
var channel = channels[i];
var unslicedChannel = unslicedChannels[i];
if (length > oldLength) {
// We are increasing the size of the buffer
var oldChannel = channel;
if (!lazy ||
!unslicedChannel ||
unslicedChannel.length < length) {
// Unsliced channel is not empty when it needs to be,
// does not exist, or is not large enough, so needs to be
// (re)created
unslicedChannel = new Float32Array(length);
}
channel = unslicedChannel.subarray(0, length);
if (!lazy && oldChannel) {
channel.set(oldChannel, offset);
}
channelOffset = 0;
}
else {
// We are decreasing the size of the buffer
if (!unslicedChannel) {
// Unsliced channel does not exist
// We can assume that we always have at least one unsliced
// channel, so we can copy its length
var unslicedLength = unslicedChannels[0].length;
unslicedChannel = new Float32Array(unslicedLength);
}
// Begin subarray-of-subarray fix
offset = channelOffset;
channel = unslicedChannel.subarray(offset, offset + length);
// End subarray-of-subarray fix
// Uncomment the following lines when subarray-of-subarray is
// fixed.
// TODO: Write version where subarray-of-subarray is used
}
channels[i] = channel;
unslicedChannels[i] = unslicedChannel;
}
this.channels = channels.slice(0, numberOfChannels);
this.unslicedChannels = unslicedChannels.slice(0, numberOfChannels);
this.length = length;
this.numberOfChannels = numberOfChannels;
this.channelOffset = channelOffset;
};
Append the data from a second buffer to the end of the buffer
AudioletBuffer.prototype.push = function(buffer) {
var bufferLength = buffer.length;
this.resize(this.numberOfChannels, this.length + bufferLength);
this.setSection(buffer, bufferLength, 0, this.length - bufferLength);
};
Remove data from the end of the buffer, placing it in a second buffer.
AudioletBuffer.prototype.pop = function(buffer) {
var bufferLength = buffer.length;
var offset = this.length - bufferLength;
buffer.setSection(this, bufferLength, offset, 0);
this.resize(this.numberOfChannels, offset);
};
Prepend data from a second buffer to the beginning of the buffer.
AudioletBuffer.prototype.unshift = function(buffer) {
var bufferLength = buffer.length;
this.resize(this.numberOfChannels, this.length + bufferLength, false,
bufferLength);
this.setSection(buffer, bufferLength, 0, 0);
};
Remove data from the beginning of the buffer, placing it in a second buffer.
AudioletBuffer.prototype.shift = function(buffer) {
var bufferLength = buffer.length;
buffer.setSection(this, bufferLength, 0, 0);
this.resize(this.numberOfChannels, this.length - bufferLength,
false, bufferLength);
};
Make all values in the buffer 0
AudioletBuffer.prototype.zero = function() {
var numberOfChannels = this.numberOfChannels;
for (var i = 0; i < numberOfChannels; i++) {
var channel = this.getChannelData(i);
var length = this.length;
for (var j = 0; j < length; j++) {
channel[j] = 0;
}
}
};
Copy the buffer into a single Float32Array, with each channel appended to the end of the previous one.
AudioletBuffer.prototype.combined = function() {
var channels = this.channels;
var numberOfChannels = this.numberOfChannels;
var length = this.length;
var combined = new Float32Array(numberOfChannels * length);
for (var i = 0; i < numberOfChannels; i++) {
combined.set(channels[i], i * length);
}
return combined;
};
Copy the buffer into a single Float32Array, with the channels interleaved.
AudioletBuffer.prototype.interleaved = function() {
var channels = this.channels;
var numberOfChannels = this.numberOfChannels;
var length = this.length;
var interleaved = new Float32Array(numberOfChannels * length);
for (var i = 0; i < length; i++) {
for (var j = 0; j < numberOfChannels; j++) {
interleaved[numberOfChannels * i + j] = channels[j][i];
}
}
return interleaved;
};
Return a new copy of the buffer.
AudioletBuffer.prototype.copy = function() {
var buffer = new AudioletBuffer(this.numberOfChannels, this.length);
buffer.set(this);
return buffer;
};
Load a .wav or .aiff file into the buffer using audiofile.js
AudioletBuffer.prototype.load = function(path, async, callback) {
var request = new AudioFileRequest(path, async);
request.onSuccess = function(decoded) {
this.length = decoded.length;
this.numberOfChannels = decoded.channels.length;
this.unslicedChannels = decoded.channels;
this.channels = decoded.channels;
this.channelOffset = 0;
if (callback) {
callback();
}
}.bind(this);
request.onFailure = function() {
console.error('Could not load', path);
}.bind(this);
request.send();
};
AudioletParameters are used to provide either constant or varying values to be used inside AudioletNodes. AudioletParameters hold a static value, and can also be linked to an AudioletInput. If a node or group is connected to the linked input, then the dynamic value taken from the node should be prioritised over the stored static value. If no node is connected then the static value should be used.
var AudioletParameter = function(node, inputIndex, value) {
this.node = node;
if (typeof inputIndex != 'undefined' && inputIndex != null) {
this.input = node.inputs[inputIndex];
}
else {
this.input = null;
}
this.value = value || 0;
};
Check whether the static value should be used.
AudioletParameter.prototype.isStatic = function() {
return (this.input.samples.length == 0);
};
Check whether the dynamic values should be used.
AudioletParameter.prototype.isDynamic = function() {
return (this.input.samples.length > 0);
};
Set the stored static value
AudioletParameter.prototype.setValue = function(value) {
this.value = value;
};
Get the stored static value
AudioletParameter.prototype.getValue = function() {
if (this.input != null && this.input.samples.length > 0) {
return this.input.samples[0];
}
else {
return this.value;
}
};
Class representing a single input of an AudioletNode
var AudioletInput = function(node, index) {
this.node = node;
this.index = index;
this.connectedFrom = [];
// Minimum sized buffer, which we can resize from accordingly
this.samples = [];
};
Connect the input to an output
AudioletInput.prototype.connect = function(output) {
this.connectedFrom.push(output);
};
Disconnect the input from an output
AudioletInput.prototype.disconnect = function(output) {
var numberOfStreams = this.connectedFrom.length;
for (var i = 0; i < numberOfStreams; i++) {
if (output == this.connectedFrom[i]) {
this.connectedFrom.splice(i, 1);
break;
}
}
if (this.connectedFrom.length == 0) {
this.samples = [];
}
};
toString
AudioletInput.prototype.toString = function() {
return this.node.toString() + 'Input #' + this.index;
};
Class representing a single output of an AudioletNode
var AudioletOutput = function(node, index) {
this.node = node;
this.index = index;
this.connectedTo = [];
this.samples = [];
this.linkedInput = null;
this.numberOfChannels = 1;
};
Connect the output to an input
AudioletOutput.prototype.connect = function(input) {
this.connectedTo.push(input);
};
Disconnect the output from an input
AudioletOutput.prototype.disconnect = function(input) {
var numberOfStreams = this.connectedTo.length;
for (var i = 0; i < numberOfStreams; i++) {
if (input == this.connectedTo[i]) {
this.connectedTo.splice(i, 1);
break;
}
}
};
Link the output to an input, forcing the output to always contain the same number of channels as the input.
AudioletOutput.prototype.linkNumberOfChannels = function(input) {
this.linkedInput = input;
};
Unlink the output from its linked input
AudioletOutput.prototype.unlinkNumberOfChannels = function() {
this.linkedInput = null;
};
Get the number of output channels, taking the value from the input if the output is linked.
AudioletOutput.prototype.getNumberOfChannels = function() {
if (this.linkedInput && this.linkedInput.connectedFrom.length) {
return (this.linkedInput.samples.length);
}
return (this.numberOfChannels);
};
toString
AudioletOutput.prototype.toString = function() {
return this.node.toString() + 'Output #' + this.index + ' - ';
};
Group containing all of the components for the Audiolet output chain. The chain consists of:
Input => Scheduler => UpMixer => Output
Inputs
var AudioletDestination = function(audiolet, sampleRate, numberOfChannels,
bufferSize) {
AudioletGroup.call(this, audiolet, 1, 0);
this.device = new AudioletDevice(audiolet, sampleRate,
numberOfChannels, bufferSize);
audiolet.device = this.device; // Shortcut
this.scheduler = new Scheduler(audiolet);
audiolet.scheduler = this.scheduler; // Shortcut
this.upMixer = new UpMixer(audiolet, this.device.numberOfChannels);
this.inputs[0].connect(this.scheduler);
this.scheduler.connect(this.upMixer);
this.upMixer.connect(this.device);
};
extend(AudioletDestination, AudioletGroup);
toString
AudioletDestination.prototype.toString = function() {
return 'Destination';
};
Audio output device. Uses sink.js to output to a range of APIs.
function AudioletDevice(audiolet, sampleRate, numberOfChannels, bufferSize) {
AudioletNode.call(this, audiolet, 1, 0);
this.sink = Sink(this.tick.bind(this), numberOfChannels, bufferSize,
sampleRate);
// Re-read the actual values from the sink. Sample rate especially is
// liable to change depending on what the soundcard allows.
this.sampleRate = this.sink.sampleRate;
this.numberOfChannels = this.sink.channelCount;
this.bufferSize = this.sink.preBufferSize;
this.writePosition = 0;
this.buffer = null;
this.paused = false;
this.needTraverse = true;
this.nodes = [];
}
extend(AudioletDevice, AudioletNode);
Overridden tick function. Pulls data from the input and writes it to the device.
AudioletDevice.prototype.tick = function(buffer, numberOfChannels) {
if (!this.paused) {
var input = this.inputs[0];
var samplesNeeded = buffer.length / numberOfChannels;
for (var i = 0; i < samplesNeeded; i++) {
if (this.needTraverse) {
this.nodes = this.traverse([]);
this.needTraverse = false;
}
// Tick in reverse order up to, but not including this node
for (var j = this.nodes.length - 1; j > 0; j--) {
this.nodes[j].tick();
}
// Cut down tick to just sum the input samples
this.createInputSamples();
for (var j = 0; j < numberOfChannels; j++) {
buffer[i * numberOfChannels + j] = input.samples[j];
}
this.writePosition += 1;
}
}
};
Get the current output position
AudioletDevice.prototype.getPlaybackTime = function() {
return this.sink.getPlaybackTime();
};
Get the current write position
AudioletDevice.prototype.getWriteTime = function() {
return this.writePosition;
};
Pause the output stream, and stop everything from ticking. The playback time will continue to increase, but the write time will be paused.
AudioletDevice.prototype.pause = function() {
this.paused = true;
};
Restart the output stream.
AudioletDevice.prototype.play = function() {
this.paused = false;
};
toString
AudioletDevice.prototype.toString = function() {
return 'Audio Output Device';
};
A sample-accurate scheduler built as an AudioletNode. The scheduler works by storing a queue of events, and running callback functions when the correct sample is being processed. All timing and events are handled in beats, which are converted to sample positions using a master tempo.
Inputs
Outputs
var Scheduler = function(audiolet, bpm) {
PassThroughNode.call(this, audiolet, 1, 1);
this.linkNumberOfOutputChannels(0, 0);
this.bpm = bpm || 120;
this.queue = new PriorityQueue(null, function(a, b) {
return (a.time < b.time);
});
this.time = 0;
this.beat = 0;
this.beatInBar = 0;
this.bar = 0;
this.seconds = 0;
this.beatsPerBar = 0;
this.lastBeatTime = 0;
this.beatLength = 60 / this.bpm * this.audiolet.device.sampleRate;
};
extend(Scheduler, PassThroughNode);
Set the tempo of the scheduler.
Scheduler.prototype.setTempo = function(bpm) {
this.bpm = bpm;
this.beatLength = 60 / this.bpm * this.audiolet.device.sampleRate;
};
Add an event relative to the current write position
Scheduler.prototype.addRelative = function(beats, callback) {
var event = {};
event.callback = callback;
event.time = this.time + beats * this.beatLength;
this.queue.push(event);
return event;
};
Add an event at an absolute beat position
Scheduler.prototype.addAbsolute = function(beat, callback) {
if (beat < this.beat ||
beat == this.beat && this.time > this.lastBeatTime) {
// Nah
return null;
}
var event = {};
event.callback = callback;
event.time = this.lastBeatTime + (beat - this.beat) * this.beatLength;
this.queue.push(event);
return event;
};
Schedule patterns to play, and provide the values generated to a callback. The durationPattern argument can be either a number, giving a constant time between each event, or a pattern, allowing varying time difference.
Scheduler.prototype.play = function(patterns, durationPattern, callback) {
var event = {};
event.patterns = patterns;
event.durationPattern = durationPattern;
event.callback = callback;
// TODO: Quantizing start time
event.time = this.audiolet.device.getWriteTime();
this.queue.push(event);
return event;
};
Schedule patterns to play starting at an absolute beat position, and provide the values generated to a callback. The durationPattern argument can be either a number, giving a constant time between each event, or a pattern, allowing varying time difference.
Scheduler.prototype.playAbsolute = function(beat, patterns, durationPattern,
callback) {
if (beat < this.beat ||
beat == this.beat && this.time > this.lastBeatTime) {
// Nah
return null;
}
var event = {};
event.patterns = patterns;
event.durationPattern = durationPattern;
event.callback = callback;
event.time = this.lastBeatTime + (beat - this.beat) * this.beatLength;
this.queue.push(event);
return event;
};
Remove a scheduled event from the scheduler
Scheduler.prototype.remove = function(event) {
var idx = this.queue.heap.indexOf(event);
if (idx != -1) {
this.queue.heap.splice(idx, 1);
// Recreate queue with event removed
this.queue = new PriorityQueue(this.queue.heap, function(a, b) {
return (a.time < b.time);
});
}
};
Alias for remove, so for simple events we have add/remove, and for patterns we have play/stop.
Scheduler.prototype.stop = function(event) {
this.remove(event);
};
Overridden tick method. Process any events which are due to take place either now or previously.
Scheduler.prototype.tick = function() {
PassThroughNode.prototype.tick.call(this);
this.tickClock();
while (!this.queue.isEmpty() &&
this.queue.peek().time <= this.time) {
var event = this.queue.pop();
this.processEvent(event);
}
};
Update the various representations of time within the scheduler.
Scheduler.prototype.tickClock = function() {
this.time += 1;
this.seconds = this.time / this.audiolet.device.sampleRate;
if (this.time >= this.lastBeatTime + this.beatLength) {
this.beat += 1;
this.beatInBar += 1;
if (this.beatInBar == this.beatsPerBar) {
this.bar += 1;
this.beatInBar = 0;
}
this.lastBeatTime += this.beatLength;
}
};
Process a single event, grabbing any necessary values, calling the event's callback, and rescheduling it if necessary.
Scheduler.prototype.processEvent = function(event) {
var durationPattern = event.durationPattern;
if (durationPattern) {
// Pattern event
var args = [];
var patterns = event.patterns;
var numberOfPatterns = patterns.length;
for (var i = 0; i < numberOfPatterns; i++) {
var pattern = patterns[i];
var value = pattern.next();
if (value != null) {
args.push(value);
}
else {
// Null value for an argument, so don't process the
// callback or add any further events
return;
}
}
event.callback.apply(null, args);
var duration;
if (durationPattern instanceof Pattern) {
duration = durationPattern.next();
}
else {
duration = durationPattern;
}
if (duration) {
// Beats -> time
event.time += duration * this.beatLength;
this.queue.push(event);
}
}
else {
// Regular event
event.callback();
}
};
toString
Scheduler.prototype.toString = function() {
return 'Scheduler';
};
A specialized type of AudioletNode where values from the inputs are passed straight to the corresponding outputs in the most efficient way possible. PassThroughNodes are used in AudioletGroups to provide the inputs and outputs, and can also be used in analysis nodes where no modifications to the incoming audio are made.
var PassThroughNode = function(audiolet, numberOfInputs, numberOfOutputs) {
AudioletNode.call(this, audiolet, numberOfInputs, numberOfOutputs);
};
extend(PassThroughNode, AudioletNode);
Create output samples for each channel, copying any input samples to the corresponding outputs.
PassThroughNode.prototype.createOutputSamples = function() {
var numberOfOutputs = this.outputs.length;
// Copy the inputs buffers straight to the output buffers
for (var i = 0; i < numberOfOutputs; i++) {
var input = this.inputs[i];
var output = this.outputs[i];
if (input && input.samples.length != 0) {
// Copy the input buffer straight to the output buffers
output.samples = input.samples;
}
else {
// Create the correct number of output samples
var numberOfChannels = output.getNumberOfChannels();
if (output.samples.length == numberOfChannels) {
continue;
}
else if (output.samples.length > numberOfChannels) {
output.samples = output.samples.slice(0, numberOfChannels);
continue;
}
for (var j = output.samples.length; j < numberOfChannels; j++) {
output.samples[j] = 0;
}
}
}
};
toString
PassThroughNode.prototype.toString = function() {
return 'Pass Through Node';
};
A type of AudioletNode designed to allow AudioletGroups to exactly replicate
the behaviour of AudioletParameters. By linking one of the group's inputs
to the ParameterNode's input, and calling this.parameterName =
parameterNode in the group's constructor, this.parameterName will behave
as if it were an AudioletParameter contained within an AudioletNode.
Inputs
Outputs
Parameters
var ParameterNode = function(audiolet, value) {
AudioletNode.call(this, audiolet, 1, 1);
this.parameter = new AudioletParameter(this, 0, value);
};
extend(ParameterNode, AudioletNode);
Process samples
ParameterNode.prototype.generate = function() {
this.outputs[0].samples[0] = this.parameter.getValue();
};
toString
ParameterNode.prototype.toString = function() {
return 'Parameter Node';
};
Priority Queue based on python heapq module http://svn.python.org/view/python/branches/release27-maint/Lib/heapq.py
var PriorityQueue = function(array, compare) {
if (compare) {
this.compare = compare;
}
if (array) {
this.heap = array;
for (var i = 0; i < Math.floor(this.heap.length / 2); i++) {
this.siftUp(i);
}
}
else {
this.heap = [];
}
};
Add an item to the queue
PriorityQueue.prototype.push = function(item) {
this.heap.push(item);
this.siftDown(0, this.heap.length - 1);
};
Remove and return the top item from the queue.
PriorityQueue.prototype.pop = function() {
var lastElement, returnItem;
lastElement = this.heap.pop();
if (this.heap.length) {
var returnItem = this.heap[0];
this.heap[0] = lastElement;
this.siftUp(0);
}
else {
returnItem = lastElement;
}
return (returnItem);
};
Return the top item from the queue, without removing it.
PriorityQueue.prototype.peek = function() {
return (this.heap[0]);
};
Check whether the queue is empty.
PriorityQueue.prototype.isEmpty = function() {
return (this.heap.length == 0);
};
Sift item down the queue.
PriorityQueue.prototype.siftDown = function(startPosition, position) {
var newItem = this.heap[position];
while (position > startPosition) {
var parentPosition = (position - 1) >> 1;
var parent = this.heap[parentPosition];
if (this.compare(newItem, parent)) {
this.heap[position] = parent;
position = parentPosition;
continue;
}
break;
}
this.heap[position] = newItem;
};
Sift item up the queue.
PriorityQueue.prototype.siftUp = function(position) {
var endPosition = this.heap.length;
var startPosition = position;
var newItem = this.heap[position];
var childPosition = 2 * position + 1;
while (childPosition < endPosition) {
var rightPosition = childPosition + 1;
if (rightPosition < endPosition &&
!this.compare(this.heap[childPosition],
this.heap[rightPosition])) {
childPosition = rightPosition;
}
this.heap[position] = this.heap[childPosition];
position = childPosition;
childPosition = 2 * position + 1;
}
this.heap[position] = newItem;
this.siftDown(startPosition, position);
};
Default compare function.
PriorityQueue.prototype.compare = function(a, b) {
return (a < b);
};
A method for extending a javascript pseudo-class Taken from http://peter.michaux.ca/articles/class-based-inheritance-in-javascript
function extend(subclass, superclass) {
function Dummy() {}
Dummy.prototype = superclass.prototype;
subclass.prototype = new Dummy();
subclass.prototype.constructor = subclass;
}
Bidirectional shim for the renaming of slice to subarray. Provides backwards compatibility with old browser releases
var Int8Array, Uint8Array, Int16Array, Uint16Array;
var Int32Array, Uint32Array, Float32Array, Float64Array;
var types = [Int8Array, Uint8Array, Int16Array, Uint16Array,
Int32Array, Uint32Array, Float32Array, Float64Array];
var original, shim;
for (var i = 0; i < types.length; ++i) {
if (types[i]) {
if (types[i].prototype.slice === undefined) {
original = 'subarray';
shim = 'slice';
}
else if (types[i].prototype.subarray === undefined) {
original = 'slice';
shim = 'subarray';
}
Object.defineProperty(types[i].prototype, shim, {
value: types[i].prototype[original],
enumerable: false
});
}
}
Audio generation and processing nodes
Sine wave oscillator
Inputs
Outputs
Parameters
var Sine = function(audiolet, frequency) {
AudioletNode.call(this, audiolet, 1, 1);
this.frequency = new AudioletParameter(this, 0, frequency || 440);
this.phase = 0;
};
extend(Sine, AudioletNode);
Process samples
Sine.prototype.generate = function() {
var output = this.outputs[0];
var frequency = this.frequency.getValue();
var sampleRate = this.audiolet.device.sampleRate;
output.samples[0] = Math.sin(this.phase);
this.phase += 2 * Math.PI * frequency / sampleRate;
if (this.phase > 2 * Math.PI) {
this.phase %= 2 * Math.PI;
}
};
toString
Sine.prototype.toString = function() {
return 'Sine';
};
Triangle wave oscillator using a lookup table
Inputs
Outputs
Parameters
var Triangle = function(audiolet, frequency) {
AudioletNode.call(this, audiolet, 1, 1);
this.frequency = new AudioletParameter(this, 0, frequency || 440);
this.phase = 0;
};
extend(Triangle, AudioletNode);
Process samples
Triangle.prototype.generate = function() {
var output = this.outputs[0];
var frequency = this.frequency.getValue();
var sampleRate = this.audiolet.device.sampleRate;
output.samples[0] = 1 - 4 * Math.abs((this.phase + 0.25) % 1 - 0.5);
this.phase += frequency / sampleRate;
if (this.phase > 1) {
this.phase %= 1;
}
};
toString
Triangle.prototype.toString = function() {
return 'Triangle';
};
Saw wave oscillator using a lookup table
Inputs
Outputs
Parameters
var Saw = function(audiolet, frequency) {
AudioletNode.call(this, audiolet, 1, 1);
this.frequency = new AudioletParameter(this, 0, frequency || 440);
this.phase = 0;
};
extend(Saw, AudioletNode);
Process samples
Saw.prototype.generate = function() {
var output = this.outputs[0];
var frequency = this.frequency.getValue();
var sampleRate = this.audiolet.device.sampleRate;
output.samples[0] = ((this.phase / 2 + 0.25) % 0.5 - 0.25) * 4;
this.phase += frequency / sampleRate;
if (this.phase > 1) {
this.phase %= 1;
}
};
toString
Saw.prototype.toString = function() {
return 'Saw';
};
Square wave oscillator
Inputs
Outputs
Parameters
var Square = function(audiolet, frequency) {
AudioletNode.call(this, audiolet, 1, 1);
this.frequency = new AudioletParameter(this, 0, frequency || 440);
this.phase = 0;
};
extend(Square, AudioletNode);
Process samples
Square.prototype.generate = function() {
var output = this.outputs[0];
var frequency = this.frequency.getValue();
var sampleRate = this.audiolet.device.sampleRate;
output.samples[0] = this.phase > 0.5 ? 1 : -1;
this.phase += frequency / sampleRate;
if (this.phase > 1) {
this.phase %= 1;
}
};
toString
Square.prototype.toString = function() {
return 'Square';
};
Pulse wave oscillator.
Inputs
Outputs
Parameters
var Pulse = function(audiolet, frequency, pulseWidth) {
AudioletNode.call(this, audiolet, 2, 1);
this.frequency = new AudioletParameter(this, 0, frequency || 440);
this.pulseWidth = new AudioletParameter(this, 1, pulseWidth || 0.5);
this.phase = 0;
};
extend(Pulse, AudioletNode);
Process samples
Pulse.prototype.generate = function() {
var pulseWidth = this.pulseWidth.getValue();
this.outputs[0].samples[0] = (this.phase < pulseWidth) ? 1 : -1;
var frequency = this.frequency.getValue();
var sampleRate = this.audiolet.device.sampleRate;
this.phase += frequency / sampleRate;
if (this.phase > 1) {
this.phase %= 1;
}
};
toString
Pulse.prototype.toString = function() {
return 'Pulse';
};
A white noise source
Outputs
var WhiteNoise = function(audiolet) {
AudioletNode.call(this, audiolet, 0, 1);
};
extend(WhiteNoise, AudioletNode);
Process samples
WhiteNoise.prototype.generate = function() {
this.outputs[0].samples[0] = Math.random() * 2 - 1;
};
toString
WhiteNoise.prototype.toString = function() {
return 'White Noise';
};
A generic envelope consisting of linear transitions of varying duration between a series of values.
Inputs
Outputs
Parameters
var Envelope = function(audiolet, gate, levels, times, releaseStage,
onComplete) {
AudioletNode.call(this, audiolet, 1, 1);
this.gate = new AudioletParameter(this, 0, gate || 1);
this.levels = levels;
this.times = times;
this.releaseStage = releaseStage;
this.onComplete = onComplete;
this.stage = null;
this.time = null;
this.changeTime = null;
this.level = this.levels[0];
this.delta = 0;
this.gateOn = false;
};
extend(Envelope, AudioletNode);
Process samples
Envelope.prototype.generate = function() {
var gate = this.gate.getValue();
var stageChanged = false;
if (gate && !this.gateOn) {
// Key pressed
this.gateOn = true;
this.stage = 0;
this.time = 0;
this.delta = 0;
this.level = this.levels[0];
if (this.stage != this.releaseStage) {
stageChanged = true;
}
}
if (this.gateOn && !gate) {
// Key released
this.gateOn = false;
if (this.releaseStage != null) {
// Jump to the release stage
this.stage = this.releaseStage;
stageChanged = true;
}
}
if (this.changeTime) {
// We are not sustaining, and we are playing, so increase the
// time
this.time += 1;
if (this.time >= this.changeTime) {
// Need to go to the next stage
this.stage += 1;
if (this.stage != this.releaseStage) {
stageChanged = true;
}
else {
// If we reach the release stage then sustain the value
// until the gate is released rather than moving on
// to the next level.
this.changeTime = null;
this.delta = 0;
}
}
}
if (stageChanged) {
// level = this.levels[stage];
if (this.stage != this.times.length) {
// Actually update the variables
this.delta = this.calculateDelta(this.stage, this.level);
this.changeTime = this.calculateChangeTime(this.stage, this.time);
}
else {
// Made it to the end, so finish up
if (this.onComplete) {
this.onComplete();
}
this.stage = null;
this.time = null;
this.changeTime = null;
this.delta = 0;
}
}
this.level += this.delta;
this.outputs[0].samples[0] = this.level;
};
Calculate the change in level needed each sample for a section
Envelope.prototype.calculateDelta = function(stage, level) {
var delta = this.levels[stage + 1] - level;
var stageTime = this.times[stage] * this.audiolet.device.sampleRate;
return (delta / stageTime);
};
Calculate the time in samples at which the next stage starts
Envelope.prototype.calculateChangeTime = function(stage, time) {
var stageTime = this.times[stage] * this.audiolet.device.sampleRate;
return (time + stageTime);
};
toString
Envelope.prototype.toString = function() {
return 'Envelope';
};
Linear attack-decay-sustain-release envelope
Inputs
Outputs
Parameters
var ADSREnvelope = function(audiolet, gate, attack, decay, sustain, release,
onComplete) {
var levels = [0, 1, sustain, 0];
var times = [attack, decay, release];
Envelope.call(this, audiolet, gate, levels, times, 2, onComplete);
};
extend(ADSREnvelope, Envelope);
toString
ADSREnvelope.prototype.toString = function() {
return 'ADSR Envelope';
};
Simple attack-release envelope
Inputs
Outputs
Parameters
var PercussiveEnvelope = function(audiolet, gate, attack, release,
onComplete) {
var levels = [0, 1, 0];
var times = [attack, release];
Envelope.call(this, audiolet, gate, levels, times, null, onComplete);
};
extend(PercussiveEnvelope, Envelope);
toString
PercussiveEnvelope.prototype.toString = function() {
return 'Percussive Envelope';
};
Play the contents of an audio buffer
Inputs
Outputs
Parameters
var BufferPlayer = function(audiolet, buffer, playbackRate, startPosition,
loop, onComplete) {
AudioletNode.call(this, audiolet, 3, 1);
this.buffer = buffer;
this.setNumberOfOutputChannels(0, this.buffer.numberOfChannels);
this.position = startPosition || 0;
this.playbackRate = new AudioletParameter(this, 0, playbackRate || 1);
this.restartTrigger = new AudioletParameter(this, 1, 0);
this.startPosition = new AudioletParameter(this, 2, startPosition || 0);
this.loop = new AudioletParameter(this, 3, loop || 0);
this.onComplete = onComplete;
this.restartTriggerOn = false;
this.playing = true;
};
extend(BufferPlayer, AudioletNode);
Process samples
BufferPlayer.prototype.generate = function() {
var output = this.outputs[0];
// Cache local variables
var numberOfChannels = output.samples.length;
if (this.buffer.length == 0 || !this.playing) {
// No buffer data, or not playing, so output zeros and return
for (var i=0; i<numberOfChannels; i++) {
output.samples[i] = 0;
}
return;
}
// Crap load of parameters
var playbackRate = this.playbackRate.getValue();
var restartTrigger = this.restartTrigger.getValue();
var startPosition = this.startPosition.getValue();
var loop = this.loop.getValue();
if (restartTrigger > 0 && !this.restartTriggerOn) {
// Trigger moved from <=0 to >0, so we restart playback from
// startPosition
this.position = startPosition;
this.restartTriggerOn = true;
this.playing = true;
}
if (restartTrigger <= 0 && this.restartTriggerOn) {
// Trigger moved back to <= 0
this.restartTriggerOn = false;
}
var numberOfChannels = this.buffer.channels.length;
for (var i = 0; i < numberOfChannels; i++) {
var inputChannel = this.buffer.getChannelData(i);
output.samples[i] = inputChannel[Math.floor(this.position)];
}
this.position += playbackRate;
if (this.position >= this.buffer.length) {
if (loop) {
// Back to the start
this.position %= this.buffer.length;
}
else {
// Finish playing until a new restart trigger
this.playing = false;
if (this.onComplete) {
this.onComplete();
}
}
}
};
toString
BufferPlayer.prototype.toString = function() {
return ('Buffer player');
};
Simple gain control
Inputs
Outputs
Parameters
var Gain = function(audiolet, gain) {
// Same DSP as operators/Multiply.js, but different parameter name
Multiply.call(this, audiolet, gain);
this.gain = this.value;
};
extend(Gain, Multiply);
toString
Gain.prototype.toString = function() {
return ('Gain');
};
Position a single-channel input in stereo space
Inputs
Outputs
Parameters
var Pan = function(audiolet, pan) {
AudioletNode.call(this, audiolet, 2, 1);
// Hardcode two output channels
this.setNumberOfOutputChannels(0, 2);
if (pan == null) {
var pan = 0.5;
}
this.pan = new AudioletParameter(this, 1, pan);
};
extend(Pan, AudioletNode);
Process samples
Pan.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var pan = this.pan.getValue();
var value = input.samples[0] || 0;
var scaledPan = pan * Math.PI / 2;
output.samples[0] = value * Math.cos(scaledPan);
output.samples[1] = value * Math.sin(scaledPan);
};
toString
Pan.prototype.toString = function() {
return 'Stereo Panner';
};
Upmix an input to a constant number of output channels
Inputs
Outputs
var UpMixer = function(audiolet, outputChannels) {
AudioletNode.call(this, audiolet, 1, 1);
this.outputs[0].numberOfChannels = outputChannels;
};
extend(UpMixer, AudioletNode);
Process samples
UpMixer.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var numberOfInputChannels = input.samples.length;
var numberOfOutputChannels = output.samples.length;
if (numberOfInputChannels == numberOfOutputChannels) {
output.samples = input.samples;
}
else {
for (var i = 0; i < numberOfOutputChannels; i++) {
output.samples[i] = input.samples[i % numberOfInputChannels];
}
}
};
toString
UpMixer.prototype.toString = function() {
return 'UpMixer';
};
Equal-power cross-fade between two signals
Inputs
Outputs
Parameters
var CrossFade = function(audiolet, position) {
AudioletNode.call(this, audiolet, 3, 1);
this.linkNumberOfOutputChannels(0, 0);
this.position = new AudioletParameter(this, 2, position || 0.5);
};
extend(CrossFade, AudioletNode);
Process samples
CrossFade.prototype.generate = function() {
var inputA = this.inputs[0];
var inputB = this.inputs[1];
var output = this.outputs[0];
// Local processing variables
var position = this.position.getValue();
var scaledPosition = position * Math.PI / 2;
var gainA = Math.cos(scaledPosition);
var gainB = Math.sin(scaledPosition);
var numberOfChannels = output.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
var valueA = inputA.samples[i] || 0;
var valueB = inputB.samples[i] || 0;
output.samples[i] = valueA * gainA + valueB * gainB;
}
};
toString
CrossFade.prototype.toString = function() {
return 'Cross Fader';
};
Linear cross-fade between two signals
Inputs
Outputs
Parameters
var LinearCrossFade = function(audiolet, position) {
AudioletNode.call(this, audiolet, 3, 1);
this.linkNumberOfOutputChannels(0, 0);
this.position = new AudioletParameter(this, 2, position || 0.5);
};
extend(LinearCrossFade, AudioletNode);
Process samples
LinearCrossFade.prototype.generate = function() {
var inputA = this.inputs[0];
var inputB = this.inputs[1];
var output = this.outputs[0];
var position = this.position.getValue();
var gainA = 1 - position;
var gainB = position;
var numberOfChannels = output.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
var valueA = inputA.samples[i] || 0;
var valueB = inputB.samples[i] || 0;
output.samples[i] = valueA * gainA + valueB * gainB;
}
};
toString
LinearCrossFade.prototype.toString = function() {
return 'Linear Cross Fader';
};
A simple (and frankly shoddy) zero-lookahead limiter.
Inputs
Outputs
Parameters
var Limiter = function(audiolet, threshold, attack, release) {
AudioletNode.call(this, audiolet, 4, 1);
this.linkNumberOfOutputChannels(0, 0);
// Parameters
this.threshold = new AudioletParameter(this, 1, threshold || 0.95);
this.attack = new AudioletParameter(this, 2, attack || 0.01);
this.release = new AudioletParameter(this, 2, release || 0.4);
this.followers = [];
};
extend(Limiter, AudioletNode);
Process samples
Limiter.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var sampleRate = this.audiolet.device.sampleRate;
// Local processing variables
var attack = Math.pow(0.01, 1 / (this.attack.getValue() *
sampleRate));
var release = Math.pow(0.01, 1 / (this.release.getValue() *
sampleRate));
var threshold = this.threshold.getValue();
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
if (i >= this.followers.length) {
this.followers.push(0);
}
var follower = this.followers[i];
var value = input.samples[i];
// Calculate amplitude envelope
var absValue = Math.abs(value);
if (absValue > follower) {
follower = attack * (follower - absValue) + absValue;
}
else {
follower = release * (follower - absValue) + absValue;
}
var diff = follower - threshold;
if (diff > 0) {
output.samples[i] = value / (1 + diff);
}
else {
output.samples[i] = value;
}
this.followers[i] = follower;
}
};
toString
Limiter.prototype.toString = function() {
return 'Limiter';
};
Generic biquad filter. The coefficients (a0, a1, a2, b0, b1 and b2) are set using the calculateCoefficients function, which should be overridden and will be called automatically when new values are needed.
Inputs
Outputs
Parameters
var BiquadFilter = function(audiolet, frequency) {
AudioletNode.call(this, audiolet, 2, 1);
// Same number of output channels as input channels
this.linkNumberOfOutputChannels(0, 0);
this.frequency = new AudioletParameter(this, 1, frequency || 22100);
this.lastFrequency = null; // See if we need to recalculate coefficients
// Delayed values
this.xValues = [];
this.yValues = [];
// Coefficients
this.b0 = 0;
this.b1 = 0;
this.b2 = 0;
this.a0 = 0;
this.a1 = 0;
this.a2 = 0;
};
extend(BiquadFilter, AudioletNode);
Calculate the biquad filter coefficients. This should be overridden.
BiquadFilter.prototype.calculateCoefficients = function(frequency) {
};
Process samples
BiquadFilter.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0]
var xValueArray = this.xValues;
var yValueArray = this.yValues;
var frequency = this.frequency.getValue();
if (frequency != this.lastFrequency) {
// Recalculate the coefficients
this.calculateCoefficients(frequency);
this.lastFrequency = frequency;
}
var a0 = this.a0;
var a1 = this.a1;
var a2 = this.a2;
var b0 = this.b0;
var b1 = this.b1;
var b2 = this.b2;
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
if (i >= xValueArray.length) {
xValueArray.push([0, 0]);
yValueArray.push([0, 0]);
}
var xValues = xValueArray[i];
var x1 = xValues[0];
var x2 = xValues[1];
var yValues = yValueArray[i];
var y1 = yValues[0];
var y2 = yValues[1];
var x0 = input.samples[i];
var y0 = (b0 / a0) * x0 +
(b1 / a0) * x1 +
(b2 / a0) * x2 -
(a1 / a0) * y1 -
(a2 / a0) * y2;
output.samples[i] = y0;
xValues[0] = x0;
xValues[1] = x1;
yValues[0] = y0;
yValues[1] = y1;
}
};
toString
BiquadFilter.prototype.toString = function() {
return 'Biquad Filter';
};
Low-pass filter
Inputs
Outputs
Parameters
var LowPassFilter = function(audiolet, frequency) {
BiquadFilter.call(this, audiolet, frequency);
};
extend(LowPassFilter, BiquadFilter);
Calculate the biquad filter coefficients using maths from http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt
LowPassFilter.prototype.calculateCoefficients = function(frequency) {
var w0 = 2 * Math.PI * frequency /
this.audiolet.device.sampleRate;
var cosw0 = Math.cos(w0);
var sinw0 = Math.sin(w0);
var alpha = sinw0 / (2 / Math.sqrt(2));
this.b0 = (1 - cosw0) / 2;
this.b1 = 1 - cosw0;
this.b2 = this.b0;
this.a0 = 1 + alpha;
this.a1 = -2 * cosw0;
this.a2 = 1 - alpha;
};
toString
LowPassFilter.prototype.toString = function() {
return 'Low Pass Filter';
};
High-pass filter
Inputs
Outputs
Parameters
var HighPassFilter = function(audiolet, frequency) {
BiquadFilter.call(this, audiolet, frequency);
};
extend(HighPassFilter, BiquadFilter);
Calculate the biquad filter coefficients using maths from http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt
HighPassFilter.prototype.calculateCoefficients = function(frequency) {
var w0 = 2 * Math.PI * frequency /
this.audiolet.device.sampleRate;
var cosw0 = Math.cos(w0);
var sinw0 = Math.sin(w0);
var alpha = sinw0 / (2 / Math.sqrt(2));
this.b0 = (1 + cosw0) / 2;
this.b1 = - (1 + cosw0);
this.b2 = this.b0;
this.a0 = 1 + alpha;
this.a1 = -2 * cosw0;
this.a2 = 1 - alpha;
};
toString
HighPassFilter.prototype.toString = function() {
return 'High Pass Filter';
};
Band-pass filter
Inputs
Outputs
Parameters
var BandPassFilter = function(audiolet, frequency) {
BiquadFilter.call(this, audiolet, frequency);
};
extend(BandPassFilter, BiquadFilter);
Calculate the biquad filter coefficients using maths from http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt
BandPassFilter.prototype.calculateCoefficients = function(frequency) {
var w0 = 2 * Math.PI * frequency / this.audiolet.device.sampleRate;
var cosw0 = Math.cos(w0);
var sinw0 = Math.sin(w0);
var alpha = sinw0 / (2 / Math.sqrt(2));
this.b0 = alpha;
this.b1 = 0;
this.b2 = -alpha;
this.a0 = 1 + alpha;
this.a1 = -2 * cosw0;
this.a2 = 1 - alpha;
};
toString
BandPassFilter.prototype.toString = function() {
return 'Band Pass Filter';
};
Band-reject filter
Inputs
Outputs
Parameters
var BandRejectFilter = function(audiolet, frequency) {
BiquadFilter.call(this, audiolet, frequency);
};
extend(BandRejectFilter, BiquadFilter);
Calculate the biquad filter coefficients using maths from http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt
BandRejectFilter.prototype.calculateCoefficients = function(frequency) {
var w0 = 2 * Math.PI * frequency /
this.audiolet.device.sampleRate;
var cosw0 = Math.cos(w0);
var sinw0 = Math.sin(w0);
var alpha = sinw0 / (2 / Math.sqrt(2));
this.b0 = 1;
this.b1 = -2 * cosw0;
this.b2 = 1;
this.a0 = 1 + alpha;
this.a1 = -2 * cosw0;
this.a2 = 1 - alpha;
};
toString
BandRejectFilter.prototype.toString = function() {
return 'Band Reject Filter';
};
All-pass filter
Inputs
Outputs
Parameters
var AllPassFilter = function(audiolet, frequency) {
BiquadFilter.call(this, audiolet, frequency);
};
extend(AllPassFilter, BiquadFilter);
Calculate the biquad filter coefficients using maths from http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt
AllPassFilter.prototype.calculateCoefficients = function(frequency) {
var w0 = 2 * Math.PI * frequency /
this.audiolet.device.sampleRate;
var cosw0 = Math.cos(w0);
var sinw0 = Math.sin(w0);
var alpha = sinw0 / (2 / Math.sqrt(2));
this.b0 = 1 - alpha;
this.b1 = -2 * cosw0;
this.b2 = 1 + alpha;
this.a0 = 1 + alpha;
this.a1 = -2 * cosw0;
this.a2 = 1 - alpha;
};
toString
AllPassFilter.prototype.toString = function() {
return 'All Pass Filter';
};
Filter for leaking DC offset. Maths is taken from https://ccrma.stanford.edu/~jos/filters/DC_Blocker.html
Inputs
Outputs
Parameters
var DCFilter = function(audiolet, coefficient) {
AudioletNode.call(this, audiolet, 2, 1);
// Same number of output channels as input channels
this.linkNumberOfOutputChannels(0, 0);
this.coefficient = new AudioletParameter(this, 1, coefficient || 0.995);
// Delayed values
this.xValues = [];
this.yValues = [];
};
extend(DCFilter, AudioletNode);
Process samples
DCFilter.prototype.generate = function() {
var coefficient = this.coefficient.getValue();
var input = this.inputs[0];
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
if (i >= this.xValues.length) {
this.xValues.push(0);
}
if (i >= this.yValues.length) {
this.yValues.push(0);
}
var x0 = input.samples[i];
var y0 = x0 - this.xValues[i] + coefficient * this.yValues[i];
this.outputs[0].samples[i] = y0;
this.xValues[i] = x0;
this.yValues[i] = y0;
}
};
toString
DCFilter.prototype.toString = function() {
return 'DC Filter';
};
Exponential lag for smoothing signals.
Inputs
Outputs
Parameters
var Lag = function(audiolet, value, lagTime) {
AudioletNode.call(this, audiolet, 2, 1);
this.value = new AudioletParameter(this, 0, value || 0);
this.lag = new AudioletParameter(this, 1, lagTime || 1);
this.lastValue = 0;
this.log001 = Math.log(0.001);
};
extend(Lag, AudioletNode);
Process samples
Lag.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var sampleRate = this.audiolet.device.sampleRate;
var value = this.value.getValue();
var lag = this.lag.getValue();
var coefficient = Math.exp(this.log001 / (lag * sampleRate));
var outputValue = ((1 - coefficient) * value) +
(coefficient * this.lastValue);
output.samples[0] = outputValue;
this.lastValue = outputValue;
};
toString
Lag.prototype.toString = function() {
return 'Lag';
};
A simple delay line.
Inputs
Outputs
Parameters
var Delay = function(audiolet, maximumDelayTime, delayTime) {
AudioletNode.call(this, audiolet, 2, 1);
this.linkNumberOfOutputChannels(0, 0);
this.maximumDelayTime = maximumDelayTime;
this.delayTime = new AudioletParameter(this, 1, delayTime || 1);
var bufferSize = maximumDelayTime * this.audiolet.device.sampleRate;
this.buffers = [];
this.readWriteIndex = 0;
};
extend(Delay, AudioletNode);
Process samples
Delay.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var sampleRate = this.audiolet.device.sampleRate;
var delayTime = this.delayTime.getValue() * sampleRate;
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
if (i >= this.buffers.length) {
var bufferSize = this.maximumDelayTime * sampleRate;
this.buffers.push(new Float32Array(bufferSize));
}
var buffer = this.buffers[i];
output.samples[i] = buffer[this.readWriteIndex];
buffer[this.readWriteIndex] = input.samples[i];
}
this.readWriteIndex += 1;
if (this.readWriteIndex >= delayTime) {
this.readWriteIndex = 0;
}
};
toString
Delay.prototype.toString = function() {
return 'Delay';
};
Delay line with feedback
Inputs
Outputs
Parameters
var FeedbackDelay = function(audiolet, maximumDelayTime, delayTime, feedback,
mix) {
AudioletNode.call(this, audiolet, 4, 1);
this.linkNumberOfOutputChannels(0, 0);
this.maximumDelayTime = maximumDelayTime;
this.delayTime = new AudioletParameter(this, 1, delayTime || 1);
this.feedback = new AudioletParameter(this, 2, feedback || 0.5);
this.mix = new AudioletParameter(this, 3, mix || 1);
var bufferSize = maximumDelayTime * this.audiolet.device.sampleRate;
this.buffers = [];
this.readWriteIndex = 0;
};
extend(FeedbackDelay, AudioletNode);
Process samples
FeedbackDelay.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var sampleRate = this.audiolet.output.device.sampleRate;
var delayTime = this.delayTime.getValue() * sampleRate;
var feedback = this.feedback.getValue();
var mix = this.mix.getValue();
var numberOfChannels = input.samples.length;
var numberOfBuffers = this.buffers.length;
for (var i = 0; i < numberOfChannels; i++) {
if (i >= numberOfBuffers) {
// Create buffer for channel if it doesn't already exist
var bufferSize = this.maximumDelayTime * sampleRate;
this.buffers.push(new Float32Array(bufferSize));
}
var buffer = this.buffers[i];
var inputSample = input.samples[i];
var bufferSample = buffer[this.readWriteIndex];
output.samples[i] = mix * bufferSample + (1 - mix) * inputSample;
buffer[this.readWriteIndex] = inputSample + feedback * bufferSample;
}
this.readWriteIndex += 1;
if (this.readWriteIndex >= delayTime) {
this.readWriteIndex = 0;
}
};
toString
FeedbackDelay.prototype.toString = function() {
return 'Feedback Delay';
};
Undamped comb filter
Inputs
Outputs
Parameters
var CombFilter = function(audiolet, maximumDelayTime, delayTime, decayTime) {
AudioletNode.call(this, audiolet, 3, 1);
this.linkNumberOfOutputChannels(0, 0);
this.maximumDelayTime = maximumDelayTime;
this.delayTime = new AudioletParameter(this, 1, delayTime || 1);
this.decayTime = new AudioletParameter(this, 2, decayTime);
this.buffers = [];
this.readWriteIndex = 0;
};
extend(CombFilter, AudioletNode);
Process samples
CombFilter.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var sampleRate = this.audiolet.device.sampleRate;
var delayTime = this.delayTime.getValue() * sampleRate;
var decayTime = this.decayTime.getValue() * sampleRate;
var feedback = Math.exp(-3 * delayTime / decayTime);
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
if (i >= this.buffers.length) {
// Create buffer for channel if it doesn't already exist
var bufferSize = this.maximumDelayTime * sampleRate;
this.buffers.push(new Float32Array(bufferSize));
}
var buffer = this.buffers[i];
var outputValue = buffer[this.readWriteIndex];
output.samples[i] = outputValue;
buffer[this.readWriteIndex] = input.samples[i] + feedback * outputValue;
}
this.readWriteIndex += 1;
if (this.readWriteIndex >= delayTime) {
this.readWriteIndex = 0;
}
};
toString
CombFilter.prototype.toString = function() {
return 'Comb Filter';
};
Damped comb filter
Inputs
Outputs
Parameters
var DampedCombFilter = function(audiolet, maximumDelayTime, delayTime,
decayTime, damping) {
AudioletNode.call(this, audiolet, 4, 1);
this.linkNumberOfOutputChannels(0, 0);
this.maximumDelayTime = maximumDelayTime;
this.delayTime = new AudioletParameter(this, 1, delayTime || 1);
this.decayTime = new AudioletParameter(this, 2, decayTime);
this.damping = new AudioletParameter(this, 3, damping);
var bufferSize = maximumDelayTime * this.audiolet.device.sampleRate;
this.buffers = [];
this.readWriteIndex = 0;
this.filterStores = [];
};
extend(DampedCombFilter, AudioletNode);
Process samples
DampedCombFilter.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var sampleRate = this.audiolet.device.sampleRate;
var delayTime = this.delayTime.getValue() * sampleRate;
var decayTime = this.decayTime.getValue() * sampleRate;
var damping = this.damping.getValue();
var feedback = Math.exp(-3 * delayTime / decayTime);
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
if (i >= this.buffers.length) {
var bufferSize = this.maximumDelayTime * sampleRate;
this.buffers.push(new Float32Array(bufferSize));
}
if (i >= this.filterStores.length) {
this.filterStores.push(0);
}
var buffer = this.buffers[i];
var filterStore = this.filterStores[i];
var outputValue = buffer[this.readWriteIndex];
filterStore = (outputValue * (1 - damping)) +
(filterStore * damping);
output.samples[i] = outputValue;
buffer[this.readWriteIndex] = input.samples[i] +
feedback * filterStore;
this.filterStores[i] = filterStore;
}
this.readWriteIndex += 1;
if (this.readWriteIndex >= delayTime) {
this.readWriteIndex = 0;
}
};
toString
DampedCombFilter.prototype.toString = function() {
return 'Damped Comb Filter';
};
Port of the Freeverb Schrodoer/Moorer reverb model. See https://ccrma.stanford.edu/~jos/pasp/Freeverb.html for a description of how each part works.
Inputs
Outputs
Parameters
var Reverb = function(audiolet, mix, roomSize, damping) {
AudioletNode.call(this, audiolet, 4, 1);
// Constants
this.initialMix = 0.33;
this.fixedGain = 0.015;
this.initialDamping = 0.5;
this.scaleDamping = 0.4;
this.initialRoomSize = 0.5;
this.scaleRoom = 0.28;
this.offsetRoom = 0.7;
// Parameters: for 44.1k or 48k
this.combTuning = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
this.allPassTuning = [556, 441, 341, 225];
// Controls
// Mix control
var mix = mix || this.initialMix;
this.mix = new AudioletParameter(this, 1, mix);
// Room size control
var roomSize = roomSize || this.initialRoomSize;
this.roomSize = new AudioletParameter(this, 2, roomSize);
// Damping control
var damping = damping || this.initialDamping;
this.damping = new AudioletParameter(this, 3, damping);
// Damped comb filters
this.combBuffers = [];
this.combIndices = [];
this.filterStores = [];
var numberOfCombs = this.combTuning.length;
for (var i = 0; i < numberOfCombs; i++) {
this.combBuffers.push(new Float32Array(this.combTuning[i]));
this.combIndices.push(0);
this.filterStores.push(0);
}
// All-pass filters
this.allPassBuffers = [];
this.allPassIndices = [];
var numberOfFilters = this.allPassTuning.length;
for (var i = 0; i < numberOfFilters; i++) {
this.allPassBuffers.push(new Float32Array(this.allPassTuning[i]));
this.allPassIndices.push(0);
}
};
extend(Reverb, AudioletNode);
Process samples
Reverb.prototype.generate = function() {
var mix = this.mix.getValue();
var roomSize = this.roomSize.getValue();
var damping = this.damping.getValue();
var numberOfCombs = this.combTuning.length;
var numberOfFilters = this.allPassTuning.length;
var value = this.inputs[0].samples[0] || 0;
var dryValue = value;
value *= this.fixedGain;
var gainedValue = value;
var damping = damping * this.scaleDamping;
var feedback = roomSize * this.scaleRoom + this.offsetRoom;
for (var i = 0; i < numberOfCombs; i++) {
var combIndex = this.combIndices[i];
var combBuffer = this.combBuffers[i];
var filterStore = this.filterStores[i];
var output = combBuffer[combIndex];
filterStore = (output * (1 - damping)) +
(filterStore * damping);
value += output;
combBuffer[combIndex] = gainedValue + feedback * filterStore;
combIndex += 1;
if (combIndex >= combBuffer.length) {
combIndex = 0;
}
this.combIndices[i] = combIndex;
this.filterStores[i] = filterStore;
}
for (var i = 0; i < numberOfFilters; i++) {
var allPassBuffer = this.allPassBuffers[i];
var allPassIndex = this.allPassIndices[i];
var input = value;
var bufferValue = allPassBuffer[allPassIndex];
value = -value + bufferValue;
allPassBuffer[allPassIndex] = input + (bufferValue * 0.5);
allPassIndex += 1;
if (allPassIndex >= allPassBuffer.length) {
allPassIndex = 0;
}
this.allPassIndices[i] = allPassIndex;
}
this.outputs[0].samples[0] = mix * value + (1 - mix) * dryValue;
};
toString
Reverb.prototype.toString = function() {
return 'Reverb';
};
A soft-clipper, which distorts at values over +-0.5.
Inputs
Outputs
var SoftClip = function(audiolet) {
AudioletNode.call(this, audiolet, 1, 1);
this.linkNumberOfOutputChannels(0, 0);
};
extend(SoftClip, AudioletNode);
Process samples
SoftClip.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
var value = input.samples[i];
if (value > 0.5 || value < -0.5) {
output.samples[i] = (Math.abs(value) - 0.25) / value;
}
else {
output.samples[i] = value;
}
}
};
toString
SoftClip.prototype.toString = function() {
return ('SoftClip');
};
Reduce the bitrate of incoming audio
Inputs
Outputs
Parameters
var BitCrusher = function(audiolet, bits) {
AudioletNode.call(this, audiolet, 2, 1);
this.linkNumberOfOutputChannels(0, 0);
this.bits = new AudioletParameter(this, 1, bits);
};
extend(BitCrusher, AudioletNode);
Process samples
BitCrusher.prototype.generate = function() {
var input = this.inputs[0];
var maxValue = Math.pow(2, this.bits.getValue()) - 1;
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
this.outputs[0].samples[i] = Math.floor(input.samples[i] * maxValue) /
maxValue;
}
};
toString
BitCrusher.prototype.toString = function() {
return 'BitCrusher';
};
Amplitude envelope follower
Inputs
Outputs
Parameters
var Amplitude = function(audiolet, attack, release) {
AudioletNode.call(this, audiolet, 3, 1);
this.linkNumberOfOutputChannels(0, 0);
this.followers = [];
this.attack = new AudioletParameter(this, 1, attack || 0.01);
this.release = new AudioletParameter(this, 2, release || 0.01);
};
extend(Amplitude, AudioletNode);
Process samples
Amplitude.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var followers = this.followers;
var numberOfFollowers = followers.length;
var sampleRate = this.audiolet.device.sampleRate;
// Local processing variables
var attack = this.attack.getValue();
attack = Math.pow(0.01, 1 / (attack * sampleRate));
var release = this.release.getValue();
release = Math.pow(0.01, 1 / (release * sampleRate));
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
if (i >= numberOfFollowers) {
followers.push(0);
}
var follower = followers[i];
var value = Math.abs(input.samples[i]);
if (value > follower) {
follower = attack * (follower - value) + value;
}
else {
follower = release * (follower - value) + value;
}
output.samples[i] = follower;
followers[i] = follower;
}
};
toString
Amplitude.prototype.toString = function() {
return ('Amplitude');
};
Detect discontinuities in the input stream. Looks for consecutive samples with a difference larger than a threshold value.
Inputs
Outputs
var DiscontinuityDetector = function(audiolet, threshold, callback) {
AudioletNode.call(this, audiolet, 1, 1);
this.linkNumberOfOutputChannels(0, 0);
this.threshold = threshold || 0.2;
if (callback) {
this.callback = callback;
}
this.lastValues = [];
};
extend(DiscontinuityDetector, AudioletNode);
Default callback. Logs the size and position of the discontinuity.
DiscontinuityDetector.prototype.callback = function(size, channel) {
console.error('Discontinuity of ' + size + ' detected on channel ' +
channel);
};
Process samples
DiscontinuityDetector.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
if (i >= this.lastValues.length) {
this.lastValues.push(0);
}
var value = input.samples[i];
var diff = Math.abs(this.lastValues[i] - value);
if (diff > this.threshold) {
this.callback(diff, i);
}
this.lastValues[i] = value;
}
};
toString
DiscontinuityDetector.prototype.toString = function() {
return 'Discontinuity Detector';
};
Detect potentially hazardous values in the audio stream. Looks for undefineds, nulls, NaNs and Infinities.
Inputs
Outputs
var BadValueDetector = function(audiolet, callback) {
PassThroughNode.call(this, audiolet, 1, 1);
this.linkNumberOfOutputChannels(0, 0);
if (callback) {
this.callback = callback;
}
};
extend(BadValueDetector, PassThroughNode);
Default callback. Logs the value and position of the bad value.
BadValueDetector.prototype.callback = function(value, channel) {
console.error(value + ' detected at channel ' + channel);
};
Process samples
BadValueDetector.prototype.generate = function() {
var input = this.inputs[0];
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
var value = input.samples[i];
if (typeof value == 'undefined' ||
value == null ||
isNaN(value) ||
value == Infinity ||
value == -Infinity) {
this.callback(value, i);
}
}
};
toString
BadValueDetector.prototype.toString = function() {
return 'Bad Value Detector';
};
Simple trigger which allows you to set a single sample to be 1 and then resets itself.
Outputs
Parameters
var TriggerControl = function(audiolet, trigger) {
AudioletNode.call(this, audiolet, 0, 1);
this.trigger = new AudioletParameter(this, null, trigger || 0);
};
extend(TriggerControl, AudioletNode);
Process samples
TriggerControl.prototype.generate = function() {
if (this.trigger.getValue() > 0) {
this.outputs[0].samples[0] = 1;
this.trigger.setValue(0);
}
else {
this.outputs[0].samples[0] = 0;
}
};
toString
TriggerControl.prototype.toString = function() {
return 'Trigger Control';
};
Fast Fourier Transform
Inputs
Outputs
var FFT = function(audiolet, bufferSize) {
AudioletNode.call(this, audiolet, 2, 1);
this.linkNumberOfOutputChannels(0, 0);
this.bufferSize = bufferSize;
this.readWriteIndex = 0;
this.buffer = new Float32Array(this.bufferSize);
this.realBuffer = new Float32Array(this.bufferSize);
this.imaginaryBuffer = new Float32Array(this.bufferSize);
this.reverseTable = new Uint32Array(this.bufferSize);
this.calculateReverseTable();
};
extend(FFT, AudioletNode);
Process samples
FFT.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
if (input.samples.length == 0) {
return;
}
this.buffer[this.readWriteIndex] = input.samples[0];
output.samples[0] = [this.realBuffer[this.readWriteIndex],
this.imaginaryBuffer[this.readWriteIndex]];
this.readWriteIndex += 1;
if (this.readWriteIndex >= this.bufferSize) {
this.transform();
this.readWriteIndex = 0;
}
};
Precalculate the reverse table. TODO: Split the function out so it can be reused in FFT and IFFT
FFT.prototype.calculateReverseTable = function() {
var limit = 1;
var bit = this.bufferSize >> 1;
while (limit < this.bufferSize) {
for (var i = 0; i < limit; i++) {
this.reverseTable[i + limit] = this.reverseTable[i] + bit;
}
limit = limit << 1;
bit = bit >> 1;
}
};
Calculate the FFT for the saved buffer
FFT.prototype.transform = function() {
for (var i = 0; i < this.bufferSize; i++) {
this.realBuffer[i] = this.buffer[this.reverseTable[i]];
this.imaginaryBuffer[i] = 0;
}
var halfSize = 1;
while (halfSize < this.bufferSize) {
var phaseShiftStepReal = Math.cos(-Math.PI / halfSize);
var phaseShiftStepImag = Math.sin(-Math.PI / halfSize);
var currentPhaseShiftReal = 1;
var currentPhaseShiftImag = 0;
for (var fftStep = 0; fftStep < halfSize; fftStep++) {
var i = fftStep;
while (i < this.bufferSize) {
var off = i + halfSize;
var tr = (currentPhaseShiftReal * this.realBuffer[off]) -
(currentPhaseShiftImag * this.imaginaryBuffer[off]);
var ti = (currentPhaseShiftReal * this.imaginaryBuffer[off]) +
(currentPhaseShiftImag * this.realBuffer[off]);
this.realBuffer[off] = this.realBuffer[i] - tr;
this.imaginaryBuffer[off] = this.imaginaryBuffer[i] - ti;
this.realBuffer[i] += tr;
this.imaginaryBuffer[i] += ti;
i += halfSize << 1;
}
var tmpReal = currentPhaseShiftReal;
currentPhaseShiftReal = (tmpReal * phaseShiftStepReal) -
(currentPhaseShiftImag *
phaseShiftStepImag);
currentPhaseShiftImag = (tmpReal * phaseShiftStepImag) +
(currentPhaseShiftImag *
phaseShiftStepReal);
}
halfSize = halfSize << 1;
}
};
toString
FFT.prototype.toString = function() {
return 'FFT';
};
Inverse Fast Fourier Transform. Code liberally stolen with kind permission of Corben Brook from DSP.js (https://github.com/corbanbrook/dsp.js).
Inputs
Outputs
var IFFT = function(audiolet, bufferSize) {
AudioletNode.call(this, audiolet, 2, 1);
this.linkNumberOfOutputChannels(0, 0);
this.bufferSize = bufferSize;
this.readWriteIndex = 0;
this.buffer = new Float32Array(this.bufferSize);
this.realBuffer = new Float32Array(this.bufferSize);
this.imaginaryBuffer = new Float32Array(this.bufferSize);
this.reverseTable = new Uint32Array(this.bufferSize);
this.calculateReverseTable();
this.reverseReal = new Float32Array(this.bufferSize);
this.reverseImaginary = new Float32Array(this.bufferSize);
};
extend(IFFT, AudioletNode);
Process samples
IFFT.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
if (!input.samples.length) {
return;
}
var values = input.samples[0];
this.realBuffer[this.readWriteIndex] = values[0];
this.imaginaryBuffer[this.readWriteIndex] = values[1];
output.samples[0] = this.buffer[this.readWriteIndex];
this.readWriteIndex += 1;
if (this.readWriteIndex >= this.bufferSize) {
this.transform();
this.readWriteIndex = 0;
}
};
Precalculate the reverse table. TODO: Split the function out so it can be reused in FFT and IFFT
IFFT.prototype.calculateReverseTable = function() {
var limit = 1;
var bit = this.bufferSize >> 1;
while (limit < this.bufferSize) {
for (var i = 0; i < limit; i++) {
this.reverseTable[i + limit] = this.reverseTable[i] + bit;
}
limit = limit << 1;
bit = bit >> 1;
}
};
Calculate the inverse FFT for the saved real and imaginary buffers
IFFT.prototype.transform = function() {
var halfSize = 1;
for (var i = 0; i < this.bufferSize; i++) {
this.imaginaryBuffer[i] *= -1;
}
for (var i = 0; i < this.bufferSize; i++) {
this.reverseReal[i] = this.realBuffer[this.reverseTable[i]];
this.reverseImaginary[i] = this.imaginaryBuffer[this.reverseTable[i]];
}
this.realBuffer.set(this.reverseReal);
this.imaginaryBuffer.set(this.reverseImaginary);
while (halfSize < this.bufferSize) {
var phaseShiftStepReal = Math.cos(-Math.PI / halfSize);
var phaseShiftStepImag = Math.sin(-Math.PI / halfSize);
var currentPhaseShiftReal = 1;
var currentPhaseShiftImag = 0;
for (var fftStep = 0; fftStep < halfSize; fftStep++) {
i = fftStep;
while (i < this.bufferSize) {
var off = i + halfSize;
var tr = (currentPhaseShiftReal * this.realBuffer[off]) -
(currentPhaseShiftImag * this.imaginaryBuffer[off]);
var ti = (currentPhaseShiftReal * this.imaginaryBuffer[off]) +
(currentPhaseShiftImag * this.realBuffer[off]);
this.realBuffer[off] = this.realBuffer[i] - tr;
this.imaginaryBuffer[off] = this.imaginaryBuffer[i] - ti;
this.realBuffer[i] += tr;
this.imaginaryBuffer[i] += ti;
i += halfSize << 1;
}
var tmpReal = currentPhaseShiftReal;
currentPhaseShiftReal = (tmpReal * phaseShiftStepReal) -
(currentPhaseShiftImag *
phaseShiftStepImag);
currentPhaseShiftImag = (tmpReal * phaseShiftStepImag) +
(currentPhaseShiftImag *
phaseShiftStepReal);
}
halfSize = halfSize << 1;
}
for (i = 0; i < this.bufferSize; i++) {
this.buffer[i] = this.realBuffer[i] / this.bufferSize;
}
};
toString
IFFT.prototype.toString = function() {
return 'IFFT';
};
Mathematical operator nodes
Add values
Inputs
Outputs
Parameters
var Add = function(audiolet, value) {
AudioletNode.call(this, audiolet, 2, 1);
this.linkNumberOfOutputChannels(0, 0);
this.value = new AudioletParameter(this, 1, value || 0);
};
extend(Add, AudioletNode);
Process samples
Add.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var value = this.value.getValue();
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
output.samples[i] = input.samples[i] + value;
}
};
toString
Add.prototype.toString = function() {
return 'Add';
};
Subtract values
Inputs
Outputs
Parameters
var Subtract = function(audiolet, value) {
AudioletNode.call(this, audiolet, 2, 1);
this.linkNumberOfOutputChannels(0, 0);
this.value = new AudioletParameter(this, 1, value || 0);
};
extend(Subtract, AudioletNode);
Process samples
Subtract.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var value = this.value.getValue();
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
output.samples[i] = input.samples[i] - value;
}
};
toString
Subtract.prototype.toString = function() {
return 'Subtract';
};
Multiply values
Inputs
Outputs
Parameters
var Multiply = function(audiolet, value) {
AudioletNode.call(this, audiolet, 2, 1);
this.linkNumberOfOutputChannels(0, 0);
this.value = new AudioletParameter(this, 1, value || 1);
};
extend(Multiply, AudioletNode);
Process samples
Multiply.prototype.generate = function() {
var value = this.value.getValue();
var input = this.inputs[0];
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
this.outputs[0].samples[i] = input.samples[i] * value;
}
};
toString
Multiply.prototype.toString = function() {
return 'Multiply';
};
Divide values
Inputs
Outputs
Parameters
var Divide = function(audiolet, value) {
AudioletNode.call(this, audiolet, 2, 1);
this.linkNumberOfOutputChannels(0, 0);
this.value = new AudioletParameter(this, 1, value || 1);
};
extend(Divide, AudioletNode);
Process samples
Divide.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var value = this.value.getValue();
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
output.samples[i] = input.samples[i] / value;
}
};
toString
Divide.prototype.toString = function() {
return 'Divide';
};
Modulo values
Inputs
Outputs
Parameters
var Modulo = function(audiolet, value) {
AudioletNode.call(this, audiolet, 2, 1);
this.linkNumberOfOutputChannels(0, 0);
this.value = new AudioletParameter(this, 1, value || 1);
};
extend(Modulo, AudioletNode);
Process samples
Modulo.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var value = this.value.getValue();
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
output.samples[i] = input.samples[i] % value;
}
};
toString
Modulo.prototype.toString = function() {
return 'Modulo';
};
Reciprocal (1/x) of values
Inputs
Outputs
var Reciprocal = function(audiolet) {
AudioletNode.call(this, audiolet, 1, 1);
this.linkNumberOfOutputChannels(0, 0);
};
extend(Reciprocal, AudioletNode);
Process samples
Reciprocal.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
output.samples[i] = 1 / input.samples[i];
}
};
toString
Reciprocal.prototype.toString = function() {
return 'Reciprocal';
};
Multiply and add values
Inputs
Outputs
Parameters
var MulAdd = function(audiolet, mul, add) {
AudioletNode.call(this, audiolet, 3, 1);
this.linkNumberOfOutputChannels(0, 0);
this.mul = new AudioletParameter(this, 1, mul || 1);
this.add = new AudioletParameter(this, 2, add || 0);
};
extend(MulAdd, AudioletNode);
Process samples
MulAdd.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var mul = this.mul.getValue();
var add = this.add.getValue();
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
output.samples[i] = input.samples[i] * mul + add;
}
};
toString
MulAdd.prototype.toString = function() {
return 'Multiplier/Adder';
};
Hyperbolic tangent of values. Works nicely as a distortion function.
Inputs
Outputs
var Tanh = function(audiolet) {
AudioletNode.call(this, audiolet, 1, 1);
this.linkNumberOfOutputChannels(0, 0);
};
extend(Tanh, AudioletNode);
Process samples
Tanh.prototype.generate = function() {
var input = this.inputs[0];
var output = this.outputs[0];
var numberOfChannels = input.samples.length;
for (var i = 0; i < numberOfChannels; i++) {
var value = input.samples[i];
output.samples[i] = (Math.exp(value) - Math.exp(-value)) /
(Math.exp(value) + Math.exp(-value));
}
};
toString
Tanh.prototype.toString = function() {
return ('Tanh');
};
Musical scales
Representation of a generic musical scale. Can be subclassed to produce specific scales.
var Scale = function(degrees, tuning) {
this.degrees = degrees;
this.tuning = tuning || new EqualTemperamentTuning(12);
};
Get the frequency of a note in the scale.
Scale.prototype.getFrequency = function(degree, rootFrequency, octave) {
var frequency = rootFrequency;
octave += Math.floor(degree / this.degrees.length);
degree %= this.degrees.length;
frequency *= Math.pow(this.tuning.octaveRatio, octave);
frequency *= this.tuning.ratios[this.degrees[degree]];
return frequency;
};
Major scale.
var MajorScale = function() {
Scale.call(this, [0, 2, 4, 5, 7, 9, 11]);
};
extend(MajorScale, Scale);
Minor scale.
var MinorScale = function() {
Scale.call(this, [0, 2, 3, 5, 7, 8, 10]);
};
extend(MinorScale, Scale);
Musical tunings
Representation of a generic musical tuning. Can be subclassed to produce specific tunings.
var Tuning = function(semitones, octaveRatio) {
this.semitones = semitones;
this.octaveRatio = octaveRatio || 2;
this.ratios = [];
var tuningLength = this.semitones.length;
for (var i = 0; i < tuningLength; i++) {
this.ratios.push(Math.pow(2, this.semitones[i] / tuningLength));
}
};
Equal temperament tuning.
var EqualTemperamentTuning = function(pitchesPerOctave) {
var semitones = [];
for (var i = 0; i < pitchesPerOctave; i++) {
semitones.push(i);
}
Tuning.call(this, semitones, 2);
};
extend(EqualTemperamentTuning, Tuning);
Schedulable patterns of values
A generic pattern. Patterns are simple classes which return the next value in a sequence when the next function is called. Patterns can be embedded inside other patterns to produce complex sequences of values. When a pattern is finished its next function returns null.
var Pattern = function() {
};
Default next function.
Pattern.prototype.next = function() {
return null;
};
Return the current value of an item contained in a pattern.
Pattern.prototype.valueOf = function(item) {
if (item instanceof Pattern) {
return (item.next());
}
else {
return (item);
}
};
Default reset function.
Pattern.prototype.reset = function() {
};
Iterate through a list of values.
var PSequence = function(list, repeats, offset) {
Pattern.call(this);
this.list = list;
this.repeats = repeats || 1;
this.position = 0;
this.offset = offset || 0;
};
extend(PSequence, Pattern);
Generate the next value in the pattern.
PSequence.prototype.next = function() {
var returnValue;
if (this.position < this.repeats * this.list.length) {
var index = (this.position + this.offset) % this.list.length;
var item = this.list[index];
var value = this.valueOf(item);
if (value != null) {
if (!(item instanceof Pattern)) {
this.position += 1;
}
returnValue = value;
}
else {
if (item instanceof Pattern) {
item.reset();
}
this.position += 1;
returnValue = this.next();
}
}
else {
returnValue = null;
}
return (returnValue);
};
Reset the pattern
PSequence.prototype.reset = function() {
this.position = 0;
for (var i = 0; i < this.list.length; i++) {
var item = this.list[i];
if (item instanceof Pattern) {
item.reset();
}
}
};
Supercollider alias
var Pseq = PSequence;
Iterate through a list of values.
var PSeries = function(list, repeats, offset) {
Pattern.call(this);
this.list = list;
this.repeats = repeats || 1;
this.position = 0;
this.offset = offset || 0;
};
extend(PSeries, Pattern);
Generate the next value in the pattern.
PSeries.prototype.next = function() {
var returnValue;
if (this.position < this.repeats) {
var index = (this.position + this.offset) % this.list.length;
var item = this.list[index];
var value = this.valueOf(item);
if (value != null) {
if (!(item instanceof Pattern)) {
this.position += 1;
}
returnValue = value;
}
else {
if (item instanceof Pattern) {
item.reset();
}
this.position += 1;
returnValue = this.next();
}
}
else {
returnValue = null;
}
return (returnValue);
};
Reset the pattern
PSeries.prototype.reset = function() {
this.position = 0;
for (var i = 0; i < this.list.length; i++) {
var item = this.list[i];
if (item instanceof Pattern) {
item.reset();
}
}
};
Supercollider alias
var Pser = PSeries;
Reorder an array, then iterate through it's values.
var PShuffle = function(list, repeats) {
Pattern.call(this);
this.list = [];
// Shuffle values into new list
while (list.length) {
var index = Math.floor(Math.random() * list.length);
var value = list.splice(index, 1);
this.list.push(value);
}
this.repeats = repeats;
this.position = 0;
};
extend(PShuffle, Pattern);
Generate the next value in the pattern.
PShuffle.prototype.next = function() {
var returnValue;
if (this.position < this.repeats * this.list.length) {
var index = (this.position + this.offset) % this.list.length;
var item = this.list[index];
var value = this.valueOf(item);
if (value != null) {
if (!(item instanceof Pattern)) {
this.position += 1;
}
returnValue = value;
}
else {
if (item instanceof Pattern) {
item.reset();
}
this.position += 1;
returnValue = this.next();
}
}
else {
returnValue = null;
}
return (returnValue);
};
Supercollider alias
var Pshuffle = PShuffle;
Arithmetic sequence. Adds a value to a running total on each next call.
var PArithmetic = function(start, step, repeats) {
Pattern.call(this);
this.start = start;
this.value = start;
this.step = step;
this.repeats = repeats;
this.position = 0;
};
extend(PArithmetic, Pattern);
Generate the next value in the pattern.
PArithmetic.prototype.next = function() {
var returnValue;
if (this.position == 0) {
returnValue = this.value;
this.position += 1;
}
else if (this.position < this.repeats) {
var step = this.valueOf(this.step);
if (step != null) {
this.value += step;
returnValue = this.value;
this.position += 1;
}
else {
returnValue = null;
}
}
else {
returnValue = null;
}
return (returnValue);
};
Reset the pattern
PArithmetic.prototype.reset = function() {
this.value = this.start;
this.position = 0;
if (this.step instanceof Pattern) {
this.step.reset();
}
};
Supercollider alias
var Pseries = PArithmetic;
Geometric sequence. Multiplies a running total by a value on each next call.
var PGeometric = function(start, step, repeats) {
Pattern.call(this);
this.start = start;
this.value = start;
this.step = step;
this.repeats = repeats;
this.position = 0;
};
extend(PGeometric, Pattern);
Generate the next value in the pattern.
PGeometric.prototype.next = function() {
var returnValue;
if (this.position == 0) {
returnValue = this.value;
this.position += 1;
}
else if (this.position < this.repeats) {
var step = this.valueOf(this.step);
if (step != null) {
this.value *= step;
returnValue = this.value;
this.position += 1;
}
else {
returnValue = null;
}
}
else {
returnValue = null;
}
return (returnValue);
};
Reset the pattern
PGeometric.prototype.reset = function() {
this.value = this.start;
this.position = 0;
if (this.step instanceof Pattern) {
this.step.reset();
}
};
Supercollider alias
var Pgeom = PGeometric;
Choose a random value from an array.
var PChoose = function(list, repeats) {
Pattern.call(this);
this.list = list;
this.repeats = repeats || 1;
this.position = 0;
};
extend(PChoose, Pattern);
Generate the next value in the pattern.
PChoose.prototype.next = function() {
var returnValue;
if (this.position < this.repeats) {
var index = Math.floor(Math.random() * this.list.length);
var item = this.list[index];
var value = this.valueOf(item);
if (value != null) {
if (!(item instanceof Pattern)) {
this.position += 1;
}
returnValue = value;
}
else {
if (item instanceof Pattern) {
item.reset();
}
this.position += 1;
returnValue = this.next();
}
}
else {
returnValue = null;
}
return (returnValue);
};
Reset the pattern
PChoose.prototype.reset = function() {
this.position = 0;
for (var i = 0; i < this.list.length; i++) {
var item = this.list[i];
if (item instanceof Pattern) {
item.reset();
}
}
};
Supercollider alias
var Prand = PChoose;
Sequence of random numbers.
var PRandom = function(low, high, repeats) {
Pattern.call(this);
this.low = low;
this.high = high;
this.repeats = repeats;
this.position = 0;
};
extend(PRandom, Pattern);
Generate the next value in the pattern.
PRandom.prototype.next = function() {
var returnValue;
if (this.position < this.repeats) {
var low = this.valueOf(this.low);
var high = this.valueOf(this.high);
if (low != null && high != null) {
returnValue = low + Math.random() * (high - low);
this.position += 1;
}
else {
returnValue = null;
}
}
else {
returnValue = null;
}
return (returnValue);
};
Reset the pattern
PRandom.prototype.reset = function() {
this.position = 0;
};
Supercollider alias
var Pwhite = PRandom;
Proxy pattern. Holds a pattern which can safely be replaced by a different pattern while it is running.
var PProxy = function(pattern) {
Pattern.call(this);
if (pattern) {
this.pattern = pattern;
}
};
extend(PProxy, Pattern);
Generate the next value in the pattern.
PProxy.prototype.next = function() {
var returnValue;
if (this.pattern) {
var returnValue = this.pattern.next();
}
else {
returnValue = null;
}
return returnValue;
};
Alias
var Pp = PProxy;