Skip to content

feat(studio): fade handles on the corners of an audio clip - #3347

Open
miguel-heygen wants to merge 2 commits into
feat/studio-precision-trim-viewfrom
feat/studio-clip-fade-handles
Open

feat(studio): fade handles on the corners of an audio clip#3347
miguel-heygen wants to merge 2 commits into
feat/studio-precision-trim-viewfrom
feat/studio-clip-fade-handles

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #3343, which is stacked on #3341. Only the last commit is
this PR. Before merging the stack, retarget each PR to main and rebase —
deleting a base branch on merge auto-closes the PR above it.

What

Fade handles on an audio clip's top corners. Drag one in to set the fade, drag
it back to the corner to remove it, double-click it to step the shape through
linear, smooth and sharp.

Dragging the fade grips on an audio clip: the wedges follow, then the curve steps to smooth

The clip draws the resulting curve, sampled through the same interpolator that
plays it back, so the wedge on screen is the fade you will hear.

Why

Fading a clip today means expanding its automation lane, drawing a selection box
over the stretch you want, and picking "Ramp up" from the shape menu. Every
other editor puts a grip on the clip corner. The data model for it already
exists here — this is the gesture it was missing.

How

  • A fade is not stored as a fade. It is the leading and trailing segment of
    the clip's ordinary volume envelope, so the grip reads back exactly the shape
    it wrote: draw a fade, expand the automation lane, and the breakpoints are
    there to edit by hand.
  • Reading is conservative. A head segment counts as a fade-in only if it
    starts on the clip's first frame, starts at silence, and rises to full level.
    A duck in the middle, or a ramp that starts at 40%, is somebody's automation
    and the grip reports no fade rather than offering to flatten it.
  • Writing rewrites only the head and tail, carrying every point the author
    placed in between across untouched — and dropping the lane entirely when the
    last fade is dragged away, so the clip goes back to carrying no attribute.
  • The gesture is local to the component rather than folded into the timeline's
    drag coordinator: a fade has no lane to change, nothing to snap to and nothing
    to collide with, so all of that machinery would sit unused. It writes through
    the automation binding the lane UI already uses — live preview on every move,
    one coalesced persist on release, so a whole drag is one undo step.
  • Grips only appear on the selected clip, matching the rule its automation lane
    already follows (an unselected clip's binding is read-only). The wedges are
    drawn either way, so you can see a fade you are not editing.

Test plan

  • Unit tests added/updated — 29 new tests across the fade math (reading,
    clamping, writing, wedge geometry) and the binding (audio-only gate,
    preview vs commit, lane removal, read-only, curve cycling). The wedge
    geometry test exists because the first version drew the fade-out mirrored
    and only a screenshot caught it; the endpoints are now pinned.

  • Manual testing performed — drove a real Chrome against Studio with an
    audio clip and read back the project file after each gesture:

    Step data-automation on disk
    Drag fade in to 2s volume: [{t:0,v:0},{t:2,v:1}]
    Drag fade out to 3s + [{t:13,v:1},{t:16,v:0}] (16s clip)
    Reload, re-read grips back at 2.04s / 2.96s — the round-trip holds
    Drag fade in back to 0 only the fade-out points remain
    Double-click the grip "curve":0.35 on both segments

    Grips are hidden until the clip is selected, and the wedges render at the
    right lengths:

    An audio clip with a 2.5s fade in and a 3.5s fade out, both on the smooth curve
  • Full packages/studio suite green (4340 tests), typecheck, oxlint, oxfmt.

Not covered

  • Visual clips. A div, image or video fades on opacity, which lives in the
    composition's GSAP animation rather than in this envelope. Per the call on
    this branch, that is a separate change — and the one-way part, since a minted
    tween carries no marker saying "this is the clip's fade", so the grip could
    create one but not recognise it later.
  • Rebasing the envelope when a clip is trimmed. Trim a faded clip shorter
    and its envelope still addresses the old length. Pre-existing for hand-drawn
    automation; the reader here clamps so the grip still shows the part of the
    fade inside the clip rather than claiming it vanished, but nothing rewrites
    the points. Worth its own change now that fades make it visible.
  • Keyboard. The grips carry slider semantics for screen readers but are not
    yet arrow-key adjustable.

Fading a clip meant expanding its automation lane, drawing a selection box
over the stretch you wanted and picking a shape from a menu. Every other
editor gives you a grip on the clip's corner; this adds one.

Drag it in to set the fade, drag it back to the corner to remove it, and
double-click it to step the shape through linear, smooth and sharp. The clip
draws the resulting curve, sampled through the same interpolator that plays
it back, so the wedge on screen is the fade you will hear.

A fade is not stored as a fade: it is the leading and trailing segment of the
clip's ordinary volume envelope, which is what makes the grip two-way — draw
one, open the lane, and the breakpoints are there. Reading is deliberately
conservative (a head segment counts only if it starts at the clip's first
frame, at silence, and rises to full level) and writing rewrites only the
head and tail, so a hand-drawn envelope survives being faded.

Audio clips only. A visual clip fades on opacity, which lives in the
composition's animation rather than in this envelope.
The wedge was one closed path carrying both the fill and the stroke, so the
fill's straight top and side got outlined too and the eye read a rectangle
butted onto the curve rather than one level line.

Split it: the level is an open path and the only thing stroked, the region it
takes away is that same line closed back through the clip's corner and never
stroked. Both come out of one function so they cannot drift apart, and the
endpoints are pinned by a test.
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.

1 participant