const ISQRT2 = Math.sqrt(0.5);
const SQRT8 = Math.sqrt(8);
const ISQRT8 = 1/SQRT8;
// const fibodelays = [1410,1662,1872,1993,2049,2114,2280,2610]; // X^8 = X+1
const fibodelays = [1467,1691,1932,2138,2286,2567,3141,3897]; // X^8 = X+2
let meandelay = fibodelays[3] * ditty.dt;
class Delayline {
constructor(n) {
this.n = n;
this.p = 0;
this.data = new Float32Array(n);
}
current() {
return this.data[this.p]; // using lastOut results in 1 sample excess delay
}
clock(input) {
this.data[this.p] = input;
if(++this.p >= this.n) {
this.p = 0;
}
}
}
const reverb = filter.def(class {
constructor(options) {
this.outgain = 0.3;
this.dls = [];
this.s0 = new Float32Array(8); // Lowpass filter memory
for(let i=0; i<8; i++) {
this.dls.push(new Delayline(fibodelays[i]));
this.s0[i] = 0;
}
}
process(input, options) {
let rt60 = options.rt60;
let loopgain = 10 ** (-3*meandelay / rt60) * ISQRT8;
let higain = 10 ** (-3*meandelay / options.rtHi) * ISQRT8;
let v = this.dls.map( (dl) => dl.current());
// Fast Walsh-Hadamard transform
// https://formulasearchengine.com/wiki/Fast_Walsh%E2%80%93Hadamard_transform
let w = [v[0]+v[4], v[1]+v[5], v[2]+v[6], v[3]+v[7],
v[0]-v[4], v[1]-v[5], v[2]-v[6], v[3]-v[7]];
let x = [w[0]+w[2], w[1]+w[3], w[0]-w[2], w[1]-w[3],
w[4]+w[6], w[5]+w[7], w[4]-w[6], w[5]-w[7]];
let y = [x[0]+x[1], x[0]-x[1], x[2]+x[3], x[2]-x[3],
x[4]+x[5], x[4]-x[5], x[6]+x[7], x[6]-x[7]];
y[0] += input[0]*SQRT8;
y[2] += input[1]*SQRT8;
let a0 = clamp01(2*Math.PI*options.cutoff*ditty.dt);
for(let i=0; i<8; i++) {
let hipass = y[i] - this.s0[i];
this.dls[i].clock(this.s0[i] * loopgain + hipass * higain);
this.s0[i] += a0 * hipass;
}
return [lerp(input[0], v[0], options.mix),
lerp(input[1], v[1], options.mix)];
}
}, {mix:0.2, rt60:1, cutoff:5000, rtHi:0.8});
input.reverbMix = 0.8; // min=0, max=1, step=0.01
input.reverbTime = 3.0; // min=0.1, max=10, step=0.1
input.reverbTimeHi = 1.3; // min=0.1, max=10, step=0.1
input.reverbCutoff = 4000; // min=200, max=10000, step=10
const hallverb = reverb.createShared({
mix:() => input.reverbMix,
rt60:() => input.reverbTime,
cutoff:() => input.reverbCutoff,
rtHi:() => input.reverbTimeHi
});
// A very simplified "physical modeling" flute using a single delay line, a nonlinearity, a wide bandpass filter, and noise.
// Basically:
//
// [ noise ] ---> (+) ---> [ nonlinearity ] ---> [ bandpass filter ] ---> output
// ^ |
// | |
// |------------------ [ delay line ] <----------------
//
// This model can do flute-like attacks quite nicely.
// However it can be quite out of tune due to the filters.
// One way to compensate for it is to let the filters follow the note as well (but that's cheating!)
// It is unable to perform transitions between notes, and
// does not account for the jump to higher harmonics when increasing blowing pressure.
// Make sure to play with the sliders!
// Flute
input.fluteGain = 1.23; // min=1, max=1.5, step=0.01
input.fluteOffset = 0.16; // min=0, max=0.5, step=0.01
input.fluteNoiseAmount = 0.05; // min=0.005, max=0.5, step=0.005
const saturate = (x) => x / Math.sqrt(1 + x*x);
// Adapted from https://forums.codeguru.com/showthread.php?473996-How-to-do-cubic-interpolation-with-an-audio-sample
const cubic_interpolate = (y0, y1, y2, y3, mu ) => {
let mu2 = mu*mu;
let a0 = y3 - y2 - y0 + y1; //p
let a1 = y0 - y1 - a0;
let a2 = y2 - y0;
let a3 = y1;
return ( a0*mu*mu2 + a1*mu2 + a2*mu + a3 );
};
const flute = synth.def(class {
constructor(options) {
let freq = midi_to_hz(options.note);
let delay_samples = 1 / (freq * ditty.dt); // Duration of one period in samples
this.len = Math.floor(delay_samples) + 2; // buffer size
this.fd = delay_samples % 1; // fractional delay to interpolate between samples
this.buf = new Float32Array(this.len); // buffer used to create a delay
this.pos = 0; // current position of the reading/writing head
let lpcutoff = 5*freq;
this.a0 = clamp01(2 * Math.PI * lpcutoff * ditty.dt); // Lowpass filter the reinjection
let hpcutoff = 0.1*freq;
this.a1 = clamp01(2 * Math.PI * hpcutoff * ditty.dt); // hipass filter the reinjection
this.s0 = 0; // Signal value history for the lowpass filter
this.s1 = 0; // Signal value history for the hipass filter
this.windEnv = adsr.create({attack: 0.05, release: 0.1, duration:options.duration});
}
process(note, env, tick, options) {
let pos = this.pos;
/*
let value = lerp(this.buf[pos],
this.buf[(pos+1)%this.len],
this.fd); // linear interpolation for the fractional delay
*/
let value = cubic_interpolate(this.buf[pos],
this.buf[(pos+1)%this.len],
this.buf[(pos+2)%this.len],
this.buf[(pos+3)%this.len],
this.fd);
/*
if(tick < options.duration) {
// Add noise
value += input.fluteNoiseAmount * (Math.random() - 0.5);
// Nonlinearity to gain amplitude
value = input.fluteGain * saturate(value + input.fluteOffset);
} else {
value *= 0.95;
}*/
// Add noise
value += this.windEnv.value * input.fluteNoiseAmount * (Math.random() - 0.5);
// Nonlinearity to gain amplitude
let offset = (options.offset >= 0) ? options.offset : input.fluteOffset;
value = saturate(value + offset);
// Lower amplitude when we stop blowing
value *= lerp(0.95, input.fluteGain, this.windEnv.value);
this.s0 += this.a0 * (value - this.s0); // lowpass filter
this.s1 += this.a1 * (this.s0 - this.s1); // hipass filter
this.buf[pos] = this.s0 - this.s1;
this.pos = (pos+1)%this.len;
return this.buf[pos] * env.value; // Apply envelope to avoid clicks
}
}, {env:adsr, attack:0.05, offset:-1, duration:1, release:1});
// lpcutoff:2000, hpcutoff:50
ditty.bpm = 60;
// Manually adjusted retuning, starting from C4
// Only valid for the default parameters!
const out_of_tune = [
-31,-30,
-29,-26,-26,-26,-28,
-24,-27,-25,-25,-18,
-22, -31, -15, -16-7-9, -14-2,
-26+2, -29, -19-7+16, -28+8+16, -27+15+5,
-13+5-20+1, -24-8+22-4, -9, -11-22, -15,
0, -10, -25, 6, 22,
-36, 29];
// Try to correct the pitch of the note
const retune = (nn) => {
let i = nn - 48;
if(i < 0) {
return nn - 0.01*out_of_tune[0];
}
if(i < out_of_tune.length) {
return nn - 0.01*out_of_tune[i];
}
return nn;
};
//let i = 48;
//flute.play(retune(i));
// Single note
// Try to turn the gain all the way down, and then to some value, to simulate the attack of the flute.
//flute.play(fs4, {duration: 120});
// First few bars of Fauré's Pavane
loop( () => {
// Flute part
let notes = [fs4, gs4, a4, b4, a4, gs4, a4,fs4,gs4,a4,gs4,fs4,gs4,e4,fs4,f4,cs4];
let durs = [1.75, 0.25, 1.75, 0.25, 0.5, 0.5, 0.5, 0.5, 1.75, 0.25, 0.5, 0.5, 0.5, 0.5, 1.75, 0.25, 4];
for(let i=0; i<notes.length; i++){
flute.play(retune(notes[i]), {duration:durs[i], pan:-0.2, amp:0.3});
sleep(durs[i]);
}
// Oboe + clarinet
let notes1 = [cs5, d5, e5, fs5, e5, d5, e5, cs5, d5, e5, d5, cs5, d5, b4, cs5, c5, cs5];
let notes2 = [a4, b4, cs5, d5, cs5, b4, cs5, a4, b4, cs5, b4, a4, b4, g4, a4, gs4, gs4];
durs = [1.75, 0.25, 1.75, 0.25, 0.5, 0.5, 0.5, 0.5, 1.75, 0.25, 0.5, 0.5, 0.5, 0.5, 1.5, 0.5, 3];
for(let i=0; i<notes1.length; i++){
flute.play(retune(notes1[i]), {duration:durs[i], pan:0.2, amp:0.25, offset:0.23});
flute.play(retune(notes2[i]), {duration:durs[i], pan:-0.2, amp:0.25, offset:0.06});
sleep(durs[i]);
}
sleep(1);
}, {name:"Flute, Oboe, Clarinet"}).connect(hallverb);
loop( () => {
// Cello part
let notes = [fs3, d3, e3, cs3, d3, b2, cs3, b2, a2, fs2, b2, e2, a2, d3, cs3, 0];
for(let i=0; i<notes.length; i++) {
flute.play(retune(notes[i]), {duration:0.75, pan:0.4, amp:0.6});
sleep(2);
}
}, {name:"Cello", amp:0.45}).connect(hallverb);
loop( () => {
// Viola part
let notes = [0, a3, cs4, a3, 0, cs4, fs4, cs4, 0, b3, e4, b3, 0, b3, e4, b3,
0, a3, d4, a3, 0, a3, d4, a3, 0, gs3, f4, gs3, 0, gs3, f4, gs3,
0, fs4, cs4, fs4, 0, cs4, a3, cs4, 0, fs4, a3, fs4, 0, b3, g3, b3,
0, e4, g3, e4, 0, fs4, gs3, fs4, 0, gs3, cs4, d4, b3, cs4, a3, gs3];
for(let i=0; i<notes.length; i++){
if(notes[i]) {
flute.play(retune(notes[i]), {duration:0.3, pan:-0.3, amp:0.3});
}
sleep(0.5);
}
}, {name: "Viola"}).connect(hallverb);
loop( () => {
let notes = [0, a4, g4, fs4, f4, 0];
let durs = [4*4+2, 4, 4, 2, 3, 1];
for(let i=0; i<notes.length; i++) {
if(notes[i]) {
flute.play(retune(notes[i]), {duration: durs[i], pan:-0.6, amp:0.25});
}
sleep(durs[i]);
}
}, {name: "Violin I"}).connect(hallverb);