Timehandling: choose safer representation for fractional seconds (closes #939)

When drafting the time handling framework some years ago,
I foresaw the possible danger of mixing up numbers relating
to fractional seconds, with other plain numbers intended as
frame counts or as micro ticks. Thus I deliberately picked
an incompatible integer type for FSecs = boost::rational<long>

However, using long is problematic in itself, since its actual
bit length is not fixed, and especially on 32bit platforms long
is quite surprisingly defined to be the same as int.

However, meanwhile, using the new C++ features, I have blocked
pretty much any possible implicit conversion path, requiring
explicit conversions in the relevant ctor invocations. So,
after weighting in the alternatives, FSecs is now defined
as boost::rational<int64_t>.
This commit is contained in:
Fischlurch 2020-02-17 02:36:54 +01:00
parent 8867ae55ad
commit 38837da65e
7 changed files with 214 additions and 48 deletions

View file

@ -24,7 +24,7 @@
/** @file grid.hpp
** definition of a time grid abstraction for time and timecode handling.
** This interface is the foundation to deal with _quantised_ (grid aligned)
** time values, as is essential for handling of timecode data.
** time values, which is essential for handling of timecode data.
*/
@ -43,12 +43,13 @@ namespace time {
/**
* Abstraction of a value alignment grid.
* Such a grid has an underlying scale (origin and measurement)
* and is comprised of distinct grid intervals, which can be addressed
* by a ordering number, centred at origin with interval number zero.
* The typical example is a 25fps time frame grid, but indeed the
* spacing of the intervals is not necessarily constant. An entity
* defining such a grid provides functions to calculate the
* grid coordinates and to convert back to plain values.
* and is comprised of consecutive grid intervals, joined at the
* _grid points._ These can be addressed by an ordering number,
* centred at origin with grid point number zero.
* The classical example is a 25fps time frame grid, but in fact
* the length of the intervals is not necessarily constant. An
* entity defining such a grid provides functions to calculate
* the grid coordinates and to convert back to plain values.
* This includes a way of rounding to the next lower
* grid point, usable for grid aligning values.
*

View file

@ -55,6 +55,7 @@ extern "C" {
#include <math.h>
#include <limits>
#include <string>
#include <sstream>
#include <boost/rational.hpp>
#include <boost/lexical_cast.hpp>
@ -73,6 +74,9 @@ namespace error = lumiera::error;
namespace lib {
namespace meta {
extern const std::string FAILURE_INDICATOR;
}
namespace time {
@ -175,17 +179,30 @@ namespace time {
}
namespace {
template<typename INT>
string
renderFraction (INT const& frac, Literal postfx) noexcept
try {
std::ostringstream buffer;
if (1 == frac.denominator() or 0 == frac.numerator())
buffer << frac.numerator() << postfx;
else
buffer << frac <<postfx;
return buffer.str();
}
catch(...)
{ return meta::FAILURE_INDICATOR; }
}
/** visual framerate representation (for diagnostics) */
FrameRate::operator string() const
{
return 1==denominator() ? lexical_cast<string> (numerator())+"FPS"
: lexical_cast<string> (numerator())
+ "/"
+ lexical_cast<string> (denominator())
+ "FPS";
return renderFraction (*this, "FPS");
}
/** @internal backdoor to sneak in a raw time value
* bypassing any normalisation and limiting */
TimeValue
@ -247,6 +264,13 @@ namespace time {
}} // namespace lib::Time
namespace util {
string
StringConv<lib::time::FSecs, void>::invoke (lib::time::FSecs val) noexcept
{
return lib::time::renderFraction (val, "sec");
}
} // namespace util
@ -285,7 +309,7 @@ lumiera_tmpbuf_print_time (gavl_time_t time)
gavl_time_t
lumiera_rational_to_time (FSecs const& fractionalSeconds)
{
return rational_cast<gavl_time_t> (lib::time::TimeValue::SCALE * fractionalSeconds);
return rational_cast<gavl_time_t> (fractionalSeconds * int{lib::time::TimeValue::SCALE});
}
gavl_time_t
@ -304,7 +328,7 @@ lumiera_frame_duration (FrameRate const& fps)
throw error::Logic ("Impossible to quantise to an zero spaced frame grid"
, error::LUMIERA_ERROR_BOTTOM_VALUE);
FSecs duration = rational_cast<FSecs> (1/fps);
FSecs duration = 1 / fps;
return lumiera_rational_to_time (duration);
}

View file

@ -122,14 +122,14 @@ namespace time {
static regex fracSecs_parser ("(?:^|[^\\./\\d\\-])(\\-?\\d+)(?:([\\-\\+]\\d+)?/(\\d+))?sec");
//__no leading[./-\d] number [+-] number '/' number 'sec'
#define SUB_EXPR(N) lexical_cast<long> (match[N])
#define SUB_EXPR(N) lexical_cast<int> (match[N])
smatch match;
if (regex_search (seconds, match, fracSecs_parser))
if (match[2].matched)
{
// complete spec with all parts
FSecs fractionalPart (SUB_EXPR(2), SUB_EXPR(3));
long fullSeconds (SUB_EXPR(1));
int fullSeconds (SUB_EXPR(1));
return grid.timeOf (fullSeconds + fractionalPart);
}
else

View file

@ -205,7 +205,7 @@ namespace time {
/** rational representation of fractional seconds
* @warning do not mix up gavl_time_t and FSecs */
typedef boost::rational<long> FSecs;
typedef boost::rational<int64_t> FSecs;
/**
@ -550,12 +550,10 @@ namespace time {
class FrameRate
: public boost::rational<uint>
{
typedef boost::rational<uint> IFrac;
public:
FrameRate (uint fps) ;
FrameRate (uint num, uint denom);
FrameRate (IFrac const& fractionalRate);
FrameRate (boost::rational<uint> const& fractionalRate);
// standard copy acceptable;
@ -576,7 +574,7 @@ namespace time {
inline FSecs
operator/ (int n, FrameRate rate)
{
return n / boost::rational_cast<FSecs> (rate);
return FSecs{ n*rate.denominator(), rate.numerator()};
}
@ -621,17 +619,17 @@ namespace time {
inline
FrameRate::FrameRate (uint fps)
: IFrac (__ensure_nonzero(fps))
: boost::rational<uint> (__ensure_nonzero(fps))
{ }
inline
FrameRate::FrameRate (uint num, uint denom)
: IFrac (__ensure_nonzero(num), denom)
: boost::rational<uint> (__ensure_nonzero(num), denom)
{ }
inline
FrameRate::FrameRate (IFrac const& fractionalRate)
: IFrac (__ensure_nonzero(fractionalRate))
FrameRate::FrameRate (boost::rational<uint> const& fractionalRate)
: boost::rational<uint> (__ensure_nonzero(fractionalRate))
{ }
inline double
@ -654,5 +652,16 @@ namespace util {
return 0 == dur;
}
// repeated or forward declaration, see meta/util.hpp
template<typename X, typename COND>
struct StringConv;
/** specialisation: render fractional seconds (for diagnostics) */
template<>
struct StringConv<lib::time::FSecs, void>
{
static std::string
invoke (lib::time::FSecs) noexcept;
};
}
#endif
#endif /*LIB_TIME_TIMEVALUE_H*/

View file

@ -43,8 +43,8 @@ namespace test{
namespace {
const uint MAX_FRAMES = 25*500;
const uint DIRT_GRAIN = 50;
const int MAX_FRAMES = 25*500;
const int DIRT_GRAIN = 50;
const FSecs F25(1,25); // duration of one PAL frame
@ -91,10 +91,10 @@ namespace test{
{
FixedFrameQuantiser fixQ(25);
uint frames = (rand() % MAX_FRAMES);
int frames = (rand() % MAX_FRAMES);
FSecs dirt = (F25 / (2 + rand() % DIRT_GRAIN));
Time rawTime = Time(frames*F25) + Duration(dirt); ////////////////TICKET #939 : should better use 64bit base type for FSecs ??
Time rawTime = Time(frames*F25) + Duration(dirt);
CHECK (Time( frames *F25) <= rawTime);
CHECK (Time((frames+1)*F25) > rawTime);

View file

@ -52,8 +52,8 @@ namespace test {
namespace { // Test definitions...
const Time testOrigin (12,34);
const FrameRate testFps (5,6);
const Time TEST_ORIGIN (12,34);
const FrameRate TEST_FPS (5,6);
const uint MAX_FRAMES = 1000;
const uint DIRT_GRAIN = 50;
@ -89,8 +89,8 @@ namespace test {
CHECK ( spec.origin == TimeValue(0));
CHECK (!spec.predecessor);
spec.fps = testFps;
spec.origin = testOrigin;
spec.fps = TEST_FPS;
spec.origin = TEST_ORIGIN;
PGrid myGrid = spec.commit();
CHECK (myGrid);
@ -101,15 +101,15 @@ namespace test {
int randomFrame = (rand() % MAX_FRAMES);
Time point (myGrid->timeOf (randomFrame));
CHECK (point == testOrigin + randomFrame * testFps.duration());
CHECK (point == TEST_ORIGIN + randomFrame * TEST_FPS.duration());
uint fract = 2 + rand() % DIRT_GRAIN;
FSecs dirt = rational_cast<FSecs> (1 / testFps / fract);
ASSERT (Time(dirt) < testFps.duration());
int fract = 2 + rand() % DIRT_GRAIN;
FSecs dirt = (1/TEST_FPS) / fract;
ASSERT (Time(dirt) < TEST_FPS.duration());
ASSERT (0 < dirt);
Time dirty(point + Time(dirt));
CHECK (point == testOrigin + myGrid->gridAlign(dirty));
CHECK (point == TEST_ORIGIN + myGrid->gridAlign(dirty));
}

View file

@ -44967,8 +44967,7 @@
fr&#252;her war das so eine typische &quot;n&#246;rgel&quot;-Warnung, die man unter den Teppich kehren konnte: 'ey, der Compiler bekommt es ja trotzdem richtig hin.
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<icon BUILTIN="clanbomber"/>
</node>
</node>
@ -44989,8 +44988,7 @@
</li>
</ul>
</body>
</html>
</richcontent>
</html></richcontent>
<icon BUILTIN="bell"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1439644368572" ID="ID_688656048" MODIFIED="1581813253348" TEXT="Doku: Referenz-System">
@ -45363,9 +45361,144 @@
<icon BUILTIN="idea"/>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1581740733368" ID="ID_1726260734" MODIFIED="1581740740712" TEXT="TICKET #939 : should better use 64bit base type for FSecs?">
<icon BUILTIN="flag-yellow"/>
<node CREATED="1581740750350" ID="ID_1497724243" MODIFIED="1581740764240" TEXT="macht sich als 32bit vs 64bit-Problematik bemerkbar"/>
<node COLOR="#338800" CREATED="1581740733368" ID="ID_1726260734" MODIFIED="1581903029633" TEXT="TICKET #939 : should better use 64bit base type for FSecs?">
<icon BUILTIN="button_ok"/>
<node CREATED="1581740750350" ID="ID_1497724243" MODIFIED="1581819702167" TEXT="macht sich als 32bit vs 64bit-Problematik bemerkbar">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
als &quot;workaround&quot; hatte ich boost::rational&lt;long&gt; genommen
</p>
</body>
</html>
</richcontent>
</node>
<node CREATED="1581819704923" ID="ID_1047068236" MODIFIED="1581819708983" TEXT="was ist die Gefahr?">
<node CREATED="1581819714050" ID="ID_314031332" MODIFIED="1581819747785" TEXT="wenn FSecs == rational&lt;int64_t&gt;"/>
<node CREATED="1581819818091" ID="ID_135964502" MODIFIED="1581819829735" TEXT="k&#xf6;nnte dann...">
<node CREATED="1581819830610" ID="ID_1892644470" MODIFIED="1581819869673" TEXT="ein gavl_time_t als FSecs reinrutschen?"/>
<node CREATED="1581819875124" ID="ID_1881211496" MODIFIED="1581819926345" TEXT="ein FrameCnt (==int64_t) als FSecs reinrutschen"/>
</node>
<node CREATED="1581830913676" ID="ID_1851233163" MODIFIED="1581831276538" TEXT="m&#xf6;glicherweise">
<icon BUILTIN="smiley-neutral"/>
<node CREATED="1581830921543" ID="ID_1634734053" MODIFIED="1581830932821" TEXT="FixedFramesQuantiser-ctor">
<node CREATED="1581830948928" ID="ID_1558541350" MODIFIED="1581831146865" TEXT="Framerate">
<node CREATED="1581831157480" ID="ID_425744081" MODIFIED="1581831161915" TEXT="via uint fps">
<node CREATED="1581833787784" ID="ID_208241204" MODIFIED="1581833800782" TEXT="hat Vorfahrt bei einfachen Zahlen">
<icon BUILTIN="idea"/>
</node>
</node>
<node CREATED="1581831174129" ID="ID_283797015" MODIFIED="1581831181172" TEXT="via rational&lt;uint&gt;"/>
</node>
<node CREATED="1581830970509" ID="ID_892094269" MODIFIED="1581831262411" TEXT="via Duration">
<icon BUILTIN="button_cancel"/>
<node CREATED="1581831241536" ID="ID_1150857436" MODIFIED="1581831259798" TEXT="keine Gefahr"/>
<node CREATED="1581831244519" ID="ID_270912881" MODIFIED="1581833828374" TEXT="ctor via FSecs ist explicit"/>
</node>
</node>
<node CREATED="1581831295432" ID="ID_897252634" MODIFIED="1581831298540" TEXT="Grid::timeOf">
<node CREATED="1581831305831" ID="ID_775298577" MODIFIED="1581831317561" TEXT="FrameCnt gridPoint"/>
<node CREATED="1581831336819" ID="ID_742904480" MODIFIED="1581831547730" TEXT="FSecs [, offset]">
<icon BUILTIN="button_cancel"/>
<node CREATED="1581831549990" ID="ID_1767803348" MODIFIED="1581831553618" TEXT="keine Gefahr"/>
<node CREATED="1581831554078" ID="ID_292991581" MODIFIED="1581831588989" TEXT="da stets der direkte Match auf FrameCnt bevorzugt wird"/>
</node>
</node>
<node CREATED="1581833987508" ID="ID_1945258109" MODIFIED="1581834001127" TEXT="die sonstiten TimeValues sind bereits &quot;wasserdicht&quot;">
<node CREATED="1581834003850" ID="ID_1726640094" MODIFIED="1581834011693" TEXT="alle gef&#xe4;hrlichen ctor sind explicit"/>
</node>
</node>
</node>
<node CREATED="1581834016097" ID="ID_943401392" MODIFIED="1581834026595" TEXT="L&#xf6;sungs-Alternativen">
<node COLOR="#338800" CREATED="1581834028399" ID="ID_1290001374" MODIFIED="1581905054869" TEXT="FSecs := rational&lt;int64_t&gt;">
<icon BUILTIN="back"/>
<node CREATED="1581834219506" ID="ID_1492786906" MODIFIED="1581834235727" TEXT="erlaubt Einstieg mit maximal gro&#xdf;en Sekundenbetr&#xe4;gen"/>
<node CREATED="1581904944430" ID="ID_95309143" MODIFIED="1581905174089">
<richcontent TYPE="NODE"><html>
<head>
</head>
<body>
<p>
und wir brauchen die <b>definitiv</b>&#160;zum sinnvollen Rechenen mit Zeiten auf micro-Scala
</p>
</body>
</html>
</richcontent>
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
Der kritsche Fall ist n&#228;mlich, wenn wir mit FSecs anfangen, und dann irgendwo in der Rechnung mal Time::SCALE multiplizieren, um auf die &#181;-Skala zu wechseln. Am Ende der Rechnung w&#252;rde dann typischerweise ein rational_cast stehen. Damit das funktioniert, mu&#223; vor allem der Z&#228;hler des Bruches die volle Zeitskala unterst&#252;tzen. Daher sind <b>64bit zwingend</b>
</p>
</body>
</html>
</richcontent>
<linktarget COLOR="#265a98" DESTINATION="ID_95309143" ENDARROW="Default" ENDINCLINATION="389;0;" ID="Arrow_ID_1170592297" SOURCE="ID_38565900" STARTARROW="None" STARTINCLINATION="670;0;"/>
<icon BUILTIN="idea"/>
</node>
</node>
<node CREATED="1581834047228" ID="ID_622470274" MODIFIED="1581905031525" TEXT="FSecs := rational&lt;int&gt;">
<icon BUILTIN="button_cancel"/>
<node CREATED="1581834077245" ID="ID_897585288" MODIFIED="1581834173627" TEXT="h&#xe4;tte den Charme, da&#xdf; weder ein FrameCnt, noch eine gavl-Time direkt reinrutschen kann">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
weil 64bit -&gt; 32bit eine <i>narrowing conversion</i>&#160;ist, die zumindest eine Warnung erzeugt
</p>
</body>
</html>
</richcontent>
</node>
<node COLOR="#338800" CREATED="1581834174595" ID="ID_1571757083" MODIFIED="1581902077277" TEXT="TODO: ist dieses Argument stichhaltig?">
<icon BUILTIN="button_ok"/>
<node CREATED="1581902078156" ID="ID_1759780689" MODIFIED="1581902089742" TEXT="ja... durch Experiment gepr&#xfc;ft"/>
<node CREATED="1581902090474" ID="ID_355415579" MODIFIED="1581902102205" TEXT="aber ohnehin ist lib::Time schon sehr gut abgedichtet"/>
</node>
<node CREATED="1581905033868" ID="ID_1903781960" MODIFIED="1581905047679" TEXT="aber die Genauigkeit reicht nicht">
<icon BUILTIN="stop-sign"/>
</node>
</node>
<node CREATED="1581834243675" ID="ID_1426215906" MODIFIED="1581902070280" TEXT="FSecs-ctor explicit machen">
<icon BUILTIN="button_cancel"/>
<node CREATED="1581834266215" ID="ID_221160071" MODIFIED="1581834285216" TEXT="man w&#xfc;rde damit ein &quot;Schlupfloch&quot; schlie&#xdf;en"/>
<node CREATED="1581834285684" ID="ID_1178771813" MODIFIED="1581834312204" TEXT="im Moment kann man schreiben Time(12L) (und bekommt 12 Sekunden)"/>
<node CREATED="1581834377896" ID="ID_897382203" MODIFIED="1581834390340" TEXT="geplant: Integration mit std::chrono">
<icon BUILTIN="idea"/>
<node CREATED="1581834393889" ID="ID_220525990" MODIFIED="1581834410119" TEXT="das w&#xfc;rde wieder eine bequeme Notation bieten"/>
<node CREATED="1581834411251" ID="ID_509607126" MODIFIED="1581834424225" TEXT="wobei, &quot;FSecs(12)&quot; ist jetzt nicht sooo schlecht"/>
<node CREATED="1581834424753" ID="ID_1621101278" MODIFIED="1581834434707" TEXT="und au&#xdf;erdem mache ich es in der Praxis ohnehin so"/>
</node>
<node BACKGROUND_COLOR="#ccb59b" COLOR="#6e2a38" CREATED="1581834553800" ID="ID_1858355977" MODIFIED="1581834582609" TEXT="erscheint mir als die sauberste Variante">
<font ITALIC="true" NAME="SansSerif" SIZE="14"/>
<icon BUILTIN="yes"/>
</node>
<node CREATED="1581902038450" ID="ID_676891454" MODIFIED="1581902066045" TEXT="nicht einfach realisierbar">
<icon BUILTIN="stop-sign"/>
</node>
</node>
</node>
<node CREATED="1581834456924" ID="ID_515357939" MODIFIED="1581834461008" TEXT="Typ-Fragen">
<node CREATED="1581834461832" ID="ID_1605358591" MODIFIED="1581834471551" TEXT="warum ist der FrameCnt ein signed integer?">
<node CREATED="1581883352929" ID="ID_1846298026" MODIFIED="1581883367592" TEXT="FrameCnt kann ein spezieller Timecode sein"/>
<node CREATED="1581883325779" ID="ID_573587391" MODIFIED="1581883342672" TEXT="weil es sich um eine Koordinate handelt"/>
<node CREATED="1581883343219" ID="ID_613888469" MODIFIED="1581883351652" TEXT="FrameCnt ist bisweilen negativ"/>
<node COLOR="#338800" CREATED="1581883360863" ID="ID_1550240767" MODIFIED="1581883366505" TEXT="also OK">
<icon BUILTIN="button_ok"/>
</node>
</node>
<node CREATED="1581834473819" ID="ID_38565900" MODIFIED="1581905067507" TEXT="brauchen / wollen wir 64bit f&#xfc;r die FSecs?">
<arrowlink COLOR="#265a98" DESTINATION="ID_95309143" ENDARROW="Default" ENDINCLINATION="389;0;" ID="Arrow_ID_1170592297" STARTARROW="None" STARTINCLINATION="670;0;"/>
</node>
</node>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1581813859601" ID="ID_728753164" MODIFIED="1581813869304" TEXT="auf C++17 heben">
@ -45406,8 +45539,7 @@
<font color="#d40222">WICHTIG</font>: keine vorgreifende Infos publizieren!!!!!
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1446482445325" ID="ID_1134936512" MODIFIED="1581813646885" TEXT="Build-Tutorial">
<richcontent TYPE="NOTE"><html>