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