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"});