From cba53a20233531ef3e6c3692993eac8f74e991a1 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Wed, 13 Sep 2017 19:31:42 -0400 Subject: add _locked() variants to new tempo experiment --- nutemp/t | 0 nutemp/t.cc | 1243 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ nutemp/t.h | 402 +++++++++++++++++++ 3 files changed, 1645 insertions(+) create mode 100644 nutemp/t create mode 100644 nutemp/t.cc create mode 100644 nutemp/t.h (limited to 'nutemp') diff --git a/nutemp/t b/nutemp/t new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nutemp/t.cc b/nutemp/t.cc new file mode 100644 index 0000000000..8741377d7b --- /dev/null +++ b/nutemp/t.cc @@ -0,0 +1,1243 @@ +#include "t.h" + +using namespace ARDOUR; +using std::cerr; +using std::cout; +using std::endl; + +/* overloaded operator* that avoids floating point math when multiplying a superclock position by a number of quarter notes */ +superclock_t operator*(superclock_t sc, Evoral::Beats const & b) { return (sc * ((b.get_beats() * Evoral::Beats::PPQN) + b.get_ticks())) / Evoral::Beats::PPQN; } + +Timecode::BBT_Time +Meter::bbt_add (Timecode::BBT_Time const & bbt, Timecode::BBT_Offset const & add) const +{ + int32_t bars = bbt.bars; + int32_t beats = bbt.beats; + int32_t ticks = bbt.ticks; + + if ((bars ^ add.bars) < 0) { + /* signed-ness varies */ + if (abs(add.bars) >= abs(bars)) { + /* addition will change which side of "zero" the answer is on; + adjust bbt.bars towards zero to deal with "unusual" BBT math + */ + if (bars < 0) { + bars++; + } else { + bars--; + } + } + } + + if ((beats ^ add.beats) < 0) { + /* signed-ness varies */ + if (abs (add.beats) >= abs (beats)) { + /* adjust bbt.beats towards zero to deal with "unusual" BBT math */ + if (beats < 0) { + beats++; + } else { + beats--; + } + } + } + + Timecode::BBT_Offset r (bars + add.bars, beats + add.beats, ticks + add.ticks); + + if (r.ticks >= Evoral::Beats::PPQN) { + r.beats += r.ticks / Evoral::Beats::PPQN; + r.ticks %= Evoral::Beats::PPQN; + } + + if (r.beats > _divisions_per_bar) { + r.bars += r.beats / _divisions_per_bar; + r.beats %= _divisions_per_bar; + } + + if (r.beats == 0) { + r.beats = 1; + } + + if (r.bars == 0) { + r.bars = 1; + } + + return Timecode::BBT_Time (r.bars, r.beats, r.ticks); +} + +Timecode::BBT_Time +Meter::bbt_subtract (Timecode::BBT_Time const & bbt, Timecode::BBT_Offset const & sub) const +{ + int32_t bars = bbt.bars; + int32_t beats = bbt.beats; + int32_t ticks = bbt.ticks; + + if ((bars ^ sub.bars) < 0) { + /* signed-ness varies */ + if (abs (sub.bars) >= abs (bars)) { + /* adjust bbt.bars towards zero to deal with "unusual" BBT math */ + if (bars < 0) { + bars++; + } else { + bars--; + } + } + } + + if ((beats ^ sub.beats) < 0) { + /* signed-ness varies */ + if (abs (sub.beats) >= abs (beats)) { + /* adjust bbt.beats towards zero to deal with "unusual" BBT math */ + if (beats < 0) { + beats++; + } else { + beats--; + } + } + } + + Timecode::BBT_Offset r (bars - sub.bars, beats - sub.beats, ticks - sub.ticks); + + if (r.ticks < 0) { + r.beats -= 1 - (r.ticks / Evoral::Beats::PPQN); + r.ticks = Evoral::Beats::PPQN + (r.ticks % Evoral::Beats::PPQN); + } + + if (r.beats <= 0) { + r.bars -= 1 - (r.beats / _divisions_per_bar); + r.beats = _divisions_per_bar + (r.beats % _divisions_per_bar); + } + + if (r.beats == 0) { + r.beats = 1; + } + + if (r.bars <= 0) { + r.bars -= 1; + } + + return Timecode::BBT_Time (r.bars, r.beats, r.ticks); +} + +Timecode::BBT_Offset +Meter::bbt_delta (Timecode::BBT_Time const & a, Timecode::BBT_Time const & b) const +{ + return Timecode::BBT_Offset (a.bars - b.bars, a.beats - b.beats, a.ticks - b.ticks); +} + +Timecode::BBT_Time +Meter::round_up_to_bar (Timecode::BBT_Time const & bbt) const +{ + Timecode::BBT_Time b = bbt.round_up_to_beat (); + if (b.beats > 1) { + b.bars++; + b.beats = 1; + } + return b; +} + +Evoral::Beats +Meter::to_quarters (Timecode::BBT_Offset const & offset) const +{ + Evoral::Beats b; + + b += (offset.bars * _divisions_per_bar * 4) / _note_value; + cerr << offset.bars << " bars as quarters : " << b << " nv = " << (int) _note_value << endl; + b += (offset.beats * 4) / _note_value; + cerr << offset.beats << " beats as quarters : " << (offset.beats * 4) / _note_value << " nv = " << (int) _note_value << endl; + b += Evoral::Beats::ticks (offset.ticks); + + return b; +} + +superclock_t +TempoMetric::superclock_per_note_type_at_superclock (superclock_t sc) const +{ + return superclocks_per_note_type () * expm1 (_c_per_superclock * sc); +} + +superclock_t +TempoMetric::superclocks_per_grid (framecnt_t sr) const +{ + return (superclock_ticks_per_second * Meter::note_value()) / (note_types_per_minute() / Tempo::note_type()); +} + +superclock_t +TempoMetric::superclocks_per_bar (framecnt_t sr) const +{ + return superclocks_per_grid (sr) * _divisions_per_bar; +} + +/* +Ramp Overview + + | * +Tempo | * +Tt----|-----------------*| +Ta----|--------------|* | + | * | | + | * | | + | * | | +T0----|* | | + * | | | + _______________|___|____ + time a t (next tempo) + [ c ] defines c + +Duration in beats at time a is the integral of some Tempo function. +In our case, the Tempo function (Tempo at time t) is +T(t) = T0(e^(ct)) + +>>1/S(t) = (1/S0)(e^ct) => (1/S)(t) = (e^(ct))/S0 => S(t) = S0/(e^(ct)) + +with function constant +c = log(Ta/T0)/a + +>>c = log ((1/Sa)/(1/S0)) / a => c = log (S0/Sa) / a + +so +a = log(Ta/T0)/c + +>>a = log ((1/Ta)/(1/S0) / c => a = log (S0/Sa) / c + +The integral over t of our Tempo function (the beat function, which is the duration in beats at some time t) is: +b(t) = T0(e^(ct) - 1) / c + +>>b(t) = 1/S0(e^(ct) - 1) / c => b(t) = (e^(ct) - 1) / (c * S0) + +To find the time t at beat duration b, we use the inverse function of the beat function (the time function) which can be shown to be: +t(b) = log((c.b / T0) + 1) / c + +>>t(b) = log((c*b / (1/S0)) + 1) / c => t(b) = log ((c*b * S0) + 1) / c + +The time t at which Tempo T occurs is a as above: +t(T) = log(T / T0) / c + +>> t(1/S) = log ((1/S) / (1/S0) /c => t(1/S) = log (S0/S) / c + +The beat at which a Tempo T occurs is: +b(T) = (T - T0) / c + +>> b(1/S) = (1/S - 1/S0) / c + +The Tempo at which beat b occurs is: +T(b) = b.c + T0 + +>> T(b) = b.c + (1/S0) + +We define c for this tempo ramp by placing a new tempo section at some time t after this one. +Our problem is that we usually don't know t. +We almost always know the duration in beats between this and the new section, so we need to find c in terms of the beat function. +Where a = t (i.e. when a is equal to the time of the next tempo section), the beat function reveals: +t = b log (Ta / T0) / (T0 (e^(log (Ta / T0)) - 1)) + +By substituting our expanded t as a in the c function above, our problem is reduced to: +c = T0 (e^(log (Ta / T0)) - 1) / b + +>> c = (1/S0) (e^(log ((1/Sa) / (1/S0))) - 1) / b => c = (1/S0) (e^(log (S0/Sa)) - 1) / b => c (e^(log (S0/Sa)) - 1) / (b * S0) + +Of course the word 'beat' has been left loosely defined above. +In music, a beat is defined by the musical pulse (which comes from the tempo) +and the meter in use at a particular time (how many pulse divisions there are in one bar). +It would be more accurate to substitute the work 'pulse' for 'beat' above. + + */ + +/* equation to compute c is: + * + * c = log (Ta / T0) / a + * + * where + * + * a : time into section (from section start + * T0 : tempo at start of section + * Ta : tempo at time a into section + * + * THE UNITS QUESTION + * + * log (Ta / T0) / (time-units) => C is in per-time-units (1/time-units) + * + * so the question is what are the units of a, and thus c? + * + * we could use ANY time units (because we can measure a in any time units) + * but whichever one we pick dictates how we can use c in the future since + * all subsequent computations will need to use the same time units. + * + * options: + * + * pulses ... whole notes, possibly useful, since we can use it for any other note_type + * quarter notes ... linearly related to pulses + * beats ... not a fixed unit of time + * minutes ... linearly related to superclocks + * samples ... needs sample rate + * superclocks ... frequently correct + * + * so one answer might be to compute c in two different units so that we have both available. + * + * hence, compute_c_superclocks() and compute_c_pulses() + */ + +void +TempoMetric::compute_c_superclock (framecnt_t sr, superclock_t end_scpqn, superclock_t superclock_duration) +{ + if ((superclocks_per_quarter_note() == end_scpqn) || !ramped()) { + _c_per_superclock = 0.0; + return; + } + + _c_per_superclock = log ((double) superclocks_per_quarter_note () / end_scpqn) / superclock_duration; +} +void +TempoMetric::compute_c_quarters (framecnt_t sr, superclock_t end_scpqn, Evoral::Beats const & quarter_duration) +{ + if ((superclocks_per_quarter_note () == end_scpqn) || !ramped()) { + _c_per_quarter = 0.0; + return; + } + + _c_per_quarter = log (superclocks_per_quarter_note () / (double) end_scpqn) / quarter_duration.to_double(); +} + +superclock_t +TempoMetric::superclock_at_qn (Evoral::Beats const & qn) const +{ + if (_c_per_quarter == 0.0) { + /* not ramped, use linear */ + return llrint (superclocks_per_quarter_note () * qn.to_double()); + } + + return llrint (superclocks_per_quarter_note() * (log1p (_c_per_quarter * qn.to_double()) / _c_per_quarter)); +} + +Evoral::Beats +TempoMapPoint::quarters_at (superclock_t sc) const +{ + /* This TempoMapPoint must already have a fully computed metric and position */ + + if (!ramped()) { + return _quarters + Evoral::Beats ((sc - _sclock) / (double) (metric().superclocks_per_quarter_note ())); + } + + return _quarters + Evoral::Beats (expm1 (metric().c_per_superclock() * (sc - _sclock)) / (metric().c_per_superclock() * metric().superclocks_per_quarter_note ())); +} + +Evoral::Beats +TempoMapPoint::quarters_at (Timecode::BBT_Time const & bbt) const +{ + /* This TempoMapPoint must already have a fully computed metric and position */ + + Timecode::BBT_Offset offset = metric().bbt_delta (bbt, _bbt); + cerr << "QA BBT DELTA between " << bbt << " and " << _bbt << " = " << offset << " as quarters for " << static_cast (metric()) << " = " << metric().to_quarters (offset) << endl; + return _quarters + metric().to_quarters (offset); +} + +Timecode::BBT_Time +TempoMapPoint::bbt_at (Evoral::Beats const & qn) const +{ + /* This TempoMapPoint must already have a fully computed metric and position */ + + Evoral::Beats quarters_delta = qn - _quarters; + int32_t ticks_delta = quarters_delta.to_ticks (Evoral::Beats::PPQN); + return metric().bbt_add (_bbt, Timecode::BBT_Offset (0, 0, ticks_delta)); +} + +TempoMap::TempoMap (Tempo const & initial_tempo, Meter const & initial_meter, framecnt_t sr) + : _sample_rate (sr) +{ + TempoMapPoint tmp (TempoMapPoint::Flag (TempoMapPoint::ExplicitMeter|TempoMapPoint::ExplicitTempo), initial_tempo, initial_meter, 0, Evoral::Beats(), Timecode::BBT_Time(), AudioTime); + _points.push_back (tmp); +} + +Meter const & +TempoMap::meter_at (superclock_t sc) const +{ + Glib::Threads::RWLock::ReaderLock lm (_lock); + return meter_at_locked (sc); +} + +Meter const & +TempoMap::meter_at (Evoral::Beats const & b) const +{ + Glib::Threads::RWLock::ReaderLock lm (_lock); + return meter_at_locked (b); +} + +Meter const & +TempoMap::meter_at (Timecode::BBT_Time const & bbt) const +{ + Glib::Threads::RWLock::ReaderLock lm (_lock); + return meter_at_locked (bbt); +} + +Tempo const & +TempoMap::tempo_at (superclock_t sc) const +{ + Glib::Threads::RWLock::ReaderLock lm (_lock); + return tempo_at_locked (sc); +} + +Tempo const & +TempoMap::tempo_at (Evoral::Beats const &b) const +{ + Glib::Threads::RWLock::ReaderLock lm (_lock); + return tempo_at_locked (b); +} + +Tempo const & +TempoMap::tempo_at (Timecode::BBT_Time const & bbt) const +{ + Glib::Threads::RWLock::ReaderLock lm (_lock); + return tempo_at_locked (bbt); +} + +void +TempoMap::rebuild (superclock_t limit) +{ + Glib::Threads::RWLock::WriterLock lm (_lock); + rebuild_locked (limit); +} + +void +TempoMap::rebuild_locked (superclock_t limit) +{ + /* step one: remove all implicit points after a dirty explicit point */ + + restart: + TempoMapPoints::iterator tmp = _points.begin(); + TempoMapPoints::iterator first_explicit_dirty = _points.end(); + + while ((tmp != _points.end()) && (tmp->is_implicit() || (tmp->is_explicit() && !tmp->dirty ()))) { + ++tmp; + } + + first_explicit_dirty = tmp; + + /* remove all implicit points, because we're going to recalculate them all */ + + while (tmp != _points.end()) { + TempoMapPoints::iterator next = tmp; + ++next; + + if (tmp->is_implicit()) { + _points.erase (tmp); + } + + tmp = next; + } + + /* compute C for all ramped sections */ + + for (tmp = first_explicit_dirty; tmp != _points.end(); ) { + TempoMapPoints::iterator nxt = tmp; + ++nxt; + + if (tmp->ramped() && (nxt != _points.end())) { + tmp->metric().compute_c_quarters (_sample_rate, nxt->metric().superclocks_per_quarter_note (), nxt->quarters() - tmp->quarters()); + } + + tmp = nxt; + } + + TempoMapPoints::iterator prev = _points.end(); + + /* Compute correct quarter-note and superclock times for all music-time locked explicit points */ + + for (tmp = first_explicit_dirty; tmp != _points.end(); ) { + + TempoMapPoints::iterator next = tmp; + ++next; + + if (prev != _points.end()) { + if ((tmp->lock_style() == MusicTime)) { + /* determine superclock and quarter note time for this (music-time) locked point */ + + cerr << "MT-lock, prev = " << *prev << endl; + + Evoral::Beats qn = prev->quarters_at (tmp->bbt()); + cerr << "MT-lock @ " << tmp->bbt() << " => " << qn << endl; + superclock_t sc = prev->sclock() + prev->metric().superclock_at_qn (qn - prev->quarters()); + cerr << "MT-lock sc is " << prev->metric().superclock_at_qn (qn - prev->quarters()) << " after " << prev->sclock() << " = " << sc + << " secs = " << prev->metric().superclock_at_qn (qn - prev->quarters()) / (double) superclock_ticks_per_second + << endl; + + if (qn != tmp->quarters() || tmp->sclock() != sc) { + cerr << "Ned to move " << *tmp << endl; + tmp->set_quarters (qn); + tmp->set_sclock (sc); + cerr << "using " << *prev << " moved music-time-locked @ " << tmp->bbt() << " to " << sc << " aka " << qn << endl; + _points.sort (TempoMapPoint::SuperClockComparator()); + cerr << "Restart\n"; + goto restart; + } + } + } + + prev = tmp; + tmp = next; + } + + /* _points is guaranteed sorted in superclock and quarter note order. It may not be sorted BBT order because of re-ordering + * of music-time locked points. + */ + + cerr << "POST-SORT\n"; + dump (cerr); + + prev = _points.end(); + + /* step two: add new implicit points between each pair of explicit + * points, after the dirty explicit point + */ + + for (tmp = _points.begin(); tmp != _points.end(); ) { + + if (!tmp->dirty()) { + ++tmp; + continue; + } + + TempoMapPoints::iterator next = tmp; + ++next; + + if (prev != _points.end()) { + if ((tmp->lock_style() == AudioTime)) { + cerr << "AT: check " << *tmp << endl + << "\t\tusing " << *prev << endl; + /* audio-locked explicit point: recompute it's BBT and quarter-note position since this may have changed */ + tmp->set_quarters (prev->quarters_at (tmp->sclock())); + cerr << "AT - recompute quarters at " << tmp->quarters () << endl; + if (static_cast(tmp->metric()) != static_cast(prev->metric())) { + /* new meter, must be on bar/measure start */ + tmp->set_bbt (prev->metric().round_up_to_bar (prev->bbt_at (tmp->quarters()))); + } else { + /* no meter change, tempo change required to be on beat */ + tmp->set_bbt (prev->bbt_at (tmp->quarters()).round_up_to_beat()); + } + cerr << "AT - recompute bbt at " << tmp->bbt () << endl; + } + } + + superclock_t sc = tmp->sclock(); + Evoral::Beats qn (tmp->quarters ()); + Timecode::BBT_Time bbt (tmp->bbt()); + const bool ramped = tmp->ramped () && next != _points.end(); + + /* Evoral::Beats are really quarter notes. This counts how many quarter notes + there are between grid points in this section of the tempo map. + */ + const Evoral::Beats qn_step = (Evoral::Beats (1) * 4) / tmp->metric().note_value(); + + /* compute implicit points as far as the next explicit point, or limit, + whichever comes first. + */ + + const superclock_t sc_limit = (next == _points.end() ? limit : (*next).sclock()); + + while (1) { + + /* define next beat in superclocks, beats and bbt */ + + qn += qn_step; + bbt = tmp->metric().bbt_add (bbt, Timecode::BBT_Offset (0, 1, 0)); + + if (!ramped) { + sc += tmp->metric().superclocks_per_note_type(); + } else { + sc = tmp->sclock() + tmp->metric().superclock_at_qn (qn - tmp->quarters()); + } + + if (sc >= sc_limit) { + break; + } + + _points.insert (next, TempoMapPoint (*tmp, sc, qn, bbt)); + } + + (*tmp).set_dirty (false); + prev = tmp; + tmp = next; + } +} + +bool +TempoMap::set_tempo (Tempo const & t, superclock_t sc, bool ramp) +{ + Glib::Threads::RWLock::WriterLock lm (_lock); + + assert (!_points.empty()); + + /* special case: first map entry is later than the new point */ + + if (_points.front().sclock() > sc) { + /* first point is later than sc. There's no iterator to reference a point at or before sc */ + + /* determine beats and BBT time for this new tempo point. Note that tempo changes (points) must be deemed to be on beat, + even if the user moves them later. Even after moving, the TempoMapPoint that was beat N is still beat N, and is not + fractional. + */ + + Evoral::Beats b = _points.front().quarters_at (sc).round_to_beat(); + Timecode::BBT_Time bbt = _points.front().bbt_at (b).round_to_beat (); + + _points.insert (_points.begin(), TempoMapPoint (TempoMapPoint::ExplicitTempo, t, _points.front().metric(), sc, b, bbt, AudioTime, ramp)); + return true; + } + + /* special case #3: only one map entry, at the same time as the new point. + This is the common case when editing tempo/meter in a session with a single tempo/meter + */ + + if (_points.size() == 1 && _points.front().sclock() == sc) { + /* change tempo */ + *((Tempo*) &_points.front().metric()) = t; + _points.front().make_explicit (TempoMapPoint::ExplicitTempo); + return true; + } + + /* Remember: iterator_at() returns an iterator that references the TempoMapPoint at or BEFORE sc */ + + TempoMapPoints::iterator i = iterator_at (sc); + TempoMapPoints::iterator nxt = i; + ++nxt; + + if (i->sclock() == sc) { + /* change tempo */ + *((Tempo*) &i->metric()) = t; + i->make_explicit (TempoMapPoint::ExplicitTempo); + /* done */ + return true; + } + + if (sc - i->sclock() < i->metric().superclocks_per_note_type()) { + cerr << "new tempo too close to previous ...\n"; + return false; + } + + Meter const & meter (i->metric()); + + if (i->metric().ramped()) { + /* need to adjust ramp constants for preceding explict point, since the new point will be positioned right after it + and thus defines the new ramp distance. + */ + i->metric().compute_c_superclock (_sample_rate, t.superclocks_per_quarter_note (), sc); + } + + /* determine beats and BBT time for this new tempo point. Note that tempo changes (points) must be deemed to be on beat, + even if the user moves them later. Even after moving, the TempoMapPoint that was beat N is still beat N, and is not + fractional. + */ + + Evoral::Beats qn = i->quarters_at (sc).round_to_beat(); + + /* rule: all Tempo changes must be on-beat. So determine the nearest later beat to "sc" + */ + + Timecode::BBT_Time bbt = i->bbt_at (qn).round_up_to_beat (); + + /* Modify the iterator to reference the point AFTER this new one, because STL insert is always "insert-before" + */ + + if (i != _points.end()) { + ++i; + } + _points.insert (i, TempoMapPoint (TempoMapPoint::ExplicitTempo, t, meter, sc, qn, bbt, AudioTime, ramp)); + return true; +} + +bool +TempoMap::set_tempo (Tempo const & t, Timecode::BBT_Time const & bbt, bool ramp) +{ + Glib::Threads::RWLock::WriterLock lm (_lock); + + /* tempo changes are required to be on-beat */ + + Timecode::BBT_Time on_beat = bbt.round_up_to_beat(); + + cerr << "Try to set tempo @ " << on_beat << " to " << t << endl; + + assert (!_points.empty()); + + if (_points.front().bbt() > on_beat) { + cerr << "Cannot insert tempo at " << bbt << " before first point at " << _points.front().bbt() << endl; + return false; + } + + if (_points.size() == 1 && _points.front().bbt() == on_beat) { + /* change Meter */ + *((Tempo*) &_points.front().metric()) = t; + _points.front().make_explicit (TempoMapPoint::ExplicitTempo); + return true; + } + + TempoMapPoints::iterator i = iterator_at (on_beat); + + if (i->bbt() == on_beat) { + *((Tempo*) &i->metric()) = t; + i->make_explicit (TempoMapPoint::ExplicitTempo); + return true; + } + + Meter const & meter (i->metric()); + ++i; + + /* stick a prototype music-locked point up front and let ::rebuild figure out the superclock and quarter time */ + _points.insert (i, TempoMapPoint (TempoMapPoint::ExplicitTempo, t, meter, 0, Evoral::Beats(), on_beat, MusicTime, ramp)); + return true; +} + +bool +TempoMap::set_meter (Meter const & m, Timecode::BBT_Time const & bbt) +{ + Glib::Threads::RWLock::WriterLock lm (_lock); + Timecode::BBT_Time measure_start (m.round_up_to_bar (bbt)); + + cerr << "Try to set meter @ " << measure_start << " to " << m << endl; + + assert (!_points.empty()); + + if (_points.front().bbt() > measure_start) { + cerr << "Cannot insert meter at " << bbt << " before first point at " << _points.front().bbt() << endl; + return false; + } + + if (_points.size() == 1 && _points.front().bbt() == measure_start) { + /* change Meter */ + cerr << "Found the single point\n"; + *((Meter*) &_points.front().metric()) = m; + cerr << "Updated meter to " << m << endl; + _points.front().make_explicit (TempoMapPoint::ExplicitMeter); + return true; + } + + TempoMapPoints::iterator i = iterator_at (measure_start); + + if (i->bbt() == measure_start) { + *((Meter*) &i->metric()) = m; + cerr << "Updated meter to " << m << endl; + i->make_explicit (TempoMapPoint::ExplicitMeter); + return true; + } + + Evoral::Beats qn = i->quarters_at (measure_start); + superclock_t sc = i->sclock() + i->metric().superclock_at_qn (qn); + + Tempo const & tempo (i->metric()); + ++i; + + cerr << "NEW METER, provisionally @ " + << TempoMapPoint (TempoMapPoint::ExplicitMeter, tempo, m, sc, qn, measure_start, MusicTime) + << endl; + + _points.insert (i, TempoMapPoint (TempoMapPoint::ExplicitMeter, tempo, m, sc, qn, measure_start, MusicTime)); + return true; +} + +bool +TempoMap::set_meter (Meter const & m, superclock_t sc) +{ + Glib::Threads::RWLock::WriterLock lm (_lock); + assert (!_points.empty()); + + /* special case #2: first map entry is later than the new point */ + + if (_points.front().sclock() > sc) { + /* determine quarters and BBT time for this new tempo point. Note that tempo changes (points) must be deemed to be on beat, + even if the user moves them later. Even after moving, the TempoMapPoint that was beat N is still beat N, and is not + fractional. + */ + + Evoral::Beats b = _points.front().quarters_at (sc).round_to_beat(); + Timecode::BBT_Time bbt = _points.front().bbt_at (b).round_to_beat (); + + _points.insert (_points.begin(), TempoMapPoint (TempoMapPoint::ExplicitMeter, _points.front().metric(), m, sc, b, bbt, AudioTime)); + return true; + } + + /* special case #3: only one map entry, at the same time as the new point. + + This is the common case when editing tempo/meter in a session with a single tempo/meter + */ + + if (_points.size() == 1 && _points.front().sclock() == sc) { + /* change meter */ + *((Meter*) &_points.front().metric()) = m; + _points.front().make_explicit (TempoMapPoint::ExplicitMeter); + return true; + } + + TempoMapPoints::iterator i = iterator_at (sc); + + if (i->sclock() == sc) { + /* change meter */ + *((Meter*) &i->metric()) = m; + + /* enforce rule described below regarding meter change positions */ + + if (i->bbt().beats != 1) { + i->set_bbt (Timecode::BBT_Time (i->bbt().bars + 1, 1, 0)); + } + + i->make_explicit (TempoMapPoint::ExplicitMeter); + + return true; + } + + if (sc - i->sclock() < i->metric().superclocks_per_note_type()) { + cerr << "new tempo too close to previous ...\n"; + return false; + } + + /* determine quarters and BBT time for this new tempo point. Note that tempo changes (points) must be deemed to be on beat, + even if the user moves them later. Even after moving, the TempoMapPoint that was beat N is still beat N, and is not + fractional. + */ + + Evoral::Beats b = i->quarters_at (sc).round_to_beat(); + + /* rule: all Meter changes must start a new measure. So determine the nearest, lower beat to "sc". If this is not + the first division of the measure, move to the next measure. + */ + + Timecode::BBT_Time bbt = i->bbt_at (b).round_down_to_beat (); + + if (bbt.beats != 1) { + bbt.bars += 1; + bbt.beats = 1; + bbt.ticks = 0; + } + + Tempo const & tempo (i->metric()); + ++i; + + _points.insert (i, TempoMapPoint (TempoMapPoint::ExplicitMeter, tempo, m, sc, b, bbt, AudioTime)); + return true; +} + +TempoMapPoints::iterator +TempoMap::iterator_at (superclock_t sc) +{ + /* CALLER MUST HOLD LOCK */ + + if (_points.empty()) { + throw EmptyTempoMapException(); + } + + if (_points.size() == 1) { + return _points.begin(); + } + + /* Construct an arbitrary TempoMapPoint. The only property we care about is it's superclock time, + so other values used in the constructor are arbitrary and irrelevant. + */ + + TempoMetric const & metric (_points.front().metric()); + const TempoMapPoint tp (TempoMapPoint::Flag (0), metric, metric, sc, Evoral::Beats(), Timecode::BBT_Time(), AudioTime); + TempoMapPoint::SuperClockComparator scmp; + + TempoMapPoints::iterator tmp = upper_bound (_points.begin(), _points.end(), tp, scmp); + + if (tmp != _points.begin()) { + return --tmp; + } + + return tmp; +} + +TempoMapPoints::iterator +TempoMap::iterator_at (Evoral::Beats const & qn) +{ + /* CALLER MUST HOLD LOCK */ + + if (_points.empty()) { + throw EmptyTempoMapException(); + } + + if (_points.size() == 1) { + return _points.begin(); + } + + /* Construct an arbitrary TempoMapPoint. The only property we care about is its quarters time, + so other values used in the constructor are arbitrary and irrelevant. + */ + + TempoMetric const & metric (_points.front().metric()); + const TempoMapPoint tp (TempoMapPoint::Flag (0), metric, metric, 0, qn, Timecode::BBT_Time(), AudioTime); + TempoMapPoint::QuarterComparator bcmp; + + TempoMapPoints::iterator tmp = upper_bound (_points.begin(), _points.end(), tp, bcmp); + + if (tmp != _points.begin()) { + return --tmp; + } + + return tmp; +} + +TempoMapPoints::iterator +TempoMap::iterator_at (Timecode::BBT_Time const & bbt) +{ + /* CALLER MUST HOLD LOCK */ + + if (_points.empty()) { + throw EmptyTempoMapException(); + } + + if (_points.size() == 1) { + return _points.begin(); + } + + /* Construct an arbitrary TempoMapPoint. The only property we care about is its bbt time, + so other values used in the constructor are arbitrary and irrelevant. + */ + + TempoMetric const & metric (_points.front().metric()); + const TempoMapPoint tp (TempoMapPoint::Flag(0), metric, metric, 0, Evoral::Beats(), bbt, MusicTime); + TempoMapPoint::BBTComparator bcmp; + + TempoMapPoints::iterator tmp = upper_bound (_points.begin(), _points.end(), tp, bcmp); + + if (tmp != _points.begin()) { + return --tmp; + } + + return tmp; +} + +Timecode::BBT_Time +TempoMap::bbt_at (superclock_t sc) const +{ + Glib::Threads::RWLock::ReaderLock lm (_lock); + return bbt_at_locked (sc); +} + +Timecode::BBT_Time +TempoMap::bbt_at_locked (superclock_t sc) const +{ + TempoMapPoint point (const_point_at (sc)); + Evoral::Beats b ((sc - point.sclock()) / (double) point.metric().superclocks_per_quarter_note()); + return point.metric().bbt_add (point.bbt(), Timecode::BBT_Offset (0, b.get_beats(), b.get_ticks())); +} + +Timecode::BBT_Time +TempoMap::bbt_at (Evoral::Beats const & qn) const +{ + Glib::Threads::RWLock::ReaderLock lm (_lock); + return bbt_at_locked (qn); +} + +Timecode::BBT_Time +TempoMap::bbt_at_locked (Evoral::Beats const & qn) const +{ + //Glib::Threads::RWLock::ReaderLock lm (_lock); + TempoMapPoint const & point (const_point_at (qn)); + Evoral::Beats delta (qn - point.quarters()); + return point.metric().bbt_add (point.bbt(), Timecode::BBT_Offset (0, delta.get_beats(), delta.get_ticks())); +} + +Evoral::Beats +TempoMap::quarter_note_at (superclock_t sc) const +{ + Glib::Threads::RWLock::ReaderLock lm (_lock); + return quarter_note_at_locked (sc); +} + +Evoral::Beats +TempoMap::quarter_note_at_locked (superclock_t sc) const +{ + return const_point_at (sc).quarters_at (sc); +} + +Evoral::Beats +TempoMap::quarter_note_at (Timecode::BBT_Time const & bbt) const +{ + Glib::Threads::RWLock::ReaderLock lm (_lock); + return quarter_note_at_locked (bbt); +} + +Evoral::Beats +TempoMap::quarter_note_at_locked (Timecode::BBT_Time const & bbt) const +{ + //Glib::Threads::RWLock::ReaderLock lm (_lock); + TempoMapPoint const & point (const_point_at (bbt)); + + Timecode::BBT_Time bbt_delta (point.metric().bbt_subtract (bbt, point.bbt())); + /* XXX need to convert the metric division to quarters to match Evoral::Beats == Evoral::Quarters */ + return point.quarters() + Evoral::Beats ((point.metric().divisions_per_bar() * bbt_delta.bars) + bbt_delta.beats, bbt_delta.ticks); +} + +superclock_t +TempoMap::superclock_at (Evoral::Beats const & qn) const +{ + Glib::Threads::RWLock::ReaderLock lm (_lock); + return superclock_at_locked (qn); +} + +superclock_t +TempoMap::superclock_at_locked (Evoral::Beats const & qn) const +{ + assert (!_points.empty()); + + /* only the quarters property matters, since we're just using it to compare */ + const TempoMapPoint tmp (TempoMapPoint::Flag (0), Tempo (120), Meter (4, 4), 0, qn, Timecode::BBT_Time(), MusicTime); + TempoMapPoint::QuarterComparator bcmp; + + TempoMapPoints::const_iterator i = upper_bound (_points.begin(), _points.end(), tmp, bcmp); + + if (i == _points.end()) { + --i; + } + + /* compute distance from reference point to b. Remember that Evoral::Beats is always interpreted as quarter notes */ + + const Evoral::Beats q_delta = qn - i->quarters(); + superclock_t sclock_delta = i->metric().superclocks_per_quarter_note () * q_delta; + return i->sclock() + sclock_delta; +} + +superclock_t +TempoMap::superclock_at (Timecode::BBT_Time const & bbt) const +{ + //Glib::Threads::RWLock::ReaderLock lm (_lock); + return superclock_at_locked (bbt); +} + +superclock_t +TempoMap::superclock_at_locked (Timecode::BBT_Time const & bbt) const +{ + //Glib::Threads::RWLock::ReaderLock lm (_lock); + assert (!_points.empty()); + + /* only the bbt property matters, since we're just using it to compare */ + const TempoMapPoint tmp (TempoMapPoint::Flag (0), Tempo (120), Meter (4, 4), 0, Evoral::Beats(), bbt, MusicTime); + TempoMapPoint::BBTComparator bcmp; + + TempoMapPoints::const_iterator i = upper_bound (_points.begin(), _points.end(), tmp, bcmp); + + if (i == _points.end()) { + --i; + } + + /* compute distance from reference point to b. Remember that Evoral::Beats is always interpreted as quarter notes */ + + //const Evoral::Beats delta = b - i->beats(); + //return i->sclock() + samples_to_superclock ((delta / i->metric().quarter_notes_per_minute ()).to_double() * _sample_rate, _sample_rate); + return 0; +} + +void +TempoMap::set_sample_rate (framecnt_t new_sr) +{ + //Glib::Threads::RWLock::ReaderLock lm (_lock); + double ratio = new_sr / (double) _sample_rate; + + for (TempoMapPoints::iterator i = _points.begin(); i != _points.end(); ++i) { + i->map_reset_set_sclock_for_sr_change (llrint (ratio * i->sclock())); + } +} + void +TempoMap::dump (std::ostream& ostr) +{ + //Glib::Threads::RWLock::ReaderLock lm (_lock); + ostr << "\n\n------------\n"; + for (TempoMapPoints::iterator i = _points.begin(); i != _points.end(); ++i) { + ostr << *i << std::endl; + } +} + +void +TempoMap::remove_explicit_point (superclock_t sc) +{ + //Glib::Threads::RWLock::WriterLock lm (_lock); + TempoMapPoints::iterator p = iterator_at (sc); + + if (p->sclock() == sc) { + _points.erase (p); + } +} + +void +TempoMap::move_explicit (superclock_t current, superclock_t destination) +{ + //Glib::Threads::RWLock::WriterLock lm (_lock); + TempoMapPoints::iterator p = iterator_at (current); + + if (p->sclock() != current) { + return; + } + + move_explicit_to (p, destination); +} + +void +TempoMap::move_explicit_to (TempoMapPoints::iterator p, superclock_t destination) +{ + /* CALLER MUST HOLD LOCK */ + + TempoMapPoint point (*p); + point.set_sclock (destination); + + TempoMapPoints::iterator prev; + + prev = p; + if (p != _points.begin()) { + --prev; + } + + /* remove existing */ + + _points.erase (p); + prev->set_dirty (true); + + /* find insertion point */ + + p = iterator_at (destination); + + /* STL insert semantics are always "insert-before", whereas ::iterator_at() returns iterator-at-or-before */ + ++p; + + _points.insert (p, point); +} + +void +TempoMap::move_implicit (superclock_t current, superclock_t destination) +{ + //Glib::Threads::RWLock::WriterLock lm (_lock); + TempoMapPoints::iterator p = iterator_at (current); + + if (p->sclock() != current) { + return; + } + + if (p->is_implicit()) { + p->make_explicit (TempoMapPoint::Flag (TempoMapPoint::ExplicitMeter|TempoMapPoint::ExplicitTempo)); + } + + move_explicit_to (p, destination); +} + +/*******/ + +#define SAMPLERATE 48000 +#define SECONDS_TO_SUPERCLOCK(s) (superclock_ticks_per_second * s) + +using std::cerr; +using std::cout; +using std::endl; + +void +test_bbt_math () +{ + using namespace Timecode; + + BBT_Time a; + BBT_Time b1 (1,1,1919); + BBT_Time n1 (-1,1,1919); + std::vector meters; + + meters.push_back (Meter (4, 4)); + meters.push_back (Meter (5, 8)); + meters.push_back (Meter (11, 7)); + meters.push_back (Meter (3, 4)); + +#define PRINT_RESULT(m,str,op,op1,Bars,Beats,Ticks) cout << m << ' ' << (op1) << ' ' << str << ' ' << BBT_Offset ((Bars),(Beats),(Ticks)) << " = " << m.op ((op1), BBT_Offset ((Bars), (Beats), (Ticks))) << endl; + + for (std::vector::iterator m = meters.begin(); m != meters.end(); ++m) { + for (int B = 1; B < 4; ++B) { + for (int b = 1; b < 13; ++b) { + PRINT_RESULT((*m), "+", bbt_add, a, B, b, 0); + PRINT_RESULT((*m), "+", bbt_add, a, B, b, 1); + PRINT_RESULT((*m), "+", bbt_add, a, B, b, Evoral::Beats::PPQN/2); + PRINT_RESULT((*m), "+", bbt_add, a, B, b, Evoral::Beats::PPQN); + PRINT_RESULT((*m), "+", bbt_add, a, B, b, Evoral::Beats::PPQN - 1); + PRINT_RESULT((*m), "+", bbt_add, a, B, b, Evoral::Beats::PPQN - 2); + + PRINT_RESULT((*m), "+", bbt_add, b1, B, b, 0); + PRINT_RESULT((*m), "+", bbt_add, b1, B, b, 1); + PRINT_RESULT((*m), "+", bbt_add, b1, B, b, Evoral::Beats::PPQN/2); + PRINT_RESULT((*m), "+", bbt_add, b1, B, b, Evoral::Beats::PPQN); + PRINT_RESULT((*m), "+", bbt_add, b1, B, b, Evoral::Beats::PPQN - 1); + PRINT_RESULT((*m), "+", bbt_add, b1, B, b, Evoral::Beats::PPQN - 2); + + PRINT_RESULT((*m), "+", bbt_add, n1, B, b, 0); + PRINT_RESULT((*m), "+", bbt_add, n1, B, b, 1); + PRINT_RESULT((*m), "+", bbt_add, n1, B, b, Evoral::Beats::PPQN/2); + PRINT_RESULT((*m), "+", bbt_add, n1, B, b, Evoral::Beats::PPQN); + PRINT_RESULT((*m), "+", bbt_add, n1, B, b, Evoral::Beats::PPQN - 1); + PRINT_RESULT((*m), "+", bbt_add, n1, B, b, Evoral::Beats::PPQN - 2); + } + } + + for (int B = 1; B < 4; ++B) { + for (int b = 1; b < 13; ++b) { + PRINT_RESULT ((*m), "-", bbt_subtract, a, B, b, 0); + PRINT_RESULT ((*m), "-", bbt_subtract, a, B, b, 1); + PRINT_RESULT ((*m), "-", bbt_subtract, a, B, b, Evoral::Beats::PPQN/2); + PRINT_RESULT ((*m), "-", bbt_subtract, a, B, b, Evoral::Beats::PPQN); + PRINT_RESULT ((*m), "-", bbt_subtract, a, B, b, Evoral::Beats::PPQN - 1); + PRINT_RESULT ((*m), "-", bbt_subtract, a, B, b, Evoral::Beats::PPQN - 2); + } + } + } +} + +int +main () +{ + TempoMap tmap (Tempo (140), Meter (4,4), SAMPLERATE); + + //test_bbt_math (); + //return 0; + + tmap.set_tempo (Tempo (7), SECONDS_TO_SUPERCLOCK(7)); + tmap.set_tempo (Tempo (23), SECONDS_TO_SUPERCLOCK(23)); + tmap.set_tempo (Tempo (24), SECONDS_TO_SUPERCLOCK(24), true); + tmap.set_tempo (Tempo (40), SECONDS_TO_SUPERCLOCK(28), true); + tmap.set_tempo (Tempo (100), SECONDS_TO_SUPERCLOCK(100)); + tmap.set_tempo (Tempo (123), SECONDS_TO_SUPERCLOCK(23)); + + tmap.set_meter (Meter (3, 4), SECONDS_TO_SUPERCLOCK(23)); + tmap.set_meter (Meter (5, 8), SECONDS_TO_SUPERCLOCK(100)); + tmap.set_meter (Meter (5, 7), SECONDS_TO_SUPERCLOCK(7)); + tmap.set_meter (Meter (4, 4), SECONDS_TO_SUPERCLOCK(24)); + tmap.set_meter (Meter (11, 7), SECONDS_TO_SUPERCLOCK(23)); + + tmap.set_meter (Meter (3, 8), Timecode::BBT_Time (17, 1, 0)); + + tmap.rebuild (SECONDS_TO_SUPERCLOCK (120)); + tmap.dump (std::cout); + + return 0; +} + +std::ostream& +operator<<(std::ostream& str, Meter const & m) +{ + return str << m.divisions_per_bar() << '/' << m.note_value(); +} + +std::ostream& +operator<<(std::ostream& str, Tempo const & t) +{ + return str << t.note_types_per_minute() << " 1/" << t.note_type() << " notes per minute (" << t.superclocks_per_note_type() << " sc-per-1/" << t.note_type() << ')'; +} + +std::ostream& +operator<<(std::ostream& str, TempoMapPoint const & tmp) +{ + str << '@' << std::setw (12) << tmp.sclock() << ' ' << tmp.sclock() / (double) superclock_ticks_per_second + << (tmp.is_explicit() ? " EXP" : " imp") + << " qn " << tmp.quarters () + << " bbt " << tmp.bbt() + << " lock to " << tmp.lock_style() + ; + + if (tmp.is_explicit()) { + str << " tempo " << *((Tempo*) &tmp.metric()) + << " meter " << *((Meter*) &tmp.metric()) + ; + } + + if (tmp.is_explicit() && tmp.ramped()) { + str << " ramp c/sc = " << tmp.metric().c_per_superclock() << " c/qn " << tmp.metric().c_per_quarter(); + } + return str; +} diff --git a/nutemp/t.h b/nutemp/t.h new file mode 100644 index 0000000000..645893f51c --- /dev/null +++ b/nutemp/t.h @@ -0,0 +1,402 @@ +#ifndef __ardour_tempo_h__ +#define __ardour_tempo_h__ + +#include +#include +#include +#include +#include + +#include + +#include "evoral/Beats.hpp" + +#include "ardour/ardour.h" +#include "ardour/superclock.h" + +#include "timecode/bbt_time.h" + +namespace ARDOUR { + +class Meter; +class TempoMap; + +/** Tempo, the speed at which musical time progresses (BPM). + */ + +class LIBARDOUR_API Tempo { + public: + /** + * @param npm Note Types per minute + * @param type Note Type (default `4': quarter note) + */ + Tempo (double npm, int type = 4) : _superclocks_per_note_type (double_npm_to_sc (npm)), _note_type (type) {} + + /* these two methods should only be used to show and collect information to the user (for whom + * bpm as a floating point number is the obvious representation) + */ + double note_types_per_minute () const { return (superclock_ticks_per_second * 60.0) / _superclocks_per_note_type; } + void set_note_types_per_minute (double npm) { _superclocks_per_note_type = double_npm_to_sc (npm); } + + int note_type () const { return _note_type; } + + superclock_t superclocks_per_note_type () const { + return _superclocks_per_note_type; + } + superclock_t superclocks_per_note_type (int note_type) const { + return (_superclocks_per_note_type * _note_type) / note_type; + } + superclock_t superclocks_per_quarter_note () const { + return superclocks_per_note_type (4); + } + + Tempo& operator=(Tempo const& other) { + if (&other != this) { + _superclocks_per_note_type = other._superclocks_per_note_type; + _note_type = other._note_type; + } + return *this; + } + + protected: + superclock_t _superclocks_per_note_type; + int8_t _note_type; + + static inline double sc_to_double_npm (superclock_t sc) { return (superclock_ticks_per_second * 60.0) / sc; } + static inline superclock_t double_npm_to_sc (double npm) { return llrint ((superclock_ticks_per_second / npm) * 60.0); } +}; + +/** Meter, or time signature (subdivisions per bar, and which note type is a single subdivision). */ +class LIBARDOUR_API Meter { + public: + Meter (int8_t dpb, int8_t nv) : _note_value (nv), _divisions_per_bar (dpb) {} + + int divisions_per_bar () const { return _divisions_per_bar; } + int note_value() const { return _note_value; } + + inline bool operator==(const Meter& other) { return _divisions_per_bar == other.divisions_per_bar() && _note_value == other.note_value(); } + inline bool operator!=(const Meter& other) { return _divisions_per_bar != other.divisions_per_bar() || _note_value != other.note_value(); } + + Meter& operator=(Meter const & other) { + if (&other != this) { + _divisions_per_bar = other._divisions_per_bar; + _note_value = other._note_value; + } + return *this; + } + + Timecode::BBT_Time bbt_add (Timecode::BBT_Time const & bbt, Timecode::BBT_Offset const & add) const; + Timecode::BBT_Time bbt_subtract (Timecode::BBT_Time const & bbt, Timecode::BBT_Offset const & sub) const; + Timecode::BBT_Offset bbt_delta (Timecode::BBT_Time const & bbt, Timecode::BBT_Time const & sub) const; + + Timecode::BBT_Time round_up_to_bar (Timecode::BBT_Time const &) const; + Timecode::BBT_Time round_down_to_bar (Timecode::BBT_Time const &) const; + Timecode::BBT_Time round_to_bar (Timecode::BBT_Time const &) const; + + Evoral::Beats to_quarters (Timecode::BBT_Offset const &) const; + + protected: + /** The type of "note" that a division represents. For example, 4 is + a quarter (crotchet) note, 8 is an eighth (quaver) note, etc. + */ + int8_t _note_value; + /* how many of '_note_value' make up a bar or measure */ + int8_t _divisions_per_bar; +}; + +/** Helper class to keep track of the Meter *AND* Tempo in effect + at a given point in time. +*/ +class LIBARDOUR_API TempoMetric : public Tempo, public Meter { + public: + TempoMetric (Tempo const & t, Meter const & m, bool ramp) : Tempo (t), Meter (m), _c_per_quarter (0.0), _c_per_superclock (0.0), _ramped (ramp) {} + ~TempoMetric () {} + + double c_per_superclock () const { return _c_per_superclock; } + double c_per_quarter () const { return _c_per_quarter; } + + void compute_c_superclock (framecnt_t sr, superclock_t end_superclocks_per_note_type, superclock_t duration); + void compute_c_quarters (framecnt_t sr, superclock_t end_superclocks_per_note_type, Evoral::Beats const & duration); + + superclock_t superclocks_per_bar (framecnt_t sr) const; + superclock_t superclocks_per_grid (framecnt_t sr) const; + + superclock_t superclock_at_qn (Evoral::Beats const & qn) const; + superclock_t superclock_per_note_type_at_superclock (superclock_t) const; + + bool ramped () const { return _ramped; } + void set_ramped (bool yn) { _ramped = yn; } /* caller must mark something dirty to force recompute */ + + private: + double _c_per_quarter; + double _c_per_superclock; + bool _ramped; +}; + +/** Tempo Map - mapping of timecode to musical time. + * convert audio-samples, sample-rate to Bar/Beat/Tick, Meter/Tempo + */ + +/* TempoMap concepts + + we have several different ways of talking about time: + + * PULSE : whole notes, just because. These are linearly related to any other + note type, so if you know a number of pulses (whole notes), you + know the corresponding number of any other note type (e.g. quarter + notes). + + * QUARTER NOTES : just what the name says. A lot of MIDI software and + concepts assume that a "beat" is a quarter-note. + + * BEAT : a fraction of a PULSE. Defined by the meter in effect, so requires + meter (time signature) information to convert to/from PULSE or QUARTER NOTES. + In a 5/8 time, a BEAT is 1/8th note. In a 4/4 time, a beat is quarter note. + This means that measuring time in BEATS is potentially non-linear (if + the time signature changes, there will be a different number of BEATS + corresponding to a given time in any other unit). + + * SUPERCLOCK : a very high resolution clock whose frequency + has as factors all common sample rates and all common note + type divisors. Related to MINUTES or SAMPLES only when a + sample rate is known. Related to PULSE or QUARTER NOTES only + when a tempo is known. + + * MINUTES : wallclock time measurement. related to SAMPLES or SUPERCLOCK + only when a sample rate is known. + + + * SAMPLES : audio time measurement. Related to MINUTES or SUPERCLOCK only + when a sample rate is known + + * BBT : bars|beats|ticks ... linearly related to BEATS but with the added + semantics of bars ("measures") added, in which beats are broken up + into groups of bars ("measures"). Requires meter (time signature) + information to compute to/from a given BEATS value. Contains no + additional time information compared to BEATS, but does have + additional semantic information. + + Nick sez: not every note onset is on a tick + Paul wonders: if it's 8 samples off, does it matter? + Nick sez: it should not phase with existing audio + + */ + +class LIBARDOUR_API TempoMapPoint +{ + public: + enum Flag { + ExplicitTempo = 0x1, + ExplicitMeter = 0x2, + }; + + TempoMapPoint (Flag f, Tempo const& t, Meter const& m, superclock_t sc, Evoral::Beats const & q, Timecode::BBT_Time const & bbt, PositionLockStyle psl, bool ramp = false) + : _flags (f), _explicit (t, m, psl, ramp), _sclock (sc), _quarters (q), _bbt (bbt), _dirty (true) {} + TempoMapPoint (TempoMapPoint const & tmp, superclock_t sc, Evoral::Beats const & q, Timecode::BBT_Time const & bbt) + : _flags (Flag (0)), _reference (&tmp), _sclock (sc), _quarters (q), _bbt (bbt), _dirty (true) {} + ~TempoMapPoint () {} + + bool is_explicit() const { return _flags != Flag (0); } + bool is_implicit() const { return _flags == Flag (0); } + + superclock_t superclocks_per_note_type (int8_t note_type) const { + if (is_explicit()) { + return _explicit.metric.superclocks_per_note_type (note_type); + } + return _reference->superclocks_per_note_type (note_type); + } + + struct BadTempoMetricLookup : public std::exception { + virtual const char* what() const throw() { return "cannot obtain non-const Metric from implicit map point"; } + }; + + bool dirty() const { return _dirty; } + + superclock_t sclock() const { return _sclock; } + Evoral::Beats const & quarters() const { return _quarters; } + Timecode::BBT_Time const & bbt() const { return _bbt; } + bool ramped() const { return metric().ramped(); } + TempoMetric const & metric() const { return is_explicit() ? _explicit.metric : _reference->metric(); } + /* Implicit points are not allowed to return non-const references to their reference metric */ + TempoMetric & metric() { if (is_explicit()) { return _explicit.metric; } throw BadTempoMetricLookup(); } + PositionLockStyle lock_style() const { return is_explicit() ? _explicit.lock_style : _reference->lock_style(); } + + /* None of these properties can be set for an Implicit point, because + * they are determined by the TempoMapPoint pointed to by _reference. + */ + + void set_sclock (superclock_t sc) { if (is_explicit()) { _sclock = sc; _dirty = true; } } + void set_quarters (Evoral::Beats const & q) { if (is_explicit()) { _quarters = q; _dirty = true; } } + void set_bbt (Timecode::BBT_Time const & bbt) { if (is_explicit()) { _bbt = bbt; _dirty = true; } } + void set_dirty (bool yn) { if (is_explicit()) { _dirty = yn; } } + void set_lock_style (PositionLockStyle psl) { if (is_explicit()) { _explicit.lock_style = psl; _dirty = true; } } + + void make_explicit (Flag f) { + _flags = Flag (_flags|f); + /* since _metric and _reference are part of an anonymous union, + avoid possible compiler glitches by copying to a stack + variable first, then assign. + */ + TempoMetric tm (_explicit.metric); + _explicit.metric = tm; + _dirty = true; + } + + void make_implicit (TempoMapPoint & tmp) { _flags = Flag (0); _reference = &tmp; } + + Evoral::Beats quarters_at (superclock_t sc) const; + Evoral::Beats quarters_at (Timecode::BBT_Time const &) const; + + Timecode::BBT_Time bbt_at (Evoral::Beats const &) const; + +#if 0 + XMLNode& get_state() const; + int set_state (XMLNode const&, int version); +#endif + + struct SuperClockComparator { + bool operator() (TempoMapPoint const & a, TempoMapPoint const & b) const { return a.sclock() < b.sclock(); } + }; + + struct QuarterComparator { + bool operator() (TempoMapPoint const & a, TempoMapPoint const & b) const { return a.quarters() < b.quarters(); } + }; + + struct BBTComparator { + bool operator() (TempoMapPoint const & a, TempoMapPoint const & b) const { return a.bbt() < b.bbt(); } + }; + + protected: + friend class TempoMap; + void map_reset_set_sclock_for_sr_change (superclock_t sc) { _sclock = sc; } + + private: + struct ExplicitInfo { + ExplicitInfo (Tempo const & t, Meter const & m, PositionLockStyle psl, bool ramp) : metric (t, m, ramp), lock_style (psl) {} + + TempoMetric metric; + PositionLockStyle lock_style; + }; + + Flag _flags; + union { + TempoMapPoint const * _reference; + ExplicitInfo _explicit; + }; + superclock_t _sclock; + Evoral::Beats _quarters; + Timecode::BBT_Time _bbt; + bool _dirty; +}; + +typedef std::list TempoMapPoints; + +class LIBARDOUR_API TempoMap +{ + public: + TempoMap (Tempo const & initial_tempo, Meter const & initial_meter, framecnt_t sr); + + void set_sample_rate (framecnt_t sr); + framecnt_t sample_rate() const { return _sample_rate; } + + void remove_explicit_point (superclock_t); + + void move_implicit (superclock_t current, superclock_t destination); + void move_explicit (superclock_t current, superclock_t destination); + + //bool set_tempo_at (Tempo const &, Evoral::Beats const &, PositionLockStyle psl, bool ramp = false); + bool set_tempo (Tempo const &, Timecode::BBT_Time const &, bool ramp = false); + bool set_tempo (Tempo const &, superclock_t, bool ramp = false); + + //bool set_meter_at (Meter const &, Evoral::Beats const &); + + bool set_meter (Meter const &, Timecode::BBT_Time const &); + bool set_meter (Meter const &, superclock_t); + + Meter const & meter_at (superclock_t sc) const; + Meter const & meter_at (Evoral::Beats const & b) const; + Meter const & meter_at (Timecode::BBT_Time const & bbt) const; + Tempo const & tempo_at (superclock_t sc) const; + Tempo const & tempo_at (Evoral::Beats const &b) const; + Tempo const & tempo_at (Timecode::BBT_Time const & bbt) const; + + Timecode::BBT_Time bbt_at (superclock_t sc) const; + Timecode::BBT_Time bbt_at (Evoral::Beats const &) const; + Evoral::Beats quarter_note_at (superclock_t sc) const; + Evoral::Beats quarter_note_at (Timecode::BBT_Time const &) const; + superclock_t superclock_at (Evoral::Beats const &) const; + superclock_t superclock_at (Timecode::BBT_Time const &) const; + + struct EmptyTempoMapException : public std::exception { + virtual const char* what() const throw() { return "TempoMap is empty"; } + }; + + void dump (std::ostream&); + void rebuild (superclock_t limit); + + private: + TempoMapPoints _points; + framecnt_t _sample_rate; + mutable Glib::Threads::RWLock _lock; + + /* these return an iterator that refers to the TempoMapPoint at or most immediately preceding the given position. + * + * Conceptually, these could be const methods, but C++ prevents them returning a non-const iterator in that case. + * + * Note that they cannot return an invalid iterator (e.g. _points.end()) because: + * + * - if the map is empty, an exception is thrown + * - if the given time is before the first map entry, _points.begin() is returned + * - if the given time is after the last map entry, the equivalent of _points.rbegin() is returned + * - if the given time is within the map entries, a valid iterator will be returned + */ + + TempoMapPoints::iterator iterator_at (superclock_t sc); + TempoMapPoints::iterator iterator_at (Evoral::Beats const &); + TempoMapPoints::iterator iterator_at (Timecode::BBT_Time const &); + + TempoMapPoints::const_iterator const_iterator_at (superclock_t sc) const { return const_cast(this)->iterator_at (sc); } + TempoMapPoints::const_iterator const_iterator_at (Evoral::Beats const & b) const { return const_cast(this)->iterator_at (b); } + TempoMapPoints::const_iterator const_iterator_at (Timecode::BBT_Time const & bbt) const { return const_cast(this)->iterator_at (bbt); } + + /* Returns the TempoMapPoint at or most immediately preceding the given time. If the given time is + * before the first map entry, then the first map entry will be returned, which underlies the semantics + * that the first map entry's values propagate backwards in time if not at absolute zero. + * + * As for iterator_at(), define both const+const and non-const variants, because C++ won't let us return a non-const iterator + from a const method (which is a bit silly, but presumably aids compiler reasoning). + */ + + TempoMapPoint & point_at (superclock_t sc) { return *iterator_at (sc); } + TempoMapPoint & point_at (Evoral::Beats const & b) { return *iterator_at (b); } + TempoMapPoint & point_at (Timecode::BBT_Time const & bbt) { return *iterator_at (bbt); } + + TempoMapPoint const & const_point_at (superclock_t sc) const { return *const_iterator_at (sc); } + TempoMapPoint const & const_point_at (Evoral::Beats const & b) const { return *const_iterator_at (b); } + TempoMapPoint const & const_point_at (Timecode::BBT_Time const & bbt) const { return *const_iterator_at (bbt); } + + Meter const & meter_at_locked (superclock_t sc) const { return const_point_at (sc).metric(); } + Meter const & meter_at_locked (Evoral::Beats const & b) const { return const_point_at (b).metric(); } + Meter const & meter_at_locked (Timecode::BBT_Time const & bbt) const { return const_point_at (bbt).metric(); } + Tempo const & tempo_at_locked (superclock_t sc) const { return const_point_at (sc).metric(); } + Tempo const & tempo_at_locked (Evoral::Beats const &b) const { return const_point_at (b).metric(); } + Tempo const & tempo_at_locked (Timecode::BBT_Time const & bbt) const { return const_point_at (bbt).metric(); } + Timecode::BBT_Time bbt_at_locked (superclock_t sc) const; + Timecode::BBT_Time bbt_at_locked (Evoral::Beats const &) const; + Evoral::Beats quarter_note_at_locked (superclock_t sc) const; + Evoral::Beats quarter_note_at_locked (Timecode::BBT_Time const &) const; + superclock_t superclock_at_locked (Evoral::Beats const &) const; + superclock_t superclock_at_locked (Timecode::BBT_Time const &) const; + + void move_explicit_to (TempoMapPoints::iterator, superclock_t destination); + + void rebuild_locked (superclock_t limit); +}; + +} + +std::ostream& operator<<(std::ostream&, ARDOUR::TempoMapPoint const &); +std::ostream& operator<<(std::ostream&, ARDOUR::Tempo const &); +std::ostream& operator<<(std::ostream&, ARDOUR::Meter const &); + +#endif /* __ardour_tempo_h__ */ -- cgit v1.2.3