desc:GFX G Rainbow // gfx-g-rainbow - Time-Lag Accumulator Organ (GFX) //tags: instrument synthesizer organ drone delay //author: JGWizrad / Garden FX (GFX) // // Requires gfx-ui (.jsfx-inc) in the same folder. // // A minimalist drone organ inspired by modified-organ / tape-loop workflows of the 1960s. // // VOICE (8-voice polyphonic, smooth stealing): // Two oscillator RANKS per voice, each at a modal INTERVAL above/below the played note, with a // band-limited WAVE blend (sine > triangle > saw > pulse), LEVEL and DETUNE. // -> ZDF ladder FILTER (4-pole, tanh-saturated feedback, half key-tracking), CUTOFF / RESO / DRIVE // -> AMP envelope (ATTACK / RELEASE) with combo-organ VIBRATO. Sustain pedal (CC64) supported. // // TIME-LAG ACCUMULATOR: two virtual tape decks in series. The organ is recorded on deck 1; // deck 1's playback (head 1) feeds deck 2 (head 2), and deck 2's playback is fed back into // deck 1's record head, so loops pile up layer on layer: // DECK 1 / DECK 2 loop lengths, 0.5 - 20 s, or click the label to sync to bars // FEEDBACK how much of each pass is re-recorded AGE tape darkening + saturation per pass // WOW wow & flutter of the transports MIX level of the loops against the organ // HOLD stop recording new input, keep the loop circulating (freeze) // // DRIFT (GFX instrument drift): slow audible wander of pitch, rank detune, pulse width and // filter, plus a small random tuning offset on every note. options:maxmem=16777216 in_pin:none out_pin:Left out_pin:Right slider1:0<0,15,1{Clean Organ,Rainbow,Dervish,Poppy,In C Pulse,Keyboard Study,Copper Cathedral,Rust Drone,Sunrise Drone,Reed Swarm,Tape Echoes,Modal Fifths,Glass Harmonics,Minor Third Mist,Pulse Machine,Frozen Choir}>-Preset slider2:30<0,100,1>-Drift (%) slider3:-6<-24,6,0.1>-Output (dB) slider10:2<0,9,1{-2 Oct,-1 Oct,Unison,Min 3rd,Maj 3rd,4th,5th,Octave,12th,2 Oct}>-Rank A Interval slider11:55<0,100,1>-Rank A Wave slider12:80<0,100,1>-Rank A Level slider13:0<-50,50,0.1>-Rank A Detune (cents) slider20:7<0,9,1{-2 Oct,-1 Oct,Unison,Min 3rd,Maj 3rd,4th,5th,Octave,12th,2 Oct}>-Rank B Interval slider21:85<0,100,1>-Rank B Wave slider22:50<0,100,1>-Rank B Level slider23:4<-50,50,0.1>-Rank B Detune (cents) slider30:2400<40,16000,1>-Filter Cutoff (Hz) slider31:20<0,100,1>-Filter Reso (%) slider32:25<0,100,1>-Filter Drive (%) slider34:30<1,3000,1>-Attack (ms) slider35:600<10,8000,1>-Release (ms) slider36:20<0,100,1>-Vibrato (%) slider40:4<0.5,20,0.01>-Deck 1 Length (s) slider41:6<0.5,20,0.01>-Deck 2 Length (s) slider42:55<0,100,1>-Feedback (%) slider43:30<0,100,1>-Age (%) slider44:20<0,100,1>-Wow (%) slider45:45<0,100,1>-Loop Mix (%) slider46:0<0,1,1{Off,On}>-Hold slider47:0<0,1,1{Sec,Bars}>-Deck 1 Sync slider48:2<0,7,1{1/2,1,2,3,4,6,8,12}>-Deck 1 Bars slider49:0<0,1,1{Sec,Bars}>-Deck 2 Sync slider50:3<0,7,1{1/2,1,2,3,4,6,8,12}>-Deck 2 Bars import gfx-ui.jsfx-inc @init // ===================================================================== // Helpers // ===================================================================== nextmem = 0; function alloc(n) local(ptr) ( ptr = nextmem; nextmem += n; ptr; ); TWO_PI = 2*$pi; NV = 8; // voices VS = 32; // slots per voice function tanh(v) ( v = max(-20, min(20, v)); 1 - 2/(exp(2*v) + 1); ); function db2g(d) ( 10^(d/20); ); function cms(t) ( 1 - exp(-1/(max(0.00001, t)*srate)); ); function opc(fc) ( 1 - exp(-TWO_PI*min(fc, srate*0.45)/srate); ); // smoothed random: p[0] countdown, p[1] target, p[2] value (-1..1) function rgen(p, period) ( p[0] -= 1; p[0] <= 0 ? ( p[0] = period*srate; p[1] = rand(2) - 1; ); p[2] += cms(period)*(p[1] - p[2]); p[2]; ); // interval index -> semitones function ivs(i) ( i == 0 ? -24 : i == 1 ? -12 : i == 2 ? 0 : i == 3 ? 3 : i == 4 ? 4 : i == 5 ? 5 : i == 6 ? 7 : i == 7 ? 12 : i == 8 ? 19 : 24; ); // bars index -> bars function bars(i) ( i == 0 ? 0.5 : i == 1 ? 1 : i == 2 ? 2 : i == 3 ? 3 : i == 4 ? 4 : i == 5 ? 6 : i == 6 ? 8 : 12; ); // ---- band-limited oscillator pieces ---- function blep(t, dt) local(x) ( t < dt ? ( x = t/dt; x + x - x*x - 1; ) : t > 1 - dt ? ( x = (t - 1)/dt; x*x + x + x + 1; ) : 0; ); // wave blend w 0..1: sine > triangle > saw > pulse (pw = pulse width) function osc(ph, dt, w, pw) local(sg, a, b, t2) ( sg = w*3; sg < 1 ? ( a = sin(TWO_PI*ph); b = 4*abs(ph - 0.5) - 1; t2 = sg; ) : sg < 2 ? ( a = 4*abs(ph - 0.5) - 1; b = 2*ph - 1 - blep(ph, dt); t2 = sg - 1; ) : ( a = 2*ph - 1 - blep(ph, dt); b = (ph < pw ? 1 : -1) + blep(ph, dt) - blep((ph - pw + 1) - floor(ph - pw + 1), dt); t2 = sg - 2; ); a + (b - a)*t2; ); // ===================================================================== // Voices // ===================================================================== // v[0] note 1 gate 2 env 3 active 4 phA 5 phB 6 incA 7 incB 8 vel 9 start count // 10 sustained 11 random tune (cents) 12..15 ladder states 16 G 17 pan vb = alloc(NV*VS); memset(vb, 0, NV*VS); vcount = 0; // pitch increment for a voice's rank (cycles per sample) function vinc(v, iv, det) ( 440*2^((v[0] - 69 + ivs(iv) + (det + dTune + v[11])/100)/12)/srate; ); // filter coefficient for a voice: cutoff with half key-tracking and drift function vcoef(v) local(fc) ( fc = max(20, min(srate*0.45, slider30*2^((v[0] - 60)/24 + 0.35*drf*dFil))); fc = tan($pi*fc/srate); v[16] = fc/(1 + fc); ); function note_on(n, vel) local(i, v, best, bv, found) ( found = -1; i = 0; loop(NV, v = vb + i*VS; (v[3] && v[0] == n) ? found = i; i += 1; ); // same note: reuse found < 0 ? ( i = 0; loop(NV, v = vb + i*VS; (found < 0 && !v[3]) ? found = i; i += 1; ); ); // free voice found < 0 ? ( // steal: oldest released, else oldest best = 10^18; i = 0; loop(NV, v = vb + i*VS; bv = v[9] - (v[1] ? 0 : 10^12); bv < best ? ( best = bv; found = i; ); i += 1; ); ); v = vb + found*VS; v[0] = n; v[1] = 1; v[3] = 1; v[10] = 0; v[8] = 0.35 + 0.65*vel/127; v[9] = (vcount += 1); v[11] = (rand(2) - 1)*5*drf; // per-note tuning offset v[17] = (found % 2) ? 0.18 : -0.18; vcoef(v); // env continues from its current level: smooth when retriggering or stealing ); function note_off(n) local(i, v) ( i = 0; loop(NV, v = vb + i*VS; (v[3] && v[0] == n && v[1]) ? ( sus ? v[10] = 1 : v[1] = 0; ); i += 1; ); ); function all_off() local(i) ( i = 0; loop(NV, (vb + i*VS)[1] = 0; (vb + i*VS)[10] = 0; i += 1; ); ); // MIDI message handler (called from @block) function handle_midi(m1, m2, m3) local(st, i, v) ( st = m1 & 0xF0; (st == 0x90 && m3 > 0) ? note_on(m2, m3) : (st == 0x80 || (st == 0x90 && m3 == 0)) ? note_off(m2) : st == 0xB0 ? ( m2 == 64 ? ( sus = m3 >= 64; !sus ? ( i = 0; loop(NV, v = vb + i*VS; v[10] ? ( v[10] = 0; v[1] = 0; ); i += 1; ); ); ) : (m2 == 120 || m2 == 123) ? all_off(); ); midi_led = 1; ); // ===================================================================== // Time-lag accumulator (mono tape loop, two playback heads) // ===================================================================== TN = floor(srate*41) + 8; // 20 s + 20 s + wow headroom tape = alloc(TN); memset(tape, 0, TN); tw = 0; L1s = slider40; L2s = slider41; // smoothed lengths (s) function tread(d) local(pos, i, f, a, b) ( pos = tw - d*srate; pos < 0 ? pos += TN; pos < 0 ? pos += TN; i = floor(pos); f = pos - i; a = tape[i]; b = tape[(i + 1) % TN]; a + (b - a)*f; ); // drift generators rg = alloc(15); memset(rg, 0, 15); // output limiter + meters limEnv = 0; limRel = exp(-1/(0.15*srate)); mOut = 0; midi_led = 0; vph = 0; wph = 0; fph = 0; ageLP = 0; ageHP = 0; // ===================================================================== // Presets // ===================================================================== function pr(ia, wa, la, da, ib, wb, lb, db) ( slider10 = ia; slider11 = wa; slider12 = la; slider13 = da; slider20 = ib; slider21 = wb; slider22 = lb; slider23 = db; ); function pf(cut, res, drv, atk, rel, vib) ( slider30 = cut; slider31 = res; slider32 = drv; slider34 = atk; slider35 = rel; slider36 = vib; ); function pa(l1, l2, fb, age, wow, mix) ( slider40 = l1; slider41 = l2; slider42 = fb; slider43 = age; slider44 = wow; slider45 = mix; slider46 = 0; slider47 = 0; slider49 = 0; ); function apply_preset(n) ( slider2 = 30; slider3 = -6; n == 0 ? ( pr(2, 55, 80, 0, 7, 85, 45, 4); pf(3200, 15, 20, 15, 300, 25); pa(4, 6, 0, 0, 0, 0); ) : // Clean Organ n == 1 ? ( pr(2, 70, 75, 0, 7, 90, 55, 5); pf(2400, 25, 30, 40, 900, 20); pa(4, 6, 60, 30, 20, 50); ) : // Rainbow n == 2 ? ( pr(2, 85, 70, -3, 6, 85, 60, 3); pf(1800, 35, 45, 60, 1200, 30); pa(3.5, 5.5, 75, 45, 35, 55); ) : // Dervish n == 3 ? ( pr(2, 95, 80, 0, 8, 70, 35, 6); pf(2800, 30, 35, 20, 500, 40); pa(8, 12, 65, 35, 25, 50); ) : // Poppy n == 4 ? ( pr(2, 60, 80, 0, 7, 40, 40, 2); pf(3000, 20, 25, 5, 250, 10); pa(4, 6, 50, 25, 10, 45); slider47 = 1; slider48 = 1; slider49 = 1; slider50 = 2; ) : // In C Pulse n == 5 ? ( pr(2, 30, 85, 0, 7, 33, 40, 3); pf(5000, 10, 15, 5, 350, 15); pa(1.5, 2.5, 55, 20, 15, 45); ) : // Keyboard Study n == 6 ? ( pr(1, 60, 70, 0, 9, 50, 45, 7); pf(1500, 20, 30, 400, 4000, 15); pa(12, 18, 80, 60, 25, 60); ) : // Copper Cathedral n == 7 ? ( pr(1, 90, 80, -6, 6, 95, 70, 8); pf(900, 60, 85, 300, 3000, 35); pa(6, 9, 85, 80, 60, 60); ) : // Rust Drone n == 8 ? ( pr(1, 20, 80, 0, 6, 40, 55, 3); pf(1200, 15, 15, 800, 4000, 0); pa(10, 14, 70, 40, 15, 55); ) : // Sunrise Drone n == 9 ? ( pr(2, 95, 75, -4, 7, 90, 55, 6); pf(3500, 30, 40, 10, 400, 45); pa(2, 3, 70, 30, 25, 50); ) : // Reed Swarm n == 10 ? ( pr(2, 50, 80, 0, 8, 30, 35, 2); pf(4000, 10, 15, 5, 300, 15); pa(0.75, 1.25, 45, 20, 10, 40); ) : // Tape Echoes n == 11 ? ( pr(2, 60, 75, 0, 6, 60, 70, 4); pf(2600, 20, 25, 60, 1200, 20); pa(5, 7.5, 65, 35, 20, 50); ) : // Modal Fifths n == 12 ? ( pr(7, 10, 70, 0, 9, 15, 50, 3); pf(9000, 10, 5, 300, 3000, 10); pa(6, 9, 75, 20, 30, 60); ) : // Glass Harmonics n == 13 ? ( pr(2, 45, 75, 0, 3, 50, 60, 5); pf(1800, 25, 20, 500, 3500, 15); pa(7, 11, 70, 50, 25, 55); ) : // Minor Third Mist n == 14 ? ( pr(2, 75, 80, 0, 7, 85, 50, 3); pf(2200, 45, 50, 2, 150, 0); pa(4, 6, 60, 30, 5, 50); slider47 = 1; slider48 = 0; slider49 = 1; slider50 = 1; ) : // Pulse Machine ( pr(2, 35, 75, 0, 6, 30, 40, 4); pf(1600, 15, 15, 1200, 6000, 25); pa(15, 20, 90, 25, 20, 65); ); // Frozen Choir ); #ui_man = "gfx-g-rainbow"; // the ? pop-up links to this manual !ui_dflt ? ( ui_sz = 2; ui_hw = 1; ui_thm = 0; lastPreset = slider1; ); // hardware look, copper scheme, large ui_dflt = 1; @serialize file_var(0, ui_sz); file_var(0, ui_szinit); file_var(0, lastPreset); file_var(0, ui_hw); file_var(0, ui_thm); @slider slider1 != lastPreset ? ( apply_preset(slider1); lastPreset = slider1; ); @block // ---- MIDI ---- while (midirecv(mofs, m1, m2, m3)) ( handle_midi(m1, m2, m3); ); // ---- control rate ---- drf = slider2/100; outG = db2g(slider3); lvA = slider12/100*0.5; lvB = slider22/100*0.5; wA = slider11/100; wB = slider21/100; atkC = cms(slider34*0.001*0.3); // ~ to 95% in the attack time relC = cms(slider35*0.001*0.3); kRes = slider31/100*4.1; // ladder feedback (self-oscillates near the top) drv = 1 + slider32/100*5; vibD = slider36/100*30; // vibrato depth, cents // drift (block-rate wander, smoothed per sample by the slow generators) dTune = 8*drf*rgen(rg, 3) ; // pitch wander, cents dDet = 6*drf*rgen(rg + 3, 4); // extra rank B detune, cents dPw = 0.15*drf*rgen(rg + 6, 5); // pulse width wander dFil = rgen(rg + 9, 6); // filter wander (scaled in vcoef) dWow = rgen(rg + 12, 2.5); // loop speed wander pw = 0.5 + dPw; // per-voice increments and filter coefficients i = 0; loop(NV, v = vb + i*VS; v[3] ? ( v[6] = vinc(v, slider10, slider13); v[7] = vinc(v, slider20, slider23 + dDet); vcoef(v); ); i += 1; ); // ---- accumulator lengths (seconds), synced to bars if chosen ---- spb = tempo > 0 ? 60/tempo : 0.5; // seconds per beat L1t = slider47 > 0.5 ? min(20, bars(slider48)*ts_num*spb) : slider40; L2t = slider49 > 0.5 ? min(20, bars(slider50)*ts_num*spb) : slider41; fbG = slider46 > 0.5 ? 0.995 : slider42/100*0.99; inG = slider46 > 0.5 ? 0 : 1; ageC = opc(16000*(1 - 0.84*slider43/100)); // tape darkening per pass ageH = opc(35); ageSat = 1 + slider43/100*1.5; wowD = slider44/100; loopMix = slider45/100; lenC = cms(0.4); // tape-speed glide when lengths change @sample // ---- vibrato (shared) ---- vph += 5.8*(1 + 0.08*drf*dWow)/srate; vph -= floor(vph); vm = 2^(vibD*sin(TWO_PI*vph)/1200); // ---- voices ---- oL = 0; oR = 0; i = 0; loop(NV, v = vb + i*VS; v[3] ? ( // envelope (continues from the current level on retrigger / steal) v[2] += v[1] ? atkC*(1.0 - v[2]) : relC*(0 - v[2]); (!v[1] && v[2] < 0.0001) ? ( v[3] = 0; v[2] = 0; v[12] = v[13] = v[14] = v[15] = 0; ) : ( // ranks dtA = v[6]*vm; dtB = v[7]*vm; v[4] += dtA; v[4] -= floor(v[4]); v[5] += dtB; v[5] -= floor(v[5]); x = lvA*osc(v[4], dtA, wA, pw) + lvB*osc(v[5], dtB, wB, 1 - pw); // ZDF 4-pole ladder with tanh feedback saturation G = v[16]; sig = (1 - G)*(G*G*G*v[12] + G*G*v[13] + G*v[14] + v[15]); u = (x*drv - kRes*sig)/(1 + kRes*G*G*G*G); u = tanh(u); t = G*(u - v[12]); y = t + v[12]; v[12] = y + t; t = G*(y - v[13]); y = t + v[13]; v[13] = y + t; t = G*(y - v[14]); y = t + v[14]; v[14] = y + t; t = G*(y - v[15]); y = t + v[15]; v[15] = y + t; y *= (1 + 0.5*kRes)/drv^0.5; y *= v[2]*v[8]; oL += y*(1 - v[17]); oR += y*(1 + v[17]); ); ); i += 1; ); oL *= 0.35; oR *= 0.35; // ---- time-lag accumulator ---- L1s += lenC*(L1t - L1s); L2s += lenC*(L2t - L2s); wph += (0.43 + 0.1*drf*dWow)/srate; wph -= floor(wph); fph += 6.7/srate; fph -= floor(fph); wobble = wowD*(0.006*sin(TWO_PI*wph) + 0.0004*sin(TWO_PI*fph)); // seconds tap1 = tread(L1s + wobble); tap2 = tread(L1s + L2s + wobble*1.3); fbs = fbG*tap2; // deck 2 playback -> deck 1 record ageLP += ageC*(fbs - ageLP); // darken ageHP += ageH*(ageLP - ageHP); // keep the lows clean fbs = tanh((ageLP - ageHP)*ageSat)/ageSat; // tape saturation tape[tw] = inG*0.5*(oL + oR) + fbs; tw += 1; tw >= TN ? tw = 0; aL = loopMix*(0.85*tap1 + 0.35*tap2); aR = loopMix*(0.35*tap1 + 0.85*tap2); // ---- output ---- outL = (oL + aL)*outG; outR = (oR + aR)*outG; pk = max(abs(outL), abs(outR)); limEnv = pk > limEnv ? pk : limEnv*limRel; lg = limEnv > 0.966 ? 0.966/limEnv : 1; spl0 = max(-1, min(1, outL*lg)); spl1 = max(-1, min(1, outR*lg)); mOut = max(mOut*0.9997, max(abs(spl0), abs(spl1))); @gfx 1050 420 // ===================================================================== // GUI - copper oxide & rust, 1400 x 560 design space, scaled to fill the window (proportions kept; // any spare strip is rust texture). S / M / L = text size. HW = detailed hardware look, off = flat. // COBALT = complementary scheme: every colour c becomes max(r,g,b) + min(r,g,b) - c. // Knobs: drag (Shift = fine), double-click / Ctrl-click = reset. Click the LCD for the preset list. // Click a DECK label to switch its length between seconds and bars. // ===================================================================== // house scaffolding: S / M / L / ? sizing (gfx-ui), mouse in design units, automation on every change ui_begin(1050, 420); S = sc*0.75; ox = 0; oy = 0; tf = 1; hw = ui_hw; thm = ui_thm; mx = mouse_x/S; my = mouse_y/S; gfx_setfont(9, "Copperplate Gothic Bold", 34*S); gfx_setfont(10, "Arial Bold", 17*S); gfx_setfont(11, "Arial Bold", 15*S); gfx_setfont(12, "Arial Bold", 14*S); gfx_setfont(13, "Arial Bold", 13*S); gfx_setfont(14, "Courier New", 22*S, 'b'); gfx_setfont(15, "Courier New", 14*S, 'b'); gfx_setfont(16, "Arial Bold", 15*S); // ---- helpers ---- function col(r, g, b) local(q) ( thm ? ( q = max(r, max(g, b)) + min(r, min(g, b)); r = q - r; g = q - g; b = q - b; ); // complement gfx_r = r; gfx_g = g; gfx_b = b; gfx_a = 1; ); function cola(r, g, b, a) ( col(r, g, b); gfx_a = a; ); function cink() ( col(0.17, 0.08, 0.03); ); // stamped ink on copper function R(x, y, w, h) ( gfx_rect(ox + x*S, oy + y*S, max(1, w*S), max(1, h*S)); ); function LN(x1, y1, x2, y2) ( gfx_line(ox + x1*S, oy + y1*S, ox + x2*S, oy + y2*S, 1); ); function TX(x, y, txt) ( gfx_x = ox + x*S; gfx_y = oy + y*S; gfx_drawstr(txt); ); function TC(cx, y, txt) local(w, h) ( gfx_measurestr(txt, w, h); gfx_x = ox + cx*S - w*0.5; gfx_y = oy + y*S; gfx_drawstr(txt); ); function TR(rx, y, txt) local(w, h) ( gfx_measurestr(txt, w, h); gfx_x = ox + rx*S - w; gfx_y = oy + y*S; gfx_drawstr(txt); ); function CIRC(x, y, rr, fill) ( gfx_circle(ox + x*S, oy + y*S, rr*S, fill, 1); ); function clicked(x, y, w, h) local(h_) ( h_ = pressed && drag_id == 0 && mx >= x && mx <= x+w && my >= y && my <= y+h; h_ ? drag_id = -1; h_; ); function setk(idx, v) ( slider(idx) != v ? ( slider(idx) = v; ui_auto(idx); ); ); function hash(k) local(q) ( q = sin(k*12.9898)*43758.5453; q - floor(q); ); function rivet(x, y) ( hw ? ( col(0.32, 0.17, 0.09); CIRC(x, y, 5, 1); col(0.78, 0.50, 0.30); CIRC(x - 1, y - 1, 2.5, 1); ); ); // Copper plate with patina and rivets; title stamped in the top-left corner function plate(x, y, w, h, title, seed) local(k, px_, py_) ( hw ? ( col(0.20, 0.09, 0.04); R(x + 3, y + 4, w, h); ); // shadow on the rust col(0.60, 0.35, 0.19); R(x, y, w, h); hw ? ( col(0.78, 0.50, 0.30); R(x, y, w, 2); R(x, y, 2, h); // bevel light col(0.34, 0.17, 0.08); R(x, y + h - 2, w, 2); R(x + w - 2, y, 2, h); ) : ( col(0.40, 0.21, 0.10); R(x, y, w, 2); R(x, y + h - 2, w, 2); R(x, y, 2, h); R(x + w - 2, y, 2, h); ); rivet(x + 9, y + 9); rivet(x + w - 9, y + 9); rivet(x + 9, y + h - 9); rivet(x + w - 9, y + h - 9); title != 0 ? ( gfx_setfont(10); col(0.82, 0.58, 0.38); TX(x + 21, y + 11, title); // engraved highlight cink(); TX(x + 20, y + 10, title); ); ); // Copper button: lit = verdigris glow function btn(x, y, w, h, lbl, on, fnt) local(sw, sh) ( on ? col(0.36, 0.72, 0.62) : col(0.42, 0.23, 0.12); R(x, y, w, h); hw ? ( on ? col(0.70, 0.95, 0.86) : col(0.70, 0.44, 0.26); R(x, y, w, 2); col(0.18, 0.08, 0.04); R(x, y + h - 2, w, 2); ); on ? col(0.05, 0.12, 0.10) : col(0.95, 0.85, 0.70); gfx_setfont(fnt); gfx_measurestr(lbl, sw, sh); gfx_x = ox + (x + w*0.5)*S - sw*0.5; gfx_y = oy + (y + h*0.5)*S - sh*0.5; gfx_drawstr(lbl); clicked(x, y, w, h); ); // Patina knob. lgt = log taper, bip = bipolar arc, def = reset value function knob(id, cx, cy, rad, v, vmin, vmax, lgt, bip, def) local(dx, dy, fr, nf, ang, a0, sx, sy, sr, k, t, ca, sa, ta) ( fr = lgt ? log(v/vmin)/log(vmax/vmin) : (v - vmin)/(vmax - vmin); dx = mx - cx; dy = my - cy; kn_hot = drag_id == id || (dx*dx + dy*dy) < (rad*rad*1.6); // house style: values show on hover / drag only pressed && drag_id == 0 && (dx*dx + dy*dy) < (rad*rad*1.6) ? ( ((mouse_cap & 4) || (kc_id == id && time_precise() - kc_t < 0.35)) ? ( v = def; kc_t = 0; k_rst = id; drag_id = -1; ) : ( drag_id = id; drag_sy = my; drag_sf = fr; kc_id = id; kc_t = time_precise(); ); ); drag_id == id ? ( (mouse_cap & 1) ? ( nf = max(0, min(1, drag_sf + (drag_sy - my)/((mouse_cap & 8) ? 720 : 180))); v = lgt ? vmin*(vmax/vmin)^nf : vmin + nf*(vmax - vmin); ); ); fr = lgt ? log(v/vmin)/log(vmax/vmin) : (v - vmin)/(vmax - vmin); ang = (-0.75 + 1.5*fr)*$pi; a0 = bip ? 0 : -0.75*$pi; sx = ox + cx*S; sy = oy + cy*S; sr = rad*S; // track + verdigris value arc col(0.22, 0.10, 0.05); k = 0; loop(max(2, floor(4*S + 0.5)), gfx_arc(sx, sy, sr + 3*S + k, -0.75*$pi, 0.75*$pi, 1); k += 1; ); col(0.40, 0.78, 0.68); abs(ang - a0) > 0.002 ? ( k = 0; loop(max(2, floor(4*S + 0.5)), gfx_arc(sx, sy, sr + 3*S + k, min(a0, ang), max(a0, ang), 1); k += 1; ); ); hw ? ( // bronze skirt, knurling, copper cap with patina ring col(0.22, 0.12, 0.06); gfx_circle(sx, sy, sr, 1, 1); col(0.40, 0.24, 0.13); k = 0; loop(24, ta = k*2*$pi/24; gfx_line(sx + sin(ta)*sr*0.8, sy - cos(ta)*sr*0.8, sx + sin(ta)*(sr - 1), sy - cos(ta)*(sr - 1), 1); k += 1; ); col(0.68, 0.40, 0.22); gfx_circle(sx, sy, sr*0.7, 1, 1); col(0.36, 0.66, 0.58); gfx_circle(sx, sy, sr*0.7, 0, 1); col(0.82, 0.56, 0.34); gfx_circle(sx - sr*0.18, sy - sr*0.2, sr*0.18, 1, 1); ) : ( // flat: plain copper cap with a dark rim col(0.68, 0.40, 0.22); gfx_circle(sx, sy, sr, 1, 1); col(0.30, 0.15, 0.07); gfx_circle(sx, sy, sr, 0, 1); ); // cream pointer col(0.96, 0.90, 0.76); sa = sin(ang); ca = cos(ang); t = max(2, floor(3*S + 0.5)); k = 0; loop(t, gfx_line(sx + sa*sr*0.15 + ca*(k - t*0.5), sy - ca*sr*0.15 + sa*(k - t*0.5), sx + sa*sr*0.92 + ca*(k - t*0.5), sy - ca*sr*0.92 + sa*(k - t*0.5), 1); k += 1; ); v; ); function touch(id, name, val) ( (drag_id == id || k_rst == id) ? ( strcpy(#lcdn, name); strcpy(#lcdv, val); lcd_t = time_precise(); k_rst == id ? k_rst = -1; ); ); function klab(cx, y, lbl, val) ( cink(); gfx_setfont(11); TC(cx, y, lbl); gfx_setfont(12); (kn_hot || !kl_num) ? TC(cx, y + 18, val); kl_num = 1; ); // value on hover / drag only (kl_num = 0: a word, always shown) function fpct(v) ( sprintf(#vt, "%d%%", floor(v + 0.5)); #vt; ); function fct(v) ( sprintf(#vt, v >= 0 ? "+%.1f ct" : "%.1f ct", v); #vt; ); function fhz(v) ( v >= 1000 ? sprintf(#vt, "%.1f kHz", v/1000) : sprintf(#vt, "%d Hz", floor(v + 0.5)); #vt; ); function fms(v) ( v >= 1000 ? sprintf(#vt, "%.2f s", v/1000) : sprintf(#vt, "%d ms", floor(v + 0.5)); #vt; ); function fsec(v) ( sprintf(#vt, "%.2f s", v); #vt; ); function fdb(v) ( sprintf(#vt, v > 0 ? "+%.1f dB" : "%.1f dB", v); #vt; ); function fbars(i) ( i == 0 ? "1/2 BAR" : i == 1 ? "1 BAR" : i == 2 ? "2 BARS" : i == 3 ? "3 BARS" : i == 4 ? "4 BARS" : i == 5 ? "6 BARS" : i == 6 ? "8 BARS" : "12 BARS"; ); function ivname(i) ( i == 0 ? "-2 OCT -24" : i == 1 ? "-1 OCT -12" : i == 2 ? "UNISON" : i == 3 ? "MIN 3RD +3" : i == 4 ? "MAJ 3RD +4" : i == 5 ? "4TH +5" : i == 6 ? "5TH +7" : i == 7 ? "OCTAVE +12" : i == 8 ? "12TH +19" : "2 OCT +24"; ); function pname(n) ( n == 0 ? "CLEAN ORGAN" : n == 1 ? "RAINBOW" : n == 2 ? "DERVISH" : n == 3 ? "POPPY" : n == 4 ? "IN C PULSE" : n == 5 ? "KEYBOARD STUDY" : n == 6 ? "COPPER CATHEDRAL" : n == 7 ? "RUST DRONE" : n == 8 ? "SUNRISE DRONE" : n == 9 ? "REED SWARM" : n == 10 ? "TAPE ECHOES" : n == 11 ? "MODAL FIFTHS" : n == 12 ? "GLASS HARMONICS" : n == 13 ? "MINOR THIRD MIST" : n == 14 ? "PULSE MACHINE" : "FROZEN CHOIR"; ); function go_preset(n) ( slider1 = n; apply_preset(n); lastPreset = n; lcd_t = 0; slider_automate(1125899906842623); // sliders 1-50 ); function step_preset(dir) ( go_preset((slider1 + dir + 16) % 16); ); // Rank plate: interval selector, wave scope, WAVE / LEVEL / DETUNE function rank(x, y, base, title, seed, kid) local(iv, k, px_, py_, w_, yv, py0) ( plate(x, y, 245, 225, title, seed); // interval display with arrows col(0.06, 0.15, 0.14); R(x + 14, y + 36, 217, 32); iv = floor(slider(base) + 0.5); btn(x + 14, y + 36, 30, 32, "◄", 0, 16) ? ( slider(base) = max(0, iv - 1); ui_auto(base); strcpy(#lcdn, title); strcpy(#lcdv, ivname(slider(base))); lcd_t = time_precise(); ); btn(x + 201, y + 36, 30, 32, "►", 0, 16) ? ( slider(base) = min(9, iv + 1); ui_auto(base); strcpy(#lcdn, title); strcpy(#lcdv, ivname(slider(base))); lcd_t = time_precise(); ); col(0.55, 0.95, 0.82); gfx_setfont(15); TC(x + 122, y + 45, ivname(floor(slider(base) + 0.5))); // wave scope: two cycles of the blend col(0.06, 0.15, 0.14); R(x + 14, y + 76, 217, 46); col(0.20, 0.36, 0.33); LN(x + 14, y + 99, x + 231, y + 99); w_ = slider(base + 1)/100; col(0.55, 0.95, 0.82); k = 0; py0 = 0; loop(108, px_ = x + 16 + k*2; yv = osc((k/54) - floor(k/54), 0.004, w_, 0.5); py_ = y + 99 - yv*18; k > 0 ? LN(px_ - 2, py0, px_, py_); py0 = py_; k += 1; ); // knobs setk(base + 1, knob(kid + 1, x + 45, y + 162, 21, slider(base + 1), 0, 100, 0, 0, 55)); kl_num = 0; klab(x + 45, y + 190, "WAVE", w_ < 0.17 ? "SINE" : w_ < 0.5 ? "TRI" : w_ < 0.83 ? "SAW" : "PULSE"); // dominant wave touch(kid + 1, title, fpct(slider(base + 1))); setk(base + 2, knob(kid + 2, x + 122, y + 162, 21, slider(base + 2), 0, 100, 0, 0, 70)); klab(x + 122, y + 190, "LEVEL", fpct(slider(base + 2))); touch(kid + 2, title, fpct(slider(base + 2))); setk(base + 3, knob(kid + 3, x + 199, y + 162, 21, slider(base + 3), -50, 50, 0, 1, 0)); klab(x + 199, y + 190, "DETUNE", fct(slider(base + 3))); touch(kid + 3, title, fct(slider(base + 3))); ); // Tape deck: two spinning reels, head, length readout, loop position function deck(x, y, label, len, synced, nbar, ph, spin) local(k, a, rx, rr) ( col(0.14, 0.07, 0.03); R(x, y, 310, 160); col(0.30, 0.16, 0.08); R(x + 2, y + 2, 306, 2); rr = 0; loop(2, rx = x + 78 + rr*154; col(0.10, 0.05, 0.02); CIRC(rx, y + 70, 54, 1); // tape pack col(0.62, 0.38, 0.20); CIRC(rx, y + 70, 50, 0); CIRC(rx, y + 70, 49, 0); col(0.36, 0.66, 0.58); CIRC(rx, y + 70, 12, 1); col(0.70, 0.44, 0.24); k = 0; loop(3, a = spin + k*2*$pi/3 + rr*0.7; gfx_line(ox + (rx + sin(a)*13)*S, oy + (y + 70 - cos(a)*13)*S, ox + (rx + sin(a)*46)*S, oy + (y + 70 - cos(a)*46)*S, 1); gfx_line(ox + (rx + sin(a)*13)*S + 1, oy + (y + 70 - cos(a)*13)*S, ox + (rx + sin(a)*46)*S + 1, oy + (y + 70 - cos(a)*46)*S, 1); k += 1; ); rr += 1; ); // tape path and head col(0.35, 0.20, 0.10); LN(x + 78, y + 124, x + 232, y + 124); col(0.75, 0.75, 0.72); R(x + 145, y + 116, 20, 14); // labels col(0.95, 0.80, 0.58); gfx_setfont(13); TX(x + 10, y + 8, label); gfx_setfont(12); TR(x + 300, y + 8, synced ? fbars(nbar) : fsec(len)); // loop position col(0.25, 0.12, 0.05); R(x + 10, y + 142, 290, 8); col(0.40, 0.78, 0.68); R(x + 10, y + 142, 290*ph, 8); ); // ===================================================================== // Draw // ===================================================================== // rust ground with speckles col(0.31, 0.14, 0.07); gfx_rect(0, 0, gfx_w, gfx_h); // rust speckles across the whole window (so spare strips match the panel) k = 0; loop(hw ? min(2500, floor(700*(gfx_w*gfx_h)/(1400*560*S*S))) : 0, hv = hash(k); hv < 0.5 ? cola(0.45, 0.21, 0.09, 0.5) : hv < 0.8 ? cola(0.20, 0.08, 0.03, 0.5) : cola(0.55, 0.30, 0.14, 0.4); gfx_rect(hash(k + 1000)*gfx_w, hash(k + 2000)*gfx_h, (2 + 4*hash(k + 3000))*S, (2 + 3*hash(k + 4000))*S); k += 1; ); gfx_a = 1; // ---- header ---- col(0.95, 0.76, 0.52); gfx_setfont(9); TX(24, 14, "G RAINBOW"); col(0.40, 0.78, 0.68); gfx_setfont(13); TX(28, 60, "TIME-LAG ACCUMULATOR ORGAN"); clicked(420, 16, 300, 62) ? ( strcpy(#pm, ""); k = 0; loop(16, k == slider1 ? strcat(#pm, "!"); strcat(#pm, pname(k)); k < 15 ? strcat(#pm, "|"); k += 1; ); gfx_x = mouse_x; gfx_y = mouse_y; psel = gfx_showmenu(#pm); psel > 0 ? go_preset(psel - 1); ); col(0.12, 0.06, 0.02); R(416, 12, 308, 70); col(0.05, 0.14, 0.13); R(420, 16, 300, 62); col(0.55, 0.95, 0.82); time_precise() - lcd_t < 1.5 ? ( gfx_setfont(15); TX(432, 24, #lcdn); gfx_setfont(14); TX(432, 44, #lcdv); ) : ( gfx_setfont(15); sprintf(#lcd1, "PRESET %02d/16", slider1 + 1); TX(432, 24, #lcd1); gfx_setfont(14); TX(432, 44, pname(slider1)); ); btn(376, 16, 36, 62, "◄", 0, 16) ? step_preset(-1); btn(728, 16, 36, 62, "►", 0, 16) ? step_preset(1); col(0.95, 0.80, 0.58); gfx_setfont(13); TX(800, 14, "SIZE"); btn(800, 34, 40, 30, "S", ui_sz == 1, 16) ? ui_sz = 1; btn(846, 34, 40, 30, "M", ui_sz == 0, 16) ? ui_sz = 0; btn(892, 34, 40, 30, "L", ui_sz == 2, 16) ? ui_sz = 2; btn(938, 34, 18, 30, "?", ui_help_open, 16) ? ( ui_help_open = 1; ui_help_new = 1; ); btn(960, 34, 80, 30, "PANIC", 0, 16) ? all_off(); midi_led > 0.05 ? col(0.55, 1.0, 0.85) : col(0.15, 0.30, 0.27); CIRC(1062, 49, 7, 1); col(0.95, 0.80, 0.58); gfx_setfont(13); TX(1074, 43, "MIDI"); btn(1140, 34, 56, 30, "HW", ui_hw, 16) ? ui_hw = !ui_hw; btn(1206, 34, 110, 30, "COBALT", ui_thm, 16) ? ui_thm = !ui_thm; midi_led *= 0.85; // ---- ranks ---- rank(20, 100, 10, "RANK A", 11, 100); rank(275, 100, 20, "RANK B", 37, 200); // ---- filter ---- plate(20, 335, 245, 213, "LADDER FILTER", 61); setk(30, knob(301, 65, 400, 22, slider30, 40, 16000, 1, 0, 2400)); klab(65, 430, "CUTOFF", fhz(slider30)); touch(301, "CUTOFF", fhz(slider30)); setk(31, knob(302, 142, 400, 22, slider31, 0, 100, 0, 0, 20)); klab(142, 430, "RESO", fpct(slider31)); touch(302, "RESONANCE", fpct(slider31)); setk(32, knob(303, 219, 400, 22, slider32, 0, 100, 0, 0, 25)); klab(219, 430, "DRIVE", fpct(slider32)); touch(303, "DRIVE", fpct(slider32)); // response sketch of the 4-pole ladder col(0.06, 0.15, 0.14); R(34, 470, 217, 64); col(0.55, 0.95, 0.82); kq = slider31/100*4.1; k = 0; py0 = 0; loop(108, fq = 20*1000^(k/107); wr = fq/slider30; // |1/((1+jw)^4 + k)| via complex arithmetic re1 = 1; im1 = 0; t2 = 0; loop(4, t2 = re1 - im1*wr; im1 = im1 + re1*wr; re1 = t2; ); re1 += kq; hm = (1 + 0.5*kq)/sqrt(re1*re1 + im1*im1); py_ = 470 + 64 - max(0, min(62, (20*log10(max(0.00001, hm)) + 36)/48*62)); k > 0 ? LN(34 + (k - 1)*2, py0, 34 + k*2, py_); py0 = py_; k += 1; ); // ---- amp ---- plate(275, 335, 245, 213, "AMP", 83); setk(34, knob(304, 320, 400, 22, slider34, 1, 3000, 1, 0, 30)); klab(320, 430, "ATTACK", fms(slider34)); touch(304, "ATTACK", fms(slider34)); setk(35, knob(305, 397, 400, 22, slider35, 10, 8000, 1, 0, 600)); klab(397, 430, "RELEASE", fms(slider35)); touch(305, "RELEASE", fms(slider35)); setk(36, knob(306, 474, 400, 22, slider36, 0, 100, 0, 0, 20)); klab(474, 430, "VIBRATO", fpct(slider36)); touch(306, "VIBRATO", fpct(slider36)); // envelope sketch col(0.06, 0.15, 0.14); R(289, 470, 217, 64); col(0.55, 0.95, 0.82); ea = 10 + 60*log(slider34)/log(3000); er = 10 + 80*log(slider35/10)/log(800); LN(293, 530, 293 + ea, 478); LN(293 + ea, 478, 293 + ea + 60, 478); k = 0; py0 = 478; loop(20, py_ = 478 + (530 - 478)*(1 - exp(-4*(k + 1)/20)); LN(293 + ea + 60 + k*er/20, py0, 293 + ea + 60 + (k + 1)*er/20, py_); py0 = py_; k += 1; ); // ---- time-lag accumulator ---- plate(530, 100, 700, 448, "TIME-LAG ACCUMULATOR", 101); active = slider45 > 0 || slider46 > 0.5; spin = active ? time_precise()*4.5 : 0; ph1 = L1s > 0 ? ((tw/srate) - floor((tw/srate)/L1s)*L1s)/L1s : 0; ph2 = L2s > 0 ? ((tw/srate) - floor((tw/srate)/L2s)*L2s)/L2s : 0; deck(548, 136, "DECK 1 RECORD", L1t, slider47 > 0.5, slider48, ph1, spin); deck(902, 136, "DECK 2 PLAYBACK", L2t, slider49 > 0.5, slider50, ph2, spin*0.8); // signal flow: deck 1 -> deck 2 -> back into deck 1 col(0.95, 0.80, 0.58); LN(858, 216, 902, 216); LN(895, 211, 902, 216); LN(895, 221, 902, 216); LN(1057, 296, 1057, 312); LN(1057, 312, 703, 312); LN(703, 312, 703, 296); LN(698, 303, 703, 296); LN(708, 303, 703, 296); gfx_setfont(13); TC(880, 316, "FEEDBACK - DECK 2 PLAYBACK INTO DECK 1 RECORD"); // knobs row kx = 0; loop(6, kxx = 590 + kx*105; kx == 0 ? ( slider47 > 0.5 ? ( v1 = knob(401, kxx, 392, 24, slider48, 0, 7, 0, 0, 2); setk(48, floor(v1 + 0.5)); ) : ( setk(40, knob(401, kxx, 392, 24, slider40, 0.5, 20, 1, 0, 4)); ); cink(); gfx_setfont(11); TC(kxx, 424, "DECK 1"); gfx_setfont(12); TC(kxx, 442, slider47 > 0.5 ? fbars(slider48) : fsec(slider40)); clicked(kxx - 46, 420, 92, 20) ? ( slider47 = slider47 > 0.5 ? 0 : 1; ui_auto(47); strcpy(#lcdn, "DECK 1"); strcpy(#lcdv, slider47 > 0.5 ? "SYNC TO BARS" : "SECONDS"); lcd_t = time_precise(); ); touch(401, "DECK 1", slider47 > 0.5 ? fbars(slider48) : fsec(slider40)); ) : kx == 1 ? ( slider49 > 0.5 ? ( v1 = knob(402, kxx, 392, 24, slider50, 0, 7, 0, 0, 3); setk(50, floor(v1 + 0.5)); ) : ( setk(41, knob(402, kxx, 392, 24, slider41, 0.5, 20, 1, 0, 6)); ); cink(); gfx_setfont(11); TC(kxx, 424, "DECK 2"); gfx_setfont(12); TC(kxx, 442, slider49 > 0.5 ? fbars(slider50) : fsec(slider41)); clicked(kxx - 46, 420, 92, 20) ? ( slider49 = slider49 > 0.5 ? 0 : 1; ui_auto(49); strcpy(#lcdn, "DECK 2"); strcpy(#lcdv, slider49 > 0.5 ? "SYNC TO BARS" : "SECONDS"); lcd_t = time_precise(); ); touch(402, "DECK 2", slider49 > 0.5 ? fbars(slider50) : fsec(slider41)); ) : kx == 2 ? ( setk(42, knob(403, kxx, 392, 24, slider42, 0, 100, 0, 0, 55)); klab(kxx, 424, "FEEDBACK", fpct(slider42)); touch(403, "FEEDBACK", fpct(slider42)); ) : kx == 3 ? ( setk(43, knob(404, kxx, 392, 24, slider43, 0, 100, 0, 0, 30)); klab(kxx, 424, "AGE", fpct(slider43)); touch(404, "TAPE AGE", fpct(slider43)); ) : kx == 4 ? ( setk(44, knob(405, kxx, 392, 24, slider44, 0, 100, 0, 0, 20)); klab(kxx, 424, "WOW", fpct(slider44)); touch(405, "WOW & FLUTTER", fpct(slider44)); ) : ( setk(45, knob(406, kxx, 392, 24, slider45, 0, 100, 0, 0, 45)); klab(kxx, 424, "MIX", fpct(slider45)); touch(406, "LOOP MIX", fpct(slider45)); ); kx += 1; ); // HOLD btn(548, 480, 150, 44, "HOLD", slider46 > 0.5, 16) ? ( slider46 = slider46 > 0.5 ? 0 : 1; ui_auto(46); strcpy(#lcdn, "ACCUMULATOR"); strcpy(#lcdv, slider46 > 0.5 ? "HOLD - FROZEN" : "RECORDING"); lcd_t = time_precise(); ); cink(); gfx_setfont(13); TX(712, 488, "FREEZE THE LOOP: STOP RECORDING,"); TX(712, 502, "KEEP IT CIRCULATING. PLAY OVER IT."); // ---- master ---- plate(1240, 100, 140, 448, "MASTER", 131); setk(2, knob(501, 1310, 176, 26, slider2, 0, 100, 0, 0, 30)); klab(1310, 210, "DRIFT", fpct(slider2)); touch(501, "DRIFT", fpct(slider2)); setk(3, knob(502, 1310, 286, 26, slider3, -24, 6, 0, 1, -6)); klab(1310, 320, "OUTPUT", fdb(slider3)); touch(502, "OUTPUT", fdb(slider3)); // output meter: verdigris LED ladder mdb = mOut > 0.0000001 ? 20*log10(mOut) : -100; mp = max(0, min(1, (mdb + 48)/51)); col(0.10, 0.05, 0.02); R(1290, 360, 40, 170); k = 0; loop(14, f = (k + 0.5)/14; d = -48 + 51*f; d > 0 ? col(1.0, 0.25, 0.12) : d > -6 ? col(1.0, 0.70, 0.20) : col(0.40, 0.85, 0.72); f > mp ? ( gfx_r *= 0.22; gfx_g *= 0.22; gfx_b *= 0.22; ); R(1296, 360 + 170 - 4 - (k + 1)*11.8, 28, 9); k += 1; ); cink(); gfx_setfont(13); TC(1310, 534, "OUT"); ui_end();