Skip to content

feat(editor): add playback speed multiplier for time-lapse export - #19

Open
adisagar2003 wants to merge 3 commits into
jkuri:mainfrom
adisagar2003:feat/timelapse-speed
Open

adisagar2003 wants to merge 3 commits into
jkuri:mainfrom
adisagar2003:feat/timelapse-speed

Conversation

@adisagar2003

@adisagar2003 adisagar2003 commented Sep 11, 2026 •

Copy link
Copy Markdown

What

Adds a time-lapse speed multiplier (1x–16x) to the editor's Video tab. The exported video is
shortened by that factor: duration_out = duration_in / speed.

There's currently no way to speed a recording up, so a long build-along or refactor session can
only be exported at 1x.

How

The three exporters are hand-rolled AVAssetReader → FrameRenderer → AVAssetWriter loops where
output time, composition time and source time are the same value. This change decouples the
writer clock from the source clock and nothing else:

totalFrames = TimeLapse.frameCount(duration: trimDuration, fps: fps, speed: speed)
outputTime  = CMTime(frameIndex, timescale: fps)          // writer PTS, unchanged
sourceTime  = outputTime * speed                          // reader cursor + renderer clock

FrameRenderer keeps receiving source-domain composition time, so cursor metadata, zoom
keyframes, captions, camera/video/spotlight regions and transition durations all keep working
untouched — no changes in RegionRemapping, CompositionInstruction or FrameRenderer. That's
why this seam was chosen over composition.scaleTimeRange(...), which would desync those
domains. reader.timeRange still covers the full trim, so every source sample is still read.

Speed above 1x:

  • forces the compositor path (checkNeedsCompositor) — passthrough can't resample;
  • drops audio entirely: system, mic, mic noise reduction and click sounds. Sped-up audio isn't
    usable and time-scaling it was deliberately removed in v0.14.6 (22dc93c), so it isn't
    reintroduced here.

Preview applies the same multiplier to the player rate (multiplied onto the existing drift
ratios, not assigned over them) and mutes audio above 1x. The timeline stays in source seconds —
only the export gets shorter — and the panel shows the resulting length.

Tests

TimeLapse holds all the arithmetic and is covered by unit tests, including the two cases that
would otherwise bite: degenerate speeds (0 / negative / NaN → division by zero) and the last
frame never reading past the source duration.

⚠️ This adds the repo's first test target (ReframedTests, host-less so it doesn't launch the
app, Swift Testing) plus make test. Happy to split that into its own PR, or drop it, if you'd
rather keep the project test-free.

Verification status

Not compiled by me: I don't have Xcode on this machine, only the Command Line Tools, so
make build / make test never ran here. Flagging that up front rather than burying it. What
was verified:

  • TimeLapse logic: unit tests run green through a standalone SPM harness, and mutation-checked
    (flipping ceil→floor and dropping the speed factor both make them fail).
  • Every changed file passes swiftc -parse, and swift format is clean.

Still needs a real build + an export check:

  • 4x on a ~30 s recording → ≈7.5 s and no audio stream (ffprobe), with cursor/zoom/captions
    still landing on the same on-screen events
  • all three export paths (parallel, normal, GIF), plus a 1x regression (still passthrough)
  • trim and/or a cut region combined with speed → cut points still land correctly
  • preview at 8x/16x — AVPlayer can refuse rates above 2 for items that don't support fast
    forward, which would silently stall playback rather than error

Not included

  • Per-range speed on the timeline (Screen-Studio style)
  • Capture-time low-fps time-lapse
  • Pitch-corrected sped-up audio

adisagar2003 and others added 3 commits September 11, 2026 16:24
Pure output-frame to source-time mapping used by the exporters, plus the
repo's first test target (host-less, Swift Testing) and a make test target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a 1x-16x speed setting to the editor's Video tab. Export keeps reading
every source sample but emits fewer frames: writer PTS stays frameIndex/fps
while the reader cursor and renderer clock advance at outputTime * speed, so
cursor, zoom, captions and all regions keep being sampled in source time and
need no remapping. Speed above 1x forces the compositor path and drops audio
(system, mic, noise reduction and click sounds), since sped-up audio is
unusable and time-scaling it was removed in v0.14.6.

Preview applies the same multiplier to the player rate. The timeline stays in
source seconds; the panel shows the resulting export length.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…k sounds

- clamp(-.infinity) returned 16x instead of 1x
- speed panel reported the untrimmed asset length as the export length; it
  now uses the same source span the exporter does (trim, or video regions)
- click sound generation guards on speed itself instead of being passed a
  nil cursor snapshot, matching how mic noise reduction opts out
- needsCompositor compares the clamped speed, so an out-of-range value in
  project.json no longer forces a pointless re-encode
- footnote no longer claims audio was removed from silent recordings
- test target gets runpath search paths; added a direct clamp test

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@adisagar2003
adisagar2003 marked this pull request as ready for review September 11, 2026 20:25
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