From 26b13d7026579546b09ef5570397340a14ac5ddf Mon Sep 17 00:00:00 2001 From: Joshua Minor Date: Wed, 26 Oct 2022 15:11:29 -0700 Subject: [PATCH] When flattening or trimming tracks, optionally honor linear timewarps. --- src/opentimelineio/stackAlgorithm.cpp | 22 +++-- src/opentimelineio/stackAlgorithm.h | 8 +- src/opentimelineio/track.h | 7 ++ src/opentimelineio/trackAlgorithm.cpp | 90 +++++++++++++++-- src/opentimelineio/trackAlgorithm.h | 3 +- tests/test_track.cpp | 136 ++++++++++++++++++++++++++ 6 files changed, 247 insertions(+), 19 deletions(-) diff --git a/src/opentimelineio/stackAlgorithm.cpp b/src/opentimelineio/stackAlgorithm.cpp index 6800cbc2d..e3dbbb09d 100644 --- a/src/opentimelineio/stackAlgorithm.cpp +++ b/src/opentimelineio/stackAlgorithm.cpp @@ -17,7 +17,8 @@ _flatten_next_item( std::vector const& tracks, int track_index, optional trim_range, - ErrorStatus* error_status) + ErrorStatus* error_status, + TrimPolicy trim_policy) { if (track_index < 0) { @@ -34,7 +35,7 @@ _flatten_next_item( SerializableObject::Retainer track_retainer; if (trim_range) { - track = track_trimmed_to_range(track, *trim_range, error_status); + track = track_trimmed_to_range(track, *trim_range, error_status, trim_policy); if (track == nullptr || is_error(error_status)) { return; @@ -105,7 +106,8 @@ _flatten_next_item( tracks, track_index - 1, trim, - error_status); + error_status, + trim_policy); if (track == nullptr || is_error(error_status)) { return; @@ -124,7 +126,9 @@ _flatten_next_item( } Track* -flatten_stack(Stack* in_stack, ErrorStatus* error_status) +flatten_stack(Stack* in_stack, + ErrorStatus* error_status, + TrimPolicy trim_policy) { std::vector tracks; tracks.reserve(in_stack->children().size()); @@ -161,12 +165,15 @@ flatten_stack(Stack* in_stack, ErrorStatus* error_status) tracks, -1, nullopt, - error_status); + error_status, + trim_policy); return flat_track; } Track* -flatten_stack(std::vector const& tracks, ErrorStatus* error_status) +flatten_stack(std::vector const& tracks, + ErrorStatus* error_status, + TrimPolicy trim_policy) { Track* flat_track = new Track; flat_track->set_name("Flattened"); @@ -178,7 +185,8 @@ flatten_stack(std::vector const& tracks, ErrorStatus* error_status) tracks, -1, nullopt, - error_status); + error_status, + trim_policy); return flat_track; } }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/stackAlgorithm.h b/src/opentimelineio/stackAlgorithm.h index 7fb22af8a..c5d03f1ad 100644 --- a/src/opentimelineio/stackAlgorithm.h +++ b/src/opentimelineio/stackAlgorithm.h @@ -9,9 +9,13 @@ namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { -Track* flatten_stack(Stack* in_stack, ErrorStatus* error_status = nullptr); +Track* flatten_stack( + Stack* in_stack, + ErrorStatus* error_status = nullptr, + TrimPolicy trim_policy = IgnoreTimeEffects); Track* flatten_stack( std::vector const& tracks, - ErrorStatus* error_status = nullptr); + ErrorStatus* error_status = nullptr, + TrimPolicy trim_policy = IgnoreTimeEffects); }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/src/opentimelineio/track.h b/src/opentimelineio/track.h index 443c75806..cd4de5ecc 100644 --- a/src/opentimelineio/track.h +++ b/src/opentimelineio/track.h @@ -8,6 +8,13 @@ namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { +enum TrimPolicy +{ + IgnoreTimeEffects = 0, + HonorTimeEffectsExactly, + HonorTimeEffectsWithSnapping +}; + class Clip; class Track : public Composition diff --git a/src/opentimelineio/trackAlgorithm.cpp b/src/opentimelineio/trackAlgorithm.cpp index 4b6e6e5be..ae5fb5a99 100644 --- a/src/opentimelineio/trackAlgorithm.cpp +++ b/src/opentimelineio/trackAlgorithm.cpp @@ -3,14 +3,61 @@ #include "opentimelineio/trackAlgorithm.h" #include "opentimelineio/transition.h" +#include "opentimelineio/linearTimeWarp.h" namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { +static double _compute_time_scalar(Item* item) +{ + double time_scalar = 1.0; + + for (const auto& effect : item->effects()) + { + // Note: FreezeFrame is handled here because it is a subclass of + // LinearTimeWarp. However, generic TimeEffect is not, since non-linear + // warps cannot be decomposed into a simple scalar value. Non-linear + // warps are simply ignored here and left for future development. + if (const auto linear_time_warp = + dynamic_cast(effect.value)) { + time_scalar *= linear_time_warp->time_scalar(); + } + } + + return time_scalar; +} + +static void _snap_timewarps(Item* item) +{ + int item_frames = item->duration().to_frames(); + auto range = item->trimmed_range(); + // snap the item's start time to whole frames + item->set_source_range( + TimeRange(RationalTime(range.start_time().to_frames(), + range.start_time().rate()), + range.duration())); + + for (const auto& effect : item->effects()) + { + if (const auto linear_time_warp = + dynamic_cast(effect.value)) { + // TODO: What happens with FreezeFrame here? + // snap the time_scalar to be a rational number + double time_scalar = linear_time_warp->time_scalar(); + int media_frames = rint(item_frames * time_scalar); + time_scalar = (double)media_frames / item_frames; + linear_time_warp->set_time_scalar(time_scalar); + } + } +} + +// TODO: SnappedLinearTimeWarp??? + Track* track_trimmed_to_range( Track* in_track, TimeRange trim_range, - ErrorStatus* error_status) + ErrorStatus* error_status, + TrimPolicy trimPolicy) { Track* new_track = dynamic_cast(in_track->clone(error_status)); if (is_error(error_status) || !new_track) @@ -83,26 +130,51 @@ track_trimmed_to_range( return nullptr; } + float time_scalar = 1.0; + switch (trimPolicy) { + case HonorTimeEffectsExactly: + case HonorTimeEffectsWithSnapping: + time_scalar = _compute_time_scalar(child_item); + break; + case IgnoreTimeEffects: + default: + break; + } + + // TODO: What if there are non-linear time warp here? + + // Does the trim start after this child? + // If so, we need to remove the beginning of the child. if (trim_range.start_time() > child_range.start_time()) { auto trim_amount = - trim_range.start_time() - child_range.start_time(); - child_source_range = TimeRange( - child_source_range.start_time() + trim_amount, - child_source_range.duration() - trim_amount); + trim_range.start_time() - child_range.start_time(); + auto scaled_trim_amount = + RationalTime(trim_amount.value() * time_scalar, + trim_amount.rate()); + child_source_range = + TimeRange(child_source_range.start_time() + + scaled_trim_amount, + child_source_range.duration() - trim_amount); } + // Does the trim end before this child ends? + // If so, we need to remove the end of the child. auto trim_end = trim_range.end_time_exclusive(); auto child_end = child_range.end_time_exclusive(); if (trim_end < child_end) { - auto trim_amount = child_end - trim_end; - child_source_range = TimeRange( - child_source_range.start_time(), - child_source_range.duration() - trim_amount); + auto trim_amount = child_end - trim_end; + child_source_range = + TimeRange( + child_source_range.start_time(), + child_source_range.duration() - trim_amount); } child_item->set_source_range(child_source_range); + if (trimPolicy == HonorTimeEffectsWithSnapping) { + _snap_timewarps(child_item); + } } } diff --git a/src/opentimelineio/trackAlgorithm.h b/src/opentimelineio/trackAlgorithm.h index 54ce6d35e..ae2532228 100644 --- a/src/opentimelineio/trackAlgorithm.h +++ b/src/opentimelineio/trackAlgorithm.h @@ -11,6 +11,7 @@ namespace opentimelineio { namespace OPENTIMELINEIO_VERSION { Track* track_trimmed_to_range( Track* in_track, TimeRange trim_range, - ErrorStatus* error_status = nullptr); + ErrorStatus* error_status = nullptr, + TrimPolicy trimPolicy = IgnoreTimeEffects); }} // namespace opentimelineio::OPENTIMELINEIO_VERSION diff --git a/tests/test_track.cpp b/tests/test_track.cpp index d66fcbfb5..b58b2b5cc 100644 --- a/tests/test_track.cpp +++ b/tests/test_track.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include @@ -107,7 +109,141 @@ main(int argc, char** argv) assertEqual(result[0].value, cl0.value); assertEqual(result[1].value, cl1.value); }); + tests.add_test( + "test_track_trimmed_to_range", [] { + using namespace otio; + SerializableObject::Retainer cl0 = + new Clip(); + cl0->set_source_range(TimeRange(RationalTime(0,24), + RationalTime(10,24))); + SerializableObject::Retainer cl1 = + new Clip(); + cl1->set_source_range(TimeRange(RationalTime(20,24), + RationalTime(10,24))); + SerializableObject::Retainer tr = + new Track(); + tr->append_child(cl0); + tr->append_child(cl1); + + opentimelineio::v1_0::ErrorStatus err; + auto trimmed_track = + track_trimmed_to_range(tr, + TimeRange(RationalTime(5,24), + RationalTime(7,24)), + &err); + assertFalse(is_error(err)); + + assertEqual(trimmed_track->children().size(), 2); + const auto cl0_trimmed = dynamic_cast(trimmed_track->children().at(0).value); + const auto cl1_trimmed = dynamic_cast(trimmed_track->children().at(1).value); + const auto cl0_trimmed_range = cl0_trimmed->trimmed_range(); + const auto cl1_trimmed_range = cl1_trimmed->trimmed_range(); + assertEqual(cl0_trimmed_range, + TimeRange(RationalTime(5,24), + RationalTime(5,24))); + assertEqual(cl1_trimmed_range, + TimeRange(RationalTime(20,24), + RationalTime(2,24))); + }); + tests.add_test( + "test_track_trimmed_to_range_with_timewarp", [] { + using namespace otio; + SerializableObject::Retainer cl0 = + new Clip(); + cl0->set_source_range(TimeRange(RationalTime(0,24), + RationalTime(10,24))); + SerializableObject::Retainer tw0 = + new LinearTimeWarp("tw0", "", 0.51); + cl0->effects().push_back(dynamic_retainer_cast(tw0)); + SerializableObject::Retainer cl1 = + new Clip(); + cl1->set_source_range(TimeRange(RationalTime(20,24), + RationalTime(10,24))); + SerializableObject::Retainer tw1 = + new LinearTimeWarp("tw1", "", 0.51); + cl1->effects().push_back(dynamic_retainer_cast(tw1)); + SerializableObject::Retainer tr = + new Track(); + tr->append_child(cl0); + tr->append_child(cl1); + + opentimelineio::v1_0::ErrorStatus err; + + // Try IgnoreTimeEffects + auto trimmed_track = + track_trimmed_to_range(tr, + TimeRange(RationalTime(5,24), + RationalTime(7,24)), + &err, + IgnoreTimeEffects); + assertFalse(is_error(err)); + assertEqual(trimmed_track->children().size(), 2); + auto cl0_trimmed = dynamic_cast(trimmed_track->children().at(0).value); + auto cl1_trimmed = dynamic_cast(trimmed_track->children().at(1).value); + auto cl0_trimmed_range = cl0_trimmed->trimmed_range(); + auto cl1_trimmed_range = cl1_trimmed->trimmed_range(); + assertEqual(cl0_trimmed_range, + TimeRange(RationalTime(5,24), + RationalTime(5,24))); + auto tw = dynamic_cast(cl0_trimmed->effects()[0].value); + assertEqual(tw->time_scalar(), 0.51); + assertEqual(cl1_trimmed_range, + TimeRange(RationalTime(20,24), + RationalTime(2,24))); + tw = dynamic_cast(cl1_trimmed->effects()[0].value); + assertEqual(tw->time_scalar(), 0.51); + + // Try HonorTimeEffectsExactly + auto trimmed_track2 = + track_trimmed_to_range(tr, + TimeRange(RationalTime(5,24), + RationalTime(7,24)), + &err, + HonorTimeEffectsExactly); + assertFalse(is_error(err)); + + assertEqual(trimmed_track2->children().size(), 2); + cl0_trimmed = dynamic_cast(trimmed_track2->children().at(0).value); + cl1_trimmed = dynamic_cast(trimmed_track2->children().at(1).value); + cl0_trimmed_range = cl0_trimmed->trimmed_range(); + cl1_trimmed_range = cl1_trimmed->trimmed_range(); + assertEqual(cl0_trimmed_range, + TimeRange(RationalTime(2.55,24), + RationalTime(5,24))); + tw = dynamic_cast(cl0_trimmed->effects()[0].value); + assertEqual(tw->time_scalar(), 0.51); + assertEqual(cl1_trimmed_range, + TimeRange(RationalTime(20,24), + RationalTime(2,24))); + tw = dynamic_cast(cl1_trimmed->effects()[0].value); + assertEqual(tw->time_scalar(), 0.51); + + // Try HonorTimeEffectsWithSnapping + auto trimmed_track3 = + track_trimmed_to_range(tr, + TimeRange(RationalTime(5,24), + RationalTime(7,24)), + &err, + HonorTimeEffectsWithSnapping); + assertFalse(is_error(err)); + + assertEqual(trimmed_track3->children().size(), 2); + cl0_trimmed = dynamic_cast(trimmed_track3->children().at(0).value); + cl1_trimmed = dynamic_cast(trimmed_track3->children().at(1).value); + cl0_trimmed_range = cl0_trimmed->trimmed_range(); + cl1_trimmed_range = cl1_trimmed->trimmed_range(); + assertEqual(cl0_trimmed_range, + TimeRange(RationalTime(2,24), + RationalTime(5,24))); + tw = dynamic_cast(cl1_trimmed->effects()[0].value); + assertEqual(tw->time_scalar(), 0.5); + assertEqual(cl1_trimmed_range, + TimeRange(RationalTime(20,24), + RationalTime(2,24))); + tw = dynamic_cast(cl1_trimmed->effects()[0].value); + assertEqual(tw->time_scalar(), 0.5); + }); tests.run(argc, argv); return 0; }