From 36c7f7a11e7e04c2495348d031ebfe6466b4616e Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Fri, 11 Apr 2025 12:21:06 +0200 Subject: [PATCH] Refactor/cleanup of SyncTrack and added serialization to/from json. --- src/SyncTrack.h | 177 ++++++++++++++++--------- tests/SyncTrackTests.cc | 279 ++++++++++++++++++++++------------------ 2 files changed, 269 insertions(+), 187 deletions(-) diff --git a/src/SyncTrack.h b/src/SyncTrack.h index a3aff31..a14c2ad 100644 --- a/src/SyncTrack.h +++ b/src/SyncTrack.h @@ -6,57 +6,62 @@ #define ANIMTESTBED_SYNCTRACK_H #include -#include -#include + +#include "3rdparty/json/json.hpp" constexpr int cSyncTrackMaxIntervals = 8; +/// Metadata used for synced animation blending. +// +// A SyncTrack consists of multiple SyncInterval that are adjacent to each +// other. +// +// Important definitions: +// +// - Absolute Time: time within an animation duration in seconds. +// - Ratio: time relative to the animations duration, e.g. 0.5 corresponds to +// 50% of the duration. +// - SyncTime is a floating point value where the integer parts defines the +// SyncInterval and the fractional part the fraction within the interval. I.e. +// a SyncTime of 5.332 means it is ~33% through interval 5. +// +// A SyncInterval is defined by a ratio of the starting point and the ratio of +// the interval's duration. struct SyncTrack { - SyncTrack() : m_duration(0.f), m_num_intervals(0) { + SyncTrack() : m_duration(0.f), m_num_intervals(1) { for (int i = 0; i < cSyncTrackMaxIntervals; i++) { - m_sync_markers[i] = 0.f; - m_interval_ratio[i] = 0.f; - m_interval_ratio[i] = 0.f; + m_interval_start_ratio[i] = 0.f; + m_interval_duration_ratio[i] = 0.f; } + + m_interval_duration_ratio[0] = 1.0f; } float m_duration; int m_num_intervals; - float m_sync_markers[cSyncTrackMaxIntervals]; - float m_interval_start[cSyncTrackMaxIntervals]; - float m_interval_ratio[cSyncTrackMaxIntervals]; - - void CalcIntervals() { - if (m_num_intervals == 0) { - m_num_intervals = 1; - m_sync_markers[0] = 0.f; - } - for (int i = 0; i < m_num_intervals; i++) { - assert(m_sync_markers[i] >= 0.f && m_sync_markers[i] <= 1.0f); - - int end_index = i < m_num_intervals - 1 ? i + 1 : 0; - - m_interval_start[i] = m_sync_markers[i]; - float interval_end = m_sync_markers[end_index]; - - if (interval_end < m_interval_start[i]) { - interval_end += 1.0f; - } - - m_interval_ratio[i] = interval_end - m_interval_start[i]; - } - } + float m_interval_start_ratio + [cSyncTrackMaxIntervals]; //< Starting time of interval in absolute time. + float m_interval_duration_ratio[cSyncTrackMaxIntervals]; //< float CalcSyncFromAbsTime(float abs_time) { - float sync_time = fmodf(abs_time, m_duration) / m_duration; + for (int i = 0; i < m_num_intervals; i++) { + float query_abs_time = abs_time; + float interval_start = m_interval_start_ratio[i] * m_duration; + float interval_end = + interval_start + m_interval_duration_ratio[i] * m_duration; - int interval_index = 0; - while (sync_time >= m_interval_ratio[interval_index]) { - sync_time -= m_interval_ratio[interval_index]; - interval_index++; + if (query_abs_time < interval_start) { + query_abs_time += m_duration; + } + if (query_abs_time >= interval_start && query_abs_time < interval_end) { + return float(i) + + (query_abs_time - interval_start) + / (interval_end - interval_start); + } } - return float(interval_index) + sync_time / m_interval_ratio[interval_index]; + assert(false && "Invalid absolute time"); + return -1.f; } float CalcRatioFromSyncTime(float sync_time) { @@ -64,8 +69,8 @@ struct SyncTrack { int interval = int(sync_time - interval_ratio); return fmodf( - m_interval_start[interval] - + m_interval_ratio[interval] * interval_ratio, + m_interval_start_ratio[interval] + + m_interval_duration_ratio[interval] * interval_ratio, 1.0f); } @@ -78,8 +83,11 @@ struct SyncTrack { } for (int i = 0; i < m_num_intervals; i++) { - if ((fabsf(m_interval_start[i] - other.m_interval_start[i]) > 1.0e-5) - || (fabsf(m_interval_ratio[i] - other.m_interval_ratio[i]) + if ((fabsf(m_interval_start_ratio[i] - other.m_interval_start_ratio[i]) + > 1.0e-5) + || (fabsf( + m_interval_duration_ratio[i] + - other.m_interval_duration_ratio[i]) > 1.0e-5)) { return false; } @@ -88,18 +96,40 @@ struct SyncTrack { return true; } + /** Constructs SyncTrack from markers. + * + * Markers are specified in absolute time and must be >= 0 and <= duration. + * They define the start of the interval. The last marker is implicitly the + * (possibly looped) first marker. + */ static SyncTrack CreateFromMarkers( float duration, - int n_markers, - float markers[cSyncTrackMaxIntervals]) { + const std::vector& markers) { + assert(markers.size() > 0); + assert(markers.size() < cSyncTrackMaxIntervals); + SyncTrack result; result.m_duration = duration; - result.m_num_intervals = n_markers; - for (int i = 0; i < n_markers; i++) { - result.m_sync_markers[i] = markers[i]; - } + result.m_num_intervals = markers.size(); - result.CalcIntervals(); + for (int i = 0; i < markers.size(); i++) { + assert(markers[i] >= 0.f && markers[i] <= duration); + + int end_index = i == (markers.size() - 1) ? 0 : i + 1; + + float interval_start = markers[i]; + float interval_end = markers[end_index]; + + if (interval_end == interval_start) { + interval_end = interval_start + duration; + } else if (interval_end < interval_start) { + interval_end += duration; + } + + result.m_interval_start_ratio[i] = interval_start / duration; + result.m_interval_duration_ratio[i] = + (interval_end - interval_start) / duration; + } return result; } @@ -115,41 +145,62 @@ struct SyncTrack { (1.0f - weight) * track_A.m_duration + weight * track_B.m_duration; float interval_0_offset = - track_B.m_interval_start[0] - track_A.m_interval_start[0]; + track_B.m_interval_start_ratio[0] - track_A.m_interval_start_ratio[0]; if (interval_0_offset > 0.5f) { interval_0_offset = -fmodf(1.f - interval_0_offset, 1.0f); } else if (interval_0_offset < -0.5) { interval_0_offset = fmodf(1.f + interval_0_offset, 1.0f); } - result.m_interval_start[0] = fmodf( - 1.0 + (1.0f - weight) * track_A.m_interval_start[0] - + weight * (track_A.m_interval_start[0] + interval_0_offset), + result.m_interval_start_ratio[0] = fmodf( + 1.0 + (1.0f - weight) * track_A.m_interval_start_ratio[0] + + weight * (track_A.m_interval_start_ratio[0] + interval_0_offset), 1.0f); - result.m_sync_markers[0] = result.m_interval_start[0]; for (int i = 0; i < result.m_num_intervals; i++) { - float interval_duration_A = track_A.m_interval_ratio[i]; - float interval_duration_B = track_B.m_interval_ratio[i]; - result.m_interval_ratio[i] = + float interval_duration_A = track_A.m_interval_duration_ratio[i]; + float interval_duration_B = track_B.m_interval_duration_ratio[i]; + result.m_interval_duration_ratio[i] = (1.0f - weight) * interval_duration_A + weight * interval_duration_B; if (i < cSyncTrackMaxIntervals) { - result.m_interval_start[i + 1] = - result.m_interval_start[i] + result.m_interval_ratio[i]; - if (result.m_interval_start[i + 1] > 1.0f) { - result.m_interval_start[i + 1] = - fmodf(result.m_interval_start[i + 1], 1.0f); + result.m_interval_start_ratio[i + 1] = + result.m_interval_start_ratio[i] + + result.m_interval_duration_ratio[i]; + if (result.m_interval_start_ratio[i + 1] > 1.0f) { + result.m_interval_start_ratio[i + 1] = + fmodf(result.m_interval_start_ratio[i + 1], 1.0f); } - - result.m_sync_markers[i + 1] = result.m_interval_start[i + 1]; } } - assert (result.m_num_intervals < cSyncTrackMaxIntervals); + assert(result.m_num_intervals < cSyncTrackMaxIntervals); return result; } }; +inline void to_json(nlohmann::json& j, const SyncTrack& sync_track) { + j["type"] = "SyncTrack"; + j["duration"] = sync_track.m_duration; + for (int i = 0; i < sync_track.m_num_intervals; i++) { + j["interval"][i]["start"] = sync_track.m_interval_start_ratio[i]; + j["interval"][i]["ratio"] = sync_track.m_interval_duration_ratio[i]; + } +} + +inline void from_json(const nlohmann::json& j, SyncTrack& sync_track) { + assert(j["type"] == "SyncTrack"); + + sync_track.m_duration = j["duration"]; + + sync_track.m_num_intervals = j["interval"].size(); + assert(sync_track.m_num_intervals < cSyncTrackMaxIntervals); + + for (int i = 0; i < sync_track.m_num_intervals; i++) { + sync_track.m_interval_start_ratio[i] = j["interval"][i]["start"]; + sync_track.m_interval_duration_ratio[i] = j["interval"][i]["ratio"]; + } +} + #endif //ANIMTESTBED_SYNCTRACK_H diff --git a/tests/SyncTrackTests.cc b/tests/SyncTrackTests.cc index 9190434..5bdee75 100644 --- a/tests/SyncTrackTests.cc +++ b/tests/SyncTrackTests.cc @@ -6,197 +6,228 @@ #include "catch.hpp" TEST_CASE("Basic", "[SyncTrack]") { - SyncTrack track_A; - track_A.m_num_intervals = 2; - track_A.m_duration = 2.0; - track_A.m_interval_start[0] = 0.f; - track_A.m_interval_ratio[0] = 0.7; - track_A.m_interval_start[1] = 0.7f; - track_A.m_interval_ratio[1] = 0.3; + SyncTrack track_a; + track_a.m_num_intervals = 2; + track_a.m_duration = 2.0; + track_a.m_interval_start_ratio[0] = 0.f; + track_a.m_interval_duration_ratio[0] = 0.7; + track_a.m_interval_start_ratio[1] = 0.7f; + track_a.m_interval_duration_ratio[1] = 0.3; - SyncTrack track_B; - track_B.m_num_intervals = 2; - track_B.m_duration = 1.5; - track_B.m_interval_start[0] = 0.0f; - track_B.m_interval_ratio[0] = 0.6; - track_B.m_interval_start[1] = 0.6f; - track_B.m_interval_ratio[1] = 0.4; + SyncTrack track_b; + track_b.m_num_intervals = 2; + track_b.m_duration = 1.5; + track_b.m_interval_start_ratio[0] = 0.0f; + track_b.m_interval_duration_ratio[0] = 0.6; + track_b.m_interval_start_ratio[1] = 0.6f; + track_b.m_interval_duration_ratio[1] = 0.4; WHEN("Calculating sync time of track_B at 0.5 duration") { float sync_time_at_0_75 = - track_B.CalcSyncFromAbsTime(0.5 * track_B.m_duration); + track_b.CalcSyncFromAbsTime(0.5 * track_b.m_duration); REQUIRE(sync_time_at_0_75 == Catch::Detail::Approx(0.83333)); } WHEN("Calculating sync time of track_B at 0.6 duration") { float sync_time_at_0_6 = - track_B.CalcSyncFromAbsTime(0.6 * track_B.m_duration); + track_b.CalcSyncFromAbsTime(0.6 * track_b.m_duration); REQUIRE(sync_time_at_0_6 == Catch::Detail::Approx(1.0)); } WHEN("Calculating sync time of track_B at 0.7 duration") { float sync_time_at_0_7 = - track_B.CalcSyncFromAbsTime(0.7 * track_B.m_duration); + track_b.CalcSyncFromAbsTime(0.7 * track_b.m_duration); REQUIRE(sync_time_at_0_7 == Catch::Detail::Approx(1.25)); } WHEN("Calculating sync time of track_B at 0.0 duration") { float sync_time_at_1_0 = - track_B.CalcSyncFromAbsTime(0.0 * track_B.m_duration); + track_b.CalcSyncFromAbsTime(0.0 * track_b.m_duration); REQUIRE(sync_time_at_1_0 == Catch::Detail::Approx(0.0)); } WHEN("Calculating sync time of track_B at 1.0 duration") { float sync_time_at_1_0 = - track_B.CalcSyncFromAbsTime(0.9999 * track_B.m_duration); + track_b.CalcSyncFromAbsTime(0.9999 * track_b.m_duration); REQUIRE(sync_time_at_1_0 == Catch::Detail::Approx(2.0).epsilon(0.001f)); } WHEN("Calculating ratio from sync time on track_A at 0.83333") { - float ratio = track_A.CalcRatioFromSyncTime(0.83333333); + float ratio = track_a.CalcRatioFromSyncTime(0.83333333); REQUIRE(ratio == Catch::Detail::Approx(0.5833333)); } WHEN("Calculating ratio from sync time on track_A at 0.83333") { - float ratio = track_A.CalcRatioFromSyncTime(1.25); + float ratio = track_a.CalcRatioFromSyncTime(1.25); REQUIRE(ratio == Catch::Detail::Approx(0.775)); } WHEN("Blending two synctracks with weight 0.") { - SyncTrack blended = SyncTrack::Blend(0.f, track_A, track_B); + SyncTrack blended = SyncTrack::Blend(0.f, track_a, track_b); - THEN("Result must equal track_A") { REQUIRE(track_A == blended); } + THEN("Result must equal track_A") { REQUIRE(track_a == blended); } } WHEN("Blending two synctracks with weight 1.") { - SyncTrack blended = SyncTrack::Blend(1.f, track_A, track_B); + SyncTrack blended = SyncTrack::Blend(1.f, track_a, track_b); - THEN("Result must equal track_B") { REQUIRE(track_B == blended); } + THEN("Result must equal track_B") { REQUIRE(track_b == blended); } } } -TEST_CASE("Sync Marker Interval Calculation", "[SyncTrack]") { - SyncTrack track_A; - track_A.m_num_intervals = 2; - track_A.m_duration = 2.0; - track_A.m_sync_markers[0] = 0.9; - track_A.m_sync_markers[1] = 0.2; +TEST_CASE("Sync Track From Marker", "[SyncTrack]") { + SyncTrack track = SyncTrack::CreateFromMarkers(2.0f, {0.9f, 0.2f}); - WHEN("Calculating intervals") { - track_A.CalcIntervals(); + WHEN("Querying Ratios") { + CHECK(track.m_interval_start_ratio[0] == Catch::Detail::Approx(0.45f)); + CHECK(track.m_interval_duration_ratio[0] == Catch::Detail::Approx(0.65f)); - CHECK(track_A.m_interval_start[0] == Catch::Detail::Approx(0.9f)); - CHECK(track_A.m_interval_ratio[0] == Catch::Detail::Approx(0.3f)); + CHECK(track.m_interval_start_ratio[1] == Catch::Detail::Approx(0.1f)); + CHECK(track.m_interval_duration_ratio[1] == Catch::Detail::Approx(0.35f)); - CHECK(track_A.m_interval_start[1] == Catch::Detail::Approx(0.2f)); - CHECK(track_A.m_interval_ratio[1] == Catch::Detail::Approx(0.7f)); + WHEN("Querying ratio at sync time at 0.001") { + float ratio = track.CalcRatioFromSyncTime(0.0001f); + CHECK(ratio == Catch::Detail::Approx(0.45).epsilon(0.001)); + } - WHEN("Querying ratio at sync time at 1.001") { - float ratio = track_A.CalcRatioFromSyncTime(1.0001f); - CHECK(ratio == Catch::Detail::Approx(0.2).epsilon(0.001)); + WHEN("Querying ratio at sync time at 0.9999") { + float ratio = track.CalcRatioFromSyncTime(0.9999f); + CHECK(ratio == Catch::Detail::Approx(0.1).epsilon(0.001)); } WHEN("Querying ratio at sync time at 1.001") { - float ratio = track_A.CalcRatioFromSyncTime(0.0001f); - CHECK(ratio == Catch::Detail::Approx(0.9).epsilon(0.001)); + float ratio = track.CalcRatioFromSyncTime(1.0001f); + CHECK(ratio == Catch::Detail::Approx(0.1).epsilon(0.001)); } WHEN("Querying ratio at sync time at 1.9999") { - float ratio = track_A.CalcRatioFromSyncTime(0.9999f); - CHECK(ratio == Catch::Detail::Approx(0.2).epsilon(0.001)); + float ratio = track.CalcRatioFromSyncTime(1.9999f); + CHECK(ratio == Catch::Detail::Approx(0.45).epsilon(0.001)); } } - WHEN("Blending sync track with 3 events") { - track_A.m_num_intervals = 3; - track_A.m_duration = 2.0; - track_A.m_sync_markers[0] = 0.; - track_A.m_sync_markers[1] = 0.3; - track_A.m_sync_markers[2] = 0.9; - track_A.CalcIntervals(); - - SyncTrack track_B; - track_B.m_num_intervals = 3; - track_B.m_duration = 1.5; - track_B.m_sync_markers[0] = 0.7; - track_B.m_sync_markers[1] = 0.9; - track_B.m_sync_markers[2] = 0.2; - track_B.CalcIntervals(); - - WHEN("Calculating A's durations") { - CHECK(track_A.m_interval_ratio[0] == Catch::Detail::Approx(0.3)); - CHECK(track_A.m_interval_ratio[1] == Catch::Detail::Approx(0.6)); - CHECK(track_A.m_interval_ratio[2] == Catch::Detail::Approx(0.1)); + WHEN("Querying SyncTime from Absolute Time") { + WHEN("Querying absolute time at 0.9001s") { + float sync_time = track.CalcSyncFromAbsTime(0.9001f); + CHECK_THAT(sync_time, Catch::WithinAbs(0.0, 0.001)); } - WHEN("Calculating B's durations") { - CHECK(track_B.m_interval_ratio[0] == Catch::Detail::Approx(0.2)); - CHECK(track_B.m_interval_ratio[1] == Catch::Detail::Approx(0.3)); - CHECK(track_B.m_interval_ratio[2] == Catch::Detail::Approx(0.5)); + WHEN("Querying absolute time at 0.2001s") { + float sync_time = track.CalcSyncFromAbsTime(0.2001f); + CHECK_THAT(sync_time, Catch::WithinAbs(1.0, 0.001)); } - WHEN("Blending two synctracks with weight 0.") { - SyncTrack blended = SyncTrack::Blend(0.f, track_A, track_B); - - THEN("Result must equal track_A") { REQUIRE(track_A == blended); } + WHEN("Querying absolute time at 0.8999s") { + float sync_time = track.CalcSyncFromAbsTime(0.8999f); + CHECK_THAT(sync_time, Catch::WithinAbs(1.999, 0.001)); } - WHEN("Blending two synctracks with weight 1.") { - SyncTrack blended = SyncTrack::Blend(1.f, track_A, track_B); - - THEN("Result must equal track_B") { REQUIRE(track_B == blended); } + WHEN("Querying absolute time at 1.9999s") { + float sync_time = track.CalcSyncFromAbsTime(1.9999f); + CHECK_THAT(sync_time, Catch::WithinAbs(0.84615384, 0.001)); } + } +} - WHEN("Blending with weight 0.2") { - float weight = 0.2f; - SyncTrack blended = SyncTrack::Blend(weight, track_A, track_B); +TEST_CASE("Sync Track Blending", "[SyncTrack]") { + SyncTrack track_a = SyncTrack::CreateFromMarkers(2.0, {0., 0.6, 1.8}); + SyncTrack track_b = SyncTrack::CreateFromMarkers(1.5f, {1.05, 1.35, 0.3}); - REQUIRE( - blended.m_duration - == (1.0f - weight) * track_A.m_duration - + weight * track_B.m_duration); - REQUIRE( - blended.m_interval_start[0] - == fmodf( - (1.0f - weight) * (track_A.m_interval_start[0] + 1.0f) - + weight * (track_B.m_interval_start[0]), - 1.0f)); - REQUIRE( - blended.m_interval_ratio[1] - == (1.0f - weight) * (track_A.m_interval_ratio[1]) - + weight * (track_B.m_interval_ratio[1]) - ); - REQUIRE( - blended.m_interval_ratio[2] - == (1.0f - weight) * (track_A.m_interval_ratio[2]) - + weight * (track_B.m_interval_ratio[2]) - ); - } + WHEN("Calculating A's durations") { + CHECK(track_a.m_interval_duration_ratio[0] == Catch::Detail::Approx(0.3)); + CHECK(track_a.m_interval_duration_ratio[1] == Catch::Detail::Approx(0.6)); + CHECK(track_a.m_interval_duration_ratio[2] == Catch::Detail::Approx(0.1)); + } - WHEN("Inverted blending with weight 0.2") { - float weight = 0.2f; - SyncTrack blended = SyncTrack::Blend(weight, track_B, track_A); + WHEN("Calculating B's durations") { + CHECK(track_b.m_interval_duration_ratio[0] == Catch::Detail::Approx(0.2)); + CHECK(track_b.m_interval_duration_ratio[1] == Catch::Detail::Approx(0.3)); + CHECK(track_b.m_interval_duration_ratio[2] == Catch::Detail::Approx(0.5)); + } - REQUIRE( - blended.m_duration - == (1.0f - weight) * track_B.m_duration - + weight * track_A.m_duration); - REQUIRE( - blended.m_interval_start[0] - == fmodf( - (1.0f - weight) * (track_B.m_interval_start[0]) - + weight * (track_A.m_interval_start[0] + 1.0f), - 1.0f)); - REQUIRE( - blended.m_interval_ratio[1] - == (1.0f - weight) * (track_B.m_interval_ratio[1]) - + weight * (track_A.m_interval_ratio[1]) - ); - REQUIRE( - blended.m_interval_ratio[2] - == (1.0f - weight) * (track_B.m_interval_ratio[2]) - + weight * (track_A.m_interval_ratio[2]) - ); - } + WHEN("Blending two synctracks with weight 0.") { + SyncTrack blended = SyncTrack::Blend(0.f, track_a, track_b); + + THEN("Result must equal track_A") { REQUIRE(track_a == blended); } + } + + WHEN("Blending two synctracks with weight 1.") { + SyncTrack blended = SyncTrack::Blend(1.f, track_a, track_b); + + THEN("Result must equal track_B") { REQUIRE(track_b == blended); } + } + + WHEN("Blending with weight 0.2") { + float weight = 0.2f; + SyncTrack blended = SyncTrack::Blend(weight, track_a, track_b); + + REQUIRE( + blended.m_duration + == (1.0f - weight) * track_a.m_duration + weight * track_b.m_duration); + REQUIRE( + blended.m_interval_start_ratio[0] + == fmodf( + (1.0f - weight) * (track_a.m_interval_start_ratio[0] + 1.0f) + + weight * (track_b.m_interval_start_ratio[0]), + 1.0f)); + REQUIRE( + blended.m_interval_duration_ratio[1] + == (1.0f - weight) * (track_a.m_interval_duration_ratio[1]) + + weight * (track_b.m_interval_duration_ratio[1])); + REQUIRE( + blended.m_interval_duration_ratio[2] + == (1.0f - weight) * (track_a.m_interval_duration_ratio[2]) + + weight * (track_b.m_interval_duration_ratio[2])); + } + + WHEN("Inverted blending with weight 0.2") { + float weight = 0.2f; + SyncTrack blended = SyncTrack::Blend(weight, track_b, track_a); + + REQUIRE( + blended.m_duration + == (1.0f - weight) * track_b.m_duration + weight * track_a.m_duration); + REQUIRE( + blended.m_interval_start_ratio[0] + == fmodf( + (1.0f - weight) * (track_b.m_interval_start_ratio[0]) + + weight * (track_a.m_interval_start_ratio[0] + 1.0f), + 1.0f)); + REQUIRE( + blended.m_interval_duration_ratio[1] + == (1.0f - weight) * (track_b.m_interval_duration_ratio[1]) + + weight * (track_a.m_interval_duration_ratio[1])); + REQUIRE( + blended.m_interval_duration_ratio[2] + == (1.0f - weight) * (track_b.m_interval_duration_ratio[2]) + + weight * (track_a.m_interval_duration_ratio[2])); + } +} + +TEST_CASE("Serialization", "[SyncTrack]") { + SyncTrack track; + track.m_num_intervals = 3; + track.m_duration = 2.0; + track.m_interval_start_ratio[0] = 0.f; + track.m_interval_duration_ratio[0] = 0.7; + track.m_interval_start_ratio[1] = 0.7f; + track.m_interval_duration_ratio[1] = 0.3; + track.m_interval_start_ratio[2] = 0.7f; + track.m_interval_duration_ratio[2] = 0.3; + + nlohmann::json synctrack_json = track; + + const SyncTrack synctrack_deserialized = synctrack_json; + + CHECK(synctrack_deserialized.m_duration == track.m_duration); + CHECK(synctrack_deserialized.m_num_intervals == track.m_num_intervals); + + for (int i = 0; i < track.m_num_intervals; i++) { + CHECK( + synctrack_deserialized.m_interval_start_ratio[i] + == track.m_interval_start_ratio[i]); + CHECK( + synctrack_deserialized.m_interval_duration_ratio[i] + == track.m_interval_duration_ratio[i]); } } \ No newline at end of file