desc:RMS 201 [mastering limiter] (GFX Lab)
//tags: mastering compressor limiter rms true peak saturation tape tube diode tilt
//author: JGWizrad / Garden FX (GFX)
//
// Requires gfx-lab-input, gfx-lab-ui, gfx-lab-panel, gfx-lab-os and gfx-lab-bypass (.jsfx-inc) in the same folder.
//
// RMS 201. A mastering compressor and limiter in the spirit of a classic 1970s broadcast / mastering peak limiter
// with a feed-forward RMS detector (fast but transparent, a musical knee), grown into a modern master processor:
// spectral TILT COMP, an adaptive "ghost" AUTO release, a post-limiter COLOUR stage and a true-peak ceiling.
//
// SIGNAL FLOW
//   input -> TRIM (+ optional GUARD) -> COMPRESSOR (RMS detector through the SC HPF and M/S weighting; TILT COMP;
//   LINK; MIX) -> LIMITER (1.5 ms look-ahead, true peak) -> [oversampled: COLOUR] -> DC blocker -> CEILING (0.5 ms
//   look-ahead, true peak: catches what COLOUR adds) -> out.
// DETECTOR: an RMS level (scaled so a steady sine compresses at the same THRESHOLD as a peak detector would).
//   RMS TIME: AUTO couples the window to ATTACK (2 x ATTACK, 3 - 40 ms); or 3 / 10 / 30 / 50 ms.
//   SC HPF: 20 - 300 Hz, 12 dB/oct, glided - the detector stops hearing the kick and bass, so they don't pump the mix.
//   SC M/S: the detector's balance of Mid and Side (-100 = Mid only, 0 = L / R as they are, +100 = Side only).
//   LINK: on = one gain for both sides, off = each side its own detector (dual mono; glided 10 ms).
// COMPRESSOR: THRESHOLD -40 .. 0 dBFS, RATIO 1 .. 10 : 1, KNEE 0 - 18 dB (soft), ATTACK 0.1 - 100 ms, RELEASE
//   20 ms - 2.5 s, MAKE-UP 0 - 20 dB, MIX 0 - 100 % (parallel: the uncompressed signal under the compressed one).
//   TILT COMP (0 - 100 %): the compressor splits at 800 Hz and leans its ratio towards whichever end of the spectrum
//   is carrying the energy - when the lows are driving the compression the highs get less of it (up to half as much
//   at 100 %), so the top stays open instead of being ducked by the bass. 0 % = one flat gain (nulls with the plain
//   compressor).
//   AUTO ("ghost" release): RELEASE becomes programme dependent. A fast release that follows the transient density
//   (sparse hits recover quickly, dense busy material recovers more slowly so it doesn't flutter), under a slow
//   release that only builds up under long, sustained compression (no breathing), and in the gaps (the level well
//   under THRESHOLD) both recover three times faster, so each phrase starts fresh. RELEASE scales it all.
// LIMITER: CEILING -12 .. 0 dBFS, RELEASE 5 ms - 1 s (AUTO: slows the release while the limiting is sustained),
//   TRUE PEAK on = an 8x interpolated detector (neither the samples nor the peaks between them pass the ceiling),
//   off = sample peaks. Both look ahead 1.5 ms, so changing TRUE PEAK doesn't change the latency.
// COLOUR (after the limiter, off by default): TAPE (soft, symmetrical: mostly 3rd harmonic), TUBE (asymmetrical:
//   mostly 2nd) or DIODE (a harder, solid-state knee: brighter odd harmonics). DRIVE pushes it (the curve is set
//   against the CEILING, so quiet signal passes untouched), BLEND mixes it with the clean signal. Types crossfade.
//   The CEILING stage after it (with the LIMITER in) keeps the colour's extra peaks under the ceiling.
// OS (1x / 2x / 4x, default 2x): COLOUR runs at the chosen rate (gfx-lab-os linear-phase half-bands). Changing it
//   dips the sound for 5 ms and swaps at the silent point.
// BYPASS (top bar): crossfades to the dry input, kept in time with the latency.
// Every key crossfades (15 ms), every knob glides (30 ms).
// No DRIFT and no mains / warm-up (mastering unit).
// METERS: IN and OUT = an LED row each for L and R (-42 .. +3 dBFS, 1.2 s floating peak hold; click to clear); GR =
//   the compressor (red) and the limiter + ceiling (amber), 0 - 20 dB; TILT = where the compressor is leaning
//   (lows / highs); the TRANSFER window draws the compressor's curve (THRESHOLD, RATIO, KNEE, MAKE-UP) and the
//   CEILING, with a dot riding it at the detector's level.
// GUI: drag a knob (SHIFT = fine), wheel, double-click = default; values show while a knob is touched. RMS TIME and
//   TYPE: click the box for the list (TYPE's < > keys step through it). Keys light when on. S / M / L / ? = size and help.
// PRESETS: Init, Gentle Glue, Transparent Master, Loud and Open, Punchy Pop, Dense Rock, Acoustic Air, Low End Hold,
//   Tape Master, Tube Glow, Diode Edge, Broadcast Ride (they leave TRIM, GUARD and OS as they are).
// Latency: 1.5 ms + 0.5 ms + 14 samples (110 at 48 kHz) at 1x; OS adds 31 (2x) / 37 (4x); reported. On a
//   multichannel track the channels above 1 / 2 are delayed by the same amount (they are not processed).

slider1:thr_db=-18<-40,0,0.1>-Threshold (dB)
slider2:ratio=2<1,10,0.01>-Ratio
slider3:knee_db=6<0,18,0.1>-Knee (dB)
slider4:att_ms=10<0.1,100,0.1>-Attack (ms)
slider5:rel_ms=250<20,2500,1>-Release (ms)
slider6:rel_auto=0<0,1,1{Off,On}>-Auto release
slider7:mkup_db=0<0,20,0.1>-Make-up (dB)
slider8:mix_pc=100<0,100,1>-Mix (%)
slider9:comp_in=1<0,1,1{Out,In}>-Compressor
slider10:rms_t=0<0,4,1{Auto,3 ms,10 ms,30 ms,50 ms}>-RMS time
slider11:sc_hz=60<20,300,1>-SC HPF (Hz)
slider12:msw=0<-100,100,1>-SC M/S weighting (%)
slider13:tilt=0<0,100,1>-Tilt comp (%)
slider14:lnk=1<0,1,1{Off,On}>-Link
slider15:lim_ceil=-1<-12,0,0.1>-Ceiling (dB)
slider16:lim_rel=60<5,1000,1>-Limiter release (ms)
slider17:lim_auto=0<0,1,1{Off,On}>-Limiter auto release
slider18:tp_on=1<0,1,1{Off,On}>-True peak
slider19:lim_in=1<0,1,1{Out,In}>-Limiter
slider20:col_type=0<0,2,1{Tape,Tube,Diode}>-Colour type
slider21:col_drv=30<0,100,1>-Colour drive (%)
slider22:col_mix=50<0,100,1>-Colour blend (%)
slider23:col_in=0<0,1,1{Out,In}>-Colour
slider24:os_sl=1<0,2,1{1x,2x,4x}>-Oversampling
slider25:trim_db=0<-24,12,0.1>-Input trim (dB)
slider26:guard=0<0,1,1{Off,On}>-Guard
slider27:byp=0<0,1,1{Off,On}>-Bypass
slider28:prs_sel=0<0,12,1{No preset,Init,Gentle Glue,Transparent Master,Loud and Open,Punchy Pop,Dense Rock,Acoustic Air,Low End Hold,Tape Master,Tube Glow,Diode Edge,Broadcast Ride}>-Preset

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

import gfx-lab-input.jsfx-inc
import gfx-lab-ui.jsfx-inc
import gfx-lab-panel.jsfx-inc
import gfx-lab-os.jsfx-inc
import gfx-lab-bypass.jsfx-inc

@init
#ui_man = "gfx-lab-rms-201";
ext_noinit = 1;

LIMA = 100000;                                // the limiter's buffers (6000: see la_init)
LIMB = 106000;                                // the ceiling's buffers
TPK = 112000;                                 // true-peak interpolator taps: 7 phases x 16
DRY = 113000;                                 // dry input delayed by the latency, L / R (1024 each): the resume fade
XCH = 116000;                                 // channels 3 - 64 (1024 each): delayed by the latency too
RMASK = 1023;

function db2g(d) ( exp(d * 0.11512925464970228); );
function g2db(g) ( log(max(g, 0.000000001)) * 8.685889638065035; );
function coef(t) ( 1 - exp(-1 / (max(t, 0.00001) * srate)); );

// TPT state-variable high-pass (12 dB/oct, Q 0.707) on the glided cutoff word sc_gw (the SC HPF)
function svf_hp(x) instance(s1, s2) local(hp, v1, v2, bp) (
  hp = (x - (sc_gw + 1.4142135623730951) * s1 - s2) * sc_h;
  v1 = sc_gw * hp; bp = v1 + s1; s1 = bp + v1;
  v2 = sc_gw * bp; s2 = s2 + 2 * v2;
  hp;
);
// TPT state-variable low-pass (12 dB/oct, Q 0.707) at 800 Hz: the TILT split (high = x - low, so it sums flat)
function svf_lp(x) instance(s1, s2) local(hp, v1, v2, bp, lp) (
  hp = (x - (tl_g + 1.4142135623730951) * s1 - s2) * tl_h;
  v1 = tl_g * hp; bp = v1 + s1; s1 = bp + v1;
  v2 = tl_g * bp; lp = s2 + v2; s2 = lp + v2;
  lp;
);
// TPT one-pole low-pass (the TILT balance detector)
function op_lp(x, gg) instance(s) local(v, y) ( v = (x - s) * gg; y = v + s; s = y + v; y; );

// static curve: dB over threshold -> dB of reduction (slope, knee width)
function gcurve(o, sl, kw) (
  kw < 0.01 ? (o > 0 ? o * sl : 0) :
  o <= -0.5 * kw ? 0 : o >= 0.5 * kw ? o * sl : sl * sqr(o + 0.5 * kw) / (2 * kw);
);

// ---------- true peak ----------
// 16-tap Kaiser-windowed sinc (beta 2.5) at the seven 8x points between taps 7 and 8: flat (or reading a little high,
// the safe side) up to 0.45 x the rate, so bright material (cymbals, noise up to 20 kHz) can't sneak past the ceiling
function tp_design() local(p, k, t, w, x, s, i0) (
  p = 0; i0 = os_bi0(2.5);
  loop(7,
    s = 0; k = 0;
    loop(16,
      t = (k - 7) - (p + 1) * 0.125;
      x = t / 8.5; w = abs(x) < 1 ? os_bi0(2.5 * sqrt(1 - x * x)) / i0 : 0;
      TPK[p * 16 + k] = (abs(t) < 0.000001 ? 1 : sin($pi * t) / ($pi * t)) * w;
      s += TPK[p * 16 + k]; k += 1;
    );
    k = 0; loop(16, TPK[p * 16 + k] /= s; k += 1; );
    p += 1;
  );
);
// one new sample into the history at hb (32 slots, kept twice so the 16 taps read straight through), write count w
// -> the largest of samples n-8, n-7 and (tp on) the seven 8x points between them, the loudest refined by a parabola
function tp_peak(hb, w, x, tp) local(h, b, p, m, y, a, c, e, d, q, pv) (
  h = hb + (w & 15);
  h[0] = x; h[16] = x;
  h = hb + ((w + 1) & 15);                    // oldest of the 16
  !tp ? max(abs(h[7]), abs(h[8])) : (
    pv = h[7]; m = abs(pv); a = c = e = 0; p = 0;
    loop(8,
      p < 7 ? (
        b = TPK + p * 16;
        y = b[0] * h[0] + b[1] * h[1] + b[2] * h[2] + b[3] * h[3] + b[4] * h[4] + b[5] * h[5] + b[6] * h[6] + b[7] * h[7]
          + b[8] * h[8] + b[9] * h[9] + b[10] * h[10] + b[11] * h[11] + b[12] * h[12] + b[13] * h[13] + b[14] * h[14]
          + b[15] * h[15];
      ) : y = h[8];
      abs(y) > m ? ( m = abs(y); a = pv; c = y; e = 0; q = 1; ) : q ? ( e = y; q = 0; );
      pv = y; p += 1;
    );
    c != 0 && e != 0 ? (
      c < 0 ? ( a = -a; c = -c; e = -e; );
      d = 2 * c - a - e;
      d > 0.000000001 ? (
        b = max(min(0.5 * (e - a) / d, 0.5), -0.5);
        m = max(m, c + 0.5 * (e - a) * b - 0.5 * d * b * b);
      );
    );
    m;
  );
);

// ---------- look-ahead limiter (used twice: the LIMITER and the CEILING) ----------
// buffers from base B: audio L / R (B, B + 1024), gain history (B + 2048), sliding-minimum queue values / times
// (B + 3072, B + 4096), true-peak histories L / R (B + 5120, 32 each). n = the look-ahead (moving average) length.
function la_init(base, n) instance(bb, nn, ww, dd, gsum, grl, wp, tn, qh, qt, tw, g) local(k) (
  bb = base; nn = n; ww = n + 1; dd = n + 7;  // the audio waits for the true-peak detector's 7 samples too
  memset(bb, 0, 5184);
  k = 0; loop(1024, bb[2048 + k] = 1; k += 1; );
  gsum = nn; grl = 1; wp = tn = qh = qt = tw = 0; g = 1;
);
// one stereo sample in -> this.ol / this.orr (delayed by dd), this.g = the gain applied (amt 0..1 = how far in)
function la_run(xl, xr, cg, crel, tp, amt) instance(bb, nn, ww, dd, gsum, grl, wp, tn, qh, qt, tw, g, ol, orr)
  local(pk, gq, gmin, k) (
  pk = max(tp_peak(bb + 5120, tw, xl, tp), tp_peak(bb + 5152, tw, xr, tp)); tw += 1;
  gq = pk > cg ? cg / pk : 1;
  // sliding minimum over ww samples
  while (qt > qh && bb[3072 + ((qt - 1) & RMASK)] >= gq) ( qt -= 1; );
  bb[3072 + (qt & RMASK)] = gq; bb[4096 + (qt & RMASK)] = tn; qt += 1;
  while (bb[4096 + (qh & RMASK)] <= tn - ww) ( qh += 1; );
  gmin = bb[3072 + (qh & RMASK)];
  // release (never above the minimum), then a moving average over nn samples
  grl = gmin < grl ? gmin : grl + (gmin - grl) * crel;
  gsum += grl - bb[2048 + ((wp - nn) & RMASK)]; bb[2048 + (wp & RMASK)] = grl;
  bb[wp & RMASK] = xl; bb[1024 + (wp & RMASK)] = xr;
  ol = bb[(wp - dd) & RMASK]; orr = bb[1024 + ((wp - dd) & RMASK)];
  wp += 1; tn += 1;
  (wp & 65535) == 0 ? ( gsum = 0; k = 0; loop(nn, gsum += bb[2048 + ((wp - 1 - k) & RMASK)]; k += 1; ); );
  g = 1 + amt * (min(gsum / nn, 1) - 1);
  ol *= g; orr *= g;
  // the last safety: never a sample over the ceiling
  amt > 0.999 ? ( ol = max(min(ol, cg), -cg); orr = max(min(orr, cg), -cg); );
);

// ---------- COLOUR ----------
// smooth saturator: x (27 + x^2) / (27 + 9 x^2), flat from |x| = 3 (slope 1 at 0); never louder than its input
function sat3(x) ( x = max(-3, min(3, x)); x * (27 + x * x) / (27 + 9 * x * x); );
// one oversampled sample through the three curves (weights cw_tp / cw_tb / cw_dd glide), the drive set against the
// ceiling (cl_k = drive / ceiling: DRIVE -6 .. +8 dB at the ceiling), level-matched on programme (cl_o: a level
// 10 dB under the ceiling comes out as it went in); BLEND (cl_m) with the clean signal
function col_curve(v) local(y, a) (
  y = 0;
  cw_tp > 0.0001 ? y += cw_tp * sat3(v);
  cw_tb > 0.0001 ? y += cw_tb * (sat3(v + 0.35) - sat3_b) * sat3_d;
  cw_dd > 0.0001 ? ( a = v * v; y += cw_dd * v / sqrt(sqrt(1 + a * a)); );
  y;
);
function col_run(u) ( u + cl_m * (col_curve(u * cl_k) * cl_o - u); );

function set_rate() (
  la_n = ceil(0.0015 * srate); lc_n = ceil(0.0005 * srate);
  limx.la_init(LIMA, la_n); ceilx.la_init(LIMB, lc_n);
  os_base = la_n + 7 + lc_n + 7;
  c_sw = coef(0.015); c_kn = coef(0.03); c_lk = coef(0.01);
  c_cr = coef(0.03);                          // the ceiling's release
  c_1s = coef(1); c_tb = coef(0.3);
  c_fe = coef(0.03); c_fa = coef(0.0005); c_se = coef(0.3); c_sa = coef(0.04);
  ho_n = floor(0.04 * srate);
  tl_g = tan($pi * 800 / srate); tl_h = 1 / (1 + tl_g * (tl_g + 1.4142135623730951));
  tb_g = tan($pi * 800 / srate); tb_g = tb_g / (1 + tb_g);
  dc_c = 1 - 2 * $pi * 5 / srate;
);

// parameters derived from the sliders: run from @slider and every @block (panel moves don't trigger @slider)
function hy_params() local(t) (
  sc_tt = tan($pi * min(max(sc_hz, 20), 300) / srate);
  c_att = coef(att_ms * 0.001);
  c_rel = coef(rel_ms * 0.001);
  t = rms_t == 0 ? min(max(2 * att_ms, 3), 40) : rms_t == 1 ? 3 : rms_t == 2 ? 10 : rms_t == 3 ? 30 : 50;
  c_rms = coef(t * 0.001);
  c_aa = coef(1.2); c_as = coef(1.5 * rel_ms / 250);    // AUTO: the slow release and its (slow) build-up
  ceil_t = db2g(lim_ceil);
  in_setup(trim_db, guard);
  os_tf = os_f2n(os_sl);
);

function clear_dsp() (
  limx.la_init(LIMA, la_n); ceilx.la_init(LIMB, lc_n);
  memset(DRY, 0, 2048); memset(XCH, 0, 62 * 1024);
  ul.os_clr(); ur.os_clr(); dl_.os_clr(); dr_.os_clr();
  dcx_l = dcy_l = dcx_r = dcy_r = 0;
);

in_init();
os_design();
tp_design();
sat3_b = sat3(0.35); sat3_d = 1 / ((sat3(0.3501) - sat3(0.3499)) * 5000);
set_rate();
os_f = os_tf = os_f2n(os_sl);
os_gsetup();
hy_params();
clear_dsp();

rs_f = 1; rs_c = 1 / (0.015 * srate); dw = 0;
env_fa = env_fb = env_sa = env_sb = 0; rq_a = rq_b = 0;
thr_s = thr_db; rat_s = ratio; kn_s = knee_db; mk_s = db2g(mkup_db); mx_s = mix_pc * 0.01;
lk_s = lnk; cin_s = comp_in; lin_s = lim_in; ceil_s = ceil_t; lrl_s = lim_rel;
sc_gw = sc_tt; sc_h = 1 / (1 + sc_gw * (sc_gw + 1.4142135623730951));
ms_s = msw * 0.01; tl_s = tilt * 0.01; tbal = 0;
cw_tp = col_type == 0; cw_tb = col_type == 1; cw_dd = col_type == 2;
cdr_s = col_drv * 0.01; cmx_s = col_mix * 0.01; cin2_s = col_in;
lv_d = -150; dens = 0; ho_c = 0; fe = se = 0; gh_f = 1; sus = 0; cr_n = 0;

// ---------- presets (TRIM, GUARD and OS stay as they are) ----------
#hy_pmn = "Init|Gentle Glue|Transparent Master|Loud and Open|Punchy Pop|Dense Rock|Acoustic Air|Low End Hold|Tape Master|Tube Glow|Diode Edge|Broadcast Ride";
function load_preset(p) local(i) (
  thr_db = -18; ratio = 2; knee_db = 6; att_ms = 10; rel_ms = 250; rel_auto = 0; mkup_db = 0; mix_pc = 100;
  comp_in = 1; rms_t = 0; sc_hz = 60; msw = 0; tilt = 0; lnk = 1;
  lim_ceil = -1; lim_rel = 60; lim_auto = 0; tp_on = 1; lim_in = 1;
  col_type = 0; col_drv = 30; col_mix = 50; col_in = 0;
  p == 2 ? ( thr_db = -22; ratio = 1.5; knee_db = 12; att_ms = 30; rel_auto = 1; mkup_db = 4; sc_hz = 90; ) :
  p == 3 ? ( thr_db = -20; ratio = 1.8; knee_db = 10; att_ms = 20; rel_ms = 400; mkup_db = 4; sc_hz = 120; tilt = 30; lim_auto = 1; ) :
  p == 4 ? ( thr_db = -24; ratio = 2.5; knee_db = 8; att_ms = 15; rel_auto = 1; mkup_db = 11; sc_hz = 100; tilt = 60;
             lim_ceil = -1; lim_auto = 1; ) :
  p == 5 ? ( thr_db = -20; ratio = 3; knee_db = 6; att_ms = 25; rel_ms = 120; mkup_db = 7; sc_hz = 80; tilt = 40; lim_rel = 40; ) :
  p == 6 ? ( thr_db = -22; ratio = 4; knee_db = 6; att_ms = 5; rel_auto = 1; mkup_db = 11.5; mix_pc = 70; sc_hz = 80;
             col_in = 1; col_type = 1; col_drv = 40; col_mix = 40; ) :
  p == 7 ? ( thr_db = -24; ratio = 1.6; knee_db = 14; att_ms = 40; rel_ms = 600; mkup_db = 3.5; sc_hz = 150; tilt = 80; msw = -30; ) :
  p == 8 ? ( thr_db = -22; ratio = 2; knee_db = 10; att_ms = 30; rel_auto = 1; mkup_db = 5; sc_hz = 200; tilt = 50; ) :
  p == 9 ? ( thr_db = -20; ratio = 2; att_ms = 20; rel_auto = 1; mkup_db = 4.5; sc_hz = 90;
             col_in = 1; col_type = 0; col_drv = 45; col_mix = 60; lim_auto = 1; ) :
  p == 10 ? ( thr_db = -20; ratio = 1.8; knee_db = 10; att_ms = 25; rel_auto = 1; mkup_db = 4.5; sc_hz = 90;
              col_in = 1; col_type = 1; col_drv = 55; col_mix = 45; ) :
  p == 11 ? ( thr_db = -22; ratio = 2.5; att_ms = 10; rel_ms = 200; mkup_db = 7; sc_hz = 100; tilt = 30;
              col_in = 1; col_type = 2; col_drv = 35; col_mix = 30; ) :
  p == 12 ? ( thr_db = -26; ratio = 4; knee_db = 4; att_ms = 3; rel_auto = 1; rel_ms = 400; mkup_db = 16; rms_t = 2;
              sc_hz = 120; tilt = 50; lim_ceil = -0.5; lim_auto = 1; );
  i = 1; loop(23, ui_auto(i); i += 1; );
  pr_cur = p;
);

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;

@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

@slider
prs_chk();
hy_params();

@block
prs_chk();
hy_params();
// resume after REAPER's bypass (or any gap in processing): the buffers hold stale audio, so @sample clears them
// and fades in from the dry signal
rs_now = time_precise();
rs_last > 0 && rs_now - rs_last > max(0.15, 4 * samplesblock / srate) ? rs_req = 1;
rs_last = rs_now;
// the track's other channels (3 and up) pass through untouched but delayed by the same latency
xn = min(num_ch, 64) - 2;
pdc_bot_ch = 0; pdc_top_ch = 2 + max(xn, 0);

@sample
rs_req ? (
  rs_req = 0; rs_f = 0; rs_x = pdc_delay > 0 ? 0 : 1; rs_wait = pdc_delay;
  limx.la_init(LIMA, la_n); ceilx.la_init(LIMB, lc_n);   // (the gains start open: the best guess after a gap)
  ul.os_clr(); ur.os_clr(); dl_.os_clr(); dr_.os_clr();
  memset(DRY, 0, 2048); memset(XCH, 0, 62 * 1024);
);
rw_l = spl0; rw_r = spl1;

// ---------- glides ----------
thr_s += (thr_db - thr_s) * c_kn;
rat_s += (ratio - rat_s) * c_kn;
kn_s += (knee_db - kn_s) * c_kn;
mk_s += (db2g(mkup_db) - mk_s) * c_kn;
mx_s += (mix_pc * 0.01 - mx_s) * c_kn;
lk_s += (lnk - lk_s) * c_lk;
cin_s += (comp_in - cin_s) * c_sw;
lin_s += (lim_in - lin_s) * c_sw;
ceil_s += (ceil_t - ceil_s) * c_kn;
lrl_s += (lim_rel - lrl_s) * c_kn;
sc_gw += (sc_tt - sc_gw) * c_kn;
sc_h = 1 / (1 + sc_gw * (sc_gw + 1.4142135623730951));
ms_s += (msw * 0.01 - ms_s) * c_kn;
tl_s += (tilt * 0.01 - tl_s) * c_kn;
cw_tp += ((col_type == 0) - cw_tp) * c_sw; cw_tb += ((col_type == 1) - cw_tb) * c_sw; cw_dd += ((col_type == 2) - cw_dd) * c_sw;
cdr_s += (col_drv * 0.01 - cdr_s) * c_kn;
cmx_s += (col_mix * 0.01 - cmx_s) * c_kn;
cin2_s += (col_in - cin2_s) * c_sw;
cl_m = cmx_s * cin2_s;
cl_m > 0 || cl_m != cl_mq ? (
  cl_mq = cl_m; cl_d = db2g(-6 + 14 * cdr_s); cl_k = cl_d / ceil_s;
  cl_o = 0.3 * ceil_s / col_curve(0.3 * cl_d);
);
os_gate();
os_chg ? (
  ul.os_clr(); ur.os_clr(); dl_.os_clr(); dr_.os_clr();
  os_hold += 2 * samplesblock;                // stay silent while REAPER takes up the new latency
);

in_process(spl0, spl1);
xl = in_l; xr = in_r;
mi_l_d = max(mi_l_d, abs(xl)); mi_r_d = max(mi_r_d, abs(xr));

// ---------- detector: M/S weighting, SC HPF, RMS ----------
sm = 0.5 * (xl + xr); ss = 0.5 * (xl - xr);
dgm = min(1, 1 - ms_s); dgs = min(1, 1 + ms_s);
ha = hpa.svf_hp(dgm * sm + dgs * ss); hb = hpb.svf_hp(dgm * sm - dgs * ss);
rq_a += (ha * ha - rq_a) * c_rms; rq_b += (hb * hb - rq_b) * c_rms;
da = sqrt(2 * rq_a); db = sqrt(2 * rq_b);
dm = max(da, db); da += (dm - da) * lk_s; db += (dm - db) * lk_s;
lv_db = g2db(dm);
sl = 1 - 1 / max(rat_s, 1);
tga = gcurve(g2db(da) - thr_s, sl, kn_s); tgb = gcurve(g2db(db) - thr_s, sl, kn_s);

// ---------- AUTO ("ghost") release: transient density, sustained build-up, fast recovery in the gaps ----------
fe += (dm - fe) * (dm > fe ? c_fa : c_fe);   // fast envelope
se += (dm - se) * (dm > se ? c_sa : c_se);   // slow envelope
ho_c > 0 ? ho_c -= 1 : fe > 1.6 * se && fe > 0.0001 ? ( dens += 1; ho_c = ho_n; );   // an onset (4 dB over the body)
dens -= dens * c_1s;                         // = onsets in about the last second
(cr_n -= 1) <= 0 ? (
  cr_n = 32;
  dn_k = min(dens / 8, 1);                   // 0 = sparse, 1 = 8+ hits a second
  gh_f = lv_db < thr_s - 10 ? 3 : 1;         // the gaps: recover three times faster
  c_af = coef(rel_ms * 0.001 * (0.35 + 0.85 * dn_k) / gh_f);
  c_as2 = 1 - (1 - c_as) ^ gh_f;
  sus += ((limx.g < 0.98) - sus) * 32 * c_1s;
  c_lr = coef(lrl_s * 0.001 * (lim_auto ? (0.6 + 3 * sus) / gh_f : 1));
);
rel_auto ? (
  env_fa += (tga - env_fa) * (tga > env_fa ? c_att : c_af);
  env_fb += (tgb - env_fb) * (tgb > env_fb ? c_att : c_af);
  env_sa += (tga - env_sa) * (tga > env_sa ? c_aa : c_as2);
  env_sb += (tgb - env_sb) * (tgb > env_sb ? c_aa : c_as2);
  gra = max(env_fa, env_sa); grb = max(env_fb, env_sb);
) : (
  // AUTO just switched off: carry its gain reduction over, so it releases instead of jumping
  env_fa = max(env_fa, env_sa); env_fb = max(env_fb, env_sb);
  env_fa += (tga - env_fa) * (tga > env_fa ? c_att : c_rel);
  env_fb += (tgb - env_fb) * (tgb > env_fb ? c_att : c_rel);
  gra = env_fa; grb = env_fb;
  env_sa = env_sb = 0;
);
grc_disp = max(grc_disp, max(gra, grb) * cin_s);
lv_d = lv_db;

// ---------- TILT COMP: lean the ratio towards the end of the spectrum that carries the energy ----------
blo = tbl.op_lp(sm, tb_g); bhi = sm - blo;
e_lo += (blo * blo - e_lo) * c_tb; e_hi += (bhi * bhi - e_hi) * c_tb;
tb_t = e_lo + e_hi > 0.00000001 ? min(max((10 * log10((e_lo + 0.000000000001) / (e_hi + 0.000000000001)) - 3) / 12, -1), 1) : 0;
tbal += (tb_t - tbal) * c_tb;
tk = 0.5 * tl_s * tbal;                      // +: the lows carry it (the lows get more, the highs less)
tilt_d = tk * (max(gra, grb) > 0.1);

// ---------- compressor gain (two bands when TILT is up; one gain when it's at 0) ----------
la = lsa.svf_lp(xl); lb = lsb.svf_lp(xr);
gla = db2g(-gra * (1 + tk)); gha = db2g(-gra * (1 - tk));
glb = db2g(-grb * (1 + tk)); ghb = db2g(-grb * (1 - tk));
wa = (la * gla + (xl - la) * gha) * mk_s; wb = (lb * glb + (xr - lb) * ghb) * mk_s;
cl = xl + (wa - xl) * mx_s * cin_s; cr = xr + (wb - xr) * mx_s * cin_s;

// ---------- limiter ----------
limx.la_run(cl, cr, ceil_s, c_lr, tp_on, lin_s);
cl = limx.ol; cr = limx.orr;

// ---------- COLOUR (oversampled) ----------
ul.os_up(cl); ur.os_up(cr);
a0 = col_run(ul.s0); b0 = col_run(ur.s0);
os_f == 1 ? (
  cl = a0; cr = b0;
) : (
  a1 = col_run(ul.s1); b1 = col_run(ur.s1);
  os_f == 2 ? (
    cl = dl_.os_dn(a0, a1, 0, 0); cr = dr_.os_dn(b0, b1, 0, 0);
  ) : (
    a2 = col_run(ul.s2); b2 = col_run(ur.s2);
    a3 = col_run(ul.s3); b3 = col_run(ur.s3);
    cl = dl_.os_dn(a0, a1, a2, a3); cr = dr_.os_dn(b0, b1, b2, b3);
  );
);
// DC blocker (5 Hz): TUBE's offset
dcv = cl - dcx_l + dc_c * dcy_l; dcx_l = cl; dcy_l = dcv; cl = dcv;
dcv = cr - dcx_r + dc_c * dcy_r; dcx_r = cr; dcy_r = dcv; cr = dcv;

// ---------- ceiling (with the limiter) ----------
ceilx.la_run(cl, cr, ceil_s, c_cr, tp_on, lin_s);
ol = ceilx.ol; orr = ceilx.orr;
lgr_disp = max(lgr_disp, -g2db(limx.g * ceilx.g));

dip_s = os_g; dip_s *= dip_s * (3 - 2 * dip_s);   // an S-curve dip: no corners
spl0 = ol * dip_s; spl1 = orr * dip_s;
// the resume fade: the dry input (as REAPER's bypass passed it, delayed by the latency) into the processed sound
DRY[dw] = rw_l; DRY[1024 + dw] = rw_r;
rs_f < 1 ? (
  rs_wait > 0 ? rs_wait -= 1 : ( rs_f = min(rs_f + rs_c, 1); rs_x = min(rs_x + 3 * rs_c, 1); );
  rs_s = rs_f * rs_f * (3 - 2 * rs_f); rs_xs = rs_x * rs_x * (3 - 2 * rs_x);
  rs_dl = rw_l + (DRY[(dw - pdc_delay) & RMASK] - rw_l) * rs_xs;
  rs_dr = rw_r + (DRY[1024 + ((dw - pdc_delay) & RMASK)] - rw_r) * rs_xs;
  spl0 = rs_dl + (spl0 - rs_dl) * rs_s; spl1 = rs_dr + (spl1 - rs_dr) * rs_s;
);
// BYPASS: crossfades (15 ms) to the dry input, delayed by the latency so it stays in time
by_s += (byp - by_s) * c_sw;
by_s > 0.0001 ? (
  rs_dl = DRY[(dw - pdc_delay) & RMASK]; rs_dr = DRY[1024 + ((dw - pdc_delay) & RMASK)];
  spl0 += (rs_dl - spl0) * by_s; spl1 += (rs_dr - spl1) * by_s;
);
xn > 0 ? (
  xs = rs_f < 1 ? rs_xs : 1; k = 0;
  loop(xn,
    XCH[k * 1024 + dw] = spl(k + 2);
    spl(k + 2) = (spl(k + 2) + (XCH[k * 1024 + ((dw - pdc_delay) & RMASK)] - spl(k + 2)) * xs) * dip_s;
    k += 1;
  );
);
dw = (dw + 1) & RMASK;
mo_l_d = max(mo_l_d, abs(spl0)); mo_r_d = max(mo_r_d, abs(spl1));
lat_disp = pdc_delay;

@gfx 1080 560
ui_begin(1080, 560);
// =====================================================================
// GUI (design space 1080 x 560; S / M / L = 85 / 100 / 130 %). Brick-red faceplate, cream print, terracotta caps
// (deep) and a pale terracotta for accents (JG). Two plates: the controls (DETECTOR | COMPRESSOR | LIMITER | COLOUR)
// and the meters (TRIM, IN, TRANSFER, GR + TILT, OUT).
// =====================================================================
BG = 0x382B28; HDR = 0x2B201E; PL = 0x4A3B37; PLD = 0x40312E; INK = 0xEEE7D2; DIM = 0xA5A891;
GDEEP = 0x974C39; GLITE = 0xF1DFDB; HPFY = 0xD9B535;
sc != hy_lsc ? (
  hy_lsc = sc;
  hy_f2 = ui_fitfont(2, 15 * sc);                    // knob labels / values
  gfx_setfont(10, "Trebuchet MS", 30 * sc, 'b');     // the unit's name
  gfx_setfont(11, "Trebuchet MS", 17 * sc, 'b');     // section titles
  gfx_setfont(13, "Trebuchet MS", 12.5 * sc, 'b');   // small print
  gfx_setfont(14, "Trebuchet MS", 14.5 * sc, 'b');   // key legends, display boxes
  gfx_setfont(15, "Trebuchet MS", 18 * sc, 'b');     // the OUT peak figure
);
gfx_setfont(2, "Trebuchet MS", hy_f2, 'b'); gfx_setfont(3, "Trebuchet MS", hy_f2, 'b');
C_INK = INK; lk_font = 14; lk_font2 = 14; lk_well = 0x1D1615;

function hy_r(x, y, w, h) ( gfx_rect(x * sc, y * sc, max(1, w * sc), max(1, h * sc)); );
function hy_rr(x, y, w, h, r) ( ui_rrs(x * sc, y * sc, w * sc, h * sc, r * sc); );
function hy_tl(x, y, s) ( gfx_x = x * sc; gfx_y = y * sc; gfx_drawstr(s); );
function hy_tc(cx, y, s) ( ui_text(cx * sc, y * sc, s); );
function hy_tr(x, y, s) local(w, h) ( gfx_measurestr(s, w, h); gfx_x = x * sc - w; gfx_y = y * sc; gfx_drawstr(s); );
function hy_in(x, y, w, h) ( mouse_x >= x * sc && mouse_x < (x + w) * sc && mouse_y >= y * sc && mouse_y < (y + h) * sc; );
// soft drop shadow, light from the top left: 4 layers, (+2, +3) to (+6, +8)
function hy_shadow(x, y, w, h, r) local(j) (
  j = 0; loop(4, ui_cola(0x000000, 0.13); hy_rr(x + 2 + j * 1.3, y + 3 + j * 1.7, w + j * 0.4, h + j * 0.4, r + j); j += 1; );
);
// a plate: shadow, a light top / left edge, a dark bottom / right edge
function hy_plate(x, y, w, h) (
  hy_shadow(x, y, w, h, 6);
  ui_col(0x1F1816); hy_rr(x, y, w, h, 6);
  ui_col(0x6B5D5A); hy_rr(x, y, w - 1, h - 1, 6);
  ui_col(PL); hy_rr(x + 1, y + 1, w - 2.5, h - 2.5, 5);
  gfx_gradrect((x + 2) * sc, (y + 2) * sc, (w - 4) * sc, h * 0.4 * sc, 1, 1, 1, 0.05, 0, 0, 0, 0, 0, 0, 0, -0.05 / (h * 0.4 * sc));
);
// a section header band (32 high): the name on a sea-green plate, a pale line under the band
function hy_head(x, w, title) local(tw, th) (
  ui_col(PLD); hy_r(x + 1, 81, w - 2, 32);
  ui_cola(GLITE, 0.55); hy_r(x + 1, 113, w - 2, 1);
  gfx_setfont(11); gfx_measurestr(title, tw, th);
  ui_col(0x602E22); hy_rr(x + 11, 84, tw / sc + 22, 26, 4);
  ui_col(GDEEP); hy_rr(x + 12, 85, tw / sc + 20, 24, 3);
  ui_col(INK); gfx_x = (x + 22) * sc; gfx_y = 97 * sc - th * 0.5; gfx_drawstr(title);
);
// a divider between two sections: a dark and a light hairline
function hy_div(x) ( ui_col(0x2A211F); hy_r(x, 82, 1, 268); ui_cola(0xFFFFFF, 0.07); hy_r(x + 1, 82, 1, 268); );
// a dark recessed display box (the current item, a down-pointing triangle); a click opens the list. Its name sits
// above it. Returns the chosen index (0-based) or -1.
function hy_box(x, y, w, h, name, menu, v) local(hov, sel, n, i, tw, th) (
  hov = hy_in(x, y, w, h);
  gfx_setfont(13); ui_col(DIM); hy_tc(x + w * 0.5, y - 18, name);
  ui_col(0x161110); hy_rr(x - 1, y - 1, w + 2, h + 2, 4);
  ui_col(0x241D1B); hy_rr(x, y, w, h, 3);
  ui_cola(0x000000, 0.5); hy_r(x + 1, y + 1, w - 2, 2);
  hov ? ( ui_cola(0xFFFFFF, 0.06); hy_rr(x, y, w, h, 3); );
  pk_pitem(menu, v + 1, #hy_di);
  gfx_setfont(14); ui_col(GLITE); gfx_measurestr(#hy_di, tw, th);
  gfx_x = (x + (w - 14) * 0.5) * sc - tw * 0.5; gfx_y = (y + h * 0.5) * sc - th * 0.5; gfx_drawstr(#hy_di);
  ui_col(DIM); ui_sstri((x + w - 17) * sc, (y + h * 0.5 - 3) * sc, (x + w - 7) * sc, (y + h * 0.5 - 3) * sc, (x + w - 12) * sc, (y + h * 0.5 + 3) * sc);
  sel = -1;
  pressed && hov && drag_id == 0 ? (
    drag_id = -1;
    strcpy(#hy_dm, ""); n = pk_pcount(menu); i = 1;
    loop(n, i == v + 1 ? strcat(#hy_dm, "!"); strcat(#hy_dm, pk_pitem(menu, i, #hy_dt)); i < n ? strcat(#hy_dm, "|"); i += 1; );
    gfx_x = x * sc; gfx_y = (y + h) * sc; i = gfx_showmenu(#hy_dm); i > 0 ? sel = i - 1;
  );
  sel;
);
// a desk knob (ui_knob does the mouse, the value on touch and the label)
function hy_kn(cx, cy, r, idx, lo, hi, def, lg, cap, lbl, fmt) (
  ui_kdesk = cap; ui_kdesk_r = r; ui_kdesk_p = 0;
  ui_knob(cx * sc, cy * sc, idx, lo, hi, def, lg, lbl, fmt);
  ui_kdesk = 0; ui_kdesk_r = 0;
);
// small print outside a dial at the normalised position n
function hy_mark(cx, cy, r, n, s) local(a) (
  a = -0.75 * $pi + n * 1.5 * $pi; gfx_setfont(13); ui_col(GLITE);
  hy_tc(cx + sin(a) * (r + 14), cy - cos(a) * (r + 14) - 7, s);
);
// a lamp key from its slider: lit = on; the click toggles
function hy_key(x, y, w, h, idx, col, s) (
  lk_key(x, y, w, h, slider(idx) >= 0.5, col, s) ? ( slider(idx) = slider(idx) >= 0.5 ? 0 : 1; ui_auto(idx); );
);
// a GR bar: 20 segments of 1 dB, from the left
function hy_gr(x, y, w, h, gr, col) local(i, c, lit) (
  ui_col(0x100C0C); hy_r(x - 2, y - 2, w + 4, h + 4);
  c = w / 20; i = 0;
  loop(20,
    lit = gr >= i + 0.5;
    lit ? ( ui_cola(col, 0.3); hy_r(x + i * c - 0.5, y - 1, c, h + 2); ui_col(col); ) : ui_cola(col, 0.12);
    hy_r(x + i * c + 0.5, y, c - 1.5, h);
    i += 1;
  );
);
// the TILT bar: a centre-zero row of 10 segments each way (lows to the left, highs to the right)
function hy_tilt(x, y, w, h, t) local(i, c, n, lit) (
  ui_col(0x100C0C); hy_r(x - 2, y - 2, w + 4, h + 4);
  c = w / 20; n = floor(abs(t) * 20 + 0.5); i = 0;
  loop(20,
    lit = t > 0 ? (i < 10 && 10 - i <= n) : (i >= 10 && i - 9 <= n);
    lit ? ( ui_cola(GLITE, 0.3); hy_r(x + i * c - 0.5, y - 1, c, h + 2); ui_col(GLITE); ) : ui_cola(GLITE, 0.12);
    hy_r(x + i * c + 0.5, y, c - 1.5, h);
    i += 1;
  );
  ui_col(INK); hy_r(x + w * 0.5 - 0.5, y - 3, 1, h + 6);
);
// the compressor's static curve for the TRANSFER window (the panel's own copy of the audio thread's gcurve)
function hy_curve(o, sl, kw) (
  kw < 0.01 ? (o > 0 ? o * sl : 0) :
  o <= -0.5 * kw ? 0 : o >= 0.5 * kw ? o * sl : sl * sqr(o + 0.5 * kw) / (2 * kw);
);
// TRANSFER: input -48 .. 0 dBFS across, output -48 .. 0 up; grid every 12 dB, the unity diagonal, the CEILING
// (amber, with the limiter in), the compressor's curve (with MAKE-UP), and a dot at the detector's level
function hy_xfer(x, y, s, lvl) local(i, p, q, xi, yo, px, py, ox, oy, sl, cy, d) (
  ui_col(0x161110); hy_rr(x - 2, y - 2, s + 4, s + 4, 4);
  ui_col(0x211917); hy_r(x, y, s, s);
  ui_cola(0x000000, 0.5); hy_r(x, y, s, 2);
  i = 1; loop(3, ui_cola(GLITE, 0.12); hy_r(x + s * i / 4, y, 1, s); hy_r(x, y + s * i / 4, s, 1); i += 1; );
  ui_cola(INK, 0.25); ui_ssline(x * sc, (y + s) * sc, (x + s) * sc, y * sc, sc, 1);
  sl = 1 - 1 / max(ratio, 1);
  lim_in ? (
    cy = y + s * (-lim_ceil) / 48; ui_cola(0xF2A41E, 0.8); hy_r(x, cy, s, 1.5);
  );
  i = 0; ox = oy = 0;
  loop(97,
    xi = -48 + i * 0.5;
    yo = xi + (comp_in ? -hy_curve(xi - thr_db, sl, knee_db) * mix_pc * 0.01 + mkup_db : 0);
    lim_in ? yo = min(yo, lim_ceil);
    px = x + s * (xi + 48) / 48; py = y + s * (-max(min(yo, 0), -48)) / 48;
    i > 0 ? ( ui_col(GLITE); ui_ssline(ox * sc, oy * sc, px * sc, py * sc, 2 * sc, 1); );
    ox = px; oy = py; i += 1;
  );
  lvl > -48 ? (
    xi = min(lvl, 0);
    yo = xi + (comp_in ? -hy_curve(xi - thr_db, sl, knee_db) * mix_pc * 0.01 + mkup_db : 0);
    lim_in ? yo = min(yo, lim_ceil);
    px = x + s * (xi + 48) / 48; py = y + s * (-max(min(yo, 0), -48)) / 48;
    ui_cola(0xFFF2D0, 0.25); gfx_circle(px * sc, py * sc, 7 * sc, 1, 1);
    ui_col(0xFFF2D0); gfx_circle(px * sc, py * sc, 3.5 * sc, 1, 1);
  );
  gfx_setfont(13); ui_col(DIM); hy_tc(x + s * 0.5, y - 20, "TRANSFER");
);

// ---------- the meters' hand-off (the audio thread fills, the panel takes and clears) ----------
hy_grc = max(grc_disp, hy_grc * 0.85); grc_disp = 0;
hy_grl = max(lgr_disp, hy_grl * 0.85); lgr_disp = 0;
hy_il = mi_l_d; hy_ir = mi_r_d; mi_l_d = mi_r_d = 0;
hy_ol = mo_l_d; hy_or = mo_r_d; mo_l_d = mo_r_d = 0;
hy_lv += (lv_d - hy_lv) * 0.5;
hy_tk += (tilt_d - hy_tk) * 0.3;

// ---------- faceplate and header ----------
ui_col(BG); gfx_rect(0, 0, gfx_w, gfx_h);
ui_col(HDR); hy_r(0, 0, 1080, 64);
ui_col(GLITE); hy_r(0, 63, 1080, 2); ui_col(0x191312); hy_r(0, 65, 1080, 1);
ui_cola(0x000000, 0.35); hy_r(1077, 0, 3, 560); hy_r(0, 557, 1080, 3);       // a narrow darker right / bottom edge
ui_screw(9 * sc, 9 * sc); ui_screw((1080 - 9) * sc, 9 * sc); ui_screw(9 * sc, (560 - 9) * sc); ui_screw((1080 - 9) * sc, (560 - 9) * sc);
ui_col(INK); gfx_circle(40 * sc, 32 * sc, 16 * sc, 1, 1);
gfx_setfont(13); ui_col(HDR); ui_keytext(40 * sc, 32 * sc, "GFX");
gfx_setfont(10); ui_col(INK); hy_tl(68, 15, "RMS 201"); gfx_measurestr("RMS 201", hy_tw, hy_th);
gfx_setfont(13); ui_col(GLITE); hy_tl(82 + hy_tw / sc, 20, "MASTERING"); hy_tl(82 + hy_tw / sc, 34, "COMP / LIMITER");
prs_cur_d > 0 ? ( pr_cur = prs_cur_d; prs_cur_d = 0; );     // PRESET parameter: show a preset loaded by the host
hy_k = pk_preset(392 * sc, 15 * sc, 290 * sc, 34 * sc, pr_cur, #hy_pmn);
hy_k > 0 ? prs_dirty = hy_k;
by_keywh(812, 18, 84, 28, 27, 16);                      // BYPASS: the house keycap
os_key(960, 21, 24, INK, 12);
ui_size_picker(998 * sc, 23 * sc);

// ---------- the controls plate ----------
hy_plate(16, 80, 1048, 272);
hy_head(16, 264, "DETECTOR"); hy_head(280, 400, "COMPRESSOR"); hy_head(680, 196, "LIMITER"); hy_head(876, 188, "COLOUR");
hy_div(280); hy_div(680); hy_div(876);

// DETECTOR
hy_kn(64, 172, 20, 11, 20, 300, 60, 1, HPFY, "SC HPF", "%.0f Hz");
hy_kn(148, 172, 20, 12, -100, 100, 0, 0, GDEEP, "SC M/S", "%+.0f %%");
gfx_setfont(13); ui_col(GLITE);                          // M / 0 / S at the ends and the centre (as the WIDTH dials)
hy_tc(148 - 0.707 * 28 - 3, 172 + 0.707 * 28 - 10, "M"); hy_tc(148, 172 - 28 - 7, "0"); hy_tc(148 + 0.707 * 28 + 3, 172 + 0.707 * 28 - 10, "S");
hy_kn(232, 172, 20, 13, 0, 100, 0, 0, GDEEP, "TILT", "%.0f %%");
hy_v = hy_box(36, 272, 104, 30, "RMS TIME", "AUTO|3 ms|10 ms|30 ms|50 ms", rms_t);
hy_v >= 0 ? ( rms_t = hy_v; ui_auto(10); );
hy_key(164, 270, 92, 34, 14, LK_BLU_LT, "LINK");

// COMPRESSOR
hy_kn(340, 186, 30, 1, -40, 0, -18, 0, GDEEP, "THRESHOLD", "%.1f dB");
hy_mark(340, 186, 30 + 3, 22 / 40, "0");                 // 0 VU (-18 dBFS)
hy_kn(444, 166, 19, 2, 1, 10, 2, 1, GDEEP, "RATIO", "%.1f : 1");
hy_kn(530, 166, 19, 3, 0, 18, 6, 0, GDEEP, "KNEE", "%.1f dB");
hy_kn(616, 166, 19, 4, 0.1, 100, 10, 1, GDEEP, "ATTACK", "%.1f ms");
hy_kn(444, 276, 19, 5, 20, 2500, 250, 1, GDEEP, "RELEASE", "%.0f ms");
hy_kn(530, 276, 19, 7, 0, 20, 0, 0, 0xC9A227, "MAKE-UP", "+%.1f dB");
hy_kn(616, 276, 19, 8, 0, 100, 100, 0, GDEEP, "MIX", "%.0f %%");
hy_key(300, 270, 80, 34, 6, LK_BLU_LT, "AUTO");

// LIMITER
hy_kn(736, 178, 26, 15, -12, 0, -1, 0, GDEEP, "CEILING", "%.1f dB");
hy_kn(828, 178, 19, 16, 5, 1000, 60, 1, GDEEP, "RELEASE", "%.0f ms");
hy_key(694, 256, 80, 32, 17, LK_BLU_LT, "AUTO");
hy_key(784, 256, 84, 32, 18, LK_BLU_LT, "TRUE PEAK");
gfx_setfont(13); ui_col(DIM);
sprintf(#hy_s, "LATENCY %d SAMPLES", lat_disp); hy_tc(778, 312, #hy_s);

// COLOUR
hy_v = hy_box(928, 146, 100, 30, "TYPE", "TAPE|TUBE|DIODE", col_type);
hy_v >= 0 ? ( col_type = hy_v; ui_auto(20); );
// < > step the TYPE (wrapping), the preset bar's arrow keys
ui_noled = 1; ui_button(898 * sc, 146 * sc, 24 * sc, 30 * sc, "", 0, 1) ? ( col_type = col_type <= 0 ? 2 : col_type - 1; ui_auto(20); );
pk_arrow(898 * sc, 146 * sc, 24 * sc, 30 * sc, -1);
ui_noled = 1; ui_button(1034 * sc, 146 * sc, 24 * sc, 30 * sc, "", 0, 1) ? ( col_type = col_type >= 2 ? 0 : col_type + 1; ui_auto(20); );
pk_arrow(1034 * sc, 146 * sc, 24 * sc, 30 * sc, 1);
hy_kn(926, 252, 20, 21, 0, 100, 30, 0, 0xB0563C, "DRIVE", "%.0f %%");
hy_kn(1014, 252, 20, 22, 0, 100, 50, 0, GDEEP, "BLEND", "%.0f %%");

// a switched-off section: greyed, its IN key drawn on top
!comp_in ? ( ui_cola(PL, 0.5); hy_r(281, 114, 398, 236); );
!lim_in ? ( ui_cola(PL, 0.5); hy_r(681, 114, 194, 236); );
!col_in ? ( ui_cola(PL, 0.5); hy_r(877, 114, 186, 236); );
lk_font = lk_font2 = 14;
hy_key(612, 85, 56, 24, 9, LK_AMB, "IN");
hy_key(808, 85, 56, 24, 19, LK_AMB, "IN");
hy_key(996, 85, 56, 24, 23, LK_AMB, "IN");

// ---------- the meters plate ----------
hy_plate(16, 368, 1048, 176);
// TRIM + GUARD
ui_kdesk = 0x6B6260; ui_kdesk_r = 17; ui_knob(54 * sc, 418 * sc, 25, -24, 12, 0, 0, "TRIM", "%+.1f dB"); ui_kdesk = 0; ui_kdesk_r = 0;
hy_key(22, 480, 64, 28, 26, LK_AMB, "GUARD");
// IN: L and R rows
gfx_setfont(13); ui_col(DIM); hy_tl(112, 384, "IN (dBFS)");
ui_col(INK); hy_tl(112, 409, "L"); hy_tl(112, 437, "R");
mIL.ui_ledm(132 * sc, 410 * sc, 200 * sc, 14 * sc, hy_il); mIR.ui_ledm(132 * sc, 438 * sc, 200 * sc, 14 * sc, hy_ir);
gfx_setfont(13); ui_col(DIM);
hy_tc(132 + 200 * 2 / 15, 458, "-36"); hy_tc(132 + 200 * 6 / 15, 458, "-24"); hy_tc(132 + 200 * 10 / 15, 458, "-12");
hy_tc(132 + 200 * 14 / 15, 458, "0");
// TRANSFER
hy_xfer(372, 404, 128, hy_lv);
// GR + TILT
gfx_setfont(13); ui_col(DIM); hy_tl(536, 384, "GAIN REDUCTION (dB)");
ui_col(INK); hy_tl(536, 409, "COMP"); hy_tl(536, 445, "LIMIT");
hy_gr(596, 410, 170, 14, hy_grc, 0xE0463A);
hy_gr(596, 446, 170, 14, hy_grl, 0xF2A41E);
ui_col(DIM); hy_i = 0;
loop(5, hy_n = hy_i == 0 ? 0 : hy_i == 1 ? 3 : hy_i == 2 ? 6 : hy_i == 3 ? 10 : 20;
  sprintf(#hy_s, "%d", hy_n); hy_tc(596 + hy_n * 8.5 - (hy_n == 20 ? 6 : 0) + (hy_n == 0 ? 4 : 0), 428, #hy_s); hy_i += 1; );
ui_col(INK); hy_tl(536, 493, "TILT");
hy_tilt(596, 494, 170, 12, hy_tk);
gfx_setfont(13); ui_col(DIM); hy_tl(596, 512, "LOWS"); hy_tr(766, 512, "HIGHS");
// OUT: L and R rows, the floating peak and its figure
gfx_setfont(13); ui_col(DIM); hy_tl(796, 384, "OUT (dBFS)");
ui_col(INK); hy_tl(796, 409, "L"); hy_tl(796, 437, "R");
mOL.ui_ledm(816 * sc, 410 * sc, 180 * sc, 14 * sc, hy_ol); mOR.ui_ledm(816 * sc, 438 * sc, 180 * sc, 14 * sc, hy_or);
gfx_setfont(13); ui_col(DIM);
hy_tc(816 + 180 * 2 / 15, 458, "-36"); hy_tc(816 + 180 * 6 / 15, 458, "-24"); hy_tc(816 + 180 * 10 / 15, 458, "-12");
hy_tc(816 + 180 * 14 / 15, 458, "0");
hy_pk = max(mOL.hold, mOR.hold);
gfx_setfont(15); ui_col(hy_pk > -0.05 ? 0xFF5A4A : INK);
hy_pk > -60 ? sprintf(#hy_s, "%.1f", hy_pk) : strcpy(#hy_s, "--"); hy_tr(1048, 414, #hy_s);
gfx_setfont(13); ui_col(DIM); hy_tr(1048, 442, "PEAK");

pk_end();
ui_end();
