@hyperframes/studio 0.8.186 → 0.8.188
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-B_skzvYC.js → hyperframes-player-DwWIKKZG.js} +1 -1
- package/dist/assets/{index-CgLbvCUo.js → index-Cjwz7_13.js} +1 -1
- package/dist/assets/{index-Dn5FAk2e.js → index-M8lEpS0Q.js} +130 -130
- package/dist/index.html +1 -1
- package/dist/index.js +28 -20
- package/package.json +8 -8
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +57 -22
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +29 -25
- package/src/components/editor/useVolumeAutomation.test.tsx +2 -8
- package/src/components/editor/useVolumeAutomation.ts +2 -7
package/dist/index.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
document.documentElement.dataset.theme = "paper";
|
|
15
15
|
}
|
|
16
16
|
</script>
|
|
17
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
+
<script type="module" crossorigin src="/assets/index-M8lEpS0Q.js"></script>
|
|
18
18
|
<link rel="stylesheet" crossorigin href="/assets/index-CESvLzz0.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
package/dist/index.js
CHANGED
|
@@ -66847,7 +66847,7 @@ function useVolumeAutomation(element, currentTime, onSetAttributeQuiet, linkedSp
|
|
|
66847
66847
|
if (!linkedSpeed) return writeOwn(edit);
|
|
66848
66848
|
void linkedSpeed.commitAttribute(
|
|
66849
66849
|
HF_AUDIO_AUTOMATION_ATTR2,
|
|
66850
|
-
(
|
|
66850
|
+
(raw) => automationAttrValue(edit(readPanelAutomation(raw, void 0))) || null,
|
|
66851
66851
|
"Edit speed"
|
|
66852
66852
|
);
|
|
66853
66853
|
};
|
|
@@ -66862,10 +66862,7 @@ function useVolumeAutomation(element, currentTime, onSetAttributeQuiet, linkedSp
|
|
|
66862
66862
|
onCommitAt: (v) => writeLane((a) => withPointAt(a, target, clipTimeSec, v))
|
|
66863
66863
|
};
|
|
66864
66864
|
};
|
|
66865
|
-
const
|
|
66866
|
-
const parsed = raw ? Number(raw) : 1;
|
|
66867
|
-
const current = Number.isFinite(parsed) ? parsed : 1;
|
|
66868
|
-
const volume = laneBinding(VOLUME_TARGET3, current, writeOwn);
|
|
66865
|
+
const volume = laneBinding(VOLUME_TARGET3, 1, writeOwn);
|
|
66869
66866
|
const rateAttr = Number.parseFloat(element.dataAttributes?.["playback-rate"] ?? "");
|
|
66870
66867
|
const rate = laneBinding(
|
|
66871
66868
|
RATE_TARGET2,
|
|
@@ -67048,8 +67045,8 @@ function FlatMediaSection({
|
|
|
67048
67045
|
const isImage = element.tagName === "img";
|
|
67049
67046
|
const isVisualMedia = isVideo || isImage;
|
|
67050
67047
|
const el = element.element;
|
|
67051
|
-
const
|
|
67052
|
-
const
|
|
67048
|
+
const gain = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
|
|
67049
|
+
const volume = volumeAutomated && automatedVolumeValue !== void 0 ? automatedVolumeValue : 1;
|
|
67053
67050
|
const { mediaStart, mediaStartAttr } = readClipInPoint(element.dataAttributes);
|
|
67054
67051
|
const constantRate = Number.parseFloat(element.dataAttributes["playback-rate"] ?? "1") || 1;
|
|
67055
67052
|
const playbackRate = rate?.automated && rate.automatedValue !== void 0 ? rate.automatedValue : constantRate;
|
|
@@ -67183,6 +67180,25 @@ function FlatMediaSection({
|
|
|
67183
67180
|
] })
|
|
67184
67181
|
] }),
|
|
67185
67182
|
(isVideo || isAudio) && /* @__PURE__ */ jsxs134(Fragment48, { children: [
|
|
67183
|
+
/* @__PURE__ */ jsx176(
|
|
67184
|
+
FlatSlider,
|
|
67185
|
+
{
|
|
67186
|
+
label: "Gain",
|
|
67187
|
+
value: audioGainToFaderPosition3(gain),
|
|
67188
|
+
min: AUDIO_GAIN_FADER_MIN3,
|
|
67189
|
+
max: AUDIO_GAIN_FADER_MAX3,
|
|
67190
|
+
tier: gain === 1 ? "default" : "explicitCustom",
|
|
67191
|
+
displayValue: audioGainToText3(gain),
|
|
67192
|
+
centerTick: true,
|
|
67193
|
+
onCommit: (next) => void onSetAttribute("volume", formatAudioGain4(audioFaderPositionToGain3(next))),
|
|
67194
|
+
onCommitText: (text) => {
|
|
67195
|
+
const next = parseGainInput(text);
|
|
67196
|
+
if (next === null) return false;
|
|
67197
|
+
void onSetAttribute("volume", formatAudioGain4(next));
|
|
67198
|
+
return true;
|
|
67199
|
+
}
|
|
67200
|
+
}
|
|
67201
|
+
),
|
|
67186
67202
|
/* @__PURE__ */ jsxs134(
|
|
67187
67203
|
"div",
|
|
67188
67204
|
{
|
|
@@ -67193,25 +67209,17 @@ function FlatMediaSection({
|
|
|
67193
67209
|
FlatSlider,
|
|
67194
67210
|
{
|
|
67195
67211
|
label: "Volume",
|
|
67196
|
-
value:
|
|
67212
|
+
value: audioGainToFaderPosition3(volume),
|
|
67197
67213
|
min: AUDIO_GAIN_FADER_MIN3,
|
|
67198
67214
|
max: AUDIO_GAIN_FADER_MAX3,
|
|
67199
67215
|
tier: volume === 1 ? "default" : "explicitCustom",
|
|
67200
67216
|
displayValue: audioGainToText3(volume),
|
|
67201
67217
|
centerTick: true,
|
|
67202
|
-
onCommit: (next) =>
|
|
67203
|
-
const gain = audioFaderPositionToGain3(next);
|
|
67204
|
-
if (volumeAutomated) {
|
|
67205
|
-
onCommitVolumeAt?.(gain);
|
|
67206
|
-
} else {
|
|
67207
|
-
void onSetAttribute("volume", formatAudioGain4(gain));
|
|
67208
|
-
}
|
|
67209
|
-
},
|
|
67218
|
+
onCommit: (next) => onCommitVolumeAt?.(audioFaderPositionToGain3(next)),
|
|
67210
67219
|
onCommitText: (text) => {
|
|
67211
|
-
const
|
|
67212
|
-
if (
|
|
67213
|
-
|
|
67214
|
-
else void onSetAttribute("volume", formatAudioGain4(gain));
|
|
67220
|
+
const next = parseGainInput(text);
|
|
67221
|
+
if (next === null) return false;
|
|
67222
|
+
onCommitVolumeAt?.(next);
|
|
67215
67223
|
return true;
|
|
67216
67224
|
}
|
|
67217
67225
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.188",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"dockview-react": "8.3.1",
|
|
60
60
|
"gsap": "^3.13.0",
|
|
61
61
|
"mediabunny": "^1.45.3",
|
|
62
|
-
"@hyperframes/
|
|
63
|
-
"@hyperframes/
|
|
64
|
-
"@hyperframes/shader-transitions": "0.8.
|
|
65
|
-
"@hyperframes/studio-server": "0.8.
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
62
|
+
"@hyperframes/player": "0.8.188",
|
|
63
|
+
"@hyperframes/core": "0.8.188",
|
|
64
|
+
"@hyperframes/shader-transitions": "0.8.188",
|
|
65
|
+
"@hyperframes/studio-server": "0.8.188",
|
|
66
|
+
"@hyperframes/parsers": "0.8.188",
|
|
67
|
+
"@hyperframes/sdk": "0.8.188"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.188"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -9,6 +9,7 @@ import type { DomEditSelection } from "./domEditing";
|
|
|
9
9
|
import type { PatchOperation } from "../../utils/sourcePatcher";
|
|
10
10
|
import { formatTimingValue } from "./propertyPanelHelpers";
|
|
11
11
|
import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration";
|
|
12
|
+
import { audioGainToFaderPosition } from "@hyperframes/core/audio-gain";
|
|
12
13
|
|
|
13
14
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
14
15
|
|
|
@@ -163,7 +164,7 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
163
164
|
act(() => root.unmount());
|
|
164
165
|
});
|
|
165
166
|
|
|
166
|
-
it("commits +12 dB of boost from the upper half of the
|
|
167
|
+
it("commits +12 dB of boost from the upper half of the gain fader", () => {
|
|
167
168
|
const onSetAttribute = vi.fn();
|
|
168
169
|
const element = makeVideoElement({ dataAttributes: { volume: "1" } });
|
|
169
170
|
const host = document.createElement("div");
|
|
@@ -182,7 +183,7 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
182
183
|
/>,
|
|
183
184
|
);
|
|
184
185
|
});
|
|
185
|
-
const volumeTrack = host.
|
|
186
|
+
const volumeTrack = host.querySelector('[data-flat-slider-track="true"][aria-label="Gain"]')!;
|
|
186
187
|
Object.defineProperty(volumeTrack, "getBoundingClientRect", {
|
|
187
188
|
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
|
188
189
|
});
|
|
@@ -197,10 +198,8 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
197
198
|
act(() => root.unmount());
|
|
198
199
|
});
|
|
199
200
|
|
|
200
|
-
it("
|
|
201
|
-
const
|
|
202
|
-
const onCommitVolumeAt = vi.fn();
|
|
203
|
-
const element = makeVideoElement({ dataAttributes: { volume: "1" } });
|
|
201
|
+
it("shows the clip's gain and its lane's level as two sliders", () => {
|
|
202
|
+
const element = makeVideoElement({ dataAttributes: { volume: "0.5" } });
|
|
204
203
|
const host = document.createElement("div");
|
|
205
204
|
document.body.append(host);
|
|
206
205
|
const root = createRoot(host);
|
|
@@ -211,28 +210,62 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
211
210
|
element={element}
|
|
212
211
|
styles={{}}
|
|
213
212
|
onSetStyle={vi.fn()}
|
|
214
|
-
onSetAttribute={
|
|
213
|
+
onSetAttribute={vi.fn()}
|
|
215
214
|
onSetHtmlAttribute={vi.fn()}
|
|
216
215
|
onSetAttributeBatch={vi.fn()}
|
|
217
216
|
volumeAutomated
|
|
218
|
-
|
|
217
|
+
automatedVolumeValue={0.25}
|
|
219
218
|
/>,
|
|
220
219
|
);
|
|
221
220
|
});
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
act(() => {
|
|
227
|
-
volumeTrack.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 100 }));
|
|
228
|
-
volumeTrack.dispatchEvent(new MouseEvent("pointerup", { bubbles: true, clientX: 100 }));
|
|
229
|
-
});
|
|
230
|
-
expect(onCommitVolumeAt).toHaveBeenCalledTimes(1);
|
|
231
|
-
expect(onCommitVolumeAt.mock.calls[0][0]).toBeCloseTo(3.981072, 6);
|
|
232
|
-
expect(onSetAttribute).not.toHaveBeenCalledWith("volume", expect.anything());
|
|
221
|
+
const [gain, volume] = host.querySelectorAll('[data-flat-slider-track="true"]');
|
|
222
|
+
expect(labelsOf(host).slice(0, 2)).toEqual(["Gain", "Volume"]);
|
|
223
|
+
expect(gain?.getAttribute("aria-valuenow")).toBe(String(audioGainToFaderPosition(0.5)));
|
|
224
|
+
expect(volume?.getAttribute("aria-valuenow")).toBe(String(audioGainToFaderPosition(0.25)));
|
|
233
225
|
act(() => root.unmount());
|
|
234
226
|
});
|
|
235
227
|
|
|
228
|
+
it.each([true, false])(
|
|
229
|
+
"writes Volume through the lane, never data-volume (automated: %s)",
|
|
230
|
+
(automated) => {
|
|
231
|
+
const onSetAttribute = vi.fn();
|
|
232
|
+
const onCommitVolumeAt = vi.fn();
|
|
233
|
+
const element = makeVideoElement({ dataAttributes: { volume: "1" } });
|
|
234
|
+
const host = document.createElement("div");
|
|
235
|
+
document.body.append(host);
|
|
236
|
+
const root = createRoot(host);
|
|
237
|
+
act(() => {
|
|
238
|
+
root.render(
|
|
239
|
+
<FlatMediaSection
|
|
240
|
+
projectDir={null}
|
|
241
|
+
element={element}
|
|
242
|
+
styles={{}}
|
|
243
|
+
onSetStyle={vi.fn()}
|
|
244
|
+
onSetAttribute={onSetAttribute}
|
|
245
|
+
onSetHtmlAttribute={vi.fn()}
|
|
246
|
+
onSetAttributeBatch={vi.fn()}
|
|
247
|
+
volumeAutomated={automated}
|
|
248
|
+
onCommitVolumeAt={onCommitVolumeAt}
|
|
249
|
+
/>,
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
const volumeTrack = host.querySelector(
|
|
253
|
+
'[data-flat-slider-track="true"][aria-label="Volume"]',
|
|
254
|
+
)!;
|
|
255
|
+
Object.defineProperty(volumeTrack, "getBoundingClientRect", {
|
|
256
|
+
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
|
257
|
+
});
|
|
258
|
+
act(() => {
|
|
259
|
+
volumeTrack.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 100 }));
|
|
260
|
+
volumeTrack.dispatchEvent(new MouseEvent("pointerup", { bubbles: true, clientX: 100 }));
|
|
261
|
+
});
|
|
262
|
+
expect(onCommitVolumeAt).toHaveBeenCalledTimes(1);
|
|
263
|
+
expect(onCommitVolumeAt.mock.calls[0][0]).toBeCloseTo(3.981072, 6);
|
|
264
|
+
expect(onSetAttribute).not.toHaveBeenCalledWith("volume", expect.anything());
|
|
265
|
+
act(() => root.unmount());
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
|
|
236
269
|
it("commits a new rate value on slider track pointerdown", () => {
|
|
237
270
|
const onSetAttribute = vi.fn();
|
|
238
271
|
const element = makeVideoElement();
|
|
@@ -252,7 +285,7 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
252
285
|
/>,
|
|
253
286
|
);
|
|
254
287
|
});
|
|
255
|
-
const rateTrack = host.
|
|
288
|
+
const rateTrack = host.querySelector('[data-flat-slider-track="true"][aria-label="Speed"]')!;
|
|
256
289
|
Object.defineProperty(rateTrack, "getBoundingClientRect", {
|
|
257
290
|
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
|
258
291
|
});
|
|
@@ -284,7 +317,9 @@ describe("FlatMediaSection — volume/rate/media-start", () => {
|
|
|
284
317
|
/>,
|
|
285
318
|
);
|
|
286
319
|
});
|
|
287
|
-
const mediaStartTrack = host.
|
|
320
|
+
const mediaStartTrack = host.querySelector(
|
|
321
|
+
'[data-flat-slider-track="true"][aria-label="Media start"]',
|
|
322
|
+
)!;
|
|
288
323
|
Object.defineProperty(mediaStartTrack, "getBoundingClientRect", {
|
|
289
324
|
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
|
|
290
325
|
});
|
|
@@ -465,7 +500,7 @@ describe("FlatMediaSection — audio clips", () => {
|
|
|
465
500
|
it("shows Fade in / Fade out rows for audio, and for video only when it carries audio", () => {
|
|
466
501
|
const audio = renderWithRate(makeAudioElement());
|
|
467
502
|
expect(labelsOf(audio.host)).toEqual(
|
|
468
|
-
expect.arrayContaining(["Volume", "Speed", "Media start", "Fade in", "Fade out"]),
|
|
503
|
+
expect.arrayContaining(["Gain", "Volume", "Speed", "Media start", "Fade in", "Fade out"]),
|
|
469
504
|
);
|
|
470
505
|
act(() => audio.root.unmount());
|
|
471
506
|
|
|
@@ -64,7 +64,7 @@ export function FlatMediaSection({
|
|
|
64
64
|
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
|
65
65
|
onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
|
|
66
66
|
onSetAttributeBatch: CommitDomAttributeBatch;
|
|
67
|
-
/** A volume lane in the timeline drives the level; the slider writes a keyframe
|
|
67
|
+
/** A volume lane in the timeline drives the level; the Volume slider writes a keyframe. */
|
|
68
68
|
volumeAutomated?: boolean;
|
|
69
69
|
onAutomateVolume?: () => void;
|
|
70
70
|
onRemoveVolumeAutomation?: () => void;
|
|
@@ -88,14 +88,10 @@ export function FlatMediaSection({
|
|
|
88
88
|
const isVisualMedia = isVideo || isImage;
|
|
89
89
|
const el = element.element;
|
|
90
90
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
const volume =
|
|
95
|
-
volumeAutomated && automatedVolumeValue !== undefined
|
|
96
|
-
? automatedVolumeValue
|
|
97
|
-
: (parseNumericValue(element.dataAttributes.volume ?? "") ?? 1);
|
|
98
|
-
const volumeFaderPosition = audioGainToFaderPosition(volume);
|
|
91
|
+
// Gain is `data-volume`, ahead of the effects; Volume is the lane's level at the
|
|
92
|
+
// playhead, unity without one. What plays is gain times level, as in an editor.
|
|
93
|
+
const gain = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
|
|
94
|
+
const volume = volumeAutomated && automatedVolumeValue !== undefined ? automatedVolumeValue : 1;
|
|
99
95
|
const { mediaStart, mediaStartAttr } = readClipInPoint(element.dataAttributes);
|
|
100
96
|
const constantRate = Number.parseFloat(element.dataAttributes["playback-rate"] ?? "1") || 1;
|
|
101
97
|
const playbackRate =
|
|
@@ -246,9 +242,25 @@ export function FlatMediaSection({
|
|
|
246
242
|
)}
|
|
247
243
|
{(isVideo || isAudio) && (
|
|
248
244
|
<>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
245
|
+
<FlatSlider
|
|
246
|
+
label="Gain"
|
|
247
|
+
value={audioGainToFaderPosition(gain)}
|
|
248
|
+
min={AUDIO_GAIN_FADER_MIN}
|
|
249
|
+
max={AUDIO_GAIN_FADER_MAX}
|
|
250
|
+
tier={gain === 1 ? "default" : "explicitCustom"}
|
|
251
|
+
displayValue={audioGainToText(gain)}
|
|
252
|
+
centerTick
|
|
253
|
+
onCommit={(next) =>
|
|
254
|
+
void onSetAttribute("volume", formatAudioGain(audioFaderPositionToGain(next)))
|
|
255
|
+
}
|
|
256
|
+
onCommitText={(text) => {
|
|
257
|
+
const next = parseGainInput(text);
|
|
258
|
+
if (next === null) return false;
|
|
259
|
+
void onSetAttribute("volume", formatAudioGain(next));
|
|
260
|
+
return true;
|
|
261
|
+
}}
|
|
262
|
+
/>
|
|
263
|
+
{/* A commit writes a keyframe at the playhead, starting a lane when there is none. */}
|
|
252
264
|
<div
|
|
253
265
|
className="hf-volume-row flex items-center gap-1"
|
|
254
266
|
data-volume-automated={volumeAutomated ? "" : undefined}
|
|
@@ -256,25 +268,17 @@ export function FlatMediaSection({
|
|
|
256
268
|
<div className="min-w-0 flex-1">
|
|
257
269
|
<FlatSlider
|
|
258
270
|
label="Volume"
|
|
259
|
-
value={
|
|
271
|
+
value={audioGainToFaderPosition(volume)}
|
|
260
272
|
min={AUDIO_GAIN_FADER_MIN}
|
|
261
273
|
max={AUDIO_GAIN_FADER_MAX}
|
|
262
274
|
tier={volume === 1 ? "default" : "explicitCustom"}
|
|
263
275
|
displayValue={audioGainToText(volume)}
|
|
264
276
|
centerTick
|
|
265
|
-
onCommit={(next) =>
|
|
266
|
-
const gain = audioFaderPositionToGain(next);
|
|
267
|
-
if (volumeAutomated) {
|
|
268
|
-
onCommitVolumeAt?.(gain);
|
|
269
|
-
} else {
|
|
270
|
-
void onSetAttribute("volume", formatAudioGain(gain));
|
|
271
|
-
}
|
|
272
|
-
}}
|
|
277
|
+
onCommit={(next) => onCommitVolumeAt?.(audioFaderPositionToGain(next))}
|
|
273
278
|
onCommitText={(text) => {
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
else void onSetAttribute("volume", formatAudioGain(gain));
|
|
279
|
+
const next = parseGainInput(text);
|
|
280
|
+
if (next === null) return false;
|
|
281
|
+
onCommitVolumeAt?.(next);
|
|
278
282
|
return true;
|
|
279
283
|
}}
|
|
280
284
|
/>
|
|
@@ -59,22 +59,16 @@ describe("useVolumeAutomation", () => {
|
|
|
59
59
|
expect(bind({ volume: "0.55", automation }).binding.volumeAutomated).toBe(false);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it("seeds a new lane at
|
|
62
|
+
it("seeds a new lane at unity, which data-volume then multiplies", () => {
|
|
63
63
|
// Automating a track must not change how loud it is.
|
|
64
64
|
const { binding, onSetAttributeQuiet } = bind({ volume: "0.55" });
|
|
65
65
|
act(() => binding.onAutomateVolume());
|
|
66
66
|
expect(onSetAttributeQuiet).toHaveBeenCalledWith(
|
|
67
67
|
"data-automation",
|
|
68
|
-
JSON.stringify({ version: 1, lanes: [{ target: "volume", points: [{ t: 0, v:
|
|
68
|
+
JSON.stringify({ version: 1, lanes: [{ target: "volume", points: [{ t: 0, v: 1 }] }] }),
|
|
69
69
|
);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
it("treats a missing data-volume as unity", () => {
|
|
73
|
-
const { binding, onSetAttributeQuiet } = bind({});
|
|
74
|
-
act(() => binding.onAutomateVolume());
|
|
75
|
-
expect(writtenAutomation(onSetAttributeQuiet).lanes[0].points[0].v).toBe(1);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
72
|
it("keeps FX lanes when adding the volume one", () => {
|
|
79
73
|
const automation = JSON.stringify({
|
|
80
74
|
version: 1,
|
|
@@ -108,13 +108,8 @@ export function useVolumeAutomation(
|
|
|
108
108
|
onCommitAt: (v: number) => writeLane((a) => withPointAt(a, target, clipTimeSec, v)),
|
|
109
109
|
};
|
|
110
110
|
};
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
// empty value as unity.
|
|
114
|
-
const raw = element.dataAttributes?.["volume"];
|
|
115
|
-
const parsed = raw ? Number(raw) : 1;
|
|
116
|
-
const current = Number.isFinite(parsed) ? parsed : 1;
|
|
117
|
-
const volume = laneBinding(VOLUME_TARGET, current, writeOwn);
|
|
111
|
+
// Unity: `data-volume` multiplies the lane, so seeding it there would play it twice.
|
|
112
|
+
const volume = laneBinding(VOLUME_TARGET, 1, writeOwn);
|
|
118
113
|
const rateAttr = Number.parseFloat(element.dataAttributes?.["playback-rate"] ?? "");
|
|
119
114
|
const rate = laneBinding(
|
|
120
115
|
RATE_TARGET,
|