instrument: snap live params onto a fresh voice, roll a live drag back on capture loss, serialize the seqlock's two writers
This commit is contained in:
@@ -160,23 +160,28 @@ Daniel's ruling, verbatim: *"hell no, I was going to bring that up for the other
|
||||
must live compute, latching the parameters at note on is not acceptable. long term these will be
|
||||
automatable parameters."* It rejects the precedent, not one instance of it.
|
||||
|
||||
- **Which controls are live is ONE decision, recorded in ONE place** — `isLiveDeckParam`
|
||||
(`ui/deck_groups`). Continuous playback controls go live: the six filter tone/modulation
|
||||
knobs, and every stage time and stage level on all three envelopes. Everything else reloads
|
||||
or drops to the voice-param rebuild. Two controls look continuous but are deliberately not
|
||||
live — the pitch key-track and the velocity→cutoff depth feed values a voice latches at
|
||||
note-on (the pitch ratio, the velocity-curve result), so making them live would retune or
|
||||
re-gain a note already struck. The three capture-anchored overrides (root, loop span, start
|
||||
frame) reload because they name positions in the decoded PCM.
|
||||
- **Which controls are live is ONE decision, recorded in ONE place** — `isLiveDeckParam` and
|
||||
`liveCommitFor` (`ui/deck_groups`), whose header is the home for why each excluded control is
|
||||
excluded. Continuous playback controls go live: the six filter tone/modulation knobs, and
|
||||
every stage time and stage level on all three envelopes. Everything else reloads or drops to
|
||||
the voice-param rebuild. **FIVE continuous controls are outside the live set** — pitch
|
||||
key-track, velocity→cutoff depth, and Trigger's %-length, fade-in and fade-out — so a
|
||||
Trigger-mode instance gets no live delivery on its amplitude controls at all; only the filter
|
||||
and pitch-envelope knobs move a sounding one-shot.
|
||||
- **Ownership sits ABOVE every snapshot.** `SampleData::live` is a NON-OWNING pointer to the one
|
||||
block the shell owns per instance; a block owned by a snapshot would leave the drain slot's
|
||||
still-sounding voices deaf to the knob under them. A drain voice tracking the knob is the
|
||||
DESIRED behaviour — it is the note the user is hearing.
|
||||
block the shell owns per instance. The member-ordering constraint that enforces it, and why,
|
||||
are recorded at `liveParams_` in `shell/instrument/reasampler_processor.h`. A drain voice
|
||||
tracking the knob is the DESIRED behaviour — it is the note the user is hearing.
|
||||
- **Null is the bare engine.** `live == nullptr` is byte-identical to the pre-live core, which
|
||||
is why `sampler_core`'s regression baselines needed no change.
|
||||
- **Observation is at block boundaries, never per frame.** `VoiceEngine` reads the seqlock once
|
||||
per `render()` and once per note-on; the per-sample path gained one predicted branch
|
||||
(`filterRamping_`) and no indirection.
|
||||
per `render()` and once per note-on; the per-sample path gained three predicted branches (the
|
||||
voice's filter-ramp check and each envelope smoother's active check), all false at rest, and
|
||||
no indirection.
|
||||
- **A fresh note SNAPS, a sounding one holds φ.** They are different entry points on purpose
|
||||
(`snapLive` vs `applyLive`): a voice that has rendered nothing has no phase to hold, and the
|
||||
φ rule reads its stage-0 position under a stale zero-length stage as a completed stage. One
|
||||
function serving both silently discarded every newly-dialled attack.
|
||||
- **The mid-stage rule is HOLD NORMALIZED STAGE POSITION** (Daniel's pick among six candidates):
|
||||
φ = elapsed/duration is held across a stage-time change, so the level is continuous by
|
||||
construction and the remainder takes its share of the new duration. Stated over normalized
|
||||
@@ -232,7 +237,7 @@ slider couldn't. Two pure modules split the forward (draw) and inverse (edit) ma
|
||||
|
||||
- The engine is the `sampler_core` CMake target over FOUR headers and TWO TUs, split on its own responsibility seam — cold note routing vs the hot per-sample render:
|
||||
- `play_params.h` — the value layer: `PlayParams`/`AdsrParams`/`TriggerParams`/`PitchEnvParams`/`FilterParams`, the per-instance mode enums (`ChannelMode`/`VoiceMode`/`MonoTrigger`), and `SampleData` (the ONE loaded capture: decoded PCM + root + loop + start + keyTrack + velocity curve + play params). Shared by the engine, the codec, and the editor, so a UI/codec TU reading a param struct doesn't recompile when a `Voice` member changes. `FilterParams` stores the filter module's own `FilterSettings` by value rather than a parallel copy of its normalized positions.
|
||||
- `envelopes.h` — the three per-frame evaluators (`AdsrEnvelope` AHDSR, `TriggerEnvelope` fade shape, `PitchEnvelope` AD offset), CONCRETE and fully header-inline. Never give them a common base or a virtual `tick()`: they are called per-voice-per-sample. The filter envelope is a SECOND `AdsrEnvelope` instance on the voice, not a fourth class. `AdsrEnvelope`/`PitchEnvelope` also own `applyLive` (the φ-holding mid-stage rule) and `StepSmoother`, the bounded offset that absorbs the two level steps φ cannot cover.
|
||||
- `envelopes.h` — the three per-frame evaluators (`AdsrEnvelope` AHDSR, `TriggerEnvelope` fade shape, `PitchEnvelope` AD offset), CONCRETE and fully header-inline. Never give them a common base or a virtual `tick()`: they are called per-voice-per-sample. The filter envelope is a SECOND `AdsrEnvelope` instance on the voice, not a fourth class. `AdsrEnvelope`/`PitchEnvelope` also own `applyLive` (the φ-holding mid-stage rule), its fresh-note peer `snapLive`, and `StepSmoother`, the bounded offset that absorbs the two level steps φ cannot cover.
|
||||
- `live_params.h` / `live_params.cpp` — the live-parameter block: `LiveValues` (the plain, trivially-copyable bundle the audio thread observes), the single-writer `LiveParams` seqlock that publishes it without a lock or a torn read, `foldLive` (the ONE derivation from `PlayParams` — every publisher goes through it so the two representations cannot drift), and `ValueRamp`, the per-frame glide whose EXACT termination is what lets the filter's equality-compare cutoff skip re-engage. Links no engine: the block is a value the voice observes, not a thing the engine owns.
|
||||
- `voice.h` / `voice.cpp` — one voice. The per-SAMPLE render half (`advanceFrame` and everything it calls) is INLINE IN THE HEADER by RT constraint; the per-NOTE half (note-on setup incl. the Preserve ring prime, legato retune, gate-off, the off-thread shifter presize) is out of line in the TU. The voice owns its own `VoiceFilter` and filter envelope, run between the pitch stage and the amp multiply — see `engine/filter/CLAUDE.md`.
|
||||
- `voice_engine.h` / `voice_engine.cpp` — `VoiceEngine`: note routing, bounded-stealing allocation, user-parameterized voice count (1–32, default 16), `VoiceMode` Poly/Mono (last-note held-note stack, `MonoTrigger` Retrigger/Legato), two-tier panic (CC 123 = all-notes-off release, CC 120 = immediate hard-stop including Trigger one-shots), and the block render loops. Preview injects a synthetic note-on at the loaded capture's root note into the main `VoiceEngine` — no dedicated `PreviewCard`; preview obeys polyphony/mono/voice-stealing/envelopes.
|
||||
@@ -261,7 +266,7 @@ slider couldn't. Two pure modules split the forward (draw) and inverse (edit) ma
|
||||
- `param_slider` — parameter control-panel: vertical stack of TOGGLE (two-segment selector) and SLIDER (horizontal track) rows; maps normalized value to/from handle pixel.
|
||||
- `embed_strip` — compact single-row control layout for embed mode in the track FX chain.
|
||||
- `knob_deck` — pure knob-deck layout + hit-test (FB1): group-box / caption-row / compact-toggle / knob-cell geometry, deterministic whole-group wrap, `DeckLayout` / `DeckHit`. Mirror of `action_bar`/`param_slider`; no LICE or REAPER types.
|
||||
- `deck_groups` — also home to `isLiveDeckParam`, the editor's commit-tier routing predicate (see "Live parameter delivery" above); WHICH groups the Sample face's deck carries, split from `knob_deck`'s HOW they lay out: the `DeckParam` control-id space (the editor's `ParamControl` is an alias of it), the `DeckGroupId` list, `sampleDeckGroups` in signal-flow order (**pitch → filter → amp**, then voice/master), and the deck's bipolar-knob law. Reads `PlayMode` for the AMP group's Gate/Trigger face, which is why this and not `knob_deck` is the module that touches the engine's value layer.
|
||||
- `deck_groups` — also home to `isLiveDeckParam` and `liveCommitFor`, the editor's whole commit-tier routing decision (see "Live parameter delivery" above); WHICH groups the Sample face's deck carries, split from `knob_deck`'s HOW they lay out: the `DeckParam` control-id space (the editor's `ParamControl` is an alias of it), the `DeckGroupId` list, `sampleDeckGroups` in signal-flow order (**pitch → filter → amp**, then voice/master), and the deck's bipolar-knob law. Reads `PlayMode` for the AMP group's Gate/Trigger face, which is why this and not `knob_deck` is the module that touches the engine's value layer.
|
||||
- `curve_popup` — pure curve-popup geometry + dismissal test (FB1): centered sheet over the Sample face — width/height clamps, title row, Close button rect, curve-box rect, outside-sheet dismissal test. Mirror of `overflow_menu`; no LICE or REAPER types.
|
||||
- `envelope_overlay` — pure amp-envelope→polyline geometry for the Sample-view envelope overlay (read from `envelope_overlay.h`): maps Gate's AHDSR shape or Trigger's fade-in/unity/%-length/fade-out shape to a polyline inside a rect at the shared time base (Gate: a bounded param-domain schematic, sample-length-free; Trigger: PCM-aligned wall-clock), every vertex clamped in-canvas (`x`/`y` inside the rect). Shares the `EnvNode`/`AmpEnvelope`/`timeToX`/`levelToY` vocabulary with `envelope_edit` so the drawn handle and its grab region agree pixel-for-pixel. No VST3/REAPER/LICE types at the boundary.
|
||||
- `envelope_edit` — pure node hit-test + pixel-delta→clamped-param inverse map for the draggable envelope nodes (read from `envelope_edit.h`): `nodeAtPoint` resolves a grab to the nearest node within a pick radius (Chebyshev distance, draw-order tie-break); `resolveNodeDrag` maps a pixel delta since grab to a new `AmpEnvelope`, enforcing monotonic-in-time ordering between neighbouring nodes and the same caller-supplied per-param clamp bounds the sliders use — a drag can never produce a param a slider couldn't. Mirror of `card_drag`/`waveform_view`; the inverse of `envelope_overlay`'s params→polyline forward map, so node-drag and slider-edit read/write one shared model and can never diverge.
|
||||
@@ -280,13 +285,14 @@ slider couldn't. Two pure modules split the forward (draw) and inverse (edit) ma
|
||||
- **A filter envelope only advances while its depth is non-zero.** `tickFilterCutoff`'s exact
|
||||
skip at `modAmount == 0` skips the envelope tick along with the solve, so dialling depth up
|
||||
mid-note starts the envelope from the note's stage-0 position rather than from where it would
|
||||
have been. Continuous either way (the contribution starts at 0), and keeping the skip is what
|
||||
holds the at-rest per-sample path byte-identical — but don't read a live depth move as
|
||||
"resuming" an envelope that was never running.
|
||||
have been. Its step smoother is frozen with it — an absorbed step sits in the offset and
|
||||
emits when depth is next dialled up (bounded, and scaled by a depth ramping from 0).
|
||||
Continuous either way (the contribution starts at 0), and keeping the skip is what holds the
|
||||
at-rest per-sample path byte-identical — but don't read a live depth move as "resuming" an
|
||||
envelope that was never running.
|
||||
- **A live edit leaves the snapshot's own `sample.play` stale, on purpose.** The block, not the
|
||||
snapshot, is the audio thread's source; a new voice latches the stale copy and is corrected by
|
||||
`applyLive(snap)` before its first frame. The persisted `params_` is written by the same
|
||||
commit, so `getState` is never stale.
|
||||
`snapLive` before its first frame.
|
||||
- **`keyboard_strip`'s width-uniformity guarantee is client-pixel only.** Its test sweep
|
||||
covers client-pixel widths (including multiples standing in for larger client areas);
|
||||
nothing in the instrument implements `IPlugViewContentScaleSupport`, so host-side DPI
|
||||
|
||||
Reference in New Issue
Block a user