Connect musical durations, pattern transformations and continuous controls using the supported time rules.
One set of temporal words spans both time worlds: fast, slow, and rev mean the same thing on a discrete pattern and on a continuous control signal, and the pan/gain lane slots take either a stepped pattern or a riding LFO. Everything folds at compile time — the runtime never learns a combinator was involved.
1. What works where#
| Combinator | On a pattern | On a control signal |
|---|---|---|
fast n / slow n | scales the cycle (as always) | scales the modulator's rate in place |
rev | mirrors the events | negates the odd LFO shapes; mirrors baked event lists |
every n f | as always | rejected with a hint — apply it before the lift |
wobble = lfoSine 1Hz |> fast 2 -- IS a 2 Hz LFO
sweep = lfoSaw (4c) |> rev -- falling ramp (4-cycle period)
stairs = stepSeq !"0.1 0.4 0.7 1" |> rev -- mirrored steps, hold law kept
drift = signal !"0 1 0.5" |> slow 2 -- lifted pattern at half speed
A pattern subject in a signal slot (!"0 1" |> fast 2 inside a patch field) resolves through the pattern world first and lands on the same PatternMod node a bare literal takes — so every, degrade, and the whole pattern algebra compose there before the lift.
What fast/slow can scale#
Anything that carries a rate or a cycle: the four phase LFOs (lfoSine/lfoSaw/lfoTri/lfoSquare — constant, lane, or live-signal rates; a signal rate is wrapped in a compile-time Product), lfoSH, wander (its slew τ rides along, preserving the τ = half-period law), lorenz/henon, wow, brake/spinUp (duration divides), stepSeq, signal, and any pattern-backed modulator. Anything else — a raw oscillator, a follower, a macro — errors with the list. The factor must be a positive constant.
What rev reverses#
Time-reversal f(−t), exactly: sine/saw/square are odd (rev = negate), the triangle is even (rev = identity), Negate/Slew wrappers recurse. A PatternMod (from stepSeq, signal, or a lifted pattern) mirrors its event list within the cycle; when the pre-first-onset default obeyed the stepSeq hold law ("carry the last value"), it is recomputed so it still does. lfoSH/wander/chaos have no reversible cycle and say so.
Typing#
A fully-applied temporal combinator whose subject is a Sig keeps the signal's type (lfoSine 1Hz |> fast 2 : Sig Control Bipolar) — so the binding routes as a signal binding, inlines into patches like any free-floating modulator, and never mis-enters the pattern-lane path. Partial application (fast 2 handed to every) still types as a pattern transform, unchanged.
2. Continuous signals in lane slots#
pan and gain — the two lanes whose parameters are continuous by nature — accept a signal where they take a pattern:
tPad = play pad padPat |> pan (lfoSine (8c)) -- bus auto-pan ride
tTrem = play lead riff |> gain (lfoSine 4) -- full-depth tremolo
tPad2 = play pad prog |> gain (0.8 + 0.2 * lfoTri (2c)) -- shallow ride
tHats = play hat hatPat |> gain (!"1 0.6 0.85 0.6") -- stepped, as before
What the slot accepts (the lane recognizer — a fold, not a full lowerer, because track effects have no modulation graph): one LFO with a constant rate, optionally scaled/offset by constant arithmetic (+ - *, unary minus), optionally wearing the §1 folds (|> fast 2, |> rev), directly or through a named top-level signal binding. Anything richer — lfoSH, wander, stepSeq, chained modulators — lowers to a persistent control graph on the same insert (AutoPan / GainControl with a control_ref; gain (stepSeq !"0.05 0.05") measured −26.0 dB against the plain track, offline, not listened to), and a macro "x" base (mod) binding rides these slots too.
Semantics.
pan (lfo)— a bus-levelAutoPan(the same effect the macro form builds) with the LFO inline: position = base + scale·wave, clamped to [−1, 1], constant-power law per sample. A macro base and the ride compose.gain (lfo)— a bus-level tremolo (TremoloTrack): gain = base + scale·wave, floored at 0. A zero-offset LFO reads as unipolar depth: barelfoSine 4trembles [0, 1],lfoSine 4 * 0.6trembles [0, 0.6] — full depth bottoms at silence instead of clipping negative. An explicit offset (0.8 + 0.2 * lfoTri 2) is taken literally.- Engine-side, the lane LFO uses the same band-limited sampler and f64 phase accumulator as the voice
Lfonode, so a lane LFO and a patch LFO at the same rate are the same waveform. Its phase is re-anchored to absolute transport time every block (fract(t · rate), thePatternModclock convention), so a cycle-denominated ride is bar-locked, seek-safe, block-size independent, and identical between live and export — a chain rebuilt mid-song picks up mid-phase. The applied wave rides a ~5 ms anti-pop glide (the stepped lane's step-glide equivalent), so square/saw rides step cleanly. Both effects are scene-addressable (track.panTrack.pan,track.gain.gain).
The stepped lanes stay stepped. speed, warp, pitch, variantP, push, and velocity sample per event; a continuous signal there is rejected with a hint naming sampleAdd, the two lanes that ride (and what they accept), and the macro spelling for anything richer. Constant and pattern forms of every lane are unchanged.
Rate spelling. Write LFO rates as 4 (Hz), 4Hz, 250ms, or (4c) (a four-cycle period). Cycle units survive constant folding: a bound name (r = 8c), arithmetic (2 * 4c, (3c/8)), and the fraction spelling (1/4c) (a quarter-cycle period) all convert through the tempo like the literal. Not (1/4) — compile-time integer division truncates to 0 and is rejected.
Since 2026-08-22 that holds in every rate slot, not just a lane: the patch-side lower_rate folds the same way and shares the lane's conversion table (rate_hz_from_unit), so lfoSine @ 1/4c, lfoSine (1/4c) and a bound r = 1/4c are one rate, and a zero rate is refused in a patch field and in an explicit-Hz lane step too (production-gaps §28 X1).
Live rates carry their unit too (2026-09-01, production-gaps AA2 — the live twin of X1). A rate slot's signal fallthrough has always existed: any non-constant expression lowers to a rate_node the engine reads per sample, so a pitch-derived rate — lfoTri (const 2Hz + keytrack (linear 0.08) ~G1), the "low notes wobble slower" move — has always worked. (The seesaw header comment claiming it had no spelling was wrong, and the gap entry with it.) What did not work is a live rate carrying a PERIOD unit: the engine reads a rate node as Hz, so lfoTri (0.5c * knob) silently meant 0.5·knob Hz instead of "half a cycle per period" — off by 1/cycle_seconds, with no diagnostic. lower_rate now applies the same rate_hz_from_unit reciprocal as one Quotient node when the rate signal's unit is Cycles / Seconds / Milliseconds; a Hz or dimensionless live rate is untouched, which is why no shipped song's IR moved.
Note-on phase offsets (2026-09-01, production-gaps AA3). |> phase n now accepts the four periodic lfo* shapes and seeds their accumulator at note-on, in cycles, exactly as it does an oscillator's — so N copies of one LFO at fixed offsets are spellable: quadrature pan/filter pairs, Shepard amplitude windows, a continuous Risset glissando. It is the same property that admits a cycle-rate lane: those four shapes carry a phase accumulator. lfoSH, wander and drift address a hashed grid by absolute time, have no accumulator to offset, and are still refused — with a diagnostic that names the four that work.
Duration spelling. The same unit list, read the other way round: in a duration slot a Cycles number is a musical length in cycles, so (1/8c), 0.125c and (1c/8) are one length (0.25 s at 120 BPM), as are (3/8c) ≡ (3c/8) ≡ 0.375c. That holds in every duration slot — the voice delay/feedbackAt/grainSize, the track delayTrack/pingpong/tape/bbdDelay/grainSizeT, holdFor, and the shred wait/trigger/record — because the fold now runs in the unit domain (fold_unit_duration) and converts once through seconds_from_unit / beats_from_unit, the duration twins of rate_hz_from_unit. It used to convert each leaf to seconds first, which read (N/Mc) as N / (M · cycle_seconds): wrong by cycle_seconds² and tempo-INVERTED, so a slower tempo made the echo shorter. Not (1/8) — the same truncating integer division, rejected here too, along with a duration that folds to zero or a negative in the delay/grain family (2026-08-22, production-gaps §28 X1).
3. What deliberately does NOT exist#
- No
Temporaltrait — the laws are shared, the dispatch is amatchin the lowerer (Traits §4 still owns user-extensible instances, which nothing here needed). - No unified
Timelinetype over patterns/shreds/scenes — closed; the ecosystem's answer is three peers with conversions (Musical time for patterns and signals §5). - No runtime mechanism at all for §1: the folds are compile-time rewrites of existing node parameters, byte-identical to writing the folded values by hand.
4. Code anchors#
compiler/src/check.rs—infer_app's signal-subject rule.compiler/src/lower/expr.rs—lower_temporal_combinator,scale_modulator_rate,reverse_modulator.compiler/src/lower/track.rs—lane_lfo_signal(+lane_rate_hz,reject_continuous_lane), thePan/PanTrack/Gainlane arms.ir/src/ir.rs—LaneLfo,AutoPan { pan, lfo },TremoloTrack { gain, lfo }.engine/src/effect.rs—TrackLfo,TremoloT, theAutoPanLFO branch.- Tests:
compiler/src/lower/tests.rs("unified-time §2"/"§4" clusters),engine/src/effect.rs(auto_pan_lfo_rides_the_field,tremolo_track_rides_the_gain), goldenunitime.