diff --git a/src/lib/time/digxel.hpp b/src/lib/time/digxel.hpp index 00a569311..95814bd37 100644 --- a/src/lib/time/digxel.hpp +++ b/src/lib/time/digxel.hpp @@ -302,19 +302,29 @@ namespace time { /** special Digxel to show a sign. * @note values limited to +1 and -1 */ - struct Signum - : Digxel + class Signum + : public Digxel { - Signum() { setValueRaw(1); } + static int + just_the_sign (int val) + { + return val<0? -1:+1; + } + + public: + Signum() + { + setValueRaw(1); + mutator = just_the_sign; + } void operator= (int n) { - int newSign = 0 > mutator(n)? -1:+1; - this->setValueRaw (newSign); + this->setValueRaw (mutator(n)); } - friend int operator*= (Signum s, int c) { s = c*s; return s; } + friend int operator*= (Signum& s, int c) { s = c*s; return s; } }; diff --git a/src/lib/time/formats.hpp b/src/lib/time/formats.hpp index 80d7f9722..05adaaecb 100644 --- a/src/lib/time/formats.hpp +++ b/src/lib/time/formats.hpp @@ -84,6 +84,7 @@ namespace time { static void rebuild (SmpteTC&, QuantR, TimeValue const&); static TimeValue evaluate (SmpteTC const&, QuantR); static uint getFramerate (QuantR, TimeValue const&); + static void rangeLimitStrategy (SmpteTC&, int& rawHours); }; diff --git a/src/lib/time/timecode.cpp b/src/lib/time/timecode.cpp index a57106ecb..9a0ff6f27 100644 --- a/src/lib/time/timecode.cpp +++ b/src/lib/time/timecode.cpp @@ -34,6 +34,7 @@ using std::string; using util::unConst; using util::isSameObject; +using util::floorwrap; namespace lib { @@ -68,6 +69,7 @@ namespace time { void Smpte::rebuild (SmpteTC& tc, QuantR quantiser, TimeValue const& rawTime) { + tc.clear(); tc.frames = quantiser.gridPoint (rawTime); // will automatically wrap over to the secs, minutes and hour fields } @@ -82,7 +84,7 @@ namespace time { + tc.secs * frameRate + tc.mins * frameRate * 60 + tc.hours * frameRate * 60 * 60; - return quantiser.timeOf (gridPoint); + return quantiser.timeOf (tc.sgn * gridPoint); } /** yield the Framerate in effect at that point. @@ -108,9 +110,73 @@ namespace time { ENSURE (0 < effectiveFrames); return uint(effectiveFrames); } - + + + /** handle the limits of SMPTE timecode range. + * This is an extension and configuration point to control how + * to handle values beyond the official SMPTE timecode range of + * 0:0:0:0 to 23:59:59:##. When this strategy function is invoked, + * the frames, seconds and minutes fields have already been processed + * under the assumption the overall value stays in range. After returning + * from this strategy function, the rawHours value will be returned to be + * stored into the hours field without any further adjustments. + * @note currently the range is extended "naturally" (i.e. mathematically). + * The representation is flipped around the zero point and the value + * of the hours is just allowed to increase beyond 23 + * @todo If necessary, this extension point should be converted into a + * configurable strategy. Possible variations + * - clip values beyond the boundaries + * - wrap around from 23:59:59:## to 0:0:0:0 + * - just make the hour negative, but continue with the same + * orientation (0:0:0:0 - 1sec = -1:59:59:0) + */ + void + Smpte::rangeLimitStrategy (SmpteTC& tc, int& rawHours) + { + if ((rawHours^tc.sgn) >= 0) return; + + tc.sgn = rawHours; // transfer sign into the sign field + rawHours = abs(rawHours); + + REQUIRE (0 <= tc.frames && uint(tc.frames) < tc.getFps()); + REQUIRE (0 <= tc.secs && tc.secs < 60 ); + REQUIRE (0 <= tc.mins && tc.mins < 60 ); + + // sign flip was detected: + // switch orientation of all timecode fields + // i.e. -h + (m+s+f) becomes - (h+m+s+f) + uint fr (tc.getFps() - tc.frames); + uint secs (60 - tc.secs); + uint mins (60 - tc.mins); + + ASSERT (fr <= tc.getFps()); + ASSERT (0 < secs); + if (fr < tc.getFps()) + --secs; + else + fr = 0; + + ASSERT (secs <= 60); + ASSERT (0 < mins); + if (secs < 60) + --mins; + else + secs = 0; + + ASSERT (mins <= 60); + ASSERT (0 < rawHours); + if (mins < 60) + --rawHours; + else + mins = 0; + + tc.frames.setValueRaw (fr); + tc.secs.setValueRaw (secs); + tc.mins.setValueRaw (mins); + } } + namespace { // Timecode implementation details typedef util::IDiv Div; @@ -118,7 +184,7 @@ namespace time { int wrapFrames (SmpteTC* thisTC, int rawFrames) { - Div scaleRelation(rawFrames, thisTC->getFps()); + Div scaleRelation = floorwrap (rawFrames, thisTC->getFps()); thisTC->secs += scaleRelation.quot; return scaleRelation.rem; } @@ -126,7 +192,7 @@ namespace time { int wrapSeconds (SmpteTC* thisTC, int rawSecs) { - Div scaleRelation(rawSecs, 60); + Div scaleRelation = floorwrap (rawSecs, 60); thisTC->mins += scaleRelation.quot; return scaleRelation.rem; } @@ -134,7 +200,7 @@ namespace time { int wrapMinutes (SmpteTC* thisTC, int rawMins) { - Div scaleRelation(rawMins, 60); + Div scaleRelation = floorwrap (rawMins, 60); thisTC->hours += scaleRelation.quot; return scaleRelation.rem; } @@ -142,7 +208,7 @@ namespace time { int wrapHours (SmpteTC* thisTC, int rawHours) { - thisTC->sgn = rawHours; + format::Smpte::rangeLimitStrategy (*thisTC, rawHours); return rawHours; } @@ -229,10 +295,21 @@ namespace time { void - SmpteTC::rebuild() const + SmpteTC::clear() + { + frames.setValueRaw(0); + secs.setValueRaw (0); + mins.setValueRaw (0); + hours.setValueRaw (0); + sgn.setValueRaw (+1); + } + + + void + SmpteTC::rebuild() { TimeValue point = Format::evaluate (*this, *quantiser_); - Format::rebuild (unConst(*this), *quantiser_, point); + Format::rebuild (*this, *quantiser_, point); } diff --git a/src/lib/time/timecode.hpp b/src/lib/time/timecode.hpp index faaf00adb..8392aed9f 100644 --- a/src/lib/time/timecode.hpp +++ b/src/lib/time/timecode.hpp @@ -120,9 +120,11 @@ namespace time { SmpteTC (SmpteTC const&); SmpteTC& operator= (SmpteTC const&); - void rebuild() const; uint getFps() const; + void clear(); + void rebuild(); + HourDigit hours; SexaDigit mins; SexaDigit secs; diff --git a/tests/40components.tests b/tests/40components.tests index 3020d34c7..2b426a360 100644 --- a/tests/40components.tests +++ b/tests/40components.tests @@ -674,7 +674,8 @@ return: 0 END -PLANNED "Time formats and timecodes" TimeFormats_test < -
+
The handling of [[Timecode]] is closely related to [[time representation and quantisation|TimeQuant]]. In fact, these two topics blend into one another. Time will be quantised into a //grid,// but this grid only makes sense when linked to a externally relevant meaning and representation, which is the Timecode. But a timecode value also denotes a specific point in time -- performing operations on a timecode is equivalent to manipulating a quantised time value.
 
 !Problem of dependencies
@@ -6773,7 +6773,24 @@ And last but not least, it is possible to get a new ~TimeValue, reflecting the c
 * @@color:green;✔@@ determine the primitives which need to be on the //effective quantiser API.//
 * @@color:green;✔@@ find out how a format can address the individual components it's comprised of &rarr; direct access by concrete type
 * @@color:green;✔@@ decide how a concrete TC value can refer to his quantiser &rarr; always using smart-ptr
-* @@color:red;??@@ maybe coin a //value handle// -- to tie the three required parts together
+* @@color:green;✔@@ decide on how to handle wrap-around and negative values &rarr; by strategy
+
+!negative values and wrap-around
+Many timecode formats were defined in the era of analogue media -- typically the key point of a timecode format was the way it can be encoded into some nits and bits within the available channel bandwidth. Often this causes the use of fixed field length representations, imposing hard limits on the number range. Adhering strictly to such ancient specifications in the context of a general purpose computer program usually results in lots of additional complexity without a fundamental reason.
+
+When in doubt, for the design of Lumiera we tend to err for the basic mathematical definition. For time values this means to use an practically unlimited time axis with an arbitrary time origin. Under this assumptions, negative time values are just natural and will be handled without case distinctions. This way, at least the core is kept simple and straight forward. Which leaves us with some of the aforementioned additional complexities showing up when it comes to interfacing with an existing timecode format. Especially, the following points need to be clarified:
+* wrap-around: when a timecode component (e.g. the seconds) exceeds the defined range, this creates a propagation (e.g. to the minutes) and a remainder (wrapped seconds value).
+* range limitation: what happens when an internal time value exceeds the range of possible timecode values? (e.g. what is 23:59:59 + 70 frames?)
+* how to extend the definition to negative values, if applicable.
+
+Especially ''SMPTE Timecode'' is limited to the range from 0:0:0:0 to 23:59:59:##.
+But because for Lumiera the SMPTE format is just a presentation rule applied to a more orthogonal internal repesentation, we could think of extending the allowed values...
+# by just letting the hours increase arbitrarily
+# by letting the timecode wrap from 23:59:59:## to 0:0:0:0 and treat this junction as continuous (effectively a "modulo 1 day").
+# by making the hours-field signed, but otherwise contain the same wrapping scheme (0:0:0:0 - 1sec = -1:59:59:0)
+# by a representation symmetrical to the zero point (0:0:0:0 - 1sec = -0:0:1:0) -- but beware: //the underlying frame quantisation won't flip//
+Because this decision is considered arbitrary and any reasoning will be context dependant, the decision is to provide a hook for a //strategy// --
+Currently (1/11), the strategy is implemented according to (1) and (4) above, leaving the actual configuration of a strategy for later.