Components used for the execution of a score. More...
Detailed Description
Components used for the execution of a score.
Execution state-machine explanation:
- There are multiple sources for what controls the transport:
- From the GUI
- From the local tree or remote control plug-in
- From an external software (e.g. JACK transport)
- Automatically (e.g. –autoplay argument on command-line)
- No matter the transport source, if there's a GUI its state must be consistent with what happens.
Thus:
- The do_*** functions are requests
- The trigger_*** functions will perform the action and set the GUI state correctly
- The on_*** contain the actual implementation of the transport operation
Notes on QAction:
- QAction::toggle(); -> will send toggled(true/false);
- QAction::trigger(); -> will send toggled(true/false); triggered(true/false);
- QAction::check(b); -> will send toggled(b);
- GUI: like QAction::trigger
Thus, triggered can be used to differentiate between
- called from the GUI
- called from the software
Example of call sequence:
- Pressing the "global play button" (stop -> play): -> QAction::toggled(true) -> GUI changes (see TransportActions ctor) -> QAction::triggered(true) -> ApplicationPlugin::request_play_global(true) -> TransportInterface::requestPlay() -> the transport implementation eventually sends the "play()" signal -> &ApplicationPlugin::trigger_play_global
- Pressing the "global play button" (play -> pause): -> QAction::toggled(false) -> GUI changes (see TransportActions ctor) -> QAction::triggered(false) -> ApplicationPlugin::request_play_global(false) -> TransportInterface::requestPause() -> the transport implementation eventually sends the "pause()" signal -> &ApplicationPlugin::trigger_pause()
- Playing with "play from here": -> ApplicationPlugin::request_play(true, t) -> TransportInterface::requestTransport(t) -> TransportInterface::requestPlay -> The transport implementation eventually sends the "transport()" signal -> ?? -> The transport implementation eventually sends the "play()" signal -> &ApplicationPlugin::trigger_play_global
- Starting the transport from QJackCtl with a GUI -> the transport implementation eventually sends the "play()" signal -> &ApplicationPlugin::trigger_play_global
- Starting the transport from QJackCtl without a GUI -> the transport implementation eventually sends the "play()" signal -> &ApplicationPlugin::trigger_play_global
- Receiving a "/play" message to the local tree with a GUI -> ApplicationPlugin::request_play -> TransportInterface::requestPlay -> The transport implementation eventually sends the "play()" signal -> &ApplicationPlugin::trigger_play_global
- Receiving a "/play" message to the local tree without a GUI
This hierarchy of types is used to create the OSSIA structures for the execution behind score::Components.
Currently, all the execution structures are recreated from scratch when doing "play".
The classes inheriting from Execution::ProcessComponent are first created, then the Execution::IntervalComponent will ask them for their ossia::time_process which it gives to the matching ossia::time_interval.
Live modification during execution.
The execution engine allows live modification of scores. Since the execution happens in a different thread than edition, we have to be extremely careful however.
Instead of locking all the data structures of the OSSIA API with mutex, which may slow down the execution, we instead have a lock-free queue of edition commands.
Modifications are submitted from the component hierarchy :
- Execution::ScenarioComponent
- Execution::IntervalComponent
- etc...
To the OSSIA structures :
- ossia::time_process
- ossia::time_interval
- etc...
A modification follows this pattern :
For modification of values, for instance the duration of a Interval, this is easily visible. See for instance Execution::IntervalComponentBase 's constructor.
For creation and removal of objects, this should be handled automatically by the various ComponentHierarchy classes which take care of creating and removing the objects in the correct order. The Component classes just have to provide functions that will do the actual instantiation, and pre- & post- removal steps.
The actual "root" execution algorithm is given in Execution::DefaulClock::makeDefaultCallback
Execution Thread Safety
One must take care when modifying the Execution classes, since thins happen on two different threads.
The biggest problem is that the score structures could be created and deleted in a single tick. For instance when doing a complete undo - redo of the whole undo stack.
This means that anything send to the command queue must absolutely never access any of the score structures (for instance Scenario::IntervalModel, etc) directly : they have to be copied. Else, there will be crashes, someday.
In the flow graph shown before, everything up to and including "Command inserted into the execution queue" happens in the GUI thread, hence one can rely on everything "being here" at this point. However, in the actual commands, the only things safe to use are :
- Copies of data : simple values, ints, etc, are safe.
- Shared pointers : unlike most other places in score, the Execution components are not owned by their parents, but through shared pointers. This means that shall the component be removed, if the pointer was copied in the ExecutionCommand, there is no risk of crash. But one must take care of copying the actual
shared_ptr
and not just thethis
pointer for instance. Multiple classes inherit fromstd::enable_shared_from_this
to allow ashared_from_this()
function that gives back ashared_ptr
to the this instance.
Typedefs | |
using | ExecutionCommand = smallfun::function< void(), 128, std::max((int) 8,(int) std::max(alignof(std::function< void()>), alignof(double))), smallfun::Methods::Move > |
using | GCCommand = smallfun::function< void(), 128+4 *8, std::max((int) 8,(int) std::max(alignof(std::function< void()>), alignof(double))), smallfun::Methods::Move > |
using | Component = score::GenericComponent< const Context > |
using | time_function = smallfun::function< ossia::time_value(const TimeVal &)> |
Sets-up and manages the main execution clock. More... | |
using | reverse_time_function = smallfun::function< TimeVal(const ossia::time_value &)> |
using | ExecutionCommandQueue = ossia::spsc_queue< ExecutionCommand, 1024 > |
using | EditionCommandQueue = moodycamel::ConcurrentQueue< ExecutionCommand > |
using | GCCommandQueue = moodycamel::ConcurrentQueue< GCCommand > |
using | TransportInterface = Transport::TransportInterface |
using | exec_setup_fun = std::function< void(const Execution::Context &, Execution::BaseScenarioElement &)> |
using | tick_fun = ossia::audio_engine::fun_type |
using | FaustEffectComponentFactory = Execution::ProcessComponentFactory_T< FaustEffectComponent > |
using | JitEffectComponentFactory = Execution::ProcessComponentFactory_T< JitEffectComponent > |
using | MergerComponentFactory = ::Execution::ProcessComponentFactory_T< MergerComponent > |
using | MetroComponentFactory = ::Execution::ProcessComponentFactory_T< MetroComponent > |
using | SoundComponentFactory = ::Execution::ProcessComponentFactory_T< SoundComponent > |
using | StepComponentFactory = ::Execution::ProcessComponentFactory_T< StepComponent > |
using | ScenarioComponentHierarchy = HierarchicalScenarioComponent< ScenarioComponentBase, Scenario::ProcessModel, IntervalComponent, EventComponent, TimeSyncComponent, StateComponent, false > |
using | ScenarioComponentFactory = Execution::ProcessComponentFactory_T< ScenarioComponent > |
Functions | |
ossia::net::node_base * | findNode (const ossia::execution_state &st, const State::Address &addr) |
std::optional< ossia::destination > | makeDestination (const ossia::execution_state &devices, const State::AddressAccessor &addr) |
template<typename T , typename Impl > | |
void | set_destination_impl (const Context &plug, const State::AddressAccessor &address, const T &port, Impl &&append) |
template<typename T > | |
constexpr auto | gc (T &&t) noexcept |
Audio::tick_fun | makeExecutionTick (ossia::tick_setup_options opt, Execution::DocumentPlugin &plug, const std::shared_ptr< Execution::BaseScenarioElement > &scenar) |
Audio::tick_fun | makeBenchmarkTick (ossia::tick_setup_options opt, Execution::DocumentPlugin &plug, const std::shared_ptr< Execution::BaseScenarioElement > &scenar) |
std::pair< std::optional< ossia::tempo_curve >, Scenario::TempoProcess * > | tempoCurve (const Scenario::IntervalModel &itv, const Execution::Context &ctx) |
ossia::time_signature_map | timeSignatureMap (const Scenario::IntervalModel &itv, const Execution::Context &ctx) |
auto | propagatedOutlets (const Process::Outlets &outlets) noexcept |
void | connectPropagated (const ossia::node_ptr &process_node, const ossia::node_ptr &interval_node, ossia::graph_interface &g, const ossia::pod_vector< std::size_t > &propagated_outlets) noexcept |
void | updatePropagated (const ossia::node_ptr &process_node, const ossia::node_ptr &interval_node, ossia::graph_interface &g, std::size_t port_idx, bool is_propagated) noexcept |
template<> | |
IntervalComponent * | ScenarioComponentBase::make< IntervalComponent, Scenario::IntervalModel > (Scenario::IntervalModel &cst) |
template<> | |
StateComponent * | ScenarioComponentBase::make< StateComponent, Scenario::StateModel > (Scenario::StateModel &st) |
template<> | |
EventComponent * | ScenarioComponentBase::make< EventComponent, Scenario::EventModel > (Scenario::EventModel &ev) |
template<> | |
TimeSyncComponent * | ScenarioComponentBase::make< TimeSyncComponent, Scenario::TimeSyncModel > (Scenario::TimeSyncModel &tn) |
Typedef Documentation
◆ time_function
typedef smallfun::function< ossia::time_value(const TimeVal &)> Execution::time_function |
Sets-up and manages the main execution clock.
This class allows to create control mechanisms for the score execution clock.
The default implementation, DefaultClock, uses sleep() and threading to do the execution.
Other implementations could be used to synchronize to external clocks, such as the sound card clock, or the host clock if score is used as a plug-in.
The derived constructors should set-up the clock drive mode, speed, etc.