Skip to content

Host state, transport ticks, and update() dispatch (CLAP + VST3)#173

Open
jcelerier wants to merge 13 commits into
mainfrom
feature/state
Open

Host state, transport ticks, and update() dispatch (CLAP + VST3)#173
jcelerier wants to merge 13 commits into
mainfrom
feature/state

Conversation

@jcelerier

@jcelerier jcelerier commented Jul 20, 2026

Copy link
Copy Markdown
Member

Session persistence for custom processor state, host transport ticks, and update() dispatch on host parameter changes.

This branch now carries the transport work as well: #174 was merged into it rather than into main, so the two features arrive together. Rebased onto main after #175 landed; the parameter-space commits it shares with #175 were dropped as already-upstream. The design rationale lives in the new "Host state" chapter of the manual.

Host state

  • avnd::has_custom_state: a processor's opaque, self-versioned blob, detected through concepts and accepted in several spellings (byte-container return or zero-alloc writer; ptr+size or span load, bool- or void-returning).
  • CLAP gains CLAP_EXT_STATE (it had none): a versioned container holding parameter values and the blob, tolerant of partial stream IO.
  • VST3 stores the blob in a magic-tagged trailer after the existing parameter doubles, so sessions written by earlier builds still load.
  • Parameters are restored before the blob; a rejected blob leaves the object untouched rather than half-applied.
  • Parameters in the container are addressed by id rather than by ordinal, so processors whose parameters follow other ports (audio, MIDI) restore onto the right fields. update() is not dispatched while restoring: a port that derives other state from its own value would otherwise clobber freshly restored values in save-order-dependent ways.

Transport

  • avnd::transport_tick, a host-independent carrier that the existing current_tick() machinery maps onto whatever tick a processor declares.
  • CLAP fills it from clap_process::transport plus CLAP_EVENT_TRANSPORT; VST3 from ProcessContext, honouring each validity flag with documented fallbacks.
  • Both converters are pure functions, unit-tested without a host; the VST3 one is templated over the context type so it can be exercised against a structural mirror.
  • A processor that never receives transport information gets exactly today's frames-only tick.

update() dispatch

Host parameter changes now invoke a port's update() callback in the CLAP and VST3 bindings, matching the ossia binding and the documented contract (the book previously listed ossia as the only support).

Testing

test_state, test_state_clap, test_transport, test_update_controls, plus the pre-existing suites: 106 tests pass locally (MSVC 19.50, Windows). The CLAP state path is covered by a round-trip through the real extension with forced 3-byte partial transfers and corruption/truncation rejection.

Validated against clap-validator 0.3.2 with a plug-in whose shape exercises the id-addressing path (audio and MIDI ports before ~14 mixed-type parameters, plus a large custom blob): 16 passed / 0 failed, and pluginval strictness 10 passes on its VST3 build.

Open

  • The VST3 state path is compile-verified and covered through the shared container logic, but has no runtime test against a real IBStream.
  • Loop/cycle ranges and intra-block tempo ramps are not carried by the tick; last_signature_change stays 0.

🤖 Generated with Claude Code

jcelerier and others added 12 commits July 20, 2026 16:40
avnd::has_custom_state detects save_state/load_state member functions in
several ergonomic spellings (byte-container or writer-callback save;
pointer+size or span load, bool- or void-returning). avnd::save_state /
avnd::load_state normalize them for the bindings. Static detection tests
for accepted spellings and rejected shapes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CLAP gains CLAP_EXT_STATE (previously absent): a versioned container with
the normalized parameter values followed by the processor's opaque blob.
VST3 appends a magic-tagged trailer after the existing parameter doubles,
keeping current session data byte-compatible (the controller's
setComponentState never reads past the doubles; old sessions hit EOF
before the trailer and load fine).

Malformed blobs are rejected without partial application. Polyphonic
containers save from the first instance and load into all.

Adds the examples/Raw/CustomState.hpp reference example (compiled for
both bindings) and a Catch2 round-trip test driving the real CLAP
extension through in-memory streams with forced partial IO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CMake 4 removed the bundled FindBoost module; the shim defers to Boost's
own config files when installed and otherwise satisfies the header-only
requirement from BOOST_ROOT.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shim only probed <root>/boost/version.hpp, so a BOOST_ROOT pointing at
an installed tree (<root>/include/boost/version.hpp) was rejected even
though the headers were there. Probe with find_path, which covers both
layouts and the default search paths, and report through
find_package_handle_standard_args so the requested version is honoured.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
avnd::transport_tick is a plain aggregate holding one block's transport
snapshot (tempo, time signature, playing state, musical and time-based
positions, bar boundaries, flicks); current_tick() maps its members onto
whatever tick type a processor declares. Also map a 'playing' member in
current_tick so processor ticks can carry the transport running state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Track clap_process::transport at block start plus CLAP_EVENT_TRANSPORT
in-events, and convert to an avnd::transport_tick handed to the process
adapters: fixed-point beat/second positions (exact division by
CLAP_BEATTIME_FACTOR), tempo/time-signature/playing flags with 120 BPM
and 4/4 fallbacks, bar boundaries at block start and end, and
end_position_in_quarters computed at constant tempo over the block.
Hosts that never provide transport keep the previous frames-only tick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Convert ProcessData::processContext to an avnd::transport_tick honoring
the validity flags (kTempoValid, kTimeSigValid, kProjectTimeMusicValid,
kBarPositionValid, kSystemTimeValid, kPlaying) with fallbacks: 120 BPM,
4/4, musical position derived from projectTimeSamples at the current
tempo, bar boundaries assumed aligned at zero when unreported. The
converter is templated over the context type so it stays testable
without the SDK; without a ProcessContext the previous frames-only
invocation is preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixed-point beat conversion exactness, validity-flag fallbacks for both
bindings, bar-boundary advancement across blocks, seconds-from-beats
derivation, steady-time fallback, and end-to-end mapping of a transport
tick onto a processor-declared tick type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports with an update() member were only notified at initialization
(init_controls) and by the ossia binding; host-driven parameter changes
through the clap and vst3 bindings silently skipped the callback. Apply
the same convention as init_controls: if_possible(ctl.update(state.effect))
right after the new value is written, for every effect instance.

The vst3 parameter application moves to stv3::apply_control in
param_apply.hpp, free of SDK types so the path is testable on its own.
Bulk value restores (state loads) dispatch the callbacks too, through the
new avnd::update_controls helper. Tests drive both bindings' application
paths and verify one invocation per change with the new value in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The state container stored parameter values as dense-ordinal doubles
while the params extension speaks raw field ids: on processors whose
parameters follow other ports (audio, midi), restores could land values
on the wrong fields and the two spaces drifted across versions. The
container (version 2) now stores (raw id, plain value) pairs -- the same
id and value space as the extension -- and skips unknown ids so states
survive parameter removals.

State restores are also no longer treated as host gestures: update()
callbacks are not dispatched while restoring (CLAP load, VST3 setState).
A derive-style handler -- one that rewrites other ports when its own
value changes -- would otherwise clobber freshly restored values in
save-order-dependent ways. Processors deriving internal state from their
ports reconcile in their custom-state load instead. Event- and
flush-driven changes keep dispatching update() as before.

Tests: a mixed-shape processor (audio port before the parameters, so raw
ids differ from ordinals, plus a derive-style int port) saves modified
values and restores them into a fresh instance, asserting every value
through the extension vtable and that the derive handler did not run.
Adds the MixedParams example exercising the same shape as a plug-in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jcelerier jcelerier changed the title Host state: session persistence for custom processor state (CLAP + VST3) Host state, transport ticks, and update() dispatch (CLAP + VST3) Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant