/* * Copyright (C) 2019 Robin Gareus * Copyright (C) 2019 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include "pbd/error.h" #include "pbd/i18n.h" #include "pbd/pthread_utils.h" #include "pbd/stacktrace.h" #include "ardour/debug.h" #include "ardour/session.h" #include "ardour/transport_fsm.h" using namespace ARDOUR; using namespace PBD; Pool* TransportFSM::Event::pool = 0; void TransportFSM::Event::init_pool () { pool = new Pool (X_("Events"), sizeof (Event), 128); } void* TransportFSM::Event::operator new (size_t) { return pool->alloc(); } void TransportFSM::Event::operator delete (void *ptr, size_t /*size*/) { return pool->release (ptr); } TransportFSM::TransportFSM (TransportAPI& tapi) : _last_locate (Locate, 0, MustRoll, false, false, false) /* all but first argument don't matter */ , api (&tapi) , processing (0) { init (); } void TransportFSM::init () { _motion_state = Stopped; _butler_state = NotWaitingForButler; _last_locate.target = max_samplepos; } void TransportFSM::process_events () { processing++; while (!queued_events.empty()) { MotionState oms = _motion_state; ButlerState obs = _butler_state; Event* ev = &queued_events.front(); bool deferred; /* must remove from the queued_events list now, because * process_event() may defer the event. This will lead to * insertion into the deferred_events list, and its not possible * with intrusive lists to be present in two lists at once * (without additional hooks). */ queued_events.pop_front (); if (process_event (*ev, false, deferred)) { /* event processed successfully */ if (oms != _motion_state || obs != _butler_state) { /* state changed, so now check deferred events * to see if they can be processed now */ if (!deferred_events.empty() ){ DEBUG_TRACE (DEBUG::TFSMEvents, string_compose ("processing %1 deferred events\n", deferred_events.size())); for (EventList::iterator e = deferred_events.begin(); e != deferred_events.end(); ) { Event* deferred_ev = &(*e); bool deferred2; if (process_event (*e, true, deferred2)) { /* event processed, remove from deferred */ e = deferred_events.erase (e); delete deferred_ev; } else { ++e; } } } } } if (!deferred) { delete ev; } } processing--; } /* This is the transition table from the original boost::msm * implementation of this FSM. It is more easily readable and * consultable. Please keep it updated as the FSM changes. * * Here's a hint about how to read each line of this table: * * "if the current state is Start and event Event arrives, new state is Next and we execute Action()" * * with a variant: * * "if the current state is Start and event Event arrives, new state is Next and we execute Action() ***IF*** Guard() returns true" * * This new implementation, however, does not use metaprogramming to achieve all this, * but just uses a large-ish switch() block. * */ /* Start Event Next Action Guard +----------------------+----------------+------------------+---------------------+---------------------------------+ a_row < Stopped, start_transport, Rolling, &T::start_playback >, _row < Stopped, stop_transport, Stopped >, a_row < Stopped, locate, WaitingForLocate, &T::start_locate_while_stopped >, g_row < WaitingForLocate, locate_done, Stopped, &T::should_not_roll_after_locate >, _row < Rolling, butler_done, Rolling >, _row < Rolling, start_transport, Rolling >, a_row < Rolling, stop_transport, DeclickToStop, &T::stop_playback >, a_row < DeclickToStop, declick_done, Stopped, >, a_row < DeclickToStop, stop_transport, DeclickToStop >, a_row < Rolling, locate, DeclickToLocate, &T::start_declick_for_locate >, a_row < DeclickToLocate, declick_done, WaitingForLocate, &T::start_locate_after_declick >, row < WaitingForLocate, locate_done, Rolling, &T::roll_after_locate, &T::should_roll_after_locate >, a_row < NotWaitingForButler, butler_required, WaitingForButler, &T::schedule_butler_for_transport_work >, a_row < WaitingForButler, butler_required, WaitingForButler, &T::schedule_butler_for_transport_work >, _row < WaitingForButler, butler_done, NotWaitingForButler >, a_row < WaitingForLocate, locate, WaitingForLocate, &T::interrupt_locate >, a_row < DeclickToLocate, locate, DeclickToLocate, &T::interrupt_locate >, // Deferrals #define defer(start_state,ev) boost::msm::front::Row defer (DeclickToLocate, start_transport), defer (DeclickToLocate, stop_transport), defer (DeclickToStop, start_transport), defer (WaitingForLocate, start_transport), defer (WaitingForLocate, stop_transport) #undef defer */ std::string TransportFSM::current_state () const { std::stringstream s; s << enum_2_string (_motion_state) << '/' << enum_2_string (_butler_state); return s.str(); } void TransportFSM::bad_transition (Event const & ev) { error << "bad transition, current state = " << current_state() << " event = " << enum_2_string (ev.type) << endmsg; std::cerr << "bad transition, current state = " << current_state() << " event = " << enum_2_string (ev.type) << std::endl; PBD::stacktrace (std::cerr, 30); } bool TransportFSM::process_event (Event& ev, bool already_deferred, bool& deferred) { DEBUG_TRACE (DEBUG::TFSMEvents, string_compose ("process %1\n", enum_2_string (ev.type))); deferred = false; switch (ev.type) { case StartTransport: switch (_motion_state) { case Stopped: transition (Rolling); start_playback (); break; case Rolling: break; case DeclickToLocate: case WaitingForLocate: if (!already_deferred) { defer (ev); deferred = true; } break; case DeclickToStop: if (!already_deferred) { defer (ev); deferred = true; } break; default: bad_transition (ev); return false; break; } break; case StopTransport: switch (_motion_state) { case Rolling: transition (DeclickToStop); stop_playback (ev); break; case Stopped: break; case DeclickToLocate: case WaitingForLocate: if (!already_deferred) { defer (ev); deferred = true; } break; case DeclickToStop: /* already doing it */ break; default: bad_transition (ev); return false; break; } break; case Locate: DEBUG_TRACE (DEBUG::TFSMEvents, string_compose ("locate, ltd = %1 flush = %2 target = %3 loop %4 force %5\n", enum_2_string (ev.ltd), ev.with_flush, ev.target, ev.for_loop_end, ev.force)); switch (_motion_state) { case Stopped: transition (WaitingForLocate); start_locate_while_stopped (ev); break; case Rolling: if (ev.for_loop_end) { /* we will finish the locate synchronously, so * that after returning from * ::locate_for_loop() we will already have * received (and re-entrantly handled) * LocateDone and returned back to Rolling. * * This happens because we only need to do a * realtime locate and continue rolling. No * disk I/O is required - the loop is * automically present in buffers already. * * Note that ev.ltd is ignored and * assumed to be true because we're looping. */ transition (WaitingForLocate); locate_for_loop (ev); } else { transition (DeclickToLocate); start_declick_for_locate (ev); } break; case WaitingForLocate: case DeclickToLocate: interrupt_locate (ev); break; default: bad_transition (ev); return false; } break; case LocateDone: switch (_motion_state) { case WaitingForLocate: if (should_not_roll_after_locate()) { transition (Stopped); /* already stopped, nothing to do */ } else { transition (Rolling); roll_after_locate (); } break; default: bad_transition (ev); return false; } break; case DeclickDone: switch (_motion_state) { case DeclickToLocate: transition (WaitingForLocate); start_locate_after_declick (); break; case DeclickToStop: transition (Stopped); /* transport already stopped */ break; default: bad_transition (ev); return false; } break; case ButlerRequired: switch (_butler_state) { case NotWaitingForButler: transition (WaitingForButler); schedule_butler_for_transport_work (); break; case WaitingForButler: schedule_butler_for_transport_work (); break; default: bad_transition (ev); return false; } break; case ButlerDone: switch (_butler_state) { case WaitingForButler: transition (NotWaitingForButler); break; default: bad_transition (ev); return false; } break; } return true; } /* transition actions */ void TransportFSM::start_playback () { DEBUG_TRACE (DEBUG::TFSMEvents, "start_playback\n"); _last_locate.target = max_samplepos; current_roll_after_locate_status = boost::none; api->start_transport(); } void TransportFSM::stop_playback (Event const & s) { DEBUG_TRACE (DEBUG::TFSMEvents, "stop_playback\n"); _last_locate.target = max_samplepos; current_roll_after_locate_status = boost::none; api->stop_transport (s.abort, s.clear_state); } void TransportFSM::set_roll_after (bool with_roll) const { current_roll_after_locate_status = with_roll; } void TransportFSM::start_declick_for_locate (Event const & l) { assert (l.type == Locate); DEBUG_TRACE (DEBUG::TFSMEvents, string_compose ("start_declick_for_locate, crals %1 ltd %2 speed %3 sral %4\n", (bool) current_roll_after_locate_status, enum_2_string (l.ltd), api->speed(), api->should_roll_after_locate())); _last_locate = l; if (!current_roll_after_locate_status) { set_roll_after (compute_should_roll (l.ltd)); } api->stop_transport (false, false); } void TransportFSM::start_locate_while_stopped (Event const & l) const { assert (l.type == Locate); DEBUG_TRACE (DEBUG::TFSMEvents, "start_locate_while_stopped\n"); set_roll_after (compute_should_roll (l.ltd)); api->locate (l.target, current_roll_after_locate_status.get(), l.with_flush, l.for_loop_end, l.force); } bool TransportFSM::compute_should_roll (LocateTransportDisposition ltd) const { switch (ltd) { case MustRoll: return true; case MustStop: return false; case RollIfAppropriate: /* by the time we call this, if we were rolling before the locate, we've already transitioned into DeclickToLocate */ if (_motion_state == DeclickToLocate) { return true; } else { return api->should_roll_after_locate (); } break; } /*NOTREACHED*/ return true; } void TransportFSM::locate_for_loop (Event const & l) { assert (l.type == Locate); DEBUG_TRACE (DEBUG::TFSMEvents, string_compose ("locate_for_loop, wl = %1\n", l.for_loop_end)); const bool should_roll = compute_should_roll (l.ltd); current_roll_after_locate_status = should_roll; _last_locate = l; api->locate (l.target, should_roll, l.with_flush, l.for_loop_end, l.force); } void TransportFSM::start_locate_after_declick () const { DEBUG_TRACE (DEBUG::TFSMEvents, string_compose ("start_locate_after_declick, have crals ? %1 roll will be %2\n", (bool) current_roll_after_locate_status, current_roll_after_locate_status ? current_roll_after_locate_status.get() : compute_should_roll (_last_locate.ltd))); const bool roll = current_roll_after_locate_status ? current_roll_after_locate_status.get() : compute_should_roll (_last_locate.ltd); api->locate (_last_locate.target, roll, _last_locate.with_flush, _last_locate.for_loop_end, _last_locate.force); } void TransportFSM::interrupt_locate (Event const & l) const { assert (l.type == Locate); DEBUG_TRACE (DEBUG::TFSMEvents, string_compose ("interrupt to %1 versus %2\n", l.target, _last_locate.target)); /* Because of snapping (e.g. of mouse position) we could be * interrupting an existing locate to the same position. If we go ahead * with this, the code in Session::do_locate() will notice that it's a * repeat position, will do nothing, will queue a "locate_done" event * that will arrive in the next process cycle. But this event may be * processed before the original (real) locate has completed in the * butler thread, and processing it may transition us back to Rolling * before some (or even all) tracks are actually ready. * * So, we must avoid this from happening, and this seems like the * simplest way. */ if (l.target == _last_locate.target && !l.force) { return; } /* maintain original "with-roll" choice of initial locate, even though * we are interrupting the locate to start a new one. */ api->locate (l.target, false, l.with_flush, l.for_loop_end, l.force); } void TransportFSM::schedule_butler_for_transport_work () const { api->schedule_butler_for_transport_work (); } bool TransportFSM::should_roll_after_locate () const { bool roll; if (current_roll_after_locate_status) { roll = current_roll_after_locate_status.get(); current_roll_after_locate_status = boost::none; // used it } else { roll = api->should_roll_after_locate (); } DEBUG_TRACE (DEBUG::TFSMEvents, string_compose ("should_roll_after_locate() ? %1\n", roll)); return roll; } void TransportFSM::roll_after_locate () const { DEBUG_TRACE (DEBUG::TFSMEvents, string_compose ("rolling after locate, was for_loop ? %1\n", _last_locate.for_loop_end)); current_roll_after_locate_status = boost::none; api->start_transport (); } void TransportFSM::defer (Event& ev) { DEBUG_TRACE (DEBUG::TFSMEvents, string_compose ("Defer %1 during %2\n", enum_2_string (ev.type), current_state())); deferred_events.push_back (ev); } void TransportFSM::transition (MotionState ms) { const MotionState old = _motion_state; _motion_state = ms; DEBUG_TRACE (DEBUG::TFSMState, string_compose ("Leave %1, enter %2\n", enum_2_string (old), current_state())); } void TransportFSM::transition (ButlerState bs) { const ButlerState old = _butler_state; _butler_state = bs; DEBUG_TRACE (DEBUG::TFSMState, string_compose ("Leave %1, enter %2\n", enum_2_string (old), current_state())); } void TransportFSM::enqueue (Event* ev) { DEBUG_TRACE (DEBUG::TFSMState, string_compose ("queue tfsm event %1\n", enum_2_string (ev->type))); queued_events.push_back (*ev); if (!processing) { process_events (); } }