flocking
Version:
Creative audio synthesis for the Web
831 lines (725 loc) • 27.1 kB
JavaScript
/*
* Flocking Filters
* https://github.com/continuing-creativity/flocking
*
* Copyright 2011-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";
var Filter = flock.requireModule("webarraymath", "Filter");
flock.ugen.lag = function (inputs, output, options) {
var that = flock.ugen(inputs, output, options);
that.gen = function (numSamps) {
var m = that.model,
out = that.output,
inputs = that.inputs,
time = inputs.time.output[0],
source = inputs.source.output,
prevSamp = m.prevSamp,
lagCoef = m.lagCoef,
i,
j,
currSamp,
outVal;
if (time !== m.prevTime) {
m.prevtime = time;
lagCoef = m.lagCoef = time === 0 ? 0.0 : Math.exp(flock.LOG001 / (time * m.sampleRate));
}
for (i = j = 0; i < numSamps; i++, j += m.strides.source) {
currSamp = source[j];
outVal = currSamp + lagCoef * (prevSamp - currSamp);
out[i] = prevSamp = outVal;
}
m.prevSamp = prevSamp;
that.mulAdd(numSamps);
};
that.onInputChanged();
return that;
};
flock.ugenDefaults("flock.ugen.lag", {
rate: "audio",
inputs: {
source: null,
time: 0.1
},
ugenOptions: {
strideInputs: ["source"],
model: {
prevSamp: 0.0,
lagCoef: 0.0,
prevTime: 0.0
}
}
});
/**
* A generic FIR and IIR filter engine. You specify the coefficients, and this will do the rest.
*/
// TODO: Unit tests.
flock.ugen.filter = function (inputs, output, options) {
var that = flock.ugen(inputs, output, options);
that.gen = function () {
var m = that.model,
out = that.output,
inputs = that.inputs,
q = inputs.q.output[0],
freq = inputs.freq.output[0];
if (m.prevFreq !== freq || m.prevQ !== q) {
that.updateCoefficients(m, freq, q);
}
that.filterEngine.filter(out, that.inputs.source.output);
m.prevQ = q;
m.prevFreq = freq;
m.value = m.unscaledValue = out[out.length - 1];
};
that.init = function () {
var recipeOpt = that.options.recipe;
var recipe = typeof (recipeOpt) === "string" ? flock.get(recipeOpt) : recipeOpt;
if (!recipe) {
throw new Error("Can't instantiate a flock.ugen.filter() without specifying a filter coefficient recipe.");
}
that.filterEngine = new Filter(recipe.sizes.b, recipe.sizes.a);
that.model.coeffs = {
a: that.filterEngine.a,
b: that.filterEngine.b
};
that.updateCoefficients = flock.get(recipe, that.options.type);
that.onInputChanged();
};
that.init();
return that;
};
flock.ugenDefaults("flock.ugen.filter", {
rate: "audio",
inputs: {
freq: 440,
q: 1.0,
source: null
}
});
/**
* An optimized biquad filter unit generator.
*/
// TODO: Unit tests.
flock.ugen.filter.biquad = function (inputs, output, options) {
var that = flock.ugen(inputs, output, options);
that.gen = function (numSamps) {
var m = that.model,
inputs = that.inputs,
out = that.output,
co = m.coeffs,
freq = inputs.freq.output[0],
q = inputs.q.output[0],
source = inputs.source.output,
i,
w;
if (m.prevFreq !== freq || m.prevQ !== q) {
that.updateCoefficients(m, freq, q);
}
for (i = 0; i < numSamps; i++) {
w = source[i] - co.a[0] * m.d0 - co.a[1] * m.d1;
out[i] = co.b[0] * w + co.b[1] * m.d0 + co.b[2] * m.d1;
m.d1 = m.d0;
m.d0 = w;
}
m.prevQ = q;
m.prevFreq = freq;
m.value = m.unscaledValue = flock.ugen.lastOutputValue(numSamps, out);
};
that.onInputChanged = function () {
var typeOpt = that.options.type;
that.updateCoefficients = typeof (typeOpt) === "string" ?
flock.get(typeOpt) : typeOpt;
};
that.init = function () {
that.model.d0 = 0.0;
that.model.d1 = 0.0;
that.model.coeffs = {
a: new Float32Array(2),
b: new Float32Array(3)
};
that.onInputChanged();
};
that.init();
return that;
};
flock.ugenDefaults("flock.ugen.filter.biquad", {
inputs: {
freq: 440,
q: 1.0,
source: null
}
});
flock.ugen.filter.biquad.types = {
"hp": {
inputDefaults: {
freq: 440,
q: 1.0
},
options: {
type: "flock.coefficients.butterworth.highPass"
}
},
"rhp": {
inputDefaults: {
freq: 440,
q: 1.0
},
options: {
type: "flock.coefficients.rbj.highPass"
}
},
"lp": {
inputDefaults: {
freq: 440,
q: 1.0
},
options: {
type: "flock.coefficients.butterworth.lowPass"
}
},
"rlp": {
inputDefaults: {
freq: 440,
q: 1.0
},
options: {
type: "flock.coefficients.rbj.lowPass"
}
},
"bp": {
inputDefaults: {
freq: 440,
q: 4.0
},
options: {
type: "flock.coefficients.butterworth.bandPass"
}
},
"br": {
inputDefaults: {
freq: 440,
q: 1.0
},
options: {
type: "flock.coefficients.butterworth.bandReject"
}
}
};
// Convenience methods for instantiating common types of biquad filters.
flock.aliasUGens("flock.ugen.filter.biquad", flock.ugen.filter.biquad.types);
flock.coefficients = {
butterworth: {
sizes: {
a: 2,
b: 3
},
lowPass: function (model, freq) {
var co = model.coeffs;
var lambda = 1 / Math.tan(Math.PI * freq / model.sampleRate);
var lambdaSquared = lambda * lambda;
var rootTwoLambda = flock.ROOT2 * lambda;
var b0 = 1 / (1 + rootTwoLambda + lambdaSquared);
co.b[0] = b0;
co.b[1] = 2 * b0;
co.b[2] = b0;
co.a[0] = 2 * (1 - lambdaSquared) * b0;
co.a[1] = (1 - rootTwoLambda + lambdaSquared) * b0;
},
highPass: function (model, freq) {
var co = model.coeffs;
var lambda = Math.tan(Math.PI * freq / model.sampleRate);
// Works around NaN values in cases where the frequency
// is precisely half the sampling rate, and thus lambda
// is Infinite.
if (lambda === Infinity) {
lambda = 0;
}
var lambdaSquared = lambda * lambda;
var rootTwoLambda = flock.ROOT2 * lambda;
var b0 = 1 / (1 + rootTwoLambda + lambdaSquared);
co.b[0] = b0;
co.b[1] = -2 * b0;
co.b[2] = b0;
co.a[0] = 2 * (lambdaSquared - 1) * b0;
co.a[1] = (1 - rootTwoLambda + lambdaSquared) * b0;
},
bandPass: function (model, freq, q) {
var co = model.coeffs;
var bw = freq / q;
var lambda = 1 / Math.tan(Math.PI * bw / model.sampleRate);
var theta = 2 * Math.cos(flock.TWOPI * freq / model.sampleRate);
var b0 = 1 / (1 + lambda);
co.b[0] = b0;
co.b[1] = 0;
co.b[2] = -b0;
co.a[0] = -(lambda * theta * b0);
co.a[1] = b0 * (lambda - 1);
},
bandReject: function (model, freq, q) {
var co = model.coeffs;
var bw = freq / q;
var lambda = Math.tan(Math.PI * bw / model.sampleRate);
var theta = 2 * Math.cos(flock.TWOPI * freq / model.sampleRate);
var b0 = 1 / (1 + lambda);
var b1 = -theta * b0;
co.b[0] = b0;
co.b[1] = b1;
co.b[2] = b0;
co.a[0] = b1;
co.a[1] = (1 - lambda) * b0;
}
},
// From Robert Brisow-Johnston's Filter Cookbook:
// https://dspwiki.com/index.php?title=Cookbook_Formulae_for_audio_EQ_biquad_filter_coefficients
rbj: {
sizes: {
a: 2,
b: 3
},
lowPass: function (model, freq, q) {
var co = model.coeffs;
var w0 = flock.TWOPI * freq / model.sampleRate;
var cosw0 = Math.cos(w0);
var sinw0 = Math.sin(w0);
var alpha = sinw0 / (2 * q);
var oneLessCosw0 = 1 - cosw0;
var a0 = 1 + alpha;
var b0 = (oneLessCosw0 / 2) / a0;
co.b[0] = b0;
co.b[1] = oneLessCosw0 / a0;
co.b[2] = b0;
co.a[0] = (-2 * cosw0) / a0;
co.a[1] = (1 - alpha) / a0;
},
highPass: function (model, freq, q) {
var co = model.coeffs;
var w0 = flock.TWOPI * freq / model.sampleRate;
var cosw0 = Math.cos(w0);
var sinw0 = Math.sin(w0);
var alpha = sinw0 / (2 * q);
var onePlusCosw0 = 1 + cosw0;
var a0 = 1 + alpha;
var b0 = (onePlusCosw0 / 2) / a0;
co.b[0] = b0;
co.b[1] = (-onePlusCosw0) / a0;
co.b[2] = b0;
co.a[0] = (-2 * cosw0) / a0;
co.a[1] = (1 - alpha) / a0;
},
bandPass: function (model, freq, q) {
var co = model.coeffs;
var w0 = flock.TWOPI * freq / model.sampleRate;
var cosw0 = Math.cos(w0);
var sinw0 = Math.sin(w0);
var alpha = sinw0 / (2 * q);
var a0 = 1 + alpha;
var qByAlpha = q * alpha;
co.b[0] = qByAlpha / a0;
co.b[1] = 0;
co.b[2] = -qByAlpha / a0;
co.a[0] = (-2 * cosw0) / a0;
co.a[1] = (1 - alpha) / a0;
},
bandReject: function (model, freq, q) {
var co = model.coeffs;
var w0 = flock.TWOPI * freq / model.sampleRate;
var cosw0 = Math.cos(w0);
var sinw0 = Math.sin(w0);
var alpha = sinw0 / (2 * q);
var a0 = 1 + alpha;
var ra0 = 1 / a0;
var b1 = (-2 * cosw0) / a0;
co.b[0] = ra0;
co.b[1] = b1;
co.b[2] = ra0;
co.a[0] = b1;
co.a[1] = (1 - alpha) / a0;
}
}
};
/**
* A Moog-style 24db resonant low-pass filter.
*
* This unit generator is based on the following musicdsp snippet:
* http://www.musicdsp.org/showArchiveComment.php?ArchiveID=26
*
* Inputs:
* - source: the source signal to process
* - cutoff: the cutoff frequency
* - resonance: the filter resonance [between 0 and 4, where 4 is self-oscillation]
*/
// TODO: Unit tests.
flock.ugen.filter.moog = function (inputs, output, options) {
var that = flock.ugen(inputs, output, options);
that.gen = function (numSamps) {
var m = that.model,
inputs = that.inputs,
out = that.output,
source = inputs.source.output,
sourceInc = m.strides.source,
res = inputs.resonance.output,
resInc = m.strides.resonance,
cutoff = inputs.cutoff.output,
cutoffInc = m.strides.cutoff,
f = m.f,
fSq = m.fSq,
fSqSq = m.fSqSq,
oneMinusF = m.oneMinusF,
fb = m.fb,
i,
j,
k,
l,
currCutoff,
currRes,
val;
for (i = j = k = l = 0; i < numSamps; i++, j += sourceInc, k += resInc, l += cutoffInc) {
currCutoff = cutoff[l];
currRes = res[k];
if (currCutoff !== m.prevCutoff) {
if (currCutoff > m.nyquistRate) {
currCutoff = m.nyquistRate;
}
f = m.f = (currCutoff / m.nyquistRate) * 1.16;
fSq = m.fSq = f * f;
fSqSq = m.fSqSq = fSq * fSq;
oneMinusF = m.oneMinusF = 1 - f;
m.prevRes = undefined; // Flag the need to update fb.
}
if (currRes !== m.prevRes) {
if (currRes > 4) {
currRes = 4;
} else if (currRes < 0) {
currRes = 0;
}
fb = m.fb = currRes * (1.0 - 0.15 * fSq);
}
val = source[j] - (m.out4 * fb);
val *= 0.35013 * fSqSq;
m.out1 = val + 0.3 * m.in1 + oneMinusF * m.out1;
m.in1 = val;
m.out2 = m.out1 + 0.3 * m.in2 + oneMinusF * m.out2;
m.in2 = m.out1;
m.out3 = m.out2 + 0.3 * m.in3 + oneMinusF * m.out3;
m.in3 = m.out2;
m.out4 = m.out3 + 0.3 * m.in4 + oneMinusF * m.out4;
m.in4 = m.out3;
out[i] = m.out4;
}
m.unscaledValue = m.out4;
that.mulAdd(numSamps);
m.value = flock.ugen.lastOutputValue(numSamps, out);
};
that.onInputChanged();
return that;
};
flock.ugenDefaults("flock.ugen.filter.moog", {
rate: "audio",
inputs: {
cutoff: 3000,
resonance: 3.99,
source: null
},
ugenOptions: {
model: {
in1: 0.0,
in2: 0.0,
in3: 0.0,
in4: 0.0,
out1: 0.0,
out2: 0.0,
out3: 0.0,
out4: 0.0,
prevCutoff: undefined,
prevResonance: undefined,
f: undefined,
fSq: undefined,
fSqSq: undefined,
oneMinusF: undefined,
fb: undefined,
unscaledValue: 0.0,
value: 0.0
},
strideInputs: ["source", "cutoff", "resonance"]
}
});
flock.ugen.delay = function (inputs, output, options) {
var that = flock.ugen(inputs, output, options);
that.gen = function (numSamps) {
var m = that.model,
inputs = that.inputs,
out = that.output,
source = inputs.source.output,
time = inputs.time.output[0],
delayBuffer = that.delayBuffer,
i,
val;
if (time !== m.time) {
m.time = time;
m.delaySamps = time * that.model.sampleRate;
}
for (i = 0; i < numSamps; i++) {
if (m.pos >= m.delaySamps) {
m.pos = 0;
}
out[i] = val = delayBuffer[m.pos];
delayBuffer[m.pos] = source[i];
m.pos++;
}
m.unscaledValue = val;
that.mulAdd(numSamps);
m.value = flock.ugen.lastOutputValue(numSamps, out);
};
that.onInputChanged = function (inputName) {
flock.onMulAddInputChanged(that);
if (!inputName || inputName === "maxTime") {
var delayBufferLength = that.model.sampleRate * that.inputs.maxTime.output[0];
that.delayBuffer = new Float32Array(delayBufferLength);
}
};
that.onInputChanged();
return that;
};
flock.ugenDefaults("flock.ugen.delay", {
rate: "audio",
inputs: {
maxTime: 1.0,
time: 1.0,
source: null
},
ugenOptions: {
model: {
pos: 0,
unscaledValue: 0.0,
value: 0.0
}
}
});
// Simple optimised delay for exactly 1 sample
flock.ugen.delay1 = function (inputs, output, options) {
var that = flock.ugen(inputs, output, options);
that.gen = function (numSamps) {
var m = that.model,
inputs = that.inputs,
out = that.output,
source = inputs.source.output,
prevVal = m.prevVal,
i,
val;
for (i = 0; i < numSamps; i++) {
out[i] = val = prevVal;
prevVal = source[i];
}
m.prevVal = prevVal;
m.unscaledValue = val;
that.mulAdd(numSamps);
m.value = flock.ugen.lastOutputValue(numSamps, out);
};
that.onInputChanged = function () {
flock.onMulAddInputChanged(that);
};
that.onInputChanged();
return that;
};
flock.ugenDefaults("flock.ugen.delay1", {
rate: "audio",
inputs: {
source: null
},
ugenOptions: {
model: {
prevVal: 0.0,
unscaledValue: 0.0,
value: 0.0
}
}
});
flock.ugen.freeverb = function (inputs, output, options) {
var that = flock.ugen(inputs, output, options);
that.tunings = that.options.tunings;
that.allpassTunings = that.options.allpassTunings;
that.gen = function (numSamps) {
var m = that.model,
inputs = that.inputs,
out = that.output,
source = inputs.source.output,
mix = inputs.mix.output[0],
dry = 1 - mix,
roomsize = inputs.room.output[0],
room_scaled = roomsize * 0.28 + 0.7,
damp = inputs.damp.output[0],
damp1 = damp * 0.4,
damp2 = 1.0 - damp1,
i,
j,
val;
for (i = 0; i < numSamps; i++) {
// read inputs
var inp = source[i];
var inp_scaled = inp * 0.015;
// read samples from the allpasses
for (j = 0; j < that.buffers_a.length; j++) {
if (++that.bufferindices_a[j] === that.allpassTunings[j]) {
that.bufferindices_a[j] = 0;
}
that.readsamp_a[j] = that.buffers_a[j][that.bufferindices_a[j]];
}
// foreach comb buffer, we perform same filtering (only bufferlen differs)
for (j = 0; j < that.buffers_c.length; j++) {
if (++that.bufferindices_c[j] === that.tunings[j]) {
that.bufferindices_c[j] = 0;
}
var bufIdx_c = that.bufferindices_c[j],
readsamp_c = that.buffers_c[j][bufIdx_c];
that.filterx_c[j] = (damp2 * that.filtery_c[j]) + (damp1 * that.filterx_c[j]);
that.buffers_c[j][bufIdx_c] = inp_scaled + (room_scaled * that.filterx_c[j]);
that.filtery_c[j] = readsamp_c;
}
// each allpass is handled individually,
// with different calculations made and stored into the delaylines
var ftemp8 = (that.filtery_c[6] + that.filtery_c[7]);
that.buffers_a[3][that.bufferindices_a[3]] = ((((0.5 * that.filterx_a[3]) + that.filtery_c[0]) +
(that.filtery_c[1] + that.filtery_c[2])) +
((that.filtery_c[3] + that.filtery_c[4]) + (that.filtery_c[5] + ftemp8)));
that.filterx_a[3] = that.readsamp_a[3];
that.filtery_a[3] = (that.filterx_a[3] - (((that.filtery_c[0] + that.filtery_c[1]) +
(that.filtery_c[2] + that.filtery_c[3])) +
((that.filtery_c[4] + that.filtery_c[5]) + ftemp8)));
that.buffers_a[2][that.bufferindices_a[2]] = ((0.5 * that.filterx_a[2]) + that.filtery_a[3]);
that.filterx_a[2] = that.readsamp_a[2];
that.filtery_a[2] = (that.filterx_a[2] - that.filtery_a[3]);
that.buffers_a[1][that.bufferindices_a[1]] = ((0.5 * that.filterx_a[1]) + that.filtery_a[2]);
that.filterx_a[1] = that.readsamp_a[1];
that.filtery_a[1] = (that.filterx_a[1] - that.filtery_a[2]);
that.buffers_a[0][that.bufferindices_a[0]] = ((0.5 * that.filterx_a[0]) + that.filtery_a[1]);
that.filterx_a[0] = that.readsamp_a[0];
that.filtery_a[0] = (that.filterx_a[0] - that.filtery_a[1]);
val = ((dry * inp) + (mix * that.filtery_a[0]));
out[i] = val;
}
m.unscaledValue = val;
that.mulAdd(numSamps);
m.value = flock.ugen.lastOutputValue(numSamps, out);
};
that.initDelayLines = function () {
// Initialise the delay lines
that.buffers_c = new Array(8);
that.bufferindices_c = new Int32Array(8);
that.filterx_c = new Float32Array(8);
that.filtery_c = new Float32Array(8);
var spread = that.model.spread;
var i, j;
for(i = 0; i < that.buffers_c.length; i++) {
that.buffers_c[i] = new Float32Array(that.tunings[i]+spread);
that.bufferindices_c[i] = 0;
that.filterx_c[i] = 0;
that.filtery_c[i] = 0;
for(j = 0; j < that.tunings[i]+spread; j++) {
that.buffers_c[i][j] = 0;
}
}
that.buffers_a = new Array(4);
that.bufferindices_a = new Int32Array(4);
that.filterx_a = new Float32Array(4);
that.filtery_a = new Float32Array(4);
// "readsamp" vars are temporary values read back from the delay lines,
// not stored but only used in the gen loop
that.readsamp_a = new Float32Array(4);
for (i = 0; i < that.buffers_a.length; i++) {
that.bufferindices_a[i] = 0;
that.filterx_a[i] = 0;
that.filtery_a[i] = 0;
that.readsamp_a[i] = 0;
// TODO is this what the spread is meant to do?
for (j = 0; j < that.allpassTunings.length; j++) {
that.allpassTunings[j] += spread;
}
that.buffers_a[i] = new Float32Array(that.allpassTunings[i]);
for (j = 0; j < that.allpassTunings[i]; j++) {
that.buffers_a[i][j] = 0;
}
}
};
that.init = function () {
that.initDelayLines();
that.onInputChanged();
};
that.init();
return that;
};
flock.ugenDefaults("flock.ugen.freeverb", {
rate: "audio",
inputs: {
source: null,
mix: 0.33,
room: 0.5,
damp: 0.5
},
ugenOptions: {
model: {
spread: 0,
unscaledValue: 0.0,
value: 0.0
},
tunings: [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617],
allpassTunings: [556, 441, 341, 225]
}
});
flock.ugen.decay = function (inputs, output, options) {
var that = flock.ugen(inputs, output, options);
that.gen = function (numSamps) {
var m = that.model,
inputs = that.inputs,
out = that.output,
source = inputs.source.output,
time = inputs.time.output[0],
i,
val;
if (time !== m.time) {
m.time = time;
m.coeff = time === 0.0 ? 0.0 : Math.exp(flock.LOG001 / (time * that.model.sampleRate));
}
// TODO: Optimize this conditional.
if (m.coeff === 0.0) {
for (i = 0; i < numSamps; i++) {
out[i] = val = source[i];
}
} else {
for (i = 0; i < numSamps; i++) {
m.lastSamp = source[i] + m.coeff * m.lastSamp;
out[i] = val = m.lastSamp;
}
}
m.unscaledValue = val;
that.mulAdd(numSamps);
m.value = flock.ugen.lastOutputValue(numSamps, out);
};
that.onInputChanged();
return that;
};
flock.ugenDefaults("flock.ugen.decay", {
rate: "audio",
inputs: {
source: null,
time: 1.0
},
ugenOptions: {
model: {
time: 0,
lastSamp: 0,
coeff: 0,
value: 0.0
}
}
});
}());