desc: Roomservice - by dmajorsound
//tags: reverb room acoustics image-source filter education
//author: Bernd Dormayer
//version: 1.0
//
// Physical room reverb:
//   Direct sound  : delay r/c, level 1/r, air absorption, stereo by facing direction (level + delay)
//   Reflections   : image-source method up to 2nd order (4 walls + floor/ceiling) plus a 24-tap early cluster,
//                   level sqrt(1-alpha)/L, walls attenuate the highs (free field: ground + two reflectors)
//   Late reverb   : 8-channel feedback delay network, length = RT60 from room volume and absorption (Eyring + air; Sabine: 0.161 * V / A),
//                   late level after Barron (falls with the distance beyond the critical distance r_c = 0.057 * sqrt(V/RT60))
// Source = the input as a point source with a stereo side path (Stereo width, Mono mix), optionally through a device model.
// Moving the source / listener produces a true Doppler effect (the delays glide).

slider1:0<0,1,1{Room,Free field}>-Mode
slider2:5.5<0.8,40,0.001>-Room width W (m)
slider3:4.5<0.8,40,0.001>-Room depth D (m)
slider4:2.6<0.8,15,0.001>-Room height H (m)
slider5:0.04<0.01,1,0.0001>-Wall alpha Top
slider6:0.04<0.01,1,0.0001>-Wall alpha Right
slider7:0.04<0.01,1,0.0001>-Wall alpha Bottom
slider8:0.04<0.01,1,0.0001>-Wall alpha Left
slider9:0.33<0.01,1,0.0001>-Floor + ceiling alpha
slider10:2.75<0,40,0.0001>-Source X (m)
slider11:0.25<0,40,0.0001>-Source Y (m)
slider12:1.5<0,15,0.0001>-Source height (m)
slider13:2.75<0,40,0.0001>-Listener X (m)
slider14:2.25<0,40,0.0001>-Listener Y (m)
slider15:1.5<0,15,0.0001>-Listener height (m)
slider16:1<0,2,1{Face source,Fixed,Along the path}>-Listener facing
slider17:0<-180,180,0.01>-Facing angle (deg, 0 = top wall)
slider18:100<0,100,0.01>-Mix dry/wet (%)
slider19:1<0,1,1{Off,On}>-Floor/ceiling reflections
slider20:0<-60,12,0.001>-Early reflections (dB)
slider21:0<-60,12,0.001>-Late reverb (dB)
slider22:0<-72,24,0.001>-Output (dB)
slider23:1<0,1,1{Off,On}>-Limiter
slider24:0<0,1,1{Off,On}>-Low cut
slider25:80<20,2000,0.01>-Low cut frequency (Hz)
slider26:1<0,7,1{6 dB/oct,12 dB/oct,18 dB/oct,24 dB/oct,30 dB/oct,36 dB/oct,42 dB/oct,48 dB/oct}>-Low cut slope
slider27:0<0,1,1{Off,On}>-High cut
slider28:12000<1000,20000,0.1>-High cut frequency (Hz)
slider29:1<0,7,1{6 dB/oct,12 dB/oct,18 dB/oct,24 dB/oct,30 dB/oct,36 dB/oct,42 dB/oct,48 dB/oct}>-High cut slope
slider30:0<0,1,1{Pre reverb,Post reverb}>-Filter position
slider31:1<0.25,4,0.0001>-Size (room scale)
slider32:1<0.01,10,0.0001>-Decay (RT60 factor)
slider33:0<-100,100,0.01>-Tone (dark … bright)
slider34:0<0,200,0.01>-Predelay (ms)
slider35:0<0,12,1{Original,Telephone,Mobile phone,Smartphone,Walkie-talkie,Radio,Boombox,TV,Laptop,Tablet,Mini speaker,Headphones,Megaphone}>-Source model
slider36:0<0,1,1{On,Off}>-Room engine (on / off)
slider37:0<0,100,0.01>-Source distortion (%)
slider38:80<0,100,0.01>-Density (%)
slider39:0<0,1,1{Off,On}>-Filter bypass
slider40:0<-72,48,0.001>-Input (dB)
slider41:0<0,3,1{Off,Wall,Window,Door}>-Barrier
slider42:1.25<0,160,0.001>-Barrier position (m from back wall)
slider43:0<0,100,0.1>-Barrier open (%)
slider44:25<0,70,0.1>-Barrier damping (dB)
slider45:2.75<0,160,0.001>-Barrier element X (m)
slider46:0<0,9,1{Same as room,Concrete,Glass,Metal,Wood,Curtain,Foam,Wool,Tiles,Plaster}>-Back part material
slider47:0<-10,10,0.001>-Source EQ: high-pass shift (oct)
slider48:0<-10,10,0.001>-Source EQ: low-pass shift (oct)
slider49:0<-10,10,0.001>-Source EQ: band 1 frequency shift (oct)
slider50:0<-24,24,0.01>-Source EQ: band 1 gain offset (dB)
slider51:0<-3,3,0.001>-Source EQ: band 1 Q (oct)
slider52:0<-10,10,0.001>-Source EQ: band 2 frequency shift (oct)
slider53:0<-24,24,0.01>-Source EQ: band 2 gain offset (dB)
slider54:0<-3,3,0.001>-Source EQ: band 2 Q (oct)
slider55:0<-10,10,0.001>-Source EQ: band 3 frequency shift (oct)
slider56:0<-24,24,0.01>-Source EQ: band 3 gain offset (dB)
slider57:0<-3,3,0.001>-Source EQ: band 3 Q (oct)
slider58:0<0,8,1{Model,6 dB/oct,12 dB/oct,18 dB/oct,24 dB/oct,30 dB/oct,36 dB/oct,42 dB/oct,48 dB/oct}>-Source EQ: high-pass slope
slider59:0<0,8,1{Model,6 dB/oct,12 dB/oct,18 dB/oct,24 dB/oct,30 dB/oct,36 dB/oct,42 dB/oct,48 dB/oct}>-Source EQ: low-pass slope
slider60:0<0,1,1{Off,On}>-Equal loudness
slider61:0<-180,180,0.1>-Source angle (deg; Fixed: 0 = towards the front wall, Listener: 0 = at the listener)
slider63:1<0,1,1{Listener,Fixed}>-Source aim
slider62:100<25,400,0.1>-Reflectivity (% of the materials' reflection, 100 = as built)
slider64:0<0,16,0.001>-Snapshot morph (0 = off, 1-16 = snapshot, between = morph to the next)
slider65:0<0,1,1{Off,On}>-Delay
slider66:0<0,1,1{Stereo,Ping-pong}>-Delay mode
slider67:0<0,1,1{ms,Tempo}>-Delay time unit
slider68:1<0,1,1{Off,On}>-Delay link time L+R
slider69:1<0,1,1{Off,On}>-Delay link EQ L+R
slider70:50<1,4000,0.1>-Delay time L (ms)
slider71:50<1,4000,0.1>-Delay time R (ms)
slider72:8<0,12,1{1/1,1/2 dotted,1/2,1/4 dotted,1/2 triplet,1/4,1/8 dotted,1/4 triplet,1/8,1/16 dotted,1/8 triplet,1/16,1/32}>-Delay note L (tempo)
slider73:8<0,12,1{1/1,1/2 dotted,1/2,1/4 dotted,1/2 triplet,1/4,1/8 dotted,1/4 triplet,1/8,1/16 dotted,1/8 triplet,1/16,1/32}>-Delay note R (tempo)
slider74:0<0,300,0.01>-Delay tempo (BPM, 0 = project tempo)
slider75:30<0,95,0.1>-Delay feedback L (%)
slider76:30<0,95,0.1>-Delay feedback R (%)
slider77:0<-48,6,0.01>-Delay level L (dB)
slider78:0<-48,6,0.01>-Delay level R (dB)
slider79:120<20,2000,0.1>-Delay low cut L (Hz)
slider80:120<20,2000,0.1>-Delay low cut R (Hz)
slider81:10000<1000,20000,0.1>-Delay high cut L (Hz)
slider82:10000<1000,20000,0.1>-Delay high cut R (Hz)
slider83:0<0,100,0.1>-Delay tape wow + flutter (%)
slider84:0<0,100,0.1>-Delay saturation (%)
slider85:1<0,1,1{Off,On}>-Source model on / off
slider86:1<0,1,1{Off,On}>-Delay link level L+R
slider87:1<0,1,1{Off,On}>-Delay link feedback L+R
slider88:100<0,100,0.1>-Mix Filter (%)
slider89:100<0,100,0.1>-Mix Source model (%)
slider90:100<0,100,0.1>-Mix Delay (%)
slider91:100<0,100,0.1>-Mix Room + reverb (%)
slider92:100<0,100,0.1>-Mix Barrier (%)
slider93:0<0,1,1{Off,On}>-Delay mute L
slider94:0<0,1,1{Off,On}>-Delay mute R
slider95:0<0,40,0.01>-Barrier width (m, 0 = wall to wall)
slider96:0<0,15,0.01>-Barrier height (m, 0 = floor to ceiling)
slider97:100<50,200,1>-Barrier door / window size (%)
slider98:100<0,200,0.1>-Source width (%)
slider99:0<0,24,0.01>-Delay ducking (dB)
slider100:0<0,1,1{Off,On}>-Motion
slider101:0<0,2,1{Source,Listener,Both}>-Motion moves
slider102:0<0,7,1{Circle,Left - right,Front - back,Up - down,Pass-by,Square,Wave,Drawn}>-Motion pattern
slider103:0.25<0.02,10,0.001>-Motion rate (Hz)
slider104:2<0.1,20,0.01>-Motion size (m)
slider105:0<0,360,1>-Motion start (deg)
slider106:0<0,1,1{Clockwise,Counter-clockwise}>-Motion direction
slider107:0<0,2,1{Listener,Own spot,Room center}>-Motion around
slider108:0<0,8,1{Hz,8 bars,4 bars,2 bars,1 bar,1/2,1/4,1/8,1/16}>-Motion tempo
slider109:0.5<0,5,0.01>-Motion glide (s)
slider110:0<0,1,1{Off,On}>-Input mono mix
slider111:0<0,100,0.1>-Source call: duplex (%)
slider112:0<0,100,0.1>-Source call: codec artifacts (%)
slider113:0<0,100,0.1>-Source call: dropouts (%)
slider114:0<0,3,1{Off,Narrowband,Wideband,Super-wideband}>-Source call: line bandwidth
slider115:0<0,3,1{Clean,Tape,Analog,Lo-Fi}>-Delay type
slider116:0<0,1,1{Off,On}>-Delay repeats through the device
slider117:0<0,100,0.1>-Distance perspective (%)
slider118:1<0,16,0.1>-Motion Doppler (0 = off, 1 = natural, 2 … 16 = boosted)
slider119:0<0,3,1{Off,Hard (asphalt),Grass,Snow}>-Free field: ground reflection
slider120:0<0,5,1{Off,Facade,Glass,Rock,Fence,Hedge}>-Reflector 1 material
slider121:0<0,200,0.01>-Reflector 1 X (m)
slider122:0<0,200,0.01>-Reflector 1 Y (m)
slider123:0<0,180,0.1>-Reflector 1 angle (deg)
slider124:0<0,640,0.01>-Reflector 1 length (m)
slider125:0<0,40,0.01>-Reflector 1 height (m)
slider126:0<0,5,1{Off,Facade,Glass,Rock,Fence,Hedge}>-Reflector 2 material
slider127:0<0,200,0.01>-Reflector 2 X (m)
slider128:0<0,200,0.01>-Reflector 2 Y (m)
slider129:0<0,180,0.1>-Reflector 2 angle (deg)
slider130:0<0,640,0.01>-Reflector 2 length (m)
slider131:0<0,40,0.01>-Reflector 2 height (m)
slider132:12<-12,24,1>-Reflector 1 level (dB)
slider133:12<-12,24,1>-Reflector 2 level (dB)
slider134:0<0,1,1{On,Off}>-Reflectors bypass
slider135:0<0,1,1{On,Off}>-Doppler when dragging by hand

in_pin:left input
in_pin:right input
out_pin:left output
out_pin:right output

// all keys go to the plugin while its window has the focus (spaces in snapshot names) - REAPER: FX window menu,
// "Send all keyboard input to plug-in" turns it off per instance (then Space = play / stop again)
options:want_all_kb

@init
// THE VERSION, once: the footer text (the desc line and //version above must say the same)
strcpy(#verstr, "v 1.0 - developed with AI assistance by dmajorsound");
ext_noinit = 1;                            // do not re-run @init at every playback start
gfx_ext_retina = 1;
C_SND = 343;
NT = 49; NIMG = 25; NCL = 24; TS = 64; NSL = 119; NSN = 117;               // taps: 0 direct, 1-6 first order (N E S W floor ceiling), 7-24 second order, 25-48 early cluster; NSL = slider count, NSN = sliders in a snapshot

MAXIN   = ceil(srate * 2.2) + 8;         // input delay line: 2nd-order images of the largest room (~480 m) + ITD + pre-delay
LINEMAX = ceil(srate * 0.3) + 8;         // per FDN line
APMAX   = ceil(srate * 0.06) + 8;        // per diffusion allpass

// SAVED STATE at FIXED addresses (snapshots, names, view): the buffers below depend on the sample rate, so anything
// placed after them would move when REAPER re-runs @init with another rate (e.g. after loading a project) - and the
// state restored by @serialize would be lost. @serialize uses the same constants (it can run before or after @init).
uslot = 64;                             // snapshots: 16 x 64 (64 … 1087)
vopt  = 2048;                           // view options + GUI arrays + snapshot names (2048 … ~8100)
ibuf    = 16384;                        // the rings start here (8192 … 16383 above it: ser_scr, scratch for loading the state in @serialize)
// THE DELAY LINES (rings) depend on the sample rate: input line, 8 + 4 FDN lines, 4 allpasses, the two delay lines and the
// side line of the delay - one after the other from ibuf. Everything else (tables, per-tap arrays, coefficients) sits at a
// fixed address (arr), so the rings can be laid out again when the sample rate changes without @init (see srsetup).
function srlayout() (
  MAXIN   = ceil(srate * 2.2) + 8;
  LINEMAX = ceil(srate * 0.3) + 8;
  APMAX   = ceil(srate * 0.06) + 8;
  fdnbase = ibuf + MAXIN;
  apbase  = fdnbase + 8 * LINEMAX;
  fdn2base = apbase + 4 * APMAX;           // 2nd network (neighbor room behind the barrier): 4 lines
  DLMAX = ceil(srate * (srate > 192000 ? 1.02 : 4.05)) + 8;   // delay lines: 4 s (above 192 kHz 1 s)
  DLMAX = min(DLMAX, floor((4194304 - 64 - (fdn2base + 4 * LINEMAX) - MAXIN) / 2));   // never into the fixed area above
  dlL = fdn2base + 4 * LINEMAX; dlR = dlL + DLMAX; sbuf = dlR + DLMAX;
);
srlayout();
arr     = 4194304 + 12000;              // fixed: per-tap arrays, tables, coefficients, filter states (… about + 20000)

// per-tap arrays (stride TS): x = target, c = current, o = old set during a crossfade
xDL = arr; xDR = xDL + TS; xGL = xDR + TS; xGR = xGL + TS; xA = xGR + TS; cDL = xA + TS; 
cDR = cDL + TS; cGL = cDR + TS; cGR = cGL + TS; cA = cGR + TS; lpL = cA + TS; lpR = lpL + TS; 
tapT = lpR + TS; tapG = tapT + TS; tapOn = tapG + TS; tapZ = tapOn + TS; 


oDL = tapZ + TS; oDR = oDL + TS; oGL = oDR + TS; oGR = oGL + TS; oA = oGR + TS; 
olpL = oA + TS; 
olpR = olpL + TS; tIX = olpR + TS; tIY = tIX + TS; tIZ = tIY + TS; 

fzSs = 4194304 + 50400; memset(fzSs, 0, 40);   // Source: filter states of the stereo side (layout as fzS: HP 0-7, LP 8-15, bands 16-21, first order 32-35)
// MIX without cancellation (Filter, Source model): complementary coefficients and the states of the phase-matched dry path
FAP = 4194304 + 50500; memset(FAP, 0, 80);                     // Filter: dry path states, L (0-39) and R (40-79)
lcCc = 4194304 + 50600; lcFc = lcCc + 20; hcCc = lcFc + 4; hcFc = hcCc + 20;   // Filter: complements (low cut -> its low-pass, high cut -> its high-pass)
fzCc = 4194304 + 50700;                                          // Source: complements of the model's high-pass (0-19, 40-42) and low-pass (20-39, 44-46)
FZA = 4194304 + 50800; memset(FZA, 0, 120);                     // Source: dry path states, mid (0-59) and side (60-119)
FZO = 4194304 + 50950; memset(FZO, 0, 44);                      // Source: dry path through the 4x anti-alias filters + DC blocker, mid (0-21), side (22-43)
// ALLOCATOR for the fixed (sample-rate independent) areas added since alloc(n) hands out the next n slots from
// 4194304 + 121000 upwards (above everything placed by hand: the map below). No hand-picked offsets any more - two areas
// cannot overlap. Hand-placed map (offsets from 4194304): 0 dsc (snapshot descriptions), 4096 snapv/gchg/gtouch/gval/gheld,
// 6000 anaR (tempo analysis), 9000 dptab (delay presets), 12000 arr (per-tap arrays, tables, coefficients - the delay lines sit at ibuf), 30000 abSt (A / B),
// 31000 mtx (menu transforms), 32000 urb (My rooms; + 1900 rmX / rmY), 40000 MTDP (drawn path), 41000 MTDS (stored paths), 44200 LBB (labels),
// 45000 MTAP (Own spot), 45100 MZR (module boxes), 50000 fcLog/fdA/fdSM (3 x TS), 50400 fzSs, 50500 FAP, 50600 lcCc.., 50700 fzCc, 50800 FZA,
// 50950 FZO, 51000 PT (62 x 20), 53000 UNB/RDB/UBS (undo + redo: 2 x 20 x USTR + USTR), 62000 uslotR (snapshot reflectors), 120000 fqGc (8);
// the spectrum buffers (SPB …) come from allocA / alloc above 121000
ALC = 4194304 + 121000;
MZR = 4194304 + 45100;   // [id * 4 … + 3] = screen box of each module card (last full frame), [80 + id] = its frame stamp mzfr
MTAP = 4194304 + 45000;   // the user-defined spot of Around > Own spot: [0] x, [1] y (slider units), [2] = 1 when set (saved via @serialize)
MTDS = 4194304 + 41000;   // four stored drawn paths (4 x 770, as MTDP) + [3080] the slot in use (saved via @serialize)
MTDP = 4194304 + 40000;   // drawn motion path: 256 points (x, y) at equal TIME steps, -1 … 1 of Size around the center; [512] = 256 when one was drawn, [513] = its duration in s, [514 …769] the heights (the same scale) (saved via @serialize)
// Bump allocator: returns the current offset ALC and advances it by n slots.
function alloc(n) local(p) ( p = ALC; ALC += n; p; );
function bpdrd(buf, d) local(rp, i, f, ym, y0, y1, y2, c1, c2, c3) ( rp = bpdW - d; i = floor(rp); f = rp - i; ym = buf[(i - 1) & 65535]; y0 = buf[i & 65535]; y1 = buf[(i + 1) & 65535]; y2 = buf[(i + 2) & 65535]; c1 = 0.5 * (y1 - ym); c2 = ym - 2.5 * y0 + 2 * y1 - 0.5 * y2; c3 = 0.5 * (y2 - ym) + 1.5 * (y0 - y1); ((c3 * f + c2) * f + c1) * f + y0; );   // 4-point Hermite read, d >= 2 samples
cvY = 0; function allocA(n) local(p) ( p = ALC; (p % 65536) + n > 65536 ? p = (floor(p / 65536) + 1) * 65536; ALC = p + n; p; );   // for fft(): the buffer must not cross a 65536 boundary
fcLog = 4194304 + 50000; fdA = fcLog + TS; fdSM = fdA + TS; dDL = alloc(TS); dDR = alloc(TS); mtRmp = 0; itdN = NIMG;   // tap ramps while a motion runs
HSN = 32; xHL = alloc(10 * HSN); xHR = xHL + HSN; cHL = xHR + HSN; cHR = cHL + HSN; hdsL = cHR + HSN; hdsR = hdsL + HSN; oHL = hdsR + HSN; oHR = oHL + HSN; ohdsL = oHR + HSN; ohdsR = ohdsL + HSN; memset(xHL, 0, 10 * HSN);   // head shadow (direct + first AND second order, taps 0-24): far-ear low-pass per tap
nfSL = nfSR = 0; xNF = cNF = 0; nfA = exp(-2 * $pi * 250 / srate);   // near field: low shelf on the direct sound below 1 m
// LINE MODULATION as a random walk: every 150 ... 400 ms a line draws a new target and glides to it
// through two one-poles (150 ms) - never periodic, so a held note behind a wall does not "breathe" in a rhythm, and the lines
// drift independently (more decorrelation). Deterministic (Park-Miller, re-seeded at every playback start): the same render twice sounds identical
BPDM = alloc(65536); BPDS = alloc(65536); bpdW = 0; bpdC = 0; bpdO = 0; bpdI = 0; bpdF = 1; bpdFI = 0; bpdOnP = 0; bpdKP = 1;   // Room off + Motion - the Doppler delay of the stereo panner path (mid, side)
PLQ = alloc(64); mtRB = alloc(3072); mtRSm = alloc(512); mtRBz = alloc(1024); mtRSz = alloc(256); SDFBUF = alloc(80); WFB = alloc(8 * 64);   // WFB: the wavefronts of the waves view (particles) // SDFBUF: the strokes of a smooth icon // the stroke being drawn (up to 1024 points, meters) and its resampled copy // (mtRBz / mtRSz - the height of every point)
MDS = alloc(60); mdSd = MDS; mdTg = MDS + 12; mdV1 = MDS + 24; mdV2 = MDS + 36; mdCn = MDS + 48;   // 12 lines (8 + 4)
// Re-seeds the 12 line-modulation random walks (8 main FDN lines + 4 neighbor-room lines): seeds, targets, values, first countdowns
function modreset() local(k) ( k = 0; loop(12, mdSd[k] = 1234 + k * 7919; mdTg[k] = mdV1[k] = mdV2[k] = 1; mdCn[k] = (k + 1) * 0.02 * srate; k += 1; ); );
modreset(); mdK = 1 - exp(-1 / (0.15 * srate));
// one step of line k's walk: the read offset 0 ... 2 (x moddep)
function modstep(k) (
  mdCn[k] -= 1;
  mdCn[k] <= 0 ? ( mdSd[k] = (mdSd[k] * 16807) % 2147483647; mdTg[k] = 2 * mdSd[k] / 2147483647; mdSd[k] = (mdSd[k] * 16807) % 2147483647; mdCn[k] = (0.15 + 0.25 * mdSd[k] / 2147483647) * srate; );
  mdV1[k] += (mdTg[k] - mdV1[k]) * mdK; mdV2[k] += (mdV1[k] - mdV2[k]) * mdK;
);
// CALL LINE (source, devices only): STFT stage for codec artifacts and packet loss - frame 512 at 44.1 / 48 kHz
// (11 ms, as the 10-20 ms frames of speech codecs), 1024 at 88.2 / 96, 2048 above; hop 1/4, sqrt-Hann in and out
// (sum of the products = 2 -> scale 1 / (2 N), EEL's ifft is not normalized). The fft buffer must not cross a 65536 boundary.
QLN = srate > 130000 ? 2048 : (srate > 66000 ? 1024 : 512); QLH = QLN / 4; qlSc = 1 / (2 * QLN);
qlF = alloc(8192); floor(qlF / 65536) != floor((qlF + 4095) / 65536) ? qlF = ceil(qlF / 65536) * 65536;
// call-line buffers: STFT in / out frames, window, per-bin magnitude / phase and band maps (codec, envelope), duplex and distortion states
qlIn = alloc(2048); qlOut = alloc(2048); qlW = alloc(2048); qlMg = alloc(1032); qlPh = alloc(1032); qlBd = alloc(1032); dvS = alloc(80); memset(dvS, 0, 80); dvPk = 1; dtL1 = dtL2 = dtR1 = dtR2 = 0; dtyC = 0; qlBM = alloc(32); qlDM = alloc(2048); qlDS = alloc(2048); qlC = alloc(32); qlS = alloc(16); qlEb = alloc(1032); qlEP = alloc(72); odS = alloc(160); memset(odS, 0, 160); odOn = sdrL = sdrR = 0; memset(qlS, 0, 16);
// Sets up the call-line codec per sample rate: FFT size QLN, sqrt-Hann window, codec and envelope band maps, frames per 20 ms packet.
function qlsetup() local(k, f)
(
  QLN = srate > 130000 ? 2048 : (srate > 66000 ? 1024 : 512); QLH = QLN / 4; qlSc = 1 / (2 * QLN);
  k = 0; loop(QLN, qlW[k] = sqrt(0.5 - 0.5 * cos(2 * $pi * k / QLN)); k += 1; );
  // codec bands above 1 kHz in quarter octaves (-1 = below: always coded)
  k = 0; loop(QLN / 2 + 1, f = k * srate / QLN; qlBd[k] = f < 1000 ? -1 : min(23, floor(log(f / 1000) / log(2) * 4)); k += 1; );
  // third-octave bands from 100 Hz: the spectral envelope the codec's bit allocation follows
  k = 0; loop(QLN / 2 + 1, f = k * srate / QLN; qlEb[k] = f < 100 ? 0 : min(34, 1 + floor(log(f / 100) / log(2) * 3)); k += 1; );
  qlPF = max(1, floor(0.02 * srate / QLH + 0.5));                 // frames per 20 ms packet
);
// Clears the codec's buffers and state (in/out, magnitude, phase, delay lines, band mask, counters, noise seed) at playback start.
function qlclear() ( memset(qlIn, 0, 2048); memset(qlOut, 0, 2048); memset(qlMg, 0, 1032); memset(qlPh, 0, 1032); memset(qlDM, 0, 2048); memset(qlDS, 0, 2048);
  k0 = 0; loop(32, qlBM[k0] = 1; k0 += 1; ); qlPos = qlCnt = qlDP = 0; qlPC = 0; qlBad = 0; qlLN = 0; qlPG = 1; qlRs = 4711; );
// Resets the duplex (speakerphone) simulation: filters, hold/dip/cooldown state, gains to 1, and its noise seed.
function dqclear() ( dqF = dqS = dqPk = 0; dqAct = 0; dqHold = 0; dqAg = dqGg = 1; dqDip = dqCool = 0; dqRs = 977; );
qlsetup(); qlclear(); dqclear(); qlOnL = 0; lastqlBW = -1;
GONN = 512; gonB = alloc(2 * GONN); memset(gonB, 0, 2 * GONN); gonP = gonC = 0; gonPk = 0.001;   // goniometer: every 4th output sample, L / R
EDN = 1024; edB0 = alloc(4 * EDN); edB1 = edB0 + EDN; edB2 = edB1 + EDN; edB3 = edB2 + EDN; memset(edB0, 0, 4 * EDN); edi = 0;   // early scattering allpasses
edL0 = min(EDN - 1, floor(0.0043 * srate)); edL1 = min(EDN - 1, floor(0.0067 * srate)); edL2 = min(EDN - 1, floor(0.0049 * srate)); edL3 = min(EDN - 1, floor(0.0079 * srate));
memset(fcLog, 0, 3 * TS); k0 = 0; loop(TS, fcLog[k0] = -1000; k0 += 1; ); fmov = 0;   // wall / air filter glide per tap: log corner, steps (free block above the labels)
xSM = tIZ + TS; cSM = xSM + TS; oSM = cSM + TS;     // per tap: share of the unfiltered signal (sound through a wall keeps part of its highs)
tSt = oSM + TS; xfS = tSt + TS;                 // crossfade: taps / reverb lines whose delay did not change stay out of it
tapW = xfS + 16;                                 // per tap: level in the mid band (for equal loudness)
flen = tapW + TS; fg = flen + 8; fb = fg + 8; fst = fb + 8; fac = fst + 8; cFL = fac + 8; oFL = cFL + 8;
fqGc = 4194304 + 120000; memset(fqGc, 0, 8); smQ = 1 - exp(-1 / (0.020 * srate));   // the loop gain glides in 20 ms
// DECAY CROSSFADE: the 8 x 30 loop-filter coefficients (fqC, new per block when Decay / Tone / walls change) no longer
// switch at the block edge - fqCc is what the lines run on, and it goes in a straight line to fqC over the block (a decay
// automation or a snapshot morph passes through every in-between state instead of stepping per block)
fqCc = alloc(240); fqCd = alloc(240); fqmov = 0;
// the same for the ROOM MODES: their resonator coefficients (frequency / damping - the damping follows Decay) ran on
// the new values from the block edge; now mdC0 / mdC1 glide there within the block (mdcmov)
mdC0 = alloc(128); mdC1 = alloc(128); mdD0 = alloc(128); mdD1 = alloc(128); mdQc0 = alloc(128); mdQc1 = alloc(128); mdcmov = 0;
apLen = oFL + 8; apCur = apLen + 8; apOld = apLen + 12; apX = 0; apgN = 1; apgO = 0;
flen2 = apLen + 32; fg2 = flen2 + 8; fb2 = fg2 + 8; fst2 = fb2 + 8; cFL2 = fst2 + 8; oFL2 = cFL2 + 8;
fac2 = oFL2 + 8; modph2 = fac2 + 8; modrt2 = modph2 + 8;
fgL = modrt2 + 8; fhs = fgL + 8; fls = fhs + 8; fgL2 = fls + 8; fhs2 = fgL2 + 8; fls2 = fhs2 + 8;   // bass decay (low shelf) per line
scRat = fls2 + 8; scC = scRat + 16; scE = scC + 16; scD = scE + 16; scN = scD + 16;   // burst guard per line: ratio, cursors, limits (see @block)
memend = scN + 16;
lcC = memend + 8;  lcF = lcC + 20;  hcC = lcF + 4;  hcF = hcC + 20;   // filter coefficients
stb = hcF + 4;                                                        // filter states
lcSL = stb; lcSR = stb + 8; hcSL = stb + 16; hcSR = stb + 24;
lcFL = stb + 32; lcFR = stb + 34; hcFL = stb + 36; hcFR = stb + 38;
bsC = stb + 48; bsS = bsC + 8; dkS = bsS + 4;                    // tone: shelf coeffs, shelf states, dark states
memset(ibuf, 0, dlL - ibuf); memset(arr, 0, stb + 64 - arr);
lastbr = lastdk = -1000;
USL = 64; USX = 56;                     // snapshot slot size: flag + sliders 1 … 63 (64 = morph control, not stored); extension per slot (USX): sliders 65 … 119
uslotX = 1152;                          // snapshot extension: sliders 65 … 119 of each slot (16 x 56, 1152 … 2047)
igT = 0.8912509; igK = 1 / (1 - igT);   // Input Guard: knee at -1 dBFS (see @sample); igK = 1 / (1 - knee)
uslotR = 4194304 + 62000; URR = 16;   // snapshot extension 2 - the 15 reflector values (vopt[52 … 66]) of each slot: [0] = flag (1 = stored), [1 … 15] = values
// uslot: fixed address (see above) - NOT cleared, saved via @serialize
NPRE = 62; NPG1 = 31; PRS = 20;            // room presets: 62 on two pages (31 + 31, 6 categories each, see roomcat); PRS values each
// room presets x 20: W D H, alpha of the walls back / right / front / left, alpha floor + ceiling, sx sy sz lx ly lz,
// size predelay tone early late density - a fixed block of its own (PT); the old place (rtab) stays reserved
PT = 4194304 + 51000;
// Walls carry a material value, floor + ceiling are tuned so that RT60 (Eyring + air) lands on a typical measured value of
// that kind of room. Levels and tone stay at 0 = the physical model; density says how smooth or how echo-like the tail is.
NSNAP = 16;                                // snapshot slots (2 rows of 8 in the top bar)
rtab = stb + 72;
PT[0] = 2.5; PT[1] = 2; PT[2] = 2.4; PT[3] = 0.02; PT[4] = 0.02; PT[5] = 0.02; PT[6] = 0.02; PT[7] = 0.14; PT[8] = 1.25; PT[9] = 0.5; PT[10] = 1.5; PT[11] = 1.25; PT[12] = 1.5; PT[13] = 1.5; PT[14] = 1; PT[15] = 8; PT[16] = 0; PT[17] = 0; PT[18] = 0; PT[19] = 45;   // Household – Bathroom: RT60 1.01 s
PT[20] = 4; PT[21] = 3; PT[22] = 2.5; PT[23] = 0.02; PT[24] = 0.02; PT[25] = 0.02; PT[26] = 0.02; PT[27] = 0.28; PT[28] = 2; PT[29] = 0.8; PT[30] = 1.5; PT[31] = 2; PT[32] = 2.2; PT[33] = 1.5; PT[34] = 1; PT[35] = 6; PT[36] = 0; PT[37] = 0; PT[38] = 0; PT[39] = 55;   // Household – Kitchen: RT60 0.60 s
PT[40] = 5.5; PT[41] = 4.5; PT[42] = 2.6; PT[43] = 0.04; PT[44] = 0.04; PT[45] = 0.04; PT[46] = 0.04; PT[47] = 0.33; PT[48] = 2.75; PT[49] = 1.25; PT[50] = 1.5; PT[51] = 2.75; PT[52] = 3.25; PT[53] = 1.5; PT[54] = 1; PT[55] = 10; PT[56] = 0; PT[57] = 0; PT[58] = 0; PT[59] = 80;   // Household – Living room: RT60 0.51 s
PT[60] = 4; PT[61] = 3.5; PT[62] = 2.5; PT[63] = 0.04; PT[64] = 0.04; PT[65] = 0.04; PT[66] = 0.04; PT[67] = 0.4; PT[68] = 2; PT[69] = 1.05; PT[70] = 1.5; PT[71] = 2; PT[72] = 2.27; PT[73] = 1.5; PT[74] = 1; PT[75] = 8; PT[76] = 0; PT[77] = 0; PT[78] = 0; PT[79] = 85;   // Household – Bedroom: RT60 0.40 s
PT[80] = 2; PT[81] = 8; PT[82] = 2.5; PT[83] = 0.04; PT[84] = 0.04; PT[85] = 0.04; PT[86] = 0.04; PT[87] = 0.17; PT[88] = 1; PT[89] = 3; PT[90] = 1.5; PT[91] = 1; PT[92] = 5; PT[93] = 1.5; PT[94] = 1; PT[95] = 12; PT[96] = 0; PT[97] = 0; PT[98] = 0; PT[99] = 55;   // Household – Hallway: RT60 0.82 s
PT[100] = 3; PT[101] = 3; PT[102] = 12; PT[103] = 0.05; PT[104] = 0.05; PT[105] = 0.05; PT[106] = 0.05; PT[107] = 0.056; PT[108] = 1.5; PT[109] = 1; PT[110] = 1.5; PT[111] = 1.5; PT[112] = 2.2; PT[113] = 1.5; PT[114] = 1; PT[115] = 25; PT[116] = 0; PT[117] = 0; PT[118] = 0; PT[119] = 50;   // Household – Staircase: RT60 2.00 s
PT[120] = 6; PT[121] = 4; PT[122] = 2.3; PT[123] = 0.05; PT[124] = 0.05; PT[125] = 0.05; PT[126] = 0.05; PT[127] = 0.1; PT[128] = 3; PT[129] = 1.2; PT[130] = 1.5; PT[131] = 3; PT[132] = 2.6; PT[133] = 1.5; PT[134] = 1; PT[135] = 15; PT[136] = 0; PT[137] = 0; PT[138] = 0; PT[139] = 55;   // Household – Cellar: RT60 1.18 s
PT[140] = 6; PT[141] = 5.5; PT[142] = 2.4; PT[143] = 0.05; PT[144] = 0.05; PT[145] = 0.05; PT[146] = 0.05; PT[147] = 0.07; PT[148] = 3; PT[149] = 1.2; PT[150] = 1.5; PT[151] = 3; PT[152] = 4.2; PT[153] = 1.5; PT[154] = 1; PT[155] = 20; PT[156] = 0; PT[157] = 0; PT[158] = 0; PT[159] = 55;   // Household – Garage: RT60 1.63 s
PT[160] = 5; PT[161] = 4; PT[162] = 2.7; PT[163] = 0.04; PT[164] = 0.04; PT[165] = 0.04; PT[166] = 0.04; PT[167] = 0.34; PT[168] = 2.5; PT[169] = 1; PT[170] = 1.5; PT[171] = 2.5; PT[172] = 3; PT[173] = 1.5; PT[174] = 1; PT[175] = 8; PT[176] = 0; PT[177] = 0; PT[178] = 0; PT[179] = 80;   // Work & school – Office: RT60 0.50 s
PT[180] = 15; PT[181] = 12; PT[182] = 3; PT[183] = 0.04; PT[184] = 0.04; PT[185] = 0.04; PT[186] = 0.04; PT[187] = 0.33; PT[188] = 7.5; PT[189] = 5; PT[190] = 1.5; PT[191] = 7.5; PT[192] = 7; PT[193] = 1.5; PT[194] = 1; PT[195] = 12; PT[196] = 0; PT[197] = 0; PT[198] = 0; PT[199] = 85;   // Work & school – Open-plan office: RT60 0.60 s
PT[200] = 9; PT[201] = 7; PT[202] = 3.2; PT[203] = 0.04; PT[204] = 0.04; PT[205] = 0.04; PT[206] = 0.04; PT[207] = 0.35; PT[208] = 4.5; PT[209] = 1; PT[210] = 1.5; PT[211] = 4.5; PT[212] = 5; PT[213] = 1.5; PT[214] = 1; PT[215] = 12; PT[216] = 0; PT[217] = 0; PT[218] = 0; PT[219] = 75;   // Work & school – Classroom: RT60 0.59 s
PT[220] = 15; PT[221] = 20; PT[222] = 6; PT[223] = 0.1; PT[224] = 0.1; PT[225] = 0.1; PT[226] = 0.1; PT[227] = 0.38; PT[228] = 7.5; PT[229] = 3; PT[230] = 1.5; PT[231] = 7.5; PT[232] = 12; PT[233] = 1.5; PT[234] = 1; PT[235] = 25; PT[236] = 0; PT[237] = 0; PT[238] = 0; PT[239] = 80;   // Work & school – Lecture hall: RT60 0.91 s
PT[240] = 18; PT[241] = 30; PT[242] = 8; PT[243] = 0.05; PT[244] = 0.05; PT[245] = 0.05; PT[246] = 0.05; PT[247] = 0.28; PT[248] = 9; PT[249] = 4; PT[250] = 1.5; PT[251] = 9; PT[252] = 18; PT[253] = 1.5; PT[254] = 1; PT[255] = 35; PT[256] = 0; PT[257] = 0; PT[258] = 0; PT[259] = 65;   // Work & school – Gym hall: RT60 1.79 s
PT[260] = 3.6; PT[261] = 5.2; PT[262] = 2.4; PT[263] = 0.45; PT[264] = 0.45; PT[265] = 0.45; PT[266] = 0.45; PT[267] = 0.41; PT[268] = 1.8; PT[269] = 1; PT[270] = 1.5; PT[271] = 1.8; PT[272] = 3; PT[273] = 1.5; PT[274] = 0.5; PT[275] = 0; PT[276] = 0; PT[277] = 0; PT[278] = 0; PT[279] = 85;   // Vehicles – Car: RT60 0.08 s
PT[280] = 2.5; PT[281] = 12; PT[282] = 2.3; PT[283] = 0.03; PT[284] = 0.03; PT[285] = 0.03; PT[286] = 0.03; PT[287] = 0.3; PT[288] = 1.25; PT[289] = 5; PT[290] = 1.5; PT[291] = 1.25; PT[292] = 7; PT[293] = 1.5; PT[294] = 1; PT[295] = 3; PT[296] = 0; PT[297] = 0; PT[298] = 0; PT[299] = 70;   // Vehicles – Bus: RT60 0.51 s
PT[300] = 2.8; PT[301] = 2.8; PT[302] = 4.6; PT[303] = 0.08; PT[304] = 0.08; PT[305] = 0.08; PT[306] = 0.08; PT[307] = 0.17; PT[308] = 1.4; PT[309] = 0.84; PT[310] = 1.5; PT[311] = 1.4; PT[312] = 1.82; PT[313] = 1.5; PT[314] = 0.5; PT[315] = 2; PT[316] = 0; PT[317] = 0; PT[318] = 0; PT[319] = 40;   // Vehicles – Elevator: RT60 0.40 s
PT[320] = 2; PT[321] = 2; PT[322] = 2.2; PT[323] = 0.45; PT[324] = 0.45; PT[325] = 0.45; PT[326] = 0.45; PT[327] = 0.191; PT[328] = 1; PT[329] = 0.6; PT[330] = 1.5; PT[331] = 1; PT[332] = 1.4; PT[333] = 1.5; PT[334] = 1; PT[335] = 0; PT[336] = 0; PT[337] = 0; PT[338] = 0; PT[339] = 90;   // Studio & media – Vocal booth: RT60 0.12 s
PT[340] = 5; PT[341] = 4.5; PT[342] = 3; PT[343] = 0.1; PT[344] = 0.1; PT[345] = 0.1; PT[346] = 0.1; PT[347] = 0.657; PT[348] = 2.5; PT[349] = 1.35; PT[350] = 1.5; PT[351] = 2.5; PT[352] = 2.92; PT[353] = 1.5; PT[354] = 1; PT[355] = 3; PT[356] = 0; PT[357] = 0; PT[358] = 0; PT[359] = 90;   // Studio & media – Control room: RT60 0.25 s
PT[360] = 6; PT[361] = 5; PT[362] = 3; PT[363] = 0.1; PT[364] = 0.1; PT[365] = 0.1; PT[366] = 0.1; PT[367] = 0.556; PT[368] = 3; PT[369] = 1.5; PT[370] = 1.5; PT[371] = 3; PT[372] = 3.5; PT[373] = 1.5; PT[374] = 1; PT[375] = 4; PT[376] = 0; PT[377] = 0; PT[378] = 0; PT[379] = 85;   // Studio & media – Studio: RT60 0.30 s
PT[380] = 9; PT[381] = 7; PT[382] = 4; PT[383] = 0.1; PT[384] = 0.1; PT[385] = 0.1; PT[386] = 0.1; PT[387] = 0.37; PT[388] = 4.5; PT[389] = 2.5; PT[390] = 1.5; PT[391] = 4.5; PT[392] = 4.5; PT[393] = 1.5; PT[394] = 1; PT[395] = 10; PT[396] = 0; PT[397] = 0; PT[398] = 0; PT[399] = 80;   // Studio & media – Live room: RT60 0.59 s
PT[400] = 14; PT[401] = 20; PT[402] = 7; PT[403] = 0.1; PT[404] = 0.1; PT[405] = 0.1; PT[406] = 0.1; PT[407] = 0.754; PT[408] = 7; PT[409] = 1; PT[410] = 1.5; PT[411] = 7; PT[412] = 13; PT[413] = 1.5; PT[414] = 1; PT[415] = 15; PT[416] = 0; PT[417] = 0; PT[418] = 0; PT[419] = 90;   // Studio & media – Cinema: RT60 0.50 s
PT[420] = 10; PT[421] = 14; PT[422] = 3.5; PT[423] = 0.1; PT[424] = 0.1; PT[425] = 0.1; PT[426] = 0.1; PT[427] = 0.25; PT[428] = 5; PT[429] = 3; PT[430] = 1.5; PT[431] = 5; PT[432] = 8; PT[433] = 1.5; PT[434] = 1; PT[435] = 15; PT[436] = 0; PT[437] = 0; PT[438] = 0; PT[439] = 75;   // Venues – Jazz club: RT60 0.81 s
PT[440] = 12; PT[441] = 10; PT[442] = 3.2; PT[443] = 0.04; PT[444] = 0.04; PT[445] = 0.04; PT[446] = 0.04; PT[447] = 0.3; PT[448] = 6; PT[449] = 4; PT[450] = 1.5; PT[451] = 6; PT[452] = 6; PT[453] = 1.5; PT[454] = 1; PT[455] = 12; PT[456] = 0; PT[457] = 0; PT[458] = 0; PT[459] = 80;   // Venues – Restaurant: RT60 0.70 s
PT[460] = 18; PT[461] = 25; PT[462] = 12; PT[463] = 0.1; PT[464] = 0.1; PT[465] = 0.1; PT[466] = 0.1; PT[467] = 0.55; PT[468] = 9; PT[469] = 4; PT[470] = 1.5; PT[471] = 9; PT[472] = 14; PT[473] = 1.5; PT[474] = 1; PT[475] = 25; PT[476] = 0; PT[477] = 0; PT[478] = 0; PT[479] = 85;   // Venues – Theater: RT60 1.19 s
PT[480] = 20; PT[481] = 40; PT[482] = 15; PT[483] = 0.1; PT[484] = 0.1; PT[485] = 0.1; PT[486] = 0.1; PT[487] = 0.4; PT[488] = 10; PT[489] = 5; PT[490] = 1.5; PT[491] = 10; PT[492] = 22; PT[493] = 1.5; PT[494] = 1; PT[495] = 40; PT[496] = 0; PT[497] = 0; PT[498] = 0; PT[499] = 90;   // Venues – Concert hall: RT60 1.99 s
PT[500] = 18; PT[501] = 40; PT[502] = 15; PT[503] = 0.05; PT[504] = 0.05; PT[505] = 0.05; PT[506] = 0.05; PT[507] = 0.15; PT[508] = 9; PT[509] = 5; PT[510] = 1.5; PT[511] = 9; PT[512] = 22; PT[513] = 1.5; PT[514] = 1; PT[515] = 60; PT[516] = 0; PT[517] = 0; PT[518] = 0; PT[519] = 75;   // Venues – Church: RT60 5.03 s
PT[520] = 30; PT[521] = 40; PT[522] = 10; PT[523] = 0.08; PT[524] = 0.08; PT[525] = 0.08; PT[526] = 0.08; PT[527] = 0.19; PT[528] = 15; PT[529] = 19; PT[530] = 1.5; PT[531] = 15; PT[532] = 21; PT[533] = 1.5; PT[534] = 1; PT[535] = 30; PT[536] = 0; PT[537] = 0; PT[538] = 0; PT[539] = 55;   // Industrial & large – Warehouse: RT60 2.99 s
PT[540] = 25; PT[541] = 40; PT[542] = 12; PT[543] = 0.05; PT[544] = 0.05; PT[545] = 0.05; PT[546] = 0.05; PT[547] = 0.296; PT[548] = 12.5; PT[549] = 19; PT[550] = 1.5; PT[551] = 12.5; PT[552] = 21; PT[553] = 1.5; PT[554] = 1; PT[555] = 25; PT[556] = 0; PT[557] = 0; PT[558] = 0; PT[559] = 55;   // Industrial & large – Factory hall: RT60 2.50 s
PT[560] = 30; PT[561] = 40; PT[562] = 2.6; PT[563] = 0.05; PT[564] = 0.05; PT[565] = 0.05; PT[566] = 0.05; PT[567] = 0.09; PT[568] = 15; PT[569] = 19; PT[570] = 1.5; PT[571] = 15; PT[572] = 21; PT[573] = 1.5; PT[574] = 1; PT[575] = 25; PT[576] = 0; PT[577] = 0; PT[578] = 0; PT[579] = 45;   // Industrial & large – Car park: RT60 1.99 s
PT[580] = 8; PT[581] = 40; PT[582] = 6; PT[583] = 0.05; PT[584] = 0.05; PT[585] = 0.05; PT[586] = 0.05; PT[587] = 0.06; PT[588] = 4; PT[589] = 5; PT[590] = 1.5; PT[591] = 4; PT[592] = 25; PT[593] = 1.5; PT[594] = 1; PT[595] = 35; PT[596] = 0; PT[597] = 0; PT[598] = 0; PT[599] = 40;   // Industrial & large – Tunnel: RT60 4.18 s
PT[600] = 40; PT[601] = 40; PT[602] = 15; PT[603] = 0.05; PT[604] = 0.05; PT[605] = 0.05; PT[606] = 0.05; PT[607] = 0.806; PT[608] = 20; PT[609] = 8; PT[610] = 1.5; PT[611] = 20; PT[612] = 30; PT[613] = 1.5; PT[614] = 3; PT[615] = 50; PT[616] = 0; PT[617] = 0; PT[618] = 0; PT[619] = 30;   // Industrial & large – Stadium: RT60 3.00 s
PT[620] = 0.9; PT[621] = 1.5; PT[622] = 2.4; PT[623] = 0.02; PT[624] = 0.02; PT[625] = 0.1; PT[626] = 0.02; PT[627] = 0.099; PT[628] = 0.45; PT[629] = 0.33; PT[630] = 1.5; PT[631] = 0.45; PT[632] = 1.2; PT[633] = 1.5; PT[634] = 1; PT[635] = 0.5; PT[636] = 0; PT[637] = 0; PT[638] = 0; PT[639] = 45;   // Homes – WC: RT60 0.75 s
PT[640] = 2.4; PT[641] = 3; PT[642] = 2.5; PT[643] = 0.02; PT[644] = 0.04; PT[645] = 0.04; PT[646] = 0.03; PT[647] = 0.279; PT[648] = 1.2; PT[649] = 0.66; PT[650] = 1.5; PT[651] = 1.2; PT[652] = 2.4; PT[653] = 1.5; PT[654] = 1; PT[655] = 3.5; PT[656] = 0; PT[657] = 0; PT[658] = 0; PT[659] = 55;   // Homes – Small kitchen: RT60 0.55 s
PT[660] = 6; PT[661] = 4; PT[662] = 2.6; PT[663] = 0.02; PT[664] = 0.03; PT[665] = 0.04; PT[666] = 0.04; PT[667] = 0.284; PT[668] = 3; PT[669] = 0.88; PT[670] = 1.5; PT[671] = 3; PT[672] = 2.88; PT[673] = 1.5; PT[674] = 1; PT[675] = 6; PT[676] = 0; PT[677] = 0; PT[678] = 0; PT[679] = 65;   // Homes – Kitchen-diner: RT60 0.60 s
PT[680] = 6; PT[681] = 5; PT[682] = 3; PT[683] = 0.08; PT[684] = 0.02; PT[685] = 0.02; PT[686] = 0.08; PT[687] = 0.17; PT[688] = 3; PT[689] = 1.1; PT[690] = 1.5; PT[691] = 3; PT[692] = 3.1; PT[693] = 1.5; PT[694] = 1; PT[695] = 7; PT[696] = 0; PT[697] = 0; PT[698] = 0; PT[699] = 50;   // Homes – Pro kitchen: RT60 1.00 s
PT[700] = 4.5; PT[701] = 4; PT[702] = 2.5; PT[703] = 0.04; PT[704] = 0.45; PT[705] = 0.1; PT[706] = 0.04; PT[707] = 0.288; PT[708] = 2.25; PT[709] = 0.88; PT[710] = 1.5; PT[711] = 2.25; PT[712] = 2.88; PT[713] = 1.5; PT[714] = 1; PT[715] = 5; PT[716] = 0; PT[717] = 0; PT[718] = 0; PT[719] = 85;   // Homes – Cozy living room: RT60 0.38 s
PT[720] = 9; PT[721] = 7; PT[722] = 3.8; PT[723] = 0.05; PT[724] = 0.03; PT[725] = 0.05; PT[726] = 0.04; PT[727] = 0.251; PT[728] = 4.5; PT[729] = 1.25; PT[730] = 1.5; PT[731] = 4.5; PT[732] = 3.25; PT[733] = 1.5; PT[734] = 1; PT[735] = 10.5; PT[736] = 0; PT[737] = 0; PT[738] = 0; PT[739] = 70;   // Homes – Loft: RT60 0.95 s
PT[740] = 3; PT[741] = 3.5; PT[742] = 2.6; PT[743] = 0.04; PT[744] = 0.03; PT[745] = 0.04; PT[746] = 0.04; PT[747] = 0.363; PT[748] = 1.5; PT[749] = 0.77; PT[750] = 1.5; PT[751] = 1.5; PT[752] = 2.77; PT[753] = 1.5; PT[754] = 1; PT[755] = 4; PT[756] = 0; PT[757] = 0; PT[758] = 0; PT[759] = 75;   // Work & school – Small office: RT60 0.45 s
PT[760] = 6; PT[761] = 4; PT[762] = 2.8; PT[763] = 0.03; PT[764] = 0.03; PT[765] = 0.04; PT[766] = 0.1; PT[767] = 0.349; PT[768] = 3; PT[769] = 0.88; PT[770] = 1.5; PT[771] = 3; PT[772] = 2.88; PT[773] = 1.5; PT[774] = 1; PT[775] = 6; PT[776] = 0; PT[777] = 0; PT[778] = 0; PT[779] = 80;   // Work & school – Meeting room: RT60 0.50 s
PT[780] = 8; PT[781] = 7; PT[782] = 3.5; PT[783] = 0.04; PT[784] = 0.03; PT[785] = 0.04; PT[786] = 0.04; PT[787] = 0.201; PT[788] = 4; PT[789] = 1.25; PT[790] = 1.5; PT[791] = 4; PT[792] = 3.25; PT[793] = 1.5; PT[794] = 1; PT[795] = 10; PT[796] = 0; PT[797] = 0; PT[798] = 0; PT[799] = 70;   // Work & school – Old classroom: RT60 1.10 s
PT[800] = 9; PT[801] = 7; PT[802] = 3.3; PT[803] = 0.1; PT[804] = 0.45; PT[805] = 0.1; PT[806] = 0.1; PT[807] = 0.192; PT[808] = 4.5; PT[809] = 1.25; PT[810] = 1.5; PT[811] = 4.5; PT[812] = 3.25; PT[813] = 1.5; PT[814] = 1; PT[815] = 10; PT[816] = 0; PT[817] = 0; PT[818] = 0; PT[819] = 85;   // Work & school – Music classroom: RT60 0.70 s
PT[820] = 2.2; PT[821] = 1.8; PT[822] = 2.1; PT[823] = 0.03; PT[824] = 0.45; PT[825] = 0.1; PT[826] = 0.45; PT[827] = 0.174; PT[828] = 1.1; PT[829] = 0.4; PT[830] = 1.37; PT[831] = 1.1; PT[832] = 1.44; PT[833] = 1.37; PT[834] = 1; PT[835] = 2; PT[836] = 0; PT[837] = 0; PT[838] = 0; PT[839] = 60;   // Transport – Train compartment: RT60 0.22 s
PT[840] = 2.8; PT[841] = 20; PT[842] = 2.2; PT[843] = 0.08; PT[844] = 0.03; PT[845] = 0.08; PT[846] = 0.03; PT[847] = 0.239; PT[848] = 1.4; PT[849] = 1.25; PT[850] = 1.43; PT[851] = 1.4; PT[852] = 8.25; PT[853] = 1.43; PT[854] = 1; PT[855] = 5.5; PT[856] = 0; PT[857] = 0; PT[858] = 0; PT[859] = 60;   // Transport – Train coach: RT60 0.60 s
PT[860] = 1.3; PT[861] = 1.9; PT[862] = 1.1; PT[863] = 0.03; PT[864] = 0.45; PT[865] = 0.45; PT[866] = 0.45; PT[867] = 0.368; PT[868] = 0.65; PT[869] = 0.42; PT[870] = 0.72; PT[871] = 0.65; PT[872] = 1.52; PT[873] = 0.72; PT[874] = 1; PT[875] = 0.5; PT[876] = 0; PT[877] = 0; PT[878] = 0; PT[879] = 85;   // Transport – Small car: RT60 0.08 s
PT[880] = 1.7; PT[881] = 3.2; PT[882] = 1.4; PT[883] = 0.08; PT[884] = 0.08; PT[885] = 0.08; PT[886] = 0.08; PT[887] = 0.1; PT[888] = 0.85; PT[889] = 0.7; PT[890] = 0.91; PT[891] = 0.85; PT[892] = 2.56; PT[893] = 0.91; PT[894] = 1; PT[895] = 1.5; PT[896] = 0; PT[897] = 0; PT[898] = 0; PT[899] = 40;   // Transport – Empty van: RT60 0.53 s
PT[900] = 2.3; PT[901] = 2; PT[902] = 1.8; PT[903] = 0.03; PT[904] = 0.03; PT[905] = 0.45; PT[906] = 0.03; PT[907] = 0.35; PT[908] = 1.15; PT[909] = 0.44; PT[910] = 1.17; PT[911] = 1.15; PT[912] = 1.6; PT[913] = 1.17; PT[914] = 1; PT[915] = 2; PT[916] = 0; PT[917] = 0; PT[918] = 0; PT[919] = 70;   // Transport – Truck cab: RT60 0.22 s
PT[920] = 2.45; PT[921] = 7; PT[922] = 2.4; PT[923] = 0.08; PT[924] = 0.08; PT[925] = 0.08; PT[926] = 0.08; PT[927] = 0.09; PT[928] = 1.225; PT[929] = 1.25; PT[930] = 1.5; PT[931] = 1.225; PT[932] = 3.25; PT[933] = 1.5; PT[934] = 1; PT[935] = 4.5; PT[936] = 0; PT[937] = 0; PT[938] = 0; PT[939] = 40;   // Transport – Truck box: RT60 0.93 s
PT[940] = 5; PT[941] = 7; PT[942] = 2.3; PT[943] = 0.05; PT[944] = 0.05; PT[945] = 0.05; PT[946] = 0.05; PT[947] = 0.085; PT[948] = 2.5; PT[949] = 1.25; PT[950] = 1.49; PT[951] = 2.5; PT[952] = 3.25; PT[953] = 1.49; PT[954] = 1; PT[955] = 6; PT[956] = 0; PT[957] = 0; PT[958] = 0; PT[959] = 50;   // Basements & huts – Concrete basement: RT60 1.40 s
PT[960] = 5; PT[961] = 6; PT[962] = 2.3; PT[963] = 0.04; PT[964] = 0.1; PT[965] = 0.04; PT[966] = 0.45; PT[967] = 0.163; PT[968] = 2.5; PT[969] = 1.25; PT[970] = 1.49; PT[971] = 2.5; PT[972] = 3.25; PT[973] = 1.49; PT[974] = 1; PT[975] = 6; PT[976] = 0; PT[977] = 0; PT[978] = 0; PT[979] = 70;   // Basements & huts – Hobby basement: RT60 0.55 s
PT[980] = 4; PT[981] = 5; PT[982] = 2.6; PT[983] = 0.1; PT[984] = 0.1; PT[985] = 0.1; PT[986] = 0.1; PT[987] = 0.261; PT[988] = 2; PT[989] = 1.1; PT[990] = 1.5; PT[991] = 2; PT[992] = 3.1; PT[993] = 1.5; PT[994] = 1; PT[995] = 5.5; PT[996] = 0; PT[997] = 0; PT[998] = 0; PT[999] = 70;   // Basements & huts – Wood hut: RT60 0.50 s
PT[1000] = 2; PT[1001] = 2.2; PT[1002] = 2.1; PT[1003] = 0.1; PT[1004] = 0.1; PT[1005] = 0.1; PT[1006] = 0.1; PT[1007] = 0.12; PT[1008] = 1; PT[1009] = 0.48; PT[1010] = 1.37; PT[1011] = 1; PT[1012] = 1.76; PT[1013] = 1.37; PT[1014] = 1; PT[1015] = 2; PT[1016] = 0; PT[1017] = 0; PT[1018] = 0; PT[1019] = 55;   // Basements & huts – Sauna: RT60 0.50 s
PT[1020] = 7; PT[1021] = 14; PT[1022] = 7; PT[1023] = 0.03; PT[1024] = 0.05; PT[1025] = 0.1; PT[1026] = 0.05; PT[1027] = 0.153; PT[1028] = 3.5; PT[1029] = 1.25; PT[1030] = 1.5; PT[1031] = 3.5; PT[1032] = 6.15; PT[1033] = 1.5; PT[1034] = 1; PT[1035] = 17; PT[1036] = 0; PT[1037] = 0; PT[1038] = 0; PT[1039] = 75;   // Churches & caves – Chapel: RT60 2.20 s
PT[1040] = 12; PT[1041] = 28; PT[1042] = 11; PT[1043] = 0.03; PT[1044] = 0.05; PT[1045] = 0.1; PT[1046] = 0.05; PT[1047] = 0.177; PT[1048] = 6; PT[1049] = 1.25; PT[1050] = 1.5; PT[1051] = 6; PT[1052] = 11.05; PT[1053] = 1.5; PT[1054] = 1; PT[1055] = 30.5; PT[1056] = 0; PT[1057] = 0; PT[1058] = 0; PT[1059] = 75;   // Churches & caves – Village church: RT60 3.20 s
PT[1060] = 30; PT[1061] = 40; PT[1062] = 15; PT[1063] = 0.05; PT[1064] = 0.05; PT[1065] = 0.05; PT[1066] = 0.05; PT[1067] = 0.22; PT[1068] = 15; PT[1069] = 1.25; PT[1070] = 1.5; PT[1071] = 15; PT[1072] = 13.25; PT[1073] = 1.5; PT[1074] = 2; PT[1075] = 60; PT[1076] = 0; PT[1077] = 0; PT[1078] = 0; PT[1079] = 80;   // Churches & caves – Cathedral: RT60 7.50 s
PT[1080] = 4; PT[1081] = 6; PT[1082] = 2.5; PT[1083] = 0.05; PT[1084] = 0.05; PT[1085] = 0.05; PT[1086] = 0.05; PT[1087] = 0.1; PT[1088] = 2; PT[1089] = 1.25; PT[1090] = 1.5; PT[1091] = 2; PT[1092] = 3.25; PT[1093] = 1.5; PT[1094] = 1; PT[1095] = 6; PT[1096] = 0; PT[1097] = 0; PT[1098] = 0; PT[1099] = 45;   // Churches & caves – Small cave: RT60 1.25 s
PT[1100] = 25; PT[1101] = 40; PT[1102] = 12; PT[1103] = 0.05; PT[1104] = 0.05; PT[1105] = 0.05; PT[1106] = 0.05; PT[1107] = 0.131; PT[1108] = 12.5; PT[1109] = 1.25; PT[1110] = 1.5; PT[1111] = 12.5; PT[1112] = 13.25; PT[1113] = 1.5; PT[1114] = 1; PT[1115] = 44.5; PT[1116] = 0; PT[1117] = 0; PT[1118] = 0; PT[1119] = 50;   // Churches & caves – Large cave: RT60 4.99 s
PT[1120] = 3; PT[1121] = 3; PT[1122] = 4; PT[1123] = 0.02; PT[1124] = 0.02; PT[1125] = 0.02; PT[1126] = 0.02; PT[1127] = 0.02; PT[1128] = 1.5; PT[1129] = 0.66; PT[1130] = 1.5; PT[1131] = 1.5; PT[1132] = 2.4; PT[1133] = 1.5; PT[1134] = 1; PT[1135] = 5; PT[1136] = 0; PT[1137] = 0; PT[1138] = 0; PT[1139] = 30;   // Effect rooms – Metal tank: RT60 4.07 s
PT[1140] = 4; PT[1141] = 8; PT[1142] = 2.5; PT[1143] = 0.05; PT[1144] = 0.05; PT[1145] = 0.05; PT[1146] = 0.05; PT[1147] = 0.05; PT[1148] = 2; PT[1149] = 1.25; PT[1150] = 1.5; PT[1151] = 2; PT[1152] = 3.25; PT[1153] = 1.5; PT[1154] = 1; PT[1155] = 6; PT[1156] = 0; PT[1157] = 0; PT[1158] = 0; PT[1159] = 40;   // Effect rooms – Concrete bunker: RT60 1.96 s
PT[1160] = 4; PT[1161] = 6; PT[1162] = 2.8; PT[1163] = 0.08; PT[1164] = 0.08; PT[1165] = 0.08; PT[1166] = 0.08; PT[1167] = 0.065; PT[1168] = 2; PT[1169] = 1.25; PT[1170] = 1.5; PT[1171] = 2; PT[1172] = 3.25; PT[1173] = 1.5; PT[1174] = 1; PT[1175] = 6; PT[1176] = 0; PT[1177] = 0; PT[1178] = 0; PT[1179] = 45;   // Effect rooms – Tin shed: RT60 1.34 s
PT[1180] = 3; PT[1181] = 3; PT[1182] = 2.5; PT[1183] = 0.03; PT[1184] = 0.03; PT[1185] = 0.03; PT[1186] = 0.03; PT[1187] = 0.035; PT[1188] = 1.5; PT[1189] = 0.66; PT[1190] = 1.5; PT[1191] = 1.5; PT[1192] = 2.4; PT[1193] = 1.5; PT[1194] = 1; PT[1195] = 4; PT[1196] = 0; PT[1197] = 0; PT[1198] = 0; PT[1199] = 40;   // Effect rooms – Glass box: RT60 2.25 s
PT[1200] = 4; PT[1201] = 5; PT[1202] = 3; PT[1203] = 0.85; PT[1204] = 0.85; PT[1205] = 0.85; PT[1206] = 0.85; PT[1207] = 0.85; PT[1208] = 2; PT[1209] = 1.1; PT[1210] = 1.5; PT[1211] = 2; PT[1212] = 3.1; PT[1213] = 1.5; PT[1214] = 1; PT[1215] = 6; PT[1216] = 0; PT[1217] = 0; PT[1218] = 0; PT[1219] = 90;   // Effect rooms – Dead room: RT60 0.08 s
PT[1220] = 1; PT[1221] = 40; PT[1222] = 1; PT[1223] = 0.05; PT[1224] = 0.05; PT[1225] = 0.05; PT[1226] = 0.05; PT[1227] = 0.05; PT[1228] = 0.5; PT[1229] = 1.25; PT[1230] = 0.65; PT[1231] = 0.5; PT[1232] = 7.25; PT[1233] = 0.65; PT[1234] = 1; PT[1235] = 0.5; PT[1236] = 0; PT[1237] = 0; PT[1238] = 0; PT[1239] = 35;   // Effect rooms – Drain pipe: RT60 0.77 s
saveArm = 0;

limbase = rtab + 31 * 17 + 8;               // (end of the old preset table: the memory after it stays where it was)
// ---- MATERIALS per frequency band: mid alpha (500 Hz - 1 kHz) and log(alpha / mid alpha) at 125 Hz and at 4 kHz
// (typical measured values: glazed tiles, window glass, plaster on masonry, rough concrete, sheet metal, wood paneling on
// battens, heavy velour curtain, 5 cm foam, 5 cm mineral wool). Any alpha in between: the shape is interpolated
mtab = limbase; NMAT = 9;
mtab[0] = 0.02; mtab[1] = log(0.67); mtab[2] = log(1.33);     // tiles
mtab[3] = 0.03; mtab[4] = log(8.0);  mtab[5] = log(0.35);   // glass: panes resonate in the bass (more extreme - thin bass, long bright highs)
mtab[6] = 0.04; mtab[7] = log(0.6);  mtab[8] = log(1.6);      // plaster
mtab[9] = 0.05; mtab[10] = log(0.12); mtab[11] = log(3.0);   // concrete (more extreme - booming long bass, dark highs)
mtab[12] = 0.08; mtab[13] = log(1.6); mtab[14] = log(0.35);   // sheet metal (more extreme - very bright, ringing highs)
mtab[15] = 0.10; mtab[16] = log(2.15); mtab[17] = log(0.85);  // wood paneling: bass absorber
mtab[18] = 0.45; mtab[19] = log(0.22); mtab[20] = log(1.02);  // curtain
mtab[21] = 0.70; mtab[22] = log(0.11); mtab[23] = log(1.27);  // foam: almost nothing in the bass
mtab[24] = 0.85; mtab[25] = log(0.16); mtab[26] = log(1.03);  // mineral wool
// the same materials per octave 125 / 250 / 500 / 1k / 2k / 4k Hz (alpha / mid alpha; 500 and 1k average 1) - typical
// measured tables (Hall / Everest / Cox-D'Antonio); 125 Hz and 4 kHz are the values above
mtx = 4194304 + 31000;
// Stores the six per-band absorption ratios of material m (v0..v5) as logs in the material shape table mtx.
function msetx(m, v0, v1, v2, v3, v4, v5) ( mtx[m * 6] = log(v0); mtx[m * 6 + 1] = log(v1); mtx[m * 6 + 2] = log(v2); mtx[m * 6 + 3] = log(v3); mtx[m * 6 + 4] = log(v4); mtx[m * 6 + 5] = log(v5); );
msetx(0, 0.67, 0.75, 0.90, 1.10, 1.20, 1.33);   // tiles
msetx(1, 8.00, 2.40, 1.20, 0.80, 0.45, 0.35);   // glass: the pane resonates in the bass
msetx(2, 0.60, 0.70, 0.90, 1.10, 1.30, 1.60);   // plaster
msetx(3, 0.12, 0.30, 0.75, 1.25, 2.10, 3.00);   // concrete
msetx(4, 1.60, 1.30, 1.10, 0.90, 0.55, 0.35);   // sheet metal
msetx(5, 2.15, 1.69, 1.31, 0.69, 0.77, 0.85);   // wood paneling on battens
msetx(6, 0.22, 0.55, 0.87, 1.13, 1.10, 1.02);   // heavy curtain
msetx(7, 0.11, 0.33, 0.80, 1.20, 1.30, 1.27);   // foam
msetx(8, 0.16, 0.60, 0.93, 1.07, 1.10, 1.03);   // mineral wool
fqBase = mtx + 64;                               // per-octave reverb: RT ratios (8), barrier-mix copy (8), gains (8), coefficients (8 x 30), states (8 x 12), fit matrix (49 + 49), work
fqR = fqBase; fqRo = fqBase + 8; fqG = fqBase + 16; fqC = fqBase + 24; fqS = fqC + 240; fqB = fqS + 96; fqBi = fqB + 49; fqW = fqBi + 49;
memset(fqS, 0, 96); k = 0; loop(8, fqR[k] = 1; fqG[k] = 0; k += 1; ); fqsr = 0;
// ---- SOURCE DIRECTIVITY per source model: pattern p = (a + (1 - a) cos)^n, floor (leak to the back), darker off-axis
// (share of the highs lost towards the back), high shelf of the power that goes into the room (directional highs)
dtab = mtab + 32;
// Stores the directivity parameters of source model m (pattern a, power n, floor, highs corner, shelf gain) in dtab.
function dset(m, dpa, pn, pfl, phb, psh) ( dtab[m * 5] = dpa; dtab[m * 5 + 1] = pn; dtab[m * 5 + 2] = pfl; dtab[m * 5 + 3] = phb; dtab[m * 5 + 4] = psh; );
dset(0, 0.65, 1, 0.25, 2500, 0.5);      // Original: a talker (Q 2.2, highs beam: behind -16 dB at 4 kHz, -21 dB at 8 kHz)
dset(1, 0.80, 1, 0.30, 5000, 0.7);      // Telephone: small speaker, nearly round
dset(2, 0.80, 1, 0.30, 5000, 0.7);      // Mobile phone
dset(3, 0.80, 1, 0.30, 5000, 0.7);      // Smartphone
dset(4, 0.80, 1, 0.30, 5000, 0.7);      // Walkie-talkie
dset(5, 0.50, 1, 0.20, 3000, 0.5);      // Radio: speaker in a box, front only (Q 3)
dset(6, 0.50, 1, 0.20, 3000, 0.5);      // Boombox
dset(7, 0.55, 1, 0.20, 3000, 0.5);      // TV
dset(8, 0.75, 1, 0.30, 4000, 0.6);      // Laptop
dset(9, 0.75, 1, 0.30, 4000, 0.6);      // Tablet
dset(10, 0.95, 1, 0.80, 9000, 0.8);     // Mini speaker: 360 degrees
dset(11, 1.00, 1, 1.00, 14000, 0.9);    // Headphones (leaking sound): round
dset(12, 0.50, 3, 0.08, 1200, 0.35);    // Megaphone: horn, Q 7, highs strongly beamed
// ---- EARLY CLUSTER: 24 directions (Fibonacci sphere) and arrival fractions, density growing with t^2 like real reflections
cdir = dtab + 72; cufr = cdir + 80;
// DELAY (before the room): two lines of 4 s, the side signal for the direct sound (as long as the input line), filter
// coefficients and states; TEMPO ANALYSIS: onset strength in 10 ms frames (10 s)
dlC = cufr + 32; dlSt = dlC + 32; anaNv = dlSt + 16; ANAMAX = 1000;   // (the delay lines themselves: see srlayout)
memset(dlL, 0, 2 * DLMAX + MAXIN); memset(dlC, 0, 48 + ANAMAX + 80);
dwi = 0; dlhold = 0; shold = 0; sideIn = 0; cDON = 0; dllkey = -1; anaOn = 0; anaN = 0; anaDone = 0; anaSc = anaNv + ANAMAX + 8;
osC = anaSc + 80; osU = osC + 32; osD = osU + 16; memset(osU, 0, 32);   // 4x oversampling of the source-model distortion
mdM = osD + 16; mdT = mdM + 128 * 6 + 8; mdX = mdT + 300 * 4 + 8; mdXS = mdX + 64; mdP = mdXS + 32; mdPf = mdP + 300; mdF = mdPf + 100;
mdI = mdF + 132; mdPi = mdI + 132; mdQ = mdPi + 132; mdQf = mdQ + 128 * 6 + 8; mdQi = mdQf + 132; mdUs = mdQi + 132;   // modes: ids (nx ny nz), merge buffers
bpkC = mdUs + 132; bpkS = bpkC + 8; memset(bpkS, 0, 8);   // barrier coincidence peak: coefficients, states (out L R, neighbor L R)
memset(mdM, 0, 128 * 6); memset(mdXS, 0, 24); mdN = 0; mdPN = 0; mdPend = 0; mdx1 = mdx2 = 0;   // ROOM MODES
// osC: 5 biquad sections (b0 b1 b2 a1 a2 each) of the elliptic low-pass used for the 4x up / down sampling; oc0 … oc24 below are scalar copies of the same values (faster in the inner loop)
osC[0] = 0.00013044032859283806; osC[1] = 0.00019700083762148941; osC[2] = 0.00013044032859283803; osC[3] = -1.5232480565269744; osC[4] = 0.59407966273118606;
osC[5] = 1; osC[6] = -0.08847383427992779; osC[7] = 1.0000000000000002; osC[8] = -1.4997225020050786; osC[9] = 0.67207517614275214;
osC[10] = 1; osC[11] = -0.8130179791719655; osC[12] = 0.99999999999999989; osC[13] = -1.4717689893251873; osC[14] = 0.78105330244098525;
osC[15] = 1; osC[16] = -1.088083953915002; osC[17] = 1.0000000000000002; osC[18] = -1.4590266712276223; osC[19] = 0.88063362441987625;
osC[20] = 1; osC[21] = -1.1853552911927485; osC[22] = 0.99999999999999978; osC[23] = -1.4746145339349073; osC[24] = 0.96224384683568642;
oc0 = osC[0]; oc1 = osC[1]; oc2 = osC[2]; oc3 = osC[3]; oc4 = osC[4]; oc5 = osC[5]; oc6 = osC[6]; oc7 = osC[7]; oc8 = osC[8]; oc9 = osC[9];
oc10 = osC[10]; oc11 = osC[11]; oc12 = osC[12]; oc13 = osC[13]; oc14 = osC[14]; oc15 = osC[15]; oc16 = osC[16]; oc17 = osC[17]; oc18 = osC[18]; oc19 = osC[19];
oc20 = osC[20]; oc21 = osC[21]; oc22 = osC[22]; oc23 = osC[23]; oc24 = osC[24]; su0 = su1 = su2 = su3 = su4 = su5 = su6 = su7 = su8 = su9 = 0; sd0 = sd1 = sd2 = sd3 = sd4 = sd5 = sd6 = sd7 = sd8 = sd9 = 0;
k = 0;
loop(NCL,
  cz0 = 1 - 2 * (k + 0.5) / NCL; cr0 = sqrt(1 - cz0 * cz0); cph = k * 2.39996323;
  cdir[k * 3] = cr0 * cos(cph); cdir[k * 3 + 1] = cr0 * sin(cph); cdir[k * 3 + 2] = cz0;
  cufr[k] = ((k + 0.5) / NCL + 0.37 * sin(k * 12.9898) / NCL);     // jittered, stable
  k += 1;
);
// shuffle the directions against the times (otherwise early = from above, late = from below)
// k <-> (7k + 3) mod 24 pairs every index with exactly one partner - each pair is now swapped once (j0 > k) and with all
// three components; before, every pair was swapped twice (back again) and the shuffle did nothing
k = 0; loop(NCL, j0 = (k * 7 + 3) % NCL; j0 > k ? (
  cs0 = cdir[k * 3]; cdir[k * 3] = cdir[j0 * 3]; cdir[j0 * 3] = cs0;
  cs0 = cdir[k * 3 + 1]; cdir[k * 3 + 1] = cdir[j0 * 3 + 1]; cdir[j0 * 3 + 1] = cs0;
  cs0 = cdir[k * 3 + 2]; cdir[k * 3 + 2] = cdir[j0 * 3 + 2]; cdir[j0 * 3 + 2] = cs0; ); k += 1; );
// vopt: fixed address (see above): [0] marker, [1] paths, [2] direct line, [3] tips, [4] dark, [5] link, [6] waves, [7] b&w, [8] predelay follows the room
// first start of an instance: view defaults (vopt[64], vopt[65] = reflector levels +12 dB, vopt[66] = reflectors off flag (0 = on), vopt[67] = free-field outline view ON, vopt[68] = Input Guard OFF)
vopt[0] != 7333 ? ( vopt[64] = vopt[65] = 12; vopt[66] = 0; vopt[67] = 1; vopt[68] = 0; vopt[69] = 0; vopt[0] = 7333; vopt[1] = 0; vopt[2] = 0; vopt[3] = 0; vopt[4] = 1; vopt[8] = 1; vopt[16] = 1; vopt[11] = 1; vopt[12] = 2304; vopt[14] = vopt[15] = 0; memset(vopt + 21, 0, 24); vopt[45] = 0; vopt[46] = 1; vopt[47] = 0; );   // (Expert: Timeline and Decay by band folded)   // new instance: Basic view
lastlc = lasthc = -1;
// AUTOMATION FROM THE GUI (REAPER 6.74+):
//   slider_automate(sliderX, 0)  begins / continues a touch session  (called on every value change while held)
//   slider_automate(sliderX, 1)  ends the session                   (called on mouse release)
//   slider_automate(sliderX)     legacy one-shot write, no session   (buttons, presets, reset, wheel)
// The one-argument form must NOT be used for gestures: REAPER treats it as a momentary write and the
// envelope takes over again between GUI frames (only visible in Touch/Latch during playback).
// While a gesture is held, @block re-asserts the GUI value each block as a safety net.
// ---- DELAY PRESETS (GUI): 37 practical settings in 3 groups - film & post (ms), music (tempo), sound design ----
// per preset: mode, unit, link time, link EQ, time L / R (ms), note L / R, feedback L / R, level L / R, low cut L / R,
// high cut L / R, tape, drive, link level (sliders 66 … 73, 75 … 84, 86). The tempo source (slider 74) stays as it is.
NDP = 37; dptab = 4194304 + 9000;
dptab[0] = 0; dptab[1] = 0; dptab[2] = 0; dptab[3] = 1; dptab[4] = 85; dptab[5] = 97; dptab[6] = 8; dptab[7] = 8; dptab[8] = 8; dptab[9] = 8; dptab[10] = -9; dptab[11] = -9; dptab[12] = 250; dptab[13] = 250; dptab[14] = 5000; dptab[15] = 5000; dptab[16] = 0; dptab[17] = 0; dptab[18] = 1; dptab[19] = 0; dptab[20] = 0; dptab[21] = 0; dptab[22] = 1; dptab[23] = 28; dptab[24] = 33; dptab[25] = 8; dptab[26] = 8; dptab[27] = 62; dptab[28] = 62; dptab[29] = -12; dptab[30] = -12; dptab[31] = 500; dptab[32] = 500; dptab[33] = 7000; dptab[34] = 7000; dptab[35] = 0; dptab[36] = 0; dptab[37] = 1; dptab[38] = 0; dptab[39] = 0; dptab[40] = 0; dptab[41] = 1; dptab[42] = 140; dptab[43] = 190; dptab[44] = 8; dptab[45] = 8; dptab[46] = 20; dptab[47] = 20; dptab[48] = -10; dptab[49] = -10; dptab[50] = 200; dptab[51] = 200; dptab[52] = 4000; dptab[53] = 4000; dptab[54] = 0; dptab[55] = 0; dptab[56] = 1; dptab[57] = 0; dptab[58] = 0; dptab[59] = 0; dptab[60] = 1; dptab[61] = 75; dptab[62] = 90; dptab[63] = 8; dptab[64] = 8; dptab[65] = 45; dptab[66] = 45; dptab[67] = -12; dptab[68] = -12; dptab[69] = 300; dptab[70] = 300; dptab[71] = 4500; dptab[72] = 4500; dptab[73] = 0; dptab[74] = 0; dptab[75] = 1; dptab[76] = 0; dptab[77] = 0; dptab[78] = 0; dptab[79] = 1; dptab[80] = 240; dptab[81] = 340; dptab[82] = 8; dptab[83] = 8; dptab[84] = 42; dptab[85] = 46; dptab[86] = -7; dptab[87] = -7; dptab[88] = 350; dptab[89] = 350; dptab[90] = 3000; dptab[91] = 3000; dptab[92] = 0; dptab[93] = 0; dptab[94] = 1; dptab[95] = 0; dptab[96] = 0; dptab[97] = 0; dptab[98] = 1; dptab[99] = 160; dptab[100] = 185; dptab[101] = 8; dptab[102] = 8; dptab[103] = 30; dptab[104] = 30; dptab[105] = -9; dptab[106] = -9; dptab[107] = 600; dptab[108] = 600; dptab[109] = 3500; dptab[110] = 3500; dptab[111] = 0; dptab[112] = 20; dptab[113] = 1; dptab[114] = 0; dptab[115] = 0; dptab[116] = 1; dptab[117] = 1; dptab[118] = 110; dptab[119] = 110; dptab[120] = 8; dptab[121] = 8; dptab[122] = 20; dptab[123] = 20; dptab[124] = -10; dptab[125] = -10; dptab[126] = 700; dptab[127] = 700; dptab[128] = 3000; dptab[129] = 3000; dptab[130] = 0; dptab[131] = 40; dptab[132] = 1; dptab[133] = 0; dptab[134] = 0; dptab[135] = 1; dptab[136] = 1; dptab[137] = 180; dptab[138] = 180; dptab[139] = 8; dptab[140] = 8; dptab[141] = 10; dptab[142] = 10; dptab[143] = -18; dptab[144] = -18; dptab[145] = 500; dptab[146] = 500; dptab[147] = 3000; dptab[148] = 3000; dptab[149] = 0; dptab[150] = 20; dptab[151] = 1; dptab[152] = 0; dptab[153] = 0; dptab[154] = 0; dptab[155] = 1; dptab[156] = 320; dptab[157] = 380; dptab[158] = 8; dptab[159] = 8; dptab[160] = 15; dptab[161] = 15; dptab[162] = -16; dptab[163] = -16; dptab[164] = 300; dptab[165] = 300; dptab[166] = 2500; dptab[167] = 2500; dptab[168] = 0; dptab[169] = 0; dptab[170] = 1; dptab[171] = 0; dptab[172] = 0; dptab[173] = 0; dptab[174] = 1; dptab[175] = 190; dptab[176] = 230; dptab[177] = 8; dptab[178] = 8; dptab[179] = 55; dptab[180] = 55; dptab[181] = -11; dptab[182] = -11; dptab[183] = 200; dptab[184] = 200; dptab[185] = 2200; dptab[186] = 2200; dptab[187] = 0; dptab[188] = 0; dptab[189] = 1; dptab[190] = 0; dptab[191] = 0; dptab[192] = 0; dptab[193] = 1; dptab[194] = 600; dptab[195] = 720; dptab[196] = 8; dptab[197] = 8; dptab[198] = 35; dptab[199] = 35; dptab[200] = -12; dptab[201] = -12; dptab[202] = 250; dptab[203] = 250; dptab[204] = 2500; dptab[205] = 2500; dptab[206] = 0; dptab[207] = 0; dptab[208] = 1; dptab[209] = 0; dptab[210] = 0; dptab[211] = 0; dptab[212] = 1; dptab[213] = 1100; dptab[214] = 1400; dptab[215] = 8; dptab[216] = 8; dptab[217] = 25; dptab[218] = 25; dptab[219] = -14; dptab[220] = -14; dptab[221] = 200; dptab[222] = 200; dptab[223] = 1800; dptab[224] = 1800; dptab[225] = 0; dptab[226] = 0; dptab[227] = 1; dptab[228] = 1; dptab[229] = 0; dptab[230] = 0; dptab[231] = 1; dptab[232] = 380; dptab[233] = 570; dptab[234] = 8; dptab[235] = 8; dptab[236] = 55; dptab[237] = 55; dptab[238] = -8; dptab[239] = -8; dptab[240] = 300; dptab[241] = 300; dptab[242] = 2500; dptab[243] = 2500; dptab[244] = 45; dptab[245] = 0; dptab[246] = 1; dptab[247] = 0; dptab[248] = 1; dptab[249] = 1; dptab[250] = 1; dptab[251] = 50; dptab[252] = 50; dptab[253] = 5; dptab[254] = 5; dptab[255] = 25; dptab[256] = 25; dptab[257] = -10; dptab[258] = -10; dptab[259] = 350; dptab[260] = 350; dptab[261] = 5000; dptab[262] = 5000; dptab[263] = 0; dptab[264] = 0; dptab[265] = 1; dptab[266] = 0; dptab[267] = 1; dptab[268] = 1; dptab[269] = 1; dptab[270] = 50; dptab[271] = 50; dptab[272] = 3; dptab[273] = 3; dptab[274] = 30; dptab[275] = 30; dptab[276] = -12; dptab[277] = -12; dptab[278] = 300; dptab[279] = 300; dptab[280] = 3500; dptab[281] = 3500; dptab[282] = 10; dptab[283] = 0; dptab[284] = 1; dptab[285] = 0; dptab[286] = 1; dptab[287] = 1; dptab[288] = 1; dptab[289] = 50; dptab[290] = 50; dptab[291] = 2; dptab[292] = 2; dptab[293] = 30; dptab[294] = 30; dptab[295] = -6; dptab[296] = -6; dptab[297] = 450; dptab[298] = 450; dptab[299] = 4000; dptab[300] = 4000; dptab[301] = 15; dptab[302] = 0; dptab[303] = 1; dptab[304] = 0; dptab[305] = 1; dptab[306] = 1; dptab[307] = 1; dptab[308] = 50; dptab[309] = 50; dptab[310] = 8; dptab[311] = 8; dptab[312] = 15; dptab[313] = 15; dptab[314] = -9; dptab[315] = -9; dptab[316] = 600; dptab[317] = 600; dptab[318] = 4500; dptab[319] = 4500; dptab[320] = 0; dptab[321] = 15; dptab[322] = 1; dptab[323] = 0; dptab[324] = 1; dptab[325] = 1; dptab[326] = 1; dptab[327] = 50; dptab[328] = 50; dptab[329] = 11; dptab[330] = 11; dptab[331] = 20; dptab[332] = 20; dptab[333] = -12; dptab[334] = -12; dptab[335] = 400; dptab[336] = 400; dptab[337] = 6000; dptab[338] = 6000; dptab[339] = 0; dptab[340] = 0; dptab[341] = 1; dptab[342] = 0; dptab[343] = 1; dptab[344] = 1; dptab[345] = 1; dptab[346] = 50; dptab[347] = 50; dptab[348] = 6; dptab[349] = 6; dptab[350] = 38; dptab[351] = 38; dptab[352] = -8; dptab[353] = -8; dptab[354] = 250; dptab[355] = 250; dptab[356] = 5500; dptab[357] = 5500; dptab[358] = 0; dptab[359] = 0; dptab[360] = 1; dptab[361] = 0; dptab[362] = 1; dptab[363] = 0; dptab[364] = 1; dptab[365] = 50; dptab[366] = 50; dptab[367] = 5; dptab[368] = 6; dptab[369] = 28; dptab[370] = 28; dptab[371] = -10; dptab[372] = -10; dptab[373] = 250; dptab[374] = 250; dptab[375] = 5000; dptab[376] = 5000; dptab[377] = 0; dptab[378] = 0; dptab[379] = 1; dptab[380] = 1; dptab[381] = 1; dptab[382] = 1; dptab[383] = 1; dptab[384] = 50; dptab[385] = 50; dptab[386] = 8; dptab[387] = 8; dptab[388] = 42; dptab[389] = 42; dptab[390] = -8; dptab[391] = -8; dptab[392] = 250; dptab[393] = 250; dptab[394] = 6000; dptab[395] = 6000; dptab[396] = 0; dptab[397] = 0; dptab[398] = 1; dptab[399] = 1; dptab[400] = 1; dptab[401] = 1; dptab[402] = 1; dptab[403] = 50; dptab[404] = 50; dptab[405] = 3; dptab[406] = 3; dptab[407] = 40; dptab[408] = 40; dptab[409] = -9; dptab[410] = -9; dptab[411] = 300; dptab[412] = 300; dptab[413] = 4500; dptab[414] = 4500; dptab[415] = 0; dptab[416] = 0; dptab[417] = 1; dptab[418] = 1; dptab[419] = 1; dptab[420] = 1; dptab[421] = 1; dptab[422] = 50; dptab[423] = 50; dptab[424] = 10; dptab[425] = 10; dptab[426] = 35; dptab[427] = 35; dptab[428] = -10; dptab[429] = -10; dptab[430] = 300; dptab[431] = 300; dptab[432] = 7000; dptab[433] = 7000; dptab[434] = 0; dptab[435] = 0; dptab[436] = 1; dptab[437] = 0; dptab[438] = 1; dptab[439] = 0; dptab[440] = 1; dptab[441] = 50; dptab[442] = 50; dptab[443] = 8; dptab[444] = 6; dptab[445] = 30; dptab[446] = 30; dptab[447] = -9; dptab[448] = -9; dptab[449] = 300; dptab[450] = 300; dptab[451] = 5000; dptab[452] = 5000; dptab[453] = 0; dptab[454] = 0; dptab[455] = 1; dptab[456] = 0; dptab[457] = 1; dptab[458] = 1; dptab[459] = 1; dptab[460] = 50; dptab[461] = 50; dptab[462] = 8; dptab[463] = 8; dptab[464] = 45; dptab[465] = 45; dptab[466] = -8; dptab[467] = -8; dptab[468] = 150; dptab[469] = 150; dptab[470] = 3200; dptab[471] = 3200; dptab[472] = 35; dptab[473] = 45; dptab[474] = 1; dptab[475] = 0; dptab[476] = 1; dptab[477] = 1; dptab[478] = 1; dptab[479] = 50; dptab[480] = 50; dptab[481] = 5; dptab[482] = 5; dptab[483] = 40; dptab[484] = 40; dptab[485] = -9; dptab[486] = -9; dptab[487] = 200; dptab[488] = 200; dptab[489] = 2500; dptab[490] = 2500; dptab[491] = 20; dptab[492] = 30; dptab[493] = 1; dptab[494] = 1; dptab[495] = 1; dptab[496] = 1; dptab[497] = 1; dptab[498] = 50; dptab[499] = 50; dptab[500] = 3; dptab[501] = 3; dptab[502] = 72; dptab[503] = 72; dptab[504] = -6; dptab[505] = -6; dptab[506] = 350; dptab[507] = 350; dptab[508] = 1800; dptab[509] = 1800; dptab[510] = 25; dptab[511] = 35; dptab[512] = 1; dptab[513] = 0; dptab[514] = 0; dptab[515] = 1; dptab[516] = 1; dptab[517] = 110; dptab[518] = 110; dptab[519] = 8; dptab[520] = 8; dptab[521] = 12; dptab[522] = 12; dptab[523] = -6; dptab[524] = -6; dptab[525] = 200; dptab[526] = 200; dptab[527] = 4200; dptab[528] = 4200; dptab[529] = 25; dptab[530] = 20; dptab[531] = 1; dptab[532] = 0; dptab[533] = 0; dptab[534] = 0; dptab[535] = 0; dptab[536] = 1; dptab[537] = 15; dptab[538] = 8; dptab[539] = 8; dptab[540] = 0; dptab[541] = 0; dptab[542] = -48; dptab[543] = -3; dptab[544] = 20; dptab[545] = 20; dptab[546] = 20000; dptab[547] = 20000; dptab[548] = 0; dptab[549] = 0; dptab[550] = 0; dptab[551] = 0; dptab[552] = 0; dptab[553] = 0; dptab[554] = 1; dptab[555] = 22; dptab[556] = 31; dptab[557] = 8; dptab[558] = 8; dptab[559] = 0; dptab[560] = 0; dptab[561] = -6; dptab[562] = -6; dptab[563] = 150; dptab[564] = 150; dptab[565] = 9000; dptab[566] = 9000; dptab[567] = 30; dptab[568] = 0; dptab[569] = 1; dptab[570] = 0; dptab[571] = 0; dptab[572] = 0; dptab[573] = 1; dptab[574] = 6; dptab[575] = 8; dptab[576] = 8; dptab[577] = 8; dptab[578] = 85; dptab[579] = 85; dptab[580] = -14; dptab[581] = -14; dptab[582] = 300; dptab[583] = 300; dptab[584] = 9000; dptab[585] = 9000; dptab[586] = 0; dptab[587] = 0; dptab[588] = 1; dptab[589] = 0; dptab[590] = 0; dptab[591] = 0; dptab[592] = 1; dptab[593] = 3; dptab[594] = 4; dptab[595] = 8; dptab[596] = 8; dptab[597] = 70; dptab[598] = 70; dptab[599] = -10; dptab[600] = -10; dptab[601] = 200; dptab[602] = 200; dptab[603] = 12000; dptab[604] = 12000; dptab[605] = 0; dptab[606] = 0; dptab[607] = 1; dptab[608] = 0; dptab[609] = 1; dptab[610] = 1; dptab[611] = 1; dptab[612] = 50; dptab[613] = 50; dptab[614] = 12; dptab[615] = 12; dptab[616] = 60; dptab[617] = 60; dptab[618] = -8; dptab[619] = -8; dptab[620] = 300; dptab[621] = 300; dptab[622] = 8000; dptab[623] = 8000; dptab[624] = 0; dptab[625] = 0; dptab[626] = 1; dptab[627] = 1; dptab[628] = 0; dptab[629] = 0; dptab[630] = 1; dptab[631] = 37; dptab[632] = 53; dptab[633] = 8; dptab[634] = 8; dptab[635] = 65; dptab[636] = 65; dptab[637] = -12; dptab[638] = -12; dptab[639] = 600; dptab[640] = 600; dptab[641] = 5000; dptab[642] = 5000; dptab[643] = 80; dptab[644] = 0; dptab[645] = 1; dptab[646] = 1; dptab[647] = 1; dptab[648] = 1; dptab[649] = 1; dptab[650] = 50; dptab[651] = 50; dptab[652] = 11; dptab[653] = 11; dptab[654] = 75; dptab[655] = 75; dptab[656] = -10; dptab[657] = -10; dptab[658] = 500; dptab[659] = 500; dptab[660] = 3000; dptab[661] = 3000; dptab[662] = 60; dptab[663] = 0; dptab[664] = 1; dptab[665] = 0; dptab[666] = 0; dptab[667] = 0; dptab[668] = 1; dptab[669] = 90; dptab[670] = 110; dptab[671] = 8; dptab[672] = 8; dptab[673] = 94; dptab[674] = 94; dptab[675] = -16; dptab[676] = -16; dptab[677] = 400; dptab[678] = 400; dptab[679] = 2000; dptab[680] = 2000; dptab[681] = 15; dptab[682] = 0; dptab[683] = 1; dptab[684] = 0; dptab[685] = 0; dptab[686] = 0; dptab[687] = 1; dptab[688] = 450; dptab[689] = 530; dptab[690] = 8; dptab[691] = 8; dptab[692] = 92; dptab[693] = 92; dptab[694] = -14; dptab[695] = -14; dptab[696] = 300; dptab[697] = 300; dptab[698] = 1500; dptab[699] = 1500; dptab[700] = 30; dptab[701] = 20; dptab[702] = 1;
dpIX = vopt + 6000; dpIY = vopt + 6050;   // delay preset menu: item positions (GUI, up to 48 presets; below the state scratch at 8192)
SLOTS = 120;                               // per-slider GUI arrays: index 1 … 119 must fit (slider count + margin)
snapv = 4194304 + 4096; gchg = snapv + SLOTS; gtouch = gchg + SLOTS; gval = gtouch + SLOTS; gheld = gval + SLOTS;   // vopt[0..31] stay clear (options); last GUI value per slider + "gesture held" flag
memset(snapv, 0, 5 * SLOTS);   // far above the audio buffers, next to the snapshot descriptions (dsc)
wlv = vopt + 1500;                         // waves view: level of the last 32 emitted wavefronts (GUI only)
earO = vopt + 1600; earI = earO + 56; earNO = 28; earNI = 16;   // listener icon: ear outline + inner fold (u = away from the head, v = to the front)
earO[0] = -0.90; earO[1] = 2.80; earO[2] = -0.69; earO[3] = 3.32; earO[4] = -0.39; earO[5] = 4.04; earO[6] = 0.00; earO[7] = 4.60; earO[8] = 0.49; earO[9] = 4.87; earO[10] = 1.06; earO[11] = 4.97; earO[12] = 1.60; earO[13] = 4.90; earO[14] = 2.10; earO[15] = 4.62; earO[16] = 2.56; earO[17] = 4.16; earO[18] = 2.90; earO[19] = 3.60; earO[20] = 3.07; earO[21] = 2.93; earO[22] = 3.11; earO[23] = 2.17; earO[24] = 3.10; earO[25] = 1.40; earO[26] = 3.04; earO[27] = 0.66; earO[28] = 2.93; earO[29] = -0.08; earO[30] = 2.80; earO[31] = -0.80; earO[32] = 2.69; earO[33] = -1.49; earO[34] = 2.57; earO[35] = -2.16; earO[36] = 2.40; earO[37] = -2.80; earO[38] = 2.15; earO[39] = -3.47; earO[40] = 1.84; earO[41] = -4.13; earO[42] = 1.50; earO[43] = -4.60; earO[44] = 1.10; earO[45] = -4.84; earO[46] = 0.66; earO[47] = -4.90; earO[48] = 0.30; earO[49] = -4.80; earO[50] = 0.06; earO[51] = -4.46; earO[52] = -0.10; earO[53] = -3.96; earO[54] = -0.20; earO[55] = -3.60;
earI[0] = 0.20; earI[1] = 2.20; earI[2] = 0.44; earI[3] = 2.47; earI[4] = 0.77; earI[5] = 2.84; earI[6] = 1.10; earI[7] = 3.00; earI[8] = 1.41; earI[9] = 2.82; earI[10] = 1.71; earI[11] = 2.44; earI[12] = 1.90; earI[13] = 2.00; earI[14] = 1.92; earI[15] = 1.50; earI[16] = 1.83; earI[17] = 0.93; earI[18] = 1.70; earI[19] = 0.40; earI[20] = 1.54; earI[21] = -0.03; earI[22] = 1.35; earI[23] = -0.41; earI[24] = 1.20; earI[25] = -0.80; earI[26] = 1.13; earI[27] = -1.24; earI[28] = 1.11; earI[29] = -1.69; earI[30] = 1.10; earI[31] = -2.00;
// ---- output limiter: look-ahead peak limiter, ceiling -1 dBFS ----
// The needed gain of every sample (ceiling / peak) goes through a sliding minimum over the look-ahead window, an exponential
// release and a moving average over the same window. The average is the attack ramp: it reaches the needed gain exactly when
// the peak leaves the delay line, so no sample passes above the ceiling and the gain never steps (no click, no distortion).
LMLA = min(590, max(1, floor(0.0015 * srate + 0.5)));   // look-ahead 1.5 ms = latency, reported to REAPER (always, also with the limiter off); at most 590 samples - the buffers (5 x LMLA + 7) must stay below vopt + 5000 (above ~400 kHz they reached the snapshot names)
LMN = LMLA + 1; LMQ = LMN + 2; LMCEIL = 0.89;   // window (samples), queue size, ceiling -1 dBFS (the soft clipper above it stays as a safety net)
lmb = vopt + 2000; lmDL = lmb; lmDR = lmDL + LMLA; lmW = lmDR + LMLA; lmQv = lmW + LMN; lmQt = lmQv + LMQ;
memset(lmDL, 0, 2 * LMLA); k = 0; loop(LMN, lmW[k] = 1; k += 1; );
lmsum = LMN; lmqh = lmqn = 0; lmt = 0; lmd = 0; lmdd = 0; lmr = 1; lmga = 1; lmgv = 1;
lmrel = 1 - exp(-1 / (0.15 * srate));        // release 150 ms
crk = exp(-1 / (0.3 * srate)); crXY = crXX = crYY = 0;   // correlation meter: 0.3 s
pdc_delay = LMLA + 1; pdc_bot_ch = 0; pdc_top_ch = 2;
fzb = vopt + 400; fzS = fzb + 80; fzS2 = fzb + 104; fzS1 = fzb + 112; fzP = fzb + 130;   // fzS2: codec band-limit states, fzS1: first-order states (odd slopes)
fzP = fzb + 130; fzt = fzb + 160; fzTP = fzb + 240; fzresp = fzb + 270;
fzcur = alloc(400); fzBP = fzb + 1010;   // fzcur 400 points (from the allocator - the fixed address overlapped) // GUI: live response of the edited source EQ (400 points), unedited model parameters
memset(fzb, 0, 1090);
fgP = vopt + 5000; fg2P = fgP + 8; memset(fgP, 0, 16);
snam = vopt + 5200; sneb = vopt + 5700; rmX = 4194304 + 32000 + 1900; rmY = rmX + 100; kprev = vopt + 5850; xpf = vopt + 5880; dwT = vopt + 5900; dwA2 = vopt + 5920; anaR = 4194304 + 6000; anaS = anaR + 1024; anaRefId = -1; anaRes = 0;   // snam: snapshot names, NSNAP x SNL bytes (UTF-8, 0 = end) - saved via @serialize; sneb: name being edited   // rmX / rmY: room menu item positions (GUI)   // anaR / anaS: tempo fine step, autocorrelation (GUI)
SNL = 24; SNDBL = 0.35; SNPW = 360; SNPH = 178;   // name length (bytes), double-click time (s), snapshot popup size
dsc = 4194304; DSL = 128; dseb = dsc + 16 * DSL;   // snapshot descriptions: 16 x DSL bytes (UTF-8) far above all audio buffers (fixed - @serialize may run first); description being edited
tbo = vopt + 5960; secop = vopt + 5100; k = 1; loop(23, secop[k] = 1; k += 1; ); rtbv = vopt + 5130; stord = vopt + 5140; stnat = vopt + 5150; stal = vopt + 5160; barL = 1;   // GUI: how far each section is open (folding animation)   // loop gains of the reverb lines in the last block (burst guard, see @block)
stC = fzb + 1050; stS = fzb + 1070;        // bass steering: Linkwitz-Riley split at 250 Hz (2 + 2 biquads), 16 filter states
// 4-point Hermite (Catmull-Rom) read from a circular buffer: flat to ~-0.3 dB at 10 kHz for any fraction
// (linear interpolation loses up to 2 dB at 10 kHz and 6 dB at 16 kHz at half-sample positions)
function rdh(b, n, rp) local(i0, im, i1, i2, t, xm, x0, x1, x2)
(
  rp < 0 ? rp += n;
  i0 = floor(rp); t = rp - i0; i0 >= n ? i0 -= n;
  im = i0 - 1; im < 0 ? im += n;
  i1 = i0 + 1; i1 >= n ? i1 -= n;
  i2 = i1 + 1; i2 >= n ? i2 -= n;
  xm = b[im]; x0 = b[i0]; x1 = b[i1]; x2 = b[i2];
  x0 + t * (0.5 * (x1 - xm) + t * ((xm - 2.5 * x0 + 2 * x1 - 0.5 * x2) + t * (0.5 * (x2 - xm) + 1.5 * (x0 - x1))));
);
// BURST GUARD, step by step: the old content of reverb line j (j < 8: main network, 8 … 11: neighbor room) is scaled by
// scRat[j] - but only the samples that are about to be read, block by block (scaling a whole line at once costs up to 1 ms at
// 96 kHz: a dropout). A line has up to two readers: the current length and, during a crossfade, the old length. Cursor scC
// runs ahead of the newer reader up to scE (= first sample written after the change); cursor scD runs ahead of the older
// reader up to scN (where the newer reader started) - so no sample is scaled twice.
function scbase(j) ( j < 8 ? fdnbase + j * LINEMAX : fdn2base + (j - 8) * LINEMAX; );
// Multiplies samples [a,b) (wrapping at LINEMAX) of FDN delay line j by its pending scale ratio scRat[j].
function scrng(j, a, b) local(bp, p)
(
  b > a ? ( bp = scbase(j); p = a % LINEMAX; loop(b - a, bp[p] *= scRat[j]; p += 1; p >= LINEMAX ? p = 0; ); );
);
// Completes a pending lazy line rescale on line j: applies the remaining ranges at once and marks both jobs done.
function scfinish(j) ( scrng(j, scC[j], scE[j]); scC[j] = scE[j]; scrng(j, scD[j], scN[j]); scD[j] = scN[j]; );
// Starts a lazy gain rescale of FDN line j by sratio, computing the sample ranges the new and old readers will touch.
function scstart(j, sratio, dnew, dold) local(sa, sb)
(
  scfinish(j);                                                 // a job still running: finish it first
  sa = max(fwn - LINEMAX + 1, fwn - ceil(dnew + 2 * moddep) - 8); sb = max(fwn - LINEMAX + 1, fwn - ceil(dold + 2 * moddep) - 8);
  scRat[j] = sratio; scE[j] = fwn; scC[j] = max(sa, sb); scD[j] = min(sa, sb); scN[j] = scC[j];
);
// per block: dmin / dmax = delays of the newer / older reader this block; olda = the older reader still reads
function scstep(j, dmin, dmax, olda)
(
  scC[j] < scE[j] ? ( scrng(j, scC[j], min(scE[j], fwn + samplesblock - floor(dmin) + 4)); scC[j] = max(scC[j], min(scE[j], fwn + samplesblock - floor(dmin) + 4)); );
  scD[j] < scN[j] ? (
    olda ? ( scrng(j, scD[j], min(scN[j], fwn + samplesblock - floor(dmax) + 4)); scD[j] = max(scD[j], min(scN[j], fwn + samplesblock - floor(dmax) + 4)); )
         : ( scD[j] = scN[j]; );                               // nobody reads that part any more
  );
);
// antiderivatives for first-order anti-aliased (ADAA) waveshaping in the source models
function tnr(u) ( u = min(3, max(-3, u)); u * (27 + u * u) / (27 + 9 * u * u); );   // tanh, rational approximation (0 at 0, saturates to +-1 at +-3)
// Numerically stable log(cosh(u)), used as the ADAA antiderivative of tanh.
function lcosh(u) local(a) ( a = abs(u); a + log(1 + exp(-2 * a)) - 0.693147180559945; );
// tanh(u) computed via a clamped exponential (u limited to +-20 equivalent), safe against overflow.
function thf(u) local(e) ( e = exp(min(40, max(-40, 2 * u))); (e - 1) / (e + 1); );
// Antiderivative of the hard clip (clamp to +-1): u^2/2 inside, |u|-0.5 outside, for ADAA clipping.
function clipF(u) local(a) ( a = abs(u); a <= 1 ? 0.5 * u * u : a - 0.5; );
fzU1 = fzU2 = 0; fzL1 = odS[33] = odS[73] = lcosh(0); fzDx = fzDy = 0; fzDR = 1 - 2 * $pi * 20 / srate; fzAsym = 0; fzAsT = 0; fzSatN = fzClipN = 1; fzOutG = 1; fzLin = 1; xEQL = cEQL = 1; bcmpE = 1; lastfzo = -1; lastfzn = -1; fpanic = 0; fzHPb = fzHPo = fzLPb = fzLPo = 0;
fzrel = exp(-1 / (0.05 * srate)); lastfz = -1; fzon = 0; shpin = 0; shsec = 0; shlast = 0; cfade = 1; FADET = 0.28; shprev = 0; shstart = 0; tipstart = 0; helpstart = 0;   // help texts fade in over 0.28 s (FADET)
// (these were lost inside the comment above and did not run - fzrs = 1 above all: it is the seed of the source model's own
//  noise generator, and a seed of 0 is a fixed point, so the hiss became a DC offset until the transport was started once)
eqdrag = 0; lasteqn = -1; lasteqt = 0; eqhov = -1; lasteqk = -1; wmenu = 0; emenu = 0; fzPush = 1; fzrespok = 0; fzacc = 0; fzhold = 0; fzenv = 0; fzgate = 0; fzrs = 1;
rmenu = smenu = helpon = 0; rpmenu = 0; bmenu = 0; kmenu = 0; icOL = 0; zoomv = 0; segfont = 2; fmenu = 0;
pXs = 1; rflSel = 0; rflHov = 0; rflHx0 = rflHy0 = rflHx1 = rflHy1 = 0; rflHv0 = rflHv1 = 0; lrnTp = -1; lrnB = 0; lrnW = 0; lrnShow = 0; lrnTL = 0; tpon = 0; tmbOn = 0; xpon = 0; tptg = 2; tnsel = 1; dmenu = 0; tapN = 0; tapLast = 0; anaSel = 0; anaCmd = 0; anaStp = 0;   // Tempo tools (Expert), Expert-settings list (Basic), tap, analysis requests
// GUI: keep the framebuffer between frames (partial redraw of the meters when nothing else changed)
gfx_clear = -1; lastukey = -1; fullcnt = 0; lasttip = 0; crvZ = 2; vzcX0 = vzcX1 = 99999;   // (vzcX1: the left edge of the Fit / 1:1 chips, see rflTop)
wlast = -100; wlife = 2; lastwe = -1;                  // waves view: time of the last front with signal, life of a front
IOX = 410; IOY = 226; IOW = 180; IOH = 72;      // I/O section
// explicit slider assignment by index (slider(i) is read-only in practice)
function sset(i, v) (
  i >= 10 && i <= 15 ? ganDirty = 1;   // a position set by the GUI (hand, preset, undo) moves the ghost path - the host's automation does not
  i == 1 ? slider1 = v;
  i == 2 ? slider2 = v;
  i == 3 ? slider3 = v;
  i == 4 ? slider4 = v;
  i == 5 ? slider5 = v;
  i == 6 ? slider6 = v;
  i == 7 ? slider7 = v;
  i == 8 ? slider8 = v;
  i == 9 ? slider9 = v;
  i == 10 ? slider10 = v;
  i == 11 ? slider11 = v;
  i == 12 ? slider12 = v;
  i == 13 ? slider13 = v;
  i == 14 ? slider14 = v;
  i == 15 ? slider15 = v;
  i == 16 ? slider16 = v;
  i == 17 ? slider17 = v;
  i == 18 ? slider18 = v;
  i == 19 ? slider19 = v;
  i == 20 ? slider20 = v;
  i == 21 ? slider21 = v;
  i == 22 ? slider22 = v;
  i == 23 ? slider23 = v;
  i == 24 ? slider24 = v;
  i == 25 ? slider25 = v;
  i == 26 ? slider26 = v;
  i == 27 ? slider27 = v;
  i == 28 ? slider28 = v;
  i == 29 ? slider29 = v;
  i == 30 ? slider30 = v;
  i == 31 ? slider31 = v;
  i == 32 ? slider32 = v;
  i == 33 ? slider33 = v;
  i == 34 ? slider34 = v;
  i == 35 ? slider35 = v;
  i == 36 ? slider36 = v;
  i == 37 ? slider37 = v;
  i == 38 ? slider38 = v;
  i == 39 ? slider39 = v;
  i == 40 ? slider40 = v;
  i == 41 ? slider41 = v;
  i == 42 ? slider42 = v;
  i == 43 ? slider43 = v;
  i == 44 ? slider44 = v;
  i == 45 ? slider45 = v;
  i == 46 ? slider46 = v;
  i == 47 ? slider47 = v;
  i == 48 ? slider48 = v;
  i == 49 ? slider49 = v;
  i == 50 ? slider50 = v;
  i == 51 ? slider51 = v;
  i == 52 ? slider52 = v;
  i == 53 ? slider53 = v;
  i == 54 ? slider54 = v;
  i == 55 ? slider55 = v;
  i == 56 ? slider56 = v;
  i == 57 ? slider57 = v;
  i == 58 ? slider58 = v;
  i == 59 ? slider59 = v;
  i == 60 ? slider60 = v;
  i == 61 ? slider61 = v;
  i == 62 ? slider62 = v;
  i == 63 ? slider63 = v;
  i == 64 ? slider64 = v;
  i == 65 ? slider65 = v;
  i == 66 ? slider66 = v;
  i == 67 ? slider67 = v;
  i == 68 ? slider68 = v;
  i == 69 ? slider69 = v;
  i == 70 ? slider70 = v;
  i == 71 ? slider71 = v;
  i == 72 ? slider72 = v;
  i == 73 ? slider73 = v;
  i == 74 ? slider74 = v;
  i == 75 ? slider75 = v;
  i == 76 ? slider76 = v;
  i == 77 ? slider77 = v;
  i == 78 ? slider78 = v;
  i == 79 ? slider79 = v;
  i == 80 ? slider80 = v;
  i == 81 ? slider81 = v;
  i == 82 ? slider82 = v;
  i == 83 ? slider83 = v;
  i == 84 ? slider84 = v;
  i == 85 ? slider85 = v;
  i == 86 ? slider86 = v;
  i == 87 ? slider87 = v;
  i == 88 ? slider88 = v;
  i == 89 ? slider89 = v;
  i == 90 ? slider90 = v;
  i == 91 ? slider91 = v;
  i == 92 ? slider92 = v;
  i == 93 ? slider93 = v;
  i == 94 ? slider94 = v;
  i == 95 ? slider95 = v;
  i == 96 ? slider96 = v;
  i == 97 ? slider97 = v;
  i == 98 ? slider98 = v;
  i == 99 ? slider99 = v;
  i == 100 ? slider100 = v;
  i == 101 ? slider101 = v;
  i == 102 ? slider102 = v;
  i == 103 ? slider103 = v;
  i == 104 ? slider104 = v;
  i == 105 ? slider105 = v;
  i == 106 ? slider106 = v;
  i == 107 ? slider107 = v;
  i == 108 ? slider108 = v;
  i == 109 ? slider109 = v;
  i == 110 ? slider110 = v;
  i == 111 ? slider111 = v; i == 112 ? slider112 = v; i == 113 ? slider113 = v; i == 114 ? slider114 = v; i == 115 ? slider115 = v; i == 116 ? slider116 = v; i == 117 ? slider117 = v; i == 118 ? slider118 = v; i == 119 ? slider119 = v;
);
// audio-thread copy (see the note at the GUI design functions: one function must not run in two threads)
function a_sset(i, v) (
  i == 1 ? slider1 = v;
  i == 2 ? slider2 = v;
  i == 3 ? slider3 = v;
  i == 4 ? slider4 = v;
  i == 5 ? slider5 = v;
  i == 6 ? slider6 = v;
  i == 7 ? slider7 = v;
  i == 8 ? slider8 = v;
  i == 9 ? slider9 = v;
  i == 10 ? slider10 = v;
  i == 11 ? slider11 = v;
  i == 12 ? slider12 = v;
  i == 13 ? slider13 = v;
  i == 14 ? slider14 = v;
  i == 15 ? slider15 = v;
  i == 16 ? slider16 = v;
  i == 17 ? slider17 = v;
  i == 18 ? slider18 = v;
  i == 19 ? slider19 = v;
  i == 20 ? slider20 = v;
  i == 21 ? slider21 = v;
  i == 22 ? slider22 = v;
  i == 23 ? slider23 = v;
  i == 24 ? slider24 = v;
  i == 25 ? slider25 = v;
  i == 26 ? slider26 = v;
  i == 27 ? slider27 = v;
  i == 28 ? slider28 = v;
  i == 29 ? slider29 = v;
  i == 30 ? slider30 = v;
  i == 31 ? slider31 = v;
  i == 32 ? slider32 = v;
  i == 33 ? slider33 = v;
  i == 34 ? slider34 = v;
  i == 35 ? slider35 = v;
  i == 36 ? slider36 = v;
  i == 37 ? slider37 = v;
  i == 38 ? slider38 = v;
  i == 39 ? slider39 = v;
  i == 40 ? slider40 = v;
  i == 41 ? slider41 = v;
  i == 42 ? slider42 = v;
  i == 43 ? slider43 = v;
  i == 44 ? slider44 = v;
  i == 45 ? slider45 = v;
  i == 46 ? slider46 = v;
  i == 47 ? slider47 = v;
  i == 48 ? slider48 = v;
  i == 49 ? slider49 = v;
  i == 50 ? slider50 = v;
  i == 51 ? slider51 = v;
  i == 52 ? slider52 = v;
  i == 53 ? slider53 = v;
  i == 54 ? slider54 = v;
  i == 55 ? slider55 = v;
  i == 56 ? slider56 = v;
  i == 57 ? slider57 = v;
  i == 58 ? slider58 = v;
  i == 59 ? slider59 = v;
  i == 60 ? slider60 = v;
  i == 61 ? slider61 = v;
  i == 62 ? slider62 = v;
  i == 63 ? slider63 = v;
  i == 64 ? slider64 = v;
  i == 65 ? slider65 = v;
  i == 66 ? slider66 = v;
  i == 67 ? slider67 = v;
  i == 68 ? slider68 = v;
  i == 69 ? slider69 = v;
  i == 70 ? slider70 = v;
  i == 71 ? slider71 = v;
  i == 72 ? slider72 = v;
  i == 73 ? slider73 = v;
  i == 74 ? slider74 = v;
  i == 75 ? slider75 = v;
  i == 76 ? slider76 = v;
  i == 77 ? slider77 = v;
  i == 78 ? slider78 = v;
  i == 79 ? slider79 = v;
  i == 80 ? slider80 = v;
  i == 81 ? slider81 = v;
  i == 82 ? slider82 = v;
  i == 83 ? slider83 = v;
  i == 84 ? slider84 = v;
  i == 85 ? slider85 = v;
  i == 86 ? slider86 = v;
  i == 87 ? slider87 = v;
  i == 88 ? slider88 = v;
  i == 89 ? slider89 = v;
  i == 90 ? slider90 = v;
  i == 91 ? slider91 = v;
  i == 92 ? slider92 = v;
  i == 93 ? slider93 = v;
  i == 94 ? slider94 = v;
  i == 95 ? slider95 = v;
  i == 96 ? slider96 = v;
  i == 97 ? slider97 = v;
  i == 98 ? slider98 = v;
  i == 99 ? slider99 = v;
  i == 100 ? slider100 = v;
  i == 101 ? slider101 = v;
  i == 102 ? slider102 = v;
  i == 103 ? slider103 = v;
  i == 104 ? slider104 = v;
  i == 105 ? slider105 = v;
  i == 106 ? slider106 = v;
  i == 107 ? slider107 = v;
  i == 108 ? slider108 = v;
  i == 109 ? slider109 = v;
  i == 110 ? slider110 = v;
  i == 111 ? slider111 = v; i == 112 ? slider112 = v; i == 113 ? slider113 = v; i == 114 ? slider114 = v; i == 115 ? slider115 = v; i == 116 ? slider116 = v; i == 117 ? slider117 = v; i == 118 ? slider118 = v; i == 119 ? slider119 = v;
);
// PREDELAY FOLLOWS THE ROOM (vopt[8], Reverb row): when width, depth, height or size are changed by hand, the predelay
// goes with the room as in the presets - about 3.5 ms per meter of mean free path (4 V / S) above 0.8 m, 0 ... 60 ms
// (living room 6, classroom 10, lecture hall 22, concert hall 45 ms). Setting the predelay by hand ends the following.
function pdroom() local(w, d, h, v, a) (
  w = slider2 * slider31; d = slider3 * slider31; h = slider4 * slider31; v = w * d * h; a = 2 * (w * d + w * h + d * h);
  floor(min(60, max(0, 3.5 * (4 * v / max(0.01, a) - 0.8))) * 2 + 0.5) / 2;
);
// Sets slider i to v from the GUI, flags it changed, holds the value for the audio thread; manages predelay auto-follow when room size changes.
function setp(i, v) (
  abs(slider(i) - v) > 0.0000001 ? ( sset(i, v); gchg[i] = 1; );
  gval[i] = v; gheld[i] = 1;                 // audio thread re-asserts this value while the gesture is held
  i == 34 && !pdint ? ( vopt[8] = 0; );      // the predelay set by hand: it no longer follows the room
  vopt[8] >= 0.5 && (i == 2 || i == 3 || i == 4 || i == 31) ? (   // (set here directly: EEL2 functions cannot call themselves)
    pdv = pdroom(); abs(slider34 - pdv) > 0.0000001 ? ( sset(34, pdv); gchg[34] = 1; ); gval[34] = pdv; gheld[34] = 1;
  );
);

tIX[0] = 0; tIY[0] = 0; tIZ[0] = 0;
tIX[1] = 0; tIY[1] = -1; tIZ[1] = 0;     // N (back)
tIX[2] = 1; tIY[2] = 0; tIZ[2] = 0;      // E (right)
tIX[3] = 0; tIY[3] = 1; tIZ[3] = 0;      // S (front)
tIX[4] = -1; tIY[4] = 0; tIZ[4] = 0;     // W (left)
tIX[5] = 0; tIY[5] = 0; tIZ[5] = -1;     // floor
tIX[6] = 0; tIY[6] = 0; tIZ[6] = 1;      // ceiling
// second-order image cells (taps 7-24): all integer offsets (i, j, m) in -2 … 2 with |i| + |j| + |m| = 2 (18 combinations), stored in tIX / tIY / tIZ
k = 7; i = -2;
loop(5, j = -2; loop(5, m = -2; loop(5,
  abs(i) + abs(j) + abs(m) == 2 ? ( tIX[k] = i; tIY[k] = j; tIZ[k] = m; k += 1; );
  m += 1; ); j += 1; ); i += 1; );
// FDN line length ratios of the main network (8 lines, 1 … 2): mutually 'irrational' spacing so the lines share few common modes
fac[0] = 1.000; fac[1] = 1.137; fac[2] = 1.271; fac[3] = 1.418;
fac[4] = 1.559; fac[5] = 1.703; fac[6] = 1.861; fac[7] = 1.997;
// (modph / modrt: the former sine LFOs of the FDN lines - the delay modulation is the random walk (modstep); no longer computed, the slots stay reserved so the memory layout does not move)
modph = apLen + 16; modrt = modph + 8;
fac2[0] = 1.083; fac2[1] = 1.297; fac2[2] = 1.533; fac2[3] = 1.811;           // other ratios than net 1: no shared modes
moddep = 0.00015 * srate;   // 0.15 ms; +-12 samples at 48 kHz (0.25 ms, at most 2 cents on the fastest line) - measured: the fixed comb
                                          // structure of the tail (the same peaks at 25 / 50 / 75 % of RT60) falls from r = 0.4 to about 0.1 in large rooms

wi = 0; fwi = 0; api = 0; fwn = 0; cMuD = cMuE = cMuL = tMuD = tMuE = tMuL = 1; mrlast = -1;   // fwn = samples written to the reverb lines (not wrapped)
cPAN = xPAN = 0; bpM = bpS = 0; bpM1 = bpS1 = 0; inL1 = inR1 = 0; pbD = 1; cSTW = xSTW = 1; cSWD = xSWD = 1; cMONO = xMONO = 0; sdE = mdE = 0; bdL = bdR = 0; cPK1 = 0; cDUK = 1; dkE = 0; xDUK = 0; cSON = xSON = 1; bgoff = 0; bgblend = 0; bpkM = 0; last_mdkey = -1; cABG = xABG = 1; abPw = 0; abE0 = abE1 = 0; abN0 = abN1 = 0; cFMX = xFMX = cSMX = xSMX = cDMX = xDMX = cRMX = xRMX = 1; cDML = xDML = cDMR = xDMR = 1; cSOX = xSOX = 1; cSDRY = xSDRY = 1; xBYPe = 0; soloM = 0; soloT = -100; soA = 0;   // source switch, solo (see @block)
// +-1e-20 alternating offset added to the room input (anti-denormal)
dn = 0.00000000000000000001;
last_mfp = -1;
last_gkey = -1;
last_pkey = -1;
sm  = 1 - exp(-1 / (0.03 * srate));      // 30 ms smoothing for delays/gains
smm = 1 - exp(-1 / (0.012 * srate));     // 12 ms: tap gains while a motion runs
gRK = 10 ^ (3 / 20 / (srate / 1000));     // tap gains rise at most 3 dB per ms
smf = 1 - exp(-1 / (0.15 * srate));      // 150 ms glide for FDN line lengths
smu = 1 - exp(-1 / (0.12 * srate));      // rising gains: 120 ms (a reflection that gets louder comes in gently)
dlsm = 1 - exp(-1 / (0.08 * srate));   // delay time: 80 ms glide (like tape - no clicks when the time changes)
dwr1 = 2 * $pi * 0.53 / srate; dwr2 = 2 * $pi * 0.61 / srate; dwr3 = 2 * $pi * 6.1 / srate; dwr4 = 2 * $pi * 6.7 / srate;   // tape: wow / flutter rates L, R
dwA = 0.001 * srate; dfA = 0.00008 * srate; dwp2 = 1.9; dwp4 = 0.7;     // tape: wow depth 1 ms (dwA) and flutter depth 0.08 ms (dfA), in samples; dwp2 / dwp4 = start phases of the R oscillators (L start at 0)
gwarn = 0;
pkIL = pkIR = pkOL = pkOR = 0; hIL = hIR = hOL = hOR = -150; tIL = tIR = tOL = tOR = 0; hTl = 0;
pmdec = exp(-1 / (0.15 * srate));      // level meters: ~58 dB/s fall (time constant 150 ms)
clipwarn = 0;
snap = 1;
cLLP = xLLP = 0; lpLt = 0; cLM = xLM = 0; cLDf = xLDf = 1; cLPL = xLPL = 0; cLPR = xLPR = 0;
xfR = 1; cXR = 1; xfG = 0; xfcnt = 1000000000; xfRI = 1 / (0.003 * srate); sm5 = 1 - exp(-1 / (0.005 * srate));   // scene change of the reverb network (see @block)
xTK = cTK = 0; tkn = 0; tkstep = 1; bndS = 0; xBNG = cBNG = 0; xBNA = 0;   // tail level matching on room changes; boundary bass of the reverb
lfc = 1 - exp(-2 * $pi * 250 / srate);
dirC = 1 - exp(-2 * $pi * 3000 / srate); dirS = 0;    // directivity: shelf corners of the reverb input (highs / bass)
dirCB = 1 - exp(-2 * $pi * 250 / srate); dirB = 0;
cLG2 = xLG2 = 0; cLD2 = xLD2 = 0; cLLP2 = xLLP2 = 0; lpLt2 = 0; cP2L = cP2R = xP2L = xP2R = 1; last_gkey2 = -1; oLD2 = 0;
xfade = 0; xfstep = 1 / (0.08 * srate); JTHR = srate / 343; JTHRF = 0.003 * srate;   // scene crossfade: 80 ms, triggered by jumps > 1 m / 3 ms
JTS = 0.0005 * srate; dfrz = 0; smd = sm; smfd = smf; ldS = ld2S = 0;                 // < 0.5 ms change = unchanged for the crossfade
// FDN feedback mixing angle theta = 0.5 pi (0.12 + 0.88 * density): cos / sin weigh the identity against the Hadamard part; start value for Density 80 %
cTH = xTH = cos(0.5 * $pi * 0.824); sTH = xSH = sin(0.5 * $pi * 0.824);

// ---------- filters: Butterworth cascades (6..48 dB/oct = order 1..8) ----------

// DELAY note values in quarter notes (slider 72 / 73): 1/1, 1/2., 1/2, 1/4., 1/2T, 1/4, 1/8., 1/4T, 1/8, 1/16., 1/8T, 1/16, 1/32
function dnote(n) ( n = floor(n + 0.5); n == 0 ? 4 : (n == 1 ? 3 : (n == 2 ? 2 : (n == 3 ? 1.5 : (n == 4 ? 4 / 3 : (n == 5 ? 1 : (n == 6 ? 0.75 : (n == 7 ? 2 / 3 : (n == 8 ? 0.5 : (n == 9 ? 0.375 : (n == 10 ? 1 / 3 : (n == 11 ? 0.25 : 0.125))))))))))); );
function g_dnote(n) ( n = floor(n + 0.5); n == 0 ? 4 : (n == 1 ? 3 : (n == 2 ? 2 : (n == 3 ? 1.5 : (n == 4 ? 4 / 3 : (n == 5 ? 1 : (n == 6 ? 0.75 : (n == 7 ? 2 / 3 : (n == 8 ? 0.5 : (n == 9 ? 0.375 : (n == 10 ? 1 / 3 : (n == 11 ? 0.25 : 0.125))))))))))); );   // (GUI copy)
// Returns the note-value name (1/1 ... 1/32, dotted, triplet) for delay index n, rounded to the nearest integer.
function dnname(n) ( n = floor(n + 0.5); n == 0 ? "1/1" : (n == 1 ? "1/2 dot." : (n == 2 ? "1/2" : (n == 3 ? "1/4 dot." : (n == 4 ? "1/2 trip." : (n == 5 ? "1/4" : (n == 6 ? "1/8 dot." : (n == 7 ? "1/4 trip." : (n == 8 ? "1/8" : (n == 9 ? "1/16 dot." : (n == 10 ? "1/8 trip." : (n == 11 ? "1/16" : "1/32"))))))))))); );
// Designs a Butterworth chain of order nord at f (high-pass if ishp, else low-pass): nord / 2 biquads into cb (5 coefficients each), plus a first-order section into fb for odd orders.
function fdesign(cb, fb, ishp, f, nord) local(k, q, w0, cw, al, a0, tk)
(
  f = min(f, 0.45 * srate);
  k = 0;
  loop(floor(nord / 2),
    q = (nord % 2) ? 1 / (2 * cos((k + 1) * $pi / nord)) : 1 / (2 * cos((2 * k + 1) * $pi / (2 * nord)));
    w0 = 2 * $pi * f / srate; cw = cos(w0); al = sin(w0) / (2 * q); a0 = 1 + al;
    ishp ? ( cb[k * 5] = (1 + cw) / 2 / a0; cb[k * 5 + 1] = -(1 + cw) / a0; cb[k * 5 + 2] = (1 + cw) / 2 / a0; )
         : ( cb[k * 5] = (1 - cw) / 2 / a0; cb[k * 5 + 1] = (1 - cw) / a0;  cb[k * 5 + 2] = (1 - cw) / 2 / a0; );
    cb[k * 5 + 3] = -2 * cw / a0; cb[k * 5 + 4] = (1 - al) / a0;
    k += 1;
  );
  (nord % 2) ? (
    tk = tan($pi * f / srate);
    ishp ? ( fb[0] = 1 / (1 + tk); fb[1] = -1 / (1 + tk); ) : ( fb[0] = tk / (1 + tk); fb[1] = tk / (1 + tk); );
    fb[2] = (tk - 1) / (tk + 1);
  );
);
// Runs x through n cascaded biquads (5 coefficients each at cb, 2 states each at st, transposed direct form II); returns the output.
function bqrun(x, cb, st, n) local(i, y, c, z)
(
  i = 0;
  loop(n,
    c = cb + i * 5; z = st + i * 2;
    y = c[0] * x + z[0];
    z[0] = c[1] * x - c[3] * y + z[1];
    z[1] = c[2] * x - c[4] * y;
    x = y; i += 1;
  );
  x;
);
// Runs x through one first-order section (coefficients c[0..2], state z[0..1]) and returns the output.
function forun(x, c, z) local(y) ( y = c[0] * x + c[1] * z[0] - c[2] * z[1]; z[0] = x; z[1] = y; y; );
// Writes high-shelf biquad coefficients into cb[0..4] for corner f (Hz) and gain gdb (dB) at the current sample rate.
function shelfdesign(cb, f, gdb) local(shA, w0, cw, sw, al, a0, sa)
(
  shA = 10 ^ (gdb / 40); w0 = 2 * $pi * f / srate; cw = cos(w0); sw = sin(w0);
  al = sw / 2 * sqrt((shA + 1 / shA) * (1 / 0.707 - 1) + 2); sa = 2 * sqrt(shA) * al;
  a0 = (shA + 1) - (shA - 1) * cw + sa;
  cb[0] = shA * ((shA + 1) + (shA - 1) * cw + sa) / a0;
  cb[1] = -2 * shA * ((shA - 1) + (shA + 1) * cw) / a0;
  cb[2] = shA * ((shA + 1) + (shA - 1) * cw - sa) / a0;
  cb[3] = 2 * ((shA - 1) - (shA + 1) * cw) / a0;
  cb[4] = ((shA + 1) - (shA - 1) * cw - sa) / a0;
);
// Runs x through the enabled low-cut then high-cut filter chains (biquads plus optional first-order) with the given states; returns it.
function fchain(x, ls, lf, hs, hf)
(
  lcon ? ( x = bqrun(x, lcC, ls, lcNb); lcNo ? x = forun(x, lcF, lf); );
  hcon ? ( x = bqrun(x, hcC, hs, hcNb); hcNo ? x = forun(x, hcF, hf); );
  x;
);

// ---------- source models ("futz"): color the source before it enters the room ----------
// parameter block pb: 0 HPf 1 HPn 2 LPf 3 LPn 4 nPeaks 5-13 peaks (f, gain dB, Q) x3 14 sat 15 clip 16 crushSR 17 bits 18 hiss 19 gain 20 asymmetry
// Values follow typical measured responses: POTS telephone 300-3400 Hz, walkie-talkie (narrow-band radio) 400-2800 Hz, small speakers roll off
// with 24 dB/oct below their resonance; asymmetry adds the even harmonics of a small loudspeaker driven hard
function fzparams(m, pb)
(
  pb[0] = 20; pb[1] = 0; pb[2] = 20000; pb[3] = 0; pb[4] = 0; pb[14] = 0; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1; pb[20] = 0;
  m == 1 ? ( pb[0] = 300; pb[1] = 6; pb[2] = 3400; pb[3] = 8; pb[4] = 2; pb[5] = 1000; pb[6] = 3; pb[7] = 0.9; pb[8] = 2700; pb[9] = 5; pb[10] = 2; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 2.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.25; pb[20] = 0.1; );
  m == 2 ? ( pb[0] = 300; pb[1] = 8; pb[2] = 3400; pb[3] = 8; pb[4] = 2; pb[5] = 1800; pb[6] = 4; pb[7] = 1; pb[8] = 500; pb[9] = -3; pb[10] = 1.5; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 0; pb[15] = 0; pb[16] = 8000; pb[17] = 8; pb[18] = 0; pb[19] = 1.3; pb[20] = 0; );
  m == 3 ? ( pb[0] = 700; pb[1] = 4; pb[2] = 14000; pb[3] = 2; pb[4] = 3; pb[5] = 3200; pb[6] = 6; pb[7] = 1.2; pb[8] = 1500; pb[9] = 3; pb[10] = 1; pb[11] = 7000; pb[12] = 3; pb[13] = 1; pb[14] = 1.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.2; pb[20] = 0.15; );
  m == 4 ? ( pb[0] = 400; pb[1] = 6; pb[2] = 2800; pb[3] = 8; pb[4] = 2; pb[5] = 1700; pb[6] = 8; pb[7] = 1.5; pb[8] = 800; pb[9] = -2; pb[10] = 1; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 0; pb[15] = 8; pb[16] = 0; pb[17] = 0; pb[18] = 0.012; pb[19] = 1; pb[20] = 0; );
  m == 5 ? ( pb[0] = 180; pb[1] = 4; pb[2] = 4500; pb[3] = 6; pb[4] = 3; pb[5] = 800; pb[6] = 5; pb[7] = 1; pb[8] = 2200; pb[9] = 3; pb[10] = 1.5; pb[11] = 3600; pb[12] = -2; pb[13] = 1; pb[14] = 3.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.2; pb[20] = 0.15; );
  m == 6 ? ( pb[0] = 70; pb[1] = 4; pb[2] = 12000; pb[3] = 2; pb[4] = 3; pb[5] = 110; pb[6] = 6; pb[7] = 1.2; pb[8] = 600; pb[9] = -3; pb[10] = 1; pb[11] = 5000; pb[12] = 3; pb[13] = 1; pb[14] = 2.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.1; pb[20] = 0.2; );
  m == 7 ? ( pb[0] = 180; pb[1] = 4; pb[2] = 10000; pb[3] = 2; pb[4] = 3; pb[5] = 400; pb[6] = 4; pb[7] = 2; pb[8] = 2500; pb[9] = 3; pb[10] = 1.2; pb[11] = 1000; pb[12] = -2; pb[13] = 1.2; pb[14] = 1.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.2; pb[20] = 0.1; );
  m == 8 ? ( pb[0] = 400; pb[1] = 4; pb[2] = 15000; pb[3] = 2; pb[4] = 3; pb[5] = 800; pb[6] = -4; pb[7] = 1; pb[8] = 3800; pb[9] = 5; pb[10] = 1; pb[11] = 1600; pb[12] = -2; pb[13] = 1.5; pb[14] = 1.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.3; pb[20] = 0.15; );
  m == 9 ? ( pb[0] = 300; pb[1] = 4; pb[2] = 15000; pb[3] = 2; pb[4] = 2; pb[5] = 2600; pb[6] = 5; pb[7] = 1; pb[8] = 650; pb[9] = -4; pb[10] = 1; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 1.2; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.2; pb[20] = 0.1; );
  m == 10 ? ( pb[0] = 140; pb[1] = 4; pb[2] = 13000; pb[3] = 2; pb[4] = 3; pb[5] = 170; pb[6] = 4; pb[7] = 1.4; pb[8] = 500; pb[9] = -3; pb[10] = 1; pb[11] = 2500; pb[12] = 3; pb[13] = 1; pb[14] = 2; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.15; pb[20] = 0.2; );
  m == 11 ? ( pb[0] = 1500; pb[1] = 4; pb[2] = 9000; pb[3] = 2; pb[4] = 2; pb[5] = 3500; pb[6] = 6; pb[7] = 1; pb[8] = 7000; pb[9] = 3; pb[10] = 1.5; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 0; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 0.45; pb[20] = 0; );
  m == 12 ? ( pb[0] = 450; pb[1] = 6; pb[2] = 4000; pb[3] = 6; pb[4] = 2; pb[5] = 1300; pb[6] = 9; pb[7] = 2; pb[8] = 2600; pb[9] = 5; pb[10] = 3; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 0; pb[15] = 10; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1; pb[20] = 0; );
);
// Writes peaking-EQ biquad coefficients into cb[0..4] for center f (Hz, capped at 0.45 fs), gain gdb (dB) and Q.
function pkdesign(cb, f, gdb, q) local(pA, w0, cw, al, a0)
(
  pA = 10 ^ (gdb / 40); w0 = 2 * $pi * min(f, 0.45 * srate) / srate; cw = cos(w0); al = sin(w0) / (2 * q);
  a0 = 1 + al / pA;
  cb[0] = (1 + al * pA) / a0; cb[1] = -2 * cw / a0; cb[2] = (1 - al * pA) / a0; cb[3] = -2 * cw / a0; cb[4] = (1 - al / pA) / a0;
);
// coefficient block cb: 0 HP biquads (24) · 24 LP biquads (24) · 48 peaks (15) · 64 / 68 first-order slots
// SOURCE EQ: the model's high-pass, low-pass and three bells can be moved (sliders 47-57 are OFFSETS from the model:
// 0 = the model as designed, so old projects and model changes keep working). Bands a model does not use start flat.
function fzfill(pb, m) local(j, b)                    // model parameters + neutral defaults for unused bands
(
  fzparams(m, pb);
  j = pb[4];
  while ( j < 3 ? ( b = 5 + 3 * j; pb[b] = j == 0 ? 250 : (j == 1 ? 1000 : 4000); pb[b + 1] = 0; pb[b + 2] = 1; ); j += 1; j < 3; );
);
// Applies the Source EQ slider offsets (HP/LP frequency, slopes, three bells) to the model parameter block pb in place.
function fzedit(pb) local(j, b, n, si)
(
  abs(slider47) > 0.0005 ? ( pb[0] = min(20000, max(20, pb[0] * 2 ^ slider47)); pb[1] < 2 ? pb[1] = 2; );
  abs(slider48) > 0.0005 ? ( pb[2] = min(20000, max(20, pb[2] * 2 ^ slider48)); pb[3] < 2 ? pb[3] = 2; );
  slider58 >= 0.5 ? pb[1] = min(8, floor(slider58 + 0.5));      // slopes chosen by hand: order 1..8 = 6..48 dB/oct
  slider59 >= 0.5 ? pb[3] = min(8, floor(slider59 + 0.5));
  n = pb[4]; j = 0;
  loop(3,
    b = 5 + 3 * j; si = 49 + 3 * j;
    (abs(slider(si)) > 0.0005 || abs(slider(si + 1)) > 0.005 || abs(slider(si + 2)) > 0.0005) ? (
      pb[b] = min(20000, max(20, pb[b] * 2 ^ slider(si)));
      pb[b + 1] = min(24, max(-24, pb[b + 1] + slider(si + 1)));
      pb[b + 2] = min(12, max(0.2, pb[b + 2] * 2 ^ slider(si + 2)));
      n = max(n, j + 1);
    );
    j += 1;
  );
  pb[4] = n;
);
// Returns a checksum of the 13 Source EQ sliders (47-59), used to detect when the EQ changed.
function fzkeyf() local(i, k) ( k = 0; i = 47; loop(13, k += slider(i) * (i * 131 + 7); i += 1; ); k; );
function fzanyf() local(i, k) ( k = 0; i = 47; loop(13, abs(slider(i)) > 0.0005 ? k = 1; i += 1; ); k; );   // is the source EQ edited?
// GUI-thread copy of fzkeyf: checksum of the 13 Source EQ sliders (47-59).
function g_fzkeyf() local(i, k) ( k = 0; i = 47; loop(13, k += slider(i) * (i * 131 + 7); i += 1; ); k; );
// Builds the source-model filter set into cb: fills pb for model m, applies EQ edits if ed, designs HP, LP and up to three bells.
function fzdesign(cb, pb, m, ed)
(
  fzfill(pb, m);
  ed ? fzedit(pb);
  pb[1] > 0 ? fdesign(cb, cb + 64, 1, pb[0], pb[1]);
  pb[3] > 0 ? fdesign(cb + 24, cb + 68, 0, pb[2], pb[3]);
  pb[4] >= 1 ? pkdesign(cb + 48, pb[5], pb[6], pb[7]);
  pb[4] >= 2 ? pkdesign(cb + 53, pb[8], pb[9], pb[10]);
  pb[4] >= 3 ? pkdesign(cb + 58, pb[11], pb[12], pb[13]);
);
// GUI-thread copies of the design functions: EEL2 functions keep their parameters and locals in static storage, so the
// audio thread and the GUI must never run the same function at the same time (a shared copy corrupts the coefficients)
function g_fdesign(cb, fb, ishp, f, nord) local(k, q, w0, cw, al, a0, tk)
(
  f = min(f, 0.45 * srate);
  k = 0;
  loop(floor(nord / 2),
    q = (nord % 2) ? 1 / (2 * cos((k + 1) * $pi / nord)) : 1 / (2 * cos((2 * k + 1) * $pi / (2 * nord)));
    w0 = 2 * $pi * f / srate; cw = cos(w0); al = sin(w0) / (2 * q); a0 = 1 + al;
    ishp ? ( cb[k * 5] = (1 + cw) / 2 / a0; cb[k * 5 + 1] = -(1 + cw) / a0; cb[k * 5 + 2] = (1 + cw) / 2 / a0; )
         : ( cb[k * 5] = (1 - cw) / 2 / a0; cb[k * 5 + 1] = (1 - cw) / a0;  cb[k * 5 + 2] = (1 - cw) / 2 / a0; );
    cb[k * 5 + 3] = -2 * cw / a0; cb[k * 5 + 4] = (1 - al) / a0;
    k += 1;
  );
  (nord % 2) ? (
    tk = tan($pi * f / srate);
    ishp ? ( fb[0] = 1 / (1 + tk); fb[1] = -1 / (1 + tk); ) : ( fb[0] = tk / (1 + tk); fb[1] = tk / (1 + tk); );
    fb[2] = (tk - 1) / (tk + 1);
  );
);
// GUI-thread copy: fills parameter block pb with the filter/distortion/gain settings of source model m (0-12).
function g_fzparams(m, pb)
(
  pb[0] = 20; pb[1] = 0; pb[2] = 20000; pb[3] = 0; pb[4] = 0; pb[14] = 0; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1; pb[20] = 0;
  m == 1 ? ( pb[0] = 300; pb[1] = 6; pb[2] = 3400; pb[3] = 8; pb[4] = 2; pb[5] = 1000; pb[6] = 3; pb[7] = 0.9; pb[8] = 2700; pb[9] = 5; pb[10] = 2; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 2.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.25; pb[20] = 0.1; );
  m == 2 ? ( pb[0] = 300; pb[1] = 8; pb[2] = 3400; pb[3] = 8; pb[4] = 2; pb[5] = 1800; pb[6] = 4; pb[7] = 1; pb[8] = 500; pb[9] = -3; pb[10] = 1.5; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 0; pb[15] = 0; pb[16] = 8000; pb[17] = 8; pb[18] = 0; pb[19] = 1.3; pb[20] = 0; );
  m == 3 ? ( pb[0] = 700; pb[1] = 4; pb[2] = 14000; pb[3] = 2; pb[4] = 3; pb[5] = 3200; pb[6] = 6; pb[7] = 1.2; pb[8] = 1500; pb[9] = 3; pb[10] = 1; pb[11] = 7000; pb[12] = 3; pb[13] = 1; pb[14] = 1.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.2; pb[20] = 0.15; );
  m == 4 ? ( pb[0] = 400; pb[1] = 6; pb[2] = 2800; pb[3] = 8; pb[4] = 2; pb[5] = 1700; pb[6] = 8; pb[7] = 1.5; pb[8] = 800; pb[9] = -2; pb[10] = 1; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 0; pb[15] = 8; pb[16] = 0; pb[17] = 0; pb[18] = 0.012; pb[19] = 1; pb[20] = 0; );
  m == 5 ? ( pb[0] = 180; pb[1] = 4; pb[2] = 4500; pb[3] = 6; pb[4] = 3; pb[5] = 800; pb[6] = 5; pb[7] = 1; pb[8] = 2200; pb[9] = 3; pb[10] = 1.5; pb[11] = 3600; pb[12] = -2; pb[13] = 1; pb[14] = 3.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.2; pb[20] = 0.15; );
  m == 6 ? ( pb[0] = 70; pb[1] = 4; pb[2] = 12000; pb[3] = 2; pb[4] = 3; pb[5] = 110; pb[6] = 6; pb[7] = 1.2; pb[8] = 600; pb[9] = -3; pb[10] = 1; pb[11] = 5000; pb[12] = 3; pb[13] = 1; pb[14] = 2.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.1; pb[20] = 0.2; );
  m == 7 ? ( pb[0] = 180; pb[1] = 4; pb[2] = 10000; pb[3] = 2; pb[4] = 3; pb[5] = 400; pb[6] = 4; pb[7] = 2; pb[8] = 2500; pb[9] = 3; pb[10] = 1.2; pb[11] = 1000; pb[12] = -2; pb[13] = 1.2; pb[14] = 1.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.2; pb[20] = 0.1; );
  m == 8 ? ( pb[0] = 400; pb[1] = 4; pb[2] = 15000; pb[3] = 2; pb[4] = 3; pb[5] = 800; pb[6] = -4; pb[7] = 1; pb[8] = 3800; pb[9] = 5; pb[10] = 1; pb[11] = 1600; pb[12] = -2; pb[13] = 1.5; pb[14] = 1.5; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.3; pb[20] = 0.15; );
  m == 9 ? ( pb[0] = 300; pb[1] = 4; pb[2] = 15000; pb[3] = 2; pb[4] = 2; pb[5] = 2600; pb[6] = 5; pb[7] = 1; pb[8] = 650; pb[9] = -4; pb[10] = 1; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 1.2; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.2; pb[20] = 0.1; );
  m == 10 ? ( pb[0] = 140; pb[1] = 4; pb[2] = 13000; pb[3] = 2; pb[4] = 3; pb[5] = 170; pb[6] = 4; pb[7] = 1.4; pb[8] = 500; pb[9] = -3; pb[10] = 1; pb[11] = 2500; pb[12] = 3; pb[13] = 1; pb[14] = 2; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1.15; pb[20] = 0.2; );
  m == 11 ? ( pb[0] = 1500; pb[1] = 4; pb[2] = 9000; pb[3] = 2; pb[4] = 2; pb[5] = 3500; pb[6] = 6; pb[7] = 1; pb[8] = 7000; pb[9] = 3; pb[10] = 1.5; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 0; pb[15] = 0; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 0.45; pb[20] = 0; );
  m == 12 ? ( pb[0] = 450; pb[1] = 6; pb[2] = 4000; pb[3] = 6; pb[4] = 2; pb[5] = 1300; pb[6] = 9; pb[7] = 2; pb[8] = 2600; pb[9] = 5; pb[10] = 3; pb[11] = 0; pb[12] = 0; pb[13] = 1; pb[14] = 0; pb[15] = 10; pb[16] = 0; pb[17] = 0; pb[18] = 0; pb[19] = 1; pb[20] = 0; );
);
// GUI-thread copy of pkdesign: peaking-EQ biquad coefficients into cb[0..4] for f (Hz), gdb (dB), q.
function g_pkdesign(cb, f, gdb, q) local(pA, w0, cw, al, a0)
(
  pA = 10 ^ (gdb / 40); w0 = 2 * $pi * min(f, 0.45 * srate) / srate; cw = cos(w0); al = sin(w0) / (2 * q);
  a0 = 1 + al / pA;
  cb[0] = (1 + al * pA) / a0; cb[1] = -2 * cw / a0; cb[2] = (1 - al * pA) / a0; cb[3] = -2 * cw / a0; cb[4] = (1 - al / pA) / a0;
);
function g_fzfill(pb, m) local(j, b)                    // model parameters + neutral defaults for unused bands
(
  g_fzparams(m, pb);
  j = pb[4];
  while ( j < 3 ? ( b = 5 + 3 * j; pb[b] = j == 0 ? 250 : (j == 1 ? 1000 : 4000); pb[b + 1] = 0; pb[b + 2] = 1; ); j += 1; j < 3; );
);
// GUI-thread copy of fzedit: applies the Source EQ slider offsets to parameter block pb in place.
function g_fzedit(pb) local(j, b, n, si)
(
  abs(slider47) > 0.0005 ? ( pb[0] = min(20000, max(20, pb[0] * 2 ^ slider47)); pb[1] < 2 ? pb[1] = 2; );
  abs(slider48) > 0.0005 ? ( pb[2] = min(20000, max(20, pb[2] * 2 ^ slider48)); pb[3] < 2 ? pb[3] = 2; );
  slider58 >= 0.5 ? pb[1] = min(8, floor(slider58 + 0.5));      // slopes chosen by hand: order 1..8 = 6..48 dB/oct
  slider59 >= 0.5 ? pb[3] = min(8, floor(slider59 + 0.5));
  n = pb[4]; j = 0;
  loop(3,
    b = 5 + 3 * j; si = 49 + 3 * j;
    (abs(slider(si)) > 0.0005 || abs(slider(si + 1)) > 0.005 || abs(slider(si + 2)) > 0.0005) ? (
      pb[b] = min(20000, max(20, pb[b] * 2 ^ slider(si)));
      pb[b + 1] = min(24, max(-24, pb[b + 1] + slider(si + 1)));
      pb[b + 2] = min(12, max(0.2, pb[b + 2] * 2 ^ slider(si + 2)));
      n = max(n, j + 1);
    );
    j += 1;
  );
  pb[4] = n;
);
// GUI-thread copy of fzdesign: builds HP, LP and bell coefficients into cb for model m (ed = apply EQ edits) using the g_ helpers.
function g_fzdesign(cb, pb, m, ed)
(
  g_fzfill(pb, m);
  ed ? g_fzedit(pb);
  pb[1] > 0 ? g_fdesign(cb, cb + 64, 1, pb[0], pb[1]);
  pb[3] > 0 ? g_fdesign(cb + 24, cb + 68, 0, pb[2], pb[3]);
  pb[4] >= 1 ? g_pkdesign(cb + 48, pb[5], pb[6], pb[7]);
  pb[4] >= 2 ? g_pkdesign(cb + 53, pb[8], pb[9], pb[10]);
  pb[4] >= 3 ? g_pkdesign(cb + 58, pb[11], pb[12], pb[13]);
);
// signal order as in a real device: input high-pass -> (codec) -> amplifier distortion -> loudspeaker resonances and low-pass.
// The distortion products are shaped by the speaker response instead of passing unfiltered.
// audio-thread copies of the magnitude helpers (the GUI has its own): a_bqmag2 = squared magnitude of one biquad (c[0..4]) at angular frequency w
// (a_fzlin below uses them: mean power gain of the source filter for speech, 16 points from 200 Hz to 5 kHz, equal weight per octave)
function a_bqmag2(c, w) local(nr, ni, dr, di)
(
  nr = c[0] + c[1] * cos(w) + c[2] * cos(2 * w); ni = -(c[1] * sin(w) + c[2] * sin(2 * w));
  dr = 1 + c[3] * cos(w) + c[4] * cos(2 * w);     di = -(c[3] * sin(w) + c[4] * sin(2 * w));
  (nr * nr + ni * ni) / max(0.0000000001, dr * dr + di * di);
);
// Audio-thread copy: squared magnitude of a first-order section (coefficients c[0..2]) at angular frequency w.
function a_fomag2(c, w) ( (sqr(c[0] + c[1] * cos(w)) + sqr(c[1] * sin(w))) / max(0.0000000001, sqr(1 + c[2] * cos(w)) + sqr(c[2] * sin(w))); );
// Returns the mean power gain of the current source filter (fzb/fzP) over 16 points, 200 Hz to 5 kHz, for equal-loudness compensation.
function a_fzlin() local(i, j, w, m2, acc)
(
  acc = 0; i = 0;
  loop(16,
    w = 2 * $pi * min(200 * 25 ^ (i / 15), 0.49 * srate) / srate; m2 = 1;
    j = 0; loop(floor(fzP[1] / 2), m2 *= a_bqmag2(fzb + j * 5, w); j += 1; );
    (fzP[1] % 2) ? m2 *= a_fomag2(fzb + 64, w);
    j = 0; loop(floor(fzP[3] / 2), m2 *= a_bqmag2(fzb + 24 + j * 5, w); j += 1; );
    (fzP[3] % 2) ? m2 *= a_fomag2(fzb + 68, w);
    j = 0; loop(fzP[4], m2 *= a_bqmag2(fzb + 48 + j * 5, w); j += 1; );
    acc += m2; i += 1;
  );
  acc / 16;
);

// the highest power gain of the model's filter (120 points 60 Hz … 12 kHz, 1/15 octave) - the delay loop through the device is
// scaled by it, so a resonant model never makes the repeats grow
function a_fzpk() local(i, j, w, m2, mx)
(
  mx = 0.000001; i = 0;
  loop(120,
    w = 2 * $pi * min(60 * 200 ^ (i / 119), 0.49 * srate) / srate; m2 = 1;
    j = 0; loop(floor(fzP[1] / 2), m2 *= a_bqmag2(fzb + j * 5, w); j += 1; );
    (fzP[1] % 2) ? m2 *= a_fomag2(fzb + 64, w);
    j = 0; loop(floor(fzP[3] / 2), m2 *= a_bqmag2(fzb + 24 + j * 5, w); j += 1; );
    (fzP[3] % 2) ? m2 *= a_fomag2(fzb + 68, w);
    j = 0; loop(fzP[4], m2 *= a_bqmag2(fzb + 48 + j * 5, w); j += 1; );
    mx = max(mx, m2); i += 1;
  );
  mx;
);
// ---- ROOM MODES (small rooms, low frequencies) ----
// Below the Schroeder frequency fS = 2000 sqrt(T / V) a room does not answer with a smooth diffuse tail but with single
// resonances - its modes f = c/2 sqrt((nx/Lx)^2 + (ny/Ly)^2 + (nz/Lz)^2). How strongly each one sounds depends on where
// source and listener stand (cos shapes: loud near walls and corners, silent in the nodes). The late tail's bass between
// the lowest mode and fS (max 300 Hz) is replaced by the sum of these modes (resonators, bandwidth from the bass RT60),
// normalized to the same average energy - so the room keeps its level, but gets its booming and missing notes.
// Rooms with a barrier: the modes of the listener's part; a source on the other side excites them at the opening.
function mdcalc() local(qWx, qDy, qHz, y0, mVol, mTb, fc, nx, ny, nz, mx, my, mz, f, k, n, j, w, mps, pl, e, msxe, msye, msze, lye, re, im, d, q, fq, sum, best, bi, t0, mr, th, mdBw, fmin, fA, g)
(
  mdPN = 0;
  !isfree ? (
    qWx = roomW; qHz = roomH;
    mdbon ? ( qDy = sideL ? roomD - yb : yb; y0 = sideL ? yb : 0; ) : ( qDy = roomD; y0 = 0; );   // (barrier mix below 50 %: the whole room)
    qDy = max(0.5, qDy);
    mVol = qWx * qDy * qHz; mTb = min(20, max(0.05, rt * lfrP));
    fc = min(300, 2000 * sqrt(mTb / mVol));
    (!mdbon || bsame) ? ( msxe = sx; msye = sy - y0; msze = sz; ) : (
      brOA > 0.0001 ? ( msxe = brOX; msze = brOZ; )
                     : ( t0 = (yb - sy) / (ly - sy); msxe = sx + (lx - sx) * t0; msze = sz + (lz - sz) * t0; );
      msye = sideL ? 0.05 : qDy - 0.05;
    );
    lye = ly - y0;
    mx = min(40, floor(2 * qWx * fc / C_SND)); my = min(40, floor(2 * qDy * fc / C_SND)); mz = min(40, floor(2 * qHz * fc / C_SND));
    n = 0; nx = 0;
    loop(mx + 1,
      ny = 0;
      loop(my + 1,
        nz = 0;
        loop(mz + 1,
          nx + ny + nz > 0 && n < 300 ? (
            f = 0.5 * C_SND * sqrt(sqr(nx / qWx) + sqr(ny / qDy) + sqr(nz / qHz));
            f <= fc ? ( mdT[n * 4] = f; mdT[n * 4 + 1] = nx; mdT[n * 4 + 2] = ny; mdT[n * 4 + 3] = nz; n += 1; );
          );
          nz += 1;
        );
        ny += 1;
      );
      nx += 1;
    );
    // keep the lowest 96 (selection)
    k = 0;
    loop(min(96, n),
      best = mdT[k * 4]; bi = k; j = k + 1;
      loop(n - k - 1, mdT[j * 4] < best ? ( best = mdT[j * 4]; bi = j; ); j += 1; );
      bi != k ? ( j = 0; loop(4, t0 = mdT[k * 4 + j]; mdT[k * 4 + j] = mdT[bi * 4 + j]; mdT[bi * 4 + j] = t0; j += 1; ); );
      k += 1;
    );
    n = min(96, n);
    n >= 2 ? (
      mdBw = 2.2 / mTb;                                       // modal bandwidth: delta / pi, delta = 6.91 / T
      fmin = mdT[0]; fA = 0.85 * fmin;
      fc > 1.3 * fA ? (
        k = 0;
        loop(n,
          f = mdT[k * 4]; nx = mdT[k * 4 + 1]; ny = mdT[k * 4 + 2]; nz = mdT[k * 4 + 3]; mdPi[k] = nx * 4096 + ny * 64 + nz;
          mps = cos(nx * $pi * msxe / qWx) * cos(ny * $pi * msye / qDy) * cos(nz * $pi * msze / qHz);
          pl = cos(nx * $pi * lx / qWx) * cos(ny * $pi * lye / qDy) * cos(nz * $pi * lz / qHz);
          e = (nx > 0 ? 2 : 1) * (ny > 0 ? 2 : 1) * (nz > 0 ? 2 : 1);
          mdT[k * 4 + 1] = mps * pl * e / f;                 // weight (the indices are no longer needed)
          k += 1;
        );
        // normalize: mean |sum of modes|^2 over the band fA … fc = 1 (64 points, log spaced)
        sum = 0; j = 0;
        loop(64,
          fq = fA * (fc / fA) ^ ((j + 0.5) / 64);
          re = im = 0; k = 0;
          loop(n, f = mdT[k * 4]; q = f / mdBw; d = q * (fq / f - f / fq); w = mdT[k * 4 + 1] / (1 + d * d); re += w; im -= w * d; k += 1; );
          sum += re * re + im * im; j += 1;
        );
        g = sum > 0.0000000001 ? 1 / sqrt(sum / 64) : 0;
        k = 0;
        // one resonator per mode: 2-pole coefficients (a1 = -2 r cos(theta), a2 = r^2), input gain 0.5 (1 - r^2) * mode weight * normalization g
        loop(n,
          f = mdT[k * 4]; mr = exp(-$pi * mdBw / srate); th = 2 * $pi * f / srate;
          mdP[k * 3] = -2 * mr * cos(th); mdP[k * 3 + 1] = mr * mr; mdP[k * 3 + 2] = 0.5 * (1 - mr * mr) * mdT[k * 4 + 1] * g; mdPf[k] = f;
          k += 1;
        );
        mdPN = n; mdPc = fc; mdPa = fA;
      );
    );
  );
);

// apply the modes from mdcalc: the same set (source / listener moved, decay changed) -> new weights, glided per sample;
// a new set (other room) -> fade the modes out, swap, fade in (in @block: mdswap) - never a jump of a ringing resonator
function mdapply() local(k, j, n, id, fd, nm, ok)
(
  // MODES FOLLOW THE ROOM: the new set is matched to the playing one mode by mode (nx ny nz). If the room moved only a little
  // (every mode that stays within 4 %, at least half of them stay) they GLIDE: a mode that stays keeps ringing with its new
  // frequency, damping and weight; a new one fades in from 0; one that left rings out (weight 0) while it is still heard.
  // Automating the room, a position or the barrier keeps the modes playing. A big change (preset, snapshot): fade out,
  // swap, fade in (mdswap).
  ok = mdPN > 0 && mdN > 0 && !mdPend; nm = 0;
  ok ? (
    memset(mdUs, 0, 128);
    k = 0;
    loop(mdPN,
      id = mdPi[k]; j = 0; fd = -1;
      while ( j < mdN && fd < 0 ? ( mdI[j] == id ? fd = j; j += 1; 1; ) : 0; );
      mdQi[k] = fd;
      fd >= 0 ? ( mdUs[fd] = 1; nm += 1; abs(mdPf[k] - mdF[fd]) > 0.04 * mdF[fd] ? ok = 0; );
      k += 1;
    );
    nm < 0.5 * mdPN ? ok = 0;
  );
  ok ? (
    n = 0; k = 0;
    loop(mdPN,
      fd = mdQi[k];
      mdQ[n * 6] = mdP[k * 3]; mdQ[n * 6 + 1] = mdP[k * 3 + 1]; mdQ[n * 6 + 5] = mdP[k * 3 + 2];
      fd >= 0 ? ( mdQ[n * 6 + 2] = mdM[fd * 6 + 2]; mdQ[n * 6 + 3] = mdM[fd * 6 + 3]; mdQ[n * 6 + 4] = mdM[fd * 6 + 4]; mdQc0[n] = mdC0[fd]; mdQc1[n] = mdC1[fd]; )
              : ( mdQ[n * 6 + 2] = 0; mdQ[n * 6 + 3] = 0; mdQ[n * 6 + 4] = 0; mdQc0[n] = mdP[k * 3]; mdQc1[n] = mdP[k * 3 + 1]; );   // (a new mode starts on its own coefficients)
      mdQf[n] = mdPf[k]; mdQi[n] = mdPi[k]; n += 1; k += 1;
    );
    j = 0;
    loop(mdN,
      !mdUs[j] && n < 128 && (abs(mdM[j * 6 + 2]) > 0.000001 || abs(mdM[j * 6 + 3]) > 0.000001) ? (
        mdQ[n * 6] = mdM[j * 6]; mdQ[n * 6 + 1] = mdM[j * 6 + 1]; mdQ[n * 6 + 2] = mdM[j * 6 + 2];
        mdQ[n * 6 + 3] = mdM[j * 6 + 3]; mdQ[n * 6 + 4] = mdM[j * 6 + 4]; mdQ[n * 6 + 5] = 0; mdQc0[n] = mdC0[j]; mdQc1[n] = mdC1[j];
        mdQf[n] = mdF[j]; mdQi[n] = mdI[j]; n += 1;
      );
      j += 1;
    );
    memcpy(mdM, mdQ, n * 6); memcpy(mdF, mdQf, n); memcpy(mdI, mdQi, n); mdN = n; memcpy(mdC0, mdQc0, n); memcpy(mdC1, mdQc1, n);
    // crossovers of the mode band follow (coefficients only - the filter states run on)
    abs(mdPc - mdFc) > 0.002 * mdFc ? ( fdesign(mdX, mdX + 50, 0, mdPc, 2); memcpy(mdX + 5, mdX, 5); fdesign(mdX + 30, mdX + 50, 1, mdPc, 2); memcpy(mdX + 35, mdX + 30, 5); mdFc = mdPc; );
    abs(mdPa - mdFa) > 0.002 * mdFa ? ( fdesign(mdX + 10, mdX + 50, 0, mdPa, 2); memcpy(mdX + 15, mdX + 10, 5); fdesign(mdX + 40, mdX + 50, 1, mdPa, 2); memcpy(mdX + 45, mdX + 40, 5); mdFa = mdPa; );
    mdPend = 0;
  ) : ( mdPN > 0 || mdN > 0 ? mdPend = 1; );
);

// Commits the pending room-mode tables (mdP*) to the active ones (mdM / mdF / mdI), redesigns the LR4 crossovers at mdPc / mdPa, and resets their states.
function mdswap() local(k)
(
  k = 0; loop(mdPN, mdM[k * 6] = mdP[k * 3]; mdM[k * 6 + 1] = mdP[k * 3 + 1]; mdM[k * 6 + 2] = mdM[k * 6 + 5] = mdP[k * 3 + 2]; mdM[k * 6 + 3] = mdM[k * 6 + 4] = 0; mdF[k] = mdPf[k]; mdI[k] = mdPi[k]; k += 1; );
  mdPN > 0 ? (
    fdesign(mdX, mdX + 50, 0, mdPc, 2); memcpy(mdX + 5, mdX, 5);           // LR4 low-pass fc
    fdesign(mdX + 30, mdX + 50, 1, mdPc, 2); memcpy(mdX + 35, mdX + 30, 5);  // LR4 high-pass fc
    fdesign(mdX + 10, mdX + 50, 0, mdPa, 2); memcpy(mdX + 15, mdX + 10, 5);  // LR4 low-pass fA
    fdesign(mdX + 40, mdX + 50, 1, mdPa, 2); memcpy(mdX + 45, mdX + 40, 5);  // LR4 high-pass fA
    memset(mdXS, 0, 24); mdx1 = mdx2 = 0;
  );
  mdN = mdPN; mdFc = mdPc; mdFa = mdPa; mdPend = 0;
  k = 0; loop(mdN, mdC0[k] = mdM[k * 6]; mdC1[k] = mdM[k * 6 + 1]; k += 1; ); mdcmov = 0;
);
// ---- 4x OVERSAMPLING for the distortion of the source models ----
// the nonlinear stage (runs at 4x): tanh saturation with a small bias (even harmonics) and hard clip, both with
// first-order anti-derivative anti-aliasing (ADAA) on top
function fznl(x) local(y, du, fzL0)
(
  fzSat > 0 ? (
    y = fzSat * fzPush * x + fzAsym; du = y - fzU1; fzL0 = lcosh(y);   // lcosh of the previous sample is kept (fzL1) - half the log / exp calls, the same numbers
    x = ((abs(du) > 0.00001 ? (fzL0 - fzL1) / du : thf(0.5 * (y + fzU1))) - fzAsT) / fzSatN;
    fzU1 = y; fzL1 = fzL0;
  );
  fzClip > 0 ? (
    y = fzClip * fzPush * x; du = y - fzU2;
    x = (abs(du) > 0.00001 ? (clipF(y) - clipF(fzU2)) / du : min(1, max(-1, 0.5 * (y + fzU2)))) / fzClipN;
    fzU2 = y;
  );
  x;
);
// the same filters written out (no index arithmetic, the zero-stuffed inputs skip their multiplications) - same numbers, less CPU
function osup(x) local(y) ( y = oc0 * x + su0; su0 = oc1 * x - oc3 * y + su1; su1 = oc2 * x - oc4 * y; x = y;
  y = oc5 * x + su2; su2 = oc6 * x - oc8 * y + su3; su3 = oc7 * x - oc9 * y; x = y;
  y = oc10 * x + su4; su4 = oc11 * x - oc13 * y + su5; su5 = oc12 * x - oc14 * y; x = y;
  y = oc15 * x + su6; su6 = oc16 * x - oc18 * y + su7; su7 = oc17 * x - oc19 * y; x = y;
  y = oc20 * x + su8; su8 = oc21 * x - oc23 * y + su9; su9 = oc22 * x - oc24 * y; x = y;
  x; );
// One step of the 4x upsampler's elliptic low-pass fed with a zero (stuffed) sample; returns the filtered output.
function osup0() local(x, y) ( y = su0; su0 = su1 - oc3 * y; su1 = -oc4 * y; x = y;
  y = oc5 * x + su2; su2 = oc6 * x - oc8 * y + su3; su3 = oc7 * x - oc9 * y; x = y;
  y = oc10 * x + su4; su4 = oc11 * x - oc13 * y + su5; su5 = oc12 * x - oc14 * y; x = y;
  y = oc15 * x + su6; su6 = oc16 * x - oc18 * y + su7; su7 = oc17 * x - oc19 * y; x = y;
  y = oc20 * x + su8; su8 = oc21 * x - oc23 * y + su9; su9 = oc22 * x - oc24 * y; x = y;
  x; );
// One step of the 10th-order elliptic anti-alias low-pass used before decimating the 4x signal; returns the filtered sample.
function osdn(x) local(y) ( y = oc0 * x + sd0; sd0 = oc1 * x - oc3 * y + sd1; sd1 = oc2 * x - oc4 * y; x = y;
  y = oc5 * x + sd2; sd2 = oc6 * x - oc8 * y + sd3; sd3 = oc7 * x - oc9 * y; x = y;
  y = oc10 * x + sd4; sd4 = oc11 * x - oc13 * y + sd5; sd5 = oc12 * x - oc14 * y; x = y;
  y = oc15 * x + sd6; sd6 = oc16 * x - oc18 * y + sd7; sd7 = oc17 * x - oc19 * y; x = y;
  y = oc20 * x + sd8; sd8 = oc21 * x - oc23 * y + sd9; sd9 = oc22 * x - oc24 * y; x = y;
  x; );
// Runs the source-model nonlinearity (fznl) at 4x oversampling: upsample x, distort, low-pass, keep every 4th sample; returns it.
function fzos(x) local(d)
(
  d = osdn(fznl(osup(4 * x)));       // zero-stuffing + low-pass = 4x upsampling; distortion at 4x; low-pass, keep every 4th sample
  d = osdn(fznl(osup0()));
  d = osdn(fznl(osup0()));
  d = osdn(fznl(osup0()));
  d;
);
// ---- CALL LINE: what a call does to a voice before the device plays it (devices only; Original stays the plain input)
// order as in a real call: talker's phone (AGC + half-duplex switch) -> network (band limit, codec, lost packets) -> the
// device's speaker (the model: its filters, saturation, clipping)
// DUPLEX (slider 111): a speakerphone / echo suppressor lets only one direction talk at a time and its AGC levels the voice.
// Heard as: word onsets clipped (the switch opens late), tails chopped (it closes fast after a short hold), the level
// pumps after loud words, now and then a dip in the middle of a phrase (a loud syllable taken for echo), and comfort
// noise in the gaps (fades out 2-3 s after the voice). All deterministic (own noise generator, reset at playback start).
function fzduck(x) local(t, g)
(
  dqF = max(abs(x), dqF * dqRF);                       // fast envelope (30 ms release)
  dqS += (x * x - dqS) * dqKS;                         // mean square, 50 ms
  dqPk = max(dqF, dqPk * dqRP);                        // recent peak (2.5 s release): the reference of the detector
  t = max(0.002, dqPk * 0.04);                         // voice present: within 28 dB of the recent peak, above -54 dBFS
  dqF > t ? ( dqAct = 1; dqHold = dqHN; ) : ( dqHold > 0 ? dqHold -= 1 : dqAct = 0; );
  // AGC: towards -23 dBFS RMS (at most +12 dB) while the voice is present (fast down 12 ms, slow up 0.5 s: it pumps after loud words)
  dqAct ? ( g = min(4, max(0.25, 0.07 / sqrt(max(0.00000001, dqS)))); dqAg += (g - dqAg) * (g < dqAg ? dqKA : dqKR); );
  // the switch: opens slowly at an onset, closes fast after the hold; a misfire dips it for 45 ms (at most every 0.35 s)
  dqCool > 0 ? dqCool -= 1;
  dqDip <= 0 && dqCool <= 0 && dqAct && dqF > 4 * sqrt(dqS) && dqF > 3 * t ? (
    dqRs = (dqRs * 16807) % 2147483647; dqRs / 2147483647 < 0.35 * fzDk ? ( dqDip = dqDN; dqCool = dqCN; ); );   // the cooldown only after a real dip (it started on every candidate - fewer dips than meant)
  dqDip > 0 ? ( dqDip -= 1; dqGg += (dqFl - dqGg) * dqKC; ) : ( dqAct ? dqGg += (1 - dqGg) * dqKO : dqGg += (dqFl - dqGg) * dqKC; );
  x *= (1 + (dqAg - 1) * fzDk) * dqGg;
  dqRs = (dqRs * 16807) % 2147483647;
  x + (dqRs / 1073741823.5 - 1) * 0.0004 * fzDk * (1 - dqGg) * min(1, dqPk * 40);
);
// the linear part of the source model for the delay loop (same order as fzside, own states st: 40)
function dvlin(x, st) (
  fzHPb > 0 ? x = bqrun(x, fzb, st, fzHPb);
  fzHPo ? x = forun(x, fzb + 64, st + 32);
  fzNP > 0 ? x = bqrun(x, fzb + 48, st + 16, fzNP);
  fzLPb > 0 ? x = bqrun(x, fzb + 24, st + 8, fzLPb);
  fzLPo ? x = forun(x, fzb + 68, st + 34);
  x;
);
// CODEC + PACKET LOSS (sliders 112, 113): one STFT frame every hop. Lost packets (Gilbert model: losses come in bursts)
// are concealed as codecs do - the last spectrum is repeated with running phases (the robotic, frozen vowel of a bad
// line), fading after 40 ms, silent after 60 ms. Codec artifacts: weak bins drop out (musical noise), levels are coarsely
// quantized, whole bands above 1 kHz go unfunded per packet (swirling, watery highs) and the top of the band loses its
// phase (hoarse noise fill).
function qlframe() local(i, j, k, re, im, m2, mx, thr, sq, g, rp, L, b, ph, kh, pl, pb, pg, tg)
(
  i = 0; j = qlPos;
  loop(QLN, qlF[2 * i] = qlIn[j] * qlW[i]; qlF[2 * i + 1] = 0; j += 1; j >= QLN ? j = 0; i += 1; );
  fft(qlF, QLN); fft_permute(qlF, QLN);
  qlPC -= 1;
  qlPC <= 0 ? (
    qlPC = qlPF;
    // packet clock: lost or not (mean loss up to 40 %, mean burst 1 … 5 packets)
    fzDO > 0 ? (
      pl = 0.4 * fzDO; pb = 1 / (1 + 4 * fzDO); pg = pl * pb / (1 - pl);
      qlRs = (qlRs * 16807) % 2147483647;
      qlBad ? ( qlRs / 2147483647 < pb ? qlBad = 0; ) : ( qlRs / 2147483647 < pg ? qlBad = 1; );
    ) : ( qlBad = 0; );
    qlBad ? qlLN += 1 : qlLN = 0;
    // bit allocation of this packet: which bands above 1 kHz get no bits (more often the higher they are)
    k = 0; loop(24, qlRs = (qlRs * 16807) % 2147483647; qlBM[k] = qlRs / 2147483647 >= fzCA * (0.15 + 0.5 * k / 15); k += 1; );
  );
  qlBad ? (
    tg = qlLN <= 1 ? 1 : (qlLN == 2 ? 0.5 : (qlLN == 3 ? 0.15 : 0)); qlPG += (tg - qlPG) * 0.4; qlLost += 1 / qlPF;
    k = 0;
    loop(QLN / 2 + 1,
      qlRs = (qlRs * 16807) % 2147483647;
      qlPh[k] += k * 2 * $pi * QLH / QLN + (qlRs / 2147483647 - 0.5) * 0.8;
      m2 = qlMg[k] * qlPG; qlF[2 * k] = m2 * cos(qlPh[k]); qlF[2 * k + 1] = m2 * sin(qlPh[k]);
      k += 1;
    );
  ) : (
    qlPG = 1;
    fzCA > 0 ? (
      // what gets bits: a bin well below the mean of its third octave (the envelope) is dropped - from 26 dB below at
      // 0 % to 6 dB below at 100 % (more and more holes: musical noise) - and anything 100 … 50 dB under the loudest bin
      mx = 0; memset(qlEP, 0, 72); k = 1;
      loop(QLN / 2, m2 = sqr(qlF[2 * k]) + sqr(qlF[2 * k + 1]); mx = max(mx, m2); b = qlEb[k]; qlEP[2 * b] += m2; qlEP[2 * b + 1] += 1; k += 1; );
      thr = mx * 10 ^ (-(100 - 50 * fzCA) / 10); pl = 10 ^ (-(26 - 20 * fzCA) / 10);
      sq = (0.5 + 5.5 * fzCA) * 0.1151292546;            // level step in dB, as a natural-log step of the magnitude
      kh = floor(qlHF * QLN / srate);
      k = 1;
      loop(QLN / 2,
        re = qlF[2 * k]; im = qlF[2 * k + 1]; m2 = re * re + im * im;
        b = qlEb[k];
        m2 < thr || m2 < qlEP[2 * b] / max(1, qlEP[2 * b + 1]) * pl ? ( qlF[2 * k] = qlF[2 * k + 1] = 0; ) : (
          L = 0.5 * log(m2); g = exp(floor(L / sq + 0.5) * sq - L); rp = 0;
          b = qlBd[k]; b >= 0 && !qlBM[b] ? ( g *= 0.18; rp = 1; );
          k >= kh ? ( qlRs = (qlRs * 16807) % 2147483647; qlRs / 2147483647 < 0.6 * fzCA ? rp = 1; );
          rp ? ( qlRs = (qlRs * 16807) % 2147483647; ph = 2 * $pi * qlRs / 2147483647; m2 = sqrt(m2) * g;
                 qlF[2 * k] = m2 * cos(ph); qlF[2 * k + 1] = m2 * sin(ph); )
             : ( qlF[2 * k] = re * g; qlF[2 * k + 1] = im * g; );
        );
        k += 1;
      );
    );
    // keep the frame for the concealment
    k = 0; loop(QLN / 2 + 1, re = qlF[2 * k]; im = qlF[2 * k + 1]; qlMg[k] = sqrt(re * re + im * im); qlPh[k] = atan2(im, re); k += 1; );
  );
  // real signal: bin N - k = conjugate of bin k
  qlF[1] = 0; qlF[QLN + 1] = 0;
  k = 1; loop(QLN / 2 - 1, qlF[2 * (QLN - k)] = qlF[2 * k]; qlF[2 * (QLN - k) + 1] = -qlF[2 * k + 1]; k += 1; );
  fft_ipermute(qlF, QLN); ifft(qlF, QLN);
  i = 0; j = qlPos;
  loop(QLN, qlOut[j] += qlF[2 * i] * qlW[i] * qlSc; j += 1; j >= QLN ? j = 0; i += 1; );
);
// one sample through the STFT stage (latency: one frame)
function qlstep(x) local(y)
(
  y = qlOut[qlPos]; qlOut[qlPos] = 0; qlIn[qlPos] = x;
  qlPos += 1; qlPos >= QLN ? qlPos = 0;
  qlCnt += 1; qlCnt >= QLH ? ( qlCnt = 0; qlframe(); );
  y;
);
// Runs one sample through the source model: duck, hiss, HP, codec crush, band limit/packet loss, 4x saturation/clip, bells, LP, then output gain.
function fzrun(x) local(y, du)
(
  fzDk > 0 || dqGg < 0.9999 ? x = fzduck(x);
  fzHiss > 0 ? (                                     // squelch: noise only while the source is active
    fzenv = max(abs(x), fzenv * fzrel);
    fzgate += ((fzenv > 0.004) - fzgate) * 0.002;
    fzrs = (fzrs * 16807) % 2147483647; x += (fzrs / 1073741823.5 - 1) * fzHiss * fzgate;   // own noise generator: the same hiss in every render
  );
  fzHPb > 0 ? x = bqrun(x, fzb, fzS, fzHPb);
  fzHPo ? x = forun(x, fzb + 64, fzS1);
  fzCrSR > 0 ? (                                     // codec: band limit first (no aliasing), then sample-and-hold + quantization
    fzLPb > 0 ? x = bqrun(x, fzb + 24, fzS2, fzLPb);
    fzLPo ? x = forun(x, fzb + 68, fzS1 + 4);
    fzacc += fzCrSR / srate;
    // quantized in the companded domain (mu-law, as telephone codecs do): quiet signals keep their resolution instead of
    // falling silent below the first step; fewer bits = more noise that follows the level
    fzacc >= 1 ? (
      fzacc -= 1;
      y = floor(log(1 + 255 * min(1, abs(x))) * 0.180336880 * fzq + 0.5) / fzq;
      fzhold = (x < 0 ? -1 : 1) * (exp(y * 5.545177444) - 1) * 0.003921569;
    );
    x = fzhold;
  );
  fzBW > 0 ? x = bqrun(x, qlC, qlS, 6);   // the network's band (HP 4th + LP 8th order)
  qlOn ? x = qlstep(x);   // codec artifacts / lost packets
  // saturation and hard clip run 4x oversampled (elliptic anti-alias filters, see fzos) - their harmonics above the
  // audio band no longer fold back as inharmonic tones
  fzSat > 0 || fzClip > 0 ? (
    x = fzos(x);
    fzSat > 0 && fzAsym > 0 ? ( y = x - fzDx + fzDR * fzDy; fzDx = x; fzDy = y; x = y; );   // the bias rectifies: remove the DC it creates (20 Hz)
  );
  fzNP > 0 ? x = bqrun(x, fzb + 48, fzS + 16, fzNP);
  fzLPb > 0 ? x = bqrun(x, fzb + 24, fzS + 8, fzLPb);
  fzLPo ? x = forun(x, fzb + 68, fzS1 + 2);
  x * fzOutG;
);
// MIX WITHOUT CANCELLATION: the dry part of a Mix is not the plain input but AP = P(x) + c Q(x) - the filter P and its
// complement Q (same poles, the other numerators; c = (-1)^biquads keeps them adding). Odd orders: an exact all-pass;
// even orders: at most +3 dB right at the corner. Its phase follows P where P passes, so filtered and dry part no longer
// cancel (a plain blend of a steep filter with the input notched up to -85 dB in the pass band)
function apstage(x, cP, apf1, cQ, apf2, nb, no, st) local(p, q)
(
  p = bqrun(x, cP, st, nb); no ? p = forun(p, apf1, st + 8);
  q = bqrun(x, cQ, st + 10, nb); no ? q = forun(q, apf2, st + 18);
  (nb & 1) ? p - q : p + q;
);
// Filter module: the dry path (low cut, then high cut - as fchain), per channel (st: 40 states)
function fapath(x, st) (
  lcon ? x = apstage(x, lcC, lcF, lcCc, lcFc, lcNb, lcNo, st);
  hcon ? x = apstage(x, hcC, hcF, hcCc, hcFc, hcNb, hcNo, st + 20);
  x;
);
// the linear part of fzos: the same 4x anti-alias filters (up, down), no distortion - the phase the saturating path has
function ecasc(x, st) local(y) (
  y = oc0 * x + st[0]; st[0] = oc1 * x - oc3 * y + st[1]; st[1] = oc2 * x - oc4 * y; x = y;
  y = oc5 * x + st[2]; st[2] = oc6 * x - oc8 * y + st[3]; st[3] = oc7 * x - oc9 * y; x = y;
  y = oc10 * x + st[4]; st[4] = oc11 * x - oc13 * y + st[5]; st[5] = oc12 * x - oc14 * y; x = y;
  y = oc15 * x + st[6]; st[6] = oc16 * x - oc18 * y + st[7]; st[7] = oc17 * x - oc19 * y; x = y;
  y = oc20 * x + st[8]; st[8] = oc21 * x - oc23 * y + st[9]; st[9] = oc22 * x - oc24 * y; y;
);
// Linear counterpart of fzos: passes x through the same 4x up/down elliptic filters (state st) without distortion, for matching phase.
function oslin(x, st) (
  ecasc(ecasc(4 * x, st), st + 10); ecasc(ecasc(0, st), st + 10); ecasc(ecasc(0, st), st + 10);
  ecasc(ecasc(0, st), st + 10);                                   // (as fzos: the 4th sample of the four)
);
// ORIGINAL + DISTORTION, one channel: 4x up, tanh with first-order ADAA, 4x down, DC blocker (st: 40 states)
function odsat(x, st) local(y, u, du, k, lcv)
(
  k = 0;
  loop(4,
    u = odG * ecasc(k == 0 ? 4 * x : 0, st) + odA; du = u - st[30]; lcv = lcosh(u);   // (st[33] = lcosh of the previous u,)
    y = ((abs(du) > 0.00001 ? (lcv - st[33]) / du : thf(0.5 * (u + st[30]))) - odAT) / odN;
    st[30] = u; st[33] = lcv; y = ecasc(y, st + 10);
    k += 1;
  );
  odA > 0 ? ( st[31] = y - st[32] + fzDR * st[31]; st[32] = y; y = st[31]; );
  y;
);
// its dry part (module mix, on / off): the same 4x filters and DC blocker, linear - no comb against the saturated part
function odlin(x, st) local(y) ( y = oslin(x, st); odA > 0 ? ( st[31] = y - st[32] + fzDR * st[31]; st[32] = y; y = st[31]; ); y; );
// Source model: the dry path (its high-pass, its low-pass - twice with a codec, the 4x filters and the DC blocker of the
// saturation, as fzrun), st: 60 states, so: 22 states
function fzap(x, st, so) (
  fzSat > 0 || fzClip > 0 ? (
    x = oslin(x, so);
    fzSat > 0 && fzAsym > 0 ? ( so[21] = x - so[20] + fzDR * so[21]; so[20] = x; x = so[21]; );
  );
  fzHPb + fzHPo > 0 ? x = apstage(x, fzb, fzb + 64, fzCc, fzCc + 40, fzHPb, fzHPo, st);
  fzLPb + fzLPo > 0 ? (
    x = apstage(x, fzb + 24, fzb + 68, fzCc + 20, fzCc + 44, fzLPb, fzLPo, st + 20);
    fzCrSR > 0 ? x = apstage(x, fzb + 24, fzb + 68, fzCc + 20, fzCc + 44, fzLPb, fzLPo, st + 40);
  );
  x;
);
// the linear part of fzrun for the stereo side (Original + EQ): high-pass, EQ bands, low-pass, gain - the same coefficients and
// order as the mid, own states (fzSs). Devices are mono: their side fades out instead (see STEREO WIDTH)
function fzside(x) (
  fzHPb > 0 ? x = bqrun(x, fzb, fzSs, fzHPb);
  fzHPo ? x = forun(x, fzb + 64, fzSs + 32);
  fzNP > 0 ? x = bqrun(x, fzb + 48, fzSs + 16, fzNP);
  fzLPb > 0 ? x = bqrun(x, fzb + 24, fzSs + 8, fzLPb);
  fzLPo ? x = forun(x, fzb + 68, fzSs + 34);
  x * fzOutG;
);
// Returns the linear magnitude of the biquad at cb c (b0,b1,b2,a1,a2) at angular frequency w.
function bqmag(c, w) local(nr, ni, dr, di)
(
  nr = c[0] + c[1] * cos(w) + c[2] * cos(2 * w); ni = -(c[1] * sin(w) + c[2] * sin(2 * w));
  dr = 1 + c[3] * cos(w) + c[4] * cos(2 * w);     di = -(c[3] * sin(w) + c[4] * sin(2 * w));
  sqrt((nr * nr + ni * ni) / max(0.0000000001, dr * dr + di * di));
);
// first-order section (odd slopes): magnitude at w
function g_fomag(c, w) ( sqrt((sqr(c[0] + c[1] * cos(w)) + sqr(c[1] * sin(w))) / max(0.0000000001, sqr(1 + c[2] * cos(w)) + sqr(c[2] * sin(w)))); );
// response of the GUI's temp design fzt / fzTP at fq, in dB (source EQ display, help page)
function fzmagdb(fq) local(w, mag, j)
(
  w = 2 * $pi * min(fq, 0.49 * srate) / srate; mag = fzTP[19];
  j = 0; loop(floor(fzTP[1] / 2), mag *= bqmag(fzt + j * 5, w); j += 1; );
  (fzTP[1] % 2) ? mag *= g_fomag(fzt + 64, w);
  j = 0; loop(floor(fzTP[3] / 2), mag *= bqmag(fzt + 24 + j * 5, w); j += 1; );
  (fzTP[3] % 2) ? mag *= g_fomag(fzt + 68, w);
  j = 0; loop(fzTP[4], mag *= bqmag(fzt + 48 + j * 5, w); j += 1; );
  20 * log10(max(0.000001, mag));
);
function fzcache() local(m, gi)       // frequency responses for the help page (GUI thread, temp blocks)
(
  m = 0;
  loop(13,
    g_fzdesign(fzt, fzTP, m, 0);
    gi = 0; loop(48, fzresp[m * 48 + gi] = fzmagdb(40 * 500 ^ (gi / 47)); gi += 1; );
    m += 1;
  );
);
// SOURCE PRESETS (the second column of the model menu): model, distortion, call line - EQ flat, model on, mix 100 %
NSP = 23; spTb = alloc(8 * NSP);   // per preset: category, model, distortion, duplex, codec, dropouts, line, EQ shape (spq)
spTb[0] = 0; spTb[1] = 1; spTb[2] = 45; spTb[3] = 0; spTb[4] = 0; spTb[5] = 0; spTb[6] = 1;   // Landline call
spTb[8] = 0; spTb[9] = 2; spTb[10] = 55; spTb[11] = 15; spTb[12] = 55; spTb[13] = 8; spTb[14] = 1;   // Old mobile (2G)
spTb[16] = 0; spTb[17] = 3; spTb[18] = 30; spTb[19] = 0; spTb[20] = 15; spTb[21] = 0; spTb[22] = 2;   // Mobile HD call
spTb[24] = 0; spTb[25] = 2; spTb[26] = 50; spTb[27] = 25; spTb[28] = 75; spTb[29] = 45; spTb[30] = 1;   // Bad connection
spTb[32] = 0; spTb[33] = 3; spTb[34] = 40; spTb[35] = 65; spTb[36] = 25; spTb[37] = 5; spTb[38] = 2;   // Speakerphone on a table
spTb[40] = 0; spTb[41] = 10; spTb[42] = 35; spTb[43] = 55; spTb[44] = 30; spTb[45] = 5; spTb[46] = 2;   // Conference call
spTb[48] = 0; spTb[49] = 8; spTb[50] = 30; spTb[51] = 40; spTb[52] = 35; spTb[53] = 10; spTb[54] = 3;   // Video call
spTb[56] = 0; spTb[57] = 3; spTb[58] = 25; spTb[59] = 0; spTb[60] = 45; spTb[61] = 0; spTb[62] = 2;   // Voice message
spTb[64] = 1; spTb[65] = 4; spTb[66] = 60; spTb[67] = 30; spTb[68] = 0; spTb[69] = 0; spTb[70] = 1;   // Walkie-talkie
spTb[72] = 1; spTb[73] = 4; spTb[74] = 70; spTb[75] = 35; spTb[76] = 60; spTb[77] = 15; spTb[78] = 1;   // Police radio
spTb[80] = 1; spTb[81] = 12; spTb[82] = 60; spTb[83] = 0; spTb[84] = 0; spTb[85] = 0; spTb[86] = 0;   // Megaphone
spTb[88] = 1; spTb[89] = 12; spTb[90] = 35; spTb[91] = 0; spTb[92] = 0; spTb[93] = 0; spTb[94] = 2;   // PA announcement
spTb[96] = 1; spTb[97] = 1; spTb[98] = 65; spTb[99] = 50; spTb[100] = 20; spTb[101] = 0; spTb[102] = 1;   // Intercom
spTb[104] = 1; spTb[105] = 5; spTb[106] = 75; spTb[107] = 45; spTb[108] = 30; spTb[109] = 0; spTb[110] = 1;   // Drive-thru
spTb[112] = 2; spTb[113] = 5; spTb[114] = 55; spTb[115] = 0; spTb[116] = 0; spTb[117] = 0; spTb[118] = 0;   // Old radio
spTb[120] = 2; spTb[121] = 6; spTb[122] = 50; spTb[123] = 0; spTb[124] = 0; spTb[125] = 0; spTb[126] = 0;   // Boombox
spTb[128] = 2; spTb[129] = 7; spTb[130] = 40; spTb[131] = 0; spTb[132] = 0; spTb[133] = 0; spTb[134] = 0;   // TV in the room
spTb[136] = 2; spTb[137] = 8; spTb[138] = 35; spTb[139] = 0; spTb[140] = 0; spTb[141] = 0; spTb[142] = 0;   // Laptop speakers
spTb[144] = 2; spTb[145] = 9; spTb[146] = 30; spTb[147] = 0; spTb[148] = 15; spTb[149] = 0; spTb[150] = 3;   // Tablet video
spTb[152] = 2; spTb[153] = 10; spTb[154] = 55; spTb[155] = 0; spTb[156] = 25; spTb[157] = 3; spTb[158] = 2;   // Bluetooth speaker
spTb[168] = 2; spTb[169] = 0; spTb[170] = 10; spTb[171] = 0; spTb[172] = 0; spTb[173] = 0; spTb[174] = 0; spTb[175] = 1;   // Car stereo: clean full range, EQ shape 1 (bass lift, mid scoop, bright highs)
spTb[176] = 2; spTb[177] = 0; spTb[178] = 15; spTb[179] = 0; spTb[180] = 0; spTb[181] = 0; spTb[182] = 0; spTb[183] = 2;   // Stadium music system: full range, a fat sub bass, warm low mids, big highs
spTb[160] = 2; spTb[161] = 11; spTb[162] = 20; spTb[163] = 0; spTb[164] = 0; spTb[165] = 0; spTb[166] = 0;   // Headphones leaking
// Returns the display name of source preset k (call, radio, PA and device presets).
function spname(k) ( (k == 0 ? "Landline call" : (k == 1 ? "Old mobile (2G)" : (k == 2 ? "Mobile HD call" : (k == 3 ? "Bad connection" : (k == 4 ? "Speakerphone on a table" : (k == 5 ? "Conference call" : (k == 6 ? "Video call" : (k == 7 ? "Voice message" : (k == 8 ? "Walkie-talkie" : (k == 9 ? "Police radio" : (k == 10 ? "Megaphone" : (k == 11 ? "PA announcement" : (k == 12 ? "Intercom" : (k == 13 ? "Drive-thru" : (k == 14 ? "Old radio" : (k == 15 ? "Boombox" : (k == 16 ? "TV in the room" : (k == 17 ? "Laptop speakers" : (k == 18 ? "Tablet video" : (k == 19 ? "Bluetooth speaker" : (k == 20 ? "Headphones leaking" : (k == 21 ? "Car stereo" : "Stadium music system")))))))))))))))))))))); );
// Returns the one-line description text of source preset k.
function spdesc(k) ( (k == 0 ? "a classic call: telephone handset, 300 Hz – 3.4 kHz" : (k == 1 ? "an early mobile call: narrowband, the codec warbles" : (k == 2 ? "a modern phone call: HD voice (wideband), clean" : (k == 3 ? "breaking up: codec artifacts, robotic drops, gaps" : (k == 4 ? "hands-free: onsets clipped, the level pumps" : (k == 5 ? "a conference puck in a meeting room" : (k == 6 ? "a laptop video call (VoIP, super-wideband)" : (k == 7 ? "a recorded voice message played on a phone" : (k == 8 ? "push to talk: narrow, hard clipping, squelch hiss" : (k == 9 ? "digital radio: vocoder artifacts and drop-outs" : (k == 10 ? "a hand-held horn: loud, narrow, beamed" : (k == 11 ? "a station or hall announcement" : (k == 12 ? "a door intercom: small speaker, switching" : (k == 13 ? "a drive-thru speaker post: crackly and switching" : (k == 14 ? "a small radio in a box: mid-heavy, warm distortion" : (k == 15 ? "a portable stereo: bass and presence" : (k == 16 ? "a TV set playing in the room" : (k == 17 ? "small laptop speakers: no bass" : (k == 18 ? "a video on a tablet" : (k == 19 ? "a small Bluetooth speaker, 360 degrees" : (k == 20 ? "what leaks out of someone's headphones" : (k == 21 ? "a modern car stereo: full range with a bass lift, a slight mid scoop and bright highs - clean, not vintage" : "a big stadium music system: full range, a fat sub bass, warm low mids and big, bright highs (music, not an announcement)")))))))))))))))))))))); );
// Returns the icon height in pixels for preset k's icon (31 for k=4, 28 for 2, 27 for 7, 24 for 3, else 22).
function ichgt(k) ( k == 4 ? 31 : (k == 2 ? 28 : (k == 7 ? 27 : (k == 3 ? 24 : 22))); );
function icfit(k) ( min(1, 24 / ichgt(k)); );   // tall icons (walkie-talkie, mobile) stay inside their row
// Returns the source preset category heading for index c: Calls, Radio and PA, or Devices.
function spcat(c) ( c == 0 ? "Calls" : (c == 1 ? "Radio and PA" : "Devices"); );
// row of preset k in the menu (category headings of 15 px between the groups)
function spry(k) ( srcmy + 4 + k * srcih + (spTb[k * 8] + 1) * 15; );
// Returns the preset index whose menu row contains y position yy, or -1 if none.
function spritem(yy) local(ri, k) ( ri = -1; k = 0; loop(NSP, yy >= spry(k) && yy < spry(k) + srcih ? ri = k; k += 1; ); ri; );
// the Source EQ values of a preset's EQ shape (0 = none; 1 = car stereo: bass +7 dB at about 90 Hz, mids -1.5 dB, treble +2.5 dB at about 5.6 kHz)
function spq(f, i) ( f == 2 ? (i == 49 ? -2 : (i == 50 ? 10 : (i == 51 ? -0.4 : (i == 52 ? -1 : (i == 53 ? 3 : (i == 55 ? 0.4 : (i == 56 ? 3.5 : 0))))))) : (f == 1 ? (i == 49 ? -1.5 : (i == 50 ? 7 : (i == 53 ? -1.5 : (i == 55 ? 0.5 : (i == 56 ? 2.5 : 0))))) : 0); );   // 2 = stadium system: sub bass +10 dB at about 60 Hz (wide), warm low mids +3 dB, highs +3.5 dB
// does preset k describe the current source settings?
function spmatch(k) local(b, m, i) ( b = spTb + k * 8; m = slider85 >= 0.5 && fzsel == b[1] && abs(slider37 - b[2]) < 0.05 && abs(slider111 - b[3]) < 0.05 && abs(slider112 - b[4]) < 0.05
  && abs(slider113 - b[5]) < 0.05 && floor(slider114 + 0.5) == b[6]; i = 47; loop(13, abs(slider(i) - spq(b[7], i)) > 0.0005 ? m = 0; i += 1; ); m; );
// Returns the name of source model k (Original, Telephone, ... Megaphone).
function srcname(k) ( k == 0 ? "Original" : (k == 1 ? "Telephone" : (k == 2 ? "Mobile phone" : (k == 3 ? "Smartphone" : (k == 4 ? "Walkie-talkie" : (k == 5 ? "Radio" : (k == 6 ? "Boombox" : (k == 7 ? "TV" : (k == 8 ? "Laptop" : (k == 9 ? "Tablet" : (k == 10 ? "Mini speaker" : (k == 11 ? "Headphones" : "Megaphone"))))))))))) );
// Returns the short characterization subtitle of source model k (e.g. frequency range).
function srcsub(k) ( k == 0 ? "Full range" : (k == 1 ? "300 Hz – 3.4 kHz" : (k == 2 ? "Codec 8 kHz" : (k == 3 ? "No bass" : (k == 4 ? "400 Hz – 2.8 kHz" : (k == 5 ? "180 Hz – 4.5 kHz" : (k == 6 ? "Big bass" : (k == 7 ? "Boxy" : (k == 8 ? "Thin" : (k == 9 ? "Light" : (k == 10 ? "Bluetooth" : (k == 11 ? "Leaking earbuds" : "Horn"))))))))))) );
// Returns the call-line band name for value v: Off, Narrowband, Wideband or Super-wideband.
function lnname(v) ( v < 0.5 ? "Off" : (v < 1.5 ? "Narrowband" : (v < 2.5 ? "Wideband" : "Super-wideband")); );
// Returns the short call-line band label for value v: Off, Narrow, Wide or Super.
function lnshort(v) ( v < 0.5 ? "Off" : (v < 1.5 ? "Narrow" : (v < 2.5 ? "Wide" : "Super")); );
// Returns the call-line menu entry text for index k, including its frequency range.
function lnmenu(k) ( k == 0 ? "Off – the device only" : (k == 1 ? "Narrowband  0.3 – 3.4 kHz" : (k == 2 ? "Wideband  0.1 – 7 kHz" : "Super-wideband  0.05 – 14 kHz")); );
// Formats a percentage v into string #fv as 'off' (below 0.05) or '<n> %'; returns it.
function clval(v) ( v < 0.05 ? strcpy(#fv, "off") : ( sprintf(#fv, "%.0f", v); strcat(#fv, " %"); ); );
// Returns the second subtitle line (character of the processing) of source model k.
function srcsub2(k) ( k == 0 ? "unprocessed" : (k == 1 ? "saturation" : (k == 2 ? "codec crush" : (k == 3 ? "bright" : (k == 4 ? "clip + hiss" : (k == 5 ? "warm saturation" : (k == 6 ? "crunchy" : (k == 7 ? "400 Hz resonance" : (k == 8 ? "hollow mids" : (k == 9 ? "little low end" : (k == 10 ? "small box" : (k == 11 ? "thin, distant" : "heavy distortion"))))))))))) );

// ---------- physics ------------------------------------------------------------
// ---------- BARRIER (wall / window / door parallel to the back wall) ----------
// back-part material (the part between back wall and barrier): 0 = same as the room
function g_bkA(i) ( i == 1 ? 0.05 : (i == 2 ? 0.03 : (i == 3 ? 0.08 : (i == 4 ? 0.10 : (i == 5 ? 0.45 : (i == 6 ? 0.70 : (i == 7 ? 0.85 : (i == 8 ? 0.02 : 0.04))))))); );// GUI copy
// Returns the mid-band absorption coefficient of barrier material i (1 concrete ... 8 tiles, else plaster).
function bkA(i) ( i == 1 ? 0.05 : (i == 2 ? 0.03 : (i == 3 ? 0.08 : (i == 4 ? 0.10 : (i == 5 ? 0.45 : (i == 6 ? 0.70 : (i == 7 ? 0.85 : (i == 8 ? 0.02 : 0.04))))))); );
function bkord(p) ( p == 0 ? 0 : (p == 1 ? 8 : (p == 2 ? 2 : (p == 3 ? 9 : (p == 4 ? 1 : (p == 5 ? 3 : (p == 6 ? 4 : (p == 7 ? 5 : (p == 8 ? 6 : 7)))))))); );   // menu row -> material
// Returns the name of barrier material i (0 = Same as room).
function bkname(i) ( i == 0 ? "Same as room" : (i == 1 ? "Concrete" : (i == 2 ? "Glass" : (i == 3 ? "Metal" : (i == 4 ? "Wood" : (i == 5 ? "Curtain" : (i == 6 ? "Foam" : (i == 7 ? "Wool" : (i == 8 ? "Tiles" : "Plaster")))))))); );
// ---- AIR ABSORPTION ----
// AIR ABSORPTION after ISO 9613-1 (dB per meter) for frequency f, temperature tc (C) and relative humidity rh (%)
function airdb(f, tc, rh) local(tk, tr, h, frO, frN)
(
  tk = tc + 273.15; tr = tk / 293.15;
  h = rh * 10 ^ (-6.8346 * (273.16 / tk) ^ 1.261 + 4.6151);          // molar concentration of water vapor (%)
  frO = 24 + 40400 * h * (0.02 + h) / (0.391 + h);
  frN = tr ^ -0.5 * (9 + 280 * h * exp(-4.170 * (tr ^ (-1 / 3) - 1)));
  8.686 * f * f * (0.0000000000184 * tr ^ 0.5 + tr ^ -2.5 * (0.01275 * exp(-2239.1 / tk) / (frO + f * f / frO) + 0.1068 * exp(-3352 / tk) / (frN + f * f / frN)));
);
// alpha of a surface in band b (0 = 125 Hz, 1 = 4 kHz) from its mid alpha: the material shape, interpolated (log alpha)
function aband(a, b) local(i, t, arat)
(
  a <= mtab[0] ? ( arat = mtab[1 + b]; ) : ( a >= mtab[(NMAT - 1) * 3] ? ( arat = mtab[(NMAT - 1) * 3 + 1 + b]; ) : (
    i = 0; while ( i < NMAT - 2 && a > mtab[(i + 1) * 3] ? ( i += 1; 1; ) : 0; );
    t = log(a / mtab[i * 3]) / log(mtab[(i + 1) * 3] / mtab[i * 3]);
    arat = mtab[i * 3 + 1 + b] + (mtab[(i + 1) * 3 + 1 + b] - mtab[i * 3 + 1 + b]) * t;
  ); );
  arat = min(0.99, max(0.005, a * exp(arat)));
  // bass: every real surface also loses some low end through flexing, gaps, doors and furniture (~3 %) - without it a
  // room with soft walls would boom far longer than measured rooms do
  b == 0 ? arat + 0.03 * (1 - arat) : arat;
);
// alpha in octave j (0 = 125 Hz ... 5 = 4 kHz) from the mid alpha: the material shape per octave, interpolated (log alpha);
// the bass leak (flexing, gaps, furniture) as in aband: 3 % at 125 Hz, 1.5 % at 250 Hz
function abandX(a, j) local(i, t, arat)
(
  a <= mtab[0] ? ( arat = mtx[j]; ) : ( a >= mtab[(NMAT - 1) * 3] ? ( arat = mtx[(NMAT - 1) * 6 + j]; ) : (
    i = 0; while ( i < NMAT - 2 && a > mtab[(i + 1) * 3] ? ( i += 1; 1; ) : 0; );
    t = log(a / mtab[i * 3]) / log(mtab[(i + 1) * 3] / mtab[i * 3]);
    arat = mtx[i * 6 + j] + (mtx[(i + 1) * 6 + j] - mtx[i * 6 + j]) * t;
  ); );
  arat = min(0.99, max(0.005, a * exp(arat)));
  j == 0 ? arat + 0.03 * (1 - arat) : (j == 1 ? arat + 0.015 * (1 - arat) : arat);
);
// Floor + ceiling absorption in octave j (0 = 125 Hz … 5 = 4 kHz): the neutral shape of abandF per octave (x0.8 … x1.1), clamped 0.005-0.99, with the bass leak as in abandX.
function abandFX(a, j) local(arat)
(
  arat = min(0.99, max(0.005, a * (j == 0 ? 0.8 : (j == 1 ? 0.88 : (j == 2 ? 0.96 : (j == 3 ? 1.04 : (j == 4 ? 1.07 : 1.1)))))));
  j == 0 ? arat + 0.03 * (1 - arat) : (j == 1 ? arat + 0.015 * (1 - arat) : arat);
);
// Returns the total absorption area for band j of a box bw x bd x bh: two walls each pair plus floor and ceiling, using abandX/abandFX.
function areaX(j, bw, bd, bh, a1, a2, a3, a4, a5) ( bw * bh * (abandX(a1, j) + abandX(a2, j)) + bd * bh * (abandX(a3, j) + abandX(a4, j)) + 2 * bw * bd * abandFX(a5, j); );
// ---- PER-OCTAVE LOOP FILTER: low shelf 177 Hz, peaks 250 / 500 / 2k / 4k (Q 1.41), high shelf 5.6 kHz, gain.
// The 7 gains are fitted to the wanted attenuation per pass at 125 / 250 / 500 / 1k / 2k / 4k / 8k Hz (interaction matrix,
// inverted once per sample rate, then 2 corrections); below 63 Hz the low shelf holds the 125 Hz value
function fqshelf(cb, f, gdb, hi) local(fqa, w0, cw, sw, al, sa, a0)
(
  fqa = 10 ^ (gdb / 40); w0 = 2 * $pi * min(f, 0.45 * srate) / srate; cw = cos(w0); sw = sin(w0); al = sw / 2 * sqrt(2); sa = 2 * sqrt(fqa) * al;
  hi ? (
    a0 = (fqa + 1) - (fqa - 1) * cw + sa;
    cb[0] = fqa * ((fqa + 1) + (fqa - 1) * cw + sa) / a0; cb[1] = -2 * fqa * ((fqa - 1) + (fqa + 1) * cw) / a0; cb[2] = fqa * ((fqa + 1) + (fqa - 1) * cw - sa) / a0;
    cb[3] = 2 * ((fqa - 1) - (fqa + 1) * cw) / a0; cb[4] = ((fqa + 1) - (fqa - 1) * cw - sa) / a0;
  ) : (
    a0 = (fqa + 1) + (fqa - 1) * cw + sa;
    cb[0] = fqa * ((fqa + 1) - (fqa - 1) * cw + sa) / a0; cb[1] = 2 * fqa * ((fqa - 1) - (fqa + 1) * cw) / a0; cb[2] = fqa * ((fqa + 1) - (fqa - 1) * cw - sa) / a0;
    cb[3] = -2 * ((fqa - 1) + (fqa + 1) * cw) / a0; cb[4] = ((fqa + 1) + (fqa - 1) * cw - sa) / a0;
  );
);
// level of a biquad at f in dB
function bqdb(cb, f) local(w, c1, s1, c2, s2, nr, ni, dr, di)
(
  w = 2 * $pi * f / srate; c1 = cos(w); s1 = sin(w); c2 = cos(2 * w); s2 = sin(2 * w);
  nr = cb[0] + cb[1] * c1 + cb[2] * c2; ni = -(cb[1] * s1 + cb[2] * s2);
  dr = 1 + cb[3] * c1 + cb[4] * c2; di = -(cb[3] * s1 + cb[4] * s2);
  10 * log10(max(0.0000000001, (nr * nr + ni * ni) / max(0.0000000001, dr * dr + di * di)));
);
// Returns the center frequency in Hz of loop-filter fit point i (125 Hz ... 8 kHz, octave steps).
function fqfreq(i) ( i == 0 ? 125 : (i == 1 ? 250 : (i == 2 ? 500 : (i == 3 ? 1000 : (i == 4 ? 2000 : (i == 5 ? 4000 : 8000))))); );
// the same at a frequency of the table fqT (cos / sin of w and 2w, made once per sample rate): 0-6 = the fit points, 7-22 = check grid
function bqdbI(cb, i) local(t, nr, ni, dr, di)
(
  t = fqT + i * 4;
  nr = cb[0] + cb[1] * t[0] + cb[2] * t[2]; ni = -(cb[1] * t[1] + cb[2] * t[3]);
  dr = 1 + cb[3] * t[0] + cb[4] * t[2]; di = -(cb[3] * t[1] + cb[4] * t[3]);
  10 * log10(max(0.0000000001, (nr * nr + ni * ni) / max(0.0000000001, dr * dr + di * di)));
);
// Returns the loop filter's total level in dB (gain g0 plus six biquads at cb) at tabulated frequency index i.
function fqrespI(cb, g0, i) ( g0 + bqdbI(cb, i) + bqdbI(cb + 5, i) + bqdbI(cb + 10, i) + bqdbI(cb + 15, i) + bqdbI(cb + 20, i) + bqdbI(cb + 25, i); );
// the 6 filters of one line from the gains x[1..6] (x[0] = broadband gain, not a filter)
function fqset(cb, x)
(
  fqshelf(cb, 177, x[1], 0); pkdesign(cb + 5, 250, x[2], 1.41); pkdesign(cb + 10, 500, x[3], 1.41);
  pkdesign(cb + 15, 2000, x[4], 1.41); pkdesign(cb + 20, 4000, x[5], 1.41); fqshelf(cb + 25, 5657, x[6], 1);
);
// once per sample rate: response matrix for 1 dB in each of the 7 gains, inverted (Gauss-Jordan, partial pivot)
function fqinit() local(i, j, k, rq, piv, t, x, cb, f, w)
(
  x = fqW; cb = fqW + 16; fqT = fqW + 100;
  i = 0; f = 20;
  loop(23,
    w = 2 * $pi * min(i < 7 ? fqfreq(i) : f, 0.45 * srate) / srate;
    fqT[i * 4] = cos(w); fqT[i * 4 + 1] = sin(w); fqT[i * 4 + 2] = cos(2 * w); fqT[i * 4 + 3] = sin(2 * w);
    i >= 7 ? f *= 1.6; i += 1;
  );
  j = 0;
  loop(7,
    memset(x, 0, 7); x[j] = 1; fqset(cb, x);
    i = 0; loop(7, fqB[i * 7 + j] = fqrespI(cb, x[0], i); i += 1; );
    j += 1;
  );
  // [B | I] -> [I | B^-1]
  i = 0; loop(7, j = 0; loop(7, fqBi[i * 7 + j] = i == j; j += 1; ); i += 1; );
  k = 0;
  loop(7,
    piv = k; rq = k + 1; loop(6 - k, abs(fqB[rq * 7 + k]) > abs(fqB[piv * 7 + k]) ? piv = rq; rq += 1; );
    piv != k ? ( j = 0; loop(7, t = fqB[k * 7 + j]; fqB[k * 7 + j] = fqB[piv * 7 + j]; fqB[piv * 7 + j] = t;
                                 t = fqBi[k * 7 + j]; fqBi[k * 7 + j] = fqBi[piv * 7 + j]; fqBi[piv * 7 + j] = t; j += 1; ); );
    t = fqB[k * 7 + k]; j = 0; loop(7, fqB[k * 7 + j] /= t; fqBi[k * 7 + j] /= t; j += 1; );
    rq = 0;
    loop(7,
      rq != k ? ( t = fqB[rq * 7 + k]; j = 0; loop(7, fqB[rq * 7 + j] -= t * fqB[k * 7 + j]; fqBi[rq * 7 + j] -= t * fqBi[k * 7 + j]; j += 1; ); );
      rq += 1;
    );
    k += 1;
  );
  fqsr = srate;
);
// one line: length L samples, decay rt (mid), ratios fqR[1..7] (125 Hz ... 8 kHz) -> its 6 filters + gain.
// Attenuation per pass is held at -35 dB at most (a band that loses more is gone after one pass anyway; the fit stays exact)
function fqdesign(k, L) local(t, x, e, cb, i, j, it, mxr, f)
(
  t = fqW + 64; x = fqW + 72; e = fqW + 80; cb = fqC + k * 30;
  i = 0; loop(7, t[i] = max(-35, -60 * L / (max(0.05, rt * fqR[i + 1]) * srate)); i += 1; );
  i = 0; loop(7, x[i] = 0; j = 0; loop(7, x[i] += fqBi[i * 7 + j] * t[j]; j += 1; ); i += 1; );
  it = 0;
  loop(2,
    fqset(cb, x);
    i = 0; loop(7, e[i] = t[i] - fqrespI(cb, x[0], i); i += 1; );
    i = 0; loop(7, j = 0; loop(7, x[i] += fqBi[i * 7 + j] * e[j]; j += 1; ); i += 1; );
  );
  fqset(cb, x);
  // never louder than 0 dB anywhere (20 Hz ... 0.45 fs): else the line would ring up - the gain comes down by the excess
  mxr = -99; i = 7;
  loop(16, mxr = max(mxr, fqrespI(cb, x[0], i)); i += 1; );
  fqG[k] = 10 ^ ((x[0] - max(0, mxr + 0.01)) / 20);
);
// floor + ceiling: one value stands for everything that is not a wall (floor covering, ceiling, furniture, people) - a
// neutral shape (a little less in the bass, a little more in the highs) instead of one material
function abandF(a, b) local(arat) ( arat = min(0.99, max(0.005, a * (b == 0 ? 0.8 : 1.1))); b == 0 ? arat + 0.03 * (1 - arat) : arat; );
// Returns the relative HF-to-mid pressure ratio per reflection for floor/ceiling alpha a: sqrt((1-aHF)/(1-a)).
function hfrelF(a) ( sqrt(max(0.0001, 1 - abandF(a, 1)) / max(0.0001, 1 - a)); );
// absorption area of a box in band b (walls N S E W, floor + ceiling F)
function areaB(b, bw, bd, bh, a1, a2, a3, a4, a5) ( bw * bh * (aband(a1, b) + aband(a2, b)) + bd * bh * (aband(a3, b) + aband(a4, b)) + 2 * bw * bd * abandF(a5, b); );
// highs of one reflection relative to the mids (amplitude at 4 kHz / amplitude at 500 Hz - 1 kHz)
function hfrel(a) ( sqrt(max(0.0001, 1 - aband(a, 1)) / max(0.0001, 1 - a)); );
// ---- REFLECTORS (free field): two vertical planes, state in vopt[52 ...] (6 values each: material, X, Y, angle, length, height) ----
RFB = 52;   // (RFN was never read - removed)
// Returns the absorption coefficient of reflector type m (facade 0.04, glass 0.03, rock 0.02, fence 0.12, hedge 0.5).
function rflA(m) ( m < 1.5 ? 0.04 : (m < 2.5 ? 0.03 : (m < 3.5 ? 0.02 : (m < 4.5 ? 0.12 : 0.5))); );
// Clears the 12 reflector option slots at RFB and sets the next two (RFB+12, RFB+13) to 12.
function rflclr() local(i) ( i = RFB; loop(12, vopt[i] = 0; i += 1; ); vopt[RFB + 12] = vopt[RFB + 13] = 12; );
// Returns the reflector name for mode m: Off, Facade, Glass, Rock, Fence or Hedge.
// REFLECTOR AUTOMATION (0.385): the 15 reflector values (vopt[RFB] … vopt[RFB + 14]) are mirrored in sliders 120 … 134,
// so the host can automate them. The audio thread copies a changed slider into vopt (automation, host); the GUI pushes its
// own changes (knobs, handles, setups, undo, snapshots) into the sliders at the end of each frame. rfSl = last synced value.
function a_rfset(i, v) (
  i == 0 ? slider120 = v;
  i == 1 ? slider121 = v;
  i == 2 ? slider122 = v;
  i == 3 ? slider123 = v;
  i == 4 ? slider124 = v;
  i == 5 ? slider125 = v;
  i == 6 ? slider126 = v;
  i == 7 ? slider127 = v;
  i == 8 ? slider128 = v;
  i == 9 ? slider129 = v;
  i == 10 ? slider130 = v;
  i == 11 ? slider131 = v;
  i == 12 ? slider132 = v;
  i == 13 ? slider133 = v;
  i == 14 ? slider134 = v;
);
function rfset(i, v) (
  i == 0 ? slider120 = v;
  i == 1 ? slider121 = v;
  i == 2 ? slider122 = v;
  i == 3 ? slider123 = v;
  i == 4 ? slider124 = v;
  i == 5 ? slider125 = v;
  i == 6 ? slider126 = v;
  i == 7 ? slider127 = v;
  i == 8 ? slider128 = v;
  i == 9 ? slider129 = v;
  i == 10 ? slider130 = v;
  i == 11 ? slider131 = v;
  i == 12 ? slider132 = v;
  i == 13 ? slider133 = v;
  i == 14 ? slider134 = v;
);
// automation notification for reflector slider i: e = 0 touch start, 1 touch end, 2 one-shot
function rfauto(i, e) (
  e == 0 ? (
  i == 0 ? slider_automate(slider120, 0);
  i == 1 ? slider_automate(slider121, 0);
  i == 2 ? slider_automate(slider122, 0);
  i == 3 ? slider_automate(slider123, 0);
  i == 4 ? slider_automate(slider124, 0);
  i == 5 ? slider_automate(slider125, 0);
  i == 6 ? slider_automate(slider126, 0);
  i == 7 ? slider_automate(slider127, 0);
  i == 8 ? slider_automate(slider128, 0);
  i == 9 ? slider_automate(slider129, 0);
  i == 10 ? slider_automate(slider130, 0);
  i == 11 ? slider_automate(slider131, 0);
  i == 12 ? slider_automate(slider132, 0);
  i == 13 ? slider_automate(slider133, 0);
  i == 14 ? slider_automate(slider134, 0);
  ) : e == 1 ? (
  i == 0 ? slider_automate(slider120, 1);
  i == 1 ? slider_automate(slider121, 1);
  i == 2 ? slider_automate(slider122, 1);
  i == 3 ? slider_automate(slider123, 1);
  i == 4 ? slider_automate(slider124, 1);
  i == 5 ? slider_automate(slider125, 1);
  i == 6 ? slider_automate(slider126, 1);
  i == 7 ? slider_automate(slider127, 1);
  i == 8 ? slider_automate(slider128, 1);
  i == 9 ? slider_automate(slider129, 1);
  i == 10 ? slider_automate(slider130, 1);
  i == 11 ? slider_automate(slider131, 1);
  i == 12 ? slider_automate(slider132, 1);
  i == 13 ? slider_automate(slider133, 1);
  i == 14 ? slider_automate(slider134, 1);
  ) : (
  i == 0 ? slider_automate(slider120);
  i == 1 ? slider_automate(slider121);
  i == 2 ? slider_automate(slider122);
  i == 3 ? slider_automate(slider123);
  i == 4 ? slider_automate(slider124);
  i == 5 ? slider_automate(slider125);
  i == 6 ? slider_automate(slider126);
  i == 7 ? slider_automate(slider127);
  i == 8 ? slider_automate(slider128);
  i == 9 ? slider_automate(slider129);
  i == 10 ? slider_automate(slider130);
  i == 11 ? slider_automate(slider131);
  i == 12 ? slider_automate(slider132);
  i == 13 ? slider_automate(slider133);
  i == 14 ? slider_automate(slider134);
  );
);
function rflName(m) ( m < 0.5 ? "Off" : (m < 1.5 ? "Facade" : (m < 2.5 ? "Glass" : (m < 3.5 ? "Rock" : (m < 4.5 ? "Fence" : "Hedge")))); );
// Eyring RT60 in seconds of a box (volume bV, surface bSu, absorption area bA) including air absorption, clamped 0.08-20 s.
function rtEy(bV, bSu, bA) local(ab) ( ab = min(0.95, max(0.005, bA / bSu)); min(20, max(0.08, 0.161 * bV / (-bSu * log(1 - ab) + 4 * airm5 * bV))); );
// HF / mid decay ratio of a box: walls absorb more highs (abar), air absorbs much more at 4 kHz (m = 0.0068 /m)
function hfrEy(bV, bSu, bA, bAH) local(rm, rh)
(
  rm = 0.161 * bV / (-bSu * log(1 - min(0.95, max(0.005, bA / bSu))) + 4 * airm5 * bV);
  rh = 0.161 * bV / (-bSu * log(1 - min(0.95, max(0.005, bAH / bSu))) + 4 * airm4k * bV);
  min(1, max(0.1, rh / rm));                                // RT60 at 4 kHz / RT60 mid: materials + air
);
// bass RT60 / mid RT60 of a box (no air absorption at low frequencies)
function lfrEy(bV, bSu, bA, bAL) local(rm, rl)
(
  rm = 0.161 * bV / (-bSu * log(1 - min(0.95, max(0.005, bA / bSu))) + 4 * airm5 * bV);
  rl = 0.161 * bV / (-bSu * log(1 - min(0.95, max(0.005, bAL / bSu))) + 4 * airm125 * bV);
  min(1.8, max(0.4, rl / rm));                             // bass RT60 / mid RT60 (glass, panels: shorter; soft rooms: longer)
);
// fold an unfolded (image-space) coordinate back into the room 0 .. L
function fold(u, L) local(m) ( m = u - floor(u / (2 * L)) * 2 * L; m <= L ? m : 2 * L - m; );
function foldsg(u, L) local(m) ( m = u - floor(u / (2 * L)) * 2 * L; m <= L ? 1 : -1; );   // +1 = this image cell is not mirrored
// one crossing of the barrier plane at (fx, fz), path lengths d1 / d2 on both sides:
// bxE = energy factor at 500 Hz, bxF = equivalent one-pole corner (0 = no extra filtering)
//   through the opening: free (small openings pass less: energy ~ area below ~0.5 m2)
//   blocked: material (Rw = DAMP, mass law -> highs blocked, one-pole at 300 Hz)
//            + diffraction over the nearest opening edge (Maekawa: bA = 10 log(3 + 20 N), N = 2 delta / lambda)
function bxing(fx, fz, d1, d2) local(brx, brz, rho, dlt, a5, a4, dE, mE, dm, df, eE, eA, e5, ef, j, inelm, mfc, mm, mx5, mcmp, edx, edz, ejb, eon, egf)
(
  (fx < brX0 - 0.0001 || fx > brX1 + 0.0001 || fz > brH + 0.0001) ? (
    // beside or above a smaller partition: free (a narrow gap passes less)
    bxE = isfree ? 1 : min(1, (fz > brH ? roomH - brH : (fx < brX0 ? brX0 : roomW - brX1)) * 500 / C_SND); bxF = 0; bxM = 0; bxC = 0; bxT = 5; bxV = 0;   // a slit narrower than the wavelength passes less (w f / c at 500 Hz)
  ) :
  (aarea > 0.0001 && fx >= ax0 && fx <= ax1 && fz >= az0 && fz <= az1) ? (
    bxE = asz; bxF = 0; bxM = 0; bxC = 0; bxT = 1; bxV = 0;       // 1 = straight through the opening: heard where the source is
  ) : (
    inelm = bew > 0 && fx >= bex0 && fx <= bex1 && fz >= bez0 && fz <= bez1;
    dm = inelm ? dEl : dWall; mfc = inelm ? bfEl : bfWl; mm = inelm ? bmEl : bmWl;
    mE = 10 ^ (-dm / 10);
    dE = 0; df = 0; bxT = 2; edx = fx; edz = fz;
    aarea > 0.0001 ? (
      edx = min(ax1, max(ax0, fx)); edz = min(az1, max(az0, fz));   // nearest point of the opening: bent sound arrives from there
      brx = fx < ax0 ? ax0 - fx : (fx > ax1 ? fx - ax1 : 0);
      brz = fz < az0 ? az0 - fz : (fz > az1 ? fz - az1 : 0);
      rho = sqrt(brx * brx + brz * brz);
      dlt = sqrt(d1 * d1 + rho * rho) + sqrt(d2 * d2 + rho * rho) - d1 - d2;
      a5 = 10 * log10(3 + 40 * dlt * 500 / C_SND);
      a4 = 10 * log10(3 + 40 * dlt * 4000 / C_SND);
      dE = asz * 10 ^ (-a5 / 10);
      // one-pole corner that produces the extra loss (a4 - a5) at 4 kHz
      df = (a4 - a5) > 0.05 ? 4000 / sqrt(10 ^ ((a4 - a5) / 10) - 1) : 0;
    );
    // free edges: the free-standing wall (free field) and a partition smaller than the room end at their top / side
    // edges - sound bends around them (Maekawa per edge, energies add; a narrow gap passes less)
    isfree || brW < roomW - 0.01 || brH < roomH - 0.01 ? (
      eE = 0; eA = 0;
      j = 0;
      loop(3,
        eon = isfree || (j == 0 ? brH < roomH - 0.01 : (j == 1 ? brX0 > 0.01 : brX1 < roomW - 0.01));
        eon ? (
          rho = j == 0 ? brH - fz : (j == 1 ? fx - brX0 : brX1 - fx);    // distance to the top / left / right edge
          egf = isfree ? 1 : min(1, (j == 0 ? roomH - brH : (j == 1 ? brX0 : roomW - brX1)) * 500 / C_SND);
          dlt = sqrt(d1 * d1 + rho * rho) + sqrt(d2 * d2 + rho * rho) - d1 - d2;
          a5 = 10 * log10(3 + 40 * dlt * 500 / C_SND); a4 = 10 * log10(3 + 40 * dlt * 4000 / C_SND);
          e5 = egf * 10 ^ (-a5 / 10);
          e5 > eA ? ( eA = e5; ejb = j; ef = (a4 - a5) > 0.05 ? 4000 / sqrt(10 ^ ((a4 - a5) / 10) - 1) : 0; );
          eE += e5;
        );
        j += 1;
      );
      bxT = eE > dE ? 4 : 2;                                       // 4 = around the outer edges, 2 = bent through the opening
      eE > dE ? ( edx = ejb == 1 ? brX0 : (ejb == 2 ? brX1 : fx); edz = ejb == 0 ? brH : fz; dE += eE; df = ef; ) : ( dE += eE; );   // around the loudest outer edge
    );
    // through the material: DAMP holds at 500 Hz. Below, the mass law lets more bass through (6 dB/oct); above, real
    // partitions stop gaining damping (coincidence, gaps, side paths) -> a shelf: low-pass at mfc with the share mm of the
    // highs passing unfiltered (relative to the damping at 500 Hz):
    //   corner mfc and share mm per material, from DAMP (see the barrier setup in compute_geo: bfEl / bmEl, bfWl / bmWl)
    mx5 = sqr(500 / mfc); mcmp = (1 + mx5) / (1 + mm * mm * mx5);            // level of the shelf at 500 Hz, given back
    mcmp * mE > 1 ? ( mcmp = 1 / mE; mm = sqrt(max(0, ((1 + mx5) / mcmp - 1) / mx5)); );   // low DAMP: the bass cannot pass louder than it arrives -> flatter shelf
    dE >= mE ? ( bxE = dE + mE; bxF = df; bxM = 0; bxC = 0; ) : ( bxE = dE + mE * mcmp; bxF = mfc; bxM = mm; bxC = 10 * log10(mcmp); bxT = 3; );   // 3 = through the material
    bmixk < 1 ? ( bxE = min(1, bxE); );                                 // (mix: never more than without a barrier)
    // apparent direction: sound through the material keeps the direction of the source, bent sound arrives from the
    // edge it bends around - weighted by their energies (no jump when one takes over from the other)
    bxV = dE / max(0.000000000001, dE + mE); bxEx = edx; bxEz = edz;
  );
);
// all barrier crossings of the path listener -> image source (image space): btE energy factor, btF corner (0 = none)
function btrans(px, py, pz) local(j, j0, j1, yj, t, dtot, gE, gF, gM, gN, gC, ncr, crX, crZ, crY, crV, crEx, crEz, vpx, vpz, dv1, dv2)
(
  gE = 1; gF = 0; gM = 1; gN = 0; gC = 0; ncr = 0;
  dtot = sqrt(sqr(px - lx) + sqr(py - ly) + sqr(pz - lz));
  j0 = min(0, floor(py / roomD)); j1 = max(0, floor(py / roomD));
  j = j0;
  loop(j1 - j0 + 1,
    yj = (abs(j) % 2) ? (j + 1) * roomD - yb : j * roomD + yb;      // the barrier, mirrored into image cell j
    (yj - ly) * (yj - py) < 0 ? (
      t = (yj - ly) / (py - ly);
      bxing(fold(lx + t * (px - lx), roomW), fold(lz + t * (pz - lz), roomH), t * dtot, (1 - t) * dtot);
      gE *= bxE; bxF > 0 ? ( gF += 1 / sqr(bxF); gM *= bxM; gN = 1; ); gC += bxC;
      ncr += 1; crX = lx + t * (px - lx); crZ = lz + t * (pz - lz); crY = yj; crV = bxV; crEx = bxEx; crEz = bxEz;
    );
    j += 1;
  );
  btE = gE; btF = gF > 0 ? 1 / sqrt(gF) : 0; btM = gN ? gM : 0; btC = gC;
  // One crossing that is (partly) bent around an edge: the listener hears it from that edge, and the way is longer.
  // -> apparent source = on the line listener -> edge, at the length of the bent path
  btV = 0;
  ncr == 1 && crV > 0.001 ? (
    vpx = crX + crV * foldsg(crX, roomW) * (crEx - fold(crX, roomW));
    vpz = crZ + crV * foldsg(crZ, roomH) * (crEz - fold(crZ, roomH));
    dv1 = sqrt(sqr(vpx - lx) + sqr(crY - ly) + sqr(vpz - lz)); dv2 = sqrt(sqr(px - vpx) + sqr(py - crY) + sqr(pz - vpz));
    dv1 > 0.001 ? ( btV = 1; btVx = lx + (vpx - lx) * (dv1 + dv2) / dv1; btVy = ly + (crY - ly) * (dv1 + dv2) / dv1; btVz = lz + (vpz - lz) * (dv1 + dv2) / dv1; );
  );
);

// stereo position: constant-power pan with a limited side (see below) from lat = -1 … +1 (lateral component, front / back folded) -> gains pgL / pgR.
// Until this was the TANGENT LAW for loudspeakers at +-30 degrees: a source 10 degrees right was heard 10 degrees right between the speakers.
// Beyond the speakers nothing can be shown, so the angle was compressed smoothly into the base (30 deg -> 23 deg, 60 deg -> 29 deg, 90 deg -> hard).
// (before 0.176: sine law, which pulled every source toward the center - 30 degrees came out at 13)
function pang(lat) local(pp)
(
// constant-power pan with a LIMITED side: a sound straight to the side keeps 15 dB less in the other channel, not
  // silence (two ears on one head never hear nothing). Before: tangent law for 30-degree speakers - everything beyond
  // about 50 degrees sat in one channel alone, and a circling source jumped from one side to the other
  pp = min(1, max(-1, lat)) * 0.78;   // 78 % = 15 dB at the side, 9 dB at 45 degrees - the highs get more from the head shadow (19 dB broadband) // +-1 -> +-86 % of the pot = 19 dB at the side, 10 dB at 45 degrees (15 / 7 dB - too little direction)
  pgL = cos((pp + 1) * 0.78539816); pgR = sin((pp + 1) * 0.78539816);
);
// image-source tap: delay, distance gain, air / wall absorption and the stereo position (pan law, see pang)
// SOURCE DIRECTIVITY: a voice or a loudspeaker radiates mostly forward, the more the higher the frequency (directivity
// index of the human voice about 1.5 dB at 250 Hz, 3 dB at 1 kHz, 6 dB at 4 kHz). The source faces the listener; every
// reflection leaves it in another direction (the image path mirrored back) and gets its level and its highs from that
// angle: first-order pattern 0.65 + 0.35 cos (Q = 2.2 in the mids, back -10 dB) and a low-pass that closes towards the back.
// The diffuse tail gets the total power (1 / Q) and is a bit darker. Result: near the source the bright direct sound
// dominates, further away the darker room - distance is heard as in a real room (before: an omnidirectional source).
// (QDIR, the pattern and the source axis come from compute_geo: per source model, turned by SOURCE ANGLE)
QDIR = 1 / (0.65 * 0.65 + 0.35 * 0.35 / 3); lastdmd = -1; lastair = -1;
dirA = 0.65; dirN = 1; dirFl = 0.25; dirHb = 2500; dirShG = 0.5; dirBG = 0.29; srAx = 0; srAy = 1; srAz = 0;
// Source directivity gain for cosine c of the angle off axis: (a + (1-a) c)^n, never below the floor dirFl.
function dpat(c) ( max(dirFl, max(0, dirA + (1 - dirA) * c) ^ dirN); );
// kind: 0 = direct sound, 1 = image source, 2 = early cluster (diffuse: no directivity, the tail's 1 / Q already holds it)
function settap(k, ix, iy, iz, refl, hff, kind, trE, trF, trM)
local(ddx, ddy, ddz, hd, d3, dist, rel, lat, fwd, gg, fc, emx, emy, emz, eln, cse, isdir, dpv, fdir, fdst, itd, fshd, shA, dEL, dER)
(
  isdir = kind == 0;
  ddx = ix - lx; ddy = iy - ly; ddz = iz - lz;
  hd   = sqrt(ddx*ddx + ddy*ddy);
  d3   = sqrt(ddx*ddx + ddy*ddy + ddz*ddz);
  dist = max(0.25, d3);
  rel  = (hd > 0.001) ? atan2(ddy, ddx) - face : 0;
  lat  = (d3 > 0.001) ? min(1, max(-1, sin(rel) * hd / d3)) : 0;      // lateral component (+ = right)
  fwd  = (d3 > 0.001) ? cos(rel) * hd / d3 : 1;                       // front component (- = behind)
  gg   = refl * sqrt(trE) / dist;
// GROUND (free field): the floor image gets more weight than the plain 1 / distance law gives it (fourth root of the path ratio) -
  // close up, the ground reflection is then clearly audible (comb of hard ground, duller grass / snow); far away the law is the same
  isfree && slider119 >= 0.5 && tIZ[k] == -1 && tIX[k] == 0 && tIY[k] == 0 ? (
    fdst = max(0.25, sqrt(sqr(sx - lx) + sqr(sy - ly) + sqr(sz - lz)));
    gg = refl * sqrt(trE) / fdst * (fdst / dist) ^ 0.25 * exp(vopt[69] * 0.11512925);   // x Ground level (dB, vopt[69])
  );
  // directivity: angle between the direct path and the direction this path leaves the source
  cse = 1;
  kind < 2 ? (
    // direction this path leaves the source (the image path mirrored back) against the source axis
    emx = -ddx * ((abs(tIX[k]) % 2) ? -1 : 1); emy = -ddy * ((abs(tIY[k]) % 2) ? -1 : 1); emz = -ddz * ((abs(tIZ[k]) % 2) ? -1 : 1);
    rflOv ? ( emx = rflEx; emy = rflEy; emz = rflEz; );   // reflector: the path leaves the source towards the reflection point
    eln = sqrt(emx * emx + emy * emy + emz * emz);
    cse = eln > 0.001 ? min(1, max(-1, (emx * srAx + emy * srAy + emz * srAz) / eln)) : 1;
    dpv = dpat(cse);
    kind == 1 ? ( ceO += sqr(gg); ceD += sqr(gg * dpv); ceU += sqr(gg * dpv) * (1 - cse) / 2; );   // for the early cluster
    gg *= dpv;
  );
  // air absorption (ISO 9613-1, temperature + humidity): one-pole matched at 4 kHz; the walls' own highs per bounce (hff =
  // amplitude at 4 kHz / mid of all its bounces) as a second one-pole in cascade
  fc   = 4000 / sqrt(max(0.000001, 10 ^ (airdb4k * dist / 10) - 1));
// DISTANCE: beyond the ISO air absorption (small indoors) a gentle, perceptual high loss with distance - the way a
  // voice or a step sounds duller the further away it is (1 m: none, 4 m: -3 dB at 10 kHz, 16 m: at 5 kHz)
  kind < 2 ? ( fdst = 20000 / sqrt(max(1, dist)); fc = fc * fdst / sqrt(fc * fc + fdst * fdst); );
  !isdir && hff < 0.999 ? fc = 1 / sqrt(1 / sqr(fc) + (1 / sqr(max(0.05, hff)) - 1) / sqr(4000));
  fc   = isdir ? max(1000, fc) : max(500, fc);
  // scattering: no real wall is a perfect mirror for short waves - with every bounce part of the highs leaves the mirror
  // path and joins the reverb (about -1 dB at 4 kHz, -4.5 dB at 10 kHz per bounce)
  !isdir && tnb > 0 ? fc = max(800, 1 / sqrt(1 / sqr(fc) + tnb / sqr(7000)));
  fc   = min(0.45 * srate, fc * (1 - 0.35 * max(0, -fwd)));            // from behind: a bit darker
  // directivity of the highs: a low-pass whose corner falls the more the path leaves the source sideways / backwards
  // (dirHb = corner straight behind, twice that at 90 degrees, no effect on axis) - in Hz, the same at every sample rate
  kind < 2 ? ( fdir = dirHb / max(0.02, (1 - cse) / 2); fc = fc * fdir / sqrt(fc * fc + fdir * fdir); );
  kind == 2 && clFd > 0 ? ( fc = fc * clFd / sqrt(fc * fc + clFd * clFd); );   // early cluster: the mean of the reflections
  trF > 0 ? fc = fc * trF / sqrt(fc * fc + trF * trF);              // barrier: cascade of the two low-passes
  xA[k]  = exp(-2 * $pi * fc / srate);
  xSM[k]  = trF > 0 ? trM : 0;
  pang(lat); xGL[k] = gg * pgL; xGR[k] = gg * pgR;
// NEAR-FIELD ILD: close to the head the two ears are at clearly different distances (8.75 cm from the center each):
  // a source 30 cm to the right is 21 cm from the right ear and 39 cm from the left - 5 dB more level difference from the
  // distance alone, on top of pan law and head shadow. Far away both ratios go to 1 (nothing changes beyond ~2 m)
  k < NIMG ? ( dER = sqrt(dist * dist + 0.00766 - 0.175 * dist * lat); dEL = sqrt(dist * dist + 0.00766 + 0.175 * dist * lat);
    xGL[k] *= dist / max(0.05, dEL); xGR[k] *= dist / max(0.05, dER); );
  xDL[k] = xDR[k] = min(MAXIN - 4, dist / C_SND * srate);
// INTERAURAL TIME DIFFERENCE (also the second order, taps 7-24): the far ear hears the direct sound and the reflections later (up to 0.66 ms,
  // Woodworth for a head of 8.75 cm) - with the level difference this is what places a sound in the stereo field and makes a
  // movement sound like a movement
  k < NIMG ? ( itd = 0.00066 * lat * srate * 0.5; xDL[k] = min(MAXIN - 4, xDL[k] + itd); xDR[k] = min(MAXIN - 4, max(1, xDR[k] - itd));
// HEAD SHADOW: the far ear loses the highs - the head is in the way of short waves, not of long ones (a real head:
    // about 5 dB at 500 Hz, 20 dB at 8 kHz straight from the side). A one-pole low-pass on the far ear whose corner falls
    // with the lateral angle (20 kHz ahead, 3.5 kHz at 45 degrees, 2.2 kHz at the side) - the level difference is now
    // frequency dependent, as it is between two ears, and a sound moving past gets its direction from the highs as well
    fshd = 20000 / (1 + 8 * abs(lat) ^ 1.5); shA = exp(-2 * $pi * min(0.45 * srate, fshd) / srate);
    lat > 0 ? ( xHL[k] = shA; xHR[k] = 0; ) : ( xHL[k] = 0; xHR[k] = shA; );
  );
// PROXIMITY: the source ON the listener (linked) = the input as it is - gain 1 per channel, no ear timing, no head
  // shadow, no air / distance filter, no near-field lows, no bass steering; pulled apart, these cues fade in over the first
  // meter (smooth, no step). Every parameter goes to its neutral value on its own - no blend of two signals, so no comb.
  // Delay: 1 sample + the real distance (+ the ear timing, never below 1 sample: the cubic read needs one sample of past)
  isdir ? (
    pbD = min(1, d3 / 1.0); pbD = pbD * pbD * (3 - 2 * pbD);
    xGL[k] = 1 + (xGL[k] - 1) * pbD; xGR[k] = 1 + (xGR[k] - 1) * pbD;
    xA[k] *= pbD; xHL[k] *= pbD; xHR[k] *= pbD;
    itd = 0.00066 * lat * srate * 0.5 * pbD;
    xDL[k] = min(MAXIN - 4, 1 + d3 / C_SND * srate + itd + abs(itd)); xDR[k] = min(MAXIN - 4, 1 + d3 / C_SND * srate - itd + abs(itd));
  );
  tapT[k] = dist / C_SND * 1000;
  tapG[k] = gg;
);

// image coordinate along one axis (room length L, source at s) for image index i; wall hits near (0) / far (L)
function imgc(i, L, sp) ( (abs(i) % 2) ? (i + 1) * L - sp : i * L + sp; );
// Number of hits on the near wall for image index i (floor(i/2) for i>=0, ceil(-i/2) for negative).
function hitN(i) ( i >= 0 ? floor(i / 2) : ceil(-i / 2); );
// Number of hits on the far wall for image index i (ceil(i/2) for i>=0, floor(-i/2) for negative).
function hitF(i) ( i >= 0 ? ceil(i / 2) : floor(-i / 2); );

// Recomputes all room geometry and acoustics from the sliders: sizes, absorption, RT60 per band, image sources, taps, filters and loudness trims.
function compute_geo()
local(hfW, hfE, hfN, hfS, hfFl, fcturn, k, ddx, ddy, g0, gh, lsum, gm, ghm, gkey, base, ds, hfr, lfP, lfQ, rtt, rpx, rpy, rpz, rhit, bfl, bcmp, mg, dirg, fcg, pre, hr, hc, hB, ix, iy, iz, rf, hf, px, py, pz, pV, pS2, pA, qV, qS, qA, rtL, rtQ, rcS, rcP, bshare, pcx, pcz, pD, AeP, AeQ, base2, gkey2, plat, dq, clt, clw, cldist, clhf, ecW, clA, clB, srL, srA0, srAng, airkey, dmd)
(
  rs = min(4, max(0.25, slider31));
  roomW = max(0.8, slider2) * rs; roomD = max(0.8, slider3) * rs; roomH = max(0.8, slider4) * rs;
  isfree = slider1 >= 0.5;
  aN = min(1, max(0.01, slider5)); aE = min(1, max(0.01, slider6));
  aS = min(1, max(0.01, slider7)); aW = min(1, max(0.01, slider8));
  aF = min(1, max(0.01, slider9));
  // REFLECTIVITY: how much all surfaces reflect, relative to their materials - 200 % = half the absorption (livelier, longer),
  // 50 % = twice the absorption (drier, shorter); walls, floor + ceiling, back part alike, so the room keeps its character
  rflf = 100 / min(400, max(25, slider62));
  aN = min(0.99, max(0.005, aN * rflf)); aE = min(0.99, max(0.005, aE * rflf)); aS = min(0.99, max(0.005, aS * rflf));
  aW = min(0.99, max(0.005, aW * rflf)); aF = min(0.99, max(0.005, aF * rflf));
  // air: absorption per band from temperature and humidity (energy constants m for RT60, dB/m at 4 kHz for the reflections)
  airT = 20; airH = 50;                                                 // air: 20 C, 50 % (ISO 9613-1)
  airkey = airT * 1000 + airH;
  airkey != lastair ? (
    airm125 = airdb(125, airT, airH) / 4.343; airm5 = airdb(500, airT, airH) / 4.343;
    airdb4k = airdb(4000, airT, airH); airm4k = airdb4k / 4.343; lastair = airkey;
    k = 0; loop(8, airOB[k] = airdb(62.5 * 2 ^ k, airT, airH) / 4.343; k += 1; );   // the 8 octave bands once (they were computed on every geometry change)
  );
  // source directivity of the selected model
  dmd = slider85 >= 0.5 ? min(12, max(0, floor(slider35 + 0.5))) : 0;   // source model off: radiates like the Original
  dmd != lastdmd ? (
    dirA = dtab[dmd * 5]; dirN = dtab[dmd * 5 + 1]; dirFl = dtab[dmd * 5 + 2]; dirHb = dtab[dmd * 5 + 3]; dirShG = dtab[dmd * 5 + 4];
    // Q = 2 / integral of p^2 sin(theta) over 0 … pi (numerically)
    dq = 0; k = 0; loop(90, dq += sqr(dpat(cos((k + 0.5) * $pi / 90))) * sin((k + 0.5) * $pi / 90) * $pi / 90; k += 1; );
    QDIR = 2 / max(0.01, dq);
    dirBG = max(0, sqrt(QDIR / 1.3) - 1);                           // nearly round in the bass: give the power back below ~250 Hz
    lastdmd = dmd;
  );
  mg = 0.1;
// MOTION: the pattern position blended in (mtE = 0 … 1: the glide in / out), the sliders stay the center
  sx = slider10 * rs; sy = slider11 * rs; sz = slider12; lx = slider13 * rs; ly = slider14 * rs; lz = slider15;
  mtSon ? ( sx += (mtSX * rs - sx) * mtE; sy += (mtSY * rs - sy) * mtE; sz += (mtSZ - sz) * mtE; );
  mtLon ? ( lx += (mtLX * rs - lx) * mtE; ly += (mtLY * rs - ly) * mtE; lz += (mtLZ - lz) * mtE; );
  sx = min(max(sx, mg), roomW - mg); sy = min(max(sy, mg), roomD - mg);
  // heights: the acoustic center of an ear or a loudspeaker lying on the floor is still about 10 cm above it. Exactly on the
  // floor the direct sound and its floor reflection would arrive at the very same instant and double at EVERY frequency -
  // that only happens for a microphone built into the surface
  sz = min(max(sz, mg), roomH - mg);                 // heights are absolute meters: SIZE scales the room, not the people in it
  lx = min(max(lx, mg), roomW - mg); ly = min(max(ly, mg), roomD - mg);
  lz = min(max(lz, mg), roomH - mg);

  // SOURCE AXIS, turned by SOURCE ANGLE around the vertical (+ = to the source's left).
  // Fixed (default): the source points in one direction of the room (0 = towards the front wall), like a real loudspeaker
  // or actor - walk behind it and you hear its back. Listener: it always aims at the listener (0 = at the listener).
  slider63 >= 0.5 ? ( srAx = 0; srAy = 1; srAz = 0; ) : (
    srAx = lx - sx; srAy = ly - sy; srAz = lz - sz; srL = sqrt(srAx * srAx + srAy * srAy + srAz * srAz);
    srL > 0.001 ? ( srAx /= srL; srAy /= srL; srAz /= srL; ) : ( srAx = 0; srAy = 1; srAz = 0; );
  );
  srAng = min(180, max(-180, slider61)) * $pi / 180;
  srA0 = srAx; srAx = srA0 * cos(srAng) - srAy * sin(srAng); srAy = srA0 * sin(srAng) + srAy * cos(srAng);
  slider16 < 0.5 ? (
    ddx = sx - lx; ddy = sy - ly;
    face = (abs(ddx) + abs(ddy) > 0.0001) ? atan2(ddy, ddx) : -$pi / 2;
    fcturn = 1;                                                       // (with a barrier between: turned to the opening, below)
  ) : (
    face = (slider17 - 90) * $pi / 180;
  );

  // ---- barrier geometry (absolute meters, scaled with SIZE like all positions) ----
  bt = bgoff ? 0 : floor(slider41 + 0.5); bon = bt > 0;                      // also in the free field: a free-standing wall W x H
  ybm = min(0.3, 0.25 * roomD);                                     // margin to back / front wall (smaller in tiny rooms)
  yb = min(max(slider42 * rs, ybm), roomD - ybm);
  bopen = min(1, max(0, slider43 / 100)); bdamp = min(70, max(0, slider44));
  // DAMP is the installed value (R'w at 500 Hz: gaps and side paths included) - what you set is what you get.
  // Around a door / window: an interior brick wall, R'w 45 dB
  bmixk = min(1, max(0, slider92 / 100));                               // MIX of the barrier: share of its damping
  dEl = bdamp * bmixk; dWall = (bt == 1 ? bdamp : 45) * bmixk;
  // SHELF of the partition (see bxing): corner + share of the highs, fitted to typical measured sound transmission loss
  // (125 Hz - 4 kHz, relative to 500 Hz) of the three materials of each type - DAMP between two of them = in between (log),
  // outside = the nearest. Relative to 500 Hz (125 / 250 / 1k / 2k / 4k):
  //   drywall +10 / +5 / -4 / -5 / -6   brick +9 / +5 / -6 / -10 / -13   concrete +11 / +6 / -6 / -11 / -14
  //   single glazing +7 / +4 / -3 / -4 / -4   double +6 / +4 / -4 / -8 / -10   acoustic glazing +8 / +5 / -5 / -9 / -10
  //   doors without seals (hollow-core, heavy wood): +5 / +2 / -1 / -2 / -2 - the gaps pass the highs, so a closed
  //   ordinary door is hardly darker above 1 kHz;   acoustic door (seals) +9 / +5 / -4 / -6 / -6
  // per type (bt 3 door, 2 window, else wall; materials in the order of the legend above): bsh0..2 = DAMP (dB) of the 3 materials, bf0..2 = shelf corner (Hz), bm0..2 = share of the highs that passes above the corner (linear)
  bt == 3 ? ( bsh0 = 17; bsh1 = 25; bsh2 = 37; bf0 = 158; bf1 = 93; bf2 = 113; bm0 = 0.378; bm1 = 0.323; bm2 = 0.119; ) : (
  bt == 2 ? ( bsh0 = 24; bsh1 = 30; bsh2 = 39; bf0 = 192; bf1 = 262; bf2 = 146; bm0 = 0.130; bm1 = 0.050; bm2 = 0.080; ) : (
              bsh0 = 38; bsh1 = 45; bsh2 = 55; bf0 = 30; bf1 = 133; bf2 = 65; bm0 = 0.020; bm1 = 0.045; bm2 = 0.020; ); );
  // COINCIDENCE (glass, gypsum board): around the frequency where the bending waves of the pane match the sound in air the
  // partition lets clearly more through - a peak on the transmitted sound (frequency, Q, gain dB) per material:
  //   single glazing 4 mm 4.5 kHz +12 dB (Q 3), double glazing 4 kHz +9 dB (Q 3), laminated: damped (none);
  //   drywall 2.5 kHz +7 dB (Q 1.2); masonry, concrete and doors: below the audio band / no effect
  //   (fitted with the shelf to the transmission-loss curves: single glazing 125 Hz - 4 kHz within 0.3 dB, double within 2.3 dB)
  bt == 2 ? ( pf0 = 4500; pq0 = 3; pg0 = 12; pf1 = 4000; pq1 = 3; pg1 = 9; pf2 = 4000; pq2 = 3; pg2 = 0; ) : (
  bt == 1 ? ( pf0 = 2500; pq0 = 1.2; pg0 = 7; pf1 = 2500; pq1 = 1.2; pg1 = 0; pf2 = 2500; pq2 = 1.2; pg2 = 0; ) : (
              pf0 = pf1 = pf2 = 3000; pq0 = pq1 = pq2 = 1; pg0 = pg1 = pg2 = 0; ); );
  // bsht = position between the 3 materials (0 = first, 1 = second, 2 = third) from DAMP; below, corner and highs share are interpolated geometrically, peak Q and dB linearly
  bsht = bdamp <= bsh0 ? 0 : (bdamp >= bsh2 ? 2 : (bdamp < bsh1 ? (bdamp - bsh0) / (bsh1 - bsh0) : 1 + (bdamp - bsh1) / (bsh2 - bsh1)));
  bsht < 1 ? ( bfEl = bf0 * (bf1 / bf0) ^ bsht; bmEl = bm0 * (bm1 / bm0) ^ bsht; ) : ( bfEl = bf1 * (bf2 / bf1) ^ (bsht - 1); bmEl = bm1 * (bm2 / bm1) ^ (bsht - 1); );
  bsht < 1 ? ( bpkF = pf0 * (pf1 / pf0) ^ bsht; bpkQ = pq0 + (pq1 - pq0) * bsht; bpkG = pg0 + (pg1 - pg0) * bsht; )
           : ( bpkF = pf1 * (pf2 / pf1) ^ (bsht - 1); bpkQ = pq1 + (pq2 - pq1) * (bsht - 1); bpkG = pg1 + (pg2 - pg1) * (bsht - 1); );
  bt == 1 ? ( bfWl = bfEl; bmWl = bmEl; ) : ( bfWl = 133; bmWl = 0.045; );   // the wall itself / the brick wall around a door or window
  bmixk < 1 ? ( bmEl = 1 - (1 - bmEl) * bmixk; bmWl = 1 - (1 - bmWl) * bmixk; ); bpkG *= bmixk;            // mix < 100 %: less of the highs held back
  // BARRIER SIZE: width x height of the partition (0 = wall to wall / floor to ceiling). It stands on the floor, centered at
  // its X position (the door / window stays inside it). Beside and above it the sound passes freely and bends around its
  // free edges; the gap also couples the two parts of the room (opening area).
  brW = slider95 > 0.001 ? min(roomW, max(0.3, slider95 * rs)) : roomW; brH = slider96 > 0.001 ? min(roomH, max(0.3, slider96 * rs)) : roomH;
  brC = min(max(slider45 * rs, brW / 2), roomW - brW / 2); brX0 = brC - brW / 2; brX1 = brC + brW / 2;
  // DOOR / WINDOW SIZE (slider 97, 50 … 200 %): the door 0.9 x 2.1 m grows from the floor, the window 1.2 x 1.2 m around its
  // center 1.5 m above the floor - the opening, the coupling of the two parts and the sound through it follow
  besz = min(2, max(0.5, slider97 / 100));
  bt == 3 ? ( bew = 0.9 * besz; bez0 = 0; bez1 = min(2.1 * besz, brH - 0.05); ) : (
    bt == 2 ? ( bew = 1.2 * besz; bez0 = min(1.5 - 0.6 * besz, brH - 0.05 - 0.6 * besz); bez1 = min(bez0 + 1.2 * besz, brH - 0.05); ) : ( bew = 0; bez0 = bez1 = 0; );
  );
  bew = min(bew, brW - 0.1); bez0 = max(0, bez0); bez1 = max(bez0 + 0.1, bez1);   // element never larger than the wall (tiny rooms)
  bex = min(max(slider45 * rs, brX0 + bew / 2 + 0.05), brX1 - bew / 2 - 0.05); bex0 = bex - bew / 2; bex1 = bex + bew / 2;
  brHX = brX1 - 0.08 * brW;                                          // depth handle (top edge, right)
  brEon = bew > 0 || brW < roomW - 0.01; brEX = bew > 0 ? bex : brC; brEZ = bew > 0 ? bez1 : brH;   // side handle: element or partition
  brMG = brW < roomW - 0.01 ? brW / 2 : bew / 2 + 0.05;            // how close the side handle gets to the walls
  sideS = sy >= yb; sideL = ly >= yb; bsame = sideS == sideL;
  // opening: door = gap between frame and leaf edge (chord 2 w sin(phi/2), leaf turns on the left hinge, up to 90 degrees)
  //          window = two casement sashes, hinged left and right (each: chord w/2 x 2 sin(phi/2), up to its width)
  bphi = bopen * 0.5 * $pi;
  bt == 3 ? ( ax1 = bex1; ax0 = bex1 - min(bew, 2 * bew * sin(0.5 * bphi)); az0 = bez0; az1 = bez1; ) : (
    bt == 2 ? ( ax0 = bex - min(bew / 2, bew * sin(0.5 * bphi)); ax1 = 2 * bex - ax0; az0 = bez0; az1 = bez1; ) : ( ax0 = ax1 = az0 = az1 = 0; );
  );
  aarea = bon ? max(0, (ax1 - ax0) * (az1 - az0)) : 0;
  asz = min(1, aarea / 0.49);                                   // openings smaller than ~0.7 x 0.7 m pass less
  // gap around a smaller partition (left, right, above): open area, its center (energy-weighted with the opening)
  brGL = bon ? brX0 * roomH : 0; brGR = bon ? (roomW - brX1) * roomH : 0; brGT = bon ? brW * (roomH - brH) : 0; brGA = brGL + brGR + brGT;
// a gap narrower than the wavelength couples less - per gap w f / c at 500 Hz (a 5 cm slit along the ceiling: 7 %, 40 cm: 58 %)
  brGSl = min(1, brX0 * 500 / C_SND); brGSr = min(1, (roomW - brX1) * 500 / C_SND); brGSt = min(1, (roomH - brH) * 500 / C_SND);
  brOA = aarea + brGA;
  brOA > 0.0001 ? ( brOX = (aarea * 0.5 * (ax0 + ax1) + brGL * brX0 / 2 + brGR * (brX1 + roomW) / 2 + brGT * brC) / brOA;
                    brOZ = (aarea * 0.5 * (az0 + az1) + (brGL + brGR) * roomH / 2 + brGT * (brH + roomH) / 2) / brOA; ) : ( brOX = bex; brOZ = roomH / 2; );
  // barrier surface: absorption seen from either side (the opening absorbs everything that goes through it; closed door leaf 0.10, everything else closed 0.04)
  bAel = bew * (bez1 - bez0); bAw = max(0, brW * brH - bAel);
  abB = min(1, (aarea + brGA + (bAel - aarea) * (bt == 3 ? 0.10 : 0.04) + bAw * 0.04) / (roomW * roomH));
  bmixk2 = min(1, max(0, slider92 / 100)); bmixk2 < 1 ? ( abB = 1 - (1 - abB) * bmixk2; );   // barrier mix: less of its reflection
  // coupling area for the reverberant energy (opening + transmission of the closed parts)
  bEop = aarea * asz + brGL * brGSl + brGR * brGSr + brGT * brGSt; bEel = (bAel - aarea) * 10 ^ (-dEl / 10); bEwl = bAw * 10 ^ (-dWall / 10); bEmat = bEel + bEwl;
  bcA = bEop + bEmat;                                            // coupling area at 500 Hz (opening + closed parts)
  // FACING = Source with a barrier in between: the listener turns to where the direct sound comes from (the opening or the
  // edge it bends around), like a person who hears a voice through a door - the direct sound stays in the center
  fcturn && bon && !bsame ? (
    btrans(sx, sy, sz);
    btV ? ( ddx = btVx - lx; ddy = btVy - ly; abs(ddx) + abs(ddy) > 0.0001 ? face = atan2(ddy, ddx); );
  );
  fcturn = 0;

  // reverberation time RT60 = 0.161 * V / A (Sabine form; the Eyring version with air absorption follows below)
  roomV = roomW * roomD * roomH;
  roomS = 2 * (roomW * roomD + roomW * roomH + roomD * roomH);
  roomA = roomW * roomH * (aN + aS) + roomD * roomH * (aE + aW) + 2 * roomW * roomD * aF;
  // Eyring instead of Sabine (Sabine overestimates RT60 in absorbent rooms), plus air absorption 4mV
  // (air: m per band from temperature and humidity, ISO 9613-1 - at 20 C / 50 %: 0.00063 /m at 500 Hz, 0.0068 /m at 4 kHz)
  abar = min(0.95, max(0.005, roomA / roomS));
  roomAe = -roomS * log(1 - abar);
  rtRoom = min(20, max(0.08, 0.161 * roomV / (roomAe + 4 * airm5 * roomV)));   // physical RT60 of the room (mid band)
  // highs: RT60 at 4 kHz from the materials' own high-frequency absorption and the air (temperature, humidity)
  hfrP = hfrEy(roomV, roomS, roomA, areaB(1, roomW, roomD, roomH, aN, aS, aE, aW, aF)); rbsel = 0;
  // image room along the depth: the whole room, or the listener's part when the barrier separates nothing
  // materials per part: the back part (back wall .. barrier) may have its own material, the front part keeps the room's
  mB = bon && !isfree ? floor(slider46 + 0.5) : 0; aBm = min(0.99, max(0.005, bkA(mB) * rflf));
  kN = mB ? aBm : aN; kE = mB ? aBm : aE; kW = mB ? aBm : aW; kF = mB ? aBm : aF;
  Dp = sideL ? roomD - yb : yb; qD = roomD - Dp;                 // listener's part P, other part Q
  pNy = sideL ? abB : kN; pSy = sideL ? aS : abB; pwE = sideL ? aE : kE; pwW = sideL ? aW : kW; pwF = sideL ? aF : kF;
  qNy = sideL ? kN : abB; qSy = sideL ? abB : aS; qwE = sideL ? kE : aE; qwW = sideL ? kW : aW; qwF = sideL ? kF : aF;
  // image room: the listener's part (same side), or the whole room with per-path barrier checks (different sides)
  bon && bsame ? ( yo = sideL ? yb : 0; aNy = pNy; aSy = pSy; iaE = pwE; iaW = pwW; iaF = pwF; ) : (
    bon ? ( yo = 0; Dp = roomD; aNy = kN; aSy = aS; iaE = pwE; iaW = pwW; iaF = pwF; )
        : ( yo = 0; Dp = roomD; aNy = aN; aSy = aS; iaE = aE; iaW = aW; iaF = aF; );
  );
  bcpl = 0; xLLP = 0; xLM = 0; bnet = 0; bspl = 0; bcmpE = 1; bpkGe = 0;
  bon ? (
    pD = roomD - qD;
    pV = roomW * pD * roomH; pS2 = 2 * (roomW * pD + roomW * roomH + pD * roomH);
    pA = roomW * roomH * (pNy + pSy) + pD * roomH * (pwE + pwW) + 2 * roomW * pD * pwF;
    qV = roomW * qD * roomH; qS = 2 * (roomW * qD + roomW * roomH + qD * roomH);
    qA = roomW * roomH * (qNy + qSy) + qD * roomH * (qwE + qwW) + 2 * roomW * qD * qwF;
    rtL = rtEy(pV, pS2, pA); rtQ = rtEy(qV, qS, qA);
    AeP = -pS2 * log(1 - min(0.95, pA / pS2)); AeQ = -qS * log(1 - min(0.95, qA / qS));
    bshare = bEop / max(0.000001, bcA);                                   // opening share of the coupling (keeps the highs)
    bpkGe = bpkG * (1 - min(1, max(0, bshare))) * (bt == 1 ? 1 : min(1, bEel / max(0.000000000001, bEmat)));   // coincidence: only through the element's material
    // tail through the closed parts: same shelf as the direct sound (energy-weighted element / wall), open = unfiltered
    bfm = (bEel * bfEl + bEwl * bfWl) / max(0.000000000001, bEmat); bmm = (bEel * bmEl + bEwl * bmWl) / max(0.000000000001, bEmat);
    bfl = bfm + bshare * (20000 - bfm); xLM = (1 - bshare) * bmm;
    bcmp = (1 + sqr(500 / bfl)) / (1 + sqr(xLM * 500 / bfl));    // its level loss at 500 Hz, given back
    bcmp = min(bcmp, max(1, (bAel - aarea + bAw) / max(0.000001, bEmat)));   // ... but never more than the closed parts can pass
    bcmpE = bcmp;
    bsame ? (
      rtRoom = rtL; roomVe = pV; mfpE = 4 * pV / pS2; hfrP = hfrEy(pV, pS2, pA, areaB(1, roomW, pD, roomH, pNy, pSy, pwE, pwW, pwF)); rbsel = 1;
      // neighbor room: excited through the barrier, heard back through it (2nd network, own RT60)
      rcP = 0.057 * sqrt(pV / rtL);
      bE2 = (1 / (rcP * rcP)) * bcA * bcA / ((AeQ + bcA) * (AeP + bcA)) * bcmp / QDIR;
      bspl = 10 * log10(max(0.000000000001, bcA * bcA / ((AeQ + bcA) * (AeP + bcA))));   // shown in BARRIER: there and back
      bnet = bE2 > 0.00000001 && qD > 0.25;
      rtQd = rtQ; hfr2 = hfrEy(qV, qS, qA, areaB(1, roomW, qD, roomH, qNy, qSy, qwE, qwW, qwF)); mfpQ = 4 * qV / qS;
      xLLP2 = exp(-2 * $pi * bfl / srate);
    ) : (
      // two coupled rooms: the listener hears the source room's reverberant field through the coupling area;
      // the slower room sets the decay
      rtRoom = max(rtL, rtQ); roomVe = rtQ >= rtL ? qV : pV;
      mfpE = 4 * pV / pS2;
      hfrP = rtQ >= rtL ? hfrEy(qV, qS, qA, areaB(1, roomW, qD, roomH, qNy, qSy, qwE, qwW, qwF)) : hfrEy(pV, pS2, pA, areaB(1, roomW, pD, roomH, pNy, pSy, pwE, pwW, pwF)); rbsel = rtQ >= rtL ? 2 : 1;
      rcS = 0.057 * sqrt(qV / rtQ);
      bcpl = (1 / (rcS * rcS)) * bcA / (AeP + bcA) * bcmp;
      // ... and on where the source stands in its room. The barrier is driven by the reverberant field of that room
      // (falling slowly with distance, Barron: exp(-0.04 r / T)) PLUS the direct sound that hits it - the share of the
      // source's power that falls on the barrier (solid angle of a disc of the same area, seen from the source).
      // Relative to the reverberant part that is  (Omega / 4 pi) * A / S  (A = absorption area of the source room, S = barrier area).
      bsh = abs(sy - yb); bsw = brW * brH;
      bsd = bsh * sqrt(sqr(lx - sx) + sqr(ly - sy) + sqr(lz - sz)) / max(0.001, abs(ly - sy));     // source -> point where the straight line crosses
      bsrc = exp(-0.04 * bsd / rtQ) + 0.5 * (1 - bsh / sqrt(bsh * bsh + bsw / $pi)) * AeQ / bsw;
      bcpl *= bsrc;
      // level difference between the two rooms at 500 Hz, D = -10 log(S tau / (A + S tau)): it follows the damping AND the
      // size of the wall, the opening and the absorption of the listener's room - shown in the BARRIER section
      bspl = 10 * log10(max(0.000000000001, bsrc * bcA / (AeP + bcA)));
      xLLP = exp(-2 * $pi * bfl / srate);
    );
  ) : ( roomVe = roomV; mfpE = 4 * roomV / roomS; );
  // bass decay: full room, listener part, or the slower of two coupled parts
  lfrP = lfrEy(roomV, roomS, roomA, areaB(0, roomW, roomD, roomH, aN, aS, aE, aW, aF));
  bon ? (
    lfP = lfrEy(pV, pS2, pA, areaB(0, roomW, pD, roomH, pNy, pSy, pwE, pwW, pwF));
    lfQ = lfrEy(qV, qS, qA, areaB(0, roomW, qD, roomH, qNy, qSy, qwE, qwW, qwF));
    lfrP = bsame ? lfP : (rtQ >= rtL ? lfQ : lfP);
    lfr2 = lfQ;
  );
  rt  = min(20, max(0.01, rtRoom * min(10, max(0.01, slider32)))); // DECAY knob scales it, max 20 s
  // BARRIER MIX below 100 %: decay, its bass and the size of the reverb lines move towards the room without the barrier
  bgblend ? ( rt = exp(bmixg * log(rt) + (1 - bmixg) * log(gRTo)); lfrP = lfrP * bmixg + gLFo * (1 - bmixg); );
  // the coincidence peak: on everything heard (other side) or on the neighbor room (same side)
  bpkM = bon && bpkGe > 0.05 && !bgoff ? (bsame ? 2 : 1) : 0;
  bpkM ? ( pkdesign(bpkC, min(0.45 * srate, bpkF), bpkGe, bpkQ); );
  rc  = bon && !bsame ? rcS : 0.057 * sqrt(roomVe / rt);
  rt2 = min(20, max(0.01, (bnet ? rtQd : 0.08) * min(10, max(0.01, slider32))));
  mfp = mfpE;
  bgblend ? ( mfp = mfp * bmixg + gMFo * (1 - bmixg); );
  rdir = max(0.25, sqrt(sqr(sx - lx) + sqr(sy - ly) + sqr(sz - lz)));

  // one tap per image source k (k = 0 is the direct sound, 1-6 first order, 7-24 second order)
  // image sources up to 2nd order: amplitude = product of sqrt(1 - alpha) over all bounces, highs per bounce
  ceO = ceD = ceU = 0;                                              // reflections: energy without / with directivity, how far off-axis
  hfW = hfrel(iaW); hfE = hfrel(iaE); hfN = hfrel(aNy); hfS = hfrel(aSy); hfFl = hfrelF(iaF);   // once per geometry (they were computed per image - 125 band evaluations)
  k = 0;
  loop(NIMG,
    ix = tIX[k]; iy = tIY[k]; iz = tIZ[k];
    rf = (1 - iaW) ^ (0.5 * hitN(ix)) * (1 - iaE) ^ (0.5 * hitF(ix)) * (1 - aNy) ^ (0.5 * hitN(iy)) * (1 - aSy) ^ (0.5 * hitF(iy))
       * (1 - iaF) ^ (0.5 * (hitN(iz) + hitF(iz)));
    hf = hfW ^ hitN(ix) * hfE ^ hitF(ix) * hfN ^ hitN(iy) * hfS ^ hitF(iy)
       * hfFl ^ (hitN(iz) + hitF(iz));                   // highs of this path relative to its mids (materials)
    tnb = abs(ix) + abs(iy) + abs(iz);                             // bounces of this image
    px = imgc(ix, roomW, sx); py = yo + imgc(iy, Dp, sy - yo); pz = imgc(iz, roomH, sz);
    bon && !bsame ? btrans(px, py, pz) : ( btE = 1; btF = 0; btM = 0; btC = 0; btV = 0; );      // source and listener on different sides
    k == 0 ? ( bdirE = btE; bdirC = btC; bdirT = bon && !bsame ? bxT : 0; );        // direct sound through the barrier: shown in the BARRIER section
// GROUND (free field): the floor image - hard ground (asphalt) reflects almost all, grass and snow absorb much
    isfree && slider119 >= 0.5 && iz == -1 && ix == 0 && iy == 0 ? ( gAg = slider119 < 1.5 ? 0 : (slider119 < 2.5 ? 1 : 2); rf = (gAg == 0 ? 0.95 : (gAg == 1 ? 0.55 : 0.4)) * exp(vopt[69] * 0.11512925); hf = gAg == 0 ? 1 : (gAg == 1 ? 0.45 : 0.08); );   // asphalt reflects almost all (deep comb), grass absorbs a lot and dulls, snow is soft and very dull
    isfree && k == 1 && bon && bsame ? (
      // free field: reflection from the barrier itself - only where the reflection point hits the closed barrier
      px = sx; py = 2 * yb - sy; pz = sz;
      rtt = (yb - ly) / (py - ly); rpx = lx + rtt * (px - lx); rpz = lz + rtt * (pz - lz);
      rhit = rpx >= brX0 && rpx <= brX1 && rpz >= 0 && rpz <= brH && !(aarea > 0.0001 && rpx >= ax0 && rpx <= ax1 && rpz >= az0 && rpz <= az1);
      rf = rhit ? sqrt(1 - min(0.99, abB)) : 0; hf = hfrel(abB);
    );
// REFLECTORS (free field): a facade, rock face, fence or hedge - a vertical plane (center, angle, length, height). The image
    // of the source in that plane gives the reflection; it only counts where the path from the image to the listener really hits
    // the plane inside the length and height of the reflector (source and listener on the same side)
    rflOv = 0;
    isfree && (k == 2 || k == 3) ? (
      rfO = RFB + (k - 2) * 6; rf = 0; hf = 1;
      vopt[rfO] >= 0.5 && vopt[RFB + 14] < 0.5 ? (
        rfa = vopt[rfO + 3] * $pi / 180; rux = cos(rfa); ruy = sin(rfa); rnx = -ruy; rny = rux;
        rcx = vopt[rfO + 1]; rcy = vopt[rfO + 2];
        rds = (sx - rcx) * rnx + (sy - rcy) * rny; rdl = (lx - rcx) * rnx + (ly - rcy) * rny;
        px = sx - 2 * rds * rnx; py = sy - 2 * rds * rny; pz = sz; tnb = 1;
        rds * rdl > 0.0001 ? (
          rtt = rds / (rds + rdl); rpx = px + rtt * (lx - px); rpy = py + rtt * (ly - py); rpz = pz + rtt * (lz - pz);
          rhit = abs((rpx - rcx) * rux + (rpy - rcy) * ruy) <= 0.5 * vopt[rfO + 4] && rpz >= 0 && rpz <= vopt[rfO + 5];
          rfal = rflA(vopt[rfO]);
          rf = rhit ? sqrt(1 - rfal) * 10 ^ (vopt[RFB + 10 + k] / 20) : 0; hf = hfrel(rfal);   // + the Level of this reflector (dB)
          rflOv = 1; rflEx = rpx - sx; rflEy = rpy - sy; rflEz = rpz - sz;
        );
        bon && !bsame ? btrans(px, py, pz) : ( btE = 1; btF = 0; btM = 0; btC = 0; btV = 0; );
      );
    );
    btV ? settap(k, btVx, btVy, btVz, rf, hf, k > 0, btE, btF, btM) : settap(k, px, py, pz, rf, hf, k > 0, btE, btF, btM);
    tapW[k] = tapG[k] * 10 ^ (-btC / 20);                       // mid-band level (without the bass that passes a wall more easily)
    rflOv = 0;
    tapZ[k] = iz != 0;
    k += 1;
  );
  // EARLY CLUSTER: between the second-order images and the onset of the reverb network the real room already has many
  // reflections (their number grows with t^2). 24 diffuse reflections fill that window: arrival times with t^2 density,
  // directions from all around, energy = the part of the room's reflected energy (Barron, exp(-13.8 t / T)) that falls
  // into the window minus what the images there already bring. Highs: the walls' highs for as many bounces as fit (c t /
  // mean free path). Not in the free field, not when the barrier separates source and listener (coupled rooms).
  clA = 1.5 * mfp / C_SND; clB = 3.4 * mfp / C_SND;                       // window after the direct sound (s)
  barE = (bon && !bsame ? bcpl : exp(-0.04 * rdir / rt) / (rc * rc)) / QDIR;
  ecW = barE * max(0, exp(-13.82 * clA / rt) - exp(-13.82 * clB / rt));
  k = 1; loop(NIMG - 1, (tapT[k] - tapT[0]) / 1000 >= clA && (tapT[k] - tapT[0]) / 1000 < clB ? ( ecW -= sqr(tapG[k]); ); k += 1; );
  ecW = isfree || (bon && !bsame) ? 0 : max(0, ecW);
  // SOURCE DIRECTION in the early cluster: the 24 reflections of the next 2-4 bounces still remember where the source
  // points. Turned towards a near wall: more and brighter early sound; turned into the open room or away: less and darker.
  // Energy factor = what the discrete reflections get from the pattern against the average over all directions (1 / Q),
  // taken half way (square root, max +-3 dB); highs = the mean off-axis angle of the reflections, half way
  ceR = ceO > 0.0000001 ? ceD / ceO * QDIR : 1;
  ecW *= min(2, max(0.5, sqrt(ceR)));
  clFd = ceD > 0.0000001 ? 2 * dirHb / max(0.02, ceU / ceD) : 0;
  clw = 0; k = 0;
  loop(NCL, clt = (clA ^ 3 + min(1, max(0, cufr[k])) * (clB ^ 3 - clA ^ 3)) ^ (1 / 3); clw += exp(-13.82 * clt / rt) / (clt * clt); k += 1; );
  clhf = (hfrel(abar) ^ 1);                                             // highs per bounce (room average)
  k = 0;
  loop(NCL,
    clt = (clA ^ 3 + min(1, max(0, cufr[k])) * (clB ^ 3 - clA ^ 3)) ^ (1 / 3);
    cldist = rdir + clt * C_SND;
    tnb = max(1, clt * C_SND / max(0.5, mfp) + 1);                        // bounces so far
    tIX[NIMG + k] = tIY[NIMG + k] = tIZ[NIMG + k] = 0;
    settap(NIMG + k, lx + cdir[k * 3] * cldist, ly + cdir[k * 3 + 1] * cldist, lz + cdir[k * 3 + 2] * cldist,
           sqrt(ecW * exp(-13.82 * clt / rt) / (clt * clt) / max(0.000000001, clw)) * cldist, clhf ^ tnb, 2, 1, 0, 0);
    tapW[NIMG + k] = tapG[NIMG + k]; tapZ[NIMG + k] = 0;
    k += 1;
  );

  xMIX = min(1, max(0, slider18 / 100));
  erg   = 10 ^ (slider20 / 20);
  lateg = 10 ^ (slider21 / 20);
  xOG   = slider22 <= -71.9 ? 0 : 10 ^ (slider22 / 20);   // fully left = -inf (silence), max +24 dB
  clipon = slider23 >= 0.5;
  xIG = slider40 <= -71.9 ? 0 : 10 ^ (slider40 / 20); fbyp = slider39 >= 0.5;
  fzm = min(12, max(0, floor(slider35 + 0.5))); xBYP = slider36 >= 0.5; xDEN = min(1, max(0, slider38 / 100));
  // DENSITY -> rotation between self-feedback (I) and full mixing (S): M = cos(th) I + sin(th) S is orthogonal
  // for every th, so the FDN stays lossless and RT60 no longer depends on DENSITY
  xTH = cos(0.5 * $pi * (0.12 + 0.88 * xDEN)); xSH = sin(0.5 * $pi * (0.12 + 0.88 * xDEN));
  fzkeyv = fzm + 1 + fzkeyf();
  fzkeyv != lastfz ? (
    fzdesign(fzb, fzP, fzm, 1);
    fzP[1] > 0 ? fdesign(fzCc, fzCc + 40, 0, fzP[0], fzP[1]); fzP[3] > 0 ? fdesign(fzCc + 20, fzCc + 44, 1, fzP[2], fzP[3]);   // complements (Mix)
    fzHPb = floor(fzP[1] / 2); fzHPo = fzP[1] % 2; fzLPb = floor(fzP[3] / 2); fzLPo = fzP[3] % 2;
    fzNP = fzP[4]; fzCrSR = fzP[16]; fzHiss = fzP[18]; fzGain = fzP[19];
    fzAsym = fzP[14] > 0 ? fzP[20] : 0; fzAsT = thf(fzAsym);
    lastfz = fzkeyv;
    fzLin = a_fzlin();
    dvPk = 0.9 / max(0.05, sqrt(a_fzpk()));
    // another model or another slope: start from clean filter states (old contents would come out as a click)
    fzord = fzm * 10000 + fzP[1] * 100 + fzP[3];
    fzord != lastfzo ? ( memset(fzS, 0, 40); memset(fzSs, 0, 40); memset(FZA, 0, 120); memset(FZO, 0, 44); memset(odS, 0, 160); fzU1 = fzU2 = 0; fzL1 = odS[33] = odS[73] = lcosh(0); fzhold = 0; fzacc = 0; fzDx = fzDy = 0; lastfzo = fzord; );
    fzNP != lastfzn ? ( memset(fzS + 16, 0, 6); memset(fzSs + 16, 0, 6); lastfzn = fzNP; );
  );
  fzon = fzm > 0 || fzanyf();           // source filter runs: a model is selected or the EQ is edited
  // DRIVE (slider 37): drf scales the model's saturation / clip amount, fzPush is the extra gain into the non-linearity above 50 %
  // DRIVE: 50 % = model default; 0 % = 1/8 (almost clean); 100 % = 16x plus 12 dB extra push into the non-linearity
  drv = min(100, max(0, slider37)); drf = (drv >= 50) ? 16 ^ ((drv - 50) / 50) : 8 ^ ((drv - 50) / 50);
  fzPush = 10 ^ (max(0, drv - 50) / 50 * 12 / 20);
  fzSat = fzP[14] > 0 ? fzP[14] * drf : ((fzm > 0 && fzP[15] == 0) ? 8 * max(0, (drv - 50) / 50) : 0);
  fzClip = fzP[15] > 0 ? fzP[15] * drf : 0;
  // level reference of the non-linearity: above 50 % the ceiling stays where the model has it (more drive = more
  // distortion, not less level); below 50 % the gain for quiet signals stays 1
  fzSatN = fzP[14] > 0 ? min(fzSat, fzP[14]) : (fzSat > 1 ? sqrt(fzSat) : fzSat);
  fzClipN = min(fzClip, fzP[15]);
// ORIGINAL + DISTORTION: no device - left and right each through a saturator (tanh, a little bias for even
  // harmonics, 4x oversampled), before the source EQ. 0 % = untouched (the input 1:1), 100 % = drive x16; quiet signals
  // keep their level (gain 1), loud parts are squashed. True stereo: the image and the phase stay (a mid-only saturator
  // moved the correlation from +0.60 to +0.02 at 100 %)
  fzm == 0 && drv > 0.001 && slider85 >= 0.5 ? ( odG = (0.25 + 3.75 * drv / 100) * fzPush; odA = 0.12 * drv / 100; odAT = thf(odA); odN = odG * (1 - odAT * odAT); odOn = 1; ) : ( odOn = 0; );
  fzq = 2 ^ (min(12, max(4, fzP[17] - (drv - 50) / 50 * 3)) - 1);   // codec: 8 bit at 50 %, 5 bit at 100 % (steps per polarity)
  // EQUAL LOUDNESS, source: undo the level change of the model - its filter (mean power gain for speech) and what its
  // distortion does to a typical level (-12 dBFS peak)
  fzRef = 0.25;
  fzSat > 0 ? fzRef = (thf(fzSat * fzPush * fzRef + fzAsym) - fzAsT) / fzSatN;
  fzClip > 0 ? fzRef = min(1, fzClip * fzPush * fzRef) / fzClipN;
  fzOutG = slider60 >= 0.5 ? min(20, 1 / max(0.05, sqrt(max(0.000001, fzLin)) * fzRef / 0.25)) : fzGain;
  lcon = slider24 >= 0.5; hcon = slider27 >= 0.5; fpost = slider30 >= 0.5; 
  lcN = floor(slider26 + 0.5) + 1; hcN = floor(slider29 + 0.5) + 1;
  lcNb = floor(lcN / 2); lcNo = lcN % 2; hcNb = floor(hcN / 2); hcNo = hcN % 2;
  lckey = slider25 * 16 + lcN; hckey = slider28 * 16 + hcN;
  lckey != lastlc ? ( fdesign(lcC, lcF, 1, max(20, slider25), lcN); fdesign(lcCc, lcFc, 0, max(20, slider25), lcN); lastlc = lckey; );
  hckey != lasthc ? ( fdesign(hcC, hcF, 0, max(1000, slider28), hcN); fdesign(hcCc, hcFc, 1, max(1000, slider28), hcN); lasthc = hckey; );
  // TONE (slider 33): > 0 = bright: high shelf at 4 kHz (0.12 x value); < 0 = dark: one-pole low-pass sweeping from 20 kHz (0) down to 1.5 kHz (-100)
  bron = slider33 > 0.5; dkon = slider33 < -0.5;
  bron && slider33 != lastbr ? ( shelfdesign(bsC, 4000, slider33 * 0.12); lastbr = slider33; );
  dkon && slider33 != lastdk ? ( dkA = exp(-2 * $pi * (20000 * (1500 / 20000) ^ (-slider33 / 100)) / srate); lastdk = slider33; );
  fcg   = (slider19 >= 0.5) && !isfree;

  k = 0;
  loop(NT,
    tapOn[k] = (k == 0) ? 1 : (isfree ? ((k == 1 && bon && bsame) || (k == 2 && vopt[RFB] >= 0.5 && vopt[RFB + 14] < 0.5) || (k == 3 && vopt[RFB + 6] >= 0.5 && vopt[RFB + 14] < 0.5) || (slider119 >= 0.5 && k < NIMG && tIZ[k] == -1 && tIX[k] == 0 && tIY[k] == 0)) : (!tapZ[k] || fcg));   // free field: the barrier reflection and the ground
    dirg = (k == 0) ? 1 : erg * tapOn[k];
    xGL[k] *= dirg; xGR[k] *= dirg;
    k += 1;
  );

  // FDN geometry (only when the room size changes)
  abs(mfp - last_mfp) > 0.0001 ? (
    // 8 delay lines: base length = 1.2 mean free paths (20 - 110 ms) x the spread factors fac[], rounded to an odd sample count; 4 allpass lengths scaled with the room (ds 0.5 ... 2.5)
    base = min(0.11, max(0.02, 1.2 * mfp / C_SND));
    k = 0;
    loop(8, flen[k] = min(LINEMAX - 2, floor(base * fac[k] * srate) | 1); k += 1; );
    ds = min(2.5, max(0.5, mfp / 3.5));
    apLen[0] = floor(0.0047 * ds * srate); apLen[1] = floor(0.0036 * ds * srate);
    apLen[2] = floor(0.0127 * ds * srate); apLen[3] = floor(0.0093 * ds * srate);
    last_mfp = mfp;
  );

  // loop gains: low frequencies decay with RT60, highs faster (more absorption = darker)
  hfr = hfrP;                                      // HF / LF decay ratio from walls + air (Eyring)
  bgoff ? ( gHFo = hfr; ) : ( bgblend ? ( hfr = hfr * bmixg + gHFo * (1 - bmixg); ); );   // (hfr is local: kept here)
  // RT60 PER OCTAVE (63 Hz ... 8 kHz) relative to the mids: Eyring with the materials per octave and the air of each band,
  // for the same room as hfr / lfrP; 125 Hz and 4 kHz are exactly lfrP and hfr (bass of the modes, barrier mix, ...)
  rbsel == 1 ? ( rbV = pV; rbS = pS2; rbA = pA; rbD = pD; rb1 = pNy; rb2 = pSy; rb3 = pwE; rb4 = pwW; rb5 = pwF; ) : (
  rbsel == 2 ? ( rbV = qV; rbS = qS; rbA = qA; rbD = qD; rb1 = qNy; rb2 = qSy; rb3 = qwE; rb4 = qwW; rb5 = qwF; ) : (
               rbV = roomV; rbS = roomS; rbA = roomA; rbD = roomD; rb1 = aN; rb2 = aS; rb3 = aE; rb4 = aW; rb5 = aF; ); );
  rbm = 0.161 * rbV / (-rbS * log(1 - min(0.95, max(0.005, rbA / rbS))) + 4 * airm5 * rbV);
  k = 0;
  loop(8,
    rbj = min(5, max(0, k - 1)); rbAk = areaX(rbj, roomW, rbD, roomH, rb1, rb2, rb3, rb4, rb5);
    rbt = 0.161 * rbV / (-rbS * log(1 - min(0.95, max(0.005, rbAk / rbS))) + 4 * airOB[k] * rbV);
    // RT relative to the mids, limited: bass bands (k <= 2) 0.4 ... 1.8, mids 0.1 ... 1.8, highs (k >= 6) 0.1 ... 1 (never longer than the mids)
    fqR[k] = min(k <= 2 ? 1.8 : (k >= 6 ? 1 : 1.8), max(k <= 2 ? 0.4 : 0.1, rbt / rbm));
    k += 1;
  );
  fqR[1] = lfrP; fqR[6] = hfr;
  k = 0; loop(8, bgoff ? ( fqRo[k] = fqR[k]; ) : ( bgblend ? ( fqR[k] = fqR[k] * bmixg + fqRo[k] * (1 - bmixg); ); ); k += 1; );
  fqR[1] = lfrP; fqR[6] = hfr;
  fqsr != srate ? fqinit();
  // cache key: the loop gains are redesigned only when one of the decay inputs changes (prime weights keep the sum from colliding)
  gkey = rt * 1000003 + hfr * 1009 + last_mfp + lfrP * 7919 + fqR[2] * 104729 + fqR[3] * 7907 + fqR[4] * 6053 + fqR[5] * 5821 + fqR[7] * 4643;
  gkey != last_gkey ? (
    lsum = 0; k = 0;
    loop(8,
      g0 = 10 ^ (-3 * flen[k] / (rt * srate));
      gh = 10 ^ (-3 * flen[k] / (rt * hfr * srate));
      // one-pole damping matched at 4 kHz (not at Nyquist): DC gain g0, gain gh at 4 kHz
      // -> the tail really decays with RT60 x hfr at 4 kHz (Nyquist-matched poles left 4 kHz almost undamped)
      hr = gh / g0; hc = cos(2 * $pi * 4000 / srate);
      hr < 0.99999 ? ( hB = (2 - 2 * hr * hr * hc) / (hr * hr - 1); fb[k] = (-hB - sqrt(hB * hB - 4)) / 2; ) : ( fb[k] = 0; );
      fg[k] = g0;
      fgL[k] = g0 + (10 ^ (-3 * flen[k] / (min(20, rt * lfrP) * srate)) - g0) / 0.8944;   // bass RT60 at 125 Hz (shelf corner 250 Hz compensated)
      fqdesign(k, flen[k]);                                                    // per octave (what the lines play)
      lsum += flen[k]; k += 1;
    );
    gm  = 10 ^ (-3 * (lsum / 8) / (rt * srate));
    ghm = 10 ^ (-3 * (lsum / 8) / (rt * hfr * srate));
    tailn = sqrt((1 - gm * gm) / (gm * gm));     // tail energy normalized to 1 in the low/mid band (highs decay faster on purpose)
    last_gkey = gkey;
  );
  // scaled so direct == reverb exactly at r_c
  // late level after Barron's revised theory (instead of the classic constant diffuse field):
  //   total reflected energy  Etot = (1/rc^2) * exp(-0.04 r / T)      (decays with distance from the source)
  //   late part (> 80 ms)     Elat = Etot * exp(-1.11 / T)
  // The image-source taps and the early cluster are already rendered as their own taps, so their energy is taken out of
  // the reverb tail (no double counting); the tail never drops below the late part.
  // (barE: computed with the early cluster above - other side of the barrier: coupled energy; / Q: directional source)
  erE = 0; k = 1; loop(NT - 1, erE += tapOn[k] * sqr(tapG[k]); k += 1; );
  ecl80 = 0; k = NIMG; loop(NCL, (tapT[k] - tapT[0]) > 80 ? ecl80 += tapOn[k] * sqr(tapG[k]); k += 1; );   // cluster energy after 80 ms
  tailE = max(barE * exp(-1.11 / rt) - ecl80, barE - erE);
  latecorr = tailE * rc * rc;                                    // tail energy relative to the classic model (echogram)
  xLG = isfree ? 0 : lateg * sqrt(tailE) * tailn;
  pre = min(200, max(0, slider34)) / 1000 * srate;           // PREDELAY: delays early + late, not the direct sound
  isfree ? pre = 0;   // (outdoors no predelay - the ground reflection arrives where physics puts it)
  k = 1; loop(NT - 1, xDL[k] = min(MAXIN - 4, xDL[k] + pre); xDR[k] = min(MAXIN - 4, xDR[k] + pre); k += 1; );
  k = 0; loop(NT, xDL[k] += dopA; xDR[k] += dopA; k += 1; );   // the Doppler extra delay in use (see DOPPLER)
  bon && !bsame ? (
    // tail starts after the way through the barrier: via the opening, or straight through the wall
    brOA > 0.0001 ? ( pcx = brOX; pcz = brOZ; ) : ( pcx = sx + (lx - sx) * (yb - sy) / (ly - sy); pcz = sz + (lz - sz) * (yb - sy) / (ly - sy); );
    brd = sqrt(sqr(sx - pcx) + sqr(sy - yb) + sqr(sz - pcz)) + sqrt(sqr(lx - pcx) + sqr(ly - yb) + sqr(lz - pcz));
    // Room spill = the reverberant sound of the source's room, radiated into the listener's room by the wall / opening.
    // Part of it reaches the listener straight from there (direct field of that surface, Q = 2), the rest only after it has
    // been reflected around the listener's room:  direct / reverberant = A / (8 pi r^2)  (A = absorption area, r = distance).
    // The direct part is placed where it comes through, so the spill follows the listener, the door and the source.
    spx = min(roomW, max(0, sx + (lx - sx) * (yb - sy) / (ly - sy))); spz = min(roomH, max(0, sz + (lz - sz) * (yb - sy) / (ly - sy)));
    brOA > 0.0001 ? ( spx += bshare * (brOX - spx); spz += bshare * (brOZ - spz); );   // energy-weighted: opening <-> wall
    sphd = sqrt(sqr(spx - lx) + sqr(yb - ly)); spd3 = max(0.5, sqrt(sqr(sphd) + sqr(spz - lz)));
    sprat = isfree ? 0 : min(4, AeP / (8 * $pi * spd3 * spd3));
    splf = isfree ? 1 : exp(-0.04 * spd3 / rtL);                 // reverberant part in the listener's room (Barron)
    xLG *= sqrt(splf + sprat); bspl += 10 * log10(splf + sprat);
    plat = sin(atan2(yb - ly, spx - lx) - face) * sphd / spd3;
    xLDf = sqrt(splf / (splf + sprat)); pang(plat); xLPL = sqrt(sprat / (splf + sprat)) * pgL; xLPR = sqrt(sprat / (splf + sprat)) * pgR;
  ) : ( brd = rdir; xLDf = 1; xLPL = 0; xLPR = 0; );
  xLD = min(MAXIN - 4, (brd + mfp) / C_SND * srate + pre) + dopA;
  // BASS STEERING weight (precedence effect): full while the direct sound is within 10 dB of everything that follows it (early
  // reflections + the reverb as it is played, incl. what comes through a barrier), none below -20 dB (e.g. a closed door) -
  // in between it fades
  xSTW = min(1, max(0, (10 * log10(max(0.000000000001, sqr(tapG[0])) / max(0.000000000001, erE + sqr(xLG / max(0.000001, tailn)))) + 20) / 10));
  xSTW *= pbD;   // no bass steering with the source on the listener (it would turn the phase of the input)
  // ---- 2nd network: the neighbor room (same side only) ----
  xLG2 = 0;
  bnet && !isfree ? (
    base2 = min(0.11, max(0.02, 1.2 * mfpQ / C_SND));
    k = 0; loop(4, flen2[k] = min(LINEMAX - 2, floor(base2 * fac2[k] * srate) | 1); k += 1; );
    gkey2 = rt2 * 1000003 + hfr2 * 1009 + base2 + lfr2 * 7919;
    gkey2 != last_gkey2 ? (
      lsum = 0; k = 0;
      loop(4,
        g0 = 10 ^ (-3 * flen2[k] / (rt2 * srate)); gh = 10 ^ (-3 * flen2[k] / (rt2 * hfr2 * srate));
        hr = gh / g0; hc = cos(2 * $pi * 4000 / srate);
        hr < 0.99999 ? ( hB = (2 - 2 * hr * hr * hc) / (hr * hr - 1); fb2[k] = (-hB - sqrt(hB * hB - 4)) / 2; ) : ( fb2[k] = 0; );
        fg2[k] = g0; fgL2[k] = g0 + (10 ^ (-3 * flen2[k] / (min(20, rt2 * lfr2) * srate)) - g0) / 0.8944; lsum += flen2[k]; k += 1;
      );
      gm = 10 ^ (-3 * (lsum / 4) / (rt2 * srate));
      tailn2 = sqrt((1 - gm * gm) / (gm * gm));
      last_gkey2 = gkey2;
    );
    xLG2 = lateg * sqrt(bE2) * tailn2;
    // it arrives through the opening (or the barrier): delay of the way through the neighbor room, stereo position of the opening
    brOA > 0.0001 ? ( pcx = brOX; pcz = brOZ; ) : ( pcx = 0.5 * (sx + lx); pcz = 0.5 * (sz + lz); );
    xLD2 = min(MAXIN - 4, (sqrt(sqr(sx - pcx) + sqr(sy - yb) + sqr(sz - pcz)) + mfpQ + sqrt(sqr(lx - pcx) + sqr(ly - yb) + sqr(lz - pcz))) / C_SND * srate + pre) + dopA;
    plat = sin(atan2(yb - ly, pcx - lx) - face) * 0.6;          // 60 % of the opening's direction: a wide source
    pang(plat); xP2L = pgL * 1.41421356; xP2R = pgR * 1.41421356;
  );
  // EQUAL LOUDNESS, room: total energy that reaches the listener (direct + reflections + reverb, mid band), relative to
  // a source 1 m away in the open. Switched on, the room sound is brought to that level: every room, distance and
  // barrier plays about as loud as the input (limited to +36 dB)
  eqE = sqr(tapW[0]); k = 1; loop(NT - 1, eqE += tapOn[k] * sqr(tapW[k] * erg); k += 1; );
  eqE += sqr(xLG / max(0.000000001, tailn)) / bcmpE;
  xLG2 > 0 ? eqE += sqr(xLG2 / max(0.000000001, tailn2)) / bcmpE;
  xEQL = slider60 >= 0.5 ? min(63, 1 / sqrt(max(0.0000001, eqE))) : 1;
// DISTANCE PERSPECTIVE (film): beyond 2 m the whole room sound - direct, reflections, reverb, echoes - falls by an
  // extra 6 dB per doubling of the distance at 100 % (0 % = physics: in a big hall the reverb keeps the level up). The
  // stereo picture and the direct / room ratio stay as they are. Not with Equal loudness (that one keeps the level)
  dpd = sqrt(sqr(sx - lx) + sqr(sy - ly) + sqr(sz - lz)); dpG = slider60 >= 0.5 ? 1 : (2 / max(2, dpd)) ^ min(1, max(0, slider117 / 100));
  xEQL *= dpG;
  // how loud the reverb of THIS room is for a given input (energy): used to bring a tail that is still ringing down to the
  // level it would have in the new room when the room changes (see @block)
  xTK = isfree ? 0 : sqr(xLG / max(0.000000001, tailn));
  // BOUNDARY EFFECT in the reverb: a source close to a hard surface radiates more low-frequency power into the room (up to
  // twice, +3 dB) - for wavelengths that are long against the distance, i.e. below c / (2 pi d). The direct sound and the
  // early reflections get their boundary gain from the mirror sources already; this adds it to the tail. Nearest surface only.
  bndd = roomH; bnda = 1;
  fcg ? ( sz < bndd ? ( bndd = sz; bnda = aF; ); roomH - sz < bndd ? ( bndd = roomH - sz; bnda = aF; ); );
  !isfree ? (
    sx < bndd ? ( bndd = sx; bnda = aW; ); roomW - sx < bndd ? ( bndd = roomW - sx; bnda = aE; );
    sy < bndd ? ( bndd = sy; bnda = aN; ); roomD - sy < bndd ? ( bndd = roomD - sy; bnda = aS; );
  );
  bndd = max(0.1, bndd);
  xBNG = isfree ? 0 : sqrt(1 + (1 - bnda)) - 1;                 // extra amplitude below the corner
  xBNA = 1 - exp(-2 * $pi * min(800, max(20, C_SND / (2 * $pi * bndd))) / srate);
);


// ---------- GUI helpers (design units, 760 wide, scaled by gs) ----------
// black & white mode: every color becomes its gray value (luminance), nothing else changes
function gmono() local(v) ( monoOn ? ( v = 0.299 * gfx_r + 0.587 * gfx_g + 0.114 * gfx_b; v = min(1, max(0, (v - 0.5) * 1.5 + 0.5)); gfx_r = gfx_g = gfx_b = v; ); );   // gray value, contrast x1.5
// cfade: opacity of everything drawn through col() - 1 normally, ramps up while a help text fades in
function col(c, al) ( gfx_r = floor(c / 65536) / 255; gfx_g = (floor(c / 256) % 256) / 255; gfx_b = (c % 256) / 255; gfx_a = al * cfade; gmono(); );
// ---- pixel-exact boxes ----
// Every box is snapped to whole screen pixels FIRST (left / top / right / bottom edge each rounded once), so neighbors
// share their edges exactly, fill and outline of one shape cover the same pixels, and nothing sticks out by a pixel.
// PXR leaves the box in gpx0, gpy0 (top left) and gpw, gph (size in pixels).
function PXR(x, y, w, h)
(
  gpx0 = floor(gox + x * gs + 0.5); gpy0 = floor(goy + y * gs + 0.5);
  gpw = floor(gox + (x + w) * gs + 0.5) - gpx0; gph = floor(goy + (y + h) * gs + 0.5) - gpy0;
);
// Draws a filled rectangle in GUI units (x, y, w, h) with the current color, clipped to the room view when gclp is set.
function R(x, y, w, h) local(x2, y2)
(
  gclp ? ( x2 = min(x + w, clX1); y2 = min(y + h, clY1); x = max(x, clX0); y = max(y, clY0); w = x2 - x; h = y2 - y; );   // inside the room view
  w > 0 && h > 0 ? ( PXR(x, y, w, h); gfx_rect(gpx0, gpy0, max(1, gpw), max(1, gph)); );
);
// filled rounded box: drawn row by row (no part is painted twice - also correct with transparency), the two pixels at
// the ends of each corner row get the share of the circle they cover (smooth corners)
function RR(x, y, w, h, rad) local(crad, i, yc, ins, cins, fr, a0)
(
  w > 0 && h > 0 ? (
    PXR(x, y, w, h); gpw = max(1, gpw); gph = max(1, gph);
    crad = min(floor(rad * gs + 0.5), floor(min(gpw, gph) / 2));
    crad < 2 ? gfx_rect(gpx0, gpy0, gpw, gph) : (
      a0 = gfx_a;
      gph > 2 * crad ? gfx_rect(gpx0, gpy0 + crad, gpw, gph - 2 * crad);
      i = 0;
      loop(crad,
        yc = crad - i - 0.5; ins = crad - sqrt(crad * crad - yc * yc);      // how far this row is set in from the side
        cins = ceil(ins - 0.0001); fr = cins - ins;
        gpw > 2 * cins ? ( gfx_rect(gpx0 + cins, gpy0 + i, gpw - 2 * cins, 1); gfx_rect(gpx0 + cins, gpy0 + gph - 1 - i, gpw - 2 * cins, 1); );
        fr > 0.04 && cins >= 1 ? (
          gfx_a = a0 * fr;
          gfx_rect(gpx0 + cins - 1, gpy0 + i, 1, 1); gfx_rect(gpx0 + gpw - cins, gpy0 + i, 1, 1);
          gfx_rect(gpx0 + cins - 1, gpy0 + gph - 1 - i, 1, 1); gfx_rect(gpx0 + gpw - cins, gpy0 + gph - 1 - i, 1, 1);
          gfx_a = a0;
        );
        i += 1;
      );
    );
  );
);
// outline of the same box: on its outermost pixels
function RRO(x, y, w, h, rad) local(crad)
(
  w > 0 && h > 0 ? (
    PXR(x, y, w, h); gpw = max(2, gpw); gph = max(2, gph);
    crad = min(floor(rad * gs + 0.5), floor(min(gpw, gph) / 2));
    gfx_roundrect(gpx0, gpy0, gpw - 1, gph - 1, min(crad, floor((min(gpw, gph) - 1) / 2)), 1);   // pill ends: two clean half circles
  );
);
// the source icons are small free drawings: they keep the unsnapped versions
function RRF(x, y, w, h, rad) local(rx0, ry0, pw, ph, pr)
(
  rx0 = gox + x * gs; ry0 = goy + y * gs; pw = w * gs; ph = h * gs;
  pr = min(rad * gs, min(pw, ph) / 2);
  gfx_rect(rx0 + pr, ry0, pw - 2 * pr, ph);
  gfx_rect(rx0, ry0 + pr, pr, ph - 2 * pr);
  gfx_rect(rx0 + pw - pr, ry0 + pr, pr, ph - 2 * pr);
  gfx_circle(rx0 + pr, ry0 + pr, pr, 1, 1);
  gfx_circle(rx0 + pw - pr - 1, ry0 + pr, pr, 1, 1);
  gfx_circle(rx0 + pr, ry0 + ph - pr - 1, pr, 1, 1);
  gfx_circle(rx0 + pw - pr - 1, ry0 + ph - pr - 1, pr, 1, 1);
);
// Draws a 1 px anti-aliased rounded-rectangle outline at design-unit x,y,w,h with corner radius rad, scaled by gs and offset by gox/goy.
function RROF(x, y, w, h, rad) ( gfx_roundrect(gox + x * gs, goy + y * gs, w * gs, h * gs, rad * gs, 1); );
// PIXEL-SNAPPED HAIRLINE (screen pixels): a horizontal or vertical line lies exactly on whole device pixels (no blur across two
// half-gray rows at an odd scale) and grows with the scale (1 px up to 2.3x, then 2 px ...); a slanted line stays anti-aliased, as thick
function LNPX(u1, v1, u2, v2) local(t, a, b, nx, ny, l, k)
(
  t = max(1, floor(gs * 0.75 + 0.25)); l = sqrt(sqr(u2 - u1) + sqr(v2 - v1));
  l < 0.75 ? 0 : ( abs(v2 - v1) < 0.01 ? (   // (a stroke shorter than 3/4 px is left out - as the anti-aliased line drew next to nothing)
    a = floor(min(u1, u2) + 0.5); b = floor(max(u1, u2) + 0.5); gfx_rect(a, floor(v1 + 0.5 - t / 2), b - a + 1, t);
  ) : ( abs(u2 - u1) < 0.01 ? (
    a = floor(min(v1, v2) + 0.5); b = floor(max(v1, v2) + 0.5); gfx_rect(floor(u1 + 0.5 - t / 2), a, t, b - a + 1);
  ) : (
    gfx_line(u1, v1, u2, v2, 1);
    t > 1 ? ( nx = -(v2 - v1) / l; ny = (u2 - u1) / l; k = 1;
      loop(t - 1, gfx_line(u1 + nx * k, v1 + ny * k, u2 + nx * k, v2 + ny * k, 1); k += 1; ); );
  ); ); );
);
// Draws a 1 px line in design units; in the room view (gclp) it is first clipped to the view rect clX0..clY1 (Liang-Barsky).
function LN(x1, y1, x2, y2) local(dx, dy, t0, t1, k, p, q, r, ok)
(
  gclp ? (   // clipped to the room view (Liang-Barsky)
    dx = x2 - x1; dy = y2 - y1; t0 = 0; t1 = 1; ok = 1; k = 0;
    // Liang-Barsky: per view edge k (left, right, top, bottom) p = direction towards it, q = distance inside; t0 / t1 = visible part of the segment (0 ... 1), ok = 0 when fully outside
    loop(4,
      p = k == 0 ? -dx : (k == 1 ? dx : (k == 2 ? -dy : dy)); q = k == 0 ? x1 - clX0 : (k == 1 ? clX1 - x1 : (k == 2 ? y1 - clY0 : clY1 - y1));
      abs(p) < 0.000001 ? ( q < 0 ? ok = 0; ) : ( r = q / p; p < 0 ? ( r > t1 ? ok = 0 : ( r > t0 ? t0 = r; ); ) : ( r < t0 ? ok = 0 : ( r < t1 ? t1 = r; ); ); );
      k += 1;
    );
    ok && t1 >= t0 ? LNPX(gox + (x1 + t0 * dx) * gs, goy + (y1 + t0 * dy) * gs, gox + (x1 + t1 * dx) * gs, goy + (y1 + t1 * dy) * gs);
  ) : LNPX(gox + x1 * gs, goy + y1 * gs, gox + x2 * gs, goy + y2 * gs);
);
// Draws a line of width wd (design units): thin = stacked 1 px lines, wider = filled band with soft edges and round caps; clipped when gclp.
function LNW(x1, y1, x2, y2, wd) local(len, nx, ny, n, q, off, w, a, px1, py1, px2, py2, dx, dy, t0, t1, k, p, r, ok, ux, uy, hw, ex, ey)
(
  w = wd * gs; a = gfx_a;
  w < 1.6 ? (
    // thin: as before (1 px lines side by side)
    n = max(1, floor(wd * gs + 0.5));
    len = sqrt(sqr(x2 - x1) + sqr(y2 - y1));
    len > 0 ? (
      nx = -(y2 - y1) / len / gs; ny = (x2 - x1) / len / gs;
      q = 0;
      loop(n, off = q - (n - 1) / 2; LN(x1 + nx * off, y1 + ny * off, x2 + nx * off, y2 + ny * off); q += 1; );
    );
  ) : (
// SMOOTH: a filled band (core) with anti-aliased edge lines and round caps - in the room view clipped first
    ok = 1; t0 = 0; t1 = 1; dx = x2 - x1; dy = y2 - y1;
    gclp ? ( k = 0;
      loop(4,
        p = k == 0 ? -dx : (k == 1 ? dx : (k == 2 ? -dy : dy)); q = k == 0 ? x1 - clX0 : (k == 1 ? clX1 - x1 : (k == 2 ? y1 - clY0 : clY1 - y1));
        abs(p) < 0.000001 ? ( q < 0 ? ok = 0; ) : ( r = q / p; p < 0 ? ( r > t1 ? ok = 0 : ( r > t0 ? t0 = r; ); ) : ( r < t0 ? ok = 0 : ( r < t1 ? t1 = r; ); ); );
        k += 1; ); );
    ok && t1 >= t0 ? (
      px1 = gox + (x1 + t0 * dx) * gs; py1 = goy + (y1 + t0 * dy) * gs; px2 = gox + (x1 + t1 * dx) * gs; py2 = goy + (y1 + t1 * dy) * gs;
      len = sqrt(sqr(px2 - px1) + sqr(py2 - py1));
      len > 0.01 ? (
        ux = (px2 - px1) / len; uy = (py2 - py1) / len; hw = max(0.25, (w - 1) / 2); ex = -uy; ey = ux;
        gfx_triangle(px1 + ex * hw, py1 + ey * hw, px2 + ex * hw, py2 + ey * hw, px2 - ex * hw, py2 - ey * hw, px1 - ex * hw, py1 - ey * hw);
        gfx_a = a * 0.9;
        gfx_line(px1 + ex * (hw + 0.5), py1 + ey * (hw + 0.5), px2 + ex * (hw + 0.5), py2 + ey * (hw + 0.5), 1);
        gfx_line(px1 - ex * (hw + 0.5), py1 - ey * (hw + 0.5), px2 - ex * (hw + 0.5), py2 - ey * (hw + 0.5), 1);
        gfx_a = a;
        a > 0.98 ? ( gfx_circle(px1, py1, w / 2 - 0.5, 1, 1); gfx_circle(px2, py2, w / 2 - 0.5, 1, 1); );   // round caps (smooth joins)
      );
    );
  );
);
// Draws a dashed line from (x1,y1) to (x2,y2) with the given dash and gap lengths (design units) and width wd, using LNW segments.
function DL(x1, y1, x2, y2, dash, gap, wd) local(len, ux1, uy1, t, t2)
(
  len = sqrt(sqr(x2 - x1) + sqr(y2 - y1));
  len > 0 ? (
    ux1 = (x2 - x1) / len; uy1 = (y2 - y1) / len; t = 0;
    while (
      t2 = min(len, t + dash);
      LNW(x1 + ux1 * t, y1 + uy1 * t, x1 + ux1 * t2, y1 + uy1 * t2, wd);
      t += dash + gap;
      t < len;
    );
  );
);
// circles and arcs sit on a whole pixel: a letter or a pointer placed on the same point is exactly in the middle
function CI(x, y, rad, fill) ( gclp && (x < clX0 || x > clX1 || y < clY0 || y > clY1) ? 0 : gfx_circle(floor(gox + x * gs + 0.5), floor(goy + y * gs + 0.5), rad * gs, fill, 1); );
// Draws a circle (filled if fill) at design coords x,y, radius rad, at sub-pixel position; skipped when outside the clip rect (gclp).
function CIF(x, y, rad, fill) ( gclp && (x < clX0 || x > clX1 || y < clY0 || y > clY1) ? 0 : gfx_circle(gox + x * gs, goy + y * gs, rad * gs, fill, 1); );
// text centered on the pixel that CI uses as center
function txtc(s, x, y) local(tw2, th2)
(
  gfx_measurestr(s, tw2, th2);
  gfx_x = floor(floor(gox + x * gs + 0.5) + 0.5 - tw2 / 2 + 0.5); gfx_y = floor(floor(goy + y * gs + 0.5) + 0.5 - th2 / 2 + 0.5);
  gfx_drawstr(s);
);
// Fills the quad (x1,y1)...(x4,y4) as triangles in design units; in the room view (gclp) it is clipped to the view rect first (Sutherland-Hodgman).
function POLY4(x1, y1, x2, y2, x3, y3, x4, y4) local(n, e, m, i, j, ax, ay, bx, by, pda, pdb, t)
(
  gclp ? (
// in the room view the polygon is clipped to the view (Sutherland-Hodgman) - walls, barrier face and shadows no longer reach out of it when the view is zoomed
    PLQ[0] = x1; PLQ[1] = y1; PLQ[2] = x2; PLQ[3] = y2; PLQ[4] = x3; PLQ[5] = y3; PLQ[6] = x4; PLQ[7] = y4; n = 4; e = 0;
    // clip against the 4 view edges in turn (e = left, right, top, bottom): pda / pdb = distance inside the edge (>= 0 = inside); the result goes to PLQ + 32 and is copied back
    while (e < 4 && n > 0) (
      m = 0; i = 0;
      loop(n,
        j = i + 1 >= n ? 0 : i + 1;
        ax = PLQ[2 * i]; ay = PLQ[2 * i + 1]; bx = PLQ[2 * j]; by = PLQ[2 * j + 1];
        pda = e == 0 ? ax - clX0 : (e == 1 ? clX1 - ax : (e == 2 ? ay - clY0 : clY1 - ay));
        pdb = e == 0 ? bx - clX0 : (e == 1 ? clX1 - bx : (e == 2 ? by - clY0 : clY1 - by));
        pdb >= 0 ? (
          pda < 0 ? ( t = pda / (pda - pdb); PLQ[32 + 2 * m] = ax + t * (bx - ax); PLQ[33 + 2 * m] = ay + t * (by - ay); m += 1; );
          PLQ[32 + 2 * m] = bx; PLQ[33 + 2 * m] = by; m += 1;
        ) : ( pda >= 0 ? ( t = pda / (pda - pdb); PLQ[32 + 2 * m] = ax + t * (bx - ax); PLQ[33 + 2 * m] = ay + t * (by - ay); m += 1; ); );
        i += 1;
      );
      memcpy(PLQ, PLQ + 32, 2 * m); n = m; e += 1;
    );
    i = 1;
    loop(max(0, n - 2),
      gfx_triangle(gox + PLQ[0] * gs, goy + PLQ[1] * gs, gox + PLQ[2 * i] * gs, goy + PLQ[2 * i + 1] * gs, gox + PLQ[2 * i + 2] * gs, goy + PLQ[2 * i + 3] * gs);
      i += 1;
    );
  ) : gfx_triangle(gox + x1 * gs, goy + y1 * gs, gox + x2 * gs, goy + y2 * gs,
               gox + x3 * gs, goy + y3 * gs, gox + x4 * gs, goy + y4 * gs);
);

// ---------- source icons (design units, origin = bottom center) ----------
function icR(x, y, w, h, rr0, c) ( icOL ? ( col(icInk, 1); RROF(icx + x * ik, icy + y * ik, w * ik, h * ik, rr0 * ik); ) : ( col(c, 1); RRF(icx + x * ik, icy + y * ik, w * ik, h * ik, rr0 * ik); ); );
// Icon part: circle at (x,y) radius rr0 in icon space (origin icx,icy, scale ik); filled in color c, or ink outline when icOL is set.
function icC(x, y, rr0, c) ( icOL ? ( col(icInk, 1); CIF(icx + x * ik, icy + y * ik, rr0 * ik, rr0 * ik < 1.2); ) : ( col(c, 1); CIF(icx + x * ik, icy + y * ik, rr0 * ik, 1); ); );
// Icon part: line (x1,y1)-(x2,y2) of width w in icon space; color c, or icInk with width capped at 1.2 in outline mode (icOL).
function icL(x1, y1, x2, y2, w, c) ( col(icOL ? icInk : c, 1); LNW(icx + x1 * ik, icy + y1 * ik, icx + x2 * ik, icy + y2 * ik, icOL ? min(w * ik, 1.2) : w * ik); );
// Icon part: quad of four points in icon space, filled in color c, or drawn as a 4-line outline in icInk when icOL is set.
function icP(x1, y1, x2, y2, x3, y3, x4, y4, c)
(
  icOL ? (
    col(icInk, 1);
    LN(icx + x1 * ik, icy + y1 * ik, icx + x2 * ik, icy + y2 * ik); LN(icx + x2 * ik, icy + y2 * ik, icx + x3 * ik, icy + y3 * ik);
    LN(icx + x3 * ik, icy + y3 * ik, icx + x4 * ik, icy + y4 * ik); LN(icx + x4 * ik, icy + y4 * ik, icx + x1 * ik, icy + y1 * ik);
  ) : (
    col(c, 1); POLY4(icx + x1 * ik, icy + y1 * ik, icx + x2 * ik, icy + y2 * ik, icx + x3 * ik, icy + y3 * ik, icx + x4 * ik, icy + y4 * ik);
  );
);
// Draws source icon number kind (0-12) with origin bottom-center at (qx0,qy0), scale k, body color ink and detail color det.
function srcicon(kind, qx0, qy0, k, ink, det) local(j, a1, a2, xx, yy)
(
  icx = qx0; icy = qy0; ik = k; icInk = ink;
  // icon kind = source model (slider 35): 0 loudspeaker (Original), 1 telephone, 2 mobile phone, 3 smartphone, 4 walkie-talkie, 5 radio, 6 boombox, 7 TV, 8 laptop, 9 tablet, 10 mini speaker, 11 headphones, 12 megaphone
  kind == 0 ? ( icR(-7, -21, 14, 21, 2, ink); icC(0, -7, 4.2, det); icC(0, -16, 2, det); icC(0, -7, 1.4, ink); );
  kind == 1 ? ( icR(-9, -8, 18, 8, 2, ink); icC(0, -4, 2.2, det);
                icL(-10, -12, -6, -18, 3.2, ink); icL(-6, -18, 6, -18, 3.2, ink); icL(6, -18, 10, -12, 3.2, ink);
                icR(-13, -13, 7, 5, 2, ink); icR(6, -13, 7, 5, 2, ink); );
  kind == 2 ? ( icR(2, -28, 2, 5, 1, ink); icR(-5, -24, 10, 24, 2, ink); icR(-3.5, -21, 7, 6, 1, det);
                yy = -11; loop(3, xx = -2.5; loop(3, icC(xx, yy, 0.8, det); xx += 2.5; ); yy += 3.5; ); );
  kind == 3 ? ( icR(-6.5, -24, 13, 24, 3, ink); icR(-5.3, -22.8, 10.6, 21.6, 2.2, det); icR(-2, -21.8, 4, 1.5, 0.7, ink); );
  kind == 4 ? ( icL(-3, -22, -3, -31, 2, ink); icR(-6, -22, 12, 22, 2, ink); icR(1, -25, 3, 3, 1, ink);
                yy = -17; loop(3, icL(-3.5, yy, 3.5, yy, 1, det); yy += 3; ); icR(-3.5, -7, 7, 3, 1, det); );
  kind == 5 ? ( icL(-7, -17, -5, -21, 1.6, ink); icL(-5, -21, 5, -21, 1.6, ink); icL(5, -21, 7, -17, 1.6, ink);
                icR(-11, -17, 22, 17, 3, ink); icC(-4.5, -8.5, 4.5, det); icC(-4.5, -8.5, 1.5, ink);
                icR(2, -13, 6, 3, 1, det); icC(3.5, -5, 1.3, det); icC(6.8, -5, 1.3, det); );
  kind == 6 ? ( icL(-9, -16, -7, -21, 1.6, ink); icL(-7, -21, 7, -21, 1.6, ink); icL(7, -21, 9, -16, 1.6, ink);
                icR(-15, -16, 30, 16, 3, ink); icC(-8.5, -7.5, 5, det); icC(-8.5, -7.5, 2, ink); icC(8.5, -7.5, 5, det); icC(8.5, -7.5, 2, ink);
                icR(-3, -13, 6, 4, 0.8, det); icR(-2.5, -6, 5, 2, 0.6, det); );
  kind == 7 ? ( icL(0, -19, -6, -27, 1.3, ink); icL(0, -19, 6, -27, 1.3, ink);
                icR(-12, -19, 24, 17, 2.5, ink); icR(-9.5, -16.5, 15, 12, 1.5, det); icC(8, -13, 1.3, det); icC(8, -8, 1.3, det);
                icR(-9, -2.5, 3, 2.5, 0.5, ink); icR(6, -2.5, 3, 2.5, 0.5, ink); );
  kind == 8 ? ( icR(-10, -20, 20, 14, 1.5, ink); icR(-8.5, -18.5, 17, 11, 1, det);
                icP(-12, -5.5, 12, -5.5, 14.5, -2, -14.5, -2, ink); icR(-3, -4.6, 6, 1.2, 0.5, det); );
  kind == 9 ? ( icR(-8, -22, 16, 22, 2.5, ink); icR(-6.6, -20.2, 13.2, 18.4, 1.5, det); icC(0, -21.1, 0.55, det); );
  kind == 10 ? ( icR(-12, -12, 24, 12, 6, ink);
                 xx = -6; loop(3, icC(xx, -8, 0.9, det); icC(xx, -4, 0.9, det); xx += 4; ); icC(8, -6, 1.3, 0x1D9E75); );
  kind == 11 ? ( j = 0; loop(13, a1 = $pi + j / 13 * $pi; a2 = $pi + (j + 1) / 13 * $pi;
                   icL(cos(a1) * 9, -12 + sin(a1) * 9, cos(a2) * 9, -12 + sin(a2) * 9, 2.2, ink); j += 1; );
                 icR(-12, -14, 6, 11, 2.5, ink); icR(6, -14, 6, 11, 2.5, ink); );
  kind == 12 ? ( icP(-6, -15, 10, -22, 10, -2, -6, -9, ink); icR(-11, -15, 5, 6, 1, ink);
                 icL(-3, -9, -1, -2, 2.5, ink); icL(10, -21, 10, -3, 1, det); );
);
// ---- text helpers: txt / txtbox / txtboxR / txtfit / txtplate (font slot 9 holds a shrunken copy of the current font when a label has to fit) ----
// Positions come from gfx_measurestr and are rounded to whole pixels, so letters sit exactly centered in circles and buttons.
// font sizes / bold flags as set in setfonts() (used to shrink a label that would not fit its box)
function fsz(i) ( i == 1 ? FS1 : (i == 2 ? FS2 : (i == 3 ? FS3 : (i == 4 ? FS4 : (i == 5 ? FS5 : (i == 6 ? FS6 : (i == 15 ? FS15 : (i == 16 ? FS16 : FS7))))))); );
// Returns 1 if font slot i is drawn bold (every slot except 4, 7 and 16), used when making a shrunken copy of the font.
function fbd(i) ( i != 4 && i != 7 && i != 16; );
// text at (x, yc): al 0 = left, 1 = centered, 2 = right; vertically centered on yc using the measured line height
function txt(s, x, yc, al) local(px, py, tw2, th2)
(
  gclp && (x < clX0 || x > clX1 || yc < clY0 || yc > clY1) ? 0 : (
  gfx_measurestr(s, tw2, th2);
  px = gox + x * gs; py = goy + yc * gs;
  gfx_x = floor((al == 1 ? px - tw2 / 2 : (al == 2 ? px - tw2 : px)) + 0.5);
  gfx_y = floor(py - th2 / 2 + 0.5);
  gfx_drawstr(s);
  );
);
// centered text that always stays inside a box w x h: a smaller copy of the font is used when it would not fit
function txtbox(s, cx, cy, w, h, fi) local(tw2, th2, k)
(
  gfx_setfont(fi); gfx_measurestr(s, tw2, th2);
  k = min(1, min((w - 4) * gs / max(1, tw2), (h - 1) * gs / max(1, th2)));
  k < 0.999 ? gfx_setfont(9, "Arial", fsz(fi) * k, fbd(fi) ? 'b' : 0);
  txt(s, cx, cy, 1);
);
// Returns the width of string s in design units (measured in the current font, divided by gs).
function tws(s) local(tw2, th2) ( gfx_measurestr(s, tw2, th2); tw2 / gs; );
// knob values in ONE size - fitted to the reference "1.50 m" (the widest usual value), so the size does not jump
// from knob to knob or while turning; only a value wider than that gets smaller
function txtboxR(s, cx, cy, w, h, fi) local(tw2, th2, tr, k)
(
  gfx_setfont(fi); gfx_measurestr("1.50 m", tr, th2); gfx_measurestr(s, tw2, th2);
  k = min(1, min((w - 4) * gs / max(1, max(tw2, tr)), (h - 1) * gs / max(1, th2)));
  k < 0.999 ? gfx_setfont(9, "Arial", fsz(fi) * k, fbd(fi) ? 'b' : 0);
  txt(s, cx, cy, 1);
);
// (txtplate, the text on a backing plate, follows after txtfit)
// (txtbox / txtboxR above shrink the font to fit; txtfit cuts the text instead)
// text that must stay inside a box (dropdowns): shortened with "…" when the scaled font is relatively wider
function txtfit(s, x, yc, w) local(n)
(
  strcpy(#tfit, s);
  tws(#tfit) > w ? (
    n = strlen(#tfit);
    while ( n -= 1; str_setlen(#tfit, n); strcpy(#tfit2, #tfit); strcat(#tfit2, "…"); n > 1 && tws(#tfit2) > w; );
    strcpy(#tfit, #tfit2);
  );
  txt(#tfit, x, yc, 0);
);
// Draws text s in the current color on a rounded 88 percent surface-color plate (4 px padding) at x,yc; al 0 left, 1 center, 2 right.
function txtplate(s, x, yc, al) local(w, h, x0, tr, tg, tb)
(
  tr = gfx_r; tg = gfx_g; tb = gfx_b;
  w = tws(s) + 8; h = 14;
  x0 = al == 1 ? x - w / 2 : (al == 2 ? x - w : x);
  col(cSURF, 0.88); RR(x0, yc - h / 2, w, h, 4);
  gfx_r = tr; gfx_g = tg; gfx_b = tb; gfx_a = 1;
  txt(s, x0 + 4, yc, 0);
);
// automation notifications in the documented sliderX form (one slider per call)
function sauto(i) (
  i == 1 ? slider_automate(slider1, 0);
  i == 2 ? slider_automate(slider2, 0);
  i == 3 ? slider_automate(slider3, 0);
  i == 4 ? slider_automate(slider4, 0);
  i == 5 ? slider_automate(slider5, 0);
  i == 6 ? slider_automate(slider6, 0);
  i == 7 ? slider_automate(slider7, 0);
  i == 8 ? slider_automate(slider8, 0);
  i == 9 ? slider_automate(slider9, 0);
  i == 10 ? slider_automate(slider10, 0);
  i == 11 ? slider_automate(slider11, 0);
  i == 12 ? slider_automate(slider12, 0);
  i == 13 ? slider_automate(slider13, 0);
  i == 14 ? slider_automate(slider14, 0);
  i == 15 ? slider_automate(slider15, 0);
  i == 16 ? slider_automate(slider16, 0);
  i == 17 ? slider_automate(slider17, 0);
  i == 18 ? slider_automate(slider18, 0);
  i == 19 ? slider_automate(slider19, 0);
  i == 20 ? slider_automate(slider20, 0);
  i == 21 ? slider_automate(slider21, 0);
  i == 22 ? slider_automate(slider22, 0);
  i == 23 ? slider_automate(slider23, 0);
  i == 24 ? slider_automate(slider24, 0);
  i == 25 ? slider_automate(slider25, 0);
  i == 26 ? slider_automate(slider26, 0);
  i == 27 ? slider_automate(slider27, 0);
  i == 28 ? slider_automate(slider28, 0);
  i == 29 ? slider_automate(slider29, 0);
  i == 30 ? slider_automate(slider30, 0);
  i == 31 ? slider_automate(slider31, 0);
  i == 32 ? slider_automate(slider32, 0);
  i == 33 ? slider_automate(slider33, 0);
  i == 34 ? slider_automate(slider34, 0);
  i == 35 ? slider_automate(slider35, 0);
  i == 36 ? slider_automate(slider36, 0);
  i == 37 ? slider_automate(slider37, 0);
  i == 38 ? slider_automate(slider38, 0);
  i == 39 ? slider_automate(slider39, 0);
  i == 40 ? slider_automate(slider40, 0);
  i == 41 ? slider_automate(slider41, 0);
  i == 42 ? slider_automate(slider42, 0);
  i == 43 ? slider_automate(slider43, 0);
  i == 44 ? slider_automate(slider44, 0);
  i == 45 ? slider_automate(slider45, 0);
  i == 46 ? slider_automate(slider46, 0);
  i == 47 ? slider_automate(slider47, 0);
  i == 48 ? slider_automate(slider48, 0);
  i == 49 ? slider_automate(slider49, 0);
  i == 50 ? slider_automate(slider50, 0);
  i == 51 ? slider_automate(slider51, 0);
  i == 52 ? slider_automate(slider52, 0);
  i == 53 ? slider_automate(slider53, 0);
  i == 54 ? slider_automate(slider54, 0);
  i == 55 ? slider_automate(slider55, 0);
  i == 56 ? slider_automate(slider56, 0);
  i == 57 ? slider_automate(slider57, 0);
  i == 58 ? slider_automate(slider58, 0);
  i == 59 ? slider_automate(slider59, 0);
  i == 60 ? slider_automate(slider60, 0);
  i == 61 ? slider_automate(slider61, 0);
  i == 62 ? slider_automate(slider62, 0);
  i == 63 ? slider_automate(slider63, 0);
  i == 64 ? slider_automate(slider64, 0);
  i == 65 ? slider_automate(slider65, 0);
  i == 66 ? slider_automate(slider66, 0);
  i == 67 ? slider_automate(slider67, 0);
  i == 68 ? slider_automate(slider68, 0);
  i == 69 ? slider_automate(slider69, 0);
  i == 70 ? slider_automate(slider70, 0);
  i == 71 ? slider_automate(slider71, 0);
  i == 72 ? slider_automate(slider72, 0);
  i == 73 ? slider_automate(slider73, 0);
  i == 74 ? slider_automate(slider74, 0);
  i == 75 ? slider_automate(slider75, 0);
  i == 76 ? slider_automate(slider76, 0);
  i == 77 ? slider_automate(slider77, 0);
  i == 78 ? slider_automate(slider78, 0);
  i == 79 ? slider_automate(slider79, 0);
  i == 80 ? slider_automate(slider80, 0);
  i == 81 ? slider_automate(slider81, 0);
  i == 82 ? slider_automate(slider82, 0);
  i == 83 ? slider_automate(slider83, 0);
  i == 84 ? slider_automate(slider84, 0);
  i == 85 ? slider_automate(slider85, 0);
  i == 86 ? slider_automate(slider86, 0);
  i == 87 ? slider_automate(slider87, 0);
  i == 88 ? slider_automate(slider88, 0);
  i == 89 ? slider_automate(slider89, 0);
  i == 90 ? slider_automate(slider90, 0);
  i == 91 ? slider_automate(slider91, 0);
  i == 92 ? slider_automate(slider92, 0);
  i == 93 ? slider_automate(slider93, 0);
  i == 94 ? slider_automate(slider94, 0);
  i == 95 ? slider_automate(slider95, 0);
  i == 96 ? slider_automate(slider96, 0);
  i == 97 ? slider_automate(slider97, 0);
  i == 98 ? slider_automate(slider98, 0);
  i == 99 ? slider_automate(slider99, 0);
  i == 100 ? slider_automate(slider100, 0);
  i == 101 ? slider_automate(slider101, 0);
  i == 102 ? slider_automate(slider102, 0);
  i == 103 ? slider_automate(slider103, 0);
  i == 104 ? slider_automate(slider104, 0);
  i == 105 ? slider_automate(slider105, 0);
  i == 106 ? slider_automate(slider106, 0);
  i == 107 ? slider_automate(slider107, 0);
  i == 108 ? slider_automate(slider108, 0);
  i == 109 ? slider_automate(slider109, 0);
  i == 110 ? slider_automate(slider110, 0);
  i == 111 ? slider_automate(slider111, 0); i == 112 ? slider_automate(slider112, 0); i == 113 ? slider_automate(slider113, 0); i == 114 ? slider_automate(slider114, 0); i == 115 ? slider_automate(slider115, 0); i == 116 ? slider_automate(slider116, 0); i == 117 ? slider_automate(slider117, 0); i == 118 ? slider_automate(slider118, 0); i == 119 ? slider_automate(slider119, 0);
);
// Calls slider_automate(sliderN, 1) for slider number i (1-based), ending the automation touch started by sauto(i).
function sauto_end(i) (
  i == 1 ? slider_automate(slider1, 1);
  i == 2 ? slider_automate(slider2, 1);
  i == 3 ? slider_automate(slider3, 1);
  i == 4 ? slider_automate(slider4, 1);
  i == 5 ? slider_automate(slider5, 1);
  i == 6 ? slider_automate(slider6, 1);
  i == 7 ? slider_automate(slider7, 1);
  i == 8 ? slider_automate(slider8, 1);
  i == 9 ? slider_automate(slider9, 1);
  i == 10 ? slider_automate(slider10, 1);
  i == 11 ? slider_automate(slider11, 1);
  i == 12 ? slider_automate(slider12, 1);
  i == 13 ? slider_automate(slider13, 1);
  i == 14 ? slider_automate(slider14, 1);
  i == 15 ? slider_automate(slider15, 1);
  i == 16 ? slider_automate(slider16, 1);
  i == 17 ? slider_automate(slider17, 1);
  i == 18 ? slider_automate(slider18, 1);
  i == 19 ? slider_automate(slider19, 1);
  i == 20 ? slider_automate(slider20, 1);
  i == 21 ? slider_automate(slider21, 1);
  i == 22 ? slider_automate(slider22, 1);
  i == 23 ? slider_automate(slider23, 1);
  i == 24 ? slider_automate(slider24, 1);
  i == 25 ? slider_automate(slider25, 1);
  i == 26 ? slider_automate(slider26, 1);
  i == 27 ? slider_automate(slider27, 1);
  i == 28 ? slider_automate(slider28, 1);
  i == 29 ? slider_automate(slider29, 1);
  i == 30 ? slider_automate(slider30, 1);
  i == 31 ? slider_automate(slider31, 1);
  i == 32 ? slider_automate(slider32, 1);
  i == 33 ? slider_automate(slider33, 1);
  i == 34 ? slider_automate(slider34, 1);
  i == 35 ? slider_automate(slider35, 1);
  i == 36 ? slider_automate(slider36, 1);
  i == 37 ? slider_automate(slider37, 1);
  i == 38 ? slider_automate(slider38, 1);
  i == 39 ? slider_automate(slider39, 1);
  i == 40 ? slider_automate(slider40, 1);
  i == 41 ? slider_automate(slider41, 1);
  i == 42 ? slider_automate(slider42, 1);
  i == 43 ? slider_automate(slider43, 1);
  i == 44 ? slider_automate(slider44, 1);
  i == 45 ? slider_automate(slider45, 1);
  i == 46 ? slider_automate(slider46, 1);
  i == 47 ? slider_automate(slider47, 1);
  i == 48 ? slider_automate(slider48, 1);
  i == 49 ? slider_automate(slider49, 1);
  i == 50 ? slider_automate(slider50, 1);
  i == 51 ? slider_automate(slider51, 1);
  i == 52 ? slider_automate(slider52, 1);
  i == 53 ? slider_automate(slider53, 1);
  i == 54 ? slider_automate(slider54, 1);
  i == 55 ? slider_automate(slider55, 1);
  i == 56 ? slider_automate(slider56, 1);
  i == 57 ? slider_automate(slider57, 1);
  i == 58 ? slider_automate(slider58, 1);
  i == 59 ? slider_automate(slider59, 1);
  i == 60 ? slider_automate(slider60, 1);
  i == 61 ? slider_automate(slider61, 1);
  i == 62 ? slider_automate(slider62, 1);
  i == 63 ? slider_automate(slider63, 1);
  i == 64 ? slider_automate(slider64, 1);
  i == 65 ? slider_automate(slider65, 1);
  i == 66 ? slider_automate(slider66, 1);
  i == 67 ? slider_automate(slider67, 1);
  i == 68 ? slider_automate(slider68, 1);
  i == 69 ? slider_automate(slider69, 1);
  i == 70 ? slider_automate(slider70, 1);
  i == 71 ? slider_automate(slider71, 1);
  i == 72 ? slider_automate(slider72, 1);
  i == 73 ? slider_automate(slider73, 1);
  i == 74 ? slider_automate(slider74, 1);
  i == 75 ? slider_automate(slider75, 1);
  i == 76 ? slider_automate(slider76, 1);
  i == 77 ? slider_automate(slider77, 1);
  i == 78 ? slider_automate(slider78, 1);
  i == 79 ? slider_automate(slider79, 1);
  i == 80 ? slider_automate(slider80, 1);
  i == 81 ? slider_automate(slider81, 1);
  i == 82 ? slider_automate(slider82, 1);
  i == 83 ? slider_automate(slider83, 1);
  i == 84 ? slider_automate(slider84, 1);
  i == 85 ? slider_automate(slider85, 1);
  i == 86 ? slider_automate(slider86, 1);
  i == 87 ? slider_automate(slider87, 1);
  i == 88 ? slider_automate(slider88, 1);
  i == 89 ? slider_automate(slider89, 1);
  i == 90 ? slider_automate(slider90, 1);
  i == 91 ? slider_automate(slider91, 1);
  i == 92 ? slider_automate(slider92, 1);
  i == 93 ? slider_automate(slider93, 1);
  i == 94 ? slider_automate(slider94, 1);
  i == 95 ? slider_automate(slider95, 1);
  i == 96 ? slider_automate(slider96, 1);
  i == 97 ? slider_automate(slider97, 1);
  i == 98 ? slider_automate(slider98, 1);
  i == 99 ? slider_automate(slider99, 1);
  i == 100 ? slider_automate(slider100, 1);
  i == 101 ? slider_automate(slider101, 1);
  i == 102 ? slider_automate(slider102, 1);
  i == 103 ? slider_automate(slider103, 1);
  i == 104 ? slider_automate(slider104, 1);
  i == 105 ? slider_automate(slider105, 1);
  i == 106 ? slider_automate(slider106, 1);
  i == 107 ? slider_automate(slider107, 1);
  i == 108 ? slider_automate(slider108, 1);
  i == 109 ? slider_automate(slider109, 1);
  i == 110 ? slider_automate(slider110, 1);
  i == 111 ? slider_automate(slider111, 1); i == 112 ? slider_automate(slider112, 1); i == 113 ? slider_automate(slider113, 1); i == 114 ? slider_automate(slider114, 1); i == 115 ? slider_automate(slider115, 1); i == 116 ? slider_automate(slider116, 1); i == 117 ? slider_automate(slider117, 1); i == 118 ? slider_automate(slider118, 1); i == 119 ? slider_automate(slider119, 1);
);
// Calls slider_automate(sliderN) with no touch flag for slider number i (1-based), a one-shot automation notification.
function sauto1(i) (
  i == 1 ? slider_automate(slider1);
  i == 2 ? slider_automate(slider2);
  i == 3 ? slider_automate(slider3);
  i == 4 ? slider_automate(slider4);
  i == 5 ? slider_automate(slider5);
  i == 6 ? slider_automate(slider6);
  i == 7 ? slider_automate(slider7);
  i == 8 ? slider_automate(slider8);
  i == 9 ? slider_automate(slider9);
  i == 10 ? slider_automate(slider10);
  i == 11 ? slider_automate(slider11);
  i == 12 ? slider_automate(slider12);
  i == 13 ? slider_automate(slider13);
  i == 14 ? slider_automate(slider14);
  i == 15 ? slider_automate(slider15);
  i == 16 ? slider_automate(slider16);
  i == 17 ? slider_automate(slider17);
  i == 18 ? slider_automate(slider18);
  i == 19 ? slider_automate(slider19);
  i == 20 ? slider_automate(slider20);
  i == 21 ? slider_automate(slider21);
  i == 22 ? slider_automate(slider22);
  i == 23 ? slider_automate(slider23);
  i == 24 ? slider_automate(slider24);
  i == 25 ? slider_automate(slider25);
  i == 26 ? slider_automate(slider26);
  i == 27 ? slider_automate(slider27);
  i == 28 ? slider_automate(slider28);
  i == 29 ? slider_automate(slider29);
  i == 30 ? slider_automate(slider30);
  i == 31 ? slider_automate(slider31);
  i == 32 ? slider_automate(slider32);
  i == 33 ? slider_automate(slider33);
  i == 34 ? slider_automate(slider34);
  i == 35 ? slider_automate(slider35);
  i == 36 ? slider_automate(slider36);
  i == 37 ? slider_automate(slider37);
  i == 38 ? slider_automate(slider38);
  i == 39 ? slider_automate(slider39);
  i == 40 ? slider_automate(slider40);
  i == 41 ? slider_automate(slider41);
  i == 42 ? slider_automate(slider42);
  i == 43 ? slider_automate(slider43);
  i == 44 ? slider_automate(slider44);
  i == 45 ? slider_automate(slider45);
  i == 46 ? slider_automate(slider46);
  i == 47 ? slider_automate(slider47);
  i == 48 ? slider_automate(slider48);
  i == 49 ? slider_automate(slider49);
  i == 50 ? slider_automate(slider50);
  i == 51 ? slider_automate(slider51);
  i == 52 ? slider_automate(slider52);
  i == 53 ? slider_automate(slider53);
  i == 54 ? slider_automate(slider54);
  i == 55 ? slider_automate(slider55);
  i == 56 ? slider_automate(slider56);
  i == 57 ? slider_automate(slider57);
  i == 58 ? slider_automate(slider58);
  i == 59 ? slider_automate(slider59);
  i == 60 ? slider_automate(slider60);
  i == 61 ? slider_automate(slider61);
  i == 62 ? slider_automate(slider62);
  i == 63 ? slider_automate(slider63);
  i == 64 ? slider_automate(slider64);
  i == 65 ? slider_automate(slider65);
  i == 66 ? slider_automate(slider66);
  i == 67 ? slider_automate(slider67);
  i == 68 ? slider_automate(slider68);
  i == 69 ? slider_automate(slider69);
  i == 70 ? slider_automate(slider70);
  i == 71 ? slider_automate(slider71);
  i == 72 ? slider_automate(slider72);
  i == 73 ? slider_automate(slider73);
  i == 74 ? slider_automate(slider74);
  i == 75 ? slider_automate(slider75);
  i == 76 ? slider_automate(slider76);
  i == 77 ? slider_automate(slider77);
  i == 78 ? slider_automate(slider78);
  i == 79 ? slider_automate(slider79);
  i == 80 ? slider_automate(slider80);
  i == 81 ? slider_automate(slider81);
  i == 82 ? slider_automate(slider82);
  i == 83 ? slider_automate(slider83);
  i == 84 ? slider_automate(slider84);
  i == 85 ? slider_automate(slider85);
  i == 86 ? slider_automate(slider86);
  i == 87 ? slider_automate(slider87);
  i == 88 ? slider_automate(slider88);
  i == 89 ? slider_automate(slider89);
  i == 90 ? slider_automate(slider90);
  i == 91 ? slider_automate(slider91);
  i == 92 ? slider_automate(slider92);
  i == 93 ? slider_automate(slider93);
  i == 94 ? slider_automate(slider94);
  i == 95 ? slider_automate(slider95);
  i == 96 ? slider_automate(slider96);
  i == 97 ? slider_automate(slider97);
  i == 98 ? slider_automate(slider98);
  i == 99 ? slider_automate(slider99);
  i == 100 ? slider_automate(slider100);
  i == 101 ? slider_automate(slider101);
  i == 102 ? slider_automate(slider102);
  i == 103 ? slider_automate(slider103);
  i == 104 ? slider_automate(slider104);
  i == 105 ? slider_automate(slider105);
  i == 106 ? slider_automate(slider106);
  i == 107 ? slider_automate(slider107);
  i == 108 ? slider_automate(slider108);
  i == 109 ? slider_automate(slider109);
  i == 110 ? slider_automate(slider110);
  i == 111 ? slider_automate(slider111); i == 112 ? slider_automate(slider112); i == 113 ? slider_automate(slider113); i == 114 ? slider_automate(slider114); i == 115 ? slider_automate(slider115); i == 116 ? slider_automate(slider116); i == 117 ? slider_automate(slider117); i == 118 ? slider_automate(slider118); i == 119 ? slider_automate(slider119);
);
function autom(m) local(i) ( i = 1; loop(31, (m & (2 ^ (i - 1))) ? sauto1(i); i += 1; ); );   // one-shot
// snapshot the sliders, then notify only those whose value changed
// A batch of parameter changes from the GUI (preset, snapshot, reset, model): gbusy = 1 while the GUI writes them, so the
// audio thread does not pick up half of the new values (e.g. the new room size with the old positions) - it applies the
// whole batch in its next block
function snap_take() local(i) ( gbusy = 1; i = 1; loop(NSL, snapv[i] = slider(i); i += 1; ); );
// Sends slider_automate for every slider 1..NSL whose value differs from the stored snapv[] copy, then clears gbusy.
function snap_notify() local(i, m, m32)
(
  i = 1;
  loop(NSL, abs(slider(i) - snapv[i]) > 0.0000001 ? sauto1(i); i += 1; );
  gbusy = 0;
);
// a source preset (see spTb): model, distortion, EQ flat, call line, model on, mix 100 %
function spapply(k) local(b, i) ( b = spTb + k * 8; snap_take(); sset(35, b[1]); sset(37, b[2]); i = 47; loop(13, sset(i, spq(b[7], i)); i += 1; );
  sset(111, b[3]); sset(112, b[4]); sset(113, b[5]); sset(114, b[6]); sset(85, 1); sset(89, 100); snap_notify(); );

// ---- snapshot names: stored as bytes (one per value) so that @serialize can keep them with the slots ----
function sn_str(b, s) local(i, z) (
  strcpy(s, ""); i = 0; z = 0;
  loop(SNL, b[i] <= 0 ? ( z = 1; ); !z ? ( sprintf(#snch, "%c", b[i]); strcat(s, #snch); ); i += 1; );
  s;
);
function sn_add(c) local(n, tb, tln, tmx) (     // append one character (Unicode code point) as UTF-8 to the active field
  tb = snf ? dseb : sneb; tln = snf ? dselen : snelen; tmx = snf ? DSL : SNL;
  n = c < 128 ? 1 : (c < 2048 ? 2 : (c < 65536 ? 3 : 4));
  c >= 32 && tln + n <= tmx - 1 ? (
    n == 1 ? ( tb[tln] = c; ) : (
    n == 2 ? ( tb[tln] = 192 | (c >> 6); tb[tln + 1] = 128 | (c & 63); ) : (
    n == 3 ? ( tb[tln] = 224 | (c >> 12); tb[tln + 1] = 128 | ((c >> 6) & 63); tb[tln + 2] = 128 | (c & 63); ) : (
      tb[tln] = 240 | (c >> 18); tb[tln + 1] = 128 | ((c >> 12) & 63); tb[tln + 2] = 128 | ((c >> 6) & 63); tb[tln + 3] = 128 | (c & 63); )));
    tln += n; tb[tln] = 0;
    snf ? ( dselen = tln; ) : ( snelen = tln; );
  );
);
function sn_back() local(c, tb, tln) (           // backspace: remove the last character (all its UTF-8 bytes)
  tb = snf ? dseb : sneb; tln = snf ? dselen : snelen; c = 128;
  while ( tln > 0 && (c & 192) == 128 ? ( tln -= 1; c = tb[tln]; tb[tln] = 0; 1; ) : 0; );
  snf ? ( dselen = tln; ) : ( snelen = tln; );
);
function sn_strn(b, n, s) local(i, z) (           // bytes -> string (n bytes max, 0 = end)
  strcpy(s, ""); i = 0; z = 0;
  loop(n, b[i] <= 0 ? ( z = 1; ); !z ? ( sprintf(#snch, "%c", b[i]); strcat(s, #snch); ); i += 1; );
  s;
);
function sn_sub(b, a, e, s) local(i) (            // bytes a … e-1 -> string
  strcpy(s, ""); i = a;
  loop(max(0, e - a), sprintf(#snch, "%c", b[i]); strcat(s, #snch); i += 1; );
  s;
);
// word-wrapped text (UTF-8 bytes), max ml lines; snCx / snCy = end of the text (for the caret)
function sn_wrap(b, n, x, y, w, wlh, ml) local(ls, i, i2, sp, fit, e, wln, wlw, wch)
(
  ls = 0; wln = 0; wlw = 0;
  while (
    i = ls; sp = -1; fit = 1; strcpy(#swl, "");
    while ( i < n && fit ? (
        i2 = i + 1; while ( i2 < n && (b[i2] & 192) == 128 ? ( i2 += 1; 1; ) : 0; );
        strcpy(#swc, #swl); strcat(#swc, sn_sub(b, i, i2, #swt)); gfx_measurestr(#swc, wlw, wch);
        wlw > w * gs && i > ls ? ( fit = 0; ) : ( b[i] == 32 ? ( sp = i; ); strcpy(#swl, #swc); i = i2; );
        1;
      ) : 0; );
    e = i;
    e < n && sp > ls ? ( e = sp + 1; );
    sn_sub(b, ls, e, #swl);
    wln < ml ? txt(#swl, x, y + wln * wlh, 0);
    ls = e; wln += 1;
    ls < n && wln < ml + 1;
  );
  snCx = x + tws(#swl); snCy = y + min(wln - 1, ml - 1) * wlh;
);
function snslx(k) ( basic ? 346 + k * 20 : 346 + (k % 8) * 17; );   // snapshot button k: left edge in the top bar (Basic: one row, larger buttons)
function snsly(k) ( basic ? 14 : 10 + floor(k / 8) * 15; );   // snapshot button k: top edge; larger snapshot buttons (15 x 13) in Expert
// Opens the snapshot editor for slot k: copies its name (snam) and description (dsc) into the edit buffers and sets snedit=k+1, name field active.
function sn_open(k) local(i) (
  snelen = 0; i = 0;
  loop(SNL, sneb[i] = snam[k * SNL + i]; snelen == i && sneb[i] > 0 ? ( snelen += 1; ); i += 1; );
  sneb[snelen] = 0;
  dselen = 0; i = 0;
  loop(DSL, dseb[i] = dsc[k * DSL + i]; dselen == i && dseb[i] > 0 ? ( dselen += 1; ); i += 1; );
  dseb[dselen] = 0;
  snf = 0; snedit = k + 1; snt0 = time_precise();
);
// snapshot popup geometry (top-bar scale): centered under the top bar
function sn_geo() (
  snpx = min(max(4, (WTN - SNPW) / 2 + pdSX), WTN - SNPW - 4); snpy = min(max(4, 40 + pdSY), HTN - SNPH - 4); snby = snpy + SNPH - 28;
  snfx = snpx + 12; snfw = SNPW - 24; snny = snpy + 42; snnh = 24; sndy = snny + snnh + 22; sndh = 54;
);
// Returns true if the mouse (mx,my) is inside the rectangle x,y,w,h.
function inbox(x, y, w, h) ( mx >= x && mx < x + w && my >= y && my < y + h; );
// If the mouse is in the rect x,y,w,h, requests tooltip text s (tipon, tipstr) anchored at its center, below it when y < 60 else above.
function tipbox(x, y, w, h, s) ( inbox(x, y, w, h) ? ( tipon = 1; tipstr = s; tipx = x + w / 2; tipy = y < 60 ? y + h + 12 : y - 12; ); );
// icon buttons: the tooltip also with Tips off - then only the name and a short line (Tips on: the full text)
function itip(x, y, w, h, sf, ss) ( inbox(x, y, w, h) ? ( tipbox(x, y, w, h, tipShow ? sf : ss); tipforce = !tipShow && !(mouse_cap & 1); ); );
// Tooltip s over a knob centered at (cx,cy): hit rect of 44 x 48 units scaled by KSC (calls tipbox).
function knobtip(cx, cy, s) ( tipbox(cx - 22 * KSC, cy - 14 * KSC, 44 * KSC, 48 * KSC, s); );
// Sets the font sizes FS1-FS7 from the display scale gs (20 percent larger in zoomed tab modules) and creates Arial font slots 1-7 and 11-14.
function setfonts()
(
  fbz = tabm && szoom && gs == gsZ && gsZ != gsN ? 1.2 : 1; fgs = gs * fbz;   // text in the tab modules 20 % larger
  FS6 = 8.5 * fgs; FS7 = 8 * fgs; FS1 = 8.5 * fgs; FS2 = 9.5 * fgs; FS3 = 11 * fgs; FS4 = 8.5 * fgs; FS5 = 13.5 * fgs;
  gfx_setfont(6, "Arial", FS6, 'b');        // small buttons, chips
  gfx_setfont(7, "Arial", FS7);             // small hints
  gfx_setfont(1, "Arial", FS1, 'b');        // labels
  gfx_setfont(2, "Arial", FS2, 'b');        // headings, knob values
  gfx_setfont(3, "Arial", FS3, 'b');        // values (Basic knobs)
  gfx_setfont(4, "Arial", FS4);             // small text, knob names
  gfx_setfont(5, "Arial", FS5, 'b');        // title
  gfx_setfont(11, "Arial", 6.5 * gs);        // tiny label (Snapshots)
  gfx_setfont(12, "Arial", 7 * gs, 'b');     // snapshot numbers
  gfx_setfont(13, "Arial", 10 * gs);         // info window: names
  gfx_setfont(14, "Arial", 10.5 * gs, 'b');  // info window: values
);
// initial button (a letter on a soft rounded square): filled in color c when active, light gray otherwise; the tooltip shows on hover; returns 1 on click
function rbtn(x, y, rad, s, act, c, tip) local(hit)
(
  // icon button: a soft rounded square - filled in its color when active, light gray otherwise (darker under the mouse)
  hit = sqr(mx - x) + sqr(my - y) < sqr(rad + 2);
  act ? ( col(c, 1); RR(x - rad - 1, y - rad - 1, 2 * rad + 2, 2 * rad + 2, 3.5); col(cSURF, 1); )
      : ( col(hit ? cHAIR : cLINE, 1); RR(x - rad - 1, y - rad - 1, 2 * rad + 2, 2 * rad + 2, 3.5); col(hit ? cTEXT : cMUTED, 1); );
  gfx_setfont(8, "Arial", floor(rad * 1.45 * gs + 0.5), 'b'); txtc(s, x, y);
  hit ? ( tipon = 1; tipstr = tip; tipx = x; tipy = y < 60 ? y + rad + 14 : y - rad - 12; );
  click && hit;
);
// chevrons (thin strokes): down = open / menu, right = folded
function chevd(x, yc, c) ( col(c, 1); LNW(x - 3.4, yc - 1.6, x, yc + 1.8, 1.4); LNW(x, yc + 1.8, x + 3.4, yc - 1.6, 1.4); );
// Draws a thin right-pointing chevron (folded state) centered at x,yc in color c.
function chevr(x, yc, c) ( col(c, 1); LNW(x - 1.6, yc - 3.4, x + 1.8, yc, 1.4); LNW(x + 1.8, yc, x - 1.6, yc + 3.4, 1.4); );
// track of a pill-style segmented switch (the items are drawn on it with segp)
function segtrack(x, y, w, h) ( col(cLINE, 1); RR(x, y, w, h, h / 2); );
// Blends two 0xRRGGBB colors per channel by t (0 = c1, 1 = c2) and returns the packed color.
function mixc(c1, c2, t) ( floor(floor(c1 / 65536) * (1 - t) + floor(c2 / 65536) * t + 0.5) * 65536 + floor((floor(c1 / 256) % 256) * (1 - t) + (floor(c2 / 256) % 256) * t + 0.5) * 256 + floor((c1 % 256) * (1 - t) + (c2 % 256) * t + 0.5); );
// TEXT ON A COLOR (now one fixed rule per mode instead of picking by contrast): oncol = text color on a fill,
// chfill = in light mode deepens a pastel fill until white text has enough contrast (rlum = relative luminance, ratio >= 3.2)
function rlum(c) local(r, g, b) ( r = (floor(c / 65536) / 255) ^ 2.2; g = ((floor(c / 256) % 256) / 255) ^ 2.2; b = ((c % 256) / 255) ^ 2.2; 0.2126 * r + 0.7152 * g + 0.0722 * b; );
function oncol(c) ( darkmode ? cSURF : 0xFFFFFF; );   // one rule per mode - light: white text, dark: dark text
function chfill(c) local(k) ( k = 0; !darkmode ? ( while ( 1.05 / (rlum(c) + 0.05) < 3.2 && k < 8 ? ( c = mixc(c, 0x101828, 0.08); k += 1; 1; ) : 0; ); ); c; );   // light: deepen a pastel fill until white reads
// Draws item k of n in a pill-style segmented switch (filled in color c when act) and returns 1 if it was clicked.
function segp(x, y, w, h, n, k, s, act, c) local(iw, ix, hit)
(
  cCTLon ? ( c = cTEXT; );
  iw = w / n; ix = x + k * iw; hit = inbox(ix, y, iw, h);
  act ? ( c == cTEXT ? ( col(cACT, 1); RR(ix + 2, y + 2, iw - 4, h - 4, (h - 4) / 2); cCTLon ? ( col(cACTO, 1); RRO(ix + 2, y + 2, iw - 4, h - 4, (h - 4) / 2); RRO(ix + 2.5, y + 2.5, iw - 5, h - 5, (h - 4) / 2); ); col(cACTX, 1); ) : ( col(monoOn ? c : chfill(c), 1); RR(ix + 2, y + 2, iw - 4, h - 4, (h - 4) / 2); col(monoOn ? cSURF : oncol(c), 1); ); ) : col(hit ? cTEXT : cMUTED, 1);   // neutral active = soft blue fill + blue outline
  txtbox(s, ix + iw / 2, y + h / 2, iw - 4, h - 2, 6);
  click && hit;
);
// BARRIER icons (line drawings, ~16 x 16 around cx, cy): 1 wall (bricks), 2 window (sash cross), 3 door (leaf + handle)
function LNBOX(x, y, w, h, wd) ( LNW(x, y, x + w, y, wd); LNW(x + w, y, x + w, y + h, wd); LNW(x + w, y + h, x, y + h, wd); LNW(x, y + h, x, y, wd); );   // a box of lines (round joins)
// Draws a ~16x16 line icon centered at cx,cy in color c: barrier type t = 1 wall (bricks), 2 window, 3 door.
function baricon(t, cx, cy, c)
(
// finer line icons (0.9 / 0.7), all in a 16 x 16 box: wall = bricks in offset rows, window = frame, sashes, sill,
  // door = frame on the floor, the leaf with a handle
  col(c, 1);
  t == 1 ? (
    LNBOX(cx - 8, cy - 6.5, 16, 13, 0.9);
    LNW(cx - 8, cy - 2.2, cx + 8, cy - 2.2, 0.7); LNW(cx - 8, cy + 2.2, cx + 8, cy + 2.2, 0.7);
    LNW(cx - 1.5, cy - 6.5, cx - 1.5, cy - 2.2, 0.7); LNW(cx + 4, cy - 2.2, cx + 4, cy + 2.2, 0.7); LNW(cx - 5, cy - 2.2, cx - 5, cy + 2.2, 0.7); LNW(cx + 0.5, cy + 2.2, cx + 0.5, cy + 6.5, 0.7);
  );
  t == 2 ? (
    LNBOX(cx - 6.5, cy - 7.5, 13, 14, 0.9); LNBOX(cx - 4.8, cy - 5.8, 9.6, 10.6, 0.7);
    LNW(cx, cy - 5.8, cx, cy + 4.8, 0.7); LNW(cx - 4.8, cy - 0.5, cx + 4.8, cy - 0.5, 0.7);
    LNW(cx - 8.5, cy + 8.3, cx + 8.5, cy + 8.3, 0.9);
  );
  t == 3 ? (
    LNW(cx - 6, cy + 8.3, cx - 6, cy - 7.5, 0.9); LNW(cx - 6, cy - 7.5, cx + 6, cy - 7.5, 0.9); LNW(cx + 6, cy - 7.5, cx + 6, cy + 8.3, 0.9);
    LNBOX(cx - 4.2, cy - 5.8, 8.4, 14.1, 0.7); CIF(cx + 2.2, cy + 1.2, 0.9, 1);
    LNW(cx - 9, cy + 8.3, cx + 9, cy + 8.3, 0.9);
  );
);
// Draws a rounded surface card (radius 8; bordered in mono mode) and stores its position/width in crdX, crdY, crdW.
function card(x, y, w, h) ( crdX = x; crdY = y; crdW = w; crdH = h; col(cSURF, 1); RR(x, y, w, h, 8); monoOn ? ( col(cBORD, 1); RRO(x, y, w, h, 8); ); );
// (the module ON / OFF switch is the round power button tgl() below; chip is the pill toggle)
// pill toggle: filled in its color when on, outlined when off; returns 1 on click
function chip(x, y, w, h, s, act, c) local(hit)
(
  cCTLon ? ( c = cTEXT; );
  hit = inbox(x, y, w, h);
  act ? ( c == cTEXT ? ( col(cACT, 1); RR(x, y, w, h, h / 2); cCTLon ? ( col(cACTO, 1); RRO(x, y, w, h, h / 2); RRO(x + 0.5, y + 0.5, w - 1, h - 1, h / 2); ); col(cACTX, 1); ) : ( col(monoOn ? c : chfill(c), 1); RR(x, y, w, h, h / 2); col(monoOn ? cSURF : oncol(c), 1); ); )   // neutral active = soft blue fill + blue outline
      : ( col(hit ? cHAIR : cLINE, 1); RR(x, y, w, h, h / 2); col(hit ? cTEXT : cMUTED, 1); );
  txtbox(s, x + w / 2, y + h / 2, w - 2, h - 2, 6);
  click && hit;
);
// ICON BUTTONS (Output header): three buttons of one size - Detail, Equal loudness, Limiter - a pill with an icon,
// filled in the module color when on; the name comes with the tooltip. ibtn() draws the pill and leaves the icon color set
function ibtn(x, y, w, h, act, c) local(hit)
(
  cCTLon ? ( c = cCTL; );
  hit = inbox(x, y, w, h);
  act ? ( cCTLon ? ( col(cACT, 1); RR(x, y, w, h, h / 2); col(cACTO, 1); RRO(x, y, w, h, h / 2); RRO(x + 0.5, y + 0.5, w - 1, h - 1, h / 2); col(cACTX, 1); ) : ( col(monoOn ? c : chfill(c), 1); RR(x, y, w, h, h / 2); col(monoOn ? cSURF : oncol(c), 1); ) ) : ( col(hit ? cHAIR : cLINE, 1); RR(x, y, w, h, h / 2); col(hit ? cTEXT : cMUTED, 1); );
  click && hit;
);
function ic_detail(cx, cy, hi) local(k) (          // three bars of growing height: High = all three, Normal = the third only outlined; 2 = Stereo-safe: two dots joined by a bar (L = R)
  hi == 2 ? ( CI(cx - 4.5, cy, 2.6, 1); CI(cx + 4.5, cy, 2.6, 1); RR(cx - 4.5, cy - 0.7, 9, 1.4, 0.7); ) :   /* 0.311: L = R (was two bars: looked like pause) */
  ( k = 0; loop(3,
    k < 2 || hi ? RR(cx - 6 + k * 4.5, cy + 4 - (3 + k * 2.5), 3, 3 + k * 2.5, 1) : RRO(cx - 6 + k * 4.5, cy + 4 - (3 + k * 2.5), 3, 3 + k * 2.5, 1);
    k += 1; ); );
);
function ic_eql(cx, cy) (                          // a loudspeaker and an equals sign
  RR(cx - 7.5, cy - 2, 2.5, 4, 0.5); POLY4(cx - 5, cy - 2, cx - 1.5, cy - 5, cx - 1.5, cy + 5, cx - 5, cy + 2);
  RR(cx + 2, cy - 2.2, 6, 1.6, 0.8); RR(cx + 2, cy + 0.6, 6, 1.6, 0.8);
);
// Input Guard icon: a soft curve that bends flat under a ceiling line - the input is bent, never cut
function ic_grd(cx, cy) (
  RR(cx - 7, cy - 5.5, 14, 1.4, 0.7);
  LNW(cx - 6, cy + 5, cx - 2, cy + 0.5, 1.4); LNW(cx - 2, cy + 0.5, cx + 1, cy - 2, 1.4); LNW(cx + 1, cy - 2, cx + 3.5, cy - 3, 1.4); LNW(cx + 3.5, cy - 3, cx + 6, cy - 3.3, 1.4);
);
function ic_lim(cx, cy) (                          // peaks under a ceiling line: the limiter holds everything below it
  RR(cx - 7, cy - 5.5, 14, 1.4, 0.7);
  LNW(cx - 6, cy + 5, cx - 3.5, cy - 2.5, 1.4); LNW(cx - 3.5, cy - 2.5, cx - 1, cy + 5, 1.4);
  LNW(cx - 1, cy + 5, cx + 1.5, cy - 0.5, 1.4); LNW(cx + 1.5, cy - 0.5, cx + 3.5, cy + 5, 1.4); LNW(cx + 3.5, cy + 5, cx + 6, cy - 2.5, 1.4);
);
// stacked-arc stroke: width wd as several 1 px arcs (large arcs such as knob rings; small ones use the smooth ARCS)
function ARCST(x, y, rad, a1, a2, wd) local(q)
(
  q = 0;
  loop(max(1, floor(2 * wd * gs + 0.5)), gfx_arc(floor(gox + x * gs + 0.5), floor(goy + y * gs + 0.5), rad * gs - q, a1, a2, 1); q += 0.5; );   // half-pixel steps: no gaps (smoother than a band of quads - measured)
);
// Clears the smooth-stroke buffer (sdN = 0) before collecting strokes for SDDRAW.
function SDB0() ( sdN = 0; );
// stroke record (8 slots in SDFBUF): line = [0, x1, y1, x2, y2], arc = [1, cx, cy, rad, a1, a2]; all in screen pixels
// Adds a line stroke (design units, stored in screen pixels) to the stroke buffer SDFBUF; ignored beyond 10 strokes.
function SDSEG(x1, y1, x2, y2) ( sdN < 10 ? ( SDFBUF[sdN * 8] = 0; SDFBUF[sdN * 8 + 1] = gox + x1 * gs; SDFBUF[sdN * 8 + 2] = goy + y1 * gs; SDFBUF[sdN * 8 + 3] = gox + x2 * gs; SDFBUF[sdN * 8 + 4] = goy + y2 * gs; sdN += 1; ); );
// Adds an arc stroke (center x,y, radius rad, angles a1..a2, 0 = up, clockwise) to the stroke buffer SDFBUF; max 10 strokes.
function SDARC(x, y, rad, a1, a2) ( sdN < 10 ? ( SDFBUF[sdN * 8] = 1; SDFBUF[sdN * 8 + 1] = gox + x * gs; SDFBUF[sdN * 8 + 2] = goy + y * gs; SDFBUF[sdN * 8 + 3] = rad * gs; SDFBUF[sdN * 8 + 4] = a1; SDFBUF[sdN * 8 + 5] = a2; sdN += 1; ); );
// Renders all buffered strokes at width wd as anti-aliased pixels by distance to the nearest stroke, in the current color, then empties the buffer.
function SDDRAW(wd) local(w, x0, y0, x1, y1, k, b, px, py, d, dd, qvx, qvy, t, r, al, cv, ex, ey, ux, uy, l2)
(
  w = wd * gs; al = gfx_a; x0 = 100000; y0 = 100000; x1 = -100000; y1 = -100000; k = 0;
  loop(sdN, b = SDFBUF + k * 8;
    b[0] == 0 ? ( x0 = min(x0, min(b[1], b[3])); x1 = max(x1, max(b[1], b[3])); y0 = min(y0, min(b[2], b[4])); y1 = max(y1, max(b[2], b[4])); )
              : ( x0 = min(x0, b[1] - b[3]); x1 = max(x1, b[1] + b[3]); y0 = min(y0, b[2] - b[3]); y1 = max(y1, b[2] + b[3]); );
    k += 1; );
  x0 = floor(x0 - w / 2 - 1); y0 = floor(y0 - w / 2 - 1); x1 = ceil(x1 + w / 2 + 1); y1 = ceil(y1 + w / 2 + 1);
  py = y0;
  loop(min(200, y1 - y0),
    px = x0;
    loop(min(200, x1 - x0),
      d = 1000; k = 0;
      loop(sdN, b = SDFBUF + k * 8;
        b[0] == 0 ? (
          ux = b[3] - b[1]; uy = b[4] - b[2]; l2 = ux * ux + uy * uy;
          t = l2 > 0.000001 ? min(1, max(0, ((px + 0.5 - b[1]) * ux + (py + 0.5 - b[2]) * uy) / l2)) : 0;
          dd = sqrt(sqr(px + 0.5 - b[1] - t * ux) + sqr(py + 0.5 - b[2] - t * uy));
        ) : (
          qvx = px + 0.5 - b[1]; qvy = py + 0.5 - b[2]; r = sqrt(qvx * qvx + qvy * qvy);
          t = atan2(qvx, -qvy) - b[4]; t -= floor(t / (2 * $pi)) * 2 * $pi;
          t <= b[5] - b[4] ? ( dd = abs(r - b[3]); ) : (
            ex = b[1] + sin(b[4]) * b[3]; ey = b[2] - cos(b[4]) * b[3]; dd = sqrt(sqr(px + 0.5 - ex) + sqr(py + 0.5 - ey));
            ex = b[1] + sin(b[5]) * b[3]; ey = b[2] - cos(b[5]) * b[3]; dd = min(dd, sqrt(sqr(px + 0.5 - ex) + sqr(py + 0.5 - ey))); );
        );
        d = min(d, dd); k += 1; );
      cv = min(1, max(0, w / 2 - d + 0.5));
      cv > 0.01 ? ( gfx_a = al * cv; gfx_rect(px, py, 1, 1); );
      px += 1; );
    py += 1; );
  gfx_a = al; sdN = 0;
);
// a filled disc on the same sub-pixel grid as the strokes (a button and its sign share one center)
function SDDISC(x, y, rad) local(cx, cy, R, x0, y0, n, px, py, cv, al)
(
  cx = gox + x * gs; cy = goy + y * gs; R = rad * gs; al = gfx_a;
  x0 = floor(cx - R - 1); y0 = floor(cy - R - 1); n = min(120, ceil(2 * R + 3)); py = y0;
  loop(n, px = x0;
    loop(n, cv = min(1, max(0, R - sqrt(sqr(px + 0.5 - cx) + sqr(py + 0.5 - cy)) + 0.5));
      cv > 0.01 ? ( gfx_a = al * cv; gfx_rect(px, py, 1, 1); ); px += 1; );
    py += 1; );
  gfx_a = al;
);
// Draws one smooth anti-aliased arc stroke (center x,y, radius rad, angles a1..a2, width wd) via the stroke buffer.
function ARCS(x, y, rad, a1, a2, wd) ( SDB0(); SDARC(x, y, rad, a1, a2); SDDRAW(wd); );
// every small arc (icons, rings up to 16 px) is drawn smooth (distance field); large ones (knobs) stay stacked
function ARC(x, y, rad, a1, a2, wd) ( rad * gs < 16 && gfx_a > 0.05 ? ARCS(x, y, rad, floor(a1 * 200 + 0.5) / 200, floor(a2 * 200 + 0.5) / 200, wd) : ARCST(x, y, rad, a1, a2, wd); );   // (angles in 1/200 rad steps: no sub-pixel creep from frame to frame)
// an arc with round ends (knob value arcs)
function ARCR(x, y, rad, a1, a2, wd) local(r2)
(
  ARC(x, y, rad, a1, a2, wd); r2 = rad - wd / 2;
  wd * gs >= 1.6 && gfx_a > 0.98 ? ( CIF(x + sin(a1) * r2, y - cos(a1) * r2, wd / 2 - 0.25 / gs, 1); CIF(x + sin(a2) * r2, y - cos(a2) * r2, wd / 2 - 0.25 / gs, 1); );
);
// tgl below is a round POWER BUTTON: filled in the module color when on, outlined when off; the
// power glyph (open ring + bar) in white / dim. Same place and hit area as the switch before (x, y = its top left)
// SMOOTH ICON STROKES (SDB0 / SDSEG / SDARC / SDDRAW above; distance field): the strokes of an icon (lines and arcs) are collected, then every pixel of
// their box gets the color with an alpha from its exact distance to the nearest stroke - even width, soft edges, round
// ends, on the sub-pixel position, the same on every display scale (no stepped 1 px arcs, no jagged polygon edges)
// a chevron arrow head: tip (x, y), pointing along (dx, dy) (unit), legs len long
function CHEV(x, y, dx, dy, len, wd) ( SDSEG(x, y, x - dx * len + dy * len * 0.75, y - dy * len - dx * len * 0.75); SDSEG(x, y, x - dx * len - dy * len * 0.75, y - dy * len + dx * len * 0.75); );   // adds two strokes (draw with SDDRAW)
// CLOSE BUTTON of every panel / page: a round button with a fine cross (smooth strokes) - the same everywhere;
// returns the hover state (the click is checked by the caller)
function closex(xx, xy) local(h, k)
(
  h = sqr(mx - xx) + sqr(my - xy) <= 110;
  col(h ? cTEXT : cSURF, 1); SDDISC(xx, xy, 9); col(h ? cTEXT : cBORD, 1); ARCS(xx, xy, 8.6, 0, 2 * $pi, 0.9);
  col(h ? cSURF : cTEXT, 1); k = 3.4; SDB0(); SDSEG(xx - k, xy - k, xx + k, xy + k); SDSEG(xx - k, xy + k, xx + k, xy - k); SDDRAW(1.3);
  h;
);
// GROUND of the free field: Off / Hard / Grass / Snow (the reflection from the ground under source and listener)
function grndseg(x, y, w) local(g)
(
  g = floor(slider119 + 0.5); gfx_setfont(4); col(cMUTED, 1); txt("Ground", x, y + 8, 0);
  segtrack(x + 48, y, w - 48, 16);
  segp(x + 48, y, w - 48, 16, 4, 0, "Off", g == 0, cTEXT) ? setp(119, 0);
  segp(x + 48, y, w - 48, 16, 4, 1, "Hard", g == 1, cTEXT) ? setp(119, 1);
  segp(x + 48, y, w - 48, 16, 4, 2, "Grass", g == 2, cTEXT) ? setp(119, 2);
  segp(x + 48, y, w - 48, 16, 4, 3, "Snow", g == 3, cTEXT) ? setp(119, 3);
  tipbox(x, y, w, 16, "Ground reflection outdoors: the sound also arrives via the ground – Hard (asphalt, concrete) reflects almost everything, Grass and Snow absorb much. With a moving source it gives the typical sweeping color of a pass-by");
);
// Draws a round ON/OFF power button at x,y (filled in color c when on, thin ring when off) and returns 1 if clicked.
function tgl(x, y, on, c) local(hit, cx, cy)
(
  cCTLon ? ( c = on ? cACTO : cCTL; );   // ON = blue (white on white was hard to tell from off in dark mode)
  hit = inbox(x - 3, y - 3, 32, 20); cx = x + 15; cy = y + 7;   // smaller (radius 7, was 9), same right edge, same hit area
// lighter - off = a thin ring only, on = filled; a finer power sign (smooth arc, round ends)
// (disc, ring and sign on one sub-pixel grid; the sign centered in the circle - its box, not the arc center)
  on ? ( col(c, 1); SDDISC(cx, cy, 7); col(monoOn ? cBG : (cCTLon && darkmode ? 0x0E1F33 : 0xFFFFFF), 1); )
     : ( hit ? ( col(cHAIR, 1); SDDISC(cx, cy, 7); ); col(hit ? cDIM2 : cBORD, 1); ARCS(cx, cy, 6.5, 0, 2 * $pi, 0.9); col(hit ? cTEXT : cDIM2, 1); );
  SDB0(); SDARC(cx, cy + 0.45, 3.3, 0.72, 2 * $pi - 0.72); SDSEG(cx, cy - 4.1, cx, cy - 0.8); SDDRAW(1.05);
  click && hit;
);
// ---- VALUE ENTRY: right-click, Ctrl / Cmd + click (Mac: Ctrl+click is a right-click) on a knob, slider or mix -> type the value in the unit shown (Enter = set, Esc or a
// click elsewhere = cancel). kmap(type, a, b, s) before a control tells how a typed value becomes its position:
// type 0 = linear a … b, 1 = logarithmic a … b, 2 = output level, 3 = input level, -1 = no entry; the value typed is divided
// by s first (e.g. the decay: seconds / the room's own reverb time). Hz: "6k" or "6" on a kHz control = 6000 Hz.
kmapT = -1; kmapA = 0; kmapB = 1; kmapS = 1; vedit = 0; vedPend = 0;
abSt = 4194304 + 30000; abOn = 0; abCur = 0; abMatch = 1;   // A / B: the two settings (2 x USTR = 282 values, session only)
urb = 4194304 + 32000; URM = 24; URL = 66; URP = 40;   // MY ROOMS: 24 x [used, category, name (24 bytes), 40 room values (the 40th, door / window size: 0 in older saved rooms = 100 %)] (filled by @serialize)
rsnb = urb + 1600; rqb = urb + 1630; rtb = urb + 1660; urX = urb + 1730; urY = urb + 1760; rmH = urb + 1790;   // name / search buffers, menu layout (rmX / rmY: urb + 1900 / 2000, 100 each)
rsave = 0; rsnlen = 0; rscat = 6; rqlen = 0; memset(rqb, 0, 30); urctx = 0; rsren = 0; urflash = 0; urflu = 0;
cvY = alloc(4100); SPFN = 8192; SPB = allocA(2 * SPFN); spRI = alloc(SPFN); spRO = alloc(SPFN); spWin = alloc(SPFN); SPN = 217; spSm = alloc(SPN); spBI = alloc(SPN); spBO = alloc(SPN);   // FFT 8192 // 1/24 octave (217 bands, was 1/6 = 55) // SPECTRUM: fft, rings in / out, window, bands
memset(spRI, 0, 8192); memset(spRO, 0, 8192); spw = 0; spWinOk = 0; i = 0; loop(SPN, spBI[i] = spBO[i] = -120; i += 1; ); spT = 0;          // spectrum: in / out rings and band levels cleared (-120 dB)
// Sets how a typed value maps to the next control's position: kmapT type (0 lin, 1 log, 2 out level, 3 in level, -1 none), range a..b, divisor sc.
function kmap(t, a, b, sc) ( kmapT = t; kmapA = a; kmapB = b; kmapS = sc; );
// Parses typed text st to a number ('-' sign, '.' or ',' decimal, 'k' = x1000); sets vedok (digits seen) and vedk (k used).
function vedparse(st) local(i, n, c, v, sg, fr, vdp, k1, vany)
(
  n = strlen(st); i = 0; v = 0; sg = 1; fr = 1; vdp = 0; k1 = 0; vany = 0;
  loop(n,
    c = str_getchar(st, i);
    c == '-' ? ( sg = -1; ) : ( (c == '.' || c == ',') ? ( vdp = 1; ) : ( (c == 'k' || c == 'K') ? ( k1 = 1; ) : (
      c >= '0' && c <= '9' ? ( vany = 1; vdp ? ( fr *= 0.1; v += (c - '0') * fr; ) : ( v = v * 10 + (c - '0'); ); );
    ); ); );
    i += 1;
  );
  vedok = vany; vedk = k1;
  sg * v * (k1 ? 1000 : 1);
);
// the typed value -> knob position (0 … 1) with the mapping of the control that is drawn now
function vedmap(v) local(f)
(
  v /= max(0.000001, kmapS);
  kmapT == 1 && !vedk && kmapA >= 20 && v < kmapA && v * 1000 >= kmapA && v * 1000 <= kmapB ? ( v *= 1000; );   // "6" on a kHz control
  kmapT == 0 ? ( f = (v - kmapA) / (kmapB - kmapA); );
  kmapT == 1 ? ( f = log(max(kmapA, v) / kmapA) / log(kmapB / kmapA); );
  kmapT == 2 ? ( v = min(24, max(-72, v)); f = v <= -24 ? 0.25 * (v + 72) / 48 : 0.25 + 0.75 * (v + 24) / 48; );   // (= lvl2f)
  kmapT == 3 ? ( v = min(48, max(-72, v)); f = v <= -24 ? 0.25 * (v + 72) / 48 : 0.25 + 0.75 * (v + 24) / 72; );   // (= ilvl2f)
  min(1, max(0, f));
);
// Starts typed value entry for control id (vedit), pre-filling the entry text #ved with the numeric characters of the shown string vs.
function vedopen(id, vs) local(n, i, c)
(
  vedit = id; vedNew = 1; strcpy(#ved, "");
  n = strlen(vs); i = 0;                                                      // start from the number shown (the unit dropped)
  loop(n, c = str_getchar(vs, i); (c >= '0' && c <= '9') || c == '.' || c == '-' || c == ',' || c == 'k' ? ( sprintf(#vch, "%c", c); strcat(#ved, #vch); ); i += 1; );
  vedT0 = time_precise(); kdrag = 0; vedJust = 1;               // (placed by its control from the next frame on)
);
// the entry box (drawn by the control itself, in its own scale); its place on screen is kept for the click test
function vedplace(cx, yc, w) ( vedCX = cx; vedCY = yc; vedW = w; vedGS = gs; vedGX = gox; vedGY = goy; vedKC = kcol; vedSeen = 1; );
// Draws the value-entry box with the typed text #ved and a blinking caret at cx,yc (min width 54), stores its screen rect and hides tooltips.
function veddraw(cx, yc, w) local(bw)
(
  bw = max(w, 54);
  col(cINK, 0.14); RR(cx - bw / 2 + 1, yc - 8, bw, 17, 4);
  col(cSURF, 1); RR(cx - bw / 2, yc - 9, bw, 17, 4); col(kcol > 0 ? kcol : cTEXT, 1); RRO(cx - bw / 2, yc - 9, bw, 17, 4);
  vedNew && strlen(#ved) ? ( col(kcol > 0 ? kcol : cTEXT, 0.18); gfx_setfont(2); RR(cx - tws(#ved) / 2 - 2, yc - 6, tws(#ved) + 4, 12, 2); );
  col(cTEXT, 1); gfx_setfont(2); txt(#ved, cx, yc, 1);
  (floor((time_precise() - vedT0) * 2) & 1) == 0 && !vedNew ? ( gfx_setfont(2); LNW(cx + tws(#ved) / 2 + 1.5, yc - 5, cx + tws(#ved) / 2 + 1.5, yc + 5, 1); );
  vedPX0 = gox + (cx - bw / 2) * gs; vedPX1 = gox + (cx + bw / 2) * gs; vedPY0 = goy + (yc - 9) * gs; vedPY1 = goy + (yc + 8) * gs;
  tipon = 0;
);
// copies the 15 reflector values (vopt[52 … 66]: two reflectors x 6, two levels, the on/off flag) into b[0 … 14] / back from it
function rfget(b) local(i) ( i = 0; loop(15, b[i] = vopt[RFB + i]; i += 1; ); );
function rfput(b) local(i) ( i = 0; loop(15, vopt[RFB + i] = b[i]; i += 1; ); );
// Stores all NSL slider values and the reflectors into the A/B comparison slot k (abSt; [0] = 1 marks the slot as filled).
function abstore(k) local(i) ( i = 1; loop(NSL, abSt[k * USTR + i] = slider(i); i += 1; ); rfget(abSt + k * USTR + 126); abSt[k * USTR] = 1; );
// Recalls A/B slot k into the sliders (all except slider 64) and the reflectors as one undoable change, then sends automation notifications.
function abload(k) local(i) ( snap_take(); i = 1; loop(NSL, i != 64 ? sset(i, abSt[k * USTR + i]); i += 1; ); abSt[k * USTR] >= 0.5 ? rfput(abSt + k * USTR + 126); snap_notify(); );
// ---- UNDO / REDO (GUI): every change made in the plugin window (a drag, the wheel, a click, a key, a snapshot or preset) is one
// step; 20 steps back and forward. Changes from REAPER (automation, its own undo) are not recorded - they only move the base.
// memory: undo 20 x 141, redo 20 x 141, base (state after the last step) 141 (USTR per state: slots 1 … 119 = sliders, 120 … 125 = the six motion centers, 126 … 140 = the 15 reflector values) - far above the audio buffers
UNMAX = 20; USTR = 141; UNB = 4194304 + 53000; RDB = UNB + UNMAX * USTR; UBS = RDB + UNMAX * USTR;   // 0.301: 121 per step - 114 sliders + the 6 motion centers (undo of a path drag); RDB / UBS were swallowed by this comment before 0.301 (redo and the undo base wrote over the snapshot slots at address 0 …)   // 0.271: 112 per step (sliders 1 … 109)
// Copies all NSL slider values, the six motion center coordinates (mtC0/mtC1 x,y,z) and the 15 reflector values (b[126 … 140]) into the state array b.
function ucopy(b) local(i) ( rfget(b + 126); i = 1; loop(NSL, b[i] = slider(i); i += 1; ); b[120] = mtC0x; b[121] = mtC0y; b[122] = mtC0z; b[123] = mtC1x; b[124] = mtC1y; b[125] = mtC1z; );
function udiff(b) local(i, d, mr) ( d = 0; mr = mtE > 0.001 || slider100 >= 0.5; i = 1; loop(NSL, !(mr && i >= 10 && i <= 15) && abs(slider(i) - b[i]) > 0.000000001 ? d = 1; i += 1; ); abs(mtC0x - b[120]) + abs(mtC0y - b[121]) + abs(mtC1x - b[123]) + abs(mtC1y - b[124]) > 0.000000001 ? d = 1; i = 0; loop(15, abs(vopt[52 + i] - b[126 + i]) > 0.000000001 ? d = 1; i += 1; ); d; );   // (a running motion writes 10-15 itself: not a change; the centers are)
// Appends the current base state UBS to undo/redo stack st holding n steps (drops the oldest at UNMAX); returns the new count.
function upush(st, n) ( n >= UNMAX ? ( memcpy(st, st + USTR, (UNMAX - 1) * USTR); n = UNMAX - 1; ); memcpy(st + n * USTR, UBS, USTR); n + 1; );
// Restores state b into the sliders (and motion centers when motion runs), notifies automation and makes it the new base state UBS.
function uapply(b) local(i) ( snap_take(); i = 1; loop(NSL, sset(i, b[i]); i += 1; ); rfput(b + 126); snap_notify();
  (mtE > 0.001 || slider100 >= 0.5) ? ( mtC0x = b[120]; mtC0y = b[121]; mtC0z = b[122]; mtC1x = b[123]; mtC1y = b[124]; mtC1z = b[125]; );   // a running motion gets its centers back (the path, not the ball)
  ucopy(UBS); );
// a finished gesture with a change -> one undo step (the redo list is cleared); a change without a gesture -> new base only
function ucommit() ( udiff(UBS) ? ( ugest ? ( unN = upush(UNB, unN); rdN = 0; ); ucopy(UBS); ); ugest = 0; );
// Undo: commits any pending change, pushes the current state to the redo stack and applies the previous undo step.
function udo() ( ucommit(); unN > 0 ? ( rdN = upush(RDB, rdN); unN -= 1; uapply(UNB + unN * USTR); ); ugest = 0; );
// Redo: commits any pending change, pushes the current state to the undo stack and applies the next redo step.
function uredo() ( ucommit(); rdN > 0 ? ( unN = upush(UNB, unN); rdN -= 1; uapply(RDB + rdN * USTR); ); ugest = 0; );
// ---- KEYS: every shortcut on one page (key ?, the ? button in the strip); a click or Esc closes it ----
function krow(x, y, k, d, ovkw, ovdw) local(w)
(
  gfx_setfont(14); w = tws(k) + 12;
  col(cHAIR, 1); RR(x, y - 9, w, 18, 5); col(cTEXT, 1); txt(k, x + 6, y, 0);
  col(cMUTED, 1); gfx_setfont(13); txtfit(d, x + max(ovkw, w + 10), y, ovdw - max(ovkw, w + 10));
);
// ---- a PAGE over everything (help, keys) - the title row stays, the body is drawn into image 2 and shown through
// a window of the page's height; the wheel or the bar at its right edge scrolls it, the round × on its corner, Esc or a
// click beside it close it. pgopen(id, width, height, body height, title) ... body ... pgclose() ----
function pgopen(id, cw, ch, bodyh, tt) local(ih, iw)
(
  pgWho != id ? ( pgSc = 0; pgWho = id; );
  pgCW = cw; pgCH = ch; pgCX = (WT - cw) / 2; pgCY = max(12, (HT - ch) / 2) + scy;
  pgBY = pgCY + 44; pgBH = ch - 54; pgMax = max(0, bodyh - pgBH); pgSc = min(max(pgSc, 0), pgMax);
  col(cINK, 0.16); RR(pgCX + 2, pgCY + 4, cw, ch, 11);
  col(cBORD, 1); RR(pgCX - 1, pgCY - 1, cw + 2, ch + 2, 11); col(cSURF, 1); RR(pgCX, pgCY, cw, ch, 10);
  col(cTEXT, 1); gfx_setfont(5); txt(tt, pgCX + 20, pgCY + 22, 0);
  col(cMUTED, 1); gfx_setfont(13); txt(pgMax > 0 ? "wheel = scroll  ·  × or Esc = close" : "× or Esc = close", pgCX + cw - 40, pgCY + 22, 2);
  col(cHAIR, 1); LN(pgCX + 12, pgBY - 1, pgCX + cw - 12, pgBY - 1);
  // the body into image 2: page coordinates, its top left = (pgCX, pgBY); the mouse moves with the scroll
  pgGX = gox; pgGY = goy; iw = ceil(cw * gs) + 1; ih = ceil(max(bodyh, pgBH) * gs) + 1;
  gfx_setimgdim(2, iw, ih); gfx_dest = 2;
  col(cSURF, 1); gfx_a = 1; gfx_mode = 0; gfx_rect(0, 0, iw, ih);
  gox = -pgCX * gs; goy = -pgBY * gs;
  pgMY = my; my += pgSc;
);
// Ends a page body: restores drawing offsets, blits the scrolled body image to the screen, draws scroll bar and close button, stores click rects.
function pgclose() local(bx, th, by, xx, xy, k)
(
  gox = pgGX; goy = pgGY; my = pgMY; gfx_dest = 1; gfx_a = 1; gfx_mode = 0;
  gfx_blit(2, 1, 0, 0, pgSc * gs, pgCW * gs, pgBH * gs, gox + pgCX * gs, goy + pgBY * gs, pgCW * gs, pgBH * gs);
  pgMax > 0 ? (                                                       // the scroll bar at the right edge of the body
    bx = pgCX + pgCW - 9; th = max(16, pgBH * pgBH / (pgBH + pgMax)); by = pgBY + (pgBH - th) * pgSc / pgMax;
    col(cTEXT, pgDrag || inbox(bx - 4, pgBY, 12, pgBH) ? 0.45 : 0.22); RR(bx, by, 4, th, 2);
    pgClk && inbox(bx - 4, pgBY, 12, pgBH) ? ( pgDrag = 1; pgDmy = my; pgDs0 = pgSc; );
    pgDrag && (mouse_cap & 1) ? ( pgSc = min(max(pgDs0 + (my - pgDmy) * (pgBH + pgMax) / pgBH, 0), pgMax); ) : ( pgDrag = 0; );
  ) : ( pgDrag = 0; );
  // the round × on the top right corner (as on the zoom windows)
  xx = pgCX + pgCW - 16; xy = pgCY + 16; pgXh = sqr(mx - xx) + sqr(my - xy) <= 110;
  closex(xx, xy);
  pgXh ? ( tipon = 1; tipforce = 0; tipstr = "Close (Esc)"; tipx = xx - 30; tipy = xy + 22; );
  // the page's screen place for the click test at the start of the next frame
  pwX0 = gox + pgCX * gs; pwX1 = pwX0 + pgCW * gs; pwY0 = goy + pgCY * gs; pwY1 = pwY0 + pgCH * gs;
  pxX = gox + xx * gs; pxY = goy + xy * gs; pxR = 10 * gs; pbY0 = goy + pgBY * gs; pbY1 = pbY0 + pgBH * gs;
  pgClk = 0;
);
// Draws the 'Keys and mouse' help page (keyboard and mouse shortcut lists, one or two columns) via pgopen/pgclose.
function kovldraw() local(cw, ovch, cx, cy, c1, c2, y, ovkw, ovdw)
(
// page and body heights are fixed values that must fit the rows below (25 px per row): two columns 577 / 523, one column 1006 / 952 (+ 4 mouse rows)
  cw = min(WT - 24, 720); c2 = cw >= 560; ovch = min(c2 ? 577 : 1006, HT - 24);
  pgopen(2, cw, ovch, c2 ? 523 : 952, "Keys and mouse");
  cx = pgCX; cy = pgBY - 44;
  ovdw = c2 ? cw / 2 - 30 : cw - 40; ovkw = c2 ? 108 : 120;
  c1 = cx + 20; y = cy + 58;
  col(cTEXT, 1); gfx_setfont(2); txt("Keyboard", c1, y, 0); y += 24;
  krow(c1, y, "1 … 9", "Tabs: open tab 1 … 9 · else snapshot 1 … 8", ovkw, ovdw); y += 25;
  krow(c1, y, "Shift + 1 … 8", "Tabs: snapshot 1 … 8 · else 9 … 16", ovkw, ovdw); y += 25;
  krow(c1, y, "Arrows", "Move the listener 10 cm (Shift: 2 cm)", ovkw, ovdw); y += 25;
  krow(c1, y, "Alt + arrows", "Move the source", ovkw, ovdw); y += 25;
  krow(c1, y, "Ctrl/Cmd + Z", "Undo (20 steps)", ovkw, ovdw); y += 25;
  krow(c1, y, "+ Shift / Ctrl+Y", "Redo", ovkw, ovdw); y += 25;
  krow(c1, y, "V", "Hide / show the Stage View (Expert)", ovkw, ovdw); y += 25;
  krow(c1, y, "H", "Stereo handles of the source on / off", ovkw, ovdw); y += 25;
  krow(c1, y, "B  ·  E  ·  L", "View: Basic  ·  Expert  ·  Learn", ovkw, ovdw); y += 25;
  krow(c1, y, "[  /  ]", "Tabs: previous / next module", ovkw, ovdw); y += 25;
  krow(c1, y, "0", "Expert: Tabs <-> All (All with Fit all)", ovkw, ovdw); y += 25;
  krow(c1, y, "T  ·  A  ·  F", "Expert: Tabs  ·  All  ·  Fit on / off", ovkw, ovdw); y += 25;
  krow(c1, y, "Z", "Zoom what is under the mouse (Stage View: every mode)", ovkw, ovdw); y += 25;
  krow(c1, y, "Esc", "Close zoom, menus, solo, help", ovkw, ovdw); y += 25;
  krow(c1, y, "?", "This page", ovkw, ovdw); y += 25;
  krow(c1, y, "Hold Ctrl/Cmd", "Help text under the mouse (also Tips off)", ovkw, ovdw); y += 25;
  c2 ? ( c1 = cx + cw / 2 + 10; y = cy + 58; ) : ( y += 14; );
  col(cTEXT, 1); gfx_setfont(2); txt("Mouse", c1, y, 0); y += 24;
  krow(c1, y, "Drag", "Knob up / down (Shift: fine)", ovkw, ovdw); y += 25;
  krow(c1, y, "Wheel", "Knob in steps; page scroll elsewhere", ovkw, ovdw); y += 25;
  krow(c1, y, "Double-click", "Knob: type a value", ovkw, ovdw); y += 25;
  krow(c1, y, "Right-click", "Type a value (Mac: also Ctrl+click)", ovkw, ovdw); y += 25;
  krow(c1, y, "Alt + click", "Knob back to its default", ovkw, ovdw); y += 25;
  krow(c1, y, "Plan: drag", "Move source / listener (Shift: height)", ovkw, ovdw); y += 25;
  krow(c1, y, "Alt + drag ball", "Turn the source / the listener (Expert)", ovkw, ovdw); y += 25;
  krow(c1, y, "2× on a ball", "To the room center · listener arrow: ahead", ovkw, ovdw); y += 25;
  krow(c1, y, "Alt + 2× source", "The listener looks straight ahead again", ovkw, ovdw); y += 25;
  krow(c1, y, "Reflector", "Drag = move · Alt+drag = turn · squares = size", ovkw, ovdw); y += 25;
  krow(c1, y, "Ctrl/Cmd + drag", "Source / listener on a line only", ovkw, ovdw); y += 25;
  krow(c1, y, "Plan: wall", "Drag = stretch (Alt: height, Ctrl/Cmd: size)", ovkw, ovdw); y += 25;
  krow(c1, y, "Room corner", "Drag = size (Ctrl/Cmd: all, Alt: height)", ovkw, ovdw); y += 25;
  krow(c1, y, "Wheel on floor", "Zoom the Stage View in / out 0.1 … 5x (Fit, 1:1)", ovkw, ovdw); y += 25;
  krow(c1, y, "Module corner", "Drag = size, double-click = standard", ovkw, ovdw); y += 25;
  krow(c1, y, "Bar over tabs", "Drag = Stage View height, double-click = auto", ovkw, ovdw); y += 25;
  krow(c1, y, "Shift + click", "Module in the strip: solo it", ovkw, ovdw); y += 25;
  krow(c1, y, "Alt + click", "Section heading: fold / open all", ovkw, ovdw); y += 25;
  pgclose();
);
// Draws a rotary knob (0 = up, 270 deg) with value text vs and name nm, handles hover, wheel and value entry; returns new position 0..1, sets kchg.
function knob(id, cx, cy, nm, vs, f, fdef) local(a, nf, t, hit)
(
  cCTLon ? ( kcol = cCTL; );
  // one size for every rotary control of a view: ring radius KR (Expert 10, Basic 14), name + value below
  col(sqr(mx - cx) + sqr(my - cy) < sqr(KR + 5) && !kdim ? cBORD : cHAIR, 1); ARC(cx, cy, KR - KLW * 0.22, -0.75 * $pi, 0.75 * $pi, KLW * 0.56);   // hover = a darker track // a thin track, the value arc wider with round ends
  f > 0.004 ? ( col(kdim ? cDIM : kcol, 1); ARCR(cx, cy, KR, -0.75 * $pi, -0.75 * $pi + 1.5 * $pi * f, KLW); );
  a = -0.75 * $pi + 1.5 * $pi * fdef; col(cDIM2, 1); LNW(cx + sin(a) * (KR + KLW / 2 + 0.8), cy - cos(a) * (KR + KLW / 2 + 0.8), cx + sin(a) * (KR + KLW / 2 + 3), cy - cos(a) * (KR + KLW / 2 + 3), 1.1);   // default mark
  a = -0.75 * $pi + 1.5 * $pi * f;
  col(kdim ? cDIM : kcol, 1); CIF(cx + sin(a) * (KR - KLW / 2), cy - cos(a) * (KR - KLW / 2), KLW * 0.95, 1);
  col(cSURF, 1); CIF(cx + sin(a) * (KR - KLW / 2), cy - cos(a) * (KR - KLW / 2), KLW * 0.38, 1);   // the pointer: a ring with a light center
  col(kdim ? cDIM2 : cTEXT, 1); txtboxR(vs, cx, cy, 2 * KR - 2 * KLW - 3, 16 * KSC, KVF);           // the value inside the ring
  col(kdim ? cDIM2 : cMUTED, 1); gfx_setfont(KNF); txt(nm, cx, cy + KR + 8 * KNS, 1);    // the name below
  nf = f; kchg = 0;                       // kchg: the user changed it in this frame (drag, wheel, double-click) - nothing else
  hit = sqr(mx - cx) + sqr(my - cy) < sqr(KR + 5);
  ((click && (mouse_cap & 36)) || rclick) && hit && kmapT >= 0 && !kdim ? ( vedopen(id, vs); click = 0; rclick = 0; );
  vedPend && vedId == id && kmapT >= 0 ? ( nf = vedmap(vedVal); kchg = 1; vedPend = 0; );
  vedit == id ? vedplace(cx, cy + KR + 8 * KNS, 2 * KR + 12);
  click && hit ? (
    t = time_precise();
    // order of the checks: Alt+click first, then the 0.5 s double-click window, else a plain click arms a drag
    // Alt+click -> default; double-click -> type a value (no entry: default); a click starts a drag
    (mouse_cap & 16) ? ( nf = fdef; kdrag = 0; lastkid = -1; kchg = 1; ) :
    (lastkid == id && t - lastkt < 0.5) ? ( kmapT >= 0 && !kdim ? ( vedopen(id, vs); lastkid = -1; ) : ( nf = fdef; kdrag = 0; lastkid = -1; kchg = 1; ); )
                                        : ( kdrag = id; kmy = my; kf0 = f; lastkid = id; kmv = 0; );
    lastkt = t;
  );
  // the drag starts after 3 px (a double-click does not move the value)
  kdrag == id && !kmv && abs(kmy - my) > 2 ? ( kmv = 1; kmy = my; );
  kdrag == id && kmv ? ( nf = min(1, max(0, kf0 + (kmy - my) / ((mouse_cap & 8) ? 600 : 150))); !(mouse_cap & 8) && abs(nf - fdef) < 0.012 ? nf = fdef; kchg = 1; );   // detent at the default
  mouse_wheel != 0 && hit && !(mouse_cap & 1) ? ( nf = min(1, max(0, f + mouse_wheel / 120 * 0.02)); wheelused = 1; kchg = 1; );
  kmapT = -1; kmapS = 1;
  nf;
);
// DIRECTION DIAL (Src angle): a full circle - the dot points where the source points, seen from above as in the plan;
// the small mark = 0° (front wall with Aim Fixed, the listener with Aim Listener), the arc = the turn from there.
// Drag = the dot follows the mouse around the dial (snaps to 15° steps within 2°), Shift+drag = fine (0.25° per pixel),
// wheel = 1° (Shift 0.1°), double-click / right-click = type a value, Alt+click = 0°
function dirknob(id, cx, cy, nm, vs, deg, base) local(a, a0, nf, t, hit, dx, dy, q, sh)
(
  cCTLon ? ( kcol = cCTL; );
  a0 = (base + 180) * $pi / 180; a = a0 + deg * $pi / 180;
  col(cHAIR, 1); ARC(cx, cy, KR, 0, 2 * $pi, KLW);
  abs(deg) > 0.5 ? ( col(kdim ? cDIM : kcol, 0.5); ARC(cx, cy, KR, min(a0, a), max(a0, a), KLW); );
  col(cMUTED, 1); CIF(cx + sin(a0) * (KR - KLW - 2.5), cy - cos(a0) * (KR - KLW - 2.5), 1.7, 1);   // 0° mark inside the ring
  col(kdim ? cDIM : kcol, 1); CIF(cx + sin(a) * (KR - KLW / 2 + 0.5), cy - cos(a) * (KR - KLW / 2 + 0.5), KLW * 0.95, 1);
  col(kdim ? cDIM2 : cTEXT, 1); txtbox(vs, cx, cy, 2 * KR - 2 * KLW - 3, 16 * KSC, KVF);
  col(kdim ? cDIM2 : cMUTED, 1); gfx_setfont(KNF); txt(nm, cx, cy + KR + 8 * KNS, 1);
  nf = deg; kchg = 0; sh = (mouse_cap & 8) > 0;
  hit = sqr(mx - cx) + sqr(my - cy) < sqr(KR + 5);
  ((click && (mouse_cap & 36)) || rclick) && hit ? ( vedopen(id, vs); click = 0; rclick = 0; );
  vedPend && vedId == id ? ( nf = min(180, max(-180, vedVal)); kchg = 1; vedPend = 0; );
  vedit == id ? vedplace(cx, cy + KR + 8 * KNS, 2 * KR + 12);
  click && hit ? (
    t = time_precise();
    (mouse_cap & 16) ? ( nf = 0; kdrag = 0; lastkid = -1; kchg = 1; ) :
    (lastkid == id && t - lastkt < 0.5) ? ( vedopen(id, vs); lastkid = -1; kdrag = 0; )
                                        : ( kdrag = id; dkmy = my; dkmx = mx; kf0 = deg; lastkid = id; kmv = 0; dkSh = sh; );
    lastkt = t;
  );
  kdrag == id && !kmv && abs(dkmy - my) + abs(dkmx - mx) > 2 ? ( kmv = 1; );
  kdrag == id && kmv ? (
    sh != dkSh ? ( kf0 = deg; dkmy = my; dkSh = sh; );            // Shift pressed / released: continue from here
    sh ? ( nf = kf0 + (dkmy - my) * 0.25; ) : (
      dx = mx - cx; dy = my - cy;
      dx * dx + dy * dy > 16 ? (
        nf = atan2(-dx, dy) * 180 / $pi - base;
        q = 15 * floor(nf / 15 + 0.5); abs(nf - q) < 2 ? ( nf = q; );
      );
    );
    kchg = 1;
  );
  mouse_wheel != 0 && hit && !(mouse_cap & 1) ? ( nf = deg + mouse_wheel / 120 * (sh ? 0.1 : 1); wheelused = 1; kchg = 1; );
  nf = nf - 360 * floor((nf + 180) / 360);                        // -180 … 180
  kmapT = -1; kmapS = 1;
  nf;
);
// knob size of a section: 1 = the standard size of the view; a taller (and wide enough) section draws its knobs larger -
// ring, value and name (the name grows a little less). kscale(1) after the section.
function kscale(k) (
  KSC = max(1, min(2, k));
  KSC > 1.001 ? (
    KR = KR0 * KSC; KLW = KLW0 * KSC; KNS = 1 + (KSC - 1) * 0.6;
    FS15 = fsz(KVF0) * KSC; FS16 = FS4 * KNS; gfx_setfont(15, "Arial", FS15, 'b'); gfx_setfont(16, "Arial", FS16); KVF = 15; KNF = 16;
  ) : ( KSC = 1; KR = KR0; KLW = KLW0; KNS = 1; KVF = KVF0; KNF = 4; );
);
// vertical room (each side of a knob row) -> knob scale
function kfit(sp) ( 1 + max(0, sp - 2) / (KR0 + 5); );
// Basic: one knob size for the whole row of cards - the card with the least room decides
function kbrow() ( min(min((bwR - 20) / 3 / (2 * KR0 + 12), (bwB - 125) / (2 * KR0)), min(2 * floor((bwO - 20) / 4) / (2 * KR0 + 12), kfit((vopt[32] + bfill) * 0.25))); );
// MIX of a module (heading, Expert): small ring + "Amount 100%" (Tabs) or just "100%" (All) - drag up / down (Shift = fine), wheel, double-click = 100 %
function hmix(id, x, yc, si, c, tip) local(v, f, nf, hit, t)
(
  v = slider(si); f = min(1, max(0, v / 100)); hit = inbox(x - 3, yc - 8, 76, 16);
  col(cHAIR, 1); ARC(x + 6, yc, 5.5, -0.75 * $pi, 0.75 * $pi, 2);
  f > 0.004 ? ( col(c, 1); ARC(x + 6, yc, 5.5, -0.75 * $pi, -0.75 * $pi + 1.5 * $pi * f, 2); );
  col(hit || kdrag == id ? cTEXT : (f < 0.999 ? c : cMUTED), 1); gfx_setfont(6);
  sprintf(#hmx, tabm ? "Amount %.0f%%" : "%.0f%%", v); txt(#hmx, x + (tabm ? 15 : 40), yc, 0);   // All: the ring and the number only (more room in the heading)
  nf = f; kchg = 0;
  ((click && (mouse_cap & 36)) || rclick) && hit ? ( kmap(0, 0, 100, 1); vedopen(id, #hmx); click = 0; rclick = 0; );
  vedPend && vedId == id ? ( kmap(0, 0, 100, 1); nf = vedmap(vedVal); kchg = 1; vedPend = 0; kmapT = -1; );
  vedit == id ? vedplace(x + 30, yc, 56);
  click && hit ? (
    t = time_precise();
    (mouse_cap & 16) ? ( nf = 1; kdrag = 0; lastkid = -1; kchg = 1; ) :
    (lastkid == id && t - lastkt < 0.5) ? ( kmap(0, 0, 100, 1); vedopen(id, #hmx); kmapT = -1; lastkid = -1; ) : ( kdrag = id; kmy = my; kf0 = f; lastkid = id; kmv = 0; );
    lastkt = t; click = 0;
  );
  kdrag == id && !kmv && abs(kmy - my) > 2 ? ( kmv = 1; kmy = my; );
  kdrag == id && kmv ? ( nf = min(1, max(0, kf0 + (kmy - my) / ((mouse_cap & 8) ? 600 : 150))); kchg = 1; );
  mouse_wheel != 0 && hit && !(mouse_cap & 1) ? ( nf = min(1, max(0, f + mouse_wheel / 120 * 0.02)); wheelused = 1; kchg = 1; );
  kchg ? setp(si, nf * 100);
  tipbox(x - 3, yc - 8, 60, 16, tip);
);
// horizontal slider (Drive): drag left / right (Shift = fine), wheel, double-click = default
function hsl(id, x, y, w, nm, vs, f, fdef, c) local(nf, t, hit)
(
  cCTLon ? ( c = cCTL; );
  col(cMUTED, 1); gfx_setfont(4); txt(nm, x, y, 0);
  col(cTEXT, 1); gfx_setfont(2); txt(vs, x + w, y, 2);
  col(cHAIR, 1); RR(x, y + 7, w, 3, 1.5);
  f > 0.004 ? ( col(c, 1); RR(x, y + 7, max(3, w * f), 3, 1.5); );
  col(cDIM2, 1); R(x + w * fdef - 0.5, y + 4, 1, 9);                 // default mark (0 dB …)
  col(c, 1); CIF(x + w * f, y + 8.5, 3.8, 1);
  nf = f; kchg = 0;
  hit = inbox(x - 5, y - 6, w + 10, 20);
  ((click && (mouse_cap & 36)) || rclick) && hit && kmapT >= 0 ? ( vedopen(id, vs); click = 0; rclick = 0; );
  vedPend && vedId == id && kmapT >= 0 ? ( nf = vedmap(vedVal); kchg = 1; vedPend = 0; );
  vedit == id ? vedplace(x + w - 24, y, 48);
  click && hit ? (
    t = time_precise();
    (mouse_cap & 16) ? ( nf = fdef; kdrag = 0; lastkid = -1; kchg = 1; ) :
    (lastkid == id && t - lastkt < 0.5) ? ( kmapT >= 0 ? ( vedopen(id, vs); lastkid = -1; ) : ( nf = fdef; kdrag = 0; lastkid = -1; kchg = 1; ); )
                                        : ( kdrag = id; ksx = mx; kf0 = f; lastkid = id; kmv = 0; );
    lastkt = t;
  );
  kdrag == id && !kmv && abs(ksx - mx) > 2 ? ( kmv = 1; ksx = mx; );
  kdrag == id && kmv ? ( nf = min(1, max(0, kf0 + (mx - ksx) / ((mouse_cap & 8) ? 4 * w : w))); !(mouse_cap & 8) && abs(nf - fdef) < 0.012 ? nf = fdef; kchg = 1; );   // detent at the default
  mouse_wheel != 0 && hit && !(mouse_cap & 1) ? ( nf = min(1, max(0, f + mouse_wheel / 120 * 0.02)); wheelused = 1; kchg = 1; );
  kmapT = -1; kmapS = 1;
  nf;
);
// output level: knob 0 .. 0.25 covers -inf(-72) .. -24 dB, 0.25 .. 1 covers -24 .. +24 dB
function lvl2f(v) ( v <= -24 ? 0.25 * (v + 72) / 48 : 0.25 + 0.75 * (v + 24) / 48; );
// Converts an output-level knob position f (0..1) to dB: 0..0.25 maps -72..-24 dB, 0.25..1 maps -24..+24 dB.
function f2lvl(f) ( f <= 0.25 ? -72 + f / 0.25 * 48 : -24 + (f - 0.25) / 0.75 * 48; );
// input level: knob 0 .. 0.25 covers -inf(-72) .. -24 dB, 0.25 .. 1 covers -24 .. +48 dB
function ilvl2f(v) ( v <= -24 ? 0.25 * (v + 72) / 48 : 0.25 + 0.75 * (v + 24) / 72; );
// Converts an input-level knob position f (0..1) to dB: 0..0.25 maps -72..-24 dB, 0.25..1 maps -24..+48 dB.
function f2ilvl(f) ( f <= 0.25 ? -72 + f / 0.25 * 48 : -24 + (f - 0.25) / 0.75 * 72; );
// dropdown box: text (shortened to fit) + arrow; en = enabled, opn = its menu is open
function ddbox(x, y, w, h, s, en, opn)
(
  col(cSURF, 1); RR(x, y, w, h, min(8, h / 2)); col(opn ? cMUTED : (inbox(x, y, w, h) && en ? cDIM2 : cBORD), 1); RRO(x, y, w, h, min(8, h / 2));   // the radius of the cards
  col(en ? cTEXT : cDIM2, 1); gfx_setfont(6); txtfit(s, x + 7, y + h / 2, w - 22);
  chevd(x + w - 10, y + h / 2, en ? cMUTED : cDIM2);
);
// wall materials (WALLS dropdown): name, absorption, tail density, help text
// wall materials, hard -> soft: absorption α at mid frequencies (500 Hz - 1 kHz) and the density of the tail
function wmatname(k) ( k == 0 ? "Tiles" : (k == 1 ? "Glass" : (k == 2 ? "Plaster" : (k == 3 ? "Concrete" : (k == 4 ? "Metal" : (k == 5 ? "Wood" : (k == 6 ? "Curtain" : (k == 7 ? "Foam" : "Wool"))))))); );
// Returns the mid-frequency absorption coefficient alpha of wall material k (0 Tiles 0.02 ... 8 Wool 0.85).
function wmata(k) ( k == 0 ? 0.02 : (k == 1 ? 0.03 : (k == 2 ? 0.04 : (k == 3 ? 0.05 : (k == 4 ? 0.08 : (k == 5 ? 0.10 : (k == 6 ? 0.45 : (k == 7 ? 0.70 : 0.85))))))); );
// Returns the tail density value (20-90) of wall material k (0 Tiles ... 8 Wool).
function wmatd(k) ( k == 0 ? 45 : (k == 1 ? 28 : (k == 2 ? 65 : (k == 3 ? 80 : (k == 4 ? 20 : (k == 5 ? 70 : (k == 6 ? 80 : (k == 7 ? 85 : 90))))))); );
// Returns the help/tooltip text for wall material k (name, alpha and typical use).
function wmattip(k) ( k == 0 ? "Glazed tiles α 0.02: bathroom, kitchen, swimming pool – the hardest, brightest walls" : (k == 1 ? "Glass α 0.03: window panes – thin bass (the pane gives way), very bright, long-ringing highs" : (k == 2 ? "Plaster α 0.04: painted plaster or plasterboard – the usual wall at home and in offices" : (k == 3 ? "Concrete α 0.05: bare concrete or stone – booming, long bass, dark highs that die quickly" : (k == 4 ? "Sheet metal α 0.08: very bright, metallic and ringing – sparse, flutter-like reflections" : (k == 5 ? "Wood α 0.10: paneling – hard surface, lively sound" : (k == 6 ? "Curtain α 0.45: heavy fabric, absorbs about half of the sound" : (k == 7 ? "Acoustic foam α 0.70: absorbs most mid and high frequencies" : "Mineral wool α 0.85: strong absorber, makes the room very dry"))))))); );
// what thickness of a typical absorber gives this α (broadband average, mineral wool / foam on a hard wall; the bass needs more)
function wthick(a) ( a < 0.035 ? "no absorber at all: glass, glazed tile or polished stone" : (a < 0.07 ? "no absorber: plain plaster, plasterboard or bare concrete" : (a < 0.15 ? "wood paneling, or under 1 cm of carpet" : (a < 0.25 ? "about 1 cm of felt or a thin carpet" : (a < 0.38 ? "about 2 cm of mineral wool, or a light curtain" : (a < 0.52 ? "about 3 cm of mineral wool, or a heavy curtain" : (a < 0.65 ? "about 4 cm of mineral wool, or 3 cm of foam" : (a < 0.78 ? "about 5 cm of acoustic foam" : (a < 0.92 ? "5 to 10 cm of mineral wool" : "10 cm or more of mineral wool with an air gap")))))))) );
// Returns tooltip string #wtp: s followed by the typical absorber thickness for absorption a (via wthick).
function wtip(s, a) ( sprintf(#wtp, "%s  ·  thickness: %s (bass needs more)", s, wthick(a)); #wtp; );
// Returns the name of wall-surface selection k (0 All walls, 1-4 Back/Right/Front/Left wall, 5 Floor + ceiling).
function wsurfname(k) ( k == 0 ? "All walls" : (k == 1 ? "Back wall" : (k == 2 ? "Right wall" : (k == 3 ? "Front wall" : (k == 4 ? "Left wall" : "Floor + ceiling")))); );

// Returns the display name of delay preset k.
function dpname(k) ( k == 0 ? "Slapback alley" : (k == 1 ? "Courtyard flutter" : (k == 2 ? "Street shot" : (k == 3 ? "Underground station" : (k == 4 ? "Stadium PA" : (k == 5 ? "PA announcement" : (k == 6 ? "Radio comms" : (k == 7 ? "Phone line echo" : (k == 8 ? "Forest edge" : (k == 9 ? "Cave" : (k == 10 ? "Canyon echo" : (k == 11 ? "Mountain call" : (k == 12 ? "Dream / flashback" : (k == 13 ? "Vocal 1/4" : (k == 14 ? "Ballad 1/4 dotted" : (k == 15 ? "Vocal throw 1/2" : (k == 16 ? "Rap 1/8 throws" : (k == 17 ? "Snare 1/16" : (k == 18 ? "Guitar 1/8 dotted" : (k == 19 ? "Lead 1/4 + 1/8 dotted" : (k == 20 ? "Ping-pong 1/8" : (k == 21 ? "Ping-pong 1/4 dotted" : (k == 22 ? "Synth 1/8 triplet" : (k == 23 ? "Wide 1/8 + 1/8 dotted" : (k == 24 ? "Tape echo 1/8" : (k == 25 ? "Analog BBD 1/4" : (k == 26 ? "Dub 1/4 dotted" : (k == 27 ? "Rockabilly slap" : (k == 28 ? "Haas widener" : (k == 29 ? "Doubler" : (k == 30 ? "Metal resonator" : (k == 31 ? "Robot voice comb" : (k == 32 ? "Glitch stutter" : (k == 33 ? "Alien chatter" : (k == 34 ? "Space ripples" : (k == 35 ? "Time freeze" : (k == 36 ? "Infinite wash" : (""))))))))))))))))))))))))))))))))))))); );
// Returns the one-line description of delay preset k.
function dpdesc(k) ( k == 0 ? "one wall across the street" : (k == 1 ? "flutter between parallel walls" : (k == 2 ? "gunshot / bang between two building rows" : (k == 3 ? "tiled platform, tunnel echo" : (k == 4 ? "delayed loudspeaker towers" : (k == 5 ? "station, airport, hall" : (k == 6 ? "intercom, cockpit, walkie echo" : (k == 7 ? "long-distance call, faint return" : (k == 8 ? "one echo from a tree line" : (k == 9 ? "stone cave, dense dark echoes" : (k == 10 ? "rock walls far away" : (k == 11 ? "valley, very distant echo" : (k == 12 ? "soft, wavering ping-pong" : (k == 13 ? "classic vocal echo" : (k == 14 ? "soft vocal tail for ballads" : (k == 15 ? "one word thrown into the space" : (k == 16 ? "short throws on line ends" : (k == 17 ? "tight rhythmic snare echo" : (k == 18 ? "rhythmic guitar delay" : (k == 19 ? "wide lead guitar or synth" : (k == 20 ? "left - right in eighths" : (k == 21 ? "wide, lazy bounce" : (k == 22 ? "shuffling triplet bounce" : (k == 23 ? "two rhythms left / right" : (k == 24 ? "warm, wobbly repeats" : (k == 25 ? "dark bucket-brigade repeats" : (k == 26 ? "long dark feedback" : (k == 27 ? "50s slapback, tape" : (k == 28 ? "moves the image to the left" : (k == 29 ? "second voice, slightly detuned" : (k == 30 ? "pipe, tube, robot ring" : (k == 31 ? "metallic comb on a voice" : (k == 32 ? "machine-gun repeats in 1/32" : (k == 33 ? "wobbling, warped echoes" : (k == 34 ? "sci-fi sixteenths" : (k == 35 ? "almost endless smear" : (k == 36 ? "endless dark cloud" : (""))))))))))))))))))))))))))))))))))))); );
// Returns the category of delay preset k: 0 for k < 13, 1 for k < 28, else 2.
function dpcat(k) ( k < 13 ? 0 : (k < 28 ? 1 : 2); );
// Returns the name of delay preset category c: Film & post, Music (tempo) or Sound design.
function dpcatname(c) ( c == 0 ? "Film & post" : (c == 1 ? "Music (tempo)" : "Sound design"); );
function dpsl(i) ( i < 8 ? 66 + i : (i < 18 ? 67 + i : 86); );   // field -> slider (74 = tempo is skipped, 18 = link level)
// the preset the delay matches now (-1 = custom); unused fields are ignored (ms in tempo mode, notes in ms mode, R when linked)
function dpmatch() local(k, i, b, m, sl, dpr)
(
  dpr = -1; k = 0;
  loop(NDP,
    b = dptab + k * 19; m = 1; i = 0;
    loop(19,
      sl = dpsl(i);
      ((i == 4 || i == 5) && b[1] >= 0.5) || ((i == 6 || i == 7) && b[1] < 0.5) || ((i == 5 || i == 7) && b[2] >= 0.5) || ((i == 13 || i == 15) && b[3] >= 0.5) || (i == 11 && b[18] >= 0.5) || (i == 9 && slider87 >= 0.5 && abs(b[8] - b[9]) < 0.01) ? 0 :
        ( abs(slider(sl) - b[i]) > 0.01 * max(1, abs(b[i])) ? ( m = 0; ); );
      i += 1;
    );
    m && dpr < 0 ? ( dpr = k; );
    k += 1;
  );
  dpr;
);
// Loads delay preset k (19 fields from dptab) into the delay sliders, sets slider65 on and the preset-type sliders 115/116, notifies automation.
function dpload(k) local(b, i)
(
  b = dptab + k * 19;
  snap_take();
  slider65 = 1;
  i = 0; loop(19, sset(dpsl(i), b[i]); i += 1; );
  sset(87, abs(b[8] - b[9]) < 0.01);
  sset(115, k == 24 || k == 26 || k == 27 ? 1 : (k == 25 ? 2 : (k == 6 || k == 32 ? 3 : 0))); sset(116, k == 7 ? 1 : 0);   // type of the preset
  snap_notify();
);
// TEMPO TOOLS: set the delay to a note value (Time -> Tempo) for L, R or both (tptg 0 / 1 / 2) at the tempo now chosen
// the note value (code of slider 72 / 73) nearest to a time in ms at a tempo
function tpnear(ms, bpm) local(n, b, d, bd) ( b = 8; bd = 1000000000; n = 0; loop(13, d = abs(log(max(1, g_dnote(n) * 60000 / bpm) / max(1, ms))); d < bd ? ( bd = d; b = n; ); n += 1; ); b; );
// Switches the delay to Tempo mode with note code at bpm (-1 keeps tempo source) for L, R or both per tptg; converts old ms times to nearest notes.
function tpapply(code, bpm) local(nb, wasms)
(
  snap_take();
  wasms = slider67 < 0.5;
  bpm >= 0 ? ( slider74 = bpm; );   // -1: keep the tempo source
  nb = slider74 > 0.5 ? slider74 : (tempo > 1 ? tempo : 120);
  // from ms to Tempo with only one side chosen (times not linked): the other side keeps (nearly) its time
  wasms && slider68 < 0.5 && tptg == 0 ? ( slider73 = tpnear(slider71, nb); );
  wasms && slider68 < 0.5 && tptg == 1 ? ( slider72 = tpnear(slider70, nb); );
  wasms && code < 0 ? ( slider72 = tpnear(slider70, nb); slider73 = tpnear(slider71, nb); );   // a tempo alone: the notes nearest the times
  slider67 = 1;
  code >= 0 ? (
    tptg != 1 ? ( slider72 = code; );
    tptg != 0 ? ( slider73 = code; );
    tptg == 1 ? ( slider68 = 0; );
  );
  snap_notify();
);
// TEMPO ANALYSIS (GUI thread, once per analysis): onset strength in 10 ms frames, slightly smoothed -> autocorrelation.
// Score of a beat period L (60 … 200 BPM, in quarter frames, interpolated): r(L) + r(2L) / 2 + r(4L) / 2 (beat, half bar,
// bar - so syncopated patterns do not win), a mild preference around 120 BPM. Then the fine step: the autocorrelation at
// 1, 2, 3 … periods - the error of one frame is divided by the number of periods (about 0.1 BPM).
// Confidence = how far the best score stands out (z-score); onsets = local maxima of the onset strength above mean + 1.5 sd.
function anari(x) local(i0) ( i0 = floor(x); i0 < anaN - 2 ? anaR[i0] + (anaR[i0 + 1] - anaR[i0]) * (x - i0) : 0; );
// Estimates tempo from the onset envelope anaNv: autocorrelation, best beat period; sets anaBf (BPM), anaConf, anaOns (onset count), anaRes (1 ok, -1 flat).
function anaest() local(n, mn, sd, i, L, acc, sc, bS, bL, sm2, sq, cnt, w, rPer, rK, k)
(
  n = anaN;
  i = 0; loop(n, anaS[i] = 0.5 * anaNv[i] + 0.25 * (i > 0 ? anaNv[i - 1] : anaNv[i]) + 0.25 * (i < n - 1 ? anaNv[i + 1] : anaNv[i]); i += 1; );
  mn = 0; i = 0; loop(n, mn += anaS[i]; i += 1; ); mn /= max(1, n);
  sd = 0; i = 0; loop(n, sd += sqr(anaS[i] - mn); i += 1; ); sd = sqrt(sd / max(1, n));
  L = 0; loop(n - 1, acc = 0; i = 0; loop(n - L, acc += (anaS[i] - mn) * (anaS[i + L] - mn); i += 1; ); anaR[L] = acc / (n - L); L += 1; );
  bS = -1000000000; bL = 50; sm2 = 0; sq = 0; cnt = 0; L = 30;
  loop(281,
    w = exp(-0.5 * sqr(log(6000 / L / 120) / log(2)));
    sc = (anari(L) + 0.5 * anari(2 * L) + 0.5 * anari(4 * L)) * (0.7 + 0.3 * w);
    sm2 += sc; sq += sc * sc; cnt += 1;
    sc > bS ? ( bS = sc; bL = L; );
    L += 0.25;
  );
  sm2 /= cnt; sq = sqrt(max(0, sq / cnt - sm2 * sm2));
  anaConf = sq > 0 ? (bS - sm2) / sq : 0;
  bS = -1000000000; rPer = bL - 1; L = bL;
  loop(201,
    rK = max(1, floor(0.8 * n / rPer)); sc = 0; k = 1;
    loop(rK, sc += anari(k * rPer); k += 1; );
    sc /= rK; sc > bS ? ( bS = sc; L = rPer; );
    rPer += 0.01;
  );
  anaBf = 6000 / L; abs(anaBf - floor(anaBf + 0.5)) < 0.12 ? ( anaBf = floor(anaBf + 0.5); );   // songs are mostly in whole BPM
  anaOns = 0; i = 1; loop(max(0, n - 2), anaNv[i] > mn + 1.5 * sd && anaNv[i] >= anaNv[i - 1] && anaNv[i] > anaNv[i + 1] ? anaOns += 1; i += 1; );
  anaRes = sd > 0.000001 ? 1 : -1;
);
// EXPERT SETTINGS ACTIVE (Basic): short names of the hidden Expert settings that change the sound
function xpname(k) ( k == 0 ? "Delay" : (k == 1 ? "Filter" : (k == 2 ? "Single walls" : (k == 3 ? "Source EQ" : (k == 4 ? "Reflect" : (k == 5 ? "Early / late level" : (k == 6 ? "Source direction" : (k == 7 ? "Facing" : (k == 8 ? "Floor + ceiling off" : (k == 9 ? "Barrier element" : (k == 10 ? "Input gain" : (k == 11 ? "Output" : (k == 12 ? "Snapshot morph" : (k == 13 ? "Hear" : (k == 14 ? "Module mix" : "Motion")))))))))))))); );
// (the tap table itself - tnc / tnname - follows below the motion helpers)
// TEMPO TOOLS FOR MOTION (opened from the Motion module): a note value sets the motion's cycle at once
// (straight 1/2 … 1/16 -> Rate: Tempo 1/2 … 1/16; 1/1 = 1 bar), Use sets the tempo and switches the motion to Tempo
function mtcode(q) ( q == 4 ? 0 : (q == 5 ? 2 : (q == 6 ? 5 : (q == 7 ? 8 : (q == 8 ? 11 : -1)))); );   // motion tempo -> note code
// Maps a delay note code (0,2,5,8,11) to motion tempo rate 4..8 and sets slider 108 to it via setp; other codes are ignored.
function tpmot(code) local(q) ( q = code == 0 ? 4 : (code == 2 ? 5 : (code == 5 ? 6 : (code == 8 ? 7 : (code == 11 ? 8 : -1)))); q > 0 ? setp(108, q); );
// Applies a tempo-tool choice: in the motion context (tpctx 14) sets tempo slider 74 and motion rate, otherwise calls tpapply(code, bpm).
function tpuse(code, bpm) ( tpctx == 14 ? ( bpm >= 0 ? setp(74, bpm); floor(slider108 + 0.5) == 0 ? setp(108, 4); ) : tpapply(code, bpm); );
// Returns the note code (slider 72/73 value) of tap-table entry k (1/4, 1/8, 1/8 dotted, 1/8 triplet, 1/16, 1/16 dotted).
function tnc(k) ( k == 0 ? 5 : (k == 1 ? 8 : (k == 2 ? 6 : (k == 3 ? 10 : (k == 4 ? 11 : 9)))); );
// Returns the label of tap-table note entry k (1/4, 1/8, 1/8 dotted, 1/8 triplet, 1/16, 1/16 dotted).
function tnname(k) ( k == 0 ? "1/4" : (k == 1 ? "1/8" : (k == 2 ? "1/8 dotted" : (k == 3 ? "1/8 triplet" : (k == 4 ? "1/16" : "1/16 dotted")))); );
// Maps frequency f (20 Hz-20 kHz, clamped, log scale) to an x pixel inside the EQ square plot (sqpx, width sqpw).
function sqfx(f) ( sqpx + log(max(20, min(20000, f)) / 20) / log(1000) * sqpw; );
// Maps gain g in dB (clamped -30..+15) to a y pixel inside the EQ square plot (sqpy, height sqph); +15 dB at the top.
function sqgy(g) ( sqpy + (15 - min(15, max(-30, g))) / 45 * sqph; );
// Stores the slider values (snap_take), sets the 13 EQ band sliders (47..59) to 0, then sends automation for the changed sliders.
function eqreset() local(i) ( snap_take(); i = 47; loop(13, sset(i, 0); i += 1; ); snap_notify(); );
// ---------- section help: "?" beside every section title -> description + signal flow with the section highlighted ----------
// ids: 1 ROOM, 2 REVERB, 3 SOURCE, 4 WALLS (REFLECTORS in free field), 5 BARRIER, 6 FILTER, 7 I/O, 8 echogram, 10 DELAY, 11 DECAY BY BAND, 14 MOTION, 15 FREE FIELD.  A click opens the panel (and closes it
// again); mouse-over opens it only while the tips (T in VIEW) are on.
function sechelp(id, s, x, yc, c) local(hx, hit)
(
  gfx_setfont(2); hx = x + (strlen(s) > 0 ? tws(s) + 9 : 0);
  hit = sqr(mx - hx) + sqr(my - yc) < 49;
  (hit || shpin == id) ? ( col(c, 1); CI(hx, yc, 5.5, 1); col(cSURF, 1); )
                       : ( col(cLINE, 1); CI(hx, yc, 5.5, 1); col(cMUTED, 1); );
  gfx_setfont(8, "Arial", floor(7.5 * gs + 0.5), 'b'); txtc("?", hx, yc);
  hit && tipShow ? ( shsec = id; shax = hx; shay = yc; );       // mouse-over only with the tips (T in VIEW) switched on
  shpin == id ? ( shbx = hx; shby = yc; );
  click && hit ? ( shpin = shpin == id ? 0 : id; shclr = 0; shbx = hx; shby = yc; );
);
// section heading of the redesign: color mark, name, "?"
function sechead(id, s, x, yc, c) ( col(c, 1); RR(x, yc - 6, 3, 12, 1.5); col(cTEXT, 1); gfx_setfont(2); txt(s, x + 9, yc, 0); sechelp(id, s, x + 9, yc, c); );
// Returns the section-help panel title string for section id (1 ROOM ... 15 FREE FIELD).
function shname(id) ( id == 15 ? "FREE FIELD  –  outdoors, no walls" : id == 14 ? "MOTION  –  source or listener on a path" : id == 11 ? "DECAY BY BAND  –  reverb time per octave" : id == 10 ? "DELAY  –  echoes before the room" : id == 1 ? "ROOM" : (id == 2 ? "REVERB" : (id == 3 ? "SOURCE" : (id == 4 ? (isfree ? "REFLECTORS  –  two free walls outdoors" : "WALLS") : (id == 5 ? "BARRIER" : (id == 6 ? "FILTER" : (id == 7 ? "OUTPUT  –  levels and limiter" : "TIMELINE  –  the room over time (echogram)")))))); );
// Returns the theme color (cXXTT constant) that goes with section id, used to tint its help panel.
function shcol(id) ( id == 15 ? cFFTT : id == 14 ? cMOTT : id == 11 ? cRVTT : id == 10 ? cDLTT : id == 1 ? cRMTT : (id == 2 ? cRVTT : (id == 3 ? cSRTT : (id == 4 ? cWALTT : (id == 5 ? cBARTT : (id == 6 ? cFLTT : (id == 7 ? cIOTT : cTEXT)))))); );
// Returns line hline (0-2) of the section-help description text for section id; the WALLS text differs in free field.
function shtext(id, hline) (
  id == 15 ? (hline == 0 ? "Outdoors: no walls, no ceiling, no reverb – only the direct sound and, with Ground, its" : (hline == 1 ? "reflection from the floor (Hard = asphalt, Grass, Snow) and the two Reflectors (facade, rock …)." : "Free field on = room off. Presets set area, ground and reflectors. Distance = film perspective.")) :
  id == 14 ? (hline == 0 ? "Moves the source, the listener or both on a pattern: circle, square, left – right, front –" : (hline == 1 ? "back, up – down, wave, a drawn path or a pass-by. Rate in Hz or tempo, Size in m, Glide = the" : "fade in / out. Runs with the transport – the same place at every playback.")) :
  id == 11 ? (hline == 0 ? "How long the reverb rings in each octave (RT60 = time to fall by 60 dB), exactly as the reverb" : (hline == 1 ? "engine plays it. Hard walls ring long and bright, soft ones swallow the highs first, air dampens them too." : "Green bars: room modes – below this frequency a small room rings on its own tones.")) :
  id == 10 ? (hline == 0 ? "Echoes before the room: L and R each have Time, Feedback (repeats), Level and" : (hline == 1 ? "a low / high cut in the feedback loop – every repeat gets thinner and darker." : "The echoes play into the room, so each one is reverberated. Off = no change.")) :
  id == 1 ? (hline == 0 ? "Room: width, depth, height, Size, absorption, heights and the angle of the source." : (hline == 1 ? "Reverb (2nd row): Early / Late level, Decay = RT60, Predelay, Density, Tone and" : "Reflect = how much all surfaces reflect (100 % = as built). Off = only the source model.")) : (
  id == 2 ? (hline == 0 ? "How the room answers: Early = level of the reflections, Late = level of the" : (hline == 1 ? "reverb, Decay = reverb time RT60, Predelay = gap before the room answers," : "Density = smooth or grainy tail, Tone = darker or brighter room sound.")) : (
  id == 3 ? (hline == 0 ? "What is making the sound: a phone, a radio, a megaphone … Each model is a" : (hline == 1 ? "filter curve plus distortion. Drag the points H, 1–3 and L to reshape the curve." : "Distortion pushes the device. Call line (Expert): band, duplex, codec, dropouts.")) : (
  id == 4 && isfree ? (hline == 0 ? "Two freely placed vertical planes outdoors: a facade, glass, rock face, fence or hedge." : (hline == 1 ? "Each one adds a real echo from where it stands (X, Y, Angle, Length, Height) – the sound" : "bounces off it like off a house wall. Drag the R1 / R2 handles in the Stage View.")) :
  id == 4 ? (hline == 0 ? "What the room is made of. Choose a surface, then a material: hard materials" : (hline == 1 ? "(glass, concrete) reflect – long, bright reverb; soft materials (curtain, foam)" : "absorb – short, dry room. Floor + ceiling switches their reflections on or off.")) : (
  id == 5 ? (hline == 0 ? "A wall, window or door between source and listener. Damp = how much it" : (hline == 1 ? "blocks (bass passes more), Open = how far the door or window is open," : "Pos = where it stands, Back part = material of the room behind it.")) : (
  id == 6 ? (hline == 0 ? "Low cut removes everything below its frequency (rumble), high cut everything" : (hline == 1 ? "above it (hiss). The dB value is the steepness per octave. Pre = filter the" : "input before source and room, Post = filter the finished output.")) :
  (id == 7 ? (hline == 0 ? "Levels in and out: In = gain into the plugin, Out = gain out of it," : (hline == 1 ? "Mix = balance between dry input and room. Limiter on: a look-ahead limiter" : "keeps every peak below −1 dBFS (1.5 ms latency, compensated by REAPER)."))
           : (hline == 0 ? "What the room does to one short click: time runs left to right (ms), level" : (hline == 1 ? "bottom to top (dB). Green bar = direct sound, blue bars = wall reflections," : "shaded area = reverb tail. RT60 = time until the reverb has dropped by 60 dB.")))
  )))));
);
function shboxname(k) ( k == 0 ? "IN" : (k == 1 ? "FILTER" : (k == 2 ? "SOURCE" : (k == 3 ? "DELAY" : (k == 4 ? "ROOM" : (k == 5 ? "MOTION" : (k == 6 ? "MIX" : (k == 7 ? "OUT" : (k == 8 ? "FILTER" : "LIMIT")))))))); );   // + MOTION
// highlight of flow box k for section id: 0 = none, 1 = solid (this section works here), 2 = outline (the section's other possible place)
function shhl(id, k) local(fp)
(
  fp = slider30 >= 0.5;
  id == 3 ? (k == 2) : (
  id == 14 ? (k == 5) : (
  id == 10 ? (k == 3) : (
  id == 6 ? ((k == 1 || k == 8) ? ((k == 8) == fp ? 1 : 2) : 0) : (
  id == 7 ? (k == 0 || k == 6 || k == 7 || k == 9) : (k == 4)))));
);
// Draws the section-help popup for id near (ax, ay): title, 3 text lines, and the signal-flow boxes with the section highlighted.
function drawsechelp(id, ax, ay) local(ffh, pw, ph, px, py, k, bx, by, hl, c, tw1, sw1, sw2, sw3, sx0)
(
  c = shcol(id);
  gfx_setfont(4); tw1 = max(tws(shtext(id, 0)), max(tws(shtext(id, 1)), tws(shtext(id, 2))));
  pw = max(10 * 36 + 9 * 8, tw1) + 20; ph = 108;
  px = min(max(ax + 10, 6), WT - 6 - pw); py = ay + 12; py + ph > scy + HT - 6 ? py = ay - 12 - ph;
  col(cINK, 0.14); RR(px + 1, py + 3, pw, ph, 8);
  col(cSURF, 1); RR(px, py, pw, ph, 8); col(c, 1); RRO(px, py, pw, ph, 8);
  col(c, 1); gfx_setfont(2); txt(shname(id), px + 10, py + 12, 0);
  col(cTEXT, 1); gfx_setfont(4);
  txt(shtext(id, 0), px + 10, py + 27, 0); txt(shtext(id, 1), px + 10, py + 38, 0); txt(shtext(id, 2), px + 10, py + 49, 0);
  col(cMUTED, 1); gfx_setfont(6); txt("SIGNAL FLOW", px + 10, py + 64, 0);
  by = py + 72; k = 0;
  loop(10,
    bx = px + 10 + k * 44; hl = shhl(id, k);
    hl == 1 ? ( col(c, 1); RR(bx, by, 36, 16, 4); col(cSURF, 1); )
            : ( col(cLINE, 1); RR(bx, by, 36, 16, 4); hl == 2 ? ( col(c, 1); RRO(bx, by, 36, 16, 4); ) : col(cMUTED, 1); );
    txtbox(shboxname(k), bx + 18, by + 8, 36, 16, 6);
    k < 9 ? ( col(cMUTED, 1); LN(bx + 37, by + 8, bx + 42, by + 8); POLY4(bx + 40, by + 5.5, bx + 43.5, by + 8, bx + 40, by + 10.5, bx + 40, by + 10.5); );
    k += 1;
  );
  // below the boxes: where the filter sits, and what is inside ROOM
  gfx_setfont(7);
  col(slider30 < 0.5 ? cTEXT : cDIM2, 1); txt("pre", px + 10 + 44 + 18, by + 23, 1);
  col(slider30 >= 0.5 ? cTEXT : cDIM2, 1); txt("post", px + 10 + 8 * 44 + 18, by + 23, 1);
// in the free field the ROOM box holds: free field (ground), reflectors, barrier - no walls, no reverb
  ffh = isfree || id == 15;
  ffh ? ( sw1 = tws("free field"); sw2 = tws("reflectors"); sw3 = tws("barrier"); ) : ( sw1 = tws("walls"); sw2 = tws("barrier"); sw3 = tws("reverb"); );
  sx0 = px + 10 + 4 * 44 + 18 - (sw1 + sw2 + sw3 + 12) / 2;
  ffh ? (
    col(id == 15 ? c : cMUTED, 1); txt("free field", sx0, by + 23, 0);
    col(id == 4 ? c : cMUTED, 1); txt("reflectors", sx0 + sw1 + 6, by + 23, 0);
    col(id == 5 ? c : cMUTED, 1); txt("barrier", sx0 + sw1 + sw2 + 12, by + 23, 0);
  ) : (
    col(id == 4 ? c : cMUTED, 1); txt("walls", sx0, by + 23, 0);
    col(id == 5 ? c : cMUTED, 1); txt("barrier", sx0 + sw1 + 6, by + 23, 0);
    col(id == 2 ? c : cMUTED, 1); txt("reverb", sx0 + sw1 + sw2 + 12, by + 23, 0);
  );
);
// Maps value v in [lo,hi] to a linear 0..1 fraction (clamped).
function lin2f(v, lo, hi) ( min(1, max(0, (v - lo) / (hi - lo))); );
// Inverse of lin2f: maps a 0..1 fraction f to a value in [lo,hi] linearly.
function f2lin(f, lo, hi) ( lo + f * (hi - lo); );
// Maps value v in [lo,hi] to a logarithmic 0..1 fraction (clamped).
function log2f(v, lo, hi) ( min(1, max(0, log(v / lo) / log(hi / lo))); );
// Inverse of log2f: maps a 0..1 fraction f to a value in [lo,hi] on a log scale.
function f2log(f, lo, hi) ( lo * exp(f * log(hi / lo)); );
// Sets the density parameter (slider 38) to d through setp, as a gesture-held GUI change.
function setdens(d) ( setp(38, d); );
// Sets wall absorption v (clamped 0.01..1) on the selected wall (selw: 0 = four side walls, 1-4 single walls, 5 floor/ceiling).
function setalpha(v)
(
  v = min(1, max(0.01, v));
  selw == 0 ? ( setp(5, v); setp(6, v); setp(7, v); setp(8, v); );
  selw == 1 ? setp(5, v);
  selw == 2 ? setp(6, v);
  selw == 3 ? setp(7, v);
  selw == 4 ? setp(8, v);
  selw == 5 ? setp(9, v);
);
// Sets the gfx color for a wall of absorption a (greener/darker when more absorbent, with wlift offset) and alpha al, then applies gmono().
function wallcol(a, al) ( gfx_r = (120 + wlift - 90 * a) / 255; gfx_g = (120 + wlift + 40 * a) / 255; gfx_b = (120 + wlift - 20 * a) / 255; gfx_a = al; gmono(); );
// Converts a linear amplitude v to dB (20*log10); returns -200 for values at or below 1e-10.
function dbs(v) ( v > 0.0000000001 ? 20 * log10(v) : -200; );

// oblique 3D projection : x right, y depth (0 = back wall), z up
function PJ(x, y, z) ( qx = pX0 + x * pXs + y * pKX; qy = pY0 + y * pKY - z * pSZ; );
// ROOM CORNERS in the 3D view: the two lower front corners (1 = left, 2 = right) within 8 px, or 0
function cornerhit() local(d1, d2)
(
  PJ(0, roomD, 0); d1 = sqrt(sqr(mx - qx) + sqr(my - qy));
  PJ(roomW, roomD, 0); d2 = sqrt(sqr(mx - qx) + sqr(my - qy));
  d2 < 8 && d2 <= d1 ? 2 : (d1 < 8 ? 1 : 0);
);
// ROOM EDGES in the 3D view: index of the edge under the mouse (0-3 along the width, 4-7 along the depth, 8-11 along the
// height) or -1; 6 px catch distance
function edgehit() local(e, ax, j, o1, o2, x0, y0, z0, x1, y1, z1, ax0, ay0, ux, uy, l2, t, d, best)
(
  ehx = -1; best = 6;
  e = 0;
  loop(12,
    ax = floor(e / 4); j = e % 4; o1 = j & 1; o2 = (j >> 1) & 1;
    ax == 0 ? ( x0 = 0; x1 = roomW; y0 = y1 = o1 * roomD; z0 = z1 = o2 * roomH; ) : (
    ax == 1 ? ( x0 = x1 = o1 * roomW; y0 = 0; y1 = roomD; z0 = z1 = o2 * roomH; ) : (
              x0 = x1 = o1 * roomW; y0 = y1 = o2 * roomD; z0 = 0; z1 = roomH; ));
    PJ(x0, y0, z0); ax0 = qx; ay0 = qy; PJ(x1, y1, z1);
    ux = qx - ax0; uy = qy - ay0; l2 = max(0.0001, ux * ux + uy * uy);
    t = min(1, max(0, ((mx - ax0) * ux + (my - ay0) * uy) / l2));
    d = sqrt(sqr(mx - ax0 - t * ux) + sqr(my - ay0 - t * uy));
    d < best && e != 3 && (!sidevOn || ax == 2 || (frontv ? (ax == 0 && o2 == 1) : (ax == 1 && o2 == 1))) && (!hidew || (ax < 2 && o2 == 0)) && (!planv || (ax < 2 && o2 == 0)) ? ( best = d; ehx = e; );   // not the faint top front edge; walls hidden / plan: floor edges only
    e += 1;
  );
  ehx;
);
// Draws room box edge e (0-11: 4 per axis x/y/z) in color c as a projected 3D line.
function edgedraw(e, c) local(ax, j, o1, o2, x0, y0, z0, x1, y1, z1, ax0, ay0)
(
  ax = floor(e / 4); j = e % 4; o1 = j & 1; o2 = (j >> 1) & 1;
  ax == 0 ? ( x0 = 0; x1 = roomW; y0 = y1 = o1 * roomD; z0 = z1 = o2 * roomH; ) : (
  ax == 1 ? ( x0 = x1 = o1 * roomW; y0 = 0; y1 = roomD; z0 = z1 = o2 * roomH; ) : (
            x0 = x1 = o1 * roomW; y0 = y1 = o2 * roomD; z0 = 0; z1 = roomH; ));
  PJ(x0, y0, z0); ax0 = qx; ay0 = qy; PJ(x1, y1, z1);
  col(c, 1); LNW(ax0, ay0, qx, qy, 3);
);

// point on a wall face: id 1 = top/back, 2 = right, 3 = bottom/front, 4 = left; u along wall, t up
function facept(id, u, t)
(
  id == 1 ? PJ(u * roomW, 0, t * roomH) : (
    id == 2 ? PJ(roomW, u * roomD, t * roomH) : (
      id == 3 ? PJ(u * roomW, roomD, t * roomH) : PJ(0, u * roomD, t * roomH)));
);
// Draws the projected 4-sided outline of wall face id with line width wd.
function faceoutline(id, wd) local(x1, y1, x2, y2, x3, y3, x4, y4)
(
  facept(id, 0, 0); x1 = qx; y1 = qy; facept(id, 1, 0); x2 = qx; y2 = qy;
  facept(id, 1, 1); x3 = qx; y3 = qy; facept(id, 0, 1); x4 = qx; y4 = qy;
  LNW(x1, y1, x2, y2, wd); LNW(x2, y2, x3, y3, wd); LNW(x3, y3, x4, y4, wd); LNW(x4, y4, x1, y1, wd);
);
// Draws wall face id as a tinted projected quad plus diagonal hatching whose density follows absorption a.
function drawface(id, a) local(x1, y1, x2, y2, x3, y3, x4, y4, u, k, st, t0, t1, hx, hy)
(
  facept(id, 0, 0); x1 = qx; y1 = qy; facept(id, 1, 0); x2 = qx; y2 = qy;
  facept(id, 1, 1); x3 = qx; y3 = qy; facept(id, 0, 1); x4 = qx; y4 = qy;
  wallcol(a, 0.10); POLY4(x1, y1, x2, y2, x3, y3, x4, y4);
  a >= 0.03 ? (
    wallcol(a, 0.45);
    k = 0.18; st = max(0.025, 0.22 - 0.2 * a); u = -k;
    while (
      t0 = max(0, -u / k); t1 = min(1, (1 - u) / k);
      facept(id, u + k * t0, t0); hx = qx; hy = qy;
      facept(id, u + k * t1, t1);
      LN(hx, hy, qx, qy);
      u += st;
      u < 1;
    );
  );
  wallcol(a, 0.9); faceoutline(id, 1.5);
);
// is (mx,my) inside the projected wall face?
function inface(id) local(x1, y1, x2, y2, x3, y3, x4, y4, c1, c2, c3, c4)
(
  facept(id, 0, 0); x1 = qx; y1 = qy; facept(id, 1, 0); x2 = qx; y2 = qy;
  facept(id, 1, 1); x3 = qx; y3 = qy; facept(id, 0, 1); x4 = qx; y4 = qy;
  c1 = (x2 - x1) * (my - y1) - (y2 - y1) * (mx - x1);
  c2 = (x3 - x2) * (my - y2) - (y3 - y2) * (mx - x2);
  c3 = (x4 - x3) * (my - y3) - (y4 - y3) * (mx - x3);
  c4 = (x1 - x4) * (my - y4) - (y1 - y4) * (mx - x4);
  (c1 >= 0 && c2 >= 0 && c3 >= 0 && c4 >= 0) || (c1 <= 0 && c2 <= 0 && c3 <= 0 && c4 <= 0);
);
// Draws a 72-segment ring (center cx0,cy0, radius rad) on the floor, clipped to the room; dashed 1 = every other, 2 = every third segment.
function floorring(cx0, cy0, rad, dashed) local(q, a1, a2, xq1, yq1, xq2, yq2, hx, hy)
(
  q = 0;
  loop(72,
    a1 = q / 72 * 2 * $pi; a2 = (q + 1) / 72 * 2 * $pi;
    xq1 = cx0 + rad * cos(a1); yq1 = cy0 + rad * sin(a1);
    xq2 = cx0 + rad * cos(a2); yq2 = cy0 + rad * sin(a2);
    (!dashed || (dashed == 2 ? q % 3 == 0 : (q & 1) == 0)) && xq1 >= 0 && xq1 <= roomW && yq1 >= 0 && yq1 <= roomD && xq2 >= 0 && xq2 <= roomW && yq2 >= 0 && yq2 <= roomD ? (
      PJ(xq1, yq1, 0); hx = qx; hy = qy; PJ(xq2, yq2, 0); LNW(hx, hy, qx, qy, 0.7);
    );
    q += 1;
  );
);
// Draws a soft matte ellipse shadow on the floor under an object at room position (x, y) using pixel rows.
function shadow(x, y) local(cx, cy, rx, ry, k, n, t, w)
(
// a soft matte ellipse on the floor (pixel rows, no triangle seams)
  PJ(x, y, 0); cx = gox + qx * gs; cy = goy + qy * gs; rx = 0.3 * pS * gs; ry = max(1, 0.3 * pKY * gs);
  col(cINK, 0.09); n = floor(2 * ry); k = 0;
  loop(n, t = (k + 0.5 - ry) / ry; w = rx * sqrt(max(0, 1 - t * t)); gfx_rect(floor(cx - w + 0.5), floor(cy - ry + k), max(1, floor(2 * w + 0.5)), 1); k += 1; );
);
// Draws the dashed source-to-reflection-point-to-listener path with a dot; point clamped into the room, brighter when on, alpha scaled by 1-a.
function reflpath(px, py, pz, a, on) local(hx, hy, x2, y2)
(
  px = min(roomW, max(0, px)); py = min(roomD, max(0, py)); pz = min(roomH, max(0, pz));   // the reflection point always on a surface of the room
  PJ(sx, sy, sz); hx = qx; hy = qy;                                // from the center of the source ball (the old icon sat 10 px higher)
  PJ(px, py, pz); x2 = qx; y2 = qy;
  col(cRMTT, on ? 0.25 + 0.6 * (1 - a) : 0.12);   // reflections are blue (as in the Timeline; was the old teal)
  DL(hx, hy, x2, y2, 4, 3, 0.7);
  PJ(lx, ly, lz); DL(x2, y2, qx, qy, 4, 3, 0.7);
  col(cRMTT, on ? 1 : 0.3); CI(x2, y2, 2, 1);
);
// padlock beside the name plate of a locked source / listener: its own small plate of the same height (14), fine lines.
// (x, yc) = center of the plate
function drawlock(x, yc, c) local(y)
(
  col(cSURF, 0.92); RR(x - 6, yc - 7, 12, 14, 4);
  y = yc - 0.6; col(c, 1);
  SDB0(); SDARC(x, y - 2, 2.5, -0.5 * $pi, 0.5 * $pi); SDSEG(x - 2.5, y - 2, x - 2.5, y + 0.4); SDSEG(x + 2.5, y - 2, x + 2.5, y + 0.4); SDDRAW(1.2);   // smooth shackle
  SDB0(); SDSEG(x - 1.6, y + 2.3, x + 1.6, y + 2.3); SDDRAW(4.6);           // body: a rounded block (smooth edges)
  col(cSURF, 1); SDDISC(x, y + 1.7, 0.85); SDB0(); SDSEG(x, y + 2, x, y + 3.6); SDDRAW(0.7);   // keyhole
);
// ---- source and listener as shaded balls with a direction arrow - in the plan and in 3D, in every mode ----
// a ball: color c, radius r (px at the page scale), lit from the top left; letter inside (S / L)
function ball3d(x, y, r, c, s)
(
// matte - one flat color, a fine edge a little darker, the letter in white (B&W: source solid, listener a ring)
  monoOn ? (
    strcmp(s, "L") == 0 ? ( col(cSURF, 1); CIF(x, y, r, 1); col(cTEXT, 1); CIF(x, y, r, 0); CIF(x, y, r - 0.7, 0); CIF(x, y, r - 1.4, 0); col(cTEXT, 1); )
                        : ( col(cTEXT, 1); CIF(x, y, r, 1); col(cBG, 1); );
  ) : (
    col(0x000000, darkmode ? 0.35 : 0.10); CIF(x + 0.6, y + 1.8, r + 1.2, 1); col(0x000000, darkmode ? 0.25 : 0.07); CIF(x + 0.4, y + 1.2, r + 0.6, 1);   // a soft drop shadow
    col(c, 1); CIF(x, y, r, 1);
    col(0xFFFFFF, darkmode ? 0.06 : 0.11); CIF(x - r * 0.22, y - r * 0.26, r * 0.62, 1);   // a faint highlight
    col(0x000000, 0.14); CIF(x, y, r, 0); CIF(x, y, r - 0.6, 0);
    col(darkmode ? 0x101418 : 0xFFFFFF, 1);   // dark mode: light pastel balls, dark letter
  );
  gfx_setfont(6); txt(s, x, y + 0.5, 1);
);
// direction arrow from the edge of a ball (center x0, y0, radius r) to x1, y1 (projected)
function arrow3d(x0, y0, x1, y1, r, c, al) local(dx, dy, l, ex, ey)
(
  dx = x1 - x0; dy = y1 - y0; l = max(0.001, sqrt(dx * dx + dy * dy)); dx /= l; dy /= l;
  l > 0.6 ? (                                            // (pointing straight at the viewer: no arrow)
    l = max(l, r + 17); x1 = x0 + dx * l; y1 = y0 + dy * l;  // at least a readable arrow beside the ball
    ex = x1 - dx * 6; ey = y1 - dy * 6;
    col(c, al); LNW(x0 + dx * (r + 2), y0 + dy * (r + 2), ex, ey, 1.5);
    POLY4(x1, y1, ex - dy * 3.6, ey + dx * 3.6, ex + dy * 3.6, ey - dx * 3.6, x1, y1);
  );
);
// model loaded (and on), 3D: an info sign in the bottom right corner of the stage - icon + "Source model: …"
function srcsign(fz) local(w, x, y)
(
  gfx_setfont(6); sprintf(#ssg, "Source model: %s", srcname(fz)); w = tws(#ssg) + 34;
  x = VX + VW - 16 - w; y = VY + VH - 26 - 26;
  x < VX + 204 ? y -= 30;   // not under the source pill (bottom left) in a narrow view
  col(cSURF, 0.94); RR(x, y, w, 20, 6); col(cSRC, 1); RRO(x, y, w, 20, 6);
  icOL = 1; srcicon(fz, x + 13, y + 15.5, 0.55, cTEXT, cSURF); icOL = 0;   // line icons everywhere
  col(cTEXT, 1); gfx_setfont(6); txt(#ssg, x + 26, y + 10, 0);
);
// STEREO WIDTH helpers: the value in % or m (vopt[50]: 100 % = 1 m by convention - the side signal has no place of
// its own, the meter is the width the span in the room view shows), the unit toggle, the Mono mix power button
function swfmt() ( vopt[50] >= 0.5 ? sprintf(#fv, "%.2f m", slider98 / 100) : sprintf(#fv, "%.0f %%", slider98); kmap(0, 0, 200, 1); );
// Draws the % / m unit toggle for stereo width (sets vopt[50]) with a tooltip, at (x, y).
function swunit(x, y) local(u) (
  u = vopt[50] >= 0.5;
  chip(x, y, 14, 11, "%", !u, cSRTT) ? ( vopt[50] = 0; click = 0; ); chip(x + 16, y, 14, 11, "m", u, cSRTT) ? ( vopt[50] = 1; click = 0; );
  tipbox(x, y, 30, 11, "Stereo width in % of the input or in meters (100 % = 1 m): the same control, another label – the span in the Stage View is always in meters");
);
// SOURCE PILL: stereo width and mono mix of the source, in the room view - visible and adjustable from every
// module and in Basic, 136 x 20 (plus the Handles chip,) in the lower left corner of the plan / 3D view
function srcpill(x, y) local(on, nf, hit, dpw)
(
  on = slider110 >= 0.5;
  col(cSURF, 0.92); RR(x, y, 136, 20, 7); col(cBORD, 1); RRO(x, y, 136, 20, 7);   // compact
  col(cSRTT, 1); RR(x + 6, y + 5, 3, 10, 1.5);
  tgl(x + 6, y + 3, on, cSRTT) ? ( slider110 = on ? 0 : 1; sauto1(110); click = 0; );
  col(on ? cTEXT : cMUTED, 1); gfx_setfont(7); txt("mono", x + 31, y + 10, 0);
  tipbox(x + 6, y, 48, 20, on ? "Source – mono mix on: the input is summed to mono before anything else. Click: stereo again" : "Source – mono mix: sum the input to mono before any processing (the same switch as in the Source module)");
  swfmt(); kdim = on || devm;
  nf = hsl(86, x + 60, y + 1, 36, "", "", slider98 / 200, 0.5, on || devm ? cDIM : cSRTT); kdim = 0;
  col(on || devm ? cDIM2 : cTEXT, 1); gfx_setfont(7); txt(#fv, x + 131, y + 10, 2);
  hit = inbox(x + 55, y, 80, 20);
// the stereo handles (the span of the source in the view) on / off - drawn only while the chip fits into the view
  x + 200 <= VX + VW - 8 ? (
    chip(x + 142, y + 3, 58, 14, "Handles", vopt[51] < 0.5, cSRTT) ? ( vopt[51] = vopt[51] >= 0.5 ? 0 : 1; click = 0; );
    tipbox(x + 142, y + 3, 58, 14, vopt[51] < 0.5 ? "Stereo handles on (key H): the span of the source is always drawn with two ends you can drag – display only, the signal is not touched. Click: hide them (Alt+Shift+drag on the source ball still sets the width)" : "Stereo handles hidden (key H). Click: show them again");
  );
// Doppler while dragging by hand (slider135): on = the real pitch glide, off = only the distance (level, filter, room)
  dpw = x + 264 <= VX + VW - 8 ? 58 : 44;   // a narrow view: the short label still fits
  x + 206 + dpw <= VX + VW - 8 ? (
    chip(x + 206, y + 3, dpw, 14, dpw > 50 ? "Doppler" : "Dop", slider135 < 0.5, cSRTT) ? ( slider135 = slider135 >= 0.5 ? 0 : 1; slider_automate(slider135); click = 0; );
    tipbox(x + 206, y + 3, dpw, 14, slider135 < 0.5 ? "Doppler on: dragging source or listener glides the delays – a real pitch change while they move (fast drags are audible). Click: off – only the distance changes (level, filter, room), no pitch. Motion has its own Doppler setting" : "Doppler off while dragging by hand: the pitch stays, only the distance changes. Click: on again (the real pitch glide)");
  );
  kchg && !(on || devm) ? setp(98, nf * 200); kchg = 0;
  hit && mouse_wheel != 0 ? mouse_wheel = 0;   // (not the floor zoom)
  tipbox(x + 55, y, 80, 20, devm ? "Source – stereo width: no effect with a device (a mono loudspeaker)" : on ? "Source – stereo width: off while mono mix is on" : "Source – stereo width: 100 % = as the input comes in, 0 % = mono, up to 200 % = wider (drag, wheel, Alt+click = 100 %; the same control as in the Source module, % or m there)");
);
// Draws the Mono mix toggle (slider110) at (x, y), label beside it if side else below; click flips it and sends automation.
function monobtn(x, y, side) local(on) (
  on = slider110 >= 0.5;
  tgl(x, y, on, cSRTT) ? ( slider110 = on ? 0 : 1; sauto1(110); click = 0; );
  col(on ? cTEXT : cMUTED, 1); gfx_setfont(7); side ? txt("Mono mix", x + 28, y + 7, 0) : txt("Mono mix", x + 15, y + 24, 1);
  tipbox(x - 3, y - 3, side ? 80 : 32, side ? 20 : 32, devm && !on ? "Mono mix: with a device only the dry part of Dry / wet changes (the device itself is mono anyway)" : on ? "Mono mix on: the input is summed to (L + R) / 2 before anything else – click: stereo again" : "Mono mix: sum the input to mono before any processing (automatable) – the stereo width stays set, just switched off");
);
// the stereo span of the source in the room view (plan and 3D): a soft, matt capsule through the ball, across its aim,
// as long as the stereo width (100 % = 1 m) - only with a stereo input, without mono mix and without a device model
function drawspan() local(h, px, py, x1, y1, x2, y2, ok)
(
  ok = vopt[51] < 0.5 && slider110 < 0.5 && !(slider85 >= 0.5 && fzsel > 0);   // (also at 0 %: the handles stay, so the width can be opened again)
  spHov = 0;
  ok ? (
    h = 0.5 * slider98 / 100; px = -srAy; py = srAx;
    PJ(sx + px * h, sy + py * h, sz); x1 = qx; y1 = qy; PJ(sx - px * h, sy - py * h, sz); x2 = qx; y2 = qy;
// in a large area (free field) 1 m is only a few pixels - the span is drawn at least 2 x 18 px long so the handles can be seen and grabbed (display only)
    spHL = sqrt(sqr(x2 - x1) + sqr(y2 - y1)) / 2; spTr = max(0.01, spHL); spPx = slider98 > 2 ? spHL / slider98 : max(0.01, abs(pXs)) / 200;   // spPx: px per % (below 2 % from the scale of the view, so the step does not jump)   // spTr: the true half length (the drag measures against it - the drawn one can be longer)
// the drawn minimum shrinks a little with the width (0 % = 16 px, 100 % = 18 px); at 0 % the capsule is gone and a thin
// ring hugs the ball (one channel) - so a small room still shows mono against stereo
    spMn = 16 + 2 * min(1, slider98 / 100);
    spHL < spMn ? (
      spCx = (x1 + x2) / 2; spCy = (y1 + y2) / 2;
      PJ(sx + px, sy + py, sz); spUx = qx - spCx; spUy = qy - spCy; h = sqrt(sqr(spUx) + sqr(spUy));   // the direction across the aim (also at 0 %)
      h > 0.01 ? ( spUx /= h; spUy /= h; ) : ( spUx = 1; spUy = 0; );
      x1 = spCx - spUx * spMn; y1 = spCy - spUy * spMn; x2 = spCx + spUx * spMn; y2 = spCy + spUy * spMn;
    );
    spAl = min(1, slider98 / 25);   // the capsule fades out below 25 % (the handles stay)
    spAl > 0.01 ? ( col(cSRC, 0.20 * spAl); LNW(x1, y1, x2, y2, 7); );
    col(cSRC, 0.20); CIF(x1, y1, 3.5, 1); CIF(x2, y2, 3.5, 1);
    spAl > 0.01 ? ( col(cSRC, 0.55 * spAl); LNW(x1, y1, x2, y2, 1.4); );
    col(cSRC, 0.55); CIF(x1, y1, 2.2, 1); CIF(x2, y2, 2.2, 1);
    slider98 < 0.5 ? ( PJ(sx, sy, sz); col(cSRC, 1); CI(qx, qy, 12.6, 0); CI(qx, qy, 13.2, 0); );   // mono: one thin ring around the ball (one channel)
// the two ends are handles - drag = stereo width (also Alt+Shift+drag on the source ball, see the room view mouse)
    spX1 = x1; spY1 = y1; spX2 = x2; spY2 = y2; spCx = (x1 + x2) / 2; spCy = (y1 + y2) / 2;
    spHL = max(1, sqrt(sqr(x2 - x1) + sqr(y2 - y1)) / 2); spUx = (x2 - x1) / (2 * spHL); spUy = (y2 - y1) / (2 * spHL);
    spHov = spHL >= 12 && (sqr(mx - x1) + sqr(my - y1) < 64 || sqr(mx - x2) + sqr(my - y2) < 64);   // the mouse on a handle (the walls and edges stand back - tooltips and clicks)
    spHov || drag == 13 ? (
      col(cSRC, 0.9); CIF(x1, y1, 4.2, 0); CIF(x2, y2, 4.2, 0);
      swfmt(); strcpy(#swt, "Stereo width "); strcat(#swt, #fv); strcat(#swt, " – drag the ends (Shift = fine) · double-click = 100 % / 0 % · Alt+click = 100 % · Ctrl/Cmd+click = 0 %");
      tipon = 1; tipforce = 0; tipstr = #swt; tipx = spCx; tipy = min(y1, y2) - 16;
    );
  );
  spOk = ok;
);
// Draws the source in the room view: shadow, stem, aim arrow, stereo span and the 'S' ball, including the source model/lock markers.
function drawsource() local(fz3, bx, by)
(
  fz3 = slider85 >= 0.5 ? fzsel : 0;                // source model off: the plain source
  !planv ? shadow(sx, sy);
  PJ(sx, sy, 0); bx = qx; by = qy; PJ(sx, sy, sz);
  col(cSRC, 0.7); DL(bx, by, qx, qy, 3, 3, 1);
  // where the source points (its axis): a short arrow on the floor plane - turned by SOURCE ANGLE
  sax = qx; say = qy; PJ(sx + srAx * 0.7, sy + srAy * 0.7, sz);
// the ball design (the old icon / name-plate branches that never ran are removed)
  arrow3d(sax, say, qx, qy, 10, cSRC, abs(slider61) > 0.5 || slider63 >= 0.5 ? 0.95 : 0.4);
  drawspan();
  PJ(sx, sy, sz); ball3d(qx, qy, 10, cSRC, "S");
  dpqx = qx; dpqy = qy - 18; dpok = 1; dpsw = 24;            // (the delay plate goes above the ball)
  lockS ? drawlock(qx + 17, qy + 8, cSRC);
  fz3 > 0 ? srcsign(fz3);
  // height: shown in the HEIGHT section of the room view
);
// Draws the listener in the room view: shadow, stem, facing arrow, the 'L' ball, lock icon and the OUCH burst after a wall hit.
function drawlistener() local(bx, by, hx, hy)
(
  !planv ? shadow(lx, ly);
  PJ(lx, ly, 0); bx = qx; by = qy; PJ(lx, ly, lz);
  col(cLIS, 0.7); DL(bx, by, qx, qy, 3, 3, 1);
  hx = qx; hy = qy; PJ(lx + cos(face) * 0.7, ly + sin(face) * 0.7, lz);
  arrow3d(hx, hy, qx, qy, 10, cLIS, 0.95); ball3d(hx, hy, 10, cLIS, "L"); hy -= 9;
  PJ(lx, ly, lz); lockL ? drawlock(qx + 17, qy + 8, cLIS);
// OUCH as a comic burst: a jagged star (16 spikes, yellow, red zigzag edge) that pops up over the head, grows a
  // little in the first 120 ms and holds for 0.8 s
  wca = time_precise() - wcT;
  wca >= 0 && wca < 0.8 ? (
    gfx_setfont(2); wcw = tws("OUCH!"); wsc = wca < 0.12 ? 0.85 + 0.15 * wca / 0.12 : 1; wbcx = hx; wbcy = hy - 46;
    wrx = (wcw / 2 + 16) * wsc; wry = 17 * wsc; wk = 0;
    loop(16,
      wa0 = wk * 0.3926991; wa1 = wa0 + 0.19635; wa2 = wa0 + 0.3926991;
      col(0xFFE34D, 1); POLY4(wbcx, wbcy, wbcx + wrx * cos(wa0), wbcy + wry * sin(wa0), wbcx + wrx * 0.72 * cos(wa1), wbcy + wry * 0.72 * sin(wa1), wbcx + wrx * cos(wa2), wbcy + wry * sin(wa2));
      col(0xD03030, 1); LNW(wbcx + wrx * cos(wa0), wbcy + wry * sin(wa0), wbcx + wrx * 0.72 * cos(wa1), wbcy + wry * 0.72 * sin(wa1), 1.3);
      LNW(wbcx + wrx * 0.72 * cos(wa1), wbcy + wry * 0.72 * sin(wa1), wbcx + wrx * cos(wa2), wbcy + wry * sin(wa2), 1.3);
      wk += 1;
    );
    col(0xD03030, 1); txt("OUCH!", wbcx, wbcy, 1);
  );
  // height: shown in the HEIGHT section of the room view
);
// source and listener at exactly the same spot: the 'L' ball with its arrow and a smaller 'S' ball beside it
function drawcombined() local(fz3, bx, by, hx, hy)
(
  fz3 = slider85 >= 0.5 ? fzsel : 0;                // source model off: the plain source
  !planv ? shadow(sx, sy);
  PJ(sx, sy, 0); bx = qx; by = qy; PJ(sx, sy, max(sz, lz));
  col(cLIS, 0.7); DL(bx, by, qx, qy, 3, 3, 1);
  PJ(lx, ly, lz); hx = qx; hy = qy; PJ(lx + cos(face) * 0.7, ly + sin(face) * 0.7, lz); arrow3d(hx, hy, qx, qy, 10, cLIS, 0.95); ball3d(hx, hy, 10, cLIS, "L");
  PJ(sx, sy, sz); ball3d(qx + 9, qy + 4, 8, cSRC, "S");
  PJ(sx, sy, max(sz, lz));
  fz3 > 0 ? srcsign(fz3); lockS || lockL ? drawlock(qx + 20, qy + 8, lockL ? cLIS : cSRC);
  // height: shown in the HEIGHT section of the room view
);

// ---------- waves view: one wavefront circle in the horizontal plane z = cz, clipped to x 0..W, y y0..y1 ----------
// segments on the far side of the barrier (seen from the circle's center) are drawn faint: what gets through
function wring(cx, cy, cz, wrr, y0, y1, al, c) local(n, i, a0, a1, wxa, wya, wxb, wyb, wxm, wym, hx, hy, f)
(
  n = max(16, min(72, floor(wrr * pS / 4)));
  i = 0;
  loop(n,
    a0 = 2 * $pi * i / n; a1 = 2 * $pi * (i + 1) / n;
    wxa = cx + wrr * cos(a0); wya = cy + wrr * sin(a0); wxb = cx + wrr * cos(a1); wyb = cy + wrr * sin(a1);
    wxm = 0.5 * (wxa + wxb); wym = 0.5 * (wya + wyb);
    wxm >= 0 && wxm <= roomW && wym >= y0 && wym <= y1 ? (
      f = (bon && ((wym - yb) * (cy - yb) < 0)) ? 0.22 : 1;
      col(c, al * f);
      PJ(wxa, wya, cz); hx = qx; hy = qy; PJ(wxb, wyb, cz); LNW(hx, hy, qx, qy, 1.3);
    );
    i += 1;
  );
);
// ---------- barrier materials: 3 presets per type (Rw in dB) ----------
function bmatrw(t, i) ( t == 3 ? (i == 0 ? 17 : (i == 1 ? 25 : 37)) : (t == 2 ? (i == 0 ? 24 : (i == 1 ? 30 : 39)) : (i == 0 ? 38 : (i == 1 ? 45 : 55))); );   // installed values R'w
// Returns the barrier material name for barrier type t (1 wall, 2 window, 3 door) and material index i (0-2).
function bmatname(t, i) ( t == 3 ? (i == 0 ? "Hollow-core door" : (i == 1 ? "Heavy wood door" : "Acoustic door"))
                        : (t == 2 ? (i == 0 ? "Single glazing" : (i == 1 ? "Double glazing" : "Acoustic glazing"))
                                  : (i == 0 ? "Drywall" : (i == 1 ? "Brick wall 12 cm" : "Concrete 20 cm"))); );
// Returns the tooltip text (construction and approximate dB insulation) for barrier type t and material index i.
function bmattip(t, i) ( t == 3 ? (i == 0 ? "Hollow-core door, 40 mm honeycomb panel: about 17 dB installed (lab Rw 20–22)" : (i == 1 ? "Heavy wood door, 45 mm solid wood, no seals: about 25 dB installed (lab Rw 30–32)" : "Acoustic door, solid core + double seals: about 37 dB installed (lab Rw 40–42)"))
                       : (t == 2 ? (i == 0 ? "Single glazing, 4 mm float glass: about 24 dB installed (lab Rw 27–29)" : (i == 1 ? "Double glazing 4/16/4 mm: about 30 dB installed (lab Rw 32–34)" : "Acoustic glazing 8/16/6 mm laminated: about 39 dB installed (lab Rw 41–43)"))
                                 : (i == 0 ? "Drywall, 2 × 12.5 mm gypsum on studs + mineral wool: about 38 dB installed (lab Rw 42)" : (i == 1 ? "Interior brick wall 11.5 cm, plastered: about 45 dB installed (lab Rw 47–48)" : "Concrete 20 cm: about 55 dB installed (lab Rw 57–59)"))); );
// Returns the index (0-2) of the barrier material whose Rw matches slider44 for type bt, or -1 if none matches.
function bmatsel() local(i, bmr) ( bmr = -1; i = 0; loop(3, abs(slider44 - bmatrw(bt, i)) < 0.05 ? bmr = i; i += 1; ); bmr; );
// one rectangle of the barrier plane (x0..x1, z0..z1) at depth yb
function bquad(x0, x1, z0, z1) local(ax, ay, bx2, by2, cx2, cy2)
(
  x1 > x0 + 0.001 && z1 > z0 + 0.001 ? (
    PJ(x0, yb, z0); ax = qx; ay = qy; PJ(x1, yb, z0); bx2 = qx; by2 = qy; PJ(x1, yb, z1); cx2 = qx; cy2 = qy; PJ(x0, yb, z1);
    POLY4(ax, ay, bx2, by2, cx2, cy2, qx, qy);
  );
);
// the barrier in the 3D view: translucent gray plane (opacity and hatching = damping), the element with its
// opening, the door leaf / window sash turned into the source's side, the depth handle on the top edge
function drawbarrier() local(al, u, st, x1, x2, hx, hy, sg, lx2, ly2, zt, dz, hz, hw, cs, sn, k, ax)
(
  al = 0.10 + 0.22 * bdamp / 70;
  col(cBARV, al);
  bt == 1 ? bquad(brX0, brX1, 0, brH) : (
    bquad(brX0, bex0, 0, brH); bquad(bex1, brX1, 0, brH); bquad(bex0, bex1, 0, bez0); bquad(bex0, bex1, bez1, brH);
  );
  // hatching: denser = more damping (outside the element)
  col(cBARV, 0.35);
  st = max(0.12, 0.7 - 0.008 * bdamp) * max(1, brW / 8); u = brX0 + st * 0.5;
  while (
    x1 = u; x2 = min(brX1, u + 0.25 * brH);
    (bt == 1 || x2 < bex0 || x1 > bex1) ? ( PJ(x1, yb, 0); hx = qx; hy = qy; PJ(x2, yb, brH); LN(hx, hy, qx, qy); );
    u += st; u < brX1;
  );
  col(cBARV, 0.9);
  // DIVIDING LINE on the floor (an open door leaves a gap the listener can pass); plan: thicker with more damping
  // (a sign, not to scale) - door and window are drawn in their gap
  dz = planv ? 2.5 + 3.5 * min(1, bdamp / 60) : 2.6;
  (bt == 3 && (slider43 >= 40 || planv)) || (bt == 2 && planv) ? (
    PJ(brX0, yb, 0); hx = qx; hy = qy; PJ(bex0, yb, 0); LNW(hx, hy, qx, qy, dz);
    PJ(bex1, yb, 0); hx = qx; hy = qy; PJ(brX1, yb, 0); LNW(hx, hy, qx, qy, dz);
    !planv ? ( PJ(bex0, yb, 0); hx = qx; hy = qy; PJ(bex1, yb, 0); DL(hx, hy, qx, qy, 3, 3, 1); );
  ) : ( PJ(brX0, yb, 0); hx = qx; hy = qy; PJ(brX1, yb, 0); LNW(hx, hy, qx, qy, dz); );
  !planv ? (
    PJ(brX0, yb, brH); hx = qx; hy = qy; PJ(brX1, yb, brH); LNW(hx, hy, qx, qy, 1);
    PJ(brX0, yb, 0); hx = qx; hy = qy; PJ(brX0, yb, brH); LN(hx, hy, qx, qy);
    PJ(brX1, yb, 0); hx = qx; hy = qy; PJ(brX1, yb, brH); LN(hx, hy, qx, qy);
  );
  sg = sideS ? 1 : -1;                                            // leaf / sashes turn into the source's side
  bt == 3 ? (
    // door frame + leaf on the left hinge
    PJ(bex0, yb, 0); hx = qx; hy = qy; PJ(bex0, yb, bez1); LNW(hx, hy, qx, qy, 1.2);
    PJ(bex1, yb, 0); hx = qx; hy = qy; PJ(bex1, yb, bez1); LNW(hx, hy, qx, qy, 1.2);
    !planv ? ( PJ(bex0, yb, bez1); hx = qx; hy = qy; PJ(bex1, yb, bez1); LNW(hx, hy, qx, qy, 1.2); );
    lx2 = bex0 + bew * cos(bphi); ly2 = yb + sg * bew * sin(bphi);
    PJ(bex0, yb, 0); hx = qx; hy = qy; PJ(lx2, ly2, 0); x1 = qx; x2 = qy; PJ(lx2, ly2, bez1); u = qx; st = qy; PJ(bex0, yb, bez1);
    col(cBARV, 0.45); POLY4(hx, hy, x1, x2, u, st, qx, qy);
    col(cBARV, 0.95); LNW(hx, hy, x1, x2, 1); LNW(x1, x2, u, st, 1.2); LNW(u, st, qx, qy, 1);
    planv ? (                                                      // plan: the leaf as a solid line, its swing dashed
      col(cBARV, 0.75); u = 0;
      loop(14, (u & 1) == 0 ? ( PJ(bex0 + bew * cos(bphi * u / 14), yb + sg * bew * sin(bphi * u / 14), 0); hx = qx; hy = qy;
                                PJ(bex0 + bew * cos(bphi * (u + 1) / 14), yb + sg * bew * sin(bphi * (u + 1) / 14), 0); LNW(hx, hy, qx, qy, 1.2); ); u += 1; );
      col(cBARV, 1); PJ(bex0, yb, 0); hx = qx; hy = qy; PJ(lx2, ly2, 0); LNW(hx, hy, qx, qy, 3);
    );
  );
  bt == 2 ? (
    // WINDOW: frame (blue) + two casement sashes, hinged at the left and the right, opening into the source's side
    col(cGLS, 0.9);
    !planv ? (
      PJ(bex0, yb, bez0); hx = qx; hy = qy; PJ(bex1, yb, bez0); LNW(hx, hy, qx, qy, 1.2);
      PJ(bex0, yb, bez1); hx = qx; hy = qy; PJ(bex1, yb, bez1); LNW(hx, hy, qx, qy, 1.2);
      PJ(bex0, yb, bez0); hx = qx; hy = qy; PJ(bex0, yb, bez1); LNW(hx, hy, qx, qy, 1.2);
      PJ(bex1, yb, bez0); hx = qx; hy = qy; PJ(bex1, yb, bez1); LNW(hx, hy, qx, qy, 1.2);
    ) : ( col(cGLS, 0.45); PJ(bex0, yb, 0); hx = qx; hy = qy; PJ(bex1, yb, 0); LNW(hx, hy, qx, qy, 1); );   // plan: the sill
    hw = bew / 2; cs = cos(bphi); sn = sg * sin(bphi); k = 0;
    loop(2,
      ax = k == 0 ? bex0 : bex1; x1 = k == 0 ? 1 : -1;            // hinge, direction to the middle
      lx2 = ax + x1 * hw * cs; ly2 = yb + hw * sn;
      PJ(ax, yb, bez0); hx = qx; hy = qy; PJ(lx2, ly2, bez0); x2 = qx; zt = qy; PJ(lx2, ly2, bez1); u = qx; st = qy; PJ(ax, yb, bez1);
      col(cGLS, 0.20); POLY4(hx, hy, x2, zt, u, st, qx, qy);
      col(cGLS, 0.9); LNW(hx, hy, x2, zt, 1.2); LNW(x2, zt, u, st, 1.2); LNW(u, st, qx, qy, 1.2);
      planv ? (                                                    // plan: each sash a solid line, its swing dashed
        col(cGLS, 0.7); u = 0;
        loop(10, (u & 1) == 0 ? ( PJ(ax + x1 * hw * cos(bphi * u / 10), yb + sg * hw * sin(bphi * u / 10), 0); hx = qx; hy = qy;
                                  PJ(ax + x1 * hw * cos(bphi * (u + 1) / 10), yb + sg * hw * sin(bphi * (u + 1) / 10), 0); LNW(hx, hy, qx, qy, 1.2); ); u += 1; );
        col(cGLS, 1); PJ(ax, yb, 0); hx = qx; hy = qy; PJ(lx2, ly2, 0); LNW(hx, hy, qx, qy, 3);
      );
      k += 1;
    );
  );
  // handles: depth (top edge, right) and side (top of the element, or of a partition narrower than the room)
  PJ(brHX, yb, brH); col(cSURF, 1); CI(qx, qy, 4.5, 1); col(cBARV, 1); CI(qx, qy, 4.5, 0); CI(qx, qy, 1.5, 1);
  brEon ? ( PJ(brEX, yb, brEZ); col(cSURF, 1); CI(qx, qy - 6, 3.5, 1); col(cBARV, 1); CI(qx, qy - 6, 3.5, 0); );
  // padlocks beside the handles of what is locked
  lockB ? ( PJ(brHX, yb, brH); drawlock(qx + 14, qy, cBARTT); );
  lockE && brEon ? ( PJ(brEX, yb, brEZ); drawlock(qx + 13, qy - 6, cELK); );
);

// LOAD A ROOM PRESET: the room as defined, source and listener on the center line (no left / right tilt), listener
// facing the back wall, no barrier. "Keep" options in the menu: vopt[16] = keep source + listener (and facing),
// vopt[17] = keep the barrier - both are fitted into the new room.
// KEEP: a position that fits into the new room stays where it is (in meters); one that does not keeps its place relative
// to the room (so two of them never end up squeezed into the same corner)
function keepfit(a, ol, nl) ( a >= 0.1 && a <= nl - 0.1 ? a : min(max(a / max(0.1, ol) * nl, 0.1), nl - 0.1); );
// the standard arrangement of source and listener in a room (slider units: before the size factor rs)
// listener in the middle, source 2 m in front of it towards the back wall (in a small room: near the back wall) - same rule as Reset positions
function pdefsy(d, r) ( max(d / 2 - 2 / r, d * 0.05); );
// Loads room preset k: takes an undo snapshot, stops motion, sets room size, walls, reverb sliders and re-places source/listener (keeping default layout if untouched).
function loadroom(k) local(b, ors, ow, od, oh, nw, nd, nh, isdef)
(
  scnlast = -1;
  b = PT + k * PRS; snap_take(); mtE > 0.001 || slider100 >= 0.5 ? ( mtKill = 1; slider100 = 0; );   // a running motion ends (the new room has new places)
  lhE0 = eqE; lhT = time_precise(); lhPre = rsel; lhNew = k; lhDb = -1000;   // the loudness hint (level of the new room against the one before)
  ors = slider31; ow = slider2 * ors; od = slider3 * ors; oh = slider4 * ors; nw = b[0] * b[14]; nd = b[1] * b[14]; nh = b[2] * b[14];
  // untouched (still the standard arrangement of the old room)? Then the new room gets its own standard arrangement -
  // source and listener stay in the same place RELATIVE to the room (middle, 2 m apart), whatever preset is loaded
  isdef = (abs(slider13 - (ow / ors) / 2) < 0.02 && abs(slider14 - (od / ors) / 2) < 0.02 && abs(slider10 - (ow / ors) / 2) < 0.02
       && (abs(slider11 - pdefsy(od / ors, ors)) < 0.02 || abs(slider11 - max((od / ors) / 2 - 2 / ors, (od / ors) * 0.15)) < 0.02) && abs(slider12 - slider15) < 0.02 && slider16 >= 0.5 && abs(slider17) < 0.5 && abs(slider61) < 0.5);
  slider1 = 0; slider2 = b[0]; slider3 = b[1]; slider4 = b[2]; slider36 = 0;    // room engine on
  slider5 = b[3]; slider6 = b[4]; slider7 = b[5]; slider8 = b[6]; slider9 = b[7];   // walls back / right / front / left, floor + ceiling
  ((basic || (vopt[16] >= 0.5 && isdef)) && !lockS && !lockL) ? (   // Basic: every preset starts with source and listener at the standard places
    slider13 = b[0] / 2; slider14 = b[1] / 2; slider10 = b[0] / 2; slider11 = pdefsy(b[1], b[14]);
    basic ? ( slider15 = min(max(1.5, 0.1), nh - 0.1); slider16 = 1; slider17 = 0; slider63 = 1; slider61 = 0; ) : slider15 = min(max(slider15, 0.1), nh - 0.1);
    slider12 = slider15;
  ) : (vopt[16] >= 0.5) ? (
    // KEEP (the default): source and listener stay where they are - only moved inside if the new room is smaller
    slider10 = keepfit(slider10 * ors, ow, nw) / b[14]; slider11 = keepfit(slider11 * ors, od, nd) / b[14]; slider12 = keepfit(slider12, oh, nh);
    slider13 = keepfit(slider13 * ors, ow, nw) / b[14]; slider14 = keepfit(slider14 * ors, od, nd) / b[14]; slider15 = keepfit(slider15, oh, nh);
  ) : (
    slider10 = b[8]; slider11 = b[9]; slider12 = b[10]; slider13 = b[11]; slider14 = b[12]; slider15 = b[13];
    slider16 = 1; slider17 = 0;                                                   // facing: fixed, towards the back wall
    slider63 = 1; slider61 = abs(b[11] - b[8]) + abs(b[12] - b[9]) > 0.01 ? atan2(-(b[11] - b[8]), b[12] - b[9]) * 180 / $pi : 0;   // source aims at the listener
  );
  vopt[17] >= 0.5 ? (
    slider42 = min(max(slider42, 0.3), b[1] - 0.3); slider45 = min(max(slider45, 0), b[0]);
  ) : (
    slider41 = 0; slider42 = b[1] / 2; slider43 = 0; slider44 = 25; slider45 = b[0] / 2; slider46 = 0; slider95 = 0; slider96 = 0; slider97 = 100;   // no barrier
  );
  slider19 = 1; slider31 = b[14]; slider34 = b[15]; slider33 = b[16]; slider20 = b[17]; slider21 = b[18]; slider38 = b[19]; selw = 0;
  vopt[8] >= 0.5 ? ( slider34 = pdroom(); );       // predelay follows the room: from its size
  slider32 = 1; slider62 = 100;                    // DECAY and REFLECTIVITY back: the menu shows the room's own reverb time
  snap_notify();
);
// Returns true if the current room size, wall absorptions and scale sliders equal those of preset k (within 0.001).
function roommatch(k) local(b)
(
  b = PT + k * PRS;
  abs(slider2 - b[0]) < 0.001 && abs(slider3 - b[1]) < 0.001 && abs(slider4 - b[2]) < 0.001 &&
  abs(slider5 - b[3]) < 0.001 && abs(slider6 - b[4]) < 0.001 && abs(slider7 - b[5]) < 0.001 &&
  abs(slider8 - b[6]) < 0.001 && abs(slider9 - b[7]) < 0.001 && abs(slider31 - b[14]) < 0.001;
);
// Estimates preset k's RT60 in seconds (Eyring with air absorption), clamped to 0.08..20.
function presetrt(k) local(b, w, d, h, v, a)
(
  b = PT + k * PRS; w = b[0] * b[14]; d = b[1] * b[14]; h = b[2] * b[14];
  v = w * d * h; a = w * h * (b[3] + b[5]) + d * h * (b[4] + b[6]) + 2 * w * d * b[7];
  a = -2 * (w * d + w * h + d * h) * log(1 - min(0.95, a / (2 * (w * d + w * h + d * h))));   // Eyring
  min(20, max(0.08, 0.161 * v / (a + 4 * airm5 * v)));
);

// ---- SCENARIO presets: situations from film mixing - source, room, barrier and delay in one click (menu page 3) ----
NSC = 33; scnX = alloc(NSC); scnY = alloc(NSC); scnH = alloc(NSC); scnlast = -1; lkT = -100;
// Returns the display name of scenario preset k.
function scname(k) ( k == 0 ? "Party next door" : (k == 1 ? "Neighbor's TV" : (k == 2 ? "Argument behind the wall" : (k == 3 ? "Club thump from the street" : (k == 4 ? "Outside the door" : (k == 5 ? "Door ajar" : (k == 6 ? "TV in the next room" : (k == 7 ? "Voice from the cellar" : (k == 8 ? "Street through the window" : (k == 9 ? "Window tilted open" : (k == 10 ? "In the woods" : (k == 11 ? "Canyon shout" : (k == 12 ? "Gunshot in the city" : (k == 13 ? "Snowfield hush" : (k == 14 ? "Station announcement" : (k == 15 ? "Stadium PA" : (k == 16 ? "Phone call in the stairwell" : (k == 17 ? "Radio in the tunnel" : (k == 18 ? "Car radio" : (k == 19 ? "Car next to you" : (k == 20 ? "Hands-free call in the car" : (k == 21 ? "Taxi dispatch radio" : (k == 22 ? "Bus announcement" : (k == 23 ? "Radio in the bathroom" : (k == 24 ? "Elevator speaker" : (k == 25 ? "Hiding in the closet" : (k == 26 ? "Call from the bathroom" : (k == 27 ? "Door intercom" : (k == 28 ? "Headphone leak on the train" : (k == 29 ? "Video call at the desk" : (k == 30 ? "Boombox in the park" : (k == 31 ? "Megaphone in the street" : (k == 32 ? "Radio in the garage" : (""))))))))))))))))))))))))))))))))); );
// Returns a short label (e.g. insulation value) for scenario preset k.
function scshort(k) ( k == 0 ? "wall 45 dB" : (k == 1 ? "wall 38 dB" : (k == 2 ? "wall 42 dB" : (k == 3 ? "concrete" : (k == 4 ? "door 25 dB" : (k == 5 ? "door 12 %" : (k == 6 ? "door 35 %" : (k == 7 ? "door + echo" : (k == 8 ? "glass 30 dB" : (k == 9 ? "glass 20 %" : (k == 10 ? "free field" : (k == 11 ? "free field" : (k == 12 ? "free field" : (k == 13 ? "free field" : (k == 14 ? "PA + echo" : (k == 15 ? "music + echo" : (k == 16 ? "mobile call" : (k == 17 ? "walkie + echo" : (k == 18 ? "radio" : (k == 19 ? "glass 24 dB" : (k == 20 ? "speakerphone" : (k == 21 ? "radio + echo" : (k == 22 ? "PA + echo" : (k == 23 ? "bluetooth" : (k == 24 ? "small box" : (k == 25 ? "door 17 dB" : (k == 26 ? "phone + echo" : (k == 27 ? "intercom" : (k == 28 ? "leak" : (k == 29 ? "video call" : (k == 30 ? "free field" : (k == 31 ? "free field + echo" : (k == 32 ? "radio + echo" : (""))))))))))))))))))))))))))))))))); );
// Returns a one-line description of what scenario preset k sounds like.
function scdesc(k) ( k == 0 ? "bass thump through a brick wall, the highs are gone: the lows carry the party" : (k == 1 ? "dialog and music through drywall: muffled, but still half intelligible" : (k == 2 ? "raised voices through a wall: mostly vowels and rhythm get through" : (k == 3 ? "kick and bass through 20 cm concrete, with a faint slapback from the alley" : (k == 4 ? "voice behind a closed wooden door: dull and quiet, your own room tone stays" : (k == 5 ? "the same door open a crack: a narrow gap lets the highs back in" : (k == 6 ? "TV through a half-open door: boxy, indirect, a little room around it" : (k == 7 ? "a call from a concrete cellar through a half-open door, a trace of cave echo" : (k == 8 ? "traffic and voices through a closed double-glazed window" : (k == 9 ? "the window tilted open: the street gets brighter and louder" : (k == 10 ? "a voice among trees: soft ground, no walls, a faint tree-line echo" : (k == 11 ? "a shout across a valley with a late, hard echo from the rock" : (k == 12 ? "a shot between building rows: hard ground and a slap from the facades" : (k == 13 ? "fresh snow swallows everything: dry, very quiet, no reflections" : (k == 14 ? "tiled platform: a PA voice with a long underground echo" : (k == 15 ? "the stadium music system at one end of a huge stadium, you far up in the stands: fat sub bass and full range, the direct sound arrives late, dense long echoes from the stands, a very big tail" : (k == 16 ? "a mobile call in a hard stairwell: narrow band, bright room" : (k == 17 ? "walkie-talkie voice in a tunnel with a radio echo" : (k == 18 ? "the radio from the dashboard speakers in a small, damped car cabin" : (k == 19 ? "the boombox of the car at the traffic light: bass through the side window" : (k == 20 ? "a call over the car speakers: narrow band, the cabin around it" : (k == 21 ? "a scratchy dispatch radio in the front, a short comms echo" : (k == 22 ? "a stop announcement in a long bus: PA voice and a little hall" : (k == 23 ? "a small speaker on the sink: tiles, bright and boomy" : (k == 24 ? "thin ceiling speaker in a tiny steel cabin, a short echo" : (k == 25 ? "you are in a tiny room, the voices outside behind a thin door" : (k == 26 ? "a phone call in a tiled room: line echo on top of the tile reverb" : (k == 27 ? "a voice from the intercom in a hallway, a faint echo" : (k == 28 ? "thin, distant sound leaking from earbuds in a train compartment" : (k == 29 ? "a laptop call in a small office: thin voice, close room" : (k == 30 ? "a boombox on the grass, 20 m away: open air, soft ground" : (k == 31 ? "a megaphone between facades: hard ground, slapback from the walls" : (k == 32 ? "an old radio on the workbench: concrete garage, a short slapback" : (""))))))))))))))))))))))))))))))))); );
// Returns the category index (0-7) of scenario preset k.
function sccat(k) ( k < 4 ? 0 : (k < 8 ? 1 : (k < 10 ? 2 : (k < 14 ? 3 : (k < 18 ? 4 : (k < 23 ? 5 : (k < 28 ? 6 : 7)))))); );
// Returns the name of scenario category c.
function sccatname(c) ( c == 0 ? "Through walls" : (c == 1 ? "Doors" : (c == 2 ? "Windows" : (c == 3 ? "Outdoors" : (c == 4 ? "Spaces & comms" : (c == 5 ? "Vehicles" : (c == 6 ? "Small rooms" : "Devices & media")))))); );
// Returns 1 if scenario category c is shown in the right menu column (categories 3, 4 and 6+), else 0.
function sccol(c) ( c == 3 || c == 4 || c >= 6; );

// Resets the scenario-controlled sliders to neutral (reflectors cleared, device/delay/filter/EQ bands and motion off, dry-wet and levels to 100).
function scnbase() local(i)
(
  rflclr(); sset(41, 0); sset(43, 0); sset(65, 0); sset(90, 100); sset(117, 0); sset(119, 0); sset(37, 0); sset(35, 0); sset(85, 1); sset(89, 100); sset(98, 100);
  i = 47; loop(13, sset(i, 0); i += 1; );
  sset(111, 0); sset(112, 0); sset(113, 0); sset(114, 0);
);
// a barrier between a source behind it (sy) and the listener in front (ly): positions as fractions of the room depth, both on the center line
function scbar(t, dmp, pos, op, sy, ly) local(hh)
(
  hh = min(1.5, slider4 * 0.6);
  sset(41, t); sset(44, dmp); sset(42, pos * slider3); sset(43, op); sset(45, slider2 / 2); sset(46, 0); sset(95, 0); sset(96, 0); sset(97, 100);
  sset(10, slider2 / 2); sset(11, sy * slider3); sset(13, slider2 / 2); sset(14, ly * slider3); sset(12, hh); sset(15, hh);
  sset(16, 1); sset(17, 0); sset(63, 1); sset(61, 0);
);
// open air: 40 x 40 m area, source and listener on the center line (meters), ground and distance perspective
function scff(gr, dist, sy, ly)
(
  sset(1, 1); sset(2, 40); sset(3, 40); sset(4, 12); sset(119, gr); sset(117, dist);
  sset(10, 20); sset(11, sy); sset(12, 1.5); sset(13, 20); sset(14, ly); sset(15, 1.5);
  sset(16, 1); sset(17, 0); sset(63, 1); sset(61, 0);
);
// the places of a scene without a barrier: source and listener on the center line (fractions of the room depth)
function scpos(sy, ly) ( sset(10, slider2 / 2); sset(11, sy * slider3); sset(13, slider2 / 2); sset(14, ly * slider3); sset(16, 1); sset(17, 0); sset(63, 1); sset(61, 0); );
// a reflector of a free-field scenario (r = 0 / 1; material 1 Facade 2 Glass 3 Rock 4 Fence 5 Hedge; X, Y, length, height in meters, angle in degrees; the reflector level is set to +12 dB)
function scrf(r, m, x, y, a, l, h) local(o) ( vopt[RFB + 12 + r] = 12; o = RFB + r * 6; vopt[o] = m; vopt[o + 1] = x; vopt[o + 2] = y; vopt[o + 3] = a; vopt[o + 4] = l; vopt[o + 5] = h; );
// Loads scenario preset k: picks its room, source model, delay preset and mix via lookup tables, then applies the scenario's barrier, free-field and position setup.
function scnload(k) local(rm, sp, dpk, dmx, pre)
(
  pre = scnlast >= 0 ? -2 - scnlast : rsel;   // what plays now (a scenario or a room) - for the loudness hint
  // lookup tables per scenario k: rm = room preset, sp = source model (-1 = none), dpk = delay preset (-1 = none), dmx = delay mix % (100 = default)
  rm = k == 0 ? 3 : (k == 2 ? 8 : (k == 3 ? 35 : (k == 7 || k == 16 ? 5 : (k == 14 ? 29 : (k == 15 ? 30 : (k == 17 ? 29 : (k >= 18 && k <= 21 ? 13 : (k == 22 ? 14 : (k == 23 || k == 26 ? 0 : (k == 24 ? 15 : (k == 25 ? 31 : (k == 27 ? 4 : (k == 28 ? 41 : (k == 29 ? 37 : (k == 32 ? 7 : 2)))))))))))))));
  sp = k == 14 || k == 15 || k == 22 ? 11 : (k == 16 || k == 26 ? 2 : (k == 17 ? 8 : (k == 18 ? 21 : (k == 32 ? 14 : (k == 20 ? 4 : (k == 21 ? 9 : (k == 23 ? 19 : (k == 24 ? 17 : (k == 27 ? 12 : (k == 28 ? 20 : (k == 29 ? 6 : (k == 30 ? 15 : (k == 31 ? 10 : -1)))))))))))));
  k == 15 ? sp = 22;   // Stadium PA: the stadium MUSIC system (fat), not the thin announcement speaker
  dpk = k == 3 ? 0 : (k == 7 ? 9 : (k == 10 ? 8 : (k == 11 ? 10 : (k == 12 ? 2 : (k == 14 ? 3 : (k == 15 ? 4 : (k == 17 ? 6 : (k == 21 ? 6 : (k == 22 ? 5 : (k == 24 ? 5 : (k == 26 ? 7 : (k == 27 ? 6 : (k == 31 || k == 32 ? 0 : -1)))))))))))));
  dmx = k == 3 ? 35 : (k == 7 ? 30 : (k == 17 ? 60 : (k == 21 ? 40 : (k == 22 ? 50 : (k == 24 ? 30 : (k == 26 ? 45 : (k == 27 ? 30 : (k == 31 ? 60 : (k == 32 ? 30 : 100)))))))));
  loadroom(rm);
  lhNew = -2 - k; lhPre = pre;   // the hint names the scenario
  snap_take(); scnbase();
  k == 0 ? ( scbar(1, bmatrw(1, 1), 0.5, 0, 0.2, 0.8); sset(35, 6); sset(37, 25); );              // brick wall, boombox
  k == 1 ? ( scbar(1, bmatrw(1, 0), 0.5, 0, 0.2, 0.8); sset(35, 7); sset(37, 25); );              // drywall, TV
  k == 2 ? ( scbar(1, 42, 0.5, 0, 0.2, 0.8); );                                                   // a wall, the plain voice
  k == 3 ? ( scbar(1, bmatrw(1, 2), 0.5, 0, 0.2, 0.8); sset(35, 6); sset(37, 35); );              // concrete, boombox
  k == 4 ? ( scbar(3, bmatrw(3, 1), 0.4, 0, 0.1, 0.75); );                                        // heavy wood door, closed
  k == 5 ? ( scbar(3, bmatrw(3, 1), 0.4, 12, 0.1, 0.75); );                                       // ... open a crack
  k == 6 ? ( scbar(3, bmatrw(3, 1), 0.4, 35, 0.1, 0.75); sset(35, 7); sset(37, 20); );            // half open, TV
  k == 7 ? ( scbar(3, bmatrw(3, 1), 0.4, 25, 0.1, 0.8); sset(46, 1); );                           // concrete back part = the cellar
  k == 8 ? ( scbar(2, bmatrw(2, 1), 0.4, 0, 0.1, 0.75); );                                        // double glazing, closed
  k == 9 ? ( scbar(2, bmatrw(2, 1), 0.4, 20, 0.1, 0.75); );                                       // tilted open
  k == 10 ? ( scff(2, 60, 6, 26); scrf(0, 5, 31, 16, 90, 30, 8); );                                                              // grass
  k == 11 ? ( scff(1, 70, 4, 30); scrf(0, 3, 20, 39, 0, 38, 30); );                                                              // rock: hard ground
  k == 12 ? ( scff(1, 45, 10, 28); scrf(0, 1, 30, 19, 90, 30, 20); scrf(1, 1, 10, 19, 90, 30, 20); );   // facades left and right
  k == 13 ? ( scff(3, 65, 8, 28); );                                                              // snow
  k == 15 ? ( scpos(0.1, 0.55); sset(20, 3); sset(21, 4); );   // the Stadium room: the PA tower at one end, you far away in the stands
  k == 14 || k == 16 || k == 17 ? ( scpos(0.15, 0.75); );
  k >= 18 && k <= 22 ? ( scpos(0.1, 0.6); );                                                      // vehicles: the speakers in front, the listener behind
  k == 19 ? ( scbar(2, bmatrw(2, 0), 0.3, 0, 0.1, 0.7); sset(35, 6); sset(37, 30); );             // single glazing, the boombox of the next car
  k == 23 || k == 26 || k == 27 || k == 28 || k == 29 || k == 32 ? ( scpos(0.2, 0.7); );
  k == 24 ? ( scpos(0.5, 0.5); sset(12, slider4 * 0.9); sset(11, 0.45 * slider3); sset(14, 0.6 * slider3); );   // speaker in the ceiling
  k == 25 ? ( scbar(3, bmatrw(3, 0), 0.35, 0, 0.1, 0.8); );                                        // thin door, you inside the small room
  k == 30 ? ( scff(2, 50, 8, 28); );
  k == 31 ? ( scff(1, 40, 8, 26); scrf(0, 1, 27, 17, 90, 30, 14); scrf(1, 1, 13, 17, 90, 30, 14); );   // the street canyon
  snap_notify();
  sp >= 0 ? spapply(sp);
  dpk >= 0 ? ( dpload(dpk); dmx < 100 ? ( snap_take(); sset(90, dmx); snap_notify(); ); );
  scnlast = k;   // the loudness hint (with the Equal loudness chip) comes as with a room preset
);
NFF = 17; FFP = alloc(16 * NFF);   // free field presets: area W, D, max height, ground, reflector 1 and 2 (material, X, Y, angle, length, height)
FFP[0] = 20; FFP[1] = 30; FFP[2] = 8; FFP[3] = 1; FFP[4] = 1; FFP[5] = 0; FFP[6] = 15; FFP[7] = 90; FFP[8] = 30; FFP[9] = 4; FFP[10] = 1; FFP[11] = 20; FFP[12] = 15; FFP[13] = 90; FFP[14] = 30; FFP[15] = 4;
FFP[16] = 6; FFP[17] = 30; FFP[18] = 15; FFP[19] = 1; FFP[20] = 1; FFP[21] = 0; FFP[22] = 15; FFP[23] = 90; FFP[24] = 30; FFP[25] = 15; FFP[26] = 1; FFP[27] = 6; FFP[28] = 15; FFP[29] = 90; FFP[30] = 30; FFP[31] = 15;
FFP[32] = 24; FFP[33] = 40; FFP[34] = 15; FFP[35] = 1; FFP[36] = 1; FFP[37] = 0; FFP[38] = 20; FFP[39] = 90; FFP[40] = 40; FFP[41] = 22; FFP[42] = 1; FFP[43] = 24; FFP[44] = 20; FFP[45] = 90; FFP[46] = 40; FFP[47] = 22;
FFP[48] = 40; FFP[49] = 40; FFP[50] = 14; FFP[51] = 1; FFP[52] = 1; FFP[53] = 0; FFP[54] = 20; FFP[55] = 90; FFP[56] = 40; FFP[57] = 12; FFP[58] = 1; FFP[59] = 40; FFP[60] = 20; FFP[61] = 90; FFP[62] = 40; FFP[63] = 12;
FFP[64] = 70; FFP[65] = 100; FFP[66] = 10; FFP[67] = 2; FFP[68] = 1; FFP[69] = 35; FFP[70] = 99; FFP[71] = 0; FFP[72] = 60; FFP[73] = 12; FFP[74] = 0; FFP[75] = 0; FFP[76] = 0; FFP[77] = 0; FFP[78] = 0; FFP[79] = 0;
FFP[80] = 40; FFP[81] = 40; FFP[82] = 8; FFP[83] = 2; FFP[84] = 0; FFP[85] = 0; FFP[86] = 0; FFP[87] = 0; FFP[88] = 0; FFP[89] = 0; FFP[90] = 0; FFP[91] = 0; FFP[92] = 0; FFP[93] = 0; FFP[94] = 0; FFP[95] = 0;
FFP[96] = 40; FFP[97] = 40; FFP[98] = 8; FFP[99] = 1; FFP[100] = 1; FFP[101] = 20; FFP[102] = 1; FFP[103] = 0; FFP[104] = 40; FFP[105] = 9; FFP[106] = 4; FFP[107] = 39; FFP[108] = 20; FFP[109] = 90; FFP[110] = 40; FFP[111] = 1.8;
FFP[112] = 10; FFP[113] = 40; FFP[114] = 10; FFP[115] = 1; FFP[116] = 5; FFP[117] = 0; FFP[118] = 20; FFP[119] = 90; FFP[120] = 40; FFP[121] = 10; FFP[122] = 5; FFP[123] = 10; FFP[124] = 20; FFP[125] = 90; FFP[126] = 40; FFP[127] = 10;
FFP[128] = 40; FFP[129] = 40; FFP[130] = 15; FFP[131] = 1; FFP[132] = 3; FFP[133] = 20; FFP[134] = 38; FFP[135] = 0; FFP[136] = 60; FFP[137] = 50; FFP[138] = 0; FFP[139] = 0; FFP[140] = 0; FFP[141] = 0; FFP[142] = 0; FFP[143] = 0;
FFP[144] = 30; FFP[145] = 40; FFP[146] = 15; FFP[147] = 1; FFP[148] = 3; FFP[149] = 0; FFP[150] = 20; FFP[151] = 90; FFP[152] = 40; FFP[153] = 40; FFP[154] = 3; FFP[155] = 30; FFP[156] = 20; FFP[157] = 90; FFP[158] = 40; FFP[159] = 40;
FFP[160] = 20; FFP[161] = 20; FFP[162] = 10; FFP[163] = 1; FFP[164] = 1; FFP[165] = 0; FFP[166] = 10; FFP[167] = 90; FFP[168] = 20; FFP[169] = 10; FFP[170] = 1; FFP[171] = 10; FFP[172] = 0; FFP[173] = 0; FFP[174] = 20; FFP[175] = 10;
FFP[176] = 16; FFP[177] = 30; FFP[178] = 8; FFP[179] = 1; FFP[180] = 2; FFP[181] = 0; FFP[182] = 15; FFP[183] = 90; FFP[184] = 30; FFP[185] = 6; FFP[186] = 2; FFP[187] = 16; FFP[188] = 15; FFP[189] = 90; FFP[190] = 30; FFP[191] = 6;
FFP[192] = 20; FFP[193] = 20; FFP[194] = 6; FFP[195] = 2; FFP[196] = 4; FFP[197] = 0; FFP[198] = 10; FFP[199] = 90; FFP[200] = 20; FFP[201] = 1.8; FFP[202] = 5; FFP[203] = 20; FFP[204] = 10; FFP[205] = 90; FFP[206] = 20; FFP[207] = 2.2;
FFP[208] = 40; FFP[209] = 40; FFP[210] = 8; FFP[211] = 3; FFP[212] = 0; FFP[213] = 0; FFP[214] = 0; FFP[215] = 0; FFP[216] = 0; FFP[217] = 0; FFP[218] = 0; FFP[219] = 0; FFP[220] = 0; FFP[221] = 0; FFP[222] = 0; FFP[223] = 0;
FFP[224] = 15; FFP[225] = 15; FFP[226] = 6; FFP[227] = 1; FFP[228] = 1; FFP[229] = 0; FFP[230] = 7.5; FFP[231] = 90; FFP[232] = 15; FFP[233] = 1.1; FFP[234] = 1; FFP[235] = 7.5; FFP[236] = 0; FFP[237] = 0; FFP[238] = 15; FFP[239] = 1.1;
FFP[240] = 30; FFP[241] = 50; FFP[242] = 8; FFP[243] = 1; FFP[244] = 1; FFP[245] = 30; FFP[246] = 25; FFP[247] = 90; FFP[248] = 50; FFP[249] = 4; FFP[250] = 0; FFP[251] = 0; FFP[252] = 0; FFP[253] = 0; FFP[254] = 0; FFP[255] = 0;
FFP[256] = 30; FFP[257] = 40; FFP[258] = 10; FFP[259] = 1; FFP[260] = 1; FFP[261] = 15; FFP[262] = 39; FFP[263] = 0; FFP[264] = 30; FFP[265] = 10; FFP[266] = 0; FFP[267] = 0; FFP[268] = 0; FFP[269] = 0; FFP[270] = 0; FFP[271] = 0;
// Returns the name of free-field place preset k.
function ffpname(k) ( (k == 0 ? "Street (default)" : (k == 1 ? "Narrow alley" : (k == 2 ? "City street canyon" : (k == 3 ? "Boulevard" : (k == 4 ? "Soccer field" : (k == 5 ? "Meadow" : (k == 6 ? "Parking lot" : (k == 7 ? "Forest road" : (k == 8 ? "Mountain wall" : (k == 9 ? "Canyon" : (k == 10 ? "Courtyard corner" : (k == 11 ? "Shopping street" : (k == 12 ? "Garden" : (k == 13 ? "Snow field" : (k == 14 ? "Rooftop" : (k == 15 ? "Highway barrier" : "Harbour quay")))))))))))))))); );
// Returns the description of free-field place preset k.
function ffpdesc(k) ( (k == 0 ? "a street between low buildings: hard asphalt, a facade on each side (4 m high) - the sound runs along it and slaps back from both sides" : (k == 1 ? "a 6 m alley between tall walls: a dense, fast flutter between the facades" : (k == 2 ? "a wide street between high-rises: high facades, a late, bright wash" : (k == 3 ? "a broad avenue: the facades are far away, the echoes come late and soft" : (k == 4 ? "open grass, a stand behind the far goal: one late, soft echo" : (k == 5 ? "open grass, nothing around: only the soft ground" : (k == 6 ? "hard asphalt, a building behind you and a low fence at the side" : (k == 7 ? "a gravel road between dense trees: soft, dull reflections from both sides" : (k == 8 ? "open ground, a rock face far ahead: one hard, late echo" : (k == 9 ? "a narrow valley with rock on both sides: long, hard echoes" : (k == 10 ? "two facades meeting at a right angle: the sound comes back from two directions" : (k == 11 ? "glass shop fronts on both sides: bright, ringing reflections" : (k == 12 ? "grass between a wooden fence and a hedge: very little reflection" : (k == 13 ? "fresh snow, nothing around: dry and very quiet" : (k == 14 ? "a flat roof with low parapet walls: reflections only at low height" : (k == 15 ? "a concrete noise barrier along one side: a hard slap from the right" : "a hard quay in front of a warehouse wall: one wide, hard echo")))))))))))))))); );
// the Delay module helps the sound of every place: delay preset (-1 = delay off) and its mix %
FFD = alloc(40);
FFD[0] = 2; FFD[1] = 30;    FFD[2] = 0; FFD[3] = 45;    FFD[4] = 2; FFD[5] = 45;    FFD[6] = 2; FFD[7] = 25;
FFD[8] = 4; FFD[9] = 30;    FFD[10] = -1; FFD[11] = 0;  FFD[12] = 2; FFD[13] = 20;  FFD[14] = 8; FFD[15] = 40;
FFD[16] = 11; FFD[17] = 50; FFD[18] = 10; FFD[19] = 60; FFD[20] = 1; FFD[21] = 45;  FFD[22] = 2; FFD[23] = 25;
FFD[24] = -1; FFD[25] = 0;  FFD[26] = -1; FFD[27] = 0;  FFD[28] = 0; FFD[29] = 20;  FFD[30] = 0; FFD[31] = 25;
FFD[32] = 0; FFD[33] = 30;
// Applies free-field place k from the FFP table: sets area size, ground, positions, reflectors and its delay preset, with an undo snapshot.
function ffpapply(k) local(b, sz1, i)
(
  b = FFP + k * 16; sz1 = max(1, ceil(max(b[0], b[1]) / 40 * 4) / 4);   // the Size factor that makes the area fit the 40 m sliders
  snap_take();
  mtE > 0.001 || slider100 >= 0.5 ? ( mtKill = 1; slider100 = 0; );
  sset(1, 1); sset(31, sz1); sset(2, b[0] / sz1); sset(3, b[1] / sz1); sset(4, max(0.8, min(15, b[2] / sz1)));
  sset(119, b[3]); sset(117, 0);
  sset(13, b[0] / sz1 / 2); sset(14, b[1] / sz1 / 2); sset(10, b[0] / sz1 / 2); sset(11, b[1] / sz1 / 2 - 1.5 / sz1);   // listener in the middle, the source 1.5 m in front of it (as in the room)
  sset(12, 1.5); sset(15, 1.5); sset(16, 1); sset(17, 0); sset(63, 1); sset(61, 0);
  i = 0; loop(12, vopt[RFB + i] = b[4 + i]; i += 1; ); vopt[RFB + 12] = vopt[RFB + 13] = 12;
  rflSel = 0; ffpLast = k;
  FFD[k * 2] >= 0 ? ( dpload(FFD[k * 2]); sset(90, FFD[k * 2 + 1]); ) : sset(65, 0);
  snap_notify();
);
// REFLECTOR SETUPS (Reflectors module, free field): typical placements around the listener and the source as they stand now -
// the area, the ground and the positions stay; only the two reflectors are set (inside the area). NRP setups, rpLast = the last one
NRP = 12; rpLast = -1; rpSnap = alloc(15);
function rpname(k) ( k == 0 ? "Facade behind the listener" : k == 1 ? "Facade behind the source" : k == 2 ? "House wall beside you" : k == 3 ? "Street canyon (both sides)" :
  k == 4 ? "Narrow alley" : k == 5 ? "Building corner" : k == 6 ? "Shop window" : k == 7 ? "Garden fence" : k == 8 ? "Hedge behind you" :
  k == 9 ? "Rock face far away (echo)" : k == 10 ? "Courtyard (two walls)" : "No reflectors"; );
function rpdesc(k) ( k == 0 ? "a facade 3 m behind the listener, parallel - a short slap from behind, the voice gets fuller" :
  k == 1 ? "a facade 3 m behind the source - the source sounds stronger, a short echo follows it" :
  k == 2 ? "a house wall 4 m to the left - one strong early reflection from the side, wider and closer" :
  k == 3 ? "facades 6 m to the left and right - the sound slaps between them (flutter), typical city street" :
  k == 4 ? "walls 1.8 m on both sides - a dense, fast flutter, close and hard" :
  k == 5 ? "a wall behind and one to the left (an L) - two early echoes from different sides" :
  k == 6 ? "a glass front 2.5 m to the right - bright, hard reflection from one side" :
  k == 7 ? "a 1.8 m fence 2 m to the left - a weak, low reflection, mostly highs" :
  k == 8 ? "a 2 m hedge 1.5 m behind - soft, damped, the space feels a little closer" :
  k == 9 ? "a high rock face at the far edge of the area in front - a clear, late echo (the larger the area, the later)" :
  k == 10 ? "two high walls 8 m behind and 8 m to the left - a courtyard, echoes from two sides" : "both reflectors off - only the direct sound and the ground"; );
// keeps a reflector center inside the area (0.3 m from its edge)
function rpcx(v) ( min(max(0.3, v), max(0.8, slider2) * min(4, max(0.25, slider31)) - 0.3); );
function rpcy(v) ( min(max(0.3, v), max(0.8, slider3) * min(4, max(0.25, slider31)) - 0.3); );
function rpapply(k) local(lxm, lym, sxm, sfy, i)
(
  snap_take();
  lxm = slider13 * min(4, max(0.25, slider31)); lym = slider14 * min(4, max(0.25, slider31)); sxm = slider10 * min(4, max(0.25, slider31)); sfy = slider11 * min(4, max(0.25, slider31));
  vopt[RFB] = 0; vopt[RFB + 6] = 0;   // (R2 stays off unless the setup uses it)
  k == 0 ? scrf(0, 1, rpcx(lxm), rpcy(lym + 3), 0, 12, 8);
  k == 1 ? scrf(0, 1, rpcx(sxm), rpcy(sfy - 3), 0, 12, 8);
  k == 2 ? scrf(0, 1, rpcx(lxm - 4), rpcy(lym), 90, 15, 9);
  k == 3 ? ( scrf(0, 1, rpcx(lxm - 6), rpcy(lym), 90, 40, 15); scrf(1, 1, rpcx(lxm + 6), rpcy(lym), 90, 40, 15); );
  k == 4 ? ( scrf(0, 1, rpcx(lxm - 1.8), rpcy(lym), 90, 25, 10); scrf(1, 1, rpcx(lxm + 1.8), rpcy(lym), 90, 25, 10); );
  k == 5 ? ( scrf(0, 1, rpcx(lxm), rpcy(lym + 3), 0, 10, 8); scrf(1, 1, rpcx(lxm - 4), rpcy(lym - 2), 90, 10, 8); );
  k == 6 ? scrf(0, 2, rpcx(lxm + 2.5), rpcy(lym), 90, 8, 3);
  k == 7 ? scrf(0, 4, rpcx(lxm - 2), rpcy(lym), 90, 15, 1.8);
  k == 8 ? scrf(0, 5, rpcx(lxm), rpcy(lym + 1.5), 0, 8, 2);
  k == 9 ? scrf(0, 3, rpcx(sxm), rpcy(0.3), 0, 40, 25);
  k == 10 ? ( scrf(0, 1, rpcx(lxm), rpcy(lym + 8), 0, 20, 12); scrf(1, 1, rpcx(lxm - 8), rpcy(lym), 90, 20, 12); );
  vopt[RFB + 14] = 0; rflSel = 0; rpLast = k;
  i = 0; loop(15, rpSnap[i] = vopt[RFB + i]; i += 1; );
  snap_notify();
);
// the setup shown on the menu button: the last one while nothing was changed by hand since, else "Custom"
function rpcur() local(i, eq) ( eq = rpLast >= 0; i = 0; loop(15, abs(vopt[RFB + i] - rpSnap[i]) > 0.0001 ? eq = 0; i += 1; ); eq ? rpLast : -1; );
// entering / leaving the free field with the mode switch: the room and the free-field setup are remembered (this session)
// rfLoad counts the project loads (written by @serialize alone); the audio thread and the GUI each keep their own copy (rfLoadA / rfLoadG)
ffRm = alloc(28); ffFf = alloc(28); dRfl = alloc(12); airOB = alloc(8); lastair = -1; WSA = alloc(2 * 2700); wsaLast = -1; rflSx0 = alloc(2); rflSy0 = alloc(2); rflSx1 = alloc(2); rflSy1 = alloc(2); rflLHv = alloc(2); rflLHx = alloc(2); rflLHy = alloc(2); rflHHv = alloc(2); rflHHx = alloc(2); rflHHy = alloc(2); rflLM = alloc(2); rflLM[0] = rflLM[1] = 1; rflLK = alloc(2); rflLK[0] = rflLK[1] = 1; rflHK = alloc(2); rflHK[0] = rflHK[1] = 1; rfSl = alloc(15); rfTch = alloc(15); memset(rfSl, -1000000000, 15); rfLoad = 0; rfLoadA = 0; rfLoadG = 0; ffpmX = alloc(20); ffpmY = alloc(20); ffRmOk = 0; ffFfOk = 0; ffpLast = 0;   // airOB = the air absorption of the 8 octave bands; lastair = -1: fill them again after a new @init; WSA: the still-air points of the waves view
// Stores the current room/free-field sliders (size, source / listener positions, distance, ground) and reflector options into array d.
function ffsave(d) local(i) ( d[0] = slider2; d[1] = slider3; d[2] = slider4; d[3] = slider31; d[4] = slider10; d[5] = slider11; d[6] = slider12; d[7] = slider13; d[8] = slider14; d[9] = slider15; d[10] = slider117; d[11] = slider119;
  i = 0; loop(12, d[12 + i] = vopt[RFB + i]; i += 1; ); d[24] = vopt[RFB + 12]; d[25] = vopt[RFB + 13]; );
// Restores room/free-field sliders and reflector options from array d (the inverse of ffsave).
function ffrest(d) local(i) ( sset(2, d[0]); sset(3, d[1]); sset(4, d[2]); sset(31, d[3]); sset(10, d[4]); sset(11, d[5]); sset(12, d[6]); sset(13, d[7]); sset(14, d[8]); sset(15, d[9]); sset(117, d[10]); sset(119, d[11]);
  i = 0; loop(12, vopt[RFB + i] = d[12 + i]; i += 1; ); vopt[RFB + 12] = d[24]; vopt[RFB + 13] = d[25]; );
// Switches Room/Free field mode (v >= 0.5 is free field), saving the old mode's setup and restoring the new mode's, then sets param 1.
function modeset(v)
(
  v >= 0.5 && !isfree ? ( snap_take(); ffsave(ffRm); ffRmOk = 1; rbyp ? sset(36, 0);   // the Room power off would silence the free-field controls
   ffFfOk ? ffrest(ffFf) : ffpapply(0); snap_notify(); )
  : ( v < 0.5 && isfree ? ( snap_take(); ffsave(ffFf); ffFfOk = 1; ffRmOk ? ffrest(ffRm); snap_notify(); ); );
  setp(1, v);
);
// categories (menu page 1: Household, Work & school, Vehicles left - Studio & media, Venues, Industrial & large right;
// page 2: Homes, Offices & schools, Trains, cars & trucks left - Basements & huts, Religious & caves, Effect rooms right)
function roomcat(k) ( k < 8 ? 0 : (k < 13 ? 1 : (k < 16 ? 2 : (k < 21 ? 3 : (k < 26 ? 4 : (k < 31 ? 5 :
                     (k < 37 ? 6 : (k < 41 ? 7 : (k < 47 ? 8 : (k < 51 ? 9 : (k < 56 ? 10 : 11)))))))))); );   // page 2: 6 … 11
// Returns the name of room category c (menu pages 1 and 2).
function roomcatname(c) ( c == 0 ? "Household" : (c == 1 ? "Work & school" : (c == 2 ? "Vehicles" : (c == 3 ? "Studio & media" : (c == 4 ? "Venues" : (c == 5 ? "Industrial & large" :
  (c == 6 ? "Homes" : (c == 7 ? "Offices & schools" : (c == 8 ? "Trains, cars & trucks" : (c == 9 ? "Basements & huts" : (c == 10 ? "Religious & caves" : "Effect rooms")))))))))); );
// Returns the name of built-in room preset k for the first block of presets.
function roomnameA(k) ( k == 0 ? "Bathroom" : (k == 1 ? "Kitchen" : (k == 2 ? "Living room" : (k == 3 ? "Bedroom" : (k == 4 ? "Hallway" : (k == 5 ? "Staircase" : (k == 6 ? "Cellar" : (k == 7 ? "Garage" : (k == 8 ? "Office" : (k == 9 ? "Open-plan office" : (k == 10 ? "Classroom" : (k == 11 ? "Lecture hall" : (k == 12 ? "Gym hall" : (k == 13 ? "Car" : (k == 14 ? "Bus" : (k == 15 ? "Elevator" : (k == 16 ? "Vocal booth" : (k == 17 ? "Control room" : (k == 18 ? "Studio" : (k == 19 ? "Live room" : (k == 20 ? "Cinema" : (k == 21 ? "Jazz club" : (k == 22 ? "Restaurant" : (k == 23 ? "Theater" : (k == 24 ? "Concert hall" : (k == 25 ? "Church" : (k == 26 ? "Warehouse" : (k == 27 ? "Factory hall" : (k == 28 ? "Car park" : (k == 29 ? "Tunnel" : ("Stadium")))))))))))))))))))))))))))))); );
// Returns the name of room preset k for the extended block (k = 31 and up).
function roomnameB(k) ( (k == 31 ? "WC" : (k == 32 ? "Small kitchen" : (k == 33 ? "Kitchen-diner" : (k == 34 ? "Pro kitchen" : (k == 35 ? "Cozy living room" : (k == 36 ? "Loft" : (k == 37 ? "Small office" : (k == 38 ? "Meeting room" : (k == 39 ? "Old classroom" : (k == 40 ? "Music classroom" : (k == 41 ? "Train compartment" : (k == 42 ? "Train coach" : (k == 43 ? "Small car" : (k == 44 ? "Empty van" : (k == 45 ? "Truck cab" : (k == 46 ? "Truck box" : (k == 47 ? "Concrete basement" : (k == 48 ? "Hobby basement" : (k == 49 ? "Wood hut" : (k == 50 ? "Sauna" : (k == 51 ? "Chapel" : (k == 52 ? "Village church" : (k == 53 ? "Cathedral" : (k == 54 ? "Small cave" : (k == 55 ? "Large cave" : (k == 56 ? "Metal tank" : (k == 57 ? "Concrete bunker" : (k == 58 ? "Tin shed" : (k == 59 ? "Glass box" : (k == 60 ? "Dead room" : "Drain pipe")))))))))))))))))))))))))))))); );
// Returns the name of room preset k, choosing roomnameA or roomnameB by index (NPG1 boundary).
function roomname(k) ( k < NPG1 ? roomnameA(k) : roomnameB(k); );
// Clamps an x position (unscaled units) to stay at least 0.1 m inside the room width.
function clampx(v) ( min(max(v, 0.1 / rs), (roomW - 0.1) / rs); );
// Clamps a y position (unscaled units) to stay at least 0.1 m inside the room depth.
function clampy(v) ( min(max(v, 0.1 / rs), (roomD - 0.1) / rs); );
function clampz(v) ( min(max(v, 0.1), roomH - 0.1); );   // heights: absolute meters
// THE LISTENER IS A PERSON: it cannot walk through walls. The room walls stop it, the barrier too - a wall and a window
// always (it gets as close as its body allows, 16 cm), a door only when it is open far enough (40 %) and the listener
// walks through the opening. Pressing against a wall: "Ouch!" (wcT). In: target in slider units, out: wcX, wcY.
function lis_walk(nx, ny) local(ox, oy, tx, ty, wr, wtyp, wyb, wym, wbw, wbx, wb0, wb1, pas, inb, hit)
(
  wr = 0.16; hit = 0;   // 16 cm - the head is 8 cm, the wall line 4; the Ouch came while there was still a gap
  ox = slider13 * rs; oy = slider14 * rs; tx = nx * rs; ty = ny * rs;
  // room walls (not in the free field: there the edge is only the end of the drawing)
  !isfree ? ( (tx < 0.1 - 0.06 || tx > roomW - 0.1 + 0.06 || ty < 0.1 - 0.06 || ty > roomD - 0.1 + 0.06) ? ( hit = 1; ); );
  tx = min(max(tx, 0.1), roomW - 0.1); ty = min(max(ty, 0.1), roomD - 0.1);
  wtyp = floor(slider41 + 0.5);
  wtyp > 0 ? (
    // barrier geometry in meters: wyb = depth of the barrier plane, wbw = opening width (door 0.9, window 1.2, wall none), wb0..wb1 = free passage between the frame posts (0.15 m inset on each side)
    wym = min(0.3, 0.25 * roomD); wyb = min(max(slider42 * rs, wym), roomD - wym);
    wbw = wtyp == 3 ? 0.9 : (wtyp == 2 ? 1.2 : 0); wbw = min(wbw, roomW - 0.1);
    wbx = min(max(slider45 * rs, wbw / 2 + 0.05), roomW - wbw / 2 - 0.05); wb0 = wbx - wbw / 2 + 0.15; wb1 = wbx + wbw / 2 - 0.15;
    pas = wtyp == 3 && slider43 >= 40;                                   // a door, open far enough for a person
    inb = abs(oy - wyb) < wr;                                            // standing in the doorway
    pas && inb ? (
      (tx < wb0 - 0.04 || tx > wb1 + 0.04) && abs(ty - wyb) < wr ? ( hit = 1; );   // the door frame
      abs(ty - wyb) < wr ? ( tx = min(max(tx, wb0), wb1); );
    ) : (
      !(pas && tx >= wb0 && tx <= wb1) ? (
        oy < wyb ? ( ty > wyb - wr + 0.06 ? ( hit = 1; ); ty = min(ty, wyb - wr); )
                 : ( ty < wyb + wr - 0.06 ? ( hit = 1; ); ty = max(ty, wyb + wr); );   // (6 cm into the stop zone = really pushing)
      );
    );
  );
  hit ? ( time_precise() - wcT > 0.9 ? ( wcT = time_precise(); ); );            // one "Ouch!" per bump
  wcX = tx / rs; wcY = ty / rs;
);
// ON A LINE (Ctrl/Cmd held while dragging source or listener): from where it was when the key went down it moves only
// left / right or only forward / back - the first few pixels of mouse movement choose which. Out: axNX, axNY (slider units)
function axcon(which, nx, ny, thr)
(
  (mouse_cap & 4) ? (
    !axOn ? ( axOn = 1; axA = 0; axX0 = which == 1 ? slider10 : slider13; axY0 = which == 1 ? slider11 : slider14; axMX = mx; axMY = my; );
    axA == 0 && abs(mx - axMX) + abs(my - axMY) > thr ? ( axA = abs(nx - axX0) >= abs(ny - axY0) ? 1 : 2; );
    axNX = axA == 1 ? nx : axX0; axNY = axA == 2 ? ny : axY0;
  ) : ( axOn = 0; axNX = nx; axNY = ny; );
);
// START POSITIONS (implemented by posreset / prstep further below): source and listener back to where a new instance has them - listener in the middle of the room, the
// source 2 m in front of it (closer in a small room), both 1.5 m high, facing and aim fixed, straight ahead. The room
// (size, materials, barrier) stays as it is; a locked one stays where it is. One undo step.
// They glide there in 0.35 s (prstep, every frame): the sound follows smoothly instead of jumping with a click.

// MOTION PRESETS: practical moves for film - set the pattern and start it (from where the balls are)
NMP = 14;
// SHAPES of the new patterns (audio thread copy - the GUI has its own, gmtpt): phase ph 0 … 1 -> amU, amV in -1 … 1
// 5 Square: along the edge of a square (starts right of the center, like the circle); 6 Wave: left - right like the auto pan,
// on a sine line (two waves across); 7 Drawn: the path drawn with the mouse, one round per cycle
// FULL SIZE: the extent of a pattern around its center (in units of Size) - the center moves in from the walls as
// far as needed, so the path always runs at its full Size (a path larger than the room is centered instead)
function amtext(p) local(k)
(
  amX0 = amY0 = -1; amX1 = amY1 = 1; amZ0 = amZ1 = 0;
  p == 1 ? ( amY0 = amY1 = 0; ); p == 2 ? ( amX0 = amX1 = 0; ); p == 3 ? ( amX0 = amX1 = amY0 = amY1 = 0; amZ0 = -1; amZ1 = 1; );
  p == 6 ? ( amY0 = -0.35; amY1 = 0.35; );
  p == 7 && MTDP[512] >= 256 ? ( amX0 = amY0 = amZ0 = 1; amX1 = amY1 = amZ1 = -1; k = 0;
    loop(256, amX0 = min(amX0, MTDP[2 * k]); amX1 = max(amX1, MTDP[2 * k]); amY0 = min(amY0, MTDP[2 * k + 1]); amY1 = max(amY1, MTDP[2 * k + 1]); amZ0 = min(amZ0, MTDP[514 + k]); amZ1 = max(amZ1, MTDP[514 + k]); k += 1; ); );
);
// Fits motion-path center c inside a box of width w with margin mg and path extents lo/hi; returns the midpoint if the path doesn't fit.
function amfit(c, lo, hi, w, mg) local(a, b) ( a = mg - lo; b = w - mg - hi; a <= b ? min(max(c, a), b) : (a + b) / 2; );
// MTDP (drawn motion path, 770 values): [2k], [2k + 1] = x, y of point k (k = 0 … 255, -1 … 1), [512] = number of points (256 = a path exists), [514 + k] = z; the four path slots are kept in MTDS (770 each)
// Evaluates motion pattern p (square, wave or drawn) at phase ph; sets globals amU, amV, amW as the unit-path x/y/z offset.
function amtpt(p, ph) local(s, i0, i1, f)
(
  ph -= floor(ph); amW = 0;
  // Square: s = ph * 8 walks the four edges (2 units each), starting in the middle of the right edge and going up; Wave: two sine periods across; else the drawn path (linear interpolation) or a circle
  p == 5 ? ( s = ph * 8;
    s < 1 ? ( amU = 1; amV = s; ) : ( s < 3 ? ( amU = 2 - s; amV = 1; ) : ( s < 5 ? ( amU = -1; amV = 4 - s; ) : ( s < 7 ? ( amU = s - 6; amV = -1; ) : ( amU = 1; amV = s - 8; ); ); ); );
  ) : ( p == 6 ? ( amU = sin(2 * $pi * ph); amV = 0.35 * sin(2 * $pi * amU); ) : (
    MTDP[512] >= 256 ? ( s = ph * 256; i0 = floor(s); f = s - i0; i0 = i0 % 256; i1 = (i0 + 1) % 256;
      amU = MTDP[2 * i0] + (MTDP[2 * i1] - MTDP[2 * i0]) * f; amV = MTDP[2 * i0 + 1] + (MTDP[2 * i1 + 1] - MTDP[2 * i0 + 1]) * f; amW = MTDP[514 + i0] + (MTDP[514 + i1] - MTDP[514 + i0]) * f; ) : ( amU = cos(2 * $pi * ph); amV = sin(2 * $pi * ph); );
  ); );
);
// Returns the name of motion preset k.
function mpname(k) ( k == 0 ? "Circle around the listener" : (k == 1 ? "Insect around the head" : (k == 2 ? "Car passing by" : (k == 3 ? "Motorbike, fast pass-by" : (k == 4 ? "Someone walks past" :
  (k == 5 ? "Comes closer and goes away" : (k == 6 ? "Auto pan left - right" : (k == 7 ? "Up and down (ladder, lift)" : (k == 8 ? "Listener walks around the room" : (k == 9 ? "Listener walks through the room" : (k == 10 ? "Square around the listener" : (k == 11 ? "Slalom in front" : (k == 12 ? "Lying eight (∞)" : "Standing eight (8)")))))))))))); );
// Returns a one-line description of motion preset k.
function mpdesc(k) ( k == 0 ? "the source circles the listener at its present distance, slowly" : (k == 1 ? "small and fast circles close to the ears" : (k == 2 ? "from where the source is to the right, 3 s" : (k == 3 ? "1.2 s, the pitch drops as it passes" : (k == 4 ? "a few meters from here to the right, 5 s" :
  (k == 5 ? "forward and back from where it is" : (k == 6 ? "a sine across the room, 4 s per swing" : (k == 7 ? "the source rises and sinks" : (k == 8 ? "the listener circles the room center" : (k == 9 ? "the listener walks left and right" : (k == 10 ? "along the edges of a square around the listener" : (k == 11 ? "a wave line left and right from where it is, 10 s" : (k == 12 ? "a figure of eight lying on its side, from where it is" : "a figure of eight from front to back, from where it is")))))))))))); );
// the presets start from where source and listener are now - nothing is moved first; the size is kept inside the room
function mpapply(k) local(rsc, W, D, lx0, ly0, sx0, sy0, sz0, mv, pat, ar, rate, sz, st, gl, mx0, my0, dx, dy, dd, j, a)
(
  rsc = min(4, max(0.25, slider31)); W = slider2; D = slider3;                                     // slider units (meters / Size)
  sx0 = slider10; sy0 = slider11; sz0 = slider12; lx0 = slider13; ly0 = slider14;
  // defaults: mv = who moves (0 source, 1 listener), pat = pattern (0 circle, 1 left - right, 2 front - back, 3 up - down, 4 pass-by, 5 square, 6 wave, 7 drawn), ar = around (0 listener, 1 own spot, 2 room center), rate in Hz, sz = size (slider units), st = start angle in degrees, gl = glide in s
  mv = 0; pat = 0; ar = 0; rate = 0.1; sz = 2 / rsc; st = 0; gl = 1;
  k == 1 ? ( rate = 0.6; sz = 0.6 / rsc; gl = 0.3; );
  k == 2 ? ( pat = 4; rate = 1 / 3; sz = 8 / rsc; gl = 0.2; );
  k == 3 ? ( pat = 4; rate = 1 / 1.2; sz = 10 / rsc; gl = 0.2; );
  k == 4 ? ( pat = 4; rate = 1 / 5; sz = 3 / rsc; gl = 0.2; );
  k == 5 ? ( pat = 2; ar = 1; rate = 0.08; sz = 2 / rsc; );
  k == 6 ? ( pat = 1; ar = 1; rate = 0.25; sz = 2 / rsc; );
  k == 7 ? ( pat = 3; ar = 1; rate = 0.1; sz = 0.9 / rsc; );
  k == 8 ? ( mv = 1; ar = 2; rate = 0.03; sz = min(W, D) / 3; );
  k == 9 ? ( mv = 1; pat = 1; ar = 1; rate = 0.05; sz = W / 2 - 0.6; );
  k == 10 ? ( pat = 5; rate = 0.06; sz = 2 / rsc; );
  k == 11 ? ( pat = 6; ar = 1; rate = 0.1; sz = 3 / rsc; );
// FIGURE EIGHT (lying / standing): written as a path into the slot in use, then run as Drawn
  k >= 12 ? ( pat = 7; ar = 1; rate = 0.1; sz = 2 / rsc; j = 0;
    loop(256, a = 2 * $pi * j / 256; MTDP[2 * j] = k == 12 ? sin(a) : 0.6 * sin(2 * a); MTDP[2 * j + 1] = k == 12 ? 0.6 * sin(2 * a) : sin(a); MTDP[514 + j] = 0; j += 1; );
    MTDP[512] = 256; MTDP[513] = 0; memcpy(MTDS + 770 * min(3, max(0, floor(MTDS[3080] + 0.5))), MTDP, 770); );
  // a circle around the listener: radius and start = where the source is now (it does not jump)
  dx = sx0 - lx0; dy = sy0 - ly0; dd = sqrt(dx * dx + dy * dy);
  k == 0 && dd > 0.3 / rsc ? ( sz = dd; st = atan2(dy, dx) * 180 / $pi; st < 0 ? st += 360; );
  // keep the path inside the room (from the present spots)
  mx0 = mv == 1 ? lx0 : sx0; my0 = mv == 1 ? ly0 : sy0;
  (pat == 0 || pat == 5) && mv == 0 && k != 0 ? ( sz = min(sz, min(min(lx0, W - lx0), min(ly0, D - ly0)) - 0.3 / rsc); );
  pat == 1 || pat == 6 ? ( sz = min(sz, min(mx0, W - mx0) - 0.3 / rsc); );
  pat == 7 ? ( sz = min(sz, max(0.3, min(W, D) / 2 - 0.3 / rsc)); );   // (the full-size fit moves the center in)
  pat == 4 ? ( sz = min(sz, (W - mx0 - 0.3 / rsc) / 2); );                 // the pass-by runs from here to the right (2 x Size)
  pat == 2 ? ( sz = min(sz, min(my0, D - my0) - 0.3 / rsc); );
  pat == 3 ? ( sz = min(sz, min(sz0 - 0.2, slider4 * rsc - 0.2 - sz0) / rsc); );   // the room height in meters (slider4 x Size) - the source height is in meters
  sz = max(0.1, sz);
  snap_take();
  mtE > 0.001 || slider100 >= 0.5 ? ( mtKill = 1; );
  sset(101, mv); sset(102, pat); sset(103, rate); sset(104, sz); sset(105, st); sset(106, 0); sset(107, ar); sset(108, 0); sset(109, gl);
  sset(100, 1);
  snap_notify();
);
// Starts a smooth reset of unlocked source and/or listener to the default arrangement (middle of room, 1.5 m high, facing fixed); sets prA*/prB* targets and prOn.
function posreset()
(
  snap_take(); mtE > 0.001 || slider100 >= 0.5 ? ( mtKill = 1; slider100 = 0; );
  prLo = !lockL; prSo = !lockS;
  prA0 = slider10; prA1 = slider11; prA2 = slider12; prA3 = slider13; prA4 = slider14; prA5 = slider15; prA6 = slider17; prA7 = slider61;
  prB0 = slider2 / 2; prB1 = clampy(pdefsy(slider3, rs)); prB2 = clampz(1.5);
  prB3 = slider2 / 2; prB4 = slider3 / 2; prB5 = clampz(1.5); prB6 = 0; prB7 = 0;
  prLo ? ( setp(16, 1); ); prSo ? ( setp(63, 1); );                 // facing and aim: fixed (the angles glide)
  prOn = 1; prT0 = time_precise();
);
// Interpolates angle a to b in degrees by fraction e via the shortest way round; result wrapped to -180..180.
function prang(a, b, e) local(d) ( d = b - a; d > 180 ? d -= 360; d < -180 ? d += 360; d = a + d * e; d > 180 ? d - 360 : (d < -180 ? d + 360 : d); );
// Per-frame step of the position-reset glide (0.35 s, smoothstep): eases source/listener position and angles; ends with prOn = 0.
function prstep() local(t, e)
(
  t = min(1, (time_precise() - prT0) / 0.35); e = t * t * (3 - 2 * t);
  prSo ? ( setp(10, prA0 + (prB0 - prA0) * e); setp(11, prA1 + (prB1 - prA1) * e); setp(12, prA2 + (prB2 - prA2) * e); setp(61, prang(prA7, prB7, e)); );
  prLo ? ( setp(13, prA3 + (prB3 - prA3) * e); setp(14, prA4 + (prB4 - prA4) * e); setp(15, prA5 + (prB5 - prA5) * e); setp(17, prang(prA6, prB6, e)); );
  t >= 1 ? ( prOn = 0; snap_notify(); );
);
// Moves source (which = 1) or listener (2) to x/y (nx, ny); shifts the motion pattern center while MOTION runs and drags the linked partner with Link S+L.
function moveobj(which, nx, ny) local(dx, dy)
(
// MOTION running: the drag moves the CENTER of the pattern (the audio thread writes the ball's position itself)
  mtEg > 0.001 && slider100 < 0.5 ? mtKill = 1;   // a drag during the fade-out ends the glide at once (the ball is the user's again)
  mtEg > 0.001 && slider100 >= 0.5 && ((which == 1 && mtSon) || (which == 2 && mtLon)) ? (
    !mtDrg ? ( mtDrg = 1; mtGx = nx; mtGy = ny; mtGcx = mtApOn ? MTAP[0] : (which == 1 ? mtC0x : mtC1x); mtGcy = mtApOn ? MTAP[1] : (which == 1 ? mtC0y : mtC1y); );   // grab: the center and the mouse
    mtApOn ? ( MTAP[0] = clampx(mtGcx + nx - mtGx); MTAP[1] = clampy(mtGcy + ny - mtGy); )   // with a placed Own spot the drag moves the spot
    : which == 1 ? ( mtC0x = clampx(mtGcx + nx - mtGx); mtC0y = clampy(mtGcy + ny - mtGy); )                              // the path follows the mouse by its delta
               : ( mtC1x = clampx(mtGcx + nx - mtGx); mtC1y = clampy(mtGcy + ny - mtGy); );
  ) :
  which == 1 ? (
    !lockS ? (
      dx = nx - slider10; dy = ny - slider11; setp(10, clampx(nx)); setp(11, clampy(ny));
      linksl && !lockL ? ( lis_walk(slider13 + dx, slider14 + dy); setp(13, wcX); setp(14, wcY); );
      mtEg > 0.001 && slider100 >= 0.5 ? ( !mtSon ? ( mtC0x = slider10; mtC0y = slider11; ); !mtLon && linksl && !lockL ? ( mtC1x = slider13; mtC1y = slider14; ); );   // a ball the pattern does not move is the CENTER of the path around it - it takes the center along
    );
  ) : (
    !lockL ? (
      dx = nx - slider13; dy = ny - slider14; lis_walk(nx, ny); setp(13, wcX); setp(14, wcY);
      linksl && !lockS ? ( setp(10, clampx(slider10 + dx)); setp(11, clampy(slider11 + dy)); );
      mtEg > 0.001 && slider100 >= 0.5 ? ( !mtLon ? ( mtC1x = slider13; mtC1y = slider14; ); !mtSon && linksl && !lockS ? ( mtC0x = slider10; mtC0y = slider11; ); );   // the listener stays where it is put - and the path around it follows (the source circled around the old spot before)
    );
  );
);
// Sets height of source (which = 1) or listener (2) to nz (clamped), moving the other by the same offset when linked and unlocked.
function moveobjz(which, nz) local(dz)
(
  which == 1 ? ( !lockS ? ( dz = nz - slider12; setp(12, clampz(nz)); linksl && !lockL ? setp(15, clampz(slider15 + dz)); ); )
             : ( !lockL ? ( dz = nz - slider15; setp(15, clampz(nz)); linksl && !lockS ? setp(12, clampz(slider12 + dz)); ); );
);
// Returns the index (0-12) of the source-model menu item under mouse y yy, or -1 if none.
function srcitem(yy) local(ri, k, top)
(
  ri = -1; k = 0;
  // item k sits at its row k * srcih; 5 px group gaps open up before item 1 and before item 5
  loop(13, top = srcmy + 4 + k * srcih + (k > 0) * 5 + (k > 4) * 5; yy >= top && yy < top + srcih ? ri = k; k += 1; );
  ri;
);
// how a parameter morphs: 0 linear, 1 logarithmic, 2 switch (half way), 3 angle (the short way round)
function mtype(i) (
  (i == 1 || i == 16 || i == 19 || i == 23 || i == 24 || i == 26 || i == 27 || i == 29 || i == 30 || i == 35 || i == 36 || i == 39
   || i == 41 || i == 46 || i == 58 || i == 59 || i == 60 || i == 63 || (i >= 65 && i <= 69) || i == 72 || i == 73 || i == 85 || i == 86 || i == 87 || i == 93 || i == 94 || i == 110 || i == 114 || i == 115 || i == 116) ? 2 : ((i == 25 || i == 28 || i == 31 || i == 32 || i == 62 || i == 70 || i == 71 || (i >= 79 && i <= 82)) ? 1 : ((i == 17 || i == 61) ? 3 : 0));
);
// Returns the default value of slider i (GUI-thread copy; a_sdef is the audio-thread copy).
function sdef(i) ( i == 118 ? 1 : i >= 111 ? 0 : (i == 110 ? 0 : (i == 100 ? 0 : (i == 101 ? 0 : (i == 102 ? 0 : (i == 103 ? 0.25 : (i == 104 ? 2 : (i == 105 ? 0 : (i == 106 ? 0 : (i == 107 ? 0 : (i == 108 ? 0 : (i == 109 ? 0.5 : (i == 98 ? 100 : (i == 99 ? 0 : (i == 1 ? 0 : (i == 2 ? 5.5 : (i == 3 ? 4.5 : (i == 4 ? 2.6 : (i == 5 ? 0.04 : (i == 6 ? 0.04 : (i == 7 ? 0.04 : (i == 8 ? 0.04 : (i == 9 ? 0.33 : (i == 10 ? 2.75 : (i == 11 ? 0.25 : (i == 12 ? 1.5 : (i == 13 ? 2.75 : (i == 14 ? 2.25 : (i == 15 ? 1.5 : (i == 16 ? 1 : (i == 17 ? 0 : (i == 18 ? 100 : (i == 19 ? 1 : (i == 20 ? 0 : (i == 21 ? 0 : (i == 22 ? 0 : (i == 23 ? 1 : (i == 24 ? 0 : (i == 25 ? 80 : (i == 26 ? 1 : (i == 27 ? 0 : (i == 28 ? 12000 : (i == 29 ? 1 : (i == 30 ? 0 : (i == 31 ? 1 : (i == 32 ? 1 : (i == 33 ? 0 : (i == 34 ? 0 : (i == 35 ? 0 : (i == 36 ? 0 : (i == 37 ? 0 : (i == 38 ? 80 : (i == 39 ? 0 : (i == 40 ? 0 : (i == 41 ? 0 : (i == 42 ? 1.25 : (i == 43 ? 0 : (i == 44 ? 25 : (i == 45 ? 2.75 : (i == 46 ? 0 : (i == 47 ? 0 : (i == 48 ? 0 : (i == 49 ? 0 : (i == 50 ? 0 : (i == 51 ? 0 : (i == 52 ? 0 : (i == 53 ? 0 : (i == 54 ? 0 : (i == 55 ? 0 : (i == 56 ? 0 : (i == 57 ? 0 : (i == 58 ? 0 : (i == 59 ? 0 : (i == 60 ? 0 : (i == 61 ? 0 : (i == 62 ? 100 : (i == 63 ? 1 : (i == 64 ? 0 : (i == 65 ? 0 : (i == 66 ? 0 : (i == 67 ? 0 : (i == 68 ? 1 : (i == 69 ? 1 : (i == 70 ? 50 : (i == 71 ? 50 : (i == 72 ? 8 : (i == 73 ? 8 : (i == 74 ? 0 : (i == 75 ? 30 : (i == 76 ? 30 : (i == 77 ? 0 : (i == 78 ? 0 : (i == 79 ? 120 : (i == 80 ? 120 : (i == 81 ? 10000 : (i == 82 ? 10000 : (i == 83 ? 0 : (i == 84 ? 0 : (i == 85 ? 1 : (i == 86 ? 1 : (i == 87 ? 1 : (i >= 88 && i <= 92 ? 100 : (i == 97 ? 100 : 0)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))); );
function a_sdef(i) ( i == 118 ? 1 : i >= 111 ? 0 : (i == 110 ? 0 : (i == 100 ? 0 : (i == 101 ? 0 : (i == 102 ? 0 : (i == 103 ? 0.25 : (i == 104 ? 2 : (i == 105 ? 0 : (i == 106 ? 0 : (i == 107 ? 0 : (i == 108 ? 0 : (i == 109 ? 0.5 : (i == 98 ? 100 : (i == 99 ? 0 : (i == 1 ? 0 : (i == 2 ? 5.5 : (i == 3 ? 4.5 : (i == 4 ? 2.6 : (i == 5 ? 0.04 : (i == 6 ? 0.04 : (i == 7 ? 0.04 : (i == 8 ? 0.04 : (i == 9 ? 0.33 : (i == 10 ? 2.75 : (i == 11 ? 0.25 : (i == 12 ? 1.5 : (i == 13 ? 2.75 : (i == 14 ? 2.25 : (i == 15 ? 1.5 : (i == 16 ? 1 : (i == 17 ? 0 : (i == 18 ? 100 : (i == 19 ? 1 : (i == 20 ? 0 : (i == 21 ? 0 : (i == 22 ? 0 : (i == 23 ? 1 : (i == 24 ? 0 : (i == 25 ? 80 : (i == 26 ? 1 : (i == 27 ? 0 : (i == 28 ? 12000 : (i == 29 ? 1 : (i == 30 ? 0 : (i == 31 ? 1 : (i == 32 ? 1 : (i == 33 ? 0 : (i == 34 ? 0 : (i == 35 ? 0 : (i == 36 ? 0 : (i == 37 ? 0 : (i == 38 ? 80 : (i == 39 ? 0 : (i == 40 ? 0 : (i == 41 ? 0 : (i == 42 ? 1.25 : (i == 43 ? 0 : (i == 44 ? 25 : (i == 45 ? 2.75 : (i == 46 ? 0 : (i == 47 ? 0 : (i == 48 ? 0 : (i == 49 ? 0 : (i == 50 ? 0 : (i == 51 ? 0 : (i == 52 ? 0 : (i == 53 ? 0 : (i == 54 ? 0 : (i == 55 ? 0 : (i == 56 ? 0 : (i == 57 ? 0 : (i == 58 ? 0 : (i == 59 ? 0 : (i == 60 ? 0 : (i == 61 ? 0 : (i == 62 ? 100 : (i == 63 ? 1 : (i == 64 ? 0 : (i == 65 ? 0 : (i == 66 ? 0 : (i == 67 ? 0 : (i == 68 ? 1 : (i == 69 ? 1 : (i == 70 ? 50 : (i == 71 ? 50 : (i == 72 ? 8 : (i == 73 ? 8 : (i == 74 ? 0 : (i == 75 ? 30 : (i == 76 ? 30 : (i == 77 ? 0 : (i == 78 ? 0 : (i == 79 ? 120 : (i == 80 ? 120 : (i == 81 ? 10000 : (i == 82 ? 10000 : (i == 83 ? 0 : (i == 84 ? 0 : (i == 85 ? 1 : (i == 86 ? 1 : (i == 87 ? 1 : (i >= 88 && i <= 92 ? 100 : (i == 97 ? 100 : 0)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))); );   // (audio-thread copy: EEL keeps parameters in static storage)   // default of every slider (audio-thread use)
// Returns the minimum value of slider i (GUI-thread copy of a_smin).
function smin(i) ( i >= 111 ? 0 : (i == 110 ? 0 : (i == 100 ? 0 : (i == 101 ? 0 : (i == 102 ? 0 : (i == 103 ? 0.02 : (i == 104 ? 0.1 : (i == 105 ? 0 : (i == 106 ? 0 : (i == 107 ? 0 : (i == 108 ? 0 : (i == 109 ? 0 : (i == 98 ? 0 : (i == 99 ? 0 : (i == 1 ? 0 : (i == 2 ? 0.8 : (i == 3 ? 0.8 : (i == 4 ? 0.8 : (i == 5 ? 0.01 : (i == 6 ? 0.01 : (i == 7 ? 0.01 : (i == 8 ? 0.01 : (i == 9 ? 0.01 : (i == 10 ? 0 : (i == 11 ? 0 : (i == 12 ? 0 : (i == 13 ? 0 : (i == 14 ? 0 : (i == 15 ? 0 : (i == 16 ? 0 : (i == 17 ? -180 : (i == 18 ? 0 : (i == 19 ? 0 : (i == 20 ? -60 : (i == 21 ? -60 : (i == 22 ? -72 : (i == 23 ? 0 : (i == 24 ? 0 : (i == 25 ? 20 : (i == 26 ? 0 : (i == 27 ? 0 : (i == 28 ? 1000 : (i == 29 ? 0 : (i == 30 ? 0 : (i == 31 ? 0.25 : (i == 32 ? 0.01 : (i == 33 ? -100 : (i == 34 ? 0 : (i == 35 ? 0 : (i == 36 ? 0 : (i == 37 ? 0 : (i == 38 ? 0 : (i == 39 ? 0 : (i == 40 ? -72 : (i == 41 ? 0 : (i == 42 ? 0 : (i == 43 ? 0 : (i == 44 ? 0 : (i == 45 ? 0 : (i == 46 ? 0 : (i == 47 ? -10 : (i == 48 ? -10 : (i == 49 ? -10 : (i == 50 ? -24 : (i == 51 ? -3 : (i == 52 ? -10 : (i == 53 ? -24 : (i == 54 ? -3 : (i == 55 ? -10 : (i == 56 ? -24 : (i == 57 ? -3 : (i == 58 ? 0 : (i == 59 ? 0 : (i == 60 ? 0 : (i == 61 ? -180 : (i == 62 ? 25 : (i == 63 ? 0 : (i == 64 ? 0 : (i == 65 ? 0 : (i == 66 ? 0 : (i == 67 ? 0 : (i == 68 ? 0 : (i == 69 ? 0 : (i == 70 ? 1 : (i == 71 ? 1 : (i == 72 ? 0 : (i == 73 ? 0 : (i == 74 ? 0 : (i == 75 ? 0 : (i == 76 ? 0 : (i == 77 ? -48 : (i == 78 ? -48 : (i == 79 ? 20 : (i == 80 ? 20 : (i == 81 ? 1000 : (i == 82 ? 1000 : (i == 83 ? 0 : (i == 84 ? 0 : (i == 85 ? 0 : (i == 97 ? 50 : 0))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))); );
function a_smin(i) ( i >= 111 ? 0 : (i == 110 ? 0 : (i == 100 ? 0 : (i == 101 ? 0 : (i == 102 ? 0 : (i == 103 ? 0.02 : (i == 104 ? 0.1 : (i == 105 ? 0 : (i == 106 ? 0 : (i == 107 ? 0 : (i == 108 ? 0 : (i == 109 ? 0 : (i == 98 ? 0 : (i == 99 ? 0 : (i == 1 ? 0 : (i == 2 ? 0.8 : (i == 3 ? 0.8 : (i == 4 ? 0.8 : (i == 5 ? 0.01 : (i == 6 ? 0.01 : (i == 7 ? 0.01 : (i == 8 ? 0.01 : (i == 9 ? 0.01 : (i == 10 ? 0 : (i == 11 ? 0 : (i == 12 ? 0 : (i == 13 ? 0 : (i == 14 ? 0 : (i == 15 ? 0 : (i == 16 ? 0 : (i == 17 ? -180 : (i == 18 ? 0 : (i == 19 ? 0 : (i == 20 ? -60 : (i == 21 ? -60 : (i == 22 ? -72 : (i == 23 ? 0 : (i == 24 ? 0 : (i == 25 ? 20 : (i == 26 ? 0 : (i == 27 ? 0 : (i == 28 ? 1000 : (i == 29 ? 0 : (i == 30 ? 0 : (i == 31 ? 0.25 : (i == 32 ? 0.01 : (i == 33 ? -100 : (i == 34 ? 0 : (i == 35 ? 0 : (i == 36 ? 0 : (i == 37 ? 0 : (i == 38 ? 0 : (i == 39 ? 0 : (i == 40 ? -72 : (i == 41 ? 0 : (i == 42 ? 0 : (i == 43 ? 0 : (i == 44 ? 0 : (i == 45 ? 0 : (i == 46 ? 0 : (i == 47 ? -10 : (i == 48 ? -10 : (i == 49 ? -10 : (i == 50 ? -24 : (i == 51 ? -3 : (i == 52 ? -10 : (i == 53 ? -24 : (i == 54 ? -3 : (i == 55 ? -10 : (i == 56 ? -24 : (i == 57 ? -3 : (i == 58 ? 0 : (i == 59 ? 0 : (i == 60 ? 0 : (i == 61 ? -180 : (i == 62 ? 25 : (i == 63 ? 0 : (i == 64 ? 0 : (i == 65 ? 0 : (i == 66 ? 0 : (i == 67 ? 0 : (i == 68 ? 0 : (i == 69 ? 0 : (i == 70 ? 1 : (i == 71 ? 1 : (i == 72 ? 0 : (i == 73 ? 0 : (i == 74 ? 0 : (i == 75 ? 0 : (i == 76 ? 0 : (i == 77 ? -48 : (i == 78 ? -48 : (i == 79 ? 20 : (i == 80 ? 20 : (i == 81 ? 1000 : (i == 82 ? 1000 : (i == 83 ? 0 : (i == 84 ? 0 : (i == 85 ? 0 : (i == 97 ? 50 : 0))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))); );   // (audio-thread copy: EEL keeps parameters in static storage)
// Returns the maximum value of slider i (GUI-thread copy of a_smax).
function smax(i) ( i == 118 ? 16 : (i == 119 ? 3 : (i >= 111 ? (i == 114 || i == 115 ? 3 : (i == 116 ? 1 : 100)) : (i == 110 ? 1 : (i == 100 ? 1 : (i == 101 ? 2 : (i == 102 ? 7 : (i == 103 ? 10 : (i == 104 ? 20 : (i == 105 ? 360 : (i == 106 ? 1 : (i == 107 ? 2 : (i == 108 ? 8 : (i == 109 ? 5 : (i == 98 ? 200 : (i == 99 ? 24 : (i == 1 ? 1 : (i == 2 ? 40 : (i == 3 ? 40 : (i == 4 ? 15 : (i == 5 ? 1 : (i == 6 ? 1 : (i == 7 ? 1 : (i == 8 ? 1 : (i == 9 ? 1 : (i == 10 ? 40 : (i == 11 ? 40 : (i == 12 ? 15 : (i == 13 ? 40 : (i == 14 ? 40 : (i == 15 ? 15 : (i == 16 ? 2 : (i == 17 ? 180 : (i == 18 ? 100 : (i == 19 ? 1 : (i == 20 ? 12 : (i == 21 ? 12 : (i == 22 ? 24 : (i == 23 ? 1 : (i == 24 ? 1 : (i == 25 ? 2000 : (i == 26 ? 7 : (i == 27 ? 1 : (i == 28 ? 20000 : (i == 29 ? 7 : (i == 30 ? 1 : (i == 31 ? 4 : (i == 32 ? 10 : (i == 33 ? 100 : (i == 34 ? 200 : (i == 35 ? 12 : (i == 36 ? 1 : (i == 37 ? 100 : (i == 38 ? 100 : (i == 39 ? 1 : (i == 40 ? 48 : (i == 41 ? 3 : (i == 42 ? 160 : (i == 43 ? 100 : (i == 44 ? 70 : (i == 45 ? 160 : (i == 46 ? 9 : (i == 47 ? 10 : (i == 48 ? 10 : (i == 49 ? 10 : (i == 50 ? 24 : (i == 51 ? 3 : (i == 52 ? 10 : (i == 53 ? 24 : (i == 54 ? 3 : (i == 55 ? 10 : (i == 56 ? 24 : (i == 57 ? 3 : (i == 58 ? 8 : (i == 59 ? 8 : (i == 60 ? 1 : (i == 61 ? 180 : (i == 62 ? 400 : (i == 63 ? 1 : (i == 64 ? 16 : (i == 65 ? 1 : (i == 66 ? 1 : (i == 67 ? 1 : (i == 68 ? 1 : (i == 69 ? 1 : (i == 70 ? 4000 : (i == 71 ? 4000 : (i == 72 ? 12 : (i == 73 ? 12 : (i == 74 ? 300 : (i == 75 ? 95 : (i == 76 ? 95 : (i == 77 ? 6 : (i == 78 ? 6 : (i == 79 ? 2000 : (i == 80 ? 2000 : (i == 81 ? 20000 : (i == 82 ? 20000 : (i == 83 ? 100 : (i == 84 ? 100 : (i >= 88 && i <= 92 ? 100 : (i == 95 ? 40 : (i == 96 ? 15 : (i == 97 ? 200 : 1))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))); )));
function a_smax(i) ( i == 118 ? 16 : (i == 119 ? 3 : (i >= 111 ? (i == 114 || i == 115 ? 3 : (i == 116 ? 1 : 100)) : (i == 110 ? 1 : (i == 100 ? 1 : (i == 101 ? 2 : (i == 102 ? 7 : (i == 103 ? 10 : (i == 104 ? 20 : (i == 105 ? 360 : (i == 106 ? 1 : (i == 107 ? 2 : (i == 108 ? 8 : (i == 109 ? 5 : (i == 98 ? 200 : (i == 99 ? 24 : (i == 1 ? 1 : (i == 2 ? 40 : (i == 3 ? 40 : (i == 4 ? 15 : (i == 5 ? 1 : (i == 6 ? 1 : (i == 7 ? 1 : (i == 8 ? 1 : (i == 9 ? 1 : (i == 10 ? 40 : (i == 11 ? 40 : (i == 12 ? 15 : (i == 13 ? 40 : (i == 14 ? 40 : (i == 15 ? 15 : (i == 16 ? 2 : (i == 17 ? 180 : (i == 18 ? 100 : (i == 19 ? 1 : (i == 20 ? 12 : (i == 21 ? 12 : (i == 22 ? 24 : (i == 23 ? 1 : (i == 24 ? 1 : (i == 25 ? 2000 : (i == 26 ? 7 : (i == 27 ? 1 : (i == 28 ? 20000 : (i == 29 ? 7 : (i == 30 ? 1 : (i == 31 ? 4 : (i == 32 ? 10 : (i == 33 ? 100 : (i == 34 ? 200 : (i == 35 ? 12 : (i == 36 ? 1 : (i == 37 ? 100 : (i == 38 ? 100 : (i == 39 ? 1 : (i == 40 ? 48 : (i == 41 ? 3 : (i == 42 ? 160 : (i == 43 ? 100 : (i == 44 ? 70 : (i == 45 ? 160 : (i == 46 ? 9 : (i == 47 ? 10 : (i == 48 ? 10 : (i == 49 ? 10 : (i == 50 ? 24 : (i == 51 ? 3 : (i == 52 ? 10 : (i == 53 ? 24 : (i == 54 ? 3 : (i == 55 ? 10 : (i == 56 ? 24 : (i == 57 ? 3 : (i == 58 ? 8 : (i == 59 ? 8 : (i == 60 ? 1 : (i == 61 ? 180 : (i == 62 ? 400 : (i == 63 ? 1 : (i == 64 ? 16 : (i == 65 ? 1 : (i == 66 ? 1 : (i == 67 ? 1 : (i == 68 ? 1 : (i == 69 ? 1 : (i == 70 ? 4000 : (i == 71 ? 4000 : (i == 72 ? 12 : (i == 73 ? 12 : (i == 74 ? 300 : (i == 75 ? 95 : (i == 76 ? 95 : (i == 77 ? 6 : (i == 78 ? 6 : (i == 79 ? 2000 : (i == 80 ? 2000 : (i == 81 ? 20000 : (i == 82 ? 20000 : (i == 83 ? 100 : (i == 84 ? 100 : (i >= 88 && i <= 92 ? 100 : (i == 95 ? 40 : (i == 96 ? 15 : (i == 97 ? 200 : 1))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))); )));   // (audio-thread copy: EEL keeps parameters in static storage)
// a snapshot value inside the slider's range (a damaged or very old snapshot can never set a parameter to nonsense)
function sclamp(i, v) ( v == v ? min(smax(i), max(smin(i), v)) : sdef(i); );
// Audio-thread version of sclamp: clamps v to slider i's range, returning the default if v is NaN.
function a_sclamp(i, v) ( v == v ? min(a_smax(i), max(a_smin(i), v)) : a_sdef(i); );
// Returns the morph default for slider i: 100 for 62, 0 for 86/87, the slider default for i >= 65, else 0.
function mdef(i) ( i == 62 ? 100 : (i == 86 || i == 87 ? 0 : (i >= 65 ? sdef(i) : 0)); );
function a_mdef(i) ( i == 62 ? 100 : (i == 86 || i == 87 ? 0 : (i >= 65 ? a_sdef(i) : 0)); );   // (audio-thread copy; the value a slider takes when a snapshot predates it, see loadslot)
// Saves all slider values into snapshot slot k (format flag 19), with sliders above 64 stored in the extended array, then calls sliderchange(-1).
function saveslot(k) local(b, i)
(
  b = uslot + k * USL; b[0] = 19; i = 1;   // 19: + distance perspective (117)
    // flag history (see loadslot): 18: + delay type, repeats through the device (115, 116); 17: distortion counts on Original (older: 0 there); 16: + source call line (111-114); 15: + input mono mix (110); 14: + motion (100-109); 6: sliders 1-63 + 65-85 (5: 65-84, without the source switch); 4: no delay (65 and up); 3: before 0.201, no source aim = aimed at the listener; 2: 0.197, slider 62 was air temperature; 1: sliders 1-60
  loop(NSN, i < 64 ? ( b[i] = slider(i); ) : ( i > 64 ? ( uslotX[k * USX + i - 64] = slider(i); ); ); i += 1; );
  uslotR[k * URR] = 1; rfget(uslotR + k * URR + 1);   // the reflectors belong to the snapshot
  sliderchange(-1);
);
// clear a snapshot slot: settings, name and description
function snclear(k) (
  uslot[k * USL] = 0; memset(uslot + k * USL + 1, 0, USL - 1); memset(uslotX + k * USX, 0, USX); memset(uslotR + k * URR, 0, URR);
  memset(snam + k * SNL, 0, SNL); memset(dsc + k * DSL, 0, DSL);
  snlast == k + 1 ? ( snlast = 0; );
  sliderchange(-1);
);
// Loads snapshot slot k with an undo snapshot, applying only sliders that existed in the slot's format version (older slots keep defaults for newer ones).
function loadslot(k) local(b, i)
(
  b = uslot + k * USL;
  // a slider newer than the slot's format flag b[0] (thresholds as in the flag list in saveslot) takes the morph default mdef(i); otherwise the stored value, clamped
  b[0] ? ( snap_take(); i = 1; loop(NSN, i != 64 ? sset(i, (i > 60 && b[0] < 1.5) || (i >= 62 && b[0] < 2.5) || (i == 63 && b[0] < 3.5) || (i > 64 && b[0] < 4.5) || (i > 84 && b[0] < 5.5) || (i > 85 && b[0] < 6.5) || (i > 86 && b[0] < 7.5) || (i > 87 && b[0] < 8.5) || (i > 92 && b[0] < 9.5) || (i > 94 && b[0] < 10.5) || (i > 96 && b[0] < 11.5) || (i > 97 && b[0] < 12.5) || (i > 99 && b[0] < 13.5) || (i > 109 && b[0] < 14.5) || (i > 110 && b[0] < 15.5) || (i == 37 && b[0] < 16.5 && floor(b[35] + 0.5) == 0) || (i > 114 && b[0] < 17.5) || (i > 116 && b[0] < 18.5) ? mdef(i) : sclamp(i, i < 64 ? b[i] : uslotX[k * USX + i - 64])); i += 1; ); uslotR[k * URR] >= 0.5 ? rfput(uslotR + k * URR + 1); selw = 0; snap_notify(); );   // (a snapshot with reflectors brings them back; an older one leaves them as they are)
);
// Returns true if slot k is filled and all of its saved slider values equal the current ones (up to the range its format version covers).
function slotmatch(k) local(b, i, m)
(
  b = uslot + k * USL; m = b[0] > 0; i = 1;
  // loop count = the highest slider number the slot's format flag stores (same thresholds as loadslot)
  loop(b[0] < 1.5 ? 60 : (b[0] < 2.5 ? 61 : (b[0] < 3.5 ? 62 : (b[0] < 4.5 ? 63 : (b[0] < 5.5 ? 84 : (b[0] < 6.5 ? 85 : (b[0] < 7.5 ? 86 : (b[0] < 8.5 ? 87 : (b[0] < 9.5 ? 92 : (b[0] < 10.5 ? 94 : (b[0] < 11.5 ? 96 : (b[0] < 12.5 ? 97 : (b[0] < 13.5 ? 99 : (b[0] < 14.5 ? 109 : (b[0] < 15.5 ? 110 : (b[0] < 17.5 ? 114 : (b[0] < 18.5 ? 116 : NSN)))))))))))))))), i != 64 && !(i == 37 && b[0] < 16.5 && floor(b[35] + 0.5) == 0) ? ( abs(slider(i) - (i < 64 ? b[i] : uslotX[k * USX + i - 64])) > 0.0001 ? m = 0; ); i += 1; );
// in the free field the reflectors must match too (in a room they play no part)
  m && slider1 >= 0.5 && uslotR[k * URR] >= 0.5 ? ( i = 0; loop(15, abs(vopt[RFB + i] - uslotR[k * URR + 1 + i]) > 0.0001 ? m = 0; i += 1; ); );
  m;
);

function sn_commit(k, store) local(i) (             // store = 1: name + all current settings, 0: name only
  i = 0; loop(SNL, snam[k * SNL + i] = i < snelen ? sneb[i] : 0; i += 1; );
  i = 0; loop(DSL, dsc[k * DSL + i] = i < dselen ? dseb[i] : 0; i += 1; );
  store ? ( saveslot(k); snlast = k + 1; ) : sliderchange(-1);
  snedit = 0;
);
// hash of all sound-relevant sliders; compute_geo (7 image sources, trig, exp) runs only when it changes
function geo_if_changed() local(i, key, rfi)
(
  key = 0; i = 1;
  loop(64, key += slider(i) * (i * 7919 + 1); i += 1; );
  rfi = 0; loop(15, key += vopt[RFB + rfi] * (1913.7 + rfi * 37.3); rfi += 1; );   // the reflectors
  key += slider117 * 1597.3 + slider85 * 991.3 + slider92 * 1237.7 + slider95 * 1301.3 + slider96 * 1409.9 + slider97 * 1499.7;
  key += slider119 * 1663.1 + vopt[69] * 1777.7;   // the ground (Free field) and its level - they were missing: a change counted only once something moved (Motion)
  key != last_pkey ? (
    // BARRIER MIX below 100 %: the paths through it lose that share of the damping (see bxing); the reverb tail is blended
    // in energy between the barrier as set and the room without it (the two-part model alone would get too loud for a
    // barrier that lets nearly everything through)
    // a partition smaller than the room: its gap couples the two parts - the tail moves towards the open room
// the share is the coupling of two rooms through an opening - open area S against the absorption area A of the
    // far part: S / (S + A). A 10 cm slit over a 5.5 m wall (0.55 m2) couples about 5 %, a 40 cm gap 20 %, a half-height
    // partition about 45 % (the square root used before gave 20 % for the slit - the tail jumped up with a tiny gap)
    bgW = max(0.8, slider2) * min(4, max(0.25, slider31)); bgH = max(0.8, slider4) * min(4, max(0.25, slider31));
    bgBW = slider95 > 0.001 ? min(bgW, max(0.3, slider95 * min(4, max(0.25, slider31)))) : bgW; bgBH = slider96 > 0.001 ? min(bgH, max(0.3, slider96 * min(4, max(0.25, slider31)))) : bgH;
    bgS = (bgW - bgBW) * bgH + bgBW * (bgH - bgBH);                                                                      // the open part of the barrier plane (m2)
    bgA = max(1, roomA > 0 ? roomA * 0.5 : bgW * bgH);                                                                   // absorption area of the far part (half the room)
    bmixg = min(1, max(0, slider92 / 100)) * (1 - bgS / (bgS + bgA));
    bgblend = 0;
    slider41 >= 0.5 && bmixg < 0.999 ? ( bgoff = 1; compute_geo(); gLGo = xLG; gEQLo = xEQL; gSTWo = xSTW; gRTo = rt; gLFo = lfrP; gMFo = mfp; bgoff = 0; bgblend = 1; );
    compute_geo(); bgblend = 0;
    slider41 >= 0.5 && bmixg < 0.999 ? (
      xLG = sqrt(bmixg * sqr(xLG) + (1 - bmixg) * sqr(gLGo));
      xLLP *= bmixg; xLDf = 1 - (1 - xLDf) * bmixg; xLPL *= bmixg; xLPR *= bmixg; xLG2 *= bmixg;
      xEQL = 1 / sqrt(bmixg / sqr(max(0.0001, xEQL)) + (1 - bmixg) / sqr(max(0.0001, gEQLo)));
      xSTW = xSTW * bmixg + gSTWo * (1 - bmixg);
    );
    // ROOM MODES only when what they depend on changed (room, positions, barrier, bass decay) - automating a level, the mix,
    // a filter or the EQ no longer recalculates up to 96 modes every block
    mdbon = bon && bmixg >= 0.5;
    mdkey = roomW * 1.31 + roomD * 7.17 + roomH * 13.3 + sx * 3.7 + sy * 5.9 + sz * 11.1 + lx * 17.3 + ly * 19.7 + lz * 23.9
          + rt * lfrP * 29.3 + bon * 31.1 + yb * 37.7 + sideL * 41.3 + bsame * 43.7 + brOA * 47.9 + ax0 * 53.3 + az0 * 59.9 + brX0 * 71.3 + brH * 73.9 + isfree * 61.7 + mdbon * 67.1;
    mdkey != last_mdkey ? ( mdcalc(); mdapply(); last_mdkey = mdkey; );
    last_pkey = key;
  );
  mdPend && cMD < 0.001 ? mdswap();   // modes faded out: take the new set
);

// SAMPLE RATE: everything that depends on it. Called at the end of @init and from @block whenever srate is not the rate it
// was set up for - REAPER does not re-run @init on a sample-rate change with ext_noinit, and srate may still be wrong in
// @init when a project loads (then the plugin would run with the coefficients of another rate: other filters, other
// smoothing, other modulation - a render after reopening the project would sound different).
function srsetup() local(k)
(
  srini = srate;
  srlayout();
  memset(ibuf, 0, dlL - ibuf); memset(dlL, 0, 2 * DLMAX + MAXIN);
  // reverb modulation (Hz -> radians per sample), depth
  moddep = 0.00015 * srate;   // (see @init)
  edL0 = min(EDN - 1, floor(0.0043 * srate)); edL1 = min(EDN - 1, floor(0.0067 * srate)); edL2 = min(EDN - 1, floor(0.0049 * srate)); edL3 = min(EDN - 1, floor(0.0079 * srate)); memset(edB0, 0, 4 * EDN);
  // smoothing, delay tape, meters, scene crossfade, shelves
  // one-pole coefficients 1 - exp(-1 / (T * srate)): sm 30 ms, smm 12 ms, smf 150 ms, smu 120 ms, dlsm 80 ms; gRK = gain factor per sample for 3 dB per ms (limits how fast a gain may rise)
  sm  = 1 - exp(-1 / (0.03 * srate)); smm = 1 - exp(-1 / (0.012 * srate)); gRK = 10 ^ (3 / 20 / (srate / 1000)); smf = 1 - exp(-1 / (0.15 * srate)); smu = 1 - exp(-1 / (0.12 * srate)); dlsm = 1 - exp(-1 / (0.08 * srate));
  smd = sm; smfd = smf;
  dwr1 = 2 * $pi * 0.53 / srate; dwr2 = 2 * $pi * 0.61 / srate; dwr3 = 2 * $pi * 6.1 / srate; dwr4 = 2 * $pi * 6.7 / srate;
  dwA = 0.001 * srate; dfA = 0.00008 * srate;
  pmdec = exp(-1 / (0.15 * srate)); sm5 = 1 - exp(-1 / (0.005 * srate));
  lfc = 1 - exp(-2 * $pi * 250 / srate); dirC = 1 - exp(-2 * $pi * 3000 / srate); dirCB = 1 - exp(-2 * $pi * 250 / srate);
  xfstep = 1 / (0.08 * srate); JTHR = srate / 343; JTHRF = 0.003 * srate; JTS = 0.0005 * srate;
  fzDR = 1 - 2 * $pi * 20 / srate; fzrel = exp(-1 / (0.05 * srate));
  // limiter: look-ahead 1.5 ms (= latency reported to REAPER), release 150 ms
  LMLA = min(590, max(1, floor(0.0015 * srate + 0.5))); LMN = LMLA + 1; LMQ = LMN + 2;
  lmDL = lmb; lmDR = lmDL + LMLA; lmW = lmDR + LMLA; lmQv = lmW + LMN; lmQt = lmQv + LMQ;
  memset(lmDL, 0, 2 * LMLA); k = 0; loop(LMN, lmW[k] = 1; k += 1; );
  lmsum = LMN; lmqh = lmqn = 0; lmt = 0; lmd = 0; lmdd = 0; lmr = 1; lmga = 1;
  lmrel = 1 - exp(-1 / (0.15 * srate)); pdc_delay = LMLA + 1; crk = exp(-1 / (0.3 * srate)); smQ = 1 - exp(-1 / (0.005 * srate)); xfRI = 1 / (0.003 * srate);   // (pdc +1: see the direct sound,)
  smab = 1 - exp(-1 / (0.25 * srate));                               // A / B trim: 250 ms glide
  // bass steering crossover: 4th-order Linkwitz-Riley = two equal 2nd-order Butterworth sections (low and high stay in phase)
  fdesign(stC, stC + 40, 0, 250, 2); memcpy(stC + 5, stC, 5);
  fdesign(stC + 10, stC + 40, 1, 250, 2); memcpy(stC + 15, stC + 10, 5);
  // every cached design is made again for the new rate (geometry, reverb lines, modes, source model, tone, filters, delay)
  last_pkey = -1; last_mdkey = -1; last_mfp = -1; last_gkey = -1; last_gkey2 = -1; lastfz = -1; lastfzo = -1; lastfzn = -1;
  lastbr = lastdk = -1000; lastlc = lasthc = -1; dllkey = -1;
  fpanic = 1; snap = 1;
);
srsetup();
compute_geo(); mdbon = bon && slider92 >= 50; mdcalc(); mdswap(); cMD = 0;

@serialize
// Every variable here starts with ser_: REAPER runs @serialize on its own thread (e.g. for every undo point when a
// parameter changes) - a shared name like sm (the smoothing of the audio) would be overwritten in the middle of playback.
// REAPER: file_avail(0) is < 0 while saving and 0 while loading (it does NOT tell how much is left), so the state is
// written with a header (magic, version, sizes) and read back by those sizes. Older projects (0.176 … 0.191, no header)
// are read in one go - file_mem returns how many values there were - and split up by the old block order.
uslot = 64; uslotX = 1152; vopt = 2048; snam = vopt + 5200; USL = 64; USX = 56; SNL = 24; dsc = 4194304; DSL = 128; ser_scr = 8192; urb = 4194304 + 32000; URM = 24; URL = 66; MTDP = 4194304 + 40000; MTDS = 4194304 + 41000; MTAP = 4194304 + 45000; uslotR = 4194304 + 62000; URR = 16;   // fixed addresses, as in @init (may run first)
ser_sermagic = 7391.25;
file_avail(0) < 0 ? (
  // ---- save ----
  ser_sm = ser_sermagic; file_var(0, ser_sm); ser_sv = 18; file_var(0, ser_sv);
  ser_sn = 16; file_var(0, ser_sn); ser_su = USL; file_var(0, ser_su); ser_sl = SNL; file_var(0, ser_sl); ser_so = 70; file_var(0, ser_so);
  file_mem(0, uslot, 16 * USL); file_mem(0, snam, 16 * SNL); file_mem(0, vopt, ser_so);   // 0.362: the count written is the count in the header (0.359 - 0.361 wrote 51 values under the number 52: a project saved with them is read with 51)
  ser_sd = DSL; file_var(0, ser_sd); file_mem(0, dsc, 16 * DSL);   // v2: descriptions
  ser_sx = USX; file_var(0, ser_sx); file_mem(0, uslotX, 16 * USX);   // v4: snapshot extension (delay); 56 per slot (motion)
  ser_su2 = URM * URL; file_var(0, ser_su2); file_mem(0, urb, URM * URL);   // v9: my rooms
  file_mem(0, MTDP, 770); file_mem(0, MTDS, 3081); file_mem(0, MTAP, 3); file_mem(0, uslotR, 16 * URR);   // v17: + the reflectors of the 16 snapshots (URR = 16 values per slot) // v16: + the Own spot // v15: + the four path slots // v14: the drawn motion path with its timing and heights (v13: 514 values, v12: 128 points, 257 values)
) : (
  // ---- load ----
  ser_sm = 0; file_var(0, ser_sm);
  ser_sm == ser_sermagic ? (
    file_var(0, ser_sv); file_var(0, ser_sn); file_var(0, ser_su); file_var(0, ser_sl); file_var(0, ser_so);
    ser_sv < 18 ? rfLoad += 1;   // before 0.385 the reflectors had no sliders: the loaded values go into them (not the defaults the other way)
    ser_sn = min(16, max(0, ser_sn)); ser_su = max(1, ser_su); ser_sl = max(1, ser_sl);
    memset(uslot, 0, 16 * USL); memset(snam, 0, 16 * SNL);
    ser_k = 0; loop(ser_sn, file_mem(0, ser_scr, ser_su); memcpy(uslot + ser_k * USL, ser_scr, min(ser_su, USL)); ser_k += 1; );
    ser_k = 0; loop(ser_sn, file_mem(0, ser_scr, ser_sl); memcpy(snam + ser_k * SNL, ser_scr, min(ser_sl, SNL)); ser_k += 1; );
    vopt[18] = vopt[19] = vopt[20] = 0; memset(vopt + 21, 0, 24); ser_cn = ser_so == 52 ? 51 : min(ser_so, 70); file_mem(0, ser_scr, ser_cn); memcpy(vopt, ser_scr, ser_cn); vopt[18] = 0; ser_so < 49 ? ( vopt[48] = 0; ); ser_so < 50 ? ( vopt[49] = 0; ); ser_so < 51 ? ( vopt[50] = 0; ); ser_so < 52 ? ( vopt[51] = 0; );   // Hear is never loaded (monitor only)
    ser_so < 48 ? ( vopt[45] = 0; vopt[46] = 1; vopt[47] = 0; vopt[12] = vopt[12] - (vopt[12] & 512); );   // before 0.246: open in the new tabs (the All tab = the two columns as before)
    ser_sv < 10 ? ( vopt[14] = 0; memset(vopt + 21, 0, 21); vopt[12] = vopt[12] - (vopt[12] & 1024); );   // before 0.230: layout A - standard sizes, the delay open, Plan view
    ser_sv < 11 && floor(slider35 + 0.5) == 0 ? ( slider37 = 0; );   // 0.304: before, distortion did nothing on Original (now it saturates)
    ser_sv < 3 ? ( slider63 = 0; );   // projects before 0.201: the source kept aiming at the listener - they sound as before
    ser_sv < 5 ? ( vopt[12] = 1024 + (vopt[12] & 512); );   // before 0.210: Expert opens with the delay folded, all other sections open
    ser_sv < 6 ? ( vopt[12] = vopt[12] | 2304; );   // before 0.212: Timeline (echogram) and Decay by band start folded
    ser_sv < 8 ? ( slider87 = 0; );                    // before 0.214: no feedback link - L and R keep their feedback
    ser_sv < 7 ? ( slider86 = 0; );                    // before 0.213: no level link - L and R keep the levels they had
    memset(dsc, 0, 16 * DSL);
    ser_sv >= 2 ? (
      ser_sd = 0; file_var(0, ser_sd); ser_sd = min(4096, max(0, ser_sd));
      ser_k = 0; loop(ser_sn, file_mem(0, ser_scr, ser_sd); memcpy(dsc + ser_k * DSL, ser_scr, min(ser_sd, DSL)); dsc[ser_k * DSL + DSL - 1] = 0; ser_k += 1; );
    );
    memset(uslotX, 0, 16 * USX);
    ser_sv >= 4 ? (
      ser_sx = 0; file_var(0, ser_sx); ser_sx = min(4096, max(0, ser_sx));
      ser_k = 0; loop(ser_sn, file_mem(0, ser_scr, ser_sx); memcpy(uslotX + ser_k * USX, ser_scr, min(ser_sx, USX)); ser_k += 1; );
    );
    memset(urb, 0, URM * URL);
    ser_sv >= 9 ? (
      ser_su2 = 0; file_var(0, ser_su2); ser_su2 = min(4000, max(0, ser_su2));
      file_mem(0, ser_scr, ser_su2);
      ser_su2 == URM * 64 ? ( ser_k = 0; loop(URM, memcpy(urb + ser_k * URL, ser_scr + ser_k * 64, 64); ser_k += 1; ); )   // before 0.232: 64 per room
                          : memcpy(urb, ser_scr, min(ser_su2, URM * URL));
    );
    memset(MTDP, 0, 770); memset(MTDS, 0, 3081); memset(MTAP, 0, 3); memset(uslotR, 0, 16 * URR);
    ser_sv >= 15 ? ( file_mem(0, MTDP, 770); file_mem(0, MTDS, 3081); ser_sv >= 16 ? file_mem(0, MTAP, 3); ser_sv >= 17 ? file_mem(0, uslotR, 16 * URR); ) :
    // older formats: v14 = 770 values, v13 = 514, v12 = 128 xy points + a count (257 values), resampled here to 256 points by linear interpolation
    ser_sv == 14 ? ( file_mem(0, MTDP, 770); ) : ser_sv == 13 ? ( file_mem(0, MTDP, 514); ) : (
    ser_sv == 12 ? ( file_mem(0, ser_scr, 257);
      ser_scr[256] >= 128 ? ( ser_k = 0; loop(256, ser_j = floor(ser_k / 2); ser_f = (ser_k / 2) - ser_j;
        MTDP[2 * ser_k] = ser_scr[2 * ser_j] + (ser_scr[2 * ((ser_j + 1) % 128)] - ser_scr[2 * ser_j]) * ser_f;
        MTDP[2 * ser_k + 1] = ser_scr[2 * ser_j + 1] + (ser_scr[2 * ((ser_j + 1) % 128) + 1] - ser_scr[2 * ser_j + 1]) * ser_f; ser_k += 1; ); MTDP[512] = 256; ); ); );
    ser_sv < 15 && MTDP[512] >= 256 ? ( memcpy(MTDS, MTDP, 770); MTDS[3080] = 0; );   // (0.331: an older path goes into slot 1)
  ) : (
    floor(slider35 + 0.5) == 0 ? ( slider37 = 0; );   // (see above)
    rfLoad += 1;
    // legacy stream: the first value was already read (slot 1 flag)
    ser_scr[0] = ser_sm; ser_lgn = 1 + file_mem(0, ser_scr + 1, 4000); ser_lgp = 0;
    // old block order (each block zero-filled when the stream ends before it): slots 0-3 + 12 view options, slots 4-5, 2 view options, names 0-5, slots 6-9, names 6-9, 2 view options, slots 10-15, names 10-15, 2 view options
    ser_lgn >= 268 ? (
    ser_lgp + 256 <= ser_lgn ? memcpy(uslot, ser_scr + ser_lgp, 256) : memset(uslot, 0, 256); ser_lgp += 256;
    ser_lgp + 12 <= ser_lgn ? memcpy(vopt, ser_scr + ser_lgp, 12) : memset(vopt, 0, 12); ser_lgp += 12;
    ser_lgp + 128 <= ser_lgn ? memcpy(uslot + 4 * USL, ser_scr + ser_lgp, 128) : memset(uslot + 4 * USL, 0, 128); ser_lgp += 128;
    ser_lgp + 2 <= ser_lgn ? memcpy(vopt + 12, ser_scr + ser_lgp, 2) : memset(vopt + 12, 0, 2); ser_lgp += 2; vopt[12] = 3328 + (vopt[12] & 512);
    ser_lgp + 144 <= ser_lgn ? memcpy(snam, ser_scr + ser_lgp, 144) : memset(snam, 0, 144); ser_lgp += 144;
    ser_lgp + 256 <= ser_lgn ? memcpy(uslot + 6 * USL, ser_scr + ser_lgp, 256) : memset(uslot + 6 * USL, 0, 256); ser_lgp += 256;
    ser_lgp + 96 <= ser_lgn ? memcpy(snam + 6 * SNL, ser_scr + ser_lgp, 96) : memset(snam + 6 * SNL, 0, 96); ser_lgp += 96;
    ser_lgp + 2 <= ser_lgn ? memcpy(vopt + 14, ser_scr + ser_lgp, 2) : memset(vopt + 14, 0, 2); ser_lgp += 2;
    ser_lgp + 384 <= ser_lgn ? memcpy(uslot + 10 * USL, ser_scr + ser_lgp, 384) : memset(uslot + 10 * USL, 0, 384); ser_lgp += 384;
    ser_lgp + 144 <= ser_lgn ? memcpy(snam + 10 * SNL, ser_scr + ser_lgp, 144) : memset(snam + 10 * SNL, 0, 144); ser_lgp += 144;
    ser_lgp + 2 <= ser_lgn ? memcpy(vopt + 16, ser_scr + ser_lgp, 2) : memset(vopt + 16, 0, 2); ser_lgp += 2;
    ) : ( memset(uslot, 0, 16 * USL); memset(snam, 0, 16 * SNL); );
    memset(dsc, 0, 16 * DSL); memset(uslotX, 0, 16 * 40); slider63 = 0; slider86 = 0; slider87 = 0;
  );
);
// first start of an instance: view defaults (vopt[64], vopt[65] = reflector levels +12 dB, vopt[66] = reflectors off flag (0 = on), vopt[67] = free-field outline view ON, vopt[68] = Input Guard OFF)
vopt[0] != 7333 ? ( vopt[64] = vopt[65] = 12; vopt[66] = 0; vopt[67] = 1; vopt[68] = 0; vopt[69] = 0; vopt[0] = 7333; vopt[1] = 0; vopt[2] = 0; vopt[3] = 0; vopt[4] = 1; vopt[8] = 1; vopt[11] = 1; vopt[12] = 2304; vopt[14] = vopt[15] = 0; vopt[16] = 1; vopt[17] = 0; memset(vopt + 21, 0, 24); vopt[45] = 0; vopt[46] = 1; vopt[47] = 0; );   // new instance: Basic view
@block
// sample rate changed (REAPER does not re-run @init then, see srsetup): set everything up for the new rate
srate > 1000 && srate != srini ? ( srsetup(); geoagain = 2; );
// while a GUI gesture is held, keep the GUI value in the slider: envelope playback must not win
// between two GUI frames (REAPER's own sliders get this from the host; a JSFX GUI has to do it)
bi = 1; loop(NSL, gheld[bi] ? a_sset(bi, gval[bi]); bi += 1; );
// playback start after a pause in processing (REAPER did not run the FX while stopped): every delay line still holds the
// audio from before the stop - limiter line, direct sound, reflections - and would play it in front of the new audio
// (a click). Then everything is cleared. While REAPER keeps the FX running when stopped nothing is stale and nothing is
// cleared; during playback nothing is ever cleared.
// Detected as: playing, processing had paused (> 50 ms without a block) and the transport position jumped or playback just
// started. A loop wrap (jump without a pause) and REAPER's anticipative processing (pauses without a jump) clear nothing.
plst = play_state & 1; plpos = play_position;
// Every start of playback (and so every render) begins from the same, empty state - two renders with the same settings
// are sample-identical. A loop wrap clears nothing.
plst && (!plstl || (time_precise() - lastblk > 0.05 && abs(plpos - plexp) > 0.05)) ? ( fpanic = 1; );
plexp = plpos + samplesblock / srate; plstl = plst;
fpanic ? (
  memset(ibuf, 0, dlL - ibuf);                         // all delay lines (input, FDN, allpasses)
  memset(lpL, 0, 2 * TS); memset(olpL, 0, 2 * TS);
  memset(fst, 0, 8); memset(fhs, 0, 8); memset(fls, 0, 8); memset(fqS, 0, 96); memset(fst2, 0, 8); memset(fhs2, 0, 8); memset(fls2, 0, 8);
  memset(stb, 0, 64); lastbr = lastdk = -1000; lastlc = lasthc = -1;
  memset(fzS, 0, 40); memset(fzSs, 0, 40); memset(FZA, 0, 120); memset(FAP, 0, 80); memset(FZO, 0, 44); memset(odS, 0, 160); fzU1 = fzU2 = 0; fzL1 = odS[33] = odS[73] = lcosh(0); fzhold = 0; fzDx = fzDy = 0; fzenv = 0; fzgate = 0; memset(stS, 0, 16);
  lpLt = lpLt2 = 0; lpSd = 0; memset(bpkS, 0, 8); bndS = 0; dirS = dirB = 0; tkn = 0; last_pkey = -1; fpanic = 0; memset(scRat, 0, 80);
  memset(lmDL, 0, 2 * LMLA); k0 = 0; loop(LMN, lmW[k0] = 1; k0 += 1; ); lmsum = LMN; lmqh = lmqn = 0; lmr = 1;
  memset(dlL, 0, 2 * DLMAX + MAXIN); memset(dlSt, 0, 8); memset(osU, 0, 32); su0 = su1 = su2 = su3 = su4 = su5 = su6 = su7 = su8 = su9 = 0; sd0 = sd1 = sd2 = sd3 = sd4 = sd5 = sd6 = sd7 = sd8 = sd9 = 0;
  // the rest of the running state: modulation phases, pointers, noise generator, modes, tape, limiter clock - and every
  // smoothed parameter jumps to its value (snap)
  modreset();
  wi = 0; fwi = 0; api = 0; fwn = 0; dn = 0.00000000000000000001; fzacc = 0; fzrs = 1;
  qlsetup(); qlclear(); dqclear(); memset(qlS, 0, 16); lastqlBW = -1; memset(dvS, 0, 80); dtL1 = dtL2 = dtR1 = dtR2 = 0;
  dwi = 0; dlhold = 0; shold = 0; lpSd = 0; dwp1 = 0; dwp2 = 1.9; dwp3 = 0; dwp4 = 0.7;
  k0 = 0; loop(128, mdM[k0 * 6 + 3] = mdM[k0 * 6 + 4] = 0; mdM[k0 * 6 + 2] = mdM[k0 * 6 + 5]; k0 += 1; ); memset(mdXS, 0, 24); mdx1 = mdx2 = 0;
  lmt = 0; lmd = 0; lmdd = 0; lmga = 1; xfade = 0; xfG = 0; mrlast = -1; snap = 1;
  memset(tSt, 0, TS); memset(xfS, 0, 16); memset(oDL, 0, 5 * TS); memset(oSM, 0, TS); ldS = ld2S = 0; oLD = oLD2 = 0;   // crossfade bookkeeping (also the order of the sums)
);   // audio-thread loop counters must not be shared with @gfx
// sanity: a parameter that is not a number (e.g. a new slider REAPER hands over from an older project) -> its default;
// otherwise one NaN would silence the whole output (the runaway guard mutes it every block)
si = 1; loop(NSL, sv0 = slider(si); !(sv0 == sv0) || abs(sv0) > 1000000 ? a_sset(si, a_sdef(si)); si += 1; );
// reflector sliders (see REFLECTOR AUTOMATION): an older project / a NaN = the reflectors as loaded go into the sliders;
// a slider that changed (automation, host) = into the reflector; while the GUI holds a reflector the GUI value stays
si = 0; loop(15, sv0 = slider(120 + si);
  rfLoad != rfLoadA || !(sv0 == sv0) || rfTch[si] ? ( a_rfset(si, vopt[RFB + si]); rfSl[si] = vopt[RFB + si]; ) :
  abs(sv0 - rfSl[si]) > 0.0000001 ? ( vopt[RFB + si] = sv0; rfSl[si] = sv0; );
  si += 1; ); rfLoadA = rfLoad;
// SOLO (monitoring, GUI only - never saved): 1 = source model alone, 2 = the delay echoes alone, 3 = the room alone. It ends
// by itself when the plugin window is closed (no GUI frame for 0.5 s) - a render can never run soloed by mistake.
// A / B: loudness of each side while it plays (mean square of the output, gated at -70 dBFS, running mean over the
// first 3 s, then a 3 s window); B is trimmed to the loudness of A. Only while the plugin window is open (like solo) -
// a render with the window closed is never trimmed.
abA = time_precise() - soloT < 0.5 && abOn;
abBP = abPw / max(1, samplesblock); abPw = 0; abdt = samplesblock / srate;
abCur != abCurL ? ( abSk = 1; abCurL = abCur; );                      // after a switch: 1 s settling (the old tail dies away)
abSk > 0 ? ( abSk -= abdt; abBP = 0; );
abA && abBP > 0.0000001 ? (
  abCur ? ( abN1 += abdt; abE1 += (abBP - abE1) * min(1, abdt / min(3, abN1)); ) : ( abN0 += abdt; abE0 += (abBP - abE0) * min(1, abdt / min(3, abN0)); );
);
!abA ? ( abE0 = abE1 = 0; abN0 = abN1 = 0; );
xABG = abA && abMatch && abCur && abN0 > 0.5 && abN1 > 0.5 ? min(8, max(0.125, sqrt(abE0 / max(0.0000000001, abE1)))) : 1;
spOn = time_precise() - soloT < 0.5;
soA = time_precise() - soloT < 0.5 ? soloM : 0; !(soA == 1 || soA == 2 || soA == 3) ? soA = 0;
// monitor only (heartbeat of the window); a NaN or out-of-range value counts as 0 (the guard used to sit inside the comment)
lsm = vopt[11] >= 0.5 || time_precise() - soloT >= 0.5 ? 0 : vopt[18];
!(lsm == lsm) || lsm < 0 || lsm > 7 ? ( lsm = 0; );
tMuD = (lsm & 1) || soA == 3 ? 0 : 1; tMuE = (lsm & 2) ? 0 : 1; tMuL = (lsm & 4) ? 0 : 1;
// SNAPSHOT MORPH (automatable): 0 = off, 1 … 16 = that snapshot, in between = morph towards the next filled snapshot.
// Continuous parameters glide (sizes, positions, levels; frequencies, SIZE and DECAY logarithmically, angles the short way
// round), switches and menus change half way. Written only when the morph value moves - a knob turned by hand stays.
mrv = min(16, max(0, slider64));
mrv != mrlast ? (
  mrlast = mrv;
  mrv >= 1 ? (
    mra = floor(mrv); mrf = mrv - mra; mrb = min(16, mra + 1);
    mpa = uslot + (mra - 1) * USL; mpb = uslot + (mrb - 1) * USL; mpaX = uslotX + (mra - 1) * USX - 64; mpbX = uslotX + (mrb - 1) * USX - 64;   // stride USX
    mpa[0] > 0 ? (
      (mpb[0] <= 0 || mrf < 0.00001) ? ( mpb = mpa; mpbX = mpaX; mrf = 0; );
      mi = 1;
      loop(NSN,
        mi != 64 ? (
        // mva / mvb: value of slider mi in snapshot A / B; a slider newer than that snapshot's format flag takes its morph default (same thresholds as loadslot)
        mva = (mi > 60 && mpa[0] < 1.5) || (mi >= 62 && mpa[0] < 2.5) || (mi == 63 && mpa[0] < 3.5) || (mi > 64 && mpa[0] < 4.5) || (mi > 84 && mpa[0] < 5.5) || (mi > 85 && mpa[0] < 6.5) || (mi > 86 && mpa[0] < 7.5) || (mi > 87 && mpa[0] < 8.5) || (mi > 92 && mpa[0] < 9.5) || (mi > 94 && mpa[0] < 10.5) || (mi > 96 && mpa[0] < 11.5) || (mi > 97 && mpa[0] < 12.5) || (mi > 99 && mpa[0] < 13.5) || (mi > 109 && mpa[0] < 14.5) || (mi > 110 && mpa[0] < 15.5) || (mi == 37 && mpa[0] < 16.5 && floor(mpa[35] + 0.5) == 0) || (mi > 114 && mpa[0] < 17.5) || (mi > 116 && mpa[0] < 18.5) ? a_mdef(mi) : (mi < 64 ? mpa[mi] : mpaX[mi]); mvb = (mi > 60 && mpb[0] < 1.5) || (mi >= 62 && mpb[0] < 2.5) || (mi == 63 && mpb[0] < 3.5) || (mi > 64 && mpb[0] < 4.5) || (mi > 84 && mpb[0] < 5.5) || (mi > 85 && mpb[0] < 6.5) || (mi > 86 && mpb[0] < 7.5) || (mi > 87 && mpb[0] < 8.5) || (mi > 92 && mpb[0] < 9.5) || (mi > 94 && mpb[0] < 10.5) || (mi > 96 && mpb[0] < 11.5) || (mi > 97 && mpb[0] < 12.5) || (mi > 99 && mpb[0] < 13.5) || (mi > 109 && mpb[0] < 14.5) || (mi > 110 && mpb[0] < 15.5) || (mi == 37 && mpb[0] < 16.5 && floor(mpb[35] + 0.5) == 0) || (mi > 114 && mpb[0] < 17.5) || (mi > 116 && mpb[0] < 18.5) ? a_mdef(mi) : (mi < 64 ? mpb[mi] : mpbX[mi]);
        mty = mtype(mi);
        mty == 2 ? ( mv = mrf < 0.5 ? mva : mvb; ) : (
        mty == 1 ? ( mv = mva > 0 && mvb > 0 ? mva * (mvb / mva) ^ mrf : mva + (mvb - mva) * mrf; ) : (
        mty == 3 ? ( mdd = mvb - mva; mdd > 180 ? mdd -= 360; mdd < -180 ? mdd += 360; mv = mva + mdd * mrf; mv > 180 ? mv -= 360; mv < -180 ? mv += 360; ) : (
          mv = mva + (mvb - mva) * mrf; ); ); );
        mv = a_sclamp(mi, mv); abs(slider(mi) - mv) > 0.0000001 ? a_sset(mi, mv);
        );
        mi += 1;
      );
    );
  );
);
// ---- MOTION: source and / or listener on a pattern (circle, left-right, front-back, up-down, pass-by, square, wave, drawn). The positions
// are WRITTEN to the position sliders (10-15) every block and reported to REAPER (slider_automate), so the movement can be
// recorded as automation (Write / Latch) and is seen in the parameter list. The center of the pattern = where the ball
// stood when Motion was switched on (slider units); the glide fades from the center into the pattern and back to it.
// Phase: starts from the beginning at every playback start (the same place at every playback), free-running otherwise
mtOn = slider100 >= 0.5; mtTq = floor(slider108 + 0.5);
itdN = vopt[49] >= 1.5 ? 0 : (vopt[49] >= 0.5 ? 7 : NIMG);   // 2 = Stereo-safe - one read for both ears: level panning only, no ear timing, no head shadow // Detail: taps with their own right-ear read (ITD + head shadow)
mtKill ? ( mtE = 0; mtOn = 0; mtWas = 0; mtKill = 0; );                               // a preset / reset from the GUI: the pattern ends at once
// beats per cycle in tempo mode (slider108): 1-4 = 8 / 4 / 2 / 1 bars, 5-8 = 1/2, 1/4, 1/8, 1/16 note; counted in time-signature beats, converted to quarter notes below
mtBeats = mtTq == 1 ? 8 * ts_num : (mtTq == 2 ? 4 * ts_num : (mtTq == 3 ? 2 * ts_num : (mtTq == 4 ? ts_num : (mtTq == 5 ? 2 : (mtTq == 6 ? 1 : (mtTq == 7 ? 0.5 : 0.25))))));
mtTq > 0 ? ( mtBeats *= 4 / max(1, ts_denom); );
mtRate = mtTq > 0 ? max(20, min(300, slider74 > 0.5 ? slider74 : tempo)) / 60 / max(0.0625, mtBeats) : max(0.02, min(10, slider103));
mtBlk += 1;
mtOn && !mtWas ? ( mtC0x = slider10; mtC0y = slider11; mtC0z = slider12; mtC1x = slider13; mtC1y = slider14; mtC1z = slider15; mtT0 = play_position; mtU = 0;   // switched on: remember the centers (+ the start of a pass-by)
  mtPh = 0; mtOne = 0; mtHold = 0; dopRq = 1;   // every pattern starts at its beginning (at Start) the moment it is switched on
);
// TRIGGER (Start / Restart in the module): the pattern begins again from its start, now
mtTrg != mtTrgS ? ( mtTrgS = mtTrg; mtPh = 0; mtU = 0; mtT0 = play_position; mtOne = 0; mtOneT = 0; mtHold = 0; dopRq = 1; );   // RESTART: from the start, and on (the one-shot of is kept in the code, off)
mtWas = mtOn;
mtRun = mtOn || mtE > 0.0005;
mtRun ? (
// the phase moves on from where it is (a new Rate changes the speed, not the place); it is reset from the play
  // position only at a playback start - a loop or a jump while playing goes on smoothly; every playback from the same spot still sounds the same
  mtJmp = (play_state & 1) && (!mtPlay || abs(play_position - mtPP - mtPB) > 0.05);
  mtInc = mtRate * samplesblock / srate;
  (play_state & 1) && !mtPlay ? ( mtPh = 0; mtOne = 0; mtHold = 0; ) : ( !mtHold ? ( mtPh += mtInc; mtOne ? ( mtOneT += mtInc; mtOneT >= 1 ? ( mtPh = 0; mtHold = 1; ); ); ); );   // a playback start = the beginning of the pattern // set from the play position only at a playback START - a loop or a jump while playing goes on smoothly (a loop shorter than one cycle no longer cuts the path short)
  mtPh -= floor(mtPh);
  mtStep = samplesblock / srate / max(0.02, slider109);
  mtE = mtOn ? min(1, mtE + mtStep) : max(0, mtE - mtStep);
  mtTh = 2 * $pi * (mtPh + slider105 / 360) * (slider106 >= 0.5 ? -1 : 1);
// PASS-BY (one shot): a straight line through the center, from Size before it to Size after it, once per cycle time.
  // It starts again at every playback start and at every loop (the play position jumps back); free-running while stopped
  (play_state & 1) ? (
    mtJmp ? ( mtT0 = play_position; mtU = 0; ) : ( mtU += mtRate * samplesblock / srate; );   // integrated (Rate changes keep the place)
    mtPP = play_position; mtPB = samplesblock / srate; mtPlay = 1;
  ) : ( mtPlay = 0; mtU += mtRate * samplesblock / srate; );
  mtOn && mtE < 0.999 ? ( mtT0 = play_position; mtU = 0; );   // the pass starts once the glide has brought the ball to A
  mtU = min(1, max(0, mtU)); mtUs = (2 * mtU - 1) * (slider106 >= 0.5 ? -1 : 1); mtUo = mtUs + (slider106 >= 0.5 ? -1 : 1);   // the pass-by starts at the ball (mtUo 0 … ±2)
  mtDx = cos(slider105 * $pi / 180); mtDy = sin(slider105 * $pi / 180);
  mtP = floor(slider102 + 0.5); mtM = floor(slider101 + 0.5); mtC = floor(slider107 + 0.5);
  mtRs = min(4, max(0.25, slider31)); mtR = slider104;   // the size in slider units - it scales with the room (SIZE) like the positions; shown in meters = x SIZE
  mtRW = max(0.8, slider2); mtRD = max(0.8, slider3); mtRH = max(0.8, slider4) * mtRs;
  mtSon = mtM != 1; mtLon = mtM != 0;
  // the source: around the listener (its center), its own spot or the room center
  mtCX = mtC == 0 ? mtC1x : (mtC == 1 ? mtC0x : mtRW / 2); mtCY = mtC == 0 ? mtC1y : (mtC == 1 ? mtC0y : mtRD / 2);
  mtP == 1 || mtP == 2 || mtP == 6 || mtP == 7 ? ( mtCX = mtC0x; mtCY = mtC0y; );   // lines and the wave around its own spot (they start there)
// OWN SPOT = a point of the user's own in the room (Circle, Square): source and listener go around it
  mtApOn = mtC == 1 && MTAP[2] >= 0.5 && (mtP == 0 || mtP == 5);
  mtApOn ? ( mtCX = MTAP[0]; mtCY = MTAP[1]; );
// the full Size - the center moves in from the walls (not a circle / square around the listener: that one stays put)
  mtCZ = mtC0z; mtPX = mtC0x; mtPY = mtC0y;
  !(mtC == 0 && (mtP == 0 || mtP == 5)) && mtP != 4 ? ( amtext(mtP); mtCX = amfit(mtCX, mtR * amX0, mtR * amX1, mtRW, 0.1 / mtRs); mtCY = amfit(mtCY, mtR * amY0, mtR * amY1, mtRD, 0.1 / mtRs);
    mtCZ = amfit(mtC0z, mtR * mtRs * amZ0, mtR * mtRs * amZ1, mtRH, 0.1); );
// PASS-BY in a room smaller than the line: the line keeps its direction (Start) and is cut to what fits (half length
  // mtPRh); the ball runs it at the full speed and waits at its ends (before: x and y were cut apart - the path bent, the ball
  // sat at a wall and the Doppler did nothing)
  mtP == 4 ? ( mtPRh = min(mtR, min(abs(mtDx) > 0.001 ? (mtRW - 0.2 / mtRs) / (2 * abs(mtDx)) : 100000, abs(mtDy) > 0.001 ? (mtRD - 0.2 / mtRs) / (2 * abs(mtDy)) : 100000));
    mtQd = mtR * (slider106 >= 0.5 ? -1 : 1);
    mtPX = amfit(mtC0x + mtQd * mtDx, -mtPRh * abs(mtDx), mtPRh * abs(mtDx), mtRW, 0.1 / mtRs); mtPY = amfit(mtC0y + mtQd * mtDy, -mtPRh * abs(mtDy), mtPRh * abs(mtDy), mtRD, 0.1 / mtRs);
    // the visible part of the line in time (u0 … u1), run with a short speed ramp at both ends (8 % of it): the ball
    // starts and stops smoothly - no step in pitch at the start and the end of a pass (the Doppler glides in)
    mtPu0 = 0.5 - 0.5 * mtPRh / max(0.000001, mtR); mtPv = min(1, max(0, (mtU - mtPu0) / max(0.000001, 1 - 2 * mtPu0)));
    mtPe = 0.08; mtPvm = 1 / (1 - mtPe);
    mtPq = mtPv < mtPe ? mtPvm * mtPv * mtPv / (2 * mtPe) : (mtPv > 1 - mtPe ? 1 - mtPvm * sqr(1 - mtPv) / (2 * mtPe) : mtPvm * (mtPv - mtPe / 2));
    mtPU = mtPRh * (2 * mtPq - 1) * (slider106 >= 0.5 ? -1 : 1); mtMSX = mtPX; mtMSY = mtPY; );
  mtP >= 5 ? ( amtpt(mtP, mtTh / (2 * $pi)); mtSX = mtCX + mtR * amU; mtSY = mtCY + mtR * amV; mtSZ = mtCZ + mtR * mtRs * amW; ) : (   // Square, Wave, Drawn
  mtP == 0 ? ( mtSX = mtCX + mtR * cos(mtTh); mtSY = mtCY + mtR * sin(mtTh); mtSZ = mtC0z; ) : (
  mtP == 1 ? ( mtSX = mtCX + mtR * sin(mtTh); mtSY = mtC0y; mtSZ = mtC0z; ) : (
  mtP == 2 ? ( mtSX = mtC0x; mtSY = mtCY + mtR * sin(mtTh); mtSZ = mtC0z; ) : (
  mtP == 4 ? ( mtSX = mtPX + mtPU * mtDx; mtSY = mtPY + mtPU * mtDy; mtSZ = mtC0z; ) : (   // (pass-by: through its own spot - that is the closest point)
               mtSX = mtC0x; mtSY = mtC0y; mtSZ = mtCZ + mtR * mtRs * sin(mtTh); )))));
  // the listener: around its own spot or the room center ("Listener" = its own spot); Both = opposite phase
  mtTh2 = mtM == 2 ? mtTh + $pi : mtTh;
  mtCX = mtC == 2 ? mtRW / 2 : mtC1x; mtCY = mtC == 2 ? mtRD / 2 : mtC1y;
  mtM == 1 && mtC == 0 ? ( mtCX = slider10; mtCY = slider11; );   // the listener alone goes around the SOURCE (it stays where it is)
  mtP == 1 || mtP == 2 || mtP == 6 || mtP == 7 ? ( mtCX = mtC1x; mtCY = mtC1y; );
  mtApOn ? ( mtCX = MTAP[0]; mtCY = MTAP[1]; );
  mtCZ = mtC1z; mtPX = mtC1x; mtPY = mtC1y;
  mtP != 4 ? ( amtext(mtP); mtCX = amfit(mtCX, mtR * amX0, mtR * amX1, mtRW, 0.1 / mtRs); mtCY = amfit(mtCY, mtR * amY0, mtR * amY1, mtRD, 0.1 / mtRs);
    mtCZ = amfit(mtC1z, mtR * mtRs * amZ0, mtR * mtRs * amZ1, mtRH, 0.1); );
  mtP == 4 ? ( mtQg = mtM == 2 ? -1 : 1; mtQd = mtR * (slider106 >= 0.5 ? -1 : 1) * mtQg;
    mtPX = amfit(mtC1x + mtQd * mtDx, -mtPRh * abs(mtDx), mtPRh * abs(mtDx), mtRW, 0.1 / mtRs); mtPY = amfit(mtC1y + mtQd * mtDy, -mtPRh * abs(mtDy), mtPRh * abs(mtDy), mtRD, 0.1 / mtRs); mtMLX = mtPX; mtMLY = mtPY; );
  mtP >= 5 ? ( amtpt(mtP, mtTh2 / (2 * $pi)); mtLX = mtCX + mtR * amU; mtLY = mtCY + mtR * amV; mtLZ = mtCZ + mtR * mtRs * amW; ) : (
  mtP == 0 ? ( mtLX = mtCX + mtR * cos(mtTh2); mtLY = mtCY + mtR * sin(mtTh2); mtLZ = mtC1z; ) : (
  mtP == 1 ? ( mtLX = mtCX + mtR * sin(mtTh2); mtLY = mtC1y; mtLZ = mtC1z; ) : (
  mtP == 2 ? ( mtLX = mtC1x; mtLY = mtCY + mtR * sin(mtTh2); mtLZ = mtC1z; ) : (
  mtP == 4 ? ( mtLX = mtPX + mtQg * mtPU * mtDx; mtLY = mtPY + mtQg * mtPU * mtDy; mtLZ = mtC1z; ) : (
               mtLX = mtC1x; mtLY = mtC1y; mtLZ = mtCZ + mtR * mtRs * sin(mtTh2); )))));
// something else moves the ball while the motion writes it (a REAPER envelope in Read / Touch on Source / Listener X, Y):
  // the pattern cannot run - the module says so
  mtEnvN = (mtSon && mtWr && (abs(slider10 - mtW10) > 0.0005 || abs(slider11 - mtW11) > 0.0005)) || (mtLon && mtWr && (abs(slider13 - mtW13) > 0.0005 || abs(slider14 - mtW14) > 0.0005));
  mtEnvC = mtEnvN ? min(50, mtEnvC + 1) : max(0, mtEnvC - 1); mtEnv = mtEnvC > 8;
  // blend from the center (glide), keep inside the room, write the sliders and tell REAPER
  mtSon ? (
    slider10 = min(max(mtC0x + (mtSX - mtC0x) * mtE, 0.1 / mtRs), mtRW - 0.1 / mtRs); slider11 = min(max(mtC0y + (mtSY - mtC0y) * mtE, 0.1 / mtRs), mtRD - 0.1 / mtRs);
    slider12 = min(max(mtC0z + (mtSZ - mtC0z) * mtE, 0.1), mtRH - 0.1);
    slider_automate(slider10); slider_automate(slider11); mtP == 3 || mtP == 7 ? slider_automate(slider12);
    mtW10 = slider10; mtW11 = slider11;
  );
  mtLon ? (
    slider13 = min(max(mtC1x + (mtLX - mtC1x) * mtE, 0.1 / mtRs), mtRW - 0.1 / mtRs); slider14 = min(max(mtC1y + (mtLY - mtC1y) * mtE, 0.1 / mtRs), mtRD - 0.1 / mtRs);
    slider15 = min(max(mtC1z + (mtLZ - mtC1z) * mtE, 0.1), mtRH - 0.1);
    slider_automate(slider13); slider_automate(slider14); mtP == 3 || mtP == 7 ? slider_automate(slider15);
    mtW13 = slider13; mtW14 = slider14;
// FACING = PATH: the listener looks in the direction it moves (smoothed; standing still: the last direction)
    slider16 >= 1.5 ? (
      mtHdx = slider13 - mtHpx; mtHdy = slider14 - mtHpy; mtHpx = slider13; mtHpy = slider14;
      mtHvx = mtHvx * 0.9 + mtHdx * 0.1; mtHvy = mtHvy * 0.9 + mtHdy * 0.1;
      sqr(mtHvx) + sqr(mtHvy) > sqr(0.00002 * samplesblock / 64) ? ( mtHa = atan2(mtHvy, mtHvx) * 180 / $pi + 90; mtHa > 180 ? mtHa -= 360; mtHa < -180 ? mtHa += 360; slider17 = mtHa; slider_automate(slider17); );
    );
  );
  mtWr = 1;
) : ( mtSon = 0; mtLon = 0; mtE = 0; mtWr = 0; mtEnvC = 0; mtEnv = 0; );
// audio back after a pause (the GUI computes the geometry itself while REAPER is stopped): compute it here again in the next two blocks - should the GUI have been in the middle of its own calculation, the second pass repairs it
time_precise() - lastblk > 0.3 ? ( geoagain = 2; );
geoagain > 0 ? ( last_pkey = -1; geoagain -= 1; );
gbusy ? ( gbusyn += 1; ) : ( gbusyn = 0; );
(!gbusy || gbusyn > 8 || snap) ? geo_if_changed();         // a GUI batch still being written: next block (never stuck)
xNF = 0.585 * min(1, max(0, (1 - tapT[0] * C_SND / 1000) / 0.75)) * pbD;   // (none with the source on the listener) // near field: +4 dB lows at 25 cm, nothing from 1 m
// PLAYBACK START (snap, after the clear above): the room as it is set NOW, at once - no fade of the room modes, no burst guard
// and no tail-level matching against whatever ran before (a room set while stopped, the state right after loading a
// project): every render of the same settings starts from exactly the same state, whatever happened before it
snap ? (
  mdPend ? mdswap();
  k0 = 0; loop(8, fgP[k0] = fg[k0]; k0 += 1; ); k0 = 0; loop(4, fg2P[k0] = fg2[k0]; k0 += 1; );
  cTK = xTK; tkn = 0; xfR = 1; xfG = 0;
);
// Reverb lines store what they will play back BEFORE their loop gain. In a very damped room (vocal booth, car) that gain is
// tiny, so the lines hold a hot signal that comes out very quietly. When the loop gain jumps up (preset / snapshot to a room
// with a much longer decay) that hot content would come out at once, up to 15 dB too loud. So when a line's gain rises by more
// than 2 dB in one block, its content is scaled by old / new gain: the tail goes on at the level it had and decays like the
// new room. Small steps (turning DECAY or SIZE) stay below this and change nothing.
fgchg = 0; k0 = 0; loop(8, fgP[k0] < 0.8 * fg[k0] ? fgchg = 1; k0 += 1; );
fgchg ? (
  k0 = 0;
  loop(8,
    fgr = fg[k0] > 0.000001 ? min(1, fgP[k0] / fg[k0]) : 1;
    fgr < 0.999 ? ( scstart(k0, fgr, flen[k0], cFL[k0]); fhs[k0] *= fgr; fls[k0] *= fgr; fst[k0] *= fgr; i0 = 0; loop(12, fqS[k0 * 12 + i0] *= fgr; i0 += 1; ); );
    k0 += 1;
  );
);
fgchg = 0; k0 = 0; loop(4, fg2P[k0] < 0.8 * fg2[k0] ? fgchg = 1; k0 += 1; );
fgchg ? (
  k0 = 0;
  loop(4,
    fgr = fg2[k0] > 0.000001 ? min(1, fg2P[k0] / fg2[k0]) : 1;
    fgr < 0.999 ? ( scstart(8 + k0, fgr, flen2[k0], cFL2[k0]); fhs2[k0] *= fgr; fls2[k0] *= fgr; fst2[k0] *= fgr; );
    k0 += 1;
  );
);
k0 = 0; loop(8, fgP[k0] = fg[k0]; k0 += 1; ); k0 = 0; loop(4, fg2P[k0] = fg2[k0]; k0 += 1; );
lastblk = time_precise();
// ---- DELAY targets (before the room): times (ms or note value x tempo), feedback, levels, filters in the loop ----
xDON = soA == 2 ? 1 : (soA == 1 || soA == 3 ? 0 : slider65 >= 0.5); dlpp = slider66 >= 0.5;   // room solo bypasses the delay too
// MIX per module (Expert): 100 % = the module as set, 0 % = as if it were off, in between = both in parallel
xFMX = min(1, max(0, slider88 / 100)); xSMX = min(1, max(0, slider89 / 100)); xDMX = min(1, max(0, slider90 / 100));
xRMX = soA == 3 ? 1 : min(1, max(0, slider91 / 100));
xDML = slider93 < 0.5; xDMR = slider94 < 0.5;            // delay L / R: 1 = heard, 0 = muted
xSON = slider85 >= 0.5;                                   // source model on / off (crossfaded)
xSOX = soA == 2 ? 0 : 1;                                  // delay solo: the source itself leaves the room - only its echoes stay
xSDRY = soA == 0 ? 1 : 0;                                 // any solo: no dry signal (Mix)
xBYPe = soA == 1 || soA == 2 ? 1 : (soA == 3 ? 0 : xBYP);   // source solo and delay solo: room off; room solo: room on
xSWD = min(2, max(0, slider98 / 100));   // source width
xMONO = slider110 >= 0.5 ? 1 : 0;   // input mono mix
xPAN = min(1, max(-1, (slider10 - max(0.8, slider2) / 2) / max(0.1, max(0.8, slider2) / 2 - 0.25)));   // room off: left wall -1 … right wall +1 (the ball keeps 0.25 m from the wall)
// CALL LINE (devices only: Original stays the plain input)
fzDk = fzm > 0 ? min(1, max(0, slider111 / 100)) : 0; fzCA = fzm > 0 ? min(1, max(0, slider112 / 100)) : 0; fzDO = fzm > 0 ? min(1, max(0, slider113 / 100)) : 0;
fzBW = fzm > 0 ? min(3, max(0, floor(slider114 + 0.5))) : 0;
fzBW != lastqlBW ? (
  // call line bandwidth: 1 = narrowband 300 - 3400 Hz, 2 = wideband 100 - 7000 Hz, 3 = super-wideband 50 - 14000 Hz (high-pass + low-pass)
  fzBW > 0 ? ( fdesign(qlC, qlC + 30, 1, fzBW == 1 ? 300 : (fzBW == 2 ? 100 : 50), 4); fdesign(qlC + 10, qlC + 30, 0, fzBW == 1 ? 3400 : (fzBW == 2 ? 7000 : 14000), 8); );
  memset(qlS, 0, 16); lastqlBW = fzBW;
);
qlHF = fzBW == 2 ? 4000 : (fzBW == 3 ? 7000 : 2000);   // noise fill (codec) above
qlOn = fzm > 0 && slider85 >= 0.5 && (fzCA > 0 || fzDO > 0);
qlOn && !qlOnL ? qlclear(); qlOnL = qlOn;              // switched on: no old audio in the frame buffers
dqRF = exp(-1 / (0.03 * srate)); dqKS = 1 - exp(-1 / (0.05 * srate)); dqRP = exp(-1 / (2.5 * srate)); dqHN = floor(0.07 * srate);
dqKA = 1 - exp(-1 / (0.012 * srate)); dqKR = 1 - exp(-1 / (0.5 * srate)); dqKC = 1 - exp(-1 / (0.010 * srate));
dqKO = 1 - exp(-1 / ((0.004 + 0.06 * fzDk) * srate)); dqFl = 10 ^ (-2 * fzDk); dqDN = floor(0.045 * srate); dqCN = floor(0.35 * srate);
xDUK = min(24, max(0, slider99)); dkAt = 1 - exp(-1 / (0.004 * srate)); dkRl = 1 - exp(-1 / (0.25 * srate)); dkEr = exp(-1 / (0.12 * srate));   // delay ducking
xDTY = min(3, max(0, floor(slider115 + 0.5))); xDDV = slider116 >= 0.5 && fzon && slider85 >= 0.5;   // delay type, repeats through the device
dtyA = exp(-2 * $pi * (xDTY == 1 ? 6500 : 3200) / srate);   // tape: one-pole 6.5 kHz per pass; analog (BBD): two poles at 3.2 kHz
dtyH = max(1, floor(srate / 11025 + 0.5));                  // lo-fi: sample and hold at about 11 kHz, 10 bit towards zero (quiet repeats die away, no buzz that hangs on)
xDTP = min(1, max(0, slider83 / 100)); xDDR = min(1, max(0, slider84 / 100));   // tape: wow + flutter, drive
dlbpm = slider74 > 0.5 ? slider74 : (tempo > 1 ? tempo : 120);
dltL = slider67 >= 0.5 ? dnote(slider72) * 60000 / dlbpm : slider70;
dltR = slider68 >= 0.5 ? dltL : (slider67 >= 0.5 ? dnote(slider73) * 60000 / dlbpm : slider71);
xDTL = min(DLMAX - 8, max(2, dltL * srate / 1000)); xDTR = min(DLMAX - 8, max(2, dltR * srate / 1000));
xDFL = min(0.95, max(0, slider75 / 100)); xDFR = slider87 >= 0.5 ? xDFL : min(0.95, max(0, slider76 / 100));   // link feedback: R = L
xDGL = slider77 <= -47.9 ? 0 : 10 ^ (slider77 / 20); xDGR = slider86 >= 0.5 ? xDGL : (slider78 <= -47.9 ? 0 : 10 ^ (slider78 / 20));   // link level: R = L
dllc = min(2000, max(20, slider79)); dlhc = min(20000, max(1000, slider81));
dlrc = slider69 >= 0.5 ? dllc : min(2000, max(20, slider80)); dlrh = slider69 >= 0.5 ? dlhc : min(20000, max(1000, slider82));
dlk = dllc + dlhc * 3.1 + dlrc * 7.3 + dlrh * 11.7;
dlk != dllkey ? (
  fdesign(dlC, dlC + 28, 1, dllc, 2); fdesign(dlC + 5, dlC + 28, 0, dlhc, 2);
  fdesign(dlC + 10, dlC + 28, 1, dlrc, 2); fdesign(dlC + 15, dlC + 28, 0, dlrh, 2);
  dllkey = dlk;
);
cDON < 0.000001 && xDON ? ( cDTL = xDTL; cDTR = xDTR; cDFL = xDFL; cDFR = xDFR; cDGL = xDGL; cDGR = xDGR; );   // switched on: start at the set values (no glide)
// ---- TEMPO ANALYSIS: the GUI asks (counter), the audio listens while playing, 10 s or until stopped ----
anaCmd != anaSeen ? ( anaSeen = anaCmd; anaEm = 0; anaN = 0; anaDone = 0; anaOn = 1; anaPrev = 0; anaAcc = 0; anaCnt = 0; anaXp = 0; anaHop = floor(srate * 0.01 + 0.5); );
anaStp != anaStpSeen ? ( anaStpSeen = anaStp; anaOn ? ( anaOn = 0; anaDone = anaN > 300 ? 1 : -1; ); );
anaOn && anaN >= ANAMAX ? ( anaOn = 0; anaDone = 1; );
k0 = 0; loop(40, abs(fzS[k0]) < 1000000 ? 0 : ( fzS[k0] = 0; ); abs(fzSs[k0]) < 1000000 ? 0 : ( fzSs[k0] = 0; ); k0 += 1; );   // source filter states: clear anything not finite
k0 = 0; loop(120, abs(FZA[k0]) < 1000000 ? 0 : ( FZA[k0] = 0; ); k0 < 80 ? ( abs(FAP[k0]) < 1000000 ? 0 : ( FAP[k0] = 0; ); ); k0 < 44 ? ( abs(FZO[k0]) < 1000000 ? 0 : ( FZO[k0] = 0; ); ); k0 += 1; );
// big jump (preset, snapshot, reset, automation step): crossfade old -> new instead of gliding the delays
// (a glide is a Doppler pitch sweep - right for a moving source, wrong for a scene change)
// DOPPLER (Motion): Off / Natural / boosted x 2 … 16. The extra delay dopA is ADDED TO THE TAP TARGETS
// (every tap, the late reverb): a base L (boost only, up to 0.35 s - keeps the extra above zero when the source comes
// closer than where it started) plus (k - 1) x the change of the direct delay since the motion started (k = 0: the
// direct delay stays put = no pitch change). Small steps (a moving source) glide like any motion = the Doppler sweep;
// a jump of the base (switching the boost, Size) is larger than JTHR and becomes the usual scene crossfade - no pitch
// dive, no lasting detune when Motion stops (before, the base glided at 8 % pitch)
dopK = min(16, max(0, slider118)); abs(dopK - 1) < 0.05 ? dopK = 1;
dopL = dopK > 1 ? min(0.35 * srate, (dopK - 1) * (2 * max(0.5, slider104 * min(4, max(0.25, slider31))) + 1) / C_SND * srate) : 0;
dopRaw = xDL[0] - dopA;
dopMx = 0; k0 = 0; loop(NT, dopMx = max(dopMx, max(xDL[k0], xDR[k0]) - dopA); k0 += 1; ); dopMx = max(dopMx, xLD - dopA);
// DRAGGING BY HAND (no motion running): slider135 = Off takes the Doppler out of a drag - the direct delay is held where it was
// when the switch went off (the same compensation as Motion's Doppler Off); the distance still changes level, filter and room
!(slider135 == slider135) ? slider135 = 0; dopHo = slider135 >= 0.5 && !mtRun;
dopRq || !dopWas || (dopHo && mtRun < 0.5 && dopMw) ? ( dopRef = dopRaw; dopRq = 0; ); dopWas = (dopK != 1 && mtRun) || dopHo; dopMw = mtRun;   // (Motion stops, the hand switch takes over: a new reference)
dopT = dopK != 1 && mtRun ? dopL + (dopK - 1) * (dopRaw - dopRef) * mtE : (dopHo ? -(dopRaw - dopRef) : dopL);
dopT = max(dopK < 1 || dopHo ? 2 - dopRaw : 0, min(min(dopK < 1 || dopHo ? MAXIN : 2 * dopL + 1, MAXIN - 16 - dopMx), dopT));
dopD = dopT - dopA;
abs(dopD) > 0.0001 ? ( k0 = 0; loop(NT, xDL[k0] += dopD; xDR[k0] += dopD; k0 += 1; ); xLD += dopD; xLD2 += dopD; dopA = dopT; );
!snap ? (
  bflip = bon && bsame != lastbsame; lastbsame = bsame;   // source or listener crossed the barrier plane - every tap changes its nature
// a running motion moves the taps by up to (speed x Doppler factor) per block - that is the Doppler sweep, never a
  // scene jump (a fast pass-by with × 6 / × 12 crossed JTHR and was crossfaded: the sine jumped). A jump of the Doppler base
  // (factor or Size changed) is still a crossfade (dopLj)
  dopLj = abs(dopL - dopLp) > 0.5 || dopHo != dopHp; dopLp = dopL; dopHp = dopHo;   // (the hand switch flipped: the held delay goes back in a crossfade, not a sweep)
  JTHRm = mtRun ? JTHR * 4 * max(1, dopK) : JTHR;
  jmp = abs(xLD - cLD) > JTHRm || bflip || dopLj;
  k0 = 0; loop(NT, abs(xDL[k0] - cDL[k0]) > JTHRm ? jmp = 1; k0 += 1; );   // (also during a motion - its steps stay far below 1 m per block, but crossing a barrier IS a scene change)
  k0 = 0; loop(8, abs(flen[k0] - cFL[k0]) > JTHRF ? jmp = 1; k0 += 1; );
  xLG2 > 0 ? ( abs(xLD2 - cLD2) > JTHR ? jmp = 1; k0 = 0; loop(4, abs(flen2[k0] - cFL2[k0]) > JTHRF ? jmp = 1; k0 += 1; ); );
  // Two rules keep a scene change free of clicks and bumps:
  // 1. a crossfade is never restarted while one is running - the delays hold still until it is done, then the next
  //    crossfade leads to the newest state (restarting cut the old set off and jumped the half-faded one to full level)
  // 2. whatever did not move stays out of the crossfade - two copies of the same sound (e.g. the direct sound while only
  //    the walls move) would add up to +3 dB in the middle of an equal-power fade
  jmp ? (
    xfade > 0 ? ( dfrz = 1; ) : (
      dfrz = 0;
      k0 = 0;
      loop(NT,
        tSt[k0] = abs(xDL[k0] - cDL[k0]) < JTS && !bflip;                  // (a barrier flip: even a tap whose delay stays is crossfaded - its level and filter jump)
        tSt[k0] ? ( oGL[k0] = oGR[k0] = 0; ) : (
          oDL[k0] = cDL[k0]; oDR[k0] = cDR[k0]; oGL[k0] = cGL[k0]; oGR[k0] = cGR[k0]; oA[k0] = cA[k0]; oSM[k0] = cSM[k0];
          olpL[k0] = lpL[k0]; olpR[k0] = lpR[k0];
          k0 < NIMG ? ( oHL[k0] = cHL[k0]; oHR[k0] = cHR[k0]; ohdsL[k0] = hdsL[k0]; ohdsR[k0] = hdsR[k0]; );
          cDL[k0] = xDL[k0]; cDR[k0] = xDR[k0]; cGL[k0] = xGL[k0]; cGR[k0] = xGR[k0];
        );
        k0 += 1;
      );
      ldS = abs(xLD - cLD) < JTS; !ldS ? ( oLD = cLD; cLD = xLD; );
      ld2S = abs(xLD2 - cLD2) < JTS; !ld2S ? ( oLD2 = cLD2; cLD2 = xLD2; );
      k0 = 0; loop(4, xfS[8 + k0] = abs(flen2[k0] - cFL2[k0]) < JTS; !xfS[8 + k0] ? ( oFL2[k0] = cFL2[k0]; cFL2[k0] = flen2[k0]; ); k0 += 1; );
      k0 = 0; loop(8, xfS[k0] = abs(flen[k0] - cFL[k0]) < JTS; !xfS[k0] ? ( oFL[k0] = cFL[k0]; cFL[k0] = flen[k0]; ); k0 += 1; );
      // The reverb network changes room at once - three things keep that from producing a burst:
      // 1. its input level jumps to the new room's value now (a small room feeds its reverb far hotter than a hall; that
      //    level must not keep feeding lines that already have the hall's long decay)
      // 2. the lines of the new room stay silent until sound written AFTER the change reaches their (new, often much longer)
      //    read position - otherwise they would replay what the old room wrote up to 0.1 s ago, at the old room's level
      // 3. the old room's tail fades out over the crossfade and is fed on at the level it would have in the new room (xfR)
      xfR = cTK > 0.000000000001 ? min(1, sqrt(xTK / cTK)) : 1; cTK = xTK; tkn = 0;
      // only when the new room's reverb is clearly quieter (more than 3 dB): between similar rooms - e.g. while a size knob
      // is turned - the lines keep reading on without a gap
      xfG = xfR < 0.7; xfcnt = 0;
      // a room with a longer decay is fed far more gently (vocal booth -> staircase: about 20 dB less input): the lower input
      // level applies at once - fed for even 30 ms at the old, hot level, the nearly lossless new lines would build up a burst
      xLG < cLG || xfG ? ( cLG = xLG; );
      xfade = 1;
    );
  ) : ( dfrz = 0; );
);
// DIFFUSION ALLPASSES: their lengths follow the room size - a new length is crossfaded in over 20 ms (the read jump was a click)
apX <= 0 ? ( apNew = 0; k0 = 0; loop(4, apCur[k0] != apLen[k0] ? apNew = 1; k0 += 1; );
  apNew ? ( k0 = 0; loop(4, apOld[k0] = apCur[k0]; apCur[k0] = apLen[k0]; k0 += 1; ); apX = 1; apgN = 0; apgO = 1; apStp = 1 / (0.02 * srate); ); );
mtRun ? ( dfrz = 0; );   // a motion keeps its taps moving through a crossfade (frozen, they would catch up in one block: a chirp)
smd = dfrz ? 0 : sm; smfd = dfrz ? 0 : smf;        // waiting for a running crossfade: delays hold still
// burst guard: scale what this block will read (newest read = write position - shortest delay)
k0 = 0;
loop(12,
  scE[k0] > scC[k0] || scN[k0] > scD[k0] ? (
    k0 < 8 ? ( scd1 = cFL[k0]; scd2 = xfade > 0 && !xfS[k0] ? oFL[k0] : cFL[k0]; ) : ( scd1 = cFL2[k0 - 8]; scd2 = xfade > 0 && !xfS[k0] ? oFL2[k0 - 8] : cFL2[k0 - 8]; );
    scstep(k0, min(min(scd1, scd2), k0 < 8 ? flen[k0] : flen2[k0 - 8]) - 2, max(max(scd1, scd2), k0 < 8 ? flen[k0] : flen2[k0 - 8]), xfade > 0 || abs(scd1 - (k0 < 8 ? flen[k0] : flen2[k0 - 8])) > 1);
  );
  k0 += 1;
);
// A tail that is still ringing was fed for the OLD room. When the new room has a quieter reverb (small room -> hall, LATE
// turned down, free field), the old tail would ring on far too loud with the new, long decay. So the contents of the reverb
// lines are brought down to the level they would have in the new room - one line per block, each as a ramp along the line (see).
// only a JUMP (preset, snapshot: the new room's tail at least 6 dB quieter) is matched - a Decay or Late automation
// moves in small steps, and those the loop gain handles by itself (the 3 % threshold fired on every block of a decay sweep
// and the 16 content steps per line were heard as a zipper). And the match is a RAMP along the line: factor 1 at the sample
// read next, the full ratio at the write head - the level glides down over one line length instead of stepping
tkn == 0 ? (
  xTK < cTK * 0.25 ? ( tkstep = sqrt(max(0.000001, xTK) / max(0.000001, cTK)); tkn = 8; );   // (amplitude ratio)
  cTK = xTK;
);
tkn > 0 ? (
  tkl = 8 - tkn; tkp = fdnbase + tkl * LINEMAX; tkLn = floor(cFL[tkl] + 2 * moddep + 6); tkq = fwi - tkLn; tkq < 0 ? tkq += LINEMAX; i0 = 0;
  loop(tkLn, tkp[tkq] *= 1 + (tkstep - 1) * i0 / tkLn; tkq += 1; tkq >= LINEMAX ? tkq = 0; i0 += 1; );
  tkn -= 1;                                                      // (the filter states are not scaled - they follow within a few samples, and scaling them stepped)
);
// block-rate one-pole smoothing for the tap filters below: smb (30 ms) for the share of unfiltered highs, smfw (50 ms) for the corner frequency in log(w)
smb = 1 - exp(-samplesblock / (0.03 * srate)); smfw = 1 - exp(-samplesblock / (0.05 * srate));
// decay crossfade: do the loop-filter coefficients differ from what the lines run on? Then a straight line to them
fqmov = 0; k0 = 0; loop(240, abs(fqC[k0] - fqCc[k0]) > 0.0000001 ? fqmov = 1; k0 += 1; );
fqmov ? ( k0 = 0; loop(240, fqCd[k0] = (fqC[k0] - fqCc[k0]) / samplesblock; k0 += 1; ); );
mdcmov = 0; k0 = 0; loop(mdN, abs(mdM[k0 * 6] - mdC0[k0]) > 0.000000001 || abs(mdM[k0 * 6 + 1] - mdC1[k0]) > 0.000000001 ? ( mdcmov = 1; mdD0[k0] = (mdM[k0 * 6] - mdC0[k0]) / samplesblock; mdD1[k0] = (mdM[k0 * 6 + 1] - mdC1[k0]) / samplesblock; ) : ( mdD0[k0] = mdD1[k0] = 0; ); k0 += 1; );
// SLEEP: the input has been silent and the output below -140 dB for longer than every delay in the plugin (taps, reverb
// lines, delay with feedback): @sample does nothing until the input comes back (no CPU while a track is silent). Asleep,
// every smoothed value goes straight to its target - the first sound after the pause starts exactly where it should.
SLIN = 0.00000001; SLOUT = 0.0000002;
slhold = MAXIN + 2 * LINEMAX + (xDON || dlhold > 0 ? DLMAX : 0) + floor(srate * 0.5);
slp ? (
  snap = 1; xfade = 0; xfG = 0;
  mp = mdM; loop(mdN, mp[2] = mp[5]; mp += 6; ); mdmov = 0;
  pkIL = pkIR = pkOL = pkOR = 0; lmgv = 1;
);
// ROOM MODES: their gains are smoothed per sample only while they move (within 1e-12: arrived)
!slp ? ( mdmov = 0; mp = mdM; loop(mdN, abs(mp[5] - mp[2]) > 0.000000000001 ? ( mdmov = 1; ) : ( mp[2] = mp[5]; ); mp += 6; ); );
// taps settled (delays within 0.001 sample, gains within -100 dB of their targets): snap them and skip the per-sample smoothing
tmov = 0; k0 = 0;
loop(NT,
  abs(xDL[k0] - cDL[k0]) > 0.001 || abs(xDR[k0] - cDR[k0]) > 0.001 || abs(xGL[k0] - cGL[k0]) > 0.00001 * (xGL[k0] + 0.00001) || abs(xGR[k0] - cGR[k0]) > 0.00001 * (xGR[k0] + 0.00001) ? ( tmov = 1; ) : (
    smd > 0 ? ( cDL[k0] = xDL[k0]; cDR[k0] = xDR[k0]; ); cGL[k0] = xGL[k0]; cGR[k0] = xGR[k0]; );
  k0 += 1;
);
dfrz ? ( tmov = 1; );
// MOTION = DOPPLER: while a motion runs, every tap's delay and gains move in a STRAIGHT LINE through the block to the
// new target (constant velocity = a clean pitch shift) instead of the 30 ms one-pole glide (a staircase smoothed per block,
// which modulated the pitch at the block rate - audible as roughness on a circling source)
mtRmp = mtRun && tmov;
mtRmp ? ( k0 = 0; loop(NT, dDL[k0] = (xDL[k0] - cDL[k0]) / samplesblock; dDR[k0] = (xDR[k0] - cDR[k0]) / samplesblock; k0 += 1; ); );
// WALL / AIR FILTERS of the taps (one-pole low-pass cA, share of the unfiltered highs cSM) glide evenly in OCTAVES: the
// corner frequency is smoothed in log(w) (w = -ln cA, 50 ms) at block rate, and per sample cA / cSM go in a straight line to
// the value at the end of the block. Smoothed in cA itself, the corner jumped from 30 Hz to several kHz in about 2 ms when a
// barrier passed the listener (blocked -> free) - a 20 dB step in the mids, heard as a short distortion.
// (the log corner is only computed while a tap moves - settled taps cost one comparison; -1000 = start from the current cA)
fmov = 0; k0 = 0;
loop(NT,
  abs(xA[k0] - cA[k0]) > 0.000001 || abs(xSM[k0] - cSM[k0]) > 0.000001 ? (
    fmov = 1;
    flwx = log(-log(min(0.999999, max(0.000001, xA[k0]))));
    fcLog[k0] < -999 ? ( fcLog[k0] = log(-log(min(0.999999, max(0.000001, cA[k0])))); );
    fcLog[k0] += (flwx - fcLog[k0]) * smfw; fend = abs(fcLog[k0] - flwx) < 0.001 ? xA[k0] : exp(-exp(fcLog[k0]));
    fdA[k0] = (fend - cA[k0]) / samplesblock;
    fdSM[k0] = (cSM[k0] + (xSM[k0] - cSM[k0]) * smb - cSM[k0]) / samplesblock;
  ) : ( cA[k0] = xA[k0]; cSM[k0] = xSM[k0]; fcLog[k0] = -1000; fdA[k0] = fdSM[k0] = 0; );
  k0 += 1;
);
// SNAP (playback start, wake-up from sleep, sample-rate change): every glided / crossfaded state (c* = current, x* = target) jumps to its target at once
snap ? (
  k0 = 0;
  loop(NT,
    cDL[k0] = xDL[k0]; cDR[k0] = xDR[k0]; cGL[k0] = xGL[k0]; cGR[k0] = xGR[k0]; cA[k0] = xA[k0]; cSM[k0] = xSM[k0];
    fcLog[k0] = -1000; fdA[k0] = fdSM[k0] = 0; k0 < NIMG ? ( cHL[k0] = xHL[k0]; cHR[k0] = xHR[k0]; );
    k0 += 1;
  );
  k0 = 0; loop(4, apCur[k0] = apLen[k0]; apOld[k0] = apLen[k0]; k0 += 1; ); apX = 0; memcpy(fqGc, fqG, 8); memcpy(fqCc, fqC, 240); fqmov = 0;
  cSTW = xSTW; cLD = xLD; cLG = xLG; cEQL = xEQL; cTK = xTK; cBNG = xBNG; cOG = xOG; cMIX = xMIX; cBYP = xBYPe; cSON = xSON; cFMX = xFMX; cSMX = xSMX; cDMX = xDMX; cRMX = xRMX; cDML = xDML; cDMR = xDMR; cSOX = xSOX; cSDRY = xSDRY; cDEN = xDEN; cIG = xIG; cTH = xTH; sTH = xSH;
  k0 = 0; loop(8, cFL[k0] = flen[k0]; k0 += 1; );
  k0 = 0; loop(4, cFL2[k0] = flen2[k0]; k0 += 1; ); cLD2 = xLD2; cLG2 = xLG2; cP2L = xP2L; cP2R = xP2R; cLLP2 = xLLP2; cLDf = xLDf; cLPL = xLPL; cLPR = xLPR;
  cLLP = xLLP; cLM = xLM; cMuD = tMuD; cMuE = tMuE; cMuL = tMuL; cMD = mdN > 0 && !mdOff && !mdPend;
  cDON = xDON; cDTL = xDTL; cDTR = xDTR; cDFL = xDFL; cDFR = xDFR; cDGL = xDGL; cDGR = xDGR; cDTP = xDTP; cDDR = xDDR;
  snap = 0; dfrz = 0; smd = sm; smfd = smf; xfcnt = 1000000000; cXR = 1;
);


// ROOM OFF + MOTION: the stereo panner path gets the Doppler too - its delay is the travel time source -> listener
// (x the Doppler factor: Natural = 1, boosted × 2 … × 16), only while the motion runs and the room is off. Switching on / off
// and a change of the factor = a short crossfade (no pitch dive); Motion off = the panner exactly as before (no delay)
bpdK = min(16, max(0, slider118)); abs(bpdK - 1) < 0.05 ? bpdK = 1;
bpdOn = bpdK > 0 && mtRun && xBYPe >= 0.5;
bpdDs = sqrt(sqr((slider10 - slider13) * rs) + sqr((slider11 - slider14) * rs) + sqr(slider12 - slider15));
bpdT = bpdOn ? max(2, min(65536 - 8 - samplesblock, bpdK * bpdDs / C_SND * srate)) : 0;
bpdOn != bpdOnP || (bpdOn && bpdK != bpdKP) ? ( bpdO = bpdC; bpdC = bpdT; bpdI = 0; bpdF = 0; bpdFI = 1 / max(1, 0.03 * srate); bpdOnP = bpdOn; bpdKP = bpdK; )
                                            : ( bpdI = (bpdT - bpdC) / max(1, samplesblock); );

@sample
slp && abs(spl0) + abs(spl1) < SLIN ? ( spl0 = 0; spl1 = 0; ) : (
slp = 0;
inL = spl0; inR = spl1;
!(abs(inL) < 1000000) ? inL = 0; !(abs(inR) < 1000000) ? inR = 0;   // NaN / Inf / absurd values from the host or another plugin never get in
cIG += (xIG - cIG) * sm; inL *= cIG; inR *= cIG;          // INPUT gain (-inf … +48 dB)
// INPUT GUARD (vopt[68], off by default - Original stays sample-exact): a soft limiter right after the input gain. Up to -1 dBFS
// nothing changes; above, the peak bends smoothly (slope 1 at the knee) towards 0 dBFS and never passes it - a hot input (+48 dB gain,
// a clipped file) cannot drive the reverb network into a blast.
vopt[68] >= 0.5 ? (
  igA = abs(inL); igA > igT ? ( igE = (igA - igT) * igK; inL = (inL < 0 ? -1 : 1) * (igT + (1 - igT) * igE / (1 + igE)); );
  igA = abs(inR); igA > igT ? ( igE = (igA - igT) * igK; inR = (inR < 0 ? -1 : 1) * (igT + (1 - igT) * igE / (1 + igE)); );
);
// INPUT MONO MIX: (L + R) / 2 on both channels before any processing (smoothed switch); the mid / side energy of the
// input (0.3 s) tells the GUI whether a stereo signal comes in at all
cMONO += (xMONO - cMONO) * sm; cMONO > 0.00001 ? ( inM = (inL + inR) * 0.5; inL += (inM - inL) * cMONO; inR += (inM - inR) * cMONO; );
sdE = sdE * crk + sqr(inL - inR) * 0.25; mdE = mdE * crk + sqr(inL + inR) * 0.25;
pkIL = max(abs(inL), pkIL * pmdec); pkIR = max(abs(inR), pkIR * pmdec); spIn = (inL + inR) * 0.5;
fany = (lcon || hcon) && !fbyp;
cFMX += (xFMX - cFMX) * sm;
!fpost && fany ? ( fmL = fchain(inL, lcSL, lcFL, hcSL, hcFL); fmR = fchain(inR, lcSR, lcFR, hcSR, hcFR);
  cFMX >= 0.99999 ? ( inL += (fmL - inL) * cFMX; inR += (fmR - inR) * cFMX; )
                  : ( inL = fmL * cFMX + fapath(inL, FAP) * (1 - cFMX); inR = fmR * cFMX + fapath(inR, FAP + 40) * (1 - cFMX); );   // Mix: phase-matched dry part
);
// ORIGINAL + DISTORTION: left and right saturated each on their own (see odsat); the source uses sdrL / sdrR, the dry
// path of Dry / wet keeps the input
odOn ? ( odk = cSON * cSMX; odDL = odlin(inL, odS + 80); odDR = odlin(inR, odS + 120);
         sdrL = odDL + (odsat(inL, odS) - odDL) * odk; sdrR = odDR + (odsat(inR, odS + 40) - odDR) * odk; ) : ( sdrL = inL; sdrR = inR; );
xsrc = (sdrL + sdrR) * 0.5;
qlOn ? ( qlXd = qlDM[qlDP]; qlDM[qlDP] = xsrc; qlSd = qlDS[qlDP]; qlDS[qlDP] = (sdrL - sdrR) * 0.5; qlDP += 1; qlDP >= QLN ? qlDP = 0; );   // dry part of a Mix, one frame late
cSON += (xSON - cSON) * sm;
cSMX += (xSMX - cSMX) * sm; sonk = cSON * cSMX;
fzon && sonk > 0.00001 ? ( fzx = fzrun(xsrc);   // source model colors the source before the room (on / off: crossfade)
  cSMX >= 0.99999 ? ( xsrc = sonk >= 1 ? fzx : xsrc + (fzx - xsrc) * sonk; )
                  : ( fzmx = fzx * cSMX + fzap(qlOn ? qlXd : xsrc, FZA, FZO) * (1 - cSMX); xsrc += (fzmx - xsrc) * cSON; );   // Mix: phase-matched dry part
);
// STEREO WIDTH of the input (left - right): the room places the mid as its source; the side travels with the direct sound
// (same distance, level, air and barrier filters - see sbuf) and passes as it is with the room off. A device model (phone,
// radio …) is a mono loudspeaker: its side fades out with the model.
sideIn = (sdrL - sdrR) * 0.5;
// Original with its EQ / high-pass / low-pass: the side runs through the SAME linear filters as the mid (own states) - left and
// right are filtered identically (an unfiltered side would let L and R drift apart in level and phase)
fzon && sonk > 0.00001 ? (
  cSMX >= 0.99999 ? ( fzm > 0 ? ( sideIn *= 1 - sonk; ) : ( fzsx = fzside(sideIn); sideIn = sonk >= 1 ? fzsx : sideIn + (fzsx - sideIn) * sonk; ); )
                  : ( fzsx = (fzm > 0 ? 0 : fzside(sideIn)) * cSMX + fzap(qlOn ? qlSd : sideIn, FZA + 60, FZO + 22) * (1 - cSMX); sideIn += (fzsx - sideIn) * cSON; );   // Mix: the same dry path as the mid
);
abs(sideIn) > 0.000000001 ? ( shold = MAXIN; ) : ( shold > 0 ? shold -= 1; );
// TEMPO ANALYSIS: onset strength of the input (log-energy rise of the first difference, 10 ms frames) while playing
// onset: energy against its 0.5 s mean, log(1 + x) - loud hits count more than quiet ones (log(x) made a soft hi-hat as
// strong as the kick: 90 BPM read as 60), and the same at any input level
anaOn && (play_state & 1) ? (
  anaD = (spl0 + spl1) * 0.5; anaAcc += sqr(anaD - anaXp); anaXp = anaD; anaCnt += 1;
  anaCnt >= anaHop ? ( anaEm = anaEm > 0 ? anaEm + (anaAcc - anaEm) * 0.02 : anaAcc; anaLe = log(1 + anaAcc / max(0.000000000001, anaEm)); anaN < ANAMAX ? ( anaNv[anaN] = max(0, anaLe - anaPrev); anaN += 1; ); anaPrev = anaLe; anaAcc = 0; anaCnt = 0; );
);
// DELAY (before the room): L / R lines with feedback through low cut + high cut (each repeat darker and thinner), stereo or
// ping-pong. The echoes go into the room like the source itself (mid), their left / right difference (side) joins the direct
// sound. Off: nothing of this runs - the room gets exactly the signal it got before.
cDON += (xDON - cDON) * sm;
cSWD += (xSWD - cSWD) * sm;
cSOX += (xSOX - cSOX) * sm; cSDRY += (xSDRY - cSDRY) * sm;
dyL = dyR = 0; xroom = xsrc * cSOX;
cDON > 0.00001 || dlhold > 0 ? (
  cDON > 0.00001 ? ( dlhold = MAXIN; ) : ( dlhold -= 1; );
  cDTL += (xDTL - cDTL) * dlsm; cDTR += (xDTR - cDTR) * dlsm;
  cDFL += (xDFL - cDFL) * sm; cDFR += (xDFR - cDFR) * sm; cDGL += (xDGL - cDGL) * sm; cDGR += (xDGR - cDGR) * sm;
  // TAPE: wow (about 0.55 Hz) + flutter (about 6.3 Hz) move the playback head - together up to about 1 % pitch at 100 %,
  // left and right slightly different; the head only moves further back, never ahead of the set time
  cDTP += (xDTP - cDTP) * sm; cDDR += (xDDR - cDDR) * sm;
  cDTP > 0.0001 ? (
    dwp1 += dwr1; dwp1 > 6.2831853 ? dwp1 -= 6.2831853; dwp2 += dwr2; dwp2 > 6.2831853 ? dwp2 -= 6.2831853;
    dwp3 += dwr3; dwp3 > 6.2831853 ? dwp3 -= 6.2831853; dwp4 += dwr4; dwp4 > 6.2831853 ? dwp4 -= 6.2831853;
    dmL = cDTP * (dwA * (1 + sin(dwp1)) + dfA * (1 + sin(dwp3))); dmR = cDTP * (dwA * (1 + sin(dwp2)) + dfA * (1 + sin(dwp4)));
  ) : ( dmL = dmR = 0; );
  drL = bqrun(bqrun(rdh(dlL, DLMAX, dwi - cDTL - dmL), dlC, dlSt, 1), dlC + 5, dlSt + 2, 1);
  drR = bqrun(bqrun(rdh(dlR, DLMAX, dwi - cDTR - dmR), dlC + 10, dlSt + 4, 1), dlC + 15, dlSt + 6, 1);
// DELAY TYPE: what every pass through the loop does to the repeat (it adds up: each echo is a copy of the one before)
  xDTY == 1 ? ( dtL1 = drL + (dtL1 - drL) * dtyA; dtR1 = drR + (dtR1 - drR) * dtyA; drL = tnr(1.4 * dtL1) / 1.4; drR = tnr(1.4 * dtR1) / 1.4; ) : (   // tape: darker, round
  xDTY == 2 ? ( dtL1 = drL + (dtL1 - drL) * dtyA; dtL2 = dtL1 + (dtL2 - dtL1) * dtyA; dtR1 = drR + (dtR1 - drR) * dtyA; dtR2 = dtR1 + (dtR2 - dtR1) * dtyA;
                drL = dtL2 / (1 + 0.25 * dtL2 * dtL2); drR = dtR2 / (1 + 0.25 * dtR2 * dtR2); ) : (                                                      // analog: dark, soft clip
  xDTY == 3 ? ( dtyC += 1; dtyC >= dtyH ? ( dtyC = 0; dtL1 = (drL < 0 ? -1 : 1) * floor(abs(drL) * 512) / 512; dtR1 = (drR < 0 ? -1 : 1) * floor(abs(drR) * 512) / 512; ); drL = dtL1; drR = dtR1; ); ) );   // lo-fi
// REPEATS THROUGH THE DEVICE: every echo passes the source model's filter again (normalized to its peak: never louder)
  xDDV ? ( drL = dvlin(drL, dvS) * dvPk; drR = dvlin(drR, dvS + 40) * dvPk; );
  dfbL = drL * cDFL; dfbL /= 1 + 0.1 * dfbL * dfbL; dfbR = drR * cDFR; dfbR /= 1 + 0.1 * dfbR * dfbR;   // soft limit: never runs away
  dxin = xsrc * cDON;
  // PING-PONG: the input goes left; every echo crosses to the other side - left -> right at full level (the right echo is
  // as loud as the left one before it), right -> left with the feedback (both feedback knobs: their geometric mean), so
  // FEEDBACK sets how often an L + R pair repeats. Before: left -> right through the feedback too - the right side came
  // 10 dB lower and the delay sounded as if it worked on the left only
  dlpp ? ( dpa = drL / (1 + 0.1 * drL * drL); dpb = drR * sqrt(cDFL * cDFR); dpb /= 1 + 0.1 * dpb * dpb; dwL = dxin + dpb; dwR = dpa; )
       : ( dxs = sideIn * cDON * cSWD; dwL = dxin + dxs + dfbL; dwR = dxin - dxs + dfbR; );   // the input's stereo width reaches the echoes
  // TAPE DRIVE: the record head saturates (tanh, gain 1 for quiet signals - the loop never gets louder); knee from 0 dBFS
  // (0 %) down to about -14 dBFS (100 %): loud repeats get rounder and denser, quiet ones stay clean
  cDDR > 0.0001 ? ( dk = 1 + 4 * cDDR; dwL = tnr(dk * dwL) / dk; dwR = tnr(dk * dwR) / dk; );
  dlL[dwi] = dwL; dlR[dwi] = dwR;
  cDMX += (xDMX - cDMX) * sm;
  cDML += (xDML - cDML) * sm; cDMR += (xDMR - cDMR) * sm;
  xDUK > 0.001 || cDUK < 0.999999 ? (
    dkE = max(abs(xsrc), dkE * dkEr);                                 // input envelope
    dkT = xDUK > 0.001 ? 10 ^ (-xDUK / 20 * min(1, dkE * 4)) : 1;     // full ducking from -12 dBFS input
    cDUK += (dkT - cDUK) * (dkT < cDUK ? dkAt : dkRl);
    dyL = drL * cDGL * cDON * cDMX * cDML * cDUK; dyR = drR * cDGR * cDON * cDMX * cDMR * cDUK;
  ) : (
  dyL = drL * cDGL * cDON * cDMX * cDML; dyR = drR * cDGR * cDON * cDMX * cDMR;
  );
  xroom = xsrc * cSOX + (dyL + dyR) * 0.5; sbuf[wi] = (dyL - dyR) * 0.5 + sideIn * cSOX * cSWD;
) : ( dlL[dwi] = 0; dlR[dwi] = 0; sbuf[wi] = sideIn * cSOX * cSWD; );
dwi += 1; dwi >= DLMAX ? dwi = 0;
bpM1 = bpM; bpS1 = bpS; bpM = xroom; bpS = sbuf[wi];   // Room off plays bpM1 / bpS1 (one sample, as the direct sound) // the room input as it is (mid incl. echoes, side) - for Room off
BPDM[bpdW] = bpM1; BPDS[bpdW] = bpS1; bpMo = bpM1; bpSo = bpS1;
bpdC > 0 || bpdF < 1 ? (
  bpdC += bpdI; bpdC > 0.5 ? ( bpMo = bpdrd(BPDM, bpdC); bpSo = bpdrd(BPDS, bpdC); );
  bpdF < 1 ? ( bpdF = min(1, bpdF + bpdFI); bpdO > 0.5 ? ( bpdOm = bpdrd(BPDM, bpdO); bpdOs = bpdrd(BPDS, bpdO); ) : ( bpdOm = bpM1; bpdOs = bpS1; );
    bpMo = bpdOm + (bpMo - bpdOm) * bpdF; bpSo = bpdOs + (bpSo - bpdOs) * bpdF; );
);
bpdW = (bpdW + 1) & 65535;
xin = xroom + dn; dn = -dn;
ibuf[wi] = xin; wiD = wi;   // (the Doppler extra delay sits in the tap targets now)

// ROOM SKIP: Room off (or its mix at 0 %) and the fade done - taps, reverb network, neighbor room and modes do not run.
// Back on: the reverb starts empty (as if the room had just been switched on) and fades in with the room bypass.
rmskip = (xBYPe >= 0.5 && cBYP >= 0.99999) || (xRMX <= 0 && cRMX <= 0.00001);
rmskip ? ( rmwas = 1; dirL = dirR = erL = erR = 0; lateL = lateR = 0;
// room off: tap 0 (the direct sound) alone keeps running - from the positions, with its glide / motion ramps (the result bdL / bdR is no longer used by the output)
  mtRmp ? ( cDL[0] += dDL[0]; cDR[0] += dDR[0]; gsm = smm; ) : ( cDL[0] += (xDL[0] - cDL[0]) * sm; cDR[0] += (xDR[0] - cDR[0]) * sm; gsm = xGL[0] > cGL[0] ? smu : sm; );
  gnx = cGL[0] + (xGL[0] - cGL[0]) * gsm; gnx > cGL[0] * gRK + 0.00001 ? gnx = cGL[0] * gRK + 0.00001; cGL[0] = min(gnx, max(xGL[0], cGL[0]));
  gnx = cGR[0] + (xGR[0] - cGR[0]) * gsm; gnx > cGR[0] * gRK + 0.00001 ? gnx = cGR[0] * gRK + 0.00001; cGR[0] = min(gnx, max(xGR[0], cGR[0]));
  cA[0] += (xA[0] - cA[0]) * sm; cSM[0] = xSM[0];
  bdv = rdh(ibuf, MAXIN, wiD - cDL[0]); lpL[0] = bdv + (lpL[0] - bdv) * cA[0]; bdv = lpL[0] + (bdv - lpL[0]) * cSM[0]; bdL = bdv * cGL[0];
  bdv = rdh(ibuf, MAXIN, wiD - cDR[0]); lpR[0] = bdv + (lpR[0] - bdv) * cA[0]; bdv = lpR[0] + (bdv - lpR[0]) * cSM[0]; bdR = bdv * cGR[0];
  cHL[0] += (xHL[0] - cHL[0]) * sm; cHR[0] += (xHR[0] - cHR[0]) * sm; hdsL[0] = bdL + (hdsL[0] - bdL) * cHL[0]; bdL = hdsL[0]; hdsR[0] = bdR + (hdsR[0] - bdR) * cHR[0]; bdR = hdsR[0];   // head shadow
  cNF += (xNF - cNF) * sm; nfSL = bdL + (nfSL - bdL) * nfA; bdL += cNF * nfSL; nfSR = bdR + (nfSR - bdR) * nfA; bdR += cNF * nfSR;   // near field
) : (
rmwas ? (
  rmwas = 0;
  memset(fdnbase, 0, 8 * LINEMAX); memset(fdn2base, 0, 4 * LINEMAX); memset(apbase, 0, 4 * APMAX); memset(edB0, 0, 4 * EDN);
  memset(lpL, 0, 2 * TS); memset(olpL, 0, 2 * TS); memset(fst, 0, 8); memset(fhs, 0, 8); memset(fls, 0, 8); memset(fqS, 0, 96); memcpy(fqCc, fqC, 240); fqmov = 0;
  memset(fst2, 0, 8); memset(fhs2, 0, 8); memset(fls2, 0, 8); memset(stS, 0, 16);
  lpLt = lpLt2 = 0; lpSd = 0; bndS = 0; dirS = dirB = 0;
  k0 = 0; loop(128, mdM[k0 * 6 + 3] = mdM[k0 * 6 + 4] = 0; mdM[k0 * 6 + 2] = mdM[k0 * 6 + 5]; k0 += 1; ); memset(mdXS, 0, 24); mdx1 = mdx2 = 0;
  k0 = 0; loop(NT, cDL[k0] = xDL[k0]; cGL[k0] = xGL[k0]; cGR[k0] = xGR[k0]; cA[k0] = xA[k0]; cSM[k0] = xSM[k0]; fdA[k0] = fdSM[k0] = 0; k0 += 1; );
  memcpy(cHL, xHL, HSN); memcpy(cHR, xHR, HSN); memset(hdsL, 0, 2 * HSN); nfSL = nfSR = 0;
  k0 = 0; loop(8, cFL[k0] = flen[k0]; k0 += 1; ); k0 = 0; loop(4, cFL2[k0] = flen2[k0]; k0 += 1; );
  cLD = xLD; cLD2 = xLD2; xfade = 0; xfG = 0;
);
dirL = dirR = erL = erR = 0; sdL = sdR = seL = seR = 0;
ti = 0;
loop(NT,
  tmov ? (                                    // (settled taps - nothing moving - skip the smoothing, see @block)
    mtRmp ? ( cDL[ti] += dDL[ti]; cDR[ti] += dDR[ti]; gsm = smm; ) : (   // motion: straight delay ramps (Doppler); the gains on a 12 ms one-pole
    cDL[ti] += (xDL[ti] - cDL[ti]) * smd; cDR[ti] += (xDR[ti] - cDR[ti]) * smd;   // (direct + first order: the far ear later; the rest share one delay)
    gsm = xGL[ti] + xGR[ti] > cGL[ti] + cGR[ti] ? smu : sm; );     // louder: slowly, quieter: at once
// a gain that RISES from nearly nothing (a source coming out from behind a wall) climbs at most 3 dB per ms -
    // a one-pole in linear amplitude jumped 8 dB in the first millisecond, which clicked
    gnx = cGL[ti] + (xGL[ti] - cGL[ti]) * gsm; gnx > cGL[ti] * gRK + 0.00001 ? gnx = cGL[ti] * gRK + 0.00001; cGL[ti] = min(gnx, max(xGL[ti], cGL[ti]));
    gnx = cGR[ti] + (xGR[ti] - cGR[ti]) * gsm; gnx > cGR[ti] * gRK + 0.00001 ? gnx = cGR[ti] * gRK + 0.00001; cGR[ti] = min(gnx, max(xGR[ti], cGR[ti]));
  );
  fmov ? ( cA[ti] += fdA[ti]; cSM[ti] += fdSM[ti]; );   // wall / air filters: a straight line to the end of the block while they move (see @block)
  (cGL[ti] + cGR[ti] > 0.0000001) ? (
    vR = rdh(ibuf, MAXIN, wiD - cDL[ti]);   // one read per tap (cubic, every reflection; a linear read for the cluster measured no gain - stays cubic)
    lpL[ti] = vR + (lpL[ti] - vR) * cA[ti];                 // air / wall absorption
    vL = lpL[ti] + (vR - lpL[ti]) * cSM[ti];                 // through a wall / door: part of the highs gets through (shelf)
    ti < itdN ? ( vR0 = rdh(ibuf, MAXIN, wiD - cDR[ti]); lpR[ti] = vR0 + (lpR[ti] - vR0) * cA[ti]; vR = lpR[ti] + (vR0 - lpR[ti]) * cSM[ti];   // the right ear reads its own time
      cHL[ti] += (xHL[ti] - cHL[ti]) * sm; cHR[ti] += (xHR[ti] - cHR[ti]) * sm;   // the head shadow of the far ear
      hdsL[ti] = vL + (hdsL[ti] - vL) * cHL[ti]; vL = hdsL[ti]; hdsR[ti] = vR + (hdsR[ti] - vR) * cHR[ti]; vR = hdsR[ti];
    ) : ( vR = vL; );
    tSt[ti] ? ( ti == 0 ? ( sdL += vL * cGL[ti]; sdR += vR * cGR[ti]; ) : ( seL += vL * cGL[ti]; seR += vR * cGR[ti]; ); )   // unchanged: not faded
            : ( ti == 0 ? ( dirL += vL * cGL[ti]; dirR += vR * cGR[ti]; ) : ( erL += vL * cGL[ti]; erR += vR * cGR[ti]; ); );
  );
  ti += 1;
);
// crossfade: the old tap set keeps playing (frozen parameters) and fades out with equal power
xfade > 0 ? (
  xodL = xodR = xoeL = xoeR = 0;
  ti = 0;
  loop(NT,
    (oGL[ti] + oGR[ti] > 0.0000001) ? (
      vR = rdh(ibuf, MAXIN, wiD - oDL[ti]);
      olpL[ti] = vR + (olpL[ti] - vR) * oA[ti];
      vL = olpL[ti] + (vR - olpL[ti]) * oSM[ti];
      ti < itdN ? ( vR0 = rdh(ibuf, MAXIN, wiD - oDR[ti]); olpR[ti] = vR0 + (olpR[ti] - vR0) * oA[ti]; vR = olpR[ti] + (vR0 - olpR[ti]) * oSM[ti];
        ohdsL[ti] = vL + (ohdsL[ti] - vL) * oHL[ti]; vL = ohdsL[ti]; ohdsR[ti] = vR + (ohdsR[ti] - vR) * oHR[ti]; vR = ohdsR[ti];
      ) : ( vR = vL; );
      ti == 0 ? ( xodL += vL * oGL[ti]; xodR += vR * oGR[ti]; ) : ( xoeL += vL * oGL[ti]; xoeR += vR * oGR[ti]; );
    );
    ti += 1;
  );
  // equal power (sin / cos) along a smoothstep: no slope at either end. sqrt(1 - xfade) rose with infinite slope - the new
  // set was at 10 % after 1 % of the fade, an audible click (loud when the barrier passes the listener)
  xfu = 1 - xfade; xfth = xfu * xfu * (3 - 2 * xfu) * 1.5707963; xgN = sin(xfth); xgO = cos(xfth);
  dirL = dirL * xgN + xodL * xgO; dirR = dirR * xgN + xodR * xgO;
  erL = erL * xgN + xoeL * xgO; erR = erR * xgN + xoeR * xgO;
);
dirL += sdL; dirR += sdR; erL += seL; erR += seR;
// NEAR FIELD: closer than 1 m the direct sound gains lows (a low shelf at 250 Hz, up to +4 dB at 25 cm) - the
// proximity of a voice right at the ear; a source that comes very close sounds close, not only loud
cNF += (xNF - cNF) * sm;
nfSL = dirL + (nfSL - dirL) * nfA; dirL += cNF * nfSL; nfSR = dirR + (nfSR - dirR) * nfA; dirR += cNF * nfSR;
// EARLY SCATTERING: no real wall is a mirror - every reflection arrives a little smeared. Two short allpasses per side
// (4.3 / 6.7 ms left, 4.9 / 7.9 ms right, lossless) spread each reflection over a few milliseconds: the early sound loses
// its "slap" and the echo density of large rooms rises, the direct sound and the tail stay as they are
edi >= EDN ? edi = 0;
edv = erL + 0.5 * edB0[(edi - edL0 + EDN) % EDN]; erL = edB0[(edi - edL0 + EDN) % EDN] - 0.5 * edv; edB0[edi] = edv;
edv = erL + 0.5 * edB1[(edi - edL1 + EDN) % EDN]; erL = edB1[(edi - edL1 + EDN) % EDN] - 0.5 * edv; edB1[edi] = edv;
edv = erR + 0.5 * edB2[(edi - edL2 + EDN) % EDN]; erR = edB2[(edi - edL2 + EDN) % EDN] - 0.5 * edv; edB2[edi] = edv;
edv = erR + 0.5 * edB3[(edi - edL3 + EDN) % EDN]; erR = edB3[(edi - edL3 + EDN) % EDN] - 0.5 * edv; edB3[edi] = edv;
edi += 1; edi >= EDN ? edi = 0;
// delay: the left / right difference of the echoes arrives with the direct sound - same distance, same level AND the same
// filters on the way: air, and a barrier (wall / window / door) in between. Without those filters the echoes behind a
// barrier would keep all their highs (a hiss that hangs on with feedback).
dlhold > 0 || shold > 0 ? (                   // (also the input's stereo width, see STEREO WIDTH)
  dsv = rdh(sbuf, MAXIN, wi - cDL[0]);
  lpSd = dsv + (lpSd - dsv) * cA[0]; dsv = lpSd + (dsv - lpSd) * cSM[0];
  dsv *= (cGL[0] + cGR[0]) * 0.5; dirL += dsv; dirR -= dsv;
) : ( lpSd = 0; );
// LISTEN (teaching): direct, early reflections and reverb tail can be muted one by one (smoothed: no clicks)
cMuD += (tMuD - cMuD) * sm; cMuE += (tMuE - cMuE) * sm; cMuL += (tMuL - cMuL) * sm;
dirL *= cMuD; dirR *= cMuD; erL *= cMuE; erR *= cMuE;

// ---- bass steering ----
// Every reflection is panned to the direction it arrives from. Below ~250 Hz that goes wrong on two loudspeakers: direct sound
// and reflections cancel differently in the left and in the right channel, and the bass wanders away from the source
// (it can even end up on the other side). At a real listening position the bass is ONE pressure - the sum of all arrivals -
// and its direction is that of the first wavefront. So: low band of (direct + reflections), summed, placed where the direct
// sound is. The sum of left + right stays exactly as it was (the room's bass response is untouched), only its place changes.
(
  stL = dirL + erL; stR = dirR + erR;
  stM = bqrun(stL, stC, stS, 2) + bqrun(stR, stC, stS + 4, 2);
  stG = cGL[0] + cGR[0]; stP = stG > 0.000000001 ? cGL[0] / stG : 0.5;
  // ... but only as far as the first wavefront can lead (precedence): a direct sound far below the reflections - e.g. behind a
  // closed wall or a door - no longer places the bass; then the bass stays where the reflections bring it (cSTW, see compute_geo)
  cSTW += (xSTW - cSTW) * sm;
  dirL += (bqrun(stL, stC + 10, stS + 8, 2) + stM * stP - stL) * cSTW;
  dirR += (bqrun(stR, stC + 10, stS + 12, 2) + stM * (1 - stP) - stR) * cSTW;
);

// ---- late reverb: pre-delay -> 4 diffusers -> 8-line FDN ----
cLD += (xLD - cLD) * smd;
cLG += (xLG - cLG) * (xLG > cLG ? smu : sm);
dv0 = rdh(ibuf, MAXIN, wiD - cLD);
xfade > 0 && !ldS ? dv0 = dv0 * xgN + rdh(ibuf, MAXIN, wiD - oLD) * xgO;
cLLP += (xLLP - cLLP) * sm; cLM += (xLM - cLM) * sm; lpLt = dv0 + (lpLt - dv0) * cLLP; dv0 = lpLt + (dv0 - lpLt) * cLM;   // barrier: tail heard through closed parts is dark
cBNG += (xBNG - cBNG) * sm; bndS += (dv0 - bndS) * xBNA; dv0 += bndS * cBNG;   // boundary effect: more bass power from a source near a surface
// directivity of the source: it radiates about Q = 2.2 in the mids (taken out of the reverb level, see barE), nearly round
// in the bass (Q ~ 1.3: +2.3 dB back below ~250 Hz) and narrower in the highs (-3 dB at 4 kHz, -5 dB at 8 kHz, like a voice)
dirB += (dv0 - dirB) * dirCB; dirS += (dv0 - dirS) * dirC; dv0 += dirB * dirBG; dv0 = dirS + (dv0 - dirS) * dirShG;
cDEN += (xDEN - cDEN) * sm; apg = 0.72 * cDEN;
cTH += (xTH - cTH) * sm; sTH += (xSH - sTH) * sm;

ak = 0;
loop(4,
  abp = apbase + ak * APMAX;
  rp2 = api - apCur[ak]; rp2 < 0 ? rp2 += APMAX;
  dv = abp[rp2];
  apX > 0 ? ( rp3 = api - apOld[ak]; rp3 < 0 ? rp3 += APMAX; dv = dv * apgN + abp[rp3] * apgO; );   // new length faded in (no read jump)
  ww = dv0 - apg * dv;
  abp[api] = ww;
  dv0 = dv + apg * ww;
  ak += 1;
);
api += 1; api >= APMAX ? api = 0;
apX > 0 ? ( apX = max(0, apX - apStp); apu = 1 - apX; apu = apu * apu * (3 - 2 * apu) * 1.5707963; apgN = sin(apu); apgO = cos(apu); );

fqmov ? ( fqk = 0; loop(240, fqCc[fqk] += fqCd[fqk]; fqk += 1; ); );   // decay crossfade (see @block)
fk = 0;
loop(8,
  fbp = fdnbase + fk * LINEMAX;
  cFL[fk] += (flen[fk] - cFL[fk]) * smfd;
  modstep(fk); mdOf = moddep * mdV2[fk];
  ov = rdh(fbp, LINEMAX, fwi - cFL[fk] - mdOf);
  xfG && !xfS[fk] ? ov *= min(1, max(0, (xfcnt - (cFL[fk] + 2 * moddep + 4)) * xfRI));   // new room: nothing to read yet - then a 3 ms fade-in (no step)
  xfade > 0 && !xfS[fk] ? ov = ov * xgN + rdh(fbp, LINEMAX, fwi - oFL[fk] - mdOf) * xgO * cXR;
  // per line: the loop filter - every octave 125 Hz ... 8 kHz loses what the room's RT60 in that octave asks for
  fqG[fk] > fqGc[fk] * 1.26 ? fqGc[fk] = fqG[fk] : fqGc[fk] += (fqG[fk] - fqGc[fk]) * smQ; fst[fk] = fqGc[fk] * bqrun(ov, fqCc + fk * 30, fqS + fk * 12, 6);   // glides up as well (a jump of more than 2 dB still snaps - the hot-content scaling counts on it) // loop gain glides (5 ms) - a decay change no longer clicks
  fk += 1;
);
f0 = fst[0]; f1 = fst[1]; f2 = fst[2]; f3 = fst[3];
f4 = fst[4]; f5 = fst[5]; f6 = fst[6]; f7 = fst[7];

// the late level is applied where the sound ENTERS the network (inj): when the room shrinks and the level rises,
// only new sound gets the new level - the tail already ringing is not turned up with it
lateL = (f0 - f1 + f2 - f3 + f4 - f5 + f6 - f7) * cMuL;
lateR = (f0 + f1 - f2 - f3 + f4 + f5 - f6 - f7) * cMuL;
// (output allpasses for a wider tail were tried - above 200 Hz the tail is already uncorrelated (r = -0.06); the r = 0.36
// broadband comes from the room modes below 200 Hz, which ARE the same at both ears. Nothing to fix - removed again)
// room spill through a barrier: the share that arrives straight from the wall / opening is placed there
cLDf += (xLDf - cLDf) * sm; cLPL += (xLPL - cLPL) * sm; cLPR += (xLPR - cLPR) * sm;
cLPL + cLPR > 0.000001 ? ( spm = (lateL + lateR) * 0.70710678; lateL = lateL * cLDf + spm * cLPL; lateR = lateR * cLDf + spm * cLPR; );

// feedback matrix M = cos(th) I + sin(th) S, S = skew-symmetric conference matrix of order 8 (Paley, q = 7) / sqrt(7):
// orthogonal for every th (lossless) - low density = lines feed mostly themselves (distinct echoes),
// high density = every line feeds all others (smooth tail)
s0 = f1 + f2 + f3 + f4 + f5 + f6 + f7;
s1 = -f0 + f2 + f3 - f4 + f5 - f6 - f7;
s2 = -f0 - f1 + f3 + f4 - f5 + f6 - f7;
s3 = -f0 - f1 - f2 + f4 + f5 - f6 + f7;
s4 = -f0 + f1 - f2 - f3 + f5 + f6 - f7;
s5 = -f0 - f1 + f2 - f3 - f4 + f6 + f7;
s6 = -f0 + f1 - f2 + f3 - f4 - f5 + f7;
s7 = -f0 + f1 + f2 - f3 + f4 - f5 - f6;
sn = sTH * 0.377964473;                              // 1 / sqrt(7)
hn = 0.35355339;
inj = dv0 * hn * cLG;
fbp = fdnbase;
fbp[fwi] = cTH * f0 + sn * s0 + inj; fbp += LINEMAX;
fbp[fwi] = cTH * f1 + sn * s1 - inj; fbp += LINEMAX;
fbp[fwi] = cTH * f2 + sn * s2 + inj; fbp += LINEMAX;
fbp[fwi] = cTH * f3 + sn * s3 + inj; fbp += LINEMAX;
fbp[fwi] = cTH * f4 + sn * s4 - inj; fbp += LINEMAX;
fbp[fwi] = cTH * f5 + sn * s5 + inj; fbp += LINEMAX;
fbp[fwi] = cTH * f6 + sn * s6 - inj; fbp += LINEMAX;
fbp[fwi] = cTH * f7 + sn * s7 - inj;
// ---- 2nd network: neighbor room heard through the barrier (only while a barrier separates a second room) ----
cLG2 += (xLG2 - cLG2) * sm;
cLG2 > 0.0000001 ? (
  cLD2 += (xLD2 - cLD2) * smd; cP2L += (xP2L - cP2L) * sm; cP2R += (xP2R - cP2R) * sm; cLLP2 += (xLLP2 - cLLP2) * sm;
  dvb = rdh(ibuf, MAXIN, wiD - cLD2);
  xfade > 0 && !ld2S ? dvb = dvb * xgN + rdh(ibuf, MAXIN, wiD - oLD2) * xgO;
  lpLt2 = dvb + (lpLt2 - dvb) * cLLP2; dvb = lpLt2 + (dvb - lpLt2) * cLM;
  fk = 0;
  loop(4,
    fbp = fdn2base + fk * LINEMAX;
    cFL2[fk] += (flen2[fk] - cFL2[fk]) * smfd;
    modstep(8 + fk); mdOf = moddep * mdV2[8 + fk];
    ov = rdh(fbp, LINEMAX, fwi - cFL2[fk] - mdOf);
    xfG && !xfS[8 + fk] ? ov *= min(1, max(0, (xfcnt - (cFL2[fk] + 2 * moddep + 4)) * xfRI));
    xfade > 0 && !xfS[8 + fk] ? ov = ov * xgN + rdh(fbp, LINEMAX, fwi - oFL2[fk] - mdOf) * xgO;
    fhs2[fk] = (1 - fb2[fk]) * ov + fb2[fk] * fhs2[fk];
    fls2[fk] += (ov - fls2[fk]) * lfc;
    fst2[fk] = fg2[fk] * fhs2[fk] + (fgL2[fk] - fg2[fk]) * fls2[fk];
    fk += 1;
  );
  e0 = fst2[0]; e1 = fst2[1]; e2 = fst2[2]; e3 = fst2[3];
  // Hadamard 4x4 / 2: orthogonal, lossless
  fbp = fdn2base;       fbp[fwi] = 0.5 * (e0 + e1 + e2 + e3) + 0.5 * dvb;
  fbp += LINEMAX;       fbp[fwi] = 0.5 * (e0 - e1 + e2 - e3) - 0.5 * dvb;
  fbp += LINEMAX;       fbp[fwi] = 0.5 * (e0 + e1 - e2 - e3) + 0.5 * dvb;
  fbp += LINEMAX;       fbp[fwi] = 0.5 * (e0 - e1 - e2 + e3) - 0.5 * dvb;
  n2L = (e0 - e1 + e2 - e3) * cLG2; n2R = (e0 + e1 - e2 - e3) * cLG2;
  bpkM == 2 ? ( n2L = bqrun(n2L, bpkC, bpkS + 4, 1); n2R = bqrun(n2R, bpkC, bpkS + 6, 1); );   // coincidence: through the barrier
  lateL += n2L * cP2L; lateR += n2R * cP2R;
);
fwi += 1; fwi >= LINEMAX ? fwi = 0; fwn += 1;

// ---- ROOM MODES: the late bass between the lowest mode and fS becomes the sum of the room's modes (see mdcalc) ----
xMD = mdN > 0 && !mdOff && !mdPend;
cMD += (xMD - cMD) * sm;
cMD > 0.0001 ? (
  // Linkwitz-Riley splits (low + high = all-pass: inaudible on the diffuse tail): highs of L / R stay, the bass below fc
  // (one pressure at the listener: mono) is split again at fA - below it stays, the band fA … fc becomes the modes
  mhL = bqrun(lateL, mdX + 30, mdXS, 2); mhR = bqrun(lateR, mdX + 30, mdXS + 4, 2);
  mlo = 0.70710678 * (bqrun(lateL, mdX, mdXS + 8, 2) + bqrun(lateR, mdX, mdXS + 12, 2));   // L, R uncorrelated: same power per channel
  mbd = bqrun(mlo, mdX + 40, mdXS + 16, 2); msub = bqrun(mlo, mdX + 10, mdXS + 20, 2);
  mdxd = mbd - mdx2; mdx2 = mdx1; mdx1 = mbd;
  msum = 0; mp = mdM;
  mdcmov ? ( mk = 0; loop(mdN, mdC0[mk] += mdD0[mk]; mdC1[mk] += mdD1[mk]; mp[2] += (mp[5] - mp[2]) * sm; yv = mp[2] * mdxd - mdC0[mk] * mp[3] - mdC1[mk] * mp[4]; mp[4] = mp[3]; mp[3] = yv; msum += yv; mp += 6; mk += 1; ); ) : (   // coefficients on their way (decay automation)
  mdmov ? loop(mdN, mp[2] += (mp[5] - mp[2]) * sm; yv = mp[2] * mdxd - mp[0] * mp[3] - mp[1] * mp[4]; mp[4] = mp[3]; mp[3] = yv; msum += yv; mp += 6; )
        : loop(mdN, yv = mp[2] * mdxd - mp[0] * mp[3] - mp[1] * mp[4]; mp[4] = mp[3]; mp[3] = yv; msum += yv; mp += 6; ); );
  lateL += (mhL + msub + msum - lateL) * cMD; lateR += (mhR + msub + msum - lateR) * cMD;
);

);   // end of the room (skipped while it is not heard)
wi += 1; wi >= MAXIN ? wi = 0;
xfade > 0 ? xfade = max(0, xfade - xfstep);
xfcnt += 1; cXR += ((xfade > 0 ? xfR : 1) - cXR) * sm5;

// ---- output ----
cOG += (xOG - cOG) * sm;
cMIX += (xMIX - cMIX) * sm;
cBYP += (xBYPe - cBYP) * sm;                 // room bypass: crossfade room -> source model only
// ROOM OFF as a 3D panner (direction, distance 1 / r from 1 m, height, air and distance filter, Doppler) was replaced by the
// stereo panner of below: bdL / bdR (the direct sound alone, also computed in the room-skip branch above) are no longer used
// by the output; the input's stereo width and the delay were handled inside that direct path (the taps read the room input)
!rmskip ? ( bdL = dirL; bdR = dirR; );
// ROOM OFF = a plain STEREO PANNER of the source (no listener, no distance, no height, no ear filters): the source's
// place between the left and the right wall pans the mid (constant power, 0 dB in the center, +3 dB at a wall), the side
// narrows towards a wall. Source in the center: output = input, sample for sample (a 3D panner that changes
// level and phase would turn a correlation of +0.6 in into -0.5 out)
cPAN += (xPAN - cPAN) * sm;
abs(cPAN) > 0.0000001 ? ( panA = (cPAN + 1) * 0.7853981634; panL = cos(panA) * 1.4142135624; panR = sin(panA) * 1.4142135624; panW = 1 - abs(cPAN); )
                      : ( panL = panR = panW = 1; );
byL = bpMo * panL + bpSo * panW; byR = bpMo * panR - bpSo * panW;   // through the Doppler delay (= bpM1 / bpS1 when it is not in use)
// TONE (REVERB section): colors only the room sound – early reflections + tail; the direct sound stays as it is
rmL = erL + lateL; rmR = erR + lateR;
bron ? ( rmL = bqrun(rmL, bsC, bsS, 1); rmR = bqrun(rmR, bsC, bsS + 2, 1); );          // bright: high shelf at 4 kHz, up to +12 dB
dkon ? ( dkS[0] = rmL + (dkS[0] - rmL) * dkA; rmL = dkS[0]; dkS[1] = rmR + (dkS[1] - rmR) * dkA; rmR = dkS[1]; );   // dark: one-pole low-pass 20 kHz -> 1.5 kHz
cEQL += (xEQL - cEQL) * (xEQL > cEQL ? smu : sm);          // equal loudness: make-up gain of the room sound
cRMX += (xRMX - cRMX) * sm; rmxk = (1 - cBYP) * cRMX;          // room share: mix x (room on)
cPK1 += ((bpkM == 1) - cPK1) * sm;   // the coincidence peak fades in / out (30 ms) - switched at the barrier plane it was a 5 dB step
cPK1 > 0.0001 ? ( rmL += cPK1 * (bqrun(dirL + rmL, bpkC, bpkS, 1) - dirL - rmL); rmR += cPK1 * (bqrun(dirR + rmR, bpkC, bpkS + 2, 1) - dirR - rmR); );   // coincidence: all came through the barrier
wetL = (dirL + rmL) * cEQL * rmxk + byL * (1 - rmxk);
wetR = (dirR + rmR) * cEQL * rmxk + byR * (1 - rmxk);
outL = ((1 - cMIX) * cSDRY * inL1 + cMIX * wetL) * cOG;   // the dry part one sample late, as the wet one
outR = ((1 - cMIX) * cSDRY * inR1 + cMIX * wetR) * cOG; inL1 = inL; inR1 = inR;
fpost && fany ? ( fmL = fchain(outL, lcSL, lcFL, hcSL, hcFL); fmR = fchain(outR, lcSR, lcFR, hcSR, hcFR);
  cFMX >= 0.99999 ? ( outL += (fmL - outL) * cFMX; outR += (fmR - outR) * cFMX; )
                  : ( outL = fmL * cFMX + fapath(outL, FAP) * (1 - cFMX); outR = fmR * cFMX + fapath(outR, FAP + 40) * (1 - cFMX); );   // Mix: phase-matched dry part
);
// A / B LOUDNESS MATCH: measure what goes out (before the trim), play B as loud as A
abPw += outL * outL + outR * outR; cABG += (xABG - cABG) * smab; outL *= cABG; outR *= cABG;
(abs(outL) + abs(outR) < 100000) ? 0 : ( outL = outR = 0; fpanic = 1; );   // not a number / runaway: mute, flush at the next block
// ---- limiter: 1.5 ms look-ahead, ceiling -1 dBFS, 150 ms release, stereo-linked ----
lmpk = max(abs(outL), abs(outR));
lmg = clipon && lmpk > LMCEIL ? LMCEIL / lmpk : 1;                        // gain this sample needs
while ( lmqn > 0 && lmQv[(lmqh + lmqn - 1) % LMQ] >= lmg ? ( lmqn -= 1; 1; ) : 0; );   // sliding minimum: drop what is no longer the smallest
lmqi = (lmqh + lmqn) % LMQ; lmQv[lmqi] = lmg; lmQt[lmqi] = lmt; lmqn += 1;
lmQt[lmqh] <= lmt - LMN ? ( lmqh = (lmqh + 1) % LMQ; lmqn -= 1; );         // the oldest left the window
lmt += 1; lmt > 1000000000 ? ( k0 = 0; loop(LMQ, lmQt[k0] -= 1000000000; k0 += 1; ); lmt -= 1000000000; );
lmr = lmQv[lmqh] < lmr ? lmQv[lmqh] : lmr + (lmQv[lmqh] - lmr) * lmrel;  // instant down, 150 ms back up
lmsum += lmr - lmW[lmd]; lmW[lmd] = lmr;                                   // moving average over the window = attack ramp
lmd += 1; lmd >= LMN ? ( lmd = 0; lmsum = 0; k0 = 0; loop(LMN, lmsum += lmW[k0]; k0 += 1; ); );   // (re-sum once per window: no drift)
lmga = min(1, lmsum / LMN);
lmx = lmDL[lmdd]; lmDL[lmdd] = outL; outL = lmx; lmx = lmDR[lmdd]; lmDR[lmdd] = outR; outR = lmx;   // delay = look-ahead
lmdd += 1; lmdd >= LMLA ? lmdd = 0;
outL *= lmga; outR *= lmga;
lmgv = min(lmga, 1 - (1 - lmgv) * pmdec);                                  // gain-reduction meter (GUI)
lmga < 0.944 ? gwarn = srate * 0.25;                                       // more than 0.5 dB: shown as limiting
gwarn > 0 ? gwarn -= 1;
clipon ? (
  abs(outL) > 0.891 ? ( outL = (outL > 0 ? 1 : -1) * (0.891 + 0.109 * thf((abs(outL) - 0.891) / 0.109)); clipwarn = srate * 0.25; );
  abs(outR) > 0.891 ? ( outR = (outR > 0 ? 1 : -1) * (0.891 + 0.109 * thf((abs(outR) - 0.891) / 0.109)); clipwarn = srate * 0.25; );
);
clipwarn > 0 ? clipwarn -= 1;

pkOL = max(abs(outL), pkOL * pmdec); pkOR = max(abs(outR), pkOR * pmdec);
crXY = crXY * crk + outL * outR; crXX = crXX * crk + outL * outL; crYY = crYY * crk + outR * outR;   // correlation (GUI)
spOn ? ( spRI[spw] = spIn; spRO[spw] = (outL + outR) * 0.5; spw = (spw + 1) & 8191; );   // spectrum (only while the window is open)
spOn ? ( gonC += 1; gonC >= 4 ? ( gonC = 0; gonB[2 * gonP] = outL; gonB[2 * gonP + 1] = outR; gonP = (gonP + 1) & (GONN - 1); ); );   // goniometer ring

// SAFETY: a non-finite output (NaN / Inf) is replaced by silence and the long buffers are cleared - one bad sample must not
// leave the plugin dead (a NaN in a feedback loop stays forever)
nanB = !(abs(outL) < 1000000) || !(abs(outR) < 1000000);
nanB ? (
  outL = 0; outR = 0; nanCnt += 1;
  // the full reset of the next block (@block, fpanic) - it also clears the recursive states that are not in the buffers
  // below: the FDN loop filters, the source model's biquads, the oversampler, the room modes (a partial reset left a NaN
  // sitting in one of those, and it came straight back on the next sample - the plugin stayed silent until playback restarted)
  fpanic = 1;
  memset(ibuf, 0, dlL - ibuf); memset(arr, 0, stb + 64 - arr); memset(dlL, 0, 2 * DLMAX + MAXIN); memset(dlC, 0, 48 + ANAMAX + 80);
  memset(lmDL, 0, 2 * LMLA); lmsum = LMN; lmr = 1; lmga = 1; k0 = 0; loop(LMN, lmW[k0] = 1; k0 += 1; );
);
spl0 = outL;
spl1 = outR;
// silence in and out: count; long enough -> sleep
slc = abs(inL) + abs(inR) < SLIN && abs(outL) + abs(outR) < SLOUT ? slc + 1 : 0;
slc > slhold ? ( slp = 1; slc = 0; );
);

@gfx 760 574
gclp = 0;
// reflector sliders changed by the host (automation while the audio is stopped, too) -> the reflectors (see REFLECTOR AUTOMATION)
rfLoad == rfLoadG ? ( i = 0; loop(15, sv = slider(120 + i); sv == sv && !rfTch[i] && abs(sv - rfSl[i]) > 0.0000001 ? ( vopt[RFB + i] = sv; rfSl[i] = sv; ); i += 1; ); );
gfx_clear = -1;                          // never auto-clear: the frame is kept for partial (meter-only) redraws
// the whole frame is drawn into image 1 (the plugin's own framebuffer, kept between frames) and put on the screen
// with ONE blit at the very end of @gfx - the screen never shows a half-drawn frame (the cleared background, a cleared
// module before its meters are back), which flickered on a Mac in the All view (Metal presents the window at any moment)
(fbW != gfx_w || fbH != gfx_h) ? ( gfx_setimgdim(1, gfx_w, gfx_h); fbW = gfx_w; fbH = gfx_h; fullcnt = 0; );
gfx_dest = 1;
// the correlation of this frame (2 = no signal), taken once at its start (the modules read it from here)
crvG = time_precise() - lastblk > 0.3 || crXX + crYY < 0.000000001 ? 2 : crXY / sqrt(crXX * crYY + 0.00000000000000000001);
// calm display: smoothed per frame, shown in steps of 0.02 at most 8 times a second, and "–" (no signal) only after
// 0.6 s of silence - the number and the bar no longer jump and blink from frame to frame (quiet tails, a moving source)
crvT = time_precise();
crvG <= 1.5 ? ( crvSl = crvT; crvD = crvD > 1.5 ? crvG : crvD + (crvG - crvD) * 0.2; ) : ( crvT - crvSl > 0.6 ? crvD = 2; );
(crvT - crvLt > 0.125 && abs(crvD - crvS) >= 0.035) || crvT - crvLt > 0.6 || (crvD > 1.5) != (crvS > 1.5) ? (   // small wobbles (< 0.035) wait 0.6 s - the number no longer blinks between two neighbours
   crvS = crvD > 1.5 ? 2 : floor(crvD * 50 + 0.5) / 50; crvLt = crvT; );
inStereo0 = time_precise() - lastblk < 0.3 && sdE > 0.000000001 && sdE > mdE * 0.0001;
inStereo0 ? stHold = time_precise(); inStereo = inStereo0 || time_precise() - stHold < 1.5;   // kept for 1.5 s - a slow audio block (Barrier: Window / Door / Open recompute) must not make the span handles blink // a stereo input (side at least -40 dB under the mid) - the span in the room view
// BARRIER TILES (Basic and Expert): Wall / Window / Door as icon tiles - filled in the accent color when chosen; a click on another
// type switches the barrier to it (with that type's typical damping)
// a barrier that is switched on stands half way between source and listener (depth and side) - unless they stand on the
// same spot (linked) or the barrier is locked
// from the position sliders themselves (sx / lx are the audio thread's copy - stale while it has not run since the
// balls were moved, so the barrier sometimes landed between the OLD places); no exception for two balls on one spot
function bcenter() (
  !lockB ? ( slider42 = (slider11 + slider14) / 2; sauto1(42); );
  !lockE ? ( slider45 = (slider10 + slider13) / 2; sauto1(45); );
);
// Draws the Wall/Window/Door barrier-type tiles in a row at (x,y,w,h); click selects type k (sets slider41, slider44 default material)
function bartiles(x, y, w, h) local(cw, k, tx, act, hit, ic)
(
  cw = (w - 8) / 3; k = 1;
  loop(3,
    tx = x + (k - 1) * (cw + 4); act = btg == k; hit = inbox(tx, y, cw, h);
    col(act ? cBARTT : (hit ? cHAIR : cLINE), 1); RR(tx, y, cw, h, 8);
    ic = act ? cSURF : (hit ? cTEXT : cMUTED); gfx_setfont(6); btw = tws("Window");   // the widest name decides - one layout for all three
    h >= 36 ? ( baricon(k, tx + cw / 2, y + (h - 12) / 2, ic); col(ic, 1); gfx_setfont(6); txt(k == 1 ? "Wall" : (k == 2 ? "Window" : "Door"), tx + cw / 2, y + h - 8, 1); )   // icon above, name below
            : ( baricon(k, tx + cw / 2 - btw / 2 - 6, y + h / 2, ic); col(ic, 1); gfx_setfont(6); txt(k == 1 ? "Wall" : (k == 2 ? "Window" : "Door"), tx + cw / 2 + 8, y + h / 2, 1); );
    click && hit && !act ? ( btg == 0 ? bcenter(); slider41 = k; sauto1(41); slider44 = bmatrw(k, 1); sauto1(44); );
    tipbox(tx, y, cw, h, k == 1 ? "Wall between source and listener, parallel to the back wall" : (k == 2 ? "Wall with a window (1.2 × 1.2 m, sill 0.9 m, two sashes) – Open swings them open to the left and right" : "Wall with a door (0.9 × 2.1 m) – Open swings the leaf up to 90°"));
    k += 1;
  );
);
// padlock icon (cx, cy = center of the body), locked = closed shackle
function padlock(cx, cy, lk)
(
  RR(cx - 3.6, cy - 0.8, 7.2, 5.6, 1.2);   // smooth shackle
  SDB0(); lk ? ( SDSEG(cx - 2.2, cy - 0.8, cx - 2.2, cy - 3); SDSEG(cx + 2.2, cy - 0.8, cx + 2.2, cy - 3); SDARC(cx, cy - 3, 2.2, -0.5 * $pi, 0.5 * $pi); )
             : ( SDSEG(cx - 2.2, cy - 0.8, cx - 2.2, cy - 4.6); SDSEG(cx + 2.2, cy - 3.6, cx + 2.2, cy - 4.6); SDARC(cx, cy - 4.6, 2.2, -0.5 * $pi, 0.5 * $pi); ); SDDRAW(1.05);
);
// ---- sections: current height (folding animation) and "fully open" ----
function sech(id, full) ( HDR + secop[id + basic * 12] * (full - HDR); );
// Returns true if section id is unfolded; folded state kept in secop[], offset 12 per view mode (Basic vs Expert)
function secopen(id) ( secop[id + basic * 12] >= 1; );
// extra height of a section, set with the mouse (Expert: per section, Basic: the row of four)
function secx(id) ( basic ? vopt[32] + bfill : vopt[id == 14 ? 37 : 20 + id]; );   // (Motion: vopt[37])   // Basic: + the height the cards get to fill the window
// a section's controls only exist while it is drawn: in a section zoom only the zoomed one
function secvis(id) ( !learn && secopen(id) && (!szoom || szoom == id); );
// DROPDOWN MENU placement: inside the visible part of the window (mnvl … mnvb, current scale); pref 0 = below the box first,
// 1 = above first; scaled down (mfk < 1) only when it is taller / wider than the visible part. -> mfx, mfy, mfk
function mfit(bx, by, bw, bh, mw, mh, pref) local(sw, sh)
(
  mfk = max(0.5, min(1, min((mnvr - mnvl) / mw, (mnvb - mnvt) / mh))); sw = mw * mfk; sh = mh * mfk;
  pref ? ( mfy = by - sh - 3; mfy < mnvt ? ( mfy = by + bh + 3; ); ) : ( mfy = by + bh + 3; mfy + sh > mnvb ? ( mfy = by - sh - 3; ); );
  mfy = max(mnvt, min(mfy, mnvb - sh));
  mfx = max(mnvl, min(bx, mnvr - sw));
);
// a menu with its own scale k around its top left corner (ax, ay): draw (mtx … mtxend) and read the mouse (mloc … mrest)
function mtx(ax, ay, k) (
  gsMn = gs; goxMn = gox; goyMn = goy; mxMn = mx; myMn = my;
  gs = gsMn * k; gox = goxMn + ax * (gsMn - gs); goy = goyMn + ay * (gsMn - gs);
  setfonts(); mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs; mtxa = ax; mtya = ay; mtxk = k;
);
// Ends a scaled-menu block started by mtx: restores gs, gox, goy, fonts and mouse mx/my, and maps the tooltip back to page scale
function mtxend() (
  tipon ? ( tipx = mtxa + (tipx - mtxa) * mtxk; tipy = mtya + (tipy - mtya) * mtxk; );   // tooltip back to the page scale
  gs = gsMn; gox = goxMn; goy = goyMn; setfonts(); mx = mxMn; my = myMn;
);
// Returns true if the mouse is inside box (x,y,w,h) in the unscaled normal page coordinates (goxN, goyN, gsN)
function inboxN(x, y, w, h) local(px, py) ( px = (mouse_x - goxN) / gsN; py = (mouse_y - goyN) / gsN; px >= x && px < x + w && py >= y && py < y + h; );
// Maps mouse mx,my into a menu's local scale k around anchor (ax,ay), saving the originals in mxMn/myMn; undo with mrest
function mloc(ax, ay, k) ( mxMn = mx; myMn = my; mx = ax + (mx - ax) / k; my = ay + (my - ay) / k; );
// Restores mouse mx,my saved by mloc
function mrest() ( mx = mxMn; my = myMn; );
// CLOSE BUTTON (x) of a preset window: a round badge on its top right corner (x, y, w = the window; k = its scale when drawn small)
function menuxhit(x, y, w, k) ( sqr(mx - (x + (w - 3) * k)) + sqr(my - (y + 3 * k)) < sqr(8.5 * k); );
// Draws the round close badge (x) at the top right corner of a preset window at (x,y,w); highlights when hovered
function menux(x, y, w) local(cx, cy, hit)
(
  cx = x + w - 3; cy = y + 3; hit = sqr(mx - cx) + sqr(my - cy) < 72;
  col(cINK, 0.12); CIF(cx + 0.5, cy + 1.5, 8, 1);
  col(hit ? cTEXT : cSURF, 1); CIF(cx, cy, 8, 1); col(hit ? cTEXT : cBORD, 1); CI(cx, cy, 8, 0);
  col(hit ? cSURF : cMUTED, 1); LNW(cx - 2.8, cy - 2.8, cx + 2.8, cy + 2.8, 1.3); LNW(cx - 2.8, cy + 2.8, cx + 2.8, cy - 2.8, 1.3);
);
// Maps a resizable part's id to its index in the vopt[] extra-height array (Motion 14 -> 37, ids 9/21/22 special, else 20+id)
function rzidx(id) ( id == 9 ? 33 : (id == 21 ? 34 : (id == 22 ? 32 : (id == 14 ? 37 : 20 + id))); );
// Returns true if resizable part id is currently available (view parts need vop>=1, id 22 needs section 3 open, else section open)
function rzok(id) ( id == 9 || id == 21 ? vop >= 1 : (id == 22 ? secopen(3) : secopen(id)); );
// bottom edge of a resizable part: rzgx, rzgw (x range), rzgy (edge)
function rzgeo(id) (
  id == 9 || id == 21 ? ( rzgx = VX; rzgw = VW; rzgy = VY + VH; ) : (
  id == 22 ? ( rzgx = 8; rzgw = bcw; rzgy = max(yS2 + hbB, yO2 + hbO); ) : (
  id == 4 || id == 5 ? ( rzgx = R0; rzgw = RWc; rzgy = id == 4 ? syW + shW : syB + shB; ) : (
  id == 6 ? ( rzgx = flX; rzgw = flW; rzgy = syF + shF; ) : (
  id == 3 ? ( rzgx = srX; rzgw = srW; rzgy = syS + shS; ) : (
  id == 7 ? ( rzgx = IOX; rzgw = IOW; rzgy = IOY + IOH; ) : (     // Output: where it is (right column while the room view is hidden)
  rzgx = L0; rzgw = LW; rzgy = id == 10 ? syD + shD : syR + shR;
  ))))));
);
// section heading: fold arrow (Expert), color mark, name, "?" - a click on arrow or name folds / unfolds the section
// FOLDING: Expert - every section folds on its own; Basic - the arrow of Source folds the row of four.
// Alt+click on any arrow
// (also the one of the 3D view) folds / opens everything.
// RESET of one module: its parameters back to the defaults - the power button (on / off) stays; one undo step
function mrr(a, b) local(i) ( i = a; loop(b - a + 1, sset(i, sdef(i)); i += 1; ); );
// Resets module id's sliders to defaults via sset/mrr (Filter, Source, Delay, Room, Walls/Reflectors, Free field); takes an undo snapshot first
function modulereset(id) local(i) (
  id != 15 ? snap_take();
  id == 6 ? ( mrr(24, 30); sset(88, 100); );                                                             // Filter (39 = its power)
  id == 3 ? ( sset(35, 0); sset(37, 0); mrr(47, 59); sset(89, 100); sset(98, 100); sset(110, 0); mrr(111, 114); );   // Source (85 = power)
  id == 10 ? ( mrr(66, 84); sset(86, sdef(86)); sset(87, sdef(87)); sset(90, 100); sset(93, 0); sset(94, 0); sset(99, 0); sset(115, 0); sset(116, 0); );   // Delay (65)
  id == 1 ? ( mrr(2, 4); mrr(10, 15); sset(20, 0); sset(21, 0); mrr(31, 34); sset(38, sdef(38)); sset(61, 0); sset(62, 100); sset(91, 100); sset(117, 0); vopt[8] = 1; );   // Room (36; the free field / room mode stays as it is)
  id == 4 ? ( isfree ? ( i = 0; loop(12, vopt[RFB + i] = FFP[ffpLast * 16 + 4 + i]; i += 1; ); vopt[RFB + 12] = vopt[RFB + 13] = 12; vopt[RFB + 14] = 0; rflSel = 0; ) : ( mrr(5, 9); sset(19, 1); ); );   // free field: the two reflectors back to the loaded place
  id == 15 ? ffpapply(0);                                                                               // Free field: back to the Street (power stays)
  id == 5 ? ( mrr(42, 46); sset(92, 100); mrr(95, 97); );                                                // Barrier (41 = type / power)
  id == 7 ? ( sset(18, 100); sset(22, 0); sset(23, 1); sset(40, 0); sset(60, 0); vopt[49] = 0; );       // Output
  id == 14 ? ( mrr(101, 109); );                                                                         // Motion (100)
  id != 15 ? snap_notify();
);
function ic_reset(cx, cy) local(px, py, dx, dy) (   // an open circle with an arrow head (counter-clockwise): reset
// the arc runs from the upper left over the top, right and bottom round to the lower left; the head sits on the
  // upper-left end and points on along the circle (counter-clockwise) into the gap
  SDB0(); SDARC(cx, cy, 3.9, -0.95, 4.15);
  px = cx + sin(-0.95) * 3.9; py = cy - cos(-0.95) * 3.9; dx = -cos(-0.95); dy = -sin(-0.95);
  CHEV(px + dx * 1.1, py + dy * 1.1, dx, dy, 2.3, 1); SDDRAW(1);
);
// Toggles all sections folded/unfolded in the section-bit mask vopt[12] (Expert) or vopt[13] (Basic): all open if any is closed, else all closed
function foldall() local(vi, allm) (
  vi = basic ? 13 : 12; allm = basic ? 682 : 4094 + 16384;                 // all section bits + 512 (3D view) (+ 1024 Delay, 2048 Decay by band, 16384 Motion)
  vopt[vi] = (vopt[vi] & allm) != allm ? allm : 0;
);
// Draws a section header at (x,y) with title s and color c: fold chevron, click folds (Alt+click toggles all), sets zoom close button position
function sechdr(id, s, x, y, c) local(m, op, ax, ay, hw, vi, head, colm, anyo)
(
  ay = y + 12;
  szoom == id ? ( zxbX = gox + (crdX + crdW) * gs; zxbY = goy + crdY * gs; zxbOn = 1; );   // zoom window: its corner for the X
  (
    vi = basic ? 13 : 12; ax = x + 10;
    head = tabm ? 0 : 1;   // tabs: no fold arrow (the tab is the module); Expert and Basic: every module folds on its own (Alt+click: all)
    colm = 2 ^ id;
    anyo = (vopt[vi] & colm) != colm; op = anyo;
    head ? ( op ? chevd(ax, ay, vlock ? cDIM2 : cMUTED) : chevr(ax, ay, vlock ? cDIM2 : cMUTED); );   // open: chevron down, folded: right
    sechead(id, s, x + 19, ay, c);
    gfx_setfont(2); hw = 30 + tws(s);
    m = 2 ^ id;
    head && !szoom && !vlock && click && inbox(x + 2, y + 2, hw, 20) ? (
      (mouse_cap & 16) ? foldall() : ( vopt[vi] = vopt[vi] - (vopt[vi] & colm) + (anyo ? colm : 0); );
      shpin ? ( shpin = 0; );
    );
    // zoom (Source, Walls, Echogram in Expert): magnifier after the "?" - the section alone, larger; again (or Esc) = back
    !tabm && (!basic || id == 1 || id == 3 || id == 5 || id == 7) ? (
      zx = x + 28 + tws(s) + 24; zhit = sqr(mx - zx) + sqr(my - ay) < 49;
      !learn && !szoom ? ( MZR[id * 4] = gox + crdX * gs; MZR[id * 4 + 1] = goy + crdY * gs; MZR[id * 4 + 2] = crdW * gs; MZR[id * 4 + 3] = crdH * gs; MZR[80 + id] = mzfr; );   // the module's place on screen - key Z zooms the one under the mouse (All / Fit)
      col(szoom == id ? c : (zhit ? cTEXT : cMUTED), 1);   // smooth strokes
      SDB0(); SDARC(zx - 1, ay - 1, 3.5, 0, 2 * $pi); SDSEG(zx + 1.6, ay + 1.6, zx + 4.3, ay + 4.3); SDSEG(zx - 2.6, ay - 1, zx + 0.6, ay - 1); szoom != id ? SDSEG(zx - 1, ay - 2.6, zx - 1, ay + 0.6); SDDRAW(1.05);
      click && zhit ? ( szoom = szoom == id ? 0 : id; basic ? ( vopt[13] & m ? ( vopt[13] -= m; ); ) : ( vopt[12] & m ? ( vopt[12] -= m; ); ); click = 0; );
      zhit ? ( tipon = 1; tipforce = 0; tipstr = szoom == id ? "Back to all sections (Esc or key Z)" : "Zoom: this section alone, larger (key Z with the mouse over it)"; tipx = zx; tipy = ay + 16; );
    );
// reset this module (after the magnifier, or where it would be)
    (id == 1 || id == 3 || id == 4 || id == 5 || id == 6 || id == 7 || id == 10 || id == 14 || id == 15) && !(basic && id != 1 && id != 3 && id != 5 && id != 7) ? (
      gfx_setfont(2); rsix = x + 28 + tws(s) + ((!tabm && (!basic || id == 1 || id == 3 || id == 5 || id == 7)) ? 40 : 24); rsih = sqr(mx - rsix) + sqr(my - ay) < 42;
      col(rsih ? c : cMUTED, 1); ic_reset(rsix, ay);
      click && rsih ? ( modulereset(id); click = 0; );
      rsih ? ( tipon = 1; tipforce = 0; tipstr = "Reset: this module back to its defaults (on / off stays) – Ctrl/Cmd+Z undoes it"; tipx = rsix; tipy = ay + 16; );
    );
    head ? tipbox(x + 2, y + 2, hw - 14, 20, vlock ? "GUI locked – unlock it with Lock GUI at the right of the signal-flow strip" : basic ? (op ? "Click: fold the four sections (Alt+click: fold everything)" : "Click: open the four sections (Alt+click: open everything)")
                                                    : (op ? "Click: fold this section (Alt+click: fold everything)" : "Click: open this section (Alt+click: open everything)"));
  );
);
// ---- SIGNAL-FLOW STRIP: items 0 IN, 1 FILTER, 2 SOURCE, 3 DELAY, 4 ROOM, 5 BARRIER, 6 OUT ----
function mtname(p) ( p == 0 ? "Circle" : (p == 1 ? "Left - right" : (p == 2 ? "Front - back" : (p == 3 ? "Up - down" : (p == 4 ? "Pass-by" : (p == 5 ? "Square" : (p == 6 ? "Wave" : "Drawn")))))); );
// Returns the tempo-sync label for index q: 1..8 maps to "8 bars", "4 bars" ... "1 bar", "1/2" ... "1/16"
function mttempo(q) ( q == 1 ? "8 bars" : (q == 2 ? "4 bars" : (q == 3 ? "2 bars" : (q == 4 ? "1 bar" : (q == 5 ? "1/2" : (q == 6 ? "1/4" : (q == 7 ? "1/8" : "1/16")))))); );
// Builds the Motion status text into #sts: "off", or path name plus tempo-sync division or rate in Hz
function mtstat() local(q) (
  slider100 < 0.5 ? strcpy(#sts, "off") : (
    q = floor(slider108 + 0.5);
    q > 0 ? sprintf(#sts, "%s · %s", mtname(floor(slider102 + 0.5)), mttempo(q)) : sprintf(#sts, "%s · %.2f Hz", mtname(floor(slider102 + 0.5)), slider103); );
);
// Returns the signal-flow strip item name for index q (0 IN, 1 FILTER, 2 SOURCE, 3 DELAY, 4 ROOM, 5 BARRIER, 6 OUT, 7 MOTION)
function stname(q) ( q == 7 ? "MOTION" : q == 0 ? "IN" : (q == 1 ? "FILTER" : (q == 2 ? "SOURCE" : (q == 3 ? "DELAY" : (q == 4 ? "ROOM" : (q == 5 ? "BARRIER" : "OUT"))))); );
// Returns the theme text color for signal-flow strip item q (Output color for q 0 and 6)
function stcol(q) ( q == 7 ? cMOTT : q == 1 ? cFLTT : (q == 2 ? cSRTT : (q == 3 ? cDLTT : (q == 4 ? cRMTT : (q == 5 ? cBARTT : cIOTT)))); );
// Returns true if strip item q is active (Filter and Room are on when their bypass sliders are below 0.5; In/Out always 1)
function ston(q) ( q == 7 ? slider100 >= 0.5 : q == 1 ? slider39 < 0.5 : (q == 2 ? slider85 >= 0.5 : (q == 3 ? slider65 >= 0.5 : (q == 4 ? slider36 < 0.5 : (q == 5 ? slider41 >= 0.5 : 1)))); );
function stsolo(q) ( q == 2 ? 1 : (q == 3 ? 2 : (q == 4 ? 3 : 0)); );   // solo code of an item (0 = cannot be soloed)
// Returns the tooltip text for signal-flow strip item q
function stip(q) (
  q == 7 ? "Motion: source or listener on a path – click = on / off (it glides in and out)" : q == 0 ? "Input gain into the plugin (the In knob in Output)" : (
  q == 1 ? "Filter: low / high cut – before the room (Pre) or after it (Post). Click = on / off" : (
  q == 2 ? "Source model (phone, radio …): click = on / off to compare · Shift+click = solo: only the source model – no delay, no room" : (
  q == 3 ? "Delay before the room: click = on / off · Shift+click = solo: only the echoes – no source, no room" : (
  q == 4 ? "Room + reverb: click = on / off · Shift+click = solo: only the room sound – reflections and reverb, no direct sound, no delay" : (
  q == 5 ? "Barrier between source and listener: click = on / off (the type is remembered)" : "Output gain and limiter")))));
);
// barrier on / off: off remembers the type, on brings it back
function bartog() local(t) ( t = floor(slider41 + 0.5); t > 0 ? ( barL = t; slider41 = 0; ) : ( slider41 = min(3, max(1, barL)); bcenter(); ); sauto1(41); );
// Toggles strip item q on/off (Filter, Source, Delay, Room sliders, Barrier via bartog, Motion via setp) and notifies automation
function sttog(q) (
  q == 1 ? ( slider39 = slider39 >= 0.5 ? 0 : 1; sauto1(39); );
  q == 2 ? ( slider85 = slider85 >= 0.5 ? 0 : 1; sauto1(85); );
  q == 3 ? ( slider65 = slider65 >= 0.5 ? 0 : 1; sauto1(65); );
  q == 4 ? ( slider36 = slider36 >= 0.5 ? 0 : 1; sauto1(36); );
  q == 5 ? bartog();
  q == 7 ? setp(100, slider100 >= 0.5 ? 0 : 1);   // Motion in the chain
);
// status text of an item -> #sts
function ststat(q) local(t, i, e)
(
  q == 7 ? mtstat() : q == 0 ? ( slider40 <= -71.9 ? strcpy(#sts, "-inf dB") : ( abs(slider40) < 0.05 ? strcpy(#sts, "0 dB") : sprintf(#sts, "%+.1f dB", slider40); ); slider110 >= 0.5 ? ( abs(slider40) < 0.05 ? strcpy(#sts, "mono") : strcat(#sts, " mono"); ); ) : (
  q == 1 ? ( slider39 >= 0.5 ? strcpy(#sts, "off") : (
      slider24 >= 0.5 && slider27 >= 0.5 ? sprintf(#sts, "%.0f Hz – %.1f kHz", slider25, slider28 / 1000) : (
      slider24 >= 0.5 ? sprintf(#sts, "low cut %.0f Hz", slider25) : (
      slider27 >= 0.5 ? sprintf(#sts, "high cut %.1f kHz", slider28 / 1000) : strcpy(#sts, "no cut set") ) ); ); ) : (
  q == 2 ? ( slider85 < 0.5 ? strcpy(#sts, "off") : (
      strcpy(#sts, srcname(fzsel)); e = 0; i = 47; loop(13, abs(slider(i)) > 0.0005 ? e = 1; i += 1; ); e ? strcat(#sts, " *"); fzsel > 0 && (slider111 > 0.05 || slider112 > 0.05 || slider113 > 0.05 || slider114 >= 0.5) ? strcat(#sts, " · call"); ); ) : (
  q == 3 ? ( slider65 < 0.5 ? strcpy(#sts, "off") : (
      t = dpmatch(); t >= 0 ? strcpy(#sts, dpname(t)) : (
      gdtm ? ( strcpy(#sts, "L "); strcat(#sts, dnname(slider72)); strcat(#sts, " · R "); strcat(#sts, dnname(gdlt ? slider72 : slider73)); )
           : sprintf(#sts, "L %.0f · R %.0f ms", gtL, gtR); ); ); ) : (
  q == 4 ? ( slider36 >= 0.5 ? strcpy(#sts, "off") : strcpy(#sts, #rnm); ) : (
  q == 5 ? ( t = floor(slider41 + 0.5);
      t == 0 ? strcpy(#sts, "off") : ( t == 1 ? strcpy(#sts, "Wall") : (
      slider43 >= 0.5 ? sprintf(#sts, t == 2 ? "Window · %.0f°" : "Door · %.0f°", slider43 * 0.9) : strcpy(#sts, t == 2 ? "Window" : "Door") ); ); ) : (
    slider22 <= -71.9 ? strcpy(#sts, "-inf dB") : ( abs(slider22) < 0.05 ? strcpy(#sts, "0 dB") : sprintf(#sts, "%+.1f dB", slider22); );
  ))))));
);
// DECAY BY BAND: reverb time RT60 at frequency f, as the reverb lines play it - averaged over the 8 lines. Per line and pass:
// gain fg x (one-pole high damping) + (fgL - fg) x (one-pole low shelf), the same filters as in @sample
// (GUI copies: a JSFX function keeps its locals in one place - the audio thread designs with bqdb / fqresp at the same time)
function bqdbG(cb, f) local(w, c1, s1, c2, s2, nr, ni, dr, di)
(
  w = 2 * $pi * f / srate; c1 = cos(w); s1 = sin(w); c2 = cos(2 * w); s2 = sin(2 * w);
  nr = cb[0] + cb[1] * c1 + cb[2] * c2; ni = -(cb[1] * s1 + cb[2] * s2);
  dr = 1 + cb[3] * c1 + cb[4] * c2; di = -(cb[3] * s1 + cb[4] * s2);
  10 * log10(max(0.0000000001, (nr * nr + ni * ni) / max(0.0000000001, dr * dr + di * di)));
);
// Returns the gain in dB at frequency f: offset g0 plus the sum of six biquad bands stored at coefficient base cb (5 slots apart)
function fqrespG(cb, g0, f) ( g0 + bqdbG(cb, f) + bqdbG(cb + 5, f) + bqdbG(cb + 10, f) + bqdbG(cb + 15, f) + bqdbG(cb + 20, f) + bqdbG(cb + 25, f); );
// Returns estimated RT60 in seconds at frequency f, averaged over the 8 delay lines (line length and gain plus filter response); capped at 99
function rtband(f) local(k, gdb, acc)
(
  acc = 0; k = 0;
  loop(8,
    gdb = min(-0.0000001, 20 * log10(max(0.0000001, fqG[k])) + fqrespG(fqC + k * 30, 0, min(f, 0.45 * srate)));
    acc += -60 * flen[k] / srate / gdb;
    k += 1;
  );
  min(99, acc / 8);
);
// ---- OUTPUT section helpers: spectrum analysis, pixel curves, level bars (drawio below) ----
// SPECTRUM: the last 8192 samples (SPFN) of input and output (mono sum), Hann window, FFT, 1/24-octave bands from 31.5 Hz (dBFS of a
// sine), shown with fast rise / slower fall; when the audio stops the curves sink
function spcalc() local(k, i, b, fc, k0, k1, kfr, fr, pw, v, ring, dst, w0, bh, bw6)
(
// FFT 8192, 1/24-octave bands; where a band is narrower than two FFT bins (the bass) the power is interpolated
  // between the neighboring bins in log frequency - no stair steps. Level as before (the power of a 1/6-octave band).
  !spWinOk ? ( i = 0; loop(SPFN, spWin[i] = 0.5 - 0.5 * cos(2 * $pi * i / SPFN); i += 1; ); spWinOk = 1; );
  bh = srate / SPFN; k = 0;
  loop(2,
    ring = k == 0 ? spRI : spRO; dst = k == 0 ? spBI : spBO; w0 = spw;
    i = 0; loop(SPFN, SPB[2 * i] = ring[(w0 + i) & 8191] * spWin[i]; SPB[2 * i + 1] = 0; i += 1; );
    fft(SPB, SPFN); fft_permute(SPB, SPFN);
    b = 0;
    loop(SPN,
      fc = 31.5 * 2 ^ (b / 24); bw6 = fc * (2 ^ (1 / 12) - 2 ^ (-1 / 12)) / bh;          // width of a 1/6-octave band in bins
      k0 = max(1, floor(fc * 2 ^ (-1 / 48) / bh + 0.5)); k1 = min(SPFN / 2 - 1, max(k0, floor(fc * 2 ^ (1 / 48) / bh + 0.5)));
      k1 - k0 >= 2 ? (
        pw = 0; i = k0; loop(k1 - k0 + 1, pw += sqr(SPB[2 * i]) + sqr(SPB[2 * i + 1]); i += 1; ); pw /= (k1 - k0 + 1);
      ) : (
        kfr = max(1, min(SPFN / 2 - 2, fc / bh)); i = floor(kfr); fr = kfr - i;
        pw = (sqr(SPB[2 * i]) + sqr(SPB[2 * i + 1])) * (1 - fr) + (sqr(SPB[2 * i + 2]) + sqr(SPB[2 * i + 3])) * fr;
      );
      // 6291456 = 3/32 x SPFN^2: Hann-window FFT power of a full-scale sine summed over its bins -> 0 dBFS; pw x bw6 = power of a 1/6-octave band
      v = 10 * log10(pw * max(1, bw6) / 6291456 + 0.0000000000001);
      dst[b] += (v - dst[b]) * (v > dst[b] ? 0.6 : 0.18);
      b += 1;
    );
    k += 1;
  );
);
// PIXEL CURVES (as in fine analyser / EQ plugins): one value per SCREEN pixel column in cvY (screen y), drawn
// straight in screen pixels - the area as 1 px columns with an anti-aliased top pixel, the line as anti-aliased 1 px
// segments (a second, fainter pass makes it about 1.6 px). No polygons, no stacked lines: smooth at any scale.
function cvfill(cvx0, n, cvyb, c, al, cvyt) local(i, t, tq, r, g, b, H, a0)
(
// each 1 px column fades towards the bottom (alpha falls by 85 % over the height of the graph)
  r = floor(c / 65536) / 255; g = (floor(c / 256) % 256) / 255; b = (c % 256) / 255; H = max(1, cvyb - cvyt); i = 0;
  loop(n,
    t = min(cvyb, cvY[i]); tq = ceil(t); a0 = al * (1 - 0.85 * (tq - cvyt) / H) * cfade;
    tq < cvyb ? gfx_gradrect(cvx0 + i, tq, 1, cvyb - tq, r, g, b, a0, 0, 0, 0, 0, 0, 0, 0, -al * 0.85 * cfade / H);
    tq - t > 0.02 ? ( col(c, a0 / max(0.0001, cfade) * (tq - t)); gfx_rect(cvx0 + i, tq - 1, 1, 1); );
    i += 1;
  );
);
// Draws the polyline cvY[0..n-1] at screen x from cvx0 in color c, alpha al, adding a faint second pass for lines wider than 1.2
function cvline(cvx0, n, c, al, cvw) local(i)
(
  col(c, al); i = 1; loop(n - 1, gfx_line(cvx0 + i - 1, cvY[i - 1], cvx0 + i, cvY[i], 1); i += 1; );
  cvw > 0.6 ? ( col(c, al * 0.75); i = 1; loop(n - 1, gfx_line(cvx0 + i - 1, cvY[i - 1] + 0.7, cvx0 + i, cvY[i] + 0.7, 1); i += 1; ); );   // stronger curves - a second pass for every size, ...
  cvw > 1.2 ? ( col(c, al * 0.55); i = 1; loop(n - 1, gfx_line(cvx0 + i - 1, cvY[i - 1] - 0.7, cvx0 + i, cvY[i] - 0.7, 1); i += 1; ); );   // ... a third one above it for the larger ones
);
function spv(a, b) ( min(-6, max(-90, 0.25 * a[max(0, min(SPN - 1, b - 1))] + 0.5 * a[max(0, min(SPN - 1, b))] + 0.25 * a[max(0, min(SPN - 1, b + 1))])); );   // a light smoothing over neighboring bands (calm, still detailed)
// Catmull-Rom spline value (dB) between bands b and b+1 at fraction t; a = the smoothed bands (spSm, filled once per curve by spsmooth)
// spv ran four times per screen pixel (12 clamped reads each) - now once per band, the spline reads the array
function spsmooth(a) local(b) ( b = 0; loop(SPN, spSm[b] = spv(a, b); b += 1; ); );
function spcr(a, b, t) local(p0, p1, p2, p3) ( p0 = a[max(0, b - 1)]; p1 = a[b]; p2 = a[min(SPN - 1, b + 1)]; p3 = a[min(SPN - 1, b + 2)]; 0.5 * (2 * p1 + (p2 - p0) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t * t + (3 * p1 - p0 - 3 * p2 + p3) * t * t * t); );
// Draws the live input/output spectrum panel at (x,y,w,h): log grid, interpolated per-pixel curves from spBI/spBO, labels and tooltip
function spdraw(x, y, w, h) local(b, px, py, lx, ly, f, gx, v, gy, t, n, spx0, spyb, k, bp)
(
  col(cBG, 1); RR(x, y, w, h, 4);
  col(cHAIR, 1); f = 100; loop(3, gx = x + log(f / 31.5) / log(16000 / 31.5) * w; LN(gx, y + 2, gx, y + h - 2); f *= 10; );
  col(cHAIR, 0.6); f = -24; loop(3, gy = y + h - 1 - (f + 90) / 84 * (h - 3); LN(x + 2, gy, x + w - 2, gy); f -= 24; );   // faint level lines (-24, -48, -72 dB)
// one value per screen pixel (cvfill / cvline)
  spx0 = floor(gox + x * gs + 0.5); n = min(4096, max(2, floor(w * gs))); spyb = floor(goy + (y + h - 1) * gs + 0.5);
  k = 0;
  loop(2,
    spsmooth(k == 0 ? spBI : spBO); b = 0;
    loop(n,
      bp = b / (n - 1) * (SPN - 1);
      v = min(-6, max(-90, spcr(spSm, floor(bp), bp - floor(bp))));
      cvY[b] = goy + (y + h - 1 - (v + 90) / 84 * (h - 3)) * gs; b += 1;
    );
    k == 0 ? cvfill(spx0, n, spyb, cDIM, 0.6, floor(goy + y * gs)) : ( cvfill(spx0, n, spyb, cIOTT, 0.22, floor(goy + y * gs)); cvline(spx0, n, cIOTT, 1, 1.6); );
    k += 1;
  );
  gfx_setfont(7); col(cDIM2, 1);
  txt("100", x + log(100 / 31.5) / log(16000 / 31.5) * w + 2, y + h - 6, 0); txt("1k", x + log(1000 / 31.5) / log(16000 / 31.5) * w + 2, y + h - 6, 0); txt("10k", x + log(10000 / 31.5) / log(16000 / 31.5) * w + 2, y + h - 6, 0);
  col(cMUTED, 1); txt("in", x + w - 26, y + 7, 2); col(cIOTT, 1); txt("out", x + w - 6, y + 7, 2);
  tipbox(x, y, w, h, "Spectrum, live: input (gray area) and output (colored line), 1/24 octave, 31.5 Hz – 16 kHz, -90 … -6 dBFS – what the room adds and takes away");
);
// ---- TAB ROW (page scale, between room view and module): the modules in signal order - dot = on / off,
// name = open the tab, S = solo (Source, Delay, Room; monitor only), Shift / Alt + click = solo too; All = the two columns.
// The splitter above it sets the height of the room view (double-click = automatic).
function tabname(id) ( id == 15 ? "Free field" : id == 1 ? "Room" : (id == 3 ? "Source" : (id == 4 ? (isfree ? "Reflectors" : "Walls") : (id == 5 ? "Barrier" : (id == 6 ? "Filter" : (id == 7 ? "Output" : (id == 14 ? "Motion" : "Delay")))))); );
// Returns the sample reference text shown under tab id's name in the tab row (e.g. "Living room", "low cut 120 Hz")
function tabref(id) ( id == 15 ? "outdoors" : id == 1 ? "Living room" : (id == 3 ? "Telephone" : (id == 4 ? "Plaster · α 0.04" : (id == 5 ? "Door · 90°" : (id == 6 ? "low cut 120 Hz" : (id == 7 ? "-12.5 dB" : (id == 14 ? "Front - back · 8 bars" : "L 320 · R 480")))))); );
function tabq(id) ( id == 1 ? 4 : (id == 3 ? 2 : (id == 5 ? 5 : (id == 6 ? 1 : (id == 10 ? 3 : (id == 14 ? 7 : 0))))); );   // its item in the signal-flow strip (0 = none)
// Returns the theme color for tab id's module (Delay color is the default)
function tabc(id) ( id == 15 ? cFFTT : id == 1 ? cRMTT : (id == 3 ? cSRTT : (id == 4 ? cWALTT : (id == 5 ? cBARTT : (id == 6 ? cFLTT : (id == 7 ? cIOTT : (id == 14 ? cMOTT : cDLTT)))))); );
// Fills tbo[] with the tab ids in signal order (pre/post Filter position by slider30); returns the tab count
function taborder() local(n) (
  n = 0; slider30 < 0.5 ? ( tbo[n] = 6; n += 1; );
  tbo[n] = 3; tbo[n + 1] = 10; tbo[n + 2] = 1; tbo[n + 3] = 15; tbo[n + 4] = 4; tbo[n + 5] = 5; n += 6;   // + Free field (its own tab after Room)
  tbo[n] = 14; n += 1; slider30 >= 0.5 ? ( tbo[n] = 6; n += 1; ); tbo[n] = 7; n + 1;   // + Motion (before the post filter and Output - the chain order)
);
// Selects the tab d steps (+1 / -1) from the current tab in taborder, wrapping around; writes vopt[46]
function tabstep(d) local(n, i, k) (
  n = taborder(); k = 0; i = 0; loop(n, tbo[i] == tabsel ? k = i; i += 1; );
  vopt[46] = tbo[(k + d + n) % n];
);
// CORRELATION bar: the scale is colored - red left of the middle (parts cancel in mono), green right (mono
// compatible); the fill from the middle to the value and the marker take the value's color (yellow close to 0)
function corrbar(x, yc, w, crv) local(cx0)
(
  col(cHAIR, 1); RR(x, yc - 1.5, w, 3, 1.5);
  col(0xD9534F, 0.22); RR(x, yc - 1.5, w / 2, 3, 1.5); col(0x2E9E6A, 0.22); RR(x + w / 2, yc - 1.5, w / 2, 3, 1.5);
  cx0 = x + w / 2; col(cDIM2, 1); R(cx0 - 0.5, yc - 4, 1, 8);
  crvC = cTEXT;
  crv <= 1.5 ? (
// the color zone has a margin (0.03) - a value close to 0 or 0.3 no longer flips the color (the mini meter blinked)
    crvZt = crv < 0 ? 0 : (crv < 0.3 ? 1 : 2);
    crvZt != crvZ ? ( (crvZt > crvZ ? crv >= (crvZ == 0 ? 0.03 : 0.33) : crv < (crvZ == 2 ? 0.27 : -0.03)) ? crvZ = crvZt; );
    crvC = crvZ == 0 ? 0xD9534F : (crvZ == 1 ? cWARN : 0x2E9E6A);
    col(crvC, 1); crv >= 0 ? R(cx0, yc - 1.5, w / 2 * crv, 3) : R(cx0 + w / 2 * crv, yc - 1.5, -w / 2 * crv, 3);
    R(x + w * (crv + 1) / 2 - 1.2, yc - 4, 2.4, 8);
  );
);
// MINI CORRELATION in the signal-flow strip (every tab and view, Expert): "Corr", the colored bar, the value - how the processing changes the
// mono compatibility while you work in Source, Room or Delay. Repainted alone on the meter-only frames (its place is kept)
function mcorrw() ( gfx_setfont(7); (WT >= 1500 ? tws("Corr") + 6 : 0) + 46 + tws("+0.00"); );   // its width (the word only on a wide window)
// Draws the compact correlation meter at (x,cy): label, 40 px bar, signed value text; stores its hit area in mcX/mcCY/mcGS/mcGX/mcGY and sets mcOn
function mcorrdraw(x, cy) local(crv, mlw)
(
  gfx_setfont(7); mlw = 0; WT >= 1500 ? ( mlw = tws("Corr") + 6; col(cMUTED, 1); txt("Corr", x, cy, 0); );
  crv = crvS; corrbar(x + mlw, cy, 40, crv);
  crv <= 1.5 ? sprintf(#w1, "%+.2f", crv) : strcpy(#w1, "–"); col(crvC, 1); gfx_setfont(7); txt(#w1, x + mlw + 46, cy, 0);
  mcW = mcorrw();
  tipbox(x, cy - 9, mcW, 18, "Correlation of the output, left against right (mono compatibility): green = mono compatible, yellow = very wide, red = parts cancel in a mono or TV downmix. The full meter and the goniometer are in Output");
  mcX = x; mcCY = cy; mcGS = gs; mcGX = gox; mcGY = goy; mcOn = 1;
);
// vopt layout per reflector r (base RFB + 6 r): +0 material (0 = off), +1 X, +2 Y, +3 angle, +4 length, +5 height; RFB + 12 + r = level (dB); RFB + 14 = both reflectors off
function rflset(r, m) local(o)
(
  o = RFB + r * 6; m < 0.5 && vopt[o] >= 0.5 ? rflLM[r] = vopt[o]; vopt[o] = m;
  m >= 0.5 && vopt[o + 4] < 0.5 ? ( vopt[o + 1] = max(0.8, slider2) * min(4, max(0.25, slider31)) * (r == 0 ? 0.92 : 0.08); vopt[o + 2] = max(0.8, slider3) * min(4, max(0.25, slider31)) * 0.5; vopt[o + 3] = 90; vopt[o + 4] = max(0.8, slider3) * min(4, max(0.25, slider31)) * 1.1; vopt[o + 5] = 4; );
);
// Draws the module tab row (dot on/off, name, status text, solo button) and handles clicks; shortens status texts to fit the window width
function tabrow() local(n, i, id, q, x, w, on, act, c, hit, dhit, shit, cy, sq, sx, nw, tdq)
(
  n = taborder(); cy = TBY + TBH / 2 - 1;   // the tab texts 3 px higher - more space between the status text (e.g. "Original") and the tab line
   x = tbX0 = max(8, (goxZ - goxN) / gsN + 0.2); i = 0;
// nine tabs - on a narrow window the status texts get shorter so the whole row stays inside the window (a name never gets cut)
  tbtot = 0; tbmin = 0;
  loop(n, id = tbo[i]; gfx_setfont(7); stw2 = tws(tabref(id)); gfx_setfont(2); tbnw = max(tws(tabname(id)), stw2); tbtot += 28 + tbnw + (stsolo(tabq(id)) > 0 ? 22 : 0) + 2 + 3 + 7; tbmin += max(0, tbnw - tws(tabname(id))); i += 1; );
  tbcut = min(1, max(0, tbtot - 7 - (WT - 8 - tbX0 - (stpwT > 0 ? stpwT + 30 : 0))) / max(1, tbmin)); i = 0;   // share of the status width that has to go (0 = all fit) // the row starts on the module's left edge - the first tab's frame runs straight into it
  loop(n,
    id = tbo[i]; q = tabq(id); c = tabc(id); act = id == tabsel; on = id == 15 ? isfree : (id == 4 && isfree ? vopt[RFB + 14] < 0.5 && (vopt[RFB] >= 0.5 || vopt[RFB + 6] >= 0.5) : (q > 0 ? (id == 1 ? ston(q) && !isfree : ston(q)) : 1)); sq = q > 0 ? stsolo(q) : 0; tdq = q > 0 || id == 15 || (id == 4 && isfree);   // Reflectors (Walls tab outdoors) - a round on / off button like Barrier // Free field and Room switch each other (the dot of the one that is off brings it back)
    q > 0 ? ststat(q == 6 ? 6 : q) : ( id == 7 ? ststat(6) : ( id == 14 ? mtstat() : ( id == 15 ? strcpy(#sts, isfree ? "outdoors" : "off") : ( id == 4 && isfree ? sprintf(#sts, "R1 %s · R2 %s", rflName(vopt[RFB]), rflName(vopt[RFB + 6])) : strcpy(#sts, #wmt) ) ) ) );
    gfx_setfont(7); stw2 = tws(tabref(id));   // a fixed width per tab - it does not jump with its values (longer status text is shortened)
    gfx_setfont(2); nw = max(tws(tabname(id)), stw2); nw -= (nw - tws(tabname(id))) * tbcut; w = 28 + nw + (sq > 0 ? 22 : 0) + 2;
    hit = inbox(x, TBY + 2, w, TBH); dhit = tdq && sqr(mx - (x + 12)) + sqr(my - cy) < 56;
    sx = x + w - 24; shit = sq > 0 && inbox(sx - 2, cy - 9, 20, 18);
    act ? ( tbAX = x; tbAW = w; col(c, 1); cCTLon ? ( RR(x - 1.2, TBY - 1.2, w + 2.4, TBH + 6.6, 7.2); col(szoom ? cSURF : mixc(cSURF, c, cCTLon == 2 ? 0.22 : 0.16), 1); RR(x, TBY, w, TBH + 6, 6); R(x, TAY - 2, w, 6); ) : ( RR(x - 1.6, TBY - 1.6, w + 3.2, TBH + 7, 7.4); col(cSURF, 1); RR(x, TBY, w, TBH + 6, 6); R(x, TAY - 2, w, 6); ); )   // the active tab: a frame in the module color, 1.6 wide (no line over the title)
        : ( col(hit ? cHAIR : cLINE, 1); RR(x, TBY + 3, w, TBH - 3, 6); );
    tdq ? ( on ? ( col(c, 1); CI(x + 12, cy, 4.8, 1); ) : ( col(cSURF, 1); CI(x + 12, cy, 4.8, 1); col(dhit ? cTEXT : cDIM2, 1); CI(x + 12, cy, 4.8, 0); CI(x + 12, cy, 4.0, 0); ); )
          : ( col(c, 1); RR(x + 9.5, cy - 6, 3, 12, 1.5); );
    col(act ? cTEXT : (on ? cMUTED : cDIM2), 1); gfx_setfont(2); txt(tabname(id), x + 24, cy - 5, 0);
    col(on ? cMUTED : cDIM2, 1); gfx_setfont(7); txtfit(#sts, x + 24, cy + 7, nw);
    sq > 0 ? (
      soloM == sq ? ( col(cAMB, 1); RR(sx, cy - 7, 16, 14, 4); col(0xFFFFFF, 1); ) : ( col(shit ? cTEXT : cBORD, 1); RRO(sx, cy - 7, 16, 14, 4); col(shit ? cTEXT : cMUTED, 1); );   // (S centered on both lines)
      gfx_setfont(6); txt("S", sx + 8, cy, 1);
    );
    click && (shit || (hit && (mouse_cap & 24) && sq > 0)) ? ( soloM == sq ? ( soloM = 0; ) : ( !(id == 1 ? ston(q) : on) ? sttog(q); soloM = sq; ); click = 0; ) : (
    click && dhit ? ( id == 4 && isfree ? ( on ? ( vopt[RFB + 14] = 1; ) : ( vopt[RFB + 14] = 0; vopt[RFB] < 0.5 && vopt[RFB + 6] < 0.5 ? rflset(0, 1); ); ) :   // (on with no reflector set: R1 becomes a facade, as Barrier starts with a wall)
    ( id == 15 || (id == 1 && isfree) ? ( modeset(id == 15 && isfree ? 0 : (id == 1 ? 0 : 1)); snlast = 0; ) : sttog(q); ); click = 0; ) : (
    click && hit ? ( vopt[46] = id; click = 0; ); ); );
    shit ? ( tipon = 1; tipforce = 0; tipx = sx + 8; tipy = TBY - 12;
      tipstr = soloM == sq ? "Solo on – click to end it (or Esc)" : (sq == 1 ? "Solo: only the source model – no delay, no room. Monitor only, not saved, ends when the window closes"
        : (sq == 2 ? "Solo: only the echoes of the delay – no source, no room. Monitor only, not saved, ends when the window closes" : "Solo: only the room – reflections and reverb, no direct sound, no delay. Monitor only, not saved, ends when the window closes")); ) : (
    dhit ? ( tipon = 1; tipforce = 0; tipx = x + 12; tipy = TBY - 12; tipstr = id == 4 && isfree ? (on ? "Reflectors are on – click: both off (their settings stay)" : "Reflectors are off – click: on again (none set: R1 becomes a facade)") : id == 15 ? (isfree ? "Free field is on – click: back to the room" : "Free field: outdoors, no walls, no reverb – click: switch it on (the room is off then)") : (id == 1 && isfree ? "Free field is on, the room is off – click: back to the room" : (on ? "On – click: bypass this module" : "Bypassed – click: on")); ) : (
    hit ? ( tipon = 1; tipx = x + w / 2; tipy = TBY - 12; tipstr = sq > 0 ? "Open this module · Shift+click = solo · keys 1 … 9 open the tabs, [ and ] = previous / next" : "Open this module · keys 1 … 9 open the tabs, [ and ] = previous / next"; ); ); );
    x += w + 3;
    i < n - 1 ? ( col(cDIM2, 1); gfx_setfont(6); txt(id == 4 ? "+" : (id == 15 ? "›" : (id == 1 ? "/" : (id == 7 ? "·" : "›"))), x + 2, cy, 1); x += 7; );
    i += 1;
  );
  tbEndX = x;   // All and Fit moved under the view modes (Tabs | All | Fit)
  // solo / hear sign (violet pill with x, right of the tabs): names the active solo or room-part listening; a click ends it
  stpwT > 0 ? (
    gfx_setfont(7); x = WT - 14 - tws("Latency 1.5 ms") - 14 - stpwT;
    col(cAMB, 1); RR(x, cy - 8, stpwT, 16, 8); col(0xFFFFFF, 1); gfx_setfont(6); txt(#stp, x + 9, cy, 0);
    LNW(x + stpwT - 13, cy - 2.5, x + stpwT - 8, cy + 2.5, 1.3); LNW(x + stpwT - 13, cy + 2.5, x + stpwT - 8, cy - 2.5, 1.3);
    click && inbox(x, cy - 9, stpwT, 18) ? ( soloM > 0 ? ( soloM = 0; ) : ( vopt[18] = 0; ); click = 0; );
    tipbox(x, cy - 9, stpwT, 18, soloM > 0 ? "Solo: only this module is heard – click to end it (or Esc). Monitor only: not saved, ends when the window closes" : "Hear: only a part of the room sound plays (set in Room) – click: all three again");
  );
  // latency readout: the limiter looks 1.5 ms ahead - always on, reported to REAPER (it compensates)
  sprintf(#tlat, "Latency %.1f ms", pdc_delay / srate * 1000); gfx_setfont(7);
  tbEndX + 12 < WT - 14 - tws(#tlat) ? ( col(cMUTED, 1); txt(#tlat, WT - 14, cy, 2);   // only where it does not run into the tab row
  tipbox(WT - 14 - tws(#tlat), TBY + 2, tws(#tlat), TBH, "Latency of the plugin: the limiter looks 1.5 ms ahead. It is always on and reported to REAPER, which compensates it"); );
  // splitter between room view and tabs: drag = height of the room view, double-click = automatic
  hit = !vhid && !vgone && my >= VY + VH && my < TBY && mx >= VX && mx < VX + VW;
  click && hit ? ( time_precise() - tbdt < 0.35 ? ( vopt[47] = 0; vopt[48] = 0; vopt[35] = 0; tbdrag = 0; ) : ( tbdrag = 1; tbdy = my - VH; ); tbdt = time_precise(); click = 0; );
  tbdrag ? ( (mouse_cap & 1) ? ( tbnv = floor(max(VFH + 30, min(tabmax, my - tbdy)) + 0.5); (vopt[48] >= 0.5 || vopt[35] >= 0.99) && abs(tbnv - VH) < 1 ? 0 : ( vopt[47] = tbnv; vopt[35] = 0; vopt[48] = 0; ); ) : ( tbdrag = 0; ); );   // also with Fit on - moving it ends Fit
// the splitter bar sits 4 px under the view and 9 px above the tab lines
  !vhid && !vgone ? ( col(hit || tbdrag ? cTEXT : cDIM2, 1); RR(VX + VW / 2 - 18, VY + VH + 4, 36, 3, 1.5); );
  hit ? ( tipon = 1; tipx = VX + VW / 2; tipy = TBY - 14; tipstr = "Drag: Stage View higher / lower · double-click: automatic"; );
);
// VIEW: display options as underlined words (display only, not automatable); "View" itself = all on / off
function viewgrp(vgx, vgd) local(gk, gq, vtx, von, nv)
(
col(cTEXT, 1); gfx_setfont(2); txt("View", vgx, 17 + vgd, 0);
click && inbox(vgx - 2, 7 + vgd, 26, 20) ? ( nv = showinfo ? 0 : 1; !basic ? ( vopt[1] = nv; vopt[2] = nv; ); vopt[3] = nv; vopt[6] = nv; );
tipbox(vgx - 2, 7 + vgd, 26, 20, basic ? "View: switch tips and waves on or off at once" : "View: switch tips, paths, direct line and waves on or off at once");
gk = 0;
loop(basic ? 2 : 5,
  gq = basic ? (gk == 0 ? 0 : 3) : gk;   // Basic: Tips, Waves
  vtx = vgx + 26 + gk * 30;
  von = gq == 0 ? tipsOn : (gq == 1 ? showpaths : (gq == 2 ? showdirect : (gq == 3 ? wavesOn : !hidew)));
  col(von ? cTEXT : cMUTED, 1); gfx_setfont(von ? 6 : 4); txt(gq == 0 ? "Tips" : (gq == 1 ? "Paths" : (gq == 2 ? "Direct" : (gq == 3 ? "Waves" : "Walls"))), vtx + 13, 17 + vgd, 1);
  von ? ( col(cRMTT, 1); LNW(vtx + 3, 24 + vgd, vtx + 23, 24 + vgd, 1.4); );
  click && inbox(vtx, 7 + vgd, 28, 20) ? (
    gq == 0 ? ( vopt[3] = !tipsOn; ); gq == 1 ? ( vopt[1] = !showpaths; ); gq == 2 ? ( vopt[2] = !showdirect; ); gq == 3 ? ( vopt[6] = !wavesOn; ); gq == 4 ? ( vopt[10] = !hidew; );
  );
  tipbox(vtx, 7 + vgd, 28, 20, gq == 0 ? "Tips: help texts on mouse-over" : (gq == 1 ? "Paths: reflection paths and the critical-distance circle" : (gq == 2 ? "Direct: the direct-sound line between source and listener"
    : (gq == 3 ? "Waves: how the sound spreads – the direct wavefront (blue) and the wall reflections (violet) as moving air, slowed down so you can follow it" : (hidew ? "Walls are hidden – click to show them again (the sound is the same)" : "Hide the walls in the Stage View: easier to grab source and listener – the sound does not change")))));
  gk += 1;
);
);
// ---- BASIC: a row of typical rooms as tiles - line icon, name, reverb time; click = load the room
// (like the room preset menu: source and listener stay where they are when they fit)
// film locations (; the set was changed in - see the next line)
// sorted by reverb time, Outdoors (no reverb) first; Bathroom and Classroom in, Gym and Cave out; "Religious" (neutral icon)
// preset index (PT + k * PRS) of the factory room behind Basic tile i; the tile row itself starts with Outdoors (see basictiles)
function bastile(i) ( i == 0 ? 13 : (i == 1 ? 3 : (i == 2 ? 2 : (i == 3 ? 10 : (i == 4 ? 4 : (i == 5 ? 0 : (i == 6 ? 26 : 25)))))); );
// Returns the Basic-view room preset tile name for index i (0 Car ... 7 Religious)
function bastname(i) ( i == 0 ? "Car" : (i == 1 ? "Small room" : (i == 2 ? "Living room" : (i == 3 ? "Classroom" : (i == 4 ? "Hallway" : (i == 5 ? "Bathroom" : (i == 6 ? "Warehouse" : "Religious")))))); );
// line icons, 16 x 14 units, top left x, y - drawn in the current color
function tileicon(i, x, y)
(
  i == 0 ? ( LNW(x, y + 10, x + 16, y + 10, 1); LNW(x + 2, y + 10, x + 4.5, y + 4, 1); LNW(x + 4.5, y + 4, x + 11, y + 4, 1); LNW(x + 11, y + 4, x + 14, y + 10, 1);
             CIF(x + 4.5, y + 11.5, 1.8, 0); CIF(x + 11.5, y + 11.5, 1.8, 0); ) : (                                 // car
  i == 1 ? ( LNW(x + 1, y + 3, x + 1, y + 13, 1); LNW(x + 1, y + 13, x + 15, y + 13, 1); LNW(x + 1, y + 9, x + 15, y + 9, 1);
             LNW(x + 15, y + 9, x + 15, y + 13, 1); RRO(x + 3, y + 6, 4, 3, 1); ) : (                                   // bed
  i == 2 ? ( RRO(x + 1, y + 7, 14, 5, 1.5); LNW(x + 3, y + 7, x + 3, y + 3.5, 1); LNW(x + 3, y + 3.5, x + 13, y + 3.5, 1); LNW(x + 13, y + 3.5, x + 13, y + 7, 1);
             LNW(x + 3, y + 12, x + 3, y + 14, 1); LNW(x + 13, y + 12, x + 13, y + 14, 1); ) : (                       // sofa
  i == 3 ? ( LNBOX(x + 2, y, 12, 7.5, 0.9); LNW(x + 4.5, y + 3, x + 9, y + 3, 0.8); LNW(x + 4.5, y + 5, x + 7.5, y + 5, 0.8);
             LNW(x + 1, y + 11, x + 15, y + 11, 1); LNW(x + 3, y + 11, x + 3, y + 14, 0.9); LNW(x + 13, y + 11, x + 13, y + 14, 0.9); ) : (   // classroom: board and desk
  i == 4 ? ( LNBOX(x, y, 16, 14, 0.9); LNBOX(x + 5.5, y + 4.5, 5, 5, 0.8); LNW(x, y, x + 5.5, y + 4.5, 0.8); LNW(x + 16, y, x + 10.5, y + 4.5, 0.8);
             LNW(x, y + 14, x + 5.5, y + 9.5, 0.8); LNW(x + 16, y + 14, x + 10.5, y + 9.5, 0.8); ) : (                // corridor
  i == 5 ? ( LNW(x + 0.5, y + 8, x + 15.5, y + 8, 1); LNW(x + 1.5, y + 8, x + 3, y + 12.5, 1); LNW(x + 3, y + 12.5, x + 13, y + 12.5, 1); LNW(x + 13, y + 12.5, x + 14.5, y + 8, 1);
             LNW(x + 4, y + 12.5, x + 3.5, y + 14, 0.9); LNW(x + 12, y + 12.5, x + 12.5, y + 14, 0.9);
             LNW(x + 3, y + 8, x + 3, y + 1.5, 0.9); LNW(x + 3, y + 1.5, x + 6.5, y + 1.5, 0.9); LNW(x + 6.5, y + 1.5, x + 6.5, y + 3, 0.9); CIF(x + 6.5, y + 4.8, 0.6, 1); ) : (   // bathroom: tub and tap
  i == 6 ? ( LNW(x, y + 14, x, y + 6, 1); LNW(x + 16, y + 14, x + 16, y + 6, 1); LNW(x, y + 14, x + 16, y + 14, 1);
             LNW(x, y + 6, x + 5.3, y + 2, 1); LNW(x + 5.3, y + 2, x + 5.3, y + 6, 1); LNW(x + 5.3, y + 6, x + 10.6, y + 2, 1);
             LNW(x + 10.6, y + 2, x + 10.6, y + 6, 1); LNW(x + 10.6, y + 6, x + 16, y + 2, 1); LNW(x + 16, y + 2, x + 16, y + 6, 1); ) : (   // sawtooth hall
             LNW(x + 1, y + 14, x + 1, y + 6.5, 1); LNW(x + 15, y + 14, x + 15, y + 6.5, 1); LNW(x - 0.5, y + 14, x + 16.5, y + 14, 1);
             ARCS(x + 8, y + 6.5, 7, -0.5 * $pi, 0.5 * $pi, 1);
             LNW(x + 6, y + 14, x + 6, y + 10.5, 0.9); LNW(x + 10, y + 14, x + 10, y + 10.5, 0.9); ARCS(x + 8, y + 10.5, 2, -0.5 * $pi, 0.5 * $pi, 0.9);
             LNW(x + 4, y + 7, x + 4, y + 10, 0.9); LNW(x + 12, y + 7, x + 12, y + 10, 0.9); )   // religious: a vaulted hall (no symbol of one faith)
  ))))));
);
// ---- BASIC: a row of film locations as small tiles - icon, name, reverb time; click = load the room
// lower tiles (28 instead of 34), Outdoors first
function basictiles() local(n, i, k, x, w, b, hit, on, ty, th, j)
(
  n = 9; w = (WT - 16 - (n - 1) * 6) / n; ty = 64; th = 28;
  i = 0;
  loop(n,
    x = 8 + i * (w + 6); hit = inbox(x, ty, w, th);
    i == 0 ? ( on = isfree;
      col(cSURF, 1); RR(x, ty, w, th, 6);
      on ? ( col(cRMTT, 1); RRO(x, ty, w, th, 6); RRO(x + 0.6, ty + 0.6, w - 1.2, th - 1.2, 5.6); ) : ( hit ? ( col(cBORD, 1); RRO(x, ty, w, th, 6); ); );
      col(on ? cRMTT : cMUTED, 1); LNW(x + 6, ty + 21, x + 22, ty + 21, 1); LNW(x + 8, ty + 21, x + 12, ty + 13, 1); LNW(x + 12, ty + 13, x + 16, ty + 21, 1); LNW(x + 13, ty + 21, x + 18, ty + 15, 1); LNW(x + 18, ty + 15, x + 22, ty + 21, 1); CIF(x + 19, ty + 9, 2, 0);
      col(cTEXT, 1); gfx_setfont(6); txtfit("Outdoors", x + 27, ty + 9, w - 29); col(cMUTED, 1); gfx_setfont(7); txt("no room", x + 27, ty + 20, 0);
      click && hit ? ( modeset(1); snlast = 0; click = 0; );
      hit ? ( tipon = 1; tipx = x + w / 2; tipy = ty + th + 12; tipstr = "Outdoors (free field): no walls, no reverb – source and listener still set distance, direction and panning. Click a room tile to go back inside"; );
    ) : (
    j = i - 1; k = bastile(j); b = PT + k * PRS; on = !isfree && roommatch(k);
    col(cSURF, 1); RR(x, ty, w, th, 6);
    on ? ( col(cRMTT, 1); RRO(x, ty, w, th, 6); RRO(x + 0.6, ty + 0.6, w - 1.2, th - 1.2, 5.6); ) : ( hit ? ( col(cBORD, 1); RRO(x, ty, w, th, 6); ); );
    col(on ? cRMTT : cMUTED, 1); tileicon(j == 3 ? 3 : (j == 4 ? 4 : j), x + 6, ty + 7);
    col(cTEXT, 1); gfx_setfont(6); txtfit(bastname(j), x + 27, ty + 9, w - 29);
    sprintf(#w1, "%.2f s", presetrt(k)); col(cMUTED, 1); gfx_setfont(7); txt(#w1, x + 27, ty + 20, 0);
    click && hit ? ( loadroom(k); snlast = 0; click = 0; );
    hit ? ( tipon = 1; tipx = x + w / 2; tipy = ty + th + 12; sprintf(#rtip, "%s (preset “%s”) – %.1f × %.1f × %.1f m, reverb time %.2f s. Click: load this room – more rooms in the Room preset menu", bastname(j), roomname(k), b[0] * b[14], b[1] * b[14], b[2] * b[14], presetrt(k)); tipstr = #rtip; );
    );
    i += 1;
  );
);
// ---------- MY ROOMS ----------
// the room values of a user room (what a factory room sets, plus decay, reflectivity, source aim, room + barrier mix)
function urparam(i) ( i < 17 ? i + 1 : (i == 17 ? 19 : (i == 18 ? 20 : (i == 19 ? 21 : (i < 24 ? i + 11 : (i == 24 ? 36 : (i == 25 ? 38 : (i < 32 ? i + 15 : (i == 32 ? 61 : (i == 33 ? 62 : (i == 34 ? 63 : (i == 35 ? 91 : (i == 36 ? 92 : (i == 37 ? 95 : (i == 38 ? 96 : 97)))))))))))))); );
// Returns the number of used slots in the user room library (urb, URM slots of URL values)
function urcount() local(u, n) ( n = 0; u = 0; loop(URM, urb[u * URL] ? n += 1; u += 1; ); n; );
// Returns the category name for user room category c (room category name if c<6, else "Other")
function urcatname(c) ( c < 6 ? roomcatname(c) : "Other"; );
// text buffers (UTF-8 bytes): append a character / remove the last one; they return the new length
function bufadd(tb, tln, tmx, c) local(n)
(
  n = c < 128 ? 1 : (c < 2048 ? 2 : (c < 65536 ? 3 : 4));
  c >= 32 && tln + n <= tmx - 1 ? (
    n == 1 ? ( tb[tln] = c; ) : (
    n == 2 ? ( tb[tln] = 192 | (c >> 6); tb[tln + 1] = 128 | (c & 63); ) : (
    n == 3 ? ( tb[tln] = 224 | (c >> 12); tb[tln + 1] = 128 | ((c >> 6) & 63); tb[tln + 2] = 128 | (c & 63); ) : (
      tb[tln] = 240 | (c >> 18); tb[tln + 1] = 128 | ((c >> 12) & 63); tb[tln + 2] = 128 | ((c >> 6) & 63); tb[tln + 3] = 128 | (c & 63); )));
    tln += n; tb[tln] = 0;
  );
  tln;
);
// Removes the last UTF-8 character from byte buffer tb of length tln (zeroing its bytes); returns the new length
function bufback(tb, tln) local(c) ( c = 128; while ( tln > 0 && (c & 192) == 128 ? ( tln -= 1; c = tb[tln]; tb[tln] = 0; 1; ) : 0; ); tln; );
// Returns byte c lower-cased if it is an ASCII letter A-Z, otherwise unchanged
function lcb(c) ( c >= 65 && c <= 90 ? c + 32 : c; );
// search: the query (bytes) inside a name (bytes, 0 = end), letters A-Z without case
function bmatch(sb, qb, qn) local(i, j, ok, hit, sn)
(
  qn <= 0 ? 1 : (
    sn = 0; while ( sn < 63 && sb[sn] > 0 ? ( sn += 1; 1; ) : 0; );
    hit = 0; i = 0;
    loop(max(0, sn - qn + 1), !hit ? ( ok = 1; j = 0; loop(qn, ok && lcb(sb[i + j]) != lcb(qb[j]) ? ok = 0; j += 1; ); ok ? hit = 1; ); i += 1; );
    hit;
  );
);
// Copies string st (max 63 chars) into byte buffer b as characters, zero-terminated; returns b
function strtob(st, b) local(i, n) ( n = min(63, strlen(st)); i = 0; loop(n, b[i] = str_getchar(st, i, 'cu'); i += 1; ); b[n] = 0; b; );
// Returns the index of the user library room whose 24-byte name equals nb, or -1 if none
function urfind(nb) local(u, f, i, eq) ( f = -1; u = 0; loop(URM, urb[u * URL] && f < 0 ? ( eq = 1; i = 0; loop(24, urb[u * URL + 2 + i] != nb[i] ? eq = 0; i += 1; ); eq ? f = u; ); u += 1; ); f; );
// store the current room under the name in rsnb (same name = replaced); 0 = library full
function ursave() local(u, i, b)
(
  u = urfind(rsnb); u < 0 ? ( u = 0; while ( u < URM && urb[u * URL] ? ( u += 1; 1; ) : 0; ); );
  u < URM ? (
    b = urb + u * URL; b[0] = 1; b[1] = rscat; i = 0; loop(24, b[2 + i] = rsnb[i]; i += 1; );
    i = 0; loop(URP, b[26 + i] = slider(urparam(i)); i += 1; );
    1;
  ) : 0;
);
function urover(u) local(b, i) ( b = urb + u * URL; i = 0; loop(URP, b[26 + i] = slider(urparam(i)); i += 1; ); );   // same name and category, the current room
// Deletes user library slot u by shifting later slots down and clearing the last one
function urdel(u) local(v) ( v = u; loop(URM - 1 - u, memcpy(urb + v * URL, urb + (v + 1) * URL, URL); v += 1; ); memset(urb + (URM - 1) * URL, 0, URL); );
// load: like a factory room - "Keep when loading" keeps source + listener and / or the barrier (moved inside the new room)
function urload(u) local(b, i, sp, kp, kb, hz)
(
  b = urb + u * URL; kp = vopt[16] >= 0.5; kb = vopt[17] >= 0.5;
  snap_take();
  i = 0;
  loop(URP,
    sp = urparam(i);
    // "Keep when loading": skip the source / listener sliders (10-17, 61, 63) and the barrier sliders (41-46, 92, 95+); a stored 0 in slider 97 loads as its default 100
    !((kp && ((sp >= 10 && sp <= 17) || sp == 61 || sp == 63)) || (kb && ((sp >= 41 && sp <= 46) || sp == 92 || sp >= 95))) ? sset(sp, sp == 97 && b[26 + i] < 1 ? 100 : b[26 + i]);
    i += 1;
  );
  kp ? (
    hz = slider4 * slider31 - 0.1;
    slider10 = min(max(slider10, 0.1), slider2 - 0.1); slider11 = min(max(slider11, 0.1), slider3 - 0.1); slider12 = min(max(slider12, 0.1), hz);
    slider13 = min(max(slider13, 0.1), slider2 - 0.1); slider14 = min(max(slider14, 0.1), slider3 - 0.1); slider15 = min(max(slider15, 0.1), hz);
  );
  kb ? ( slider42 = min(max(slider42, 0.3), slider3 - 0.3); slider45 = min(max(slider45, 0), slider2); );
  selw = 0; snap_notify();
);
// the current settings are this user room (its room values)
function urmatch(u) local(b, i, ok) ( b = urb + u * URL; ok = b[0] > 0; i = 0; while ( ok && i < URP ? ( i != 32 && i != 34 && abs(slider(urparam(i)) - (i == 39 && b[26 + i] < 1 ? 100 : b[26 + i])) > 0.001 ? ok = 0; i += 1; 1; ) : 0; ); ok; );
// GONIOMETER: the output as a cloud of points, left against right - L up-left, R up-right, so mono = a vertical
// line, out of phase = a horizontal line, a wide mix = a round cloud. Automatic gain: the loudest point fills the circle
function gondraw(gx, gy, gr) local(k, l, r2, xx, yy, pk, sc, d)
(
  col(cBG, 1); RR(gx - gr - 4, gy - gr - 4, 2 * gr + 8, 2 * gr + 8, 5);
  col(cHAIR, 1); CI(gx, gy, gr, 0); d = gr * 0.7071;
  LNW(gx - d, gy - d, gx + d, gy + d, 1); LNW(gx + d, gy - d, gx - d, gy + d, 1);
  col(cMUTED, 1); gfx_setfont(7); txt("L", gx - d - 6, gy - d - 5, 1); txt("R", gx + d + 6, gy - d - 5, 1);
  !aidle ? (
    pk = 0.001; k = 0; loop(GONN, pk = max(pk, abs(gonB[2 * k]) + abs(gonB[2 * k + 1])); k += 1; );
    gonPk = pk > gonPk ? pk : gonPk * 0.97 + pk * 0.03;
    sc = gr * 0.95 * 1.4142 / gonPk; d = max(1, floor(1.3 * gs + 0.5));
    col(cIOTT, 0.5); k = 0;
    loop(GONN,
      l = gonB[2 * k]; r2 = gonB[2 * k + 1];
      xx = gox + (gx + (r2 - l) * 0.7071 * sc) * gs; yy = goy + (gy - (l + r2) * 0.7071 * sc) * gs;
      gfx_rect(floor(xx), floor(yy), d, d);
      k += 1;
    );
  );
  tipbox(gx - gr, gy - gr, 2 * gr, 2 * gr, "Goniometer: the output, left against right – a vertical line = mono, a horizontal line = out of phase, a round cloud = wide. Automatic gain: the loudest moment fills the circle");
);
// LEVEL BARS: one smooth bar in three zones (green up to -12 dB, amber up to -3 dB, red above) on a soft track
function mzc(z) ( z == 0 ? (darkmode ? 0x4CC38A : 0x2E9E6A) : (z == 1 ? cWARN : 0xE5484D); );
// Draws a vertical level bar at (x,y,w,h) for linear level lv (-60..0 dB scale), colored green/amber/red above 80% and 95%
function mbarV(x, y, w, h, lv) local(f, a, b)
(
  col(cHAIR, 1); RR(x, y, w, h, 2);
  f = lin2f(lv, -60, 0);
  f > 0.005 ? (
    a = min(f, 0.8); col(mzc(0), 1); R(x, y + h * (1 - a), w, h * a);
    f > 0.8 ? ( b = min(f, 0.95); col(mzc(1), 1); R(x, y + h * (1 - b), w, h * (b - 0.8)); );
    f > 0.95 ? ( col(mzc(2), 1); R(x, y + h * (1 - f), w, h * (f - 0.95)); );
  );
);
// Draws a horizontal rounded level bar at (x,y,w,h) for linear level lv (-60..0 dB scale), colored green/amber/red above 80% and 95%
function mbarH(x, y, w, h, lv) local(f, a, b)
(
  col(cHAIR, 1); RR(x, y, w, h, h / 2);
  f = lin2f(lv, -60, 0);
  f > 0.02 ? (
    a = min(f, 0.8); col(mzc(0), 1); RR(x, y, max(h, w * a), h, h / 2);
    f > 0.8 ? ( b = min(f, 0.95); col(mzc(1), 1); R(x + w * 0.8, y, w * (b - 0.8), h); );
    f > 0.95 ? ( col(mzc(2), 1); R(x + w * 0.95, y, w * (f - 0.95), h); );
  );
);
// Draws the OUTPUT section: meters with peak hold, spectrum, correlation, gain faders, limiter and detail/equal-loudness buttons
function drawio()
(
// ---------- OUTPUT: input / output meters + limiter gain reduction, IN / OUT gain, MIX, limiter, equal loudness ----------
card(IOX, IOY, IOW, IOH);
sechdr(7, "Output", IOX, IOY, cIOTT);
secopen(7) ? (
  tnow = time_precise();
  // audio not running (REAPER stopped and does not process the FX): the meters show silence instead of the last values
  aidle = tnow - lastblk > 0.3;
  aidle ? ( mIL = mIR = mOL = mOR = 0; mGV = 1; ) : ( mIL = pkIL; mIR = pkIR; mOL = pkOL; mOR = pkOR; mGV = lmgv; );
// PEAK HOLD as in a DAW: every channel keeps its highest peak for 3 s, then falls at 20 dB/s (the numbers can be
  // read); a click on the numbers resets them
  hdt = min(0.2, max(0, tnow - hTl)); hTl = tnow;
  v = dbs(mIL); v >= hIL ? ( hIL = v; tIL = tnow; ) : ( tnow - tIL > 3 ? hIL = max(v, hIL - 20 * hdt); );
  v = dbs(mIR); v >= hIR ? ( hIR = v; tIR = tnow; ) : ( tnow - tIR > 3 ? hIR = max(v, hIR - 20 * hdt); );
  v = dbs(mOL); v >= hOL ? ( hOL = v; tOL = tnow; ) : ( tnow - tOL > 3 ? hOL = max(v, hOL - 20 * hdt); );
  v = dbs(mOR); v >= hOR ? ( hOR = v; tOR = tnow; ) : ( tnow - tOR > 3 ? hOR = max(v, hOR - 20 * hdt); );
  hIn = max(hIL, hIR); hOut = max(hOL, hOR);
  limact = clipon && gwarn > 0 && !aidle;
  // icon buttons in the header (Expert only)
// three icon buttons of one size (28 x 16) at the right end of the header - Detail, Equal loudness, Limiter
  chx = IOX + IOW - 4 * 32 - 2; chy = IOY + 4;   // four buttons - Detail, Equal loudness, Limiter, Input Guard
  lmcol = limact && (floor(time_precise() * 8) & 1) == 0 ? cWARN : cIOTT;   // blinks in the warning yellow
  eqlon = slider60 >= 0.5;
  !basic ? (
// DETAIL: High = interaural time difference and head shadow for the direct sound and the reflections of first AND
    // second order; Normal counts the first order only (about 17 % less CPU); Stereo-safe pans by level only, no phase shift
    dtm = floor(vopt[49] + 0.5);
    ibtn(chx, chy, 28, 16, dtm != 1, cIOTT) ? ( vopt[49] = (dtm + 1) % 3; ); ic_detail(chx + 14, chy + 8, dtm == 0 ? 1 : (dtm == 1 ? 0 : 2));
    itip(chx, chy, 28, 16, dtm == 0 ? "Detail: High – ear timing and head shadow for the direct sound and the reflections of first and second order (the most real, but left and right differ in time). Click: Normal" : (dtm == 1 ? "Detail: Normal – ear timing and head shadow for the direct sound and the first reflections only (about 17 % less CPU). Click: Stereo-safe" : "Detail: Stereo-safe – positions by level only: no ear timing, no head shadow, left and right stay in phase (mono compatible). Click: High"),
         dtm == 0 ? "Detail: High – ear timing for direct + 1st and 2nd reflections" : (dtm == 1 ? "Detail: Normal – 1st reflections only, less CPU" : "Detail: Stereo-safe – level panning only, no phase shift"));
    ibtn(chx + 32, chy, 28, 16, eqlon, cIOTT) ? ( slider60 = eqlon ? 0 : 1; sauto1(60); ); ic_eql(chx + 46, chy + 8);
    itip(chx + 32, chy, 28, 16, eqlon ? "Equal loudness on: every room, distance, barrier and source model plays about as loud as the input – for comparing sounds (the level no longer tells the distance). Click: off" : "Equal loudness: every room, distance, barrier and source model plays about as loud as the input – for comparing sounds (the level no longer tells the distance)",
         eqlon ? "Equal loudness: on – everything as loud as the input" : "Equal loudness: off – the level tells the distance");
    ibtn(chx + 64, chy, 28, 16, clipon, lmcol) ? ( slider23 = clipon ? 0 : 1; autom(4194304); ); ic_lim(chx + 78, chy + 8);
    itip(chx + 64, chy, 28, 16, clipon ? "Limiter on: look-ahead peak limiter, every peak stays below -1 dBFS (1.5 ms latency, REAPER compensates it) – blinks while it works. Click: off" : "Limiter off – click: on (every peak stays below -1 dBFS)",
         clipon ? "Limiter: on – peaks stay below -1 dBFS" : "Limiter: off");
// INPUT GUARD: a soft limiter right after the Input gain - protects the reverb from hot inputs (off = the input stays exactly as it is)
    grdon = vopt[68] >= 0.5;
    ibtn(chx + 96, chy, 28, 16, grdon, cIOTT) ? ( vopt[68] = grdon ? 0 : 1; ); ic_grd(chx + 110, chy + 8);
    itip(chx + 96, chy, 28, 16, grdon ? "Input Guard on: a soft limiter right after the Input gain – up to -1 dBFS nothing changes, above that peaks bend smoothly and never pass 0 dBFS. Protects the room from a hot input. Click: off" : "Input Guard off – the input stays exactly as it is. Click: on (a soft limiter after the Input gain, from -1 dBFS)",
         grdon ? "Input Guard: on – soft limit after the input gain" : "Input Guard: off");
  );
  tabm || basic ? (
    // TABS: input meters (L R) left, spectrum in the middle, output meters right - same height; under them the faders
    // Input | Dry / wet | Output; bottom row: correlation and the limiter's gain reduction
// BASIC: the same look, compact - spectrum | output meters, faders Dry / wet | Output
    omw = 64; omy = IOY + (basic ? 44 : 54); omh = IOH - (basic ? 44 + 46 : 54 + 80); omx1 = IOX + 14; omx2 = IOX + IOW - 14 - omw;   // 8 px more under the meters - L / R sat on the fader names
    gj = basic ? 1 : 0;
    loop(basic ? 1 : 2,
      omx = gj == 0 ? omx1 : omx2;
      !basic ? ( col(cMUTED, 1); gfx_setfont(4); txt(gj == 0 ? "Input" : "Output", omx + omw / 2, IOY + 30, 1); );
      gk = 0;
      loop(2,
        omv = gj == 0 ? (gk == 0 ? mIL : mIR) : (gk == 0 ? mOL : mOR); mdb = dbs(omv);
        bx = omx + 10 + gk * 26;
        mbarV(bx + 2, omy, 12, omh, mdb);   // a smooth bar
        gk == 0 ? ( col(cDIM2, 1); i = 0; loop(5, sdb = i == 0 ? -3 : (i == 1 ? -6 : (i == 2 ? -12 : (i == 3 ? -24 : -48))); sy0 = omy + omh * (1 - lin2f(sdb, -60, 0)); R(bx - 2, sy0 - 0.4, 3, 0.8); i += 1; ); );
        col(cMUTED, 1); gfx_setfont(7); txt(gk == 0 ? "L" : "R", bx + 8, omy + omh + 8, 1);
        mdh = gj == 0 ? (gk == 0 ? hIL : hIR) : (gk == 0 ? hOL : hOR);   // the held peak (3 s), not the moving level
        col(mdh > -0.05 ? cIOTT : cTEXT, 1); gfx_setfont(6); mdh < -59.9 ? strcpy(#w1, "-inf") : sprintf(#w1, "%.1f", mdh); txt(#w1, bx + 8, omy - 8, 1);
        click && inbox(bx - 8, omy - 16, 32, 16) ? ( hIL = hIR = hOL = hOR = -150; click = 0; );
        gk += 1;
      );
      mh = gj == 0 ? hIn : hOut; mh > -60 ? ( col(mh > -1 ? cIOTT : cTEXT, 1); R(omx + 8, omy + omh * (1 - lin2f(mh, -60, 0)) - 0.6, 46, 1.2); );   // peak hold
      gj == 1 ? (                                                        // limiter gain reduction: a thin bar from the top (output color)
        grdb = -dbs(mGV); clipon && grdb > 0.05 ? ( col(cIOTT, 1); R(omx + 60, omy, 3, omh * min(1, grdb / 12)); );
      );
      tipbox(omx, omy - 14, omw, omh + 28, gj == 0 ? "Input level, left and right (peak, dBFS) – numbers and line = peak hold (3 s, click the numbers = reset)" : "Output level after the limiter, left and right (peak, dBFS) – numbers and line = peak hold (3 s, click the numbers = reset), the thin blue bar = gain reduction of the limiter (0 … 12 dB)");
      gj += 1;
    );
    // spectrum between the meters, as high as they are
    iosx = basic ? IOX + 14 : omx1 + omw + 14; iosw = omx2 - 14 - iosx; spy = omy; sph = omh;
    gon = !basic && iosw >= sph + 150; gon ? ( iosw -= sph + 14; gondraw(iosx + iosw + 14 + sph / 2, spy + sph / 2, sph / 2 - 4); col(cMUTED, 1); gfx_setfont(4); txt("Goniometer", iosx + iosw + 14 + sph / 2, IOY + 30, 1); );   // goniometer right of the spectrum
    !basic ? ( col(cMUTED, 1); gfx_setfont(4); txt("Spectrum · input gray, output line", iosx + iosw / 2, IOY + 30, 1); );
    // spectrum refresh at most every 35 ms; while the audio is idle the curves sink 3 dB per refresh (floor -120 dB)
    time_precise() - spT > 0.035 ? ( spT = time_precise(); aidle ? ( spk = 0; loop(SPN, spBI[spk] = max(-120, spBI[spk] - 3); spBO[spk] = max(-120, spBO[spk] - 3); spk += 1; ); ) : spcalc(); );
    spdraw(iosx, spy, iosw, sph);
    // faders: Input under the input meters, Output under the output meters, Dry / wet in the middle
    fy = IOY + IOH - (basic ? 30 : 54); fw = basic ? (IOW - 28 - 24) / 2 : (IOW - 28 - 2 * 24) / 3;
    !basic ? (
    sprintf(#fv, slider40 <= -71.9 ? "-inf" : (slider40 > 0.05 ? "+%.1f dB" : "%.1f dB"), slider40);
    kmap(3, 0, 1, 1); nf = hsl(22, IOX + 14, fy, fw, "Input", #fv, ilvl2f(slider40), ilvl2f(0), cIOTT);
    tipbox(IOX + 14, fy - 6, fw, 20, "Input gain: -inf … +48 dB into the plugin – the mark = 0 dB (double-click = type a value, Alt+click = 0 dB)");
    kchg ? setp(40, f2ilvl(nf));
    );
    dwx = basic ? IOX + 14 : IOX + 14 + fw + 24;
    sprintf(#fv, "%.0f %%", slider18);
    kmap(0, 0, 100, 1); nf = hsl(12, dwx, fy, fw, "Dry / wet", #fv, slider18 / 100, 1, cIOTT);
    tipbox(dwx, fy - 6, fw, 20, "Dry / wet of the whole plugin: 0 % = the original only, 100 % = full room simulation (Alt+click = 100 %)");
    kchg ? setp(18, nf * 100);
    sprintf(#fv, slider22 <= -71.9 ? "-inf" : (slider22 > 0.05 ? "+%.1f dB" : "%.1f dB"), slider22);
    kmap(2, 0, 1, 1); nf = hsl(7, IOX + IOW - 14 - fw, fy, fw, "Output", #fv, lvl2f(slider22), lvl2f(0), cIOTT);
    tipbox(IOX + IOW - 14 - fw, fy - 6, fw, 20, "Output gain: -inf … +24 dB out of the plugin – the mark = 0 dB, the standard (double-click = type a value, Alt+click = 0 dB)");
    kchg ? setp(22, f2lvl(nf));
    // bottom row: correlation (middle), gain reduction (right) - not in Basic
    !basic ? (
    cyy = IOY + IOH - 22; cxx = IOX + 14 + fw + 24; cww = fw;
    col(cMUTED, 1); gfx_setfont(4); txt("Correlation · mono compatibility", cxx, cyy - 13, 0);   // over its bar
    crv = crvS;
    corrbar(cxx, cyy, cww, crv); crv <= 1.5 ? sprintf(#w1, "%+.2f", crv) : strcpy(#w1, "–");
    col(crvC, 1); gfx_setfont(2); txt(#w1, cxx + cww, cyy - 13, 2);
    col(cMUTED, 1); gfx_setfont(4); txt("−1", cxx - 4, cyy, 2); txt("+1", cxx + cww + 4, cyy, 0);
    tipbox(IOX + 10, cyy - 7, 2 * fw + 30, 14, "Correlation of the output, left against right: +1 = mono, 0 = wide, below 0 = parts cancel in a mono or TV downmix");
    grdb = -dbs(mGV); sprintf(#w1, clipon ? (grdb > 0.05 ? "Limiter −%.1f dB" : "Limiter idle") : "Limiter off", grdb);
    col(clipon && grdb > 0.05 ? cIOTT : cMUTED, 1); gfx_setfont(4); txt(#w1, IOX + IOW - 14, cyy, 2);
    );
    kcol = cTEXT; kscale(1);
  ) : (
  // meters
  // wide: meters | spectrum | knobs in one row; medium (below 640): meters over the spectrum | knobs - the spectrum keeps its width
  iorow = !basic && IOW >= 400; iostk = iorow && IOW < 640;
  iorow ? ( kscale(min(1.6, kfit(secx(7) / 2 - 2))); osp = 2 * KR + 22; ocx = IOX + IOW - 12 - 1.5 * osp; oy = IOY + 24 + (IOH - 24) / 2 - 4; );
  mtw = iostk ? ocx - 1.5 * osp - 4 - IOX : (iorow ? max(130, floor(IOW * 0.3)) : IOW);
  gonS = 0; iostk ? ( gonS = IOH - 48; mtw - gonS - 14 >= 210 ? ( mtw -= gonS + 14; ) : ( gonS = 0; ); );   // stacked layout: the goniometer takes a square left of the knobs
  mx0 = basic ? IOX + 30 : IOX + 36; mbw = basic ? IOW - 74 : mtw - 86; my0 = basic ? IOY + 56 : (iorow && !iostk ? IOY + 24 + (IOH - 24) / 2 - 8 : IOY + 34);
  gj = basic ? 1 : 0; basic ? ( my0 = IOY + 44 - 13; );   // Basic: the output meter only
  loop(basic ? 1 : 2,
    myy = my0 + gj * 13;
    gj == 0 ? ( ml = mIL; mr = mIR; mh = hIn; ms1 = "In"; ) : ( ml = mOL; mr = mOR; mh = hOut; ms1 = "Out"; );
    col(cMUTED, 1); gfx_setfont(4); txt(ms1, IOX + (basic ? 8 : 12), myy, 0);
    mbarH(mx0, myy - 3, mbw, 2.5, dbs(ml)); mbarH(mx0, myy + 0.7, mbw, 2.5, dbs(mr));   // green / amber / red zones
    mh > -60 ? ( col(mh > -1 ? cIOTT : cTEXT, 1); R(mx0 + mbw * lin2f(mh, -60, 0) - 0.6, myy - 4, 1.2, 8); );
    col(cTEXT, 1); gfx_setfont(2);
    mh < -59.9 ? strcpy(#w1, "-inf") : sprintf(#w1, "%.1f", mh); txt(#w1, IOX + mtw - 8, myy, 2);
    gj += 1;
  );
  grdb = -dbs(mGV);                                  // limiter gain reduction: a thin bar from the right (output color), 0 … 12 dB
  clipon && grdb > 0.05 ? ( col(cIOTT, 1); gw = mbw * min(1, grdb / 12); R(mx0 + mbw - gw, my0 + 18, gw, 1.5); );
  !basic ? (
    cyy = my0 + 31; col(cMUTED, 1); gfx_setfont(4); txt("Corr", IOX + 12, cyy, 0);
    crv = crvS;
    corrbar(mx0, cyy, mbw, crv); crv <= 1.5 ? sprintf(#w1, "%+.2f", crv) : strcpy(#w1, "–");
    col(crvC, 1); gfx_setfont(2); txt(#w1, IOX + mtw - 8, cyy, 2);
    tipbox(IOX + 4, cyy - 6, mtw - 8, 12, "Correlation of the output, left against right: +1 = mono, 0 = wide, below 0 = parts cancel in a mono or TV downmix");
  );
  !basic ? tipbox(IOX + 4, my0 - 7, mtw - 8, 13, "Input level (peak, dBFS) – upper bar = left, lower bar = right, marker = peak hold");
  tipbox(IOX + 4, my0 + 6, mtw - 8, 16, "Output level (peak, dBFS) after the limiter – the thin blue bar from the right = gain reduction of the limiter (0 … 12 dB)");
  // SPECTRUM between the meters and the knobs
  iorow ? (
    iostk ? ( iosx = IOX + 12; iosw = mtw - 12; spy = IOY + 72; sph = max(30, IOH - 80); gonS > 0 ? gondraw(IOX + mtw + 14 + gonS / 2, IOY + 34 + gonS / 2, gonS / 2 - 6); )
          : ( iosx = IOX + mtw + 6; spy = IOY + 30; sph = IOH - 40; iosw = ocx - 1.5 * osp - 4 - iosx;
              iosw >= sph + 150 ? ( iosw -= sph + 10; gondraw(iosx + iosw + 10 + sph / 2, spy + sph / 2, sph / 2 - 5); ); );   // goniometer between spectrum and knobs
  ) : ( sph = basic ? 34 : 44; spy = basic ? IOY + 56 : IOY + 76; iosx = IOX + 12; iosw = IOW - 24; );
  // spectrum refresh at most every 35 ms; while the audio is idle the curves sink 3 dB per refresh (floor -120 dB)
  time_precise() - spT > 0.035 ? ( spT = time_precise(); aidle ? ( spk = 0; loop(SPN, spBI[spk] = max(-120, spBI[spk] - 3); spBO[spk] = max(-120, spBO[spk] - 3); spk += 1; ); ) : spcalc(); );
  spdraw(iosx, spy, iosw, sph);
  // gains + mix
  kcol = cIOTT;
  !iorow ? (
    oy = (basic ? IOY + 120 : IOY + 136) + secx(7) / 2; osp = basic ? floor((IOW - 20) / 3) : 70 + max(0, floor((IOW - 268) / 4)); ocx = IOX + IOW / 2;
    kscale(basic ? kbrow() : min(osp / (2 * KR0 + 22), kfit(secx(7) / 2 - 2)));
  );
  sprintf(#fv, slider40 <= -71.9 ? "-inf" : (slider40 > 0.05 ? "+%.1f" : "%.1f"), slider40);
  basic ? ( osp = floor((IOW - 20) / 4); ocx = IOX + IOW / 2 - osp; ) : (   // Basic: Out + Mix
  kmap(3, 0, 1, 1);
  nf = knob(22, ocx - osp, oy, "In dB", #fv, ilvl2f(slider40), ilvl2f(0));
  knobtip(ocx - osp, oy, "Input gain: -inf … +48 dB into the plugin (double-click = type a value, Alt+click = 0 dB)");
  kchg ? setp(40, f2ilvl(nf));
  );
  sprintf(#fv, slider22 <= -71.9 ? "-inf" : (slider22 > 0.05 ? "+%.1f" : "%.1f"), slider22);
  kmap(2, 0, 1, 1);
  nf = knob(7, ocx, oy, "Out dB", #fv, lvl2f(slider22), lvl2f(0));
  knobtip(ocx, oy, "Output gain: -inf … +24 dB out of the plugin (double-click = type a value, Alt+click = 0 dB)");
  kchg ? setp(22, f2lvl(nf));
  sprintf(#fv, "%.0f", slider18); strcat(#fv, " %");
  kmap(0, 0, 100, 1);
  nf = knob(12, ocx + (basic ? 2 * osp : osp), oy, "Dry / wet", #fv, slider18 / 100, 1);
  knobtip(ocx + (basic ? 2 * osp : osp), oy, "Mix (PlugIn: Dry/Wet): 0 % = original signal only, 100 % = full room simulation (double-click = type a value, Alt+click = 100 %)");
  kchg ? setp(18, nf * 100);
  kcol = cTEXT; kscale(1);
  );   // end of: classic Output
);
);
// BARRIER: what the closed wall / door / window lets through, 63 Hz - 16 kHz, relative to 500 Hz (its level there = Damp):
// the shelf of the material (mass law, gaps) and its coincidence peak, as the sound engine uses them
function barresp(f) local(mx5, mcm, mm, h5r, h5i, hr, hi, a, w, pk, p5)
(
  // shelf |H| = mm + (1 - mm) / (1 + jw), w = f / bfEl (corner); mm is limited so the loss at 500 Hz never exceeds dEl (Damp, dB); then the coincidence peak (bpkG dB at bpkF, Q bpkQ) is added; all relative to 500 Hz
  mm = bmEl; mx5 = sqr(500 / bfEl); mcm = (1 + mx5) / (1 + mm * mm * mx5);
  mcm * 10 ^ (-dEl / 10) > 1 ? ( mcm = 10 ^ (dEl / 10); mm = sqrt(max(0, ((1 + mx5) / mcm - 1) / mx5)); );
  w = f / bfEl; hr = mm + (1 - mm) / (1 + w * w); hi = -(1 - mm) * w / (1 + w * w);
  w = 500 / bfEl; h5r = mm + (1 - mm) / (1 + w * w); h5i = -(1 - mm) * w / (1 + w * w);
  a = 10 * log10(max(0.0000001, (hr * hr + hi * hi) / (h5r * h5r + h5i * h5i)));
  bpkG > 0.05 ? (
    pk = 10 ^ (bpkG / 40); w = f / bpkF; p5 = 500 / bpkF;
    a += 10 * log10(((1 - w * w) ^ 2 + sqr(w * pk / bpkQ)) / ((1 - w * w) ^ 2 + sqr(w / (pk * bpkQ))));
    a -= 10 * log10(((1 - p5 * p5) ^ 2 + sqr(p5 * pk / bpkQ)) / ((1 - p5 * p5) ^ 2 + sqr(p5 / (pk * bpkQ))));
  );
  a;
);
// Draws the barrier transmission graph at (x,y,w,h): response 63 Hz-16 kHz via barresp, relative to 500 Hz, with grid and tooltip
function bargraph(x, y, w, h) local(k, f, gx, gy, py, px, v)
(
  col(cBG, 1); RR(x, y, w, h, 4);
  col(cHAIR, 1); k = 0; loop(3, f = k == 0 ? 125 : (k == 1 ? 1000 : 8000); gx = x + log(f / 63) / log(16000 / 63) * w; LN(gx, y + 2, gx, y + h - 2); k += 1; );
  gy = y + 3 + 15 / 40 * (h - 6); LN(x + 2, gy, x + w - 2, gy);              // 0 dB = the level at 500 Hz (Damp)
  btg > 0 ? (
    col(cBARTT, 1); k = 0;
    // 41 log-spaced points, 63 Hz - 16 kHz; vertical range +15 … -25 dB (0 dB line at 15/40 of the height)
    loop(41,
      f = 63 * (16000 / 63) ^ (k / 40); v = min(15, max(-25, barresp(f)));
      px = x + 3 + k / 40 * (w - 6); py = y + 3 + (15 - v) / 40 * (h - 6);
      k > 0 ? LNW(gx, gy, px, py, 1.4); gx = px; gy = py; k += 1;
    );
  );
  h >= 24 ? ( col(cDIM2, 1); gfx_setfont(7); txt("125", x + log(125 / 63) / log(16000 / 63) * w, y + h - 5, 1); txt("1k", x + log(1000 / 63) / log(16000 / 63) * w, y + h - 5, 1); txt("8k", x + log(8000 / 63) / log(16000 / 63) * w, y + h - 5, 1); );
  btg > 0 ? tipbox(x, y, w, h, "What the closed part lets through, 63 Hz – 16 kHz, relative to 500 Hz (the level there = Damp): more bass (mass law), fewer highs – glass and drywall pass more around their coincidence frequency, doors without seals keep their highs through the gaps");
);
// BARRIER info: what reaches the listener (direct sound, room spill / next room, path)
function barinfo(x, y, xr)
(
  gfx_setfont(4); col(btg > 0 ? cMUTED : cDIM2, 1);
  txt("Direct sound", x, y, 0); txt(bil3 ? "Next room" : "Room leak", x, y + 14, 0);
  gfx_setfont(2); col(btg > 0 ? cBARTT : cDIM2, 1); txt(#bi1, xr, y, 2);
  col(btg > 0 ? cTEXT : cDIM2, 1); txt(#bi3, xr, y + 14, 2);
  gfx_setfont(7); col(btg > 0 ? cTEXT : cDIM2, 1); txt(#bi2, x, y + 29, 0);
  btg > 0 ? tipbox(x, y - 6, xr - x, 12, isfree ? "Free field: a free-standing wall (room width × height) – sound bends around its top and side edges" : (btg == 1 ? "Wall over the full width and height" : (btg == 2 ? "Window 1.2 × 1.2 m, sill 0.9 m – the wall around it is brick, 45 dB" : "Door 0.9 × 2.1 m – the wall around it is brick, 45 dB")));
  btg > 0 && !isfree ? tipbox(x, y + 8, xr - x, 12, bil3 ? "Next room: how much quieter the reverb of the room behind the barrier comes back – depends on Damp, the opening and the size of both rooms" : "Room spill: how much quieter the room sound of the other side arrives – depends on Damp, the opening, the wall area, the room sizes and on how close source and listener are to the barrier");
);
// SOURCE EQ curve (edit = 1: points H, 1-3, L can be dragged; 0 = display only, Basic view)
function eqview(edit)
(
!fzrespok ? ( fzcache(); fzrespok = 1; );
sqpx = sqx + 6; sqpy = sqy + 4; sqpw = sqw - 12; sqph = sqh - 16;
col(cSURF, 1); RR(sqx, sqy, sqw, sqh, 3); col(cHAIR, 1); RRO(sqx, sqy, sqw, sqh, 3);
col(cHAIR, 1);
gi = 0; loop(3, fq = gi == 0 ? 100 : (gi == 1 ? 1000 : 10000); gx = sqfx(fq); LN(gx, sqy + 3, gx, sqpy + sqph); gi += 1; );
col(cBORD, 1); LN(sqpx, sqgy(0), sqpx + sqpw, sqgy(0));
col(cMUTED, 1); gfx_setfont(7);
txt("100", sqfx(100), sqy + sqh - 6, 1); txt("1k", sqfx(1000), sqy + sqh - 6, 1); txt("10k", sqfx(10000), sqy + sqh - 6, 1);
txt("0 dB", sqpx + sqpw, sqgy(0) - 5, 2);
g_fzfill(fzBP, fzsel);                                 // the unedited model (drag = offset from it)
g_fzdesign(fzt, fzTP, fzsel, 1);                       // the edited model, as it is heard
eqk = fzsel * 1000 + g_fzkeyf() + srate;
eqk != lasteqk ? ( gi = 0; loop(400, fzcur[gi] = fzmagdb(20 * 1000 ^ (gi / 399)); gi += 1; ); lasteqk = eqk; );
// one value per screen pixel (linear between the 400 points)
sqn = min(4096, max(2, floor(sqpw * gs))); gi = 0;
loop(sqn, sqt = gi / (sqn - 1) * 399; sqi = min(398, floor(sqt)); sqf = sqt - sqi; cvY[gi] = goy + sqgy(fzcur[sqi] * (1 - sqf) + fzcur[sqi + 1] * sqf) * gs; gi += 1; );
cvfill(floor(gox + sqpx * gs + 0.5), sqn, floor(goy + (sqpy + sqph) * gs + 0.5), cSRTT, 0.24, floor(goy + sqpy * gs));
cvline(floor(gox + sqpx * gs + 0.5), sqn, cSRTT, 1, 1.6);
// the call line on top: device x network band, dashed (Butterworth HP 4th / LP 8th order, as in the audio path)
fzsel > 0 && slider114 >= 0.5 ? (
  lnfh = slider114 < 1.5 ? 300 : (slider114 < 2.5 ? 100 : 50); lnfl = slider114 < 1.5 ? 3400 : (slider114 < 2.5 ? 7000 : 14000);
  gi = 0;
  loop(399,
    fq = 20 * 1000 ^ (gi / 399); fq2 = 20 * 1000 ^ ((gi + 1) / 399);
    y1 = sqgy(fzcur[gi] - 10 * log10(1 + (fq / lnfl) ^ 16) - 10 * log10(1 + (lnfh / fq) ^ 8));
    y2 = sqgy(fzcur[gi + 1] - 10 * log10(1 + (fq2 / lnfl) ^ 16) - 10 * log10(1 + (lnfh / fq2) ^ 8));
    (gi % 8) < 4 ? ( col(cTEXT, 0.75); LNW(sqpx + gi / 399 * sqpw, min(sqpy + sqph, y1), sqpx + (gi + 1) / 399 * sqpw, min(sqpy + sqph, y2), 1.1); );
    gi += 1;
  );
);
eqhov = -1;
edit ? (
  // points: 0 = H, 1-3 = bells, 4 = L. Drag = frequency (+ gain for bells), wheel = Q, double-click / Alt+click = back to the model
  // sliders: 47 = H cutoff offset (octaves), 48 = L, 49 + 3 x (n-1) = bell n (offset, gain dB, Q), 58 / 59 = H / L slope; fzTP[] = [0] H freq, [1] H slope, [2] L freq, [3] L slope, [4] bell count, then freq / gain / Q per bell from [5]; point hit radius 9 px (81 = 9^2)
  eqbest = 81;
  gk = 0;
  loop(5,
    eqf = gk == 0 ? fzTP[0] : (gk == 4 ? fzTP[2] : fzTP[5 + 3 * (gk - 1)]);
    eqnx = sqfx(eqf); eqny = sqgy(fzmagdb(eqf));
    eqd2 = sqr(mx - eqnx) + sqr(my - eqny);
    eqd2 < eqbest ? ( eqbest = eqd2; eqhov = gk; );
    gk += 1;
  );
  eqdrag ? eqhov = eqdrag - 1;
  click && eqhov >= 0 ? (
    tnow2 = time_precise(); eqs = eqhov == 0 ? 47 : (eqhov == 4 ? 48 : 49 + 3 * (eqhov - 1));
    ((lasteqn == eqhov && tnow2 - lasteqt < 0.5) || (mouse_cap & 16)) ? (
      snap_take(); sset(eqs, 0); eqhov > 0 && eqhov < 4 ? ( sset(eqs + 1, 0); sset(eqs + 2, 0); ) : ( sset(eqhov == 0 ? 58 : 59, 0); ); snap_notify();
      eqdrag = 0; lasteqn = -1;
    ) : (
      eqdrag = eqhov + 1; eqmx0 = mx; eqmy0 = my; eqv0 = slider(eqs); eqg0 = slider(eqs + 1); lasteqn = eqhov;
    );
    lasteqt = tnow2;
  );
  eqdrag ? (
    gk = eqdrag - 1; eqs = gk == 0 ? 47 : (gk == 4 ? 48 : 49 + 3 * (gk - 1));
    eqb = gk == 0 ? fzBP[0] : (gk == 4 ? fzBP[2] : fzBP[5 + 3 * (gk - 1)]);                 // the model's own frequency: keep the point inside 20 Hz … 20 kHz
    eqnv = eqv0 + (mx - eqmx0) / sqpw * (log(1000) / log(2));
    eqnv = min(log(20000 / eqb) / log(2), max(log(20 / eqb) / log(2), eqnv));
    setp(eqs, min(10, max(-10, eqnv)));
    gk > 0 && gk < 4 ? setp(eqs + 1, min(24, max(-24, eqg0 - (my - eqmy0) / sqph * 45)));
  );
  mouse_wheel != 0 && !(mouse_cap & 1) && eqhov > 0 && eqhov < 4 ? (
    wheelused = 1;
    eqs = 49 + 3 * (eqhov - 1);
    setp(eqs + 2, min(3, max(-3, slider(eqs + 2) + mouse_wheel / 120 * 0.25)));
  );
// the wheel on H / L = the slope, one notch = 6 dB/oct (up = steeper, 6 … 48; from "Model" it starts at the model's)
  mouse_wheel != 0 && !(mouse_cap & 1) && (eqhov == 0 || eqhov == 4) ? (
    wheelused = 1;
    eqcur = eqhov == 0 ? fzTP[1] : fzTP[3];
    setp(eqhov == 0 ? 58 : 59, min(8, max(1, eqcur + (mouse_wheel > 0 ? 1 : -1))));
    mouse_wheel = 0;
  );
  gk = 0;
  loop(5,
    eqf = gk == 0 ? fzTP[0] : (gk == 4 ? fzTP[2] : fzTP[5 + 3 * (gk - 1)]);
    eqnx = sqfx(eqf); eqny = sqgy(fzmagdb(eqf));
    eqact = gk == 0 ? fzTP[1] > 0 : (gk == 4 ? fzTP[3] > 0 : (gk - 1 < fzTP[4] && abs(fzTP[6 + 3 * (gk - 1)]) > 0.05));   // does this point shape the sound right now?
    gk == eqhov ? ( col(cSRTT, 1); CI(eqnx, eqny, 5, 1); col(cSURF, 1); )
                : ( col(cSURF, 1); CI(eqnx, eqny, 5, 1); col(eqact ? cSRTT : cDIM2, 1); CI(eqnx, eqny, 5, 0); );
    gfx_setfont(8, "Arial", floor(6.5 * gs + 0.5), 'b');
    txtc(gk == 0 ? "H" : (gk == 4 ? "L" : (gk == 1 ? "1" : (gk == 2 ? "2" : "3"))), eqnx, eqny);
    gk += 1;
  );
  eqhov >= 0 ? (
    eqhov == 0 ? ( fzTP[1] > 0 ? sprintf(#w1, "High-pass %.0f Hz · %d dB/oct  (wheel = slope)", fzTP[0], fzTP[1] * 6) : strcpy(#w1, "High-pass off  (wheel = on, 6 dB/oct)"); ) : (
    eqhov == 4 ? ( fzTP[3] > 0 ? sprintf(#w1, "Low-pass %.0f Hz · %d dB/oct  (wheel = slope)", fzTP[2], fzTP[3] * 6) : strcpy(#w1, "Low-pass off  (wheel = on, 6 dB/oct)"); ) : (
      eqb = 5 + 3 * (eqhov - 1);
      sprintf(#w1, fzTP[eqb + 1] >= 0 ? "Band %d:  %.0f Hz  +%.1f dB  Q %.1f" : "Band %d:  %.0f Hz  %.1f dB  Q %.1f", eqhov, fzTP[eqb], fzTP[eqb + 1], fzTP[eqb + 2]);
    ));
    col(cSRTT, 1); gfx_setfont(7); txt(#w1, sqx + 7, sqy + 8, 0);
  ) : (
    tipbox(sqx, sqy, sqw, sqh, "Source EQ: drag H / L = cut-off, wheel on H / L = slope, drag 1–3 = frequency and gain, wheel on 1–3 = Q, double-click = back to the model");
  );
) : (
  tipbox(sqx, sqy, sqw, sqh, "Frequency response of the source model – edit its points in Expert");
);
);
// dimension label in the room view: plate, value; outlined while it can be grabbed (drag = resize the room)
function dimlab(s, x, yc, al, hov) local(w, x0)
(
  gfx_setfont(1); w = tws(s) + 8; x0 = al == 1 ? x - w / 2 : (al == 2 ? x - w : x);
  col(cSURF, 1); RR(x0, yc - 6.5, w, 13, 3);
  hov ? ( col(cRMTT, 1); RRO(x0, yc - 6.5, w, 13, 3); );
  col(cTEXT, 1); txt(s, x0 + 4, yc, 0);
);
// ================= GUI: Basic / Expert =================
mtEg = slider100 < 0.5 && mtE > 0.001 && time_precise() - lastblk > 0.5 ? 0 : mtE;   // Motion was stopped but no audio block comes (transport stopped / bypassed): the fade-out can never finish - the GUI treats it as ended (mtEg), so source and listener are free again
time_precise() - lastblk > 0.5 ? geo_if_changed();   // also during playback when no audio block comes (plugin bypassed / offline): the room view never shows an old geometry
// (geo_if_changed above runs only while no audio block arrives, so it never races the audio thread)

// ---- layout: width 760 design units; the height follows the open sections. Never smaller than 1:1 at the display's
// pixel density (4K / Retina: gfx_ext_retina) - when the window is too low for that, the page scrolls.
TOPY = 66; BW0 = vopt[11] >= 0.5 ? 760 : (vopt[43] >= 0.5 ? 880 : 1080);   // Expert: two columns side by side - every module visible at once                    // TOPY: top of the sections (top bar 0 … 32, signal-flow strip 36 … 60; the view modes below move it)
szclr ? ( szoom = 0; zoomv = 0; szclr = 0; );   // a mode / view switch of the last frame
basic = vopt[11] >= 0.5; learn = !basic && vopt[43] >= 0.5;   // LEARN: Expert sound, its own page (view + explanations)                             // Basic view: the main controls only (display only - every setting stays as it is)
basic ? ( selw = 0; tpon = 0; tmbOn = 0; ) : ( xpon = 0; );
// TABS (Expert): the room view over the whole width, under it a row of tabs and ONE module, as large as the window
// allows - no scrolling. vopt[45] = 1: the All tab = every module at once in two columns .
tabm = !basic && !learn && vopt[45] < 0.5;
tabsel = floor(vopt[46] + 0.5);
!(tabsel == 1 || tabsel == 15 || tabsel == 3 || tabsel == 4 || tabsel == 5 || tabsel == 6 || tabsel == 7 || tabsel == 10 || tabsel == 14) ? ( tabsel = 1; vopt[46] = 1; );
ffTab = tabm && tabsel == 15;   // the Free field tab shows the Room card's place
tabm ? ( vopt[12] & (2 ^ (ffTab ? 1 : tabsel)) ? ( vopt[12] -= 2 ^ (ffTab ? 1 : tabsel); ); TOPY = 48; );   // 10 more for the Tabs | All | Fit switch under the view modes
!basic && !learn && !tabm ? TOPY = 76;   // All - 10 more for the Tabs | All | Fit switch under the view modes
basic ? ( TOPY = 98; );   // lower tiles - Basic: the row of room tiles under the top bar (TOPY 98)
vlock = vopt[15] >= 0.5;                                // layout locked: folding and column widths stay (also on Reset)
KR = vopt[11] < 0.5 && vopt[43] < 0.5 && vopt[45] < 0.5 ? 18 : 21; KLW = KR * 0.2; KVF = 3;   // tabs: knobs a little smaller (the text larger)
 KR0 = KR; KLW0 = KLW; KVF0 = KVF; KSC = 1; KNS = 1; KNF = 4;   // one knob size per view mode (Basic: larger)
// each section folds on its own (Expert and Basic); the sections below move up. Folding is animated.
// state (bit 2^id set = folded): Expert vopt[12] (ids 1-11, 14 = Motion), Basic vopt[13] (its own: 1 Room, 3 Source, 5 Barrier, 7 Output) - secop[id] / secop[12 + id]
tanim = time_precise(); adt = min(0.1, max(0, tanim - lastanim)); lastanim = tanim; animing = 0;
k = 1;
loop(23,
  tg = k == 14 ? !(vopt[12] & 16384) : (k <= 11 ? !(vopt[12] & (2 ^ k)) : !(vopt[13] & (2 ^ (k - 12))));   // 14 = Motion (Expert, bit 16384)     // 9 = the 3D view, 10 = Delay, 11 = Decay by band (Expert)
  abs(tg - secop[k]) < 0.02 ? ( secop[k] = tg; ) : ( secop[k] += (tg - secop[k]) * min(1, adt * 14); animing = 1; );
  k += 1;
);
HDR = 24; VFH = 96; bfill = 0; LP1 = 206;   // LEARN FLICKER: the panels' height comes from their text, the text wraps by the width, the width follows the scale, the scale follows the height - in some
  // windows (e.g. 2000 x 1300) this flipped every frame. Now the larger of this and the last frame's need counts: a flip between two values settles on the larger one
  lp2use = max(lp2need, lp2old); lp2old = lp2need; lp3use = max(lp3need, lp3old); lp3old = lp3need;
  LP2 = lp2use > 60 ? min(420, lp2use) : 300; LP3 = lp3use > 60 ? min(320, lp3use) : 196;   // Learn: What you hear, Your turn, Words (the last two: as tall as their text)
k = 21; loop(12, vopt[k] = min(400, max(0, floor(vopt[k] + 0.5))); k += 1; ); vopt[37] = min(400, max(0, floor(vopt[37] + 0.5)));   // module heights (mouse): extra 0 … 400
k = 33; loop(2, vopt[k] = vopt[k] < 1 ? 0 : min(1400, max(140, floor(vopt[k] + 0.5))); k += 1; );   // room view: 0 = automatic
shD = sech(10, 196 + secx(10)); shR = sech(1, 182 + secx(1)); shV = 0; shS = sech(3, (basic ? 136 : 160) + secx(3)); shE = 0; shK = 0;   // Expert: + 24 for the call strip (and the filter next to it) // left: FILTER | SOURCE, DELAY, ROOM + REVERB (+ OUTPUT while the room view is shown)
shW = sech(4, 118 + secx(4)); shB = sech(5, 170 + secx(5)); shM = sech(14, (mtPW0 < 600 ? 286 : (mtPW0 < 830 ? 260 : 224)) + secx(14)); shF = sech(6, (basic ? 136 : 160) + secx(6)); shO = sech(7, (basic ? 180 : 112) + secx(7));   // right: room view, WALLS, BARRIER, MOTION (+ OUTPUT when the room view is hidden) (narrow column: Direction / Rate on their own row) // shF / shO had slipped into a comment in - Filter and Output cards in All had no height
learn ? (
  vop = 1; CH = TOPY + LP1 + LP2 + LP3 + 12 + 18;
) : basic ? (
  hbR = sech(1, 206 + secx(1)); hbS = sech(3, 206 + secx(3)); hbB = sech(5, 180 + secx(5)); hbO = sech(7, 180 + secx(7));
  hb1 = max(hbS, hbR); hb2 = max(hbB, hbO); hbM = max(hbS + hbB, hbR + hbO) + 6;   // two columns of two cards (Source over Barrier, Room over Output): a folded card lets the one under it move up
  vop = secop[21]; CH = TOPY + max(hbM, HDR + vop * ((vopt[34] > 0 ? vopt[34] : hbM) - HDR)) + 18;
) : (
  vhid = (vopt[12] & 512) > 0; VFH = 96;                                   // room view hidden: a bar with a mini plan, Output goes right
// FIT ALL FLICKER: the Motion card is taller in a narrow column (mtPW0 < 600), the column width comes from the scale,
  // the scale from the page height - at some window sizes this flipped every frame (gs 1.035 <-> 1.000, the whole page
  // jumping = the blinking). Fit all now sizes the page for the taller Motion card: one scale, no feedback
  shMc = !basic && vopt[42] >= 0.5 ? sech(14, 286 + secx(14)) : shM;   // the taller Motion card (286, tempo tools and knobs inside) - 236 cut it off at the bottom
  rsum = shW + shB + shMc + 18 + (vhid ? shO + 6 : 0); lsec = max(shF, shS) + shD + shR + 12 + (vhid ? 0 : shO + 6);
  vop = secop[9]; CH = TOPY + max(lsec, rsum + VFH + vop * ((vopt[33] > 0 ? vopt[33] : max(!basic && vopt[42] >= 0.5 ? 200 : 260, lsec - rsum)) - VFH)) + 18;   // Fit all: the room view may be lower (200)
);
gsmin = max(1, gfx_ext_retina);
// EXPERT, standard: exactly the scale Basic has in this window - text and knobs as large as in Basic, the page scrolls
// (mouse wheel). Two columns when the window is wide enough for them at that scale, else one column.
// "Fit all" (strip, vopt[42]): the whole page in the window, smaller.
ebig = !basic && !learn && vopt[42] < 0.5;
ebig ? (
  gs = max(0.25, min(gfx_w / 998, min(gfx_w / 760, max(gsmin, gfx_h / max(450 + 2 * vopt[32], TOPY + vopt[34] + 18)))));   // never narrower than the two columns
  vopt[14] > 0 ? ( gs = max(0.25, min(gs, gfx_w / (536 / (1 - min(0.6, vopt[14])) + 22))); );   // right column dragged wider than the window allows: the page gets smaller (the left column keeps its 536)
  rzid ? ( gs = rzgs; );                   // while a height is dragged the scale stays (the edge follows the mouse)
// (the one-column layout of earlier versions is gone - the room view, walls and barrier always stay at the right)
  BW0 = max(760, floor(gfx_w / gs));
) : (
  // LEARN: larger than "fit the page" - the explanations are read, not scanned; the right column scrolls when the window is low
  gs = learn ? max(0.25, min(gfx_w / BW0, max(gsmin, gfx_h / max(640, CH)))) : max(0.25, min(gfx_w / BW0, max(basic && vopt[36] >= 0.5 ? 0.25 : (!basic && !learn && vopt[42] >= 0.5 ? 0.6 * gsmin : gsmin), gfx_h / CH)));   // Basic Fit all: no lower limit - the page fits   // Learn: large text, but the whole page in the window (no scrolling)
  !learn && !basic && vopt[14] > 0 ? ( gs = max(0.25, min(gs, gfx_w / (536 / (1 - min(0.6, vopt[14])) + 22))); );
  rzid ? ( gs = rzgs; );
);
tabm ? ( gs = max(0.25, min(gfx_w / 860, gfx_h / 560)); BW0 = max(760, floor(gfx_w / gs)); );   // tabs: one scale for top bar, room view and tabs
XW = max(0, floor(gfx_w / gs - BW0)); WT = BW0 + XW; HT = gfx_h / gs;  // HT = visible height
LH = zoomv || tabm ? HT : max(HT, CH);                                      // height of the page
scmax = max(0, LH - HT); scy = min(max(scy, 0), scmax);
gox = 0; goy = -floor(scy * gs + 0.5);
avail = LH - TOPY - 18;
// BASIC: a window taller than the page - the two rows of cards grow (knobs too) to the height of the room view
basic && !zoomv && !szoom && vopt[34] <= 0 ? (
  bfill = max(0, floor((avail - hbM) / 2));
  bfill > 0 ? ( hbR = sech(1, 206 + secx(1)); hbS = sech(3, 206 + secx(3)); hbB = sech(5, 180 + secx(5)); hbO = sech(7, 180 + secx(7)); hb1 = max(hbS, hbR); hb2 = max(hbB, hbO); hbM = max(hbS + hbB, hbR + hbO) + 6; );
);
learn ? (
  // LEARN: the room view at the left, the explanations at the right
  L0 = 8; colT = WT - 16; LRW = max(380, floor(colT * 0.38)); VX = 8; VY = TOPY; VW = colT - LRW - 6; VH = min(avail, max(360, floor(HT) - TOPY - 18));   // the room view stays in the window
  LPX = VX + VW + 6; LQ1 = TOPY; LQ2 = LQ1 + LP1 + 6; LQ3 = LQ2 + LP2 + 6; LQH = max(LP3, TOPY + avail - LQ3);
  LW = VW; RWc = LRW; R0 = LPX; IOX = NOPOS; IOY = 0; IOW = 0; IOH = 0;
) : basic ? (
  // four cards 2 x 2 at the left in signal order (Source | Room, Barrier | Output), the room view at the right takes the rest;
  // card area: standard 62 % of the width (at least 372), dragged = a share of the width (corner of Room or Output)
  L0 = 8; bTot = WT - 16;
  bcw = vopt[41] > 0 ? floor(min(bTot - 280, max(372, vopt[41] * bTot)) + 0.5) : max(372, floor(bTot * 0.62));   // the cards get more, the room view less (it was too big)
  bwS = floor((bcw - 6) / 2); bwB = bwS; bwR = bcw - 6 - bwS; bwO = bwR;
  sxS = 8; sxB = 8; sxR = sxS + bwS + 6; sxO = sxR;
  yS1 = TOPY; yS2 = yS1 + hbS + 6; yO2 = yS1 + hbR + 6;   // Barrier under Source, Output under Room
  VX = 8 + bcw + 6; VY = TOPY; VW = WT - 8 - VX; VH = VFH + vop * ((vopt[34] > 0 ? vopt[34] : max(hbM, avail)) - VFH);   // folded = the bar with the mini plan (as in Expert)
  IOX = sxO; IOY = yO2; IOW = bwO; IOH = hbO;
) : (
  // LAYOUT A: left = the chain the signal passes, top to bottom (Filter | Source, Delay, Room + Reverb, Output);
  // right = the room itself (room view with Plan / 3D / Timeline / RT60, Walls, Barrier). The columns share 1058 + XW:
  // the standard is right 42 %; once dragged it is a share of the total, never narrower than 440 (right) / 536 (left)
  L0 = 8; colT = BW0 - 22 + XW;
  RWc = vopt[14] > 0 ? floor(min(colT - 536, max(440, vopt[14] * colT)) + 0.5) : floor(min(colT - 536, max(440, colT * 0.42)));
  LW = colT - RWc; R0 = L0 + LW + 6;
  flW = vopt[39] > 0 ? floor(min(LW - 346, max(190, vopt[39] * LW)) + 0.5) : floor(min(LW - 346, max(190, LW * 0.32)));   // Filter | Source share the first row
  flX = L0; srX = L0 + flW + 6; srW = LW - flW - 6;
  syF = TOPY; syS = TOPY; syD = syS + max(shF, shS) + 6; syR = syD + shD + 6; syV = syR + 90 + secx(1) / 2; syO = syR + shR + 6; syE = 0; syK = 0;
  VX = R0; VY = TOPY; VW = RWc; VH = VFH + vop * ((vopt[33] > 0 ? vopt[33] : avail - rsum) - VFH);
  syW = VY + VH + 6; syB = syW + shW + 6; syM = syB + shB + 6; mtPX0 = R0; mtPW0 = RWc;
  vhid ? ( syO = syM + shM + 6; IOX = R0; IOY = syO; IOW = RWc; IOH = shO; ) : ( IOX = L0; IOY = syO; IOW = LW; IOH = shO; );
);
tabm ? (
  // TABS: the room view over the whole width - its height dragged (vopt[47]) or 46 % of the window; folded = the mini plan.
  // Under it the tab row (TBY), then the module (TAY … bottom) - the module keeps at least 150 units
  vhid = (vopt[12] & 512) > 0; TBH = 32;
  vopt[48] < 0.5 ? zmz = 0;   // the Z zoom ends when Fit is switched off
  vgone = zmz > 0;   // Z zoom - no Stage View at all, the tab row sits right under the top bar
// the room view has ONE height for every tab (switching tabs never changes it) - measured on the tallest module
  tzhA = max(max(max(182 + secx(1), 260), 200 + secx(14)), max(max(150 + secx(4), 196 + secx(5)), max(max(210 + secx(6), 210 + secx(7)), 196)));
  tabmax = HT - TOPY - 12 - TBH - 16 - max(150, 0.6 * tzhA);   // the room view may grow until the module is at 60 % of its size // the module never smaller than the page scale (the room view gives way)
  VX = 8; VY = TOPY; VW = WT - 16;
  tfit = vopt[48] >= 0.5;
  tcs = min(1.8, max(1, vopt[35])); tcus = !tfit && vopt[35] >= 0.99;   // the module scaled at its corner (x the page scale; 0 = automatic)
  tfit ? ( tfs = min(1.8, (WT - 16) / 600); tfh = HT - TOPY - 12 - TBH - 16 - tzhA * tfs;   // page units left for the room view
           tfh < 150 ? ( tfh = 150; ); )
       : ( tcus ? ( tfh = max(150, HT - TOPY - 12 - TBH - 16 - tzhA * tcs); ) : ( tfh = 0; ); );
  VH = vhid ? VFH : (tfit || tcus ? min(tabmax, tfh) : max(VFH + 30, min(tabmax, vopt[47] > 0 ? vopt[47] : floor((HT - TOPY - 12) * 0.46) - 8)));   // -8 = the larger gap above the tabs comes out of the view, the tabs stay where they were
  VH = max(HDR, min(VH, max(HDR, tabmax)));
  TBY = VY + VH + 16; TAY = TBY + TBH;
  vgone ? ( VH = 0; TBY = VY + 2; TAY = TBY + TBH; );
);
!tabm ? vgone = 0;
zoomv ? ( VX = 8; VY = TOPY; VW = WT - 16; VH = HT - TOPY - 18; );         // zoom: room view fills the window
// SECTION ZOOM: any section alone, drawn larger (the same controls, a bigger scale); top bar and signal-flow strip unchanged
zoomv ? ( szoom = 0; );
// VALUE ENTRY open: digits . , - k, Backspace, Enter = set, Esc = cancel; a click outside the box cancels
vedit ? (
  kqn = 0;
  while (
    gch = gfx_getchar();
    gch > 0 ? (
      gch == 13 || gch == 10 ? ( vedVal = vedparse(#ved); vedok ? ( vedPend = 1; vedId = vedit; ); vedit = 0; ) : (
      gch == 27 ? ( vedit = 0; ) : (
      gch == 8 || gch == 127 ? ( vedNew ? strcpy(#ved, "") : ( strlen(#ved) > 0 ? str_setlen(#ved, strlen(#ved) - 1); ); vedNew = 0; ) : (
      (gch >= '0' && gch <= '9') || gch == '.' || gch == ',' || gch == '-' || gch == 'k' || gch == 'K' ? (
        vedNew ? ( strcpy(#ved, ""); vedNew = 0; ); strlen(#ved) < 12 ? ( sprintf(#vch, "%c", gch); strcat(#ved, #vch); );
      ); ); ); );
    );
    kqn += 1; gch > 0 && kqn < 64 && vedit;
  );
  (mouse_cap & 1) && !(lastcap & 1) && !(mouse_x >= vedPX0 && mouse_x < vedPX1 && mouse_y >= vedPY0 && mouse_y < vedPY1) ? ( vedit = 0; );
);
// MY ROOMS: keys go to the search (room menu open) or to the name (save dialog)
rsave && !snedit && !vedit ? (
  kqn = 0; rsact = 0;
  while (
    gch = gfx_getchar();
    gch > 0 ? (
      gch == 13 ? ( rsact = 1; ) : ( gch == 27 ? ( rsact = 2; ) : ( gch == 8 ? ( rsnlen = bufback(rsnb, rsnlen); ) : (
      // gfx_getchar returns 117 << 24 ('u') + code point for non-ASCII characters
      gch >= 117 * 16777216 && gch < 118 * 16777216 ? ( rsnlen = bufadd(rsnb, rsnlen, 24, gch - 117 * 16777216); ) : (
      gch >= 32 && gch < 256 && gch != 127 ? ( rsnlen = bufadd(rsnb, rsnlen, 24, gch); ); ))));
    );
    kqn += 1; gch > 0 && kqn < 64 && !rsact;
  );
) : (
rmenu && !snedit && !vedit ? (
  kqn = 0;
  while (
    gch = gfx_getchar();
    gch > 0 ? (
      gch == 27 ? ( rqlen > 0 ? ( rqlen = 0; memset(rqb, 0, 30); ) : ( rmenu = 0; ); ) : (
      gch == 13 ? ( urfirst >= 0 ? ( urload(urfirst); snlast = 0; rmenu = 0; ) : ( rmfirst >= 0 ? ( isfree ? modeset(0); loadroom(rmfirst); snlast = 0; rmenu = 0; ) : (   // Enter: the first one found (my room, room, scene, free field)
        sh = -1; k = NSC - 1; loop(NSC, scnX[k] != NOPOS ? sh = k; k -= 1; );
        sh >= 0 ? ( isfree ? modeset(0); scnload(sh); snlast = 0; rmenu = 0; ) : ( ffpmH >= 0 ? ( !isfree ? modeset(1); ffpapply(ffpmH); snlast = 0; rmenu = 0; ); ); ); ); ) : (
      gch == 8 ? ( rqlen = bufback(rqb, rqlen); ) : (
      gch >= 117 * 16777216 && gch < 118 * 16777216 ? ( rqlen = bufadd(rqb, rqlen, 24, gch - 117 * 16777216); ) : (
      gch >= 32 && gch < 256 && gch != 127 ? ( rqlen = bufadd(rqb, rqlen, 24, gch); ); ))));
    );
    kqn += 1; gch > 0 && kqn < 64;
  );
); );
!snedit && !vedit && !rsave && !rmenu ? ( kqn = 0; while ( gch = gfx_getchar(); gch == 27 && mpmenu ? ( mpmenu = 0; ); gch == 27 && ffpmenu ? ( ffpmenu = 0; ); gch == 27 && rpmenu ? ( rpmenu = 0; ); gch == 27 && tpon ? ( tpon = 0; ); gch == 27 && tmbOn ? ( tmbOn = 0; ); gch == 27 && zmz && tabm ? ( vopt[48] = zfit0; (vopt[12] & 512) && !zhid0 ? vopt[12] -= 512; zmz = 0; ); gch == 27 && szoom ? ( szoom = 0; ); gch == 27 && zoomv ? ( zoomv = 0; ); (gch == 'v' || gch == 'V') && !basic && !vlock && !zoomv ? ( vopt[12] = (vopt[12] & 512) ? vopt[12] - 512 : vopt[12] + 512; ); gch == 27 ? ( tpon = 0; xpon = 0; dmenu = 0; helpon = 0; snctx = 0; hlpon = 0; soloM = 0; kovl = 0; ); gch == '?' ? ( kovl = !kovl; ); tabm && gch == '[' ? tabstep(-1); tabm && gch == ']' ? tabstep(1); (gch == 116 || gch == 84) && !basic && !learn && !vlock ? ( vopt[45] = 0; szclr = 1; zmz ? ( vopt[48] = zfit0; (vopt[12] & 512) && !zhid0 ? vopt[12] -= 512; zmz = 0; ); );   // T = Tabs, A = All, F = Fit on / off (0 = Tabs <-> All with Fit all)
  (gch == 97 || gch == 65) && !basic && !learn && !vlock ? ( vopt[45] = 1; szclr = 1; zmz ? ( vopt[48] = zfit0; (vopt[12] & 512) ? vopt[12] -= 512; zmz = 0; ); scy = 0; );
  (gch == 98 || gch == 66) ? ( vopt[11] = 1; vopt[43] = 0; szclr = 1; );   // B = Basic, E = Expert, L = Learn
  (gch == 101 || gch == 69) ? ( vopt[11] = 0; vopt[43] = 0; szclr = 1; );
  (gch == 108 || gch == 76) ? ( vopt[11] = 0; vopt[43] = 1; szclr = 1; );
  (gch == 104 || gch == 72) && !vlock ? ( vopt[51] = vopt[51] >= 0.5 ? 0 : 1; );   // H = stereo handles on / off (display only)
  (gch == 102 || gch == 70) && !basic && !learn && !vlock ? ( tabm ? ( vopt[48] = vopt[48] >= 0.5 ? 0 : 1; vopt[35] = 0; ) : ( vopt[42] >= 0.5 ? ( vopt[42] = 0; ) : ( vopt[42] = 1; memset(vopt + 21, 0, 10); vopt[33] = 0; ); scy = 0; ); );
  gch == '0' && !basic && !learn && !vlock ? (   // 0 = every module at once, all fitted into the window (again = back to Tabs, as it was)
    vopt[45] >= 0.5 ? ( vopt[45] = 0; vopt[42] = zfitPrev; ) : ( zfitPrev = vopt[42]; vopt[45] = 1; vopt[42] = 1; scy = 0; ); szoom = 0; zmz = 0; );
  // key Z with the mouse over the Stage View (every mode): the Stage View alone over the whole window (as its magnifier); Z again = back
  zvk = 0; (gch == 122 || gch == 90) && !vlock && (zoomv || (svzF == mzfr && mouse_x >= svzX && mouse_x < svzX + svzW && mouse_y >= svzY && mouse_y < svzY + svzH)) ? (
    zoomv = !zoomv; vvi = basic ? 13 : 12; zoomv && basic && (vopt[vvi] & 512) ? ( vopt[vvi] -= 512; ); zvk = 1; );
  (gch == 122 || gch == 90) && !zvk ? ( tabm && !vlock && !learn ? (   // key z - the module of the tab as large as it gets - room view hidden, fitted to the window (again = back)
// Z zoom = the Stage View is gone completely (vgone), only the module; Z again = the Stage View comes back, opened
    zmz ? ( vopt[48] = zfit0; (vopt[12] & 512) && !zhid0 ? vopt[12] -= 512; zmz = 0; )
        : ( zfit0 = vopt[48]; zhid0 = (vopt[12] & 512) > 0; vopt[48] = 1; vopt[35] = 0; zmz = 1; ); ) : (   // zhid0 = the Stage View was hidden before - it stays hidden after the zoom
// All / Fit (Expert) - Z zooms the module under the mouse (as its magnifier does); Z again (or Esc) = back
    !tabm && !learn && !vlock ? ( zoomv ? ( zoomv = 0; ) : ( szoom ? ( szoom = 0; ) : (   // (Basic too: its four cards)
      zhovId = -1; k = 0; loop(21, MZR[80 + k] == mzfr && mouse_x >= MZR[k * 4] && mouse_x < MZR[k * 4] + MZR[k * 4 + 2] && mouse_y >= MZR[k * 4 + 1] && mouse_y < MZR[k * 4 + 1] + MZR[k * 4 + 3] ? zhovId = k; k += 1; );
      zhovId > 0 ? ( szoom = zhovId; basic ? ( vopt[13] & (2 ^ zhovId) ? vopt[13] -= 2 ^ zhovId; ) : ( vopt[12] & (2 ^ zhovId) ? vopt[12] -= 2 ^ zhovId; ); ) : ( zhovId == 0 ? zoomv = 1; ); ); ); );   // (0 = the Stage View; a folded module opens for the zoom)
  ); );  !vlock && !udone && (gch == 26 || gch == 25) ? ( udone = 1; gch == 25 || (mouse_cap & 8) ? uredo() : udo(); ); kqn += 1; gch > 0 && kqn < 256; ); );   // queue: Esc = back from the zoom; the rest is dropped (not while a snapshot is being named)
gsN = gs; goxN = gox; goyN = goy; WTN = WT; HTN = HT;
tabm && !zoomv ? ( szoom = ffTab ? 1 : tabsel; tabzs = 1; ) : ( tabzs ? ( szoom = 0; tabzs = 0; ); );   // leaving the Tabs (key 0, the All button) must not keep the tab's module in zoom // tabs: the module of the tab = a section zoom docked under the room view
szoom ? (
  basic ? (
    // Basic: one of the four cards, at a comfortable width
    yS1 = 0; yS2 = 0; zw = 200; bfill = 0; hbR = sech(1, 206 + secx(1)); hbS = sech(3, 206 + secx(3)); hbB = sech(5, 180 + secx(5)); hbO = sech(7, 180 + secx(7)); zh = szoom == 1 ? hbR : (szoom == 3 ? hbS : (szoom == 5 ? hbB : hbO));   // no fill height in the zoom (the controls moved down by it and were cut off at the bottom) // as tall as the card really is (Source and Room are 206 - they were cut off)
    szoom == 1 ? ( sxR = 0; bwR = 200; );
    szoom == 3 ? ( sxS = 0; bwS = 200; );
    szoom == 5 ? ( sxB = 0; bwB = 200; );
    szoom == 7 ? ( sxO = 0; bwO = 200; IOX = 0; IOY = 0; IOW = 200; IOH = hbO; );
  ) : (
    szoom == 1 ? ( L0 = 0; LW = 600; syR = 0; syV = 90 + secx(1) / 2; zw = 600; zh = 182 + secx(1); );
    szoom == 3 ? ( srX = 0; srW = 560; syS = 0; shS = 260; zw = 560; zh = 260; );
    szoom == 4 ? ( R0 = 0; RWc = 444; syW = 0; shW = (tabm ? 150 : 118) + secx(4); zw = 444; zh = shW; );
    szoom == 5 ? ( R0 = 0; RWc = 444; syB = 0; zw = 444; zh = (tabm ? 196 : 170) + secx(5); tabm ? shB = zh; );   // tabs: room for the three text lines beside the curve
    szoom == 6 ? ( flX = 0; flW = 268; syF = 0; shF = tabm ? 210 + secx(6) : shF; zw = tabm ? 560 : 268; zh = tabm ? 210 + secx(6) : 136 + secx(6); );
    szoom == 7 ? ( IOX = 0; IOY = 0; IOW = 600; IOH = tabm ? 210 + secx(7) : shO; zw = 600; zh = tabm ? 210 + secx(7) : 112 + secx(7); );
    szoom == 10 ? ( L0 = 0; LW = 600; syD = 0; shD = 196; zw = tabm ? 960 : 600; zh = 196; );
    szoom == 14 ? ( mtPX0 = 0; mtPW0 = 600; syM = 0; shM = 260 + secx(14); zw = 600; zh = shM; );   // tabs: wide enough for L | R side by side
  );
  tabm ? (
    // TABS: as large as the area under the tabs allows (at most 1.7 x the page), the module takes the width (at most 1.8 x its own)
    tzPW = gfx_w - 16 * gsN; tzPH = gfx_h - (TAY + 11) * gsN;
    gsZ = max(0.25, min((tfit ? 1.8 : (tcus ? tcs : 1.7)) * gsN, min(tzPW / 600, tzPH / tzhA)));   // one scale for every tab (measured on the tallest module) - switching tabs keeps the size
    zwT = floor(tzPW / gsZ);   // every tab's module has the same size - the whole area under the tabs
    szoom == 1 || szoom == 10 ? ( LW = zwT; ); szoom == 10 ? ( shD = max(196, min(300, floor(tzPH / gsZ) - 4)); zh = shD; );   // delay: as tall as the area (the echo display grows)
    szoom == 3 ? ( srW = zwT; ); szoom == 4 || szoom == 5 ? ( RWc = zwT; ); szoom == 6 ? ( flW = zwT; ); szoom == 7 ? ( IOW = zwT; ); szoom == 14 ? ( mtPW0 = zwT; );
    gox = floor(8 * gsN + (tzPW - zwT * gsZ) / 2); goy = floor(TAY * gsN);
    zhF = max(zh, floor(tzPH / gsZ) - 2); tabm ? ( szoom == 1 ? ( shR = zhF; ); szoom == 3 ? ( shS = zhF; ); szoom == 4 ? ( shW = zhF; ); szoom == 5 ? ( shB = zhF; ); szoom == 6 ? ( shF = zhF; ); szoom == 7 ? ( IOH = zhF; ); szoom == 10 ? ( shD = zhF; ); szoom == 14 ? ( shM = zhF; ); zh = zhF; );   // the frame and the card: always the whole area under the tabs
  ) : (
    gsZ = max(0.25, min(2.5 * gsN, min((gfx_w - 24 * gsN) / zw, (gfx_h - 96 * gsN) / zh)));   // at most 2.5 x larger (a margin all round)
    gox = floor((gfx_w - zw * gsZ) / 2); goy = floor(74 * gsN + (gfx_h - 86 * gsN - zh * gsZ) / 2);
  );
  goxZ = gox; goyZ = goy;
  gs = gsZ; WT = (gfx_w - gox) / gs; HT = (gfx_h - goy) / gs; LH = HT; scmax = 0; scy = 0;
);
setfonts();
mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs;
click = (mouse_cap & 1) && !(lastcap & 1);
!(mouse_cap & 1) ? ( mtDrg = 0; );   // (a motion-path drag ends with the button)
// TABS: the corner of the module (lower right) - drag = the module larger / smaller (the room view takes the rest), double-click = automatic
tch = 0;
tabm && szoom && !zoomv && !vlock && !helpon && !(rmenu || smenu || fmenu || bmenu || kmenu || wmenu || emenu || xpon || dmenu || tpin) ? (
  tch = !tcd && mx >= zwT - 16 && mx <= zwT + 2 && my >= zhF - 16 && my <= zhF + 2;
  click && tch ? (
    time_precise() - tcdt < 0.35 ? ( vopt[35] = 0; tcd = 0; ) : ( tcd = 1; tcm0 = mouse_x + mouse_y; tcs0 = gsZ / gsN; tcD0 = (zwT + zhF) * gsZ; );
    tcdt = time_precise(); click = 0;
  );
);
tcd ? (
  (mouse_cap & 1) ? ( vopt[35] = min(1.8, max(1, tcs0 * (tcD0 + mouse_x + mouse_y - tcm0) / max(1, tcD0))); vopt[48] = 0; click = 0; ) : ( tcd = 0; );
);
rclick = (mouse_cap & 2) && !(lastcap & 2);   // right-click (Mac: also Ctrl+click) = value entry
// SNAPSHOT MENU (right-click on a slot, Expert): like My rooms - its click is taken first, before anything under it; - Rename, Overwrite, Clear (an empty slot: Store, Name and store)
snctx ? (
  snmN = uslot[(snctx - 1) * USL] ? 3 : 2;
  smx = (mouse_x - snqX) / snmS; smy = (mouse_y - snqY) / snmS;          // menu units (top-bar scale, fixed on screen)
  snmHov = smx >= 0 && smx < 210 && smy >= 2 && smy < 2 + snmN * 18 ? floor((smy - 2) / 18) : -1;
  click || rclick ? (
    k = snctx - 1;
    snmN == 3 ? (
      snmHov == 0 ? ( sn_open(k); ) : ( snmHov == 1 ? ( saveslot(k); snlast = k + 1; snflash = time_precise(); snflk = k; ) : ( snmHov == 2 ? ( snclear(k); ); ); );
    ) : (
      snmHov == 0 ? ( saveslot(k); snlast = k + 1; snflash = time_precise(); snflk = k; ) : ( snmHov == 1 ? ( sn_open(k); ); );
    );
    snctx = 0; click = 0; rclick = 0; snPend = 0;
  );
);
!uinit ? ( uinit = 1; unN = rdN = 0; ugest = 0; ucopy(UBS); );
(mouse_cap & 3) || mouse_wheel != 0 ? ( ugest = 1; ulast = time_precise(); );
// the help and keys pages: × or a click beside the page closes it, a click inside stays on the page (the scroll
// bar takes it), the wheel scrolls it; on the help page the button "All keys ..." opens the keys page
(kovl || hlpon) && (click || rclick) ? (
  sqr(mouse_x - pxX) + sqr(mouse_y - pxY) <= sqr(pxR + 1) || !(mouse_x >= pwX0 && mouse_x < pwX1 && mouse_y >= pwY0 && mouse_y < pwY1) ? ( kovl = 0; hlpon = 0; ) : (
    hlpon && click && mouse_x >= hkX0 && mouse_x < hkX1 && mouse_y >= max(hkY0, pbY0) && mouse_y < min(hkY1, pbY1) ? ( hlpon = 0; kovl = 1; ) : ( pgClk = click; ); );
  click = 0; rclick = 0; mouse_wheel = 0;
);
(kovl || hlpon) && mouse_wheel != 0 ? ( pgSc = min(max(pgSc - mouse_wheel / 120 * 40, 0), pgMax); mouse_wheel = 0; );
!(kovl || hlpon) ? ( pgWho = 0; pgDrag = 0; );
vedit && mouse_x >= vedPX0 && mouse_x < vedPX1 && mouse_y >= vedPY0 && mouse_y < vedPY1 ? ( click = 0; rclick = 0; );
tipon = 0; tipforce = 0; cfade = 1; wheelused = 0;
shsec = 0; shclr = click && shpin;          // section help: hover target of this frame; a click anywhere else closes a pinned panel
// ---- dropdown menus + help: geometry and click handling (before any other control sees the click) ----
fzsel = min(12, max(0, floor(slider35 + 0.5))); spcur = -1; k = 0; loop(NSP, spcur < 0 && spmatch(k) ? spcur = k; k += 1; );   /* 0.309: the matching preset, once per frame */ devm = slider85 >= 0.5 && fzsel > 0 && slider89 >= 99.95; rbyp = slider36 >= 0.5;   // devm: a device plays - a mono loudspeaker, the stereo width does nothing (isfree comes from the audio block)
roombx = 224; roomby = 11; roombw = 116; roombh = 17;
roommx = roombx; roommy = roomby + roombh + 4; roomih = 16; RMCW = 240; RMHH = 16; roommw = 2 * RMCW + 12;
NOPOS = -99999;
rmhy = roommy + 6; rsqx = roommx + 8; rsqw = 148; rsvw = 104; rsvx = roommx + roommw - 8 - rsvw;   // header: search field, Save room
rmyl = roommy + 30; rmyr = rmyl; rmfirst = -1; urfirst = -1;
// MY ROOMS (the ones the search finds): two columns under one heading
urnv = 0; u = 0; loop(URM, urX[u] = NOPOS; urb[u * URL] && bmatch(urb + u * URL + 2, rqb, rqlen) ? ( urX[u] = 0; urnv += 1; ); u += 1; );
urnv > 0 ? (
  rmyl += RMHH; urhy = rmyl - 7; i = 0; u = 0;
  loop(URM, urX[u] == 0 ? ( urfirst < 0 ? urfirst = u; urX[u] = roommx + 4 + (i % 2) * (RMCW + 4); urY[u] = rmyl + floor(i / 2) * roomih; i += 1; ); u += 1; );
  rmyl += ceil(urnv / 2) * roomih + 8; rmyr = rmyl; ursep = rmyl - 4;
);
// factory rooms: a category heading above the first one that is shown
rmsc = rmpg == 2 && rqlen <= 0; rmfp = rmpg == 3 && rqlen <= 0;   // page 4 = free-field presets // page 3 = the scenes
rcl0 = rcl1 = -1; k = 0;
loop(NPRE,
  // category rcc: (rcc % 6) < 3 = left column, otherwise right column
  rcc = roomcat(k); rcr = (rcc % 6) >= 3; rmH[k] = 0;
  (rqlen <= 0 ? (!rmsc && !rmfp && (k >= NPG1) == rmpg) : bmatch(strtob(roomname(k), rtb), rqb, rqlen)) ? (   // one page; a search finds rooms on both
    rmfirst < 0 ? rmfirst = k;
    (rcr ? rcl1 : rcl0) != rcc ? ( rmH[k] = 1; rcr ? ( rmyr += RMHH + (rcl1 >= 0) * 4; rcl1 = rcc; ) : ( rmyl += RMHH + (rcl0 >= 0) * 4; rcl0 = rcc; ); );
    rmX[k] = roommx + 4 + rcr * (RMCW + 4); rmY[k] = rcr ? rmyr : rmyl;
    rcr ? ( rmyr += roomih; ) : ( rmyl += roomih; );
  ) : ( rmX[k] = NOPOS; );
  k += 1;
);
scl0 = scl1 = -1; k = 0;
loop(NSC,
  rcc = sccat(k); rcr = sccol(rcc); scnH[k] = 0;
  rmsc || (rqlen > 0 && bmatch(strtob(scname(k), rtb), rqb, rqlen)) ? (   // page 3 - or a search finds it (all pages)
    (rcr ? scl1 : scl0) != rcc ? ( scnH[k] = 1; rcr ? ( rmyr += RMHH + (scl1 >= 0) * 4; scl1 = rcc; ) : ( rmyl += RMHH + (scl0 >= 0) * 4; scl0 = rcc; ); );
    scnX[k] = roommx + 4 + rcr * (RMCW + 4); scnY[k] = rcr ? rmyr : rmyl;
    rcr ? ( rmyr += roomih; ) : ( rmyl += roomih; );
  ) : ( scnX[k] = NOPOS; );
  k += 1;
);
k = 0; loop(NFF, ffpmX[k] = NOPOS; k += 1; ); ffpmH = -1; ffpn = 0;
rmfp ? (
  k = 0; loop(NFF, ffpmX[k] = roommx + 4 + (k >= 9) * (RMCW + 4); ffpmY[k] = rmyl + (k % 9) * roomih; k += 1; );
  rmyl += 9 * roomih; rmyr = rmyl;
) : rqlen > 0 ? (
  // a search also finds the free-field presets (page 4): under the heading "Free field", two columns below everything else
  k = 0; loop(NFF, bmatch(strtob(ffpname(k), rtb), rqb, rqlen) ? ( ffpmX[k] = 0; ffpn += 1; ); k += 1; );
  ffpn > 0 ? (
    rmyl = max(rmyl, rmyr) + RMHH + 4 * (rmfirst >= 0 || urnv > 0 || scl0 >= 0 || scl1 >= 0); i = 0;
    k = 0; loop(NFF, ffpmX[k] == 0 ? ( ffpmH < 0 ? ffpmH = k; ffpmX[k] = roommx + 4 + (i % 2) * (RMCW + 4); ffpmY[k] = rmyl + floor(i / 2) * roomih; i += 1; ); k += 1; );
    rmyl += ceil(ffpn / 2) * roomih; rmyr = rmyl;
  );
);
rmnone = urnv == 0 && rmfirst < 0 && !rmsc && !rmfp && scl0 < 0 && scl1 < 0 && ffpn == 0;
rmX[NPRE] = NOPOS; rmY[NPRE] = max(rmyl, rmyr) + 7;   // row below both columns (the "Keep when loading" chips sit in it; Free field is its own tab, no longer a row here)
roommh = rmY[NPRE] + roomih + 4 - roommy;
rkx = roommx + 12; rky = rmY[NPRE] + 1; rkc1 = rkx + 74; rkc2 = rkc1 + 84;          // "Keep when loading" chips
rpgx = rsvx - 6 - 208;                                                // pages 1 | 2 | 3 | 4 (not while searching: the search shows the room pages together)
NOPOS = -99999;                                                          // position of a control that is not shown
mnvl = szoom ? 4 - gox / gs : 4; mnvr = WT - 4;                            // visible part of the page (current scale)
mnvt = szoom ? (68 * gsN - goy) / gs : scy + 4; mnvb = szoom ? HT - 4 : scy + HT - 4;
basic ? ( srcbx = secvis(3) ? sxS + 10 : NOPOS; srcby = yS1 + 28; srcbw = bwS - 20; srcbh = 40; ) : ( srcbx = secvis(3) ? srX + 118 : NOPOS; srcby = syS + 5; srcbw = max(90, min(170, srW - 238)); srcbh = 16; );   // the box ends before the mix ring (+ the reset icon)
SMW1 = 196; srcmw = SMW1 + 222; srcih = 16; srcmh = max(srcih * 13 + 18, 4 + NSP * srcih + 3 * 15 + 8); mfit(srcbx, srcby, srcbw, srcbh, srcmw, srcmh, 1); srcmx = mfx; srcmy = mfy; srcmk = mfk;   // models | presets
esx = srX + srW - (tabm ? 236 : 164); eslx1 = basic || !secvis(3) ? NOPOS : esx; eslx2 = eslx1 + (tabm ? 116 : 80); esly = syS + 30; eslw = tabm ? 110 : 74; eslh = 15;   // tabs: wider slope boxes (the text was cut) // SOURCE slopes
// CALL LINE strip under the source curve (Expert): Line menu + Duplex, Codec, Dropouts
// (All: two rows of two under the curve - the module is 24 px taller; Tabs: a row of knobs)
clsh = tabm ? 108 : 48; clx = srX + 10; clw = srW - (tabm ? 256 : 184); clyy = syS + shS - (tabm ? 12 : 8) - clsh + 6;
lnbx = eslx1 == NOPOS ? NOPOS : clx; lnbw = tabm ? 170 : floor((clw - 10) / 2); lnbh = 15; lnby = tabm ? clyy + 46 : clyy + 1;
emw = emenu == 4 ? 190 : (emenu == 3 ? 170 : 96); eih = 15; emh = (emenu >= 3 ? 4 : 9) * eih + 8; mfit(emenu == 4 ? dtbx : (emenu == 3 ? lnbx : (emenu == 2 ? eslx2 : eslx1)), emenu == 4 ? dtby : (emenu == 3 ? lnby : esly), emenu == 4 ? dtbw : (emenu == 3 ? lnbw : eslw), eslh, emw, emh, 1); emx = mfx; emy = mfy; emk = mfk;
fe6 = secx(6); fhw = floor(flW / 2); fstk = !basic;
fstk ? (
  fkr = max(KR0, min(min(2 * KR0, fhw / 2 - 18), KR0 + fe6 / 3)); fks = fkr / KR0;     // knob over its slope menu, under Pre / Post
  fky = syF + 50 + fkr + fe6 / 6; fcby = fky + fkr + 8 * (1 + (fks - 1) * 0.6) + 9;
  lcbx = basic || !secvis(6) ? NOPOS : flX + fhw / 2 - 24;
) : (
  fks = 1; fky = syF + 52 + fe6 / 2; fcby = syF + 42 + fe6 / 2;
  lcbx = basic || !secvis(6) ? NOPOS : flX + 78;
);
hcbx = lcbx + fhw; fcbw = tabm ? 58 : 48; fcbh = 15;   // FILTER slopes (tabs - the larger text needs 58)
tabm && szoom == 6 ? ( fcby = syF + 27; lcbx = flX + flW / 2 - 70; hcbx = flX + flW - 62; );   // tabs: slopes in the first row   // FILTER slopes
// Returns the tooltip text describing filter slope choice k (6, 12, 18 ... dB/oct)
function fsltip(k) ( k == 0 ? "6 dB / oct: very gentle, hardly changes the phase – a soft tilt, not a real cut" : (k == 1 ? "12 dB / oct: gentle, natural – the usual choice for rumble and hiss" : (k == 2 ? "18 dB / oct: moderate – clearly audible cut" : (k == 3 ? "24 dB / oct: steep – a clean cut, little left past the corner" : (k == 4 ? "30 dB / oct: steeper still – the band edge is easy to hear" : (k == 5 ? "36 dB / oct: very steep – can ring a little at the corner" : (k == 6 ? "42 dB / oct: almost a wall – more ringing at the corner" : "48 dB / oct: brick wall – strongest cut, most ringing")))))); );
// Computes placement (fmx1, fmx2, fmy, fmk) and size of the Filter slope dropdown menu via mfit; runs again before the menu opens
function fmgeo() ( fmw = 96; fih = 15; fmh = fih * 8 + 8; mfit(fmenu == 2 ? hcbx : lcbx, fcby, fcbw, fcbh, fmw, fmh, 0); fmx1 = fmx2 = mfx; fmy = mfy; fmk = mfk; );
fmgeo();
basic ? ( bdx = NOPOS; bdy = yS2 + 52; bdw = bwB - 20; bdh = 16; bky = NOPOS; bkbx = NOPOS; )   // Basic: no element menu (the default element of each type)
      : ( bdx = secvis(5) ? R0 + 10 : NOPOS; bgx = secx(5) * 0.2; bdy = syB + 70 + bgx; bdw = floor((RWc - 26) / 2); bdh = 15; bky = bdy; bkbx = bdx + bdw + 6; );   // element | back part side by side   // BARRIER element / back part
bmw = bdw; bmih = 18; bmh = 3 * bmih + 8; mfit(bdx, bdy, bdw, bdh, bmw, bmh, 0); bmx = mfx; bmy = mfy; bmk = mfk;
basic ? ( wsx = NOPOS; wdy1 = 0; wdw = 0; wmx2 = secvis(1) ? sxR + 10 : NOPOS; wdy2 = yS1 + 122 + secx(1) * 0.6; wdw2 = bwR - 20; wdh = 16; )
      : ( wsx = secvis(4) ? R0 + 10 : NOPOS; wdy1 = syW + 28; wdw = floor((RWc - 26) * 118 / 242); wmx2 = wsx + wdw + 6; wdy2 = wdy1; wdw2 = RWc - 26 - wdw; wdh = 15; );   // WALLS surface / material
function wmgeo() (   // (a function - called again after the click that opens the menu, so the first frame is already in place)
  wmih = 16; wmn = wmenu == 1 ? 6 : 9; wmh = wmn * wmih + 8; wmxx = wmenu == 1 ? wsx : wmx2; wmw = wmenu == 1 ? wdw : wdw2;
  mfit(wmenu == 1 ? wsx : wmx2, wmenu == 1 ? wdy1 : wdy2, wmw, wmenu == 1 ? 15 : wdh, wmw, wmh, 0); wmxx = mfx; wmy = mfy; wmk = mfk;
);
wmgeo();
kbw = 120; kbx = zoomv ? VX + VW - 40 - kbw : VX + 190; kby = VY + 5; kbRs = bon && !isfree && zoomv ? kbw + 72 : 0;   // zoom: at the right end of the row (it sat on Reset positions); kbRs = what it takes from the row // back part in the enlarged room view
kmw = (kmenu && kmsrc == 2) ? 150 : bdw; kmih = 16; kmh = 10 * kmih + 8; kmk = 1;
kmenu ? (                                                             // keep the open menu attached to its dropdown, also while resizing
  kmsrc == 2 ? mfit(kbx, kby, kmw, 14, kmw, kmh, 0) : mfit(bkbx, bky, kmw, 15, kmw, kmh, 0); kmx = mfx; kmy = mfy; kmk = mfk;
);
mdlx = basic || !secvis(3) ? NOPOS : esx + 68; mdly = tabm ? syS + 56 : syS + 84;     // "Models" (Expert), beside Reset model under Drive
// ([ ] = previous / next tab, 0 = tabs <-> All - typed characters in the queue above, any keyboard layout)
// ---- KEYBOARD: 1 … 9 = open tab (Tabs) or snapshot 1 … 8, Shift + 1 … 8 = snapshots 9 … 16 (All / Basic); arrows = listener 10 cm (Alt: source, Shift: 2 cm) ----
// (only the key states are read - the key queue stays for REAPER, so Space etc. keep working; only while the mouse is
// over the plugin window and the window has the focus)
kfoc = gfx_getchar(65536); kfoc = kfoc > 0 && (kfoc & 1) && (kfoc & 2);
!snedit && !vedit && !rsave && !rmenu && kfoc ? (
  gk = 0;
  loop(9,   // 1 … 9 (nine tabs)
    kdn = gfx_getchar(49 + gk) > 0;
// TABS: 1 … 9 = open that tab (in the order of the row); Shift + 1 … 8 = snapshots 1 … 8. All / Basic: 1 … 8 = snapshots 1 … 8, Shift + 1 … 8 = snapshots 9 … 16
    kdn && !kprev[gk] ? (
      tabm && !(mouse_cap & 8) ? ( gk < taborder() ? ( vopt[46] = tbo[gk]; ); ) : (
        gk < 8 ? ( kslot = gk + ((mouse_cap & 8) && !tabm ? 8 : 0); uslot[kslot * USL] ? ( loadslot(kslot); snlast = kslot + 1; ugest = 1; ulast = time_precise(); ); ); ); );
    kprev[gk] = kdn; gk += 1;
  );
  kzd = gfx_getchar('z') > 0 && (mouse_cap & 4);
  kzd && !kzp && !udone && !vlock ? ( udone = 1; (mouse_cap & 8) ? uredo() : udo(); );
  kzp = kzd;
  // arrow keys (4-char key codes "left", "rght", "up", "down") -> bits 1 left, 2 right, 4 up, 8 down
  karw = (gfx_getchar(1818584692) > 0) * 1 + (gfx_getchar(1919379572) > 0) * 2 + (gfx_getchar(30064) > 0) * 4 + (gfx_getchar(1685026670) > 0) * 8;
  karw ? ( ugest = 1; ulast = time_precise(); );
  karw && (karw != karwp || time_precise() - karwt > 0.12) ? (   // (room off: the arrows do nothing)
    kst = ((mouse_cap & 8) ? 0.02 : 0.1) / rs; kwh = (mouse_cap & 16) ? 1 : 2;
    kox = kwh == 1 ? slider10 : slider13; koy = kwh == 1 ? slider11 : slider14;
    moveobj(kwh, kox + ((karw & 2) ? kst : 0) - ((karw & 1) ? kst : 0), koy - ((karw & 4) ? kst : 0) + ((karw & 8) ? kst : 0));
    karwt = time_precise();
  );
  karwp = karw;
);
// ---- SNAPSHOT NAME popup (double-click on a slot): keys and clicks go to it first ----
snedit ? (
  snk = snedit - 1; snmx = (mouse_x - goxN) / gsN; snmy = (mouse_y - goyN) / gsN;
  sn_geo();
  click && snmx >= snpx && snmx < snpx + SNPW && snmy >= snpy && snmy < snpy + 26 ? ( pdrag = 2; pdm0x = mouse_x; pdm0y = mouse_y; pd0x = pdSX; pd0y = pdSY; click = 0; );   // heading: move the window
  snact = 0; sngd = 0;                                                  // 1 = save, 2 = rename only, 3 = cancel
  while (
    gch = gfx_getchar();
    gch > 0 ? (
      gch == 9 ? ( snf = !snf; snt0 = time_precise(); ) : (                       // Tab: other field
      gch == 13 ? ( snact = 1; ) : (
      gch == 27 ? ( snact = 3; ) : (
      gch == 8 ? ( sn_back(); ) : (
      gch >= 117 * 16777216 && gch < 118 * 16777216 ? ( sn_add(gch - 117 * 16777216); ) : (
      gch >= 32 && gch < 256 && gch != 127 ? ( sn_add(gch); ); )))));
    );
    sngd += 1;
    gch > 0 && sngd < 64 && !snact;
  );
  click ? (
    snmx >= snfx && snmx < snfx + snfw ? (
      snmy >= snny && snmy < snny + snnh ? ( snf = 0; snt0 = time_precise(); );
      snmy >= sndy && snmy < sndy + sndh ? ( snf = 1; snt0 = time_precise(); );
    );
    snmy >= snby && snmy < snby + 18 ? (
      snmx >= snpx + 12 && snmx < snpx + 76 ? ( snact = 1; );
      uslot[snk * USL] && snmx >= snpx + 82 && snmx < snpx + 146 ? ( snact = 2; );
      snmx >= snpx + SNPW - 76 && snmx < snpx + SNPW - 12 ? ( snact = 3; );
    );
    !(snmx >= snpx && snmx < snpx + SNPW && snmy >= snpy && snmy < snpy + SNPH) ? ( snact = 3; );   // click outside = cancel
    click = 0;
  );
  snact == 1 ? ( sn_commit(snk, 1); ) : ( snact == 2 ? ( sn_commit(snk, 0); ) : ( snact == 3 ? ( snedit = 0; ); ); );
  mouse_wheel = 0;
);
// MY ROOMS save dialog: name, category, Save / Cancel (modal)
function rs_geo() ( rspw = 360; rsph = 186; rspx = min(max(4, (WTN - rspw) / 2 + pdRX), WTN - rspw - 4); rspy = min(max(4, 40 + pdRY), HTN - rsph - 4); rsfx = rspx + 12; rsfw = rspw - 24; rsny = rspy + 42; rscy = rspy + 92; rsby = rspy + rsph - 28; );
rsave ? (
  rs_geo(); rsmx = (mouse_x - goxN) / gsN; rsmy = (mouse_y - goyN) / gsN;
  click && rsmx >= rspx && rsmx < rspx + rspw && rsmy >= rspy && rsmy < rspy + 26 ? ( pdrag = 3; pdm0x = mouse_x; pdm0y = mouse_y; pd0x = pdRX; pd0y = pdRY; click = 0; );
  rsfull = !rsren && urfind(rsnb) < 0 && urcount() >= URM;
  click ? (
    k = 0; loop(7, rcx0 = rsfx + (k % 4) * (rsfw + 4) / 4; rcy0 = rscy + floor(k / 4) * 20;
      rsmx >= rcx0 && rsmx < rcx0 + (rsfw + 4) / 4 - 4 && rsmy >= rcy0 && rsmy < rcy0 + 16 ? rscat = k; k += 1; );
    rsmy >= rsby && rsmy < rsby + 18 ? (
      rsmx >= rspx + 12 && rsmx < rspx + 88 ? rsact = 1;
      rsmx >= rspx + rspw - 76 && rsmx < rspx + rspw - 12 ? rsact = 2;
    );
    !(rsmx >= rspx && rsmx < rspx + rspw && rsmy >= rspy && rsmy < rspy + rsph) ? rsact = 2;
    click = 0;
  );
  rsclash = rsren && urfind(rsnb) >= 0 && urfind(rsnb) != rsren - 1;           // rename: the name of another room
  rsact == 1 ? (
    rsren ? ( rsnlen > 0 && !rsclash ? ( i = 0; loop(24, urb[(rsren - 1) * URL + 2 + i] = rsnb[i]; i += 1; ); urb[(rsren - 1) * URL + 1] = rscat; rsave = 0; rsren = 0; ); )
          : ( rsnlen > 0 && !rsfull ? ( ursave(); rsave = 0; ); );
  ) : ( rsact == 2 ? ( rsave = 0; rsren = 0; ); );
  rsact = 0; mouse_wheel = 0; menuused = 1;
);
menuused = 0;
pclick = 0;
// POPUP DRAG: tempo tools (1), snapshot dialog (2), save / rename room (3), Expert-settings list (4), source models (5) - moved by the heading
pdrag ? (
  (mouse_cap & 1) ? (
    pdrag == 1 ? ( pdTX = pd0x + (mouse_x - pdm0x) / gs; pdTY = pd0y + (mouse_y - pdm0y) / gs; );
    pdrag == 2 ? ( pdSX = pd0x + (mouse_x - pdm0x) / gsN; pdSY = pd0y + (mouse_y - pdm0y) / gsN; );
    pdrag == 3 ? ( pdRX = pd0x + (mouse_x - pdm0x) / gsN; pdRY = pd0y + (mouse_y - pdm0y) / gsN; );
    pdrag == 4 ? ( pdXX = pd0x + (mouse_x - pdm0x) / gs; pdXY = pd0y + (mouse_y - pdm0y) / gs; );
    pdrag == 5 ? ( pdMX = pd0x + (mouse_x - pdm0x) / gs; pdMY = pd0y + (mouse_y - pdm0y) / gs; );
    click = 0;
  ) : ( pdrag = 0; );
);
!rmenu ? ( urctx = 0; );
// Expert list (Basic): modal. Tempo tools (Expert): a panel above the delay - only clicks inside it are its own
tpvis = tpon && !basic && !learn && (!szoom || szoom == 10 || szoom == 14) && !zoomv;   // (also from Motion) // also over the zoomed delay
tpvis ? (
  // drawn larger when there is room (up to 1.3 x, in screen terms); opens in the right column (two columns), at the
  // right of the zoomed delay, else above / below the delay. Dragged by its heading anywhere in the visible part of the page
  TPW = 330; TPH = 312;   // (+ the note table)
  szoom != tpzw ? ( tpmv = 0; tpzw = szoom; );                                      // zoom on / off: its own place again
  tpvt = szoom ? mnvt : scy + 32;                                                    // (below the top bar)
  tpvh = mnvb - tpvt - 8; tpvw = mnvr - mnvl - 8;                                    // visible part of the page
  szoom ? ( tpk = max(0.4, min(1.3 * gsN / gs, min(tpvw / TPW, tpvh / TPH)));
            tpSW = TPW * tpk; tpSH = TPH * tpk; tpx = mnvr - tpSW - 4; tpy = tpvt + 4; ) : (
  RWc > 200 ? (
    tpk = max(0.8, min(1.3, min((RWc - 12) / TPW, tpvh / TPH))); tpSW = TPW * tpk; tpSH = TPH * tpk;
    tpx = R0 + max(6, (RWc - tpSW) / 2); tpy = scy + TOPY + 4;
  ) : (
    tpk = max(0.8, min(1.3, min((LW - 12) / TPW, (HT - TOPY - 16) / TPH))); tpSW = TPW * tpk; tpSH = TPH * tpk;
    tpx = max(L0 + 6, L0 + LW - tpSW - 6);
    tpy = syD - tpSH - 6;
    tpy < max(VY + 28, scy + TOPY + 4) ? ( tpy = syD + shD + 6; );
  ); );
  tpmv ? ( tpx = pdTX; tpy = pdTY + scy; );                                         // where it was dragged to (on screen)
  tpx = max(mnvl, min(tpx, mnvr - tpSW)); tpy = max(tpvt, min(tpy, mnvb - tpSH));
  click && inbox(tpx, tpy, tpSW - 30 * tpk, 24 * tpk) && !inbox(tpx + (tbx - tpx0) * tpk, tpy + 6 * tpk, tbw * tpk, 14 * tpk) ? ( pdrag = 1; pdm0x = mouse_x; pdm0y = mouse_y; pd0x = tpx; pd0y = tpy - scy; tpmv = 1; pdTX = pd0x; pdTY = pd0y; click = 0; );   // (not on the BPM: typed there)
);
tpin = tpvis && inbox(tpx, tpy, tpSW, tpSH);
// the panel's place ON SCREEN from the last frame decides too (pixels, independent of the page / zoom scale - in the
// Motion tab the two did not agree, so clicks went to the module under the panel and the panel got none)
tpvis && tpSOk && mouse_x >= tpSX0 && mouse_x <= tpSX1 && mouse_y >= tpSY0 && mouse_y <= tpSY1 ? tpin = 1;
!tpvis ? tpSOk = 0;
xpon ? ( pclick = click; click = 0; menuused = 1; ) : ( tpin ? ( pclick = click; click = 0; mouse_wheel = 0; ); );
// DELAY PRESET menu (opened from the delay heading): geometry from the button, two columns, headings per group
(basic || (szoom && szoom != 10) || !dmenu) ? ( dmenu = 0; ) : (
  // laid out in menu units, then scaled down when needed (dmk <= 1, like the tempo tools) and placed so that the whole
  // menu is always inside the visible part of the window: below the button, else above it, else where it fits
  DPCW = 236; dpih = 16; dpmw = 2 * DPCW + 12;
  k = 0; dpyl = 4; dpyr = 4;
  loop(NDP,
    dcc = dpcat(k); dcr = dcc == 1;
    (k == 0 || dpcat(k - 1) != dcc) ? ( dcr ? ( dpyr += 16; ) : ( dpyl += 16 + (k > 0) * 6; ); );
    dpIX[k] = 4 + dcr * (DPCW + 4); dpIY[k] = dcr ? dpyr : dpyl;
    dcr ? ( dpyr += dpih; ) : ( dpyl += dpih; );
    k += 1;
  );
  dpmh = max(dpyl, dpyr) + 22;
  dmvl = szoom ? 4 - gox / gs : 4; dmvr = WT - 4;                               // visible part of the page (page units)
  dmvt = szoom ? (68 * gsN - goy) / gs : scy + 4; dmvb = szoom ? HT - 4 : scy + HT - 4;
  dmk = max(0.4, min(1, min((dmvr - dmvl) / dpmw, (dmvb - dmvt) / dpmh))); dmsw = dpmw * dmk; dmsh = dpmh * dmk;
  dpmy = dpby + dpbh + 4;
  dpmy + dmsh > dmvb ? ( dpmy = dpby - dmsh - 4; dpmy < dmvt ? ( dpmy = max(dmvt, dmvb - dmsh); ); );
  dpmx = max(dmvl, min(dpbx, dmvr - dmsw));
  k = 0; loop(NDP, dpIX[k] += dpmx; dpIY[k] += dpmy; k += 1; );               // item positions: menu units around (dpmx, dpmy)
  click ? (
    !menuxhit(dpmx, dpmy, dmsw, dmk) && inbox(dpmx, dpmy, dmsw, dmsh) ? (
      dpmxl = dpmx + (mx - dpmx) / dmk; dpmyl = dpmy + (my - dpmy) / dmk;     // the mouse in menu units
      hk = -1; k = 0; loop(NDP, dpmxl >= dpIX[k] && dpmxl < dpIX[k] + DPCW && dpmyl >= dpIY[k] && dpmyl < dpIY[k] + dpih ? ( hk = k; ); k += 1; ); hk >= 0 ? dpload(hk);
    );
    !inbox(dpbx, dpby, dpbw, dpbh) || menuxhit(dpmx, dpmy, dmsw, dmk) ? ( dmenu = 0; );
    click = 0; menuused = 1;
  );
  mouse_wheel = 0;
);
helpon ? (
  // SOURCE MODELS: a small panel like the tempo tools - it floats over the page and the plugin stays usable (only clicks
  // inside it are its own); drag it by its heading. A click on a model = Source uses it (unedited, switched on), the panel
  // stays open to compare; ×, Esc or the Models button closes it
  MPW = 452; MPH = 254;
  mps = max(0.75 * gsN, min(1.15 * gsN, min((gfx_w - 16) / MPW, (gfx_h - 16) / MPH)));   // its size on screen: about the page's
  mpk = mps / gs; mpSW = MPW * mpk; mpSH = MPH * mpk;
  mpx = mdlx != NOPOS ? mdlx - 150 : (WT - mpSW) / 2; mpy = mdlx != NOPOS ? mdly + 22 : scy + 80;
  mpx = min(max(mpx + pdMX, 4), WT - mpSW - 4); mpy = min(max(mpy + pdMY, scy + 4), scy + HT - mpSH - 6);   // + where it was dragged
  hpmx = (mx - mpx) / mpk; hpmy = (my - mpy) / mpk;                       // panel units
  mpin = hpmx >= 0 && hpmx < MPW && hpmy >= 0 && hpmy < MPH;
  hphov = -1; k = 0;
  loop(13, hcx = 10 + (k % 5) * 87; hcy = 32 + floor(k / 5) * 72; hpmx >= hcx && hpmx < hcx + 83 && hpmy >= hcy && hpmy < hcy + 68 ? ( hphov = k; ); k += 1; );
  hpxh = sqr(hpmx - (MPW - 16)) + sqr(hpmy - 15) < 110;
  mpin ? (
    click ? (
      hpxh ? ( helpon = 0; ) : (
        hphov >= 0 ? ( snap_take(); slider35 = hphov; slider37 = hphov > 0 ? 50 : 0; i = 47; loop(13, sset(i, 0); i += 1; ); slider85 = 1; snap_notify(); ) : (
          hpmy < 26 ? ( pdrag = 5; pdm0x = mouse_x; pdm0y = mouse_y; pd0x = pdMX; pd0y = pdMY; );   // the heading: move it
        );
      );
      click = 0; menuused = 1;
    );
    mouse_wheel = 0;
  );
);
(
  rmenu ? (
    tabm && szoom ? ( mxZs = mx; myZs = my; mx = (mouse_x - goxN) / gsN; my = (mouse_y - goyN) / gsN; );   // tabs: the menu lives at the page scale (top bar)
    // right-click on one of my rooms: a small menu (Rename, Overwrite with the current settings, Delete)
    rclick && !urctx ? (
      uh = -1; u = 0; loop(URM, urX[u] != NOPOS && inbox(urX[u], urY[u], RMCW, roomih) ? uh = u; u += 1; );
      uh >= 0 ? ( urctx = uh + 1; urcx = mx; urcy = my; rclick = 0; menuused = 1; );
    );
    urctx && click ? (
      u = urctx - 1;
      inbox(urcx, urcy, 190, 18) ? (                                              // Rename: the name dialog, this room
        rsave = 1; rsren = urctx; rmenu = 0; rst0 = time_precise();
        i = 0; loop(24, rsnb[i] = urb[u * URL + 2 + i]; i += 1; ); rscat = urb[u * URL + 1];
        rsnlen = 0; while ( rsnlen < 23 && rsnb[rsnlen] > 0 ? ( rsnlen += 1; 1; ) : 0; );
      ) : ( inbox(urcx, urcy + 18, 190, 18) ? ( urover(u); urflash = time_precise(); urflu = u; ) : (   // Overwrite: the current settings, same name
            inbox(urcx, urcy + 36, 190, 18) ? ( urdel(u); ); ); );
      urctx = 0; click = 0; menuused = 1;
    );
    click ? ( menuxhit(roommx, roommy, roommw, 1) ? ( rmenu = 0; ) : (
      // room page tabs (208 px wide at rpgx): 0 = first, 1 = second, 2 = scenes, 3 = free field
      rqlen <= 0 && inbox(rpgx, rmhy + 1, 208, 16) ? ( rmpg = mx < rpgx + 100 ? (mx < rpgx + 50 ? 0 : 1) : (mx < rpgx + 156 ? 2 : 3); ) : (
      !rmsc && !rmfp && inbox(rkc1, rky, 80, 14) ? ( vopt[16] = vopt[16] >= 0.5 ? 0 : 1; ) : (
      !rmsc && !rmfp && inbox(rkc2, rky, 50, 14) ? ( vopt[17] = vopt[17] >= 0.5 ? 0 : 1; ) : (
      !inbox(rsqx, rmhy, rsqw, 18) ? (                  // the search field: type to search
      inbox(rsvx, rmhy + 1, rsvw, 16) ? (                                               // Save room: the name dialog
        rsave = 1; rsren = 0; rmenu = 0; rst0 = time_precise(); rscat = 6;
        u = -1; i = 0; loop(URM, u < 0 && urmatch(i) ? u = i; i += 1; );           // the current room is a user room: its name
        u >= 0 ? ( i = 0; loop(24, rsnb[i] = urb[u * URL + 2 + i]; i += 1; ); rscat = urb[u * URL + 1]; ) : (
          sprintf(#rsn0, "My room %d", urcount() + 1); strtob(#rsn0, rsnb); );
        rsnlen = 0; while ( rsnlen < 23 && rsnb[rsnlen] > 0 ? ( rsnlen += 1; 1; ) : 0; );
      ) : (
        inbox(roommx, roommy, roommw, roommh) ? (
          uh = -1; u = 0; loop(URM, urX[u] != NOPOS && inbox(urX[u], urY[u], RMCW, roomih) ? uh = u; u += 1; );
          uh >= 0 ? (
            mx >= urX[uh] + RMCW - 20 ? ( urdel(uh); ) : ( urload(uh); snlast = 0; rmenu = 0; );   // x = delete (the menu stays), else load
          ) : (
            hk = -1; k = 0; loop(NPRE + 1, rmX[k] != NOPOS && inbox(rmX[k], rmY[k], RMCW, roomih) ? ( hk = k; ); k += 1; );
            sh = -1; k = 0; loop(NSC, scnX[k] != NOPOS && inbox(scnX[k], scnY[k], RMCW, roomih) ? ( sh = k; ); k += 1; );
            fh = -1; k = 0; loop(NFF, ffpmX[k] != NOPOS && inbox(ffpmX[k], ffpmY[k], RMCW, roomih) ? fh = k; k += 1; );
            fh >= 0 ? ( !isfree ? modeset(1); ffpapply(fh); snlast = 0; ) : (
            sh >= 0 ? ( isfree ? modeset(0); scnload(sh); snlast = 0; ) : (
            hk >= 0 && hk < NPRE ? ( isfree ? modeset(0); loadroom(hk); snlast = 0; ); ); );
            rmenu = 0;
          );
        ) : ( rmenu = 0; );
      ); ); ); ); ); );
      menuused = 1;
    );
    tabm && szoom ? ( mx = mxZs; my = myZs; );
  ) : (
    rpmenu ? (
      click ? ( mloc(rpmx, rpmy, rpmk); hk = !menuxhit(rpmx, rpmy, rpmw, 1) && inbox(rpmx, rpmy, rpmw, rpmh) ? floor((my - rpmy - 4) / 16) : -1; mrest(); hk >= 0 && hk < NRP ? rpapply(hk); rpmenu = 0; menuused = 1; click = 0; );
    ) :
    ffpmenu ? (
      click ? ( mloc(ffmx, ffmy, ffmk); hk = !menuxhit(ffmx, ffmy, ffmw, 1) && inbox(ffmx, ffmy, ffmw, ffmh) ? floor((my - ffmy - 4) / 16) : -1; mrest(); hk >= 0 && hk < NFF ? ffpapply(hk); ffpmenu = 0; menuused = 1; click = 0; );
    ) :
    mpmenu ? (
      click ? ( hk = !menuxhit(mpmx, mpmy, mpmw, 1) && inbox(mpmx, mpmy, mpmw, mpmh) ? floor((my - mpmy - 4) / 16) : -1; hk >= 0 && hk < NMP ? mpapply(hk); mpmenu = 0; menuused = 1; click = 0; );
    ) :
    smenu ? (
      click ? (
        mloc(srcmx, srcmy, srcmk); hko = !menuxhit(srcmx, srcmy, srcmw, 1) && inbox(srcmx, srcmy, srcmw, srcmh); hkp = mx >= srcmx + SMW1 + 2; hk = hkp ? spritem(my) : srcitem(my); mrest();
        smkeep = hko && hk < 0; hko && hkp ? ( hk >= 0 ? spapply(hk); hko = 0; );   // a preset (a heading or gap keeps the menu open)
        hko ? ( hk >= 0 ? ( snap_take(); slider35 = hk; slider37 = hk > 0 ? 50 : 0; i = 47; loop(13, sset(i, 0); i += 1; ); slider85 = 1; snap_notify(); ); );   // a new model starts unedited (and is heard)
        !smkeep ? ( smenu = 0; ); menuused = 1;
      );
    ) : (
      kmenu ? (
        click ? (
          mloc(kmx, kmy, kmk); hko = inbox(kmx, kmy, kmw, kmh); hk = floor((my - kmy - 4) / kmih); mrest();
          hko ? ( hk >= 0 && hk < 10 ? ( slider46 = bkord(hk); sauto1(46); ); );
          kmenu = 0; menuused = 1;
        );
      ) : (
      bmenu ? (
        click ? (
          mloc(bmx, bmy, bmk); hko = inbox(bmx, bmy, bmw, bmh); hk = floor((my - bmy - 4) / bmih); mrest();
          hko ? ( hk >= 0 && hk < 3 ? ( slider44 = bmatrw(floor(slider41 + 0.5), hk); sauto1(44); ); );
          bmenu = 0; menuused = 1;
        );
      ) : (
      emenu ? (
        click ? (
          mloc(emx, emy, emk); hko = inbox(emx, emy, emw, emh); hk = floor((my - emy - 4) / eih); mrest();
          hko ? (
            emenu == 4 ? ( hk >= 0 && hk < 4 ? ( slider115 = hk; sauto1(115); ); ) : ( emenu == 3 ? ( hk >= 0 && hk < 4 ? ( slider114 = hk; sauto1(114); ); ) : ( hk >= 0 && hk < 9 ? ( emenu == 1 ? ( slider58 = hk; sauto1(58); ) : ( slider59 = hk; sauto1(59); ); ); ) );
          );
          emenu = 0; menuused = 1;
        );
      ) : (
      wmenu ? (
        click ? (
          mloc(wmxx, wmy, wmk); hko = inbox(wmxx, wmy, wmw, wmh); hk = floor((my - wmy - 4) / wmih); mrest();
          hko ? (
            hk >= 0 && hk < wmn ? ( wmenu == 1 ? ( selw = hk; ) : ( setalpha(wmata(hk)); setdens(wmatd(hk)); ); );
          );
          wmenu = 0; menuused = 1;
        );
      ) : (
      fmenu ? (
        click ? (
          fmx = fmenu == 1 ? fmx1 : fmx2;
          mloc(fmx, fmy, fmk); hko = inbox(fmx, fmy, fmw, fmh); hk = floor((my - fmy - 4) / fih); mrest();
          hko ? (
            hk >= 0 && hk < 8 ? ( fmenu == 1 ? ( slider26 = hk; autom(33554432); ) : ( slider29 = hk; autom(268435456); ); );
          );
          fmenu = 0; menuused = 1;
        );
      ) : (
        (!szoom || tabm) && click && (tabm && szoom ? inboxN(roombx, roomby, roombw, roombh) : inbox(roombx, roomby, roombw, roombh)) ? ( rmenu = 1; menuused = 1; rmpg = isfree ? 3 : (scnlast >= 0 ? 2 : rsel >= NPG1); );   // opens on the page of the current room
        !zoomv && click && inbox(srcbx, srcby, srcbw, srcbh) ? ( smenu = 1; menuused = 1; );
        !zoomv && !basic && click && inbox(lcbx, fcby, fcbw, fcbh) ? ( fmenu = 1; menuused = 1; fmgeo(); );
        !zoomv && !basic && click && inbox(hcbx, fcby, fcbw, fcbh) ? ( fmenu = 2; menuused = 1; fmgeo(); );
        !zoomv && !basic && click && inbox(mdlx, mdly, 44, 14) ? ( helpon = !helpon; click = 0; menuused = 1; );
        !zoomv && click && slider41 >= 0.5 && inbox(bdx, bdy, bdw, bdh) ? ( bmenu = 1; menuused = 1; );
        !zoomv && !basic && click && inbox(eslx1, esly, eslw, eslh) ? ( emenu = 1; menuused = 1; );
        !zoomv && !basic && click && inbox(eslx2, esly, eslw, eslh) ? ( emenu = 2; menuused = 1; );
        !zoomv && !basic && click && fzsel > 0 && inbox(lnbx, lnby, lnbw, lnbh) ? ( emenu = 3; menuused = 1; );
        !zoomv && !basic && !isfree && click && inbox(wsx, wdy1, wdw, 15) ? ( wmenu = 1; menuused = 1; wmgeo(); );
        !zoomv && !isfree && click && inbox(wmx2, wdy2, wdw2, wdh) ? ( wmenu = 2; menuused = 1; wmgeo(); );
        // BACK PART: in the BARRIER section (Expert), in the room view only when it is enlarged
        click && bon && !isfree && zoomv && inbox(kbx, kby, kbw, 14) ? ( kmenu = 1; menuused = 1; kmsrc = 2; kmx = kbx; kmy = kby + 16; kmk = 1; );
        click && bon && !isfree && !zoomv && !basic && inbox(bkbx, bky, bdw, 15) ? ( kmenu = 1; menuused = 1; kmsrc = 1; kmx = bkbx; kmy = bky + 17; kmk = 1; );
      );
      );
      );
      );
      );
    );
  );
);
menuused ? ( click = 0; );
// MODULE CORNER: the lower right corner of a section = its width and height together. Expert: the width is the column's
// (the other column makes room), Basic: the card's (the other three share the rest). Double-click = standard size.
function rcgeo(id) (
  id >= 41 ? ( rcgx = id == 42 || id == 44 ? sxR + bwR : -99999; rcgy = id <= 42 ? yS1 + hbR : yO2 + hbO; ) : ( rzgeo(id); rcgx = rzgx + rzgw; rcgy = rzgy; );   // one column: no column widths
);
rch = 0; rchR = 0;
rzon0 = !learn && !zoomv && !szoom && !vlock && !snedit && !helpon && !(rmenu || smenu || fmenu || bmenu || kmenu || wmenu || emenu || xpon || dmenu || tpin);
rzon0 && !rzid && !rcid && !coldrag ? (
  k = 0;
  loop(basic ? 4 : 8,
    rid = basic ? 41 + k : (k == 0 ? 9 : (k == 1 ? 6 : (k == 2 ? 3 : (k == 3 ? 10 : (k == 4 ? 1 : (k == 5 ? 7 : (k == 6 ? 4 : 5)))))));
    !rch && rzok(basic ? 22 : rid) ? ( rcgeo(rid); mx >= rcgx - 11 && mx < rcgx + 2 && my >= rcgy - 11 && my < rcgy + 4 ? ( rch = rid; rchR = basic ? 22 : rid; ); );
    k += 1;
  );
);
// COLUMN DIVIDER (Expert): drag the gap between the columns = one wider, the other narrower; double-click = standard
coldv = !basic && !learn && !zoomv && !szoom && !vlock && !snedit;
colhit = coldv && !rch && mx >= R0 - 8 && mx < R0 + 2 && my >= TOPY && my < LH - 18;
click && colhit ? (
  time_precise() - coltc < 0.35 ? ( vopt[14] = 0; coldrag = 0; ) : ( coldrag = 1; );
  coltc = time_precise(); click = 0;
);
coldrag ? ( (mouse_cap & 1) && coldv ? ( vopt[14] = min(0.6, max(440 / colT, (colT - (mx - 3 - L0)) / colT)); ) : ( coldrag = 0; ); );   // 440 (bottom row of the room view) … 60 % (wider than the window allows: the page gets smaller)
// MODULE HEIGHT: drag the bottom edge of an open section (or of the room view) up / down; double-click = standard height
// (the room view: automatic again - it fills the window). Expert: every section on its own, Basic: the row of four.
rzon = !learn && !zoomv && !szoom && !vlock && !snedit && !helpon && !(rmenu || smenu || fmenu || bmenu || kmenu || wmenu || emenu || xpon || dmenu || tpin);
rzh = 0;
rzon && !colhit && !coldrag && !rzid ? (
  k = 0;
  loop(basic ? 2 : 8,
    rid = basic ? 21 + k : (k == 0 ? 9 : (k == 1 ? 6 : (k == 2 ? 3 : (k == 3 ? 10 : (k == 4 ? 1 : (k == 5 ? 7 : (k == 6 ? 4 : 5)))))));
    !rzh && rzok(rid) ? ( rzgeo(rid); mx >= rzgx + 4 && mx < rzgx + rzgw - 4 && my >= rzgy - 3 && my < rzgy + 5 ? ( rzh = rid; ); );
    k += 1;
  );
);
rch ? rzh = rchR;
click && rch ? (
  time_precise() - rztc < 0.35 && rzlast == rzh ? (
    basic ? ( vopt[41] = 0; ) : ( rch == 6 ? ( vopt[39] = 0; ) : ( vopt[14] = 0; ); );     // double-click: standard widths
    rcid = 0;
  ) : (
    rcid = rch; rcx0 = mouse_x; rcgs = gs;
    basic ? ( rcw0 = bcw; rccT = bTot; ) : ( rcw0 = rch == 6 ? flW : (rch == 9 || rch == 4 || rch == 5 || (rch == 7 && vhid) ? RWc : LW); rccT = rch == 6 ? LW : colT; );
  );
);
click && rzh ? (
  time_precise() - rztc < 0.35 && rzlast == rzh ? ( vopt[rzidx(rzh)] = 0; rzid = 0; ) : (
    rzid = rzh; rzy0 = mouse_y; rzgs = gs; rzv0 = vopt[rzidx(rzh)] + (rzh == 22 ? bfill : 0);
    (rzh == 9 || rzh == 21) && rzv0 <= 0 ? ( rzv0 = VH; );                  // the room view starts at the height it has now
    // a section under an automatic room view: the room view keeps its height, so the dragged edge follows the mouse
    !basic && (rzh == 4 || rzh == 5) && vop >= 1 && vopt[33] <= 0 ? ( vopt[33] = VH; );
  );
  rztc = time_precise(); rzlast = rzh; click = 0;
);
rzid ? (
  (mouse_cap & 1) && rzon ? (
    rzv = rzv0 + (mouse_y - rzy0) / rzgs / (rzid == 22 ? 2 : 1);   // Basic: the edge under two rows of cards
    rzid == 9 || rzid == 21 ? ( vopt[rzidx(rzid)] = min(1400, max(140, rzv)); ) : ( vopt[rzidx(rzid)] = min(400, max(0, rzv)); );
  ) : ( rzid = 0; );
);
rcid ? (
  (mouse_cap & 1) && rzon ? (
    rcdx = (mouse_x - rcx0) / rcgs;
    basic ? ( vopt[41] = min(rccT - 280, max(372, rcw0 + rcdx)) / rccT; ) : (      // Basic: the cards wider, the room view narrower
      rcid == 6 ? ( vopt[39] = min(rccT - 346, max(190, rcw0 + rcdx)) / rccT; ) : (   // Filter: wider, Source narrower
      rcRW = rcid == 9 || rcid == 4 || rcid == 5 || (rcid == 7 && vhid) ? rcw0 + rcdx : rccT - (rcw0 + rcdx);   // right column: wider to the right
      vopt[14] = min(0.6 * rccT, max(440, rcRW)) / rccT; );
    );
  ) : ( rcid = 0; );
);
curk = (rch || rcid || tch || tcd) ? 32642 : ((colhit || coldrag) ? 32644 : ((rzh || rzid) ? 32645 : 32512));
curk != lastcurk ? ( gfx_setcursor(curk); lastcurk = curk; );   // diagonal arrow (corner) / left-right (column divider) / up-down (edge) / normal arrow
(rmenu || smenu || fmenu || bmenu || kmenu || wmenu || emenu || tpin || xpon || dmenu) ? ( mouse_wheel = 0; );
vtab = min(3, max(0, floor(vopt[40] + 0.5))); (basic || learn) && vtab >= 2 ? vtab = 0;   // room view tab: 0 Plan (default), 1 3D, 2 Timeline, 3 RT60 (Expert)
planv = vtab == 0;                                                    // display only
hidew = !basic && vopt[10] >= 0.5;                                     // walls hidden in the room view (display only - the sound is untouched)
lockS = !basic && (vopt[9] & 1) > 0; lockL = !basic && (vopt[9] & 2) > 0; lockB = !basic && (vopt[9] & 4) > 0; lockE = !basic && (vopt[9] & 8) > 0;   // B and D / W were inside the comment (since their lock buttons did nothing) (a ball on its motion path can be dragged - that moves the whole path) // Basic: no locks // lock bits: 1 source, 2 listener, 4 barrier position, 8 door / window
prOn ? ( prstep(); );                                             // Reset positions: the glide (see posreset)
lkb = vopt[9];
cELK = floor(slider41 + 0.5) == 2 ? (vopt[4] >= 0.5 ? 0x6FB1F0 : 0x185FA5) : cBARV;   // lock color: window = blue (glass), door = the neutral barrier gray (no brown, no orange)
gsrs = min(4, max(0.25, slider31));   // geometry not yet computed for the sliders (a new preset, the audio thread
geostale = abs(roomW - max(0.8, slider2) * gsrs) > 0.001 || abs(roomD - max(0.8, slider3) * gsrs) > 0.001 || abs(roomH - max(0.8, slider4) * gsrs) > 0.001
        || (!isfree && (abs(sx - slider10 * gsrs) > 0.3 || abs(sy - slider11 * gsrs) > 0.3 || abs(lx - slider13 * gsrs) > 0.3 || abs(ly - slider14 * gsrs) > 0.3));   // or the positions // not yet through): no paths drawn from the old room
showpaths = basic || geostale ? 0 : vopt[1]; showdirect = basic || geostale ? 0 : vopt[2]; tipsOn = vopt[3]; tipShow = tipsOn || ((mouse_cap & 36) && !(mouse_cap & 1)); wavesOn = vopt[6] >= 0.5;   // Basic: tips and waves only
showinfo = showpaths || showdirect || tipsOn || wavesOn;   // master: any info visible (incl. waves)?
linksl = !basic && vopt[5] >= 0.5;
// DELAY values for the GUI (the same as in @block): times in ms (ms or note value x tempo), the tempo used
dpok = 0;   // the delay plate is placed by drawsource()
gdon = slider65 >= 0.5; gdpp = slider66 >= 0.5; gdtm = slider67 >= 0.5; gdlt = slider68 >= 0.5; gdle = slider69 >= 0.5; gdlv = slider86 >= 0.5; gdlf = slider87 >= 0.5; gdmL = slider93 >= 0.5; gdmR = slider94 >= 0.5;
gbpm = slider74 > 0.5 ? slider74 : (tempo > 1 ? tempo : 120);
gtL = min(4050, max(1, gdtm ? g_dnote(slider72) * 60000 / gbpm : slider70));
gtR = gdlt ? gtL : min(4050, max(1, gdtm ? g_dnote(slider73) * 60000 / gbpm : slider71));
actcol = cTEXT; kcol = cTEXT;

// ---- redraw decision: full frame only when something besides the meters can have changed ----
ukey = 0; i = 1; loop(NSL, ukey += slider(i) * (i * 7919 + 1); i += 1; );
soloT = time_precise();                  // heartbeat: a solo lasts only while this window is drawn
ukey += soloM * 4201 + selw * 53.3 + mouse_x * 3.1 + mouse_y * 5.3 + mouse_cap * 1000.7 + gfx_w * 7.7 + gfx_h * 11.3 + zoomv * 31 + saveArm * 37;
i = 1; loop(7, ukey += vopt[i] * (101 + i * 13); i += 1; ); ukey += vopt[14] * 7717 + vopt[15] * 1229 + snlast * 17 + vopt[9] * 977 + vopt[10] * 1039 + vopt[11] * 1103 + vopt[12] * 1171 + vopt[13] * 1187 + scy * 3.7 + szoom * 1213;
i = 0; loop(NSNAP, ukey += uslot[i * USL] * (211 + i); i += 1; );
ri = RFB; loop(15, ukey += vopt[ri] * (4501.7 + ri * 31.9); ri += 1; ); ukey += rflSel * 4517.3 + drag * 13.1;   // (all 15 reflector values - the levels and the off flag too)
ukey += vopt[40] * 4391.3 + sidev * 4397.1 + zmz * 4409.1 + vgone * 4423.7 + vopt[50] * 4349.3 + vopt[51] * 4363.9 + inStereo * 4357.7 + vopt[49] * 4327.1 + geostale * 4231.7 + vopt[36] * 4261.1 + vopt[35] * 4241.3 + (tch || tcd) * 4253.9 + srAx * 4111 + srAy * 4127 + srAz * 4133 + vopt[18] * 4139 + vopt[20] * 4153 + tempo * 4157 + lp2need * 4159 + lp3need * 4177 + vopt[44] * 4201.9 + vopt[45] * 4421.3 + vopt[46] * 4423.7 + vopt[47] * 0.731 + vopt[48] * 4447.9 + vzf * 4493.7 + vzox * 0.913 + vzoy * 0.917 + xpn * 4457.3 + xpon * 4463.1 + snctx * 4219.3 + (time_precise() - snflash < 0.7) * 4229.7 + snconf * 4447.3 + (time_precise() - snconfT < 4) * 4451.9 + hlpon * 4231.1 + hlpH * 0.37;   // Learn: a panel that measured a new height draws once more   // srAx / srAy / srAz: the source axis comes from the geometry (audio thread) - redraw when it turns
i = 21; loop(14, ukey += vopt[i] * (3001 + i * 7); i += 1; ); ukey += rzh * 3301 + rzid * 3307;   // module heights (vopt[21 … 34]) and the resize drags
gtnow = time_precise(); gfxgap = gtnow - lastgfx; lastgfx = gtnow;   // window reopened / un-hidden -> new, empty framebuffer
// (open menus and panels only redraw when something changes - their state is part of ukey; the tempo panel animates
// only while it taps or listens)
ukey += rqlen * 97.3 + rsnlen * 89.1 + rscat * 83.3 + rsave * 4337.1 + urcount() * 79.7 + rmenu * 4201.3 + smenu * 4211.7 + fmenu * 4219.1 + bmenu * 4229.3 + kmenu * 4231.9 + wmenu * 4241.3 + emenu * 4243.7 + helpon * 4253.1 + tpon * 4259.9 + xpon * 4261.3 + dmenu * 4271.7 + tptg * 4273.1 + tnsel * 4283.3 + anaSel * 4289.9 + anaDone * 4297.1 + soloM * 7.7;
dofull = mtRec || (mtE > 0 && time_precise() - lastblk < 0.3) || ((szoom == 7 || szoom == 6) && time_precise() - lastblk < 0.3) || tbdrag || fhdrag || prOn || vedit || vedPend || kovl || kovlw || hlpon || hlpw || snctx || snctxw || (abOn && abCur == 1 && abMatch && (floor(time_precise() * 4) != abfr ? ( abfr = floor(time_precise() * 4); 1; ) : 0)) || time_precise() - wcT < 1 || ukey != lastukey || fullcnt <= 0 || gfxgap > 0.25 || lasttip || (mouse_cap & 4) || (lastcap & 4) || mouse_wheel != 0 || (tpon && (anaOn || anaCmd != anaSeen || time_precise() - tapLast < 2.2)) || snedit || snPend || shpin || shlast || kdrag || eqdrag || wbdrag || animing || sbdrag || (mouse_cap & 1) || (wavesOn && (time_precise() - wlast < wlife + 0.2 || dbs(max(pkIL, pkIR)) > -59 && time_precise() - lastblk < 0.3));   // waves: animate only while fronts are alive
dofull ? ( lastukey = ukey; fullcnt = 15; ) : ( fullcnt -= 1; );
vedSeen = 0;                              // set again by the control that owns an open value box
// ---- theme: dark (the default) or light, both in the gray design (block below); black & white (Expert) has its own palette ----
// (only the wall lift of dark mode is set here - the palettes follow below)
cCTLon = 0; darkmode = vopt[4] >= 0.5; monoOn = !basic && vopt[7] >= 0.5;   // black & white: Expert only
wlift = darkmode ? 60 : 0;   // dark mode: the wall shading of the room view a little lighter
// black & white: own high-contrast palette (pure black / white, light gray sections) instead of grayed colors
monoOn ? (
  darkmode ? (
    cBG = 0x000000; cSURF = 0x161616; cBORD = 0x8A8A8A; cTEXT = 0xFFFFFF; cMUTED = 0xC8C8C8; cLINE = 0x2E2E2E; cHAIR = 0x3A3A3A;
    cDIM = 0x555555; cDIM2 = 0x6A6A6A; cINK = 0xFFFFFF; cLIS = 0xFFFFFF; cSRC = 0xFFFFFF; cVIEWT = 0x222222; mtit = 0xFFFFFF;
  ) : (
    cBG = 0xFFFFFF; cSURF = 0xFFFFFF; cBORD = 0x8C8C8C; cTEXT = 0x000000; cMUTED = 0x3A3A3A; cLINE = 0xD6D6D6; cHAIR = 0xD0D0D0;
    cDIM = 0xBDBDBD; cDIM2 = 0xA0A0A0; cINK = 0x000000; cLIS = 0x000000; cSRC = 0x000000; cVIEWT = 0xE4E4E4; mtit = 0x000000;
  );
  cRMTT = cBARTT = cWALTT = cSRTT = cIOTT = cRVTT = cFLTT = cDLTT = cMOTT = cFFTT = mtit; cSTAGE = cSURF;
  cACT = cTEXT; cACTX = cBG; cWALLV = cTEXT;
);
// DESIGN: gray controls, colored signals. Knobs, sliders, buttons and chips stay gray; only what moves (curves, meters, waves,
// paths, balls) carries color: blue (room, filter, motion ...) and green (source). A chosen mode / tab = soft blue fill + blue outline.
!monoOn ? (
  darkmode ? (
  cBG = 0x202125; cSURF = 0x303136; cBORD = 0x72737B; cTEXT = 0xFFFFFF; cMUTED = 0xC6C7CD; cLINE = 0x3A3B41; cHAIR = 0x55565D;
  cDIM = 0x7C7D85; cDIM2 = 0xA2A3AA; cSTAGE = 0x18191C; cVIEWT = 0x3D3E44; cINK = 0xFFFFFF;
  cRMTT = cBARTT = cWALTT = cIOTT = cRVTT = cFLTT = cDLTT = cMOTT = 0x7FBEFF;
  cSRTT = cFFTT = 0x80E2AC;
  cACT = 0x2F5382; cACTX = 0xEAF3FF; cACTO = 0x7FBEFF; cSRC = 0x9DEBC0; cLIS = 0xFFFFFF; cWALLV = 0xC6C7CD;
  cCTL = 0xE4E5E9; cCTLon = 2;
  ) : (
  cBG = 0xE6E6E8; cSURF = 0xF8F8F9; cBORD = 0xBABBC0; cTEXT = 0x2B2E33; cMUTED = 0x62666D; cLINE = 0xEAEAEC; cHAIR = 0xCDCED2;
  cDIM = 0xA2A4A9; cDIM2 = 0x82848A; cSTAGE = 0xF8F8F9; cVIEWT = 0xDFDFE2; cINK = 0x2B2E33;
  cRMTT = cBARTT = cWALTT = cIOTT = cRVTT = cFLTT = cDLTT = cMOTT = 0x4C93DC;
  cSRTT = cFFTT = 0x3FA874;
  cACT = 0xD3E5F8; cACTX = 0x1A4A86; cACTO = 0x4C93DC; cSRC = 0x3FA874; cLIS = 0x3A3E46; cWALLV = 0x585C64;
  cCTL = 0x565A62; cCTLon = 1;
  );
);
cBARV = darkmode ? 0xC9D1DB : 0x5A5F67; cCRV = darkmode ? 0xA7B4C4 : 0x6A7580; cAMB = darkmode ? 0xB5A9E0 : 0x8D80BB; cWARN = darkmode ? 0xD9C66A : 0xB59B2A;
cGLS = monoOn ? cTEXT : (darkmode ? 0x6FB1F0 : 0x185FA5);
cDLS = monoOn ? cTEXT : (darkmode ? 0xE59BD0 : 0xB9529A);   // the delay echoes as a signal (Timeline, waves, plate) - magenta, apart from the blue early reflections // glass (window), the direct sound line, the wave ring - a light blue on the dark stage (the dark blue had no contrast there)
// ---- the full frame, split into functions (each compiled on its own: Apple Silicon branch range) ----
// the path in the room view (plan and 3D): dashed in the motion color, a faint trail behind the moving ball, a sign with Stop
// GUI copy of amtpt (the threads never share a function): ph 0 … 1 -> gmU, gmV
function gmtpt(p, ph) local(q, j0, j1, fr)
(
  ph -= floor(ph); gmW = 0;
  // Square: the perimeter of the square -1 … 1 in 8 equal steps from (1, 0); Wave: u = sin(2 pi ph), v = 0.35 sin(2 pi u)
  p == 5 ? ( q = ph * 8;
    q < 1 ? ( gmU = 1; gmV = q; ) : ( q < 3 ? ( gmU = 2 - q; gmV = 1; ) : ( q < 5 ? ( gmU = -1; gmV = 4 - q; ) : ( q < 7 ? ( gmU = q - 6; gmV = -1; ) : ( gmU = 1; gmV = q - 8; ); ); ); );
  ) : ( p == 6 ? ( gmU = sin(2 * $pi * ph); gmV = 0.35 * sin(2 * $pi * gmU); ) : (
    // Drawn path: MTDP holds 256 (u, v) pairs at [0 … 511], [512] >= 256 = a path is stored, 256 heights (w) from [514]; linear interpolation between the points
    MTDP[512] >= 256 ? ( q = ph * 256; j0 = floor(q); fr = q - j0; j0 = j0 % 256; j1 = (j0 + 1) % 256;
      gmU = MTDP[2 * j0] + (MTDP[2 * j1] - MTDP[2 * j0]) * fr; gmV = MTDP[2 * j0 + 1] + (MTDP[2 * j1 + 1] - MTDP[2 * j0 + 1]) * fr; gmW = MTDP[514 + j0] + (MTDP[514 + j1] - MTDP[514 + j0]) * fr; ) : ( gmU = cos(2 * $pi * ph); gmV = sin(2 * $pi * ph); );
  ); );
);
// the outline of a Square / Wave / Drawn path around (cx, cy) in meters: short dashes like the circle
function mtshape(p, cx, cy, cz, r) local(k, x1, y1, n)
(
  n = p == 7 ? 256 : 64; k = 0;
  loop(n,
    gmtpt(p, k / n); PJ(min(max(cx + r * gmU, 0), roomW), min(max(cy + r * gmV, 0), roomD), min(max(cz + r * gmW, 0), roomH)); x1 = qx; y1 = qy;
    gmtpt(p, (k + 0.6) / n); PJ(min(max(cx + r * gmU, 0), roomW), min(max(cy + r * gmV, 0), roomD), min(max(cz + r * gmW, 0), roomH)); LNW(x1, y1, qx, qy, 1);
    k += 1;
  );
);
// SIZE HANDLE of the motion path: a small square on the outer edge of the pattern - drag = scale the whole path
// (Size) from its center; mid = 1: (cx, cy) is the middle of a pass-by line, 0: its start (the ghost)
function nearmth() ( mtHv && sqr(mx - mtHsx) + sqr(my - mtHsy) < 144; );   // the mouse is on the size handle - the walls do not react
// Draws the Motion path size handle for path type p at center (cx,cy,cz), radius r; sets mtHsx/mtHsy/mtHv and avoids covering the balls
function mthandle(p, cx, cy, cz, r, sg, mid) local(hx, hy, hz, k, km, ux, uy, hit, mhx)
(
  hx = cx + r; hy = cy; hz = cz;
  p == 0 ? ( k = 2 * $pi * slider105 / 360 * (slider106 >= 0.5 ? -1 : 1) + $pi; hx = cx + r * cos(k); hy = cy + r * sin(k); );   // opposite the start mark
  p == 5 ? ( hx = cx - r; hy = cy - r; );
  p == 2 ? ( hx = cx; hy = cy + r; );
  p == 3 ? ( hx = cx; hz = cz + r; );
  p == 4 ? ( ux = sg * cos(slider105 * $pi / 180) * (slider106 >= 0.5 ? -1 : 1); uy = sg * sin(slider105 * $pi / 180) * (slider106 >= 0.5 ? -1 : 1);
             hx = cx + (mid ? 1 : 2) * r * ux; hy = cy + (mid ? 1 : 2) * r * uy; cx -= mid ? r * ux : 0; cy -= mid ? r * uy : 0; );   // the anchor of a pass-by = its start A
  p == 7 && MTDP[512] >= 256 ? ( km = 0; k = 0; loop(256, MTDP[2 * k] > MTDP[2 * km] ? km = k; k += 1; ); hx = cx + r * MTDP[2 * km]; hy = cy + r * MTDP[2 * km + 1]; hz = cz + r * MTDP[514 + km]; );
  r > 0.05 ? (
    PJ(cx, cy, cz); mtHcx = qx; mtHcy = qy;
    k = 16 / max(1, pS);   // the handle keeps 16 px from the walls (they would catch the mouse)
// the handle never sits on a ball (it took the grab away: the source / listener could not be moved) - then it turns 90 degrees around the center
    PJ(sx, sy, sz); mhbx = qx; mhby = qy; PJ(lx, ly, lz); mhlx = qx; mhly = qy;
    PJ(min(max(hx, k), roomW - k), min(max(hy, k), roomD - k), min(max(hz, 0.15), roomH - 0.15));
    sqr(qx - mhbx) + sqr(qy - mhby) < 400 || sqr(qx - mhlx) + sqr(qy - mhly) < 400 ? (
      mhx = hx; hx = cx - (hy - cy); hy = cy + (mhx - cx);
      PJ(min(max(hx, k), roomW - k), min(max(hy, k), roomD - k), min(max(hz, 0.15), roomH - 0.15));
      sqr(qx - mhbx) + sqr(qy - mhby) < 400 || sqr(qx - mhlx) + sqr(qy - mhly) < 400 ? (
        hx = 2 * cx - hx; hy = 2 * cy - hy;
        PJ(min(max(hx, k), roomW - k), min(max(hy, k), roomD - k), min(max(hz, 0.15), roomH - 0.15));
      );
    );
    mtHsx = qx; mtHsy = qy; mtHv = 1;
    hit = sqr(mx - qx) + sqr(my - qy) < 64 || drag == 21;
    col(cSURF, 1); R(qx - 3.5, qy - 3.5, 7, 7); col(cMOTT, 1); LNBOX(qx - 3.5, qy - 3.5, 7, 7, hit ? 1.6 : 1);
    hit && drag != 21 ? tipbox(qx - 5, qy - 5, 10, 10, "Size of the path: drag = scale it from its center (Size). Drag a moving ball = move the whole path");
  );
);
// START MARK: a thin tick across the path where the motion begins (Start; a pass-by: A) - moves with the Start knob
function mtstartmark(p, cx, cy, cz, r, sg, mid, thoff) local(t0, t1, x0, y0, z0, x1, y1, z1, ux, uy, ax, ay, l, a, tx, ty)
(
  r > 0.05 ? (
    t0 = 2 * $pi * slider105 / 360 * (slider106 >= 0.5 ? -1 : 1) + thoff; t1 = t0 + 0.05 * (slider106 >= 0.5 ? -1 : 1);
    p == 4 ? ( ux = sg * cos(slider105 * $pi / 180) * (slider106 >= 0.5 ? -1 : 1); uy = sg * sin(slider105 * $pi / 180) * (slider106 >= 0.5 ? -1 : 1);
               x0 = mid ? cx - r * ux : cx; y0 = mid ? cy - r * uy : cy; z0 = cz; x1 = x0 + 0.05 * ux; y1 = y0 + 0.05 * uy; z1 = cz; ) : (
    p >= 5 ? ( gmtpt(p, t0 / (2 * $pi)); x0 = cx + r * gmU; y0 = cy + r * gmV; z0 = cz + r * gmW; gmtpt(p, t1 / (2 * $pi)); x1 = cx + r * gmU; y1 = cy + r * gmV; z1 = cz + r * gmW; ) : (
    p == 0 ? ( x0 = cx + r * cos(t0); y0 = cy + r * sin(t0); x1 = cx + r * cos(t1); y1 = cy + r * sin(t1); z0 = z1 = cz; ) : (
    p == 1 ? ( x0 = cx + r * sin(t0); x1 = cx + r * sin(t0) + 0.05; y0 = y1 = cy; z0 = z1 = cz; ) : (
    p == 2 ? ( y0 = cy + r * sin(t0); y1 = y0 + 0.05; x0 = x1 = cx; z0 = z1 = cz; ) : (
             z0 = cz + r * sin(t0); z1 = z0 + 0.05; x0 = x1 = cx; y0 = y1 = cy; )))));
    PJ(min(max(x0, 0), roomW), min(max(y0, 0), roomD), min(max(z0, 0), roomH)); ax = qx; ay = qy;
    PJ(min(max(x1, 0), roomW), min(max(y1, 0), roomD), min(max(z1, 0), roomH)); ux = qx - ax; uy = qy - ay; l = sqrt(ux * ux + uy * uy);
    l > 0.0001 ? ( ux /= l; uy /= l; ) : ( ux = 1; uy = 0; );
    col(cMOTT, 1); a = gfx_a; LNW(ax - uy * 7, ay + ux * 7, ax + uy * 7, ay - ux * 7, 1);
// the label never sits on the size handle (Both moving in a circle around an Own spot: the handle lay right on the second path's "start")
    tx = ax + uy * 10 + 2; ty = ay - ux * 10;
// nor on the room's depth label (right of the room) - then it goes to the other side of the tick
    !isfree && tx < dmdx + dmdw + 2 && tx + 26 > dmdx - 2 && abs(ty - dmdy) < 12 ? ( tx = ax - uy * 10 - 30; );
    PJ(roomW, roomD / 2, 0); tx + 25 > qx - 1 ? tx = ax - 32;   // the label stays inside the room / area (the view cuts it at the wall) - else it goes left of the tick
    PJ(0, roomD / 2, 0); tx < qx + 1 ? tx = ax + 6;
    // (the size handle last: it decides over the side the label took)
    mtHv && tx < mtHsx + 7 && tx + 26 > mtHsx - 7 && abs(ty - mtHsy) < 11 ? ( ty += ty >= mtHsy ? 12 - (ty - mtHsy) : -12 + (mtHsy - ty); );
    col(cMUTED, 1); gfx_setfont(7); txt("start", tx, ty, 0);
  );
);
// Draws the Motion path preview of path type slider102 for who (1 source, 2 listener) in the room view, with size handle and start mark
function mtpath(who) local(cx, cy, cz, r, k, a, x1, y1, x2, y2, p, th0, sg, ux, uy, ax, ay)
(
  p = floor(slider102 + 0.5); r = slider104 * rs * mtE; sg = who == 1 ? 1 : (mtM == 2 ? -1 : 1);
  who == 1 ? ( cx = mtSX * rs; cy = mtSY * rs; cz = mtSZ; th0 = mtTh; ) : ( cx = mtLX * rs; cy = mtLY * rs; cz = mtLZ; th0 = mtM == 2 ? mtTh + $pi : mtTh; );   // (the pattern position: slider units x SIZE)
  p >= 5 ? ( gmtpt(p, th0 / (2 * $pi)); cx -= r * gmU; cy -= r * gmV; cz -= r * gmW; ) :
  p == 0 ? ( cx -= r * cos(th0); cy -= r * sin(th0); ) : ( p == 1 ? ( cx -= r * sin(th0); ) : ( p == 2 ? ( cy -= r * sin(th0); ) : ( p == 4 ? ( r = mtPRh * rs * mtE; cx -= sg * mtPU * rs * mtE * mtDx; cy -= sg * mtPU * rs * mtE * mtDy; ) : ( cz -= r * sin(th0); ); ); ); );   // the cut line (mtPRh)
  col(cMOTT, 0.8);
  !basic && !learn ? ( mthandle(p, cx, cy, cz, p == 4 ? r : slider104 * rs * mtE, sg, 1); mtstartmark(p, cx, cy, cz, p == 4 ? r : slider104 * rs * mtE, sg, 1, who == 2 && mtM == 2 ? $pi : 0); );
  col(cMOTT, 0.8);
  p >= 5 ? mtshape(p, cx, cy, cz, r) :
  p == 0 ? (
    k = 0;
    loop(36,
      a = k * 2 * $pi / 36; PJ(min(max(cx + r * cos(a), 0), roomW), min(max(cy + r * sin(a), 0), roomD), cz); x1 = qx; y1 = qy;
      a += 2 * $pi / 36 * 0.6; PJ(min(max(cx + r * cos(a), 0), roomW), min(max(cy + r * sin(a), 0), roomD), cz); LNW(x1, y1, qx, qy, 1);
      k += 1;
    );
  ) : (
    p == 1 ? ( PJ(max(0, cx - r), cy, cz); x1 = qx; y1 = qy; PJ(min(roomW, cx + r), cy, cz); ) : (
    p == 2 ? ( PJ(cx, max(0, cy - r), cz); x1 = qx; y1 = qy; PJ(cx, min(roomD, cy + r), cz); ) : (
    p == 4 ? (
      // the pass-by line A -> B (direction = Start angle, CCW = the other way), an arrow head at B
      ux = sg * mtDx * (slider106 >= 0.5 ? -1 : 1); uy = sg * mtDy * (slider106 >= 0.5 ? -1 : 1);
      PJ(min(max(cx - r * ux, 0), roomW), min(max(cy - r * uy, 0), roomD), cz); x1 = qx; y1 = qy;
      PJ(min(max(cx + r * ux, 0), roomW), min(max(cy + r * uy, 0), roomD), cz);
      ax = qx - x1; ay = qy - y1; a = sqrt(ax * ax + ay * ay); a > 1 ? ( ax /= a; ay /= a; LNW(qx, qy, qx - 9 * ax + 4 * ay, qy - 9 * ay - 4 * ax, 1.2); LNW(qx, qy, qx - 9 * ax - 4 * ay, qy - 9 * ay + 4 * ax, 1.2); );
    ) : (
             PJ(cx, cy, max(0.1, cz - r)); x1 = qx; y1 = qy; PJ(cx, cy, min(roomH - 0.1, cz + r)); )));
    DL(x1, y1, qx, qy, 4, 3, 1);
  );
  // the trail: three faint balls where it was a moment ago
  k = 1;
  loop(3,
    a = th0 - k * 0.22 * (slider106 >= 0.5 ? -1 : 1);
    p >= 5 ? ( gmtpt(p, a / (2 * $pi)); PJ(min(max(cx + r * gmU, 0.05), roomW - 0.05), min(max(cy + r * gmV, 0.05), roomD - 0.05), min(max(cz + r * gmW, 0.1), roomH - 0.1)); ) :
    p == 0 ? PJ(min(max(cx + r * cos(a), 0.05), roomW - 0.05), min(max(cy + r * sin(a), 0.05), roomD - 0.05), cz) : (
    p == 1 ? PJ(min(max(cx + r * sin(a), 0.05), roomW - 0.05), cy, cz) : (
    p == 2 ? PJ(cx, min(max(cy + r * sin(a), 0.05), roomD - 0.05), cz) : (
    p == 4 ? ( a = sg * min(r, max(-r, (mtPU - k * 0.12 * slider104 * (slider106 >= 0.5 ? -1 : 1)) * rs * mtE)); PJ(min(max(cx + a * mtDx, 0.05), roomW - 0.05), min(max(cy + a * mtDy, 0.05), roomD - 0.05), cz); ) :
             PJ(cx, cy, min(max(cz + r * sin(a), 0.1), roomH - 0.1)); ); ); );
    col(who == 1 ? cSRC : cLIS, 0.3 - k * 0.08); CIF(qx, qy, 10 - k * 1.5, 1);
    k += 1;
  );
);
// GHOST PATH: while Motion is off and its module is in view, the path it WOULD take - from the balls' present places,
// the pattern, Around, Size and Start (faint, no trail): what you set is what you see before you switch it on
// GUI copies of amtext / amfit (the full-size fit - the preview shows where the motion will really run)
function gmtext(p) local(k)
(
  gmX0 = gmY0 = -1; gmX1 = gmY1 = 1; gmZ0 = gmZ1 = 0;
  p == 1 ? ( gmY0 = gmY1 = 0; ); p == 2 ? ( gmX0 = gmX1 = 0; ); p == 3 ? ( gmX0 = gmX1 = gmY0 = gmY1 = 0; gmZ0 = -1; gmZ1 = 1; );
  p == 6 ? ( gmY0 = -0.35; gmY1 = 0.35; );
  p == 7 && MTDP[512] >= 256 ? ( gmX0 = gmY0 = gmZ0 = 1; gmX1 = gmY1 = gmZ1 = -1; k = 0;
    loop(256, gmX0 = min(gmX0, MTDP[2 * k]); gmX1 = max(gmX1, MTDP[2 * k]); gmY0 = min(gmY0, MTDP[2 * k + 1]); gmY1 = max(gmY1, MTDP[2 * k + 1]); gmZ0 = min(gmZ0, MTDP[514 + k]); gmZ1 = max(gmZ1, MTDP[514 + k]); k += 1; ); );
);
// Returns center c clamped so a shape with offsets lo..hi fits inside width w with margin mg; centered if it cannot fit
function gmfit(c, lo, hi, w, mg) local(a, b) ( a = mg - lo; b = w - mg - hi; a <= b ? min(max(c, a), b) : (a + b) / 2; );
// OWN SPOT (data in MTAP; not drawn by ffmark below): a point of the user's own in the room (the center of a Circle / Square when Around = Own spot). Not set yet = the
// ball's own place (as before). The marker sits on the floor; drag = place it, "Reset" = back to the ball.
// FREE FIELD SIZE: the free field has no walls to pull - a handle on the far corner of the floor scales the whole area and the
// positions in it (the same as the Size knob in Room); drag it away from the front corner = larger
function ffmark() local(hit)
(
  ffHv = 0;
  isfree && !learn ? (
    PJ(0, 0, 0); ffAx = qx; ffAy = qy; PJ(roomW, roomD, 0); ffHx = qx; ffHy = qy; ffHv = 1;
    hit = sqr(mx - ffHx) + sqr(my - ffHy) < 100 || drag == 23;
    col(cSURF, 1); R(ffHx - 4, ffHy - 4, 8, 8); col(cTEXT, hit ? 1 : 0.6); LNBOX(ffHx - 4, ffHy - 4, 8, 8, hit ? 1.6 : 1);
    LNW(ffHx - 1.5, ffHy + 1.5, ffHx + 1.5, ffHy - 1.5, 1);
    hit && drag != 23 ? tipbox(ffHx - 8, ffHy - 8, 16, 16, "Scale the free field like a room: drag = wider / deeper (right / down), Ctrl/Cmd = in proportion, Alt = height, Shift = fine – source, listener and reflectors move with it");
  );
);
// REFLECTORS in the view: a slate line (Plan) or plane (3D) per reflector, a handle at its center (drag = move X, Y)
// reflector panels in 3D: filled, with a material texture (facade windows, glass streaks, rock strata, fence pickets, hedge leaves)
// Draws a hill silhouette (a half ellipse in 18 vertical slices): center cx, half-width w, height h, standing on the horizon line hzy, clipped to skx0 … skx1
function hillf(cx, w, h, c, al) local(i, u0, u1, x0, x1, h0, h1)
(
  col(c, al); i = 0;
  loop(18,
    u0 = -1 + i / 9; u1 = -1 + (i + 1) / 9; x0 = max(skx0, cx + w * u0); x1 = min(skx1, cx + w * u1 + 0.8);
    h0 = h * sqrt(max(0, 1 - u0 * u0)) ; h1 = h * sqrt(max(0, 1 - u1 * u1));
    x1 > x0 ? POLY4(x0, hzy, x1, hzy, x1, hzy - h1, x0, hzy - h0);
    i += 1;
  );
);
// Draws a quad on the current reflector panel between panel coordinates u0..u1 (-1..1 along length) and v0..v1 (0..1 height) via PJ
function pnq(u0, u1, v0, v1) local(a0, b0, a1, b1, a2, b2, a3, b3)
(
  PJ(pnC + pnUx * pnHl * u0, pnCy + pnUy * pnHl * u0, pnH * v0); a0 = qx; b0 = qy;
  PJ(pnC + pnUx * pnHl * u1, pnCy + pnUy * pnHl * u1, pnH * v0); a1 = qx; b1 = qy;
  PJ(pnC + pnUx * pnHl * u1, pnCy + pnUy * pnHl * u1, pnH * v1); a2 = qx; b2 = qy;
  PJ(pnC + pnUx * pnHl * u0, pnCy + pnUy * pnHl * u0, pnH * v1); a3 = qx; b3 = qy;
  sidevOn ? RR(min(a0, a1), min(b0, b3), abs(a1 - a0) + 0.6, abs(b3 - b0) + 0.6, 0.5) : POLY4(a0, b0, a1, b1, a2, b2, a3, b3);   // side / front view: a plain rectangle (no triangle seam)
);
// Draws a line of width w on the current reflector panel from (u0,v0) to (u1,v1) in panel coordinates via PJ
function pnl(u0, v0, u1, v1, w) local(a0, b0)
(
  PJ(pnC + pnUx * pnHl * u0, pnCy + pnUy * pnHl * u0, pnH * v0); a0 = qx; b0 = qy;
  PJ(pnC + pnUx * pnHl * u1, pnCy + pnUy * pnHl * u1, pnH * v1); LNW(a0, b0, qx, qy, w);
);
// Draws the surface texture of reflector panel material m (1 facade windows, 2 glass, 3 rock strata, 4 fence pickets, 5 hedge leaves) using pnq and pnl
function pntex(m) local(i, j, nc, nr, u, v)
(
  m < 1.5 ? (
    nc = min(24, max(2, floor(pnHl * 2 / 3.2))); nr = min(8, max(1, floor(pnH / 3)));
    col(darkmode ? 0xB8C2CF : 0x7F8EA6, 1);   // opaque: two overlapping half-transparent triangles would show a seam
    i = 0; loop(nc, j = 0; loop(nr, pnq(-1 + (i + 0.2) * 2 / nc, -1 + (i + 0.8) * 2 / nc, (j + 0.25) / nr, (j + 0.8) / nr); j += 1; ); i += 1; );
  ) : ( m < 2.5 ? (
    col(0xFFFFFF, 0.34); pnl(-0.85, 0, -0.5, 1, 7); pnl(-0.2, 0, 0.15, 1, 4); pnl(0.45, 0, 0.8, 1, 8);
    nc = min(20, max(2, floor(pnHl * 2 / 2.5)));
    col(0x2F4C5C, 0.3); i = 1; loop(nc - 1, pnl(-1 + i * 2 / nc, 0, -1 + i * 2 / nc, 1, 1); i += 1; );
    pnl(-1, 0.5, 1, 0.5, 1);
  ) : ( m < 3.5 ? (
    col(0x3E3A33, 0.32); j = 1;
    loop(5, i = 0; loop(7, u = -1 + i * 2 / 7; pnl(u, j / 6 + ((i * 5 + j * 3) % 4 - 1.5) * 0.025, u + 2 / 7, j / 6 + (((i + 1) * 5 + j * 3) % 4 - 1.5) * 0.025, 1.6); i += 1; ); j += 1; );
    col(0xFFFFFF, 0.1); pnq(-0.9, -0.5, 0.1, 0.4); pnq(0.1, 0.6, 0.55, 0.85);
  ) : ( m < 4.5 ? (
    nc = min(40, max(3, floor(pnHl * 2 / 0.6)));
    col(0x3C3C44, 0.8); i = 0; loop(nc + 1, u = -1 + i * 2 / nc; pnl(u, 0, u, 1, 1.3); i += 1; );
    pnl(-1, 0.25, 1, 0.25, 2); pnl(-1, 0.7, 1, 0.7, 2);
  ) : (
    // 60 leaf dots at fixed pseudo-random places (prime multipliers), alternating dark / light green
    i = 0;
    loop(60,
      u = -1 + 2 * ((i * 7919) % 997) / 997; v = 0.04 + 0.92 * ((i * 104729) % 991) / 991;
      col(i % 2 ? 0x2F6B3E : 0x9CCB86, 0.55); PJ(pnC + pnUx * pnHl * u, pnCy + pnUy * pnHl * u, pnH * v); CIF(qx, qy, 2 + (i % 3), 1);
      i += 1;
    );
  ) ) ) );
);
// distance of point (px, py) to the segment (x0, y0)-(x1, y1), screen units
function segd(px, py, x0, y0, x1, y1) local(dx, dy, t)
(
  dx = x1 - x0; dy = y1 - y0; t = dx * dx + dy * dy > 0.000001 ? min(1, max(0, ((px - x0) * dx + (py - y0) * dy) / (dx * dx + dy * dy))) : 0;
  sqrt(sqr(px - x0 - t * dx) + sqr(py - y0 - t * dy));
);
// the mouse is on a scale handle of reflector r (length at the end of the foot line, height at the top edge in 3D)
function rflhh(r) ( (rflLHv[r] && sqr(mx - rflLHx[r]) + sqr(my - rflLHy[r]) < 64) || (rflHHv[r] && sqr(mx - rflHHx[r]) + sqr(my - rflHHy[r]) < 64); );
// the mouse is on reflector r (its handle, 12 px, or anywhere along its foot line, 6 px) - easier to grab
function rflon(r) ( r == 0 ? rflHv0 && (sqr(mx - rflHx0) + sqr(my - rflHy0) < 144 || segd(mx, my, rflSx0[0], rflSy0[0], rflSx1[0], rflSy1[0]) < 6)
                           : rflHv1 && (sqr(mx - rflHx1) + sqr(my - rflHy1) < 144 || segd(mx, my, rflSx0[1], rflSy0[1], rflSx1[1], rflSy1[1]) < 6); );
// Draws the free-field reflector panels R1/R2 with drag handles and non-overlapping labels; sets rflHv0/1, rflHx/y and rflHov
// Top edge a reflector handle may be clamped to: 30 px under the view's top, and under the chip row (Fit, 1:1) where it is
function rflTop(hx) ( hx >= vzcX1 - 10 ? VY + 50 : VY + 30; );
// The screen box (vzbX0 … vzbY1) of the room (free field: the area on the ground) and the reflectors that are on, in the current view
function vzfbox() local(r, o, a, ux, uy, hl, k, z)
(
  vzbX0 = vzbY0 = 100000; vzbX1 = vzbY1 = -100000;
  k = 0; loop(isfree || planv ? 4 : 8, z = k >= 4 ? roomH : 0; PJ((k & 1) ? roomW : 0, (k & 2) ? roomD : 0, z);
    vzbX0 = min(vzbX0, qx); vzbX1 = max(vzbX1, qx); vzbY0 = min(vzbY0, qy); vzbY1 = max(vzbY1, qy); k += 1; );
  isfree && vopt[RFB + 14] < 0.5 ? ( r = 0; loop(2, o = RFB + r * 6;
    vopt[o] >= 0.5 ? ( a = vopt[o + 3] * $pi / 180; ux = cos(a); uy = sin(a); hl = 0.5 * vopt[o + 4];
      k = 0; loop(planv ? 2 : 4, PJ(vopt[o + 1] + ((k & 1) ? ux : -ux) * hl, vopt[o + 2] + ((k & 1) ? uy : -uy) * hl, (k & 2) ? vopt[o + 5] : 0);
        vzbX0 = min(vzbX0, qx); vzbX1 = max(vzbX1, qx); vzbY0 = min(vzbY0, qy); vzbY1 = max(vzbY1, qy); k += 1; ); );
    r += 1; ); );
);
// Fit: back to the view without zoom, then the zoom (0.1 … 5) and the shift that put that box into the middle of the view
function vzfit() local(cx, cy, x0, x1, y0, y1, f)
(
  vzfbox(); cx = VX + VW / 2; cy = VY + VH / 2;
  x0 = cx + (vzbX0 - cx - vzox) / vzf; x1 = cx + (vzbX1 - cx - vzox) / vzf; y0 = cy + (vzbY0 - cy - vzoy) / vzf; y1 = cy + (vzbY1 - cy - vzoy) / vzf;
  f = min(5, max(0.1, min((VW - 96) / max(1, x1 - x0), (VH - 100) / max(1, y1 - y0))));
  vzox = VX + VW / 2 - cx - ((x0 + x1) / 2 - cx) * f; vzoy = VY + 46 + (VH - 100) / 2 - cy - ((y0 + y1) / 2 - cy) * f; vzf = f;
);
function rflmark() local(r, o, a, ux, uy, hl, rht, c, hit, k, cx, cy, ox, oy, tx, ty, bad, okc, bx1, by1, bx2, by2, lhx, lhy, lhit, hhx, hhy, hhit)
(
  rflHv0 = rflHv1 = 0; rflHov = 0; rflLx0 = rflLy0 = -999;
  isfree ? (   // also in Learn and in the side view (the panels seen from the side; the handles only in Plan / 3D)
    r = 0;
    loop(2,
      // per reflector (stride 6): vopt[o] = material (0 = off), +1 = X, +2 = Y (note: c / cx below), +3 = angle (deg), +4 = length, +5 = height; vopt[RFB + 14] >= 0.5 = both reflectors off
      o = RFB + r * 6;
      vopt[o] >= 0.5 && vopt[RFB + 14] < 0.5 ? (
        a = vopt[o + 3] * $pi / 180; ux = cos(a); uy = sin(a); hl = 0.5 * vopt[o + 4]; rht = vopt[o + 5]; c = vopt[o + 1]; cx = vopt[o + 2];
        rflCol = vopt[o] < 1.5 ? 0x8C8F99 : (vopt[o] < 2.5 ? 0x7FB6C9 : (vopt[o] < 3.5 ? 0x8A8378 : (vopt[o] < 4.5 ? 0x9A9AA4 : 0x6FA37A)));
        col(rflCol, 0.9); 
        PJ(c - ux * hl, cx - uy * hl, 0); rfx0 = qx; rfy0 = qy; PJ(c + ux * hl, cx + uy * hl, 0); rfx1 = qx; rfy1 = qy;
        rflSx0[r] = rfx0; rflSy0[r] = rfy0; rflSx1[r] = rfx1; rflSy1[r] = rfy1;   // its foot line (grab it anywhere)
        PJ(slider10 * rs, slider11 * rs, 0); bx1 = qx; by1 = qy; PJ(slider13 * rs, slider14 * rs, 0); bx2 = qx; by2 = qy;   // the balls (the handles keep off them)
        !planv ? (
          PJ(c - ux * hl, cx - uy * hl, rht); rfx2 = qx; rfy2 = qy; PJ(c + ux * hl, cx + uy * hl, rht); rfx3 = qx; rfy3 = qy;
          PJ(slider10 * rs, slider11 * rs, 0); bx1 = qx; by1 = qy; PJ(slider13 * rs, slider14 * rs, 0); bx2 = qx; by2 = qy; pnf = cfade;
          pnx0 = min(min(rfx0, rfx1), min(rfx2, rfx3)); pnx1 = max(max(rfx0, rfx1), max(rfx2, rfx3)); pny0 = min(min(rfy0, rfy1), min(rfy2, rfy3)); pny1 = max(max(rfy0, rfy1), max(rfy2, rfy3));
          (bx1 > pnx0 - 14 && bx1 < pnx1 + 14 && by1 > pny0 - 14 && by1 < pny1 + 14) || (bx2 > pnx0 - 14 && bx2 < pnx1 + 14 && by2 > pny0 - 14 && by2 < pny1 + 14) ? cfade = cfade * 0.4;   // the panel in front of a ball: see-through, the ball stays findable
          pnC = c; pnCy = cx; pnUx = ux; pnUy = uy; pnHl = hl; pnH = rht;
          vopt[67] < 0.5 ? ( col(rflCol, vopt[o] > 4.5 ? 0.5 : (vopt[o] > 3.5 ? 0.07 : (vopt[o] > 2.5 ? 0.62 : (vopt[o] > 1.5 ? 0.3 : 0.6)))); sidevOn && !frontv ? RR(min(rfx0, rfx1), min(rfy0, rfy2), abs(rfx1 - rfx0) + 0.6, abs(rfy2 - rfy0) + 0.6, 0.5) : POLY4(rfx0, rfy0, rfx1, rfy1, rfx3, rfy3, rfx2, rfy2); );
          vopt[67] < 0.5 ? pntex(vopt[o]); cfade = pnf;
          col(rflCol, 0.9); LNW(rfx0, rfy0, rfx1, rfy1, 1.6); LNW(rfx2, rfy2, rfx3, rfy3, 1.6); LNW(rfx0, rfy0, rfx2, rfy2, 1.1); LNW(rfx1, rfy1, rfx3, rfy3, 1.1);
        ) : ( LNW(rfx0, rfy0, rfx1, rfy1, 3.2); );
        PJ(c, cx, 0); hit = !sidevOn && (sqr(mx - qx) + sqr(my - qy) < 144 || segd(mx, my, rfx0, rfy0, rfx1, rfy1) < 6 || drag == 24 + r || drag == 30 + r);
        !sidevOn ? ( col(cSURF, 1); CIF(qx, qy, 5, 1); col(rflCol, 1); CIF(qx, qy, hit ? 3.6 : 2.6, 1); );
        // SCALE HANDLES: a small square at the right end of the foot line = drag along the line: Length; in 3D a second one at the
        // top edge's center = drag up / down: Height. Neither is drawn when it would sit on the area corner handle or a ball
        rflLHv[r] = rflHHv[r] = 0;
        !sidevOn ? (
          // a panel longer than the view: the handle slides along the foot line to the view's edge (rflLK = how much longer
          // the panel is than the handle's distance from its center - the drag keeps that ratio, so nothing jumps)
          PJ(c, cx, 0); ox = qx; oy = qy;
          drag != 26 + r ? (
            tx = 1; hl > 0.01 ? (
              rfx1 > VX + VW - 16 && rfx1 > ox ? tx = min(tx, (VX + VW - 16 - ox) / (rfx1 - ox)); rfx1 < VX + 16 && rfx1 < ox ? tx = min(tx, (VX + 16 - ox) / (rfx1 - ox));
              rfy1 > VY + VH - 32 && rfy1 > oy ? tx = min(tx, (VY + VH - 32 - oy) / (rfy1 - oy)); rfy1 < rflTop(rfx1) && rfy1 < oy ? tx = min(tx, (rflTop(rfx1) - oy) / (rfy1 - oy));
            );
            rflLK[r] = 1 / max(0.05, tx);
          );
          lhx = ox + (rfx1 - ox) / rflLK[r]; lhy = oy + (rfy1 - oy) / rflLK[r];
          !((ffHv && sqr(lhx - ffHx) + sqr(lhy - ffHy) < 324) || sqr(lhx - bx1) + sqr(lhy - by1) < 256 || sqr(lhx - bx2) + sqr(lhy - by2) < 256) ? (
            rflLHv[r] = 1; rflLHx[r] = lhx; rflLHy[r] = lhy; lhit = sqr(mx - lhx) + sqr(my - lhy) < 64 || drag == 26 + r;
            col(cSURF, 1); R(lhx - 3.5, lhy - 3.5, 7, 7); col(rflCol, 1); LNBOX(lhx - 3.5, lhy - 3.5, 7, 7, lhit ? 1.6 : 1);
            lhit && drag != 26 + r ? tipbox(lhx - 5, lhy - 5, 10, 10, "Length: drag along the panel = longer / shorter (the center stays; the knob Length does the same)");
          );
          !planv ? (
            PJ(c, cx, rht); hhx = qx; hhy = qy;
            // a panel higher than the view: the handle stays at the view's top edge (rflHK: as above)
            drag != 28 + r ? ( rflHK[r] = hhy < rflTop(hhx) && oy - hhy > 1 ? (oy - hhy) / max(1, oy - rflTop(hhx)) : 1; );
            hhy = oy - (oy - hhy) / rflHK[r];
            !(sqr(hhx - bx1) + sqr(hhy - by1) < 256 || sqr(hhx - bx2) + sqr(hhy - by2) < 256) ? (
              rflHHv[r] = 1; rflHHx[r] = hhx; rflHHy[r] = hhy; hhit = sqr(mx - hhx) + sqr(my - hhy) < 64 || drag == 28 + r;
              col(cSURF, 1); R(hhx - 3.5, hhy - 3.5, 7, 7); col(rflCol, 1); LNBOX(hhx - 3.5, hhy - 3.5, 7, 7, hhit ? 1.6 : 1);
              hhit && drag != 28 + r ? tipbox(hhx - 5, hhy - 5, 10, 10, "Height: drag up / down = higher / lower (the knob Height does the same)");
            );
          );
        );
// the label sits where it overlaps nothing: above / below / beside the handle - not on the title, the balls or the other label
        PJ(slider10 * rs, slider11 * rs, 0); bx1 = qx; by1 = qy; PJ(slider13 * rs, slider14 * rs, 0); bx2 = qx; by2 = qy; PJ(c, cx, 0);
        okc = 0; k = 0; tx = qx; ty = qy - 11;
        loop(6,
          !okc ? (
            ox = k == 2 ? -15 : (k == 3 ? 15 : (k == 4 ? -15 : (k == 5 ? 15 : 0))); oy = k == 0 ? -11 : (k == 1 ? 11 : (k < 4 ? 0 : (k == 4 ? -14 : 14)));
            tx = qx + ox; ty = qy + oy;
            bad = (abs(tx - (VX + VW / 2)) < 52 && abs(ty - (VY + 36)) < 13) || sqr(tx - bx1) + sqr(ty - by1) < 1000 || sqr(tx - bx2) + sqr(ty - by2) < 1000
               || (r == 1 && abs(tx - rflLx0) < 16 && abs(ty - rflLy0) < 11) || ty < VY + 24 || ty > VY + VH - 12 || tx < VX + 8 || tx > VX + VW - 8;
            !bad ? okc = 1;
          );
          k += 1;
        );
        !okc ? ( tx = qx; ty = qy - 11; );
        r == 0 ? ( rflLx0 = tx; rflLy0 = ty; );
        !sidevOn ? ( gfx_setfont(4); col(cTEXT, 0.85); txt(r == 0 ? "R1" : "R2", tx, ty, 1); );
        !sidevOn ? ( r == 0 ? ( rflHv0 = 1; rflHx0 = qx; rflHy0 = qy; ) : ( rflHv1 = 1; rflHx1 = qx; rflHy1 = qy; ); );
        hit && drag != 24 + r ? ( rflHov = 1; tipbox(qx - 9, qy - 9, 18, 18, "Reflector: drag the dot or the panel = move it · Alt+drag = turn it · the small squares: Length (end of the panel) and, in 3D, Height (top) – the knobs are in the Reflectors tab"); );
      );
      r += 1;
    );
  );
);
// Initializes the Own spot MTAP[] to the source or listener position (per slider101) if it is not yet placed
function apinit() ( MTAP[2] < 0.5 ? ( MTAP[0] = floor(slider101 + 0.5) == 1 ? slider13 : slider10; MTAP[1] = floor(slider101 + 0.5) == 1 ? slider14 : slider11; MTAP[2] = 1; ); );
// Draws the Own spot + marker (path center) in the room view, sets apHx/apHy/apHv and hover state with tooltip
function apmark() local(ax, ay, hit, set)
(
  set = MTAP[2] >= 0.5;
  ax = set ? MTAP[0] : (floor(slider101 + 0.5) == 1 ? slider13 : slider10); ay = set ? MTAP[1] : (floor(slider101 + 0.5) == 1 ? slider14 : slider11);
  PJ(min(max(ax * rs, 0.1), roomW - 0.1), min(max(ay * rs, 0.1), roomD - 0.1), 0);
  !set ? qx += 28;   // not placed yet - the + stands beside the ball (on it, it took the grab away from the ball: source / listener could not be moved)
  apHx = qx; apHy = qy; apHv = 1;
  hit = sqr(mx - qx) + sqr(my - qy) < 100 || drag == 22;
  col(cMOTT, set ? 0.28 : 0.1); CIF(qx, qy, 8, 1);
  col(cMOTT, 1); CIF(qx, qy, 8, 0); LNW(qx - 11, qy, qx + 11, qy, hit ? 1.6 : 1); LNW(qx, qy - 11, qx, qy + 11, hit ? 1.6 : 1);
  hit && drag != 22 ? tipbox(qx - 11, qy - 11, 22, 22, set ? "Own spot: the center of the circle / square. Drag = place it anywhere in the room" : "Own spot: not placed yet – the pattern uses the ball. Drag this + to place it anywhere in the room");
);
// GHOST ANCHOR: where the balls were last put by hand (or when Motion stopped). The preview path is drawn around THESE places - when the host's
// automation (Latch / Read) moves the source or the listener, the path stays where it is
function ganupd() ( ganDirty || !ganOk || mtEg > 0.001 ? ( ganSX = slider10; ganSY = slider11; ganSZ = slider12; ganLX = slider13; ganLY = slider14; ganLZ = slider15; ganOk = 1; ganDirty = 0; ); );
// Draws the ghost preview of the Motion path around the last hand-placed ball positions (gan*), so it stays put during automation
function mtghost(who) local(p, r, cx, cy, cz, k, a, x1, y1, th0, sg, ux, uy, c, m, p0)
(
  // p0 = pattern (0 Circle, 1 Left-right, 2 Front-back, 3 Up-down, 4 Pass-by, 5 Square, 6 Wave, 7 Drawn); p = the base geometry it is laid out like (Square / Wave as Circle, Drawn as Pass-by); m = Move (0 Source, 1 Listener, 2 Both); c = Around (0 the other ball, 1 own spot, 2 room center)
  p = floor(slider102 + 0.5); p0 = p; p = p == 5 || p == 6 ? 0 : (p == 7 ? 4 : p);r = slider104 * rs; m = floor(slider101 + 0.5); c = floor(slider107 + 0.5); th0 = slider105 * $pi / 180;   // the center as the circle / as the pass-by
  p0 == 1 || p0 == 2 || p0 == 6 ? c = 1;   // around its own spot
  who == 1 ? ( cx = (p == 0 && c == 0 ? ganLX : (p == 0 && c == 2 ? max(0.8, slider2) / 2 : ganSX)) * rs; cy = (p == 0 && c == 0 ? ganLY : (p == 0 && c == 2 ? max(0.8, slider3) / 2 : ganSY)) * rs; cz = ganSZ;
               p == 1 && c == 2 ? cx = max(0.8, slider2) / 2 * rs; p == 2 && c == 2 ? cy = max(0.8, slider3) / 2 * rs; p == 1 && c == 0 ? cx = ganLX * rs; p == 2 && c == 0 ? cy = ganLY * rs; )
           : ( cx = (c == 2 && p != 4 && p != 3 ? max(0.8, slider2) / 2 : ganLX) * rs; cy = (c == 2 && p != 4 && p != 3 ? max(0.8, slider3) / 2 : ganLY) * rs; cz = ganLZ;
               p == 1 ? cy = ganLY * rs; p == 2 ? cx = ganLX * rs; );
  c == 1 && MTAP[2] >= 0.5 && (p0 == 0 || p0 == 5) ? ( cx = MTAP[0] * rs; cy = MTAP[1] * rs; );   // the Own spot of the user
  who == 2 && m == 1 && c == 0 && (p0 == 0 || p0 == 5) ? ( cx = ganSX * rs; cy = ganSY * rs; );   // the listener around the source
  sg = who == 1 ? 1 : (m == 2 ? -1 : 1);
// the same full-size fit as the audio (the center moves in from the walls; a pass-by starts earlier)
  p0 == 4 ? ( ux = sg * cos(th0) * (slider106 >= 0.5 ? -1 : 1); uy = sg * sin(th0) * (slider106 >= 0.5 ? -1 : 1);   // the middle of the cut line (as the audio)
              cx += r * ux; cy += r * uy; r = min(r, min(abs(ux) > 0.001 ? (roomW - 0.2) / (2 * abs(ux)) : 100000, abs(uy) > 0.001 ? (roomD - 0.2) / (2 * abs(uy)) : 100000));
              cx = gmfit(cx, -r * abs(ux), r * abs(ux), roomW, 0.1); cy = gmfit(cy, -r * abs(uy), r * abs(uy), roomD, 0.1); ) : (
  !(who == 1 && c == 0 && (p0 == 0 || p0 == 5)) ? ( gmtext(p0); cx = gmfit(cx, r * gmX0, r * gmX1, roomW, 0.1); cy = gmfit(cy, r * gmY0, r * gmY1, roomD, 0.1); cz = gmfit(cz, r * gmZ0, r * gmZ1, roomH, 0.1); ); );
  mthandle(p0, cx, cy, cz, r, sg, p0 == 4); mtstartmark(p0, cx, cy, cz, r, sg, p0 == 4, who == 2 && m == 2 ? $pi : 0);
  col(cMOTT, 0.38);
  p0 >= 5 ? mtshape(p0, cx, cy, cz, r) :
  p == 0 ? (
    k = 0;
    loop(36,
      a = k * 2 * $pi / 36; PJ(min(max(cx + r * cos(a), 0), roomW), min(max(cy + r * sin(a), 0), roomD), cz); x1 = qx; y1 = qy;
      a += 2 * $pi / 36 * 0.6; PJ(min(max(cx + r * cos(a), 0), roomW), min(max(cy + r * sin(a), 0), roomD), cz); LNW(x1, y1, qx, qy, 1);
      k += 1;
    );
  ) : (
    p == 1 ? ( PJ(max(0, cx - r), cy, cz); x1 = qx; y1 = qy; PJ(min(roomW, cx + r), cy, cz); ) : (
    p == 2 ? ( PJ(cx, max(0, cy - r), cz); x1 = qx; y1 = qy; PJ(cx, min(roomD, cy + r), cz); ) : (
    p == 4 ? ( ux = sg * cos(th0) * (slider106 >= 0.5 ? -1 : 1); uy = sg * sin(th0) * (slider106 >= 0.5 ? -1 : 1);   // (cx, cy is the middle already)
               PJ(min(max(cx - r * ux, 0), roomW), min(max(cy - r * uy, 0), roomD), cz); x1 = qx; y1 = qy; PJ(min(max(cx + r * ux, 0), roomW), min(max(cy + r * uy, 0), roomD), cz); ) : (
               PJ(cx, cy, max(0.1, cz - r)); x1 = qx; y1 = qy; PJ(cx, cy, min(roomH - 0.1, cz + r)); )));
    DL(x1, y1, qx, qy, 4, 3, 1);
  );
);
// Draws the "Motion running" badge at the top right of the room view with path summary and a Stop button (setp(100,0))
function mtsign() local(w, x, y, hit)
(
  mtstat(); sprintf(#w1, "Motion  %s · %s · %.1f m", mtM == 0 ? "Source" : (mtM == 1 ? "Listener" : "Both"), #sts, slider104 * rs);
  gfx_setfont(7); w = tws(#w1) + 56; x = VX + VW - 14 - w; y = VY + 30;
  col(cSURF, 0.92); RR(x, y, w, 18, 6); col(cMOTT, 1); RRO(x, y, w, 18, 6); CI(x + 10, y + 9, 3, 1);
  col(cTEXT, 1); txt(#w1, x + 18, y + 9, 0);
  hit = inbox(x + w - 36, y + 2, 32, 14);
  col(hit ? cMOTT : cLINE, 1); RR(x + w - 36, y + 2, 32, 14, 5); col(hit ? cSURF : cTEXT, 1); gfx_setfont(7); txt(slider100 >= 0.5 ? "Stop" : "…", x + w - 20, y + 9, 1);
  click && hit && slider100 >= 0.5 ? ( setp(100, 0); click = 0; );
  tipbox(x, y, w, 18, "Motion is running – drag a moving ball to move its whole path; Stop switches the motion off (the glide brings source and listener back)");
);
// LOUDNESS HINT: for 8 s after a room preset was loaded - how much louder or quieter the new room plays (from the
// engine's loudness estimate eqE: direct + reflections + tail). Small rooms sit up to 10 dB above a living room (the short
// distance), which runs a mix straight into the limiter; the chip switches Equal loudness on
function lhsign() local(w, x, y, hit)
(
  lhDb < -999 && lhE0 > 0.0000001 && eqE != lhE0 ? lhDb = 10 * log10(max(0.0000001, eqE) / lhE0);
  lhvis = lhDb > -999 && time_precise() - lhT < 8 && abs(lhDb) >= 1;
  lhvis ? (
    sprintf(#w1, "%s plays %.0f dB %s than %s", lhNew <= -2 ? scname(-2 - lhNew) : (lhNew >= 0 ? roomname(lhNew) : "This room"), abs(lhDb), lhDb > 0 ? "louder" : "softer", lhPre <= -2 ? scname(-2 - lhPre) : (lhPre >= 0 ? roomname(lhPre) : "before"));
    gfx_setfont(7); tws(#w1) + 118 > VW - 28 ? sprintf(#w1, "%s: %.0f dB %s", lhNew <= -2 ? scname(-2 - lhNew) : (lhNew >= 0 ? roomname(lhNew) : "This room"), abs(lhDb), lhDb > 0 ? "louder" : "softer");
    gfx_setfont(7); w = min(tws(#w1) + (slider60 < 0.5 ? 118 : 24), VW - 28); x = VX + 14; y = VY + 30;
    ffnR = max(ffnR, x + w); ffnB = max(ffnB, y + 18);   // right / bottom edge of this note: the FREE FIELD title keeps clear of it
    col(cSURF, 0.92); RR(x, y, w, 18, 6); col(lhDb > 6 ? cIOTT : cLINE, 1); RRO(x, y, w, 18, 6);
    col(cTEXT, 1); txtfit(#w1, x + 10, y + 9, w - (slider60 < 0.5 ? 118 : 24));
    slider60 < 0.5 ? (
      hit = inbox(x + w - 104, y + 2, 98, 14);
      col(hit ? cIOTT : cLINE, 1); RR(x + w - 104, y + 2, 98, 14, 5); col(hit ? cSURF : cTEXT, 1); txt("Equal loudness", x + w - 55, y + 9, 1);
      click && hit ? ( slider60 = 1; sauto1(60); click = 0; );
      tipbox(x + w - 104, y + 2, 98, 14, "Equal loudness on: every room, distance and barrier plays about as loud as the input");
    );
  );
);
// EQUAL LOUDNESS HINT: while Equal loudness is on and the distance between source and listener changes (drag, keys,
// motion, automation), a note says that the distance no longer changes the level - for 4 s after the last change; the
// chip switches Equal loudness off
function elsign() local(d, w, x, y, hit)
(
  d = sqrt(sqr(slider10 - slider13) + sqr(slider11 - slider14) + sqr(slider12 - slider15));
  slider60 < 0.5 || !eldI ? ( eldL = d; elT = 0; eldI = 1; ) : ( abs(d - eldL) > 0.05 ? ( eldL = d; elT = time_precise(); ); );
  slider60 >= 0.5 && elT > 0 && time_precise() - elT < 4 ? (
    gfx_setfont(7); w = min(max(tws("Equal loudness is on"), tws("the distance does not change the level")) + 100, VW - 28); x = VX + 14; y = VY + 30 + (lhvis ? 22 : 0);
    !lhvis ? ( ffnR = max(ffnR, x + w); ffnB = max(ffnB, y + 30); );   // (with the first note showing, this one sits one row lower, below the title)
    col(cSURF, 0.95); RR(x, y, w, 30, 6); col(cIOTT, 1); RRO(x, y, w, 30, 6);
    col(cTEXT, 1); txtfit("Equal loudness is on", x + 10, y + 9, w - 100); col(cMUTED, 1); txtfit("the distance does not change the level", x + 10, y + 21, w - 100);
    hit = inbox(x + w - 80, y + 8, 70, 14);
    col(hit ? cIOTT : cLINE, 1); RR(x + w - 80, y + 8, 70, 14, 5); col(hit ? cSURF : cTEXT, 1); txt("Turn off", x + w - 45, y + 15, 1);
    click && hit ? ( slider60 = 0; sauto1(60); elT = 0; click = 0; );
    tipbox(x + w - 80, y + 8, 70, 14, "Equal loudness off: the level tells the distance again (Output module)");
  );
);
// Draws the room view (stage, 3D/plan/side projection) from room dimensions: sets scale pS, skews pKX/pKY/pSZ and origin pX0/pY0
function gfxp1()
(
spHov = 0; spOk = 0;   // (drawspan sets them again; with source and listener on the same spot it is not drawn at all)
col(cSTAGE, 1); RR(VX + 8, VY + 24, VW - 16, VH - 50, 7);   // the stage the room stands on
// 28 px top (facing), 52 px bottom (width line + tool row); 44 px left (height), 44 px right (depth)
// the view angle follows the stage: from 0.58 (steep, tall stage) down to 0.40 (flat, wide stage) - the largest room wins
// pHt = the height the view makes room for: the room's height; in the free field (no ceiling) only what stands there -
// source, listener and the reflectors (up to the area's size; taller ones: Fit) - so the area fills the view like a room
pHt = roomH; isfree ? ( pHt = max(1.2, max(sz, lz) + 0.4);
  vopt[RFB + 14] < 0.5 ? ( vopt[RFB] >= 0.5 ? pHt = max(pHt, min(vopt[RFB + 5], max(roomW, roomD))); vopt[RFB + 6] >= 0.5 ? pHt = max(pHt, min(vopt[RFB + 11], max(roomW, roomD))); ); );
pS = 0; k3 = 0.58; kY3 = 0.58; kXb = 0.4;   // (the depth skew stays 0.4 - the look since; a variable-name clash had fixed it there)
loop(10, kx3 = 0.22 + (0.58 - k3) * 0.9; t3 = min((VW - 96) / (roomW + kx3 * roomD), (VH - 80) / (k3 * roomD + 0.95 * pHt));
  t3 > pS * 1.03 ? ( pS = t3; kY3 = k3; kXb = 0.4;  ); k3 -= 0.02; );
pKX = kXb * pS; pKY = kY3 * pS; pSZ = 0.95 * pS;
pX0 = VX + (VW - pS * (roomW + kXb * roomD)) / 2;
pY0 = VY + 28 + (VH - 80 - pS * (kY3 * roomD + 0.95 * pHt)) / 2 + 0.95 * pHt * pS;
// PLAN: straight from above - x to the right, y (back wall -> front) down, height not shown; to scale
planv ? (
  pS = min((VW - 96) / roomW, (VH - 100) / roomD); pKX = 0; pKY = pS; pSZ = 0;
  pX0 = VX + (VW - pS * roomW) / 2; pY0 = VY + 46 + (VH - 100 - pS * roomD) / 2;
);
// SIDE VIEW (3D tab, chip "Side"): the room from the side, to scale - the depth to the right (back wall at the left), the height up; the width is not shown
// FRONT VIEW (chip "Front"): the room from the front wall, to scale - x to the right, the height up; the depth is not shown. sidev: 0 = 3D, 1 = Side, 2 = Front
sidevOn = vtab == 1 && sidev > 0; frontv = sidevOn && sidev == 2;
sidevOn && !frontv ? (
  pS = min((VW - 96) / roomD, (VH - 100) / pHt); pKX = pS; pKY = 0.0001 * pS; pSZ = pS;
  pX0 = VX + (VW - pS * roomD) / 2; pY0 = VY + 46 + (VH - 100 - pS * pHt) / 2 + pS * pHt;
);
frontv ? (
  pS = min((VW - 96) / roomW, (VH - 100) / pHt); pKX = 0.0001 * pS; pKY = 0.0001 * pS; pSZ = pS;
  pX0 = VX + (VW - pS * roomW) / 2; pY0 = VY + 46 + (VH - 100 - pS * pHt) / 2 + pS * pHt;
);
// while an edge or a size label is dragged the projection holds still as long as the room fits into it: the edge stays
// under the mouse (smaller room: no zoom-in that would pull the edge away). A room that grows beyond it: the view zooms out.
(drag == 11 || drag == 12 || (drag >= 7 && drag <= 9)) && (mouse_cap & 1) ? (
  frzok = frontv ? (dpX00 + roomW * dpS0 <= VX + VW - 47.5 && dpY00 - pHt * dpSZ0 >= VY + 27.5) : sidevOn ? (dpX00 + roomD * dpKX0 <= VX + VW - 47.5 && dpY00 - pHt * dpSZ0 >= VY + 27.5) : (dpX00 + roomW * dpS0 + roomD * dpKX0 <= VX + VW - 47.5 && dpY00 + roomD * dpKY0 <= VY + VH - 51.5 && dpY00 - pHt * dpSZ0 >= VY + 27.5);
  frzok ? ( pS = dpS0; pKX = dpKX0; pKY = dpKY0; pSZ = dpSZ0; pX0 = dpX00; pY0 = dpY00; );   // (a room too big for the held view: the view zooms out)
);

// VIEW ZOOM: wheel over the empty floor = zoom around the mouse (0.1 … 5 x, also out: a reflector larger than the area),
// the room keeps its size; "Fit" = the area / room and the reflectors fill the view; "1:1" = back
!(vzf >= 0.1 && vzf <= 5) ? ( vzf = 1; vzox = vzoy = 0; );
vzOn = abs(vzf - 1) > 0.0001 || abs(vzox) > 0.01 || abs(vzoy) > 0.01;
vzOn ? (
  vzcx = VX + VW / 2; vzcy = VY + VH / 2;
  pS *= vzf; pKX *= vzf; pKY *= vzf; pSZ *= vzf;
  pX0 = vzcx + (pX0 - vzcx) * vzf + vzox; pY0 = vzcy + (pY0 - vzcy) * vzf + vzoy;
);
pXs = sidevOn && !frontv ? 0.0001 * pS : pS;   // the x of the room does not move anything on the screen in the side view
// free field scenery (3D only): flat sky bands, two rows of hills, clouds, flat ground (cartoon style)
isfree && !planv && !sidevOn && vopt[67] < 0.5 ? (   // vopt[67] = outline view (no colored scenery)
  PJ(0, 0, 0); hzy = min(max(qy - 4, VY + 80), VY + VH - 70); PJ(0, roomD, 0); gyF = min(qy + 6, VY + VH - 4);
  sk0 = VY + 26; skx0 = VX + 4; skx1 = VX + VW - 4;
// flat, abstract scenery (cartoon style): three calm sky bands, two rows of round hills, two clouds, one flat ground
  skh = hzy - sk0;
  col(darkmode ? 0x1B2340 : 0xD2E6F6, 1); POLY4(skx0, sk0, skx1, sk0, skx1, sk0 + skh * 0.4, skx0, sk0 + skh * 0.4);
  col(darkmode ? 0x232C50 : 0xE0EEF9, 1); POLY4(skx0, sk0 + skh * 0.4, skx1, sk0 + skh * 0.4, skx1, sk0 + skh * 0.75, skx0, sk0 + skh * 0.75);
  col(darkmode ? 0x2B3660 : 0xEEF6FC, 1); POLY4(skx0, sk0 + skh * 0.75, skx1, sk0 + skh * 0.75, skx1, hzy, skx0, hzy);
  mhmax = hzy - (VY + 56);
  mhmax > 14 ? (
    ms = min(1, mhmax / 72);
    hillf(VX + VW * 0.22, VW * 0.26, ms * 50, darkmode ? 0x34406C : 0xC4D7EA, 1);
    hillf(VX + VW * 0.62, VW * 0.3, ms * 62, darkmode ? 0x34406C : 0xC4D7EA, 1);
    hillf(VX + VW * 0.42, VW * 0.24, ms * 34, darkmode ? 0x3F4C7C : 0xAFC6DE, 1);
    hillf(VX + VW * 0.84, VW * 0.22, ms * 40, darkmode ? 0x3F4C7C : 0xAFC6DE, 1);
    hillf(VX + VW * 0.06, VW * 0.2, ms * 38, darkmode ? 0x3F4C7C : 0xAFC6DE, 1);
  );
  !darkmode && hzy - (VY + 76) > 40 ? (
    col(0xFFFFFF, 1); RR(VX + VW * 0.14, VY + 72, 46, 9, 4.5); RR(VX + VW * 0.14 + 10, VY + 66, 24, 9, 4.5);
    RR(VX + VW * 0.6, VY + 86, 38, 8, 4); RR(VX + VW * 0.6 + 8, VY + 81, 20, 8, 4);
  );
  // ground color by slider119 (0 = neutral, 1 = asphalt, 2 = grass, 3 = snow), dark / light mode
  gcF = slider119 < 0.5 ? (darkmode ? 0x4A463C : 0xDCD5C0) : (slider119 < 1.5 ? (darkmode ? 0x3A4048 : 0xBCC2CB) : (slider119 < 2.5 ? (darkmode ? 0x35573A : 0xA9D6A2) : (darkmode ? 0x6A7A8C : 0xF1F6FB)));
  col(gcF, 1); POLY4(skx0, hzy, skx1, hzy, skx1, gyF, skx0, gyF);
  col(darkmode ? 0xFFFFFF : 0x000000, 0.05); POLY4(skx0, hzy + (gyF - hzy) * 0.18, skx1, hzy + (gyF - hzy) * 0.18, skx1, hzy + (gyF - hzy) * 0.42, skx0, hzy + (gyF - hzy) * 0.42);   // two flat ground bands give the depth
);

// floor
PJ(0, 0, 0); fx1 = qx; fy1 = qy; PJ(roomW, 0, 0); fx2 = qx; fy2 = qy;
PJ(roomW, roomD, 0); fx3 = qx; fy3 = qy; PJ(0, roomD, 0); fx4 = qx; fy4 = qy;
col(cINK, darkmode ? 0.05 : 0.035); POLY4(fx1, fy1, fx2, fy2, fx3, fy3, fx4, fy4);
// free field: the ground material tints the area (asphalt grey, grass green, snow white-blue)
gAm = isfree && vopt[67] < 0.5 && slider119 >= 0.5 ? (slider119 < 1.5 ? 1 : (slider119 < 2.5 ? 2 : 3)) : 0;
gAm > 0 ? (
  col(gAm == 1 ? 0x6E7480 : (gAm == 2 ? 0x6FAE5A : 0xCFE0F2), gAm == 3 ? (darkmode ? 0.34 : 0.62) : (darkmode ? 0.24 : 0.30));
  POLY4(fx1, fy1, fx2, fy2, fx3, fy3, fx4, fy4);
);
pS >= 6 ? (
  col(cINK, darkmode ? 0.10 : 0.06);
  gstep = pS >= 10 ? 1 : 5;              // 1 m grid, 5 m grid in large rooms
  gi = gstep; loop(floor((roomW - 0.001) / gstep), PJ(gi, 0, 0); hx0 = qx; hy0 = qy; PJ(gi, roomD, 0); LN(hx0, hy0, qx, qy); gi += gstep; );
  gi = gstep; loop(floor((roomD - 0.001) / gstep), PJ(0, gi, 0); hx0 = qx; hy0 = qy; PJ(roomW, gi, 0); LN(hx0, hy0, qx, qy); gi += gstep; );
);

// ground marks: flat style - a few simple signs only (grass: little V tufts, snow: soft dots, asphalt: nothing)
gAm > 1 ? (
  gGi = 0;
  loop(16,
    // deterministic pseudo-random tuft / dot positions (sin hash, fractional part), so they do not flicker between frames
    gGt = sin(gGi * 12.9898 + 1.7) * 43758.5453; gGu = gGt - floor(gGt);
    gGt = sin(gGi * 78.233 + 4.1) * 43758.5453; gGv = gGt - floor(gGt);
    PJ(gGu * roomW, gGv * roomD, 0);
    gAm == 2 ? ( col(0x4E8F3D, 0.7); LNW(qx - 3, qy, qx, qy - 4, 1.4); LNW(qx, qy - 4, qx + 3, qy, 1.4); )
    : ( col(0xFFFFFF, darkmode ? 0.5 : 0.95); CI(qx, qy, 2.2, 1); );
    gGi += 1;
  );
);
!isfree ? (
  // floor: diagonal hatching, density = floor/ceiling absorption (same language as the walls)
  aF >= 0.03 && !planv ? (
    wallcol(aF, 0.30);
    fhk = 0.18; fhst = max(0.025, 0.22 - 0.2 * aF); fu = -fhk;
    while (
      ft0 = max(0, -fu / fhk); ft1 = min(1, (1 - fu) / fhk);
      PJ((fu + fhk * ft0) * roomW, ft0 * roomD, 0); hx0 = qx; hy0 = qy;
      PJ((fu + fhk * ft1) * roomW, ft1 * roomD, 0);
      LN(hx0, hy0, qx, qy);
      fu += fhst;
      fu < 1;
    );
  );
  // walls: drawn in full, or (WALLS off) only as a thin outline of the floor - nothing in the way of source and listener
  !hidew ? (
  !planv ? ( drawface(1, aN); drawface(4, aW); drawface(2, aE); );
  planv ? ( col(cWALLV, 1); R(fx1 - 1, fy1 - 1, fx2 - fx1 + 2, 2); R(fx2 - 1, fy2 - 1, 2, fy3 - fy2 + 1); R(fx1 - 1, fy1 - 1, 2, fy4 - fy1 + 1); );   // plan: the walls as fine solid lines (matte)
  // front wall: open – hatching ticks along its floor edge (density = absorption) and a faint top edge
  aS >= 0.03 && !planv ? (
    wallcol(aS, 0.45);
    fhst = max(0.025, 0.22 - 0.2 * aS); fu = 0;
    while (
      PJ(fu * roomW, roomD, 0); hx0 = qx; hy0 = qy;
      PJ(min(1, fu + 0.03) * roomW, roomD, roomH * 0.12);
      LN(hx0, hy0, qx, qy);
      fu += fhst;
      fu < 1;
    );
  );
  wallcol(aS, 0.9); planv ? col(cWALLV, 1);
  planv ? DL(fx4, fy4, fx3, fy3, 6, 4, 1.6) : ( aS >= 0.03 ? DL(fx4, fy4, fx3, fy3, max(2, 10 - 8 * aS), max(2, 3 + 3 * aS), 2.5) : LNW(fx4, fy4, fx3, fy3, 2.5); );
  PJ(0, roomD, roomH); hx0 = qx; hy0 = qy; PJ(roomW, roomD, roomH);
  wallcol(aS, 0.22); LN(hx0, hy0, qx, qy);
  ) : (
    col(cINK, 0.28); LN(fx1, fy1, fx2, fy2); LN(fx2, fy2, fx3, fy3); LN(fx3, fy3, fx4, fy4); LN(fx4, fy4, fx1, fy1);
  );
  // selected surface
  col(cTEXT, 0.9);
  selw >= 1 && selw <= 4 && !hidew ? faceoutline(selw, 2.2);
  selw == 5 ? ( LNW(fx1, fy1, fx2, fy2, 2.2); LNW(fx2, fy2, fx3, fy3, 2.2); LNW(fx3, fy3, fx4, fy4, 2.2); LNW(fx4, fy4, fx1, fy1, 2.2); );

  // back part with its own material: floor tinted + hatched like the walls
  bon && !isfree && slider46 >= 0.5 ? (
    PJ(0, 0, 0); hx0 = qx; hy0 = qy; PJ(roomW, 0, 0); hx1 = qx; hy1 = qy; PJ(roomW, yb, 0); hx2 = qx; hy2 = qy; PJ(0, yb, 0);
    wallcol(aBm, 0.16); POLY4(hx0, hy0, hx1, hy1, hx2, hy2, qx, qy);
    wallcol(aBm, 0.35);
    fu = 0; fhst = max(0.04, 0.22 - 0.2 * aBm);
    while ( PJ(fu * roomW, 0, 0); hx0 = qx; hy0 = qy; PJ(min(1, fu + 0.12) * roomW, yb, 0); LN(hx0, hy0, qx, qy); fu += fhst; fu < 1; );
  );
  // critical distance on the floor (dotted, neutral gray-blue)
  showpaths ? ( col(cCRV, 0.65); floorring(sx, sy, rc * sqrt(QDIR), 2); );   // dotted (the critical distance) - paths are dashed

  // 1st-order reflections, 3D (source -> reflection point -> listener)
  // (with a barrier: back / front wall of the listener's part; all paths faint when the barrier separates them)
  pyN = yo; pyS = yo + Dp; pon = !(bon && !bsame);
  // mirror-image method: tt = fraction of the way listener -> source at which the reflection touches each surface (back, right, front, left wall, then floor and ceiling)
  tt = (ly - pyN) / max(0.001, (sy - pyN) + (ly - pyN)); showpaths ? reflpath(lx + (sx - lx) * tt, pyN, lz + (sz - lz) * tt, aNy, pon);
  tt = (roomW - lx) / (2 * roomW - sx - lx); showpaths ? reflpath(roomW, ly + (sy - ly) * tt, lz + (sz - lz) * tt, aE, pon);
  tt = (pyS - ly) / max(0.001, (pyS - sy) + (pyS - ly)); showpaths ? reflpath(lx + (sx - lx) * tt, pyS, lz + (sz - lz) * tt, aSy, pon);
  tt = lx / (sx + lx);                     showpaths ? reflpath(0, ly + (sy - ly) * tt, lz + (sz - lz) * tt, aW, pon);
  tt = lz / max(0.001, sz + lz);           showpaths ? reflpath(lx + (sx - lx) * tt, ly + (sy - ly) * tt, 0, aF, tapOn[5] && pon);
  tt = (roomH - lz) / (2 * roomH - sz - lz); showpaths ? reflpath(lx + (sx - lx) * tt, ly + (sy - ly) * tt, roomH, aF, tapOn[6] && pon);

) : (
  // distance rings (1/2/4/8 m, -6 dB per doubling): only with Paths on, labels only when readable
  showpaths ? (
    gj = 1;
    loop(4,
      frad = gj == 1 ? 1 : (gj == 2 ? 2 : (gj == 3 ? 4 : 8));
      col(cINK, 0.2); floorring(sx, sy, frad, 1);
      sx + frad < roomW && frad * pS > 14 ? ( PJ(sx + frad, sy, 0); col(cINK, 0.45); gfx_setfont(4); sprintf(#w1, "%d m", frad); txt(#w1, qx + 3, qy - 6, 0); );
      gj += 1;
    );
  );
  isfree && vopt[67] >= 0.5 ? ( 0; ) : darkmode ? (
    // night sky: moon + stars (fixed pseudo-random positions)
    gi = 0;
    loop(46,
      stx = VX + 14 + ((gi * 7919) % 997) / 997 * (VW - 28); sty = VY + 34 + ((gi * 104729) % 991) / 991 * (VH * 0.25);
      col(0xFFFFFF, 0.35 + ((gi * 31) % 7) / 10); CI(stx, sty, 0.8 + ((gi * 13) % 3) * 0.5, 1); gi += 1;
    );
    col(0xF2C94C, 1); CI(VX + VW - 52, VY + 58, 14, 1); col(cSURF, 1); CI(VX + VW - 45, VY + 53, 12, 1);
  ) : (
    col(0xFFD66B, 0.22); CI(VX + VW - 52, VY + 58, 24, 1); col(0xFFD66B, 0.35); CI(VX + VW - 52, VY + 58, 17, 1); col(0xFFD66B, 1); CI(VX + VW - 52, VY + 58, 12, 1);   // flat sun, no rays
  );
);

  // dimension lines: width (front edge), depth (right floor edge), height (left back edge) – drag a label = resize the room; in the free field the area (width, depth)
  vedPend && vedId >= 321 && vedId <= 323 ? (
    vedId == 321 ? setp(2, min(40, max(0.8, vedVal / rs))) : (vedId == 322 ? setp(3, min(40, max(0.8, vedVal / rs))) : setp(4, min(15, max(0.8, vedVal / rs))));
    vedPend = 0;
  );
  col(cMUTED, 0.8);
  PJ(0, roomD, 0); hx0 = qx; hy0 = qy + 13; PJ(roomW, roomD, 0); hx1 = qx;
  !sidevOn || frontv ? ( LN(hx0, hy0, hx1, hy0); LN(hx0, hy0 - 3, hx0, hy0 + 3); LN(hx1, hy0 - 3, hx1, hy0 + 3); );
  sprintf(#w1, "%.2f m", roomW); strcpy(#dmw, #w1); gfx_setfont(1); dmww = tws(#w1) + 8; dmwx = min(max((hx0 + hx1) / 2, VX + 10 + dmww / 2), VX + VW - 10 - dmww / 2); dmwy = min(hy0, VY + VH - (vtab >= 2 ? 4 : 26) - 8);   // labels stay in the view
// the width label never sits on the source pill + Handles chip (lower left of the view, narrow views): it moves right of it
  vtab < 2 && !lrnTL && VW >= 230 && VH >= 170 && dmwy + 7 > VY + VH - 52 && dmwy - 7 < VY + VH - 28 && dmwx - dmww / 2 < VX + 280 ? dmwx = min(VX + VW - 10 - dmww / 2, VX + 280 + dmww / 2);
  vedit == 321 ? vedplace(dmwx, dmwy + 16, 56);
  sidevOn && !frontv ? ( dmwx = -9999; dmww = 0; ) : dimlab(#w1, dmwx, dmwy, 1, drag == 7 || inbox(dmwx - dmww / 2, dmwy - 7, dmww, 14));   // the width is not visible from the side
  col(cMUTED, 0.8);
  PJ(roomW, 0, 0); hx0 = qx + 12; hy0 = qy; PJ(roomW, roomD, 0); hx1 = qx + 12; hy1 = qy;
  frontv ? 0 : sidevOn ? ( PJ(0, 0, 0); hx0 = qx; hy0 = qy + 13; PJ(0, roomD, 0); hx1 = qx; hy1 = hy0; LN(hx0, hy0, hx1, hy1); LN(hx0, hy0 - 3, hx0, hy0 + 3); LN(hx1, hy1 - 3, hx1, hy1 + 3); ) :
  ( LN(hx0, hy0, hx1, hy1); LN(hx0 - 3, hy0, hx0 + 3, hy0); LN(hx1 - 3, hy1, hx1 + 3, hy1); );
  sprintf(#w1, "%.2f m", roomD); strcpy(#dmd, #w1); gfx_setfont(1); dmdw = tws(#w1) + 8; dmdx = frontv ? -9999 : sidevOn ? min(max((hx0 + hx1) / 2 - dmdw / 2, VX + 10), VX + VW - 10 - dmdw) : min((hx0 + hx1) / 2 + 4, VX + VW - 10 - dmdw); dmdy = min(max((hy0 + hy1) / 2, VY + 32), VY + VH - 34);
  vedit == 322 ? vedplace(dmdx + dmdw / 2, dmdy + 16, 56);
  frontv ? ( dmdw = 0; ) : dimlab(#w1, dmdx, dmdy, 0, drag == 8 || inbox(dmdx, dmdy - 7, dmdw, 14));   // (the depth is not visible from the front)
  !isfree ? (   // (the free field has no height)
    col(cMUTED, 0.8);
    PJ(0, 0, 0); hx0 = qx - 10; hy0 = qy; PJ(0, 0, roomH); hy1 = qy;
    !planv ? ( LN(hx0, hy0, hx0, hy1); LN(hx0 - 3, hy0, hx0 + 3, hy0); LN(hx0 - 3, hy1, hx0 + 3, hy1); );
    sprintf(#w1, "%.2f m", roomH); strcpy(#dmh, #w1); gfx_setfont(1); dmhw = tws(#w1) + 8; dmhx = max(hx0 - 4, VX + 10 + dmhw); dmhy = min(max((hy0 + hy1) / 2, VY + 32), VY + VH - 34);
    vedit == 323 ? vedplace(dmhx - dmhw / 2, dmhy + 16, 56);
    !planv ? dimlab(#w1, dmhx, dmhy, 2, drag == 9 || inbox(dmhx - dmhw, dmhy - 7, dmhw, 14));
  ) : ( dmhx = -9999; dmhw = 0; );

// ---- waves: wavefronts spreading from the source (direct, blue) and from the 4 wall images (reflections, violet),
//      drawn on the floor like ripples seen from above ----
wavesOn ? (
  wtm = time_precise(); wslw = 1 + max(0, max(roomW, roomD) - 8) / 12; WP = 0.3 * min(2, wslw);   // large rooms slower (and fronts further apart)
  vvis = max(roomW, roomD) / (1.1 * wslw);                                   // visual speed: crosses a small room in ~1 s, a large one in up to ~4 s
  rmax = 1.6 * sqrt(roomW * roomW + roomD * roomD);
  we0 = floor(wtm / WP);
  // a new wavefront only while signal comes in (> -59 dBFS, after the input gain); louder input = stronger front
  we0 != lastwe ? (
    wlin = time_precise() - lastblk > 0.3 ? -150 : dbs(max(pkIL, pkIR));   // audio not running: no new wavefronts
    wlv[we0 & 31] = wlin > -59 ? max(0.18, lin2f(wlin, -48, 0)) : 0;
    wlin > -59 ? wlast = wtm;
    lastwe = we0;
  );
  wlife = rmax / vvis;
  // reflections live in the source's part of the room (the barrier reflects too)
  rys0 = bon ? (sideS ? yb : 0) : 0; rys1 = bon ? (sideS ? roomD : yb) : roomD;
  raN = bon ? (sideS ? abB : kN) : aN; raS = bon ? (sideS ? aS : abB) : aS;
  raE = bon && !sideS ? kE : aE; raW = bon && !sideS ? kW : aW;
  wdl = sqrt(sqr(lx - sx) + sqr(ly - sy)); whit = 0;
  // DELAY (Expert, on): the echoes leave the source as magenta fronts behind each wavefront. Their real distance would be
  // c x delay time (17 m for 50 ms) - far outside the room - so the spacing is shortened: the first echo follows at
  // 35 % of the maximum wave radius behind it, the next ones in proportion to their delay time
  den = 0;
  !basic && gdon ? (
    dfLw = slider75 / 100; dfRw = gdlf ? dfLw : slider76 / 100;
    dgLw = slider77 <= -47.9 ? 0 : 10 ^ (slider77 / 20); dgRw = gdlv ? dgLw : (slider78 <= -47.9 ? 0 : 10 ^ (slider78 / 20));
    gdmL ? ( dgLw = 0; ); gdmR ? ( dgRw = 0; );
    dtw = 0; dvw = 1; dnLw = 0; dnRw = 0; gj = 0;
    loop(12,
      gdpp ? ( dew2 = gj % 2; dtw += dew2 == 0 ? gtL : gtR; dawv = dew2 == 0 ? dgLw * dvw : dgRw * dvw; dew2 == 1 ? ( dvw *= sqrt(dfLw * dfRw); ); )
           : ( dew2 = gj % 2; dew2 == 0 ? ( dnLw += 1; dtw = dnLw * gtL; dawv = dgLw * dfLw ^ (dnLw - 1); ) : ( dnRw += 1; dtw = dnRw * gtR; dawv = dgRw * dfRw ^ (dnRw - 1); ); );
      dawv > 0.03 && den < 8 ? ( dwT[den] = dtw; dwA2[den] = min(1, dawv); den += 1; );
      gj += 1;
    );
    dt1 = 1000000; gj = 0; loop(den, dt1 = min(dt1, dwT[gj]); gj += 1; );
  );
  gi = 0; wfn = 0;
  loop(ceil(rmax / vvis / WP),
    wr = (wtm - (we0 - gi) * WP) * vvis;
    wr > 0.05 && wr < rmax ? (
      wal = wlv[(we0 - gi) & 31] / (1 + wr / 4) * (1 - wr / rmax);
// the fronts are collected (center, radius, strength, y range, kind) - the AIR PARTICLES below show them
      wal > 0.001 && wfn < 60 ? ( wfq = WFB + wfn * 8; wfq[0] = sx; wfq[1] = sy; wfq[2] = wr; wfq[3] = wal; wfq[4] = 0; wfq[5] = roomD; wfq[6] = 0; wfn += 1; );
      !isfree && wal > 0.001 ? (
        gj = 0;
        // 4 mirrored sources (wall images): 0 = back, 1 = front, 2 = left, 3 = right wall; strength scaled by sqrt(1 - alpha of that wall)
        loop(4,
          wfn < 60 ? ( wfq = WFB + wfn * 8;
            wfq[0] = gj < 2 ? sx : (gj == 2 ? -sx : 2 * roomW - sx); wfq[1] = gj == 0 ? 2 * rys0 - sy : (gj == 1 ? 2 * rys1 - sy : sy);
            wfq[2] = wr; wfq[3] = wal * sqrt(1 - (gj == 0 ? raN : (gj == 1 ? raS : (gj == 2 ? raW : raE)))); wfq[4] = rys0; wfq[5] = rys1; wfq[6] = 1;
            wfq[3] > 0.001 ? wfn += 1; );
          gj += 1; );
      );
      gj = 0;
      loop(den,
        wre = wr - 0.35 * rmax * dwT[gj] / dt1;
        wre > 0.05 && wre < rmax ? (
          wae = wlv[(we0 - gi) & 31] / (1 + wre / 4) * (1 - wre / rmax) * dwA2[gj];
          wae > 0.001 ? wring(sx, sy, 0, wre, 0, roomD, min(1, 1.6 * wae), cDLS);
        );
        gj += 1;
      );
      abs(wr - wdl) < vvis * 0.06 ? whit = max(whit, wal);           // the direct front reaches the listener
    );
    gi += 1;
  );
// AIR PARTICLES, fine: the still air as a faint scatter of single pixels, and every front as a thin band of densely
  // crowded particles (scattered across the band like a bell curve, as many as the front is strong). Two clearly different
  // colors: blue = the direct sound, violet = reflections
  wfn > 0 ? (
    wsg = max(0.025, max(roomW, roomD) * 0.007);
    wcD = darkmode ? 0x6FA8FF : 0x2F6FED; wcR = cAMB;
    wpn = 2400; wnx = max(8, floor(sqrt(wpn * roomW / max(0.5, roomD)) + 0.5)); wny = max(8, floor(wpn / wnx + 0.5));
// the still air (2400 points) is projected only when the room, the projection or the clip changed - a key of everything PJ and the clip use;
    // the screen points go to WSA (x, y; -1 = outside), the animated frames draw them with two reads instead of a projection each
    wsaKey = wnx * 7.3 + wny * 11.1 + roomW * 101.7 + roomD * 103.9 + pX0 * 3.1 + pY0 * 3.7 + pS * 5.3 + pKX * 5.9 + pKY * 6.1 + pSZ * 6.7 + gox * 7.1 + goy * 7.9 + gs * 8.3 + gclp * 9.1 + clX0 * 1.3 + clX1 * 1.7 + clY0 * 1.9 + clY1 * 2.3;
    wsaKey != wsaLast ? ( wsaLast = wsaKey; gi = 0;
      loop(wnx * wny,
        wpx = ((gi % wnx) + 0.5 + 0.8 * (((gi * 7919) % 997) / 997 - 0.5)) * roomW / wnx; wpy = (floor(gi / wnx) + 0.5 + 0.8 * (((gi * 104729) % 991) / 991 - 0.5)) * roomD / wny;
        PJ(wpx, wpy, 0); gclp && (qx < clX0 || qx > clX1 || qy < clY0 || qy > clY1) ? ( WSA[gi * 2] = -1; ) : ( WSA[gi * 2] = floor(gox + qx * gs); WSA[gi * 2 + 1] = floor(goy + qy * gs); );
        gi += 1; ); );
    col(cMUTED, 0.22); gi = 0;
    loop(wnx * wny, WSA[gi * 2] >= 0 ? gfx_rect(WSA[gi * 2], WSA[gi * 2 + 1], 1, 1); gi += 1; );
    wtot = 0; gj = 0;
    loop(wfn, wfq = WFB + gj * 8;
      // particle count ~ circumference in pixels x strength (max 2400 per front, 11000 per frame)
      wcnt = floor(2 * $pi * wfq[2] * pS * gs * 1.6 * min(1, wfq[3] * 2.5)); wcnt = max(0, min(min(2400, wcnt), 11000 - wtot)); wtot += wcnt;
      col(wfq[6] ? wcR : wcD, 1); wal0 = min(1, wfq[3] * 2);
      k = 0;
      loop(wcnt,
        // golden-ratio angle steps spread the particles evenly around the ring; wgo = bell-shaped radial offset (two hashes summed) in units of the band width wsg
        wa = k * 0.6180339887 + gj * 0.137; wa = 2 * $pi * (wa - floor(wa));
        wgo = ((((k * 7919 + gj * 104729) % 997) / 997) + (((k * 6271 + gj * 3571) % 991) / 991) - 1) * 2.2;
        wprr = wfq[2] + wgo * wsg; wpx = wfq[0] + wprr * cos(wa); wpy = wfq[1] + wprr * sin(wa);
        wpx >= 0 && wpx <= roomW && wpy >= wfq[4] && wpy <= wfq[5] ? (
          PJ(wpx, wpy, 0);
          gclp && (qx < clX0 || qx > clX1 || qy < clY0 || qy > clY1) ? 0 : ( gfx_a = wal0 * (1 - abs(wgo) / 2.5); gfx_rect(floor(gox + qx * gs), floor(goy + qy * gs), 1, 1); );
        );
        k += 1; );
      gj += 1; );
  );
  whit > 0 ? ( PJ(lx, ly, lz); col(cGLS, min(1, 2 * whit)); CI(qx, qy, 13, 0); CI(qx, qy, 14, 0); );
  col(cMUTED, 1); gfx_setfont(4); sprintf(#w1, "waves %.0f× slower than real", C_SND / vvis); den > 0 ? strcat(#w1, "  ·  delay echoes (magenta) closer than real"); txt(#w1, VX + VW - 10, VY + VH - 32, 2);   // (right - the source pill sits left)
);

// direct sound (blue)
showdirect ? (
  PJ(sx, sy, sz); hx0 = qx; hy0 = qy; PJ(lx, ly, lz);
  col(cGLS, 0.85); LNW(hx0, hy0, qx, qy, 1);   // ball center to ball center
  // label beside the line: above it when the line is mostly horizontal, to its right when mostly vertical
  sprintf(#w2, "%.2f m", rdir); col(cSRC, 1); gfx_setfont(4);
  abs(qx - hx0) > abs(qy - hy0) ? txtplate(#w2, (hx0 + qx) / 2, (hy0 + qy) / 2 - 10, 1)
                                : txtplate(#w2, (hx0 + qx) / 2 + 10, (hy0 + qy) / 2, 0);
);

// objects, back to front
abs(sx - lx) < 0.02 && abs(sy - ly) < 0.02 ? ( bon ? drawbarrier(); drawcombined(); ) : (
  bon ? (
    // back to front: objects behind the barrier, the barrier, objects in front of it
    !sideS && !sideL ? ( sy <= ly ? ( drawsource(); drawlistener(); ) : ( drawlistener(); drawsource(); ); drawbarrier(); ) : (
      sideS && sideL ? ( drawbarrier(); sy <= ly ? ( drawsource(); drawlistener(); ) : ( drawlistener(); drawsource(); ); ) : (
        !sideS ? ( drawsource(); drawbarrier(); drawlistener(); ) : ( drawlistener(); drawbarrier(); drawsource(); );
      );
    );
  ) : ( sy <= ly ? ( drawsource(); drawlistener(); ) : ( drawlistener(); drawsource(); ); );
  // side / front view: when one ball hides the other (same depth / same left-right), the hidden one shows as a thin ring on top
  sidevOn && !(abs(sx - lx) < 0.02 && abs(sy - ly) < 0.02) ? (
    PJ(sx, sy, sz); gbx = qx; gby = qy; PJ(lx, ly, lz);
    sqr(qx - gbx) + sqr(qy - gby) < 196 ? ( (frontv ? sy <= ly : sx <= lx) ? ( col(cSRC, 1); CI(gbx, gby, 11, 0); CI(gbx, gby, 11.8, 0); ) : ( col(cLIS, 1); CI(qx, qy, 11, 0); CI(qx, qy, 11.8, 0); ); );
  );
  ffnR = ffnB = 0; !basic && vopt[19] >= 0.5 ? ( ffnR = VX + 8 + 172; ffnB = VY + 78; );   // ffnR: right end of the notes in the top left corner (info window, loudness notes)
  !learn ? lhsign(); lhvis = !learn && lhvis; elsign();
  ganupd(); mtHv = 0; apHv = 0; ffHv = 0; mtEg > 0.001 ? ( mtSon ? mtpath(1); mtLon ? mtpath(2); !basic && !learn ? mtsign(); );   // the motion path and its sign
  mtE <= 0.001 && slider100 < 0.5 && !basic && !learn && (tabm ? tabsel == 14 : secopen(14)) ? ( floor(slider101 + 0.5) != 1 ? mtghost(1); floor(slider101 + 0.5) != 0 ? mtghost(2); );   // ghost path
  !basic && !learn && floor(slider107 + 0.5) == 1 && (floor(slider102 + 0.5) == 0 || floor(slider102 + 0.5) == 5) && (tabm ? tabsel == 14 : secopen(14)) ? apmark();
  ffmark();
  rflmark();
  // the title pill sits in the top left corner of the view (14 px from the left edge, under the view's chip row); a note
  // in that corner (loudness, info window) pushes it below the note - never in the middle, where the delay plate and the
  // Motion sign live
  isfree ? (
    gfx_setfont(1); ftw = tws("FREE FIELD") + 16; ftx = VX + 14 + ftw / 2; fty = ffnR > 0 ? ffnB + 6 : VY + 30;
    col(cSURF, 0.8); RR(ftx - ftw / 2, fty, ftw, 16, 8); col(cMUTED, 1); txt("FREE FIELD", ftx, fty + 8, 1);
  );   // the title on a pill
);
// ROOM OFF (bypass): veil over the view + message (the room engine does not run)
rbyp ? (
// room off = a stereo panner - the walls are faded, only the source's left / right place counts (the listener rests)
  col(cSURF, 0.45); R(VX + 1, VY + 24, VW - 2, VH - 48);
  gfx_setfont(7); bpw = tws("ROOM OFF – stereo panner: drag the source left / right") + 20;
  col(cSURF, 0.9); RR(VX + 14, VY + 30, bpw, 18, 6); col(cBORD, 1); RRO(VX + 14, VY + 30, bpw, 18, 6);   // (top: the source pill sits at the bottom)
  col(cTEXT, 1); txt("ROOM OFF – stereo panner: drag the source left / right", VX + 24, VY + 39, 0);
)
);
// Room view overlays: info window, delay plate, bottom row (Snap, Link, Lock, Aim), and the back-part material chip
function gfxp2()
(
// INFO window (button "i" in the heading, off by default): distance, critical distance, direct / room ratio
vopt[19] >= 0.5 && !rbyp && !basic ? (
  ldir = sqr(tapG[0]); lrm = sqr(erg) * erE + (isfree ? 0 : sqr(lateg) * tailE);
  ldrr = 10 * log10(max(0.0000000001, ldir) / max(0.0000000001, lrm));
  lnx = VX + 18; lny = VY + 46; lnl = 17; infw = 172; lnh = isfree ? 2 * lnl + 14 : 3 * lnl + 14;
  col(cINK, 0.10); RR(VX + 9, VY + 32, infw, lnh, 7);
  col(cSURF, 1); RR(VX + 8, VY + 30, infw, lnh, 7); col(cBORD, 1); RRO(VX + 8, VY + 30, infw, lnh, 7);
  col(cMUTED, 1); gfx_setfont(13); txt("Distance", lnx, lny, 0);
  sprintf(#w1, "%.2f m", rdir); col(cTEXT, 1); gfx_setfont(14); txt(#w1, lnx + infw - 20, lny, 2);
  !isfree ? (
    col(cMUTED, 1); gfx_setfont(13); txt("Critical distance", lnx, lny + lnl, 0);
    sprintf(#w1, "%.2f m", rc * sqrt(QDIR)); col(cTEXT, 1); gfx_setfont(14); txt(#w1, lnx + infw - 20, lny + lnl, 2);
  );
  col(cMUTED, 1); gfx_setfont(13); txt("Direct / room", lnx, lny + (isfree ? 1 : 2) * lnl, 0);
  sprintf(#w1, "%+.1f dB", ldrr); col(ldrr >= 0 ? cSRC : cRVTT, 1); gfx_setfont(14); txt(#w1, lnx + infw - 20, lny + (isfree ? 1 : 2) * lnl, 2);
  tipbox(VX + 8, VY + 30, infw, lnh, "Distance source → listener; critical distance = where direct sound and room sound are equally loud (the circle in Paths); Direct / room > 0 dB: close and clear, < 0 dB: far and roomy");
);
// DELAY plate above the SOURCE plate (Expert, delay on, "3D" on in the delay heading) - moves beside the info window
!basic && gdon && vopt[20] < 0.5 && dpok && !rbyp ? (
  gdtm ? ( sprintf(#w1, "DELAY  L %s · R %s · %.1f BPM", dnname(slider72), dnname(gdlt ? slider72 : slider73), gbpm); )
       : ( sprintf(#w1, "DELAY  L %.0f · R %.0f ms", gtL, gtR); );
  gfx_setfont(6); dpw = tws(#w1) + 8; dpx = dpqx - dpw / 2; dpy = dpqy - 16;
  dpy < VY + 32 ? (                                                   // no room above (source near the top): beside the SOURCE plate
    dpy = dpqy; dpx = dpqx + dpsw / 2 + 4;
    dpx + dpw > VX + VW - 4 ? ( dpx = dpqx - dpsw / 2 - 4 - dpw; );
  );
  vopt[19] >= 0.5 ? ( dpx < VX + 8 + infw + 4 && dpy + 7 > VY + 30 && dpy - 7 < VY + 30 + lnh ? ( dpx = VX + 8 + infw + 6; ); );   // info window: move right of it
  dpx = max(VX + 4, min(dpx, VX + VW - 4 - dpw));
  col(cDLS, 1); txtplate(#w1, dpx, dpy, 0);
);
// bottom row of the room view: Snap, Link, Lock, hint, zoom
gclp = 0;   // (the tool row is not part of the clipped room drawing)
BRY = VY + VH - 13;
!basic ? (
col(cMUTED, 1); gfx_setfont(4); txt("Snap", VX + 10, BRY, 0);
rbtn(VX + 40, BRY, 5.5, "S", 0, cTEXT, "Snap the source to the exact center of the room (same as double-click)") ? moveobj(1, roomW / 2 / rs, roomD / 2 / rs);
rbtn(VX + 54, BRY, 5.5, "L", 0, cTEXT, "Snap the listener to the exact center of the room (same as double-click)") ? moveobj(2, roomW / 2 / rs, roomD / 2 / rs);
col(cMUTED, 1); gfx_setfont(4); txt("Link", VX + 70, BRY, 0);
rbtn(VX + 97, BRY, 5.5, "", linksl, cLIS, "Link: source and listener move together (position and height), keeping their distance") ? ( vopt[5] = linksl ? 0 : 1; );
col(linksl ? cSURF : cMUTED, 1);
RRO(VX + 97 - 4.4, BRY - 2.4, 5.2, 3.2, 1.6); RRO(VX + 97 - 0.8, BRY - 0.8, 5.2, 3.2, 1.6);
// LOCK: source / listener / barrier / door-window stay where they are (mouse, wheel, knobs, snap) - presets and automation still move them
col(cMUTED, 1); gfx_setfont(4); txt("Lock", VX + 113, BRY, 0);
// vopt[9] is the lock bitmask: 1 = source, 2 = listener, 4 = barrier, 8 = door / window (lkb = its value at the start of the frame)
rbtn(VX + 142, BRY, 5.5, "S", lockS, cSRC, lockS ? "Source locked – click to unlock" : "Lock the source: it can no longer be moved by mistake") ? ( vopt[9] = lkb + (lockS ? -1 : 1); );
rbtn(VX + 156, BRY, 5.5, "L", lockL, cLIS, lockL ? "Listener locked – click to unlock" : "Lock the listener: it can no longer be moved by mistake") ? ( vopt[9] = lkb + (lockL ? -2 : 2); );
rbtn(VX + 170, BRY, 5.5, "B", lockB, cBARTT, lockB ? "Barrier position locked – click to unlock" : "Lock the position of the barrier (handle in the room and Pos knob)") ? ( vopt[9] = lkb + (lockB ? -4 : 4); );
rbtn(VX + 184, BRY, 5.5, floor(slider41 + 0.5) == 2 ? "W" : "D", lockE, cELK, lockE ? "Door / window locked – click to unlock" : "Lock the door / window: its place in the wall and how far it is open (mouse, wheel, Open knob)") ? ( vopt[9] = lkb + (lockE ? -8 : 8); );
) : (
  col(cMUTED, 1); gfx_setfont(4); txtfit(isfree ? "Drag = move  ·  Ctrl/Cmd + drag = on a line  ·  Shift + drag = height  ·  Alt+Shift + drag = stereo width  ·  double-click = center" : "Drag = move  ·  Ctrl/Cmd + drag = on a line  ·  Shift + drag = height  ·  Alt+Shift + drag = stereo width  ·  double-click = center  ·  front corner = whole room  ·  drag a room edge = room size", VX + 10, BRY, VW - 60);
);
// BACK PART material in the enlarged view (only with a barrier): own material for the part between back wall and barrier
bon && !isfree && zoomv ? (
  col(cMUTED, 1); gfx_setfont(4); txt("Back part", kbx - 6, kby + 7, 2);
  kmsel = floor(slider46 + 0.5);
  kmsel > 0 ? sprintf(#w1, "%s  α %.2f", bkname(kmsel), g_bkA(kmsel)) : strcpy(#w1, bkname(0));
  ddbox(kbx, kby, kbw, 14, #w1, 1, kmenu && kmsrc == 2);
  tipbox(kbx, kby, kbw, 14, "Material of the back part (back wall to barrier): own walls, floor and ceiling – e.g. a tiled bathroom next to a dry room");
);
// FACING is a sound parameter: gray chips like every control
!basic ? (
VW >= 530 ? ( col(cMUTED, 1); gfx_setfont(4); txt("Aim", VX + VW - 311, BRY, 2); );   // in a narrow room view the label would run into the Lock buttons
VW >= 500 ? (
segtrack(VX + VW - 306, BRY - 8, 86, 16);
segp(VX + VW - 306, BRY - 8, 86, 16, 2, 0, "Fixed", slider63 >= 0.5, cTEXT) ? ( slider63 < 0.5 ? ( setp(63, 1); setp(61, 0); ); );
segp(VX + VW - 306, BRY - 8, 86, 16, 2, 1, "Listener", slider63 < 0.5, cTEXT) ? ( slider63 >= 0.5 ? ( setp(63, 0); setp(61, 0); ); );
tipbox(VX + VW - 306, BRY - 8, 43, 16, "Fixed: the source points in one direction of the room (the arrow) – walk behind it and you hear its back. Alt+drag the source to turn it");
tipbox(VX + VW - 263, BRY - 8, 43, 16, "Listener: the source always aims at the listener (like a talker who turns towards you) – Src angle turns it away");
);   // too narrow for the chips next to the Lock buttons - the Aim chips give way
col(cMUTED, 1); gfx_setfont(4); txt("Facing", VX + VW - 156, BRY, 2);
segtrack(VX + VW - 150, BRY - 8, 120, 16);   // + Path (the listener looks where it walks)
segp(VX + VW - 150, BRY - 8, 120, 16, 3, 0, "Fixed", slider16 >= 0.5 && slider16 < 1.5, cTEXT) ? ( slider16 = 1; autom(32768); );
segp(VX + VW - 150, BRY - 8, 120, 16, 3, 1, "Source", slider16 < 0.5, cTEXT) ? ( slider16 = 0; autom(32768); );
segp(VX + VW - 150, BRY - 8, 120, 16, 3, 2, "Path", slider16 >= 1.5, cTEXT) ? ( slider16 = 2; autom(32768); );
tipbox(VX + VW - 70, BRY - 8, 40, 16, "Path: while the listener moves (Motion), it looks where it walks – like a camera on a walk; standing still it keeps the last direction");
tipbox(VX + VW - 150, BRY - 8, 40, 16, "Fixed: the listener looks in one direction (default: the back wall, like a camera) – sounds are placed left/right in the stereo field. Alt+drag turns the head, Alt+double-click on the source sets it straight ahead again");
tipbox(VX + VW - 110, BRY - 8, 40, 16, "Source: the listener always turns to the source – the direct sound is then always in the center (with a barrier in between: to the opening the sound comes through)");
// RESET BUTTONS: Aim = the source points to the front again (Aim Fixed, Src angle 0); Facing = the listener looks straight ahead (Fixed, Lis angle 0)
rsAh = VW >= 500 && sqr(mx - (VX + VW - 208)) + sqr(my - BRY) < 64;
VW >= 500 ? ( col(rsAh ? cTEXT : cMUTED, 1); ic_reset(VX + VW - 208, BRY);
  rsAh && click ? ( snap_take(); setp(63, 1); setp(61, 0); snap_notify(); click = 0; );
  rsAh ? ( tipon = 1; tipforce = 0; tipstr = "Reset Aim: the source points to the front again (Aim Fixed, Src angle 0°)"; tipx = VX + VW - 208; tipy = BRY + 14; ); );
rsFh = sqr(mx - (VX + VW - 18)) + sqr(my - BRY) < 64;
col(rsFh ? cTEXT : cMUTED, 1); ic_reset(VX + VW - 18, BRY);
rsFh && click ? ( snap_take(); setp(16, 1); setp(17, 0); snap_notify(); click = 0; );
rsFh ? ( tipon = 1; tipforce = 0; tipstr = "Reset Facing: the listener looks straight ahead again (Facing Fixed, Lis angle 0°)"; tipx = VX + VW - 18; tipy = BRY + 14; );
) : (
  // EXPERT SETTINGS ACTIVE: hidden Expert settings that change the sound - a click lists them (Reset to Basic / Open in Expert)
  // xpf[k] = 1 when the hidden Expert setting k (name: xpname(k)) differs from its default; eqq = any Source EQ gain (sliders 47 … 59) is not 0
  btq = floor(slider41 + 0.5); eqq = 0; i = 47; loop(13, abs(slider(i)) > 0.0005 ? eqq = 1; i += 1; );
  xpf[0] = slider65 >= 0.5;
  xpf[1] = slider39 < 0.5 && (slider24 >= 0.5 || slider27 >= 0.5);
  xpf[2] = !(abs(slider5 - slider6) < 0.001 && abs(slider5 - slider7) < 0.001 && abs(slider5 - slider8) < 0.001);
  xpf[3] = eqq;
  xpf[4] = abs(slider62 - 100) > 0.05;
  xpf[5] = abs(slider20) > 0.05 || abs(slider21) > 0.05;   // predelay is not an Expert setting - Basic has it (Room card, auto = follows the room)
  xpf[6] = abs(slider61) > 0.05 || slider63 < 0.5;
  xpf[7] = slider16 < 0.5 || abs(slider17) > 0.05;
  xpf[8] = slider19 < 0.5 && !isfree;
  xpf[9] = btq > 0 && (abs(slider44 - bmatrw(btq, 1)) > 0.05 || slider46 >= 0.5);
  xpf[10] = abs(slider40) > 0.05;
  xpf[11] = slider60 >= 0.5 || slider23 < 0.5;
  xpf[12] = slider64 > 0.0005;
  xpf[13] = vopt[18] > 0.5;
  xpf[14] = slider88 < 99.95 || slider89 < 99.95 || slider90 < 99.95 || slider91 < 99.95 || slider92 < 99.95;
  xpf[15] = slider100 >= 0.5;
  xpn = 0; strcpy(#xpt, "Expert settings active: "); k = 0;
  loop(16, xpf[k] ? ( xpn > 0 ? strcat(#xpt, " · "); strcat(#xpt, xpname(k)); xpn += 1; ); k += 1; );
  xpn > 0 ? (
    0;   // drawn in the strip (fixed place), see the strip
  ) : ( xpon = 0; );
);
actcol = cTEXT;
!isfree ? (
  inbox(dmwx - dmww / 2, dmwy - 7, dmww, 14) ? tipbox(dmwx - dmww / 2, dmwy - 7, dmww, 14, "Room width – drag left / right to change it (Shift = fine), double-click = type a value");
  inbox(dmdx, dmdy - 7, dmdw, 14) ? tipbox(dmdx, dmdy - 7, dmdw, 14, "Room depth – drag up / down to change it (Shift = fine), double-click = type a value");
  inbox(dmhx - dmhw, dmhy - 7, dmhw, 14) ? tipbox(dmhx - dmhw, dmhy - 7, dmhw, 14, "Room height – drag up / down to change it (Shift = fine), double-click = type a value");
);
inbox(VX, VY, VW, VH) ? (
  cnv = drag == 12 ? cnI : (!isfree && !sidevOn && !drag && !nearmth() && !spHov ? cornerhit() : 0);   // (no corner handle in the side / front view)
  cnv ? (
    PJ(cnv == 2 ? roomW : 0, roomD, 0); col(cRMTT, 1); CIF(qx, qy, 4.5, 1); col(cSURF, 1); CIF(qx, qy, 2, 1);
    drag != 12 ? ( tipon = 1; tipx = mx; tipy = my - 18;
      tipstr = "Room corner – drag: left / right = width, up / down = depth · Ctrl/Cmd+drag = all in proportion · Alt+drag = height · Shift = fine"; );
  );
  ehv = cnv ? -1 : (drag == 11 ? -2 : (!isfree && !drag && !nearmth() && drag != 21 && !spHov ? edgehit() : -1));
  drag == 11 ? edgedraw(eIdx, cRMTT);
  ehv >= 0 ? (
    edgedraw(ehv, cRMTT);
    tipon = 1; tipx = mx; tipy = my - 18;
    tipstr = sidevOn ? (floor(ehv / 4) == 2 ? (frontv ? "Room width – drag left / right (Shift = fine) · Ctrl/Cmd = size" : "Room depth – drag left / right (Shift = fine) · Ctrl/Cmd = size") : "Room height – drag up / down (Shift = fine) · Ctrl/Cmd = size") :
           floor(ehv / 4) == 0 ? "Room width – drag along the line or mouse wheel (Shift = fine) · Alt = height · Ctrl/Cmd = size"
           : (floor(ehv / 4) == 1 ? "Room depth – drag along the line or mouse wheel (Shift = fine) · Alt = height · Ctrl/Cmd = size"
                                  : "Room height – drag up / down or mouse wheel (Shift = fine) · Ctrl/Cmd = size");
  ) :
  !basic && !isfree && !hidew && !spHov && (inface(1) || inface(2) || inface(4)) ? ( tipon = 1; tipx = mx; tipy = my - 18;
    tipstr = "Wall: hatching shows absorption α – click to select it for the α knob and materials"; );
  PJ(sx, sy, sz); tipbox(qx - 12, qy - 12, 24, 24, basic ? "Source: drag = move  ·  Ctrl/Cmd+drag = on a line  ·  Shift+drag or mouse wheel = height  ·  double-click = center of the room" : "Source: drag = move  ·  Ctrl/Cmd+drag = on a line (left / right or forward / back)  ·  Shift+drag or mouse wheel = height  ·  Alt+drag = turn (where it points)  ·  double-click = center of the room");
  PJ(lx, ly, lz); tipbox(qx - 12, qy - 12, 24, 24, basic ? "Listener: drag = move  ·  Ctrl/Cmd+drag = on a line  ·  Shift+drag or wheel = height  ·  double-click = center" : "Listener: drag = move  ·  Ctrl/Cmd+drag = on a line (left / right or forward / back)  ·  Shift+drag or wheel = height  ·  Alt+drag = turn  ·  double-click = center");
);

bon ? (
  PJ(brHX, yb, brH); tipbox(qx - 7, qy - 7, 14, 14, "Barrier: drag = move it between back and front wall");
  brEon && bew <= 0 ? ( PJ(brEX, yb, brEZ); tipbox(qx - 6, qy - 12, 12, 12, "Partition: drag = move it sideways"); );
  bew > 0 ? (
    PJ(bex0, yb, bez1); ex0s = qx; ey0s = qy; PJ(bex1, yb, bez0);
    tipbox(ex0s, ey0s - 14, qx - ex0s, qy - ey0s + 14, bt == 3 ? "Door: drag = slide · Shift+drag or wheel = open / close · double-click = close" : "Window: drag = slide · Shift+drag or wheel = open / close the two sashes · double-click = close");
  );
)
);
// the stroke -> 256 points at equal TIME steps (the speed of the drawing is kept: fast and slow parts), closed into a
// loop (the way back to the start at the average speed), smoothed a little; center + size + duration in MTDP; the motion starts
function mtfinish() local(zcm, L, k, j, t, f, cxm, cym, R, mv, Tdur, Tback, Ttot, Lc, v, n, p0, p1, x0, y0, t0)
(
  mtRec = 0; n = mtRN;
  L = 0; k = 0; loop(max(0, n - 1), L += sqrt(sqr(mtRB[3 * k + 3] - mtRB[3 * k]) + sqr(mtRB[3 * k + 4] - mtRB[3 * k + 1])); k += 1; );
  n >= 4 && L > 0.3 ? (
    Tdur = mtRB[3 * n - 1] - mtRB[2];
    Tdur < 0.3 ? ( t = 0;   // (no usable timing: even speed along the line)
                k = 1; mtRB[2] = 0; loop(n - 1, t += sqrt(sqr(mtRB[3 * k] - mtRB[3 * k - 3]) + sqr(mtRB[3 * k + 1] - mtRB[3 * k - 2])) / L; mtRB[3 * k + 2] = t; k += 1; ); Tdur = 1; )
          : ( t0 = mtRB[2]; k = 0; loop(n, mtRB[3 * k + 2] -= t0; k += 1; ); );
    Lc = sqrt(sqr(mtRB[0] - mtRB[3 * n - 3]) + sqr(mtRB[1] - mtRB[3 * n - 2])); v = L / Tdur; Tback = min(Tdur, Lc / max(0.001, v)); Ttot = Tdur + max(0.05, Tback);
    k = 0; j = 0;
    loop(256,
      t = k * Ttot / 256;
      t <= Tdur ? (
        while ( j < n - 2 && mtRB[3 * j + 5] < t ? ( j += 1; 1; ) : 0; );
        f = mtRB[3 * j + 5] - mtRB[3 * j + 2] > 0.000001 ? min(1, max(0, (t - mtRB[3 * j + 2]) / (mtRB[3 * j + 5] - mtRB[3 * j + 2]))) : 0;
        mtRSm[2 * k] = mtRB[3 * j] + (mtRB[3 * j + 3] - mtRB[3 * j]) * f; mtRSm[2 * k + 1] = mtRB[3 * j + 1] + (mtRB[3 * j + 4] - mtRB[3 * j + 1]) * f;
        mtRSz[k] = mtRBz[j] + (mtRBz[j + 1] - mtRBz[j]) * f;
      ) : (
        f = (t - Tdur) / max(0.05, Ttot - Tdur);
        mtRSm[2 * k] = mtRB[3 * n - 3] + (mtRB[0] - mtRB[3 * n - 3]) * f; mtRSm[2 * k + 1] = mtRB[3 * n - 2] + (mtRB[1] - mtRB[3 * n - 2]) * f;
        mtRSz[k] = mtRBz[n - 1] + (mtRBz[0] - mtRBz[n - 1]) * f;
      );
      k += 1;
    );
    // two passes of a gentle smoothing (1 2 1, around the loop): no frame-rate jitter in the speed
    loop(2,
      x0 = mtRSm[510]; y0 = mtRSm[511]; p0 = mtRSm[0]; p1 = mtRSm[1]; k = 0;
      loop(256, j = (k + 1) % 256; f = k == 255 ? p0 : mtRSm[2 * j]; t = k == 255 ? p1 : mtRSm[2 * j + 1];
        cxm = mtRSm[2 * k]; cym = mtRSm[2 * k + 1];
        mtRSm[2 * k] = 0.25 * x0 + 0.5 * cxm + 0.25 * f; mtRSm[2 * k + 1] = 0.25 * y0 + 0.5 * cym + 0.25 * t; x0 = cxm; y0 = cym; k += 1; );
    );
    cxm = 0; cym = 0; k = 0; loop(256, cxm += mtRSm[2 * k]; cym += mtRSm[2 * k + 1]; k += 1; ); cxm /= 256; cym /= 256;
    R = 0.05; k = 0; loop(256, R = max(R, sqrt(sqr(mtRSm[2 * k] - cxm) + sqr(mtRSm[2 * k + 1] - cym))); k += 1; );
    snap_take();
    mtEg > 0.001 || slider100 >= 0.5 ? ( mtKill = 1; slider100 = 0; );
    zcm = 0; k = 0; loop(256, zcm += mtRSz[k]; k += 1; ); zcm /= 256;
    // MTDP layout (770 values): 0 … 511 = x, y per point in units of R around the center, 512 = point count, 513 = duration (s), 514 … 769 = z; MTDS keeps four such slots
    k = 0; loop(256, MTDP[2 * k] = (mtRSm[2 * k] - cxm) / R; MTDP[2 * k + 1] = (mtRSm[2 * k + 1] - cym) / R; MTDP[514 + k] = (mtRSz[k] - zcm) / R; k += 1; ); MTDP[512] = 256; MTDP[513] = Ttot; memcpy(MTDS + 770 * min(3, max(0, floor(MTDS[3080] + 0.5))), MTDP, 770);   // kept in the slot in use
    mv = floor(slider101 + 0.5);
    // the center stays put: the ball goes to the start of the drawing (it glides there)
    mv == 1 ? ( sset(13, cxm / rs); sset(14, cym / rs); sset(15, zcm); ) : ( sset(10, cxm / rs); sset(11, cym / rs); sset(12, zcm); );
    // 102 = pattern (7 = Drawn), 104 = size in m, 105 = start angle, 106 = direction; 100 = Motion on (the rate 103 follows below)
    sset(102, 7); sset(104, max(0.1, R / rs)); sset(105, 0); sset(106, 0);
    floor(slider108 + 0.5) == 0 ? sset(103, min(4, max(0.02, 1 / Ttot)));
    sset(100, 1);
    snap_notify();
  );
  mtRN = 0;
);
// Room view mouse handling: drag source/listener/walls/room edges, height, turn, snap, size labels and motion-path drawing
function gfxp3()
(
// ---------- mouse: drag = move, shift+drag = height, alt+drag = turn listener, click wall = select ----------
time_precise() - lkT < 2.5 ? ( tipon = 1; tipforce = 1; tipstr = lkWho == 2 ? "The listener is locked – unlock it with L in the Lock row under the Stage View" : "The source is locked – unlock it with S in the Lock row under the Stage View"; tipx = mx; tipy = my - 22; );
(mouse_cap & 1) ? (
  click && inbox(VX, VY, VW, VH) ? (
    PJ(sx, sy, sz); dSrc = sqrt(sqr(mx - qx) + sqr(my - qy));
    abs(sx - lx) < 0.02 && abs(sy - ly) < 0.02 ? ( dSrc = sqrt(sqr(mx - qx - 9) + sqr(my - qy - 4)); );   // together - the S ball is drawn beside the L ball
    PJ(lx, ly, lz); dLs = sqrt(sqr(mx - qx) + sqr(my - qy));
    dLs < 15 && dSrc < dLs ? ( dLs = 99; );   // the nearer ball wins (source and listener close together)
// the listener can be moved with the room engine off too (the source always could) - the position counts when the room is back on
    lockL && dLs < 15 ? ( lkT = time_precise(); lkWho = 2; ) : ( lockS && dSrc < 15 ? ( lkT = time_precise(); lkWho = 1; ); );   // a locked ball says so
// STEREO WIDTH in the view: an end of the span, or Alt+Shift on the source ball (also with no span drawn)
    swDrag = 0;
// the ball wins when the span is short (free field: a small scale)
    !devm && slider110 < 0.5 && !lockS ? (
      spOk && spHL >= 12 && dSrc >= 10 && !rflHov && !rflhh(0) && !rflhh(1) && (sqr(mx - spX1) + sqr(my - spY1) < 64 || sqr(mx - spX2) + sqr(my - spY2) < 64) ? ( swDrag = 1; swMode = 1;   // (a reflector under the handle wins)   // Alt+click = 100 %, Ctrl/Cmd+click = 0 % (mono), double-click = 100 % <-> 0 %
        swClk = 0; (mouse_cap & 16) && !(mouse_cap & 8) ? ( setp(98, 100); swClk = 1; ) : (mouse_cap & 4) ? ( setp(98, 0); swClk = 1; ) : time_precise() - swDT < 0.35 ? ( setp(98, slider98 > 50 ? 0 : 100); swClk = 1; swDT = 0; ) : ( swDT = time_precise(); );
        swClk ? ( swDrag = 0; click = 0; dSrc = dLs = 999; dBh = dBe = 99; inEl = 0; swSet = 1; ); ) : (
      (mouse_cap & 16) && (mouse_cap & 8) && dSrc < 15 ? ( swDrag = 1; swMode = 0; ); );
      swDrag ? ( swW0 = slider98; swX0 = mx; spHL0 = spTr; swD0 = max(4, abs((mx - spCx) * spUx + (my - spCy) * spUy)); dSrc = dLs = 999; dBh = dBe = 99; inEl = 0; );
    );   // room off = stereo panner - the listener rests (only the source pans) // (room off = 3D panner - the positions count again) // room off - source and listener cannot be moved (it would not change the sound)
    hitw = 0;
    !isfree && !hidew ? (
      !planv ? ( inface(1) ? hitw = 1; inface(2) ? hitw = 2; inface(4) ? hitw = 4; ) : (
        PJ(0, 0, 0); hx0 = qx; hy0 = qy; PJ(roomW, roomD, 0);
        mx >= hx0 - 4 && mx <= qx + 4 && my >= hy0 - 4 && my <= qy + 4 ? (
          abs(my - hy0) < 5 ? hitw = 1 : (abs(mx - qx) < 5 ? hitw = 2 : (abs(my - qy) < 5 ? hitw = 3 : (abs(mx - hx0) < 5 ? hitw = 4 : 0)));
        );
      );
      PJ(0, roomD, 0); hx0 = qx; hy0 = qy; PJ(roomW, roomD, 0);
      mx >= hx0 && mx <= qx && abs(my - (hy0 + (qy - hy0) * (mx - hx0) / max(1, qx - hx0))) < 5 ? hitw = 3;
    );
    zmode = (mouse_cap & 8) ? 1 : 0; dmy0 = my;
    dBh = dBe = 99; inEl = 0;
    bon ? ( PJ(brHX, yb, brH); dBh = sqrt(sqr(mx - qx) + sqr(my - qy)); brEon ? ( PJ(brEX, yb, brEZ); dBe = sqrt(sqr(mx - qx) + sqr(my - qy + 6)); ); );
    // the door / window itself: in the plane y = yb the element is an upright rectangle on screen
    bon && bew > 0 ? ( PJ(bex0, yb, bez1); ex0s = qx; ey0s = qy; PJ(bex1, yb, bez0); inEl = mx >= ex0s - 3 && mx <= qx + 3 && my >= ey0s - (planv ? 7 : 3) && my <= qy + (planv ? 7 : 3); );
    lockB ? dBh = 99; lockE ? ( dBe = 99; inEl = 0; );           // locked: neither handle nor door / window can be grabbed
// the Own spot marker wins over what lies under it (the ball it sits on, a wall)
    apBall = dLs < 15 || dSrc < 15;   // a ball under the mouse always wins over the Own spot + and the free field handle
    apH0 = !apBall && ((apHv && !mtRec && sqr(mx - apHx) + sqr(my - apHy) < 100) || (ffHv && sqr(mx - ffHx) + sqr(my - ffHy) < 100) || rflhh(0) || rflhh(1) || rflon(0) || rflon(1)); apH0 ? ( dLs = dSrc = dBh = dBe = 99; inEl = 0; );
    sidevOn && !frontv ? ( dBe = 99; inEl = 0; hitw = 0; );   // side view - the balls, the barrier line and the height are edited here
    frontv ? ( dBh = 99; hitw = 0; );   // front view - the barrier's door / window can be moved and opened, its depth not
    // double-click on source / listener: snap it to the exact center of the room (height unchanged)
    dctgt = dLs < 15 ? 2 : (dSrc < 15 ? 1 : (inEl ? 3 : 0));
    !basic && (mouse_cap & 16) && dSrc < 15 && !(dLs < dSrc) ? dctgt = 0;   // Alt + click on the source has its own double-click (below) - not the 'to the room center'
// double-click on the listener's facing arrow (its tip, outside the ball) = Facing back to the default (Fixed, straight ahead)
    PJ(lx + cos(face) * 0.7, ly + sin(face) * 0.7, lz); dTip = sqrt(sqr(mx - qx) + sqr(my - qy));
    !lockL && !(mouse_cap & 16) && dTip < 9 && dTip < dLs && dLs >= 8 && dSrc >= 12 ? dctgt = 4;
    dcnow = time_precise();
    dctgt > 0 && dctgt == dclast && dcnow - dctime < 0.5 ? (
      dctgt == 4 ? ( snap_take(); setp(16, 1); setp(17, 0); snap_notify(); ) : ( dctgt == 3 ? setp(43, 0) : moveobj(dctgt, roomW / 2 / rs, roomD / 2 / rs); );   // door / window: double-click closes it
      dclast = 0; drag = 0;
    ) : (
      dclast = dctgt; dctime = dcnow;
      // ROOM EDGE: drag = stretch the room along that line, Alt = height, Ctrl/Cmd = size (Shift = fine)
      ehc = !isfree && !apH0 && dLs >= 15 && dSrc >= 15 && dBh >= 9 && dBe >= 9 && !inEl && !nearmth() ? edgehit() : -1;
      ehc >= 0 ? (
        drag = 11; eIdx = ehc; eA = floor(ehc / 4); eMd = (mouse_cap & 4) ? 3 : ((mouse_cap & 16) && !planv ? 2 : 1); sidevOn && eMd != 3 ? ( eMd = eA == 2 ? (frontv ? 5 : 4) : 2; );   // side / front view: the vertical wall lines set the depth / the width, the ceiling line the height   // side view - the vertical wall lines set the depth, the ceiling line the height
        dvx0 = mx; dvy0 = my; dW0 = slider2; dD0 = slider3; dH0 = slider4; dS0 = slider31; dpS0 = pS; dpKX0 = pKX; dpKY0 = pKY; dpSZ0 = pSZ; dpX00 = pX0; dpY00 = pY0;
      ) :
// Alt + double-click on the source: the listener looks straight ahead into the room again (the default Facing: Fixed, angle 0 = towards the back wall)
      (mouse_cap & 16) && !basic ? ( dSrc < 15 && !(dLs < dSrc) ? (
        dcnow - altdct < 0.5 ? ( setp(16, 1); setp(17, 0); altdct = 0; drag = 0; ) : ( altdct = dcnow; drag = 10; );
      ) : ( drag = 3; ); ) : (   // Alt: turn (Expert)
        dLs < 15 ? (drag = 2; dz0 = lz) : (
          dSrc < 15 ? (drag = 1; dz0 = sz) : (
            dBh < 9 ? (drag = 4) : ( dBe < 9 ? (drag = 5) : (
            inEl ? ( (mouse_cap & 8) ? ( drag = 6; dop0 = slider43; ) : (drag = 5) ) : (
            hitw > 0 && !basic ? (selw = hitw; drag = 0) : (drag = 0)))))));
      // ROOM CORNER: the whole room in proportion (the size factor stays)
      cnc = !isfree && !sidevOn && dLs >= 15 && dSrc >= 15 && dBh >= 9 && dBe >= 9 && !inEl && !nearmth() ? cornerhit() : 0;
      cnc ? (
        drag = 12; cnI = cnc; dvx0 = mx; dvy0 = my; dW0 = slider2; dD0 = slider3; dH0 = slider4;
        dSX0 = slider10; dSY0 = slider11; dSZ0 = slider12; dLX0 = slider13; dLY0 = slider14; dLZ0 = slider15; dBP0 = slider42; dBE0 = slider45; dMC0x = mtC0x; dMC0y = mtC0y; dMC1x = mtC1x; dMC1y = mtC1y; dAP0x = MTAP[0]; dAP0y = MTAP[1];
        dpS0 = pS; dpKX0 = pKX; dpKY0 = pKY; dpSZ0 = pSZ; dpX00 = pX0; dpY00 = pY0; drs0 = rs;
      );
    );
  );
  // size labels in the room view: drag = width / depth / height
  click ? (
    dmhit = (!sidevOn || frontv) && inbox(dmwx - dmww / 2, dmwy - 7, dmww, 14) ? 1 : (!frontv && inbox(dmdx, dmdy - 7, dmdw, 14) ? 2 : (!planv && !isfree && inbox(dmhx - dmhw, dmhy - 7, dmhw, 14) ? 3 : 0));
    dmhit && lastkid == 320 + dmhit && time_precise() - lastkt < 0.5 ? (
      vedopen(320 + dmhit, dmhit == 1 ? #dmw : (dmhit == 2 ? #dmd : #dmh)); lastkid = -1; dmhit = 0; drag = 0;
    ) : ( dmhit ? ( lastkid = 320 + dmhit; lastkt = time_precise(); ); );
    dmhit ? ( drag = 6 + dmhit; dvx0 = mx; dvy0 = my; dpS0 = pS; dpKX0 = pKX; dpKY0 = pKY; dpSZ0 = pSZ; dpX00 = pX0; dpY00 = pY0; dvv0 = dmhit == 1 ? slider2 : (dmhit == 2 ? slider3 : slider4); );
  );
  dmfine = (mouse_cap & 8) ? 0.2 : 1;
  swDrag ? ( drag = 13; ); swSet ? ( drag = 0; swSet = 0; );   // (a click that set the width: nothing else starts)
// the size handle of the motion path (before balls and walls)
  click && mtHv && !apBall && !mtRec && sqr(mx - mtHsx) + sqr(my - mtHsy) < 64 ? ( drag = 21; swDrag = 0; mtHr0 = slider104; mtHax = mtHcx; mtHay = mtHcy; mtHd0 = max(4, sqrt(sqr(mx - mtHax) + sqr(my - mtHay))); click = 0; );
// (the anchor is held from the click on - it no longer follows the path while it grows, which made it jump and blink)
  click && apHv && !apBall && !mtRec && sqr(mx - apHx) + sqr(my - apHy) < 100 ? ( drag = 22; swDrag = 0; apinit(); click = 0; );   // the Own spot
  click && ffHv && !apBall && !(apHv && sqr(mx - apHx) + sqr(my - apHy) < 100) && sqr(mx - ffHx) + sqr(my - ffHy) < 100 ? (   // exactly the corner of a room (drag 12): right / down = wider / deeper, Ctrl/Cmd = in proportion, Alt = height
  drag = 12; cnI = 2; swDrag = 0; click = 0; dvx0 = mx; dvy0 = my; dW0 = slider2; dD0 = slider3; dH0 = slider4;
  dSX0 = slider10; dSY0 = slider11; dSZ0 = slider12; dLX0 = slider13; dLY0 = slider14; dLZ0 = slider15; dBP0 = slider42; dBE0 = slider45; dMC0x = mtC0x; dMC0y = mtC0y; dMC1x = mtC1x; dMC1y = mtC1y; dAP0x = MTAP[0]; dAP0y = MTAP[1];
  dpS0 = pS; dpKX0 = pKX; dpKY0 = pKY; dpSZ0 = pSZ; dpX00 = pX0; dpY00 = pY0; drs0 = rs;
  fsI = 0; loop(12, dRfl[fsI] = vopt[RFB + fsI]; fsI += 1; );
);
  // the scale handles first (they lie on the panel): length along the foot line, height at the top edge (3D)
  click && !apBall && rflLHv[0] && sqr(mx - rflLHx[0]) + sqr(my - rflLHy[0]) < 64 ? ( drag = 26; swDrag = 0; rflD0 = -1; rflSel = 0; click = 0; );
  click && !apBall && drag != 26 && rflLHv[1] && sqr(mx - rflLHx[1]) + sqr(my - rflLHy[1]) < 64 ? ( drag = 27; swDrag = 0; rflD0 = -1; rflSel = 1; click = 0; );
  click && !apBall && drag < 26 && rflHHv[0] && sqr(mx - rflHHx[0]) + sqr(my - rflHHy[0]) < 64 ? ( drag = 28; swDrag = 0; rflD0 = -1; rflSel = 0; click = 0; );
  click && !apBall && drag < 26 && rflHHv[1] && sqr(mx - rflHHx[1]) + sqr(my - rflHHy[1]) < 64 ? ( drag = 29; swDrag = 0; rflD0 = -1; rflSel = 1; click = 0; );
  // Alt + drag on the dot or the panel = turn it around its center (Angle); plain drag = move it
  click && !apBall && drag < 26 && (mouse_cap & 16) && rflon(0) ? ( drag = 30; swDrag = 0; rflSel = 0; click = 0; );
  click && !apBall && drag < 26 && (mouse_cap & 16) && rflon(1) ? ( drag = 31; swDrag = 0; rflSel = 1; click = 0; );
  click && !apBall && drag < 26 && rflon(0) ? ( drag = 24; swDrag = 0; rflSel = 0; click = 0; rflGx = mx - rflHx0; rflGy = my - rflHy0; );   // grabbed where the mouse is (no jump)
  click && !apBall && drag != 24 && drag < 26 && rflon(1) ? ( drag = 25; swDrag = 0; rflSel = 1; click = 0; rflGx = mx - rflHx1; rflGy = my - rflHy1; );
  drag == 24 || drag == 25 ? (
    rflO2 = RFB + (drag - 24) * 6;
    rflIy = (my - rflGy - pY0) / pKY; rflIx = (mx - rflGx - pX0 - rflIy * pKX) / pXs;
    vopt[rflO2 + 1] = min(roomW * 1.25, max(0, rflIx)); vopt[rflO2 + 2] = min(roomD * 1.25, max(0, rflIy));
    sprintf(#etip, "Reflector %.0f  X %.1f m · Y %.1f m", drag - 23, vopt[rflO2 + 1], vopt[rflO2 + 2]); tipon = 1; tipforce = 1; tipstr = #etip; tipx = mx; tipy = my - 20;
  );
  drag == 26 || drag == 27 ? (   // LENGTH: the mouse along the panel's line, measured from its center, x 2 (the center and the angle stay)
    rflO2 = RFB + (drag - 26) * 6;
    rflIy = (my - pY0) / pKY; rflIx = (mx - pX0 - rflIy * pKX) / pXs;
    rflLn = abs((rflIx - vopt[rflO2 + 1]) * cos(vopt[rflO2 + 3] * $pi / 180) + (rflIy - vopt[rflO2 + 2]) * sin(vopt[rflO2 + 3] * $pi / 180));   // the distance of the mouse from the center along the panel
    rflD0 < 0 ? ( rflD0 = max(0.01, rflLn); rflL0 = vopt[rflO2 + 4]; ); rflLn = rflL0 * rflLn / rflD0;   // relative to the grab: nothing jumps (also when the handle sat at the view's edge)
    vopt[rflO2 + 4] = min(max(max(10, 4 * max(roomW, roomD)), rflL0), max(0.5, rflLn));
    sprintf(#etip, "Reflector %.0f  Length %.1f m", drag - 25, vopt[rflO2 + 4]); tipon = 1; tipforce = 1; tipstr = #etip; tipx = mx; tipy = my - 20;
  );
  drag == 28 || drag == 29 ? (   // HEIGHT (3D): the mouse above the panel's foot center, in meters
    rflO2 = RFB + (drag - 28) * 6; PJ(vopt[rflO2 + 1], vopt[rflO2 + 2], 0);
    rflD0 < 0 ? ( rflD0 = max(1, qy - my); rflL0 = vopt[rflO2 + 5]; ); vopt[rflO2 + 5] = min(40, max(1, rflL0 * max(0, qy - my) / rflD0));   // relative to the grab (see Length)
    sprintf(#etip, "Reflector %.0f  Height %.1f m", drag - 27, vopt[rflO2 + 5]); tipon = 1; tipforce = 1; tipstr = #etip; tipx = mx; tipy = my - 20;
  );
  drag == 30 || drag == 31 ? (   // ANGLE (Alt + drag): the panel turns around its center towards the mouse (snaps to 15° steps within 2°; Shift = no snap)
    rflO2 = RFB + (drag - 30) * 6;
    rflIy = (my - pY0) / pKY; rflIx = (mx - pX0 - rflIy * pKX) / pXs;
    sqr(rflIx - vopt[rflO2 + 1]) + sqr(rflIy - vopt[rflO2 + 2]) > 0.04 ? (
      rflAn = atan2(rflIy - vopt[rflO2 + 2], rflIx - vopt[rflO2 + 1]) * 180 / $pi; rflAn < 0 ? rflAn += 180; rflAn >= 180 ? rflAn -= 180;   // a panel is the same line turned by 180°
      rflAq = 15 * floor(rflAn / 15 + 0.5); !(mouse_cap & 8) && abs(rflAn - rflAq) < 2 ? rflAn = rflAq; rflAn >= 180 ? rflAn -= 180;
      vopt[rflO2 + 3] = rflAn;
    );
    sprintf(#etip, "Reflector %.0f  Angle %.0f°", drag - 29, vopt[rflO2 + 3]); tipon = 1; tipforce = 1; tipstr = #etip; tipx = mx; tipy = my - 20;
  );
  drag == 23 ? ( setp(31, min(4, max(0.25, ffS0 * sqrt(sqr(mx - ffAx) + sqr(my - ffAy)) / ffD0))); sprintf(#etip, "Size %.2f× · %.1f × %.1f m", slider31, slider2 * slider31, slider3 * slider31); tipon = 1; tipforce = 1; tipstr = #etip; tipx = mx; tipy = my - 20; );
  drag == 22 ? (
    frontv ? ( iy = MTAP[1] * rs; ix = (mx - pX0) / pS; ) : sidevOn ? ( ix = MTAP[0] * rs; iy = (mx - pX0) / pKX; ) : ( iy = (my - pY0) / pKY; ix = (mx - pX0 - iy * pKX) / pS; );   // side view - the spot moves in depth, its left / right stays; front view: left / right, the depth stays
    ix /= rs; iy /= rs;
    MTAP[0] = clampx(ix); MTAP[1] = clampy(iy); MTAP[2] = 1;
    sprintf(#etip, "Own spot  %.2f m · %.2f m", MTAP[0] * rs, MTAP[1] * rs); tipon = 1; tipforce = 1; tipstr = #etip; tipx = mx; tipy = my - 20;
  );
  drag == 21 ? ( setp(104, min(20, max(0.1, mtHr0 * sqrt(sqr(mx - mtHax) + sqr(my - mtHay)) / mtHd0))); sprintf(#etip, "Size %.2f m", slider104 * rs); tipon = 1; tipforce = 1; tipstr = #etip; tipx = mx; tipy = my - 20; );
// DRAW A MOTION PATH: armed (Draw) - a drag in the view records the stroke (at the height of the ball that moves)
  click && mtRec && !sidevOn && inbox(VX, VY + 24, VW, VH - 24) ? ( drag = 20; swDrag = 0; mtRN = 0; mtRT0 = time_precise(); mtRX = -999; mtRY = -999; mtRZ = floor(slider101 + 0.5) == 1 ? lz : sz; mtZm = 0; click = 0; );
  drag == 20 ? (
// one point per frame with its time - pauses and fast strokes are kept (up to 1024 frames)
// 3D view + Shift held = the HEIGHT: the mouse up / down raises and lowers the point where it is
    !planv && (mouse_cap & 8) && mtRN > 0 ? ( !mtZm ? ( mtZm = 1; mtZy0 = my; mtZz0 = mtRZ; ); mtRZ = min(max(mtZz0 - (my - mtZy0) / max(0.001, pSZ), 0.1), roomH - 0.1); ) : ( mtZm = 0; );
    mtRN < 1024 && (mtRN == 0 || time_precise() - mtRB[3 * mtRN - 1] >= 0.012) ? (
      mtZm ? ( ix = mtRB[3 * mtRN - 3]; iy = mtRB[3 * mtRN - 2]; ) : ( iy = (my + mtRZ * pSZ - pY0) / pKY; ix = (mx - pX0 - iy * pKX) / pS; );
      mtRB[3 * mtRN] = min(max(ix, 0.1), roomW - 0.1); mtRB[3 * mtRN + 1] = min(max(iy, 0.1), roomD - 0.1); mtRB[3 * mtRN + 2] = time_precise(); mtRBz[mtRN] = mtRZ; mtRN += 1; mtRX = mx; mtRY = my;
    );
    col(cMOTT, 1); mtk = 1;
    loop(mtRN - 1, PJ(mtRB[3 * mtk - 3], mtRB[3 * mtk - 2], mtRBz[mtk - 1]); mtq1 = qx; mtq2 = qy; PJ(mtRB[3 * mtk], mtRB[3 * mtk + 1], mtRBz[mtk]); LNW(mtq1, mtq2, qx, qy, 2); mtk += 1; );
    mtRN > 1 ? ( col(cMOTT, 0.45); PJ(mtRB[0], mtRB[1], mtRBz[0]); mtq1 = qx; mtq2 = qy; PJ(mtRB[3 * mtRN - 3], mtRB[3 * mtRN - 2], mtRBz[mtRN - 1]); DL(mtq1, mtq2, qx, qy, 4, 3, 1); );   // it closes into a loop
    mtZm ? ( PJ(mtRB[3 * mtRN - 3], mtRB[3 * mtRN - 2], 0); mtq1 = qx; mtq2 = qy; PJ(mtRB[3 * mtRN - 3], mtRB[3 * mtRN - 2], mtRZ); col(cMOTT, 0.5); DL(mtq1, mtq2, qx, qy, 3, 3, 1); );   // the height: a dashed post to the floor
  );
  drag == 13 ? (
    // the handle: the width follows the mouse's distance from the center - at the center it is 0 % (also when the drawn span is
    // longer than the real one, free field); from 0 % it opens by the scale of the view (a step of 1 % per px); Shift = a quarter
    swMode ? ( swDd = abs((mx - spCx) * spUx + (my - spCy) * spUy); swK = swW0 > 1 ? swW0 / swD0 : 100 / max(18, 100 * spPx);
               swNw = (mouse_cap & 8) ? swW0 + (swDd - swD0) * swK * 0.25 : swW0 + (swDd - swD0) * swK; )
           : ( swNw = swW0 + (mx - swX0) * ((mouse_cap & 4) ? 0.25 : 1); );   // (Alt+Shift+drag on the ball: Shift is the gesture, so Ctrl/Cmd is the fine step)
    !(mouse_cap & 8) && abs(swNw - 100) < 1.5 ? swNw = 100;   // a detent at 100 %
    setp(98, min(200, max(0, swNw)));
  );
  drag == 11 ? (
    eMd == 4 ? ( edm = (mx - dvx0) / dpKX0; ) : eMd == 5 ? ( edm = (mx - dvx0) / dpS0; ) : eMd == 2 ? ( edm = (dvy0 - my) / dpSZ0; ) : (
      eA == 0 ? ( evx = dpS0; evy = 0; ) : ( eA == 1 ? ( evx = dpKX0; evy = dpKY0; ) : ( evx = 0; evy = -dpSZ0; ); );
      edm = ((mx - dvx0) * evx + (my - dvy0) * evy) / max(0.0001, evx * evx + evy * evy);   // meters along the line
    );
    edm *= dmfine;
    eMd == 3 ? ( setp(31, min(4, max(0.25, dS0 * exp(edm / max(1, (eA == 0 ? dW0 : (eA == 1 ? dD0 : dH0)) * dS0))))); ) : (
    eMd == 4 ? setp(3, min(40, max(0.8, dD0 + edm / rs))) : eMd == 5 ? setp(2, min(40, max(0.8, dW0 + edm / rs))) : (eMd == 2 || eA == 2 ? setp(4, min(15, max(0.8, dH0 + edm / rs))) : (
    eA == 0 ? setp(2, min(40, max(0.8, dW0 + edm / rs))) : setp(3, min(40, max(0.8, dD0 + edm / rs))); ); ); );
  );
  // ROOM CORNER: scale k along the line origin -> corner (the back left floor corner stays where it is)
  drag == 12 ? (
    cux = (cnI == 2 ? dW0 * drs0 * dpS0 : 0) + dD0 * drs0 * dpKX0; cuy = dD0 * drs0 * dpKY0;
    cl2 = max(1, cux * cux + cuy * cuy);
    (mouse_cap & 4) ? (
      // Ctrl / Cmd: the whole room in proportion
      ck = 1 + (((mx - dvx0) * cux + (my - dvy0) * cuy) / cl2) * dmfine;
      ck = min(min(40 / dW0, 40 / dD0), min(15 / dH0, max(0.8 / max(dW0, max(dD0, dH0)), ck)));
      ckx = max(0.8, dW0 * ck) / dW0; cky = max(0.8, dD0 * ck) / dD0; ckz = max(0.8, dH0 * ck) / dH0;
    ) : (mouse_cap & 16) ? (
      // Alt: the height - up = higher (plan: the same, the height is not drawn there)
      ckx = cky = 1; ckz = min(15, max(0.8, dH0 + (dvy0 - my) * dmfine / (dpSZ0 > 0 ? dpSZ0 : dpS0) / drs0)) / dH0;
    ) : (
      // free: right = wider, left = narrower; down = deeper, up = shallower (each side 0.8 … 40 m)
      ckx = min(40, max(0.8, dW0 + (mx - dvx0) * dmfine / dpS0 / drs0)) / dW0;
      cky = min(40, max(0.8, dD0 + (my - dvy0) * dmfine / dpKY0 / drs0)) / dD0; ckz = 1;
    );
    setp(2, dW0 * ckx); setp(3, dD0 * cky); setp(4, dH0 * ckz);
    !lockS ? ( setp(10, dSX0 * ckx); setp(11, dSY0 * cky); setp(12, dSZ0 * ckz); );      // everything inside scales with the room (per axis)
    mtEg > 0.001 ? ( mtC0x = dMC0x * ckx; mtC0y = dMC0y * cky; mtC1x = dMC1x * ckx; mtC1y = dMC1y * cky; MTAP[2] >= 0.5 ? ( MTAP[0] = dAP0x * ckx; MTAP[1] = dAP0y * cky; ); );   // a placed Own spot scales with it // a running motion scales with it
    !lockL ? ( setp(13, dLX0 * ckx); setp(14, dLY0 * cky); setp(15, dLZ0 * ckz); );
    !lockB ? ( setp(42, dBP0 * cky); ); !lockE ? ( setp(45, dBE0 * ckx); );
    isfree ? (   // the two reflectors keep their place, length, angle and height when the area changes (they used to scale with it and moved) - only kept inside the area
      fsI = 0;
      loop(2,
        fsO = RFB + fsI * 6; fsB = dRfl + fsI * 6;
        vopt[fsO + 1] = min(max(0.8, slider2) * min(4, max(0.25, slider31)) * 1.25, fsB[1]); vopt[fsO + 2] = min(max(0.8, slider3) * min(4, max(0.25, slider31)) * 1.25, fsB[2]);
        fsI += 1;
      );
    );
// no value box - the measures are on the room (dimension lines, wall notes)
  );
  drag == 7 ? setp(2, min(40, max(0.8, dvv0 + (mx - dvx0) / dpS0 / rs * dmfine)));
  drag == 8 ? setp(3, min(40, max(0.8, dvv0 + (sidevOn ? (mx - dvx0) / dpKX0 : (my - dvy0) / dpKY0) / rs * dmfine)));
  drag == 9 ? setp(4, min(15, max(0.8, dvv0 + (dvy0 - my) / dpSZ0 / rs * dmfine)));
  // the new value beside the mouse while dragging
  drag == 11 && eMd == 3 ? ( sprintf(#etip, "Size %.2f×", slider31); tipon = 1; tipforce = 1; tipstr = #etip; tipx = mx; tipy = my - 20; );   // only the size factor (it is not drawn on the room); the other dimensions show on the room's labels
  !(drag == 1 || drag == 2) && !mmdrag ? ( axOn = 0; );             // (a new drag starts a new line)
  (drag == 1 || drag == 2) ? (
    sidevOn ? (   // side view - the mouse sets the depth and the height, left / right stays; front view: left / right and the height, the depth stays
      nz = min(max((pY0 - my) / pSZ, 0), roomH - 0.1);
      frontv ? moveobj(drag == 1 ? 1 : 2, (mx - pX0) / pS / rs, (drag == 1 ? sy : ly) / rs) : moveobj(drag == 1 ? 1 : 2, (drag == 1 ? sx : lx) / rs, (mx - pX0) / pKX / rs);
      moveobjz(drag == 1 ? 1 : 2, nz);
    ) : (mouse_cap & 8) && !planv ? (
      !zmode ? ( zmode = 1; dmy0 = my; dz0 = drag == 1 ? sz : lz; );
      nz = min(max(dz0 - (my - dmy0) / pSZ, 0), roomH - 0.1);
      moveobjz(drag == 1 ? 1 : 2, nz);
    ) : (
      zmode = 0;
      zz = drag == 1 ? sz : lz;
      iy = (my + zz * pSZ - pY0) / pKY; ix = (mx - pX0 - iy * pKX) / pS;
      ix = ix / rs; iy = iy / rs;                                    // clamped in moveobj (the listener: walls, see lis_walk)
      axcon(drag, ix, iy, 6); ix = axNX; iy = axNY;                 // Ctrl/Cmd: on a line only (left / right or forward / back)
      moveobj(drag == 1 ? 1 : 2, ix, iy);
      axOn ? (
        // the guide line(s) through where it was when the key went down, at its height, across the whole room
        col(drag == 1 ? cSRC : cLIS, 1);
        axA != 2 ? ( PJ(0, axY0 * rs, zz); axq0 = qx; axq1 = qy; PJ(roomW, axY0 * rs, zz); DL(axq0, axq1, qx, qy, 5, 4, axA ? 1.4 : 0.8); );
        axA != 1 ? ( PJ(axX0 * rs, 0, zz); axq0 = qx; axq1 = qy; PJ(axX0 * rs, roomD, zz); DL(axq0, axq1, qx, qy, 5, 4, axA ? 1.4 : 0.8); );
        tipon = 1; tipforce = 0; tipx = mx; tipy = my - 22;
        tipstr = axA == 1 ? "On a line: left / right only (Ctrl/Cmd)" : (axA == 2 ? "On a line: forward / back only (Ctrl/Cmd)" : "On a line – move the mouse along the one you want");
      );
    );
  );
  drag == 4 ? ( iy = sidevOn ? (mx - pX0) / pKX : (my + brH * pSZ - pY0) / pKY; setp(42, min(max(iy, ybm), roomD - ybm) / rs); );
  drag == 5 ? ( ix = (mx - pX0 - yb * pKX) / pS; setp(45, min(max(ix, brMG), roomW - brMG) / rs); );
  drag == 6 ? setp(43, min(100, max(0, dop0 + (dmy0 - my) / 1.2)));      // Shift+drag up = open (120 px = 0 … 90°)
  drag == 10 ? (
    iy = (my + sz * pSZ - pY0) / pKY; ix = (mx - pX0 - iy * pKX) / pS;
    abs(ix - sx) + abs(iy - sy) > 0.05 ? (
      slider63 < 0.5 ? setp(63, 1);
      setp(61, atan2(-(ix - sx), iy - sy) * 180 / $pi);
    );
  );
  drag == 3 ? (
    iy = (my + lz * pSZ - pY0) / pKY; ix = (mx - pX0 - iy * pKX) / pS;
    deg = atan2(iy - ly, ix - lx) * 180 / $pi + 90;
    deg > 180 ? deg -= 360;
    slider16 < 0.5 ? ( slider16 = 1; autom(32768); );
    setp(17, deg);
  );
) : (
  drag == 20 ? mtfinish();
  drag = 0; kdrag = 0; eqdrag = 0; wbdrag = 0; swDrag = 0;
  memset(gheld, 0, SLOTS);
  i = 1; loop(NSL, gtouch[i] ? ( sauto_end(i); gtouch[i] = 0; ); i += 1; );   // ...and the touch sessions end
);
);
// TIMELINE and RT60 PER OCTAVE: the analysis tabs of the room view (Expert)
function gfxpT()
(
// ---- ECHOGRAM: the room over time ----
vtab == 2 || lrnTL ? (
!lrnTL ? ( EX = VX; EY = VY + 8; EW = VW; EH = VH - 8; );
(
  ppx = EX + 30; ppy = EY + 58; ppw = EW - 40; pph = EH - 80;
  // HEAR: direct / early / late - click = only this one (again: all three), Shift+click = on / off (the chips are the legend of the colors)
  ehm = vopt[18]; gk = 0;
  loop(3,
    hcx = EX + 10 + gk * 44; hon = !(ehm & (2 ^ gk)); hrcol = gk == 0 ? cSRC : (gk == 1 ? cRMTT : cRVTT);
    chip(hcx, EY + 24, 42, 14, gk == 0 ? "Direct" : (gk == 1 ? "Early" : "Late"), hon, hrcol) ? (
      (mouse_cap & 8) ? ( vopt[18] = (ehm & (2 ^ gk)) ? ehm - 2 ^ gk : ehm + 2 ^ gk; ) : ( vopt[18] = ehm == 7 - 2 ^ gk ? 0 : 7 - 2 ^ gk; );
    );
    tipbox(hcx, EY + 24, 42, 14, gk == 0 ? "Direct sound – click: hear only this (again: all three) · Shift+click: on / off · monitor only: not saved, ends when the window closes" : (gk == 1 ? "Early reflections, the first echoes from the walls – click: hear only these (again: all three) · Shift+click: on / off · monitor only: not saved, ends when the window closes" : "Late reverb, the diffuse tail – click: hear only the tail (again: all three) · Shift+click: on / off · monitor only: not saved, ends when the window closes"));
    gk += 1;
  );
// dB range of the plot: DBT = top, DBB = bottom; tlate = where the late tail starts (ms)
DBT = 6; DBB = -60;
tlate = (brd + mfp) / C_SND * 1000;
tmax = 2000;   // fixed window (sqrt time axis): the axis never moves, only the bars and the tail change
col(cINK, 0.06);
gi = 0; loop(3, gy = ppy + (DBT + gi * 30) / (DBT - DBB) * pph; LN(ppx, gy, ppx + ppw, gy); gi += 1; );
col(cMUTED, 1); gfx_setfont(4);
gi = 0; loop(3, gy = ppy + (DBT + gi * 30) / (DBT - DBB) * pph; sprintf(#w1, "%d", -gi * 30); txt(#w1, ppx - 4, gy, 2); gi += 1; );
txt("0", ppx, ppy + pph + 10, 0);
col(cINK, 0.08); gx = ppx + sqrt(10 / tmax) * ppw; LN(gx, ppy, gx, ppy + pph); gx2 = ppx + sqrt(100 / tmax) * ppw; LN(gx2, ppy, gx2, ppy + pph); gx3 = ppx + sqrt(1000 / tmax) * ppw; LN(gx3, ppy, gx3, ppy + pph);
col(cMUTED, 1); txt("10", gx, ppy + pph + 10, 1); txt("100", gx2, ppy + pph + 10, 1);
txt("1000 ms", gx3, ppy + pph + 10, 1);
!isfree ? (
  cfade = (ehm & 4) ? 0 : 1;                        // tail switched off (HEAR): not drawn
  // el0 holds the level (dB) of the late tail at its start tlate (diffuse-field estimate from RT60 and critical distance, plus the slider21 offset); it then falls 60 dB per RT60
  el0 = 10 * log10(13.8 / (rt * rc * rc) * 0.001) + 10 * log10(max(0.000001, latecorr)) + slider21;
  col(cAMB, 0.18);
  gi = 0;
  loop(ppw,
    tt = sqr(gi / ppw) * tmax;
    tt >= tlate ? (
      ldb = el0 - 60 * (tt - tlate) / (rt * 1000);
      ldb > DBB ? ( gy = ppy + (DBT - min(DBT, ldb)) / (DBT - DBB) * pph; R(ppx + gi, gy, 1, ppy + pph - gy); );
    );
    gi += 1;
  );
  col(cAMB, 1);
  tend = min(tmax, tlate + rt * 1000 * (el0 - DBB) / 60);
  gi = 0;
  tlate < tmax ? loop(40,
    tt = tlate + (tend - tlate) * gi / 40; tt2 = tlate + (tend - tlate) * (gi + 1) / 40;
    tx1 = ppx + sqrt(tt / tmax) * ppw; ty1 = ppy + (DBT - min(DBT, max(DBB, el0 - 60 * (tt - tlate) / (rt * 1000)))) / (DBT - DBB) * pph;
    tx2 = ppx + sqrt(tt2 / tmax) * ppw; ty2 = ppy + (DBT - min(DBT, max(DBB, el0 - 60 * (tt2 - tlate) / (rt * 1000)))) / (DBT - DBB) * pph;
    LNW(tx1, ty1, tx2, ty2, 1.4);
    gi += 1;
  );
  // density: the tail drawn as impulses – few tall echoes at low density, many small ones at high density
  dn_ = min(1, max(0, slider38 / 100)); nimp = floor(8 + dn_ * 72);
  col(cAMB, 0.28 + 0.4 * (1 - dn_));
  gi = 0;
  loop(nimp,
    // deterministic jitter so the pattern is stable; spacing shrinks with density
    tt = tlate + (tend - tlate) * ((gi + 0.5 + 0.35 * sin(gi * 12.9898)) / nimp);
    tt > tlate && tt < tmax ? (
      edb = el0 - 60 * (tt - tlate) / (rt * 1000) - (1 - dn_) * 4 * abs(sin(gi * 7.7));
      gx = ppx + sqrt(tt / tmax) * ppw; gy = ppy + (DBT - min(DBT, max(DBB, edb))) / (DBT - DBB) * pph;
      LNW(gx, ppy + pph, gx, gy, dn_ > 0.6 ? 0.8 : 1.4);
    );
    gi += 1;
  );
  // neighbor room (2nd network): dashed tail with its own level and RT60
  xLG2 > 0 ? (
    l2 = 10 * log10(max(0.0000000001, 13.8 * 0.001 / rt2 * bE2)) + slider21; t2 = xLD2 / srate * 1000;
    col(cLIS, 0.9);
    tend2 = min(tmax, t2 + rt2 * 1000 * (l2 - DBB) / 60);
    t2 < tmax && l2 > DBB ? (
      gi = 0;
      loop(24,
        tt = t2 + (tend2 - t2) * gi / 24; tt2 = t2 + (tend2 - t2) * (gi + 1) / 24;
        (gi & 1) == 0 ? (
          tx1 = ppx + sqrt(tt / tmax) * ppw; ty1 = ppy + (DBT - min(DBT, max(DBB, l2 - 60 * (tt - t2) / (rt2 * 1000)))) / (DBT - DBB) * pph;
          tx2 = ppx + sqrt(tt2 / tmax) * ppw; ty2 = ppy + (DBT - min(DBT, max(DBB, l2 - 60 * (tt2 - t2) / (rt2 * 1000)))) / (DBT - DBB) * pph;
          LNW(tx1, ty1, tx2, ty2, 1.2);
        );
        gi += 1;
      );
    );
    gfx_setfont(4); sprintf(#w1, "next room %.2f s", rt2); txt(#w1, ppx + ppw, ppy + 8, 2);
  );
  cfade = 1;
  gfx_setfont(1); sprintf(#w1, "RT60 %.2f s", rt); col(cAMB, 1); txt(#w1, EX + EW - 10, EY + 31, 2);
  col(cMUTED, 1); gfx_setfont(4); sprintf(#w1, "density %.0f %%", slider38);
  sprintf(#w2, "   bass %.2f s   4 kHz %.2f s", min(20, rt * lfrP), rt * hfrP); strcat(#w1, #w2);
  mdN > 0 && !isfree ? ( sprintf(#w2, "   modes up to %.0f Hz", mdFc); strcat(#w1, #w2); );
  txt(#w1, EX + EW - 10, EY + 44, 2);
  gi = 1;
  loop(NT - 1,
    !(ehm & 2) && tapOn[gi] && tapT[gi] < tmax ? (
      edb = dbs(tapG[gi] * erg);
      gx = ppx + sqrt(tapT[gi] / tmax) * ppw; gy = ppy + (DBT - min(DBT, max(DBB, edb))) / (DBT - DBB) * pph;
      col(cRMTT, gi <= 6 ? 1 : 0.5); LNW(gx, ppy + pph, gx, gy, gi <= 6 ? 1.6 : 1);   // 1st order bright, 2nd order faint
    );
    gi += 1;
  );
);
rbyp ? ( col(cSURF, 0.9); R(ppx + 1, ppy, ppw, pph); col(cMUTED, 1); gfx_setfont(4); txt("stereo panner only", ppx + ppw, ppy + 8, 2); );
!(ehm & 1) ? (
  edb = dbs(tapG[0]);                         // direct sound incl. a barrier in the way
  gx = ppx + sqrt(tapT[0] / tmax) * ppw; gy = ppy + (DBT - min(DBT, max(DBB, edb))) / (DBT - DBB) * pph;
  col(cSRC, 1); LNW(gx, ppy + pph, gx, gy, 2);
);
// DELAY echoes (magenta): each one reaches the listener like the source - direct path + echo time, at the echo's level
gdon && !rbyp ? (
  egL = slider77 <= -47.9 ? 0 : 10 ^ (slider77 / 20); egR = gdlv ? egL : (slider78 <= -47.9 ? 0 : 10 ^ (slider78 / 20)); gdmL ? ( egL = 0; ); gdmR ? ( egR = 0; ); efL = slider75 / 100; efR = gdlf ? efL : slider76 / 100;
  gk = 0; ett = 0; evL = 1; enL = 0; enR = 0;
  loop(60,
    gdpp ? ( ech = gk % 2; ett += ech == 0 ? gtL : gtR; eamp = ech == 0 ? egL * evL : egR * evL; ech == 1 ? ( evL *= sqrt(efL * efR); ); )
         : ( ech = gk % 2; ech == 0 ? ( enL += 1; ett = enL * gtL; eamp = egL * efL ^ (enL - 1); ) : ( enR += 1; ett = enR * gtR; eamp = egR * efR ^ (enR - 1); ); );
    etx = tapT[0] + ett; edb = dbs(tapG[0] * eamp);
    etx < tmax && edb > DBB ? (
      gx = ppx + sqrt(etx / tmax) * ppw; gy = ppy + (DBT - min(DBT, max(DBB, edb))) / (DBT - DBB) * pph;
      col(cDLS, 1); LNW(gx, ppy + pph, gx, gy, 1.6); CI(gx, gy, 1.8, 1);
    );
    gk += 1;
  );
  chip(EX + 10 + 3 * 44, EY + 24, 42, 14, "Delay", 1, cDLS);
  tipbox(EX + 10 + 3 * 44, EY + 24, 42, 14, "Magenta: the delay echoes as they reach the listener (direct path + echo time) – switch the delay in its section");
);
);
);
// ---- DECAY BY BAND: reverb time per octave as the reverb engine plays it (its loop filters), room modes below their limit ----
vtab == 3 && !lrnTL ? (
dbx0 = VX; dbw0 = VW; dkY = VY + 4; dkH = VH - 4;
(
  dbpx = dbx0 + 40; dbpw = floor((dbw0 - 52) * 0.6); dbpy = dkY + 38; dbph = dkH - 82;
  dbix = dbpx + dbpw + 26; dbiw = dbx0 + dbw0 - 12 - dbix;
  isfree || rbyp ? (
    col(cMUTED, 1); gfx_setfont(4); txt(isfree ? "Free field: no walls – no reverb" : "Room off – no reverb", dbx0 + dbw0 / 2, dkY + dkH / 2 + 6, 1);
  ) : (
    k = 0; dbvmx = 0.05;
    loop(8, rtbv[k] = rtband(62.5 * 2 ^ k); dbvmx = max(dbvmx, rtbv[k]); k += 1; );
    // grid step (s) picked from the tallest bar so the axis gets a handful of lines; dbtop = axis top, the next step above the tallest bar + 10 %
    dbst = dbvmx > 8 ? 4 : (dbvmx > 4 ? 2 : (dbvmx > 2 ? 1 : (dbvmx > 1 ? 0.5 : (dbvmx > 0.4 ? 0.2 : 0.1))));
    dbtop = ceil(dbvmx * 1.1 / dbst) * dbst;
    gi = 0;
    loop(floor(dbtop / dbst + 0.5) + 1,
      gv = gi * dbst; gy = dbpy + dbph - gv / dbtop * dbph;
      col(cHAIR, 1); LN(dbpx, gy, dbpx + dbpw, gy);
      col(cMUTED, 1); gfx_setfont(7); sprintf(#w1, dbst < 1 ? "%.1f" : "%.0f", gv); txt(#w1, dbpx - 5, gy, 2);
      gi += 1;
    );
    col(cMUTED, 1); gfx_setfont(7); txt("s", dbpx - 5, dbpy - 12, 2);
    dbbw = dbpw / 8; k = 0;
    loop(8,
      dbmd = mdN > 0 && 62.5 * 2 ^ k < mdFc;                         // below the modes' limit: the room's own tones
      dbbh = min(1, rtbv[k] / dbtop) * dbph; dbbx = dbpx + k * dbbw + dbbw * 0.18;
      col(dbmd ? cSRTT : cRVTT, 1); RR(dbbx, dbpy + dbph - max(2, dbbh), dbbw * 0.64, max(2, dbbh), 2.5);
      col(cTEXT, 1); gfx_setfont(7); sprintf(#w1, rtbv[k] < 9.95 ? "%.2f" : "%.1f", rtbv[k]); txt(#w1, dbbx + dbbw * 0.32, dbpy + dbph - max(2, dbbh) - 7, 1);
      col(cMUTED, 1); txt(k == 0 ? "63" : (k == 1 ? "125" : (k == 2 ? "250" : (k == 3 ? "500" : (k == 4 ? "1k" : (k == 5 ? "2k" : (k == 6 ? "4k" : "8k")))))), dbbx + dbbw * 0.32, dbpy + dbph + 9, 1);
      k += 1;
    );
    txt("Hz", dbpx + dbpw + 3, dbpy + dbph + 9, 0);
    tipbox(dbpx, dbpy - 6, dbpw, dbph + 18, "Reverb time RT60 per octave band – exactly what the reverb engine plays (walls, materials, air). Green = room modes: below that frequency a small room rings on its own tones");
    dby2 = dkY + dkH - 13;
    mdN > 0 ? sprintf(#w2, "room modes – below %.0f Hz", mdFc) : strcpy(#w2, "no room modes (large room)");
    col(cSRTT, 1); RR(dbpx, dby2 - 4, 8, 8, 2); col(cMUTED, 1); gfx_setfont(7); txt(#w2, dbpx + 12, dby2, 0); dblw = tws(#w2);
    col(cRVTT, 1); RR(dbpx + 26 + dblw, dby2 - 4, 8, 8, 2); col(cMUTED, 1); txt("diffuse reverb", dbpx + 38 + dblw, dby2, 0);
    // the numbers that explain the bars
    dbry = dbpy + 2; gi = 0;
    loop(5,
      gi == 0 ? ( strcpy(#w1, "RT60 (mid)"); sprintf(#w2, "%.2f s", rt); ) : (
      gi == 1 ? ( strcpy(#w1, "Room volume"); dbvv = roomW * roomD * roomH; sprintf(#w2, dbvv < 100 ? "%.1f m³" : "%.0f m³", dbvv); ) : (
      gi == 2 ? ( strcpy(#w1, "Mean free path"); sprintf(#w2, "%.2f m", mfp); ) : (
      gi == 3 ? ( strcpy(#w1, "Critical distance"); sprintf(#w2, "%.2f m", rc * sqrt(QDIR)); ) : (
                ( strcpy(#w1, "Modes up to"); mdN > 0 ? sprintf(#w2, "%.0f Hz", mdFc) : strcpy(#w2, "–"); ) ))));
      col(cMUTED, 1); gfx_setfont(4); txtfit(#w1, dbix, dbry, dbiw - 46);
      col(cTEXT, 1); gfx_setfont(2); txt(#w2, dbix + dbiw, dbry, 2);
      gi < 4 ? ( col(cHAIR, 1); LN(dbix, dbry + 9, dbix + dbiw, dbry + 9); );
      dbry += 18; gi += 1;
    );
    tipbox(dbix, dbpy - 6, dbiw, 92, "Mean free path = average way of the sound between two reflections; critical distance = where direct sound and room sound are equally loud; modes = the room's own resonances (Schroeder frequency)");
  );
);
);
);
// =================== LEARN MODE (GUI only - the sound is exactly the same as in Expert) ===================
// text that wraps at width w (line height lh), current font and color; returns the height used
function wrapt(s, x, y, w, lnh) local(n, i, c, ly)
(
  strcpy(#wsrc, s); n = strlen(#wsrc); strcpy(#wln, ""); strcpy(#wwd, ""); ly = y; i = 0;
  loop(n + 1,
    c = i < n ? str_getchar(#wsrc, i) : 32;
    c == 32 ? (
      strcpy(#wtry, #wln); strlen(#wtry) > 0 ? strcat(#wtry, " "); strcat(#wtry, #wwd);
      tws(#wtry) > w && strlen(#wln) > 0 ? ( txt(#wln, x, ly, 0); ly += lnh; strcpy(#wln, #wwd); ) : ( strcpy(#wln, #wtry); );
      strcpy(#wwd, "");
    ) : ( sprintf(#wch, "%c", c); strcat(#wwd, #wch); );
    i += 1;
  );
  strlen(#wln) > 0 ? ( txt(#wln, x, ly, 0); ly += lnh; );
  ly - y;
);
// ---- PLUGIN HELP (the Help button in the top bar): how roomservice works and the most used keys, one page; a click
// beside it or Esc closes it, "All keys and mouse actions" opens the keys page ----
function hlpsec(x, y, w, hd, bd) local(h) (
  col(cTEXT, 1); gfx_setfont(2); txt(hd, x, y, 0);
  col(cMUTED, 1); gfx_setfont(13); h = wrapt(bd, x, y + 16, w, 15);
  h + 30;
);
// Draws the 'how it works' help popup in one or two columns; its 'All keys and mouse actions' button opens the keys page
function hlpdraw() local(cw, two, cx, cy, hpch, x1, x2, w1, y, ya, yb)
(
  cw = min(WT - 24, 780); two = cw >= 640; hpch = min(HT - 24, max(200, hlpH + 54));
  pgopen(1, cw, hpch, max(hlpH, 100), "Roomservice – how it works");
  cx = pgCX; cy = pgBY - 44;
  w1 = two ? cw / 2 - 34 : cw - 40; x1 = cx + 20; x2 = two ? cx + cw / 2 + 14 : x1;
  y = cy + 54;
  y += hlpsec(x1, y, w1, "Signal flow", "In → Filter → Source → Delay → Room → Barrier → Out. The strip under the top bar shows the chain: click a module = on / off, Shift+click = solo, hear it alone.");
  y += hlpsec(x1, y, w1, "Three views", "Basic: film locations as tiles, room, decay, source and barrier at a glance – a hint in the strip shows Expert settings that still change the sound. Expert: the Stage View on top, one module per tab under it ([ and ] = previous / next) – or All = every module at once (key 0). Learn: ten listening tasks that explain what you hear.");
  y += hlpsec(x1, y, w1, "Room and positions", "Choose a room preset or one of My rooms. Drag source and listener in the plan (Shift+drag = height, 3D shows it; Ctrl/Cmd+drag = only left / right or only forward / back, a dashed line shows it). Reset positions = both back to their start places (listener in the middle, source 2 m in front), the room stays. Loading a room preset keeps source and listener in the same place relative to the room as long as you have not moved them; moved or locked positions stay. Drag the front corner = the room larger or smaller (Ctrl/Cmd = in proportion, Alt = height). Mouse wheel on the floor = zoom the view 0.1 … 5x (in and out); the chips Fit and 1:1 fit the whole room / area or go back. With the room off the direct sound still follows the positions (a 3D panner: direction, distance, height), only walls and reverb are gone. Direction (Expert): Src angle and Lis angle dials, or Alt+drag a ball to turn it; Aim = where the source points (Fixed or at the listener), Facing = where the listener looks (Fixed, at the source or along its path). The round arrows next to Aim and Facing reset them; double-click the listener's arrow (or Alt+double-click the source) = the listener looks straight ahead again. Doppler (chip beside Handles): on = a drag glides the pitch like a real move, off = only the distance changes.");
  y += hlpsec(x1, y, w1, "Free field", "Outdoors: no walls, no reverb – only the direct sound, the ground and two reflectors. Ground: Off, Hard, Grass or Snow, its strength with Ground level. Area width and depth = how far source and listener can move. The reflectors R1 and R2 (facade, glass, rock, fence, hedge): drag their dot or panel in the Stage View to move them, Alt+drag turns them, the small square at the panel's end sets the length and, in 3D, the one on the top edge the height; the knobs are in the Reflectors tab, its round button switches both on / off; they keep their place when the area changes. A reflector larger than the view: Fit (or the wheel on the floor, out to 10 %) shows all of it. All reflector values can be automated.");
  y += hlpsec(x1, y, w1, "Barrier", "A wall, a window or a door between source and listener: Pos, Size, Height, Open and Damp. The thin line around the room modules marks what belongs to the room.");
  y += hlpsec(x1, y, w1, "Delay and tempo", "The delay sits before the room – every echo is reverberated too. Tempo tools: tap, listen to the song or use the project tempo; a note value sets the delay at once. A click on Link links or unlinks all.");
  y += hlpsec(x1, y, w1, "Output", "In and Out = gain before and after the plugin. Dry / wet = the mix. Limiter = a look-ahead peak limiter at -1 dBFS (1.5 ms latency, REAPER compensates it) – a safety net, not a loudness tool. Equal loudness = every room, distance and model plays about as loud as the input, for comparing sounds.");
  ya = y; y = two ? cy + 54 : y;
  y += hlpsec(x2, y, w1, "Four ways to keep a sound", "A / B = compare quickly while you work (not saved; Match level plays both equally loud). Snapshots 1–16 = saved in this project: click = load, Save then a slot = store (a slot in use asks once), right-click = rename / overwrite / clear, keys 1–8 and Shift+1–8 (Tabs view: Shift+1–8 only, 1–8 open the tabs, 0 = All); Snapshot Morph blends them and can be automated. Room presets = typical rooms for every project. My rooms = your own rooms, kept on this computer.");
  y += hlpsec(x2, y, w1, "Motion (Expert)", "Moves the source, the listener or both on a pattern: Circle or Square (around the listener, its own spot or the room center), Left – right, Front – back, Up – down, Wave, a Drawn path (Draw: draw it in the Stage View) or Pass-by (once along a straight line through its own spot, A to B in one cycle time – the status shows the speed in m/s; it starts again with every playback start and loop). Rate in Hz or locked to the tempo (bars and notes), Size in meters, Start = where it begins, Glide = fade in / out. It runs with REAPER's transport, so every playback sounds the same; free-running while stopped. On / off, pattern, rate and size are automatable and stored in snapshots and A / B. The positions are written to the Source / Listener X, Y and height parameters, so REAPER can record the movement as automation (Write or Latch); an envelope in read mode on them overrides the pattern. The Stage View draws the path; Stop on its sign switches it off.");
  y += hlpsec(x2, y, w1, "Layout", "Drag the edges and corners of the modules and the column divider. Tabs: drag the corner of the module = larger / smaller (double-click = automatic), the bar over the tabs = height of the Stage View, Fit = the module as large as the window allows. Fit all = every module in the window, Lock GUI = the layout stays. V hides the Stage View, the magnifier enlarges a module – or key Z: in Tabs the open module, in All / Fit the module or the Stage View under the mouse (Z again or Esc = back).");
  y += hlpsec(x2, y, w1, "Most used keys", "1 … 9 tabs (Tabs view) or snapshots  ·  Ctrl/Cmd+Z undo  ·  arrows move the listener  ·  [ ] tabs  ·  0 Tabs / All  ·  Z zoom  ·  V Stage View  ·  Esc closes  ·  hold Ctrl/Cmd = help text under the mouse  ·  ? every key");
  yb = max(ya, y) + 4;
  // the keys page (its screen place is kept for the click test at the start of the next frame)
  col(inbox(cx + cw - 200, yb, 180, 20) ? cHAIR : cLINE, 1); RR(cx + cw - 200, yb, 180, 20, 6); col(cTEXT, 1); gfx_setfont(6); txt("All keys and mouse actions", cx + cw - 110, yb + 10, 1);
  hlpH = yb + 34 - pgBY;                                               // the body as tall as its text (next frame)
  pgclose();
  hkX0 = gox + (cx + cw - 200) * gs; hkX1 = hkX0 + 180 * gs; hkY0 = goy + (yb - pgSc) * gs; hkY1 = hkY0 + 20 * gs;   // (screen place, scrolled)
);
// a label in the room view: title in its color, one line below; kept inside the view
LBB = 4194304 + 44200;   // labels already placed in this frame (x, y, w) - a new one moves out of their way
// Draws a two-line label box in the plan view, nudged to avoid overlapping other labels and kept inside the view
function lbox(x, y, t1, t2, c) local(w, w2, k, b, mv, n)
(
  gfx_setfont(6); w = tws(t1); gfx_setfont(7); w2 = tws(t2); w = max(w, w2) + 14;
  x = min(max(x, VX + 6), VX + VW - w - 6); y = min(max(y, VY + 28), VY + VH - 52);
  n = 0; mv = 1;
  while ( mv && n < 6 ? (
    mv = 0; k = 0;
    loop(lbN, b = LBB + k * 3;
      x < b[0] + b[2] + 4 && b[0] < x + w + 4 && y < b[1] + 34 && b[1] < y + 34 ? (
        y = b[1] + 34 + 34 <= VY + VH - 22 ? b[1] + 36 : b[1] - 36; mv = 1; );
      k += 1; );
    n += 1; 1; ) : 0; );
  lbN < 6 ? ( b = LBB + lbN * 3; b[0] = x; b[1] = y; b[2] = w; lbN += 1; );
  col(cSURF, 1); RR(x, y, w, 30, 5); col(c, 1); RRO(x, y, w, 30, 5);
  gfx_setfont(6); txt(t1, x + 7, y + 9, 0); col(cMUTED, 1); gfx_setfont(7); txt(t2, x + 7, y + 21, 0);
);
// labels in the plan: critical distance, direct sound, the strongest first reflection (+ mirror source), reverb
function lanno() local(lrc, dx1, dy1, lbk, lbg, k, t, rpx, rpy, msx, msy, rx, ry)
(
  lbN = 0;
  !isfree ? (
    lrc = rc * sqrt(QDIR);
    col(cCRV, 0.85); floorring(sx, sy, lrc, 1);
    // the SOURCE label is an obstacle; the box sits at the upper left of the circle (on the right when there is no room)
    PJ(sx, sy, 0); LBB[0] = qx - 34; LBB[1] = qy - 36; LBB[2] = 68; lbN = 1;
    sprintf(#lw1, "Critical distance %.1f m", lrc); gfx_setfont(7); lbw = tws("inside: the direct sound is louder than the reverb") + 14;
    PJ(sx, sy, 0); msx = qx; msy = qy;
    PJ(max(0, sx - lrc * 0.707), max(0, sy - lrc * 0.707), 0);
    rx = qx - lbw - 6 < VX + 6 ? qx + 2 * (lrc * 0.707) * pS + 6 : qx - lbw - 6; ry = qy - 34;
// near the back wall the box landed on the source ball - then it goes to the right of the circle, level with the ball
    rx < msx + 16 && rx + lbw > msx - 16 && ry < msy + 14 && ry + 30 > msy - 14 ? ( PJ(sx + lrc, sy, 0); rx = qx + 8; ry = msy - 14; rx + lbw > VX + VW - 6 ? ( PJ(sx - lrc, sy, 0); rx = qx - lbw - 8; ); );
    lbox(rx, ry, #lw1, "inside: the direct sound is louder than the reverb", cCRV);
  );
  // the strongest first-order wall reflection (one open room)
  !isfree && !bon ? (
    lbk = 0; lbg = 0; k = 1;
    loop(4,                                                         // the strongest one that is not on the line source - listener
      k == 1 ? ( t = sy / max(0.001, sy + ly); rpx = sx + (lx - sx) * t; rpy = 0; ) : ( k == 2 ? ( t = (roomW - sx) / max(0.001, 2 * roomW - sx - lx); rpx = roomW; rpy = sy + (ly - sy) * t; ) :
      ( k == 3 ? ( t = (roomD - sy) / max(0.001, 2 * roomD - sy - ly); rpx = sx + (lx - sx) * t; rpy = roomD; ) : ( t = sx / max(0.001, sx + lx); rpx = 0; rpy = sy + (ly - sy) * t; ); ); );
      abs((lx - sx) * (rpy - sy) - (ly - sy) * (rpx - sx)) / max(0.01, sqrt(sqr(lx - sx) + sqr(ly - sy))) > 0.3 && tapOn[k] && tapG[k] > lbg ? ( lbg = tapG[k]; lbk = k; );
      k += 1;
    );
    lbk > 0 ? (
      lbk == 1 ? ( t = sy / max(0.001, sy + ly); rpx = sx + (lx - sx) * t; rpy = 0; msx = sx; msy = -sy; strcpy(#lw3, "back wall"); ) :
      lbk == 2 ? ( t = (roomW - sx) / max(0.001, 2 * roomW - sx - lx); rpx = roomW; rpy = sy + (ly - sy) * t; msx = 2 * roomW - sx; msy = sy; strcpy(#lw3, "right wall"); ) :
      lbk == 3 ? ( t = (roomD - sy) / max(0.001, 2 * roomD - sy - ly); rpx = sx + (lx - sx) * t; rpy = roomD; msx = sx; msy = 2 * roomD - sy; strcpy(#lw3, "front wall"); ) :
                 ( t = sx / max(0.001, sx + lx); rpx = 0; rpy = sy + (ly - sy) * t; msx = -sx; msy = sy; strcpy(#lw3, "left wall"); );
      PJ(sx, sy, 0); dx1 = qx; dy1 = qy; PJ(rpx, rpy, 0); rx = qx; ry = qy;
      col(cRMTT, 1); LNW(dx1, dy1, rx, ry, 2); PJ(lx, ly, 0); LNW(rx, ry, qx, qy, 2); CIF(rx, ry, 3, 1);
      PJ(msx, msy, 0);
      qx > VX + 10 && qx < VX + VW - 10 && qy > VY + 30 && qy < VY + VH - 30 ? (
        col(cRMTT, 0.75); DL(rx, ry, qx, qy, 4, 4, 1.2); DL(qx - 6, qy - 6, qx + 6, qy - 6, 2, 2, 1); DL(qx + 6, qy - 6, qx + 6, qy + 6, 2, 2, 1);
        DL(qx + 6, qy + 6, qx - 6, qy + 6, 2, 2, 1); DL(qx - 6, qy + 6, qx - 6, qy - 6, 2, 2, 1);
        gfx_setfont(7); txt("mirror source", qx + 10, qy, 0);
      );
      strcpy(#lw1, "1st reflection – "); strcat(#lw1, #lw3);
      sprintf(#lw2, "+%.1f ms after the direct sound · %.0f dB", max(0, tapT[lbk] - tapT[0]), 20 * log10(max(0.000001, lbg) / max(0.000001, tapG[0])));
      lbox(rx + 10, ry + 8, #lw1, #lw2, cRMTT);
    );
  );
  // the direct sound
  PJ(sx, sy, 0); dx1 = qx; dy1 = qy; PJ(lx, ly, 0);
  col(cSRC, 1); LNW(dx1, dy1, qx, qy, 2.5);
  sprintf(#lw2, "%.1f m · arrives after %.1f ms · %.0f dB", rdir, tapT[0], 20 * log10(max(0.000001, tapG[0])));
  lbox((dx1 + qx) / 2 + 14, (dy1 + qy) / 2 + 6, "Direct sound", #lw2, cSRC);
  // the reverb: in the corner of the room
  !isfree ? (
    PJ(0, roomD, 0); sprintf(#lw2, "thousands of reflections, after about %.0f ms – RT60 %.2f s", (brd + mfp) / C_SND * 1000, rt);
    lbox(qx + 10, qy - 44, "Reverb – from everywhere", #lw2, cAMB);
  );
);
// ---- tasks ----
function ltitle(t) (
  t == 0 ? "How far away is the voice?" : (t == 1 ? "Why does a small room sound boxy?" : (t == 2 ? "Hard walls, soft walls" : (t == 3 ? "What makes a room sound big?" :
  (t == 4 ? "A voice behind a door" : (t == 5 ? "Turn the speaker away" : (t == 6 ? "Three parts of every sound" : (t == 7 ? "A phone call in a car" :
  (t == 8 ? "Why does a passing car change its pitch?" : "Sound bends around a wall"))))))));
);
// Returns the text of step j (0..2) of Learn task t (the instruction shown in the Your turn panel)
function lstep(t, j) (
  t == 0 ? ( j == 0 ? "Drag the listener close to the source – closer than 1 m." : (j == 1 ? "Now drag it far away – more than 4 m." : "Listen: what changes besides the loudness? Tick it.") ) : (
  t == 1 ? ( j == 0 ? "Make the room smaller than 3 × 3 m – drag its front corner in the Stage View." : (j == 1 ? "Drag the listener into a corner, close to two walls." : "Listen to the bass: does it get boomier? Tick it.") ) : (
  t == 2 ? ( j == 0 ? "Turn α walls to 0.45 or more (like heavy curtains)." : (j == 1 ? "Watch What you hear: RT60 falls below 0.3 s." : "Listen to the end of a word: what is missing now? Tick it.") ) : (
  t == 3 ? ( j == 0 ? "Turn Size to 3× or more." : (j == 1 ? "In What you hear the gap between the green line and the first blue lines grows. Tick it." : "Listen: does it sound bigger, or only longer? Tick it.") ) : (
  t == 4 ? ( j == 0 ? "Switch the door on (Door below)." : (j == 1 ? "Drag source and listener to different sides of the wall." : "Open the door to more than 45°.") ) : (
  t == 5 ? ( j == 0 ? "Turn Src angle until the source looks away from the listener (about 180°)." : (j == 1 ? "Listen: darker and further away? Tick it." : "Turn it back until it faces the listener again.") ) : (
  t == 6 ? ( j == 0 ? "In What you hear play only Early: switch Direct and Late off." : (j == 1 ? "Now play only Late – the reverb tail." : "Switch all three on again.") ) : (
  t == 7 ? ( j == 0 ? "Load the room Car (button below)." : (j == 1 ? "Switch the source to Telephone (button below)." : "Listen: which part tells you car, which part phone? Tick it.") ) : (
  t == 8 ? ( j == 0 ? "Switch Pass-by on (button below): the source runs along a line past the listener." : (j == 1 ? "Listen as it passes: the pitch drops, and the sound moves from one ear to the other. Tick it." : "Make it faster: Duration 1 s or less – the pitch jump grows.") ) : (
           ( j == 0 ? "Switch the wall on (Wall below) – it stands between source and listener." : (j == 1 ? "Make it lower: Height 1.2 m (knob below) – the sound comes over the top." : "Listen: the treble comes back first, the bass stays muffled? Tick it.") ) )))))))));
);
// Returns the explanation text of Learn task t (what happens and why)
function lwhat(t) (
  t == 0 ? "Close up, the direct sound is much louder than the room: dry and present. Far away it drops 6 dB per doubling of the distance while the reverb stays almost the same – the voice sinks into the room. On the dashed circle both are equally loud." : (
  t == 1 ? "In a small hard room a few bass notes fit exactly between the walls and ring on – room modes. In a corner they are all at their loudest. Big rooms have so many modes that they melt into a smooth reverb." : (
  t == 2 ? "Soft surfaces swallow part of the sound at every bounce, so the reverb dies faster and gets darker – the room sounds dry like a vocal booth. Hard tiles and glass reflect almost everything and keep it ringing." : (
  t == 3 ? "The time between the direct sound and the first reflections tells your brain how far away the walls are. A big room has a long gap and a long reverb – together they sound big. A long reverb alone only sounds like a hard room." : (
  t == 4 ? "A closed door lets only part of the sound through, mostly the bass – the voice gets quiet and muffled. Open, the sound comes through the opening and bends around its edges: louder, clearer, and it seems to come from the door." : (
  t == 5 ? "Voices and loudspeakers send the treble mainly forward, the bass in all directions. Turned away, you hear less direct treble and more room – darker and further away, even at the same distance." : (
  t == 6 ? "The direct sound tells where the source is, the early reflections the size and shape of the room, the reverb how big and how hard it is. In a film mix these three parts are often shaped separately." :
  t == 7 ? "The tiny, soft car gives almost no reverb – only very early reflections that color the voice. The telephone cuts the bass below 300 Hz and the treble above 3.4 kHz. Both together: a call from a car." : (
  t == 8 ? "Three things happen at once. Doppler: while the source comes closer, each wave arrives a little earlier than the last – the pitch is higher; moving away, lower. At 5 m/s that is only 1.5 %, at 30 m/s (a car) 9 % – a whole tone. Direction: the ear nearer to the source hears it up to 0.7 ms earlier and louder, the head shadows the treble on the far side – this is what moves the sound across the stereo field. Distance: level and treble rise as it comes close and fall again." :
           "A wall stops the straight path, but sound bends around edges – the lower the frequency (the longer the wave), the better. Over a low wall the bass arrives almost as if there were no wall, the treble is shadowed. Through the wall itself only the bass gets through: everything behind a wall sounds muffled, and a lowered wall gives the treble back first.")))))));
);
// a step: 1 = done now, 0 = not yet, -1 = you tick it yourself (listening)
function lcond(t, j) local(dx, dy, dln, c)
(
  t == 0 ? ( j == 0 ? rdir < 1 : (j == 1 ? rdir > 4 : -1) ) : (
  t == 1 ? ( j == 0 ? roomW < 3.05 && roomD < 3.05 : (j == 1 ? (lx < 0.7 || lx > roomW - 0.7) && (ly < 0.7 || ly > roomD - 0.7) : -1) ) : (
  t == 2 ? ( j == 0 ? min(min(slider5, slider6), min(slider7, slider8)) >= 0.445 : (j == 1 ? rt < 0.3 : -1) ) : (
  t == 3 ? ( j == 0 ? slider31 >= 2.95 : -1 ) : (
  t == 4 ? ( j == 0 ? floor(slider41 + 0.5) == 3 : (j == 1 ? bon && !bsame : slider43 >= 50) ) : (
  // (lrnB & 2^k) = step k was already reached earlier - e.g. task 5: 'faces the listener again' only counts after the turn-away step
  t == 5 ? ( dx = lx - sx; dy = ly - sy; dln = max(0.001, sqrt(dx * dx + dy * dy)); c = (srAx * dx + srAy * dy) / dln;
             j == 0 ? c < -0.85 : (j == 1 ? -1 : c > 0.95 && (lrnB & 1) > 0) ) : (
  t == 6 ? ( j == 0 ? vopt[18] == 5 : (j == 1 ? vopt[18] == 3 : vopt[18] == 0 && (lrnB & 3) == 3) ) : (
  t == 7 ? ( j == 0 ? rsel == 13 : (j == 1 ? fzsel == 1 && slider85 >= 0.5 : -1) ) : (
  t == 8 ? ( j == 0 ? slider100 >= 0.5 && floor(slider102 + 0.5) == 4 : (j == 1 ? -1 : slider100 >= 0.5 && floor(slider102 + 0.5) == 4 && slider103 >= 0.95 && (lrnB & 2) > 0) ) : (
           ( j == 0 ? floor(slider41 + 0.5) == 1 && bon && !bsame : (j == 1 ? floor(slider41 + 0.5) == 1 && bon && slider96 > 0.05 && slider96 <= 1.3 : -1) ) )))))))));
);
// Show me: everything set so the result can be heard; Start again: back to where the task begins
function lshow(t) local(i)
(
  snap_take();
  t == 0 ? ( sset(10, roomW / 2 / rs); sset(11, min(0.6, roomD * 0.15) / rs); sset(13, roomW / 2 / rs); sset(14, (roomD - min(0.6, roomD * 0.15)) / rs); );
  t == 1 ? ( loadroom(0); sset(13, slider2 - 0.4 / max(0.01, slider31)); sset(14, slider3 - 0.4 / max(0.01, slider31)); );
  t == 2 ? ( sset(5, 0.45); sset(6, 0.45); sset(7, 0.45); sset(8, 0.45); );
  t == 3 ? ( sset(31, 3); );
  t == 4 ? ( sset(11, 0.22 * slider3); sset(14, 0.78 * slider3); sset(41, 3); sset(44, bmatrw(3, 1)); sset(42, 0.5 * slider3); sset(43, 55); );
  t == 5 ? ( sset(63, 0); sset(61, 180); );
  t == 6 ? ( vopt[18] = 5; );
  t == 7 ? ( loadroom(13); sset(35, 1); sset(37, 50); i = 47; loop(13, sset(i, 0); i += 1; ); sset(85, 1); );
  t == 8 ? ( sset(13, roomW / 2 / rs); sset(14, roomD * 0.6 / rs); sset(10, roomW / 2 / rs); sset(11, max(0.3, roomD * 0.6 - 1) / rs); sset(101, 0); sset(102, 4); sset(105, 0); sset(104, roomW / 2 / rs); sset(103, 0.33); sset(107, 1); sset(109, 0); sset(100, 1); );
  t == 9 ? ( sset(11, 0.22 * slider3); sset(14, 0.78 * slider3); sset(41, 1); sset(44, bmatrw(1, 1)); sset(42, 0.5 * slider3); sset(45, slider2 / 2); sset(95, 0); sset(96, 1.2); );
  snap_notify();
);
// Resets the room/settings for Learn task t after taking a snapshot, so the task starts from a clean state
function lreset(t)
(
  snap_take();
  t == 0 || t == 1 ? ( loadroom(2); );
  t == 2 ? ( sset(5, 0.04); sset(6, 0.04); sset(7, 0.04); sset(8, 0.04); );
  t == 3 ? ( sset(31, 1); );
  t == 4 ? ( sset(41, 0); sset(43, 0); );
  t == 5 ? ( sset(63, 0); sset(61, 0); );
  t == 6 ? ( vopt[18] = 0; );
  t == 7 ? ( loadroom(2); sset(35, 0); sset(37, 0); );
  t == 8 ? ( sset(100, 0); );
  t == 9 ? ( sset(41, 0); sset(96, 0); );
  snap_notify();
);
// the control a task needs (Learn has no modules): knob, dial or buttons
function lctl(t, x, yc) local(nf, v)
(
  t == 1 || t == 2 ? (
    v = (slider5 + slider6 + slider7 + slider8) / 4; sprintf(#fv, "%.2f", v); kmap(1, 0.01, 1, 1); kcol = cWALTT;
    nf = knob(72, x + 30, yc, "α walls", #fv, log2f(max(0.01, v), 0.01, 1), log2f(0.04, 0.01, 1));
    knobtip(x + 30, yc, "Absorption α of all four walls – 0.02 tiles, glass · 0.04 plaster · 0.45 heavy curtain · 0.85 mineral wool");
    kchg ? ( v = f2log(nf, 0.01, 1); setp(5, v); setp(6, v); setp(7, v); setp(8, v); );
  );
  t == 3 ? (
    sprintf(#fv, "%.2f×", slider31); kmap(1, 0.25, 4, 1); kcol = cRMTT;
    nf = knob(73, x + 30, yc, "Size", #fv, log2f(slider31, 0.25, 4), log2f(1, 0.25, 4));
    knobtip(x + 30, yc, "Size: the whole room and everything in it, larger or smaller");
    kchg ? setp(31, f2log(nf, 0.25, 4));
  );
  t == 4 ? (
    chip(x, yc - 8, 54, 16, "Door", floor(slider41 + 0.5) == 3, cBARTT) ? (
      snap_take(); floor(slider41 + 0.5) == 3 ? sset(41, 0) : ( btg == 0 ? bcenter(); sset(41, 3); sset(44, bmatrw(3, 1)); ); snap_notify();
    );
    btg >= 2 ? sprintf(#fv, "%.0f°", slider43 * 0.9) : strcpy(#fv, "–"); kmap(0, 0, 90, 1); kcol = cBARTT; kdim = btg <= 1;
    nf = knob(74, x + 100, yc, "Open", #fv, slider43 / 100, 0);
    knobtip(x + 100, yc, "Open: how far the door is open (0° = closed, 90° = wide open)");
    !lockE && kchg ? setp(43, nf * 100); kdim = 0;
  );
  t == 5 ? (
    kcol = cSRC; sprintf(#fv, abs(slider61 - floor(slider61 + 0.5)) > 0.05 ? "%.1f°" : "%.0f°", slider61);
    nf = dirknob(30, x + 30, yc, "Src angle", #fv, slider61, slider63 >= 0.5 ? 0 : (abs(lx - sx) + abs(ly - sy) > 0.001 ? atan2(-(lx - sx), ly - sy) * 180 / $pi : 0));
    knobtip(x + 30, yc, "Where the source points, as seen in the plan – drag around the dial");
    kchg && (!rbyp || isfree) ? setp(61, nf);
    kcol = cLIS; sprintf(#fv, abs(slider17 - floor(slider17 + 0.5)) > 0.05 ? "%.1f°" : "%.0f°", slider17); kdim = slider16 < 0.5;   // the listener's angle next to it
    nf = dirknob(35, x + 100, yc, "Lis angle", #fv, slider17, 180);
    knobtip(x + 100, yc, "Listener angle: where the listener looks – 0° = straight ahead (Facing Fixed)");
    kchg ? ( slider16 < 0.5 ? ( slider16 = 1; autom(32768); ); setp(17, nf); ); kdim = 0;
  );
  t == 8 ? (
    chip(x, yc - 8, 64, 16, "Pass-by", slider100 >= 0.5 && floor(slider102 + 0.5) == 4, cMOTT) ? ( snap_take(); slider100 >= 0.5 ? sset(100, 0) : ( sset(101, 0); sset(102, 4); sset(107, 1); sset(109, 0); sset(104, roomW / 2 / rs); sset(100, 1); ); snap_notify(); );
    sprintf(#fv, "%.1f s", 1 / slider103); kmap(1, 0.1, 50, 1); kcol = cMOTT;   // as in the Motion module - up = longer, typed seconds exact
    nf = knob(75, x + 110, yc, "Duration", #fv, 1 - log(slider103 / 0.02) / log(500), 1 - log(0.33 / 0.02) / log(500));
    knobtip(x + 110, yc, "Duration of one pass (A to B): up = longer, slower – shorter = faster = a bigger pitch jump");
    kchg ? setp(103, 0.02 * 500 ^ (1 - nf));
  );
  t == 9 ? (
    chip(x, yc - 8, 54, 16, "Wall", floor(slider41 + 0.5) == 1, cBARTT) ? (
      snap_take(); floor(slider41 + 0.5) == 1 ? sset(41, 0) : ( btg == 0 ? bcenter(); sset(41, 1); sset(44, bmatrw(1, 1)); ); snap_notify();
    );
    slider96 > 0.05 ? sprintf(#fv, "%.1f m", slider96) : strcpy(#fv, "full"); kmap(0, 0, 3, 1); kcol = cBARTT; kdim = floor(slider41 + 0.5) != 1;
    nf = knob(76, x + 100, yc, "Height", #fv, slider96 <= 0.05 ? 1 : min(1, slider96 / 3), 1);
    knobtip(x + 100, yc, "Height of the wall: full = floor to ceiling, lower = sound passes over the top and bends around its edge");
    kchg ? setp(96, nf >= 0.99 ? 0 : max(0.3, nf * 3)); kdim = 0;
  );
  t == 7 ? (
    chip(x, yc - 8, 54, 16, "Car", rsel == 13, cRMTT) ? ( loadroom(13); snlast = 0; );
    chip(x + 60, yc - 8, 76, 16, "Telephone", fzsel == 1 && slider85 >= 0.5, cSRTT) ? ( snap_take(); sset(35, 1); sset(37, 50); i = 47; loop(13, sset(i, 0); i += 1; ); sset(85, 1); snap_notify(); );
  );
  col(cMUTED, 1); gfx_setfont(7);
  t == 0 ? txtfit("Drag source and listener in the Stage View.", x, yc, LRW - 24) : (
  t == 6 ? txtfit("Use the chips Direct · Early · Late in What you hear.", x, yc, LRW - 24) : (
  t == 4 ? txtfit("then drag the two in the Stage View", x + 140, yc, LRW - 164) : (
  t == 7 || t == 8 || t == 9 ? 0 : txtfit("or Show me", x + 70, yc, LRW - 94) )));
  kcol = cTEXT;
);
// ---- words ----
function lword(k) ( k == 0 ? "Direct sound" : (k == 1 ? "Early reflections" : (k == 2 ? "Reverb" : (k == 3 ? "RT60" : (k == 4 ? "Absorption α" : (k == 5 ? "Critical distance" : (k == 6 ? "Room modes" : "Damping dB")))))); );
// Returns the definition text of Learn word k (direct, early, late, RT60, absorption, critical distance, modes, loss)
function lwdef(k) (
  k == 0 ? "The sound that travels straight from the source to your ears – the first and loudest part. It tells you where the source is, and it gets 6 dB quieter with every doubling of the distance." : (
  k == 1 ? "The first echoes from walls, floor and ceiling, within about 50–80 ms. You do not hear them as echoes: they melt into the direct sound and tell your brain the size and shape of the room." : (
  k == 2 ? "Thousands of reflections that come from everywhere and fade away together. It has no direction any more – it tells how big the room is and what its surfaces are made of." : (
  k == 3 ? "Reverb time: how long the sound needs to fade away by 60 dB, to a thousandth. A car about 0.1 s, a living room 0.5 s, a classroom 0.7 s, a church 5 s." : (
  k == 4 ? "How much of the sound a surface swallows instead of reflecting it: 0 = a perfect mirror (tiles and glass about 0.02), 1 = nothing comes back (an open window). A heavy curtain 0.45, mineral wool 0.85." : (
  k == 5 ? "The distance from the source where direct sound and reverb are equally loud – the dashed circle. Closer: clear and present. Further: the room takes over." : (
  k == 6 ? "In small rooms some bass notes fit exactly between two walls and ring on longer than the rest – the room sounds boomy or boxy. In a corner they are at their loudest." :
           "How much quieter the sound gets through a wall, door or window: 25 dB a light interior door, 45 dB a brick wall. Bass gets through more easily than treble – that is why music next door sounds muffled."))))));
);
// Returns the 'Try:' hint line for Learn word k
function lwtry(k) (
  k == 0 ? "Try: drag the listener away and watch the green line in What you hear." : (k == 1 ? "Try: play only Early, then move the source close to a wall." : (k == 2 ? "Try: play only Late." : (
  k == 3 ? "Try: Your turn, task 3 – soft walls." : (k == 4 ? "Try: Your turn, task 3." : (k == 5 ? "Try: drag the listener across the dashed circle." : (k == 6 ? "Try: Your turn, task 2." : "Try: Your turn, task 5 – a voice behind a door."))))));
);
// ---- the three panels ----
function lpan1()
(
  card(LPX, LQ1, LRW, LP1);
  col(cAMB, 1); RR(LPX + 10, LQ1 + 6, 3, 12, 1.5); col(cTEXT, 1); gfx_setfont(2); txt("What you hear", LPX + 19, LQ1 + 12, 0);
  lrnTL = 1; EX = LPX; EY = LQ1 + 8; EW = LRW; EH = LP1 - 50; gfxpT(); lrnTL = 0;
  rt < 0.3 ? strcpy(#lw2, "very dry, like a vocal booth or a car") : (rt < 0.7 ? strcpy(#lw2, "like a living room or an office") : (rt < 1.3 ? strcpy(#lw2, "like a bathroom or a classroom") : (rt < 2.5 ? strcpy(#lw2, "like a concert hall") : strcpy(#lw2, "like a church or a tunnel"))));
  sprintf(#lw1, "Reverb time RT60 = %.2f s – ", rt); strcat(#lw1, #lw2);
  col(cTEXT, 1); gfx_setfont(2); txtfit(#lw1, LPX + 12, LQ1 + LP1 - 26, LRW - 24);
  col(cMUTED, 1); gfx_setfont(7); txtfit("the time the sound needs to fade away by 60 dB · Direct / Early / Late: listen to one part alone", LPX + 12, LQ1 + LP1 - 11, LRW - 24);
);
// Learn panel 2: 'Your turn' task list with steps, auto-checked conditions, the needed controls and the What happened text
function lpan2() local(k, y, c, h, on, wy, done, lkn)
(
  card(LPX, LQ2, LRW, LP2);
  lrnT = min(9, max(0, floor(vopt[44] + 0.5)));
  lrnT != lrnTp ? ( lrnB = 0; lrnShow = 0; lrnTp = lrnT; );
  col(cRMTT, 1); RR(LPX + 10, LQ2 + 6, 3, 12, 1.5); col(cTEXT, 1); gfx_setfont(2); sprintf(#lw1, "Your turn – task %d of 10", lrnT + 1); txt(#lw1, LPX + 19, LQ2 + 12, 0);
  rbtn(LPX + LRW - 36, LQ2 + 12, 6, "‹", 0, cTEXT, "Previous task") ? ( vopt[44] = (lrnT + 9) % 10; );
  rbtn(LPX + LRW - 18, LQ2 + 12, 6, "›", 0, cTEXT, "Next task") ? ( vopt[44] = (lrnT + 1) % 10; );
  chip(LPX + LRW - 182, LQ2 + 5, 58, 14, "Show me", lrnShow, cRMTT) ? ( lshow(lrnT); lrnShow = 1; );
  tipbox(LPX + LRW - 182, LQ2 + 5, 58, 14, "Show me: sets everything so you can hear the result – and shows what happened");
  chip(LPX + LRW - 120, LQ2 + 5, 72, 14, "Start again", 0, cTEXT) ? ( lreset(lrnT); lrnB = 0; lrnShow = 0; );
  tipbox(LPX + LRW - 120, LQ2 + 5, 72, 14, "Start again: back to where this task begins");
  col(cTEXT, 1); gfx_setfont(3); h = wrapt(ltitle(lrnT), LPX + 12, LQ2 + 38, LRW - 24, 15);
  y = LQ2 + 40 + h + 4; k = 0;
  loop(3,
    c = lcond(lrnT, k); c > 0 ? ( lrnB = lrnB | (2 ^ k); );
    on = (lrnB & (2 ^ k)) > 0;
    on ? ( col(cRMTT, 1); RR(LPX + 12, y - 6, 12, 12, 3); col(cSURF, 1); LNW(LPX + 15, y, LPX + 17.5, y + 3, 1.6); LNW(LPX + 17.5, y + 3, LPX + 21, y - 3, 1.6); )
       : ( col(c < 0 ? cTEXT : cBORD, 1); RRO(LPX + 12, y - 6, 12, 12, 3); );
    c < 0 && click && inbox(LPX + 8, y - 8, LRW - 16, 18) ? ( lrnB = on ? lrnB - (2 ^ k) : lrnB | (2 ^ k); click = 0; );
    tipbox(LPX + 8, y - 8, 22, 18, c < 0 ? "Tick it yourself when you have listened" : "This ticks itself when it is done");
    col(on ? cMUTED : cTEXT, 1); gfx_setfont(13); h = wrapt(lstep(lrnT, k), LPX + 32, y, LRW - 44, 13);
    y += max(18, h + 6); k += 1;
  );
  lkn = (lrnT >= 1 && lrnT <= 5) || lrnT >= 8;                                           // tasks with a knob need its height, the others one line
  lctl(lrnT, LPX + 12, y + (lkn ? 28 : 12));
  wy = y + (lkn ? 64 : 28); done = lrnB == 7 || lrnShow;
  // the box is as tall as its text (measured in the frame before): the panel - and the page - only take what they need
  col(done ? cVIEWT : cBG, 1); RR(LPX + 10, wy, LRW - 20, max(26, lp2wh), 6);
  done ? ( col(cRMTT, 1); gfx_setfont(6); txt("What happened", LPX + 18, wy + 11, 0);
           col(cTEXT, 1); gfx_setfont(13); h = wrapt(lwhat(lrnT), LPX + 18, wy + 27, LRW - 36, 13); lp2wh = 27 + h + 8; )
       : ( col(cMUTED, 1); gfx_setfont(7); h = wrapt("Do the three steps – then the explanation appears here. Stuck? Show me.", LPX + 18, wy + 13, LRW - 36, 11); lp2wh = 13 + h + 6; );
  lp2need = wy + max(26, lp2wh) + 8 - LQ2;
);
// Learn panel 3: 'Words' glossary - word chips plus definition and try-it hint of the selected word
function lpan3() local(k, x, y, w, h, h2)
(
  card(LPX, LQ3, LRW, LQH);
  col(cRVTT, 1); RR(LPX + 10, LQ3 + 6, 3, 12, 1.5); col(cTEXT, 1); gfx_setfont(2); txt("Words", LPX + 19, LQ3 + 12, 0);
  x = LPX + 10; y = LQ3 + 26; k = 0;
  loop(8,
    gfx_setfont(6); w = tws(lword(k)) + 14;
    x + w > LPX + LRW - 10 ? ( x = LPX + 10; y += 18; );
    chip(x, y, w, 14, lword(k), lrnW == k, cRVTT) ? ( lrnW = k; );
    x += w + 4; k += 1;
  );
  y += 22;
  col(cBG, 1); RR(LPX + 10, y, LRW - 20, LQ3 + LQH - 8 - y, 6);
  col(cRVTT, 1); gfx_setfont(6); txt(lword(lrnW), LPX + 18, y + 11, 0);
  col(cTEXT, 1); gfx_setfont(13); h = wrapt(lwdef(lrnW), LPX + 18, y + 27, LRW - 36, 13);
  col(cRMTT, 1); gfx_setfont(14); h2 = wrapt(lwtry(lrnW), LPX + 18, y + 30 + h, LRW - 36, 13);
  lp3need = y + 30 + h + h2 + 10 + 8 - LQ3;                                // (the panel takes what its text needs)
);
// Learn mode: draws the three Learn panels (What you hear, Your turn, Words)
function gfxpL() ( lpan1(); lpan2(); lpan3(); );
// Heading and chrome of the Stage View: fold/zoom/lock/info, morph, hide, view tabs, start positions, snapshot name
function gfxp4()
(
// ---------- 3D room ----------
card(VX, VY, VW, VH);
zoomv ? ( zxbX = gox + (VX + VW) * gs; zxbY = goy + VY * gs; zxbOn = 1; );
vwopen = zoomv || learn || secop[9 + basic * 12] >= 1;
// heading of the 3D view: fold arrow + name, zoom magnifier, fold / open all sections
vay = VY + 12;
!zoomv ? (
  vop = secop[9 + basic * 12]; vax = VX + 10; vvi = basic ? 13 : 12;
  vop > 0.5 ? chevd(vax, vay, vlock ? cDIM2 : cMUTED) : chevr(vax, vay, vlock ? cDIM2 : cMUTED);
  col(cDIM2, 1); RR(VX + 19, vay - 6, 3, 12, 1.5); col(cTEXT, 1); gfx_setfont(2); txt("Stage View", VX + 28, vay, 0);
  vhw = 30 + tws("Stage View");
  !vlock && !learn && click && inbox(VX + 2, VY + 2, vhw, 20) ? ( (mouse_cap & 16) ? foldall() : ( (vopt[vvi] & 512) ? ( vopt[vvi] -= 512; ) : ( vopt[vvi] += 512; ); ); );
  tipbox(VX + 2, VY + 2, vhw - 14, 20, vop > 0.5 ? "Click: fold the Stage View (Alt+click: fold everything)" : "Click: open the Stage View (Alt+click: open everything)");
  ZBX = VX + 28 + tws("Stage View") + 12;
) : ( ZBX = VX + 16; );
// zoom: magnifier, the same symbol as in the sections (+ = enlarge, - = back)
zrh = sqr(mx - ZBX) + sqr(my - vay) < 49;
col(zoomv ? cRMTT : (zrh ? cTEXT : cMUTED), 1); SDB0(); SDARC(ZBX - 1, vay - 1, 3.5, 0, 2 * $pi); SDSEG(ZBX + 1.6, vay + 1.6, ZBX + 4.3, vay + 4.3); SDSEG(ZBX - 2.6, vay - 1, ZBX + 0.6, vay - 1); !zoomv ? SDSEG(ZBX - 1, vay - 2.6, ZBX - 1, vay + 0.6); SDDRAW(1.05);

!zoomv ? ( svzX = gox + VX * gs; svzY = goy + VY * gs; svzW = VW * gs; svzH = VH * gs; svzF = mzfr; );   // the Stage View's place in every mode (Tabs, Basic, Learn too) - key Z over it zooms it
!basic && !learn && !tabm && !zoomv && !szoom ? ( MZR[0] = gox + VX * gs; MZR[1] = goy + VY * gs; MZR[2] = VW * gs; MZR[3] = VH * gs; MZR[80] = mzfr; );   // the Stage View's place - key Z zooms it (All / Fit)
click && zrh ? ( zoomv = !zoomv; vvi = basic ? 13 : 12; zoomv && basic && (vopt[vvi] & 512) ? ( vopt[vvi] -= 512; ); click = 0; );   // Expert: a hidden view stays hidden after the zoom
zrh ? ( tipon = 1; tipforce = 0; tipstr = zoomv ? "Back to the normal view (Esc or key Z)" : "Zoom: the Stage View alone, over the whole window (All / Fit: key Z with the mouse over it)"; tipx = ZBX; tipy = vay + 16; );
// (the fold-all arrows are gone - Alt+click on a heading folds / opens everything)
!zoomv ? (
  FBX = ZBX + 1;
  // (the layout lock is Lock GUI in the signal-flow strip - here vlock only dims the fold arrow and blocks folding)
  // INFO: shows / hides the window with the live numbers
  INX = FBX + 17; ion = vopt[19] >= 0.5;
  !basic && !learn ? ( rbtn(INX, vay, 5.5, "i", ion, cTEXT, ion ? "Hide the info window" : "Info window: distance, critical distance and direct / room ratio, live") ? ( vopt[19] = ion ? 0 : 1; ); ) : ( INX = FBX + 3; );
  SIX = INX + 14;
);
zoomv ? ( SIX = ZBX + 14; );
// MORPH in the heading (tabs): a fader beside the snapshot name - off, 1 … 16 = that snapshot, between = a blend; automatable
tabm && !zoomv ? (
  mrk = slider64; mphx = VX + VW - 78 - 182 - 150;
  mrk < 1 ? strcpy(#fv, "off") : ( abs(mrk - floor(mrk + 0.5)) < 0.005 ? sprintf(#fv, "snapshot %d", floor(mrk + 0.5)) : sprintf(#fv, "%d → %d  %.0f %%", floor(mrk), floor(mrk) + 1, (mrk - floor(mrk)) * 100) );
  kmap(0, 0, 16, 1);
  nf = hsl(33, mphx, vay - 7, 136, "Morph", #fv, mrk / 16, 0, cRMTT);
  tipbox(mphx - 4, vay - 9, 144, 18, "Snapshot morph (automatable): off, 1 … 16 = that snapshot, in between = blend into the next one (3.5 = half 3, half 4). Automate it in REAPER to change rooms with the picture – double-click = type a value");
  kchg ? setp(64, nf < 0.5 / 16 ? 0 : nf * 16);
);
// HIDE / SHOW the room view (Expert, also key V): hidden = a bar with a live mini plan, every other module gets the room
!basic && !learn && !zoomv ? (
  vhx = VX + VW - 70; vhon = (vopt[12] & 512) > 0;
  chip(vhx, vay - 7, 62, 14, vhon ? "Show view" : "Hide view", vhon, cTEXT) ? ( !vlock ? ( vopt[12] = vhon ? vopt[12] - 512 : vopt[12] + 512; ); click = 0; );
  tipbox(vhx, vay - 7, 62, 14, vhon ? "Show the Stage View again (key V) – the zoom magnifier shows it over the whole window" : "Hide the Stage View (key V): the other modules get its place; a mini plan stays, the zoom magnifier shows the view over the whole window");
);
// ROOM VIEW TABS: Plan (from above, the standard), 3D; Expert also Timeline and RT60 per octave - drawn in the view
gk = 0; vtx = SIX;
loop(basic || learn ? 2 : 4,
  vtw = gk == 0 ? 34 : (gk == 1 ? 26 : (gk == 2 ? 52 : 40));
  chip(vtx, vay - 7, vtw, 14, gk == 0 ? "Plan" : (gk == 1 ? "3D" : (gk == 2 ? "Timeline" : "RT60")), vtab == gk, cTEXT) ? ( vopt[40] = gk; vtab = gk; planv = gk == 0; click = 0; vvi = basic ? 13 : 12; (vopt[vvi] & 512) ? ( basic ? ( !vlock ? ( vopt[vvi] -= 512; ); ) : ( zoomv = 1; ); ); );   // hidden: Expert shows it over the window, Basic opens it
  tipbox(vtx, vay - 7, vtw, 14, gk == 0 ? "Plan: the room from above, to scale – positions, distances and the barrier are easy to set" : (gk == 1 ? "3D: the room in perspective – heights of source and listener (Shift+drag)"
    : (gk == 2 ? "Timeline: direct sound, early reflections and the reverb tail over time – what reaches the listener" : "RT60 per octave: the reverb time in each frequency band as the reverb plays it, the room modes and the numbers behind them")));
  vtx += vtw + 2; gk += 1;
);
SIX = vtx + 8;
// START POSITIONS (every view, also in the zoom): source and listener back to their start places, the room stays.
// Right after the tabs; short label when the heading is narrow
rpbx = SIX - 4; rpbr = VX + VW - (zoomv ? 40 + kbRs : (basic || learn ? 10 : 78)) - rpbx;
tabm && !zoomv ? rpbr = min(rpbr, mphx - 10 - rpbx);   // (Tabs: the Morph fader sits in this row - the chips stop before it)
rpbw = rpbr >= 88 ? 86 : (rpbr >= 58 ? 56 : 34);
vtab == 1 && rpbr < rpbw + 112 ? ( rpbw = rpbr >= 56 + 112 ? 56 : (rpbr >= 34 + 112 ? 34 : (rpbr >= 56 + 64 ? 56 : (rpbr >= 34 + 64 ? 34 : rpbw))); );   // 3D tab: a shorter Reset button when that makes room for Side and Front (or at least Side)
rpbr >= 36 ? (
  chip(rpbx, vay - 7, rpbw, 14, rpbw > 80 ? "Reset positions" : (rpbw > 40 ? "Reset pos." : "S L"), 0, cTEXT) ? ( !vlock ? posreset(); click = 0; );
  tipbox(rpbx, vay - 7, rpbw, 14, (lockS || lockL) ? "Source and listener back to their start places (listener in the middle, source 2 m in front) – the room stays as it is; locked ones stay where they are" : "Source and listener back to their start places: listener in the middle of the room, source 2 m in front of it, 1.5 m high, facing straight ahead – the room stays as it is");
  SIX = rpbx + rpbw + 8;
) : ( rpbx = SIX; rpbw = 0; );
// SIDE VIEW chip (3D tab): beside the start positions
vtab == 1 && rpbw > 0 && rpbr >= rpbw + 64 ? (   // (room for Side; Front only when there is room for both)
  chip(SIX - 4, vay - 7, 40, 14, "Side", sidev == 1, cTEXT) ? ( sidev = sidev == 1 ? 0 : 1; click = 0; );
  tipbox(SIX - 4, vay - 7, 40, 14, "Side view: the room from the side, to scale – the back wall at the left. Drag source or listener to set the depth and the height, drag the barrier line to move it. Click again: back to 3D");
  rpbr >= rpbw + 112 && (basic || learn || SIX + 84 < mphx - 6) ? (
    chip(SIX + 40, vay - 7, 44, 14, "Front", sidev == 2, cTEXT) ? ( sidev = sidev == 2 ? 0 : 2; click = 0; );
    tipbox(SIX + 40, vay - 7, 44, 14, "Front view: the room seen from the front wall, to scale – left / right and the height, the barrier's door or window upright. Drag source or listener to set left / right and the height. Click again: back to 3D");
    SIX += 88;
  ) : ( sidev == 2 ? sidev = 0; SIX += 44; );
);
// SNAPSHOT INFO: the active snapshot (slot + name); " *" = changed since it was loaded / stored
snik = snmk > 0 ? snmk : snlast;
slider64 >= 1 ? (
  mra2 = floor(slider64); mrf2 = slider64 - mra2;
  mrf2 > 0.005 && mra2 < 16 && uslot[mra2 * USL] ? sprintf(#sni, "Morph %d → %d  %.0f %%", mra2, mra2 + 1, mrf2 * 100) : sprintf(#sni, "Snapshot %d (morph)", mra2);
  col(cRMTT, 1); CI(SIX + 2, vay, 2, 1); col(cTEXT, 1); gfx_setfont(6); txtfit(#sni, SIX + 8, vay, (basic && (vwopen || zoomv) ? VX + VW - 220 : VX + VW - (basic ? 10 : (zoomv ? 10 + kbRs : (tabm ? 426 : 80)))) - SIX - 8);
) : snik > 0 && uslot[(snik - 1) * USL] ? (
  sprintf(#sni, "Snapshot %d", snik);
  snam[(snik - 1) * SNL] > 0 ? ( strcat(#sni, " – "); strcat(#sni, sn_str(snam + (snik - 1) * SNL, #snt)); );
  snmk == 0 ? strcat(#sni, " *");
  col(cRMTT, 1); CI(SIX + 2, vay, 2, 1);
  sniw = (basic && (vwopen || zoomv) ? VX + VW - 220 : VX + VW - (basic ? 10 : (zoomv ? 10 + kbRs : (tabm ? 426 : 80)))) - SIX - 8;
  col(cTEXT, 1); gfx_setfont(6); txtfit(#sni, SIX + 8, vay, sniw);
  dsc[(snik - 1) * DSL] > 0 ? (
    gfx_setfont(6); sniw2 = min(sniw, tws(#sni)); col(cMUTED, 1); LN(SIX + 8, vay + 6, SIX + 8 + sniw2, vay + 6);   // underline = has a description
    inbox(SIX - 2, vay - 7, sniw2 + 12, 14) ? ( tipon = 1; tipforce = 0; tipstr = sn_strn(dsc + (snik - 1) * DSL, DSL, #snt2); tipx = SIX + 8 + sniw2 / 2; tipy = vay + 18; );
  );
);
// MINI PLAN (view folded, Expert and Basic): the room from above - walls, barrier, source, listener; a click = the view over the window
!learn && !zoomv && secop[9 + basic * 12] < 0.02 ? (
  mmh = VFH - 32; mmS = min((VW - 200) / max(0.1, roomW), mmh / max(0.1, roomD)); mmW = roomW * mmS; mmD = roomD * mmS;
  mmx = VX + 12; mmy = VY + 26 + (mmh - mmD) / 2;
  col(cBG, 1); RR(VX + 8, VY + 24, mmW + 8, mmh + 4, 4);
  !isfree ? ( col(cWALTT, 1); RRO(mmx, mmy, mmW, mmD, 1); ) : ( col(cDIM2, 1); DL(mmx, mmy + mmD, mmx + mmW, mmy + mmD, 3, 3, 1); );
  bon ? ( col(cBARV, 1); LNW(mmx + brX0 * mmS, mmy + yb * mmS, mmx + brX1 * mmS, mmy + yb * mmS, 2);
          bew > 0 && slider43 > 0.5 ? ( col(cSURF, 1); LNW(mmx + ax0 * mmS, mmy + yb * mmS, mmx + ax1 * mmS, mmy + yb * mmS, 2); ); );
  // what is under the mouse: listener first (as in the big view), then source, then the barrier line
  mmhit = inbox(VX + 8, VY + 24, mmW + 8, mmh + 4);
  mmdS = sqrt(sqr(mx - (mmx + sx * mmS)) + sqr(my - (mmy + sy * mmS)));
  mmdL = sqrt(sqr(mx - (mmx + lx * mmS)) + sqr(my - (mmy + ly * mmS)));
  mmdB = bon && !isfree && mx >= mmx && mx <= mmx + mmW ? abs(my - (mmy + yb * mmS)) : 99;
  mmdC = !isfree ? sqrt(sqr(mx - (mmx + mmW)) + sqr(my - (mmy + mmD))) : 99;    // the front right corner: the whole room
  mmov = mmdrag ? mmdrag : (!mmhit ? 0 : (mmdL < 8 && !lockL ? 2 : (mmdS < 8 && !lockS ? 1 : (mmdC < 8 && !vlock ? 4 : (mmdB < 4 && !lockB ? 3 : 0)))));
  // S = source (green, the color of the Source module), L = listener (white, dark gray in light mode): a letter in each dot, larger under the mouse
  mmr = mmov == 1 ? 7 : 6; col(cSRTT, 1); CIF(mmx + sx * mmS, mmy + sy * mmS, mmr, 1);
  col(cSURF, 1); gfx_setfont(8, "Arial", floor(8 * gs + 0.5), 'b'); txtc("S", mmx + sx * mmS, mmy + sy * mmS);
  mmr = mmov == 2 ? 7 : 6; col(cLIS, 1); CIF(mmx + lx * mmS, mmy + ly * mmS, mmr, 1);
  col(cSURF, 1); gfx_setfont(8, "Arial", floor(8 * gs + 0.5), 'b'); txtc("L", mmx + lx * mmS, mmy + ly * mmS);
  mmov == 3 ? ( col(cBARV, 1); LNW(mmx, mmy + yb * mmS, mmx + mmW, mmy + yb * mmS, 3); );
  mmhit || mmdrag ? ( col(cRMTT, 1); RRO(VX + 8, VY + 24, mmW + 8, mmh + 4, 4); );
  // corner grip (front right): drag = the whole room larger or smaller, in proportion
  !isfree ? ( col(cSURF, 1); CIF(mmx + mmW, mmy + mmD, mmov == 4 ? 4.5 : 3.5, 1); col(mmov == 4 ? cRMTT : cWALTT, 1); CIF(mmx + mmW, mmy + mmD, mmov == 4 ? 4.5 : 3.5, 0);
              LNW(mmx + mmW - 1.6, mmy + mmD + 1.6, mmx + mmW + 1.6, mmy + mmD - 1.6, 1); );
  // MINI PLAN: drag the source, the listener or the barrier (locks apply); Alt+click or double-click = the view over the window
  click && mmhit ? (
    (mouse_cap & 16) ? ( zoomv = 1; ) : (
      mmov ? ( mmdrag = mmov;
        mmov == 4 ? ( mm0x = mx; mm0y = my; mmcx = mmW; mmcy = mmD; dW0 = slider2; dD0 = slider3; dH0 = slider4;
                      dSX0 = slider10; dSY0 = slider11; dSZ0 = slider12; dLX0 = slider13; dLY0 = slider14; dLZ0 = slider15; dBP0 = slider42; dBE0 = slider45; dMC0x = mtC0x; dMC0y = mtC0y; dMC1x = mtC1x; dMC1y = mtC1y; dAP0x = MTAP[0]; dAP0y = MTAP[1]; );
      ) : ( time_precise() - mmtc < 0.35 ? ( zoomv = 1; ); mmtc = time_precise(); );
    );
    click = 0;
  );
  mmdrag ? (
    (mouse_cap & 1) ? (
      mmix = (mx - mmx) / mmS; mmiy = (my - mmy) / mmS;
      mmdrag <= 2 ? (
        axcon(mmdrag, mmix / rs, mmiy / rs, 4); moveobj(mmdrag, axNX, axNY);    // Ctrl/Cmd: on a line only
        axOn ? (
          col(mmdrag == 1 ? cSRTT : cLIS, 1);
          axA != 2 ? ( DL(mmx, mmy + axY0 * rs * mmS, mmx + mmW, mmy + axY0 * rs * mmS, 3, 3, 1); );
          axA != 1 ? ( DL(mmx + axX0 * rs * mmS, mmy, mmx + axX0 * rs * mmS, mmy + mmD, 3, 3, 1); );
        );
      ) : ( mmdrag == 4 ? (
        // the room in proportion along the line origin -> corner (as the front corner in the big view); Shift = fine
        ck = 1 + ((mx - mm0x) * mmcx + (my - mm0y) * mmcy) / max(1, mmcx * mmcx + mmcy * mmcy) * ((mouse_cap & 8) ? 0.2 : 1);
        ck = min(min(40 / dW0, 40 / dD0), min(15 / dH0, max(0.8 / max(dW0, max(dD0, dH0)), ck)));
        ckx = max(0.8, dW0 * ck) / dW0; cky = max(0.8, dD0 * ck) / dD0; ckz = max(0.8, dH0 * ck) / dH0;
        setp(2, dW0 * ckx); setp(3, dD0 * cky); setp(4, dH0 * ckz);
        !lockS ? ( setp(10, dSX0 * ckx); setp(11, dSY0 * cky); setp(12, dSZ0 * ckz); );
        !lockL ? ( setp(13, dLX0 * ckx); setp(14, dLY0 * cky); setp(15, dLZ0 * ckz); );
        mtEg > 0.001 ? ( mtC0x = dMC0x * ckx; mtC0y = dMC0y * cky; mtC1x = dMC1x * ckx; mtC1y = dMC1y * cky; MTAP[2] >= 0.5 ? ( MTAP[0] = dAP0x * ckx; MTAP[1] = dAP0y * cky; ); );   // a placed Own spot scales with it
        !lockB ? ( setp(42, dBP0 * cky); ); !lockE ? ( setp(45, dBE0 * ckx); );
      ) : ( setp(42, min(max(mmiy, ybm), roomD - ybm) / rs); ); );
    ) : ( mmdrag = 0; axOn = 0; );
  );
  (mmhit || mmdrag) && !(mmdrag && mmov == 4) ? ( tipon = 1; tipforce = 0; tipx = VX + 8 + (mmW + 8) / 2; tipy = VY + 24 + mmh + 18;
    mmov == 4 ? sprintf(#mmtp, mmdrag ? "%.2f × %.2f × %.2f m" : "Drag the corner: the whole room larger or smaller (Shift = fine) – %.2f × %.2f × %.2f m", slider2 * rs, slider3 * rs, slider4 * rs);
    tipstr = mmov == 4 ? #mmtp : (mmov == 1 ? "Drag: move the source" : (mmov == 2 ? "Drag: move the listener" : (mmov == 3 ? "Drag: move the barrier" : "Drag source, listener or barrier · Alt+click or double-click: the Stage View over the whole window"))); );
  mtx0 = VX + mmW + 26;
  col(cMUTED, 1); gfx_setfont(4); txt("Stage View hidden", mtx0, VY + 38, 0);
  gfx_setfont(7); txtfit("Drag source, listener, barrier or the corner (room size) here  ·  Alt+click = over the whole window  ·  V = back", mtx0, VY + 54, VX + VW - 10 - mtx0);
  rdir > 0 ? ( sprintf(#w1, "Distance %.2f m  ·  RT60 %.2f s", rdir, rt); col(cTEXT, 1); gfx_setfont(6); txtfit(#w1, mtx0, VY + 72, VX + VW - 10 - mtx0); );
);
vwopen ? ( clX0 = VX + 8; clX1 = VX + VW - 8; clY0 = VY + 24; clY1 = VY + VH - (vtab >= 2 ? 4 : 26); gclp = 1; vtab < 2 ? (gfxp1();
gfxp2();
gfxp3();
mtRec ? (   // the hint at the bottom right of the view (not over the room), beside the source pill
  gfx_setfont(6); strcpy(#mtbt, drag == 20 ? (planv ? "Drawing – let go to use the path" : "Drawing – hold Shift: up / down = height") : (planv ? "Draw the motion path: hold the mouse button and draw in the room" : "Draw the path in the room – hold Shift for the height"));
  mtBw = tws(#mtbt) + 24; VX + VW - 10 - mtBw < VX + 156 ? ( strcpy(#mtbt, drag == 20 ? "Drawing…" : "Draw the path in the room"); mtBw = tws(#mtbt) + 24; );
  col(cMOTT, 1); RR(VX + VW - 10 - mtBw, VY + VH - 50, mtBw, 20, 7); col(cSURF, 1); txt(#mtbt, VX + VW - 10 - mtBw / 2, VY + VH - 40, 1);
);
learn && planv ? lanno()) : gfxpT(); gclp = 0;
  vtab < 2 ? (
    vzcX1 = vzcX0; vzcX0 = VX + VW;   // (the row of the last frame stays readable while this one is built)
    // FIT: the room / the area and the reflectors fill the view. The chip shows when something sticks out of the view or it is zoomed
    // after Fit: a new area / room size (when nothing is held any more) fits again by itself
    vzFitOn && (vtab != vzFitT || sidev != vzFitS || isfree != vzFitF) ? ( vzFitOn = 0; );   // another view: its own zoom again
    vzFitOn && drag == 0 && abs(roomW * 3.1 + roomD * 7.3 + pHt * 11.9 - vzFitK) > 0.001 ? ( vzfit(); vzFitK = roomW * 3.1 + roomD * 7.3 + pHt * 11.9; );
    vzfbox(); vzbx = VX + VW - 16; vzby = VY + 28;
    vzOut = vzbX0 < VX + 8 || vzbX1 > VX + VW - 8 || vzbY0 < VY + 24 || vzbY1 > VY + VH - 26;
    vzOut || vzOn ? (
      gfx_setfont(6); vzw = tws("Fit") + 16; vzbx -= vzw;
      vzh = inbox(vzbx, vzby, vzw, 16); col(vzh ? cTEXT : cSURF, 0.95); RR(vzbx, vzby, vzw, 16, 8); col(cBORD, 1); RRO(vzbx, vzby, vzw, 16, 8);
      col(vzh ? cSURF : cTEXT, 1); txt("Fit", vzbx + vzw / 2, vzby + 8, 1);
      vzcX0 = min(vzcX0, vzbx);   // the chips of this frame: a clamped reflector handle must not land under them
      click && vzh ? ( vzfit(); vzFitOn = 1; vzFitK = roomW * 3.1 + roomD * 7.3 + pHt * 11.9; vzFitT = vtab; vzFitS = sidev; vzFitF = isfree; click = 0; );
      tipbox(vzbx, vzby, vzw, 16, "Fit: zoom so the whole room / free-field area and the reflectors are in view (the handles, too). Mouse wheel over the empty floor: zoom in / out");
      vzbx -= 6;
    );
  );
  vzOn && vtab < 2 ? (
    sprintf(#vzt, "%.0f %%  ·  1:1", vzf * 100); gfx_setfont(6); vzw = tws(#vzt) + 16; vzbx -= vzw; vzcX0 = min(vzcX0, vzbx);
    vzh = inbox(vzbx, vzby, vzw, 16); col(vzh ? cTEXT : cSURF, 0.95); RR(vzbx, vzby, vzw, 16, 8); col(cBORD, 1); RRO(vzbx, vzby, vzw, 16, 8);
    col(vzh ? cSURF : cTEXT, 1); txt(#vzt, vzbx + vzw / 2, vzby + 8, 1);
    click && vzh ? ( vzf = 1; vzox = vzoy = 0; vzFitOn = 0; click = 0; );
    tipbox(vzbx, vzby, vzw, 16, "View zoom (mouse wheel over the empty floor: in and out) – click: back to 1:1. The room keeps its size");
  );
  vtab < 2 && !lrnTL && VW >= 230 && VH >= 170 ? srcpill(VX + 10, VY + VH - 50);   // source width + mono, from every module
);   // end of: room view open (Plan / 3D: the room, Timeline / RT60: the analysis)
);
// Basic mode body: ROOM, SOURCE, BARRIER and OUTPUT sections under the room view, plus the no-room veil
function gfxp5()
(
// =================== BASIC: four sections under the room – ROOM | SOURCE | BARRIER | OUTPUT ===================
y0 = yS1;
(!szoom || szoom == 1) ? (
// ROOM: size, absorption of the walls, decay, tone + material
card(sxR, y0, bwR, hbR);
sechdr(1, "Room", sxR, y0, cRMTT);
tgl(sxR + bwR - 36, y0 + 5, !rbyp, cRMTT) ? ( slider36 = rbyp ? 0 : 1; sauto1(36); );
tipbox(sxR + bwR - 36, y0 + 5, 26, 14, "Room engine on / off – Off: only the source model is heard (no distance, walls, barrier or reverb)");
secopen(1) ? (
  ky = y0 + 68 + secx(1) * 0.35; kcol = cRMTT; kspB = (bwR - 20) / 3; kx0 = sxR + 10 + kspB / 2;
  kscale(kbrow());
  sprintf(#fv, "%.2f×", slider31);
  kmap(1, 0.25, 4, 1);
  nf = knob(13, kx0, ky, "Size", #fv, log2f(slider31, 0.25, 4), log2f(1, 0.25, 4));
  knobtip(kx0, ky, "Size: scales the whole room and the positions inside it – ×1 = the dimensions set in the room (drag the size labels there)");
  kchg ? setp(31, f2log(nf, 0.25, 4));
  kcol = cRVTT; sprintf(#fv, "%.2f s", rt); kdim = isfree;   // free field - no reverb
  kmap(1, 0.01, 10, max(0.001, rt / max(0.01, slider32)));
  nf = knob(14, kx0 + kspB, ky, "Decay", #fv, log2f(min(10, max(0.01, slider32)), 0.01, 10), log2f(1, 0.01, 10));
  knobtip(kx0 + kspB, ky, "Decay: reverb time RT60 in seconds – Alt+click = the physical value of this room, max 20 s");
  kchg ? setp(32, f2log(nf, 0.01, 10));   // the same range as the drawing (0.05 here made the knob jump when touched)
  tv = slider33;
  tv < -0.5 ? sprintf(#fv, "-%.0f", -tv) : ( tv > 0.5 ? sprintf(#fv, "+%.0f", tv) : strcpy(#fv, "0") );
  kmap(0, -100, 100, 1);
  nf = knob(15, kx0 + 2 * kspB, ky, "Tone", #fv, (tv + 100) / 200, 0.5);
  knobtip(kx0 + 2 * kspB, ky, "Tone of the room sound: left = darker, right = brighter – the direct sound stays unchanged");
  kchg ? setp(33, nf * 200 - 100); kdim = 0;
  // PREDELAY (Basic too, a bar like Drive): gap before the early reflections and the reverb; the direct sound is not delayed
  sprintf(#fv, "%.0f ms", slider34); kmap(0, 0, 200, 1);
  nf = hsl(16, sxR + 10, y0 + 158 + secx(1) * 0.6, bwR - 20, "Predelay", #fv, slider34 / 200, 0, cRVTT);
  pfnf = nf; pfch = kchg; gfx_setfont(4); pfx = sxR + 10 + tws("Predelay") + 8; pfon = vopt[8] >= 0.5;
  chip(pfx, y0 + 151 + secx(1) * 0.6, 34, 13, "auto", pfon, cRVTT) ? ( vopt[8] = pfon ? 0 : 1; !pfon ? ( pdint = 1; setp(34, pdroom()); pdint = 0; ); );
  tipbox(pfx, y0 + 151 + secx(1) * 0.6, 34, 13, pfon ? "auto: the predelay follows the room size (0 … 60 ms) – moving the bar by hand switches it off" : "Click: the predelay follows the room size again");
  nf = pfnf; kchg = pfch;
  tipbox(sxR + 10, y0 + 152 + secx(1) * 0.6, bwR - 20, 20, "Predelay: 0–200 ms gap between the voice and the first reflections and the reverb – the direct sound is not delayed. Larger = the room sounds further away; 0 = no gap");
  kchg ? setp(34, nf * 200);
  sprintf(#fv, "%.0f %%", slider117); kmap(0, 0, 100, 1); kdim = slider60 >= 0.5;   // DISTANCE (perspective)
  nf = hsl(34, sxR + 10, y0 + 186 + secx(1) * 0.6, bwR - 20, "Distance", #fv, slider117 / 100, 0, cRMTT); kdim = 0;
  isfree ? ( gfx_setfont(4); col(cMUTED, 1); sprintf(#w1, "Free field: no reverb · source distance %.2f m", dpd); txt(#w1, sxR + 10, y0 + 222 + secx(1) * 0.6, 0); );
  tipbox(sxR + 10, y0 + 180 + secx(1) * 0.6, bwR - 20, 20, slider60 >= 0.5 ? "Distance: no effect while Equal loudness is on" : "Distance (perspective, for film): far away = quieter – 0 % = physics (in a big hall the reverb keeps the level up), 100 % = 6 dB more per doubling beyond 2 m");
  kchg ? setp(117, nf * 100);
  kcol = cTEXT; kscale(1);
  strcpy(#w1, "All walls: "); strcat(#w1, #wmt);
  isfree ? grndseg(wmx2, wdy2 + (wdh - 16) / 2, wdw2) : ddbox(wmx2, wdy2, wdw2, wdh, #w1, !isfree, wmenu == 2);   // outdoors - the ground
  !isfree ? tipbox(wmx2, wdy2, wdw2, wdh, wtip(wmsel >= 0 ? wmattip(wmsel) : "One material for all four walls – sets their absorption α and the density of the tail (Mixed = the walls differ, set in Expert)", ta));
);
);
(!szoom || szoom == 3) ? (
// SOURCE: model, its frequency response, drive
card(sxS, y0, bwS, hbS);
sechdr(3, "Source", sxS, y0, cSRTT);
tgl(sxS + bwS - 36, y0 + 5, slider85 >= 0.5, cSRTT) ? ( slider85 = slider85 >= 0.5 ? 0 : 1; sauto1(85); );
tipbox(sxS + bwS - 36, y0 + 5, 26, 14, "Source model on / off – off: the input as it is, to compare");
secopen(3) ? (
  // the source as a tile: device icon, name, what it sounds like
  col(smenu || inbox(srcbx, srcby, srcbw, srcbh) ? cHAIR : cLINE, 1); RR(srcbx, srcby, srcbw, srcbh, 8);
  icOL = 1; srcicon(fzsel, srcbx + 19, srcby + 32, fzsel == 4 ? 0.8 : 0.95, cTEXT, cLINE); icOL = 0;
  spcur >= 0 ? strcpy(#w1, spname(spcur)) : ( strcpy(#w1, srcname(fzsel)); eqed ? strcat(#w1, " *"); );   // the preset by name
  spcur >= 0 ? ( strcpy(#w2, srcname(fzsel)); strcat(#w2, "  ·  "); strcat(#w2, srcsub(fzsel)); ) : strcpy(#w2, srcsub(fzsel));
  col(cTEXT, 1); gfx_setfont(3); txtfit(#w1, srcbx + 40, srcby + 14, srcbw - 62);
  col(cMUTED, 1); gfx_setfont(7); txtfit(#w2, srcbx + 40, srcby + 28, srcbw - 62);
  chevd(srcbx + srcbw - 12, srcby + srcbh / 2, cMUTED);
  strcpy(#ssub, srcsub(fzsel)); strcat(#ssub, "  ·  "); strcat(#ssub, srcsub2(fzsel));
  tipbox(srcbx, srcby, srcbw, srcbh, #ssub);
  sqx = sxS + 10; sqy = y0 + 76; sqw = bwS - 20; sqh = 42 + secx(3) * 0.6;
  eqview(0);
  sprintf(#fv, "%.0f", slider37); strcat(#fv, " %");
  kmap(0, 0, 100, 1);
  nf = hsl(17, sxS + 10, y0 + 130 + secx(3) * 0.6, bwS - 20, "Distortion", #fv, slider37 / 100, 0.5, cSRTT);
  tipbox(sxS + 10, y0 + 124 + secx(3) * 0.6, bwS - 20, 20, "Distortion: how hard the source is pushed – 50 % = typical, less = cleaner, more = crunchier (saturation, clipping, codec)");
  kchg ? setp(37, nf * 100);
// stereo width, mono mix, reset to Original - also in Basic
  swfmt(); wdm = slider110 >= 0.5 || devm; kdim = wdm;
  nf = hsl(19, sxS + 10, y0 + 154 + secx(3) * 0.6, bwS - 20, "Stereo width", #fv, slider98 / 200, 0.5, wdm ? cDIM : cSRTT); kdim = 0;
  tipbox(sxS + 10, y0 + 148 + secx(3) * 0.6, bwS - 20, 20, wdm ? (devm ? "Stereo width – no effect with a device: a phone, radio … is a mono loudspeaker (it counts with Original or a module mix under 100 %)" : "Stereo width – off while Mono mix is on") : "Stereo width of the input: 100 % = as it comes in (true stereo), 0 % = mono, up to 200 % = wider (Alt+click = 100 %)");
  kchg && !wdm ? setp(98, nf * 200); kchg = 0;
  monobtn(sxS + 10, y0 + 176 + secx(3) * 0.6, 1);
  swunit(sxS + bwS - 10 - 76 - 36, y0 + 178 + secx(3) * 0.6);
);
);
y0 = yS2;                                                                 // under Source: Barrier (Output: under Room, drawio)
(!szoom || szoom == 5) ? (
// BARRIER: type, element, opening, what reaches the listener
card(sxB, y0, bwB, hbB);
sechdr(5, "Barrier", sxB, y0, cBARTT);
tgl(sxB + bwB - 36, y0 + 5, btg > 0, cBARTT) ? bartog();
tipbox(sxB + bwB - 36, y0 + 5, 26, 14, btg > 0 ? "Barrier on – click: off (one open room), the type is remembered" : "Barrier off – click: on again");
secopen(5) ? (
  bartiles(sxB + 10, y0 + 28, bwB - 20, 42 + secx(5) * 0.3); bby = y0 + secx(5) * 0.55;
  kscale(kbrow()); bkx = sxB + 11 + KR; btx = bkx + KR + 13; btw = sxB + bwB - 10 - btx;
  btg > 0 ? ( bmsel >= 0 ? strcpy(#w1, bmatname(btg, bmsel)) : sprintf(#w1, "Custom · %.0f dB", slider44); ) : strcpy(#w1, "–");
  kdim = btg <= 1; kcol = cBARTT;
  btg >= 2 ? sprintf(#fv, "%.0f°", slider43 * 0.9) : strcpy(#fv, "–");
  kmap(0, 0, 90, 1);
  nf = knob(27, bkx, bby + 110, "Open", #fv, slider43 / 100, 0);
  knobtip(bkx, bby + 110, btg == 2 ? "Open: swing angle of the two window sashes – they open to the left and right like a casement (0° = closed, 90° = fully open; also: mouse wheel on the barrier in the plan)" : "Open: swing angle of the door leaf (0° = closed, 90° = fully open)");
  !lockE && kchg ? setp(43, nf * 100);
  kdim = 0; kcol = cTEXT; kscale(1);
  gfx_setfont(4); col(btg > 0 ? cMUTED : cDIM2, 1); txt("Direct sound", btx, bby + 102, 0);
  gfx_setfont(2); col(btg > 0 ? cBARTT : cDIM2, 1); txt(btg > 0 && bdirT == 0 ? "0 dB" : #bi1, sxB + bwB - 10, bby + 102, 2);
  gfx_setfont(7); col(btg > 0 ? cTEXT : cDIM2, 1); txtfit(#bi2, btx, bby + 118, btw);
  btg > 0 ? tipbox(btx, bby + 94, btw, 30, "How much quieter the direct sound arrives through or around the barrier, and its way to the listener");
  // DAMP (Basic: a bar beside the Open knob): how many dB the barrier takes off - wall, window or door (the size: Expert / the plan)
  btg > 0 ? sprintf(#fv, "%.0f dB", slider44) : strcpy(#fv, "–");
  kmap(0, 0, 70, 1);
  nf = hsl(75, btx, bby + 140, btw, "Damp", #fv, slider44 / 70, bmatrw(max(1, btg), 1) / 70, btg > 0 ? cBARTT : cDIM2);
  tipbox(btx, bby + 134, btw, 20, btg == 1 ? "Damp: how many dB the wall takes off the sound through it (at 500 Hz; bass passes more, highs less – Alt+click = the default material)" : btg >= 2 ? "Damp: how many dB the closed window or door takes off, as installed (at 500 Hz; bass passes more – Alt+click = the default material). Open lets sound past it" : "Damp of the barrier – choose Wall, Window or Door first");
  btg > 0 && kchg ? setp(44, nf * 70);
);
);
// OUTPUT: drawn by drawio()
(!szoom || szoom == 7) ? drawio();
// ROOM OFF (bypass): veil over what belongs to the room engine; the On / Off switch stays usable
rbyp ? ( col(cSURF, 0.62); (!szoom || szoom == 1) ? R(sxR + 2, yS1 + 24, bwR - 4, hbR - 26); (!szoom || szoom == 5) ? R(sxB + 2, yS2 + 2, bwB - 4, hbB - 4); );
slider85 < 0.5 && secopen(3) && (!szoom || szoom == 3) ? ( col(cSURF, 0.55); R(sxS + 2, yS1 + 24, bwS - 4, hbS - 26); );   // source model off
);
// the note value (index 0 … 12) whose length at the delay's tempo is closest to t ms
function dnear(t) local(k, b, e, d)
(
  b = 8; e = 99; k = 0;
  loop(13, d = abs(log(max(1, g_dnote(k) * 60000 / max(1, gbpm)) / max(1, t))); d < e ? ( e = d; b = k; ); k += 1; );
  b;
);
// Delay section body: mode, time unit, links, tempo field, and L/R knobs for time, feedback, level and low/high cut
function gfxp6()
(
  // switches: mode, time unit, links
  cy0 = syD + 30;
  col(cMUTED, 1); gfx_setfont(4); txt("Mode", x0 + 12, cy0 + 7, 0);
  chip(x0 + 44, cy0, 46, 14, "Stereo", !gdpp, cTEXT) ? ( gdpp ? ( slider66 = 0; sauto1(66); ); );
  chip(x0 + 93, cy0, 62, 14, "Ping-pong", gdpp, cTEXT) ? ( !gdpp ? ( slider66 = 1; sauto1(66); ); );
  tipbox(x0 + 44, cy0, 46, 14, "Stereo: L repeats on the left, R on the right, each with its own time");
  tipbox(x0 + 93, cy0, 62, 14, "Ping-pong: the echoes bounce from left to right – L time, then R time, then L time …");
  col(cMUTED, 1); gfx_setfont(4); txt("Time", x0 + 172, cy0 + 7, 0);
  chip(x0 + 200, cy0, 34, 14, "ms", !gdtm, cTEXT) ? ( gdtm ? ( slider70 = min(4000, max(1, gtL)); slider71 = min(4000, max(1, gtR)); sauto1(70); sauto1(71); slider67 = 0; sauto1(67); ); );   // the times the tempo gave
  chip(x0 + 237, cy0, 44, 14, "Tempo", gdtm, cTEXT) ? ( !gdtm ? ( slider72 = dnear(gtL); slider73 = dnear(gtR); sauto1(72); sauto1(73); slider67 = 1; sauto1(67); ); );   // the nearest note values
  tipbox(x0 + 200, cy0, 34, 14, "Time in milliseconds (1 … 4000 ms) – from Tempo: the times the note values had at that tempo");
  tipbox(x0 + 237, cy0, 44, 14, "Time as a note value of the tempo: the project tempo, or a tapped / analyzed tempo (Tempo tools)");
  // the word LINK: a click links all four (Time, Feedback, Level, EQ) or - all linked - sets them all apart (R starts where L is)
  lkall = gdlt && gdlf && gdlv && gdle; lkh = inbox(x0 + 294, cy0, 28, 14);
  col(lkh ? cTEXT : cMUTED, 1); gfx_setfont(4); txt("Link", x0 + 298, cy0 + 7, 0);
  lkh ? ( LNW(x0 + 298, cy0 + 13, x0 + 298 + tws("Link"), cy0 + 13, 1); );
  click && lkh ? (
    lkall ? ( slider68 = 0; slider71 = slider70; slider73 = slider72; slider87 = 0; slider76 = slider75; slider86 = 0; slider78 = slider77; slider69 = 0; slider80 = slider79; slider82 = slider81;
              sauto1(68); sauto1(71); sauto1(73); sauto1(87); sauto1(76); sauto1(86); sauto1(78); sauto1(69); sauto1(80); sauto1(82); )
          : ( slider68 = 1; slider87 = 1; slider86 = 1; slider69 = 1; sauto1(68); sauto1(87); sauto1(86); sauto1(69); );
    click = 0;
  );
  tipbox(x0 + 294, cy0, 28, 14, lkall ? "Link: all four linked – click = set them all apart (R starts where L is)" : "Link: click = link all four – R follows L in time, feedback, level and EQ");
  chip(x0 + 324, cy0, 40, 14, "Time", gdlt, cTEXT) ? (
    gdlt ? ( slider68 = 0; slider71 = slider70; slider73 = slider72; sauto1(68); sauto1(71); sauto1(73); ) : ( slider68 = 1; sauto1(68); );   // unlinked: R starts where L is
  );
  chip(x0 + 473, cy0, 34, 14, "EQ", gdle, cTEXT) ? (
    gdle ? ( slider69 = 0; slider80 = slider79; slider82 = slider81; sauto1(69); sauto1(80); sauto1(82); ) : ( slider69 = 1; sauto1(69); );
  );
  tipbox(x0 + 324, cy0, 40, 14, gdlt ? "Time linked: R uses the time of L – click to set them separately" : "Link the time: R uses the time of L");
  chip(x0 + 367, cy0, 58, 14, "Feedback", gdlf, cTEXT) ? (
    gdlf ? ( slider87 = 0; slider76 = slider75; sauto1(87); sauto1(76); ) : ( slider87 = 1; sauto1(87); );   // unlinked: R starts where L is
  );
  tipbox(x0 + 367, cy0, 58, 14, gdlf ? "Feedback linked: R uses the feedback of L – click to set them separately" : "Link the feedback: R uses the feedback of L");
  chip(x0 + 428, cy0, 42, 14, "Level", gdlv, cTEXT) ? (
    gdlv ? ( slider86 = 0; slider78 = slider77; sauto1(86); sauto1(78); ) : ( slider86 = 1; sauto1(86); );   // unlinked: R starts where L is
  );
  tipbox(x0 + 428, cy0, 42, 14, gdlv ? "Level linked: R uses the level of L – click to set them separately" : "Link the level: R uses the level of L");
  // TEMPO as a field: click = type a BPM - the delay takes it at once (Time -> Tempo; from ms: the note values nearest the times)
  w0 > 580 ? (
    bpx = x0 + 517; dbpw = min(w0 - 529, 112); bph = inbox(bpx - 3, cy0, dbpw + 6, 14);
    sprintf(#w1, slider74 > 0.5 ? "%.1f BPM set" : (tempo > 1 ? "%.1f BPM project" : "%.0f BPM"), gbpm);
    bph || vedit == 78 ? ( col(cLINE, 1); RR(bpx - 3, cy0, dbpw + 6, 14, 4); );
    col(gdtm ? cTEXT : cMUTED, 1); gfx_setfont(7); txtfit(#w1, bpx, cy0 + 7, dbpw);
    kmap(0, 20, 300, 1);
    (click || rclick) && bph ? ( sprintf(#bpv, "%.1f", gbpm); vedopen(78, #bpv); click = 0; rclick = 0; );
    vedit == 78 ? vedplace(bpx + dbpw / 2, cy0 + 7, 60);
    vedPend && vedId == 78 ? ( bpn = min(300, max(20, 20 + vedmap(vedVal) * 280)); vedPend = 0; tpapply(-1, floor(bpn * 100 + 0.5) / 100); );
    tipbox(bpx - 3, cy0, dbpw + 6, 14, gdtm ? "Tempo of the delay: click = type a BPM (20 … 300) – the note values stay, their times follow" : "Click = type a BPM (20 … 300): the delay switches to Tempo, with the note values nearest its times");
  );
  tipbox(x0 + 473, cy0, 34, 14, gdle ? "EQ linked: R uses low cut and high cut of L – click to set them separately" : "Link the EQ: R uses low cut and high cut of L");
  // knobs: two rows, L and R
  dew = floor(min(170, max(112, w0 * 0.25))); dex = x0 + w0 - 14 - dew;   // echo display: a fixed share at the right, the knobs get the rest
  dle = secx(10); dkv = kfit(dle / 4);                                    // taller section: rows move apart, knobs grow
  dlaneW = (dex - 10 - x0) / 2; dside = tabm && dlaneW >= 300; xD0 = x0;     // tabs, wide: L | R side by side
  dside ? ( dksp = (dlaneW - 52 - 2 * KR0) / 4; kscale(min(min(1.5, 1 + max(0, shD - 200) / 120), dksp / (2 * KR0 + 18))); dksp = (dlaneW - 48 - 2 * KR) / 4; )
        : ( dksp = min(84 * dkv, (dex - 76 - 2 * KR0 * dkv - x0) / 4); kscale(min(dkv, dksp / (2 * KR0 + 22)));
            dkx0 = x0 + 38 + KR; dksp = min(84 * KSC, (dex - 20 - KR - dkx0) / 4); );
  gk = 0;
  loop(2,
    dside ? ( x0 = xD0 + gk * dlaneW; dkx0 = x0 + 38 + KR; ky = syD + 54 + (shD - 54) / 2 - 4;
              gk == 1 ? ( col(cHAIR, 1); LN(x0 - 2, syD + 58, x0 - 2, syD + shD - 14); ); )
            : ( ky = syD + 82 + dle / 4 + gk * (76 + dle / 2); ); kb = gk * 5;
    // L / R badge = MUTE (click) / SOLO (Shift+click) of that side's echoes; muted: gray and crossed, solo: violet ring
    dbm = gk == 0 ? gdmL : gdmR; dbo = gk == 0 ? gdmR : gdmL; dlbh = inbox(x0 + 9, ky - 12, 24, 24);
    !dbm && dbo ? ( col(cAMB, 1); CI(x0 + 21, ky, 12, 0); CI(x0 + 21, ky, 11.4, 0); );
    col(dbm ? cDIM : (gk == 0 ? cSRC : cLIS), 1); RR(x0 + 12, ky - 9, 18, 18, 9);
    col(dbm ? cDIM2 : cSURF, 1); gfx_setfont(6); txt(gk == 0 ? "L" : "R", x0 + 21, ky, 1);
    dbm ? ( col(cSURF, 1); LNW(x0 + 14, ky + 7, x0 + 28, ky - 7, 1.6); );
    dlbh ? ( col(cTEXT, 0.35); CI(x0 + 21, ky, 10, 0); );
    click && dlbh ? (
      snap_take();
      (mouse_cap & 8) ? (
        // solo: only this side - again (it is already alone): both back
        !dbm && dbo ? ( slider93 = 0; slider94 = 0; ) : ( gk == 0 ? ( slider93 = 0; slider94 = 1; ) : ( slider93 = 1; slider94 = 0; ); );
      ) : (
        gk == 0 ? ( slider93 = gdmL ? 0 : 1; ) : ( slider94 = gdmR ? 0 : 1; );
      );
      snap_notify(); click = 0;
    );
    tipbox(x0 + 9, ky - 12, 24, 24, gk == 0 ? (gdmL ? "Echoes L muted – click: on again · Shift+click: L alone (solo)" : "Click: mute the left echoes · Shift+click: only the left echoes (solo) – again: both")
                                           : (gdmR ? "Echoes R muted – click: on again · Shift+click: R alone (solo)" : "Click: mute the right echoes · Shift+click: only the right echoes (solo) – again: both"));
    kcol = cDLTT; kx = dkx0;
    // TIME (R linked: shows the time of L, grayed)
    kdim = gk == 1 && gdlt;
    gdtm ? (
      dnv = gk == 0 || gdlt ? floor(slider72 + 0.5) : floor(slider73 + 0.5);
      sprintf(#fv, "%.0f", gk == 0 ? gtL : gtR);
      // the knob steps through note values; a TYPED value is milliseconds (double-click / right-click): it switches to ms
      dtyp = vedPend && vedId == 40 + kb;
      kmap(1, 1, 4000, 1);
      nf = knob(40 + kb, kx, ky, dnname(dnv), #fv, (12 - dnv) / 12, 4 / 12);
      knobtip(kx, ky, gk == 0 ? "Time L as a note value of the tempo (the name; the number in the ring = the time in ms) – turn right = longer (1/32 … 1/1), double-click = type a time in ms (switches to ms), Alt+click = 1/8" : "Time R as a note value of the tempo – double-click = type a time in ms (switches to ms), Alt+click = 1/8");
      !kdim && kchg ? (
        dtyp ? ( dms = f2log(nf, 1, 4000);                        // typed ms: Time -> ms, this side the typed time, the other side keeps its time
                 setp(70, gk == 0 ? dms : min(4000, max(1, gtL))); setp(71, gk == 1 ? dms : min(4000, max(1, gtR))); setp(67, 0); )
             : ( nn = 12 - floor(nf * 12 + 0.5); nn != dnv ? setp(gk == 0 ? 72 : 73, nn); );
      );
    ) : (
      dtv = gk == 0 || gdlt ? slider70 : slider71;
      sprintf(#fv, dtv < 100 ? "%.1f" : "%.0f", dtv);
      kmap(1, 1, 4000, 1);
      nf = knob(40 + kb, kx, ky, "Time ms", #fv, log2f(dtv, 1, 4000), log2f(50, 1, 4000));
      knobtip(kx, ky, gk == 0 ? "Time L: 1 … 4000 ms (double-click = type a value, Alt+click = 50 ms) – short times thicken the sound, long ones are clear echoes" : "Time R: 1 … 4000 ms (double-click = type a value, Alt+click = 50 ms)");
      !kdim && kchg ? setp(gk == 0 ? 70 : 71, f2log(nf, 1, 4000));
    );
    kdim = 0; kx += dksp;
    // FEEDBACK
    kdim = gk == 1 && gdlf;                                    // R linked: grayed, shows the feedback of L
    dfv = gk == 0 || gdlf ? slider75 : slider76; sprintf(#fv, "%.0f%%", dfv);
    kmap(0, 0, 95, 1);
    nf = knob(41 + kb, kx, ky, "Feedback", #fv, dfv / 95, 30 / 95);
    knobtip(kx, ky, "Feedback: how much of each echo comes back as the next one – more = more repeats (max 95 %, double-click = type a value, Alt+click = 30 %)");
    !kdim && kchg ? setp(gk == 0 ? 75 : 76, nf * 95);
    kdim = 0;
    kx += dksp;
    // LEVEL
    kdim = gk == 1 && gdlv;                                    // R linked: grayed, shows the level of L
    dlv = gk == 0 || gdlv ? slider77 : slider78; dlv <= -47.9 ? strcpy(#fv, "off") : sprintf(#fv, "%+.1f", dlv);
    kmap(0, -48, 6, 1);
    nf = knob(42 + kb, kx, ky, "Level dB", #fv, lin2f(dlv, -48, 6), lin2f(-6, -48, 6));
    knobtip(kx, ky, "Level of the echoes – they go into the room like the source itself (double-click = type a value, Alt+click = −6 dB, far left = off)");
    !kdim && kchg ? setp(gk == 0 ? 77 : 78, f2lin(nf, -48, 6));
    kdim = 0;
    kx += dksp;
    // LOW CUT / HIGH CUT (R linked: grayed, shows the values of L)
    kdim = gk == 1 && gdle;
    dcv = gk == 0 || gdle ? slider79 : slider80; sprintf(#fv, "%.0f", dcv);
    kmap(1, 20, 2000, 1);
    nf = knob(43 + kb, kx, ky, "Low cut Hz", #fv, log2f(dcv, 20, 2000), log2f(200, 20, 2000));
    knobtip(kx, ky, "Low cut in the feedback loop (12 dB/oct): every repeat gets thinner (double-click = type a value, Alt+click = 200 Hz)");
    !kdim && kchg ? setp(gk == 0 ? 79 : 80, f2log(nf, 20, 2000));
    kx += dksp;
    dhv = gk == 0 || gdle ? slider81 : slider82; sprintf(#fv, "%.1fk", dhv / 1000);
    kmap(1, 1000, 20000, 1);
    nf = knob(44 + kb, kx, ky, "High cut Hz", #fv, log2f(dhv, 1000, 20000), log2f(6000, 1000, 20000));
    knobtip(kx, ky, "High cut in the feedback loop (12 dB/oct): every repeat gets darker, like tape or analog delays (double-click = type a value, Alt+click = 6 kHz)");
    !kdim && kchg ? setp(gk == 0 ? 81 : 82, f2log(nf, 1000, 20000));
    kdim = 0;
    gk += 1;
  );
  kcol = cTEXT;
  gdle ? ( dlkx = dkx0 + 3.5 * dksp; col(cDLTT, 1); RR(dlkx - 26, ky - KR - 18, 52, 14, 7); col(cSURF, 1); gfx_setfont(6); txt("EQ L = R", dlkx, ky - KR - 11, 1); );
  kscale(1); x0 = xD0;
  // ECHO DISPLAY: L above, R below the axis; each echo with its room tail behind it (violet)
// TYPE + DEVICE row over the echo display
  dtbx = dex; dtby = syD + 52; dtbw = dew - 58;
  dtyn = floor(slider115 + 0.5); strcpy(#w1, "Type: "); strcat(#w1, dtyn == 1 ? "Tape" : (dtyn == 2 ? "Analog" : (dtyn == 3 ? "Lo-Fi" : "Clean")));
  ddbox(dtbx, dtby, dtbw, 15, #w1, 1, emenu == 4);
  click && inbox(dtbx, dtby, dtbw, 15) ? ( emenu = 4; menuused = 1; click = 0; );
  tipbox(dtbx, dtby, dtbw, 15, "Delay type: what every repeat goes through – Clean, Tape (darker, round), Analog / BBD (dark, soft), Lo-Fi (11 kHz, 10 bit) – it adds up from echo to echo");
  ddvok = fzsel > 0 || eqed;
  chip(dex + dew - 52, dtby, 52, 15, "Device", slider116 >= 0.5 && ddvok, ddvok ? cDLTT : cDIM) ? ( slider116 = slider116 >= 0.5 ? 0 : 1; sauto1(116); );
  tipbox(dex + dew - 52, dtby, 52, 15, ddvok ? "Device: every repeat goes through the source model again (the phone, radio …) – each echo smaller and narrower; normalized, never louder" : "Device: choose a source model first – then every repeat goes through it again");
  dey = syD + 72; deh = shD - (tabm ? 162 : 150); demid = dey + (deh - 12) / 2;
  col(darkmode ? cSTAGE : cBG, 1); RR(dex, dey, dew, deh, 5);   // dark mode - a darker ground, brighter echoes (more contrast)
  col(darkmode ? cDIM : cHAIR, 1); LN(dex + 8, demid, dex + dew - 8, demid);
  col(cMUTED, 1); gfx_setfont(7); txt("L", dex + 6, dey + 9, 0); txt("R", dex + 6, dey + deh - 22, 0);
  dper = gdpp ? gtL + gtR : max(gtL, gtR); dneed = gdpp ? 2.2 * dper : 4.4 * dper;
  // time axis length dtmx (ms): the next round value (50, 100, 200, 250, 500, 1000 ...) that holds dneed = roughly the first four echoes per side
  dtmx = 50; while ( dtmx < dneed && dtmx < 16000 ? ( dtmx = dtmx == 50 ? 100 : (dtmx == 100 ? 200 : (dtmx == 200 ? 250 : (dtmx == 250 ? 500 : 2 * dtmx))); 1; ) : 0; );
  dlw = dew - 24; dpx0 = dex + 16;
  gi = 0; loop(5, gx = dpx0 + gi / 4 * dlw; col(cHAIR, 1); LN(gx, dey + 4, gx, dey + deh - 14); gi += 1; );
  dtmx >= 2000 ? ( sprintf(#w1, "%.0f s", dtmx / 2000); sprintf(#w2, "%.0f s", dtmx / 1000); ) : ( sprintf(#w1, "%.0f ms", dtmx / 2); sprintf(#w2, "%.0f", dtmx); );
  col(cDIM2, 1); gfx_setfont(7); txt("0", dpx0, dey + deh - 7, 1); txt(#w1, dpx0 + 0.5 * dlw, dey + deh - 7, 1); txt(#w2, dpx0 + dlw, dey + deh - 7, 1);
  col(cTEXT, 0.9); LNW(dpx0, demid - (deh / 2 - 16), dpx0, demid + (deh / 2 - 16), 1.4);
  drtl = rbyp ? 0 : min(1, rt * 1000 / dtmx) * dlw;          // room tail on the time axis (RT60)
  dfL = slider75 / 100; dfR = gdlf ? dfL : slider76 / 100;
  dlgL = slider77 <= -47.9 ? 0 : 10 ^ (slider77 / 20); dlgR = gdlv ? dlgL : (slider78 <= -47.9 ? 0 : 10 ^ (slider78 / 20));
  gdmL ? ( dlgL = 0; ); gdmR ? ( dlgR = 0; );
  // echo sequence: stereo = L n·tL, R n·tR; ping-pong = L tL, R tL+tR, L 2tL+tR …
  // tone per pass through the loop filters (12 dB/oct): highs at 4 kHz and bass at 120 Hz that are left -> paler echoes
  dqL = sqrt(1 / sqrt(1 + (4000 / slider81) ^ 4) / sqrt(1 + (slider79 / 120) ^ 4));
  dqR = gdle ? dqL : sqrt(1 / sqrt(1 + (4000 / slider82) ^ 4) / sqrt(1 + (slider80 / 120) ^ 4));
  gk = 0; dtt = 0; dvL = 1; dnL = 0; dnR = 0; dbP = 1;
  loop(80,
    gdpp ? ( dech = gk % 2; dtt += dech == 0 ? gtL : gtR; damp = dech == 0 ? dlgL * dvL : dlgR * dvL; dech == 1 ? ( dvL *= sqrt(dfL * dfR); ); dbP *= dech == 0 ? dqL : dqR; dbr = dbP; )
         : ( dech = gk % 2; dech == 0 ? ( dnL += 1; dtt = dnL * gtL; damp = dlgL * dfL ^ (dnL - 1); dbr = dqL ^ dnL; ) : ( dnR += 1; dtt = dnR * gtR; damp = dlgR * dfR ^ (dnR - 1); dbr = dqR ^ dnR; ); );
    dtt <= dtmx && damp > 0.004 ? (
      // bar height = echo level mapped from -48 … +6 dB to 0 … 1 of the half height
      dtx = dpx0 + dtt / dtmx * dlw; dsg = dech == 0 ? -1 : 1; dhh = (deh / 2 - 18) * min(1, max(0, (20 * log10(damp) + 48) / 54));
      drtl > 2 ? (
        // room tail behind the echo: falls 60 dB (exp(-6.9)) over drtl = RT60 in pixels, drawn at 60 % of the echo height
        col(darkmode ? 0xD2C8FF : cAMB, darkmode ? 0.6 : 0.30); dstp = max(2.4, drtl / 30); k = 1;
        loop(30, dxx = k * dstp; dxx < drtl && dtx + dxx < dpx0 + dlw ? LN(dtx + dxx, demid, dtx + dxx, demid + dsg * dhh * 0.6 * exp(-6.9 * dxx / drtl)); k += 1; );
      );
      col(dech == 0 ? cSRC : cLIS, darkmode ? 0.5 + 0.5 * dbr : 0.22 + 0.78 * dbr); LNW(dtx, demid, dtx, demid + dsg * dhh, 1.3); CIF(dtx, demid + dsg * dhh, 1.15, 1);   // finer - a thin stem and a small dot
    );
    gk += 1;
  );
  deh >= 70 ? (   // a small graph (All, Fit) has no legend - it covered the echoes (the tooltip names the colors)
    gfx_setfont(7); dlgw = max(tws("echoes"), rbyp ? 0 : tws("+ their room")); col(darkmode ? cSTAGE : cBG, 0.88); RR(dex + dew - dlgw - 13, dey + 3, dlgw + 9, rbyp ? 12 : 23, 4);   // a backdrop - the legend no longer lies on the echo bars (small module)
  col(cMUTED, 1); gfx_setfont(7); txt("echoes", dex + dew - 8, dey + 9, 2); !rbyp ? ( col(darkmode ? 0xD2C8FF : cAMB, 1); txt("+ their room", dex + dew - 8, dey + 20, 2); );
  );
  tipbox(dex, dey, dew, deh, "Echoes: L above, R below the line, height = level (dB), paler = darker and thinner after the filters; violet = the room tail every echo gets");
  // TAPE: wow + flutter and drive (both sides)
  sprintf(#fv, "%.0f %%", slider83);
  kmap(0, 0, 100, 1);
  nf = hsl(50, dex + 2, syD + shD - (tabm ? 12 : 0) - 44, dew - 4, "Wow + flutter", #fv, slider83 / 100, 0, cDLTT);
  tipbox(dex, syD + shD - (tabm ? 12 : 0) - 50, dew, 20, "Tape: wow + flutter – the echoes waver in pitch like on an old tape echo (0 % = clean, double-click = type a value, Alt+click = 0)");
  kchg ? setp(83, nf * 100);
  sprintf(#fv, "%.0f %%", slider84);
  kmap(0, 0, 100, 1);
  nf = hsl(51, dex + 2, syD + shD - (tabm ? 12 : 0) - 22, dew - 4, "Saturation", #fv, slider84 / 100, 0, cDLTT);
  tipbox(dex, syD + shD - (tabm ? 12 : 0) - 28, dew, 20, "Saturation in the feedback loop: the tape saturates – loud repeats get rounder and denser, quiet ones stay clean (0 % = clean)");
  kchg ? setp(84, nf * 100);
  sprintf(#fv, slider99 < 0.05 ? "off" : "%.1f dB", slider99);
  kmap(0, 0, 24, 1);
  nf = hsl(52, dex + 2, syD + shD - (tabm ? 12 : 0) - 66, dew - 4, "Ducking", #fv, slider99 / 24, 0, cDLTT);
  tipbox(dex, syD + shD - (tabm ? 12 : 0) - 72, dew, 20, "Ducking: the echoes get quieter while the input plays and come back in the pauses – keeps dialogue clear (0 = off, up to 24 dB, automatable)");
  kchg ? setp(99, nf * 24);
  // delay off: veil (the switches stay usable)
  !gdon ? ( col(cSURF, 0.55); R(x0 + 2, syD + 48, w0 - 4, shD - 50); );
);
// Delay section card: header, on/off, mix, Tempo tools, 3D chip and preset menu; draws the body via gfxp6 when open
function gfxp7()
(
x0 = L0; w0 = LW;
card(x0, syD, w0, shD); sechdr(10, "Delay", x0, syD, cDLTT);
tgl(x0 + w0 - 36, syD + 5, gdon, cDLTT) ? ( slider65 = gdon ? 0 : 1; sauto1(65); );
tipbox(x0 + w0 - 36, syD + 5, 26, 14, "Delay on / off – it works before the room, so every echo is reverberated too. Off: the room gets exactly the source");
tpbx = x0 + w0 - (tabm ? 218 : 198);
hmix(60, x0 + w0 - (tabm ? 132 : 112), syD + 12, 90, cDLTT, "Amount of the delay (module mix): the share of the echoes – 100 % = as set, 0 % = no echoes (double-click = type a value, Alt+click = 100 %, automatable)");
chip(tpbx, syD + 5, 76, 14, "Tempo tools", tpon && tpctx != 14, cDLTT) ? ( tpon = !tpon; tpctx = 10; tpmv = 0; );   // opens at its place again
tipbox(tpbx, syD + 5, 76, 14, "Tempo tools: tap the tempo or let Roomservice listen to the song – then send a note value to the delay");
d3on = vopt[20] < 0.5;
chip(tpbx - 32, syD + 5, 28, 14, "3D", d3on, cTEXT) ? ( vopt[20] = d3on ? 1 : 0; );
tipbox(tpbx - 32, syD + 5, 28, 14, d3on ? "Delay times at the source in the 3D view: shown (while the delay is on) – click to hide" : "Delay times at the source in the 3D view: hidden – click to show");
// PRESET menu (like the room presets): film & post, music, sound design
gfx_setfont(2); dpbx = x0 + 28 + tws("Delay") + (tabm ? 54 : 58);   /* 0.311: + the reset icon */ dpbw = max(90, min(secopen(10) ? 190 : 140, tpbx - 40 - dpbx)); dpby = syD + 4; dpbh = 16;
dpsel = dpmatch();
strcpy(#w1, dpsel >= 0 ? dpname(dpsel) : "Custom");
ddbox(dpbx, dpby, dpbw, dpbh, #w1, 1, dmenu);
!secopen(10) && tpbx - 38 - (dpbx + dpbw + 10) > 30 ? (                // folded: what the delay does, in one line
  gdtm ? ( strcpy(#w2, "L "); strcat(#w2, dnname(slider72)); strcat(#w2, " · R "); strcat(#w2, dnname(gdlt ? slider72 : slider73)); sprintf(#w3, " · %.0f BPM", gbpm); strcat(#w2, #w3); )
       : sprintf(#w2, "L %.0f · R %.0f ms", gtL, gtR);
  gdpp ? strcat(#w2, " · ping-pong");
  col(gdon ? cMUTED : cDIM2, 1); gfx_setfont(4); txtfit(#w2, dpbx + dpbw + 10, syD + 12, tpbx - 38 - (dpbx + dpbw + 10));
);
click && inbox(dpbx, dpby, dpbw, dpbh) ? ( dmenu = !dmenu; click = 0; );
tipbox(dpbx, dpby, dpbw, dpbh, dpsel >= 0 ? dpdesc(dpsel) : "Delay presets for film / post, music and sound design – loading one switches the delay on");
secopen(10) ? (gfxp6());
);
// FREE FIELD (its own tab after Room, Tabs view): outdoors - no walls, no reverb. On = the room is off (one switch for both).
// The card takes the place of the Room card: heights and angle of the source, Distance, and the ground reflection
// GROUND PICTURES: the three grounds seen from above as material swatches. Click = choose
// Draws one clickable ground swatch picture (asphalt, grass or snow) with name and tooltip; click selects the ground
function gndpanel(x, y, w, h, m, nm, ds)
local(i, r, r2, bx, by, hv, gsl, n, ix, iy, iw, ih, a, ln)
(
  hv = inbox(x, y, w, h); gsl = floor(slider119 + 0.5) == m + 1;
  col(gsl ? cFFTT : (hv ? cBORD : cHAIR), 1); RR(x, y, w, h, 6);
  ix = x + 3; iy = y + 3; iw = w - 6; ih = h - 6;
// flat, abstract swatches (cartoon style): one calm colour and two or three simple shapes per ground
  col(m == 0 ? (darkmode ? 0x5A5D64 : 0x777B83) : (m == 1 ? (darkmode ? 0x4E7D66 : 0xA3CBB3) : (darkmode ? 0xD2DCE5 : 0xEAF0F5)), 1); RR(ix, iy, iw, ih, 4);   // muted tones that fit the gray design
  m == 0 ? (
    col(darkmode ? 0x45484E : 0x666A72, 1); RR(ix, iy + ih * 0.62, iw, ih * 0.38, 3);   // the kerb side
    col(0xEEF0F3, 1); by = iy + ih * 0.36; bx = ix + iw * 0.08; i = 0;
    loop(4, RR(bx, by, iw * 0.14, max(2, ih * 0.08), 1); bx += iw * 0.23; i += 1; );   // road marking
  );
  m == 1 ? (
    col(darkmode ? 0x43705B : 0x8FBBA0, 1); RR(ix, iy + ih * 0.58, iw, ih * 0.42, 4);   // a darker lawn strip
    col(darkmode ? 0x78B096 : 0x5C987C, 1); i = 0;
    loop(4, bx = ix + iw * (0.14 + i * 0.22); by = iy + ih * (0.3 + (i % 2) * 0.28); LNW(bx - 3, by, bx, by - 5, 1.6); LNW(bx, by - 5, bx + 3, by, 1.6); i += 1; );   // little tufts
  );
  m == 2 ? (
    col(darkmode ? 0xB9C8D6 : 0xD5E0EA, 1); CIF(ix + iw * 0.24, iy + ih * 0.68, ih * 0.26, 1); CIF(ix + iw * 0.5, iy + ih * 0.7, ih * 0.22, 1); CIF(ix + iw * 0.76, iy + ih * 0.66, ih * 0.27, 1); RR(ix + iw * 0.24, iy + ih * 0.68, iw * 0.52, ih * 0.26, 2);   // soft drifts
    col(0xFFFFFF, 1); CIF(ix + iw * 0.3, iy + ih * 0.28, 1.6, 1); CIF(ix + iw * 0.55, iy + ih * 0.18, 1.2, 1); CIF(ix + iw * 0.8, iy + ih * 0.34, 1.6, 1);
  );
  gfx_setfont(4); col(gsl ? cTEXT : cMUTED, 1); txt(nm, x + w / 2, y + h + 8, 1);
  tipbox(x, y, w, h + 14, ds);
  click && hv ? ( setp(119, m + 1); click = 0; );
);
// Lays out the three ground swatches (asphalt, grass, snow) side by side
function gndpics(x, y, w, h)
local(pw, gp)
(
  gp = 10; pw = min(120, floor((w - 2 * gp) / 3));
  gndpanel(x, y, pw, h, 0, "Asphalt", "Hard ground (asphalt, concrete): reflects almost everything – a bright, clear second arrival; with a moving source the typical sweeping comb filter");
  gndpanel(x + pw + gp, y, pw, h, 1, "Grass", "Grass: absorbs much and the highs most – a weaker, duller reflection, the comb is shallow");
  gndpanel(x + 2 * (pw + gp), y, pw, h, 2, "Snow", "Fresh snow: very soft – only a muffled trace of the ground reflection is left");
);
// Free field tab: outdoors card with source heights/angle, distance, ground reflection and ground pictures
function gfxpff()
(
x0 = L0; w0 = LW; ksp = (w0 - 40) / 9; kx0 = x0 + 20 + ksp / 2;
card(x0, syR, w0, shR); sechdr(15, "Free field", x0, syR, cFFTT);
tgl(x0 + w0 - 36, syR + 5, isfree, cFFTT) ? ( modeset(isfree ? 0 : 1); snlast = 0; );
tipbox(x0 + w0 - 36, syR + 5, 26, 14, "Free field on / off – outdoors, no walls, no reverb. On: the room is off; off: the room is back");
gfx_setfont(2); ffpx = x0 + 28 + tws("Free field") + 40; ffpy = syR + 4; ffpw = min(190, max(110, w0 - (ffpx - x0) - 70));
ffpx + ffpw < x0 + w0 - 50 ? (
  ddbox(ffpx, ffpy, ffpw, 16, ffpLast >= 0 ? ffpname(ffpLast) : "Presets", 1, ffpmenu);
  click && inbox(ffpx, ffpy, ffpw, 16) ? ( ffpmenu = 1; click = 0; );
  tipbox(ffpx, ffpy, ffpw, 16, "Free field presets: a street, an alley, a soccer field, a canyon … – area, ground and the two reflectors are set for the place (the listener in the middle, the source 1.5 m in front)");
);
secopen(1) ? (
  kscale(min(ksp / (2 * KR0 + 22), kfit(secx(1) / 4))); ky = syR + 50 + secx(1) / 4; kx = kx0; kdim = 0;
  hmax = max(0.1, roomH - 0.1);
  kcol = cSRC; sprintf(#fv, "%.2f m", sz);
  kmap(0, 0, hmax, 1);
  nf = knob(20, kx, ky, "Src height", #fv, min(1, sz / hmax), min(1, 1.5 / hmax));
  knobtip(kx, ky, "Source height above the ground in meters – double-click = type a value, Alt+click = 1.5 m");
  kchg && (!rbyp || isfree) ? moveobjz(1, nf * hmax);
  kx += ksp; kcol = cLIS; sprintf(#fv, "%.2f m", lz);
  kmap(0, 0, hmax, 1);
  nf = knob(21, kx, ky, "Lis height", #fv, min(1, lz / hmax), min(1, 1.5 / hmax));
  knobtip(kx, ky, "Listener height above the ground in meters – double-click = type a value, Alt+click = 1.5 m");
  kchg && (!rbyp || isfree) ? moveobjz(2, nf * hmax);
  kx += ksp; kcol = cSRC; sprintf(#fv, abs(slider61 - floor(slider61 + 0.5)) > 0.05 ? "%.1f°" : "%.0f°", slider61);
  nf = dirknob(30, kx, ky, "Src angle", #fv, slider61, slider63 >= 0.5 ? 0 : (abs(lx - sx) + abs(ly - sy) > 0.001 ? atan2(-(lx - sx), ly - sy) * 180 / $pi : 0));
  knobtip(kx, ky, "Source angle: where the source points – 0° (mark) = to the front, +90° = left, −90° = right, ±180° = to the back. Drag around the dial (snaps to 15°), Shift = fine, wheel = 1°, double-click = type");
  kchg && (!rbyp || isfree) ? setp(61, nf);
  kx += ksp; kcol = cLIS; sprintf(#fv, abs(slider17 - floor(slider17 + 0.5)) > 0.05 ? "%.1f°" : "%.0f°", slider17); kdim = slider16 < 0.5;   // the listener's own angle (Facing); with Facing: Source it is not used - turning it switches to Fixed
  nf = dirknob(35, kx, ky, "Lis angle", #fv, slider17, 180);
  knobtip(kx, ky, "Listener angle (Facing Fixed): where the listener looks – 0° (mark) = straight ahead, to the back wall, +90° = right, −90° = left, ±180° = to the front. Drag around the dial (snaps to 15°), Shift = fine, wheel = 1°, double-click = type, Alt+click = 0°. Facing Source / Path: turning it switches to Fixed");
  kchg ? ( slider16 < 0.5 ? ( slider16 = 1; autom(32768); ); setp(17, nf); ); kdim = 0;
  kx += ksp; kcol = cFFTT; sprintf(#fv, "%.1f m", slider4 * rs);   // real meters (the preset Size factor is not shown here)
  kmap(1, 0.8, 15, rs);
  nf = knob(3, kx, ky, "Max height", #fv, log2f(slider4, 0.8, 15), log2f(3, 0.8, 15));
  knobtip(kx, ky, "Highest point source and listener can reach outdoors (the room height – the same control as Height in Room)");
  kchg ? setp(4, f2log(nf, 0.8, 15));
  kx += ksp; kcol = cFFTT; sprintf(#fv, "%.0f %%", slider117); kdim = slider60 >= 0.5;
  kmap(0, 0, 100, 1);
  nf = knob(34, kx, ky, "Distance", #fv, slider117 / 100, 0); kdim = 0;
  knobtip(kx, ky, slider60 >= 0.5 ? "Distance: no effect while Equal loudness is on (Output)" : "Distance (perspective, for film): beyond 2 m the sound gets quieter with the distance – 0 % = physics, 100 % = 6 dB more per doubling, like a film mix");
  kchg ? setp(117, nf * 100);
// the area source and listener can walk in outdoors (the same width / depth as in the Room tab)
  kx += ksp; kcol = cFFTT; sprintf(#fv, "%.1f m", slider2 * rs);
  kmap(1, 0.8, 40, rs);
  nf = knob(1, kx, ky, "Area width", #fv, log2f(slider2, 0.8, 40), log2f(10, 0.8, 40));
  knobtip(kx, ky, "Width of the area source and listener can move in – up to 40 m (the same control as Width in Room)");
  kchg ? setp(2, f2log(nf, 0.8, 40));
  kx += ksp; sprintf(#fv, "%.1f m", slider3 * rs);
  kmap(1, 0.8, 40, rs);
  nf = knob(2, kx, ky, "Area depth", #fv, log2f(slider3, 0.8, 40), log2f(7, 0.8, 40));
  knobtip(kx, ky, "Depth of the area source and listener can move in – up to 40 m (the same control as Depth in Room)");
  kchg ? setp(3, f2log(nf, 0.8, 40));
  kcol = cTEXT;
  col(cHAIR, 1); LN(x0 + 12, syV + 2, x0 + w0 - 12, syV + 2);
  col(cFFTT, 1); RR(x0 + 19, syV + 9, 3, 12, 1.5);
  grndseg(x0 + 28, syV + 7, min(260, w0 - 60));
  gfx_setfont(4); olx = x0 + 28 + min(260, w0 - 60) + 14;
  olx + 84 < x0 + w0 - 14 - tws("Free field: no reverb · distance 00.00 m") - 8 ? (
    chip(olx, syV + 7, 84, 16, "Outline view", vopt[67] >= 0.5, cFFTT) ? vopt[67] = 1 - vopt[67];
    tipbox(olx, syV + 7, 84, 16, "Outline view (display only, the sound does not change): the colored sky, hills, ground and panel fills are hidden – only lines and the plain floor remain");
  );
// the pictures of the grounds under the Ground row (when the card is tall enough)
  gnpy = syV + 30; gnph = min(40, syR + shR - 24 - gnpy);   // small swatches (own variables: gph / gpw belong to the pixel-rect helpers and are overwritten by every drawing call)
// GROUND LEVEL: a slider right of the pictures (-12 .. +6 dB, 0 = as before) scales the strength of the ground reflection
  gnlw = w0 > 620 ? 170 : 0;
  gnph >= 30 ? gndpics(x0 + 19, gnpy, w0 - 38 - (gnlw ? gnlw + 16 : 0), gnph);
  gnlw && gnph >= 30 ? (
    sprintf(#fv, "%+.1f dB", vopt[69]); kmap(0, -12, 6, 1);
    nf = hsl(177, x0 + w0 - 19 - gnlw, gnpy + gnph / 2 - 7, gnlw, "Ground level", #fv, (vopt[69] + 12) / 18, 12 / 18, cFFTT);
    kchg ? vopt[69] = floor((nf * 18 - 12) * 2 + 0.5) / 2;
    tipbox(x0 + w0 - 19 - gnlw - 4, gnpy + gnph / 2 - 10, gnlw + 8, 24, "Strength of the ground reflection: 0 dB = as modeled, lower = softer ground, higher = a more obvious comb effect (double-click = 0 dB)");
  );
  gfx_setfont(4); col(cMUTED, 1); sprintf(#w1, isfree ? "Free field: no reverb · distance %.2f m" : "Free field is off – the room is on", dpd); txt(#w1, x0 + w0 - 14, syV + 15, 2);
);
);
// Expert left/right columns: Delay, Room (size, reverb, hear, morph), Source, Walls and Motion sections
function gfxp8()
(
// =================== EXPERT: two columns as in 0.172 – every section folds ===================
// ---- DELAY (left column, top): stereo / ping-pong delay before the room - every echo is reverberated too ----
(!szoom || szoom == 10) ? (gfxp7());
// ---- ROOM (left, under the room view): dimensions, size, absorption, heights ----
x0 = L0; w0 = LW; ksp = (w0 - 40) / 10; kx0 = x0 + 20 + ksp / 2;   // nine places (+ Distance); ten (+ Lis angle)
ffTab ? gfxpff();
(!szoom || szoom == 1) && !ffTab ? (
card(x0, syR, w0, shR); sechdr(1, "Room", x0, syR, cRMTT);
tgl(x0 + w0 - 36, syR + 5, !rbyp, cRMTT) ? ( slider36 = rbyp ? 0 : 1; sauto1(36); );
w0 >= 400 && !tabm ? ( chip(x0 + w0 - (tabm ? 132 : 112) - 74, syR + 5, 66, 14, "Free field", isfree, cRMTT) ? ( modeset(isfree ? 0 : 1); snlast = 0; click = 0; ); tipbox(x0 + w0 - (tabm ? 132 : 112) - 74, syR + 5, 66, 14, isfree ? "Free field is on – click: back to the room" : "Free field: outdoors, no walls, no reverb (quick switch, same as the Free field tab)"); );
hmix(61, x0 + w0 - (tabm ? 132 : 112), syR + 12, 91, cRMTT, "Amount of room + reverb (module mix, one for both): 100 % = the room as set, 0 % = as if the room were off – only the source (and the delay), in between = both (double-click = type a value, Alt+click = 100 %, automatable)");
tipbox(x0 + w0 - 36, syR + 5, 26, 14, "Room engine on / off – Off: only the source model is heard (no distance, walls, barrier or reverb)");
secopen(1) ? (
  kscale(min(ksp / (2 * KR0 + 22), kfit(secx(1) / 4))); ky = syR + 50 + secx(1) / 4; kcol = cRMTT;
  kx = kx0; sprintf(#fv, "%.1f m", slider2);
  kmap(1, 0.8, 40, 1);
  nf = knob(1, kx, ky, "Width", #fv, log2f(slider2, 0.8, 40), log2f(10, 0.8, 40));
  knobtip(kx, ky, "Room width – a bigger room gives longer delays and more reverb (also: drag the width label in the room)");
  kchg ? setp(2, f2log(nf, 0.8, 40));
  kx += ksp; sprintf(#fv, "%.1f m", slider3);
  kmap(1, 0.8, 40, 1);
  nf = knob(2, kx, ky, "Depth", #fv, log2f(slider3, 0.8, 40), log2f(7, 0.8, 40));
  knobtip(kx, ky, "Room depth – changes volume, reflection times and reverb time");
  kchg ? setp(3, f2log(nf, 0.8, 40));
  kx += ksp; sprintf(#fv, "%.1f m", slider4); kdim = isfree;   // free field - no ceiling, no walls
  kmap(1, 0.8, 15, 1);
  nf = knob(3, kx, ky, "Height", #fv, log2f(slider4, 0.8, 15), log2f(3, 0.8, 15));
  knobtip(kx, ky, "Ceiling height – changes the room volume and the floor/ceiling reflections");
  kchg ? setp(4, f2log(nf, 0.8, 15));
  kx += ksp; sprintf(#fv, "%.2f×", slider31);
  kmap(1, 0.25, 4, 1);
  nf = knob(13, kx, ky, "Size", #fv, log2f(slider31, 0.25, 4), log2f(1, 0.25, 4));
  knobtip(kx, ky, "Size: scales the room and the positions in it (not the heights of source and listener) – ×1 = the dimensions set here");
  kchg ? setp(31, f2log(nf, 0.25, 4));
  kx += ksp; sprintf(#fv, "%.2f", ta);
  kmap(0, 0.01, 1, 1);
  nf = knob(4, kx, ky, tname, #fv, lin2f(ta, 0.01, 1), lin2f(selw == 5 ? 0.17 : 0.45, 0.01, 1));
  knobtip(kx, ky, "Absorption α of the surface chosen in Walls: 0 = reflects all sound, 1 = absorbs all sound");
  kchg ? setalpha(f2lin(nf, 0.01, 1)); kdim = 0;
  hmax = max(0.1, roomH - 0.1);                     // heights in meters above the floor (respect Link S+L)
  kx += ksp; kcol = cSRC; sprintf(#fv, "%.2f m", sz);
  kmap(0, 0, hmax, 1);
  nf = knob(20, kx, ky, "Src height", #fv, min(1, sz / hmax), min(1, 1.5 / hmax));
  knobtip(kx, ky, "Source height above the floor in meters – double-click = type a value, Alt+click = 1.5 m (in every room)");
  kchg && (!rbyp || isfree) ? moveobjz(1, nf * hmax);
  kx += ksp; kcol = cLIS; sprintf(#fv, "%.2f m", lz);
  kmap(0, 0, hmax, 1);
  nf = knob(21, kx, ky, "Lis height", #fv, min(1, lz / hmax), min(1, 1.5 / hmax));
  knobtip(kx, ky, "Listener height above the floor in meters – double-click = type a value, Alt+click = 1.5 m (in every room)");
  kchg && (!rbyp || isfree) ? moveobjz(2, nf * hmax);
  kx += ksp; kcol = cSRC; sprintf(#fv, abs(slider61 - floor(slider61 + 0.5)) > 0.05 ? "%.1f°" : "%.0f°", slider61);
  nf = dirknob(30, kx, ky, "Src angle", #fv, slider61, slider63 >= 0.5 ? 0 : (abs(lx - sx) + abs(ly - sy) > 0.001 ? atan2(-(lx - sx), ly - sy) * 180 / $pi : 0));
  knobtip(kx, ky, slider63 >= 0.5 ? "Source angle (Aim: Fixed): the dot points where the source points, as seen in the plan – 0° (mark) = towards the front wall, +90° = left wall, −90° = right wall, ±180° = back wall. Drag around the dial (snaps to 15°), Shift = fine, wheel = 1°, double-click = type" : "Source angle (Aim: Listener): the dot points where the source points, as seen in the plan – 0° (mark) = at the listener, ±180° = its back to the listener. Drag around the dial (snaps to 15°), Shift = fine, wheel = 1°, double-click = type");
  kchg && (!rbyp || isfree) ? setp(61, nf);
  kx += ksp; kcol = cLIS; sprintf(#fv, abs(slider17 - floor(slider17 + 0.5)) > 0.05 ? "%.1f°" : "%.0f°", slider17); kdim = slider16 < 0.5;   // the listener's own angle (Facing); with Facing: Source it is not used - turning it switches to Fixed
  nf = dirknob(35, kx, ky, "Lis angle", #fv, slider17, 180);
  knobtip(kx, ky, "Listener angle (Facing Fixed): where the listener looks – 0° (mark) = straight ahead, to the back wall, +90° = right, −90° = left, ±180° = to the front. Drag around the dial (snaps to 15°), Shift = fine, wheel = 1°, double-click = type, Alt+click = 0°. Facing Source / Path: turning it switches to Fixed");
  kchg ? ( slider16 < 0.5 ? ( slider16 = 1; autom(32768); ); setp(17, nf); ); kdim = 0;
// DISTANCE (perspective): extra level drop with the distance beyond 2 m - 0 % = physics, 100 % = + 6 dB per doubling
  kx += ksp; kcol = cRMTT; sprintf(#fv, "%.0f %%", slider117); kdim = slider60 >= 0.5;
  kmap(0, 0, 100, 1);
  nf = knob(34, kx, ky, "Distance", #fv, slider117 / 100, 0); kdim = 0;
  knobtip(kx, ky, slider60 >= 0.5 ? "Distance: no effect while Equal loudness is on (Output)" : "Distance (perspective, for film): beyond 2 m the whole room sound gets quieter with the distance – 0 % = physics (in a big hall the reverb keeps the level up), 100 % = 6 dB more per doubling, like a film mix. The direct / room ratio stays real – double-click = type a value, Alt+click = 0 %");
  kchg ? setp(117, nf * 100);
  kcol = cTEXT;
);
// ---- REVERB: second row of the ROOM section ----
secopen(1) ? (
  col(cHAIR, 1); LN(x0 + 12, syV + 2, x0 + w0 - 12, syV + 2);
  col(cRVTT, 1); RR(x0 + 19, syV + 9, 3, 12, 1.5); col(cTEXT, 1); gfx_setfont(2); txt("Reverb", x0 + 28, syV + 15, 0);
  // HEAR: direct / early / late on / off (click = only this one, again = all three; Shift+click = switch this one on / off) - the same switches as in the echogram
  rvhx = x0 + 27 + tws("Reverb") + 18; col(cMUTED, 1); gfx_setfont(4); txt("Hear", rvhx, syV + 15, 0);
  rvhx += tws("Hear") + 6; rvm = vopt[18]; gk = 0;
  loop(3,
    hcx = rvhx + gk * 46; hon = !(rvm & (2 ^ gk)); hrcol = gk == 0 ? cSRC : (gk == 1 ? cRMTT : cRVTT);
    chip(hcx, syV + 8, 42, 14, gk == 0 ? "Direct" : (gk == 1 ? "Early" : "Late"), hon, hrcol) ? (
      (mouse_cap & 8) ? ( vopt[18] = (rvm & (2 ^ gk)) ? rvm - 2 ^ gk : rvm + 2 ^ gk; ) : ( vopt[18] = rvm == 7 - 2 ^ gk ? 0 : 7 - 2 ^ gk; );
    );
    tipbox(hcx, syV + 8, 42, 14, gk == 0 ? "Direct sound – click: hear only this (again: all three) · Shift+click: on / off · monitor only: not saved, ends when the window closes" : (gk == 1 ? "Early reflections, the first echoes from the walls – click: hear only these (again: all three) · Shift+click: on / off · monitor only: not saved, ends when the window closes" : "Late reverb, the diffuse tail – click: hear only the tail (again: all three) · Shift+click: on / off · monitor only: not saved, ends when the window closes"));
    gk += 1;
  );
  pfx = rvhx + 3 * 46 + 14; pfon = vopt[8] >= 0.5;
  isfree && !tabm ? ( grndseg(pfx, syV + 7, 220); ) : (   // outdoors - the ground instead of the predelay link (in the Tabs it is in the Free field tab)
  chip(pfx, syV + 8, 124, 14, "Predelay follows room", pfon, cRVTT) ? ( vopt[8] = pfon ? 0 : 1; !pfon ? ( pdint = 1; setp(34, pdroom()); pdint = 0; ); );
  tipbox(pfx, syV + 8, 124, 14, pfon ? "On: when width, depth, height or size change, the predelay goes with the room (as in the presets: larger room = longer gap, 0 … 60 ms). Turning Predelay by hand switches this off"
                                     : "Off: the predelay stays where you set it. Click: it follows the room size again (set now for this room)"); );
  isfree && (tabm || x0 + w0 - 14 - 235 > pfx + 226) ? ( gfx_setfont(4); col(cMUTED, 1); sprintf(#w1, tabm ? "Free field is on – the room is off (tab Free field)" : "Free field: no reverb · distance %.2f m", dpd); txt(#w1, x0 + w0 - 14, syV + 15, 2); );   // not over the Ground row when the card is narrow
  ky = syV + 50 + secx(1) / 4; kx = kx0; kcol = cRVTT; kdim = isfree;
  sprintf(#fv, "%.1f", slider20);
  kmap(0, -60, 12, 1);
  nf = knob(5, kx, ky, "Early dB", #fv, lin2f(slider20, -60, 12), lin2f(0, -60, 12));
  knobtip(kx, ky, "Level of the early reflections – 0 dB = physically correct level");
  kchg ? setp(20, f2lin(nf, -60, 12));
  kx += ksp; sprintf(#fv, "%.1f", slider21);
  kmap(0, -60, 12, 1);
  nf = knob(6, kx, ky, "Late dB", #fv, lin2f(slider21, -60, 12), lin2f(0, -60, 12));
  knobtip(kx, ky, "Level of the late reverb tail – 0 dB = physically correct level for this room");
  kchg ? setp(21, f2lin(nf, -60, 12));
  kx += ksp; sprintf(#fv, "%.2f s", rt);
  kmap(1, 0.01, 10, max(0.001, rt / max(0.01, slider32)));
  nf = knob(14, kx, ky, "Decay", #fv, log2f(min(10, max(0.01, slider32)), 0.01, 10), log2f(1, 0.01, 10));
  knobtip(kx, ky, "Decay: reverb time RT60 in seconds – Alt+click = the physical value of this room, max 20 s");
  kchg ? setp(32, f2log(nf, 0.01, 10));   // the same range as the drawing (0.05 here made the knob jump when touched)
  kx += ksp; sprintf(#fv, "%.0f ms", slider34);
  kmap(0, 0, 200, 1);
  nf = knob(16, kx, ky, "Predelay", #fv, slider34 / 200, 0);
  knobtip(kx, ky, "Predelay: extra time before reflections and reverb start (0–200 ms) – bigger = more distance between voice and room");
  kchg ? setp(34, nf * 200);
  kx += ksp; sprintf(#fv, "%.0f", slider38); strcat(#fv, " %");
  kmap(0, 0, 100, 1);
  nf = knob(18, kx, ky, "Density", #fv, slider38 / 100, 0.8);
  knobtip(kx, ky, "Density: how smooth the tail is – low = sparse, distinct echoes (slap), high = dense, smooth reverb");
  kchg ? setp(38, nf * 100);
  kx += ksp; tv = slider33;
  tv < -0.5 ? sprintf(#fv, "-%.0f", -tv) : ( tv > 0.5 ? sprintf(#fv, "+%.0f", tv) : strcpy(#fv, "0") );
  kmap(0, -100, 100, 1);
  nf = knob(15, kx, ky, "Tone", #fv, (tv + 100) / 200, 0.5);
  knobtip(kx, ky, "Tone of the room sound: left = darker, right = brighter – the direct sound stays unchanged");
  kchg ? setp(33, nf * 200 - 100);
  kx += ksp; sprintf(#fv, "%.0f %%", slider62);
  kmap(1, 25, 400, 1);
  nf = knob(31, kx, ky, "Reflect", #fv, log2f(slider62, 25, 400), 0.5);
  knobtip(kx, ky, "Reflectivity of all surfaces: 100 % = the materials as they are, 200 % = half the absorption (livelier, longer reverb), 50 % = twice the absorption (drier) – double-click = type a value, Alt+click = 100 %");
  kchg ? setp(62, f2log(nf, 25, 400)); kdim = 0;
  // MORPH (automatable, Expert only): off, 1 … 16 = that snapshot, in between = blend into the next one (tabs: in the room view heading)
  !basic && !tabm ? (
  kx += ksp; kcol = cRMTT; mrk = slider64;
  mrk < 1 ? strcpy(#fv, "off") : ( abs(mrk - floor(mrk + 0.5)) < 0.005 ? sprintf(#fv, "snap %d", floor(mrk + 0.5)) : sprintf(#fv, "%d→%d %.0f%%", floor(mrk), floor(mrk) + 1, (mrk - floor(mrk)) * 100) );
  kmap(0, 0, 16, 1);
  nf = knob(33, kx, ky, "Snapshot Morph", #fv, mrk / 16, 0);
  knobtip(kx, ky, "Snapshot morph (automatable): off, 1 … 16 = that snapshot, in between = blend into the next one (3.5 = half 3, half 4). Automate it in REAPER to change rooms with the picture – double-click = type a value, Alt+click = off");
  kchg ? setp(64, nf < 0.5 / 16 ? 0 : nf * 16);
  );
  kcol = cTEXT;
);
);
kscale(1);
// ---- SOURCE: model, Reset model, Models, editable EQ, slopes, drive ----
(!szoom || szoom == 3) ? (
x0 = srX; w0 = srW;
card(x0, syS, w0, shS); sechdr(3, "Source", x0, syS, cSRTT);
tgl(x0 + w0 - 36, syS + 5, slider85 >= 0.5, cSRTT) ? ( slider85 = slider85 >= 0.5 ? 0 : 1; sauto1(85); );
hmix(63, x0 + w0 - (tabm ? 132 : 112), syS + 12, 89, cSRTT, "Amount of the source model (module mix): 100 % = the model as set, 0 % = the input as it is, in between = both (double-click = type a value, Alt+click = 100 %, automatable)");
tipbox(x0 + w0 - 36, syS + 5, 26, 14, "Source model on / off – off: the input as it is, to compare (the model and its EQ are kept)");
secopen(3) ? (
  col(cSURF, 1); RR(srcbx, srcby, srcbw, srcbh, 4); col(smenu ? cMUTED : cBORD, 1); RRO(srcbx, srcby, srcbw, srcbh, 4);
  icOL = 1; srcicon(fzsel, srcbx + 11, srcby + 14, 0.46 * icfit(fzsel), cTEXT, cSURF); icOL = 0;   // tall icons inside the box (line icon)
  spcur >= 0 ? strcpy(#w1, spname(spcur)) : ( strcpy(#w1, srcname(fzsel)); eqed ? strcat(#w1, " *"); );
  col(cTEXT, 1); gfx_setfont(6); txtfit(#w1, srcbx + 22, srcby + srcbh / 2, srcbw - 40);
  col(cMUTED, 1); POLY4(srcbx + srcbw - 13, srcby + 6, srcbx + srcbw - 6, srcby + 6, srcbx + srcbw - 9.5, srcby + 10, srcbx + srcbw - 9.5, srcby + 10);
  strcpy(#ssub, srcsub(fzsel)); strcat(#ssub, "  ·  "); strcat(#ssub, srcsub2(fzsel));
  tipbox(srcbx, srcby, srcbw, srcbh, #ssub);
  chip(esx, mdly, 64, 14, "Reset model", 0, cSRTT) ? ( eqed ? eqreset(); );
  tipbox(esx, mdly, 64, 14, "Reset model: back to the filter curve and slopes of the selected model");
  chip(mdlx, mdly, 44, 14, "Models", helpon, cSRTT);
  tipbox(mdlx, mdly, 44, 14, "Models: all source models with their filter curves – click one to use it");
  sqx = x0 + 10; sqy = syS + 28; sqw = w0 - (tabm ? 256 : 184); sqh = shS - (tabm ? 52 : 40) - clsh;
  eqview(1);
  fzTP[1] > 0 ? sprintf(#w1, "Low cut %d dB", fzTP[1] * 6) : strcpy(#w1, "Low cut off");
  ddbox(eslx1, esly, eslw, eslh, #w1, 1, emenu == 1);
  tipbox(eslx1, esly, eslw, eslh, "Low cut (high-pass) steepness in dB per octave – Model = as the device is built, or choose 6 … 48");
  fzTP[3] > 0 ? sprintf(#w1, "High cut %d dB", fzTP[3] * 6) : strcpy(#w1, "High cut off");
  ddbox(eslx2, esly, eslw, eslh, #w1, 1, emenu == 2);
  tipbox(eslx2, esly, eslw, eslh, "High cut (low-pass) steepness in dB per octave – Model = as the device is built, or choose 6 … 48");
// CALL LINE: what the call does before the device plays it (devices only - Original stays the plain input)
  cldm = fzsel == 0;
  tabm ? ( col(cMUTED, 1); gfx_setfont(4); txt(cldm ? "Call line  –  choose a device first" : "Call line  –  the way to the device", clx, clyy + 10, 0); col(cLINE, 1); R(clx, clyy + 20, clw, 1); );
  strcpy(#w1, "Line: "); tabm || szoom == 3 ? strcat(#w1, lnname(slider114)) : strcat(#w1, lnshort(slider114));
  ddbox(lnbx, lnby, lnbw, lnbh, #w1, !cldm, emenu == 3);
  tipbox(lnbx, lnby, lnbw, lnbh, cldm ? "Call line: choose a device model first – Original stays the plain input" : "Line: the network the call travels through before the device plays it – Narrowband 300 Hz – 3.4 kHz (classic call), Wideband 100 Hz – 7 kHz (HD Voice), Super-wideband 50 Hz – 14 kHz (VoIP). Off = the device only");
  kmap(0, 0, 100, 1);
  tabm ? (
    kscale(1.15); kcol = cSRTT; kdim = cldm; clky = lnby + 7;
    clval(slider111); nf = knob(90, lnbx + lnbw + 70, clky, "Duplex", #fv, slider111 / 100, 0);
    knobtip(lnbx + lnbw + 70, clky, cldm ? "Duplex: choose a device model first" : "Duplex: the speakerphone switch and AGC of the other side – word onsets clipped, tails chopped, the level pumps, comfort noise in the gaps (0 = off)");
    kchg && !cldm ? setp(111, nf * 100); kchg = 0; kmap(0, 0, 100, 1);
    clval(slider112); nf = knob(91, lnbx + lnbw + 170, clky, "Codec", #fv, slider112 / 100, 0);
    knobtip(lnbx + lnbw + 170, clky, cldm ? "Codec: choose a device model first" : "Codec: low-bitrate artifacts – holes in the spectrum, watery highs, coarse levels (a bad mobile or VoIP call); adds one frame of latency (11 ms)");
    kchg && !cldm ? setp(112, nf * 100); kchg = 0; kmap(0, 0, 100, 1);
    clval(slider113); nf = knob(92, lnbx + lnbw + 270, clky, "Dropouts", #fv, slider113 / 100, 0);
    knobtip(lnbx + lnbw + 270, clky, cldm ? "Dropouts: choose a device model first" : "Dropouts: lost packets – short robotic repeats of the last sound, then gaps (up to 40 % of the packets, in bursts); adds one frame of latency (11 ms)");
    kchg && !cldm ? setp(113, nf * 100); kchg = 0;
    kdim = 0; kcol = cTEXT; kscale(1);
  ) : (
    clhw = lnbw; clx2 = clx + clhw + 10;
    clval(slider111); nf = hsl(90, clx2, clyy + 3, clhw, "Duplex", #fv, slider111 / 100, 0, cldm ? cDIM : cSRTT);
    tipbox(clx2, clyy - 3, clhw, 20, cldm ? "Duplex: choose a device model first" : "Duplex: the speakerphone switch and AGC of the other side – word onsets clipped, tails chopped, the level pumps, comfort noise in the gaps (0 = off)");
    kchg && !cldm ? setp(111, nf * 100); kchg = 0; kmap(0, 0, 100, 1);
    clval(slider112); nf = hsl(91, clx, clyy + 26, clhw, "Codec", #fv, slider112 / 100, 0, cldm ? cDIM : cSRTT);
    tipbox(clx, clyy + 20, clhw, 20, cldm ? "Codec: choose a device model first" : "Codec: low-bitrate artifacts – holes in the spectrum, watery highs, coarse levels (a bad mobile or VoIP call); adds 11 ms latency");
    kchg && !cldm ? setp(112, nf * 100); kchg = 0; kmap(0, 0, 100, 1);
    clval(slider113); nf = hsl(92, clx2, clyy + 26, clhw, "Dropouts", #fv, slider113 / 100, 0, cldm ? cDIM : cSRTT);
    tipbox(clx2, clyy + 20, clhw, 20, cldm ? "Dropouts: choose a device model first" : "Dropouts: lost packets – short robotic repeats, then gaps (up to 40 % of the packets, in bursts); adds 11 ms latency");
    kchg && !cldm ? setp(113, nf * 100); kchg = 0;
  );
  sprintf(#fv, "%.0f", slider37); strcat(#fv, " %");
  kmap(0, 0, 100, 1);
  tabm ? (
    kscale(1.3); kcol = cSRTT; dsky = syS + 112 + KR;
    nf = knob(17, esx + 56, dsky, "Distortion", #fv, slider37 / 100, 0.5);
    knobtip(esx + 56, dsky, "Distortion: how hard the source is pushed – 50 % = typical, less = cleaner, more = crunchier (saturation, clipping, codec)");
    kchg ? setp(37, nf * 100);
    swfmt(); wdm = slider110 >= 0.5 || devm;
    kdim = wdm; nf = knob(19, esx + 184, dsky, "Stereo width", #fv, slider98 / 200, 0.5); kdim = 0;
    knobtip(esx + 184, dsky, wdm ? (devm ? "Stereo width – no effect with a device: a phone, radio … is a mono loudspeaker (it counts with Original or a module mix under 100 %)" : "Stereo width – off while Mono mix is on") : "Stereo width of the input: 100 % = as it comes in (true stereo), 0 % = mono, up to 200 % = wider than the input (watch the correlation). The side signal travels with the direct sound (double-click = type, Alt+click = 100 %)");
    kchg && !wdm ? setp(98, nf * 200);
    kchg = 0;
    swunit(esx + 184 - 14, dsky + KR + 17 * KNS);
    monobtn(esx + 108, dsky - 7, 0);
    kcol = cTEXT; kscale(1);
  ) : (
  nf = hsl(17, esx, syS + 62, 154, "Distortion", #fv, slider37 / 100, 0.5, cSRTT);
  tipbox(esx, syS + 56, 154, 20, "Distortion: how hard the source is pushed – 50 % = typical, less = cleaner, more = crunchier (saturation, clipping, codec)");
  kchg ? setp(37, nf * 100); kchg = 0;
  swfmt(); wdm = slider110 >= 0.5 || devm;
  kdim = wdm; nf = hsl(19, esx, syS + 108, 154, "Stereo width", #fv, slider98 / 200, 0.5, wdm ? cDIM : cSRTT); kdim = 0;
  tipbox(esx, syS + 102, 154, 20, wdm ? (devm ? "Stereo width – no effect with a device: a phone, radio … is a mono loudspeaker (it counts with Original or a module mix under 100 %)" : "Stereo width – off while Mono mix is on") : "Stereo width of the input: 100 % = as it comes in (true stereo), 0 % = mono, up to 200 % = wider than the input (Alt+click = 100 %)");
  kchg && !wdm ? setp(98, nf * 200); kchg = 0;
  swunit(esx + 154 - 30, syS + 126);
  monobtn(esx, syS + 119, 1);
  );
  slider85 < 0.5 ? ( col(cSURF, 0.55); R(x0 + 2, syS + 24, w0 - 4, shS - 26); );   // source model off
);
);
// ---- WALLS (right column, top): floor / ceiling, surface + material, absorption of all six surfaces ----
x0 = R0; w0 = RWc
);
// ---- MOTION: source and / or listener on a pattern - the module (Expert; tab or the right column of All) ----
// icons of the motion patterns, p = pattern index 0 … 7 (24 x 20 around cx, cy)
function mticon(p, cx, cy)
(
  p == 0 ? ( CIF(cx, cy, 7, 0); LNW(cx + 5.5, cy - 5.5, cx + 7.5, cy - 9, 0.85); LNW(cx + 7.5, cy - 9, cx + 9.5, cy - 5, 0.85); CIF(cx, cy, 1.6, 1); ) : (
  p == 1 ? ( LNW(cx - 11, cy, cx + 11, cy, 0.85); LNW(cx - 11, cy, cx - 7, cy - 3, 0.85); LNW(cx - 11, cy, cx - 7, cy + 3, 0.85); LNW(cx + 11, cy, cx + 7, cy - 3, 0.85); LNW(cx + 11, cy, cx + 7, cy + 3, 0.85); CIF(cx + 3, cy, 1.7, 1); ) : (
  p == 2 ? ( LNW(cx, cy - 9, cx, cy + 9, 0.85); LNW(cx, cy - 9, cx - 3, cy - 5, 0.85); LNW(cx, cy - 9, cx + 3, cy - 5, 0.85); LNW(cx, cy + 9, cx - 3, cy + 5, 0.85); LNW(cx, cy + 9, cx + 3, cy + 5, 0.85); CIF(cx, cy + 3, 1.7, 1); ) : (
  p == 3 ? ( POLY4(cx - 9, cy + 8, cx - 3, cy + 4, cx + 9, cy + 4, cx + 3, cy + 8); LNW(cx, cy - 10, cx, cy + 5, 0.85); LNW(cx, cy - 10, cx - 3, cy - 6, 0.85); LNW(cx, cy - 10, cx + 3, cy - 6, 0.85); CIF(cx, cy - 2, 1.7, 1); ) : (
  p == 4 ? ( LNW(cx - 11, cy + 4, cx + 11, cy + 4, 0.85); LNW(cx + 11, cy + 4, cx + 7, cy + 1, 0.85); LNW(cx + 11, cy + 4, cx + 7, cy + 7, 0.85); CIF(cx - 1, cy + 4, 1.7, 1); CIF(cx - 6, cy + 4, 1.1, 1); CIF(cx, cy - 5, 2.2, 0); ) : (
  p == 5 ? ( LNBOX(cx - 7, cy - 7, 14, 14, 0.85); LNW(cx + 7, cy - 1, cx + 4, cy + 2, 0.85); LNW(cx + 7, cy - 1, cx + 10, cy + 2, 0.85); CIF(cx, cy, 1.6, 1); ) : (
  p == 6 ? ( mik = 0; loop(12, LNW(cx - 11 + mik * 22 / 12, cy - 5 * sin(mik * $pi / 3), cx - 11 + (mik + 1) * 22 / 12, cy - 5 * sin((mik + 1) * $pi / 3), 0.85); mik += 1; ); CIF(cx + 0.5, cy - 4.3, 1.7, 1); ) : (
             mik = 0; loop(14, LNW(cx - 10 + mik * 1.3 + 3 * sin(mik * 0.9), cy + 6 - mik * 0.75 - 4 * cos(mik * 0.7), cx - 10 + (mik + 1) * 1.3 + 3 * sin((mik + 1) * 0.9), cy + 6 - (mik + 1) * 0.75 - 4 * cos((mik + 1) * 0.7), 0.85); mik += 1; );
             LNW(cx + 7, cy - 9, cx + 11, cy - 5, 1.6); CIF(cx + 7, cy - 9, 1.2, 1); )))))));
);
// a pencil (tip bottom left), cx cy = its middle
function mtpencil(cx, cy) local(ux, uy, nx, ny, hw, tx, ty, bx, by, ex, ey)
(
  ux = 0.7071; uy = -0.7071; nx = 0.7071; ny = 0.7071; hw = 2.4;
  tx = cx - 9 * ux; ty = cy - 9 * uy; bx = tx + 5 * ux; by = ty + 5 * uy; ex = tx + 19 * ux; ey = ty + 19 * uy;
  LNW(bx + nx * hw, by + ny * hw, ex + nx * hw, ey + ny * hw, 1); LNW(bx - nx * hw, by - ny * hw, ex - nx * hw, ey - ny * hw, 1);
  LNW(ex + nx * hw, ey + ny * hw, ex - nx * hw, ey - ny * hw, 1);
  LNW(ex - 3 * ux + nx * hw, ey - 3 * uy + ny * hw, ex - 3 * ux - nx * hw, ey - 3 * uy - ny * hw, 0.8);
  LNW(bx + nx * hw, by + ny * hw, bx - nx * hw, by - ny * hw, 0.8);
  LNW(bx + nx * hw, by + ny * hw, tx, ty, 1); LNW(bx - nx * hw, by - ny * hw, tx, ty, 1);
  CIF(tx + 1.2 * ux, ty + 1.2 * uy, 0.9, 1);
);
// Motion module: pattern tiles, start/restart, presets, around/direction/rate settings and drawn-path slots
function gfxpM() local(x0, w0, y0, mtq, pat, mvs, ctr, k, cx, cy, tw1, bx, bw, nf, kx, by, ksp, hz, sec, mtNar, dx2, arOff, lin, clip, cx0, cy0, rsz, fitr, fz0, wd, lx, sx, px, py, pw, ph, tw, th, q, tx, ty, kx0, stx, sty, stw, on, k0q, roomy)
(
  x0 = mtPX0; w0 = mtPW0; y0 = syM;
  card(x0, y0, w0, shM); sechdr(14, "Motion", x0, y0, cMOTT);
  tgl(x0 + w0 - 36, y0 + 5, slider100 >= 0.5, cMOTT) ? ( setp(100, slider100 >= 0.5 ? 0 : 1); );
  tipbox(x0 + w0 - 36, y0 + 5, 26, 14, slider100 >= 0.5 ? "Motion on – click: off (the glide brings source and listener back to where they started)" : "Motion off – click: start the pattern from where the balls stand now (it fades in over Glide). The positions are written to Source / Listener X, Y (and height) – record them in REAPER with Write or Latch");
// START / RESTART: the motion begins now (switched on if it is off) - the same at every click
  !learn && secopen(14) ? (
// RESTART: the pattern begins again from its start, now - and goes on; off: it is switched on
    chip(x0 + w0 - 122, y0 + 3, 78, 18, "Restart", slider100 >= 0.5, cMOTT) ? ( slider100 < 0.5 ? setp(100, 1); mtTrg += 1; );
    tipbox(x0 + w0 - 122, y0 + 3, 78, 18, slider100 >= 0.5 ? "Restart: the pattern begins again from its start (the Start point; a pass-by from A) and runs on" : "Restart: switches Motion on and starts the pattern from its beginning");
    mffx = x0 + w0 - 122 - 8 - 96 - 56;   // Free field - in the heading row, larger (it needs room: not on a narrow module)
    gfx_setfont(2); mfq = x0 + 28 + tws("Motion") + 56; mffx >= mfq + min(170, max(90, w0 - (mfq - x0) - 320)) + 6 + 50 + 8 ? ( chip(mffx, y0 + 3, 96, 18, "Free field", isfree, cMOTT) ? ( modeset(isfree ? 0 : 1); snlast = 0; click = 0; ); tipbox(mffx, y0 + 3, 96, 18, isfree ? "Free field is on (no walls, no reverb): the Doppler pitch change is heard without a room – click: back to the room" : "Free field: no walls, no reverb – the best way to hear the Doppler pitch change. Click again for the room"); );   // quick switch
  );
  mtEg > 0.001 ? ( col(cMOTT, 1); gfx_setfont(7); txt(mtE >= 0.999 ? (slider100 >= 0.5 ? "running" : "fading out") : "fading in", x0 + w0 - 128, y0 + 12, 2); );
// presets (practical moves) and Center (listener in the middle, source 2 m in front)
  gfx_setfont(2); mppx = x0 + 28 + tws("Motion") + 56; mppw = min(170, max(90, w0 - (mppx - x0) - 320)); mppy = y0 + 4;
  !learn && secopen(14) ? (
    ddbox(mppx, mppy, mppw, 16, "Presets", 1, mpmenu);
    click && inbox(mppx, mppy, mppw, 16) ? ( mpmenu = 1; click = 0; );
    tipbox(mppx, mppy, mppw, 16, "Motion presets: practical moves for film – each starts from where source and listener are now (nothing jumps)");
    chip(mppx + mppw + 6, mppy + 1, 50, 14, "Center", 0, cMOTT) ? ( !vlock ? posreset(); );
    tipbox(mppx + mppw + 6, mppy + 1, 50, 14, "Center: the listener to the middle of the room, the source 2 m in front (the motion stops) – then a circle goes around the listener");
  );
  secopen(14) ? (
    mtq = floor(slider108 + 0.5); pat = floor(slider102 + 0.5); mvs = floor(slider101 + 0.5); ctr = floor(slider107 + 0.5);
// LAYOUT: the patterns in a panel of their own (a 3 x 3 grid of tiles, left), the settings right of it in rows;
    // a narrow column: the tiles in one row under "Moves", the settings below
    wd = w0 >= 600; mtNar = !wd;
    wd ? ( px = x0 + 10; py = y0 + 28; tw = 60; th = 40; pw = 3 * tw + 2 * 4 + 12; ph = 18 + 3 * th + 2 * 4 + 6; lx = px + pw + 16; )
       : ( px = x0 + 10; py = y0 + 48; pw = w0 - 20; ph = 34; tw = (pw - 8 - 8 * 4) / 9; th = 26; lx = x0 + 10; );
    sx = lx + 50;
    col(cMOTT, 0.07); RR(px, py, pw, ph, 8); col(cMOTT, 0.35); RRO(px, py, pw, ph, 8);
    wd ? ( gfx_setfont(7); col(cMUTED, 1); txt("Pattern", px + 8, py + 10, 0); );
    gfx_setfont(4); col(cMUTED, 1);
    // what moves
    cy = wd ? y0 + 38 : y0 + 36; txt("Moves", lx, cy, 0);
    segtrack(sx, cy - 8, 168, 16);
    segp(sx, cy - 8, 168, 16, 3, 0, "Source", mvs == 0, cTEXT) ? setp(101, 0);
    segp(sx, cy - 8, 168, 16, 3, 1, "Listener", mvs == 1, cTEXT) ? setp(101, 1);
    segp(sx, cy - 8, 168, 16, 3, 2, "Both", mvs == 2, cTEXT) ? setp(101, 2);
    tipbox(sx, cy - 8, 168, 16, "What moves: the source, the listener or both (Both = the same pattern, opposite phase – they pass each other)");
    // the tiles: Circle Square Left-right / Front-back Up-down Pass-by / Wave Drawn Draw (q = 8: the pencil)
    k = 0;
    loop(9,
      // q = pattern index shown by tile k (the tile order differs from the pattern numbers; q = 8 is the Draw pencil, not a pattern)
      q = k == 0 ? 0 : (k == 1 ? 5 : (k == 2 ? 1 : (k == 3 ? 2 : (k == 4 ? 3 : (k == 5 ? 4 : (k == 6 ? 6 : (k == 7 ? 7 : 8)))))));
      wd ? ( tx = px + 6 + (k % 3) * (tw + 4); ty = py + 18 + floor(k / 3) * (th + 4); ) : ( tx = px + 4 + k * (tw + 4); ty = py + 4; );
      bx = tx; bw = tw; cy = ty + th / 2;
      on = q == 8 ? mtRec : pat == q;
      col(on ? cMOTT : (inbox(tx, ty, tw, th) ? cHAIR : cSURF), 1); RR(tx, ty, tw, th, 6); !on ? ( col(cLINE, 1); RRO(tx, ty, tw, th, 6); );
      col(on ? cSURF : cTEXT, 1);
      wd ? ( q == 8 ? mtpencil(tx + tw / 2, ty + 15) : mticon(q, tx + tw / 2, ty + 15);
             gfx_setfont(7); strcpy(#w1, q == 8 ? "Draw" : (q == 1 ? "Left–right" : (q == 2 ? "Front–back" : (q == 3 ? "Up–down" : mtname(q)))));
             tws(#w1) > tw - 4 ? strcpy(#w1, q == 1 ? "L – R" : (q == 2 ? "F – B" : (q == 3 ? "Up – dn" : #w1))); txt(#w1, tx + tw / 2, ty + 32, 1); )
         : ( q == 8 ? mtpencil(tx + tw / 2, cy) : mticon(q, tx + tw / 2, cy); );
      k += 1; k0q = k; k = q;
      q == 8 ? (
        click && inbox(bx, ty, bw, th) ? ( mtRec = !mtRec; click = 0; );
        tipbox(bx, ty, bw, th, mtRec ? "Drawing is armed – hold the mouse button and draw the path in the Stage View. Click here again to cancel" : "Draw a motion path: click, then draw it with the mouse in the Stage View (hold the button). When you let go it is smoothed, saved with the project and runs at once (pattern Drawn) – with the speed you drew: fast and slow parts are kept");
      ) : (
      click && inbox(bx, ty, bw, th) ? ( k == 7 && MTDP[512] < 256 ? ( mtRec = 1; ) : setp(102, k); click = 0; );   // Drawn with no path yet: draw one
      tipbox(bx, ty, bw, th, k == 5 ? "Square: along the edges of a square around the center (listener, own spot or room center) – Size = half the side" : (k == 6 ? "Wave: left – right on a wavy line (a slalom) – Size = half the swing" : (k == 7 ? "Drawn: the path you drew with the mouse (Draw), one round per cycle – Size scales it, the center is the ball's own spot" : (k == 0 ? "Circle: around the listener, its own spot or the room center – Size = the radius" : (k == 1 ? "Left – right: a sine across the room (the classic auto pan – with distance, reflections and room)" : (k == 2 ? "Front – back: towards the listener and away – level, direct-to-reverb ratio and predelay move together" : (k == 3 ? "Up – down: between floor and ceiling – floor and ceiling reflections move (3D shows it)" : "Pass-by: once along a straight line through its own spot – from Size before it to Size after it, in one cycle time (Rate). Start = the angle of the line (0° = left to right), CCW = the other way. Starts again at every playback start and loop; Both: source and listener pass each other")))))));
      );
      k = k0q;
    );
    // row 2: around, direction, rate mode
    cy = wd ? y0 + 62 : y0 + 100; gfx_setfont(4); col(cMUTED, 1);
    pat == 7 ? (
// PATH SLOTS: four drawn paths - a click on a stored one uses it, on an empty one: draw into it
      txt("Path", lx, cy, 0); k = 0; mtSl = floor(MTDS[3080] + 0.5);
      loop(4,
        mtSf = MTDS[770 * k + 512] >= 256; sprintf(#w1, mtSf ? "%d" : "%d +", k + 1);
        chip(sx + k * 46, cy - 7, 42, 14, #w1, k == mtSl, cMOTT) ? ( MTDS[3080] = k; mtSf ? ( memcpy(MTDP, MTDS + 770 * k, 770); mtEg > 0.001 ? mtTrg += 1; ) : ( mtRec = 1; ); );
        tipbox(sx + k * 46, cy - 7, 42, 14, mtSf ? "Drawn path – click: use this one (Draw: draw it again)" : "Empty – click, then draw a path in the Stage View: it is stored here");
        k += 1; );
    ) : (
    txt("Around", lx, cy, 0);
    arOff = pat != 0 && pat != 5;   // only Circle and Square have a center to choose - every other pattern dims the row
    segtrack(sx, cy - 8, 220, 16);
    mvs == 1 ? (   // a moving listener alone has two choices - its own spot ("Listener" would be the same) or the room center
      segp(sx, cy - 8, 220, 16, 3, 0, "Source", ctr == 0, cTEXT) && !arOff ? setp(107, 0);   // the listener walks around the source
      segp(sx, cy - 8, 220, 16, 3, 1, "Own spot", ctr == 1, cTEXT) && !arOff ? ( apinit(); setp(107, 1); );
      segp(sx, cy - 8, 220, 16, 3, 2, "Room center", ctr == 2, cTEXT) && !arOff ? setp(107, 2);
    ) : (
      segp(sx, cy - 8, 220, 16, 3, 0, "Listener", ctr == 0, cTEXT) && !arOff ? setp(107, 0);
      segp(sx, cy - 8, 220, 16, 3, 1, "Own spot", ctr == 1, cTEXT) && !arOff ? ( apinit(); setp(107, 1); );
      segp(sx, cy - 8, 220, 16, 3, 2, "Room center", ctr == 2, cTEXT) && !arOff ? setp(107, 2);
    );
    );
    pat != 7 ? ( arOff ? ( col(cSURF, 0.6); R(lx - 2, cy - 9, sx - lx + 224, 18); tipbox(sx, cy - 8, 220, 16, pat == 3 ? "Up – down moves on the spot – no center to choose" : (pat == 1 || pat == 2 || pat == 6 ? "Lines and the wave start where the ball is and swing around that spot – no center to choose" : (pat == 7 ? "A drawn path runs around its own spot – drag the moving ball to move it" : "A pass-by starts where the ball is – no center to choose"))); )
          : tipbox(sx, cy - 8, 220, 16, "Center of the pattern: the listener (when the listener moves: the source), your own spot (a point you place: with Circle or Square, drag the cross on the floor of the Stage View) or the middle of the room. Drag a moving ball = move the whole path") );
    pat != 7 && !arOff && ctr == 1 && MTAP[2] < 0.5 ? ( gfx_setfont(7); col(cMUTED, 1); txt("drag the + on the floor of the Stage View", sx + 228, cy, 0); );
    pat != 7 && !arOff && ctr == 1 && MTAP[2] >= 0.5 ? ( chip(sx + 226, cy - 7, 44, 14, "Reset", 0, cMOTT) ? MTAP[2] = 0; tipbox(sx + 226, cy - 7, 44, 14, "Back to the ball: the center is where the ball stands (as before you placed a spot)"); );
    cy = wd ? y0 + 86 : y0 + 124; dx2 = lx - 288;   // Direction and Rate on their own row
    gfx_setfont(4); col(cMUTED, 1); txt("Direction", lx, cy, 0);
    segtrack(sx, cy - 8, 96, 16);
    lin = pat > 0 && pat != 5 && pat != 7;   // lines, Up - down, Pass-by and Wave: A -> B or B -> A (CW / CCW only means something on a circle)
    segp(sx, cy - 8, 96, 16, 2, 0, lin ? "A → B" : (pat == 7 ? "Fwd" : "CW"), slider106 < 0.5, cTEXT) ? setp(106, 0);
    segp(sx, cy - 8, 96, 16, 2, 1, lin ? "B → A" : (pat == 7 ? "Rev" : "CCW"), slider106 >= 0.5, cTEXT) ? setp(106, 1);
    tipbox(sx, cy - 8, 96, 16, lin ? (pat == 4 ? "Which way along the line: A → B runs in the Start direction (0° = left to right), B → A the other way" : "Which way the swing starts: A → B towards the Start side, B → A the other way") : "Direction of the circle (seen from above): clockwise or counter-clockwise");
    gfx_setfont(4); col(cMUTED, 1); txt("Rate", sx + 112, cy, 0);
    segtrack(sx + 142, cy - 8, 96, 16);
    segp(sx + 142, cy - 8, 96, 16, 2, 0, "Hz", mtq == 0, cTEXT) ? setp(108, 0);
    segp(sx + 142, cy - 8, 96, 16, 2, 1, "Tempo", mtq > 0, cTEXT) ? ( mtq == 0 ? setp(108, 4); );
    mtq > 0 && x0 + w0 - 10 >= sx + 330 ? ( chip(sx + 248, cy - 7, 80, 14, "Tempo tools", tmbOn, cMOTT) ? ( tmbOn = !tmbOn; tpon = 0; );   // the Motion's own tempo tools, inside the module (no floating panel)
      tipbox(sx + 248, cy - 7, 80, 14, tmbOn ? "Close the tempo tools (the knobs come back)" : "Tempo tools: tap, listen to the song, type the tempo or use the project tempo – right here in the Motion module (the tempo is shared with the delay)"); );
    tipbox(sx + 142, cy - 8, 96, 16, "Rate in Hz (turns or swings per second) or locked to the project tempo (bars and notes, 4/4 = four beats a bar)");
    // row 3: presets
    cy = wd ? y0 + 109 : y0 + 148; bx = sx;
    mtq == 0 ? (
// a pass-by gets durations (s) instead of Hz; no chip lights up while the Rate / Duration knob is turned
      k = 0; rcsp = min(44, floor((x0 + w0 - 34 - sx) / (pat == 4 ? 6 : 8)));   // eight Hz presets (to 10 Hz) - narrower chips in a narrow card
      loop(pat == 4 ? 6 : 8,
        pat == 4 ? ( hz = 1 / (k == 0 ? 1 : (k == 1 ? 2 : (k == 2 ? 3 : (k == 3 ? 4 : (k == 4 ? 6 : 10))))); sprintf(#w1, "%.0f", 1 / hz); )
                 : ( hz = k == 0 ? 0.05 : (k == 1 ? 0.1 : (k == 2 ? 0.25 : (k == 3 ? 0.5 : (k == 4 ? 1 : (k == 5 ? 2 : (k == 6 ? 5 : 10)))))); hz < 1 ? sprintf(#w1, "%.2f", hz) : sprintf(#w1, "%.0f", hz); );
        chip(bx, cy - 7, rcsp - 4, 14, #w1, kdrag != 81 && abs(slider103 - hz) < 0.0005 * max(1, hz), cMOTT) ? ( setp(103, hz); );
        bx += rcsp; k += 1;
      );
      gfx_setfont(7); col(cMUTED, 1); txt(pat == 4 ? "s" : "Hz", bx + 2, cy, 0);
      tipbox(sx, cy - 7, pat == 4 ? 264 : 360, 14, pat == 4 ? "Duration presets: the pass-by from A to B in 1 … 10 seconds" : "Rate presets in Hz – 0.25 Hz = one turn in 4 s. The knob Rate sets it freely");
    ) : (
      k = 1;
      loop(8, chip(bx, cy - 7, x0 + w0 - sx < 400 ? 36 : 44, 14, mttempo(k), mtq == k, cMOTT) ? ( setp(108, k); ); bx += x0 + w0 - sx < 400 ? 40 : 48; k += 1; );
      tipbox(sx, cy - 7, 384, 14, "One turn or swing per … – from the project tempo, or the tempo of the Tempo tools (Delay) when one is set");
    );
// DOPPLER: natural (the physics as it is) or boosted - the pitch change k times stronger
    cy = wd ? y0 + 133 : y0 + 172; gfx_setfont(4); col(cMUTED, 1); txt("Doppler", lx, cy, 0);
    segtrack(sx, cy - 8, 240, 16); dpk0 = slider118 < 0.5 ? -1 : (slider118 < 1.05 ? 0 : (slider118 < 4.5 ? 1 : (slider118 < 9 ? 2 : 3)));
    segp(sx, cy - 8, 240, 16, 5, 0, "Off", dpk0 == -1, cTEXT) ? setp(118, 0);
    segp(sx, cy - 8, 240, 16, 5, 1, "Natural", dpk0 == 0, cTEXT) ? setp(118, 1);
    segp(sx, cy - 8, 240, 16, 5, 2, "× 3", dpk0 == 1, cTEXT) ? setp(118, 3);
    segp(sx, cy - 8, 240, 16, 5, 3, "× 6", dpk0 == 2, cTEXT) ? setp(118, 6);
    segp(sx, cy - 8, 240, 16, 5, 4, "× 12", dpk0 == 3, cTEXT) ? setp(118, 12);
    dpk0 > 0 && x0 + w0 - 10 >= sx + 320 ? ( gfx_setfont(7); col(cMUTED, 1); sprintf(#w1, "+%.0f ms", min(350, (slider118 - 1) * (2 * max(0.5, slider104 * rs) + 1) / C_SND * 1000)); txt(#w1, sx + 248, cy, 0); );
// the Free field button moved up to the heading row (larger) - see below Restart
    tipbox(sx, cy - 8, 240, 16, "Doppler: Off = no pitch change at all (the sound only moves in level, direction and room). Natural = the real pitch change (at walking speed in a room only a few cents). × 3 / × 6 / × 12 = the pitch changes that many times more – a pass-by sounds like a car or a plane. The room sound comes a little later while it is on (the time shown)");
// TEMPO TOOLS of the Motion: in the place of the knobs (Tempo mode only) - tap, listen, type, project tempo
    tmbS = tmbOn && mtq > 0 && !learn;
    tmbS ? (
      tmbX = wd ? lx : x0 + 10; tmbY = wd ? y0 + 146 : y0 + 190; tmbW = x0 + w0 - 10 - tmbX; tmbH = max(64, min(110, y0 + shM - 8 - tmbY));
      col(cMOTT, 0.07); RR(tmbX, tmbY, tmbW, tmbH, 8); col(cMOTT, 0.35); RRO(tmbX, tmbY, tmbW, tmbH, 8);
      col(cMOTT, 1); RR(tmbX + 8, tmbY + 6, 3, 11, 1.5); col(cTEXT, 1); gfx_setfont(4); txt("Tempo", tmbX + 16, tmbY + 12, 0);
      sprintf(#w2, slider74 > 0.5 ? "%.1f BPM" : (tempo > 1 ? "project %.1f BPM" : "%.0f BPM"), gbpm);
      gfx_setfont(6); tmbBw = tws(#w2) + 12; tmbBx = tmbX + 62; tmbBh = inbox(tmbBx, tmbY + 5, tmbBw, 15);
      col(tmbBh || vedit == 89 ? cHAIR : cSURF, 1); RR(tmbBx, tmbY + 5, tmbBw, 15, 4); col(cLINE, 1); RRO(tmbBx, tmbY + 5, tmbBw, 15, 4);
      col(cTEXT, 1); txt(#w2, tmbBx + tmbBw / 2, tmbY + 12, 1);
      kmap(0, 20, 300, 1);
      click && tmbBh ? ( sprintf(#bpv, "%.1f", gbpm); vedopen(89, #bpv); click = 0; );
      vedit == 89 ? vedplace(tmbBx + tmbBw / 2, tmbY + 12, 60);
      vedPend && vedId == 89 ? ( tmbN = min(300, max(20, 20 + vedmap(vedVal) * 280)); vedPend = 0; setp(74, floor(tmbN * 100 + 0.5) / 100); );
      tipbox(tmbBx, tmbY + 5, tmbBw, 15, "Click = type the tempo in BPM (20 … 300) – the motion and the delay play at it");
      tmbCh = closex(tmbX + tmbW - 13, tmbY + 12); click && tmbCh ? ( tmbOn = 0; click = 0; );
      tmbCh ? tipbox(tmbX + tmbW - 22, tmbY + 3, 18, 18, "Close the tempo tools (Esc)");
      // TAP (left)
      tmbTy = tmbY + 24; tmbTh = tmbH - 30; tmbTw = min(160, (tmbW - 24) * 0.3); tmbNow = time_precise();
      click && inbox(tmbX + 8, tmbTy, tmbTw, tmbTh) ? (
        tmbNow - tapLast > 2 || tapN < 1 ? ( tapN = 1; tap0 = tmbNow; ) : ( tapN += 1; );
        tapLast = tmbNow; click = 0;
        tapN >= 2 ? ( tapBPM = floor(min(300, max(20, 60 * (tapN - 1) / max(0.05, tapLast - tap0))) + 0.5); );
      );
      tmbFl = max(0, 1 - (tmbNow - tapLast) / 0.15);
      col(cMOTT, 1); RR(tmbX + 8, tmbTy, tmbTw, tmbTh, 8); tmbFl > 0 ? ( col(cSURF, 0.35 * tmbFl); RR(tmbX + 8, tmbTy, tmbTw, tmbTh, 8); );
      col(cSURF, 1); gfx_setfont(5); txt("TAP", tmbX + 8 + tmbTw / 2, tmbTy + tmbTh / 2 - 6, 1);
      tapN >= 2 && tapBPM > 0 ? sprintf(#w1, "%.0f BPM · %d taps", tapBPM, tapN) : strcpy(#w1, "click in time");
      gfx_setfont(7); txt(#w1, tmbX + 8 + tmbTw / 2, tmbTy + tmbTh / 2 + 9, 1);
      tipbox(tmbX + 8, tmbTy, tmbTw, tmbTh, "Tap in time (8 taps or more = more exact), then Use tap – best with playback stopped");
      // LISTEN (middle)
      tmbLx = tmbX + 16 + tmbTw; tmbLw = min(260, (tmbW - 24 - tmbTw) * 0.55);
      anaDone == 1 && anaRefId != anaSeen && !anaOn ? ( anaest(); anaRefId = anaSeen; );
      anaok = anaDone == 1 && anaRes == 1 && anaRefId == anaSeen;
      anaReq = anaCmd != anaSeen; anaRun = anaOn || anaReq;
      chip(tmbLx, tmbTy, tmbLw, 15, anaRun ? "Stop listening" : "Listen to the song", anaRun, cMOTT) ? ( anaRun ? ( anaStp += 1; ) : ( anaCmd += 1; anaSel = 0; ); click = 0; );
      tipbox(tmbLx, tmbTy, tmbLw, 15, "Click, then play the selected clip or the time selection – Roomservice listens for 10 s and finds the tempo");
      col(cHAIR, 1); RR(tmbLx, tmbTy + 19, tmbLw, 3, 1.5); anaN > 0 ? ( col(cMOTT, 1); RR(tmbLx, tmbTy + 19, max(3, tmbLw * anaN / ANAMAX), 3, 1.5); );
      anaRun ? ( (play_state & 1) && anaOn ? sprintf(#w1, "listening %.1f s of 10 s", anaN / 100) : strcpy(#w1, "waiting – start playback") )
             : ( anaok ? ( selb = anaSel == 1 ? anaBf / 2 : (anaSel == 2 ? anaBf * 2 : anaBf); sprintf(#w1, "found %.1f BPM (%s)", selb, anaConf >= 4.5 ? "sure" : (anaConf >= 3 ? "fairly sure" : "unsure")); )
                     : (anaDone == -1 || (anaDone == 1 && anaRes == -1) ? strcpy(#w1, "no clear beat – try a rhythmic part") : strcpy(#w1, "not started")) );
      col(cMUTED, 1); gfx_setfont(7); txtfit(#w1, tmbLx, tmbTy + 30, tmbLw);
      anaok && tmbTh >= 52 ? (
        tmbCw = (tmbLw - 6) / 3;
        sprintf(#w1, "%.1f", anaBf); chip(tmbLx, tmbTy + 38, tmbCw, 13, #w1, anaSel == 0, cTEXT) ? ( anaSel = 0; click = 0; );
        sprintf(#w1, "½ %.0f", anaBf / 2); chip(tmbLx + tmbCw + 3, tmbTy + 38, tmbCw, 13, #w1, anaSel == 1, cTEXT) ? ( anaSel = 1; click = 0; );
        anaBf * 2 <= 300 ? ( sprintf(#w1, "2× %.0f", anaBf * 2); chip(tmbLx + 2 * tmbCw + 6, tmbTy + 38, tmbCw, 13, #w1, anaSel == 2, cTEXT) ? ( anaSel = 2; click = 0; ); );
        tipbox(tmbLx, tmbTy + 38, tmbLw, 13, "The analysis can land on half or double the tempo – choose the one that feels right");
      );
      // USE (right)
      tmbUx = tmbLx + tmbLw + 8; tmbUw = min(170, tmbX + tmbW - 8 - tmbUx); tmbUh = min(15, (tmbTh - 6) / 3);
      tapok = tapN >= 2 && tapBPM > 0;
      tapok ? sprintf(#w1, "Use tap %.0f", tapBPM) : strcpy(#w1, "Use tap");
      chip(tmbUx, tmbTy, tmbUw, tmbUh, #w1, 0, tapok ? cMOTT : cDIM2) && tapok ? ( setp(74, tapBPM); click = 0; );
      tipbox(tmbUx, tmbTy, tmbUw, tmbUh, "Use the tapped tempo for the motion (and the delay)");
      anaok ? sprintf(#w1, "Use %.1f", selb) : strcpy(#w1, "Use analysis");
      chip(tmbUx, tmbTy + tmbUh + 3, tmbUw, tmbUh, #w1, 0, anaok ? cMOTT : cDIM2) && anaok ? ( setp(74, floor(min(300, selb) * 100 + 0.5) / 100); click = 0; );
      tipbox(tmbUx, tmbTy + tmbUh + 3, tmbUw, tmbUh, "Use the analyzed tempo for the motion (and the delay)");
      chip(tmbUx, tmbTy + 2 * tmbUh + 6, tmbUw, tmbUh, "Project tempo", slider74 < 0.5, cTEXT) ? ( setp(74, 0); click = 0; );
      tipbox(tmbUx, tmbTy + 2 * tmbUh + 6, tmbUw, tmbUh, "Follow the tempo of the REAPER project (also when it changes)");
    );
    !tmbS ? (
    // row 4: knobs
    roomy = wd && x0 + w0 - lx - 10 >= 564;   // room for the status text right of the knobs (else: under them)
    kscale(min(kfit(secx(14) * 0.4), (wd ? (x0 + w0 - lx - 10 - (roomy ? 260 : 0)) / 4 : (w0 - 40) / 4.4) / (2 * KR0 + 14)));
    ksp = 2 * KR + 26; by = (wd ? y0 + 156 : y0 + 186) + KR; kx = (wd ? lx : x0 + 10) + ksp / 2 - 4; kcol = cMOTT; kdim = 0;   // narrow - the knobs 14 px higher so the status text stays inside the card
    sprintf(#fv, "%.2f m", slider104 * rs); kmap(0, 0.1 * rs, 20 * rs, 1);
    nf = knob(80, kx, by, "Size", #fv, lin2f(slider104 * rs, 0.1 * rs, 20 * rs), lin2f(2, 0.1, 20));
    knobtip(kx, by, "Size: the radius of the circle, half the side of the square, half the swing of a line, half the length of a pass-by (meters). It scales with the room's SIZE knob and the corner drag, like the positions; the path stays inside the room");
    kchg ? setp(104, f2lin(nf, 0.1, 20));
    kx += ksp; kdim = mtq > 0;
    mtDrw = pat == 7 && MTDP[513] > 0 && mtq == 0;   // a drawn path - the speed relative to the drawing (1.00× = as drawn)
    mtq > 0 ? strcpy(#fv, mttempo(mtq)) : ( mtDrw ? sprintf(#fv, "%.2f×", slider103 * MTDP[513]) : ( pat == 4 ? sprintf(#fv, "%.1f s", 1 / slider103) : sprintf(#fv, slider103 < 1 ? "%.3f" : "%.2f", slider103); ); ); mtDur = pat == 4 && mtq == 0; mtDrw ? kmap(1, 0.1, 10, 1) : ( mtDur ? kmap(1, 0.1, 50, 1) : kmap(1, 0.02, 10, 1); );   // the ranges of the knob scales (typed 1 Hz gave 1.96 Hz) // (Duration: type seconds; Speed: type the factor)
// Duration turns the other way - up = longer (slower)
// SPEED of a drawn path: its own scale 0.1× … 10× around 1.00× (the middle = exactly the drawn speed; it snaps there, double-click = 1.00×)
    mtSpd = mtDrw ? slider103 * MTDP[513] : 1;
    nf = mtDrw ? knob(81, kx, by, "Speed", #fv, min(1, max(0, log(max(0.1, mtSpd) / 0.1) / log(100))), 0.5)
               : knob(81, kx, by, mtDur ? "Duration" : "Rate Hz", #fv, mtDur ? 1 - log(slider103 / 0.02) / log(500) : log(slider103 / 0.02) / log(500), mtDur ? 1 - log(0.25 / 0.02) / log(500) : log(0.25 / 0.02) / log(500));
    knobtip(kx, by, mtq > 0 ? "Rate: locked to the tempo (switch Rate to Hz to set it freely)" : mtDrw ? "Speed of the drawn path: 1.00× = exactly as you drew it (fast and slow parts kept), 2.00× = twice as fast" : (pat == 4 ? "Duration of the pass-by (A to B) in seconds – up = longer, slower (double-click = type the seconds)" : "Rate in Hz: 0.02 … 10 turns or swings per second (double-click = type a value)"));
    kchg && mtq == 0 ? ( mtDrw ? ( mtSpd = 0.1 * 100 ^ nf; abs(mtSpd - 1) < 0.03 ? mtSpd = 1; setp(103, min(10, max(0.02, mtSpd / MTDP[513]))); ) : setp(103, 0.02 * 500 ^ (mtDur ? 1 - nf : nf)); );
    kx += ksp; kdim = 0;
    sprintf(#fv, "%.0f°", slider105); kmap(0, 0, 360, 1);
    nf = knob(82, kx, by, "Start", #fv, slider105 / 360, 0);
    knobtip(kx, by, "Start: where on the path the pattern begins at the start of the project (0° = to the right of the center); Pass-by: the angle of the line (0° = left to right, 90° = front to back)");
    kchg ? setp(105, nf * 360);
    kx += ksp;
    sprintf(#fv, "%.1f s", slider109); kmap(0, 0, 5, 1);
    nf = knob(83, kx, by, "Glide", #fv, slider109 / 5, 0.1);
    knobtip(kx, by, "Glide: how long the pattern takes to fade in when Motion is switched on and to fade out when it is switched off (0 = at once)");
    kchg ? setp(109, nf * 5);
    kx += ksp; kcol = cTEXT; kscale(1);
    roomy ? ( stx = kx - KR; sty = by; ) : ( stx = wd ? lx : x0 + 12; sty = by + KR + 38; );
    mtStyG = sty;   // (for the Motion-card test)
    // status: what the pattern does, in words
    // sec = seconds per cycle: tempo-locked (mtq 1 … 8 = 8, 4, 2, 1 bars, 1/2, 1/4, 1/8, 1/16 note) or 1 / Rate
    sec = mtq > 0 ? 60 / max(20, min(300, slider74 > 0.5 ? slider74 : tempo)) * max(0.0625, (mtq == 1 ? 8 * ts_num : (mtq == 2 ? 4 * ts_num : (mtq == 3 ? 2 * ts_num : (mtq == 4 ? ts_num : (mtq == 5 ? 2 : (mtq == 6 ? 1 : (mtq == 7 ? 0.5 : 0.25))))))) * 4 / max(1, ts_denom)) : 1 / slider103;   // the real time signature, as the audio counts it
// does the path leave the room? (then the ball waits at the wall until the path comes back in)
    rsz = slider104 * rs; clip = 0;
    mvs != 1 ? ( cx0 = (pat == 0 && ctr == 0 ? ganLX : (pat == 0 && ctr == 2 ? max(0.8, slider2) / 2 : ganSX)) * rs; cy0 = (pat == 0 && ctr == 0 ? ganLY : (pat == 0 && ctr == 2 ? max(0.8, slider3) / 2 : ganSY)) * rs;
      mtEg > 0.001 ? ( cx0 = (ctr == 0 && pat == 0 ? mtC1x : (ctr == 2 && pat == 0 ? max(0.8, slider2) / 2 : mtC0x)) * rs; cy0 = (ctr == 0 && pat == 0 ? mtC1y : (ctr == 2 && pat == 0 ? max(0.8, slider3) / 2 : mtC0y)) * rs; );
      pat == 5 ? ( cx0 = (ctr == 0 ? (mtEg > 0.001 ? mtC1x : ganLX) : (ctr == 2 ? max(0.8, slider2) / 2 : (mtEg > 0.001 ? mtC0x : ganSX))) * rs; cy0 = (ctr == 0 ? (mtEg > 0.001 ? mtC1y : ganLY) : (ctr == 2 ? max(0.8, slider3) / 2 : (mtEg > 0.001 ? mtC0y : ganSY))) * rs; );
      mtBx0 = -1; mtBx1 = 1; mtBy0 = pat == 6 ? -0.35 : -1; mtBy1 = -mtBy0;   // the extent of the shape (Drawn: from the path)
      pat == 7 && MTDP[512] >= 256 ? ( mtBx0 = mtBy0 = 1; mtBx1 = mtBy1 = -1; k = 0; loop(256, mtBx0 = min(mtBx0, MTDP[2 * k]); mtBx1 = max(mtBx1, MTDP[2 * k]); mtBy0 = min(mtBy0, MTDP[2 * k + 1]); mtBy1 = max(mtBy1, MTDP[2 * k + 1]); k += 1; ); );
      ctr == 1 && MTAP[2] >= 0.5 && (pat == 0 || pat == 5) ? ( cx0 = MTAP[0] * rs; cy0 = MTAP[1] * rs; );
      pat >= 5 ? ( cx0 + rsz * mtBx0 < 0 || cx0 + rsz * mtBx1 > roomW || cy0 + rsz * mtBy0 < 0 || cy0 + rsz * mtBy1 > roomD ? clip = 1; );
      pat == 0 || pat == 1 ? ( cx0 - rsz < 0 || cx0 + rsz > roomW ? clip = 1; ); pat == 0 || pat == 2 ? ( cy0 - rsz < 0 || cy0 + rsz > roomD ? clip = 1; );
      pat == 4 ? ( mtPdx = cos(slider105 * $pi / 180) * (slider106 >= 0.5 ? -1 : 1); mtPdy = sin(slider105 * $pi / 180) * (slider106 >= 0.5 ? -1 : 1);   // from the ball, 2 x Size
        cx0 + 2 * rsz * mtPdx < 0 || cx0 + 2 * rsz * mtPdx > roomW || cy0 + 2 * rsz * mtPdy < 0 || cy0 + 2 * rsz * mtPdy > roomD ? clip = 1; );
      pat == 3 ? ( (mtEg > 0.001 ? mtC0z : slider12) - rsz < 0.1 || (mtEg > 0.001 ? mtC0z : slider12) + rsz > roomH - 0.1 ? clip = 1; );
      // the largest Size that stays inside (meters) - for the Fit path chip
      fz0 = mtEg > 0.001 ? mtC0z : slider12;
      fitr = pat >= 5 ? min(min(mtBx0 < -0.01 ? cx0 / -mtBx0 : 1000, mtBx1 > 0.01 ? (roomW - cx0) / mtBx1 : 1000), min(mtBy0 < -0.01 ? cy0 / -mtBy0 : 1000, mtBy1 > 0.01 ? (roomD - cy0) / mtBy1 : 1000)) :
             pat == 0 ? min(min(cx0, roomW - cx0), min(cy0, roomD - cy0)) : (pat == 1 ? min(cx0, roomW - cx0) : (pat == 2 ? min(cy0, roomD - cy0) : (pat == 3 ? min(fz0 - 0.1, roomH - 0.1 - fz0) :
             min(abs(mtPdx) > 0.001 ? (mtPdx > 0 ? roomW - cx0 : cx0) / abs(mtPdx) / 2 : 1000, abs(mtPdy) > 0.001 ? (mtPdy > 0 ? roomD - cy0 : cy0) / abs(mtPdy) / 2 : 1000))));   // pass-by from the ball
// the center moves in from the walls by itself (not a circle / square around the listener) - only a path larger
      // than the room is cut; Fit path = the largest Size that fits the room
      !(ctr == 0 && (pat == 0 || pat == 5)) ? (
        mtExW = pat == 4 ? 2 * abs(cos(slider105 * $pi / 180)) : ((pat == 2 || pat == 3) ? 0 : (pat == 7 ? mtBx1 - mtBx0 : 2));
        mtExD = pat == 4 ? 2 * abs(sin(slider105 * $pi / 180)) : ((pat == 1 || pat == 3) ? 0 : (pat == 7 ? mtBy1 - mtBy0 : (pat == 6 ? 0.7 : 2)));
        clip = rsz * mtExW > roomW - 0.2 || rsz * mtExD > roomD - 0.2 || (pat == 3 && 2 * rsz > roomH - 0.2);
        fitr = min(mtExW > 0.001 ? (roomW - 0.2) / mtExW : 1000, mtExD > 0.001 ? (roomD - 0.2) / mtExD : 1000); pat == 3 ? fitr = (roomH - 0.2) / 2;
      );
    );
    pat == 4 ? ( mtq > 0 ? sprintf(#w1, "Pass-by · %s · %.1f m in %.1f s = %.1f m/s (%.0f km/h)", mttempo(mtq), 2 * slider104 * rs, sec, 2 * slider104 * rs / sec, 7.2 * slider104 * rs / sec) : sprintf(#w1, "Pass-by · %.1f m in %.1f s = %.1f m/s (%.0f km/h)", 2 * slider104 * rs, sec, 2 * slider104 * rs / sec, 7.2 * slider104 * rs / sec); ) : (
    mtq > 0 ? sprintf(#w1, "%s · %s · %.1f s per cycle", mtname(pat), mttempo(mtq), sec) : sprintf(#w1, "%s · %.2f Hz · %.1f s per cycle", mtname(pat), slider103, sec); );
    col(cTEXT, 1); gfx_setfont(6); txtfit(#w1, stx, sty - 10, x0 + w0 - 10 - stx);
    gfx_setfont(7);
    mtEnv ? ( col(0xD9534F, 1); txtfit("An automation envelope (Source / Listener X, Y) moves the ball – the motion cannot run: switch the envelope off or delete it", stx, sty + 6, x0 + w0 - 10 - stx); ) :
    clip ? ( col(cMOTT, 1); txtfit(pat == 4 ? "Line longer than the room – it is cut to the room, the ball waits at its ends" : "Path larger than the room – the ball waits at the wall until it comes back in", stx, sty + 6, x0 + w0 - 10 - stx - 66);
      chip(x0 + w0 - 70, sty - 1, 60, 14, "Fit path", 0, cMOTT) ? ( setp(104, max(0.1, fitr - 0.02) / rs); );   // Size down to what fits
      tipbox(x0 + w0 - 70, sty - 1, 60, 14, "Fit path: sets Size to the largest value that keeps the whole path inside the room"); )
         : ( col(cMUTED, 1); txtfit(pat == 4 ? "Starts again with every playback start and loop; free-running while stopped" : "Starts from its beginning at every playback start and with Start / Restart; loops run on smoothly", stx, sty + 6, x0 + w0 - 10 - stx); );
    tipbox(stx, sty - 18, x0 + w0 - 10 - stx, 36, "The positions are written to Source / Listener X, Y (and height) – record them in REAPER with Write or Latch; a read envelope on them overrides the pattern. Drag a moving ball = move its whole path");
    );   // end of: knobs and status (not while the tempo tools are open)
  );
);
// ---- REFLECTOR EDITOR (free field): the Walls module shows two reflectors instead of the room walls (rflset: above the tab row) ----
// Reflector panel: selects R1/R2, switches them on/off and edits their size, position and material
function gfxrfl() local(o, gk, ksp, rw, rd, kx, ky, nf, mm, av, lo, hi, df)
(
  o = RFB + rflSel * 6; mm = floor(vopt[o] + 0.5);
  rw = max(0.8, slider2) * min(4, max(0.25, slider31)); rd = max(0.8, slider3) * min(4, max(0.25, slider31));
// a click on R1 / R2 selects it and switches it on / off (it comes back with the material it had); the power button: both
  chip(x0 + 10, syW + 28, 30, 16, "R1", rflSel == 0, cWALTT) ? ( rflSel = 0; vopt[RFB] >= 0.5 ? rflset(0, 0) : rflset(0, rflLM[0] >= 0.5 ? rflLM[0] : 1); );
  chip(x0 + 44, syW + 28, 30, 16, "R2", rflSel == 1, cWALTT) ? ( rflSel = 1; vopt[RFB + 6] >= 0.5 ? rflset(1, 0) : rflset(1, rflLM[1] >= 0.5 ? rflLM[1] : 1); );
  tgl(x0 + w0 - 36, syW + 5, vopt[RFB + 14] < 0.5, cWALTT) ? ( vopt[RFB + 14] = vopt[RFB + 14] >= 0.5 ? 0 : 1; );
  tipbox(x0 + w0 - 36, syW + 5, 26, 14, "Reflectors on / off (both) – the settings stay");
  vopt[RFB] >= 0.5 ? ( col(cWALTT, 1); CIF(x0 + 35, syW + 36, 1.6, 1); ); vopt[RFB + 6] >= 0.5 ? ( col(cWALTT, 1); CIF(x0 + 69, syW + 36, 1.6, 1); );
  tipbox(x0 + 10, syW + 28, 64, 16, "R1 / R2: click = this reflector on / off (and select it for the knobs). Two freely placed vertical planes (facade, rock face, fence, hedge) – each one adds a real echo. A dot = on");
  segtrack(x0 + 82, syW + 28, w0 - 92, 16);
  gk = 0;
  loop(6,
    segp(x0 + 82, syW + 28, w0 - 92, 16, 6, gk, rflName(gk), mm == gk, cTEXT) ? ( rflset(rflSel, gk); mm = gk; );
    gk += 1;
  );
  tipbox(x0 + 82, syW + 28, w0 - 92, 16, "Material: Facade (brick, stone), Glass, Rock, Fence (reflects little), Hedge (soft) – Off removes the reflector");
// the knobs as large as those of the Free field tab (the same rule: nine places over its width, its extra height) - only smaller when this card is too low
  ksp = (w0 - 20) / 6; kscale(min(min(((LW - 40) / 9) / (2 * KR0 + 22), kfit(secx(1) / 4)), (shW - 96) / (2 * KR0 + 36))); kcol = cWALTT;
  ky = syW + 56 + KR + min(10, max(0, (shW - 56 - 2 * KR - 30) / 2));   // under the material row, as in the Free field tab (no longer centered in a tall card)
  gk = 0;
  loop(6,
    kx = x0 + 10 + ksp * (gk + 0.5);
    lo = gk == 3 ? 0.5 : gk == 4 ? 1 : gk == 5 ? -12 : 0; hi = gk == 0 ? rw * 1.25 : gk == 1 ? rd * 1.25 : gk == 2 ? 180 : gk == 3 ? max(10, 4 * max(rw, rd)) : gk == 5 ? 24 : 40;
    av = gk == 5 ? vopt[RFB + 12 + rflSel] : vopt[o + 1 + gk]; df = gk == 0 ? rw * 0.8 : gk == 1 ? rd * 0.5 : gk == 2 ? 90 : gk == 3 ? rd * 1.1 : gk == 5 ? 12 : 4;
    kdim = mm < 1 || vopt[RFB + 14] >= 0.5; gk == 2 ? sprintf(#fv, "%.0f°", av) : (gk == 5 ? sprintf(#fv, "%+.0f dB", av) : sprintf(#fv, "%.1f m", av));
    kmap(0, lo, hi, 1);
    nf = knob(330 + gk, kx, ky, gk == 0 ? "X" : gk == 1 ? "Y" : gk == 2 ? "Angle" : gk == 3 ? "Length" : gk == 4 ? "Height" : "Level", #fv, lin2f(av, lo, hi), lin2f(df, lo, hi));
    knobtip(kx, ky, gk < 2 ? "Position of the reflector's center in the free-field area (also: drag its handle in the Stage View)" : gk == 2 ? "Direction of the wall line seen from above: 0° = along the width, 90° = along the depth" : gk == 3 ? "Length of the reflector – the echo only exists where the sound hits it" : gk == 5 ? "Level of this echo in dB. 0 = physically exact (a facade 10 m away is 20 dB below the direct sound – hardly audible); +12 = film default, clearly audible; up to +24" : "Height of the reflector – a low fence reflects only low sound paths");
    mm >= 1 && kchg ? ( gk == 5 ? vopt[RFB + 12 + rflSel] = floor(f2lin(nf, lo, hi) + 0.5) : vopt[o + 1 + gk] = f2lin(nf, lo, hi); );
    kdim = 0; gk += 1;
  );
  kcol = cTEXT; kscale(1);
);
// Expert right column: Floor + ceiling, Barrier, Filter (low/high cut with live spectrum) and Output sections
function gfxp9()
(
(!szoom || szoom == 4) ? (
card(x0, syW, w0, shW); sechdr(4, isfree ? "Reflectors" : "Walls", x0, syW, cWALTT);
isfree && !basic && !learn ? (   // reflector setups
  gfx_setfont(2); rsuX = x0 + 28 + tws("Reflectors") + (tabm ? 40 : 58); rsuY = syW + 4; rsuW = min(210, max(110, w0 - (rsuX - x0) - 70));   // (All: after the magnifier and the reset arrow; rsu = its own names - rpy is the audio thread's)
  rsuX + rsuW < x0 + w0 - 50 ? (
    rpk = rpcur(); ddbox(rsuX, rsuY, rsuW, 16, rpk >= 0 ? rpname(rpk) : (rpLast >= 0 ? "Custom" : "Setups"), 1, rpmenu);
    click && inbox(rsuX, rsuY, rsuW, 16) ? ( rpmenu = 1; click = 0; );
    tipbox(rsuX, rsuY, rsuW, 16, "Reflector setups: a facade behind you, a street canyon, an alley, a shop window, a rock face … placed around the listener and the source as they stand now (the area and the ground stay)");
  );
);
!isfree ? (
col(cMUTED, 1); gfx_setfont(4); txt("Floor + ceiling", x0 + w0 - 42, syW + 12, 2);
tgl(x0 + w0 - 36, syW + 5, slider19 >= 0.5, cWALTT) ? ( slider19 = slider19 >= 0.5 ? 0 : 1; autom(262144); );
tipbox(x0 + w0 - 36, syW + 5, 26, 14, "Reflections from floor and ceiling on / off");
);
secopen(4) && isfree ? gfxrfl() :
secopen(4) ? (
  ddbox(wsx, wdy1, wdw, 15, wsurfname(selw), 1, wmenu == 1);
  tipbox(wsx, wdy1, wdw, 15, "Surface: which wall the α knob and the material are set for (also: click a wall or a bar below)");
  ddbox(wmx2, wdy2, wdw2, wdh, #wmt, 1, wmenu == 2);
  tipbox(wmx2, wdy2, wdw2, wdh, wtip(wmsel >= 0 ? wmattip(wmsel) : "Material of the chosen surface – sets its absorption α and the density of the tail", ta));
  tabm ? (
    wksp = (w0 - 40) / 6; kscale(min(2.4, min(wksp / (2 * KR0 + 22), (shW - 96) / (2 * KR0 + 36)))); kcol = cWALTT; ky = syW + 52 + KR + (shW - 52 - 2 * KR - 30) / 2;   // the knobs grow with the module (they sat small in a tall tab)
    gk = 0;
    loop(6,
      kx = x0 + 20 + wksp * (gk + 0.5); bsid = gk < 4 ? gk + 1 : 5; bav = gk == 0 ? aN : (gk == 1 ? aE : (gk == 2 ? aS : (gk == 3 ? aW : aF)));
      ben = !isfree && (gk < 4 || slider19 >= 0.5); bsel = selw == bsid || (selw == 0 && gk < 4);
      kdim = !ben; sprintf(#fv, "%.2f", bav);
      kmap(0, 0.01, 1, 1);
      nf = knob(320 + gk, kx, ky, gk == 0 ? "Back" : (gk == 1 ? "Right" : (gk == 2 ? "Front" : (gk == 3 ? "Left" : (gk == 4 ? "Floor" : "Ceiling")))), #fv, lin2f(bav, 0.01, 1), lin2f(gk < 4 ? 0.04 : 0.33, 0.01, 1));
      bsel && ben ? ( col(cWALTT, 1); RR(kx - 9, ky + KR + 17 * KNS, 18, 2.5, 1.2); );   // the chosen surface (material menu above)
      knobtip(kx, ky, wtip(gk < 4 ? "Absorption α of this wall: 0 = hard, 1 = absorbs all – turning it chooses this wall for the material menu" : "Absorption α of floor and ceiling (one value for both)", bav));
      ben && kchg ? ( selw = bsid; setalpha(f2lin(nf, 0.01, 1)); );
      ben && click && sqr(mx - kx) + sqr(my - ky) < sqr(KR + 5) ? ( selw = bsid; );
      kdim = 0; gk += 1;
    );
    kcol = cTEXT; kscale(1);
  ) : (
  gk = 0;
  loop(6,
    bx = x0 + 14 + gk * (w0 - 56) / 5; bsid = gk < 4 ? gk + 1 : 5; bav = gk == 0 ? aN : (gk == 1 ? aE : (gk == 2 ? aS : (gk == 3 ? aW : aF)));
    ben = !isfree && (gk < 4 || slider19 >= 0.5);
    bsel = selw == bsid || (selw == 0 && gk < 4);
    wbh = 34 + secx(4); bhh = wbh * min(1, max(0, bav));
    col(cLINE, 1); RR(bx, syW + 50, 28, wbh, 2);
    col(ben ? (gk < 4 ? cWALTT : cDIM2) : cDIM, 1); bhh > 0.5 ? RR(bx, syW + 50 + wbh - max(3, bhh), 28, max(3, bhh), 2);
    bsel && ben ? ( col(cTEXT, 0.55); RRO(bx - 1, syW + 49, 30, wbh + 2, 3); );
    col(cMUTED, 1); gfx_setfont(4); txt(gk == 0 ? "Back" : (gk == 1 ? "Right" : (gk == 2 ? "Front" : (gk == 3 ? "Left" : (gk == 4 ? "Floor" : "Ceil.")))), bx + 14, syW + 59 + wbh, 1);
    col(ben ? cTEXT : cDIM2, 1); gfx_setfont(2); sprintf(#w1, "%.2f", bav); txt(#w1, bx + 14, syW + 71 + wbh, 1);
    vedPend && vedId == 300 + gk ? ( selw = bsid; setalpha(min(1, max(0.01, vedVal))); vedPend = 0; );
    vedit == 300 + gk ? vedplace(bx + 14, syW + 71 + wbh, 44);
    ben && click && inbox(bx - 2, syW + 48, 32, wbh + 4) ? (
      lastkid == 300 + gk && time_precise() - lastkt < 0.5 ? ( selw = bsid; vedopen(300 + gk, #w1); lastkid = -1; wbdrag = 0; )
                                                           : ( selw = bsid; wbdrag = gk + 1; wbv0 = bav; wbmy0 = my; lastkid = 300 + gk; lastkt = time_precise(); );
    );
    wbdrag == gk + 1 ? ( selw = bsid; setalpha(min(1, max(0.01, wbv0 + (wbmy0 - my) / ((mouse_cap & 8) ? 4 * wbh : wbh)))); );
    tipbox(bx - 2, syW + 48, 32, wbh + 4, wtip("Absorption α of this surface – click = select, drag up / down = change (Shift = fine), double-click = type", bav));
    gk += 1;
  );
  );   // end of: classic bars
);
);
(!szoom || szoom == 5) ? (
// ---- BARRIER: type, element, back part, position / opening / damping, info ----
card(x0, syB, w0, shB); sechdr(5, "Barrier", x0, syB, cBARTT);
secopen(5) ? bartiles(x0 + 10, syB + 28, w0 - 20, 36 + bgx);
tgl(x0 + w0 - 36, syB + 5, btg > 0, cBARTT) ? bartog();
hmix(62, x0 + w0 - (tabm ? 132 : 112), syB + 12, 92, cBARTT, "Amount of the barrier (module mix): how much of its damping acts – 100 % = as set, 0 % = it lets all sound through (the room keeps its two parts and their reverb) (double-click = type a value, Alt+click = 100 %, automatable)");
tipbox(x0 + w0 - 36, syB + 5, 26, 14, btg > 0 ? "Barrier on – click: off (one open room), the type is remembered" : "Barrier off – click: on again (or choose Wall, Window or Door)");
secopen(5) ? (
  btg > 0 ? ( bmsel >= 0 ? strcpy(#w1, bmatname(btg, bmsel)) : sprintf(#w1, "Custom · %.0f dB", slider44); ) : strcpy(#w1, "Element –");
  ddbox(bdx, bdy, bdw, bdh, #w1, btg > 0, bmenu);
  btg > 0 ? tipbox(bdx, bdy, bdw, bdh, bmsel >= 0 ? bmattip(btg, bmsel) : "Custom damping – choose a material preset or set Damp freely");
  bkon = btg > 0 && !isfree;                                      // back part: only inside a room
  kmsel = floor(slider46 + 0.5);
  kmsel > 0 ? sprintf(#w1, "Back part: %s  α %.2f", bkname(kmsel), g_bkA(kmsel)) : sprintf(#w1, "Back part: %s", bkname(0));
  ddbox(bkbx, bky, bdw, 15, #w1, bkon, kmenu && kmsrc == 1);
  bkon ? tipbox(bkbx, bky, bdw, 15, "Material of the back part (back wall to barrier): own walls, floor and ceiling – e.g. a tiled bathroom next to a dry room");
  // the knobs always start under the menus (taller section: they grow, never into the menus)
  kscale(min(kfit(secx(5) * 0.4), (w0 - 150) / 5 / (2 * KR0 + 14)));
  bksp = 2 * KR + 14; by = bdy + bdh + 10 + KR; kx = x0 + 10 + bksp / 2; bkd = bksp;
  kcol = cBARTT;
  kdim = btg == 0;
  sprintf(#fv, "%.1f m", yb);
  kmap(0, ybm, roomD - ybm, 1);
  nf = knob(26, kx, by, "Pos", #fv, lin2f(yb, ybm, roomD - ybm), lin2f(roomD / 2, ybm, roomD - ybm));
  knobtip(kx, by, "Position: distance of the barrier from the back wall (also: drag its handle in the room)");
  !lockB && kchg ? setp(42, f2lin(nf, ybm, roomD - ybm) / rs);
  // SIZE of the door / window (50 … 200 %): the element grows - its opening and the sound through it follow (Wall: no element)
  kx += bkd; kdim = btg <= 1;
  btg >= 2 ? sprintf(#fv, "%.0f %%", slider97) : strcpy(#fv, "–");
  kmap(0, 50, 200, 1);
  nf = knob(70, kx, by, "Size", #fv, (slider97 - 50) / 150, 50 / 150);
  btg >= 2 ? sprintf(#bsztip, "Size of the %s: %.2f × %.2f m now (100 %% = %s). Larger = a bigger opening – more sound comes through and the two parts of the room are coupled more; smaller = less (double-click = 100 %%)", btg == 3 ? "door" : "window", bew, bez1 - bez0, btg == 3 ? "0.9 × 2.1 m" : "1.2 × 1.2 m")
           : strcpy(#bsztip, "Size of the door or window – choose Window or Door first (a wall has no opening)");
  knobtip(kx, by, #bsztip);
  !lockE && kchg ? setp(97, 50 + nf * 150);
  kx += bkd; kdim = btg == 0;
  bhf = brH >= roomH - 0.005 ? 1 : (brH - 0.3) / max(0.01, roomH - 0.3);
  bhf >= 0.999 ? strcpy(#fv, "full") : sprintf(#fv, "%.1f m", brH);
  kmap(0, 0.3, roomH, 1);
  nf = knob(71, kx, by, "Height", #fv, bhf, 1);
  knobtip(kx, by, "Height of the barrier: full = floor to ceiling, lower = sound passes over it and bends around its top edge (a door or window always stays inside it)");
  kchg ? setp(96, nf >= 0.995 ? 0 : (0.3 + nf * (roomH - 0.3)) / rs);
  kx += bkd; kdim = btg <= 1;
  btg >= 2 ? sprintf(#fv, "%.0f°", slider43 * 0.9) : strcpy(#fv, "–");
  kmap(0, 0, 90, 1);
  nf = knob(27, kx, by, "Open", #fv, slider43 / 100, 0);
  knobtip(kx, by, btg == 2 ? "Open: swing angle of the two window sashes – they open to the left and right like a casement (0° = closed, 90° = fully open; also: mouse wheel on the barrier in the plan)" : "Open: swing angle of the door leaf (0° = closed, 90° = fully open)");
  !lockE && kchg ? setp(43, nf * 100);
  kx += bkd; kdim = btg == 0;
  sprintf(#fv, "%.0f dB", slider44);
  kmap(0, 0, 70, 1);
  nf = knob(28, kx, by, "Damp", #fv, slider44 / 70, bmatrw(max(1, btg), 1) / 70);
  knobtip(kx, by, btg >= 2 ? (btg == 3 ? "Damping of the closed door at 500 Hz, as installed – the wall around it is brick, 45 dB. Bass passes more; an ordinary door (no seals) passes the highs through its gaps, an acoustic door with seals keeps them back" : "Damping of the closed window at 500 Hz, as installed – the wall around it is brick, 45 dB. Bass passes more, highs less (double / acoustic glazing more than single)") : "Damping of the wall at 500 Hz, as built – doubling its mass adds about 6 dB (mass law). Bass passes more, highs less");
  kchg ? setp(44, nf * 70);
  kdim = 0; kcol = cTEXT;
  kscale(1);
  bgx0 = x0 + 12 + 5 * bkd; bargraph(bgx0, by - KR, x0 + w0 - 10 - bgx0, 24); barinfo(bgx0, by - KR + 36, x0 + w0 - 10);
);
);
(!szoom || szoom == 14) ? (gfxpM());
// ---- FILTER: low / high cut with on-dot and slope, pre / post, bypass ----
(!szoom || szoom == 6) ? (
fbypd = slider39 >= 0.5;
x0 = flX; w0 = flW;
card(x0, syF, w0, shF); sechdr(6, "Filter", x0, syF, cFLTT);
secopen(6) ? (
  chip(x0 + 10, syF + 28, 34, 14, "Pre", !fpost, cFLTT) ? ( fpost ? ( slider30 = 0; autom(536870912); ); );
  chip(x0 + 47, syF + 28, 36, 14, "Post", fpost, cFLTT) ? ( !fpost ? ( slider30 = 1; autom(536870912); ); );
  tipbox(x0 + 10, syF + 28, 34, 14, "Pre: the signal is filtered before it enters the room");
  tipbox(x0 + 47, syF + 28, 36, 14, "Post: the finished room sound is filtered");
);
tgl(x0 + w0 - 36, syF + 5, !fbypd, cFLTT) ? ( slider39 = fbypd ? 0 : 1; sauto1(39); );
hmix(64, !tabm && w0 < 240 && secopen(6) ? x0 + 94 : x0 + w0 - (tabm ? 132 : 112), !tabm && w0 < 240 && secopen(6) ? syF + 35 : syF + 12, 88, cFLTT, "Amount of the filter (module mix): 100 % = filtered as set, 0 % = unfiltered, in between = both in parallel (double-click = type a value, Alt+click = 100 %, automatable)");
tipbox(x0 + w0 - 36, syF + 5, 26, 14, "Filter on / off (bypass, automatable): low cut and high cut together");
tabm && secopen(6) ? (
  // TABS: first row Pre / Post | low cut: dot, frequency, slope | high cut: dot, frequency, slope
  // under it the live spectrum (input gray, output line) with the filter curve and two handles: drag = frequency,
  // Alt+click = default (80 Hz / 12 kHz), wheel = slope, Shift+wheel = frequency
  gk = 0;
  loop(2,
    fon = gk == 0 ? lcon : hcon; fdx = gk == 0 ? lcbx - 106 : hcbx - 112;
    rbtn(fdx + 6, syF + 34.5, 5, "", fon && !fbypd, cFLTT, gk == 0 ? "Low cut on / off – removes rumble and low end" : "High cut on / off – removes hiss and brilliance") ? (
      gk == 0 ? ( slider24 = lcon ? 0 : 1; autom(8388608); ) : ( slider27 = hcon ? 0 : 1; autom(67108864); );
    );
    gk == 0 ? ( slider25 < 1000 ? sprintf(#w2, "Low cut %.0f Hz", slider25) : sprintf(#w2, "Low cut %.2f kHz", slider25 / 1000); )
            : ( sprintf(#w2, "High cut %.1f kHz", slider28 / 1000); );
    col(fon && !fbypd ? cTEXT : cDIM2, 1); gfx_setfont(6); txt(#w2, fdx + 16, syF + 34.5, 0);
    fi = floor((gk == 0 ? slider26 : slider29) + 0.5); sprintf(#w1, "%d dB", (fi + 1) * 6);
    ddbox(gk == 0 ? lcbx : hcbx, fcby, fcbw, fcbh, #w1, fon && !fbypd, fmenu == gk + 1);
    tipbox(gk == 0 ? lcbx : hcbx, fcby, fcbw, fcbh, gk == 0 ? "Low cut steepness in dB per octave – click to choose 6 … 48" : "High cut steepness in dB per octave – click to choose 6 … 48");
    gk += 1;
  );
  flsx = x0 + 12; flsw = w0 - 24; flsy = syF + 50; flsh = shF - (tabm ? 72 : 60);
  time_precise() - spT > 0.035 ? ( spT = time_precise(); (time_precise() - lastblk > 0.3) ? ( spk = 0; loop(SPN, spBI[spk] = max(-120, spBI[spk] - 3); spBO[spk] = max(-120, spBO[spk] - 3); spk += 1; ); ) : spcalc(); );
  spdraw(flsx, flsy, flsw, flsh);
  // the filter curve: 0 dB high in the box, -48 dB at the bottom (Butterworth of the chosen order = 6 dB/oct per order)
  fn1 = floor(slider26 + 0.5) + 1; fn2 = floor(slider29 + 0.5) + 1; fry0 = flsy + flsh * 0.16; frys = flsh * 0.8 / 48;
  col(cFLTT, fbypd ? 0.35 : 1); i = 0; flcx = 0; flcy = 0;   // (own names - lx / ly are the listener's position in the audio thread)
  fcn = min(4096, max(2, floor(flsw * gs)));   // one value per screen pixel
  // one point per pixel on a log axis 31.5 Hz … 16 kHz: Butterworth magnitude squared, 1 / (1 + (fc / f) ^ 2n) for low cut and (f / fc) ^ 2n for high cut, in dB (floor -48)
  loop(fcn,
    ff = 31.5 * (16000 / 31.5) ^ (i / (fcn - 1)); g2 = 1;
    lcon ? ( g2 *= 1 / (1 + (slider25 / ff) ^ (2 * fn1)); ); hcon ? ( g2 *= 1 / (1 + (ff / slider28) ^ (2 * fn2)); );
    gdb = max(-48, 10 * log10(max(0.000000001, g2))); cvY[i] = goy + (fry0 - gdb * frys) * gs; i += 1;
  );
  cvfill(floor(gox + flsx * gs + 0.5), fcn, floor(goy + (flsy + flsh - 1) * gs + 0.5), cFLTT, fbypd ? 0.06 : 0.22, floor(goy + flsy * gs));
  cvline(floor(gox + flsx * gs + 0.5), fcn, cFLTT, fbypd ? 0.35 : 1, 2);
  col(cDIM2, 1); gfx_setfont(7); txt("0 dB", flsx + 4, fry0 - 6, 0);
  // the two handles at the cut frequencies (on the curve, -3 dB)
  gk = 0;
  loop(2,
    fon = gk == 0 ? lcon : hcon; fq = gk == 0 ? slider25 : slider28;
    hx = flsx + min(1, max(0, log(fq / 31.5) / log(16000 / 31.5))) * flsw; hy = fry0 + 3 * frys;
    fhit = sqr(mx - hx) + sqr(my - hy) < 81;
    col(cSURF, 1); CI(hx, hy, 6, 1); col(fon && !fbypd ? cFLTT : cDIM2, 1); CI(hx, hy, 6, 0); CI(hx, hy, 5.2, 0); fhit || fhdrag == gk + 1 ? CI(hx, hy, 2.5, 1);
    gk == 0 ? ( kmap(1, 20, 2000, 1); fvid = 8; slider25 < 1000 ? sprintf(#fvs, "%.0f", slider25) : sprintf(#fvs, "%.2fk", slider25 / 1000); )
            : ( kmap(1, 1000, 20000, 1); fvid = 10; sprintf(#fvs, "%.2fk", slider28 / 1000); );
    fhdbl = click && fhit && time_precise() - (gk == 0 ? fhdt0 : fhdt1) < 0.35; click && fhit ? ( gk == 0 ? fhdt0 = time_precise() : fhdt1 = time_precise(); );
    (((click && (mouse_cap & 36)) || rclick || fhdbl) && fhit) ? ( vedopen(fvid, #fvs); click = 0; rclick = 0; fhdrag = 0; );   // type the frequency
    vedPend && vedId == fvid ? ( ff = vedmap(vedVal); gk == 0 ? setp(25, f2log(ff, 20, 2000)) : setp(28, f2log(ff, 1000, 20000)); vedPend = 0; );
    vedit == fvid ? vedplace(hx - 24, hy - 30, 48);
    click && fhit ? ( (mouse_cap & 16) ? ( setp(gk == 0 ? 25 : 28, gk == 0 ? 80 : 12000); ) : ( fhdrag = gk + 1; ); click = 0; );
    fhdrag == gk + 1 ? (
      (mouse_cap & 1) ? ( ff = 31.5 * (16000 / 31.5) ^ min(1, max(0, (mx - flsx) / flsw));
                          gk == 0 ? setp(25, min(2000, max(20, ff))) : setp(28, min(20000, max(1000, ff))); )
                      : ( fhdrag = 0; );
    );
// wheel = slope (one notch = 6 dB/oct, up = steeper, 6 … 48), Shift+wheel = frequency (1/12 octave per notch)
    fhit && mouse_wheel != 0 && !(mouse_cap & 1) ? (
      (mouse_cap & 8) ? ( ff = fq * 2 ^ (mouse_wheel / 120 / 12); gk == 0 ? setp(25, min(2000, max(20, ff))) : setp(28, min(20000, max(1000, ff))); )
                      : ( fsl = floor((gk == 0 ? slider26 : slider29) + 0.5) + (mouse_wheel > 0 ? 1 : -1); setp(gk == 0 ? 26 : 29, min(7, max(0, fsl))); );
      wheelused = 1; mouse_wheel = 0; );
    fhit ? ( tipon = 1; tipforce = 0; tipx = hx; tipy = hy - 14; fsl = (floor((gk == 0 ? slider26 : slider29) + 0.5) + 1) * 6;
      gk == 0 ? sprintf(#ftp, "Low cut %d dB/oct: drag = frequency · double-click = type it · wheel = slope · Shift+wheel = fine · Alt+click = 80 Hz", fsl)
              : sprintf(#ftp, "High cut %d dB/oct: drag = frequency · double-click = type it · wheel = slope · Shift+wheel = fine · Alt+click = 12 kHz", fsl); tipstr = #ftp; );
    gk += 1;
  );
);
!tabm && secopen(6) ? (
  col(cHAIR, 1); LN(x0 + w0 / 2, syF + 50, x0 + w0 / 2, syF + shF - 8);
  kscale(fks);
  gk = 0;
  loop(2,
    hx = x0 + gk * floor(w0 / 2); kx = fstk ? hx + fhw / 2 : hx + 52; ky = fky; fon = gk == 0 ? lcon : hcon;
    rbtn(hx + 16, ky, 5, "", fon && !fbypd, cFLTT, gk == 0 ? "Low cut on/off – removes rumble and low end" : "High cut on/off – removes hiss and brilliance") ? (
      gk == 0 ? ( slider24 = lcon ? 0 : 1; autom(8388608); ) : ( slider27 = hcon ? 0 : 1; autom(67108864); );
    );
    kdim = !fon || fbypd; kcol = cFLTT;
    gk == 0 ? (
      slider25 < 1000 ? sprintf(#fv, "%.0f", slider25) : sprintf(#fv, "%.2fk", slider25 / 1000);
      kmap(1, 20, 2000, 1);
      nf = knob(8, kx, ky, "Low cut Hz", #fv, log2f(slider25, 20, 2000), log2f(80, 20, 2000));
      knobtip(kx, ky, "Low cut frequency: sound below this frequency is reduced");
      kchg ? setp(25, f2log(nf, 20, 2000));
    ) : (
      sprintf(#fv, "%.1fk", slider28 / 1000);
      kmap(1, 1000, 20000, 1);
      nf = knob(10, kx, ky, "High cut Hz", #fv, log2f(slider28, 1000, 20000), log2f(12000, 1000, 20000));
      knobtip(kx, ky, "High cut frequency: sound above this frequency is reduced");
      kchg ? setp(28, f2log(nf, 1000, 20000));
    );
    fi = floor((gk == 0 ? slider26 : slider29) + 0.5); sprintf(#w1, "%d dB", (fi + 1) * 6);
    ddbox(gk == 0 ? lcbx : hcbx, fcby, fcbw, fcbh, #w1, fon && !fbypd, fmenu == gk + 1);
    tipbox(gk == 0 ? lcbx : hcbx, fcby, fcbw, fcbh, gk == 0 ? "Low cut steepness in dB per octave – click to choose 6 … 48" : "High cut steepness in dB per octave – click to choose 6 … 48");
    gk += 1;
  );
  kscale(1);
  kdim = 0; kcol = cTEXT;
);
);
// ---- OUTPUT ----
(!szoom || szoom == 7) ? drawio()
);
// ROOM GROUP: while the room engine is on, a thin line in the room color around the modules that belong to it -
// Room + Reverb, the room view, Walls and (when it is on) Barrier: they are one thing, set in four places
function actlines() (
  !szoom && !zoomv && !learn && !rbyp ? (
    col(cRMTT, 0.6);
    basic ? (
      RRO(sxR - 2, yS1 - 2, bwR + 4, hbR + 4, 10);
      btg > 0 ? RRO(sxB - 2, yS2 - 2, bwB + 4, hbB + 4, 10);
    ) : (
      RRO(L0 - 2, syR - 2, LW + 4, shR + 4, 10);
      RRO(R0 - 2, syW - 2, RWc + 4, shW + 4, 10);
      btg > 0 ? RRO(R0 - 2, syB - 2, RWc + 4, shB + 4, 10);
    );
    RRO(VX - 2, VY - 2, VW + 4, VH + 4, 10);
  );
);
// Draws the 'room off' veil over room-engine modules (keeping the On/Off switch usable) and the module group lines
function gfxp10()
(
// ROOM OFF: veil over everything that belongs to the room engine; the On / Off switch stays usable
rbyp && !szoom ? (
  col(cSURF, 0.62);
  secopen(1) ? R(L0 + 2, syR + 24, LW - 4, shR - 26);
  R(R0 + 2, syW + 22, RWc - 4, shW - 24); R(R0 + 2, syB + 2, RWc - 4, shB - 4);
);
actlines();
);
// Top bar: name, Basic/Expert and view mode switches, Tabs/All/Fit, room presets menu button and snapshot slots
function gfxp11()
(
gclp = 0;   // the clipping of the room view never reaches the top bar
col(cBG, 1); gfx_rect(0, 0, gfx_w, gfx_h);

szoom ? ( gs = gsN; gox = goxN; goy = goyN; WT = WTN; HT = HTN; setfonts(); mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs; );
// ---------- top bar ----------
col(cTEXT, 1); gfx_setfont(5, "Arial", 12 * fgs, 'b'); txt("Roomservice", 11, 17, 0); gfx_setfont(5, "Arial", FS5, 'b');   // the name a little smaller: it no longer touches the Basic | Expert | Learn switch
// Basic | Expert | Learn: one segmented switch, the active segment filled
// VIEW MODE: a dark switch right after the name (the plugin's own view - not a preset, not a snapshot)
col(cTEXT, 1); RR(98, 8, 116, 18, 9);
gk = 0;
loop(3,
  mdon = gk == 0 ? basic : (gk == 1 ? !basic && !learn : learn); mdh = inbox(98 + gk * 38, 8, 38, 18);
  mdon ? ( col(cACT, 1); RR(100 + gk * 38, 10, 36, 14, 7); cCTLon ? ( col(cACTO, 1); RRO(100 + gk * 38, 10, 36, 14, 7); ); col(cACTX, 1); ) : col(mdh ? cSURF : cDIM2, 1);
  gfx_setfont(6); txt(gk == 0 ? "Basic" : (gk == 1 ? "Expert" : "Learn"), 118 + gk * 38, 17, 1);
  click && mdh ? ( vopt[11] = gk == 0; vopt[43] = gk == 2; szclr = 1; );   // the zoom is cleared at the start of the next frame (clearing it here drew every module on top of each other for one frame = a flash)
  gk += 1;
);
tipbox(98, 8, 38, 18, "Basic view (B): room, decay, source and barrier at a glance – every other setting stays as it is");
tipbox(136, 8, 38, 18, "Expert view (E): all controls, every module at once");
tipbox(174, 8, 38, 18, "Learn (L): the plugin explains what you hear – labels in the room, the sound in three parts, short tasks and the words in plain language (the sound is the same)");
// TABS | ALL | FIT (Expert): under the view modes - toggles between the tab view and every module at once. Fit = the module as large as the window allows (Tabs) / every module fitted into the window (All)
!basic && !learn ? (
  mvfit = tabm ? vopt[48] >= 0.5 : vopt[42] >= 0.5;
  col(cHAIR, 1); RR(98, 31, 116, 12, 6);
  gk = 0;
  loop(3,
    mvx = 98 + (gk == 0 ? 0 : (gk == 1 ? 40 : 78)); mvw = gk == 0 ? 40 : 38; mvh2 = inbox(mvx, 31, mvw, 12);
    mvon = gk == 0 ? tabm : (gk == 1 ? !tabm : mvfit);
    mvon ? ( col(cACT, 1); RR(mvx + 1, 32, mvw - 2, 10, 5); cCTLon ? ( col(cACTO, 1); RRO(mvx + 1, 32, mvw - 2, 10, 5); ); col(cACTX, 1); ) : col(mvh2 ? cTEXT : cMUTED, 1);
    gfx_setfont(7); txt(gk == 0 ? "Tabs" : (gk == 1 ? "All" : "Fit"), mvx + mvw / 2, 37.5, 1);
    click && mvh2 ? (
      gk == 0 ? ( vopt[45] = 0; szclr = 1; ) : ( gk == 1 ? ( vopt[45] = 1; szclr = 1; ) : (
        tabm ? ( vopt[48] = mvfit ? 0 : 1; vopt[35] = 0; ) : ( vopt[42] = mvfit ? 0 : 1; !mvfit && !vlock ? ( memset(vopt + 21, 0, 10); vopt[33] = 0; ); scy = 0; ); ); );
      click = 0;
    );
    gk += 1;
  );
  tipbox(98, 31, 40, 12, "Tabs: the Stage View on top and one module under it – larger, no scrolling (key T · key 0 switches Tabs <-> All · key Z zooms the module)");
  tipbox(138, 31, 38, 12, "All: every module at once in two columns (key A · key 0 switches Tabs <-> All · key Z zooms the module under the mouse)");
  tipbox(176, 31, 38, 12, tabm ? (mvfit ? "Fit on (key F): the module fills the width, the Stage View gets the rest – click: standard" : "Fit (key F): make the module as large as the window allows – text, knobs and graphics grow, the Stage View gets smaller")
                               : (mvfit ? "Fit all (key F): every module in the window at once – smaller text. Click: text and knobs as large as in Basic (the page scrolls with the mouse wheel)" : "Fit all: fit every module into the window (smaller) – module heights you dragged go back to standard"));
);
col(cBORD, 1); LNW(219, 7, 219, 27, 1);                                // separator: the plugin's view | what you load
col(cMUTED, 1); gfx_setfont(11); txt("Presets", roombx, 5, 0);
// room presets: dropdown (menu drawn at the end, on top)
rsel = -1; k = 0; loop(NPRE, !isfree && roommatch(k) ? rsel = k; k += 1; );
col(cSURF, 1); RR(roombx, roomby, roombw, roombh, 8); col(rmenu ? cMUTED : (inbox(roombx, roomby, roombw, roombh) ? cDIM2 : cBORD), 1); RRO(roombx, roomby, roombw, roombh, 8);
col(isfree ? cMUTED : cRMTT, 1); RR(roombx + 7, roomby + roombh / 2 - 5, 3, 10, 1.5);   // color mark of the room
// preset changed by hand (decay, levels, tone, predelay, density, floor / ceiling, barrier): name + " *", the time is the one you hear
rmod = 0; rsel >= 0 ? ( pb0 = PT + rsel * PRS;
  rmod = abs(slider32 - 1) > 0.001 || abs(slider62 - 100) > 0.05 || abs(slider20 - pb0[17]) > 0.05 || abs(slider21 - pb0[18]) > 0.05 || abs(slider33 - pb0[16]) > 0.5
      || (vopt[8] < 0.5 && abs(slider34 - pb0[15]) > 0.5) || abs(slider38 - pb0[19]) > 0.5 || slider19 < 0.5 || slider41 >= 0.5; );   // (predelay following the room: not a change)
urs = -1; rsel < 0 && !isfree ? ( i = 0; loop(URM, urs < 0 && urmatch(i) ? urs = i; i += 1; ); );
isfree ? ( strcpy(#rnm, ffpname(ffpLast)); ffbm = FFP + ffpLast * 16; (abs(slider2 * slider31 - ffbm[0]) > 0.1 || abs(slider3 * slider31 - ffbm[1]) > 0.1 || slider119 != ffbm[3] || vopt[RFB] != ffbm[4] || vopt[RFB + 6] != ffbm[10]) ? strcat(#rnm, " *"); ) : ( rsel >= 0 ? ( strcpy(#rnm, roomname(rsel)); rmod ? strcat(#rnm, " *"); ) : ( urs >= 0 ? sn_strn(urb + urs * URL + 2, 24, #rnm) : strcpy(#rnm, "Custom") ) );
col(cTEXT, 1); gfx_setfont(2); txtfit(#rnm, roombx + 15, roomby + roombh / 2, roombw - 30);
chevd(roombx + roombw - 11, roomby + roombh / 2, cMUTED);
tipbox(roombx, roomby, roombw, roombh, rmod ? "Presets: room preset, changed (*): decay, levels, tone, predelay, density, floor / ceiling or a barrier differ from the preset – the time is the reverb time RT60 you hear now" : "Presets: typical rooms – the time is the reverb time RT60, calculated from size and materials");
// snapshots 1-16 (NSNAP; two rows of 8, Basic shows 8): click = load, double-click = name + store, Save then slot = store
// a single click loads only when no second click follows (otherwise the double-click would store the loaded settings)
snPend && time_precise() - snPt >= SNDBL ? ( loadslot(snPend - 1); uslot[(snPend - 1) * USL] ? ( snlast = snPend; ); snPend = 0; );
snmk = 0;
col(cMUTED, 1); gfx_setfont(basic ? 7 : 11); txt("Snapshots", 346, basic ? 6 : 5, 0);
snWd = basic ? 18 : 15; snHt = basic ? 14 : 13;           // size of one snapshot button
gk = 0;
loop(basic ? 8 : NSNAP,
  snx = snslx(gk); sny = snsly(gk); snm = slotmatch(gk); snh = inbox(snx, sny, snWd, snHt);
  snm && snmk == 0 ? ( snmk = gk + 1; );                                            // active snapshot (shown in the 3D view)
  snm ? ( col(cTEXT, 1); RR(snx, sny, snWd, snHt, 3.5); col(cSURF, 1); )
      : ( col(snh ? cHAIR : cLINE, 1); RR(snx, sny, snWd, snHt, 3.5); saveArm ? ( col(cIOTT, 1); RRO(snx, sny, snWd, snHt, 3.5); ); col(uslot[gk * USL] ? cTEXT : cDIM2, 1); );
  sprintf(#ul, "%d", gk + 1); gfx_setfont(basic ? 6 : 12); txt(#ul, snx + snWd / 2, sny + snHt / 2, 1);       // empty slots: gray number
  uslot[gk * USL] && !snm ? ( col(cRMTT, 1); CI(snx + snWd - 2, sny + 2.5, basic ? 1.4 : 1, 1); );     // filled: a blue dot
  snflk == gk && time_precise() - snflash < 0.6 ? ( col(cRMTT, 1); RRO(snx - 1, sny - 1, snWd + 2, snHt + 2, 4); );   // stored: a blue ring for a moment
  saveArm && snconf == gk + 1 && time_precise() - snconfT < 4 ? ( col(cAMB, 1); RRO(snx - 1.5, sny - 1.5, snWd + 3, snHt + 3, 4); RRO(snx - 1, sny - 1, snWd + 2, snHt + 2, 4);
    sprintf(#tipb, "Snapshot %d is in use – click it again to replace it", gk + 1); tipstr = #tipb; tipon = 1; tipforce = 1; tipx = snx + 7; tipy = 42; );   // replace: asks once
  rclick && snh ? ( snctx = gk + 1; snmS = gs; snqX = max(2, min(gox + snx * gs, gfx_w - 214 * gs)); snqY = goy + (basic ? sny + snHt + 4 : snsly(NSNAP - 1) + snHt + 4) * gs; rclick = 0; click = 0; snPend = 0; );   // menu: rename / overwrite / clear
  click && snh ? (
    saveArm && uslot[gk * USL] && !(snconf == gk + 1 && time_precise() - snconfT < 4) ? ( snconf = gk + 1; snconfT = time_precise(); ) :   // filled: click again to replace
    saveArm ? ( saveslot(gk); saveArm = 0; snlast = gk + 1; snconf = 0; ) : (
      snPend == gk + 1 && time_precise() - snPt < SNDBL ? ( snPend = 0; sn_open(gk); ) : ( snPend = gk + 1; snPt = time_precise(); );
    );
  );
  snh ? (
    sprintf(#tipb, "Snapshot %d", gk + 1);
    snam[gk * SNL] > 0 ? ( strcat(#tipb, " “"); strcat(#tipb, sn_str(snam + gk * SNL, #snt)); strcat(#tipb, "”"); );
    dsc[gk * DSL] > 0 ? ( strcat(#tipb, " – "); strcat(#tipb, sn_strn(dsc + gk * DSL, DSL, #snt)); );
    strcat(#tipb, saveArm ? ": click to store the current settings here" : (uslot[gk * USL] ? ": click = load, double-click = rename / store, right-click = rename / overwrite / clear" : ": empty – double-click = name it and store the current settings"));
    !(saveArm && snconf == gk + 1 && time_precise() - snconfT < 4) ? ( tipstr = #tipb; tipon = 1; tipx = snx + 7; tipy = 42; );
  );
  gk += 1;
);
!basic ? (
col(saveArm ? cIOTT : cMUTED, 1); gfx_setfont(2); txt("Save", 487, 17, 0);
saveArm ? ( LNW(487, 24, 487 + tws("Save"), 24, 1.4); );
click && inbox(483, 7, 28, 20) ? ( saveArm = !saveArm; );
tipbox(483, 7, 28, 20, saveArm ? "Now click a snapshot 1–16 to store the current settings" : "Save: click, then choose snapshot 1–16 (or double-click a snapshot to give it a name)");
) : (
// Basic: Save beside the label above the 8 snapshots - click, then a snapshot = store the current settings there
gfx_setfont(7); sbsx = 346 + tws("Snapshots") + 10; sbsh = inbox(sbsx - 4, 0, tws("Save") + 8, 12);
col(saveArm ? cIOTT : (sbsh ? cTEXT : cMUTED), 1); gfx_setfont(7); txt("Save", sbsx, 6, 0);
LNW(sbsx, 11, sbsx + tws("Save"), 11, saveArm ? 1.4 : 0.8);                          // underlined: a button
click && sbsh ? ( saveArm = !saveArm; );
tipbox(sbsx - 4, 0, tws("Save") + 8, 12, saveArm ? "Now click a snapshot 1–8 to store the current settings" : "Save: click, then choose snapshot 1–8 to store the current settings there");
);
// HELP: how the plugin works and the most used keys (a page over the plugin) - where the top bar has room for it
hlpbx = 516;
!tabm && !basic && WT - 204 - 42 - 6 - hlpbx >= 46 ? (   // Basic - the "?" in the strip opens the help
  hbqh = inbox(hlpbx, 9, 19, 16);   // a "?" like in Expert
  col(hlpon ? cSRC : (hbqh ? cHAIR : cLINE), 1); RR(hlpbx, 9, 19, 16, 8);
  col(hlpon ? 0xFFFFFF : (hbqh ? cTEXT : cMUTED), 1); gfx_setfont(6); txt("?", hlpbx + 9.5, 17, 1);
  click && hbqh ? ( hlpon = !hlpon; click = 0; );
  tipbox(hlpbx, 9, 19, 16, "Help: how Roomservice works and the most used keys");
);
// RESET: a button next to View (Save belongs to the snapshots)
vgx = WT - 204; rsx = tabm ? 522 : vgx - 42;
col(cHAIR, 1); LNW(rsx - 6, 9, rsx - 6, 25, 1);                      // separator: Save belongs to the snapshots
chip(rsx, 9, 34, 16, "Reset", 0, cTEXT);
tipbox(rsx, 9, 34, 16, vlock ? "Reset: only the room stays on – living room, listener in the middle, source 2 m in front of it; delay, barrier, filters, source model and EQ off. The limiter stays on as a protection. The layout is locked and stays (snapshots are kept)"
                             : "Reset: only the room stays on – living room, listener in the middle, source 2 m in front of it; delay, barrier, filters, source model and EQ off, all sections open (the delay folded). The limiter stays on as a protection (snapshots are kept)");
click && inbox(rsx, 9, 34, 16) ? (
  snap_take();
  slider1 = 0; slider2 = 5.5; slider3 = 4.5; slider4 = 2.6;          // default room: living room (= the living-room preset)
  slider5 = slider6 = slider7 = slider8 = 0.04; slider9 = 0.33;
  slider10 = 2.75; slider11 = 0.25; slider12 = 1.5; slider13 = 2.75; slider14 = 2.25; slider15 = 1.5;   // listener in the middle, source 2 m in front of it
  slider16 = 1; slider17 = 0; slider18 = 100; slider19 = 1;
  slider20 = 0; slider21 = 0; slider22 = 0; slider23 = 1;
  slider24 = 0; slider25 = 80; slider26 = 1; slider27 = 0; slider28 = 12000; slider29 = 1; slider30 = 0; slider31 = 1; slider32 = 1; slider33 = 0; slider34 = 0; slider35 = 0; slider36 = 0; slider37 = 0; slider38 = 80; slider39 = 0; slider40 = 0; slider41 = 0; slider60 = 0; slider95 = 0; slider96 = 0; slider97 = 100; slider42 = 1.25; slider43 = 0; slider44 = 25; slider45 = 2.75; slider46 = 0;
  // Source EQ offsets (sliders 47 … 59: shifts, gains, Qs, slopes) back to zero
  i = 47; loop(13, sset(i, 0); i += 1; );
  slider61 = 0; slider62 = 100; slider63 = 1; slider64 = 0;   // source angle (fixed, to the front), reflectivity, aim, morph off
  i = 65; loop(30, sset(i, sdef(i)); i += 1; ); slider98 = 100; slider99 = 0; i = 100; loop(10, sset(i, sdef(i)); i += 1; ); mtKill = 1; i = 110; loop(8, sset(i, sdef(i)); i += 1; );                 // delay off with its defaults (50 ms, 30 % feedback, 0 dB, 120 Hz - 10 kHz), source width 100 %, ducking 0, Motion off with its defaults (a running motion is killed), input mono / call line / distance back to defaults
  rflclr(); vopt[18] = 0; tpon = 0; xpon = 0;                              // hear: direct, early and late on again
  selw = 0;
  !vlock ? (                                                         // a locked layout stays as it is
    vopt[1] = vopt[2] = vopt[3] = vopt[6] = 0; vopt[10] = 0;          // view: no tips, paths, direct line or waves; walls shown
    vopt[12] = 2304; vopt[13] = 0;   // the room view stays open (hiding it folded the page in an animation - it looked like flicker)
    vopt[14] = 0; memset(vopt + 21, 0, 21); vopt[47] = 0; zoomv = 0; !tabm ? szoom = 0; scy = 0;   // Tabs - szoom is the open tab; clearing it drew the whole page (all modules on top of each other) for one frame = the blink // layout: every section open, standard column widths, no zoom, top of the page (the room view stays open, see vopt[12])
  );
  snlast = 0;
  snap_notify();
);
!tabm ? viewgrp(vgx, 0);
// black & white (all colors off) and light / dark: top right, one above the other
!basic ? (
mbx = WT - 18; mby = 11;
col(cTEXT, 1); CI(mbx, mby, 4.5, 1); col(cBG, 1); R(mbx + 0.5, mby - 5.5, 6, 11); col(cTEXT, 1); CI(mbx, mby, 4.5, 0);   // half black, half white
mhit = sqr(mx - mbx) + sqr(my - mby) < 42;
click && mhit ? ( vopt[7] = monoOn ? 0 : 1; );
mhit ? ( tipon = 1; tipstr = monoOn ? "Colors on" : "Black & white: switch all colors off"; tipx = mbx - 60; tipy = mby + 22; );
);
tx = WT - 18; ty = 24;
thit = sqr(mx - tx) + sqr(my - ty) < 42;
darkmode ? ( col(0xF2C94C, 1); CI(tx, ty, 4.2, 1); col(cBG, 1); CI(tx + 2.2, ty - 1.6, 3.6, 1); )
         : ( col(cAMB, 1); CI(tx, ty, 2.6, 1); gk = 0; loop(8, an1 = gk * $pi / 4; LNW(tx + cos(an1) * 4.2, ty + sin(an1) * 4.2, tx + cos(an1) * 5.8, ty + sin(an1) * 5.8, 1.1); gk += 1; ); );
click && thit ? ( vopt[4] = darkmode ? 0 : 1; );
thit ? ( tipon = 1; tipstr = darkmode ? "Switch to light mode" : "Switch to dark mode"; tipx = tx - 50; tipy = ty + 22; );
// ---------- SIGNAL-FLOW STRIP: the modules in the order the sound passes them - dot = on / off (click), Shift / Alt + click = solo ----------
fsY = tabm ? 5 : (!basic && !learn ? 46 : 36); fsH = 24; fsC = fsY + fsH / 2;                 // tabs: the right part of the strip sits in the top bar
!tabm ? card(8, fsY, WT - 16, fsH);
// strip order: module ids 0 IN, 1 FILTER, 2 SOURCE, 3 DELAY, 4 ROOM, 5 BARRIER, 6 OUT, 7 MOTION; the Filter sits right after IN (Pre reverb) or just before OUT (Post reverb), per slider30; Basic shows only Source, Room, Barrier, OUT
basic ? ( stord[0] = 2; stord[1] = 4; stord[2] = 5; stord[3] = 6; stn = 4; ) : (
  stord[0] = 0; stn = 1; slider30 < 0.5 ? ( stord[stn] = 1; stn += 1; );
  stord[stn] = 2; stord[stn + 1] = 3; stord[stn + 2] = 4; stord[stn + 3] = 5; stord[stn + 4] = 7; stn += 5;   // + MOTION
  slider30 >= 0.5 ? ( stord[stn] = 1; stn += 1; ); stord[stn] = 6; stn += 1;
);
tabm ? stn = 0;                                                      // tabs: the modules are the tab row
basic && soloM == 2 ? soloM = 0;                                     // (the delay is Expert)
(soloM == 1 && slider85 < 0.5) || (soloM == 2 && slider65 < 0.5) || (soloM == 3 && (slider36 >= 0.5 || isfree)) ? soloM = 0;   // a module switched off ends its solo
gfx_setfont(6);
hearM = basic ? 0 : floor(vopt[18] + 0.5); tabm ? ( stpw = 0; );
soloM > 0 ? ( strcpy(#stp, soloM == 1 ? "SOLO  Source" : (soloM == 2 ? "SOLO  Delay" : "SOLO  Room")); stpw = tws(#stp) + 30; ) : (
  hearM > 0 ? ( strcpy(#stp, hearM == 6 ? "HEAR  Direct" : (hearM == 5 ? "HEAR  Early" : (hearM == 3 ? "HEAR  Late" : "HEAR  part"))); stpw = tws(#stp) + 30; ) : ( stpw = 0; ); );
stpwT = stpw; tabm ? stpw = 0;   // tabs: the solo sign is in the tab row
gfx_setfont(6); lkw = tws(vlock ? "GUI locked" : "Lock GUI") + 28;        // GUI lock: right end of the strip
mcw2 = !basic && !learn ? mcorrw() + 8 : 0; mcOn = 0;
tabm && abOn ? mcw2 = 0;   // the mini correlation meter at the end of the chain (Expert) - Tabs: the strip sits in the top bar and the meter takes the place of the word "compare"; with A / B running there is no room
stRm = tabm ? 34 : 18; abw = abOn ? 126 : (mcw2 > 0 ? 46 : (tabm ? 88 : 126)); fitw = learn || tabm || !basic ? 0 : 52; udw = 74 + (basic && xpn > 0 ? 150 : 0); fsX0 = 20; fsX1 = WT - stRm - lkw - 10 - fitw - (stpw > 0 ? stpw + 10 : 0) - mcw2 - abw - 10 - udw;
stfix = 0; k = 0;
loop(stn, q = stord[k]; ststat(q); gfx_setfont(4); stnat[k] = tws(#sts) + 1; stal[k] = -1; gfx_setfont(6); stfix += (q == 0 || q == 6 ? 0 : 15) + tws(stname(q)) + 5; k += 1; );
stfix += (stn - 1) * 16;
mcw2 > 0 && !tabm && fsX1 - fsX0 - stfix < 30 ? ( fsX1 += mcw2 + abw - 126; mcw2 = 0; abw = 126; );   // All: the chain's texts come first
// the status texts share the free width: short ones keep their width, the long ones split the rest
strem = fsX1 - fsX0 - stfix; stun = stn;
loop(stn,
  stsh = stun > 0 ? strem / stun : 0; stch = 0; k = 0;
  loop(stn, stal[k] < 0 && stnat[k] <= stsh ? ( stal[k] = stnat[k]; strem -= stnat[k]; stun -= 1; stch = 1; ); k += 1; );
  !stch && stun > 0 ? ( k = 0; loop(stn, stal[k] < 0 ? ( stal[k] = max(12, stsh); ); k += 1; ); stun = 0; );
);
// a status that would be cut to a stump (< 44) is left out; its width goes to the others that are cut
stfr = 0; stcn = 0; k = 0; loop(stn, stal[k] < stnat[k] ? ( stal[k] < 44 ? ( stfr += stal[k] + 5; stal[k] = 0; ) : ( stcn += 1; ); ); k += 1; );
stcn > 0 && stfr > 0 ? ( k = 0; loop(stn, stal[k] > 0 && stal[k] < stnat[k] ? ( stal[k] = min(stnat[k], stal[k] + stfr / stcn); ); k += 1; ); );
fsX = fsX0; k = 0
);
// Bottom strip and menus: A/B compare, undo/redo/keys, GUI lock, footer, barrier info rows, room preset menu
function gfxp12()
(
loop(stn,
  q = stord[k]; sonq = ston(q); scq = stcol(q); swq = q != 0 && q != 6; ssq = soloM > 0 && stsolo(q) == soloM;
  ststat(q); stw = stal[k];
  gfx_setfont(6); stnw = tws(stname(q)); itw = (swq ? 15 : 0) + stnw + (stw > 0 ? 5 + stw : 0);
  sthit = inbox(fsX - 5, fsY + 2, itw + 10, fsH - 4);
  swq ? (
    ssq ? ( col(cAMB, 1); CI(fsX + 5.5, fsC, 7, 1); );
    sonq ? ( col(scq, 1); CI(fsX + 5.5, fsC, 4.5, 1); ) : ( col(cSURF, 1); CI(fsX + 5.5, fsC, 4.5, 1); col(scq, 1); CI(fsX + 5.5, fsC, 4.5, 0); CI(fsX + 5.5, fsC, 3.7, 0); );
  );
  col(ssq ? cAMB : (sonq ? cTEXT : cMUTED), 1); gfx_setfont(6); txt(stname(q), fsX + (swq ? 15 : 0), fsC, 0);
  col(sthit && swq ? cTEXT : cMUTED, 1); gfx_setfont(4); tws(#sts) <= stw || stw >= 44 ? txtfit(#sts, fsX + (swq ? 15 : 0) + stnw + 5, fsC, stw);   // no stump like "Origi…" - too little room = the name alone
  click && sthit && tabm && !(mouse_cap & 24) && (!swq || mx > fsX + 12) ? (
    // tab index of each strip module: Filter 6, Source 3, Delay 10, Room 1, Barrier 5, Motion 14, else 7 (Output)
    vopt[46] = q == 1 ? 6 : (q == 2 ? 3 : (q == 3 ? 10 : (q == 4 ? 1 : (q == 5 ? 5 : (q == 7 ? 14 : 7))))); click = 0;
  );
  click && sthit ? (
    (mouse_cap & 24) && stsolo(q) > 0 ? ( soloM == stsolo(q) ? ( soloM = 0; ) : ( !sonq ? sttog(q); soloM = stsolo(q); ); ) : ( swq ? sttog(q); );   // solo of a module that is off: switch it on
    click = 0;
  );
  sthit ? ( tipon = 1; tipstr = stip(q); tipx = fsX + itw / 2; tipy = fsY + fsH + 12; );
  fsX += itw;
  k < stn - 1 ? ( col(cDIM2, 1); POLY4(fsX + 6, fsC - 3.5, fsX + 10, fsC, fsX + 6, fsC + 3.5, fsX + 6, fsC + 3.5); fsX += 16; );
  k += 1;
);
!tabm && (soloM > 0 || hearM > 0) ? (
  stpx = WT - stRm - lkw - 10 - stpw;
  col(cAMB, 1); RR(stpx, fsY + 5, stpw, 14, 7); col(0xFFFFFF, 1); gfx_setfont(6); txt(#stp, stpx + 9, fsC, 0);
  LNW(stpx + stpw - 13, fsC - 2.5, stpx + stpw - 8, fsC + 2.5, 1.3); LNW(stpx + stpw - 13, fsC + 2.5, stpx + stpw - 8, fsC - 2.5, 1.3);
  click && inbox(stpx, fsY + 3, stpw, 18) ? ( soloM > 0 ? ( soloM = 0; ) : ( vopt[18] = 0; ); click = 0; );
  tipbox(stpx, fsY + 3, stpw, 18, soloM > 0 ? "Solo: only this module is heard – click to end it (or Esc). Monitor only: not saved, ends when the plugin window closes" : "Hear: only a part of the room sound plays (set in Reverb) – click: all three again. This one is saved with the project");
);
// A / B COMPARE: B starts as a copy of what you hear (A); change B, then switch. Loudness match: B is played as loud as A.
abx = WT - stRm - lkw - 10 - fitw - (stpw > 0 ? stpw + 10 : 0) - mcw2 - abw;
// UNDO / REDO / KEYS (left of A / B)
ux = abx - (basic && xpn > 0 ? 74 : udw) + 2; uy = fsY + 4;
// BASIC: Expert settings active - a fixed sign in the strip, left of undo (click = the list)
basic && xpn > 0 ? (
  xpx = ux - 150; xpw = 142;
  col(cAMB, 1); RR(xpx, fsY + 4, xpw, 16, 8); col(cSURF, 1); CI(xpx + 10, fsC, 2.4, 1);
  gfx_setfont(6); sprintf(#w1, "%d Expert setting%s", xpn, xpn == 1 ? "" : "s"); txtfit(#w1, xpx + 18, fsC, xpw - 24);
  click && inbox(xpx, fsY + 4, xpw, 16) ? ( xpon = !xpon; click = 0; );
  tipbox(xpx, fsY + 4, xpw, 16, #xpt);
  xpbx = xpx;
);
gk = 0;
loop(2,
  upx = ux + gk * 21; uhit = inbox(upx, uy, 19, 16); uok = (gk == 0 ? unN : rdN) > 0 && !vlock;
  col(uhit && uok ? cHAIR : cLINE, 1); RR(upx, uy, 19, 16, 5);
  col(uok ? cTEXT : cDIM2, 1);
  SDB0(); gk == 0 ? ( SDARC(upx + 10, uy + 9, 4, -1.75, 1.7); CHEV(upx + 5.9, uy + 10.6, -0.2, 0.98, 2.6, 1); )   // smooth arcs, line heads
                  : ( SDARC(upx + 9, uy + 9, 4, -1.7, 1.75); CHEV(upx + 13.1, uy + 10.6, 0.2, 0.98, 2.6, 1); ); SDDRAW(1.1);
  click && uhit ? ( uok ? ( gk == 0 ? udo() : uredo(); ); click = 0; );
  gk == 0 ? ( sprintf(#utip0, "Undo (Ctrl/Cmd+Z) – %d step%s back", unN, unN == 1 ? "" : "s"); tipbox(upx, uy, 19, 16, #utip0); )
          : ( sprintf(#utip1, "Redo (Ctrl/Cmd+Shift+Z or Ctrl+Y) – %d step%s forward", rdN, rdN == 1 ? "" : "s"); tipbox(upx, uy, 19, 16, #utip1); );
  gk += 1;
);
upx = ux + 46; uhit = inbox(upx, uy, 19, 16);
col(kovl || ((tabm || basic) && hlpon) ? cSRC : (uhit ? cHAIR : cLINE), 1); RR(upx, uy, 19, 16, 8);
col(kovl || ((tabm || basic) && hlpon) ? 0xFFFFFF : (uhit ? cTEXT : cMUTED), 1); gfx_setfont(6); txt("?", upx + 9.5, uy + 8, 1);
click && uhit ? ( tabm || basic ? ( hlpon = !hlpon; ) : ( kovl = !kovl; ); click = 0; );
tipbox(upx, uy, 19, 16, tabm || basic ? "Help: how Roomservice works – with every key and mouse shortcut (key ?)" : "Keys and mouse: every shortcut on one page (key ?)");
// the Tabs | All | Fit switch moved under the view modes (modeview) - no Tabs | All switch and no Fit all chip in the strip any more
basic ? (   // BASIC Fit all - the whole page in the window (no scrolling, smaller on a low window)
  fitx = WT - stRm - lkw - 8 - 44 - (stpw > 0 ? stpw + 10 : 0); fiton = vopt[36] >= 0.5;
  chip(fitx, fsY + 4, 44, 16, "Fit all", fiton, cTEXT) ? ( vopt[36] = fiton ? 0 : 1; scy = 0; click = 0; );
  tipbox(fitx, fsY + 4, 44, 16, fiton ? "Fit all: the whole page in the window – smaller on a low window. Click: the standard size (the page scrolls with the mouse wheel when the window is low)"
                                    : "Fit all: make the whole page fit into the window – no scrolling, text and knobs get smaller on a low window");
);
gk = 0;
loop(2,
  abpx = abx + gk * 21; abh = inbox(abpx, fsY + 4, 19, 16); abact = abOn && abCur == gk;
  col(abact ? (gk == 0 ? cSRC : cAMB) : (abh ? cHAIR : cLINE), 1); RR(abpx, fsY + 4, 19, 16, 5);
  col(abact ? 0xFFFFFF : (abOn || abh ? cTEXT : cMUTED), 1); gfx_setfont(6); txt(gk == 0 ? "A" : "B", abpx + 9.5, fsC, 1);
  click && abh ? (
    !abOn ? ( gk == 1 ? ( abstore(0); abstore(1); abOn = 1; abCur = 1; ); ) : (
      abCur != gk ? ( abstore(abCur); abload(gk); abCur = gk; );
    );
    click = 0;
  );
  abh ? ( tipon = 1; tipforce = 0; tipx = abpx + 10; tipy = fsY + fsH + 12;
    tipstr = !abOn ? (gk == 1 ? "A / B compare: click B to start – B begins as a copy of what you hear now (A). Change B, then switch with A / B" : "A / B compare: click B to start")
                   : (gk == 0 ? "A: the settings you started with – click to hear A (what you changed in B is kept)" : "B: click to hear B – its loudness is matched to A while Match is on"); );
  gk += 1;
);
abOn ? (
  abmx = abx + 46; abmw = 62; abmh = inbox(abmx, fsY + 4, abmw, 16);
  abtd = 20 * log10(max(0.0001, cABG));
  col(abMatch ? (abmh ? cHAIR : cLINE) : cSURF, 1); RR(abmx, fsY + 4, abmw, 16, 5); !abMatch ? ( col(cBORD, 1); RRO(abmx, fsY + 4, abmw, 16, 5); );
  abMatch && abCur == 1 && abs(abtd) >= 0.05 ? sprintf(#abt, "%+.1f", abtd) : strcpy(#abt, "Match level");
  col(abMatch ? cTEXT : cMUTED, 1); gfx_setfont(6); txt(#abt, abmx + abmw / 2, fsC, 1);
  click && abmh ? ( abMatch = !abMatch; click = 0; );
  abmh ? ( tipon = 1; tipforce = 0; tipx = abmx + 20; tipy = fsY + fsH + 12;
    tipstr = abMatch ? "Loudness match on: B plays as loud as A (measured, about 3 s each – best with a looped passage); the number = the trim on B in dB. Only while this window is open – click: off" : "Loudness match off: A and B at their own level – click: on"; );
  abxx = abx + 112; abxh = inbox(abxx - 2, fsY + 4, 14, 16);
  col(abxh ? cTEXT : cMUTED, 1); LNW(abxx + 2, fsC - 3.5, abxx + 9, fsC + 3.5, 1.3); LNW(abxx + 2, fsC + 3.5, abxx + 9, fsC - 3.5, 1.3);
  click && abxh ? ( abOn = 0; click = 0; );
  abxh ? ( tipon = 1; tipforce = 0; tipx = abxx + 5; tipy = fsY + fsH + 12; tipstr = "End A / B: keep what you hear now"; );
) : (
  abw >= 88 ? ( col(cMUTED, 1); gfx_setfont(7); txt("compare", abx + 46, fsC, 0); );
);
mcw2 > 0 ? mcorrdraw(abx + abw + 6, fsC);   // Corr – the mono compatibility of the output, in every tab and view
// GUI LOCK: folding, column widths and view options stay as they are (also on Reset) - the sound is not affected
lkx = WT - stRm - lkw; lkhit = inbox(lkx, fsY + 4, lkw, 16);
col(vlock ? cRMTT : (lkhit ? cHAIR : cLINE), 1); RR(lkx, fsY + 4, lkw, 16, 8);
col(vlock ? cSURF : (lkhit ? cTEXT : cMUTED), 1); padlock(lkx + 12, fsC + 0.6, vlock);
gfx_setfont(6); txt(vlock ? "GUI locked" : "Lock GUI", lkx + 21, fsC, 0);
click && lkhit ? ( vopt[15] = vlock ? 0 : 1; click = 0; );
lkhit ? ( tipon = 1; tipforce = 0; tipstr = vlock ? "GUI locked: folding, column widths and view options stay (also on Reset) – click to unlock. The sound is not affected" : "Lock the GUI: folding, column widths and view options stay as they are (also on Reset) – saved with the project. The sound is not affected"; tipx = lkx + lkw / 2 - 120; tipy = fsY + fsH + 12; );
// footer
!szoom ? ( col(cMUTED, 1); gfx_setfont(7); txt(#verstr, 12, LH - 9, 0); );
tabm && szoom ? ( !vgone ? gfxp4(); !zoomv && !vgone ? viewgrp(VX + VW - 78 - 180, VY + 12 - 17); tabrow();   // tabs: room view and tab row at the page scale, above the module
  !zoomv ? ( col(cMUTED, 1); gfx_setfont(7); txt(#verstr, 12, HT - 6, 0); ); );   // the footer also in Tabs
szoom ? (                                                    // back to the zoom scale for the section
  tabm && tipon ? ( tipx = (tipx * gsN + goxN - goxZ) / gsZ; tipy = (tipy * gsN + goyN - goyZ) / gsZ; ) : ( tipon = 0; );   // tabs: a tip of the room view stays
  gs = gsZ; gox = goxZ; goy = goyZ; WT = (gfx_w - gox) / gs; HT = (gfx_h - goy) / gs;
  setfonts(); mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs;
);

!szoom ? (gfxp4());   // room view (not while a section is zoomed)
(szoom || !vwopen || vtab >= 2) && !(mouse_cap & 1) ? (     // section zoom / folded room view / analysis tab: mouse released
  drag = 0; kdrag = 0; eqdrag = 0; wbdrag = 0; swDrag = 0;
  memset(gheld, 0, SLOTS);
  i = 1; loop(NSL, gtouch[i] ? ( sauto_end(i); gtouch[i] = 0; ); i += 1; );
);
!zoomv ? (
// values shared by both views
ta = selw == 0 ? (aN + aE + aS + aW) / 4 : (selw == 1 ? aN : (selw == 2 ? aE : (selw == 3 ? aS : (selw == 4 ? aW : aF))));
tname = selw == 0 ? "α all" : (selw == 1 ? "α back" : (selw == 2 ? "α right" : (selw == 3 ? "α front" : (selw == 4 ? "α left" : "α floor"))));
isall = selw != 0 || (abs(aN - aE) < 0.001 && abs(aN - aS) < 0.001 && abs(aN - aW) < 0.001);
wmsel = -1; gk = 0; loop(9, isall && abs(ta - wmata(gk)) < 0.001 ? wmsel = gk; gk += 1; );
wmsel >= 0 ? sprintf(#wmt, "%s · α %.2f", wmatname(wmsel), ta) : ( isall ? sprintf(#wmt, "Custom · α %.2f", ta) : strcpy(#wmt, "Mixed") );
btg = floor(slider41 + 0.5);
bmsel = bmatsel();
// eqed = 1 when any Source EQ offset (sliders 47 … 59) is away from zero
eqed = 0; i = 47; loop(13, abs(slider(i)) > 0.0005 ? eqed = 1; i += 1; );
// what happens to the direct sound behind a barrier: three label / value rows
btg == 0 ? ( strcpy(#bi1, "–"); strcpy(#bi2, "one open room"); strcpy(#bi3, "–"); bil3 = 0; ) : (
  bdirT == 0 ? (
    strcpy(#bi1, "0 dB"); strcpy(#bi2, "same side");
    !isfree && bspl < -0.05 ? sprintf(#bi3, "%.0f dB", bspl) : strcpy(#bi3, "–"); bil3 = 1;
  ) : (
    sprintf(#bi1, "%.0f dB", 10 * log10(max(0.0000001, bdirE)) - (bdirT == 3 ? bdirC : 0));
    strcpy(#bi2, bdirT == 1 ? "through the opening" : (bdirT == 2 ? "bent through opening" : (bdirT == 3 ? "through the wall" : (bdirT == 5 ? "past the barrier" : "around the edges"))));
    !isfree ? sprintf(#bi3, "%.0f dB", bspl) : strcpy(#bi3, "–"); bil3 = 0;
  );
);
learn ? gfxpL() : (basic ? (!szoom ? basictiles(); gfxp5(); actlines()) : (gfxp8();
gfxp9();
gfxp10()));   // end of: Basic / Expert
tabm && szoom && !zoomv ? (                                   // tabs: frame of the module, open under its tab
  zhF > zh + 1 ? ( col(cSURF, 1); R(0, zh - 10, zwT, zhF - zh + 10 - 0.5); col(cSURF, 1); RR(0, zhF - 12, zwT, 12, 8); );   // a shorter module: its card reaches down to the frame
  col(tabc(tabsel), 1); RRO(-0.6, -0.6, zwT + 1.2, zhF + 1.2, 8.5); RRO(-1.3, -1.3, zwT + 2.6, zhF + 2.6, 9.2);   // as thick as the tab frame
// the tab frame is 1.2 wide (1.9, 2.6) - the module frame (the two lines above) as well, so the two meet cleanly
  tbzx = (tbAX * gsN + goxN - gox) / gs; tbzw = tbAW * gsN / gs;
  cCTLon ? ( col(cSURF, 1); R(tbzx, -1.6, tbzw, 2.1); ) : ( col(cSURF, 1); R(tbzx + 0.2, -2.2, tbzw - 0.4, 2.7); );
  abs(tbAX - tbX0) < 0.01 ? (   // the first tab is open: no rounded corner under it - its left line runs straight into the module's
    tlw = (cCTLon ? 1.2 : 0.8) * gsN / gs; col(cSURF, 1); R(cCTLon ? -1.3 + tlw : -0.6 + tlw, -0.6, 12 * gsN / gs, (cCTLon ? 18 : 12) * gsN / gs); col(tabc(tabsel), 1); R(cCTLon ? -1.3 : -0.6, cCTLon ? -1.4 : -0.6, tlw, (cCTLon ? 20 : 12) * gsN / gs);
  );
  tcl = tch || tcd; col(tcl ? cTEXT : cDIM, 1);   // corner grip - the module larger / smaller
  LNW(zwT - 12, zhF - 3, zwT - 3, zhF - 12, tcl ? 1.4 : 1); LNW(zwT - 8, zhF - 3, zwT - 3, zhF - 8, tcl ? 1.4 : 1); LNW(zwT - 4, zhF - 3, zwT - 3, zhF - 4, tcl ? 1.4 : 1);
  tch && !tcd ? tipbox(zwT - 16, zhF - 16, 18, 18, "Drag the corner: the module larger or smaller – the Stage View takes the rest (double-click = automatic)");
);
);   // end of: sections hidden while the room view is enlarged

// ---- room preset menu ----
(rmenu || smenu) ? ( tipon = 0; tipforce = 0; );
dmenu ? (
  tipon = 0; tipforce = 0;
  gsS = gs; goxS = gox; goyS = goy; mxS = mx; myS = my;                          // own scale dmk around its top left corner
  gs = gsS * dmk; gox = goxS + dpmx * (gsS - gs); goy = goyS + dpmy * (gsS - gs);
  setfonts(); mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs;
  col(cINK, 0.12); RR(dpmx + 1, dpmy + 3, dpmw, dpmh, 8);
  col(cSURF, 1); RR(dpmx, dpmy, dpmw, dpmh, 8); col(cBORD, 1); RRO(dpmx, dpmy, dpmw, dpmh, 8);
  k = 0;
  loop(NDP,
    ix = dpIX[k]; iy = dpIY[k]; b = dptab + k * 19;
    (k == 0 || dpcat(k - 1) != dpcat(k)) ? ( col(cMUTED, 1); gfx_setfont(7); txt(dpcatname(dpcat(k)), ix + 8, iy - 7, 0); );
    msel = dpsel == k; mhov = inbox(ix, iy, DPCW, dpih);
    msel ? ( col(cDLTT, 1); RR(ix, iy + 1, DPCW, dpih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(ix, iy + 1, DPCW, dpih - 2, 5); ); );
    col(msel ? cSURF : cTEXT, 1); gfx_setfont(6); txt(dpname(k), ix + 8, iy + dpih / 2, 0);
    b[1] >= 0.5 ? ( b[2] >= 0.5 || b[6] == b[7] ? strcpy(#rmd, dnname(b[6])) : sprintf(#rmd, "%s / %s", dnname(b[6]), dnname(b[7])); )
                : ( b[2] >= 0.5 ? sprintf(#rmd, "%.0f ms", b[4]) : sprintf(#rmd, "%.0f / %.0f ms", b[4], b[5]); );
    b[0] >= 0.5 ? strcat(#rmd, " pp");
    col(msel ? cSURF : cMUTED, 1); gfx_setfont(4); txt(#rmd, ix + DPCW - 8, iy + dpih / 2, 2);
    mhov ? ( strcpy(#stip, dpdesc(k)); sprintf(#w2, " – feedback %.0f %%, %.0f Hz – %.1f kHz", b[8], b[12], b[14] / 1000); strcat(#stip, #w2); b[16] > 0 || b[17] > 0 ? ( sprintf(#w2, ", tape %.0f %%, drive %.0f %%", b[16], b[17]); strcat(#stip, #w2); );
             tipon = 1; tipforce = 0; tipstr = #stip; tipx = ix + DPCW / 2; tipy = iy + dpih + 12 > scy + HT - 46 ? iy - 4 : iy + dpih + 12; );   // only with Tips on // flips above the row near the bottom edge
    k += 1;
  );
  col(cMUTED, 1); gfx_setfont(7); txt("Tempo presets follow the tempo the delay uses (project, tap or analysis). Loading switches the delay on.", dpmx + 10, dpmy + dpmh - 9, 0);
  menux(dpmx, dpmy, dpmw);
  tipon ? ( tipx = dpmx + (tipx - dpmx) * dmk; tipy = dpmy + (tipy - dpmy) * dmk; );   // tooltip back to the page scale
  gs = gsS; gox = goxS; goy = goyS; setfonts(); mx = mxS; my = myS;
)
);
// Draws the popup menus: room presets (search, my rooms), motion presets, free field presets and source model menu
function gfxp13()
(
rmenu ? (
  rmZ = tabm && szoom;   // tabs: the room menu at the page scale (it opens from the top bar)
  rmZ ? ( gsS = gs; goxS = gox; goyS = goy; mxS = mx; myS = my; WTS = WT; HTS = HT; gs = gsN; gox = goxN; goy = goyN; WT = WTN; HT = HTN; setfonts(); mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs; );
  col(cINK, 0.12); RR(roommx + 1, roommy + 3, roommw, roommh, 8);
  col(cSURF, 1); RR(roommx, roommy, roommw, roommh, 8); col(cBORD, 1); RRO(roommx, roommy, roommw, roommh, 8);
  // header: search field + Save room
  col(cBG, 1); RR(rsqx, rmhy, rsqw, 18, 5); col(rqlen > 0 ? cRMTT : cBORD, 1); RRO(rsqx, rmhy, rsqw, 18, 5);
  col(cMUTED, 1); CI(rsqx + 10, rmhy + 8, 3.4, 0); LNW(rsqx + 12.5, rmhy + 10.5, rsqx + 15, rmhy + 13, 1.2);
  rqlen > 0 ? ( sn_strn(rqb, 30, #rqs); col(cTEXT, 1); gfx_setfont(2); txt(#rqs, rsqx + 22, rmhy + 9, 0); rqcx = rsqx + 23 + tws(#rqs); )
            : ( col(cDIM2, 1); gfx_setfont(4); txt("Search – just type", rsqx + 22, rmhy + 9, 0); rqcx = rsqx + 22; );
  (floor(time_precise() * 2) & 1) == 0 ? ( col(cTEXT, 1); LNW(rqcx, rmhy + 4, rqcx, rmhy + 14, 1); );
  rqlen > 0 ? ( col(cMUTED, 1); gfx_setfont(7); txt("Esc = clear  ·  Enter = load the first", rsqx + rsqw + 8, rmhy + 9, 0); );
  chip(rsvx, rmhy + 1, rsvw, 16, "+ Save this room", 0, cRMTT);
  rqlen <= 0 ? (
    chip(rpgx, rmhy + 1, 48, 16, "Page 1", rmpg == 0, cRMTT); chip(rpgx + 50, rmhy + 1, 48, 16, "Page 2", rmpg == 1, cRMTT); chip(rpgx + 100, rmhy + 1, 54, 16, "Scenario", rmpg == 2, cRMTT); chip(rpgx + 156, rmhy + 1, 52, 16, "Outdoor", rmpg == 3, cFFTT);
    inbox(rpgx, rmhy + 1, 208, 16) ? ( tipon = 1; tipforce = 0; tipx = rpgx + 52; tipy = rmhy + 30;
      tipstr = mx >= rpgx + 156 ? "Outdoor: free-field presets – a street, an alley, a soccer field, a canyon … (area, ground and the two reflectors; switches the free field on)" : mx < rpgx + 50 ? "Page 1: household, work & school, vehicles, studio & media, venues, industrial & large" : (mx < rpgx + 100 ? "Page 2: homes, offices & schools, trains, cars & trucks, basements & huts, churches & caves, effect rooms" : "Scenario presets: film situations in one click – source, room, barrier and delay (behind a wall, outside a door, in the woods, …)"); );
  );
  inbox(rsvx, rmhy + 1, rsvw, 16) ? ( tipon = 1; tipforce = 0; tipx = rsvx + rsvw / 2; tipy = rmhy + 30;
    tipstr = "Save the current room (size, materials, positions, barrier, reverb) under a name in My rooms – stored with the project; for all projects: REAPER's FX window + → Save preset / Save as default"; );
  // my rooms
  urnv > 0 ? (
    col(cRMTT, 1); gfx_setfont(7); txt("My rooms", roommx + 12, urhy, 0);
    u = 0;
    loop(URM,
      urX[u] != NOPOS ? (
        ix = urX[u]; iy = urY[u]; b0 = urb + u * URL; mhov = inbox(ix, iy, RMCW, roomih); msel = urmatch(u);
        msel ? ( col(cTEXT, 1); RR(ix, iy + 1, RMCW, roomih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(ix, iy + 1, RMCW, roomih - 2, 5); ); );
        col(msel ? cSURF : cTEXT, 1); gfx_setfont(6); sn_strn(b0 + 2, 24, #urnm); txtfit(#urnm, ix + 8, iy + roomih / 2, 88);
        col(msel ? cSURF : cMUTED, 1); gfx_setfont(4);
        sprintf(#rmd, "%.1f × %.1f × %.1f m", b0[27] * b0[46], b0[28] * b0[46], b0[29] * b0[46]); txt(#rmd, ix + 100, iy + roomih / 2, 0);
        mhov ? ( col(msel ? cSURF : cIOTT, 1); gfx_setfont(5); txt("×", ix + RMCW - 10, iy + roomih / 2, 1);
                 tipon = 1; tipforce = 0; tipx = ix + RMCW / 2; tipy = iy - 10; strcpy(#urtp, urcatname(b0[1])); strcat(#urtp, " – click: load  ·  right-click: rename / overwrite  ·  ×: delete"); tipstr = #urtp; )
             : ( gfx_setfont(7); txt(urcatname(b0[1]), ix + RMCW - 8, iy + roomih / 2, 2); );
      );
      u += 1;
    );
    col(cLINE, 1); LN(roommx + 8, ursep, roommx + roommw - 8, ursep);
  );
  // "overwritten": a short blue mark on that room
  time_precise() - urflash < 1.2 && urX[urflu] != NOPOS ? ( col(cRMTT, 1); gfx_setfont(7); txt("overwritten ✓", urX[urflu] + RMCW - 30, urY[urflu] + roomih / 2, 2); );
  rmnone ? ( col(cMUTED, 1); gfx_setfont(4); txt("No room matches – Esc clears the search", roommx + 12, rmY[NPRE] - 4, 0); );
  k = 0;
  loop(NPRE + 1,
    ix = rmX[k]; iy = rmY[k];
    k < NPRE && rmH[k] && ix != NOPOS ? (                               // category heading
      col(cMUTED, 1); gfx_setfont(7); txt(roomcatname(roomcat(k)), ix + 8, iy - 7, 0);
    );
    ix != NOPOS ? (
    k == NPRE ? ( col(cLINE, 1); LN(roommx + 8, iy - 4, roommx + roommw - 8, iy - 4); );
    msel = k < NPRE ? (!isfree && rsel == k) : isfree;
    mhov = inbox(ix, iy, RMCW, roomih);
    msel ? ( col(cTEXT, 1); RR(ix, iy + 1, RMCW, roomih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(ix, iy + 1, RMCW, roomih - 2, 5); ); );
    col(msel ? cSURF : cTEXT, 1); gfx_setfont(6); txt(k < NPRE ? roomname(k) : "Free field", ix + 8, iy + roomih / 2, 0);
    col(msel ? cSURF : cMUTED, 1); gfx_setfont(4);
    k < NPRE ? ( pb0 = PT + k * PRS; sprintf(#rmd, "%.1f × %.1f × %.1f m", pb0[0] * pb0[14], pb0[1] * pb0[14], pb0[2] * pb0[14]); sprintf(#rmt, "%.2f s", presetrt(k)); )
          : ( strcpy(#rmd, "no walls"); strcpy(#rmt, "–"); );
    txt(#rmd, ix + 100, iy + roomih / 2, 0); txt(#rmt, ix + RMCW - 8, iy + roomih / 2, 2);
    );
    k += 1;
  );
  k = 0;
  loop(NSC,
    ix = scnX[k]; iy = scnY[k];
    ix != NOPOS ? (
      scnH[k] ? ( col(cMUTED, 1); gfx_setfont(7); txt(sccatname(sccat(k)), ix + 8, iy - 7, 0); );
      msel = scnlast == k; mhov = inbox(ix, iy, RMCW, roomih);
      msel ? ( col(cTEXT, 1); RR(ix, iy + 1, RMCW, roomih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(ix, iy + 1, RMCW, roomih - 2, 5); ); );
      col(msel ? cSURF : cTEXT, 1); gfx_setfont(6); txtfit(scname(k), ix + 8, iy + roomih / 2, 150);
      col(msel ? cSURF : cMUTED, 1); gfx_setfont(4); txt(scshort(k), ix + RMCW - 8, iy + roomih / 2, 2);
      mhov ? ( tipon = 1; tipforce = 0; tipx = ix + RMCW / 2; tipy = iy - 10; strcpy(#sctp, scname(k)); strcat(#sctp, ":  "); strcat(#sctp, scdesc(k)); tipstr = #sctp; );
    );
    k += 1;
  );
  k = 0;
  loop(NFF,
    ix = ffpmX[k]; iy = ffpmY[k];
    ix != NOPOS ? (
      ffpmH == k ? ( col(cMUTED, 1); gfx_setfont(7); txt("Free field", ix + 8, iy - 7, 0); );   // (a search: the heading above the first one found)
      msel = isfree && ffpLast == k; mhov = inbox(ix, iy, RMCW, roomih);
      msel ? ( col(cTEXT, 1); RR(ix, iy + 1, RMCW, roomih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(ix, iy + 1, RMCW, roomih - 2, 5); ); );
      col(msel ? cSURF : cTEXT, 1); gfx_setfont(6); txtfit(ffpname(k), ix + 8, iy + roomih / 2, 150);
      ffb = FFP + k * 16; sprintf(#w1, "%.0f × %.0f m", ffb[0], ffb[1]); col(msel ? cSURF : cMUTED, 1); gfx_setfont(4); txt(#w1, ix + RMCW - 8, iy + roomih / 2, 2);
      mhov ? ( tipon = 1; tipforce = 0; tipx = ix + RMCW / 2; tipy = iy - 10; strcpy(#sctp, ffpname(k)); strcat(#sctp, ":  "); strcat(#sctp, ffpdesc(k)); tipstr = #sctp; );
    );
    k += 1;
  );
  rmfp ? ( col(cMUTED, 1); gfx_setfont(7); txt("A free-field preset sets area, ground and both reflectors – the room comes back when you pick a room", rkx, rky + 7, 0); ) :
  rmsc ? ( col(cMUTED, 1); gfx_setfont(7); txt("A scenario sets room, source, listener, barrier and delay – undo (Ctrl/Cmd + Z) brings the old settings back", rkx, rky + 7, 0); ) : (
  col(cMUTED, 1); gfx_setfont(7); txt("Keep when loading", rkx, rky + 7, 0);
  chip(rkc1, rky, 80, 14, "Source + listener", vopt[16] >= 0.5, cTEXT);
  chip(rkc2, rky, 50, 14, "Barrier", vopt[17] >= 0.5, cBARTT); );
  !rmsc && !rmfp && inbox(rkc1, rky, 80, 14) ? ( tipon = 1; tipforce = 0; tipstr = vopt[16] >= 0.5 ? "On: source, listener and facing stay where you put them (moved inside the new room if needed)" : "Off: the preset sets source and listener on the center line, the listener faces the back wall"; tipx = rkc1 + 40; tipy = rky + 26; );
  !rmsc && !rmfp && inbox(rkc2, rky, 50, 14) ? ( tipon = 1; tipforce = 0; tipstr = vopt[17] >= 0.5 ? "On: the barrier (type, position, opening, damping, back part) stays when a preset is loaded" : "Off: loading a preset removes the barrier"; tipx = rkc2 + 25; tipy = rky + 26; );
  urctx ? (
    tipon = 0; tipforce = 0;
    col(cINK, 0.14); RR(urcx + 2, urcy + 3, 190, 58, 6);
    col(cSURF, 1); RR(urcx, urcy, 190, 58, 6); col(cBORD, 1); RRO(urcx, urcy, 190, 58, 6);
    k = 0;
    loop(3,
      inbox(urcx, urcy + 2 + k * 18, 190, 18) ? ( col(cLINE, 1); RR(urcx + 3, urcy + 3 + k * 18, 184, 16, 4); );
      col(k == 2 ? cIOTT : cTEXT, 1); gfx_setfont(6);
      txt(k == 0 ? "Rename …" : (k == 1 ? "Overwrite with the current settings" : "Delete"), urcx + 10, urcy + 11 + k * 18, 0);
      k += 1;
    );
  );  menux(roommx, roommy, roommw);
  rmZ ? ( gs = gsS; gox = goxS; goy = goyS; WT = WTS; HT = HTS; setfonts(); mx = mxS; my = myS; );
);
// ---- motion preset menu ----
(basic || learn || (tabm ? tabsel != 14 : !secopen(14))) ? mpmenu = 0;   // closes when its module is not on the page (Basic, another tab)
mpmenu ? (
  mpmw = 300; mpmh = NMP * 16 + 8; mpmx = max(4, min(mppx, WT - mpmw - 4)); mpmy = mppy + 18; mpmy + mpmh > scy + HT - 4 ? mpmy = mppy - mpmh - 2;
  col(cINK, 0.12); RR(mpmx + 1, mpmy + 3, mpmw, mpmh, 8); col(cSURF, 1); RR(mpmx, mpmy, mpmw, mpmh, 8); col(cBORD, 1); RRO(mpmx, mpmy, mpmw, mpmh, 8);
  k = 0;
  loop(NMP,
    iy = mpmy + 4 + k * 16; mhov = inbox(mpmx + 4, iy, mpmw - 8, 16);
    k == 8 ? ( col(cLINE, 1); LN(mpmx + 8, iy - 0.5, mpmx + mpmw - 8, iy - 0.5); );
    mhov ? ( col(cLINE, 1); RR(mpmx + 4, iy + 1, mpmw - 8, 14, 5); );
    col(cTEXT, 1); gfx_setfont(6); txt(mpname(k), mpmx + 12, iy + 8, 0);
    mhov ? ( strcpy(#stip, mpname(k)); strcat(#stip, ":  "); strcat(#stip, mpdesc(k)); tipon = 1; tipforce = 0; tipstr = #stip; tipx = mpmx + mpmw / 2; tipy = mpmy + mpmh + 14 > scy + HT - 46 ? mpmy - 4 : mpmy + mpmh + 14; );   // only with Tips on or Ctrl/Cmd held; clear of the window edge (else above the menu, never over the rows)
    k += 1;
  );
  menux(mpmx, mpmy, mpmw);
);
// ---- free field preset menu ----
(basic || learn || !isfree || (tabm ? tabsel != 15 : !secopen(1))) ? ffpmenu = 0;
ffpmenu ? (
  ffmw = 330; ffmh = NFF * 16 + 8 + 26; mfit(ffpx, ffpy, ffpw, 16, ffmw, ffmh, 0); ffmx = mfx; ffmy = mfy; ffmk = mfk; mtx(ffmx, ffmy, ffmk);
  col(cINK, 0.12); RR(ffmx + 1, ffmy + 3, ffmw, ffmh, 8); col(cSURF, 1); RR(ffmx, ffmy, ffmw, ffmh, 8); col(cBORD, 1); RRO(ffmx, ffmy, ffmw, ffmh, 8);
  k = 0; ffhov = -1;
  loop(NFF,
    iy = ffmy + 4 + k * 16; mhov = inbox(ffmx + 4, iy, ffmw - 8, 16) && !menuxhit(ffmx, ffmy, ffmw, 1); mhov ? ffhov = k;
    mhov ? ( col(cLINE, 1); RR(ffmx + 4, iy + 1, ffmw - 8, 14, 5); );
    ffpLast == k ? ( col(cFFTT, 1); RR(ffmx + 8, iy + 4, 3, 8, 1.5); );
    col(cTEXT, 1); gfx_setfont(6); txt(ffpname(k), ffmx + 16, iy + 8, 0);
    ffb = FFP + k * 16; sprintf(#w1, "%.0f × %.0f m", ffb[0], ffb[1]); col(cMUTED, 1); gfx_setfont(4); txt(#w1, ffmx + ffmw - 12, iy + 8, 2);
    k += 1;
  );
  col(cLINE, 1); LN(ffmx + 8, ffmy + ffmh - 25, ffmx + ffmw - 8, ffmy + ffmh - 25);
  col(cMUTED, 1); gfx_setfont(7); ffhov >= 0 ? txtfit(ffpdesc(ffhov), ffmx + 12, ffmy + ffmh - 13, ffmw - 24) : txtfit("Hover a place for its description", ffmx + 12, ffmy + ffmh - 13, ffmw - 24);
  menux(ffmx, ffmy, ffmw);
  mtxend();
);
// ---- reflector setup menu ----
(basic || learn || !isfree || (tabm ? tabsel != 4 : !secopen(4))) ? rpmenu = 0;
rpmenu ? (
  rpmw = 340; rpmh = NRP * 16 + 8 + 26; mfit(rsuX, rsuY, rsuW, 16, rpmw, rpmh, 0); rpmx = mfx; rpmy = mfy; rpmk = mfk; mtx(rpmx, rpmy, rpmk);
  col(cINK, 0.12); RR(rpmx + 1, rpmy + 3, rpmw, rpmh, 8); col(cSURF, 1); RR(rpmx, rpmy, rpmw, rpmh, 8); col(cBORD, 1); RRO(rpmx, rpmy, rpmw, rpmh, 8);
  k = 0; rphov = -1;
  loop(NRP,
    iy = rpmy + 4 + k * 16; mhov = inbox(rpmx + 4, iy, rpmw - 8, 16) && !menuxhit(rpmx, rpmy, rpmw, 1); mhov ? rphov = k;
    mhov ? ( col(cLINE, 1); RR(rpmx + 4, iy + 1, rpmw - 8, 14, 5); );
    rpcur() == k ? ( col(cWALTT, 1); RR(rpmx + 8, iy + 4, 3, 8, 1.5); );
    col(cTEXT, 1); gfx_setfont(6); txt(rpname(k), rpmx + 16, iy + 8, 0);
    k += 1;
  );
  col(cLINE, 1); LN(rpmx + 8, rpmy + rpmh - 25, rpmx + rpmw - 8, rpmy + rpmh - 25);
  col(cMUTED, 1); gfx_setfont(7); txtfit(rphov >= 0 ? rpdesc(rphov) : "Hover a setup for its description", rpmx + 12, rpmy + rpmh - 13, rpmw - 24);
  menux(rpmx, rpmy, rpmw);
  mtxend();
);
// ---- source model menu ----
smenu ? (
  mtx(srcmx, srcmy, srcmk);
  col(cINK, 0.12); RR(srcmx + 1, srcmy + 3, srcmw, srcmh, 8);
  col(cSURF, 1); RR(srcmx, srcmy, srcmw, srcmh, 8); col(cBORD, 1); RRO(srcmx, srcmy, srcmw, srcmh, 8);
  k = 0;
  loop(13,
    iy = srcmy + 4 + k * srcih + (k > 0) * 5 + (k > 4) * 5;
    (k == 1 || k == 5) ? ( col(cLINE, 1); LN(srcmx + 8, iy - 2.5, srcmx + SMW1 - 6, iy - 2.5); );   // only the model column
    msel = k == fzsel; mhov = inbox(srcmx + 4, iy, SMW1 - 8, srcih);
    msel ? ( col(cTEXT, 1); RR(srcmx + 4, iy + 1, SMW1 - 8, srcih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(srcmx + 4, iy + 1, SMW1 - 8, srcih - 2, 5); ); );
    icOL = 1; srcicon(k, srcmx + 16, iy + 13.5, 0.46 * icfit(k), msel ? cSURF : cTEXT, msel ? cTEXT : (mhov ? cLINE : cSURF)); icOL = 0;
    col(msel ? cSURF : cTEXT, 1); gfx_setfont(2); txt(srcname(k), srcmx + 32, iy + srcih / 2, 0);
    mhov ? (
      strcpy(#stip, srcname(k)); strcat(#stip, "  ·  "); strcat(#stip, srcsub(k)); strcat(#stip, ", "); strcat(#stip, srcsub2(k));
      gfx_setfont(4); tipon = 1; tipstr = #stip; tipx = srcmx + srcmw + 8 + (tws(#stip) + 14) / 2; tipy = iy + srcih / 2;
    );
    k += 1;
  );
// PRESETS: the right column - a whole source in one click (model, distortion, call line)
  col(cLINE, 1); LN(srcmx + SMW1 + 1, srcmy + 8, srcmx + SMW1 + 1, srcmy + srcmh - 8);
  smPX = srcmx + SMW1 + 6; smPW = srcmw - SMW1 - 10;   // own names (spw = spectrum index of the audio thread, spx = geometry) - the bars flickered and ran over the menu
  k = 0;
  loop(NSP,
    iy = spry(k);
    (k == 0 || spTb[k * 8] != spTb[k * 8 - 8]) ? ( k > 0 ? ( col(cLINE, 1); LN(smPX + 4, iy - 14.5, smPX + smPW - 2, iy - 14.5); ); col(cMUTED, 1); gfx_setfont(7); txt(k == 0 ? "Presets  ·  Calls" : spcat(spTb[k * 8]), smPX + 6, iy - 6.5, 0); );
    msel = k == spcur; mhov = inbox(smPX, iy, smPW, srcih);
    msel ? ( col(cTEXT, 1); RR(smPX, iy + 1, smPW, srcih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(smPX, iy + 1, smPW, srcih - 2, 5); ); );
    icOL = 1; srcicon(spTb[k * 8 + 1], smPX + 12, iy + 13.5, 0.46 * icfit(spTb[k * 8 + 1]), msel ? cSURF : cTEXT, msel ? cTEXT : (mhov ? cLINE : cSURF)); icOL = 0;
    col(msel ? cSURF : cTEXT, 1); gfx_setfont(2); txt(spname(k), smPX + 28, iy + srcih / 2, 0);
    mhov ? ( strcpy(#stip, spname(k)); strcat(#stip, ":  "); strcat(#stip, spdesc(k)); gfx_setfont(4); tipon = 1; tipstr = #stip; tipx = srcmx + srcmw + 8 + (tws(#stip) + 14) / 2; tipy = iy + srcih / 2; );
    k += 1;
  );
  menux(srcmx, srcmy, srcmw);
  mtxend();
);
// ---- back-part material menu ----
kmenu ? (
  mtx(kmx, kmy, kmk);
  tipon = 0; kmsel = floor(slider46 + 0.5);
  col(cINK, 0.12); RR(kmx + 1, kmy + 3, kmw, kmh, 8);
  col(cSURF, 1); RR(kmx, kmy, kmw, kmh, 8); col(cBORD, 1); RRO(kmx, kmy, kmw, kmh, 8);
  k = 0;
  loop(10,
    iy = kmy + 4 + k * kmih; km = bkord(k);
    msel = km == kmsel; mhov = inbox(kmx + 4, iy, kmw - 8, kmih);
    msel ? ( col(cTEXT, 1); RR(kmx + 4, iy + 1, kmw - 8, kmih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(kmx + 4, iy + 1, kmw - 8, kmih - 2, 5); ); );
    col(msel ? cSURF : cTEXT, 1); gfx_setfont(6); txt(bkname(km), kmx + 12, iy + kmih / 2, 0);
    km > 0 ? ( sprintf(#w1, "α %.2f", g_bkA(km)); col(msel ? cSURF : cMUTED, 1); txt(#w1, kmx + kmw - 10, iy + kmih / 2, 2); );
    k += 1;
  );
  mtxend();
);
// ---- barrier material menu ----
bmenu ? (
  mtx(bmx, bmy, bmk);
  tipon = 0; btg = floor(slider41 + 0.5); bmsel = bmatsel();
  col(cINK, 0.12); RR(bmx + 1, bmy + 3, bmw, bmh, 8);
  col(cSURF, 1); RR(bmx, bmy, bmw, bmh, 8); col(cBORD, 1); RRO(bmx, bmy, bmw, bmh, 8);
  k = 0;
  loop(3,
    iy = bmy + 4 + k * bmih;
    msel = k == bmsel; mhov = inbox(bmx + 4, iy, bmw - 8, bmih);
    msel ? ( col(cTEXT, 1); RR(bmx + 4, iy + 1, bmw - 8, bmih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(bmx + 4, iy + 1, bmw - 8, bmih - 2, 5); ); );
    col(msel ? cSURF : cTEXT, 1); gfx_setfont(6); txt(bmatname(btg, k), bmx + 12, iy + bmih / 2, 0);
    sprintf(#w1, "%d dB", bmatrw(btg, k)); col(msel ? cSURF : cMUTED, 1); txt(#w1, bmx + bmw - 10, iy + bmih / 2, 2);
    mhov ? ( strcpy(#stip, bmattip(btg, k)); gfx_setfont(4); tipon = 1; tipstr = #stip; tipx = bmx - 8 - (tws(#stip) + 14) / 2; tipy = iy + bmih / 2; );
    k += 1;
  );
  mtxend();
);
// ---- walls menus: surface / material ----
wmenu ? (
  mtx(wmxx, wmy, wmk);
  tipon = 0;
  col(cINK, 0.12); RR(wmxx + 1, wmy + 3, wmw, wmh, 8);
  col(cSURF, 1); RR(wmxx, wmy, wmw, wmh, 8); col(cBORD, 1); RRO(wmxx, wmy, wmw, wmh, 8);
  k = 0;
  loop(wmn,
    iy = wmy + 4 + k * wmih;
    msel = wmenu == 1 ? (k == selw) : (k == wmsel); mhov = inbox(wmxx + 4, iy, wmw - 8, wmih);
    msel ? ( col(cTEXT, 1); RR(wmxx + 4, iy + 1, wmw - 8, wmih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(wmxx + 4, iy + 1, wmw - 8, wmih - 2, 5); ); );
    col(msel ? cSURF : cTEXT, 1); gfx_setfont(6); txt(wmenu == 1 ? wsurfname(k) : wmatname(k), wmxx + 12, iy + wmih / 2, 0);
    wmenu == 2 ? ( sprintf(#w1, "α %.2f", wmata(k)); col(msel ? cSURF : cMUTED, 1); txt(#w1, wmxx + wmw - 10, iy + wmih / 2, 2); );
    wmenu == 2 && mhov ? ( strcpy(#stip, wtip(wmattip(k), wmata(k))); gfx_setfont(4); tipon = 1; tipstr = #stip; tipx = wmxx - 8 - (tws(#stip) + 14) / 2; tipy = iy + wmih / 2; );
    k += 1;
  );
  mtxend();
);
// ---- source slope menus ----
emenu ? (
  mtx(emx, emy, emk);
  tipon = 0; fsel = floor((emenu == 4 ? slider115 : (emenu == 3 ? slider114 : (emenu == 1 ? slider58 : slider59))) + 0.5); emo = emenu == 1 ? fzBP[1] : fzBP[3];
  col(cINK, 0.12); RR(emx + 1, emy + 3, emw, emh, 8);
  col(cSURF, 1); RR(emx, emy, emw, emh, 8); col(cBORD, 1); RRO(emx, emy, emw, emh, 8);
  k = 0;
  loop(emenu >= 3 ? 4 : 9,
    iy = emy + 4 + k * eih;
    msel = k == fsel; mhov = inbox(emx + 4, iy, emw - 8, eih);
    msel ? ( col(cTEXT, 1); RR(emx + 4, iy + 1, emw - 8, eih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(emx + 4, iy + 1, emw - 8, eih - 2, 5); ); );
    emenu == 4 ? strcpy(#w1, k == 0 ? "Clean" : (k == 1 ? "Tape – darker, round" : (k == 2 ? "Analog (BBD) – dark, soft" : "Lo-Fi – 11 kHz, 10 bit"))) : ( emenu == 3 ? strcpy(#w1, lnmenu(k)) : (
    k == 0 ? ( emo > 0 ? sprintf(#w1, "Model · %d dB", emo * 6) : strcpy(#w1, "Model · off"); ) : sprintf(#w1, "%d dB / oct", k * 6); ); );
    col(msel ? cSURF : cTEXT, 1); gfx_setfont(2); txt(#w1, emx + 12, iy + eih / 2, 0);
    k += 1;
  );
  mtxend();
);
// ---- filter slope menus ----
fmenu ? (
  mtx((fmenu == 2 ? fmx2 : fmx1), fmy, fmk);
  tipon = 0; fmx = fmenu == 1 ? fmx1 : fmx2; fsel = floor((fmenu == 1 ? slider26 : slider29) + 0.5);
  col(cINK, 0.12); RR(fmx + 1, fmy + 3, fmw, fmh, 8);
  col(cSURF, 1); RR(fmx, fmy, fmw, fmh, 8); col(cBORD, 1); RRO(fmx, fmy, fmw, fmh, 8);
  k = 0;
  loop(8,
    iy = fmy + 4 + k * fih;
    msel = k == fsel; mhov = inbox(fmx + 4, iy, fmw - 8, fih);
    msel ? ( col(cTEXT, 1); RR(fmx + 4, iy + 1, fmw - 8, fih - 2, 5); ) : ( mhov ? ( col(cLINE, 1); RR(fmx + 4, iy + 1, fmw - 8, fih - 2, 5); ); );
    sprintf(#w1, "%d dB / oct", (k + 1) * 6);
    col(msel ? cSURF : cTEXT, 1); gfx_setfont(2); txt(#w1, fmx + 12, iy + fih / 2, 0);
    mhov ? ( strcpy(#stip, fsltip(k)); gfx_setfont(4); tipon = 1; tipforce = 0; tipstr = #stip; tipx = fmenu == 2 ? fmx - 8 - (tws(#stip) + 14) / 2 : fmx + fmw + 8 + (tws(#stip) + 14) / 2; tipy = iy + fih / 2; );   // tips in the slope menu (only with Tips on / Ctrl, like every menu)
    k += 1;
  );
  mtxend();
);
// ---- help page: source models with their frequency responses ----
helpon ? (
  // ---- SOURCE MODELS panel: floats over the page like the tempo tools (own scale mpk around its top left corner) ----
  mpin ? ( tipon = 0; );
  cfade = 1;
  hgx = gox; hgy = goy; hgs = gs;
  gs = hgs * mpk; gox = hgx + mpx * (hgs - gs); goy = hgy + mpy * (hgs - gs); setfonts();
  col(cINK, 0.16); RR(mpx + 2, mpy + 4, MPW, MPH, 8);
  col(cSURF, 1); RR(mpx, mpy, MPW, MPH, 8); col(cSRTT, 1); RRO(mpx, mpy, MPW, MPH, 8);
  col(cSRTT, 1); RR(mpx + 10, mpy + 8, 3, 12, 1.5); col(cTEXT, 1); gfx_setfont(2); txt("Source models", mpx + 19, mpy + 14, 0);
  col(cMUTED, 1); gfx_setfont(7); txt("click a model = use it  ·  drag here = move", mpx + MPW - 30, mpy + 14, 2);
  closex(mpx + MPW - 16, mpy + 15);
  !fzrespok ? ( fzcache(); fzrespok = 1; );
  k = 0;
  loop(13,
    hcx = mpx + 10 + (k % 5) * 87; hcy = mpy + 32 + floor(k / 5) * 72; hsel = k == fzsel; hhv = k == hphov && !hpxh;
    col(hsel ? cLINE : (hhv ? cBG : cSURF), 1); RR(hcx, hcy, 83, 68, 6);
    hsel ? ( col(cSRTT, 1); RRO(hcx, hcy, 83, 68, 6); RRO(hcx + 0.6, hcy + 0.6, 81.8, 66.8, 5.5); ) : ( col(hhv ? cMUTED : cBORD, 1); RRO(hcx, hcy, 83, 68, 6); );
    icOL = 1; srcicon(k, hcx + 14, hcy + 20, 0.6, cTEXT, hsel ? cLINE : (hhv ? cBG : cSURF)); icOL = 0;
    col(cTEXT, 1); gfx_setfont(6); txtfit(srcname(k), hcx + 28, hcy + 12, 52);
    col(cMUTED, 1); gfx_setfont(7); txtfit(srcsub(k), hcx + 28, hcy + 23, 52);
    hpx = hcx + 6; hpy = hcy + 33; hpw = 71; hph = 28;
    col(cLINE, 1); RR(hpx, hpy, hpw, hph, 3);
    col(cBORD, 1); LN(hpx, hpy + hph / 4, hpx + hpw, hpy + hph / 4);
    gi = 0; loop(3, fq = gi == 0 ? 100 : (gi == 1 ? 1000 : 10000); gx = hpx + log(fq / 40) / log(500) * hpw; LN(gx, hpy, gx, hpy + hph); gi += 1; );
    col(k == 0 ? cGLS : (darkmode ? 0x6FD49E : 0x1D9E75), 1);
    gi = 0;
    loop(47,
      y1 = hpy + (12 - min(12, max(-36, fzresp[k * 48 + gi]))) / 48 * hph;
      y2 = hpy + (12 - min(12, max(-36, fzresp[k * 48 + gi + 1]))) / 48 * hph;
      LNW(hpx + gi / 47 * hpw, y1, hpx + (gi + 1) / 47 * hpw, y2, 1.1);
      gi += 1;
    );
    k += 1;
  );
  // beside the last model: what Source uses now
  hux = mpx + 10 + 3 * 87 + 4; huy = mpy + 32 + 2 * 72;
  slider85 < 0.5 ? ( col(cMUTED, 1); gfx_setfont(6); txt("Source model is off", hux, huy + 18, 0);
                     gfx_setfont(7); txtfit("A click on a model switches it on.", hux, huy + 32, 160); )
                 : ( col(cMUTED, 1); gfx_setfont(4); txt("In use", hux, huy + 14, 0);
                     strcpy(#w1, srcname(fzsel)); eqed ? strcat(#w1, "  (EQ edited)"); col(cTEXT, 1); gfx_setfont(2); txtfit(#w1, hux, huy + 30, 160);
                     col(cMUTED, 1); gfx_setfont(7); txtfit("Curve: 40 Hz – 20 kHz, −36 … +12 dB", hux, huy + 46, 160); );
  gs = hgs; setfonts(); gox = hgx; goy = hgy; mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs; cfade = 1;
);
// ---- SNAPSHOT MENU (drawn last: above everything; its place was taken on screen at the right-click) ----
snctx ? (
  tipon = 0; tipforce = 0;
  hgx = gox; hgy = goy; hgs = gs; gs = snmS; gox = snqX; goy = snqY; setfonts();
  snmH = snmN * 18 + 4;
  col(cINK, 0.14); RR(2, 3, 210, snmH, 6);
  col(cSURF, 1); RR(0, 0, 210, snmH, 6); col(cBORD, 1); RRO(0, 0, 210, snmH, 6);
  k = 0;
  loop(snmN,
    snmHov == k ? ( col(cLINE, 1); RR(3, 3 + k * 18, 204, 16, 4); );
    col(snmN == 3 && k == 2 ? cIOTT : cTEXT, 1); gfx_setfont(6);
    txt(snmN == 3 ? (k == 0 ? "Rename …" : (k == 1 ? "Overwrite with the current settings" : "Clear this snapshot"))
                  : (k == 0 ? "Store the current settings here" : "Name and store …"), 10, 11 + k * 18, 0);
    k += 1;
  );
  gs = hgs; setfonts(); gox = hgx; goy = hgy; mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs;
);

// ---- section help panel ----
shclr ? shpin = 0;
shlast = 0;
shcur = (!zoomv && !helpon) ? (shsec ? shsec : shpin) : 0;
shcur != shprev ? ( shstart = time_precise(); shprev = shcur; );      // a new panel: fade it in
!zoomv && !helpon ? (
  cfade = min(1, (time_precise() - shstart) / FADET);
  // a zoomed section: the help panel at the normal scale of the page (inside the window, readable), under its "?" on screen
  shcur && szoom ? (
    shqx = gox + (shsec ? shax : shbx) * gs; shqy = goy + (shsec ? shay : shby) * gs;
    gs = gsN; gox = goxN; goy = goyN; WT = WTN; HT = HTN; setfonts();
    shqx = (shqx - gox) / gs; shqy = (shqy - goy) / gs;
    shsec ? ( tipon = 0; drawsechelp(shsec, shqx, shqy); ) : drawsechelp(shpin, shqx, shqy); shlast = 1;
    gs = gsZ; gox = floor((gfx_w - zw * gsZ) / 2); goy = floor(70 * gsN + (gfx_h - 74 * gsN - zh * gsZ) / 2); WT = (gfx_w - gox) / gs; HT = (gfx_h - goy) / gs;
    setfonts(); mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs;
  ) : (
  shsec ? ( tipon = 0; drawsechelp(shsec, shax, shay); shlast = 1; ) : ( shpin ? ( drawsechelp(shpin, shbx, shby); shlast = 1; ); );
  );
  cfade = 1;
);
// ---- column divider: a line while the mouse is over the gap or drags it ----
// grips: a small handle in each gap that can be dragged (column divider, bottom edges); the one under the mouse is lit
!basic && rzon ? (
  (colhit || coldrag) ? (
    col(cRMTT, 0.9); LNW(R0 - 3, TOPY, R0 - 3, LH - 22, 2);
    tipbox(R0 - 8, TOPY, 10, LH - TOPY - 18, "Drag: one column wider, the other narrower – double-click = standard widths");
  ) : (
    gcy = scy + (min(LH, scy + HT) - scy) / 2; col(cDIM, 1); RR(R0 - 4.25, gcy - 14, 2.5, 28, 1.25);
  );
)
);
// the corner grip of one section (three short diagonals; lit under the mouse or while dragged)
function rcdraw(rid) (
  rcgeo(rid); rclit = rid == (rcid ? rcid : rch);
  col(rclit ? cTEXT : cDIM, 1);
  LNW(rcgx - 10, rcgy - 3, rcgx - 3, rcgy - 10, rclit ? 1.4 : 1); LNW(rcgx - 7, rcgy - 3, rcgx - 3, rcgy - 7, rclit ? 1.4 : 1); LNW(rcgx - 4, rcgy - 3, rcgx - 3, rcgy - 4, rclit ? 1.4 : 1);
);
// Draws corner resize grips plus the My rooms save dialog and the snapshot name/description popup
function gfxp14()
(
rzon ? (
  k = 0;
  loop(basic ? 2 : 8,
    rid = basic ? 21 + k : (k == 0 ? 9 : (k == 1 ? 6 : (k == 2 ? 3 : (k == 3 ? 10 : (k == 4 ? 1 : (k == 5 ? 7 : (k == 6 ? 4 : 5)))))));
    rzok(rid) ? (
      rzgeo(rid); rlit = !rch && !rcid && rid == (rzid ? rzid : rzh); gcx = rzgx + rzgw / 2;
      rlit ? (
        col(rid == 9 || rid == 21 ? cRMTT : cTEXT, 0.9); LNW(rzgx + 8, rzgy + 3, rzgx + rzgw - 8, rzgy + 3, 2);
        tipbox(rzgx + 4, rzgy - 3, rzgw - 8, 8, rid == 9 || rid == 21 ? "Drag: height of the Stage View – double-click = automatic (fills the window)"
                                                     : (rid == 22 ? "Drag: height of the four sections – double-click = standard height" : "Drag: height of this section – double-click = standard height"));
      ) : (
        col(cDIM, 1); RR(gcx - 14, rzgy + 1.75, 28, 2.5, 1.25);
      );
    );
    k += 1;
  );
);
// corner grips: three short diagonals in the lower right corner of every section that can be resized
rzon ? (
  k = 0;
  loop(basic ? 4 : 8,
    rid = basic ? 41 + k : (k == 0 ? 9 : (k == 1 ? 6 : (k == 2 ? 3 : (k == 3 ? 10 : (k == 4 ? 1 : (k == 5 ? 7 : (k == 6 ? 4 : 5)))))));
    rzok(basic ? 22 : rid) ? (
      rcdraw(rid);
      rclit && !rcid ? tipbox(rcgx - 11, rcgy - 11, 13, 15, basic ? "Drag the corner: the cards wider (the Stage View narrower) and taller – double-click = standard size"
                                                                     : (rid == 6 ? "Drag the corner: Filter wider (Source narrower) and taller – double-click = standard size" : "Drag the corner: width of the column and height of this section – double-click = standard size"));
    );
    k += 1;
  );
);
// ---- MY ROOMS save dialog (top-bar scale) ----
rsave ? (
  tipon = 0; tipforce = 0;
  gs = gsN; gox = goxN; goy = goyN; WT = WTN; setfonts(); mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs;
  rs_geo();
  col(cINK, 0.10); R(0, 0, WTN, 2000);
  col(cINK, 0.16); RR(rspx + 1, rspy + 4, rspw, rsph, 9);
  col(cSURF, 1); RR(rspx, rspy, rspw, rsph, 9); col(cBORD, 1); RRO(rspx, rspy, rspw, rsph, 9);
  col(cTEXT, 1); gfx_setfont(2); txt(rsren ? "Rename room" : "Save room", rspx + 12, rspy + 15, 0);
  col(cMUTED, 1); gfx_setfont(7); txt("Enter = save   Esc = cancel", rspx + rspw - 12, rspy + 15, 2);
  rsex = !rsren && urfind(rsnb) >= 0; rsclash = rsren && urfind(rsnb) >= 0 && urfind(rsnb) != rsren - 1;
  col(cTEXT, 1); gfx_setfont(6); txt("Name", rsfx, rsny - 8, 0);
  rsex ? ( col(cAMB, 1); gfx_setfont(7); txt("a room with this name is replaced", rsfx + rsfw, rsny - 8, 2); );
  rsclash ? ( col(cIOTT, 1); gfx_setfont(7); txt("another room has this name", rsfx + rsfw, rsny - 8, 2); );
  col(cBG, 1); RR(rsfx, rsny, rsfw, 24, 4); col(cRMTT, 1); RRO(rsfx, rsny, rsfw, 24, 4);
  sn_strn(rsnb, 24, #rsn); gfx_setfont(2); col(cTEXT, 1); txt(#rsn, rsfx + 7, rsny + 12, 0);
  (floor((time_precise() - rst0) * 2) & 1) == 0 ? ( rsc = rsfx + 8 + tws(#rsn); LNW(rsc, rsny + 6, rsc, rsny + 18, 1.2); );
  col(cTEXT, 1); gfx_setfont(6); txt("Category", rsfx, rscy - 8, 0);
  k = 0;
  loop(7,
    rcx0 = rsfx + (k % 4) * (rsfw + 4) / 4; rcy0 = rscy + floor(k / 4) * 20;
    chip(rcx0, rcy0, (rsfw + 4) / 4 - 4, 16, urcatname(k), rscat == k, cRMTT);
    k += 1;
  );
  col(cMUTED, 1); gfx_setfont(7);
  txt("Stored with this project. For every project: FX window + → Save preset / Save as default.", rsfx, rsby - 12, 0);
  rsfull = !rsex && urcount() >= URM;
  col(rsnlen > 0 && !rsfull && !rsclash ? cTEXT : cDIM, 1); RR(rspx + 12, rsby, 76, 18, 4); col(cSURF, 1); gfx_setfont(6); txt(rsren ? "Rename" : (rsex ? "Replace" : "Save"), rspx + 50, rsby + 9, 1);
  rsfull ? ( col(cIOTT, 1); gfx_setfont(7); txt("My rooms is full (24) – delete one in the room menu first", rspx + 96, rsby + 9, 0); );
  col(cSURF, 1); RR(rspx + rspw - 76, rsby, 64, 18, 4); col(cBORD, 1); RRO(rspx + rspw - 76, rsby, 64, 18, 4); col(cMUTED, 1); txt("Cancel", rspx + rspw - 44, rsby + 9, 1);
);
// ---- snapshot name popup (top-bar scale, also in section zoom) ----
snedit ? (
  tipon = 0; tipforce = 0;
  gs = gsN; gox = goxN; goy = goyN; WT = WTN; setfonts(); mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs;
  snk = snedit - 1; sn_geo();
  col(cINK, 0.10); R(0, 0, WTN, 2000);                                                  // dim the plugin behind the dialog
  col(cINK, 0.16); RR(snpx + 1, snpy + 4, SNPW, SNPH, 9);
  col(cSURF, 1); RR(snpx, snpy, SNPW, SNPH, 9); col(cBORD, 1); RRO(snpx, snpy, SNPW, SNPH, 9);
  sprintf(#snh, "Snapshot %d", snk + 1); col(cTEXT, 1); gfx_setfont(2); txt(#snh, snpx + 12, snpy + 15, 0); snhw = tws(#snh);
  col(cMUTED, 1); gfx_setfont(7); txt(uslot[snk * USL] ? "stored" : "empty", snpx + 18 + snhw, snpy + 15, 0);
  txt("Tab = next field   Enter = save   Esc = cancel", snpx + SNPW - 12, snpy + 15, 2);
  // NAME
  col(snf == 0 ? cTEXT : cMUTED, 1); gfx_setfont(6); txt("Name", snfx, snny - 8, 0);
  sprintf(#snc, "%d / %d", snelen, SNL - 1); col(cDIM2, 1); gfx_setfont(7); txt(#snc, snfx + snfw, snny - 8, 2);
  col(cBG, 1); RR(snfx, snny, snfw, snnh, 4); col(snf == 0 ? cRMTT : cBORD, 1); RRO(snfx, snny, snfw, snnh, 4);
  sn_str(sneb, #sne); gfx_setfont(2); col(cTEXT, 1); txt(#sne, snfx + 7, snny + snnh / 2, 0);
  snf == 0 && (floor((time_precise() - snt0) * 2) & 1) == 0 ? ( snc = snfx + 8 + tws(#sne); col(cTEXT, 1); LNW(snc, snny + 6, snc, snny + snnh - 6, 1.2); );
  // DESCRIPTION (word-wrapped, 3 lines)
  col(snf == 1 ? cTEXT : cMUTED, 1); gfx_setfont(6); txt("Description", snfx, sndy - 8, 0);
  sprintf(#snc, "%d / %d", dselen, DSL - 1); col(cDIM2, 1); gfx_setfont(7); txt(#snc, snfx + snfw, sndy - 8, 2);
  col(cBG, 1); RR(snfx, sndy, snfw, sndh, 4); col(snf == 1 ? cRMTT : cBORD, 1); RRO(snfx, sndy, snfw, sndh, 4);
  gfx_setfont(4); col(cTEXT, 1); sn_wrap(dseb, dselen, snfx + 7, sndy + 11, snfw - 16, 15, 3);
  dselen == 0 && snf != 1 ? ( col(cDIM2, 1); txt("e.g. scene, mic position, what changes in this room", snfx + 7, sndy + 11, 0); );
  snf == 1 && (floor((time_precise() - snt0) * 2) & 1) == 0 ? ( col(cTEXT, 1); LNW(snCx + 1, snCy - 6, snCx + 1, snCy + 6, 1.2); );
  // buttons: Save (texts + all settings), Rename (texts only, filled slots), Cancel
  col(cTEXT, 1); RR(snpx + 12, snby, 64, 18, 4); col(cSURF, 1); gfx_setfont(6); txt("Save", snpx + 44, snby + 9, 1);
  uslot[snk * USL] ? ( col(cSURF, 1); RR(snpx + 82, snby, 64, 18, 4); col(cBORD, 1); RRO(snpx + 82, snby, 64, 18, 4); col(cTEXT, 1); txt("Rename", snpx + 114, snby + 9, 1); );
  col(cSURF, 1); RR(snpx + SNPW - 76, snby, 64, 18, 4); col(cBORD, 1); RRO(snpx + SNPW - 76, snby, 64, 18, 4); col(cMUTED, 1); txt("Cancel", snpx + SNPW - 44, snby + 9, 1);
  tipbox(snpx + 12, snby, 64, 18, "Enter: store name, description and all current settings in this snapshot");
  uslot[snk * USL] ? ( tipbox(snpx + 82, snby, 64, 18, "Only change name and description – the stored settings stay as they are"); );
  tipbox(snpx + SNPW - 76, snby, 64, 18, "Esc: close without changes");
);
// ---- EXPERT SETTINGS ACTIVE (Basic): the list under the plaque - Reset to Basic / Open in Expert ----
xpon && basic ? (
  tipon = 0; click = pclick;
  xpw2 = 300; xph = 64 + xpn * 20 + 42; xppx = min(max(4, xpbx + pdXX), WT - xpw2 - 4); xppy = min(max(scy + 4, fsY + fsH + 4 + pdXY), scy + HT - xph - 4);   // under its sign in the strip
  click && inbox(xppx, xppy, xpw2 - 30, 26) ? ( pdrag = 4; pdm0x = mouse_x; pdm0y = mouse_y; pd0x = pdXX; pd0y = pdXY; click = 0; );
  col(cINK, 0.12); RR(xppx + 2, xppy + 4, xpw2, xph, 8);
  col(cSURF, 1); RR(xppx, xppy, xpw2, xph, 8); col(cBORD, 1); RRO(xppx, xppy, xpw2, xph, 8);
  col(cTEXT, 1); gfx_setfont(2); txt("Expert settings active", xppx + 12, xppy + 15, 0);
  col(cMUTED, 1); gfx_setfont(7); txt("They change the sound but are hidden in Basic.", xppx + 12, xppy + 31, 0);
  closex(xppx + xpw2 - 18, xppy + 16);
  k = 0; xi = 0;
  loop(16,
    xpf[k] ? (
      iy = xppy + 54 + xi * 20;
      k == 0 ? ( gdtm ? sprintf(#w1, "on · L %s · R %s", dnname(slider72), dnname(gdlt ? slider72 : slider73)) : sprintf(#w1, "on · L %.0f · R %.0f ms", gtL, gtR); ) :
      k == 1 ? ( slider24 >= 0.5 && slider27 >= 0.5 ? sprintf(#w1, "%.0f Hz – %.1f kHz", slider25, slider28 / 1000) : (slider24 >= 0.5 ? sprintf(#w1, "low cut %.0f Hz", slider25) : sprintf(#w1, "high cut %.1f kHz", slider28 / 1000)); strcat(#w1, slider30 >= 0.5 ? " (post)" : " (pre)"); ) :
      k == 2 ? ( strcpy(#w1, "mixed materials"); ) :
      k == 3 ? ( strcpy(#w1, "edited"); ) :
      k == 4 ? ( sprintf(#w1, "%.0f %%", slider62); ) :
      k == 5 ? ( sprintf(#w1, "%+.1f / %+.1f dB", slider20, slider21); ) :
      k == 6 ? ( slider63 < 0.5 ? strcpy(#w1, "aims at the listener") : sprintf(#w1, "%.0f°", slider61); ) :
      k == 7 ? ( slider16 < 0.5 ? strcpy(#w1, "turns to the source") : sprintf(#w1, "%.0f°", slider17); ) :
      k == 8 ? ( strcpy(#w1, "no floor / ceiling reflections"); ) :
      k == 9 ? ( slider46 >= 0.5 && abs(slider44 - bmatrw(btq, 1)) <= 0.05 ? strcpy(#w1, "back part material") : sprintf(#w1, "damping %.0f dB", slider44); ) :
      k == 10 ? ( sprintf(#w1, "%+.1f dB", slider40); ) :
      k == 11 ? ( slider60 >= 0.5 && slider23 < 0.5 ? strcpy(#w1, "equal loud on, limiter off") : (slider60 >= 0.5 ? strcpy(#w1, "equal loudness on") : strcpy(#w1, "limiter off")); ) :
      k == 12 ? ( sprintf(#w1, "%.2f", slider64); ) :
      k == 13 ? ( strcpy(#w1, "direct / early / late muted"); ) :
      k == 15 ? ( mtstat(); strcpy(#w1, #sts); ) :
      ( strcpy(#w1, ""); i = 88;
        loop(5, slider(i) < 99.95 ? ( strlen(#w1) ? strcat(#w1, " · ");
          strcat(#w1, i == 88 ? "filter " : (i == 89 ? "source " : (i == 90 ? "delay " : (i == 91 ? "room " : "barrier ")))); sprintf(#w2, "%.0f %%", slider(i)); strcat(#w1, #w2); ); i += 1; ); );
      col(cAMB, 1); CI(xppx + 16, iy, 2.6, 1);
      col(cTEXT, 1); gfx_setfont(6); txt(xpname(k), xppx + 26, iy, 0);
      col(cMUTED, 1); gfx_setfont(4); txt(#w1, xppx + xpw2 - 12, iy, 2);
      col(cHAIR, 1); LN(xppx + 12, iy + 10, xppx + xpw2 - 12, iy + 10);
      xi += 1;
    );
    k += 1;
  );
  xby = xppy + xph - 36;
  chip(xppx + 12, xby, 134, 20, "Reset to Basic", 1, cAMB) ? (
    snap_take();
    slider65 = 0; slider24 = 0; slider27 = 0;
    xpf[2] ? ( xav = (slider5 + slider6 + slider7 + slider8) / 4; slider5 = slider6 = slider7 = slider8 = xav; );   // one material for all walls
    // Source EQ offsets back to zero
    i = 47; loop(13, sset(i, 0); i += 1; );
    slider62 = 100; slider20 = 0; slider21 = 0;                    // (predelay stays: a Basic control)
    slider61 = 0; slider63 = 1; slider16 = 1; slider17 = 0; slider19 = 1;
    btq > 0 ? ( slider44 = bmatrw(btq, 1); ); slider46 = 0;
    slider40 = 0; slider60 = 0; slider23 = 1; slider64 = 0; slider100 = 0;
    // the five module mix sliders (88 … 92) back to 100 %
    i = 88; loop(5, sset(i, 100); i += 1; );
    vopt[18] = 0;
    snap_notify(); xpon = 0;
  );
  chip(xppx + xpw2 - 146, xby, 134, 20, "Open in Expert", 0, cTEXT) ? ( vopt[11] = 0; xpon = 0; );
  col(cMUTED, 1); gfx_setfont(7); txt("Reset to Basic keeps room, positions and source model", xppx + xpw2 / 2, xph + xppy - 6, 1);
  click && (!inbox(xppx, xppy, xpw2, xph) || inbox(xppx + xpw2 - 28, xppy + 6, 20, 20)) ? ( xpon = 0; );   // outside or × = close
  click = 0;
)
);
// Tempo tools panel (movable): tap tempo, analyze BPM, note values, and note table in ms; sets delay or motion time
function gfxp15()
(
// ---- TEMPO TOOLS (Expert): a small panel beside the delay - the plugin stays usable, every delay knob can be changed while
// it is open. Tap, analysis, and the note values: a click on a note value sets the delay at once (Time -> Tempo) ----
tpvis ? (
  tpin ? ( tipon = 0; );
  click = pclick;
  // its own scale tpk around its top left corner (panel units = the design units of the 330 x 312 panel)
  gsS = gs; goxS = gox; goyS = goy; mxS = mx; myS = my;
  gs = gsS * tpk; gox = goxS + tpx * (gsS - gs); goy = goyS + tpy * (gsS - gs);
  setfonts(); mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs;
  tpx0 = tpx; tpy0 = tpy;
  tpSX0 = gox + tpx0 * gs; tpSY0 = goy + tpy0 * gs; tpSX1 = gox + (tpx0 + TPW) * gs; tpSY1 = goy + (tpy0 + TPH) * gs; tpSOk = 1;   // on screen
  col(cINK, 0.16); RR(tpx0 + 2, tpy0 + 4, TPW, TPH, 8);
  col(cSURF, 1); RR(tpx0, tpy0, TPW, TPH, 8); col(cDLTT, 1); RRO(tpx0, tpy0, TPW, TPH, 8);
  col(tpctx == 14 ? cMOTT : cDLTT, 1); RR(tpx0 + 10, tpy0 + 7, 3, 12, 1.5); col(cTEXT, 1); gfx_setfont(2); txt(tpctx == 14 ? "Tempo tools · Motion" : "Tempo tools", tpx0 + 19, tpy0 + 13, 0);
  tpctx == 14 ? ( floor(slider108 + 0.5) > 0 ? sprintf(#w2, "motion: %s at %.1f BPM", mttempo(floor(slider108 + 0.5)), gbpm) : strcpy(#w2, "motion: Hz – pick a note") ) :
  gdtm ? sprintf(#w2, slider74 > 0.5 ? "delay: %.1f BPM" : (tempo > 1 ? "delay: project %.1f BPM" : "delay: %.0f BPM"), gbpm)
       : sprintf(#w2, "delay: %.0f / %.0f ms", gtL, gtR);
  gfx_setfont(7); tbw = tws(#w2) + 10; tbx = tpx0 + TPW - 34 - tbw + 4; tbhit = inbox(tbx, tpy0 + 6, tbw, 14);
  tbhit || vedit == 79 ? ( col(cLINE, 1); RR(tbx, tpy0 + 6, tbw, 14, 4); );
  col(tbhit ? cTEXT : cMUTED, 1); gfx_setfont(7); txt(#w2, tpx0 + TPW - 26, tpy0 + 13, 2);
  // the tempo, typed: click on it - the delay takes it (note values stay; from ms: the nearest ones)
  kmap(0, 20, 300, 1);
  click && tbhit ? ( sprintf(#bpv, "%.1f", gbpm); vedopen(79, #bpv); click = 0; );
  vedit == 79 ? vedplace(tbx + tbw / 2, tpy0 + 13, 60);
  vedPend && vedId == 79 ? ( bpn = min(300, max(20, 20 + vedmap(vedVal) * 280)); vedPend = 0; tpapply(-1, floor(bpn * 100 + 0.5) / 100); );
  tbhit ? ( tipon = 1; tipforce = 0; tipstr = "Click = type the tempo in BPM (20 … 300)"; tipx = tbx + tbw / 2; tipy = tpy0 + 28; );
  tcx = tpx0 + TPW - 16; tch = closex(tcx, tpy0 + 14);   // the round close button of every panel
  click && tch ? ( tpon = 0; );
  // ---- TAP (left) ----
  tlx = tpx0 + 10; tlw = 150; tnow4 = time_precise();
  click && inbox(tlx, tpy0 + 26, tlw, 50) ? (
    tnow4 - tapLast > 2 || tapN < 1 ? ( tapN = 1; tap0 = tnow4; ) : ( tapN += 1; );
    tapLast = tnow4;
    tapN >= 2 ? ( tapBPM = floor(min(300, max(20, 60 * (tapN - 1) / max(0.05, tapLast - tap0))) + 0.5); );   // whole BPM: a click is timed to the GUI frame (about 30 ms) - decimals would only pretend
  );
  tfl = max(0, 1 - (tnow4 - tapLast) / 0.15);
  col(cDLTT, 1); RR(tlx, tpy0 + 26, tlw, 50, 8); tfl > 0 ? ( col(cSURF, 0.35 * tfl); RR(tlx, tpy0 + 26, tlw, 50, 8); );
  col(cSURF, 1); gfx_setfont(5); txt("TAP", tlx + tlw / 2, tpy0 + 45, 1);
  tapN >= 1 && tnow4 - tapLast <= 2 ? sprintf(#w1, "%d taps · more = more exact", tapN) : strcpy(#w1, "click in time · 8 taps or more");
  gfx_setfont(7); txt(#w1, tlx + tlw / 2, tpy0 + 64, 1);
  tapok = tapN >= 2 && tapBPM > 0;
  tapok ? sprintf(#w1, "%.0f BPM", tapBPM) : strcpy(#w1, "– BPM");
  col(tapok ? cTEXT : cDIM2, 1); gfx_setfont(2); txt(#w1, tlx, tpy0 + 90, 0);
  chip(tlx + tlw - 44, tpy0 + 83, 44, 14, "Use", 0, tapok ? cDLTT : cDIM2) && tapok ? ( tpuse(gdtm ? -1 : tnc(tnsel), tapBPM); );
  tipbox(tlx + tlw - 44, tpy0 + 83, 44, 14, "Use the tapped tempo: the delay keeps its note values and plays them at this tempo (in ms mode: the note value chosen below)");
  col(cMUTED, 1); gfx_setfont(7); txt("best with playback stopped", tlx, tpy0 + 106, 0);
  // ---- ANALYZE (right) ----
  arx = tpx0 + 170; arw = 150;
  anaDone == 1 && anaRefId != anaSeen && !anaOn ? ( anaest(); anaRefId = anaSeen; );   // once per analysis
  anaok = anaDone == 1 && anaRes == 1 && anaRefId == anaSeen;
  anaReq = anaCmd != anaSeen; anaRun = anaOn || anaReq;
  chip(arx, tpy0 + 26, arw, 16, anaRun ? "Stop listening" : "Listen to the song", anaRun, cDLTT) ? ( anaRun ? ( anaStp += 1; ) : ( anaCmd += 1; anaSel = 0; ); );
  tipbox(arx, tpy0 + 26, arw, 16, "Click, then play the selected clip or the time selection – Roomservice listens for 10 s (a JSFX cannot read files directly)");
  col(cHAIR, 1); RR(arx, tpy0 + 46, arw, 3, 1.5); anaN > 0 ? ( col(cDLTT, 1); RR(arx, tpy0 + 46, max(3, arw * anaN / ANAMAX), 3, 1.5); );
  col(cBG, 1); RR(arx, tpy0 + 52, arw, 13, 3);
  anaN > 4 ? (
    // onset-strength strip: anaN values condensed into 74 columns of 2 px, each showing the peak of its slice, scaled to the loudest value
    omx = 0.000001; i = 0; loop(anaN, omx = max(omx, anaNv[i]); i += 1; );
    ob = 0;
    loop(74,
      oi0 = floor(ob * ANAMAX / 74); oi1 = min(anaN, floor((ob + 1) * ANAMAX / 74)); onv = 0;
      i = oi0; loop(max(0, oi1 - oi0), onv = max(onv, anaNv[i]); i += 1; );
      onv > 0 ? ( oh = 10 * onv / omx; col(oh > 5 ? cTEXT : cDIM2, 1); LN(arx + 2 + ob * 2, tpy0 + 63, arx + 2 + ob * 2, tpy0 + 63 - oh); );
      ob += 1;
    );
  );
  anaRun ? ( (play_state & 1) && anaOn ? sprintf(#w1, "listening %.1f s of 10 s", anaN / 100) : strcpy(#w1, "waiting – start playback") )
         : ( anaok ? sprintf(#w1, "done · %d onsets", anaOns) : (anaDone == -1 || (anaDone == 1 && anaRes == -1) ? strcpy(#w1, "no clear beat – try a rhythmic part") : strcpy(#w1, "not started")) );
  col(cMUTED, 1); gfx_setfont(7); txtfit(#w1, arx, tpy0 + 73, arw);
  anaok ? (
    selb = anaSel == 1 ? anaBf / 2 : (anaSel == 2 ? anaBf * 2 : anaBf);
    sprintf(#w1, "%.1f BPM", selb); col(cTEXT, 1); gfx_setfont(2); txt(#w1, arx, tpy0 + 88, 0);
    col(cMUTED, 1); gfx_setfont(7); txt(anaConf >= 4.5 ? "sure" : (anaConf >= 3 ? "fairly sure" : "unsure"), arx + arw, tpy0 + 88, 2);
    tcw = (arw - 6) / 3;
    sprintf(#w1, "%.1f", anaBf); chip(arx, tpy0 + 97, tcw, 13, #w1, anaSel == 0, cTEXT) ? ( anaSel = 0; );
    sprintf(#w1, "½ %.1f", anaBf / 2); chip(arx + tcw + 3, tpy0 + 97, tcw, 13, #w1, anaSel == 1, cTEXT) ? ( anaSel = 1; );
    anaBf * 2 <= 300 ? ( sprintf(#w1, "2× %.1f", anaBf * 2); chip(arx + 2 * tcw + 6, tpy0 + 97, tcw, 13, #w1, anaSel == 2, cTEXT) ? ( anaSel = 2; ); );
    tipbox(arx, tpy0 + 97, arw, 13, "The analysis can land on half or double the tempo – choose the one that feels right");
  ) : ( col(cDIM2, 1); gfx_setfont(2); txt("– BPM", arx, tpy0 + 88, 0); );
  chip(arx, tpy0 + 115, 72, 15, "Use", 0, anaok ? cDLTT : cDIM2) && anaok ? ( tpuse(gdtm ? -1 : tnc(tnsel), floor(min(300, selb) * 100 + 0.5) / 100); );
  tipbox(arx, tpy0 + 115, 72, 15, "Use the analyzed tempo: the delay keeps its note values and plays them at this tempo (in ms mode: the note value chosen below)");
  chip(arx + 76, tpy0 + 115, arw - 76, 15, "Project tempo", slider74 < 0.5 && gdtm, cTEXT) ? ( tpapply(gdtm ? -1 : tnc(tnsel), 0); );
  tipbox(arx + 76, tpy0 + 115, arw - 76, 15, "Use the tempo of the REAPER project: the delay follows it, also when the project tempo changes");
  col(cHAIR, 1); LN(tpx0 + 10, tpy0 + 138, tpx0 + TPW - 10, tpy0 + 138);
  // ---- NOTE VALUE: a click sets the delay at once ----
  col(cMUTED, 1); gfx_setfont(4); txt("Note", tpx0 + 10, tpy0 + 154, 0);
  tnw = (TPW - 50 - 5 * 3) / 6; tcur = gdtm ? floor((tptg == 1 ? slider73 : slider72) + 0.5) : -1;
  k = 0;
  loop(6,
    tnx = tpx0 + 40 + k * (tnw + 3);
    chip(tnx, tpy0 + 147, tnw, 15, k == 0 ? "1/4" : (k == 1 ? "1/8" : (k == 2 ? "1/8 ." : (k == 3 ? "1/8 T" : (k == 4 ? "1/16" : "1/16 .")))), tpctx == 14 ? tnc(k) == mtcode(floor(slider108 + 0.5)) : tnc(k) == tcur, tpctx == 14 ? cMOTT : cDLTT) ? ( tpctx == 14 ? tpmot(tnc(k)) : ( tnsel = k; tpapply(tnc(k), -1); ); );
    sprintf(#tpt, "%s: %.0f ms at %.1f BPM – click: the delay plays it now", tnname(k), g_dnote(tnc(k)) * 60000 / gbpm, gbpm); tipbox(tnx, tpy0 + 147, tnw, 15, #tpt);
    k += 1;
  );
  col(cMUTED, 1); gfx_setfont(4); txt("To", tpx0 + 10, tpy0 + 176, 0);
  chip(tpx0 + 40, tpy0 + 169, 34, 14, "L", tptg == 0, cSRC) ? ( tptg = 0; );
  chip(tpx0 + 77, tpy0 + 169, 34, 14, "R", tptg == 1, cLIS) ? ( tptg = 1; );
  chip(tpx0 + 114, tpy0 + 169, 40, 14, "L + R", tptg == 2, cTEXT) ? ( tptg = 2; );
  tipbox(tpx0 + 40, tpy0 + 169, 114, 14, "Which side the note value goes to");
  gdtm ? sprintf(#w1, "L %s · R %s = %.0f / %.0f ms", dnname(slider72), dnname(gdlt ? slider72 : slider73), gtL, gtR) : strcpy(#w1, "Time in ms – a note value switches to Tempo");
  col(cMUTED, 1); gfx_setfont(7); txtfit(#w1, tpx0 + 162, tpy0 + 176, TPW - 172);
  // ---- NOTE TABLE: every note value at this tempo in ms, straight / dotted / triplet, and their lengths as bars;
  // a click on a time sets it (to the side chosen above) ----
  col(cHAIR, 1); LN(tpx0 + 10, tpy0 + 190, tpx0 + TPW - 10, tpy0 + 190);
  sprintf(#w1, "Note values at %.1f BPM", gbpm); col(cTEXT, 1); gfx_setfont(4); txt(#w1, tpx0 + 10, tpy0 + 203, 0);
  col(cMUTED, 1); gfx_setfont(7); txt("click a time = use it", tpx0 + TPW - 10, tpy0 + 203, 2);
  ty0 = tpy0 + 220;
  col(cMUTED, 1); gfx_setfont(7);
  txt("straight", tpx0 + 50 + 30, ty0, 1); txt("dotted", tpx0 + 50 + 66 + 30, ty0, 1); txt("triplet", tpx0 + 50 + 132 + 30, ty0, 1); txt("length", tpx0 + 254, ty0, 0);
  tbase = 60000 / max(1, gbpm); tsel = gdtm ? floor((tptg == 1 ? slider73 : slider72) + 0.5) : -1;
  k = 0;
  loop(5,
    tnry = ty0 + 15 + k * 17; tbt = 2 / (2 ^ k);                   // beats: 1/2 = 2 … 1/32 = 0.125
    col(cTEXT, 1); gfx_setfont(6); txt(k == 0 ? "1/2" : (k == 1 ? "1/4" : (k == 2 ? "1/8" : (k == 3 ? "1/16" : "1/32"))), tpx0 + 12, tnry, 0);
    j = 0;
    loop(3,
      tms = tbt * tbase * (j == 1 ? 1.5 : (j == 2 ? 2 / 3 : 1));
      // tncd holds the delay note index (list of slider72) for row k: straight / dotted / triplet; -1 means no such note value
      tncd = j == 0 ? (k == 0 ? 2 : (k == 1 ? 5 : (k == 2 ? 8 : (k == 3 ? 11 : 12)))) : (j == 1 ? (k == 0 ? 1 : (k == 1 ? 3 : (k == 2 ? 6 : (k == 3 ? 9 : -1))))
                                                                                             : (k == 0 ? 4 : (k == 1 ? 7 : (k == 2 ? 10 : -1))));
      sprintf(#tpt, tms < 100 ? "%.1f ms" : "%.0f ms", tms);
      tncx = tpx0 + 50 + j * 66;
      tncd >= 0 ? (
        chip(tncx, tnry - 7, 60, 14, #tpt, tpctx == 14 ? tncd == mtcode(floor(slider108 + 0.5)) : tncd == tsel, tpctx == 14 ? cMOTT : cDLTT) ? ( tpctx == 14 ? tpmot(tncd) : tpapply(tncd, -1); );
      ) : ( col(cDIM2, 1); gfx_setfont(6); txt(#tpt, tncx + 30, tnry, 1); );
      j += 1;
    );
    // the lengths: triplet (tick), straight (bar), dotted (lighter part) - scaled to the dotted half note
    tgx = tpx0 + 254; tgw = TPW - 264; tsc = tgw / 3;
    col(cHAIR, 1); RR(tgx, tnry - 3, tgw, 6, 3);
    col(cDLTT, 0.35); RR(tgx, tnry - 3, max(2, tsc * tbt * 1.5), 6, 3);
    col(cDLTT, 1); RR(tgx, tnry - 3, max(2, tsc * tbt), 6, 3);
    col(cTEXT, 1); LNW(tgx + tsc * tbt * 2 / 3, tnry - 5, tgx + tsc * tbt * 2 / 3, tnry + 5, 1.2);
    k += 1;
  );
  click = 0;
  tipon && tpin ? ( tipx = tpx + (tipx - tpx) * tpk; tipy = tpy + (tipy - tpy) * tpk; );   // tooltip back to the page scale
  gs = gsS; gox = goxS; goy = goyS; setfonts(); mx = mxS; my = myS;
);
// ---- CLOSE X of a zoom window (section zoom, room view zoom): a round button on its top-right corner, top-bar scale ----
zxbOn && ((szoom && !tabm) || zoomv) && !rsave && !snedit && !snctx && !hlpon && !kovl ? (
  zxbR = max(10, 10 * gsN); zxbX = min(gfx_w - zxbR - 3, zxbX - 4 * gsN); zxbY = zxbY + 4 * gsN;
  zxbH = sqr(mouse_x - zxbX) + sqr(mouse_y - zxbY) <= sqr(zxbR + 1);
  col(zxbH ? cTEXT : cSURF, 1); gfx_circle(zxbX, zxbY, zxbR, 1, 1);
  col(zxbH ? cTEXT : cBORD, 1); gfx_circle(zxbX, zxbY, zxbR, 0, 1);
  col(zxbH ? cSURF : cTEXT, 1); zxk = zxbR * 0.42;
  gfx_line(zxbX - zxk, zxbY - zxk, zxbX + zxk, zxbY + zxk, 1); gfx_line(zxbX - zxk, zxbY + zxk, zxbX + zxk, zxbY - zxk, 1);
  gfx_line(zxbX - zxk + 1, zxbY - zxk, zxbX + zxk + 1, zxbY + zxk, 1); gfx_line(zxbX - zxk + 1, zxbY + zxk, zxbX + zxk + 1, zxbY - zxk, 1);
  zxbH ? ( tipon = 1; tipforce = 0; tipstr = "Close the zoom (Esc)"; tipx = (zxbX - 150 * gsN - gox) / gs; tipy = (zxbY + 14 * gsN - goy) / gs; );
  zxbH && click ? ( szoom = 0; zoomv = 0; click = 0; );
);
zxbOn = 0;
// ---- VALUE ENTRY box (on top of everything, in the scale of its control) ----
vedit ? (
  vedSeen ? (
    vgsS = gs; vgxS = gox; vgyS = goy; vkcS = kcol;
    gs = vedGS; gox = vedGX; goy = vedGY; kcol = vedKC; setfonts();
    veddraw(vedCX, vedCY, vedW);
    gs = vgsS; gox = vgxS; goy = vgyS; kcol = vkcS; setfonts();
  ) : ( !vedJust ? vedit = 0; );                        // its control is not shown any more (section closed, view changed)
);
vedJust = 0;
vedPend ? ( vedPAge += 1; vedPAge > 2 ? ( vedPend = 0; vedPAge = 0; ); ) : ( vedPAge = 0; );   // nobody took the value
// ---- tooltip (drawn last, on top) ----
tipon && (tipShow || tipforce) && (!(mouse_cap & 1) || tipforce) ? (   // tipShow = Tips on, or Ctrl/Cmd held
  !lasttip ? tipstart = time_precise();
  cfade = min(1, (time_precise() - tipstart) / FADET);
  gfx_setfont(4); tw = min(tws(tipstr) + 14, WT - 12);
  tipl = min(max(tipx - tw / 2, 6), WT - 6 - tw);
  tipy = min(tipy, HT - 30);   // never glued to the bottom edge (footer line)
  col(monoOn ? cTEXT : (darkmode ? 0x3B5F8E : 0x1A4A86), 1); RR(tipl, tipy - 10, tw, 20, 5);   // the tooltip bubble in the blue of the design
  col(monoOn ? cBG : 0xFFFFFF, 1); tws(tipstr) + 14 > tw ? txtfit(tipstr, tipl + 7, tipy, tw - 14) : txt(tipstr, tipl + tw / 2, tipy, 1);
);
cfade = 1;
lasttip = tipon && (tipShow || tipforce) && (!(mouse_cap & 1) || tipforce);
);
dofull ? (mzfr += 1;   // a new stamp for the module boxes (the section headers of the full frame write it; a box with an older stamp was not drawn)
gfxp11();
gfxp12();
gfxp13();
gfxp14();
gfxp15();
) : (
  // partial frame: only the I/O meters move (playback) - repaint that section alone (image 1 still holds the last frame)
  // iokey = fingerprint of everything the I/O meters show (values quantized, weighted by small primes) - repaint only when it changes
  iokey = floor(dbs(pkIL) * 2) + floor(dbs(pkIR) * 2) * 3 + floor(dbs(pkOL) * 2) * 7 + floor(dbs(pkOR) * 2) * 11
        + floor(hIn) * 13 + floor(hOut) * 17 + floor(hIL * 10) * 31 + floor(hIR * 10) * 37 + floor(hOL * 10) * 41 + floor(hOR * 10) * 43 + (gwarn > 0 && time_precise() - lastblk < 0.3) * floor(time_precise() * 8) * 19 + floor(dbs(lmgv) * 4) * 29 + (time_precise() - lastblk > 0.3) * 31 + floor(time_precise() / 1.5) * 23 + (secopen(7) && time_precise() - lastblk < 3) * floor(time_precise() * 25) * 41 + floor((crvS > 1.5 ? 3 : crvS) * 50 + 0.5) * 43;   // the displayed (calm) correlation decides the repaint - the raw value changed every frame and repainted the meters and the mini meter for nothing
  // an open menu, panel or page lies on top: no repaint of the section through it - the next frame is a full one
  ovlon = rsave || rmenu || smenu || fmenu || bmenu || kmenu || wmenu || emenu || helpon || xpon || dmenu || tpvis || snedit || shpin || shcur || hlpon || snctx || mpmenu || ffpmenu || rpmenu || kovl;   // + the Motion, Free field and Reflector menus and the keys page
  iokey != lastiokey ? (
    !zoomv && !szoom ? ( ovlon ? ( fullcnt = 0; ) : ( col(cBG, 1); R(IOX - 1, IOY - 1, IOW + 2, IOH + 2); drawio(); rzon && rzok(basic ? 22 : 7) ? rcdraw(basic ? 44 : 7); ); );   // (its corner grip lies inside: drawn again)
// the mini correlation meter in the strip moves too - also in Tabs, where another module is open (szoom)
    mcOn && !ovlon && (tabm || (!zoomv && !szoom)) ? ( mgsS = gs; mgxS = gox; mgyS = goy; gs = mcGS; gox = mcGX; goy = mcGY; setfonts(); col(tabm ? cBG : cSURF, 1); R(mcX - 2, mcCY - 8, mcW + 4, 16); mcorrdraw(mcX, mcCY); gs = mgsS; gox = mgxS; goy = mgyS; setfonts(); );
    lastiokey = iokey; );
);   // end of: full / partial frame

// mouse wheel over source or listener -> height (Shift = fine)
tabm && szoom ? ( gs = gsN; gox = goxN; goy = goyN; mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs; );   // tabs: the room view lies on the page scale
vwopen && (!szoom || tabm) && mouse_wheel != 0 && !(mouse_cap & 1) && inbox(VX, VY, VW, VH) ? (
  PJ(lx, ly, lz); wLs = sqrt(sqr(mx - qx) + sqr(my - qy));
  PJ(sx, sy, sz); wSr = sqrt(sqr(mx - qx) + sqr(my - qy));
  wstep = mouse_wheel / 120 * ((mouse_cap & 8) ? 0.02 : 0.1);
  wEl = 0;
  bon && bew > 0 ? (
    planv ? ( PJ(brX0, yb, 0); ex0s = qx; ey0s = qy; PJ(brX1, yb, 0); wEl = mx >= ex0s - 6 && mx <= qx + 8 && abs(my - qy) <= 8; )   // plan: the whole line
          : ( PJ(bex0, yb, bez1); ex0s = qx; ey0s = qy; PJ(bex1, yb, bez0); wEl = mx >= ex0s - 3 && mx <= qx + 3 && my >= ey0s - 3 && my <= qy + 3; );
  );
  weh = !isfree && wLs >= 18 && wSr >= 18 && !wEl && !nearmth() ? edgehit() : -1;

  wLs < 18 ? moveobjz(2, lz + wstep) : ( wSr < 18 ? moveobjz(1, sz + wstep) : (
    wEl && !lockE ? setp(43, min(100, max(0, slider43 + mouse_wheel / 120 * ((mouse_cap & 8) ? 1 : 5))));   // wheel over door / window: opening
    weh >= 0 ? (                                                   // wheel over a room edge: that dimension (Alt = height, Ctrl/Cmd = size)
      wf = (1 + ((mouse_cap & 8) ? 0.005 : 0.02)) ^ (mouse_wheel / 120);
      (mouse_cap & 4) ? setp(31, min(4, max(0.25, slider31 * wf))) : (
      sidevOn && floor(weh / 4) == 2 ? (frontv ? setp(2, min(40, max(0.8, slider2 * wf))) : setp(3, min(40, max(0.8, slider3 * wf)))) : ((mouse_cap & 16) || floor(weh / 4) == 2 || sidevOn ? setp(4, min(15, max(0.8, slider4 * wf))) : (
      floor(weh / 4) == 0 ? setp(2, min(40, max(0.8, slider2 * wf))) : setp(3, min(40, max(0.8, slider3 * wf))); ); ); );
    );
    !wEl && weh < 0 && vtab < 2 && my > VY + 22 ? (   // the empty floor - zoom the view around the mouse
      vz2 = min(5, max(0.1, vzf * 1.12 ^ (mouse_wheel / 120)));
      abs(vz2 - 1) < 0.02 && abs(vzf - 1) >= 0.02 ? ( vz2 = 1; vzox = vzoy = 0; ) : (   // passing 1:1 snaps there (centered)
        vzcx = VX + VW / 2; vzcy = VY + VH / 2;
        vzox = mx - vzcx - (mx - vzcx - vzox) * vz2 / vzf; vzoy = my - vzcy - (my - vzcy - vzoy) * vz2 / vzf; );
      vzf = vz2; wheelused = 1; vzFitOn = 0;
    );
  ); );
);
// page scroll (only when the window is too low for the open sections at a readable size): wheel outside the room view
// and the controls, or the bar at the right edge
scmax > 0 ? (
  mouse_wheel != 0 && !wheelused && !(mouse_cap & 1) && !inbox(VX, VY, VW, VH) && !(rmenu || smenu || fmenu || bmenu || kmenu || wmenu || emenu) ? (
    scy = min(max(scy - mouse_wheel / 120 * 48, 0), scmax);
  );
  sbh = HT * HT / LH; sby = scy + scy / LH * HT; sbx = WT - 6;
  click && inbox(sbx - 3, scy, 10, HT) ? ( sbdrag = 1; sbmy0 = mouse_y; sbs0 = scy; );
  sbdrag && (mouse_cap & 1) ? ( scy = min(max(sbs0 + (mouse_y - sbmy0) / gs * LH / HT, 0), scmax); ) : ( sbdrag = 0; );
  dofull ? ( col(cTEXT, sbdrag || inbox(sbx - 3, scy, 10, HT) ? 0.45 : 0.22); RR(sbx, sby + 2, 4, max(12, sbh - 4), 2); );   // not on the Models page; only on full frames (no alpha build-up)
) : ( scy = 0; sbdrag = 0; );
mouse_wheel = 0;
// UNDO: a gesture is finished when no button is held, no value is being typed and 0.4 s have passed (wheel turns and key
// repeats within that time are one step)
!(mouse_cap & 3) && !vedit && !snedit && !rsave && time_precise() - ulast > 0.4 ? ucommit();
udone = 0;
(hlpon || kovl) && tabm && szoom ? ( gs = gsN; gox = goxN; goy = goyN; WT = WTN; HT = HTN; setfonts(); mx = (mouse_x - gox) / gs; my = (mouse_y - goy) / gs; );   // pages at the page scale
hlpon ? ( tipon = 0; hlpdraw(); );
kovl ? ( tipon = 0; kovldraw(); );
kovlw = kovl; hlpw = hlpon; snctxw = snctx;   // (the frame after closing a page or menu is a full one too)
// ---- notify REAPER about this frame's gesture changes (one call per frame keeps the touch alive) ----
// reflectors changed in the GUI (knobs, handles, setups, undo, snapshots, A/B) -> their sliders + automation (touch while the mouse is held)
rfLoad == rfLoadG ? ( i = 0; loop(15,
  abs(vopt[RFB + i] - rfSl[i]) > 0.0000001 ? ( rfset(i, vopt[RFB + i]); rfSl[i] = vopt[RFB + i];
    (mouse_cap & 1) ? ( rfauto(i, 0); rfTch[i] = 1; ) : rfauto(i, 2); );   // held: every changed frame writes (as sauto does), the touch ends on release
  rfTch[i] && !(mouse_cap & 1) ? ( rfauto(i, 1); rfTch[i] = 0; );
  i += 1; ); );
rfLoad != rfLoadG ? ( i = 0; loop(15, rfset(i, vopt[RFB + i]); rfSl[i] = vopt[RFB + i]; i += 1; ); rfLoadG = rfLoad; );   // a project loaded while the audio thread rests: the GUI pushes it too (rfLoad is written by the loader alone, each thread keeps its own copy)
i = 1;
loop(NSL,
  gchg[i] ? ( (mouse_cap & 1) ? ( sauto(i); gtouch[i] = 1; ) : ( sauto1(i); ); gchg[i] = 0; );
  i += 1;
);

lastcap = mouse_cap;
// the finished frame to the screen in one piece (see the top of @gfx)
gfx_dest = -1; gfx_a = 1; gfx_mode = 0; gfx_x = gfx_y = 0; gfx_blit(1, 1, 0);
