fix(core): stop reading a blank FX parameter as zero

`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) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-08-08 12:13:25 -07:00
co-authored by Claude Opus 5
parent eb60133e33
commit eb0080dec8
2 changed files with 35 additions and 1 deletions
+21
View File
@@ -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");
+14 -1
View File
@@ -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;