/* * Copyright (C) 1999-2019 Paul Davis * Copyright (C) 2005-2009 Taybin Rutkin * Copyright (C) 2006-2007 Jesse Chappell * Copyright (C) 2006-2012 David Robillard * Copyright (C) 2007-2012 Carl Hetherington * Copyright (C) 2008-2009 Hans Baier * Copyright (C) 2012-2019 Robin Gareus * Copyright (C) 2014-2018 Ben Loftis * Copyright (C) 2015 GZharun * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifdef WAF_BUILD #include "libardour-config.h" #endif #include #include #include #include #include "pbd/error.h" #include "pbd/enumwriter.h" #include "pbd/i18n.h" #include "pbd/memento_command.h" #include "pbd/pthread_utils.h" #include "pbd/stacktrace.h" #include "pbd/undo.h" #include "midi++/mmc.h" #include "midi++/port.h" #include "ardour/audio_backend.h" #include "ardour/audioengine.h" #include "ardour/auditioner.h" #include "ardour/automation_watch.h" #include "ardour/butler.h" #include "ardour/click.h" #include "ardour/debug.h" #include "ardour/disk_reader.h" #include "ardour/location.h" #include "ardour/playlist.h" #include "ardour/profile.h" #include "ardour/scene_changer.h" #include "ardour/session.h" #include "ardour/transport_fsm.h" #include "ardour/transport_master.h" #include "ardour/transport_master_manager.h" #include "ardour/tempo.h" #include "ardour/operations.h" #include "ardour/vca.h" #include "ardour/vca_manager.h" using namespace std; using namespace ARDOUR; using namespace PBD; #ifdef NDEBUG # define ENSURE_PROCESS_THREAD do {} while (0) #else # define ENSURE_PROCESS_THREAD \ do { \ if (!AudioEngine::instance()->in_process_thread()) { \ PBD::stacktrace (std::cerr, 30); \ } \ } while (0) #endif #define TFSM_EVENT(evtype) { _transport_fsm->enqueue (new TransportFSM::Event (evtype)); } #define TFSM_STOP(abort,clear) { _transport_fsm->enqueue (new TransportFSM::Event (TransportFSM::StopTransport,abort,clear)); } #define TFSM_LOCATE(target,ltd,flush,loop,force) { _transport_fsm->enqueue (new TransportFSM::Event (TransportFSM::Locate,target,ltd,flush,loop,force)); } /* ***************************************************************************** * REALTIME ACTIONS (to be called on state transitions) * ****************************************************************************/ void Session::realtime_stop (bool abort, bool clear_state) { ENSURE_PROCESS_THREAD; DEBUG_TRACE (DEBUG::Transport, string_compose ("realtime stop @ %1 speed = %2\n", _transport_sample, _transport_speed)); PostTransportWork todo = PostTransportStop; /* this resets the speed we will start at if just requested to roll * again. Don't do it if we are stopping to locate ... in those * conditions, keep the current default speed so that when we start * again we resume that speed */ if (!_transport_fsm->declicking_for_locate()) { _default_transport_speed = 1.0; } /* call routes */ boost::shared_ptr r = routes.reader (); for (RouteList::iterator i = r->begin (); i != r->end(); ++i) { (*i)->realtime_handle_transport_stopped (); } DEBUG_TRACE (DEBUG::Transport, string_compose ("stop complete, auto-return scheduled for return to %1\n", _requested_return_sample)); if (abort) { todo = PostTransportWork (todo | PostTransportAbort); } if (clear_state) { todo = PostTransportWork (todo | PostTransportClearSubstate); } if (todo) { add_post_transport_work (todo); } _clear_event_type (SessionEvent::RangeStop); _clear_event_type (SessionEvent::RangeLocate); //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)); if (clear_state && !Config->get_loop_is_mode()) { unset_play_loop (); } reset_punch_loop_constraint (); _transport_speed = 0; _engine_speed = 1.0; g_atomic_int_set (&_playback_load, 100); g_atomic_int_set (&_capture_load, 100); if (config.get_use_video_sync()) { waiting_for_sync_offset = true; } if (todo) { TFSM_EVENT (TransportFSM::ButlerRequired); } } /** @param with_mmc true to send a MMC locate command when the locate is done */ void Session::locate (samplepos_t target_sample, bool with_roll, bool with_flush, bool for_loop_end, bool force, bool with_mmc) { ENSURE_PROCESS_THREAD; if (target_sample < 0) { error << _("Locate called for negative sample position - ignored") << endmsg; return; } bool need_butler = false; /* Locates for seamless looping are fairly different from other * locates. They assume that the diskstream buffers for each track * already have the correct data in them, and thus there is no need to * actually tell the tracks to locate. What does need to be done, * though, is all the housekeeping that is associated with non-linear * changes in the value of _transport_sample. */ DEBUG_TRACE (DEBUG::Transport, string_compose ("rt-locate to %1 ts = %7, roll %2 flush %3 for loop end %4 force %5 mmc %6\n", target_sample, with_roll, with_flush, for_loop_end, force, with_mmc, _transport_sample)); if (!force && (_transport_sample == target_sample) && !for_loop_end) { /* already at the desired position. Not forced to locate, so unless we're told to start rolling also, there's nothing to do but tell the world where we are (again). */ if (with_roll) { set_transport_speed (1.0, false, false, false); } TFSM_EVENT (TransportFSM::LocateDone); Located (); /* EMIT SIGNAL */ return; } // Update Timecode time _transport_sample = target_sample; _nominal_jack_transport_sample = boost::none; // Bump seek counter so that any in-process locate in the butler // thread(s?) can restart. g_atomic_int_inc (&_seek_counter); _last_roll_or_reversal_location = target_sample; if (!for_loop_end) { _remaining_latency_preroll = worst_latency_preroll_buffer_size_ceil (); } timecode_time(_transport_sample, transmitting_timecode_time); // XXX here? /* do "stopped" stuff if: * * we are rolling AND * no autoplay in effect AND * we're not going to keep rolling after the locate AND * !(playing a loop with JACK sync) AND * we're not synced to an external transport master * */ /* it is important here that we use the internal state of the transport FSM, not the public facing result of ::transport_rolling() */ bool transport_was_stopped = !_transport_fsm->rolling(); if (!transport_was_stopped && (!auto_play_legal || !config.get_auto_play()) && !with_roll && !(synced_to_engine() && get_play_loop ()) && !(config.get_external_sync() && !synced_to_engine())) { realtime_stop (false, true); // XXX paul - check if the 2nd arg is really correct transport_was_stopped = true; } else { /* Tell all routes to do the RT part of locate */ boost::shared_ptr r = routes.reader (); for (RouteList::iterator i = r->begin(); i != r->end(); ++i) { (*i)->realtime_locate (for_loop_end); } } if (force || !for_loop_end) { PostTransportWork todo = PostTransportLocate; if (with_roll && transport_was_stopped) { todo = PostTransportWork (todo | PostTransportRoll); } add_post_transport_work (todo); need_butler = true; } else { /* this is functionally what clear_clicks() does but with a tentative lock */ Glib::Threads::RWLock::WriterLock clickm (click_lock, Glib::Threads::TRY_LOCK); if (clickm.locked()) { for (Clicks::iterator i = clicks.begin(); i != clicks.end(); ++i) { delete *i; } clicks.clear (); } } if (with_roll) { /* switch from input if we're going to roll */ if (Config->get_monitoring_model() == HardwareMonitoring) { set_track_monitor_input_status (!config.get_auto_input()); } } else { /* otherwise we're going to stop, so do the opposite */ if (Config->get_monitoring_model() == HardwareMonitoring) { set_track_monitor_input_status (true); } } /* cancel looped playback if transport pos outside of loop range */ if (get_play_loop ()) { Location* al = _locations->auto_loop_location(); if (al) { if (_transport_sample < al->start() || _transport_sample >= al->end()) { // located outside the loop: cancel looping directly, this is called from event handling context have_looped = false; if (!Config->get_loop_is_mode()) { set_play_loop (false, false); } else { /* this will make the non_realtime_locate() in the butler which then causes seek() in tracks actually do the right thing. */ set_track_loop (false); } } else if (_transport_sample == al->start()) { // located to start of loop - this is looping, basically boost::shared_ptr rl = routes.reader(); for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) { boost::shared_ptr tr = boost::dynamic_pointer_cast (*i); if (tr && tr->rec_enable_control()->get_value()) { // tell it we've looped, so it can deal with the record state tr->transport_looped (_transport_sample); } } if (for_loop_end) { have_looped = true; TransportLooped(); // EMIT SIGNAL } } } } if (need_butler) { TFSM_EVENT (TransportFSM::ButlerRequired); } else { TFSM_EVENT (TransportFSM::LocateDone); } _send_timecode_update = true; if (with_mmc) { send_mmc_locate (_transport_sample); } _last_roll_location = _last_roll_or_reversal_location = _transport_sample; Located (); /* EMIT SIGNAL */ } void Session::post_locate () { if (transport_master_is_external() && !synced_to_engine()) { const samplepos_t current_master_position = TransportMasterManager::instance().get_current_position_in_process_context(); if (abs (current_master_position - _transport_sample) > TransportMasterManager::instance().current()->resolution()) { _last_roll_location = _last_roll_or_reversal_location = _transport_sample; } } } /** Set the transport speed. * Called from the process thread. * @param speed New speed */ void Session::set_transport_speed (double speed, bool abort, bool clear_state, bool as_default) { ENSURE_PROCESS_THREAD; DEBUG_TRACE (DEBUG::Transport, string_compose ("@ %5 Set transport speed to %1 from %4 (es = %7), abort = %2 clear_state = %3, as_default %6\n", speed, abort, clear_state, _transport_speed, _transport_sample, as_default, _engine_speed)); if ((_engine_speed != 1) && (_engine_speed == fabs (speed)) && (speed * _transport_speed) >= 0) { /* engine speed is not changing and no direction change, do nothing */ DEBUG_TRACE (DEBUG::Transport, "no reason to change speed, do nothing\n"); return; } /* max speed is somewhat arbitrary but based on guestimates regarding disk i/o capability and user needs. XXX We really need CD-style "skip" playback for ffwd and rewind. */ if (speed > 0) { speed = min ((double) Config->get_max_transport_speed(), speed); } else if (speed < 0) { speed = max ((double) -Config->get_max_transport_speed(), speed); } double new_engine_speed = 1.0; if (speed != 0) { new_engine_speed = fabs (speed); if (speed < 0) speed = -1; if (speed > 0) speed = 1; } if (_transport_speed == speed && new_engine_speed == _engine_speed) { if (as_default && speed == 0.0) { // => reset default transport speed. hacky or what? _default_transport_speed = 1.0; } return; } #if 0 // TODO pref: allow vari-speed recording if (actively_recording() && speed != 1.0 && speed != 0.0) { /* no varispeed during recording */ DEBUG_TRACE (DEBUG::Transport, string_compose ("No varispeed during recording cur_speed %1, sample %2\n", _transport_speed, _transport_sample)); return; } #endif _engine_speed = new_engine_speed; if (transport_rolling() && speed == 0.0) { /* we are rolling and we want to stop */ if (Config->get_monitoring_model() == HardwareMonitoring) { set_track_monitor_input_status (true); } if (synced_to_engine ()) { if (clear_state) { /* do this here because our response to the slave won't take care of it. */ _play_range = false; _count_in_once = false; unset_play_loop (); } } TFSM_STOP (abort, false); } else if (transport_stopped() && speed == 1.0) { if (as_default) { _default_transport_speed = speed; } /* we are stopped and we want to start rolling at speed 1 */ if (Config->get_loop_is_mode() && get_play_loop ()) { Location *location = _locations->auto_loop_location(); if (location != 0) { if (_transport_sample != location->start()) { /* force tracks to do their thing */ set_track_loop (true); /* jump to start and then roll from there */ request_locate (location->start(), MustRoll); return; } } } if (Config->get_monitoring_model() == HardwareMonitoring && config.get_auto_input()) { set_track_monitor_input_status (false); } TFSM_EVENT (TransportFSM::StartTransport); } else { /* not zero, not 1.0 ... varispeed */ // TODO handled transport start.. _remaining_latency_preroll // and reversal of playback direction. if ((synced_to_engine()) && speed != 0.0 && speed != 1.0) { warning << string_compose ( _("Global varispeed cannot be supported while %1 is connected to JACK transport control"), PROGRAM_NAME) << endmsg; return; } #if 0 if (actively_recording()) { return; } #endif if (speed > 0.0 && _transport_sample == current_end_sample()) { return; } if (speed < 0.0 && _transport_sample == 0) { return; } clear_clicks (); /* if we are reversing relative to the current speed, or relative to the speed before the last stop, then we have to do extra work. */ _transport_speed = speed; if (as_default) { _default_transport_speed = speed; } DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC3 with speed = %1\n", _transport_speed)); /* throttle signal emissions. * when slaved [_last]_transport_speed * usually changes every cycle (tiny amounts due to DLL). * Emitting a signal every cycle is overkill and unwarranted. * * Using _transport_speed is not acceptable, * since it allows for large changes over a long period * of time. Hence we introduce a dedicated variable to keep track * * The 0.2% dead-zone is somewhat arbitrary. Main use-case * for TransportStateChange() here is the ShuttleControl display. */ if (fabs (_signalled_varispeed - actual_speed ()) > .002 // still, signal hard changes to 1.0 and 0.0: || (actual_speed () == 1.0 && _signalled_varispeed != 1.0) || (actual_speed () == 0.0 && _signalled_varispeed != 0.0) ) { TransportStateChange (); /* EMIT SIGNAL */ _signalled_varispeed = actual_speed (); } } } /** Stop the transport. */ void Session::stop_transport (bool abort, bool clear_state) { ENSURE_PROCESS_THREAD; _count_in_once = false; DEBUG_TRACE (DEBUG::Transport, string_compose ("time to actually stop with TS @ %1\n", _transport_sample)); realtime_stop (abort, clear_state); } /** Called from the process thread */ void Session::start_transport () { ENSURE_PROCESS_THREAD; DEBUG_TRACE (DEBUG::Transport, "start_transport\n"); _last_roll_location = _transport_sample; _last_roll_or_reversal_location = _transport_sample; if (!have_looped && !_exporting) { _remaining_latency_preroll = worst_latency_preroll_buffer_size_ceil (); } have_looped = false; /* if record status is Enabled, move it to Recording. if its already Recording, move it to Disabled. */ switch (record_status()) { case Enabled: if (!config.get_punch_in()) { /* This is only for UIs (keep blinking rec-en before * punch-in, don't show rec-region etc). The UI still * depends on SessionEvent::PunchIn and ensuing signals. * * The disk-writers handle punch in/out internally * in their local delay-compensated timeframe. */ enable_record (); } break; case Recording: if (!get_play_loop ()) { disable_record (false); } break; default: break; } maybe_allow_only_loop (); maybe_allow_only_punch (); _transport_speed = _default_transport_speed; if (!_engine.freewheeling()) { Timecode::Time time; timecode_time_subframes (_transport_sample, time); if (transport_master()->type() != MTC) { // why not when slaved to MTC? send_immediate_mmc (MIDI::MachineControlCommand (MIDI::MachineControl::cmdDeferredPlay)); } if ((actively_recording () || (config.get_punch_in () && get_record_enabled ())) && click_data && (config.get_count_in () || _count_in_once)) { _count_in_once = false; /* calculate count-in duration (in audio samples) * - use [fixed] tempo/meter at _transport_sample * - calc duration of 1 bar + time-to-beat before or at transport_sample */ const Tempo& tempo = _tempo_map->tempo_at_sample (_transport_sample); const Meter& meter = _tempo_map->meter_at_sample (_transport_sample); const double num = meter.divisions_per_bar (); const double den = meter.note_divisor (); const double barbeat = _tempo_map->exact_qn_at_sample (_transport_sample, 0) * den / (4. * num); const double bar_fract = fmod (barbeat, 1.0); // fraction of bar elapsed. _count_in_samples = meter.samples_per_bar (tempo, _current_sample_rate); double dt = _count_in_samples / num; if (bar_fract == 0) { /* at bar boundary, count-in 2 bars before start. */ _count_in_samples *= 2; } else { /* beats left after full bar until roll position */ _count_in_samples *= 1. + bar_fract; } if (_count_in_samples > _remaining_latency_preroll) { _remaining_latency_preroll = _count_in_samples; } int clickbeat = 0; samplepos_t cf = _transport_sample - _count_in_samples; samplecnt_t offset = _click_io->connected_latency (true); clear_clicks (); _clicks_cleared = cf; while (cf < _transport_sample + offset) { add_click (cf, clickbeat == 0); cf += dt; clickbeat = fmod (clickbeat + 1, num); } if (_count_in_samples < _remaining_latency_preroll) { _count_in_samples = _remaining_latency_preroll; } } } DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC4 with speed = %1\n", _transport_speed)); TransportStateChange (); /* EMIT SIGNAL */ } bool Session::should_roll_after_locate () const { /* a locate must previously have been requested and completed before * this answer can be considered correct */ return ((!config.get_external_sync() && (auto_play_legal && config.get_auto_play())) && !_exporting) || (post_transport_work() & PostTransportRoll); } /** Do any transport work in the audio thread that needs to be done after the * butler thread is finished. Audio thread, realtime safe. */ void Session::butler_completed_transport_work () { ENSURE_PROCESS_THREAD; PostTransportWork ptw = post_transport_work (); DEBUG_TRACE (DEBUG::Transport, string_compose ("Butler done, RT cleanup for %1\n", enum_2_string (ptw))); if (ptw & PostTransportAudition) { if (auditioner && auditioner->auditioning()) { _remaining_latency_preroll = 0; process_function = &Session::process_audition; } else { process_function = &Session::process_with_events; } ptw = PostTransportWork (ptw & ~PostTransportAudition); set_post_transport_work (ptw); } if (ptw & PostTransportLocate) { post_locate (); ptw = PostTransportWork (ptw & ~PostTransportLocate); set_post_transport_work (ptw); TFSM_EVENT (TransportFSM::LocateDone); } bool start_after_butler_done_msg = false; if (ptw & PostTransportRoll) { start_after_butler_done_msg = true; } /* the butler finished its work so clear all PostTransportWork flags */ set_post_transport_work (PostTransportWork (0)); set_next_event (); if (_transport_fsm->waiting_for_butler()) { TFSM_EVENT (TransportFSM::ButlerDone); } if (start_after_butler_done_msg) { if (_transport_speed) { /* reversal is done ... tell TFSM that it is time to start*/ TFSM_EVENT (TransportFSM::StartTransport); } } } void Session::schedule_butler_for_transport_work () { assert (_transport_fsm->waiting_for_butler ()); DEBUG_TRACE (DEBUG::Butler, "summon butler for transport work\n"); _butler->schedule_transport_work (); } bool Session::maybe_stop (samplepos_t limit) { ENSURE_PROCESS_THREAD; if ((_transport_speed > 0.0f && _transport_sample >= limit) || (_transport_speed < 0.0f && _transport_sample == 0)) { if (synced_to_engine ()) { _engine.transport_stop (); } else { TFSM_STOP (false, false); } return true; } return false; } int Session::micro_locate (samplecnt_t distance) { ENSURE_PROCESS_THREAD; boost::shared_ptr rl = routes.reader(); for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) { boost::shared_ptr tr = boost::dynamic_pointer_cast (*i); if (tr && !tr->can_internal_playback_seek (distance)) { return -1; } } DEBUG_TRACE (DEBUG::Transport, string_compose ("micro-locate by %1\n", distance)); for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) { boost::shared_ptr tr = boost::dynamic_pointer_cast (*i); if (tr) { tr->internal_playback_seek (distance); } } _transport_sample += distance; return 0; } void Session::flush_all_inserts () { ENSURE_PROCESS_THREAD; boost::shared_ptr r = routes.reader (); for (RouteList::iterator i = r->begin(); i != r->end(); ++i) { (*i)->flush_processors (); } } /* ***************************************************************************** * END REALTIME ACTIONS * ****************************************************************************/ void Session::add_post_transport_work (PostTransportWork ptw) { PostTransportWork oldval; PostTransportWork newval; int tries = 0; while (tries < 8) { oldval = (PostTransportWork) g_atomic_int_get (&_post_transport_work); newval = PostTransportWork (oldval | ptw); if (g_atomic_int_compare_and_exchange (&_post_transport_work, oldval, newval)) { /* success */ return; } } error << "Could not set post transport work! Crazy thread madness, call the programmers" << endmsg; } bool Session::should_ignore_transport_request (TransportRequestSource src, TransportRequestType type) { if (config.get_external_sync()) { if (TransportMasterManager::instance().current()->allow_request (src, type)) { /* accepting a command means dropping external sync first */ config.set_external_sync (false); return true; } } return false; } bool Session::synced_to_engine() const { return config.get_external_sync() && TransportMasterManager::instance().current()->type() == Engine; } void Session::request_sync_source (boost::shared_ptr tm) { SessionEvent* ev = new SessionEvent (SessionEvent::SetTransportMaster, SessionEvent::Add, SessionEvent::Immediate, 0, 0.0); ev->transport_master = tm; DEBUG_TRACE (DEBUG::Slave, "sent request for new transport master\n"); queue_event (ev); } void Session::request_transport_speed (double speed, bool as_default, TransportRequestSource origin) { if (synced_to_engine()) { if (speed != 0) { _engine.transport_start (); } else { _engine.transport_stop (); } return; } if (speed == 1. || speed == 0. || speed == -1.) { if (should_ignore_transport_request (origin, TR_StartStop)) { return; } } else { if (should_ignore_transport_request (origin, TR_Speed)) { return; } } SessionEvent* ev = new SessionEvent (SessionEvent::SetTransportSpeed, SessionEvent::Add, SessionEvent::Immediate, 0, speed); ev->third_yes_or_no = as_default; // as_default DEBUG_TRACE (DEBUG::Transport, string_compose ("Request transport speed = %1 as default = %2\n", speed, as_default)); queue_event (ev); } /** Request a new transport speed, but if the speed parameter is exactly zero then use * a very small +ve value to prevent the transport actually stopping. This method should * be used by callers who are varying transport speed but don't ever want to stop it. */ void Session::request_transport_speed_nonzero (double speed, bool as_default, TransportRequestSource origin) { if (speed == 0) { speed = DBL_EPSILON; } request_transport_speed (speed, as_default); } void Session::request_stop (bool abort, bool clear_state, TransportRequestSource origin) { if (synced_to_engine()) { _engine.transport_stop (); return; } if (should_ignore_transport_request (origin, TR_StartStop)) { return; } SessionEvent* ev = new SessionEvent (SessionEvent::SetTransportSpeed, SessionEvent::Add, SessionEvent::Immediate, audible_sample(), 0.0, abort, clear_state); DEBUG_TRACE (DEBUG::Transport, string_compose ("Request transport stop, audible %3 transport %4 abort = %1, clear state = %2\n", abort, clear_state, audible_sample(), _transport_sample)); queue_event (ev); } void Session::request_locate (samplepos_t target_sample, LocateTransportDisposition ltd, TransportRequestSource origin) { if (synced_to_engine()) { _engine.transport_locate (target_sample); return; } if (should_ignore_transport_request (origin, TR_Locate)) { return; } SessionEvent::Type type; switch (ltd) { case MustRoll: type = SessionEvent::LocateRoll; break; case MustStop: type = SessionEvent::Locate; break; case RollIfAppropriate: if (config.get_auto_play()) { type = SessionEvent::LocateRoll; } else { type = SessionEvent::Locate; } break; } SessionEvent *ev = new SessionEvent (type, SessionEvent::Add, SessionEvent::Immediate, target_sample, 0, false); ev->locate_transport_disposition = ltd; DEBUG_TRACE (DEBUG::Transport, string_compose ("Request locate to %1 ltd = %2\n", target_sample, enum_2_string (ltd))); queue_event (ev); } void Session::force_locate (samplepos_t target_sample, LocateTransportDisposition ltd) { SessionEvent *ev = new SessionEvent (SessionEvent::Locate, SessionEvent::Add, SessionEvent::Immediate, target_sample, 0, true); ev->locate_transport_disposition = ltd; DEBUG_TRACE (DEBUG::Transport, string_compose ("Request forced locate to %1 roll %2\n", target_sample, enum_2_string (ltd))); queue_event (ev); } void Session::unset_preroll_record_trim () { _preroll_record_trim_len = 0; } void Session::request_preroll_record_trim (samplepos_t rec_in, samplecnt_t preroll) { if (actively_recording ()) { return; } unset_preroll_record_trim (); config.set_punch_in (false); config.set_punch_out (false); samplepos_t pos = std::max ((samplepos_t)0, rec_in - preroll); _preroll_record_trim_len = preroll; maybe_enable_record (); request_locate (pos, MustRoll); set_requested_return_sample (rec_in); } void Session::request_count_in_record () { if (actively_recording ()) { return; } if (transport_rolling()) { return; } maybe_enable_record (); _count_in_once = true; request_transport_speed (1.0, true); } void Session::request_play_loop (bool yn, bool change_transport_roll) { if (transport_master_is_external() && yn) { // don't attempt to loop when not using Internal Transport // see also gtk2_ardour/ardour_ui_options.cc parameter_changed() return; } SessionEvent* ev; Location *location = _locations->auto_loop_location(); double target_speed; if (location == 0 && yn) { error << _("Cannot loop - no loop range defined") << endmsg; return; } if (change_transport_roll) { if (transport_rolling()) { /* start looping at current speed */ target_speed = transport_speed (); } else { /* currently stopped */ if (yn) { /* start looping at normal speed */ target_speed = 1.0; } else { target_speed = 0.0; } } } else { /* leave the speed alone */ target_speed = transport_speed (); } ev = new SessionEvent (SessionEvent::SetLoop, SessionEvent::Add, SessionEvent::Immediate, 0, target_speed, yn, change_transport_roll); DEBUG_TRACE (DEBUG::Transport, string_compose ("Request set loop = %1, change roll state ? %2\n", yn, change_transport_roll)); queue_event (ev); } void Session::request_play_range (list* range, bool leave_rolling) { SessionEvent* ev = new SessionEvent (SessionEvent::SetPlayAudioRange, SessionEvent::Add, SessionEvent::Immediate, 0, (leave_rolling ? 1.0 : 0.0)); if (range) { ev->audio_range = *range; } else { ev->audio_range.clear (); } DEBUG_TRACE (DEBUG::Transport, string_compose ("Request play range, leave rolling ? %1\n", leave_rolling)); queue_event (ev); } void Session::request_cancel_play_range () { SessionEvent* ev = new SessionEvent (SessionEvent::CancelPlayAudioRange, SessionEvent::Add, SessionEvent::Immediate, 0, 0); queue_event (ev); } bool Session::solo_selection_active () { if (_soloSelection.empty()) { return false; } return true; } void Session::solo_selection (StripableList &list, bool new_state) { boost::shared_ptr solo_list (new ControlList); boost::shared_ptr unsolo_list (new ControlList); if (new_state) _soloSelection = list; else _soloSelection.clear(); boost::shared_ptr rl = get_routes(); for (ARDOUR::RouteList::iterator i = rl->begin(); i != rl->end(); ++i) { if ( !(*i)->is_track() ) { continue; } boost::shared_ptr s (*i); bool found = (std::find(list.begin(), list.end(), s) != list.end()); if ( new_state && found ) { solo_list->push_back (s->solo_control()); //must invalidate playlists on selected tracks, so only selected regions get heard boost::shared_ptr track = boost::dynamic_pointer_cast (*i); if (track) { boost::shared_ptr playlist = track->playlist(); if (playlist) { playlist->ContentsChanged(); } } } else { unsolo_list->push_back (s->solo_control()); } } set_controls (solo_list, 1.0, Controllable::NoGroup); set_controls (unsolo_list, 0.0, Controllable::NoGroup); } void Session::butler_transport_work () { /* Note: this function executes in the butler thread context */ restart: boost::shared_ptr r = routes.reader (); int on_entry = g_atomic_int_get (&_butler->should_do_transport_work); bool finished = true; PostTransportWork ptw = post_transport_work(); #ifndef NDEBUG uint64_t before; #endif DEBUG_TRACE (DEBUG::Transport, string_compose ("Butler transport work, todo = [%1] (0x%3%4%5) at %2\n", enum_2_string (ptw), (before = g_get_monotonic_time()), std::hex, ptw, std::dec)); if (ptw & PostTransportLocate) { if (get_play_loop()) { DEBUG_TRACE (DEBUG::Butler, "flush loop recording fragment to disk\n"); /* this locate might be happening while we are * loop recording. * * Non-seamless looping will require a locate (below) that * will reset capture buffers and throw away data. * * Rather than first find all tracks and see if they * have outstanding data, just do a flush anyway. It * may be cheaper this way anyway, and is certainly * more accurate. */ bool more_disk_io_to_do = false; uint32_t errors = 0; do { more_disk_io_to_do = _butler->flush_tracks_to_disk_after_locate (r, errors); if (errors) { break; } if (more_disk_io_to_do) { continue; } } while (false); } } if (ptw & PostTransportAdjustPlaybackBuffering) { /* need to prevent concurrency with ARDOUR::Reader::run(), * DiskWriter::adjust_buffering() re-allocates the ringbuffer */ Glib::Threads::Mutex::Lock lx (AudioEngine::instance()->process_lock ()); for (RouteList::iterator i = r->begin(); i != r->end(); ++i) { boost::shared_ptr tr = boost::dynamic_pointer_cast (*i); if (tr) { tr->adjust_playback_buffering (); /* and refill those buffers ... */ } (*i)->non_realtime_locate (_transport_sample); } VCAList v = _vca_manager->vcas (); for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) { (*i)->non_realtime_locate (_transport_sample); } } if (ptw & PostTransportAdjustCaptureBuffering) { /* need to prevent concurrency with ARDOUR::DiskWriter::run(), * DiskWriter::adjust_buffering() re-allocates the ringbuffer */ Glib::Threads::Mutex::Lock lx (AudioEngine::instance()->process_lock ()); for (RouteList::iterator i = r->begin(); i != r->end(); ++i) { boost::shared_ptr tr = boost::dynamic_pointer_cast (*i); if (tr) { tr->adjust_capture_buffering (); } } } if (ptw & PostTransportLocate) { DEBUG_TRACE (DEBUG::Transport, "nonrealtime locate invoked from BTW\n"); non_realtime_locate (); } if (ptw & PostTransportStop) { non_realtime_stop (ptw & PostTransportAbort, on_entry, finished); if (!finished) { g_atomic_int_dec_and_test (&_butler->should_do_transport_work); goto restart; } } if (ptw & PostTransportOverWrite) { non_realtime_overwrite (on_entry, finished, (ptw & PostTransportLoopChanged)); if (!finished) { g_atomic_int_dec_and_test (&_butler->should_do_transport_work); goto restart; } } if (ptw & PostTransportAudition) { non_realtime_set_audition (); } g_atomic_int_dec_and_test (&_butler->should_do_transport_work); DEBUG_TRACE (DEBUG::Transport, string_compose (X_("Butler transport work all done after %1 usecs @ %2 ptw %3 trw = %4\n"), g_get_monotonic_time() - before, _transport_sample, enum_2_string (post_transport_work()), _butler->transport_work_requested())); } void Session::non_realtime_overwrite (int on_entry, bool& finished, bool update_loop_declicks) { if (update_loop_declicks) { DiskReader::reset_loop_declick (_locations->auto_loop_location(), sample_rate()); } boost::shared_ptr rl = routes.reader(); for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) { boost::shared_ptr tr = boost::dynamic_pointer_cast (*i); if (tr && tr->pending_overwrite ()) { tr->overwrite_existing_buffers (); } if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) { finished = false; return; } } } void Session::non_realtime_locate () { DEBUG_TRACE (DEBUG::Transport, string_compose ("locate tracks to %1\n", _transport_sample)); if (Config->get_loop_is_mode() && get_play_loop()) { Location *loc = _locations->auto_loop_location(); if (!loc || (_transport_sample < loc->start() || _transport_sample >= loc->end())) { /* jumped out of loop range: stop tracks from looping, but leave loop (mode) enabled. */ set_track_loop (false); } else if (loc && ((loc->start() <= _transport_sample) || (loc->end() > _transport_sample))) { /* jumping to start of loop. This might have been done before but it is * idempotent and cheap. Doing it here ensures that when we start playback * outside the loop we still flip tracks into the magic seamless mode * when needed. */ set_track_loop (true); } else if (loc) { set_track_loop (false); } } else { /* no more looping .. should have been noticed elsewhere */ } microseconds_t start; uint32_t nt = 0; samplepos_t tf; { boost::shared_ptr rl = routes.reader(); restart: gint sc = g_atomic_int_get (&_seek_counter); tf = _transport_sample; start = get_microseconds (); for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i, ++nt) { (*i)->non_realtime_locate (tf); if (sc != g_atomic_int_get (&_seek_counter)) { goto restart; } } microseconds_t end = get_microseconds (); int usecs_per_track = lrintf ((end - start) / (double) nt); std::cerr << "locate took " << (end - start) << " usecs for " << nt << " tracks = " << usecs_per_track << " per track\n"; if (usecs_per_track > g_atomic_int_get (¤t_usecs_per_track)) { g_atomic_int_set (¤t_usecs_per_track, usecs_per_track); } } { /* VCAs are quick to locate because they have no data (except automation) associated with them. Don't bother with a restart mechanism here, but do use the same transport sample that the Routes used. */ VCAList v = _vca_manager->vcas (); for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) { (*i)->non_realtime_locate (tf); } } _scene_changer->locate (_transport_sample); /* XXX: it would be nice to generate the new clicks here (in the non-RT thread) rather than clearing them so that the RT thread has to spend time constructing them (in Session::click). */ clear_clicks (); } bool Session::select_playhead_priority_target (samplepos_t& jump_to) { if (!transport_master_no_external_or_using_engine() || !config.get_auto_return()) { return false; } jump_to = _last_roll_location; return jump_to >= 0; } void Session::follow_playhead_priority () { samplepos_t target; if (select_playhead_priority_target (target)) { request_locate (target); } } void Session::non_realtime_stop (bool abort, int on_entry, bool& finished) { struct tm* now; time_t xnow; bool did_record; bool saved; PostTransportWork ptw = post_transport_work(); did_record = false; saved = false; boost::shared_ptr rl = routes.reader(); for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) { boost::shared_ptr tr = boost::dynamic_pointer_cast (*i); if (tr && tr->get_captured_samples () != 0) { did_record = true; break; } } /* stop and locate are merged here because they share a lot of common stuff */ time (&xnow); now = localtime (&xnow); if (auditioner) { auditioner->cancel_audition (); } if (did_record) { begin_reversible_command (Operations::capture); _have_captured = true; } DEBUG_TRACE (DEBUG::Transport, X_("Butler post-transport-work, non realtime stop\n")); if (abort && did_record) { /* no reason to save the session file when we remove sources */ _state_of_the_state = StateOfTheState (_state_of_the_state | InCleanup); } for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) { boost::shared_ptr tr = boost::dynamic_pointer_cast (*i); if (tr) { tr->transport_stopped_wallclock (*now, xnow, abort); } } if (abort && did_record) { _state_of_the_state = StateOfTheState (_state_of_the_state & ~InCleanup); } boost::shared_ptr r = routes.reader (); if (did_record) { commit_reversible_command (); /* increase take name */ if (config.get_track_name_take () && !config.get_take_name ().empty()) { string newname = config.get_take_name(); config.set_take_name(bump_name_number (newname)); } } if (_engine.running()) { PostTransportWork ptw = post_transport_work (); for (RouteList::iterator i = r->begin(); i != r->end(); ++i) { (*i)->non_realtime_transport_stop (_transport_sample, !(ptw & PostTransportLocate)); } VCAList v = _vca_manager->vcas (); for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) { (*i)->non_realtime_transport_stop (_transport_sample, !(ptw & PostTransportLocate)); } } /* If we are not synced to a "true" external master, and we're not * handling an explicit locate, we should consider whether or not to * "auto-return". This could mean going to a specifically requested * location, or just back to the start of the last roll. */ if (transport_master_no_external_or_using_engine() && !locate_initiated()) { bool do_locate = false; if (_requested_return_sample >= 0) { /* explicit return request pre-queued in event list. overrides everything else */ _transport_sample = _requested_return_sample; /* cancel this request */ _requested_return_sample = -1; do_locate = true; } else if (Config->get_auto_return_target_list()) { samplepos_t jump_to; if (select_playhead_priority_target (jump_to)) { /* there's a valid target (we don't care how it * was derived here) */ _transport_sample = jump_to; do_locate = true; } else if (abort) { /* roll aborted (typically capture) with * auto-return enabled */ _transport_sample = _last_roll_location; do_locate = true; } } if (do_locate && synced_to_engine()) { /* We will unconditionally locate to _transport_sample * below, which will refill playback buffers based on * _transport_sample, and maximises the buffering they * represent. * * But if we are synced to engine (JACK), we should * locate the engine (JACK) as well. We would follow * the engine (JACK) on the next process cycle, but * since we're going to do a locate below anyway, * it seems pointless to not use just do it ourselves * right now, rather than wait for the engine (JACK) to * provide the new position on the next cycle. * * Despite the generic name of the called method * (::transport_locate()) this method only does * anything if the audio/MIDI backend is JACK. */ _engine.transport_locate (_transport_sample); } } clear_clicks(); unset_preroll_record_trim (); /* do this before seeking, because otherwise the tracks will do the wrong thing in seamless loop mode. */ if (ptw & (PostTransportClearSubstate|PostTransportStop)) { unset_play_range (); if (!Config->get_loop_is_mode() && get_play_loop() && !loop_changing) { unset_play_loop (); } } /* reset loop_changing so it does not affect next transport action */ loop_changing = false; if (!_transport_fsm->declicking_for_locate()) { DEBUG_TRACE (DEBUG::Transport, X_("Butler PTW: locate\n")); for (RouteList::iterator i = r->begin(); i != r->end(); ++i) { DEBUG_TRACE (DEBUG::Transport, string_compose ("Butler PTW: locate on %1\n", (*i)->name())); (*i)->non_realtime_locate (_transport_sample); if (on_entry != g_atomic_int_get (&_butler->should_do_transport_work)) { finished = false; /* we will be back */ return; } } VCAList v = _vca_manager->vcas (); for (VCAList::const_iterator i = v.begin(); i != v.end(); ++i) { (*i)->non_realtime_locate (_transport_sample); } } have_looped = false; /* don't bother with this stuff if we're disconnected from the engine, because there will be no process callbacks to deliver stuff from */ if (_engine.running() && !_engine.freewheeling()) { // need to queue this in the next RT cycle _send_timecode_update = true; if (transport_master()->type() != MTC) { // why? send_immediate_mmc (MIDI::MachineControlCommand (MIDI::MachineControl::cmdStop)); /* This (::non_realtime_stop()) gets called by main process thread, which will lead to confusion when calling AsyncMIDIPort::write(). Something must be done. XXX */ send_mmc_locate (_transport_sample); } } if ((ptw & PostTransportLocate) && get_record_enabled()) { /* This is scheduled by realtime_stop(), which is also done * when a slave requests /locate/ for an initial sync. * We can't hold up the slave for long with a save() here, * without breaking its initial sync cycle. * * save state only if there's no slave or if it's not yet locked. */ if (!transport_master_is_external() || !transport_master()->locked()) { DEBUG_TRACE (DEBUG::Transport, X_("Butler PTW: requests save\n")); SaveSessionRequested (_current_snapshot_name); saved = true; } } /* save the current state of things if appropriate */ if (did_record && !saved) { SaveSessionRequested (_current_snapshot_name); } PositionChanged (_transport_sample); /* EMIT SIGNAL */ DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC with speed = %1\n", _transport_speed)); TransportStateChange (); /* EMIT SIGNAL */ AutomationWatch::instance().transport_stop_automation_watches (_transport_sample); } void Session::set_play_loop (bool yn, bool change_transport_state) { ENSURE_PROCESS_THREAD; /* Called from event-handling context */ DEBUG_TRACE (DEBUG::Transport, string_compose ("set_play_loop (%1)\n", yn)); Location *loc; if (yn == get_play_loop () || (actively_recording() && yn) || (loc = _locations->auto_loop_location()) == 0) { /* nothing to do, or can't change loop status while recording */ return; } if (yn && synced_to_engine()) { warning << string_compose ( _("Looping cannot be supported while %1 is using JACK transport.\n" "Recommend changing the configured options"), PROGRAM_NAME) << endmsg; return; } if (yn && !maybe_allow_only_loop (true)) { return; } if (yn) { play_loop = true; have_looped = false; unset_play_range (); /* set all tracks to use internal looping */ set_track_loop (true); merge_event (new SessionEvent (SessionEvent::AutoLoop, SessionEvent::Replace, loc->end(), loc->start(), 0.0f)); if (!Config->get_loop_is_mode()) { if (transport_rolling()) { /* set loop_changing to ensure that non_realtime_stop does not unset_play_loop */ loop_changing = true; } /* args: position, disposition, flush=true, for_loop_end=false, force=true */ TFSM_LOCATE (loc->start(), MustRoll, true, false, true); } else { if (!transport_rolling()) { /* loop-is-mode: not rolling, just locate to loop start */ TFSM_LOCATE (loc->start(), MustStop, true, false, true); } } TransportStateChange (); /* EMIT SIGNAL */ } else { unset_play_loop (); } DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC2 with speed = %1\n", _transport_speed)); } void Session::unset_play_loop (bool change_transport_state) { if (!get_play_loop()) { return; } play_loop = false; clear_events (SessionEvent::AutoLoop); set_track_loop (false); /* likely need to flush track buffers: this will locate us to wherever we are */ if (change_transport_state && transport_rolling ()) { TFSM_STOP (false, false); } overwrite_some_buffers (boost::shared_ptr(), LoopDisabled); TransportStateChange (); /* EMIT SIGNAL */ } void Session::set_track_loop (bool yn) { Location* loc = _locations->auto_loop_location (); if (!loc) { yn = false; } boost::shared_ptr rl = routes.reader (); for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) { if (*i && !(*i)->is_private_route()) { (*i)->set_loop (yn ? loc : 0); } } DiskReader::reset_loop_declick (loc, nominal_sample_rate()); } samplecnt_t Session::worst_latency_preroll () const { return _worst_output_latency + _worst_input_latency; } samplecnt_t Session::worst_latency_preroll_buffer_size_ceil () const { return lrintf (ceil ((_worst_output_latency + _worst_input_latency) / (float) current_block_size) * current_block_size); } void Session::unset_play_range () { _play_range = false; _clear_event_type (SessionEvent::RangeStop); _clear_event_type (SessionEvent::RangeLocate); } void Session::set_play_range (list& range, bool leave_rolling) { SessionEvent* ev; /* Called from event-processing context */ unset_play_range (); if (range.empty()) { /* _play_range set to false in unset_play_range() */ if (!leave_rolling) { /* stop transport */ SessionEvent* ev = new SessionEvent (SessionEvent::SetTransportSpeed, SessionEvent::Add, SessionEvent::Immediate, 0, 0.0f, false); merge_event (ev); } return; } _play_range = true; /* cancel loop play */ unset_play_loop (); list::size_type sz = range.size(); if (sz > 1) { list::iterator i = range.begin(); list::iterator next; while (i != range.end()) { next = i; ++next; /* locating/stopping is subject to delays for declicking. */ samplepos_t requested_sample = i->end; if (requested_sample > current_block_size) { requested_sample -= current_block_size; } else { requested_sample = 0; } if (next == range.end()) { ev = new SessionEvent (SessionEvent::RangeStop, SessionEvent::Add, requested_sample, 0, 0.0f); } else { ev = new SessionEvent (SessionEvent::RangeLocate, SessionEvent::Add, requested_sample, (*next).start, 0.0f); } merge_event (ev); i = next; } } else if (sz == 1) { ev = new SessionEvent (SessionEvent::RangeStop, SessionEvent::Add, range.front().end, 0, 0.0f); merge_event (ev); } /* save range so we can do auto-return etc. */ current_audio_range = range; /* now start rolling at the right place */ ev = new SessionEvent (SessionEvent::LocateRoll, SessionEvent::Add, SessionEvent::Immediate, range.front().start, 0.0f, false); merge_event (ev); DEBUG_TRACE (DEBUG::Transport, string_compose ("send TSC5 with speed = %1\n", _transport_speed)); TransportStateChange (); /* EMIT SIGNAL */ } void Session::request_bounded_roll (samplepos_t start, samplepos_t end) { AudioRange ar (start, end, 0); list lar; lar.push_back (ar); request_play_range (&lar, true); } void Session::set_requested_return_sample (samplepos_t return_to) { _requested_return_sample = return_to; } void Session::request_roll_at_and_return (samplepos_t start, samplepos_t return_to) { SessionEvent *ev = new SessionEvent (SessionEvent::LocateRollLocate, SessionEvent::Add, SessionEvent::Immediate, return_to, 1.0); ev->target2_sample = start; queue_event (ev); } void Session::engine_halted () { /* there will be no more calls to process(), so we'd better clean up for ourselves, right now. We can't queue SessionEvents because they only get handled from within a process callback. */ /* this just stops the FSM engine ... it doesn't change the state of * the FSM directly or anything else ... but the FSM will be * reinitialized when we call its ::start() method from * ::engine_running() (if we ever get there) */ _transport_fsm->stop (); /* Synchronously do the realtime part of a transport stop. * * Calling this will cause the butler to asynchronously run * ::non_realtime_stop() where the rest of the "stop" work will be * done. */ realtime_stop (false, true); } void Session::engine_running () { _transport_fsm->start (); reset_xrun_count (); } void Session::xrun_recovery () { ++_xrun_count; Xrun (_transport_sample); /* EMIT SIGNAL */ if (Config->get_stop_recording_on_xrun() && actively_recording()) { /* it didn't actually halt, but we need * to handle things in the same way. */ engine_halted(); /* ..and start the FSM engine again */ _transport_fsm->start (); } } void Session::route_processors_changed (RouteProcessorChange c) { if (g_atomic_int_get (&_ignore_route_processor_changes) > 0) { g_atomic_int_set (&_ignored_a_processor_change, 1); return; } if (c.type == RouteProcessorChange::MeterPointChange) { set_dirty (); return; } if (c.type == RouteProcessorChange::RealTimeChange) { set_dirty (); return; } resort_routes (); update_latency_compensation (false, false); set_dirty (); } void Session::allow_auto_play (bool yn) { auto_play_legal = yn; } void Session::send_mmc_locate (samplepos_t t) { if (t < 0) { return; } if (!_engine.freewheeling()) { Timecode::Time time; timecode_time_subframes (t, time); send_immediate_mmc (MIDI::MachineControlCommand (time)); } } /** Ask the transport to not send timecode until further notice. The suspension * will come into effect some finite time after this call, and timecode_transmission_suspended() * should be checked by the caller to find out when. */ void Session::request_suspend_timecode_transmission () { SessionEvent* ev = new SessionEvent (SessionEvent::SetTimecodeTransmission, SessionEvent::Add, SessionEvent::Immediate, 0, 0, false); queue_event (ev); } void Session::request_resume_timecode_transmission () { SessionEvent* ev = new SessionEvent (SessionEvent::SetTimecodeTransmission, SessionEvent::Add, SessionEvent::Immediate, 0, 0, true); queue_event (ev); } bool Session::timecode_transmission_suspended () const { return g_atomic_int_get (&_suspend_timecode_transmission) == 1; } boost::shared_ptr 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 master = TransportMasterManager::instance().current(); if (master->can_loop()) { request_play_loop (false); } else if (master->has_loop()) { request_play_loop (true); } /* slave change, reset any DiskIO block on disk output because it is no longer valid with a new slave. */ TransportMasterManager::instance().unblock_disk_output (); #if 0 we should not be treating specific transport masters as special cases because there maybe > 1 of a particular type boost::shared_ptr mtc_master = boost::dynamic_pointer_cast (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_master = boost::dynamic_pointer_cast (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 rl = routes.reader(); const bool externally_slaved = transport_master_is_external(); for (RouteList::iterator i = rl->begin(); i != rl->end(); ++i) { boost::shared_ptr tr = boost::dynamic_pointer_cast (*i); if (tr && !tr->is_private_route()) { tr->set_slaved (externally_slaved); } } set_dirty(); } bool Session::transport_stopped() const { return _transport_fsm->stopped(); } bool Session::transport_stopped_or_stopping() const { return _transport_fsm->stopped() || _transport_fsm->stopping(); } bool Session::transport_rolling() const { return _transport_speed != 0.0 && _count_in_samples == 0 && _remaining_latency_preroll == 0; } bool Session::locate_pending () const { return _transport_fsm->locating(); } bool Session::locate_initiated() const { return _transport_fsm->declicking_for_locate() || _transport_fsm->locating(); } bool Session::declick_in_progress () const { return _transport_fsm->declick_in_progress(); } bool Session::transport_will_roll_forwards () const { return _transport_fsm->will_roll_fowards (); }