/*
 * 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"
#endif

#include <cmath>
#include <cerrno>
#include <unistd.h>

#include <boost/algorithm/string/erase.hpp>

#include "pbd/error.h"
#include "pbd/enumwriter.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/audio_backend.h"
#include "ardour/audioengine.h"
#include "ardour/auditioner.h"
#include "ardour/automation_watch.h"
#include "ardour/butler.h"
#include "ardour/click.h"
#include "ardour/debug.h"
#include "ardour/disk_reader.h"
#include "ardour/location.h"
#include "ardour/playlist.h"
#include "ardour/profile.h"
#include "ardour/scene_changer.h"
#include "ardour/session.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"

using namespace std;
using namespace ARDOUR;
using namespace PBD;


#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


#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,ltd,flush,loop,force) { _transport_fsm->enqueue (new TransportFSM::Event (TransportFSM::Locate,target,ltd,flush,loop,force)); }

/* *****************************************************************************
 * REALTIME ACTIONS (to be called on state transitions)
 * ****************************************************************************/

void
Session::realtime_stop (bool abort, bool clear_state)
{
	ENSURE_PROCESS_THREAD;

	DEBUG_TRACE (DEBUG::Transport, string_compose ("realtime stop @ %1 speed = %2\n", _transport_sample, _transport_speed));
	PostTransportWork todo = PostTransportStop;

	/* this resets the speed we will start at if just requested to roll
	 * again. Don't do it if we are stopping to locate ... in those
	 * conditions, keep the current default speed so that when we start
	 * again we resume that speed
	 */
	if (!_transport_fsm->declicking_for_locate()) {
		_default_transport_speed = 1.0;
	}

	/* call routes */

	boost::shared_ptr<RouteList> r = routes.reader ();

	for (RouteList::iterator i = r->begin (); i != r->end(); ++i) {
		(*i)->realtime_handle_transport_stopped ();
	}

	DEBUG_TRACE (DEBUG::Transport, string_compose ("stop complete, auto-return scheduled for return to %1\n", _requested_return_sample));

	if (abort) {
		todo = PostTransportWork (todo | PostTransportAbort);
	}

	if (clear_state) {
		todo = PostTransportWork (todo | PostTransportClearSubstate);
	}

	if (todo) {
		add_post_transport_work (todo);
	}

	_clear_event_type (SessionEvent::RangeStop);
	_clear_event_type (SessionEvent::RangeLocate);

	/* 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));

	if (clear_state && !Config->get_loop_is_mode()) {
		unset_play_loop ();
	}

	reset_punch_loop_constraint ();

	_transport_speed = 0;
	_engine_speed = 1.0;

	g_atomic_int_set (&_playback_load, 100);
	g_atomic_int_set (&_capture_load, 100);

	if (config.get_use_video_sync()) {
		waiting_for_sync_offset = true;
	}

	if (todo) {
		TFSM_EVENT (TransportFSM::ButlerRequired);
	}
}

/** @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_end, bool force, bool with_mmc)
{
	ENSURE_PROCESS_THREAD;

	if (target_sample < 0) {
		error << _("Locate called for negative sample position - ignored") << endmsg;
		return;
	}

	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.
	 */

	DEBUG_TRACE (DEBUG::Transport, string_compose ("rt-locate to %1 ts = %7, roll %2 flush %3 for loop end %4 force %5 mmc %6\n",
	                                               target_sample, with_roll, with_flush, for_loop_end, force, with_mmc, _transport_sample));

	if (!force && (_transport_sample == target_sample) && !for_loop_end) {

		/* already at the desired position. Not forced to locate, 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, false, false, false);
		}
		TFSM_EVENT (TransportFSM::LocateDone);
		Located (); /* EMIT SIGNAL */
		return;
	}

	// Update Timecode time
	_transport_sample = target_sample;
	_nominal_jack_transport_sample = boost::none;
	// 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;
	if (!for_loop_end) {
		_remaining_latency_preroll = worst_latency_preroll_buffer_size_ceil ();
	}
	timecode_time(_transport_sample, transmitting_timecode_time); // XXX here?

	/* 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
	 *
	 */


	/* 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->rolling();

	if (!transport_was_stopped &&
	    (!auto_play_legal || !config.get_auto_play()) &&
	    !with_roll &&
	    !(synced_to_engine() && get_play_loop ()) &&
	    !(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 {

		/* Tell all routes to do the RT part of locate */

		boost::shared_ptr<RouteList> r = routes.reader ();
		for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
			(*i)->realtime_locate (for_loop_end);
		}
	}

	if (force || !for_loop_end) {

		PostTransportWork todo = PostTransportLocate;

		if (with_roll && transport_was_stopped) {
			todo = PostTransportWork (todo | PostTransportRoll);
		}

		add_post_transport_work (todo);
		need_butler = true;

	} else {

		/* this is functionally what clear_clicks() does but with a tentative lock */

		Glib::Threads::RWLock::WriterLock clickm (click_lock, Glib::Threads::TRY_LOCK);

		if (clickm.locked()) {

			for (Clicks::iterator i = clicks.begin(); i != clicks.end(); ++i) {
				delete *i;
			}

			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 {
		/* 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 (get_play_loop ()) {

		Location* al = _locations->auto_loop_location();

		if (al) {
			if (_transport_sample < al->start() || _transport_sample >= al->end()) {

				// located outside the loop: cancel looping directly, this is called from event handling context

				have_looped = false;

				if (!Config->get_loop_is_mode()) {
					set_play_loop (false, false);
				} else {
					/* 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);
				}

			} else if (_transport_sample == al->start()) {

				// located to start of loop - this is looping, basically

				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->rec_enable_control()->get_value()) {
						// tell it we've looped, so it can deal with the record state
						tr->transport_looped (_transport_sample);
					}
				}

				if (for_loop_end) {
					have_looped = true;
					TransportLooped(); // EMIT SIGNAL
				}
			}
		}
	}

	if (need_butler) {
		TFSM_EVENT (TransportFSM::ButlerRequired);
	} else {
		TFSM_EVENT (TransportFSM::LocateDone);
	}

	_send_timecode_update = true;

	if (with_mmc) {
		send_mmc_locate (_transport_sample);
	}

	_last_roll_location = _last_roll_or_reversal_location =  _transport_sample;

	Located (); /* EMIT SIGNAL */
}

void
Session::post_locate ()
{
	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::set_transport_speed (double speed, bool abort, bool clear_state, bool as_default)
{
	ENSURE_PROCESS_THREAD;
	DEBUG_TRACE (DEBUG::Transport, string_compose ("@ %5 Set transport speed to %1 from %4 (es = %7), abort = %2 clear_state = %3, as_default %6\n",
	                                               speed, abort, clear_state, _transport_speed, _transport_sample, as_default, _engine_speed));

	if ((_engine_speed != 1) && (_engine_speed == fabs (speed)) && (speed * _transport_speed) >= 0) {
		/* engine speed is not changing and no direction change, do nothing */
		DEBUG_TRACE (DEBUG::Transport, "no reason to change speed, do nothing\n");
		return;
	}

	/* max speed is somewhat arbitrary but based on guestimates regarding disk i/o capability
	   and user needs. XXX We really need CD-style "skip" playback for ffwd and rewind.
	*/

	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);
	}

	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 (_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 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

	_engine_speed = new_engine_speed;

	if (transport_rolling() && speed == 0.0) {

		/* we are rolling and we want to stop */

		if (Config->get_monitoring_model() == HardwareMonitoring) {
			set_track_monitor_input_status (true);
		}

		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 ();
			}
		}

		TFSM_STOP (abort, false);

	} 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 */

		if (Config->get_loop_is_mode() && get_play_loop ()) {

			Location *location = _locations->auto_loop_location();

			if (location != 0) {
				if (_transport_sample != location->start()) {

					/* force tracks to do their thing */
					set_track_loop (true);

					/* jump to start and then roll from there */

					request_locate (location->start(), MustRoll);
					return;
				}
			}
		}

		if (Config->get_monitoring_model() == HardwareMonitoring && config.get_auto_input()) {
			set_track_monitor_input_status (false);
		}

		TFSM_EVENT (TransportFSM::StartTransport);

	} else {

		/* not zero, not 1.0 ... varispeed */

		// TODO handled transport start..  _remaining_latency_preroll
		// and reversal of playback direction.

		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;
		}

#if 0
		if (actively_recording()) {
			return;
		}
#endif

		if (speed > 0.0 && _transport_sample == current_end_sample()) {
			return;
		}

		if (speed < 0.0 && _transport_sample == 0) {
			return;
		}

		clear_clicks ();

		/* 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.
		*/

		_transport_speed = speed;

		if (as_default) {
			_default_transport_speed = speed;
		}

		DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC3 with speed = %1\n", _transport_speed));

		/* 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 _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::stop_transport (bool abort, bool clear_state)
{
	ENSURE_PROCESS_THREAD;

	_count_in_once = false;

	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::start_transport ()
{
	ENSURE_PROCESS_THREAD;
	DEBUG_TRACE (DEBUG::Transport, "start_transport\n");

	_last_roll_location = _transport_sample;
	_last_roll_or_reversal_location = _transport_sample;
	if (!have_looped && !_exporting) {
		_remaining_latency_preroll = worst_latency_preroll_buffer_size_ceil ();
	}

	have_looped = false;

	/* if record status is Enabled, move it to Recording. if its
	   already Recording, move it to Disabled.
	*/

	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;

	case Recording:
		if (!get_play_loop ()) {
			disable_record (false);
		}
		break;

	default:
		break;
	}

	maybe_allow_only_loop ();
	maybe_allow_only_punch ();

	_transport_speed = _default_transport_speed;

	if (!_engine.freewheeling()) {
		Timecode::Time time;
		timecode_time_subframes (_transport_sample, time);
		if (transport_master()->type() != MTC) { // why not when slaved to MTC?
			send_immediate_mmc (MIDI::MachineControlCommand (MIDI::MachineControl::cmdDeferredPlay));
		}

		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);

			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.

			_count_in_samples = meter.samples_per_bar (tempo, _current_sample_rate);

			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;
			}

			if (_count_in_samples > _remaining_latency_preroll) {
				_remaining_latency_preroll = _count_in_samples;
			}

			int clickbeat = 0;
			samplepos_t cf = _transport_sample - _count_in_samples;
			samplecnt_t offset = _click_io->connected_latency (true);
			clear_clicks ();
			_clicks_cleared = cf;
			while (cf < _transport_sample + offset) {
				add_click (cf, clickbeat == 0);
				cf += dt;
				clickbeat = fmod (clickbeat + 1, num);
			}

			if (_count_in_samples < _remaining_latency_preroll) {
				_count_in_samples = _remaining_latency_preroll;
			}
		}
	}

	DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC4 with speed = %1\n", _transport_speed));
	TransportStateChange (); /* EMIT SIGNAL */
}

bool
Session::need_declick_before_locate () const
{
	/* At this time (July 2020) only audio playback from disk readers is
	   de-clicked. MIDI tracks with audio output really need it too.
	*/
	return naudiotracks() > 0;
}

bool
Session::should_roll_after_locate () const
{
	/* a locate must previously have been requested and completed before
	 * this answer can be considered correct
	 */

	return ((!config.get_external_sync() && (auto_play_legal && config.get_auto_play())) && !_exporting) || (post_transport_work() & PostTransportRoll);

}

/** 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::butler_completed_transport_work ()
{
	ENSURE_PROCESS_THREAD;
	PostTransportWork ptw = post_transport_work ();

	DEBUG_TRACE (DEBUG::Transport, string_compose ("Butler done, RT cleanup for %1\n", enum_2_string (ptw)));

	if (ptw & PostTransportAudition) {
		if (auditioner && auditioner->auditioning()) {
			_remaining_latency_preroll = 0;
			process_function = &Session::process_audition;
		} else {
			process_function = &Session::process_with_events;
		}
		ptw = PostTransportWork (ptw & ~PostTransportAudition);
		set_post_transport_work (ptw);
	}

	if (ptw & PostTransportLocate) {
		post_locate ();
		ptw = PostTransportWork (ptw & ~PostTransportLocate);
		set_post_transport_work (ptw);
		TFSM_EVENT (TransportFSM::LocateDone);
	}

	bool start_after_butler_done_msg = false;

	if (ptw & PostTransportRoll) {
		start_after_butler_done_msg = true;
	}

	/* the butler finished its work so clear all PostTransportWork flags
	 */

	set_post_transport_work (PostTransportWork (0));

	set_next_event ();

	if (_transport_fsm->waiting_for_butler()) {
		TFSM_EVENT (TransportFSM::ButlerDone);
	}

	if (start_after_butler_done_msg) {
		if (_transport_speed) {
			/* reversal is done ... tell TFSM that it is time to start*/
			TFSM_EVENT (TransportFSM::StartTransport);
		}
	}
}

void
Session::schedule_butler_for_transport_work ()
{
	assert (_transport_fsm->waiting_for_butler ());
	DEBUG_TRACE (DEBUG::Butler, string_compose ("summon butler for transport work (%1)\n", enum_2_string (post_transport_work())));
	_butler->schedule_transport_work ();
}

bool
Session::maybe_stop (samplepos_t limit)
{
	ENSURE_PROCESS_THREAD;
	if ((_transport_speed > 0.0f && _transport_sample >= limit) || (_transport_speed < 0.0f && _transport_sample == 0)) {
		if (synced_to_engine ()) {
			_engine.transport_stop ();
		} else {
			TFSM_STOP (false, false);
		}
		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);
		if (tr && !tr->can_internal_playback_seek (distance)) {
			return -1;
		}
	}

	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) {
			tr->internal_playback_seek (distance);
		}
	}

	_transport_sample += distance;
	return 0;
}

void
Session::flush_all_inserts ()
{
	ENSURE_PROCESS_THREAD;
	boost::shared_ptr<RouteList> r = routes.reader ();

	for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
		(*i)->flush_processors ();
	}
}

/* *****************************************************************************
 * END REALTIME ACTIONS
 * ****************************************************************************/

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;
}

bool
Session::should_ignore_transport_request (TransportRequestSource src, TransportRequestType type)
{
	if (config.get_external_sync()) {
		if (TransportMasterManager::instance().current()->allow_request (src, type)) {
			/* accepting a command means dropping external sync first */
			config.set_external_sync (false);
			return true;
		}
	}
	return false;
}

bool
Session::synced_to_engine() const
{
	return config.get_external_sync() && TransportMasterManager::instance().current()->type() == Engine;
}

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);
}

void
Session::request_transport_speed (double speed, bool as_default, TransportRequestSource origin)
{
	if (synced_to_engine()) {
		if (speed != 0) {
			_engine.transport_start ();
		} else {
			_engine.transport_stop ();
		}
		return;
	}

	if (speed == 1. || speed == 0. || speed == -1.) {
		if (should_ignore_transport_request (origin, TR_StartStop)) {
			return;
		}
	} else {
		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);
}

/** 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 (speed == 0) {
		speed = DBL_EPSILON;
	}

	request_transport_speed (speed, as_default);
}

void
Session::request_stop (bool abort, bool clear_state, TransportRequestSource origin)
{
	if (synced_to_engine()) {
		_engine.transport_stop ();
		return;
	}

	if (should_ignore_transport_request (origin, TR_StartStop)) {
		return;
	}

	/* clear our solo-selection, if there is one */
	if ( solo_selection_active() ) {
		solo_selection ( _soloSelection, false );
	}

	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, LocateTransportDisposition ltd, TransportRequestSource origin)
{
	if (synced_to_engine()) {
		_engine.transport_locate (target_sample);
		return;
	}

	if (should_ignore_transport_request (origin, TR_Locate)) {
		return;
	}

	SessionEvent::Type type;

	switch (ltd) {
	case MustRoll:
		type = SessionEvent::LocateRoll;
		break;
	case MustStop:
		type = SessionEvent::Locate;
		break;
	case RollIfAppropriate:
		if (config.get_auto_play()) {
			type = SessionEvent::LocateRoll;
		} else {
			type = SessionEvent::Locate;
		}
		break;
	}

	SessionEvent *ev = new SessionEvent (type, SessionEvent::Add, SessionEvent::Immediate, target_sample, 0, false);
	ev->locate_transport_disposition = ltd;
	DEBUG_TRACE (DEBUG::Transport, string_compose ("Request locate to %1 ltd = %2\n", target_sample, enum_2_string (ltd)));
	queue_event (ev);
}

void
Session::force_locate (samplepos_t target_sample, LocateTransportDisposition ltd)
{
	SessionEvent *ev = new SessionEvent (SessionEvent::Locate, SessionEvent::Add, SessionEvent::Immediate, target_sample, 0, true);
	ev->locate_transport_disposition = ltd;
	DEBUG_TRACE (DEBUG::Transport, string_compose ("Request forced locate to %1 roll %2\n", target_sample, enum_2_string (ltd)));
	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, MustRoll);
	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, change_transport_roll);
	DEBUG_TRACE (DEBUG::Transport, string_compose ("Request set loop = %1, change roll state ? %2\n", yn, change_transport_roll));
	queue_event (ev);
}

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);

	boost::shared_ptr<RouteList> rl = get_routes();

	for (ARDOUR::RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {

		if ( !(*i)->is_track() ) {
			continue;
		}

		boost::shared_ptr<Stripable> s (*i);

		bool found = (std::find(list.begin(), list.end(), s) != list.end());
		if ( found ) {
			/* must invalidate playlists on selected track, so disk reader
			 * will re-fill with the new selection state for solo_selection */
			boost::shared_ptr<Track> track = boost::dynamic_pointer_cast<Track> (*i);
			if (track) {
				boost::shared_ptr<Playlist> playlist = track->playlist();
				if (playlist) {
					playlist->ContentsChanged();
				}
			}
		}

		if ( found & new_state ) {
			solo_list->push_back (s->solo_control());
		} else {
			unsolo_list->push_back (s->solo_control());
		}
	}

	/* set/unset solos of the associated tracks */
	set_controls (solo_list, 1.0, Controllable::NoGroup);
	set_controls (unsolo_list, 0.0, Controllable::NoGroup);

	if (new_state)
		_soloSelection = list;
	else
		_soloSelection.clear();
}


void
Session::butler_transport_work ()
{
	/* Note: this function executes in the butler thread context */

  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();
#ifndef NDEBUG
	uint64_t before;
#endif

	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));

	if (ptw & PostTransportLocate) {

		if (get_play_loop()) {

			DEBUG_TRACE (DEBUG::Butler, "flush loop recording fragment to disk\n");

			/* 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.
			 */

			bool more_disk_io_to_do = false;
			uint32_t errors = 0;

			do {
				more_disk_io_to_do = _butler->flush_tracks_to_disk_after_locate (r, errors);

				if (errors) {
					break;
				}

				if (more_disk_io_to_do) {
					continue;
				}

			} while (false);

		}
	}

	if (ptw & PostTransportAdjustPlaybackBuffering) {
		/* need to prevent concurrency with ARDOUR::Reader::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_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);
		}
	}

	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 ();
			}
		}
	}

	const int butler = g_atomic_int_get (&_butler_seek_counter);
	const int rtlocates = g_atomic_int_get (&_seek_counter);

	if (butler != rtlocates) {
		DEBUG_TRACE (DEBUG::Transport, string_compose ("nonrealtime locate invoked from BTW (butler has done %1, rtlocs %2)\n", butler, rtlocates));
		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 (ptw & PostTransportOverWrite) {
		non_realtime_overwrite (on_entry, finished, (ptw & PostTransportLoopChanged));
		if (!finished) {
			g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
			goto restart;
		}
	}

	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 ptw %3 trw = %4\n"), g_get_monotonic_time() - before, _transport_sample, enum_2_string (post_transport_work()), _butler->transport_work_requested()));
}

void
Session::non_realtime_overwrite (int on_entry, bool& finished, bool update_loop_declicks)
{
	if (update_loop_declicks) {
		DiskReader::reset_loop_declick (_locations->auto_loop_location(), sample_rate());
	}

	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;
		}
	}
}

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 && ((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);
		}

	} else {

		/* no more looping .. should have been noticed elsewhere */
	}

	microseconds_t start;
	uint32_t nt = 0;

	samplepos_t tf;
	gint sc;

	{
		boost::shared_ptr<RouteList> rl = routes.reader();

	  restart:
		sc = g_atomic_int_get (&_seek_counter);
		tf = _transport_sample;
		start = get_microseconds ();

		for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i, ++nt) {
			(*i)->non_realtime_locate (tf);
			if (sc != g_atomic_int_get (&_seek_counter)) {
				goto restart;
			}
		}

		microseconds_t end = get_microseconds ();
		int usecs_per_track = lrintf ((end - start) / (double) nt);
#ifndef NDEBUG
		std::cerr << "locate to " << tf << " took " << (end - start) << " usecs for " << nt << " tracks = " << usecs_per_track << " per track\n";
#endif
		if (usecs_per_track > g_atomic_int_get (&current_usecs_per_track)) {
			g_atomic_int_set (&current_usecs_per_track, usecs_per_track);
		}
	}

	/* we've caught up with whatever the _seek_counter was when we did the
	   non-realtime locates.
	*/
	g_atomic_int_set (&_butler_seek_counter, sc);

	{
		/* 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 ();
}

bool
Session::select_playhead_priority_target (samplepos_t& jump_to)
{
	if (!transport_master_no_external_or_using_engine() || !config.get_auto_return()) {
		return false;
	}

	jump_to = _last_roll_location;
	return jump_to >= 0;
}

void
Session::follow_playhead_priority ()
{
	samplepos_t target;

	if (select_playhead_priority_target (target)) {
		request_locate (target);
	}
}

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();

	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 ();
	}

	if (did_record) {
		begin_reversible_command (Operations::capture);
		_have_captured = true;
	}

	DEBUG_TRACE (DEBUG::Transport, X_("Butler post-transport-work, non realtime 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);
	}

	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 (abort && did_record) {
		_state_of_the_state = StateOfTheState (_state_of_the_state & ~InCleanup);
	}

	boost::shared_ptr<RouteList> r = routes.reader ();

	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 (_engine.running()) {
		PostTransportWork ptw = post_transport_work ();

		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 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.
	 */

	if (transport_master_no_external_or_using_engine() && !locate_initiated()) {

		bool do_locate = false;

		if (_requested_return_sample >= 0) {

			/* explicit return request pre-queued in event list. overrides everything else */

			_transport_sample = _requested_return_sample;

			/* cancel this request */
			_requested_return_sample = -1;
			do_locate = true;

		} else if (Config->get_auto_return_target_list()) {

			samplepos_t jump_to;

			if (select_playhead_priority_target (jump_to)) {

				/* there's a valid target (we don't care how it
				 * was derived here)
				 */

				_transport_sample = jump_to;
				do_locate = true;

			} else if (abort) {

				/* roll aborted (typically capture) with
				 * auto-return enabled
				 */

				_transport_sample = _last_roll_location;
				do_locate = true;

			}
		}


		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.
			 */

			_engine.transport_locate (_transport_sample);

		}
	}

	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() && get_play_loop() && !loop_changing) {
			unset_play_loop ();
		}
	}

	/* reset loop_changing so it does not affect next transport action */
	loop_changing = false;

	if (!_transport_fsm->declicking_for_locate()) {

		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);

			if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) {
				finished = false;
				/* we will be back */
				return;
			}
		}

		VCAList v = _vca_manager->vcas ();
		for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
			(*i)->non_realtime_locate (_transport_sample);
		}
	}

	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.running() && !_engine.freewheeling()) {
		// need to queue this in the next RT cycle
		_send_timecode_update = true;

		if (transport_master()->type() != MTC) { // why?
			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().

			   Something must be done. XXX
			*/
			send_mmc_locate (_transport_sample);
		}
	}

	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;
		}
	}

	/* save the current state of things if appropriate */

	if (did_record && !saved) {
		SaveSessionRequested (_current_snapshot_name);
	}

	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);
}

void
Session::set_play_loop (bool yn, bool change_transport_state)
{
	ENSURE_PROCESS_THREAD;
	/* Called from event-handling context */

	DEBUG_TRACE (DEBUG::Transport, string_compose ("set_play_loop (%1)\n", yn));

	Location *loc;

	if (yn == get_play_loop () || (actively_recording() && yn) || (loc = _locations->auto_loop_location()) == 0) {
		/* nothing to do, or can't change loop status while recording */
		return;
	}

	if (yn && synced_to_engine()) {
		warning << string_compose (
			_("Looping cannot be supported while %1 is using JACK transport.\n"
			  "Recommend changing the configured options"), PROGRAM_NAME)
			<< endmsg;
		return;
	}

	if (yn && !maybe_allow_only_loop (true)) {
		return;
	}

	if (yn) {

		play_loop = true;
		have_looped = false;

		unset_play_range ();
		/* set all tracks to use internal looping */
		set_track_loop (true);

		merge_event (new SessionEvent (SessionEvent::AutoLoop, SessionEvent::Replace, loc->end(), loc->start(), 0.0f));

		if (!Config->get_loop_is_mode()) {
			if (transport_rolling()) {
				/* set loop_changing to ensure that non_realtime_stop does not unset_play_loop */
				loop_changing = true;
			}
			/* args: position, disposition, flush=true, for_loop_end=false, force=true */
			TFSM_LOCATE (loc->start(), MustRoll, true, false, true);
		} else {
			if (!transport_rolling()) {
				/* loop-is-mode: not rolling, just locate to loop start */
				TFSM_LOCATE (loc->start(), MustStop, true, false, true);
			}
		}
		TransportStateChange (); /* EMIT SIGNAL */
	} else {
		unset_play_loop ();
	}

	DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC2 with speed = %1\n", _transport_speed));
}

void
Session::unset_play_loop (bool change_transport_state)
{
	if (!get_play_loop()) {
		return;
	}

	play_loop = false;
	clear_events (SessionEvent::AutoLoop);
	set_track_loop (false);

	/* likely need to flush track buffers: this will locate us to wherever we are */

	if (change_transport_state && transport_rolling ()) {
		TFSM_STOP (false, false);
	}

	overwrite_some_buffers (boost::shared_ptr<Route>(), LoopDisabled);
	TransportStateChange (); /* EMIT SIGNAL */
}

void
Session::set_track_loop (bool yn)
{
	Location* loc = _locations->auto_loop_location ();

	if (!loc) {
		yn = false;
	}

	boost::shared_ptr<RouteList> rl = routes.reader ();

	for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
		if (*i && !(*i)->is_private_route()) {
			(*i)->set_loop (yn ? loc : 0);
		}
	}

	DiskReader::reset_loop_declick (loc, nominal_sample_rate());
}

samplecnt_t
Session::worst_latency_preroll () const
{
	return _worst_output_latency + _worst_input_latency;
}

samplecnt_t
Session::worst_latency_preroll_buffer_size_ceil () const
{
	return lrintf (ceil ((_worst_output_latency + _worst_input_latency) / (float) current_block_size) * current_block_size);
}

void
Session::unset_play_range ()
{
	_play_range = false;
	_clear_event_type (SessionEvent::RangeStop);
	_clear_event_type (SessionEvent::RangeLocate);
}

void
Session::set_play_range (list<AudioRange>& range, bool leave_rolling)
{
	SessionEvent* ev;

	/* Called from event-processing context */

	unset_play_range ();

	if (range.empty()) {
		/* _play_range set to false in unset_play_range()
		 */
		if (!leave_rolling) {
			/* stop transport */
			SessionEvent* ev = new SessionEvent (SessionEvent::SetTransportSpeed, SessionEvent::Add, SessionEvent::Immediate, 0, 0.0f, false);
			merge_event (ev);
		}
		return;
	}

	_play_range = true;

	/* cancel loop play */
	unset_play_loop ();

	list<AudioRange>::size_type sz = range.size();

	if (sz > 1) {

		list<AudioRange>::iterator i = range.begin();
		list<AudioRange>::iterator next;

		while (i != range.end()) {

			next = i;
			++next;

			/* locating/stopping is subject to delays for declicking.
			 */

			samplepos_t requested_sample = i->end;

			if (requested_sample > current_block_size) {
				requested_sample -= current_block_size;
			} else {
				requested_sample = 0;
			}

			if (next == range.end()) {
				ev = new SessionEvent (SessionEvent::RangeStop, SessionEvent::Add, requested_sample, 0, 0.0f);
			} else {
				ev = new SessionEvent (SessionEvent::RangeLocate, SessionEvent::Add, requested_sample, (*next).start, 0.0f);
			}

			merge_event (ev);

			i = next;
		}

	} else if (sz == 1) {

		ev = new SessionEvent (SessionEvent::RangeStop, SessionEvent::Add, range.front().end, 0, 0.0f);
		merge_event (ev);

	}

	/* save range so we can do auto-return etc. */

	current_audio_range = range;

	/* now start rolling at the right place */

	ev = new SessionEvent (SessionEvent::LocateRoll, SessionEvent::Add, SessionEvent::Immediate, range.front().start, 0.0f, false);
	merge_event (ev);

	DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC5 with speed = %1\n", _transport_speed));
	TransportStateChange (); /* EMIT SIGNAL */
}

void
Session::request_bounded_roll (samplepos_t start, samplepos_t end)
{
	AudioRange ar (start, end, 0);
	list<AudioRange> lar;

	lar.push_back (ar);
	request_play_range (&lar, true);
}

void
Session::set_requested_return_sample (samplepos_t return_to)
{
	_requested_return_sample = return_to;
}

void
Session::request_roll_at_and_return (samplepos_t start, samplepos_t return_to)
{
	SessionEvent *ev = new SessionEvent (SessionEvent::LocateRollLocate, SessionEvent::Add, SessionEvent::Immediate, return_to, 1.0);
	ev->target2_sample = start;
	queue_event (ev);
}

void
Session::engine_halted ()
{
	/* there will be no more calls to process(), so
	   we'd better clean up for ourselves, right now.

	   We can't queue SessionEvents because they only get
	   handled from within a process callback.
	*/

	/* 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)
	 */

	_transport_fsm->stop ();

	/* 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 ()
{
	_transport_fsm->start ();
	reset_xrun_count ();
}

void
Session::xrun_recovery ()
{
	++_xrun_count;

	Xrun (_transport_sample); /* EMIT SIGNAL */

	if (Config->get_stop_recording_on_xrun() && actively_recording()) {

		/* it didn't actually halt, but we need
		 * to handle things in the same way.
		 */

		engine_halted();

		/* ..and start the FSM engine again */
		_transport_fsm->start ();
	}
}

void
Session::route_processors_changed (RouteProcessorChange c)
{
	if (g_atomic_int_get (&_ignore_route_processor_changes) > 0) {
		g_atomic_int_set (&_ignored_a_processor_change, 1);
		return;
	}

	if (c.type == RouteProcessorChange::MeterPointChange) {
		set_dirty ();
		return;
	}

	if (c.type == RouteProcessorChange::RealTimeChange) {
		set_dirty ();
		return;
	}

	resort_routes ();
	update_latency_compensation (false, false);

	set_dirty ();
}

void
Session::allow_auto_play (bool yn)
{
	auto_play_legal = yn;
}


void
Session::send_mmc_locate (samplepos_t t)
{
	if (t < 0) {
		return;
	}

	if (!_engine.freewheeling()) {
		Timecode::Time time;
		timecode_time_subframes (t, time);
		send_immediate_mmc (MIDI::MachineControlCommand (time));
	}
}

/** Ask the transport to not send timecode until further notice.  The suspension
 *  will come into effect some finite time after this call, and timecode_transmission_suspended()
 *  should be checked by the caller to find out when.
 */
void
Session::request_suspend_timecode_transmission ()
{
	SessionEvent* ev = new SessionEvent (SessionEvent::SetTimecodeTransmission, SessionEvent::Add, SessionEvent::Immediate, 0, 0, false);
	queue_event (ev);
}

void
Session::request_resume_timecode_transmission ()
{
	SessionEvent* ev = new SessionEvent (SessionEvent::SetTimecodeTransmission, SessionEvent::Add, SessionEvent::Immediate, 0, 0, true);
	queue_event (ev);
}

bool
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();

	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.
	*/

	TransportMasterManager::instance().unblock_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_stopped_or_stopping() const
{
	return _transport_fsm->stopped() || _transport_fsm->stopping();
}

bool
Session::transport_state_rolling() const
{
	return _transport_fsm->rolling();
}

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::locate_initiated() const
{
	return _transport_fsm->declicking_for_locate() || _transport_fsm->locating();
}

bool
Session::declick_in_progress () const
{
	return _transport_fsm->declick_in_progress();
}

bool
Session::transport_will_roll_forwards () const
{
	return _transport_fsm->will_roll_fowards ();
}
