Skip to content

Commit c2846a9

Browse files
committed
refactor(studio): lift the audio FX group out of PropertyPanelFlat
`PropertyPanelFlat.tsx` was 672 lines against the repo's 600-line cap, so the required File size check was red — the sole reason #3014 and #3022 are blocked. Both reviews say the same thing: "mechanical fix, not a design problem. Code itself is LGTM." Moves `AudioFxGroup` and `audioFxSummary` into `propertyPanelAudioFxGroup.tsx`, which is where a later branch puts them anyway — done here so the file is under the cap from the point it first crosses it, rather than ten branches later. 533 lines now. The four audio imports it no longer needs go with it. Not fixed here: three `FxSection carve` tests fail on this branch with "Cannot read properties of undefined (reading 'toFixed')". Confirmed pre-existing by stashing this change and re-running — that is the separate `Test` failure the review also flags.
1 parent cfafa22 commit c2846a9

2 files changed

Lines changed: 134 additions & 124 deletions

File tree

‎packages/studio/src/components/editor/PropertyPanelFlat.tsx‎

Lines changed: 1 addition & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,10 @@ import { FlatTextSection } from "./propertyPanelFlatTextSection";
1313
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
1414
import { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
1515
import { FlatMotionSection } from "./propertyPanelFlatMotionSection";
16-
import {
17-
HF_AUDIO_FX_ATTR,
18-
parseAudioFxChain,
19-
serializeAudioFxChain,
20-
type HfAudioFxChain,
21-
} from "@hyperframes/core/audio-fx";
22-
import {
23-
analyseCarveBands,
24-
carveBandsToChain,
25-
HF_AUDIO_CARVE_ATTR,
26-
normalizeCarveSettings,
27-
type HfCarveSettings,
28-
} from "@hyperframes/core/audio-carve";
2916
import { isCanaryEnabled } from "../../telemetry/canary";
3017
import { audioFxSummary } from "./audioFxSummary";
18+
import { AudioFxGroup } from "./propertyPanelAudioFxGroup";
3119
import { FlatMediaSection } from "./propertyPanelFlatMediaSection";
32-
import type { DomEditSelection } from "./domEditing";
33-
import { FxSection, type AudioTrackOption } from "./propertyPanelFxSection";
3420
import { deriveElementTiming } from "./propertyPanelFlatTimingDerivation";
3521
import { createGsapLivePreview } from "./gsapLivePreview";
3622
import { formatTextFieldPreview } from "./propertyPanelSections";
@@ -548,112 +534,3 @@ export function PropertyPanelFlat({
548534
</DesignPanelInputProvider>
549535
);
550536
}
551-
552-
/**
553-
* Bridges the FX panel to the element/attribute world. Chain and carve are
554-
* serialised onto the element the way colour grading carries its config, so
555-
* persistence is an ordinary attribute write with no new server route.
556-
*/
557-
function AudioFxGroup({
558-
element,
559-
onSetAttribute,
560-
onSetAttributeLive,
561-
}: {
562-
element: DomEditSelection;
563-
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
564-
onSetAttributeLive: (attr: string, value: string | null) => void | Promise<void>;
565-
}) {
566-
const chain = ((): HfAudioFxChain => {
567-
const raw = element.dataAttributes?.["fx-chain"];
568-
if (!raw) return { version: 1, nodes: [] };
569-
try {
570-
return parseAudioFxChain(raw);
571-
} catch {
572-
// Show an unreadable chain as empty rather than breaking the panel; the
573-
// attribute is left untouched until the user changes something.
574-
return { version: 1, nodes: [] };
575-
}
576-
})();
577-
578-
const carve = ((): HfCarveSettings | null => {
579-
const raw = element.dataAttributes?.["fx-carve"];
580-
if (!raw) return null;
581-
try {
582-
return normalizeCarveSettings(JSON.parse(raw));
583-
} catch {
584-
return null;
585-
}
586-
})();
587-
588-
const sourceOptions: AudioTrackOption[] = (() => {
589-
const doc = element.element?.ownerDocument;
590-
if (!doc) return [];
591-
return Array.from(doc.querySelectorAll<HTMLAudioElement>("audio[id]"))
592-
.filter((a) => a.id !== element.id)
593-
.map((a) => ({ id: a.id, label: a.id }));
594-
})();
595-
596-
const [analysing, setAnalysing] = useState(false);
597-
598-
/**
599-
* Decodes the chosen voice track and turns its spectrum into peaking filters
600-
* on this one. The bands replace any previous carve output but leave
601-
* hand-added effects alone, so re-analysing does not discard other work.
602-
*/
603-
const analyse = async (): Promise<void> => {
604-
if (!carve?.source) return;
605-
const doc = element.element?.ownerDocument;
606-
const voice = doc?.getElementById(carve.source) as HTMLAudioElement | null;
607-
const src = voice?.getAttribute("src");
608-
if (!src) return;
609-
setAnalysing(true);
610-
try {
611-
const res = await fetch(new URL(src, doc!.baseURI).href);
612-
const bytes = await res.arrayBuffer();
613-
const Ctor =
614-
window.AudioContext ??
615-
(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
616-
if (!Ctor) return;
617-
const ctx = new Ctor();
618-
try {
619-
const buffer = await ctx.decodeAudioData(bytes);
620-
const bands = analyseCarveBands(buffer.getChannelData(0), buffer.sampleRate, carve);
621-
const carved = carveBandsToChain(bands);
622-
// Carve output is tagged so a re-run replaces it instead of stacking.
623-
const kept = chain.nodes.filter((n) => !n.fromCarve);
624-
const next = {
625-
version: 1,
626-
nodes: [...carved.nodes.map((n) => ({ ...n, fromCarve: true })), ...kept],
627-
};
628-
onSetAttribute(HF_AUDIO_FX_ATTR, serializeAudioFxChain(next));
629-
} finally {
630-
void ctx.close().catch(() => undefined);
631-
}
632-
} catch {
633-
// Leave the chain as it was; the button simply re-enables.
634-
} finally {
635-
setAnalysing(false);
636-
}
637-
};
638-
639-
return (
640-
<FxSection
641-
chain={chain}
642-
onChainChange={(next) =>
643-
onSetAttribute(HF_AUDIO_FX_ATTR, next.nodes.length ? serializeAudioFxChain(next) : "")
644-
}
645-
onChainPreview={(next) =>
646-
// Live writes skip the preview refresh, so dragging a knob no longer
647-
// reloads the composition and restarts playback on every pixel.
648-
onSetAttributeLive(HF_AUDIO_FX_ATTR, next.nodes.length ? serializeAudioFxChain(next) : null)
649-
}
650-
carve={carve}
651-
onCarveChange={(next) =>
652-
onSetAttribute(HF_AUDIO_CARVE_ATTR, next ? JSON.stringify(next) : "")
653-
}
654-
sourceOptions={sourceOptions}
655-
onAnalyseCarve={() => void analyse()}
656-
analysing={analysing}
657-
/>
658-
);
659-
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* The audio FX panel's bridge to the element/attribute world.
3+
*
4+
* Chain and carve are serialised onto the element the way colour grading carries
5+
* its config, so persistence is an ordinary attribute write with no new server
6+
* route. Split out of PropertyPanelFlat, which is at its size budget.
7+
*/
8+
9+
import { useState } from "react";
10+
import {
11+
HF_AUDIO_FX_ATTR,
12+
parseAudioFxChain,
13+
serializeAudioFxChain,
14+
type HfAudioFxChain,
15+
} from "@hyperframes/core/audio-fx";
16+
import {
17+
analyseCarveBands,
18+
carveBandsToChain,
19+
HF_AUDIO_CARVE_ATTR,
20+
normalizeCarveSettings,
21+
type HfCarveSettings,
22+
} from "@hyperframes/core/audio-carve";
23+
import type { DomEditSelection } from "./domEditing";
24+
import { FxSection, type AudioTrackOption } from "./propertyPanelFxSection";
25+
26+
/**
27+
* Bridges the FX panel to the element/attribute world. Chain and carve are
28+
* serialised onto the element the way colour grading carries its config, so
29+
* persistence is an ordinary attribute write with no new server route.
30+
*/
31+
export function AudioFxGroup({
32+
element,
33+
onSetAttribute,
34+
onSetAttributeLive,
35+
}: {
36+
element: DomEditSelection;
37+
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
38+
onSetAttributeLive: (attr: string, value: string | null) => void | Promise<void>;
39+
}) {
40+
const chain = ((): HfAudioFxChain => {
41+
const raw = element.dataAttributes?.["fx-chain"];
42+
if (!raw) return { version: 1, nodes: [] };
43+
try {
44+
return parseAudioFxChain(raw);
45+
} catch {
46+
// Show an unreadable chain as empty rather than breaking the panel; the
47+
// attribute is left untouched until the user changes something.
48+
return { version: 1, nodes: [] };
49+
}
50+
})();
51+
52+
const carve = ((): HfCarveSettings | null => {
53+
const raw = element.dataAttributes?.["fx-carve"];
54+
if (!raw) return null;
55+
try {
56+
return normalizeCarveSettings(JSON.parse(raw));
57+
} catch {
58+
return null;
59+
}
60+
})();
61+
62+
const sourceOptions: AudioTrackOption[] = (() => {
63+
const doc = element.element?.ownerDocument;
64+
if (!doc) return [];
65+
return Array.from(doc.querySelectorAll<HTMLAudioElement>("audio[id]"))
66+
.filter((a) => a.id !== element.id)
67+
.map((a) => ({ id: a.id, label: a.id }));
68+
})();
69+
70+
const [analysing, setAnalysing] = useState(false);
71+
72+
/**
73+
* Decodes the chosen voice track and turns its spectrum into peaking filters
74+
* on this one. The bands replace any previous carve output but leave
75+
* hand-added effects alone, so re-analysing does not discard other work.
76+
*/
77+
const analyse = async (): Promise<void> => {
78+
if (!carve?.source) return;
79+
const doc = element.element?.ownerDocument;
80+
const voice = doc?.getElementById(carve.source) as HTMLAudioElement | null;
81+
const src = voice?.getAttribute("src");
82+
if (!src) return;
83+
setAnalysing(true);
84+
try {
85+
const res = await fetch(new URL(src, doc!.baseURI).href);
86+
const bytes = await res.arrayBuffer();
87+
const Ctor =
88+
window.AudioContext ??
89+
(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
90+
if (!Ctor) return;
91+
const ctx = new Ctor();
92+
try {
93+
const buffer = await ctx.decodeAudioData(bytes);
94+
const bands = analyseCarveBands(buffer.getChannelData(0), buffer.sampleRate, carve);
95+
const carved = carveBandsToChain(bands);
96+
// Carve output is tagged so a re-run replaces it instead of stacking.
97+
const kept = chain.nodes.filter((n) => !n.fromCarve);
98+
const next = {
99+
version: 1,
100+
nodes: [...carved.nodes.map((n) => ({ ...n, fromCarve: true })), ...kept],
101+
};
102+
onSetAttribute(HF_AUDIO_FX_ATTR, serializeAudioFxChain(next));
103+
} finally {
104+
void ctx.close().catch(() => undefined);
105+
}
106+
} catch {
107+
// Leave the chain as it was; the button simply re-enables.
108+
} finally {
109+
setAnalysing(false);
110+
}
111+
};
112+
113+
return (
114+
<FxSection
115+
chain={chain}
116+
onChainChange={(next) =>
117+
onSetAttribute(HF_AUDIO_FX_ATTR, next.nodes.length ? serializeAudioFxChain(next) : "")
118+
}
119+
onChainPreview={(next) =>
120+
// Live writes skip the preview refresh, so dragging a knob no longer
121+
// reloads the composition and restarts playback on every pixel.
122+
onSetAttributeLive(HF_AUDIO_FX_ATTR, next.nodes.length ? serializeAudioFxChain(next) : null)
123+
}
124+
carve={carve}
125+
onCarveChange={(next) =>
126+
onSetAttribute(HF_AUDIO_CARVE_ATTR, next ? JSON.stringify(next) : "")
127+
}
128+
sourceOptions={sourceOptions}
129+
onAnalyseCarve={() => void analyse()}
130+
analysing={analysing}
131+
/>
132+
);
133+
}

0 commit comments

Comments
 (0)