desc:GFX M-Sum //tags: summing mixer bus console transformer vca compressor stems //author: JGWizrad / Garden FX (GFX) // // Requires gfx-drift, gfx-output, gfx-ui, gfx-panel, gfx-instrument and gfx-dyn (.jsfx-inc) in the same folder. // // An analogue summing bus in the spirit of an 80s British broadcast console's mix bus (personal use; the sound // is designed from scratch): up to eight stereo stems summed with the console's bus physics, the output // transformer's iron, and the desk's fast VCA bus compressor. // // SET-UP (REAPER): put GFX M-Sum on a "mix bus" track and set that track to 16 channels (ROUTE > Track channels). // Send each stem track to it on its own channel pair (in the send's audio routing pick 3/4, 5/6 ... 15/16) and // turn off the stem tracks' own master send. Channels 1/2 are the bus track's own audio (MAIN) and also the // output, so the summed mix leaves on 1/2: // 1/2 MAIN 3/4 STEM 1 5/6 STEM 2 7/8 STEM 3 9/10 STEM 4 11/12 STEM 5 13/14 STEM 6 15/16 STEM 7 // // SIGNAL FLOW: stems (level, mute) > bus sag + sum (DRIVE, CROSSTALK) > transformer (IRON) > VCA compressor > OUTPUT // BUS SAG - the more energy on the bus, the further the virtual power rail sags (to 65% at most): // V_rail = max(0.65, 1 - sum|stems| x SAG). The energy is smoothed (2 ms up, 60 ms down) so it // behaves as a soft, natural bus compression rather than distortion. The STRESS light glows orange // as the rail sags; the RAIL bar shows how far. // DRIVE - level into the bus and the transformer (it pushes the iron and the sag harder). // CROSSTALK - the left and right buses bleeding into each other, as on a real desk (fully left = off). // IRON - the output transformer: the lows below ~150 Hz are split off and saturated with a tanh (iron // core) curve, mostly 3rd harmonic, level-neutral when quiet; plus a +0.5 dB core resonance at 35 Hz. // The mids and highs pass untouched (low phase distortion up top, like the real thing). // VCA COMP - fast feed-forward VCA bus compressor: peak detector on both sides (linked), THRESH, RATIO, // ATTACK (0.1 - 20 ms), RELEASE (10 - 600 ms, program dependent: it slows the longer it works), // MAKEUP or AUTO (makes up half the reduction a full-scale peak would get). IN switches it. // STEMS - LEVEL, SOLO (solo-in-place: any number at once; only the soloed stems are heard) and MUTE // (a muted stem stays muted when soloed). The green light shows signal arriving on a stem that is heard. // METERS - VU: gain reduction and bus output. // PRESETS. // NAMES - click the name at the top of a channel strip and type (e.g. Drums); Enter keeps it, Escape cancels, // an empty name goes back to the number. The names are saved with the project. import gfx-drift.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:0<0,24,0.1>-Console input drive (dB) slider2:0.15<0,1,0.01>-Iron core saturation slider3:0.05<0,0.3,0.01>-Power rail bus sag slider4:-12<-30,0,0.5>-VCA threshold (dB) slider5:4<1,20,0.5>-VCA ratio slider6:1<0.1,20,0.1>-VCA attack (ms) slider7:150<10,600,1>-VCA release (ms) slider8:0<0,18,0.5>-VCA makeup (dB) slider9:-60<-60,0,1>-Crosstalk / bleed (dB, -60 = off) slider10:0<-24,12,0.5>-Master output trim (dB) slider11:1<0,1,1{Out,In}>-VCA in slider12:0<0,1,1{Off,On}>-VCA auto makeup slider13:0<-60,6,0.1>-Main 1/2 level (dB) slider14:0<-60,6,0.1>-Stem 1 level (dB) slider15:0<-60,6,0.1>-Stem 2 level (dB) slider16:0<-60,6,0.1>-Stem 3 level (dB) slider17:0<-60,6,0.1>-Stem 4 level (dB) slider18:0<-60,6,0.1>-Stem 5 level (dB) slider19:0<-60,6,0.1>-Stem 6 level (dB) slider20:0<-60,6,0.1>-Stem 7 level (dB) slider21:0<0,1,1{Off,On}>-Main 1/2 mute slider22:0<0,1,1{Off,On}>-Stem 1 mute slider23:0<0,1,1{Off,On}>-Stem 2 mute slider24:0<0,1,1{Off,On}>-Stem 3 mute slider25:0<0,1,1{Off,On}>-Stem 4 mute slider26:0<0,1,1{Off,On}>-Stem 5 mute slider27:0<0,1,1{Off,On}>-Stem 6 mute slider28:0<0,1,1{Off,On}>-Stem 7 mute slider29:0<0,1,1{Off,On}>-Main 1/2 solo slider30:0<0,1,1{Off,On}>-Stem 1 solo slider31:0<0,1,1{Off,On}>-Stem 2 solo slider32:0<0,1,1{Off,On}>-Stem 3 solo slider33:0<0,1,1{Off,On}>-Stem 4 solo slider34:0<0,1,1{Off,On}>-Stem 5 solo slider35:0<0,1,1{Off,On}>-Stem 6 solo slider36:0<0,1,1{Off,On}>-Stem 7 solo slider37:0<0,100,1>-Band sat (%) slider38:30<0,100,1>-Bias (%) slider39:120<40,400,1>-Sat low band (Hz) slider40:6000<2000,16000,10>-Sat high band (Hz) slider41:-0.3<-24,0,0.1>-Ceiling / limit threshold (dB) slider42:1<0,2,1{Off,Clip,Limit}>-Output stage in_pin:main L in_pin:main R in_pin:stem 1 L in_pin:stem 1 R in_pin:stem 2 L in_pin:stem 2 R in_pin:stem 3 L in_pin:stem 3 R in_pin:stem 4 L in_pin:stem 4 R in_pin:stem 5 L in_pin:stem 5 R in_pin:stem 6 L in_pin:stem 6 R in_pin:stem 7 L in_pin:stem 7 R out_pin:mix L out_pin:mix R @init #ui_man = "gfx-m-sum"; // the ? pop-up links to this manual LHD = 180; // signal-light hold per input (gfx) DEN = 0.00000000000000000001; // tiny offset that keeps filter states out of denormals SG = 100; SGT = 110; SPK = 120; // per-stem gains (smoothed), targets, peak meters (8 each) memset(SG, 0, 30); function update() local(i) ( drv = 10 ^ (slider1 / 20); iron_d = 1 + slider2 * 3; // iron core drive into the tanh sag = slider3; thr = slider4; rat = max(slider5, 1); ac = dy_ms(slider6); rc_f = dy_ms(slider7); rc_s = dy_ms(slider7 * 4); bleed = slider9 <= -59.5 ? 0 : 10 ^ (slider9 / 20); trim = 10 ^ (slider10 / 20); c_on = slider11; // makeup: manual, or AUTO = half the reduction a 0 dBFS peak would get mk = 10 ^ ((slider12 ? 0.5 * -thr * (1 - 1 / rat) : slider8) / 20); i = 0; any_solo = 0; i = 0; loop(8, any_solo |= slider(29 + i); i += 1; ); i = 0; loop(8, // heard: not muted, and soloed if anything is SGT[i] = slider(21 + i) || (any_solo && !slider(29 + i)) || slider(13 + i) <= -59.9 ? 0 : 10 ^ (slider(13 + i) / 20); i += 1; ); ost_setup(0, 0, 120, 6000, -0.3, 0); // no GFX output stage on this unit: its own character does that job ); // ---------- presets (stem levels, mutes and solos are left alone) ---------- // drive, iron, sag, crosstalk, thresh, ratio, attack, release, makeup, auto, comp in, trim function pp(dv, ir, sg, ct, t, r, a, rl, mu, au, ci, tr) ( slider1 = dv; slider2 = ir; slider3 = sg; slider9 = ct; slider4 = t; slider5 = r; slider6 = a; slider7 = rl; slider8 = mu; slider12 = au; slider11 = ci; slider10 = tr; ); function load_preset(p) ( p == 1 ? pp(0, 0.1, 0.03, -60, -12, 2, 5, 200, 0, 0, 0, 0) : // Clean Sum p == 2 ? pp(3, 0.3, 0.06, -54, -14, 2, 10, 250, 1, 0, 1, -1) : // Warm Console p == 3 ? pp(8, 0.55, 0.1, -50, -16, 3, 3, 150, 2, 0, 1, -4) : // Driven Desk p == 4 ? pp(2, 0.2, 0.05, -60, -18, 4, 1, 120, 0, 1, 1, -1) : // Broadcast Glue p == 5 ? pp(6, 1, 0.08, -60, -12, 2, 10, 300, 0, 0, 1, -3) : // Heavy Iron p == 6 ? pp(4, 0.3, 0.12, -48, -20, 6, 0.3, 80, 0, 1, 1, -2) : // Punch Bus p == 7 ? pp(10, 0.7, 0.2, -45, -10, 2, 10, 300, 0, 0, 1, -6) : // Stressed Rail p == 8 ? pp(0, 0, 0, -60, -12, 1, 1, 150, 0, 0, 0, 0); // Straight Sum (no colour) p >= 1 && p <= 8 ? slider_automate(4095); // sliders 1-12 p == 9 ? ( // Reset all to defaults pp(0, 0.15, 0.05, -60, -12, 4, 1, 150, 0, 0, 1, 0); i = 0; loop(8, slider(13 + i) = 0; slider(21 + i) = 0; slider(29 + i) = 0; i += 1; ); slider37 = 0; slider38 = 30; slider39 = 120; slider40 = 6000; slider41 = -0.3; slider42 = 1; slider_automate(4398046511103); // sliders 1-42 ); ); ost_init(20000); ostL.ost_ch_init(); ostR.ost_ch_init(); en_a = 1 - exp(-1 / (0.002 * srate)); en_r = 1 - exp(-1 / (0.06 * srate)); lp_c = 1 - exp(-2 * $pi * 150 / srate); // transformer split: 1-pole low-pass at 150 Hz bumpL.bq_set(2, 35, 1.2, 0.5); bumpR.bq_set(2, 35, 1.2, 0.5); // +0.5 dB core resonance at 35 Hz det_r = exp(-1 / (0.005 * srate)); // peak detector decay sm = 1 - exp(-1 / (0.02 * srate)); vuc = 1 - exp(-1 / (0.3 * srate)); !ui_dflt ? ui_thm = 5; !ui_dflt ? ui_hw = 1; // fixed faceplate PNL = 0x74777C; INKW = 0xF4F2EA; YEL = 0xF2D04A; KDK = 0x3A3C40; update(); i = 0; loop(8, SG[i] = SGT[i]; i += 1; ); ui_dflt = 1; // theme defaults above apply once: @init re-runs (playback start, device reset) must not undo the saved theme @slider update(); @block update(); @serialize file_var(0, ui_sz); gn = 0; loop(8, file_avail(0) != 0 ? file_string(0, UI_NM + gn); gn += 1; ); // channel names (older projects have none) file_avail(0) != 0 ? file_var(0, pr_cur); // the preset in use (older projects end before it) @sample // ---------- A: stems, bus stress + rail sag, sum ---------- sl = 0; sr = 0; ab = 0; k = 0; loop(8, SG[k] += (SGT[k] - SG[k]) * sm; // smoothed stem level / mute xl = spl(2 * k) * SG[k]; xr = spl(2 * k + 1) * SG[k]; SPK[k] = max(SPK[k], max(abs(spl(2 * k)), abs(spl(2 * k + 1)))); sl += xl; sr += xr; ab += abs(xl) + abs(xr); // cumulative instantaneous voltage on the bus k += 1; ); ab *= drv; en += (ab - en) * (ab > en ? en_a : en_r); // smoothed bus energy v_rail = max(0.65, 1 - en * sag); // V_rail = max(0.65, 1 - sum|S| x sag) g = v_rail * drv; bl = (sl + sr * bleed) * g; br = (sr + sl * bleed) * g; // L / R buses, with crosstalk // ---------- B: transformer: split the lows, iron-core tanh, +0.5 dB at 35 Hz, recombine ---------- loL += (bl - loL) * lp_c + DEN; loR += (br - loR) * lp_c + DEN; satL = ost_tanh(loL * iron_d) / iron_d; satR = ost_tanh(loR * iron_d) / iron_d; // level-neutral when quiet bl = bumpL.bq_run(bl - loL + satL); br = bumpR.bq_run(br - loR + satR); // ---------- C: VCA bus compressor (feed-forward, peak, linked) ---------- pk = max(abs(bl), abs(br)); env = max(pk, env * det_r) + DEN; c_on ? ( t_ = -max(0, 20 * log10(env) - thr) * (1 - 1 / rat); // dB_GR = -max(0, dB_in - thr) x (1 - 1/ratio) t_ < e_ ? e_ += (t_ - e_) * ac : ( e_ += (t_ - e_) * (rc_f + (rc_s - rc_f) * min(hold, 1)); // release slows the longer it has worked ); hold = e_ < -2 ? min(hold + 1 / srate, 1) : max(hold - 2 / srate, 0); cg = 10 ^ (e_ / 20) * mk; ) : ( e_ = 0; cg = 1; ); bl *= cg * trim; br *= cg * trim; grm = min(grm, e_); // ---------- output stage ---------- bl = ostL.ost_band(bl); br = ostR.ost_band(br); ost_out(bl, br); spl0 = ost_l; spl1 = ost_r; vm += (0.5 * (spl0 * spl0 + spl1 * spl1) - vm) * vuc; railm = min(railm, v_rail); @gfx 900 410 ui_begin(900, ins_show_out ? 530 : 410); dy_ink = INKW; dy_bg = PNL; dy_kst = 3; // brushed medium-grey metal function brushed(x, y, w, h) local(i, gk) ( i = 0; loop(floor(h * sc), gk = ui_hash(i * 1.37 + 0.5); ui_cola(gk > 0.5 ? 0xFFFFFF : 0x000000, 0.012 + 0.04 * abs(gk - 0.5)); gfx_line(x * sc, y * sc + i, (x + w) * sc, y * sc + i); i += 1; ); ); function sect(x, y, w, h, t) ( ui_cola(0x000000, 0.18); gfx_rect(x * sc, y * sc, w * sc, h * sc); ui_cola(0xFFFFFF, 0.15); gfx_rect(x * sc, (y + h) * sc, w * sc, 1 * sc); gfx_setfont(6, "Trebuchet MS", 13 * sc, 'b'); ui_col(YEL); gfx_x = (x + 8) * sc; gfx_y = (y + 5) * sc; gfx_drawstr(t); ui_col(YEL); gfx_rect((x + 8) * sc, (y + 21) * sc, (w - 16) * sc, 1 * sc); ); // ---------- panel ---------- fh = ins_show_out ? 522 : 402; ui_col(0x0B0C0D); gfx_rect(0, 0, gfx_w, gfx_h); pk_chassis(8 * sc, 4 * sc, 884 * sc, fh * sc, PNL); brushed(10, 6, 880, fh - 6); ui_col(INKW); gfx_circle(40 * sc, 34 * sc, 14 * sc, 1, 1); gfx_setfont(4); ui_col(PNL); ui_text(40 * sc, 29 * sc, "GFX"); gfx_setfont(6, "Trebuchet MS", 24 * sc, 'b'); ui_col(INKW); gfx_x = 62 * sc; gfx_y = 20 * sc; gfx_drawstr("M-SUM"); gfx_setfont(4); ui_col(YEL); gfx_x = 160 * sc; gfx_y = 30 * sc; gfx_drawstr("BROADCAST CONSOLE SUMMING BUS"); pk_pextra = "Reset all to defaults"; // the house preset display: PRESETS, prev / next, LCD (click it for the list) k_ = pk_preset(470 * sc, 14 * sc, 346 * sc, 36 * sc, pr_cur, "Clean Sum|Warm Console|Driven Desk|Broadcast Glue|Heavy Iron|Punch Bus|Stressed Rail|Straight Sum (no colour)"); k_ > 0 ? ( load_preset(k_); pr_cur = k_ > 8 ? 0 : k_; ); ui_size_picker(828 * sc, 22 * sc); ui_col(YEL); gfx_rect(14 * sc, 60 * sc, 872 * sc, 2 * sc); // ---------- STEMS ---------- sect(18, 70, 466, 172, "STEMS"); gk = 0; loop(8, cx = 50 + gk * 56; gk == 0 ? strcpy(#sn, "MAIN") : sprintf(#sn, "%d", gk); ui_namebox(cx, 91, 52, gk, #sn, INKW); // click to name the stem sprintf(#sn, "%d-%d", 2 * gk + 1, 2 * gk + 2); dy_lbl(cx, 112, #sn, 0.55); LHD[gk] = max(SPK[gk], LHD[gk] * 0.9); SPK[gk] = 0; // held, so the light does not flicker between audio blocks pk_led(cx * sc, 128 * sc, 3.5 * sc, LHD[gk] > 0.001 && SGT[gk] > 0, dy_grn); slider(13 + gk) <= -59.9 ? strcpy(#v, "OFF") : sprintf(#v, "%+.1f", slider(13 + gk)); dy_knob(cx, 152, 13, 13 + gk, -60, 6, 0, 0, INKW, "", #v); pk_ledbtn((cx - 24) * sc, 200 * sc, 22 * sc, 20 * sc, slider(29 + gk), YEL) ? ( slider(29 + gk) = 1 - slider(29 + gk); ui_auto(29 + gk); ); pk_ledbtn((cx + 2) * sc, 200 * sc, 22 * sc, 20 * sc, slider(21 + gk), dy_red) ? ( slider(21 + gk) = 1 - slider(21 + gk); ui_auto(21 + gk); ); dy_lbl(cx - 13, 224, "SOLO", 0.6); dy_lbl(cx + 13, 224, "MUTE", 0.6); gk += 1; ); // ---------- CONSOLE BUS ---------- sect(496, 70, 388, 172, "CONSOLE BUS"); sprintf(#v, "%.1f dB", slider1); dy_knob(536, 140, 18, 1, 0, 24, 0, 0, YEL, "DRIVE", #v); sprintf(#v, "%.0f", slider2 * 100); dy_knob(612, 140, 18, 2, 0, 1, 0.15, 0, YEL, "IRON", #v); sprintf(#v, "%.2f", slider3); dy_knob(688, 140, 18, 3, 0, 0.3, 0.05, 0, INKW, "SAG", #v); slider9 <= -59.5 ? strcpy(#v, "OFF") : sprintf(#v, "%.0f dB", slider9); dy_knob(764, 140, 18, 9, -60, 0, -60, 0, INKW, "CROSSTALK", #v); // bus stress: orange as the rail sags; RAIL bar from 100% down to 65% rd = railm; railm = 1; rdisp = rd < rdisp ? rd : rdisp + (rd - rdisp) * 0.08; // falls at once, recovers slowly: no flicker pk_led(840 * sc, 112 * sc, 5 * sc, rdisp < 0.985, 0xFF8A1A); dy_lbl(840, 124, "STRESS", 0.8); ui_col(0x101112); gfx_rect(832 * sc, 140 * sc, 16 * sc, 70 * sc); ui_col(rdisp < 0.9 ? 0xFF8A1A : dy_grn); gfx_rect(834 * sc, (142 + (1 - (rdisp - 0.65) / 0.35) * 66) * sc, 12 * sc, ((rdisp - 0.65) / 0.35 * 66) * sc); dy_lbl(840, 214, "RAIL", 0.8); sprintf(#v, "%.0f", rdisp * 100); dy_lbl(840, 226, #v, 0.6); // ---------- VCA COMPRESSOR ---------- sect(18, 254, 518, 140, "VCA BUS COMPRESSOR"); sprintf(#v, "%.1f dB", slider4); dy_knob(60, 316, 18, 4, -30, 0, -12, 0, YEL, "THRESH", #v); sprintf(#v, "%.1f:1", slider5); dy_knob(132, 316, 18, 5, 1, 20, 4, 1, INKW, "RATIO", #v); sprintf(#v, "%.1f ms", slider6); dy_knob(204, 316, 18, 6, 0.1, 20, 1, 1, INKW, "ATTACK", #v); sprintf(#v, "%.0f ms", slider7); dy_knob(276, 316, 18, 7, 10, 600, 150, 1, INKW, "RELEASE", #v); slider12 ? strcpy(#v, "AUTO") : sprintf(#v, "+%.1f dB", slider8); dy_knob(348, 316, 18, 8, 0, 18, 0, 0, INKW, "MAKEUP", #v); dy_btn(388, 290, slider11, dy_grn, "IN") ? ( slider11 = 1 - slider11; ui_auto(11); ); dy_btn(434, 290, slider12, YEL, "AUTO") ? ( slider12 = 1 - slider12; ui_auto(12); ); sprintf(#v, "%+.1f dB", slider10); dy_knob(504, 316, 18, 10, -24, 12, 0, 0, YEL, "OUTPUT", #v); // ---------- METERS ---------- sect(548, 254, 336, 140, "METERS"); grd = min(grm, grd * 0.85); grm = 0; vuG.ui_vu(558 * sc, 282 * sc, 156 * sc, 100 * sc, grd, "GR"); vuO.ui_vu(720 * sc, 282 * sc, 156 * sc, 100 * sc, 10 * log10(vm + 0.0000000001) + 18, "BUS OUT"); pk_end(); ui_end();