The term »Time« spans a variety of vastly different entities.
Within a NLE we get to deal with various flavours of time values.
- continuous time
without any additional assumptions, points in time can be specified with arbitrary precision. The time values are just numbers; the point of reference and the meaning is implicit. Within Lumiera, time is encoded as integral number of micro ticks, practically continuous
- time distance
a range of time, a distance on the time axis, measured with the same arbitrary precision as time points. Distances can be determined by subtracting two time points, consequently they are signed numbers.
a distance can be used to adjust (offset) a time or a duration: this means applying a relative change. The target of an offset operation is a time or duration, while it’s argument is a distance (synonymous to offset).
the length of a time range yields a time metric.
the duration can be defined as the absolute value of the offset between start and endpoint of the time range. A duration always abstracts from the time when this duration or distance happens, the relation to any time scale remains implicit
- time span
contrary to a mere duration, a time interval or time span is actually anchored at a specific point in time. It can be seen as a special kind of duration, which explicitly states the information when this time span takes place.
- changing time
Time values are immutable, like numbers.
Only a time variable can be changed — yet some of the special time entities can recieve mutation messages, allowing e.g. for adjustments to a time interval selection from the GUI
- internal time
While the basic continuous time values don’t imply any commitment regarding the time scale and origin to be used, actually, within the implementation of the application, the meaning of time values is uniform and free of contradictions. Thus effectively there is an implementation time scale — but its scope of validity is strictly limited to the implementation level of a single application instance. It is never exposed and never persisted. It might not be reproducible over multiple instantiations of the application. The implementation reserves the right to recalibrate this internal scale. Later, when Lumiera gains the capability to run within a network of render nodes, these instance connections will include a negotiation about the internal time scale, which remains completely opaque to the outer world. This explains, why lumiera::Time instances lack the ability to show their time value beyond debugging purposes. This is to avoid confusion and to stress their opaque nature.
- wall clock and system time
The core property of any external real world time is that it is running — we have to synchronise to an external time source. This implies the presence of a running synchronisation process, with the authority to adjust the time base;
contrast this to the internal time, which is static and unconnected --
- quantised time
The act of quantisation transforms a continuous property into a discrete structure. Prominent examples can be found in the domain of micro physics and with digital information processing. In a broader sense, any measurement or quantification also encompasses a quantisation. Regarding time and time measurement, quantisation means alignment to a predefined time grid. Quantisation necessarily is an irreversible process — possible additional information is discarded.
Note that quantisation introduces a time origin and a reference scale
- frame count
within the context of film and media editing, the specification of a frame number is an especially important instance of quantisation.
all the properties of quantisation apply indeed to this special case: it is a time measurement or specification, where the values are aligned to a grid, and there is a reference time point where the counting starts (origin) and a reference scale (frames per second). Handling of quantised time values in Lumiera is defined such as to ensure the presence of all those bits of information. Without such precautions, operating with bare frame numbers leads itself to all kinds of confusions, mismatches, quantisation errors and unnecessary limitations of functionality.
Quantisation also is the foundation of all kinds of formalised time specifications actually even a frame count is some kind of (informal) timecode — other timecodes employ a standardised format. //Every// presentation of time values and every persistent storage and exchange of such values is based on time codes. Yet quantisation and time code aren’t identical: a given quantised time value typically can be cast into multiple timecode formats.
Patterns for handling quantised time
When it comes to actually handling quantised time values, several patterns are conceivable for dealing with the quantisation operation and representing quantised data. As guideline for judging these patterns, the general properties of time quantisation, as detailed above, should be taken into account. Quantising a time value means both discarding information, while at the same time adding explicit information pertaining the assumptions of the context.
this is rather an frequently encountered anti pattern. When reading such
code, the most striking observation is the sense of general unawareness of the
problem, which is then “discovered” on a per case base, which leads to
numerous repetitions of the same basic undertakings, but done with individual
treatment of each instance (not so much copy-n-paste).
Typical code smells:
the rounding, modulo and subtract-base operations pertinent with scale handling are seemingly inserted as bugfix
local code path forks to circumvent or compensate for otherwise hard wired calculations based on specific ways to invoke a function
playing strikingly clever tricks or employing heuristics to "figure out" the missing scale information from accessible context after the fact
advertising support for some of the conceivable cases as special feature, or adding it as plugin or extension module with limited scope
linking parts of the necessary additional information to completely unrelated other structures, thus causing code tangling and scattering
result or behaviour of calculations depends on the way things are set up in a seemingly contingent way, forcing users to stick to very specific procedures and ordered steps.
an analysis of the cases to be expected establishes common patterns and some base cases, which are then represented by distinct types with well established conversions. This can be combined with generic programming for the common parts. Close to the data input, a factory establishes these statically typed values.
quantised values are explicitly created out of continuous values by a quantiser entity. These quantised data values contain a copy of the original data, adjusted to be exactly aligned with respect to the underlying time grid. In addition, they carry a tag or ID to denote the respective scale, grid or timecode system. This tag can be used later on to assess compatibility or to recast values into another timecode system.
with this approach, the information loss is delayed as long as possible. Quantised time values are rather treated as promise for quantisation, while the actual time data remains unaltered. Additionally, they carry a tag, or even a direct link to the responsible quantiser instance. Effectively, these are specialised time values, instances of a sub-concept, able to stand-in for general time values, but exposing additional accessors to get a quantised value.
For Lumiera, the static typing approach is of limited value — it excels when values belonging to different scales are actually treated differently. There are such cases, but rather on the data handling level, e.g. sound samples are always handled block wise. But regarding time values, the unifying aspect is more important, which leads to prefering a dynamic (run time typed) approach, while erasing the special differences most of the time. Yet the dynamic and open nature of the Lumiera high-level model favours the delayed quantisation pattern; the same values may require different quantisation depending on the larger model context an object is encountered in. This solution might be too general and heavy weight at times though. Thus, for important special cases, the accessors should return tagged values, preferably even with differing static type. Time codes can be integrated this way, but most notably the frame numbers used for addressing throughout the backend, can be implemented as such specifically typed tagged values; the tag here denotes the quantiser and thus the underlying grid — it should be implemented as hash-ID for smooth integration with code written in plain C.
At the level of individual timecode formats, we’re lacking a common denominator; thus it is preferrable to work with different concrete timecode classes through generic programming. This way, each timecode format can expose operations specific only to the given format. Especially, different timecode formats expose different component fields, modelled by the generic Digxel concept. There is a common baseclass TCode though, which can be used as marker or for type erasure.
→ more on usage situations