refactor: drop the non-null assertion and two casts the guards already cover

Three violations of the repo's "avoid `any`/`as T`" rule and the standing
no-`!` rule, all in the carve's analysis path.

`resolveAutomationRange` cast to `HfAudioFxNumberParam` immediately after
`if (!param || param.kind !== "number") return null` — the discriminated union
was already narrowed, so the cast asserted what the compiler knew. Removing it
leaves tsc clean, which is the proof it was dead.

`analyse` read `doc!.baseURI` and `el.getAttribute("src")!`, and laundered
`getElementById`'s `HTMLElement | null` through `as HTMLAudioElement | null`
into a predicate that only checked for a `src` attribute — so the "is this an
audio element" claim was never actually tested by anything. The document is now
guarded once at the top, and the voices are read out to `{ src, start }` values
as they are found, which makes both fields non-null by construction rather than
by assertion.

The element check is by `tagName`, not `instanceof HTMLAudioElement`: these
elements belong to the composition's iframe document, so this realm's
constructor never matches them. It is not a narrowing either — `sourceOptions`
is built from `doc.querySelectorAll("audio[id]")`, so a carve source is an
`<audio>` element by construction.

No behaviour change, so no new test. The path is covered: forcing the voice
collection to yield nothing fails 8 existing tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-08-08 12:48:43 -07:00
co-authored by Claude Opus 5
parent c67f254cd3
commit ac0e372474
2 changed files with 28 additions and 16 deletions
+8 -9
View File
@@ -11,7 +11,7 @@
* curve, three consumers, or the picture and the sound disagree.
*/
import { getAudioFxDef, type HfAudioFxChain, type HfAudioFxNumberParam } from "./audioFx.js";
import { getAudioFxDef, type HfAudioFxChain } from "./audioFx.js";
export const HF_AUDIO_AUTOMATION_ATTR = "data-automation";
@@ -138,15 +138,14 @@ export function resolveAutomationRange(
const def = getAudioFxDef(node.type);
const param = def?.params.find((p) => p.key === parsed.param);
if (!param || param.kind !== "number") return null;
const p = param as HfAudioFxNumberParam;
return {
min: p.min,
max: p.max,
step: p.step,
unit: p.unit,
label: `${def?.label ?? node.type} · ${p.label}`,
scale: p.scale === "log" && p.min > 0 ? "log" : "linear",
default: p.default,
min: param.min,
max: param.max,
step: param.step,
unit: param.unit,
label: `${def?.label ?? node.type} · ${param.label}`,
scale: param.scale === "log" && param.min > 0 ? "log" : "linear",
default: param.default,
};
}
@@ -481,11 +481,24 @@ export function AudioFxGroup({
const analyse = async (active: HfCarveSettings | null = carve): Promise<void> => {
if (!active?.sources.length) return;
const doc = element.element?.ownerDocument;
if (!doc) return;
// Every named voice that is actually there with something to decode. A source
// naming a deleted track is skipped rather than failing the whole analysis.
const voices = active.sources
.map((id) => doc?.getElementById(id) as HTMLAudioElement | null)
.filter((el): el is HTMLAudioElement => Boolean(el?.getAttribute("src")));
//
// Read out to plain values here rather than carrying elements around: it is
// what lets the src and the start be non-null by construction downstream
// instead of by assertion.
const voices: { src: string; start: string | null }[] = [];
for (const id of active.sources) {
const el = doc.getElementById(id);
// By tag name, not `instanceof HTMLAudioElement`: these elements belong to
// the composition's iframe document, so the constructor they were made
// from is not this realm's and the instanceof is false for every one.
if (el?.tagName !== "AUDIO") continue;
const src = el.getAttribute("src");
if (!src) continue;
voices.push({ src, start: el.getAttribute("data-start") });
}
if (voices.length === 0) return;
setAnalysing(true);
try {
@@ -498,7 +511,7 @@ export function AudioFxGroup({
.webkitOfflineAudioContext;
if (!Ctor) return;
const decode = async (relative: string): Promise<AudioBuffer> => {
const res = await fetch(new URL(relative, doc!.baseURI).href);
const res = await fetch(new URL(relative, doc.baseURI).href);
return new Ctor(1, 1, DECODE_SAMPLE_RATE).decodeAudioData(await res.arrayBuffer());
};
const bedStart = clipStart(element.dataAttributes?.["start"]);
@@ -508,9 +521,9 @@ export function AudioFxGroup({
// is also what lets the bands and the envelopes stay a single set: the chain is
// fixed, so there is no per-voice filter to switch between.
const decoded = await Promise.all(
voices.map(async (el) => ({
samples: (await decode(el.getAttribute("src")!)).getChannelData(0),
offsetSeconds: clipStart(el.getAttribute("data-start")) - bedStart,
voices.map(async (voice) => ({
samples: (await decode(voice.src)).getChannelData(0),
offsetSeconds: clipStart(voice.start) - bedStart,
})),
);
const voiceMix = mixCarveSources(decoded, DECODE_SAMPLE_RATE);