goodbye USE_TRACKS_CODE_FEATURES and is_tracks_build
[ardour.git] / libs / ardour / session_transport.cc
index b69565f3849741d07b703062ff2e3eb15c72871b..dbda0aabb3a3ebe2e6be24443d9ca2d6e31901ab 100644 (file)
@@ -1,21 +1,28 @@
 /*
-    Copyright (C) 1999-2003 Paul Davis
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with this program; if not, write to the Free Software
-    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-
-*/
+ * Copyright (C) 1999-2019 Paul Davis <paul@linuxaudiosystems.com>
+ * Copyright (C) 2005-2009 Taybin Rutkin <taybin@taybin.com>
+ * Copyright (C) 2006-2007 Jesse Chappell <jesse@essej.net>
+ * Copyright (C) 2006-2012 David Robillard <d@drobilla.net>
+ * Copyright (C) 2007-2012 Carl Hetherington <carl@carlh.net>
+ * Copyright (C) 2008-2009 Hans Baier <hansfbaier@googlemail.com>
+ * Copyright (C) 2012-2019 Robin Gareus <robin@gareus.org>
+ * Copyright (C) 2014-2018 Ben Loftis <ben@harrisonconsoles.com>
+ * Copyright (C) 2015 GZharun <grygoriiz@wavesglobal.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
 
 #ifdef WAF_BUILD
 #include "libardour-config.h"
 #include <cerrno>
 #include <unistd.h>
 
-#include "pbd/undo.h"
+#include <boost/algorithm/string/erase.hpp>
+
 #include "pbd/error.h"
 #include "pbd/enumwriter.h"
-#include "pbd/pthread_utils.h"
+#include "pbd/i18n.h"
 #include "pbd/memento_command.h"
+#include "pbd/pthread_utils.h"
 #include "pbd/stacktrace.h"
+#include "pbd/undo.h"
 
 #include "midi++/mmc.h"
 #include "midi++/port.h"
 #include "ardour/profile.h"
 #include "ardour/scene_changer.h"
 #include "ardour/session.h"
-#include "ardour/slave.h"
+#include "ardour/transport_fsm.h"
+#include "ardour/transport_master.h"
+#include "ardour/transport_master_manager.h"
 #include "ardour/tempo.h"
 #include "ardour/operations.h"
 #include "ardour/vca.h"
 #include "ardour/vca_manager.h"
 
-#include "pbd/i18n.h"
-
 using namespace std;
 using namespace ARDOUR;
 using namespace PBD;
 
-void
-Session::add_post_transport_work (PostTransportWork ptw)
-{
-       PostTransportWork oldval;
-       PostTransportWork newval;
-       int tries = 0;
-
-       while (tries < 8) {
-               oldval = (PostTransportWork) g_atomic_int_get (&_post_transport_work);
-               newval = PostTransportWork (oldval | ptw);
-               if (g_atomic_int_compare_and_exchange (&_post_transport_work, oldval, newval)) {
-                       /* success */
-                       return;
-               }
-       }
-
-       error << "Could not set post transport work! Crazy thread madness, call the programmers" << endmsg;
-}
-
-void
-Session::request_sync_source (Slave* new_slave)
-{
-       SessionEvent* ev = new SessionEvent (SessionEvent::SetSyncSource, SessionEvent::Add, SessionEvent::Immediate, 0, 0.0);
-       bool seamless;
-
-       seamless = Config->get_seamless_loop ();
-
-       if (dynamic_cast<Engine_Slave*>(new_slave)) {
-               /* JACK cannot support seamless looping at present */
-               Config->set_seamless_loop (false);
-       } else {
-               /* reset to whatever the value was before we last switched slaves */
-               Config->set_seamless_loop (_was_seamless);
-       }
-
-       /* save value of seamless from before the switch */
-       _was_seamless = seamless;
-
-       ev->slave = new_slave;
-       DEBUG_TRACE (DEBUG::Slave, "sent request for new slave\n");
-       queue_event (ev);
-}
-
-void
-Session::request_transport_speed (double speed, bool as_default)
-{
-       SessionEvent* ev = new SessionEvent (SessionEvent::SetTransportSpeed, SessionEvent::Add, SessionEvent::Immediate, 0, speed);
-       ev->third_yes_or_no = as_default; // as_default
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request transport speed = %1 as default = %2\n", speed, as_default));
-       queue_event (ev);
-}
-
-/** Request a new transport speed, but if the speed parameter is exactly zero then use
- *  a very small +ve value to prevent the transport actually stopping.  This method should
- *  be used by callers who are varying transport speed but don't ever want to stop it.
- */
-void
-Session::request_transport_speed_nonzero (double speed, bool as_default)
-{
-       if (speed == 0) {
-               speed = DBL_EPSILON;
-       }
-
-       request_transport_speed (speed, as_default);
-}
-
-void
-Session::request_stop (bool abort, bool clear_state)
-{
-       SessionEvent* ev = new SessionEvent (SessionEvent::SetTransportSpeed, SessionEvent::Add, SessionEvent::Immediate, audible_sample(), 0.0, abort, clear_state);
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request transport stop, audible %3 transport %4 abort = %1, clear state = %2\n", abort, clear_state, audible_sample(), _transport_sample));
-       queue_event (ev);
-}
-
-void
-Session::request_locate (samplepos_t target_sample, bool with_roll)
-{
-       SessionEvent *ev = new SessionEvent (with_roll ? SessionEvent::LocateRoll : SessionEvent::Locate, SessionEvent::Add, SessionEvent::Immediate, target_sample, 0, false);
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request locate to %1\n", target_sample));
-       queue_event (ev);
-}
-
-void
-Session::force_locate (samplepos_t target_sample, bool with_roll)
-{
-       SessionEvent *ev = new SessionEvent (with_roll ? SessionEvent::LocateRoll : SessionEvent::Locate, SessionEvent::Add, SessionEvent::Immediate, target_sample, 0, true);
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request forced locate to %1\n", target_sample));
-       queue_event (ev);
-}
-
-void
-Session::unset_preroll_record_trim ()
-{
-       _preroll_record_trim_len = 0;
-}
-
-void
-Session::request_preroll_record_trim (samplepos_t rec_in, samplecnt_t preroll)
-{
-       if (actively_recording ()) {
-               return;
-       }
-       unset_preroll_record_trim ();
-
-       config.set_punch_in (false);
-       config.set_punch_out (false);
-
-       samplepos_t pos = std::max ((samplepos_t)0, rec_in - preroll);
-       _preroll_record_trim_len = preroll;
-       maybe_enable_record ();
-       request_locate (pos, true);
-       set_requested_return_sample (rec_in);
-}
-
-void
-Session::request_count_in_record ()
-{
-       if (actively_recording ()) {
-               return;
-       }
-       if (transport_rolling()) {
-               return;
-       }
-       maybe_enable_record ();
-       _count_in_once = true;
-       request_transport_speed (1.0, true);
-}
-
-void
-Session::request_play_loop (bool yn, bool change_transport_roll)
-{
-       if (_slave && yn) {
-               // don't attempt to loop when not using Internal Transport
-               // see also gtk2_ardour/ardour_ui_options.cc parameter_changed()
-               return;
-       }
-
-       SessionEvent* ev;
-       Location *location = _locations->auto_loop_location();
-       double target_speed;
-
-       if (location == 0 && yn) {
-               error << _("Cannot loop - no loop range defined")
-                     << endmsg;
-               return;
-       }
-
-       if (change_transport_roll) {
-               if (transport_rolling()) {
-                       /* start looping at current speed */
-                       target_speed = transport_speed ();
-               } else {
-                       /* currently stopped */
-                       if (yn) {
-                               /* start looping at normal speed */
-                               target_speed = 1.0;
-                       } else {
-                               target_speed = 0.0;
-                       }
-               }
-       } else {
-               /* leave the speed alone */
-               target_speed = transport_speed ();
-       }
-
-       ev = new SessionEvent (SessionEvent::SetLoop, SessionEvent::Add, SessionEvent::Immediate, 0, target_speed, yn);
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request set loop = %1, change roll state ? %2\n", yn, change_transport_roll));
-       queue_event (ev);
-
-       if (yn) {
-               if (!change_transport_roll) {
-                       if (!transport_rolling()) {
-                               /* we're not changing transport state, but we do want
-                                  to set up position for the new loop. Don't
-                                  do this if we're rolling already.
-                               */
-                               request_locate (location->start(), false);
-                       }
-               }
-       } else {
-               if (!change_transport_roll && Config->get_seamless_loop() && transport_rolling()) {
-                       // request an immediate locate to refresh the tracks
-                       // after disabling looping
-                       request_locate (_transport_sample-1, false);
-               }
-       }
-}
-
-void
-Session::request_play_range (list<AudioRange>* range, bool leave_rolling)
-{
-       SessionEvent* ev = new SessionEvent (SessionEvent::SetPlayAudioRange, SessionEvent::Add, SessionEvent::Immediate, 0, (leave_rolling ? 1.0 : 0.0));
-       if (range) {
-               ev->audio_range = *range;
-       } else {
-               ev->audio_range.clear ();
-       }
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request play range, leave rolling ? %1\n", leave_rolling));
-       queue_event (ev);
-}
-
-void
-Session::request_cancel_play_range ()
-{
-       SessionEvent* ev = new SessionEvent (SessionEvent::CancelPlayAudioRange, SessionEvent::Add, SessionEvent::Immediate, 0, 0);
-       queue_event (ev);
-}
-
 
-bool
-Session::solo_selection_active ()
-{
-       if ( _soloSelection.empty() ) {
-               return false;
-       }
-       return true;
-}
-
-void
-Session::solo_selection ( StripableList &list, bool new_state  )
-{
-       boost::shared_ptr<ControlList> solo_list (new ControlList);
-       boost::shared_ptr<ControlList> unsolo_list (new ControlList);
-
-       if (new_state)
-               _soloSelection = list;
-       else
-               _soloSelection.clear();
-       
-       boost::shared_ptr<RouteList> rl = get_routes();
-       for (ARDOUR::RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
+#ifdef NDEBUG
+# define ENSURE_PROCESS_THREAD do {} while (0)
+#else
+# define ENSURE_PROCESS_THREAD                           \
+  do {                                                   \
+    if (!AudioEngine::instance()->in_process_thread()) { \
+      PBD::stacktrace (std::cerr, 30);                   \
+    }                                                    \
+  } while (0)
+#endif
 
-               if ( !(*i)->is_track() ) {
-                       continue;
-               }
-               
-               boost::shared_ptr<Stripable> s (*i);
 
-               bool found = (std::find(list.begin(), list.end(), s) != list.end());
-               if ( new_state && found ) {
-                       
-                       solo_list->push_back (s->solo_control());
-                       
-                       //must invalidate playlists on selected tracks, so only selected regions get heard
-                       boost::shared_ptr<Track> track = boost::dynamic_pointer_cast<Track> (*i);
-                       if (track) {
-                               boost::shared_ptr<Playlist> playlist = track->playlist();
-                               if (playlist) {
-                                       playlist->ContentsChanged();
-                               }
-                       }
-               } else {
-                       unsolo_list->push_back (s->solo_control());
-               }
-       }
+#define TFSM_EVENT(evtype) { _transport_fsm->enqueue (new TransportFSM::Event (evtype)); }
+#define TFSM_STOP(abort,clear) { _transport_fsm->enqueue (new TransportFSM::Event (TransportFSM::StopTransport,abort,clear)); }
+#define TFSM_LOCATE(target,roll,flush,loop,force) { _transport_fsm->enqueue (new TransportFSM::Event (TransportFSM::Locate,target,roll,flush,loop,force)); }
 
-       set_controls (solo_list, 1.0, Controllable::NoGroup);
-       set_controls (unsolo_list, 0.0, Controllable::NoGroup);
-}
+/* *****************************************************************************
+ * REALTIME ACTIONS (to be called on state transitions)
+ * ****************************************************************************/
 
 void
 Session::realtime_stop (bool abort, bool clear_state)
 {
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("realtime stop @ %1\n", _transport_sample));
-       PostTransportWork todo = PostTransportWork (0);
+       ENSURE_PROCESS_THREAD;
 
-       /* assume that when we start, we'll be moving forwards */
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("realtime stop @ %1 speed = %2\n", _transport_sample, _transport_speed));
+       PostTransportWork todo = PostTransportWork (0);
 
-       if (_transport_speed < 0.0f) {
+       if (_last_transport_speed < 0.0f) {
                todo = (PostTransportWork (todo | PostTransportStop | PostTransportReverse));
                _default_transport_speed = 1.0;
+               DiskReader::inc_no_disk_output (); // for the buffer reversal
        } else {
                todo = PostTransportWork (todo | PostTransportStop);
        }
@@ -345,10 +116,6 @@ Session::realtime_stop (bool abort, bool clear_state)
 
        DEBUG_TRACE (DEBUG::Transport, string_compose ("stop complete, auto-return scheduled for return to %1\n", _requested_return_sample));
 
-       /* the duration change is not guaranteed to have happened, but is likely */
-
-       todo = PostTransportWork (todo | PostTransportDuration);
-
        if (abort) {
                todo = PostTransportWork (todo | PostTransportAbort);
        }
@@ -361,7 +128,6 @@ Session::realtime_stop (bool abort, bool clear_state)
                add_post_transport_work (todo);
        }
 
-       _clear_event_type (SessionEvent::StopOnce);
        _clear_event_type (SessionEvent::RangeStop);
        _clear_event_type (SessionEvent::RangeLocate);
 
@@ -369,7 +135,7 @@ Session::realtime_stop (bool abort, bool clear_state)
        if ( solo_selection_active() ) {
                solo_selection ( _soloSelection, false );
        }
-       
+
        /* if we're going to clear loop state, then force disabling record BUT only if we're not doing latched rec-enable */
        disable_record (true, (!Config->get_latched_record_enable() && clear_state));
 
@@ -390,776 +156,654 @@ Session::realtime_stop (bool abort, bool clear_state)
                waiting_for_sync_offset = true;
        }
 
-       transport_sub_state = 0;
-}
-
-void
-Session::realtime_locate ()
-{
-       boost::shared_ptr<RouteList> r = routes.reader ();
-       for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
-               (*i)->realtime_locate ();
+       if (todo) {
+               TFSM_EVENT (TransportFSM::ButlerRequired);
        }
 }
 
 void
-Session::butler_transport_work ()
+Session::locate (samplepos_t target_sample, bool with_roll, bool with_flush, bool for_loop_enabled, bool force, bool with_mmc)
 {
-       /* Note: this function executes in the butler thread context */
+       ENSURE_PROCESS_THREAD;
 
-  restart:
-       bool finished;
-       PostTransportWork ptw;
-       boost::shared_ptr<RouteList> r = routes.reader ();
-       uint64_t before;
+       if (target_sample < 0) {
+               error << _("Locate called for negative sample position - ignored") << endmsg;
+               return;
+       }
 
-       int on_entry = g_atomic_int_get (&_butler->should_do_transport_work);
-       finished = true;
-       ptw = post_transport_work();
+       if (synced_to_engine()) {
 
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("Butler transport work, todo = %1 at %2\n", enum_2_string (ptw), (before = g_get_monotonic_time())));
+               double sp;
+               samplepos_t pos;
+               samplepos_t ignore1, ignore2;
 
+               transport_master()->speed_and_position (sp, pos, ignore1, ignore2, 0);
 
-       if (ptw & PostTransportLocate) {
+               if (target_sample != pos) {
 
-               if (get_play_loop() && !Config->get_seamless_loop()) {
+                       if (config.get_jack_time_master()) {
+                               /* actually locate now, since otherwise jack_timebase_callback
+                                  will use the incorrect _transport_sample and report an old
+                                  and incorrect time to Jack transport
+                               */
+                               do_locate (target_sample, with_roll, with_flush, for_loop_enabled, force, with_mmc);
+                       }
 
-                       DEBUG_TRACE (DEBUG::Butler, "flush loop recording fragment to disk\n");
+                       /* tell JACK to change transport position, and we will
+                          follow along later in ::follow_slave()
+                       */
 
-                       /* this locate might be happening while we are
-                        * loop recording.
-                        *
-                        * Non-seamless looping will require a locate (below) that
-                        * will reset capture buffers and throw away data.
-                        *
-                        * Rather than first find all tracks and see if they
-                        * have outstanding data, just do a flush anyway. It
-                        * may be cheaper this way anyway, and is certainly
-                        * more accurate.
-                        */
+                       _engine.transport_locate (target_sample);
 
-                       bool more_disk_io_to_do = false;
-                       uint32_t errors = 0;
+                       if (sp != 1.0f && with_roll) {
+                               _engine.transport_start ();
+                       }
 
-                       do {
-                               more_disk_io_to_do = _butler->flush_tracks_to_disk_after_locate (r, errors);
+               }
 
-                               if (errors) {
-                                       break;
-                               }
+       } else {
+               do_locate (target_sample, with_roll, with_flush, for_loop_enabled, force, with_mmc);
+       }
+}
 
-                               if (more_disk_io_to_do) {
-                                       continue;
-                               }
+/** @param with_mmc true to send a MMC locate command when the locate is done */
+void
+Session::do_locate (samplepos_t target_sample, bool with_roll, bool with_flush, bool for_loop_enabled, bool force, bool with_mmc)
+{
+       ENSURE_PROCESS_THREAD;
 
-                       } while (false);
+       bool need_butler = false;
 
-               }
-       }
+       /* Locates for seamless looping are fairly different from other
+        * locates. They assume that the diskstream buffers for each track
+        * already have the correct data in them, and thus there is no need to
+        * actually tell the tracks to locate. What does need to be done,
+        * though, is all the housekeeping that is associated with non-linear
+        * changes in the value of _transport_sample.
+        */
 
-       if (ptw & PostTransportAdjustPlaybackBuffering) {
-               for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
-                       boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
-                       if (tr) {
-                               tr->adjust_playback_buffering ();
-                               /* and refill those buffers ... */
-                       }
-                       (*i)->non_realtime_locate (_transport_sample);
-               }
-               VCAList v = _vca_manager->vcas ();
-               for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
-                       (*i)->non_realtime_locate (_transport_sample);
-               }
-       }
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("rt-locate to %1, roll %2 flush %3 loop-enabled %4 force %5 mmc %6\n",
+                                                      target_sample, with_roll, with_flush, for_loop_enabled, force, with_mmc));
 
-       if (ptw & PostTransportAdjustCaptureBuffering) {
-               for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
-                       boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
-                       if (tr) {
-                               tr->adjust_capture_buffering ();
-                       }
-               }
-       }
+       if (!force && _transport_sample == target_sample && !loop_changing && !for_loop_enabled) {
 
-       if (ptw & PostTransportCurveRealloc) {
-               for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
-                       (*i)->curve_reallocate();
+               /* already at the desired position. Not forced to locate,
+                  the loop isn't changing, so unless we're told to
+                  start rolling also, there's nothing to do but
+                  tell the world where we are (again).
+               */
+
+               if (with_roll) {
+                       set_transport_speed (1.0, 0, false);
                }
+               loop_changing = false;
+               TFSM_EVENT (TransportFSM::LocateDone);
+               Located (); /* EMIT SIGNAL */
+               return;
        }
 
-       if (ptw & PostTransportSpeed) {
-               non_realtime_set_speed ();
-       }
+       // Update Timecode time
+       _transport_sample = target_sample;
+       // Bump seek counter so that any in-process locate in the butler
+       // thread(s?) can restart.
+       g_atomic_int_inc (&_seek_counter);
+       _last_roll_or_reversal_location = target_sample;
+       _remaining_latency_preroll = worst_latency_preroll ();
+       timecode_time(_transport_sample, transmitting_timecode_time); // XXX here?
 
-       if (ptw & PostTransportReverse) {
+       /* do "stopped" stuff if:
+        *
+        * we are rolling AND
+        * no autoplay in effect AND
+        * we're not going to keep rolling after the locate AND
+        * !(playing a loop with JACK sync) AND
+        * we're not synced to an external transport master
+        *
+        */
 
-               clear_clicks();
-               cumulative_rf_motion = 0;
-               reset_rf_scale (0);
 
-               /* don't seek if locate will take care of that in non_realtime_stop() */
+       /* it is important here that we use the internal state of the transport
+          FSM, not the public facing result of ::transport_rolling()
+       */
+       bool transport_was_stopped = _transport_fsm->stopped();
 
-               if (!(ptw & PostTransportLocate)) {
-                       for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
-                               (*i)->non_realtime_locate (_transport_sample);
+       if (!transport_was_stopped &&
+           (!auto_play_legal || !config.get_auto_play()) &&
+           !with_roll &&
+           !(synced_to_engine() && play_loop) &&
+           (!Profile->get_trx() || !(config.get_external_sync() && !synced_to_engine()))) {
 
-                               if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) {
-                                       /* new request, stop seeking, and start again */
-                                       g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
-                                       goto restart;
-                               }
-                       }
-                       VCAList v = _vca_manager->vcas ();
-                       for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
-                               (*i)->non_realtime_locate (_transport_sample);
-                       }
-               }
-       }
+               realtime_stop (false, true); // XXX paul - check if the 2nd arg is really correct
+               transport_was_stopped = true;
 
-       if (ptw & PostTransportLocate) {
-               DEBUG_TRACE (DEBUG::Transport, "nonrealtime locate invoked from BTW\n");
-               non_realtime_locate ();
-       }
+       } else {
 
-       if (ptw & PostTransportStop) {
-               non_realtime_stop (ptw & PostTransportAbort, on_entry, finished);
-               if (!finished) {
-                       g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
-                       goto restart;
-               }
-       }
+               /* Tell all routes to do the RT part of locate */
 
-       if (ptw & PostTransportOverWrite) {
-               non_realtime_overwrite (on_entry, finished);
-               if (!finished) {
-                       g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
-                       goto restart;
+               boost::shared_ptr<RouteList> r = routes.reader ();
+               for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
+                       (*i)->realtime_locate ();
                }
        }
 
-       if (ptw & PostTransportAudition) {
-               non_realtime_set_audition ();
-       }
-
-       g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
-
-       DEBUG_TRACE (DEBUG::Transport, string_compose (X_("Butler transport work all done after %1 usecs @ %2 trw = %3\n"), g_get_monotonic_time() - before, _transport_sample, _butler->transport_work_requested()));
-}
+       if (force || !for_loop_enabled || loop_changing) {
 
-void
-Session::non_realtime_set_speed ()
-{
-       boost::shared_ptr<RouteList> rl = routes.reader();
-       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
-               boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
-               if (tr) {
-                       tr->non_realtime_speed_change ();
-               }
-       }
-}
+               PostTransportWork todo = PostTransportLocate;
 
-void
-Session::non_realtime_overwrite (int on_entry, bool& finished)
-{
-       boost::shared_ptr<RouteList> rl = routes.reader();
-       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
-               boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
-               if (tr && tr->pending_overwrite ()) {
-                       tr->overwrite_existing_buffers ();
-               }
-               if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) {
-                       finished = false;
-                       return;
+               if (with_roll && transport_was_stopped) {
+                       todo = PostTransportWork (todo | PostTransportRoll);
                }
-       }
-}
-
 
-void
-Session::non_realtime_locate ()
-{
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("locate tracks to %1\n", _transport_sample));
+               add_post_transport_work (todo);
+               need_butler = true;
 
-       if (Config->get_loop_is_mode() && get_play_loop()) {
+       } else {
 
-               Location *loc  = _locations->auto_loop_location();
+               /* this is functionally what clear_clicks() does but with a tentative lock */
 
-               if (!loc || (_transport_sample < loc->start() || _transport_sample >= loc->end())) {
-                       /* jumped out of loop range: stop tracks from looping,
-                          but leave loop (mode) enabled.
-                        */
-                       set_track_loop (false);
+               Glib::Threads::RWLock::WriterLock clickm (click_lock, Glib::Threads::TRY_LOCK);
 
-               } else if (loc && Config->get_seamless_loop() &&
-                   ((loc->start() <= _transport_sample) ||
-                   (loc->end() > _transport_sample) ) ) {
+               if (clickm.locked()) {
 
-                       /* jumping to start of loop. This  might have been done before but it is
-                        * idempotent and cheap. Doing it here ensures that when we start playback
-                        * outside the loop we still flip tracks into the magic seamless mode
-                        * when needed.
-                        */
-                       set_track_loop (true);
+                       for (Clicks::iterator i = clicks.begin(); i != clicks.end(); ++i) {
+                               delete *i;
+                       }
 
-               } else if (loc) {
-                       set_track_loop (false);
+                       clicks.clear ();
                }
+       }
 
+       if (with_roll) {
+               /* switch from input if we're going to roll */
+               if (Config->get_monitoring_model() == HardwareMonitoring) {
+                       set_track_monitor_input_status (!config.get_auto_input());
+               }
        } else {
-
-               /* no more looping .. should have been noticed elsewhere */
+               /* otherwise we're going to stop, so do the opposite */
+               if (Config->get_monitoring_model() == HardwareMonitoring) {
+                       set_track_monitor_input_status (true);
+               }
        }
 
+       /* cancel looped playback if transport pos outside of loop range */
+       if (play_loop) {
 
-       samplepos_t tf;
+               Location* al = _locations->auto_loop_location();
 
-       {
-               boost::shared_ptr<RouteList> rl = routes.reader();
+               if (al) {
+                       if (_transport_sample < al->start() || _transport_sample >= al->end()) {
 
-         restart:
-               gint sc = g_atomic_int_get (&_seek_counter);
-               tf = _transport_sample;
-
-               for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
-                       (*i)->non_realtime_locate (tf);
-                       if (sc != g_atomic_int_get (&_seek_counter)) {
-                               goto restart;
-                       }
-               }
-       }
-
-       {
-               /* VCAs are quick to locate because they have no data (except
-                  automation) associated with them. Don't bother with a
-                  restart mechanism here, but do use the same transport sample
-                  that the Routes used.
-               */
-               VCAList v = _vca_manager->vcas ();
-               for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
-                       (*i)->non_realtime_locate (tf);
-               }
-       }
-
-       _scene_changer->locate (_transport_sample);
-
-       /* XXX: it would be nice to generate the new clicks here (in the non-RT thread)
-          rather than clearing them so that the RT thread has to spend time constructing
-          them (in Session::click).
-        */
-       clear_clicks ();
-}
-
-#ifdef USE_TRACKS_CODE_FEATURES
-bool
-Session::select_playhead_priority_target (samplepos_t& jump_to)
-{
-       jump_to = -1;
-
-       AutoReturnTarget autoreturn = Config->get_auto_return_target_list ();
-
-       if (!autoreturn) {
-               return false;
-       }
+                               // located outside the loop: cancel looping directly, this is called from event handling context
 
-       if (Profile->get_trx() && transport_rolling() ) {
-               // We're playing, so do nothing.
-               // Next stop will put us where we need to be.
-               return false;
-       }
+                               have_looped = false;
 
-       /* Note that the order of checking each AutoReturnTarget flag defines
-          the priority each flag.
+                               if (!Config->get_loop_is_mode()) {
+                                       set_play_loop (false, _transport_speed);
+                               } else {
+                                       if (Config->get_seamless_loop()) {
+                                               /* this will make the non_realtime_locate() in the butler
+                                                  which then causes seek() in tracks actually do the right
+                                                  thing.
+                                               */
+                                               set_track_loop (false);
+                                       }
+                               }
 
-          Ardour/Mixbus: Last Locate
-                         Range Selection
-                         Loop Range
-                         Region Selection
+                       } else if (_transport_sample == al->start()) {
 
-          Tracks:        Range Selection
-                          Loop Range
-                          Region Selection
-                          Last Locate
-       */
+                               // located to start of loop - this is looping, basically
 
-       if (autoreturn & RangeSelectionStart) {
-               if (!_range_selection.empty()) {
-                       jump_to = _range_selection.from;
-               } else {
-                       if (transport_rolling ()) {
-                               /* Range selection no longer exists, but we're playing,
-                                  so do nothing. Next stop will put us where
-                                  we need to be.
-                               */
-                               return false;
-                       }
-               }
-       }
+                               if (!have_looped) {
+                                       /* first time */
+                                       if (_last_roll_location != al->start()) {
+                                               /* didn't start at loop start - playback must have
+                                                * started before loop since we've now hit the loop
+                                                * end.
+                                                */
+                                               add_post_transport_work (PostTransportLocate);
+                                               need_butler = true;
+                                       }
 
-       if (jump_to < 0 && (autoreturn & Loop) && get_play_loop()) {
-               /* don't try to handle loop play when synced to JACK */
+                               }
 
-               if (!synced_to_engine()) {
-                       Location *location = _locations->auto_loop_location();
+                               boost::shared_ptr<RouteList> rl = routes.reader();
 
-                       if (location) {
-                               jump_to = location->start();
+                               for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
+                                       boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
 
-                               if (Config->get_seamless_loop()) {
-                                       /* need to get track buffers reloaded */
-                                       set_track_loop (true);
+                                       if (tr && tr->rec_enable_control()->get_value()) {
+                                               // tell it we've looped, so it can deal with the record state
+                                               tr->transport_looped (_transport_sample);
+                                       }
                                }
+
+                               have_looped = true;
+                               TransportLooped(); // EMIT SIGNAL
                        }
                }
        }
 
-       if (jump_to < 0 && (autoreturn & RegionSelectionStart)) {
-               if (!_object_selection.empty()) {
-                       jump_to = _object_selection.from;
-               }
+       if (need_butler) {
+               TFSM_EVENT (TransportFSM::ButlerRequired);
+       } else {
+               TFSM_EVENT (TransportFSM::LocateDone);
        }
 
-       if (jump_to < 0 && (autoreturn & LastLocate)) {
-               jump_to = _last_roll_location;
-       }
+       loop_changing = false;
 
-       return jump_to >= 0;
-}
-#else
+       _send_timecode_update = true;
 
-bool
-Session::select_playhead_priority_target (samplepos_t& jump_to)
-{
-       if (config.get_external_sync() || !config.get_auto_return()) {
-               return false;
+       if (with_mmc) {
+               send_mmc_locate (_transport_sample);
        }
 
-       jump_to = _last_roll_location;
-       return jump_to >= 0;
+       _last_roll_location = _last_roll_or_reversal_location =  _transport_sample;
+       if (!synced_to_engine () || _transport_sample == _engine.transport_sample ()) {
+               Located (); /* EMIT SIGNAL */
+       }
 }
 
-#endif
-
 void
-Session::follow_playhead_priority ()
+Session::post_locate ()
 {
-       samplepos_t target;
-
-       if (select_playhead_priority_target (target)) {
-               request_locate (target);
+       if (transport_master_is_external() && !synced_to_engine()) {
+               const samplepos_t current_master_position = TransportMasterManager::instance().get_current_position_in_process_context();
+               if (abs (current_master_position - _transport_sample) > TransportMasterManager::instance().current()->resolution()) {
+                       _last_roll_location = _last_roll_or_reversal_location =  _transport_sample;
+               }
        }
 }
 
+/** Set the transport speed.
+ *  Called from the process thread.
+ *  @param speed New speed
+ */
 void
-Session::non_realtime_stop (bool abort, int on_entry, bool& finished)
+Session::set_transport_speed (double speed, samplepos_t destination_sample, bool abort, bool clear_state, bool as_default)
 {
-       struct tm* now;
-       time_t     xnow;
-       bool       did_record;
-       bool       saved;
-       PostTransportWork ptw = post_transport_work();
-
-       did_record = false;
-       saved = false;
-
-       boost::shared_ptr<RouteList> rl = routes.reader();
-       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
-               boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
-               if (tr && tr->get_captured_samples () != 0) {
-                       did_record = true;
-                       break;
-               }
-       }
-
-       /* stop and locate are merged here because they share a lot of common stuff */
-
-       time (&xnow);
-       now = localtime (&xnow);
-
-       if (auditioner) {
-               auditioner->cancel_audition ();
-       }
+       ENSURE_PROCESS_THREAD;
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("@ %5 Set transport speed to %1, abort = %2 clear_state = %3, current = %4 as_default %6\n",
+                                                      speed, abort, clear_state, _transport_speed, _transport_sample, as_default));
 
-       cumulative_rf_motion = 0;
-       reset_rf_scale (0);
+       /* max speed is somewhat arbitrary but based on guestimates regarding disk i/o capability
+          and user needs. We really need CD-style "skip" playback for ffwd and rewind.
+       */
 
-       if (did_record) {
-               begin_reversible_command (Operations::capture);
-               _have_captured = true;
+       if (speed > 0) {
+               speed = min ((double) Config->get_max_transport_speed(), speed);
+       } else if (speed < 0) {
+               speed = max ((double) -Config->get_max_transport_speed(), speed);
        }
 
-       DEBUG_TRACE (DEBUG::Transport, X_("Butler PTW: DS stop\n"));
-
-       if (abort && did_record) {
-               /* no reason to save the session file when we remove sources
-                */
-               _state_of_the_state = StateOfTheState (_state_of_the_state|InCleanup);
+       double new_engine_speed = 1.0;
+       if (speed != 0) {
+               new_engine_speed = fabs (speed);
+               if (speed < 0) speed = -1;
+               if (speed > 0) speed = 1;
        }
 
-       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
-               boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
-               if (tr) {
-                       tr->transport_stopped_wallclock (*now, xnow, abort);
+       if (_transport_speed == speed && new_engine_speed == _engine_speed) {
+               if (as_default && speed == 0.0) { // => reset default transport speed. hacky or what?
+                       _default_transport_speed = 1.0;
                }
+               return;
        }
 
-       if (abort && did_record) {
-               _state_of_the_state = StateOfTheState (_state_of_the_state & ~InCleanup);
+#if 0 // TODO pref: allow vari-speed recording
+       if (actively_recording() && speed != 1.0 && speed != 0.0) {
+               /* no varispeed during recording */
+               DEBUG_TRACE (DEBUG::Transport, string_compose ("No varispeed during recording cur_speed %1, sample %2\n",
+                                                      _transport_speed, _transport_sample));
+               return;
        }
+#endif
 
-       boost::shared_ptr<RouteList> r = routes.reader ();
+       _target_transport_speed = fabs(speed);
+       _engine_speed = new_engine_speed;
 
-       if (did_record) {
-               commit_reversible_command ();
-               /* increase take name */
-               if (config.get_track_name_take () && !config.get_take_name ().empty()) {
-                       string newname = config.get_take_name();
-                       config.set_take_name(bump_name_number (newname));
-               }
-       }
+       if (transport_rolling() && speed == 0.0) {
 
-       if (_engine.running()) {
-               PostTransportWork ptw = post_transport_work ();
+               /* we are rolling and we want to stop */
 
-               for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
-                       (*i)->non_realtime_transport_stop (_transport_sample, !(ptw & PostTransportLocate));
-               }
-               VCAList v = _vca_manager->vcas ();
-               for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
-                       (*i)->non_realtime_transport_stop (_transport_sample, !(ptw & PostTransportLocate));
+               if (Config->get_monitoring_model() == HardwareMonitoring) {
+                       set_track_monitor_input_status (true);
                }
 
-               update_latency_compensation ();
-       }
-
-       bool const auto_return_enabled = (!config.get_external_sync() && (Config->get_auto_return_target_list() || abort));
-
-       if (auto_return_enabled ||
-           (ptw & PostTransportLocate) ||
-           (_requested_return_sample >= 0) ||
-           synced_to_engine()) {
-
-               // rg: what is the logic behind this case?
-               // _requested_return_sample should be ignored when synced_to_engine/slaved.
-               // currently worked around in MTC_Slave by forcing _requested_return_sample to -1
-               // 2016-01-10
-               if ((auto_return_enabled || synced_to_engine() || _requested_return_sample >= 0) &&
-                   !(ptw & PostTransportLocate)) {
-
-                       /* no explicit locate queued */
+               if (synced_to_engine ()) {
+                       if (clear_state) {
+                               /* do this here because our response to the slave won't
+                                  take care of it.
+                               */
+                               _play_range = false;
+                               _count_in_once = false;
+                               unset_play_loop ();
+                       }
+                       _engine.transport_stop ();
+               } else {
+                       bool const auto_return_enabled = (!config.get_external_sync() && (Config->get_auto_return_target_list() || abort));
 
-                       bool do_locate = false;
+                       if (!auto_return_enabled) {
+                               _requested_return_sample = destination_sample;
+                       }
 
-                       if (_requested_return_sample >= 0) {
+                       TFSM_STOP (abort, false);
+               }
 
-                               /* explicit return request pre-queued in event list. overrides everything else */
+       } else if (transport_stopped() && speed == 1.0) {
+               if (as_default) {
+                       _default_transport_speed = speed;
+               }
+               /* we are stopped and we want to start rolling at speed 1 */
 
-                               _transport_sample = _requested_return_sample;
-                               do_locate = true;
+               if (Config->get_loop_is_mode() && play_loop) {
 
-                       } else {
-                               samplepos_t jump_to;
+                       Location *location = _locations->auto_loop_location();
 
-                               if (select_playhead_priority_target (jump_to)) {
+                       if (location != 0) {
+                               if (_transport_sample != location->start()) {
 
-                                       _transport_sample = jump_to;
-                                       do_locate = true;
+                                       if (Config->get_seamless_loop()) {
+                                               /* force tracks to do their thing */
+                                               set_track_loop (true);
+                                       }
 
-                               } else if (abort) {
+                                       /* jump to start and then roll from there */
 
-                                       _transport_sample = _last_roll_location;
-                                       do_locate = true;
+                                       request_locate (location->start(), true);
+                                       return;
                                }
                        }
-
-                       _requested_return_sample = -1;
-
-                       if (do_locate) {
-                               _engine.transport_locate (_transport_sample);
-                       }
                }
 
-       }
+               if (Config->get_monitoring_model() == HardwareMonitoring && config.get_auto_input()) {
+                       set_track_monitor_input_status (false);
+               }
 
-       clear_clicks();
-       unset_preroll_record_trim ();
-
-       /* do this before seeking, because otherwise the tracks will do the wrong thing in seamless loop mode.
-       */
-
-       if (ptw & PostTransportClearSubstate) {
-               unset_play_range ();
-               if (!Config->get_loop_is_mode()) {
-                       unset_play_loop ();
+               if (synced_to_engine()) {
+                       _engine.transport_start ();
+                       _count_in_once = false;
+               } else {
+                       TFSM_EVENT (TransportFSM::StartTransport);
                }
-       }
 
-       /* this for() block can be put inside the previous if() and has the effect of ... ??? what */
+       } else {
 
-       {
-               DEBUG_TRACE (DEBUG::Transport, X_("Butler PTW: locate\n"));
-               for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
-                       DEBUG_TRACE (DEBUG::Transport, string_compose ("Butler PTW: locate on %1\n", (*i)->name()));
-                       (*i)->non_realtime_locate (_transport_sample);
+               /* not zero, not 1.0 ... varispeed */
 
-                       if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) {
-                               finished = false;
-                               /* we will be back */
-                               return;
-                       }
-               }
-       }
+               // TODO handled transport start..  _remaining_latency_preroll
+               // and reversal of playback direction.
 
-       {
-               VCAList v = _vca_manager->vcas ();
-               for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
-                       (*i)->non_realtime_locate (_transport_sample);
+               if ((synced_to_engine()) && speed != 0.0 && speed != 1.0) {
+                       warning << string_compose (
+                               _("Global varispeed cannot be supported while %1 is connected to JACK transport control"),
+                               PROGRAM_NAME)
+                               << endmsg;
+                       return;
                }
-       }
-
-       have_looped = false;
-
-       /* don't bother with this stuff if we're disconnected from the engine,
-          because there will be no process callbacks to deliver stuff from
-       */
-
-       if (_engine.connected() && !_engine.freewheeling()) {
-               // need to queue this in the next RT cycle
-               _send_timecode_update = true;
-
-               if (!dynamic_cast<MTC_Slave*>(_slave)) {
-                       send_immediate_mmc (MIDI::MachineControlCommand (MIDI::MachineControl::cmdStop));
 
-                       /* This (::non_realtime_stop()) gets called by main
-                          process thread, which will lead to confusion
-                          when calling AsyncMIDIPort::write().
+#if 0
+               if (actively_recording()) {
+                       return;
+               }
+#endif
 
-                          Something must be done. XXX
-                       */
-                       send_mmc_locate (_transport_sample);
+               if (speed > 0.0 && _transport_sample == current_end_sample()) {
+                       return;
                }
-       }
 
-       if ((ptw & PostTransportLocate) && get_record_enabled()) {
-               /* This is scheduled by realtime_stop(), which is also done
-                * when a slave requests /locate/ for an initial sync.
-                * We can't hold up the slave for long with a save() here,
-                * without breaking its initial sync cycle.
-                *
-                * save state only if there's no slave or if it's not yet locked.
-                */
-               if (!_slave || !_slave->locked()) {
-                       DEBUG_TRACE (DEBUG::Transport, X_("Butler PTW: requests save\n"));
-                       SaveSessionRequested (_current_snapshot_name);
-                       saved = true;
+               if (speed < 0.0 && _transport_sample == 0) {
+                       return;
                }
-       }
 
-       /* always try to get rid of this */
+               clear_clicks ();
 
-       remove_pending_capture_state ();
+               /* if we are reversing relative to the current speed, or relative to the speed
+                  before the last stop, then we have to do extra work.
+               */
 
-       /* save the current state of things if appropriate */
+               PostTransportWork todo = PostTransportWork (0);
 
-       if (did_record && !saved) {
-               SaveSessionRequested (_current_snapshot_name);
-       }
+               if ((_transport_speed && speed * _transport_speed < 0.0) || (_last_transport_speed * speed < 0.0) || (_last_transport_speed == 0.0 && speed < 0.0)) {
+                       todo = PostTransportWork (todo | PostTransportReverse);
+                       DiskReader::inc_no_disk_output (); // for the buffer reversal
+                       _last_roll_or_reversal_location = _transport_sample;
+               }
 
-       if (ptw & PostTransportStop) {
-               unset_play_range ();
-               if (!Config->get_loop_is_mode()) {
-                       unset_play_loop ();
+               _last_transport_speed = _transport_speed;
+               _transport_speed = speed;
+
+               if (as_default) {
+                       _default_transport_speed = speed;
                }
-       }
 
-       PositionChanged (_transport_sample); /* EMIT SIGNAL */
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC with speed = %1\n", _transport_speed));
-       TransportStateChange (); /* EMIT SIGNAL */
-       AutomationWatch::instance().transport_stop_automation_watches (_transport_sample);
+               if (todo) {
+                       add_post_transport_work (todo);
+                       TFSM_EVENT (TransportFSM::ButlerRequired);
+               }
 
-       /* and start it up again if relevant */
+               DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC3 with speed = %1\n", _transport_speed));
 
-       if ((ptw & PostTransportLocate) && !config.get_external_sync()) {
-               request_transport_speed (1.0);
+               /* throttle signal emissions.
+                * when slaved [_last]_transport_speed
+                * usually changes every cycle (tiny amounts due to DLL).
+                * Emitting a signal every cycle is overkill and unwarranted.
+                *
+                * Using _last_transport_speed is not acceptable,
+                * since it allows for large changes over a long period
+                * of time. Hence we introduce a dedicated variable to keep track
+                *
+                * The 0.2% dead-zone is somewhat arbitrary. Main use-case
+                * for TransportStateChange() here is the ShuttleControl display.
+                */
+               if (fabs (_signalled_varispeed - actual_speed ()) > .002
+                   // still, signal hard changes to 1.0 and 0.0:
+                   || (actual_speed () == 1.0 && _signalled_varispeed != 1.0)
+                   || (actual_speed () == 0.0 && _signalled_varispeed != 0.0)
+                  )
+               {
+                       TransportStateChange (); /* EMIT SIGNAL */
+                       _signalled_varispeed = actual_speed ();
+               }
        }
 }
 
+/** Stop the transport.  */
 void
-Session::unset_play_loop ()
+Session::stop_transport (bool abort, bool clear_state)
 {
-       if (play_loop) {
-               play_loop = false;
-               clear_events (SessionEvent::AutoLoop);
-               set_track_loop (false);
+       ENSURE_PROCESS_THREAD;
 
+       _count_in_once = false;
 
-               if (Config->get_seamless_loop()) {
-                       /* likely need to flush track buffers: this will locate us to wherever we are */
-                       add_post_transport_work (PostTransportLocate);
-                       _butler->schedule_transport_work ();
-               }
-       }
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("time to actually stop with TS @ %1\n", _transport_sample));
+
+       realtime_stop (abort, clear_state);
 }
 
+/** Called from the process thread */
 void
-Session::set_track_loop (bool yn)
+Session::start_transport ()
 {
-       Location* loc = _locations->auto_loop_location ();
+       ENSURE_PROCESS_THREAD;
+       DEBUG_TRACE (DEBUG::Transport, "start_transport\n");
 
-       if (!loc) {
-               yn = false;
-       }
+       _last_roll_location = _transport_sample;
+       _last_roll_or_reversal_location = _transport_sample;
+       _remaining_latency_preroll = worst_latency_preroll ();
 
-       boost::shared_ptr<RouteList> rl = routes.reader ();
+       have_looped = false;
 
-       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
-               if (*i && !(*i)->is_private_route()) {
-                       (*i)->set_loop (yn ? loc : 0);
-               }
-       }
-}
+       /* if record status is Enabled, move it to Recording. if its
+          already Recording, move it to Disabled.
+       */
 
-void
-Session::set_play_loop (bool yn, double speed)
-{
-       /* Called from event-handling context */
+       switch (record_status()) {
+       case Enabled:
+               if (!config.get_punch_in()) {
+                       /* This is only for UIs (keep blinking rec-en before
+                        * punch-in, don't show rec-region etc). The UI still
+                        * depends on SessionEvent::PunchIn and ensuing signals.
+                        *
+                        * The disk-writers handle punch in/out internally
+                        * in their local delay-compensated timeframe.
+                        */
+                       enable_record ();
+               }
+               break;
 
-       Location *loc;
+       case Recording:
+               if (!play_loop) {
+                       disable_record (false);
+               }
+               break;
 
-       if (yn == play_loop || (actively_recording() && yn) || (loc = _locations->auto_loop_location()) == 0) {
-               /* nothing to do, or can't change loop status while recording */
-               return;
+       default:
+               break;
        }
 
-       if (yn && Config->get_seamless_loop() && synced_to_engine()) {
-               warning << string_compose (
-                       _("Seamless looping cannot be supported while %1 is using JACK transport.\n"
-                         "Recommend changing the configured options"), PROGRAM_NAME)
-                       << endmsg;
-               return;
-       }
+       _transport_speed = _default_transport_speed;
+       _target_transport_speed = _transport_speed;
 
-       if (yn) {
+       if (!_engine.freewheeling()) {
+               Timecode::Time time;
+               timecode_time_subframes (_transport_sample, time);
+               if (transport_master()->type() == MTC) {
+                       send_immediate_mmc (MIDI::MachineControlCommand (MIDI::MachineControl::cmdDeferredPlay));
+               }
 
-               play_loop = true;
-               have_looped = false;
+               if ((actively_recording () || (config.get_punch_in () && get_record_enabled ()))
+                   && click_data && (config.get_count_in () || _count_in_once)) {
+                       _count_in_once = false;
+                       /* calculate count-in duration (in audio samples)
+                        * - use [fixed] tempo/meter at _transport_sample
+                        * - calc duration of 1 bar + time-to-beat before or at transport_sample
+                        */
+                       const Tempo& tempo = _tempo_map->tempo_at_sample (_transport_sample);
+                       const Meter& meter = _tempo_map->meter_at_sample (_transport_sample);
 
-               if (loc) {
+                       const double num = meter.divisions_per_bar ();
+                       const double den = meter.note_divisor ();
+                       const double barbeat = _tempo_map->exact_qn_at_sample (_transport_sample, 0) * den / (4. * num);
+                       const double bar_fract = fmod (barbeat, 1.0); // fraction of bar elapsed.
 
-                       unset_play_range ();
+                       _count_in_samples = meter.samples_per_bar (tempo, _current_sample_rate);
 
-                       if (Config->get_seamless_loop()) {
-                               if (!Config->get_loop_is_mode()) {
-                                       /* set all tracks to use internal looping */
-                                       set_track_loop (true);
-                               } else {
-                                       /* we will do this in the locate to the start OR when we hit the end
-                                        * of the loop for the first time
-                                        */
-                               }
+                       double dt = _count_in_samples / num;
+                       if (bar_fract == 0) {
+                               /* at bar boundary, count-in 2 bars before start. */
+                               _count_in_samples *= 2;
                        } else {
-                               /* set all tracks to NOT use internal looping */
-                               set_track_loop (false);
+                               /* beats left after full bar until roll position */
+                               _count_in_samples *= 1. + bar_fract;
                        }
 
-                       /* Put the delick and loop events in into the event list.  The declick event will
-                          cause a de-clicking fade-out just before the end of the loop, and it will also result
-                          in a fade-in when the loop restarts.  The AutoLoop event will peform the actual loop.
-                       */
+                       if (_count_in_samples > _remaining_latency_preroll) {
+                               _remaining_latency_preroll = _count_in_samples;
+                       }
 
-                       samplepos_t dcp;
-                       samplecnt_t dcl;
-                       auto_loop_declick_range (loc, dcp, dcl);
-                       merge_event (new SessionEvent (SessionEvent::AutoLoop, SessionEvent::Replace, loc->end(), loc->start(), 0.0f));
-
-                       /* if requested to roll, locate to start of loop and
-                        * roll but ONLY if we're not already rolling.
-
-                          args: positition, roll=true, flush=true, with_loop=false, force buffer refill if seamless looping
-                       */
+                       int clickbeat = 0;
+                       samplepos_t cf = _transport_sample - _count_in_samples;
+                       samplecnt_t offset = _click_io->connected_latency (true);
+                       while (cf < _transport_sample + offset) {
+                               add_click (cf, clickbeat == 0);
+                               cf += dt;
+                               clickbeat = fmod (clickbeat + 1, num);
+                       }
 
-                       if (Config->get_loop_is_mode()) {
-                               /* loop IS a transport mode: if already
-                                  rolling, do not locate to loop start.
-                               */
-                               if (!transport_rolling() && (speed != 0.0)) {
-                                       start_locate (loc->start(), true, true, false, true);
-                               }
-                       } else {
-                               if (speed != 0.0) {
-                                       start_locate (loc->start(), true, true, false, true);
-                               }
+                       if (_count_in_samples < _remaining_latency_preroll) {
+                               _count_in_samples = _remaining_latency_preroll;
                        }
                }
-
-       } else {
-
-               unset_play_loop ();
        }
 
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC2 with speed = %1\n", _transport_speed));
-       TransportStateChange ();
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC4 with speed = %1\n", _transport_speed));
+       TransportStateChange (); /* EMIT SIGNAL */
 }
-void
-Session::flush_all_inserts ()
+
+bool
+Session::should_roll_after_locate () const
 {
-       boost::shared_ptr<RouteList> r = routes.reader ();
+       /* a locate must previously have been requested and completed */
+
+       return ((!config.get_external_sync() && (auto_play_legal && config.get_auto_play())) && !_exporting) || (post_transport_work() & PostTransportRoll);
 
-       for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
-               (*i)->flush_processors ();
-       }
 }
 
+/** Do any transport work in the audio thread that needs to be done after the
+ * butler thread is finished.  Audio thread, realtime safe.
+ */
 void
-Session::start_locate (samplepos_t target_sample, bool with_roll, bool with_flush, bool for_loop_enabled, bool force)
+Session::butler_completed_transport_work ()
 {
-       if (target_sample < 0) {
-               error << _("Locate called for negative sample position - ignored") << endmsg;
-               return;
-       }
-
-       if (synced_to_engine()) {
+       ENSURE_PROCESS_THREAD;
+       PostTransportWork ptw = post_transport_work ();
 
-               double sp;
-               samplepos_t pos;
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("Butler done, RT cleanup for %1\n", enum_2_string (ptw)));
 
-               _slave->speed_and_position (sp, pos);
+       if (ptw & PostTransportAudition) {
+               if (auditioner && auditioner->auditioning()) {
+                       process_function = &Session::process_audition;
+               } else {
+                       process_function = &Session::process_with_events;
+               }
+               ptw = PostTransportWork (ptw & ~PostTransportAudition);
+               set_post_transport_work (ptw);
+       }
 
-               if (target_sample != pos) {
+       if (ptw & PostTransportLocate) {
+               post_locate ();
+               ptw = PostTransportWork (ptw & ~PostTransportLocate);
+               set_post_transport_work (ptw);
+               TFSM_EVENT (TransportFSM::LocateDone);
+       }
 
-                       if (config.get_jack_time_master()) {
-                               /* actually locate now, since otherwise jack_timebase_callback
-                                  will use the incorrect _transport_sample and report an old
-                                  and incorrect time to Jack transport
-                               */
-                               locate (target_sample, with_roll, with_flush, for_loop_enabled, force);
-                       }
+       if (ptw & PostTransportAdjustPlaybackBuffering) {
+               /* we blocked output while this happened */
+               DiskReader::dec_no_disk_output ();
+               ptw = PostTransportWork (ptw & ~PostTransportAdjustPlaybackBuffering);
+               set_post_transport_work (ptw);
+       }
 
-                       /* tell JACK to change transport position, and we will
-                          follow along later in ::follow_slave()
-                       */
+       ptw = PostTransportWork (ptw & ~(PostTransportAdjustCaptureBuffering|PostTransportOverWrite));
+       set_post_transport_work (ptw);
 
-                       _engine.transport_locate (target_sample);
+       set_next_event ();
 
-                       if (sp != 1.0f && with_roll) {
-                               _engine.transport_start ();
-                       }
+       if (_transport_fsm->waiting_for_butler()) {
+               TFSM_EVENT (TransportFSM::ButlerDone);
+       }
 
-               }
+       DiskReader::dec_no_disk_output ();
+}
 
-       } else {
-               locate (target_sample, with_roll, with_flush, for_loop_enabled, force);
-       }
+void
+Session::schedule_butler_for_transport_work ()
+{
+       assert (_transport_fsm->waiting_for_butler ());
+       DEBUG_TRACE (DEBUG::Butler, "summon butler for transport work\n");
+       _butler->schedule_transport_work ();
 }
 
-samplecnt_t
-Session::worst_latency_preroll () const
+bool
+Session::maybe_stop (samplepos_t limit)
 {
-       return _worst_output_latency + _worst_input_latency;
+       ENSURE_PROCESS_THREAD;
+       if ((_transport_speed > 0.0f && _transport_sample >= limit) || (_transport_speed < 0.0f && _transport_sample == 0)) {
+               if (synced_to_engine () && config.get_jack_time_master ()) {
+                       _engine.transport_stop ();
+               } else if (!synced_to_engine ()) {
+                       TFSM_EVENT (TransportFSM::StopTransport);
+               }
+               return true;
+       }
+       return false;
 }
 
 int
 Session::micro_locate (samplecnt_t distance)
 {
+       ENSURE_PROCESS_THREAD;
+
        boost::shared_ptr<RouteList> rl = routes.reader();
        for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
                boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
@@ -1168,6 +812,8 @@ Session::micro_locate (samplecnt_t distance)
                }
        }
 
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("micro-locate by %1\n", distance));
+
        for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
                boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
                if (tr) {
@@ -1179,704 +825,906 @@ Session::micro_locate (samplecnt_t distance)
        return 0;
 }
 
-/** @param with_mmc true to send a MMC locate command when the locate is done */
 void
-Session::locate (samplepos_t target_sample, bool with_roll, bool with_flush, bool for_loop_enabled, bool force, bool with_mmc)
+Session::flush_all_inserts ()
 {
-       bool need_butler = false;
+       ENSURE_PROCESS_THREAD;
+       boost::shared_ptr<RouteList> r = routes.reader ();
 
-       /* Locates for seamless looping are fairly different from other
-        * locates. They assume that the diskstream buffers for each track
-        * already have the correct data in them, and thus there is no need to
-        * actually tell the tracks to locate. What does need to be done,
-        * though, is all the housekeeping that is associated with non-linear
-        * changes in the value of _transport_sample.
-        */
+       for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
+               (*i)->flush_processors ();
+       }
+}
 
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("rt-locate to %1, roll %2 flush %3 loop-enabled %4 force %5 mmc %6\n",
-                                                      target_sample, with_roll, with_flush, for_loop_enabled, force, with_mmc));
+void
+Session::set_play_loop (bool yn, double speed)
+{
+       ENSURE_PROCESS_THREAD;
+       /* Called from event-handling context */
 
-       if (!force && _transport_sample == target_sample && !loop_changing && !for_loop_enabled) {
+       Location *loc;
 
-               /* already at the desired position. Not forced to locate,
-                  the loop isn't changing, so unless we're told to
-                  start rolling also, there's nothing to do but
-                  tell the world where we are (again).
-               */
+       if (yn == play_loop || (actively_recording() && yn) || (loc = _locations->auto_loop_location()) == 0) {
+               /* nothing to do, or can't change loop status while recording */
+               return;
+       }
 
-               if (with_roll) {
-                       set_transport_speed (1.0, 0, false);
-               }
-               loop_changing = false;
-               Located (); /* EMIT SIGNAL */
+       if (yn && Config->get_seamless_loop() && synced_to_engine()) {
+               warning << string_compose (
+                       _("Seamless looping cannot be supported while %1 is using JACK transport.\n"
+                         "Recommend changing the configured options"), PROGRAM_NAME)
+                       << endmsg;
                return;
        }
 
-       cerr << "... now doing the actual locate\n";
+       if (yn) {
 
-       // Update Timecode time
-       _transport_sample = target_sample;
-       // Bump seek counter so that any in-process locate in the butler
-       // thread(s?) can restart.
-       g_atomic_int_inc (&_seek_counter);
-       _last_roll_or_reversal_location = target_sample;
-       _remaining_latency_preroll = worst_latency_preroll ();
-       timecode_time(_transport_sample, transmitting_timecode_time); // XXX here?
+               play_loop = true;
+               have_looped = false;
 
-       /* do "stopped" stuff if:
-        *
-        * we are rolling AND
-        * no autoplay in effect AND
-        * we're not going to keep rolling after the locate AND
-        * !(playing a loop with JACK sync)
-        *
-        */
+               if (loc) {
 
-       bool transport_was_stopped = !transport_rolling();
+                       unset_play_range ();
 
-       if (!transport_was_stopped && (!auto_play_legal || !config.get_auto_play()) && !with_roll && !(synced_to_engine() && play_loop) &&
-           (!Profile->get_trx() || !(config.get_external_sync() && !synced_to_engine()))) {
-               realtime_stop (false, true); // XXX paul - check if the 2nd arg is really correct
-               transport_was_stopped = true;
-       } else {
-               /* otherwise tell the world that we located */
-               realtime_locate ();
-       }
+                       if (Config->get_seamless_loop()) {
+                               if (!Config->get_loop_is_mode()) {
+                                       /* set all tracks to use internal looping */
+                                       set_track_loop (true);
+                               } else {
+                                       /* we will do this in the locate to the start OR when we hit the end
+                                        * of the loop for the first time
+                                        */
+                               }
+                       } else {
+                               /* set all tracks to NOT use internal looping */
+                               set_track_loop (false);
+                       }
 
-       if (force || !for_loop_enabled || loop_changing) {
+                       /* Put the delick and loop events in into the event list.  The declick event will
+                          cause a de-clicking fade-out just before the end of the loop, and it will also result
+                          in a fade-in when the loop restarts.  The AutoLoop event will peform the actual loop.
+                       */
 
-               PostTransportWork todo = PostTransportLocate;
+                       samplepos_t dcp;
+                       samplecnt_t dcl;
+                       auto_loop_declick_range (loc, dcp, dcl);
+                       merge_event (new SessionEvent (SessionEvent::AutoLoop, SessionEvent::Replace, loc->end(), loc->start(), 0.0f));
 
-               if (with_roll && transport_was_stopped) {
-                       todo = PostTransportWork (todo | PostTransportRoll);
-               }
+                       /* if requested to roll, locate to start of loop and
+                        * roll but ONLY if we're not already rolling.
 
-               add_post_transport_work (todo);
-               need_butler = true;
+                          args: positition, roll=true, flush=true, with_loop=false, force buffer refill if seamless looping
+                       */
+
+                       if (Config->get_loop_is_mode()) {
+                               /* loop IS a transport mode: if already
+                                  rolling, do not locate to loop start.
+                               */
+                               if (!transport_rolling() && (speed != 0.0)) {
+                                       TFSM_LOCATE (loc->start(), true, true, false, true);
+                               }
+                       } else {
+                               if (speed != 0.0) {
+                                       TFSM_LOCATE (loc->start(), true, true, false, true);
+                               }
+                       }
+               }
 
        } else {
 
-               /* this is functionally what clear_clicks() does but with a tentative lock */
+               unset_play_loop ();
+       }
 
-               Glib::Threads::RWLock::WriterLock clickm (click_lock, Glib::Threads::TRY_LOCK);
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC2 with speed = %1\n", _transport_speed));
+       TransportStateChange ();
+}
 
-               if (clickm.locked()) {
+/* *****************************************************************************
+ * END REALTIME ACTIONS
+ * ****************************************************************************/
 
-                       for (Clicks::iterator i = clicks.begin(); i != clicks.end(); ++i) {
-                               delete *i;
-                       }
 
-                       clicks.clear ();
+void
+Session::add_post_transport_work (PostTransportWork ptw)
+{
+       PostTransportWork oldval;
+       PostTransportWork newval;
+       int tries = 0;
+
+       while (tries < 8) {
+               oldval = (PostTransportWork) g_atomic_int_get (&_post_transport_work);
+               newval = PostTransportWork (oldval | ptw);
+               if (g_atomic_int_compare_and_exchange (&_post_transport_work, oldval, newval)) {
+                       /* success */
+                       return;
                }
        }
 
-       if (with_roll) {
-               /* switch from input if we're going to roll */
-               if (Config->get_monitoring_model() == HardwareMonitoring) {
-                       set_track_monitor_input_status (!config.get_auto_input());
-               }
-       } else {
-               /* otherwise we're going to stop, so do the opposite */
-               if (Config->get_monitoring_model() == HardwareMonitoring) {
-                       set_track_monitor_input_status (true);
+       error << "Could not set post transport work! Crazy thread madness, call the programmers" << endmsg;
+}
+
+bool
+Session::should_ignore_transport_request (TransportRequestSource src, TransportRequestType type) const
+{
+       if (config.get_external_sync()) {
+               if (TransportMasterManager::instance().current()->allow_request (src, type)) {
+                       return false;
+               } else {
+                       return true;
                }
        }
+       return false;
+}
 
-       /* cancel looped playback if transport pos outside of loop range */
-       if (play_loop) {
+bool
+Session::synced_to_engine() const {
+       return config.get_external_sync() && TransportMasterManager::instance().current()->type() == Engine;
+}
 
-               Location* al = _locations->auto_loop_location();
+void
+Session::request_sync_source (boost::shared_ptr<TransportMaster> tm)
+{
+       SessionEvent* ev = new SessionEvent (SessionEvent::SetTransportMaster, SessionEvent::Add, SessionEvent::Immediate, 0, 0.0);
+       ev->transport_master = tm;
+       DEBUG_TRACE (DEBUG::Slave, "sent request for new transport master\n");
+       queue_event (ev);
+}
 
-               if (al) {
-                       if (_transport_sample < al->start() || _transport_sample >= al->end()) {
+void
+Session::request_transport_speed (double speed, bool as_default, TransportRequestSource origin)
+{
+       if (should_ignore_transport_request (origin, TR_Speed)) {
+               return;
+       }
+       SessionEvent* ev = new SessionEvent (SessionEvent::SetTransportSpeed, SessionEvent::Add, SessionEvent::Immediate, 0, speed);
+       ev->third_yes_or_no = as_default; // as_default
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request transport speed = %1 as default = %2\n", speed, as_default));
+       queue_event (ev);
+}
 
-                               // located outside the loop: cancel looping directly, this is called from event handling context
+/** Request a new transport speed, but if the speed parameter is exactly zero then use
+ *  a very small +ve value to prevent the transport actually stopping.  This method should
+ *  be used by callers who are varying transport speed but don't ever want to stop it.
+ */
+void
+Session::request_transport_speed_nonzero (double speed, bool as_default, TransportRequestSource origin)
+{
+       if (should_ignore_transport_request (origin, TransportRequestType (TR_Speed|TR_Start))) {
+               return;
+       }
 
-                               have_looped = false;
+       if (speed == 0) {
+               speed = DBL_EPSILON;
+       }
 
-                               if (!Config->get_loop_is_mode()) {
-                                       set_play_loop (false, _transport_speed);
-                               } else {
-                                       if (Config->get_seamless_loop()) {
-                                               /* this will make the non_realtime_locate() in the butler
-                                                  which then causes seek() in tracks actually do the right
-                                                  thing.
-                                               */
-                                               set_track_loop (false);
-                                       }
-                               }
+       request_transport_speed (speed, as_default);
+}
 
-                       } else if (_transport_sample == al->start()) {
+void
+Session::request_stop (bool abort, bool clear_state, TransportRequestSource origin)
+{
+       if (should_ignore_transport_request (origin, TR_Stop)) {
+               return;
+       }
 
-                               // located to start of loop - this is looping, basically
+       SessionEvent* ev = new SessionEvent (SessionEvent::SetTransportSpeed, SessionEvent::Add, SessionEvent::Immediate, audible_sample(), 0.0, abort, clear_state);
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request transport stop, audible %3 transport %4 abort = %1, clear state = %2\n", abort, clear_state, audible_sample(), _transport_sample));
+       queue_event (ev);
+}
 
-                               if (!have_looped) {
-                                       /* first time */
-                                       if (_last_roll_location != al->start()) {
-                                               /* didn't start at loop start - playback must have
-                                                * started before loop since we've now hit the loop
-                                                * end.
-                                                */
-                                               add_post_transport_work (PostTransportLocate);
-                                               need_butler = true;
-                                       }
+void
+Session::request_locate (samplepos_t target_sample, bool with_roll, TransportRequestSource origin)
+{
+       if (should_ignore_transport_request (origin, TR_Locate)) {
+               return;
+       }
 
-                               }
+       SessionEvent *ev = new SessionEvent (with_roll ? SessionEvent::LocateRoll : SessionEvent::Locate, SessionEvent::Add, SessionEvent::Immediate, target_sample, 0, false);
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request locate to %1\n", target_sample));
+       queue_event (ev);
+}
 
-                               boost::shared_ptr<RouteList> rl = routes.reader();
+void
+Session::force_locate (samplepos_t target_sample, bool with_roll)
+{
+       SessionEvent *ev = new SessionEvent (with_roll ? SessionEvent::LocateRoll : SessionEvent::Locate, SessionEvent::Add, SessionEvent::Immediate, target_sample, 0, true);
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request forced locate to %1\n", target_sample));
+       queue_event (ev);
+}
 
-                               for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
-                                       boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
+void
+Session::unset_preroll_record_trim ()
+{
+       _preroll_record_trim_len = 0;
+}
 
-                                       if (tr && tr->rec_enable_control()->get_value()) {
-                                               // tell it we've looped, so it can deal with the record state
-                                               tr->transport_looped (_transport_sample);
-                                       }
-                               }
+void
+Session::request_preroll_record_trim (samplepos_t rec_in, samplecnt_t preroll)
+{
+       if (actively_recording ()) {
+               return;
+       }
+       unset_preroll_record_trim ();
+
+       config.set_punch_in (false);
+       config.set_punch_out (false);
+
+       samplepos_t pos = std::max ((samplepos_t)0, rec_in - preroll);
+       _preroll_record_trim_len = preroll;
+       maybe_enable_record ();
+       request_locate (pos, true);
+       set_requested_return_sample (rec_in);
+}
+
+void
+Session::request_count_in_record ()
+{
+       if (actively_recording ()) {
+               return;
+       }
+       if (transport_rolling()) {
+               return;
+       }
+       maybe_enable_record ();
+       _count_in_once = true;
+       request_transport_speed (1.0, true);
+}
+
+void
+Session::request_play_loop (bool yn, bool change_transport_roll)
+{
+       if (transport_master_is_external() && yn) {
+               // don't attempt to loop when not using Internal Transport
+               // see also gtk2_ardour/ardour_ui_options.cc parameter_changed()
+               return;
+       }
+
+       SessionEvent* ev;
+       Location *location = _locations->auto_loop_location();
+       double target_speed;
+
+       if (location == 0 && yn) {
+               error << _("Cannot loop - no loop range defined")
+                     << endmsg;
+               return;
+       }
+
+       if (change_transport_roll) {
+               if (transport_rolling()) {
+                       /* start looping at current speed */
+                       target_speed = transport_speed ();
+               } else {
+                       /* currently stopped */
+                       if (yn) {
+                               /* start looping at normal speed */
+                               target_speed = 1.0;
+                       } else {
+                               target_speed = 0.0;
+                       }
+               }
+       } else {
+               /* leave the speed alone */
+               target_speed = transport_speed ();
+       }
+
+       ev = new SessionEvent (SessionEvent::SetLoop, SessionEvent::Add, SessionEvent::Immediate, 0, target_speed, yn);
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request set loop = %1, change roll state ? %2\n", yn, change_transport_roll));
+       queue_event (ev);
+
+       if (yn) {
+               if (!change_transport_roll) {
+                       if (!transport_rolling()) {
+                               /* we're not changing transport state, but we do want
+                                  to set up position for the new loop. Don't
+                                  do this if we're rolling already.
+                               */
+                               request_locate (location->start(), false);
+                       }
+               }
+       } else {
+               if (!change_transport_roll && Config->get_seamless_loop() && transport_rolling()) {
+                       // request an immediate locate to refresh the tracks
+                       // after disabling looping
+                       request_locate (_transport_sample-1, false);
+               }
+       }
+}
+
+void
+Session::request_play_range (list<AudioRange>* range, bool leave_rolling)
+{
+       SessionEvent* ev = new SessionEvent (SessionEvent::SetPlayAudioRange, SessionEvent::Add, SessionEvent::Immediate, 0, (leave_rolling ? 1.0 : 0.0));
+       if (range) {
+               ev->audio_range = *range;
+       } else {
+               ev->audio_range.clear ();
+       }
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("Request play range, leave rolling ? %1\n", leave_rolling));
+       queue_event (ev);
+}
+
+void
+Session::request_cancel_play_range ()
+{
+       SessionEvent* ev = new SessionEvent (SessionEvent::CancelPlayAudioRange, SessionEvent::Add, SessionEvent::Immediate, 0, 0);
+       queue_event (ev);
+}
+
+
+bool
+Session::solo_selection_active ()
+{
+       if (_soloSelection.empty()) {
+               return false;
+       }
+       return true;
+}
+
+void
+Session::solo_selection (StripableList &list, bool new_state)
+{
+       boost::shared_ptr<ControlList> solo_list (new ControlList);
+       boost::shared_ptr<ControlList> unsolo_list (new ControlList);
+
+       if (new_state)
+               _soloSelection = list;
+       else
+               _soloSelection.clear();
+
+       boost::shared_ptr<RouteList> rl = get_routes();
+
+       for (ARDOUR::RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
 
-                               have_looped = true;
-                               TransportLooped(); // EMIT SIGNAL
-                       }
+               if ( !(*i)->is_track() ) {
+                       continue;
                }
-       }
 
-       if (need_butler) {
-               _butler->schedule_transport_work ();
-       }
+               boost::shared_ptr<Stripable> s (*i);
 
-       loop_changing = false;
+               bool found = (std::find(list.begin(), list.end(), s) != list.end());
+               if ( new_state && found ) {
 
-       _send_timecode_update = true;
+                       solo_list->push_back (s->solo_control());
 
-       if (with_mmc) {
-               send_mmc_locate (_transport_sample);
+                       //must invalidate playlists on selected tracks, so only selected regions get heard
+                       boost::shared_ptr<Track> track = boost::dynamic_pointer_cast<Track> (*i);
+                       if (track) {
+                               boost::shared_ptr<Playlist> playlist = track->playlist();
+                               if (playlist) {
+                                       playlist->ContentsChanged();
+                               }
+                       }
+               } else {
+                       unsolo_list->push_back (s->solo_control());
+               }
        }
 
-       _last_roll_location = _last_roll_or_reversal_location =  _transport_sample;
-       if (!synced_to_engine () || _transport_sample == _engine.transport_sample ()) {
-               Located (); /* EMIT SIGNAL */
-       }
+       set_controls (solo_list, 1.0, Controllable::NoGroup);
+       set_controls (unsolo_list, 0.0, Controllable::NoGroup);
 }
 
-/** Set the transport speed.
- *  Called from the process thread.
- *  @param speed New speed
- */
+
 void
-Session::set_transport_speed (double speed, samplepos_t destination_sample, bool abort, bool clear_state, bool as_default)
+Session::butler_transport_work ()
 {
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("@ %5 Set transport speed to %1, abort = %2 clear_state = %3, current = %4 as_default %6\n",
-                                                      speed, abort, clear_state, _transport_speed, _transport_sample, as_default));
+       /* Note: this function executes in the butler thread context */
 
-       /* max speed is somewhat arbitrary but based on guestimates regarding disk i/o capability
-          and user needs. We really need CD-style "skip" playback for ffwd and rewind.
-       */
+  restart:
+       boost::shared_ptr<RouteList> r = routes.reader ();
+       int on_entry = g_atomic_int_get (&_butler->should_do_transport_work);
+       bool finished = true;
+       PostTransportWork ptw = post_transport_work();
+       uint64_t before;
 
-       if (speed > 0) {
-               speed = min (8.0, speed);
-       } else if (speed < 0) {
-               speed = max (-8.0, speed);
-       }
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("Butler transport work, todo = [%1] (0x%3%4%5) at %2\n", enum_2_string (ptw), (before = g_get_monotonic_time()), std::hex, ptw, std::dec));
 
-       double new_engine_speed = 1.0;
-       if (speed != 0) {
-               new_engine_speed = fabs (speed);
-               if (speed < 0) speed = -1;
-               if (speed > 0) speed = 1;
-       }
+       if (ptw & PostTransportLocate) {
 
-       if (_transport_speed == speed && new_engine_speed == _engine_speed) {
-               if (as_default && speed == 0.0) { // => reset default transport speed. hacky or what?
-                       _default_transport_speed = 1.0;
-               }
-               return;
-       }
+               if (get_play_loop() && !Config->get_seamless_loop()) {
 
-#if 0 // TODO pref: allow vari-speed recording
-       if (actively_recording() && speed != 1.0 && speed != 0.0) {
-               /* no varispeed during recording */
-               DEBUG_TRACE (DEBUG::Transport, string_compose ("No varispeed during recording cur_speed %1, sample %2\n",
-                                                      _transport_speed, _transport_sample));
-               return;
-       }
-#endif
+                       DEBUG_TRACE (DEBUG::Butler, "flush loop recording fragment to disk\n");
 
-       _target_transport_speed = fabs(speed);
-       _engine_speed = new_engine_speed;
+                       /* this locate might be happening while we are
+                        * loop recording.
+                        *
+                        * Non-seamless looping will require a locate (below) that
+                        * will reset capture buffers and throw away data.
+                        *
+                        * Rather than first find all tracks and see if they
+                        * have outstanding data, just do a flush anyway. It
+                        * may be cheaper this way anyway, and is certainly
+                        * more accurate.
+                        */
 
-       if (transport_rolling() && speed == 0.0) {
+                       bool more_disk_io_to_do = false;
+                       uint32_t errors = 0;
 
-               /* we are rolling and we want to stop */
+                       do {
+                               more_disk_io_to_do = _butler->flush_tracks_to_disk_after_locate (r, errors);
 
-               if (Config->get_monitoring_model() == HardwareMonitoring) {
-                       set_track_monitor_input_status (true);
-               }
+                               if (errors) {
+                                       break;
+                               }
 
-               if (synced_to_engine ()) {
-                       if (clear_state) {
-                               /* do this here because our response to the slave won't
-                                  take care of it.
-                               */
-                               _play_range = false;
-                               _count_in_once = false;
-                               unset_play_loop ();
-                       }
-                       _engine.transport_stop ();
-               } else {
-                       bool const auto_return_enabled = (!config.get_external_sync() && (Config->get_auto_return_target_list() || abort));
+                               if (more_disk_io_to_do) {
+                                       continue;
+                               }
 
-                       if (!auto_return_enabled) {
-                               _requested_return_sample = destination_sample;
-                       }
+                       } while (false);
 
-                       stop_transport (abort);
                }
+       }
 
-       } else if (transport_stopped() && speed == 1.0) {
-               if (as_default) {
-                       _default_transport_speed = speed;
+       if (ptw & PostTransportAdjustPlaybackBuffering) {
+               for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
+                       boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
+                       if (tr) {
+                               tr->adjust_playback_buffering ();
+                               /* and refill those buffers ... */
+                       }
+                       (*i)->non_realtime_locate (_transport_sample);
                }
-               /* we are stopped and we want to start rolling at speed 1 */
+               VCAList v = _vca_manager->vcas ();
+               for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
+                       (*i)->non_realtime_locate (_transport_sample);
+               }
+       }
 
-               if (Config->get_loop_is_mode() && play_loop) {
+       if (ptw & PostTransportAdjustCaptureBuffering) {
+               /* need to prevent concurrency with ARDOUR::DiskWriter::run(),
+                * DiskWriter::adjust_buffering() re-allocates the ringbuffer */
+               Glib::Threads::Mutex::Lock lx (AudioEngine::instance()->process_lock ());
+               for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
+                       boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
+                       if (tr) {
+                               tr->adjust_capture_buffering ();
+                       }
+               }
+       }
 
-                       Location *location = _locations->auto_loop_location();
+       if (ptw & PostTransportReverse) {
 
-                       if (location != 0) {
-                               if (_transport_sample != location->start()) {
+               clear_clicks();
 
-                                       if (Config->get_seamless_loop()) {
-                                               /* force tracks to do their thing */
-                                               set_track_loop (true);
-                                       }
+               /* don't seek if locate will take care of that in non_realtime_stop() */
 
-                                       /* jump to start and then roll from there */
+               if (!(ptw & PostTransportLocate)) {
+                       for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
+                               (*i)->non_realtime_locate (_transport_sample);
 
-                                       request_locate (location->start(), true);
-                                       return;
+                               if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) {
+                                       /* new request, stop seeking, and start again */
+                                       g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
+                                       goto restart;
                                }
                        }
+                       VCAList v = _vca_manager->vcas ();
+                       for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
+                               (*i)->non_realtime_locate (_transport_sample);
+                       }
                }
+       }
 
-               if (Config->get_monitoring_model() == HardwareMonitoring && config.get_auto_input()) {
-                       set_track_monitor_input_status (false);
+       if (ptw & PostTransportLocate) {
+               DEBUG_TRACE (DEBUG::Transport, "nonrealtime locate invoked from BTW\n");
+               non_realtime_locate ();
+       }
+
+       if (ptw & PostTransportStop) {
+               non_realtime_stop (ptw & PostTransportAbort, on_entry, finished);
+               if (!finished) {
+                       g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
+                       goto restart;
                }
+       }
 
-               if (synced_to_engine()) {
-                       _engine.transport_start ();
-                       _count_in_once = false;
-               } else {
-                       start_transport ();
+       if (ptw & PostTransportOverWrite) {
+               non_realtime_overwrite (on_entry, finished);
+               if (!finished) {
+                       g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
+                       goto restart;
                }
+       }
 
-       } else {
+       if (ptw & PostTransportAudition) {
+               non_realtime_set_audition ();
+       }
 
-               /* not zero, not 1.0 ... varispeed */
+       g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
 
-               // TODO handled transport start..  _remaining_latency_preroll
-               // and reversal of playback direction.
+       DEBUG_TRACE (DEBUG::Transport, string_compose (X_("Butler transport work all done after %1 usecs @ %2 trw = %3\n"), g_get_monotonic_time() - before, _transport_sample, _butler->transport_work_requested()));
+}
 
-               if ((synced_to_engine()) && speed != 0.0 && speed != 1.0) {
-                       warning << string_compose (
-                               _("Global varispeed cannot be supported while %1 is connected to JACK transport control"),
-                               PROGRAM_NAME)
-                               << endmsg;
-                       return;
+void
+Session::non_realtime_overwrite (int on_entry, bool& finished)
+{
+       boost::shared_ptr<RouteList> rl = routes.reader();
+       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
+               boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
+               if (tr && tr->pending_overwrite ()) {
+                       tr->overwrite_existing_buffers ();
                }
-
-#if 0
-               if (actively_recording()) {
+               if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) {
+                       finished = false;
                        return;
                }
-#endif
+       }
+}
 
-               if (speed > 0.0 && _transport_sample == current_end_sample()) {
-                       return;
+void
+Session::non_realtime_locate ()
+{
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("locate tracks to %1\n", _transport_sample));
+
+       if (Config->get_loop_is_mode() && get_play_loop()) {
+
+               Location *loc  = _locations->auto_loop_location();
+
+               if (!loc || (_transport_sample < loc->start() || _transport_sample >= loc->end())) {
+                       /* jumped out of loop range: stop tracks from looping,
+                          but leave loop (mode) enabled.
+                        */
+                       set_track_loop (false);
+
+               } else if (loc && Config->get_seamless_loop() &&
+                          ((loc->start() <= _transport_sample) || (loc->end() > _transport_sample))) {
+
+                       /* jumping to start of loop. This  might have been done before but it is
+                        * idempotent and cheap. Doing it here ensures that when we start playback
+                        * outside the loop we still flip tracks into the magic seamless mode
+                        * when needed.
+                        */
+                       set_track_loop (true);
+
+               } else if (loc) {
+                       set_track_loop (false);
                }
 
-               if (speed < 0.0 && _transport_sample == 0) {
-                       return;
-               }
+       } else {
 
-               clear_clicks ();
+               /* no more looping .. should have been noticed elsewhere */
+       }
 
-               /* if we are reversing relative to the current speed, or relative to the speed
-                  before the last stop, then we have to do extra work.
-               */
 
-               PostTransportWork todo = PostTransportWork (0);
+       samplepos_t tf;
 
-               if ((_transport_speed && speed * _transport_speed < 0.0) || (_last_transport_speed * speed < 0.0) || (_last_transport_speed == 0.0 && speed < 0.0)) {
-                       todo = PostTransportWork (todo | PostTransportReverse);
-                       _last_roll_or_reversal_location = _transport_sample;
-               }
+       {
+               boost::shared_ptr<RouteList> rl = routes.reader();
 
-               _last_transport_speed = _transport_speed;
-               _transport_speed = speed;
+         restart:
+               gint sc = g_atomic_int_get (&_seek_counter);
+               tf = _transport_sample;
 
-               if (as_default) {
-                       _default_transport_speed = speed;
+               for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
+                       (*i)->non_realtime_locate (tf);
+                       if (sc != g_atomic_int_get (&_seek_counter)) {
+                               std::cerr << "\n\nLOCATE INTERRUPTED BY LOCATE!!!\n\n";
+                               goto restart;
+                       }
                }
+       }
 
-               if (todo) {
-                       add_post_transport_work (todo);
-                       _butler->schedule_transport_work ();
+       {
+               /* VCAs are quick to locate because they have no data (except
+                  automation) associated with them. Don't bother with a
+                  restart mechanism here, but do use the same transport sample
+                  that the Routes used.
+               */
+               VCAList v = _vca_manager->vcas ();
+               for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
+                       (*i)->non_realtime_locate (tf);
                }
+       }
 
-               DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC3 with speed = %1\n", _transport_speed));
+       _scene_changer->locate (_transport_sample);
 
-               /* throttle signal emissions.
-                * when slaved [_last]_transport_speed
-                * usually changes every cycle (tiny amounts due to DLL).
-                * Emitting a signal every cycle is overkill and unwarranted.
-                *
-                * Using _last_transport_speed is not acceptable,
-                * since it allows for large changes over a long period
-                * of time. Hence we introduce a dedicated variable to keep track
-                *
-                * The 0.2% dead-zone is somewhat arbitrary. Main use-case
-                * for TransportStateChange() here is the ShuttleControl display.
-                */
-               if (fabs (_signalled_varispeed - actual_speed ()) > .002
-                   // still, signal hard changes to 1.0 and 0.0:
-                   || (actual_speed () == 1.0 && _signalled_varispeed != 1.0)
-                   || (actual_speed () == 0.0 && _signalled_varispeed != 0.0)
-                  )
-               {
-                       TransportStateChange (); /* EMIT SIGNAL */
-                       _signalled_varispeed = actual_speed ();
-               }
-       }
+       /* XXX: it would be nice to generate the new clicks here (in the non-RT thread)
+          rather than clearing them so that the RT thread has to spend time constructing
+          them (in Session::click).
+        */
+       clear_clicks ();
 }
 
-
-/** Stop the transport.  */
-void
-Session::stop_transport (bool abort, bool clear_state)
+bool
+Session::select_playhead_priority_target (samplepos_t& jump_to)
 {
-       _count_in_once = false;
-       if (_transport_speed == 0.0f) {
-               return;
+       if (!transport_master_no_external_or_using_engine() || !config.get_auto_return()) {
+               return false;
        }
 
-       DEBUG_TRACE (DEBUG::Transport, "time to actually stop\n");
-
-       realtime_stop (abort, clear_state);
-       _butler->schedule_transport_work ();
+       jump_to = _last_roll_location;
+       return jump_to >= 0;
 }
 
-/** Called from the process thread */
 void
-Session::start_transport ()
+Session::follow_playhead_priority ()
 {
-       DEBUG_TRACE (DEBUG::Transport, "start_transport\n");
+       samplepos_t target;
 
-       _last_roll_location = _transport_sample;
-       _last_roll_or_reversal_location = _transport_sample;
-       _remaining_latency_preroll = worst_latency_preroll ();
+       if (select_playhead_priority_target (target)) {
+               request_locate (target);
+       }
+}
 
-       have_looped = false;
+void
+Session::non_realtime_stop (bool abort, int on_entry, bool& finished)
+{
+       struct tm* now;
+       time_t     xnow;
+       bool       did_record;
+       bool       saved;
+       PostTransportWork ptw = post_transport_work();
 
-       /* if record status is Enabled, move it to Recording. if its
-          already Recording, move it to Disabled.
-       */
+       did_record = false;
+       saved = false;
 
-       switch (record_status()) {
-       case Enabled:
-               if (!config.get_punch_in()) {
-                       /* This is only for UIs (keep blinking rec-en before
-                        * punch-in, don't show rec-region etc). The UI still
-                        * depends on SessionEvent::PunchIn and ensuing signals.
-                        *
-                        * The disk-writers handle punch in/out internally
-                        * in their local delay-compensated timeframe.
-                        */
-                       enable_record ();
+       boost::shared_ptr<RouteList> rl = routes.reader();
+       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
+               boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
+               if (tr && tr->get_captured_samples () != 0) {
+                       did_record = true;
+                       break;
                }
-               break;
+       }
 
-       case Recording:
-               if (!play_loop) {
-                       disable_record (false);
-               }
-               break;
+       /* stop and locate are merged here because they share a lot of common stuff */
 
-       default:
-               break;
+       time (&xnow);
+       now = localtime (&xnow);
+
+       if (auditioner) {
+               auditioner->cancel_audition ();
        }
 
-       _transport_speed = _default_transport_speed;
-       _target_transport_speed = _transport_speed;
+       if (did_record) {
+               begin_reversible_command (Operations::capture);
+               _have_captured = true;
+       }
 
-       if (!_engine.freewheeling()) {
-               Timecode::Time time;
-               timecode_time_subframes (_transport_sample, time);
-               if (!dynamic_cast<MTC_Slave*>(_slave)) {
-                       send_immediate_mmc (MIDI::MachineControlCommand (MIDI::MachineControl::cmdDeferredPlay));
-               }
+       DEBUG_TRACE (DEBUG::Transport, X_("Butler PTW: DS stop\n"));
 
-               if (actively_recording() && click_data && (config.get_count_in () || _count_in_once)) {
-                       _count_in_once = false;
-                       /* calculate count-in duration (in audio samples)
-                        * - use [fixed] tempo/meter at _transport_sample
-                        * - calc duration of 1 bar + time-to-beat before or at transport_sample
-                        */
-                       const Tempo& tempo = _tempo_map->tempo_at_sample (_transport_sample);
-                       const Meter& meter = _tempo_map->meter_at_sample (_transport_sample);
+       if (abort && did_record) {
+               /* no reason to save the session file when we remove sources
+                */
+               _state_of_the_state = StateOfTheState (_state_of_the_state | InCleanup);
+       }
 
-                       const double num = meter.divisions_per_bar ();
-                       const double den = meter.note_divisor ();
-                       const double barbeat = _tempo_map->exact_qn_at_sample (_transport_sample, 0) * den / (4. * num);
-                       const double bar_fract = fmod (barbeat, 1.0); // fraction of bar elapsed.
+       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
+               boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
+               if (tr) {
+                       tr->transport_stopped_wallclock (*now, xnow, abort);
+               }
+       }
 
-                       _count_in_samples = meter.samples_per_bar (tempo, _current_sample_rate);
+       if (abort && did_record) {
+               _state_of_the_state = StateOfTheState (_state_of_the_state & ~InCleanup);
+       }
 
-                       double dt = _count_in_samples / num;
-                       if (bar_fract == 0) {
-                               /* at bar boundary, count-in 2 bars before start. */
-                               _count_in_samples *= 2;
-                       } else {
-                               /* beats left after full bar until roll position */
-                               _count_in_samples *= 1. + bar_fract;
-                       }
+       boost::shared_ptr<RouteList> r = routes.reader ();
 
-                       if (_count_in_samples > _remaining_latency_preroll) {
-                               _remaining_latency_preroll = _count_in_samples;
-                       }
+       if (did_record) {
+               commit_reversible_command ();
+               /* increase take name */
+               if (config.get_track_name_take () && !config.get_take_name ().empty()) {
+                       string newname = config.get_take_name();
+                       config.set_take_name(bump_name_number (newname));
+               }
+       }
 
-                       int clickbeat = 0;
-                       samplepos_t cf = _transport_sample - _count_in_samples;
-                       samplecnt_t offset = _click_io->connected_latency (true);
-                       while (cf < _transport_sample + offset) {
-                               add_click (cf, clickbeat == 0);
-                               cf += dt;
-                               clickbeat = fmod (clickbeat + 1, num);
-                       }
+       if (_engine.running()) {
+               PostTransportWork ptw = post_transport_work ();
 
-                       if (_count_in_samples < _remaining_latency_preroll) {
-                               _count_in_samples = _remaining_latency_preroll;
-                       }
+               for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
+                       (*i)->non_realtime_transport_stop (_transport_sample, !(ptw & PostTransportLocate));
                }
+               VCAList v = _vca_manager->vcas ();
+               for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
+                       (*i)->non_realtime_transport_stop (_transport_sample, !(ptw & PostTransportLocate));
+               }
+
+               update_latency_compensation ();
        }
 
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC4 with speed = %1\n", _transport_speed));
-       TransportStateChange (); /* EMIT SIGNAL */
-}
+       /* If we are not synced to a "true" external master, and we're not
+        * handling an explicit locate, we should consider whether or not to
+        * "auto-return". This could mean going to a specifically requested
+        * location, or just back to the start of the last roll.
+        */
 
-/** Do any transport work in the audio thread that needs to be done after the
- * transport thread is finished.  Audio thread, realtime safe.
- */
-void
-Session::post_transport ()
-{
-       PostTransportWork ptw = post_transport_work ();
+       if (transport_master_no_external_or_using_engine() && !(ptw & PostTransportLocate)) {
 
-       if (ptw & PostTransportAudition) {
-               if (auditioner && auditioner->auditioning()) {
-                       process_function = &Session::process_audition;
-               } else {
-                       process_function = &Session::process_with_events;
-               }
-       }
+               bool do_locate = false;
 
-       if (ptw & PostTransportStop) {
+               if (_requested_return_sample >= 0) {
 
-               transport_sub_state = 0;
-       }
+                       /* explicit return request pre-queued in event list. overrides everything else */
 
-       if (ptw & PostTransportLocate) {
+                       _transport_sample = _requested_return_sample;
 
-               if (((!config.get_external_sync() && (auto_play_legal && config.get_auto_play())) && !_exporting) || (ptw & PostTransportRoll)) {
-                       _count_in_once = false;
-                       start_transport ();
-               } else {
-                       transport_sub_state = 0;
-               }
-       }
+                       /* cancel this request */
+                       _requested_return_sample = -1;
+                       do_locate = true;
 
-       set_next_event ();
-       /* XXX is this really safe? shouldn't we just be unsetting the bits that we actually
-          know were handled ?
-       */
-       set_post_transport_work (PostTransportWork (0));
-}
+               } else if (Config->get_auto_return_target_list()) {
 
-void
-Session::reset_rf_scale (samplecnt_t motion)
-{
-       cumulative_rf_motion += motion;
-
-       if (cumulative_rf_motion < 4 * _current_sample_rate) {
-               rf_scale = 1;
-       } else if (cumulative_rf_motion < 8 * _current_sample_rate) {
-               rf_scale = 4;
-       } else if (cumulative_rf_motion < 16 * _current_sample_rate) {
-               rf_scale = 10;
-       } else {
-               rf_scale = 100;
-       }
+                       samplepos_t jump_to;
 
-       if (motion != 0) {
-               set_dirty();
-       }
-}
+                       if (select_playhead_priority_target (jump_to)) {
+
+                               /* there's a valid target (we don't care how it
+                                * was derived here)
+                                */
 
-void
-Session::mtc_status_changed (bool yn)
-{
-       g_atomic_int_set (&_mtc_active, yn);
-       MTCSyncStateChanged( yn );
-}
+                               _transport_sample = jump_to;
+                               do_locate = true;
 
-void
-Session::ltc_status_changed (bool yn)
-{
-       g_atomic_int_set (&_ltc_active, yn);
-       LTCSyncStateChanged( yn );
-}
+                       } else if (abort) {
 
-void
-Session::use_sync_source (Slave* new_slave)
-{
-       /* Runs in process() context */
+                               /* roll aborted (typically capture) with
+                                * auto-return enabled
+                                */
 
-       if (!_slave && !new_slave) {
-               return;
-       }
+                               _transport_sample = _last_roll_location;
+                               do_locate = true;
 
-       bool non_rt_required = false;
+                       }
+               }
 
-       /* XXX this deletion is problematic because we're in RT context */
 
-       delete _slave;
-       _slave = new_slave;
+               if (do_locate && synced_to_engine()) {
 
+                       /* We will unconditionally locate to _transport_sample
+                        * below, which will refill playback buffers based on
+                        * _transport_sample, and maximises the buffering they
+                        * represent.
+                        *
+                        * But if we are synced to engine (JACK), we should
+                        * locate the engine (JACK) as well. We would follow
+                        * the engine (JACK) on the next process cycle, but
+                        * since we're going to do a locate below anyway,
+                        * it seems pointless to not use just do it ourselves
+                        * right now, rather than wait for the engine (JACK) to
+                        * provide the new position on the next cycle.
+                        *
+                        * Despite the generic name of the called method
+                        * (::transport_locate()) this method only does
+                        * anything if the audio/MIDI backend is JACK.
+                        */
 
-       /* slave change, reset any DiskIO block on disk output because it is no
-          longer valid with a new slave.
-       */
-       DiskReader::set_no_disk_output (false);
+                       _engine.transport_locate (_transport_sample);
 
-       MTC_Slave* mtc_slave = dynamic_cast<MTC_Slave*>(_slave);
-       if (mtc_slave) {
-               mtc_slave->ActiveChanged.connect_same_thread (mtc_status_connection, boost::bind (&Session::mtc_status_changed, this, _1));
-               MTCSyncStateChanged(mtc_slave->locked() );
-       } else {
-               if (g_atomic_int_get (&_mtc_active) ){
-                       g_atomic_int_set (&_mtc_active, 0);
-                       MTCSyncStateChanged( false );
                }
-               mtc_status_connection.disconnect ();
        }
 
-       LTC_Slave* ltc_slave = dynamic_cast<LTC_Slave*> (_slave);
-       if (ltc_slave) {
-               ltc_slave->ActiveChanged.connect_same_thread (ltc_status_connection, boost::bind (&Session::ltc_status_changed, this, _1));
-               LTCSyncStateChanged (ltc_slave->locked() );
-       } else {
-               if (g_atomic_int_get (&_ltc_active) ){
-                       g_atomic_int_set (&_ltc_active, 0);
-                       LTCSyncStateChanged( false );
+       clear_clicks();
+       unset_preroll_record_trim ();
+
+       /* do this before seeking, because otherwise the tracks will do the wrong thing in seamless loop mode.
+       */
+
+       if (ptw & (PostTransportClearSubstate|PostTransportStop)) {
+               unset_play_range ();
+               if (!Config->get_loop_is_mode()) {
+                       unset_play_loop ();
                }
-               ltc_status_connection.disconnect ();
        }
 
-       DEBUG_TRACE (DEBUG::Slave, string_compose ("set new slave to %1\n", _slave));
+       /* this for() block can be put inside the previous if() and has the effect of ... ??? what */
 
-       // need to queue this for next process() cycle
-       _send_timecode_update = true;
+       {
+               DEBUG_TRACE (DEBUG::Transport, X_("Butler PTW: locate\n"));
+               for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
+                       DEBUG_TRACE (DEBUG::Transport, string_compose ("Butler PTW: locate on %1\n", (*i)->name()));
+                       (*i)->non_realtime_locate (_transport_sample);
 
-       boost::shared_ptr<RouteList> rl = routes.reader();
-       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
-               boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
-               if (tr && !tr->is_private_route()) {
-                       tr->set_slaved (_slave != 0);
+                       if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) {
+                               finished = false;
+                               /* we will be back */
+                               return;
+                       }
                }
        }
 
-       if (non_rt_required) {
-               add_post_transport_work (PostTransportSpeed);
-               _butler->schedule_transport_work ();
+       {
+               VCAList v = _vca_manager->vcas ();
+               for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
+                       (*i)->non_realtime_locate (_transport_sample);
+               }
        }
 
-       set_dirty();
-}
+       have_looped = false;
 
-void
-Session::drop_sync_source ()
-{
-       request_sync_source (0);
-}
+       /* don't bother with this stuff if we're disconnected from the engine,
+          because there will be no process callbacks to deliver stuff from
+       */
 
-void
-Session::switch_to_sync_source (SyncSource src)
-{
-       Slave* new_slave;
+       if (_engine.running() && !_engine.freewheeling()) {
+               // need to queue this in the next RT cycle
+               _send_timecode_update = true;
 
-       DEBUG_TRACE (DEBUG::Slave, string_compose ("Setting up sync source %1\n", enum_2_string (src)));
+               if (transport_master()->type() == MTC) {
+                       send_immediate_mmc (MIDI::MachineControlCommand (MIDI::MachineControl::cmdStop));
 
-       switch (src) {
-       case MTC:
-               if (_slave && dynamic_cast<MTC_Slave*>(_slave)) {
-                       return;
-               }
+                       /* This (::non_realtime_stop()) gets called by main
+                          process thread, which will lead to confusion
+                          when calling AsyncMIDIPort::write().
 
-               try {
-                       new_slave = new MTC_Slave (*this, *_midi_ports->mtc_input_port());
+                          Something must be done. XXX
+                       */
+                       send_mmc_locate (_transport_sample);
                }
+       }
 
-               catch (failed_constructor& err) {
-                       return;
+       if ((ptw & PostTransportLocate) && get_record_enabled()) {
+               /* This is scheduled by realtime_stop(), which is also done
+                * when a slave requests /locate/ for an initial sync.
+                * We can't hold up the slave for long with a save() here,
+                * without breaking its initial sync cycle.
+                *
+                * save state only if there's no slave or if it's not yet locked.
+                */
+               if (!transport_master_is_external() || !transport_master()->locked()) {
+                       DEBUG_TRACE (DEBUG::Transport, X_("Butler PTW: requests save\n"));
+                       SaveSessionRequested (_current_snapshot_name);
+                       saved = true;
                }
-               break;
+       }
 
-       case LTC:
-               if (_slave && dynamic_cast<LTC_Slave*>(_slave)) {
-                       return;
-               }
+       /* save the current state of things if appropriate */
 
-               try {
-                       new_slave = new LTC_Slave (*this);
-               }
+       if (did_record && !saved) {
+               SaveSessionRequested (_current_snapshot_name);
+       }
 
-               catch (failed_constructor& err) {
-                       return;
-               }
+       PositionChanged (_transport_sample); /* EMIT SIGNAL */
+       DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC with speed = %1\n", _transport_speed));
+       TransportStateChange (); /* EMIT SIGNAL */
+       AutomationWatch::instance().transport_stop_automation_watches (_transport_sample);
 
-               break;
+       ptw = PostTransportWork (ptw & ~(PostTransportStop|PostTransportClearSubstate));
+       set_post_transport_work (ptw);
+}
 
-       case MIDIClock:
-               if (_slave && dynamic_cast<MIDIClock_Slave*>(_slave)) {
-                       return;
-               }
+void
+Session::unset_play_loop ()
+{
+       if (play_loop) {
+               play_loop = false;
+               clear_events (SessionEvent::AutoLoop);
+               set_track_loop (false);
 
-               try {
-                       new_slave = new MIDIClock_Slave (*this, *_midi_ports->midi_clock_input_port(), 24);
-               }
 
-               catch (failed_constructor& err) {
-                       return;
+               if (Config->get_seamless_loop()) {
+                       /* likely need to flush track buffers: this will locate us to wherever we are */
+                       add_post_transport_work (PostTransportLocate);
+                       TFSM_EVENT (TransportFSM::ButlerRequired);
                }
-               break;
+               TransportStateChange (); /* EMIT SIGNAL */
+       }
+}
 
-       case Engine:
-               if (_slave && dynamic_cast<Engine_Slave*>(_slave)) {
-                       return;
-               }
+void
+Session::set_track_loop (bool yn)
+{
+       Location* loc = _locations->auto_loop_location ();
 
-               if (config.get_video_pullup() != 0.0f) {
-                       return;
-               }
+       if (!loc) {
+               yn = false;
+       }
 
-               new_slave = new Engine_Slave (*AudioEngine::instance());
-               break;
+       boost::shared_ptr<RouteList> rl = routes.reader ();
 
-       default:
-               new_slave = 0;
-               break;
-       };
+       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
+               if (*i && !(*i)->is_private_route()) {
+                       (*i)->set_loop (yn ? loc : 0);
+               }
+       }
+}
 
-       request_sync_source (new_slave);
+samplecnt_t
+Session::worst_latency_preroll () const
+{
+       return _worst_output_latency + _worst_input_latency;
 }
 
 void
@@ -1993,27 +1841,37 @@ Session::request_roll_at_and_return (samplepos_t start, samplepos_t return_to)
 void
 Session::engine_halted ()
 {
-       bool ignored;
-
        /* there will be no more calls to process(), so
           we'd better clean up for ourselves, right now.
 
-          but first, make sure the butler is out of
-          the picture.
+          We can't queue SessionEvents because they only get
+          handled from within a process callback.
        */
 
-       if (_butler) {
-               _butler->stop ();
-       }
+       /* this just stops the FSM engine ... it doesn't change the state of
+        * the FSM directly or anything else ... but the FSM will be
+        * reinitialized when we call its ::start() method from
+        * ::engine_running() (if we ever get there)
+        */
 
-       realtime_stop (false, true);
-       non_realtime_stop (false, 0, ignored);
-       transport_sub_state = 0;
+       _transport_fsm->stop ();
 
-       DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC6 with speed = %1\n", _transport_speed));
-       TransportStateChange (); /* EMIT SIGNAL */
+       /* Synchronously do the realtime part of a transport stop.
+        *
+        * Calling this will cause the butler to asynchronously run
+        * ::non_realtime_stop() where the rest of the "stop" work will be
+        * done.
+        */
+
+       realtime_stop (false, true);
 }
 
+void
+Session::engine_running ()
+{
+       initialize_latencies ();
+       _transport_fsm->start ();
+}
 
 void
 Session::xrun_recovery ()
@@ -2061,19 +1919,6 @@ Session::allow_auto_play (bool yn)
        auto_play_legal = yn;
 }
 
-bool
-Session::maybe_stop (samplepos_t limit)
-{
-       if ((_transport_speed > 0.0f && _transport_sample >= limit) || (_transport_speed < 0.0f && _transport_sample == 0)) {
-               if (synced_to_engine () && config.get_jack_time_master ()) {
-                       _engine.transport_stop ();
-               } else if (!synced_to_engine ()) {
-                       stop_transport ();
-               }
-               return true;
-       }
-       return false;
-}
 
 void
 Session::send_mmc_locate (samplepos_t t)
@@ -2112,3 +1957,121 @@ Session::timecode_transmission_suspended () const
 {
        return g_atomic_int_get (&_suspend_timecode_transmission) == 1;
 }
+
+boost::shared_ptr<TransportMaster>
+Session::transport_master() const
+{
+       return TransportMasterManager::instance().current();
+}
+
+bool
+Session::transport_master_is_external () const
+{
+       return TransportMasterManager::instance().current() && config.get_external_sync();
+}
+
+bool
+Session::transport_master_no_external_or_using_engine () const
+{
+       return !TransportMasterManager::instance().current() || !config.get_external_sync() || (TransportMasterManager::instance().current()->type() == Engine);
+}
+
+void
+Session::sync_source_changed (SyncSource type, samplepos_t pos, pframes_t cycle_nframes)
+{
+       /* Runs in process() context */
+
+       boost::shared_ptr<TransportMaster> master = TransportMasterManager::instance().current();
+
+       /* save value of seamless from before the switch */
+       _was_seamless = Config->get_seamless_loop ();
+
+       if (type == Engine) {
+               /* JACK cannot support seamless looping at present */
+               Config->set_seamless_loop (false);
+       } else {
+               /* reset to whatever the value was before we last switched slaves */
+               Config->set_seamless_loop (_was_seamless);
+       }
+
+       if (master->can_loop()) {
+               request_play_loop (false);
+       } else if (master->has_loop()) {
+               request_play_loop (true);
+       }
+
+       /* slave change, reset any DiskIO block on disk output because it is no
+          longer valid with a new slave.
+       */
+
+       DiskReader::dec_no_disk_output ();
+
+#if 0
+       we should not be treating specific transport masters as special cases because there maybe > 1 of a particular type
+
+       boost::shared_ptr<MTC_TransportMaster> mtc_master = boost::dynamic_pointer_cast<MTC_TransportMaster> (master);
+
+       if (mtc_master) {
+               mtc_master->ActiveChanged.connect_same_thread (mtc_status_connection, boost::bind (&Session::mtc_status_changed, this, _1));
+               MTCSyncStateChanged(mtc_master->locked() );
+       } else {
+               if (g_atomic_int_compare_and_exchange (&_mtc_active, 1, 0)) {
+                       MTCSyncStateChanged( false );
+               }
+               mtc_status_connection.disconnect ();
+       }
+
+       boost::shared_ptr<LTC_TransportMaster> ltc_master = boost::dynamic_pointer_cast<LTC_TransportMaster> (master);
+
+       if (ltc_master) {
+               ltc_master->ActiveChanged.connect_same_thread (ltc_status_connection, boost::bind (&Session::ltc_status_changed, this, _1));
+               LTCSyncStateChanged (ltc_master->locked() );
+       } else {
+               if (g_atomic_int_compare_and_exchange (&_ltc_active, 1, 0)) {
+                       LTCSyncStateChanged( false );
+               }
+               ltc_status_connection.disconnect ();
+       }
+#endif
+
+       DEBUG_TRACE (DEBUG::Slave, string_compose ("set new slave to %1\n", master));
+
+       // need to queue this for next process() cycle
+       _send_timecode_update = true;
+
+       boost::shared_ptr<RouteList> rl = routes.reader();
+       const bool externally_slaved = transport_master_is_external();
+
+       for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
+               boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
+               if (tr && !tr->is_private_route()) {
+                       tr->set_slaved (externally_slaved);
+               }
+       }
+
+       set_dirty();
+}
+
+bool
+Session::transport_stopped() const
+{
+       return _transport_fsm->stopped();
+}
+
+bool
+Session::transport_rolling() const
+{
+       return _transport_speed != 0.0 && _count_in_samples == 0 && _remaining_latency_preroll == 0;
+}
+
+bool
+Session::locate_pending () const
+{
+       return _transport_fsm->locating();
+}
+
+bool
+Session::declick_in_progress () const
+{
+       return _transport_fsm->declick_in_progress();
+}