UNPKG

flocking

Version:

Creative audio synthesis for the Web

1,628 lines (1,348 loc) 49.6 kB
/* * 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?"); }; }());