@hyperframes/studio 0.7.107 → 0.7.108

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.
Files changed (126) hide show
  1. package/dist/assets/{hyperframes-player-BLrfwq5o.js → hyperframes-player-QqavTWRK.js} +1 -1
  2. package/dist/assets/{index-BA9yhzfR.js → index-BQjXA1pz.js} +1 -1
  3. package/dist/assets/{index-BSBk5srs.js → index-BhHHMprP.js} +1 -1
  4. package/dist/assets/index-DNkh9mbV.css +1 -0
  5. package/dist/assets/index-GqONLcOl.js +428 -0
  6. package/dist/index.d.ts +85 -2
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +17696 -12512
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/components/StudioRightPanel.tsx +18 -1
  12. package/src/components/TimelineToolbar.test.tsx +46 -0
  13. package/src/components/TimelineToolbar.tsx +17 -0
  14. package/src/components/editor/PropertyPanel.test.tsx +5 -1
  15. package/src/components/editor/PropertyPanel.tsx +7 -23
  16. package/src/components/editor/PropertyPanelFlat.tsx +25 -0
  17. package/src/components/editor/audioFxSummary.test.ts +75 -0
  18. package/src/components/editor/audioFxSummary.ts +36 -0
  19. package/src/components/editor/audioFxTelemetry.test.ts +129 -0
  20. package/src/components/editor/audioFxTelemetry.ts +230 -0
  21. package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +1830 -0
  22. package/src/components/editor/propertyPanelAudioFxGroup.tsx +299 -0
  23. package/src/components/editor/propertyPanelAudioFxGroupUtils.ts +21 -0
  24. package/src/components/editor/propertyPanelAutomation.test.ts +60 -0
  25. package/src/components/editor/propertyPanelAutomation.ts +97 -0
  26. package/src/components/editor/propertyPanelFlatMediaSection.tsx +37 -9
  27. package/src/components/editor/propertyPanelFlatProps.ts +1 -0
  28. package/src/components/editor/propertyPanelFxAddMenu.tsx +209 -0
  29. package/src/components/editor/propertyPanelFxBandRuler.tsx +72 -0
  30. package/src/components/editor/propertyPanelFxCarveModule.tsx +382 -0
  31. package/src/components/editor/propertyPanelFxControls.test.tsx +115 -0
  32. package/src/components/editor/propertyPanelFxControls.tsx +356 -0
  33. package/src/components/editor/propertyPanelFxEqModule.tsx +196 -0
  34. package/src/components/editor/propertyPanelFxFamily.ts +83 -0
  35. package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +269 -0
  36. package/src/components/editor/propertyPanelFxNodeParams.tsx +112 -0
  37. package/src/components/editor/propertyPanelFxNodeRow.tsx +339 -0
  38. package/src/components/editor/propertyPanelFxPresetMenu.tsx +159 -0
  39. package/src/components/editor/propertyPanelFxPresetRun.tsx +215 -0
  40. package/src/components/editor/propertyPanelFxPresetStyle.test.ts +142 -0
  41. package/src/components/editor/propertyPanelFxPresetStyle.ts +216 -0
  42. package/src/components/editor/propertyPanelFxRackChain.tsx +202 -0
  43. package/src/components/editor/propertyPanelFxSection.test.tsx +1747 -0
  44. package/src/components/editor/propertyPanelFxSection.tsx +550 -0
  45. package/src/components/editor/propertyPanelFxSectionTypes.ts +91 -0
  46. package/src/components/editor/propertyPanelTypes.ts +5 -0
  47. package/src/components/editor/useFxAudition.ts +95 -0
  48. package/src/components/editor/useFxCarve.ts +0 -0
  49. package/src/components/editor/useFxChainObserved.ts +83 -0
  50. package/src/components/editor/useFxLevelling.ts +235 -0
  51. package/src/components/editor/useVolumeAutomation.test.tsx +122 -0
  52. package/src/components/editor/useVolumeAutomation.ts +54 -0
  53. package/src/contexts/DomEditContext.tsx +11 -0
  54. package/src/hooks/useAppHotkeys.test.ts +198 -0
  55. package/src/hooks/useAppHotkeys.ts +30 -3
  56. package/src/hooks/useAutomationSelectionKeyboard.test.tsx +429 -0
  57. package/src/hooks/useAutomationSelectionKeyboard.ts +371 -0
  58. package/src/hooks/useDomEditAttributeCommits.ts +90 -10
  59. package/src/hooks/useDomEditCommits.test.tsx +38 -0
  60. package/src/hooks/useDomEditCommits.ts +2 -0
  61. package/src/hooks/useDomEditSession.ts +2 -0
  62. package/src/hooks/useDomEditTextCommits.ts +2 -0
  63. package/src/hooks/useElementLifecycleOps.ts +3 -0
  64. package/src/hooks/useLivePlayheadTime.ts +49 -0
  65. package/src/hooks/usePreviewPersistence.ts +7 -0
  66. package/src/hooks/useTimelineEditing.ts +3 -0
  67. package/src/player/components/AutomationSelectionMenu.tsx +76 -0
  68. package/src/player/components/AutomationValueInput.tsx +51 -0
  69. package/src/player/components/LayerDisclosureRow.tsx +5 -4
  70. package/src/player/components/Timeline.test.ts +84 -0
  71. package/src/player/components/TimelineAutomationLane.test.tsx +1720 -0
  72. package/src/player/components/TimelineAutomationLane.tsx +674 -0
  73. package/src/player/components/TimelineAutomationLaneSlot.test.tsx +193 -0
  74. package/src/player/components/TimelineLanes.test.tsx +24 -5
  75. package/src/player/components/TimelineLanes.tsx +142 -154
  76. package/src/player/components/TimelineTrackHeader.test.tsx +212 -1
  77. package/src/player/components/TimelineTrackHeader.tsx +166 -6
  78. package/src/player/components/TimelineTrackRow.tsx +8 -1
  79. package/src/player/components/automationClipboard.test.ts +94 -0
  80. package/src/player/components/automationClipboard.ts +136 -0
  81. package/src/player/components/automationGestureKeys.test.ts +49 -0
  82. package/src/player/components/automationGestureKeys.ts +49 -0
  83. package/src/player/components/automationLaneData.test.ts +270 -0
  84. package/src/player/components/automationLaneData.ts +258 -0
  85. package/src/player/components/automationLaneDragMath.ts +194 -0
  86. package/src/player/components/automationLaneGeometry.test.ts +239 -0
  87. package/src/player/components/automationLaneGeometry.ts +287 -0
  88. package/src/player/components/automationLaneHeight.ts +15 -0
  89. package/src/player/components/automationLanePointer.ts +20 -0
  90. package/src/player/components/automationLaneSelection.test.ts +183 -0
  91. package/src/player/components/automationLaneSelection.ts +135 -0
  92. package/src/player/components/automationShapes.test.ts +82 -0
  93. package/src/player/components/automationShapes.ts +70 -0
  94. package/src/player/components/automationSimplify.test.ts +48 -0
  95. package/src/player/components/automationSimplify.ts +51 -0
  96. package/src/player/components/timelineClipGestureHandlers.ts +223 -0
  97. package/src/player/components/timelineLaneProps.ts +19 -0
  98. package/src/player/components/timelineLayout.ts +11 -5
  99. package/src/player/components/useAutomationEdgeStretch.test.ts +27 -0
  100. package/src/player/components/useAutomationEdgeStretch.ts +269 -0
  101. package/src/player/components/useAutomationLaneGestures.ts +537 -0
  102. package/src/player/components/useAutomationLanes.test.tsx +112 -0
  103. package/src/player/components/useAutomationLanes.ts +170 -0
  104. package/src/player/components/useAutomationRangeDrag.ts +114 -0
  105. package/src/player/components/useTimelineTrackLayout.test.ts +107 -1
  106. package/src/player/components/useTimelineTrackLayout.ts +83 -7
  107. package/src/player/hooks/useExpandedTimelineElements.ts +7 -0
  108. package/src/player/hooks/useTimelinePlayer.ts +13 -1
  109. package/src/player/lib/automationStoreSync.test.ts +83 -0
  110. package/src/player/lib/automationStoreSync.ts +56 -0
  111. package/src/player/lib/timelineDOM.test.ts +33 -0
  112. package/src/player/lib/timelineDOM.ts +12 -0
  113. package/src/player/store/automationSelectionSlice.test.ts +30 -0
  114. package/src/player/store/automationSelectionSlice.ts +54 -0
  115. package/src/player/store/playerStore.test.ts +15 -0
  116. package/src/player/store/playerStore.ts +54 -65
  117. package/src/player/store/timelineElement.ts +78 -0
  118. package/src/styles/studio.css +89 -0
  119. package/src/telemetry/agentRuntime.test.ts +45 -0
  120. package/src/telemetry/agentRuntime.ts +62 -0
  121. package/src/telemetry/system.ts +8 -0
  122. package/src/utils/studioFileHistory.test.ts +49 -0
  123. package/src/utils/studioFileHistory.ts +17 -1
  124. package/src/utils/studioTelemetry.ts +16 -10
  125. package/dist/assets/index-dF-CmLZu.js +0 -428
  126. 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
+ }