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/Timeline.hpp>
24 
25 namespace ableton
26 {
27 namespace link
28 {
29 
30 // Clamp the tempo of the given timeline to the valid Link range
31 inline Timeline clampTempo(const Timeline timeline)
32 {
33   const double kMinBpm = 20.0;
34   const double kMaxBpm = 999.0;
35   return {Tempo{(std::min)((std::max)(timeline.tempo.bpm(), kMinBpm), kMaxBpm)},
36     timeline.beatOrigin, timeline.timeOrigin};
37 }
38 
39 // Given an existing client timeline, a session timeline, and the
40 // global host transform of the session, return a new version of the client
41 // timeline. The resulting new client timeline is continuous with the
42 // previous timeline so that curClient.toBeats(atTime) ==
43 // result.toBeats(atTime).
44 inline Timeline updateClientTimelineFromSession(const Timeline curClient,
45   const Timeline session,
46   const std::chrono::microseconds atTime,
47   const GhostXForm xform)
48 {
49   // An intermediate timeline representing the continuation of the
50   // existing client timeline with the tempo from the session timeline.
51   const auto tempTl = Timeline{session.tempo, curClient.toBeats(atTime), atTime};
52   // The host time corresponding to beat 0 on the session
53   // timeline. Beat 0 on the session timeline is important because it
54   // serves as the origin of the quantization grid for all participants.
55   const auto hostBeatZero = xform.ghostToHost(session.fromBeats(Beats{INT64_C(0)}));
56   // The new client timeline becomes the result of sliding the
57   // intermediate timeline back so that it's anchor corresponds to
58   // beat zero on the session timeline. The result preserves the
59   // magnitude of beats on the client timeline while encoding the
60   // quantization reference point in the time and beatOrigins.
61   return {tempTl.tempo, tempTl.toBeats(hostBeatZero), hostBeatZero};
62 }
63 
64 
65 inline Timeline updateSessionTimelineFromClient(const Timeline curSession,
66   const Timeline client,
67   const std::chrono::microseconds atTime,
68   const GhostXForm xform)
69 {
70   // The client timeline was constructed so that it's timeOrigin
71   // corresponds to beat 0 on the session timeline.
72   const auto ghostBeat0 = xform.hostToGhost(client.timeOrigin);
73 
74   const auto zero = Beats{INT64_C(0)};
75   // If beat 0 was not shifted and there is not a new tempo, an update
76   // of the session timeline is not required. Don't create an
77   // equivalent timeline with different anchor points if not needed as
78   // this will trigger other unnecessary changes.
79   if (curSession.toBeats(ghostBeat0) == zero && client.tempo == curSession.tempo)
80   {
81     return curSession;
82   }
83   else
84   {
85     // An intermediate timeline representing the new tempo, the
86     // effective time, and a possibly adjusted origin.
87     const auto tempTl = Timeline{client.tempo, zero, ghostBeat0};
88     // The final session timeline must have the beat corresponding to
89     // atTime on the old session timeline as its beatOrigin because this is
90     // used for prioritization of timelines among peers - we can't let a
91     // modification applied by the client artificially increase or
92     // reduce the timeline's priority in the session. The new beat
93     // origin should be as close as possible to lining up with atTime,
94     // but we must also make sure that it's > curSession.beatOrigin
95     // because otherwise it will get ignored.
96     const auto newBeatOrigin = (std::max)(curSession.toBeats(xform.hostToGhost(atTime)),
97       curSession.beatOrigin + Beats{INT64_C(1)});
98     return {client.tempo, newBeatOrigin, tempTl.fromBeats(newBeatOrigin)};
99   }
100 }
101 
102 // Shift timeline so result.toBeats(t) == client.toBeats(t) +
103 // shift. This takes into account the fact that the timeOrigin
104 // corresponds to beat 0 on the session timeline. Using this function
105 // and then setting the session timeline with the result will change
106 // the phase of the session by the given shift amount.
107 inline Timeline shiftClientTimeline(Timeline client, const Beats shift)
108 {
109   const auto timeDelta = client.fromBeats(shift) - client.fromBeats(Beats{INT64_C(0)});
110   client.timeOrigin = client.timeOrigin - timeDelta;
111   return client;
112 }
113 
114 } // namespace link
115 } // namespace ableton
116