feat(core): speed ramps as a rate lane in data-automation - #4129
Conversation
somanshreddy
left a comment
There was a problem hiding this comment.
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:208clamps every lane pointvto[MIN_PLAYBACK_RATE, MAX_PLAYBACK_RATE]= [0.1, 10] at parse/resolve time, sobuildTable/sourceTimeAtonly ever see rate ≥ 0.1.audioAutomation.ts:219uses log scale only whenmin > 0, else falls back to linear — a defensive guard against the same.pushskipst <= prevT, sotsis strictly increasing (no zero-width interpolation gap), and with rate ≥ 0.1ssis strictly increasing too → the inverse is well-formed. Boundary extrapolation (flatfirstRate/lastRateoutside 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
- 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. - It's coupled to #4143 and can't pass CI alone. The red
Testjob is one failure in@hyperframes/engine'svideoFrameExtractor.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. - Format is red on 4 of this PR's own files (
speedRamp.ts,clock.test.ts,media.test.ts,playbackRate.test.ts) — trivialbun 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
left a comment
There was a problem hiding this comment.
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:
Format/Preflightfail on this PR's own files —packages/core/src/{speedRamp,clock,runtime/media,runtime/playbackRate}.test.ts+speedRamp.ts. Needsbun run format(a new commit — which will move the head off6d5cda5e).Testfails (5m50s) on@hyperframes/engine'svideoFrameExtractor.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
left a comment
There was a problem hiding this comment.
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
videoFrameExtractorcoupling 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;Testpasses on this head. Format/Preflight/Test/studio-coreall 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.
…amp in the engine tests
18837f4 to
21241b4
Compare
What ships
A clip can carry a speed ramp: a
ratelane indata-automation(clip-local seconds, speed multiplier, 0.1 to 10, interpolated geometrically so 1x to 4x passes 2x halfway).sourceTimeAtinpackages/core/src/speedRamp.tsintegrates 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 constantdata-playback-ratestays the fallback. Pitch is preserved unlessdata-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 aratekind 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 --noEmitclean. Mutations checked: log to linear at both sampling sites, and droppingdata-automationfrom 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.