Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/src/audioAutomation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export interface AutomationRange {
unit: string;
label: string;
scale: "linear" | "log";
/** Where an empty lane draws its flat line, and what a new point starts at. */
default: number;
}

export const VOLUME_RANGE: AutomationRange = {
Expand All @@ -97,6 +99,7 @@ export const VOLUME_RANGE: AutomationRange = {
unit: "",
label: "Volume",
scale: "linear",
default: 1,
};

/**
Expand Down Expand Up @@ -124,6 +127,7 @@ export function resolveAutomationRange(
unit: p.unit,
label: `${def?.label ?? node.type} · ${p.label}`,
scale: p.scale === "log" && p.min > 0 ? "log" : "linear",
default: p.default,
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/studio/src/components/StudioRightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export function StudioRightPanel({
handleDomStyleCommit,
handleDomAttributeCommit,
handleDomAttributeLiveCommit,
handleDomAttributeQuietCommit,
handleDomHtmlAttributeCommit,
handleDomAttributesCommit,
handleDomPathOffsetCommit,
Expand Down Expand Up @@ -361,6 +362,7 @@ export function StudioRightPanel({
onSetAttribute={handleDomAttributeCommit}
onSetAttributes={handleDomAttributesCommit}
onSetAttributeLive={handleDomAttributeLiveCommit}
onSetAttributeQuiet={handleDomAttributeQuietCommit}
onApplyColorGradingScope={handleApplyColorGradingScope}
onSetHtmlAttribute={handleDomHtmlAttributeCommit}
onRemoveBackground={handleRemoveBackground}
Expand Down
133 changes: 8 additions & 125 deletions packages/studio/src/components/editor/PropertyPanelFlat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,11 @@ import { FlatTextSection } from "./propertyPanelFlatTextSection";
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
import { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
import { FlatMotionSection } from "./propertyPanelFlatMotionSection";
import {
HF_AUDIO_FX_ATTR,
parseAudioFxChain,
serializeAudioFxChain,
type HfAudioFxChain,
} from "@hyperframes/core/audio-fx";
import {
analyseCarveBands,
carveBandsToChain,
HF_AUDIO_CARVE_ATTR,
normalizeCarveSettings,
type HfCarveSettings,
} from "@hyperframes/core/audio-carve";
import { parseAudioFxChain } from "@hyperframes/core/audio-fx";
import { AudioFxGroup } from "./propertyPanelAudioFxGroup.js";
import { useVolumeAutomation } from "./useVolumeAutomation";
import { FlatMediaSection } from "./propertyPanelFlatMediaSection";
import type { DomEditSelection } from "./domEditing";
import { FxSection, type AudioTrackOption } from "./propertyPanelFxSection";
import { deriveElementTiming } from "./propertyPanelFlatTimingDerivation";
import { createGsapLivePreview } from "./gsapLivePreview";
import { formatTextFieldPreview } from "./propertyPanelSections";
Expand Down Expand Up @@ -79,6 +68,7 @@ export function PropertyPanelFlat({
onSetAttribute,
onSetAttributes,
onSetAttributeLive,
onSetAttributeQuiet,
onApplyColorGradingScope,
onSetHtmlAttribute,
onRemoveBackground,
Expand Down Expand Up @@ -267,6 +257,8 @@ export function PropertyPanelFlat({
const showMotionEffects = gsapEffectHandlers !== null;
const showMotionGroup = showMotionTiming || showMotionEffects;

const volumeAutomation = useVolumeAutomation(element, onSetAttribute);

const groups: FlatGroupDescriptor[] = [];
if (isTextEditable) {
groups.push({
Expand Down Expand Up @@ -443,8 +435,7 @@ export function PropertyPanelFlat({
content: (
<AudioFxGroup
element={element}
onSetAttribute={onSetAttribute}
onSetAttributeLive={onSetAttributeLive}
onSetAttributeQuiet={onSetAttributeQuiet ?? onSetAttributeLive}
/>
),
});
Expand All @@ -463,6 +454,7 @@ export function PropertyPanelFlat({
onSetAttribute={onSetAttribute}
onSetHtmlAttribute={onSetHtmlAttribute}
onRemoveBackground={onRemoveBackground}
{...volumeAutomation}
/>
),
});
Expand Down Expand Up @@ -561,112 +553,3 @@ function audioFxSummary(element: DomEditSelection): string {
if (carve) parts.push("carve");
return parts.length > 0 ? parts.join(" + ") : "none";
}

/**
* Bridges the FX panel to the element/attribute world. Chain and carve are
* serialised onto the element the way colour grading carries its config, so
* persistence is an ordinary attribute write with no new server route.
*/
function AudioFxGroup({
element,
onSetAttribute,
onSetAttributeLive,
}: {
element: DomEditSelection;
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
onSetAttributeLive: (attr: string, value: string | null) => void | Promise<void>;
}) {
const chain = ((): HfAudioFxChain => {
const raw = element.dataAttributes?.["fx-chain"];
if (!raw) return { version: 1, nodes: [] };
try {
return parseAudioFxChain(raw);
} catch {
// Show an unreadable chain as empty rather than breaking the panel; the
// attribute is left untouched until the user changes something.
return { version: 1, nodes: [] };
}
})();

const carve = ((): HfCarveSettings | null => {
const raw = element.dataAttributes?.["fx-carve"];
if (!raw) return null;
try {
return normalizeCarveSettings(JSON.parse(raw));
} catch {
return null;
}
})();

const sourceOptions: AudioTrackOption[] = (() => {
const doc = element.element?.ownerDocument;
if (!doc) return [];
return Array.from(doc.querySelectorAll<HTMLAudioElement>("audio[id]"))
.filter((a) => a.id !== element.id)
.map((a) => ({ id: a.id, label: a.id }));
})();

const [analysing, setAnalysing] = useState(false);

/**
* Decodes the chosen voice track and turns its spectrum into peaking filters
* on this one. The bands replace any previous carve output but leave
* hand-added effects alone, so re-analysing does not discard other work.
*/
const analyse = async (): Promise<void> => {
if (!carve?.source) return;
const doc = element.element?.ownerDocument;
const voice = doc?.getElementById(carve.source) as HTMLAudioElement | null;
const src = voice?.getAttribute("src");
if (!src) return;
setAnalysing(true);
try {
const res = await fetch(new URL(src, doc!.baseURI).href);
const bytes = await res.arrayBuffer();
const Ctor =
window.AudioContext ??
(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (!Ctor) return;
const ctx = new Ctor();
try {
const buffer = await ctx.decodeAudioData(bytes);
const bands = analyseCarveBands(buffer.getChannelData(0), buffer.sampleRate, carve);
const carved = carveBandsToChain(bands);
// Carve output is tagged so a re-run replaces it instead of stacking.
const kept = chain.nodes.filter((n) => !n.fromCarve);
const next = {
version: 1,
nodes: [...carved.nodes.map((n) => ({ ...n, fromCarve: true })), ...kept],
};
onSetAttribute(HF_AUDIO_FX_ATTR, serializeAudioFxChain(next));
} finally {
void ctx.close().catch(() => undefined);
}
} catch {
// Leave the chain as it was; the button simply re-enables.
} finally {
setAnalysing(false);
}
};

return (
<FxSection
chain={chain}
onChainChange={(next) =>
onSetAttribute(HF_AUDIO_FX_ATTR, next.nodes.length ? serializeAudioFxChain(next) : "")
}
onChainPreview={(next) =>
// Live writes skip the preview refresh, so dragging a knob no longer
// reloads the composition and restarts playback on every pixel.
onSetAttributeLive(HF_AUDIO_FX_ATTR, next.nodes.length ? serializeAudioFxChain(next) : null)
}
carve={carve}
onCarveChange={(next) =>
onSetAttribute(HF_AUDIO_CARVE_ATTR, next ? JSON.stringify(next) : "")
}
sourceOptions={sourceOptions}
onAnalyseCarve={() => void analyse()}
analysing={analysing}
/>
);
}
Loading
Loading