desc:GLU34 [stereo bus compressor] (GFX Lab)
//tags: bus compressor glue vca stereo mix bus drive sidechain hpf external key auto release parallel
//author: JGWizrad / Garden FX (GFX)
//
// Requires gfx-lab-input, gfx-lab-output, gfx-lab-drift, gfx-lab-ui, gfx-lab-panel, gfx-lab-dyn, gfx-lab-clip and gfx-lab-os
// (.jsfx-inc) in the same folder.
//
// GLU34. A stereo bus compressor in the spirit of the classic British console bus compressor (the sound is designed
// from scratch): the glue for a mix bus, a drum bus or a parallel chain. Linked stereo: one detector, the louder side
// wins. The Lab GLU34 adds Sifon's COLOUR after the compressor, an OS key and a blue backlit GR meter.
//
// SIGNAL FLOW
//   input -> COMPRESSOR (detector through the SC HPF, or the key) -> [oversampled: VCA DRIVE -> MAKEUP -> MIX -> IN
//   -> COLOUR] -> COLOUR tone and DIP dips -> out.
//
//   THRESHOLD  -30 .. 0 dBFS. RATIO (stepped) 1.5, 2, 3, 4, 10, 20 : 1 - each with its own knee: 2:1 eases in over a
//              very wide, gentle knee (12 dB) for transparent bus work; the higher ratios bite harder (down to 1 dB).
//   ATTACK     (stepped) 0.1, 0.3, 1, 3, 10, 30 ms.   RELEASE (stepped) 0.1, 0.3, 0.6, 1.2, 2.4 s and AUTO: a two-stage,
//              programme-dependent release - loud peaks let go quickly, the body of the sound recovers slowly.
//   MAKEUP     -5 .. +15 dB.   MIX 0 - 100 % (parallel compression, the dry side in phase).
//   VCA DRIVE  0 - 10. At 0 the VCA runs balanced: clean and punchy. Above 0 it runs unbalanced: a console's 2nd-harmonic
//              richness that grows as it is turned up and as the compressor works harder, to a dense, aggressive
//              colour (up to about 10 % THD on hot material) at 10.
//   SC HPF     (stepped) OFF, 40, 80, 120, 185, 250 Hz, 12 dB/oct, in the detector: the lows stop pumping the mix.
//   EXT SC     the detector listens to inputs 3 / 4 (a genuine stereo key: print stems against the full mix).
//   IN         the compressor in / out (10 ms fade; the VCA's colour and makeup go with it; COLOUR stays).
//   COLOUR LOW / HIGH (either, both or neither; Sifon's COLOUR, without its IRON): a parallel HEAD saturation (the
//              tape curve of HeadSmoother) on each band, driven harder by its own TEXTURE dial (0 - +18 dB), only its
//              harmonics added. HIGH: the band above ~3 kHz (widening down to ~1 kHz as its TEXTURE rises), mostly 2nd
//              harmonic (sheen, air, upper-mid edge). LOW: the band below ~300 Hz (weight). Each band also shelves the
//              whole mix: HIGH a first-order shelf from 4.7 kHz (up to +3.1 dB) and, above 50 % TEXTURE, a shelf above
//              15 kHz (to +3 dB); LOW a lift below 300 Hz (up to +1.25 dB at 50 %) and, above 50 %, a shelf below
//              120 Hz (peak about +3 dB) with a 30 Hz subsonic filter fading in.
//              DIP 220 (with LOW on) / DIP 3K (with HIGH on): a fixed dip on that band's saturation and a dynamic dip
//              on the clean signal that cuts only while the region stands out from the whole mix (up to 6 dB).
//              LOW WIDTH / HIGH WIDTH (0 - 200 %, 100 = as made): where each band's added harmonics sit - 0 = centre,
//              200 = sides only. SOLO: hear only the band(s) being worked on. Every switch crossfades (30 ms).
//   OS         1x / 2x / 4x (default 1x = the Stable GLU34's sound): VCA DRIVE and COLOUR run at that rate (one
//              oversampled section). Changing it dips the sound for 5 ms and swaps at the silent point.
//   The house output stage passes straight through (a mix tool: no clip).
//   DRIFT      (0 - 100 %, 50 % = standard, 0 = none): the VCA's bias (its asymmetry breathes) and the control voltage
//              (the threshold wanders by a few tenths of a dB), and each COLOUR band's drive breathes (about +/-1 dB of
//              TEXTURE, its own slow wander per band: the sheen and the weight move a little, as circuits do).
//   VFD / MATRIX  the panel's look (VFD charcoal and blue; MATRIX battleship grey and cream). The look only.
//   PRESETS    12 (Init + 11) plus Reset all to defaults. A preset sets every control but EXT SC, DRIFT, OS and the look.
// GUI: the GR needle meter's face is backlit in blue (the house steady glow).
// Latency: 0 / 31 / 37 samples at 1x / 2x / 4x (reported).

slider1:1<0,1,1{Out,In}>-In
slider2:-12<-30,0,0.1>-Threshold (dBFS)
slider3:1<0,5,1{1.5:1,2:1,3:1,4:1,10:1,20:1}>-Ratio
slider4:4<0,5,1{0.1 ms,0.3 ms,1 ms,3 ms,10 ms,30 ms}>-Attack
slider5:5<0,5,1{0.1 s,0.3 s,0.6 s,1.2 s,2.4 s,Auto}>-Release
slider6:0<0,5,1{Off,40 Hz,80 Hz,120 Hz,185 Hz,250 Hz}>-SC HPF
slider7:0<-5,15,0.1>-Makeup (dB)
slider8:0<0,10,0.1>-VCA drive
slider9:100<0,100,1>-Mix (%)
slider10:0<0,1,1{Off,On}>-External sidechain (in 3/4)
slider11:50<0,100,1>-Drift (%)
slider12:0<0,1,1{VFD,Matrix}>-Look
slider13:0<0,3,1{Off,High,Low,Both}>-Colour
slider14:30<0,100,1>-Low texture (%)
slider15:30<0,100,1>-High texture (%)
slider16:0<0,1,1{Off,On}>-Dip 220 Hz
slider17:0<0,1,1{Off,On}>-Dip 3 kHz
slider18:100<0,200,1>-Low colour width (%)
slider19:100<0,200,1>-High colour width (%)
slider20:0<0,1,1{Off,On}>-Colour solo
slider21:0<0,2,1{1x,2x,4x}>-Oversampling
slider22:prs_sel=0<0,12,1{No preset,Init,Mix Bus Glue,Gentle Master,Drum Bus Smash,Punchy Drums,Parallel Crush,Vocal Bus,Piano and Room,Bass Lock,Console Grit,Pumping Groove,Clean Modern}>-Preset

in_pin:left input
in_pin:right input
in_pin:key left
in_pin:key right
out_pin:left output
out_pin:right output

import gfx-lab-input.jsfx-inc
import gfx-lab-output.jsfx-inc
import gfx-lab-drift.jsfx-inc
import gfx-lab-ui.jsfx-inc
import gfx-lab-panel.jsfx-inc
import gfx-lab-dyn.jsfx-inc
import gfx-lab-clip.jsfx-inc
import gfx-lab-os.jsfx-inc

@init
#ui_man = "gfx-lab-glu34";                               // the ? pop-up links to this manual
ext_noinit = 1;
clp_aa = 1; clp_pf_on = 0; clp_kt_on = 0;             // gfx-clip: ADAA clippers only (the VCA's colour)

function gl_tn(x) ( abs(x) < 0.000000000001 ? 0 : x; );
function gl_tc(ms) ( 1 - exp(-1 / (max(ms, 0.005) * 0.001 * srate)); );
function gl_sm(g) ( g * g * (3 - 2 * g); );
function gl_ratio(i) ( i == 0 ? 1.5 : i == 1 ? 2 : i == 2 ? 3 : i == 3 ? 4 : i == 4 ? 10 : 20; );
function gl_knee(i) ( i == 0 ? 12 : i == 1 ? 12 : i == 2 ? 6 : i == 3 ? 4 : i == 4 ? 2 : 1; );
function gl_att(i) ( i == 0 ? 0.1 : i == 1 ? 0.3 : i == 2 ? 1 : i == 3 ? 3 : i == 4 ? 10 : 30; );
function gl_rel(i) ( i == 0 ? 100 : i == 1 ? 300 : i == 2 ? 600 : i == 3 ? 1200 : 2400; );
function gl_hpf(i) ( i == 1 ? 40 : i == 2 ? 80 : i == 3 ? 120 : i == 4 ? 185 : 250; );

// the sidechain high-pass (12 dB/oct): SCC[0..4]
SCC = 100;
function gl_schp(f) local(w, cw, al, n) (
  w = 2 * $pi * min(f, 0.45 * srate) / srate; cw = cos(w); al = sin(w) / (2 * 0.70710678); n = 1 + al;
  SCC[0] = (1 + cw) * 0.5 / n; SCC[1] = -(1 + cw) / n; SCC[2] = SCC[0]; SCC[3] = -2 * cw / n; SCC[4] = (1 - al) / n;
);
function gl_bq(x) instance(s1, s2) local(y) ( y = SCC[0] * x + s1; s1 = SCC[1] * x - SCC[3] * y + s2; s2 = SCC[2] * x - SCC[4] * y; y; );


// ---------- COLOUR (Sifon's LOW / HIGH, without its IRON) ----------
function db2g(d) ( exp(d * 0.11512925464970228); );
function coef(t) ( 1 - exp(-1 / (max(t, 0.00001) * srate)); );
function op_lp(x, gg) instance(s) local(v, y) ( v = (x - s) * gg; y = v + s; s = y + v; y; );
// RBJ biquads at the rate cb_fs (the COLOUR tone): 0 high-pass, 1 peak, 2 low shelf, 3 high shelf, 4 band-pass
function cb_set(t, f, q, gdb) instance(b0, b1, b2, a1, a2) local(w, cw, al, a, sa, n) (
  w = 2 * $pi * min(f, 0.45 * cb_fs) / cb_fs; cw = cos(w); al = sin(w) / (2 * q); a = 10 ^ (gdb / 40);
  t == 0 ? ( n = 1 + al; b0 = (1 + cw) / 2 / n; b1 = -(1 + cw) / n; b2 = b0; a1 = -2 * cw / n; a2 = (1 - al) / n; ) :
  t == 1 ? ( n = 1 + al / a; b0 = (1 + al * a) / n; b1 = -2 * cw / n; b2 = (1 - al * a) / n; a1 = b1; a2 = (1 - al / a) / n; ) :
  t == 2 ? ( sa = 2 * sqrt(a) * al; n = (a + 1) + (a - 1) * cw + sa;
    b0 = a * ((a + 1) - (a - 1) * cw + sa) / n; b1 = 2 * a * ((a - 1) - (a + 1) * cw) / n; b2 = a * ((a + 1) - (a - 1) * cw - sa) / n;
    a1 = -2 * ((a - 1) + (a + 1) * cw) / n; a2 = ((a + 1) + (a - 1) * cw - sa) / n; ) :
  t == 4 ? ( n = 1 + al; b0 = al / n; b1 = 0; b2 = -b0; a1 = -2 * cw / n; a2 = (1 - al) / n; ) :
  ( sa = 2 * sqrt(a) * al; n = (a + 1) - (a - 1) * cw + sa;
    b0 = a * ((a + 1) + (a - 1) * cw + sa) / n; b1 = -2 * a * ((a - 1) + (a + 1) * cw) / n; b2 = a * ((a + 1) + (a - 1) * cw - sa) / n;
    a1 = 2 * ((a - 1) - (a + 1) * cw) / n; a2 = ((a + 1) - (a - 1) * cw - sa) / n; );
);
function cb_run(x) instance(b0, b1, b2, a1, a2, z1, z2) local(y) ( y = b0 * x + z1; z1 = b1 * x - a1 * y + z2; z2 = b2 * x - a2 * y; y; );
// harmonics only: takes out of a saturated band (h) the part that still follows the band itself (x), measured over
// 30 ms, so COLOUR adds harmonics without turning the band down
function hfund(h, x) instance(cxy, cxx) (
  cxy += (h * x - cxy) * cg_cr; cxx += (x * x - cxx) * cg_cr;
  h - x * (cxx > 0.000000000001 ? cxy / cxx : 1);
);
// the parallel saturation (the HEAD curve of HeadSmoother): tanh with a small bias, its gain set so a band peaking at
// -12 dBFS comes out at the same level whatever the drive
function th(v) local(e) ( e = exp(2 * max(min(v, 20), -20)); (e - 1) / (e + 1); );
function hsat(x, g, b, tb, c) ( (th(g * x + b) - tb) * c; );
function hs_coefs() (
  // each band's TEXTURE drives it from 0 to +18 dB; DRIFT breathes each drive by about +/-1 dB
  g_hl = db2g(18 * tex_s + co_dl); g_hh = db2g(18 * txh_s + co_dh); co_dlq = co_dl; co_dhq = co_dh;
  tb_hi = th(0.3); c_hi = 0.25 / (th(g_hh * 0.125 + 0.3) - th(0.3 - g_hh * 0.125));
  tb_lo = th(0.4); c_lo = 0.25 / (th(g_hl * 0.125 + 0.4) - th(0.4 - g_hl * 0.125));
  // the tone moves above 50 % TEXTURE. On the whole mix (1x): HIGH a shelf above 15 kHz, LOW a shelf below 120 Hz (the
  // subsonic filter at 30 Hz fades in with t2). On the parallel saturation only (oversampled): HIGH's harmonics dip
  // broadly at 3 kHz, LOW's narrowly at 220 Hz (the DIP keys)
  t2 = max(0, 2 * tex_s - 1); t2h = max(0, 2 * txh_s - 1); tex_q = tex_s; txh_q = txh_s;
  cb_fs = srate;
  eqL.h2.cb_set(3, 15000, 0.707, 3 * t2h); eqL.l1.cb_set(2, 120, 0.707, 2.3 * t2);
  eqR.h2.cb_set(3, 15000, 0.707, 3 * t2h); eqR.l1.cb_set(2, 120, 0.707, 2.3 * t2);
  gd3_q = -6 * d3_s; gd22_q = -9 * d22_s;
  cb_fs = srate * os_f;
  chL.hd.cb_set(1, 3000, 0.6, gd3_q); chL.ld.cb_set(1, 220, 2.5, gd22_q);
  chR.hd.cb_set(1, 3000, 0.6, gd3_q); chR.ld.cb_set(1, 220, 2.5, gd22_q);
);
// the DIP keys' dynamic EQ on the clean signal: a dip at 220 Hz (narrow) and at 3 kHz (broad) that cuts only while
// that region stands out from the whole mix, half the excess, up to 6 dB (detected on the mid, linked)
function dyn_init() (
  cb_fs = srate; dd_a.cb_set(4, 220, 2.5, 0); dd_b.cb_set(4, 3000, 0.6, 0);
  c_dpw = coef(0.02); c_dat = coef(0.005); c_drl = coef(0.12);
);
function dyn_run(m) local(p22, p3) (
  p22 = dd_a.cb_run(m); p3 = dd_b.cb_run(m);
  dp_f += (m * m - dp_f) * c_dpw; dp_a += (p22 * p22 - dp_a) * c_dpw; dp_b += (p3 * p3 - dp_b) * c_dpw;
  (dd_n += 1) >= 16 ? (
    dd_n = 0;
    dp_f > 0.0000000001 ? (
      dx_a = min(6, max(0, 0.5 * (10 * log10(dp_a / dp_f + 0.000000000001) + 10)));
      dx_b = min(6, max(0, 0.5 * (10 * log10(dp_b / dp_f + 0.000000000001) + 9)));
    ) : ( dx_a = dx_b = 0; );
    dx_a *= d22_s * w_blue; dx_b *= d3_s * w_red;
    dg_a += (dx_a - dg_a) * (dx_a > dg_a ? 16 * c_dat : 16 * c_drl);
    dg_b += (dx_b - dg_b) * (dx_b > dg_b ? 16 * c_dat : 16 * c_drl);
    cb_fs = srate;
    dqL.a.cb_set(1, 220, 2.5, -dg_a); dqR.a.cb_set(1, 220, 2.5, -dg_a);
    dqL.b.cb_set(1, 3000, 0.6, -dg_b); dqR.b.cb_set(1, 3000, 0.6, -dg_b);
  );
);
function hs_tone(x) instance(h2, l1, l3) local(y) (
  y = x + w_red * (this.h2.cb_run(x) - x);
  y += t2 * (this.l3.cb_run(y) - y) * w_blue;
  y + w_blue * (this.l1.cb_run(y) - y);
);
// COLOUR on one oversampled sample (per-channel filter states in the instance)
function colour_run(u) instance(rp, bp, ep, hc, lc, sp, hd, ld, dlo, dhi) local(hi, bl, dr, dd, y, band, ev) (
  hi = u - this.rp.op_lp(u, cg_red);                       // HIGH band, above 3 kHz - 1 kHz (TEXTURE)
  bl = this.bp.op_lp(u, cg_blue);                          // LOW band, below ~300 Hz
  dd = this.ld.cb_run(this.lc.hfund(hsat(bl, g_hl, 0.4, tb_lo, c_lo), bl));
  ev = this.hc.hfund(hsat(hi, g_hh, 0.3, tb_hi, c_hi), hi);
  dr = this.hd.cb_run(ev - this.ep.op_lp(ev, cg_red));
  // the LOW / HIGH harmonics go to co_add() (their own widths)
  dlo = w_blue * dd; dhi = w_red * dr;
  // + each band's lift: HIGH a first-order shelf from 4.7 kHz (+3.1 dB at 100 %), LOW a shelf below 300 Hz
  y = u + w_red * sh_hi * (u - this.sp.op_lp(u, cg_sh)) + w_blue * sh_lo * bl;
  // SOLO: only the band(s) being worked on
  band = max(1 - w_red - w_blue, 0) * u + w_red * hi + w_blue * bl;
  y + so_s * (band - y);
);
// the added harmonics in M / S (0 % = centre only, 100 % = as made, 200 % = sides only), DC-blocked (5 Hz):
// the 2nd harmonic's offset
function co_add() local(m, d) (
  m = 0.5 * (chL.dlo + chR.dlo); d = 0.5 * (chL.dlo - chR.dlo);
  cwa = m * lwm_g + d * lws_g; cwb = m * lwm_g - d * lws_g;
  m = 0.5 * (chL.dhi + chR.dhi); d = 0.5 * (chL.dhi - chR.dhi);
  cwa += m * hwm_g + d * hws_g; cwb += m * hwm_g - d * hws_g;
  m = cwa - co_xa + co_dc * co_ya; co_xa = cwa; co_ya = m; cwa = m;
  m = cwb - co_xb + co_dc * co_yb; co_xb = cwb; co_yb = m; cwb = m;
);
function colour_coefs(f) local(g) (
  g = tan($pi * 3000 * 2 ^ (-1.6 * txh_s) / (srate * f)); cg_red = g / (1 + g); tex_r = txh_s;
  g = tan($pi * 300 / (srate * f)); cg_blue = g / (1 + g);
  cg_cr = 1 - exp(-1 / (0.03 * srate * f));
  g = tan($pi * 4730 / (srate * f)); cg_sh = g / (1 + g);
  co_dc = 1 - 2 * $pi * 5 / (srate * f);
);
function colour_clear() (
  uxL.os_clr(); uxR.os_clr(); uyL.os_clr(); uyR.os_clr(); dnL.os_clr(); dnR.os_clr();
  chL.rp.s = chL.bp.s = chL.ep.s = chL.sp.s = chR.rp.s = chR.bp.s = chR.ep.s = chR.sp.s = 0;
  chL.hc.cxy = chL.hc.cxx = chL.lc.cxy = chL.lc.cxx = chR.hc.cxy = chR.hc.cxx = chR.lc.cxy = chR.lc.cxx = 0;
  chL.hd.z1 = chL.hd.z2 = chL.ld.z1 = chL.ld.z2 = chR.hd.z1 = chR.hd.z2 = chR.ld.z1 = chR.ld.z2 = 0;
  co_xa = co_ya = co_xb = co_yb = 0;
);
// one oversampled sample: the VCA's colour, the makeup, the mix, IN and COLOUR (x = the dry input, y = after the
// VCA's gain); returns L in gl_pl, R in gl_pr
function gl_os(xl, xr, ya, yb) local(ca, cb) (
  gl_drs > 0.001 ? (
    ca = (vc_a.clp_run(ya * gl_kv + gl_vb) - gl_rest) / gl_kv; cb = (vc_b.clp_run(yb * gl_kv + gl_vb) - gl_rest) / gl_kv;
    ya += gl_w * (ca - ya); yb += gl_w * (cb - yb);
  );
  ya *= gl_mks; yb *= gl_mks;
  ya = xl + gl_mxs * (ya - xl); yb = xr + gl_mxs * (yb - xr);
  ya = xl + gl_eI * (ya - xl); yb = xr + gl_eI * (yb - xr);
  co_on ? ( gl_pl = chL.colour_run(ya); gl_pr = chR.colour_run(yb); co_add(); gl_pl += cwa; gl_pr += cwb; )
        : ( gl_pl = ya; gl_pr = yb; );
);

// ---------- presets ----------
#gl_pm = "Init|Mix Bus Glue|Gentle Master|Drum Bus Smash|Punchy Drums|Parallel Crush|Vocal Bus|Piano and Room|Bass Lock|Console Grit|Pumping Groove|Clean Modern";
function gl_set(th, ra, at, re, hp, mk, dr, mx) (
  slider1 = 1; slider2 = th; slider3 = ra; slider4 = at; slider5 = re; slider6 = hp; slider7 = mk; slider8 = dr; slider9 = mx;
  slider13 = 0; slider14 = 30; slider15 = 30; slider16 = 0; slider17 = 0; slider18 = 100; slider19 = 100; slider20 = 0;
);
// COLOUR: bands (0 off, 1 HIGH, 2 LOW, 3 both), LOW / HIGH TEXTURE, DIP 220, DIP 3K
function gl_col(b, tl, th, d2, d3) ( slider13 = b; slider14 = tl; slider15 = th; slider16 = d2; slider17 = d3; );
function load_preset(p) local(i) (
  //                 thr  rat att rel hpf  mk   drv  mix
  p == 1  ? gl_set(-12, 1, 4, 5, 0, 0, 0, 100) :     // Init
  p == 2  ? ( gl_set(-16, 1, 5, 5, 2, 1.5, 1.5, 100); gl_col(1, 30, 25, 0, 0); ) :     // Mix Bus Glue
  p == 3  ? gl_set(-14, 0, 5, 5, 2, 0.8, 0, 100) :     // Gentle Master
  p == 4  ? ( gl_set(-24, 4, 0, 0, 1, 11, 4, 100); gl_col(3, 50, 40, 1, 0); ) :     // Drum Bus Smash
  p == 5  ? gl_set(-20, 3, 3, 0, 1, 5.7, 2, 100) :     // Punchy Drums
  p == 6  ? gl_set(-28, 5, 0, 0, 0, 15, 6, 50)  :     // Parallel Crush
  p == 7  ? ( gl_set(-18, 2, 2, 1, 3, 2.6, 1, 100); gl_col(1, 30, 35, 0, 1); ) :     // Vocal Bus
  p == 8  ? gl_set(-18, 1, 4, 2, 2, 2.5, 0.5, 100) :     // Piano and Room
  p == 9  ? ( gl_set(-18, 3, 2, 1, 0, 7.5, 1.5, 100); gl_col(2, 45, 30, 1, 0); ) :     // Bass Lock
  p == 10 ? ( gl_set(-20, 3, 2, 5, 1, 7.8, 8, 100); gl_col(3, 60, 60, 0, 0); ) :     // Console Grit
  p == 11 ? gl_set(-24, 3, 5, 0, 0, 8.2, 2, 100) :     // Pumping Groove
  p == 12 ? gl_set(-14, 1, 5, 5, 2, 1, 0, 100) :     // Clean Modern
  p == 13 ? ( gl_set(-12, 1, 4, 5, 0, 0, 0, 100); slider10 = 0; slider11 = 50; slider12 = 0; );   // Reset all to defaults
  i = 1; loop(20, ui_auto(i); i += 1; );
  pr_cur = p == 13 ? 1 : p;
);

// ---------- state ----------
function gl_srinit() (
  gl_sr = srate;
  gl_kd = gl_tc(1);                                   // the detector: 1 ms RMS-ish average
  gl_krf = gl_tc(100); gl_krs = gl_tc(1600);          // AUTO: fast and slow release paths
  gl_fst = 1 / (0.01 * srate);                        // IN: 10 ms
  gl_gsm = 1 - exp(-1 / (0.015 * srate));
  gl_hpi = -1;
  ost_init(0);
  c_kn = coef(0.03); c_sw = coef(0.015);
  cb_fs = srate; eqL.l3.cb_set(0, 30, 0.707, 0); eqR.l3.cb_set(0, 30, 0.707, 0);
  hs_coefs(); dyn_init(); colour_coefs(os_f); colour_clear();
);
os_design();
os_f = os_tf = os_f2n(slider21); os_base = 0;
w_red = slider13 & 1; w_blue = (slider13 & 2) > 0; tex_s = slider14 * 0.01; txh_s = slider15 * 0.01;
d22_s = slider16; d3_s = slider17; so_s = slider20; lw_s = slider18 * 0.01; hw_s = slider19 * 0.01;
gl_srinit();
os_gsetup();
in_init();
dV.drift_init(0.8 + rand(1) * 0.4); dT.drift_init(0.8 + rand(1) * 0.4); dCL.drift_init(0.8 + rand(1) * 0.4); dCH.drift_init(0.8 + rand(1) * 0.4);
drift_macro(50);
gl_snap = 1;

ui_thm = 5; ui_hw = 1;                                // house keycaps for the preset bar and the size picker
!ui_dflt ? ( ui_sz = 0; pr_cur = 1; );                // new instances open in Medium, on Init
ui_dflt = 1;

// PRESET parameter (JG, 28 Oct 2026: accessibility - a screen reader reaches the presets through REAPER's parameter list).
// A change of prs_sel (a screen reader, the host, automation) loads that preset (0 = No preset: nothing loaded); the preset bar asks through
// prs_dirty. Both load here, on the audio side only. prs_last is saved, so opening a project or recalling its
// state never reloads a preset over the saved settings.
function prs_chk() local(k) (
  prs_dirty > 0 ? (
    k = prs_dirty; prs_dirty = 0; load_preset(k);
    k <= 12 ? ( prs_sel = k; slider_automate(prs_sel); prs_cur_d = k; );
    prs_last = prs_sel;
  ) : prs_ok ? (
    prs_sel = max(0, min(12, prs_sel | 0));
    prs_sel != prs_last ? ( prs_last = prs_sel; prs_sel > 0 ? ( load_preset(prs_sel); prs_cur_d = prs_sel; ); );
  ) : ( prs_last = prs_sel | 0; prs_ok = 1; );
);
prs_ok = 0; prs_dirty = 0;

@slider
prs_chk();

@serialize
file_var(0, ui_sz);
file_var(0, pr_cur);
file_avail(0) != 0 ? file_var(0, prs_last) : prs_last = prs_sel | 0; prs_ok = 1;   // PRESET parameter: no reload on recall

@block
prs_chk();
srate != gl_sr ? gl_srinit();
in_setup(0, 0);
ost_setup(0, 0.3, 120, 6000, -0.3, 0);                // the house output stage: straight through (a mix tool)
os_tf = os_f2n(slider21); pdc_delay = os_lat(os_f);    // (ost_setup zeroes pdc_delay)
pdc_bot_ch = 0; pdc_top_ch = 2;                        // (in_pin / out_pin: REAPER carries any other channels round)
drift_macro(slider11);
gl_n = samplesblock / srate;
gl_vv = dV.drift_tick(0.05 * dm_s * gl_n); gl_vt = dT.drift_tick(0.05 * dm_s * gl_n);
co_dl = dm_a * dCL.drift_tick(0.04 * dm_s * gl_n); co_dh = dm_a * dCH.drift_tick(0.04 * dm_s * gl_n);   // COLOUR drive, dB
gl_tI = slider1 >= 0.5;
gl_tht = min(max(slider2, -30), 0) + 0.3 * dm_a * gl_vt;              // the control voltage wanders a little
gl_ri = floor(min(max(slider3, 0), 5) + 0.5); gl_slt = 1 - 1 / gl_ratio(gl_ri); gl_kwt = gl_knee(gl_ri);   // (glided in @sample)
gl_ka = gl_tc(gl_att(floor(min(max(slider4, 0), 5) + 0.5)));
gl_rli = floor(min(max(slider5, 0), 5) + 0.5); gl_auto = gl_rli == 5; gl_kr = gl_tc(gl_rel(gl_rli));
gl_kas = gl_tc(10 * gl_att(floor(min(max(slider4, 0), 5) + 0.5)) + 20); // AUTO: the slow path charges more slowly
gl_hi = floor(min(max(slider6, 0), 5) + 0.5); gl_hon = gl_hi > 0;
gl_hon && gl_hi != gl_hpi ? ( gl_hpi = gl_hi; gl_schp(gl_hpf(gl_hi)); );
gl_ext = slider10 >= 0.5;
gl_mkt = 10 ^ (min(max(slider7, -5), 15) / 20);
gl_mxt = min(max(slider9, 0), 100) / 100;
gl_drt = min(max(slider8, 0), 10) / 10;
gl_vb = 0.12 + 0.04 * dm_a * gl_vv;                   // the unbalanced VCA's bias (its 2nd harmonic), wandering
vc_a.clp_set(1, 1, 0.8, 0); vc_b.clp_set(1, 1, 0.8, 0); gl_rest = vc_a.clp_crv(gl_vb);
gl_snap ? ( gl_th = gl_tht; gl_sl = gl_slt; gl_kw = gl_kwt; gl_gI = gl_tI; gl_mks = gl_mkt; gl_mxs = gl_mxt; gl_drs = gl_drt;
            vc_a.x1 = vc_b.x1 = gl_vb; vc_a.ad1 = vc_b.ad1 = vc_a.clp_ad(gl_vb); );
gl_snap = 0;
vc_a.clp_tidy(); vc_b.clp_tidy(); gl_ms = gl_tn(gl_ms); sa.s1 = gl_tn(sa.s1); sa.s2 = gl_tn(sa.s2); sb.s1 = gl_tn(sb.s1); sb.s2 = gl_tn(sb.s2);
// the panel's mailboxes
gla_gr_d = min(gla_gr_d, gl_grm); gl_grm = 0;
gla_ol_d = max(gla_ol_d, gl_ol); gla_or_d = max(gla_or_d, gl_or); gl_ol = gl_or = 0;

@sample
spl0 != spl0 ? spl0 = 0; spl1 != spl1 ? spl1 = 0; spl2 != spl2 ? spl2 = 0; spl3 != spl3 ? spl3 = 0;
in_process(spl0, spl1);
gl_xl = in_l; gl_xr = in_r;
abs(gl_xl) < 0.000000000000001 ? gl_xl = 0; abs(gl_xr) < 0.000000000000001 ? gl_xr = 0;
gl_gI != gl_tI ? ( gl_gI = gl_tI ? min(gl_gI + gl_fst, 1) : max(gl_gI - gl_fst, 0); );
gl_eI = gl_sm(gl_gI);
gl_th += (gl_tht - gl_th) * c_kn; gl_sl += (gl_slt - gl_sl) * c_kn; gl_kw += (gl_kwt - gl_kw) * c_kn;   // no steps on a preset change
gl_mks += (gl_mkt - gl_mks) * gl_gsm; gl_mxs += (gl_mxt - gl_mxs) * gl_gsm; gl_drs += (gl_drt - gl_drs) * gl_gsm;
// the detector: the key or the input, through the SC HPF (always running: warm when switched)
gl_da = gl_ext ? spl2 : gl_xl; gl_db = gl_ext ? spl3 : gl_xr;
gl_ha = sa.gl_bq(gl_da); gl_hb = sb.gl_bq(gl_db);
gl_hon ? ( gl_da = gl_ha; gl_db = gl_hb; );
gl_ms += gl_kd * (max(gl_da * gl_da, gl_db * gl_db) - gl_ms);       // linked: the louder side
gl_lv = 10 * log10(max(gl_ms, 0.00000000001)) + 3.0103;
gl_ov = gl_lv - gl_th;
gl_k2 = gl_kw * 0.5;
gl_t = gl_ov <= -gl_k2 ? 0 : gl_ov >= gl_k2 ? gl_ov * gl_sl : gl_sl * sqr(gl_ov + gl_k2) / (2 * gl_kw);
gl_t = -gl_t;
gl_auto ? (                                                          // AUTO: two stages
  gl_gf += (gl_t < gl_gf ? gl_ka : gl_krf) * (gl_t - gl_gf);
  gl_gs += (gl_t < gl_gs ? gl_kas : gl_krs) * (gl_t - gl_gs);
  gl_gdb = min(gl_gf, gl_gs);
) : (
  gl_gdb += (gl_t < gl_gdb ? gl_ka : gl_kr) * (gl_t - gl_gdb);
  gl_gf = gl_gs = gl_gdb;
);
gl_grm = min(gl_grm, gl_gdb * gl_eI);
// the VCA's gain at 1x, then the oversampled section: the VCA's colour (unbalanced above DRIVE 0: richer as it works
// harder), the makeup, the mix, IN and COLOUR
gl_g = exp(0.115129255 * gl_gdb);
gl_ya = gl_xl * gl_g; gl_yb = gl_xr * gl_g;
gl_drs > 0.001 ? (
  gl_kv = (0.15 + 2.6 * gl_drs * gl_drs) * (1 - 0.11 * max(gl_gdb, -20)); // (gl_gdb <= 0: more colour with more GR)
  gl_w = min(gl_drs * 8, 1);                                          // (the first step of DRIVE switches it to unbalanced)
);
// COLOUR's glides
w_red += ((slider13 & 1) - w_red) * c_kn; w_blue += (((slider13 & 2) > 0) - w_blue) * c_kn;
!(slider13 & 1) && w_red < 0.00001 ? w_red = 0; !(slider13 & 2) && w_blue < 0.00001 ? w_blue = 0;
tex_s += (slider14 * 0.01 - tex_s) * c_kn; txh_s += (slider15 * 0.01 - txh_s) * c_kn;
so_s += ((slider20 >= 0.5) - so_s) * c_sw; slider20 < 0.5 && so_s < 0.00001 ? so_s = 0;
lw_s += (slider18 * 0.01 - lw_s) * c_kn; hw_s += (slider19 * 0.01 - hw_s) * c_kn;
lwm_g = min(1, 2 - lw_s); lws_g = min(1, lw_s); hwm_g = min(1, 2 - hw_s); hws_g = min(1, hw_s);
co_on = w_red + w_blue > 0;
co_on ? (
  // HIGH widens as it is pushed: its band starts at 3 kHz at TEXTURE 0 and comes down to 1 kHz at 100 %
  abs(txh_s - tex_r) > 0.0005 ? ( tex_r = txh_s; tx_g = tan($pi * 3000 * 2 ^ (-1.6 * txh_s) / (srate * os_f)); cg_red = tx_g / (1 + tx_g); );
  sh_hi = db2g(3.1 * txh_s) - 1; sh_lo = db2g(2.5 * min(tex_s, 0.5)) - 1;   // each band lifts: HIGH up to +3.1 dB, LOW +1.25 dB
);
d22_s += (slider16 - d22_s) * c_sw; d3_s += (slider17 - d3_s) * c_sw;
abs(d22_s - slider16) < 0.0001 ? d22_s = slider16; abs(d3_s - slider17) < 0.0001 ? d3_s = slider17;
abs(tex_s - tex_q) > 0.002 || (tex_s != tex_q && tex_s == slider14 * 0.01) ||
  abs(txh_s - txh_q) > 0.002 || (txh_s != txh_q && txh_s == slider15 * 0.01) ||
  abs(co_dl - co_dlq) > 0.05 || abs(co_dh - co_dhq) > 0.05 ||
  abs(-6 * d3_s - gd3_q) > 0.03 || abs(-9 * d22_s - gd22_q) > 0.03 ? hs_coefs();
os_gate();
os_chg ? ( colour_coefs(os_f); hs_coefs(); colour_clear(); os_hold += 2 * samplesblock; );
uxL.os_up(gl_xl); uxR.os_up(gl_xr); uyL.os_up(gl_ya); uyR.os_up(gl_yb);
gl_os(uxL.s0, uxR.s0, uyL.s0, uyR.s0); gl_a0 = gl_pl; gl_b0 = gl_pr;
os_f == 1 ? ( gl_yl = gl_a0; gl_yr = gl_b0; ) : (
  gl_os(uxL.s1, uxR.s1, uyL.s1, uyR.s1); gl_a1 = gl_pl; gl_b1 = gl_pr;
  os_f == 2 ? ( gl_yl = dnL.os_dn(gl_a0, gl_a1, 0, 0); gl_yr = dnR.os_dn(gl_b0, gl_b1, 0, 0); ) : (
    gl_os(uxL.s2, uxR.s2, uyL.s2, uyR.s2); gl_a2 = gl_pl; gl_b2 = gl_pr;
    gl_os(uxL.s3, uxR.s3, uyL.s3, uyR.s3); gl_a3 = gl_pl; gl_b3 = gl_pr;
    gl_yl = dnL.os_dn(gl_a0, gl_a1, gl_a2, gl_a3); gl_yr = dnR.os_dn(gl_b0, gl_b1, gl_b2, gl_b3);
  );
);
// COLOUR's tone (above 50 % TEXTURE) and the DIP keys' dynamic dips, at 1x
co_on ? ( gl_yl = eqL.hs_tone(gl_yl); gl_yr = eqR.hs_tone(gl_yr); );
d22_s * w_blue + d3_s * w_red > 0.0001 ? (
  dyn_run(0.5 * (gl_yl + gl_yr));
  gl_yl = dqL.b.cb_run(dqL.a.cb_run(gl_yl)); gl_yr = dqR.b.cb_run(dqR.a.cb_run(gl_yr));
) : ( dg_a = dg_b = 0; dqL.a.z1 = dqL.a.z2 = dqL.b.z1 = dqL.b.z2 = dqR.a.z1 = dqR.a.z2 = dqR.b.z1 = dqR.b.z2 = 0; dd_n = 16; );
gl_os_g = os_g * os_g * (3 - 2 * os_g);                               // the OS swap's dip (an S-curve)
gl_yl *= gl_os_g; gl_yr *= gl_os_g;
gl_yl != gl_yl ? gl_yl = 0; gl_yr != gl_yr ? gl_yr = 0;
ost_out(gl_yl, gl_yr);
spl0 = max(-4, min(4, ost_l));
spl1 = max(-4, min(4, ost_r));
gl_ol = max(gl_ol, abs(spl0)); gl_or = max(gl_or, abs(spl1));

@gfx 1080 560
ui_begin(1080, 560);
// the panel's own copies of the step tables (the audio thread has gl_att / gl_rel / gl_hpf)
function gv_att(i) ( i == 0 ? 0.1 : i == 1 ? 0.3 : i == 2 ? 1 : i == 3 ? 3 : i == 4 ? 10 : 30; );
function gv_rel(i) ( i == 0 ? 100 : i == 1 ? 300 : i == 2 ? 600 : i == 3 ? 1200 : 2400; );
function gv_hpf(i) ( i == 1 ? 40 : i == 2 ? 80 : i == 3 ? 120 : i == 4 ? 185 : 250; );
// =====================================================================
// GUI - in the 34 series manner (KRT81): sharp-cornered sections, black bakelite knobs with printed scales (the stepped
// ones click between their six positions), an analogue GR needle meter (backlit in blue in the Lab copy), a COLOUR
// row along the bottom. Two looks: VFD (charcoal and blue) and MATRIX
// (battleship grey, elephant's breath print, cream). Design space 1080 x 560. Knobs: drag (SHIFT = fine), wheel,
// double-click = default; the name shows the value while the knob is touched.
// =====================================================================
gv_mx = 0;                                  // one look (VFD); slider12 kept so old projects load
!gv_mx ? (                                                // VFD: charcoal and blue
  KRP = 0x2E2F31; KRF = 0x202123; KRI = 0xDDE9F5; KRA = 0x5FA8E8; KRW = 0x121314; KRM = 0x55575A;
  KRC = 0x5FA8E8; KRPT = 0x08182A; KRCN = 0x5FA8E8; KRGH = 0x252B33; KRAMB = 0xFFD24A; KRRED = 0xFF3B1F; KRKT = 0x08182A;
) : (                                                     // MATRIX: battleship grey, elephant's breath, cream
  KRP = 0x222528; KRF = ui_mix(0x222528, 0x000000, 0.25); KRI = 0xC7BEB3; KRA = 0xF3E5C0; KRW = ui_mix(0x222528, 0x000000, 0.45);
  KRM = 0x4A4E4A; KRC = 0xF3E5C0; KRPT = 0x17191B; KRCN = 0x1E5631; KRGH = 0x2C302E; KRAMB = 0xD4A373; KRRED = 0xBC3908; KRKT = 0x17191B;
);
KRH = ui_mix(KRP, 0x000000, 0.35); KRI2 = ui_mix(KRI, KRF, 0.35);

gfx_setfont(6,  "Trebuchet MS", 27 * sc, 'b');
gfx_setfont(10, "Trebuchet MS", 12 * sc, 'b');
gfx_setfont(11, "Trebuchet MS", 13 * sc, 'b');
gfx_setfont(12, "Trebuchet MS", 13 * sc, 'b');
gfx_setfont(13, "Trebuchet MS", 12 * sc, 'b');
gfx_setfont(14, "Trebuchet MS", 15 * sc, 'b');
gfx_setfont(15, "Trebuchet MS", 12 * sc, 'b');     // lamp key legends

function kr_r(x, y, w, h) ( gfx_rect(x * sc, y * sc, w * sc, h * sc); );
function kr_txt(x, y, s) ( gfx_x = x * sc; gfx_y = y * sc; gfx_drawstr(s); );
function kr_ctxt(cx, cy, s) local(tw, th) ( gfx_measurestr(s, tw, th); gfx_x = cx * sc - tw * 0.5; gfx_y = cy * sc - th * 0.5; gfx_drawstr(s); );
function kr_rtxt(rx, cy, s) local(tw, th) ( gfx_measurestr(s, tw, th); gfx_x = rx * sc - tw; gfx_y = cy * sc - th * 0.5; gfx_drawstr(s); );
function kr_lbl(x, y, s) ( gfx_setfont(11); ui_col(KRI2); kr_txt(x, y, s); );
function kr_hov(x, y, w, h) ( mouse_x >= x * sc && mouse_x < (x + w) * sc && mouse_y >= y * sc && mouse_y < (y + h) * sc; );
function kr_thick(x1, y1, x2, y2, w) local(dx, dy, l, nx, ny, k) (       // (drawn here: works with any system files)
  dx = x2 - x1; dy = y2 - y1; l = max(sqrt(dx * dx + dy * dy), 0.001); nx = -dy / l; ny = dx / l;
  k = -0.5 * w; loop(floor(w * 2.5) + 1, gfx_line(x1 + nx * k, y1 + ny * k, x2 + nx * k, y2 + ny * k, 1); k += 0.4; );
);
// panel keys: house lamp keys (gfx-ui). kr_kc = the key's lamp colour (set before the call; cleared after;
// none = the face's accent colour)
function kr_key(x, y, w, h, lbl, on) local(c) (
  c = kr_kc ? kr_kc : KRA; kr_kc = 0;
  lk_well = 0x0A0A0B; lk_font = 15; lk_font2 = 15;
  lk_fit(x, y, w, h, on, c, lbl);
);
// a section that's switched off: greyed slightly (its IN key is redrawn on top)
function kr_off(x, y, w, h) ( ui_cola(KRF, 0.5); kr_r(x + 1, y + 1, w - 2, h - 2); );
// a key that isn't live: an unlit lamp key, dimmed
function kr_dim(x, y, w, h, lbl) local(c) (
  c = kr_kc ? kr_kc : KRA; kr_kc = 0;
  lk_well = 0x0A0A0B; lk_font = 15; lk_font2 = 15; lk_inert = 1;
  lk_fit(x, y, w, h, 0, c, lbl);
  ui_cola(KRF, 0.55); kr_r(x, y, w, h); 0;
);
function kr_sect(x, y, w, h, name) (
  ui_col(0x040404); kr_r(x, y, w, h); ui_col(KRF); kr_r(x + 1, y + 1, w - 2, h - 2);
  strlen(name) ? kr_lbl(x + 9, y + 6, name);
);
function kr_scale(cx, cy, r, n, nr, labels, minor) local(i, k, k0, len, c, a, ca, sa, m) (
  len = strlen(labels); k0 = 0; i = 0; k = 0;
  minor > 0 ? (
    m = 0; ui_col(KRI2);
    loop((n - 1) * minor + 1,
      a = -0.75 * $pi + m / ((n - 1) * minor) * 1.5 * $pi; ca = sin(a); sa = -cos(a);
      gfx_line((cx + ca * (r + 4)) * sc, (cy + sa * (r + 4)) * sc, (cx + ca * (r + 6)) * sc, (cy + sa * (r + 6)) * sc, 1);
      m += 1;
    );
  );
  gfx_setfont(10);
  loop(len + 1,
    c = k < len ? str_getchar(labels, k) : 124;
    c == 124 ? (
      strcpy_substr(#krv_sl, labels, k0, k - k0);
      a = -0.75 * $pi + i / max(n - 1, 1) * 1.5 * $pi; ca = sin(a); sa = -cos(a);
      ui_col(KRI);
      gfx_line((cx + ca * (r + 3)) * sc, (cy + sa * (r + 3)) * sc, (cx + ca * (r + 7)) * sc, (cy + sa * (r + 7)) * sc, 1);
      strlen(#krv_sl) ? kr_ctxt(cx + ca * (r + nr), cy + sa * (r + nr), #krv_sl);
      i += 1; k0 = k + 1;
    );
    k += 1;
  );
);
function kr_bake(cx, cy, r, a) local(i, t, ca, sa, k) (
  ca = sin(a); sa = -cos(a);
  ui_cola(0x000000, 0.55); gfx_circle(cx + 2.5 * sc, cy + 4.5 * sc, r * 1.02, 1, 1);
  ui_col(0x0A0A0A); gfx_circle(cx, cy, r + 1 * sc, 1, 1);
  ui_col(0x191817); gfx_circle(cx, cy, r, 1, 1);
  i = 0;
  loop(16,
    t = i / 16 * 2 * $pi; k = 0.5 + 0.5 * cos(t + 0.8);
    ui_col(ui_mix(0x0D0C0B, 0x48443F, k * 0.8)); gfx_circle(cx + sin(t) * r * 0.86, cy - cos(t) * r * 0.86, r * 0.15, 1, 1);
    ui_col(0x0D0C0B); gfx_circle(cx + sin(t + 0.196) * r * 0.9, cy - cos(t + 0.196) * r * 0.9, r * 0.075, 1, 1);
    i += 1;
  );
  ui_col(0x090808); gfx_circle(cx, cy, r * 0.70 + 1 * sc, 1, 1);
  ui_col(0x22201E); gfx_circle(cx, cy, r * 0.70, 1, 1);
  ui_cola(0xFFFFFF, 0.08); gfx_circle(cx, cy, r * 0.70, 0, 1);
  ui_col(0x060606); kr_thick(cx + ca * r * 0.18, cy + sa * r * 0.18, cx + ca * r * 0.98, cy + sa * r * 0.98, 4.2 * sc);
  ui_col(KRA); kr_thick(cx + ca * r * 0.22, cy + sa * r * 0.22, cx + ca * r * 0.95, cy + sa * r * 0.95, 2.5 * sc);
);
function kr_scap(cx, cy, r, a, cap) local(ca, sa, k, pc) (
  ca = sin(a); sa = -cos(a);
  ui_cola(0x000000, 0.5); gfx_circle(cx + 1.5 * sc, cy + 2.5 * sc, r, 1, 1);
  ui_col(0x121212); gfx_circle(cx, cy, r, 1, 1);
  ui_cola(0xFFFFFF, 0.12); gfx_circle(cx, cy, r, 0, 1);
  k = r * 0.74;
  ui_col(cap); gfx_circle(cx, cy, k, 1, 1);
  ui_col(ui_mix(cap, 0x000000, 0.35)); gfx_circle(cx, cy, k, 0, 1);
  pc = (cap & 255) + ((cap >> 8) & 255) + ((cap >> 16) & 255) > 380 ? KRPT : 0xF4F0E6;
  ui_col(pc); kr_thick(cx + ca * k * 0.2, cy + sa * k * 0.2, cx + ca * r * 0.94, cy + sa * r * 0.94, 2.6 * sc);
);
krv_small = 0; krv_lg = 0; krv_step = 0; krv_cap = 0;
function kr_kn(v, lo, hi) ( krv_lg ? log(max(v, lo) / lo) / log(hi / lo) : (v - lo) / (hi - lo); );
function kr_kv(n, lo, hi) ( krv_lg ? lo * exp(n * log(hi / lo)) : lo + n * (hi - lo); );
function kr_knob(cx, cy, r, idx, lo, hi, def) local(v, n, dx, dy, hov, did) (
  did = 1700 + idx; v = slider(idx);
  dx = mouse_x - cx * sc; dy = mouse_y - cy * sc; hov = dx * dx + dy * dy <= sqr((r + 5) * sc);
  pressed && hov && drag_id == 0 ? (
    time_precise() - krv_lt < 0.35 && krv_lid == idx ? ( slider(idx) = def; ui_auto(idx); drag_id = -1; krv_lt = 0; ) : (
      drag_id = did; krv_y = mouse_y; krv_lt = time_precise(); krv_lid = idx; krv_n = kr_kn(v, lo, hi);
    );
  );
  drag_id == did ? (
    krv_n = min(max(krv_n + (krv_y - mouse_y) / (((mouse_cap & 8) ? 1200 : (krv_step ? 160 : 240)) * sc), 0), 1); krv_y = mouse_y;
    v = kr_kv(krv_n, lo, hi); krv_step ? v = floor(v + 0.5);
    slider(idx) != v ? ( slider(idx) = v; ui_auto(idx); );
  );
  hov && mouse_wheel != 0 && drag_id == 0 ? (
    krv_step ? slider(idx) = min(max(floor(v + 0.5) + sign(mouse_wheel), lo), hi) : (
      n = min(max(kr_kn(v, lo, hi) + mouse_wheel / 120 * ((mouse_cap & 8) ? 0.005 : 0.025), 0), 1);
      slider(idx) = kr_kv(n, lo, hi);
    );
    ui_auto(idx); mouse_wheel = 0;
  );
  v = slider(idx); n = min(max(kr_kn(v, lo, hi), 0), 1);
  krv_small ? kr_scap(cx * sc, cy * sc, r * sc, -0.75 * $pi + n * 1.5 * $pi, krv_cap ? krv_cap : KRC) : kr_bake(cx * sc, cy * sc, r * sc, -0.75 * $pi + n * 1.5 * $pi);
  hov || drag_id == did ? ( ui_cola(0xFFFFFF, 0.07); gfx_circle(cx * sc, cy * sc, (r + 2) * sc, 1, 1); );
  hov || drag_id == did;
);
function kr_name(cx, cy, act, name, val) ( gfx_setfont(14); ui_col(act ? KRA : KRI); kr_ctxt(cx, cy, act ? val : name); );
// a small knob with a tick ring (or n step ticks) and its name; the value while touched
function kr_small(cx, cy, r, idx, lo, hi, def, nt, name, val) local(i, t, act) (
  ui_cola(KRI, 0.85); i = 0;
  loop(nt, t = -0.75 * $pi + i / (nt - 1) * 1.5 * $pi;
    gfx_line((cx + sin(t) * (r + 4)) * sc, (cy - cos(t) * (r + 4)) * sc, (cx + sin(t) * (r + 8)) * sc, (cy - cos(t) * (r + 8)) * sc, 1); i += 1; );
  krv_small = 1; act = kr_knob(cx, cy, r, idx, lo, hi, def); krv_small = 0;
  strlen(name) ? ( gfx_setfont(11); ui_col(act ? KRA : KRI); kr_ctxt(cx, cy + r + 15, act ? val : name); );
  act;
);
function kr_cell(x, y, w, h, lit, zone) local(c) (
  c = zone == 2 ? KRRED : zone == 1 ? KRAMB : KRCN;
  lit ? ( ui_cola(c, 0.16); kr_r(x - 0.5, y - 1, w + 1, h + 2); ui_col(c); kr_r(x, y, w, h); ui_cola(0xFFFFFF, 0.22); kr_r(x, y, w, 1.5); )
      : ( ui_col(KRGH); kr_r(x, y, w, h); ui_cola(0x000000, 0.25); kr_r(x, y + h - 1, w, 1); );
);
function kr_hbar(x, y, w, h, v) local(n, i, sw, d) (
  n = 32; sw = w / n; i = 0;
  loop(n, d = -48 + (i + 0.5) / n * 48; kr_cell(x + i * sw + 0.5, y, sw - 2, h, v >= d, d > -3 ? 2 : d > -12 ? 1 : 0); i += 1; );
);
function kr_grbar(x, y, w, h, gr) local(n, i, sw, d) (                     // gain reduction: 0 .. -24 dB, from the left
  n = 24; sw = w / n; i = 0;
  loop(n, d = -(i + 0.5); kr_cell(x + i * sw + 0.5, y, sw - 2, h, gr <= d, 1); i += 1; );
);
function kr_window(x, y, w, h) ( ui_col(0x040404); kr_r(x, y, w, h); ui_col(KRW); kr_r(x + 1, y + 1, w - 2, h - 2); );
function kr_db(v) ( sprintf(#krd, "%+.1f dB", v); #krd; );
function kr_tog(idx) ( slider(idx) = slider(idx) >= 0.5 ? 0 : 1; ui_auto(idx); );

// =====================================================================
// Faceplate and header
// =====================================================================
ui_col(ui_mix(KRP, 0x000000, 0.35)); gfx_rect(0, 0, gfx_w, gfx_h);
ui_cola(0xFFFFFF, 0.08); kr_r(0, 0, 1080, 1.5); kr_r(0, 0, 1.5, 560);
ui_cola(0x000000, 0.35); kr_r(0, 558.5, 1080, 1.5); kr_r(1078.5, 0, 1.5, 560);
ui_col(KRP); kr_r(12, 12, 1056, 536);
ui_cola(0x000000, 0.30); kr_r(12, 12, 1056, 1.5); kr_r(12, 12, 1.5, 536);
ui_cola(0xFFFFFF, 0.07); kr_r(12, 546.5, 1056, 1.5); kr_r(1066.5, 12, 1.5, 536);
ui_col(KRH); kr_r(13.5, 13.5, 1053, 50.5);
ui_col(KRA); kr_r(13.5, 64, 1053, 2);
ui_col(KRA); gfx_circle(45 * sc, 39 * sc, 16 * sc, 1, 1);
gfx_setfont(4); ui_col(KRKT); ui_text(45 * sc, 33.5 * sc, "GFX");
gfx_setfont(6); ui_col(KRI); kr_txt(72, 24, "GLU34"); gfx_measurestr("GLU34", gv_tw, gv_th);
gv_tx = 72 + gv_tw / sc + 14;
gfx_setfont(11); ui_col(KRI2); kr_txt(gv_tx, 25, "STEREO BUS"); kr_txt(gv_tx, 40, "COMPRESSOR");
pk_pextra = "Reset all to defaults";
prs_cur_d > 0 ? ( pr_cur = prs_cur_d; prs_cur_d = 0; );     // PRESET parameter: show a preset loaded by the host
k_ = pk_preset(318 * sc, 22 * sc, 290 * sc, 34 * sc, pr_cur, #gl_pm);
k_ > 0 ? prs_dirty = k_;
os_key(956, 28, 21, KRI2, 16);
ui_size_picker(1000 * sc, 30 * sc);

gv_k1 = 72 + 128; gv_k2 = 72 + 268;
// ---------- COMPRESSOR ----------
kr_sect(22, 72, 520, 338, "COMPRESSOR");
kr_kc = LK_AMB; kr_key(470, 80, 64, 26, "IN", slider1 >= 0.5) ? kr_tog(1);
kr_scale(98, gv_k1, 40, 4, 19, "-30|-20|-10|0", 2);
gv_a = kr_knob(98, gv_k1, 40, 2, -30, 0, -12); sprintf(#gvv, "%.1f dBFS", slider2); kr_name(98, gv_k1 + 70, gv_a, "THRESHOLD", #gvv);
krv_step = 1;
kr_scale(232, gv_k1, 30, 6, 17, "1.5|2|3|4|10|20", 0);
gv_a = kr_knob(232, gv_k1, 30, 3, 0, 5, 1); sprintf(#gvv, "%s : 1", slider3 < 0.5 ? "1.5" : slider3 < 1.5 ? "2" : slider3 < 2.5 ? "3" : slider3 < 3.5 ? "4" : slider3 < 4.5 ? "10" : "20");
kr_name(232, gv_k1 + 60, gv_a, "RATIO", #gvv);
kr_scale(352, gv_k1, 30, 6, 17, ".1|.3|1|3|10|30", 0);
gv_a = kr_knob(352, gv_k1, 30, 4, 0, 5, 4); sprintf(#gvv, "%g ms", gv_att(floor(slider4 + 0.5))); kr_name(352, gv_k1 + 60, gv_a, "ATTACK", #gvv);
kr_scale(472, gv_k1, 30, 6, 17, ".1|.3|.6|1.2|2.4|A", 0);
gv_a = kr_knob(472, gv_k1, 30, 5, 0, 5, 5); slider5 >= 4.5 ? strcpy(#gvv, "AUTO") : sprintf(#gvv, "%g s", gv_rel(floor(slider5 + 0.5)) / 1000);
kr_name(472, gv_k1 + 60, gv_a, "RELEASE", #gvv);
gfx_setfont(10); ui_col(KRI2); kr_ctxt(352, gv_k1 + 76, "ms"); kr_ctxt(472, gv_k1 + 76, "s");
// the lower row: MAKEUP, SC HPF, EXT SC
krv_step = 0;
kr_scale(98, gv_k2, 30, 5, 17, "-5|0|+5|+10|+15", 0);
gv_a = kr_knob(98, gv_k2, 30, 7, -5, 15, 0); kr_name(98, gv_k2 + 58, gv_a, "MAKEUP", kr_db(slider7));
krv_step = 1;
kr_scale(232, gv_k2, 30, 6, 17, "OFF|40|80|120|185|250", 0);
gv_a = kr_knob(232, gv_k2, 30, 6, 0, 5, 0); slider6 < 0.5 ? strcpy(#gvv, "OFF") : sprintf(#gvv, "%d Hz", gv_hpf(floor(slider6 + 0.5)));
kr_name(232, gv_k2 + 58, gv_a, "SC HPF", #gvv);
krv_step = 0;
kr_kc = LK_RED; kr_key(316, gv_k2 - 18, 100, 34, "EXT SC", slider10 >= 0.5) ? kr_tog(10);
gfx_setfont(10); ui_col(KRI2); kr_ctxt(366, gv_k2 + 30, "KEY = INPUTS 3 / 4");

slider1 < 0.5 ? ( kr_off(22, 72, 520, 338); kr_kc = LK_AMB; kr_key(470, 80, 64, 26, "IN", 0) ? kr_tog(1); );
// ---------- GR: the needle meter, and the output ----------
kr_sect(548, 72, 300, 338, "GAIN REDUCTION");
ui_glblue = 1; ui_spill(558 * sc, 96 * sc, 280 * sc, 178 * sc, 1);           // the blue lamp's light round the bezel
ui_col(0x040404); kr_r(558, 96, 280, 178);
ui_glow(floor(559 * sc + 0.5), floor(97 * sc + 0.5), 278 * sc, 176 * sc, 1);  // the backlit face (blue)
ui_glblue = 0;
GVINK = 0x10233A; GVRED = 0xB3341D;                                           // the print on the lit face
gv_g = gla_gr_d; gla_gr_d = 0;
gv_nd += ((gv_g < gv_nd ? 0.55 : 0.18) * (gv_g - gv_nd));                     // the needle's ballistics
function gv_ang(gr) ( 0.72 - 1.44 * (min(-gr, 20) / 20) ^ 0.6; );               // 0 dB right .. -20 dB left
gv_px = 698; gv_py = 300; gv_rr = 176;
gv_i = 0;
loop(8,
  gv_d = gv_i == 0 ? 0 : gv_i == 1 ? -2 : gv_i == 2 ? -4 : gv_i == 3 ? -6 : gv_i == 4 ? -8 : gv_i == 5 ? -10 : gv_i == 6 ? -14 : -20;
  gv_t = gv_ang(gv_d); gv_ca = sin(gv_t); gv_sa = -cos(gv_t);
  ui_col(gv_d <= -10 ? GVRED : GVINK);
  kr_thick((gv_px + gv_ca * (gv_rr - 40)) * sc, (gv_py + gv_sa * (gv_rr - 40)) * sc, (gv_px + gv_ca * (gv_rr - 28)) * sc, (gv_py + gv_sa * (gv_rr - 28)) * sc, 1.6 * sc);
  gfx_setfont(10); sprintf(#gvd, "%d", -gv_d); kr_ctxt(gv_px + gv_ca * (gv_rr - 16), gv_py + gv_sa * (gv_rr - 16), #gvd);
  gv_i += 1;
);
gfx_setfont(11); ui_col(GVINK); kr_ctxt(698, 112, "GR  dB");
gv_t = gv_ang(gv_nd); gv_ca = sin(gv_t); gv_sa = -cos(gv_t);
ui_cola(0x0A1A2C, 0.35); kr_thick((gv_px + gv_ca * 54 + 1.5) * sc, (gv_py + gv_sa * 54 + 2) * sc, (gv_px + gv_ca * (gv_rr - 30) + 1.5) * sc, (gv_py + gv_sa * (gv_rr - 30) + 2) * sc, 2.4 * sc);
ui_col(0x0B0D10); kr_thick((gv_px + gv_ca * 54) * sc, (gv_py + gv_sa * 54) * sc, (gv_px + gv_ca * (gv_rr - 30)) * sc, (gv_py + gv_sa * (gv_rr - 30)) * sc, 2 * sc);
ui_col(KRW); kr_r(560, 250, 276, 22);
ui_col(0x0A0A0A); gfx_circle(gv_px * sc, 268 * sc, 9 * sc, 1, 1);
sprintf(#gvg, "%.1f dB", gv_nd); gfx_setfont(14); ui_col(KRA); kr_ctxt(698, 292, #gvg);
// the output: L / R bars
function kr_fall(v, pk) ( pk = pk > 0.0000001 ? 20 * log10(pk) : -90; pk > v ? pk : max(v - 0.6, -90); );
gv_ml = kr_fall(gv_ml, gla_ol_d); gv_mr = kr_fall(gv_mr, gla_or_d); gla_ol_d = gla_or_d = 0;
kr_window(558, 314, 280, 50);
kr_hbar(588, 324, 242, 12, gv_ml); kr_hbar(588, 340, 242, 12, gv_mr);
gfx_setfont(10); ui_col(KRI); kr_txt(566, 324, "L"); kr_txt(566, 340, "R");
gfx_setfont(10); ui_col(KRI2); gv_i = 0;
loop(5, gv_d = -gv_i * 12; kr_ctxt(588 + (gv_d + 48) / 48 * 242 - (gv_i == 0 ? 6 : 0), 376, gv_i == 0 ? "0" : sprintf(#gvd, "%d", gv_d)); gv_i += 1; );
gfx_setfont(11); ui_col(KRI2); kr_txt(566, 390, "OUT  dBFS");

// ---------- VCA: VCA DRIVE, MIX, DRIFT ----------
kr_sect(854, 72, 204, 338, "VCA");
kr_scale(956, gv_k1, 34, 6, 18, "0|2|4|6|8|10", 0);
gv_a = kr_knob(956, gv_k1, 34, 8, 0, 10, 0); slider8 < 0.05 ? strcpy(#gvv, "BALANCED") : sprintf(#gvv, "%.1f", slider8); kr_name(956, gv_k1 + 64, gv_a, "VCA DRIVE", #gvv);
gfx_setfont(10); ui_col(slider8 < 0.05 ? KRA : KRI2); kr_ctxt(956, gv_k1 + 82, slider8 < 0.05 ? "CLEAN" : "UNBALANCED");
sprintf(#gvv, "%d%%", floor(slider9 + 0.5)); kr_small(912, gv_k2, 20, 9, 0, 100, 100, 11, "MIX", #gvv);
sprintf(#gvv, "%d%%", floor(slider11 + 0.5)); kr_small(1002, gv_k2, 16, 11, 0, 100, 50, 11, "DRIFT", #gvv);

// ---------- COLOUR (Sifon's): LOW and HIGH, each with TEXTURE, WIDTH and its DIP; SOLO ----------
// a WIDTH dial's print: M / C / S at the ends and the centre, one dot in each gap
function kr_wscale(cx, cy, r) local(i, a) (
  gfx_setfont(10); ui_col(KRI); i = 0;
  loop(3, a = -0.75 * $pi + i * 0.75 * $pi; kr_ctxt(cx + sin(a) * (r + 13), cy - cos(a) * (r + 13), i == 0 ? "M" : i == 1 ? "C" : "S"); i += 1; );
  ui_col(KRI2); i = 0;
  loop(2, a = -0.375 * $pi + i * 0.75 * $pi; gfx_circle((cx + sin(a) * (r + 7)) * sc, (cy - cos(a) * (r + 7)) * sc, 1.6 * sc, 1, 1); i += 1; );
);
// one band: its key, TEXTURE, WIDTH and DIP (hx = the half's left edge; bit 1 = HIGH, 2 = LOW)
function kr_band(hx, bit, kc, name, tidx, widx, didx, dc, dname) local(on, a) (
  on = (slider13 & bit) > 0;
  kr_scale(hx + 180, 474, 26, 5, 16, "0||50||100", 0);
  a = kr_knob(hx + 180, 474, 26, tidx, 0, 100, 30); sprintf(#gvv, "%d %%", floor(slider(tidx) + 0.5)); kr_name(hx + 180, 527, a, "TEXTURE", #gvv);
  kr_wscale(hx + 300, 474, 18);
  krv_small = 1; a = kr_knob(hx + 300, 474, 18, widx, 0, 200, 100); krv_small = 0;
  sprintf(#gvv, "%d %%", floor(slider(widx) + 0.5)); kr_name(hx + 300, 527, a, "WIDTH", #gvv);
  kr_kc = dc; kr_key(hx + 376, 457, 84, 34, dname, slider(didx) >= 0.5 && on) ? kr_tog(didx);
  !on ? kr_off(hx + 4, 446, 508, 94);
  kr_kc = kc; kr_key(hx + 22, 457, 76, 34, name, on) ? ( slider13 = slider13 ~ bit; ui_auto(13); );
);
kr_sect(22, 416, 1036, 126, "COLOUR");
ui_col(0x040404); kr_r(539.5, 444, 1, 92); ui_cola(0xFFFFFF, 0.06); kr_r(540.5, 444, 1, 92);
kr_band(22, 2, LK_BLU_LT, "LOW", 14, 18, 16, LK_GRN, "DIP 220");
kr_band(542, 1, LK_RED, "HIGH", 15, 19, 17, LK_AMB, "DIP 3K");
kr_kc = LK_YEL; kr_key(986, 422, 64, 26, "SOLO", slider20 >= 0.5) ? kr_tog(20);

pk_end();
ui_end();
