diff options
Diffstat (limited to 'public/js')
| -rw-r--r-- | public/js/vendor/Audiolet.js | 7281 | ||||
| -rw-r--r-- | public/js/vendor/Audiolet.min.js | 252 |
2 files changed, 7533 insertions, 0 deletions
diff --git a/public/js/vendor/Audiolet.js b/public/js/vendor/Audiolet.js new file mode 100644 index 0000000..b7be12e --- /dev/null +++ b/public/js/vendor/Audiolet.js @@ -0,0 +1,7281 @@ +/** + * A variable size multi-channel audio buffer. + * + * @constructor + * @param {Number} numberOfChannels The initial number of channels. + * @param {Number} length The length in samples of each channel. + */ +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 + * + * @param {Number} channel The index of the channel. + * @return {Float32Array} The requested channel. + */ +AudioletBuffer.prototype.getChannelData = function(channel) { + return (this.channels[channel]); +}; + +/** + * Set the data in the buffer by copying data from a second buffer + * + * @param {AudioletBuffer} buffer The buffer to copy data from. + */ +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 + * + * @param {AudioletBuffer} buffer The buffer to copy data from. + * @param {Number} length The number of samples to copy. + * @param {Number} [inputOffset=0] An offset to read data from. + * @param {Number} [outputOffset=0] An offset to write data to. + */ +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 + /*! + var channel1 = this.getChannelData(i).subarray(outputOffset, + outputOffset + + length); + var channel2 = buffer.getChannelData(i).subarray(inputOffset, + inputOffset + + length); + */ + channel1.set(channel2); + } +}; + +/** + * Add the data from a second buffer to the data in this buffer + * + * @param {AudioletBuffer} buffer The buffer to add data from. + */ +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 + * + * @param {AudioletBuffer} buffer The buffer to add data from. + * @param {Number} length The number of samples to add. + * @param {Number} [inputOffset=0] An offset to read data from. + * @param {Number} [outputOffset=0] An offset to write data to. + */ +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. + * + * @param {Number} numberOfChannel The new number of channels. + * @param {Number} length The new length of each channel. + * @param {Boolean} [lazy=false] If true a resized buffer may not be empty. + * @param {Number} [offset=0] An offset to resize from. + */ +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 + * + * @param {AudioletBuffer} buffer The buffer to append to this 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. + * + * @param {AudioletBuffer} buffer The buffer to move data into. + */ +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. + * + * @param {AudioletBuffer} buffer The buffer to prepend to this 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. + * + * @param {AudioletBuffer} buffer The buffer to move data into. + */ +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. + * + * @return {Float32Array} The combined array of data. + */ +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. + * + * @return {Float32Array} The interleaved array of data. + */ +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. + * + * @return {AudioletBuffer} The 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 + * + * @param {String} path The path to the file. + * @param {Boolean} [async=true] Whether to load the file asynchronously. + * @param {Function} [callback] Function called if the file loaded sucessfully. + */ +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(); +}; + +/** + * 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. + * + * @constructor + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} numberOfInputs The number of inputs. + * @param {Number} numberOfOutputs The number of outputs. + */ +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 + * + * @param {AudioletNode|AudioletGroup} node The node to connect to. + * @param {Number} [output=0] The index of the output to connect from. + * @param {Number} [input=0] The index of the input to connect to. + */ +AudioletGroup.prototype.connect = function(node, output, input) { + this.outputs[output || 0].connect(node, 0, input); +}; + +/** + * Disconnect the group from another node or group + * + * @param {AudioletNode|AudioletGroup} node The node to disconnect from. + * @param {Number} [output=0] The index of the output to disconnect. + * @param {Number} [input=0] The index of the input to disconnect. + */ +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(); + } +}; + +/*! + * @depends AudioletGroup.js + */ + +/** + * Group containing all of the components for the Audiolet output chain. The + * chain consists of: + * + * Input => Scheduler => UpMixer => Output + * + * **Inputs** + * + * - Audio + * + * @constructor + * @extends AudioletGroup + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [sampleRate=44100] The sample rate to run at. + * @param {Number} [numberOfChannels=2] The number of output channels. + * @param {Number} [bufferSize=8192] A fixed buffer size to use. + */ +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 + * + * @return {String} String representation. + */ +AudioletDestination.prototype.toString = function() { + return 'Destination'; +}; + +/** + * 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. + * + * @constructor + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} numberOfInputs The number of inputs. + * @param {Number} numberOfOutputs The number of outputs. + * @param {Function} [generate] A replacement for the generate function. + */ +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. + * + * @param {AudioletNode|AudioletGroup} node The node to connect to. + * @param {Number} [output=0] The index of the output to connect from. + * @param {Number} [input=0] The index of the input to connect to. + */ +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 + * + * @param {AudioletNode|AudioletGroup} node The node to disconnect from. + * @param {Number} [output=0] The index of the output to disconnect. + * @param {Number} [input=0] The index of the input to disconnect. + */ +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. + * + * @param {Number} output The index of the output. + * @param {Number} numberOfChannels The 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. + * + * @param {Number} output The index of the output. + * @param {Number} input The index of 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. + * + * @param {AudioletNode[]} nodes Array to add nodes to. + */ +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); + } + } +}; + + +/*! + * @depends AudioletNode.js + */ + +/** + * Audio output device. Uses sink.js to output to a range of APIs. + * + * @constructor + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [sampleRate=44100] The sample rate to run at. + * @param {Number} [numberOfChannels=2] The number of output channels. + * @param {Number} [bufferSize=8192] A fixed buffer size to use. + */ +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. +* +* @param {Float32Array} buffer Buffer to write data to. +* @param {Number} numberOfChannels Number of channels in the buffer. +*/ +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 + * + * @return {Number} Output position in samples. + */ +AudioletDevice.prototype.getPlaybackTime = function() { + return this.sink.getPlaybackTime(); +}; + +/** + * Get the current write position + * + * @return {Number} Write position in samples. + */ +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 + * + * @return {String} String representation. + */ +AudioletDevice.prototype.toString = function() { + return 'Audio Output Device'; +}; + +/** + * Class representing a single input of an AudioletNode + * + * @constructor + * @param {AudioletNode} node The node which the input belongs to. + * @param {Number} index The index of the input. + */ +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 + * + * @param {AudioletOutput} output The output to connect to. + */ +AudioletInput.prototype.connect = function(output) { + this.connectedFrom.push(output); +}; + +/** + * Disconnect the input from an output + * + * @param {AudioletOutput} output The output to disconnect from. + */ +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 + * + * @return {String} String representation. + */ +AudioletInput.prototype.toString = function() { + return this.node.toString() + 'Input #' + this.index; +}; + + +/** + * The base audiolet object. Contains an output node which pulls data from + * connected nodes. + * + * @constructor + * @param {Number} [sampleRate=44100] The sample rate to run at. + * @param {Number} [numberOfChannels=2] The number of output channels. + * @param {Number} [bufferSize] Block size. If undefined uses a sane default. + */ +var Audiolet = function(sampleRate, numberOfChannels, bufferSize) { + this.output = new AudioletDestination(this, sampleRate, + numberOfChannels, bufferSize); +}; + + +/** + * Class representing a single output of an AudioletNode + * + * @constructor + * @param {AudioletNode} node The node which the input belongs to. + * @param {Number} index The index of the input. + */ +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 + * + * @param {AudioletInput} input The input to connect to. + */ +AudioletOutput.prototype.connect = function(input) { + this.connectedTo.push(input); +}; + +/** + * Disconnect the output from an input + * + * @param {AudioletInput} input The input to disconnect from. + */ +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. + * + * @param {AudioletInput} input The input to link to. + */ +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. + * + * @return {Number} The number of output channels. + */ +AudioletOutput.prototype.getNumberOfChannels = function() { + if (this.linkedInput && this.linkedInput.connectedFrom.length) { + return (this.linkedInput.samples.length); + } + return (this.numberOfChannels); +}; + +/** + * toString + * + * @return {String} String representation. + */ +AudioletOutput.prototype.toString = function() { + return this.node.toString() + 'Output #' + this.index + ' - '; +}; + + +/** + * 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. + * + * @constructor + * @param {AudioletNode} node The node which the parameter is associated with. + * @param {Number} [inputIndex] The index of the AudioletInput to link to. + * @param {Number} [value=0] The initial static value to store. + */ +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. + * + * @return {Boolean} True if the static value should be used. + */ +AudioletParameter.prototype.isStatic = function() { + return (this.input.samples.length == 0); +}; + +/** + * Check whether the dynamic values should be used. + * + * @return {Boolean} True if the dynamic values should be used. + */ +AudioletParameter.prototype.isDynamic = function() { + return (this.input.samples.length > 0); +}; + +/** + * Set the stored static value + * + * @param {Number} value The value to store. + */ +AudioletParameter.prototype.setValue = function(value) { + this.value = value; +}; + +/** + * Get the stored static value + * + * @return {Number} 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; + } +}; + +/* + * A method for extending a javascript pseudo-class + * Taken from + * http://peter.michaux.ca/articles/class-based-inheritance-in-javascript + * + * @param {Object} subclass The class to extend. + * @param {Object} superclass The class to be extended. + */ +function extend(subclass, superclass) { + function Dummy() {} + Dummy.prototype = superclass.prototype; + subclass.prototype = new Dummy(); + subclass.prototype.constructor = subclass; +} + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * 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** + * + * - Parameter input + * + * **Outputs** + * + * - Parameter value + * + * **Parameters** + * + * - parameter The contained parameter. Linked to input 0. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} value The initial static value of the parameter. + */ +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 + * + * @return {String} String representation. + */ +ParameterNode.prototype.toString = function() { + return 'Parameter Node'; +}; + +/*! + * @depends AudioletNode.js + */ + +/** + * 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. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} numberOfInputs The number of inputs. + * @param {Number} numberOfOutputs The number of outputs. + */ +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 + * + * @return {String} String representation. + */ +PassThroughNode.prototype.toString = function() { + return 'Pass Through Node'; +}; + +/** + * Priority Queue based on python heapq module + * http://svn.python.org/view/python/branches/release27-maint/Lib/heapq.py + * + * @constructor + * @param {Object[]} [array] Initial array of values to store. + * @param {Function} [compare] Compare function. + */ +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 + * + * @param {Object} item The item to add. + */ +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. + * + * @return {Object} The top item. + */ +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. + * + * @return {Object} The top item. + */ +PriorityQueue.prototype.peek = function() { + return (this.heap[0]); +}; + +/** + * Check whether the queue is empty. + * + * @return {Boolean} True if the queue is empty. + */ +PriorityQueue.prototype.isEmpty = function() { + return (this.heap.length == 0); +}; + + +/** + * Sift item down the queue. + * + * @param {Number} startPosition Queue start position. + * @param {Number} position Item position. + */ +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. + * + * @param {Number} position Item position. + */ +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. + * + * @param {Number} a First item. + * @param {Number} b Second item. + * @return {Boolean} True if a < b. + */ +PriorityQueue.prototype.compare = function(a, b) { + return (a < b); +}; + +/*! + * @depends PassThroughNode.js + */ + +/** + * 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** + * + * - Audio + * + * **Outputs** + * + * - Audio + * + * @constructor + * @extends PassThroughNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [bpm=120] Initial tempo. + */ +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. + * + * @param {Number} bpm The tempo in beats per minute. + */ +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 + * + * @param {Number} beats How many beats in the future to schedule the event. + * @param {Function} callback A function called when it is time for the event. + * @return {Object} The event object. + */ +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 + * + * @param {Number} beat The beat at which the event should take place. + * @param {Function} callback A function called when it is time for the event. + * @return {Object} The event object. + */ +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. + * + * @param {Pattern[]} patterns An array of patterns to play. + * @param {Pattern|Number} durationPattern The number of beats between events. + * @param {Function} callback Function called with the generated pattern values. + * @return {Object} The event object. + */ +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. + * + * @param {Number} beat The beat at which the event should take place. + * @param {Pattern[]} patterns An array of patterns to play. + * @param {Pattern|Number} durationPattern The number of beats between events. + * @param {Function} callback Function called with the generated pattern values. + * @return {Object} The event object. + */ +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 + * + * @param {Object} event The event to remove. + */ +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. + * + * @param {Object} event The event to remove. + */ +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. + * + * @param {Object} event The event to process. + */ +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 + * + * @return {String} String representation. + */ +Scheduler.prototype.toString = function() { + return 'Scheduler'; +}; + +/** + * 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 + }); + } +} + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * A generic envelope consisting of linear transitions of varying duration + * between a series of values. + * + * **Inputs** + * + * - Gate + * + * **Outputs** + * + * - Envelope + * + * **Parameters** + * + * - gate Gate controlling the envelope. Values should be 0 (off) or 1 (on). + * Linked to input 0. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [gate=1] Initial gate value. + * @param {Number[]} levels An array (of length n) of values to move between. + * @param {Number[]} times An array of n-1 durations - one for each transition. + * @param {Number} [releaseStage] Sustain at this stage until the the gate is 0. + * @param {Function} [onComplete] Function called as the envelope finishes. + */ +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 = []; + for (var i=0; i<levels.length; i++) { + this.levels.push(new AudioletParameter(this, null, levels[i])); + } + + this.times = []; + for (var i=0; i<times.length; i++) { + this.times.push(new AudioletParameter(this, null, times[i])); + } + + this.releaseStage = releaseStage; + this.onComplete = onComplete; + + this.stage = null; + this.time = null; + this.changeTime = null; + + this.level = this.levels[0].getValue(); + 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].getValue(); + 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 + * + * @param {Number} stage The index of the current stage. + * @param {Number} level The current level. + * @return {Number} The change in level. + */ +Envelope.prototype.calculateDelta = function(stage, level) { + var delta = this.levels[stage + 1].getValue() - level; + var stageTime = this.times[stage].getValue() * + this.audiolet.device.sampleRate; + return (delta / stageTime); +}; + +/** + * Calculate the time in samples at which the next stage starts + * + * @param {Number} stage The index of the current stage. + * @param {Number} time The current time. + * @return {Number} The change time. + */ +Envelope.prototype.calculateChangeTime = function(stage, time) { + var stageTime = this.times[stage].getValue() * + this.audiolet.device.sampleRate; + return (time + stageTime); +}; + +/** + * toString + * + * @return {String} String representation. + */ +Envelope.prototype.toString = function() { + return 'Envelope'; +}; + +/*! + * @depends Envelope.js + */ + +/** + * Linear attack-decay-sustain-release envelope + * + * **Inputs** + * + * - Gate + * + * **Outputs** + * + * - Envelope + * + * **Parameters** + * + * - gate The gate turning the envelope on and off. Value changes from 0 -> 1 + * trigger the envelope. Value changes from 1 -> 0 make the envelope move to + * its release stage. Linked to input 0. + * + * @constructor + * @extends Envelope + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} gate The initial gate value. + * @param {Number} attack The attack time in seconds. + * @param {Number} decay The decay time in seconds. + * @param {Number} sustain The sustain level (between 0 and 1). + * @param {Number} release The release time in seconds. + * @param {Function} onComplete A function called after the release stage. + */ +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); + + this.attack = this.times[0]; + this.decay = this.times[1]; + this.sustain = this.levels[2]; + this.release = this.levels[2]; +}; +extend(ADSREnvelope, Envelope); + +/** + * toString + * + * @return {String} String representation. + */ +ADSREnvelope.prototype.toString = function() { + return 'ADSR Envelope'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * 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** + * + * - Audio + * - Filter frequency + * + * **Outputs** + * + * - Filtered audio + * + * **Parameters** + * + * - frequency The filter frequency. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} frequency The initial frequency. + */ +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. + * + * @param {Number} frequency The filter frequency. + */ +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 + * + * @return {String} String representation. + */ +BiquadFilter.prototype.toString = function() { + return 'Biquad Filter'; +}; + +/*! + * @depends BiquadFilter.js + */ + +/** + * All-pass filter + * + * **Inputs** + * + * - Audio + * - Filter frequency + * + * **Outputs** + * + * - Filtered audio + * + * **Parameters** + * + * - frequency The filter frequency. Linked to input 1. + * + * @constructor + * @extends BiquadFilter + * + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} frequency The initial frequency. + */ +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 + * + * @param {Number} frequency The filter frequency. + */ +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 + * + * @return {String} String representation. + */ +AllPassFilter.prototype.toString = function() { + return 'All Pass Filter'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Amplitude envelope follower + * + * **Inputs** + * + * - Audio + * - Attack time + * - Release time + * + * **Outputs** + * + * - Amplitude envelope + * + * **Parameters** + * + * - attack The attack time of the envelope follower. Linked to input 1. + * - release The release time of the envelope follower. Linked to input 2. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [attack=0.01] The initial attack time in seconds. + * @param {Number} [release=0.01] The initial release time in seconds. + */ +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 + * + * @return {String} String representation. + */ +Amplitude.prototype.toString = function() { + return ('Amplitude'); +}; + +/*! + * @depends ../core/PassThroughNode.js + */ + +/** + * Detect potentially hazardous values in the audio stream. Looks for + * undefineds, nulls, NaNs and Infinities. + * + * **Inputs** + * + * - Audio + * + * **Outputs** + * + * - Audio + * + * @constructor + * @extends PassThroughNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Function} [callback] Function called if a bad value is detected. + */ +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. + * + * @param {Number|Object|'undefined'} value The value detected. + * @param {Number} channel The index of the channel the value was found in. + * @param {Number} index The sample index the value was found at. + */ +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 + * + * @return {String} String representation. + */ +BadValueDetector.prototype.toString = function() { + return 'Bad Value Detector'; +}; + +/*! + * @depends BiquadFilter.js + */ + +/** + * Band-pass filter + * + * **Inputs** + * + * - Audio + * - Filter frequency + * + * **Outputs** + * + * - Filtered audio + * + * **Parameters** + * + * - frequency The filter frequency. Linked to input 1. + * + * @constructor + * @extends BiquadFilter + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} frequency The initial frequency. + */ +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 + * + * @param {Number} frequency The filter frequency. + */ +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 + * + * @return {String} String representation. + */ +BandPassFilter.prototype.toString = function() { + return 'Band Pass Filter'; +}; + +/*! + * @depends BiquadFilter.js + */ + +/** + * Band-reject filter + * + * **Inputs** + * + * - Audio + * - Filter frequency + * + * **Outputs** + * + * - Filtered audio + * + * **Parameters** + * + * - frequency The filter frequency. Linked to input 1. + * + * @constructor + * @extends BiquadFilter + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} frequency The initial frequency. + */ +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 + * + * @param {Number} frequency The filter frequency. + */ +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 + * + * @return {String} String representation. + */ +BandRejectFilter.prototype.toString = function() { + return 'Band Reject Filter'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Reduce the bitrate of incoming audio + * + * **Inputs** + * + * - Audio 1 + * - Number of bits + * + * **Outputs** + * + * - Bit Crushed Audio + * + * **Parameters** + * + * - bits The number of bit to reduce to. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} bits The initial number of bits. + */ +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 + * + * @return {String} String representation. + */ +BitCrusher.prototype.toString = function() { + return 'BitCrusher'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Play the contents of an audio buffer + * + * **Inputs** + * + * - Playback rate + * - Restart trigger + * - Start position + * - Loop on/off + * + * **Outputs** + * + * - Audio + * + * **Parameters** + * + * - playbackRate The rate that the buffer should play at. Value of 1 plays at + * the regular rate. Values > 1 are pitched up. Values < 1 are pitched down. + * Linked to input 0. + * - restartTrigger Changes of value from 0 -> 1 restart the playback from the + * start position. Linked to input 1. + * - startPosition The position at which playback should begin. Values between + * 0 (the beginning of the buffer) and 1 (the end of the buffer). Linked to + * input 2. + * - loop Whether the buffer should loop when it reaches the end. Linked to + * input 3 + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {AudioletBuffer} buffer The buffer to play. + * @param {Number} [playbackRate=1] The initial playback rate. + * @param {Number} [startPosition=0] The initial start position. + * @param {Number} [loop=0] Initial value for whether to loop. + * @param {Function} [onComplete] Called when the buffer has finished playing. + */ +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 + * + * @return {String} String representation. + */ +BufferPlayer.prototype.toString = function() { + return ('Buffer player'); +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Undamped comb filter + * + * **Inputs** + * + * - Audio + * - Delay Time + * - Decay Time + * + * **Outputs** + * + * - Filtered audio + * + * **Parameters** + * + * - delayTime The delay time in seconds. Linked to input 1. + * - decayTime Time for the echoes to decay by 60dB. Linked to input 0. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} maximumDelayTime The largest allowable delay time. + * @param {Number} delayTime The initial delay time. + * @param {Number} decayTime The initial decay time. + */ +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 + * + * @return {String} String representation. + */ +CombFilter.prototype.toString = function() { + return 'Comb Filter'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Sine wave oscillator + * + * **Inputs** + * + * - Frequency + * + * **Outputs** + * + * - Sine wave + * + * **Parameters** + * + * - frequency The frequency of the oscillator. Linked to input 0. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [frequency=440] Initial frequency. + */ +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 + * + * @return {String} String representation. + */ +Sine.prototype.toString = function() { + return 'Sine'; +}; + + +/*! + * @depends ../core/AudioletNode.js + * @depends Sine.js + */ + +/** + * Equal-power cross-fade between two signals + * + * **Inputs** + * + * - Audio 1 + * - Audio 2 + * - Fade Position + * + * **Outputs** + * + * - Mixed audio + * + * **Parameters** + * + * - position The fade position. Values between 0 (Audio 1 only) and 1 (Audio + * 2 only). Linked to input 2. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [position=0.5] The initial fade position. + */ +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 + * + * @return {String} String representation. + */ +CrossFade.prototype.toString = function() { + return 'Cross Fader'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Damped comb filter + * + * **Inputs** + * + * - Audio + * - Delay Time + * - Decay Time + * - Damping + * + * **Outputs** + * + * - Filtered audio + * + * **Parameters** + * + * - delayTime The delay time in seconds. Linked to input 1. + * - decayTime Time for the echoes to decay by 60dB. Linked to input 2. + * - damping The amount of high-frequency damping of echoes. Linked to input 3. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} maximumDelayTime The largest allowable delay time. + * @param {Number} delayTime The initial delay time. + * @param {Number} decayTime The initial decay time. + * @param {Number} damping The initial amount of damping. + */ +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 + * + * @return {String} String representation. + */ +DampedCombFilter.prototype.toString = function() { + return 'Damped Comb Filter'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Filter for leaking DC offset. Maths is taken from + * https://ccrma.stanford.edu/~jos/filters/DC_Blocker.html + * + * **Inputs** + * + * - Audio + * - Filter coefficient + * + * **Outputs** + * + * - Filtered audio + * + * **Parameters** + * + * - coefficient The filter coefficient. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [coefficient=0.995] The initial coefficient. + */ +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 + * + * @return {String} String representation. + */ +DCFilter.prototype.toString = function() { + return 'DC Filter'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * A simple delay line. + * + * **Inputs** + * + * - Audio + * - Delay Time + * + * **Outputs** + * + * - Delayed audio + * + * **Parameters** + * + * - delayTime The delay time in seconds. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} maximumDelayTime The largest allowable delay time. + * @param {Number} delayTime The initial delay time. + */ +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 + * + * @return {String} String representation. + */ +Delay.prototype.toString = function() { + return 'Delay'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Detect discontinuities in the input stream. Looks for consecutive samples + * with a difference larger than a threshold value. + * + * **Inputs** + * + * - Audio + * + * **Outputs** + * + * - Audio + * + * @constructor + * @extends PassThroughNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [threshold=0.2] The threshold value. + * @param {Function} [callback] Function called if a discontinuity is detected. + */ +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. + * + * @param {Number} size The size of the discontinuity. + * @param {Number} channel The index of the channel the samples were found in. + * @param {Number} index The sample index the discontinuity was found at. + */ +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 + * + * @return {String} String representation. + */ +DiscontinuityDetector.prototype.toString = function() { + return 'Discontinuity Detector'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Delay line with feedback + * + * **Inputs** + * + * - Audio + * - Delay Time + * - Feedback + * - Mix + * + * **Outputs** + * + * - Delayed audio + * + * **Parameters** + * + * - delayTime The delay time in seconds. Linked to input 1. + * - feedback The amount of feedback. Linked to input 2. + * - mix The amount of delay to mix into the dry signal. Linked to input 3. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} maximumDelayTime The largest allowable delay time. + * @param {Number} delayTime The initial delay time. + * @param {Number} feedabck The initial feedback amount. + * @param {Number} mix The initial mix amount. + */ +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 + * + * @return {String} String representation. + */ +FeedbackDelay.prototype.toString = function() { + return 'Feedback Delay'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Fast Fourier Transform + * + * **Inputs** + * + * - Audio + * - Delay Time + * + * **Outputs** + * + * - Fourier transformed audio + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} bufferSize The FFT buffer size. + */ +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 + * + * @return {String} String representation. + */ +FFT.prototype.toString = function() { + return 'FFT'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/* + * Multiply values + * + * **Inputs** + * + * - Audio 1 + * - Audio 2 + * + * **Outputs** + * + * - Multiplied audio + * + * **Parameters** + * + * - value The value to multiply by. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [value=1] The initial value to multiply by. + */ +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 + * + * @return {String} String representation. + */ +Multiply.prototype.toString = function() { + return 'Multiply'; +}; + + +/*! + * @depends ../operators/Multiply.js + */ + +/** + * Simple gain control + * + * **Inputs** + * + * - Audio + * - Gain + * + * **Outputs** + * + * - Audio + * + * **Parameters** + * + * - gain The amount of gain. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [gain=1] Initial gain. + */ +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 + * + * @return {String} String representation. + */ +Gain.prototype.toString = function() { + return ('Gain'); +}; + +/*! + * @depends BiquadFilter.js + */ + +/** + * High-pass filter + * + * **Inputs** + * + * - Audio + * - Filter frequency + * + * **Outputs** + * + * - Filtered audio + * + * **Parameters** + * + * - frequency The filter frequency. Linked to input 1. + * + * @constructor + * @extends BiquadFilter + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} frequency The initial frequency. + */ +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 + * + * @param {Number} frequency The filter frequency. + */ +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 + * + * @return {String} String representation. + */ +HighPassFilter.prototype.toString = function() { + return 'High Pass Filter'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Inverse Fast Fourier Transform. Code liberally stolen with kind permission + * of Corben Brook from DSP.js (https://github.com/corbanbrook/dsp.js). + * + * **Inputs** + * + * - Fourier transformed audio + * - Delay Time + * + * **Outputs** + * + * - Audio + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} bufferSize The FFT buffer size. + */ +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 + * + * @return {String} String representation. + */ +IFFT.prototype.toString = function() { + return 'IFFT'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Exponential lag for smoothing signals. + * + * **Inputs** + * + * - Value + * - Lag time + * + * **Outputs** + * + * - Lagged value + * + * **Parameters** + * + * - value The value to lag. Linked to input 0. + * - lag The 60dB lag time. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [value=0] The initial value. + * @param {Number} [lagTime=1] The initial lag time. + */ +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 + * + * @return {String} String representation. + */ +Lag.prototype.toString = function() { + return 'Lag'; +}; + + +/*! + * @depends ../core/AudioletGroup.js + */ + +/** + * A simple (and frankly shoddy) zero-lookahead limiter. + * + * **Inputs** + * + * - Audio + * - Threshold + * - Attack + * - Release + * + * **Outputs** + * + * - Limited audio + * + * **Parameters** + * + * - threshold The limiter threshold. Linked to input 1. + * - attack The attack time in seconds. Linked to input 2. + * - release The release time in seconds. Linked to input 3. + * + * @constructor + * @extends AudioletGroup + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [threshold=0.95] The initial threshold. + * @param {Number} [attack=0.01] The initial attack time. + * @param {Number} [release=0.4] The initial release time. + */ +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 + * + * @return {String} String representation. + */ +Limiter.prototype.toString = function() { + return 'Limiter'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Linear cross-fade between two signals + * + * **Inputs** + * + * - Audio 1 + * - Audio 2 + * - Fade Position + * + * **Outputs** + * + * - Mixed audio + * + * **Parameters** + * + * - position The fade position. Values between 0 (Audio 1 only) and 1 (Audio + * 2 only). Linked to input 2. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [position=0.5] The initial fade position. + */ +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 + * + * @return {String} String representation. + */ +LinearCrossFade.prototype.toString = function() { + return 'Linear Cross Fader'; +}; + +/*! + * @depends BiquadFilter.js + */ + +/** + * Low-pass filter + * + * **Inputs** + * + * - Audio + * - Filter frequency + * + * **Outputs** + * + * - Filtered audio + * + * **Parameters** + * + * - frequency The filter frequency. Linked to input 1. + * + * @constructor + * @extends BiquadFilter + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} frequency The initial frequency. + */ +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 + * + * @param {Number} frequency The filter frequency. + */ +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 + * + * @return {String} String representation. + */ +LowPassFilter.prototype.toString = function() { + return 'Low Pass Filter'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Position a single-channel input in stereo space + * + * **Inputs** + * + * - Audio + * - Pan Position + * + * **Outputs** + * + * - Panned audio + * + * **Parameters** + * + * - pan The pan position. Values between 0 (hard-left) and 1 (hard-right). + * Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [pan=0.5] The initial pan position. + */ +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 + * + * @return {String} String representation. + */ +Pan.prototype.toString = function() { + return 'Stereo Panner'; +}; + +/*! + * @depends Envelope.js + */ + +/** + * Simple attack-release envelope + * + * **Inputs** + * + * - Gate + * + * **Outputs** + * + * - Envelope + * + * **Parameters** + * + * - gate The gate controlling the envelope. Value changes from 0 -> 1 + * trigger the envelope. Linked to input 0. + * + * @constructor + * @extends Envelope + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} gate The initial gate value. + * @param {Number} attack The attack time in seconds. + * @param {Number} release The release time in seconds. + * @param {Function} [onComplete] A function called after the release stage. + */ +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); + + this.attack = this.times[0]; + this.release = this.times[1]; +}; +extend(PercussiveEnvelope, Envelope); + +/** + * toString + * + * @return {String} String representation. + */ +PercussiveEnvelope.prototype.toString = function() { + return 'Percussive Envelope'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Pulse wave oscillator. + * + * **Inputs** + * + * - Frequency + * - Pulse width + * + * **Outputs** + * + * - Waveform + * + * **Parameters** + * + * - frequency The oscillator frequency. Linked to input 0. + * - pulseWidth The pulse width. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [frequency=440] The initial frequency. + * @param {Number} [pulseWidth=0.5] The initial pulse width. + */ +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 + * + * @return {String} String representation. + */ +Pulse.prototype.toString = function() { + return 'Pulse'; +}; + + +/*! + * @depends ../core/AudioletNode.js + * @depends ../core/AudioletGroup.js + */ + +/** + * 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** + * + * - Audio + * - Mix + * - Room Size + * - Damping + * + * **Outputs** + * + * - Reverberated Audio + * + * **Parameters** + * + * - mix The wet/dry mix. Values between 0 and 1. Linked to input 1. + * - roomSize The reverb's room size. Values between 0 and 1. Linked to input + * 2. + * - damping The amount of high-frequency damping. Values between 0 and 1. + * Linked to input 3. + * + * @constructor + * @extends AudioletGroup + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [mix=0.33] The initial wet/dry mix. + * @param {Number} [roomSize=0.5] The initial room size. + * @param {Number} [damping=0.5] The initial damping amount. + */ +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 + * + * @return {String} String representation. + */ +Reverb.prototype.toString = function() { + return 'Reverb'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Saw wave oscillator using a lookup table + * + * **Inputs** + * + * - Frequency + * + * **Outputs** + * + * - Saw wave + * + * **Parameters** + * + * - frequency The frequency of the oscillator. Linked to input 0. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [frequency=440] Initial frequency. + */ +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 + * + * @return {String} String representation. + */ +Saw.prototype.toString = function() { + return 'Saw'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * A soft-clipper, which distorts at values over +-0.5. + * + * **Inputs** + * + * - Audio + * + * **Outputs** + * + * - Clipped audio + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + */ + +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 + * + * @return {String} String representation. + */ +SoftClip.prototype.toString = function() { + return ('SoftClip'); +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Square wave oscillator + * + * **Inputs** + * + * - Frequency + * + * **Outputs** + * + * - Square wave + * + * **Parameters** + * + * - frequency The frequency of the oscillator. Linked to input 0. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [frequency=440] Initial frequency. + */ +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 + * + * @return {String} String representation. + */ +Square.prototype.toString = function() { + return 'Square'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Triangle wave oscillator using a lookup table + * + * **Inputs** + * + * - Frequency + * + * **Outputs** + * + * - Triangle wave + * + * **Parameters** + * + * - frequency The frequency of the oscillator. Linked to input 0. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [frequency=440] Initial frequency. + */ +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 + * + * @return {String} String representation. + */ +Triangle.prototype.toString = function() { + return 'Triangle'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Simple trigger which allows you to set a single sample to be 1 and then + * resets itself. + * + * **Outputs** + * + * - Triggers + * + * **Parameters** + * + * - trigger Set to 1 to fire a trigger. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [trigger=0] The initial trigger state. + */ +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 + * + * @return {String} String representation. + */ +TriggerControl.prototype.toString = function() { + return 'Trigger Control'; +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Upmix an input to a constant number of output channels + * + * **Inputs** + * + * - Audio + * + * **Outputs** + * + * - Upmixed audio + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} outputChannels The number of output channels. + */ +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 + * + * @return {String} String representation. + */ +UpMixer.prototype.toString = function() { + return 'UpMixer'; +}; + + +var WebKitBufferPlayer = function(audiolet, onComplete) { + AudioletNode.call(this, audiolet, 0, 1); + this.onComplete = onComplete; + this.isWebKit = this.audiolet.device.sink instanceof Sink.sinks.webkit; + this.ready = false; + + // Until we are loaded, output no channels. + this.setNumberOfOutputChannels(0, 0); + + if (!this.isWebKit) { + return; + } + + this.context = this.audiolet.device.sink._context; + this.jsNode = null; + this.source = null; + + this.ready = false; + this.loaded = false; + + this.buffers = []; + this.readPosition = 0; + + this.endTime = null; +}; +extend(WebKitBufferPlayer, AudioletNode); + +WebKitBufferPlayer.prototype.load = function(url, onLoad, onError) { + if (!this.isWebKit) { + return; + } + + this.stop(); + + // Request the new file + this.xhr = new XMLHttpRequest(); + this.xhr.open("GET", url, true); + this.xhr.responseType = "arraybuffer"; + this.xhr.onload = this.onLoad.bind(this, onLoad, onError); + this.xhr.onerror = onError; + this.xhr.send(); +}; + +WebKitBufferPlayer.prototype.stop = function() { + this.ready = false; + this.loaded = false; + + this.buffers = []; + this.readPosition = 0; + this.endTime = null; + + this.setNumberOfOutputChannels(0); + + this.disconnectWebKitNodes(); +}; + +WebKitBufferPlayer.prototype.disconnectWebKitNodes = function() { + if (this.source && this.jsNode) { + this.source.disconnect(this.jsNode); + this.jsNode.disconnect(this.context.destination); + this.source = null; + this.jsNode = null; + } +}; + +WebKitBufferPlayer.prototype.onLoad = function(onLoad, onError) { + // Load the buffer into memory for decoding +// this.fileBuffer = this.context.createBuffer(this.xhr.response, false); + this.context.decodeAudioData(this.xhr.response, function(buffer) { + this.onDecode(buffer); + onLoad(); + }.bind(this), onError); +}; + +WebKitBufferPlayer.prototype.onDecode = function(buffer) { + this.fileBuffer = buffer; + + // Create the WebKit buffer source for playback + this.source = this.context.createBufferSource(); + this.source.buffer = this.fileBuffer; + + // Make sure we are outputting the right number of channels on Audiolet's + // side + var numberOfChannels = this.fileBuffer.numberOfChannels; + this.setNumberOfOutputChannels(0, numberOfChannels); + + // Create the JavaScript node for reading the data into Audiolet + this.jsNode = this.context.createJavaScriptNode(4096, numberOfChannels, 0); + this.jsNode.onaudioprocess = this.onData.bind(this); + + // Connect it all up + this.source.connect(this.jsNode); + this.jsNode.connect(this.context.destination); + this.source.noteOn(0); + this.endTime = this.context.currentTime + this.fileBuffer.duration; + + this.loaded = true; +}; + +WebKitBufferPlayer.prototype.onData = function(event) { + if (this.loaded) { + this.ready = true; + } + + var numberOfChannels = event.inputBuffer.numberOfChannels; + + for (var i=0; i<numberOfChannels; i++) { + this.buffers[i] = event.inputBuffer.getChannelData(i); + this.readPosition = 0; + } +}; + +WebKitBufferPlayer.prototype.generate = function() { + if (!this.ready) { + return; + } + + var output = this.outputs[0]; + + var numberOfChannels = output.samples.length; + for (var i=0; i<numberOfChannels; i++) { + output.samples[i] = this.buffers[i][this.readPosition]; + } + this.readPosition += 1; + + if (this.context.currentTime > this.endTime) { + this.stop(); + this.onComplete(); + } +}; + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * A white noise source + * + * **Outputs** + * + * - White noise + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + */ +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 + * + * @return {String} String representation. + */ +WhiteNoise.prototype.toString = function() { + return 'White Noise'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Add values + * + * **Inputs** + * + * - Audio 1 + * - Audio 2 + * + * **Outputs** + * + * - Summed audio + * + * **Parameters** + * + * - value The value to add. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [value=0] The initial value to add. + */ +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 + * + * @return {String} String representation. + */ +Add.prototype.toString = function() { + return 'Add'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Divide values + * + * **Inputs** + * + * - Audio 1 + * - Audio 2 + * + * **Outputs** + * + * - Divided audio + * + * **Parameters** + * + * - value The value to divide by. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [value=1] The initial value to divide by. + */ +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 + * + * @return {String} String representation. + */ +Divide.prototype.toString = function() { + return 'Divide'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Modulo values + * + * **Inputs** + * + * - Audio 1 + * - Audio 2 + * + * **Outputs** + * + * - Moduloed audio + * + * **Parameters** + * + * - value The value to modulo by. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [value=1] The initial value to modulo by. + */ +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 + * + * @return {String} String representation. + */ +Modulo.prototype.toString = function() { + return 'Modulo'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/* + * Multiply and add values + * + * **Inputs** + * + * - Audio + * - Multiply audio + * - Add audio + * + * **Outputs** + * + * - MulAdded audio + * + * **Parameters** + * + * - mul The value to multiply by. Linked to input 1. + * - add The value to add. Linked to input 2. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [mul=1] The initial value to multiply by. + * @param {Number} [add=0] The initial value to add. + */ +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 + * + * @return {String} String representation. + */ +MulAdd.prototype.toString = function() { + return 'Multiplier/Adder'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Reciprocal (1/x) of values + * + * **Inputs** + * + * - Audio + * + * **Outputs** + * + * - Reciprocal audio + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + */ +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 + * + * @return {String} String representation. + */ +Reciprocal.prototype.toString = function() { + return 'Reciprocal'; +}; + + +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Subtract values + * + * **Inputs** + * + * - Audio 1 + * - Audio 2 + * + * **Outputs** + * + * - Subtracted audio + * + * **Parameters** + * + * - value The value to subtract. Linked to input 1. + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [value=0] The initial value to subtract. + */ +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 + * + * @return {String} String representation. + */ +Subtract.prototype.toString = function() { + return 'Subtract'; +}; + + +/** + * @depends ../core/AudioletNode.js + */ + +/** + * Hyperbolic tangent of values. Works nicely as a distortion function. + * + * **Inputs** + * + * - Audio + * + * **Outputs** + * + * - Tanh audio + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + */ + +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 + * + * @return {String} String representation. + */ +Tanh.prototype.toString = function() { + return ('Tanh'); +}; + + +/** + * 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. + * + * @constructor + */ +var Pattern = function() { +}; + +/** + * Default next function. + * + * @return {null} Null. + */ +Pattern.prototype.next = function() { + return null; +}; + +/** + * Return the current value of an item contained in a pattern. + * + * @param {Pattern|Object} The item. + * @return {Object} The value of the item. + */ +Pattern.prototype.valueOf = function(item) { + if (item instanceof Pattern) { + return (item.next()); + } + else { + return (item); + } +}; + +/** + * Default reset function. + */ +Pattern.prototype.reset = function() { +}; + + +/*! + * @depends Pattern.js + */ + +/** + * Arithmetic sequence. Adds a value to a running total on each next call. + * + * @constructor + * @extends Pattern + * @param {Number} start Starting value. + * @param {Pattern|Number} step Value to add. + * @param {Number} repeats Number of values to generate. + */ +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. + * + * @return {Number} The next value. + */ +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; + + +/*! + * @depends Pattern.js + */ + +/** + * Choose a random value from an array. + * + * @constructor + * @extends Pattern + * @param {Object[]} list Array of items to choose from. + * @param {Number} [repeats=1] Number of values to generate. + */ +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. + * + * @return {Number} The next value. + */ +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; + + +/*! + * @depends Pattern.js + */ + + +/** + * Geometric sequence. Multiplies a running total by a value on each next + * call. + * + * @constructor + * @extends Pattern + * @param {Number} start Starting value. + * @param {Pattern|Number} step Value to multiply by. + * @param {Number} repeats Number of values to generate. + */ +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. + * + * @return {Number} The next value. + */ +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; + + +/*! + * @depends Pattern.js + */ + +/** + * Proxy pattern. Holds a pattern which can safely be replaced by a different + * pattern while it is running. + * + * + * @constructor + * @extends Pattern + * @param {Pattern} pattern The initial pattern. + */ +var PProxy = function(pattern) { + Pattern.call(this); + if (pattern) { + this.pattern = pattern; + } +}; +extend(PProxy, Pattern); + +/** + * Generate the next value in the pattern. + * + * @return {Number} The next value. + */ +PProxy.prototype.next = function() { + var returnValue; + if (this.pattern) { + var returnValue = this.pattern.next(); + } + else { + returnValue = null; + } + return returnValue; +}; + +/** + * Alias + */ +var Pp = PProxy; + + +/*! + * @depends Pattern.js + */ + +/** + * Sequence of random numbers. + * + * @constructor + * @extends Pattern + * @param {Number|Pattern} low Lowest possible value. + * @param {Number|Pattern} high Highest possible value. + * @param {Number} repeats Number of values to generate. + */ +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. + * + * @return {Number} The next value. + */ +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; + + +/*! + * @depends Pattern.js + */ + +/** + * Iterate through a list of values. + * + * @constructor + * @extends Pattern + * @param {Object[]} list Array of values. + * @param {Number} [repeats=1] Number of times to loop through the array. + * @param {Number} [offset=0] Index to start from. + */ +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. + * + * @return {Number} The next value. + */ +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; + + +/*! + * @depends Pattern.js + */ + +/** + * Iterate through a list of values. + * + * @constructor + * @extends Pattern + * @param {Object[]} list Array of values. + * @param {Number} [repeats=1] Number of values to generate. + * @param {Number} [offset=0] Index to start from. + */ +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. + * + * @return {Number} The next value. + */ +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; + + +/*! + * @depends Pattern.js + */ + +/** + * Reorder an array, then iterate through it's values. + * + * @constructor + * @extends Pattern + * @param {Object[]} list Array of values. + * @param {Number} repeats Number of times to loop through the array. + */ +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. + * + * @return {Number} The next value. + */ +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; + + +/** + * Representation of a generic musical scale. Can be subclassed to produce + * specific scales. + * + * @constructor + * @param {Number[]} degrees Array of integer degrees. + * @param {Tuning} [tuning] The scale's tuning. Defaults to 12-tone ET. + */ +var Scale = function(degrees, tuning) { + this.degrees = degrees; + this.tuning = tuning || new EqualTemperamentTuning(12); +}; + +/** + * Get the frequency of a note in the scale. + * + * @constructor + * @param {Number} degree The note's degree. + * @param {Number} rootFrequency The root frequency of the scale. + * @param {Number} octave The octave of the note. + * @return {Number} The frequency of the note in hz. + */ +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; +}; + +/*! + * @depends Scale.js + */ + +/** + * Major scale. + * + * @constructor + * @extends Scale + */ +var MajorScale = function() { + Scale.call(this, [0, 2, 4, 5, 7, 9, 11]); +}; +extend(MajorScale, Scale); + +/*! + * @depends Scale.js + */ + +/** + * Minor scale. + * + * @constructor + * @extends Scale + */ + +var MinorScale = function() { + Scale.call(this, [0, 2, 3, 5, 7, 8, 10]); +}; +extend(MinorScale, Scale); + +/** + * Representation of a generic musical tuning. Can be subclassed to produce + * specific tunings. + * + * @constructor + * @param {Number[]} semitones Array of semitone values for the tuning. + * @param {Number} [octaveRatio=2] Frequency ratio for notes an octave apart. + */ + +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)); + } +}; + +/*! + * @depends Tuning.js + */ + +/** + * Equal temperament tuning. + * + * @constructor + * @extends Tuning + * @param {Number} pitchesPerOctave The number of notes in each octave. + */ +var EqualTemperamentTuning = function(pitchesPerOctave) { + var semitones = []; + for (var i = 0; i < pitchesPerOctave; i++) { + semitones.push(i); + } + Tuning.call(this, semitones, 2); +}; +extend(EqualTemperamentTuning, Tuning); + +var Sink = this.Sink = function (global) { + +/** + * Creates a Sink according to specified parameters, if possible. + * + * @class + * + * @arg =!readFn + * @arg =!channelCount + * @arg =!bufferSize + * @arg =!sampleRate + * + * @param {Function} readFn A callback to handle the buffer fills. + * @param {Number} channelCount Channel count. + * @param {Number} bufferSize (Optional) Specifies a pre-buffer size to control the amount of latency. + * @param {Number} sampleRate Sample rate (ms). + * @param {Number} default=0 writePosition Write position of the sink, as in how many samples have been written per channel. + * @param {String} default=async writeMode The default mode of writing to the sink. + * @param {String} default=interleaved channelMode The mode in which the sink asks the sample buffers to be channeled in. + * @param {Number} default=0 previousHit The previous time of a callback. + * @param {Buffer} default=null ringBuffer The ring buffer array of the sink. If null, ring buffering will not be applied. + * @param {Number} default=0 ringOffset The current position of the ring buffer. +*/ +function Sink (readFn, channelCount, bufferSize, sampleRate) { + var sinks = Sink.sinks.list, + i; + for (i=0; i<sinks.length; i++) { + if (sinks[i].enabled) { + try { + return new sinks[i](readFn, channelCount, bufferSize, sampleRate); + } catch(e1){} + } + } + + throw Sink.Error(0x02); +} + +function SinkClass () { +} + +Sink.SinkClass = SinkClass; + +SinkClass.prototype = Sink.prototype = { + sampleRate: 44100, + channelCount: 2, + bufferSize: 4096, + + writePosition: 0, + previousHit: 0, + ringOffset: 0, + + channelMode: 'interleaved', + isReady: false, + +/** + * Does the initialization of the sink. + * @method Sink +*/ + start: function (readFn, channelCount, bufferSize, sampleRate) { + this.channelCount = isNaN(channelCount) || channelCount === null ? this.channelCount: channelCount; + this.bufferSize = isNaN(bufferSize) || bufferSize === null ? this.bufferSize : bufferSize; + this.sampleRate = isNaN(sampleRate) || sampleRate === null ? this.sampleRate : sampleRate; + this.readFn = readFn; + this.activeRecordings = []; + this.previousHit = +new Date(); + Sink.EventEmitter.call(this); + Sink.emit('init', [this].concat([].slice.call(arguments))); + }, +/** + * The method which will handle all the different types of processing applied on a callback. + * @method Sink +*/ + process: function (soundData, channelCount) { + this.emit('preprocess', arguments); + + if (this.ringBuffer) { + (this.channelMode === 'interleaved' ? this.ringSpin : this.ringSpinInterleaved).apply(this, arguments); + } + + if (this.channelMode === 'interleaved') { + this.emit('audioprocess', arguments); + + if (this.readFn) { + this.readFn.apply(this, arguments); + } + } else { + var soundDataSplit = Sink.deinterleave(soundData, this.channelCount), + args = [soundDataSplit].concat([].slice.call(arguments, 1)); + this.emit('audioprocess', args); + + if (this.readFn) { + this.readFn.apply(this, args); + } + + Sink.interleave(soundDataSplit, this.channelCount, soundData); + } + this.emit('postprocess', arguments); + this.previousHit = +new Date(); + this.writePosition += soundData.length / channelCount; + }, +/** + * Get the current output position, defaults to writePosition - bufferSize. + * + * @method Sink + * + * @return {Number} The position of the write head, in samples, per channel. +*/ + getPlaybackTime: function () { + return this.writePosition - this.bufferSize; + }, +/** + * Internal method to send the ready signal if not ready yet. + * @method Sink +*/ + ready: function () { + if (this.isReady) return; + + this.isReady = true; + this.emit('ready', []); + } +}; + +/** + * The container for all the available sinks. Also a decorator function for creating a new Sink class and binding it. + * + * @method Sink + * @static + * + * @arg {String} type The name / type of the Sink. + * @arg {Function} constructor The constructor function for the Sink. + * @arg {Object} prototype The prototype of the Sink. (optional) + * @arg {Boolean} disabled Whether the Sink should be disabled at first. +*/ + +function sinks (type, constructor, prototype, disabled, priority) { + prototype = prototype || constructor.prototype; + constructor.prototype = new Sink.SinkClass(); + constructor.prototype.type = type; + constructor.enabled = !disabled; + + var k; + for (k in prototype) { + if (prototype.hasOwnProperty(k)) { + constructor.prototype[k] = prototype[k]; + } + } + + sinks[type] = constructor; + sinks.list[priority ? 'unshift' : 'push'](constructor); +} + +Sink.sinks = Sink.devices = sinks; +Sink.sinks.list = []; + +Sink.singleton = function () { + var sink = Sink.apply(null, arguments); + + Sink.singleton = function () { + return sink; + }; + + return sink; +}; + +global.Sink = Sink; + +return Sink; + +}(function (){ return this; }()); +void function (Sink) { + +/** + * A light event emitter. + * + * @class + * @static Sink +*/ +function EventEmitter () { + var k; + for (k in EventEmitter.prototype) { + if (EventEmitter.prototype.hasOwnProperty(k)) { + this[k] = EventEmitter.prototype[k]; + } + } + this._listeners = {}; +} + +EventEmitter.prototype = { + _listeners: null, +/** + * Emits an event. + * + * @method EventEmitter + * + * @arg {String} name The name of the event to emit. + * @arg {Array} args The arguments to pass to the event handlers. +*/ + emit: function (name, args) { + if (this._listeners[name]) { + for (var i=0; i<this._listeners[name].length; i++) { + this._listeners[name][i].apply(this, args); + } + } + return this; + }, +/** + * Adds an event listener to an event. + * + * @method EventEmitter + * + * @arg {String} name The name of the event. + * @arg {Function} listener The event listener to attach to the event. +*/ + on: function (name, listener) { + this._listeners[name] = this._listeners[name] || []; + this._listeners[name].push(listener); + return this; + }, +/** + * Adds an event listener to an event. + * + * @method EventEmitter + * + * @arg {String} name The name of the event. + * @arg {Function} !listener The event listener to remove from the event. If not specified, will delete all. +*/ + off: function (name, listener) { + if (this._listeners[name]) { + if (!listener) { + delete this._listeners[name]; + return this; + } + + for (var i=0; i<this._listeners[name].length; i++) { + if (this._listeners[name][i] === listener) { + this._listeners[name].splice(i--, 1); + } + } + + if (!this._listeners[name].length) { + delete this._listeners[name]; + } + } + return this; + } +}; + +Sink.EventEmitter = EventEmitter; + +EventEmitter.call(Sink); + +}(this.Sink); +void function (Sink) { + +/** + * Creates a timer with consistent (ie. not clamped) intervals even in background tabs. + * Uses inline workers to achieve this. If not available, will revert to regular timers. + * + * @static Sink + * @name doInterval + * + * @arg {Function} callback The callback to trigger on timer hit. + * @arg {Number} timeout The interval between timer hits. + * + * @return {Function} A function to cancel the timer. +*/ + +Sink.doInterval = function (callback, timeout) { + var timer, kill; + + function create (noWorker) { + if (Sink.inlineWorker.working && !noWorker) { + timer = Sink.inlineWorker('setInterval(function (){ postMessage("tic"); }, ' + timeout + ');'); + timer.onmessage = function (){ + callback(); + }; + kill = function () { + timer.terminate(); + }; + } else { + timer = setInterval(callback, timeout); + kill = function (){ + clearInterval(timer); + }; + } + } + + if (Sink.inlineWorker.ready) { + create(); + } else { + Sink.inlineWorker.on('ready', function () { + create(); + }); + } + + return function () { + if (!kill) { + if (!Sink.inlineWorker.ready) { + Sink.inlineWorker.on('ready', function () { + if (kill) kill(); + }); + } + } else { + kill(); + } + }; +}; + +}(this.Sink); +void function (Sink) { + +var _Blob, _BlobBuilder, _URL, _btoa; + +void function (prefixes, urlPrefixes) { + function find (name, prefixes) { + var b, a = prefixes.slice(); + + for (b=a.shift(); typeof b !== 'undefined'; b=a.shift()) { + b = Function('return typeof ' + b + name + + '=== "undefined" ? undefined : ' + + b + name)(); + + if (b) return b; + } + } + + _Blob = find('Blob', prefixes); + _BlobBuilder = find('BlobBuilder', prefixes); + _URL = find('URL', urlPrefixes); + _btoa = find('btoa', ['']); +}([ + '', + 'Moz', + 'WebKit', + 'MS' +], [ + '', + 'webkit' +]); + +var createBlob = _Blob && _URL && function (content, type) { + return _URL.createObjectURL(new _Blob([content], { type: type })); +}; + +var createBlobBuilder = _BlobBuilder && _URL && function (content, type) { + var bb = new _BlobBuilder(); + bb.append(content); + + return _URL.createObjectURL(bb.getBlob(type)); +}; + +var createData = _btoa && function (content, type) { + return 'data:' + type + ';base64,' + _btoa(content); +}; + +var createDynURL = + createBlob || + createBlobBuilder || + createData; + +if (!createDynURL) return; + +if (createBlob) createDynURL.createBlob = createBlob; +if (createBlobBuilder) createDynURL.createBlobBuilder = createBlobBuilder; +if (createData) createDynURL.createData = createData; + +if (_Blob) createDynURL.Blob = _Blob; +if (_BlobBuilder) createDynURL.BlobBuilder = _BlobBuilder; +if (_URL) createDynURL.URL = _URL; + +Sink.createDynURL = createDynURL; + +Sink.revokeDynURL = function (url) { + if (typeof url === 'string' && url.indexOf('data:') === 0) { + return false; + } else { + return _URL.revokeObjectURL(url); + } +}; + +}(this.Sink); +void function (Sink) { + +/* + * A Sink-specific error class. + * + * @class + * @static Sink + * @name Error + * + * @arg =code + * + * @param {Number} code The error code. + * @param {String} message A brief description of the error. + * @param {String} explanation A more verbose explanation of why the error occured and how to fix. +*/ + +function SinkError(code) { + if (!SinkError.hasOwnProperty(code)) throw SinkError(1); + if (!(this instanceof SinkError)) return new SinkError(code); + + var k; + for (k in SinkError[code]) { + if (SinkError[code].hasOwnProperty(k)) { + this[k] = SinkError[code][k]; + } + } + + this.code = code; +} + +SinkError.prototype = new Error(); + +SinkError.prototype.toString = function () { + return 'SinkError 0x' + this.code.toString(16) + ': ' + this.message; +}; + +SinkError[0x01] = { + message: 'No such error code.', + explanation: 'The error code does not exist.' +}; +SinkError[0x02] = { + message: 'No audio sink available.', + explanation: 'The audio device may be busy, or no supported output API is available for this browser.' +}; + +SinkError[0x10] = { + message: 'Buffer underflow.', + explanation: 'Trying to recover...' +}; +SinkError[0x11] = { + message: 'Critical recovery fail.', + explanation: 'The buffer underflow has reached a critical point, trying to recover, but will probably fail anyway.' +}; +SinkError[0x12] = { + message: 'Buffer size too large.', + explanation: 'Unable to allocate the buffer due to excessive length, please try a smaller buffer. Buffer size should probably be smaller than the sample rate.' +}; + +Sink.Error = SinkError; + +}(this.Sink); +void function (Sink) { + +/** + * Creates an inline worker using a data/blob URL, if possible. + * + * @static Sink + * + * @arg {String} script + * + * @return {Worker} A web worker, or null if impossible to create. +*/ + +var define = Object.defineProperty ? function (obj, name, value) { + Object.defineProperty(obj, name, { + value: value, + configurable: true, + writable: true + }); +} : function (obj, name, value) { + obj[name] = value; +}; + +function terminate () { + define(this, 'terminate', this._terminate); + + Sink.revokeDynURL(this._url); + + delete this._url; + delete this._terminate; + return this.terminate(); +} + +function inlineWorker (script) { + function wrap (type, content, typeName) { + try { + var url = type(content, 'text/javascript'); + var worker = new Worker(url); + + define(worker, '_url', url); + define(worker, '_terminate', worker.terminate); + define(worker, 'terminate', terminate); + + if (inlineWorker.type) return worker; + + inlineWorker.type = typeName; + inlineWorker.createURL = type; + + return worker; + } catch (e) { + return null; + } + } + + var createDynURL = Sink.createDynURL; + var worker; + + if (inlineWorker.createURL) { + return wrap(inlineWorker.createURL, script, inlineWorker.type); + } + + worker = wrap(createDynURL.createBlob, script, 'blob'); + if (worker) return worker; + + worker = wrap(createDynURL.createBlobBuilder, script, 'blobbuilder'); + if (worker) return worker; + + worker = wrap(createDynURL.createData, script, 'data'); + + return worker; +} + +Sink.EventEmitter.call(inlineWorker); + +inlineWorker.test = function () { + inlineWorker.ready = inlineWorker.working = false; + inlineWorker.type = ''; + inlineWorker.createURL = null; + + var worker = inlineWorker('this.onmessage=function(e){postMessage(e.data)}'); + var data = 'inlineWorker'; + + function ready (success) { + if (inlineWorker.ready) return; + + inlineWorker.ready = true; + inlineWorker.working = success; + inlineWorker.emit('ready', [success]); + inlineWorker.off('ready'); + + if (success && worker) { + worker.terminate(); + } + + worker = null; + } + + if (!worker) { + setTimeout(function () { + ready(false); + }, 0); + } else { + worker.onmessage = function (e) { + ready(e.data === data); + }; + + worker.postMessage(data); + + setTimeout(function () { + ready(false); + }, 1000); + } +}; + +Sink.inlineWorker = inlineWorker; + +inlineWorker.test(); + +}(this.Sink); +void function (Sink) { + +/** + * A Sink class for the Mozilla Audio Data API. +*/ + +Sink.sinks('audiodata', function () { + var self = this, + currentWritePosition = 0, + tail = null, + audioDevice = new Audio(), + written, currentPosition, available, soundData, prevPos, + timer; // Fix for https://bugzilla.mozilla.org/show_bug.cgi?id=630117 + self.start.apply(self, arguments); + self.preBufferSize = isNaN(arguments[4]) || arguments[4] === null ? this.preBufferSize : arguments[4]; + + function bufferFill() { + if (tail) { + written = audioDevice.mozWriteAudio(tail); + currentWritePosition += written; + if (written < tail.length){ + tail = tail.subarray(written); + return tail; + } + tail = null; + } + + currentPosition = audioDevice.mozCurrentSampleOffset(); + available = Number(currentPosition + (prevPos !== currentPosition ? self.bufferSize : self.preBufferSize) * self.channelCount - currentWritePosition); + + if (currentPosition === prevPos) { + self.emit('error', [Sink.Error(0x10)]); + } + + if (available > 0 || prevPos === currentPosition){ + self.ready(); + + try { + soundData = new Float32Array(prevPos === currentPosition ? self.preBufferSize * self.channelCount : + self.forceBufferSize ? available < self.bufferSize * 2 ? self.bufferSize * 2 : available : available); + } catch(e) { + self.emit('error', [Sink.Error(0x12)]); + self.kill(); + return; + } + self.process(soundData, self.channelCount); + written = self._audio.mozWriteAudio(soundData); + if (written < soundData.length){ + tail = soundData.subarray(written); + } + currentWritePosition += written; + } + prevPos = currentPosition; + } + + audioDevice.mozSetup(self.channelCount, self.sampleRate); + + this._timers = []; + + this._timers.push(Sink.doInterval(function () { + // Check for complete death of the output + if (+new Date() - self.previousHit > 2000) { + self._audio = audioDevice = new Audio(); + audioDevice.mozSetup(self.channelCount, self.sampleRate); + currentWritePosition = 0; + self.emit('error', [Sink.Error(0x11)]); + } + }, 1000)); + + this._timers.push(Sink.doInterval(bufferFill, self.interval)); + + self._bufferFill = bufferFill; + self._audio = audioDevice; +}, { + // These are somewhat safe values... + bufferSize: 24576, + preBufferSize: 24576, + forceBufferSize: false, + interval: 100, + + kill: function () { + while (this._timers.length) { + this._timers.shift()(); + } + + this.emit('kill'); + }, + + getPlaybackTime: function () { + return this._audio.mozCurrentSampleOffset() / this.channelCount; + } +}, false, true); + +Sink.sinks.moz = Sink.sinks.audiodata; + +}(this.Sink); +void function (Sink) { + +/** + * A dummy Sink. (No output) +*/ + +Sink.sinks('dummy', function () { + var self = this; + self.start.apply(self, arguments); + + function bufferFill () { + var soundData = new Float32Array(self.bufferSize * self.channelCount); + self.process(soundData, self.channelCount); + } + + self._kill = Sink.doInterval(bufferFill, self.bufferSize / self.sampleRate * 1000); + + self._callback = bufferFill; +}, { + kill: function () { + this._kill(); + this.emit('kill'); + } +}, true); + +}(this.Sink); +(function (Sink, sinks) { + +sinks = Sink.sinks; + +function newAudio (src) { + var audio = document.createElement('audio'); + if (src) { + audio.src = src; + } + return audio; +} + +/* TODO: Implement a <BGSOUND> hack for IE8. */ + +/** + * A sink class for WAV data URLs + * Relies on pcmdata.js and utils to be present. + * Thanks to grantgalitz and others for the idea. +*/ +sinks('wav', function () { + var self = this, + audio = new sinks.wav.wavAudio(), + PCMData = typeof PCMData === 'undefined' ? audioLib.PCMData : PCMData; + self.start.apply(self, arguments); + var soundData = new Float32Array(self.bufferSize * self.channelCount), + zeroData = new Float32Array(self.bufferSize * self.channelCount); + + if (!newAudio().canPlayType('audio/wav; codecs=1') || !btoa) throw 0; + + function bufferFill () { + if (self._audio.hasNextFrame) return; + + self.ready(); + + Sink.memcpy(zeroData, 0, soundData, 0); + self.process(soundData, self.channelCount); + + self._audio.setSource('data:audio/wav;base64,' + btoa( + audioLib.PCMData.encode({ + data: soundData, + sampleRate: self.sampleRate, + channelCount: self.channelCount, + bytesPerSample: self.quality + }) + )); + + if (!self._audio.currentFrame.src) self._audio.nextClip(); + } + + self.kill = Sink.doInterval(bufferFill, 40); + self._bufferFill = bufferFill; + self._audio = audio; +}, { + quality: 1, + bufferSize: 22050, + + getPlaybackTime: function () { + var audio = this._audio; + return (audio.currentFrame ? audio.currentFrame.currentTime * this.sampleRate : 0) + audio.samples; + } +}); + +function wavAudio () { + var self = this; + + self.currentFrame = newAudio(); + self.nextFrame = newAudio(); + + self._onended = function () { + self.samples += self.bufferSize; + self.nextClip(); + }; +} + +wavAudio.prototype = { + samples: 0, + nextFrame: null, + currentFrame: null, + _onended: null, + hasNextFrame: false, + + nextClip: function () { + var curFrame = this.currentFrame; + this.currentFrame = this.nextFrame; + this.nextFrame = curFrame; + this.hasNextFrame = false; + this.currentFrame.play(); + }, + + setSource: function (src) { + this.nextFrame.src = src; + this.nextFrame.addEventListener('ended', this._onended, true); + + this.hasNextFrame = true; + } +}; + +sinks.wav.wavAudio = wavAudio; + +}(this.Sink)); + (function (sinks, fixChrome82795) { + +var AudioContext = typeof window === 'undefined' ? null : window.webkitAudioContext || window.AudioContext; + +/** + * A sink class for the Web Audio API +*/ + +sinks('webaudio', function (readFn, channelCount, bufferSize, sampleRate) { + var self = this, + context = sinks.webaudio.getContext(), + node = null, + soundData = null, + zeroBuffer = null; + self.start.apply(self, arguments); + node = context.createJavaScriptNode(self.bufferSize, 0, self.channelCount); + + function bufferFill(e) { + var outputBuffer = e.outputBuffer, + channelCount = outputBuffer.numberOfChannels, + i, n, l = outputBuffer.length, + size = outputBuffer.size, + channels = new Array(channelCount), + tail; + + self.ready(); + + soundData = soundData && soundData.length === l * channelCount ? soundData : new Float32Array(l * channelCount); + zeroBuffer = zeroBuffer && zeroBuffer.length === soundData.length ? zeroBuffer : new Float32Array(l * channelCount); + soundData.set(zeroBuffer); + + for (i=0; i<channelCount; i++) { + channels[i] = outputBuffer.getChannelData(i); + } + + self.process(soundData, self.channelCount); + + for (i=0; i<l; i++) { + for (n=0; n < channelCount; n++) { + channels[n][i] = soundData[i * self.channelCount + n]; + } + } + } + + self.sampleRate = context.sampleRate; + + node.onaudioprocess = bufferFill; + node.connect(context.destination); + + self._context = context; + self._node = node; + self._callback = bufferFill; + /* Keep references in order to avoid garbage collection removing the listeners, working around http://code.google.com/p/chromium/issues/detail?id=82795 */ + // Thanks to @baffo32 + fixChrome82795.push(node); +}, { + kill: function () { + this._node.disconnect(0); + + for (var i=0; i<fixChrome82795.length; i++) { + if (fixChrome82795[i] === this._node) { + fixChrome82795.splice(i--, 1); + } + } + + this._node = this._context = null; + this.emit('kill'); + }, + + getPlaybackTime: function () { + return this._context.currentTime * this.sampleRate; + } +}, false, true); + +sinks.webkit = sinks.webaudio; + +sinks.webaudio.fix82795 = fixChrome82795; + +sinks.webaudio.getContext = function () { + // For now, we have to accept that the AudioContext is at 48000Hz, or whatever it decides. + var context = new AudioContext(/*sampleRate*/); + + sinks.webaudio.getContext = function () { + return context; + }; + + return context; +}; + +}(this.Sink.sinks, [])); +(function (Sink) { + +/** + * A Sink class for the Media Streams Processing API and/or Web Audio API in a Web Worker. +*/ + +Sink.sinks('worker', function () { + var self = this, + global = (function(){ return this; }()), + soundData = null, + outBuffer = null, + zeroBuffer = null; + self.start.apply(self, arguments); + + // Let's see if we're in a worker. + + importScripts(); + + function mspBufferFill (e) { + if (!self.isReady) { + self.initMSP(e); + } + + self.ready(); + + var channelCount = self.channelCount, + l = e.audioLength, + n, i; + + soundData = soundData && soundData.length === l * channelCount ? soundData : new Float32Array(l * channelCount); + outBuffer = outBuffer && outBuffer.length === soundData.length ? outBuffer : new Float32Array(l * channelCount); + zeroBuffer = zeroBuffer && zeroBuffer.length === soundData.length ? zeroBuffer : new Float32Array(l * channelCount); + + soundData.set(zeroBuffer); + outBuffer.set(zeroBuffer); + + self.process(soundData, self.channelCount); + + for (n=0; n<channelCount; n++) { + for (i=0; i<l; i++) { + outBuffer[n * e.audioLength + i] = soundData[n + i * channelCount]; + } + } + + e.writeAudio(outBuffer); + } + + function waBufferFill(e) { + if (!self.isReady) { + self.initWA(e); + } + + self.ready(); + + var outputBuffer = e.outputBuffer, + channelCount = outputBuffer.numberOfChannels, + i, n, l = outputBuffer.length, + size = outputBuffer.size, + channels = new Array(channelCount), + tail; + + soundData = soundData && soundData.length === l * channelCount ? soundData : new Float32Array(l * channelCount); + zeroBuffer = zeroBuffer && zeroBuffer.length === soundData.length ? zeroBuffer : new Float32Array(l * channelCount); + soundData.set(zeroBuffer); + + for (i=0; i<channelCount; i++) { + channels[i] = outputBuffer.getChannelData(i); + } + + self.process(soundData, self.channelCount); + + for (i=0; i<l; i++) { + for (n=0; n < channelCount; n++) { + channels[n][i] = soundData[i * self.channelCount + n]; + } + } + } + + global.onprocessmedia = mspBufferFill; + global.onaudioprocess = waBufferFill; + + self._mspBufferFill = mspBufferFill; + self._waBufferFill = waBufferFill; + +}, { + ready: false, + + initMSP: function (e) { + this.channelCount = e.audioChannels; + this.sampleRate = e.audioSampleRate; + this.bufferSize = e.audioLength * this.channelCount; + this.ready = true; + this.emit('ready', []); + }, + + initWA: function (e) { + var b = e.outputBuffer; + this.channelCount = b.numberOfChannels; + this.sampleRate = b.sampleRate; + this.bufferSize = b.length * this.channelCount; + this.ready = true; + this.emit('ready', []); + } +}); + +}(this.Sink)); +(function (Sink) { + +/** + * Splits a sample buffer into those of different channels. + * + * @static Sink + * @name deinterleave + * + * @arg {Buffer} buffer The sample buffer to split. + * @arg {Number} channelCount The number of channels to split to. + * + * @return {Array} An array containing the resulting sample buffers. +*/ + +Sink.deinterleave = function (buffer, channelCount) { + var l = buffer.length, + size = l / channelCount, + ret = [], + i, n; + for (i=0; i<channelCount; i++){ + ret[i] = new Float32Array(size); + for (n=0; n<size; n++){ + ret[i][n] = buffer[n * channelCount + i]; + } + } + return ret; +}; + +/** + * Joins an array of sample buffers into a single buffer. + * + * @static Sink + * @name resample + * + * @arg {Array} buffers The buffers to join. + * @arg {Number} !channelCount The number of channels. Defaults to buffers.length + * @arg {Buffer} !buffer The output buffer. + * + * @return {Buffer} The interleaved buffer created. +*/ + +Sink.interleave = function (buffers, channelCount, buffer) { + channelCount = channelCount || buffers.length; + var l = buffers[0].length, + bufferCount = buffers.length, + i, n; + buffer = buffer || new Float32Array(l * channelCount); + for (i=0; i<bufferCount; i++) { + for (n=0; n<l; n++) { + buffer[i + n * channelCount] = buffers[i][n]; + } + } + return buffer; +}; + +/** + * Mixes two or more buffers down to one. + * + * @static Sink + * @name mix + * + * @arg {Buffer} buffer The buffer to append the others to. + * @arg {Buffer} bufferX The buffers to append from. + * + * @return {Buffer} The mixed buffer. +*/ + +Sink.mix = function (buffer) { + var buffers = [].slice.call(arguments, 1), + l, i, c; + for (c=0; c<buffers.length; c++){ + l = Math.max(buffer.length, buffers[c].length); + for (i=0; i<l; i++){ + buffer[i] += buffers[c][i]; + } + } + return buffer; +}; + +/** + * Resets a buffer to all zeroes. + * + * @static Sink + * @name resetBuffer + * + * @arg {Buffer} buffer The buffer to reset. + * + * @return {Buffer} The 0-reset buffer. +*/ + +Sink.resetBuffer = function (buffer) { + var l = buffer.length, + i; + for (i=0; i<l; i++){ + buffer[i] = 0; + } + return buffer; +}; + +/** + * Copies the content of a buffer to another buffer. + * + * @static Sink + * @name clone + * + * @arg {Buffer} buffer The buffer to copy from. + * @arg {Buffer} !result The buffer to copy to. + * + * @return {Buffer} A clone of the buffer. +*/ + +Sink.clone = function (buffer, result) { + var l = buffer.length, + i; + result = result || new Float32Array(l); + for (i=0; i<l; i++){ + result[i] = buffer[i]; + } + return result; +}; + +/** + * Creates an array of buffers of the specified length and the specified count. + * + * @static Sink + * @name createDeinterleaved + * + * @arg {Number} length The length of a single channel. + * @arg {Number} channelCount The number of channels. + * @return {Array} The array of buffers. +*/ + +Sink.createDeinterleaved = function (length, channelCount) { + var result = new Array(channelCount), + i; + for (i=0; i<channelCount; i++){ + result[i] = new Float32Array(length); + } + return result; +}; + +Sink.memcpy = function (src, srcOffset, dst, dstOffset, length) { + src = src.subarray || src.slice ? src : src.buffer; + dst = dst.subarray || dst.slice ? dst : dst.buffer; + + src = srcOffset ? src.subarray ? + src.subarray(srcOffset, length && srcOffset + length) : + src.slice(srcOffset, length && srcOffset + length) : src; + + if (dst.set) { + dst.set(src, dstOffset); + } else { + for (var i=0; i<src.length; i++) { + dst[i + dstOffset] = src[i]; + } + } + + return dst; +}; + +Sink.memslice = function (buffer, offset, length) { + return buffer.subarray ? buffer.subarray(offset, length) : buffer.slice(offset, length); +}; + +Sink.mempad = function (buffer, out, offset) { + out = out.length ? out : new (buffer.constructor)(out); + Sink.memcpy(buffer, 0, out, offset); + return out; +}; + +Sink.linspace = function (start, end, out) { + var l, i, n, step; + out = out.length ? (l=out.length) && out : Array(l=out); + step = (end - start) / --l; + for (n=start+step, i=1; i<l; i++, n+=step) { + out[i] = n; + } + out[0] = start; + out[l] = end; + return out; +}; + +Sink.ftoi = function (input, bitCount, output) { + var i, mask = Math.pow(2, bitCount - 1); + + output = output || new (input.constructor)(input.length); + + for (i=0; i<input.length; i++) { + output[i] = ~~(mask * input[i]); + } + + return output; +}; + +}(this.Sink)); +(function (Sink) { + +function Proxy (bufferSize, channelCount) { + Sink.EventEmitter.call(this); + + this.bufferSize = isNaN(bufferSize) || bufferSize === null ? this.bufferSize : bufferSize; + this.channelCount = isNaN(channelCount) || channelCount === null ? this.channelCount : channelCount; + + var self = this; + this.callback = function () { + return self.process.apply(self, arguments); + }; + + this.resetBuffer(); +} + +Proxy.prototype = { + buffer: null, + zeroBuffer: null, + parentSink: null, + bufferSize: 4096, + channelCount: 2, + offset: null, + + resetBuffer: function () { + this.buffer = new Float32Array(this.bufferSize); + this.zeroBuffer = new Float32Array(this.bufferSize); + }, + + process: function (buffer, channelCount) { + if (this.offset === null) { + this.loadBuffer(); + } + + for (var i=0; i<buffer.length; i++) { + if (this.offset >= this.buffer.length) { + this.loadBuffer(); + } + + buffer[i] = this.buffer[this.offset++]; + } + }, + + loadBuffer: function () { + this.offset = 0; + Sink.memcpy(this.zeroBuffer, 0, this.buffer, 0); + this.emit('audioprocess', [this.buffer, this.channelCount]); + } +}; + +Sink.Proxy = Proxy; + +/** + * Creates a proxy callback system for the sink instance. + * Requires Sink utils. + * + * @method Sink + * @method createProxy + * + * @arg {Number} !bufferSize The buffer size for the proxy. +*/ +Sink.prototype.createProxy = function (bufferSize) { + var proxy = new Sink.Proxy(bufferSize, this.channelCount); + proxy.parentSink = this; + + this.on('audioprocess', proxy.callback); + + return proxy; +}; + +}(this.Sink)); +(function (Sink) { + +(function(){ + +/** + * If method is supplied, adds a new interpolation method to Sink.interpolation, otherwise sets the default interpolation method (Sink.interpolate) to the specified property of Sink.interpolate. + * + * @arg {String} name The name of the interpolation method to get / set. + * @arg {Function} !method The interpolation method. +*/ + +function interpolation(name, method) { + if (name && method) { + interpolation[name] = method; + } else if (name && interpolation[name] instanceof Function) { + Sink.interpolate = interpolation[name]; + } + return interpolation[name]; +} + +Sink.interpolation = interpolation; + + +/** + * Interpolates a fractal part position in an array to a sample. (Linear interpolation) + * + * @param {Array} arr The sample buffer. + * @param {number} pos The position to interpolate from. + * @return {Float32} The interpolated sample. +*/ +interpolation('linear', function (arr, pos) { + var first = Math.floor(pos), + second = first + 1, + frac = pos - first; + second = second < arr.length ? second : 0; + return arr[first] * (1 - frac) + arr[second] * frac; +}); + +/** + * Interpolates a fractal part position in an array to a sample. (Nearest neighbour interpolation) + * + * @param {Array} arr The sample buffer. + * @param {number} pos The position to interpolate from. + * @return {Float32} The interpolated sample. +*/ +interpolation('nearest', function (arr, pos) { + return pos >= arr.length - 0.5 ? arr[0] : arr[Math.round(pos)]; +}); + +interpolation('linear'); + +}()); + + +/** + * Resamples a sample buffer from a frequency to a frequency and / or from a sample rate to a sample rate. + * + * @static Sink + * @name resample + * + * @arg {Buffer} buffer The sample buffer to resample. + * @arg {Number} fromRate The original sample rate of the buffer, or if the last argument, the speed ratio to convert with. + * @arg {Number} fromFrequency The original frequency of the buffer, or if the last argument, used as toRate and the secondary comparison will not be made. + * @arg {Number} toRate The sample rate of the created buffer. + * @arg {Number} toFrequency The frequency of the created buffer. + * + * @return The new resampled buffer. +*/ +Sink.resample = function (buffer, fromRate /* or speed */, fromFrequency /* or toRate */, toRate, toFrequency) { + var + argc = arguments.length, + speed = argc === 2 ? fromRate : argc === 3 ? fromRate / fromFrequency : toRate / fromRate * toFrequency / fromFrequency, + l = buffer.length, + length = Math.ceil(l / speed), + newBuffer = new Float32Array(length), + i, n; + for (i=0, n=0; i<l; i += speed) { + newBuffer[n++] = Sink.interpolate(buffer, i); + } + return newBuffer; +}; + +}(this.Sink)); +void function (Sink) { + +Sink.on('init', function (sink) { + sink.activeRecordings = []; + sink.on('postprocess', sink.recordData); +}); + +Sink.prototype.activeRecordings = null; + +/** + * Starts recording the sink output. + * + * @method Sink + * @name record + * + * @return {Recording} The recording object for the recording started. +*/ +Sink.prototype.record = function () { + var recording = new Sink.Recording(this); + this.emit('record', [recording]); + return recording; +}; +/** + * Private method that handles the adding the buffers to all the current recordings. + * + * @method Sink + * @method recordData + * + * @arg {Array} buffer The buffer to record. +*/ +Sink.prototype.recordData = function (buffer) { + var activeRecs = this.activeRecordings, + i, l = activeRecs.length; + for (i=0; i<l; i++) { + activeRecs[i].add(buffer); + } +}; + +/** + * A Recording class for recording sink output. + * + * @class + * @static Sink + * @arg {Object} bindTo The sink to bind the recording to. +*/ + +function Recording (bindTo) { + this.boundTo = bindTo; + this.buffers = []; + bindTo.activeRecordings.push(this); +} + +Recording.prototype = { +/** + * Adds a new buffer to the recording. + * + * @arg {Array} buffer The buffer to add. + * + * @method Recording +*/ + add: function (buffer) { + this.buffers.push(buffer); + }, +/** + * Empties the recording. + * + * @method Recording +*/ + clear: function () { + this.buffers = []; + }, +/** + * Stops the recording and unbinds it from it's host sink. + * + * @method Recording +*/ + stop: function () { + var recordings = this.boundTo.activeRecordings, + i; + for (i=0; i<recordings.length; i++) { + if (recordings[i] === this) { + recordings.splice(i--, 1); + } + } + }, +/** + * Joins the recorded buffers into a single buffer. + * + * @method Recording +*/ + join: function () { + var bufferLength = 0, + bufPos = 0, + buffers = this.buffers, + newArray, + n, i, l = buffers.length; + + for (i=0; i<l; i++) { + bufferLength += buffers[i].length; + } + newArray = new Float32Array(bufferLength); + for (i=0; i<l; i++) { + for (n=0; n<buffers[i].length; n++) { + newArray[bufPos + n] = buffers[i][n]; + } + bufPos += buffers[i].length; + } + return newArray; + } +}; + +Sink.Recording = Recording; + +}(this.Sink); +void function (Sink) { + +function processRingBuffer () { + if (this.ringBuffer) { + (this.channelMode === 'interleaved' ? this.ringSpin : this.ringSpinInterleaved).apply(this, arguments); + } +} + +Sink.on('init', function (sink) { + sink.on('preprocess', processRingBuffer); +}); + +Sink.prototype.ringBuffer = null; + +/** + * A private method that applies the ring buffer contents to the specified buffer, while in interleaved mode. + * + * @method Sink + * @name ringSpin + * + * @arg {Array} buffer The buffer to write to. +*/ +Sink.prototype.ringSpin = function (buffer) { + var ring = this.ringBuffer, + l = buffer.length, + m = ring.length, + off = this.ringOffset, + i; + for (i=0; i<l; i++){ + buffer[i] += ring[off]; + off = (off + 1) % m; + } + this.ringOffset = off; +}; + +/** + * A private method that applies the ring buffer contents to the specified buffer, while in deinterleaved mode. + * + * @method Sink + * @name ringSpinDeinterleaved + * + * @param {Array} buffer The buffers to write to. +*/ +Sink.prototype.ringSpinDeinterleaved = function (buffer) { + var ring = this.ringBuffer, + l = buffer.length, + ch = ring.length, + m = ring[0].length, + len = ch * m, + off = this.ringOffset, + i, n; + for (i=0; i<l; i+=ch){ + for (n=0; n<ch; n++){ + buffer[i + n] += ring[n][off]; + } + off = (off + 1) % m; + } + this.ringOffset = n; +}; + +}(this.Sink); +void function (Sink, proto) { + +proto = Sink.prototype; + +Sink.on('init', function (sink) { + sink.asyncBuffers = []; + sink.syncBuffers = []; + sink.on('preprocess', sink.writeBuffersSync); + sink.on('postprocess', sink.writeBuffersAsync); +}); + +proto.writeMode = 'async'; +proto.asyncBuffers = proto.syncBuffers = null; + +/** + * Private method that handles the mixing of asynchronously written buffers. + * + * @method Sink + * @name writeBuffersAsync + * + * @arg {Array} buffer The buffer to write to. +*/ +proto.writeBuffersAsync = function (buffer) { + var buffers = this.asyncBuffers, + l = buffer.length, + buf, + bufLength, + i, n, offset; + if (buffers) { + for (i=0; i<buffers.length; i++) { + buf = buffers[i]; + bufLength = buf.b.length; + offset = buf.d; + buf.d -= Math.min(offset, l); + + for (n=0; n + offset < l && n < bufLength; n++) { + buffer[n + offset] += buf.b[n]; + } + buf.b = buf.b.subarray(n + offset); + if (i >= bufLength) { + buffers.splice(i--, 1); + } + } + } +}; + +/** + * A private method that handles mixing synchronously written buffers. + * + * @method Sink + * @name writeBuffersSync + * + * @arg {Array} buffer The buffer to write to. +*/ +proto.writeBuffersSync = function (buffer) { + var buffers = this.syncBuffers, + l = buffer.length, + i = 0, + soff = 0; + for (;i<l && buffers.length; i++) { + buffer[i] += buffers[0][soff]; + if (buffers[0].length <= soff){ + buffers.splice(0, 1); + soff = 0; + continue; + } + soff++; + } + if (buffers.length) { + buffers[0] = buffers[0].subarray(soff); + } +}; + +/** + * Writes a buffer asynchronously on top of the existing signal, after a specified delay. + * + * @method Sink + * @name writeBufferAsync + * + * @arg {Array} buffer The buffer to write. + * @arg {Number} delay The delay to write after. If not specified, the Sink will calculate a delay to compensate the latency. + * @return {Number} The number of currently stored asynchronous buffers. +*/ +proto.writeBufferAsync = function (buffer, delay) { + buffer = this.mode === 'deinterleaved' ? Sink.interleave(buffer, this.channelCount) : buffer; + var buffers = this.asyncBuffers; + buffers.push({ + b: buffer, + d: isNaN(delay) ? ~~((+new Date() - this.previousHit) / 1000 * this.sampleRate) : delay + }); + return buffers.length; +}; + +/** + * Writes a buffer synchronously to the output. + * + * @method Sink + * @name writeBufferSync + * + * @param {Array} buffer The buffer to write. + * @return {Number} The number of currently stored synchronous buffers. +*/ +proto.writeBufferSync = function (buffer) { + buffer = this.mode === 'deinterleaved' ? Sink.interleave(buffer, this.channelCount) : buffer; + var buffers = this.syncBuffers; + buffers.push(buffer); + return buffers.length; +}; + +/** + * Writes a buffer, according to the write mode specified. + * + * @method Sink + * @name writeBuffer + * + * @arg {Array} buffer The buffer to write. + * @arg {Number} delay The delay to write after. If not specified, the Sink will calculate a delay to compensate the latency. (only applicable in asynchronous write mode) + * @return {Number} The number of currently stored (a)synchronous buffers. +*/ +proto.writeBuffer = function () { + return this[this.writeMode === 'async' ? 'writeBufferAsync' : 'writeBufferSync'].apply(this, arguments); +}; + +/** + * Gets the total amount of yet unwritten samples in the synchronous buffers. + * + * @method Sink + * @name getSyncWriteOffset + * + * @return {Number} The total amount of yet unwritten samples in the synchronous buffers. +*/ +proto.getSyncWriteOffset = function () { + var buffers = this.syncBuffers, + offset = 0, + i; + for (i=0; i<buffers.length; i++) { + offset += buffers[i].length; + } + return offset; +}; + +} (this.Sink); + diff --git a/public/js/vendor/Audiolet.min.js b/public/js/vendor/Audiolet.min.js new file mode 100644 index 0000000..388c5c5 --- /dev/null +++ b/public/js/vendor/Audiolet.min.js @@ -0,0 +1,252 @@ +var AudioletBuffer=function(a,c){this.numberOfChannels=a;this.length=c;this.channels=[];for(var b=0;b<this.numberOfChannels;b++){this.channels.push(new Float32Array(c))}this.unslicedChannels=[];for(var b=0;b<this.numberOfChannels;b++){this.unslicedChannels.push(this.channels[b])}this.isEmpty=false;this.channelOffset=0};AudioletBuffer.prototype.getChannelData=function(a){return(this.channels[a])};AudioletBuffer.prototype.set=function(a){var b=a.numberOfChannels;for(var c=0;c<b;c++){this.channels[c].set(a.getChannelData(c))}};AudioletBuffer.prototype.setSection=function(c,g,f,b){f=f||0;b=b||0;var d=c.numberOfChannels;for(var e=0;e<d;e++){f+=c.channelOffset;b+=this.channelOffset;var a=this.unslicedChannels[e].subarray(b,b+g);var h=c.unslicedChannels[e].subarray(f,f+g); +/*! + var channel1 = this.getChannelData(i).subarray(outputOffset, + outputOffset + + length); + var channel2 = buffer.getChannelData(i).subarray(inputOffset, + inputOffset + + length); + */ +a.set(h)}};AudioletBuffer.prototype.add=function(b){var f=this.length;var d=b.numberOfChannels;for(var e=0;e<d;e++){var a=this.getChannelData(e);var g=b.getChannelData(e);for(var c=0;c<f;c++){a[c]+=g[c]}}};AudioletBuffer.prototype.addSection=function(f,b,h,a){h=h||0;a=a||0;var k=f.numberOfChannels;for(var g=0;g<k;g++){var d=this.getChannelData(g);var c=f.getChannelData(g);for(var e=0;e<b;e++){d[e+a]+=c[e+h]}}};AudioletBuffer.prototype.resize=function(n,c,e,g){g=g||0;var j=this.channels;var b=this.unslicedChannels;var d=this.length;var f=this.channelOffset+g;for(var h=0;h<n;h++){var k=j[h];var m=b[h];if(c>d){var l=k;if(!e||!m||m.length<c){m=new Float32Array(c)}k=m.subarray(0,c);if(!e&&l){k.set(l,g)}f=0}else{if(!m){var a=b[0].length;m=new Float32Array(a)}g=f;k=m.subarray(g,g+c)}j[h]=k;b[h]=m}this.channels=j.slice(0,n);this.unslicedChannels=b.slice(0,n);this.length=c;this.numberOfChannels=n;this.channelOffset=f};AudioletBuffer.prototype.push=function(a){var b=a.length;this.resize(this.numberOfChannels,this.length+b);this.setSection(a,b,0,this.length-b)};AudioletBuffer.prototype.pop=function(a){var b=a.length;var c=this.length-b;a.setSection(this,b,c,0);this.resize(this.numberOfChannels,c)};AudioletBuffer.prototype.unshift=function(a){var b=a.length;this.resize(this.numberOfChannels,this.length+b,false,b);this.setSection(a,b,0,0)};AudioletBuffer.prototype.shift=function(a){var b=a.length;a.setSection(this,b,0,0);this.resize(this.numberOfChannels,this.length-b,false,b)};AudioletBuffer.prototype.zero=function(){var b=this.numberOfChannels;for(var c=0;c<b;c++){var e=this.getChannelData(c);var d=this.length;for(var a=0;a<d;a++){e[a]=0}}};AudioletBuffer.prototype.combined=function(){var a=this.channels;var b=this.numberOfChannels;var e=this.length;var d=new Float32Array(b*e);for(var c=0;c<b;c++){d.set(a[c],c*e)}return d};AudioletBuffer.prototype.interleaved=function(){var a=this.channels;var c=this.numberOfChannels;var e=this.length;var f=new Float32Array(c*e);for(var d=0;d<e;d++){for(var b=0;b<c;b++){f[c*d+b]=a[b][d]}}return f};AudioletBuffer.prototype.copy=function(){var a=new AudioletBuffer(this.numberOfChannels,this.length);a.set(this);return a};AudioletBuffer.prototype.load=function(c,a,d){var b=new AudioFileRequest(c,a);b.onSuccess=function(e){this.length=e.length;this.numberOfChannels=e.channels.length;this.unslicedChannels=e.channels;this.channels=e.channels;this.channelOffset=0;if(d){d()}}.bind(this);b.onFailure=function(){console.error("Could not load",c)}.bind(this);b.send()};var AudioletGroup=function(b,a,d){this.audiolet=b;this.inputs=[];for(var c=0;c<a;c++){this.inputs.push(new PassThroughNode(this.audiolet,1,1))}this.outputs=[];for(var c=0;c<d;c++){this.outputs.push(new PassThroughNode(this.audiolet,1,1))}};AudioletGroup.prototype.connect=function(c,b,a){this.outputs[b||0].connect(c,0,a)};AudioletGroup.prototype.disconnect=function(c,b,a){this.outputs[b||0].disconnect(c,0,a)};AudioletGroup.prototype.remove=function(){var a=this.inputs.length;for(var b=0;b<a;b++){this.inputs[b].remove()}var c=this.outputs.length;for(var b=0;b<c;b++){this.outputs[b].remove()}}; +/*! + * @depends AudioletGroup.js + */ +var AudioletDestination=function(c,b,a,d){AudioletGroup.call(this,c,1,0);this.device=new AudioletDevice(c,b,a,d);c.device=this.device;this.scheduler=new Scheduler(c);c.scheduler=this.scheduler;this.upMixer=new UpMixer(c,this.device.numberOfChannels);this.inputs[0].connect(this.scheduler);this.scheduler.connect(this.upMixer);this.upMixer.connect(this.device)};extend(AudioletDestination,AudioletGroup);AudioletDestination.prototype.toString=function(){return"Destination"};var AudioletNode=function(c,b,e,a){this.audiolet=c;this.inputs=[];for(var d=0;d<b;d++){this.inputs.push(new AudioletInput(this,d))}this.outputs=[];for(var d=0;d<e;d++){this.outputs.push(new AudioletOutput(this,d))}if(a){this.generate=a}};AudioletNode.prototype.connect=function(d,c,b){if(d instanceof AudioletGroup){d=d.inputs[b||0];b=0}var a=this.outputs[c||0];var e=d.inputs[b||0];a.connect(e);e.connect(a);this.audiolet.device.needTraverse=true};AudioletNode.prototype.disconnect=function(d,c,b){if(d instanceof AudioletGroup){d=d.inputs[b||0];b=0}var a=this.outputs[c||0];var e=d.inputs[b||0];e.disconnect(a);a.disconnect(e);this.audiolet.device.needTraverse=true};AudioletNode.prototype.setNumberOfOutputChannels=function(a,b){this.outputs[a].numberOfChannels=b};AudioletNode.prototype.linkNumberOfOutputChannels=function(b,a){this.outputs[b].linkNumberOfChannels(this.inputs[a])};AudioletNode.prototype.tick=function(){this.createInputSamples();this.createOutputSamples();this.generate()};AudioletNode.prototype.traverse=function(a){if(a.indexOf(this)==-1){a.push(this);a=this.traverseParents(a)}return a};AudioletNode.prototype.traverseParents=function(c){var a=this.inputs.length;for(var e=0;e<a;e++){var b=this.inputs[e];var f=b.connectedFrom.length;for(var d=0;d<f;d++){c=b.connectedFrom[d].node.traverse(c)}}return c};AudioletNode.prototype.generate=function(){};AudioletNode.prototype.createInputSamples=function(){var a=this.inputs.length;for(var f=0;f<a;f++){var d=this.inputs[f];var h=0;for(var e=0;e<d.connectedFrom.length;e++){var c=d.connectedFrom[e];for(var b=0;b<c.samples.length;b++){var g=c.samples[b];if(b<h){d.samples[b]+=g}else{d.samples[b]=g;h+=1}}}if(d.samples.length>h){d.samples=d.samples.slice(0,h)}}};AudioletNode.prototype.createOutputSamples=function(){var e=this.outputs.length;for(var d=0;d<e;d++){var a=this.outputs[d];var c=a.getNumberOfChannels();if(a.samples.length==c){continue}else{if(a.samples.length>c){a.samples=a.samples.slice(0,c);continue}}for(var b=a.samples.length;b<c;b++){a.samples[b]=0}}};AudioletNode.prototype.remove=function(){var g=this.inputs.length;for(var e=0;e<g;e++){var h=this.inputs[e];var c=h.connectedFrom.length;for(var d=0;d<c;d++){var a=h.connectedFrom[d];var b=a.node;b.disconnect(this,a.index,e)}}var f=this.outputs.length;for(var e=0;e<f;e++){var b=this.outputs[e];var c=b.connectedTo.length;for(var d=0;d<c;d++){var k=b.connectedTo[d];var h=k.node;this.disconnect(h,e,k.index)}}}; +/*! + * @depends AudioletNode.js + */ +function AudioletDevice(c,b,a,d){AudioletNode.call(this,c,1,0);this.sink=Sink(this.tick.bind(this),a,d,b);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);AudioletDevice.prototype.tick=function(a,d){if(!this.paused){var b=this.inputs[0];var f=a.length/d;for(var e=0;e<f;e++){if(this.needTraverse){this.nodes=this.traverse([]);this.needTraverse=false}for(var c=this.nodes.length-1;c>0;c--){this.nodes[c].tick()}this.createInputSamples();for(var c=0;c<d;c++){a[e*d+c]=b.samples[c]}this.writePosition+=1}}};AudioletDevice.prototype.getPlaybackTime=function(){return this.sink.getPlaybackTime()};AudioletDevice.prototype.getWriteTime=function(){return this.writePosition};AudioletDevice.prototype.pause=function(){this.paused=true};AudioletDevice.prototype.play=function(){this.paused=false};AudioletDevice.prototype.toString=function(){return"Audio Output Device"};var AudioletInput=function(b,a){this.node=b;this.index=a;this.connectedFrom=[];this.samples=[]};AudioletInput.prototype.connect=function(a){this.connectedFrom.push(a)};AudioletInput.prototype.disconnect=function(a){var c=this.connectedFrom.length;for(var b=0;b<c;b++){if(a==this.connectedFrom[b]){this.connectedFrom.splice(b,1);break}}if(this.connectedFrom.length==0){this.samples=[]}};AudioletInput.prototype.toString=function(){return this.node.toString()+"Input #"+this.index};var Audiolet=function(b,a,c){this.output=new AudioletDestination(this,b,a,c)};var AudioletOutput=function(b,a){this.node=b;this.index=a;this.connectedTo=[];this.samples=[];this.linkedInput=null;this.numberOfChannels=1};AudioletOutput.prototype.connect=function(a){this.connectedTo.push(a)};AudioletOutput.prototype.disconnect=function(a){var c=this.connectedTo.length;for(var b=0;b<c;b++){if(a==this.connectedTo[b]){this.connectedTo.splice(b,1);break}}};AudioletOutput.prototype.linkNumberOfChannels=function(a){this.linkedInput=a};AudioletOutput.prototype.unlinkNumberOfChannels=function(){this.linkedInput=null};AudioletOutput.prototype.getNumberOfChannels=function(){if(this.linkedInput&&this.linkedInput.connectedFrom.length){return(this.linkedInput.samples.length)}return(this.numberOfChannels)};AudioletOutput.prototype.toString=function(){return this.node.toString()+"Output #"+this.index+" - "};var AudioletParameter=function(a,c,b){this.node=a;if(typeof c!="undefined"&&c!=null){this.input=a.inputs[c]}else{this.input=null}this.value=b||0};AudioletParameter.prototype.isStatic=function(){return(this.input.samples.length==0)};AudioletParameter.prototype.isDynamic=function(){return(this.input.samples.length>0)};AudioletParameter.prototype.setValue=function(a){this.value=a};AudioletParameter.prototype.getValue=function(){if(this.input!=null&&this.input.samples.length>0){return this.input.samples[0]}else{return this.value}};function extend(a,c){function b(){}b.prototype=c.prototype;a.prototype=new b();a.prototype.constructor=a} +/*! + * @depends ../core/AudioletNode.js + */ +var ParameterNode=function(a,b){AudioletNode.call(this,a,1,1);this.parameter=new AudioletParameter(this,0,b)};extend(ParameterNode,AudioletNode);ParameterNode.prototype.generate=function(){this.outputs[0].samples[0]=this.parameter.getValue()};ParameterNode.prototype.toString=function(){return"Parameter Node"}; +/*! + * @depends AudioletNode.js + */ +var PassThroughNode=function(b,a,c){AudioletNode.call(this,b,a,c)};extend(PassThroughNode,AudioletNode);PassThroughNode.prototype.createOutputSamples=function(){var f=this.outputs.length;for(var e=0;e<f;e++){var b=this.inputs[e];var a=this.outputs[e];if(b&&b.samples.length!=0){a.samples=b.samples}else{var d=a.getNumberOfChannels();if(a.samples.length==d){continue}else{if(a.samples.length>d){a.samples=a.samples.slice(0,d);continue}}for(var c=a.samples.length;c<d;c++){a.samples[c]=0}}}};PassThroughNode.prototype.toString=function(){return"Pass Through Node"};var PriorityQueue=function(c,b){if(b){this.compare=b}if(c){this.heap=c;for(var a=0;a<Math.floor(this.heap.length/2);a++){this.siftUp(a)}}else{this.heap=[]}};PriorityQueue.prototype.push=function(a){this.heap.push(a);this.siftDown(0,this.heap.length-1)};PriorityQueue.prototype.pop=function(){var b,a;b=this.heap.pop();if(this.heap.length){var a=this.heap[0];this.heap[0]=b;this.siftUp(0)}else{a=b}return(a)};PriorityQueue.prototype.peek=function(){return(this.heap[0])};PriorityQueue.prototype.isEmpty=function(){return(this.heap.length==0)};PriorityQueue.prototype.siftDown=function(d,a){var b=this.heap[a];while(a>d){var e=(a-1)>>1;var c=this.heap[e];if(this.compare(b,c)){this.heap[a]=c;a=e;continue}break}this.heap[a]=b};PriorityQueue.prototype.siftUp=function(a){var e=this.heap.length;var d=a;var c=this.heap[a];var b=2*a+1;while(b<e){var f=b+1;if(f<e&&!this.compare(this.heap[b],this.heap[f])){b=f}this.heap[a]=this.heap[b];a=b;b=2*a+1}this.heap[a]=c;this.siftDown(d,a)};PriorityQueue.prototype.compare=function(d,c){return(d<c)}; +/*! + * @depends PassThroughNode.js + */ +var Scheduler=function(a,b){PassThroughNode.call(this,a,1,1);this.linkNumberOfOutputChannels(0,0);this.bpm=b||120;this.queue=new PriorityQueue(null,function(d,c){return(d.time<c.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);Scheduler.prototype.setTempo=function(a){this.bpm=a;this.beatLength=60/this.bpm*this.audiolet.device.sampleRate};Scheduler.prototype.addRelative=function(a,c){var b={};b.callback=c;b.time=this.time+a*this.beatLength;this.queue.push(b);return b};Scheduler.prototype.addAbsolute=function(b,c){if(b<this.beat||b==this.beat&&this.time>this.lastBeatTime){return null}var a={};a.callback=c;a.time=this.lastBeatTime+(b-this.beat)*this.beatLength;this.queue.push(a);return a};Scheduler.prototype.play=function(b,a,d){var c={};c.patterns=b;c.durationPattern=a;c.callback=d;c.time=this.audiolet.device.getWriteTime();this.queue.push(c);return c};Scheduler.prototype.playAbsolute=function(d,b,a,e){if(d<this.beat||d==this.beat&&this.time>this.lastBeatTime){return null}var c={};c.patterns=b;c.durationPattern=a;c.callback=e;c.time=this.lastBeatTime+(d-this.beat)*this.beatLength;this.queue.push(c);return c};Scheduler.prototype.remove=function(b){var a=this.queue.heap.indexOf(b);if(a!=-1){this.queue.heap.splice(a,1);this.queue=new PriorityQueue(this.queue.heap,function(d,c){return(d.time<c.time)})}};Scheduler.prototype.stop=function(a){this.remove(a)};Scheduler.prototype.tick=function(){PassThroughNode.prototype.tick.call(this);this.tickClock();while(!this.queue.isEmpty()&&this.queue.peek().time<=this.time){var a=this.queue.pop();this.processEvent(a)}};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}};Scheduler.prototype.processEvent=function(a){var h=a.durationPattern;if(h){var g=[];var b=a.patterns;var e=b.length;for(var d=0;d<e;d++){var f=b[d];var j=f.next();if(j!=null){g.push(j)}else{return}}a.callback.apply(null,g);var c;if(h instanceof Pattern){c=h.next()}else{c=h}if(c){a.time+=c*this.beatLength;this.queue.push(a)}}else{a.callback()}};Scheduler.prototype.toString=function(){return"Scheduler"};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})}} +/*! + * @depends ../core/AudioletNode.js + */ +var Add=function(a,b){AudioletNode.call(this,a,2,1);this.linkNumberOfOutputChannels(0,0);this.value=new AudioletParameter(this,1,b||0)};extend(Add,AudioletNode);Add.prototype.generate=function(){var b=this.inputs[0];var a=this.outputs[0];var e=this.value.getValue();var c=b.samples.length;for(var d=0;d<c;d++){a.samples[d]=b.samples[d]+e}};Add.prototype.toString=function(){return"Add"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Divide=function(a,b){AudioletNode.call(this,a,2,1);this.linkNumberOfOutputChannels(0,0);this.value=new AudioletParameter(this,1,b||1)};extend(Divide,AudioletNode);Divide.prototype.generate=function(){var b=this.inputs[0];var a=this.outputs[0];var e=this.value.getValue();var c=b.samples.length;for(var d=0;d<c;d++){a.samples[d]=b.samples[d]/e}};Divide.prototype.toString=function(){return"Divide"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Modulo=function(a,b){AudioletNode.call(this,a,2,1);this.linkNumberOfOutputChannels(0,0);this.value=new AudioletParameter(this,1,b||1)};extend(Modulo,AudioletNode);Modulo.prototype.generate=function(){var b=this.inputs[0];var a=this.outputs[0];var e=this.value.getValue();var c=b.samples.length;for(var d=0;d<c;d++){a.samples[d]=b.samples[d]%e}};Modulo.prototype.toString=function(){return"Modulo"}; +/*! + * @depends ../core/AudioletNode.js + */ +var MulAdd=function(a,c,b){AudioletNode.call(this,a,3,1);this.linkNumberOfOutputChannels(0,0);this.mul=new AudioletParameter(this,1,c||1);this.add=new AudioletParameter(this,2,b||0)};extend(MulAdd,AudioletNode);MulAdd.prototype.generate=function(){var b=this.inputs[0];var a=this.outputs[0];var f=this.mul.getValue();var e=this.add.getValue();var c=b.samples.length;for(var d=0;d<c;d++){a.samples[d]=b.samples[d]*f+e}};MulAdd.prototype.toString=function(){return"Multiplier/Adder"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Multiply=function(a,b){AudioletNode.call(this,a,2,1);this.linkNumberOfOutputChannels(0,0);this.value=new AudioletParameter(this,1,b||1)};extend(Multiply,AudioletNode);Multiply.prototype.generate=function(){var d=this.value.getValue();var a=this.inputs[0];var b=a.samples.length;for(var c=0;c<b;c++){this.outputs[0].samples[c]=a.samples[c]*d}};Multiply.prototype.toString=function(){return"Multiply"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Reciprocal=function(a){AudioletNode.call(this,a,1,1);this.linkNumberOfOutputChannels(0,0)};extend(Reciprocal,AudioletNode);Reciprocal.prototype.generate=function(){var b=this.inputs[0];var a=this.outputs[0];var c=b.samples.length;for(var d=0;d<c;d++){a.samples[d]=1/b.samples[d]}};Reciprocal.prototype.toString=function(){return"Reciprocal"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Subtract=function(a,b){AudioletNode.call(this,a,2,1);this.linkNumberOfOutputChannels(0,0);this.value=new AudioletParameter(this,1,b||0)};extend(Subtract,AudioletNode);Subtract.prototype.generate=function(){var b=this.inputs[0];var a=this.outputs[0];var e=this.value.getValue();var c=b.samples.length;for(var d=0;d<c;d++){a.samples[d]=b.samples[d]-e}};Subtract.prototype.toString=function(){return"Subtract"};var Tanh=function(a){AudioletNode.call(this,a,1,1);this.linkNumberOfOutputChannels(0,0)};extend(Tanh,AudioletNode);Tanh.prototype.generate=function(){var b=this.inputs[0];var a=this.outputs[0];var c=b.samples.length;for(var d=0;d<c;d++){var e=b.samples[d];a.samples[d]=(Math.exp(e)-Math.exp(-e))/(Math.exp(e)+Math.exp(-e))}};Tanh.prototype.toString=function(){return("Tanh")}; +/*! + * @depends ../core/AudioletNode.js + */ +var Envelope=function(b,d,e,g,a,f){AudioletNode.call(this,b,1,1);this.gate=new AudioletParameter(this,0,d||1);this.levels=[];for(var c=0;c<e.length;c++){this.levels.push(new AudioletParameter(this,null,e[c]))}this.times=[];for(var c=0;c<g.length;c++){this.times.push(new AudioletParameter(this,null,g[c]))}this.releaseStage=a;this.onComplete=f;this.stage=null;this.time=null;this.changeTime=null;this.level=this.levels[0].getValue();this.delta=0;this.gateOn=false};extend(Envelope,AudioletNode);Envelope.prototype.generate=function(){var b=this.gate.getValue();var a=false;if(b&&!this.gateOn){this.gateOn=true;this.stage=0;this.time=0;this.delta=0;this.level=this.levels[0].getValue();if(this.stage!=this.releaseStage){a=true}}if(this.gateOn&&!b){this.gateOn=false;if(this.releaseStage!=null){this.stage=this.releaseStage;a=true}}if(this.changeTime){this.time+=1;if(this.time>=this.changeTime){this.stage+=1;if(this.stage!=this.releaseStage){a=true}else{this.changeTime=null;this.delta=0}}}if(a){if(this.stage!=this.times.length){this.delta=this.calculateDelta(this.stage,this.level);this.changeTime=this.calculateChangeTime(this.stage,this.time)}else{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};Envelope.prototype.calculateDelta=function(b,d){var c=this.levels[b+1].getValue()-d;var a=this.times[b].getValue()*this.audiolet.device.sampleRate;return(c/a)};Envelope.prototype.calculateChangeTime=function(b,c){var a=this.times[b].getValue()*this.audiolet.device.sampleRate;return(c+a)};Envelope.prototype.toString=function(){return"Envelope"}; +/*! + * @depends Envelope.js + */ +var ADSREnvelope=function(j,e,c,d,g,f,b){var h=[0,1,g,0];var a=[c,d,f];Envelope.call(this,j,e,h,a,2,b);this.attack=this.times[0];this.decay=this.times[1];this.sustain=this.levels[2];this.release=this.levels[2]};extend(ADSREnvelope,Envelope);ADSREnvelope.prototype.toString=function(){return"ADSR Envelope"}; +/*! + * @depends ../core/AudioletNode.js + */ +var BiquadFilter=function(a,b){AudioletNode.call(this,a,2,1);this.linkNumberOfOutputChannels(0,0);this.frequency=new AudioletParameter(this,1,b||22100);this.lastFrequency=null;this.xValues=[];this.yValues=[];this.b0=0;this.b1=0;this.b2=0;this.a0=0;this.a1=0;this.a2=0};extend(BiquadFilter,AudioletNode);BiquadFilter.prototype.calculateCoefficients=function(a){};BiquadFilter.prototype.generate=function(){var k=this.inputs[0];var j=this.outputs[0];var m=this.xValues;var p=this.yValues;var l=this.frequency.getValue();if(l!=this.lastFrequency){this.calculateCoefficients(l);this.lastFrequency=l}var v=this.a0;var u=this.a1;var t=this.a2;var h=this.b0;var g=this.b1;var f=this.b2;var d=k.samples.length;for(var n=0;n<d;n++){if(n>=m.length){m.push([0,0]);p.push([0,0])}var r=m[n];var q=r[0];var o=r[1];var e=p[n];var b=e[0];var a=e[1];var s=k.samples[n];var c=(h/v)*s+(g/v)*q+(f/v)*o-(u/v)*b-(t/v)*a;j.samples[n]=c;r[0]=s;r[1]=q;e[0]=c;e[1]=b}};BiquadFilter.prototype.toString=function(){return"Biquad Filter"}; +/*! + * @depends BiquadFilter.js + */ +var AllPassFilter=function(a,b){BiquadFilter.call(this,a,b)};extend(AllPassFilter,BiquadFilter);AllPassFilter.prototype.calculateCoefficients=function(e){var b=2*Math.PI*e/this.audiolet.device.sampleRate;var a=Math.cos(b);var c=Math.sin(b);var d=c/(2/Math.sqrt(2));this.b0=1-d;this.b1=-2*a;this.b2=1+d;this.a0=1+d;this.a1=-2*a;this.a2=1-d};AllPassFilter.prototype.toString=function(){return"All Pass Filter"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Amplitude=function(b,c,a){AudioletNode.call(this,b,3,1);this.linkNumberOfOutputChannels(0,0);this.followers=[];this.attack=new AudioletParameter(this,1,c||0.01);this.release=new AudioletParameter(this,2,a||0.01)};extend(Amplitude,AudioletNode);Amplitude.prototype.generate=function(){var h=this.inputs[0];var a=this.outputs[0];var f=this.followers;var b=f.length;var l=this.audiolet.device.sampleRate;var d=this.attack.getValue();d=Math.pow(0.01,1/(d*l));var j=this.release.getValue();j=Math.pow(0.01,1/(j*l));var g=h.samples.length;for(var c=0;c<g;c++){if(c>=b){f.push(0)}var e=f[c];var k=Math.abs(h.samples[c]);if(k>e){e=d*(e-k)+k}else{e=j*(e-k)+k}a.samples[c]=e;f[c]=e}};Amplitude.prototype.toString=function(){return("Amplitude")}; +/*! + * @depends ../core/PassThroughNode.js + */ +var BadValueDetector=function(a,b){PassThroughNode.call(this,a,1,1);this.linkNumberOfOutputChannels(0,0);if(b){this.callback=b}};extend(BadValueDetector,PassThroughNode);BadValueDetector.prototype.callback=function(b,a){console.error(b+" detected at channel "+a)};BadValueDetector.prototype.generate=function(){var a=this.inputs[0];var b=a.samples.length;for(var c=0;c<b;c++){var d=a.samples[c];if(typeof d=="undefined"||d==null||isNaN(d)||d==Infinity||d==-Infinity){this.callback(d,c)}}};BadValueDetector.prototype.toString=function(){return"Bad Value Detector"}; +/*! + * @depends BiquadFilter.js + */ +var BandPassFilter=function(a,b){BiquadFilter.call(this,a,b)};extend(BandPassFilter,BiquadFilter);BandPassFilter.prototype.calculateCoefficients=function(e){var b=2*Math.PI*e/this.audiolet.device.sampleRate;var a=Math.cos(b);var c=Math.sin(b);var d=c/(2/Math.sqrt(2));this.b0=d;this.b1=0;this.b2=-d;this.a0=1+d;this.a1=-2*a;this.a2=1-d};BandPassFilter.prototype.toString=function(){return"Band Pass Filter"}; +/*! + * @depends BiquadFilter.js + */ +var BandRejectFilter=function(a,b){BiquadFilter.call(this,a,b)};extend(BandRejectFilter,BiquadFilter);BandRejectFilter.prototype.calculateCoefficients=function(e){var b=2*Math.PI*e/this.audiolet.device.sampleRate;var a=Math.cos(b);var c=Math.sin(b);var d=c/(2/Math.sqrt(2));this.b0=1;this.b1=-2*a;this.b2=1;this.a0=1+d;this.a1=-2*a;this.a2=1-d};BandRejectFilter.prototype.toString=function(){return"Band Reject Filter"}; +/*! + * @depends ../core/AudioletNode.js + */ +var BitCrusher=function(a,b){AudioletNode.call(this,a,2,1);this.linkNumberOfOutputChannels(0,0);this.bits=new AudioletParameter(this,1,b)};extend(BitCrusher,AudioletNode);BitCrusher.prototype.generate=function(){var a=this.inputs[0];var d=Math.pow(2,this.bits.getValue())-1;var b=a.samples.length;for(var c=0;c<b;c++){this.outputs[0].samples[c]=Math.floor(a.samples[c]*d)/d}};BitCrusher.prototype.toString=function(){return"BitCrusher"}; +/*! + * @depends ../core/AudioletNode.js + */ +var BufferPlayer=function(c,b,e,d,a,f){AudioletNode.call(this,c,3,1);this.buffer=b;this.setNumberOfOutputChannels(0,this.buffer.numberOfChannels);this.position=d||0;this.playbackRate=new AudioletParameter(this,0,e||1);this.restartTrigger=new AudioletParameter(this,1,0);this.startPosition=new AudioletParameter(this,2,d||0);this.loop=new AudioletParameter(this,3,a||0);this.onComplete=f;this.restartTriggerOn=false;this.playing=true};extend(BufferPlayer,AudioletNode);BufferPlayer.prototype.generate=function(){var c=this.outputs[0];var d=c.samples.length;if(this.buffer.length==0||!this.playing){for(var e=0;e<d;e++){c.samples[e]=0}return}var g=this.playbackRate.getValue();var b=this.restartTrigger.getValue();var f=this.startPosition.getValue();var a=this.loop.getValue();if(b>0&&!this.restartTriggerOn){this.position=f;this.restartTriggerOn=true;this.playing=true}if(b<=0&&this.restartTriggerOn){this.restartTriggerOn=false}var d=this.buffer.channels.length;for(var e=0;e<d;e++){var h=this.buffer.getChannelData(e);c.samples[e]=h[Math.floor(this.position)]}this.position+=g;if(this.position>=this.buffer.length){if(a){this.position%=this.buffer.length}else{this.playing=false;if(this.onComplete){this.onComplete()}}}};BufferPlayer.prototype.toString=function(){return("Buffer player")}; +/*! + * @depends ../core/AudioletNode.js + */ +var CombFilter=function(a,d,c,b){AudioletNode.call(this,a,3,1);this.linkNumberOfOutputChannels(0,0);this.maximumDelayTime=d;this.delayTime=new AudioletParameter(this,1,c||1);this.decayTime=new AudioletParameter(this,2,b);this.buffers=[];this.readWriteIndex=0};extend(CombFilter,AudioletNode);CombFilter.prototype.generate=function(){var k=this.inputs[0];var c=this.outputs[0];var l=this.audiolet.device.sampleRate;var f=this.delayTime.getValue()*l;var h=this.decayTime.getValue()*l;var a=Math.exp(-3*f/h);var j=k.samples.length;for(var e=0;e<j;e++){if(e>=this.buffers.length){var b=this.maximumDelayTime*l;this.buffers.push(new Float32Array(b))}var d=this.buffers[e];var g=d[this.readWriteIndex];c.samples[e]=g;d[this.readWriteIndex]=k.samples[e]+a*g}this.readWriteIndex+=1;if(this.readWriteIndex>=f){this.readWriteIndex=0}};CombFilter.prototype.toString=function(){return"Comb Filter"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Sine=function(a,b){AudioletNode.call(this,a,1,1);this.frequency=new AudioletParameter(this,0,b||440);this.phase=0};extend(Sine,AudioletNode);Sine.prototype.generate=function(){var a=this.outputs[0];var c=this.frequency.getValue();var b=this.audiolet.device.sampleRate;a.samples[0]=Math.sin(this.phase);this.phase+=2*Math.PI*c/b;if(this.phase>2*Math.PI){this.phase%=2*Math.PI}};Sine.prototype.toString=function(){return"Sine"}; +/*! + * @depends ../core/AudioletNode.js + * @depends Sine.js + */ +var CrossFade=function(b,a){AudioletNode.call(this,b,3,1);this.linkNumberOfOutputChannels(0,0);this.position=new AudioletParameter(this,2,a||0.5)};extend(CrossFade,AudioletNode);CrossFade.prototype.generate=function(){var j=this.inputs[0];var h=this.inputs[1];var c=this.outputs[0];var g=this.position.getValue();var b=g*Math.PI/2;var f=Math.cos(b);var e=Math.sin(b);var k=c.samples.length;for(var d=0;d<k;d++){var a=j.samples[d]||0;var l=h.samples[d]||0;c.samples[d]=a*f+l*e}};CrossFade.prototype.toString=function(){return"Cross Fader"}; +/*! + * @depends ../core/AudioletNode.js + */ +var DampedCombFilter=function(b,e,d,c,a){AudioletNode.call(this,b,4,1);this.linkNumberOfOutputChannels(0,0);this.maximumDelayTime=e;this.delayTime=new AudioletParameter(this,1,d||1);this.decayTime=new AudioletParameter(this,2,c);this.damping=new AudioletParameter(this,3,a);var f=e*this.audiolet.device.sampleRate;this.buffers=[];this.readWriteIndex=0;this.filterStores=[]};extend(DampedCombFilter,AudioletNode);DampedCombFilter.prototype.generate=function(){var m=this.inputs[0];var c=this.outputs[0];var n=this.audiolet.device.sampleRate;var g=this.delayTime.getValue()*n;var k=this.decayTime.getValue()*n;var d=this.damping.getValue();var a=Math.exp(-3*g/k);var l=m.samples.length;for(var f=0;f<l;f++){if(f>=this.buffers.length){var b=this.maximumDelayTime*n;this.buffers.push(new Float32Array(b))}if(f>=this.filterStores.length){this.filterStores.push(0)}var e=this.buffers[f];var h=this.filterStores[f];var j=e[this.readWriteIndex];h=(j*(1-d))+(h*d);c.samples[f]=j;e[this.readWriteIndex]=m.samples[f]+a*h;this.filterStores[f]=h}this.readWriteIndex+=1;if(this.readWriteIndex>=g){this.readWriteIndex=0}};DampedCombFilter.prototype.toString=function(){return"Damped Comb Filter"}; +/*! + * @depends ../core/AudioletNode.js + */ +var DCFilter=function(b,a){AudioletNode.call(this,b,2,1);this.linkNumberOfOutputChannels(0,0);this.coefficient=new AudioletParameter(this,1,a||0.995);this.xValues=[];this.yValues=[]};extend(DCFilter,AudioletNode);DCFilter.prototype.generate=function(){var c=this.coefficient.getValue();var a=this.inputs[0];var b=a.samples.length;for(var e=0;e<b;e++){if(e>=this.xValues.length){this.xValues.push(0)}if(e>=this.yValues.length){this.yValues.push(0)}var d=a.samples[e];var f=d-this.xValues[e]+c*this.yValues[e];this.outputs[0].samples[e]=f;this.xValues[e]=d;this.yValues[e]=f}};DCFilter.prototype.toString=function(){return"DC Filter"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Delay=function(a,c,b){AudioletNode.call(this,a,2,1);this.linkNumberOfOutputChannels(0,0);this.maximumDelayTime=c;this.delayTime=new AudioletParameter(this,1,b||1);var d=c*this.audiolet.device.sampleRate;this.buffers=[];this.readWriteIndex=0};extend(Delay,AudioletNode);Delay.prototype.generate=function(){var c=this.inputs[0];var b=this.outputs[0];var e=this.audiolet.device.sampleRate;var g=this.delayTime.getValue()*e;var d=c.samples.length;for(var f=0;f<d;f++){if(f>=this.buffers.length){var h=this.maximumDelayTime*e;this.buffers.push(new Float32Array(h))}var a=this.buffers[f];b.samples[f]=a[this.readWriteIndex];a[this.readWriteIndex]=c.samples[f]}this.readWriteIndex+=1;if(this.readWriteIndex>=g){this.readWriteIndex=0}};Delay.prototype.toString=function(){return"Delay"}; +/*! + * @depends ../core/AudioletNode.js + */ +var DiscontinuityDetector=function(b,a,c){AudioletNode.call(this,b,1,1);this.linkNumberOfOutputChannels(0,0);this.threshold=a||0.2;if(c){this.callback=c}this.lastValues=[]};extend(DiscontinuityDetector,AudioletNode);DiscontinuityDetector.prototype.callback=function(a,b){console.error("Discontinuity of "+a+" detected on channel "+b)};DiscontinuityDetector.prototype.generate=function(){var b=this.inputs[0];var a=this.outputs[0];var c=b.samples.length;for(var d=0;d<c;d++){if(d>=this.lastValues.length){this.lastValues.push(0)}var e=b.samples[d];var f=Math.abs(this.lastValues[d]-e);if(f>this.threshold){this.callback(f,d)}this.lastValues[d]=e}};DiscontinuityDetector.prototype.toString=function(){return"Discontinuity Detector"}; +/*! + * @depends ../core/AudioletNode.js + */ +var FeedbackDelay=function(b,e,d,a,c){AudioletNode.call(this,b,4,1);this.linkNumberOfOutputChannels(0,0);this.maximumDelayTime=e;this.delayTime=new AudioletParameter(this,1,d||1);this.feedback=new AudioletParameter(this,2,a||0.5);this.mix=new AudioletParameter(this,3,c||1);var f=e*this.audiolet.device.sampleRate;this.buffers=[];this.readWriteIndex=0};extend(FeedbackDelay,AudioletNode);FeedbackDelay.prototype.generate=function(){var j=this.inputs[0];var c=this.outputs[0];var k=this.audiolet.output.device.sampleRate;var g=this.delayTime.getValue()*k;var a=this.feedback.getValue();var n=this.mix.getValue();var h=j.samples.length;var d=this.buffers.length;for(var f=0;f<h;f++){if(f>=d){var b=this.maximumDelayTime*k;this.buffers.push(new Float32Array(b))}var e=this.buffers[f];var m=j.samples[f];var l=e[this.readWriteIndex];c.samples[f]=n*l+(1-n)*m;e[this.readWriteIndex]=m+a*l}this.readWriteIndex+=1;if(this.readWriteIndex>=g){this.readWriteIndex=0}};FeedbackDelay.prototype.toString=function(){return"Feedback Delay"}; +/*! + * @depends ../core/AudioletNode.js + */ +var FFT=function(a,b){AudioletNode.call(this,a,2,1);this.linkNumberOfOutputChannels(0,0);this.bufferSize=b;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);FFT.prototype.generate=function(){var b=this.inputs[0];var a=this.outputs[0];if(b.samples.length==0){return}this.buffer[this.readWriteIndex]=b.samples[0];a.samples[0]=[this.realBuffer[this.readWriteIndex],this.imaginaryBuffer[this.readWriteIndex]];this.readWriteIndex+=1;if(this.readWriteIndex>=this.bufferSize){this.transform();this.readWriteIndex=0}};FFT.prototype.calculateReverseTable=function(){var a=1;var c=this.bufferSize>>1;while(a<this.bufferSize){for(var b=0;b<a;b++){this.reverseTable[b+a]=this.reverseTable[b]+c}a=a<<1;c=c>>1}};FFT.prototype.transform=function(){for(var e=0;e<this.bufferSize;e++){this.realBuffer[e]=this.buffer[this.reverseTable[e]];this.imaginaryBuffer[e]=0}var f=1;while(f<this.bufferSize){var l=Math.cos(-Math.PI/f);var c=Math.sin(-Math.PI/f);var g=1;var k=0;for(var b=0;b<f;b++){var e=b;while(e<this.bufferSize){var d=e+f;var h=(g*this.realBuffer[d])-(k*this.imaginaryBuffer[d]);var a=(g*this.imaginaryBuffer[d])+(k*this.realBuffer[d]);this.realBuffer[d]=this.realBuffer[e]-h;this.imaginaryBuffer[d]=this.imaginaryBuffer[e]-a;this.realBuffer[e]+=h;this.imaginaryBuffer[e]+=a;e+=f<<1}var j=g;g=(j*l)-(k*c);k=(j*c)+(k*l)}f=f<<1}};FFT.prototype.toString=function(){return"FFT"}; +/*! + * @depends ../operators/Multiply.js + */ +var Gain=function(a,b){Multiply.call(this,a,b);this.gain=this.value};extend(Gain,Multiply);Gain.prototype.toString=function(){return("Gain")}; +/*! + * @depends BiquadFilter.js + */ +var HighPassFilter=function(a,b){BiquadFilter.call(this,a,b)};extend(HighPassFilter,BiquadFilter);HighPassFilter.prototype.calculateCoefficients=function(e){var b=2*Math.PI*e/this.audiolet.device.sampleRate;var a=Math.cos(b);var c=Math.sin(b);var d=c/(2/Math.sqrt(2));this.b0=(1+a)/2;this.b1=-(1+a);this.b2=this.b0;this.a0=1+d;this.a1=-2*a;this.a2=1-d};HighPassFilter.prototype.toString=function(){return"High Pass Filter"}; +/*! + * @depends ../core/AudioletNode.js + */ +var IFFT=function(a,b){AudioletNode.call(this,a,2,1);this.linkNumberOfOutputChannels(0,0);this.bufferSize=b;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);IFFT.prototype.generate=function(){var c=this.inputs[0];var b=this.outputs[0];if(!c.samples.length){return}var a=c.samples[0];this.realBuffer[this.readWriteIndex]=a[0];this.imaginaryBuffer[this.readWriteIndex]=a[1];b.samples[0]=this.buffer[this.readWriteIndex];this.readWriteIndex+=1;if(this.readWriteIndex>=this.bufferSize){this.transform();this.readWriteIndex=0}};IFFT.prototype.calculateReverseTable=function(){var a=1;var c=this.bufferSize>>1;while(a<this.bufferSize){for(var b=0;b<a;b++){this.reverseTable[b+a]=this.reverseTable[b]+c}a=a<<1;c=c>>1}};IFFT.prototype.transform=function(){var e=1;for(var f=0;f<this.bufferSize;f++){this.imaginaryBuffer[f]*=-1}for(var f=0;f<this.bufferSize;f++){this.reverseReal[f]=this.realBuffer[this.reverseTable[f]];this.reverseImaginary[f]=this.imaginaryBuffer[this.reverseTable[f]]}this.realBuffer.set(this.reverseReal);this.imaginaryBuffer.set(this.reverseImaginary);while(e<this.bufferSize){var l=Math.cos(-Math.PI/e);var c=Math.sin(-Math.PI/e);var g=1;var k=0;for(var b=0;b<e;b++){f=b;while(f<this.bufferSize){var d=f+e;var h=(g*this.realBuffer[d])-(k*this.imaginaryBuffer[d]);var a=(g*this.imaginaryBuffer[d])+(k*this.realBuffer[d]);this.realBuffer[d]=this.realBuffer[f]-h;this.imaginaryBuffer[d]=this.imaginaryBuffer[f]-a;this.realBuffer[f]+=h;this.imaginaryBuffer[f]+=a;f+=e<<1}var j=g;g=(j*l)-(k*c);k=(j*c)+(k*l)}e=e<<1}for(f=0;f<this.bufferSize;f++){this.buffer[f]=this.realBuffer[f]/this.bufferSize}};IFFT.prototype.toString=function(){return"IFFT"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Lag=function(a,c,b){AudioletNode.call(this,a,2,1);this.value=new AudioletParameter(this,0,c||0);this.lag=new AudioletParameter(this,1,b||1);this.lastValue=0;this.log001=Math.log(0.001)};extend(Lag,AudioletNode);Lag.prototype.generate=function(){var c=this.inputs[0];var b=this.outputs[0];var e=this.audiolet.device.sampleRate;var f=this.value.getValue();var g=this.lag.getValue();var d=Math.exp(this.log001/(g*e));var a=((1-d)*f)+(d*this.lastValue);b.samples[0]=a;this.lastValue=a};Lag.prototype.toString=function(){return"Lag"}; +/*! + * @depends ../core/AudioletGroup.js + */ +var Limiter=function(c,b,d,a){AudioletNode.call(this,c,4,1);this.linkNumberOfOutputChannels(0,0);this.threshold=new AudioletParameter(this,1,b||0.95);this.attack=new AudioletParameter(this,2,d||0.01);this.release=new AudioletParameter(this,2,a||0.4);this.followers=[]};extend(Limiter,AudioletNode);Limiter.prototype.generate=function(){var h=this.inputs[0];var a=this.outputs[0];var m=this.audiolet.device.sampleRate;var b=Math.pow(0.01,1/(this.attack.getValue()*m));var j=Math.pow(0.01,1/(this.release.getValue()*m));var d=this.threshold.getValue();var g=h.samples.length;for(var c=0;c<g;c++){if(c>=this.followers.length){this.followers.push(0)}var f=this.followers[c];var l=h.samples[c];var e=Math.abs(l);if(e>f){f=b*(f-e)+e}else{f=j*(f-e)+e}var k=f-d;if(k>0){a.samples[c]=l/(1+k)}else{a.samples[c]=l}this.followers[c]=f}};Limiter.prototype.toString=function(){return"Limiter"}; +/*! + * @depends ../core/AudioletNode.js + */ +var LinearCrossFade=function(b,a){AudioletNode.call(this,b,3,1);this.linkNumberOfOutputChannels(0,0);this.position=new AudioletParameter(this,2,a||0.5)};extend(LinearCrossFade,AudioletNode);LinearCrossFade.prototype.generate=function(){var h=this.inputs[0];var g=this.inputs[1];var b=this.outputs[0];var f=this.position.getValue();var e=1-f;var c=f;var j=b.samples.length;for(var d=0;d<j;d++){var a=h.samples[d]||0;var k=g.samples[d]||0;b.samples[d]=a*e+k*c}};LinearCrossFade.prototype.toString=function(){return"Linear Cross Fader"}; +/*! + * @depends BiquadFilter.js + */ +var LowPassFilter=function(a,b){BiquadFilter.call(this,a,b)};extend(LowPassFilter,BiquadFilter);LowPassFilter.prototype.calculateCoefficients=function(e){var b=2*Math.PI*e/this.audiolet.device.sampleRate;var a=Math.cos(b);var c=Math.sin(b);var d=c/(2/Math.sqrt(2));this.b0=(1-a)/2;this.b1=1-a;this.b2=this.b0;this.a0=1+d;this.a1=-2*a;this.a2=1-d};LowPassFilter.prototype.toString=function(){return"Low Pass Filter"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Pan=function(a,b){AudioletNode.call(this,a,2,1);this.setNumberOfOutputChannels(0,2);if(b==null){var b=0.5}this.pan=new AudioletParameter(this,1,b)};extend(Pan,AudioletNode);Pan.prototype.generate=function(){var c=this.inputs[0];var b=this.outputs[0];var e=this.pan.getValue();var d=c.samples[0]||0;var a=e*Math.PI/2;b.samples[0]=d*Math.cos(a);b.samples[1]=d*Math.sin(a)};Pan.prototype.toString=function(){return"Stereo Panner"}; +/*! + * @depends Envelope.js + */ +var PercussiveEnvelope=function(b,c,g,a,e){var d=[0,1,0];var f=[g,a];Envelope.call(this,b,c,d,f,null,e);this.attack=this.times[0];this.release=this.times[1]};extend(PercussiveEnvelope,Envelope);PercussiveEnvelope.prototype.toString=function(){return"Percussive Envelope"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Pulse=function(a,b,c){AudioletNode.call(this,a,2,1);this.frequency=new AudioletParameter(this,0,b||440);this.pulseWidth=new AudioletParameter(this,1,c||0.5);this.phase=0};extend(Pulse,AudioletNode);Pulse.prototype.generate=function(){var c=this.pulseWidth.getValue();this.outputs[0].samples[0]=(this.phase<c)?1:-1;var b=this.frequency.getValue();var a=this.audiolet.device.sampleRate;this.phase+=b/a;if(this.phase>1){this.phase%=1}};Pulse.prototype.toString=function(){return"Pulse"}; +/*! + * @depends ../core/AudioletNode.js + * @depends ../core/AudioletGroup.js + */ +var Reverb=function(d,g,a,c){AudioletNode.call(this,d,4,1);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;this.combTuning=[1116,1188,1277,1356,1422,1491,1557,1617];this.allPassTuning=[556,441,341,225];var g=g||this.initialMix;this.mix=new AudioletParameter(this,1,g);var a=a||this.initialRoomSize;this.roomSize=new AudioletParameter(this,2,a);var c=c||this.initialDamping;this.damping=new AudioletParameter(this,3,c);this.combBuffers=[];this.combIndices=[];this.filterStores=[];var b=this.combTuning.length;for(var f=0;f<b;f++){this.combBuffers.push(new Float32Array(this.combTuning[f]));this.combIndices.push(0);this.filterStores.push(0)}this.allPassBuffers=[];this.allPassIndices=[];var e=this.allPassTuning.length;for(var f=0;f<e;f++){this.allPassBuffers.push(new Float32Array(this.allPassTuning[f]));this.allPassIndices.push(0)}};extend(Reverb,AudioletNode);Reverb.prototype.generate=function(){var k=this.mix.getValue();var m=this.roomSize.getValue();var d=this.damping.getValue();var a=this.combTuning.length;var s=this.allPassTuning.length;var q=this.inputs[0].samples[0]||0;var j=q;q*=this.fixedGain;var c=q;var d=d*this.scaleDamping;var e=m*this.scaleRoom+this.offsetRoom;for(var r=0;r<a;r++){var o=this.combIndices[r];var p=this.combBuffers[r];var n=this.filterStores[r];var h=p[o];n=(h*(1-d))+(n*d);q+=h;p[o]=c+e*n;o+=1;if(o>=p.length){o=0}this.combIndices[r]=o;this.filterStores[r]=n}for(var r=0;r<s;r++){var b=this.allPassBuffers[r];var g=this.allPassIndices[r];var l=q;var f=b[g];q=-q+f;b[g]=l+(f*0.5);g+=1;if(g>=b.length){g=0}this.allPassIndices[r]=g}this.outputs[0].samples[0]=k*q+(1-k)*j};Reverb.prototype.toString=function(){return"Reverb"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Saw=function(a,b){AudioletNode.call(this,a,1,1);this.frequency=new AudioletParameter(this,0,b||440);this.phase=0};extend(Saw,AudioletNode);Saw.prototype.generate=function(){var a=this.outputs[0];var c=this.frequency.getValue();var b=this.audiolet.device.sampleRate;a.samples[0]=((this.phase/2+0.25)%0.5-0.25)*4;this.phase+=c/b;if(this.phase>1){this.phase%=1}};Saw.prototype.toString=function(){return"Saw"}; +/*! + * @depends ../core/AudioletNode.js + */ +var SoftClip=function(a){AudioletNode.call(this,a,1,1);this.linkNumberOfOutputChannels(0,0)};extend(SoftClip,AudioletNode);SoftClip.prototype.generate=function(){var b=this.inputs[0];var a=this.outputs[0];var c=b.samples.length;for(var d=0;d<c;d++){var e=b.samples[d];if(e>0.5||e<-0.5){a.samples[d]=(Math.abs(e)-0.25)/e}else{a.samples[d]=e}}};SoftClip.prototype.toString=function(){return("SoftClip")}; +/*! + * @depends ../core/AudioletNode.js + */ +var Square=function(a,b){AudioletNode.call(this,a,1,1);this.frequency=new AudioletParameter(this,0,b||440);this.phase=0};extend(Square,AudioletNode);Square.prototype.generate=function(){var a=this.outputs[0];var c=this.frequency.getValue();var b=this.audiolet.device.sampleRate;a.samples[0]=this.phase>0.5?1:-1;this.phase+=c/b;if(this.phase>1){this.phase%=1}};Square.prototype.toString=function(){return"Square"}; +/*! + * @depends ../core/AudioletNode.js + */ +var Triangle=function(a,b){AudioletNode.call(this,a,1,1);this.frequency=new AudioletParameter(this,0,b||440);this.phase=0};extend(Triangle,AudioletNode);Triangle.prototype.generate=function(){var a=this.outputs[0];var c=this.frequency.getValue();var b=this.audiolet.device.sampleRate;a.samples[0]=1-4*Math.abs((this.phase+0.25)%1-0.5);this.phase+=c/b;if(this.phase>1){this.phase%=1}};Triangle.prototype.toString=function(){return"Triangle"}; +/*! + * @depends ../core/AudioletNode.js + */ +var TriggerControl=function(b,a){AudioletNode.call(this,b,0,1);this.trigger=new AudioletParameter(this,null,a||0)};extend(TriggerControl,AudioletNode);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}};TriggerControl.prototype.toString=function(){return"Trigger Control"}; +/*! + * @depends ../core/AudioletNode.js + */ +var UpMixer=function(a,b){AudioletNode.call(this,a,1,1);this.outputs[0].numberOfChannels=b};extend(UpMixer,AudioletNode);UpMixer.prototype.generate=function(){var c=this.inputs[0];var b=this.outputs[0];var e=c.samples.length;var a=b.samples.length;if(e==a){b.samples=c.samples}else{for(var d=0;d<a;d++){b.samples[d]=c.samples[d%e]}}};UpMixer.prototype.toString=function(){return"UpMixer"};var WebKitBufferPlayer=function(a,b){AudioletNode.call(this,a,0,1);this.onComplete=b;this.isWebKit=this.audiolet.device.sink instanceof Sink.sinks.webkit;this.ready=false;this.setNumberOfOutputChannels(0,0);if(!this.isWebKit){return}this.context=this.audiolet.device.sink._context;this.jsNode=null;this.source=null;this.ready=false;this.loaded=false;this.buffers=[];this.readPosition=0;this.endTime=null};extend(WebKitBufferPlayer,AudioletNode);WebKitBufferPlayer.prototype.load=function(a,b,c){if(!this.isWebKit){return}this.stop();this.xhr=new XMLHttpRequest();this.xhr.open("GET",a,true);this.xhr.responseType="arraybuffer";this.xhr.onload=this.onLoad.bind(this,b,c);this.xhr.onerror=c;this.xhr.send()};WebKitBufferPlayer.prototype.stop=function(){this.ready=false;this.loaded=false;this.buffers=[];this.readPosition=0;this.endTime=null;this.setNumberOfOutputChannels(0);this.disconnectWebKitNodes()};WebKitBufferPlayer.prototype.disconnectWebKitNodes=function(){if(this.source&&this.jsNode){this.source.disconnect(this.jsNode);this.jsNode.disconnect(this.context.destination);this.source=null;this.jsNode=null}};WebKitBufferPlayer.prototype.onLoad=function(a,b){this.context.decodeAudioData(this.xhr.response,function(c){this.onDecode(c);a()}.bind(this),b)};WebKitBufferPlayer.prototype.onDecode=function(a){this.fileBuffer=a;this.source=this.context.createBufferSource();this.source.buffer=this.fileBuffer;var b=this.fileBuffer.numberOfChannels;this.setNumberOfOutputChannels(0,b);this.jsNode=this.context.createJavaScriptNode(4096,b,0);this.jsNode.onaudioprocess=this.onData.bind(this);this.source.connect(this.jsNode);this.jsNode.connect(this.context.destination);this.source.noteOn(0);this.endTime=this.context.currentTime+this.fileBuffer.duration;this.loaded=true};WebKitBufferPlayer.prototype.onData=function(c){if(this.loaded){this.ready=true}var a=c.inputBuffer.numberOfChannels;for(var b=0;b<a;b++){this.buffers[b]=c.inputBuffer.getChannelData(b);this.readPosition=0}};WebKitBufferPlayer.prototype.generate=function(){if(!this.ready){return}var a=this.outputs[0];var b=a.samples.length;for(var c=0;c<b;c++){a.samples[c]=this.buffers[c][this.readPosition]}this.readPosition+=1;if(this.context.currentTime>this.endTime){this.stop();this.onComplete()}}; +/*! + * @depends ../core/AudioletNode.js + */ +var WhiteNoise=function(a){AudioletNode.call(this,a,0,1)};extend(WhiteNoise,AudioletNode);WhiteNoise.prototype.generate=function(){this.outputs[0].samples[0]=Math.random()*2-1};WhiteNoise.prototype.toString=function(){return"White Noise"};var Pattern=function(){};Pattern.prototype.next=function(){return null};Pattern.prototype.valueOf=function(a){if(a instanceof Pattern){return(a.next())}else{return(a)}};Pattern.prototype.reset=function(){}; +/*! + * @depends Pattern.js + */ +var PArithmetic=function(c,b,a){Pattern.call(this);this.start=c;this.value=c;this.step=b;this.repeats=a;this.position=0};extend(PArithmetic,Pattern);PArithmetic.prototype.next=function(){var a;if(this.position==0){a=this.value;this.position+=1}else{if(this.position<this.repeats){var b=this.valueOf(this.step);if(b!=null){this.value+=b;a=this.value;this.position+=1}else{a=null}}else{a=null}}return(a)};PArithmetic.prototype.reset=function(){this.value=this.start;this.position=0;if(this.step instanceof Pattern){this.step.reset()}};var Pseries=PArithmetic; +/*! + * @depends Pattern.js + */ +var PChoose=function(b,a){Pattern.call(this);this.list=b;this.repeats=a||1;this.position=0};extend(PChoose,Pattern);PChoose.prototype.next=function(){var b;if(this.position<this.repeats){var a=Math.floor(Math.random()*this.list.length);var c=this.list[a];var d=this.valueOf(c);if(d!=null){if(!(c instanceof Pattern)){this.position+=1}b=d}else{if(c instanceof Pattern){c.reset()}this.position+=1;b=this.next()}}else{b=null}return(b)};PChoose.prototype.reset=function(){this.position=0;for(var a=0;a<this.list.length;a++){var b=this.list[a];if(b instanceof Pattern){b.reset()}}};var Prand=PChoose; +/*! + * @depends Pattern.js + */ +var PGeometric=function(c,b,a){Pattern.call(this);this.start=c;this.value=c;this.step=b;this.repeats=a;this.position=0};extend(PGeometric,Pattern);PGeometric.prototype.next=function(){var a;if(this.position==0){a=this.value;this.position+=1}else{if(this.position<this.repeats){var b=this.valueOf(this.step);if(b!=null){this.value*=b;a=this.value;this.position+=1}else{a=null}}else{a=null}}return(a)};PGeometric.prototype.reset=function(){this.value=this.start;this.position=0;if(this.step instanceof Pattern){this.step.reset()}};var Pgeom=PGeometric; +/*! + * @depends Pattern.js + */ +var PProxy=function(a){Pattern.call(this);if(a){this.pattern=a}};extend(PProxy,Pattern);PProxy.prototype.next=function(){var a;if(this.pattern){var a=this.pattern.next()}else{a=null}return a};var Pp=PProxy; +/*! + * @depends Pattern.js + */ +var PRandom=function(a,c,b){Pattern.call(this);this.low=a;this.high=c;this.repeats=b;this.position=0};extend(PRandom,Pattern);PRandom.prototype.next=function(){var b;if(this.position<this.repeats){var a=this.valueOf(this.low);var c=this.valueOf(this.high);if(a!=null&&c!=null){b=a+Math.random()*(c-a);this.position+=1}else{b=null}}else{b=null}return(b)};PRandom.prototype.reset=function(){this.position=0};var Pwhite=PRandom; +/*! + * @depends Pattern.js + */ +var PSequence=function(b,a,c){Pattern.call(this);this.list=b;this.repeats=a||1;this.position=0;this.offset=c||0};extend(PSequence,Pattern);PSequence.prototype.next=function(){var b;if(this.position<this.repeats*this.list.length){var a=(this.position+this.offset)%this.list.length;var c=this.list[a];var d=this.valueOf(c);if(d!=null){if(!(c instanceof Pattern)){this.position+=1}b=d}else{if(c instanceof Pattern){c.reset()}this.position+=1;b=this.next()}}else{b=null}return(b)};PSequence.prototype.reset=function(){this.position=0;for(var a=0;a<this.list.length;a++){var b=this.list[a];if(b instanceof Pattern){b.reset()}}};var Pseq=PSequence; +/*! + * @depends Pattern.js + */ +var PSeries=function(b,a,c){Pattern.call(this);this.list=b;this.repeats=a||1;this.position=0;this.offset=c||0};extend(PSeries,Pattern);PSeries.prototype.next=function(){var b;if(this.position<this.repeats){var a=(this.position+this.offset)%this.list.length;var c=this.list[a];var d=this.valueOf(c);if(d!=null){if(!(c instanceof Pattern)){this.position+=1}b=d}else{if(c instanceof Pattern){c.reset()}this.position+=1;b=this.next()}}else{b=null}return(b)};PSeries.prototype.reset=function(){this.position=0;for(var a=0;a<this.list.length;a++){var b=this.list[a];if(b instanceof Pattern){b.reset()}}};var Pser=PSeries; +/*! + * @depends Pattern.js + */ +var PShuffle=function(d,a){Pattern.call(this);this.list=[];while(d.length){var b=Math.floor(Math.random()*d.length);var c=d.splice(b,1);this.list.push(c)}this.repeats=a;this.position=0};extend(PShuffle,Pattern);PShuffle.prototype.next=function(){var b;if(this.position<this.repeats*this.list.length){var a=(this.position+this.offset)%this.list.length;var c=this.list[a];var d=this.valueOf(c);if(d!=null){if(!(c instanceof Pattern)){this.position+=1}b=d}else{if(c instanceof Pattern){c.reset()}this.position+=1;b=this.next()}}else{b=null}return(b)};var Pshuffle=PShuffle;var Scale=function(b,a){this.degrees=b;this.tuning=a||new EqualTemperamentTuning(12)};Scale.prototype.getFrequency=function(d,a,b){var c=a;b+=Math.floor(d/this.degrees.length);d%=this.degrees.length;c*=Math.pow(this.tuning.octaveRatio,b);c*=this.tuning.ratios[this.degrees[d]];return c}; +/*! + * @depends Scale.js + */ +var MajorScale=function(){Scale.call(this,[0,2,4,5,7,9,11])};extend(MajorScale,Scale); +/*! + * @depends Scale.js + */ +var MinorScale=function(){Scale.call(this,[0,2,3,5,7,8,10])};extend(MinorScale,Scale);var Tuning=function(d,c){this.semitones=d;this.octaveRatio=c||2;this.ratios=[];var b=this.semitones.length;for(var a=0;a<b;a++){this.ratios.push(Math.pow(2,this.semitones[a]/b))}}; +/*! + * @depends Tuning.js + */ +var EqualTemperamentTuning=function(b){var c=[];for(var a=0;a<b;a++){c.push(a)}Tuning.call(this,c,2)};extend(EqualTemperamentTuning,Tuning);var Sink=this.Sink=function(d){function c(j,h,l,f){var e=c.sinks.list,g;for(g=0;g<e.length;g++){if(e[g].enabled){try{return new e[g](j,h,l,f)}catch(k){}}}throw c.Error(2)}function b(){}c.SinkClass=b;b.prototype=c.prototype={sampleRate:44100,channelCount:2,bufferSize:4096,writePosition:0,previousHit:0,ringOffset:0,channelMode:"interleaved",isReady:false,start:function(g,f,h,e){this.channelCount=isNaN(f)||f===null?this.channelCount:f;this.bufferSize=isNaN(h)||h===null?this.bufferSize:h;this.sampleRate=isNaN(e)||e===null?this.sampleRate:e;this.readFn=g;this.activeRecordings=[];this.previousHit=+new Date();c.EventEmitter.call(this);c.emit("init",[this].concat([].slice.call(arguments)))},process:function(g,h){this.emit("preprocess",arguments);if(this.ringBuffer){(this.channelMode==="interleaved"?this.ringSpin:this.ringSpinInterleaved).apply(this,arguments)}if(this.channelMode==="interleaved"){this.emit("audioprocess",arguments);if(this.readFn){this.readFn.apply(this,arguments)}}else{var e=c.deinterleave(g,this.channelCount),f=[e].concat([].slice.call(arguments,1));this.emit("audioprocess",f);if(this.readFn){this.readFn.apply(this,f)}c.interleave(e,this.channelCount,g)}this.emit("postprocess",arguments);this.previousHit=+new Date();this.writePosition+=g.length/h},getPlaybackTime:function(){return this.writePosition-this.bufferSize},ready:function(){if(this.isReady){return}this.isReady=true;this.emit("ready",[])}};function a(l,j,f,h,g){f=f||j.prototype;j.prototype=new c.SinkClass();j.prototype.type=l;j.enabled=!h;var e;for(e in f){if(f.hasOwnProperty(e)){j.prototype[e]=f[e]}}a[l]=j;a.list[g?"unshift":"push"](j)}c.sinks=c.devices=a;c.sinks.list=[];c.singleton=function(){var e=c.apply(null,arguments);c.singleton=function(){return e};return e};d.Sink=c;return c}(function(){return this}());void function(a){function b(){var c;for(c in b.prototype){if(b.prototype.hasOwnProperty(c)){this[c]=b.prototype[c]}}this._listeners={}}b.prototype={_listeners:null,emit:function(d,c){if(this._listeners[d]){for(var e=0;e<this._listeners[d].length;e++){this._listeners[d][e].apply(this,c)}}return this},on:function(c,d){this._listeners[c]=this._listeners[c]||[];this._listeners[c].push(d);return this},off:function(c,e){if(this._listeners[c]){if(!e){delete this._listeners[c];return this}for(var d=0;d<this._listeners[c].length;d++){if(this._listeners[c][d]===e){this._listeners[c].splice(d--,1)}}if(!this._listeners[c].length){delete this._listeners[c]}}return this}};a.EventEmitter=b;b.call(a)}(this.Sink);void function(a){a.doInterval=function(f,d){var e,c;function b(g){if(a.inlineWorker.working&&!g){e=a.inlineWorker('setInterval(function (){ postMessage("tic"); }, '+d+");");e.onmessage=function(){f()};c=function(){e.terminate()}}else{e=setInterval(f,d);c=function(){clearInterval(e)}}}if(a.inlineWorker.ready){b()}else{a.inlineWorker.on("ready",function(){b()})}return function(){if(!c){if(!a.inlineWorker.ready){a.inlineWorker.on("ready",function(){if(c){c()}})}}else{c()}}}}(this.Sink);void function(d){var b,h,j,f;void function(k,m){function l(p,q){var n,o=q.slice();for(n=o.shift();typeof n!=="undefined";n=o.shift()){n=Function("return typeof "+n+p+'=== "undefined" ? undefined : '+n+p)();if(n){return n}}}b=l("Blob",k);h=l("BlobBuilder",k);j=l("URL",m);f=l("btoa",[""])}(["","Moz","WebKit","MS"],["","webkit"]);var g=b&&j&&function(l,k){return j.createObjectURL(new b([l],{type:k}))};var c=h&&j&&function(l,k){var m=new h();m.append(l);return j.createObjectURL(m.getBlob(k))};var a=f&&function(l,k){return"data:"+k+";base64,"+f(l)};var e=g||c||a;if(!e){return}if(g){e.createBlob=g}if(c){e.createBlobBuilder=c}if(a){e.createData=a}if(b){e.Blob=b}if(h){e.BlobBuilder=h}if(j){e.URL=j}d.createDynURL=e;d.revokeDynURL=function(k){if(typeof k==="string"&&k.indexOf("data:")===0){return false}else{return j.revokeObjectURL(k)}}}(this.Sink);void function(a){function b(d){if(!b.hasOwnProperty(d)){throw b(1)}if(!(this instanceof b)){return new b(d)}var c;for(c in b[d]){if(b[d].hasOwnProperty(c)){this[c]=b[d][c]}}this.code=d}b.prototype=new Error();b.prototype.toString=function(){return"SinkError 0x"+this.code.toString(16)+": "+this.message};b[1]={message:"No such error code.",explanation:"The error code does not exist."};b[2]={message:"No audio sink available.",explanation:"The audio device may be busy, or no supported output API is available for this browser."};b[16]={message:"Buffer underflow.",explanation:"Trying to recover..."};b[17]={message:"Critical recovery fail.",explanation:"The buffer underflow has reached a critical point, trying to recover, but will probably fail anyway."};b[18]={message:"Buffer size too large.",explanation:"Unable to allocate the buffer due to excessive length, please try a smaller buffer. Buffer size should probably be smaller than the sample rate."};a.Error=b}(this.Sink);void function(b){var d=Object.defineProperty?function(g,e,f){Object.defineProperty(g,e,{value:f,configurable:true,writable:true})}:function(g,e,f){g[e]=f};function c(){d(this,"terminate",this._terminate);b.revokeDynURL(this._url);delete this._url;delete this._terminate;return this.terminate()}function a(e){function f(l,m,j){try{var k=l(m,"text/javascript");var o=new Worker(k);d(o,"_url",k);d(o,"_terminate",o.terminate);d(o,"terminate",c);if(a.type){return o}a.type=j;a.createURL=l;return o}catch(n){return null}}var h=b.createDynURL;var g;if(a.createURL){return f(a.createURL,e,a.type)}g=f(h.createBlob,e,"blob");if(g){return g}g=f(h.createBlobBuilder,e,"blobbuilder");if(g){return g}g=f(h.createData,e,"data");return g}b.EventEmitter.call(a);a.test=function(){a.ready=a.working=false;a.type="";a.createURL=null;var g=a("this.onmessage=function(e){postMessage(e.data)}");var f="inlineWorker";function e(h){if(a.ready){return}a.ready=true;a.working=h;a.emit("ready",[h]);a.off("ready");if(h&&g){g.terminate()}g=null}if(!g){setTimeout(function(){e(false)},0)}else{g.onmessage=function(h){e(h.data===f)};g.postMessage(f);setTimeout(function(){e(false)},1000)}};b.inlineWorker=a;a.test()}(this.Sink);void function(a){a.sinks("audiodata",function(){var m=this,d=0,k=null,j=new Audio(),h,g,e,f,l,b;m.start.apply(m,arguments);m.preBufferSize=isNaN(arguments[4])||arguments[4]===null?this.preBufferSize:arguments[4];function c(){if(k){h=j.mozWriteAudio(k);d+=h;if(h<k.length){k=k.subarray(h);return k}k=null}g=j.mozCurrentSampleOffset();e=Number(g+(l!==g?m.bufferSize:m.preBufferSize)*m.channelCount-d);if(g===l){m.emit("error",[a.Error(16)])}if(e>0||l===g){m.ready();try{f=new Float32Array(l===g?m.preBufferSize*m.channelCount:m.forceBufferSize?e<m.bufferSize*2?m.bufferSize*2:e:e)}catch(n){m.emit("error",[a.Error(18)]);m.kill();return}m.process(f,m.channelCount);h=m._audio.mozWriteAudio(f);if(h<f.length){k=f.subarray(h)}d+=h}l=g}j.mozSetup(m.channelCount,m.sampleRate);this._timers=[];this._timers.push(a.doInterval(function(){if(+new Date()-m.previousHit>2000){m._audio=j=new Audio();j.mozSetup(m.channelCount,m.sampleRate);d=0;m.emit("error",[a.Error(17)])}},1000));this._timers.push(a.doInterval(c,m.interval));m._bufferFill=c;m._audio=j},{bufferSize:24576,preBufferSize:24576,forceBufferSize:false,interval:100,kill:function(){while(this._timers.length){this._timers.shift()()}this.emit("kill")},getPlaybackTime:function(){return this._audio.mozCurrentSampleOffset()/this.channelCount}},false,true);a.sinks.moz=a.sinks.audiodata}(this.Sink);void function(a){a.sinks("dummy",function(){var b=this;b.start.apply(b,arguments);function c(){var d=new Float32Array(b.bufferSize*b.channelCount);b.process(d,b.channelCount)}b._kill=a.doInterval(c,b.bufferSize/b.sampleRate*1000);b._callback=c},{kill:function(){this._kill();this.emit("kill")}},true)}(this.Sink);(function(b,a){a=b.sinks;function d(f){var e=document.createElement("audio");if(f){e.src=f}return e}a("wav",function(){var e=this,j=new a.wav.wavAudio(),k=typeof k==="undefined"?audioLib.PCMData:k;e.start.apply(e,arguments);var g=new Float32Array(e.bufferSize*e.channelCount),h=new Float32Array(e.bufferSize*e.channelCount);if(!d().canPlayType("audio/wav; codecs=1")||!btoa){throw 0}function f(){if(e._audio.hasNextFrame){return}e.ready();b.memcpy(h,0,g,0);e.process(g,e.channelCount);e._audio.setSource("data:audio/wav;base64,"+btoa(audioLib.PCMData.encode({data:g,sampleRate:e.sampleRate,channelCount:e.channelCount,bytesPerSample:e.quality})));if(!e._audio.currentFrame.src){e._audio.nextClip()}}e.kill=b.doInterval(f,40);e._bufferFill=f;e._audio=j},{quality:1,bufferSize:22050,getPlaybackTime:function(){var e=this._audio;return(e.currentFrame?e.currentFrame.currentTime*this.sampleRate:0)+e.samples}});function c(){var e=this;e.currentFrame=d();e.nextFrame=d();e._onended=function(){e.samples+=e.bufferSize;e.nextClip()}}c.prototype={samples:0,nextFrame:null,currentFrame:null,_onended:null,hasNextFrame:false,nextClip:function(){var e=this.currentFrame;this.currentFrame=this.nextFrame;this.nextFrame=e;this.hasNextFrame=false;this.currentFrame.play()},setSource:function(e){this.nextFrame.src=e;this.nextFrame.addEventListener("ended",this._onended,true);this.hasNextFrame=true}};a.wav.wavAudio=c}(this.Sink));(function(a,b){var c=typeof window==="undefined"?null:window.webkitAudioContext||window.AudioContext;a("webaudio",function(l,k,g,m){var n=this,d=a.webaudio.getContext(),j=null,h=null,f=null;n.start.apply(n,arguments);j=d.createJavaScriptNode(n.bufferSize,0,n.channelCount);function e(v){var o=v.outputBuffer,q=o.numberOfChannels,s,p,r=o.length,w=o.size,t=new Array(q),u;n.ready();h=h&&h.length===r*q?h:new Float32Array(r*q);f=f&&f.length===h.length?f:new Float32Array(r*q);h.set(f);for(s=0;s<q;s++){t[s]=o.getChannelData(s)}n.process(h,n.channelCount);for(s=0;s<r;s++){for(p=0;p<q;p++){t[p][s]=h[s*n.channelCount+p]}}}n.sampleRate=d.sampleRate;j.onaudioprocess=e;j.connect(d.destination);n._context=d;n._node=j;n._callback=e;b.push(j)},{kill:function(){this._node.disconnect(0);for(var d=0;d<b.length;d++){if(b[d]===this._node){b.splice(d--,1)}}this._node=this._context=null;this.emit("kill")},getPlaybackTime:function(){return this._context.currentTime*this.sampleRate}},false,true);a.webkit=a.webaudio;a.webaudio.fix82795=b;a.webaudio.getContext=function(){var d=new c();a.webaudio.getContext=function(){return d};return d}}(this.Sink.sinks,[]));(function(a){a.sinks("worker",function(){var b=this,e=(function(){return this}()),c=null,h=null,d=null;b.start.apply(b,arguments);importScripts();function f(o){if(!b.isReady){b.initMSP(o)}b.ready();var m=b.channelCount,j=o.audioLength,p,k;c=c&&c.length===j*m?c:new Float32Array(j*m);h=h&&h.length===c.length?h:new Float32Array(j*m);d=d&&d.length===c.length?d:new Float32Array(j*m);c.set(d);h.set(d);b.process(c,b.channelCount);for(p=0;p<m;p++){for(k=0;k<j;k++){h[p*o.audioLength+k]=c[p+k*m]}}o.writeAudio(h)}function g(s){if(!b.isReady){b.initWA(s)}b.ready();var j=s.outputBuffer,m=j.numberOfChannels,p,k,o=j.length,t=j.size,q=new Array(m),r;c=c&&c.length===o*m?c:new Float32Array(o*m);d=d&&d.length===c.length?d:new Float32Array(o*m);c.set(d);for(p=0;p<m;p++){q[p]=j.getChannelData(p)}b.process(c,b.channelCount);for(p=0;p<o;p++){for(k=0;k<m;k++){q[k][p]=c[p*b.channelCount+k]}}}e.onprocessmedia=f;e.onaudioprocess=g;b._mspBufferFill=f;b._waBufferFill=g},{ready:false,initMSP:function(b){this.channelCount=b.audioChannels;this.sampleRate=b.audioSampleRate;this.bufferSize=b.audioLength*this.channelCount;this.ready=true;this.emit("ready",[])},initWA:function(d){var c=d.outputBuffer;this.channelCount=c.numberOfChannels;this.sampleRate=c.sampleRate;this.bufferSize=c.length*this.channelCount;this.ready=true;this.emit("ready",[])}})}(this.Sink));(function(a){a.deinterleave=function(c,g){var b=c.length,f=b/g,d=[],e,h;for(e=0;e<g;e++){d[e]=new Float32Array(f);for(h=0;h<f;h++){d[e][h]=c[h*g+e]}}return d};a.interleave=function(d,g,c){g=g||d.length;var b=d[0].length,f=d.length,e,h;c=c||new Float32Array(b*g);for(e=0;e<f;e++){for(h=0;h<b;h++){c[e+h*g]=d[e][h]}}return c};a.mix=function(e){var d=[].slice.call(arguments,1),b,f,g;for(g=0;g<d.length;g++){b=Math.max(e.length,d[g].length);for(f=0;f<b;f++){e[f]+=d[g][f]}}return e};a.resetBuffer=function(c){var b=c.length,d;for(d=0;d<b;d++){c[d]=0}return c};a.clone=function(d,b){var c=d.length,e;b=b||new Float32Array(c);for(e=0;e<c;e++){b[e]=d[e]}return b};a.createDeinterleaved=function(e,d){var b=new Array(d),c;for(c=0;c<d;c++){b[c]=new Float32Array(e)}return b};a.memcpy=function(f,b,g,d,e){f=f.subarray||f.slice?f:f.buffer;g=g.subarray||g.slice?g:g.buffer;f=b?f.subarray?f.subarray(b,e&&b+e):f.slice(b,e&&b+e):f;if(g.set){g.set(f,d)}else{for(var c=0;c<f.length;c++){g[c+d]=f[c]}}return g};a.memslice=function(b,d,c){return b.subarray?b.subarray(d,c):b.slice(d,c)};a.mempad=function(b,c,d){c=c.length?c:new (b.constructor)(c);a.memcpy(b,0,c,d);return c};a.linspace=function(h,c,d){var b,e,g,f;d=d.length?(b=d.length)&&d:Array(b=d);f=(c-h)/--b;for(g=h+f,e=1;e<b;e++,g+=f){d[e]=g}d[0]=h;d[b]=c;return d};a.ftoi=function(d,f,c){var e,b=Math.pow(2,f-1);c=c||new (d.constructor)(d.length);for(e=0;e<d.length;e++){c[e]=~~(b*d[e])}return c}}(this.Sink));(function(a){function b(e,d){a.EventEmitter.call(this);this.bufferSize=isNaN(e)||e===null?this.bufferSize:e;this.channelCount=isNaN(d)||d===null?this.channelCount:d;var c=this;this.callback=function(){return c.process.apply(c,arguments)};this.resetBuffer()}b.prototype={buffer:null,zeroBuffer:null,parentSink:null,bufferSize:4096,channelCount:2,offset:null,resetBuffer:function(){this.buffer=new Float32Array(this.bufferSize);this.zeroBuffer=new Float32Array(this.bufferSize)},process:function(c,e){if(this.offset===null){this.loadBuffer()}for(var d=0;d<c.length;d++){if(this.offset>=this.buffer.length){this.loadBuffer()}c[d]=this.buffer[this.offset++]}},loadBuffer:function(){this.offset=0;a.memcpy(this.zeroBuffer,0,this.buffer,0);this.emit("audioprocess",[this.buffer,this.channelCount])}};a.Proxy=b;a.prototype.createProxy=function(d){var c=new a.Proxy(d,this.channelCount);c.parentSink=this;this.on("audioprocess",c.callback);return c}}(this.Sink));(function(a){(function(){function b(c,d){if(c&&d){b[c]=d}else{if(c&&b[c] instanceof Function){a.interpolate=b[c]}}return b[c]}a.interpolation=b;b("linear",function(c,g){var f=Math.floor(g),e=f+1,d=g-f;e=e<c.length?e:0;return c[f]*(1-d)+c[e]*d});b("nearest",function(c,d){return d>=c.length-0.5?c[0]:c[Math.round(d)]});b("linear")}());a.resample=function(j,b,o,p,d){var h=arguments.length,f=h===2?b:h===3?b/o:p/b*d/o,g=j.length,c=Math.ceil(g/f),m=new Float32Array(c),k,e;for(k=0,e=0;k<g;k+=f){m[e++]=a.interpolate(j,k)}return m}}(this.Sink));void function(b){b.on("init",function(c){c.activeRecordings=[];c.on("postprocess",c.recordData)});b.prototype.activeRecordings=null;b.prototype.record=function(){var c=new b.Recording(this);this.emit("record",[c]);return c};b.prototype.recordData=function(d){var f=this.activeRecordings,e,c=f.length;for(e=0;e<c;e++){f[e].add(d)}};function a(c){this.boundTo=c;this.buffers=[];c.activeRecordings.push(this)}a.prototype={add:function(c){this.buffers.push(c)},clear:function(){this.buffers=[]},stop:function(){var c=this.boundTo.activeRecordings,d;for(d=0;d<c.length;d++){if(c[d]===this){c.splice(d--,1)}}},join:function(){var g=0,h=0,e=this.buffers,c,j,f,d=e.length;for(f=0;f<d;f++){g+=e[f].length}c=new Float32Array(g);for(f=0;f<d;f++){for(j=0;j<e[f].length;j++){c[h+j]=e[f][j]}h+=e[f].length}return c}};b.Recording=a}(this.Sink);void function(b){function a(){if(this.ringBuffer){(this.channelMode==="interleaved"?this.ringSpin:this.ringSpinInterleaved).apply(this,arguments)}}b.on("init",function(c){c.on("preprocess",a)});b.prototype.ringBuffer=null;b.prototype.ringSpin=function(e){var f=this.ringBuffer,d=e.length,c=f.length,h=this.ringOffset,g;for(g=0;g<d;g++){e[g]+=f[h];h=(h+1)%c}this.ringOffset=h};b.prototype.ringSpinDeinterleaved=function(j){var h=this.ringBuffer,g=j.length,c=h.length,f=h[0].length,o=c*f,e=this.ringOffset,k,d;for(k=0;k<g;k+=c){for(d=0;d<c;d++){j[k+d]+=h[d][e]}e=(e+1)%f}this.ringOffset=d}}(this.Sink);void function(a,b){b=a.prototype;a.on("init",function(c){c.asyncBuffers=[];c.syncBuffers=[];c.on("preprocess",c.writeBuffersSync);c.on("postprocess",c.writeBuffersAsync)});b.writeMode="async";b.asyncBuffers=b.syncBuffers=null;b.writeBuffersAsync=function(e){var d=this.asyncBuffers,c=e.length,f,h,g,k,j;if(d){for(g=0;g<d.length;g++){f=d[g];h=f.b.length;j=f.d;f.d-=Math.min(j,c);for(k=0;k+j<c&&k<h;k++){e[k+j]+=f.b[k]}f.b=f.b.subarray(k+j);if(g>=h){d.splice(g--,1)}}}};b.writeBuffersSync=function(e){var d=this.syncBuffers,c=e.length,f=0,g=0;for(;f<c&&d.length;f++){e[f]+=d[0][g];if(d[0].length<=g){d.splice(0,1);g=0;continue}g++}if(d.length){d[0]=d[0].subarray(g)}};b.writeBufferAsync=function(d,e){d=this.mode==="deinterleaved"?a.interleave(d,this.channelCount):d;var c=this.asyncBuffers;c.push({b:d,d:isNaN(e)?~~((+new Date()-this.previousHit)/1000*this.sampleRate):e});return c.length};b.writeBufferSync=function(d){d=this.mode==="deinterleaved"?a.interleave(d,this.channelCount):d;var c=this.syncBuffers;c.push(d);return c.length};b.writeBuffer=function(){return this[this.writeMode==="async"?"writeBufferAsync":"writeBufferSync"].apply(this,arguments)};b.getSyncWriteOffset=function(){var c=this.syncBuffers,e=0,d;for(d=0;d<c.length;d++){e+=c[d].length}return e}}(this.Sink);
\ No newline at end of file |
