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