ardour/libs/temporal/timeline.cc

925 lines
21 KiB
C++

/*
Copyright (C) 2017 Paul Davis
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 <cstdlib>
#include <exception>
#include <sstream>
#include "pbd/enumwriter.h"
#include "pbd/error.h"
#include "pbd/compose.h"
#include "temporal/debug.h"
#include "temporal/timeline.h"
#include "temporal/tempo.h"
#include "pbd/i18n.h"
using namespace PBD;
using namespace Temporal;
struct TemporalStatistics
{
int64_t audio_to_beats;
int64_t audio_to_bars;
int64_t beats_to_audio;
int64_t beats_to_bars;
int64_t bars_to_audio;
int64_t bars_to_beats;
TemporalStatistics ()
: audio_to_beats (0)
, audio_to_bars (0)
, beats_to_audio (0)
, beats_to_bars (0)
, bars_to_audio (0)
, bars_to_beats (0)
{}
void dump (std::ostream & str) {
str << "TemporalStatistics\n"
<< "Audio => Beats " << audio_to_beats << ' '
<< "Audio => Bars " << audio_to_bars << ' '
<< "Beats => Audio " << beats_to_audio << ' '
<< "Beats => Bars " << beats_to_bars << ' '
<< "Bars => Audio " << bars_to_audio << ' '
<< "Bars => Beats " << bars_to_beats
<< std::endl;
}
};
static TemporalStatistics stats;
void Temporal::dump_stats (std::ostream& o) { stats.dump (o); }
/* timecnt */
timecnt_t timecnt_t::_max_timecnt (timecnt_t::from_superclock (int62_t::max - 1));
timecnt_t::timecnt_t (timecnt_t const & tc, timepos_t const & pos)
: _position (pos)
{
if (tc.distance() < 0) {
throw std::domain_error (X_("negative distance in timecnt constructor"));
}
_distance = tc.distance();
}
timecnt_t::timecnt_t (samplepos_t s, timepos_t const & pos)
: _position (pos)
{
assert (_position.time_domain() == AudioTime);
if (s == max_samplepos) {
_distance = int62_t (false, int62_t::max);
} else {
_distance = int62_t (false, samples_to_superclock (s, TEMPORAL_SAMPLE_RATE));
}
}
timecnt_t::timecnt_t (samplepos_t s)
: _position (AudioTime)
{
if (s == max_samplepos) {
_distance = int62_t (false, int62_t::max);
} else {
_distance = int62_t (false, samples_to_superclock (s, TEMPORAL_SAMPLE_RATE));
}
}
timepos_t
timecnt_t::end (TimeDomain return_domain) const
{
if (_distance.flagged() && _position.time_domain() == BeatTime && return_domain == BeatTime) {
/* everything in BeatTime, so just add */
return timepos_t (_position.beats() + Beats::ticks (magnitude()));
}
if (!_distance.flagged() && _position.time_domain() == AudioTime && return_domain == AudioTime) {
/* everything in AudioTime, so just add */
return timepos_t::from_superclock (_position.superclocks() + magnitude());
}
if (_distance.flagged()) { /* _distance in beats */
if (_position.time_domain() == BeatTime) {
/* distance & position in beats, so return must be audio (all 3 as beats is handled above) */
return timepos_t::from_superclock (TempoMap::use()->superclock_at ( _position.beats() + Beats::ticks (magnitude())));
} else if (_position.time_domain() == AudioTime) {
const Beats b = TempoMap::use()->quarters_at_superclock (_position.superclocks()) + Beats::ticks (magnitude());
if (return_domain == BeatTime) {
return timepos_t (b);
} else {
return timepos_t::from_superclock (TempoMap::use()->superclock_at (b));
}
}
} /* else _distance in audio time */
if (_position.time_domain() == AudioTime) {
/* distance & position in audio, so return must be beats (all 3 as audio is handled above) */
return timepos_t (TempoMap::use()->quarters_at_superclock (_position.superclocks() + magnitude()));
} /* else if (_position.time_domain() == BeatTime) { */
const superclock_t sc = TempoMap::use()->superclock_at (_position.beats()) + magnitude();
if (return_domain == AudioTime) {
return timepos_t::from_superclock (sc);
}
return timepos_t (TempoMap::use()->quarters_at_superclock (sc));
}
void
timecnt_t::set_time_domain (TimeDomain td)
{
if (time_domain() == td) {
return;
}
_position.set_time_domain (td);
if (_distance.flagged()) {
/* beats -> superclock */
_distance = int62_t (false, TempoMap::use()->superclock_at (Beats::ticks (magnitude())));
} else {
/* superclock -> beats */
_distance = int62_t (true, TempoMap::use()->quarters_at_superclock (magnitude()).to_ticks());
}
}
void
timecnt_t::set_position (timepos_t const & pos)
{
_position = pos;
}
timecnt_t
timecnt_t::abs () const
{
return timecnt_t (_distance.abs(), _position);
}
superclock_t
timecnt_t::compute_superclocks() const
{
assert (_distance.flagged());
TempoMap::SharedPtr tm (TempoMap::use());
return tm->convert_duration (*this, _position, AudioTime).superclocks();
}
Beats
timecnt_t::compute_beats() const
{
assert (!_distance.flagged());
return TempoMap::use()->convert_duration (*this, _position, BeatTime).beats();
}
timecnt_t
timecnt_t::scale (ratio_t const & r) const
{
if (time_domain() == AudioTime) {
return timecnt_t::from_superclock (PBD::muldiv_round (_distance.val(), r.numerator(), r.denominator()), _position);
} else {
return timecnt_t::from_ticks (PBD::muldiv_round (_distance.val(), r.numerator(), r.denominator()), _position);
}
}
ratio_t
timecnt_t::operator/ (timecnt_t const & other) const
{
if (time_domain() == other.time_domain()) {
return ratio_t (distance().val(), other.distance().val());
}
if (time_domain() == AudioTime) {
return ratio_t (distance().val(), other.superclocks());
}
return ratio_t (beats().to_ticks(), other.beats().to_ticks());
}
timecnt_t
timecnt_t::operator% (timecnt_t const & d) const
{
if (time_domain() == d.time_domain()) {
return timecnt_t (_distance % d.distance(), _position);
} else if (time_domain() == AudioTime) {
timecnt_t dd = d;
dd.set_time_domain (AudioTime);
return timecnt_t (_distance % dd.distance(), _position);
} else {
assert (0); // This path should never be used because..
timecnt_t self = *this;
self.set_time_domain (AudioTime);
timecnt_t rv (self.distance() % d.distance(), _position);
rv.set_time_domain (BeatTime); // it looses precision
return rv;
}
}
timecnt_t &
timecnt_t::operator%= (timecnt_t const & d)
{
if (time_domain() != d.time_domain()) {
assert (0); // TODO FIXME
}
_distance %= d.distance();
return *this;
}
bool
timecnt_t::string_to (std::string const & str)
{
superclock_t s;
samplecnt_t sm;
int64_t ticks;
Beats beats;
char sep;
if (isdigit (str[0])) {
/* old school position format: we assume samples */
std::stringstream ss (str);
ss >> sm;
_distance = int62_t (false, samples_to_superclock (sm, TEMPORAL_SAMPLE_RATE));
_position = timepos_t (AudioTime);
// std::cerr << "deserialized timecnt from older " << str << " as " << *this << std::endl;
return true;
}
std::stringstream ss (str.substr (1));
switch (str[0]) {
case 'a':
ss >> s;
_distance = int62_t (false, s);
break;
case 'b':
ss >> ticks;
_distance = int62_t (true, ticks);
break;
default:
return false;
}
/* eat separator character */
ss >> sep;
/* grab what's left, generate a new string and parse _position with it */
std::string remaining;
ss >> remaining;
_position.string_to (remaining);
return true;
}
std::string
timecnt_t::str () const
{
std::stringstream ss;
if (_distance.flagged()) {
ss << 'b';
} else {
ss << 'a';
}
ss << _distance.val();
/* add a separator. character doesn't matter as long as it will never be
parsed as part of a numerical value. Using '@' makes it "read
nicely" e.g. "3 beats at superclock 28229992292"
*/
ss << '@';
ss << _position.str();
return ss.str();
}
timecnt_t
timecnt_t::operator+ (timecnt_t const & other) const
{
if (time_domain() == other.time_domain()) {
int62_t v (_distance.flagged(), _distance.val() + other.distance().val());
return timecnt_t (v, _position);
}
/* mismatched time domains */
if (time_domain() == AudioTime) {
/* other must be beats */
return timecnt_t (_distance + other.superclocks(), _position);
}
return timecnt_t (beats() + other.beats(), _position);
}
timecnt_t
timecnt_t::operator- (timecnt_t const & other) const
{
if (time_domain() == AudioTime) {
if (other.time_domain() == AudioTime) {
return timecnt_t (_distance - other.distance(), _position);
} else {
return timecnt_t (_distance - other.superclocks(), _position);
}
}
return timecnt_t (beats() - other.beats(), _position);
}
timecnt_t &
timecnt_t::operator+= (timecnt_t const & other)
{
if (time_domain() == AudioTime) {
if (other.time_domain() == AudioTime) {
_distance += other.distance();
} else {
_distance += other.superclocks();
}
} else {
_distance += other.ticks ();
}
return *this;
}
timecnt_t
timecnt_t::operator+ (timepos_t const & other) const
{
if (time_domain() == AudioTime) {
if (other.time_domain() == AudioTime) {
/* both audio, just add and use an arbitrary position */
return timecnt_t (_distance + other.val(), _position);
} else {
return timecnt_t (_distance + other.superclocks(), _position);
}
}
return timecnt_t (beats() + other.beats(), _position);
}
timecnt_t
timecnt_t::operator- (timepos_t const & other) const
{
if (time_domain() == AudioTime) {
if (other.time_domain() == AudioTime) {
return timecnt_t (_distance - other.val(), _position);
} else {
return timecnt_t (_distance - other.superclocks(), _position);
}
}
return timecnt_t (beats() - other.beats(), _position);
}
timecnt_t &
timecnt_t::operator-= (timecnt_t const & other)
{
if (time_domain() == other.time_domain()) {
_distance -= other.distance();
} else if (time_domain() == AudioTime) {
_distance -= other.superclocks();
} else {
_distance -= other.ticks ();
}
return *this;
}
timecnt_t
timecnt_t::operator- () const
{
return timecnt_t (-_distance, _position);
}
bool
timecnt_t::expensive_lt (timecnt_t const & other) const
{
if (!_distance.flagged()) { /* Audio */
return _distance.val() < other.superclocks();
}
return Beats::ticks (_distance.val()) < other.beats ();
}
bool
timecnt_t::expensive_gt (timecnt_t const & other) const
{
if (!_distance.flagged()) { /* Audio */
return _distance.val() > other.superclocks();
}
return Beats::ticks (_distance.val()) > other.beats ();
}
bool
timecnt_t::expensive_lte (timecnt_t const & other) const
{
if (!_distance.flagged()) { /* Audio */
return _distance.val() <= other.superclocks();
}
return Beats::ticks (_distance.val()) <= other.beats ();
}
bool
timecnt_t::expensive_gte (timecnt_t const & other) const
{
if (time_domain() == AudioTime) {
return _distance.val() >= other.superclocks();
}
return Beats::ticks (_distance.val()) >= other.beats ();
}
std::ostream&
std::operator<< (std::ostream & o, timecnt_t const & tc)
{
return o << tc.str();
}
std::istream&
std::operator>> (std::istream & o, timecnt_t & tc)
{
std::string str;
o >> str; /* will break at whitespace */
tc.string_to (str);
return o;
}
/* timepos */
timepos_t::timepos_t (timecnt_t const & t)
{
v = build (t.distance().flagged(), t.distance ().val());
}
timepos_t::timepos_t (samplepos_t s)
{
if (s == max_samplepos) {
v = build (false, int62_t::max);
} else {
v = build (false, samples_to_superclock (s, TEMPORAL_SAMPLE_RATE));
}
}
void
timepos_t::set_time_domain (TimeDomain td)
{
if (td == time_domain()) {
return;
}
if (td == AudioTime) {
v = build (false, _superclocks());
} else {
v = build (true, _beats().to_ticks());
}
}
// timepos_t timepos_t::_max_timepos (Temporal::AudioTime);
timepos_t &
timepos_t::operator= (timecnt_t const & t)
{
v = build (t.distance().flagged(), t.distance().val());
return *this;
}
bool
timepos_t::operator< (timecnt_t const & t) const
{
if (time_domain() == AudioTime) {
return superclocks() < t.superclocks();
}
return beats() < t.beats ();
}
bool
timepos_t::operator> (timecnt_t const & t) const
{
if (time_domain() == AudioTime) {
return superclocks() > t.superclocks();
}
return beats() > t.beats ();
}
bool
timepos_t::operator<= (timecnt_t const & t) const
{
if (time_domain() == AudioTime) {
return superclocks() <= t.superclocks();
}
return beats() <= t.beats ();
}
bool
timepos_t::operator>= (timecnt_t const & t) const
{
if (time_domain() == AudioTime) {
return superclocks() >= t.superclocks();
}
return beats() >= t.beats ();
}
superclock_t
timepos_t::_superclocks () const
{
assert (time_domain() == BeatTime);
stats.beats_to_audio++;
return TempoMap::use()->superclock_at (beats ());
}
Temporal::Beats
timepos_t::_beats () const
{
assert (time_domain() == AudioTime);
stats.audio_to_beats++;
/* see notes in Temporal::TempoPoint::quarters_at_superclock() for
* more. Basically, specially case "max-superclocks" and return
* "max-beats"
*/
if (val() == int62_t::max) {
return std::numeric_limits<Beats>::max ();
}
return TempoMap::use()->quarters_at_superclock (v);
}
int64_t
timepos_t::_ticks () const
{
assert (time_domain() == AudioTime);
return _beats().to_ticks();
}
timepos_t
timepos_t::scale (ratio_t const & n) const
{
if (time_domain() == AudioTime) {
return timepos_t::from_superclock (PBD::muldiv_round (val(), n.numerator(), n.denominator()));
} else {
return timepos_t::from_ticks (PBD::muldiv_round (val(), n.numerator(), n.denominator()));
}
}
timepos_t
timepos_t::expensive_add (timepos_t const & other) const
{
/* Called when other's time domain does not match our own, requiring us
to call either ::beats() or ::superclocks() on other to convert it to
our time domain.
*/
assert (is_beats() != other.is_beats ());
if (is_beats()) {
/* we are known to use music time, so val() is in ticks */
return timepos_t::from_ticks (val() + other.ticks());
}
/* we are known to use audio time, so val() is in superclocks */
return timepos_t::from_superclock (val() + other.superclocks());
}
/* */
/* ::distance() assumes that @p other is later on the timeline than this, and
* thus returns a positive value if this condition is satisfied.
*/
timecnt_t
timepos_t::distance (timepos_t const & other) const
{
if (time_domain() == other.time_domain()) {
// std::cerr << "\ncomputing distance in " << enum_2_string (time_domain()) << std::endl;
return timecnt_t (int62_t (is_beats(), other.val() - val()), *this);
}
// std::cerr << "\ncomputing distance on " << enum_2_string (time_domain()) << " w/other = " << enum_2_string (other.time_domain()) << std::endl;
return expensive_distance (other);
}
timecnt_t
timepos_t::expensive_distance (timepos_t const & other) const
{
/* Called when other's time domain does not match our own, requiring us
to call either ::beats() or ::superclocks() on other to convert it to
our time domain.
*/
assert (is_beats() != other.is_beats ());
if (is_beats()) {
/* we are known to use beat time: val() is ticks */
return timecnt_t::from_ticks (other.ticks() - val(), *this);
}
/* we known to be audio: val() is superclocks */
// std::cerr << "other " << other << " SC = " << other.superclocks() << " vs. us @ " << val() << std::endl;
return timecnt_t::from_superclock (other.superclocks() - val(), *this);
}
/* */
timepos_t
timepos_t::earlier (Temporal::BBT_Offset const & offset) const
{
TempoMap::SharedPtr tm (TempoMap::use());
if (is_superclock()) {
return timepos_t (tm->superclock_at (BBT_Argument (this->superclocks(), tm->bbt_walk (BBT_Argument (this->superclocks(), tm->bbt_at (*this)), -offset))));
}
return timepos_t (tm->bbtwalk_to_quarters (beats(), -offset));
}
timepos_t
timepos_t::earlier (timepos_t const & other) const
{
if (is_superclock()) {
return timepos_t::from_superclock (val() - other.superclocks());
}
return timepos_t::from_ticks (val() - other.ticks());
}
timepos_t
timepos_t::earlier (timecnt_t const & distance) const
{
if (is_superclock()) {
return timepos_t::from_superclock (val() - distance.superclocks());
}
return timepos_t::from_ticks (val() - distance.ticks());
}
bool
timepos_t::expensive_lt (timepos_t const & other) const
{
if (time_domain() == AudioTime) {
return val() < other.superclocks();
}
return ticks() < other.ticks ();
}
bool
timepos_t::expensive_gt (timepos_t const & other) const
{
if (time_domain() == AudioTime) {
return superclocks() > other.superclocks();
}
return beats() > other.beats ();
}
bool
timepos_t::expensive_lte (timepos_t const & other) const
{
if (time_domain() == AudioTime) {
return superclocks() <= other.superclocks();
}
return beats() <= other.beats ();
}
bool
timepos_t::expensive_gte (timepos_t const & other) const
{
if (time_domain() == AudioTime) {
return superclocks() >= other.superclocks();
}
return beats() >= other.beats ();
}
/* */
timepos_t &
timepos_t::shift_earlier (timepos_t const & d)
{
if (is_superclock()) {
v = build (false, val() - d.superclocks());
} else {
v = build (true, val() - d.ticks());
}
return *this;
}
timepos_t &
timepos_t::shift_earlier (timecnt_t const & d)
{
if (is_superclock()) {
v = build (false, val() - d.superclocks());
} else {
v = build (true, val() - d.ticks());
}
return *this;
}
timepos_t &
timepos_t::shift_earlier (Temporal::BBT_Offset const & offset)
{
TempoMap::SharedPtr tm (TempoMap::use());
if (is_superclock()) {
v = build (false, (tm->superclock_at (tm->bbt_walk (BBT_Argument (this->superclocks(), tm->bbt_at (*this)), -offset))));
} else {
v = build (true, tm->bbtwalk_to_quarters (beats(), -offset).to_ticks());
}
return *this;
}
/* */
timepos_t &
timepos_t::operator+= (Temporal::BBT_Offset const & offset)
{
TempoMap::SharedPtr tm (TempoMap::use());
if (is_beats()) {
v = build (true, tm->bbtwalk_to_quarters (beats(), offset).to_ticks());
} else {
v = build (false, tm->superclock_at (tm->bbt_walk (BBT_Argument (this->superclocks(), tm->bbt_at (*this)), offset)));
}
return *this;
}
/* */
timepos_t
timepos_t::operator+(timecnt_t const & d) const
{
if (d.time_domain() == time_domain()) {
if (time_domain() == AudioTime) {
return operator+ (timepos_t::from_superclock (d.superclocks()));
} else {
return operator+ (timepos_t::from_ticks (d.ticks()));
}
}
TempoMap::SharedPtr tm (TempoMap::use());
timecnt_t dur_at_this = tm->convert_duration (d, *this, time_domain());
assert (dur_at_this.time_domain() == time_domain());
return operator+ (dur_at_this);
}
timepos_t &
timepos_t::operator+=(timecnt_t const & d)
{
if (d.time_domain() == time_domain()) {
if (time_domain() == AudioTime) {
return operator+= (timepos_t::from_superclock (d.superclocks()));
} else {
return operator+= (timepos_t::from_ticks (d.ticks()));
}
}
TempoMap::SharedPtr tm (TempoMap::use());
timecnt_t dur_at_this = tm->convert_duration (d, *this, time_domain());
assert (dur_at_this.time_domain() == time_domain());
return operator+= (dur_at_this);
}
/* */
timepos_t &
timepos_t::operator+=(timepos_t const & d)
{
if (d.is_beats() == is_beats()) {
/* same time domain, keep flag bit, add values */
v = build (flagged(), val() + d.val());
} else {
/* different time domain, return a value in the same domain as
* this one
*/
if (is_beats()) {
v = build (true, val() + d.ticks());
} else {
v = build (false, val() + d.superclocks());
}
}
return *this;
}
std::ostream&
std::operator<< (std::ostream & o, timepos_t const & tp)
{
return o << tp.str();
}
std::istream&
std::operator>> (std::istream & o, timepos_t & tp)
{
std::string str;
o >> str; /* should break on whitespace */
tp.string_to (str);
return o;
}
std::string
timepos_t::str () const
{
if (is_beats()) {
return string_compose ("b%1", val());
}
return string_compose ("a%1", val());
}
bool
timepos_t::string_to (std::string const & str)
{
using std::string;
using std::cerr;
using std::endl;
superclock_t s;
samplepos_t sm;
int64_t ticks;
Beats beats;
if (isdigit (str[0]) || (str[0] == '-' && str.length() > 1)) {
/* old school position format: we assume samples */
std::stringstream ss (str);
ss >> sm;
v = build (false, samples_to_superclock (sm, TEMPORAL_SAMPLE_RATE));
// cerr << "deserialized timepos from older " << str << " as " << *this << " with sm = " << sm << " and sr = " << TEMPORAL_SAMPLE_RATE << " s2sc " << endl;
return true;
}
std::stringstream ss (str.substr (1));
switch (str[0]) {
case 'a':
ss >> s;
v = build (false, s);
// cerr << "deserialized timepos from " << str << " as " << *this << endl;
return true;
case 'b':
ss >> ticks;
v = build (true, ticks);
// cerr << "deserialized timepos from " << str << " as " << *this << endl;
return true;
}
std::cerr << "Unknown timepos string representation \"" << str << "\"" << std::endl;
return false;
}