Skip to content

feat: one knob for the five effects that cannot honestly have one - #3188

Merged
vanceingalls merged 874 commits into
mainfrom
wa-20d4-profiles
Aug 13, 2026
Merged

feat: one knob for the five effects that cannot honestly have one#3188
vanceingalls merged 874 commits into
mainfrom
wa-20d4-profiles

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

A compressor has seven controls and an author wants one, but no single one can be its face: threshold means nothing without ratio. So the knob is derived, exactly as carveProfile already turns one number into six.

Three of the five sets of figures were wrong, and only rendering showed it. Measured through the real engine path on 20 s of narration:

  • Compressor make-up was too small and not linear. The proposed 1/3/7 dB left the track 2.5 dB quieter at full evenness — an evenness knob that turns the track down as it goes up. Now s² × 9.5: spread falls 19.1 → 8.8 dB with level unmoved.
  • Saturation's trim went the wrong way. A soft clipper at −18 dB is a limiter at −18 dB: peak fell 0.496 → 0.089. Reversed; RMS now holds within 0.4% while the peak comes down.
  • The gate did essentially nothing — 0.1 dB against a range asking for 30, because release was not in the profile. Swept, release dominates: 0.2 dB at 140 ms, 13.4 dB at 10 ms. Gaps now fall 29.6 dB with speech unmoved.

The first measurement reported the gate as working when it was doing nothing at all — a speaking-window measure excludes exactly the windows a gate acts on.

Also fixes a profiled effect arriving at registry defaults, which put the knob at 0.67 with make-up at 0 dB.

🤖 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. Preflight red is stack-wide pre-existing drift (verified below), not this PR.

Verified claims (head 96e3dd85).

  • Five effects with derived knobs = compressor, gate, saturate, reverb, bitcrush — audioFxProfiles.ts:229-340. Coverage pinned by test to EFFECT_COPY[id].primary === "strength" (audioFxProfiles.test.ts:63-73).
  • "Compressor makeup = s²·9.5, not linear 1/3/7" — audioFxProfiles.ts:256.
  • "Saturation reversed to s²·2.8 up" — audioFxProfiles.ts:301.
  • "Gate release added to profile" — audioFxProfiles.ts:280 (120 − s·105), covered by derives: [..., "release"] at line 264.
  • "Registry defaults ≠ point on curve; compressor arrived at Evenness 0.67 / makeup 0 dB." Fix: propertyPanelFxSection.tsx:702-704 seeds through applyAudioFxProfile(type, 0.5, defaultAudioFxParams(type)) when profile exists; test at propertyPanelFxSection.test.tsx:596-611.
  • NaN → 0.5 (never silence) — audioFxProfiles.ts:209-211 clamp01; test at audioFxProfiles.test.ts:164-172.

OLD-assumption counterfactuals.

  • covers exactly... fails if a primary: "strength" effect ships without a profile (five-set changes) or a profile is added for an effect the copy hasn't nominated.
  • derives only params effect has fails if derives names a key not in the registry def.params (typo migration guard).
  • stays inside declared range fails if sweep of at(s) ever exceeds param.min/max (clamping happens on write; the knob would silently go inert past the boundary).
  • moves monotonically fails on the exact old three-point-table shape the design proposed — a hand-tuned middle can turn around.
  • keeps knee/mix under Details fails if applyAudioFxProfile replaces instead of {...params, ...profile.at(s)} (verified merge at audioFxProfiles.ts:360).
  • reads strength back fails if audioFxProfileStrength maps low↔high wrong on the first derived key (regression against inverting carveProfile-style contract).
  • passes through anchors fails on the exact numeric drift the make-up/trim/gate corrections introduced (-12, 4, 90, -55, -18, 0.25/0.55/0.9, 6).
  • seeded on curve fails if withEffect reverts to defaultAudioFxParams(type) for profiled effects — the Evenness-0.67-with-0dB-makeup regression.

Cross-PR coupling verdict.

  • No new HfAudioFxNode field. Profile is a derivation over stored params; parseAudioFxChain / serializeAudioFxChain (audioFx.ts:862-949) unchanged and correctly untouched — the load-bearing-pair pattern (#3177+#3178, #3212+#3213) does not apply.
  • Coverage table analog to #3184's HF_AUDIO_FX enumeration: yes — audioFxProfiles.test.ts:63-73 enumerates EFFECT_COPY and demands exact set equality.
  • Curve mapping: per-effect specific (linear anchors for threshold/ratio/attack/release/range; s² for makeup/output; piecewise-linear for reverb size to hit the 0.25/0.55/0.9 anchors — verified at(0)/at(0.5)/at(1) in the anchors test). Monotonic per param tested at 21 samples.
  • Rack UI: derived knob replaces the primary control on the open face and Details reveals the real 5-7 params (propertyPanelFxNodeRow.tsx:482-508, 517, 526). When no profile is present (derived === null), the existing primary-param path is unchanged; oneKnob = primary !== null || derived !== null gates the disclosure. Automation correctly stays on the real params under Details — the derived knob has no AudioParam (propertyPanelFxSection.test.tsx:1195-1200).

Nits (non-blocking).

  • audioFxProfileStrength inverts on derives[0] only. For compressor that's threshold; hand-edited chains where the author sets threshold=-30 but leaves ratio/makeup at defaults will read a strength inconsistent with the mechanism. Documented at audioFxProfiles.ts:363-376 as the honest answer — noting for reviewer awareness, not blocking.
  • to2 rounding on range: -10 - s*20 at s=0 yields -10 (inside [-40, -3] presumably); range checked by test, so safe.

CI. Preflight (lint + format) FAILURE ×3 = the recurring stack-wide oxfmt --check drift: identical 9-file list (README.md, 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, skills/hyperframes-audio/SKILL.md, .../attributes.md, .../fx-registry.md) fails on parent PR #3187 (wa-20d3-schematic) with byte-identical output — pre-existing, not introduced here. Downstream player-perf / preview-regression / regression are the rollup-of-skipped shards fed by the Preflight failure. Not this PR.

— 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: one knob for the five effects that cannot honestly have one — #3188

Verdict: LGTM

All five profile mappings (compressor, gate, saturate, reverb, bitcrush) are DSP-correct with measurement-backed corrections to the original design proposal:

  • Compressor — quadratic makeup gain (s*s*9.5) compensates for accelerating gain reduction as threshold drops AND ratio rises simultaneously. The linear 1/3/7 dB proposal left the track -2.5 dB at full strength.
  • Gate — release was missing from the original proposal. Measured: "swept against a fixed threshold and range, the gaps move 0.2 dB at 140 ms and 13.4 dB at 10 ms." Adding release to the profile is the correct fix.
  • Saturate — output goes UP, reversing the design's figure. A soft clipper at -18 dB IS a limiter; peak fell from 0.496 to 0.089, making "warmer" mean "much quieter."
  • Reverb — piecewise linear for size to hit design anchors (0.55 ≠ midpoint of 0.25/0.90). Wet/dry don't sum equally because matching them makes a big room quieter.
  • Bitcrush — bits go DOWN as strength goes UP. Math.round for the integer samples parameter. This is exactly why bits can't be the module's face (the knob would go backwards).

Test coverage is thorough: bidirectional structural invariant, range bounds (0-1 sweep in 0.05 steps, all within registry min/max), monotonicity (every derived param moves in one direction), non-clobbering (preserves knee/mix), round-trip (apply → read back ≈ original), NaN handling (clamp01 returns 0.5).

The derives[0] position is implicitly the inversion key for readback — a one-line comment naming this convention would protect against accidental reordering.


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-20d3-schematic to main August 13, 2026 18:38
@vanceingalls
vanceingalls merged commit 29f516d into main Aug 13, 2026
56 of 57 checks passed
@vanceingalls
vanceingalls deleted the wa-20d4-profiles branch August 13, 2026 19:07
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