desc:Heft [bus comp] (GFX Lab)
//tags: compressor bus vca feedforward feedback stereo-link push preamp saturation tilt
//author: JGWizrad / Garden FX (GFX)
//
// Requires gfx-lab-drift, gfx-lab-input, gfx-lab-output, gfx-lab-ui, gfx-lab-panel, gfx-lab-instrument, gfx-lab-dyn, gfx-lab-os, gfx-lab-bypass (.jsfx-inc) in the same folder.
//
// Heft (Lab) - a Hybrid of the Stable Heft: a stereo VCA bus compressor in the spirit of the famous American stereo
// bus compressor with the stepped controls and the side-chain filter switch (the sound is designed from scratch),
// with TWIN PREAMPS in front of it - the lows and the highs each driven through their own preamp model - and
// Benthick's TILT. The original had neither.
//
// SIGNAL FLOW
//   input > TWIN PREAMPS (oversampled 1x / 2x / 4x: the OS key) > TILT > the compressor > MAKEUP, MIX > output
//   (straight through: a mix tool) > BYPASS.
//   TWIN PREAMPS: the signal splits at SPLIT (110 - 1100 Hz in the house 110 Hz steps, 220 Hz by default; a 24 dB/oct
//     crossover whose two bands add back flat) into LOW and HIGH. Each band has its own preamp: a model menu (IRON,
//     TRANSISTOR, SOLID STATE, VALVE - VAL60's stages), DRIVE, and an LED push button by its name that switches it in (12 ms
//     crossfade). Each model keeps its level (DRIVE adds harmonics, not volume) and has its own gentle voicing:
//       IRON         a transformer core: soft, mostly 3rd harmonic - weight and a firmer bottom (+2.5 dB at 120 Hz)
//       TRANSISTOR   a single-ended discrete stage: lop-sided (2nd harmonic), highs drive it harder - a forward edge
//                    (+2 dB at 1.5 kHz)
//       SOLID STATE  an op-amp stage: clean until near the rails, then a firm, symmetrical knee (+2.5 dB at 7 kHz)
//       VALVE        a biased triode: soft and asymmetric (-2 dB at 660 Hz)
//     A model change ducks that band 5 ms, swaps, and comes back. With both preamps out the unit is the Stable Heft.
//   TILT: a seesaw round 660 Hz (+/- 6 dB: lows down and highs up, or the other way). It sits before the compressor,
//     so it changes what the compressor reacts to as well as what you hear.
//   The compressor:
//   THRESHOLD - where it starts working.
//   ATTACK    - .03 / .1 / .3 / 1 / 3 / 10 / 30 ms.   RATIO - 1.5 / 2 / 3 / 4 / 6 / 10 / INF : 1.
//   RELEASE   - .05 / .1 / .2 / .5 / 1 / 2 s, or VAR (set by the VAR knob, 0.05 - 3 s).
//   KNEE      - HARD / MED (6 dB) / SOFT (12 dB).
//   PUSH      - what the detector hears: NORM (flat), MED (the lows count less), LOUD (the lows count much less
//               and the highs more) - the bass drives the compressor less, so mixes stay big and punchy.
//   TYPE      - NEW: feed-forward (the detector hears the input: precise, firm). OLD: feedback (it hears its own
//               output: softer, more forgiving, the ratio eases off on big peaks - the vintage feel).
//   LINK      - IND (each side on its own) to 100% (both sides share one gain). LINK FILTER shapes what is
//               shared: OFF, HP (the lows are left out of the link) or BP (only the mids are shared).
//   MAKEUP, MIX (parallel compression: the preamps' sound against its compressed copy), IN.
//   METER: the two VUs show gain reduction or output level.
//   BYPASS (the house key): crossfades (15 ms) to the untouched input, delayed by the latency.
// DRIFT (0 - 100 %, 50 % = standard; JG, 28 Oct 2026: "preamp heat and comp timing"):
//   preamp heat, each band's model its own way (only the preamps that are in):
//     valve        warm-up from load (a little quieter and more lop-sided, settling over about a minute), heater
//                  wander of bias (+/- 0.04) and gain (+/- 0.15 dB), HT sag on loud programme (up to 15 % more drive)
//     iron         the core's temperature: its saturation point wanders (+/- 8 %)
//     transistor   bias wanders (+/- 0.05) and creeps up as the stage heats on loud programme (+0.08 over seconds)
//     solid state  the rails sag on peaks: it clips a little earlier (up to 12 %)
//   comp timing: attack and release sit off nominal (up to +/- 5 %, fixed per instance) and wander (+/- 4 %).
//   0 % = none (and no warm-up), 100 % = twice the standard amount, a little faster.
// GUI: the Stable Heft's panel, a PREAMPS row (LOW, SPLIT, HIGH) and a TILT / DRIFT section below it; BYPASS and OS
//   in the header. Model boxes: click for the menu. SPLIT shows its frequency in the light box under it.
// PRESETS: the Stable ten, each now with a touch of preamp (subtle; Bus Limiter stays clean; level-matched to the
//   Stable settings), and three that lean on the preamps: Iron Weight, Drum Bus Grit, Bright Air.
// Latency: 0 / 31 / 37 samples at OS 1x / 2x / 4x (default 1x, as the Stable Heft), reported.

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

slider1:1<0,1,1{Out,In}>-In
slider2:-12<-40,0,0.1>-Threshold (dB)
slider3:3<0,6,1{.03 ms,.1 ms,.3 ms,1 ms,3 ms,10 ms,30 ms}>-Attack
slider4:3<0,6,1{1.5:1,2:1,3:1,4:1,6:1,10:1,inf:1}>-Ratio
slider5:3<0,6,1{.05 s,.1 s,.2 s,.5 s,1 s,2 s,Var}>-Release
slider6:0.3<0.05,3,0.01>-Var release (s)
slider7:1<0,2,1{Hard,Med,Soft}>-Knee
slider8:0<0,2,1{Norm,Med,Loud}>-Push
slider9:0<0,1,1{New (feed-forward),Old (feedback)}>-Type
slider10:100<0,100,1>-Link (%)
slider11:0<0,2,1{Off,HP,BP}>-Link filter
slider12:0<-10,20,0.1>-Makeup (dB)
slider13:100<0,100,1>-Mix (%)
slider14:0<0,1,1{GR,Output}>-Meter
slider15:0<-24,12,0.1>-Input trim (dB)
slider16:1<0,1,1{Off,On}>-Input guard
slider17:lo_on=0<0,1,1{Off,On}>-Low preamp
slider18:lo_md=3<0,3,1{Iron,Transistor,Solid state,Valve}>-Low preamp model
slider19:lo_dr=30<0,100,1>-Low preamp drive (%)
slider20:hi_on=0<0,1,1{Off,On}>-High preamp
slider21:hi_md=2<0,3,1{Iron,Transistor,Solid state,Valve}>-High preamp model
slider22:hi_dr=30<0,100,1>-High preamp drive (%)
slider23:sp_sel=1<0,9,1{110 Hz,220 Hz,330 Hz,440 Hz,550 Hz,660 Hz,770 Hz,880 Hz,990 Hz,1.1 kHz}>-Split
slider24:tilt=0<-6,6,0.1>-Tilt (dB)
slider25:drift=50<0,100,1>-Drift (%)
slider26:os_sel=0<0,2,1{1x,2x,4x}>-Oversampling
slider27:byp=0<0,1,1{Off,On}>-Bypass
slider28:prs_sel=0<0,13,1{No preset,Mix Bus Glue,Big Mix (loud),Drum Bus Punch,Drum Smash (parallel),Vintage Feedback,Vocal Bus,Wide Stereo (independent),Bus Limiter,Bass Control,Room Pump,Iron Weight,Drum Bus Grit,Bright Air}>-Preset

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

@init
#ui_man = "gfx-lab-heft";                                // the ? pop-up links to this manual
ext_noinit = 1;
CR = 16;                                                  // control rate (samples)

function att_ms(i) ( i == 0 ? 0.03 : i == 1 ? 0.1 : i == 2 ? 0.3 : i == 3 ? 1 : i == 4 ? 3 : i == 5 ? 10 : 30 );
function rat_v(i)  ( i == 0 ? 1.5 : i == 1 ? 2 : i == 2 ? 3 : i == 3 ? 4 : i == 4 ? 6 : i == 5 ? 10 : 1000 );
function rel_s(i)  ( i == 0 ? 0.05 : i == 1 ? 0.1 : i == 2 ? 0.2 : i == 3 ? 0.5 : i == 4 ? 1 : i == 5 ? 2 : slider6 );

// one channel's side-chain: PUSH shaping, the link-filter copy, and a fast peak detector
function sc_set(th, lf) instance(t1, t2, lk) (
  th == 0 ? ( t1.bq_set(4, 200, 0.7, 0); t2.bq_set(3, 3000, 0.7, 0); ) :
  th == 1 ? ( t1.bq_set(4, 220, 0.6, -6); t2.bq_set(3, 3000, 0.7, 1.5); ) :
            ( t1.bq_set(4, 260, 0.6, -12); t2.bq_set(3, 2500, 0.7, 4); );
  lf == 1 ? lk.bq_set(1, 250, 0.7, 0) : lf == 2 ? lk.bq_set(5, 1000, 0.6, 0) : lk.bq_set(4, 200, 0.7, 0);
);
function sc_run(x) instance(t1, t2, lk, env, envk) local(s, k) (
  s = t2.bq_run(t1.bq_run(x));
  k = lk.bq_run(s);
  env = max(abs(s), env * det_r); envk = max(abs(k), envk * det_r);
  20 * log10(env + 0.0000001);
);
function sc_lvk() instance(envk) ( 20 * log10(envk + 0.0000001) + (lfil > 0 ? 3 : 0) );
// gain: feed-forward or feedback ratio law, knee, attack / release in dB
function gc(lv) instance(e) local(o, t, f) (
  o = lv - thr; f = dy_curve(o, knee);
  t = typ ? max(-(min(rat, 30) - 1) * f, -40) : -(1 - 1 / rat) * f;
  e += (t - e) * (t < e ? ac : rc);
  e;
);

// ---------- TWIN PREAMPS: VAL60's stages (TWEAK) ----------
K_LO = 0.08;  K_HI = 3.2;     // valve drive at DRIVE 0 % and 100 % (k = K_LO x (K_HI / K_LO) ^ drive)
V_B = 0.20;                   // the valve's bias: its even harmonics
IR_LO = 0.3;  IR_HI = 8;      // iron: core drive at 0 / 100 %
TR_LO = 0.15;  TR_HI = 5;     // transistor: drive at 0 / 100 %
TR_B = 0.35;                  // transistor: its bias (lop-sided: 2nd harmonic)
TR_F = 2500;  TR_E = 1;       // transistor: pre-emphasis above TR_F, +6 dB
SS_LO = 0.2;  SS_HI = 6;      // solid state: drive at 0 / 100 %
EQ_IR_F = 120;   EQ_IR_G = 2.5;                    // each model's voicing: iron pushed lows (low shelf)
EQ_TR_F = 1500;  EQ_TR_G = 2;   EQ_TR_Q = 0.8;     // transistor: a mid lift (bell)
EQ_SS_F = 7000;  EQ_SS_G = 2.5;                    // solid state: pushed highs (high shelf)
EQ_VA_F = 660;   EQ_VA_G = -2;  EQ_VA_Q = 0.7;     // valve: a mid dip (bell)
EQC = 1000;                   // memory: the voicing biquads, 5 each per model (b0 b1 b2 a1 a2 at EQC + 5 x model)

function db2g(d) ( exp(d * 0.11512925464970228); );
function ts(x) ( x = max(-3, min(3, x));  x * (27 + x * x) / (27 + 9 * x * x); );
function dts(b) local(u, v) ( u = 27 + 9 * b * b;  v = 27 + b * b;  ((27 + 3 * b * b) * u - b * v * 18 * b) / (u * u); );
// RBJ biquads at the oversampled rate: kind 0 low shelf, 1 bell, 2 high shelf
function eq_set(t, kind, f, gdb, q, fs) local(ea, w, cw, al, a0, sq, c) (
  ea = 10 ^ (gdb / 40);  w = 2 * $pi * min(f, 0.45 * fs) / fs;  cw = cos(w);  c = EQC + 5 * t;
  kind == 1 ? (
    al = sin(w) / (2 * q);  a0 = 1 + al / ea;
    c[0] = (1 + al * ea) / a0;  c[1] = -2 * cw / a0;  c[2] = (1 - al * ea) / a0;  c[3] = -2 * cw / a0;  c[4] = (1 - al / ea) / a0;
  ) : (
    al = sin(w) / 2 * sqrt(2);  sq = 2 * sqrt(ea) * al;
    kind == 0 ? (
      a0 = (ea + 1) + (ea - 1) * cw + sq;
      c[0] = ea * ((ea + 1) - (ea - 1) * cw + sq) / a0;  c[1] = 2 * ea * ((ea - 1) - (ea + 1) * cw) / a0;
      c[2] = ea * ((ea + 1) - (ea - 1) * cw - sq) / a0;  c[3] = -2 * ((ea - 1) + (ea + 1) * cw) / a0;
      c[4] = ((ea + 1) + (ea - 1) * cw - sq) / a0;
    ) : (
      a0 = (ea + 1) - (ea - 1) * cw + sq;
      c[0] = ea * ((ea + 1) + (ea - 1) * cw + sq) / a0;  c[1] = -2 * ea * ((ea - 1) + (ea + 1) * cw) / a0;
      c[2] = ea * ((ea + 1) + (ea - 1) * cw - sq) / a0;  c[3] = 2 * ((ea - 1) - (ea + 1) * cw) / a0;
      c[4] = ((ea + 1) - (ea - 1) * cw - sq) / a0;
    );
  );
);
// the preamps' rate-dependent coefficients (at the oversampled rate)
function pre_coefs() local(fs, g) (
  fs = srate * os_f;
  dc_r2 = 1 - 2 * $pi * 5 / fs;
  g = tan($pi * min(TR_F, 0.45 * fs) / fs);
  tr_g1 = g / (1 + g);  g /= 1 + TR_E;  tr_g2 = g / (1 + g);
  eq_set(1, 0, EQ_IR_F, EQ_IR_G, 0.7, fs);  eq_set(2, 1, EQ_TR_F, EQ_TR_G, EQ_TR_Q, fs);
  eq_set(3, 2, EQ_SS_F, EQ_SS_G, 0.7, fs);  eq_set(4, 1, EQ_VA_F, EQ_VA_G, EQ_VA_Q, fs);
);
// one preamp: ty = its model (1 iron, 2 transistor, 3 solid state, 4 valve), k = drive, b = bias, sl = the curve's
// slope at the bias (unity small-signal gain)
function stage(x, ty, k, b, sl) instance(dx, dy, z1, z2, e1, e2) local(y, u, h, v, c) (
  ty == 4 ? (
    y = (ts(x * k + b) - ts(b)) / (k * sl);
    dy = y - dx + dc_r2 * dy;  dx = y;  y = dy;
  ) : ty == 1 ? (
    y = ts(x * k) / k;                                             // the core, across the band it is given
  ) : ty == 2 ? (
    v = (x - z1) * tr_g1;  h = x - (v + z1);  z1 += 2 * v;          // pre-emphasis
    u = x + TR_E * h;
    u = (ts(u * k + b) - ts(b)) / (k * sl);
    v = (u - z2) * tr_g2;  h = v + z2;  z2 += 2 * v;               // de-emphasis
    y = h + (u - h) / (1 + TR_E);
    dy = y - dx + dc_r2 * dy;  dx = y;  y = dy;
  ) : (
    u = x * k;  u *= u;
    y = x / sqrt(sqrt(1 + u * u));
  );
  c = EQC + 5 * ty;                                                // the model's voicing
  u = c[0] * y + e1;  e1 = c[1] * y - c[3] * u + e2;  e2 = c[2] * y - c[4] * u;
  u;
);
function stage_clr() instance(dx, dy, z1, z2, e1, e2) ( dx = dy = z1 = z2 = e1 = e2 = 0; );
function sk_of(ty, d) (
  ty == 1 ? IR_LO * (IR_HI / IR_LO) ^ d : ty == 2 ? TR_LO * (TR_HI / TR_LO) ^ d :
  ty == 3 ? SS_LO * (SS_HI / SS_LO) ^ d : K_LO * (K_HI / K_LO) ^ d;
);
// the crossover: a TPT state-variable filter (Butterworth); two in a row = 24 dB/oct, low + high add back flat
function svf(x) instance(i1, i2, lp, hp) local(v1, v2, v3) (
  v3 = x - i2;  v1 = xa1 * i1 + xa2 * v3;  v2 = i2 + xa2 * i1 + xa3 * v3;
  i1 = 2 * v1 - i1;  i2 = 2 * v2 - i2;
  lp = v2;  hp = x - 1.4142135623730951 * v1 - v2;
);
function svf_clr() instance(i1, i2) ( i1 = i2 = 0; );
// one channel of the twin preamps, one oversampled phase -> po
function pre(x) instance(XA, XB, XC, LS, HS) local(lo, hi) (
  XA.svf(x);  XB.svf(XA.lp);  XC.svf(XA.hp);
  lo = XB.lp;  hi = XC.hp;
  ml > 0 ? lo += (LS.stage(lo, rl, kl, bl, ll) * vgl - lo) * ml;
  mh > 0 ? hi += (HS.stage(hi, rh, kh, bh, lh) * vgh - hi) * mh;
  po = x + (lo + hi - x) * mb;
);
function pre_clr() instance(XA, XB, XC, LS, HS) ( XA.svf_clr(); XB.svf_clr(); XC.svf_clr(); LS.stage_clr(); HS.stage_clr(); );

// the DRIFT of one band's preamp (preamp heat): k, b, slope and gain for its model
function band_drift(ty, d, sg, hot, on) instance(DA, DB, k, b, l, g) local(w1, w2) (
  w1 = DA.drift_tick(dsp);  w2 = DB.drift_tick(dsp);
  k = sk_of(ty, d);  b = 0;  g = 1;
  ty == 4 ? (                                                      // valve: warm-up, heater wander, HT sag
    b = V_B + 0.04 * dm_a * w1 + 0.10 * wu;
    g = on ? db2g(0.15 * dm_a * w2 - 1.0 * wu) : 1;
    k *= 1 + 0.15 * dm_a * sg;
  ) : ty == 1 ? k *= 1 + 0.08 * dm_a * w1 :                        // iron: core temperature
  ty == 2 ? b = TR_B + dm_a * (0.05 * w1 + 0.08 * min(1, hot / 0.25)) :   // transistor: bias wander + heat
  k *= 1 + 0.12 * dm_a * sg;                                       // solid state: rail sag
  l = dts(b);
);

function update() (
  on = slider1; thr = slider2;
  drift_macro(drift);
  ac = dy_ms(att_ms(slider3) * age_a); rat = rat_v(slider4); rc = dy_ms(1000 * rel_s(slider5) * age_r);
  knee = slider7 == 0 ? 0 : slider7 == 1 ? 6 : 12;
  typ = slider9; lnk = slider10 / 100; lfil = slider11;
  scL.sc_set(slider8, lfil); scR.sc_set(slider8, lfil);
  mk = 10 ^ (slider12 / 20); mix_t = slider13 / 100;
  in_setup(slider15, slider16);
  ost_setup(0, 0, 120, 6000, -0.3, 0);   // output stage: straight through
  t_lo = lo_on >= 0.5;  t_hi = hi_on >= 0.5;
  ql = (lo_md | 0) + 1;  qh = (hi_md | 0) + 1;
  t_dl = lo_dr / 100;  t_dh = hi_dr / 100;
  t_sp = log(110 * ((sp_sel | 0) + 1));
  tgl_t = 10 ^ (-tilt / 40); tgh_t = 10 ^ (tilt / 40);
  os_tf = os_f2n(os_sel);
  pdc_delay = os_lat(os_f);              // (ost_setup zeroes it)
);

// ---------- presets ----------
// threshold, attack, ratio, release, knee, push, type, link, link filter, makeup, mix
function pp(t, a, r, rl, kn, th, ty, lk, lf, mu, mx) (
  slider1 = 1; slider2 = t; slider3 = a; slider4 = r; slider5 = rl; slider7 = kn; slider8 = th; slider9 = ty;
  slider10 = lk; slider11 = lf; slider12 = mu; slider13 = mx;
  lo_on = 0; lo_md = 3; lo_dr = 30; hi_on = 0; hi_md = 2; hi_dr = 30; sp_sel = 1; tilt = 0;
);
// the preamps: low on, model, drive, high on, model, drive, split step, tilt
function pa(a, b, c, d, e, f, s, t) ( lo_on = a; lo_md = b; lo_dr = c; hi_on = d; hi_md = e; hi_dr = f; sp_sel = s; tilt = t; );
function load_preset(p) local(i) (
  p == 1  ? ( pp(-14, 4, 1, 4, 2, 1, 0, 100, 1, 0.5, 100); pa(1, 0, 25, 1, 3, 15, 1, 0); ) :      // Mix Bus Glue
  p == 2  ? ( pp(-18, 4, 3, 3, 1, 2, 0, 100, 1, 3.8, 100); pa(1, 0, 40, 1, 2, 30, 1, 0.5); ) :      // Big Mix (loud)
  p == 3  ? ( pp(-22, 3, 4, 2, 0, 1, 0, 80, 0, 6.6, 100); pa(1, 1, 35, 1, 2, 25, 2, 0); ) :       // Drum Bus Punch
  p == 4  ? ( pp(-30, 0, 5, 1, 0, 2, 0, 100, 0, 10, 40); pa(1, 3, 60, 1, 1, 50, 2, 0); ) :      // Drum Smash (parallel)
  p == 5  ? ( pp(-16, 4, 2, 6, 2, 0, 1, 100, 0, 1.3, 100); pa(1, 0, 35, 1, 3, 35, 1, -0.5); ) :      // Vintage Feedback
  p == 6  ? ( pp(-20, 2, 3, 3, 1, 0, 0, 100, 0, 4, 100); pa(0, 3, 30, 1, 3, 25, 3, 0.5); ) :      // Vocal Bus
  p == 7  ? ( pp(-18, 5, 1, 4, 2, 1, 1, 0, 0, 2.3, 100); pa(0, 3, 30, 1, 2, 15, 1, 0); ) :        // Wide Stereo (independent)
  p == 8  ? pp(-12, 1, 6, 1, 0, 0, 0, 100, 0, 2, 100) :      // Bus Limiter
  p == 9  ? ( pp(-24, 5, 3, 4, 1, 0, 0, 100, 0, 4.1, 100); pa(1, 0, 35, 0, 2, 30, 1, 0); ) :      // Bass Control
  p == 10 ? ( pp(-26, 3, 4, 2, 1, 2, 1, 50, 2, 5, 60); pa(1, 1, 30, 1, 3, 40, 3, 0); ) :        // Room Pump
  p == 11 ? ( pp(-16, 4, 1, 4, 2, 1, 0, 100, 1, 0.8, 100); pa(1, 0, 55, 1, 3, 30, 1, -1); ) :   // Iron Weight
  p == 12 ? ( pp(-22, 3, 4, 2, 0, 1, 0, 80, 0, 6.5, 100); pa(1, 1, 50, 1, 2, 45, 2, 0.5); ) :   // Drum Bus Grit
  p == 13 ? ( pp(-15, 4, 1, 4, 2, 1, 0, 100, 1, 4.3, 100); pa(1, 3, 30, 1, 2, 40, 3, 2); );      // Bright Air
  p == 14 ? (                                                // Reset all to defaults
    pp(-12, 3, 3, 3, 1, 0, 0, 100, 0, 0, 100); slider6 = 0.3; slider14 = 0;
    slider15 = 0; slider16 = 1; drift = 50; os_sel = 0;
  );
  i = 1; loop(26, slider_automate(2 ^ (i - 1)); i += 1; );   // sliders 1 - 26 (not BYPASS)
);

in_init();
ost_init(20000); ostL.ost_ch_init(); ostR.ost_ch_init();
det_r = exp(-1 / (0.008 * srate));                  // peak detector decay (8 ms)
sm = 1 - exp(-1 / (0.03 * srate));
sm_c = 1 - exp(-CR / (0.03 * srate));               // control-rate glides (30 ms)
sw_c = 1 - exp(-1 / (0.012 * srate));               // preamp IN crossfades (12 ms)
dk_c = 1 / (0.005 * srate);                         // a model change: 5 ms out, swap, 5 ms back
sag_c = 1 - exp(-1 / (0.5 * srate));
heat_c = 1 - exp(-1 / (4 * srate));                 // the transistor heats over seconds
tl_c = 1 - exp(-2 * $pi * 660 / srate);             // TILT pivot (the house 110 Hz series)
vuc = 1 - exp(-1 / (0.3 * srate));
vu_sine = 10 * log10(2);   // +3.01 dB: RMS -> sine peak, so a sine peaking at the ref level reads 0 VU
!ui_dflt ? ui_thm = 5; !ui_dflt ? ui_hw = 1;                              // fixed faceplate
!ui_dflt ? ui_sz = 0;
PNL = 0x1D1E21; INKW = 0xF2F2EE; YEL = 0xF2C21B; BLU = 0x2F7FE0; LGRY = 0xD2D4D8; DGRY = 0x6C6F76;   // knob caps
ORA = 0xE8843A;                                                                                     // PREAMPS
os_design();
os_f = os_tf = os_f2n(os_sel);  os_base = 0;
os_gsetup();
// DRIFT sources
LD.DA.drift_init(0.8 + rand(1) * 0.4);  LD.DB.drift_init(0.7 + rand(1) * 0.4);
HD.DA.drift_init(0.8 + rand(1) * 0.4);  HD.DB.drift_init(0.7 + rand(1) * 0.4);
CPA.drift_init(0.5 + rand(1) * 0.4);  CPB.drift_init(0.5 + rand(1) * 0.4);
tol_a = rand(2) - 1;  tol_r = rand(2) - 1;
warm_t = 0;  age_a = age_r = 1;
pre_coefs();
update();
mix_s = mix_t;
rl = ql;  rh = qh;  dkl = dkh = 1;
m_lo = t_lo;  m_hi = t_hi;  m_b = max(t_lo, t_hi);
s_dl = t_dl;  s_dh = t_dh;  s_sp = t_sp;  tgl_s = tgl_t; tgh_s = tgh_t;
sag = heat = 0;  cr_n = 0;  vgl = vgh = 1;
by_setup(27);
ui_dflt = 1;   // theme defaults above apply once: @init re-runs (playback start, device reset) must not undo the saved theme

// 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_sl() (
update();
);
function prs_chk() local(k) (
  prs_dirty > 0 ? ( prs_pk = prs_dirty; prs_dirty = 0; prs_q = 1; ) : prs_ok ? (
    prs_sel = max(0, min(13, prs_sel | 0));
    prs_sel != prs_last ? ( prs_last = prs_sel; prs_sel > 0 ? ( prs_pk = prs_sel; prs_q = 0; ); );
  ) : ( prs_last = prs_sel | 0; prs_ok = 1; );
  // PRESET parameter: the swap ducks 5 ms, loads at silence (at the top of a block) and comes back over 30 ms
  prs_pk > 0 ? (
    prs_g <= 0 ? (
      k = prs_pk; prs_pk = 0; load_preset(k); prs_sl();
      prs_q ? ( k <= 13 ? ( prs_sel = k; slider_automate(prs_sel); prs_cur_d = k; ); prs_last = prs_sel; ) : prs_cur_d = k;
      prs_gs = 1 / (0.030 * srate);
    ) : prs_gs = -1 / (0.005 * srate);
  );
);
prs_ok = 0; prs_dirty = 0; prs_pk = 0; prs_g = 1; prs_gs = 0;

@slider
prs_chk();
prs_sl();

@block
prs_chk();
bdt = samplesblock / srate;
warm_t += bdt;
dsp = 0.03 * dm_s * CR / srate;                     // (band_drift ticks once per control step)
// DRIFT: comp timing (tolerance + wander), applied in update()
age_a = 1 + dm_a * (0.05 * tol_a + 0.04 * CPA.drift_tick(0.02 * dm_s * bdt));
age_r = 1 + dm_a * (0.05 * tol_r + 0.04 * CPB.drift_tick(0.02 * dm_s * bdt));
update();
// DRIFT: preamp heat (valve warm-up only while a valve preamp is in)
has_v = (t_lo && ql == 4) || (t_hi && qh == 4);
wu = has_v ? exp(-warm_t / 22) * min(dm_a, 1) : 0;
sag_amt = min(1, sag / 0.3);
pdc_bot_ch = 0; pdc_top_ch = 2;

@serialize
file_var(0, ui_sz);
file_avail(0) != 0 ? file_var(0, pr_cur);    // the preset in use (older projects end before it)
file_avail(0) != 0 ? file_var(0, prs_last) : prs_last = prs_sel | 0; prs_ok = 1;   // PRESET parameter: no reload on recall

@sample
by_in();
in_process(spl0, spl1);
// ---------- control rate: the preamps' drive, models, drift; the split ----------
cr_n <= 0 ? (
  cr_n = CR;
  s_dl += (t_dl - s_dl) * sm_c;  s_dh += (t_dh - s_dh) * sm_c;  s_sp += (t_sp - s_sp) * sm_c;
  LD.band_drift(rl, s_dl, sag_amt, heat, t_lo);  HD.band_drift(rh, s_dh, sag_amt, heat, t_hi);
  kl = LD.k;  bl = LD.b;  ll = LD.l;  vgl = LD.g;
  kh = HD.k;  bh = HD.b;  lh = HD.l;  vgh = HD.g;
  xg = tan($pi * min(exp(s_sp), 0.45 * srate * os_f) / (srate * os_f));
  xa1 = 1 / (1 + xg * (xg + 1.4142135623730951));  xa2 = xg * xa1;  xa3 = xg * xa2;
);
cr_n -= 1;
// a model change: that band out (5 ms), swap at the dry point (the new model starts from rest), back in
ql != rl ? ( dkl = max(dkl - dk_c, 0); dkl == 0 ? ( PL.LS.stage_clr(); PR.LS.stage_clr(); rl = ql; ); ) : dkl < 1 ? dkl = min(dkl + dk_c, 1);
qh != rh ? ( dkh = max(dkh - dk_c, 0); dkh == 0 ? ( PL.HS.stage_clr(); PR.HS.stage_clr(); rh = qh; ); ) : dkh < 1 ? dkh = min(dkh + dk_c, 1);
m_lo += (t_lo - m_lo) * sw_c;  m_hi += (t_hi - m_hi) * sw_c;  m_b += (max(t_lo, t_hi) - m_b) * sw_c;
m_lo < 0.00001 ? m_lo = 0;  m_hi < 0.00001 ? m_hi = 0;  m_b < 0.00001 ? m_b = 0;
ml = m_lo * dkl * dkl * (3 - 2 * dkl);  mh = m_hi * dkh * dkh * (3 - 2 * dkh);  mb = m_b;

// ---------- TWIN PREAMPS, oversampled (the dry path runs through the same filters) ----------
os_gate();
os_chg ? ( pre_coefs();  PL.pre_clr();  PR.pre_clr();  cr_n = 0; );
UPL.os_up(in_l);  UPR.os_up(in_r);
PL.pre(UPL.s0);  a0 = po;  PR.pre(UPR.s0);  c0 = po;
os_f == 1 ? ( dl = a0;  dr = c0; ) : (
  PL.pre(UPL.s1);  a1 = po;  PR.pre(UPR.s1);  c1 = po;
  os_f == 2 ? ( dl = DNL.os_dn(a0, a1, 0, 0);  dr = DNR.os_dn(c0, c1, 0, 0); ) : (
    PL.pre(UPL.s2);  a2 = po;  PR.pre(UPR.s2);  c2 = po;
    PL.pre(UPL.s3);  a3 = po;  PR.pre(UPR.s3);  c3 = po;
    dl = DNL.os_dn(a0, a1, a2, a3);  dr = DNR.os_dn(c0, c1, c2, c3);
  );
);
os_gs = os_g * os_g * (3 - 2 * os_g);                                         // the OS swap's dip
dl *= os_gs;  dr *= os_gs;
heat += (max(abs(dl), abs(dr)) - heat) * heat_c;
sag += (max(abs(dl), abs(dr)) - sag) * sag_c;

// ---------- TILT: a seesaw round 660 Hz ----------
tgl_s += (tgl_t - tgl_s) * sm; tgh_s += (tgh_t - tgh_s) * sm;
tlL += (dl - tlL) * tl_c; tlR += (dr - tlR) * tl_c;
dl = tlL * tgl_s + (dl - tlL) * tgh_s;
dr = tlR * tgl_s + (dr - tlR) * tgh_s;

// ---------- the compressor ----------
// detector input: the input (NEW) or the last output (OLD, feedback)
vL = scL.sc_run(typ ? yl : dl); vR = scR.sc_run(typ ? yr : dr);
sh = max(scL.sc_lvk(), scR.sc_lvk());
lfil == 0 ? sh = max(vL, vR);
vL += (sh - vL) * lnk; vR += (sh - vR) * lnk;
on ? (
  gL.gc(vL); gR.gc(vR);
  yl = dl * 10 ^ (gL.e / 20); yr = dr * 10 ^ (gR.e / 20);
  wl = yl * mk; wr = yr * mk;
) : ( gL.e = gR.e = 0; yl = dl; yr = dr; wl = dl; wr = dr; );
grL_d = min(grL_d, gL.e); grR_d = min(grR_d, gR.e);
mix_s += (mix_t - mix_s) * sm;
oL = dl + (wl - dl) * mix_s; oR = dr + (wr - dr) * mix_s;
oL = ostL.ost_band(oL * in_comp); oR = ostR.ost_band(oR * in_comp);
ost_out(oL, oR);
spl0 = ost_l; spl1 = ost_r;
vmL += (spl0 * spl0 - vmL) * vuc; vmR += (spl1 * spl1 - vmR) * vuc;
prs_gs != 0 ? ( prs_g += prs_gs; prs_g <= 0 ? ( prs_g = 0; prs_gs = 0; ) : prs_g >= 1 ? ( prs_g = 1; prs_gs = 0; ); );
prs_g < 1 ? ( prs_w = prs_g * prs_g * (3 - 2 * prs_g); spl0 *= prs_w; spl1 *= prs_w; );   // PRESET parameter duck: the swap ducks 5 ms, loads at silence (at the top of a block) and comes back over 30 ms
by_out();

@gfx 900 508
ui_begin(900, 508);
dy_ink = INKW; dy_bg = PNL; dy_kst = 7;            // coloured caps: light grey, dark grey, yellow, blue
dy_ntick = 5;                                      // five marks round the plain dials (JG, 29 Oct 2026: fewer markers)

function sect(x, w, t, col) (
  ui_col(col); gfx_rect(x * sc, 74 * sc, w * sc, 3 * sc);
  gfx_setfont(6, "Trebuchet MS", 13 * sc, 'b'); ui_col(col); gfx_x = (x + (x > 100 ? 6 : 0)) * sc; gfx_y = 82 * sc; gfx_drawstr(t);
);
// a section in the lower row (PREAMPS, TILT)
function sect2(x, w, t, col) (
  ui_col(col); gfx_rect(x * sc, 334 * sc, w * sc, 3 * sc);
  gfx_setfont(6, "Trebuchet MS", 13 * sc, 'b'); ui_col(col); gfx_x = (x + (x > 100 ? 6 : 0)) * sc; gfx_y = 342 * sc; gfx_drawstr(t);
);
// a preamp's LED push button (amber LED lit = in) with its name beside it
function band_btn(x, y, idx, t) local(on, tw, th) (
  on = slider(idx) >= 0.5;
  pk_ledbtn(x * sc, y * sc, 40 * sc, 26 * sc, on, 0xF2A41E) ? ( slider(idx) = on ? 0 : 1; ui_auto(idx); on = !on; );
  gfx_setfont(6, "Trebuchet MS", 15 * sc, 'b'); gfx_measurestr(t, tw, th);
  ui_col(on ? INKW : DGRY + 0x303030); gfx_x = (x + 50) * sc; gfx_y = (y + 13) * sc - th * 0.5; gfx_drawstr(t);
);
// a model box: a dark recessed box with the model in light capitals; a click opens the menu (the current one ticked)
function model_box(x, y, w, idx, on) local(v, hov, hit, s) (
  v = slider(idx) | 0;
  hov = mouse_x >= x * sc && mouse_x < (x + w) * sc && mouse_y >= y * sc && mouse_y < (y + 30) * sc;
  hit = hov && (mouse_cap & 1) && !(hf_mcap & 1);
  gfx_setfont(4); ui_col(DGRY + 0x303030); ui_text((x + w * 0.5) * sc, (y - 10) * sc, "MODEL");
  ui_col(0x000000); gfx_rect(x * sc, y * sc, w * sc, 30 * sc);
  ui_col(hov ? 0x34302B : 0x2A2622); gfx_rect((x + 1) * sc, (y + 1) * sc, (w - 2) * sc, 28 * sc);
  ui_cola(0x000000, 0.35); gfx_rect((x + 1) * sc, (y + 1) * sc, (w - 2) * sc, 2 * sc); gfx_rect((x + 1) * sc, (y + 1) * sc, 2 * sc, 28 * sc);
  gfx_setfont(6, "Trebuchet MS", 14 * sc, 'b'); ui_col(on ? 0xF2E6CC : 0x8A8478);
  ui_text((x + w * 0.5) * sc, (y + 15) * sc - 7 * sc, v == 0 ? "IRON" : v == 1 ? "TRANSISTOR" : v == 2 ? "SOLID STATE" : "VALVE");
  hit ? (
    gfx_x = x * sc; gfx_y = (y + 30) * sc;
    s = gfx_showmenu(v == 0 ? "!Iron|Transistor|Solid state|Valve" : v == 1 ? "Iron|!Transistor|Solid state|Valve" :
                     v == 2 ? "Iron|Transistor|!Solid state|Valve" : "Iron|Transistor|Solid state|!Valve");
    s > 0 ? ( slider(idx) = s - 1; ui_auto(idx); );
  );
);
// greys a switched-off preamp (face colour at about 50 % over it)
function grey(x, y, w, h) ( ui_cola(PNL, 0.5); gfx_rect(x * sc, y * sc, w * sc, h * sc); );

// ---------- panel ----------
fh = 500;
ui_col(0x050506); gfx_rect(0, 0, gfx_w, gfx_h);
pk_chassis(8 * sc, 4 * sc, 884 * sc, fh * sc, PNL);
ui_col(INKW); gfx_circle(40 * sc, 34 * sc, 14 * sc, 1, 1);
gfx_setfont(4); ui_col(PNL); ui_text(40 * sc, 29 * sc, "GFX");
gfx_setfont(6, "Trebuchet MS", 26 * sc, 'b'); ui_col(INKW); gfx_x = 62 * sc; gfx_y = 18 * sc; gfx_drawstr("HEFT");
gfx_setfont(4); ui_col(YEL); gfx_x = 150 * sc; gfx_y = 30 * sc; gfx_drawstr("STEREO BUS COMPRESSOR");
by_key(330, 18, 27, 16);
os_key(458, 21, 26, INKW, 17);
pk_pextra = "Reset all to defaults";   // the house preset display: PRESETS, prev / next, LCD (click it for the list)
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(500 * sc, 14 * sc, 316 * sc, 36 * sc, pr_cur, "Mix Bus Glue|Big Mix (loud)|Drum Bus Punch|Drum Smash (parallel)|Vintage Feedback|Vocal Bus|Wide Stereo (independent)|Bus Limiter|Bass Control|Room Pump|Iron Weight|Drum Bus Grit|Bright Air");
k_ > 0 ? ( prs_dirty = k_; pr_cur = k_ > 13 ? 0 : k_; );
ui_size_picker(828 * sc, 22 * sc);

ui_col(YEL); gfx_rect(14 * sc, 60 * sc, 520 * sc, 4 * sc);
ui_col(BLU); gfx_rect(534 * sc, 60 * sc, 352 * sc, 4 * sc);

// ---------- COMPRESSOR ----------
sect(24, 510, "COMPRESSOR", YEL);
sprintf(#v, "%.1f dB", slider2); dy_knob(74, 150, 26, 2, -40, 0, -12, 0, YEL, "THRESHOLD", #v);
dy_step = 7; dy_steps = ".03|.1|.3|1|3|10|30"; sprintf(#v, "%g ms", att_ms(slider3));
dy_knob(184, 150, 26, 3, 0, 6, 3, 0, LGRY, "ATTACK", #v);
dy_step = 7; dy_steps = "1.5|2|3|4|6|10|INF"; slider4 == 6 ? strcpy(#v, "INF : 1") : sprintf(#v, "%g : 1", rat_v(slider4));
dy_knob(294, 150, 26, 4, 0, 6, 3, 0, LGRY, "RATIO", #v);
dy_step = 7; dy_steps = ".05|.1|.2|.5|1|2|VAR"; slider5 == 6 ? sprintf(#v, "VAR %.2f s", slider6) : sprintf(#v, "%g s", rel_s(slider5));
dy_knob(404, 150, 26, 5, 0, 6, 3, 0, LGRY, "RELEASE", #v);
sprintf(#v, "%.2f s", slider6); dy_knob(478, 140, 14, 6, 0.05, 3, 0.3, 1, slider5 == 6 ? YEL : DGRY, "VAR", #v);
dy_step = 3; dy_steps = "HARD|MED|SOFT"; dy_knob(74, 266, 20, 7, 0, 2, 1, 0, DGRY, "KNEE", "");
dy_step = 3; dy_steps = "NORM|MED|LOUD"; dy_knob(184, 266, 20, 8, 0, 2, 0, 0, YEL, "PUSH", "");
dy_btn(252, 244, slider9 == 0, BLU, "NEW") ? ( slider9 = 0; ui_auto(9); );
dy_btn(300, 244, slider9 == 1, YEL, "OLD") ? ( slider9 = 1; ui_auto(9); );
dy_lbl(296, 290, slider9 ? "FEEDBACK" : "FEED-FORWARD", 0.55);
sprintf(#v, "%+.1f dB", slider12); dy_knob(404, 266, 20, 12, -10, 20, 0, 0, BLU, "MAKEUP", #v);
pk_ledbtn(462 * sc, 246 * sc, 40 * sc, 26 * sc, slider1, dy_grn) ? ( slider1 = 1 - slider1; ui_auto(1); );
dy_lbl(482, 276, "IN", 0.9);

// ---------- STEREO ----------
sect(534, 346, "STEREO", BLU);
grdL = min(grL_d, grdL * 0.85 + 0); grdR = min(grR_d, grdR * 0.85 + 0); grL_d = 0; grR_d = 0;
slider14 ? ( vaL = 10 * log10(vmL + 0.0000000001) + 18 + vu_sine; vaR = 10 * log10(vmR + 0.0000000001) + 18 + vu_sine; )
         : ( vaL = grdL; vaR = grdR; );
ui_vu_dim = byp >= 0.5;
vuL.ui_vu(546 * sc, 100 * sc, 162 * sc, 98 * sc, vaL, slider14 ? "OUT L" : "GR L");
vuR.ui_vu(716 * sc, 100 * sc, 162 * sc, 98 * sc, vaR, slider14 ? "OUT R" : "GR R");
slider10 < 0.5 ? strcpy(#w, "IND") : sprintf(#w, "%.0f", slider10);
dy_knob(580, 266, 20, 10, 0, 100, 100, 0, BLU, "LINK", #w);
dy_step = 3; dy_steps = "OFF|HP|BP"; dy_knob(680, 266, 20, 11, 0, 2, 0, 0, DGRY, "LINK FILTER", "");
dy_btn(742, 238, slider14, YEL, slider14 ? "OUT" : "GR") ? ( slider14 = 1 - slider14; ui_auto(14); );
dy_lbl(762, 282, "METER", 0.55);
sprintf(#v, "WET %.0f", slider13); dy_knob(838, 266, 20, 13, 0, 100, 100, 0, LGRY, "MIX", #v);

// ---------- PREAMPS: each band's lamp and DRIVE side by side, its model box centred under them ----------
sect2(24, 510, "PREAMPS", ORA);
band_btn(66, 379, 17, "LOW");
sprintf(#v, "%.0f %%", lo_dr); dy_knob(204, 392, 20, 19, 0, 100, 30, 0, ORA, "DRIVE", #v);
model_box(84, 452, 132, 18, lo_on >= 0.5);
lo_on < 0.5 ? ( grey(176, 364, 56, 70); grey(82, 438, 136, 46); );
band_btn(308, 379, 20, "HIGH");
sprintf(#v, "%.0f %%", hi_dr); dy_knob(454, 392, 20, 22, 0, 100, 30, 0, ORA, "DRIVE", #v);
model_box(330, 452, 132, 21, hi_on >= 0.5);
hi_on < 0.5 ? ( grey(426, 364, 56, 70); grey(328, 438, 136, 46); );
// the line between PREAMPS and TILT
ui_col(0x000000); gfx_rect(533 * sc, 352 * sc, 1 * sc, 140 * sc);
ui_col(0x3A3B42); gfx_rect(534 * sc, 352 * sc, 2 * sc, 140 * sc);

// ---------- TILT: SPLIT, TILT, DRIFT ----------
sect2(534, 346, "TILT", LGRY);
sp_f = 110 * ((sp_sel | 0) + 1);
sp_f >= 1000 ? sprintf(#w, "%.1f kHz", sp_f / 1000) : sprintf(#w, "%d Hz", sp_f);
dy_ntick = 4; dy_knob(600, 404, 20, 23, 0, 9, 1, 0, LGRY, "SPLIT", "");      // ten steps (110 Hz - 1.1 kHz), marks every third
slider(23) != floor(slider(23) + 0.5) ? ( slider(23) = floor(slider(23) + 0.5); ui_auto(23); );
ui_col(0x000000); gfx_rect(566 * sc, 446 * sc, 68 * sc, 22 * sc);
ui_col(0xE6DFC8); gfx_rect(567 * sc, 447 * sc, 66 * sc, 20 * sc);
ui_cola(0x000000, 0.18); gfx_rect(567 * sc, 447 * sc, 66 * sc, 2 * sc); gfx_rect(567 * sc, 447 * sc, 2 * sc, 20 * sc);
gfx_setfont(6, "Trebuchet MS", 13 * sc, 'b'); ui_col(0x1A1A1A); ui_text(600 * sc, 450 * sc, #w);
dy_ntick = 5; sprintf(#v, "%+.1f dB", tilt); dy_knob(716, 410, 28, 24, -6, 6, 0, 0, LGRY, "TILT", #v);
gfx_setfont(4); ui_col(DGRY + 0x404040); ui_text(676 * sc, 446 * sc, "DARK"); ui_text(756 * sc, 446 * sc, "BRIGHT");
sprintf(#v, "%.0f %%", drift); dy_knob(834, 410, 16, 25, 0, 100, 50, 0, LGRY, "DRIFT", #v);

hf_mcap = mouse_cap;
pk_end();
ui_end();
