Skip to content

Commit 3a8bad4

Browse files
authored
Improve handling of rates when converting to timecode strings (#1477)
* Deal with "close" timecode rates via a heuristic, instead of misleading entries in valid_timecode_rates table. * Renamed "valid_timecode" functions to "smpte_timecode" for clarity (old names are deprecated, but still work). * Both `is_smpte_timecode_rate` and `nearest_smpte_timecode_rate` now adhere to ST 12-1:2014 - SMPTE Standard - Time and Control Code. Signed-off-by: Joshua Minor <joshm@pixar.com>
1 parent f40c509 commit 3a8bad4

File tree

5 files changed

+123
-88
lines changed

5 files changed

+123
-88
lines changed

src/opentime/errorStatus.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ ErrorStatus::outcome_to_string(Outcome o)
1313
case OK:
1414
return std::string();
1515
case INVALID_TIMECODE_RATE:
16-
return "invalid timecode rate";
16+
return "SMPTE timecode does not support this rate";
1717
case INVALID_TIMECODE_STRING:
18-
return "string is not a valid timecode string";
18+
return "string is not a SMPTE timecode string";
1919
case TIMECODE_RATE_MISMATCH:
2020
return "timecode specifies a frame higher than its rate";
2121
case INVALID_TIME_STRING:

src/opentime/rationalTime.cpp

+42-41
Original file line numberDiff line numberDiff line change
@@ -13,75 +13,68 @@ namespace opentime { namespace OPENTIME_VERSION {
1313

1414
RationalTime RationalTime::_invalid_time{ 0, RationalTime::_invalid_rate };
1515

16-
static constexpr std::array<double, 4> dropframe_timecode_rates{ {
17-
// 23.976,
18-
// 23.98,
19-
// 23.97,
20-
// 24000.0/1001.0,
21-
29.97,
16+
static constexpr std::array<double, 2> dropframe_timecode_rates{ {
2217
30000.0 / 1001.0,
23-
59.94,
2418
60000.0 / 1001.0,
2519
} };
2620

21+
// See the official source of these numbers here:
22+
// ST 12-1:2014 - SMPTE Standard - Time and Control Code
23+
// https://ieeexplore.ieee.org/document/7291029
24+
//
2725
static constexpr std::array<double, 11> smpte_timecode_rates{
28-
{ 1.0,
29-
12.0,
30-
24000.0 / 1001.0,
26+
{ 24000.0 / 1001.0,
3127
24.0,
3228
25.0,
3329
30000.0 / 1001.0,
3430
30.0,
31+
48000.0 / 1001.0,
3532
48.0,
3633
50.0,
3734
60000.0 / 1001.0,
38-
60.0 }
39-
};
40-
41-
static constexpr std::array<double, 16> valid_timecode_rates{
42-
{ 1.0,
43-
12.0,
44-
23.97,
45-
23.976,
46-
23.98,
47-
24000.0 / 1001.0,
48-
24.0,
49-
25.0,
50-
29.97,
51-
30000.0 / 1001.0,
52-
30.0,
53-
48.0,
54-
50.0,
55-
59.94,
56-
60000.0 / 1001.0,
57-
60.0 }
35+
60.0
36+
}
5837
};
5938

39+
// deprecated in favor of `is_smpte_timecode_rate`
6040
bool
6141
RationalTime::is_valid_timecode_rate(double fps)
6242
{
63-
auto b = valid_timecode_rates.begin(), e = valid_timecode_rates.end();
43+
return is_smpte_timecode_rate(fps);
44+
}
45+
46+
bool
47+
RationalTime::is_smpte_timecode_rate(double fps)
48+
{
49+
auto b = smpte_timecode_rates.begin(), e = smpte_timecode_rates.end();
6450
return std::find(b, e, fps) != e;
6551
}
6652

53+
// deprecated in favor of `is_smpte_timecode_rate`
6754
double
6855
RationalTime::nearest_valid_timecode_rate(double rate)
56+
{
57+
return nearest_smpte_timecode_rate(rate);
58+
}
59+
60+
double
61+
RationalTime::nearest_smpte_timecode_rate(double rate)
6962
{
7063
double nearest_rate = 0;
7164
double min_diff = std::numeric_limits<double>::max();
72-
for (auto valid_rate: smpte_timecode_rates)
65+
for (auto smpte_rate: smpte_timecode_rates)
7366
{
74-
if (valid_rate == rate)
67+
if (smpte_rate == rate)
7568
{
7669
return rate;
7770
}
78-
auto diff = std::abs(rate - valid_rate);
71+
auto diff = std::abs(rate - smpte_rate);
7972
if (diff >= min_diff)
8073
{
8174
continue;
8275
}
8376
min_diff = diff;
84-
nearest_rate = valid_rate;
77+
nearest_rate = smpte_rate;
8578
}
8679
return nearest_rate;
8780
}
@@ -200,7 +193,7 @@ RationalTime::from_timecode(
200193
double rate,
201194
ErrorStatus* error_status)
202195
{
203-
if (!RationalTime::is_valid_timecode_rate(rate))
196+
if (!RationalTime::is_smpte_timecode_rate(rate))
204197
{
205198
if (error_status)
206199
{
@@ -331,7 +324,7 @@ RationalTime::from_time_string(
331324
double rate,
332325
ErrorStatus* error_status)
333326
{
334-
if (!RationalTime::is_valid_timecode_rate(rate))
327+
if (!RationalTime::is_smpte_timecode_rate(rate))
335328
{
336329
set_error(
337330
time_string,
@@ -460,7 +453,12 @@ RationalTime::to_timecode(
460453
return std::string();
461454
}
462455

463-
if (!is_valid_timecode_rate(rate))
456+
// It is common practice to use truncated or rounded values
457+
// like 29.97 instead of exact SMPTE rates like 30000/1001
458+
// so as a convenience we will snap the rate to the nearest
459+
// SMPTE rate if it is close enough.
460+
double nearest_smpte_rate = nearest_smpte_timecode_rate(rate);
461+
if (abs(nearest_smpte_rate - rate) > 0.1)
464462
{
465463
if (error_status)
466464
{
@@ -469,6 +467,9 @@ RationalTime::to_timecode(
469467
return std::string();
470468
}
471469

470+
// Let's assume this is the rate instead of the given rate.
471+
rate = nearest_smpte_rate;
472+
472473
bool rate_is_dropframe = is_dropframe_rate(rate);
473474
if (drop_frame == IsDropFrameRate::ForceYes and not rate_is_dropframe)
474475
{
@@ -504,11 +505,11 @@ RationalTime::to_timecode(
504505
}
505506
else
506507
{
507-
if ((rate == 29.97) or (rate == 30000 / 1001.0))
508+
if (rate == 30000 / 1001.0)
508509
{
509510
dropframes = 2;
510511
}
511-
else if (rate == 59.94)
512+
else if (rate == 60000 / 1001.0)
512513
{
513514
dropframes = 4;
514515
}
@@ -582,7 +583,7 @@ RationalTime::to_nearest_timecode(
582583
{
583584
*error_status = ErrorStatus();
584585

585-
double nearest_rate = nearest_valid_timecode_rate(rate);
586+
double nearest_rate = nearest_smpte_timecode_rate(rate);
586587

587588
return to_timecode(nearest_rate, drop_frame, error_status);
588589
}

src/opentime/rationalTime.h

+10-3
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,20 @@ class RationalTime
171171
start_time._rate };
172172
}
173173

174-
/// @brief Returns true if the rate is valid for use with timecode.
174+
/// @brief Returns true is the rate is supported by SMPTE timecode.
175+
[[deprecated("Use is_smpte_timecode_rate() instead")]]
175176
static bool is_valid_timecode_rate(double rate);
176177

177-
/// @brief Returns the first valid timecode rate that has the least
178-
/// difference from rate.
178+
/// @brief Returns true is the rate is supported by SMPTE timecode.
179+
static bool is_smpte_timecode_rate(double rate);
180+
181+
/// @brief Returns the SMPTE timecode rate nearest to the given rate.
182+
[[deprecated("Use nearest_smpte_timecode_rate() instead")]]
179183
static double nearest_valid_timecode_rate(double rate);
180184

185+
/// @brief Returns the SMPTE timecode rate nearest to the given rate.
186+
static double nearest_smpte_timecode_rate(double rate);
187+
181188
/// @brief Convert a frame number and rate into a time.
182189
static constexpr RationalTime
183190
from_frames(double frame, double rate) noexcept

src/py-opentimelineio/opentime-bindings/opentime_rationalTime.cpp

+13-5
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,22 @@ Compute the duration of samples from first to last (including last). This is not
102102
103103
For example, the duration of a clip from frame 10 to frame 15 is 6 frames. Result will be in the rate of start_time.
104104
)docstring")
105-
.def_static("is_valid_timecode_rate", &RationalTime::is_valid_timecode_rate, "rate"_a, "Returns true if the rate is valid for use with timecode.")
105+
.def_static("is_valid_timecode_rate", &RationalTime::is_valid_timecode_rate, "rate"_a,
106+
"Deprecated. Please use `is_smpte_timecode_rate` instead. This function will be removed in a future release.")
107+
.def_static("is_smpte_timecode_rate", &RationalTime::is_smpte_timecode_rate, "rate"_a,
108+
"Returns true if the rate is valid for use with SMPTE timecode.")
106109
.def_static("nearest_valid_timecode_rate", &RationalTime::nearest_valid_timecode_rate, "rate"_a,
107-
"Returns the first valid timecode rate that has the least difference from the given value.")
108-
.def_static("from_frames", &RationalTime::from_frames, "frame"_a, "rate"_a, "Turn a frame number and rate into a :class:`~RationalTime` object.")
110+
"Deprecated. Please use `nearest_smpte_timecode_rate` instead. This function will be removed in a future release.")
111+
.def_static("nearest_smpte_timecode_rate", &RationalTime::nearest_smpte_timecode_rate, "rate"_a,
112+
"Returns the first SMPTE timecode rate that has the least difference from the given value.")
113+
.def_static("from_frames", &RationalTime::from_frames, "frame"_a, "rate"_a,
114+
"Turn a frame number and rate into a :class:`~RationalTime` object.")
109115
.def_static("from_seconds", static_cast<RationalTime (*)(double, double)> (&RationalTime::from_seconds), "seconds"_a, "rate"_a)
110116
.def_static("from_seconds", static_cast<RationalTime (*)(double)> (&RationalTime::from_seconds), "seconds"_a)
111-
.def("to_frames", (int (RationalTime::*)() const) &RationalTime::to_frames, "Returns the frame number based on the current rate.")
112-
.def("to_frames", (int (RationalTime::*)(double) const) &RationalTime::to_frames, "rate"_a, "Returns the frame number based on the given rate.")
117+
.def("to_frames", (int (RationalTime::*)() const) &RationalTime::to_frames,
118+
"Returns the frame number based on the current rate.")
119+
.def("to_frames", (int (RationalTime::*)(double) const) &RationalTime::to_frames, "rate"_a,
120+
"Returns the frame number based on the given rate.")
113121
.def("to_seconds", &RationalTime::to_seconds)
114122
.def("to_timecode", [](RationalTime rt, double rate, std::optional<bool> drop_frame) {
115123
return rt.to_timecode(

0 commit comments

Comments
 (0)