Skip to content

Time and ticks

Every time in a project is an integer number of ticks. This page explains the unit, the ranges built from it, the frame grid, and how a clip turns timeline time into source time. After reading it you can place clips exactly on frame boundaries at any frame rate.

A time is a MediaTime: a signed integer of ticks, 120,000 to the second. Integers keep arithmetic exact. Two hosts that add the same times get the same answer, with no floating-point drift between a browser and a server.

With 120,000 ticks a second, a frame of every common frame rate lasts a whole number of ticks, including the NTSC rates, whose frames last 1001/30000 of a second and similar:

Frame rate frame_rate One frame in ticks
23.976 { "num": 24000, "den": 1001 } 5005
24 { "num": 24, "den": 1 } 5000
25 { "num": 25, "den": 1 } 4800
29.97 { "num": 30000, "den": 1001 } 4004
30 { "num": 30, "den": 1 } 4000
50 { "num": 50, "den": 1 } 2400
59.94 { "num": 60000, "den": 1001 } 2002
60 { "num": 60, "den": 1 } 2000

So at 29.97 fps, frame 1 starts at exactly 4004 ticks and frame 30 at 120,120, with no rounding. A millisecond is 120 ticks, which keeps SubRip and WebVTT timestamps exact too.

A frame rate is at most one frame per tick (120,000 fps). Serialized times are at most 2^53 − 1 ticks either way, the largest integer a JavaScript number holds exactly, so every host reads the same JSON times.

A TimeRange is half-open: start is included, end is not. A clip with { "start": 0, "end": 120000 } covers the first second, and the next clip can start at 120000 without overlapping it. A range’s length is end - start.

Clips, transitions, caption cues and run times all use half-open ranges.

The frame rate divides the timeline into frames. Frame n starts at floor(n × 120000 × den / num) ticks and lasts until the next one starts. An export walks this grid: it renders one frame per grid step, at export quality, from the range’s start to its end. A still renders the frame of the grid that holds the requested time.

A change you put between two grid points shows at the next frame. The linter reports such edges (off_frame_boundary) and transitions that no frame shows (short_transition).

  • TypeScript (alpha2/project): secondsToTicks(seconds) and ticksToSeconds(ticks), and samplesToTicks and ticksToSamples for audio.
  • Python: alpha2.TICKS_PER_SECOND. The helpers take times in seconds, or as alpha2.Ticks for an exact tick count.
  • Rust (alpha2::time): TICKS_PER_SECOND, and on FrameRate, frame_start(frame) (the tick a frame starts at) and frame_at_or_before(time) (the frame whose interval holds a time).

secondsToTicks rounds to the nearest tick, so secondsToTicks(ticksToSeconds(t)) === t. Audio sample positions don’t always fall on ticks (at 48 kHz a sample lasts 2.5 ticks): samplesToTicks gives the tick at or before a sample, and ticksToSamples the sample that contains a tick.

The engine’s JavaScript API takes times as bigint (engine.renderAt(120_000n)); the project document holds them as JSON numbers.

A clip reads its source_range while it is on the timeline for its timeline_range. Its time_map says how:

  • forward (the default) stretches the source range over the timeline range. Speed is the ratio of their lengths: a source range twice as long as the timeline range plays at double speed.
  • reverse does the same from the end of the source range to its start: pictures in reverse order, and sound played backwards.
  • freeze shows one source_time for the whole clip, and is silent.
  • loop plays the source range at its own speed for as long as the timeline range lasts, repeat (every pass forward) or ping_pong (alternating directions).

Here is a 29.97 fps project with three clips of one camera file, every boundary on the frame grid (multiples of 4004):

[
{
"id": "fast",
"source_id": "camera",
"timeline_range": { "start": 0, "end": 360360 },
"source_range": { "start": 0, "end": 720720 }
},
{
"id": "backwards",
"source_id": "camera",
"timeline_range": { "start": 360360, "end": 480480 },
"source_range": { "start": 720720, "end": 840840 },
"time_map": { "type": "reverse" }
},
{
"id": "still",
"source_id": "camera",
"timeline_range": { "start": 480480, "end": 600600 },
"source_range": { "start": 840840, "end": 960960 },
"time_map": { "type": "freeze", "source_time": 900900 }
}
]
  • fast lasts 90 frames (360,360 ticks) and reads 180 frames’ worth of source: double speed.
  • backwards plays 30 frames of source in reverse.
  • still holds the source picture at 900,900 ticks for its whole length.

retime in alpha2/edit changes a clip’s length the way an editor does: forward and reversed clips change speed to fit the same source window, frozen clips hold longer or shorter, and loops play more or fewer passes. When a clip plays at a speed other than 1, its audio is resampled, or time-stretched to keep its pitch when audio.preserve_pitch is set.

A clip that shows a composition also maps time onto it, through ClipComposition:

composition time = offset + (t − clip start) × scale

for timeline time t, rounded down to a tick. offset defaults to 0 and scale to { "num": 1, "den": 1 }. Moving a clip keeps its offset, so its animation moves with it. Trimming its start by d adds d × scale to the offset, so the rest stays in place.

Animation explains which values are keyed in timeline time, composition time or picture time.