2015-01-14 20:12:11 +01:00

492 lines
16 KiB
JavaScript

var samplerate = 44100;
// one-pole highpass coefficents
function coeff_highpass(cutoff) {
var x = Math.exp(-2.0 * Math.PI * cutoff * (1.0 / samplerate));
return [0, 0, 0.5 * (1.0 + x), -0.5 * (1.0 + x), x];
}
// one-pole allpass coefficients
function coeff_allpass(cutoff) {
var t = Math.tan(Math.PI * cutoff * (1.0 / samplerate));
var x = (t - 1.0) / (1 + 1.0);
return [0, 0, x, 1.0, -x];
}
// 12db lowpass biquad coefficients
function coeff_biquad_lowpass12db(freq, gain) {
var w = 2.0 * Math.PI * freq / samplerate;
var s = Math.sin(w);
var c = Math.cos(w);
var q = gain;
var alpha = s / (2.0 * q);
var scale = 1.0 / (1.0 + alpha);
var a1 = 2.0 * c * scale;
var a2 = (alpha - 1.0) * scale;
var b1 = (1.0 - c) * scale;
var b0 = 0.5 * b1;
var b2 = b0;
return [0, 0, 0, 0, b0, b1, b2, a1, a2];
}
// Polyfill for hyperbolic sine function because only Chrome 38+ has it.
if (Math.sinh === undefined) {
Math.sinh = function(x) {
return (Math.exp(x) - Math.exp(-x)) * 0.5;
}
}
// notch biquad coefficients
function coeff_biquad_notch(freq, bandwidth) {
var w = 2.0 * Math.PI * freq / samplerate;
var s = Math.sin(w);
var c = Math.cos(w);
var alpha = s * Math.sinh(0.5 * Math.log(2.0) * bandwidth * w / s);
var scale = 1.0 / (1.0 + alpha);
var a1 = 2.0 * c * scale;
var a2 = (alpha - 1.0) * scale;
var b0 = 1.0 * scale;
var b1 = -2.0 * c * scale;
var b2 = 1.0 * scale;
return [0, 0, 0, 0, b0, b1, b2, a1, a2];
}
// leaky integrator coefficient for reduction of 1/e in decaytime ms
function coeff_integrator(decaytime) {
return Math.exp(-1.0 / (samplerate * 0.001 * decaytime));
}
// 303 class
var TB303 = function(wavetable) {
// patterns
this.pattern = undefined
this.nextPattern = undefined
// callbacks
this.onStepChanged = function(){}
// sound settings
this.tempo = 100; // bpm
this.tuning = 0.0; // semitones
this.waveform = 0; // 0 for saw, 1 for square
this.cutoff = 240.0; // Hz
this.resonance = 1.0; // 0..1
this.envmod = 0.0; // 0..1
this.decay = 100; // ms
this.accent = 0.0; // 0..1
this.dist_shape = 0.0; // 0..1
this.dist_threshold = 1.0; // 0.1..1
this.delay_feedback = 0.5;
this.delay_send = 0.5;
this.delay_length = 20000;
this.running = true;
// filter states
this.onepole = []; // [x1,y1,b0,b1,a1] nested in [highpass1,feedbackhighpass,allpass,highpass2]
this.biquad = []; // [x1,x2,y1,y2,b0,b1,b2,a1,a2] nested in [declicker,notch]
this.tbfilter = [0, 0, 0, 0, 0]; // [y0,y1,y2,y3,y4]
this.resonance_skewed = 0;
this.tbf_b0 = 0;/*tbf_a1=0,tbf_y0=0,tbf_y1=0,tbf_y2=0,tbf_y3=0,tbf_y4=0,*/
this.tbf_k = 0;
this.tbf_g = 1.0;
// synth state
this.steplength = 0;
this.samplepos = 1000000;
this.pos = -1;
this.slidestep = 0;
this.table = 0;
this.oscpos = 0;
this.oscdelta = 0;
this.ampenv = 0;
this.filterenv = 0;
this.slide = 0;
this.filtermult = 0;
this.ampmult = 0;
this.accentgain = 0;
this.envscaler = 0;
this.envoffset = 0;
this.effective_dist_threshold = 1.0; // 0.1..1
this.dist_gain = 1.0 / this.effective_dist_threshold;
this.delaybuffer = new Float32Array(2 * samplerate);
this.delaypos = 0;
// [note<int>,accent<bool>,slide<bool>,gate<bool>,down<bool>,up<bool>]
//this.pattern = [
//[40,true,false,true,false,false],
//[40,false,true,true,false,false],
//[40,false,true,true,false,false],
//[40,false,true,true,false,false],
//[40,false,false,true,false,false],
//[40,false,true,true,false,false],
//[40,true,true,true,false,false],
//[40,false,true,true,false,false],
//[40,false,false,true,false,false],
//[47,true,false,true,false,false],
//[42,false,false,true,false,false],
//[48,true,false,true,false,false],
//[43,false,false,true,false,false],
//[50,false,false,true,false,false],
//[50,true,true,true,true,false],
//[50,false,true,true,true,false]
//];
// serialization functions
this.serialize = function() {
return [
this.pattern,
this.tempo,
this.tuning,
this.waveform,
this.cutoff,
this.resonance,
this.envmod,
this.decay,
this.accent,
this.dist_shape,
this.dist_threshold,
this.delay_feedback,
this.delay_send,
this.delay_length
];
};
this.unserialize = function(o) {
this.pattern=o[0].slice(0);
this.settempo(o[1]);
this.tuning=o[2];
this.waveform=o[3];
this.envmod=o[6];
this.setresonance(o[5]);
this.setcutoff(o[4]);
this.decay=o[7];
this.accent=o[8];
this.dist_shape=o[9];
this.setdistthreshold(o[10]);
this.delay_feedback=o[11];
this.delay_send=o[12];
this.delay_length=o[13];
};
// sound parameter setters
this.settempo = function(newtempo) {
this.tempo = newtempo;
this.steplength = samplerate * 60.0 / this.tempo / 4.0;
};
this.setdistthreshold = function(newthresh) {
// TODO: this destroys threshold value
this.dist_threshold = newthresh
this.effective_dist_threshold = 1.0 - 0.9 * this.dist_threshold;
this.dist_gain = 1.0 / this.effective_dist_threshold;
};
this.setcutoff = function(newcutoff) {
this.cutoff = newcutoff;
this.setenvmod(this.envmod);
};
this.setresonance = function(newresonance) {
this.resonance = newresonance;
this.resonance_skewed = (1.0 - Math.exp(-3.0 * this.resonance)) / (1.0 - Math.exp(-3.0));
};
this.setenvmod = function(newenvmod) {
this.envmod = newenvmod;
var c0 = 3.138152786059267e+2;
var c1 = 2.394411986817546e+3;
var c = Math.log(this.cutoff / c0) / Math.log(c1 / c0);
var slo = 3.773996325111173 * this.envmod + 0.736965594166206;
var shi = 4.194548788411135 * this.envmod + 0.864344900642434;
this.envscaler = (1.0 - c) * slo + c * shi;
this.envoffset = 0.048292930943553 * c + 0.294391201442418;
};
// reset/initialize everything
this.reset = function() {
this.onepole = [
coeff_highpass(44.486),
coeff_highpass(150.0),
coeff_allpass(14.008),
coeff_highpass(24.167)
];
this.biquad = [
coeff_biquad_lowpass12db(200.0, Math.sqrt(0.5)),
coeff_biquad_notch(7.5164, 4.7)
];
this.tbfilter = [0, 0, 0, 0, 0]; // [y0,y1,y2,y3,y4]
this.settempo(this.tempo);
this.setdistthreshold(this.dist_threshold);
this.setresonance(this.resonance);
this.setenvmod(this.envmod);
this.samplepos = 1000000;
this.pos = -1;
this.oscpos = 0.0;
};
// renderer
this.render = function() {
var anti_denormal = 1.0e-20;
if (this.running) {
// Step sequencer
if ((++this.samplepos) >= this.steplength) {
// Advance sequencer
this.samplepos = 0;
this.pos++
// Advance pattern if we reached the end
if (this.pos >= this.pattern.numberOfSteps) {
this.pos = 0
this.pattern = this.nextPattern
}
// Trigger step change callback
this.onStepChanged()
// Calculate target pitch
var step = this.pattern.steps[this.pos];
var pitch = step.pitch - step.down * 12 + step.up * 12 + this.tuning;
var f = 440.0 * Math.pow(2.0, (pitch - 69.0) / 12.0);
// Decay multiplier
this.ampmult = Math.exp(-1.0 / (0.001 * this.decay * samplerate));
if (step.accent) { // if accent
this.filtermult = Math.exp(-1.0 / (0.001 *200 * samplerate));
this.accentgain = this.accent;
} else {
this.filtermult = this.ampmult;
this.accentgain = 0.0;
}
this.ampenv = (1.0 / this.ampmult) * step.gate;
// VCO parameters
if (step.slide) { // if slide
// TODO: set up leaky integrator for slide motion
this.slide = (this.oscdelta -(f * 4096.0 / samplerate)) / 64.0;
this.slidestep = 0;
} else {
this.filterenv = 1.0 / this.filtermult;
this.oscpos = 0.0;
this.slide = 0.0;
this.slidestep = 64;
this.oscdelta = f * 4096.0 / samplerate;
this.table = this.waveform * 524288 + (step.pitch << 12);
}
}
} else {
this.ampenv = 0.0;
}
// Envelopes
this.ampenv = this.ampenv * this.ampmult + anti_denormal;
this.filterenv = this.filterenv * this.filtermult + anti_denormal;
// VCO
var idx = Math.round(this.oscpos);
var r = this.oscpos - idx;
var sample = ((1.0 - r) * wavetable[this.table + idx] + r * wavetable[this.table + ((idx + 1) & 4095)]);
this.oscpos += this.oscdelta;
if (this.oscpos > 4096.0) {
this.oscpos -= 4096.0;
}
// Modulators
if ((this.samplepos & 63) == 0) {
// Portamento
if (this.slidestep++ < 64) {
this.oscdelta -= this.slide;
}
// Cutoff modulation
var tmp1 = this.envscaler * (this.filterenv - this.envoffset);
var tmp2 = this.accentgain * this.filterenv;
var effectivecutoff = Math.min(this.cutoff * Math.pow(2.0, tmp1 + tmp2), 20000);
// Recalculate main filter coefficients
// TODO: optimize into lookup table!
var wc = ((2.0 * Math.PI) / samplerate) * effectivecutoff;
var r = this.resonance_skewed;
var fx = wc * 0.11253953951963826; // (1.0 / sqrt(2)) / (2.0 * PI)
this.tbf_b0 = (0.00045522346 + 6.1922189 * fx) / (1.0 + 12.358354 * fx + 4.4156345 * (fx * fx));
var k = fx * (fx * (fx * (fx * (fx * (fx + 7198.6997) - 5837.7917) - 476.47308) + 614.95611) + 213.87126) + 16.998792;
this.tbf_g = (((k * 0.058823529411764705) - 1.0) * r + 1.0) * (1.0 + r);
this.tbf_k = k * r;
}
// High pass filter 1
var flt = this.onepole[0];
flt[1] = flt[2] * sample + flt[3] * flt[0] + flt[4] * flt[1] + anti_denormal; // +plus denormalization constant
flt[0] = sample;
sample = flt[1];
// Main filter + feedback high pass
var tbf = this.tbfilter, tbfb0 = this.tbf_b0; // prefetch the array to save many object lookups
var fbhp = this.tbf_k * tbf[4];
flt = this.onepole[1];
flt[1] = flt[2] * fbhp + flt[3] * flt[0] + flt[4] * flt[1] + anti_denormal; // +plus denormalization constant
flt[0] = fbhp;
fbhp = flt[1];
tbf[0] = sample - fbhp;
tbf[1] += 2 * tbfb0 * (tbf[0] - tbf[1] + tbf[2]);
tbf[2] += tbfb0 * (tbf[1] - 2 * tbf[2] + tbf[3]);
tbf[3] += tbfb0 * (tbf[2] - 2 * tbf[3] + tbf[4]);
tbf[4] += tbfb0 * (tbf[3] - 2 * tbf[4]);
sample = 2 * this.tbf_g * tbf[4];
// All pass filter
flt = this.onepole[2];
flt[1] = flt[2] * sample + flt[3] * flt[0] + flt[4] * flt[1] + anti_denormal; // +plus denormalization constant
flt[0] = sample;
sample = flt[1];
// High pass filter 2
flt = this.onepole[3];
flt[1] = flt[2] * sample + flt[3] * flt[0] + flt[4] * flt[1] + anti_denormal; // +plus denormalization constant
flt[0] = sample;
sample = flt[1];
// Notch filter
flt = this.biquad[1];
var biquady = flt[4] * sample + flt[5] * flt[0] + flt[6] * flt[1] + flt[7] * flt[2] + flt[8] * flt[3] + anti_denormal; // plus denormalization constant
flt[1] = flt[0];
flt[0] = sample;
flt[3] = flt[2];
flt[2] = biquady;
sample = biquady;
// Calculate output gain with declicker filter
var outputgain = ((this.accentgain * 4.0 + 1.0) * this.ampenv);
flt = this.biquad[0];
biquady = flt[4] * outputgain + flt[5] * flt[0] + flt[6] * flt[1] + flt[7] * flt[2] + flt[8] * flt[3] + anti_denormal; // plus denormalization constant
flt[1] = flt[0];
flt[0] = outputgain;
flt[3] = flt[2];
flt[2] = biquady;
outputgain = biquady;
// Apply gain
sample *= outputgain;
// Foldback distortion
if (sample > this.effective_dist_threshold || sample < -this.effective_dist_threshold) {
//sample = Math.abs(Math.abs((sample-effective_dist_threshold)%(effective_dist_threshold*4.0))-effective_dist_threshold*2.0)-effective_dist_threshold;
var clipped = (sample > 0.0 ? 1.0 : -1.0) * this.effective_dist_threshold;
sample = (Math.abs(Math.abs((((1.0 - this.dist_shape) * clipped + this.dist_shape * sample) - this.effective_dist_threshold) % (this.effective_dist_threshold * 4.0)) - this.effective_dist_threshold * 2.0) - this.effective_dist_threshold);
}
sample *= this.dist_gain;
// Delay
var prev = this.delaybuffer[this.delaypos];
this.delaybuffer[this.delaypos] = this.delay_send * sample + this.delay_feedback * prev + anti_denormal;
this.delaypos++;
if (this.delaypos >= this.delay_length) {
this.delaypos = 0;
}
sample += prev;
return sample;
};
this.reset();
}
function loadwavetable(uri, callback) {
var img = new Image();
img.onload = function() {
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
var data = ctx.getImageData(0, 0, canvas.width, canvas.height);
wavetable = new Float32Array((new Uint8Array(data.data)).buffer);
callback();
};
img.src = uri;
}
function genwavetable() {
// create a sine wave lookup table (because Math.sin is slow)
var i;
var stab = new Float32Array(4096);
for (i = 0; i < 4096; i++) {
stab[i] = Math.sin(2.0 * Math.PI * (i / 4096.0));
}
var last = 0;
var j, k, h, m, f, invh;
// create the wavetable and zero it out (because not all browsers do that)
var wavetable = new Float32Array(2 * 524288);
for (i = 0; i < 2 * 524288; i++) {
wavetable[i] = 0.0;
}
// create a waveform for each midi note
for (i = 0; i < 128; i++) {
// compute the number of partials in the waveform
h = Math.round((samplerate >> 1) / (440.0 * Math.pow(2.0, (i - 69.0) / 12.0)));
// skip this note if the number of partials is equal to the previously generated waveform
if (h == last) {
continue;
}
// compute the waveform using fourier series up to h partials
invh = 1.0 / h;
for (j = 1; j <= h; j++) {
m = ((m = Math.cos((j - 1) * (0.5 * Math.PI) / invh)) * m) / j;
// render this partial to the wavetable
for (k = 0; k < 4096; k++) {
f = m * stab[(j * k) & 4095];
wavetable[1 + (k + (i << 12))] += f;
// only add odd partials to the square wave table
if (j & 1) {
wavetable[524288 + k + (i << 12)] += f;
}
}
}
last = h;
}
// normalize the wavetable
var max0 = 0;
var max1 = 0;
for (i = 0; i < 524288; i++) {
max0 = Math.max(max0, Math.abs(wavetable[i]));
max1 = Math.max(max1, Math.abs(wavetable[524288 + i]));
}
max0 = 1.0 / max0;
max1 = 1.0 / max1;
for (i = 0; i < 524288; i++) {
wavetable[i] *= max0;
wavetable[524288 + i] *= max1;
}
return wavetable
}