-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtimecode.go
More file actions
330 lines (298 loc) · 10.1 KB
/
Copy pathtimecode.go
File metadata and controls
330 lines (298 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
package timecode
import (
"fmt"
"strings"
"time"
)
// Timecode is a signed frame position at a given rate. Frame 0 is
// 00:00:00:00. Negative values are legal and are printed with a leading minus
// sign, which is what a subtraction that runs past the start of the day
// produces.
type Timecode struct {
rate Rate
frames int
}
// ParseError describes a timecode string that is not legal at a given rate.
type ParseError struct {
Input string
Rate Rate
Reason string
}
func (e *ParseError) Error() string {
if e.Rate.Valid() {
return fmt.Sprintf("bad timecode %q at %s: %s", e.Input, e.Rate.name, e.Reason)
}
return fmt.Sprintf("bad timecode %q: %s", e.Input, e.Reason)
}
// FromFrames returns the timecode at the given frame position.
func FromFrames(frames int, r Rate) Timecode {
return Timecode{rate: r, frames: frames}
}
// Parse reads a timecode written as HH:MM:SS:FF or HH:MM:SS;FF.
//
// Either separator is accepted before the frames field whatever the rate, so
// material logged with the wrong punctuation still reads. The separator that
// comes back out of String is the one the rate calls for.
//
// A drop frame rate rejects the labels that drop frame skips. At 29.97df the
// labels ;00 and ;01 do not exist at the start of any minute except every
// tenth minute, so 00:01:00;01 is not a timecode and is refused here.
func Parse(s string, r Rate) (Timecode, error) {
if !r.Valid() {
return Timecode{}, fmt.Errorf("cannot parse %q: no frame rate given", s)
}
raw := s
body := strings.TrimSpace(s)
negative := false
if strings.HasPrefix(body, "-") {
negative = true
body = body[1:]
}
fields, reason := splitFields(body)
if reason != "" {
return Timecode{}, &ParseError{Input: raw, Rate: r, Reason: reason}
}
names := [4]string{"hours", "minutes", "seconds", "frames"}
var value [4]int
for i, field := range fields {
if len(field) != 2 {
return Timecode{}, &ParseError{
Input: raw,
Rate: r,
Reason: fmt.Sprintf("%s field %q must be exactly two digits", names[i], field),
}
}
n := 0
for j := 0; j < 2; j++ {
c := field[j]
if c < '0' || c > '9' {
return Timecode{}, &ParseError{
Input: raw,
Rate: r,
Reason: fmt.Sprintf("%s field %q must be two digits", names[i], field),
}
}
n = n*10 + int(c-'0')
}
value[i] = n
}
h, m, sec, f := value[0], value[1], value[2], value[3]
switch {
case h > 23:
return Timecode{}, &ParseError{Input: raw, Rate: r,
Reason: "hours must be 00 to 23 because timecode rolls over at 24:00:00:00"}
case m > 59:
return Timecode{}, &ParseError{Input: raw, Rate: r, Reason: "minutes must be 00 to 59"}
case sec > 59:
return Timecode{}, &ParseError{Input: raw, Rate: r, Reason: "seconds must be 00 to 59"}
case f >= r.nominal:
return Timecode{}, &ParseError{Input: raw, Rate: r,
Reason: fmt.Sprintf("frames must be 00 to %02d because %s counts %d labels per second",
r.nominal-1, r.name, r.nominal)}
}
if r.drop > 0 && sec == 0 && f < r.drop && (h*60+m)%10 != 0 {
return Timecode{}, &ParseError{Input: raw, Rate: r,
Reason: fmt.Sprintf("frames %s do not exist at %02d:%02d:00 because drop frame skips %s labels "+
"at the start of every minute except every tenth minute; the first legal frame here is %02d",
labelRange(r.drop), h, m, numberWord(r.drop), r.drop)}
}
frames := r.labelIndex(h, m, sec, f) - r.droppedBefore(h, m)
if negative {
frames = -frames
}
return Timecode{rate: r, frames: frames}, nil
}
// splitFields cuts a timecode body into its four fields. Both ':' and ';' act
// as separators. It returns a reason string when the shape is wrong.
func splitFields(s string) ([4]string, string) {
var out [4]string
if s == "" {
return out, "the timecode is empty; the form is HH:MM:SS:FF"
}
count := 0
start := 0
for i := 0; i < len(s); i++ {
if c := s[i]; c == ':' || c == ';' {
if count == 3 {
return out, "too many separators; the form is HH:MM:SS:FF"
}
out[count] = s[start:i]
count++
start = i + 1
}
}
if count != 3 {
return out, fmt.Sprintf("expected four fields separated by : or ; but found %d; the form is HH:MM:SS:FF", count+1)
}
out[3] = s[start:]
return out, ""
}
// labelRange names the labels a drop frame rate skips.
func labelRange(drop int) string {
if drop == 2 {
return "00 and 01"
}
return fmt.Sprintf("00 to %02d", drop-1)
}
// numberWord spells the small counts that appear in messages.
func numberWord(n int) string {
switch n {
case 2:
return "two"
case 4:
return "four"
default:
return fmt.Sprint(n)
}
}
// Rate returns the rate the timecode was built at.
func (t Timecode) Rate() Rate { return t.rate }
// Frames returns the signed frame position. This is the absolute frame number
// counted from 00:00:00:00, and it is the number every other operation works
// from.
func (t Timecode) Frames() int { return t.frames }
// Fields returns the four timecode fields and whether the position is
// negative. The fields always describe the magnitude, so -00:00:00:01 comes
// back as 0, 0, 0, 1 with negative set.
func (t Timecode) Fields() (h, m, s, f int, negative bool) {
n := t.frames
if n < 0 {
negative = true
n = -n
}
h, m, s, f = t.rate.split(n)
return h, m, s, f, negative
}
// String prints the timecode with the separator its rate calls for.
func (t Timecode) String() string {
if !t.rate.Valid() {
return "<invalid>"
}
h, m, s, f, negative := t.Fields()
sign := ""
if negative {
sign = "-"
}
return fmt.Sprintf("%s%02d:%02d:%02d%c%02d", sign, h, m, s, t.rate.Separator(), f)
}
// LabelIndex returns how many labels have been counted at the nominal rate
// from 00:00:00:00 to here, including the labels drop frame never uses. At
// 29.97df, 01:00:00;00 has label index 108000 and frame count 107892.
func (t Timecode) LabelIndex() int {
h, m, s, f, negative := t.Fields()
n := t.rate.labelIndex(h, m, s, f)
if negative {
return -n
}
return n
}
// DroppedLabels returns how many labels drop frame has skipped between
// 00:00:00:00 and here. It is zero at every non drop rate.
func (t Timecode) DroppedLabels() int { return t.LabelIndex() - t.frames }
// Add returns t plus u. Both must be at the same rate.
func (t Timecode) Add(u Timecode) (Timecode, error) {
if t.rate != u.rate {
return Timecode{}, fmt.Errorf("cannot add %s at %s to %s at %s: the rates differ",
u, u.rate, t, t.rate)
}
return Timecode{rate: t.rate, frames: t.frames + u.frames}, nil
}
// Sub returns t minus u. Both must be at the same rate. The result may be
// negative.
func (t Timecode) Sub(u Timecode) (Timecode, error) {
if t.rate != u.rate {
return Timecode{}, fmt.Errorf("cannot subtract %s at %s from %s at %s: the rates differ",
u, u.rate, t, t.rate)
}
return Timecode{rate: t.rate, frames: t.frames - u.frames}, nil
}
// AddFrames returns the timecode moved by n frames, which may be negative.
func (t Timecode) AddFrames(n int) Timecode {
return Timecode{rate: t.rate, frames: t.frames + n}
}
// Wrap folds the timecode into a single day and reports how many whole days
// were taken off. A position one frame past 23:59:59;29 at 29.97df comes back
// as 00:00:00;00 with one day. A position one frame before 00:00:00:00 comes
// back as the last frame of the day with minus one day.
func (t Timecode) Wrap() (Timecode, int) {
day := t.rate.FramesPerDay()
days := t.frames / day
rest := t.frames % day
if rest < 0 {
rest += day
days--
}
return Timecode{rate: t.rate, frames: rest}, days
}
// Nanos returns the real elapsed time from 00:00:00:00 to here, in
// nanoseconds. At 29.97df, 01:00:00;00 is 3599.9964 seconds and not 3600.
func (t Timecode) Nanos() int64 { return t.rate.Nanos(t.frames) }
// Duration returns the real elapsed time from 00:00:00:00 to here.
func (t Timecode) Duration() time.Duration { return time.Duration(t.Nanos()) }
// LabelNanos returns the time the timecode would represent if the four fields
// were read as a clock, which is the label index divided by the nominal rate.
// At 29.97df, 01:00:00;00 reads as exactly 3600 seconds.
func (t Timecode) LabelNanos() int64 {
return ratioNanos(int64(t.LabelIndex()), int64(t.rate.nominal))
}
// DriftNanos returns the label time minus the real elapsed time. A positive
// result means the timecode label reads later than the wall clock, so the
// label is running fast. A negative result means the label is running slow.
//
// The whole expression is kept as one fraction so that no rounding happens
// before the final conversion to nanoseconds:
//
// drift = labelIndex/nominal - frames*den/num
// = (labelIndex*num - frames*den*nominal) / (num*nominal)
func (t Timecode) DriftNanos() int64 {
r := t.rate
if !r.Valid() {
return 0
}
numer := int64(t.LabelIndex())*int64(r.num) - int64(t.frames)*int64(r.den)*int64(r.nominal)
denom := int64(r.num) * int64(r.nominal)
return ratioNanos(numer, denom)
}
// ConvertTo returns the position at another rate that covers the same real
// elapsed time, rounded to the nearest whole frame. Rounding is unavoidable
// because the two rates rarely land on the same instant.
//
// framesOut = framesIn * (numOut/denOut) / (numIn/denIn)
// = framesIn * numOut * denIn / (denOut * numIn)
func (t Timecode) ConvertTo(other Rate) Timecode {
numer := int64(t.frames) * int64(other.num) * int64(t.rate.den)
denom := int64(other.den) * int64(t.rate.num)
return Timecode{rate: other, frames: int(divRound(numer, denom))}
}
// divRound divides and rounds half away from zero.
func divRound(a, b int64) int64 {
negative := (a < 0) != (b < 0)
if a < 0 {
a = -a
}
if b < 0 {
b = -b
}
q := (2*a + b) / (2 * b)
if negative {
return -q
}
return q
}
// DriftPerHourNanos returns the drift a rate accumulates over one hour of
// labels. It is zero at every exact rate, about minus 3.6 seconds at a non
// drop 1000/1001 rate, and about plus 3.6 milliseconds at a drop frame rate.
func (r Rate) DriftPerHourNanos() int64 {
if !r.Valid() {
return 0
}
return FromFrames(r.FramesPerHour(), r).DriftNanos()
}
// DriftPerDayNanos returns the drift a rate accumulates over one day of
// labels.
func (r Rate) DriftPerDayNanos() int64 {
if !r.Valid() {
return 0
}
return FromFrames(r.FramesPerDay(), r).DriftNanos()
}