VOLTAreference

Reference / Movement and control

Draw a change, and record one

Movement and control

Open in the workbench →
On this page · 13 sections

A filter that opens once over twenty-four bars is one gesture. A reverb that comes up in the breakdown is one curve on the reverb, not a scene per bar. A fader move you made with your hand, and then kept, is the same thing again — written down.

Hear it and change it#

Listen for: the filter opening across the whole middle of the song while the bass phrase repeats underneath it, and the reverb coming up under the last eight bars — one gesture each, neither of them a scene.

tempo = 112bpm
meter = 4/4

tone = macro 500Hz

bassV = patch { source = saw (freq pitch) |> lpf tone 1.2,
                ampEnv = adsr 4ms 160ms 0.5 180ms, gain = vel * 0.32 }
bassIdea = !"<c2 ~ g1 ~> <c2 ~ ~ eb2>"

bassT = play bassV [region 0bars 32bars bassIdea] |> reverb 0.7 0.04

opening = automation {
  points = [[0bars, 500Hz, "exp"], [24bars, 5200Hz]]
}
swell = automation {
  points = [[0bars, 0.04], [8bars, 0.42, "cubic", 0.02]],
  interpolation = "linear"
}

brightness = automate tone [region 4bars 24bars opening]
space      = automate "bassT.reverb.mix" [region 24bars 8bars swell]

main = song { timeline = [bassT, brightness, space], length = 32bars }

Make it yours: change "exp" to "linear" on the first point and play it again. The same two numbers, the same twenty-four bars — and the sweep now spends its first half somewhere it never used to be, because a filter is heard in ratios and a line in hertz is not a line in the ear.

What a lane is#

automate <destination> [region at length <curve> { … }]

An automate binding is a lane: curve regions aimed at one destination, writing that destination's base. Everything downstream of the base — the modulation algebra, a live hand on the knob, the destination's own smoothing — keeps working exactly as Macros and automation describes.

A lane is a timeline entry like a track: it goes in the song's timeline list, and its regions are placed, offset, stretched and split by the same rules as a note region (Write a song as a timeline).

Destinations#

You writeIt means
automate tone […]a declared macro, by binding name
automate acid.cut […]a macro of an alias-imported module, by its flattened name
automate "bassT.level" […]that track's channel-strip fader
automate "bassT.pan" […] / .widthits placement in the stereo field
automate "bassT.sends.hall" […]one declared send's level (presends.<bus> for the pre-fader one); an imported module's bus keeps its namespace, sends.pipeworks.room
automate "bassT.reverb.mix" […]one named insert's modulatable parameter
automate "master.plate.mix" […]the same, on a bus or on master
automate "padT.warmth.amount" […]a user effect's declared parameter, by the owner that applies it (a track, bus, master, or the patch for an on voice effect — Write a custom audio processor § Live effect parameters)
automate "keys.lpf.cutoff" […]a builtin stage written in a patch, by the builtin's name and its parameter (§ "Inside a patch")
automate "keys.cutoff" […]a field of a patch — the classic voice's cutoff / resonance, or any field holding a constant

A name that resolves to a declared macro is a macro, dot or no dot — a macro of an alias-imported module flattens to acid.cut (Names and library imports §3) and is as automatable as a local one. Anything else with a dot in it is a parameter address. The address is the one the mixer already speaks (Mixing tracks and buses §1), so a scene that rides a site and a lane that rides it are the same destination — and naming both is the conflict below, not a race.

A lane places curves on the song's time axis, so a pattern is not lane content: automate m (!"0.1 0.9") is a compile error, and it points at the statement that does take one — modulate m (!"0.1 0.9") (Macros and automation §1.3), which adds a modulation graph riding on top of the base rather than writing the base itself.

Addressed by name at every level, never by position. Inserting an effect before another effect cannot retarget a lane. Two inserts spelled the same word on one chain make the address ambiguous, and that is an error naming the count — because adding the second one later would otherwise move the lane in silence. Aim a macro at the one you mean and automate the macro.

An address that names no track, no insert, or a parameter the effect does not have is a diagnostic that says which, and lists what is addressable there. It never automates a neighbour.

What the lowerer does with an address#

It resolves it to a runtime slot: an internal mix:<address> macro, minted once, substituted at the site, seeded with whatever constant the site already declared — so a render with no automation at all is bit-identical to the un-automated one. The plan records the whole reference:

ParamRef { owner: "bassT", scope: insert("reverb"), key: "mix",
           slot: "mix:bassT.reverb.mix", unit: "Normalized",
           range: [0, 1], cadence: sample }

The inspector reads cadence to say when the next update will be heard, because a shared timing model does not upgrade every DSP destination to audio rate. The cadence is the parameter audit's, not a list of parameter names: it is read from compiler/assets/parameter-audit.json — the census docs/parameter-inventory.json is generated from, and the one place that knows which destinations are which — and the lane records the profile it came from, so a reader can ask the census for the profile's own sentence rather than a paraphrase.

Audited profileCadenceThe inspector says
control-curve, mixer (fader, sends), sample, global, tail-motion, live-filter, live-delay-time, live-timbre, live-time, live-tape-bias, live-simulation, limiter-live, musical-clock, …sample"continuously, inside the block"
control-block, endpoints, eq-grid, voice-block, plugin, and a strip's pan/width (a stereo matrix rebuilt per block)block"at the next audio block"
analysis-hophop"at the next analysis frame"
note-latch, gain-event, last-note, and a region's control curvenote"at the next note"
pattern-latch, trigger-latchstep"at the next step"
grain-latch, pulsaret-latchgrain"at the next grain"

A name list got this wrong in both directions — compress.amount, deess.amount, mbcomp.amount and ott.amount are audited control-block while blur.amount and freeze.amount are analysis-hop, all six of which it reported as per-sample; and clipOnly.ceiling, rotary.speed, inflate.curve and fatEq.out are control-curve and were reported as block. Every automatable destination is cross-checked against the census by compiler/tests/timeline.rs::every_lane_cadence_comes_from_the_parameter_audit, which also refuses an audited profile that maps to no cadence at all.

The address is resolved late, once the mixer, buses, tracks and their insert chains have been lowered — apply_scene_mixer_addresses (compiler/src/lower.rs) mints the slots, then compiler/src/lower/timeline.rs::resolve_param_lanes finishes every lane — because the destination's unit, travel and cadence cannot be known while the automate binding itself is lowering. Everything that needs that unit happens in this pass: the curve is typed against it, a dB gesture on a linear fader is linearised, and a travel-mapped insert slot's surface numbers are inverted through its range. Those conversions are applied to a copy of the curve under a slot-qualified id, so one curve shared by two destinations is converted once for each and never twice for either.

A site can only have one owner#

A destination that already rides a macro you declared is not available to an address: the lane is refused, and told to automate that macro directly. A site that carries a persistent control expression is refused for the same reason, and told to automate one of its inputs. Two spellings of one site (master.plate and master.plate.mix) are refused as the one site they are.

Inside a patch#

<patch>.<field>             keys.cutoff
<patch>.<builtin>.<param>   keys.lpf.cutoff     keys.lpf.q     acid.diode.cutoff

The owner is a patch declaration by name. The scope is a builtin stage written in that patch, by the name it is written with, or one of the patch's fields (a two-segment address is a field). The key is the builtin's parameter name — the same name the Rack case prints under the knob. The site reads a minted mix:<address> knob seeded with its constant, exactly as an insert address does, so nothing about the voice moves until a lane or scene moves it; every sounding note of the patch follows the one knob.

keys = patch { source = saw (freq pitch) |> lpf 800Hz 1.2,
               ampEnv = adsr 5ms 100ms 0.6 80ms, gain = vel * 0.3 }
t    = play keys [region 0bars 4bars (!"c3 e3 g3 b3")]

open = automation { points = [[0bars, 300Hz, "exp"], [4bars, 6000Hz]] }
lift = automate "keys.lpf.cutoff" [region 0bars 4bars open]

main = song { timeline = [t, lift], length = 4bars }

A scene speaks the same address, in the site's own unit: with { keys.lpf.cutoff = ramp 400Hz 4000Hz } (a bare number there is refused — the site reads Hz; a Q or a normalized amount is written bare).

The site holdsWhat the address does
a constant (800Hz, a named constant, a folded expression)reads the knob
a sum with exactly one constant term (260Hz + filtEnv * 900Hz)that term — the base — reads the knob; the modulation rides on it, as the Rack case draws it (an offset knob with rings)
a bare macrorefused: automate that macro
a product, a subtracted constant, or two constant termsrefused: no single base to write
a compile-time (Live::Const) or structural portrefused: fixed when the voice graph is built

Two stages of one name in one patch make the address an error naming the count, as two inserts do. A builtin inside a user function the patch calls (cathode's wreck) is not a stage of the patch: the address is refused and tells you to give the function a parameter and pass it a macro. A miss lists everything addressable in that patch.

The cadence is the parameter audit's reading of the builtin in a voice graph (call/lpf/overload/graph/cutoff is sample; an adsr time is note-latch, "at the next note"); a classic cutoff / resonance field is the implied lpf's. A lane on a stage records scope: insert(<builtin>), the scope a voice effect in a patch already uses; a lane on a field records scope: field. Design: Patch Addresses.

Curves#

shape = automation {
  points = [[0bars, 300Hz, "exp"],
            [8bars, 4000Hz, "cubic", 200],
            [16bars, 1200Hz]],
  interpolation = "linear"
}
  • Points are in the destination's own unit. A curve aimed at macro 800Hz is written in Hz; a bare number is refused there and is the right spelling for a normalized destination. A fader also accepts dB, which is linearised into what the engine reads exactly as level = -6dB is.
  • Times are content units (base cycles — one base cycle is one notational bar), strictly ascending, at least one point.
  • interpolation is the curve-wide default segment; a point's optional third element overrides the segment that leaves it, so the last point's is never read.
SegmentBetween two points
"hold"step — the left value until the next point. The default.
"linear"a line, in the destination's own unit
"exp"a·(b/a)^t — a ratio sweep. Both endpoints must be strictly positive; through or to zero is a source-local error, not a silent fallback.
"cubic"monotone (Fritsch–Carlson) through the run of consecutive cubic segments — it never overshoots between its own points

A cubic point takes an optional handle as a fourth element: the slope at that point, in value-units per content unit. An omitted handle is derived from the neighbours; an authored one is clamped to the monotonicity limit, because cubic promises no overshoot and a handle steeper than three times the secant would break that.

The editor's display scale is not the curve. A logarithmic Hz ruler is a way of drawing; playback interpolates in the destination's own unit either way. Spell the ratio sweep "exp" and you get one; draw on a log ruler and you do not.

A curve holds its endpoints: before its first point and after its last it reads that point's value, and only inside its region's half-open span.

One base writer, and how to have two#

One authored base writer per destination at a time. Two overlapping curve regions on one lane, two lanes aimed at one destination, or a scene ramp and a lane on one destination are compile errors naming both writers — a static placement is knowable before anything plays, so it is answered then instead of by whichever writer ran last.

The error's hint is satisfiable. @compose on the lane binding says how two overlapping base regions combine, and only then may they overlap:

padT = play pad [region 0bars 32bars padPat] |> plate 0.6 0.3

rise = automation { points = [[0bars, 0.05], [16bars, 0.6]], interpolation = "linear" }
fall = automation { points = [[0bars, 0.6], [16bars, 0.05]], interpolation = "linear" }

@compose "crossfade"
haze = automate "padT.plate.mix" [region 0bars 16bars rise,
                                  region 12bars 16bars fall]
@composeIn the overlap
(none)a compile error — the default, and the shipped rule
"crossfade"the outgoing curve fades out as the incoming one fades in, linearly, across the span they actually share
"priority"the later placement wins outright

Attachment: whose clock a gesture runs on#

KeyThe curve's time runs on
attach = "region"its own placement — the default. Move the region, the gesture moves.
attach = "song"song time. A gesture positioned by the song and merely displayed in a region.
attach = "marker"the marker span it starts in: the placement snaps to that section's start and length, and moving the marker moves the gesture.
markers = form.markers [[4, "build"], [12, "drop"], [24, "out"]]
padT    = play pad [region 0bars 32bars padPat]

lift = automate "padT.level" [region 14bars 2bars swell { attach = "marker" }]

lift is not a placement at bar 14 for two bars; it is the drop's gesture, resolved to bars 12–24. Move the drop marker to bar 10 and it follows. The plan records which marker owns it, so the inspector can say so.

A song with no markers, or a placement before the first one, says which.

A gap is a place too#

Between a lane's regions the destination plays its declared base. @gap "hold" on the binding plays the preceding region's final value instead, evaluated analytically — so seeking into the gap reads the same value as playing into it.

A curve that belongs to the notes#

A lane writes a shared destination: one base, one writer. A voice parameter is not shared — every note has its own — so a curve on one can belong to a placement instead:

gritV = patch { source = saw (freq pitch) |> drive (trackParam "grit"),
                ampEnv = adsr 3ms 120ms 0.4 140ms, gain = vel * 0.35 }

idea = !"c2 ~ eb2 g2"
rise = automation { points = [[0c, 0.05], [1c, 0.9]], interpolation = "linear" }
fall = automation { points = [[0c, 0.9], [1c, 0.05]], interpolation = "linear" }

t = play gritV [region 0bars 8bars idea { control = { grit = rise } },
                region 4bars 8bars idea { id = "answer", control = { grit = fall } }]

Every onset the placement admits latches its curve's value at that onset's own position into the trackParam of that name. The two regions overlap, and that is fine: there is no shared base to conflict over, and each region's notes take their own curve. Two curves on the track's reverb would be the conflict above; two curves on the voice's drive are two independent gestures.

The cadence is note-latched by construction: a sounding note keeps the value it latched. That is the honest reading for a per-note parameter, and it is what the inspector says.

Modes: what happens when you let go#

A mode is session state, not source. It answers one question — what happens when your hand leaves the control — and the answer is the whole difference between trying something and keeping it.

ModeWhile you hold itWhen you let go
Readan audition: you hear your valuethe lane resumes at the current musical position, over the return time
Touchthe gesture is recorded, while armedthe lane resumes, over the return time
Latchrecordedthe value is retained — until stop, or until you change mode
Writerecorded, inside the punch rangeretained for the rest of the punch range, then the lane resumes
Off—the authored lane is ignored entirely: your manual base stands, and separately enabled modulation keeps running

Four things are true of all of them:

  • A hand on the control always wins while it is there. The takeover model is the shipped one; the modes only say what release means.
  • The clock never pauses. Every decision is a function of the block's own beat, so a seek during a takeover lands where the music is.
  • The return is to where the lane is NOW. Releasing a Touch ramps from the value your hand left to the lane's value at the current position — never back to the value the lane had when you first touched it. The ramp is 20 ms by default, and configurable.
  • Stop clears every retained gesture. A Latch that survived a stop would pin its destination through the next pass; stop must be silent for base writers too.

Arming is per destination, and a gesture on an unarmed destination is an audition whatever the mode says. Write records only inside its declared punch range, and lets go at the punch-out — positionally, so seeking out of the range hands the lane back immediately rather than at your next move. A Write with no punch range records nothing, rather than quietly behaving like Latch.

A controller with no touch detection has no release edge of its own: the host calls an explicit release, which is exactly what letting go of an on-screen knob does.

Across a recompile, modes, arms, held gestures and return ramps follow the macro's registry name and matching unit/range, even when declarations move. A new destination or changed unit/range starts in Read mode with no arm or retained gesture. See Macros and automation.

Recording, and Commit#

A recorded gesture goes into the same performance log as notes and track changes — one recorder, not a second one. Each point carries the engine sample timestamp beside the musical beat, the destination, the value and the gesture boundary, and the raw take stays there, so the smoothing below is reversible.

Commit turns a take into curve points you can see and edit. It runs off the callback and states its own error:

  • Simplification is Ramer–Douglas–Peucker in the parameter's perceptual scale. A frequency or a gain is heard in ratios, so that is the axis the error is measured on: an absolute tolerance in hertz would throw away the bottom two octaves of a filter sweep, where every hertz is audible.
  • The default bound is 1% of the destination's declared travel — an order of magnitude below the level difference a listener reliably hears on a fader move. Commit reports the maximum error it actually introduced, not the bound it was given.
  • Three things survive whatever the tolerance says: jumps (a step of a quarter of travel inside 0.02 beats is a switch, not a sweep), extrema (the top of a sweep is the gesture), and the endpoints, which are the punch edges when there is a punch range.

A straight gesture commits to its two endpoints and nothing else.

In Studio#

Automate appears wherever an automatable parameter is drawn: on the track's automation panel, which lists the mixer and insert destinations that track has, and as a context menu on the mixer strip. Choosing one writes the curve, the lane and the song membership in one transaction — automate tone for a macro, automate "bassT.reverb.mix" for an address — and a destination that cannot be automated refuses in the compiler's own words, which name the valid spellings on that chain rather than a paraphrase that could drift.

A lane's header in Arrange shows what it writes: a macro's name, or owner · scope · key for a parameter address, with the destination's declared unit. The inspector adds the cadence sentence — when the next update will be heard — because a lane on an EQ band and a lane on a fader are not the same promise.

The curve itself is the detail editor: breakpoints are draggable in both axes and write back to source. While you hold a point, it and its curve follow the pointer (its accessible name too). Releasing writes the point you see, and Esc puts it back. Each point carries its segment (and a handle when it is cubic), and every point has an accessible name, which is what the tests query. The display scale is a display scale — dragging writes the value in the destination's own unit whatever the ruler looks like.

In the Rack case a knob a lane drives wears a badge — its accessible name gains (lane <name>) — and while the song plays its ring follows the lane: an insert parameter on a track case, and on a patch case a builtin stage's knob (keys.lpf.cutoff on the lpf panel) or the classic voice's implied filter knob (keys.cutoff), placed on the knob's own sweep.

The transport carries the mode: an auto: <mode> ●n ▾ chip beside the metronome, where ●n is how many destinations are armed. Write uses the metronome's own punch window rather than a second range that could disagree with it. Commit take sits beside it and is enabled only when there is a recorded take to commit; it reports the maximum error the simplification introduced, not the bound it was given. A host that cannot record — the screenshot harness, for one — says so rather than doing nothing.

Seeking#

Automation is evaluated analytically at the seek target — every lane, every gap under the hold rule, every composition. Seeking to bar 20 reads exactly what playing to bar 20 would have read, with nothing replayed and no dependence on whether a region happened to play.

That is the automation half of a seek. What a seek does not promise is DSP history; Write a song as a timeline § "Seeking" names the two policies.

What is not here yet#

  • Discrete destinations take hold points only — the rule is implemented (ParamRef::check_segments) and the flag is in the plan, but no addressable destination is discrete today. Every address above is continuous, so the rule is exercised by a unit test on the validator (compiler/tests/timeline.rs::a_discrete_destination_would_take_hold_points_only) rather than by a corpus destination that does not exist. It is here so it cannot be forgotten when a mode selector or a step count becomes addressable.
  • Attachment to a CONTENT is not spelled. The three scopes that ship are the region (and therefore every occurrence a combinator makes of it), the song, and a marker span. A curve attached to a content, so that every independent placement of that idea inherits it, has no spelling: write the control record on each placement. A repeat or a concat over a region that carries one does carry it to every pass, because those make occurrences of the placement.
  • A stage inside a user function, and a track's inline patch, are not addressable. § "Inside a patch" reaches a builtin written in a patch declaration and the patch's fields; keys.wreck.rust.drive (a stage inside the function wreck) and t.lpf.cutoff for t = play (patch { … }) p are Patch Addresses Stages 4–5. Give the function a parameter and pass it a macro, or bind the patch to a name. A per-note curve on a stage is still the region control record.
  • Commit's write-back is the workbench's, not the language's: the engine hands back simplified points and the error, and Studio writes the automation { … } region. There is no source-side "record into this lane" spelling.
  • Host automation ownership — exposing these destinations as plugin parameters with persistent IDs — is Stage 6 of Unified Timeline.

Next steps#