Make spline EG drags audible live: held and new notes follow a dragged knot
Contours ride their own seqlock block into a double-buffered engine copy, and the swap step is smoothed. Each drag move publishes contours without a model write; release still commits and reloads.
This commit is contained in:
@@ -203,6 +203,14 @@ automatable parameters."* It rejects the precedent, not one instance of it.
|
||||
- **Two genuine level steps are smoothed, not ruled away**: a sustain level moved while the
|
||||
voice holds it, and a stage duration dialled to exactly zero mid-stage. Both are absorbed by
|
||||
the envelope's own bounded offset smoother.
|
||||
- **A drawn contour is live too, through a second block.** `LiveContours` (all three contours
|
||||
as fixed-capacity arrays, ~9 KB) is published beside `LiveValues` rather than inside it, so a
|
||||
knob publish, the automation merge and the block compare never carry it. Each `VoiceEngine`
|
||||
double-buffers it, and `applyContoursToActive` (`voice_engine.h`) owns the rebind invariant
|
||||
that makes the double buffer safe. A cursor `start()` left cleared (a Staged EG) stays
|
||||
cleared: Staged|Spline is a note-on fact. A contour is positional, like the Trigger AHD,
|
||||
so a swap is a level step at the read position, absorbed by a per-EG `StepSmoother` — one more
|
||||
predicted branch per sample on a drawn EG, none on a Staged one.
|
||||
|
||||
### Non-goals / guardrails (instrument-specific; repo-wide invariants live in root CLAUDE.md)
|
||||
|
||||
@@ -255,8 +263,9 @@ consequences, each with one home:
|
||||
- **Normalized is what makes a contour length-independent.** There are no stored seconds to
|
||||
rescale, so a different-length capture replays the same shape proportionally.
|
||||
- **The contours sit on `PlayParams`/`PlaySeconds` directly, not inside the three envelope
|
||||
structs.** Those are copied whole into the live block, which must stay trivially copyable
|
||||
(`live_params.h`) — and a contour is not a live control anyway: like the velocity curves it
|
||||
structs.** Those are copied whole into the live block, which must stay small and trivially
|
||||
copyable (`live_params.h`). A contour still reaches the note already sounding — through its
|
||||
OWN block (`live_contours.h`), see "Live parameter delivery". Only the Staged|Spline mode
|
||||
travels by reload.
|
||||
|
||||
**Which shape a STAGED envelope takes is decided by the play mode, not by what it modulates:**
|
||||
@@ -294,7 +303,7 @@ anything for a trigger shape.
|
||||
- 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. Also the ONE home of the drawn-EG rule family — `splineActive`, `effectivePlayMode`, `enforceGateUnavailableWhileDrawn` and `effectiveLengthFraction` — all templated over the frames and seconds representations, so no consumer of either can re-read the raw fields instead.
|
||||
- `envelopes.h` — the three per-frame evaluators (`AdsrEnvelope` AHDSR, `AhdEnvelope` the sustain-less Attack/Hold/Decay, `PitchEnvelope` the AHD pitch offset), CONCRETE and fully header-inline. Never give them a common base or a virtual `tick()`: they are called per-voice-per-sample. Also home to `fitAhd`/`ahdLevelAt`, THE span split and shape every sustain-less envelope shares. A voice carries two of each shape — the amp's and the filter's — and its play mode picks which pair it reads. `AdsrEnvelope`/`PitchEnvelope` own `applyLive` (the φ-holding mid-stage rule), its fresh-note peer `snapLive`, and `StepSmoother`, the bounded offset that absorbs the level steps φ cannot cover; `AhdEnvelope` is POSITIONAL (evaluated at a source offset, not ticked), so it has no phase to hold and smooths a live reshape instead.
|
||||
- `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), the block's FIELD-wise `operator==` (never a memcmp — the header owns why the padding makes a byte compare report differences that do not exist), 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.
|
||||
- `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), the block's FIELD-wise `operator==` (never a memcmp — the header owns why the padding makes a byte compare report differences that do not exist), 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. The same library carries `live_contours.h`/`.cpp` (the drawn-contour block beside it, and `foldContours`, templated over both parameter representations like `splineActive`) and the header-only `seqlock.h`, the ONE seqlock both blocks publish through.
|
||||
- `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`. **Documented ~600-line-ceiling exception** (root `CLAUDE.md` structural heuristic 1): `voice.h` sits over the ceiling because `advanceFrame`'s RT-inline constraint forbids the seam a split would need — a documented exception, not silent overshoot.
|
||||
- `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.
|
||||
- `engine/loop/` — the sustain loop's ONE validity/clamp fold (`resolveLoop`) plus its pre-seam crossfade geometry and the editor's default handle span; see `engine/loop/CLAUDE.md`. The voice folds it once at note-on; the crossfade weight is header-inline because it rides the per-sample read.
|
||||
@@ -396,7 +405,7 @@ anything for a trigger shape.
|
||||
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
|
||||
`snapLive` before its first frame.
|
||||
`snapLive` (and a drawn EG by `applyContours`' snap) 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