// synth chords
function chd(rt, nt, sw) {
const chrr = [chord(f4, chords['7sus4']),
chord(ab3, chords['M7']),
chord(c4, chords['m7']),
chord(db3, chords['M7'])];
let y = 0.;
const ft = .5 * rt;
const ev = env_adc(fract(ft), .4, .64);
const crr = chrr.ring(ft);
for (let i = 0; i < crr.length; i++) {
const x = max(.6, fract(ft)) - cos(.7 * sin(sw * rt) + midi_to_hz(crr.ring(i)) * tau * rt);
y += sin(3 * fract(.5 + x)) + cos(mod(x, tau));
}
y *= .3 * ev;
return y;
}
// strings
const instrument = { vio0: 0, vio1: 1, vio2: 2 };
function strings(rt, mt, type) {
const melscl = scale(f3, scales['melodic_minor_desc'], 2);
let y = 0;
switch (type) {
case instrument.vio0: {
const ft = mt * rt;
y = (cos(1.57 * mod(midi_to_hz(melscl.ring(ft)) * rt * tau, tau))
+ sin(sin(2 * rt) * 3.14 * mod(midi_to_hz(melscl.ring(ft)) * rt * tau, PI / 2)))
* pow(min(1., 10. * fract(ft)) * (1. - fract(ft)), 2.);
};break;
case instrument.vio1: {
const ft = mt * rt;
y = sin(sin(8 * rt) * 2. + 0.523 * mod((2 * midi_to_hz(melscl.ring(ft)) * rt * tau), PI))
* env_adc(fract(ft), .22, .345);
};break;
case instrument.vio2: {
const ft = mt * rt;
y = (sin(2 + 0.785 * mod(midi_to_hz(melscl.ring(ft)) * rt * tau, PI))
+ cos(sin(3 * rt) * 2. + 0.628 * mod(midi_to_hz(melscl.ring(ft)) * rt *tau, tau)))
* env_adc(fract(ft), .124, .467);
};break;
}
return y;
}
//################# InstumentGroup
const InstGr = class {
constructor() {}
process(time) {
let inst0 = 0 //violine
, inst1 = 0 //cello
, inst2 = 0 //cello
, inst3 = 0 //staccato
, inst4 = 0; //chords
inst4 = chd(.15 + time, time, 24) * .7 + chd(.05 + .5025 * time, .5 * time, 12);
// phaseoffset * [...scalestart] * time
inst3 = strings(1.0025 * [1, 4, 3].ring([1, 2].ring(time) * time) + .5 * time, [4, 6].ring(.125 * time),instrument.vio2);
inst3 += .9 * strings(.50005 * [7, 9, 5].ring(.5 * time) + .5 * time, [4, 6].ring(.125 * time), instrument.vio2);
inst0 = strings(.50005 * [5, 4, 7].ring(time) + time, [6, 4].ring(.5 * time), instrument.vio1);
let htime = time * .5; // mod(time * .5, 32);
inst1 = strings(1.0025 * [1, 8, 3].ring(htime) + mod(-.5 * htime, 3) + htime, 4, instrument.vio0)
* lerp(1.,.83, mod(.25 * htime, 1))
inst2 = strings(1.00005 * [3, 5, 1].ring(htime) + mod(-htime, 2) + .5 * htime, 4, instrument.vio0)
* lerp(.77,1., mod(.25 * htime, 1));
inst0 *= .25 * gainEnv(mod(time, 32), 9, 30, 3);
inst1 *= .17 * gainEnv(time, 18, 60, 1);
inst1 += (time > 24 ? (.19 * strings(time, 4, instrument.vio0) * gainEnv(mod(time, 14), 7, 2, 2)) : 0);
inst2 *= .22 * min(1, time/22);
inst3 *= .17 * min(1, time/5);
inst4 *= .7;
return { inst0Out: inst0, inst1Out: inst1, inst2Out: inst2, inst3Out: inst3, inst4Out: inst4 };
}
}
// ################ MIX
const MIX = class {
constructor() {
this.period = 0;
this.cmain = new InstGr();
this.crev = [
new Rev_Mn(), new Rev_Mn(), new Rev_Mn(),new Rev_Mn(),
new Rev_Mn(), new Rev_Mn(),
new Rev_Mn(), new Rev_Mn(), new Rev_Mn(), new Rev_Mn()];
this.fltr = new FilterBank();
this.swidth = [new FilterBank(),new FilterBank() ];
this.eq3band = [new Band3Eq(), new Band3Eq()];
}
process() {
let inst0 = 0//violine
,inst1 = 0//cello
,inst2 = 0//cello
,inst3 = 0//staccato
,inst4 = 0;//chords
let rev_sum = [0, 0], bas_sum = [0,0];
this.period += 1;
let time = this.period / ditty.sampleRate;
const cmain = this.cmain.process(time);
inst0 = this.fltr.svf_opt2(cmain.inst0Out, 460, 1., -12., 2, 0);
inst1 = this.fltr.svf_opt2(cmain.inst1Out, 2300, 1, -7., 8, 1);
inst2 = this.fltr.svf_opt2(cmain.inst2Out, 1800, 1, -10., 8, 2);
inst3 = this.fltr.svf_opt2(cmain.inst3Out, 1600, 1, -12., 8, 3);
inst4 = cmain.inst4Out;
inst4 = lerp(inst4, this.fltr.svf_opt2(inst4, 4200, .7, 3, 1, 4), .7);
let drysum = tanh(2 * ( .7 * inst0 + .3 * inst3 + .4 * inst4 + ( .5 * (inst1 + inst2)))) ;
drysum = this.fltr.svf_opt2(drysum, 40, .7, -6., 2, 5);
let stx = min(1.1, (.75 + smoothstep(0, 16, mod(floor(time), 16.)) ));
// difference +/- 6 will induce a slight panning, which helps with frequency separation
rev_sum[0] = this.crev[0].process(inst0, 0, 25, 0.46);
rev_sum[1] = this.crev[1].process(inst0, 8, 25, 0.47);
bas_sum[0] = this.crev[2].process(inst1, 0, 46, 0.69);
bas_sum[1] = this.crev[3].process(inst1, 8, 46, 0.63);
bas_sum[0] += this.crev[4].process(inst2, 0, 50, 0.42);
bas_sum[1] += this.crev[5].process(inst2, 8, 50, 0.39);
rev_sum[0] += this.crev[6].process(inst3, 0, 30, 0.46);
rev_sum[1] += this.crev[7].process(inst3, 8, 30, 0.41);
rev_sum = this.swidth[0].stwidth(rev_sum, 1.1);
rev_sum[0] += this.crev[8].process(inst4, 0, 32, 0.56);
rev_sum[1] += this.crev[9].process(inst4, 8, 32, 0.61);
bas_sum = this.swidth[1].stwidth(bas_sum, .83);
rev_sum[0] += bas_sum[0] + drysum;
rev_sum[1] += bas_sum[1] + drysum;
// master out
const veq = {lowG:8, midG:-2, hghG:5,lowMidFq:260,midHghFq:2800,masterG:3};
rev_sum[0] = this.eq3band[0].process(rev_sum[0], veq );
rev_sum[1] = this.eq3band[1].process(rev_sum[1], veq );
return rev_sum;
}
}
// ################ REVERB
const Rev_Mn = class {
constructor() {
this.zn = Array.from({length: 8}, () => Array(2639).fill(0));
this.dcBlockState = Array.from({length: 17}, () => [0, 0]);
this.period = 0;
}
process(rv_xin, offs, rtail, scl) {
this.period += 1;
let y = [0, 0, 0, 0, 0, 0, 0, 0];
let rv = 0,itter = 0;
const mat0 = [1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, -1, -1, 1, -1, 1];
const mat1 = [-1, 1, -1, 1, 1, -1, 1, 1, 1, -1, 1, -1, 1, 1, -1, -1];
const mat2 = [1, 1, 1, 1, 1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1];
let dt = [1637, 1277, 1607, 2639, 1823, 751, 1823, 2591];
dt = dt.map((n, idx) => (scl * dt[idx]) | 0);
for (let i = 0; i < 8; i++) {
const it8 = i % 8;
const pdt = this.period % dt[it8];
const g3 = exp(-(1 * rtail * 0.022675736) / (i + 1));
for (let it = 0; it < 2; it++) {
const g4 = exp(-((1.41421 / rtail) * 0.0022675736) * (itter + 1));
y[it8] = this.dcblock((this.zn[it8][pdt] * mat0[itter % 16]), g4, itter);
itter++;
}
this.zn[it8][pdt] = g3 * y[it8] + mat2[i % 8 + offs] * rv_xin;
rv += mat1[i % 8 + offs] * y[it8];
}
rv = this.dcblock(rv, .997, 16);
return rv * 0.25;
}
dcblock(xin, at, instnc) {
let state = this.dcBlockState[instnc];
let y0 = xin - state[0] + at * state[1];
state[0] = xin;
state[1] = y0;
return y0;
}
};
//################ FILTERBANK
class FilterBank {
constructor() {
this.svf_opt2State = Array.from({length: 8}, () => [0, 0]);
this.dcBlockState = Array.from({length: 8}, () => [0, 0]);
//this.sfv_type = {low: 0,band: 1,high: 2,notch: 3,peak: 4,all: 5,bell: 6,lsf: 7,hsf: 8};
}
// http://www.cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf
svf_opt2(v0, cutoff, Q, bellgaindB, type, instance) {
let m = [0, 0, 0];
let a1 = 0, a2 = 0, a3 = 0;
let v1 = 0, v2 = 0, v3 = 0;
let ic1eq = this.svf_opt2State[instance][0]
, ic2eq = this.svf_opt2State[instance][1];
const A = Math.pow(10.0, bellgaindB / 40.0);
let g = tan(PI * cutoff / 44100);
if (type == 7) g /= sqrt(A) // low shelf
else if (type == 8) g *= sqrt(A) // high shelf
let k = 1 / (type == 6 ? (A * Q) : Q); //bell A*Q
const filter = [[0, 0, 1], [0, 1, 0], [1, -k, -1], [1, -k, 0], [1, -k, -2],
[1, -2 * k, 0], [1, k * (A * A - 1), 0], [1, k * (A - 1), (A * A - 1)], [A * A, k * (1 - A) * A, (1 - A * A)]];
a1 = 1 / (1 + g * (g + k));
a2 = g * a1;
a3 = g * a2;
m = filter[type];
v3 = v0 - ic2eq;
v1 = a1 * ic1eq + a2 * v3;
v2 = ic2eq + a2 * ic1eq + a3 * v3;
ic1eq = 2 * v1 - ic1eq;
ic2eq = 2 * v2 - ic2eq;
this.svf_opt2State[instance][0] = ic1eq;
this.svf_opt2State[instance][1] = ic2eq;
return m[0] * v0 + m[1] * v1 + m[2] * v2;
}
stwidth(xin, kw) {
const tmp = 0.5 * kw;
const nL = tmp * (xin[1] - xin[0]);
const nR = (1.0 - tmp) * (xin[1] + xin[0]);
return [nR + nL, nR - nL]
}
dcblock(xin, at, instnc) {
let state = this.dcBlockState[instnc];
let y0 = xin - state[0] + at * state[1];
state[0] = xin;
state[1] = y0;
return y0;
}
};
//Easy peasy equalizer, not the proper way to do it.
//I've just gotten used to how it sounds during composition.
// ################ Band3EQ
class Band3Eq {
constructor() {
this.lp1 = 0;
this.hp1 = 0;
}
process(xin, veq) {
const { lowG, midG, hghG, lowMidFq, midHghFq, masterG } = veq;
const adb = 8.656170245;
// Compute filter coefficients
const xLP = exp(-2.0 * PI * lowMidFq / 44100.0);
const a0LP = 1.0 - xLP;
const b1LP = (-xLP);
const xHP = exp(-2.0 * PI * midHghFq / 44100.0);
const a0HP = 1.0 - xHP;
const b1HP = (-xHP);
// Low-pass filter calculation
const lpOut = a0LP * xin - b1LP * this.lp1;
this.lp1 = lpOut;
// High-Pass filter calculation
let hpOut = xin - (a0HP * xin - b1HP * this.hp1);
this.hp1 = hpOut;
// Decompose the signal into frequency bands and apply volume levels
const lowBand = lpOut * exp(lowG / adb);
const midBand = (xin - lpOut - hpOut) * exp(midG / adb);
const highBand = hpOut * exp(hghG / adb);
// Sum the frequency bands and apply the master volume level
return (lowBand + midBand + highBand) * exp(masterG / adb);
}
}
// ++++++++++++++++++++++++++++++++++
ditty.bpm = 1.01047; // repeat at ~48 sec
loop( () => {
const playme = synth.def(MIX, { name: 'synth', amp: 1, duration:1 });
playme.play();
}, { sync:1, name: 'my first loop' });
// ++++++++++++++++++++++++++++++++++
const smoothstep = (e0, e1, value) => {
const x = max(0, min(1, (value - e0) / (e1 - e0)));
return x * x * (3 - 2 * x);
};
function gainEnv(time, fadeIn, sustain, fadeOut) {
const totalDuration = fadeIn + sustain + fadeOut;
// Fade-in
if (time < fadeIn) {
return smoothstep(0, fadeIn, time);
}
// Sustain
if (time >= fadeIn && time <= fadeIn + sustain) {
return 1;
}
// Fade-out
if (time > fadeIn + sustain) {
const fadeOutStartTime = fadeIn + sustain;
const fadeOutEndTime = totalDuration;
return 1 - smoothstep(fadeOutStartTime, fadeOutEndTime, time);
}
return 0;
}
const {sqrt,PI,floor,sin,abs,max,min,exp,cos,tanh,pow,tan} = Math;
const env_d = (t, d) => exp(-t * 5.436563 / d);
const env_ad = (t, a, d) => min(t / max(1e-5, a), env_d((t - a), d));
const env_adc = (t, a, d) => clamp(min(t / max(1e-5, a), env_d((t - a), d)), 0, 1);
const tau = (PI * 2.0);
const fract = (x) => (x - floor(x));
const mod = (x, y) => (x - floor(x / y) * y);
const msat = (x, a) => 0.5 * (abs(x + a) - abs(x - a));
const softkn = (x, kn) => x / (kn * abs(x) + 1.0);
const softkna = (x, kn, ad) => x / (kn * msat(x, ad) + 1.0);
const dbg = (x) => debug.warn(' ', x);