flocking
Version:
Creative audio synthesis for the Web
1,628 lines (1,348 loc) • 49.6 kB
JavaScript
/*
* Flocking - Creative audio synthesis for the Web!
* https://github.com/continuing-creativity/flocking
*
* Copyright 2011-2018, Colin Clark
* Dual licensed under the MIT and GPL Version 2 licenses.
*/
/*global require, Float32Array, window, AudioContext, webkitAudioContext*/
/*jshint white: false, newcap: true, regexp: true, browser: true,
forin: false, nomen: true, bitwise: false, maxerr: 100,
indent: 4, plusplus: false, curly: true, eqeqeq: true,
freeze: true, latedef: true, noarg: true, nonew: true, quotmark: double, undef: true,
unused: true, strict: true, asi: false, boss: false, evil: false, expr: false,
funcscope: false*/
var fluid = fluid || require("infusion"),
flock = fluid.registerNamespace("flock");
(function () {
"use strict";
var $ = fluid.registerNamespace("jQuery");
flock.fluid = fluid;
flock.init = function (options) {
// TODO: Distribute these from top level on the environment to the audioSystem
// so that users can more easily specify them in their environment's defaults.
var enviroOpts = !options ? undefined : {
components: {
audioSystem: {
options: {
model: options
}
}
}
};
var enviro = flock.enviro(enviroOpts);
return enviro;
};
flock.ALL_CHANNELS = 32; // TODO: This should go.
flock.OUT_UGEN_ID = "flocking-out";
flock.PI = Math.PI;
flock.TWOPI = 2.0 * Math.PI;
flock.HALFPI = Math.PI / 2.0;
flock.LOG01 = Math.log(0.1);
flock.LOG001 = Math.log(0.001);
flock.ROOT2 = Math.sqrt(2);
flock.rates = {
AUDIO: "audio",
CONTROL: "control",
SCHEDULED: "scheduled",
DEMAND: "demand",
CONSTANT: "constant"
};
fluid.registerNamespace("flock.debug");
flock.debug.failHard = true;
flock.browser = function () {
if (typeof navigator === "undefined") {
return {};
}
// This is a modified version of jQuery's browser detection code,
// which they removed from jQuery 2.0.
// Some of us still have to live in the messy reality of the web.
var ua = navigator.userAgent.toLowerCase(),
browser = {},
match,
matched;
match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
/(msie) ([\w.]+)/.exec(ua) ||
ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || [];
matched = {
browser: match[1] || "",
version: match[2] || "0"
};
if (matched.browser) {
browser[matched.browser] = true;
browser.version = matched.version;
}
// Chrome is Webkit, but Webkit is also Safari.
if (browser.chrome) {
browser.webkit = true;
} else if (browser.webkit) {
browser.safari = true;
}
return browser;
};
// TODO: Move to components in the static environment and into the appropriate platform files.
fluid.registerNamespace("flock.platform");
flock.platform.isBrowser = typeof window !== "undefined";
flock.platform.hasRequire = typeof require !== "undefined";
flock.platform.os = flock.platform.isBrowser ? window.navigator.platform : require("os").platform();
flock.platform.isLinux = flock.platform.os.indexOf("Linux") > -1;
flock.platform.isAndroid = flock.platform.isLinux && flock.platform.os.indexOf("arm") > -1;
flock.platform.isIOS = flock.platform.os === "iPhone" || flock.platform.os === "iPad" || flock.platform.os === "iPod";
flock.platform.isMobile = flock.platform.isAndroid || flock.platform.isIOS;
flock.platform.browser = flock.browser();
flock.platform.isWebAudio = typeof AudioContext !== "undefined" || typeof webkitAudioContext !== "undefined";
flock.platform.audioEngine = flock.platform.isBrowser ? "webAudio" : "unknown";
if (flock.platform.browser && flock.platform.browser.version !== undefined) {
var dotIdx = flock.platform.browser.version.indexOf(".");
flock.platform.browser.majorVersionNumber = Number(dotIdx < 0 ?
flock.platform.browser.version :
flock.platform.browser.version.substring(0, dotIdx));
}
flock.shim = {
URL: flock.platform.isBrowser ? (window.URL || window.webkitURL || window.msURL) : undefined
};
flock.requireModule = function (moduleName, globalName) {
if (flock.platform.isBrowser) {
return window[globalName || moduleName];
}
if (!flock.platform.hasRequire) {
return undefined;
}
var resolvedName = flock.requireModule.paths[moduleName] || moduleName;
var togo = require(resolvedName);
return globalName ? togo[globalName] : togo;
};
flock.requireModule.paths = {
webarraymath: "../third-party/webarraymath/js/webarraymath.js",
Random: "../third-party/simjs/js/random-0.26.js"
};
/*************
* Utilities *
*************/
flock.noOp = function () {};
flock.isIterable = function (o) {
var type = typeof o;
return o && o.length !== undefined && type !== "string" && type !== "function";
};
flock.hasValue = function (obj, value) {
var found = false;
for (var key in obj) {
if (obj[key] === value) {
found = true;
break;
}
}
return found;
};
flock.hasTag = function (obj, tag) {
if (!obj || !tag) {
return false;
}
return obj.tags && obj.tags.indexOf(tag) > -1;
};
/**
* Returns a random number between the specified low and high values.
*
* For performance reasons, this function does not perform any type checks;
* you will need ensure that your low and high arguments are Numbers.
*
* @param low the minimum value
* @param high the maximum value
* @return a random value constrained to the specified range
*/
flock.randomValue = function (low, high) {
var scaled = high - low;
return Math.random() * scaled + low;
};
/**
* Produces a random number between -1.0 and 1.0.
*
* @return a random audio value
*/
flock.randomAudioValue = function () {
return Math.random() * 2.0 - 1.0;
};
flock.fillBuffer = function (buf, fillFn) {
for (var i = 0; i < buf.length; i++) {
buf[i] = fillFn(i, buf);
}
return buf;
};
flock.fillBufferWithValue = function (buf, value) {
for (var i = 0; i < buf.length; i++) {
buf[i] = value;
}
return buf;
};
flock.generateBuffer = function (length, fillFn) {
var buf = new Float32Array(length);
return flock.fillBuffer(buf, fillFn);
};
flock.generateBufferWithValue = function (length, value) {
var buf = new Float32Array(length);
return flock.fillBufferWithValue(buf, value);
};
// Deprecated. Will be removed in Flocking 0.3.0.
// Use the faster, non-polymorphic generate/fill functions instead.
flock.generate = function (length, fillFn) {
var isFn = typeof fillFn === "function",
isNum = typeof length === "number";
var generateFn = isFn ?
(isNum ? flock.generateBuffer : flock.fillBuffer) :
(isNum ? flock.generateBufferWithValue : flock.fillBufferWithValue);
return generateFn(length, fillFn);
};
flock.generate.silence = function (length) {
return new Float32Array(length);
};
flock.clearBuffer = function (buf) {
for (var i = 0; i < buf.length; i++) {
buf[i] = 0.0;
}
return buf;
};
/**
* Performs an in-place reversal of all items in the array.
*
* @arg {Iterable} b a buffer or array to reverse
* @return {Iterable} the buffer, reversed
*/
flock.reverse = function (b) {
if (!b || !flock.isIterable(b) || b.length < 2) {
return b;
}
// A native implementation of reverse() exists for regular JS arrays
// and is partially implemented for TypedArrays. Use it if possible.
if (typeof b.reverse === "function") {
return b.reverse();
}
var t;
for (var l = 0, r = b.length - 1; l < r; l++, r--) {
t = b[l];
b[l] = b[r];
b[r] = t;
}
return b;
};
/**
* Randomly selects an index from the specified array.
*/
flock.randomIndex = function (arr) {
var max = arr.length - 1;
return Math.round(Math.random() * max);
};
/**
* Selects an item from an array-like object using the specified strategy.
*
* @param {Array-like object} arr the array to choose from
* @param {Function} a selection strategy; defaults to flock.randomIndex
* @return a randomly selected list item
*/
flock.arrayChoose = function (arr, strategy) {
strategy = strategy || flock.randomIndex;
arr = fluid.makeArray(arr);
var idx = strategy(arr);
return arr[idx];
};
/**
* Randomly selects an item from an array or object.
*
* @param {Array-like object|Object} collection the object to choose from
* @return a randomly selected item from collection
*/
flock.choose = function (collection, strategy) {
var key, val;
if (flock.isIterable(collection)) {
val = flock.arrayChoose(collection, strategy);
return val;
}
key = flock.arrayChoose(collection.keys, strategy);
val = collection[key];
return val;
};
/**
* Shuffles an array-like object in place.
* Uses the Fisher-Yates/Durstenfeld/Knuth algorithm, which is
* described here:
* https://www.frankmitchell.org/2015/01/fisher-yates/
* and here:
* https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
*
* @param arr the array to shuffle
* @return the shuffled array
*/
// TODO: Unit tests!
flock.shuffle = function (arr) {
for (var i = arr.length - 1; i > 0; i -= 1) {
var j = Math.floor(Math.random() * (i + 1));
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
};
/**
* Normalizes the specified buffer in place to the specified value.
*
* @param {Arrayable} buffer the buffer to normalize
* @param {Number} normal the value to normalize the buffer to
* @param {Arrayable} a buffer to output values into; if omitted, buffer will be modified in place
* @return the buffer, normalized in place
*/
flock.normalize = function (buffer, normal, output) {
output = output || buffer;
var maxVal = 0.0,
i,
current,
val;
normal = normal === undefined ? 1.0 : normal;
// Find the maximum value in the buffer.
for (i = 0; i < buffer.length; i++) {
current = Math.abs(buffer[i]);
if (current > maxVal) {
maxVal = current;
}
}
// And then normalize the buffer in place.
if (maxVal > 0.0) {
for (i = 0; i < buffer.length; i++) {
val = buffer[i];
output[i] = (val / maxVal) * normal;
}
}
return output;
};
flock.generateFourierTable = function (size, scale, numHarms, phase, amps) {
phase *= flock.TWOPI;
return flock.generateBuffer(size, function (i) {
var harm,
amp,
w,
val = 0.0;
for (harm = 0; harm < numHarms; harm++) {
amp = amps ? amps[harm] : 1.0;
w = (harm + 1) * (i * scale);
val += amp * Math.cos(w + phase);
}
return val;
});
};
flock.generateNormalizedFourierTable = function (size, scale, numHarms, phase, ampGenFn) {
var amps = flock.generateBuffer(numHarms, function (harm) {
return ampGenFn(harm + 1); // Harmonics are indexed from 1 instead of 0.
});
var table = flock.generateFourierTable(size, scale, numHarms, phase, amps);
return flock.normalize(table);
};
flock.fillTable = function (sizeOrTable, fillFn) {
var len = typeof (sizeOrTable) === "number" ? sizeOrTable : sizeOrTable.length;
return fillFn(sizeOrTable, flock.TWOPI / len);
};
flock.tableGenerators = {
sin: function (size, scale) {
return flock.generateBuffer(size, function (i) {
return Math.sin(i * scale);
});
},
tri: function (size, scale) {
return flock.generateNormalizedFourierTable(size, scale, 1000, 1.0, function (harm) {
// Only odd harmonics,
// amplitudes decreasing by the inverse square of the harmonic number
return harm % 2 === 0 ? 0.0 : 1.0 / (harm * harm);
});
},
saw: function (size, scale) {
return flock.generateNormalizedFourierTable(size, scale, 10, -0.25, function (harm) {
// All harmonics,
// amplitudes decreasing by the inverse of the harmonic number
return 1.0 / harm;
});
},
square: function (size, scale) {
return flock.generateNormalizedFourierTable(size, scale, 10, -0.25, function (harm) {
// Only odd harmonics,
// amplitudes decreasing by the inverse of the harmonic number
return harm % 2 === 0 ? 0.0 : 1.0 / harm;
});
},
hann: function (size) {
// Hanning envelope: sin^2(i) for i from 0 to pi
return flock.generateBuffer(size, function (i) {
var y = Math.sin(Math.PI * i / size);
return y * y;
});
},
sinWindow: function (size) {
return flock.generateBuffer(size, function (i) {
return Math.sin(Math.PI * i / size);
});
}
};
flock.range = function (buf) {
var range = {
max: Number.NEGATIVE_INFINITY,
min: Infinity
};
var i, val;
for (i = 0; i < buf.length; i++) {
val = buf[i];
if (val > range.max) {
range.max = val;
}
if (val < range.min) {
range.min = val;
}
}
return range;
};
flock.scale = function (buf) {
if (!buf) {
return;
}
var range = flock.range(buf),
mul = (range.max - range.min) / 2,
sub = (range.max + range.min) / 2,
i;
for (i = 0; i < buf.length; i++) {
buf[i] = (buf[i] - sub) / mul;
}
return buf;
};
flock.copyBuffer = function (buffer, start, end) {
if (end === undefined) {
end = buffer.length;
}
var len = end - start,
target = new Float32Array(len),
i,
j;
for (i = start, j = 0; i < end; i++, j++) {
target[j] = buffer[i];
}
return target;
};
flock.copyToBuffer = function (source, target) {
var len = Math.min(source.length, target.length);
for (var i = 0; i < len; i++) {
target[i] = source[i];
}
};
flock.parseMidiString = function (midiStr) {
if (!midiStr || midiStr.length < 2) {
return NaN;
}
midiStr = midiStr.toLowerCase();
var secondChar = midiStr.charAt(1),
splitIdx = secondChar === "#" || secondChar === "b" ? 2 : 1,
note = midiStr.substring(0, splitIdx),
octave = Number(midiStr.substring(splitIdx)),
pitchClass = flock.midiFreq.noteNames[note],
midiNum = octave * 12 + pitchClass;
return midiNum;
};
flock.midiFreq = function (midi, a4Freq, a4NoteNum, notesPerOctave) {
a4Freq = a4Freq === undefined ? 440 : a4Freq;
a4NoteNum = a4NoteNum === undefined ? 69 : a4NoteNum;
notesPerOctave = notesPerOctave || 12;
if (typeof midi === "string") {
midi = flock.parseMidiString(midi);
}
return a4Freq * Math.pow(2, (midi - a4NoteNum) * 1 / notesPerOctave);
};
flock.midiFreq.noteNames = {
"b#": 0,
"c": 0,
"c#": 1,
"db": 1,
"d": 2,
"d#": 3,
"eb": 3,
"e": 4,
"e#": 5,
"f": 5,
"f#": 6,
"gb": 6,
"g": 7,
"g#": 8,
"ab": 8,
"a": 9,
"a#": 10,
"bb": 10,
"b": 11,
"cb": 11
};
flock.interpolate = {
/**
* Performs simple truncation.
*/
none: function (idx, table) {
idx = idx % table.length;
return table[idx | 0];
},
/**
* Performs linear interpolation.
*/
linear: function (idx, table) {
var len = table.length;
idx = idx % len;
var i1 = idx | 0,
i2 = (i1 + 1) % len,
frac = idx - i1,
y1 = table[i1],
y2 = table[i2];
return y1 + frac * (y2 - y1);
},
/**
* Performs Hermite cubic interpolation.
*
* Based on Laurent De Soras' implementation at:
* http://www.musicdsp.org/showArchiveComment.php?ArchiveID=93
*
* @param idx {Number} an index into the table
* @param table {Arrayable} the table from which values around idx should be drawn and interpolated
* @return {Number} an interpolated value
*/
hermite: function (idx, table) {
var len = table.length,
intPortion = Math.floor(idx),
i0 = intPortion % len,
frac = idx - intPortion,
im1 = i0 > 0 ? i0 - 1 : len - 1,
i1 = (i0 + 1) % len,
i2 = (i0 + 2) % len,
xm1 = table[im1],
x0 = table[i0],
x1 = table[i1],
x2 = table[i2],
c = (x1 - xm1) * 0.5,
v = x0 - x1,
w = c + v,
a = w + v + (x2 - x0) * 0.5,
bNeg = w + a,
val = (((a * frac) - bNeg) * frac + c) * frac + x0;
return val;
}
};
flock.interpolate.cubic = flock.interpolate.hermite;
flock.log = {
fail: function (msg) {
fluid.log(fluid.logLevel.FAIL, msg);
},
warn: function (msg) {
fluid.log(fluid.logLevel.WARN, msg);
},
debug: function (msg) {
fluid.log(fluid.logLevel.INFO, msg);
}
};
flock.fail = function (e) {
if (flock.debug.failHard) {
e = e instanceof Error ? e : new Error(e);
throw e;
} else {
flock.log.fail(e);
}
};
flock.pathParseError = function (root, path, token) {
var msg = "Error parsing path '" + path + "'. Segment '" + token +
"' could not be resolved.";
flock.fail(msg);
};
flock.get = function (root, path) {
if (!root) {
return fluid.getGlobalValue(path);
}
if (arguments.length === 1 && typeof root === "string") {
return fluid.getGlobalValue(root);
}
if (!path || path === "") {
return;
}
var tokenized = path === "" ? [] : String(path).split("."),
valForSeg = root[tokenized[0]],
i;
for (i = 1; i < tokenized.length; i++) {
if (valForSeg === null || valForSeg === undefined) {
flock.pathParseError(root, path, tokenized[i - 1]);
return;
}
valForSeg = valForSeg[tokenized[i]];
}
return valForSeg;
};
flock.set = function (root, path, value) {
if (!root || !path || path === "") {
return;
}
var tokenized = String(path).split("."),
l = tokenized.length,
prop = tokenized[0],
i,
type;
for (i = 1; i < l; i++) {
root = root[prop];
type = typeof root;
if (type !== "object") {
flock.fail("Error while setting a value at path '" + path +
"'. A non-container object was found at segment '" + prop + "'. Value: " + root);
return;
}
prop = tokenized[i];
if (root[prop] === undefined) {
root[prop] = {};
}
}
root[prop] = value;
return value;
};
flock.invoke = function (root, path, args) {
var fn = typeof root === "function" ? root : flock.get(root, path);
if (typeof fn !== "function") {
flock.fail("Path '" + path + "' does not resolve to a function.");
return;
}
return fn.apply(null, args);
};
flock.input = {};
flock.input.shouldExpand = function (inputName) {
return flock.parse.specialInputs.indexOf(inputName) < 0;
};
// TODO: Replace this with a regular expression;
// this produces too much garbage!
flock.input.pathExpander = function (path) {
var segs = fluid.model.parseEL(path),
separator = "inputs",
len = segs.length,
penIdx = len - 1,
togo = [],
i;
for (i = 0; i < penIdx; i++) {
var seg = segs[i];
var nextSeg = segs[i + 1];
togo.push(seg);
if (nextSeg === "model" || nextSeg === "options") {
togo = togo.concat(segs.slice(i + 1, penIdx));
break;
}
if (!isNaN(Number(nextSeg))) {
continue;
}
togo.push(separator);
}
togo.push(segs[penIdx]);
return togo.join(".");
};
flock.input.expandPaths = function (paths) {
var expanded = {},
path,
expandedPath,
value;
for (path in paths) {
expandedPath = flock.input.pathExpander(path);
value = paths[path];
expanded[expandedPath] = value;
}
return expanded;
};
flock.input.expandPath = function (path) {
return (typeof path === "string") ? flock.input.pathExpander(path) : flock.input.expandPaths(path);
};
flock.input.getValueForPath = function (root, path) {
path = flock.input.expandPath(path);
var input = flock.get(root, path);
// If the unit generator is a valueType ugen, return its value, otherwise return the ugen itself.
return flock.hasTag(input, "flock.ugen.valueType") ? input.inputs.value : input;
};
flock.input.getValuesForPathArray = function (root, paths) {
var values = {},
i,
path;
for (i = 0; i < paths.length; i++) {
path = paths[i];
values[path] = flock.input.get(root, path);
}
return values;
};
flock.input.getValuesForPathObject = function (root, pathObj) {
var key;
for (key in pathObj) {
pathObj[key] = flock.input.get(root, key);
}
return pathObj;
};
/**
* Gets the value of the ugen at the specified path.
*
* @param {String} path the ugen's path within the synth graph
* @return {Number|UGen} a scalar value in the case of a value ugen, otherwise the ugen itself
*/
flock.input.get = function (root, path) {
return typeof path === "string" ? flock.input.getValueForPath(root, path) :
flock.isIterable(path) ? flock.input.getValuesForPathArray(root, path) :
flock.input.getValuesForPathObject(root, path);
};
flock.input.resolveValue = function (root, path, val, target, inputName, previousInput, valueParser) {
// Check to see if the value is actually a "get expression"
// (i.e. an EL path wrapped in ${}) and resolve it if necessary.
if (typeof val === "string") {
var extracted = fluid.extractEL(val, flock.input.valueExpressionSpec);
if (extracted) {
var resolved = flock.input.getValueForPath(root, extracted);
if (resolved === undefined) {
flock.log.debug("The value expression '" + val + "' resolved to undefined. " +
"If this isn't expected, check to ensure that your path is valid.");
}
return resolved;
}
}
return flock.input.shouldExpand(inputName) && valueParser ?
valueParser(val, path, target, previousInput) : val;
};
flock.input.valueExpressionSpec = {
ELstyle: "${}"
};
flock.input.setValueForPath = function (root, path, val, baseTarget, valueParser) {
path = flock.input.expandPath(path);
var previousInput = flock.get(root, path),
lastDotIdx = path.lastIndexOf("."),
inputName = path.slice(lastDotIdx + 1),
target = lastDotIdx > -1 ? flock.get(root, path.slice(0, path.lastIndexOf(".inputs"))) :
baseTarget,
resolvedVal = flock.input.resolveValue(root, path, val, target, inputName, previousInput, valueParser);
flock.set(root, path, resolvedVal);
if (target && target.onInputChanged) {
target.onInputChanged(inputName);
}
return resolvedVal;
};
flock.input.setValuesForPaths = function (root, valueMap, baseTarget, valueParser) {
var resultMap = {},
path,
val,
result;
for (path in valueMap) {
val = valueMap[path];
result = flock.input.set(root, path, val, baseTarget, valueParser);
resultMap[path] = result;
}
return resultMap;
};
/**
* Sets the value of the ugen at the specified path.
*
* @param {String} path the ugen's path within the synth graph
* @param {Number || UGenDef} val a scalar value (for Value ugens) or a UGenDef object
* @return {UGen} the newly created UGen that was set at the specified path
*/
flock.input.set = function (root, path, val, baseTarget, valueParser) {
return typeof path === "string" ?
flock.input.setValueForPath(root, path, val, baseTarget, valueParser) :
flock.input.setValuesForPaths(root, path, baseTarget, valueParser);
};
/***********************
* Synths and Playback *
***********************/
fluid.defaults("flock.audioSystem", {
gradeNames: ["fluid.modelComponent"],
channelRange: {
min: 1,
max: 32
},
outputBusRange: {
min: 2,
max: 1024
},
inputBusRange: {
min: 1, // TODO: This constraint should be removed.
max: 32
},
model: {
rates: {
audio: 44100,
control: 689.0625,
scheduled: 0,
demand: 0,
constant: 0
},
blockSize: 64,
numBlocks: 16, // TODO: Move this and its transform into the web/output-manager.js
chans: 2,
numInputBuses: 2,
numBuses: 8,
bufferSize: "@expand:flock.audioSystem.defaultBufferSize()"
},
modelRelay: [
{
target: "rates.control",
singleTransform: {
type: "fluid.transforms.binaryOp",
left: "{that}.model.rates.audio",
operator: "/",
right: "{that}.model.blockSize"
}
},
{
target: "numBlocks",
singleTransform: {
type: "fluid.transforms.binaryOp",
left: "{that}.model.bufferSize",
operator: "/",
right: "{that}.model.blockSize"
}
},
{
target: "chans",
singleTransform: {
type: "fluid.transforms.limitRange",
input: "{that}.model.chans",
min: "{that}.options.channelRange.min",
max: "{that}.options.channelRange.max"
}
},
{
target: "numInputBuses",
singleTransform: {
type: "fluid.transforms.limitRange",
input: "{that}.model.numInputBuses",
min: "{that}.options.inputBusRange.min",
max: "{that}.options.inputBusRange.max"
}
},
{
target: "numBuses",
singleTransform: {
type: "fluid.transforms.free",
func: "flock.audioSystem.clampNumBuses",
args: [
"{that}.model.numBuses",
"{that}.options.outputBusRange",
"{that}.model.chans",
"{that}.model.numInputBuses"
]
}
}
]
});
flock.audioSystem.clampNumBuses = function (numBuses, outputBusRange, chans, numInputBuses) {
var numInOut = numInputBuses + chans;
numBuses = Math.max(numBuses, numInOut);
numBuses = Math.max(numBuses, Math.max(chans, outputBusRange.min));
numBuses = Math.min(numBuses, outputBusRange.max);
return numBuses;
};
flock.audioSystem.defaultBufferSize = function () {
return flock.platform.isMobile ? 8192 :
flock.platform.browser.mozilla ? 2048 : 1024;
};
// TODO: Refactor how buses work so that they're clearly
// delineated into types--input, output, and interconnect.
// TODO: Get rid of the concept of buses altogether.
fluid.defaults("flock.busManager", {
gradeNames: ["fluid.modelComponent"],
model: {
nextAvailableBus: {
input: 0,
interconnect: 0
}
},
members: {
buses: {
expander: {
funcName: "flock.enviro.createAudioBuffers",
args: ["{audioSystem}.model.numBuses", "{audioSystem}.model.blockSize"]
}
}
},
invokers: {
acquireNextBus: {
funcName: "flock.busManager.acquireNextBus",
args: [
"{arguments}.0", // The type of bus, either "input" or "interconnect".
"{that}.buses",
"{that}.applier",
"{that}.model",
"{audioSystem}.model.chans",
"{audioSystem}.model.numInputBuses"
]
},
reset: {
changePath: "nextAvailableBus",
value: {
input: 0,
interconnect: 0
}
}
},
listeners: {
"onDestroy.reset": "{that}.reset()"
}
});
flock.busManager.acquireNextBus = function (type, buses, applier, m, chans, numInputBuses) {
var busNum = m.nextAvailableBus[type];
if (busNum === undefined) {
flock.fail("An invalid bus type was specified when invoking " +
"flock.busManager.acquireNextBus(). Type was: " + type);
return;
}
// Input buses start immediately after the output buses.
var offsetBusNum = busNum + chans,
offsetBusMax = chans + numInputBuses;
// Interconnect buses are after the input buses.
if (type === "interconnect") {
offsetBusNum += numInputBuses;
offsetBusMax = buses.length;
}
if (offsetBusNum >= offsetBusMax) {
flock.fail("Unable to aquire a bus. There are insufficient buses available. " +
"Please use an existing bus or configure additional buses using the enviroment's " +
"numBuses and numInputBuses parameters.");
return;
}
applier.change("nextAvailableBus." + type, ++busNum);
return offsetBusNum;
};
fluid.defaults("flock.outputManager", {
gradeNames: ["fluid.modelComponent"],
model: {
audioSettings: "{audioSystem}.model"
},
invokers: {
start: "{that}.events.onStart.fire()",
stop: "{that}.events.onStop.fire()",
reset: "{that}.events.onReset.fire"
},
events: {
onStart: "{enviro}.events.onStart",
onStop: "{enviro}.events.onStop",
onReset: "{enviro}.events.onReset"
}
});
fluid.defaults("flock.nodeListComponent", {
gradeNames: "fluid.component",
members: {
nodeList: "@expand:flock.nodeList()"
},
invokers: {
/**
* Inserts a new node at the specified index.
*
* @param {flock.node} nodeToInsert the node to insert
* @param {Number} index the index to insert it at
* @return {Number} the index at which the new node was added
*/
insert: "flock.nodeList.insert({that}.nodeList, {arguments}.0, {arguments}.1)",
/**
* Inserts a new node at the head of the node list.
*
* @param {flock.node} nodeToInsert the node to insert
* @return {Number} the index at which the new node was added
*/
head: "flock.nodeList.head({that}.nodeList, {arguments}.0)",
/**
* Inserts a new node at the head of the node list.
*
* @param {flock.node} nodeToInsert the node to insert
* @return {Number} the index at which the new node was added
*/
tail: "flock.nodeList.tail({that}.nodeList, {arguments}.0)",
/**
* Adds a node before another node.
*
* @param {flock.node} nodeToInsert the node to add
* @param {flock.node} targetNode the node to insert the new one before
* @return {Number} the index the new node was added at
*/
before: "flock.nodeList.before({that}.nodeList, {arguments}.0, {arguments}.1)",
/**
* Adds a node after another node.
*
* @param {flock.node} nodeToInsert the node to add
* @param {flock.node} targetNode the node to insert the new one after
* @return {Number} the index the new node was added at
*/
after: "flock.nodeList.after({that}.nodeList, {arguments}.0, {arguments}.1)",
/**
* Removes the specified node.
*
* @param {flock.node} nodeToRemove the node to remove
* @return {Number} the index of the removed node
*/
remove: "flock.nodeList.remove({that}.nodeList, {arguments}.0)",
/**
* Replaces a node with another, removing the old one and adding the new one.
*
* @param {flock.node} nodeToInsert the node to add
* @param {flock.node} nodeToReplace the node to replace
* @return {Number} the index the new node was added at
*/
replace: "flock.nodeList.after({that}.nodeList, {arguments}.0, {arguments}.1)"
}
});
// TODO: Factor out buffer logic into a separate component.
fluid.defaults("flock.enviro", {
gradeNames: [
"fluid.modelComponent",
"flock.nodeListComponent",
"fluid.resolveRootSingle"
],
singleRootType: "flock.enviro",
isGlobalSingleton: true,
members: {
buffers: {},
bufferSources: {}
},
components: {
asyncScheduler: {
type: "flock.scheduler.async"
},
audioSystem: {
type: "flock.audioSystem"
},
busManager: {
type: "flock.busManager"
}
},
model: {
isPlaying: false
},
invokers: {
/**
* Generates a block of samples by evaluating all registered nodes.
*/
generate: {
funcName: "flock.enviro.generate",
args: ["{busManager}.buses", "{audioSystem}.model", "{that}.nodeList.nodes"]
},
/**
* Starts generating samples from all synths.
*
* @param {Number} dur optional duration to play in seconds
*/
start: "flock.enviro.start({that}.model, {that}.events.onStart.fire)",
/**
* Deprecated. Use start() instead.
*/
play: "{that}.start",
/**
* Stops generating samples.
*/
stop: "flock.enviro.stop({that}.model, {that}.events.onStop.fire)",
/**
* Fully resets the state of the environment.
*/
reset: "{that}.events.onReset.fire()",
/**
* Registers a shared buffer.
*
* @param {BufferDesc} bufDesc the buffer description object to register
*/
registerBuffer: "flock.enviro.registerBuffer({arguments}.0, {that}.buffers)",
/**
* Releases a shared buffer.
*
* @param {String|BufferDesc} bufDesc the buffer description (or string id) to release
*/
releaseBuffer: "flock.enviro.releaseBuffer({arguments}.0, {that}.buffers)",
/**
* Saves a buffer to the user's computer.
*
* @param {String|BufferDesc} id the id of the buffer to save
* @param {String} path the path to save the buffer to (if relevant)
*/
saveBuffer: {
funcName: "flock.enviro.saveBuffer",
args: [
"{arguments}.0",
"{that}.buffers",
"{audioSystem}"
]
}
},
events: {
onStart: null,
onPlay: "{that}.events.onStart", // Deprecated. Use onStart instead.
onStop: null,
onReset: null
},
listeners: {
"onCreate.registerSingleton": {
funcName: "flock.enviro.registerGlobalSingleton",
args: ["{that}"]
},
"onStart.updatePlayState": {
changePath: "isPlaying",
value: true
},
"onStop.updatePlayState": {
changePath: "isPlaying",
value: false
},
"onReset.stop": "{that}.stop()",
"onReset.clearScheduler": {
priority: "after:stop",
func: "{asyncScheduler}.clearAll"
},
"onReset.clearAllNodes": {
priority: "after:clearScheduler",
func: "flock.nodeList.clearAll",
args: ["{that}.nodeList"]
},
"onReset.resetBusManager": {
priority: "after:clearAllNodes",
func: "{busManager}.reset"
},
"onReset.clearBuffers": {
priority: "after:resetBusManager",
funcName: "fluid.clear",
args: ["{that}.buffers"]
}
}
});
flock.enviro.registerGlobalSingleton = function (that) {
if (that.options.isGlobalSingleton) {
// flock.enviro.shared is deprecated. Use "flock.environment"
// or an IoC reference to {flock.enviro} instead
flock.environment = flock.enviro.shared = that;
}
};
flock.enviro.registerBuffer = function (bufDesc, buffers) {
if (bufDesc.id) {
buffers[bufDesc.id] = bufDesc;
}
};
flock.enviro.releaseBuffer = function (bufDesc, buffers) {
if (!bufDesc) {
return;
}
var id = typeof bufDesc === "string" ? bufDesc : bufDesc.id;
delete buffers[id];
};
flock.enviro.saveBuffer = function (o, buffers, audioSystem) {
if (typeof o === "string") {
o = {
buffer: o
};
}
if (typeof o.buffer === "string") {
var id = o.buffer;
o.buffer = buffers[id];
o.buffer.id = id;
}
o.type = o.type || "wav";
o.path = o.path || o.buffer.id + "." + o.type;
o.format = o.format || "int16";
return audioSystem.bufferWriter.save(o, o.buffer);
};
flock.enviro.generate = function (buses, audioSettings, nodes) {
flock.evaluate.clearBuses(buses,
audioSettings.numBuses, audioSettings.blockSize);
flock.evaluate.synths(nodes);
};
flock.enviro.start = function (model, onStart) {
if (!model.isPlaying) {
onStart();
}
};
flock.enviro.stop = function (model, onStop) {
if (model.isPlaying) {
onStop();
}
};
flock.enviro.createAudioBuffers = function (numBufs, blockSize) {
var bufs = [],
i;
for (i = 0; i < numBufs; i++) {
bufs[i] = new Float32Array(blockSize);
}
return bufs;
};
fluid.defaults("flock.autoEnviro", {
gradeNames: ["fluid.component"],
members: {
enviro: "@expand:flock.autoEnviro.initEnvironment()"
}
});
flock.autoEnviro.initEnvironment = function () {
// TODO: The last vestige of globalism! Remove reference to shared environment.
return !flock.environment ? flock.init() : flock.environment;
};
/**
* An environment grade that is configured to always output
* silence using a Web Audio GainNode. This is useful for unit testing,
* where failures could produce painful or unexpected output.
*/
fluid.defaults("flock.silentEnviro", {
gradeNames: "flock.enviro",
listeners: {
"onCreate.insertGainNode": {
funcName: "flock.silentEnviro.insertOutputGainNode",
args: "{that}"
}
}
});
flock.silentEnviro.insertOutputGainNode = function (that) {
if (that.audioSystem.nativeNodeManager) {
that.audioSystem.nativeNodeManager.createOutputNode({
node: "Gain",
params: {
gain: 0
}
});
}
};
fluid.defaults("flock.node", {
gradeNames: ["flock.autoEnviro", "fluid.modelComponent"],
addToEnvironment: "tail",
model: {},
members: {
generatorFunc: "@expand:fluid.getGlobalValue({that}.options.invokers.generate.funcName)"
},
components: {
enviro: "{flock.enviro}"
},
invokers: {
/**
* Plays the node. This is a convenience method that will add the
* node to the tail of the environment's node graph and then play
* the environmnent.
*
* @param {Number} dur optional duration to play this synth in seconds
*/
play: {
funcName: "flock.node.play",
args: ["{that}", "{that}.enviro", "{that}.addToEnvironment"]
},
/**
* Stops the synth if it is currently playing.
* This is a convenience method that will remove the synth from the environment's node graph.
*/
pause: "{that}.removeFromEnvironment()",
/**
* Adds the node to its environment's list of active nodes.
*
* @param {String || Boolean || Number} position the place to insert the node at;
* if undefined, the node's addToEnvironment option will be used.
*/
addToEnvironment: {
funcName: "flock.node.addToEnvironment",
args: ["{that}", "{arguments}.0", "{that}.enviro.nodeList"]
},
/**
* Removes the node from its environment's list of active nodes.
*/
removeFromEnvironment: {
funcName: "flock.node.removeFromEnvironment",
args: ["{that}", "{that}.enviro.nodeList"]
},
/**
* Returns a boolean describing if this node is currently
* active in its environment's list of nodes
* (i.e. if it is currently generating samples).
*/
isPlaying: {
funcName: "flock.nodeList.isNodeActive",
args:["{that}.enviro.nodeList", "{that}"]
},
generate: {
funcName: "fluid.identity"
}
},
listeners: {
"onCreate.addToEnvironment": {
func: "{that}.addToEnvironment",
args: ["{that}.options.addToEnvironment"]
},
"onDestroy.removeFromEnvironment": {
func: "{that}.removeFromEnvironment"
}
}
});
flock.node.addToEnvironment = function (node, position, nodeList) {
if (position === undefined) {
position = node.options.addToEnvironment;
}
// Add this node to the tail of the synthesis environment if appropriate.
if (position === undefined || position === null || position === false) {
return;
}
var type = typeof (position);
if (type === "string" && position === "head" || position === "tail") {
flock.nodeList[position](nodeList, node);
} else if (type === "number") {
flock.nodeList.insert(nodeList, node, position);
} else {
flock.nodeList.tail(nodeList, node);
}
};
flock.node.removeFromEnvironment = function (node, nodeList) {
flock.nodeList.remove(nodeList, node);
};
flock.node.play = function (node, enviro, addToEnviroFn) {
if (enviro.nodeList.nodes.indexOf(node) === -1) {
var position = node.options.addToEnvironment || "tail";
addToEnviroFn(position);
}
// TODO: This behaviour is confusing
// since calling mySynth.play() will cause
// all synths in the environment to be played.
// This functionality should be removed.
if (!enviro.model.isPlaying) {
enviro.play();
}
};
fluid.defaults("flock.noteTarget", {
gradeNames: "fluid.component",
noteChanges: {
on: {
"env.gate": 1
},
off: {
"env.gate": 0
}
},
invokers: {
set: {
funcName: "fluid.notImplemented"
},
noteOn: {
func: "{that}.events.noteOn.fire"
},
noteOff: {
func: "{that}.events.noteOff.fire"
},
noteChange: {
funcName: "flock.noteTarget.change",
args: [
"{that}",
"{arguments}.0", // The type of note; either "on" or "off"
"{arguments}.1" // The change to apply for this note.
]
}
},
events: {
noteOn: null,
noteOff: null
},
listeners: {
"noteOn.handleChange": [
"{that}.noteChange(on, {arguments}.0)"
],
"noteOff.handleChange": [
"{that}.noteChange(off, {arguments}.0)"
]
}
});
flock.noteTarget.change = function (that, type, changeSpec) {
var baseChange = that.options.noteChanges[type];
var mergedChange = $.extend({}, baseChange, changeSpec);
that.set(mergedChange);
};
/*******************************
* Error Handling Conveniences *
*******************************/
flock.bufferDesc = function () {
throw new Error("flock.bufferDesc is not defined. Did you forget to include the buffers.js file?");
};
}());