UNPKG

flocking

Version:

Creative audio synthesis for the Web

420 lines (353 loc) 13.7 kB
/* * Flocking Sequencing Unit Generators * https://github.com/continuing-creativity/flocking * * Copyright 2013-2015, 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"; /** * Changes from the <code>initial</code> input to the <code>target</code> input * at the specified <code>time</code>. An optional <code>crossfade</code> duration * may be specified to linearly crossfade between the two inputs. * * Can be used to schedule sample-accurate changes. * Note that the <code>target</code> input will be evaluated from the beginning, * even if its value isn't yet output. * */ flock.ugen.change = function (inputs, output, options) { var that = flock.ugen(inputs, output, options); that.gen = function (numSamps) { var m = that.model, initial = that.inputs.initial.output, initialInc = m.strides.initial, target = that.inputs.target.output, targetInc = m.strides.target, out = that.output, samplesLeft = m.samplesLeft, crossfadeLevel = m.crossfadeLevel, val; for (var i = 0, j = 0, k = 0; i < numSamps; i++, j += initialInc, k += targetInc) { if (samplesLeft > 0) { // We haven't hit the scheduled time yet. val = initial[j]; samplesLeft--; } else if (crossfadeLevel > 0.0) { // We've hit the scheduled time, but we still need to peform the crossfade. val = (initial[j] * crossfadeLevel) + (target[k] * (1.0 - crossfadeLevel)); crossfadeLevel -= m.crossfadeStepSize; } else { // We're done. val = target[k]; } out[i] = val; } m.samplesLeft = samplesLeft; m.crossfadeLevel = crossfadeLevel; m.value = m.unscaledValue = val; }; that.onInputChanged = function (inputName) { var m = that.model, inputs = that.inputs; if (inputName === "time" || !inputName) { m.samplesLeft = Math.round(inputs.time.output[0] * m.sampleRate); } if (inputName === "crossfade" || !inputName) { m.crossfadeStepSize = 1.0 / Math.round(inputs.crossfade.output[0] * m.sampleRate); m.crossfadeLevel = inputs.crossfade.output[0] > 0.0 ? 1.0 : 0.0; } that.calculateStrides(); }; that.onInputChanged(); return that; }; flock.ugenDefaults("flock.ugen.change", { rate: "audio", inputs: { /** * An input unit generator to output initially. * Can be audio, control, or constant rate. */ initial: 0.0, /** * The unit generator to output after the specified time. * Can be audio, control, or constant rate. */ target: 0.0, /** * The sample-accurate time (in seconds) at which the * the change should occur. */ time: 0.0, /** * The duration of the optional linear crossfade between * the two values. */ crossfade: 0.0 }, ugenOptions: { model: { samplesLeft: 0.0, crossfadeStepSize: 0, crossfadeLevel: 0.0, unscaledValue: 0.0, value: 0.0 }, strideInputs: ["initial", "target"] } }); flock.ugen.listItem = function (inputs, output, options) { var that = flock.ugen(inputs, output, options); that.gen = function (numSamps) { var m = that.model, out = that.output, list = that.inputs.list, maxIdx = list.length - 1, index = that.inputs.index.output, i, val, j, listIdx; for (i = 0, j = 0; i < numSamps; i++, j += m.strides.index) { listIdx = Math.round(index[j] * maxIdx); listIdx = Math.max(0, listIdx); listIdx = Math.min(listIdx, maxIdx); val = list[listIdx]; out[i] = val; } m.unscaledValue = val; that.mulAdd(numSamps); m.value = flock.ugen.lastOutputValue(numSamps, out); }; that.onInputChanged(); return that; }; flock.ugenDefaults("flock.ugen.listItem", { rate: "control", inputs: { index: 0, // A value between 0 and 1.0 list: [0] }, ugenOptions: { strideInputs: ["index"] } }); flock.ugen.sequence = function (inputs, output, options) { var that = flock.ugen(inputs, output, options); that.gen = function (numSamps) { var values = that.inputs.values, inputs = that.inputs, freq = inputs.freq.output, loop = inputs.loop.output[0], m = that.model, scale = m.scale, out = that.output, start = inputs.start ? Math.round(inputs.start.output[0]) : 0, end = inputs.end ? Math.round(inputs.end.output[0]) : values.length, startItem, i, j; if (m.unscaledValue === undefined) { startItem = values[start]; m.unscaledValue = (startItem === undefined) ? 0.0 : startItem; } if (m.nextIdx === undefined) { m.nextIdx = start; } for (i = 0, j = 0; i < numSamps; i++, j += m.strides.freq) { if (m.nextIdx >= end) { if (loop > 0.0) { m.nextIdx = start; } else { out[i] = m.unscaledValue; continue; } } out[i] = m.unscaledValue = values[m.nextIdx]; m.phase += freq[j] * scale; if (m.phase >= 1.0) { m.phase = 0.0; m.nextIdx++; } } that.mulAdd(numSamps); m.value = flock.ugen.lastOutputValue(numSamps, out); }; that.onInputChanged = function () { that.model.scale = that.rate !== flock.rates.DEMAND ? that.model.sampleDur : 1; if ((!that.inputs.values || that.inputs.values.length === 0) && that.inputs.list) { flock.log.warn("The 'list' input to flock.ugen.sequence is deprecated. Use 'values' instead."); that.inputs.values = that.inputs.list; } if (!that.inputs.values) { that.inputs.values = []; } that.calculateStrides(); flock.onMulAddInputChanged(that); }; that.init = function () { that.onInputChanged(); }; that.init(); return that; }; flock.ugenDefaults("flock.ugen.sequence", { rate: "control", inputs: { start: 0, freq: 1.0, loop: 0.0, values: [] }, ugenOptions: { model: { unscaledValue: undefined, value: 0.0, phase: 0 }, strideInputs: ["freq"] } }); /** * A Sequencer unit generator outputs a sequence of values * for the specified sequence of durations. * * Optionally, when the resetOnNext flag is set, * the sequencer will reset its value to 0.0 for one sample * prior to moving to the next duration. * This is useful for sequencing envelope gates, for example. * * Inputs: * durations: an array of durations (in seconds) to hold each value * values: an array of values to output * loop: if > 0, the unit generator will loop back to the beginning * of the lists when it reaches the end; defaults to 0. */ // TODO: Unit Tests! flock.ugen.sequencer = function (inputs, output, options) { var that = flock.ugen(inputs, output, options); that.gen = function (numSamps) { var m = that.model, o = that.options, resetOnNext = o.resetOnNext, out = that.output, loop = that.inputs.loop.output[0], durations = that.inputs.durations, values = that.inputs.values, i, val; if (m.shouldValidateSequences) { m.shouldValidateSequences = false; flock.ugen.sequencer.validateSequences(durations, values); } for (i = 0; i < numSamps; i++) { if (values.length === 0 || durations.length === 0) { // Nothing to output. out[i] = val = 0.0; continue; } if (m.samplesRemaining <= 0) { // We've hit the end of a stage. if (m.idx < durations.length - 1) { // Continue to the next value/duration pair. m.idx++; val = flock.ugen.sequencer.nextStage(durations, values, resetOnNext, m); } else if (loop > 0.0) { // Loop back to the first value/duration pair. m.idx = 0; val = flock.ugen.sequencer.nextStage(durations, values, resetOnNext, m); } else { // Nothing left to do. val = o.holdLastValue ? m.unscaledValue : 0.0; } } else { // Still in the midst of a stage. val = values[m.idx]; m.samplesRemaining--; } out[i] = val; } m.unscaledValue = val; that.mulAdd(numSamps); m.value = flock.ugen.lastOutputValue(numSamps, out); }; that.onInputChanged = function (inputName) { var m = that.model, inputs = that.inputs; if (inputName === "durations" || inputs.durations !== m.prevDurations) { m.idx = 0; flock.ugen.sequencer.calcDurationsSamps(inputs.durations, that.model); flock.ugen.sequencer.validateInput("durations", that); m.prevDurations = inputs.durations; } if (inputName === "values" || inputs.values !== m.prevValues) { m.idx = 0; flock.ugen.sequencer.validateInput("values", that); m.prevValues = inputs.values; } that.model.shouldValidateSequences = true; flock.onMulAddInputChanged(that); }; that.init = function () { that.onInputChanged(); }; that.init(); return that; }; flock.ugen.sequencer.validateInput = function (inputName, that) { var input = that.inputs[inputName]; if (!input || !flock.isIterable(input)) { flock.fail("No " + inputName + " array input was specified for flock.ugen.sequencer: " + fluid.prettyPrintJSON(that.options.ugenDef)); } }; flock.ugen.sequencer.validateSequences = function (durations, values) { if (durations.length !== values.length) { flock.fail("Mismatched durations and values array lengths for flock.ugen.sequencer. Durations: " + fluid.prettyPrintJSON(durations) + ", values: " + fluid.prettyPrintJSON(values)); } }; flock.ugen.sequencer.calcDurationsSamps = function (durations, m) { m.samplesRemaining = Math.floor(durations[m.idx] * m.sampleRate); }; flock.ugen.sequencer.nextStage = function (durations, values, resetOnNext, m) { flock.ugen.sequencer.calcDurationsSamps(durations, m); m.samplesRemaining--; return resetOnNext ? 0.0 : values[m.idx]; }; flock.ugenDefaults("flock.ugen.sequencer", { rate: "audio", inputs: { // TODO: start, // TODO: end, // TODO: skip // TODO: direction, durations: [], values: [], loop: 0.0 }, ugenOptions: { model: { idx: 0, samplesRemaining: 0, unscaledValue: 0.0, value: 0.0, prevDurations: [], prevValues: [] }, resetOnNext: false, holdLastvalue: false } }); }());