VOLTAreference

Reference / Start here

Learn with guided exercises

Start here

Open in the workbench →
On this page · 7 sections

Follow short, editable lessons with musical goals, starting with a first sound and building toward more complete pieces.

The interactive course in the workbench: 39 goal-checked lessons across eleven acts, from a first sine to performing a set and writing your own DSP. An open lesson has three presentations over one authoritative editor buffer: Side-by-side, Lesson focus, and Notebook build (§1b). Design: Learn with guided exercises.

The separate Structure Lab is hidden from launch navigation while its course and key guide receive further polish. The language/synthesis course remains available in full.

1. Using it#

The Course destination opens Learn as a contextual workspace. At wide widths the lesson can sit beside source; at compact widths the active Learn or Guided surface owns the useful canvas. Opening a lesson stashes the user's song and suppresses autosave; leaving Learn restores it verbatim. Each lesson loads its starter into the real editor and plays through the real engine — the pinned mini-scope (waveform + spectrum) rides along, and every edit recompiles and re-checks the lesson's goals live. Opening a course page installs that song in the engine straight away (transport stopped, so the lesson's own "press ▶ Play" goal is still the learner's move); Run is never a prerequisite for the goal checklist.

Leaving is never destructive. The buffer you were working on is stashed per lesson (work) and restored on re-entry, so closing the panel, pressing Escape, or wandering the course map all keep what you typed. "reset code" reloads the starter (and asks first, if you'd changed anything); "keep this as a song" lifts the buffer out of the course into the song library.

Returning to the course map immediately saves the departing lesson's work — but NOT the user's song: stepping back to the index is a move inside the course, so the lesson's buffer stays in the editor and its sound keeps playing, and Continue lesson resumes rather than restarting from bar 1 in silence. Leaving LEARN itself still restores the pre-lesson song verbatim (LearnUi::to_course_map, read by VoltaUi::leave_tutorial). A lesson still loading cannot acquire another lesson's buffer or score its goals. Cancelled and superseded load requests cannot replace the final selected lesson, and audible-time checks pause until the current editor revision has compiled. Reset to starter offers Keep my edits and Replace with starter when the current code has changed.

The lesson flow is act/title → prose → goal checklist → progressive hints → solution comparison → riff sandbox → further reading. Its masthead keeps Course, act/title, position, and presentation choices pinned; Back and Next stay in a pinned footer while the lesson body scrolls. Body prose is the type scale's Body role ×1.1 (learnview::LESSON_READING_SCALE, 1.45 leading), the SEE IT callout and goal titles plain Body, all at the active density; the prose is capped at 70 characters (learnview::PROSE_CH) of the reading face's measured average advance, and reads visually stronger than chrome.

While a lesson holds the editor, the Source pane's breadcrumb reads learn · <lesson> · <song>.volta is safe, back when you leave (the Structure Lab's: structure lab · scratch · …, its swap notice scratch updated ✓), and the main bar's SONG slot names the borrowed buffer instead of the song (LESSON / LAB SCRATCH, with the song's safety in the hover) — a lab starter at 120 BPM under a header still reading the song's title looked like the song had been overwritten. It reports playing / stopped — not the shell's own suspended / swapped ✓ — and the cross-surface RELATION · Track link rail stands down, so a caret on a play line no longer pushes the code down a row to offer a producer's pinning control (ui/src/app/sourcepane.rs).

The pinned mini-scope and spectrum ARE the play button: clicking either tile while the transport is stopped starts the lesson's sound, since the ▶ the tile names lives in the transport at the other end of the window (scopes::mini returns its click). The Structure Lab's monitor is calmer: stopped, it is one quiet tile the same height as the live pair, with a drawn ▶ and one centred hint (click to play); playing, the pair is unringed.

The lesson's monitors and its live play-audio goal belong to the lesson's OWN song. Opening a song while the panel stays docked sets LearnUi::foreign_song, and the scope, the goal checks and the audible-time accumulation all stand down rather than scoring somebody else's music (ui/src/app/shell.rs).

  • Goals show ✓ or ○, label stretch work explicitly, and use · later step while a Guided goal is gated. The counter reads GOALS · m OF n · STRETCH j OF k when the lesson authors stretch goals, so a third ticked row is never missing from the tally. What a lesson has proved is sticky per lesson (LearnUi::proved): clear_checks wipes the live results on every entry/exit handshake, so a re-entered lesson would otherwise show ✓ lesson complete above a reset checklist. Only Reset to starter clears the marks. A failing goal keeps the checker's why-not visible and offers authored hints one at a time. New content uses hints: string[]; a legacy singular hint is normalized to the first item. Stretch goals don't gate completion.
  • Review solution diff renders a line diff against the starter (what changed, not a wall to paste). Load full solution is a separate final action that explicitly replaces the lesson buffer.
  • Completion = all required goals pass → the checklist pulses, the lesson ticks in the map, and a Lesson complete card appears directly under the goals with Next lesson → on it (the pinned footer's next › is still there; the card is where the reading actually ends).
  • keep as song leaves a receipt on the card naming where the song went and how to reach it (the SONG chip, or File ▸ Open Song… ⌘O).
  • The play-audio goal is live: the shell accumulates seconds of audible master-bus signal while the lesson is open, from the viz frame's master RMS (ui/src/app/shell.rs) — one path in both shells.
  • A buffer that stops compiling pauses the checklist rather than scoring a stale IR: the goals box shows the compiler's first line and every IR goal reverts to ○ until it parses again.
  • Further reading uses a human label such as Read more: Language fundamentals. It opens the exact Reference document and optional anchor, carrying the lesson id and selected presentation as its return target; the Reference return action restores that lesson/presentation without replacing its work buffer.

1a. Course landing and progress#

Course opens with one primary Continue lesson card. Its resolver uses the current lesson while that lesson still has work left in it; a finished current lesson resolves FORWARD to the next unfinished one, then to the earliest unfinished lesson anywhere, then to the first authored lesson (which is also how state naming removed content recovers). The card shows act, course position, the lesson's title with its state (NEXT / RESUME / COMPLETE), the manifest's concise outcome, optional minutes, and total completion.

The lesson list below (labelled Lessons) is a map, not a lock graph. Acts become three columns at 1,050 pt of available map width, two at 680–1,049 pt, and one full-width column below 680 pt. Every lesson remains directly available. Each act reports its own completion, while lesson rows distinguish CURRENT, COMPLETE, STARTED, NEXT, and untouched states. Course settings contains a confirmed Reset course progress action; confirmation clears current, started, completion marks, and every session-stashed lesson buffer.

Learn progress separates concepts that the old single current field conflated:

  • current is the durable Continue target (the most recently opened lesson);
  • started records every lesson entered, even if it has not completed;
  • done records lessons whose required goals have passed;
  • active is only the lesson workspace currently open; and
  • work is the one in-session edited buffer stashed per lesson.

The flat shell-state representation persists these as learncurrent=, learnstarted=, and learndone=. The legacy learn= key remains accepted and still means the active lesson, so old state restores forward. Presentation state remains backward-compatible through learnfocus=1 and learnnb=1.

1b. Lesson presentations and the guided notebook#

Presentation is chosen only after a lesson opens; the Course map has no cards/notebook preference. The masthead exposes three destination tabs:

  • Side-by-side — the lesson and live editor share the canvas evenly, and evenly is enforced: SidePanel::default_width only applies the first time the panel exists, so the shell stamps the composition's own fraction (0.50 side-by-side, 0.57 lesson focus) onto the right panel's stored state whenever it changes (ui/src/app/shell.rs). Before this, a lesson re-entered from the course index inherited whatever width the previous surface left behind;
  • Lesson focus — a wider lesson beside the same live code; and
  • Notebook build — instructional prose and live declarations build up together through the Bench notebook.

Switching presentation is layout-only: it never loads a starter, forks work, or changes the stash. The current editor buffer remains authoritative across all three modes and across a reversible Reference detour.

At compact widths, Edit code opens the current lesson's source and Back to lesson returns to its instructions. This handoff keeps the active lesson, work buffer and progress; the main Source destination still exits the course.

Switching ordinary lesson code into Notebook does not append guide steps to that code. The build strip explains how to start the guided version using Reset to starter; step controls appear once the buffer has guide markers. Goals on an unstaged buffer remain available. Tutorial notebook cells always show their source and omit the per-cell hide source / show source row.

Notebook build turns the center into real Bench cells — lesson narration as prose cells, declarations as live cells with LED rows, scrub, and docks — while the Learn panel holds goals, the build strip, and navigation. The panel's body slims because the full narration is in the notebook. Shell-state view token: benchlearn.

  • Content: an optional per-lesson guide.volta — the starter re-authored as a build-up, -- prose blocks interleaved with declarations, staged by column-0 -- ▸ <title> marker lines (parser: learn.rs::parse_guide). A guide may be markerless (an unstaged notebook). Lessons without a guide open their flat starter in the notebook.
  • Opening loads prelude + step 1; the goals panel's BUILD strip offers + <next step title>, which appends the next chunk (immediate recheck + live swap). Step 1 must sound; every prefix must compile — the literate cumulative contract, enforced by validate-lessons.mjs.
  • Step progress is derived from the -- ▸ lines in the buffer itself — no counter to persist, so it survives work-stash round trips, undo and restore. It does not make the session-only lesson buffer durable across an application relaunch.
  • A goal may carry a 1-based "step"; the checklist shows it dimmed as · later step until that step is in the buffer. Completion semantics are unchanged (all required goals).
  • In course mode the tutorial compile path owns the live swap (the bench's own swap debounce is suppressed — no double hot-swap), and comment-only notebook edits still recheck (prose edits must keep the checked source and step count honest).
  • The tutorial compile reads the IR the background analysis already produced. The browser's analysis crosses a Worker boundary and arrives WITHOUT it (Analysis::ir_json is serde(skip)), so a shell in that position compiles once through the bridge (learn_compile) rather than reading the miss as "this doesn't compile": without that fallback a browser course page loaded its text, paused the checklist and left the lesson out of the engine until the learner pressed Run (2026-09-13).
  • Presentation: the course notebook opens with quiet cells on — each cell's tool chrome hides until pointed at, so a lesson reads as prose, code and readouts. The course carries its own copy of the preference (the top-strip quiet chip opts out, persisted benchcourseloud=1); the ordinary notebook's benchquiet pref is untouched. See The Studio workspace § Surface.
  • At compact widths Notebook build takes the full canvas. Its takeover banner includes Lesson, which returns to the full-width lesson without touching the notebook buffer or changing the selected presentation. Choosing the already-selected Notebook build tab returns to that same notebook; choosing Course drops the notebook canvas and shows the full-width map.
  • Screenshots/tests: volta-shot --views benchlearn --lesson-nb <id>, VoltaUi::open_lesson_notebook.

2. The pieces#

PieceWhere
Goal engineui/src/learn.rs (the shipped checkers); runtime/src/learn/checkers.ts is the offline twin the validator runs
Content loaderEngineBridge::{learn_index, learn_lesson} — app-native/src/main.rs reads the tree, runtime/public/studio-host.js fetches /learn/ and pushes it in
Lesson player (shared native/browser Rust UI)ui/src/learnview.rs
Curriculum contentruntime/public/learn/<id>/ + index.json
Compile/recheck hookthe background analysis's result (ui/src/app/buffers.rs, falling back to learn_compile when the IR did not cross a Worker) → tutorial_after_compile + recheck (ui/src/app/shell.rs), then a live hot-swap so edits are audible
Progress/presentation persistenceui/src/app/prefs.rs
Exact Learn ↔ Reference handoffui/src/learnview.rs + ui/src/app/rightcol.rs
Validatorruntime/scripts/validate-lessons.mjs (npm run test:lessons, make test-lessons)
Checker unit testsruntime/src/learn/checkers.test.ts (vitest)

Each index.json lesson entry carries id/title plus optional backward-compatible outcome and minutes metadata. A lesson directory holds lesson.json (id, title, act, intro, body, optional watch/focus, goals, riff, further) plus starter.volta, solution.volta and optionally guide.volta (§1b). Starters — and a guide's first step — must make sound immediately; solutions must compile AND pass every non-live goal — validate-lessons.mjs enforces both, plus "the starter fails at least one required goal" (and the full guide too), every guide prefix compiling, manifest metadata/title/act agreement, progressive-hint shape, and checker-id/further-pointer integrity. That script is the authoring feedback loop: run it after any lesson edit.

3. Goal checkers#

Checkers run against { module: ir.Module | null, source, audibleSeconds } and return { pass, detail } — detail is a friendly failure sentence. IR checkers walk a synth field's reachable subgraph (fields[f] → transitive inputs), so "an envelope drives the cutoff" means the Adsr node is genuinely in the cutoff's DAG.

Inventory (params abridged; see checkers.ts doc comments):

  • Module shape — compiles, tempo-set (min/max), track-count, synth-count, macro-count, key-set.
  • Macros — macro-exists (name?), macro-modulated (third-arg graph), macro-drives-field (MacroRef reachable from a field).
  • Patch internals — field-contains (ops reachable from a field), field-references-note-param (vel/pitch), adsr-shape (per-segment min/max bounds on the env rooted at a field), osc-shapes (shapes any-of / minCount / minDistinct), filter-present, resonance-min (const fold), voice-effect (per-voice effect node families), glide-set (+legato).
  • Patterns & tracks — event-count, distinct-pitches, velocity-varied, has-slide-event, euclid-onsets (k, n, rotation — matches the exact mini.rs::euclid_pulses Bjorklund grid, any rotation by default), pattern-spans-cycles (via cycle_beats), sample-track, track-effect (op families + a min on the first numeric knob, macro-Param-aware).
  • Songs & shreds — song-scenes, song-arrange, song-transitions, scene-automation (ramp?), shred-ops (required op kinds, walks nested loops).
  • Syntax — code-includes / code-excludes (regex + flags + authored describe) for goals that are purely notational (<a b>, [ ], [a [b c]], *2, !N, @N, roman numerals, arp "up", scene algebra, named chains, "you removed the bitcrush"). They match comment-stripped source — a -- line is cut at the first -- outside a "…" string — so a keyword a learner only wrote about never satisfies a goal, and commenting a stage out counts as removing it. source-includes / source-excludes are the identical comment-blind twins, kept for goals that really are about prose; every course lesson uses the code- pair. Absence is its own checker because Rust's regex crate has no look-around.
  • Live — play-audio (seconds of audible master signal; skipped by the offline validator).

4. The curriculum#

39 lessons across 11 acts; every lesson of acts I–X ships a guide.volta (2026-08-13; act IX added 2026-08-31; act XI added 2026-09-25, flat starters without guides):

ActLessons
I · First lighthello-sound · waveshapes · envelope · filter
II · Patternsrests-and-steps · alternation · euclid · drum-machine · pattern-controls
III · Sound designcutoff-formula · fatness · noise-and-grit · glide-303
IV · Modulationlfo-motion · macros · velocity-feel
V · Space & effectsreverb-delay · voice-vs-track · sidechain
VI · Harmonykey-and-numerals · arp-and-scale
VII · Structurescenes · arrange-journey · tempo-journey
VIII · Mix & finishmixing-desk · parametric-eq · bounce-freeze
IX · Reusethe-library (the one lesson that uses import)
X · Liveshreds · capstone-perform (multi-goal + a 20s live play-audio)
XI · Write your own DSPdsp-first-kernel · dsp-state · dsp-biquad · dsp-delay · dsp-effect · dsp-oversample · dsp-oscillator · dsp-pluck · dsp-spectral

Act XI follows User Dsp Tier Stage 11: first kernel (process) → state (process2, a one-pole in Hz) → an RBJ biquad → delay lines and a feedback echo on the track → the effect declaration with a scene-addressed parameter → oversample (and the ADAA helpers) → a source kernel: a naive processO saw whose aliasing the spectrum and the probe's oscillator view show, fixed with a polyBlep, named as an oscillator whose parameter a macro rides (dsp-oscillator) → a Karplus–Strong string from a noise burst, a dline and a two-tap loss filter, damped by noteGate (dsp-pluck) → a per-bin spectralMap law. Its goals are code-includes/code-excludes regexes plus macro-drives-field and track-effect (AudioFx); the recipes behind it are DSP cookbook.

Authoring rules (enforced or reviewed): motivation first, starter already sounds, 2–4 goals graded easy → stretch (capstone gets 5), a solution, a further pointer into docs/reference/. The validator enforces the stretch goal itself — capstone-perform is the single allowlisted exception — and, for a lesson that gates goals with step, that every step names a real guide step.

Authoring gotchas (learned the hard way)#

  • Velocity =N attaches to single tokens only — [bb1 c2]=0.5 and <a b>=0.5 are parse errors; write [bb1=0.5 c2=0.5].
  • supersaw/unison are signal-first (saw … |> supersaw 7 25), and |> binds looser than + — parenthesize a piped stack before adding layers.
  • humanize is pattern-first (!"…" |> humanize 0.3).
  • A named track-transform lambda (hall = \t -> …) must come LAST in a track's effect chain — further |> after it won't re-enter track position.
  • The euclidean grid the compiler renders is Tidal's Bjorklund (first pulse on slot 0) with rotation shifting pulses earlier — bd(3,8) onsets are slots 0, 3, 6 (x..x..x.).
  • Guide steps may only add top-level names (a later step redefining one is a duplicate-binding error), so "now add an effect to that track" cannot be staged as code — stage it as a prose step telling the learner what to add to the cell above, or introduce a fresh binding. Anything inside one song { … } block (arrange/edges/with) likewise cannot arrive in stages.
  • |+ refuses Pattern Pitch (numeric patterns only), chop is pipe-only, and meter is a reserved word (time-signature grammar).

5. Verified#

  • checkers.test.ts — 31 vitest cases pinning checker semantics and hint normalization on synthetic IR (including the compiler-exact euclid grid and the comment-immunity of the code- pair); cargo test -p volta-ui --lib learn pins the native twin. The native learn::euclid_onsets calls the compiler's own mini::euclid_pulses (no third copy), so an explicit rotation goal is judged the same in both shells; euclid_onsets_match_compiled_mini_notation_over_a_grid compiles c3(k,n,r) for n ≤ 12, k ≤ n + 1, every r and compares, and euclid_onsets_explicit_rotation_matches_compiled_song judges a compiled bd(3,8,2) against rotation 2 (passes) and 0/1 (fail).
  • validate-lessons.mjs — all 39 course lessons plus the standalone Structure Lab: compile ×2, solutions pass all non-live goals, starters leave work to do, every lesson carries a watch/focus for the pinned scope and a stretch goal, every guide prefix compiles (opening on sound, still leaving work to do), and the code- regex goals accept their alternative spellings (rotate either direction, @ 2c as well as @ 1/4c, any lambda parameter name, a re-tuned reverb mix, a stage commented out rather than deleted …). A lesson may import from the standard library: the validator mirrors the modules runtime/public/lib/index.json declares into a scratch dir and points VOLTA_STDLIB at it, so the CLI resolves exactly what the browser's ModuleLibrary serves at /lib/ — a lesson reaching for any other corpus song fails here instead of 404-ing in a learner's browser.
  • CDP browser smoke (headless Chrome): open learn → course loads → starter in editor → map navigation → solution diff → load solution → recompile → goals pass → completion pulse → progress persisted; no console errors.

6. Deferred (design doc §3 Stages 5–7)#

Audio-analysis goals (offline-render checks), spaced-repetition review, community-authored lessons, and richer completion celebration per act.

Checker gaps the "Mix & finish" act exposed (its goals fall back to code-includes where the IR is well-formed but unread): Module.mixer / buses / groups / master have no checker at all; pattern-control lanes (gain_pattern, pan_pattern, …, TrackEvent.controls, clock_ratio) are invisible; tempo-set sees only the map's beat-0 scalar, never its shape; field-contains matches node op only, never node fields (Sample.slices from chop, EQ band kinds…); Module.bounceDefs is unread.

Native/browser differences that remain: per-lesson work buffers are session-only in the shared Rust workbench even though current/started/done and presentation state survive a shell-state round trip. "Keep as song" writes into the on-disk song workspace on desktop and the browser library on web.