Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 82 additions & 10 deletions src/avcpp/timestamp.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#pragma once
#pragma once

#include <chrono>

Expand All @@ -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
{
Expand All @@ -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<typename Duration, typename = typename Duration::period>
Timestamp(const Duration& duration)
{
template<typename Duration,
typename = typename Duration::period,
typename = std::enable_if_t<std::is_integral_v<typename Duration::rep>>>
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");
Expand All @@ -32,6 +42,53 @@ class Timestamp
static_cast<int>(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<typename Duration,
typename PrecisionPeriod,
typename = typename Duration::period,
typename = std::enable_if_t<std::is_floating_point_v<typename Duration::rep>>>
constexpr Timestamp(const Duration& duration, PrecisionPeriod)
: Timestamp(duration, Rational{static_cast<int>(PrecisionPeriod::num), static_cast<int>(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<typename Duration,
typename = typename Duration::period,
typename = std::enable_if_t<std::is_floating_point_v<typename Duration::rep>>>
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<ValueType>(Ratio::num) * m_timebase.getDenominator();
ValueType const c = static_cast<ValueType>(Ratio::den) * m_timebase.getNumerator();

m_timestamp = static_cast<int64_t>(duration.count() * b / c);
}


int64_t timestamp() const noexcept;
int64_t timestamp(const Rational &timebase) const noexcept;
const Rational& timebase() const noexcept;
Expand All @@ -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<typename Duration>
Duration toDuration() const
template<typename Duration,
typename = typename Duration::period,
typename = typename Duration::rep>
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<int>(Ratio::num),
static_cast<int>(Ratio::den));
auto ts = m_timebase.rescale(m_timestamp, dstTimebase);
return Duration(ts);
if constexpr (std::is_integral_v<ValueType>) {
Rational dstTimebase(static_cast<int>(Ratio::num),
static_cast<int>(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<ValueType>(Ratio::den);
ValueType const c = m_timebase.getDenominator() * static_cast<ValueType>(Ratio::num);
ValueType const ts = static_cast<ValueType>(m_timestamp) * b / c;
return Duration{ts};
}
}

Timestamp& operator+=(const Timestamp &other);
Expand Down
152 changes: 131 additions & 21 deletions tests/Timestamp.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
#include <catch2/catch_test_macros.hpp>

#include <vector>
#include <version>

#ifdef __cpp_lib_print
# include <format>
Expand All @@ -14,7 +12,54 @@
#endif


using namespace std;
#if AVCPP_CXX_STANDARD < 20 || __cpp_lib_chrono < 201907L
#include <sstream>
#include <iostream>
#include <type_traits>
#include <chrono>

template<typename Rep, typename Period>
std::ostream& operator<<(std::ostream& ost, const std::chrono::duration<Rep, Period>& 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<Period, std::atto>) s << "as"sv;
else if constexpr (std::is_same_v<Period, std::femto>) s << "fs"sv;
else if constexpr (std::is_same_v<Period, std::pico>) s << "fs"sv;
else if constexpr (std::is_same_v<Period, std::nano>) s << "ns"sv;
else if constexpr (std::is_same_v<Period, std::micro>) s << "us"sv;
else if constexpr (std::is_same_v<Period, std::milli>) s << "ms"sv;
else if constexpr (std::is_same_v<Period, std::centi>) s << "cs"sv;
else if constexpr (std::is_same_v<Period, std::deci>) s << "ds"sv;
else if constexpr (std::is_same_v<Period, std::ratio<1>>) s << "s"sv;
else if constexpr (std::is_same_v<Period, std::deca>) s << "das"sv;
else if constexpr (std::is_same_v<Period, std::hecto>) s << "hs"sv;
else if constexpr (std::is_same_v<Period, std::kilo>) s << "ks"sv;
else if constexpr (std::is_same_v<Period, std::mega>) s << "Ms"sv;
else if constexpr (std::is_same_v<Period, std::giga>) s << "Gs"sv;
else if constexpr (std::is_same_v<Period, std::tera>) s << "Ts"sv;
else if constexpr (std::is_same_v<Period, std::peta>) s << "Ps"sv;
else if constexpr (std::is_same_v<Period, std::exa>) s << "Es"sv;
else if constexpr (std::is_same_v<Period, std::ratio<60>>) s << "min"sv;
else if constexpr (std::is_same_v<Period, std::ratio<3600>>) s << "h"sv;
else if constexpr (std::is_same_v<Period, std::ratio<86400>>) 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 <catch2/catch_test_macros.hpp>

TEST_CASE("Core::Timestamp", "Timestamp")
{
Expand All @@ -37,26 +82,91 @@ TEST_CASE("Core::Timestamp", "Timestamp")
CHECK(v1 == v2);
CHECK(v1 == v3);
}
}

SECTION("Floating point std::duration")
{
{
std::chrono::duration<double> seconds{1.53f};

av::Timestamp fromDuration{std::chrono::duration_cast<std::chrono::milliseconds>(seconds)};

INFO("std::chrono::duration<double>: " << 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<std::chrono::duration<double>>();
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<double, Ratio> ms{1530.17};
av::Timestamp fromDuration{std::chrono::duration_cast<std::chrono::microseconds>(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<std::chrono::duration<double, Ratio>>();
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<double, Ratio> dur{1530.17};
av::Timestamp fromDuration{std::chrono::duration_cast<std::chrono::nanoseconds>(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<std::chrono::duration<double, Ratio>>();
INFO("toDoubleDuration: " << toDoubleDuration);
REQUIRE(std::abs(toDoubleDuration.count() - dur.count()) <= 0.0001);
}

// Ctor from the floating point duration
{
std::chrono::duration<double> seconds{1.57f};

{
av::Timestamp fromDuration{seconds, std::milli{}};

auto const precision = fromDuration.timebase().getDouble();

INFO("std::chrono::duration<double>:count: " << seconds.count());
INFO("std::chrono::duration<double>: " << seconds);
INFO("av::Timestamp::seconds: " << fromDuration.seconds());
INFO("av::Timestamp: " << fromDuration);
REQUIRE(std::abs(fromDuration.seconds() - seconds.count()) < precision);

auto toDoubleDuration = fromDuration.toDuration<std::chrono::duration<double>>();
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<double>:count: " << seconds.count());
INFO("std::chrono::duration<double>: " << seconds);
INFO("av::Timestamp::seconds: " << fromDuration.seconds());
INFO("av::Timestamp: " << fromDuration);
REQUIRE(std::abs(fromDuration.seconds() - seconds.count()) < precision);

auto toDoubleDuration = fromDuration.toDuration<std::chrono::duration<double>>();
INFO("toDoubleDuration: " << toDoubleDuration);
REQUIRE(std::abs(toDoubleDuration.count() - seconds.count()) < precision);
}
}
#endif
}


Expand Down
Loading