Skip to content

feat(core): speed ramps as a rate lane in data-automation - #4129

Merged
miguel-heygen merged 3 commits into
mainfrom
feat/speed-ramp-core
Sep 19, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
feat/speed-ramp-core

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What ships

A clip can carry a speed ramp: a rate lane in data-automation (clip-local seconds, speed multiplier, 0.1 to 10, interpolated geometrically so 1x to 4x passes 2x halfway). sourceTimeAt in packages/core/src/speedRamp.ts integrates the lane to source time and is the one mapping the runtime uses for media seeking, instantaneous playback rate, natural duration, child timelines and the audio-master clock. A constant data-playback-rate stays the fallback. Pitch is preserved unless data-preserve-pitch="false". Ramped clips play through the media element, not decoded buffers, so pitch is not shifted. Lint warns on a rate lane outside video and audio (speed_ramp_on_non_media).

The clip clamp is 0.1 to 10 in playbackRateBounds.ts; the player transport rate keeps its own clamp.

Merge order

The render engine does not read the lane yet, so this PR must merge back-to-back with the engine parity PR; preview and render must not disagree on a frame.

No visible change

One line touches packages/studio (audioFxRevealTarget.ts): the widened automation target union gains a rate kind and the FX reveal lookup returns null for it. No UI path changes; the Studio rate lane comes in a later PR with its own captures.

Verification

Devbox: core 136 files, lint 16 files, core and studio tsc --noEmit clean. Mutations checked: log to linear at both sampling sites, and dropping data-automation from the duration-floor attributes, both fail tests.

Not covered here

Engine frame extraction and audio mixing, the Studio lane, docs, and a real-browser check of native audio at extreme rates.

@somanshreddy somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed at 6d5cda5e, focused on the core speedRamp.ts time-mapping since that's the shared piece. The logic is sound; the hold is on state, not code.

Core logic — verified sound

sourceTimeAt(spec, t) is the trapezoidal integral of a log-interpolated rate lane (48 cells/segment), and timeAtSourceTime its inverse via the same (ts, ss) table. The correctness hazard here is a rate ≤ 0 anywhere — log-sampling needs v > 0 (log(0)/log(neg) → NaN), and the inverse interpolate(ss, …) divides by adjacent-ss gaps, so a non-positive rate would give NaN / divide-by-zero / non-monotonic source time. That's closed at the sink, not just in lint:

  • speedRamp.ts:208 clamps every lane point v to [MIN_PLAYBACK_RATE, MAX_PLAYBACK_RATE] = [0.1, 10] at parse/resolve time, so buildTable/sourceTimeAt only ever see rate ≥ 0.1.
  • audioAutomation.ts:219 uses log scale only when min > 0, else falls back to linear — a defensive guard against the same.
  • push skips t <= prevT, so ts is strictly increasing (no zero-width interpolation gap), and with rate ≥ 0.1 ss is strictly increasing too → the inverse is well-formed. Boundary extrapolation (flat firstRate/lastRate outside the lane) is consistent between the forward and inverse.

The new lint rule speed_ramp_on_non_media (warning: a rate lane on a non-<video>/<audio> element is inert) is a sensible advisory. One shared function for runtime + Studio (+ engine in #4143) is the right shape.

Why I'm not stamping

  1. This is a GitHub draft (isDraft: true, merge state UNKNOWN). A draft can't be merged, so "one approval clears the gate / the loop presses merge" can't actually fire on this head.
  2. It's coupled to #4143 and can't pass CI alone. The red Test job is one failure in @hyperframes/engine's videoFrameExtractor.test.ts — the extractor is exactly the engine half #4143 implements. So #4129's own CI is red by construction until #4143 lands; per the body they "merge together." This should be evaluated as the #4129 + #4143 pair (a solo pass on #4129 is blind to the half that makes the extractor honor the ramp). #4143 is tai's.
  3. Format is red on 4 of this PR's own files (speedRamp.ts, clock.test.ts, media.test.ts, playbackRate.test.ts) — trivial bun run format. (The "devbox core/lint/typecheck green" line doesn't match CI here — Format and Test are red.)

Net: the format+runtime half's core math is correct and fail-safe; hold is because it's a draft, its CI red is the #4143-shaped coupling, and format needs a pass. Ready to move on the pair once it's marked ready, #4143 lands the extractor half, and format is green.

Codex disabled per workspace cap — manual pass only.

@somanshreddy somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Out of draft — good, that removes the can't-merge-a-draft block. But "ready for the stamp" doesn't hold yet: at this same head 6d5cda5e the CI is unchanged from my review (same run), still red on the two real blockers, so there's nothing green to stamp:

  1. Format / Preflight fail on this PR's own files — packages/core/src/{speedRamp,clock,runtime/media,runtime/playbackRate}.test.ts + speedRamp.ts. Needs bun run format (a new commit — which will move the head off 6d5cda5e).
  2. Test fails (5m50s) on @hyperframes/engine's videoFrameExtractor.test.ts — the extractor honoring the rate lane is the #4143 engine half, which stacks on #4129. So #4129 can't go green alone at this head; the pair has to land together (or the extractor test has to tolerate the pre-#4143 state).

My earlier code review stands unchanged: the core speedRamp.ts mapping is sound (rate clamped to [0.1,10] at the sink, log-safe, monotonic, proper inverse). The hold is purely CI state + the #4143 coupling — not the logic.

Path to a stamp: format the four files (new commit), and resolve the #4143 coupling so Test is green (merge the pair together, or make the extractor test pass without the engine half). Re-verify me at the new head once Test and Format are green — I won't stamp a red Test here, since that's the job validating this PR's own core.

Codex disabled per workspace cap — manual pass only.

@somanshreddy somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving at 18837f45 — both blockers I held on are fixed and the run is fully green (0 failure conclusions, 0 pending; out of draft).

  • Format green — the four unformatted files (speedRamp.ts + its test siblings) are now formatted.
  • The videoFrameExtractor coupling is resolved — the engine tests now read the rate range from core instead of literals, so #4129 no longer fails a test that depended on #4143's half; Test passes on this head. Format/Preflight/Test/studio-core all green.

My core-logic review carries: sourceTimeAt/timeAtSourceTime are a correct trapezoidal integral of a log-interpolated rate lane, fail-safe because every point is clamped to [0.1,10] at the sink (line ~208) — log-safe, monotonic, proper inverse; resolveRateSpec/readPreservePitch sound. merge is BLOCKED only on REVIEW_REQUIRED. Approving — this is the base of the speed-ramp stack.

Codex disabled per workspace cap — manual pass only.

A clip can carry a rate lane in data-automation. sourceTimeAt integrates it to source time and is the one mapping the runtime uses for media seeking, playback rate, natural duration, child timelines and the audio-master clock. Rate is clamped to 0.1-10 in one place, pitch is preserved unless data-preserve-pitch is false, and ramped clips play through the media element. Lint warns on a rate lane outside video and audio.
@miguel-heygen
miguel-heygen merged commit 7f452ad into main Sep 19, 2026
61 checks passed
@miguel-heygen
miguel-heygen deleted the feat/speed-ramp-core branch September 19, 2026 10:27
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