// Integral: a two-channel function generator written as a script – the built-in Calculus rebuilt with custom // functions, per-call state, loops, arrays and connected() on outputs. // // Channel 1 and 4 are the same function "channel" called twice; each call has its own state. The core is a // voltage-dependent integrator in f64 that carries the remaining time over segment ends (exact cycle length up to // audio rate) and runs twice per sample with a half-band filter (anti-aliasing). Known differences to the built-in // module: RISE, FALL and RESP are smoothed like every knob, there is no idle shortcut (the load is always the worst // case) and the half-band filter computes in f64. module integral "Integral" hp 20 theme black { desc de "Funktionsgenerator als Skript: zwei Kanäle (Hüllkurve, LFO, Slew bis Audiorate), zwei Offsets, SUM/OR/INV – ein Nachbau des Calculus." desc en "Function generator as a script: two channels (envelope, LFO, slew up to audio rate), two offsets, SUM/OR/INV – the Calculus rebuilt." // Channel 1 (left) in sig1 "SIGNAL" cv at 8 25 in trig1 "TRIG" gate at 20 25 button cycle1 "CYCLE" at 10 37 led cyc1 red at 17 37 knob rise1 "RISE" 0.0005..180 = 0.3 log unit s size l at 26 40 knob fall1 "FALL" 0.0005..180 = 0.3 log unit s size l at 26 58 knob resp1 "RESP" 0..1 = 1 / 3 unit pct tip "Response: log – linear – exp" at 26 75 in rcv1 "RISE" cv at 8 48.5 in bcv1 "BOTH" cv at 8 60 in fcv1 "FALL" cv at 8 71.5 in cin1 "CYCLE" gate at 8 83 // Channel 4 (right, mirrored) in sig4 "SIGNAL" cv at 93.3 25 in trig4 "TRIG" gate at 81.3 25 button cycle4 "CYCLE" at 91.3 37 led cyc4 red at 84.3 37 knob rise4 "RISE" 0.0005..180 = 0.3 log unit s size l at 75.3 40 knob fall4 "FALL" 0.0005..180 = 0.3 log unit s size l at 75.3 58 knob resp4 "RESP" 0..1 = 1 / 3 unit pct tip "Response: log – linear – exp" at 75.3 75 in rcv4 "RISE" cv at 93.3 48.5 in bcv4 "BOTH" cv at 93.3 60 in fcv4 "FALL" cv at 93.3 71.5 in cin4 "CYCLE" gate at 93.3 83 // Channels 2 and 3, attenuverters 1–4 and the bus in in2 "IN 2" cv at 40 25 in in3 "IN 3" cv at 61.3 25 knob att[4] "#" -1..1 = [1, 0, 0, 1] unit pct at 50.65 36 step 0 14.5 out unity1 "UNITY" env at 9 99.5 led u1p green at 17.2 95 led u1n red at 17.2 100 out var[4] "#" env at 25 99.5 step 14 0 led u4p green at 84.1 95 led u4n red at 84.1 100 out unity4 "UNITY" env at 92.3 99.5 out eor "EOR" gate at 9 113.5 led eorl yellow at 17.2 111.5 out orout "OR" cv at 32 113.5 out sum "SUM" cv at 48 113.5 led sump green at 56 109 led sumn red at 56 114 out inv "INV" cv at 64 113.5 led eocl yellow at 84.1 111.5 out eoc "EOC" gate at 92.3 113.5 const LIN = 1 / 3 // linear mark of RESP const EXPO = 2 / 3 // exponential mark const IEXP = 1 / 2.085 // curve constants: log ≈ 2×, exp ≈ 0.5×, hyper-exp ≈ 0.3× the linear time const IHYP = 1 / 3.3 const MAXR = 10 / 50e-6 // fastest segment: 10 V in 50 µs // Linear up to ±10 V, then soft up to ±10.5 V (supply rails). fn rail(x) { ax = abs(x) return ax <= 10 ? x : sign(x) * (10 + 0.5 * tanh((ax - 10) * 2)) } // First-order allpass – every call keeps its own state. fn allpass(x, a) { state x1 = 0 state y1 = 0 y = a * (x - y1) + x1 x1 = x y1 = y return y } // Two samples at double rate (mid first) → one sample: polyphase half-band. fn halfband(mid, cur) { return 0.5 * (allpass(allpass(cur, 0.07986643), 0.54532365) + allpass(allpass(mid, 0.28382935), 0.8344119)) } // Rate factor at voltage v: slower near the top for log, faster for exp. fn response(v, curve) { a = abs(v) + 1 if abs(curve - LIN) < 0.0001 { return 1 } else if curve <= LIN { lg = 3 / a return lg + (1 - lg) * (curve * 3) } else if curve <= EXPO { return 1 + (a * IEXP - 1) * ((curve - LIN) * 3) } else { return a * IEXP + (a * a * IHYP - a * IEXP) * ((curve - EXPO) * 3) } } // Integrate `left` seconds towards the apex (rising) or the signal; the time left at a segment end carries over. fn advance(v, up: bool, apex, sig, left, rup, rdn, curve, cyc: bool) { done = false for k in 0..3 { if not done { target = up ? apex : sig d = target - v if up and d <= 0 { up = false } else if d == 0 { if cyc { up = true apex = 8 } else { done = true } } else { rising = d > 0 rate = (rising ? rup : rdn) * response(v, curve) need = abs(d) / rate if need > left { v += (rising ? rate : -rate) * left done = true } else { v = target left -= need if up { up = false } else if not rising and cyc { up = true apex = 8 } done = left <= 0 } } } } return v, up, apex } // One function channel: unity output, end-of-rise state and cycling. fn channel(sig, trig, trise, tfall, curve, cbtn: bool, cin, rcv, fcv, both) { state v = 0 state up = false state apex = 10 state rising = false state cgate = false state sprev = 0 cgate = cgate ? cin > 2 : cin >= 2.5 cyc = cbtn or cgate if rise(trig) and not rising { up = true apex = cyc ? 8 : 10 } b = exp2(clamp(both, -10, 10)) rup = min(10 / max(trise, 0.00001) * max(1 - 0.11 * rcv, 0.12) * b, MAXR) rdn = min(10 / max(tfall, 0.00001) * max(1 - 0.11 * fcv, 0.12) * b, MAXR) c = clamp(curve, 0, 1) dt = 0.5 / SR mid = 0.5 * (sig + sprev) sprev = sig v, up, apex = advance(v, up, apex, mid, dt, rup, rdn, c, cyc) vmid = v v, up, apex = advance(v, up, apex, sig, dt, rup, rdn, c, cyc) rising = up or sig > v return rail(halfband(vmid, v)), not rising and v > 0.01, cyc } process { u1, hi1, cy1 = channel(sig1, trig1, rise1, fall1, resp1, cycle1, cin1, rcv1, fcv1, bcv1) u4, hi4, cy4 = channel(sig4, trig4, rise4, fall4, resp4, cycle4, cin4, rcv4, fcv4, bcv4) unity1 = u1 unity4 = u4 eor = gate(hi1) eoc = gate(not hi4) var[0] = rail(u1 * att[0]) var[1] = rail((connected(in2) ? in2 : 5) * 2 * att[1]) var[2] = rail((connected(in3) ? in3 : 2.5) * 2 * att[2]) var[3] = rail(u4 * att[3]) // Bus: only channels whose variable output is not patched. bus = 0 top = 0 for k in 0..4 { t = connected(var[k]) ? 0 : var[k] bus += t top = max(top, t) } s = rail(bus) sum = s inv = -s orout = top cyc1 = cy1 cyc4 = cy4 eorl = hi1 eocl = not hi4 u1p = u1 / 8 u1n = -u1 / 8 u4p = u4 / 8 u4n = -u4 / 8 sump = s / 10 sumn = -s / 10 } }