redesign chasing the transport master

Substantive comments associated with code in Session::plan_master_strategy.

Known not to work for reverse TC. Also, the JACK related code has not yet been tested
This commit is contained in:
Paul Davis 2020-03-20 17:38:23 -06:00
parent e6e0edbe90
commit 8296a030a5
5 changed files with 355 additions and 81 deletions

View File

@ -775,7 +775,7 @@ public:
bool transport_stopped_or_stopping() const;
bool transport_rolling() const;
bool transport_will_roll_forwards() const;
bool silent () { return _silent; }
bool punch_is_possible () const;
@ -1369,7 +1369,34 @@ private:
};
samplepos_t master_wait_end;
void track_transport_master (float slave_speed, samplepos_t slave_transport_sample);
enum TransportMasterAction {
TransportMasterRelax,
TransportMasterNoRoll,
TransportMasterLocate,
TransportMasterStart,
TransportMasterStop,
TransportMasterWait,
};
struct TransportMasterStrategy {
TransportMasterAction action;
samplepos_t target;
LocateTransportDisposition roll_disposition;
double catch_speed;
TransportMasterStrategy ()
: action (TransportMasterRelax)
, target (0)
, roll_disposition (MustStop)
, catch_speed (0.) {}
};
TransportMasterStrategy transport_master_strategy;
double plan_master_strategy (pframes_t nframes, double master_speed, samplepos_t master_transport_sample, double catch_speed);
double plan_master_strategy_engine (pframes_t nframes, double master_speed, samplepos_t master_transport_sample, double catch_speed);
bool implement_master_strategy ();
bool follow_transport_master (pframes_t nframes);
void sync_source_changed (SyncSource, samplepos_t pos, pframes_t cycle_nframes);

View File

@ -421,9 +421,11 @@ AudioEngine::process_callback (pframes_t nframes)
}
if (!_freewheeling || Freewheel.empty()) {
const double engine_speed = tmm.pre_process_transport_masters (nframes, sample_time_at_cycle_start());
double engine_speed = tmm.pre_process_transport_masters (nframes, sample_time_at_cycle_start());
engine_speed = _session->plan_master_strategy (nframes, tmm.get_current_position_in_process_context(), tmm.get_current_position_in_process_context(), engine_speed);
Port::set_speed_ratio (engine_speed);
DEBUG_TRACE (DEBUG::Slave, string_compose ("transport master (current=%1) gives speed %2 (ports using %3)\n", tmm.current() ? tmm.current()->name() : string("[]"), engine_speed, Port::speed_ratio()));
#if 0 // USE FOR DEBUG ONLY
/* use with Dummy backend, engine pulse and
* scripts/_find_nonzero_sample.lua

View File

@ -503,7 +503,8 @@ Session::process_with_events (pframes_t nframes)
}
if (!_exporting && config.get_external_sync()) {
if (!follow_transport_master (nframes)) {
if (!implement_master_strategy ()) {
no_roll (nframes);
ltc_tx_send_time_code_for_cycle (_transport_sample, end_sample, _target_transport_speed, _transport_speed, nframes);
return;
}
@ -641,7 +642,8 @@ Session::process_without_events (pframes_t nframes)
}
if (!_exporting && config.get_external_sync()) {
if (!follow_transport_master (nframes)) {
if (!implement_master_strategy ()) {
no_roll (nframes);
ltc_tx_send_time_code_for_cycle (_transport_sample, _transport_sample, 0, 0 , nframes);
return;
}
@ -1098,123 +1100,364 @@ Session::emit_thread_run ()
pthread_mutex_unlock (&_rt_emit_mutex);
}
bool
Session::follow_transport_master (pframes_t nframes)
double
Session::plan_master_strategy_engine (pframes_t nframes, double master_speed, samplepos_t master_transport_sample, double catch_speed)
{
/* JACK Transport. */
TransportMasterManager& tmm (TransportMasterManager::instance());
sampleoffset_t delta = _transport_sample - master_transport_sample;
double master_speed;
samplepos_t master_transport_sample;
sampleoffset_t delta;
if (master_speed == 0) {
if (tmm.master_invalid_this_cycle()) {
DEBUG_TRACE (DEBUG::Slave, "session told not to use the transport master this cycle\n");
goto noroll;
}
if (!actively_recording()) {
master_speed = tmm.get_current_speed_in_process_context();
master_transport_sample = tmm.get_current_position_in_process_context ();
delta = _transport_sample - master_transport_sample;
const samplecnt_t wlp = worst_latency_preroll_buffer_size_ceil ();
DEBUG_TRACE (DEBUG::Slave, string_compose ("session at %1, master at %2, delta: %3 res: %4 TFSM state %5\n", _transport_sample, master_transport_sample, delta, tmm.current()->resolution(), _transport_fsm->current_state()));
if (delta != wlp) {
if (tmm.current()->type() == Engine) {
/* if we're not aligned with the current JACK * time, then jump to it */
/* JACK Transport. */
if (!locate_pending() && !declick_in_progress() && !tmm.current()->starting()) {
if (master_speed == 0) {
const samplepos_t locate_target = master_transport_sample + wlp;
DEBUG_TRACE (DEBUG::Slave, string_compose ("JACK transport: jump to master position %1 by locating to %2\n", master_transport_sample, locate_target));
/* for JACK transport always stop after the locate (2nd argument == false) */
TFSM_LOCATE (locate_target, MustStop, true, false, false);
if (!actively_recording()) {
const samplecnt_t wlp = worst_latency_preroll_buffer_size_ceil ();
if (delta != wlp) {
/* if we're not aligned with the current JACK * time, then jump to it */
if (!locate_pending() && !declick_in_progress() && !tmm.current()->starting()) {
const samplepos_t locate_target = master_transport_sample + wlp;
DEBUG_TRACE (DEBUG::Slave, string_compose ("JACK transport: jump to master position %1 by locating to %2\n", master_transport_sample, locate_target));
/* for JACK transport always stop after the locate (2nd argument == false) */
TFSM_LOCATE (locate_target, MustStop, true, false, false);
} else {
DEBUG_TRACE (DEBUG::Slave, string_compose ("JACK Transport: locate already in process, sts = %1\n", master_transport_sample));
}
}
}
} else {
if (_transport_speed) {
/* master is rolling, and we're rolling ... with JACK we should always be perfectly in sync, so ... WTF? */
if (delta) {
if (remaining_latency_preroll() && worst_latency_preroll()) {
/* our transport position is not moving because we're doing latency alignment. Nothing in particular to do */
} else {
cerr << "\n\n\n IMPOSSIBLE! OUT OF SYNC WITH JACK TRANSPORT (rlp = " << remaining_latency_preroll() << " wlp " << worst_latency_preroll() << ")\n\n\n";
}
} else {
DEBUG_TRACE (DEBUG::Slave, string_compose ("JACK Transport: locate already in process, sts = %1\n", master_transport_sample));
}
}
}
} else {
/* This is a heuristic rather than a strictly provable rule. The idea
if (_transport_speed) {
/* master is rolling, and we're rolling ... with JACK we should always be perfectly in sync, so ... WTF? */
if (delta) {
if (remaining_latency_preroll() && worst_latency_preroll()) {
/* our transport position is not moving because we're doing latency alignment. Nothing in particular to do */
} else {
cerr << "\n\n\n IMPOSSIBLE! OUT OF SYNC WITH JACK TRANSPORT (rlp = " << remaining_latency_preroll() << " wlp " << worst_latency_preroll() << ")\n\n\n";
}
}
}
}
if (!locate_pending() && !declick_in_progress()) {
if (master_speed != 0.0) {
/* master rolling, we should be too */
if (_transport_speed == 0.0f) {
DEBUG_TRACE (DEBUG::Slave, string_compose ("slave starts transport: %1 sample %2 tf %3\n", master_speed, master_transport_sample, _transport_sample));
TFSM_EVENT (TransportFSM::StartTransport);
}
} else if (!tmm.current()->starting()) { /* master stopped, not in "starting" state */
if (_transport_speed != 0.0f) {
DEBUG_TRACE (DEBUG::Slave, string_compose ("slave stops transport: %1 sample %2 tf %3\n", master_speed, master_transport_sample, _transport_sample));
TFSM_STOP (false, false);
}
}
}
return catch_speed;
}
double
Session::plan_master_strategy (pframes_t nframes, double master_speed, samplepos_t master_transport_sample, double catch_speed)
{
/* This is called from inside AudioEngine::process_callback(),
* immediately after the TransportMasterManager has run its
* ::pre_process_transport_masters() method to allow all transport
* masters to update their information on the speed and position
* indicated by their data sources.
*
* Our task here is to determine what the Session should do during its
* process() call in order to respond to the transport master (or to
* not respond at all, if we're not using external sync). We want to
* set transport_master_strategy.action, which will be used from within
* the Session process() callback (via ::implement_master_strategy())
* to determine what, if anything to do there.
*
* The return value is the speed (aka "ratio") to be used by the port
* resampler. If we're not chasing the master, the correct answer will
* be 1.0. This can occur in a number of scenarios. If we are synced
* and locked to the master, we want to use the "catch speed" given to
* us as a parameter. This was determined by the
* TransportMasterManager as the correct speed to use in order to
* reduce the delta between the master's position and the session
* transport position.
*
* In situations where we are not synced+locked, either temporarily or
* longer term, we return 1.0, which leads to no resampling, and the
* session will run at normal speed.
*/
if (!config.get_external_sync()) {
return 1.0;
}
TransportMasterManager& tmm (TransportMasterManager::instance());
const samplecnt_t locate_threshold = 5 * current_block_size;
if (tmm.master_invalid_this_cycle()) {
DEBUG_TRACE (DEBUG::Slave, "session told not to use the transport master this cycle\n");
transport_master_strategy.action = TransportMasterNoRoll;
return 1.0;
}
if (tmm.current()->type() == Engine) {
/* JACK is fundamentally different */
return plan_master_strategy_engine (nframes, master_speed, master_transport_sample, catch_speed);
}
const sampleoffset_t delta = _transport_sample - master_transport_sample;
DEBUG_TRACE (DEBUG::Slave, string_compose ("\n\n\n\nsession at %1, master at %2, delta: %3 res: %4 TFSM state %5 action %6\n", _transport_sample, master_transport_sample, delta, tmm.current()->resolution(), _transport_fsm->current_state(), transport_master_strategy.action));
const bool interesting_transport_state_change_underway = (locate_pending() || declick_in_progress());
if ((transport_master_strategy.action == TransportMasterWait) || (transport_master_strategy.action == TransportMasterNoRoll)) {
/* We've either been:
*
* 1) waiting for the master to catch up with a position that
* we located to (Wait)
* 2) waiting to be able to use the master's speed & position
*
* The two cases are very similar, but differ in the conditions
* under which we need to initiate a (possibly successive)
* locate in response to the master's position
*
* This code is very similar to the non-wait case (the "else"
* that ends this scope). The big difference is that here we
* know that we've just finished a locate specifically in order
* to catch the master. This changes the logic a little bit.
*/
DEBUG_TRACE (DEBUG::Slave, "had been waiting for locate-to-catch-master to finish\n");
if (interesting_transport_state_change_underway) {
/* still waiting for the declick and/or locate to
finish ... nothing to do for now.
*/
DEBUG_TRACE (DEBUG::Slave, "still waiting for the locate to finish\n");
return 1.0;
}
const samplecnt_t wlp = worst_latency_preroll_buffer_size_ceil ();
bool should_locate;
if (transport_master_strategy.action == TransportMasterNoRoll) {
/* We've been waiting to be able to use the master's
* position (i.e to get a lock on the incoming data
* stream). We need to locate if we're either ahead or
* behind the master by <threshold>.
*/
should_locate = abs (delta) > locate_threshold;
} else {
/* we located to be ahead of the master's position (see
* the locate call in the next "else" scope where we
* jump ahead by a significant distance).
*
* So, we should be ahead (or behind) the master's
* position, and waiting for it to get close to us.
*
* We only need to locate again if we are actually
* behind (or ahead, for reverse motion) of the master
* by more than <threshold>.
*/
should_locate = delta < 0 && (abs (delta) > locate_threshold);
}
if (should_locate) {
/* we're too far from the master to catch it via
* varispeed ... need to locate ahead of it, wait for
* it to get cose to us, then varispeed to sync.
*
* We assume that the transport state after the locate
* is always Stopped - we don't restart the transport
* until the master catches us, or at least gets close
* to our new position.
*
* Any time we locate, we need to reset the DLL used by
* the TransportMasterManager. Do that here, since the
* TMM will not need that again until after we start
* the locate (and hence the apparent transport
* position of the Session will reflect the target we
* set here). That is because the locate will be
* initiated in the Session::process() callback that is
* about to happen right after we return.
*/
tmm.reinit (master_speed, master_transport_sample);
samplepos_t locate_target = master_transport_sample;
locate_target += wlp + lrintf (ntracks() * sample_rate() * 0.05);
DEBUG_TRACE (DEBUG::Slave, string_compose ("After locate-to-catch-master, still too far off (%1). Locate again to %2\n", delta, locate_target));
transport_master_strategy.action = TransportMasterLocate;
transport_master_strategy.target = locate_target;
transport_master_strategy.roll_disposition = MustStop;
transport_master_strategy.catch_speed = catch_speed;
return 1.0;
}
if (delta > wlp) {
/* We're close, but haven't reached the point where we
* need to start rolling for preroll latency yet.
*/
DEBUG_TRACE (DEBUG::Slave, string_compose ("master @ %1 is not yet within %2 of our position %3 (delta is %4)\n", master_transport_sample, wlp, _transport_sample, delta));
return 1.0;
}
/* case #3: we should start rolling */
DEBUG_TRACE (DEBUG::Slave, string_compose ("master @ %1 is WITHIN %2 of our position %3 (delta is %4), so start\n", master_transport_sample, wlp, _transport_sample, delta));
transport_master_strategy.action = TransportMasterStart;
transport_master_strategy.catch_speed = catch_speed;
return catch_speed;
}
/* currently we're not waiting to sync with the master. So
* check if we're way out of alignment (case #1) or just a bit
* out of alignment (case #2)
*/
if (abs (delta) > locate_threshold) {
/* CASE ONE
*
* This is a heuristic rather than a strictly provable rule. The idea
* is that if we're "far away" from the master, we should locate to its
* current position, and then varispeed to sync with it.
*
* On the other hand, if we're close to it, just varispeed.
*/
if (!actively_recording() && abs (delta) > (5 * current_block_size)) {
tmm.reinit (master_speed, master_transport_sample);
if (!locate_pending() && !declick_in_progress()) {
DEBUG_TRACE (DEBUG::Slave, string_compose ("request locate to master position %1\n", master_transport_sample));
/* note that for non-JACK transport masters, we assume that the transport state (rolling,stopped) after the locate
* remains unchanged (2nd argument, "roll-after-locate")
*/
tmm.reinit (master_speed, master_transport_sample);
TFSM_LOCATE (master_transport_sample, (master_speed != 0) ? MustRoll : MustStop, true, false, false);
}
samplepos_t locate_target = master_transport_sample;
return true;
locate_target += lrintf (ntracks() * sample_rate() * 0.05);
DEBUG_TRACE (DEBUG::Slave, string_compose ("request locate to master position %1\n", locate_target));
transport_master_strategy.action = TransportMasterLocate;
transport_master_strategy.target = locate_target;
transport_master_strategy.roll_disposition = (master_speed != 0) ? MustRoll : MustStop;
transport_master_strategy.catch_speed = catch_speed;
/* Session::process_with(out)_events() will take this
* up when called.
*/
return 1.0;
} else if (abs (delta) > tmm.current()->resolution()) {
/* CASE TWO
*
* If we're close, but not within the resolution of the
* master, just varispeed to chase the master, and be
* silent till we're synced
*/
tmm.block_disk_output ();
} else {
/* speed is set, we're locked and synced and good to go */
if (!locate_pending() && !declick_in_progress()) {
DEBUG_TRACE (DEBUG::Slave, "master/slave synced & locked\n");
tmm.unblock_disk_output ();
}
}
if (master_speed != 0.0) {
/* master rolling, we should be too */
if (_transport_speed == 0.0f) {
DEBUG_TRACE (DEBUG::Slave, string_compose ("slave starts transport: %1 sample %2 tf %3\n", master_speed, master_transport_sample, _transport_sample));
TFSM_EVENT (TransportFSM::StartTransport);
transport_master_strategy.action = TransportMasterStart;
transport_master_strategy.catch_speed = catch_speed;
return catch_speed;
}
} else if (!tmm.current()->starting()) { /* master stopped, not in "starting" state */
if (_transport_speed != 0.0f) {
DEBUG_TRACE (DEBUG::Slave, string_compose ("slave stops transport: %1 sample %2 tf %3\n", master_speed, master_transport_sample, _transport_sample));
TFSM_STOP (false, false);
transport_master_strategy.action = TransportMasterStop;
return catch_speed;
}
}
/* This is the second part of the "we're not synced yet" code. If we're
* close, but not within the resolution of the master, silence disk
* output but continue to varispeed to get in sync.
/* we were not waiting for the master, we're close enough to
* it, and our transport state already matched the master
* (stopped or rolling). We should just continue
* resampling/varispeeding at "catch_speed" in order to remain
* synced with the master.
*/
if ((tmm.current()->type() != Engine) && !actively_recording() && abs (delta) > tmm.current()->resolution()) {
/* just varispeed to chase the master, and be silent till we're synced */
tmm.block_disk_output ();
return true;
transport_master_strategy.action = TransportMasterRelax;
return catch_speed;
}
bool
Session::implement_master_strategy ()
{
/* This is called from within Session::process(), only if we are using
* external sync. The task here is simply to implement whatever actions
* where decided by ::plan_master_strategy (), from within the
* ::process() callback (the planning step is executed before
* Session::process() begins.
*/
DEBUG_TRACE (DEBUG::Slave, string_compose ("Implementing master strategy: %1\n", transport_master_strategy.action));
switch (transport_master_strategy.action) {
case TransportMasterNoRoll:
/* This is the one case where we do not want the session to
call ::roll() under any circumstances. Returning false here
will do that.
*/
return false;
case TransportMasterRelax:
break;
case TransportMasterWait:
break;
case TransportMasterLocate:
transport_master_strategy.action = TransportMasterWait;
TFSM_LOCATE(transport_master_strategy.target, transport_master_strategy.roll_disposition, true, false, false);
break;
case TransportMasterStart:
TFSM_EVENT (TransportFSM::StartTransport);
break;
case TransportMasterStop:
TFSM_STOP (false, false);
break;
}
/* speed is set, we're locked, and good to go */
tmm.unblock_disk_output ();
return true;
noroll:
/* don't move at all */
DEBUG_TRACE (DEBUG::Slave, "no roll\n")
no_roll (nframes);
return false;
}

View File

@ -133,7 +133,7 @@ TransportMaster::speed_and_position (double& speed, samplepos_t& pos, samplepos_
pos = last.position + (now - last.timestamp) * speed;
DEBUG_TRACE (DEBUG::Slave, string_compose ("%1 sync spd: %2 pos: %3 | last-pos: %4 @ %7| elapsed: %5 | speed: %6\n",
DEBUG_TRACE (DEBUG::Slave, string_compose ("%1 sync spd: %2 pos: %3 | last-pos: %4 @ %7 | elapsed: %5 | speed: %6\n",
name(), speed, pos, last.position, (now - last.timestamp), speed, when));
return true;

View File

@ -439,6 +439,8 @@ TransportMasterManager::set_current_locked (boost::shared_ptr<TransportMaster> c
master_dll_initstate = 0;
unblock_disk_output ();
DEBUG_TRACE (DEBUG::Slave, string_compose ("current transport master set to %1\n", (c ? c->name() : string ("none"))));
return 0;