1 /* Copyright 2016, Ableton AG, Berlin. All rights reserved.
2  *
3  *  This program is free software: you can redistribute it and/or modify
4  *  it under the terms of the GNU General Public License as published by
5  *  the Free Software Foundation, either version 2 of the License, or
6  *  (at your option) any later version.
7  *
8  *  This program is distributed in the hope that it will be useful,
9  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
10  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11  *  GNU General Public License for more details.
12  *
13  *  You should have received a copy of the GNU General Public License
14  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
15  *
16  *  If you would like to incorporate Link into a proprietary software application,
17  *  please contact <link-devs@ableton.com>.
18  */
19 
20 #pragma once
21 
22 #include <ableton/link/GhostXForm.hpp>
23 #include <ableton/link/SessionId.hpp>
24 #include <ableton/link/Timeline.hpp>
25 
26 namespace ableton
27 {
28 namespace link
29 {
30 
31 struct SessionMeasurement
32 {
33   GhostXForm xform;
34   std::chrono::microseconds timestamp;
35 };
36 
37 struct Session
38 {
39   SessionId sessionId;
40   Timeline timeline;
41   SessionMeasurement measurement;
42 };
43 
44 template <typename Peers,
45   typename MeasurePeer,
46   typename JoinSessionCallback,
47   typename IoContext,
48   typename Clock>
49 class Sessions
50 {
51 public:
52   using Timer = typename util::Injected<IoContext>::type::Timer;
53 
Sessions(Session init,util::Injected<Peers> peers,MeasurePeer measure,JoinSessionCallback join,util::Injected<IoContext> io,Clock clock)54   Sessions(Session init,
55     util::Injected<Peers> peers,
56     MeasurePeer measure,
57     JoinSessionCallback join,
58     util::Injected<IoContext> io,
59     Clock clock)
60     : mPeers(std::move(peers))
61     , mMeasure(std::move(measure))
62     , mCallback(std::move(join))
63     , mCurrent(std::move(init))
64     , mIo(std::move(io))
65     , mTimer(mIo->makeTimer())
66     , mClock(std::move(clock))
67   {
68   }
69 
resetSession(Session session)70   void resetSession(Session session)
71   {
72     mCurrent = std::move(session);
73     mOtherSessions.clear();
74   }
75 
resetTimeline(Timeline timeline)76   void resetTimeline(Timeline timeline)
77   {
78     mCurrent.timeline = std::move(timeline);
79   }
80 
81   // Consider the observed session/timeline pair and return a possibly
82   // new timeline that should be used going forward.
sawSessionTimeline(SessionId sid,Timeline timeline)83   Timeline sawSessionTimeline(SessionId sid, Timeline timeline)
84   {
85     using namespace std;
86     if (sid == mCurrent.sessionId)
87     {
88       // matches our current session, update the timeline if necessary
89       updateTimeline(mCurrent, move(timeline));
90     }
91     else
92     {
93       auto session = Session{move(sid), move(timeline), {}};
94       const auto range =
95         equal_range(begin(mOtherSessions), end(mOtherSessions), session, SessionIdComp{});
96       if (range.first == range.second)
97       {
98         // brand new session, insert it into our list of known
99         // sessions and launch a measurement
100         launchSessionMeasurement(session);
101         mOtherSessions.insert(range.first, move(session));
102       }
103       else
104       {
105         // we've seen this session before, update its timeline if necessary
106         updateTimeline(*range.first, move(timeline));
107       }
108     }
109     return mCurrent.timeline;
110   }
111 
112 private:
launchSessionMeasurement(Session & session)113   void launchSessionMeasurement(Session& session)
114   {
115     using namespace std;
116     auto peers = mPeers->sessionPeers(session.sessionId);
117     if (!peers.empty())
118     {
119       // first criteria: always prefer the founding peer
120       const auto it = find_if(begin(peers), end(peers),
121         [&session](const Peer& peer) { return session.sessionId == peer.first.ident(); });
122       // TODO: second criteria should be degree. We don't have that
123       // represented yet so just use the first peer for now
124       auto peer = it == end(peers) ? peers.front() : *it;
125       // mark that a session is in progress by clearing out the
126       // session's timestamp
127       session.measurement.timestamp = {};
128       mMeasure(move(peer), MeasurementResultsHandler{*this, session.sessionId});
129     }
130   }
131 
handleSuccessfulMeasurement(const SessionId & id,GhostXForm xform)132   void handleSuccessfulMeasurement(const SessionId& id, GhostXForm xform)
133   {
134     using namespace std;
135 
136     debug(mIo->log()) << "Session " << id << " measurement completed with result "
137                       << "(" << xform.slope << ", " << xform.intercept.count() << ")";
138 
139     auto measurement = SessionMeasurement{move(xform), mClock.micros()};
140 
141     if (mCurrent.sessionId == id)
142     {
143       mCurrent.measurement = move(measurement);
144       mCallback(mCurrent);
145     }
146     else
147     {
148       const auto range = equal_range(
149         begin(mOtherSessions), end(mOtherSessions), Session{id, {}, {}}, SessionIdComp{});
150 
151       if (range.first != range.second)
152       {
153         const auto SESSION_EPS = chrono::microseconds{500000};
154         // should we join this session?
155         const auto hostTime = mClock.micros();
156         const auto curGhost = mCurrent.measurement.xform.hostToGhost(hostTime);
157         const auto newGhost = measurement.xform.hostToGhost(hostTime);
158         // update the measurement for the session entry
159         range.first->measurement = move(measurement);
160         // If session times too close - fall back to session id order
161         const auto ghostDiff = newGhost - curGhost;
162         if (ghostDiff > SESSION_EPS
163             || (std::abs(ghostDiff.count()) < SESSION_EPS.count()
164                  && id < mCurrent.sessionId))
165         {
166           // The new session wins, switch over to it
167           auto current = mCurrent;
168           mCurrent = move(*range.first);
169           mOtherSessions.erase(range.first);
170           // Put the old current session back into our list of known
171           // sessions so that we won't re-measure it
172           const auto it = upper_bound(
173             begin(mOtherSessions), end(mOtherSessions), current, SessionIdComp{});
174           mOtherSessions.insert(it, move(current));
175           // And notify that we have a new session and make sure that
176           // we remeasure it periodically.
177           mCallback(mCurrent);
178           scheduleRemeasurement();
179         }
180       }
181     }
182   }
183 
scheduleRemeasurement()184   void scheduleRemeasurement()
185   {
186     // set a timer to re-measure the active session after a period
187     mTimer.expires_from_now(std::chrono::microseconds{30000000});
188     mTimer.async_wait([this](const typename Timer::ErrorCode e) {
189       if (!e)
190       {
191         launchSessionMeasurement(mCurrent);
192         scheduleRemeasurement();
193       }
194     });
195   }
196 
handleFailedMeasurement(const SessionId & id)197   void handleFailedMeasurement(const SessionId& id)
198   {
199     using namespace std;
200 
201     debug(mIo->log()) << "Session " << id << " measurement failed.";
202 
203     // if we failed to measure for our current session, schedule a
204     // retry in the future. Otherwise, remove the session from our set
205     // of known sessions (if it is seen again it will be measured as
206     // if new).
207     if (mCurrent.sessionId == id)
208     {
209       scheduleRemeasurement();
210     }
211     else
212     {
213       const auto range = equal_range(
214         begin(mOtherSessions), end(mOtherSessions), Session{id, {}, {}}, SessionIdComp{});
215       if (range.first != range.second)
216       {
217         mOtherSessions.erase(range.first);
218         mPeers->forgetSession(id);
219       }
220     }
221   }
222 
updateTimeline(Session & session,Timeline timeline)223   void updateTimeline(Session& session, Timeline timeline)
224   {
225     // We use beat origin magnitude to prioritize sessions.
226     if (timeline.beatOrigin > session.timeline.beatOrigin)
227     {
228       debug(mIo->log()) << "Adopting peer timeline (" << timeline.tempo.bpm() << ", "
229                         << timeline.beatOrigin.floating() << ", "
230                         << timeline.timeOrigin.count() << ")";
231 
232       session.timeline = std::move(timeline);
233     }
234     else
235     {
236       debug(mIo->log()) << "Rejecting peer timeline with beat origin: "
237                         << timeline.beatOrigin.floating()
238                         << ". Current timeline beat origin: "
239                         << session.timeline.beatOrigin.floating();
240     }
241   }
242 
243   struct MeasurementResultsHandler
244   {
operator ()ableton::link::Sessions::MeasurementResultsHandler245     void operator()(GhostXForm xform) const
246     {
247       Sessions& sessions = mSessions;
248       const SessionId& sessionId = mSessionId;
249       if (xform == GhostXForm{})
250       {
251         mSessions.mIo->async([&sessions, sessionId] {
252           sessions.handleFailedMeasurement(std::move(sessionId));
253         });
254       }
255       else
256       {
257         mSessions.mIo->async([&sessions, sessionId, xform] {
258           sessions.handleSuccessfulMeasurement(std::move(sessionId), std::move(xform));
259         });
260       }
261     }
262 
263     Sessions& mSessions;
264     SessionId mSessionId;
265   };
266 
267   struct SessionIdComp
268   {
operator ()ableton::link::Sessions::SessionIdComp269     bool operator()(const Session& lhs, const Session& rhs) const
270     {
271       return lhs.sessionId < rhs.sessionId;
272     }
273   };
274 
275   using Peer = typename util::Injected<Peers>::type::Peer;
276   util::Injected<Peers> mPeers;
277   MeasurePeer mMeasure;
278   JoinSessionCallback mCallback;
279   Session mCurrent;
280   util::Injected<IoContext> mIo;
281   Timer mTimer;
282   Clock mClock;
283   std::vector<Session> mOtherSessions; // sorted/unique by session id
284 };
285 
286 template <typename Peers,
287   typename MeasurePeer,
288   typename JoinSessionCallback,
289   typename IoContext,
290   typename Clock>
makeSessions(Session init,util::Injected<Peers> peers,MeasurePeer measure,JoinSessionCallback join,util::Injected<IoContext> io,Clock clock)291 Sessions<Peers, MeasurePeer, JoinSessionCallback, IoContext, Clock> makeSessions(
292   Session init,
293   util::Injected<Peers> peers,
294   MeasurePeer measure,
295   JoinSessionCallback join,
296   util::Injected<IoContext> io,
297   Clock clock)
298 {
299   using namespace std;
300   return {move(init), move(peers), move(measure), move(join), move(io), move(clock)};
301 }
302 
303 } // namespace link
304 } // namespace ableton
305