update Temporal::Beats to merge master/nutempo versions, notably with private from-double constructor

This is the libraries-only edition. It still features liberal use of Beats::from_double() but this is now
explicit and will be easier to locate the calls and remove them. Several classes that were using
Beats::to_double() have been (temporarily) made friends of Beats to allow them to keep using it,
pending the much more widespread redesigns of several structures. Once this is done, the friend
relationships can (mostly) be removed. It is expected the ARDOUR::Variant will need to continue
as a friend because it is used to pass beat counts to LV2 as doubles
This commit is contained in:
Paul Davis 2020-07-13 18:02:57 -06:00
parent ecf2028c7a
commit 4dc048b28a
16 changed files with 299 additions and 97 deletions

View File

@ -44,7 +44,7 @@
namespace ARDOUR {
class AutomationList;
class DoubleBeatsSamplesConverter;
class BeatsSamplesConverter;
/** A SharedStatefulProperty for AutomationLists */
class LIBARDOUR_API AutomationListProperty : public PBD::SharedStatefulProperty<AutomationList>
@ -86,7 +86,7 @@ public:
AutomationList& operator= (const AutomationList&);
void thaw ();
bool paste (const ControlList&, double, DoubleBeatsSamplesConverter const&);
bool paste (const ControlList&, double, BeatsSamplesConverter const&);
void set_automation_state (AutoState);
AutoState automation_state() const;

View File

@ -16,6 +16,8 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <cstdlib>
#include "temporal/beats.h"
#include "evoral/TimeConverter.h"
@ -67,6 +69,7 @@ private:
const TempoMap& _tempo_map;
};
} /* namespace ARDOUR */
#endif /* __ardour_beats_samples_converter_h__ */

View File

@ -29,39 +29,6 @@ namespace PBD {
DEFINE_ENUM_CONVERT(Evoral::ControlList::InterpolationStyle)
template <>
inline bool to_string (Temporal::Beats beats, std::string& str)
{
return double_to_string (beats.to_double (), str);
}
template <>
inline bool string_to (const std::string& str, Temporal::Beats& beats)
{
double tmp;
if (!string_to_double (str, tmp)) {
return false;
}
beats = Temporal::Beats(tmp);
return true;
}
template <>
inline std::string to_string (Temporal::Beats beats)
{
std::string tmp;
double_to_string (beats.to_double (), tmp);
return tmp;
}
template <>
inline Temporal::Beats string_to (const std::string& str)
{
double tmp;
string_to_double (str, tmp);
return Temporal::Beats (tmp);
}
} // namespace PBD
#endif // ARDOUR_EVORAL_TYPES_CONVERT_H

View File

@ -30,7 +30,7 @@ class LIBARDOUR_API Quantize : public MidiOperator {
public:
Quantize (bool snap_start, bool snap_end,
double start_grid, double end_grid,
float strength, float swing, float threshold);
float strength, float swing, Temporal::Beats const & threshold);
~Quantize ();
Command* operator() (boost::shared_ptr<ARDOUR::MidiModel>,
@ -45,7 +45,7 @@ private:
double _end_grid;
float _strength;
float _swing;
float _threshold;
Temporal::Beats _threshold;
};
} /* namespace */

View File

@ -315,7 +315,7 @@ AutomationList::thaw ()
}
bool
AutomationList::paste (const ControlList& alist, double pos, DoubleBeatsSamplesConverter const& bfc)
AutomationList::paste (const ControlList& alist, double pos, BeatsSamplesConverter const& bfc)
{
AutomationType src_type = (AutomationType)alist.parameter().type();
AutomationType dst_type = (AutomationType)_parameter.type();
@ -330,9 +330,9 @@ AutomationList::paste (const ControlList& alist, double pos, DoubleBeatsSamplesC
for (const_iterator i = alist.begin ();i != alist.end (); ++i) {
double when = (*i)->when;
if (to_sample) {
when = bfc.to ((*i)->when);
when = bfc.to (Temporal::Beats::from_double ((*i)->when));
} else {
when = bfc.from ((*i)->when);
when = bfc.from ((*i)->when).to_double ();
}
cl.fast_simple_add (when, (*i)->value);
}

View File

@ -453,8 +453,8 @@ no_audio_tracks:
for (vector<PTFFormat::midi_ev_t>::const_iterator j = a->reg.midi.begin (); j != a->reg.midi.end (); ++j) {
//printf(" : MIDI : pos=%f len=%f\n", (float)j->pos / 960000., (float)j->length / 960000.);
Temporal::Beats start = (Temporal::Beats)(j->pos / 960000.);
Temporal::Beats len = (Temporal::Beats)(j->length / 960000.);
Temporal::Beats start = Temporal::Beats::from_double (j->pos / 960000.);
Temporal::Beats len = Temporal::Beats::from_double(j->length / 960000.);
/* PT C-2 = 0, Ardour C-1 = 0, subtract twelve to convert ? */
midicmd->add (boost::shared_ptr<Evoral::Note<Temporal::Beats> > (new Evoral::Note<Temporal::Beats> ((uint8_t)1, start, len, j->note, j->velocity)));
}

View File

@ -43,7 +43,7 @@ Legatize::operator()(boost::shared_ptr<ARDOUR::MidiModel> model,
break;
}
const Temporal::Beats new_end = (*next)->time() - Temporal::Beats::tick();
const Temporal::Beats new_end = (*next)->time() - Temporal::Beats::one_tick();
if ((*i)->end_time() > new_end ||
(!_shrink_only && (*i)->end_time() < new_end)) {
const Temporal::Beats new_length(new_end - (*i)->time());

View File

@ -200,7 +200,6 @@ CLASSKEYS(std::list<Selectable*>);
CLASSKEYS(ARDOUR::AudioEngine);
CLASSKEYS(ARDOUR::BeatsSamplesConverter);
CLASSKEYS(ARDOUR::DoubleBeatsSamplesConverter);
CLASSKEYS(ARDOUR::BufferSet);
CLASSKEYS(ARDOUR::ChanCount);
CLASSKEYS(ARDOUR::ChanMapping);
@ -557,8 +556,7 @@ LuaBindings::common (lua_State* L)
.endClass ()
.beginClass <Temporal::Beats> ("Beats")
.addConstructor <void (*) (double)> ()
.addFunction ("to_double", &Temporal::Beats::to_double)
/* XXX need some way to construct beats in Lua */
.endClass ()
.beginClass <Evoral::Parameter> ("Parameter")
@ -1905,12 +1903,6 @@ LuaBindings::common (lua_State* L)
.addFunction ("from", &BeatsSamplesConverter::from)
.endClass ()
.beginClass <DoubleBeatsSamplesConverter> ("DoubleBeatsSamplesConverter")
.addConstructor <void (*) (const TempoMap&, samplepos_t)> ()
.addFunction ("to", &DoubleBeatsSamplesConverter::to)
.addFunction ("from", &DoubleBeatsSamplesConverter::from)
.endClass ()
.beginClass <TempoMap> ("TempoMap")
.addFunction ("add_tempo", &TempoMap::add_tempo)
.addFunction ("add_meter", &TempoMap::add_meter)

View File

@ -1776,6 +1776,15 @@ LV2Plugin::write_to_ui(uint32_t index,
}
return true;
}
/* this used to be computed by Temporal::Beats::to_double() but that
* method has been hidden as of February 2017 to prevent inadvertent
* use of floating point musical time.
*/
inline static double
beats_to_double (Temporal::Beats const & b)
{
return (double) b.get_beats() + (b.get_ticks() / (double) Temporal::ticks_per_beat);
}
static void
forge_variant(LV2_Atom_Forge* forge, const Variant& value)
@ -1785,7 +1794,7 @@ forge_variant(LV2_Atom_Forge* forge, const Variant& value)
break;
case Variant::BEATS:
// No atom type for this, just forge a double
lv2_atom_forge_double(forge, value.get_beats().to_double());
lv2_atom_forge_double(forge, beats_to_double (value.get_beats()));
break;
case Variant::BOOL:
lv2_atom_forge_bool(forge, value.get_bool());

View File

@ -66,7 +66,6 @@ using namespace PBD;
MidiSource::MidiSource (Session& s, string name, Source::Flag flags)
: Source(s, DataType::MIDI, name, flags)
, _writing(false)
, _length_beats(0.0)
, _capture_length(0)
, _capture_loop_length(0)
{
@ -75,7 +74,6 @@ MidiSource::MidiSource (Session& s, string name, Source::Flag flags)
MidiSource::MidiSource (Session& s, const XMLNode& node)
: Source(s, node)
, _writing(false)
, _length_beats(0.0)
, _capture_length(0)
, _capture_loop_length(0)
{

View File

@ -191,7 +191,7 @@ MidiStateTracker::resolve_notes (MidiSource& src, const MidiSource::Lock& lock,
this, (int) note, (int) channel, time));
_active_notes[note + 128 * channel]--;
/* don't stack events up at the same time */
time += Temporal::Beats::tick();
time += Temporal::Beats::one_tick();
}
}
}

View File

@ -452,7 +452,7 @@ MidiTrack::non_realtime_locate (samplepos_t pos)
(rcontrol = region->control(tcontrol->parameter()))) {
const Temporal::Beats pos_beats = bfc.from(pos - origin);
if (rcontrol->list()->size() > 0) {
tcontrol->set_value(rcontrol->list()->eval(pos_beats.to_double()), Controllable::NoGroup);
tcontrol->set_value(rcontrol->list()->eval(pos_beats), Controllable::NoGroup);
}
}
}

View File

@ -39,7 +39,7 @@ using namespace ARDOUR;
Quantize::Quantize (bool snap_start, bool snap_end,
double start_grid, double end_grid,
float strength, float swing, float threshold)
float strength, float swing, Temporal::Beats const & threshold)
: _snap_start (snap_start)
, _snap_end (snap_end)
, _start_grid(start_grid)
@ -122,8 +122,8 @@ Quantize::operator () (boost::shared_ptr<MidiModel> model,
to quantize relative to actual session beats (etc.) rather than from the
start of the model.
*/
const double round_pos = round(position.to_double() / _start_grid) * _start_grid;
const double offset = round_pos - position.to_double();
const double round_pos = (position / _start_grid) * _start_grid;
const double offset = round_pos - position;
MidiModel::NoteDiffCommand* cmd = new MidiModel::NoteDiffCommand (model, "quantize");
@ -138,8 +138,8 @@ Quantize::operator () (boost::shared_ptr<MidiModel> model,
* guaranteed to precisely align with the quantize grid(s).
*/
double new_start = round (((*i)->time().to_double() - offset) / _start_grid) * _start_grid;
double new_end = round (((*i)->end_time().to_double() - offset) / _end_grid) * _end_grid;
Temporal::Beats new_start = (((*i)->time() - offset) / _start_grid) * _start_grid;
Temporal::Beats new_end = (((*i)->end_time() - offset) / _end_grid) * _end_grid;
if (_swing) {
@ -154,25 +154,25 @@ Quantize::operator () (boost::shared_ptr<MidiModel> model,
new_end += offset;
}
double delta = new_start - (*i)->time().to_double();
Temporal::Beats delta = new_start - (*i)->time();
if (fabs (delta) >= _threshold) {
if (delta.abs() >= _threshold) {
if (_snap_start) {
delta *= _strength;
delta = delta * _strength;
cmd->change ((*i), MidiModel::NoteDiffCommand::StartTime,
(*i)->time() + delta);
}
}
if (_snap_end) {
delta = new_end - (*i)->end_time().to_double();
delta = new_end - (*i)->end_time();
if (delta.abs() >= _threshold) {
if (fabs (delta) >= _threshold) {
Temporal::Beats new_dur(new_end - new_start);
if (!new_dur) {
new_dur = Temporal::Beats(_end_grid);
new_dur = Temporal::Beats::from_double (_end_grid);
}
cmd->change ((*i), MidiModel::NoteDiffCommand::Length, new_dur);

View File

@ -67,7 +67,6 @@ SMFSource::SMFSource (Session& s, const string& path, Source::Flag flags)
, FileSource(s, DataType::MIDI, path, string(), flags)
, Evoral::SMF()
, _open (false)
, _last_ev_time_beats(0.0)
, _last_ev_time_samples(0)
, _smf_last_read_end (0)
, _smf_last_read_time (0)
@ -103,7 +102,6 @@ SMFSource::SMFSource (Session& s, const string& path)
, FileSource(s, DataType::MIDI, path, string(), Source::Flag (0))
, Evoral::SMF()
, _open (false)
, _last_ev_time_beats(0.0)
, _last_ev_time_samples(0)
, _smf_last_read_end (0)
, _smf_last_read_time (0)
@ -135,7 +133,6 @@ SMFSource::SMFSource (Session& s, const XMLNode& node, bool must_exist)
, MidiSource(s, node)
, FileSource(s, node, must_exist)
, _open (false)
, _last_ev_time_beats(0.0)
, _last_ev_time_samples(0)
, _smf_last_read_end (0)
, _smf_last_read_time (0)
@ -419,7 +416,7 @@ SMFSource::append_event_beats (const Glib::Threads::Mutex::Lock& lock,
Temporal::Beats time = ev.time();
if (time < _last_ev_time_beats) {
const Temporal::Beats difference = _last_ev_time_beats - time;
if (difference.to_double() / (double)ppqn() < 1.0) {
if (difference < Temporal::Beats::ticks (ppqn())) {
/* Close enough. This problem occurs because Sequence is not
actually ordered due to fuzzy time comparison. I'm pretty sure
this is inherently a bad idea which causes problems all over the
@ -428,7 +425,7 @@ SMFSource::append_event_beats (const Glib::Threads::Mutex::Lock& lock,
} else {
/* Out of order by more than a tick. */
warning << string_compose(_("Skipping event with unordered beat time %1 < %2 (off by %3 beats, %4 ticks)"),
ev.time(), _last_ev_time_beats, difference, difference.to_double() / (double)ppqn())
ev.time(), _last_ev_time_beats, difference, difference)
<< endmsg;
return;
}

View File

@ -28,14 +28,44 @@
#include <iostream>
#include <limits>
#include "pbd/compose.h"
#include "pbd/failed_constructor.h"
#include "pbd/string_convert.h"
#include "temporal/visibility.h"
#include "temporal/types.h"
namespace ARDOUR {
class Variant; /* Can stay since LV2 has no way to exchange beats as anything except double */
/* these all need fixing to not use ::to_double() */
class TempoMap;
class Track;
class MidiStretch;
class MidiModel;
class AutomationList;
class MidiSource;
class MidiRegion;
/* these use ::to_double() but should be removed */
class DoubleBeatsSamplesConverter;
}
namespace Evoral {
template<typename T> class Sequence;
}
/* XXX hack friends for ::do_double() access ... remove */
class QuantizeDialog;
class NoteDrag;
class NoteCreateDrag;
namespace Temporal {
/** Musical time in beats. */
class /*LIBTEMPORAL_API*/ Beats {
public:
LIBTEMPORAL_API static const int32_t PPQN = 1920;
LIBTEMPORAL_API static const int32_t PPQN = Temporal::ticks_per_beat;
Beats() : _beats(0), _ticks(0) {}
Beats(const Beats& other) : _beats(other._beats), _ticks(other._ticks) {}
@ -72,18 +102,16 @@ public:
_ticks = sign * ticks;
}
/** Create from a precise BT time. */
/** Create from a precise beats:ticks pair. */
explicit Beats(int32_t b, int32_t t) : _beats(b), _ticks(t) {
normalize();
}
/** Create from a real number of beats. */
explicit Beats(double time) {
static Beats from_double (double beats) {
double whole;
const double frac = modf(time, &whole);
_beats = whole;
_ticks = frac * PPQN;
const double frac = modf (beats, &whole);
return Beats (whole, (int32_t) rint (frac * PPQN));
}
/** Create from an integer number of beats. */
@ -107,6 +135,12 @@ public:
return Beats(ticks / ppqn, (ticks % ppqn) * PPQN / ppqn);
}
int64_t to_ticks() const { return (int64_t)_beats * PPQN + _ticks; }
int64_t to_ticks(uint32_t ppqn) const { return (int64_t)_beats * ppqn + (_ticks * ppqn / PPQN); }
int32_t get_beats() const { return _beats; }
int32_t get_ticks() const { return _ticks; }
Beats& operator=(double time) {
double whole;
const double frac = modf(time, &whole);
@ -134,9 +168,123 @@ public:
return Beats(_beats, 0);
}
Beats snap_to(const Temporal::Beats& snap) const {
Beats prev_beat() const {
/* always moves backwards even if currently on beat */
return Beats (_beats-1, 0);
}
Beats next_beat() const {
/* always moves forwards even if currently on beat */
return Beats (_beats+1, 0);
}
Beats round_to_subdivision (int subdivision, RoundMode dir) const {
uint32_t ticks = to_ticks();
const uint32_t ticks_one_subdivisions_worth = ticks_per_beat / subdivision;
uint32_t mod = ticks % ticks_one_subdivisions_worth;
uint32_t beats = _beats;
if (dir > 0) {
if (mod == 0 && dir == RoundUpMaybe) {
/* right on the subdivision, which is fine, so do nothing */
} else if (mod == 0) {
/* right on the subdivision, so the difference is just the subdivision ticks */
ticks += ticks_one_subdivisions_worth;
} else {
/* not on subdivision, compute distance to next subdivision */
ticks += ticks_one_subdivisions_worth - mod;
}
// NOTE: this code intentionally limits the rounding so we don't advance to the next beat.
// For the purposes of "jump-to-next-subdivision", we DO want to advance to the next beat.
// And since the "prev" direction DOES move beats, I assume this code is unintended.
// But I'm keeping it around, until we determine there are no terrible consequences.
// if (ticks >= BBT_Time::ticks_per_beat) {
// ticks -= BBT_Time::ticks_per_beat;
// }
} else if (dir < 0) {
/* round to previous (or same iff dir == RoundDownMaybe) */
uint32_t difference = ticks % ticks_one_subdivisions_worth;
if (difference == 0 && dir == RoundDownAlways) {
/* right on the subdivision, but force-rounding down,
so the difference is just the subdivision ticks */
difference = ticks_one_subdivisions_worth;
}
if (ticks < difference) {
ticks = ticks_per_beat - ticks;
} else {
ticks -= difference;
}
} else {
/* round to nearest */
double rem;
/* compute the distance to the previous and next subdivision */
if ((rem = fmod ((double) ticks, (double) ticks_one_subdivisions_worth)) > ticks_one_subdivisions_worth/2.0) {
/* closer to the next subdivision, so shift forward */
ticks = lrint (ticks + (ticks_one_subdivisions_worth - rem));
//DEBUG_TRACE (DEBUG::SnapBBT, string_compose ("moved forward to %1\n", ticks));
if (ticks > ticks_per_beat) {
++beats;
ticks -= ticks_per_beat;
//DEBUG_TRACE (DEBUG::SnapBBT, string_compose ("fold beat to %1\n", beats));
}
} else if (rem > 0) {
/* closer to previous subdivision, so shift backward */
if (rem > ticks) {
if (beats == 0) {
/* can't go backwards past zero, so ... */
return *this;
}
/* step back to previous beat */
--beats;
ticks = lrint (ticks_per_beat - rem);
//DEBUG_TRACE (DEBUG::SnapBBT, string_compose ("step back beat to %1\n", beats));
} else {
ticks = lrint (ticks - rem);
//DEBUG_TRACE (DEBUG::SnapBBT, string_compose ("moved backward to %1\n", ticks));
}
} else {
/* on the subdivision, do nothing */
}
}
return Beats::ticks (ticks);
}
Beats snap_to (Temporal::Beats const & snap) const {
const double snap_time = snap.to_double();
return Beats(ceil(to_double() / snap_time) * snap_time);
return Beats::from_double (ceil(to_double() / snap_time) * snap_time);
}
Beats abs () const {
return Beats (::abs (_beats), ::abs (_ticks));
}
Beats diff (Beats const & other) const {
if (other > *this) {
return other - *this;
}
return *this - other;
}
inline bool operator==(const Beats& b) const {
@ -264,6 +412,11 @@ public:
_beats = B._beats;
_ticks = B._ticks;
return *this;
/* avoids calling ::to_double() to compute ratios of two Beat distances
*/
double operator/ (Beats const & other) {
return (double) to_ticks() / (double) other.to_ticks();
}
Beats& operator+=(const Beats& b) {
@ -280,20 +433,43 @@ public:
return *this;
}
double to_double() const { return (double)_beats + (_ticks / (double)PPQN); }
int64_t to_ticks() const { return (int64_t)_beats * PPQN + _ticks; }
int64_t to_ticks(uint32_t ppqn) const { return (int64_t)_beats * ppqn + (_ticks * ppqn / PPQN); }
int32_t get_beats() const { return _beats; }
int32_t get_ticks() const { return _ticks; }
bool operator!() const { return _beats == 0 && _ticks == 0; }
operator bool () const { return _beats != 0 || _ticks != 0; }
static Beats tick() { return Beats(0, 1); }
static Beats one_tick() { return Beats(0, 1); }
private:
int32_t _beats;
int32_t _ticks;
/* almost nobody should ever be allowed to use this method */
friend class TempoPoint;
friend class ARDOUR::TempoMap;
friend class ARDOUR::Track;
friend class ARDOUR::Variant;
friend class ARDOUR::MidiStretch;
friend class ARDOUR::MidiModel;
friend class ARDOUR::AutomationList;
friend class ARDOUR::MidiSource;
friend class ARDOUR::MidiRegion;
friend class ARDOUR::DoubleBeatsSamplesConverter;
friend class ::QuantizeDialog;
friend class ::NoteDrag;
friend class ::NoteCreateDrag;
double to_double() const { return (double)_beats + (_ticks / (double)PPQN); }
/* this needs to exist because Evoral::Sequence is templated, and some
* other possible template types cannot provide ::from_double
*/
friend class Evoral::Sequence<Beats>;
explicit Beats (double beats) {
double whole;
const double frac = modf (beats, &whole);
_beats = whole;
_ticks = frac * PPQN;
}
};
/*
@ -309,17 +485,18 @@ private:
inline std::ostream&
operator<<(std::ostream& os, const Beats& t)
{
os << t.get_beats() << '.' << t.get_ticks();
os << t.get_beats() << ':' << t.get_ticks();
return os;
}
inline std::istream&
operator>>(std::istream& is, Beats& t)
operator>>(std::istream& istr, Beats& b)
{
double beats;
is >> beats;
t = Beats(beats);
return is;
int32_t beats, ticks;
char d; /* delimiter, whatever it is */
istr >> beats >> d >> ticks;
b = Beats (beats, ticks);
return istr;
}
} // namespace Evoral
@ -347,4 +524,29 @@ namespace std {
};
}
namespace PBD {
namespace DEBUG {
LIBTEMPORAL_API extern uint64_t Beats;
}
template<>
inline bool to_string (Temporal::Beats val, std::string & str)
{
std::ostringstream ostr;
ostr << val;
str = ostr.str();
return true;
}
template<>
inline bool string_to (std::string const & str, Temporal::Beats & val)
{
std::istringstream istr (str);
istr >> val;
return (bool) istr;
}
} /* end namsepace PBD */
#endif // TEMPORAL_BEATS_HPP

View File

@ -41,6 +41,40 @@ typedef int64_t samplecnt_t;
static const samplepos_t max_samplepos = INT64_MAX;
static const samplecnt_t max_samplecnt = INT64_MAX;
/* This defines the smallest division of a "beat".
The number is intended to have as many integer factors as possible so that
1/Nth divisions are integer numbers of ticks.
1920 has many factors, though going up to 3840 gets a couple more.
*/
static const int32_t ticks_per_beat = 1920;
enum TimeDomain {
/* simple ordinals, since these are mutually exclusive */
AudioTime = 0,
BeatTime = 1,
BarTime = 2,
};
enum Dirty {
/* combinable */
SampleDirty = 0x1,
BeatsDirty = 0x2,
BBTDirty = 0x4
};
enum RoundMode {
RoundDownMaybe = -2, ///< Round down only if necessary
RoundDownAlways = -1, ///< Always round down, even if on a division
RoundNearest = 0, ///< Round to nearest
RoundUpAlways = 1, ///< Always round up, even if on a division
RoundUpMaybe = 2 ///< Round up only if necessary
};
extern void setup_enum_writer ();
}
#endif /* __libpbd_position_types_h__ */