/* 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 namespace ableton { namespace link { // Clamp the tempo of the given timeline to the valid Link range inline Timeline clampTempo(const Timeline timeline) { const double kMinBpm = 20.0; const double kMaxBpm = 999.0; return {Tempo{(std::min)((std::max)(timeline.tempo.bpm(), kMinBpm), kMaxBpm)}, timeline.beatOrigin, timeline.timeOrigin}; } // Given an existing client timeline, a session timeline, and the // global host transform of the session, return a new version of the client // timeline. The resulting new client timeline is continuous with the // previous timeline so that curClient.toBeats(atTime) == // result.toBeats(atTime). inline Timeline updateClientTimelineFromSession(const Timeline curClient, const Timeline session, const std::chrono::microseconds atTime, const GhostXForm xform) { // An intermediate timeline representing the continuation of the // existing client timeline with the tempo from the session timeline. const auto tempTl = Timeline{session.tempo, curClient.toBeats(atTime), atTime}; // The host time corresponding to beat 0 on the session // timeline. Beat 0 on the session timeline is important because it // serves as the origin of the quantization grid for all participants. const auto hostBeatZero = xform.ghostToHost(session.fromBeats(Beats{INT64_C(0)})); // The new client timeline becomes the result of sliding the // intermediate timeline back so that it's anchor corresponds to // beat zero on the session timeline. The result preserves the // magnitude of beats on the client timeline while encoding the // quantization reference point in the time and beatOrigins. return {tempTl.tempo, tempTl.toBeats(hostBeatZero), hostBeatZero}; } inline Timeline updateSessionTimelineFromClient(const Timeline curSession, const Timeline client, const std::chrono::microseconds atTime, const GhostXForm xform) { // The client timeline was constructed so that it's timeOrigin // corresponds to beat 0 on the session timeline. const auto ghostBeat0 = xform.hostToGhost(client.timeOrigin); const auto zero = Beats{INT64_C(0)}; // If beat 0 was not shifted and there is not a new tempo, an update // of the session timeline is not required. Don't create an // equivalent timeline with different anchor points if not needed as // this will trigger other unnecessary changes. if (curSession.toBeats(ghostBeat0) == zero && client.tempo == curSession.tempo) { return curSession; } else { // An intermediate timeline representing the new tempo, the // effective time, and a possibly adjusted origin. const auto tempTl = Timeline{client.tempo, zero, ghostBeat0}; // The final session timeline must have the beat corresponding to // atTime on the old session timeline as its beatOrigin because this is // used for prioritization of timelines among peers - we can't let a // modification applied by the client artificially increase or // reduce the timeline's priority in the session. The new beat // origin should be as close as possible to lining up with atTime, // but we must also make sure that it's > curSession.beatOrigin // because otherwise it will get ignored. const auto newBeatOrigin = (std::max)(curSession.toBeats(xform.hostToGhost(atTime)), curSession.beatOrigin + Beats{INT64_C(1)}); return {client.tempo, newBeatOrigin, tempTl.fromBeats(newBeatOrigin)}; } } // Shift timeline so result.toBeats(t) == client.toBeats(t) + // shift. This takes into account the fact that the timeOrigin // corresponds to beat 0 on the session timeline. Using this function // and then setting the session timeline with the result will change // the phase of the session by the given shift amount. inline Timeline shiftClientTimeline(Timeline client, const Beats shift) { const auto timeDelta = client.fromBeats(shift) - client.fromBeats(Beats{INT64_C(0)}); client.timeOrigin = client.timeOrigin - timeDelta; return client; } } // namespace link } // namespace ableton