desc:GFX Chorus //tags: chorus bbd stereo modulation vintage //author: JGWizrad / Garden FX (GFX) // // Requires gfx-drift, gfx-lfo, gfx-input, gfx-output and gfx-ui (.jsfx-inc) in the same folder. // // Classic 80s poly-synth style bucket-brigade chorus: // two short modulated delay lines driven by one triangle LFO, the right side in antiphase, // so the image spreads wide. BBD tone filtering, optional hiss. // I - slow and gentle (0.513 Hz, +-1.85 ms) // II - faster and deeper (0.863 Hz, +-1.85 ms) // I+II - both at once: the I sweep (the knobs) plus a second tap swept 1.68x faster (0.863 Hz from I), // a little longer and swinging the other way - richer and wider, without the throb a single // fast sweep makes against the dry signal // OFF - chorus bypassed (input / output stages stay active) // The mode buttons set RATE / DEPTH / CENTRE to the classic values; the knobs can then be tweaked. // IN / OUT: TRIM (with the GUARD key), SAT and the OUT stage (OFF / CLIP / LIM) sit on the panel. // DRIFT: rate, depth and tone wander slowly and randomly. The DRIFT dial scales the three drift amounts // (50% = the drift settings, 100% = twice, 0 = none) and speeds the wander up with them. // LOW CUT (MASTER) - a high-pass on the effected signal only (20 = off): the bass stays dry, clean and centred. import gfx-drift.jsfx-inc import gfx-lfo.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:0<0,3,1{I,II,I+II,Off}>-Mode slider2:0<0,2,1{Free,Sync,Flow}>-Rate mode slider3:0.513<0.05,12,0.001>-Free rate (Hz) slider4:3<0,15,1{4 bars,2 bars,1 bar,1/2,1/2 D,1/2 T,1/4,1/4 D,1/4 T,1/8,1/8 D,1/8 T,1/16,1/16 D,1/16 T,1/32}>-Sync rate slider5:1.85<0,3,0.01>-Depth (ms) slider6:3.5<1,8,0.01>-Centre (ms) slider7:100<0,100,1>-Width (%) slider8:60<0,100,1>-BBD tone (%) slider9:0<0,100,1>-Hiss (%) slider10:50<0,100,1>-Mix (%) slider11:5<0,50,1>-Rate drift (%) slider12:10<0,50,1>-Depth drift (%) slider13:10<0,50,1>-Tone drift (%) slider14:0.25<0.02,2,0.01>-Drift speed (Hz) slider15:0<-24,12,0.1>-Output (dB) 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:20<20,500,1>-Wet low cut (Hz, 20 = off) slider25: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-chorus"; // the ? pop-up links to this manual // fractional delay read, 4-point Hermite function dread(buf, d) local(rp, i, f, xm1, x0, x1, x2) ( rp = wp - d; rp < 0 ? rp += bsz; i = floor(rp); f = rp - i; xm1 = buf[(i + bsz - 1) & bmask]; x0 = buf[i & bmask]; x1 = buf[(i + 1) & bmask]; x2 = buf[(i + 2) & bmask]; ((((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); ); // one BBD line: pre-filter, delay, gentle companding saturation, 2-pole post-filter // d2 >= 0: a second tap, averaged with the first (I+II) function bbd(x, buf, d, d2, c, hs) instance(pre, p1, p2) local(y) ( pre += (x + hs - pre) * c; buf[wp] = pre; y = dread(buf, d); d2 >= 0 ? y = 0.5 * (y + dread(buf, d2)); y = ost_tanh(y * 1.15) / 1.15; p1 += (y - p1) * c; p2 += (p1 - p2) * c; p2; ); function chorus_mode(m) ( slider1 = m; m == 0 ? ( slider2 = 0; slider3 = 0.513; slider5 = 1.85; slider6 = 3.5; ) : m == 1 ? ( slider2 = 0; slider3 = 0.863; slider5 = 1.85; slider6 = 3.5; ) : m == 2 ? ( slider2 = 0; slider3 = 0.513; slider5 = 1.85; slider6 = 3.5; ); slider_automate(55); // sliders 1, 2, 3, 5, 6 ); // LOW CUT: a 12 dB/oct high-pass on the wet signal only function wlc(x) instance(x1, x2, y1, y2) local(y) ( y = wl_b0 * (x - 2 * x1 + x2) - wl_a1 * y1 - wl_a2 * y2; x2 = x1; x1 = x; y2 = y1; y1 = y; y; ); function wlc_set(f) local(w0, al, n) ( w0 = 2 * $pi * min(f, srate * 0.45) / srate; al = sin(w0) / (2 * 0.7071); n = 1 + al; wl_b0 = (1 + cos(w0)) * 0.5 / n; wl_a1 = -2 * cos(w0) / n; wl_a2 = (1 - al) / n; ); function update() ( wl_on = slider24 > 20.5; wl_on ? wlc_set(slider24); mode = slider1; ratemode = slider2; frate = slider3; div = floor(slider4 + 0.5); dep_t = slider5; cen_t = slider6; wid_t = slider7 / 100; tone = slider8 / 100; hiss = (slider9 / 100) ^ 2 * 0.004; mix_t = slider10 / 100; drift_macro(slider25); rd = slider11 / 100 * dm_a; dd = min(slider12 / 100 * dm_a, 1); td = slider13 / 100 * dm_a; dspd = slider14 * dm_s; out_t = 10 ^ (slider15 / 20); lfoA.lfo_setup(1, 0); lfoB.lfo_setup(1, 0); in_setup(slider22, slider23); ost_setup(slider16 / 100, slider17 / 100, slider18, slider19, slider20, slider21); ); // ---------- presets (leave Drift speed, Output and the I/O rows alone) ---------- function setp(m, rm, fr, dv, dp, ce, wd, tn, hs, mx, r_, d_, t_) ( slider1 = m; slider2 = rm; slider3 = fr; slider4 = dv; slider5 = dp; slider6 = ce; slider7 = wd; slider8 = tn; slider9 = hs; slider10 = mx; slider11 = r_; slider12 = d_; slider13 = t_; ); function load_preset(p) ( p == 1 ? setp(0, 0, 0.513, 3, 1.85, 3.5, 100, 60, 0, 50, 5, 10, 10) : // Classic I p == 2 ? setp(1, 0, 0.863, 3, 1.85, 3.5, 100, 60, 0, 50, 5, 10, 10) : // Classic II p == 3 ? setp(2, 0, 0.513, 3, 1.85, 3.5, 100, 60, 0, 50, 5, 10, 10) : // Classic I+II p == 4 ? setp(1, 0, 0.6, 3, 2.5, 5.0, 100, 55, 0, 55, 10, 15, 10) : // Lush Wide p == 5 ? setp(0, 0, 0.3, 3, 0.8, 4.0, 70, 65, 0, 30, 5, 5, 5) : // Subtle Thicken p == 6 ? setp(1, 0, 0.863, 3, 1.85, 3.5, 100, 45, 35, 50, 8, 10, 15) : // Vintage Hiss p == 7 ? setp(1, 1, 0.863, 3, 2.0, 4.0, 100, 60, 0, 50, 0, 10, 10) : // Synced Swirl (1/2) p == 8 ? setp(0, 0, 0.5, 3, 1.85, 3.5, 100, 20, 10, 50, 5, 10, 20); // Dark Bucket p >= 1 && p <= 8 ? slider_automate(8191); // sliders 1-13 p == 9 ? ( // Reset all to defaults slider1 = 0; slider2 = 0; slider3 = 0.513; slider4 = 3; slider5 = 1.85; slider6 = 3.5; slider7 = 100; slider8 = 60; slider9 = 0; slider10 = 50; slider11 = 5; slider12 = 10; slider13 = 10; slider14 = 0.25; slider15 = 0; slider16 = 0; slider17 = 30; slider18 = 120; slider19 = 6000; slider20 = -0.3; slider21 = 1; slider22 = 0; slider23 = 1; slider24 = 20; slider25 = 50; slider_automate(33554431); ); ); // ---------- memory ---------- bsz = 8192; bmask = bsz - 1; bufL = 0; bufR = bsz; memset(0, 0, bsz * 2); wp = 0; // ---------- state ---------- sm = 1 - exp(-1 / (0.03 * srate)); vuc = 1 - exp(-1 / (0.01 * srate)); msr = srate / 1000; lfoA.lfo_init(); lfoB.lfo_init(); drR.drift_init(0.8 + rand(0.5)); drD.drift_init(0.8 + rand(0.5)); drT.drift_init(0.8 + rand(0.5)); ost_init(20000); ostL.ost_ch_init(); ostR.ost_ch_init(); in_init(); pkL = 0; pkR = 0; vu_ms = 0; ui_ovt = 1; ui_ovbg = 0x4C545E; ui_ovink = 0xE4EBF2; // Ice on a darker brushed panel !ui_dflt ? ui_thm = 1; !ui_dflt ? ui_hw = 1; // defaults for new instances: Ice, hardware (saved projects keep their own) update(); dep_s = dep_t; cen_s = cen_t; wid_s = wid_t; mix_s = mix_t; out_s = out_t; 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(); lfoA.lfo_block(ratemode, div, frate); // I+II: the second sweep runs at II's speed relative to I (0.863 / 0.513). In SYNC it runs on freely at the // scaled tempo rate (as in FLOW), since its phase can't lock to the bar at a non-musical ratio. ratemode ? ( lfoB.lfo_block(2, div, 0); lfoB.inc *= 1.682; ) : lfoB.lfo_block(0, div, frate * 1.682); @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; // drift dinc = dspd / srate; vR = drR.drift_tick(dinc); vD = drD.drift_tick(dinc); vT = drT.drift_tick(dinc); // smoothing dep_s += (dep_t - dep_s) * sm; cen_s += (cen_t - cen_s) * sm; wid_s += (wid_t - wid_s) * sm; mix_s += (mix_t - mix_s) * sm; out_s += (out_t - out_s) * sm; mode == 3 ? ( oL = inL; oR = inR; ) : ( lv = lfoA.lfo_tick(0, rd, vR); de = dep_s * (1 - dd * (0.5 + 0.5 * vD)); dmsL = max(cen_s + de * lv, 0.3); dmsR = max(cen_s + de * lv * (1 - 2 * wid_s), 0.3); fc = 3000 * (16000 / 3000) ^ min(max(tone + td * 0.25 * vT, 0), 1); cf = 1 - exp(-2 * $pi * min(fc, srate * 0.45) / srate); mode == 2 ? ( lv2 = lfoB.lfo_tick(0, rd, vR); d2L = max(cen_s + 0.8 - de * lv2, 0.3) * msr; d2R = max(cen_s + 0.8 - de * lv2 * (1 - 2 * wid_s), 0.3) * msr; ) : ( d2L = d2R = -1; ); wL = bbdL.bbd(inL, bufL, dmsL * msr, d2L, cf, hiss * (rand(2) - 1)); wR = bbdR.bbd(inR, bufR, dmsR * msr, d2R, cf, hiss * (rand(2) - 1)); wp = (wp + 1) & bmask; oL = wl_on ? inL + mix_s * (WLL.wlc(wL) - WDL.wlc(inL)) : (1 - mix_s) * inL + mix_s * wL; // LOW CUT: the lows stay dry at full level oR = wl_on ? inR + mix_s * (WLR.wlc(wR) - WDR.wlc(inR)) : (1 - mix_s) * inR + mix_s * wR; // LOW CUT: the lows stay dry at full level ); // output stage: band saturation -> output gain -> clipper / limiter oL = ostL.ost_band(oL) * out_s; oR = ostR.ost_band(oR) * out_s; ost_out(oL, oR); spl0 = ost_l; spl1 = ost_r; pkL = max(pkL, abs(spl0)); pkR = max(pkR, abs(spl1)); dispL = dmsL; dispR = dmsR; dispP = lfoA.p; @gfx 900 340 ui_begin(900, 340); // ---------- an 80s half-rack stereo chorus: teal enamel face, cream print, white keys with red LEDs ---------- FACE = 0x2C7C83; INK = 0xF1EBDD; DIM = 0xB9D6D4; RED = 0xFF5A3C; KEYC = 0xF4F1E8; CAPC = 0xF1EBDD; dy_ink = INK; dy_bg = FACE; function setv(idx, v) ( slider(idx) = v; ui_auto(idx); ); ui_col(0x0E1112); gfx_rect(0, 0, gfx_w, gfx_h); rd_plate(6, 4, 888, 332, FACE, 12); rd_screw(22, 20); rd_screw(878, 20); rd_screw(22, 320); rd_screw(878, 320); // ---------- header ---------- rd_badge(56, 36, 15, INK, FACE); rd_titlei(80, 7, "Chorus", 32, INK); rd_sub(82, 47, "BUCKET-BRIGADE STEREO CHORUS", DIM, 1); ui_cola(INK, 0.5); gfx_rect(40 * sc, 62 * sc, 820 * sc, 1 * sc); rd_sqdark = 1; rd_sq(450, 18, 50, 26, slider1 == 0, KEYC, "I") ? chorus_mode(0); rd_sq(504, 18, 50, 26, slider1 == 1, KEYC, "II") ? chorus_mode(1); rd_sq(558, 18, 50, 26, slider1 == 2, KEYC, "I+II") ? chorus_mode(2); rd_sq(612, 18, 50, 26, slider1 == 3, 0x8A9A9C, "OFF") ? setv(1, 3); rd_sqdark = 0; rd_led(475, 12, 2.5, slider1 == 0 || slider1 == 2, RED); rd_led(529, 12, 2.5, slider1 == 1 || slider1 == 2, RED); rd_led(583, 12, 2.5, slider1 == 2, RED); pk_pextra = "Reset all to defaults"; // the house preset display: PRESETS, prev / next, LCD (click it for the list) k_ = pk_preset(670 * sc, 16 * sc, 216 * sc, 28 * sc, pr_cur, "Classic I|Classic II|Classic I+II|Lush Wide|Subtle Thicken|Vintage Hiss|Synced Swirl|Dark Bucket"); k_ > 0 ? ( load_preset(k_); pr_cur = k_ > 8 ? 0 : k_; ); ui_size_picker(300 * sc, 24 * sc); // ---------- the window: two delay taps sweeping, and the LFO ---------- ui_col(0x0A0F10); rd_rr(40 * sc, 74 * sc, 300 * sc, 100 * sc, 6 * sc); ui_col(0x0F2A2C); gfx_rect(44 * sc, 78 * sc, 292 * sc, 92 * sc); LC = 0x7FF0E0; tx0 = 58; tw0 = 180; ty = 132; ui_cola(LC, 0.35); gfx_rect(tx0 * sc, ty * sc, tw0 * sc, 1 * sc); i = 0; loop(11, ui_cola(LC, 0.35); gfx_rect((tx0 + i / 10 * tw0) * sc, (ty - (i % 5 == 0 ? 5 : 2)) * sc, 1 * sc, (i % 5 == 0 ? 10 : 4) * sc); i += 1; ); gfx_setfont(4); ui_cola(LC, 0.7); gfx_x = (tx0 - 2) * sc; gfx_y = (ty + 9) * sc; gfx_drawstr("0"); gfx_x = (tx0 + tw0 * 0.5 - 3) * sc; gfx_drawstr("5"); gfx_x = (tx0 + tw0 - 12) * sc; gfx_drawstr("10 ms"); slider1 == 3 ? ( gfx_setfont(2); ui_col(LC); ui_text((tx0 + tw0 * 0.5) * sc, (ty - 26) * sc, "CHORUS OFF"); ) : ( ui_col(LC); gfx_circle((tx0 + min(dispL / 10, 1) * tw0) * sc, (ty - 10) * sc, 4 * sc, 1, 1); ui_col(0xFF9A7A); gfx_circle((tx0 + min(dispR / 10, 1) * tw0) * sc, (ty + 10) * sc, 4 * sc, 1, 1); gfx_setfont(4); ui_col(LC); gfx_x = 52 * sc; gfx_y = 84 * sc; sprintf(#t1, "L %.2f ms", dispL); gfx_drawstr(#t1); ui_col(0xFF9A7A); gfx_x = 130 * sc; sprintf(#t2, "R %.2f ms", dispR); gfx_drawstr(#t2); ); sx0 = 256; sw0 = 70; sy0 = 124; sa = 26; ui_cola(LC, 0.3); gfx_rect(sx0 * sc, sy0 * sc, sw0 * sc, 1 * sc); slider1 == 3 ? ui_cola(LC, 0.3) : ui_col(LC); i = 0; loop(41, pp = i / 40; qx = (sx0 + pp * sw0) * sc; qy = (sy0 - lfo_shape_d(pp, 1, 1, 1) * sa) * sc; i > 0 ? gfx_line(px, py, qx, qy, 1); px = qx; py = qy; i += 1; ); slider1 != 3 ? ( ui_col(0xFF9A7A); gfx_circle((sx0 + dispP * sw0) * sc, (sy0 - lfo_shape_d(dispP, 1, 1, 1) * sa) * sc, 3 * sc, 1, 1); ); gfx_setfont(4); ui_cola(LC, 0.7); gfx_x = sx0 * sc; gfx_y = 84 * sc; gfx_drawstr("LFO"); // rate mode under the window rd_sq(40, 186, 60, 22, slider2 == 0, 0xFFC040, "FREE") ? setv(2, 0); rd_sq(106, 186, 60, 22, slider2 == 1, 0xFFC040, "SYNC") ? setv(2, 1); rd_sq(172, 186, 60, 22, slider2 == 2, 0xFFC040, "FLOW") ? setv(2, 2); rd_lbl(276, 191, "RATE MODE", DIM, 1); // ---------- the chorus knobs ---------- dy_kst = 0; slider2 ? ( dy_step = 16; dy_steps = ""; dy_knob(410, 126, 28, 4, 0, 15, 3, 0, CAPC, "RATE", lfo_div_name(floor(slider4 + 0.5))); ) : ( sprintf(#v, "%.2f Hz", slider3); dy_knob(410, 126, 28, 3, 0.05, 12, 0.513, 1, CAPC, "RATE", #v); ); sprintf(#v, "%.2f ms", slider5); dy_knob(510, 120, 21, 5, 0, 3, 1.85, 0, CAPC, "DEPTH", #v); sprintf(#v, "%.2f ms", slider6); dy_knob(590, 120, 21, 6, 1, 8, 3.5, 0, CAPC, "CENTRE", #v); sprintf(#v, "%.0f%%", slider7); dy_knob(670, 120, 21, 7, 0, 100, 100, 0, CAPC, "WIDTH", #v); sprintf(#v, "%.0f%%", slider8); dy_knob(750, 120, 21, 8, 0, 100, 60, 0, 0xFFC040, "TONE", #v); sprintf(#v, "%.0f%%", slider9); dy_knob(830, 120, 21, 9, 0, 100, 0, 0, 0xFFC040, "HISS", #v); // ---------- DRIFT | IN / OUT | MASTER ---------- ui_cola(INK, 0.35); gfx_rect(40 * sc, 222 * sc, 820 * sc, 1 * sc); gfx_rect(128 * sc, 230 * sc, 1 * sc, 92 * sc); gfx_rect(400 * sc, 230 * sc, 1 * sc, 92 * sc); rd_lbl(84, 230, "DRIFT", DIM, 1); rd_lbl(264, 230, "IN / OUT", DIM, 1); rd_lbl(630, 230, "MASTER", DIM, 1); dy_nov = 1; sprintf(#v, "%.0f%%", slider25); dy_knob(84, 272, 16, 25, 0, 100, 50, 0, CAPC, "DRIFT", #v); rd_ioc1 = 0x1C4F55; rd_ioc2 = 0xFFC040; rd_ioc3 = RED; rd_iok = 20; rd_iotx = 12; rd_io(166, 250, 22, 23, 16, 21); rd_iotx = 0; dy_nov = 0; sprintf(#v, "%.0f%%", slider10); dy_knob(450, 272, 18, 10, 0, 100, 50, 0, CAPC, "MIX", #v); slider24 <= 20.5 ? strcpy(#v, "off") : sprintf(#v, "%.0f Hz", slider24); dy_kf = 1; dy_knob(524, 272, 15, 24, 20, 500, 20, 1, 0, "LOW CUT", #v); sprintf(#v, "%+.1f dB", slider15); dy_knob(598, 272, 18, 15, -24, 12, 0, 0, CAPC, "OUTPUT", #v); mL.ui_ledm(652 * sc, 258 * sc, 196 * sc, 8 * sc, pkL); mR.ui_ledm(652 * sc, 270 * sc, 196 * sc, 8 * sc, pkR); pkL *= 0.85; pkR *= 0.85; rd_lbl(750, 286, "OUTPUT LEVEL", DIM, 0.8); vdb = 10 * log10(max(vu_ms, 0.0000000001)) + 18; pk_end(); ui_end();