/*! @file Link.hpp * @copyright 2016, Ableton AG, Berlin. All rights reserved. * @brief Library for cross-device shared tempo and quantized beat grid * * @license: * 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, see . * * If you would like to incorporate Link into a proprietary software application, * please contact . */ #pragma once #include #include #include namespace ableton { /*! @class Link and BasicLink * @brief Classes representing a participant in a Link session. * The BasicLink type allows to customize the clock. The Link type * uses the recommended platform-dependent representation of the * system clock as defined in platforms/Config.hpp. * It's preferred to use Link instead of BasicLink. * * @discussion Each Link instance has its own session state which * represents a beat timeline and a transport start/stop state. The * timeline starts running from beat 0 at the initial tempo when * constructed. The timeline always advances at a speed defined by * its current tempo, even if transport is stopped. Synchronizing to the * transport start/stop state of Link is optional for every peer. * The transport start/stop state is only shared with other peers when * start/stop synchronization is enabled. * * A Link instance is initially disabled after construction, which * means that it will not communicate on the network. Once enabled, * a Link instance initiates network communication in an effort to * discover other peers. When peers are discovered, they immediately * become part of a shared Link session. * * Each method of the Link type documents its thread-safety and * realtime-safety properties. When a method is marked thread-safe, * it means it is safe to call from multiple threads * concurrently. When a method is marked realtime-safe, it means that * it does not block and is appropriate for use in the thread that * performs audio IO. * * Link provides one session state capture/commit method pair for use * in the audio thread and one for all other application contexts. In * general, modifying the session state should be done in the audio * thread for the most accurate timing results. The ability to modify * the session state from application threads should only be used in * cases where an application's audio thread is not actively running * or if it doesn't generate audio at all. Modifying the Link session * state from both the audio thread and an application thread * concurrently is not advised and will potentially lead to unexpected * behavior. * * Only use the BasicLink class if the default platform clock does not * fulfill other requirements of the client application. Please note this * will require providing a custom Clock implementation. See the clock() * documentation for details. */ template class BasicLink { public: class SessionState; /*! @brief Construct with an initial tempo. */ BasicLink(double bpm); /*! @brief Link instances cannot be copied or moved */ BasicLink(const BasicLink&) = delete; BasicLink& operator=(const BasicLink&) = delete; BasicLink(BasicLink&&) = delete; BasicLink& operator=(BasicLink&&) = delete; /*! @brief Is Link currently enabled? * Thread-safe: yes * Realtime-safe: yes */ bool isEnabled() const; /*! @brief Enable/disable Link. * Thread-safe: yes * Realtime-safe: no */ void enable(bool bEnable); /*! @brief: Is start/stop synchronization enabled? * Thread-safe: yes * Realtime-safe: no */ bool isStartStopSyncEnabled() const; /*! @brief: Enable start/stop synchronization. * Thread-safe: yes * Realtime-safe: no */ void enableStartStopSync(bool bEnable); /*! @brief How many peers are currently connected in a Link session? * Thread-safe: yes * Realtime-safe: yes */ std::size_t numPeers() const; /*! @brief Register a callback to be notified when the number of * peers in the Link session changes. * Thread-safe: yes * Realtime-safe: no * * @discussion The callback is invoked on a Link-managed thread. * * @param callback The callback signature is: * void (std::size_t numPeers) */ template void setNumPeersCallback(Callback callback); /*! @brief Register a callback to be notified when the session * tempo changes. * Thread-safe: yes * Realtime-safe: no * * @discussion The callback is invoked on a Link-managed thread. * * @param callback The callback signature is: void (double bpm) */ template void setTempoCallback(Callback callback); /*! brief: Register a callback to be notified when the state of * start/stop isPlaying changes. * Thread-safe: yes * Realtime-safe: no * * @discussion The callback is invoked on a Link-managed thread. * * @param callback The callback signature is: * void (bool isPlaying) */ template void setStartStopCallback(Callback callback); /*! @brief The clock used by Link. * Thread-safe: yes * Realtime-safe: yes * * @discussion The Clock type is a platform-dependent representation * of the system clock. It exposes a micros() method, which is a * normalized representation of the current system time in * std::chrono::microseconds. */ Clock clock() const; /*! @brief Capture the current Link Session State from the audio thread. * Thread-safe: no * Realtime-safe: yes * * @discussion This method should ONLY be called in the audio thread * and must not be accessed from any other threads. The returned * object stores a snapshot of the current Link Session State, so it * should be captured and used in a local scope. Storing the * Session State for later use in a different context is not advised * because it will provide an outdated view. */ SessionState captureAudioSessionState() const; /*! @brief Commit the given Session State to the Link session from the * audio thread. * Thread-safe: no * Realtime-safe: yes * * @discussion This method should ONLY be called in the audio * thread. The given Session State will replace the current Link * state. Modifications will be communicated to other peers in the * session. */ void commitAudioSessionState(SessionState state); /*! @brief Capture the current Link Session State from an application * thread. * Thread-safe: yes * Realtime-safe: no * * @discussion Provides a mechanism for capturing the Link Session * State from an application thread (other than the audio thread). * The returned Session State stores a snapshot of the current Link * state, so it should be captured and used in a local scope. * Storing the it for later use in a different context is not * advised because it will provide an outdated view. */ SessionState captureAppSessionState() const; /*! @brief Commit the given Session State to the Link session from an * application thread. * Thread-safe: yes * Realtime-safe: no * * @discussion The given Session State will replace the current Link * Session State. Modifications of the Session State will be * communicated to other peers in the session. */ void commitAppSessionState(SessionState state); /*! @class SessionState * @brief Representation of a timeline and the start/stop state * * @discussion A SessionState object is intended for use in a local scope within * a single thread - none of its methods are thread-safe. All of its methods are * non-blocking, so it is safe to use from a realtime thread. * It provides functions to observe and manipulate the timeline and start/stop * state. * * The timeline is a representation of a mapping between time and beats for varying * quanta. * The start/stop state represents the user intention to start or stop transport at * a specific time. Start stop synchronization is an optional feature that allows to * share the user request to start or stop transport between a subgroup of peers in * a Link session. When observing a change of start/stop state, audio playback of a * peer should be started or stopped the same way it would have happened if the user * had requested that change at the according time locally. The start/stop state can * only be changed by the user. This means that the current local start/stop state * persists when joining or leaving a Link session. After joining a Link session * start/stop change requests will be communicated to all connected peers. */ class SessionState { public: SessionState(const link::ApiState state, const bool bRespectQuantum); /*! @brief: The tempo of the timeline, in Beats Per Minute. * * @discussion This is a stable value that is appropriate for display * to the user. Beat time progress will not necessarily match this tempo * exactly because of clock drift compensation. */ double tempo() const; /*! @brief: Set the timeline tempo to the given bpm value, taking * effect at the given time. */ void setTempo(double bpm, std::chrono::microseconds atTime); /*! @brief: Get the beat value corresponding to the given time * for the given quantum. * * @discussion: The magnitude of the resulting beat value is * unique to this Link instance, but its phase with respect to * the provided quantum is shared among all session * peers. For non-negative beat values, the following * property holds: fmod(beatAtTime(t, q), q) == phaseAtTime(t, q) */ double beatAtTime(std::chrono::microseconds time, double quantum) const; /*! @brief: Get the session phase at the given time for the given * quantum. * * @discussion: The result is in the interval [0, quantum). The * result is equivalent to fmod(beatAtTime(t, q), q) for * non-negative beat values. This method is convenient if the * client is only interested in the phase and not the beat * magnitude. Also, unlike fmod, it handles negative beat values * correctly. */ double phaseAtTime(std::chrono::microseconds time, double quantum) const; /*! @brief: Get the time at which the given beat occurs for the * given quantum. * * @discussion: The inverse of beatAtTime, assuming a constant * tempo. beatAtTime(timeAtBeat(b, q), q) === b. */ std::chrono::microseconds timeAtBeat(double beat, double quantum) const; /*! @brief: Attempt to map the given beat to the given time in the * context of the given quantum. * * @discussion: This method behaves differently depending on the * state of the session. If no other peers are connected, * then this instance is in a session by itself and is free to * re-map the beat/time relationship whenever it pleases. In this * case, beatAtTime(time, quantum) == beat after this method has * been called. * * If there are other peers in the session, this instance * should not abruptly re-map the beat/time relationship in the * session because that would lead to beat discontinuities among * the other peers. In this case, the given beat will be mapped * to the next time value greater than the given time with the * same phase as the given beat. * * This method is specifically designed to enable the concept of * "quantized launch" in client applications. If there are no other * peers in the session, then an event (such as starting * transport) happens immediately when it is requested. If there * are other peers, however, we wait until the next time at which * the session phase matches the phase of the event, thereby * executing the event in-phase with the other peers in the * session. The client only needs to invoke this method to * achieve this behavior and should not need to explicitly check * the number of peers. */ void requestBeatAtTime(double beat, std::chrono::microseconds time, double quantum); /*! @brief: Rudely re-map the beat/time relationship for all peers * in a session. * * @discussion: DANGER: This method should only be needed in * certain special circumstances. Most applications should not * use it. It is very similar to requestBeatAtTime except that it * does not fall back to the quantizing behavior when it is in a * session with other peers. Calling this method will * unconditionally map the given beat to the given time and * broadcast the result to the session. This is very anti-social * behavior and should be avoided. * * One of the few legitimate uses of this method is to * synchronize a Link session with an external clock source. By * periodically forcing the beat/time mapping according to an * external clock source, a peer can effectively bridge that * clock into a Link session. Much care must be taken at the * application layer when implementing such a feature so that * users do not accidentally disrupt Link sessions that they may * join. */ void forceBeatAtTime(double beat, std::chrono::microseconds time, double quantum); /*! @brief: Set if transport should be playing or stopped, taking effect * at the given time. */ void setIsPlaying(bool isPlaying, std::chrono::microseconds time); /*! @brief: Is transport playing? */ bool isPlaying() const; /*! @brief: Get the time at which a transport start/stop occurs */ std::chrono::microseconds timeForIsPlaying() const; /*! @brief: Convenience function to attempt to map the given beat to the time * when transport is starting to play in context of the given quantum. * This function evaluates to a no-op if isPlaying() equals false. */ void requestBeatAtStartPlayingTime(double beat, double quantum); /*! @brief: Convenience function to start or stop transport at a given time and * attempt to map the given beat to this time in context of the given quantum. */ void setIsPlayingAndRequestBeatAtTime( bool isPlaying, std::chrono::microseconds time, double beat, double quantum); private: friend BasicLink; link::ApiState mOriginalState; link::ApiState mState; bool mbRespectQuantum; }; private: using Controller = ableton::link::Controller; std::mutex mCallbackMutex; link::PeerCountCallback mPeerCountCallback = [](std::size_t) {}; link::TempoCallback mTempoCallback = [](link::Tempo) {}; link::StartStopStateCallback mStartStopCallback = [](bool) {}; Clock mClock; Controller mController; }; class Link : public BasicLink { public: using Clock = link::platform::Clock; Link(double bpm) : BasicLink(bpm) { } }; } // namespace ableton #include