flocking
Version:
Creative audio synthesis for the Web
392 lines (318 loc) • 13.7 kB
JavaScript
/*
* Flocking Parser
* https://github.com/continuing-creativity/flocking
*
* Copyright 2011-2014, Colin Clark
* Dual licensed under the MIT and GPL Version 2 licenses.
*/
/*global require, Float32Array*/
/*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");
fluid.registerNamespace("flock.parse");
flock.parse.synthDef = function (ugenDef, enviro, options) {
if (!ugenDef) {
ugenDef = [];
}
if (!flock.parse.synthDef.hasOutUGen(ugenDef)) {
// We didn't get an out ugen specified, so we need to make one.
ugenDef = flock.parse.synthDef.makeOutUGenDef(ugenDef, options);
}
return flock.parse.ugenForDef(ugenDef, enviro, options);
};
flock.parse.synthDef.hasOutUGen = function (synthDef) {
// TODO: This is hostile to third-party extension.
return !flock.isIterable(synthDef) && (
synthDef.id === flock.OUT_UGEN_ID ||
synthDef.ugen === "flock.ugen.out" ||
synthDef.ugen === "flock.ugen.valueOut"
);
};
flock.parse.synthDef.makeOutUGenDef = function (ugenDef, options) {
ugenDef = {
id: flock.OUT_UGEN_ID,
ugen: "flock.ugen.valueOut",
inputs: {
sources: ugenDef
}
};
if (options.rate === flock.rates.AUDIO) {
ugenDef.ugen = "flock.ugen.out";
ugenDef.inputs.bus = 0;
ugenDef.inputs.expand = options.audioSettings.chans;
}
return ugenDef;
};
flock.parse.makeUGen = function (ugenDef, parsedInputs, enviro, options) {
var rates = options.audioSettings.rates,
blockSize = options.audioSettings.blockSize;
// Assume audio rate if no rate was specified by the user.
if (!ugenDef.rate) {
ugenDef.rate = flock.rates.AUDIO;
}
if (!flock.hasValue(flock.rates, ugenDef.rate)) {
flock.fail("An invalid rate was specified for a unit generator. ugenDef was: " +
fluid.prettyPrintJSON(ugenDef));
if (!flock.debug.failHard) {
var oldRate = ugenDef.rate;
ugenDef.rate = flock.rates.AUDIO;
flock.log.warn("Overriding invalid unit generator rate. Rate is now '" +
ugenDef.rate + "'; was: " + fluid.prettyPrintJSON(oldRate));
}
}
var sampleRate;
// Set the ugen's sample rate value according to the rate the user specified.
if (ugenDef.options && ugenDef.options.sampleRate !== undefined) {
sampleRate = ugenDef.options.sampleRate;
} else {
sampleRate = rates[ugenDef.rate];
}
// TODO: Infusion options merging!
ugenDef.options = $.extend(true, {}, ugenDef.options, {
sampleRate: sampleRate,
rate: ugenDef.rate,
audioSettings: {
rates: rates,
blockSize: blockSize
}
});
var outputBufferSize = ugenDef.rate === flock.rates.AUDIO ? blockSize : 1,
outputBuffers;
if (flock.hasTag(ugenDef.options, "flock.ugen.multiChannelOutput")) {
var numOutputs = ugenDef.options.numOutputs || 1;
outputBuffers = [];
for (var i = 0; i < numOutputs; i++) {
outputBuffers.push(new Float32Array(outputBufferSize));
}
} else {
outputBuffers = new Float32Array(outputBufferSize);
}
var ugenOpts = fluid.copy(ugenDef.options);
ugenOpts.buffers = options.buffers;
ugenOpts.buses = options.buses;
ugenOpts.enviro = enviro;
return flock.invoke(undefined, ugenDef.ugen, [
parsedInputs,
outputBuffers,
ugenOpts
]);
};
flock.parse.reservedWords = ["id", "ugen", "rate", "inputs", "options"];
flock.parse.specialInputs = ["value", "buffer", "list", "table", "envelope", "durations", "values"];
flock.parse.expandInputs = function (ugenDef) {
if (ugenDef.inputs) {
return ugenDef;
}
var inputs = {},
prop;
// Copy any non-reserved properties from the top-level ugenDef object into the inputs property.
for (prop in ugenDef) {
if (flock.parse.reservedWords.indexOf(prop) === -1) {
inputs[prop] = ugenDef[prop];
delete ugenDef[prop];
}
}
ugenDef.inputs = inputs;
return ugenDef;
};
flock.parse.ugenDefForConstantValue = function (value) {
return {
ugen: "flock.ugen.value",
rate: flock.rates.CONSTANT,
inputs: {
value: value
}
};
};
flock.parse.expandValueDef = function (ugenDef) {
var type = typeof (ugenDef);
if (type === "number") {
return flock.parse.ugenDefForConstantValue(ugenDef);
}
if (type === "object") {
return ugenDef;
}
throw new Error("Invalid value type found in ugen definition. UGenDef was: " +
fluid.prettyPrintJSON(ugenDef));
};
flock.parse.rateMap = {
"ar": flock.rates.AUDIO,
"kr": flock.rates.CONTROL,
"sr": flock.rates.SCHEDULED,
"dr": flock.rates.DEMAND,
"cr": flock.rates.CONSTANT
};
flock.parse.expandRate = function (ugenDef, options) {
ugenDef.rate = flock.parse.rateMap[ugenDef.rate] || ugenDef.rate;
if (options.overrideRate && ugenDef.rate !== flock.rates.CONSTANT) {
ugenDef.rate = options.rate;
}
return ugenDef;
};
flock.parse.ugenDef = function (ugenDefs, enviro, options) {
var parseFn = flock.isIterable(ugenDefs) ? flock.parse.ugensForDefs : flock.parse.ugenForDef;
var parsed = parseFn(ugenDefs, enviro, options);
return parsed;
};
flock.parse.ugenDef.mergeOptions = function (ugenDef) {
// TODO: Infusion options merging.
var defaults = flock.ugenDefaults(ugenDef.ugen) || {};
// TODO: Insane!
defaults = fluid.copy(defaults);
defaults.options = defaults.ugenOptions;
delete defaults.ugenOptions;
//
return $.extend(true, {}, defaults, ugenDef);
};
flock.parse.ugensForDefs = function (ugenDefs, enviro, options) {
var parsed = [],
i;
for (i = 0; i < ugenDefs.length; i++) {
parsed[i] = flock.parse.ugenForDef(ugenDefs[i], enviro, options);
}
return parsed;
};
/**
* Creates a unit generator for the specified unit generator definition spec.
*
* ugenDefs are plain old JSON objects describing the characteristics of the desired unit generator, including:
* - ugen: the type of unit generator, as string (e.g. "flock.ugen.sinOsc")
* - rate: the rate at which the ugen should be run, either "audio", "control", or "constant"
* - id: an optional unique name for the unit generator, which will make it available as a synth input
* - inputs: a JSON object containing named key/value pairs for inputs to the unit generator
* OR
* - inputs keyed by name at the top level of the ugenDef
*
* @param {UGenDef} ugenDef the unit generator definition to parse
* @param {Object} options an options object containing:
* {Object} audioSettings the environment's audio settings
* {Array} buses the environment's global buses
* {Array} buffers the environment's global buffers
* {Array of Functions} visitors an optional list of visitor functions to invoke when the ugen has been created
* @return the parsed unit generator object
*/
flock.parse.ugenForDef = function (ugenDef, enviro, options) {
enviro = enviro || flock.environment;
options = $.extend(true, {
audioSettings: enviro.audioSystem.model,
buses: enviro.busManager.buses,
buffers: enviro.buffers
}, options);
var o = options,
visitors = o.visitors,
rates = o.audioSettings.rates;
// If we receive a plain scalar value, expand it into a value ugenDef.
ugenDef = flock.parse.expandValueDef(ugenDef);
// We received an array of ugen defs.
if (flock.isIterable(ugenDef)) {
return flock.parse.ugensForDefs(ugenDef, enviro, options);
}
ugenDef = flock.parse.expandInputs(ugenDef);
flock.parse.expandRate(ugenDef, options);
ugenDef = flock.parse.ugenDef.mergeOptions(ugenDef, options);
var inputDefs = ugenDef.inputs,
inputs = {},
inputDef;
// TODO: This notion of "special inputs" should be refactored as a pluggable system of
// "input expanders" that are responsible for processing input definitions of various sorts.
// In particular, buffer management should be here so that we can initialize bufferDefs more
// proactively and remove this behaviour from flock.ugen.buffer.
for (inputDef in inputDefs) {
var inputDefVal = inputDefs[inputDef];
if (inputDefVal === null) {
continue; // Skip null inputs.
}
// Create ugens for all inputs except special inputs.
inputs[inputDef] = flock.input.shouldExpand(inputDef, ugenDef) ?
flock.parse.ugenForDef(inputDefVal, enviro, options) : // Parse the ugendef and create a ugen instance.
inputDefVal; // Don't instantiate a ugen, just pass the def on as-is.
}
if (!ugenDef.ugen) {
throw new Error("Unit generator definition lacks a 'ugen' property; " +
"can't initialize the synth graph. Value: " + fluid.prettyPrintJSON(ugenDef));
}
var ugen = flock.parse.makeUGen(ugenDef, inputs, enviro, options);
if (ugenDef.id) {
ugen.id = ugenDef.id;
}
ugen.options.ugenDef = ugenDef;
if (visitors) {
for(var i = 0; i < visitors.length; i++) {
visitors[i](ugen, ugenDef, rates);
}
}
return ugen;
};
flock.parse.expandBufferDef = function (bufDef) {
return typeof bufDef === "string" ? {id: bufDef} :
(flock.isIterable(bufDef) || bufDef.data || bufDef.format) ?
flock.bufferDesc(bufDef) : bufDef;
};
flock.parse.bufferForDef = function (bufDef, ugen, enviro) {
bufDef = flock.parse.expandBufferDef(bufDef);
if (bufDef.data && bufDef.data.channels) {
bufDef = flock.bufferDesc(bufDef);
flock.parse.bufferForDef.resolveBuffer(bufDef, ugen, enviro);
} else {
flock.parse.bufferForDef.resolveDef(bufDef, ugen, enviro);
}
};
flock.parse.bufferForDef.createBufferSource = function (enviro) {
return flock.bufferSource({
sampleRate: enviro.audioSystem.model.sampleRate
});
};
flock.parse.bufferForDef.findSource = function (defOrDesc, enviro) {
var source;
if (enviro && defOrDesc.id) {
source = enviro.bufferSources[defOrDesc.id];
if (!source) {
source = flock.parse.bufferForDef.createBufferSource(enviro);
enviro.bufferSources[defOrDesc.id] = source;
}
} else {
source = flock.parse.bufferForDef.createBufferSource(enviro);
}
return source;
};
flock.parse.bufferForDef.bindToPromise = function (p, source, ugen) {
// TODO: refactor this.
var success = function (bufDesc) {
source.events.onBufferUpdated.addListener(success);
if (ugen) {
ugen.setBuffer(bufDesc);
}
};
var error = function (msg) {
if (!msg && source.model.src && source.model.src.indexOf(".aif")) {
msg = "if this is an AIFF file, you might need to include" +
" flocking-audiofile-compatibility.js in some browsers.";
}
throw new Error("Error while resolving buffer " + source.model.src + ": " + msg);
};
p.then(success, error);
};
flock.parse.bufferForDef.resolveDef = function (bufDef, ugen, enviro) {
var source = flock.parse.bufferForDef.findSource(bufDef, enviro),
p;
bufDef.src = bufDef.url || bufDef.src;
if (bufDef.selector && typeof(document) !== "undefined") {
bufDef.src = document.querySelector(bufDef.selector).files[0];
}
p = source.get(bufDef);
flock.parse.bufferForDef.bindToPromise(p, source, ugen);
};
flock.parse.bufferForDef.resolveBuffer = function (bufDesc, ugen, enviro) {
var source = flock.parse.bufferForDef.findSource(bufDesc, enviro),
p = source.set(bufDesc);
flock.parse.bufferForDef.bindToPromise(p, source, ugen);
};
}());