desc:GFX Master EQ //tags: equaliser eq mastering dynamic eq mid side shelf air //author: JGWizrad / Garden FX (GFX) // // Requires gfx-drift, gfx-input, gfx-output, gfx-ui, gfx-panel, gfx-instrument and gfx-headsmooth (.jsfx-inc) // in the same folder. // // Master EQ - a mastering equaliser with three dynamic bands (personal use; designed from scratch). // LOW CUT / HIGH CUT - 12 dB / octave (OFF = out of circuit). // LOW SHELF - 30 / 60 / 100 / 200 Hz, +/-6 dB. BROAD: a gentle first-order (Baxandall-type) slope; // TIGHT: a steeper second-order shelf. // DYN 1-3 - bells with a static GAIN plus a one-knob dynamic part, COMP, in the "over easy" style: a // soft knee where the threshold and the ratio move together with the knob. Each band listens to // its own range: below zero it COMPRESSES the band when it gets loud (up to -12 dB), above zero // it EXPANDS it (lifts it as it gets busy, up to +6 dB). 0 = a static bell. Q sets the width. // The light shows the band working. TIMING sets the attack / release for all three. // HIGH SHELF - 2 / 4 / 8 / 12 kHz, +/-6 dB, BROAD / TIGHT. // AIR - a broad lift at 10 / 20 / 40 kHz, 0 - +6 dB. // ST / M / S - every band works on the stereo pair, the mid only or the side only. // Gains move in 0.5 dB detents for recall (Shift = free). // DRIFT: the band frequencies wander very slightly. // The swatch picks the colour theme. The oxblood strip below holds DRIFT (one dial: 50% = the drift settings) and IN / OUT (TRIM + GUARD, SAT, OUT stage). 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-dyn.jsfx-inc import gfx-faceplate.jsfx-inc import gfx-instrument.jsfx-inc import gfx-headsmooth.jsfx-inc slider1:20<20,300,1>-HPF (Hz, 20 = off) slider2:20000<4000,20000,10>-LPF (Hz, 20000 = off) slider3:1<0,3,1{30 Hz,60 Hz,100 Hz,200 Hz}>-Low shelf frequency slider4:0<-6,6,0.5>-Low shelf gain (dB) slider5:0<0,1,1{Broad,Tight}>-Low shelf shape slider6:0<0,2,1{Stereo,Mid,Side}>-Low shelf target slider7:120<20,20000,1>-Dyn 1 frequency (Hz) slider8:0<-6,6,0.5>-Dyn 1 gain (dB) slider9:1<0.4,4,0.01>-Dyn 1 Q slider10:-20<-50,0,0.5>-Dyn 1 (unused - was threshold) slider11:0<-10,10,0.1>-Dyn 1 comp (- compress / + expand) slider12:0<0,2,1{Stereo,Mid,Side}>-Dyn 1 target slider13:1000<20,20000,1>-Dyn 2 frequency (Hz) slider14:0<-6,6,0.5>-Dyn 2 gain (dB) slider15:1<0.4,4,0.01>-Dyn 2 Q slider16:-20<-50,0,0.5>-Dyn 2 (unused - was threshold) slider17:0<-10,10,0.1>-Dyn 2 comp (- compress / + expand) slider18:0<0,2,1{Stereo,Mid,Side}>-Dyn 2 target slider19:4000<20,20000,1>-Dyn 3 frequency (Hz) slider20:0<-6,6,0.5>-Dyn 3 gain (dB) slider21:1<0.4,4,0.01>-Dyn 3 Q slider22:-20<-50,0,0.5>-Dyn 3 (unused - was threshold) slider23:0<-10,10,0.1>-Dyn 3 comp (- compress / + expand) slider24:0<0,2,1{Stereo,Mid,Side}>-Dyn 3 target slider25:2<0,3,1{2 kHz,4 kHz,8 kHz,12 kHz}>-High shelf frequency slider26:0<-6,6,0.5>-High shelf gain (dB) slider27:0<0,1,1{Broad,Tight}>-High shelf shape slider28:0<0,2,1{Stereo,Mid,Side}>-High shelf target slider29:1<0,2,1{10 kHz,20 kHz,40 kHz}>-Air frequency slider30:0<0,6,0.5>-Air gain (dB) slider31:0<0,2,1{Stereo,Mid,Side}>-Air target slider32:1<0,2,1{Fast,Medium,Slow}>-Dynamics timing slider33:0<-12,12,0.1>-Output (dB) slider34:1<0,1,1{Out,In}>-EQ in slider35:0<-24,12,0.1>-Input trim (dB) slider36:1<0,1,1{Off,On}>-Input guard slider37:25<0,100,1>-Drift amount (%) slider38:0.25<0.02,2,0.01>-Drift speed (Hz) slider39:0<0,100,1>-Band sat (%) slider40:30<0,100,1>-Bias (%) slider41:120<40,400,1>-Sat low band (Hz) slider42:6000<2000,16000,10>-Sat high band (Hz) slider43:-0.3<-24,0,0.1>-Ceiling / limit threshold (dB) slider44:1<0,2,1{Off,Clip,Limit}>-Output stage slider45:50<0,100,1>-Drift (%) in_pin:left input in_pin:right input out_pin:left output out_pin:right output @init #ui_man = "gfx-master-eq"; // the ? pop-up links to this manual // ---------- static filters: RBJ biquads + first-order shelves ---------- // type: 0 low-pass, 1 high-pass, 2 peak, 3 high shelf, 4 low shelf; 5 / 6 = first-order low / high shelf function bq_set(type, f, q, gdb) instance(b0, b1, b2, a1, a2) local(w0, cw, sw, al, amp, sa, n, k, g, r) ( f = min(f, srate * 0.45); w0 = 2 * $pi * f / srate; cw = cos(w0); sw = sin(w0); al = sw / (2 * q); amp = 10 ^ (gdb / 40); type == 0 ? ( n = 1 + al; b0 = (1 - cw) * 0.5 / n; b1 = (1 - cw) / n; b2 = b0; a1 = -2 * cw / n; a2 = (1 - al) / n; ) : type == 1 ? ( n = 1 + al; b0 = (1 + cw) * 0.5 / n; b1 = -(1 + cw) / n; b2 = b0; a1 = -2 * cw / n; a2 = (1 - al) / n; ) : type == 2 ? ( n = 1 + al / amp; b0 = (1 + al * amp) / n; b1 = -2 * cw / n; b2 = (1 - al * amp) / n; a1 = b1; a2 = (1 - al / amp) / n; ) : type == 3 ? ( sa = 2 * sqrt(amp) * al; n = (amp + 1) - (amp - 1) * cw + sa; b0 = amp * ((amp + 1) + (amp - 1) * cw + sa) / n; b1 = -2 * amp * ((amp - 1) + (amp + 1) * cw) / n; b2 = amp * ((amp + 1) + (amp - 1) * cw - sa) / n; a1 = 2 * ((amp - 1) - (amp + 1) * cw) / n; a2 = ((amp + 1) - (amp - 1) * cw - sa) / n; ) : type == 4 ? ( sa = 2 * sqrt(amp) * al; n = (amp + 1) + (amp - 1) * cw + sa; b0 = amp * ((amp + 1) - (amp - 1) * cw + sa) / n; b1 = 2 * amp * ((amp - 1) - (amp + 1) * cw) / n; b2 = amp * ((amp + 1) - (amp - 1) * cw - sa) / n; a1 = -2 * ((amp - 1) + (amp + 1) * cw) / n; a2 = ((amp + 1) + (amp - 1) * cw - sa) / n; ) : ( // first-order shelves, half-way (in dB) at f k = tan($pi * f / srate); g = 10 ^ (gdb / 20); r = sqrt(g); b2 = 0; a2 = 0; type == 6 ? ( n = 1 / k + r; b0 = g * (1 / k + 1 / r) / n; b1 = g * (-1 / k + 1 / r) / n; a1 = (-1 / k + r) / n; ) : ( n = 1 / k + 1 / r; b0 = (1 / k + r) / n; b1 = (-1 / k + r) / n; a1 = (-1 / k + 1 / r) / n; ); ); ); function bq_thru() instance(b0, b1, b2, a1, a2) ( b0 = 1; b1 = 0; b2 = 0; a1 = 0; a2 = 0; ); function bq_run(x) instance(b0, b1, b2, a1, a2, x1, x2, y1, y2) local(y) ( y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; x2 = x1; x1 = x; y2 = y1; y1 = y; y; ); // ---------- dynamic bell (allpass-based, after Zolzer) ---------- // y = x + H0/2 * (x - A(x)), H0 = V0 - 1: the gain can change every sample with no recalculation. // (x - A(x)) / 2 is a unity-peak band-pass at the band: it is also the detector signal. function ap_set(f, q, gdb) instance(c, d) local(t, v) ( f = min(f, srate * 0.45); // the bandwidth (f / Q) must stay below Nyquist: past it tan() wraps and the filter goes unstable // (a wide Q on a high band, e.g. 12 kHz at Q 0.4 = 30 kHz wide) t = tan($pi * min(f / q, srate * 0.45) / srate); v = 10 ^ (gdb / 20); c = gdb >= 0 ? (t - 1) / (t + 1) : (t - v) / (t + v); d = -cos(2 * $pi * f / srate); ); function ap_run(x) instance(c, d, x1, x2, y1, y2) local(y) ( y = -c * x + d * (1 - c) * x1 + x2 - d * (1 - c) * y1 + c * y2; x2 = x1; x1 = x; y2 = y1; y1 = y; y; ); // ---------- one band: two channels (L / R or M / S) ---------- // tgt: 0 stereo, 1 mid, 2 side. Works on the globals bl / br. function st_band() instance(fa, fb, tg) local(m, s) ( tg == 0 ? ( bl = fa.bq_run(bl); br = fb.bq_run(br); ) : ( m = (bl + br) * 0.5; s = (bl - br) * 0.5; tg == 1 ? m = fa.bq_run(m) : s = fb.bq_run(s); bl = m + s; br = m - s; ); ); // dynamic band: env = band energy (linked), gdb = static + dynamic gain (smoothed by the envelope) function dy_band() instance(pa, pb, tg, env, gdb, gs, cmp, sta, grs) local(m, s, ua, ub, ya, yb, e, lv, o, h0, a, thr, rat, y, gr) ( tg == 0 ? ( ua = bl; ub = br; ) : ( m = (bl + br) * 0.5; s = (bl - br) * 0.5; ua = m; ub = s; ); h0 = 10 ^ (gs / 20) - 1; e = 0; tg != 2 ? ( ya = pa.ap_run(ua); e = abs((ua - ya) * 0.5); ua += h0 * 0.5 * (ua - ya); ); tg != 1 ? ( yb = pb.ap_run(ub); e = max(e, abs((ub - yb) * 0.5)); ub += h0 * 0.5 * (ub - yb); ); e *= e; env += (e - env) * (e > env ? dy_att : dy_rel); lv = 10 * log10(max(env * 2, 0.000000000001)); // sine-equivalent level of the band, dBFS // COMP (-1..1): the threshold falls and the ratio rises with the knob; a 12 dB soft knee ("over easy") a = abs(cmp); thr = -12 - 24 * a; rat = 1 + 3 * a; o = lv - thr; y = o <= -6 ? 0 : o >= 6 ? o : sqr(o + 6) / 24; gr = y * (1 - 1 / rat); gdb = sta + (cmp < 0 ? -min(gr, 12) : min(gr, 6)); gs += (gdb - gs) * 0.02; // light de-zipper on top of the envelope grs = max(grs, abs(gs - sta)); tg == 0 ? ( bl = ua; br = ub; ) : ( bl = ua + ub; br = ua - ub; ); ); // ---------- presets (the EQ and dynamics sections; trim, output, drift and the output stage are left alone) ---------- function pf(lc, hc) ( slider1 = lc; slider2 = hc; ); function pls(f, g, sh, t) ( slider3 = f; slider4 = g; slider5 = sh; slider6 = t; ); // (the old threshold / range values map onto COMP: range x 2.5, the threshold is now set by the knob) function pd(i, f, g, q, thr, rng, t) local(b) ( b = 7 + i * 6; slider(b) = f; slider(b + 1) = g; slider(b + 2) = q; slider(b + 3) = -20; slider(b + 4) = max(min(rng * 2.5, 10), -10); slider(b + 5) = t; ); function phs(f, g, sh, t) ( slider25 = f; slider26 = g; slider27 = sh; slider28 = t; ); function pair(f, g, t) ( slider29 = f; slider30 = g; slider31 = t; ); function load_preset(p) ( p > 0 ? ( // a clean slate first: everything flat, the dynamic bands at rest pf(20, 20000); pls(1, 0, 0, 0); pd(0, 120, 0, 1, -20, 0, 0); pd(1, 1000, 0, 1, -20, 0, 0); pd(2, 4000, 0, 1, -20, 0, 0); phs(2, 0, 0, 0); pair(1, 0, 0); slider32 = 1; slider34 = 1; p == 1 ? ( // GENTLE POLISH: a touch of weight and air, low mids and harshness held in check pf(25, 20000); pls(1, 1, 0, 0); pd(0, 220, 0, 1, -24, -1.5, 0); pd(1, 3000, 0, 1.2, -22, -1.5, 0); pd(2, 7000, 0, 2, -26, -2, 0); phs(3, 1, 0, 0); pair(1, 1.5, 0); ) : p == 2 ? ( // WARM & FULL pls(2, 2, 0, 0); pd(0, 300, 0, 0.8, -22, -2, 0); pd(2, 5000, 0, 1.5, -24, -1.5, 0); phs(2, -0.5, 0, 0); ) : p == 3 ? ( // BRIGHT & OPEN pd(0, 250, -1, 0.8, -20, 0, 0); pd(1, 2500, 0, 1.2, -22, -2, 0); pd(2, 6500, 0, 2, -24, -2.5, 0); phs(2, 2, 0, 0); pair(1, 2.5, 0); ) : p == 4 ? ( // LOW-END CONTROL: tighter bass, narrower lows pf(30, 20000); pls(2, -3, 1, 2); pd(0, 110, 0, 1, -20, -3, 1); pd(1, 300, 0, 1, -22, -2, 0); slider32 = 0; ) : p == 5 ? ( // VOCAL FORWARD: presence in the middle of the image, sibilance tamed pd(0, 250, -1, 0.9, -20, 0, 1); pd(1, 2000, 1.5, 0.8, -20, 0, 1); pd(2, 6000, 0, 2, -22, -2.5, 1); pair(1, 1, 0); ) : p == 6 ? ( // DE-HARSH pd(1, 3200, 0, 1.5, -26, -3, 0); pd(2, 8000, 0, 2, -28, -3, 0); ) : p == 7 ? ( // WIDE AIR: lift the sides on top, keep the lows centred pls(2, -2, 1, 2); phs(2, 1, 0, 2); pair(1, 3, 2); ) : p == 8 ? ( // LOUD MODERN pf(30, 20000); pls(1, 1.5, 1, 0); pd(0, 90, 0, 1.4, -18, -2, 0); pd(1, 400, -1, 0.7, -20, 0, 0); pd(2, 3500, 0, 1.2, -20, -1.5, 0); phs(3, 1.5, 1, 0); pair(1, 2, 0); slider32 = 0; ) : p == 9 ? ( // VINTAGE TAPE: round lows, softened top pf(20, 16000); pls(2, 1.5, 0, 0); phs(3, -1, 0, 0); ) : p == 10 ? ( // ACOUSTIC / CLASSICAL: barely there pls(0, 0.5, 0, 0); pd(0, 250, 0, 0.8, -26, -1, 0); phs(3, 0.5, 0, 0); pair(2, 1, 0); slider32 = 2; ); // p == 11: reset = the clean slate above slider_automate(4294967295); ); ); lsf = 60; hsf = 8000; arf = 20000; function update() local(i) ( lsf = slider3 == 0 ? 30 : slider3 == 1 ? 60 : slider3 == 2 ? 100 : 200; hsf = slider25 == 0 ? 2000 : slider25 == 1 ? 4000 : slider25 == 2 ? 8000 : 12000; arf = slider29 == 0 ? 10000 : slider29 == 1 ? 20000 : 40000; dy_att = 1 - exp(-1 / ((slider32 == 0 ? 2 : slider32 == 1 ? 10 : 30) * 0.001 * srate)); dy_rel = 1 - exp(-1 / ((slider32 == 0 ? 60 : slider32 == 1 ? 150 : 400) * 0.001 * srate)); bLS.tg = slider6; bHS.tg = slider28; bAR.tg = slider31; d1.tg = slider12; d1.cmp = slider11 / 10 * eq_on_t; d2.tg = slider18; d2.cmp = slider17 / 10 * eq_on_t; d3.tg = slider24; d3.cmp = slider23 / 10 * eq_on_t; out_t = 10 ^ (slider33 / 20); drift = 0; dspd = 0.25; // no drift: a mastering EQ stays where it is put in_setup(slider35, slider36); ost_setup(0, slider40 / 100, slider41, slider42, slider43, slider44); // no band saturation, just the OUT stage ); dF1.drift_init(0.8 + rand(0.5)); dF2.drift_init(0.8 + rand(0.5)); dF3.drift_init(0.8 + rand(0.5)); ost_init(20000); ostL.ost_ch_init(); ostR.ost_ch_init(); in_init(); sm = 1 - exp(-1 / (0.03 * srate)); mpk = 0; !ui_dflt ? ui_hw = 1; !ui_dflt ? ui_kstyle = 0; !ui_dflt ? ui_thm = 13; // default theme: Frost (light); the swatch changes it ME_DKS = 6; // amber-cap knob for COMP eq_on_t = slider34; update(); ls_g = slider4; hs_g = slider26; ar_g = slider30; d1.sta = slider8; d2.sta = slider14; d3.sta = slider20; eq_on = slider34; d1.gs = d1.sta; d2.gs = d2.sta; d3.gs = d3.sta; out_s = out_t; ui_dflt = 1; // theme defaults above apply once: @init re-runs (playback start, device reset) must not undo the saved theme @slider eq_on_t = slider34; update(); @block update(); bs = 1 - exp(-samplesblock / (0.03 * srate)); eq_on += (slider34 - eq_on) * bs; ls_g += (slider4 * eq_on - ls_g) * bs; hs_g += (slider26 * eq_on - hs_g) * bs; ar_g += (slider30 * eq_on - ar_g) * bs; d1.sta += (slider8 * eq_on - d1.sta) * bs; d2.sta += (slider14 * eq_on - d2.sta) * bs; d3.sta += (slider20 * eq_on - d3.sta) * bs; dinc = dspd * samplesblock / srate; f1m = 1 + 0.01 * drift * dF1.drift_tick(dinc); f2m = 1 + 0.01 * drift * dF2.drift_tick(dinc); f3m = 1 + 0.01 * drift * dF3.drift_tick(dinc); // filters (OFF = out of circuit) slider1 <= 20.5 ? ( lcL.bq_thru(); lcR.bq_thru(); ) : ( lcL.bq_set(1, slider1, 0.7071, 0); lcR.bq_set(1, slider1, 0.7071, 0); ); slider2 >= 19990 ? ( hcL.bq_thru(); hcR.bq_thru(); ) : ( hcL.bq_set(0, slider2, 0.7071, 0); hcR.bq_set(0, slider2, 0.7071, 0); ); // shelves and air bLS.fa.bq_set(slider5 ? 4 : 5, lsf, 0.7, ls_g); bLS.fb.bq_set(slider5 ? 4 : 5, lsf, 0.7, ls_g); bHS.fa.bq_set(slider27 ? 3 : 6, hsf, 0.7, hs_g); bHS.fb.bq_set(slider27 ? 3 : 6, hsf, 0.7, hs_g); bAR.fa.bq_set(3, arf, 0.5, ar_g); bAR.fb.bq_set(3, arf, 0.5, ar_g); // dynamic bells: the allpass follows the current gain's sign (cut / boost bandwidth) d1.pa.ap_set(slider7 * f1m, slider9, d1.gs); d1.pb.ap_set(slider7 * f1m, slider9, d1.gs); d2.pa.ap_set(slider13 * f2m, slider15, d2.gs); d2.pb.ap_set(slider13 * f2m, slider15, d2.gs); d3.pa.ap_set(slider19 * f3m, slider21, d3.gs); d3.pb.ap_set(slider19 * f3m, slider21, d3.gs); @serialize file_var(0, ui_sz); file_var(0, ui_thm); file_avail(0) != 0 ? file_var(0, pr_cur); // the preset in use (older projects end before it) @sample in_process(spl0, spl1); bl = hcL.bq_run(lcL.bq_run(in_l)); br = hcR.bq_run(lcR.bq_run(in_r)); bLS.st_band(); d1.dy_band(); d2.dy_band(); d3.dy_band(); bHS.st_band(); bAR.st_band(); out_s += (out_t - out_s) * sm; oL = ostL.ost_band(bl * out_s); oR = ostR.ost_band(br * out_s); ost_out(oL, oR); spl0 = ost_l; spl1 = ost_r; mpk = max(mpk, max(abs(spl0), abs(spl1))); @gfx 780 530 ui_begin(780, 530); ui_theme(ui_thm); ME_PNL = F_BG; ME_INK = F_INK; ME_ACC = C_ACC; // the panel takes the theme's faceplate, ink and accent // pastel group colours, so FREQ, GAIN and Q read apart at a glance P_FRQ = 0x9EC5F0; P_GAN = 0xF4B6C8; P_Q = 0xB4E3BE; ui_col(0x0B0C0D); gfx_rect(0, 0, gfx_w, gfx_h); // ST / M / S keys under a band function me_tgt(cx, y, idx) local(i, x) ( i = 0; loop(3, x = cx - 38 + i * 26; ui_button(x * sc, y * sc, 24 * sc, 18 * sc, i == 0 ? "ST" : i == 1 ? "M" : "S", slider(idx) == i, 1) ? ( slider(idx) = i; slider_automate(2 ^ (idx - 1)); ); i += 1; ); ); // section title with a pinline; lit = the band's activity light (0..1, -1 = none) function me_sec(x0, x1, y, title, lit) local(tw, th, cx) ( cx = (x0 + x1) * 0.5; gfx_setfont(4); gfx_measurestr(title, tw, th); ui_cola(ME_INK, 0.25); gfx_rect((x0 + 6) * sc, (y + 5) * sc, (cx - x0 - 6) * sc - tw * 0.5 - 6 * sc, 1 * sc); gfx_rect(cx * sc + tw * 0.5 + 6 * sc, (y + 5) * sc, (x1 - cx - 6) * sc - tw * 0.5 - 6 * sc, 1 * sc); ui_col(ME_ACC); ui_text(cx * sc, y * sc, title); lit >= 0 ? pk_led(cx * sc + tw * 0.5 + 12 * sc, (y + 5) * sc, 3 * sc, lit > 0.25, ME_ACC); ); // stepped values, after the classic mastering EQs: frequencies on the E12 series (22, 27, 33, 39, 47, 56, 68, 82, // 100, 120 ...), Q in half-octave steps, cut filters on the usual detents function me_e12(v) local(d, m, s, best, bd, i, c) ( d = 10 ^ floor(log10(max(v, 1))); m = v / d; best = 1; bd = 100; i = 0; loop(13, c = i == 0 ? 1 : i == 1 ? 1.2 : i == 2 ? 1.5 : i == 3 ? 1.8 : i == 4 ? 2.2 : i == 5 ? 2.7 : i == 6 ? 3.3 : i == 7 ? 3.9 : i == 8 ? 4.7 : i == 9 ? 5.6 : i == 10 ? 6.8 : i == 11 ? 8.2 : 10; abs(log(m / c)) < bd ? ( bd = abs(log(m / c)); best = c; ); i += 1; ); floor(best * d + 0.5); ); function me_qstep(v) local(best, bd, i, c) ( best = 1; bd = 100; i = 0; loop(8, c = i == 0 ? 0.4 : i == 1 ? 0.5 : i == 2 ? 0.7 : i == 3 ? 1 : i == 4 ? 1.4 : i == 5 ? 2 : i == 6 ? 2.8 : 4; abs(log(v / c)) < bd ? ( bd = abs(log(v / c)); best = c; ); i += 1; ); best; ); function me_lcstep(v) local(best, bd, i, c) ( best = 20; bd = 100; i = 0; loop(12, c = i == 0 ? 20 : i == 1 ? 25 : i == 2 ? 30 : i == 3 ? 40 : i == 4 ? 50 : i == 5 ? 60 : i == 6 ? 80 : i == 7 ? 100 : i == 8 ? 120 : i == 9 ? 150 : i == 10 ? 200 : 300; abs(log(v / c)) < bd ? ( bd = abs(log(v / c)); best = c; ); i += 1; ); best; ); function me_hcstep(v) local(best, bd, i, c) ( best = 20000; bd = 100; i = 0; loop(10, c = i == 0 ? 4000 : i == 1 ? 5000 : i == 2 ? 6000 : i == 3 ? 7000 : i == 4 ? 8000 : i == 5 ? 10000 : i == 6 ? 12000 : i == 7 ? 14000 : i == 8 ? 16000 : 20000; abs(log(v / c)) < bd ? ( bd = abs(log(v / c)); best = c; ); i += 1; ); best; ); // snap a knob's slider to its steps while it is being turned function me_snap(idx, kind) local(v) ( drag_id == idx || (lastclick_id == idx && mouse_wheel == 0) ? ( v = slider(idx); v = kind == 0 ? me_e12(v) : kind == 1 ? me_qstep(v) : kind == 2 ? me_lcstep(v) : me_hcstep(v); v != slider(idx) ? ( slider(idx) = v; ui_auto(idx); ); ); ); // ---------- the visualiser: the summed curve (with the dynamic bands as they move), the bands' centres ---------- // band shapes (dB at frequency f) - a display model, close to the filters function me_bell(f, fc, q, g) local(x) ( x = log(f / fc) / log(2) * q * 1.15; g / (1 + x * x * 2.2); ); function me_shelf(f, fc, g, lo, tight) local(x) ( x = log(f / fc) / log(2) * (tight ? 2.6 : 1.5); lo ? g / (1 + exp(x * 2.2)) : g / (1 + exp(-x * 2.2)); ); function me_resp(f) local(r, g_ls, g_hs, g_ar, g_d1, g_d2, g_d3) ( // Use slider values directly so the UI responds even when transport/audio is stopped g_ls = slider4 * slider34; g_hs = slider26 * slider34; g_ar = slider30 * slider34; // Use actual dynamic gain (.gs) if running, otherwise fall back to static gain slider g_d1 = (d1.gs != 0 ? d1.gs : slider8) * slider34; g_d2 = (d2.gs != 0 ? d2.gs : slider14) * slider34; g_d3 = (d3.gs != 0 ? d3.gs : slider20) * slider34; r = me_shelf(f, lsf, g_ls, 1, slider5) + me_shelf(f, hsf, g_hs, 0, slider27) + me_shelf(f, arf, g_ar * 0.9, 0, 0); r += me_bell(f, slider7, slider9, g_d1) + me_bell(f, slider13, slider15, g_d2) + me_bell(f, slider19, slider21, g_d3); slider1 > 20.5 ? r -= 10 * log10(1 + (slider1 / f) ^ 4); slider2 < 19990 ? r -= 10 * log10(1 + (f / slider2) ^ 4); r; ); function me_vis(x, y, w, h) local(i, f, r, px, py, qx, qy, gx, k, fx) ( ui_col(0x0A0C0E); rd_rr((x - 3) * sc, (y - 3) * sc, (w + 6) * sc, (h + 6) * sc, 5 * sc); ui_col(0x12161B); gfx_rect(x * sc, y * sc, w * sc, h * sc); // grid: 20 Hz - 20 kHz, +-8 dB k = 0; loop(10, f = k == 0 ? 50 : k == 1 ? 100 : k == 2 ? 200 : k == 3 ? 500 : k == 4 ? 1000 : k == 5 ? 2000 : k == 6 ? 5000 : k == 7 ? 10000 : k == 8 ? 20 : 20000; gx = x + log(f / 20) / log(1000) * w; ui_cola(0xFFFFFF, 0.06); gfx_rect(gx * sc, y * sc, 1 * sc, h * sc); k += 1; ); ui_cola(0xFFFFFF, 0.06); gfx_rect(x * sc, (y + h * 0.25) * sc, w * sc, 1 * sc); gfx_rect(x * sc, (y + h * 0.75) * sc, w * sc, 1 * sc); ui_cola(0xFFFFFF, 0.16); gfx_rect(x * sc, (y + h * 0.5) * sc, w * sc, 1 * sc); gfx_setfont(4); ui_cola(0xFFFFFF, 0.35); k = 0; loop(3, f = k == 0 ? 100 : k == 1 ? 1000 : 10000; gx = x + log(f / 20) / log(1000) * w; gfx_x = (gx + 3) * sc; gfx_y = (y + h - 12) * sc; gfx_drawstr(k == 0 ? "100" : k == 1 ? "1k" : "10k"); k += 1; ); gfx_x = (x + 4) * sc; gfx_y = (y + 2) * sc; gfx_drawstr("+8"); gfx_x = (x + 4) * sc; gfx_y = (y + h - 12) * sc; gfx_drawstr("-8"); // the band centres in their pastel colour k = 0; loop(3, fx = k == 0 ? slider7 : k == 1 ? slider13 : slider19; gx = x + log(fx / 20) / log(1000) * w; ui_cola(P_FRQ, 0.35); gfx_rect(gx * sc, y * sc, 1 * sc, h * sc); k += 1; ); // the curve, filled to the 0 dB line i = 0; loop(181, f = 20 * 1000 ^ (i / 180); r = min(max(me_resp(f), -8), 8); qx = (x + i / 180 * w) * sc; qy = (y + h * 0.5 - r / 8 * (h * 0.5 - 4)) * sc; ui_cola(ME_ACC, 0.18); gfx_rect(qx, min(qy, (y + h * 0.5) * sc), max(w / 180 * sc, 1) + 1, abs(qy - (y + h * 0.5) * sc) + 1); i > 0 ? ( ui_col(ME_ACC); gfx_line(px, py, qx, qy, 1); gfx_line(px, py + 1, qx, qy + 1, 1); ); px = qx; py = qy; i += 1; ); !slider34 ? ( ui_cola(0x000000, 0.45); gfx_rect(x * sc, y * sc, w * sc, h * sc); gfx_setfont(2); ui_col(ME_INK); ui_text((x + w * 0.5) * sc, (y + h * 0.5 - 8) * sc, "EQ OUT"); ); ); T = 8; TH = 468 + 46; hs_unit(T, TH, ME_PNL); gfx_gradrect(10 * sc, (T + 2) * sc, 760 * sc, (TH - 4) * sc, 1, 1, 1, 0.035, 0, 0, 0, -0.035 / (760 * sc), 0, 0, 0, 0); hs_badge(40, T + 8, "MASTER EQ", "DYNAMIC MASTERING EQUALISER", 0x1C1D20); ui_theme_picker(530 * sc, (T + 0) * sc); ui_size_picker(560 * sc, (T + 11) * sc); ui_keycap(622 * sc, (T + 8) * sc, 50 * sc, 24 * sc, "EQ IN", slider34, 1) ? ( slider34 = 1 - slider34; slider_automate(2 ^ 33); ); pk_ink = ME_INK; pk_bg = ME_PNL; pk_ptr = 0xEDEFF2; C_INK = ME_INK; // the preset row under the header: PRESETS, prev / next, LCD (click it for the list), at the right pk_pextra = "Reset (flat)"; k_ = pk_preset(356 * sc, (T + 48) * sc, 400 * sc, 30 * sc, pr_cur, "Gentle Polish|Warm & Full|Bright & Open|Low-End Control|Vocal Forward|De-Harsh|Wide Air|Loud Modern|Vintage Tape|Acoustic / Classical"); k_ > 0 ? ( load_preset(k_); pr_cur = k_ > 10 ? 0 : k_; ); me_vis(24, T + 90, 732, 88); // columns: FILTERS 20-110 | LOW 110-200 | (labels) | DYN 1 240-350 | DYN 2 350-460 | DYN 3 460-570 | HIGH 570-670 | AIR 670-760 Y0 = T + 192; me_sec(20, 110, Y0, "IN + FILTERS", -1); me_sec(110, 200, Y0, "LOW", -1); me_sec(240, 350, Y0, "DYN 1", d1.grs / 3); d1.grs = 0; me_sec(350, 460, Y0, "DYN 2", d2.grs / 3); d2.grs = 0; me_sec(460, 570, Y0, "DYN 3", d3.grs / 3); d3.grs = 0; me_sec(570, 670, Y0, "HIGH", -1); me_sec(670, 760, Y0, "AIR", -1); gi = 0; loop(6, ui_cola(ME_INK, 0.10); gfx_rect((gi == 0 ? 110 : gi == 1 ? 200 : gi == 2 ? 350 : gi == 3 ? 460 : gi == 4 ? 570 : 670) * sc, (Y0 + 18) * sc, 1 * sc, 290 * sc); gi += 1; ); // IN + FILTERS: TRIM (with its GUARD key) above the cut filters, then TIMING dy_ink = ME_INK; dy_bg = ME_PNL; dy_kst = 20; dy_nov = 1; sprintf(#dv, "%+.1f dB", slider35); dy_knob(50, Y0 + 48, 14, 35, -24, 12, 0, 0, 0x2A2A2E, "TRIM", #dv); ui_guardclick(1600 + 35, 36, 50, Y0 + 48, 23); // click TRIM for GUARD dy_nov = 0; dy_kst = 7; ui_section(2); ui_kfilter = 1; strcpy(#kf_lo, "20"); strcpy(#kf_hi, "300"); ui_knob(60 * sc, (Y0 + 120) * sc, 1, 20, 300, 20, 1, "HPF", slider1 <= 20.5 ? "OFF" : "%.0f Hz"); me_snap(1, 2); ui_kfilter = 2; strcpy(#kf_lo, "4k"); strcpy(#kf_hi, "20k"); ui_knob(60 * sc, (Y0 + 192) * sc, 2, 4000, 20000, 20000, 1, "LPF", slider2 >= 19990 ? "OFF" : "%.0f Hz"); me_snap(2, 3); pk_step(65, Y0 + 278, 32, 3, "FAST|MED|SLOW", "TIMING"); // LOW SHELF pk_step(155, Y0 + 48, 3, 4, "30|60|100|200", ""); ui_knonum = 2; knob_step = 0.5; ui_kcap = P_GAN; ui_knob(155 * sc, (Y0 + 112) * sc, 4, -6, 6, 0, 0, "GAIN", "%+.1f dB"); ui_knonum = 0; ui_keycap(127 * sc, (Y0 + 156) * sc, 56 * sc, 18 * sc, slider5 ? "TIGHT" : "BROAD", 0, 0) ? ( slider5 = 1 - slider5; slider_automate(2 ^ 4); ); me_tgt(155, Y0 + 292, 6); // the row names, once, to the left of DYN 1 (each in its group's pastel colour) function me_rowname(y, s, c) local(tw, th) ( gfx_setfont(4); gfx_measurestr(s, tw, th); ui_col(c); gfx_rect((206) * sc, (y - 5) * sc, 4 * sc, 10 * sc); ui_cola(ME_INK, 0.85); gfx_x = 214 * sc; gfx_y = y * sc - th * 0.5; gfx_drawstr(s); ); me_rowname(Y0 + 52, "FREQ", P_FRQ); me_rowname(Y0 + 112, "GAIN", P_GAN); me_rowname(Y0 + 168, "Q", P_Q); me_rowname(Y0 + 230, "DYN", 0xFFB030); // DYN 1-3: pastel caps, no names (they are at the left), the value shows while a knob is touched function me_dyn(cx, i0) ( ui_kcap = P_FRQ; ui_knob(cx * sc, (Y0 + 52) * sc, i0, 20, 20000, 1000, 1, "", "%.0f Hz"); me_snap(i0, 0); knob_step = 0.5; ui_kcap = P_GAN; ui_knob(cx * sc, (Y0 + 112) * sc, i0 + 1, -6, 6, 0, 0, "", "%+.1f dB"); ui_kcap = P_Q; ui_knob(cx * sc, (Y0 + 168) * sc, i0 + 2, 0.4, 4, 1, 1, "", "%.2f"); me_snap(i0 + 2, 1); // COMP / EXP: using ui_knob so popup tooltips match FREQ and GAIN knob_step = 0.1; ui_kcap = 0xFFB030; // amber cap ui_knob(cx * sc, (Y0 + 230) * sc, i0 + 4, -10, 10, 0, 0, "", slider(i0 + 4) < -0.05 ? "COMP %.1f" : slider(i0 + 4) > 0.05 ? "EXP %.1f" : "STATIC"); knob_step = 0; gfx_setfont(4); ui_cola(ME_INK, 0.6); ui_text((cx - 32) * sc, (Y0 + 260) * sc, "COMP"); ui_text((cx + 32) * sc, (Y0 + 260) * sc, "EXP"); me_tgt(cx, Y0 + 292, i0 + 5); ); ui_section(3); me_dyn(295, 7); me_dyn(405, 13); me_dyn(515, 19); // HIGH SHELF, then the OUT stage keys under its gain (left of the output meter) ui_section(2); pk_step(620, Y0 + 48, 25, 4, "2k|4k|8k|12k", ""); ui_knonum = 2; knob_step = 0.5; ui_kcap = P_GAN; ui_knob(620 * sc, (Y0 + 112) * sc, 26, -6, 6, 0, 0, "GAIN", "%+.1f dB"); ui_knonum = 0; ui_keycap(592 * sc, (Y0 + 156) * sc, 56 * sc, 18 * sc, slider27 ? "TIGHT" : "BROAD", 0, 0) ? ( slider27 = 1 - slider27; slider_automate(2 ^ 26); ); ui_outkeys(594, Y0 + 204, 52, 14, 3, 44); gfx_setfont(4); ui_cola(ME_INK, 0.7); ui_text(620 * sc, (Y0 + 256) * sc, "OUT"); me_tgt(620, Y0 + 292, 28); // AIR + OUTPUT + meter pk_step(715, Y0 + 48, 29, 3, "10k|20k|40k", ""); ui_knonum = 2; knob_step = 0.5; ui_kcap = P_GAN; ui_knob(715 * sc, (Y0 + 112) * sc, 30, 0, 6, 0, 0, "GAIN", "%+.1f dB"); ui_knob(715 * sc, (Y0 + 184) * sc, 33, -12, 12, 0, 0, "OUTPUT", "%+.1f dB"); ui_knonum = 0; knob_step = 0; me_lv = max(mpk, me_lv * 0.8); mpk = 0; gi = 0; me_db = 20 * log10(max(me_lv, 0.00001)); loop(10, pk_led((693 + (gi % 5) * 11) * sc, (Y0 + 236 + floor(gi / 5) * 12) * sc, 3.4 * sc, me_db >= (gi == 0 ? -30 : gi == 1 ? -24 : gi == 2 ? -18 : gi == 3 ? -12 : gi == 4 ? -9 : gi == 5 ? -6 : gi == 6 ? -4 : gi == 7 ? -2 : gi == 8 ? -1 : -0.1), gi < 7 ? HS_LEDG : gi < 9 ? HS_AMB : HS_RED); gi += 1; ); me_tgt(715, Y0 + 292, 31); !slider34 ? ( ui_cola(0x000000, 0.3); gfx_rect(110 * sc, (Y0 + 18) * sc, 560 * sc, 266 * sc); ); pk_end(); ui_end();