desc:GFX Duplex Q //tags: compressor de-esser limiter dual-channel mid-side broadcast //author: JGWizrad / Garden FX (GFX) // // Duplex Q: GFX Duplex with a tilt EQ and high / low-pass filters (the Tilt EQ engine) added below. // ORDER puts the EQ after the compressors or in front of them. EQ: TILT and PIVOT (GENTLE / STEEP), HIGH-PASS and // LOW-PASS with slope, TILT ON stereo / mid / side, OUTPUT, IN. // // Requires gfx-drift, gfx-input, gfx-output, gfx-ui, gfx-panel, gfx-instrument and gfx-dyn (.jsfx-inc) in the same folder. // // A dual-channel compressor / de-esser / peak limiter in the spirit of the classic 80s British two-channel // dynamics processors (personal use; the sound is designed from scratch). Each channel: // THRESHOLD, RATIO (1:1 - INF:1), ATTACK, RELEASE, GAIN - a clean VCA compressor with a gentle 6 dB knee. // AUTO - program-dependent timing: the attack follows how far over the signal is, the release // lets go quickly after short peaks and slowly after long passages of compression. // DE-ESS - the compressor listens only above DE-ESS FREQ (frequency-conscious side-chain), so it // reacts to sibilance. SPLIT: the gain reduction is applied only above that frequency, so the // esses come down and the body of the voice is untouched (the split sits at 3/4 of the frequency); off: wideband, as on the originals. // LIMIT - a separate fast peak limiter after the compressor (fully left = OFF); its light shows it working. // IN - the channel in or out. // MASTER: // MODE - STEREO: channel A runs both sides (channel B rests). DUAL: A = left, B = right. // M-S: A = mid, B = side (compress the centre and the edges separately). // LINK - in DUAL and M-S the two detectors share the louder level, so the image stays put, and the two // channels' controls are ganged: turning one moves the other by the same amount (offsets are kept). // MIX - parallel compression: the dry signal blended back in. // PRESETS. import gfx-drift.jsfx-inc import gfx-input.jsfx-inc import gfx-output.jsfx-inc import gfx-ui.jsfx-inc import gfx-panel.jsfx-inc import gfx-instrument.jsfx-inc import gfx-dyn.jsfx-inc slider1:1<0,1,1{Out,In}>-A in slider2:-20<-40,0,0.1>-A threshold (dB) slider3:3<1,20,0.1>-A ratio (20 = inf) slider4:5<0.1,50,0.1>-A attack (ms) slider5:200<20,2000,1>-A release (ms) slider6:0<0,1,1{Off,On}>-A auto slider7:0<-10,20,0.1>-A gain (dB) slider8:0<0,1,1{Off,On}>-A de-ess slider9:6000<2000,12000,10>-A de-ess frequency (Hz) slider10:1<0,1,1{Wide,Split}>-A de-ess split slider11:0<-24,0,0.1>-A limit (dB, 0 = off) slider12:1<0,1,1{Out,In}>-B in slider13:-20<-40,0,0.1>-B threshold (dB) slider14:3<1,20,0.1>-B ratio (20 = inf) slider15:5<0.1,50,0.1>-B attack (ms) slider16:200<20,2000,1>-B release (ms) slider17:0<0,1,1{Off,On}>-B auto slider18:0<-10,20,0.1>-B gain (dB) slider19:0<0,1,1{Off,On}>-B de-ess slider20:6000<2000,12000,10>-B de-ess frequency (Hz) slider21:1<0,1,1{Wide,Split}>-B de-ess split slider22:0<-24,0,0.1>-B limit (dB, 0 = off) slider23:0<0,2,1{Stereo,Dual,M-S}>-Mode slider24:1<0,1,1{Off,On}>-Link slider25:100<0,100,1>-Mix (%) slider26:0<-24,12,0.1>-Input trim (dB) slider27:1<0,1,1{Off,On}>-Input guard slider28:0<0,100,1>-Band sat (%) slider29:30<0,100,1>-Bias (%) slider30:120<40,400,1>-Sat low band (Hz) slider31:6000<2000,16000,10>-Sat high band (Hz) slider32:-0.3<-24,0,0.1>-Ceiling / limit threshold (dB) slider33:1<0,2,1{Off,Clip,Limit}>-Output stage slider34:0<-6,6,0.1>-EQ: Tilt (dB at each end) slider35:800<100,5000,1>-EQ: Pivot (Hz) slider36:0<0,1,1{Gentle,Steep}>-EQ: Shape slider37:10<10,1000,1>-EQ: High-pass (Hz, 10 = off) slider38:1<0,5,1{6,12,18,24,36,48}>-EQ: High-pass slope (dB/oct) slider39:22000<1000,22000,10>-EQ: Low-pass (Hz, 22000 = off) slider40:1<0,5,1{6,12,18,24,36,48}>-EQ: Low-pass slope (dB/oct) slider41:0<0,2,1{Stereo,Mid,Side}>-EQ: Tilt on slider42:0<-12,12,0.1>-EQ: Output (dB) slider43:1<0,1,1{Out,In}>-EQ: EQ in slider44:0<0,1,1{Comp then EQ,EQ then Comp}>-Order in_pin:left input in_pin:right input out_pin:left output out_pin:right output @init #ui_man = "gfx-duplex-q"; // the ? pop-up links to this manual // ---------- one channel's settings (parameter set) ---------- function cs_set(base) instance(on, thr, sl, ac, rc, aut, mk, de, dfc, spl, lon, ltg) ( on = slider(base); thr = slider(base + 1); sl = slider(base + 2) >= 19.95 ? 0 : 1 / slider(base + 2); ac = dy_ms(slider(base + 3)); rc = dy_ms(slider(base + 4)); aut = slider(base + 5); mk = 10 ^ (slider(base + 6) / 20); de = slider(base + 7); dfc = 1 - exp(-2 * $pi * slider(base + 8) / srate); spl = slider(base + 9); lon = slider(base + 10) < -0.05; ltg = 10 ^ (slider(base + 10) / 20); ); // ---------- one signal path (state); the settings come in as arguments ---------- // detector: RMS over ~3 ms (+3 dB so a sine reads its peak); DE-ESS = only what lies above the frequency function p_det(x, de) instance(hs, ms) local(s) ( s = de ? 2 * hs.bq_run(x) : x; ms += (s * s - ms) * det_c; 10 * log10(ms + 0.0000000001) + 3; ); // gain computer + timing (dB domain) function p_gain(lv, on, thr, sl, ac, rc, aut) instance(e, sust) local(t, d, c) ( on ? ( t = -(1 - sl) * dy_curve(lv - thr, 6); t < e ? ( d = e - t; c = aut ? min(1, max(d, 5) / (0.15 * srate)) : ac; e += (t - e) * c; ) : ( c = aut ? dy_ms(80 + 900 * sust / 1.5) : rc; e += (t - e) * c; ); sust = min(max(sust + (e < -1 ? 1 : -2) / srate, 0), 1.5); ) : e = 0; e; ); // apply: wideband, or only above the de-ess frequency (SPLIT) // SPLIT uses a 24 dB/oct Linkwitz-Riley crossover at the de-ess frequency (the two bands sum back flat) function p_xo(f) instance(hs, la, lb, ha, hb) ( hs.bq_set(1, f, 0.70710678, 0); // detector: only what lies above the frequency (+6 dB) f *= 0.75; // the split sits a little lower, so the whole ess band is caught la.bq_set(0, f, 0.70710678, 0); lb.bq_set(0, f, 0.70710678, 0); ha.bq_set(1, f, 0.70710678, 0); hb.bq_set(1, f, 0.70710678, 0); ); function p_apply(x, on, mk, de, spl) instance(e, la, lb, ha, hb) local(g) ( g = 10 ^ (e / 20); on ? ( de && spl ? ( (lb.bq_run(la.bq_run(x)) + hb.bq_run(ha.bq_run(x)) * g) * mk; ) : x * g * mk; ) : x; ); // fast peak limiter: instant attack, 50 ms release; returns the gain function p_limt(x, lon, ltg) instance(lg) local(t) ( t = lon && abs(x) > ltg ? ltg / abs(x) : 1; lg = t < lg ? t : lg + (1 - lg) * lim_r; lg; ); function update() ( cA.cs_set(1); cB.cs_set(12); mode = slider23; P1.p_xo(slider9); P2.p_xo(mode == 0 ? slider9 : slider20); linked = mode == 0 || slider24; mix_t = slider25 / 100; in_setup(0, slider27); ost_setup(0, 0, 120, 6000, -0.3, 0); // no GFX output stage on this unit: its own character does that job ); // ---------- presets (leave INPUT and OUTPUT alone) ---------- // one channel: threshold, ratio, attack, release, auto, gain, de-ess, freq, split, limit function pc(base, t, r, a, rl, au, g, de, f, sp, lm) ( slider(base) = 1; slider(base + 1) = t; slider(base + 2) = r; slider(base + 3) = a; slider(base + 4) = rl; slider(base + 5) = au; slider(base + 6) = g; slider(base + 7) = de; slider(base + 8) = f; slider(base + 9) = sp; slider(base + 10) = lm; ); function pm(md, lk, mx) ( slider23 = md; slider24 = lk; slider25 = mx; ); function load_preset(p) ( p == 1 ? ( pm(0, 1, 100); pc(1, -24, 2, 10, 300, 1, 3, 0, 6000, 1, 0); pc(12, -24, 2, 10, 300, 1, 3, 0, 6000, 1, 0); ) : // Gentle Stereo Bus p == 2 ? ( pm(0, 1, 100); pc(1, -28, 3, 5, 180, 0, 5, 0, 6000, 1, -3); pc(12, -28, 3, 5, 180, 0, 5, 0, 6000, 1, -3); ) : // Vocal Compressor p == 3 ? ( pm(0, 1, 100); pc(1, -30, 6, 0.5, 80, 0, 0, 1, 6500, 1, 0); pc(12, -30, 6, 0.5, 80, 0, 0, 1, 6500, 1, 0); ) : // De-esser (split) p == 4 ? ( pm(0, 1, 100); pc(1, -26, 4, 3, 150, 0, 4, 1, 5000, 0, -2); pc(12, -26, 4, 3, 150, 0, 4, 1, 5000, 0, -2); ) : // Broadcast Voice (wide de-ess) p == 5 ? ( pm(1, 0, 100); pc(1, -22, 3, 8, 200, 1, 3, 0, 6000, 1, -1); pc(12, -22, 3, 8, 200, 1, 3, 0, 6000, 1, -1); ) : // Dual Mono Pair p == 6 ? ( pm(2, 0, 100); pc(1, -20, 2.5, 15, 250, 0, 2, 0, 6000, 1, 0); pc(12, -30, 4, 5, 150, 0, 3, 0, 6000, 1, 0); ) : // M-S Glue (sides held) p == 7 ? ( pm(2, 0, 100); pc(1, -22, 2, 20, 300, 1, 1, 0, 6000, 1, 0); pc(12, -40, 1, 5, 200, 0, 3, 1, 7000, 1, 0); ) : // M-S Widen + Tame Sides p == 8 ? ( pm(0, 1, 100); pc(1, -12, 20, 0.3, 60, 0, 0, 0, 6000, 1, -1); pc(12, -12, 20, 0.3, 60, 0, 0, 0, 6000, 1, -1); ) : // Brickwall Safety p == 9 ? ( pm(0, 1, 40); pc(1, -36, 10, 0.5, 100, 0, 12, 0, 6000, 1, -2); pc(12, -36, 10, 0.5, 100, 0, 12, 0, 6000, 1, -2); ) : // Parallel Drums p == 10 ? ( pm(0, 1, 100); pc(1, -24, 4, 20, 400, 1, 4, 0, 6000, 1, -2); pc(12, -24, 4, 20, 400, 1, 4, 0, 6000, 1, -2); ); // Bass Leveller p >= 1 && p <= 10 ? slider_automate(33554431); // sliders 1-25 p == 11 ? ( // Reset all to defaults pm(0, 1, 100); pc(1, -20, 3, 5, 200, 0, 0, 0, 6000, 1, 0); pc(12, -20, 3, 5, 200, 0, 0, 0, 6000, 1, 0); slider26 = 0; slider27 = 1; slider28 = 0; slider29 = 30; slider30 = 120; slider31 = 6000; slider32 = -0.3; slider33 = 1; slider_automate(8589934591); // sliders 1-33 ); ); in_init(); ost_init(20000); ostL.ost_ch_init(); ostR.ost_ch_init(); det_c = 1 - exp(-1 / (0.003 * srate)); lim_r = 1 - exp(-1 / (0.05 * srate)); sm = 1 - exp(-1 / (0.03 * srate)); P1.lg = 1; P2.lg = 1; !ui_dflt ? ui_thm = 5; !ui_dflt ? ui_hw = 1; // fixed faceplate PNL = 0xA9ACA7; INKD = 0x1C201E; GRN = 0x2E7A4C; GRND = 0x1E5A36; // grey panel, black print, green CAP_C = 0xF2F2EE; CAP_G = 0xF2F2EE; CAP_D = 0x2FC46A; CAP_L = 0xE8402E; // pointer colours: compressor, gain, de-ess, limit update(); mix_s = mix_t; // ================= EQ (the Tilt EQ engine, names q_..., sliders 34 - 43) ================= // a preset mask of the EQ unit, one slider at a time function cb_auto(m, off) local(k) ( k = 1; loop(63, m - 2 * floor(m / 2) ? ui_auto(k + off); m = floor(m / 2); k += 1; ); ); #ui_man = "gfx-duplex-q"; // the ? pop-up links to this manual q_RSP = 30000; q_QT = 30400; // Butterworth section Qs: order 2, 3 (+ one pole), 4, 6, 8 q_QT[0] = 0.70711; q_QT[1] = 1.0; q_QT[2] = 0.54120; q_QT[3] = 1.30656; q_QT[4] = 0.51764; q_QT[5] = 0.70711; q_QT[6] = 1.93185; q_QT[7] = 0.50980; q_QT[8] = 0.60134; q_QT[9] = 0.89998; q_QT[10] = 2.56292; // first-order sections (bilinear): high-pass / low-pass, and the gentle tilt shelf function q_op_run(x) instance(n0, n1, d1, x1, y1) local(y) ( y = n0 * x + n1 * x1 - d1 * y1; x1 = x; y1 = y; y; ); function q_op_hp(f) instance(n0, n1, d1) local(k) ( k = tan($pi * min(f, srate * 0.45) / srate); n0 = 1 / (1 + k); n1 = -n0; d1 = (k - 1) / (k + 1); ); function q_op_lp(f) instance(n0, n1, d1) local(k) ( k = tan($pi * min(f, srate * 0.45) / srate); n0 = k / (1 + k); n1 = n0; d1 = (k - 1) / (k + 1); ); function q_op_tilt(f, gdb) instance(n0, n1, d1) local(k, a, b, q_d0) ( k = tan($pi * min(f, srate * 0.45) / srate); a = 10 ^ (gdb / 40); b = 1 / a; // first-order high shelf, gain gdb q_d0 = b / k + 1; n0 = (a / k + 1) / q_d0 / a; n1 = (1 - a / k) / q_d0 / a; d1 = (1 - b / k) / q_d0; // / a: pivots round 0 dB ); function q_op_db(f) instance(n0, n1, d1) local(w, c1, nu, de) ( w = 2 * $pi * f / srate; c1 = cos(w); nu = n0 * n0 + n1 * n1 + 2 * n0 * n1 * c1; de = 1 + d1 * d1 + 2 * d1 * c1; 10 * log10(max(nu, 0.000000000001) / max(de, 0.000000000001)); ); // a Butterworth filter of order n (1, 2, 3, 4, 6, 8): up to four biquads + one pole function q_bw_set(type, f, n) instance(q_nb, q_odd, q0) ( q_odd = n == 1 || n == 3; q_nb = n == 1 ? 0 : n == 2 || n == 3 ? 1 : n == 4 ? 2 : n == 6 ? 3 : 4; q0 = n == 2 ? 0 : n == 3 ? 1 : n == 4 ? 2 : n == 6 ? 4 : 7; q_odd ? ( type ? this.p.q_op_hp(f) : this.p.q_op_lp(f); ); q_nb > 0 ? this.s1.bq_set(type, f, q_QT[q0], 0); q_nb > 1 ? this.s2.bq_set(type, f, q_QT[q0 + 1], 0); q_nb > 2 ? this.s3.bq_set(type, f, q_QT[q0 + 2], 0); q_nb > 3 ? this.s4.bq_set(type, f, q_QT[q0 + 3], 0); ); function q_bw_run(x) instance(q_nb, q_odd) ( q_odd ? x = this.p.q_op_run(x); q_nb > 0 ? x = this.s1.bq_run(x); q_nb > 1 ? x = this.s2.bq_run(x); q_nb > 2 ? x = this.s3.bq_run(x); q_nb > 3 ? x = this.s4.bq_run(x); x; ); function q_bw_db(f) instance(q_nb, q_odd) local(d) ( d = 0; q_odd ? d += this.p.q_op_db(f); q_nb > 0 ? d += this.s1.bq_db(f); q_nb > 1 ? d += this.s2.bq_db(f); q_nb > 2 ? d += this.s3.bq_db(f); q_nb > 3 ? d += this.s4.bq_db(f); d; ); function q_ordr(i) ( i == 0 ? 1 : i == 1 ? 2 : i == 2 ? 3 : i == 3 ? 4 : i == 4 ? 6 : 8 ); // the tilt: gentle = first-order shelf, steep = a biquad shelf, both pivoting round 0 dB function q_tl_set() instance() ( q_steep ? this.b.bq_set(3, slider35, 0.707, 2 * slider34) : this.o.q_op_tilt(slider35, 2 * slider34); ); function q_tl_run(x) instance() ( q_steep ? this.b.bq_run(x) * q_tl_comp : this.o.q_op_run(x); ); function q_tl_db(f) instance() ( q_steep ? this.b.bq_db(f) - slider34 : this.o.q_op_db(f); ); function q_update() local(b, q_cs) ( q_cs = 0; b = 1; loop(10, q_cs += slider((b) + 33) * (b * 7.31 + 1); b += 1; ); q_cs != q_last_cs ? ( q_redraw = 1; q_last_cs = q_cs; ); q_steep = slider36; q_tl_comp = 10 ^ (-slider34 / 20); q_tl_on = abs(slider34) > 0.04; q_TLL.q_tl_set(); q_TLR.q_tl_set(); q_hp_on = slider37 > 10.5; q_lp_on = slider39 < 21990; q_hp_on ? ( q_HPL.q_bw_set(1, slider37, q_ordr(slider38)); q_HPR.q_bw_set(1, slider37, q_ordr(slider38)); ); q_lp_on ? ( q_LPL.q_bw_set(0, slider39, q_ordr(slider40)); q_LPR.q_bw_set(0, slider39, q_ordr(slider40)); ); q_tmode = slider41; q_og = 10 ^ (slider42 / 20); q_eq_on = slider43; ); // ---------- presets (leave INPUT and OUTPUT alone) ---------- function q_pt(t, q_pv, sh, hp, q_hs, lp, q_ls, q_md) ( slider34 = t; slider35 = q_pv; slider36 = sh; slider37 = hp; slider38 = q_hs; slider39 = lp; slider40 = q_ls; slider41 = q_md; slider42 = 0; slider43 = 1; ); function q_load_preset(p) ( p == 1 ? q_pt(1.5, 1000, 0, 25, 3, 22000, 1, 0) : // Mix Bus Lift p == 2 ? q_pt(-1.5, 800, 0, 10, 1, 18000, 1, 0) : // Warm It Up p == 3 ? q_pt(3, 1500, 1, 80, 2, 22000, 1, 0) : // Vocal Forward p == 4 ? q_pt(-3, 600, 0, 30, 3, 9000, 1, 0) : // Dark & Thick p == 5 ? q_pt(2, 700, 0, 10, 1, 22000, 1, 2) : // Bright Sides p == 6 ? q_pt(-2, 2000, 1, 10, 1, 22000, 1, 2) : // Tame the Sides p == 7 ? q_pt(0, 800, 0, 300, 3, 3000, 3, 0) : // Telephone Band p == 8 ? q_pt(2.5, 400, 0, 120, 5, 22000, 1, 0) : // Thin Out (steep HPF) p == 9 ? q_pt(-1, 1200, 0, 20, 3, 14000, 2, 1) : // Vintage Mid Roll-off p == 10 ? q_pt(0, 800, 0, 30, 4, 20000, 4, 0); // Clean Up (filters only) p >= 1 && p <= 10 ? cb_auto(1023, 33); // sliders 1-10 p == 11 ? ( // Reset all to defaults q_pt(0, 800, 0, 10, 1, 22000, 1, 0); slider26 = 0; slider27 = 1; slider28 = 0; slider29 = 30; slider30 = 120; slider31 = 6000; slider32 = -0.3; slider33 = 1; cb_auto(262143, 33); ); ); q_tlook = 1; q_last_cs = -1; // new instances: HORIZON look, hardware panel q_WIN = 0x161412; q_GOLD = 0xE2B24A; q_SKY = 0x7EA6CB; q_HAZE = 0xCFD9DD; q_SEA = 0x617487; q_SAND = 0xA48D70; q_WBAND = 0xFAF7EF; q_update(); function q_proc(cb_inl, cb_inr) ( q_xl = cb_inl; q_xr = cb_inr; q_eq_on ? ( q_hp_on ? ( q_xl = q_HPL.q_bw_run(q_xl); q_xr = q_HPR.q_bw_run(q_xr); ); q_lp_on ? ( q_xl = q_LPL.q_bw_run(q_xl); q_xr = q_LPR.q_bw_run(q_xr); ); q_tl_on ? ( q_tmode == 0 ? ( q_xl = q_TLL.q_tl_run(q_xl); q_xr = q_TLR.q_tl_run(q_xr); ) : ( q_tm_ = (q_xl + q_xr) * 0.5; q_ts_ = (q_xl - q_xr) * 0.5; q_tmode == 1 ? q_tm_ = q_TLL.q_tl_run(q_tm_) : q_ts_ = q_TLR.q_tl_run(q_ts_); q_xl = q_tm_ + q_ts_; q_xr = q_tm_ - q_ts_; ); ); ); q_xl *= q_og; q_xr *= q_og; q_oL = (q_xl); q_oR = (q_xr); q_out_l = q_oL; q_out_r = q_oR; ); q_ord = slider44; GANG = 60000; // LINK ganging: last seen values of the two channels ui_dflt = 1; // theme defaults above apply once: @init re-runs (playback start, device reset) must not undo the saved theme @slider q_update(); q_ord = slider44; update(); @block q_update(); q_ord = slider44; update(); @serialize file_var(0, ui_sz); file_avail(0) != 0 ? file_var(0, pr_cur); // the preset in use (older projects end before it) @sample in_process(spl0, spl1); !q_ord ? ( q_proc(in_l, in_r); in_l = q_out_l; in_r = q_out_r; ); // EQ first dl = in_l; dr = in_r; mode == 2 ? ( x1 = 0.5 * (dl + dr); x2 = 0.5 * (dl - dr); ) : ( x1 = dl; x2 = dr; ); // path 1 runs on channel A; path 2 on channel B (or A again in STEREO) mode == 0 ? ( v1 = P1.p_det(x1, cA.de); v2 = P2.p_det(x2, cA.de); ) : ( v1 = P1.p_det(x1, cA.de); v2 = P2.p_det(x2, cB.de); ); linked ? ( v1 = v2 = max(v1, v2); ); P1.p_gain(v1, cA.on, cA.thr, cA.sl, cA.ac, cA.rc, cA.aut); y1 = P1.p_apply(x1, cA.on, cA.mk, cA.de, cA.spl); mode == 0 ? ( P2.p_gain(v2, cA.on, cA.thr, cA.sl, cA.ac, cA.rc, cA.aut); y2 = P2.p_apply(x2, cA.on, cA.mk, cA.de, cA.spl); g1 = P1.p_limt(y1, cA.lon && cA.on, cA.ltg); g2 = P2.p_limt(y2, cA.lon && cA.on, cA.ltg); ) : ( P2.p_gain(v2, cB.on, cB.thr, cB.sl, cB.ac, cB.rc, cB.aut); y2 = P2.p_apply(x2, cB.on, cB.mk, cB.de, cB.spl); g1 = P1.p_limt(y1, cA.lon && cA.on, cA.ltg); g2 = P2.p_limt(y2, cB.lon && cB.on, cB.ltg); ); linked ? ( g1 = g2 = min(g1, g2); ); y1 *= g1; y2 *= g2; mode == 2 ? ( wl = y1 + y2; wr = y1 - y2; ) : ( wl = y1; wr = y2; ); gm1 = min(gm1, P1.e + 20 * log10(g1)); gm2 = min(gm2, P2.e + 20 * log10(g2)); lm1 = max(lm1, g1 < 0.999); lm2 = max(lm2, g2 < 0.999); // parallel mix + output stage mix_s += (mix_t - mix_s) * sm; oL = dl + (wl - dl) * mix_s; oR = dr + (wr - dr) * mix_s; q_ord ? ( q_proc(oL, oR); oL = q_out_l; oR = q_out_r; ); // EQ after oL = ostL.ost_band(oL * in_comp); oR = ostR.ost_band(oR * in_comp); ost_out(oL, oR); spl0 = ost_l; spl1 = ost_r; pkI = max(pkI, max(abs(dl), abs(dr))); pkO = max(pkO, max(abs(spl0), abs(spl1))); @gfx 900 512 ui_begin(900, 512); ui_swb(); // drawn off-screen, then shown with the units in signal order (ORDER) dy_ink = INKD; dy_bg = PNL; dy_kst = 3; dy_nov = 1; // values float only while a knob is touched function hz_txt(dst, f) ( f >= 1000 ? sprintf(dst, "%.1f kHz", f / 1000) : sprintf(dst, "%.0f Hz", f); ); // one channel (unscaled x = the section's left edge) function chan(x, base, k, name, role) local(r, gr) ( pk_box((x - 2) * sc, 72 * sc, 328 * sc, 252 * sc, GRND, ""); ui_col(GRN); gfx_rect((x - 2) * sc, 72 * sc, 328 * sc, 22 * sc); gfx_setfont(6, "Trebuchet MS", 14 * sc, 'b'); ui_col(0xF2F2EE); gfx_x = (x + 6) * sc; gfx_y = 75 * sc; gfx_drawstr(name); gfx_setfont(4); ui_cola(0xF2F2EE, 0.8); gfx_x = (x + 120) * sc; gfx_y = 79 * sc; gfx_drawstr(role); pk_ledbtn((x + 282) * sc, 98 * sc, 30 * sc, 22 * sc, slider(base), dy_grn) ? ( slider(base) = 1 - slider(base); ui_auto(base); ); dy_lbl(x + 266, 104, "IN", 0.8); // compressor row sprintf(#v, "%.1f dB", slider(base + 1)); dy_knob(x + 32, 150, 18, base + 1, -40, 0, -20, 0, CAP_C, "THRESH", #v); r = slider(base + 2); r >= 19.95 ? strcpy(#v, "INF:1") : sprintf(#v, "%.1f:1", r); dy_knob(x + 96, 150, 18, base + 2, 1, 20, 3, 1, CAP_C, "RATIO", #v); slider(base + 5) ? strcpy(#v, "AUTO") : sprintf(#v, "%.1f ms", slider(base + 3)); dy_knob(x + 160, 150, 18, base + 3, 0.1, 50, 5, 1, CAP_C, "ATTACK", #v); slider(base + 5) ? strcpy(#v, "AUTO") : sprintf(#v, "%.0f ms", slider(base + 4)); dy_knob(x + 224, 150, 18, base + 4, 20, 2000, 200, 1, CAP_C, "RELEASE", #v); sprintf(#v, "%+.1f dB", slider(base + 6)); dy_knob(x + 288, 150, 18, base + 6, -10, 20, 0, 0, CAP_G, "GAIN", #v); // de-ess + limit row hz_txt(#v, slider(base + 8)); dy_knob(x + 32, 240, 18, base + 8, 2000, 12000, 6000, 1, CAP_D, "DE-ESS", #v); slider(base + 10) < -0.05 ? sprintf(#v, "%.1f dB", slider(base + 10)) : strcpy(#v, "OFF"); dy_knob(x + 96, 240, 18, base + 10, -24, 0, 0, 0, CAP_L, "LIMIT", #v); pk_led((x + 124) * sc, 220 * sc, 3.5 * sc, k == 0 ? lmd1 : lmd2, dy_red); dy_btn(x + 150, 220, slider(base + 5), dy_amb, "AUTO") ? ( slider(base + 5) = 1 - slider(base + 5); ui_auto(base + 5); ); dy_btn(x + 200, 220, slider(base + 7), CAP_D, "DE-ESS") ? ( slider(base + 7) = 1 - slider(base + 7); ui_auto(base + 7); ); dy_btn(x + 250, 220, slider(base + 9), dy_grn, "SPLIT") ? ( slider(base + 9) = 1 - slider(base + 9); ui_auto(base + 9); ); ); // ---------- panel ---------- fh = 504; ui_col(0x0B0C0D); gfx_rect(0, 0, gfx_w, gfx_h); pk_chassis(8 * sc, 4 * sc, 884 * sc, fh * sc, PNL); ui_col(dy_ink); 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", 24 * sc, 'b'); ui_col(dy_ink); gfx_x = 62 * sc; gfx_y = 20 * sc; gfx_drawstr("DUPLEX Q"); gfx_setfont(4); ui_cola(dy_ink, 0.6); gfx_x = 196 * sc; gfx_y = 30 * sc; gfx_drawstr("COMP - DE-ESS - LIMIT"); pk_pextra = "Reset all to defaults"; // the house preset display; click the LCD for the list k_ = pk_preset(482 * sc, 12 * sc, 334 * sc, 40 * sc, pr_cur, "Gentle Stereo Bus|Vocal Compressor|De-esser (split)|Broadcast Voice|Dual Mono Pair|M-S Glue|M-S Widen + Tame Sides|Brickwall Safety|Parallel Drums|Bass Leveller"); k_ > 0 ? ( load_preset(k_); pr_cur = k_ > 10 ? 0 : k_; ); ui_size_picker(828 * sc, 22 * sc); // ORDER: in the top bar; the unit the signal meets first is shown on top ui_orderkey(336 * sc, 20 * sc, 134 * sc, 26 * sc, 44, "COMPRESSOR > EQ", "EQ > COMPRESSOR", 4); ui_col(GRN); gfx_rect(14 * sc, 60 * sc, 872 * sc, 4 * sc); ui_col(INKD); gfx_rect(14 * sc, 64 * sc, 872 * sc, 1 * sc); ui_swu(slider44 ? 492 - 327 : 0); lmd1 = lm1; lmd2 = lm2; lm1 = 0; lm2 = 0; gd1 = min(gm1, gd1 * 0.82); gd2 = min(gm2, gd2 * 0.82); gm1 = 0; gm2 = 0; chan(20, 1, 0, "CHANNEL A", slider23 == 0 ? "BOTH SIDES" : slider23 == 1 ? "LEFT" : "MID"); dy_grrow(54, 300, gd1); slider23 == 0 ? ( pk_box(352 * sc, 72 * sc, 328 * sc, 252 * sc, GRND, ""); ui_col(GRN); gfx_rect(352 * sc, 72 * sc, 328 * sc, 22 * sc); gfx_setfont(6, "Trebuchet MS", 14 * sc, 'b'); ui_col(0xF2F2EE); gfx_x = 360 * sc; gfx_y = 75 * sc; gfx_drawstr("CHANNEL B"); dy_lbl(516, 170, "STEREO MODE", 0.8); dy_lbl(516, 186, "CHANNEL A RUNS BOTH SIDES", 0.5); dy_lbl(516, 202, "(switch MODE to DUAL or M-S to use B)", 0.4); pk_led(500 * sc, 230 * sc, 3.5 * sc, lmd2, dy_red); ) : ( chan(354, 12, 1, "CHANNEL B", slider23 == 1 ? "RIGHT" : "SIDE"); ); dy_grrow(388, 300, gd2); // ---------- MASTER ---------- pk_box(686 * sc, 72 * sc, 196 * sc, 252 * sc, GRND, ""); ui_col(GRN); gfx_rect(686 * sc, 72 * sc, 196 * sc, 22 * sc); gfx_setfont(6, "Trebuchet MS", 14 * sc, 'b'); ui_col(0xF2F2EE); gfx_x = 694 * sc; gfx_y = 75 * sc; gfx_drawstr("MASTER"); pk_ink = dy_ink; pk_bg = PNL; pk_nut = 1; pk_rsc = 0.85; pk_rotsel(786 * sc, 162 * sc, 23, "STEREO", "DUAL", "M-S"); dy_lbl(786, 196, "MODE", 0.9); // LINK: always on in STEREO (one detector runs both sides), so it stays lit there and only switches in DUAL / M-S dy_btn(706, 216, slider23 == 0 || slider24, dy_grn, "LINK") && slider23 != 0 ? ( slider24 = 1 - slider24; ui_auto(24); ); sprintf(#v, "WET %.0f", slider25); dy_knob(836, 234, 18, 25, 0, 100, 100, 0, CAP_G, "MIX", #v); mI.ui_ledm(712 * sc, 296 * sc, 120 * sc, 9 * sc, pkI); mO.ui_ledm(712 * sc, 309 * sc, 120 * sc, 9 * sc, pkO); dy_lbl(700, 295, "IN", 0.7); dy_lbl(700, 308, "OUT", 0.7); pkI *= 0.85; pkO *= 0.85; // ================= the EQ strip ================= ui_swu(slider44 ? 70 - 340 : 0); // (Duplex's colours) a green-titled box like its channels: the Tilt EQ engine, left to right HIGH-PASS / TILT / LOW-PASS pk_box(14 * sc, 344 * sc, 872 * sc, 146 * sc, GRND, ""); ui_col(GRN); gfx_rect(14 * sc, 344 * sc, 872 * sc, 22 * sc); gfx_setfont(6, "Trebuchet MS", 14 * sc, 'b'); ui_col(0xF2F2EE); gfx_x = 22 * sc; gfx_y = 347 * sc; gfx_drawstr("TILT EQ"); function qhz(f) ( f >= 1000 ? sprintf(#qv, "%.1f kHz", f / 1000) : sprintf(#qv, "%.0f Hz", f); #qv; ); function qslope(k) ( k == 0 ? "6 dB" : k == 1 ? "12 dB" : k == 2 ? "18 dB" : k == 3 ? "24 dB" : k == 4 ? "36 dB" : "48 dB"; ); // a section heading with its accent colour function qsec(x0, x1, s, col) ( ui_col(col); gfx_rect(x0 * sc, 376 * sc, (x1 - x0) * sc, 2 * sc); dy_lbl((x0 + x1) * 0.5, 381, s, 0.85); ); dy_btn(30, 392, slider43, dy_grn, "IN") ? ( slider43 = 1 - slider43; ui_auto(43); ); qsec(92, 246, "HIGH-PASS", 0xE8402E); slider37 <= 10.5 ? strcpy(#v, "OFF") : strcpy(#v, qhz(slider37)); dy_kf = 1; dy_knob(130, 432, 18, 37, 10, 1000, 10, 1, 0xE8402E, "FREQ", #v); // house style: HPF cap yellow dy_step = 6; dy_knob(206, 432, 15, 38, 0, 5, 1, 0, 0xE8402E, "SLOPE", qslope(slider38)); dy_step = 0; qsec(262, 498, "TILT", 0xF2F2EE); sprintf(#v, "%+.1f dB", slider34); dy_knob(304, 432, 20, 34, -6, 6, 0, 0, 0xF2F2EE, "TILT", #v); dy_knob(380, 432, 16, 35, 100, 5000, 800, 1, 0xF2F2EE, "PIVOT", qhz(slider35)); dy_btn(426, 394, slider36 == 0, dy_amb, "GENTLE") ? ( slider36 = 0; ui_auto(36); ); dy_btn(426, 436, slider36 == 1, dy_amb, "STEEP") ? ( slider36 = 1; ui_auto(36); ); qsec(514, 668, "LOW-PASS", 0x2FC46A); slider39 >= 21990 ? strcpy(#v, "OFF") : strcpy(#v, qhz(slider39)); dy_knob(552, 432, 18, 39, 1000, 22000, 22000, 1, 0x2FC46A, "FREQ", #v); dy_step = 6; dy_knob(628, 432, 15, 40, 0, 5, 1, 0, 0x2FC46A, "SLOPE", qslope(slider40)); dy_step = 0; ui_cola(dy_ink, 0.2); gfx_rect(684 * sc, 378 * sc, 1 * sc, 96 * sc); dy_lbl(752, 381, "TILT ON", 0.7); dy_btn(696, 396, slider41 == 0, dy_grn, "ST") ? ( slider41 = 0; ui_auto(41); ); dy_btn(736, 396, slider41 == 1, dy_grn, "MID") ? ( slider41 = 1; ui_auto(41); ); dy_btn(776, 396, slider41 == 2, dy_grn, "SIDE") ? ( slider41 = 2; ui_auto(41); ); sprintf(#v, "%+.1f dB", slider42); dy_knob(850, 432, 15, 42, -12, 12, 0, 0, 0xF2F2EE, "OUTPUT", #v); // LINK (DUAL / M-S): the two channels' controls are ganged - turning one moves the other by the same amount // (dB controls by the same dB, times by the same factor), switches copy across; offsets set while unlinked stay. function gang_val(k, v) ( k == 1 ? min(max(v, -40), 0) : k == 2 ? min(max(v, 1), 20) : k == 3 ? min(max(v, 0.1), 50) : k == 4 ? min(max(v, 20), 2000) : k == 6 ? min(max(v, -10), 20) : k == 8 ? min(max(v, 2000), 12000) : min(max(v, -24), 0); ); function gang() local(k, a, b, pa, pb, mul, nv) ( slider24 && slider23 != 0 && gang_ok ? ( k = 0; loop(11, a = slider(1 + k); b = slider(12 + k); pa = GANG[k]; pb = GANG[16 + k]; mul = k == 3 || k == 4 || k == 8; a != pa && b == pb ? ( nv = k == 0 || k == 5 || k == 7 || k == 9 ? a : gang_val(k, mul ? b * a / max(pa, 0.0001) : b + a - pa); slider(12 + k) = nv; slider_automate(2 ^ (11 + k)); ) : b != pb && a == pa ? ( nv = k == 0 || k == 5 || k == 7 || k == 9 ? b : gang_val(k, mul ? a * b / max(pb, 0.0001) : a + b - pb); slider(1 + k) = nv; slider_automate(2 ^ k); ); k += 1; ); ); k = 0; loop(11, GANG[k] = slider(1 + k); GANG[16 + k] = slider(12 + k); k += 1; ); gang_ok = 1; ); gang(); ui_swe(12, 888, 70, 327, 340, 494, slider44); pk_end(); ui_end();