desc:GFX Haas Doubler //tags: stereo delay doubler haas width //author: JGWizrad / Garden FX (GFX) // // Requires gfx-drift, gfx-input, gfx-output and gfx-ui (.jsfx-inc) in the same folder. // // Modes: // L/R - dry on one side, delayed copy on the other (classic Haas) // LCR - dry centre, delayed copies hard L and R (R = delay x multiplier) // Spread - delayed copies only, L and R (R = delay x multiplier) // Dry off = wet only, for use on an FX send. // Drift: smoothed random wander of time, pan and level per copy. The DRIFT dial scales the three drift // amounts (50% = the drift settings, 100% = twice, 0 = none) and speeds the wander up with them. // Detune: each side pitch-shifted by +/- cents (a dual-tap delay-line shifter), for the classic // detuned-doubler width. In L/R mode the dry side takes its side's detune too. // IN / OUT: TRIM (with the GUARD key), SAT and the OUT stage (OFF / CLIP / LIM) sit on the panel. import gfx-drift.jsfx-inc import gfx-input.jsfx-inc import gfx-output.jsfx-inc import gfx-ui.jsfx-inc import gfx-panel.jsfx-inc import gfx-dyn.jsfx-inc import gfx-faceplate.jsfx-inc import gfx-instrument.jsfx-inc slider1:1<0,2,1{L/R,LCR,Spread}>-Mode slider2:12<1,40,0.1>-Delay (ms) slider3:0<0,4,1{x2,x3,x4,x5,x6}>-Right multiplier slider4:0<0,1,1{Off,On}>-Swap sides slider5:0<-24,6,0.1>-Doubled level (dB) slider6:150<20,1000,1>-Low cut (Hz) slider7:100<0,100,1>-Width (%) slider8:0.5<0,5,0.01>-Time drift (ms) slider9:15<0,100,1>-Pan drift (%) slider10:0.5<0,6,0.1>-Level drift (dB) slider11:0.25<0.02,2,0.01>-Drift speed (Hz) slider12:0<0,1,1{Mono sum,Stereo}>-Input slider13:0<-24,12,0.1>-Output (dB) slider14:0<0,1,1{Off,On}>-Mono check slider15:1<0,1,1{Off,On}>-Dry slider16:0<0,100,1>-Band sat (%) slider17:30<0,100,1>-Bias (%) slider18:120<40,400,1>-Sat low band (Hz) slider19:6000<2000,16000,10>-Sat high band (Hz) slider20:-0.3<-24,0,0.1>-Ceiling / limit threshold (dB) slider21:1<0,2,1{Off,Clip,Limit}>-Output stage slider22:0<-24,12,0.1>-Input trim (dB) slider23:1<0,1,1{Off,On}>-Input guard slider24:0<-50,50,0.1>-Detune left (cents) slider25:0<-50,50,0.1>-Detune right (cents) slider26:50<0,100,1>-Drift (%) in_pin:left input in_pin:right input out_pin:left output out_pin:right output @init #ui_man = "gfx-haas-doubler"; // the ? pop-up links to this manual // 12 dB/oct high-pass (shared coefficients, per-instance state) function hp_proc(x) instance(z1, z2) local(y) ( y = hb0 * x + z1; z1 = hb1 * x - ha1 * y + z2; z2 = hb2 * x - ha2 * y; y; ); // Fractional delay read, 4-point Hermite interpolation function dread(buf, d) local(rp, i, f, xm1, x0, x1, x2) ( rp = wp - d; rp < 0 ? rp += bufsize; i = floor(rp); f = rp - i; xm1 = buf[(i + bufsize - 1) & mask]; x0 = buf[i & mask]; x1 = buf[(i + 1) & mask]; x2 = buf[(i + 2) & mask]; ((((0.5 * (x2 - xm1) + 1.5 * (x0 - x1)) * f + (xm1 - 2.5 * x0 + 2 * x1 - 0.5 * x2)) * f + 0.5 * (x1 - xm1)) * f + x0); ); // Pitch shift: two taps sweeping a 30 ms window half a cycle apart, Hann crossfaded. // rt = 1 - pitch ratio (samples of delay added per sample). amt fades the shifter in and out of bypass // so the knob passing through 0 does not click (at exactly 0 a static two-tap read would comb-filter). function pshift(buf, d0, rt, on) instance(ph, amt) local(ws, p2, y, w) ( amt += ((on ? 1 : 0) - amt) * psm; amt < 0.0001 ? ( ph = 0; dread(buf, d0); ) : ( ws = 0.03 * srate; ph += rt / ws; ph -= floor(ph); p2 = ph + 0.5; p2 -= floor(p2); w = sin($pi * ph); w *= w; y = w * dread(buf, d0 + ph * ws) + (1 - w) * dread(buf, d0 + p2 * ws); amt > 0.9999 ? y : amt * y + (1 - amt) * dread(buf, d0); ); ); function update() local(w0, cw, al, q0) ( mode = slider1; base = slider2; rmult = slider3 + 2; sideA = slider4 ? 1 : -1; sideB = -sideA; dbl_t = 10 ^ (slider5 / 20); slider6 != lastHP ? ( lastHP = slider6; w0 = 2 * $pi * min(slider6, srate * 0.45) / srate; cw = cos(w0); al = sin(w0) / (2 * 0.70710678); q0 = 1 + al; hb0 = (1 + cw) * 0.5 / q0; hb1 = -(1 + cw) / q0; hb2 = hb0; ha1 = -2 * cw / q0; ha2 = (1 - al) / q0; ); wid_t = slider7 / 100; drift_macro(slider26); tdrift = slider8 * dm_a; pdrift = min(slider9 / 100 * dm_a, 1); ldrift = slider10 * dm_a; dspeed = slider11 * dm_s; inmode = slider12; out_t = 10 ^ (slider13 / 20); monochk = slider14; dryon = slider15; in_setup(slider22, slider23); ost_setup(slider16 / 100, slider17 / 100, slider18, slider19, slider20, slider21); // detune follows the side each copy sits on cA = sideA < 0 ? slider24 : slider25; cB = sideA < 0 ? slider25 : slider24; rtA = 1 - 2 ^ (cA / 1200); rtB = 1 - 2 ^ (cB / 1200); onA = abs(cA) >= 0.05; onB = abs(cB) >= 0.05; mode == 0 ? ( dA_t = 0; dB_t = base; ) : ( dA_t = base; dB_t = base * rmult; ); ); // ---------- presets (leave Dry, Swap, Input, Output, Mono check alone) ---------- function setp(m, d, x, lv, lc, w, td, pd, ld, sp) ( slider1 = m; slider2 = d; slider3 = x; slider5 = lv; slider6 = lc; slider7 = w; slider8 = td; slider9 = pd; slider10 = ld; slider11 = sp; ); function load_preset(p) ( p == 1 ? setp(1, 6, 0, -3, 150, 100, 0.2, 5, 0.3, 0.2 ) : // Short p == 2 ? setp(1, 12, 0, 0, 150, 100, 0.5, 15, 0.5, 0.25) : // Med p == 3 ? setp(1, 20, 0, -4, 200, 100, 0.8, 15, 0.5, 0.2 ) : // Long p == 4 ? setp(0, 18, 0, 0, 120, 100, 0, 0, 0, 0.25) : // Classic Haas p == 5 ? setp(2, 12, 0, 0, 100, 100, 0.6, 20, 0.5, 0.3 ) : // Wide Spread p == 6 ? setp(2, 12, 1, 0, 120, 100, 4, 60, 3, 1.5 ) : // Super Wobble p == 7 ? setp(0, 22, 0, -1, 80, 70, 2.5, 0, 1, 0.7 ) : // Vintage ADT p == 8 ? setp(1, 8, 1, -6, 300, 70, 0.3, 10, 0.3, 0.2 ); // Mono Safe p >= 1 && p <= 8 ? slider_automate(2039); // sliders 1,2,3,5-11 p == 9 ? ( // Reset all to defaults slider1 = 1; slider2 = 12; slider3 = 0; slider4 = 0; slider5 = 0; slider6 = 150; slider7 = 100; slider8 = 0.5; slider9 = 15; slider10 = 0.5; slider11 = 0.25; slider12 = 0; slider13 = 0; slider14 = 0; slider15 = 1; slider16 = 0; slider17 = 30; slider18 = 120; slider19 = 6000; slider20 = -0.3; slider21 = 1; slider22 = 0; slider23 = 1; slider24 = 0; slider25 = 0; slider26 = 50; slider_automate(8388607); slider_automate(58720256); // sliders 1-23, 24-26 ); ); // ---------- state ---------- bufsize = 131072; mask = bufsize - 1; bufA = 0; bufB = bufsize; memset(0, 0, bufsize * 2); wp = 0; msr = srate / 1000; sm = 1 - exp(-1 / (0.03 * srate)); psm = 1 - exp(-1 / (0.02 * srate)); tdA.drift_init(0.8 + rand(0.5)); tdB.drift_init(0.8 + rand(0.5)); pdA.drift_init(0.8 + rand(0.5)); pdB.drift_init(0.8 + rand(0.5)); ldA.drift_init(0.8 + rand(0.5)); ldB.drift_init(0.8 + rand(0.5)); hpA.z1 = 0; hpA.z2 = 0; hpB.z1 = 0; hpB.z2 = 0; ost_init(300000); ostL.ost_ch_init(); ostR.ost_ch_init(); in_init(); vuc = 1 - exp(-1 / (0.01 * srate)); vu_ms = 0; !ui_dflt ? ui_thm = 2; !ui_dflt ? ui_hw = 0; // defaults for new instances: Studio, flat (saved projects keep their own) lastHP = -1; update(); dA_s = dA_t * msr; dB_s = dB_t * msr; dbl_s = dbl_t; out_s = out_t; wid_s = wid_t; dry_s = dryon; pkL = 0; pkR = 0; ui_dflt = 1; // theme defaults above apply once: @init re-runs (playback start, device reset) must not undo the saved theme @slider update(); @block update(); @serialize file_var(0, ui_thm); file_var(0, ui_sz); file_var(0, ui_hw); file_avail(0) != 0 ? file_var(0, pr_cur); // the preset in use (older projects end before it) @sample // input stage: DC block, trim, guard in_process(spl0, spl1); inL = in_l; inR = in_r; vu_ms += (0.5 * (inL * inL + inR * inR) - vu_ms) * vuc; inmode == 0 ? ( sL = 0.5 * (inL + inR); sR = sL; ) : ( sL = inL; sR = inR; ); bufA[wp] = sL; bufB[wp] = sR; // drift dinc = dspeed / srate; tA = tdA.drift_tick(dinc); tB = tdB.drift_tick(dinc); pA = pdA.drift_tick(dinc); pB = pdB.drift_tick(dinc); lA = ldA.drift_tick(dinc); lB = ldB.drift_tick(dinc); // parameter smoothing dbl_s += (dbl_t - dbl_s) * sm; out_s += (out_t - out_s) * sm; wid_s += (wid_t - wid_s) * sm; dry_s += (dryon - dry_s) * sm; dA_s += ((dA_t + tdrift * tA) * msr - dA_s) * sm; dB_s += ((dB_t + tdrift * tB) * msr - dB_s) * sm; dA = min(max(dA_s, 2), bufsize - 4); dB = min(max(dB_s, 2), bufsize - 4); // voices mode == 0 ? ( yA = onA || psA.amt > 0.0001 ? psA.pshift(bufA, 2, rtA, onA) : sL; gA = dry_s; // L/R: voice A is the dry side cL = 0; cR = 0; ) : ( yA = hpA.hp_proc(psA.pshift(bufA, dA, rtA, onA)); gA = dbl_s; mode == 1 ? ( cL = sL * dry_s; cR = sR * dry_s; ) : ( cL = 0; cR = 0; ); ); yB = hpB.hp_proc(psB.pshift(bufB, dB, rtB, onB)); gB = dbl_s; gA *= exp(ldrift * lA * 0.11512925); gB *= exp(ldrift * lB * 0.11512925); // pan (wanders inward from its side), equal-power law posA = sideA * (1 - pdrift * (0.5 + 0.5 * pA)); posB = sideB * (1 - pdrift * (0.5 + 0.5 * pB)); angA = (posA + 1) * 0.25 * $pi; angB = (posB + 1) * 0.25 * $pi; oL = cL + yA * gA * cos(angA) + yB * gB * cos(angB); oR = cR + yA * gA * sin(angA) + yB * gB * sin(angB); // width, mono check, output mid = 0.5 * (oL + oR); side = 0.5 * (oL - oR) * wid_s; monochk ? side = 0; // output stage: band saturation -> output gain -> clipper / limiter oL = ostL.ost_band(mid + side) * in_comp * out_s; oR = ostR.ost_band(mid - side) * in_comp * out_s; ost_out(oL, oR); spl0 = ost_l; spl1 = ost_r; pkL = max(pkL, abs(spl0)); pkR = max(pkR, abs(spl1)); wp = (wp + 1) & mask; // for display dispPA = posA; dispPB = posB; dispGA = gA; dispGB = gB; dispDA = dA / msr; dispDB = dB / msr; @gfx 640 392 ui_begin(640, 392); ui_swb(); ui_swbg(14, 626, 45, 348, 392); // drawn off-screen, then shown with the preset row moved up under the header ui_header("DOUBLER"); // ---------- header controls ---------- ui_theme_picker(148 * sc, 17 * sc); ui_size_picker(184 * sc, 13 * sc); ui_hw_toggle(248 * sc, 13 * sc); // the preset row under the header: PRESETS, prev / next, LCD (click it for the list), at the right; // designed below the panel and shown at the top ui_swu(45 - 348); pk_pextra = "Reset all to defaults"; k_ = pk_preset(244 * sc, 356 * sc, 380 * sc, 30 * sc, pr_cur, "Short|Med|Long|Classic Haas|Wide Spread|Super Wobble|Vintage ADT|Mono Safe"); k_ > 0 ? ( load_preset(k_); pr_cur = k_ > 8 ? 0 : k_; ); ui_swu(0); ui_button(410 * sc, 10 * sc, 70 * sc, 24 * sc, "L/R", slider1 == 0, 1) ? ( slider1 = 0; slider_automate(slider1); ); ui_button(482 * sc, 10 * sc, 70 * sc, 24 * sc, "LCR", slider1 == 1, 1) ? ( slider1 = 1; slider_automate(slider1); ); ui_button(554 * sc, 10 * sc, 70 * sc, 24 * sc, "SPREAD", slider1 == 2, 1) ? ( slider1 = 2; slider_automate(slider1); ); ui_swu(44); // the panel, shown below the preset row // ---------- display window: stereo stage ---------- ui_screen(16 * sc, 52 * sc, 454 * sc, 80 * sc); lx = 46 * sc; lw = 394 * sc; my = 92 * sc; ui_col(C_SG); gfx_line(lx, my, lx + lw, my); gfx_line(lx, my - 6 * sc, lx, my + 6 * sc); gfx_line(lx + lw * 0.5, my - 4 * sc, lx + lw * 0.5, my + 4 * sc); gfx_line(lx + lw, my - 6 * sc, lx + lw, my + 6 * sc); gfx_setfont(2); ui_col(C_ST); gfx_x = 28 * sc; gfx_y = my - 6 * sc; gfx_drawstr("L"); gfx_x = 450 * sc; gfx_y = my - 6 * sc; gfx_drawstr("R"); slider1 == 1 && slider15 ? ( ui_col(C_ST); gfx_circle(lx + lw * 0.5, my, 4 * sc, 1, 1); gfx_setfont(4); gfx_x = lx + lw * 0.5 + 8 * sc; gfx_y = my - 12 * sc; gfx_drawstr("DRY"); ); xa = lx + (dispPA + 1) * 0.5 * lw; xb = lx + (dispPB + 1) * 0.5 * lw; gfx_setfont(4); slider1 != 0 || slider15 ? ( ui_col(C_VA); gfx_circle(xa, my - 13 * sc, (3 + 4 * min(dispGA, 1.5)) * sc, 1, 1); slider1 == 0 ? strcpy(#ta, "DRY") : sprintf(#ta, "%.1f ms", dispDA); ui_text(xa, my - 33 * sc, #ta); ); ui_col(C_VB); gfx_circle(xb, my + 13 * sc, (3 + 4 * min(dispGB, 1.5)) * sc, 1, 1); sprintf(#tb, "%.1f ms", dispDB); ui_text(xb, my + 23 * sc, #tb); // ---------- input VU ---------- vdb = 10 * log10(max(vu_ms, 0.0000000001)) + 18; // 0 VU = -18 dBFS RMS vuIn.ui_vu(478 * sc, 52 * sc, 146 * sc, 80 * sc, vdb, "INPUT"); // ---------- DOUBLER group ---------- ui_group(16 * sc, 140 * sc, 608 * sc, 96 * sc, 0, "DOUBLER"); knob_step = 1; ui_knob(80 * sc, 186 * sc, 2, 1, 40, 12, 0, "DELAY", "%.1f ms"); i = 0; loop(5, sprintf(#mb, "x%d", i + 2); ui_button((128 + i * 33) * sc, 148 * sc, 30 * sc, 22 * sc, #mb, slider3 == i, slider1 != 0) ? ( slider3 = i; slider_automate(slider3); ); i += 1; ); ui_button(128 * sc, 176 * sc, 162 * sc, 22 * sc, "SWAP SIDES", slider4 == 1, 1) ? ( slider4 = 1 - slider4; slider_automate(slider4); ); ui_button(128 * sc, 204 * sc, 79 * sc, 22 * sc, "MONO", slider12 == 0, 1) ? ( slider12 = 0; slider_automate(slider12); ); ui_button(211 * sc, 204 * sc, 79 * sc, 22 * sc, "STEREO", slider12 == 1, 1) ? ( slider12 = 1; slider_automate(slider12); ); ui_knob(338 * sc, 186 * sc, 5, -24, 6, 0, 0, "LEVEL", "%.1f dB"); ui_knob(414 * sc, 186 * sc, 6, 20, 1000, 150, 1, "LOW SPLIT", "%.0f Hz"); ui_knob(490 * sc, 186 * sc, 7, 0, 100, 100, 0, "WIDTH", "%.0f%%"); ui_button(538 * sc, 160 * sc, 80 * sc, 22 * sc, "DRY", slider15 == 1, 1) ? ( slider15 = 1 - slider15; slider_automate(slider15); ); ui_button(538 * sc, 188 * sc, 80 * sc, 22 * sc, "MONO CHK", slider14 == 1, 1) ? ( slider14 = 1 - slider14; slider_automate(slider14); ); // ---------- DRIFT (row 4, left) ---------- ui_group(16 * sc, 244 * sc, 92 * sc, 96 * sc, 1, "DRIFT"); ui_knob(62 * sc, 290 * sc, 26, 0, 100, 50, 0, "DRIFT", "%.0f%%"); // ---------- IN / OUT (row 4) ---------- ui_group(116 * sc, 244 * sc, 236 * sc, 96 * sc, 4, "IN / OUT"); ui_io(146, 262, 22, 23, 16, 21); // ---------- DETUNE group (row 4, middle) ---------- ui_group(360 * sc, 244 * sc, 136 * sc, 96 * sc, 3, "DETUNE"); ui_knob(412 * sc, 290 * sc, 24, -50, 50, 0, 0, "LEFT", "%+.1f ct"); ui_knob(466 * sc, 290 * sc, 25, -50, 50, 0, 0, "RIGHT", "%+.1f ct"); // ---------- MASTER group (row 4, right) ---------- ui_group(504 * sc, 244 * sc, 120 * sc, 96 * sc, 2, "MASTER"); ui_knob(552 * sc, 290 * sc, 13, -24, 12, 0, 0, "OUTPUT", "%.1f dB"); mL.ui_ledm(586 * sc, 256 * sc, 14 * sc, 70 * sc, pkL); mR.ui_ledm(604 * sc, 256 * sc, 14 * sc, 70 * sc, pkR); pkL *= 0.85; pkR *= 0.85; gfx_setfont(4); ui_col(C_INK); ui_text(603 * sc, 328 * sc, "OUT"); ui_swrow(14, 626, 45, 348, 392, 0, 0, 0, 0, 0); ui_end();