mirror of
https://github.com/thedjinn/js303.git
synced 2025-08-18 00:08:15 +02:00
492 lines
16 KiB
JavaScript
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
|
|
}
|