'(lens)

Prelude

prelude.loupe is the standard environment every patch starts with: constants, pitch names, helper functions, the builtin op surface, scales, chords, rhythms, and pattern builders. Every definition here can be shadowed by redefining it in your own patch.

; prelude.loupe: the standard environment every patch starts with.
; Shadowable: any def here can be redefined in a patch.

; ===========================================================================
; CONSTANTS
; ===========================================================================

(def VMAX 4095)         ; 12-bit ceiling
(def VMID 2048)         ; bipolar centre / unipolar midpoint
(def VMIN 0)            ; floor
(def SMAX 2047)         ; bipolar audio peak (a signed 12-bit sample spans -SMAX..SMAX)
(def OCTAVE 12)         ; semitones per OCTAVE
(def MIDI-MAX 127)      ; top MIDI note (= G9); use (spread x (add MIDI-MAX 1)) to map a 0..VMAX control across all pitches
(def half 2048)         ; VMAX / 2
(def vmax VMAX) (def vmid VMID) (def vmin VMIN)   ; lowercase aliases

; ---- rhythm + score conventions ----
; Syms in quoted patterns env-look-up to these.
(def x    4095)         ; beat hit  (gate high)
(def .    0)            ; beat rest
(def _    0)            ; score rest (no note)
(def ~    4095)         ; score tie  (hold previous)

; ===========================================================================
; PITCH NAMES: MIDI 0..127, bound by index in chromatic order.
; C-1=0, C4=60, G9=127. Bare C4 in any expression env-looks-up to 60.
; (add C4 OCTAVE) = (add 60 12) = 72.
; ===========================================================================

(def (C-1 C#-1 D-1 D#-1 E-1 F-1 F#-1 G-1 G#-1 A-1 A#-1 B-1
      C0  C#0  D0  D#0  E0  F0  F#0  G0  G#0  A0  A#0  B0
      C1  C#1  D1  D#1  E1  F1  F#1  G1  G#1  A1  A#1  B1
      C2  C#2  D2  D#2  E2  F2  F#2  G2  G#2  A2  A#2  B2
      C3  C#3  D3  D#3  E3  F3  F#3  G3  G#3  A3  A#3  B3
      C4  C#4  D4  D#4  E4  F4  F#4  G4  G#4  A4  A#4  B4
      C5  C#5  D5  D#5  E5  F5  F#5  G5  G#5  A5  A#5  B5
      C6  C#6  D6  D#6  E6  F6  F#6  G6  G#6  A6  A#6  B6
      C7  C#7  D7  D#7  E7  F7  F#7  G7  G#7  A7  A#7  B7
      C8  C#8  D8  D#8  E8  F8  F#8  G8  G#8  A8  A#8  B8
      C9  C#9  D9  D#9  E9  F9  F#9  G9))

; flat aliases: Db=C#, Eb=D#, Gb=F#, Ab=G#, Bb=A# for every octave -1..9.
; Cb_n=B_(n-1), Fb_n=E_n for completeness.
(def Db-1 C#-1) (def Eb-1 D#-1) (def Fb-1 E-1) (def Gb-1 F#-1) (def Ab-1 G#-1) (def Bb-1 A#-1)
(def Cb0  B-1) (def Db0  C#0)  (def Eb0  D#0)  (def Fb0  E0)  (def Gb0  F#0)  (def Ab0  G#0)  (def Bb0  A#0)
(def Cb1  B0)  (def Db1  C#1)  (def Eb1  D#1)  (def Fb1  E1)  (def Gb1  F#1)  (def Ab1  G#1)  (def Bb1  A#1)
(def Cb2  B1)  (def Db2  C#2)  (def Eb2  D#2)  (def Fb2  E2)  (def Gb2  F#2)  (def Ab2  G#2)  (def Bb2  A#2)
(def Cb3  B2)  (def Db3  C#3)  (def Eb3  D#3)  (def Fb3  E3)  (def Gb3  F#3)  (def Ab3  G#3)  (def Bb3  A#3)
(def Cb4  B3)  (def Db4  C#4)  (def Eb4  D#4)  (def Fb4  E4)  (def Gb4  F#4)  (def Ab4  G#4)  (def Bb4  A#4)
(def Cb5  B4)  (def Db5  C#5)  (def Eb5  D#5)  (def Fb5  E5)  (def Gb5  F#5)  (def Ab5  G#5)  (def Bb5  A#5)
(def Cb6  B5)  (def Db6  C#6)  (def Eb6  D#6)  (def Fb6  E6)  (def Gb6  F#6)  (def Ab6  G#6)  (def Bb6  A#6)
(def Cb7  B6)  (def Db7  C#7)  (def Eb7  D#7)  (def Fb7  E7)  (def Gb7  F#7)  (def Ab7  G#7)  (def Bb7  A#7)
(def Cb8  B7)  (def Db8  C#8)  (def Eb8  D#8)  (def Fb8  E8)  (def Gb8  F#8)  (def Ab8  G#8)  (def Bb8  A#8)
(def Cb9  B8)  (def Db9  C#9)  (def Eb9  D#9)  (def Fb9  E9)  (def Gb9  F#9)

; lowercase spellings of every note name, so (score '(c3 eb3 g3)) reads as
; easily as the uppercase originals.
(def a#-1 A#-1) (def a#0 A#0) (def a#1 A#1) (def a#2 A#2) (def a#3 A#3) (def a#4 A#4)
(def a#5 A#5) (def a#6 A#6) (def a#7 A#7) (def a#8 A#8) (def a-1 A-1) (def a0 A0)
(def a1 A1) (def a2 A2) (def a3 A3) (def a4 A4) (def a5 A5) (def a6 A6)
(def a7 A7) (def a8 A8) (def ab-1 Ab-1) (def ab0 Ab0) (def ab1 Ab1) (def ab2 Ab2)
(def ab3 Ab3) (def ab4 Ab4) (def ab5 Ab5) (def ab6 Ab6) (def ab7 Ab7) (def ab8 Ab8)
(def b-1 B-1) (def b0 B0) (def b1 B1) (def b2 B2) (def b3 B3) (def b4 B4)
(def b5 B5) (def b6 B6) (def b7 B7) (def b8 B8) (def bb-1 Bb-1) (def bb0 Bb0)
(def bb1 Bb1) (def bb2 Bb2) (def bb3 Bb3) (def bb4 Bb4) (def bb5 Bb5) (def bb6 Bb6)
(def bb7 Bb7) (def bb8 Bb8) (def c#-1 C#-1) (def c#0 C#0) (def c#1 C#1) (def c#2 C#2)
(def c#3 C#3) (def c#4 C#4) (def c#5 C#5) (def c#6 C#6) (def c#7 C#7) (def c#8 C#8)
(def c#9 C#9) (def c-1 C-1) (def c0 C0) (def c1 C1) (def c2 C2) (def c3 C3)
(def c4 C4) (def c5 C5) (def c6 C6) (def c7 C7) (def c8 C8) (def c9 C9)
(def cb0 Cb0) (def cb1 Cb1) (def cb2 Cb2) (def cb3 Cb3) (def cb4 Cb4) (def cb5 Cb5)
(def cb6 Cb6) (def cb7 Cb7) (def cb8 Cb8) (def cb9 Cb9) (def d#-1 D#-1) (def d#0 D#0)
(def d#1 D#1) (def d#2 D#2) (def d#3 D#3) (def d#4 D#4) (def d#5 D#5) (def d#6 D#6)
(def d#7 D#7) (def d#8 D#8) (def d#9 D#9) (def d-1 D-1) (def d0 D0) (def d1 D1)
(def d2 D2) (def d3 D3) (def d4 D4) (def d5 D5) (def d6 D6) (def d7 D7)
(def d8 D8) (def d9 D9) (def db-1 Db-1) (def db0 Db0) (def db1 Db1) (def db2 Db2)
(def db3 Db3) (def db4 Db4) (def db5 Db5) (def db6 Db6) (def db7 Db7) (def db8 Db8)
(def db9 Db9) (def e-1 E-1) (def e0 E0) (def e1 E1) (def e2 E2) (def e3 E3)
(def e4 E4) (def e5 E5) (def e6 E6) (def e7 E7) (def e8 E8) (def e9 E9)
(def eb-1 Eb-1) (def eb0 Eb0) (def eb1 Eb1) (def eb2 Eb2) (def eb3 Eb3) (def eb4 Eb4)
(def eb5 Eb5) (def eb6 Eb6) (def eb7 Eb7) (def eb8 Eb8) (def eb9 Eb9) (def f#-1 F#-1)
(def f#0 F#0) (def f#1 F#1) (def f#2 F#2) (def f#3 F#3) (def f#4 F#4) (def f#5 F#5)
(def f#6 F#6) (def f#7 F#7) (def f#8 F#8) (def f#9 F#9) (def f-1 F-1) (def f0 F0)
(def f1 F1) (def f2 F2) (def f3 F3) (def f4 F4) (def f5 F5) (def f6 F6)
(def f7 F7) (def f8 F8) (def f9 F9) (def fb-1 Fb-1) (def fb0 Fb0) (def fb1 Fb1)
(def fb2 Fb2) (def fb3 Fb3) (def fb4 Fb4) (def fb5 Fb5) (def fb6 Fb6) (def fb7 Fb7)
(def fb8 Fb8) (def fb9 Fb9) (def g#-1 G#-1) (def g#0 G#0) (def g#1 G#1) (def g#2 G#2)
(def g#3 G#3) (def g#4 G#4) (def g#5 G#5) (def g#6 G#6) (def g#7 G#7) (def g#8 G#8)
(def g-1 G-1) (def g0 G0) (def g1 G1) (def g2 G2) (def g3 G3) (def g4 G4)
(def g5 G5) (def g6 G6) (def g7 G7) (def g8 G8) (def g9 G9) (def gb-1 Gb-1)
(def gb0 Gb0) (def gb1 Gb1) (def gb2 Gb2) (def gb3 Gb3) (def gb4 Gb4) (def gb5 Gb5)
(def gb6 Gb6) (def gb7 Gb7) (def gb8 Gb8) (def gb9 Gb9)


; ===========================================================================
; HELPER FNS (composed from primitives)
; ===========================================================================

; value (0..VMAX) -> bipolar (-VMAX..+VMAX) signal.
(def bipolar  (fn (:x) (sub (mul x 2) VMAX)))

; bipolar signal (up to +/-2047, an oscillator's full swing) -> value (0..VMAX).
(def unipolar (fn (:s) (add s VMID)))

; ---- jack-boundary conversions ----
; signed: identity for audio jacks (DAC accepts signed bipolar samples).
(def signed     (fn (:x) x))
; brightness: identity for LEDs (PWM accepts 0..VMAX directly).
(def brightness (fn (:x) x))

; absolute distance between two values.
(def dist     (fn (:a :b) (abs (sub a b))))

; hard-clip a signal to the bipolar audio rails (use before an audio-buffer
; write: the buffer wraps rather than clamps, so an over-range value flips
; polarity and crackles). For a soft knee use `saturate` instead.
(def clip     (fn (:in) (min (max in (sub 0 SMAX)) SMAX)))


; ===========================================================================
; SCALES (semitone offsets from a root)
; ===========================================================================

(def minor      (lens '(0 2 3 5 7 8 10)))
(def major      (lens '(0 2 4 5 7 9 11)))
(def minor-pent (lens '(0 3 5 7 10)))
(def major-pent (lens '(0 2 4 7 9)))
(def dorian     (lens '(0 2 3 5 7 9 10)))
(def phrygian   (lens '(0 1 3 5 7 8 10)))
(def lydian     (lens '(0 2 4 6 7 9 11)))
(def mixolydian (lens '(0 2 4 5 7 9 10)))
(def chromatic  (lens '(0 1 2 3 4 5 6 7 8 9 10 11)))

; 12-bit pitch-class masks per scale; pair with (snap :scale ...).
(def scale-masks (lens '(1453 2741 1193 661 1709 1451 2773 1717 4095)))

; ===========================================================================
; BUILTINS: primitive ops the runtime implements directly.
; Empty fn body = a primitive implemented by a C kernel of the same name.
; The params list the full call surface.
; ===========================================================================

; ---- value arithmetic ----------------------------------------------------
; add/sub take :sat to saturate at the value rails instead of wrapping.
(def add       (fn (:a :b :sat)))
(def sub       (fn (:a :b :sat)))
(def mul       (fn (:a :gain)))
; div floors and mod takes the divisor's sign, so (div a n)*n + (mod a n) = a.
(def div       (fn (:a :n)))
(def mod       (fn (:a :n)))
(def transpose (fn (:in :by)))
(def invert    (fn (:in)))
(def spread    (fn (:in :n)))
; over: a 0..VMAX stream mapped across lo..hi INCLUSIVE (both may be streams;
; lo may be negative; hi >= lo). The one spelling for "knob over a range":
;   (over (knob :x) 1 20)     a count
;   (over (knob :main) C3 B4) a pitch window
;   (over cv -12 12)          a bipolar depth
; Curves compose on the stream, not as kwargs: (over (sq (knob :y)) 0 VMAX).
(def over (fn (:in :lo :hi) (add lo (spread in (add 1 (sub hi lo))))))
; fan: spread's inverse; index k of n fanned across 0..VMAX.
;   (fan (step prog) (len ops))   an op index an LED can show
(def fan  (fn (:k :n) (div (mul k VMAX) (sub n 1))))
; sq: perceptual square curve (slow start, fast finish); LED fills and
; audio-taper sweeps read through it.
(def sq   (fn (:x) (div (mul x x) VMAX)))
(def shift     (fn (:in :by)))
(def mask      (fn (:in :mask)))
(def bit       (fn (:in :n)))
(def xor       (fn (:a :b)))
(def and       (fn (:a :b)))
(def or        (fn (:a :b)))

; ---- comparisons (return 0 or VMAX) --------------------------------------
(def gt  (fn (:a :b)))
(def gte (fn (:a :b)))
(def lt  (fn (:a :b)))
(def lte (fn (:a :b)))
(def eq  (fn (:a :b)))
(def ne  (fn (:a :b)))

; ---- panel / jack leaves -------------------------------------------------
; Uniform call-form: (name :label [:interpretation...]). Every read is 0..VMAX.
; (knob :main) detents the rails (half-width 96); :detent 0 reads raw.
; (cv-in :1) is unipolar 0..VMAX; :bipolar centres on 0; :v-oct reads pitch.
; (switch :z) returns the rails VMIN/VMID/VMAX (down/middle/up).
(def knob     (fn (:main :x :y :detent)))
(def cv-in    (fn (:bipolar :v-oct)))
(def pulse-in (fn ()))
(def audio-in (fn ()))

; ---- MIDI input ----------------------------------------------------------
; From a USB keyboard plugged into the card (host) or a computer (device).
; Same (name :label) form; :ch picks a channel 1..16 (omitted = omni). midi-note
; and midi-velocity hold the last note-on; (midi-cc :N) reads CC number N;
; midi-trig fires on note-on of :note; midi-clock/midi-playing follow transport.
(def midi-note     (fn (:ch)))
(def midi-gate     (fn (:ch :note)))
(def midi-velocity (fn (:ch)))
(def midi-bend     (fn (:ch)))
(def midi-pressure (fn (:ch)))
(def midi-trig     (fn (:note)))
(def midi-cc       (fn ()))
(def midi-clock    (fn ()))
(def midi-playing  (fn ()))

; ---- oscillators ---------------------------------------------------------
; The pitch kwarg is :note (a MIDI note number) everywhere; :hz/:rate/:tempo/:bpm
; are the other rate domains (one at a time). :cents = fine detune, VMID centre,
; +-100 cents full scale. :phase drives the shaper from an external phasor ramp
; (rate kwargs then do not apply). square :width = pulse width, VMID = 50%.
(def phasor   (fn (:note :hz :rate :tempo :bpm :sync => :phase)))
(def sine     (fn (:note :hz :rate :tempo :bpm :cents :pm :depth :fb :fm :phase)))
(def triangle (fn (:note :hz :rate :tempo :bpm :cents :fm :phase)))
(def saw      (fn (:note :hz :rate :tempo :bpm :cents :fm :phase)))
(def square   (fn (:note :hz :rate :tempo :bpm :cents :width :fm :phase)))
(def wavetable (fn (:table :note :hz :rate :cents :pos :pm => :out)))
(def wt        (fn (:table :note :hz :rate :cents :pos :pm => :out)))
(def follow   (fn (:base :mult :div :rate :drift)))

; ---- audio shaping -------------------------------------------------------
(def average  (fn (:in :cut)))
(def lpf      (fn (:in :cut :poles :hz)))
(def hpf      (fn (:in :cut :poles :hz)))
(def lpg      (fn (:in :ctrl)))

; stereo modulated delays: :rate sweep speed, :depth sweep width, :fb bipolar
; around VMID (VMID = none). Ports :l :r; a bare use is the left channel.
(def chorus     (fn (:in :rate :depth :fb => :l :r)))
(def flanger    (fn (:in :rate :depth :fb => :l :r)))
; reverb: a room. :decay 0..VMAX = small..endless, :mix dry..wet. Ports :l :r.
; Heavy: ~40% of one core and ~36 KB of tape memory; spend it knowingly.
(def reverb     (fn (:in :decay :mix => :l :r)))
; echo: a stereo delay unit. :mono | :stereo | :ping-pong pick the routing,
; :ratio skews the right side's time, :seconds sizes its private ring (a
; patchable delay memory, compose a buffer + tap instead). Ports :l :r.
(def echo       (fn (:in :in-r :time :fb :mono :stereo :ping-pong :ratio :seconds => :l :r)))
; compressor: peak follower, downward gain above :thresh; :ratio sets the slope.
(def compressor (fn (:in :thresh :ratio :attack :release)))
(def envfollow (fn (:in :cut)))
(def vcf      (fn (:in :cut :res => :lp :hp :bp :notch)))
(def lpf2     (fn (:in :cut :res)))
(def hpf2     (fn (:in :cut :res)))
(def bpf2     (fn (:in :cut :res)))
; Envelope inputs are named for their semantics: `envelope` FIRES on a :trig edge
; and decays (percussive; a held gate does not sustain it); `adsr` and `dxeg`
; FOLLOW a :gate level (sustain while high, release on the fall). Gate-shaped
; sounds want adsr; hits want envelope.
(def envelope (fn (:trig :decay :peak)))
(def adsr (fn (:gate :attack :decay :sustain :release :peak)))
(def dxeg (fn (:gate :r1 :r2 :r3 :r4 :l1 :l2 :l3 :l4)))
(def dx   (fn (:bank :preset :note :gate :decay :tone => :out)))
(def slew     (fn (:in :rate)))
; The three multiplies, by role:
;   vca  = signal through a gain: :amp 0..VMAX maps to 0..1 (VMAX = unity).
;          A negative :amp inverts, so a bipolar amp is an attenuverter.
;   ring = ring mod: signal x signal, both bipolar, full x full = full scale.
;   mul  = raw value math (wraps). It never gain-scales.
(def vca      (fn (:in :amp)))
(def ring     (fn (:in :with)))
; average of its inputs (variadic, up to 4; safe, can't clip). :levels '(w0 w1 ...)
; weights them, normalised to the weight sum. For a unity-gain sum use `add`.
(def mix      (fn (:a :b :c :d :levels)))
(def wavefold (fn (:in :drive)))
(def crush    (fn (:in :rate)))
; cubic soft-clip. :drive pre-gains into the curve, :bias (bipolar) skews it for
; even-harmonic warmth, :mix blends dry/wet, :level is output makeup gain.
(def saturate (fn (:in :drive :bias :mix :level)))
; LUT waveshaper. :drive pre-gains into a baked transfer curve; :curve picks it
; (0 soft, 1 hard, 2 asym/even-harmonic, 3 overdrive); :oversample 1 turns on 4x
; anti-aliasing (off by default). Complements saturate/wavefold/crush.
(def shape    (fn (:in :drive :curve :oversample)))

; ---- generators ----------------------------------------------------------
(def noise   (fn (:hz :rate)))
(def random  (fn (:shape :trig)))
(def chance  (fn (:p :trig)))
(def walk    (fn (:step :trig)))
; a random note in a scale: root..root+range, snapped. The safe spelling of
; "musical random" (raw (random) spans 0..4095 and must be mapped before snap).
; :trig defaults to the PRELUDE master (120bpm); a patch with its own tempo
; passes its clock: (rand-note :trig master).
(def rand-note (fn (:root C3 :range 24 :scale minor :trig master)
  (snap (over (random :trig trig) root (add root range)) :scale scale)))

; ---- panel words (LEDs read at a glance) ----------------------------------
; flash: a blink per trigger, bright then fading (an envelope wearing LED
; clothes). breath: a slow unipolar swell, the "still alive" light.
(def flash  (fn (:trig :decay 900) (envelope :trig trig :decay decay)))
(def breath (fn (:hz 0.2) (unipolar (sine :hz hz))))

; vactrol: an LPG's lag. Follows a rise at once, rings down when the control
; falls; :fall trims the tail (4095 = ~85 ms). (lpg x (vactrol gate)) plucks.
(def vactrol (fn (:in :fall 4095)
  (def v (max in (sub (vca v fall) 1)))
  v))

; latest: follow whichever control moved last. b (e.g. a midi-cc :init) owns
; until a strays :near from where it was last parked; any change of b takes
; ownership back. Neither jumps the other's slow sweeps.
(def latest (fn (:a :b :near)))

; ---- edge detectors / clock state ----------------------------------------
; hold: a sample & hold (expander form, see the stateful-memory note below).
(def hold    (fn (:val :on)))
(def pickup  (fn (:value :on :init :near)))
(def toggle  (fn (:in)))
(def schmitt (fn (:in :lo :hi)))
(def gate    (fn (:in :thresh :len)))
; a clock is just a phasor (a phase ramp) named for timebase intent. Rate kwargs and
; :sync forward to the phasor; unset ones drop. A consumer edge-detects the ramp;
; a wider tick pulse belongs on the consumer: (trig clk :width 480).
(def clock   (fn (:tempo :hz :bpm :rate :sync)
  (phasor :tempo tempo :hz hz :bpm bpm :rate rate :sync sync)))

; the master clock every CLK_OP defaults to. Defined after clock/phasor.
(def master (clock :bpm 120))
; z1: the signal one sample ago. A cycle already delays by one; z1 adds more.
(def z1      (fn (:x)))

; varispeed: a 0..VMAX knob mapped to a play speed, sweeping from a slow LFO up to
; audio rate. Feed it to play/loop as the speed for a one-knob LFO-to-wavetable read.
; (2801 is the safe top of the phasor :rate input; spread maps the knob into [0, 2801).)
(def varispeed (fn (:knob) (spread knob 2801)))
(def diff    (fn (:in)))
; trig: a clock's BEAT = the phase wrap (the ramp's reset). Lands on the downbeat for
; any division/multiplication. No :trig -> defaults to master (CLK_OPS injection).
(def trig    (fn (:trig :width)))
(def turns   (fn (:trig)))
; edge: a RISING edge of a signal (a pulse arriving, a comparison becoming true).
(def edge    (fn (:in :width)))
; fall: a FALLING edge of a signal.
(def fall    (fn (:x :width)))
(def detent  (fn (:x :width)))
(def range   (fn (:in :to-index :to-value)))
(def every   (fn (:n)))
(def euclid  (fn (:pulses :steps :trig)))
; groove: a drum-machine line. Variadic voice forms, each with an :on beat tape;
; the expander rewires each voice's :on into (hits tape :trig CLK) and mixes them:
;   (groove :trig clk (kick :on four-on-floor :note A1) (snare :on backbeat) ...)
(def groove  (fn ()))

; ---- tape heads ----------------------------------------------------------
; Cable form: (<- SINK SOURCE [:trig C] [:per-sample] [:len L] [:when G] [:blend F])
;   :trig C      drives the recordhead's clock from C; default = master.
;   :per-sample  head writes one cell per audio sample.
;   :len L       caps the head's cycle length (literal int or stream).
;   :when G      only write while G is nonzero.
;   :blend F     fn (:old :new) -> :result for compositing into existing cells.

(def lookup  (fn (:tape :index :len)))
(def step    (fn (:tape :trig :len)))
(def seek    (fn (:tape :index :trig :len)))
(def len     (fn (:tape)))
; counter: beats of :trig (default master). Bare = a stopwatch (unbounded, so
; (counter :trig (clock :hz 1)) counts seconds); :bars N wraps modulo N (a bar
; position); a rising :reset zeroes it. (gte (counter) n) = "after n beats",
; (eq (counter :bars 8) 0) = "on the one".
(def counter (fn (:bars :trig :reset)))
(def wave    (fn (:tape :note :hz :rate :pos :once :slots :pick :scan :reverse :expand :len)))
(def tap     (fn (:tape :amount :span)))
(def record  (fn ()))
; delay: the signal :time samples ago (default 100ms, up to 1s). Sugar over an
; audio buffer + a per-sample write + a tap; each call gets its own buffer, or
; pass :buf to share one. For feedback, write the loop into a buffer directly
; (see utility-pair/echo.loupe):
;   (<- b (add in (vca (tap b :amount t) fb)) :per-sample)
(def delay (fn (:in :time 4800 :buf (audio :seconds 1) => :out)
  (<- buf in :per-sample)
  (<- out (tap buf :amount time))))

; play / loop: read a tape under a play head moving at `speed`, hiding the phasor.
; A play head is a phasor swept across the tape; `speed` sets how fast (and, signed,
; which way). play sweeps the whole tape; loop sweeps only the first `span` cells
; (e.g. the part of a slot actually recorded). Several plays on one tape at different
; speeds give several heads at once.
(def play (fn (:tape :speed) (lookup tape (spread (phasor :rate speed) (len tape)))))
(def loop (fn (:tape :speed :span) (lookup tape (spread (phasor :rate speed) span))))

; ---- compiler special forms ----------------------------------------------
; Handled by the expander/lowerer (no kernel of their own); listed here so the
; surface is discoverable from this file.
(def tape      (fn (:pat)))                       ; sequence buffer from a quoted list
(def audio     (fn (:seconds :length)))           ; blank audio buffer: :seconds, or
                                                  ; :length in cells for exact sample
                                                  ; counts (Karplus-Strong, combs)
(def score     (fn (:pat => :notes :rhythm)))     ; quoted melody -> pitch + gate tapes (_ rest, ~ tie)
(def morph     (fn ()))                           ; variadic crossfade across inputs by a position
(def normal    (fn (:jack :default)))             ; the jack when patched, else default
(def connected (fn (:jack)))                      ; VMAX if a cable is patched into the jack, else 0
; trim: a patched jack scaled by its knob; the bare knob when unpatched.
;   (echo in :time (trim (cv-in :1) (knob :y)))
(def trim (fn (:jack :knob) (if (connected jack) (vca jack knob) knob)))

; ---- hold (state / sample & hold) + timing regions ----------------------
; hold is a sample & hold. Three shapes, told apart by whether it carries a clock:
;   (hold VAL ON) | (hold VAL :on G)        level S&H: track VAL while the gate is high
;   (hold NAME NEXT :trig C | :per-sample [:init V])
;       a register/fold; NAME is the held value, NEXT is stored each clock edge (or
;       sample) and may reference NAME for feedback (counters, accumulators, one-poles).
;   (hold VAL :trig C | :per-sample [:init V])   edge/per-sample S&H of an outside VAL.
;     (def i   (hold n (mod (add n 1) 16) :trig clk))   ; counts 0..15, one per tick
;     (def sum (hold s (add s x) :trig clk))            ; running total
; State lives in `hold`; `def` stays a timeless binding.
; (on CLK :when G FORM...)
;   A timing region: every (<- ...) write inside defaults to :trig CLK and :when G,
;   so the clock and gate are stated once, not repeated on every line.

; ---- selectors / converters ---------------------------------------------
; (switch :z) reads the panel z-switch as rails (VMIN/VMID/VMAX). Panel INPUT only.
; To select a value, use (thru (lens a b ...) idx) for index or (if cond a b) for boolean.
(def switch   (fn (:z)))

; switch (and any 3-rail control) position as a boolean: down < middle < up.
(def up   (fn (:x) (gt x VMID)))
(def mid  (fn (:x) (eq x VMID)))
(def down (fn (:x) (lt x VMID)))
(def v-oct    (fn (:note)))
(def cv       (fn (:in :bipolar)))
(def snap     (fn (:note :scale)))
(def quantise (fn (:in :scale)))
(def degree   (fn (:in :scale)))
(def pitch    (fn (:in :scale)))
; (thru LIST IDX) -> the IDX-th cell of LIST (out-of-range IDX wraps).
; A value cell IS the value; an op-lens cell is a fn you apply: ((thru ops IDX) X).
(def thru     (fn (:list :at)))
; squint: like thru but spreads the selector across the list length, so a
; 0..VMAX control (knob, switch rail) lands on a valid cell. ((squint ops sel) x).
(def squint   (fn (:lens :selector) (thru lens (spread selector (len lens)))))

; ---- value / logic sugar ------------------------------------------------
(def if     (fn (:cond :then :else)))
(def not    (fn (:x)))
(def max    (fn (:a :b)))
(def min    (fn (:a :b)))
(def window (fn (:a :lo :hi)))
(def abs    (fn (:x)))
(def rect   (fn (:x)))
; exp2: exponential CV/VCA transfer over ~8 octaves. :in 0..VMAX maps to
; VMAX*2^(8*(in/VMAX-1)): in=VMAX -> VMAX (unity), in=0 -> ~16 (near silence).
(def exp2   (fn (:in)))
; log2: inverse of exp2, recovers the linear control from the exponential gain.
(def log2   (fn (:in)))

; ---- onset/gate/hit readers over a tape ---------------------------------
(def onsets (fn (:tape :trig)))
(def gates  (fn (:tape :trig)))
(def hits   (fn (:tape :trig)))

; ---- drum primitives ----------------------------------------------------
; :trig defaults to the master clock (a bare (kick) hits every beat).
(def kick  (fn (:note :decay :drive :sweep :trig)))
(def snare (fn (:note :decay :snappy :tone :trig)))
(def hat   (fn (:note :decay :tone :trig)))

; Karplus-Strong plucked string: :trig re-excites, :pitch (MIDI) sets the
; loop length, :damp (0..vmax) sets decay/brightness (more = faster, duller).
(def pluck (fn (:trig :pitch :damp)))

; ===========================================================================
; PATTERN BUILDERS: write rhythms and melodies as quoted lists.
; ===========================================================================
; (beat '(x . x .))         -> tape of gate values
; (notes '(C4 E4 G4 B4))    -> tape of MIDI pitches
; (score '(C4 _ E4 ~))      -> pitch tape + gate tape; _ rests, ~ ties

(def beat  (fn (:pat) (tape pat)))
(def notes (fn (:pat) (tape pat)))
; score is an expander form: a quoted list becomes a pitch tape and a
; rhythm tape. _ rests and ~ ties: neither fires the rhythm, both hold the pitch.

; midi-score: the played-in counterpart to score. Records the MIDI keyboard's
; note + gate + velocity onto a clock grid while :rec is held (the loop is as long
; as you held it), then loops the take at :speed. Returns the same :notes / :rhythm
; ports score does, plus :vel, so a performance is consumed like a written part.
(def midi-score (fn (:rec :clk :speed => :notes :rhythm :vel)
  (def np (tape :len 1600)) (def gp (tape :len 1600)) (def vp (tape :len 1600))
  (def cap (sub (len np) 1))
  (def pos (hold p (if rec (min (add p 1) cap) 0) :trig clk))
  (def lng (hold l (if rec (add pos 1) l) :trig clk :init 1))
  (on clk :when rec
    (<- (np pos) (midi-note))
    (<- (gp pos) (midi-gate))
    (<- (vp pos) (midi-velocity)))
  (<- notes  (if rec (midi-note)     (loop np speed lng)))
  (<- rhythm (if rec (midi-gate)     (loop gp speed lng)))
  (<- vel    (if rec (midi-velocity) (loop vp speed lng)))))

; ===========================================================================
; RHYTHMS: classic patterns to start from and mutate.
; ===========================================================================
; Step or onset over any of these. Rewrite cells via a chance-gated cable
; to drift into nearby rhythms while keeping the family.

; quarter notes (kick).
(def four-on-floor (beat '(x . . . x . . . x . . . x . . .)))

; snare backbeat (2 and 4).
(def backbeat      (beat '(. . . . x . . . . . . . x . . .)))

; eighth notes (hat).
(def eighths       (beat '(x . x . x . x . x . x . x . x .)))

; off-beat eighths.
(def offbeat       (beat '(. . x . . . x . . . x . . . x .)))

; sixteenth notes.
(def sixteenths    (beat '(x x x x x x x x x x x x x x x x)))

; downbeat only.
(def downbeat      (beat '(x . . . . . . . . . . . . . . .)))

; 3-3-2 tresillo over 8.
(def tresillo      (beat '(x . . x . . x .)))

; 2-1-2-1-2 cinquillo over 8.
(def cinquillo     (beat '(x . x x . x x .)))

; afro-cuban habanera over 8.
(def habanera      (beat '(x . . x x . . .)))

; 3-2 son clave over 16.
(def son-clave     (beat '(x . . x . . x . . . x . x . . .)))

; 3-2 rumba clave over 16.
(def rumba-clave   (beat '(x . . x . . . x . . x . x . . .)))

; bossa nova clave over 16.
(def bossa         (beat '(x . . x . . x . . . x . . x . .)))

; ===========================================================================
; SCALES + CHORDS by name (handy in patches without writing intervals)
; ===========================================================================
; Each is a lens whose cells are semitone offsets from the root.
; Pair with `(transpose root degree)` or `(add root (thru chord idx))`.
; The triads carry the note count (maj3/min3) so they don't shadow the min/max
; operators or the major/minor scales above.

(def maj3  (lens '(0 4 7)))
(def min3  (lens '(0 3 7)))
(def dim   (lens '(0 3 6)))
(def aug   (lens '(0 4 8)))
(def sus2  (lens '(0 2 7)))
(def sus4  (lens '(0 5 7)))
(def maj7  (lens '(0 4 7 11)))
(def min7  (lens '(0 3 7 10)))
(def dom7  (lens '(0 4 7 10)))
(def dim7  (lens '(0 3 6 9)))