@hyperframes/studio 0.7.107 → 0.7.109
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-BLrfwq5o.js → hyperframes-player-BYgqAPib.js} +1 -1
- package/dist/assets/{index-BSBk5srs.js → index-Blxia-Lt.js} +1 -1
- package/dist/assets/{index-BA9yhzfR.js → index-CIRxodzK.js} +1 -1
- package/dist/assets/index-CfBGQdrX.js +428 -0
- package/dist/assets/index-DNkh9mbV.css +1 -0
- package/dist/index.d.ts +85 -2
- package/dist/index.html +2 -2
- package/dist/index.js +17696 -12512
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/StudioRightPanel.tsx +18 -1
- package/src/components/TimelineToolbar.test.tsx +46 -0
- package/src/components/TimelineToolbar.tsx +17 -0
- package/src/components/editor/PropertyPanel.test.tsx +5 -1
- package/src/components/editor/PropertyPanel.tsx +7 -23
- package/src/components/editor/PropertyPanelFlat.tsx +25 -0
- package/src/components/editor/audioFxSummary.test.ts +75 -0
- package/src/components/editor/audioFxSummary.ts +36 -0
- package/src/components/editor/audioFxTelemetry.test.ts +129 -0
- package/src/components/editor/audioFxTelemetry.ts +230 -0
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +1830 -0
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +299 -0
- package/src/components/editor/propertyPanelAudioFxGroupUtils.ts +21 -0
- package/src/components/editor/propertyPanelAutomation.test.ts +60 -0
- package/src/components/editor/propertyPanelAutomation.ts +97 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +37 -9
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelFxAddMenu.tsx +209 -0
- package/src/components/editor/propertyPanelFxBandRuler.tsx +72 -0
- package/src/components/editor/propertyPanelFxCarveModule.tsx +382 -0
- package/src/components/editor/propertyPanelFxControls.test.tsx +115 -0
- package/src/components/editor/propertyPanelFxControls.tsx +356 -0
- package/src/components/editor/propertyPanelFxEqModule.tsx +196 -0
- package/src/components/editor/propertyPanelFxFamily.ts +83 -0
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +269 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +112 -0
- package/src/components/editor/propertyPanelFxNodeRow.tsx +339 -0
- package/src/components/editor/propertyPanelFxPresetMenu.tsx +159 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +215 -0
- package/src/components/editor/propertyPanelFxPresetStyle.test.ts +142 -0
- package/src/components/editor/propertyPanelFxPresetStyle.ts +216 -0
- package/src/components/editor/propertyPanelFxRackChain.tsx +202 -0
- package/src/components/editor/propertyPanelFxSection.test.tsx +1747 -0
- package/src/components/editor/propertyPanelFxSection.tsx +550 -0
- package/src/components/editor/propertyPanelFxSectionTypes.ts +91 -0
- package/src/components/editor/propertyPanelTypes.ts +5 -0
- package/src/components/editor/useFxAudition.ts +95 -0
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxChainObserved.ts +83 -0
- package/src/components/editor/useFxLevelling.ts +235 -0
- package/src/components/editor/useVolumeAutomation.test.tsx +122 -0
- package/src/components/editor/useVolumeAutomation.ts +54 -0
- package/src/contexts/DomEditContext.tsx +11 -0
- package/src/hooks/useAppHotkeys.test.ts +198 -0
- package/src/hooks/useAppHotkeys.ts +30 -3
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +429 -0
- package/src/hooks/useAutomationSelectionKeyboard.ts +371 -0
- package/src/hooks/useDomEditAttributeCommits.ts +90 -10
- package/src/hooks/useDomEditCommits.test.tsx +38 -0
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -0
- package/src/hooks/useDomEditTextCommits.ts +2 -0
- package/src/hooks/useElementLifecycleOps.ts +3 -0
- package/src/hooks/useLivePlayheadTime.ts +49 -0
- package/src/hooks/usePreviewPersistence.ts +7 -0
- package/src/hooks/useTimelineEditing.ts +3 -0
- package/src/player/components/AutomationSelectionMenu.tsx +76 -0
- package/src/player/components/AutomationValueInput.tsx +51 -0
- package/src/player/components/LayerDisclosureRow.tsx +5 -4
- package/src/player/components/Timeline.test.ts +84 -0
- package/src/player/components/TimelineAutomationLane.test.tsx +1720 -0
- package/src/player/components/TimelineAutomationLane.tsx +674 -0
- package/src/player/components/TimelineAutomationLaneSlot.test.tsx +193 -0
- package/src/player/components/TimelineLanes.test.tsx +24 -5
- package/src/player/components/TimelineLanes.tsx +142 -154
- package/src/player/components/TimelineTrackHeader.test.tsx +212 -1
- package/src/player/components/TimelineTrackHeader.tsx +166 -6
- package/src/player/components/TimelineTrackRow.tsx +8 -1
- package/src/player/components/automationClipboard.test.ts +94 -0
- package/src/player/components/automationClipboard.ts +136 -0
- package/src/player/components/automationGestureKeys.test.ts +49 -0
- package/src/player/components/automationGestureKeys.ts +49 -0
- package/src/player/components/automationLaneData.test.ts +270 -0
- package/src/player/components/automationLaneData.ts +258 -0
- package/src/player/components/automationLaneDragMath.ts +194 -0
- package/src/player/components/automationLaneGeometry.test.ts +239 -0
- package/src/player/components/automationLaneGeometry.ts +287 -0
- package/src/player/components/automationLaneHeight.ts +15 -0
- package/src/player/components/automationLanePointer.ts +20 -0
- package/src/player/components/automationLaneSelection.test.ts +183 -0
- package/src/player/components/automationLaneSelection.ts +135 -0
- package/src/player/components/automationShapes.test.ts +82 -0
- package/src/player/components/automationShapes.ts +70 -0
- package/src/player/components/automationSimplify.test.ts +48 -0
- package/src/player/components/automationSimplify.ts +51 -0
- package/src/player/components/timelineClipGestureHandlers.ts +223 -0
- package/src/player/components/timelineLaneProps.ts +19 -0
- package/src/player/components/timelineLayout.ts +11 -5
- package/src/player/components/useAutomationEdgeStretch.test.ts +27 -0
- package/src/player/components/useAutomationEdgeStretch.ts +269 -0
- package/src/player/components/useAutomationLaneGestures.ts +537 -0
- package/src/player/components/useAutomationLanes.test.tsx +112 -0
- package/src/player/components/useAutomationLanes.ts +170 -0
- package/src/player/components/useAutomationRangeDrag.ts +114 -0
- package/src/player/components/useTimelineTrackLayout.test.ts +107 -1
- package/src/player/components/useTimelineTrackLayout.ts +83 -7
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -0
- package/src/player/hooks/useTimelinePlayer.ts +13 -1
- package/src/player/lib/automationStoreSync.test.ts +83 -0
- package/src/player/lib/automationStoreSync.ts +56 -0
- package/src/player/lib/timelineDOM.test.ts +33 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/store/automationSelectionSlice.test.ts +30 -0
- package/src/player/store/automationSelectionSlice.ts +54 -0
- package/src/player/store/playerStore.test.ts +15 -0
- package/src/player/store/playerStore.ts +54 -65
- package/src/player/store/timelineElement.ts +78 -0
- package/src/styles/studio.css +89 -0
- package/src/telemetry/agentRuntime.test.ts +45 -0
- package/src/telemetry/agentRuntime.ts +62 -0
- package/src/telemetry/system.ts +8 -0
- package/src/utils/studioFileHistory.test.ts +49 -0
- package/src/utils/studioFileHistory.ts +17 -1
- package/src/utils/studioTelemetry.ts +16 -10
- package/dist/assets/index-dF-CmLZu.js +0 -428
- package/dist/assets/index-zQ4JFwwB.css +0 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling module families apart before the word registers.
|
|
3
|
+
*
|
|
4
|
+
* A rack of eight modules is eight lines of text, and reading it top to bottom
|
|
5
|
+
* means reading eight names. Lettering each family differently means an author
|
|
6
|
+
* knows what KIND of module they are looking at with the label out of focus —
|
|
7
|
+
* the shape of the line carries it, and the word only confirms.
|
|
8
|
+
*
|
|
9
|
+
* Two faces of type, as budgeted in `plans/audio-fx-ux/README.md`: the sans
|
|
10
|
+
* carries four families apart by weight, case and tracking, and the serif is
|
|
11
|
+
* spent on the single family that behaves differently from all of them.
|
|
12
|
+
* Non-linear is the only generative one — it invents signal rather than
|
|
13
|
+
* measuring or shaping what is there — so it should not look like the others.
|
|
14
|
+
*
|
|
15
|
+
* Alongside it, a tint step per module WITHIN its family, derived from position
|
|
16
|
+
* in the registry rather than assigned by hand: two filters read as two
|
|
17
|
+
* different modules without reading as two different families, and adding an
|
|
18
|
+
* effect upstream never re-colours its siblings.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { HF_AUDIO_FX, type HfAudioFxNode } from "@hyperframes/core/audio-fx";
|
|
22
|
+
|
|
23
|
+
export type FxFamily = "filter" | "dynamics" | "nonlinear" | "time" | "smart";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How each family letters.
|
|
27
|
+
*
|
|
28
|
+
* `smart` is not a registry group. It is the measuring modules — the carve, the
|
|
29
|
+
* Tone EQ, the leveller — which analyse the audio and write their own settings.
|
|
30
|
+
* Monospace because that is what a readout looks like, and what they show IS a
|
|
31
|
+
* readout: numbers something else decided.
|
|
32
|
+
*/
|
|
33
|
+
export const FX_FAMILY_TYPE: Record<FxFamily, string> = {
|
|
34
|
+
filter: "font-light uppercase tracking-[0.14em]",
|
|
35
|
+
dynamics: "font-bold uppercase tracking-tight",
|
|
36
|
+
nonlinear: "font-serif italic tracking-normal",
|
|
37
|
+
time: "font-extralight uppercase tracking-[0.24em]",
|
|
38
|
+
smart: "font-mono font-medium tracking-normal",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Hue per family, and a lightness ramp across the modules inside it. */
|
|
42
|
+
const FAMILY_HUE: Record<FxFamily, number> = {
|
|
43
|
+
filter: 205,
|
|
44
|
+
dynamics: 25,
|
|
45
|
+
nonlinear: 310,
|
|
46
|
+
time: 165,
|
|
47
|
+
smart: 95,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Which family a chain node belongs to.
|
|
52
|
+
*
|
|
53
|
+
* The composite tags win over the registry group, because what a node IS to the
|
|
54
|
+
* author is the module that owns it: a carve's peaking filters are not filters
|
|
55
|
+
* the author added, they are one carve.
|
|
56
|
+
*/
|
|
57
|
+
export function fxFamilyOf(
|
|
58
|
+
node: Pick<HfAudioFxNode, "type" | "fromCarve" | "fromEq" | "fromLeveller">,
|
|
59
|
+
): FxFamily {
|
|
60
|
+
if (node.fromCarve || node.fromEq || node.fromLeveller) return "smart";
|
|
61
|
+
const group = HF_AUDIO_FX.find((def) => def.id === node.type)?.group;
|
|
62
|
+
return group === "dynamics" || group === "nonlinear" || group === "time" ? group : "filter";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The module's own tint: its family's hue, stepped by where it sits among its
|
|
67
|
+
* siblings in the registry.
|
|
68
|
+
*
|
|
69
|
+
* Kept narrow on purpose — 62% to 76% lightness at low saturation. This has to
|
|
70
|
+
* separate two modules at a glance without reading as a status colour, and the
|
|
71
|
+
* panel already spends saturation on "automated" and "bypassed".
|
|
72
|
+
*/
|
|
73
|
+
export function fxFamilyTint(
|
|
74
|
+
node: Pick<HfAudioFxNode, "type" | "fromCarve" | "fromEq" | "fromLeveller">,
|
|
75
|
+
): string {
|
|
76
|
+
const family = fxFamilyOf(node);
|
|
77
|
+
const siblings = HF_AUDIO_FX.filter((def) => fxFamilyOf({ type: def.id }) === family);
|
|
78
|
+
const index = siblings.findIndex((def) => def.id === node.type);
|
|
79
|
+
const step = siblings.length > 1 ? Math.max(0, index) / (siblings.length - 1) : 0;
|
|
80
|
+
// Comma syntax rather than the space-separated modern form: it is what every
|
|
81
|
+
// CSS parser in the chain agrees on, including the one the tests run in.
|
|
82
|
+
return `hsl(${FAMILY_HUE[family]}, 32%, ${(62 + step * 14).toFixed(1)}%)`;
|
|
83
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An effect row's open face: the derived or primary one-knob control, and
|
|
3
|
+
* Details underneath it.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `propertyPanelFxNodeRow.tsx`, which owned all of this before
|
|
6
|
+
* the file grew past a size where the header and the open face were still one
|
|
7
|
+
* thing to read.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
applyAudioFxProfile,
|
|
12
|
+
audioFxProfileStrength,
|
|
13
|
+
getAudioFxProfile,
|
|
14
|
+
} from "@hyperframes/core/audio-fx-profiles";
|
|
15
|
+
import type {
|
|
16
|
+
HfAudioFxDef,
|
|
17
|
+
HfAudioFxNode,
|
|
18
|
+
HfAudioFxParam,
|
|
19
|
+
HfAudioFxParamValues,
|
|
20
|
+
} from "@hyperframes/core/audio-fx";
|
|
21
|
+
import type { EFFECT_COPY } from "@hyperframes/core/audio-fx-copy";
|
|
22
|
+
import { trackProfileCommitted } from "./audioFxTelemetry.js";
|
|
23
|
+
import { FxParamRow } from "./propertyPanelFxControls.js";
|
|
24
|
+
import { FxBandRuler } from "./propertyPanelFxBandRuler.js";
|
|
25
|
+
import { FxNodeParams, type FxNodeControlHandlers } from "./propertyPanelFxNodeParams.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The derived one-knob face, for a module with no real parameter that can be
|
|
29
|
+
* its own. Not routed through `FxNodeParams`: this knob is not in the
|
|
30
|
+
* registry, so it has no AudioParam behind it and nothing to automate. What
|
|
31
|
+
* automation there is belongs to the parameters it sets, under Details, where
|
|
32
|
+
* they can be aimed at individually.
|
|
33
|
+
*/
|
|
34
|
+
function FxNodeDerivedKnob({
|
|
35
|
+
node,
|
|
36
|
+
derived,
|
|
37
|
+
profile,
|
|
38
|
+
disabled,
|
|
39
|
+
bypassed,
|
|
40
|
+
params,
|
|
41
|
+
index,
|
|
42
|
+
onPreview,
|
|
43
|
+
onUpdate,
|
|
44
|
+
trackKind,
|
|
45
|
+
}: {
|
|
46
|
+
node: HfAudioFxNode;
|
|
47
|
+
derived: HfAudioFxParam | null;
|
|
48
|
+
profile: ReturnType<typeof getAudioFxProfile>;
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
bypassed: boolean;
|
|
51
|
+
params: HfAudioFxParamValues;
|
|
52
|
+
index: number;
|
|
53
|
+
onPreview(index: number, params: HfAudioFxParamValues): void;
|
|
54
|
+
onUpdate(index: number, patch: Partial<HfAudioFxNode>): void;
|
|
55
|
+
trackKind?: string;
|
|
56
|
+
}) {
|
|
57
|
+
if (!derived) return null;
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
<div className="hf-fx-params space-y-0.5 border-t border-panel-border-input px-1.5 py-1.5">
|
|
61
|
+
<FxParamRow
|
|
62
|
+
param={derived}
|
|
63
|
+
value={audioFxProfileStrength(node.type, params)}
|
|
64
|
+
disabled={Boolean(disabled) || bypassed}
|
|
65
|
+
onChange={(_k, v) => onPreview(index, applyAudioFxProfile(node.type, Number(v), params))}
|
|
66
|
+
onCommit={(_k, v) => {
|
|
67
|
+
trackProfileCommitted(node.type, Number(v), { trackKind });
|
|
68
|
+
onUpdate(index, { params: applyAudioFxProfile(node.type, Number(v), params) });
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
{profile ? (
|
|
73
|
+
<p className="hf-fx-node-ends flex justify-between gap-2 px-1.5 pb-1 text-[9px] text-panel-text-2">
|
|
74
|
+
<span className="truncate">{profile.ends.low}</span>
|
|
75
|
+
<span className="truncate text-right">{profile.ends.high}</span>
|
|
76
|
+
</p>
|
|
77
|
+
) : null}
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The primary knob's face: the one control, what its two ends sound like, and
|
|
84
|
+
* — for a spectral module — the ruler that teaches where it is working.
|
|
85
|
+
*/
|
|
86
|
+
function FxNodePrimaryKnob({
|
|
87
|
+
node,
|
|
88
|
+
onlyPrimary,
|
|
89
|
+
primary,
|
|
90
|
+
copy,
|
|
91
|
+
params,
|
|
92
|
+
index,
|
|
93
|
+
disabled,
|
|
94
|
+
bypassed,
|
|
95
|
+
automatedTargets,
|
|
96
|
+
liveAutomationValues,
|
|
97
|
+
onUpdate,
|
|
98
|
+
onPreview,
|
|
99
|
+
onAutomateParam,
|
|
100
|
+
onRemoveParamAutomation,
|
|
101
|
+
trackKind,
|
|
102
|
+
}: FxNodeControlHandlers & {
|
|
103
|
+
node: HfAudioFxNode;
|
|
104
|
+
onlyPrimary: HfAudioFxDef;
|
|
105
|
+
primary: string | null;
|
|
106
|
+
copy: (typeof EFFECT_COPY)[string] | undefined;
|
|
107
|
+
params: HfAudioFxParamValues;
|
|
108
|
+
index: number;
|
|
109
|
+
disabled?: boolean;
|
|
110
|
+
bypassed: boolean;
|
|
111
|
+
}) {
|
|
112
|
+
if (!primary) return null;
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
<FxNodeParams
|
|
116
|
+
node={node}
|
|
117
|
+
def={onlyPrimary}
|
|
118
|
+
index={index}
|
|
119
|
+
disabled={Boolean(disabled) || bypassed}
|
|
120
|
+
automatedTargets={automatedTargets}
|
|
121
|
+
liveAutomationValues={liveAutomationValues}
|
|
122
|
+
onUpdate={onUpdate}
|
|
123
|
+
onPreview={onPreview}
|
|
124
|
+
onAutomateParam={onAutomateParam}
|
|
125
|
+
onRemoveParamAutomation={onRemoveParamAutomation}
|
|
126
|
+
trackKind={trackKind}
|
|
127
|
+
/>
|
|
128
|
+
{/* What the two ends of that knob sound like. A number tells an author
|
|
129
|
+
where the control is; this tells them which way to move it, which is
|
|
130
|
+
the question they actually have. */}
|
|
131
|
+
{copy?.primaryEnds ? (
|
|
132
|
+
<p className="hf-fx-node-ends flex justify-between gap-2 px-1.5 pb-1 text-[9px] text-panel-text-2">
|
|
133
|
+
<span className="truncate">{copy.primaryEnds.low}</span>
|
|
134
|
+
<span className="truncate text-right">{copy.primaryEnds.high}</span>
|
|
135
|
+
</p>
|
|
136
|
+
) : null}
|
|
137
|
+
{/* Where it is working, in the words the rack shares. Only for a module
|
|
138
|
+
that acts on a range at all — there is nothing spectral about a
|
|
139
|
+
limiter, and a ruler under one would be noise. */}
|
|
140
|
+
{copy?.band && typeof params.frequency === "number" ? (
|
|
141
|
+
<FxBandRuler band={copy.band} at={params.frequency} />
|
|
142
|
+
) : null}
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Everything below the header: does-copy, the one-knob face, and Details. The
|
|
149
|
+
* derived and primary knobs are already their own components — what is left
|
|
150
|
+
* is five independent conditionals deciding which pieces of the open face to
|
|
151
|
+
* show, which is the section's actual job rather than an avoidable branch.
|
|
152
|
+
*/
|
|
153
|
+
// fallow-ignore-next-line complexity
|
|
154
|
+
export function FxNodeOpenBody({
|
|
155
|
+
node,
|
|
156
|
+
registryDef,
|
|
157
|
+
def,
|
|
158
|
+
onlyPrimary,
|
|
159
|
+
primary,
|
|
160
|
+
derived,
|
|
161
|
+
profile,
|
|
162
|
+
oneKnob,
|
|
163
|
+
details,
|
|
164
|
+
onToggleDetails,
|
|
165
|
+
copy,
|
|
166
|
+
params,
|
|
167
|
+
index,
|
|
168
|
+
disabled,
|
|
169
|
+
bypassed,
|
|
170
|
+
automatedTargets,
|
|
171
|
+
liveAutomationValues,
|
|
172
|
+
onUpdate,
|
|
173
|
+
onPreview,
|
|
174
|
+
onAutomateParam,
|
|
175
|
+
onRemoveParamAutomation,
|
|
176
|
+
trackKind,
|
|
177
|
+
}: FxNodeControlHandlers & {
|
|
178
|
+
node: HfAudioFxNode;
|
|
179
|
+
registryDef: HfAudioFxDef;
|
|
180
|
+
def: HfAudioFxDef;
|
|
181
|
+
onlyPrimary: HfAudioFxDef;
|
|
182
|
+
primary: string | null;
|
|
183
|
+
derived: HfAudioFxParam | null;
|
|
184
|
+
profile: ReturnType<typeof getAudioFxProfile>;
|
|
185
|
+
oneKnob: boolean;
|
|
186
|
+
details: boolean;
|
|
187
|
+
onToggleDetails(): void;
|
|
188
|
+
copy: (typeof EFFECT_COPY)[string] | undefined;
|
|
189
|
+
params: HfAudioFxParamValues;
|
|
190
|
+
index: number;
|
|
191
|
+
disabled?: boolean;
|
|
192
|
+
bypassed: boolean;
|
|
193
|
+
}) {
|
|
194
|
+
return (
|
|
195
|
+
<>
|
|
196
|
+
{/* What it is for, before what it is made of. */}
|
|
197
|
+
{copy?.does ? (
|
|
198
|
+
<p className="hf-fx-node-does border-t border-panel-border-input px-1.5 py-1 text-[10px] text-panel-text-2">
|
|
199
|
+
{copy.does}
|
|
200
|
+
</p>
|
|
201
|
+
) : null}
|
|
202
|
+
{!details ? (
|
|
203
|
+
<FxNodeDerivedKnob
|
|
204
|
+
node={node}
|
|
205
|
+
derived={derived}
|
|
206
|
+
profile={profile}
|
|
207
|
+
disabled={disabled}
|
|
208
|
+
bypassed={bypassed}
|
|
209
|
+
params={params}
|
|
210
|
+
index={index}
|
|
211
|
+
onPreview={onPreview}
|
|
212
|
+
onUpdate={onUpdate}
|
|
213
|
+
trackKind={trackKind}
|
|
214
|
+
/>
|
|
215
|
+
) : null}
|
|
216
|
+
{!details ? (
|
|
217
|
+
<FxNodePrimaryKnob
|
|
218
|
+
node={node}
|
|
219
|
+
onlyPrimary={onlyPrimary}
|
|
220
|
+
primary={primary}
|
|
221
|
+
copy={copy}
|
|
222
|
+
params={params}
|
|
223
|
+
index={index}
|
|
224
|
+
disabled={disabled}
|
|
225
|
+
bypassed={bypassed}
|
|
226
|
+
automatedTargets={automatedTargets}
|
|
227
|
+
liveAutomationValues={liveAutomationValues}
|
|
228
|
+
onUpdate={onUpdate}
|
|
229
|
+
onPreview={onPreview}
|
|
230
|
+
onAutomateParam={onAutomateParam}
|
|
231
|
+
onRemoveParamAutomation={onRemoveParamAutomation}
|
|
232
|
+
trackKind={trackKind}
|
|
233
|
+
/>
|
|
234
|
+
) : null}
|
|
235
|
+
{/* The DSP name lives on the disclosure, so it is read at the moment
|
|
236
|
+
the author asks what this really is — and never before. */}
|
|
237
|
+
{oneKnob ? (
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
className="hf-fx-node-details flex w-full items-center gap-1 border-t border-panel-border-input px-1.5 py-1 text-left font-mono text-[9px] uppercase tracking-wide text-panel-text-2 hover:text-panel-text-0"
|
|
241
|
+
aria-expanded={details}
|
|
242
|
+
onClick={onToggleDetails}
|
|
243
|
+
>
|
|
244
|
+
<span aria-hidden="true">{details ? "▾" : "▸"}</span>
|
|
245
|
+
Details — {registryDef.label}
|
|
246
|
+
</button>
|
|
247
|
+
) : (
|
|
248
|
+
<p className="hf-fx-node-mechanism border-t border-panel-border-input px-1.5 pt-1 font-mono text-[9px] uppercase tracking-wide text-panel-text-2">
|
|
249
|
+
Details — {registryDef.label}
|
|
250
|
+
</p>
|
|
251
|
+
)}
|
|
252
|
+
{details || !oneKnob ? (
|
|
253
|
+
<FxNodeParams
|
|
254
|
+
node={node}
|
|
255
|
+
def={def}
|
|
256
|
+
index={index}
|
|
257
|
+
disabled={Boolean(disabled) || bypassed}
|
|
258
|
+
automatedTargets={automatedTargets}
|
|
259
|
+
liveAutomationValues={liveAutomationValues}
|
|
260
|
+
onUpdate={onUpdate}
|
|
261
|
+
onPreview={onPreview}
|
|
262
|
+
onAutomateParam={onAutomateParam}
|
|
263
|
+
onRemoveParamAutomation={onRemoveParamAutomation}
|
|
264
|
+
trackKind={trackKind}
|
|
265
|
+
/>
|
|
266
|
+
) : null}
|
|
267
|
+
</>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An open effect's knobs, with whatever automation surface applies to them.
|
|
3
|
+
*
|
|
4
|
+
* Split into its own file — rather than living in `propertyPanelFxNodeRow.tsx`
|
|
5
|
+
* or `propertyPanelFxNodeOpenBody.tsx` — because both of those import it: it
|
|
6
|
+
* backs the row's own Details disclosure AND the open face's derived/primary
|
|
7
|
+
* knobs, and either owning file would have made the other import a cycle.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
defaultAudioFxParams,
|
|
12
|
+
type HfAudioFxDef,
|
|
13
|
+
type HfAudioFxNode,
|
|
14
|
+
type HfAudioFxParamValues,
|
|
15
|
+
} from "@hyperframes/core/audio-fx";
|
|
16
|
+
import { fxAutomationTarget } from "@hyperframes/core/audio-automation";
|
|
17
|
+
import { trackParamCommitted } from "./audioFxTelemetry.js";
|
|
18
|
+
import { FxParams } from "./propertyPanelFxControls.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Which of an effect's knobs already have a lane.
|
|
22
|
+
*
|
|
23
|
+
* A lane addresses a node by id, so a node the panel has not yet given one
|
|
24
|
+
* cannot be automated at all. Adding an effect mints the id, so this only
|
|
25
|
+
* affects chains written before ids existed.
|
|
26
|
+
*/
|
|
27
|
+
function automatedKeysOf(
|
|
28
|
+
node: HfAudioFxNode,
|
|
29
|
+
params: readonly { key: string }[],
|
|
30
|
+
automatedTargets: ReadonlySet<string> | undefined,
|
|
31
|
+
): Set<string> {
|
|
32
|
+
if (!node.id || !automatedTargets) return new Set();
|
|
33
|
+
const nodeId = node.id;
|
|
34
|
+
return new Set(
|
|
35
|
+
params.filter((p) => automatedTargets.has(fxAutomationTarget(nodeId, p.key))).map((p) => p.key),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Wiring a knob row hands down to whatever renders it — shared between
|
|
41
|
+
* `FxNodeParams` and `FxNodeOpenBody`, which both sit between the row and the
|
|
42
|
+
* controls.
|
|
43
|
+
*/
|
|
44
|
+
export interface FxNodeControlHandlers {
|
|
45
|
+
automatedTargets?: ReadonlySet<string>;
|
|
46
|
+
liveAutomationValues?: ReadonlyMap<string, number>;
|
|
47
|
+
onUpdate(index: number, patch: Partial<HfAudioFxNode>): void;
|
|
48
|
+
onPreview(index: number, params: HfAudioFxParamValues): void;
|
|
49
|
+
onAutomateParam?(nodeId: string, paramKey: string): void;
|
|
50
|
+
onRemoveParamAutomation?(nodeId: string, paramKey: string): void;
|
|
51
|
+
trackKind?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function FxNodeParams({
|
|
55
|
+
node,
|
|
56
|
+
def,
|
|
57
|
+
index,
|
|
58
|
+
disabled,
|
|
59
|
+
automatedTargets,
|
|
60
|
+
liveAutomationValues,
|
|
61
|
+
onUpdate,
|
|
62
|
+
onPreview,
|
|
63
|
+
onAutomateParam,
|
|
64
|
+
onRemoveParamAutomation,
|
|
65
|
+
trackKind,
|
|
66
|
+
}: FxNodeControlHandlers & {
|
|
67
|
+
node: HfAudioFxNode;
|
|
68
|
+
def: HfAudioFxDef;
|
|
69
|
+
index: number;
|
|
70
|
+
disabled: boolean;
|
|
71
|
+
}) {
|
|
72
|
+
const nodeId = node.id;
|
|
73
|
+
// Lanes address a node by id; the controls know their own parameter keys. This
|
|
74
|
+
// is the one place that translation belongs.
|
|
75
|
+
const liveValues = ((): Map<string, number> | undefined => {
|
|
76
|
+
if (!nodeId || !liveAutomationValues?.size) return undefined;
|
|
77
|
+
const byKey = new Map<string, number>();
|
|
78
|
+
for (const param of def.params) {
|
|
79
|
+
const live = liveAutomationValues.get(fxAutomationTarget(nodeId, param.key));
|
|
80
|
+
if (live !== undefined) byKey.set(param.key, live);
|
|
81
|
+
}
|
|
82
|
+
return byKey;
|
|
83
|
+
})();
|
|
84
|
+
return (
|
|
85
|
+
<FxParams
|
|
86
|
+
def={def}
|
|
87
|
+
params={node.params ?? defaultAudioFxParams(node.type)}
|
|
88
|
+
liveValues={liveValues}
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
onChange={(params: HfAudioFxParamValues) => onPreview(index, params)}
|
|
91
|
+
onCommit={(next: HfAudioFxParamValues) => {
|
|
92
|
+
// Which knob actually moved. `onCommit` hands over the whole parameter
|
|
93
|
+
// set, so without the diff every commit would report the first key and
|
|
94
|
+
// the numbers would say authors only ever touch "frequency".
|
|
95
|
+
const before = node.params ?? defaultAudioFxParams(node.type);
|
|
96
|
+
for (const [key, value] of Object.entries(next)) {
|
|
97
|
+
if (before[key] === value) continue;
|
|
98
|
+
if (typeof value !== "number" && typeof value !== "string") continue;
|
|
99
|
+
trackParamCommitted(node.type, key, value, "details", { trackKind });
|
|
100
|
+
}
|
|
101
|
+
onUpdate(index, { params: next });
|
|
102
|
+
}}
|
|
103
|
+
automatedKeys={automatedKeysOf(node, def.params, automatedTargets)}
|
|
104
|
+
onAutomate={nodeId && onAutomateParam ? (key) => onAutomateParam(nodeId, key) : undefined}
|
|
105
|
+
onRemoveAutomation={
|
|
106
|
+
nodeId && onRemoveParamAutomation
|
|
107
|
+
? (key) => onRemoveParamAutomation(nodeId, key)
|
|
108
|
+
: undefined
|
|
109
|
+
}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|