67 changes: 44 additions & 23 deletions link/ableton/Link.ipp
Expand Up @@ -24,10 +24,10 @@
namespace ableton
{

inline Link::Link(const double bpm)
template <typename Clock>
inline BasicLink<Clock>::BasicLink(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);
Expand All @@ -42,81 +42,97 @@ inline Link::Link(const double bpm)
{
}

inline bool Link::isEnabled() const
template <typename Clock>
inline bool BasicLink<Clock>::isEnabled() const
{
return mController.isEnabled();
}

inline void Link::enable(const bool bEnable)
template <typename Clock>
inline void BasicLink<Clock>::enable(const bool bEnable)
{
mController.enable(bEnable);
}

inline std::size_t Link::numPeers() const
template <typename Clock>
inline std::size_t BasicLink<Clock>::numPeers() const
{
return mController.numPeers();
}

template <typename Clock>
template <typename Callback>
void Link::setNumPeersCallback(Callback callback)
void BasicLink<Clock>::setNumPeersCallback(Callback callback)
{
std::lock_guard<std::mutex> lock(mCallbackMutex);
mPeerCountCallback = [callback](const std::size_t numPeers) { callback(numPeers); };
}

template <typename Clock>
template <typename Callback>
void Link::setTempoCallback(Callback callback)
void BasicLink<Clock>::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
template <typename Clock>
inline Clock BasicLink<Clock>::clock() const
{
return mClock;
}

inline Link::Timeline Link::captureAudioTimeline() const
template <typename Clock>
inline typename BasicLink<Clock>::Timeline BasicLink<Clock>::captureAudioTimeline() const
{
return Link::Timeline{mController.timelineRtSafe(), numPeers() > 0};
return BasicLink<Clock>::Timeline{mController.timelineRtSafe(), numPeers() > 0};
}

inline void Link::commitAudioTimeline(const Link::Timeline timeline)
template <typename Clock>
inline void BasicLink<Clock>::commitAudioTimeline(const Timeline timeline)
{
if (timeline.mOriginalTimeline != timeline.mTimeline)
{
mController.setTimelineRtSafe(timeline.mTimeline, mClock.micros());
}
}

inline Link::Timeline Link::captureAppTimeline() const
template <typename Clock>
inline typename BasicLink<Clock>::Timeline BasicLink<Clock>::captureAppTimeline() const
{
return Link::Timeline{mController.timeline(), numPeers() > 0};
return Timeline{mController.timeline(), numPeers() > 0};
}

inline void Link::commitAppTimeline(const Link::Timeline timeline)
template <typename Clock>
inline void BasicLink<Clock>::commitAppTimeline(const Timeline timeline)
{
if (timeline.mOriginalTimeline != timeline.mTimeline)
{
mController.setTimeline(timeline.mTimeline, mClock.micros());
}
}

// Link::Timeline
////////////////////
// Link::Timeline //
////////////////////

inline Link::Timeline::Timeline(const link::Timeline timeline, const bool bRespectQuantum)
template <typename Clock>
inline BasicLink<Clock>::Timeline::Timeline(
const link::Timeline timeline, const bool bRespectQuantum)
: mOriginalTimeline(timeline)
, mbRespectQuantum(bRespectQuantum)
, mTimeline(timeline)
{
}

inline double Link::Timeline::tempo() const
template <typename Clock>
inline double BasicLink<Clock>::Timeline::tempo() const
{
return mTimeline.tempo.bpm();
}

inline void Link::Timeline::setTempo(
template <typename Clock>
inline void BasicLink<Clock>::Timeline::setTempo(
const double bpm, const std::chrono::microseconds atTime)
{
const auto desiredTl =
Expand All @@ -125,26 +141,30 @@ inline void Link::Timeline::setTempo(
mTimeline.timeOrigin = desiredTl.fromBeats(mTimeline.beatOrigin);
}

inline double Link::Timeline::beatAtTime(
template <typename Clock>
inline double BasicLink<Clock>::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(
template <typename Clock>
inline double BasicLink<Clock>::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(
template <typename Clock>
inline std::chrono::microseconds BasicLink<Clock>::Timeline::timeAtBeat(
const double beat, const double quantum) const
{
return link::fromPhaseEncodedBeats(mTimeline, link::Beats{beat}, link::Beats{quantum});
}

inline void Link::Timeline::requestBeatAtTime(
template <typename Clock>
inline void BasicLink<Clock>::Timeline::requestBeatAtTime(
const double beat, std::chrono::microseconds time, const double quantum)
{
if (mbRespectQuantum)
Expand All @@ -157,7 +177,8 @@ inline void Link::Timeline::requestBeatAtTime(
forceBeatAtTime(beat, time, quantum);
}

inline void Link::Timeline::forceBeatAtTime(
template <typename Clock>
inline void BasicLink<Clock>::Timeline::forceBeatAtTime(
const double beat, const std::chrono::microseconds time, const double quantum)
{
// There are two components to the beat adjustment: a phase shift
Expand Down
113 changes: 113 additions & 0 deletions link/ableton/link/CircularFifo.hpp
@@ -0,0 +1,113 @@
/* 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 <array>
#include <atomic>
#include <cassert>

namespace ableton
{
namespace link
{

// Single producer, single consumer lockfree Fifo

template <typename Type, std::size_t size>
class CircularFifo
{
public:
struct PoppedItem
{
Type item;
bool valid;
};

CircularFifo()
: tail(0)
, head(0)
{
assert(head.is_lock_free() && tail.is_lock_free());
}

bool push(Type item)
{
const auto currentTail = tail.load();
const auto nextTail = nextIndex(currentTail);
if (nextTail != head.load())
{
data[currentTail] = std::move(item);
tail.store(nextTail);
return true;
}
return false;
}

PoppedItem pop()
{
const auto currentHead = head.load();
if (currentHead == tail.load())
{
return {Type{}, false};
}

auto item = data[currentHead];
head.store(nextIndex(currentHead));
return {std::move(item), true};
}

PoppedItem clearAndPopLast()
{
auto hasData = false;
auto currentHead = head.load();
while (currentHead != tail.load())
{
currentHead = nextIndex(currentHead);
hasData = true;
}

auto item = data[previousIndex(currentHead)];
head.store(currentHead);
return {std::move(item), hasData};
}

bool isEmpty() const
{
return tail == head;
}

private:
size_t nextIndex(const size_t index) const
{
return (index + 1) % (size + 1);
}

size_t previousIndex(const size_t index) const
{
return (index + size) % (size + 1);
}

std::atomic_size_t tail;
std::atomic_size_t head;
std::array<Type, size + 1> data;
};

} // link
} // ableton
107 changes: 83 additions & 24 deletions link/ableton/link/Controller.hpp
Expand Up @@ -20,6 +20,7 @@
#pragma once

#include <ableton/discovery/Service.hpp>
#include <ableton/link/CircularFifo.hpp>
#include <ableton/link/ClientSessionTimelines.hpp>
#include <ableton/link/Gateway.hpp>
#include <ableton/link/GhostXForm.hpp>
Expand All @@ -45,7 +46,8 @@ GhostXForm initXForm(const Clock& clock)

// The timespan in which local modifications to the timeline will be
// preferred over any modifications coming from the network.
const auto kLocalModGracePeriod = std::chrono::seconds(1);
const auto kLocalModGracePeriod = std::chrono::milliseconds(1000);
const auto kRtHandlerFallbackPeriod = kLocalModGracePeriod / 2;

} // namespace detail

Expand All @@ -61,8 +63,6 @@ template <typename PeerCountCallback,
class Controller
{
public:
using Ticks = typename Clock::Ticks;

Controller(Tempo tempo,
PeerCountCallback peerCallback,
TempoCallback tempoCallback,
Expand All @@ -81,7 +81,7 @@ class Controller
, mSessionPeerCounter(*this, std::move(peerCallback))
, mEnabled(false)
, mIo(std::move(io))
, mRealtimeIo(util::injectRef(*mIo))
, mRtTimelineSetter(*this)
, mPeers(util::injectRef(*mIo),
std::ref(mSessionPeerCounter),
SessionTimelineCallback{*this})
Expand Down Expand Up @@ -109,7 +109,7 @@ class Controller
const bool bWasEnabled = mEnabled.exchange(bEnable);
if (bWasEnabled != bEnable)
{
mRealtimeIo.async([this, bEnable] {
mIo->async([this, bEnable] {
if (bEnable)
{
// Always reset when first enabling to avoid hijacking
Expand Down Expand Up @@ -164,7 +164,7 @@ class Controller
// cached version of the timeline.
const auto now = mClock.micros();
if (now - mRtClientTimelineTimestamp > detail::kLocalModGracePeriod
&& mSessionTimingGuard.try_lock())
&& !mRtTimelineSetter.hasPendingTimelines() && mSessionTimingGuard.try_lock())
{
const auto clientTimeline = updateClientTimelineFromSession(
mRtClientTimeline, mSessionTimeline, now, mGhostXForm);
Expand All @@ -184,21 +184,17 @@ class Controller
void setTimelineRtSafe(Timeline newTimeline, const std::chrono::microseconds atTime)
{
newTimeline = clampTempo(newTimeline);
// Cache the new timeline for serving back to the client
mRtClientTimeline = newTimeline;
mRtClientTimelineTimestamp =
isEnabled() ? mClock.micros() : std::chrono::microseconds(0);

// Update the session timeline from the new client timeline
mRealtimeIo.async([this, newTimeline, atTime] {
// Synchronize with the non-rt version of the client timeline
{
std::lock_guard<std::mutex> lock(mClientTimelineGuard);
mClientTimeline = newTimeline;
}
handleTimelineFromClient(updateSessionTimelineFromClient(
mSessionTimeline, newTimeline, atTime, mGhostXForm));
});

// This will fail in case the Fifo in the RtTimelineSetter is full. This indicates a
// very high rate of calls to the setter. In this case we ignore one value because we
// expect the setter to be called again soon.
if (mRtTimelineSetter.tryPush(newTimeline, atTime))
{
// Cache the new timeline for serving back to the client
mRtClientTimeline = newTimeline;
mRtClientTimelineTimestamp =
isEnabled() ? mClock.micros() : std::chrono::microseconds(0);
}
}

private:
Expand Down Expand Up @@ -248,6 +244,16 @@ class Controller
mSessions.sawSessionTimeline(std::move(id), std::move(timeline)), mGhostXForm);
}

void handleRtTimeline(const Timeline timeline, const std::chrono::microseconds time)
{
{
std::lock_guard<std::mutex> lock(mClientTimelineGuard);
mClientTimeline = timeline;
}
handleTimelineFromClient(
updateSessionTimelineFromClient(mSessionTimeline, timeline, time, mGhostXForm));
}

void joinSession(const Session& session)
{
const bool sessionIdChanged = mSessionId != session.sessionId;
Expand Down Expand Up @@ -293,6 +299,60 @@ class Controller
Controller& mController;
};

struct RtTimelineSetter
{
using CallbackDispatcher =
typename IoContext::template LockFreeCallbackDispatcher<std::function<void()>,
std::chrono::milliseconds>;
using RtTimeline = std::pair<Timeline, std::chrono::microseconds>;

RtTimelineSetter(Controller& controller)
: mController(controller)
, mHasPendingTimelines(false)
, mCallbackDispatcher(
[this] { processPendingTimelines(); }, detail::kRtHandlerFallbackPeriod)
{
}

bool tryPush(const Timeline timeline, const std::chrono::microseconds time)
{
mHasPendingTimelines = true;
const auto success = mFifo.push({timeline, time});
if (success)
{
mCallbackDispatcher.invoke();
}
return success;
}

bool hasPendingTimelines() const
{
return mHasPendingTimelines;
}

private:
void processPendingTimelines()
{
auto result = mFifo.clearAndPopLast();

if (result.valid)
{
auto timeline = std::move(result.item);
mController.mIo->async([this, timeline]() {
mController.handleRtTimeline(timeline.first, timeline.second);
mHasPendingTimelines = false;
});
}
}

Controller& mController;
// Assuming a wake up time of one ms for the threads owned by the CallbackDispatcher
// and the ioService, buffering 16 timelines allows to set eight timelines per ms.
CircularFifo<RtTimeline, 16> mFifo;
std::atomic<bool> mHasPendingTimelines;
CallbackDispatcher mCallbackDispatcher;
};

struct SessionPeerCounter
{
SessionPeerCounter(Controller& controller, PeerCountCallback callback)
Expand Down Expand Up @@ -424,9 +484,8 @@ class Controller
std::atomic<bool> mEnabled;

util::Injected<IoContext> mIo;
// A realtime facade over the provided IoContext. This should only
// be used by realtime code, non-realtime code should use mIo.
typename IoType::template RealTimeContext<IoType&> mRealtimeIo;

RtTimelineSetter mRtTimelineSetter;

ControllerPeers mPeers;

Expand Down
37 changes: 10 additions & 27 deletions link/ableton/link/Kalman.hpp
Expand Up @@ -22,15 +22,7 @@
#include <array>
#include <cfloat>
#include <cmath>
#include <limits>

#if LINK_PLATFORM_WINDOWS
// Windows.h (or more specifically, minwindef.h) define the max(a, b) macro
// which conflicts with the symbol provided by std::numeric_limits.
#ifdef max
#undef max
#endif
#endif

namespace ableton
{
Expand All @@ -42,9 +34,6 @@ struct Kalman
{
Kalman()
: mValue(0)
, mGain(0)
, mVVariance(1)
, mWVariance(1)
, mCoVariance(1)
, mVarianceLength(n)
, mCounter(mVarianceLength)
Expand Down Expand Up @@ -124,31 +113,25 @@ struct Kalman
// prediction equations
const double prevFilterValue = mFilterValues[(mCounter - 1) % mVarianceLength];
mFilterValues[currentIndex] = prevFilterValue;
mWVariance = calculateWVariance();
const double coVarianceEstimation = mCoVariance + mWVariance;
const auto wVariance = calculateWVariance();
const double coVarianceEstimation = mCoVariance + wVariance;

// update equations
mVVariance = calculateVVariance();
if ((coVarianceEstimation + mVVariance) != 0)
{
mGain = coVarianceEstimation / (coVarianceEstimation + mVVariance);
}
else
{
mGain = std::numeric_limits<double>::max();
}
mValue = prevFilterValue + mGain * (value - prevFilterValue);
mCoVariance = (1 - mGain) * coVarianceEstimation;
const auto vVariance = calculateVVariance();
// Gain defines how easily the filter will adjust to a new condition
// With gain = 1 the output equals the input, with gain = 0 the input
// is ignored and the output equals the last filtered value
const auto divisor = coVarianceEstimation + vVariance;
const auto gain = divisor != 0. ? coVarianceEstimation / divisor : 0.7;
mValue = prevFilterValue + gain * (value - prevFilterValue);
mCoVariance = (1 - gain) * coVarianceEstimation;
}
mFilterValues[currentIndex] = mValue;

++mCounter;
}

double mValue;
double mGain;
double mVVariance;
double mWVariance;
double mCoVariance;
size_t mVarianceLength;
size_t mCounter;
Expand Down
8 changes: 6 additions & 2 deletions link/ableton/platforms/Config.hpp
Expand Up @@ -32,8 +32,8 @@
#include <ableton/platforms/posix/ScanIpIfAddrs.hpp>
#elif LINK_PLATFORM_LINUX
#include <ableton/platforms/asio/Context.hpp>
#include <ableton/platforms/linux/Clock.hpp>
#include <ableton/platforms/posix/ScanIpIfAddrs.hpp>
#include <ableton/platforms/stl/Clock.hpp>
#endif

namespace ableton
Expand All @@ -54,7 +54,11 @@ using IoContext =
platforms::asio::Context<platforms::posix::ScanIpIfAddrs, util::NullLog>;

#elif LINK_PLATFORM_LINUX
using Clock = platforms::stl::Clock;
#ifdef __ARM_ARCH_7A__
using Clock = platforms::linux::ClockMonotonicRaw;
#else
using Clock = platforms::linux::ClockMonotonic;
#endif
using IoContext =
platforms::asio::Context<platforms::posix::ScanIpIfAddrs, util::NullLog>;
#endif
Expand Down
8 changes: 4 additions & 4 deletions link/ableton/platforms/asio/Context.hpp
Expand Up @@ -22,7 +22,7 @@
#include <ableton/discovery/IpV4Interface.hpp>
#include <ableton/platforms/asio/AsioTimer.hpp>
#include <ableton/platforms/asio/AsioWrapper.hpp>
#include <ableton/platforms/asio/PooledHandlerContext.hpp>
#include <ableton/platforms/asio/LockFreeCallbackDispatcher.hpp>
#include <ableton/platforms/asio/Socket.hpp>
#include <thread>

Expand All @@ -40,12 +40,12 @@ class Context
using Timer = AsioTimer;
using Log = LogT;

template <typename Handler, typename Duration>
using LockFreeCallbackDispatcher = LockFreeCallbackDispatcher<Handler, Duration>;

template <std::size_t BufferSize>
using Socket = asio::Socket<BufferSize>;

template <typename IoContext>
using RealTimeContext = PooledHandlerContext<IoContext>;

Context()
: Context(DefaultHandler{})
{
Expand Down
89 changes: 89 additions & 0 deletions link/ableton/platforms/asio/LockFreeCallbackDispatcher.hpp
@@ -0,0 +1,89 @@
/* 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 <atomic>
#include <condition_variable>
#include <thread>

namespace ableton
{
namespace platforms
{

// Utility to signal invocation of a callback on another thread in a lock free manner.
// The callback is evoked on a thread owned by the instance of this class.
//
// A condition variable is used to notify a waiting thread, but only if the required
// lock can be acquired immediately. If that fails, we fall back on signaling
// after a timeout. This gives us a guaranteed minimum signalling rate which is defined
// by the fallbackPeriod parameter.

template <typename Callback, typename Duration>
class LockFreeCallbackDispatcher
{
public:
LockFreeCallbackDispatcher(Callback callback, Duration fallbackPeriod)
: mCallback(std::move(callback))
, mFallbackPeriod(std::move(fallbackPeriod))
, mRunning(true)
, mThread([this] { run(); })
{
}

~LockFreeCallbackDispatcher()
{
mRunning = false;
mCondition.notify_one();
mThread.join();
}

void invoke()
{
if (mMutex.try_lock())
{
mCondition.notify_one();
mMutex.unlock();
}
}

private:
void run()
{
while (mRunning.load())
{
{
std::unique_lock<std::mutex> lock(mMutex);
mCondition.wait_for(lock, mFallbackPeriod);
}
mCallback();
}
}

Callback mCallback;
Duration mFallbackPeriod;
std::atomic<bool> mRunning;
std::mutex mMutex;
std::condition_variable mCondition;
std::thread mThread;
};

} // platforms
} // ableton
115 changes: 0 additions & 115 deletions link/ableton/platforms/asio/PooledHandlerContext.hpp

This file was deleted.

64 changes: 64 additions & 0 deletions link/ableton/platforms/linux/Clock.hpp
@@ -0,0 +1,64 @@
/* 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 <chrono>
#include <cmath>
#include <ctime>

namespace ableton
{
namespace platforms
{

#ifdef linux
#undef linux
#endif

namespace linux
{

template <clockid_t CLOCK>
class Clock
{
public:
std::chrono::microseconds micros() const
{
::timespec ts;
::clock_gettime(CLOCK, &ts);
std::uint64_t ns = ts.tv_sec * 1000000000ULL + ts.tv_nsec;
return std::chrono::microseconds(ns / 1000ULL);
}
};

using ClockMonotonic = Clock<CLOCK_MONOTONIC>;
using ClockMonotonicRaw = Clock<CLOCK_MONOTONIC_RAW>;

/* Warning: the realtime clock can be subject to time change.
* One of the hard requirements of Ableton Link is that the clock must be
* monotonic.
* Only use the Realtime Clock if you can't use the monotonic ones, and
* beware that things might go wrong if time jumps.
*/
using ClockRealtime = Clock<CLOCK_REALTIME>;

} // namespace linux
} // namespace platforms
} // namespace ableton
12 changes: 2 additions & 10 deletions link/ableton/platforms/stl/Clock.hpp
Expand Up @@ -30,20 +30,12 @@ namespace stl

struct Clock
{
using Ticks = std::uint64_t;

Clock()
{
mStartTime = std::chrono::high_resolution_clock::now();
}

std::chrono::microseconds micros() const
{
using namespace std::chrono;
return duration_cast<microseconds>(high_resolution_clock::now() - mStartTime);
auto nowInMicros = time_point_cast<microseconds>(steady_clock::now());
return nowInMicros.time_since_epoch();
}

std::chrono::high_resolution_clock::time_point mStartTime;
};

} // namespace stl
Expand Down