| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,306 @@ | ||
| /*! @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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/platforms/Config.hpp> | ||
| #include <chrono> | ||
| #include <mutex> | ||
|
|
||
| namespace ableton | ||
| { | ||
|
|
||
| /*! @class Link | ||
| * @brief Class that represents a participant in a Link session. | ||
| * | ||
| * @discussion Each Link instance has its own beat timeline that | ||
| * starts running from beat 0 at the initial tempo when | ||
| * constructed. 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 Timeline capture/commit method pair for use in the | ||
| * audio thread and one for all other application contexts. In | ||
| * general, modifying the Link timeline should be done in the audio | ||
| * thread for the most accurate timing results. The ability to modify | ||
| * the Link timeline 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 | ||
| * timeline from both the audio thread and an application thread | ||
| * concurrently is not advised and will potentially lead to | ||
| * unexpected behavior. | ||
| */ | ||
| class Link | ||
| { | ||
| public: | ||
| using Clock = link::platform::Clock; | ||
| class Timeline; | ||
|
|
||
| /*! @brief Construct with an initial tempo. */ | ||
| Link(double bpm); | ||
|
|
||
| /*! @brief Link instances cannot be copied or moved */ | ||
| Link(const Link&) = delete; | ||
| Link& operator=(const Link&) = delete; | ||
| Link(Link&&) = delete; | ||
| Link& operator=(Link&&) = 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 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 <typename Callback> | ||
| 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 <typename Callback> | ||
| void setTempoCallback(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 ticks() method | ||
| * that returns the current ticks of the system clock as well as | ||
| * micros(), which is a normalized representation of the current system | ||
| * time in std::chrono::microseconds. It also provides conversion | ||
| * functions ticksToMicros() and microsToTicks() to faciliate | ||
| * converting between these units. | ||
| */ | ||
| Clock clock() const; | ||
|
|
||
| /*! @brief Capture the current Link timeline 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 | ||
| * Timeline stores a snapshot of the current Link state, so it | ||
| * should be captured and used in a local scope. Storing the | ||
| * Timeline for later use in a different context is not advised | ||
| * because it will provide an outdated view on the Link state. | ||
| */ | ||
| Timeline captureAudioTimeline() const; | ||
|
|
||
| /*! @brief Commit the given timeline 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 timeline will replace the current Link | ||
| * timeline. Modifications to the session based on the new timeline | ||
| * will be communicated to other peers in the session. | ||
| */ | ||
| void commitAudioTimeline(Timeline timeline); | ||
|
|
||
| /*! @brief Capture the current Link timeline from an application | ||
| * thread. | ||
| * Thread-safe: yes | ||
| * Realtime-safe: no | ||
| * | ||
| * @discussion Provides a mechanism for capturing the Link timeline | ||
| * from an application thread (other than the audio thread). The | ||
| * returned Timeline stores a snapshot of the current Link state, | ||
| * so it should be captured and used in a local scope. Storing the | ||
| * Timeline for later use in a different context is not advised | ||
| * because it will provide an outdated view on the Link state. | ||
| */ | ||
| Timeline captureAppTimeline() const; | ||
|
|
||
| /*! @brief Commit the given timeline to the Link session from an | ||
| * application thread. | ||
| * Thread-safe: yes | ||
| * Realtime-safe: no | ||
| * | ||
| * @discussion The given timeline will replace the current Link | ||
| * timeline. Modifications to the session based on the new timeline | ||
| * will be communicated to other peers in the session. | ||
| */ | ||
| void commitAppTimeline(Timeline timeline); | ||
|
|
||
| /*! @class Timeline | ||
| * @brief Representation of a mapping between time and beats for | ||
| * varying quanta. | ||
| * | ||
| * @discussion A Timeline 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. | ||
| */ | ||
| class Timeline | ||
| { | ||
| public: | ||
| Timeline(const link::Timeline timeline, const bool bRespectQuantum); | ||
|
|
||
| /*! @brief: The tempo of the timeline, in bpm */ | ||
| 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); | ||
|
|
||
| private: | ||
| friend Link; | ||
| link::Timeline mOriginalTimeline; | ||
| bool mbRespectQuantum; | ||
| link::Timeline mTimeline; | ||
| }; | ||
|
|
||
| private: | ||
| std::mutex mCallbackMutex; | ||
| link::PeerCountCallback mPeerCountCallback; | ||
| link::TempoCallback mTempoCallback; | ||
| Clock mClock; | ||
| link::platform::Controller mController; | ||
| }; | ||
|
|
||
| } // ableton | ||
|
|
||
| #include <ableton/Link.ipp> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/link/Phase.hpp> | ||
|
|
||
| namespace ableton | ||
| { | ||
|
|
||
| inline Link::Link(const double bpm) | ||
| : mPeerCountCallback([](std::size_t) {}) | ||
| , mTempoCallback([](link::Tempo) {}) | ||
| , mClock{} | ||
| , mController(link::Tempo(bpm), | ||
| [this](const std::size_t peers) { | ||
| std::lock_guard<std::mutex> lock(mCallbackMutex); | ||
| mPeerCountCallback(peers); | ||
| }, | ||
| [this](const link::Tempo tempo) { | ||
| std::lock_guard<std::mutex> lock(mCallbackMutex); | ||
| mTempoCallback(tempo); | ||
| }, | ||
| mClock, | ||
| util::injectVal(link::platform::IoContext{})) | ||
| { | ||
| } | ||
|
|
||
| inline bool Link::isEnabled() const | ||
| { | ||
| return mController.isEnabled(); | ||
| } | ||
|
|
||
| inline void Link::enable(const bool bEnable) | ||
| { | ||
| mController.enable(bEnable); | ||
| } | ||
|
|
||
| inline std::size_t Link::numPeers() const | ||
| { | ||
| return mController.numPeers(); | ||
| } | ||
|
|
||
| template <typename Callback> | ||
| void Link::setNumPeersCallback(Callback callback) | ||
| { | ||
| std::lock_guard<std::mutex> lock(mCallbackMutex); | ||
| mPeerCountCallback = [callback](const std::size_t numPeers) { callback(numPeers); }; | ||
| } | ||
|
|
||
| template <typename Callback> | ||
| void Link::setTempoCallback(Callback callback) | ||
| { | ||
| std::lock_guard<std::mutex> lock(mCallbackMutex); | ||
| mTempoCallback = [callback](const link::Tempo tempo) { callback(tempo.bpm()); }; | ||
| } | ||
|
|
||
| inline Link::Clock Link::clock() const | ||
| { | ||
| return mClock; | ||
| } | ||
|
|
||
| inline Link::Timeline Link::captureAudioTimeline() const | ||
| { | ||
| return Link::Timeline{mController.timelineRtSafe(), numPeers() > 0}; | ||
| } | ||
|
|
||
| inline void Link::commitAudioTimeline(const Link::Timeline timeline) | ||
| { | ||
| if (timeline.mOriginalTimeline != timeline.mTimeline) | ||
| { | ||
| mController.setTimelineRtSafe(timeline.mTimeline, mClock.micros()); | ||
| } | ||
| } | ||
|
|
||
| inline Link::Timeline Link::captureAppTimeline() const | ||
| { | ||
| return Link::Timeline{mController.timeline(), numPeers() > 0}; | ||
| } | ||
|
|
||
| inline void Link::commitAppTimeline(const Link::Timeline timeline) | ||
| { | ||
| if (timeline.mOriginalTimeline != timeline.mTimeline) | ||
| { | ||
| mController.setTimeline(timeline.mTimeline, mClock.micros()); | ||
| } | ||
| } | ||
|
|
||
| // Link::Timeline | ||
|
|
||
| inline Link::Timeline::Timeline(const link::Timeline timeline, const bool bRespectQuantum) | ||
| : mOriginalTimeline(timeline) | ||
| , mbRespectQuantum(bRespectQuantum) | ||
| , mTimeline(timeline) | ||
| { | ||
| } | ||
|
|
||
| inline double Link::Timeline::tempo() const | ||
| { | ||
| return mTimeline.tempo.bpm(); | ||
| } | ||
|
|
||
| inline void Link::Timeline::setTempo( | ||
| const double bpm, const std::chrono::microseconds atTime) | ||
| { | ||
| const auto desiredTl = | ||
| link::clampTempo(link::Timeline{link::Tempo(bpm), mTimeline.toBeats(atTime), atTime}); | ||
| mTimeline.tempo = desiredTl.tempo; | ||
| mTimeline.timeOrigin = desiredTl.fromBeats(mTimeline.beatOrigin); | ||
| } | ||
|
|
||
| inline double Link::Timeline::beatAtTime( | ||
| const std::chrono::microseconds time, const double quantum) const | ||
| { | ||
| return link::toPhaseEncodedBeats(mTimeline, time, link::Beats{quantum}).floating(); | ||
| } | ||
|
|
||
| inline double Link::Timeline::phaseAtTime( | ||
| const std::chrono::microseconds time, const double quantum) const | ||
| { | ||
| return link::phase(link::Beats{beatAtTime(time, quantum)}, link::Beats{quantum}) | ||
| .floating(); | ||
| } | ||
|
|
||
| inline std::chrono::microseconds Link::Timeline::timeAtBeat( | ||
| const double beat, const double quantum) const | ||
| { | ||
| return link::fromPhaseEncodedBeats(mTimeline, link::Beats{beat}, link::Beats{quantum}); | ||
| } | ||
|
|
||
| inline void Link::Timeline::requestBeatAtTime( | ||
| const double beat, std::chrono::microseconds time, const double quantum) | ||
| { | ||
| if (mbRespectQuantum) | ||
| { | ||
| time = timeAtBeat(link::nextPhaseMatch(link::Beats{beatAtTime(time, quantum)}, | ||
| link::Beats{beat}, link::Beats{quantum}) | ||
| .floating(), | ||
| quantum); | ||
| } | ||
| forceBeatAtTime(beat, time, quantum); | ||
| } | ||
|
|
||
| inline void Link::Timeline::forceBeatAtTime( | ||
| const double beat, const std::chrono::microseconds time, const double quantum) | ||
| { | ||
| // There are two components to the beat adjustment: a phase shift | ||
| // and a beat magnitude adjustment. | ||
| const auto curBeatAtTime = link::Beats{beatAtTime(time, quantum)}; | ||
| const auto closestInPhase = | ||
| link::closestPhaseMatch(curBeatAtTime, link::Beats{beat}, link::Beats{quantum}); | ||
| mTimeline = shiftClientTimeline(mTimeline, closestInPhase - curBeatAtTime); | ||
| // Now adjust the magnitude | ||
| mTimeline.beatOrigin = mTimeline.beatOrigin + (link::Beats{beat} - closestInPhase); | ||
| } | ||
|
|
||
| } // ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/platforms/asio/AsioWrapper.hpp> | ||
| #include <ableton/util/Injected.hpp> | ||
| #include <chrono> | ||
| #include <vector> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
|
|
||
| // Callback takes a range of asio::ip:address which is | ||
| // guaranteed to be sorted and unique | ||
| template <typename Callback, typename IoContext> | ||
| class InterfaceScanner | ||
| { | ||
| public: | ||
| using Timer = typename util::Injected<IoContext>::type::Timer; | ||
|
|
||
| InterfaceScanner(const std::chrono::seconds period, | ||
| util::Injected<Callback> callback, | ||
| util::Injected<IoContext> io) | ||
| : mPeriod(period) | ||
| , mCallback(std::move(callback)) | ||
| , mIo(std::move(io)) | ||
| , mTimer(mIo->makeTimer()) | ||
| { | ||
| } | ||
|
|
||
| void enable(const bool bEnable) | ||
| { | ||
| if (bEnable) | ||
| { | ||
| scan(); | ||
| } | ||
| else | ||
| { | ||
| mTimer.cancel(); | ||
| } | ||
| } | ||
|
|
||
| void scan() | ||
| { | ||
| using namespace std; | ||
| debug(mIo->log()) << "Scanning network interfaces"; | ||
| // Rescan the hardware for available network interface addresses | ||
| vector<asio::ip::address> addrs = mIo->scanNetworkInterfaces(); | ||
| // Sort and unique them to guarantee consistent comparison | ||
| sort(begin(addrs), end(addrs)); | ||
| addrs.erase(unique(begin(addrs), end(addrs)), end(addrs)); | ||
| // Pass them to the callback | ||
| (*mCallback)(std::move(addrs)); | ||
| // setup the next scanning | ||
| mTimer.expires_from_now(mPeriod); | ||
| using ErrorCode = typename Timer::ErrorCode; | ||
| mTimer.async_wait([this](const ErrorCode e) { | ||
| if (!e) | ||
| { | ||
| scan(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| private: | ||
| const std::chrono::seconds mPeriod; | ||
| util::Injected<Callback> mCallback; | ||
| util::Injected<IoContext> mIo; | ||
| Timer mTimer; | ||
| }; | ||
|
|
||
| // Factory function | ||
| template <typename Callback, typename IoContext> | ||
| InterfaceScanner<Callback, IoContext> makeInterfaceScanner( | ||
| const std::chrono::seconds period, | ||
| util::Injected<Callback> callback, | ||
| util::Injected<IoContext> io) | ||
| { | ||
| using namespace std; | ||
| return {period, move(callback), move(io)}; | ||
| } | ||
|
|
||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/platforms/asio/AsioService.hpp> | ||
| #include <ableton/util/Injected.hpp> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
|
|
||
| inline asio::ip::udp::endpoint multicastEndpoint() | ||
| { | ||
| return {asio::ip::address::from_string("224.76.78.75"), 20808}; | ||
| } | ||
|
|
||
| // Type tags for dispatching between unicast and multicast packets | ||
| struct MulticastTag | ||
| { | ||
| }; | ||
| struct UnicastTag | ||
| { | ||
| }; | ||
|
|
||
| template <typename IoContext, std::size_t MaxPacketSize> | ||
| class IpV4Interface | ||
| { | ||
| public: | ||
| using Socket = typename util::Injected<IoContext>::type::template Socket<MaxPacketSize>; | ||
|
|
||
| IpV4Interface(util::Injected<IoContext> io, const asio::ip::address_v4& addr) | ||
| : mIo(std::move(io)) | ||
| , mMulticastReceiveSocket(mIo->template openMulticastSocket<MaxPacketSize>(addr)) | ||
| , mSendSocket(mIo->template openUnicastSocket<MaxPacketSize>(addr)) | ||
| { | ||
| } | ||
|
|
||
| IpV4Interface(const IpV4Interface&) = delete; | ||
| IpV4Interface& operator=(const IpV4Interface&) = delete; | ||
|
|
||
| IpV4Interface(IpV4Interface&& rhs) | ||
| : mIo(std::move(rhs.mIo)) | ||
| , mMulticastReceiveSocket(std::move(rhs.mMulticastReceiveSocket)) | ||
| , mSendSocket(std::move(rhs.mSendSocket)) | ||
| { | ||
| } | ||
|
|
||
|
|
||
| std::size_t send( | ||
| const uint8_t* const pData, const size_t numBytes, const asio::ip::udp::endpoint& to) | ||
| { | ||
| return mSendSocket.send(pData, numBytes, to); | ||
| } | ||
|
|
||
| template <typename Handler> | ||
| void receive(Handler handler, UnicastTag) | ||
| { | ||
| mSendSocket.receive(SocketReceiver<UnicastTag, Handler>{std::move(handler)}); | ||
| } | ||
|
|
||
| template <typename Handler> | ||
| void receive(Handler handler, MulticastTag) | ||
| { | ||
| mMulticastReceiveSocket.receive( | ||
| SocketReceiver<MulticastTag, Handler>(std::move(handler))); | ||
| } | ||
|
|
||
| asio::ip::udp::endpoint endpoint() const | ||
| { | ||
| return mSendSocket.endpoint(); | ||
| } | ||
|
|
||
| private: | ||
| template <typename Tag, typename Handler> | ||
| struct SocketReceiver | ||
| { | ||
| SocketReceiver(Handler handler) | ||
| : mHandler(std::move(handler)) | ||
| { | ||
| } | ||
|
|
||
| template <typename It> | ||
| void operator()( | ||
| const asio::ip::udp::endpoint& from, const It messageBegin, const It messageEnd) | ||
| { | ||
| mHandler(Tag{}, from, messageBegin, messageEnd); | ||
| } | ||
|
|
||
| Handler mHandler; | ||
| }; | ||
|
|
||
| util::Injected<IoContext> mIo; | ||
| Socket mMulticastReceiveSocket; | ||
| Socket mSendSocket; | ||
| }; | ||
|
|
||
| template <std::size_t MaxPacketSize, typename IoContext> | ||
| IpV4Interface<IoContext, MaxPacketSize> makeIpV4Interface( | ||
| util::Injected<IoContext> io, const asio::ip::address_v4& addr) | ||
| { | ||
| return {std::move(io), addr}; | ||
| } | ||
|
|
||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
|
|
||
| // Message types used in the Ableton service discovery protocol. There | ||
| // are two logical messages: a state dump and a bye bye. | ||
| // | ||
| // A state dump provides all relevant information about the peer's | ||
| // current state as well as a Time To Live (TTL) value that indicates | ||
| // how many seconds this state should be considered valid. | ||
| // | ||
| // The bye bye indicates that the sender is leaving the session. | ||
|
|
||
| template <typename NodeState> | ||
| struct PeerState | ||
| { | ||
| NodeState peerState; | ||
| int ttl; | ||
| }; | ||
|
|
||
| template <typename NodeId> | ||
| struct ByeBye | ||
| { | ||
| NodeId peerId; | ||
| }; | ||
|
|
||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,293 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/discovery/NetworkByteStreamSerializable.hpp> | ||
| #include <functional> | ||
| #include <unordered_map> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
|
|
||
| struct PayloadEntryHeader | ||
| { | ||
| using Key = std::uint32_t; | ||
| using Size = std::uint32_t; | ||
|
|
||
| Key key; | ||
| Size size; | ||
|
|
||
| friend Size sizeInByteStream(const PayloadEntryHeader& header) | ||
| { | ||
| return sizeInByteStream(header.key) + sizeInByteStream(header.size); | ||
| } | ||
|
|
||
| template <typename It> | ||
| friend It toNetworkByteStream(const PayloadEntryHeader& header, It out) | ||
| { | ||
| return toNetworkByteStream( | ||
| header.size, toNetworkByteStream(header.key, std::move(out))); | ||
| } | ||
|
|
||
| template <typename It> | ||
| static std::pair<PayloadEntryHeader, It> fromNetworkByteStream(It begin, const It end) | ||
| { | ||
| using namespace std; | ||
| Key key; | ||
| Size size; | ||
| tie(key, begin) = Deserialize<Key>::fromNetworkByteStream(begin, end); | ||
| tie(size, begin) = Deserialize<Size>::fromNetworkByteStream(begin, end); | ||
| return make_pair(PayloadEntryHeader{move(key), move(size)}, move(begin)); | ||
| } | ||
| }; | ||
|
|
||
| template <typename EntryType> | ||
| struct PayloadEntry | ||
| { | ||
| PayloadEntry(EntryType entryVal) | ||
| : value(std::move(entryVal)) | ||
| { | ||
| header = {EntryType::key, sizeInByteStream(value)}; | ||
| } | ||
|
|
||
| PayloadEntryHeader header; | ||
| EntryType value; | ||
|
|
||
| friend std::uint32_t sizeInByteStream(const PayloadEntry& entry) | ||
| { | ||
| return sizeInByteStream(entry.header) + sizeInByteStream(entry.value); | ||
| } | ||
|
|
||
| template <typename It> | ||
| friend It toNetworkByteStream(const PayloadEntry& entry, It out) | ||
| { | ||
| return toNetworkByteStream( | ||
| entry.value, toNetworkByteStream(entry.header, std::move(out))); | ||
| } | ||
| }; | ||
|
|
||
| namespace detail | ||
| { | ||
|
|
||
| template <typename It> | ||
| using HandlerMap = | ||
| std::unordered_map<typename PayloadEntryHeader::Key, std::function<void(It, It)>>; | ||
|
|
||
| // Given an index of handlers and a byte range, parse the bytes as a | ||
| // sequence of payload entries and invoke the appropriate handler for | ||
| // each entry type. Entries that are encountered that do not have a | ||
| // corresponding handler in the map are ignored. Throws | ||
| // std::runtime_error if parsing fails for any entry. Note that if an | ||
| // exception is thrown, some of the handlers may have already been called. | ||
| template <typename It> | ||
| void parseByteStream(HandlerMap<It>& map, It bsBegin, const It bsEnd) | ||
| { | ||
| using namespace std; | ||
|
|
||
| while (bsBegin < bsEnd) | ||
| { | ||
| // Try to parse an entry header at this location in the byte stream | ||
| PayloadEntryHeader header; | ||
| It valueBegin; | ||
| tie(header, valueBegin) = | ||
| Deserialize<PayloadEntryHeader>::fromNetworkByteStream(bsBegin, bsEnd); | ||
|
|
||
| // Ensure that the reported size of the entry does not exceed the | ||
| // length of the byte stream | ||
| It valueEnd = valueBegin + header.size; | ||
| if (bsEnd < valueEnd) | ||
| { | ||
| throw range_error("Payload with incorrect size."); | ||
| } | ||
|
|
||
| // The next entry will start at the end of this one | ||
| bsBegin = valueEnd; | ||
|
|
||
| // Use the appropriate handler for this entry, if available | ||
| auto handlerIt = map.find(header.key); | ||
| if (handlerIt != end(map)) | ||
| { | ||
| handlerIt->second(move(valueBegin), move(valueEnd)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| } // namespace detail | ||
|
|
||
|
|
||
| // Payload encoding | ||
| template <typename... Entries> | ||
| struct Payload; | ||
|
|
||
| template <typename First, typename Rest> | ||
| struct Payload<First, Rest> | ||
| { | ||
| Payload(First first, Rest rest) | ||
| : mFirst(std::move(first)) | ||
| , mRest(std::move(rest)) | ||
| { | ||
| } | ||
|
|
||
| Payload(PayloadEntry<First> first, Rest rest) | ||
| : mFirst(std::move(first)) | ||
| , mRest(std::move(rest)) | ||
| { | ||
| } | ||
|
|
||
| template <typename RhsFirst, typename RhsRest> | ||
| using PayloadSum = | ||
| Payload<First, typename Rest::template PayloadSum<RhsFirst, RhsRest>>; | ||
|
|
||
| // Concatenate payloads together into a single payload | ||
| template <typename RhsFirst, typename RhsRest> | ||
| friend PayloadSum<RhsFirst, RhsRest> operator+( | ||
| Payload lhs, Payload<RhsFirst, RhsRest> rhs) | ||
| { | ||
| return {std::move(lhs.mFirst), std::move(lhs.mRest) + std::move(rhs)}; | ||
| } | ||
|
|
||
| friend std::size_t sizeInByteStream(const Payload& payload) | ||
| { | ||
| return sizeInByteStream(payload.mFirst) + sizeInByteStream(payload.mRest); | ||
| } | ||
|
|
||
| template <typename It> | ||
| friend It toNetworkByteStream(const Payload& payload, It streamIt) | ||
| { | ||
| return toNetworkByteStream( | ||
| payload.mRest, toNetworkByteStream(payload.mFirst, std::move(streamIt))); | ||
| } | ||
|
|
||
| PayloadEntry<First> mFirst; | ||
| Rest mRest; | ||
| }; | ||
|
|
||
| template <> | ||
| struct Payload<> | ||
| { | ||
| template <typename RhsFirst, typename RhsRest> | ||
| using PayloadSum = Payload<RhsFirst, RhsRest>; | ||
|
|
||
| template <typename RhsFirst, typename RhsRest> | ||
| friend PayloadSum<RhsFirst, RhsRest> operator+(Payload, Payload<RhsFirst, RhsRest> rhs) | ||
| { | ||
| return rhs; | ||
| } | ||
|
|
||
| friend std::size_t sizeInByteStream(const Payload&) | ||
| { | ||
| return 0; | ||
| } | ||
|
|
||
| template <typename It> | ||
| friend It toNetworkByteStream(const Payload&, It streamIt) | ||
| { | ||
| return streamIt; | ||
| } | ||
| }; | ||
|
|
||
| template <typename... Entries> | ||
| struct PayloadBuilder; | ||
|
|
||
| // Payload factory function | ||
| template <typename... Entries> | ||
| auto makePayload(Entries... entries) | ||
| -> decltype(PayloadBuilder<Entries...>{}(std::move(entries)...)) | ||
| { | ||
| return PayloadBuilder<Entries...>{}(std::move(entries)...); | ||
| } | ||
|
|
||
| template <typename First, typename... Rest> | ||
| struct PayloadBuilder<First, Rest...> | ||
| { | ||
| auto operator()(First first, Rest... rest) | ||
| -> Payload<First, decltype(makePayload(std::move(rest)...))> | ||
| { | ||
| return {std::move(first), makePayload(std::move(rest)...)}; | ||
| } | ||
| }; | ||
|
|
||
| template <> | ||
| struct PayloadBuilder<> | ||
| { | ||
| Payload<> operator()() | ||
| { | ||
| return {}; | ||
| } | ||
| }; | ||
|
|
||
| // Parse payloads to values | ||
| template <typename... Entries> | ||
| struct ParsePayload; | ||
|
|
||
| template <typename First, typename... Rest> | ||
| struct ParsePayload<First, Rest...> | ||
| { | ||
| template <typename It, typename... Handlers> | ||
| static void parse(It begin, It end, Handlers... handlers) | ||
| { | ||
| detail::HandlerMap<It> map; | ||
| collectHandlers(map, std::move(handlers)...); | ||
| detail::parseByteStream(map, std::move(begin), std::move(end)); | ||
| } | ||
|
|
||
| template <typename It, typename FirstHandler, typename... RestHandlers> | ||
| static void collectHandlers( | ||
| detail::HandlerMap<It>& map, FirstHandler handler, RestHandlers... rest) | ||
| { | ||
| using namespace std; | ||
| map[First::key] = [handler](const It begin, const It end) { | ||
| const auto res = First::fromNetworkByteStream(begin, end); | ||
| if (res.second != end) | ||
| { | ||
| std::ostringstream stringStream; | ||
| stringStream << "Parsing payload entry " << First::key | ||
| << " did not consume the expected number of bytes. " | ||
| << " Expected: " << distance(begin, end) | ||
| << ", Actual: " << distance(begin, res.second); | ||
| throw range_error(stringStream.str()); | ||
| } | ||
| handler(res.first); | ||
| }; | ||
|
|
||
| ParsePayload<Rest...>::collectHandlers(map, std::move(rest)...); | ||
| } | ||
| }; | ||
|
|
||
| template <> | ||
| struct ParsePayload<> | ||
| { | ||
| template <typename It> | ||
| static void collectHandlers(detail::HandlerMap<It>&) | ||
| { | ||
| } | ||
| }; | ||
|
|
||
| template <typename... Entries, typename It, typename... Handlers> | ||
| void parsePayload(It begin, It end, Handlers... handlers) | ||
| { | ||
| using namespace std; | ||
| ParsePayload<Entries...>::parse(move(begin), move(end), move(handlers)...); | ||
| } | ||
|
|
||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,253 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/discovery/UdpMessenger.hpp> | ||
| #include <ableton/discovery/v1/Messages.hpp> | ||
| #include <ableton/platforms/asio/AsioService.hpp> | ||
| #include <ableton/util/SafeAsyncHandler.hpp> | ||
| #include <memory> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
|
|
||
| template <typename Messenger, typename PeerObserver, typename IoContext> | ||
| class PeerGateway | ||
| { | ||
| public: | ||
| // The peer types are defined by the observer but must match with those | ||
| // used by the Messenger | ||
| using ObserverT = typename util::Injected<PeerObserver>::type; | ||
| using NodeState = typename ObserverT::GatewayObserverNodeState; | ||
| using NodeId = typename ObserverT::GatewayObserverNodeId; | ||
| using Timer = typename util::Injected<IoContext>::type::Timer; | ||
| using TimerError = typename Timer::ErrorCode; | ||
|
|
||
| PeerGateway(util::Injected<Messenger> messenger, | ||
| util::Injected<PeerObserver> observer, | ||
| util::Injected<IoContext> io) | ||
| : mpImpl(new Impl(std::move(messenger), std::move(observer), std::move(io))) | ||
| { | ||
| mpImpl->listen(); | ||
| } | ||
|
|
||
| PeerGateway(const PeerGateway&) = delete; | ||
| PeerGateway& operator=(const PeerGateway&) = delete; | ||
|
|
||
| PeerGateway(PeerGateway&& rhs) | ||
| : mpImpl(std::move(rhs.mpImpl)) | ||
| { | ||
| } | ||
|
|
||
| void updateState(NodeState state) | ||
| { | ||
| mpImpl->updateState(std::move(state)); | ||
| } | ||
|
|
||
| private: | ||
| using PeerTimeout = std::pair<std::chrono::system_clock::time_point, NodeId>; | ||
| using PeerTimeouts = std::vector<PeerTimeout>; | ||
|
|
||
| struct Impl : std::enable_shared_from_this<Impl> | ||
| { | ||
| Impl(util::Injected<Messenger> messenger, | ||
| util::Injected<PeerObserver> observer, | ||
| util::Injected<IoContext> io) | ||
| : mMessenger(std::move(messenger)) | ||
| , mObserver(std::move(observer)) | ||
| , mIo(std::move(io)) | ||
| , mPruneTimer(mIo->makeTimer()) | ||
| { | ||
| } | ||
|
|
||
| void updateState(NodeState state) | ||
| { | ||
| mMessenger->updateState(std::move(state)); | ||
| try | ||
| { | ||
| mMessenger->broadcastState(); | ||
| } | ||
| catch (const std::runtime_error& err) | ||
| { | ||
| info(mIo->log()) << "State broadcast failed on gateway: " << err.what(); | ||
| } | ||
| } | ||
|
|
||
| void listen() | ||
| { | ||
| mMessenger->receive(util::makeAsyncSafe(this->shared_from_this())); | ||
| } | ||
|
|
||
| // Operators for handling incoming messages | ||
| void operator()(const PeerState<NodeState>& msg) | ||
| { | ||
| onPeerState(msg.peerState, msg.ttl); | ||
| listen(); | ||
| } | ||
|
|
||
| void operator()(const ByeBye<NodeId>& msg) | ||
| { | ||
| onByeBye(msg.peerId); | ||
| listen(); | ||
| } | ||
|
|
||
| void onPeerState(const NodeState& nodeState, const int ttl) | ||
| { | ||
| using namespace std; | ||
| const auto peerId = nodeState.ident(); | ||
| const auto existing = findPeer(peerId); | ||
| if (existing != end(mPeerTimeouts)) | ||
| { | ||
| // If the peer is already present in our timeout list, remove it | ||
| // as it will be re-inserted below. | ||
| mPeerTimeouts.erase(existing); | ||
| } | ||
|
|
||
| auto newTo = make_pair(mPruneTimer.now() + std::chrono::seconds(ttl), peerId); | ||
| mPeerTimeouts.insert( | ||
| upper_bound(begin(mPeerTimeouts), end(mPeerTimeouts), newTo, TimeoutCompare{}), | ||
| move(newTo)); | ||
|
|
||
| sawPeer(*mObserver, nodeState); | ||
| scheduleNextPruning(); | ||
| } | ||
|
|
||
| void onByeBye(const NodeId& peerId) | ||
| { | ||
| const auto it = findPeer(peerId); | ||
| if (it != mPeerTimeouts.end()) | ||
| { | ||
| peerLeft(*mObserver, it->second); | ||
| mPeerTimeouts.erase(it); | ||
| } | ||
| } | ||
|
|
||
| void pruneExpiredPeers() | ||
| { | ||
| using namespace std; | ||
|
|
||
| const auto test = make_pair(mPruneTimer.now(), NodeId{}); | ||
| debug(mIo->log()) << "pruning peers @ " << test.first.time_since_epoch().count(); | ||
|
|
||
| const auto endExpired = | ||
| lower_bound(begin(mPeerTimeouts), end(mPeerTimeouts), test, TimeoutCompare{}); | ||
|
|
||
| for_each(begin(mPeerTimeouts), endExpired, [this](const PeerTimeout& pto) { | ||
| info(mIo->log()) << "pruning peer " << pto.second; | ||
| peerTimedOut(*mObserver, pto.second); | ||
| }); | ||
| mPeerTimeouts.erase(begin(mPeerTimeouts), endExpired); | ||
| scheduleNextPruning(); | ||
| } | ||
|
|
||
| void scheduleNextPruning() | ||
| { | ||
| // Find the next peer to expire and set the timer based on it | ||
| if (!mPeerTimeouts.empty()) | ||
| { | ||
| // Add a second of padding to the timer to avoid over-eager timeouts | ||
| const auto t = mPeerTimeouts.front().first + std::chrono::seconds(1); | ||
|
|
||
| debug(mIo->log()) << "scheduling next pruning for " | ||
| << t.time_since_epoch().count() << " because of peer " | ||
| << mPeerTimeouts.front().second; | ||
|
|
||
| mPruneTimer.expires_at(t); | ||
| mPruneTimer.async_wait([this](const TimerError e) { | ||
| if (!e) | ||
| { | ||
| pruneExpiredPeers(); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| struct TimeoutCompare | ||
| { | ||
| bool operator()(const PeerTimeout& lhs, const PeerTimeout& rhs) const | ||
| { | ||
| return lhs.first < rhs.first; | ||
| } | ||
| }; | ||
|
|
||
| typename PeerTimeouts::iterator findPeer(const NodeId& peerId) | ||
| { | ||
| return std::find_if(begin(mPeerTimeouts), end(mPeerTimeouts), | ||
| [&peerId](const PeerTimeout& pto) { return pto.second == peerId; }); | ||
| } | ||
|
|
||
| util::Injected<Messenger> mMessenger; | ||
| util::Injected<PeerObserver> mObserver; | ||
| util::Injected<IoContext> mIo; | ||
| Timer mPruneTimer; | ||
| PeerTimeouts mPeerTimeouts; // Invariant: sorted by time_point | ||
| }; | ||
|
|
||
| std::shared_ptr<Impl> mpImpl; | ||
| }; | ||
|
|
||
| template <typename Messenger, typename PeerObserver, typename IoContext> | ||
| PeerGateway<Messenger, PeerObserver, IoContext> makePeerGateway( | ||
| util::Injected<Messenger> messenger, | ||
| util::Injected<PeerObserver> observer, | ||
| util::Injected<IoContext> io) | ||
| { | ||
| return {std::move(messenger), std::move(observer), std::move(io)}; | ||
| } | ||
|
|
||
| // IpV4 gateway types | ||
| template <typename StateQuery, typename IoContext> | ||
| using IpV4Messenger = | ||
| UdpMessenger<IpV4Interface<typename util::Injected<IoContext>::type&, | ||
| v1::kMaxMessageSize>, | ||
| StateQuery, | ||
| IoContext>; | ||
|
|
||
| template <typename PeerObserver, typename StateQuery, typename IoContext> | ||
| using IpV4Gateway = | ||
| PeerGateway<IpV4Messenger<StateQuery, typename util::Injected<IoContext>::type&>, | ||
| PeerObserver, | ||
| IoContext>; | ||
|
|
||
| // Factory function to bind a PeerGateway to an IpV4Interface with the given address. | ||
| template <typename PeerObserver, typename NodeState, typename IoContext> | ||
| IpV4Gateway<PeerObserver, NodeState, IoContext> makeIpV4Gateway( | ||
| util::Injected<IoContext> io, | ||
| const asio::ip::address_v4& addr, | ||
| util::Injected<PeerObserver> observer, | ||
| NodeState state) | ||
| { | ||
| using namespace std; | ||
| using namespace util; | ||
|
|
||
| const uint8_t ttl = 5; | ||
| const uint8_t ttlRatio = 20; | ||
|
|
||
| auto iface = makeIpV4Interface<v1::kMaxMessageSize>(injectRef(*io), addr); | ||
|
|
||
| auto messenger = | ||
| makeUdpMessenger(injectVal(move(iface)), move(state), injectRef(*io), ttl, ttlRatio); | ||
| return {injectVal(move(messenger)), move(observer), move(io)}; | ||
| } | ||
|
|
||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,229 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/discovery/InterfaceScanner.hpp> | ||
| #include <ableton/platforms/asio/AsioWrapper.hpp> | ||
| #include <map> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
|
|
||
| // GatewayFactory must have an operator()(NodeState, IoRef, asio::ip::address) | ||
| // that constructs a new PeerGateway on a given interface address. | ||
| template <typename NodeState, typename GatewayFactory, typename IoContext> | ||
| class PeerGateways | ||
| { | ||
| public: | ||
| using IoType = typename util::Injected<IoContext>::type; | ||
| using Gateway = typename std::result_of<GatewayFactory( | ||
| NodeState, util::Injected<IoType&>, asio::ip::address)>::type; | ||
| using GatewayMap = std::map<asio::ip::address, Gateway>; | ||
|
|
||
| PeerGateways(const std::chrono::seconds rescanPeriod, | ||
| NodeState state, | ||
| GatewayFactory factory, | ||
| util::Injected<IoContext> io) | ||
| : mIo(std::move(io)) | ||
| { | ||
| mpScannerCallback = | ||
| std::make_shared<Callback>(std::move(state), std::move(factory), *mIo); | ||
| mpScanner = std::make_shared<Scanner>( | ||
| rescanPeriod, util::injectShared(mpScannerCallback), util::injectRef(*mIo)); | ||
| } | ||
|
|
||
| ~PeerGateways() | ||
| { | ||
| // Release the callback in the io thread so that gateway cleanup | ||
| // doesn't happen in the client thread | ||
| mIo->async(Deleter{*this}); | ||
| } | ||
|
|
||
| PeerGateways(const PeerGateways&) = delete; | ||
| PeerGateways& operator=(const PeerGateways&) = delete; | ||
|
|
||
| PeerGateways(PeerGateways&&) = delete; | ||
| PeerGateways& operator=(PeerGateways&&) = delete; | ||
|
|
||
| void enable(const bool bEnable) | ||
| { | ||
| auto pCallback = mpScannerCallback; | ||
| auto pScanner = mpScanner; | ||
|
|
||
| if (pCallback && pScanner) | ||
| { | ||
| mIo->async([pCallback, pScanner, bEnable] { | ||
| pCallback->mGateways.clear(); | ||
| pScanner->enable(bEnable); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| template <typename Handler> | ||
| void withGatewaysAsync(Handler handler) | ||
| { | ||
| auto pCallback = mpScannerCallback; | ||
| if (pCallback) | ||
| { | ||
| mIo->async([pCallback, handler] { | ||
| handler(pCallback->mGateways.begin(), pCallback->mGateways.end()); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| void updateNodeState(const NodeState& state) | ||
| { | ||
| auto pCallback = mpScannerCallback; | ||
| if (pCallback) | ||
| { | ||
| mIo->async([pCallback, state] { | ||
| pCallback->mState = state; | ||
| for (const auto& entry : pCallback->mGateways) | ||
| { | ||
| entry.second->updateNodeState(state); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // If a gateway has become non-responsive or is throwing exceptions, | ||
| // this method can be invoked to either fix it or discard it. | ||
| void repairGateway(const asio::ip::address& gatewayAddr) | ||
| { | ||
| auto pCallback = mpScannerCallback; | ||
| auto pScanner = mpScanner; | ||
| if (pCallback && pScanner) | ||
| { | ||
| mIo->async([pCallback, pScanner, gatewayAddr] { | ||
| if (pCallback->mGateways.erase(gatewayAddr)) | ||
| { | ||
| // If we erased a gateway, rescan again immediately so that | ||
| // we will re-initialize it if it's still present | ||
| pScanner->scan(); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| private: | ||
| struct Callback | ||
| { | ||
| Callback(NodeState state, GatewayFactory factory, IoType& io) | ||
| : mState(std::move(state)) | ||
| , mFactory(std::move(factory)) | ||
| , mIo(io) | ||
| { | ||
| } | ||
|
|
||
| template <typename AddrRange> | ||
| void operator()(const AddrRange& range) | ||
| { | ||
| using namespace std; | ||
| // Get the set of current addresses. | ||
| vector<asio::ip::address> curAddrs; | ||
| curAddrs.reserve(mGateways.size()); | ||
| transform(std::begin(mGateways), std::end(mGateways), back_inserter(curAddrs), | ||
| [](const typename GatewayMap::value_type& vt) { return vt.first; }); | ||
|
|
||
| // Now use set_difference to determine the set of addresses that | ||
| // are new and the set of cur addresses that are no longer there | ||
| vector<asio::ip::address> newAddrs; | ||
| set_difference(std::begin(range), std::end(range), std::begin(curAddrs), | ||
| std::end(curAddrs), back_inserter(newAddrs)); | ||
|
|
||
| vector<asio::ip::address> staleAddrs; | ||
| set_difference(std::begin(curAddrs), std::end(curAddrs), std::begin(range), | ||
| std::end(range), back_inserter(staleAddrs)); | ||
|
|
||
| // Remove the stale addresses | ||
| for (const auto& addr : staleAddrs) | ||
| { | ||
| mGateways.erase(addr); | ||
| } | ||
|
|
||
| // Add the new addresses | ||
| for (const auto& addr : newAddrs) | ||
| { | ||
| try | ||
| { | ||
| // Only handle v4 for now | ||
| if (addr.is_v4()) | ||
| { | ||
| info(mIo.log()) << "initializing peer gateway on interface " << addr; | ||
| mGateways.emplace(addr, mFactory(mState, util::injectRef(mIo), addr.to_v4())); | ||
| } | ||
| } | ||
| catch (const runtime_error& e) | ||
| { | ||
| warning(mIo.log()) << "failed to init gateway on interface " << addr | ||
| << " reason: " << e.what(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| NodeState mState; | ||
| GatewayFactory mFactory; | ||
| IoType& mIo; | ||
| GatewayMap mGateways; | ||
| }; | ||
|
|
||
| using Scanner = InterfaceScanner<std::shared_ptr<Callback>, IoType&>; | ||
|
|
||
| struct Deleter | ||
| { | ||
| Deleter(PeerGateways& gateways) | ||
| : mpScannerCallback(std::move(gateways.mpScannerCallback)) | ||
| , mpScanner(std::move(gateways.mpScanner)) | ||
| { | ||
| } | ||
|
|
||
| void operator()() | ||
| { | ||
| mpScanner.reset(); | ||
| mpScannerCallback.reset(); | ||
| } | ||
|
|
||
| std::shared_ptr<Callback> mpScannerCallback; | ||
| std::shared_ptr<Scanner> mpScanner; | ||
| }; | ||
|
|
||
| std::shared_ptr<Callback> mpScannerCallback; | ||
| std::shared_ptr<Scanner> mpScanner; | ||
| util::Injected<IoContext> mIo; | ||
| }; | ||
|
|
||
| // Factory function | ||
| template <typename NodeState, typename GatewayFactory, typename IoContext> | ||
| std::unique_ptr<PeerGateways<NodeState, GatewayFactory, IoContext>> makePeerGateways( | ||
| const std::chrono::seconds rescanPeriod, | ||
| NodeState state, | ||
| GatewayFactory factory, | ||
| util::Injected<IoContext> io) | ||
| { | ||
| using namespace std; | ||
| using Gateways = PeerGateways<NodeState, GatewayFactory, IoContext>; | ||
| return unique_ptr<Gateways>{ | ||
| new Gateways{rescanPeriod, move(state), move(factory), move(io)}}; | ||
| } | ||
|
|
||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/discovery/PeerGateways.hpp> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
|
|
||
| template <typename NodeState, typename GatewayFactory, typename IoContext> | ||
| class Service | ||
| { | ||
| public: | ||
| using ServicePeerGateways = PeerGateways<NodeState, GatewayFactory, IoContext>; | ||
|
|
||
| Service(NodeState state, GatewayFactory factory, util::Injected<IoContext> io) | ||
| : mGateways( | ||
| std::chrono::seconds(5), std::move(state), std::move(factory), std::move(io)) | ||
| { | ||
| } | ||
|
|
||
| void enable(const bool bEnable) | ||
| { | ||
| mGateways.enable(bEnable); | ||
| } | ||
|
|
||
| // Asynchronously operate on the current set of peer gateways. The | ||
| // handler will be invoked in the service's io context. | ||
| template <typename Handler> | ||
| void withGatewaysAsync(Handler handler) | ||
| { | ||
| mGateways.withGatewaysAsync(std::move(handler)); | ||
| } | ||
|
|
||
| void updateNodeState(const NodeState& state) | ||
| { | ||
| mGateways.updateNodeState(state); | ||
| } | ||
|
|
||
| // Repair the gateway with the given address if possible. Its | ||
| // sockets may have been closed, for example, and the gateway needs | ||
| // to be regenerated. | ||
| void repairGateway(const asio::ip::address& gatewayAddr) | ||
| { | ||
| mGateways.repairGateway(gatewayAddr); | ||
| } | ||
|
|
||
| private: | ||
| ServicePeerGateways mGateways; | ||
| }; | ||
|
|
||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/platforms/asio/AsioService.hpp> | ||
| #include <ableton/platforms/asio/AsioWrapper.hpp> | ||
| #include <ableton/util/SafeAsyncHandler.hpp> | ||
| #include <array> | ||
| #include <cassert> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
|
|
||
| template <std::size_t MaxPacketSize> | ||
| struct Socket | ||
| { | ||
| Socket(platforms::asio::AsioService& io) | ||
| : mpImpl(std::make_shared<Impl>(io)) | ||
| { | ||
| } | ||
|
|
||
| Socket(const Socket&) = delete; | ||
| Socket& operator=(const Socket&) = delete; | ||
|
|
||
| Socket(Socket&& rhs) | ||
| : mpImpl(std::move(rhs.mpImpl)) | ||
| { | ||
| } | ||
|
|
||
| std::size_t send( | ||
| const uint8_t* const pData, const size_t numBytes, const asio::ip::udp::endpoint& to) | ||
| { | ||
| assert(numBytes < MaxPacketSize); | ||
| return mpImpl->mSocket.send_to(asio::buffer(pData, numBytes), to); | ||
| } | ||
|
|
||
| template <typename Handler> | ||
| void receive(Handler handler) | ||
| { | ||
| mpImpl->mHandler = std::move(handler); | ||
| mpImpl->mSocket.async_receive_from( | ||
| asio::buffer(mpImpl->mReceiveBuffer, MaxPacketSize), mpImpl->mSenderEndpoint, | ||
| util::makeAsyncSafe(mpImpl)); | ||
| } | ||
|
|
||
| asio::ip::udp::endpoint endpoint() const | ||
| { | ||
| return mpImpl->mSocket.local_endpoint(); | ||
| } | ||
|
|
||
| struct Impl | ||
| { | ||
| Impl(platforms::asio::AsioService& io) | ||
| : mSocket(io.mService, asio::ip::udp::v4()) | ||
| { | ||
| } | ||
|
|
||
| ~Impl() | ||
| { | ||
| // Ignore error codes in shutdown and close as the socket may | ||
| // have already been forcibly closed | ||
| asio::error_code ec; | ||
| mSocket.shutdown(asio::ip::udp::socket::shutdown_both, ec); | ||
| mSocket.close(ec); | ||
| } | ||
|
|
||
| void operator()(const asio::error_code& error, const std::size_t numBytes) | ||
| { | ||
| if (!error && numBytes > 0 && numBytes <= MaxPacketSize) | ||
| { | ||
| const auto bufBegin = begin(mReceiveBuffer); | ||
| mHandler(mSenderEndpoint, bufBegin, bufBegin + static_cast<ptrdiff_t>(numBytes)); | ||
| } | ||
| } | ||
|
|
||
| asio::ip::udp::socket mSocket; | ||
| asio::ip::udp::endpoint mSenderEndpoint; | ||
| using Buffer = std::array<uint8_t, MaxPacketSize>; | ||
| Buffer mReceiveBuffer; | ||
| using ByteIt = typename Buffer::const_iterator; | ||
| std::function<void(const asio::ip::udp::endpoint&, ByteIt, ByteIt)> mHandler; | ||
| }; | ||
|
|
||
| std::shared_ptr<Impl> mpImpl; | ||
| }; | ||
|
|
||
| // Configure an asio socket for receiving multicast messages | ||
| template <std::size_t MaxPacketSize> | ||
| void configureMulticastSocket(Socket<MaxPacketSize>& socket, | ||
| const asio::ip::address_v4& addr, | ||
| const asio::ip::udp::endpoint& multicastEndpoint) | ||
| { | ||
| socket.mpImpl->mSocket.set_option(asio::ip::udp::socket::reuse_address(true)); | ||
| // ??? | ||
| socket.mpImpl->mSocket.set_option(asio::socket_base::broadcast(!addr.is_loopback())); | ||
| // ??? | ||
| socket.mpImpl->mSocket.set_option( | ||
| asio::ip::multicast::enable_loopback(addr.is_loopback())); | ||
| socket.mpImpl->mSocket.set_option(asio::ip::multicast::outbound_interface(addr)); | ||
| // Is from_string("0.0.0.0") best approach? | ||
| socket.mpImpl->mSocket.bind( | ||
| {asio::ip::address::from_string("0.0.0.0"), multicastEndpoint.port()}); | ||
| socket.mpImpl->mSocket.set_option( | ||
| asio::ip::multicast::join_group(multicastEndpoint.address().to_v4(), addr)); | ||
| } | ||
|
|
||
| // Configure an asio socket for receiving unicast messages | ||
| template <std::size_t MaxPacketSize> | ||
| void configureUnicastSocket( | ||
| Socket<MaxPacketSize>& socket, const asio::ip::address_v4& addr) | ||
| { | ||
| // ??? really necessary? | ||
| socket.mpImpl->mSocket.set_option( | ||
| asio::ip::multicast::enable_loopback(addr.is_loopback())); | ||
| socket.mpImpl->mSocket.set_option(asio::ip::multicast::outbound_interface(addr)); | ||
| socket.mpImpl->mSocket.bind(asio::ip::udp::endpoint{addr, 0}); | ||
| } | ||
|
|
||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,330 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/discovery/IpV4Interface.hpp> | ||
| #include <ableton/discovery/MessageTypes.hpp> | ||
| #include <ableton/discovery/v1/Messages.hpp> | ||
| #include <ableton/platforms/asio/AsioWrapper.hpp> | ||
| #include <ableton/util/Injected.hpp> | ||
| #include <ableton/util/SafeAsyncHandler.hpp> | ||
| #include <algorithm> | ||
| #include <memory> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
|
|
||
| // An exception thrown when sending a udp message fails. Stores the | ||
| // interface through which the sending failed. | ||
| struct UdpSendException : std::runtime_error | ||
| { | ||
| UdpSendException(const std::runtime_error& e, asio::ip::address ifAddr) | ||
| : std::runtime_error(e.what()) | ||
| , interfaceAddr(std::move(ifAddr)) | ||
| { | ||
| } | ||
|
|
||
| asio::ip::address interfaceAddr; | ||
| }; | ||
|
|
||
| // Throws UdpSendException | ||
| template <typename Interface, typename NodeId, typename Payload> | ||
| void sendUdpMessage(Interface& iface, | ||
| NodeId from, | ||
| const uint8_t ttl, | ||
| const v1::MessageType messageType, | ||
| const Payload& payload, | ||
| const asio::ip::udp::endpoint& to) | ||
| { | ||
| using namespace std; | ||
| v1::MessageBuffer buffer; | ||
| const auto messageBegin = begin(buffer); | ||
| const auto messageEnd = | ||
| v1::detail::encodeMessage(move(from), ttl, messageType, payload, messageBegin); | ||
| const auto numBytes = static_cast<size_t>(distance(messageBegin, messageEnd)); | ||
| try | ||
| { | ||
| iface.send(buffer.data(), numBytes, to); | ||
| } | ||
| catch (const std::runtime_error& err) | ||
| { | ||
| throw UdpSendException{err, iface.endpoint().address()}; | ||
| } | ||
| } | ||
|
|
||
| // UdpMessenger uses a "shared_ptr pImpl" pattern to make it movable | ||
| // and to support safe async handler callbacks when receiving messages | ||
| // on the given interface. | ||
| template <typename Interface, typename NodeStateT, typename IoContext> | ||
| class UdpMessenger | ||
| { | ||
| public: | ||
| using NodeState = NodeStateT; | ||
| using NodeId = typename NodeState::IdType; | ||
| using Timer = typename util::Injected<IoContext>::type::Timer; | ||
| using TimerError = typename Timer::ErrorCode; | ||
| using TimePoint = typename Timer::TimePoint; | ||
|
|
||
| UdpMessenger(util::Injected<Interface> iface, | ||
| NodeState state, | ||
| util::Injected<IoContext> io, | ||
| const uint8_t ttl, | ||
| const uint8_t ttlRatio) | ||
| : mpImpl(std::make_shared<Impl>( | ||
| std::move(iface), std::move(state), std::move(io), ttl, ttlRatio)) | ||
| { | ||
| // We need to always listen for incoming traffic in order to | ||
| // respond to peer state broadcasts | ||
| mpImpl->listen(MulticastTag{}); | ||
| mpImpl->listen(UnicastTag{}); | ||
| mpImpl->broadcastState(); | ||
| } | ||
|
|
||
| UdpMessenger(const UdpMessenger&) = delete; | ||
| UdpMessenger& operator=(const UdpMessenger&) = delete; | ||
|
|
||
| UdpMessenger(UdpMessenger&& rhs) | ||
| : mpImpl(std::move(rhs.mpImpl)) | ||
| { | ||
| } | ||
|
|
||
| ~UdpMessenger() | ||
| { | ||
| if (mpImpl != nullptr) | ||
| { | ||
| try | ||
| { | ||
| mpImpl->sendByeBye(); | ||
| } | ||
| catch (const UdpSendException& err) | ||
| { | ||
| debug(mpImpl->mIo->log()) << "Failed to send bye bye message: " << err.what(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void updateState(NodeState state) | ||
| { | ||
| mpImpl->updateState(std::move(state)); | ||
| } | ||
|
|
||
| // Broadcast the current state of the system to all peers. May throw | ||
| // std::runtime_error if assembling a broadcast message fails or if | ||
| // there is an error at the transport layer. Throws on failure. | ||
| void broadcastState() | ||
| { | ||
| mpImpl->broadcastState(); | ||
| } | ||
|
|
||
| // Asynchronous receive function for incoming messages from peers. Will | ||
| // return immediately and the handler will be invoked when a message | ||
| // is received. Handler must have operator() overloads for PeerState and | ||
| // ByeBye messages. | ||
| template <typename Handler> | ||
| void receive(Handler handler) | ||
| { | ||
| mpImpl->setReceiveHandler(std::move(handler)); | ||
| } | ||
|
|
||
| private: | ||
| struct Impl : std::enable_shared_from_this<Impl> | ||
| { | ||
| Impl(util::Injected<Interface> iface, | ||
| NodeState state, | ||
| util::Injected<IoContext> io, | ||
| const uint8_t ttl, | ||
| const uint8_t ttlRatio) | ||
| : mIo(std::move(io)) | ||
| , mInterface(std::move(iface)) | ||
| , mState(std::move(state)) | ||
| , mTimer(mIo->makeTimer()) | ||
| , mLastBroadcastTime{} | ||
| , mTtl(ttl) | ||
| , mTtlRatio(ttlRatio) | ||
| , mPeerStateHandler([](PeerState<NodeState>) {}) | ||
| , mByeByeHandler([](ByeBye<NodeId>) {}) | ||
| { | ||
| } | ||
|
|
||
| template <typename Handler> | ||
| void setReceiveHandler(Handler handler) | ||
| { | ||
| mPeerStateHandler = [handler]( | ||
| PeerState<NodeState> state) { handler(std::move(state)); }; | ||
|
|
||
| mByeByeHandler = [handler](ByeBye<NodeId> byeBye) { handler(std::move(byeBye)); }; | ||
| } | ||
|
|
||
| void sendByeBye() | ||
| { | ||
| sendUdpMessage( | ||
| *mInterface, mState.ident(), 0, v1::kByeBye, makePayload(), multicastEndpoint()); | ||
| } | ||
|
|
||
| void updateState(NodeState state) | ||
| { | ||
| mState = std::move(state); | ||
| } | ||
|
|
||
| void broadcastState() | ||
| { | ||
| using namespace std::chrono; | ||
|
|
||
| const auto minBroadcastPeriod = milliseconds{50}; | ||
| const auto nominalBroadcastPeriod = milliseconds(mTtl * 1000 / mTtlRatio); | ||
| const auto timeSinceLastBroadcast = | ||
| duration_cast<milliseconds>(mTimer.now() - mLastBroadcastTime); | ||
|
|
||
| // The rate is limited to maxBroadcastRate to prevent flooding the network. | ||
| const auto delay = minBroadcastPeriod - timeSinceLastBroadcast; | ||
|
|
||
| // Schedule the next broadcast before we actually send the | ||
| // message so that if sending throws an exception we are still | ||
| // scheduled to try again. We want to keep trying at our | ||
| // interval as long as this instance is alive. | ||
| mTimer.expires_from_now(delay > milliseconds{0} ? delay : nominalBroadcastPeriod); | ||
| mTimer.async_wait([this](const TimerError e) { | ||
| if (!e) | ||
| { | ||
| broadcastState(); | ||
| } | ||
| }); | ||
|
|
||
| // If we're not delaying, broadcast now | ||
| if (delay < milliseconds{1}) | ||
| { | ||
| debug(mIo->log()) << "Broadcasting state"; | ||
| sendPeerState(v1::kAlive, multicastEndpoint()); | ||
| } | ||
| } | ||
|
|
||
| void sendPeerState( | ||
| const v1::MessageType messageType, const asio::ip::udp::endpoint& to) | ||
| { | ||
| sendUdpMessage( | ||
| *mInterface, mState.ident(), mTtl, messageType, toPayload(mState), to); | ||
| mLastBroadcastTime = mTimer.now(); | ||
| } | ||
|
|
||
| void sendResponse(const asio::ip::udp::endpoint& to) | ||
| { | ||
| sendPeerState(v1::kResponse, to); | ||
| } | ||
|
|
||
| template <typename Tag> | ||
| void listen(Tag tag) | ||
| { | ||
| mInterface->receive(util::makeAsyncSafe(this->shared_from_this()), tag); | ||
| } | ||
|
|
||
| template <typename Tag, typename It> | ||
| void operator()(Tag tag, | ||
| const asio::ip::udp::endpoint& from, | ||
| const It messageBegin, | ||
| const It messageEnd) | ||
| { | ||
| auto result = v1::parseMessageHeader<NodeId>(messageBegin, messageEnd); | ||
|
|
||
| const auto& header = result.first; | ||
| // Ignore messages from self and other groups | ||
| if (header.ident != mState.ident() && header.groupId == 0) | ||
| { | ||
| debug(mIo->log()) << "Received message type " | ||
| << static_cast<int>(header.messageType) << " from peer " | ||
| << header.ident; | ||
|
|
||
| switch (header.messageType) | ||
| { | ||
| case v1::kAlive: | ||
| sendResponse(from); | ||
| receivePeerState(std::move(result.first), result.second, messageEnd); | ||
| break; | ||
| case v1::kResponse: | ||
| receivePeerState(std::move(result.first), result.second, messageEnd); | ||
| break; | ||
| case v1::kByeBye: | ||
| receiveByeBye(std::move(result.first.ident)); | ||
| break; | ||
| default: | ||
| info(mIo->log()) << "Unknown message received of type: " << header.messageType; | ||
| } | ||
| } | ||
| listen(tag); | ||
| } | ||
|
|
||
| template <typename It> | ||
| void receivePeerState( | ||
| v1::MessageHeader<NodeId> header, It payloadBegin, It payloadEnd) | ||
| { | ||
| try | ||
| { | ||
| auto state = NodeState::fromPayload( | ||
| std::move(header.ident), std::move(payloadBegin), std::move(payloadEnd)); | ||
|
|
||
| // Handlers must only be called once | ||
| auto handler = std::move(mPeerStateHandler); | ||
| mPeerStateHandler = [](PeerState<NodeState>) {}; | ||
| handler(PeerState<NodeState>{std::move(state), header.ttl}); | ||
| } | ||
| catch (const std::runtime_error& err) | ||
| { | ||
| info(mIo->log()) << "Ignoring peer state message: " << err.what(); | ||
| } | ||
| } | ||
|
|
||
| void receiveByeBye(NodeId nodeId) | ||
| { | ||
| // Handlers must only be called once | ||
| auto byeByeHandler = std::move(mByeByeHandler); | ||
| mByeByeHandler = [](ByeBye<NodeId>) {}; | ||
| byeByeHandler(ByeBye<NodeId>{std::move(nodeId)}); | ||
| } | ||
|
|
||
| util::Injected<IoContext> mIo; | ||
| util::Injected<Interface> mInterface; | ||
| NodeState mState; | ||
| Timer mTimer; | ||
| TimePoint mLastBroadcastTime; | ||
| uint8_t mTtl; | ||
| uint8_t mTtlRatio; | ||
| std::function<void(PeerState<NodeState>)> mPeerStateHandler; | ||
| std::function<void(ByeBye<NodeId>)> mByeByeHandler; | ||
| }; | ||
|
|
||
| std::shared_ptr<Impl> mpImpl; | ||
| }; | ||
|
|
||
| // Factory function | ||
| template <typename Interface, typename NodeState, typename IoContext> | ||
| UdpMessenger<Interface, NodeState, IoContext> makeUdpMessenger( | ||
| util::Injected<Interface> iface, | ||
| NodeState state, | ||
| util::Injected<IoContext> io, | ||
| const uint8_t ttl, | ||
| const uint8_t ttlRatio) | ||
| { | ||
| return UdpMessenger<Interface, NodeState, IoContext>{ | ||
| std::move(iface), std::move(state), std::move(io), ttl, ttlRatio}; | ||
| } | ||
|
|
||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/util/Log.hpp> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
| namespace test | ||
| { | ||
|
|
||
| class Interface | ||
| { | ||
| public: | ||
| void send(const uint8_t* const bytes, | ||
| const size_t numBytes, | ||
| const asio::ip::udp::endpoint& endpoint) | ||
| { | ||
| sentMessages.push_back( | ||
| std::make_pair(std::vector<uint8_t>{bytes, bytes + numBytes}, endpoint)); | ||
| } | ||
|
|
||
| template <typename Callback, typename Tag> | ||
| void receive(Callback callback, Tag tag) | ||
| { | ||
| mCallback = [callback, tag]( | ||
| const asio::ip::udp::endpoint& from, const std::vector<uint8_t>& buffer) { | ||
| callback(tag, from, begin(buffer), end(buffer)); | ||
| }; | ||
| } | ||
|
|
||
| template <typename It> | ||
| void incomingMessage( | ||
| const asio::ip::udp::endpoint& from, It messageBegin, It messageEnd) | ||
| { | ||
| std::vector<uint8_t> buffer{messageBegin, messageEnd}; | ||
| mCallback(from, buffer); | ||
| } | ||
|
|
||
| asio::ip::udp::endpoint endpoint() const | ||
| { | ||
| return asio::ip::udp::endpoint({}, 0); | ||
| } | ||
|
|
||
| using SentMessage = std::pair<std::vector<uint8_t>, asio::ip::udp::endpoint>; | ||
| std::vector<SentMessage> sentMessages; | ||
|
|
||
| private: | ||
| using ReceiveCallback = | ||
| std::function<void(const asio::ip::udp::endpoint&, const std::vector<uint8_t>&)>; | ||
| ReceiveCallback mCallback; | ||
| }; | ||
|
|
||
| } // namespace test | ||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/discovery/NetworkByteStreamSerializable.hpp> | ||
| #include <cstdint> | ||
| #include <utility> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
| namespace test | ||
| { | ||
|
|
||
| // Test payload entries | ||
|
|
||
| // A fixed-size entry type | ||
| struct Foo | ||
| { | ||
| enum | ||
| { | ||
| key = '_foo' | ||
| }; | ||
|
|
||
| std::int32_t fooVal; | ||
|
|
||
| friend std::uint32_t sizeInByteStream(const Foo& foo) | ||
| { | ||
| // Namespace qualification is needed to avoid ambiguous function definitions | ||
| return discovery::sizeInByteStream(foo.fooVal); | ||
| } | ||
|
|
||
| template <typename It> | ||
| friend It toNetworkByteStream(const Foo& foo, It out) | ||
| { | ||
| return discovery::toNetworkByteStream(foo.fooVal, std::move(out)); | ||
| } | ||
|
|
||
| template <typename It> | ||
| static std::pair<Foo, It> fromNetworkByteStream(It begin, It end) | ||
| { | ||
| auto result = Deserialize<decltype(fooVal)>::fromNetworkByteStream( | ||
| std::move(begin), std::move(end)); | ||
| return std::make_pair(Foo{std::move(result.first)}, std::move(result.second)); | ||
| } | ||
| }; | ||
|
|
||
| // A variable-size entry type | ||
| struct Bar | ||
| { | ||
| enum | ||
| { | ||
| key = '_bar' | ||
| }; | ||
|
|
||
| std::vector<std::uint64_t> barVals; | ||
|
|
||
| friend std::uint32_t sizeInByteStream(const Bar& bar) | ||
| { | ||
| return discovery::sizeInByteStream(bar.barVals); | ||
| } | ||
|
|
||
| template <typename It> | ||
| friend It toNetworkByteStream(const Bar& bar, It out) | ||
| { | ||
| return discovery::toNetworkByteStream(bar.barVals, out); | ||
| } | ||
|
|
||
| template <typename It> | ||
| static std::pair<Bar, It> fromNetworkByteStream(It begin, It end) | ||
| { | ||
| auto result = Deserialize<decltype(barVals)>::fromNetworkByteStream( | ||
| std::move(begin), std::move(end)); | ||
| return std::make_pair(Bar{std::move(result.first)}, std::move(result.second)); | ||
| } | ||
| }; | ||
|
|
||
| } // namespace test | ||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/platforms/asio/AsioWrapper.hpp> | ||
| #include <ableton/util/Log.hpp> | ||
| #include <ableton/util/test/IoService.hpp> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
| namespace test | ||
| { | ||
|
|
||
| class Socket | ||
| { | ||
| public: | ||
| Socket(util::test::IoService&) | ||
| { | ||
| } | ||
|
|
||
| friend void configureUnicastSocket(Socket&, const asio::ip::address_v4&) | ||
| { | ||
| } | ||
|
|
||
| std::size_t send( | ||
| const uint8_t* const pData, const size_t numBytes, const asio::ip::udp::endpoint& to) | ||
| { | ||
| sentMessages.push_back( | ||
| std::make_pair(std::vector<uint8_t>{pData, pData + numBytes}, to)); | ||
| return numBytes; | ||
| } | ||
|
|
||
| template <typename Handler> | ||
| void receive(Handler handler) | ||
| { | ||
| mCallback = [handler](const asio::ip::udp::endpoint& from, | ||
| const std::vector<uint8_t>& buffer) { handler(from, begin(buffer), end(buffer)); }; | ||
| } | ||
|
|
||
| template <typename It> | ||
| void incomingMessage( | ||
| const asio::ip::udp::endpoint& from, It messageBegin, It messageEnd) | ||
| { | ||
| std::vector<uint8_t> buffer{messageBegin, messageEnd}; | ||
| mCallback(from, buffer); | ||
| } | ||
|
|
||
| asio::ip::udp::endpoint endpoint() const | ||
| { | ||
| return asio::ip::udp::endpoint({}, 0); | ||
| } | ||
|
|
||
| using SentMessage = std::pair<std::vector<uint8_t>, asio::ip::udp::endpoint>; | ||
| std::vector<SentMessage> sentMessages; | ||
|
|
||
| private: | ||
| using ReceiveCallback = | ||
| std::function<void(const asio::ip::udp::endpoint&, const std::vector<uint8_t>&)>; | ||
| ReceiveCallback mCallback; | ||
| }; | ||
|
|
||
| } // namespace test | ||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/discovery/Payload.hpp> | ||
| #include <array> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace discovery | ||
| { | ||
| namespace v1 | ||
| { | ||
|
|
||
| // The maximum size of a message, in bytes | ||
| const std::size_t kMaxMessageSize = 512; | ||
| // Utility typedef for an array of bytes of maximum message size | ||
| using MessageBuffer = std::array<uint8_t, v1::kMaxMessageSize>; | ||
|
|
||
| using MessageType = uint8_t; | ||
| using SessionGroupId = uint16_t; | ||
|
|
||
| const MessageType kInvalid = 0; | ||
| const MessageType kAlive = 1; | ||
| const MessageType kResponse = 2; | ||
| const MessageType kByeBye = 3; | ||
|
|
||
| template <typename NodeId> | ||
| struct MessageHeader | ||
| { | ||
| MessageType messageType; | ||
| uint8_t ttl; | ||
| SessionGroupId groupId; | ||
| NodeId ident; | ||
|
|
||
| friend std::uint32_t sizeInByteStream(const MessageHeader& header) | ||
| { | ||
| return discovery::sizeInByteStream(header.messageType) | ||
| + discovery::sizeInByteStream(header.ttl) | ||
| + discovery::sizeInByteStream(header.groupId) | ||
| + discovery::sizeInByteStream(header.ident); | ||
| } | ||
|
|
||
| template <typename It> | ||
| friend It toNetworkByteStream(const MessageHeader& header, It out) | ||
| { | ||
| return discovery::toNetworkByteStream(header.ident, | ||
| discovery::toNetworkByteStream(header.groupId, | ||
| discovery::toNetworkByteStream(header.ttl, | ||
| discovery::toNetworkByteStream(header.messageType, std::move(out))))); | ||
| } | ||
|
|
||
| template <typename It> | ||
| static std::pair<MessageHeader, It> fromNetworkByteStream(It begin, const It end) | ||
| { | ||
| using namespace std; | ||
|
|
||
| MessageHeader header; | ||
| tie(header.messageType, begin) = | ||
| Deserialize<decltype(header.messageType)>::fromNetworkByteStream(begin, end); | ||
| tie(header.ttl, begin) = | ||
| Deserialize<decltype(header.ttl)>::fromNetworkByteStream(begin, end); | ||
| tie(header.groupId, begin) = | ||
| Deserialize<decltype(header.groupId)>::fromNetworkByteStream(begin, end); | ||
| tie(header.ident, begin) = | ||
| Deserialize<decltype(header.ident)>::fromNetworkByteStream(begin, end); | ||
|
|
||
| return make_pair(move(header), move(begin)); | ||
| } | ||
| }; | ||
|
|
||
| namespace detail | ||
| { | ||
|
|
||
| // Types that are only used in the sending/parsing of messages, not | ||
| // publicly exposed. | ||
| using ProtocolHeader = std::array<char, 8>; | ||
| const ProtocolHeader kProtocolHeader = {{'_', 'a', 's', 'd', 'p', '_', 'v', 1}}; | ||
|
|
||
| // Must have at least kMaxMessageSize bytes available in the output stream | ||
| template <typename NodeId, typename Payload, typename It> | ||
| It encodeMessage(NodeId from, | ||
| const uint8_t ttl, | ||
| const MessageType messageType, | ||
| const Payload& payload, | ||
| It out) | ||
| { | ||
| using namespace std; | ||
| const MessageHeader<NodeId> header = {messageType, ttl, 0, std::move(from)}; | ||
| const auto messageSize = | ||
| kProtocolHeader.size() + sizeInByteStream(header) + sizeInByteStream(payload); | ||
|
|
||
| if (messageSize < kMaxMessageSize) | ||
| { | ||
| return toNetworkByteStream( | ||
| payload, toNetworkByteStream( | ||
| header, copy(begin(kProtocolHeader), end(kProtocolHeader), move(out)))); | ||
| } | ||
| else | ||
| { | ||
| throw range_error("Exceeded maximum message size"); | ||
| } | ||
| } | ||
|
|
||
| } // namespace detail | ||
|
|
||
| template <typename NodeId, typename Payload, typename It> | ||
| It aliveMessage(NodeId from, const uint8_t ttl, const Payload& payload, It out) | ||
| { | ||
| return detail::encodeMessage(std::move(from), ttl, kAlive, payload, std::move(out)); | ||
| } | ||
|
|
||
| template <typename NodeId, typename Payload, typename It> | ||
| It responseMessage(NodeId from, const uint8_t ttl, const Payload& payload, It out) | ||
| { | ||
| return detail::encodeMessage(std::move(from), ttl, kResponse, payload, std::move(out)); | ||
| } | ||
|
|
||
| template <typename NodeId, typename It> | ||
| It byeByeMessage(NodeId from, It out) | ||
| { | ||
| return detail::encodeMessage( | ||
| std::move(from), 0, kByeBye, makePayload(), std::move(out)); | ||
| } | ||
|
|
||
| template <typename NodeId, typename It> | ||
| std::pair<MessageHeader<NodeId>, It> parseMessageHeader(It bytesBegin, const It bytesEnd) | ||
| { | ||
| using namespace std; | ||
| using ItDiff = typename iterator_traits<It>::difference_type; | ||
|
|
||
| MessageHeader<NodeId> header = {}; | ||
| const auto protocolHeaderSize = discovery::sizeInByteStream(detail::kProtocolHeader); | ||
| const auto minMessageSize = | ||
| static_cast<ItDiff>(protocolHeaderSize + sizeInByteStream(header)); | ||
|
|
||
| // If there are enough bytes in the stream to make a header and if | ||
| // the first bytes in the stream are the protocol header, then | ||
| // proceed to parse the stream. | ||
| if (distance(bytesBegin, bytesEnd) >= minMessageSize | ||
| && equal(begin(detail::kProtocolHeader), end(detail::kProtocolHeader), bytesBegin)) | ||
| { | ||
| tie(header, bytesBegin) = MessageHeader<NodeId>::fromNetworkByteStream( | ||
| bytesBegin + protocolHeaderSize, bytesEnd); | ||
| } | ||
| return make_pair(move(header), move(bytesBegin)); | ||
| } | ||
|
|
||
| } // namespace v1 | ||
| } // namespace discovery | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/discovery/NetworkByteStreamSerializable.hpp> | ||
| #include <cmath> | ||
| #include <cstdint> | ||
| #include <tuple> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace link | ||
| { | ||
|
|
||
| struct Beats : std::tuple<std::int64_t> | ||
| { | ||
| Beats() = default; | ||
|
|
||
| explicit Beats(const double beats) | ||
| : std::tuple<std::int64_t>(llround(beats * 1e6)) | ||
| { | ||
| } | ||
|
|
||
| explicit Beats(const std::int64_t microBeats) | ||
| : std::tuple<std::int64_t>(microBeats) | ||
| { | ||
| } | ||
|
|
||
| double floating() const | ||
| { | ||
| return microBeats() / 1e6; | ||
| } | ||
|
|
||
| std::int64_t microBeats() const | ||
| { | ||
| return std::get<0>(*this); | ||
| } | ||
|
|
||
| Beats operator-() const | ||
| { | ||
| return Beats{-microBeats()}; | ||
| } | ||
|
|
||
| friend Beats abs(const Beats b) | ||
| { | ||
| return Beats{std::abs(b.microBeats())}; | ||
| } | ||
|
|
||
| friend Beats operator+(const Beats lhs, const Beats rhs) | ||
| { | ||
| return Beats{lhs.microBeats() + rhs.microBeats()}; | ||
| } | ||
|
|
||
| friend Beats operator-(const Beats lhs, const Beats rhs) | ||
| { | ||
| return Beats{lhs.microBeats() - rhs.microBeats()}; | ||
| } | ||
|
|
||
| friend Beats operator%(const Beats lhs, const Beats rhs) | ||
| { | ||
| return rhs == Beats{0.} ? Beats{0.} : Beats{lhs.microBeats() % rhs.microBeats()}; | ||
| } | ||
|
|
||
| // Model the NetworkByteStreamSerializable concept | ||
| friend std::uint32_t sizeInByteStream(const Beats beats) | ||
| { | ||
| return discovery::sizeInByteStream(beats.microBeats()); | ||
| } | ||
|
|
||
| template <typename It> | ||
| friend It toNetworkByteStream(const Beats beats, It out) | ||
| { | ||
| return discovery::toNetworkByteStream(beats.microBeats(), std::move(out)); | ||
| } | ||
|
|
||
| template <typename It> | ||
| static std::pair<Beats, It> fromNetworkByteStream(It begin, It end) | ||
| { | ||
| auto result = discovery::Deserialize<std::int64_t>::fromNetworkByteStream( | ||
| std::move(begin), std::move(end)); | ||
| return std::make_pair(Beats{result.first}, std::move(result.second)); | ||
| } | ||
| }; | ||
|
|
||
| } // namespace link | ||
| } // namespace ableton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| /* Copyright 2016, Ableton AG, Berlin. All rights reserved. | ||
| * | ||
| * 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 <http://www.gnu.org/licenses/>. | ||
| * | ||
| * If you would like to incorporate Link into a proprietary software application, | ||
| * please contact <link-devs@ableton.com>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <ableton/link/GhostXForm.hpp> | ||
| #include <ableton/link/Timeline.hpp> | ||
|
|
||
| namespace ableton | ||
| { | ||
| namespace link | ||
| { | ||
|
|
||
| // Clamp the tempo of the given timeline to the valid Link range | ||
| inline Timeline clampTempo(const Timeline timeline) | ||
| { | ||
| const double kMinBpm = 20.0; | ||
| const double kMaxBpm = 999.0; | ||
| return {Tempo{(std::min)((std::max)(timeline.tempo.bpm(), kMinBpm), kMaxBpm)}, | ||
| timeline.beatOrigin, timeline.timeOrigin}; | ||
| } | ||
|
|
||
| // Given an existing client timeline, a session timeline, and the | ||
| // global host transform of the session, return a new version of the client | ||
| // timeline. The resulting new client timeline is continuous with the | ||
| // previous timeline so that curClient.toBeats(atTime) == | ||
| // result.toBeats(atTime). | ||
| inline Timeline updateClientTimelineFromSession(const Timeline curClient, | ||
| const Timeline session, | ||
| const std::chrono::microseconds atTime, | ||
| const GhostXForm xform) | ||
| { | ||
| // An intermediate timeline representing the continuation of the | ||
| // existing client timeline with the tempo from the session timeline. | ||
| const auto tempTl = Timeline{session.tempo, curClient.toBeats(atTime), atTime}; | ||
| // The host time corresponding to beat 0 on the session | ||
| // timeline. Beat 0 on the session timeline is important because it | ||
| // serves as the origin of the quantization grid for all participants. | ||
| const auto hostBeatZero = xform.ghostToHost(session.fromBeats(Beats{INT64_C(0)})); | ||
| // The new client timeline becomes the result of sliding the | ||
| // intermediate timeline back so that it's anchor corresponds to | ||
| // beat zero on the session timeline. The result preserves the | ||
| // magnitude of beats on the client timeline while encoding the | ||
| // quantization reference point in the time and beatOrigins. | ||
| return {tempTl.tempo, tempTl.toBeats(hostBeatZero), hostBeatZero}; | ||
| } | ||
|
|
||
|
|
||
| inline Timeline updateSessionTimelineFromClient(const Timeline curSession, | ||
| const Timeline client, | ||
| const std::chrono::microseconds atTime, | ||
| const GhostXForm xform) | ||
| { | ||
| // The client timeline was constructed so that it's timeOrigin | ||
| // corresponds to beat 0 on the session timeline. | ||
| const auto ghostBeat0 = xform.hostToGhost(client.timeOrigin); | ||
|
|
||
| const auto zero = Beats{INT64_C(0)}; | ||
| // If beat 0 was not shifted and there is not a new tempo, an update | ||
| // of the session timeline is not required. Don't create an | ||
| // equivalent timeline with different anchor points if not needed as | ||
| // this will trigger other unnecessary changes. | ||
| if (curSession.toBeats(ghostBeat0) == zero && client.tempo == curSession.tempo) | ||
| { | ||
| return curSession; | ||
| } | ||
| else | ||
| { | ||
| // An intermediate timeline representing the new tempo, the | ||
| // effective time, and a possibly adjusted origin. | ||
| const auto tempTl = Timeline{client.tempo, zero, ghostBeat0}; | ||
| // The final session timeline must have the beat corresponding to | ||
| // atTime on the old session timeline as its beatOrigin because this is | ||
| // used for prioritization of timelines among peers - we can't let a | ||
| // modification applied by the client artificially increase or | ||
| // reduce the timeline's priority in the session. The new beat | ||
| // origin should be as close as possible to lining up with atTime, | ||
| // but we must also make sure that it's > curSession.beatOrigin | ||
| // because otherwise it will get ignored. | ||
| const auto newBeatOrigin = (std::max)(curSession.toBeats(xform.hostToGhost(atTime)), | ||
| curSession.beatOrigin + Beats{INT64_C(1)}); | ||
| return {client.tempo, newBeatOrigin, tempTl.fromBeats(newBeatOrigin)}; | ||
| } | ||
| } | ||
|
|
||
| // Shift timeline so result.toBeats(t) == client.toBeats(t) + | ||
| // shift. This takes into account the fact that the timeOrigin | ||
| // corresponds to beat 0 on the session timeline. Using this function | ||
| // and then setting the session timeline with the result will change | ||
| // the phase of the session by the given shift amount. | ||
| inline Timeline shiftClientTimeline(Timeline client, const Beats shift) | ||
| { | ||
| const auto timeDelta = client.fromBeats(shift) - client.fromBeats(Beats{INT64_C(0)}); | ||
| client.timeOrigin = client.timeOrigin - timeDelta; | ||
| return client; | ||
| } | ||
|
|
||
| } // link | ||
| } // ableton |