Simplified clarinet
Using DWG synthesis. Note how this synthesis technique is able to capture variations of timbre due to nuance, even though the model is very simple.
Log in to post a comment.
// A very simplified "clarinet" physical model,
// consisting in:
// delay line, lowpass filter,
// first-order allpass interpolation as proposed here
// https://ccrma.stanford.edu/~jos/pasp/First_Order_Allpass_Interpolation.html#sec:apinterp
// and finally a nonlinear "logistic map" function from which the resonance emerges.
// Similar to what is proposed in
// https://ccrma.stanford.edu/~jos/swgt/Wind_Instruments.html
// but even simpler.
input.nuance = 0.25; // min=0, max=1, step=0.01
const clarinet = synth.def( class {
constructor(options) {
let freq = midi_to_hz(options.note);
let dly = 1 / (2*freq*ditty.dt); // duration of half a period in samples
let cutoff = options.cutoff;
this.a0 = clamp01(2*Math.PI*cutoff*ditty.dt);
let dly_lp = (1 - this.a0) / this.a0; // Delay induced by the lowpass filter
let dly_ap = (dly - dly_lp - 0.1) % 1 + 0.1; // Delay of the allpass filter
let dly_M = Math.round(dly - dly_lp - dly_ap); // Samples in the delay line
this.dl_s = new Float32Array(dly_M);
this.dl_pos = 0;
this.dl_M = dly_M;
this.eta = (1 - dly_ap) / (1 + dly_ap); // Coefficient of the allpass filter
// Initialize values to the steady state to avoid ugly transients / clicks
// See https://en.wikipedia.org/wiki/Logistic_map
let gamma = 2; // Initial value
let v0 = (gamma-1)/gamma;
for(let i=0; i<this.dl_M; i++) { this.dl_s[i] = v0; }
this.ap_ynm1 = v0;
this.ap_xnm1 = v0;
this.lp_znm1 = v0;
// Compensate the gain of the lowpass filter depending on the note played, so that nuance is homogeneous
let lp_gain = 1 / Math.sqrt(1 + (0.5*freq/cutoff)**2);
this.lp_gain_compensation = 1/lp_gain;
}
process(note, env, tick, options) {
let gamma = 2 + (1 + 0.449*options.nuance*(1+options.vibAmt*Math.sin(options.vibHz*6.28*tick))) * env.value;
let xn = this.dl_s[this.dl_pos];
// Apply allpass filter (fractional delay)
// https://ccrma.stanford.edu/~jos/pasp/First_Order_Allpass_Interpolation.html#sec:apinterp
let yn = this.eta * (xn - this.ap_ynm1) + this.ap_xnm1;
this.ap_xnm1 = xn; this.ap_ynm1 = yn;
let noise = 0.001 * (Math.random() - 0.5) * env.value;
// Apply lowpass filter
let zn = this.lp_znm1 + this.a0 * (yn - this.lp_znm1 + noise);
let hp = yn - zn; // Also gather hipass signal
//let zn = this.lp_znm1 + this.a0 * (yn - this.lp_znm1); // DEBUG bypassing allpass
this.lp_znm1 = zn;
// Apply nonlinearity
let wn = gamma * zn * (1-zn); // logistic map
wn *= this.lp_gain_compensation;
wn = Math.max(wn, 0);
// Feed back into delay line
this.dl_s[this.dl_pos] = wn;
this.dl_pos++;
if(this.dl_pos >= this.dl_M) {
this.dl_pos -= this.dl_M;
}
// Return the high-passed signal
return hp * env.value;
}
}, {env: adsr, attack:0.06, decay: 0.1, sustain:0.98, release: 0.2, duration: 1, cutoff:1700, nuance:() => input.nuance,
vibAmt: 0.1, vibHz: 5
});
ditty.bpm = 60;
/*
loop( (i) => {
clarinet.play(d3, {env:adsr, duration: 5, cutoff:1500, pan:0.3, vibHz: 3});
clarinet.play(a3, {env:adsr, duration: 5, cutoff:1500, pan:-0.6, vibHz: 3.62});
clarinet.play(fs4, {env:adsr, duration: 5, pan:0.6, vibHz: 4.2});
clarinet.play(d5, {env:adsr, duration: 5, pan:-0.3, vibHz: 4.7});
sleep(6);
}, {name: "ensemble", amp:1.5});
loop( (i) => {
const nn = [d4, fs4, a4, b4, fs5, a5, d5, cs5, b4, a4, fs4, e4];
let nuance = input.nuance;// + 0.07*Math.random();
clarinet.play(nn.ring(i), {release:0.5, vibAmt:(tick) => 0.3*clamp01(2*tick-0.3), vibHz: 3+Math.random()*2,
nuance:nuance, cutoff:2000});
sleep(1);
}, {name:"solo clarinet", amp:5});
*/
// Nuance test
/*
loop( (i) => {
const nn = [d4, fs4, a4, b4, fs5, a5, d5, cs5, b4, a4, fs4, e4];
clarinet.play(nn.ring(i), {env:one, duration:10, nuance:(tick) => tick/10});
sleep(10);
});
*/
loop( (i) => {
// Adapted from Brahms - Clarinet Sonata No. 2 in Eb Major Op. 120
const nn = [eb5, d5, f5, eb5, g4, c5, ab4, d4, 0, c4, eb4, bb3, bb4, 0, d4, f4, c4, c5, 0, f4, ab5, g5, e5, f5, g4, ab4, eb5, d5, b4, c5, e4, f4, eb4, d4, eb4,0];
const bps = 1.8; // beats per second - I don't like that changing ditty.bpm changes the envelope durations
const dur = [1.5,0.5,0.5,0.5,0.5,0.5, 2.0, 1.3,0.2,0.5, 1.5, 0.5, 1.2, 0.3, 0.5,1.5,0.5,1.3, 0.2, 0.5, 1.5,0.5,0.5,0.5,0.5,0.5,1.5, 0.5,0.5,0.5,0.5,0.5,2.0,2.0,2.0,2.0];
const nu = [.01,.15,0.1, .15,.12,.13, .1, .2, .01,.15,.05,0.15, .25, 0.05, .2,.1, .3, .4, 0.4, 0.4,0.08,.2,0.1,.12, .1, .2, .1, .25, .1, .2, .15, .25, .15,.07,.05,.12]
let dur_i = dur.ring(i) / bps;
if(nn.ring(i)) {
clarinet.play(nn.ring(i), {duration:dur_i, attack:0.02, sustain:1, release:0.8,
nuance: (tick) => lerp(nu.ring(i), nu.ring(i+1), tick/dur_i)});
}
sleep(dur_i);
}, {amp:5});