@hyperframes/studio 0.7.106 → 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.
- package/dist/assets/{hyperframes-player-CvVcx_CV.js → hyperframes-player-QqavTWRK.js} +1 -1
- package/dist/assets/{index-CRzCCuPH.js → index-BQjXA1pz.js} +1 -1
- package/dist/assets/{index-BbYg_isc.js → index-BhHHMprP.js} +1 -1
- package/dist/assets/index-DNkh9mbV.css +1 -0
- package/dist/assets/index-GqONLcOl.js +428 -0
- package/dist/{chunk-OBAG3GWK.js → chunk-AZYHQC6V.js} +6 -7
- package/dist/chunk-AZYHQC6V.js.map +1 -0
- package/dist/{domEditingLayers-AT7G6F4L.js → domEditingLayers-7AMZ7GFI.js} +4 -2
- package/dist/index.d.ts +86 -3
- package/dist/index.html +2 -2
- package/dist/index.js +24325 -17831
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/EditorShell.tsx +0 -2
- package/src/components/StudioErrorBoundary.test.tsx +97 -0
- package/src/components/StudioErrorBoundary.tsx +7 -0
- package/src/components/StudioOverlays.tsx +15 -13
- 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/DomEditOverlay.tsx +36 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +85 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +29 -3
- package/src/components/editor/InlineTextToolbar.test.tsx +296 -0
- package/src/components/editor/InlineTextToolbar.tsx +281 -0
- package/src/components/editor/OffCanvasIndicators.tsx +38 -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/domEditInlineText.test.ts +86 -0
- package/src/components/editor/domEditInlineText.ts +99 -0
- package/src/components/editor/domEditInlineTextElement.test.ts +59 -0
- package/src/components/editor/domEditing.ts +1 -0
- package/src/components/editor/domEditingLayers.ts +7 -6
- package/src/components/editor/inlineTextStyleRange.test.ts +743 -0
- package/src/components/editor/inlineTextStyleRange.ts +593 -0
- package/src/components/editor/inlineTextStyleRead.ts +47 -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/useDomEditNudge.ts +2 -2
- 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/useInlineTextEditing.tsx +119 -0
- package/src/components/editor/useVolumeAutomation.test.tsx +122 -0
- package/src/components/editor/useVolumeAutomation.ts +54 -0
- package/src/components/feedback/CrashFeedbackPrompt.tsx +27 -0
- package/src/components/feedback/StudioFeedbackCard.tsx +373 -0
- package/src/components/feedback/feedbackTrigger.test.ts +168 -0
- package/src/components/feedback/feedbackTrigger.ts +269 -0
- package/src/components/feedback/projectProvenance.test.ts +120 -0
- package/src/components/feedback/projectProvenance.ts +83 -0
- package/src/components/renders/useRenderQueue.ts +62 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +2 -2
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +19 -2
- package/src/contexts/DomEditContext.tsx +15 -0
- package/src/hooks/domEditPersistFailure.ts +0 -7
- package/src/hooks/useAppHotkeys.test.ts +198 -0
- package/src/hooks/useAppHotkeys.ts +31 -4
- 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 +141 -21
- package/src/hooks/useDomEditCommits.ts +4 -0
- package/src/hooks/useDomEditSession.ts +4 -0
- package/src/hooks/useDomEditTextCommits.ts +112 -11
- package/src/hooks/useElementLifecycleOps.ts +3 -0
- package/src/hooks/useFileTree.ts +8 -3
- package/src/hooks/useInlineTextEdit.test.tsx +555 -0
- package/src/hooks/useInlineTextEdit.ts +320 -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/playbackShortcuts.ts +5 -4
- 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/breadcrumbs.test.ts +68 -0
- package/src/telemetry/breadcrumbs.ts +70 -0
- package/src/telemetry/client.ts +5 -0
- package/src/telemetry/events.ts +84 -3
- package/src/telemetry/system.ts +8 -0
- package/src/utils/sourcePatcher.ts +5 -1
- package/src/utils/studioFileHistory.test.ts +49 -0
- package/src/utils/studioFileHistory.ts +17 -1
- package/src/utils/studioHelpers.ts +2 -5
- package/src/utils/studioTelemetry.ts +16 -10
- package/src/utils/timelineDiscovery.ts +3 -24
- package/src/utils/typingTarget.test.ts +54 -0
- package/src/utils/typingTarget.ts +43 -0
- package/dist/assets/index-DerI0ikN.js +0 -428
- package/dist/assets/index-tBPidglp.css +0 -1
- package/dist/chunk-OBAG3GWK.js.map +0 -1
- package/src/components/StudioFeedbackBar.tsx +0 -217
- /package/dist/{domEditingLayers-AT7G6F4L.js.map → domEditingLayers-7AMZ7GFI.js.map} +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One effect in the FX rack: its header controls, and its knobs when open.
|
|
3
|
+
*
|
|
4
|
+
* An entry in the chain, as opposed to a composite module — the carve and the
|
|
5
|
+
* Tone EQ own several nodes each and have their own files.
|
|
6
|
+
*
|
|
7
|
+
* The row speaks the author's language, not the registry's. `EFFECT_COPY`
|
|
8
|
+
* supplies the name and every knob's name, `SUMMARY` the sentence under it, and
|
|
9
|
+
* the DSP name moves inside — it is a fact about the mechanism, so it belongs
|
|
10
|
+
* with the mechanism. See `plans/audio-fx-ux/README.md` §Decided.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useMemo, useState } from "react";
|
|
14
|
+
import {
|
|
15
|
+
defaultAudioFxParams,
|
|
16
|
+
getAudioFxDef,
|
|
17
|
+
type HfAudioFxDef,
|
|
18
|
+
type HfAudioFxNode,
|
|
19
|
+
type HfAudioFxParam,
|
|
20
|
+
type HfAudioFxParamValues,
|
|
21
|
+
} from "@hyperframes/core/audio-fx";
|
|
22
|
+
import { EFFECT_COPY, SUMMARY } from "@hyperframes/core/audio-fx-copy";
|
|
23
|
+
import { trackNodeBypassed } from "./audioFxTelemetry.js";
|
|
24
|
+
import { getAudioFxProfile } from "@hyperframes/core/audio-fx-profiles";
|
|
25
|
+
import { FX_FAMILY_TYPE, fxFamilyOf, fxFamilyTint } from "./propertyPanelFxFamily.js";
|
|
26
|
+
import { FxNodeOpenBody } from "./propertyPanelFxNodeOpenBody.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The one control that carries the module, if it has one.
|
|
30
|
+
*
|
|
31
|
+
* "Two faces": a module opens on its name, a line about what it is for, and one
|
|
32
|
+
* knob — the rest is one click away and never in the way. Nothing is hidden; it
|
|
33
|
+
* is ordered.
|
|
34
|
+
*
|
|
35
|
+
* `primary` is either a real parameter key or the string "strength", which means
|
|
36
|
+
* the module wants a single DERIVED control over several parameters — the
|
|
37
|
+
* `PROFILES` idea, whose figures are proposed rather than measured and which has
|
|
38
|
+
* not shipped. Until it does, those five effects (compressor, gate, saturate,
|
|
39
|
+
* reverb, bitcrush) open on all of their controls, which is honest: the one knob
|
|
40
|
+
* they want does not exist yet, and inventing one would be a knob that lies.
|
|
41
|
+
*/
|
|
42
|
+
function primaryParamOf(def: HfAudioFxDef): string | null {
|
|
43
|
+
const primary = EFFECT_COPY[def.id]?.primary;
|
|
44
|
+
// "strength" names no real parameter, and for the five effects that declare it
|
|
45
|
+
// that is the point: their one knob is derived, and `profileRow` below builds
|
|
46
|
+
// it. Anything else has to be a parameter the effect actually has.
|
|
47
|
+
if (!primary || primary === "strength") return null;
|
|
48
|
+
return def.params.some((p) => p.key === primary) ? primary : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The derived knob, as a parameter the existing controls can render.
|
|
53
|
+
*
|
|
54
|
+
* A profile is not in the registry — it sets five real parameters and is none of
|
|
55
|
+
* them — so this fabricates the one row it needs rather than teaching `FxParams`
|
|
56
|
+
* about a second kind of control. 0..1 in hundredths, the same shape and the
|
|
57
|
+
* same feel as the carve's Strength.
|
|
58
|
+
*/
|
|
59
|
+
function profileParam(type: string): HfAudioFxParam | null {
|
|
60
|
+
const profile = getAudioFxProfile(type);
|
|
61
|
+
if (!profile) return null;
|
|
62
|
+
return {
|
|
63
|
+
kind: "number",
|
|
64
|
+
key: "strength",
|
|
65
|
+
label: profile.label,
|
|
66
|
+
unit: "",
|
|
67
|
+
min: 0,
|
|
68
|
+
max: 1,
|
|
69
|
+
step: 0.01,
|
|
70
|
+
default: 0.5,
|
|
71
|
+
hint: `Sets ${profile.derives.length} settings at once. Open Details to see where they land.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The registry's definition with the plain names written over it.
|
|
77
|
+
*
|
|
78
|
+
* Over rather than instead of: the registry stays the authority on range, step,
|
|
79
|
+
* unit and what is automatable, and only the words change. A parameter with no
|
|
80
|
+
* copy keeps its own label rather than disappearing — `audioFxCopy.test.ts` is
|
|
81
|
+
* what makes sure there is never one.
|
|
82
|
+
*/
|
|
83
|
+
function plainDef(def: HfAudioFxDef): HfAudioFxDef {
|
|
84
|
+
const copy = EFFECT_COPY[def.id];
|
|
85
|
+
if (!copy) return def;
|
|
86
|
+
return {
|
|
87
|
+
...def,
|
|
88
|
+
params: def.params.map((param) => {
|
|
89
|
+
const plain = copy.params[param.key];
|
|
90
|
+
if (!plain) return param;
|
|
91
|
+
// The registry's hint explains the mechanism, which is still the better
|
|
92
|
+
// tooltip than none — but the plain one wins where it exists.
|
|
93
|
+
return { ...param, label: plain.label, ...(plain.hint ? { hint: plain.hint } : {}) };
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface FxNodeRowProps {
|
|
99
|
+
node: HfAudioFxNode;
|
|
100
|
+
index: number;
|
|
101
|
+
/** Where it sits in the signal path, as the rack counts it. Absent means unnumbered. */
|
|
102
|
+
position?: number;
|
|
103
|
+
automatedTargets?: ReadonlySet<string>;
|
|
104
|
+
liveAutomationValues?: ReadonlyMap<string, number>;
|
|
105
|
+
onAutomateParam?(nodeId: string, paramKey: string): void;
|
|
106
|
+
onRemoveParamAutomation?(nodeId: string, paramKey: string): void;
|
|
107
|
+
open: boolean;
|
|
108
|
+
/** Last in the chain, so it cannot move further down. */
|
|
109
|
+
last: boolean;
|
|
110
|
+
disabled?: boolean;
|
|
111
|
+
onToggleOpen(): void;
|
|
112
|
+
onUpdate(index: number, patch: Partial<HfAudioFxNode>): void;
|
|
113
|
+
onMove(index: number, delta: number): void;
|
|
114
|
+
onRemove(index: number): void;
|
|
115
|
+
onPreview(index: number, params: HfAudioFxParamValues): void;
|
|
116
|
+
/** What the track reads as, carried onto this row own events. */
|
|
117
|
+
trackKind?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Reorder arrow. Disabled at the end of the chain it would move past. */
|
|
121
|
+
function FxMoveButton({
|
|
122
|
+
label,
|
|
123
|
+
glyph,
|
|
124
|
+
disabled,
|
|
125
|
+
onClick,
|
|
126
|
+
}: {
|
|
127
|
+
label: string;
|
|
128
|
+
glyph: string;
|
|
129
|
+
disabled: boolean;
|
|
130
|
+
onClick(): void;
|
|
131
|
+
}) {
|
|
132
|
+
return (
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
className="hf-fx-move px-1 font-mono text-[10px] text-panel-text-2 hover:text-panel-text-0 disabled:opacity-25"
|
|
136
|
+
title={label}
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
onClick={onClick}
|
|
139
|
+
>
|
|
140
|
+
{glyph}
|
|
141
|
+
</button>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Name, bypass, reorder and remove for one effect. */
|
|
146
|
+
function FxNodeHeader({
|
|
147
|
+
label,
|
|
148
|
+
family,
|
|
149
|
+
position,
|
|
150
|
+
open,
|
|
151
|
+
bypassed,
|
|
152
|
+
first,
|
|
153
|
+
last,
|
|
154
|
+
disabled,
|
|
155
|
+
onToggleOpen,
|
|
156
|
+
onToggleBypass,
|
|
157
|
+
onMove,
|
|
158
|
+
onRemove,
|
|
159
|
+
}: {
|
|
160
|
+
label: string;
|
|
161
|
+
/** How this family letters, so the KIND reads before the word does. */
|
|
162
|
+
family: string;
|
|
163
|
+
position?: number;
|
|
164
|
+
open: boolean;
|
|
165
|
+
bypassed: boolean;
|
|
166
|
+
first: boolean;
|
|
167
|
+
last: boolean;
|
|
168
|
+
disabled?: boolean;
|
|
169
|
+
onToggleOpen(): void;
|
|
170
|
+
onToggleBypass(): void;
|
|
171
|
+
onMove(delta: number): void;
|
|
172
|
+
onRemove(): void;
|
|
173
|
+
}) {
|
|
174
|
+
return (
|
|
175
|
+
<div className="hf-fx-node-head flex min-h-7 items-center gap-1 px-1.5">
|
|
176
|
+
{/* Two digits, because a rack reads as a path when its steps are numbered
|
|
177
|
+
and as a list when they are not — and the difference decides whether an
|
|
178
|
+
author thinks the order matters. It does; it is audible. */}
|
|
179
|
+
{position !== undefined ? (
|
|
180
|
+
<span className="hf-fx-node-index shrink-0 font-mono text-[9px] tabular-nums text-panel-text-2">
|
|
181
|
+
{String(position).padStart(2, "0")}
|
|
182
|
+
</span>
|
|
183
|
+
) : null}
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
className={`hf-fx-node-name flex-1 truncate text-left text-[11px] text-panel-text-1 hover:text-panel-text-0 ${family}`}
|
|
187
|
+
aria-expanded={open}
|
|
188
|
+
onClick={onToggleOpen}
|
|
189
|
+
>
|
|
190
|
+
{label}
|
|
191
|
+
</button>
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
className="hf-fx-bypass rounded-[3px] border border-panel-border-input px-1.5 py-0.5 font-mono text-[9px] text-panel-text-2 hover:text-panel-text-0 disabled:opacity-40"
|
|
195
|
+
aria-pressed={bypassed}
|
|
196
|
+
title={bypassed ? "Enable" : "Bypass"}
|
|
197
|
+
disabled={disabled}
|
|
198
|
+
onClick={onToggleBypass}
|
|
199
|
+
>
|
|
200
|
+
{bypassed ? "Off" : "On"}
|
|
201
|
+
</button>
|
|
202
|
+
<FxMoveButton
|
|
203
|
+
label="Move up"
|
|
204
|
+
glyph="↑"
|
|
205
|
+
disabled={Boolean(disabled) || first}
|
|
206
|
+
onClick={() => onMove(-1)}
|
|
207
|
+
/>
|
|
208
|
+
<FxMoveButton
|
|
209
|
+
label="Move down"
|
|
210
|
+
glyph="↓"
|
|
211
|
+
disabled={Boolean(disabled) || last}
|
|
212
|
+
onClick={() => onMove(1)}
|
|
213
|
+
/>
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
className="hf-fx-remove px-1 font-mono text-[11px] text-panel-text-2 hover:text-red-400 disabled:opacity-40"
|
|
217
|
+
title="Remove"
|
|
218
|
+
disabled={disabled}
|
|
219
|
+
onClick={onRemove}
|
|
220
|
+
>
|
|
221
|
+
×
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** One effect in the chain: its header controls, and its knobs when open. */
|
|
228
|
+
export function FxNodeRow({
|
|
229
|
+
node,
|
|
230
|
+
index,
|
|
231
|
+
position,
|
|
232
|
+
automatedTargets,
|
|
233
|
+
liveAutomationValues,
|
|
234
|
+
onAutomateParam,
|
|
235
|
+
onRemoveParamAutomation,
|
|
236
|
+
open,
|
|
237
|
+
last,
|
|
238
|
+
disabled,
|
|
239
|
+
onToggleOpen,
|
|
240
|
+
onUpdate,
|
|
241
|
+
onMove,
|
|
242
|
+
onRemove,
|
|
243
|
+
onPreview,
|
|
244
|
+
trackKind,
|
|
245
|
+
}: FxNodeRowProps) {
|
|
246
|
+
const registryDef = getAudioFxDef(node.type);
|
|
247
|
+
const def = useMemo(() => (registryDef ? plainDef(registryDef) : null), [registryDef]);
|
|
248
|
+
const primary = registryDef ? primaryParamOf(registryDef) : null;
|
|
249
|
+
/**
|
|
250
|
+
* The derived knob, for a module with no real parameter that can be its face.
|
|
251
|
+
*
|
|
252
|
+
* It behaves like the primary one everywhere below — one control on the open
|
|
253
|
+
* face, everything it sets behind Details — so the two are the same shape and
|
|
254
|
+
* only their source differs.
|
|
255
|
+
*/
|
|
256
|
+
const profile = getAudioFxProfile(node.type);
|
|
257
|
+
const derived = useMemo(() => profileParam(node.type), [node.type]);
|
|
258
|
+
/** The same def cut down to the one knob, so the open face reuses every wire. */
|
|
259
|
+
const onlyPrimary = useMemo(
|
|
260
|
+
() => (def && primary ? { ...def, params: def.params.filter((p) => p.key === primary) } : def),
|
|
261
|
+
[def, primary],
|
|
262
|
+
);
|
|
263
|
+
// Local, because nothing outside the row needs to know. Keyed by node id like
|
|
264
|
+
// the row itself, so it stays with its effect across a reorder.
|
|
265
|
+
const [details, setDetails] = useState(false);
|
|
266
|
+
if (!registryDef || !def || !onlyPrimary) return null;
|
|
267
|
+
const oneKnob = primary !== null || derived !== null;
|
|
268
|
+
const copy = EFFECT_COPY[node.type];
|
|
269
|
+
const bypassed = node.enabled === false;
|
|
270
|
+
const params = node.params ?? defaultAudioFxParams(node.type);
|
|
271
|
+
// What this effect is doing to the sound, as a sentence. The rack is read top
|
|
272
|
+
// to bottom far more often than any one module is opened, so this is the line
|
|
273
|
+
// that decides whether an author can follow their own mix.
|
|
274
|
+
const summary = SUMMARY[node.type]?.(params);
|
|
275
|
+
return (
|
|
276
|
+
<div
|
|
277
|
+
className={`hf-fx-node rounded-[4px] border border-l-2 border-panel-border-input${bypassed ? " opacity-50" : ""}`}
|
|
278
|
+
data-fx-node={node.type}
|
|
279
|
+
data-fx-family={fxFamilyOf(node)}
|
|
280
|
+
// The tint is on the edge rather than the text: the name already carries
|
|
281
|
+
// the family in its lettering, and colouring it too would fight the
|
|
282
|
+
// panel's own tokens for automated and bypassed.
|
|
283
|
+
style={{ borderLeftColor: fxFamilyTint(node) }}
|
|
284
|
+
>
|
|
285
|
+
<FxNodeHeader
|
|
286
|
+
family={FX_FAMILY_TYPE[fxFamilyOf(node)]}
|
|
287
|
+
position={position}
|
|
288
|
+
// The node's own job name when a preset gave it one, because that is the
|
|
289
|
+
// most specific truth available: a chain that cuts mud and then lifts
|
|
290
|
+
// clarity must not show the same name twice. Then the plain name, and
|
|
291
|
+
// the registry's only if an effect somehow has no copy.
|
|
292
|
+
label={node.label ?? EFFECT_COPY[node.type]?.title ?? registryDef.label}
|
|
293
|
+
open={open}
|
|
294
|
+
bypassed={bypassed}
|
|
295
|
+
first={index === 0}
|
|
296
|
+
last={last}
|
|
297
|
+
disabled={disabled}
|
|
298
|
+
onToggleOpen={onToggleOpen}
|
|
299
|
+
onToggleBypass={() => {
|
|
300
|
+
trackNodeBypassed(node.type, !bypassed, { trackKind });
|
|
301
|
+
onUpdate(index, { enabled: bypassed });
|
|
302
|
+
}}
|
|
303
|
+
onMove={(delta) => onMove(index, delta)}
|
|
304
|
+
onRemove={() => onRemove(index)}
|
|
305
|
+
/>
|
|
306
|
+
{summary ? (
|
|
307
|
+
<p className="hf-fx-node-summary truncate px-1.5 pb-1 text-[10px] text-panel-text-2">
|
|
308
|
+
{summary}
|
|
309
|
+
</p>
|
|
310
|
+
) : null}
|
|
311
|
+
{open ? (
|
|
312
|
+
<FxNodeOpenBody
|
|
313
|
+
node={node}
|
|
314
|
+
registryDef={registryDef}
|
|
315
|
+
def={def}
|
|
316
|
+
onlyPrimary={onlyPrimary}
|
|
317
|
+
primary={primary}
|
|
318
|
+
derived={derived}
|
|
319
|
+
profile={profile}
|
|
320
|
+
oneKnob={oneKnob}
|
|
321
|
+
details={details}
|
|
322
|
+
onToggleDetails={() => setDetails((was) => !was)}
|
|
323
|
+
copy={copy}
|
|
324
|
+
params={params}
|
|
325
|
+
index={index}
|
|
326
|
+
disabled={disabled}
|
|
327
|
+
bypassed={bypassed}
|
|
328
|
+
automatedTargets={automatedTargets}
|
|
329
|
+
liveAutomationValues={liveAutomationValues}
|
|
330
|
+
onUpdate={onUpdate}
|
|
331
|
+
onPreview={onPreview}
|
|
332
|
+
onAutomateParam={onAutomateParam}
|
|
333
|
+
onRemoveParamAutomation={onRemoveParamAutomation}
|
|
334
|
+
trackKind={trackKind}
|
|
335
|
+
/>
|
|
336
|
+
) : null}
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The preset shelf for the FX rack.
|
|
3
|
+
*
|
|
4
|
+
* Its own module rather than another block inside the section: the section is
|
|
5
|
+
* already the largest file in the panel, and this surface is going to grow —
|
|
6
|
+
* search, per-item descriptions and a preview of the chain each preset draws
|
|
7
|
+
* are all queued behind it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
audioFxPresetsByFamily,
|
|
12
|
+
HF_AUDIO_FX_PRESET_FAMILIES,
|
|
13
|
+
type HfAudioFxPresetFamily,
|
|
14
|
+
} from "@hyperframes/core/audio-fx-presets";
|
|
15
|
+
import { PRESET_PROBLEM } from "@hyperframes/core/audio-fx-copy";
|
|
16
|
+
import { useRef } from "react";
|
|
17
|
+
import type { HfAudioNameKind } from "@hyperframes/core/audio-carve";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Shelf names in the author's language, which is deliberately not the effect
|
|
21
|
+
* registry's grouping. `group` says what an effect *is* (filter, dynamics);
|
|
22
|
+
* somebody reaching for Telephone is shopping for what they *want*, and
|
|
23
|
+
* Telephone is filters and saturation — nobody looks for it under either.
|
|
24
|
+
*/
|
|
25
|
+
const FAMILY_LABEL: Record<HfAudioFxPresetFamily, string> = {
|
|
26
|
+
voice: "Voice",
|
|
27
|
+
repair: "Fix",
|
|
28
|
+
character: "Character",
|
|
29
|
+
space: "Space",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface FxPresetMenuProps {
|
|
33
|
+
/**
|
|
34
|
+
* What the track reads as, from its id and filename.
|
|
35
|
+
*
|
|
36
|
+
* Only a confident "music" or "sfx" hides anything. `unknown` keeps the whole
|
|
37
|
+
* shelf, on the same principle the carve's source picker follows: a name is a
|
|
38
|
+
* hint, and a shelf that hides what somebody came for is worse than a long
|
|
39
|
+
* one. Nothing here is irreversible either — the cost of a wrong guess is one
|
|
40
|
+
* shelf the author has to scroll past, against the cost of "My voice sounds
|
|
41
|
+
* amateur" sitting on a music bed.
|
|
42
|
+
*/
|
|
43
|
+
trackKind?: HfAudioNameKind;
|
|
44
|
+
onPick(id: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* Report that a preset was auditioned. Called at most once per preset for as
|
|
47
|
+
* long as this shelf stays mounted — a pointer crossing the column passes a
|
|
48
|
+
* dozen items in a second, and counting every crossing would describe mouse
|
|
49
|
+
* travel rather than interest, while drowning every other event in the rack.
|
|
50
|
+
*/
|
|
51
|
+
onAuditionTracked?(id: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* Play this preset on the running audio without persisting it, and revert on
|
|
54
|
+
* `null`. Absent when there is no preview channel to hear it through.
|
|
55
|
+
*/
|
|
56
|
+
onAudition?(id: string | null): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Presets read as the complaint they answer, not as their own names.
|
|
61
|
+
*
|
|
62
|
+
* The name is a thing you have to already know — "Telephone", "Broadcast" — and
|
|
63
|
+
* the author arriving here does not know it; they know their voice sounds
|
|
64
|
+
* boomy. So the sentence leads and the name follows underneath, which is also
|
|
65
|
+
* how the name gets learned. The four shelves stay because eighteen sentences
|
|
66
|
+
* in a column is a wall, and they are already the author's grouping rather than
|
|
67
|
+
* the registry's. See `plans/audio-fx-ux/README.md` §Decided.
|
|
68
|
+
*/
|
|
69
|
+
export function FxPresetMenu({
|
|
70
|
+
trackKind,
|
|
71
|
+
onPick,
|
|
72
|
+
onAudition,
|
|
73
|
+
onAuditionTracked,
|
|
74
|
+
}: FxPresetMenuProps) {
|
|
75
|
+
// Mounted when the shelf opens and thrown away when it closes, so "once per
|
|
76
|
+
// preset" resets each time the author comes back — a second visit is a second
|
|
77
|
+
// look, not a duplicate of the first.
|
|
78
|
+
const auditioned = useRef(new Set<string>());
|
|
79
|
+
const reportAudition = (id: string) => {
|
|
80
|
+
if (auditioned.current.has(id)) return;
|
|
81
|
+
auditioned.current.add(id);
|
|
82
|
+
onAuditionTracked?.(id);
|
|
83
|
+
};
|
|
84
|
+
// The voice presets all begin by cutting rumble out of a human voice and end
|
|
85
|
+
// in a compressor set for speech. On a music bed that is not a mild mismatch,
|
|
86
|
+
// it is the wrong instrument — and the shelf leads with the complaint, so it
|
|
87
|
+
// would be offering the author a problem they do not have.
|
|
88
|
+
const families =
|
|
89
|
+
trackKind === "music" || trackKind === "sfx"
|
|
90
|
+
? HF_AUDIO_FX_PRESET_FAMILIES.filter((f) => f !== "voice")
|
|
91
|
+
: HF_AUDIO_FX_PRESET_FAMILIES;
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
className="hf-fx-preset-menu space-y-1.5 rounded-[4px] border border-panel-border-input p-1.5"
|
|
95
|
+
// One handler for the shelf rather than one per button: leaving any preset
|
|
96
|
+
// for the gap between two of them has to revert, and a per-button leave
|
|
97
|
+
// fires that on the way to the next one.
|
|
98
|
+
onMouseLeave={onAudition ? () => onAudition(null) : undefined}
|
|
99
|
+
// Focus leaving the shelf is the keyboard's version of the pointer leaving
|
|
100
|
+
// it. Moving between two buttons inside fires this and then the next
|
|
101
|
+
// button's focus, so it reverts and re-auditions rather than sticking.
|
|
102
|
+
onBlur={onAudition ? () => onAudition(null) : undefined}
|
|
103
|
+
>
|
|
104
|
+
{families.map((family) => (
|
|
105
|
+
<div key={family} className="hf-fx-preset-group space-y-0.5">
|
|
106
|
+
<span className="hf-fx-preset-group-label block font-mono text-[9px] uppercase tracking-wide text-panel-text-2">
|
|
107
|
+
{FAMILY_LABEL[family]}
|
|
108
|
+
</span>
|
|
109
|
+
{audioFxPresetsByFamily(family).map((preset) => (
|
|
110
|
+
<button
|
|
111
|
+
key={preset.id}
|
|
112
|
+
type="button"
|
|
113
|
+
// `pr-8` so a long complaint does not run under the wave.
|
|
114
|
+
className="hf-fx-preset-item block w-full rounded-[3px] bg-panel-surface py-1 pl-1.5 pr-8 text-left text-panel-text-1 hover:text-panel-text-0"
|
|
115
|
+
// The description says what it does; the count is doing real work
|
|
116
|
+
// — it tells the author a preset IS a chain they can open and
|
|
117
|
+
// edit, rather than an opaque setting they cannot follow.
|
|
118
|
+
title={`${preset.description} (${preset.nodes.length} effect${
|
|
119
|
+
preset.nodes.length === 1 ? "" : "s"
|
|
120
|
+
})`}
|
|
121
|
+
onClick={() => onPick(preset.id)}
|
|
122
|
+
onMouseEnter={() => {
|
|
123
|
+
reportAudition(preset.id);
|
|
124
|
+
onAudition?.(preset.id);
|
|
125
|
+
}}
|
|
126
|
+
// Keyboard reaches this too: arrowing down the shelf auditions the
|
|
127
|
+
// same way hovering does, or the whole affordance is mouse-only.
|
|
128
|
+
onFocus={() => {
|
|
129
|
+
reportAudition(preset.id);
|
|
130
|
+
onAudition?.(preset.id);
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
<span className="hf-fx-preset-problem block truncate text-[10px]">
|
|
134
|
+
{PRESET_PROBLEM[preset.id] ?? preset.description}
|
|
135
|
+
</span>
|
|
136
|
+
<span className="hf-fx-preset-name block truncate font-mono text-[9px] text-panel-text-2">
|
|
137
|
+
{preset.label}
|
|
138
|
+
</span>
|
|
139
|
+
{/* Hovering a preset plays it, and playing is otherwise invisible:
|
|
140
|
+
the panel looks identical whether the audition is sounding or
|
|
141
|
+
the pointer is just resting there. Only rendered when there IS
|
|
142
|
+
an audition channel, so it never claims audio that is not
|
|
143
|
+
happening. Four bars because that reads as a level meter at
|
|
144
|
+
this size; three reads as an ellipsis. */}
|
|
145
|
+
{onAudition ? (
|
|
146
|
+
<span className="hf-fx-preset-wave" aria-hidden="true">
|
|
147
|
+
<span />
|
|
148
|
+
<span />
|
|
149
|
+
<span />
|
|
150
|
+
<span />
|
|
151
|
+
</span>
|
|
152
|
+
) : null}
|
|
153
|
+
</button>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|