콘텐츠로 이동

시간과 틱

프로젝트의 모든 시간은 틱 단위의 정수입니다. 이 페이지에서는 이 단위와 이 단위로 만든 구간, 프레임 그리드, 그리고 클립이 타임라인 시간을 소스 시간으로 바꾸는 방식을 설명합니다. 다 읽고 나면 어떤 프레임 레이트에서든 클립을 프레임 경계에 정확히 놓을 수 있습니다.

시간은 MediaTime입니다. 부호 있는 정수이고, 1초가 120,000틱입니다. 정수를 쓰면 산술이 정확합니다. 두 호스트가 같은 시간을 더하면 같은 답이 나오고, 브라우저와 서버 사이에 부동소수점 오차가 쌓이지 않습니다.

120,000틱을 쓰면 NTSC 레이트를 포함한 흔한 프레임 레이트에서 프레임 하나의 길이가 정수 틱이 됩니다. NTSC 프레임의 길이는 1001/30000초처럼 분자에 1001이 들어갑니다.

프레임 레이트 frame_rate 프레임 하나의 틱 수
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

따라서 29.97fps에서 1번 프레임은 정확히 4004틱에서, 30번 프레임은 120,120틱에서 반올림 없이 시작합니다. 1밀리초는 120틱이므로 SubRip과 WebVTT 타임스탬프도 정확히 표현됩니다.

프레임 레이트는 최대 1틱당 1프레임(120,000fps)입니다. 직렬화된 시간은 양쪽 모두 최대 2^53 − 1틱으로, JavaScript 숫자가 정확히 담을 수 있는 가장 큰 정수입니다. 그래서 모든 호스트가 JSON의 시간을 똑같이 읽습니다.

TimeRange는 반열린 구간입니다. start는 포함하고 end는 포함하지 않습니다. { "start": 0, "end": 120000 }인 클립은 첫 1초를 차지하고, 다음 클립은 겹치지 않고 120000에서 시작할 수 있습니다. 구간의 길이는 end - start입니다.

클립, 트랜지션, 자막 큐, 런(run) 시간이 모두 반열린 구간을 씁니다.

프레임 레이트는 타임라인을 프레임으로 나눕니다. n번 프레임은 floor(n × 120000 × den / num)틱에서 시작해 다음 프레임이 시작할 때까지 이어집니다. export는 이 그리드를 따라갑니다. 구간의 시작부터 끝까지 그리드 한 칸마다 프레임 하나를 export 품질로 렌더링합니다. 스틸은 요청한 시각을 포함하는 그리드 프레임을 렌더링합니다.

두 그리드 지점 사이에 넣은 변화는 다음 프레임에서 나타납니다. 린터는 이런 경계(off_frame_boundary)와 어떤 프레임에도 보이지 않는 트랜지션(short_transition)을 알려 줍니다.

  • TypeScript(alpha2/project): secondsToTicks(seconds)와 ticksToSeconds(ticks), 오디오용으로 samplesToTicks와 ticksToSamples.
  • Python: alpha2.TICKS_PER_SECOND. 도우미 함수는 시간을 초 단위로 받거나, 정확한 틱 수가 필요하면 alpha2.Ticks로 받습니다.
  • Rust(alpha2::time): TICKS_PER_SECOND, 그리고 FrameRate의 frame_start(frame)(프레임이 시작하는 틱)과 frame_at_or_before(time)(어떤 시각을 포함하는 프레임).

secondsToTicks는 가장 가까운 틱으로 반올림하므로 secondsToTicks(ticksToSeconds(t)) === t가 성립합니다. 오디오 샘플 위치가 항상 틱에 떨어지지는 않습니다(48kHz에서 샘플 하나는 2.5틱). samplesToTicks는 샘플 위치와 같거나 그 앞의 틱을, ticksToSamples는 그 틱을 포함하는 샘플을 돌려줍니다.

엔진의 JavaScript API는 시간을 bigint로 받습니다(engine.renderAt(120_000n)). 프로젝트 문서에서는 JSON 숫자로 저장합니다.

타임라인 시간에서 소스 시간으로

섹션 제목: “타임라인 시간에서 소스 시간으로”

클립은 timeline_range 동안 타임라인에 머물면서 source_range를 읽습니다. 그 방식은 time_map이 정합니다.

  • forward(기본값): 소스 구간을 타임라인 구간에 맞춰 늘이거나 줄입니다. 속도는 두 길이의 비율입니다. 소스 구간이 타임라인 구간보다 두 배 길면 2배속으로 재생됩니다.
  • reverse: 같은 방식으로 소스 구간의 끝에서 시작으로 재생합니다. 그림은 역순으로, 소리는 거꾸로 재생됩니다.
  • freeze: 클립 전체에서 source_time 한 순간을 보여 주며, 소리는 나지 않습니다.
  • loop: 타임라인 구간이 이어지는 동안 소스 구간을 원래 속도로 반복합니다. repeat(매번 정방향) 또는 ping_pong(정방향과 역방향을 번갈아)을 고릅니다.

다음은 카메라 파일 하나로 클립 세 개를 만든 29.97fps 프로젝트입니다. 모든 경계가 프레임 그리드(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는 90프레임(360,360틱) 길이이고, 소스에서 180프레임 분량을 읽습니다. 2배속입니다.
  • backwards는 소스 30프레임을 역방향으로 재생합니다.
  • still은 클립 길이 내내 900,900틱 지점의 소스 그림을 보여 줍니다.

alpha2/edit의 retime은 에디터처럼 클립 길이를 바꿉니다. 정방향과 역방향 클립은 같은 소스 구간에 맞게 속도를 바꾸고, 정지 클립은 더 길게 또는 짧게 멈추며, 반복 클립은 반복 횟수가 바뀝니다. 속도가 1이 아닌 클립의 오디오는 리샘플링되며, audio.preserve_pitch를 켜면 음높이를 유지하도록 타임 스트레칭됩니다.

컴포지션을 보여 주는 클립은 시간을 컴포지션에도 대응시킵니다. 이는 ClipComposition이 정합니다.

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

여기서 t는 타임라인 시간이며, 결과는 틱 단위로 내림합니다. offset의 기본값은 0, scale의 기본값은 { "num": 1, "den": 1 }입니다. 클립을 옮겨도 offset은 그대로이므로 애니메이션이 클립과 함께 움직입니다. 클립 시작을 d만큼 트림하면 offset에 d × scale이 더해져 나머지 부분은 제자리에 남습니다.

어떤 값이 타임라인 시간, 컴포지션 시간, 픽처 시간 중 무엇으로 키프레임되는지는 애니메이션에서 설명합니다.