# src
## Purpose
All engine, terminal UI, and live-control code for the nooise binary.
## Ownership
- `main.rs` — binary entry point: CLI parsing (`run`/`version`/`update`/`render`/`auto`/song code), wires up terminal + audio engine.
- `update_check.rs` — passive crates.io update notification helper; checks in the background and exposes a short TUI-safe message.
- `audio.rs` — cpal/audio-backend plumbing, sample callback wiring. The output stream asks for `LIVE_BUFFER_FRAMES` (256) clamped into the device's advertised range and prints the size it got; left to the OS, CoreAudio hands over whatever buffer the device was last set to, and every played note waits that long.
- `fluid/` — the core engine module:
- `mod.rs` — crate-facing glue: `run()` (TUI + live audio, with one random built-in Pad progression selected for a fresh session), `run_auto()` (TUI + live audio, slow-morph mode), `render_wav()` (headless wav render), `FluidTelemetry`. Song-code and auto starts preserve their authored progression; headless default renders remain deterministic. Also the shared numeric helpers every submodule reaches through the `use super::*` prelude: `smoothstep` (the one ease behind ramps and LFO glides) and `splitmix64_mix` (the one finaliser behind RNG-free seeded values).
- `auto.rs` — `nooise auto [BARS]`: `AUTO_STATES` baked-in share codes decode to a `Vec<SongState>` at startup (fatal on a bad code); `MorphState` holds the decoded endpoints — each a full `SongState` (controls + automation) — plus bars-per-leg and derives leg index/from/to/t purely from the live beat clock (no stored progress, so it stays render-deterministic); `MorphWriter` throttles the engine's control- and automation-reload tick to one recompute-and-store of both per 1/8 note.
- `controls.rs` — `FluidControls`, `MasterControls`, and per-voice control structs with defaults.
- `registry.rs` — the control registry: one `ControlSpec` table per tab (stable ID, label, kind, range, step, entry semantics, reset, accessors, display). `tab_controls`/`apply_delta`/`apply_reset`/`apply_value` all derive from it. Rows matching a common archetype use the one-line macros `gain_pct!`/`time_secs!`/`time_ms!`/`beat_interval!`/`beat_offset!`; a row deviating from its archetype in any field stays a plain `ControlSpec`. Each tab's table is one `layer_controls!` invocation listing only that layer's own base rows — the macro appends the shared tails (`chord_slot_rows!` for Pads, then `module_slot_rows!` for all eight slots), so no table hand-expands a slot row. Voice-type labels come from one `&[&str]` table per voice (`PAD_TYPES`/`BASS_TYPES`/`KICK_TYPES`/`TONAL_SYNTH_TYPES`) read through the shared `type_label`/`wrapped_index` (the one float-to-table-index wrap; Bass rhythm, Tonal phrase, and Arp pattern wrap the same way into their own tables), and each table-indexed control's `max` is `last_index_of` its table so range and labels cannot drift apart. Per-tab metadata (name, mute-target level id, control table) lives in the single `TAB_META` const, in discriminant order (test-enforced).
- `automation/` owns bounded automation stacks keyed by stable control ID. `lfo.rs` owns shapes, fields, rate pickup, and the inline `Steps` staircase. `envelope.rs` owns triggers and envelope fields. `mod.rs` owns shared addresses, lane storage, position-space summing, morphing, and the audio-side de-clicker. Each control can carry up to four LFO lanes and four envelope lanes. Lanes sum before one clamp/snap/de-click pass. Song code persists stacked lanes by repeating the control address in the existing container-v2 route records.
- `song.rs` — binary song-code export/import for controls, automation, mute state, and runtime-session records; owns the container version and every value encoding. `Ctrl+S` copies only the raw `n1_…` code and shows a short confirmation; `nooise <code>` or `cargo run -- <code>` applies the decoded song state before audio/TUI startup.
- `song_ids.rs` — `SONG_ID_TABLE`, the append-only control-id ↔ `u16` index mapping song codes intern against, plus `song_id_index`/`song_id_at`. Never reorder, remove, or reuse an index; append only. Deliberately not derived from `all_specs()` so registry order stays free to change.
- `palette.rs` — `/` control palette: fuzzy-find over every registry control id, Tab-autocomplete + inline value entry, staged batch edits committed immediately (Enter on empty prompt) or on the next bar downbeat (Ctrl+B).
- `interaction.rs` — pure semantic interaction model: cohesive navigation, exclusive keyboard-owning modes, input-phase policy, deterministic transitions, and ordered effect values.
- `runtime.rs` — Crossterm-only terminal adapter and deterministic fair scheduler: capability negotiation, transport normalization, typed semantic/deferred mapping, bounded input admission, tick cadence, and frame deadlines. Its test-only `recording` module is the single writer of the replay fixture grammar (`SanitizedTraceRecorder`, key/phase tokens) and owns the transport test scaffolding shared with `replay.rs`: `FakeClock`, `TerminalCapabilities::full`, `TransportEvent::key`.
- `replay.rs` — test-only production-path replay harness: sanitized raw-event fixtures (parsed here, written via `runtime::recording`), the scripted input adapter, shipped raw performance phase/fallback fixtures, exact binding postconditions, the shared production coordinator/tick seam, real scheduler/normalizer/kernel/effect/view/render traversal, and model-based invariant tests.
- `effect.rs` — ordered interaction-effect execution: aggregate control/automation/mute edits, auto exit, unit state, MRU/selection consequences, staged bar commits, clipboard, and typed acknowledgements/failures that keep interaction ownership synchronized. Every bridge runs its effect list through the one `run_ordered` helper, and the arms that mutate automation go through `with_automation`, which loads the frame's automation once and acknowledges the generation the edit published. Effect execution calls *down* into `edit.rs` for those mutations; it must never call into `ui.rs`.
- `session.rs` — immutable `LiveSessionSnapshot` (controls, automation, tonal state, mute state) plus its single CAS-retry `ArcSwap` transaction boundary.
- `view.rs` — cohesive immutable `UiViewModel` projection from interaction ownership, one live-session generation, telemetry, and presentation-only state; owns typed help/notice precedence and the minimum terminal contract.
- `ui.rs` — pure Ratatui rendering from `UiViewModel` and the frame area, and nothing else: it must not mutate session state, execute effects, or poll input. `render` derives one `PanelFrame` per frame (open modulator surface, numeric entry, `ModContext`, live chord slot, bar width) and composes the panel from `draw_scrim`/`draw_tabs`/`draw_control_rows`/`draw_footer`/`draw_palette`, so every section reads the same frame. Owns the control/modulator row widgets, the shared `lane_line` both modulator lanes render through, slider markers, and the flipped display-unit (`FlippedUnits`) formatting.
- `coordinator.rs` — the shared production turn coordinator (`coordinate_production_turn`/`_event`/`_action`/`_tick`) plus the live `production_ui_loop`. `coordinate_production_action` is the only path from a semantic action to the kernel and effect executor; replay tests that inject an action directly call it against a `production_frame`, never a private copy. Scheduler-due ticks commit before that turn's events, and the live scheduler loop and the replay harness both cross this same ordering seam.
- `edit.rs` — every control and automation mutation a UI gesture performs: adjust/reset/set, unit flips, route removal and reseeding, and opening a modulator editor. `active_field` resolves the cursor to exactly one target, and `with_active_field` applies one `FieldOp` (`Adjust`/`Reset`/`Set`) to it, so the three verbs cannot drift apart; Delay's time rows step through the registry like any other (`contextual` supplies the loaded clock's grid); only the clock row's Sync/Free arrow flip is Delay-specific (`apply_delay_row`). `effect.rs` calls into this module, never the reverse.
- `visualizer.rs` — the ripple visualizer: `RippleField` (telemetry-driven animation state — field time, live kick ripples, chord hue), the `FluidWidget` that draws it, chord hues, and the `fluid_hsv`/`darken` colour helpers. Presentation only; it never reaches the audio engine.
- `module.rs` — per-layer module slots: the `ModuleKind` catalog, `Domain`/`Family`, and `ModuleSlot`/`LayerModules` state. A slot stores *which* module is loaded as a value; the catalog never appears in a control id. Filter is a post-synthesis module with amount, cutoff, resonance, and type; Perc, Bass, and Kick ship with factory Filter slots instead of bespoke filter controls.
- `widget.rs` — `Dial`/`DialScale`, the shared slider vocabulary every bar renders through. Owns all value-to-bar-position mapping and tapered position stepping; no other module derives a ratio.
- `engine.rs` — `FluidEngine` (voice mixer), gain smoothers, tempo clock, grid triggers, shared per-layer/master effect bank, master bus, and temporary gesture routing. `engine/gesture_audio.rs` owns its bounded gesture processors; see `fluid/engine/AGENTS.md`.
- `gesture.rs` — normal-browsing gesture vocabulary and fixed per-layer scalar envelopes. Amounts are evaluated from monotonic audio seconds, independently of tempo, key repeat, or UI cadence.
- `voice/` — one module per voice (pad, bass, perc, kick, tonal, clap, arp, lead) plus shared helpers (`midi_to_hz`, `tune_ratio`, `note_hz`, `soft_clip`, `normalized_lfo`, `mix_and_retain`/`mix_and_retain_mono`, `noise_filter_smoothing`) in `voice/mod.rs`.
- `fx/` — shared DSP building blocks (LFO, panner, reverb) consumed by voices. See `fx/AGENTS.md`.
- `synth/` — shared synthesis primitives (envelope, oscillator, noise, multi-operator FM) consumed by voices. See `synth/AGENTS.md`.
## Local Contracts
- UI interaction work follows `docs/adr/0001-unidirectional-interaction-architecture.md`: raw terminal input normalizes to semantic intents, one exclusive interaction mode owns the keyboard, pure deterministic updates emit ordered effects, and rendering consumes one immutable view model. Coherently coupled controls, automation, and user-audible runtime state publish as one aggregate `ArcSwap` snapshot through one transaction boundary; audio reads stay lock-free. The runtime must bound input work and complete visible frames within 50 ms of application-controlled time during continuous input.
- `fluid/interaction.rs` is the terminal-independent interaction kernel. `InteractionMode` is the only keyboard owner; mode-local data stays inside its enum variant. `PaletteMode` owns its query, selection, recents, locked value entry, and staged edits so no parallel palette overlay state can exist; selection wraps over the projected registry matches, invalid locked entry indexes are safely discarded, and confirm emits typed jump/commit payloads rather than requiring adapter-side reconstruction. Sequence uses the closed `PerformanceInstrument`/`PerformanceAction` vocabulary: Space owns the one-shot interaction, repeated activation is idempotent, and full/reduced-capability completion remains explicit kernel state. Normal browsing owns temporary `z/c/v/b` gestures without entering a new mode; their input latches track physical holds and quarantine keys across mode transitions. The retired Deck has no `p` entry. `Intent::phase_policy` decides whether Press, Repeat, or Release is accepted before a transition runs. Every `Intent` also declares its owners in `Intent::handled_by`; both matches are exhaustive over `Intent`, so a new variant must decide its phase and its modes, and each mode's transition function guards on that ownership instead of restating what it ignores. `InteractionModel::update` dispatches one function per `ModeKind`, which report their next keyboard owner rather than assigning it. `LAYERS` is the single layer table: page order, page↔tab translation, and the navigation each page opens all index it in `Page`/`Tab` discriminant order (test-enforced). `INSTRUMENTS` is the single performance-instrument table: selector key, page, and the level/shape/density registry ids all index it in `PerformanceInstrument` discriminant order (test-enforced); `registry::performance_target`, the runtime key map, and Sequence's key/name text read it rather than restating the four instruments. `InteractionMode::Lead` is the Lead play keyboard: `i` from any page opens it (`EnterLeadPlay` lands navigation on the Lead page first, so the rows it edits are in view), as does Enter on the Lead page (any row that is not a module drill or the Steps row — the coordinator upgrades `TouchSelected` there, after the module check; on `lead.steps` it becomes `EnterLeadPattern` instead), inside the pattern drill included. Arrow Up/Down moves its visible control selection and Arrow Left/Right emits the ordinary `AdjustSelected` effect, so a player can edit the selected knob without yielding Lead ownership; `LEAD_PLAY_KEYS` (`a`–`l`) remain tones 1–9 as `PlayLeadTone`. `LEAD_NUDGES` is the single nudge table (`z`/`x` octave, `q`/`w` level, `e`/`r` decay, `t`/`y` glide, left key down and right key up, one dial step per press as `NudgeLead` → `InteractionEffect::LeadNudge`), which the runtime key map, the footer, and the executor all read rather than restating the pairs; Space is `ToggleLeadPattern` (the lane Off/Play, without leaving the keys), `c` is `CaptureLeadPhrase` (keep what was just played as the lane), every press is an edge so autorepeat plays nothing, and Esc returns to browsing (releasing any held key). Holding follows the shared capability rule: where the terminal reports releases (`TerminalCapabilities::supports_holds`) a press is `PlayLeadTone { hold: true }` and the key-up is `ReleaseLeadTone`, so the note sustains under the finger; only the key that owns the sounding note releases it. A reduced terminal (no releases, autorepeat arriving as presses; plexi panes today) gets `hold: false` attack/decay taps and the LEAD footer says so, since the kernel cannot tell a repeat from a re-press there. Ctrl+S/Ctrl+Q/Ctrl+C are global for every owner except the palette (its own control chords) and numeric entry (swallows every chord), so no mode traps the user. The runtime and the view both read `LEAD_PLAY_KEYS` and `LEAD_NUDGES` rather than restating the rows. `InteractionModel::update` remains deterministic and returns ordered data-only effects; adapters execute those effects elsewhere.
- `fluid/runtime.rs` is the only production adapter allowed to expose Crossterm transport details. It requests event-type and all-key keyboard enhancement together, normalizes with explicit capabilities, exposes press-only fallback through `TerminalCapabilities`, and restores pushed flags, alternate screen, and raw mode in reverse order on every exit path. Its canonical input mapper returns `Action`, `Ignored`, or a typed `Deferred` reason. Performance bindings map raw identity and phase onto typed intents; selector Repeat is inert, selector Release carries its identity, full-capability Sequence exits only on the armed action's matching Release, and reduced-capability completion stays visibly kernel-owned so autorepeat cannot leak into Browse. Global Ctrl bindings test containment so Shift and other modifier bits do not disable them. Quit requires Ctrl+Q, with Ctrl+C retained as an alias; bare `q` is inert. Palette text accepts shifted printable characters while Ctrl P/N/B keep their navigation and bar-commit meanings. `Scheduler` owns tick and frame deadlines, bounds each input turn by read count and elapsed time, backpressures reliable events at its fixed queue capacity, and run-length encodes only adjacent identical Repeat events. A requested or due frame preempts queued input.
- `fluid/session.rs` is the only publication boundary for user-audible live state. `LiveSessionSnapshot` includes gesture envelope anchors alongside controls, automation, tonal state, mutes, and live Lead presses. All writers use `LiveSession::transact`; the audio callback reads one aggregate without a mutex. `LiveSession::audio_seconds` is an audio-owned atomic clock used to evaluate gesture anchors, never a second copy of their state. Lead key presses remain live-only; active gesture envelopes persist compactly through `song.rs`.
- `fluid/effect.rs` owns interaction effect execution and adapter-side consequences. It executes each transition's owned effect list in order, stops at the first failure, supports an injected clipboard for deterministic replay, and returns an explicit acknowledgement or failure. It updates aggregate controls/automation, auto mode, mute/unit state, MRU state, staged commits, clipboard, selection, and messages only at their defined effect boundary. Performance edits resolve their typed instrument/action through the registry, publish through the aggregate session transaction, and acknowledge the real page/control row without leaving the active performance owner. Nested LFO open/close effects acknowledge the resulting depth and exact parent row; the production coordinator applies that position to the interaction model after publication. Palette jump and immediate-commit effects carry their exact typed target/edit payload and receive explicit selection/publication acknowledgements. Save failures use the `Save failed: ...` notice prefix. New kernel effects must be handled or rejected explicitly.
- Browsing gestures pin their target at Press and preserve arrows/Tab. Press/Repeat/Release ownership uses the production mapper; release still works after a modifier change. Opening a keyboard-owning mode, Escape with held gestures, reported focus loss, and shutdown release the gesture overlay. Unsupported terminals show the key-release requirement and leave gesture keys inactive. Effects publish envelope anchors through the aggregate session without exiting auto. Releasing returns toward the current song, never an old controls snapshot. Bloom/Echo feed shared processors through sends so their tails survive release; fixed per-layer storage avoids allocation when a gesture starts.
- Deliberate edits go through `EffectExecutor::edit_session`, which exits auto before publication so the morph cannot overwrite them on its next tick. `InteractionEffect::LeadTone` also bypasses it — a played note is a gesture over the song, not an edit of it, so soloing never stops an auto morph — but it also lands in the executor's `LeadPhraseBuffer` (live-only, bounded, never in a song code) so `LeadCapture` can keep it afterwards: `lead_capture` turns the phrase into `lead.steps`/`step1..16` and sets `lead.pattern` to Play through `edit_session`, so keeping a phrase exits auto like any edit, and with nothing played it is an explicit `NoChange` with a notice. `LeadOctave` and `LeadPattern` are ordinary `lead.octave`/`lead.pattern` edits and do exit auto. Mute bypasses this helper and toggles the aggregate session overlay, so auto and automation keep driving the authored Level value.
- Mute has exactly one implementation, `EffectExecutor::toggle_mute`. The engine applies a 30 ms click-free gate after each layer's full module chain and after the Master bus, so Level automation cannot raise a muted output. Mute state lives in `LiveSessionSnapshot`, renders from that same generation, and persists as a compact song-code record.
- A run of homogeneous rows (the Pads' chord slots, the Lead's steps) never sits inline on its page. It lives behind a page-local drill (`ChordDrill`, `LeadDrill`): Enter on the row that sizes it (`pad.progression` on Custom, `lead.steps`) opens the run, Esc returns to that row, the registry owns the projection (`chords_tab_controls`, `lead_tab_controls`) and its inverse (`chords_drill_for_index`, `lead_drill_for_index`) so a palette jump lands inside, and `tab_controls` stays the root page. A new voice with a lane follows this shape rather than adding rows to its page.
- `fluid/view.rs` is the only boundary that combines interaction state, a coherent `LiveSessionSnapshot`, telemetry, and presentation-only state. It derives the active `KeyboardOwner`, typed `ModeSurface`/`HelpSurface`, navigation rows, and render data before drawing. Automation resolves exactly once into an `AutomationSurface`; mode/session kind mismatches and nested LFO keys that do not identify an eligible field on the active address become an explicit unavailable surface, and render/help must not infer another editor from session state. Numeric and palette render state lives only in its owning mode surface. A mode opened from inside another editor projects that editor alongside its own state rather than replacing it — `ModeSurface::Numeric` carries the `AutomationSurface` it resumes to, so typing into a drilled-down field renders the buffer on that field instead of collapsing the editor to its parent row. Performance surfaces project the closed instrument choices, held/full/fallback completion state, and registry-backed live level/length/density values for every active instrument. `ui::render` accepts only this immutable projection and a Ratatui frame; it must not poll input, mutate state, execute effects, or recreate footer precedence. The control list scrolls through `row_scroll`: a selected row on the first screen never scrolls, and past the fold it is shown with two lines beneath it, so a page that outgrows a short frame stays reachable without the top of the page jumping. The minimum supported frame is 46x10 and every top-level owner and nested owner depth requires a full-buffer snapshot at that size.
- Add a replay regression in `fluid/replay.rs` for every interaction bug: use only sanitized key identity/phase/modifiers/repeat count, relative clock advance, resize, focus, shutdown, explicit tick/idle, and redacted event records; keep secrets, semantic intents, and pasted or mouse text out of persisted fixtures; include the smallest event sequence that reproduces the failure. Media and modifier keys use closed typed identities rather than debug strings. Performance hold and fallback regressions must enter through raw recorded key phases and traverse the production mapper; no semantic test adapter may bypass it. Every normalized `PhysicalKey` identity and all six modifier bits must round-trip without loss; fixtures with modifier bits outside that domain are invalid. Tick processing and idle turn delimiters remain explicit replay boundaries. Each trace must traverse the real normalizer, scheduler, kernel, effect executor, view projection, and Ratatui `TestBackend`. Property failures use structured replay outcomes, retain partial action/frame history, and preserve the same exact `PropertyViolation` value (derived equality, no separate key type) during delta reduction: edge failures carry the full action/before/after/effects record, while nondeterminism carries the first divergent history, frame, or result field and both values. Diagnostics report the concrete divergent action/before/after/effects record. Build new fixtures with the test-only `runtime::SanitizedTraceRecorder`, then delta-reduce them before committing.
- Touching UI edits use `EffectExecutor::edit_session`: stop auto first, recompute controls and automation from one aggregate snapshot inside the CAS retry, publish once, then acknowledge MRU state. Standalone auto-off publishes one no-op session fence; auto-morph writes verify the exact morph `Arc` they computed from so an off/on cycle cannot admit a stale generation.
- The `ControlSpec` tables in `fluid/registry.rs` are the single source of truth for every control row and stable song snapshot ID. Adding a control = adding one table entry with a durable ID; never reintroduce per-function match arms for control rows. The `control_registry_specs_are_internally_consistent` test enforces table sanity.
- `song.rs`'s snapshot codec is fully generic over `all_specs()`: any state expressible as `ControlSpec` rows persists and round-trips automatically, and old codes missing a newer id simply decode that field to its default — no `song.rs` change needed when adding controls this way. Only reach for a new versioned payload/section in `song.rs` for state that cannot be expressed as a flat control value (e.g. automation routes). Adding a control does require appending its id to `song_ids.rs`'s `SONG_ID_TABLE`; `song_ids_cover_every_registry_control` fails the build until you do.
- The song-code container version is the only version axis; record payloads carry no version byte of their own. Version 2 is the only version that exists — all v1 plumbing is deleted and a v1 code is rejected outright with `UnsupportedVersion`, never silently decoded as empty state. Control ids are interned as `SONG_ID_TABLE` indexes and continuous values stored as a u16 taper position, so those round-trip within one u16 step rather than exactly; discrete rows and the `PowerOfTwo`/`BeatGrid` step ladders stay exact, as do all beat-valued automation fields, `LfoRoute::seed`, and every tonal-sequence field. Unknown *record types* stay skippable on purpose — that is how a code from a newer build still loads everything else — while an unknown *container version* is a hard error.
- Round-trip fidelity is asserted semantically by `assert_song_states_agree`, never by byte equality: the reader clamps and substitutes defaults on the way in. Compare controls through `all_specs()`/`spec.quantized_value`, not by walking `FluidControls`; compare automation through the accessors, never `AutomationState`'s derived `PartialEq`, which also covers `open`/`open_field`/`LfoRoute::pickup` — live editor state that deliberately does not persist.
- A control whose default sits off its own step grid (`clap.filter` at 0.75, `bass.drive` at 0.15, both on a 0.02 grid) cannot store its own quantized value: the writer prunes it as equal to the default and it reloads one grid step away. Pre-existing, not introduced by the codec; fix the defaults, not the prune.
- Control rows carry `ControlKind`; use it as the source of truth for gain/continuous/timing/discrete semantics.
- The `/` palette derives everything from the registry: `palette_entries` builds one entry per unique control id at its owning tab (`tab_owning_control`), while its empty query reserves its first ten rows for the ten UI-local MRU controls globally across every page, then lists unused current-page controls and registry order. A query that is still just a layer's name or id namespace (`bas`, `bass`, `pad`, `pads`) pins that layer's `Tab::level_id` control above the fuzzy score and the MRU alike, so typing a layer name always reaches its level; the boost releases itself once the query outgrows the namespace. It must outrank score, not tie-break it — score alone puts `pad.stereo_width` above `pad.level` for `pads`. Slider interaction includes value edits, palette jumps, mute, drill, unit, and automation actions; cursor-only Up/Down navigation does not count. The MRU has no counters and is not song state. Staged edits apply through each spec's own `apply_value` entry semantics in a single clone-modify-store pass, and jumps restore Pads/Master drill state via `chords_drill_for_index`/`master_flat_index` inverses (roundtrip test-enforced). Palette value entry is always in the control's native unit — it does not consult `FlippedUnits`. Bare-digit numeric entry on the main view is untouched; the palette owns the keyboard only while open, and `/` must stay additive per the North Star 15-second floor.
- **`palette_entries` must stay a pure function of the registry.** The interaction kernel and the renderer each build the entry list independently and confirm resolves by index, so anything that made entries, haystacks, or match ordering depend on live controls could desync the two and jump to the wrong control. Live state is read only where it cannot affect indices: the rendered value column (`PaletteEntry::value`), and the adapter. Module-slot rows are excluded as controls; a module is reached through its own `PaletteEntry::Module` row, one per (layer with a chain, catalog entry). Confirming one emits `InteractionEffect::PaletteModule`, and `EffectExecutor::place_module` decides add-vs-jump: a layer already holding that module gets a jump, never a second copy; a free slot gets the module at amount 0 (added modules are always inert, whatever a pre-loaded slot's factory amount is) and the cursor lands on it; a full chain returns a message naming the count.
- Master tempo accepts 30–200 BPM; keep the registry and `MASTER_BPM_MIN` aligned when retuning the slow-morph floor.
- A song save is a session snapshot: persist every user-audible generator state that does not regenerate on its own — sequence positions, evolution counters, seeds. Controls alone are insufficient when a feature advances internal state that would otherwise restart. The bound is size: a song code is meant to be pasted into a chat message, so raw audio buffers never go in one. FX tails (delay lines, reverb combs, compressor envelopes) rebuild within a second of playback and are excluded — `song_code_stays_short_with_every_stateful_module_filled` fails the build if a code exceeds 2,000 characters with every stateful module family loaded. State that does qualify goes on a lock-free audio-to-UI snapshot path as its own song-code record; unknown records decode as skipped, so an older code simply loads without it.
- `fluid/widget.rs` owns the shared slider vocabulary. Every bar in the app is a `Dial` (value + `DialScale` + display string), and `DialScale` (`Tapered`/`BeatGrid`/`Rungs`) is the only place a bar ratio is derived. A surface must never hand-roll ratio arithmetic: build the `Dial` and let its scale decide. `DialScale::step_in_position` gives a tapered field even presses end to end and returns `None` for scales with no inverse (`BeatGrid`, `Rungs`), which own their own stepping. `LfoField`, `EnvField`, `MacroField`, `StepTarget`, and registry `ControlItem`s all expose a scale plus a `field_value`, so display and stepping cannot disagree.
- Module slots (`fluid/module.rs`, design in `docs/proposals/2026-07-30-module-slot-addressing.md`): all seven voice layers plus Master carry `MODULE_SLOTS` anonymous slots addressed by stable storage IDs. **A module's identity is a stored value, never part of a control id** — ids are permanent `SONG_ID_TABLE` entries, so putting the catalog in the id space would cost a fresh block per module forever. `MODULE_CATALOG` is append-only because saved slots store its indexes. Domain (pre/post) comes from the loaded module, not the slot index. Only modules with complete behavior for a layer may appear in its add palette; retained catalog indexes without DSP stay unavailable. Each loaded module collapses to one row, named by `ModuleKind::collapsed_field` — Amount for every family except Filter, whose most useful single knob is Cutoff; Enter opens reusable `Navigation::Module` detail, `/` exposes the same dotted sub-context, and Esc restores the parent. Pads' effects remain siblings of musical controls and never enter chord drills. Delay owns Amount/Left Time/Right Time/Feedback/Vintage; `T` independently converts each time between Sync and Free, and a time change crossfades to the new tap position over a fixed window rather than sliding the read head, so tempo-synced retargeting never pitch-bends. Reverb owns Amount/Size/Damping. Compression owns Amount/Threshold/Ratio/Release/Makeup. Filter owns Amount/Cutoff/Resonance/Type; its Amount (wet/dry mix) is detail-only and always starts fully wet — `preset_slot` overrides any caller-supplied amount for "filter" — because a palette-added filter starts audible via a maxed-out (transparent) Cutoff instead of via the usual muted Amount. Drive and Swing are amount-only. One slot-addressed post-DSP bank executes Drive/Reverb/Delay/Compression/Filter in chain order on every voice layer and Master. Slot FX state is runtime-only: a loaded song starts every delay line and reverb silent and rebuilds the tail from playback. A processor whose slot stops asking for its family — emptied, or swapped for another module — is never dropped outright; it moves to `ModuleFxBank::retiring`, keeps running on the live input while its contribution crossfades back to dry over `LEVEL_RAMP_MS`, and only then is released. It fades using `last_loaded`, the slot's fields from the last frame it was loaded, since the live slot has already been cleared by then and its zeroed Amount would silence the tail being faded. Dropping instead of retiring cuts a live tail to zero in one sample and leaves the processor's buffers frozen rather than decayed, so refilling the slot replays the previous take (`removing_a_module_fades_its_tail_instead_of_cutting_it`, `re_adding_a_module_does_not_resurrect_the_previous_tail`, `replacing_a_module_fades_the_outgoing_one`). Drive and Swing hold no tail and so have nothing to retire. Serializing those buffers put one Delay slot past a million characters in a song code.
- Retired effect controls (`pad.reverb_mix`, `tonal.reverb_mix`, `arp.reverb_mix`, `clap.room`, `perc.swing`, `tonal.swing`, `arp.swing`, `bass.drive`, `kick.drive`, `kick.filter`, and all bespoke Master Drive/Compression ids) keep their append-only `SONG_ID_TABLE` slots but are gone from the registry, and nothing translates them. A code naming any table id that no live spec claims is refused with `SongCodeError::RetiredControl`; an index *past* the table's end is a control from a newer build and stays skipped, which is the only reason the two cases are distinguished (`reject_retired_control`, `song.rs`). Retiring a control therefore invalidates every code that set it — re-author `AUTO_STATES` by decoding on the last build that understood the id and re-encoding on the new one, carrying its value, the chain slot it belonged in, and any LFO routed at it (`auto.rs`'s `AUTO_STATES` doc comment works the conversion). The default template preloads Pads Reverb at 80%, Tonal Reverb at 10%, Arp Reverb at 0%, Bass/Kick/Master Drive, and Master Compression; Clap starts with an empty chain and Swing remains optional. `resolve_module_chain` derives only pre-trigger Swing fields. Post effects read slot state directly.
- Every slider must use its full visual throw. Continuous controls map value to bar position through `Taper` (`Linear`, `Log2`, or `Exp(n)`). Irregular `Step::BeatGrid`, `Step::PowerOfTwo`, and LFO-rate ladders divide the bar by reachable arrow rungs instead of raw numeric magnitude; exact values between rungs interpolate within that segment. The base value and every modulation marker must use the same mapping. `ControlItem` carries real value/min/max plus step and taper; never bake position transforms into stored values. A continuous tapered dial with a plain `Step::Linear` steps in position space and stores full precision.
- Every time control reads through the shared `secs(seconds)` formatter (whole ms below 1 s, 2-dp seconds above) — ms-stored controls pass `ms/1000.0` — and uses `Taper::Exp(TIME_TAPER)` so envelope/decay dials read and step consistently regardless of whether the field is stored in seconds or milliseconds. Retune all time-dial feel from the single `TIME_TAPER` constant. Beat-grid/power-of-two musical controls keep their musical stepping and are left off the exp taper.
- Every continuous automation field, including LFO, envelope, and inline step targets, derives its range, arrow stepping, numeric entry, reset target, and dial scale from a `FieldSpec` table row, never from a per-field match arm. `Stepping` names how one press moves it (linear, snapped to the field's grid, along an explicit rung ladder, along the musical beat grid, or an equal fraction of a tapered throw) and `Entry` is the registry's own numeric-entry vocabulary. Only the discrete fields (LFO shape, envelope trigger) stay outside the tables: they are enums cycled by index. Display strings stay on `LfoRoute`/`EnvelopeRoute`, not in the spec. Arrow navigation clamps inside the open submenu; Escape closes it.
- LFO `rate` spans 0.125–64 beats. Arrow steps use quarter-beat rungs through 4, then 8/12/16/32/64; each rung gets an equal share of the bar and numeric entry remains exact within the range. A live rate edit never rewrites offset: the old and new globally anchored waveforms hand off at their next crossing (`LfoPickup`), after which the new rate follows the shared transport grid. Equal rates with zero offset share phase across every control.
- `FluidEngine` applies a two-second startup mix fade; keep `startup_fade_reaches_full_gain_in_two_seconds` and the seeded golden-render checksum aligned with intentional changes to that window.
- Auto morph treats drum levels musically: when a target has no drums, Perc/Kick/Clap cut together on the transition downbeat instead of morphing out; Kick also starts on that downbeat, while Perc and Clap may fade in independently.
- Per-slider `f` (LFO) and `e` (envelope) open or cycle that lane family. `Shift+F`/`Shift+E` adds and opens a lane, `x` removes only the open lane, and Escape closes the editor. `r` while browsing is `RandomizeSelected`: it sets the selected control to a random point on its own dial through `ControlSpec::apply_ratio` (position space for tapered rows, an equal share per rung for the `BeatGrid`/`PowerOfTwo` ladders, rounded onto the table for discrete rows), so a time dial is as likely to land short as long; inside an automation editor the same key is `ReseedAutomation` for the open random lane. `Shift+R` is `RandomizeScope`: in browsing it uses the coordinator's pre-action visible-row projection, so it randomizes exactly the current page or drill (including a module detail) in one session transaction; in an automation editor it randomizes every field of the open route, including the LFO Steps configuration and random seed. The roll comes from `EffectExecutor`'s one `StdRng` — entropy-seeded in production, `REPLAY_RNG_SEED` in replay — so a replayed trace rolls the same values twice. Do not add voice-specific LFO/envelope controls to core slider tabs.
- New lanes start at zero amount. Leaving a lane neutral and closing its editor prunes it. Each family is capped at four lanes per control; hitting the cap returns a visible interaction failure.
- `modulated_control_value_full` sums every active lane in dial-position space, then clamps and snaps once. UI markers show this semantic target. `AutomationPlan` applies the same sum through one 3 ms de-clicker after composition, preserving continuity across envelope retriggers, route edits, and lane removal without storing that transient in song codes.
- Automation colour identifies the modulator family: LFO lanes are pink and envelope lanes are green. Envelope polarity changes direction, never colour.
- Grid-timing controls carry `LfoSnap` in their `ControlSpec` (intervals snap modulation to power-of-two subdivisions, offsets to their step grid). `GridTrigger` may pull a scheduled hit earlier when a reshaped grid lands sooner (so a denser grid never starves), but never within half an interval of the hit already emitted (`earliest_hit`, floored on `last_hit_beat`); a reshape moves any slot by at most half an interval, so this guard stops a live swing/offset/rate change from re-firing the slot that just sounded. Grids that move later latch at the next fire.
- Chord length (`pad.chord_bars`) stores bars for the pad/bass engines but displays and accepts numeric entry in beats; typed values convert to bars and snap to the existing power-of-two grid.
- Live-read gain controls are ramped by registry-derived `GainSmoothers` in `FluidEngine`; every unique `ControlKind::Gain` spec must get a smoother automatically.
- A layer's Level scales its engine's summed output every sample; it is never captured into a voice at trigger time. Capturing it means the fader cannot reach notes already sounding, which on Tonal/Arp (up to 6 s of decay) left a layer audible long after it was pulled to zero while Mute cut instantly (`tonal_level_ducks_notes_that_are_already_sounding`). Level scales the *output* of any tone-shaping filter the engine owns, never its input: `TonalLowCut` is a high pass, and feeding it an abruptly silenced signal discharges its state as a decaying thump. Tonal and Arp still skip creating a voice at a Level of exactly 0, which keeps a silent layer from accumulating inaudible voices without affecting the fader's reach.
- Every voice reads `master.tune` at the moment it builds a note, so a tune change lands on the next trigger. `PadEngine::new` is the one constructor that must be handed the live tune rather than assuming neutral: it voices the opening chord, which then holds for a whole `chord_bars` with its release bleeding into the next, so a song code carrying a non-zero tune would otherwise open at concert pitch against a transposed Bass/Tonal/Arp (`pad_voices_its_opening_chord_at_the_master_tune`).
- TUI automation edits must go through `EffectExecutor`, which publishes through the aggregate live-session transaction on every mutation.
- While auto morph runs, each immutable UI frame reads automation from the current aggregate session snapshot, so LFO markers and song saves match what the engine hears; the first touching edit exits auto from that current snapshot.
- Pitched voices (Pad, Bass, Tonal, Arp, Lead) route note numbers through `voice::note_hz` (`midi_to_hz` under master tune); unpitched voices (Perc, Kick, Clap) do not.
- Tonal separates trigger density from phrase shape: `tonal.rate_beats` controls note trigger spacing; `tonal.step_interval_beats` is the stable cycle-length ID for phrase wrapping and evolution boundaries. `tonal.offset_beats` moves the cycle-length window forward through the phrase without phase-shifting the trigger grid, so Cycle 2 + Offset 1 plays phrase beats 2–3.
- Tonal synth selection lives in `tonal.synth_type`; it changes the voice created for new tonal notes while preserving the shared phrase/randomness/timing/master-tune/reverb path. Exploration variants may differ in harmonic tables, spectral tilt, and pitch-scaled harmonic decay; keep piano-family profiles warm and non-metallic unless the user explicitly asks for FM-like brightness.
- `tonal.octave` (`TonalControls::octave`, default 0) shifts the note selected from `TONAL_SCALE_MIDI`/the evolved phrase by whole octaves (`± round(octave) * 12` semitones) right before `note_hz` in `TonalEngine::next` (`voice/tonal.rs`) — it never touches `TONAL_SCALE_MIDI` itself, so the minor-pentatonic interval set is preserved at every octave. Arp reuses Tonal's piano synthesis but derives its own pitches from pad chords in `arp.rs`, so `tonal.octave` affects only the Tonal voice.
- Every tonal/arp note is a single attack+decay shape via the shared `attack_decay_gain` helper (Sine and all piano-family profiles): the gain ramps in over `attack` seconds, then falls from the peak to silence over `decay` seconds, so a note's whole sounding life is `attack + decay` and the envelope reaches exactly 0 there (no click at voice death). The step grid (`tonal.rate_beats`/`arp.rate_beats`) only spaces triggers — it never bounds a note's length — so notes overlap freely when `decay` outlasts the step (both engines accumulate polyphonic voices). `decay` is floored at `TONAL_DECAY_MIN` (10 ms) so the UI can't request a hard, clicking cut; only the decay curve's shape exponent (`PianoProfile::body_power`, or the sine voice's fixed sqrt taper) stays profile-owned character. Clap and perc already embody this decay-to-zero model (their inline `decay_ms` fades) and are the reference the tonal/arp envelope was unified to. Pad and Bass are deliberately separate (long sustained chords; mono voice-stealing) and keep their own envelopes.
- Tonal owns a slight fixed low cut before engine mixing so its low notes sit above sub/bass energy without requiring a user-facing control.
- Pad, Tonal, Clap, and Arp emit dry voice output. Any Reverb is owned exclusively by that layer's shared module chain; there is no voice-local or ambient-send reverb path.
- Arp follows the Pad's current chord without reaching into `PadEngine` directly: it holds a `ProgressionFollower` (pad.rs; Bass holds the same one) whose own trigger stays synced to the `pad.chord_bars` grid, reads chord tones via `pad_chord_midi`, and cycles them (Up/Down/Up-Down/Random, 1–3 octave span) on its own `arp.rate_beats` grid, phase-shifted by `arp.offset_beats` (default 0, matching the Level/Interval/Offset idiom shared with Perc/Bass/Kick/Tonal/Clap) — only `note_trigger` reads the offset; `chord_trigger` stays tied to `pad.chord_bars` unshifted. Its synth character lives in `arp.type` (`ArpControls::voice_type`), reusing Tonal's `TonalVoice`/`TONAL_SYNTH_TYPES`/`piano_profile` path (same Sine + 9 piano-profile set and labels as `tonal.synth_type`); default `6.0` ("Pluck") matches the arp's former hardcoded profile byte-for-byte. `arp.gain` defaults to 0 (silent) so adding the voice never changes an existing song or default startup. When the chord or octave span changes mid-cycle, the cycle position is clamped into the new tone list rather than reset, avoiding a click.
- Lead (`voice/lead.rs`, `LeadControls`, `Tab::Lead` between Arp and Master) is the one hands-on melodic voice: a mono additive voice whose character is `lead.type` (`LEAD_TYPES`: Saw/Square/Soft/Pure/Reed, each a `LeadRecipe` of eight signed harmonic gains plus a trim that `lead_types_render_at_a_matched_level` holds within 10%; read per sample, so a Type edit reshapes the sounding note), with a one-pole pitch glide (`lead.glide`, `glide_coefficient`) and an attack/decay life per note, retriggered in place so a new note slides from the last without a click. It plays a step lane: `lead.steps` (1–16 live steps) and `lead.step1`…`step16`, each a rest or one of `LEAD_TONE_COUNT` (9) tones, under one transport row, `lead.pattern` (`LEAD_PATTERNS`: Off/Play, default Play). Off is the solo position — the lane is silent, played keys still sound, and the trigger keeps ticking so Play resumes on the clock's step. It is a discrete control like any other, so a Steps LFO can gate the pattern in bars. There is no armed record state: a phrase enters the lane by capture after it was played (`lead_capture`: the presses since the last bar of silence, within the last 16 steps, each snapped to its nearest step, on the shortest of 4/8/16 steps that holds them, placed at `absolute step mod length` so the loop plays back where it was played), the way Ableton's Capture MIDI keeps a take nobody armed. A tone is an index into the Lead's reach (`LeadReach`), which `lead.follow` picks: Chord is the Pad's current four voiced tones through the shared `ProgressionFollower`/`pad_chord_tones` path, so keys move as chords move; Scale is `progression_scale`, every pitch class the progression's chords touch laid out from its first chord's lowest note, derived rather than declared so a custom progression and each built-in table yield their own and a chord change never moves a key. Tones past the reach wrap an octave (a four-note reach reads `1 2 3 4 1' 2' 3' 4' 1''`, `lead_tone_label`), then `lead.octave` lifts the note. The steps live in the page-local pattern drill (`LeadDrill::Pattern`, `lead_tab_controls`), never on the root page; only the live steps show there, and `DEFAULT_LEAD_STEPS` rests past the default length so lengthening the lane adds silence. The active lane row carries the same amber `♪` follower marker as the chord progression; an LFO in its `Steps` shape marks its active step too. Both markers derive from their transport clocks, so neither adds persisted state. A retrigger on a sounding voice resumes the attack from the current level (`Adsr::start_at`) with the phase untouched, so fast playing is click-free; the default glide (30 ms) is a slide between held notes, not lag under a line. The step a hit plays is derived from the transport (`lead_step_at`, like the LFO staircase), so there is no lane position to persist. Its "slightly distorted" character is a factory Drive slot at 0.1, not a bespoke control. Trims are set so full Level sits below the Bass (`lead_at_full_level_sits_under_the_bass`): a bright driven lead reads louder than it measures, and the fader's useful range has to be the whole bar. `lead.level` defaults to 0 and a silent Lead triggers nothing (a play-mode press on a silent Lead is dropped, not held), so adding the voice changed no existing song or the golden render. Play mode (`LeadEngine::observe`/`with_play_state`) sounds a pressed tone on the next sample, sliding from whatever the lane is playing; `LeadPlayState::held` sustains it (`LeadShape { hold }` sets the ADSR sustain to 1 and the release to `decay`), the flag dropping releases it, and the lane yields while a key is held so the player owns the voice; the footer names a 0 level so a silent keyboard is not mistaken for a dead one.
- The song-code mute record is indexed by `Tab::mute_bit`, a per-tab bit assigned once in the order tabs were added (Master is 7, Lead is 8), never by discriminant, and is `MUTE_BYTES` wide. Adding a tab mid-strip therefore cannot move a saved mute onto another tab; a shorter payload from an older build simply carries no bit for the newer tabs.
- A ninth `pad.progression` value (`voice::CUSTOM_PROGRESSION_INDEX`) selects a user-built progression instead of the 8 built-in tables: 8 chord slots (`PadControls::chord_slots`, `ControlSpec` rows `pad.chordN_degree/_accidental/_quality/_extension/_inversion`, visible directly on the Pads tab) each define a tonic-relative root degree, semitone accidental, quality (modal-interchange third override: -1 forces minor, +1 forces major, 0 keeps the diatonic third; the row's display resolves the inherit position to `scale (maj)`/`scale (min)` via `pad_chord_slot_is_minor`), extension (triad/6th/7th/9th-flavor top voice), and inversion; `pad.chord_count` (1–8) sets the loop length of **every** progression, built-in or custom — a built-in's 8-step table is truncated to its first `chord_count` chords, so the control can never display a length the engines don't play. `voice::pad_chord_tones`/`voice::pad_chord_count` are the single chord-source path Pad, Bass, and Arp all resolve "what chord is at this step" through — a custom progression drives all three identically without any voice reaching into another's state. Built-in chord/bass tables are otherwise untouched; the custom slot data is inert unless progression selects it.
- Bass character lives in `bass.type` (`BassControls::voice_type`): index 0 (`Sub`) is the legacy voice and the default — its DSP path must stay byte-identical to a pre-`bass.type` render. Index 1 (`Saw`) is a brighter additive-harmonic character, index 2 (`Pluck`) a shorter character with an attack transient; all three share the same trigger/rhythm, pitch (`midi_to_hz` + master tune), and drive/panner tail, and are gain-authored to a comparable perceived level.
- Bass is monophonic (`BassEngine::voice: Option<BassVoice>` in `bass.rs`): each rhythm-grid hit hard-cuts whatever is currently sounding and starts the new note immediately, regardless of `decay_time`. The replaced voice isn't dropped instantly (that clicks) — it rings down over a fixed short (`BASS_MONO_FADE_SECONDS`, ~3ms) `fading_voice` slot, independent of the voice's own envelope. This applies identically to all three `bass.type` characters. No user-facing control; this is not a voice pool.
- Post-synthesis filtering runs through the shared `ModuleFxBank` at a stable layer/slot identity. The Filter module owns its left/right state and accepts only its persisted amount, cutoff, resonance, and type fields. A zero Amount is an exact dry bypass. Shared Filters replace the retired `perc.filter`, `bass.cutoff`, and `kick.filter` controls and are preloaded into slot 1 on all three layers. Bass and Kick Drive occupy slot 2. Kick keeps fixed voice-local filters as part of each character; the shared module owns the user-facing sweep.
- Pad character lives in `pad.type` (`PadControls::voice_type`): one `PadTone` runs the shared `PadOscStack` into the shared soft-clipper, and a character is exactly one `PadStage` between them plus one output trim after. Index 0 (`Warm`) is the legacy tone and default; `PadStage::None` with a 1.0 trim keeps its original render byte-identical (`pad_type_zero_keeps_the_legacy_warm_signal_path`). `Dark` adds a lowpass, `Glass` adds a two-octave shimmer, `Choir` adds a moving upper partial, `Hollow` blends a sub-octave tone, and `Tape` adds a lowpass with slow level drift. A Type edit swaps the stage in place on every already-sounding tone, crossfading old stage output to new over `PAD_TYPE_CROSSFADE_SECONDS`; oscillators, phases, and amplitude envelopes keep running untouched, and sustaining tails convert rather than being cut. A Type edit must never voice a new layer — doing so re-attacks the whole chord from silence with its oscillators phase-aligned, which is the harsh retrigger `pad_engine_type_change_revoices_the_current_chord_immediately` now guards against. Only chord count and progression selection wait for the active loop boundary. All six share chord selection, trigger timing, attack/release, pans, and the dry-output/engine-owned-reverb contract, and are gain-authored to comparable levels.
- Kick character lives in `kick.type` (`KickControls::voice_type`, `KickVoice` enum over `LowpassKickVoice`/`WoodKickVoice`): index 0 (`Sub`) is the legacy voice and the default — its DSP path must stay byte-identical to a pre-`kick.type` render. Index 1 (`Warm`) is a round FM body (shallow depth at a hollow 1.5x modulator ratio); index 2 (`Wood`) sends the post-drive signal through a heavily-damped hand-rolled bandpass (Chamberlin SVF, centered in 110-400Hz at the fixed character position) and blends it back against the dry signal so low-end weight survives; index 3 (`Felt`) swaps the sine carrier for a naive triangle and biases its character filter darker than Sub. All four share the same trigger/scheduling path in `KickEngine::next`, the click/drive/amp-envelope/soft-attack/pan machinery (`KickVoiceCore`), and the pitch-glide/FM carrier body (`KickFmBody`, which returns a carrier phase each variant shapes itself); Sub/Warm/Felt are one `LowpassKickVoice` driven by a `LowpassKickRecipe` const (`KICK_SUB`/`KICK_WARM`/`KICK_FELT`: attack, click scale, pitch drop, FM ratio/depth, carrier wave, `KickLowPass` bias, output trim; Sub's trim is exactly 1.0). A new lowpass type is a recipe const and a dispatch arm — never its own voice struct; only a different signal path (Wood's bandpass) earns one. `KickFmBody` owns only the pitch glide; the modulator/carrier math is one `synth::fm::FmStack` pair, so a kick type is a recipe (`mod_ratio`, index, index decay, `FmWave`) rather than bespoke oscillator code.
- All four kick types render at a matched level behind their fixed character filters, enforced by `kick_types_render_at_a_matched_level`, with `no_kick_type_exceeds_the_headroom_budget` bounding peak. Output trims are measured against Sub. Wood keeps the calibrated gain from the retired filter control's 0.7 position.
- Kick types 1-3 are authored for ambient use, not drum-machine impact: each passes a nonzero attack ms and a sub-1.0 click scale to `KickVoiceCore::new`, which linearly fades the output sample in over the attack (never touching `amp`, so the decay envelope and `is_done` are unaffected) and scales `kick.click`. Sub passes `0.0`/`1.0`, both exactly inert. Do not retune these types toward a harder transient; optional aggression belongs to the shared Drive module.
- Master Drive and Compression are ordinary preloaded shared modules. They use the same root/detail interaction, DSP primitives, registry semantics, automation, and song persistence as copies on voice layers; no bespoke Master compression drill or release row exists.
- Voice RNGs must stay reseedable via `FluidEngine::reseed` so `nooise render --seed` stays byte-reproducible. `a_seeded_render_repeats_itself_exactly` pins that in every profile. The separate `GOLDEN_RENDER_CHECKSUM` tripwire for unintended DSP-math drift is `#[cfg(debug_assertions)]`: debug and release evaluate the same float expressions differently and diverge by an ULP partway through the render, so one constant cannot hold for both. Re-bless it from a debug run.
- Passive update checks must never block the TUI or audio callback; keep crates.io/network work off the main loop and show no message on failure.
- `nooise update` checks crates.io before invoking Cargo; do not force reinstall when the installed version is already current.
- `nooise auto` morphs by owning and rewriting both the live `FluidControls` and `AutomationState`, using the same clone-modify-store path as the UI. Controls are driven by registry `ControlKind`: each leg holds steady for `HOLD_FRACTION` (2/3) of its length, then transitions in the final third. `Gain`/`Continuous` glide across the transition window; `Timing`/`Discrete` snap through `spec.quantize`. The `STRUCTURAL_SNAP_IDS` group jumps atomically on the transition downbeat; other grid params stagger at `STAGGER_STEP_BARS` offsets after it. `AutomationState::morph` pairs lanes by family and ordinal. Each lane's amount glides while its curve and trigger fields snap together at the transition downbeat; lanes present on only one endpoint fade in or out. The AUTO footer reports `MorphState::position_at`: the song sounding on its own through the hold, and `playing → next` with a percentage only once the leg is actually crossing, so it never announces a transition that has not started; a live toggle labels its initial session snapshot `LIVE`. Any manual control or automation edit exits auto. A live toggle's first leg (endpoint 0 = the current live state) always runs `LIVE_FIRST_LEG_BARS` (16 bars), regardless of the loop's configured `bars` (`DEFAULT_AUTO_BARS` 64 normally) — pressing `a` mid-vibe must start audibly moving within the next 16 bars, not wait out a full slow-evolution leg; every leg after the first reverts to the loop's normal length (`MorphState::leg_bars`/`leg_at_indexed`). The CLI names one thing one way: a **song** is either a built-in number or an `n1_` code, and `nooise <SONG>` plays it — `nooise 9`, `nooise 9,10,11` (morphing through them and looping), or `nooise n1_...`. `nooise auto` is every built-in song; `--bars` is the only knob and applies to both. A song number outside the built-in range is an error, never a clamp. `auto` deliberately takes no positional so an old `nooise auto 4` fails loudly rather than silently meaning song four. Add a morph target with `just add-morph <code>`; the helper inserts at the `AUTO_STATES` marker, rejects duplicate codes, verifies decoding, and commits only `auto.rs`.
## Conventions
Crate-wide rules. A change that breaks one is a change to this section, not an exception.
- **Visibility**: `pub(crate)` on everything, fields included. Never bare `pub` — the crate is a binary and exports nothing, so bare `pub` only misleads about reach.
- **Errors**: typed enums with `Display` + `std::error::Error`, and `source()` where a cause exists. No `Result<_, String>` and no stringly channels: an error's identity is data, its prose is `Display`. A payload string is only for text a foreign backend produced (`ClipboardError`, `AudioStartError::Backend`); the variant still names which step failed. User-facing notices format with `{error}`, never `{error:?}`.
- **Infallible lookups**: `.expect()` only where a table test enforces the claim, and the doc comment names that test. Everything else routes through the same failure path as its neighbours.
- **Module docs**: every file opens with `//!` saying what it owns. Not `// ====` banners — those remain only as in-file section separators.
- **Test-only helpers**: gate with `#[cfg(test)]`. Never bare `pub(crate)` plus a dead-code allow; `#[allow(dead_code)]`/`expect(dead_code)` is reserved for enum variants in a closed vocabulary that cannot be conditionally compiled.
- **DSP process fns** take one `…Params` struct, never loose arguments. Details in `fx/AGENTS.md`.
- **Bounds**: `ControlSpec::contextual` is the single source of a control's live range, step, taper, and entry semantics. Nothing downstream re-clamps; pin a DSP's assumed range with a test instead of restating it.
- **Naming**: an index is `*_index` or `index`, never `idx`. Multi-field positions get a named struct (`ModuleScope`), not an unnamed tuple.
- **`use super::*;`** is the deliberate internal prelude of the `fluid` tree: `fluid/mod.rs` re-exports its submodules and each submodule glob-imports them back. This is the one sanctioned wildcard import.
## Verification
- `just test` (cargo test) covers engine logic and the control registry; tests live in `fluid/tests.rs`.
- `just check` runs clippy across all targets.
- `nooise render --seconds N --seed N --out X.wav` (or `just render`) renders audio headlessly for DSP verification — same seed must produce byte-identical output.
## Child DOX Index
- `fx/AGENTS.md` — shared DSP effects (LFO, panner, reverb)
- `synth/AGENTS.md` — shared synthesis primitives (envelope, oscillator, noise, multi-operator FM)
- `fluid/engine/AGENTS.md` — temporary audio gesture processing and resource bounds