UNPKG

flocking

Version:

Creative audio synthesis for the Web

777 lines (627 loc) 23.9 kB
/* * Flocking Core Unit Generators * https://github.com/continuing-creativity/flocking * * Copyright 2011-2014, Colin Clark * Dual licensed under the MIT and GPL Version 2 licenses. */ /*global require*/ /*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.ugenDefaults = function (path, defaults) { if (arguments.length === 1) { return flock.ugenDefaults.store[path]; } flock.ugenDefaults.store[path] = defaults; return defaults; }; flock.ugenDefaults.store = {}; flock.isUGen = function (obj) { return obj && obj.tags && obj.tags.indexOf("flock.ugen") > -1; }; // TODO: Check API; write unit tests. flock.aliasUGen = function (sourcePath, aliasName, inputDefaults, defaultOptions) { var root = flock.get(sourcePath); flock.set(root, aliasName, function (inputs, output, options) { options = $.extend(true, {}, defaultOptions, options); return root(inputs, output, options); }); flock.ugenDefaults(sourcePath + "." + aliasName, inputDefaults); }; // TODO: Check API; write unit tests. flock.aliasUGens = function (sourcePath, aliasesSpec) { var aliasName, settings; for (aliasName in aliasesSpec) { settings = aliasesSpec[aliasName]; flock.aliasUGen(sourcePath, aliasName, {inputs: settings.inputDefaults}, settings.options); } }; flock.copyUGenDefinition = function (ugenName, alias) { var defaults = flock.ugenDefaults(ugenName), value = fluid.getGlobalValue(ugenName); fluid.setGlobalValue(alias, value); flock.ugenDefaults(alias, fluid.copy(defaults)); }; flock.krMul = function (numSamps, output, mulInput) { var mul = mulInput.output[0], i; for (i = 0; i < numSamps; i++) { output[i] = output[i] * mul; } }; flock.mul = function (numSamps, output, mulInput) { var mul = mulInput.output, i; for (i = 0; i < numSamps; i++) { output[i] = output[i] * mul[i]; } }; flock.krAdd = function (numSamps, output, mulInput, addInput) { var add = addInput.output[0], i; for (i = 0; i < numSamps; i++) { output[i] = output[i] + add; } }; flock.add = function (numSamps, output, mulInput, addInput) { var add = addInput.output, i; for (i = 0; i < numSamps; i++) { output[i] = output[i] + add[i]; } }; flock.krMulAdd = function (numSamps, output, mulInput, addInput) { var mul = mulInput.output[0], add = addInput.output, i; for (i = 0; i < numSamps; i++) { output[i] = output[i] * mul + add[i]; } }; flock.mulKrAdd = function (numSamps, output, mulInput, addInput) { var mul = mulInput.output, add = addInput.output[0], i; for (i = 0; i < numSamps; i++) { output[i] = output[i] * mul[i] + add; } }; flock.krMulKrAdd = function (numSamps, output, mulInput, addInput) { var mul = mulInput.output[0], add = addInput.output[0], i; for (i = 0; i < numSamps; i++) { output[i] = output[i] * mul + add; } }; flock.mulAdd = function (numSamps, output, mulInput, addInput) { var mul = mulInput.output, add = addInput.output, i; for (i = 0; i < numSamps; i++) { output[i] = output[i] * mul[i] + add[i]; } }; flock.onMulAddInputChanged = function (that) { var mul = that.inputs.mul, add = that.inputs.add, fn; // If we have no mul or add inputs, bail immediately. if (!mul && !add) { that.mulAdd = that.mulAddFn = flock.noOp; return; } if (!mul) { // Only add. fn = add.rate !== flock.rates.AUDIO ? flock.krAdd : flock.add; } else if (!add) { // Only mul. fn = mul.rate !== flock.rates.AUDIO ? flock.krMul : flock.mul; } else { // Both mul and add. fn = mul.rate !== flock.rates.AUDIO ? (add.rate !== flock.rates.AUDIO ? flock.krMulKrAdd : flock.krMulAdd) : (add.rate !== flock.rates.AUDIO ? flock.mulKrAdd : flock.mulAdd); } that.mulAddFn = fn; that.mulAdd = function (numSamps) { that.mulAddFn(numSamps, that.output, that.inputs.mul, that.inputs.add); }; }; flock.ugen = function (inputs, output, options) { options = options || {}; var that = { enviro: options.enviro || flock.environment, rate: options.rate || flock.rates.AUDIO, inputs: inputs, output: output, options: options, model: options.model || { unscaledValue: 0.0, value: 0.0 }, multiInputs: {}, tags: ["flock.ugen"] }; that.lastOutputIdx = that.output.length - 1; that.get = function (path) { return flock.input.get(that.inputs, path); }; /** * Sets the value of the input at the specified path. * * @param {String} path the inputs's path relative to this ugen * @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 */ that.set = function (path, val) { return flock.input.set(that.inputs, path, val, that, function (ugenDef) { if (ugenDef === null || ugenDef === undefined) { return; } return flock.parse.ugenDef(ugenDef, that.enviro, { audioSettings: that.options.audioSettings, buses: that.buses, buffers: that.buffers }); }); }; /** * Gets or sets the named unit generator input. * * @param {String} path the input path * @param {UGenDef} val [optional] a scalar value, ugenDef, or array of ugenDefs that will be assigned to the specified input name * @return {Number|UGen} a scalar value in the case of a value ugen, otherwise the ugen itself */ that.input = function (path, val) { return !path ? undefined : typeof (path) === "string" ? arguments.length < 2 ? that.get(path) : that.set(path, val) : flock.isIterable(path) ? that.get(path) : that.set(path, val); }; // TODO: Move this into a grade. that.calculateStrides = function () { var m = that.model, strideNames = that.options.strideInputs, inputs = that.inputs, i, name, input; m.strides = m.strides || {}; if (!strideNames) { return; } for (i = 0; i < strideNames.length; i++) { name = strideNames[i]; input = inputs[name]; if (input) { m.strides[name] = input.rate === flock.rates.AUDIO ? 1 : 0; } else { fluid.log(fluid.logLevel.WARN, "An invalid input ('" + name + "') was found on a unit generator: " + that); } } }; that.collectMultiInputs = function () { var multiInputNames = that.options.multiInputNames, multiInputs = that.multiInputs, i, inputName, inputChannelCache, input; for (i = 0; i < multiInputNames.length; i++) { inputName = multiInputNames[i]; inputChannelCache = multiInputs[inputName]; if (!inputChannelCache) { inputChannelCache = multiInputs[inputName] = []; } else { // Clear the current array of buffers. inputChannelCache.length = 0; } input = that.inputs[inputName]; flock.ugen.collectMultiInputs(input, inputChannelCache); } }; // Base onInputChanged() implementation. that.onInputChanged = function (inputName) { var multiInputNames = that.options.multiInputNames; flock.onMulAddInputChanged(that); if (that.options.strideInputs) { that.calculateStrides(); } if (multiInputNames && (!inputName || multiInputNames.indexOf(inputName))) { that.collectMultiInputs(); } }; that.init = function () { var tags = fluid.makeArray(that.options.tags), m = that.model, o = that.options, i, s, valueDef; for (i = 0; i < tags.length; i++) { that.tags.push(tags[i]); } s = o.audioSettings = o.audioSettings || that.enviro.audioSystem.model; m.sampleRate = o.sampleRate || s.rates[that.rate]; m.nyquistRate = m.sampleRate; m.blockSize = that.rate === flock.rates.AUDIO ? s.blockSize : 1; m.sampleDur = 1.0 / m.sampleRate; // Assigns an interpolator function to the UGen. // This is inactive by default, but can be used in custom gen() functions. that.interpolate = flock.interpolate.none; if (o.interpolation) { var fn = flock.interpolate[o.interpolation]; if (!fn) { fluid.log(fluid.logLevel.IMPORTANT, "An invalid interpolation type of '" + o.interpolation + "' was specified. Defaulting to none."); } else { that.interpolate = fn; } } if (that.rate === flock.rates.DEMAND && that.inputs.freq) { valueDef = flock.parse.ugenDefForConstantValue(1.0); that.inputs.freq = flock.parse.ugenDef(valueDef, that.enviro); } }; that.init(); return that; }; // The term "multi input" is a bit ambiguous, // but it provides a very light (and possibly poor) abstraction for two different cases: // 1. inputs that consist of an array of multiple unit generators // 2. inputs that consist of a single unit generator that has multiple ouput channels // In either case, each channel of each input unit generator will be gathered up into // an array of "proxy ugen" objects and keyed by the input name, making easy to iterate // over sources of input quickly. // A proxy ugen consists of a simple object conforming to this contract: // {rate: <rate of parent ugen>, output: <Float32Array>} flock.ugen.collectMultiInputs = function (inputs, inputChannelCache) { if (!flock.isIterable(inputs)) { inputs = inputs = fluid.makeArray(inputs); } for (var i = 0; i < inputs.length; i++) { var input = inputs[i]; flock.ugen.collectChannelsForInput(input, inputChannelCache); } return inputChannelCache; }; flock.ugen.collectChannelsForInput = function (input, inputChannelCache) { var isMulti = flock.hasTag(input, "flock.ugen.multiChannelOutput"), channels = isMulti ? input.output : [input.output], i; for (i = 0; i < channels.length; i++) { inputChannelCache.push({ rate: input.rate, output: channels[i] }); } return inputChannelCache; }; flock.ugen.lastOutputValue = function (numSamps, out) { return out[numSamps - 1]; }; /** * Mixes buffer-related functionality into a unit generator. */ flock.ugen.buffer = function (that) { that.onBufferInputChanged = function (inputName) { var m = that.model, inputs = that.inputs; if (m.bufDef !== inputs.buffer || inputName === "buffer") { m.bufDef = inputs.buffer; flock.parse.bufferForDef(m.bufDef, that, that.enviro); } }; that.setBuffer = function (bufDesc) { that.buffer = bufDesc; if (that.onBufferReady) { that.onBufferReady(bufDesc); } }; that.initBuffer = function () { // Start with a zeroed buffer, since the buffer input may be loaded asynchronously. that.buffer = that.model.bufDef = flock.bufferDesc({ format: { sampleRate: that.options.audioSettings.rates.audio }, data: { channels: [new Float32Array(that.output.length)] } }); }; }; flock.ugen.value = function (inputs, output, options) { var that = flock.ugen(inputs, output, options); that.value = function () { return that.model.value; }; that.dynamicGen = function (numSamps) { var out = that.output, m = that.model; for (var i = 0; i < numSamps; i++) { out[i] = m.unscaledValue; } that.mulAdd(numSamps); m.value = flock.ugen.lastOutputValue(numSamps, out); }; that.onInputChanged = function () { var inputs = that.inputs, m = that.model; m.unscaledValue = inputs.value; if (that.rate !== "constant") { that.gen = that.dynamicGen; } else { that.gen = undefined; } flock.onMulAddInputChanged(that); that.dynamicGen(1); }; that.onInputChanged(); return that; }; flock.ugenDefaults("flock.ugen.value", { rate: "control", inputs: { value: 1.0, mul: null, add: null }, ugenOptions: { model: { unscaledValue: 1.0, value: 1.0 }, tags: ["flock.ugen.valueType"] } }); flock.ugen.silence = function (inputs, output, options) { var that = flock.ugen(inputs, output, options); that.onInputChanged = function () { for (var i = 0; i < that.output.length; i++) { that.output[i] = 0.0; } }; that.onInputChanged(); return that; }; flock.ugenDefaults("flock.ugen.silence", { rate: "constant" }); flock.ugen.passThrough = function (inputs, output, options) { var that = flock.ugen(inputs, output, options); that.gen = function (numSamps) { var m = that.model, source = that.inputs.source.output, out = that.output, i, val; for (i = 0; i < source.length; i++) { out[i] = val = source[i]; } for (; i < numSamps; i++) { out[i] = val = 0.0; } m.unscaledValue = val; that.mulAdd(numSamps); m.value = flock.ugen.lastOutputValue(numSamps, out); }; that.onInputChanged(); return that; }; flock.ugenDefaults("flock.ugen.passThrough", { rate: "audio", inputs: { source: null, mul: null, add: null } }); flock.ugen.out = function (inputs, output, options) { var that = flock.ugen(inputs, output, options); // TODO: Implement a "straight out" gen function for cases where the number // of sources matches the number of output buses (i.e. where no expansion is necessary). // TODO: This function is marked as unoptimized by the Chrome profiler. that.gen = function (numSamps) { var m = that.model, sources = that.multiInputs.sources, buses = that.options.buses, bufStart = that.inputs.bus.output[0], expand = that.inputs.expand.output[0], numSources, numOutputBuses, i, j, source, rate, bus, inc, outIdx, val; numSources = sources.length; numOutputBuses = Math.max(expand, numSources); if (numSources < 1) { return; } for (i = 0; i < numOutputBuses; i++) { source = sources[i % numSources]; rate = source.rate; bus = buses[bufStart + i]; inc = rate === flock.rates.AUDIO ? 1 : 0; outIdx = 0; for (j = 0; j < numSamps; j++, outIdx += inc) { val = source.output[outIdx]; // TODO: Support control rate interpolation. // TODO: Don't attempt to write to buses beyond the available number. // Provide an error at onInputChanged time if the unit generator is configured // with more sources than available buffers. bus[j] = bus[j] + val; } that.mulAddFn(numSamps, bus, that.inputs.mul, that.inputs.add); } // TODO: Consider how we should handle "value" when the number // of input channels for "sources" can be variable. // In the meantime, we just output the last source's last sample. m.value = m.unscaledValue = val; }; that.init = function () { that.sourceBuffers = []; that.onInputChanged(); }; that.init(); return that; }; flock.ugenDefaults("flock.ugen.out", { rate: "audio", inputs: { sources: null, bus: 0, expand: 2 }, ugenOptions: { tags: ["flock.ugen.outputType"], multiInputNames: ["sources"] } }); // Note: this unit generator currently only outputs values at control rate. // TODO: Unit tests. flock.ugen.valueOut = function (inputs, output, options) { var that = flock.ugen(inputs, output, options); that.arraySourceGen = function () { var m = that.model, sources = that.inputs.sources, i; for (i = 0; i < sources.length; i++) { m.value[i] = sources[i].output[0]; } }; that.ugenSourceGen = function () { that.model.value = that.model.unscaledValue = that.inputs.sources.output[0]; }; that.onInputChanged = function () { var m = that.model, sources = that.inputs.sources; if (flock.isIterable(sources)) { that.gen = that.arraySourceGen; m.value = new Float32Array(sources.length); m.unscaledValue = m.value; } else { that.gen = that.ugenSourceGen; } }; that.onInputChanged(); return that; }; flock.ugenDefaults("flock.ugen.valueOut", { rate: "control", inputs: { sources: null }, ugenOptions: { model: { unscaledValue: null, value: null }, tags: ["flock.ugen.outputType", "flock.ugen.valueType"] } }); // TODO: fix naming. // TODO: Make this a proper multiinput ugen. flock.ugen["in"] = function (inputs, output, options) { var that = flock.ugen(inputs, output, options); that.singleBusGen = function (numSamps) { var m = that.model, out = that.output; flock.ugen.in.readBus(numSamps, out, that.inputs.bus, that.options.buses); m.unscaledValue = flock.ugen.lastOutputValue(numSamps, out); that.mulAdd(numSamps); m.value = flock.ugen.lastOutputValue(numSamps, out); }; that.multiBusGen = function (numSamps) { var m = that.model, busesInput = that.inputs.bus, enviroBuses = that.options.buses, out = that.output, i, j, busIdx, val; for (i = 0; i < numSamps; i++) { val = 0; // Clear previous output values before summing a new set. for (j = 0; j < busesInput.length; j++) { busIdx = busesInput[j].output[0] | 0; val += enviroBuses[busIdx][i]; } out[i] = val; } m.unscaledValue = val; that.mulAdd(numSamps); m.value = flock.ugen.lastOutputValue(numSamps, out); }; that.onInputChanged = function () { that.gen = flock.isIterable(that.inputs.bus) ? that.multiBusGen : that.singleBusGen; flock.onMulAddInputChanged(that); }; that.onInputChanged(); return that; }; flock.ugen.in.readBus = function (numSamps, out, busInput, buses) { var busNum = busInput.output[0] | 0, bus = buses[busNum], i; for (i = 0; i < numSamps; i++) { out[i] = bus[i]; } }; flock.ugenDefaults("flock.ugen.in", { rate: "audio", inputs: { bus: 0, mul: null, add: null } }); flock.ugen.audioIn = function (inputs, output, options) { var that = flock.ugen(inputs, output, options); that.gen = function (numSamps) { var m = that.model, out = that.output, bus = that.bus, i, val; for (i = 0; i < numSamps; i++) { out[i] = val = bus[i]; } m.unscaledValue = val; that.mulAdd(numSamps); m.value = flock.ugen.lastOutputValue(numSamps, out); }; that.onInputChanged = function () { flock.onMulAddInputChanged(that); }; that.init = function () { // TODO: Direct reference to the shared environment. var busNum = that.enviro.audioSystem.inputDeviceManager.openAudioDevice(options); that.bus = that.options.buses[busNum]; that.onInputChanged(); }; that.init(); return that; }; flock.ugenDefaults("flock.ugen.audioIn", { rate: "audio", inputs: { mul: null, add: null } }); }());