SMPTE Timecode (without drop frame) unit test pass
This commit is contained in:
parent
1a07cc9bb2
commit
9d75739089
9 changed files with 139 additions and 29 deletions
|
|
@ -302,19 +302,29 @@ namespace time {
|
|||
|
||||
/** special Digxel to show a sign.
|
||||
* @note values limited to +1 and -1 */
|
||||
struct Signum
|
||||
: Digxel<int,digxel::SignFormatter>
|
||||
class Signum
|
||||
: public Digxel<int,digxel::SignFormatter>
|
||||
{
|
||||
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; }
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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<int> Div;
|
||||
|
|
@ -118,7 +184,7 @@ namespace time {
|
|||
int
|
||||
wrapFrames (SmpteTC* thisTC, int rawFrames)
|
||||
{
|
||||
Div scaleRelation(rawFrames, thisTC->getFps());
|
||||
Div scaleRelation = floorwrap<int> (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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -674,7 +674,8 @@ return: 0
|
|||
END
|
||||
|
||||
|
||||
PLANNED "Time formats and timecodes" TimeFormats_test <<END
|
||||
TEST "Time formats and timecodes" TimeFormats_test <<END
|
||||
out-lit: SMPTE=" 5:42:23:13" time = 5:42:23.520
|
||||
return: 0
|
||||
END
|
||||
|
||||
|
|
|
|||
|
|
@ -85,9 +85,9 @@ namespace test{
|
|||
CHECK (1 == sig);
|
||||
sig = -sig;
|
||||
CHECK (-1 == sig);
|
||||
sig = -98;
|
||||
CHECK (-1 == sig);
|
||||
CHECK (sig.show() == string("-"));
|
||||
sig += 98;
|
||||
CHECK (+1 == sig);
|
||||
CHECK (sig.show() == string(" "));
|
||||
sig *= -1;
|
||||
CHECK (sig.show() == string("-"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ namespace test{
|
|||
* we'll take some timings.
|
||||
* @warning the results of such tests could be unreliable,
|
||||
* but in this case here I saw a significant difference,
|
||||
* with values of about 0.5sec / 0.7sec */
|
||||
* with values of about 0.1sec / 0.7sec */
|
||||
void
|
||||
verifyDisplayCaching ()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ namespace test{
|
|||
|
||||
showTimeCode(smpte);
|
||||
CHECK (" 5:42:23:13" == string(smpte));
|
||||
CHECK (raw - Time(35,0) == smpte.getTime());
|
||||
CHECK (raw - Time(35,0) == smpte.getTime()); // quantisation to next lower frame
|
||||
CHECK (13 == smpte.frames);
|
||||
CHECK (23 == smpte.secs);
|
||||
CHECK (42 == smpte.mins);
|
||||
|
|
@ -131,20 +131,22 @@ namespace test{
|
|||
CHECK (smpte.mins-- == 40);
|
||||
CHECK (--smpte.mins == 38);
|
||||
CHECK (" 5:38:00:01" == string(smpte));
|
||||
|
||||
// extension beyond origin to negative values
|
||||
Time tx = smpte.getTime();
|
||||
smpte.hours -= 6;
|
||||
CHECK ("- 0:21:59:24"== string(smpte));
|
||||
CHECK (tx - Time(6*60*60) == smpte.getTime());
|
||||
CHECK ("- 0:21:59:24"== string(smpte)); // representation is symmetrical to origin
|
||||
CHECK (tx - Time(6*60*60) == smpte.getTime()); // Continuous time axis
|
||||
CHECK (-1 == smpte.sgn);
|
||||
smpte.sgn += 123;
|
||||
smpte.sgn += 123; // just flip the sign
|
||||
CHECK (" 0:21:59:24"== string(smpte));
|
||||
|
||||
smpte.secs.setValueRaw(61);
|
||||
smpte.secs.setValueRaw(61); // set "wrong" value, bypassing normalisation
|
||||
CHECK (smpte.secs == 61);
|
||||
CHECK (smpte.getTime() == Time(1000*24/25, 01, 22));
|
||||
CHECK (smpte.secs == 61);
|
||||
CHECK (smpte.secs == 61); // calculated value is correct, but doesn't change state
|
||||
CHECK (" 0:21:61:24"== string(smpte));
|
||||
smpte.rebuild();
|
||||
smpte.rebuild(); // but rebuilding the value includes normalisation
|
||||
CHECK (smpte.secs == 1);
|
||||
CHECK (smpte.mins == 22);
|
||||
CHECK (" 0:22:01:24"== string(smpte));
|
||||
|
|
|
|||
|
|
@ -6746,7 +6746,7 @@ Question is: how fine grained and configurable needs this to be?
|
|||
|
||||
</pre>
|
||||
</div>
|
||||
<div title="TimecodeFormat" modifier="Ichthyostega" modified="201101190857" created="201101100308" tags="Player spec discuss draft" changecount="18">
|
||||
<div title="TimecodeFormat" modifier="Ichthyostega" modified="201101211920" created="201101100308" tags="Player spec discuss draft" changecount="24">
|
||||
<pre>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.
|
||||
</pre>
|
||||
</div>
|
||||
<div title="Timeline" modifier="Ichthyostega" modified="201011220126" created="200706250721" tags="def" changecount="21">
|
||||
|
|
|
|||
Loading…
Reference in a new issue