Skip to content

feat(studio): audio gain on G, speed in clip names, additive selection and partner clamp (8/8) - #4821

Open
vanceingalls wants to merge 18 commits into
aov/07-sync-origin-menu-groupsfrom
aov/08-gain-names-multiselect
Open

vanceingalls wants to merge 18 commits into
aov/07-sync-origin-menu-groupsfrom
aov/08-gain-names-multiselect

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

Summary

The Premiere polish pass:

  • clip names show speed as [150%] / [ramp]
  • one fx badge per clip
  • an Audio Gain dialog on G
  • Link for any unlinked video + audio pair
  • Shift/Cmd-click additive selection
  • audio kept inside its partner video while dragging and trimming
  • Look previews on hover
  • Unlink leaves nothing selected

Changes

  • studio:
    • [NN%] / [ramp] clip names, and a single fx badge in place of the per-tool and link badges.
    • AudioGainDialog on G (one undo step). Shortcuts are kept out of the modal, and it refuses while recording.
    • Link for exactly one unlinked video + one unlinked audio (same source → sync origin).
    • Shift/Cmd-click add to the selection.
    • An audio clip with a partner video (linked or same sync origin) can't move or trim outside the video span, and the drag actor holds at the clamp.
    • Look ▸ previews on hover and commits on click.
    • Unlink (menu, ⌘L, Alt-edit) clears the timeline selection.

Testing

Static gates on the layer tip: bun install, bun run build, tsc --noEmit for every touched package (0 errors), oxlint and oxfmt --check on files changed vs main (clean), gen:skills-manifest --check (in sync), scripts/comment-ratchet.mjs (ok).

  • Scoped vitest: sdk 574, studio 6434. All pass. This tip's tree equals the full feature branch.
  • Mutation check: removing the unlink clearSelection fails the new "unlink leaves nothing selected" test.

Notes

  • Root cause of "after unlinking, trimming the audio still moves the video": setResizingClip seeds the trim gesture from the whole selection. After Unlink both clips stayed selected, so upstream's multi-select group resize trimmed both. Unlink now clears the selection. Trimming with both clips deliberately selected still trims both, by design (upstream's restored multi-select group resize).
  • Studio's Shift-click time-range popover is replaced by Shift/Cmd additive selection (Premiere convention). Flagged for a product decision.
  • Part of the audio-on-video stack. See the bottom PR for the overview.

🤖 Generated with Claude Code

@vanceingalls
vanceingalls added this pull request to stack #4822 October 1, 2026 01:24

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the #4821 delta at exact 49a5b9b0e2341e69d63d83eed3e65552b43aef23 against stacked #4820 ca563f775fcd9f0adba2c468f538f1dca6d6d037. Two inline holds: SDK linked-duration edits can push an offset audio partner past the video endpoint; and Studio's detached-audio containment runs before collision placement (and only for the grabbed element), so the final moved audio can escape the video. Current-head Comments is also red on five raised-comment-share files. #4820 and earlier stack layers remain held; this review is of this incremental delta only. No local browser, tests, merge, or release action. — Jerrai

Comment thread packages/sdk/src/engine/mutate.ts Outdated
const endsTogether =
Math.abs(pStart + (partner.duration ?? 0) - (gStart + (grabbed.duration ?? 0))) <
ALIGN_EPSILON_S;
if (edit.duration !== undefined && endsTogether) timing.duration = edit.duration;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When linked clips share an end but have different starts, copying the grabbed clip's duration moves the partner's end. Example: video [2,8] (duration 6) and audio [3,8] (duration 5), both linked. session.setTiming(video, {duration:4}) applies video [2,6] and then this sets audio duration 4 → [3,7], so the audio extends one second past its video even though this delta promises it stays inside. The new session test checks linked start movement; the duration test uses equal starts, which cannot catch the offset. Preserve the common end (derive partner duration from its own start and the new end) and add an unequal-start test.

const previewStart = video
? clampStartIntoBounds(floored, drag.element.duration, video)
: floored;
const placement = resolveDropPlacement(drag, clientY, previewStart, nextMove.track, ctx, group);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detached-audio/video bound is applied to previewStart, but resolveDropPlacement can move it outside that bound to avoid another clip, and line 200 returns placement.start without reclamping or rejecting it. Concrete same-lane case: video [10,30], its audio [12,20] (duration 8), obstacle [20,30]; dragging audio toward 22 clamps to 22 (the last bounded start), but nearest-free placement prefers 30 over 12, and timelineClipDragCommit.ts:240–244 persists audio [30,38]. Also, the bound is computed only for drag.element, not audio carried as a passenger of a multi-selection; dragging selected text and audio moves the audio via resolveMultiSelection without its own bounds. Please constrain final placement and each held audio mover, and test collision and non-audio-grabbed multi-select cases.

@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from 49a5b9b to fb5c6b6 Compare October 1, 2026 05:46
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Edit accuracy: 530 passing here, 530 on the base branch

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from fb5c6b6 to a64a9ad Compare October 1, 2026 06:39

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed #4821 at exact a64a9ad1a50bb07930492362c083ba1a9d77f3f4 against current #4820. The unequal-start/shared-end duration repair and the Studio collision/passenger clamp address the previous examples; Comments and SDK/Studio checks are green. One boundary in the duration repair still breaks the linked containment promise (inline): a valid short video-duration edit can end before the audio starts, the computed partner duration becomes nonpositive and is silently skipped, leaving the old audio span beyond the video. Please define and test the crossing behavior. Current-head Studio/player captures and file-size checks also fail; no live UI or audio validation was performed. Other stack layers remain separately held. No merge or release action. — Jerrai

Comment thread packages/sdk/src/engine/mutate.ts Outdated
const partnerEnd = (partner.start ?? 0) + (partner.duration ?? 0);
if (Math.abs(partnerEnd - grabbedEnd) >= ALIGN_EPSILON_S) return undefined;
const duration = (edit.start ?? grabbedStart) + edit.duration - partnerStart;
return duration > 0 ? duration : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repairs unequal-start durations only while the new shared endpoint remains after the partner's start. With linked video [2,8] and audio [3,8], setTiming(video,{duration:0.5}) makes the video [2,2.5], computes partner duration -0.5, and silently returns undefined; applySetTiming commits the video but leaves the audio [3,8] outside it. No SDK rejection runs before the first mutation. Please reject this edit atomically, remove/retime the partner under an explicit policy, or otherwise preserve the linked/containment invariant, with a regression at this crossing boundary.

@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from a64a9ad to 73fc900 Compare October 1, 2026 08:20
vanceingalls and others added 18 commits October 1, 2026 02:13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dges

Grey with no effects, white with any. The tooltip lists look, voice, other
fx, crop and ducking; a click opens the clip menu. The volume badge stays
when volume is off 100%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Set Gain to, Adjust Gain by, Normalize Max Peak to, Normalize All Peaks to
and Normalize loudness to -16 LUFS, with the selection's peak amplitude read
from the peak map. Writes data-volume on every selected clip with sound as
one undo step. The clip menu's Normalize loudness becomes Audio Gain... (G).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… recording

Keys pressed inside an aria-modal dialog no longer reach the global hotkeys,
so Delete on the focused OK button cannot delete the clips the dialog edits.
The multi-clip write refuses during a recording like the single-clip one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Link no longer requires the same file and identical timing, matching Premiere.
Same-file pairs get a sync origin so a trimmed or moved audio shows its
offset; different files link without one. Merge back is disabled until the
pair is in sync. Linked moves keep the offset, and a trim only carries to a
partner whose edge is at the same time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A modifier click on a clip replaced the selection, so a video and an audio
could never both be selected and Link never appeared. Shift, Cmd and Ctrl
now toggle the clip (and its link partners unless Alt) in the selection;
Shift no longer turns a clip click into a time-range popover.

Right-clicking a lone unlinked video or audio offers Link when exactly one
opposite-kind clip from the same file sits on another track.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dd in every host

A modifier click passed the toggle as an argument to an optional
onSelectElement call, so hosts without that handler (desktop) never ran it.

An audio clip linked to a video, or sharing its sync origin, now stays
inside that video's span while dragged or trimmed, Alt edits included,
unless the video moves or trims in the same gesture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dragged audio's ghost followed the pointer past the video's end while
the landing stayed clamped; it now stops where the clip will land.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Unlink both clips stayed selected, so the next edge trim ran the
multi-select group resize and the former partner still followed. Unlink
from the menu, the shortcut or an Alt edit now leaves nothing selected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… duration

Copying the grabbed clip's duration to a partner that starts later pushed the
partner's end past the clip both ended on. The partner's duration is now the
shared new end minus its own start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… placement too

The containment clamp ran before collision placement and only for the
grabbed clip, so a drop pushed past an obstacle, or audio carried along in a
multi-selection, could land outside its video. The allowed shift is now the
intersection over every moving audio, and a placement outside it is
rejected. Link partners are matched within their source file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ain dialog

Normalize moved from the clip menu into the Audio Gain dialog here, so the
volume-lane refusal and the saved/refused/failed checks go with it: a lane
owned clip is refused, and a failed single or multi-clip save rejects instead
of reporting success.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s start

With linked video [2,8] and audio [3,8], setTiming(video, {duration: 0.5})
computed a partner duration of -0.5, skipped it, and committed the video alone,
leaving the audio outside its video. The partner edits are now planned before
any mutation; a non-positive partner duration throws ("Linked audio would start
after the new end — unlink or trim the audio first.") and can() reports
E_LINKED_PARTNER_CROSSED, so nothing is written and the undo stack is
untouched. In Studio the refusal fails the cutover (never declines into the
legacy path) and the timeline resize toast shows the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from 73fc900 to e8968fb Compare October 1, 2026 09:47

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants