#include "rendercontext.h" #include "timing/arrangement.h" #include "timing/global_loop.h" #include "timing/operators.h" #include "timing/piecewise_linear.h" #include "timing/warp.h" #include #include #include EMSCRIPTEN_DECLARE_VAL_TYPE(TimingAutomationArray); EMSCRIPTEN_DECLARE_VAL_TYPE(WarpMarkerArray); std::shared_ptr js_TimelineTempoMap_fromArray(double bps, const TimingAutomationArray &array) { assert(array.isArray()); auto point_arr = std::vector(); for (const auto &item : array) { auto beats = item["beats"].as(); auto value = item["value"].as(); auto curve = item["curve"].as(); assert(curve == 0.0); assert(std::isfinite(beats)); assert(std::isfinite(value)); point_arr.emplace_back(time_units::Beats(beats), value); } return std::make_shared( bps, std::move(point_arr)); } std::shared_ptr> js_WarpMap_fromArray(const WarpMarkerArray &array) { assert(array.isArray()); auto markers = std::vector::WarpMarker>(); for (const auto &item : array) { auto timeInOutput = item["timeInOutput"].as(); auto timeInUnderlyingBuffer = item["timeInUnderlyingBuffer"].as(); markers.emplace_back(time_units::Beats(timeInOutput), time_units::Seconds(timeInUnderlyingBuffer)); assert(std::isfinite(timeInOutput)); assert(std::isfinite(timeInUnderlyingBuffer)); } return std::make_shared>(std::move(markers)); } EMSCRIPTEN_BINDINGS(timing) { using namespace emscripten; register_optional(); register_type( "{ beats: number, value: number, curve: number }[]"); register_type( "{ timeInOutput: number, timeInUnderlyingBuffer: number }[]"); class_("TimelineTempoMap") .smart_ptr>( "TimelineTempoMap") .class_function("fromArray(defaultBps, automationPoints)", &js_TimelineTempoMap_fromArray, nonnull()) .property("defaultBps", &time_transform::TimelineTempoMap::getDefaultDpcu) .function("transformBeatsToSeconds(beats)", &time_transform::TimelineTempoMap::js_transformBeatsToSeconds); class_>("WarpMap") .smart_ptr>>("WarpMap") .class_function("fromArray(warpMarkers)", &js_WarpMap_fromArray, nonnull()); }; template static void requireMapPoint(const T &map, const typename T::DomainTime beats, const std::optional expected, const time_transform::SegmentMarks::MaskType expectedMask = time_transform::SegmentMarks::NONE) { auto seg = map.getSegmentIteratorAt(beats); REQUIRE(seg != map.end()); auto result = seg->map_point(beats); // Log segment, result, and expected values for debugging emscripten_console_log(("Segment: " + seg->toString()).c_str()); if (result.has_value()) { emscripten_console_log(("Result: " + result->toString()).c_str()); } else { emscripten_console_log("Result: nullopt"); } emscripten_console_log( ("Expected: " + (expected.has_value() ? expected->toString() : "nullopt")) .c_str()); if (expected.has_value()) { REQUIRE(result.has_value()); REQUIRE(*result == *expected); } REQUIRE(seg->get_mask() == expectedMask); }; TEST_CASE("timing tests", "[timing]") { using namespace time_transform; using namespace time_units; SECTION("tempo map empty") { auto test_tempo_map = TimelineTempoMap(2.0, {}); requireMapPoint(test_tempo_map, Beats(0.0), Seconds(0.0)); requireMapPoint(test_tempo_map, Beats(10.0), Seconds(5.0)); requireMapPoint(test_tempo_map, Beats(-10.0), Seconds(-5.0)); // iterator should split domain into (-inf, 0) and [0, inf) auto it = test_tempo_map.begin(); emscripten_console_log(it->toString().c_str()); REQUIRE(it->source_range.start == Beats::neg_inf()); REQUIRE(it->source_range.end == Beats(0.0)); REQUIRE(it->target_range.start == Seconds::neg_inf()); REQUIRE(it->target_range.end == Seconds(0.0)); REQUIRE(!it->left_anchored); REQUIRE(it->slope == 0.5); ++it; REQUIRE(it->source_range.start == Beats(0.0)); REQUIRE(it->source_range.end == Beats::inf()); REQUIRE(it->target_range.start == Seconds(0.0)); REQUIRE(it->target_range.end == Seconds::inf()); REQUIRE(it->left_anchored); REQUIRE(it->slope == 0.5); ++it; REQUIRE(it == test_tempo_map.end()); // test iterator at auto it2 = test_tempo_map.getSegmentIteratorAt(Beats(-1.0)); auto it3 = test_tempo_map.getSegmentIteratorAt(Beats(0.0)); REQUIRE(it2 == test_tempo_map.begin()); REQUIRE(it3 == std::next(it2)); REQUIRE(findDomainPointForCodomainDelta(test_tempo_map, Beats(0.0), SecondsDelta(1.0)) == Beats(2.0)); REQUIRE(findDomainPointForCodomainDelta(test_tempo_map, Beats(2.0), SecondsDelta(1.0)) == Beats(4.0)); emscripten_console_log( ("findDomainPointForCodomainDelta(Beats(0.0), SecondsDelta(-1.0)) == " + findDomainPointForCodomainDelta(test_tempo_map, Beats(0.0), SecondsDelta(-1.0)) .toString()) .c_str()); REQUIRE(findDomainPointForCodomainDelta(test_tempo_map, Beats(0.0), SecondsDelta(-1.0)) == Beats(-2.0)); } SECTION("tempo map one point") { // in this case, the bps of the single point is used for all domain times auto test_tempo_map = TimelineTempoMap(2.0, {{Beats(4.0), 3.0}}); requireMapPoint(test_tempo_map, Beats(0.0), Seconds(0.0)); requireMapPoint(test_tempo_map, Beats(3.0), Seconds(1.0)); requireMapPoint(test_tempo_map, Beats(-3.0), Seconds(-1.0)); requireMapPoint(test_tempo_map, Beats(30.0), Seconds(10.0)); // iterator should split domain into (-inf, 4.0) and [4.0, inf) auto it = test_tempo_map.begin(); REQUIRE(it->source_range.start == Beats::neg_inf()); REQUIRE(it->source_range.end == Beats(4.0)); REQUIRE(!it->left_anchored); REQUIRE(it->slope == 1.0 / 3.0); ++it; REQUIRE(it->source_range.start == Beats(4.0)); REQUIRE(it->source_range.end == Beats::inf()); REQUIRE(it->left_anchored); REQUIRE(it->slope == 1.0 / 3.0); ++it; REQUIRE(it == test_tempo_map.end()); } SECTION("tempo map two points right of origin") { auto test_tempo_map = TimelineTempoMap(2.0, {{Beats(4.0), 3.0}, {Beats(9.0), 4.0}}); requireMapPoint(test_tempo_map, Beats(0.0), Seconds(0.0)); requireMapPoint(test_tempo_map, Beats(-3.0), Seconds(-1.0)); requireMapPoint(test_tempo_map, Beats(9.0), Seconds(3.0)); requireMapPoint(test_tempo_map, Beats(13.0), Seconds(4.0)); requireMapPoint(test_tempo_map, Beats(17.0), Seconds(5.0)); REQUIRE(findDomainPointForCodomainDelta(test_tempo_map, Beats(4.0), SecondsDelta(-0.0001)) < Beats(4.0)); } SECTION("tempo map two points left of origin") { // This isn't symmetric with "two points right of origin" because rate // changes apply to all points after (right of) the anchor point auto test_tempo_map = TimelineTempoMap(2.0, {{Beats(-100.0), 4.0}, {Beats(-9.0), 3.0}}); requireMapPoint(test_tempo_map, Beats(0.0), Seconds(0.0)); requireMapPoint(test_tempo_map, Beats(3.0), Seconds(1.0)); requireMapPoint(test_tempo_map, Beats(-9.0), Seconds(-3.0)); requireMapPoint(test_tempo_map, Beats(-13.0), Seconds(-4.0)); requireMapPoint(test_tempo_map, Beats(-17.0), Seconds(-5.0)); } SECTION("tempo map three points straddling origin") { auto test_tempo_map = TimelineTempoMap( 2.0, {{Beats(-100.0), 5.0}, {Beats(-3.0), 3.0}, {Beats(6.0), 4.0}}); requireMapPoint(test_tempo_map, Beats(0.0), Seconds(0.0)); requireMapPoint(test_tempo_map, Beats(-3.0), Seconds(-1.0)); requireMapPoint(test_tempo_map, Beats(-13.0), Seconds(-3.0)); requireMapPoint(test_tempo_map, Beats(6.0), Seconds(2.0)); requireMapPoint(test_tempo_map, Beats(10.0), Seconds(3.0)); } SECTION("tempo map stress test") { std::vector segments; for (int i = 0; i < 1000; ++i) { segments.emplace_back(Beats((double)i), 1.0 + i / 1000.0); } auto test_tempo_map = TimelineTempoMap(2.0, std::move(segments)); std::uniform_real_distribution dist(0.0, 1000.0); std::mt19937 gen; for (int i = 0; i < 100; ++i) { auto rand_value = dist(gen); auto result = test_tempo_map.getSegmentIteratorAt(Beats(rand_value)); REQUIRE(result != test_tempo_map.end()); auto mapped = result->map_point(Beats(rand_value)); REQUIRE(mapped.has_value()); auto mapped_val = *mapped; while (result != test_tempo_map.end()) { REQUIRE(result->target_range.end >= mapped_val); ++result; } } } SECTION("loop map") { auto loop_map = GlobalLoopMap(Beats(1.0), Beats(2.0)); auto it = loop_map.begin(); auto it_by_map = loop_map.getSegmentIteratorAt(Beats(-123.0)); REQUIRE(it == it_by_map); REQUIRE(it->source_range.start == Beats::neg_inf()); REQUIRE(it->source_range.end == Beats(1.0)); REQUIRE(it->target_range.start == Beats::neg_inf()); REQUIRE(it->target_range.end == Beats(1.0)); REQUIRE(!it->left_anchored); REQUIRE(it->slope == 1.0); REQUIRE(!it->has_mark(SegmentMarks::SEEK_TO_LOOP_START)); ++it; it_by_map = loop_map.getSegmentIteratorAt(Beats(1.1)); REQUIRE(it == it_by_map); REQUIRE(it->source_range.start == Beats(1.0)); REQUIRE(it->source_range.end == Beats(2.0)); REQUIRE(it->target_range.start == Beats(1.0)); REQUIRE(it->target_range.end == Beats(2.0)); REQUIRE(it->left_anchored); REQUIRE(it->slope == 1.0); REQUIRE(!it->has_mark(SegmentMarks::SEEK_TO_LOOP_START)); ++it; it_by_map = loop_map.getSegmentIteratorAt(Beats(2.1)); REQUIRE(it == it_by_map); REQUIRE(it->source_range.start == Beats(2.0)); REQUIRE(it->source_range.end == Beats(3.0)); REQUIRE(it->target_range.start == Beats(1.0)); REQUIRE(it->target_range.end == Beats(2.0)); REQUIRE(it->left_anchored); REQUIRE(it->slope == 1.0); REQUIRE(it->has_mark(SegmentMarks::SEEK_TO_LOOP_START)); } SECTION("composed loop and tempo map") { auto test_loop_map = TimelineLoopMap(Beats(1.0), Beats(2.0)); auto test_tempo_map = TimelineTempoMap(2.0, {{Beats(0.0), 2.0}, {Beats(1.5), 2.0}, {Beats(1.8), 2.0}, {Beats(2.1), 2.0}}); auto composed_map = CompositionMap(test_loop_map, test_tempo_map); // Test iteration through the composed map auto it = composed_map.getSegmentIteratorAt(Beats(0.9)); REQUIRE(it != composed_map.end()); // First segment (before loop) REQUIRE(it->source_range.end == Beats(1.0)); REQUIRE(it->target_range.end == Seconds(0.5)); // 1.0 beats at 2.0 bpm = 0.5 seconds REQUIRE(it->slope == 0.5); // 1/2.0 bpm = 0.5 seconds per beat REQUIRE(!it->has_mark(SegmentMarks::SEEK_TO_LOOP_START)); ++it; // Segments in first iteration of loop while (it != composed_map.end() && it->source_range.start < Beats(2.0)) { emscripten_console_log("step"); emscripten_console_log(it->toString().c_str()); REQUIRE(it->slope == 0.5); REQUIRE(!it->has_mark(SegmentMarks::SEEK_TO_LOOP_START)); ++it; } // After loop segment emscripten_console_log("out"); emscripten_console_log(it->toString().c_str()); REQUIRE(it->source_range.start == Beats(2.0)); REQUIRE(it->source_range.end > Beats(2.0)); REQUIRE(it->target_range.start == Seconds(0.5)); REQUIRE(it->target_range.end > Seconds(0.5)); REQUIRE(it->slope == 0.5); REQUIRE(it->has_mark(SegmentMarks::SEEK_TO_LOOP_START)); ++it; } SECTION("render context") { auto test_tempo_map = TimelineTempoMap( 2.0, {{Beats(-100.0), 5.0}, {Beats(-3.0), 3.0}, {Beats(4.0), 4.0}}); auto position = Beats(0.0); for (int i = 0; i < 30; i++) { auto newPosition = boundedSubdivide( [&](const time_transform::MappedSegment &segment, time_units::Beats position, time_units::Seconds mappedPosition, size_t offset, size_t frameCount, time_units::BeatsDelta segmentDuration) -> std::optional> { emscripten_console_log( ("position: " + position.toString() + " mappedPosition: " + mappedPosition.toString() + " slope: " + std::to_string(segment.slope) + " offset: " + std::to_string(offset) + " frameCount: " + std::to_string(frameCount)) .c_str()); return std::nullopt; }, test_tempo_map, position, 4410, 256); emscripten_console_log(("position: " + position.toString() + " -> newPosition: " + newPosition.toString()) .c_str()); position = newPosition; } } SECTION("arrangement map") { auto test_loop_map = time_transform::ArrangementBeatLoopMap( Beats(2.0), Beats(1.0), Beats(3.0)); auto test_offset_map = time_transform::ArrangementBeatOffsetMap(Beats(10.0), Beats(20.0)); auto test_arrangement_map = time_transform::ArrangementMap(test_offset_map, test_loop_map); requireMapPoint(test_arrangement_map, Beats(9.0), std::nullopt, time_transform::SegmentMarks::INVALID_ARRANGEMENT_TIME); requireMapPoint(test_arrangement_map, Beats(21.0), std::nullopt, time_transform::SegmentMarks::INVALID_ARRANGEMENT_TIME); requireMapPoint(test_arrangement_map, Beats(10.0), Beats(2.0)); requireMapPoint(test_arrangement_map, Beats(10.5), Beats(2.5)); requireMapPoint(test_arrangement_map, Beats(11.0), Beats(1.0)); auto test_tempo_map = TimelineTempoMap(1.0); auto composed_map = CompositionMap(test_arrangement_map, test_tempo_map); auto position = Beats(9.0); for (int i = 0; i < 30; i++) { auto newPosition = boundedSubdivide( [&](const time_transform::MappedSegment &segment, time_units::Beats position, time_units::Seconds mappedPosition, size_t offset, size_t frameCount, time_units::BeatsDelta segmentDuration) -> std::optional> { emscripten_console_log( ("position: " + position.toString() + " mappedPosition: " + mappedPosition.toString() + " slope: " + std::to_string(segment.slope) + " offset: " + std::to_string(offset) + " frameCount: " + std::to_string(frameCount)) .c_str()); return std::nullopt; }, composed_map, position, 4410, 256); emscripten_console_log(("position: " + position.toString() + " -> newPosition: " + newPosition.toString()) .c_str()); position = newPosition; } } SECTION("warp map") { auto empty_warp_map = WarpMap({}); requireMapPoint(empty_warp_map, Beats(-1.0), Seconds(-1.0)); requireMapPoint(empty_warp_map, Beats(0.0), Seconds(0.0)); requireMapPoint(empty_warp_map, Beats(1.0), Seconds(1.0)); requireMapPoint(empty_warp_map, Beats(2.0), Seconds(2.0)); auto single_marker_warp_map = WarpMap({{Beats(2.0), Seconds(5.0)}}); requireMapPoint(single_marker_warp_map, Beats(1.0), Seconds(4.0)); requireMapPoint(single_marker_warp_map, Beats(2.0), Seconds(5.0)); requireMapPoint(single_marker_warp_map, Beats(3.0), Seconds(6.0)); auto warp_map = WarpMap( {{Beats(0.0), Seconds(0.0)}, {Beats(1.0), Seconds(1.0)}}); requireMapPoint(warp_map, Beats(-1.0), Seconds(-1.0)); requireMapPoint(warp_map, Beats(0.0), Seconds(0.0)); requireMapPoint(warp_map, Beats(1.0), Seconds(1.0)); requireMapPoint(warp_map, Beats(0.5), Seconds(0.5)); requireMapPoint(warp_map, Beats(2.0), Seconds(2.0)); auto warp_map_2 = WarpMap({{Beats(3.0), Seconds(1.0)}, {Beats(4.0), Seconds(11.0)}, {Beats(5.0), Seconds(11.5)}, {Beats(6.0), Seconds(6.0)}}); requireMapPoint(warp_map_2, Beats(2.0), Seconds(-9.0)); requireMapPoint(warp_map_2, Beats(3.0), Seconds(1.0)); requireMapPoint(warp_map_2, Beats(3.2), Seconds(3.0)); requireMapPoint(warp_map_2, Beats(4.0), Seconds(11.0)); requireMapPoint(warp_map_2, Beats(4.5), Seconds(11.25)); requireMapPoint(warp_map_2, Beats(5.0), Seconds(11.5)); requireMapPoint(warp_map_2, Beats(5.1), Seconds(10.95)); requireMapPoint(warp_map_2, Beats(6.0), Seconds(6.0)); requireMapPoint(warp_map_2, Beats(7.0), Seconds(0.5)); } SECTION("warp map iterator behavior") { SECTION("zero markers - identity map") { WarpMap map({}); auto it = map.begin(); REQUIRE(it != map.end()); // Segment 1: (-inf, 0) -> (-inf, 0), slope 1.0 REQUIRE(it->source_range.start == Beats::neg_inf()); REQUIRE(it->source_range.end == Beats(0.0)); REQUIRE(it->target_range.start == Seconds::neg_inf()); REQUIRE(it->target_range.end == Seconds(0.0)); REQUIRE(it->slope == 1.0); REQUIRE(!it->left_anchored); // Right anchored at (0,0) auto it_prev = it; ++it; REQUIRE(it != map.end()); REQUIRE(it != it_prev); --it; REQUIRE(it == it_prev); // Test decrement ++it; // advance again // Segment 2: [0, +inf) -> [0, +inf), slope 1.0 REQUIRE(it->source_range.start == Beats(0.0)); REQUIRE(it->source_range.end == Beats::inf()); REQUIRE(it->target_range.start == Seconds(0.0)); REQUIRE(it->target_range.end == Seconds::inf()); REQUIRE(it->slope == 1.0); REQUIRE(it->left_anchored); // Left anchored at (0,0) it_prev = it; ++it; REQUIRE(it == map.end()); --it; REQUIRE(it == it_prev); // Test decrement from end // Test getSegmentIteratorAt REQUIRE(map.getSegmentIteratorAt(Beats(-10.0)) == map.begin()); REQUIRE(map.getSegmentIteratorAt(Beats(0.0)) == std::next(map.begin())); REQUIRE(map.getSegmentIteratorAt(Beats(10.0)) == std::next(map.begin())); REQUIRE(map.getSegmentIteratorAt(Beats::neg_inf()) == map.begin()); REQUIRE(map.getSegmentIteratorAt(Beats::inf()) == std::next(map.begin())); } SECTION("one marker - translation map") { WarpMap map({{Beats(5.0), Seconds(10.0)}}); auto it = map.begin(); REQUIRE(it != map.end()); // Segment 1: (-inf, 5.0) -> (-inf, 10.0), slope 1.0 REQUIRE(it->source_range.start == Beats::neg_inf()); REQUIRE(it->source_range.end == Beats(5.0)); REQUIRE(it->target_range.start == Seconds::neg_inf()); REQUIRE(it->target_range.end == Seconds(10.0)); REQUIRE(it->slope == 1.0); REQUIRE(!it->left_anchored); // Right anchored at (5,10) auto it_prev = it; ++it; REQUIRE(it != map.end()); REQUIRE(it != it_prev); --it; REQUIRE(it == it_prev); ++it; // Segment 2: [5.0, +inf) -> [10.0, +inf), slope 1.0 REQUIRE(it->source_range.start == Beats(5.0)); REQUIRE(it->source_range.end == Beats::inf()); REQUIRE(it->target_range.start == Seconds(10.0)); REQUIRE(it->target_range.end == Seconds::inf()); REQUIRE(it->slope == 1.0); REQUIRE(it->left_anchored); // Left anchored at (5,10) it_prev = it; ++it; REQUIRE(it == map.end()); --it; REQUIRE(it == it_prev); // Test getSegmentIteratorAt REQUIRE(map.getSegmentIteratorAt(Beats(0.0)) == map.begin()); REQUIRE(map.getSegmentIteratorAt(Beats(5.0)) == std::next(map.begin())); REQUIRE(map.getSegmentIteratorAt(Beats(10.0)) == std::next(map.begin())); REQUIRE(map.getSegmentIteratorAt(Beats::neg_inf()) == map.begin()); REQUIRE(map.getSegmentIteratorAt(Beats::inf()) == std::next(map.begin())); } SECTION("two markers - basic warp") { WarpMap map({{Beats(0.0), Seconds(0.0)}, {Beats(10.0), Seconds(5.0)}}); auto it = map.begin(); REQUIRE(it != map.end()); // Segment 1: Extrapolation before first marker (-inf, 0) -> (-inf, 0), // slope 0.5 REQUIRE(it->source_range.start == Beats::neg_inf()); REQUIRE(it->source_range.end == Beats(0.0)); REQUIRE(it->target_range.start == Seconds::neg_inf()); REQUIRE(it->target_range.end == Seconds(0.0)); REQUIRE(it->slope == 0.5); REQUIRE(!it->left_anchored); // Right anchored at (0,0) auto it_prev = it; ++it; REQUIRE(it != map.end()); REQUIRE(it != it_prev); --it; REQUIRE(it == it_prev); ++it; // Segment 2: Interpolation between markers [0, 10) -> [0, 5), slope 0.5 REQUIRE(it->source_range.start == Beats(0.0)); REQUIRE(it->source_range.end == Beats(10.0)); REQUIRE(it->target_range.start == Seconds(0.0)); REQUIRE(it->target_range.end == Seconds(5.0)); REQUIRE(it->slope == 0.5); REQUIRE(it->left_anchored); // Left anchored at (0,0) it_prev = it; ++it; REQUIRE(it != map.end()); REQUIRE(it != it_prev); --it; REQUIRE(it == it_prev); ++it; // Segment 3: Extrapolation after last marker [10, +inf) -> [5, +inf), // slope 0.5 REQUIRE(it->source_range.start == Beats(10.0)); REQUIRE(it->source_range.end == Beats::inf()); REQUIRE(it->target_range.start == Seconds(5.0)); REQUIRE(it->target_range.end == Seconds::inf()); REQUIRE(it->slope == 0.5); REQUIRE(it->left_anchored); // Left anchored at (10,5) it_prev = it; ++it; REQUIRE(it == map.end()); --it; REQUIRE(it == it_prev); // Test getSegmentIteratorAt auto begin_it = map.begin(); auto second_it = std::next(begin_it); auto third_it = std::next(second_it); REQUIRE(map.getSegmentIteratorAt(Beats(-5.0)) == begin_it); REQUIRE(map.getSegmentIteratorAt(Beats(0.0)) == second_it); REQUIRE(map.getSegmentIteratorAt(Beats(5.0)) == second_it); REQUIRE(map.getSegmentIteratorAt(Beats(10.0)) == third_it); REQUIRE(map.getSegmentIteratorAt(Beats(15.0)) == third_it); REQUIRE(map.getSegmentIteratorAt(Beats::neg_inf()) == begin_it); REQUIRE(map.getSegmentIteratorAt(Beats::inf()) == third_it); } SECTION("iterator bidirectional check") { WarpMap map({{Beats(0.0), Seconds(0.0)}, {Beats(10.0), Seconds(5.0)}, {Beats(20.0), Seconds(15.0)}}); auto it = map.begin(); REQUIRE(it != map.end()); auto initial_it = it; ++it; REQUIRE(it != map.end()); REQUIRE(it != initial_it); --it; REQUIRE(it == initial_it); // Check for std::prev(std::next(it)) == it, for non-end iterators if (it != map.end() && std::next(it) != map.end()) { auto next_it = std::next(it); REQUIRE(std::prev(next_it) == it); } // Iterate to the end and back std::vector::iterator> visited_its; for (auto test_it = map.begin(); test_it != map.end(); ++test_it) { visited_its.push_back(test_it); } REQUIRE(!visited_its.empty()); for (size_t i = visited_its.size(); i > 0; --i) { auto current_from_end = visited_its[i - 1]; if (i < visited_its.size()) { // not the last element from forward iteration auto prev_from_end_iter = visited_its[i]; // This doesn't test operator-- directly on map.end(), but on valid // iterators REQUIRE(std::prev(prev_from_end_iter) == current_from_end); } } // Test decrementing from map.end() auto end_it = map.end(); auto last_valid_it = std::prev(end_it); REQUIRE(last_valid_it != map.end()); REQUIRE(std::next(last_valid_it) == end_it); } } SECTION("tempo map codomain travel") { std::vector segments; for (int i = 0; i < 100; i++) { segments.push_back({Beats(i), 1.0 / (i + 1)}); } auto test_tempo_map = TimelineTempoMap(2.0, std::move(segments)); for (int i = 1; i < 100; i++) { const double rateInThisSegment = 1.0 / i; const double delta = -0.032; const auto res = findDomainPointForCodomainDelta( test_tempo_map, Beats((double)i + 1e-15), SecondsDelta(delta)); REQUIRE(res == Beats((double)i + 1e-15 + rateInThisSegment * delta)); } } }