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