/* 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 .
*
* If you would like to incorporate Link into a proprietary software application,
* please contact .
*/
#pragma once
#include
#include
#include
namespace ableton
{
namespace link
{
struct SessionMeasurement
{
GhostXForm xform;
std::chrono::microseconds timestamp;
};
struct Session
{
SessionId sessionId;
Timeline timeline;
SessionMeasurement measurement;
};
template
class Sessions
{
public:
using Timer = typename util::Injected::type::Timer;
Sessions(Session init,
util::Injected peers,
MeasurePeer measure,
JoinSessionCallback join,
util::Injected io,
Clock clock)
: mPeers(std::move(peers))
, mMeasure(std::move(measure))
, mCallback(std::move(join))
, mCurrent(std::move(init))
, mIo(std::move(io))
, mTimer(mIo->makeTimer())
, mClock(std::move(clock))
{
}
void resetSession(Session session)
{
mCurrent = std::move(session);
mOtherSessions.clear();
}
void resetTimeline(Timeline timeline)
{
mCurrent.timeline = std::move(timeline);
}
// Consider the observed session/timeline pair and return a possibly
// new timeline that should be used going forward.
Timeline sawSessionTimeline(SessionId sid, Timeline timeline)
{
using namespace std;
if (sid == mCurrent.sessionId)
{
// matches our current session, update the timeline if necessary
updateTimeline(mCurrent, std::move(timeline));
}
else
{
auto session = Session{std::move(sid), std::move(timeline), {}};
const auto range =
equal_range(begin(mOtherSessions), end(mOtherSessions), session, SessionIdComp{});
if (range.first == range.second)
{
// brand new session, insert it into our list of known
// sessions and launch a measurement
launchSessionMeasurement(session);
mOtherSessions.insert(range.first, std::move(session));
}
else
{
// we've seen this session before, update its timeline if necessary
updateTimeline(*range.first, std::move(timeline));
}
}
return mCurrent.timeline;
}
private:
void launchSessionMeasurement(Session& session)
{
using namespace std;
auto peers = mPeers->sessionPeers(session.sessionId);
if (!peers.empty())
{
// first criteria: always prefer the founding peer
const auto it = find_if(begin(peers), end(peers),
[&session](const Peer& peer) { return session.sessionId == peer.first.ident(); });
// TODO: second criteria should be degree. We don't have that
// represented yet so just use the first peer for now
auto peer = it == end(peers) ? peers.front() : *it;
// mark that a session is in progress by clearing out the
// session's timestamp
session.measurement.timestamp = {};
mMeasure(std::move(peer), MeasurementResultsHandler{*this, session.sessionId});
}
}
void handleSuccessfulMeasurement(const SessionId& id, GhostXForm xform)
{
using namespace std;
debug(mIo->log()) << "Session " << id << " measurement completed with result "
<< "(" << xform.slope << ", " << xform.intercept.count() << ")";
auto measurement = SessionMeasurement{std::move(xform), mClock.micros()};
if (mCurrent.sessionId == id)
{
mCurrent.measurement = std::move(measurement);
mCallback(mCurrent);
}
else
{
const auto range = equal_range(
begin(mOtherSessions), end(mOtherSessions), Session{id, {}, {}}, SessionIdComp{});
if (range.first != range.second)
{
const auto SESSION_EPS = chrono::microseconds{500000};
// should we join this session?
const auto hostTime = mClock.micros();
const auto curGhost = mCurrent.measurement.xform.hostToGhost(hostTime);
const auto newGhost = measurement.xform.hostToGhost(hostTime);
// update the measurement for the session entry
range.first->measurement = std::move(measurement);
// If session times too close - fall back to session id order
const auto ghostDiff = newGhost - curGhost;
if (ghostDiff > SESSION_EPS
|| (std::abs(ghostDiff.count()) < SESSION_EPS.count()
&& id < mCurrent.sessionId))
{
// The new session wins, switch over to it
auto current = mCurrent;
mCurrent = std::move(*range.first);
mOtherSessions.erase(range.first);
// Put the old current session back into our list of known
// sessions so that we won't re-measure it
const auto it = upper_bound(
begin(mOtherSessions), end(mOtherSessions), current, SessionIdComp{});
mOtherSessions.insert(it, std::move(current));
// And notify that we have a new session and make sure that
// we remeasure it periodically.
mCallback(mCurrent);
scheduleRemeasurement();
}
}
}
}
void scheduleRemeasurement()
{
// set a timer to re-measure the active session after a period
mTimer.expires_from_now(std::chrono::microseconds{30000000});
mTimer.async_wait([this](const typename Timer::ErrorCode e) {
if (!e)
{
launchSessionMeasurement(mCurrent);
scheduleRemeasurement();
}
});
}
void handleFailedMeasurement(const SessionId& id)
{
using namespace std;
debug(mIo->log()) << "Session " << id << " measurement failed.";
// if we failed to measure for our current session, schedule a
// retry in the future. Otherwise, remove the session from our set
// of known sessions (if it is seen again it will be measured as
// if new).
if (mCurrent.sessionId == id)
{
scheduleRemeasurement();
}
else
{
const auto range = equal_range(
begin(mOtherSessions), end(mOtherSessions), Session{id, {}, {}}, SessionIdComp{});
if (range.first != range.second)
{
mOtherSessions.erase(range.first);
mPeers->forgetSession(id);
}
}
}
void updateTimeline(Session& session, Timeline timeline)
{
// We use beat origin magnitude to prioritize sessions.
if (timeline.beatOrigin > session.timeline.beatOrigin)
{
debug(mIo->log()) << "Adopting peer timeline (" << timeline.tempo.bpm() << ", "
<< timeline.beatOrigin.floating() << ", "
<< timeline.timeOrigin.count() << ")";
session.timeline = std::move(timeline);
}
else
{
debug(mIo->log()) << "Rejecting peer timeline with beat origin: "
<< timeline.beatOrigin.floating()
<< ". Current timeline beat origin: "
<< session.timeline.beatOrigin.floating();
}
}
struct MeasurementResultsHandler
{
void operator()(GhostXForm xform) const
{
Sessions& sessions = mSessions;
const SessionId& sessionId = mSessionId;
if (xform == GhostXForm{})
{
sessions.handleFailedMeasurement(std::move(sessionId));
}
else
{
sessions.handleSuccessfulMeasurement(std::move(sessionId), std::move(xform));
}
}
Sessions& mSessions;
SessionId mSessionId;
};
struct SessionIdComp
{
bool operator()(const Session& lhs, const Session& rhs) const
{
return lhs.sessionId < rhs.sessionId;
}
};
using Peer = typename util::Injected::type::Peer;
util::Injected mPeers;
MeasurePeer mMeasure;
JoinSessionCallback mCallback;
Session mCurrent;
util::Injected mIo;
Timer mTimer;
Clock mClock;
std::vector mOtherSessions; // sorted/unique by session id
};
template
Sessions makeSessions(
Session init,
util::Injected peers,
MeasurePeer measure,
JoinSessionCallback join,
util::Injected io,
Clock clock)
{
using namespace std;
return {std::move(init), std::move(peers), std::move(measure), std::move(join),
std::move(io), std::move(clock)};
}
} // namespace link
} // namespace ableton