diff --git a/src/avcpp/timestamp.h b/src/avcpp/timestamp.h index 28ce40c4..693ab097 100644 --- a/src/avcpp/timestamp.h +++ b/src/avcpp/timestamp.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include @@ -9,6 +9,10 @@ namespace av { /** * @brief The Timestamp class represents timestamp value and it timebase + * + * Timestamp class can be treated as Fixed Point time representation where timestamp itself is a value and Time Base + * is a multiplicator. Be careful with construct Timestamp from the std::chrono::duration with floating point @a rep + * */ class Timestamp { @@ -18,10 +22,16 @@ class Timestamp /** * @brief Create AvCpp/FFmpeg compatible timestamp value from the std::chrono::duration/boost::chrono::duration + * + * Duration class must not be a floating point to avoid lost precision: Timestamp in the FFmpeg internally are + * int64_t with integer AVRAtional as timebase that also uses int64_t internally to store numerator and denominator. + * */ - template - Timestamp(const Duration& duration) - { + template>> + constexpr Timestamp(const Duration& duration) + { using Ratio = typename Duration::period; static_assert(Ratio::num <= INT_MAX, "To prevent precision lost, ratio numerator must be less then INT_MAX"); @@ -32,6 +42,53 @@ class Timestamp static_cast(Ratio::den)); } + /** + * @brief Create AvCpp/FFmpeg compatible timestamp value from the floating point + * std::chrono::duration/boost::chrono::duration with given precision. + * + * PrecisionPeriod defines holded TimeBase + * + */ + template>> + constexpr Timestamp(const Duration& duration, PrecisionPeriod) + : Timestamp(duration, Rational{static_cast(PrecisionPeriod::num), static_cast(PrecisionPeriod::den)}) + { + using Ratio = typename Duration::period; + static_assert(Ratio::num <= INT_MAX, "To prevent precision lost, ratio numerator must be less then INT_MAX"); + static_assert(Ratio::den <= INT_MAX, "To prevent precision lost, ratio denominator must be less then INT_MAX"); + static_assert(PrecisionPeriod::num <= INT_MAX, "To prevent precision lost, ratio numerator must be less then INT_MAX"); + static_assert(PrecisionPeriod::den <= INT_MAX, "To prevent precision lost, ratio denominator must be less then INT_MAX"); + } + + /** + * @brief Create AvCpp/FFmpeg compatible timestamp value from the floating point + * std::chrono::duration/boost::chrono::duration with given precision. + * + */ + template>> + constexpr Timestamp(const Duration& duration, const Rational& timebase) + : m_timebase(timebase) + { + using ValueType = typename Duration::rep; + using Ratio = typename Duration::period; + + static_assert(Ratio::num <= INT_MAX, "To prevent precision lost, ratio numerator must be less then INT_MAX"); + static_assert(Ratio::den <= INT_MAX, "To prevent precision lost, ratio denominator must be less then INT_MAX"); + + // rescale input ticks into integer one + // m_timestamp = ts * Raio / m_timebase + ValueType const b = static_cast(Ratio::num) * m_timebase.getDenominator(); + ValueType const c = static_cast(Ratio::den) * m_timebase.getNumerator(); + + m_timestamp = static_cast(duration.count() * b / c); + } + + int64_t timestamp() const noexcept; int64_t timestamp(const Rational &timebase) const noexcept; const Rational& timebase() const noexcept; @@ -45,19 +102,34 @@ class Timestamp /** * @brief Convert to the std::chrono::duration compatible value + * + * It possible to convert to the floating point duration without additional casts. + * */ - template - Duration toDuration() const + template + constexpr Duration toDuration() const { + using ValueType = typename Duration::rep; using Ratio = typename Duration::period; static_assert(Ratio::num <= INT_MAX, "To prevent precision lost, ratio numerator must be less then INT_MAX"); static_assert(Ratio::den <= INT_MAX, "To prevent precision lost, ratio denominator must be less then INT_MAX"); - Rational dstTimebase(static_cast(Ratio::num), - static_cast(Ratio::den)); - auto ts = m_timebase.rescale(m_timestamp, dstTimebase); - return Duration(ts); + if constexpr (std::is_integral_v) { + Rational dstTimebase(static_cast(Ratio::num), + static_cast(Ratio::den)); + auto ts = m_timebase.rescale(m_timestamp, dstTimebase); + return Duration(ts); + } else { + namespace dt = std::chrono; + // ts = m_timestamp * m_timebase / dstTimebase + ValueType const b = m_timebase.getNumerator() * static_cast(Ratio::den); + ValueType const c = m_timebase.getDenominator() * static_cast(Ratio::num); + ValueType const ts = static_cast(m_timestamp) * b / c; + return Duration{ts}; + } } Timestamp& operator+=(const Timestamp &other); diff --git a/tests/Timestamp.cpp b/tests/Timestamp.cpp index 037474a5..49cf7d23 100644 --- a/tests/Timestamp.cpp +++ b/tests/Timestamp.cpp @@ -1,6 +1,4 @@ -#include - -#include +#include #ifdef __cpp_lib_print # include @@ -14,7 +12,54 @@ #endif -using namespace std; +#if AVCPP_CXX_STANDARD < 20 || __cpp_lib_chrono < 201907L +#include +#include +#include +#include + +template +std::ostream& operator<<(std::ostream& ost, const std::chrono::duration& dur) +{ + using namespace std::literals; + + std::ostringstream s; + s.flags(ost.flags()); + s.imbue(ost.getloc()); + s.precision(ost.precision()); + + s << +dur.count(); + + // Ref: https://en.cppreference.com/w/cpp/chrono/duration/operator_ltlt.html + if constexpr (std::is_same_v) s << "as"sv; + else if constexpr (std::is_same_v) s << "fs"sv; + else if constexpr (std::is_same_v) s << "fs"sv; + else if constexpr (std::is_same_v) s << "ns"sv; + else if constexpr (std::is_same_v) s << "us"sv; + else if constexpr (std::is_same_v) s << "ms"sv; + else if constexpr (std::is_same_v) s << "cs"sv; + else if constexpr (std::is_same_v) s << "ds"sv; + else if constexpr (std::is_same_v>) s << "s"sv; + else if constexpr (std::is_same_v) s << "das"sv; + else if constexpr (std::is_same_v) s << "hs"sv; + else if constexpr (std::is_same_v) s << "ks"sv; + else if constexpr (std::is_same_v) s << "Ms"sv; + else if constexpr (std::is_same_v) s << "Gs"sv; + else if constexpr (std::is_same_v) s << "Ts"sv; + else if constexpr (std::is_same_v) s << "Ps"sv; + else if constexpr (std::is_same_v) s << "Es"sv; + else if constexpr (std::is_same_v>) s << "min"sv; + else if constexpr (std::is_same_v>) s << "h"sv; + else if constexpr (std::is_same_v>) s << "d"sv; + else if constexpr (Period::den == 1) s << '[' << Period::num << "]s"sv; + else + s << '[' << Period::num << '/' << Period::num << "]s"sv; + ost << std::move(s).str(); + return ost; +} +#endif + +#include TEST_CASE("Core::Timestamp", "Timestamp") { @@ -37,26 +82,91 @@ TEST_CASE("Core::Timestamp", "Timestamp") CHECK(v1 == v2); CHECK(v1 == v3); } + } + + SECTION("Floating point std::duration") + { + { + std::chrono::duration seconds{1.53f}; + + av::Timestamp fromDuration{std::chrono::duration_cast(seconds)}; + + INFO("std::chrono::duration: " << seconds.count()); + INFO("av::Timestamp::seconds: " << fromDuration.seconds()); + INFO("av::Timestamp: " << fromDuration); + REQUIRE(std::abs(fromDuration.seconds() - seconds.count()) <= 0.001); + + auto toDoubleDuration = fromDuration.toDuration>(); + INFO("toDoubleDuration: " << toDoubleDuration); + REQUIRE(std::abs(toDoubleDuration.count() - seconds.count()) <= 0.001); + } + + // Double and Ratio different to {1,1} + { + using Ratio = std::milli; + + std::chrono::duration ms{1530.17}; + av::Timestamp fromDuration{std::chrono::duration_cast(ms)}; + INFO("fromDuration(ms): " << fromDuration.seconds() + << "s, val: " << fromDuration); + REQUIRE(std::abs(fromDuration.seconds() - ms.count() * Ratio::num / Ratio::den) <= 0.00001); -#if 0 - av::Timestamp t1(48000, av::Rational {1, 48000}); // 1s - av::Timestamp t2 = t1; - av::Timestamp t3 = t1; - for (int64_t i = 0; i < 4194258; ++i) { - t1 = t1 + av::Timestamp {1024, t1.timebase()}; // fail case - t2 += av::Timestamp {1024, t2.timebase()}; // good - t3 = av::Timestamp {1024, t1.timebase()} + t3; - - CHECK(t3 == t2); - CHECK(t1 == t2); - CHECK(t1.seconds() >= 1.0); - - if (t1 != t2 || t2 != t3 || t1.seconds() < 1.0) { - CHECK(i < 0); // always fail, just for report - break; + auto toDoubleDuration = fromDuration.toDuration>(); + INFO("toDoubleDuration: " << toDoubleDuration); + REQUIRE(std::abs(toDoubleDuration.count() - ms.count()) <= 0.00001); + } + + // Double and non-standard ratio + { + using Ratio = std::ratio<1, 48000>; + + std::chrono::duration dur{1530.17}; + av::Timestamp fromDuration{std::chrono::duration_cast(dur)}; + INFO("fromDuration(dur): " << fromDuration.seconds() + << "s, val: " << fromDuration); + REQUIRE(std::abs(fromDuration.seconds() - dur.count() * Ratio::num / Ratio::den) <= 0.00001); + + auto toDoubleDuration = fromDuration.toDuration>(); + INFO("toDoubleDuration: " << toDoubleDuration); + REQUIRE(std::abs(toDoubleDuration.count() - dur.count()) <= 0.0001); + } + + // Ctor from the floating point duration + { + std::chrono::duration seconds{1.57f}; + + { + av::Timestamp fromDuration{seconds, std::milli{}}; + + auto const precision = fromDuration.timebase().getDouble(); + + INFO("std::chrono::duration:count: " << seconds.count()); + INFO("std::chrono::duration: " << seconds); + INFO("av::Timestamp::seconds: " << fromDuration.seconds()); + INFO("av::Timestamp: " << fromDuration); + REQUIRE(std::abs(fromDuration.seconds() - seconds.count()) < precision); + + auto toDoubleDuration = fromDuration.toDuration>(); + INFO("toDoubleDuration: " << toDoubleDuration); + REQUIRE(std::abs(toDoubleDuration.count() - seconds.count()) < precision); + } + + { + av::Timestamp fromDuration{seconds, av::Rational{1, 1000}}; + + auto const precision = fromDuration.timebase().getDouble(); + + INFO("std::chrono::duration:count: " << seconds.count()); + INFO("std::chrono::duration: " << seconds); + INFO("av::Timestamp::seconds: " << fromDuration.seconds()); + INFO("av::Timestamp: " << fromDuration); + REQUIRE(std::abs(fromDuration.seconds() - seconds.count()) < precision); + + auto toDoubleDuration = fromDuration.toDuration>(); + INFO("toDoubleDuration: " << toDoubleDuration); + REQUIRE(std::abs(toDoubleDuration.count() - seconds.count()) < precision); } } -#endif }