Skip to content

Commit 00e29ea

Browse files
committed
Timestamp: fix work for floating point chrono::duration
Fix both: construction and toDuration() conversion. Due to possible precision lost on construction from floating point chrono::duration, av::Timestamp contructors splits for integral and floating point. For the floating point chrono::duration's user must provide required precision. Fix #169
1 parent b02f110 commit 00e29ea

2 files changed

Lines changed: 165 additions & 30 deletions

File tree

src/avcpp/timestamp.h

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#pragma once
1+
#pragma once
22

33
#include <chrono>
44

@@ -9,6 +9,10 @@ namespace av {
99

1010
/**
1111
* @brief The Timestamp class represents timestamp value and it timebase
12+
*
13+
* Timestamp class can be treated as Fixed Point time representation where timestamp itself is a value and Time Base
14+
* is a multiplicator. Be careful with construct Timestamp from the std::chrono::duration with floating point @a rep
15+
*
1216
*/
1317
class Timestamp
1418
{
@@ -18,10 +22,16 @@ class Timestamp
1822

1923
/**
2024
* @brief Create AvCpp/FFmpeg compatible timestamp value from the std::chrono::duration/boost::chrono::duration
25+
*
26+
* Duration class must not be a floating point to avoid lost precision: Timestamp in the FFmpeg internally are
27+
* int64_t with integer AVRAtional as timebase that also uses int64_t internally to store numerator and denominator.
28+
*
2129
*/
22-
template<typename Duration, typename = typename Duration::period>
23-
Timestamp(const Duration& duration)
24-
{
30+
template<typename Duration,
31+
typename = typename Duration::period,
32+
typename = std::enable_if_t<std::is_integral_v<typename Duration::rep>>>
33+
constexpr Timestamp(const Duration& duration)
34+
{
2535
using Ratio = typename Duration::period;
2636

2737
static_assert(Ratio::num <= INT_MAX, "To prevent precision lost, ratio numerator must be less then INT_MAX");
@@ -32,6 +42,53 @@ class Timestamp
3242
static_cast<int>(Ratio::den));
3343
}
3444

45+
/**
46+
* @brief Create AvCpp/FFmpeg compatible timestamp value from the floating point
47+
* std::chrono::duration/boost::chrono::duration with given precision.
48+
*
49+
* PrecisionPeriod defines holded TimeBase
50+
*
51+
*/
52+
template<typename Duration,
53+
typename PrecisionPeriod,
54+
typename = typename Duration::period,
55+
typename = std::enable_if_t<std::is_floating_point_v<typename Duration::rep>>>
56+
constexpr Timestamp(const Duration& duration, PrecisionPeriod)
57+
: Timestamp(duration, Rational{static_cast<int>(PrecisionPeriod::num), static_cast<int>(PrecisionPeriod::den)})
58+
{
59+
using Ratio = typename Duration::period;
60+
static_assert(Ratio::num <= INT_MAX, "To prevent precision lost, ratio numerator must be less then INT_MAX");
61+
static_assert(Ratio::den <= INT_MAX, "To prevent precision lost, ratio denominator must be less then INT_MAX");
62+
static_assert(PrecisionPeriod::num <= INT_MAX, "To prevent precision lost, ratio numerator must be less then INT_MAX");
63+
static_assert(PrecisionPeriod::den <= INT_MAX, "To prevent precision lost, ratio denominator must be less then INT_MAX");
64+
}
65+
66+
/**
67+
* @brief Create AvCpp/FFmpeg compatible timestamp value from the floating point
68+
* std::chrono::duration/boost::chrono::duration with given precision.
69+
*
70+
*/
71+
template<typename Duration,
72+
typename = typename Duration::period,
73+
typename = std::enable_if_t<std::is_floating_point_v<typename Duration::rep>>>
74+
constexpr Timestamp(const Duration& duration, const Rational& timebase)
75+
: m_timebase(timebase)
76+
{
77+
using ValueType = typename Duration::rep;
78+
using Ratio = typename Duration::period;
79+
80+
static_assert(Ratio::num <= INT_MAX, "To prevent precision lost, ratio numerator must be less then INT_MAX");
81+
static_assert(Ratio::den <= INT_MAX, "To prevent precision lost, ratio denominator must be less then INT_MAX");
82+
83+
// rescale input ticks into integer one
84+
// m_timestamp = ts * Raio / m_timebase
85+
ValueType const b = static_cast<ValueType>(Ratio::num) * m_timebase.getDenominator();
86+
ValueType const c = static_cast<ValueType>(Ratio::den) * m_timebase.getNumerator();
87+
88+
m_timestamp = static_cast<int64_t>(duration.count() * b / c);
89+
}
90+
91+
3592
int64_t timestamp() const noexcept;
3693
int64_t timestamp(const Rational &timebase) const noexcept;
3794
const Rational& timebase() const noexcept;
@@ -45,19 +102,34 @@ class Timestamp
45102

46103
/**
47104
* @brief Convert to the std::chrono::duration compatible value
105+
*
106+
* It possible to convert to the floating point duration without additional casts.
107+
*
48108
*/
49-
template<typename Duration>
50-
Duration toDuration() const
109+
template<typename Duration,
110+
typename = typename Duration::period,
111+
typename = typename Duration::rep>
112+
constexpr Duration toDuration() const
51113
{
114+
using ValueType = typename Duration::rep;
52115
using Ratio = typename Duration::period;
53116

54117
static_assert(Ratio::num <= INT_MAX, "To prevent precision lost, ratio numerator must be less then INT_MAX");
55118
static_assert(Ratio::den <= INT_MAX, "To prevent precision lost, ratio denominator must be less then INT_MAX");
56119

57-
Rational dstTimebase(static_cast<int>(Ratio::num),
58-
static_cast<int>(Ratio::den));
59-
auto ts = m_timebase.rescale(m_timestamp, dstTimebase);
60-
return Duration(ts);
120+
if constexpr (std::is_integral_v<ValueType>) {
121+
Rational dstTimebase(static_cast<int>(Ratio::num),
122+
static_cast<int>(Ratio::den));
123+
auto ts = m_timebase.rescale(m_timestamp, dstTimebase);
124+
return Duration(ts);
125+
} else {
126+
namespace dt = std::chrono;
127+
// ts = m_timestamp * m_timebase / dstTimebase
128+
ValueType const b = m_timebase.getNumerator() * static_cast<ValueType>(Ratio::den);
129+
ValueType const c = m_timebase.getDenominator() * static_cast<ValueType>(Ratio::num);
130+
ValueType const ts = static_cast<ValueType>(m_timestamp) * b / c;
131+
return Duration{ts};
132+
}
61133
}
62134

63135
Timestamp& operator+=(const Timestamp &other);

tests/Timestamp.cpp

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
#include <catch2/catch_test_macros.hpp>
2-
3-
#include <vector>
1+
#include <catch2/catch_test_macros.hpp>
42

53
#ifdef __cpp_lib_print
64
# include <format>
@@ -37,26 +35,91 @@ TEST_CASE("Core::Timestamp", "Timestamp")
3735
CHECK(v1 == v2);
3836
CHECK(v1 == v3);
3937
}
38+
}
39+
40+
SECTION("Floating point std::duration")
41+
{
42+
{
43+
std::chrono::duration<double> seconds{1.53f};
44+
45+
av::Timestamp fromDuration{std::chrono::duration_cast<std::chrono::milliseconds>(seconds)};
46+
47+
INFO("std::chrono::duration<double>: " << seconds.count());
48+
INFO("av::Timestamp::seconds: " << fromDuration.seconds());
49+
INFO("av::Timestamp: " << fromDuration);
50+
REQUIRE(std::abs(fromDuration.seconds() - seconds.count()) <= 0.001);
51+
52+
auto toDoubleDuration = fromDuration.toDuration<std::chrono::duration<double>>();
53+
INFO("toDoubleDuration: " << toDoubleDuration);
54+
REQUIRE(std::abs(toDoubleDuration.count() - seconds.count()) <= 0.001);
55+
}
56+
57+
// Double and Ratio different to {1,1}
58+
{
59+
using Ratio = std::milli;
60+
61+
std::chrono::duration<double, Ratio> ms{1530.17};
62+
av::Timestamp fromDuration{std::chrono::duration_cast<std::chrono::microseconds>(ms)};
63+
INFO("fromDuration(ms): " << fromDuration.seconds()
64+
<< "s, val: " << fromDuration);
65+
REQUIRE(std::abs(fromDuration.seconds() - ms.count() * Ratio::num / Ratio::den) <= 0.00001);
66+
67+
auto toDoubleDuration = fromDuration.toDuration<std::chrono::duration<double, Ratio>>();
68+
INFO("toDoubleDuration: " << toDoubleDuration);
69+
REQUIRE(std::abs(toDoubleDuration.count() - ms.count()) <= 0.00001);
70+
}
71+
72+
// Double and non-standard ratio
73+
{
74+
using Ratio = std::ratio<1, 48000>;
75+
76+
std::chrono::duration<double, Ratio> dur{1530.17};
77+
av::Timestamp fromDuration{std::chrono::duration_cast<std::chrono::nanoseconds>(dur)};
78+
INFO("fromDuration(dur): " << fromDuration.seconds()
79+
<< "s, val: " << fromDuration);
80+
REQUIRE(std::abs(fromDuration.seconds() - dur.count() * Ratio::num / Ratio::den) <= 0.00001);
81+
82+
auto toDoubleDuration = fromDuration.toDuration<std::chrono::duration<double, Ratio>>();
83+
INFO("toDoubleDuration: " << toDoubleDuration);
84+
REQUIRE(std::abs(toDoubleDuration.count() - dur.count()) <= 0.0001);
85+
}
86+
87+
// Ctor from the floating point duration
88+
{
89+
std::chrono::duration<double> seconds{1.57f};
90+
91+
{
92+
av::Timestamp fromDuration{seconds, std::milli{}};
93+
94+
auto const precision = fromDuration.timebase().getDouble();
4095

41-
#if 0
42-
av::Timestamp t1(48000, av::Rational {1, 48000}); // 1s
43-
av::Timestamp t2 = t1;
44-
av::Timestamp t3 = t1;
45-
for (int64_t i = 0; i < 4194258; ++i) {
46-
t1 = t1 + av::Timestamp {1024, t1.timebase()}; // fail case
47-
t2 += av::Timestamp {1024, t2.timebase()}; // good
48-
t3 = av::Timestamp {1024, t1.timebase()} + t3;
49-
50-
CHECK(t3 == t2);
51-
CHECK(t1 == t2);
52-
CHECK(t1.seconds() >= 1.0);
53-
54-
if (t1 != t2 || t2 != t3 || t1.seconds() < 1.0) {
55-
CHECK(i < 0); // always fail, just for report
56-
break;
96+
INFO("std::chrono::duration<double>:count: " << seconds.count());
97+
INFO("std::chrono::duration<double>: " << seconds);
98+
INFO("av::Timestamp::seconds: " << fromDuration.seconds());
99+
INFO("av::Timestamp: " << fromDuration);
100+
REQUIRE(std::abs(fromDuration.seconds() - seconds.count()) < precision);
101+
102+
auto toDoubleDuration = fromDuration.toDuration<std::chrono::duration<double>>();
103+
INFO("toDoubleDuration: " << toDoubleDuration);
104+
REQUIRE(std::abs(toDoubleDuration.count() - seconds.count()) < precision);
105+
}
106+
107+
{
108+
av::Timestamp fromDuration{seconds, av::Rational{1, 1000}};
109+
110+
auto const precision = fromDuration.timebase().getDouble();
111+
112+
INFO("std::chrono::duration<double>:count: " << seconds.count());
113+
INFO("std::chrono::duration<double>: " << seconds);
114+
INFO("av::Timestamp::seconds: " << fromDuration.seconds());
115+
INFO("av::Timestamp: " << fromDuration);
116+
REQUIRE(std::abs(fromDuration.seconds() - seconds.count()) < precision);
117+
118+
auto toDoubleDuration = fromDuration.toDuration<std::chrono::duration<double>>();
119+
INFO("toDoubleDuration: " << toDoubleDuration);
120+
REQUIRE(std::abs(toDoubleDuration.count() - seconds.count()) < precision);
57121
}
58122
}
59-
#endif
60123
}
61124

62125

0 commit comments

Comments
 (0)