A simplified flute simulation, manually retuned, playing the beginning of Gabriel Fauré's Pavane.
Log in to post a comment.
// 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.4});
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.3, offset:0.23});
flute.play(retune(notes2[i]), {duration:durs[i], pan:-0.2, amp:0.3, offset:0.06});
sleep(durs[i]);
}
sleep(1);
}, {name:"Flute, Oboe, Clarinet"});
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"});
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"});
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.3});
}
sleep(durs[i]);
}
}, {name: "Violin I"});