forge f2: inject(single) accumulates into the pending batch#6405
Merged
shipyard-local[bot] merged 5 commits intoJul 22, 2026
Conversation
auto-merge was automatically disabled
July 20, 2026 20:46
Pull request was closed
danielraffel
force-pushed
the
feature/forge-f2a-bakelayer-param-injection
branch
from
July 20, 2026 20:46
dad355a to
d769efd
Compare
Bake-layer parameter injection: a control thread turns a knob on a BAKED custom node and the change lands sample-accurately in process() without re-baking and without touching the live graph's parameter ingress. - BakedParamView + CustomNodeBakedParam + process_instance_baked_param on CustomNodeType: a lowerable node declares injectable params and reads their ramped, sample-accurate values per block. - ParamInjector / InjectStatus + BakedGraphProcessor::claim_param_injection: per-node single-writer mailbox, latest-wins, exclusive-claim discipline, reusing pulp::state::ParameterEvent / ParameterEventQueue verbatim. - ParamCursor ramp introspection (ramp_target / ramp_end_offset) so a ramp longer than one block continues to completion across subsequent blocks (the C1 mid-ramp-freeze fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV
Wrap the rest of the lo-fi kit as lowerable custom nodes whose one macro knob is injectable via the F2-a bake-layer path, plus a new bitcrush/decimator (the one block with no core/signal equivalent). Header-only catalog (forge_lofi_catalog.hpp): the wrapped signal blocks are header-only templates, so this adds no new link dependency to pulp-host. Nodes (each: declares baked_params, reads its BakedParamView, lowerable=true, transport-independent — the DelayLine node's F2-a pattern): * Filter (Svf lowpass) → "Tone" cutoff_hz (sample-accurate) * Waveshaper (tanh) → "Drive" drive (sample-accurate) * Dry/Wet (DryWetMixer) → "Mix" mix (block-rate, see note) * Noise (splitmix64) → "Hiss" level (sample-accurate) * Bitcrush/decimator → "Crush" bit_depth + sample_rate_reduction (new DSP) Bitcrush is implemented inline: mid-tread quantization to 2^bit_depth levels (fractional via exp2) + sample-and-hold decimation (phase accumulator), both injectable. Tests (test_forge_lofi_catalog.cpp): for each node, bake a graph containing it and prove injecting its knob changes the baked output — cutoff attenuates a high tone, drive grows RMS/peak, mix crossfades dry/wet, level raises the noise floor, bit_depth grows quantization error, rate reduction flattens sample-to- sample steps. One RT-allocation probe over every node's inject+process path. Finding: DryWetMixerT's public API is block-rate (scalar set_mix + block mix_wet over an internal dry buffer, no per-sample gain hook), so its mix is applied at block granularity — the honest tradeoff for a mix macro; the other knobs stay per-sample. Deferred (follow-up): LFO/wow modulation of delay time is graph-internal modulation, not a macro knob. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV
Add make_delay_node() to the forge lo-fi catalog: a self-contained interpolated feedback echo (pulp::signal::DelayLine) whose time_ms and feedback are injectable baked params, live on the baked graph with no re-bake. Fills the gap where delay_line previously had no lowerable catalog realization (Forge's loader lowered it to a frozen Gain-feedback block). Covered by the F2 catalog test (feedback grows the tail; distinct tap times leave distinct near-field energy) and the RT sweep. Header-only (wraps the header-only DelayLine template) — no new host link dependency. Held for the F2 architectural/adversarial review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV
Fix the F2-review footgun: ParamInjector::inject(single) published a fresh one-event snapshot per call, so the TripleBuffer's latest-snapshot-wins collapsed two single injects to the SAME node with no intervening process() into only the last one — a multi-param node silently dropped a param. inject(single) now MERGES into the still-unconsumed pending batch (the new event supersedes only that param's pending entries; other params' pending events are preserved), so N single injects to different params between blocks all land. The audio-side drain records the applied sequence in the mailbox (consumed_sequence) so a post-consumption single inject starts a fresh batch instead of re-publishing already-applied events (a re-applied ramp would re-glide). Accumulation stays control-thread-side and single- writer; the audio side still reads one published snapshot per block, and the path remains allocation-free. If the pending batch is FULL of other params' events (a full-capacity queue publish, unconsumed) the single inject is refused loudly with PartialOverflow rather than silently evicting a pending event. Regression test: two single injects (delay time_ms=50ms then feedback=0.8) between blocks — both must land (echo position AND recirculation ratio), verified to fail against the pre-fix code. Also pins the full-pending-batch PartialOverflow branch. Hosting SKILL.md documents the bake-layer param- injection primitive and its batch-vs-accumulate publication contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV
github-merge-queue
Bot
removed this pull request from the merge queue due to no response for status checks
Jul 21, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Jul 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix the F2-review footgun: ParamInjector::inject(single) published a fresh
one-event snapshot per call, so the TripleBuffer's latest-snapshot-wins
collapsed two single injects to the SAME node with no intervening process()
into only the last one — a multi-param node silently dropped a param.
inject(single) now MERGES into the still-unconsumed pending batch (the new
event supersedes only that param's pending entries; other params' pending
events are preserved), so N single injects to different params between
blocks all land. The audio-side drain records the applied sequence in the
mailbox (consumed_sequence) so a post-consumption single inject starts a
fresh batch instead of re-publishing already-applied events (a re-applied
ramp would re-glide). Accumulation stays control-thread-side and single-
writer; the audio side still reads one published snapshot per block, and
the path remains allocation-free. If the pending batch is FULL of other
params' events (a full-capacity queue publish, unconsumed) the single
inject is refused loudly with PartialOverflow rather than silently
evicting a pending event.
Regression test: two single injects (delay time_ms=50ms then feedback=0.8)
between blocks — both must land (echo position AND recirculation ratio),
verified to fail against the pre-fix code. Also pins the full-pending-batch
PartialOverflow branch. Hosting SKILL.md documents the bake-layer param-
injection primitive and its batch-vs-accumulate publication contract.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV
🔎 Provenance
claudem5~/Code/pulp14ef2f39-9113-4e61-bd8b-8faab97cab00Resume
Jump to this tab
Relaunch (any agent)
Restore URL — https://claude.ai/code/session_015tUppDEybTgFDxS5BGKBWV
stamped 2026-07-20 16:23 UTC