From eb0080dec87429cc5581161d432bc81a472c2816 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sat, 8 Aug 2026 12:13:25 -0700 Subject: [PATCH] fix(core): stop reading a blank FX parameter as zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `normalizeAudioFxParams` coerced with a bare `Number(raw)`, and `Number(null)`, `Number("")`, `Number(false)` and `Number([])` are all 0 — every one of them finite. So a parameter that arrived missing or blanked clamped to 0 instead of falling back to the effect's declared default, and because 0 is a legal setting for most of these knobs nothing downstream could tell the difference: a compressor whose threshold came through as null sat at 0 dB and never engaged. Now only a number, or a string that actually spells one, counts — the same rule `numberOrNull` in audioAutomation.ts already applies. Panel inputs arrive as strings, so those still work. Falsified by restoring the bare `Number(raw)`: null reads as 0 dB instead of -24. Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/src/audioFx.test.ts | 21 +++++++++++++++++++++ packages/core/src/audioFx.ts | 15 ++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/core/src/audioFx.test.ts b/packages/core/src/audioFx.test.ts index e59819b952..3caaa2464c 100644 --- a/packages/core/src/audioFx.test.ts +++ b/packages/core/src/audioFx.test.ts @@ -69,6 +69,27 @@ describe("normalizeAudioFxParams", () => { expect(v.gain).toBe(0); }); + it("treats a blank or missing value as absent rather than as zero", () => { + // `Number(null)`, `Number("")`, `Number(false)` and `Number([])` are all 0 + // and all finite, so these used to clamp to 0 instead of falling back. Zero + // is a legal setting for most of these knobs, so nothing downstream could + // tell: a compressor whose threshold arrived as null sat at 0 dB and never + // engaged, silently, rather than at its declared -24 dB. + const def = defaultAudioFxParams("compressor").threshold; + expect(def).not.toBe(0); + for (const blank of [null, undefined, "", " ", false, [], {}]) { + expect( + normalizeAudioFxParams("compressor", { threshold: blank as unknown as number }).threshold, + `${JSON.stringify(blank)} was read as a number`, + ).toBe(def); + } + // A string that really does spell a number still counts — that is how the + // panel's inputs arrive. + expect( + normalizeAudioFxParams("compressor", { threshold: "-30" as unknown as number }).threshold, + ).toBe(-30); + }); + it("falls back to the default for an unrecognised enum value", () => { expect(normalizeAudioFxParams("saturate", { type: "sawtooth" }).type).toBe("tanh"); expect(normalizeAudioFxParams("saturate", { type: "atan" }).type).toBe("atan"); diff --git a/packages/core/src/audioFx.ts b/packages/core/src/audioFx.ts index 1e38c4f298..32be62bc64 100644 --- a/packages/core/src/audioFx.ts +++ b/packages/core/src/audioFx.ts @@ -761,7 +761,20 @@ export function normalizeAudioFxParams( out[p.key] = ok ? (raw as string) : p.default; continue; } - const n = typeof raw === "number" ? raw : Number(raw); + // Only a number, or a string that actually spells one. `Number(null)`, + // `Number("")`, `Number(false)` and `Number([])` are all 0 and all pass + // Number.isFinite, so a missing or blanked value used to clamp to 0 rather + // than fall back to the declared default — and 0 is a legal value for most + // of these knobs, so nothing downstream could tell. A compressor whose + // threshold arrived as null sat at 0 dB and never engaged, silently, + // instead of at its -24 dB default. `numberOrNull` in audioAutomation.ts + // already guards exactly this. + const n = + typeof raw === "number" + ? raw + : typeof raw === "string" && raw.trim() !== "" + ? Number(raw) + : Number.NaN; out[p.key] = Number.isFinite(n) ? Math.min(p.max, Math.max(p.min, n)) : p.default; } return out;