mirror of
https://github.com/thedjinn/js303.git
synced 2025-08-17 15:58:13 +02:00
449 lines
12 KiB
JavaScript
449 lines
12 KiB
JavaScript
var wavetable;
|
|
var samplerate = 44100;
|
|
|
|
// onepole filter coefficients
|
|
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];
|
|
}
|
|
|
|
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];
|
|
}
|
|
|
|
// biquad filter 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];
|
|
}
|
|
|
|
function sinh(x) {
|
|
return (Math.exp(x)-Math.exp(-x))*0.5;
|
|
}
|
|
|
|
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*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 from decay time in ms
|
|
function coeff_integrator(decaytime) {
|
|
return Math.exp(-1.0/(samplerate*0.001*decaytime));
|
|
}
|
|
|
|
// 303 class
|
|
var TB303 = 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.0;
|
|
this.envscaler=0.0;
|
|
this.envoffset=0.0;
|
|
this.dist_gain = 1.0/this.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 = 1.0-0.9*newthresh;
|
|
this.dist_gain = 1.0/this.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=(this.pos+1)&15; // TODO: modulo pattern length for non-16
|
|
|
|
// Calculate target pitch
|
|
var pitch = this.pattern[this.pos][0]-this.pattern[this.pos][4]*12+this.pattern[this.pos][5]*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 (this.pattern[this.pos][1]) { // 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)*this.pattern[this.pos][3];
|
|
|
|
// VCO parameters
|
|
if (this.pattern[this.pos][2]) { // 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+(this.pattern[this.pos][0]<<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
|
|
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.dist_threshold||sample<-this.dist_threshold) {
|
|
//sample = Math.abs(Math.abs((sample-dist_threshold)%(dist_threshold*4.0))-dist_threshold*2.0)-dist_threshold;
|
|
var clipped = (sample>0.0?1.0:-1.0)*this.dist_threshold;
|
|
sample = (Math.abs(Math.abs((((1.0-this.dist_shape)*clipped+this.dist_shape*sample)-this.dist_threshold)%(this.dist_threshold*4.0))-this.dist_threshold*2.0)-this.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() {
|
|
wavetable = new Float32Array(2*524288);
|
|
|
|
var stab = new Float32Array(4096);
|
|
for (var i=0;i<4096;i++) {
|
|
stab[i] = Math.sin(2.0*Math.PI*(i/4096.0));
|
|
}
|
|
|
|
var last = 0;
|
|
var i,j,k,h,m,f,invh;
|
|
for (i=0;i<2*524288;i++) {
|
|
wavetable[i]=0.0;
|
|
}
|
|
for (i=0;i<128;i++) {
|
|
h = Math.round((samplerate>>1)/(440.0*Math.pow(2.0,(i-69.0)/12.0)));
|
|
if (h == last) {
|
|
continue;
|
|
}
|
|
|
|
invh = 1.0/h;
|
|
for (j=1;j<=h;j++) {
|
|
m = ((m = Math.cos((j-1)*(0.5*Math.PI)/invh))*m)/j;
|
|
|
|
for (k=0;k<4096;k++) {
|
|
f = m*stab[(j*k)&4095];
|
|
wavetable[1+(k+(i<<12))] += f;
|
|
|
|
if (j&1) {
|
|
wavetable[524288+k+(i<<12)] += f;
|
|
}
|
|
}
|
|
}
|
|
|
|
last = h;
|
|
}
|
|
|
|
var max0 = 0;
|
|
var max1 = 0;
|
|
for (var 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 (var i=0;i<524288;i++) {
|
|
wavetable[i]*=max0;
|
|
wavetable[524288+i]*=max1;
|
|
}
|
|
}
|