join with latest work on timecodes and frame quantisation

Merge branch 'timequant' into gui
This commit is contained in:
Fischlurch 2011-05-15 04:28:20 +02:00
commit 99bf3c6d81
26 changed files with 2415 additions and 103 deletions

View file

@ -60,9 +60,25 @@
** specific function signature and then try to assign the named member. This allows even
** to determine if a member function of a type in question has the desired signature.
**
** All these detection building blocks are written such as to provide a bool member \v ::value,
** All these detection building blocks are written such as to provide a bool member \c ::value,
** which is in accordance to the conventions of boost metaprogramming. I.e. you can immediately
** use them within boost::enable_if
** use them within \c boost::enable_if
**
** \par some pitfalls to consider
**
** @warning The generated metafunctions all yield the \c false value by default.
** Effectively this means that an error in the test expression might go unnoticed;
** you'd be better off explicitly checking the detection result by an unit test.
**
** There are several typical problems to care about
** - a member can be both a variable or a function of that name
** - function signatures need to match precisely, including const modifiers
** - the generated metafunction (template) uses a type parameter 'TY', which could
** shadow or conflict with an type parameter in the enclosing scope
** - the member and function checks rely on member pointers, which generally rely on
** the explicit static type. These checks don't see any inherited members / functions.
** - obviously, all those checks are never able to detect anything depending on runtime
** types or RTTI
**
** @see util-foreach.hpp usage example
** @see duck-detector-test.cpp

View file

@ -0,0 +1,555 @@
/*
POLYMORPHIC-VALUE.hpp - building opaque polymorphic value objects
Copyright (C) Lumiera.org
2011, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/** @file polymorphic-value.hpp
** A mechanism to allow for opaque polymorphic value objects.
** This template helps to overcome a problem frequently encountered in
** C++ programming, based on the fundamental design of C++, which favours
** explicit low-level control, copying of values and strict ctor-dtor pairs.
** Many object oriented design patterns build on polymorphism, where the
** actual type of an object isn't disclosed and collaborations rely on
** common interfaces. This doesn't mix well with the emphasis the C/C++
** language puts on efficient handling of small data elements as values
** and explicit control of the used storage; indeed several of the modern
** object oriented and functional programming techniques more or less
** assume the presence of a garbage collector or similar mechanism,
** so 'objects' need just to be mentioned by reference.
**
** In C++ to employ many of the well known techniques, you're more or less
** bound to explicitly put the objects somewhere in heap allocated memory
** and then pass an interface pointer or reference into the actual algorithm.
** Often, this hinders a design based on constant values and small descriptor
** objects used inline, thus forcing into unnecessarily complex and heavyweight
** alternatives. While it's certainly pointless to fight the fundamental nature
** of the programming language, we may try to pull some (template based) trickery
** to make polymorphic objects fit better with the handling of small copyable
** value objects. Especially, C++ gives a special meaning to passing parameters
** as \c const& -- typically constructing an anonymous temporary object conveniently
** just for passing an abstraction barrier (while the optimiser can be expected to
** remove this barrier and the accompanying nominal copy operations altogether in
** the generated code). Consequently the ability to return a polymorphic object
** from a factory or configuration function <i>by value</i> would open a lot of
** straight forward design possibilities and concise formulations.
**
** \par how to build a copyable value without knowing it's layout in detail
**
** So the goal is to build a copyable and assignable type with value semantics,
** without disclosing the actual implementation and object layout at the usage site.
** This seemingly contradictory goal can be achieved, provided that
** - the space occupied by the actual implementation object is limited,
** so it can be placed as binary data into an otherwise opaque holder buffer
** - and the actual implementation object assists with copying and cloning
** itself, observing the actual implementation data layout
**
** The PolymorphicValue template implements this idea, by exposing an copyable
** container with value semantics to the client code. On instantiation, a common
** base interface for the actual value objects needs to be provided; the resulting
** instance will be automatically convertible to this interface. Obviously this
** common interface must be an ABC or at least contain some virtual functions.
** Moreover, the PolymorphicValue container provides static builder functions,
** allowing to place a concrete instance of a subclass into the content buffer.
** After construction, the actual type of this instance will be forgotten
** (``type erasure''), but because the embedded vtable, on access the
** proper implementation functions will be invoked.
**
** Expanding on that pattern, the copying and cloning operations of the whole
** container can be implemented by forwarding to appropriate virtual functions
** on the embedded payload (implementation) object -- the concrete implementation
** of these virtual functions can be assumed to know the real type and thus be
** able to invoke the correct copy ctor or assignment operator. For this to
** work, the interface needs to expose those copy and clone operations somehow
** as virtual functions. There are two alternatives to get at this point:
** - in the general case, the common base interface doesn't expose such operations.
** Thus we need to <i>mix in</i> an additional \em management interface; this
** can be done by \em subclassing the desired implementation type, because
** this concrete type is irrelevant after finishing the placement constructor.
** In order to re-access this management interface, so to be able to invoke
** the copy or clone operations, we need to do an elaborate re-cast operation,
** first going down to the leaf type and then back up into the mixed in
** management interface. Basically this operation is performed by using
** an \c dynamic_cast<CopyAPI&>(bufferContents)
** - but when the used client types provide some collaboration and implement
** this management interface either directly on the API or as an immediate
** sub-interface, then this copy/management interface is located within the
** direct inheritance chain and can be reached by a simple \c static_cast.
** Indeed, as we're just using a different meaning of the VTable, only a
** single indirection (virtual function call) is required at runtime in
** this case to invoke the copy ctor or assignment operator.
** Thus, in this latter (optimal) case, the fact that PolymorphicValue allows
** to conceal the actual implementation type comes with zero runtime overhead,
** compared with direct usage of a family of polymorphic types (with VTable).
**
** \par using polymorphic value objects
**
** To start with, we need a situation where polymorphic treatment and type erasure
** might be applicable. That is, we use a public API, and only that, in any client
** code, while the concrete implementation is completely self contained. Thus, in
** the intended use, the concrete implementation objects can be assembled once,
** typically in a factory, and after that, no further knowledge of the actual
** implementation type is required. All further use can be coded against
** the exposed public API.
**
** Given such a situation, it might be desirable to conceal the whereabouts of
** the implementation completely from the clients employing the generated objects.
** For example, the actual implementation might rely on a complicated subsystem
** with many compilation dependencies, and we don't want to expose all those
** details on the public API.
**
** Now, to employ PolymorphicValue in such a situation, on the usage side (header):
** - expose the public API, but not the implementation type of the objects
** - define an instantiation of PolymorphicValue with this API
** - be sure to define a hard wired size limit not to be exceeded by the
** actual implementation objects (PolymorphicValue's ctor has an assertion
** to verify this constraint)
** - provide some kind of factory for the clients to get the actual polymorphic
** value instances. Clients may then freely move and copy those objects, but
** do not need to know anything about the actual implementation object layout
** (it could be figured out using RTTI though)
**
** On the implementation side (separate compilation unit)
** - include the definition of the PolymorphicValue instantiation (of course)
** - define the implementation types to inherit from the public API
** - implement the mentioned factory function, based on the static build
** PolymorphicValue#build functions, using the actual implementation type
** as parameter.
**
** @see polymorphic-value-test.cpp
** @see opaque-holder.hpp other similar opaque inline buffer templates
** @see lib::time::Mutation usage example
*/
#ifndef LIB_POLYMORPHIC_VALUE_H
#define LIB_POLYMORPHIC_VALUE_H
#include "lib/error.hpp"
#include "lib/meta/duck-detector.hpp"
#include <boost/utility/enable_if.hpp>
namespace lib {
namespace polyvalue { // implementation details...
namespace error = lumiera::error;
using boost::enable_if;
using lumiera::Yes_t;
using lumiera::No_t;
struct EmptyBase{ };
/**
* Interface for active support of copy operations
* by the embedded client objects. When inserted into the
* inheritance chain \em above the concrete implementation objects,
* PolymorphicValue is able to perform copy operations trivially and
* without any \c dynamic_cast and other run time overhead besides a
* simple indirection through the VTable. To enable this support, the
* implementation objects should inherit from \c CopySupport<Interface>
* (where \c Interface would be the public API for all these embedded
* implementation objects).
* Alternatively, it's also possible to place this CopySupport API as parent
* to the public API (it might even be completely absent, but then you'd need
* to provide an explicit specialisation of the Traits template to tell
* PolymorphicValue how to access the copy support functions.)
*/
template<class IFA ///< the common public interface of all embedded objects
,class BA = IFA ///< direct baseclass to use for this copy support API
>
class CopySupport
: public BA
{
public:
virtual ~CopySupport() { };
virtual void cloneInto (void* targetBuffer) const =0;
virtual void copyInto (IFA& targetBase) const =0;
};
/**
* A variation for limited copy support.
* Sometimes, only cloning (copy construction) of value objects is allowed,
* but not assignment of new values to existing objects. In this case, the
* concrete values can inherit from this variant of the support API.
*/
template<class BA> ///< direct baseclass to use for this clone support API
class CloneValueSupport
: public BA
{
public:
virtual ~CloneValueSupport() { };
virtual void cloneInto (void* targetBuffer) const =0;
};
/**
* helper to detect presence of a function
* to support clone operations
*/
template<typename T>
class exposes_CloneFunction
{
META_DETECT_FUNCTION(void, cloneInto, (void*) const);
public:
enum{ value = HasFunSig_cloneInto<T>::value
};
};
/**
* helper to detect if the API supports only
* copy construction, but no assignment
*/
template<typename T>
class allow_Clone_but_no_Copy
{
META_DETECT_MEMBER(copyInto);
public:
enum{ value = exposes_CloneFunction<T>::value
&& ! HasMember_copyInto<T>::value
};
};
/**
* Policy class for invoking the assignment operator.
* By default the embedded payload objects are assumed
* to be freely assignable
*/
template<class API, class YES =void>
struct AssignmentPolicy
{
template<class IMP>
static void
assignEmbedded(IMP& dest,IMP const& src)
{
dest = src;
}
};
/**
* special case when the embedded payload objects permit
* copy construction, but no assignment to existing instances.
* Instead of invoking the assignment operator (which typically
* isn't defined at all in such cases), a misguided assignment
* to the container will raise an exception
*/
template<class API>
struct AssignmentPolicy<API, typename enable_if< allow_Clone_but_no_Copy<API> >::type>
{
template<class IMP>
static void
assignEmbedded(IMP&,IMP const&)
{
throw error::Logic("attempt to overwrite unmodifiable value");
}
};
/**
* traits template to deal with
* different ways to support copy operations.
* Default is no support by the API and implementation types.
* In this case, the CopySupport interface is mixed in at the
* level of the concrete implementation class and later on
* accessed through an \c dynamic_cast
*/
template <class TY, class YES = void>
struct Trait
{
typedef CopySupport<TY,EmptyBase> CopyAPI;
enum{ ADMIN_OVERHEAD = 2 * sizeof(void*) };
static CopyAPI&
accessCopyHandlingInterface (TY& bufferContents)
{
REQUIRE (INSTANCEOF (CopyAPI, &bufferContents));
return dynamic_cast<CopyAPI&> (bufferContents);
}
typedef CopyAPI AdapterAttachment;
typedef AssignmentPolicy<CopyAPI> Assignment;
};
/**
* Special case when the embedded types support copying
* on the API level, e.g. there is a sub-API exposing a \c cloneInto
* function. In this case, the actual implementation classes can be
* instantiated as-is and the copy operations can be accessed by a
* simple \c static_cast without runtime overhead.
*/
template <class TY>
struct Trait<TY, typename enable_if< exposes_CloneFunction<TY> >::type>
{
typedef TY CopyAPI;
enum{ ADMIN_OVERHEAD = 1 * sizeof(void*) };
template<class IFA>
static CopyAPI&
accessCopyHandlingInterface (IFA& bufferContents)
{
REQUIRE (INSTANCEOF (CopyAPI, &bufferContents));
return static_cast<CopyAPI&> (bufferContents);
}
typedef EmptyBase AdapterAttachment;
typedef AssignmentPolicy<CopyAPI> Assignment;
};
}//(End)implementation details
/**
* Template to build polymorphic value objects.
* Inline buffer with value semantics, yet holding and owning an object
* while concealing the concrete type, exposing only the public interface.
* Access to the contained object is by implicit conversion to this public
* interface. The actual implementation object might be placed into the
* buffer through a builder function; later, this buffer may be copied
* and passed on without knowing the actual contained type.
*
* For using PolymorphicValue, several \b assumptions need to be fulfilled
* - any instance placed into OpaqueHolder is below the specified maximum size
* - the caller cares for thread safety. No concurrent get calls while in mutation!
*
* @warning when a create or copy-into operation fails with exception, the whole
* PolymorphicValue object is in undefined state and must not be used further.
*/
template
< class IFA ///< the nominal Base/Interface class for a family of types
, size_t storage ///< maximum storage required for the target data to be held inline
, class CPY = IFA ///< special sub-interface to support copy operations
>
class PolymorphicValue
{
typedef polyvalue::Trait<CPY> _Traits;
typedef typename _Traits::CopyAPI _CopyHandlingAdapter;
typedef typename _Traits::Assignment _AssignmentPolicy;
enum{
siz = storage + _Traits::ADMIN_OVERHEAD
};
/* === embedded object in buffer === */
/** Storage for embedded objects */
mutable char buf_[siz];
IFA&
accessEmbedded() const
{
return reinterpret_cast<IFA&> (buf_);
}
void
destroyEmbedded()
{
accessEmbedded().~IFA();
}
template<class IMP>
PolymorphicValue (IMP*)
{
REQUIRE (siz >= sizeof(IMP));
new(&buf_) IMP();
}
template<class IMP, typename A1>
PolymorphicValue (IMP*, A1& a1)
{
REQUIRE (siz >= sizeof(IMP));
new(&buf_) IMP (a1);
}
template<class IMP, typename A1, typename A2>
PolymorphicValue (IMP*, A1& a1, A2& a2)
{
REQUIRE (siz >= sizeof(IMP));
new(&buf_) IMP (a1,a2);
}
template<class IMP, typename A1, typename A2, typename A3>
PolymorphicValue (IMP*, A1& a1, A2& a2, A3& a3)
{
REQUIRE (siz >= sizeof(IMP));
new(&buf_) IMP (a1,a2,a3);
}
/**
* Implementation Helper: supporting copy operations.
* Actually instances of this Adapter template are placed
* into the internal buffer, such that they both inherit
* from the desired implementation type and the copy
* support interface. The implementation of the
* concrete copy operations is provided here
* forwarding to the copy operations
* of the implementation object.
*/
template<class IMP>
class Adapter
: public IMP
, public _Traits::AdapterAttachment // mix-in, might be empty
{
virtual void
cloneInto (void* targetBuffer) const
{
new(targetBuffer) Adapter(*this); // forward to copy ctor
}
virtual void
copyInto (IFA& targetBase) const
{
REQUIRE (INSTANCEOF (Adapter, &targetBase));
Adapter& target = static_cast<Adapter&> (targetBase);
_AssignmentPolicy::assignEmbedded(target,*this);
} // forward to assignment operator
public: /* == forwarding ctor to implementation type == */
Adapter() : IMP() { }
template<typename A1>
Adapter (A1& a1) : IMP(a1) { }
template<typename A1, typename A2>
Adapter (A1& a1, A2& a2) : IMP(a1,a2) { }
template<typename A1, typename A2, typename A3>
Adapter (A1& a1, A2& a2, A3& a3) : IMP(a1,a2,a3) { }
/* using default copy and assignment */
};
_CopyHandlingAdapter&
accessHandlingInterface () const
{
IFA& bufferContents = accessEmbedded();
return _Traits::accessCopyHandlingInterface (bufferContents);
}
public: /* === PolymorphicValue public API === */
typedef IFA Interface;
operator Interface& ()
{
return accessEmbedded();
}
operator Interface const& () const
{
return accessEmbedded();
}
Interface*
operator-> () const
{
return &( accessEmbedded() );
}
~PolymorphicValue()
{
destroyEmbedded();
}
PolymorphicValue (PolymorphicValue const& o)
{
o.accessHandlingInterface().cloneInto (&buf_);
}
PolymorphicValue&
operator= (PolymorphicValue const& o)
{
o.accessHandlingInterface().copyInto (this->accessEmbedded());
return *this;
}
/* === static factory functions === */
template<class IMP>
static PolymorphicValue
build ()
{
Adapter<IMP>* type_to_build_in_buffer;
return PolymorphicValue (type_to_build_in_buffer);
}
template<class IMP, typename A1>
static PolymorphicValue
build (A1& a1)
{
Adapter<IMP>* type_to_build_in_buffer;
return PolymorphicValue (type_to_build_in_buffer, a1);
}
template<class IMP, typename A1, typename A2>
static PolymorphicValue
build (A1& a1, A2& a2)
{
Adapter<IMP>* type_to_build_in_buffer;
return PolymorphicValue (type_to_build_in_buffer, a1,a2);
}
template<class IMP, typename A1, typename A2, typename A3>
static PolymorphicValue
build (A1& a1, A2& a2, A3& a3)
{
Adapter<IMP>* type_to_build_in_buffer;
return PolymorphicValue (type_to_build_in_buffer, a1,a2,a3);
}
/* === support Equality by forwarding to embedded === */
friend bool
operator== (PolymorphicValue const& v1, PolymorphicValue const& v2)
{
return v1.accessEmbedded() == v2.accessEmbedded();
}
friend bool
operator!= (PolymorphicValue const& v1, PolymorphicValue const& v2)
{
return ! (v1 == v2);
}
};
} // namespace lib
#endif

View file

@ -23,6 +23,7 @@
#include "lib/query.hpp"
#include "lib/util.hpp"
#include "lib/symbol.hpp"
#include "include/logging.h"
#include <boost/algorithm/string.hpp>
@ -74,7 +75,7 @@ namespace lumiera {
map<Symbol, regex> regexTable;
Symbol matchArgument = "\\(\\s*([\\w_\\.\\-]+)\\s*\\),?\\s*";
Literal matchArgument = "\\(\\s*([\\w_\\.\\-]+)\\s*\\),?\\s*";
regex findPredicate (string("(\\w+)")+matchArgument);
inline regex&

View file

@ -81,6 +81,14 @@ lumiera_rational_to_time (FSecs const& fractionalSeconds)
return rational_cast<gavl_time_t> (GAVL_TIME_SCALE * fractionalSeconds);
}
gavl_time_t
lumiera_framecount_to_time (uint64_t frameCount, FrameRate const& fps)
{
// convert to 64bit
boost::rational<uint64_t> framerate (fps.numerator(), fps.denominator());
return rational_cast<gavl_time_t> (GAVL_TIME_SCALE * frameCount / framerate);
}
gavl_time_t
lumiera_frame_duration (FrameRate const& fps)
@ -122,7 +130,7 @@ lumiera_quantise_time (gavl_time_t time, gavl_time_t origin, gavl_time_t grid)
}
gavl_time_t
lumiera_time_of_gridpoint (long nr, gavl_time_t origin, gavl_time_t grid)
lumiera_time_of_gridpoint (int64_t nr, gavl_time_t origin, gavl_time_t grid)
{
gavl_time_t offset = nr * grid;
return origin + offset;
@ -154,14 +162,14 @@ lumiera_build_time_fps (float fps, uint frames, uint secs, uint mins, uint hours
gavl_time_t
lumiera_build_time_ntsc_drop (uint frames, uint secs, uint mins, uint hours)
{
int total_mins = 60 * hours + mins;
int total_frames = 108000 * hours
+ 1800 * mins
+ 30 * secs
+ frames
- 2 * (total_mins - total_mins / 10);
uint64_t total_mins = 60 * hours + mins;
uint64_t total_frames = 108000 * hours
+ 1800 * mins
+ 30 * secs
+ frames
- 2 * (total_mins - total_mins / 10);
return (total_frames / 29.97f) * 1000 * GAVL_TIME_SCALE_MS;
return lumiera_framecount_to_time (total_frames, FrameRate::NTSC);
}
int

View file

@ -69,6 +69,16 @@ gavl_time_t
lumiera_rational_to_time (lib::time::FSecs const& fractionalSeconds);
/**
* Converts a frame count into Lumiera's internal time scale.
* based on a framerate given as rational number (e.g. NTSC)
* @note handles only positive frame counts and assumes the
* origin to be at zero.
*/
gavl_time_t
lumiera_framecount_to_time (uint64_t frameCount, lib::time::FrameRate const& fps);
/**
* Calculates the duration of one frame in Lumiera time units.
* @param framerate underlying framerate as rational number
@ -125,7 +135,7 @@ lumiera_quantise_time (gavl_time_t time, gavl_time_t origin, gavl_time_t grid);
* @return time point (frame start) on the Lumiera internal time scale
*/
gavl_time_t
lumiera_time_of_gridpoint (long nr, gavl_time_t origin, gavl_time_t grid);
lumiera_time_of_gridpoint (int64_t nr, gavl_time_t origin, gavl_time_t grid);
/**
* Builds a time value by summing up the given components.
@ -140,6 +150,11 @@ lumiera_build_time (long millis, uint secs, uint mins, uint hours);
/**
* Builds a time value by summing up the given components.
* @todo replace float framerates by lib::time::FrameRate
* @deprecated this function doesn't fit in the general time handling concept.
* SMPTE is a timecode format, and should be distinguished from a
* raw time value. For any timecode format, we always require an explicit
* 'time grid' (coordinate system), which also defines a zero point.
* Usually, this time grid is managed and provided by the session
*/
gavl_time_t
lumiera_build_time_fps (float fps, uint frames, uint secs, uint mins, uint hours);
@ -183,6 +198,9 @@ lumiera_time_frames (gavl_time_t time, float fps);
/**
* Extract the frame count for the given time, using the given fps.
* @todo use the rational lib::time::FrameRate instead of a float
* @deprecated should be moved down into the implementation;
* this function doesn't fit into the general time handling,
* because it doesn't link to a time grid (coordinate system)
*/
int
lumiera_time_frame_count (gavl_time_t time, float fps);

View file

@ -31,6 +31,7 @@
#include "lib/typed-counter.hpp"
#include <tr1/memory>
#include <string>
#include <bitset>
@ -53,6 +54,9 @@ namespace time {
namespace format {
LUMIERA_ERROR_DECLARE (INVALID_TIMECODE); ///< timecode format error, illegal value encountered.
using std::string;
using lib::meta::NoInstance; // the following types are for metaprogramming only...
@ -67,6 +71,7 @@ namespace time {
struct Frames
: NoInstance<Frames>
{
static TimeValue parse (string const&, QuantR);
static void rebuild (FrameNr&, QuantR, TimeValue const&);
static TimeValue evaluate (FrameNr const&, QuantR);
};
@ -81,10 +86,11 @@ namespace time {
struct Smpte
: NoInstance<Smpte>
{
static TimeValue parse (string const&, QuantR);
static void rebuild (SmpteTC&, QuantR, TimeValue const&);
static TimeValue evaluate (SmpteTC const&, QuantR);
static uint getFramerate (QuantR, TimeValue const&);
static void rangeLimitStrategy (SmpteTC&);
static void applyRangeLimitStrategy (SmpteTC&);
};
@ -98,6 +104,7 @@ namespace time {
struct Hms
: NoInstance<Hms>
{
static TimeValue parse (string const&, QuantR);
static void rebuild (HmsTC&, QuantR, TimeValue const&);
static TimeValue evaluate (HmsTC const&, QuantR);
};
@ -115,6 +122,7 @@ namespace time {
struct Seconds
: NoInstance<Seconds>
{
static TimeValue parse (string const&, QuantR);
static void rebuild (Secs&, QuantR, TimeValue const&);
static TimeValue evaluate (Secs const&, QuantR);
};
@ -192,7 +200,7 @@ namespace time {
flags_.set (typeID<F>());
return define(FS());
}
Supported define(NullType) { return *this;} ///< recursion end
Supported define(NullType) { return *this;} ///< @internal recursion end
Supported() { } ///< @note use #formats to set up a new descriptor

View file

@ -62,9 +62,9 @@ namespace time {
virtual ~Grid(); ///< this is an Interface
virtual long gridPoint (TimeValue const& raw) const =0;
virtual int64_t gridPoint (TimeValue const& raw) const =0;
virtual TimeValue gridAlign (TimeValue const& raw) const =0;
virtual TimeValue timeOf (long gridPoint) const =0;
virtual TimeValue timeOf (int64_t gridPoint) const =0;
virtual TimeValue timeOf (FSecs, int =0) const =0;
};

View file

@ -58,7 +58,7 @@ namespace time {
, uint hours
)
: TimeValue(lumiera_build_time (millis,secs,mins,hours))
{ }
{ }
/** convenience constructor to build an Time value

377
src/lib/time/mutation.cpp Normal file
View file

@ -0,0 +1,377 @@
/*
Mutation - changing and adjusting time values
Copyright (C) Lumiera.org
2011, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
* *****************************************************/
#include "lib/error.hpp"
#include "lib/time/timevalue.hpp"
#include "lib/time/timequant.hpp"
#include "lib/time/timecode.hpp"
#include "lib/time/mutation.hpp"
//#include "lib/time.h"
#include "lib/util.hpp"
//#include <tr1/functional>
//using std::string;
//using util::unConst;
//using util::floorwrap;
using util::isnil;
namespace error = lumiera::error;
namespace lib {
namespace time {
LUMIERA_ERROR_DEFINE (INVALID_MUTATION, "Changing a time value in this way was not designated");
Mutation::~Mutation() { } // emit VTable here....
/** @internal actually force a change
* into a target time entity to mutate.
* Mutation is declared fried to TimeValue
* and thus is allowed to influence the basic
* value stored in each time entity
*/
void
Mutation::imposeChange (TimeValue& target, TimeValue const& valueToSet)
{
target = valueToSet;
}
/**
* concrete time value mutation:
* impose fixed new start time.
*/
class SetNewStartTimeMutation
: public ClonableMutation
{
TimeValue newTime_;
virtual void
change (Duration&) const
{
throw error::Logic ("mutating the start point of a pure Duration doesn't make sense"
, LUMIERA_ERROR_INVALID_MUTATION);
}
virtual void
change (TimeSpan& target) const
{
imposeChange (target, newTime_);
}
/** @note the re-quantisation happens automatically
* when the (changed) QuTime is materialised */
virtual void
change (QuTime& target) const
{
imposeChange (target, newTime_);
}
public:
explicit
SetNewStartTimeMutation (TimeValue t)
: newTime_(t)
{ }
};
/**
* concrete time value mutation:
* set a new overall duration for an extended timespan.
*/
class SetNewDuration
: public ClonableMutation
{
Duration changedDuration_;
virtual void
change (Duration& target) const
{
imposeChange (target, changedDuration_);
}
virtual void
change (TimeSpan& target) const
{
imposeChange (target.duration(), changedDuration_);
}
/** @note the re-quantisation happens automatically
* when the (changed) QuTime is materialised */
virtual void
change (QuTime&) const
{
throw error::Logic ("mutating the duration of a (quantised) time point doesn't make sense"
, LUMIERA_ERROR_INVALID_MUTATION);
}
public:
explicit
SetNewDuration (Duration dur)
: changedDuration_(dur)
{ }
};
/**
* concrete time value mutation:
* adjust the given time entity by an offset amount.
*/
class ImposeOffsetMutation
: public ClonableMutation
{
Offset adjustment_;
virtual void
change (Duration& target) const
{
imposeChange (target, TimeVar(target)+=adjustment_);
}
virtual void
change (TimeSpan& target) const
{
imposeChange (target, TimeVar(target)+=adjustment_);
}
/** @note the re-quantisation happens automatically
* when the (changed) QuTime is materialised */
virtual void
change (QuTime& target) const
{
imposeChange (target, TimeVar(target)+=adjustment_);
}
public:
explicit
ImposeOffsetMutation (Offset adj)
: adjustment_(adj)
{ }
};
/**
* concrete time value mutation:
* make the grid aligned time value explicit,
* and impose the resulting value to the given
* time points (or start points).
*/
class MaterialiseIntoTarget
: public SetNewStartTimeMutation
{
public:
explicit
MaterialiseIntoTarget (QuTime const& quant)
: SetNewStartTimeMutation (PQuant(quant)->materialise(quant))
{ }
};
/**
* concrete time value mutation:
* nudge target value by the given number of 'steps',
* relative to either the given grid.
*/
class NudgeMutation
: public ImposeOffsetMutation
{
static Offset
materialiseGridPoint (Symbol gridID, int steps)
{
REQUIRE (!isnil (gridID));
PQuant grid = Quantiser::retrieve(gridID);
return Offset(grid->timeOf(0), grid->timeOf(steps));
}
public:
NudgeMutation (int relativeSteps, Symbol gridID)
: ImposeOffsetMutation(materialiseGridPoint (gridID,relativeSteps))
{ }
};
/**
* concrete time value mutation:
* nudge based on a implicit grid, which is
* either a quantised target value's own grid,
* or a \em natural grid.
* @note currently the natural grid is hard wired,
* just interpreting the step parameter as
* offset in seconds.
*/
class NaturalNudgeMutation
: public ClonableMutation
{
int steps_;
virtual void
change (Duration& target) const
{
imposeChange (target, TimeVar(target)+=Time(FSecs(steps_)));
}
virtual void
change (TimeSpan& target) const
{
imposeChange (target, TimeVar(target)+=Time(FSecs(steps_)));
}
/** Special treatment: use the quantised time's own grid;
* retrieve the corresponding grid point, offset it by the step-parameter,
* then retrieve the corresponding time from the quantised time's
* underlying quantiser (grid).
* @note when the #steps_ parameter is zero, what happens here effectively
* is the materialisation of the quantised target time, i.e. making
* the quantisation explicit and storing the resulting value. */
virtual void
change (QuTime& target) const
{
PQuant const& grid (target);
int64_t originalGridPoint = grid->gridPoint(target);
int64_t adjustedGridPoint = originalGridPoint + steps_;
imposeChange (target, grid->timeOf (adjustedGridPoint));
}
public:
explicit
NaturalNudgeMutation (int relativeSteps)
: steps_(relativeSteps)
{ }
};
/** Convenience factory to yield a simple Mutation changing the absolute start time.
* This whole procedure might look quite inefficient, but actually most of the
* abstractions are removed at runtime, leaving only a single indirection
* through the VTable of the Mutation Interface.
* @throw error::Logic when attempting to change the (non existent) start time of a Duration
* @return EncapsulatedMutation, which is an "polymorphic value object",
* actually carrying an embedded Mutation subclass with the new start time
*/
EncapsulatedMutation
Mutation::changeTime (Time newStartTime)
{
return EncapsulatedMutation::build<SetNewStartTimeMutation> (newStartTime);
}
/** Convenience factory: simple Mutation to adjust the duration or length of a timespan
* @throw error::Logic when attempting to change the "duration" of a quantised time point
* @return EncapsulatedMutation, carrying the new duration value to impose
*/
EncapsulatedMutation
Mutation::changeDuration (Duration changedDur)
{
return EncapsulatedMutation::build<SetNewDuration> (changedDur);
}
/** Convenience factory: simple Mutation to adjust the duration or length of a timespan
* @throw error::Logic when attempting to change the "duration" of a quantised time point
* @return EncapsulatedMutation, carrying the new duration value to impose
*/
EncapsulatedMutation
Mutation::adjust (Offset change)
{
return EncapsulatedMutation::build<ImposeOffsetMutation> (change);
}
/** Convenience factory: materialise the given quantised time into an explicit fixed
* internal time value, according to the underlying time grid; impose the resulting
* value then as new time point or start point to the target
* @note same as materialising and then invoking #changeTime
*/
EncapsulatedMutation
Mutation::materialise (QuTime const& gridAlignedTime)
{
return EncapsulatedMutation::build<MaterialiseIntoTarget> (gridAlignedTime);
}
/** build a time mutation to \em nudge the target time value by an offset,
* defined as number of steps on an implicit nudge grid. If the target is an
* continuous (not quantised) time value or duration, an internal 'default nudge grid'
* will be used to calculate the offset value. Typically, this grid counts in seconds.
* To the contrary, when the target is a quantised value, it will be aligned to the
* grid point relative to the current value's next grid point, measured in number
* of steps. This includes \em materialising the internal time to the exact grid
* position. If especially the adjustment is zero, the internal value will
* be changed to literally equal the current value's next grid point.
*/
EncapsulatedMutation
Mutation::nudge (int adjustment)
{
return EncapsulatedMutation::build<NaturalNudgeMutation> (adjustment);
}
/** build a time mutation to \em nudge the target time value; the nudge time grid
* is specified explicitly here, instead of using a global or 'natural' nudge grid.
* In case the target itself is a quantised time value, a chaining of the two
* grids will happen: first, the nudge grid is used to get an offset value,
* according to the number of steps, then this offset is applied to the
* raw value underlying the quantised target. If this resulting target
* value later will be cast into any kind of time code or materialised
* otherwise, the quantised value's own grid will apply as well,
* resulting in the net result of two quantisation operations
* being applied in sequence.
* @param adjustment number of grid steps to apply as offset
* @param gridID symbolic name used to register or define a
* suitable nudge grid, typically somewhere globally
* in the session (as meta asset)
*/
EncapsulatedMutation
Mutation::nudge (int adjustment, Symbol gridID)
{
return EncapsulatedMutation::build<NudgeMutation> (adjustment, gridID);
}
}} // lib::time

130
src/lib/time/mutation.hpp Normal file
View file

@ -0,0 +1,130 @@
/*
MUTATION.hpp - changing and adjusting time values
Copyright (C) Lumiera.org
2011, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/** @file mutation.hpp
** Modifying time and timecode values.
** Basically, time values are designed to be immutable. Under some circumstances
** we need a means to change existing time values though. Moreover, actually performing
** these changes gets quite involved at the implementation level, due to time / timecode
** values being \em quantised (grid aligned). To keep this implementation logic separate
** from the simple time values, and to allow for later extensions and more elaborate
** change schemes, we choose the <b>visitor pattern</b> for implementing these
** time mutating operations.
**
** \par usage situations
**
** There are four basic usage situations causing an existing time value to be mutated:
**
** - dragging an object or object boundary (resizing)
** - nudging
** - placing an object (e.g. a clip)
** - update of a timecode display in the GUI
**
** All these include sending a change message to the original time spec, while the
** actual content of the change message determines the concrete behaviour. Especially,
** when the target time to be changed or the message content are quantised, an interference
** between multiple time grids may be happen, possibly resulting in a re-quantisation of
** the message into the target object's own time grid.
**
** @todo WIP-WIP-WIP
**
*/
#ifndef LIB_TIME_MUTATION_H
#define LIB_TIME_MUTATION_H
#include "lib/error.hpp"
#include "lib/time/timevalue.hpp"
#include "lib/polymorphic-value.hpp"
#include "lib/symbol.hpp"
#include <boost/noncopyable.hpp>
//#include <iostream>
//#include <boost/operators.hpp>
//#include <string>
namespace lib {
namespace time {
using lib::Symbol;
//using std::string;
//using lib::Literal;
LUMIERA_ERROR_DECLARE (INVALID_MUTATION); ///< Changing a time value in this way was not designated
class QuTime;
class Mutation;
/* The following typedefs allow to hand out
* "unspecific" mutation value objects from
* factory functions, without disclosing any
* implementation details here in this header.
*/
enum{ MUTATION_IMPL_SIZE = sizeof(TimeValue) };
typedef lib::polyvalue::CloneValueSupport<Mutation> ClonableMutation;
typedef lib::PolymorphicValue<Mutation, MUTATION_IMPL_SIZE, ClonableMutation> EncapsulatedMutation;
/**
* Interface: an opaque change imposed onto some time value.
*
* @see time::TimeSpan#accept(Mutation const&)
* @todo WIP-WIP-WIP
*/
class Mutation
{
public:
virtual ~Mutation();
virtual void change (Duration&) const =0;
virtual void change (TimeSpan&) const =0;
virtual void change (QuTime&) const =0;
/* === convenience shortcuts for simple cases === */
static EncapsulatedMutation changeTime (Time);
static EncapsulatedMutation changeDuration (Duration);
static EncapsulatedMutation adjust (Offset);
static EncapsulatedMutation materialise (QuTime const&);
static EncapsulatedMutation nudge (int adjustment);
static EncapsulatedMutation nudge (int adjustment, Symbol gridID);
protected:
static void imposeChange (TimeValue&, TimeValue const&);
};
/* === defining the visitation responses === */
inline void Duration::accept (Mutation const& muta) { muta.change (*this); }
inline void TimeSpan::accept (Mutation const& muta) { muta.change (*this); }
#ifdef LIB_TIME_TIMEQUQNT_H
inline void QuTime::accept (Mutation const& muta) { muta.change (*this); }
#endif
}} // lib::time
#endif

View file

@ -47,7 +47,7 @@ namespace time {
advice::Request<PQuant> query(gridID);
PQuant grid_found = query.getAdvice();
if (!grid_found)
throw error::Logic ("unable to fetch the quantisation grid -- is it already defined?"
throw error::Logic ("unable to fetch the quantisation grid -- was it already defined?" ////////TICKET #197
, LUMIERA_ERROR_UNKNOWN_GRID);
return grid_found;
}
@ -101,6 +101,38 @@ namespace time {
/** Access an existing grid definition or quantiser, known by the given symbolic ID.
* Typically this fetches a meta::TimeGrid (asset) from the session.
* @throw error::Logic if the given gridID wasn't registered
* @return smart-ptr to the quantiser instance */
PQuant
Quantiser::retrieve (Symbol gridID)
{
return retrieveQuantiser (gridID);
}
/** convenience shortcut: \em materialise a raw time value
* based on this grid or time axis, but returning a raw time value.
* Implemented as combination of the #gridAlign and #timeOf operations,
* i.e. we quantise into this scale, but transform the result back onto
* the raw time value scale.
* @warning this operation incurs information loss. Values may be rounded
* and / or clipped, according to the grid used. And, contrary to
* a QuTime value, the information about the actual grid is
* discarded. Please don't use this operation if you just
* "want a number" but feel to too lazy to understand
* properly what quantisation means!
*/
TimeValue
Quantiser::materialise (TimeValue const& raw) const
{
return timeOf (gridPoint (raw));
}
/** alignment to a simple fixed size grid.
* The actual calculation first determines the number
* of the grid interval containing the given rawTime,
@ -128,7 +160,7 @@ namespace time {
* range when converting back into a TimeValue.
* @see #lumiera_quantise_frames
*/
long
int64_t
FixedFrameQuantiser::gridPoint (TimeValue const& rawTime) const
{
return lumiera_quantise_frames (_raw(rawTime), _raw(origin_), _raw(raster_));
@ -141,7 +173,7 @@ namespace time {
* valid range of lumiera::Time
*/
TimeValue
FixedFrameQuantiser::timeOf (long gridPoint) const
FixedFrameQuantiser::timeOf (int64_t gridPoint) const
{
return TimeValue (lumiera_time_of_gridpoint (gridPoint, _raw(origin_), _raw(raster_)));
}

View file

@ -92,11 +92,14 @@ namespace time {
return supportedFormats_.check<FMT>();
}
static PQuant retrieve (Symbol gridID);
TimeValue materialise (TimeValue const& raw) const;
//------Grid-API----------------------------------------------
virtual long gridPoint (TimeValue const& raw) const =0;
virtual int64_t gridPoint (TimeValue const& raw) const =0;
virtual TimeValue gridAlign (TimeValue const& raw) const =0;
virtual TimeValue timeOf (long gridPoint) const =0;
virtual TimeValue timeOf (int64_t gridPoint) const =0;
virtual TimeValue timeOf (FSecs, int =0) const =0;
};
@ -123,9 +126,9 @@ namespace time {
FixedFrameQuantiser (FrameRate const& frames_per_second, TimeValue referencePoint =TimeValue(0));
FixedFrameQuantiser (Duration const& frame_duration, TimeValue referencePoint =TimeValue(0));
long gridPoint (TimeValue const&) const;
int64_t gridPoint (TimeValue const&) const;
TimeValue gridAlign (TimeValue const&) const;
TimeValue timeOf (long gridPoint) const;
TimeValue timeOf (int64_t gridPoint) const;
TimeValue timeOf (FSecs, int =0) const;
};

View file

@ -30,12 +30,19 @@
#include "lib/util.hpp"
#include <tr1/functional>
#include <boost/regex.hpp>
#include <boost/lexical_cast.hpp>
using std::string;
using util::unConst;
using util::isSameObject;
using util::floorwrap;
using boost::regex;
using boost::smatch;
using boost::regex_search;
using boost::lexical_cast;
namespace error = lumiera::error;
namespace lib {
namespace time {
@ -43,7 +50,102 @@ namespace time {
TCode::~TCode() { } // emit VTable here....
namespace format {
namespace format { /* ================= Timecode implementation details ======== */
LUMIERA_ERROR_DEFINE (INVALID_TIMECODE, "timecode format error, illegal value encountered");
/** try to parse a frame number specification
* @param frameNumber string containing an integral number with trailing '#'
* @param frameGrid coordinate system (and thus framerate) to use for the conversion
* @return (opaque internal) lumiera time value of the given frame's start position
* @throw error::Invalid in case of parsing failure
* @note the string may contain any additional content, as long as a
* regular-expression search is able to pick out a suitable value
*/
TimeValue
Frames::parse (string const& frameNumber, QuantR frameGrid)
{
static regex frameNr_parser ("(?<![\\.\\-\\d])(-?\\d+)#"); // no leading [.-\d], number+'#'
smatch match;
if (regex_search (frameNumber, match, frameNr_parser))
return frameGrid.timeOf (lexical_cast<int64_t> (match[1]));
else
throw error::Invalid ("unable to parse framecount \""+frameNumber+"\""
, LUMIERA_ERROR_INVALID_TIMECODE);
}
TimeValue
Smpte::parse (string const&, QuantR)
{
UNIMPLEMENTED("parsing SMPTE timecode");
}
TimeValue
Hms::parse (string const&, QuantR)
{
UNIMPLEMENTED("parse a hours:mins:secs time specification");
}
/** try to parse a time specification in seconds or fractional seconds.
* The value is interpreted relative to the origin of a the given time grid
* This parser recognises full seconds, fractional seconds and both together.
* In any case, the actual number is required to end with a trailing \c 'sec'
* @par Example specifications
\verbatim
12sec --> 12 * GAVL_TIME_SCALE
-4sec --> -4 * GAVL_TIME_SCALE
5/4sec --> 1.25 * GAVL_TIME_SCALE
-5/25sec --> -0.2 * GAVL_TIME_SCALE
1+1/2sec --> 1.5 * GAVL_TIME_SCALE
1-1/25sec --> 0.96 * GAVL_TIME_SCALE
-12-1/4sec --> -11.75 * GAVL_TIME_SCALE
\endverbatim
* @param seconds string containing a time spec in seconds
* @param grid coordinate system the parsed value is based on
* @return the corresponding (opaque internal) lumiera time value
* @throw error::Invalid in case of parsing failure
* @note the string may contain any additional content, as long as a
* regular-expression search is able to pick out a suitable value
*/
TimeValue
Seconds::parse (string const& seconds, QuantR grid)
{
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])
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));
return grid.timeOf (fullSeconds + fractionalPart);
}
else
if (match[3].matched)
{
// only a fractional part was given
return grid.timeOf (FSecs (SUB_EXPR(1), SUB_EXPR(3)));
}
else
{
// just simple non-fractional seconds
return grid.timeOf (FSecs (SUB_EXPR(1)));
}
else
throw error::Invalid ("unable to parse \""+seconds+"\" as (fractional)seconds"
, LUMIERA_ERROR_INVALID_TIMECODE);
}
/** build up a frame count
* by quantising the given time value
@ -51,7 +153,7 @@ namespace time {
void
Frames::rebuild (FrameNr& framecnt, QuantR quantiser, TimeValue const& rawTime)
{
framecnt.setValueRaw(quantiser.gridPoint (rawTime));
framecnt.setValueRaw (quantiser.gridPoint (rawTime));
}
/** calculate the time point denoted by this frame count */
@ -79,11 +181,11 @@ namespace time {
TimeValue
Smpte::evaluate (SmpteTC const& tc, QuantR quantiser)
{
long frameRate = tc.getFps();
long gridPoint = tc.frames
+ tc.secs * frameRate
+ tc.mins * frameRate * 60
+ tc.hours * frameRate * 60 * 60;
uint frameRate = tc.getFps();
int64_t gridPoint(tc.frames);
gridPoint += int64_t(tc.secs) * frameRate;
gridPoint += int64_t(tc.mins) * frameRate * 60;
gridPoint += int64_t(tc.hours) * frameRate * 60 * 60;
return quantiser.timeOf (tc.sgn * gridPoint);
}
@ -103,9 +205,9 @@ namespace time {
uint
Smpte::getFramerate (QuantR quantiser_, TimeValue const& rawTime)
{
long refCnt = quantiser_.gridPoint(rawTime);
long newCnt = quantiser_.gridPoint(Time(0.5,1) + rawTime);
long effectiveFrames = newCnt - refCnt;
int64_t refCnt = quantiser_.gridPoint(rawTime);
int64_t newCnt = quantiser_.gridPoint(Time(0,1) + rawTime);
int64_t effectiveFrames = newCnt - refCnt;
ENSURE (1000 > effectiveFrames);
ENSURE (0 < effectiveFrames);
return uint(effectiveFrames);
@ -131,7 +233,7 @@ namespace time {
* orientation (0:0:0:0 - 1sec = -1:59:59:0)
*/
void
Smpte::rangeLimitStrategy (SmpteTC& tc)
Smpte::applyRangeLimitStrategy (SmpteTC& tc)
{
if (tc.hours < 0)
tc.invertOrientation();
@ -171,7 +273,7 @@ namespace time {
wrapHours (SmpteTC* thisTC, int rawHours)
{
thisTC->hours.setValueRaw (rawHours);
format::Smpte::rangeLimitStrategy (*thisTC);
format::Smpte::applyRangeLimitStrategy (*thisTC);
}

View file

@ -45,7 +45,7 @@ namespace time {
/**
* Interface: fixed format timecode specification.
* @see time::Format
* @see time::format
* @todo WIP-WIP-WIP
*/
class TCode
@ -76,18 +76,25 @@ namespace time {
/**
* A frame counting timecode value.
* This is an hard-coded representation of
* TCode<format::Frames>, with additional convenience
* constructors and conversions, which basically make
* FrameNr values interchangeable with integral numbers.
* This is the hard-coded standard representation of
* format::Frames, and is defined such as to make FrameNr
* values interchangeable with integral numbers.
* Like any concrete TCode subclass, it can be created
* based on a QuTime value. This way, not only the (raw) TimeValue
* is provided, but also the (frame)-Grid to base the frame count on.
* But contrary to a QuTime value, a FrameNr value is \em materialised
* (rounded) into a definite integral number, stripping the excess
* precision contained in the original (raw) TimeValue.
* As framecount values are implemented as single display field for an
* integral value (time::Digxel), they allow for simple presentation.
*/
class FrameNr
: public TCode
, public CountVal
{
string show() const { return string(CountVal::show())+"fr"; }
Literal tcID() const { return "Frame-count"; }
string show() const { return string(CountVal::show())+"#"; }
Literal tcID() const { return "Framecount"; }
TimeValue value() const { return Format::evaluate (*this, *quantiser_); }
public:
@ -95,13 +102,36 @@ namespace time {
FrameNr (QuTime const& quantisedTime);
// CountVal implicitly convertible to long
using TCode::operator string;
// CountVal implicitly convertible to long
};
/**
* Classical Timecode value reminiscent to SMPTE format.
* After quantisation, the resulting (materialised) time value is
* decimated into a hours, a minutes, a seconds part and the remainder
* is cast into a frame number relative to the seconds. Consequently,
* a SmpteTC representation is always linked implicitly to a specific framerate.
*
* \par range extensions
* Historically, SMPTE timecode format was focused mainly on how to encode a
* unique timestamp in a way allowing to 'piggyback' these timestamps into an
* existing (analogue) media data format. As a side effect, quite tight limits
* had to be imposed on the possible component values in such a fixed-length format.
* This whole concern is completely beyond the scope of a typical computer based video
* implementation; thus we can consider ways to extend the value range to be represented
* in this SMPTE-like timecode format:
* - we can allow time values below the zero point
* - we can allow time values beyond 24 hours.
* Several different schemes how to do this extensions could be devised (and in addition,
* we could also wrap around the hours field, jumping from 23:59:59:## to 0:0:0:0).
* Currently, we implement an extension, where the timecode representation is symmetrical
* to the zero point and the hours field is just extended beyond 23 hours. To give an
* example: \c 0:0:0:0 minus 1 frame yields \c -0:0:0:1
*
* @todo the range extension scheme could be a configurable strategy
*/
class SmpteTC
: public TCode
@ -141,7 +171,7 @@ namespace time {
/**
*
* @warning missing implementation
*/
class HmsTC
: public TCode
@ -166,7 +196,7 @@ namespace time {
/**
*
* @warning partially missing implementation
*/
class Secs
: public TCode

View file

@ -75,6 +75,10 @@ namespace time {
template<class TC>
void
castInto (TC& timecode) const;
/** receive change message, which
might cause re-quantisation */
void accept (Mutation const&);
};

View file

@ -74,6 +74,9 @@ namespace time {
return *this;
}
/** some subclasses may receive modification messages */
friend class Mutation;
public:
/** explicit limit of allowed time range */
static gavl_time_t limited (gavl_time_t raw);
@ -109,7 +112,7 @@ namespace time {
* @note supports scaling by a factor,
* which \em deliberately is chosen
* as int, not gavl_time_t, because the
* multiplying times is meaningless.
* multiplying of times is meaningless.
*/
class TimeVar
: public TimeValue
@ -170,8 +173,11 @@ namespace time {
: public TimeValue
{
protected:
/** generally immutable,
* but derived classes allow some limited mutation
* through special API calls */
Offset&
operator= (Offset const& o) ///< derived classes allow mutation
operator= (Offset const& o)
{
TimeValue::operator= (o);
return *this;
@ -192,6 +198,9 @@ namespace time {
{
return TimeValue(std::llabs (t_));
}
// Supporting sign flip
Offset operator- () const;
};
//-- support linear offset chaining ---------------
@ -218,6 +227,11 @@ namespace time {
return factor*distance;
}
inline Offset
Offset::operator- () const
{
return -1 * (*this);
}
@ -237,18 +251,15 @@ namespace time {
*
* Lumiera Time provides some limited capabilities for
* direct manipulation; Time values can be created directly
* from \c (h,m,s,ms) specification and there is an string
* representation intended for internal use (reporting and
* debugging). Any real output, formatting and persistent
* from \c (ms,sec,min,hour) specification and there is an
* string representation intended for internal use (reporting
* and debugging). Any real output, formatting and persistent
* storage should be based on the (quantised) timecode
* formats though, which can be generated from time values.
*
* Non constant Time objects can receive an encapsulated
* \em mutation message, which is also the basis for
* changing time spans, durations and for re-aligning
* quantised values to some other grid.
*
* @todo define these mutations
* Similar to TimeValue, also Time objects are considered
* immutable values. As convenience shortcut, some operators
* are provided, creating a TimVar for further calculations.
*/
class Time
: public TimeValue
@ -293,14 +304,14 @@ namespace time {
class TimeSpan;
class Mutation;
/**
* Duration is the internal Lumiera time metric.
* It is an absolute (positive) value, but can be
* promoted from an offset. Similar to Time,
* Duration can receive mutation messages.
*
* @todo define these mutations
* promoted from an offset. While Duration generally
* is treated as immutable value, there is the
* possibility to send a \em Mutation message.
*/
class Duration
: public Offset
@ -314,7 +325,7 @@ namespace time {
{ }
explicit
Duration (Time const& timeSpec)
Duration (TimeValue const& timeSpec)
: Offset(Offset(timeSpec).abs())
{ }
@ -327,6 +338,8 @@ namespace time {
Duration (ulong count, FrameRate const& fps);
static const Duration NIL;
void accept (Mutation const&);
};
@ -339,20 +352,47 @@ namespace time {
* to fully specify the temporal properties of an
* object within the model.
*
* Similar to Time and Duration, a TimeSpan may
* also receive an (opaque) mutation message.
* As an exception to the generally immutable Time
* entities, a non constant TimeSpan may receive
* \em mutation messages, both for the start point
* and the duration. This allows for changing
* position and length of objects in the timeline.
*
* @todo define these mutations
*/
class TimeSpan
: public Time
, boost::totally_ordered<TimeSpan>
{
Duration dur_;
public:
TimeSpan(Time start, Duration length)
TimeSpan(TimeValue const& start, Duration const& length)
: Time(start)
, dur_(length)
{ }
///////////TODO creating timespans needs to be more convenient....
TimeSpan(TimeValue const& start, Offset const& reference_distance)
: Time(start)
, dur_(reference_distance)
{ }
TimeSpan(TimeValue const& start, TimeValue const& end)
: Time(start)
, dur_(Offset(start,end))
{ }
TimeSpan(TimeValue const& start, FSecs(duration_in_secs))
: Time(start)
, dur_(duration_in_secs)
{ }
Duration&
duration()
{
return dur_;
}
Duration
duration() const
@ -360,12 +400,27 @@ namespace time {
return dur_;
}
Time
start() const
{
return *this;
}
Time
end() const
{
TimeVar startPoint (*this);
return (startPoint + dur_);
}
/** may change start / duration */
void accept (Mutation const&);
/// Supporting extended total order, based on start and interval length
friend bool operator== (TimeSpan const& t1, TimeSpan const& t2) { return t1.t_==t2.t_ && t1.dur_==t2.dur_; }
friend bool operator< (TimeSpan const& t1, TimeSpan const& t2) { return t1.t_< t2.t_ ||
(t1.t_==t2.t_ && t1.dur_< t2.dur_);}
};

View file

@ -315,7 +315,7 @@ namespace util {
* will start with an alphanumeric character.
*
* @par Example Conversions
\verbatim
\verbatim
"Word" --> 'Word'
"a Sentence" --> 'a_Sentence'
"trailing Withespace \t \n" --> 'trailing_Withespace'

View file

@ -130,7 +130,7 @@ namespace asset {
* - asset::Media representing mediafiles
* - asset::Proc representing media data processing components (e.g. Effects)
* - asset::Struct representing structural components used in the session (e.g. Tracks)
* - asset::Meta representing meta objects created while editing (e.g. Automation)
* - asset::Meta representing metadata, parametrisation, customisation.
*
* And of course there are various concrete Asset subclasses, like asset::Clip,
* asset::Effect, asset::Codec, asset::Track, asset::Dataset.

View file

@ -48,7 +48,7 @@
** and can be another already existing asset::Meta (inheriting from meta::Descriptor)
** Generally this yields a Builder object, which can be used for outfitting the new
** or changed metadata entry, finally \em committing this builder to yield a new
** asset::Meta (which, in case of a mutation, might superseede an existin one).
** asset::Meta (which, in case of a mutation, might supersede an existing one).
**
** @see asset.hpp explanation of assets in general
** @see MetaFactory creating concrete asset::Meta instances
@ -120,7 +120,8 @@ namespace asset {
public:
static MetaFactory create;
virtual const ID<Meta>& getID() const ///< @return ID of kind Meta
/** @return ID of kind Meta */
virtual const ID<Meta>& getID() const
{
return static_cast<const ID<Meta>& > (Asset::getID());
}

View file

@ -505,6 +505,11 @@ out: can_IterForEach<CustomForwardIter> : Yes
END
TEST "Polymorphic value objects" PolymorphicValue_test <<END
return: 0
END
PLANNED "RefArray_test" RefArray_test <<END
return: 0
END
@ -678,6 +683,11 @@ return: 0
END
TEST "Changing time specifications" TimeMutation_test <<END
return: 0
END
TEST "Quantiser API basics" QuantiserBasics_test <<END
return: 0
END
@ -689,11 +699,18 @@ END
TEST "Time formats and timecodes" TimeFormats_test <<END
out: Framecount=".+#" time = .+:..:..
out: SMPTE=".+:..:..:.." time = .+:..:..
out-lit: ----SMPTE-----
out-lit: SMPTE=" 5:42:23:13" time = 5:42:23.520
return: 0
END
TEST "Parsing time values" TimeParsing_test <<END
END
TEST "Symbol_test" Symbol_test <<END
out: one
out: sizeof.+lib.Literal.+ = (4|8)

View file

@ -0,0 +1,311 @@
/*
PolymorphicValue(Test) - verify handling of opaque polymorphic values
Copyright (C) Lumiera.org
2011, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
* *****************************************************/
#include "lib/test/run.hpp"
#include "lib/test/test-helper.hpp"
#include "lib/util-foreach.hpp"
#include "lib/polymorphic-value.hpp"
#include <vector>
namespace lib {
namespace test{
using ::Test;
using util::for_each;
using util::unConst;
using util::isSameObject;
using lumiera::error::LUMIERA_ERROR_ASSERTION;
namespace { // test dummy hierarchy
// Note: largely varying space requirements
// correct function depending on concrete class
struct Interface
{
virtual ~Interface() {}
virtual long apiFunc() =0;
virtual long& localSum() =0;
bool
operator== (Interface const& o) const
{
return unConst(this)->localSum()
== unConst(o).localSum();
}
};
const uint MAX_RAND = 1000;
const uint MAX_ELM = 111;
const uint MAX_SIZ = sizeof(long[MAX_ELM]);
/* Checksums to verify proper ctor-dtor calls and copy operations */
long _checkSum = 0;
long _callSum = 0;
uint _created = 0;
/**
* Template to generate concrete implementation classes.
* @note the generated classes vary largely in size, and
* moreover the actual place to store the checksum
* also depends on that size parameter.
*/
template<uint ii, class BASE=Interface>
struct Imp : BASE
{
long localData_[ii];
~Imp()
{
mark (-1 * localSum());
CHECK (0 == localSum());
}
Imp()
{
REQUIRE (0 < ii);
localSum() = 0;
mark(ii);
++ _created;
}
Imp (Imp const& o)
{
++ _created;
copyData (o);
_checkSum += localSum();
}// adjust, because we've gotten two identical instances
Imp&
operator= (Imp const& o)
{
_checkSum -= localSum();
copyData (o);
_checkSum += localSum();
return *this;
}
private:
virtual long
apiFunc()
{
long rr = ii * (rand() % MAX_RAND);
mark (rr);
_callSum += rr;
return rr;
}
long&
localSum()
{
return localData_[ii-1];
}
void
mark (long markerValue)
{
localSum() += markerValue;
_checkSum += markerValue;
}
void
copyData (Imp const& o)
{
for (uint i=0; i<ii; ++i)
localData_[i] = o.localData_[i];
}
};
/** maximum additional storage maybe wasted
* due to alignment of the contained object
* within the embedded Holder/Buffer
*/
const size_t _ALIGN_ = sizeof(size_t);
}
typedef PolymorphicValue<Interface, MAX_SIZ> PolyVal;
typedef std::vector<PolyVal> TestList;
/**********************************************************************************
* @test build a bunch of PolymorphicValue objects. Handle them like copyable
* value objects, without knowing the exact implementation type; moreover
* execute implementation internals only disclosed to the concrete subtype.
* Verify correctness through checksums.
*/
class PolymorphicValue_test : public Test
{
virtual void
run (Arg)
{
_checkSum = 0;
_callSum = 0;
_created = 0;
verifyBasics();
{
TestList objs = createOpaqueValues ();
for_each (objs, operate);
}
CHECK (0 == _checkSum); // all dead
verifyOverrunProtection();
verifyCopySupportDetectionMetafunctions();
}
TestList
createOpaqueValues ()
{
TestList list;
list.push_back (PolyVal::build<Imp<1> > () );
list.push_back (PolyVal::build<Imp<11> > () );
list.push_back (PolyVal::build<Imp<111> >() );
list.push_back (PolyVal::build<Imp<23> > () );
list.push_back (PolyVal::build<Imp<5> > () );
return list;
} //note: copy
static void
operate (PolyVal& elm)
{
PolyVal myLocalVal(elm);
CHECK (elm == myLocalVal);
long prevSum = _callSum;
long randVal = myLocalVal->apiFunc();
CHECK (prevSum + randVal == _callSum);
CHECK (elm != myLocalVal);
elm = myLocalVal;
CHECK (elm == myLocalVal);
CHECK (!isSameObject (elm, myLocalVal));
CHECK (sizeof(myLocalVal) <= MAX_SIZ + polyvalue::Trait<Interface>::ADMIN_OVERHEAD + _ALIGN_);
}
void
verifyBasics()
{
typedef Imp<MAX_ELM> MaximumSizedImp;
// Standard case: no copy support by client objects
verifyCreation_and_Copy<PolyVal, MaximumSizedImp>();
// Special case: client objects expose extension point for copy support
typedef polyvalue::CopySupport<Interface> CopySupportAPI; // Copy support API declared as sub-interface
typedef Imp<MAX_ELM,CopySupportAPI> CopySupportingImp; // insert this sub-interface between public API and Implementation
typedef PolymorphicValue<Interface, MAX_SIZ, CopySupportAPI> OptimalPolyVal; // Make the Holder use this special attachment point
CHECK (sizeof(OptimalPolyVal) < sizeof(PolyVal)); // results in smaller Holder and less implementation overhead
verifyCreation_and_Copy<OptimalPolyVal, CopySupportingImp>();
}
template<class PV,class IMP>
void
verifyCreation_and_Copy()
{
typedef PV Holder;
typedef IMP ImpType;
typedef typename PV::Interface Api;
long prevSum = _checkSum;
uint prevCnt = _created;
Holder val = Holder::template build<ImpType>();
CHECK (prevSum+111 == _checkSum); // We got one primary ctor call
CHECK (prevCnt+1 <= _created); // Note: usually, the compiler optimises
CHECK (prevCnt+2 >= _created); // and skips the spurious copy-operation
CHECK (sizeof(Holder) >= sizeof(ImpType));
Api& embedded = val;
CHECK (isSameObject(embedded,val));
CHECK (INSTANCEOF(ImpType, &embedded));
prevCnt = _created;
Holder val2(val); // invoke copy ctor without knowing the implementation type
embedded.apiFunc();
CHECK (val != val2); // invoking the API function had an sideeffect on the state
val = val2; // assignment of copy back to the original...
CHECK (val == val2); // cancels the side effect
CHECK (prevCnt+1 == _created); // one new embedded instance was created by copy ctor
}
void
verifyOverrunProtection()
{
typedef Imp<MAX_ELM+1> OversizedImp;
CHECK (MAX_SIZ < sizeof(OversizedImp));
#if false ///////////////////////////////////////////////////////////////////////////////////////////////TICKET #537 : restore throwing ASSERT
VERIFY_ERROR (ASSERTION, PolyVal::build<OversizedImp>() );
#endif ///////////////////////////////////////////////////////////////////////////////////////////////TICKET #537 : restore throwing ASSERT
}
/** @Test internally, PolymorphicValue uses some metafunctions
* to pick a suitable code path, based on the presence of helper functions
* on the API of the embedded objects. Default is no support by these objects,
* which then requires to use a more expensive implementation. Sometimes it's
* desirable to support \em cloning only (copy ctor), but no assignment after
* the fact. In this special case, a support API with only a \cloneInto member
* can be implemented, causing the PolymorphicValue container to raise an
* exception in case the copy operator is invoked.
*/
void
verifyCopySupportDetectionMetafunctions()
{
typedef polyvalue::CopySupport<Interface> CopySupportAPI;
typedef polyvalue::CloneValueSupport<Interface> CloneOnlyAPI;
CHECK ( !polyvalue::exposes_CloneFunction<Interface>::value );
CHECK ( polyvalue::exposes_CloneFunction<CopySupportAPI>::value );
CHECK ( polyvalue::exposes_CloneFunction<CloneOnlyAPI>::value );
CHECK ( polyvalue::allow_Clone_but_no_Copy<CloneOnlyAPI>::value );
CHECK ( !polyvalue::allow_Clone_but_no_Copy<CopySupportAPI>::value );
}
};
LAUNCHER (PolymorphicValue_test, "unit common");
}} // namespace lib::test

View file

@ -26,16 +26,19 @@
#include "proc/asset/meta/time-grid.hpp"
#include "lib/time/timequant.hpp"
#include "lib/time/timecode.hpp"
#include "lib/time/mutation.hpp"
#include "lib/time/display.hpp"
#include "lib/util.hpp"
#include <boost/lexical_cast.hpp>
#include <iostream>
//#include <cstdlib>
#include <string>
#include <cstdlib>
using boost::lexical_cast;
using util::isnil;
//using std::rand;
using std::rand;
using std::string;
using std::cout;
using std::endl;
@ -45,6 +48,24 @@ namespace time{
namespace test{
using asset::meta::TimeGrid;
using format::Frames;
using format::Smpte;
namespace{
const int MAX_FRAME = 265*24*60*60*25;
string
generateRandomFrameNr()
{
uint frameNr(0);
while (!frameNr)
frameNr = rand() % (2*MAX_FRAME) - MAX_FRAME;
return lexical_cast<string>(frameNr)+"#";
}
}
/********************************************************
* @test verify handling of grid aligned timecode values.
@ -60,7 +81,7 @@ namespace test{
{
TimeGrid::build("pal0", FrameRate::PAL);
// checkTimecodeUsageCycle ();
checkTimecodeUsageCycle ();
// checkFrames ();
// checkSeconds ();
// checkHms ();
@ -70,10 +91,54 @@ namespace test{
}
/** @test demonstrate a full usage cycle of timecode and time values.
* Starting with a textual representation according to a specific timecode format,
* and based on the knowledge of the implicit underlying time grid (coordinate system,
* here with origin=0 and framerate=25fps), this timecode string may be parsed.
* This brings us (back) to the very origin, which is a raw TimeValue (internal time).
* Now, this time value might be manipulated, compared to other values etc.
* Anyway, at some point these time values need to be related to some time scale again,
* leading to quantised time values, which — finally — can be cast into a timecode format
* for external representation again, thus closing the circle.
*/
void
checkTimecodeUsageCycle ()
{
UNIMPLEMENTED ("full usage cycle for a timecode value");
string quellCode = generateRandomFrameNr();
PQuant refScale = Quantiser::retrieve("pal0");
// get internal (raw) time value
TimeValue t1 = format::Frames::parse(quellCode, *refScale);
ENSURE (0 != t1);
// manipulating
TimeVar v1(t1);
v1 += Time(FSecs(6,5));
CHECK (t1 < v1);
// quantising into an external grid
QuTime q1 (t1, "pal0");
CHECK (q1 == t1);
// further mutations (here nudge by +5 grid steps)
QuTime q2 = q1;
q2.accept (Mutation::nudge(+5));
CHECK (q1 < q2);
// converting (back) into a timecode format
FrameNr frames1(q1);
FrameNr frames2(q2);
CHECK (5 == frames2 - frames1);
q2.accept (Mutation::changeTime(v1));
CHECK (30 == q2.formatAs<Frames>() - frames1);
CHECK (quellCode == string(frames1));
CHECK (quellCode != string(frames2));
showTimeCode (frames1);
showTimeCode (frames2);
showTimeCode (q2.formatAs<Smpte>());
}
@ -94,7 +159,7 @@ namespace test{
void
checkSeconds ()
{
UNIMPLEMENTED ("verify seconds as timecode format");
UNIMPLEMENTED ("verify fractional seconds as timecode format");
}
@ -122,6 +187,7 @@ namespace test{
QuTime t1 (raw, "pal0");
SmpteTC smpte(t1);
cout << "----SMPTE-----" << endl;
showTimeCode(smpte);
CHECK (" 5:42:23:13" == string(smpte));
CHECK (raw - Time(35,0) == smpte.getTime()); // timecode value got quantised towards next lower frame

View file

@ -0,0 +1,272 @@
/*
TimeMutation(Test) - explicitly changing time spcifications
Copyright (C) Lumiera.org
2011, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
* *****************************************************/
#include "lib/test/run.hpp"
#include "lib/test/test-helper.hpp"
#include "lib/time/timevalue.hpp"
#include "lib/time/timequant.hpp"
#include "lib/time/mutation.hpp"
#include "proc/asset/meta/time-grid.hpp"
#include "lib/util.hpp"
#include <boost/lexical_cast.hpp>
#include <iostream>
#include <string>
using boost::lexical_cast;
using util::isnil;
using std::cout;
using std::endl;
using std::string;
namespace lib {
namespace time{
namespace test{
using asset::meta::TimeGrid;
namespace {
inline string
pop (Arg arg)
{
if (isnil (arg)) return "";
string entry = arg[0];
arg.erase (arg.begin());
return entry;
}
}
/****************************************************************
* @test cover all basic cases for mutating a time specification.
* - change to a given value
* - change by an offset
* - change using a grid value
* - apply an (grid) increment
*/
class TimeMutation_test : public Test
{
gavl_time_t
random_or_get (string arg)
{
if (isnil(arg))
return gavl_time_t (1 + (rand() % 100000)) * GAVL_TIME_SCALE;
else
return lexical_cast<gavl_time_t> (arg);
}
struct TestValues
{
TimeVar var;
Duration dur;
TimeSpan span;
QuTime quant;
TestValues (TimeValue o)
: var(o)
, dur(o)
, span(o, Offset(o))
, quant(o, "test_grid")
{ }
};
virtual void
run (Arg arg)
{
TimeValue o (random_or_get (pop(arg)));
TimeValue c (random_or_get (pop(arg)));
CHECK (o != c, "unsuitable testdata");
// using a 25fps-grid, but with an time origin offset by 1/50sec
TimeGrid::build("test_grid", FrameRate::PAL, Time(FSecs(1,50)));
QuTime qChange (c, "test_grid");
FrameNr count(qChange);
mutate_by_Value (o, Time(c));
mutate_by_Offset (o, Offset(c));
mutate_quantised (o, qChange);
mutate_by_Increment(o, count);
}
void
mutate_by_Value(TimeValue original, Time newStart)
{
TestValues t(original);
CHECK (t.span.start() == original);
t.span.accept (Mutation::changeTime (newStart));
CHECK (t.span.start() != original);
CHECK (t.span.start() == newStart);
// instead of invoking directly, we can store and copy mutation messages
EncapsulatedMutation change_back(Mutation::changeTime (Time(original)));
t.span.accept (change_back);
CHECK (t.span.start() == original);
CHECK (t.quant == original);
t.quant.accept (Mutation::changeTime (newStart));
CHECK (t.quant != original);
CHECK (t.quant == newStart);
// Durations have no start time...
VERIFY_ERROR (INVALID_MUTATION, t.dur.accept(change_back));
VERIFY_ERROR (INVALID_MUTATION, t.span.duration().accept(change_back));
CHECK (t.dur == original);
t.dur.accept (Mutation::changeDuration (Duration(2*t.var)));
CHECK (t.dur != original);
CHECK (t.dur == t.var*2);
CHECK (t.span.start() == original);
CHECK (t.span.duration() == original);
t.span.accept (Mutation::changeDuration(Duration(3*t.var)));
CHECK (t.span.duration() != original);
CHECK (t.span.duration() == t.var*3); // affects the duration,
CHECK (t.span.start() == original); // while the start time remains unaltered
// can't change the 'duration' of a quantised time point...
VERIFY_ERROR (INVALID_MUTATION, t.quant.accept(Mutation::changeDuration (Duration(t.var))));
}
void
mutate_by_Offset (TimeValue original, Offset change)
{
TestValues t(original);
TimeValue& should_be(t.var+=change); // use as ref for verification
CHECK (t.span == original);
CHECK (t.span != should_be);
t.span.accept (Mutation::adjust (change));
CHECK (t.span == should_be);
t.dur.accept (Mutation::adjust (change));
CHECK (t.dur == should_be);
t.quant.accept (Mutation::adjust (change));
CHECK (t.quant == should_be);
// adjustment is cumulative
EncapsulatedMutation back_off = Mutation::adjust (-change);
t.span.accept (back_off);
CHECK (t.span == original);
t.span.accept (back_off);
t.span.accept (back_off);
t.span.accept (back_off);
CHECK (t.span == Time(original) - 3*change);
}
void
mutate_quantised (TimeValue original, QuTime change)
{
TestValues t(original);
t.var = change;
CHECK (Time(change) == t.var); // the underlying raw time value
CHECK (t.span == original);
t.span.accept (Mutation::materialise (change));
CHECK (t.span != original);
CHECK (t.span != t.var); // really materialised (grid-aligned)
// simulate what happened by explicit operations...
Secs seconds = change.formatAs<format::Seconds>();
PQuant quantiser(change);
Time materialised (quantiser->materialise(change));
CHECK (t.span == materialised);
CHECK (t.span.duration() == original); // not affected by mutation as usual
VERIFY_ERROR (INVALID_MUTATION, t.dur.accept (Mutation::materialise (change)));
// not surprising, a time point has no duration!!
CHECK (t.quant == original);
t.quant.accept (Mutation::materialise (change));
CHECK (t.quant != original);
CHECK (t.quant == materialised);
// but note, here we checked the underlying raw value.
// because t.quant is itself quantised, this might
// result in a second, chained quantisation finally
// Here accidentally both the change and t.quant use the same grid.
// For a more contrived example, we try to use a different grid...
TimeGrid::build("special_funny_grid", 1, Time(0,-10)); // (1 frame per second, zero point at -10s)
QuTime funny (original, "special_funny_grid");
funny.accept (Mutation::materialise (change));
CHECK (funny == t.quant); // leading to the same raw value this far
Time doublyQuantised (PQuant(funny)->materialise(funny));
CHECK (doublyQuantised != materialised);
}
void
mutate_by_Increment (TimeValue original, int change)
{
TestValues t(original);
// without any additional specification,
// the nudge-Mutation uses a 'natural grid'
t.span.accept (Mutation::nudge (change));
t.dur.accept (Mutation::nudge (change));
t.var += Time(FSecs(change)); // natural grid is in seconds
CHECK (t.span.start() == t.var);
CHECK (t.dur == t.var);
// any other grid can be specified explicitly
t.dur.accept (Mutation::nudge (change, "test_grid"));
CHECK (t.dur != t.var);
CHECK (t.dur == t.var + change * FrameRate::PAL.duration());
// ....this time the change was measured in grid units,
// taken relative to the origin of the specified grid
PQuant testGrid = Quantiser::retrieve("test_grid");
Offset distance (testGrid->timeOf(0), testGrid->timeOf(change));
CHECK (distance == change * FrameRate::PAL.duration());
CHECK (t.dur - t.var == distance);
// To the contrary, *quantised* values behave quite differently...
long frameNr = t.quant.formatAs<format::Frames>();
t.quant.accept (Mutation::nudge (change));
CHECK (t.quant != original);
long frameNr_after = t.quant.formatAs<format::Frames>();
CHECK (frameNr_after == frameNr + change);
//i.e. the quantised time's own grid is used
}
};
/** Register this test class... */
LAUNCHER (TimeMutation_test, "unit common");
}}} // namespace lib::time::test

View file

@ -0,0 +1,215 @@
/*
TimeParsing(Test) - handling textual time(code) specifications
Copyright (C) Lumiera.org
2011, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
* *****************************************************/
#include "lib/test/run.hpp"
#include "lib/test/test-helper.hpp"
#include "proc/asset/meta/time-grid.hpp"
#include "lib/time/quantiser.hpp"
#include "lib/time/timecode.hpp"
#include "lib/symbol.hpp"
#include "lib/util.hpp"
using lib::Symbol;
using util::cStr;
namespace lib {
namespace time{
namespace test{
using asset::meta::TimeGrid;
using format::LUMIERA_ERROR_INVALID_TIMECODE;
namespace { // Helper for writing test cases
Symbol DEFAULT_GRID = "pal0";
Symbol OFFSET_GRID = "pal10";
/**
* Expression builder
* for writing time value parsing tests
*/
template<class FMT>
class Parsing
{
string const& timeSpec_;
PQuant grid_;
public:
Parsing (string const& toParse, Symbol gridID =DEFAULT_GRID)
: timeSpec_(toParse)
, grid_(Quantiser::retrieve(gridID))
{ }
void
should_yield (TimeValue const& expected)
{
TimeValue parsed = FMT::parse (timeSpec_, *grid_);
CHECK (parsed == expected, "parsing '%s' resulted in %s instead of %s"
, cStr(timeSpec_)
, cStr(Time(parsed))
, cStr(Time(expected)));
}
void
should_yield (FSecs const& expectedSecs)
{
should_yield (Time (expectedSecs));
}
void
should_fail ()
{
VERIFY_ERROR (INVALID_TIMECODE, FMT::parse (timeSpec_, *grid_));
}
};
}//(End)Test case helper
/********************************************************
* @test parse textual time specifications given
* in the supported timecode formats
*/
class TimeParsing_test : public Test
{
virtual void
run (Arg)
{
defineTestTimeGrids();
parseFrames();
parseFractionalSeconds();
/////////////////////////////////////////////TODO
// parseHms();
// parseSmpte();
// parseDropFrame();
}
void
defineTestTimeGrids()
{
TimeGrid::build(DEFAULT_GRID, FrameRate::PAL);
TimeGrid::build(OFFSET_GRID, FrameRate::PAL, Time(0,10));
}
void
parseFrames ()
{
Parsing<format::Frames> ("0#") .should_yield (0);
Parsing<format::Frames> ("1#") .should_yield (FSecs(1,25) );
Parsing<format::Frames> ("-1#") .should_yield (FSecs(-1,25) );
Parsing<format::Frames> ("-0#") .should_yield (0);
Parsing<format::Frames> ("25#") .should_yield (1 );
Parsing<format::Frames> ("26#") .should_yield (Time(40,1) );
Parsing<format::Frames> ("25#", OFFSET_GRID).should_yield (1+10 );
Parsing<format::Frames> ("-1#", OFFSET_GRID).should_yield (10 - FSecs(1,25));
Parsing<format::Frames> ("23") .should_fail();
Parsing<format::Frames> ("23 #") .should_fail();
Parsing<format::Frames> ("23.#") .should_fail();
Parsing<format::Frames> ("23x#") .should_fail();
Parsing<format::Frames> ("xxx25#xxx") .should_yield (1);
Parsing<format::Frames> ("12 25#") .should_yield (1);
Parsing<format::Frames> ("12 25# 33#") .should_yield (1); // note pitfall: the first valid number is used
Parsing<format::Frames> ("12\n 25# \n 33#") .should_yield (1);
Parsing<format::Frames> ("12.25#") .should_fail(); // rejected because of leading dot (ambiguity)
}
void
parseFractionalSeconds ()
{
Parsing<format::Seconds> ("0sec") .should_yield (0);
Parsing<format::Seconds> ("1sec") .should_yield (1);
Parsing<format::Seconds> ("10sec") .should_yield (10);
Parsing<format::Seconds> ("100sec") .should_yield (100);
Parsing<format::Seconds> ("-10sec") .should_yield (-10);
Parsing<format::Seconds> ("-0sec") .should_yield (0);
Parsing<format::Seconds> ("1/2sec") .should_yield (Time(500,0) );
Parsing<format::Seconds> ("1/25sec") .should_yield (Time( 40,0) );
Parsing<format::Seconds> ("1/250sec") .should_yield (Time( 4,0) ); // no quantisation involved in parsing
Parsing<format::Seconds> ("1/250sec", OFFSET_GRID).should_yield (Time(4,10)); // ...but the origin of the grid is used
Parsing<format::Seconds> ("10/2sec") .should_yield (5);
Parsing<format::Seconds> ("1000/200sec") .should_yield (5);
Parsing<format::Seconds> ("-10/2sec") .should_yield (-5);
Parsing<format::Seconds> ("10/-2sec") .should_fail(); // only leading sign allowed (ambiguity)
Parsing<format::Seconds> ("1+1/2sec") .should_yield (Time(500,1) );
Parsing<format::Seconds> ("1-1/2sec") .should_yield (Time(500,0) );
Parsing<format::Seconds> ("-1-1/2sec") .should_yield (-Time(500,1) );
Parsing<format::Seconds> ("-1+1/2sec") .should_yield (-Time(500,0) );
Parsing<format::Seconds> ("-1+1/-2sec") .should_fail();
Parsing<format::Seconds> ("-12+24690/12345sec", OFFSET_GRID).should_yield(0); // origin=+10sec -12sec + 2/1sec == 0
Parsing<format::Seconds> ("1") .should_fail();
Parsing<format::Seconds> ("1 sec") .should_fail();
Parsing<format::Seconds> ("--1sec") .should_fail();
Parsing<format::Seconds> ("/-1sec") .should_fail();
Parsing<format::Seconds> ("1.2sec") .should_fail();
Parsing<format::Seconds> ("1/.2sec") .should_fail();
Parsing<format::Seconds> ("1 + 2 / 4 sec") .should_fail();
Parsing<format::Seconds> ("1 + 2 / 4sec") .should_yield(4); // note pitfall: leading garbage not considered
Parsing<format::Seconds> ("xxx4secxxxx") .should_yield(4);
Parsing<format::Seconds> ("x1# 8/2sec 2sec").should_yield(4); // note pitfall: first valid number used
}
void
parseHms ()
{
UNIMPLEMENTED ("verify reading hour-minutes-seconds-millis time specs");
}
void
parseSmpte ()
{
}
void
parseDropFrame ()
{
UNIMPLEMENTED ("verify especially SMPTE-drop-frame timecode");
}
};
/** Register this test class... */
LAUNCHER (TimeParsing_test, "unit common");
}}} // namespace lib::time::test

View file

@ -29,12 +29,10 @@
#include <boost/lexical_cast.hpp>
#include <iostream>
//#include <cstdlib>
#include <string>
using boost::lexical_cast;
using util::isnil;
//using std::rand;
using std::cout;
using std::endl;
using std::string;
@ -71,11 +69,12 @@ namespace test{
checkBasicTimeValues (ref);
checkMutableTime (ref);
checkTimeConveniance (ref);
checkTimeConvenience (ref);
verify_invalidFramerateProtection();
createOffsets (ref);
buildDuration (ref);
buildTimeSpan (ref);
compareTimeSpan (ref);
}
@ -159,7 +158,7 @@ namespace test{
* especially by the canonical Time values.
*/
void
checkTimeConveniance (TimeValue org)
checkTimeConvenience (TimeValue org)
{
Time o1(org);
TimeVar v(org);
@ -296,15 +295,91 @@ namespace test{
CHECK (theLength == Offset(org,five).abs());
Time endpoint = interval.end();
TimeSpan successor (endpoint, Duration(Time(0,2,0,0)));
TimeSpan successor (endpoint, FSecs(2));
CHECK (Offset(interval,endpoint) == Offset(org,five).abs());
CHECK (Offset(endpoint,successor.end()) == Duration(successor));
cout << "Interval-1: " << interval
<< " Interval-2: " << successor
cout << "Interval-1: " << interval
<< " Interval-2: " << successor
<< " End point: " << successor.end()
<< endl;
<< endl;
}
void
compareTimeSpan (TimeValue org)
{
TimeSpan span1 (Time(org), Time(org)+Time(org)); // using the distance between start and end point
TimeSpan span2 (Time(org), Offset(org, TimeValue(0))); // note: the offset is taken absolute, as Duration
TimeSpan span3 (Time(org), FSecs(5,2)); // Duration given explicitly, in seconds
TimeSpan span4 (Time(org), FSecs(5,-2)); // again: the Duration is taken absolute
CHECK (span1 == span2);
CHECK (span2 == span1);
CHECK (span3 == span4);
CHECK (span4 == span3);
CHECK (span1 != span3);
CHECK (span3 != span1);
CHECK (span1 != span4);
CHECK (span4 != span1);
CHECK (span2 != span3);
CHECK (span3 != span2);
CHECK (span2 != span4);
CHECK (span4 != span2);
// especially note that creating a TimeSpan
// based on offset will take the offset absolute
CHECK (span1.end() == span2.end());
CHECK (span3.end() == span4.end());
// Verify the extended order on time intervals
TimeSpan span1x (Time(org)+Time(org), Duration(Time(org))); // starting later than span1
TimeSpan span3y (Time(org), FSecs(2)); // shorter than span3
TimeSpan span3z (Time(org)+Time(org), FSecs(2)); // starting later and shorter than span3
CHECK (span1 != span1x);
CHECK (span3 != span3y);
CHECK (span3 != span3z);
CHECK ( span1 < span1x);
CHECK ( span1 <= span1x);
CHECK (!(span1 > span1x));
CHECK (!(span1 >= span1x));
CHECK ( span3 > span3y);
CHECK ( span3 >= span3y);
CHECK (!(span3 < span3y));
CHECK (!(span3 <= span3y));
CHECK ( span3 < span3z); // Note: the start point takes precedence on comparison
CHECK ( span3y < span3z);
// Verify this order is really different
// than the basic ordering of time values
CHECK (span1 < span1x);
CHECK (span1.duration() == span1x.duration());
CHECK (span1.start() < span1x.start());
CHECK (span1.end() < span1x.end());
CHECK (span3 > span3y);
CHECK (span3.duration() > span3y.duration());
CHECK (span3.start() == span3y.start());
CHECK (span3.end() > span3y.end());
CHECK (Time(span3) == Time(span3y));
CHECK (span3 < span3z);
CHECK (span3.duration() > span3z.duration());
CHECK (span3.start() < span3z.start());
CHECK (span3.end() != span3z.end()); // it's shorter, and org can be random, so that's all we know
CHECK (Time(span3) < Time(span3z));
CHECK (span3y < span3z);
CHECK (span3y.duration() == span3z.duration());
CHECK (span3y.start() < span3z.start());
CHECK (span3y.end() < span3z.end());
CHECK (Time(span3) < Time(span3z));
}
};

View file

@ -4281,18 +4281,18 @@ Viewed as a micro program, the processing patterns are ''weak typed'' &amp;mdash
<pre>a given Render Engine configuration is a list of Processors. Each Processor in turn contains a Graph of ProcNode.s to do the acutal data processing. In order to cary out any calculations, the Processor needs to be called with a StateProxy containing the state information for this RenderProcess
</pre>
</div>
<div title="QuantiserImpl" modifier="Ichthyostega" modified="201101081815" created="201101061212" tags="impl spec" changecount="21">
<div title="QuantiserImpl" modifier="Ichthyostega" modified="201104252333" created="201101061212" tags="impl spec" changecount="24">
<pre>The Quantiser implementation works by determining the grid interval containing a given raw time.
These grid intervals are denoted by ordinal numbers (frame numbers), with interval #0 starting at the grid's origin and negative ordinals allowed.
!frame quantisation convention
Within Lumiera, there is a fixed convention how these frame intervals are to be defined (&amp;rArr; [[time handling RfC|http://staging.lumiera.org/documentation/devel/rfc/TimeHandling.html]])
Within Lumiera, there is a fixed convention how these frame intervals are to be defined (&amp;rArr; [[time handling RfC|http://lumiera.org/documentation/devel/rfc/TimeHandling.html]])
[img[Lumiera's frame quantisation convention|draw/FramePositions1.png]]
Especially, this convention is agnostic of the actual zero-point of the scale and allows direct length calculations and seamless sequences of intervals.
The //nominal coordinate// of an interval is also the starting point -- for automation keys frames we'll utilise special provisions.
!range limitation problems
because times are represented as 64bit integers, the time points addressable within a given scale grid can be limited, compared with time points addressable through raw (internal) time values. As an extreme example, consider a time scale with origin at Time::MIN -- such a scale is unable to represent any of the original scale's value above zero, because the resulting coordinates would exceed the range of the 64bit integer. Did I mention that 64bit micro ticks can represent about 300000 years?
because times are represented as 64bit integers, the time points addressable within a given scale grid can be limited, compared with time points addressable through raw (internal) time values. As an extreme example, consider a time scale with origin at {{{Time::MAX}}} -- such a scale is unable to represent any of the original scale's value above zero, because the resulting coordinates would exceed the range of the 64bit integer. Did I mention that 64bit micro ticks can represent about 300000 years?
Now the actual problem is that using 64bit integers already means pushing to the limit. There is no easy escape hatch, like using a larger scale data type for intermediaries -- it //is// the largest built-in type. Basically we're touching the topic of ''safe integer arithmetics'' here, which is frequently discussed as a security concern. The situation is as follows:
* every programmer promises &quot;I'll do the checks when necessary&quot; -- just to forget doing so in practice then.
@ -6708,8 +6708,8 @@ Thus no server and no network connection is needed. Simply open the file in your
* see [[Homepage|http://tiddlywiki.com]], [[Wiki-Markup|http://tiddlywiki.org/wiki/TiddlyWiki_Markup]]
</pre>
</div>
<div title="TimeMutation" modifier="Ichthyostega" modified="201101232133" created="201101231344" tags="spec discuss draft" changecount="20">
<pre>Simple time points are just like values and thus easy to change. The difficulties arise when time values are to be //quantised to an existing time grid.// The first noticeable point to note is that now the effect can be decoupled from the cause. Small changes below the threshold might be accumulated, and a tiny change might trigger a jump to the next grid point. While this might be annoying, the yet more complex questions arise when we acknowledge that the change itself might be related to a time grid.
<div title="TimeMutation" modifier="Ichthyostega" modified="201104252343" created="201101231344" tags="spec discuss draft" changecount="38">
<pre>Simple time points are just like values and thus easy to change. The difficulties arise when time values are to be //quantised to an existing time grid.// The first relevant point to note is that for quantised time values, the effect of a change can be disproportional to the cause. Small changes below the threshold might be accumulated, and a tiny change might trigger a jump to the next grid point. While this might be annoying, the yet more complex questions arise when we acknowledge that the amount of the change itself might be related to a time grid.
The problem with modification of quantised values highlights an inner contradiction or conflicting goals within the design
* the whole system should fit in naturally and just feel like using raw time values
@ -6721,7 +6721,7 @@ At this point, we should recall some general assumptions within Lumiera
* there are no hard-coded defaults regarding the specific nature of the manipulated objects (format, number of channels)
* there is not &quot;the&quot; timeline, but we have multiple timelines
* sequences might be nesetd and thus be brought into a different context
* framerate is a property of the stream type of a channel; it depends on the output. Ther is no global format or framerate.
* framerate is a property of the stream type of a channel; it depends on the output. There is no global format or framerate.
!Usage situations
The complexity arises from the mixture of several concerns, which often blend into a single usage situation.
@ -6751,15 +6751,18 @@ A mutation or an increment gets applied to a grid-aligned variable, which then (
This results in the following combinations to be implemented:
| !mutation|!receiver ||!result |
| ~TimeValue|Time || -- |
|~|Duration || -- |
|~|Duration || ↯ |
|~|~TimeSpan ||set start |
|~|quantised ||set raw value |
| Offset|Time ||offset the value |
|~|Duration ||adjust by offset |
|~|~TimeSpan ||adjust start/length |
| Duration|~TimeSpan ||set length |
|~|Duration ||set length |
|~|quantised || ↯ |
| Offset|Time || -- |
|~|Duration ||adjust length |
|~|~TimeSpan ||adjust start |
|~|quantised ||adjust raw value |
| ~QuTime|any raw ||treat as raw |
|~|~QuTime ||materialise, then set raw |
| ~QuTime|any raw ||materialise, then set raw |
|~|~QuTime ||same &amp;rarr; chanined |
| increment|~QuTime ||snap to grid point |
As rationale, consider the following
* immutable values are bliss.
@ -6770,13 +6773,23 @@ As rationale, consider the following
* time values are immutable (as far as possible)
* only allow to assign a completely new setting
* only accept raw time values for redefining a quantised interval
* only accept an abstract //modification object.//
* only accept an abstract //modification object.// &amp;rarr;{{red{No! any kind of abstraction requires an indirection}}}
{{red{Question 3/11}}} -- can we avoid accepting mutations for Time (and thus only mutate ~TimeSpan)?
Rationale: allowing mutations for Time bears the danger of making ~TimeVar obsolete
* //4/11 yes, implementing it now this way....//
* we have ~TimeVar, so the only thing that //needs// to be mutable is a whole object &amp;rArr; ~TimeSpan
* err, because MObject will include a Duration separate from the start time in the Placement, Duration needs to be mutable too
!!!usage considerations
Question is: how fine grained and configurable needs this to be?
* for example, when moving a clip taken from 50fps media, the new position might be quantised to the 50fps grid established by the media, while the target timeline runs with 25fps, allowing for finer adjustments based on the intermediate frames present in the source material.
* likely we need a &quot;nudge by unit(s)&quot;
!!! design draft
The special focus of this problem seems to lead itself to a __visitor pattern__ based implementation. Because the basic hierarchy of applicable types is fixed, but the behaviour is open ended (and not yet fully determined). A conventional implementation would scatter this behaviour over all the time entities, thus making it hard to understand and reason about. The classical ~GoF cyclic visitor solution to the contrary allows us to arrange closely related behaviour into thematically grouped visitor classes. As a plus, the concrete action can be bound dynamically, allowing for more flexibility when it comes to dealing with the intricate situations when a quantised time span (= a clip) recieves a quantised mutation (= is re-alligend to a possibly different frame grid)
Thus the possibly mutalble time entities get an {{{accept(time::Mutation&amp;)}}} function, which is defined to reflect back into the (virtual) visitation functions of the Mutation Interface. Additionally, we provide some static convenience shortcuts right in the Mutation interface, performing the trivial mutations.
</pre>
</div>
<div title="TimeQuant" modifier="Ichthyostega" modified="201101232031" created="201012181753" tags="Concepts Player spec img discuss draft" changecount="50">
@ -6841,7 +6854,7 @@ At the level of individual timecode formats, we're lacking a common denominator;
&amp;rarr; Quantiser [[implementation details|QuantiserImpl]]
</pre>
</div>
<div title="TimeUsage" modifier="Ichthyostega" modified="201101231331" created="201012230204" tags="design discuss" changecount="22">
<div title="TimeUsage" modifier="Ichthyostega" modified="201105122348" created="201012230204" tags="design discuss" changecount="27">
<pre>the following collection of usage situations helps to shape the details of the time values and time quantisation design. &amp;rarr; see also [[time quantisation|TimeQuant]]
;time position of an object
@ -6878,12 +6891,15 @@ We should note, that the problems regarding quantised durations also carry over
Thus, an especially important instance of that problem is ''how to deal with updates in a quantised environment''. If we handle quantisation stictly as a view employed on output, we run into the problems with accumulating spurious information. On the other hand, allowing for quantised changes inevitably pulls in all the complexity of mixing quantised and non-quantised values. It would be desirable somehow to move these distinctions out of the scope of this design and offload them onto the client (code using these time classes).
Another closely related problem is ''when to allow mutations'', if at all. We can't completely do away with mutations, simply because we don't have a pure functional language at our disposal. The whole concept of //reference semantics// doesn't play so well with immutable objects. The Lumiera high-level (session) model certainly relies on objects intended to be //manipulated.// Thus we need a re-settable length field in {{{MObject}}} and we need a time variable for position calculations. Yet we could make any //derived objects// into immutable descriptor records, which certainly helps with parallelism.
Another closely related problem is ''when to allow [[mutations|TimeMutation]]'', if at all. We can't completely do away with mutations, simply because we don't have a pure functional language at our disposal. The whole concept of //reference semantics// doesn't play so well with immutable objects. The Lumiera high-level (session) model certainly relies on objects intended to be //manipulated.// Thus we need a re-settable length field in {{{MObject}}} and we need a time variable for position calculations. Yet we could make any //derived objects// into immutable descriptor records, which certainly helps with parallelism.
The ''problem with playback position'' is -- that it's an attempt to conceptualise a non-existing entity. There is no such thing like &quot;the&quot; playback position. Yet most applications I'm aware off employ this concept. Likely they got trapped by the metaphor of the tape head, again. We should do away with that. On playback, we should show a //projection of wall-clock time onto the expected playback range// -- not more, not less. It should be acknowledged that there is //no direct link to the ongoing playback processes,// besides the fact that they're assumed to sync to wall-clock time as well. Recall, typically there are multiple playback processes going on in compound, and each might run on a different update rate. If we really want a //visual out-of-sync indicator,// we should treat that as a separate reporting facility and display it apart of the playback cursor.
The ''problem with playback position'' is -- that indeed it's an attempt to conceptualise a non-existing entity. There is no such thing like &quot;the&quot; playback position. Yet most applications I'm aware off //do// employ this concept. Likely they got trapped by the metaphor of the tape head, again. We should do away with that. On playback, we should show a //projection of wall-clock time onto the expected playback range// -- not more, not less. It should be acknowledged that there is //no direct link to the ongoing playback processes,// besides the fact that they're assumed to sync to wall-clock time as well. Recall, typically there are multiple playback processes going on in compound, and each might run on a different update rate. If we really want a //visual out-of-sync indicator,// we should treat that as a separate reporting facility and display it apart of the playback cursor.
Note that the ''display window might be treated as just an independent instance of quantisation''. This is similar to the approach taken above for modifying quantised time span values. We should provide a special kind of time grid, the display coordinates. The origin of these is always defined to the left (lower) side of the interval to be displayed, and they are gauged in screen units (pixels or similar, as used by the GUI toolkit set). The rest is handled by the general quantisation mechanisms. The problem of aligning the display should be transformed into a general facility to align grids, and solved for the general case. Doing so solves the remaining problems with quantised value changes and with ''specifying relative placements'' as well: If we choose to represent them as quantised values, we might (or might not) also choose to apply this //grid-alignment function.//
!the complete time value usage cycle
The way time value and quantisation handling is designed in Lumiera creates a typical usage path, which actually is a one-way route. We might start out with a textual representation according to a specific ''timecode'' format. Assumed we know the implicit underlying ''time grid'' (coordinate system, framerate), this timecode string may be parsed. This brings us (back) to the very origin, which is a raw ~TimeValue (''internal time'' value). Now, this time value might be manipulated, compared to other values, combined into a ''time span'' (time point and duration -- the most basic notion of an //object// to be manipulated in the Session). Anyway, at some point these time values need to be related to some ''time scale'' again, leading to ''quantised'' time values, which -- finally -- can be cast into a timecode format for external representation again, thus closing the circle.
!substantial problems to be solved
* how to [[align multiple grids|TimeGridAlignment]]
* how to integrate [[modifications of quantised values|TimeMutation]].
@ -6891,7 +6907,7 @@ Note that the ''display window might be treated as just an independent instance
* how to design the relation of Timecode, Timecode formatting and Quantisation &amp;rarr; [[more here|TimecodeFormat]]
</pre>
</div>
<div title="TimecodeFormat" modifier="Ichthyostega" modified="201101220135" created="201101100308" tags="Player spec discuss draft" changecount="25">
<div title="TimecodeFormat" modifier="Ichthyostega" modified="201104260233" created="201101100308" tags="Player spec discuss draft" changecount="26">
<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
@ -6937,7 +6953,7 @@ But because for Lumiera the SMPTE format is just a presentation rule applied to
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.
!!{{red{WIP 1/11}}}BUG
Implementation of this strategy is still broken: it doesn't work properly when actually the change passing over the zero point happens by propagation from lower digits. Because then -- given the way the mutators are implemented -- the //new value of the wrapping digit hasn't been stored.// It seems the only sensible solution is to change the definition of the functors, so that any value will be changed by side-effect
Implementation of this strategy is still broken: it doesn't work properly when actually the change passing over the zero point happens by propagation from lower digits. Because then -- given the way the mutators are implemented -- the //new value of the wrapping digit hasn't been stored.// It seems the only sensible solution is to change the definition of the functors, so that any value will be changed by side-effect {{red{Question 4/11 -- isn't this done and fixed by now??}}}
</pre>
</div>
<div title="Timeline" modifier="Ichthyostega" modified="201011220126" created="200706250721" tags="def" changecount="21">
@ -7166,14 +7182,14 @@ For now, as of 6/10, we use specialised QueryResolver instances explicitly and d
&amp;rarr; QueryRegistration
</pre>
</div>
<div title="ViewerPlayConnection" modifier="Ichthyostega" modified="201011100202" created="201007110305" tags="Model Player spec draft" changecount="6">
<div title="ViewerPlayConnection" modifier="Ichthyostega" modified="201105110009" created="201007110305" tags="Model Player spec draft" changecount="7">
<pre>for showing output, three entities are involved
* the [[Timeline]] holds the relevant part of the model, which gets rendered for playback
* by connecting to a viewer component, an actual output target is established
* the playback process itself is coordinated by a PlayController, which in turn creates a PlayProcess
!the viewer connection
A viewer element gets connected to a given timeline either by directly attaching it, or by //allocating an available free viewer.// Anyway, as a model element, the viewer is just like another set of global pipes cascaded behind the global pipes present in the timeline. The number and kind of pipes provided is a configurable property of the viewer element &amp;mdash; more specifically: the viewer's SwitchBoard. Thus, connecting a viewer activates the same internal logic employed when connecting a sequence into a timeline or meta-clip: a default channel association is established, which can be overridden persistently (&amp;rarr; OutputMapping). Each of the viewer's pipes in turn gets connected to a system output through an OutputManager slot &amp;mdash; again an output mapping step.
A viewer element gets connected to a given timeline either by directly attaching it, or by //allocating an available free viewer.// Anyway, as a model element, the viewer is just like another set of global pipes chained up after the global pipes present in the timeline. The number and kind of pipes provided is a configurable property of the viewer element &amp;mdash; more specifically: the viewer's SwitchBoard. Thus, connecting a viewer activates the same internal logic employed when connecting a sequence into a timeline or meta-clip: a default channel association is established, which can be overridden persistently (&amp;rarr; OutputMapping). Each of the viewer's pipes in turn gets connected to a system output through an OutputManager slot &amp;mdash; again an output mapping step.
</pre>
</div>
<div title="VirtualClip" modifier="Ichthyostega" modified="201011220152" created="200804110321" tags="def Model" changecount="17">