diff --git a/packages/studio/src/components/editor/propertyPanelFxNodeRow.tsx b/packages/studio/src/components/editor/propertyPanelFxNodeRow.tsx new file mode 100644 index 0000000000..613a4ab350 --- /dev/null +++ b/packages/studio/src/components/editor/propertyPanelFxNodeRow.tsx @@ -0,0 +1,259 @@ +/** + * One effect in the FX rack: its header controls, and its knobs when open. + * + * An entry in the chain, as opposed to a composite module — the carve and the + * Tone EQ own several nodes each and have their own files. + */ + +import { + defaultAudioFxParams, + getAudioFxDef, + type HfAudioFxDef, + type HfAudioFxNode, + type HfAudioFxParamValues, +} from "@hyperframes/core/audio-fx"; +import { fxAutomationTarget } from "@hyperframes/core/audio-automation"; +import { FxParams } from "./propertyPanelFxControls.js"; + +interface FxNodeRowProps { + node: HfAudioFxNode; + index: number; + automatedTargets?: ReadonlySet; + liveAutomationValues?: ReadonlyMap; + onAutomateParam?(nodeId: string, paramKey: string): void; + onRemoveParamAutomation?(nodeId: string, paramKey: string): void; + open: boolean; + /** Last in the chain, so it cannot move further down. */ + last: boolean; + disabled?: boolean; + onToggleOpen(): void; + onUpdate(index: number, patch: Partial): void; + onMove(index: number, delta: number): void; + onRemove(index: number): void; + onPreview(index: number, params: HfAudioFxParamValues): void; +} + +/** Reorder arrow. Disabled at the end of the chain it would move past. */ +function FxMoveButton({ + label, + glyph, + disabled, + onClick, +}: { + label: string; + glyph: string; + disabled: boolean; + onClick(): void; +}) { + return ( + + ); +} + +/** Name, bypass, reorder and remove for one effect. */ +function FxNodeHeader({ + label, + open, + bypassed, + first, + last, + disabled, + onToggleOpen, + onToggleBypass, + onMove, + onRemove, +}: { + label: string; + open: boolean; + bypassed: boolean; + first: boolean; + last: boolean; + disabled?: boolean; + onToggleOpen(): void; + onToggleBypass(): void; + onMove(delta: number): void; + onRemove(): void; +}) { + return ( +
+ + + onMove(-1)} + /> + onMove(1)} + /> + +
+ ); +} + +/** + * Which of an effect's knobs already have a lane. + * + * A lane addresses a node by id, so a node the panel has not yet given one + * cannot be automated at all. Adding an effect mints the id, so this only + * affects chains written before ids existed. + */ +function automatedKeysOf( + node: HfAudioFxNode, + params: readonly { key: string }[], + automatedTargets: ReadonlySet | undefined, +): Set { + if (!node.id || !automatedTargets) return new Set(); + const nodeId = node.id; + return new Set( + params.filter((p) => automatedTargets.has(fxAutomationTarget(nodeId, p.key))).map((p) => p.key), + ); +} + +/** An open effect's knobs, with whatever automation surface applies to them. */ +function FxNodeParams({ + node, + def, + index, + disabled, + automatedTargets, + liveAutomationValues, + onUpdate, + onPreview, + onAutomateParam, + onRemoveParamAutomation, +}: { + node: HfAudioFxNode; + def: HfAudioFxDef; + index: number; + disabled: boolean; + automatedTargets?: ReadonlySet; + liveAutomationValues?: ReadonlyMap; + onUpdate(index: number, patch: Partial): void; + onPreview(index: number, params: HfAudioFxParamValues): void; + onAutomateParam?(nodeId: string, paramKey: string): void; + onRemoveParamAutomation?(nodeId: string, paramKey: string): void; +}) { + const nodeId = node.id; + // Lanes address a node by id; the controls know their own parameter keys. This + // is the one place that translation belongs. + const liveValues = ((): Map | undefined => { + if (!nodeId || !liveAutomationValues?.size) return undefined; + const byKey = new Map(); + for (const param of def.params) { + const live = liveAutomationValues.get(fxAutomationTarget(nodeId, param.key)); + if (live !== undefined) byKey.set(param.key, live); + } + return byKey; + })(); + return ( + onPreview(index, params)} + onCommit={(params: HfAudioFxParamValues) => onUpdate(index, { params })} + automatedKeys={automatedKeysOf(node, def.params, automatedTargets)} + onAutomate={nodeId && onAutomateParam ? (key) => onAutomateParam(nodeId, key) : undefined} + onRemoveAutomation={ + nodeId && onRemoveParamAutomation + ? (key) => onRemoveParamAutomation(nodeId, key) + : undefined + } + /> + ); +} + +/** One effect in the chain: its header controls, and its knobs when open. */ +export function FxNodeRow({ + node, + index, + automatedTargets, + liveAutomationValues, + onAutomateParam, + onRemoveParamAutomation, + open, + last, + disabled, + onToggleOpen, + onUpdate, + onMove, + onRemove, + onPreview, +}: FxNodeRowProps) { + const def = getAudioFxDef(node.type); + if (!def) return null; + const bypassed = node.enabled === false; + return ( +
+ onUpdate(index, { enabled: bypassed })} + onMove={(delta) => onMove(index, delta)} + onRemove={() => onRemove(index)} + /> + {open ? ( + + ) : null} +
+ ); +} diff --git a/packages/studio/src/components/editor/propertyPanelFxSection.tsx b/packages/studio/src/components/editor/propertyPanelFxSection.tsx index 69015d5bb2..3414780d8e 100644 --- a/packages/studio/src/components/editor/propertyPanelFxSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFxSection.tsx @@ -8,11 +8,9 @@ import { useCallback, useMemo, useState } from "react"; import { defaultAudioFxParams, - getAudioFxDef, HF_AUDIO_FX, mintAudioFxNodeId, type HfAudioFxChain, - type HfAudioFxDef, type HfAudioFxGroup, type HfAudioFxNode, type HfAudioFxParamValues, @@ -26,11 +24,10 @@ import { removeAudioEq, setAudioEqBandGain, } from "@hyperframes/core/audio-fx-eq"; -import { fxAutomationTarget } from "@hyperframes/core/audio-automation"; -import { FxParams } from "./propertyPanelFxControls.js"; import { FxPresetMenu } from "./propertyPanelFxPresetMenu.js"; import { FxEqModule } from "./propertyPanelFxEqModule.js"; import { FxCarveModule, type AudioTrackOption } from "./propertyPanelFxCarveModule.js"; +import { FxNodeRow } from "./propertyPanelFxNodeRow.js"; export type { AudioTrackOption }; @@ -42,249 +39,6 @@ const GROUP_LABEL: Record = { time: "Time", }; -interface FxNodeRowProps { - node: HfAudioFxNode; - index: number; - automatedTargets?: ReadonlySet; - liveAutomationValues?: ReadonlyMap; - onAutomateParam?(nodeId: string, paramKey: string): void; - onRemoveParamAutomation?(nodeId: string, paramKey: string): void; - open: boolean; - /** Last in the chain, so it cannot move further down. */ - last: boolean; - disabled?: boolean; - onToggleOpen(): void; - onUpdate(index: number, patch: Partial): void; - onMove(index: number, delta: number): void; - onRemove(index: number): void; - onPreview(index: number, params: HfAudioFxParamValues): void; -} - -/** Reorder arrow. Disabled at the end of the chain it would move past. */ -function FxMoveButton({ - label, - glyph, - disabled, - onClick, -}: { - label: string; - glyph: string; - disabled: boolean; - onClick(): void; -}) { - return ( - - ); -} - -/** Name, bypass, reorder and remove for one effect. */ -function FxNodeHeader({ - label, - open, - bypassed, - first, - last, - disabled, - onToggleOpen, - onToggleBypass, - onMove, - onRemove, -}: { - label: string; - open: boolean; - bypassed: boolean; - first: boolean; - last: boolean; - disabled?: boolean; - onToggleOpen(): void; - onToggleBypass(): void; - onMove(delta: number): void; - onRemove(): void; -}) { - return ( -
- - - onMove(-1)} - /> - onMove(1)} - /> - -
- ); -} - -/** - * Which of an effect's knobs already have a lane. - * - * A lane addresses a node by id, so a node the panel has not yet given one - * cannot be automated at all. Adding an effect mints the id, so this only - * affects chains written before ids existed. - */ -function automatedKeysOf( - node: HfAudioFxNode, - params: readonly { key: string }[], - automatedTargets: ReadonlySet | undefined, -): Set { - if (!node.id || !automatedTargets) return new Set(); - const nodeId = node.id; - return new Set( - params.filter((p) => automatedTargets.has(fxAutomationTarget(nodeId, p.key))).map((p) => p.key), - ); -} - -/** An open effect's knobs, with whatever automation surface applies to them. */ -function FxNodeParams({ - node, - def, - index, - disabled, - automatedTargets, - liveAutomationValues, - onUpdate, - onPreview, - onAutomateParam, - onRemoveParamAutomation, -}: { - node: HfAudioFxNode; - def: HfAudioFxDef; - index: number; - disabled: boolean; - automatedTargets?: ReadonlySet; - liveAutomationValues?: ReadonlyMap; - onUpdate(index: number, patch: Partial): void; - onPreview(index: number, params: HfAudioFxParamValues): void; - onAutomateParam?(nodeId: string, paramKey: string): void; - onRemoveParamAutomation?(nodeId: string, paramKey: string): void; -}) { - const nodeId = node.id; - // Lanes address a node by id; the controls know their own parameter keys. This - // is the one place that translation belongs. - const liveValues = ((): Map | undefined => { - if (!nodeId || !liveAutomationValues?.size) return undefined; - const byKey = new Map(); - for (const param of def.params) { - const live = liveAutomationValues.get(fxAutomationTarget(nodeId, param.key)); - if (live !== undefined) byKey.set(param.key, live); - } - return byKey; - })(); - return ( - onPreview(index, params)} - onCommit={(params: HfAudioFxParamValues) => onUpdate(index, { params })} - automatedKeys={automatedKeysOf(node, def.params, automatedTargets)} - onAutomate={nodeId && onAutomateParam ? (key) => onAutomateParam(nodeId, key) : undefined} - onRemoveAutomation={ - nodeId && onRemoveParamAutomation - ? (key) => onRemoveParamAutomation(nodeId, key) - : undefined - } - /> - ); -} - -/** One effect in the chain: its header controls, and its knobs when open. */ -function FxNodeRow({ - node, - index, - automatedTargets, - liveAutomationValues, - onAutomateParam, - onRemoveParamAutomation, - open, - last, - disabled, - onToggleOpen, - onUpdate, - onMove, - onRemove, - onPreview, -}: FxNodeRowProps) { - const def = getAudioFxDef(node.type); - if (!def) return null; - const bypassed = node.enabled === false; - return ( -
- onUpdate(index, { enabled: bypassed })} - onMove={(delta) => onMove(index, delta)} - onRemove={() => onRemove(index)} - /> - {open ? ( - - ) : null} -
- ); -} - export interface FxSectionProps { chain: HfAudioFxChain; /** Targets this track already automates, as `fx..` strings. */