/*
- 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/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/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_frame(), 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_frame(), _transport_frame));
- queue_event (ev);
-}
-
-void
-Session::request_locate (framepos_t target_frame, bool with_roll)
-{
- SessionEvent *ev = new SessionEvent (with_roll ? SessionEvent::LocateRoll : SessionEvent::Locate, SessionEvent::Add, SessionEvent::Immediate, target_frame, 0, false);
- DEBUG_TRACE (DEBUG::Transport, string_compose ("Request locate to %1\n", target_frame));
- queue_event (ev);
-}
-
-void
-Session::force_locate (framepos_t target_frame, bool with_roll)
-{
- SessionEvent *ev = new SessionEvent (with_roll ? SessionEvent::LocateRoll : SessionEvent::Locate, SessionEvent::Add, SessionEvent::Immediate, target_frame, 0, true);
- DEBUG_TRACE (DEBUG::Transport, string_compose ("Request forced locate to %1\n", target_frame));
- queue_event (ev);
-}
-
-void
-Session::unset_preroll_record_punch ()
-{
- if (_preroll_record_punch_pos >= 0) {
- remove_event (_preroll_record_punch_pos, SessionEvent::RecordStart);
- }
- _preroll_record_punch_pos = -1;
-}
-
-void
-Session::unset_preroll_record_trim ()
-{
- _preroll_record_trim_len = 0;
-}
-
-void
-Session::request_preroll_record_punch (framepos_t rec_in, framecnt_t preroll)
-{
- if (actively_recording ()) {
- return;
- }
- unset_preroll_record_punch ();
- unset_preroll_record_trim ();
- framepos_t start = std::max ((framepos_t)0, rec_in - preroll);
-
- _preroll_record_punch_pos = rec_in;
- if (_preroll_record_punch_pos >= 0) {
- replace_event (SessionEvent::RecordStart, _preroll_record_punch_pos);
- config.set_punch_in (false);
- config.set_punch_out (false);
- }
- maybe_enable_record ();
- request_locate (start, true);
- set_requested_return_frame (rec_in);
-}
-
-void
-Session::request_preroll_record_trim (framepos_t rec_in, framecnt_t preroll)
-{
- if (actively_recording ()) {
- return;
- }
- unset_preroll_record_punch ();
- unset_preroll_record_trim ();
-
- config.set_punch_in (false);
- config.set_punch_out (false);
-
- framepos_t pos = std::max ((framepos_t)0, rec_in - preroll);
- _preroll_record_trim_len = preroll;
- maybe_enable_record ();
- request_locate (pos, true);
- set_requested_return_frame (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_frame-1, false);
- }
- }
-}
+#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
-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);
-}
+#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)); }
+/* *****************************************************************************
+ * 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_frame));
- 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);
}
(*i)->realtime_handle_transport_stopped ();
}
- DEBUG_TRACE (DEBUG::Transport, string_compose ("stop complete, auto-return scheduled for return to %1\n", _requested_return_frame));
-
- /* the duration change is not guaranteed to have happened, but is likely */
-
- todo = PostTransportWork (todo | PostTransportDuration);
+ 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);
add_post_transport_work (todo);
}
- _clear_event_type (SessionEvent::StopOnce);
_clear_event_type (SessionEvent::RangeStop);
_clear_event_type (SessionEvent::RangeLocate);
+ //clear our solo-selection, if there is one
+ 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));
_transport_speed = 0;
_target_transport_speed = 0;
+ _engine_speed = 1.0;
g_atomic_int_set (&_playback_load, 100);
g_atomic_int_set (&_capture_load, 100);
waiting_for_sync_offset = true;
}
- transport_sub_state = 0;
+ if (todo) {
+ TFSM_EVENT (TransportFSM::ButlerRequired);
+ }
}
void
-Session::realtime_locate ()
+Session::locate (samplepos_t target_sample, bool with_roll, bool with_flush, bool for_loop_enabled, bool force, bool with_mmc)
{
- boost::shared_ptr<RouteList> r = routes.reader ();
- for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
- (*i)->realtime_locate ();
+ ENSURE_PROCESS_THREAD;
+
+ if (target_sample < 0) {
+ error << _("Locate called for negative sample position - ignored") << endmsg;
+ return;
}
-}
-void
-Session::butler_transport_work ()
-{
- /* Note: this function executes in the butler thread context */
+ if (synced_to_engine()) {
- restart:
- bool finished;
- PostTransportWork ptw;
- boost::shared_ptr<RouteList> r = routes.reader ();
- uint64_t before;
+ double sp;
+ samplepos_t pos;
+ samplepos_t ignore1, ignore2;
- int on_entry = g_atomic_int_get (&_butler->should_do_transport_work);
- finished = true;
- ptw = post_transport_work();
+ transport_master()->speed_and_position (sp, pos, ignore1, ignore2, 0);
- DEBUG_TRACE (DEBUG::Transport, string_compose ("Butler transport work, todo = %1 at %2\n", enum_2_string (ptw), (before = g_get_monotonic_time())));
+ if (target_sample != pos) {
+ 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);
+ }
- if (ptw & PostTransportLocate) {
+ /* tell JACK to change transport position, and we will
+ follow along later in ::follow_slave()
+ */
- if (get_play_loop() && !Config->get_seamless_loop()) {
+ _engine.transport_locate (target_sample);
- DEBUG_TRACE (DEBUG::Butler, "flush loop recording fragment to disk\n");
+ if (sp != 1.0f && with_roll) {
+ _engine.transport_start ();
+ }
- /* 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;
+ } else {
+ do_locate (target_sample, with_roll, with_flush, for_loop_enabled, force, with_mmc);
+ }
+}
- do {
- more_disk_io_to_do = _butler->flush_tracks_to_disk_after_locate (r, errors);
+/** @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;
- if (errors) {
- break;
- }
+ bool need_butler = false;
- if (more_disk_io_to_do) {
- continue;
- }
+ /* 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.
+ */
- } while (false);
+ 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 (!force && _transport_sample == target_sample && !loop_changing && !for_loop_enabled) {
- 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_frame);
- }
- VCAList v = _vca_manager->vcas ();
- for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
- (*i)->non_realtime_locate (_transport_frame);
- }
- }
+ /* 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 (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 (with_roll) {
+ set_transport_speed (1.0, 0, false);
}
+ loop_changing = false;
+ TFSM_EVENT (TransportFSM::LocateDone);
+ Located (); /* EMIT SIGNAL */
+ return;
}
- if (ptw & PostTransportCurveRealloc) {
- for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
- (*i)->curve_reallocate();
- }
- }
+ // 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 & PostTransportSpeed) {
- non_realtime_set_speed ();
- }
+ /* 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
+ *
+ */
- if (ptw & PostTransportReverse) {
- clear_clicks();
- cumulative_rf_motion = 0;
- reset_rf_scale (0);
+ /* 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();
- /* don't seek if locate will take care of that in non_realtime_stop() */
+ 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 (!(ptw & PostTransportLocate)) {
- for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
- (*i)->non_realtime_locate (_transport_frame);
+ realtime_stop (false, true); // XXX paul - check if the 2nd arg is really correct
+ transport_was_stopped = true;
- 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_frame);
- }
- }
- }
+ } else {
- if (ptw & PostTransportLocate) {
- DEBUG_TRACE (DEBUG::Transport, "nonrealtime locate invoked from BTW\n");
- non_realtime_locate ();
- }
+ /* Tell all routes to do the RT part of 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;
+ boost::shared_ptr<RouteList> r = routes.reader ();
+ for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
+ (*i)->realtime_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;
+ if (force || !for_loop_enabled || loop_changing) {
+
+ PostTransportWork todo = PostTransportLocate;
+
+ if (with_roll && transport_was_stopped) {
+ todo = PostTransportWork (todo | PostTransportRoll);
}
- }
- if (ptw & PostTransportAudition) {
- non_realtime_set_audition ();
- }
+ add_post_transport_work (todo);
+ need_butler = true;
- g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
+ } else {
- 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_frame, _butler->transport_work_requested()));
-}
+ /* this is functionally what clear_clicks() does but with a tentative lock */
-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 ();
+ 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 ();
}
}
-}
-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 (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());
}
- if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) {
- finished = false;
- return;
+ } else {
+ /* otherwise we're going to stop, so do the opposite */
+ if (Config->get_monitoring_model() == HardwareMonitoring) {
+ set_track_monitor_input_status (true);
}
}
-}
-
-
-void
-Session::non_realtime_locate ()
-{
- DEBUG_TRACE (DEBUG::Transport, string_compose ("locate tracks to %1\n", _transport_frame));
- if (Config->get_loop_is_mode() && get_play_loop()) {
+ /* cancel looped playback if transport pos outside of loop range */
+ if (play_loop) {
- Location *loc = _locations->auto_loop_location();
+ Location* al = _locations->auto_loop_location();
- if (!loc || (_transport_frame < loc->start() || _transport_frame >= loc->end())) {
- /* jumped out of loop range: stop tracks from looping,
- but leave loop (mode) enabled.
- */
- set_track_loop (false);
+ if (al) {
+ if (_transport_sample < al->start() || _transport_sample >= al->end()) {
- } else if (loc && Config->get_seamless_loop() &&
- ((loc->start() <= _transport_frame) ||
- (loc->end() > _transport_frame) ) ) {
+ // located outside the loop: cancel looping directly, this is called from event handling context
- /* 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);
+ have_looped = false;
- } else if (loc) {
- set_track_loop (false);
- }
+ 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);
+ }
+ }
- } else {
+ } else if (_transport_sample == al->start()) {
- /* no more looping .. should have been noticed elsewhere */
- }
+ // located to start of loop - this is looping, basically
+ 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;
+ }
- microseconds_t begin = get_microseconds ();
+ }
- {
- boost::shared_ptr<RouteList> rl = routes.reader();
+ boost::shared_ptr<RouteList> rl = routes.reader();
- restart:
- const framepos_t tf = _transport_frame;
+ for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
+ boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
- cerr << "\n\n >>> START Non-RT locate on routes to " << tf << "\n\n";
+ 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);
+ }
+ }
- for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
- (*i)->non_realtime_locate (_transport_frame);
- if (tf != _transport_frame) {
- /* new locate request arrived while processing
- this one. start over.
- */
- cerr << "\n\n\n\n RESTART LOCATE @ " << _transport_frame << endl;
- goto restart;
+ have_looped = true;
+ TransportLooped(); // EMIT SIGNAL
}
}
-
- cerr << "\n\n <<< DONE Non-RT locate on routes\n\n";
}
- {
- VCAList v = _vca_manager->vcas ();
- for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
- (*i)->non_realtime_locate (_transport_frame);
- }
+ if (need_butler) {
+ TFSM_EVENT (TransportFSM::ButlerRequired);
+ } else {
+ TFSM_EVENT (TransportFSM::LocateDone);
}
- microseconds_t end = get_microseconds ();
- cerr << "Locate took " << setprecision (3) << ((end - begin) /1000000.0) << " secs\n";
-
- _scene_changer->locate (_transport_frame);
-
- /* 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 (framepos_t& jump_to)
-{
- jump_to = -1;
+ loop_changing = false;
- AutoReturnTarget autoreturn = Config->get_auto_return_target_list ();
+ _send_timecode_update = true;
- if (!autoreturn) {
- return false;
+ if (with_mmc) {
+ send_mmc_locate (_transport_sample);
}
- if (Profile->get_trx() && transport_rolling() ) {
- // We're playing, so do nothing.
- // Next stop will put us where we need to be.
- return false;
+ _last_roll_location = _last_roll_or_reversal_location = _transport_sample;
+ if (!synced_to_engine () || _transport_sample == _engine.transport_sample ()) {
+ Located (); /* EMIT SIGNAL */
}
+}
- /* Note that the order of checking each AutoReturnTarget flag defines
- the priority each flag.
+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;
+ }
+ }
+}
- Ardour/Mixbus: Last Locate
- Range Selection
- Loop Range
- Region Selection
+/** 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)
+{
+ 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));
- Tracks: Range Selection
- Loop Range
- Region Selection
- Last Locate
+ /* 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 (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 (speed > 0) {
+ speed = min ((double) Config->get_max_transport_speed(), speed);
+ } else if (speed < 0) {
+ speed = max ((double) -Config->get_max_transport_speed(), speed);
}
- 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();
-
- if (location) {
- jump_to = location->start();
-
- if (Config->get_seamless_loop()) {
- /* need to get track buffers reloaded */
- set_track_loop (true);
- }
- }
- }
+ 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 (jump_to < 0 && (autoreturn & RegionSelectionStart)) {
- if (!_object_selection.empty()) {
- jump_to = _object_selection.from;
+ 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 (jump_to < 0 && (autoreturn & LastLocate)) {
- jump_to = _last_roll_location;
+#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
- return jump_to >= 0;
-}
-#else
-
-bool
-Session::select_playhead_priority_target (framepos_t& jump_to)
-{
- if (config.get_external_sync() || !config.get_auto_return()) {
- return false;
- }
+ _target_transport_speed = fabs(speed);
+ _engine_speed = new_engine_speed;
- jump_to = _last_roll_location;
- return jump_to >= 0;
-}
+ if (transport_rolling() && speed == 0.0) {
-#endif
+ /* we are rolling and we want to stop */
-void
-Session::follow_playhead_priority ()
-{
- framepos_t target;
+ if (Config->get_monitoring_model() == HardwareMonitoring) {
+ set_track_monitor_input_status (true);
+ }
- if (select_playhead_priority_target (target)) {
- request_locate (target);
- }
-}
+ 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));
-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 (!auto_return_enabled) {
+ _requested_return_sample = destination_sample;
+ }
- did_record = false;
- saved = false;
+ TFSM_STOP (abort, 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_frames () != 0) {
- did_record = true;
- break;
+ } 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 */
- /* stop and locate are merged here because they share a lot of common stuff */
+ if (Config->get_loop_is_mode() && play_loop) {
- time (&xnow);
- now = localtime (&xnow);
+ Location *location = _locations->auto_loop_location();
- if (auditioner) {
- auditioner->cancel_audition ();
- }
+ if (location != 0) {
+ if (_transport_sample != location->start()) {
- cumulative_rf_motion = 0;
- reset_rf_scale (0);
+ if (Config->get_seamless_loop()) {
+ /* force tracks to do their thing */
+ set_track_loop (true);
+ }
- if (did_record) {
- begin_reversible_command (Operations::capture);
- _have_captured = true;
- }
+ /* jump to start and then roll from there */
- DEBUG_TRACE (DEBUG::Transport, X_("Butler PTW: DS stop\n"));
+ request_locate (location->start(), true);
+ return;
+ }
+ }
+ }
- 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);
- }
+ if (Config->get_monitoring_model() == HardwareMonitoring && config.get_auto_input()) {
+ set_track_monitor_input_status (false);
+ }
- 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 (synced_to_engine()) {
+ _engine.transport_start ();
+ _count_in_once = false;
+ } else {
+ TFSM_EVENT (TransportFSM::StartTransport);
}
- }
- if (abort && did_record) {
- _state_of_the_state = StateOfTheState (_state_of_the_state & ~InCleanup);
- }
+ } else {
- boost::shared_ptr<RouteList> r = routes.reader ();
+ /* not zero, not 1.0 ... varispeed */
- for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
- if (!(*i)->is_auditioner()) {
- (*i)->set_pending_declick (0);
- }
- }
+ // TODO handled transport start.. _remaining_latency_preroll
+ // and reversal of playback direction.
- 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 ((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 (_engine.running()) {
- PostTransportWork ptw = post_transport_work ();
+#if 0
+ if (actively_recording()) {
+ return;
+ }
+#endif
- for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
- (*i)->non_realtime_transport_stop (_transport_frame, !(ptw & PostTransportLocate) || pending_locate_flush);
+ if (speed > 0.0 && _transport_sample == current_end_sample()) {
+ return;
}
- VCAList v = _vca_manager->vcas ();
- for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
- (*i)->non_realtime_transport_stop (_transport_frame, !(ptw & PostTransportLocate) || pending_locate_flush);
+
+ if (speed < 0.0 && _transport_sample == 0) {
+ return;
}
- update_latency_compensation ();
- }
+ 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.
+ */
+
+ PostTransportWork todo = PostTransportWork (0);
- bool const auto_return_enabled = (!config.get_external_sync() && (Config->get_auto_return_target_list() || abort));
+ 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 (auto_return_enabled ||
- (ptw & PostTransportLocate) ||
- (_requested_return_frame >= 0) ||
- synced_to_engine()) {
+ _last_transport_speed = _transport_speed;
+ _transport_speed = speed;
- if (pending_locate_flush) {
- flush_all_inserts ();
+ if (as_default) {
+ _default_transport_speed = speed;
}
- // rg: what is the logic behind this case?
- // _requested_return_frame should be ignored when synced_to_engine/slaved.
- // currently worked around in MTC_Slave by forcing _requested_return_frame to -1
- // 2016-01-10
- if ((auto_return_enabled || synced_to_engine() || _requested_return_frame >= 0) &&
- !(ptw & PostTransportLocate)) {
+ if (todo) {
+ add_post_transport_work (todo);
+ TFSM_EVENT (TransportFSM::ButlerRequired);
+ }
- /* no explicit locate queued */
+ DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC3 with speed = %1\n", _transport_speed));
- bool do_locate = false;
+ /* 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 ();
+ }
+ }
+}
- if (_requested_return_frame >= 0) {
+/** Stop the transport. */
+void
+Session::stop_transport (bool abort, bool clear_state)
+{
+ ENSURE_PROCESS_THREAD;
- /* explicit return request pre-queued in event list. overrides everything else */
+ _count_in_once = false;
- _transport_frame = _requested_return_frame;
- do_locate = true;
+ DEBUG_TRACE (DEBUG::Transport, string_compose ("time to actually stop with TS @ %1\n", _transport_sample));
- } else {
- framepos_t jump_to;
+ realtime_stop (abort, clear_state);
+}
- if (select_playhead_priority_target (jump_to)) {
+/** Called from the process thread */
+void
+Session::start_transport ()
+{
+ ENSURE_PROCESS_THREAD;
+ DEBUG_TRACE (DEBUG::Transport, "start_transport\n");
- _transport_frame = jump_to;
- do_locate = true;
+ _last_roll_location = _transport_sample;
+ _last_roll_or_reversal_location = _transport_sample;
+ _remaining_latency_preroll = worst_latency_preroll ();
- } else if (abort) {
+ have_looped = false;
- _transport_frame = _last_roll_location;
- do_locate = true;
- }
- }
+ /* if record status is Enabled, move it to Recording. if its
+ already Recording, move it to Disabled.
+ */
- _requested_return_frame = -1;
+ 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;
- if (do_locate) {
- _engine.transport_locate (_transport_frame);
- }
+ case Recording:
+ if (!play_loop) {
+ disable_record (false);
}
+ break;
+ default:
+ break;
}
- clear_clicks();
- unset_preroll_record_trim ();
-
- /* do this before seeking, because otherwise the tracks will do the wrong thing in seamless loop mode.
- */
+ _transport_speed = _default_transport_speed;
+ _target_transport_speed = _transport_speed;
- if (ptw & PostTransportClearSubstate) {
- unset_play_range ();
- if (!Config->get_loop_is_mode()) {
- unset_play_loop ();
+ 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));
}
- }
- /* this for() block can be put inside the previous if() and has the effect of ... ??? what */
+ 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);
- {
- 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_frame);
+ 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.
- if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) {
- finished = false;
- /* we will be back */
- return;
+ _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;
}
- }
- }
- {
- VCAList v = _vca_manager->vcas ();
- for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
- (*i)->non_realtime_locate (_transport_frame);
+ 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);
+ 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;
+ }
}
}
- have_looped = false;
+ DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC4 with speed = %1\n", _transport_speed));
+ TransportStateChange (); /* EMIT SIGNAL */
+}
- /* don't bother with this stuff if we're disconnected from the engine,
- because there will be no process callbacks to deliver stuff from
- */
+bool
+Session::should_roll_after_locate () const
+{
+ /* a locate must previously have been requested and completed */
- if (_engine.connected() && !_engine.freewheeling()) {
- // need to queue this in the next RT cycle
- _send_timecode_update = true;
+ return ((!config.get_external_sync() && (auto_play_legal && config.get_auto_play())) && !_exporting) || (post_transport_work() & PostTransportRoll);
- 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().
+/** 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 ();
- Something must be done. XXX
- */
- send_mmc_locate (_transport_frame);
- }
- }
+ DEBUG_TRACE (DEBUG::Transport, string_compose ("Butler done, RT cleanup for %1\n", enum_2_string (ptw)));
- 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 (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);
}
- /* always try to get rid of this */
-
- remove_pending_capture_state ();
-
- /* save the current state of things if appropriate */
-
- if (did_record && !saved) {
- SaveSessionRequested (_current_snapshot_name);
+ if (ptw & PostTransportLocate) {
+ post_locate ();
+ ptw = PostTransportWork (ptw & ~PostTransportLocate);
+ set_post_transport_work (ptw);
+ TFSM_EVENT (TransportFSM::LocateDone);
}
- if (ptw & PostTransportStop) {
- unset_play_range ();
- if (!Config->get_loop_is_mode()) {
- unset_play_loop ();
- }
+ if (ptw & PostTransportAdjustPlaybackBuffering) {
+ /* we blocked output while this happened */
+ DiskReader::dec_no_disk_output ();
+ ptw = PostTransportWork (ptw & ~PostTransportAdjustPlaybackBuffering);
+ set_post_transport_work (ptw);
}
- PositionChanged (_transport_frame); /* 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_frame);
+ ptw = PostTransportWork (ptw & ~(PostTransportAdjustCaptureBuffering|PostTransportOverWrite));
+ set_post_transport_work (ptw);
- /* and start it up again if relevant */
+ set_next_event ();
- if ((ptw & PostTransportLocate) && !config.get_external_sync() && pending_locate_roll) {
- request_transport_speed (1.0);
+ if (_transport_fsm->waiting_for_butler()) {
+ TFSM_EVENT (TransportFSM::ButlerDone);
}
- /* Even if we didn't do a pending locate roll this time, we don't want it hanging
- around for next time.
- */
- pending_locate_roll = false;
+ DiskReader::dec_no_disk_output ();
}
void
-Session::check_declick_out ()
+Session::schedule_butler_for_transport_work ()
{
- bool locate_required = transport_sub_state & PendingLocate;
-
- /* this is called after a process() iteration. if PendingDeclickOut was set,
- it means that we were waiting to declick the output (which has just been
- done) before maybe doing something else. this is where we do that "something else".
-
- note: called from the audio thread.
- */
-
- if (transport_sub_state & PendingDeclickOut) {
-
- if (locate_required) {
- start_locate (pending_locate_frame, pending_locate_roll, pending_locate_flush);
- transport_sub_state &= ~(PendingDeclickOut|PendingLocate);
- } else {
- if (!(transport_sub_state & StopPendingCapture)) {
- stop_transport (pending_abort);
- transport_sub_state &= ~(PendingDeclickOut|PendingLocate);
- }
- }
-
- } else if (transport_sub_state & PendingLoopDeclickOut) {
- /* Nothing else to do here; we've declicked, and the loop event will be along shortly */
- transport_sub_state &= ~PendingLoopDeclickOut;
- }
+ assert (_transport_fsm->waiting_for_butler ());
+ DEBUG_TRACE (DEBUG::Butler, "summon butler for transport work\n");
+ _butler->schedule_transport_work ();
}
-void
-Session::unset_play_loop ()
+bool
+Session::maybe_stop (samplepos_t limit)
{
- if (play_loop) {
- play_loop = false;
- clear_events (SessionEvent::AutoLoop);
- clear_events (SessionEvent::AutoLoopDeclick);
- set_track_loop (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 ();
+ 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;
}
-void
-Session::set_track_loop (bool yn)
+int
+Session::micro_locate (samplecnt_t distance)
{
- Location* loc = _locations->auto_loop_location ();
+ ENSURE_PROCESS_THREAD;
- if (!loc) {
- yn = 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->can_internal_playback_seek (distance)) {
+ return -1;
+ }
}
- boost::shared_ptr<RouteList> rl = routes.reader ();
+ 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->is_private_route()) {
- tr->set_loop (yn ? loc : 0);
+ 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 ();
+ }
}
void
Session::set_play_loop (bool yn, double speed)
{
+ ENSURE_PROCESS_THREAD;
/* Called from event-handling context */
Location *loc;
in a fade-in when the loop restarts. The AutoLoop event will peform the actual loop.
*/
- framepos_t dcp;
- framecnt_t dcl;
+ samplepos_t dcp;
+ samplecnt_t dcl;
auto_loop_declick_range (loc, dcp, dcl);
- merge_event (new SessionEvent (SessionEvent::AutoLoopDeclick, SessionEvent::Replace, dcp, dcl, 0.0f));
merge_event (new SessionEvent (SessionEvent::AutoLoop, SessionEvent::Replace, loc->end(), loc->start(), 0.0f));
/* if requested to roll, locate to start of loop and
rolling, do not locate to loop start.
*/
if (!transport_rolling() && (speed != 0.0)) {
- start_locate (loc->start(), true, true, false, true);
+ TFSM_LOCATE (loc->start(), true, true, false, true);
}
} else {
if (speed != 0.0) {
- start_locate (loc->start(), true, true, false, true);
+ TFSM_LOCATE (loc->start(), true, true, false, true);
}
}
}
DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC2 with speed = %1\n", _transport_speed));
TransportStateChange ();
}
+
+/* *****************************************************************************
+ * END REALTIME ACTIONS
+ * ****************************************************************************/
+
+
void
-Session::flush_all_inserts ()
+Session::add_post_transport_work (PostTransportWork ptw)
{
- boost::shared_ptr<RouteList> r = routes.reader ();
+ PostTransportWork oldval;
+ PostTransportWork newval;
+ int tries = 0;
- for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
- (*i)->flush_processors ();
+ 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::start_locate (framepos_t target_frame, bool with_roll, bool with_flush, bool for_loop_enabled, bool force)
+bool
+Session::should_ignore_transport_request (TransportRequestSource src, TransportRequestType type) const
{
- if (target_frame < 0) {
- error << _("Locate called for negative sample position - ignored") << endmsg;
- return;
+ if (config.get_external_sync()) {
+ if (TransportMasterManager::instance().current()->allow_request (src, type)) {
+ return false;
+ } else {
+ return true;
+ }
}
+ return false;
+}
- if (synced_to_engine()) {
-
- double sp;
- framepos_t pos;
-
- _slave->speed_and_position (sp, pos);
-
- if (target_frame != pos) {
-
- if (config.get_jack_time_master()) {
- /* actually locate now, since otherwise jack_timebase_callback
- will use the incorrect _transport_frame and report an old
- and incorrect time to Jack transport
- */
- locate (target_frame, with_roll, with_flush, for_loop_enabled, force);
- }
-
- /* tell JACK to change transport position, and we will
- follow along later in ::follow_slave()
- */
-
- _engine.transport_locate (target_frame);
-
- if (sp != 1.0f && with_roll) {
- _engine.transport_start ();
- }
+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);
+}
- } else {
- locate (target_frame, with_roll, with_flush, for_loop_enabled, force);
+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);
}
-int
-Session::micro_locate (framecnt_t distance)
+/** 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)
{
- 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;
- }
+ if (should_ignore_transport_request (origin, TransportRequestType (TR_Speed|TR_Start))) {
+ return;
}
- 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);
- }
+ if (speed == 0) {
+ speed = DBL_EPSILON;
}
- _transport_frame += distance;
- return 0;
+ request_transport_speed (speed, as_default);
}
-/** @param with_mmc true to send a MMC locate command when the locate is done */
void
-Session::locate (framepos_t target_frame, bool with_roll, bool with_flush, bool for_loop_enabled, bool force, bool with_mmc)
+Session::request_stop (bool abort, bool clear_state, TransportRequestSource origin)
{
- 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_frame.
- */
-
- DEBUG_TRACE (DEBUG::Transport, string_compose ("rt-locate to %1, roll %2 flush %3 loop-enabled %4 force %5 mmc %6\n",
- target_frame, with_roll, with_flush, for_loop_enabled, force, with_mmc));
-
- if (!force && _transport_frame == target_frame && !loop_changing && !for_loop_enabled) {
+ if (should_ignore_transport_request (origin, TR_Stop)) {
+ return;
+ }
- /* 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).
- */
+ 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 (with_roll) {
- set_transport_speed (1.0, 0, false);
- }
- loop_changing = false;
- Located (); /* EMIT SIGNAL */
+void
+Session::request_locate (samplepos_t target_sample, bool with_roll, TransportRequestSource origin)
+{
+ if (should_ignore_transport_request (origin, TR_Locate)) {
return;
}
- if (_transport_speed && !(for_loop_enabled && Config->get_seamless_loop())) {
- /* Schedule a declick. We'll be called again when its done.
- We only do it this way for ordinary locates, not those
- due to **seamless** loops.
- */
+ 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);
+}
- if (!(transport_sub_state & PendingDeclickOut)) {
- transport_sub_state |= (PendingDeclickOut|PendingLocate);
- pending_locate_frame = target_frame;
- pending_locate_roll = with_roll;
- pending_locate_flush = with_flush;
- return;
- }
+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 ();
- // Update Timecode time
- _transport_frame = target_frame;
- _last_roll_or_reversal_location = target_frame;
- timecode_time(_transport_frame, transmitting_timecode_time);
+ config.set_punch_in (false);
+ config.set_punch_out (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)
- *
- */
+ 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);
+}
- bool transport_was_stopped = !transport_rolling();
+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);
+}
- 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 ();
+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;
}
- if (force || !for_loop_enabled || loop_changing) {
+ SessionEvent* ev;
+ Location *location = _locations->auto_loop_location();
+ double target_speed;
- PostTransportWork todo = PostTransportLocate;
+ if (location == 0 && yn) {
+ error << _("Cannot loop - no loop range defined")
+ << endmsg;
+ return;
+ }
- if (with_roll && transport_was_stopped) {
- todo = PostTransportWork (todo | PostTransportRoll);
+ 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;
+ }
}
-
- add_post_transport_work (todo);
- need_butler = true;
-
} else {
+ /* leave the speed alone */
+ target_speed = transport_speed ();
+ }
- /* 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()) {
+ 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);
- for (Clicks::iterator i = clicks.begin(); i != clicks.end(); ++i) {
- delete *i;
+ 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);
}
-
- clicks.clear ();
+ }
+ } 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);
}
}
+}
- 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());
- }
+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 {
- /* otherwise we're going to stop, so do the opposite */
- if (Config->get_monitoring_model() == HardwareMonitoring) {
- set_track_monitor_input_status (true);
- }
+ ev->audio_range.clear ();
}
+ DEBUG_TRACE (DEBUG::Transport, string_compose ("Request play range, leave rolling ? %1\n", leave_rolling));
+ queue_event (ev);
+}
- /* cancel looped playback if transport pos outside of loop range */
- if (play_loop) {
+void
+Session::request_cancel_play_range ()
+{
+ SessionEvent* ev = new SessionEvent (SessionEvent::CancelPlayAudioRange, SessionEvent::Add, SessionEvent::Immediate, 0, 0);
+ queue_event (ev);
+}
- Location* al = _locations->auto_loop_location();
- if (al) {
- if (_transport_frame < al->start() || _transport_frame >= al->end()) {
+bool
+Session::solo_selection_active ()
+{
+ if (_soloSelection.empty()) {
+ return false;
+ }
+ return true;
+}
- // located outside the loop: cancel looping directly, this is called from event handling context
+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);
- have_looped = false;
+ if (new_state)
+ _soloSelection = list;
+ else
+ _soloSelection.clear();
- 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);
- }
- }
+ boost::shared_ptr<RouteList> rl = get_routes();
- } else if (_transport_frame == al->start()) {
+ for (ARDOUR::RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
- // located to start of loop - this is looping, basically
+ if ( !(*i)->is_track() ) {
+ continue;
+ }
- 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;
- }
+ boost::shared_ptr<Stripable> s (*i);
- }
+ bool found = (std::find(list.begin(), list.end(), s) != list.end());
+ if ( new_state && found ) {
- boost::shared_ptr<RouteList> rl = routes.reader();
+ solo_list->push_back (s->solo_control());
- 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_frame);
- }
+ //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();
}
-
- have_looped = true;
- TransportLooped(); // EMIT SIGNAL
}
+ } else {
+ unsolo_list->push_back (s->solo_control());
}
}
- if (need_butler) {
- _butler->schedule_transport_work ();
- }
-
- loop_changing = false;
-
- _send_timecode_update = true;
-
- if (with_mmc) {
- send_mmc_locate (_transport_frame);
- }
-
- _last_roll_location = _last_roll_or_reversal_location = _transport_frame;
- if (!synced_to_engine () || _transport_frame == _engine.transport_frame ()) {
- 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, framepos_t destination_frame, 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_frame, as_default));
-
- if (_transport_speed == speed) {
- if (as_default && speed == 0.0) { // => reset default transport speed. hacky or what?
- _default_transport_speed = 1.0;
- }
- return;
- }
-
- 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, frame %2\n",
- _transport_speed, _transport_frame));
- return;
- }
-
- _target_transport_speed = fabs(speed);
-
- /* 8.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 (speed > 0) {
- speed = min (8.0, speed);
- } else if (speed < 0) {
- speed = max (-8.0, 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);
- }
+ /* Note: this function executes in the butler thread context */
- 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));
+ 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 (!auto_return_enabled) {
- _requested_return_frame = destination_frame;
- }
+ 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));
- stop_transport (abort);
- }
+ if (ptw & PostTransportLocate) {
- } 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 (get_play_loop() && !Config->get_seamless_loop()) {
- if (Config->get_loop_is_mode() && play_loop) {
+ DEBUG_TRACE (DEBUG::Butler, "flush loop recording fragment to disk\n");
- Location *location = _locations->auto_loop_location();
+ /* 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 (location != 0) {
- if (_transport_frame != location->start()) {
+ bool more_disk_io_to_do = false;
+ uint32_t errors = 0;
- if (Config->get_seamless_loop()) {
- /* force tracks to do their thing */
- set_track_loop (true);
- }
+ do {
+ more_disk_io_to_do = _butler->flush_tracks_to_disk_after_locate (r, errors);
- /* jump to start and then roll from there */
+ if (errors) {
+ break;
+ }
- request_locate (location->start(), true);
- return;
+ if (more_disk_io_to_do) {
+ continue;
}
- }
- }
- if (Config->get_monitoring_model() == HardwareMonitoring && config.get_auto_input()) {
- set_track_monitor_input_status (false);
- }
+ } while (false);
- if (synced_to_engine()) {
- _engine.transport_start ();
- _count_in_once = false;
- } else {
- start_transport ();
}
+ }
- } else {
-
- /* not zero, not 1.0 ... varispeed */
-
- 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 (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);
}
-
- if (actively_recording()) {
- return;
+ VCAList v = _vca_manager->vcas ();
+ for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) {
+ (*i)->non_realtime_locate (_transport_sample);
}
+ }
- if (speed > 0.0 && _transport_frame == current_end_frame()) {
- return;
+ 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 ();
+ }
}
+ }
- if (speed < 0.0 && _transport_frame == 0) {
- return;
- }
+ if (ptw & PostTransportReverse) {
- clear_clicks ();
+ 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.
- */
+ /* don't seek if locate will take care of that in non_realtime_stop() */
- PostTransportWork todo = PostTransportWork (0);
+ if (!(ptw & PostTransportLocate)) {
+ for (RouteList::iterator i = r->begin(); i != r->end(); ++i) {
+ (*i)->non_realtime_locate (_transport_sample);
- 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_frame;
+ 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);
+ }
}
+ }
- _last_transport_speed = _transport_speed;
- _transport_speed = speed;
+ if (ptw & PostTransportLocate) {
+ DEBUG_TRACE (DEBUG::Transport, "nonrealtime locate invoked from BTW\n");
+ non_realtime_locate ();
+ }
- if (as_default) {
- _default_transport_speed = speed;
+ 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;
}
+ }
- 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->realtime_speed_change()) {
- todo = PostTransportWork (todo | PostTransportSpeed);
- }
+ if (ptw & PostTransportOverWrite) {
+ non_realtime_overwrite (on_entry, finished);
+ if (!finished) {
+ g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
+ goto restart;
}
+ }
- if (todo) {
- add_post_transport_work (todo);
- _butler->schedule_transport_work ();
- }
+ if (ptw & PostTransportAudition) {
+ non_realtime_set_audition ();
+ }
- DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC3 with speed = %1\n", _transport_speed));
+ g_atomic_int_dec_and_test (&_butler->should_do_transport_work);
- /* 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 - speed) > .002
- // still, signal hard changes to 1.0 and 0.0:
- || ( speed == 1.0 && _signalled_varispeed != 1.0)
- || ( speed == 0.0 && _signalled_varispeed != 0.0)
- )
- {
- TransportStateChange (); /* EMIT SIGNAL */
- _signalled_varispeed = speed;
- }
- }
+ 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()));
}
-
-/** Stop the transport. */
void
-Session::stop_transport (bool abort, bool clear_state)
+Session::non_realtime_overwrite (int on_entry, bool& finished)
{
- _count_in_once = false;
- if (_transport_speed == 0.0f) {
- return;
+ 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;
+ }
}
+}
- DEBUG_TRACE (DEBUG::Transport, string_compose ("stop_transport, declick required? %1\n", get_transport_declick_required()));
-
- if (!get_transport_declick_required()) {
-
- /* stop has not yet been scheduled */
+void
+Session::non_realtime_locate ()
+{
+ DEBUG_TRACE (DEBUG::Transport, string_compose ("locate tracks to %1\n", _transport_sample));
- boost::shared_ptr<RouteList> rl = routes.reader();
- framepos_t stop_target = audible_frame();
+ if (Config->get_loop_is_mode() && get_play_loop()) {
- for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) {
- boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> (*i);
- if (tr) {
- tr->prepare_to_stop (_transport_frame, stop_target);
- }
- }
+ Location *loc = _locations->auto_loop_location();
- SubState new_bits;
+ 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);
- if (actively_recording() && /* we are recording */
- worst_input_latency() > current_block_size) { /* input latency exceeds block size, so simple 1 cycle delay before stop is not enough */
+ } else if (loc && Config->get_seamless_loop() &&
+ ((loc->start() <= _transport_sample) || (loc->end() > _transport_sample))) {
- /* we need to capture the audio that is still somewhere in the pipeline between
- wherever it was generated and the process callback. This means that even though
- the user (or something else) has asked us to stop, we have to roll
- past this point and then reset the playhead/transport location to
- the position at which the stop was requested.
+ /* 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);
- we still need playback to "stop" now, however, which is why we schedule
- a declick below.
- */
+ } else if (loc) {
+ set_track_loop (false);
+ }
- DEBUG_TRACE (DEBUG::Transport, string_compose ("stop transport requested @ %1, scheduled for + %2 = %3, abort = %4\n",
- _transport_frame, _worst_input_latency,
- _transport_frame + _worst_input_latency,
- abort));
+ } else {
- SessionEvent *ev = new SessionEvent (SessionEvent::StopOnce, SessionEvent::Replace,
- _transport_frame + _worst_input_latency,
- 0, 0, abort);
+ /* no more looping .. should have been noticed elsewhere */
+ }
- merge_event (ev);
- /* request a declick at the start of the next process cycle() so that playback ceases.
- It will remain silent until we actually stop (at the StopOnce event somewhere in
- the future). The extra flag (StopPendingCapture) is set to ensure that check_declick_out()
- does not stop the transport too early.
- */
- new_bits = SubState (PendingDeclickOut|StopPendingCapture);
+ samplepos_t tf;
- } else {
+ {
+ boost::shared_ptr<RouteList> rl = routes.reader();
- /* Not recording, schedule a declick in the next process() cycle and then stop at its end */
+ restart:
+ gint sc = g_atomic_int_get (&_seek_counter);
+ tf = _transport_sample;
- new_bits = PendingDeclickOut;
- DEBUG_TRACE (DEBUG::Transport, string_compose ("stop scheduled for next process cycle @ %1\n", _transport_frame));
+ 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;
+ }
}
+ }
- /* we'll be called again after the declick */
- transport_sub_state = SubState (transport_sub_state|new_bits);
- pending_abort = abort;
+ {
+ /* 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);
+ }
+ }
- return;
+ _scene_changer->locate (_transport_sample);
- } else {
+ /* 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 ();
+}
- DEBUG_TRACE (DEBUG::Transport, "time to actually stop\n");
+bool
+Session::select_playhead_priority_target (samplepos_t& jump_to)
+{
+ if (!transport_master_no_external_or_using_engine() || !config.get_auto_return()) {
+ return false;
+ }
- /* declick was scheduled, but we've been called again, which means it is really time to stop
+ jump_to = _last_roll_location;
+ return jump_to >= 0;
+}
- XXX: we should probably split this off into its own method and call it explicitly.
- */
+void
+Session::follow_playhead_priority ()
+{
+ samplepos_t target;
- realtime_stop (abort, clear_state);
- _butler->schedule_transport_work ();
+ if (select_playhead_priority_target (target)) {
+ request_locate (target);
}
}
-/** Called from the process thread */
void
-Session::start_transport ()
+Session::non_realtime_stop (bool abort, int on_entry, bool& finished)
{
- DEBUG_TRACE (DEBUG::Transport, "start_transport\n");
+ struct tm* now;
+ time_t xnow;
+ bool did_record;
+ bool saved;
+ PostTransportWork ptw = post_transport_work();
- _last_roll_location = _transport_frame;
- _last_roll_or_reversal_location = _transport_frame;
+ did_record = false;
+ saved = false;
- have_looped = 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;
+ }
+ }
- /* if record status is Enabled, move it to Recording. if its
- already Recording, move it to Disabled.
- */
+ /* stop and locate are merged here because they share a lot of common stuff */
- switch (record_status()) {
- case Enabled:
- if (!config.get_punch_in() && !preroll_record_punch_enabled()) {
- enable_record ();
- }
- break;
+ time (&xnow);
+ now = localtime (&xnow);
- case Recording:
- if (!play_loop) {
- disable_record (false);
- }
- break;
+ if (auditioner) {
+ auditioner->cancel_audition ();
+ }
- default:
- break;
+ if (did_record) {
+ begin_reversible_command (Operations::capture);
+ _have_captured = true;
}
- transport_sub_state |= PendingDeclickIn;
+ DEBUG_TRACE (DEBUG::Transport, X_("Butler PTW: DS stop\n"));
- _transport_speed = _default_transport_speed;
- _target_transport_speed = _transport_speed;
+ 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);
+ }
- 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->realtime_speed_change ();
+ tr->transport_stopped_wallclock (*now, xnow, abort);
}
}
- if (!_engine.freewheeling()) {
- Timecode::Time time;
- timecode_time_subframes (_transport_frame, time);
- if (!dynamic_cast<MTC_Slave*>(_slave)) {
- send_immediate_mmc (MIDI::MachineControlCommand (MIDI::MachineControl::cmdDeferredPlay));
- }
-
- 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_frame
- * - calc duration of 1 bar + time-to-beat before or at transport_frame
- */
- const Tempo& tempo = _tempo_map->tempo_at_frame (_transport_frame);
- const Meter& meter = _tempo_map->meter_at_frame (_transport_frame);
-
- const double num = meter.divisions_per_bar ();
- const double den = meter.note_divisor ();
- const double barbeat = _tempo_map->exact_qn_at_frame (_transport_frame, 0) * den / (4. * num);
- const double bar_fract = fmod (barbeat, 1.0); // fraction of bar elapsed.
-
- _count_in_samples = meter.frames_per_bar (tempo, _current_frame_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 ();
- int clickbeat = 0;
- framepos_t cf = _transport_frame - _count_in_samples;
- while (cf < _transport_frame) {
- add_click (cf - _worst_track_latency, clickbeat == 0);
- cf += dt;
- clickbeat = fmod (clickbeat + 1, num);
- }
+ 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));
}
}
- DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC4 with speed = %1\n", _transport_speed));
- TransportStateChange (); /* EMIT SIGNAL */
-}
-
-/** 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 (_engine.running()) {
+ PostTransportWork ptw = post_transport_work ();
- if (ptw & PostTransportAudition) {
- if (auditioner && auditioner->auditioning()) {
- process_function = &Session::process_audition;
- } else {
- process_function = &Session::process_with_events;
+ 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 ();
}
- if (ptw & PostTransportStop) {
+ /* 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() && !(ptw & PostTransportLocate)) {
+
+ bool do_locate = false;
- transport_sub_state = 0;
- }
+ if (_requested_return_sample >= 0) {
- if (ptw & PostTransportLocate) {
+ /* explicit return request pre-queued in event list. overrides everything else */
- 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;
- }
- }
+ _transport_sample = _requested_return_sample;
- 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));
-}
+ /* cancel this request */
+ _requested_return_sample = -1;
+ do_locate = true;
-void
-Session::reset_rf_scale (framecnt_t motion)
-{
- cumulative_rf_motion += motion;
-
- if (cumulative_rf_motion < 4 * _current_frame_rate) {
- rf_scale = 1;
- } else if (cumulative_rf_motion < 8 * _current_frame_rate) {
- rf_scale = 4;
- } else if (cumulative_rf_motion < 16 * _current_frame_rate) {
- rf_scale = 10;
- } else {
- rf_scale = 100;
- }
+ } else if (Config->get_auto_return_target_list()) {
- if (motion != 0) {
- set_dirty();
- }
-}
+ samplepos_t jump_to;
-void
-Session::mtc_status_changed (bool yn)
-{
- g_atomic_int_set (&_mtc_active, yn);
- MTCSyncStateChanged( yn );
-}
+ if (select_playhead_priority_target (jump_to)) {
-void
-Session::ltc_status_changed (bool yn)
-{
- g_atomic_int_set (&_ltc_active, yn);
- LTCSyncStateChanged( yn );
-}
+ /* there's a valid target (we don't care how it
+ * was derived here)
+ */
-void
-Session::use_sync_source (Slave* new_slave)
-{
- /* Runs in process() context */
+ _transport_sample = jump_to;
+ do_locate = true;
- bool non_rt_required = false;
+ } else if (abort) {
- /* XXX this deletion is problematic because we're in RT context */
+ /* roll aborted (typically capture) with
+ * auto-return enabled
+ */
- delete _slave;
- _slave = new_slave;
+ _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);
- 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()) {
- if (tr->realtime_speed_change()) {
- non_rt_required = true;
+ if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) {
+ finished = false;
+ /* we will be back */
+ return;
}
- tr->set_slaved (_slave != 0);
}
}
- 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
/* locating/stopping is subject to delays for declicking.
*/
- framepos_t requested_frame = i->end;
+ samplepos_t requested_sample = i->end;
- if (requested_frame > current_block_size) {
- requested_frame -= current_block_size;
+ if (requested_sample > current_block_size) {
+ requested_sample -= current_block_size;
} else {
- requested_frame = 0;
+ requested_sample = 0;
}
if (next == range.end()) {
- ev = new SessionEvent (SessionEvent::RangeStop, SessionEvent::Add, requested_frame, 0, 0.0f);
+ ev = new SessionEvent (SessionEvent::RangeStop, SessionEvent::Add, requested_sample, 0, 0.0f);
} else {
- ev = new SessionEvent (SessionEvent::RangeLocate, SessionEvent::Add, requested_frame, (*next).start, 0.0f);
+ ev = new SessionEvent (SessionEvent::RangeLocate, SessionEvent::Add, requested_sample, (*next).start, 0.0f);
}
merge_event (ev);
}
void
-Session::request_bounded_roll (framepos_t start, framepos_t end)
+Session::request_bounded_roll (samplepos_t start, samplepos_t end)
{
AudioRange ar (start, end, 0);
list<AudioRange> lar;
}
void
-Session::set_requested_return_frame (framepos_t return_to)
+Session::set_requested_return_sample (samplepos_t return_to)
{
- _requested_return_frame = return_to;
+ _requested_return_sample = return_to;
}
void
-Session::request_roll_at_and_return (framepos_t start, framepos_t return_to)
+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_frame = start;
+ ev->target2_sample = start;
queue_event (ev);
}
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 ()
{
++_xrun_count;
- Xrun (_transport_frame); /* EMIT SIGNAL */
+ Xrun (_transport_sample); /* EMIT SIGNAL */
if (Config->get_stop_recording_on_xrun() && actively_recording()) {
auto_play_legal = yn;
}
-bool
-Session::maybe_stop (framepos_t limit)
-{
- if ((_transport_speed > 0.0f && _transport_frame >= limit) || (_transport_speed < 0.0f && _transport_frame == 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 (framepos_t t)
+Session::send_mmc_locate (samplepos_t t)
{
if (t < 0) {
return;
{
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();
+}