306 changes: 306 additions & 0 deletions link/ableton/Link.hpp
@@ -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>
173 changes: 173 additions & 0 deletions link/ableton/Link.ipp
@@ -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
103 changes: 103 additions & 0 deletions link/ableton/discovery/InterfaceScanner.hpp
@@ -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
123 changes: 123 additions & 0 deletions link/ableton/discovery/IpV4Interface.hpp
@@ -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
50 changes: 50 additions & 0 deletions link/ableton/discovery/MessageTypes.hpp
@@ -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
405 changes: 405 additions & 0 deletions link/ableton/discovery/NetworkByteStreamSerializable.hpp

Large diffs are not rendered by default.

293 changes: 293 additions & 0 deletions link/ableton/discovery/Payload.hpp
@@ -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
253 changes: 253 additions & 0 deletions link/ableton/discovery/PeerGateway.hpp
@@ -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
229 changes: 229 additions & 0 deletions link/ableton/discovery/PeerGateways.hpp
@@ -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
72 changes: 72 additions & 0 deletions link/ableton/discovery/Service.hpp
@@ -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
139 changes: 139 additions & 0 deletions link/ableton/discovery/Socket.hpp
@@ -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
330 changes: 330 additions & 0 deletions link/ableton/discovery/UdpMessenger.hpp
@@ -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
75 changes: 75 additions & 0 deletions link/ableton/discovery/test/Interface.hpp
@@ -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
98 changes: 98 additions & 0 deletions link/ableton/discovery/test/PayloadEntries.hpp
@@ -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
83 changes: 83 additions & 0 deletions link/ableton/discovery/test/Socket.hpp
@@ -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
168 changes: 168 additions & 0 deletions link/ableton/discovery/v1/Messages.hpp
@@ -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
103 changes: 103 additions & 0 deletions link/ableton/link/Beats.hpp
@@ -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
115 changes: 115 additions & 0 deletions link/ableton/link/ClientSessionTimelines.hpp
@@ -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