From 8296a030a582bd620479907ab17afc8fdc92609a Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Fri, 20 Mar 2020 17:38:23 -0600 Subject: 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 --- libs/ardour/ardour/session.h | 31 ++- libs/ardour/audioengine.cc | 4 +- libs/ardour/session_process.cc | 381 ++++++++++++++++++++++++++------ libs/ardour/transport_master.cc | 2 +- libs/ardour/transport_master_manager.cc | 2 + 5 files changed, 347 insertions(+), 73 deletions(-) (limited to 'libs') diff --git a/libs/ardour/ardour/session.h b/libs/ardour/ardour/session.h index 3ab11aee91..c9b8c84798 100644 --- a/libs/ardour/ardour/session.h +++ b/libs/ardour/ardour/session.h @@ -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); diff --git a/libs/ardour/audioengine.cc b/libs/ardour/audioengine.cc index 7f4dbdfa5a..61f5ab201b 100644 --- a/libs/ardour/audioengine.cc +++ b/libs/ardour/audioengine.cc @@ -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 diff --git a/libs/ardour/session_process.cc b/libs/ardour/session_process.cc index b84b5c124c..c367ad858e 100644 --- a/libs/ardour/session_process.cc +++ b/libs/ardour/session_process.cc @@ -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()) { + + 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"; + } + } + } + } + + + 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); + } + } } - 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; + 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; + } - 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())); + 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); + } - /* JACK Transport. */ + const sampleoffset_t delta = _transport_sample - master_transport_sample; - if (master_speed == 0) { + 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)); - if (!actively_recording()) { + const bool interesting_transport_state_change_underway = (locate_pending() || declick_in_progress()); - const samplecnt_t wlp = worst_latency_preroll_buffer_size_ceil (); + if ((transport_master_strategy.action == TransportMasterWait) || (transport_master_strategy.action == TransportMasterNoRoll)) { - if (delta != wlp) { + /* 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. + */ - /* if we're not aligned with the current JACK * time, then jump to it */ + DEBUG_TRACE (DEBUG::Slave, "had been waiting for locate-to-catch-master to finish\n"); - if (!locate_pending() && !declick_in_progress() && !tmm.current()->starting()) { + 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 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); + const samplecnt_t wlp = worst_latency_preroll_buffer_size_ceil (); + bool should_locate; - } else { - DEBUG_TRACE (DEBUG::Slave, string_compose ("JACK Transport: locate already in process, sts = %1\n", master_transport_sample)); - } - } - } + 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 . + */ + + should_locate = abs (delta) > locate_threshold; } 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"; - } - } - } + /* 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 . + */ + + should_locate = delta < 0 && (abs (delta) > locate_threshold); } - } else { + 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; + } - /* This is a heuristic rather than a strictly provable rule. The idea + /* 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; + + 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 true; + 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; } diff --git a/libs/ardour/transport_master.cc b/libs/ardour/transport_master.cc index 739395efdb..c0a9e4e02c 100644 --- a/libs/ardour/transport_master.cc +++ b/libs/ardour/transport_master.cc @@ -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; diff --git a/libs/ardour/transport_master_manager.cc b/libs/ardour/transport_master_manager.cc index 0e55dbf76f..3b3923dc0e 100644 --- a/libs/ardour/transport_master_manager.cc +++ b/libs/ardour/transport_master_manager.cc @@ -439,6 +439,8 @@ TransportMasterManager::set_current_locked (boost::shared_ptr 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; -- cgit v1.2.3