desc:GFX G Harbour Keys //tags: instrument piano electric-piano rompler vaporwave physical-modeling chorus delay //author: JGWizrad / Garden FX (GFX) // // Requires gfx-drift, gfx-output, gfx-ui, gfx-panel and gfx-instrument (.jsfx-inc) in the same folder. // // A synthetic piano: a simulacrum of the early-90s rompler piano, not a recreation of a real one. // The core is the G Piano physical model (hammer, waveguide strings, soundboard, sympathetic bridge), so it still // answers velocity like an instrument; on top of it sit the things that made those ROM pianos sound the way // they did, then a tape deck, a mod rack and an 80s space. // // ENGINE GRAND / UPRIGHT core; FELT / TACK / PLUCK / MALLET striker; HARDNESS, DYNAMICS, SUSTAIN, UNISON. // BELL an FM layer on every key, the glassy top of the M1 house piano and the DX tine: a fast tine // transient over a sine body. BELL sets its level, GLASS its brightness (modulation index). // ROM sample-rate and bit-depth reduction (48 kHz / 16 bit down to ~10 kHz / 8 bit, aliasing left in). // WOW tape speed wander (slow wow, flutter, a little random drag): the vaporwave detune. // VHS worn tape: the top rolls off, hiss and soft tape saturation come up. // MOD RACK from GFX Mod Rack: CHORUS (stereo BBD-style), TREM (stereo auto-pan, the classic e-piano trick), // VIBE (photocell phaser), DELAY (ping-pong, warm saturating feedback, ms or synced to tempo). // SPACE ROOM, PLATE (bright, long) or GATED (the 80s gated reverb); SPACE sets the amount. // The finer physical controls of G Piano (stiffness, wire age, tuning variance, damper, body, sympathetic // resonance, contact, bounce, board size / density, lid, mic) are still there in the parameter list. // MIDI: sustain (CC64), soft pedal (CC67), all notes off (CC123 / 120). 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 slider1:0<0,11,1{Harbour Lights,Ocean Drive EP,Neon Rain,Sunset Strip,Mall Fountain,Night Bus,Palm Glass,Cassette Ballad,Gated Romance,Pastel Tine,Floppy Disk,Harbour Fog}>-Patch slider2:0<0,1,1{Grand,Upright}>-Model slider3:50<0,100,1>-Hardness / age (%) slider4:55<0,100,1>-Dynamics (%) slider5:40<0,100,1>-Stiffness (%) slider6:20<0,100,1>-Unison detune (%) slider7:60<0,100,1>-Sustain (%) slider8:45<0,100,1>-Damper (%) slider9:55<0,100,1>-Body (%) slider10:50<0,100,1>-Tone (%) slider11:35<0,100,1>-Sympathetic resonance (%) slider12:30<0,100,1>-Space (%) slider13:60<0,100,1>-Spread (%) slider14:15<0,100,1>-Drift (%) slider15:0<-24,6,0.1>-Volume (dB) slider16:0<0,100,1>-Band sat (%) slider17:30<0,100,1>-Bias (%) slider18:120<40,400,1>-Sat low band (Hz) slider19:6000<2000,16000,10>-Sat high band (Hz) slider20:-0.3<-24,0,0.1>-Ceiling / limit threshold (dB) slider21:1<0,2,1{Off,Clip,Limit}>-Output stage slider22:20<20,500,1>-Mono below (Hz, 20 = off) slider23:0<0,100,1>-Stereo width (%) slider24:0<0,3,1{Felt,Tack,Pluck,Mallet}>-Striker slider25:50<0,100,1>-Contact (%) slider26:40<0,100,1>-Bounce (%) slider27:0<0,100,1>-Wire age (%) slider28:15<0,100,1>-Tuning variance (%) slider29:50<0,100,1>-Board size (%) slider30:55<0,100,1>-Board density (%) slider31:0<0,2,1{Open,Half,Closed}>-Lid slider32:35<0,100,1>-Mic distance (%) slider33:35<0,100,1>-Bell (%) slider34:40<0,100,1>-Glass (%) slider35:0<0,100,1>-ROM (%) slider36:0<0,100,1>-Wow (%) slider37:0<0,100,1>-VHS (%) slider38:0.6<0.05,8,0.01>-Chorus rate (Hz) slider39:50<0,100,1>-Chorus depth (%) slider40:0<0,100,1>-Chorus mix (%) slider41:4<0.1,12,0.01>-Trem rate (Hz) slider42:0<0,100,1>-Trem depth (%) slider43:1.2<0.1,10,0.01>-Vibe rate (Hz) slider44:0<0,100,1>-Vibe depth (%) slider45:375<20,1500,1>-Delay time (ms) slider46:35<0,95,1>-Delay feedback (%) slider47:0<0,100,1>-Delay mix (%) slider48:0<0,4,1{ms,1/4,1/8 dotted,1/8,1/4 triplet}>-Delay sync slider49:1<0,2,1{Room,Plate,Gated}>-Space type in_pin:none out_pin:left output out_pin:right output @init #ui_man = "gfx-g-harbour-keys"; // the ? pop-up links to this manual // ---------- memory (allocated with nextmem, clear of the output stage at 20000) ---------- nextmem = 30000; function alloc(n) local(ptr) ( ptr = nextmem; nextmem += n; memset(ptr, 0, n); ptr; ); NV = 16; // keys sounding at once MS = 3; // strings per key (max) NSTR = NV * MS; RS = 4096; RM = RS - 1; RS2 = 2 * RS; // one rail per direction per string (A0 at 192 kHz needs ~3500) NY = 36; YS = 8192; YM = YS - 1; // open-string bank (pedal down), E1 .. D#4 FSZ = 8192; FM = FSZ - 1; // room lines // voices (keys) v_act = alloc(NV); v_note = alloc(NV); v_held = alloc(NV); v_sus = alloc(NV); v_age = alloc(NV); v_ns = alloc(NV); v_pl = alloc(NV); v_pr = alloc(NV); v_env = alloc(NV); v_t = alloc(NV); v_nd = alloc(NV); v_sil = alloc(NV); v_ka = alloc(NV); v_kl = alloc(NV); v_gn = alloc(NV); // hammers (one per key) h_on = alloc(NV); h_y = alloc(NV); h_v = alloc(NV); h_ys = alloc(NV); h_im = alloc(NV); h_K = alloc(NV); h_pw = alloc(NV); h_al = alloc(NV); h_cp = alloc(NV); h_fr = alloc(NV); // strings s_N = alloc(NSTR); s_h = alloc(NSTR); s_eta = alloc(NSTR); s_tx = alloc(NSTR); s_ty = alloc(NSTR); s_a = alloc(NSTR); s_nap = alloc(NSTR); s_x1 = alloc(NSTR); s_y1 = alloc(NSTR); s_x2 = alloc(NSTR); s_y2 = alloc(NSTR); s_x3 = alloc(NSTR); s_y3 = alloc(NSTR); s_x4 = alloc(NSTR); s_y4 = alloc(NSTR); s_p = alloc(NSTR); s_lp = alloc(NSTR); s_g = alloc(NSTR); s_gh = alloc(NSTR); s_gr = alloc(NSTR); s_w = alloc(NSTR); s_yi = alloc(NSTR); s_ol = alloc(NSTR); v_dv = alloc(NV); s_f = alloc(NSTR); s_mg = alloc(NSTR); s_tk = alloc(NSTR); s_rk = alloc(NSTR); h_typ = alloc(NV); h_fx = alloc(NV); h_ct = alloc(NV); v_tk = alloc(NV); v_tl = alloc(NV); LD = alloc(2 * 2048); PD = alloc(4096); // lid reflections (L/R), room pre-delay RAIL = alloc(NSTR * RS2); // string s: [RAIL + s*RS2] towards the bridge, [+ RS] back to the agraffe // open-string bank y_L = alloc(NY); y_eta = alloc(NY); y_tx = alloc(NY); y_ty = alloc(NY); y_p = alloc(NY); y_lp = alloc(NY); y_g = alloc(NY); y_gd = alloc(NY); y_yi = alloc(NY); YB = alloc(NY * YS); // soundboard modes: 16 x (5 coefficients + 4 state) BM = alloc(16 * 9); bm_g = alloc(16); // room FRM = alloc(4 * FSZ); // bell layer (per key) b_ph = alloc(NV); b_mp = alloc(NV); b_inc = alloc(NV); b_a = alloc(NV); b_ti = alloc(NV); b_dk = alloc(NV); b_rk = alloc(NV); b_tr = alloc(NV); // tape, chorus, vibe, delay TPN = 16384; TPA = alloc(TPN); TPB = alloc(TPN); CHN = 16384; CHA = alloc(CHN); CHB = alloc(CHN); DLN = floor(srate * 2.2) + 8; DLA = alloc(DLN); DLB = alloc(DLN); VSA = alloc(16); VSB = alloc(16); gw = 0; ost_init(20000); ostL.ost_ch_init(); ostR.ost_ch_init(); // 20000..24095 ins_drift_init(); agec = 0; sus_pedal = 0; soft_pedal = 0; sym_on = 0; sym_env = 0; TP = 2 * $pi; // ---------- tuning ---------- // phase delay (samples) of a first-order all-pass (a + z^-1) / (1 + a z^-1) at w function ap_tau(a, w) ( -(atan2(-sin(w), a + cos(w)) - atan2(-a * sin(w), 1 + a * cos(w))) / w; ); // phase delay of the loss filter y = (1 - p) x + p y1 at w function lp_tau(p, w) ( atan2(p * sin(w), 1 - p * cos(w)) / w; ); // dispersion coefficient that stretches the partials like a stiff string with inharmonicity B: // partial k should sit at k f0 sqrt(1 + B k^2) / sqrt(1 + B). Bisection on the nap all-passes. function disp_a(f, B, nap) local(w, per, kk, tgt, lo, hi, mid, dv) ( w = TP * f / srate; per = srate / f; kk = min(8, floor(0.42 * srate / f)); kk < 2 ? 0 : ( tgt = per * (1 - sqrt(1 + B) / sqrt(1 + B * kk * kk)); lo = -0.95; hi = 0; loop(22, mid = 0.5 * (lo + hi); dv = nap * (ap_tau(mid, w) - ap_tau(mid, kk * w)); dv > tgt ? lo = mid : hi = mid; ); 0.5 * (lo + hi); ); ); // tune string s (two rails, round trip 2 x rail + bridge filters = one period) and set its losses // (EEL names are case-insensitive: every local is kept distinct from the parameters) function str_set(s, f, a, nap, p, t60, trel, beta) local(w, per, dl, rl, fd, eta, err, mag, kk) ( w = TP * f / srate; per = srate / f; dl = per - nap * ap_tau(a, w) - lp_tau(p, w); kk = 0; loop(12, dl < 5 && a < -0.001 ? ( a *= 0.6; dl = per - nap * ap_tau(a, w) - lp_tau(p, w); ); kk += 1; ); rl = max(1, floor((dl - 0.5) / 2)); fd = max(0.3, dl - 2 * rl); eta = (1 - fd) / (1 + fd); err = ap_tau(eta, w) - fd; // one correction pass for the Thiran tuner at this frequency fd = max(0.3, fd - err); eta = (1 - fd) / (1 + fd); // (fd >= 0.3 keeps the all-pass pole well inside) s_N[s] = min(rl, RS - 4); s_eta[s] = eta; s_a[s] = a; s_nap[s] = nap; s_p[s] = p; s_h[s] = max(1, min(s_N[s] - 1, floor(beta * s_N[s] + 0.5))); mag = (1 - p) / sqrt(1 - 2 * p * cos(w) + p * p); s_f[s] = f; s_mg[s] = mag; // loop gain per period: the fundamental decays in t60; capped below 1 so the loop always dies away s_gh[s] = min(0.99995, 10 ^ (-3 / (t60 * f)) / mag); s_gr[s] = min(0.9995, 10 ^ (-3 / (trel * f)) / mag); ); // re-derive a sounding string's loop gains after SUSTAIN or DAMPER moved (no retuning, so no artefacts) function str_regain(s) local(t60, trel) ( t60 = max(0.35, sus_t * s_tk[s]); trel = s_rk[s] < 0 ? t60 : max(0.04, dmp_t * s_rk[s]); s_gh[s] = min(0.99995, 10 ^ (-3 / (t60 * s_f[s])) / s_mg[s]); s_gr[s] = min(0.9995, 10 ^ (-3 / (trel * s_f[s])) / s_mg[s]); ); // a fixed pseudo-random number in -1..1 for a key (and string): each key keeps its own tuning error function khash(k) local(q) ( q = sin(k * 12.9898 + 78.233) * 43758.5453; 2 * (q - floor(q)) - 1; ); function str_clear(s) local(rb) ( rb = RAIL + s * RS2; memset(rb, 0, RS2); s_tx[s] = s_ty[s] = s_x1[s] = s_y1[s] = s_x2[s] = s_y2[s] = s_x3[s] = s_y3[s] = s_x4[s] = s_y4[s] = s_lp[s] = 0; ); // open string j of the bank: a single tuned loop (no hammer, no stiffness) function sym_set(j) local(nt, f, w, per, rl, fd, eta, pp, mag, t60) ( nt = 28 + j; f = 440 * 2 ^ ((nt - 69) / 12); w = TP * f / srate; per = srate / f; pp = 0.2 * 2 ^ (-(nt - 60) / 24); rl = floor(per - lp_tau(pp, w) - 0.5); fd = per - lp_tau(pp, w) - rl; eta = (1 - fd) / (1 + fd); y_L[j] = min(rl, YS - 4); y_eta[j] = eta; y_p[j] = pp; mag = (1 - pp) / sqrt(1 - 2 * pp * cos(w) + pp * pp); t60 = max(1.5, 14 * 2 ^ (-(nt - 21) / 23)); y_g[j] = min(0.9999, 10 ^ (-3 / (t60 * f)) / mag); y_gd[j] = min(0.999, 10 ^ (-3 / (0.12 * f)) / mag); ); j = 0; loop(NY, sym_set(j); j += 1; ); // ---------- notes ---------- function note_on(n, vel) local(i, best, ba, f, ns, kk, a, nap, pp, t60, trel, det, vv, pan, s, jit, beta, mr, tc, pw, v0, B, rstk, tk, rk, aj, sty) ( best = -1; i = 0; loop(NV, v_act[i] && v_note[i] == n && best < 0 ? best = i; i += 1; ); // re-strike: same strings best < 0 ? ( i = 0; loop(NV, !v_act[i] && best < 0 ? best = i; i += 1; ); ); // free key best < 0 ? ( ba = 10 ^ 12; i = 0; loop(NV, !v_held[i] && v_age[i] < ba ? ( ba = v_age[i]; best = i; ); i += 1; ); ); best < 0 ? ( ba = 10 ^ 12; i = 0; loop(NV, v_age[i] < ba ? ( ba = v_age[i]; best = i; ); i += 1; ); ); i = best; rstk = v_act[i] && v_note[i] == n; f = 440 * 2 ^ ((n - 69) / 12) * ins_note_detune(); ns = n <= 28 ? 1 : n < 48 ? 2 : 3; nap = n < 48 ? 4 : 2; // stiffness: inharmonicity B is lowest around the tenor break and rises to both ends; the upright's short // bass strings are much stiffer kk = n < 40 ? -4.3 + 0.042 * (40 - n) : -4.3 + 0.03 * (n - 40); model ? kk += n < 40 ? 0.45 : 0.15; B = 10 ^ kk * (0.1 + 2.4 * stiff ^ 1.5); a = disp_a(f, B, nap); // losses: how fast the top of each note dies against its fundamental; sustain and damping by key pp = min(0.5, max(0.01, (model ? 0.26 : 0.16) * (0.4 + 1.2 * (1 - tone_s)) * 2 ^ (-(n - 60) / 24) * (1 + 1.5 * wage))); tk = 17 * 2 ^ (-(n - 21) / 23) * (model ? 0.65 : 1) * (1 - 0.5 * wage); // sustain factor (x SUSTAIN) rk = n >= 89 ? -1 : 0.55 * 2 ^ (-(n - 21) / 34); // damping factor (x DAMPER) t60 = max(0.35, sus_t * tk); trel = rk < 0 ? t60 : max(0.04, dmp_t * rk); sty = striker; beta = sty == 3 ? 1 / 5 : sty == 2 ? 1 / 12 : model ? 1 / 9 : 1 / 8; det = uni_c * (0.6 + 0.8 * (n / 108)); // unison spread in cents f *= 2 ^ (tvar * 6 * khash(n) / 1200); // this key's own tuning error (up to 6 cents) s = MS * i; kk = 0; loop(MS, kk < ns ? ( jit = ns == 1 ? 0 : (rand(2) - 1) * 0.15 + tvar * 2.5 * khash(n * 3.7 + kk); aj = max(-0.95, a * (1 + wage * 0.7 * khash(n * 5.3 + kk * 1.9))); // worn wire: its own stiffness str_set(s + kk, f * 2 ^ (((ns == 1 ? 0 : det * (kk / (ns - 1) - 0.5)) + jit) / 1200), aj, nap, pp, t60 * (1 + 0.12 * kk), trel, beta); s_tk[s + kk] = tk * (1 + 0.12 * kk); s_rk[s + kk] = rk; s_w[s + kk] = 1 / ns; ) : s_w[s + kk] = 0; !rstk ? str_clear(s + kk); s_g[s + kk] = s_gh[s + kk]; kk += 1; ); !rstk ? v_env[i] = 0; // hammer: speed from velocity; felt hardness, exponent and hysteresis from HAMMER and the key vel = vel / 127; v_sil[i] = vel < 0.03; // velocity 1 - 3: the key goes down silently (dampers up) v0 = vel ^ (0.6 + 1.6 * dyn) * (soft_pedal ? 0.85 : 1); mr = 0.06 * 2 ^ ((n - 21) / 24); // hammer : string mass, the treble hammer outweighs its strings // contact time: the key, the felt (HARDNESS: soft and dark .. compacted and bright), CONTACT, the soft pedal tc = 2.6 * 2 ^ (-(n - 21) / 32) * (1.45 - 0.9 * ham) * (soft_pedal ? 1.35 : 1) * contact_m; pw = 2.2 + (n - 21) / 87 + 0.4 * ham; // felt stiffening exponent sty == 1 ? ( tc *= 0.28; pw = 1.6; mr *= 1.15; ) : // tacks: metal on wire sty == 2 ? ( tc = 0.6 * contact_m; pw = 1; mr *= 30; ) : // plectrum: a stiff, linear catch sty == 3 ? ( tc *= 2.4; pw = 1.6; mr *= 4; ); // mallet: heavy and soft tc = max(srate * 0.001 * tc, max(6, 0.00015 * srate)); h_im[i] = 1 / (mr * 2 * s_N[s] * ns); h_pw[i] = pw; h_K[i] = (1 / h_im[i]) * (3.2 * 0.5 ^ ((1 - pw) / (pw + 1)) / tc) ^ (pw + 1); // hysteresis (felt that does not spring straight back): less bounce, more damping h_al[i] = sty == 2 ? 0 : srate * 0.00001 * (2 + 4 * (1 - ham)) * (2.2 - 2 * bounce) * (sty == 3 ? 2.5 : 1); // the plectrum lets go when its force passes its grip h_fx[i] = h_K[i] * (v0 * tc * 0.55) * (0.5 + contact); h_typ[i] = sty; h_ct[i] = 0; h_y[i] = 0; h_ys[i] = 0; h_v[i] = v0; h_cp[i] = 0; h_fr[i] = 0; h_on[i] = !v_sil[i]; v_t[i] = 0; v_ka[i] = v_sil[i] ? 0 : v0 * 0.012 * (model ? 1.3 : 1) * (sty == 2 ? 0.3 : 1); v_kl[i] = 0; v_tk[i] = v_sil[i] || sty != 1 ? 0 : v0 * 0.03; v_tl[i] = 0; // pan: bass left, treble right pan = min(1, max(-1, (n - 64) / 44 * spread)); v_pl[i] = cos((pan + 1) * $pi / 4); v_pr[i] = sin((pan + 1) * $pi / 4); v_gn[i] = 1.2 * 2 ^ ((n - 21) / 45); // output level by key (the treble strings carry less energy) v_gn[i] *= sty == 1 ? 0.5 * 2 ^ (-max(0, n - 60) / 14) : sty == 2 ? 0.5 : sty == 3 ? 0.85 * 2 ^ ((n - 60) / 60) : 1; // level by striker // bell layer: a fast FM tine over a sine body, at this key's (detuned) pitch b_inc[i] = f / srate; !rstk ? ( b_ph[i] = 0; b_mp[i] = 0; ); b_a[i] = v_sil[i] ? 0 : v0 * 0.3 * 2 ^ (-(n - 60) / 40); b_tr[i] = 14 * f < 0.4 * srate ? 14 : 7 * f < 0.4 * srate ? 7 : 3; b_ti[i] = v_sil[i] ? 0 : v0 * (0.6 + 2.4 * glass) * max(0, 1 - f / 5000); b_dk[i] = exp(-1 / (srate * (0.5 + 2.5 * slider7 / 100) * 2 ^ (-(n - 60) / 30))); b_rk[i] = exp(-1 / (srate * 0.06)); !v_sil[i] ? g_hold = g_hl; v_act[i] = 1; v_note[i] = n; v_held[i] = 1; v_sus[i] = 0; v_ns[i] = ns; v_nd[i] = n >= 89; // the top keys have no dampers agec += 1; v_age[i] = agec; ins_midi_hit(); ); function note_damp(i) local(s, kk) ( s = MS * i; kk = 0; loop(MS, s_g[s + kk] = s_gr[s + kk]; kk += 1; ); ); function note_off(n) local(i) ( i = 0; loop(NV, v_act[i] && v_held[i] && v_note[i] == n ? ( v_held[i] = 0; sus_pedal ? v_sus[i] = 1 : !v_nd[i] ? note_damp(i); ); i += 1; ); ); function all_off() local(i) ( i = 0; loop(NV, v_held[i] = 0; v_sus[i] = 0; v_act[i] ? note_damp(i); i += 1; ); ); // ---------- soundboard: 16 plate modes, constant Q so the high modes decay fastest ---------- function bp_set(c, f, q) local(w, al, a0) ( w = TP * min(f, srate * 0.45) / srate; al = sin(w) / (2 * q); a0 = 1 + al; c[0] = al / a0; c[1] = 0; c[2] = -al / a0; c[3] = -2 * cos(w) / a0; c[4] = (1 - al) / a0; ); function bq(x, c) local(y) ( y = c[0] * x + c[1] * c[5] + c[2] * c[6] - c[3] * c[7] - c[4] * c[8]; c[6] = c[5]; c[5] = x; c[8] = c[7]; c[7] = y; y; ); // SIZE: a bigger board moves every mode down (up to +/- 0.6 octave); DENSITY: denser wood rings longer (higher // Q) and carries more of the upper modes. Q is the same for every mode, so the high modes still die fastest. function body_setup(m, sz, dn) local(kk, f0, f1, f, sc_) ( f0 = m ? 85 : 52; f1 = m ? 4200 : 3200; sc_ = 2 ^ ((0.5 - sz) * 1.2); kk = 0; loop(16, f = f0 * (f1 / f0) ^ (kk / 15) * (1 + 0.07 * sin(kk * 2.7 + m)) * sc_; bp_set(BM + kk * 9, f, (m ? 8 : 11) * (0.45 + 1.3 * dn)); bm_g[kk] = 1 / (1 + (0.3 - 0.22 * dn) * kk); kk += 1; ); ); // ---------- mod rack helpers (after GFX Mod Rack) ---------- function hk_tanh(v) ( v = max(-20, min(20, v)); 1 - 2 / (exp(2 * v) + 1); ); function opc(fc) ( 1 - exp(-TP * min(fc, srate * 0.45) / srate); ); // 4-point Hermite read from a circular buffer at a fractional position function hread(buf, n, pos) local(ii, fr, xm1, x0, x1, x2, c1, c2, c3) ( pos < 0 ? pos += n; pos >= n ? pos -= n; ii = floor(pos); fr = pos - ii; xm1 = buf[(ii - 1 + n) % n]; x0 = buf[ii]; x1 = buf[(ii + 1) % n]; x2 = buf[(ii + 2) % n]; c1 = 0.5 * (x1 - xm1); c2 = xm1 - 2.5 * x0 + 2 * x1 - 0.5 * x2; c3 = 0.5 * (x2 - xm1) + 1.5 * (x0 - x1); ((c3 * fr + c2) * fr + c1) * fr + x0; ); // smoothed random: p[0] countdown, p[1] target, p[2] output function rgen(p, per, coef) ( p[0] -= 1; p[0] <= 0 ? ( p[0] = per * srate; p[1] = rand(2) - 1; ); p[2] += coef * (p[1] - p[2]); p[2]; ); RGW = alloc(3); RGC = alloc(3); vbFb = alloc(4); vbFb[0] = 60; vbFb[1] = 180; vbFb[2] = 480; vbFb[3] = 1300; vbAt = 1 - exp(-1 / (0.008 * srate)); vbRl = 1 - exp(-1 / (0.04 * srate)); // vibe voice: lamp-driven 4-stage photocell all-pass; vs[0..7] states, [8] lamp function vbvoice(x, phoff, vs) local(ph, lf, c, r, k, y, f, t, a1, xi) ( ph = vbPh + phoff; ph -= floor(ph); lf = 0.5 - 0.5 * cos(TP * ph); c = lf > vs[8] ? vbAt : vbRl; vs[8] += c * (lf - vs[8]); r = vs[8]; y = x; k = 0; loop(4, f = min(srate * 0.45, vbFb[k] * 2 ^ (vbOct * r)); t = tan($pi * f / srate); a1 = (t - 1) / (t + 1); xi = y; y = a1 * xi + vs[k * 2] - a1 * vs[k * 2 + 1]; vs[k * 2] = xi; vs[k * 2 + 1] = y; k += 1; ); vbWet * 0.5 * (y - x); ); dlLc = opc(3500); dlHc = opc(70); function dlloop(y, sp_) ( sp_[0] += dlLc * (y - sp_[0]); sp_[1] += dlHc * (sp_[0] - sp_[1]); hk_tanh((sp_[0] - sp_[1]) * dlFb * 1.15) / 1.15; ); DLS = alloc(4); // ---------- patches (Drift and Volume stay as they are) ---------- function setp(m, hm, dy, st, un, su, dm, bo, to, pr, ro, sp) ( slider2 = m; slider3 = hm; slider4 = dy; slider5 = st; slider6 = un; slider7 = su; slider8 = dm; slider9 = bo; slider10 = to; slider11 = pr; slider12 = ro; slider13 = sp; ); // striker, contact, bounce, wire age, tuning variance, board size, density, lid, mic function setq(sk, ct, bn, wa, tv, bz, dn, ld, mc) ( slider24 = sk; slider25 = ct; slider26 = bn; slider27 = wa; slider28 = tv; slider29 = bz; slider30 = dn; slider31 = ld; slider32 = mc; ); // bell, glass, ROM, wow, VHS, space type function setx(bl, gl, ro, wo, vh, ty) ( slider33 = bl; slider34 = gl; slider35 = ro; slider36 = wo; slider37 = vh; slider49 = ty; ); // chorus rate / depth / mix, trem rate / depth, vibe rate / depth, delay time / feedback / mix / sync function setm(cr, cd, cm, tr, td, vr, vd, dt, df, dm, ds) ( slider38 = cr; slider39 = cd; slider40 = cm; slider41 = tr; slider42 = td; slider43 = vr; slider44 = vd; slider45 = dt; slider46 = df; slider47 = dm; slider48 = ds; ); function load_patch(p) ( p == 0 ? ( setp(0, 80, 45, 45, 18, 55, 40, 40, 72, 20, 30, 55); setq(1, 40, 35, 0, 10, 60, 65, 0, 15); // Harbour Lights setx(45, 35, 15, 0, 10, 1); setm(0.5, 40, 25, 4, 0, 1.2, 0, 375, 25, 12, 2); ) : p == 1 ? ( setp(1, 60, 50, 30, 10, 55, 45, 20, 60, 15, 30, 45); setq(3, 35, 20, 0, 10, 40, 50, 0, 15); // Ocean Drive EP setx(85, 55, 0, 0, 10, 1); setm(0.6, 55, 45, 4.5, 45, 1.2, 0, 375, 30, 15, 1); ) : p == 2 ? ( setp(0, 55, 50, 40, 20, 70, 50, 50, 55, 35, 45, 70); setq(0, 50, 40, 10, 20, 60, 55, 0, 30); // Neon Rain setx(40, 40, 45, 35, 45, 1); setm(0.4, 60, 50, 4, 0, 1, 0, 400, 45, 25, 2); ) : p == 3 ? ( setp(0, 82, 35, 50, 25, 45, 35, 35, 72, 15, 20, 50); setq(1, 30, 30, 0, 15, 55, 70, 0, 10); // Sunset Strip setx(40, 30, 25, 0, 0, 0); setm(0.8, 35, 30, 4, 0, 1, 0, 375, 20, 10, 3); ) : p == 4 ? ( setp(0, 40, 55, 40, 25, 80, 55, 55, 45, 50, 70, 75); setq(0, 60, 40, 15, 25, 70, 50, 0, 50); // Mall Fountain setx(30, 30, 35, 70, 70, 1); setm(0.3, 70, 60, 3, 0, 0.8, 30, 500, 40, 20, 0); ) : p == 5 ? ( setp(1, 25, 45, 40, 25, 50, 40, 55, 35, 30, 35, 40); setq(3, 60, 20, 30, 30, 40, 45, 2, 30); // Night Bus setx(20, 20, 30, 40, 75, 0); setm(0.5, 40, 30, 2.5, 35, 1, 0, 450, 35, 18, 2); ) : p == 6 ? ( setp(0, 70, 50, 40, 15, 65, 45, 35, 65, 25, 45, 65); setq(1, 40, 30, 0, 10, 55, 60, 0, 25); // Palm Glass setx(90, 85, 10, 0, 15, 1); setm(0.7, 50, 40, 4, 0, 1, 0, 375, 40, 22, 2); ) : p == 7 ? ( setp(0, 40, 65, 40, 22, 75, 55, 60, 45, 50, 35, 65); setq(0, 60, 45, 10, 20, 75, 50, 1, 40); // Cassette Ballad setx(25, 25, 0, 30, 50, 1); setm(0.4, 40, 25, 4, 0, 1, 0, 375, 25, 0, 0); ) : p == 8 ? ( setp(0, 85, 40, 45, 20, 50, 40, 40, 72, 15, 55, 60); setq(1, 35, 30, 0, 10, 60, 65, 0, 15); // Gated Romance setx(50, 45, 10, 0, 10, 2); setm(0.6, 45, 35, 4, 0, 1, 0, 375, 20, 0, 0); ) : p == 9 ? ( setp(1, 55, 55, 30, 10, 60, 45, 20, 55, 15, 35, 50); setq(3, 40, 25, 0, 10, 40, 50, 0, 20); // Pastel Tine setx(75, 70, 15, 15, 30, 1); setm(0.5, 40, 20, 4, 0, 1.4, 60, 375, 30, 0, 0); ) : p == 10 ? ( setp(1, 75, 40, 50, 30, 45, 40, 50, 65, 20, 15, 40); setq(1, 40, 35, 20, 25, 40, 55, 0, 20); // Floppy Disk setx(40, 35, 85, 10, 30, 0); setm(0.5, 30, 0, 4, 0, 1, 0, 375, 20, 0, 0); ) : ( setp(0, 20, 55, 35, 20, 80, 55, 60, 40, 55, 70, 80); setq(3, 60, 15, 0, 15, 70, 50, 1, 50); // Harbour Fog setx(35, 25, 20, 30, 55, 1); setm(0.25, 65, 55, 3, 0, 0.6, 40, 600, 55, 30, 0); ); slider1 = p; last_patch = p; edited = 0; slider_automate(8191 + 511 * 8388608); // sliders 1-13 and 24-32 slider_automate(slider33); slider_automate(slider34); slider_automate(slider35); slider_automate(slider36); slider_automate(slider37); slider_automate(slider38); slider_automate(slider39); slider_automate(slider40); slider_automate(slider41); slider_automate(slider42); slider_automate(slider43); slider_automate(slider44); slider_automate(slider45); slider_automate(slider46); slider_automate(slider47); slider_automate(slider48); slider_automate(slider49); ); function patch_name(p) ( p == 0 ? "HARBOUR LIGHTS" : p == 1 ? "OCEAN DRIVE EP" : p == 2 ? "NEON RAIN" : p == 3 ? "SUNSET STRIP" : p == 4 ? "MALL FOUNTAIN" : p == 5 ? "NIGHT BUS" : p == 6 ? "PALM GLASS" : p == 7 ? "CASSETTE BALLAD" : p == 8 ? "GATED ROMANCE" : p == 9 ? "PASTEL TINE" : p == 10 ? "FLOPPY DISK" : "HARBOUR FOG"; ); // ---------- parameters ---------- function update() ( model = slider2 > 0.5; ham = slider3 / 100; dyn = slider4 / 100; stiff = slider5 / 100; striker = floor(slider24 + 0.5); contact = slider25 / 100; contact_m = 0.45 * 5 ^ contact; bounce = slider26 / 100; wage = slider27 / 100; tvar = slider28 / 100; bsz = slider29 / 100; bdn = slider30 / 100; lid = floor(slider31 + 0.5); mic = slider32 / 100; uni_c = (slider6 / 100) ^ 1.6 * 28; // 0 .. 28 cents (honky-tonk at the top) sus_t = 0.25 + 1.5 * (slider7 / 100) ^ 1.3; dmp_t = 0.25 + 2.75 * (slider8 / 100) ^ 1.5; // SUSTAIN / DAMPER reach the notes already sounding (sus_t != last_sus || dmp_t != last_dmp) ? ( last_sus = sus_t; last_dmp = dmp_t; k_ = 0; loop(NV, v_act[k_] ? ( j_ = 0; loop(v_ns[k_], s_ = MS * k_ + j_; str_regain(s_); s_g[s_] = (v_held[k_] || v_sus[k_] || v_nd[k_] || sus_pedal) ? s_gh[s_] : s_gr[s_]; j_ += 1; ); ); k_ += 1; ); ); body = slider9 / 100; tone_s = slider10 / 100; tilt = (tone_s - 0.5) * 1.1; res = slider11 / 100; room = slider12 / 100; spread = slider13 / 100; // bridge impedance (in string impedances): a lighter bridge couples the strings more strongly // The bridge takes a little energy whenever a key's strings swing together (the prompt sound), not when // they swing against each other (the aftersound): that is the piano's two-stage decay. zb = res > 0.005 ? 900 + 4000 * (1 - res) ^ 2 : 1000000000; drift = slider14 / 100; vol_t = 10 ^ (slider15 / 20); // SPACE: ROOM (0.5 .. 1.3 s), PLATE (bright, 1.4 .. 4 s), GATED (a long, bright tail cut short after each hit) sp_ty = floor(slider49 + 0.5); sp_k = sp_ty == 1 ? 0.6 : sp_ty == 2 ? 0.85 : 1; sp_t60 = sp_ty == 1 ? 1.4 + 2.6 * room : sp_ty == 2 ? 2.4 : 0.5 + 0.8 * room; sp_lv = sp_ty == 1 ? 0.8 : sp_ty == 2 ? 1.4 : 1; rm_g = 10 ^ (-3 * 0.015 * sp_k / sp_t60); ml0 = floor(0.0073 * srate * sp_k); ml1 = floor(0.0119 * srate * sp_k); ml2 = floor(0.0171 * srate * sp_k); ml3 = floor(0.0217 * srate * sp_k); mlp = 1 - exp(-TP * (sp_ty == 1 ? 10000 : sp_ty == 2 ? 8000 : 6500) / srate); // BELL, ROM, WOW, VHS bell_l = slider33 / 100; glass = slider34 / 100; bl_body = 0.3 + 0.9 * glass; rom = slider35 / 100; rom_on = rom > 0.005; rom_inc = 2 ^ (-2.2 * rom) * min(1, 48000 / srate); rom_q = 2 ^ (15 - 8 * rom); wow = slider36 / 100; wow_d = wow * 0.004 * srate; wow_f = wow * 0.00012 * srate; vhs = slider37 / 100; vhs_lp = opc(18000 * 2 ^ (-2.2 * vhs)); vhs_h = vhs * 0.0025; vhs_dr = 1 + 3 * vhs; // MOD RACK chR = slider38; chDep = slider39 / 100 * 4; ch_mix = slider40 / 100; trR = slider41; tr_d = slider42 / 100; vbR = slider43; vbOct = 3 * slider44 / 100; vbWet = min(1, 2 * slider44 / 100); dlFb = 0.95 * slider46 / 95; dl_mix = slider47 / 100; dl_sync = floor(slider48 + 0.5); ins_output_setup(16); ins_stereo_setup(22); ); last_model = -1; last_sus = -1; last_dmp = -1; tkdec = exp(-1 / (0.0025 * srate)); bsz_s = 0.5; bdn_s = 0.55; lid_lp = 1; lid_hs = 0; lid_bx = 0; lid_r0 = 0; lid_r1 = 0; lid_r2 = 0; mic_s = 0.35; lr0 = floor(0.0013 * srate); lr1 = floor(0.0009 * srate); lr2 = floor(0.0005 * srate); pdl = floor(0.02 * srate); hsc = 1 - exp(-TP * 5000 / srate); bxc = 1 - exp(-TP * 350 / srate); ld_w = 0; tlc = 1 - exp(-TP * 900 / srate); klc = 1 - exp(-TP * 700 / srate); kdec = exp(-1 / (0.012 * srate)); hpc = 1 - exp(-TP * 30 / srate); envc = 1 - exp(-1 / (0.02 * srate)); symc = 1 - exp(-1 / (0.05 * srate)); sm = 1 - exp(-1 / (0.03 * srate)); h_tmax = floor(0.06 * srate); tdec_b = exp(-1 / (0.035 * srate)); // bell tine transient cwr = 1 - exp(-1 / (0.4 * srate)); chBase = 8; chPc = 1 - exp(-TP * 9000 / srate); dlTs = 1 - exp(-1 / (0.25 * srate)); dlT = slider45; g_hl = floor(0.3 * srate); g_rel = 1 - exp(-1 / (0.025 * srate)); g_hold = 0; g_s = 0; OUTG = 0.5; // output calibration update(); vol_s = vol_t; !ui_dflt ? ( ui_thm = 9; ui_hw = 1; ); // fixed black-lacquer-and-brass faceplate last_patch = -1; ui_dflt = 1; @slider last_patch < 0 ? ( last_patch = slider1; ) : ( slider1 != last_patch ? load_patch(slider1); ); update(); @block update(); while (midirecv(mofs, m1, m2, m3)) ( st = m1 & 0xF0; st == 0x90 && m3 > 0 ? ( m2 >= 21 && m2 <= 108 ? note_on(m2, m3); ) : // the 88 keys, A0 - C8 st == 0x80 || (st == 0x90 && m3 == 0) ? note_off(m2) : st == 0xB0 && m2 == 64 ? ( sus_pedal = m3 >= 64; sus_pedal ? sym_on = 1 : ( i = 0; loop(NV, v_act[i] && !v_held[i] && !v_nd[i] ? note_damp(i); v_sus[i] = 0; i += 1; ); ); ) : st == 0xB0 && m2 == 67 ? soft_pedal = m3 >= 64 : st == 0xB0 && (m2 == 123 || m2 == 120) ? ( all_off(); sus_pedal = 0; ); midisend(mofs, m1, m2, m3); ); ins_drift_block(drift, samplesblock); // delay time: ms, or a note value at the project tempo (capped at 1.5 s) dq_ = dl_sync == 1 ? 1 : dl_sync == 2 ? 0.75 : dl_sync == 3 ? 0.5 : 2 / 3; dl_tgt = dl_sync > 0 && tempo > 0 ? min(1500, dq_ * 60000 / tempo) : slider45; // soundboard glides to new SIZE / DENSITY / model settings (rebuilt a little each block) (abs(bsz - bsz_s) > 0.0005 || abs(bdn - bdn_s) > 0.0005 || model != last_model) ? ( model != last_model ? ( bsz_s = bsz; bdn_s = bdn; last_model = model; ); bsz_s += (bsz - bsz_s) * 0.15; bdn_s += (bdn - bdn_s) * 0.15; body_setup(model, bsz_s, bdn_s); ); // lid targets: brightness, the lid's reflections, the boxed-in sound of a closed lid lid == 0 ? ( lt_lp = 20000; lt_hs = 0.26; lt_bx = 0; lt_r0 = 0.35; lt_r1 = 0; lt_r2 = 0; ) : lid == 1 ? ( lt_lp = 11000; lt_hs = 0; lt_bx = 0.06; lt_r0 = 0; lt_r1 = 0.22; lt_r2 = 0; ) : ( lt_lp = 3200; lt_hs = -0.3; lt_bx = 0.26; lt_r0 = 0; lt_r1 = 0; lt_r2 = 0.12; ); // mic: close .. far (air absorption, level, width, room) mic_s += (mic - mic_s) * 0.1; mic_air = 1 - exp(-TP * min(srate * 0.45, 20000 * 2 ^ (-1.6 * mic_s)) / srate); mic_dg = 1 - 0.45 * mic_s; mic_ws = 1 - 0.55 * mic_s; mic_rm = 0.25 + 1.2 * mic_s; mic_k = 1.6 - mic_s; @sample sL = sR = 0; bsum = 0; bsy = 0; nj = 0; wr = gw & RM; // ---------- keys, pass 1: hammer, then each string's wave arriving at the bridge ---------- i = 0; loop(NV, v_act[i] ? ( ns = v_ns[i]; s0 = MS * i; // hammer against the string: compression of the felt, force, both bodies move dvh = 0; h_on[i] ? ( vs = 0; s = s0; loop(ns, rb = RAIL + s * RS2; hh = s_h[s]; vs += rb[(gw - hh) & RM] + rb[RS + ((gw - s_N[s] + hh) & RM)]; s += 1; ); // felt compression solved implicitly against the string's own reaction (its impedance, 2 x strings), // so even a stiff treble felt stays stable: c = c0 + (vh - vs_in) - F(c) / Zt, a few Newton steps vin = vs / ns; cn = h_y[i] - h_ys[i]; u = h_v[i] - vin; cmp = cn + u; pw = h_pw[i]; kz = h_K[i] / (2 * ns); cmp > 0 ? ( loop(4, cpw = cmp ^ pw; cmp = max(0.0000000001, cmp - (cmp - cn - u + kz * cpw) / (1 + kz * pw * cpw / cmp)); ); cpw = cmp ^ pw; ) : cpw = 0; hfc = max(0, h_K[i] * (cpw + h_al[i] * (cpw - h_cp[i]))); h_cp[i] = cpw; h_typ[i] == 2 && hfc > h_fx[i] ? ( hfc = 0; h_on[i] = 0; ); // the plectrum slips off the string hfc > 0 ? h_ct[i] = 1; h_ys[i] += vin + hfc / (2 * ns); h_y[i] += h_v[i]; h_v[i] -= hfc * h_im[i]; dvh = hfc * 0.5 / ns; // the force launches a velocity wave both ways on each string hfc > 0 ? h_fr[i] = 0 : h_fr[i] += 1; // after the first blow a low BOUNCE lets the backcheck catch the hammer; a high one lets it strike again (h_fr[i] > 64 && h_v[i] < 0) || (h_ct[i] && h_fr[i] > 2 && bounce < 0.35) || v_t[i] > h_tmax ? h_on[i] = 0; ); v_dv[i] = dvh; vo = 0; s = s0; loop(ns, rb = RAIL + s * RS2; ri = (gw - s_N[s]) & RM; oR = rb[ri]; s_ol[s] = rb[RS + ri]; // towards the bridge: fractional tuner, stiffness dispersion, losses e = s_eta[s]; y = e * oR + s_tx[s] - e * s_ty[s]; s_tx[s] = oR; s_ty[s] = y; a = s_a[s]; o = a * y + s_x1[s] - a * s_y1[s]; s_x1[s] = y; s_y1[s] = o; y = a * o + s_x2[s] - a * s_y2[s]; s_x2[s] = o; s_y2[s] = y; s_nap[s] > 2 ? ( o = a * y + s_x3[s] - a * s_y3[s]; s_x3[s] = y; s_y3[s] = o; y = a * o + s_x4[s] - a * s_y4[s]; s_x4[s] = o; s_y4[s] = y; ); pp = s_p[s]; lp = (1 - pp) * y + pp * s_lp[s]; s_lp[s] = lp; y = s_g[s] * lp; s_yi[s] = y; bsy += y; nj += 1; vo += oR * s_w[s]; s += 1; ); // action knock v_ka[i] > 0.0000001 ? ( v_kl[i] += ((rand(2) - 1) * v_ka[i] - v_kl[i]) * klc; v_ka[i] *= kdec; ) : v_kl[i] = 0; v_tk[i] > 0.0000001 ? ( nz = rand(2) - 1; v_tl[i] = nz - v_tl[i] * 0.2; v_tk[i] *= tkdec; vx = v_tk[i] * (nz - v_tl[i]) * 0.5; ) : vx = 0; vx = (vo + v_kl[i] * mic_k) * v_gn[i] + vx; bsum += vx; b_a[i] > 0.000005 ? ( b_ph[i] += b_inc[i]; b_ph[i] -= floor(b_ph[i]); b_mp[i] += b_inc[i] * b_tr[i]; b_mp[i] -= floor(b_mp[i]); vx += bell_l * b_a[i] * sin(TP * b_ph[i] + b_ti[i] * sin(TP * b_mp[i]) + bl_body * sin(TP * b_ph[i])); b_ti[i] *= tdec_b; b_a[i] *= (v_held[i] || v_sus[i] || sus_pedal) ? b_dk[i] : b_rk[i]; ); sL += vx * v_pl[i]; sR += vx * v_pr[i]; v_env[i] += (abs(vo) - v_env[i]) * envc; ); i += 1; ); // ---------- open strings (pedal down), pass 1 ---------- ysum = 0; sym_on ? ( j = 0; loop(NY, x = YB[j * YS + ((gw - y_L[j]) & YM)]; e = y_eta[j]; y = e * x + y_tx[j] - e * y_ty[j]; y_tx[j] = x; y_ty[j] = y; pp = y_p[j]; lp = (1 - pp) * y + pp * y_lp[j]; y_lp[j] = lp; y = -(sus_pedal ? y_g[j] : y_gd[j]) * lp; y_yi[j] = y; bsy += y; nj += 1; ysum += x; j += 1; ); ); // ---------- the bridge: every string meets it at one junction ---------- // force balance Zb vb = sum Z (v_in - v_out), v_out = vb - v_in -> vb = 2 sum v_in / (Zb + N) // lossless scattering into a resistive bridge: energy is only ever shared or lost, so any number of // coupled strings stays stable. Strings with their dampers up ring in sympathy through it. vb = 2 * bsy / (zb + nj); // ---------- pass 2: reflections, hammer forces, retire silent keys ---------- i = 0; loop(NV, v_act[i] ? ( ns = v_ns[i]; s = MS * i; dvh = v_dv[i]; loop(ns, rb = RAIL + s * RS2; rb[wr] = -s_ol[s]; // agraffe: inverting reflection rb[RS + wr] = vb - s_yi[s]; // bridge junction dvh != 0 ? ( hh = s_h[s]; rb[(gw - hh) & RM] += dvh; rb[RS + ((gw - s_N[s] + hh) & RM)] += dvh; ); s += 1; ); v_t[i] += 1; v_t[i] > 2000 && !h_on[i] && b_a[i] < 0.00002 ? ( ((v_env[i] < 0.00001 && !v_held[i] && !v_sus[i]) || (v_env[i] < 0.0000005 && !v_sil[i])) ? v_act[i] = 0; ); ); i += 1; ); sym_on ? ( wy = gw & YM; j = 0; loop(NY, YB[j * YS + wy] = vb - y_yi[j]; j += 1; ); sym_env += (abs(ysum) - sym_env) * symc; !sus_pedal && sym_env < 0.000001 ? ( sym_on = 0; memset(YB, 0, NY * YS); memset(y_tx, 0, NY); memset(y_ty, 0, NY); memset(y_lp, 0, NY); ); ); gw = (gw + 1) & 1048575; // ---------- soundboard: the bridge drives the plate modes ---------- m = (bsum + 0.5 * ysum) * OUTG; hpz += (m - hpz) * hpc; mh = m - hpz; bo = 0; be = 0; kk = 0; loop(8, bo += bq(mh, BM + kk * 18) * bm_g[kk * 2]; be += bq(mh, BM + kk * 18 + 9) * bm_g[kk * 2 + 1]; kk += 1; ); xL = (sL + 0.5 * ysum * 0.7) * OUTG * (1 - 0.4 * body) + body * 1.6 * (bo + 0.5 * be); xR = (sR + 0.5 * ysum * 0.7) * OUTG * (1 - 0.4 * body) + body * 1.6 * (be + 0.5 * bo); // ---------- lid (smoothed: no zipper noise) ---------- lsm = 0.0005; lid_lp += (lt_lp - lid_lp) * lsm; lid_hs += (lt_hs - lid_hs) * lsm; lid_bx += (lt_bx - lid_bx) * lsm; lid_r0 += (lt_r0 - lid_r0) * lsm; lid_r1 += (lt_r1 - lid_r1) * lsm; lid_r2 += (lt_r2 - lid_r2) * lsm; lpc_ = 1 - exp(-TP * min(lid_lp, srate * 0.45) / srate); LD[ld_w] = xL; LD[2048 + ld_w] = xR; xL += lid_r0 * LD[(ld_w - lr0) & 2047] + lid_r1 * LD[(ld_w - lr1) & 2047] + lid_r2 * LD[(ld_w - lr2) & 2047]; xR += lid_r0 * LD[2048 + ((ld_w - lr0 - 7) & 2047)] + lid_r1 * LD[2048 + ((ld_w - lr1 - 5) & 2047)] + lid_r2 * LD[2048 + ((ld_w - lr2 - 3) & 2047)]; ld_w = (ld_w + 1) & 2047; hzL += (xL - hzL) * hsc; xL += lid_hs * (xL - hzL); hzR += (xR - hzR) * hsc; xR += lid_hs * (xR - hzR); bzL += (xL - bzL) * bxc; bwL += (bzL - bwL) * bxc; xL += lid_bx * (bzL - bwL) * 4; bzR += (xR - bzR) * bxc; bwR += (bzR - bwR) * bxc; xR += lid_bx * (bzR - bwR) * 4; lzL += (xL - lzL) * lpc_; xL = lzL; lzR += (xR - lzR) * lpc_; xR = lzR; // ---------- mic distance: air absorption, level, width ---------- azL += (xL - azL) * mic_air; azR += (xR - azR) * mic_air; mid_ = 0.5 * (azL + azR); sid_ = 0.5 * (azL - azR) * mic_ws; xL = (mid_ + sid_) * mic_dg; xR = (mid_ - sid_) * mic_dg; // ---------- ROM: sample-rate and bit-depth reduction (aliasing left in) ---------- rom_on ? ( rom_ph += rom_inc; rom_ph >= 1 ? ( rom_ph -= 1; rhL = floor(xL * rom_q + 0.5) / rom_q; rhR = floor(xR * rom_q + 0.5) / rom_q; ); xL = rhL; xR = rhR; ); // ---------- tape: WOW (speed wander) and VHS (worn top, hiss, saturation) ---------- TPA[tp_w] = xL; TPB[tp_w] = xR; wow > 0.002 ? ( wph += 0.55 / srate; wph -= floor(wph); fph += 6.5 / srate; fph -= floor(fph); dmod = 2 + wow_d * (0.5 + 0.5 * sin(TP * wph)) + wow_f * (0.5 + 0.5 * sin(TP * fph)) + wow_d * 0.3 * (0.5 + 0.5 * rgen(RGW, 1.5, cwr)); xL = hread(TPA, TPN, tp_w - dmod); xR = hread(TPB, TPN, tp_w - dmod); ); tp_w = (tp_w + 1) & (TPN - 1); vhs > 0.002 ? ( vaL += (xL - vaL) * vhs_lp; vbL_ += (vaL - vbL_) * vhs_lp; vaR += (xR - vaR) * vhs_lp; vbR_ += (vaR - vbR_) * vhs_lp; xL = hk_tanh((vbL_ + (rand(2) - 1) * vhs_h) * vhs_dr) / vhs_dr; xR = hk_tanh((vbR_ + (rand(2) - 1) * vhs_h) * vhs_dr) / vhs_dr; ); // ---------- tone (tilt about 900 Hz) ---------- tzL += (xL - tzL) * tlc; xL += tilt * (xL - 2 * tzL); tzR += (xR - tzR) * tlc; xR += tilt * (xR - 2 * tzR); // ---------- MOD RACK: chorus, vibe, trem (auto-pan), ping-pong delay ---------- cpL += chPc * (xL - cpL); cpR += chPc * (xR - cpR); CHA[ch_w] = cpL; CHB[ch_w] = cpR; ch_mix > 0.001 ? ( chPh += chR / srate; chPh -= floor(chPh); xL += ch_mix * hread(CHA, CHN, ch_w - (chBase + chDep * sin(TP * chPh)) * srate * 0.001); xR += ch_mix * hread(CHB, CHN, ch_w - (chBase + chDep * sin(TP * (chPh + 0.25))) * srate * 0.001); ); ch_w = (ch_w + 1) & (CHN - 1); vbWet > 0 ? ( vbPh += vbR / srate; vbPh -= floor(vbPh); xL += vbvoice(xL, 0, VSA); xR += vbvoice(xR, 0.25, VSB); ); tr_d > 0.001 ? ( trPh += trR / srate; trPh -= floor(trPh); sv_ = sin(TP * trPh); xL *= 1 - tr_d * (0.5 - 0.5 * sv_); xR *= 1 - tr_d * (0.5 + 0.5 * sv_); ); dlT += dlTs * (dl_tgt - dlT); dsm = max(2, min(DLN * 0.9, dlT * srate * 0.001)); dyA = hread(DLA, DLN, dl_w - dsm); dyB = hread(DLB, DLN, dl_w - dsm); DLA[dl_w] = (dl_mix > 0) * 0.5 * (xL + xR) + dlloop(dyB, DLS); // in on the left, then across: ping-pong DLB[dl_w] = dlloop(dyA, DLS + 2); xL += dl_mix * dyA; xR += dl_mix * dyB; dl_w += 1; dl_w >= DLN ? dl_w = 0; // ---------- space (4-line feedback delay network: room, plate or gated) ---------- PD[pd_w] = 0.5 * (xL + xR); pdo = PD[(pd_w - pdl) & 4095]; pd_w = (pd_w + 1) & 4095; mm = (1 - mic_s) * 0.5 * (xL + xR) + mic_s * pdo; // the far mic hears the room a little later q0 = FRM[(f_wp - ml0) & FM]; q1 = FRM[FSZ + ((f_wp - ml1) & FM)]; q2 = FRM[2 * FSZ + ((f_wp - ml2) & FM)]; q3 = FRM[3 * FSZ + ((f_wp - ml3) & FM)]; qz0 += (q0 - qz0) * mlp; qz1 += (q1 - qz1) * mlp; qz2 += (q2 - qz2) * mlp; qz3 += (q3 - qz3) * mlp; qin = mm * room * 0.6 * mic_rm; FRM[f_wp] = qin + 0.5 * rm_g * (qz0 + qz1 + qz2 + qz3); FRM[FSZ + f_wp] = qin + 0.5 * rm_g * (qz0 - qz1 + qz2 - qz3); FRM[2 * FSZ + f_wp] = qin + 0.5 * rm_g * (qz0 + qz1 - qz2 - qz3); FRM[3 * FSZ + f_wp] = qin + 0.5 * rm_g * (qz0 - qz1 - qz2 + qz3); g_hold -= 1; g_t = g_hold > 0; g_s += (g_t - g_s) * (g_t ? 0.01 : g_rel); gm = (sp_ty == 2 ? g_s : 1) * sp_lv; xL += room * (qz0 - qz2) * 0.7 * gm; xR += room * (qz1 - qz3) * 0.7 * gm; f_wp = (f_wp + 1) & FM; // ---------- volume + output stage ---------- vol_s += (vol_t - vol_s) * sm; oL = ostL.ost_band(xL) * vol_s; oR = ostR.ost_band(xR) * vol_s; ins_stereo(oL, oR); ost_out(ins_sl, ins_sr); spl0 = ost_l; spl1 = ost_r; @serialize file_var(0, ui_sz); file_var(0, edited); @gfx 860 586 ui_thm = 9; ui_hw = 1; ui_begin(860, 586); // night-drive faceplate: deep indigo into plum, neon pink and cyan // GRAND: indigo into plum; UPRIGHT: pastel blue into cyan uprt = slider2 > 0.5; uprt ? ( P_TOP = 0x9DBBD6; P_BOT = 0x6AA6BE; INK = 0x1F2D4A; NEON = 0xE8489A; CYAN = 0x5CF0FF; SUN1 = 0xFFC27A; SUN2 = 0xFF7AB6; STC = 0x154F70; TCORE = 0x5B2A8C; TGLO = 0x8A4FD0; STAR = 0.0; ) : ( P_TOP = 0x160D2E; P_BOT = 0x3B1247; INK = 0xD9D2F2; NEON = 0xFF4FB4; CYAN = 0x5CF0FF; SUN1 = 0xFFB347; SUN2 = 0xFF3E8A; STC = CYAN; TCORE = 0xFFD6F0; TGLO = NEON; STAR = 1; ); GOLD = CYAN; GOLDL = CYAN; LEDC = NEON; // ---------- helpers ---------- function lbl(x, y, s, a) ( gfx_setfont(5); ui_cola(INK, a); ui_text(x * sc, y * sc, s); ); function stitle(cx, y, s) ( gfx_setfont(5); ui_cola(STC, 0.25); ui_text(cx * sc + 1, y * sc + 1, s); ui_col(STC); ui_text(cx * sc, y * sc, s); ); function divider(x, y1, y2) ( ui_cola(NEON, 0.30); gfx_line(x * sc, y1 * sc, x * sc, y2 * sc); ); function hline(y) ( ui_cola(NEON, 0.35); gfx_line(24 * sc, y * sc, 836 * sc, y * sc); ui_cola(NEON, 0.10); gfx_line(24 * sc, y * sc + 1, 836 * sc, y * sc + 1); ); function hexf(c, k) ( ((c >> (16 - 8 * k)) & 255) / 255; ); function lkey(x, y, w, h, on, s) local(hit) ( hit = pk_ledbtn(x * sc, y * sc, w * sc, h * sc, on, LEDC); lbl(x + w * 0.5, y + h + 4, s, on ? 1 : 0.7); hit; ); function pbtn(x, y, w, h) local(hit, inb) ( inb = mouse_x >= x * sc && mouse_x < (x + w) * sc && mouse_y >= y * sc && mouse_y < (y + h) * sc; hit = pressed && drag_id == 0 && inb; hit ? drag_id = -1; ui_keyshadow(x * sc, y * sc, w * sc, h * sc, 2 * sc); ui_col(0x0E0A18); ui_rrs(x * sc, y * sc, w * sc, h * sc, 2 * sc); ui_col(mdown && inb ? 0x1A1428 : 0x251D38); ui_rrect((x + 2) * sc, (y + 2) * sc, (w - 4) * sc, (h - 5) * sc, 1.5 * sc); hit; ); function kname(id) ( id == 3 ? "HARDNESS" : id == 4 ? "DYNAMICS" : id == 6 ? "UNISON" : id == 7 ? "SUSTAIN" : id == 10 ? "TONE" : id == 12 ? "SPACE" : id == 13 ? "SPREAD" : id == 14 ? "DRIFT" : id == 33 ? "BELL" : id == 34 ? "GLASS" : id == 35 ? "ROM" : id == 36 ? "WOW" : id == 37 ? "VHS" : id == 38 ? "CHORUS RATE" : id == 39 ? "CHORUS DEPTH" : id == 40 ? "CHORUS MIX" : id == 41 ? "TREM RATE" : id == 42 ? "TREM DEPTH" : id == 43 ? "VIBE RATE" : id == 44 ? "VIBE DEPTH" : id == 45 ? "DELAY TIME" : id == 46 ? "FEEDBACK" : id == 47 ? "DELAY MIX" : ""; ); // ---------- cabinet ---------- fh = 570; ui_col(0x06040C); gfx_rect(0, 0, gfx_w, gfx_h); ui_cola(0x000000, 0.6); ui_rrect(10 * sc, 12 * sc, 844 * sc, fh * sc, 7 * sc); ui_col(0x0A0614); ui_rrect(8 * sc, 8 * sc, 844 * sc, fh * sc, 7 * sc); gh_ = (fh - 2) * sc; gfx_gradrect(9 * sc, 9 * sc, 842 * sc, gh_, hexf(P_TOP, 0), hexf(P_TOP, 1), hexf(P_TOP, 2), 1, 0, 0, 0, 0, (hexf(P_BOT, 0) - hexf(P_TOP, 0)) / gh_, (hexf(P_BOT, 1) - hexf(P_TOP, 1)) / gh_, (hexf(P_BOT, 2) - hexf(P_TOP, 2)) / gh_, 0); // stars gk_ = 0; loop(70, ax_ = 14 + 832 * ui_hash(gk_ * 3.17 + 1); ay_ = 14 + 104 * ui_hash(gk_ * 7.31 + 2); ui_cola(0xFFFFFF, STAR * (0.15 + 0.35 * ui_hash(gk_ * 5.03))); gfx_circle(ax_ * sc, ay_ * sc, (0.4 + 0.8 * ui_hash(gk_ * 2.77)) * sc, 1, 1); gk_ += 1; ); // sunset: a striped sun rising behind the horizon line sy_ = 126; sr_ = 44; sx_ = 792; gk_ = 0; loop(sr_, yy_ = sy_ - sr_ + gk_; t_ = gk_ / sr_; hw_ = sqrt(max(0, sr_ * sr_ - (sr_ - gk_) * (sr_ - gk_))); (gk_ < sr_ * 0.45 || (gk_ % 7) < 7 - 5 * t_) ? ( ui_cola(ui_mix(SUN1, SUN2, t_), 0.55); gfx_line((sx_ - hw_) * sc, yy_ * sc, (sx_ + hw_) * sc, yy_ * sc, 1); ); gk_ += 1; ); // horizon and a perspective grid on the lower deck ui_cola(NEON, 0.9); gfx_rect(20 * sc, 126 * sc, 820 * sc, 1 * sc); ui_cola(NEON, 0.25); gfx_rect(20 * sc, 127 * sc, 820 * sc, 2 * sc); hy_ = 446; ui_cola(NEON, 0.7); gfx_line(20 * sc, hy_ * sc, 840 * sc, hy_ * sc); gk_ = 0; loop(6, gy_ = hy_ + (fh - hy_) * (gk_ + 1) ^ 1.8 / 6 ^ 1.8; ui_cola(NEON, 0.06 + 0.04 * gk_); gfx_line(16 * sc, gy_ * sc, 844 * sc, gy_ * sc); gk_ += 1;); gk_ = -12; loop(25, ui_cola(NEON, 0.10); gfx_line((430 + gk_ * 12) * sc, hy_ * sc, (430 + gk_ * 75) * sc, (fh + 6) * sc); gk_ += 1;); ui_cola(0xFFFFFF, 0.10); gfx_line(16 * sc, 9 * sc, 844 * sc, 9 * sc); // ---------- brand: neon tube title ---------- ui_col(0xB8923A); gfx_circle(50 * sc, 58 * sc, 18 * sc, 1, 1); ui_cola(0xFFFFFF, 0.25); gfx_circle(45 * sc, 52 * sc, 8 * sc, 1, 1); gfx_setfont(2); ui_col(0x1A1408); ui_text(50 * sc, 51 * sc, "GFX"); gfx_setfont(6, "Georgia", 25 * sc, 'bi'); gk_ = 0; loop(8, ui_cola(TGLO, 0.08); gfx_x = (78 + 2.5 * cos(gk_ * 0.785)) * sc; gfx_y = (36 + 2.5 * sin(gk_ * 0.785)) * sc; gfx_drawstr("Harbour Keys"); gk_ += 1;); ui_col(TCORE); gfx_x = 78 * sc; gfx_y = 36 * sc; gfx_drawstr("Harbour Keys"); gfx_setfont(4); ui_cola(STC, 0.85); gfx_x = 80 * sc; gfx_y = 76 * sc; gfx_drawstr("SYNTHETIC PIANO WITH MODULATION"); // ---------- preset bar: [PRESETS][<][>][LCD] ---------- gpx_ = 322; gpw_ = 176; // bar start, LCD width gsel = pbtn(gpx_, 38, 62, 46) ? -1 : 0; gfx_setfont(5); ui_col(CYAN); ui_text((gpx_ + 31) * sc, 55 * sc, "PRESETS"); pbtn(gpx_ + 66, 38, 26, 46) ? load_patch(slider1 <= 0 ? 11 : slider1 - 1); pbtn(gpx_ + 94, 38, 26, 46) ? load_patch(slider1 >= 11 ? 0 : slider1 + 1); ui_col(CYAN); gfx_triangle((gpx_ + 75) * sc, 59 * sc, (gpx_ + 83) * sc, 54 * sc, (gpx_ + 83) * sc, 64 * sc); gfx_triangle((gpx_ + 111) * sc, 59 * sc, (gpx_ + 103) * sc, 54 * sc, (gpx_ + 103) * sc, 64 * sc); lx_ = gpx_ + 128; ui_col(0x030208); ui_rrect(lx_ * sc, 36 * sc, gpw_ * sc, 50 * sc, 4 * sc); gfx_gradrect((lx_ + 4) * sc, 40 * sc, (gpw_ - 8) * sc, 42 * sc, 0.02, 0.10, 0.13, 1, 0, 0, 0, 0, -0.01 / (42 * sc), -0.05 / (42 * sc), -0.06 / (42 * sc), 0); gfx_setfont(7, "Courier New", 14 * sc, 'b'); ui_col(CYAN); sprintf(#l1, edited ? "P%02d*%s" : "P%02d %s", slider1 + 1, patch_name(slider1)); gfx_x = (lx_ + 9) * sc; gfx_y = 45 * sc; gfx_drawstr(#l1); gfx_setfont(8, "Courier New", 11 * sc, 'b'); ui_cola(CYAN, 0.75); strlen(kname(drag_id)) > 0 ? ( drag_id == 38 || drag_id == 41 || drag_id == 43 ? sprintf(#l2, "%-12s%5.2fHz", kname(drag_id), slider(drag_id)) : drag_id == 45 ? sprintf(#l2, "%-12s%4.0f ms", kname(drag_id), slider(drag_id)) : drag_id == 6 ? sprintf(#l2, "%-12s%4.1f c", kname(drag_id), (slider6 / 100) ^ 1.6 * 28) : sprintf(#l2, "%-12s%4.0f%%", kname(drag_id), slider(drag_id)); ) : drag_id == 15 ? sprintf(#l2, "VOLUME %5.1fdB", slider15) : ( sprintf(#l2, "%s %s PED:%s", slider2 > 0.5 ? "UPRT" : "GRND", slider24 < 0.5 ? "FELT " : slider24 < 1.5 ? "TACK " : slider24 < 2.5 ? "PLUCK " : "MALLET", sus_pedal ? "ON" : "--"); ); gfx_x = (lx_ + 9) * sc; gfx_y = 66 * sc; gfx_drawstr(#l2); pressed && drag_id == 0 && mouse_x >= lx_ * sc && mouse_x < (lx_ + gpw_) * sc && mouse_y >= 36 * sc && mouse_y < 86 * sc ? ( drag_id = -1; gsel = -1; ); gsel < 0 ? ( #pm = ""; gk_ = 0; loop(12, slider1 == gk_ ? strcat(#pm, "!"); strcat(#pm, patch_name(gk_)); gk_ < 11 ? strcat(#pm, "|"); gk_ += 1;); gfx_x = gpx_ * sc; gfx_y = 88 * sc; gsel = gfx_showmenu(#pm); gsel > 0 ? load_patch(gsel - 1); ); ui_col(CYAN); gfx_triangle((lx_ + gpw_ - 16) * sc, 74 * sc, (lx_ + gpw_ - 8) * sc, 74 * sc, (lx_ + gpw_ - 12) * sc, 79 * sc); // ---------- core model ---------- lkey(642, 40, 42, 38, slider2 < 0.5, "GRAND") ? ( slider2 = 0; ui_auto(2); edited = 1; ); lkey(692, 40, 42, 38, slider2 > 0.5, "UPRIGHT") ? ( slider2 = 1; ui_auto(2); edited = 1; ); ui_size_picker(774 * sc, 26 * sc); lit = ins_midi_lit(); lit ? ( ui_cola(CYAN, 0.25); gfx_circle(758 * sc, 64 * sc, 5.5 * sc, 1, 1); ui_col(CYAN); ) : ui_col(0x123A40); gfx_circle(758 * sc, 64 * sc, 2.8 * sc, 1, 1); lbl(780, 59, "MIDI", 0.8); sus_pedal ? ( ui_cola(NEON, 0.25); gfx_circle(806 * sc, 64 * sc, 5.5 * sc, 1, 1); ui_col(NEON); ) : ui_col(0x40142E); gfx_circle(806 * sc, 64 * sc, 2.8 * sc, 1, 1); lbl(830, 59, "PEDAL", 0.8); // ---------- knob sections ---------- ui_section(2); C_INK = INK; C_KA = NEON; C_KAH = ui_mix(NEON, 0xFFFFFF, 0.3); divider(560, 138, 286); hline(296); // ENGINE stitle(290, 140, "ENGINE"); lkey(30, 160, 38, 28, slider24 < 0.5, "FELT") ? ( slider24 = 0; ui_auto(24); edited = 1; ); lkey(78, 160, 38, 28, slider24 >= 0.5 && slider24 < 1.5, "TACK") ? ( slider24 = 1; ui_auto(24); edited = 1; ); lkey(30, 218, 38, 28, slider24 >= 1.5 && slider24 < 2.5, "PLUCK") ? ( slider24 = 2; ui_auto(24); edited = 1; ); lkey(78, 218, 38, 28, slider24 >= 2.5, "MALLET") ? ( slider24 = 3; ui_auto(24); edited = 1; ); ui_knob(162 * sc, 212 * sc, 3, 0, 100, 50, 0, "HARDNESS", "%.0f%%"); ui_knob(229 * sc, 212 * sc, 4, 0, 100, 55, 0, "DYNAMICS", "%.0f%%"); ui_knob(296 * sc, 212 * sc, 7, 0, 100, 60, 0, "SUSTAIN", "%.0f%%"); ui_knob(363 * sc, 212 * sc, 6, 0, 100, 20, 0, "UNISON", "%.0f%%"); ui_knob(437 * sc, 212 * sc, 33, 0, 100, 35, 0, "BELL", "%.0f%%"); ui_knob(504 * sc, 212 * sc, 34, 0, 100, 40, 0, "GLASS", "%.0f%%"); // ROM & TAPE stitle(700, 140, "ROM & TAPE"); ui_knob(612 * sc, 212 * sc, 35, 0, 100, 0, 0, "ROM", "%.0f%%"); ui_knob(700 * sc, 212 * sc, 36, 0, 100, 0, 0, "WOW", "%.0f%%"); ui_knob(788 * sc, 212 * sc, 37, 0, 100, 0, 0, "VHS", "%.0f%%"); // MOD RACK divider(234, 306, 432); divider(388, 306, 432); divider(540, 306, 432); stitle(124, 308, "CHORUS"); ui_knob(52 * sc, 372 * sc, 38, 0.05, 8, 0.6, 1, "RATE", "%.2f Hz"); ui_knob(124 * sc, 372 * sc, 39, 0, 100, 50, 0, "DEPTH", "%.0f%%"); ui_knob(196 * sc, 372 * sc, 40, 0, 100, 0, 0, "MIX", "%.0f%%"); stitle(311, 308, "TREM / PAN"); ui_knob(274 * sc, 372 * sc, 41, 0.1, 12, 4, 1, "RATE", "%.2f Hz"); ui_knob(348 * sc, 372 * sc, 42, 0, 100, 0, 0, "DEPTH", "%.0f%%"); stitle(464, 308, "VIBE"); ui_knob(427 * sc, 372 * sc, 43, 0.1, 10, 1.2, 1, "RATE", "%.2f Hz"); ui_knob(501 * sc, 372 * sc, 44, 0, 100, 0, 0, "DEPTH", "%.0f%%"); stitle(690, 308, "DELAY"); ui_knob(580 * sc, 372 * sc, 45, 20, 1500, 375, 1, "TIME", "%.0f ms"); ui_knob(648 * sc, 372 * sc, 46, 0, 95, 35, 0, "FEEDBACK", "%.0f%%"); ui_knob(716 * sc, 372 * sc, 47, 0, 100, 0, 0, "MIX", "%.0f%%"); ds_ = floor(slider48 + 0.5); lkey(778, 352, 44, 28, ds_ > 0, ds_ == 0 ? "MS" : ds_ == 1 ? "1/4" : ds_ == 2 ? "1/8 D" : ds_ == 3 ? "1/8" : "1/4 T") ? ( slider48 = ds_ >= 4 ? 0 : ds_ + 1; ui_auto(48); edited = 1; ); lbl(800, 336, "SYNC", 0.7); // SPACE stitle(140, 460, "SPACE"); lkey(30, 502, 38, 26, slider49 < 0.5, "ROOM") ? ( slider49 = 0; ui_auto(49); edited = 1; ); lkey(76, 502, 38, 26, slider49 >= 0.5 && slider49 < 1.5, "PLATE") ? ( slider49 = 1; ui_auto(49); edited = 1; ); lkey(122, 502, 38, 26, slider49 >= 1.5, "GATED") ? ( slider49 = 2; ui_auto(49); edited = 1; ); ui_knob(212 * sc, 520 * sc, 12, 0, 100, 30, 0, "SPACE", "%.0f%%"); ui_knob(278 * sc, 520 * sc, 13, 0, 100, 60, 0, "SPREAD", "%.0f%%"); ui_knob(344 * sc, 520 * sc, 10, 0, 100, 50, 0, "TONE", "%.0f"); (drag_id >= 3 && drag_id <= 13) || (drag_id >= 24 && drag_id <= 49) ? edited = 1; // master + IN / OUT divider(392, 456, 556); ui_section(2); C_INK = INK; C_KA = NEON; C_KAH = ui_mix(NEON, 0xFFFFFF, 0.3); stitle(470, 460, "MASTER"); stitle(606, 460, "OUTPUT"); stitle(746, 460, "STEREO"); ui_knob(436 * sc, 520 * sc, 14, 0, 100, 15, 0, "DRIFT", "%.0f%%"); ui_knob(504 * sc, 520 * sc, 15, -24, 6, 0, 0, "VOLUME", "%.1f dB"); divider(538, 456, 556); ui_knob(574 * sc, 520 * sc, 16, 0, 100, 0, 0, "SAT", "%.0f%%"); ui_okfs = 1.05; ui_outkeys(612, 495, 50, 14, 3, 21); ui_okfs = 0.85; gfx_setfont(2); ui_col(INK); ui_text(637 * sc, 546 * sc, "OUT"); divider(676, 456, 556); ui_knob(712 * sc, 520 * sc, 22, 20, 500, 20, 1, "MONO", slider22 <= 20.5 ? "OFF" : "%.0f Hz"); ui_knob(780 * sc, 520 * sc, 23, 0, 100, 0, 0, "WIDTH", "+%.0f%%"); ui_end();