Skip to content

feat: switch a preset off, or ramp it, as one thing - #3189

Merged
vanceingalls merged 899 commits into
mainfrom
wa-20d5a-preset-ramp
Aug 13, 2026
Merged

feat: switch a preset off, or ramp it, as one thing#3189
vanceingalls merged 899 commits into
mainfrom
wa-20d5a-preset-ramp

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

A preset's nodes share no automatable parameter, and its worklet effects expose no AudioParams at all — Clean Voice could only ever have automated 3 of its 5 nodes. So the graph wraps each preset's run in a wet/dry pair, and fx.preset.<id> drives the blend.

Off is amount 0, not enabled: false — the switch and the lane are the same value, with the switch at its two ends. Writing enabled would take the nodes out of the graph, which a lane cannot do part-way.

The bracket also gains On/Off and × for the whole preset: it said a preset was one thing the author added, then made them treat it as five.

The round-trip for the new presetAmount field had no test until a mutation survived one — the same invariant that bit fromPreset earlier in the stack.

🤖 Generated with Claude Code

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: LGTM

Empirical claim verification (head SHA e021065)

  • presetAmount?: number on HfAudioFxNodepackages/core/src/audioFx.ts:363-373.
  • Round-trip in BOTH halves: parse (clamped) at audioFx.ts:381,391-393; serialize (omitted when 1) at audioFx.ts:403-405. Round-trip test at audioFxPresets.test.ts:417-440; clamp test at :442-454.
  • Wet/dry graph wrap at audio/audioFxGraph.ts:194-231; handle.presets at :235-236; values-only update at :693-702; unwire on dispose at :710-717.
  • Consecutive-only grouping in presetRuns at audioFxGraph.ts:149-167; amount clamp Math.min(1, Math.max(0, amount)) at :161.
  • fx.preset.<id> lane target: parseAutomationTarget at audioAutomation.ts:299-302, factory presetAutomationTarget at :322-324, orphan-lane drop via chain?.nodes.some((n) => n.fromPreset === parsed.presetId) at :348-350. Parsed BEFORE 3-part fx form, correct since PRESET_TARGET_KEY = "preset" cannot collide with minted n1/n2/… ids.
  • Lane scheduler consults presets?.[parsed.presetId] at audio/audioFxAutomation.ts:263-266; runtime plumbs handle.presets at runtime/audioFx.ts:181-183 and offline entry at stubs/audio-fx-runtime-entry.ts:487-493.
  • FE Off/On/× on the run head at propertyPanelFxSection.tsx:582-627; setRunAmount writes presetAmount (not enabled) at :359-370; run-remove sheds lanes via onRemoveNodeAutomation at :378-386. automatedPresets derivation at propertyPanelAudioFxGroup.tsx:530-535.

OLD-assumption counterfactuals (every NEW test)

  • graph exposes one blend / blends at stored amount / fully applied by default / pushes amount / wraps nothing on own nodes / unwires on dispose (audioFxGraph.test.ts:65-112) — falsify: (a) no handle.presets at all; (b) hard-coded wet=1; (c) default 0 (silences legacy chains); (d) update() returning false forcing rebuild; (e) wrapping every node not just fromPreset runs; (f) dispose only iterating handles.
  • Round-trip (audioFxPresets.test.ts:417-440) — falsifies missing presetAmount in EITHER parse or serialize (same fromPreset failure mode).
  • Clamp (:442-454) — falsifies missing 0..1 clamp letting 4 through and driving dry = -3.
  • FE switches off in one gesture — falsifies OLD per-node-Off UX AND enabled: false mechanism.
  • puts half in / seeded where it sits / driven not slider — falsifies missing run-head Amount row and missing fx.preset.<id> target.
  • reads as on while any is applied — falsifies aggregating by enabled instead of presetAmount > 0.
  • back out whole with lanes — falsifies missing orphan-lane cleanup on run remove.

Cross-PR coupling

  • New field on HfAudioFxNode: parse + serialize allow-lists both updated with pinned round-trip. Old chains fall through to fully applied via typeof node.presetAmount === "number" ? ... : 1 (audioFxGraph.ts:158) — no silent-off on legacy load.
  • Ramp is a real automation lane (fx.preset.<id>), not a snapshot; scheduler reads handle.presets targets.
  • Off mechanism is presetAmount: 0, enabled preserved so settings survive (verified in switches back on rather than deleting).

Nits (non-blocking)

  • If a reorder splits a preset into two non-consecutive runs, presetRuns emits two entries with the same preset. In buildFxChain, presetTargets[p.id] = mixTargets(...) at :235-236 overwrites (lane drives second wrap), while presets.find((p) => p.id === run.preset) at :698 matches the first wrap (static amount writes hit the first). Design comment ("no longer a unit") acknowledges this, but the two writes disagree rather than both no-op. Consider presets.filter(...) on the update path so a torn preset at least stays consistent.
  • automatedPresets in propertyPanelAudioFxGroup.tsx:530-535 is recomputed each render — cheap but a useMemo would match sibling patterns.

CI

Preflight failure is the stack-wide oxfmt --check drift on 9 .md files (README, plans/, skills/hyperframes-audio/) — none touched by this PR. player-perf / preview-regression / regression FAILUREs are if: always() aggregators reporting FAILURE on SKIPPED shards after Detect changes skipped the matrix — stack-wide, no this-PR signal.

— Review by tai (pr-review)

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat: switch a preset off, or ramp it, as one thing — #3189

Verdict: LGTM

presetAmount in both parse AND serialize — confirmed. Parse uses type guard + finiteness check + 0..1 clamp. Serialize intentionally omits when amount === 1 (fully applied) so existing chains don't bloat. The wa-18 invariant is satisfied.

Wet/dry crossfade design is sound. A 0..1 value drives a dry/wet gain bracket around the entire preset run. Amount=1 is fully applied (wet only), amount=0 is fully bypassed (dry signal, untouched). The toggle and the automation lane are the same value — one mechanism for both "switch off" and "ramp it." This dissolves the class of bug where two separate controls for the same thing get out of sync.

No rebuild on amount change. update() pushes new gain values into the running graph (returns true), so sliding the amount knob doesn't restart the audio. The chain stores mechanism values (real params), not the amount — same contract as normalizeCarveSettings.

Round-trip tests verify amounts [0, 0.4, 1] survive serialize→parse, 1 round-trips to undefined (absent = fully applied), and out-of-range values (4) clamp on parse.

Orthogonal with profiles — preset amount controls overall wet/dry blend of the run; profile strength scales individual parameter values. A preset at amount=0.5 with strength=0.7 means 70% parameter intensity, then 50/50 blend with dry. No interference by design.

7 graph tests + 7 UI tests + 2 round-trip tests. Clean coverage.


Review by Miga

🤖 Generated with Claude Code

# Conflicts:
#	packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx
#	packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx
#	packages/studio/src/components/editor/propertyPanelFxSection.tsx
#	skills-manifest.json
#	skills/hyperframes-audio/SKILL.md
#	skills/hyperframes-audio/references/attributes.md
#	skills/hyperframes-audio/scripts/carve.mjs
Base automatically changed from wa-20d4-profiles to main August 13, 2026 19:07
@github-actions

Copy link
Copy Markdown

Fallow audit report

Found 10 findings.

Duplication (2)
Severity Rule Location Description
minor fallow/code-duplication packages/core/src/audioFxEq.test.ts:33 Code clone group 1 (7 lines, 2 instances)
minor fallow/code-duplication packages/core/src/audioFxPresets.test.ts:247 Code clone group 1 (7 lines, 2 instances)
Health (8)
Severity Rule Location Description
major fallow/high-crap-score packages/core/src/audioFx.ts:890 'nodes' has CRAP score 88.0 (threshold: 30.0, cyclomatic 18)
minor fallow/high-crap-score packages/core/src/audioFx.ts:946 'nodes' has CRAP score 31.6 (threshold: 30.0, cyclomatic 10)
major fallow/high-crap-score packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx:234 'setCarve' has CRAP score 97.0 (threshold: 30.0, cyclomatic 19)
minor fallow/high-crap-score packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx:307 'carvedAgainstBy' has CRAP score 37.1 (threshold: 30.0, cyclomatic 11)
minor fallow/high-crap-score packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx:601 'auditionLevel' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
critical fallow/high-crap-score packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx:648 'analyse' has CRAP score 148.4 (threshold: 30.0, cyclomatic 24)
major fallow/high-complexity packages/studio/src/components/editor/propertyPanelFxSection.tsx:134 'FxSection' has cyclomatic complexity 21 (threshold: 20) and cognitive complexity 31 (threshold: 15)
major fallow/high-crap-score packages/studio/src/components/editor/propertyPanelFxSection.tsx:549 '<arrow>' has CRAP score 71.3 (threshold: 30.0, cyclomatic 16)

Generated by fallow.

@vanceingalls
vanceingalls merged commit acdb112 into main Aug 13, 2026
56 of 58 checks passed
@vanceingalls
vanceingalls deleted the wa-20d5a-preset-ramp branch August 13, 2026 19:35
vanceingalls added a commit that referenced this pull request Aug 13, 2026
# Conflicts:
#	packages/core/src/audio/audioFxGraph.ts
#	packages/core/src/audioFx.ts
#	packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx
#	packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx
#	packages/studio/src/components/editor/propertyPanelFxPresetMenu.tsx
#	packages/studio/src/components/editor/propertyPanelFxSection.test.tsx
#	packages/studio/src/components/editor/propertyPanelFxSection.tsx
#	plans/audio-automation-lanes/SPEC.md
#	plans/audio-fx-presets.md
#	plans/audio-fx-ux/README.md
#	plans/automation-lanes-shared-rows.md
#	plans/automation-time-selection-design.md
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.

3 participants