feat: add Convolver IR, Limiter curve, and upgraded EQ3 visualizations#1417
feat: add Convolver IR, Limiter curve, and upgraded EQ3 visualizations#1417
Conversation
#1241) Implements remaining per-effect signature visualizations: - ConvolverIRCurve: bipolar IR waveform with ER/tail markers, factory IR type badges - LimiterCurve: input/output transfer function with ceiling, GR shading, soft knee - EQ3Curve: upgraded from 150×40 to 200×80px with log freq grid, dB grid, crossovers Includes utility functions (convolverIR.ts, limiterCurve.ts) with 18 unit tests. Uses EFFECT_COLORS constants instead of hardcoded values. Closes #1241 https://claude.ai/code/session_01EeAnf2HpreRaQwsixZuRRE
There was a problem hiding this comment.
Pull request overview
Adds new limiter and convolver visualizations plus upgrades the EQ3 curve rendering to support the “per-effect signature visualizations” initiative (Issue #1241), backed by new math utilities and unit tests.
Changes:
- Introduces pure math generators for limiter transfer curves and convolver IR envelopes (+ unit tests).
- Adds new canvas visual components:
LimiterCurveandConvolverIRCurve, and wires them into the effect cards. - Upgrades EQ3 visualization (larger canvas, log-frequency grid, dB grid, crossover markers, gradient fill, color prop).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/limiterCurve.ts | New math utility to generate limiter transfer curve points (with style-based knee). |
| src/utils/convolverIR.ts | New math utility to generate synthetic IR envelope + ER spike positions/markers. |
| src/utils/tests/limiterCurve.test.ts | Unit tests covering limiter curve invariants and style behavior. |
| src/utils/tests/convolverIR.test.ts | Unit tests for IR envelope generation, ER spikes, and boundary/length helpers. |
| src/components/mixer/LimiterCurve.tsx | New canvas visualization for limiter transfer curve + GR region rendering. |
| src/components/mixer/ConvolverIRCurve.tsx | New canvas visualization for convolver IR waveform with ER/tail markers and pre-delay shading. |
| src/components/mixer/EffectCards.tsx | Wires new visualizations into Limiter/Convolver cards and upgrades EQ3 curve rendering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function generateIREnvelope( | ||
| irType: FactoryIRType, | ||
| preDelay: number, | ||
| /** Number of intervals; returns steps + 1 points including both endpoints */ | ||
| steps: number = 160, | ||
| ): IREnvelopePoint[] { | ||
| const profile = IR_PROFILES[irType]; | ||
| const preDelayS = preDelay / 1000; // ms → seconds | ||
| const totalLength = preDelayS + profile.length; |
There was a problem hiding this comment.
The generateIREnvelope/getIRReflections APIs treat preDelay as milliseconds (they immediately divide by 1000), but the JSDoc for generateIREnvelope doesn’t document the unit. Please clarify the unit in the function docs/param description to prevent callers from accidentally passing seconds (this file’s utils are otherwise unit-explicit in comments).
src/components/mixer/EffectCards.tsx
Outdated
| // Background | ||
| const grad = ctx.createRadialGradient(W / 2, drawH / 2, 0, W / 2, drawH / 2, W * 0.6); | ||
| grad.addColorStop(0, 'rgba(12, 16, 28, 0.92)'); | ||
| grad.addColorStop(1, 'rgba(4, 6, 14, 0.98)'); | ||
| ctx.fillStyle = grad; | ||
| ctx.fillRect(0, 0, W, H); | ||
|
|
||
| // Label area | ||
| ctx.fillStyle = 'rgba(0, 0, 0, 0.22)'; | ||
| ctx.fillRect(0, drawH, W, labelH); |
There was a problem hiding this comment.
This EQ3 visualization reintroduces hardcoded background/label colors (rgba(12, 16, 28, …), rgba(0, 0, 0, 0.22)) instead of using the shared canvas theme helpers/constants (e.g., fillBackground, LABEL_AREA_BG from src/utils/canvasTheme.ts). For consistency with other mixer visualizations (e.g., FilterResponseCurve.tsx / ReverbDecayCurve.tsx), consider reusing the shared theme so global styling tweaks don’t require editing this component.
| export function LimiterCurve({ | ||
| ceiling, | ||
| gain, | ||
| style, | ||
| width = 160, | ||
| height = 100, | ||
| color = '#d4a040', | ||
| }: LimiterCurveProps) { |
There was a problem hiding this comment.
The default color prop is hardcoded to #d4a040, which duplicates EFFECT_COLORS.limiter and contradicts the PR description’s “no hardcoded colors” goal. Consider making color required (since callers already pass EFFECT_COLORS.limiter) or centralizing effect colors in a non-EffectCards module that visual components can import without creating circular dependencies.
| export function ConvolverIRCurve({ | ||
| irType, | ||
| preDelay, | ||
| wet, | ||
| width = 160, | ||
| height = 100, | ||
| color = '#a07cc8', | ||
| }: ConvolverIRCurveProps) { |
There was a problem hiding this comment.
The default color prop is hardcoded to #a07cc8, duplicating EFFECT_COLORS.convolver and conflicting with the PR description’s “no hardcoded colors” goal. Consider making color required (since callers pass EFFECT_COLORS.convolver) or moving effect color constants to a dedicated module that both EffectCards and visualization components can import.
| // Find a point near ceiling where they differ | ||
| const nearCeiling = warm.findIndex((p) => p.inputDb + 6 > -1); | ||
| if (nearCeiling > 0 && nearCeiling < warm.length - 1) { | ||
| // Warm should output slightly higher (less aggressive limiting) just below ceiling | ||
| const wVal = warm[nearCeiling - 2].outputDb; | ||
| const aVal = aggressive[nearCeiling - 2].outputDb; | ||
| // Both should be similar but warm transitions more gently | ||
| expect(Math.abs(wVal - aVal)).toBeLessThan(5); | ||
| } |
There was a problem hiding this comment.
This test claims to verify that “warm style has softer knee than aggressive”, but the assertion only checks that the two values are within 5 dB of each other, which would still pass even if the knee behavior regressed or inverted. Consider asserting an explicit ordering/relationship at a point inside the knee region (e.g., warm output should be higher than aggressive for the same input near the ceiling) so the test actually protects the intended behavior.
| // Find a point near ceiling where they differ | |
| const nearCeiling = warm.findIndex((p) => p.inputDb + 6 > -1); | |
| if (nearCeiling > 0 && nearCeiling < warm.length - 1) { | |
| // Warm should output slightly higher (less aggressive limiting) just below ceiling | |
| const wVal = warm[nearCeiling - 2].outputDb; | |
| const aVal = aggressive[nearCeiling - 2].outputDb; | |
| // Both should be similar but warm transitions more gently | |
| expect(Math.abs(wVal - aVal)).toBeLessThan(5); | |
| } | |
| // Find a point inside the knee region, just before hitting the ceiling | |
| const nearCeiling = warm.findIndex((p) => p.inputDb + 6 > -1); | |
| expect(nearCeiling).toBeGreaterThan(1); | |
| expect(nearCeiling).toBeLessThan(warm.length); | |
| // Warm should output higher (less aggressive limiting) than aggressive | |
| // at the same input just below the ceiling. | |
| const kneeIdx = nearCeiling - 2; | |
| const wVal = warm[kneeIdx].outputDb; | |
| const aVal = aggressive[kneeIdx].outputDb; | |
| expect(wVal).toBeGreaterThan(aVal); |
- Add preDelay unit (ms) to convolverIR.ts JSDoc - Make color prop required on LimiterCurve and ConvolverIRCurve (no hardcoded defaults) - Use shared canvasTheme helpers (drawCanvasBackground, LABEL_AREA_BG) in EQ3Curve - Strengthen limiter knee test to assert warm > aggressive output ordering https://claude.ai/code/session_01EeAnf2HpreRaQwsixZuRRE
Summary
EFFECT_COLORSconstants throughout (no hardcoded colors)18 new unit tests for utility functions (convolverIR.ts, limiterCurve.ts).
Quality Gates
npx tsc --noEmit— 0 errorsnpm test— 4025 passed (18 new)Test plan
Closes #1241
https://claude.ai/code/session_01EeAnf2HpreRaQwsixZuRRE