LCOV - code coverage report
Current view: top level - muse/framework/mpe - automationpoint.h Coverage Total Hit
Test: filtered_coverage.info Lines: 69.1 % 68 47
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2026 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : #pragma once
      23              : 
      24              : #include <algorithm>
      25              : #include <variant>
      26              : #include <optional>
      27              : #include <type_traits>
      28              : 
      29              : #include "types/sharedmap.h"
      30              : #include "types/number.h"
      31              : 
      32              : #include "containers.h"
      33              : #include "realfn.h"
      34              : 
      35              : namespace muse::mpe {
      36              : struct AutomationPoint {
      37              :     //! NOTE: eases the segment through point (t, value)
      38              :     struct Ease {
      39              :         real_t t = real_t::make(0.5); // [0; 1]
      40              :         real_t value = real_t::make(0.5); // [0; 1]
      41              : 
      42          961 :         static constexpr Ease none() { return {}; }
      43          346 :         bool isNone() const { return *this == none(); }
      44          610 :         bool operator==(const Ease& e) const { return t == e.t && value == e.value; }
      45              :     };
      46              : 
      47              :     //! NOTE: arrival value equals whatever precedes this point in the curve
      48              :     struct ArrivalFromPrevious {
      49           88 :         bool operator==(const ArrivalFromPrevious&) const { return true; }
      50              :     };
      51              : 
      52              :     //! NOTE: arrival value is an explicit number
      53              :     struct ExplicitArrival {
      54              :         real_t value = 0.;
      55              :         Ease ease;
      56              : 
      57           59 :         bool operator==(const ExplicitArrival& o) const { return value == o.value && ease == o.ease; }
      58              :     };
      59              : 
      60              :     using InValue = std::variant<ArrivalFromPrevious, ExplicitArrival>;
      61              : 
      62              :     InValue inValue = ArrivalFromPrevious {};
      63              :     real_t outValue = 0.;
      64              : 
      65          148 :     bool operator==(const AutomationPoint& p) const
      66              :     {
      67          148 :         return inValue == p.inValue && outValue == p.outValue;
      68              :     }
      69              : };
      70              : 
      71              : template<typename KeyT>
      72              : using AutomationCurve = SharedMap<KeyT, AutomationPoint>;
      73              : 
      74              : //! NOTE: this point's ease, if it can have one; nullopt for ArrivalFromPrevious
      75          911 : inline std::optional<AutomationPoint::Ease> ease(const AutomationPoint& point) noexcept
      76              : {
      77         1822 :     return std::visit([](const auto& v) -> std::optional<AutomationPoint::Ease> {
      78              :         using T = std::decay_t<decltype(v)>;
      79              :         if constexpr (std::is_same_v<T, AutomationPoint::ArrivalFromPrevious>) {
      80          340 :             return std::nullopt;
      81              :         } else {
      82          571 :             return v.ease;
      83              :         }
      84          911 :     }, point.inValue);
      85              : }
      86              : 
      87              : //! NOTE: resolves what this point's arrival value actually is, given prevOutValue
      88              : //! (the outValue of whatever precedes it in its curve, if any)
      89         1084 : inline real_t resolveInValue(const AutomationPoint& point, std::optional<real_t> prevOutValue) noexcept
      90              : {
      91         2168 :     return std::visit([&](const auto& v) -> real_t {
      92              :         using T = std::decay_t<decltype(v)>;
      93              :         if constexpr (std::is_same_v<T, AutomationPoint::ArrivalFromPrevious>) {
      94          423 :             return prevOutValue ? *prevOutValue : real_t(0.0);
      95              :         } else {
      96          661 :             return v.value;
      97              :         }
      98         1084 :     }, point.inValue);
      99              : }
     100              : 
     101              : //! NOTE: value at normalized position t ([0, 1]) across the segment arriving at point
     102              : //! The segment is split into two quadratic Bezier arcs meeting at the bend point. The arcs
     103              : //! share a tangent there, producing a smooth bend whose position follows bend.t. When there's
     104              : //! no bend, this reduces to a straight line
     105          382 : inline real_t evaluateAt(const AutomationPoint& point, std::optional<real_t> prevOutValue, real_t t) noexcept
     106              : {
     107          382 :     const real_t prevOut = prevOutValue ? *prevOutValue : real_t(0.0);
     108          382 :     const real_t thisIn = resolveInValue(point, prevOutValue);
     109          382 :     const real_t range = thisIn - prevOut;
     110          382 :     const std::optional<AutomationPoint::Ease> pointEase = ease(point);
     111              : 
     112          382 :     if (!pointEase || pointEase->isNone() || pointEase->t <= 0.0 || pointEase->t >= 1.0) {
     113          378 :         return std::clamp(prevOut + range * t, real_t(0.0), real_t(1.0));
     114              :     }
     115              : 
     116            4 :     auto quadraticBezier = [](real_t s, real_t p0, real_t p1, real_t p2) {
     117            4 :         const real_t u = 1.0 - s;
     118            4 :         return std::clamp(u * u * p0 + 2.0 * u * s * p1 + s * s * p2, real_t(0.0), real_t(1.0));
     119              :     };
     120              : 
     121            4 :     const real_t fraction = std::clamp(pointEase->value, real_t(0.0), real_t(1.0));
     122            4 :     const real_t bendValue = prevOut + fraction * range;
     123            4 :     const real_t lo = std::min(prevOut, thisIn);
     124            4 :     const real_t hi = std::max(prevOut, thisIn);
     125            4 :     const real_t halfSlope = 0.5 * range;
     126            4 :     const real_t remainder = 1.0 - pointEase->t;
     127              : 
     128              :     // Clamped control points (q1, q2) for each arc
     129            4 :     const real_t q1 = std::clamp(bendValue - pointEase->t * halfSlope, lo, hi);
     130              : 
     131            4 :     if (t <= pointEase->t) {
     132            4 :         return quadraticBezier(t / pointEase->t, prevOut, q1, bendValue);
     133              :     }
     134              : 
     135            0 :     const real_t q2 = std::clamp(bendValue + remainder * halfSlope, lo, hi);
     136            0 :     return quadraticBezier((t - pointEase->t) / remainder, bendValue, q2, thisIn);
     137              : }
     138              : 
     139              : template<typename Key>
     140          214 : inline real_t evaluateCurveAt(const AutomationCurve<Key>& curve, const typename AutomationCurve<Key>::key_type& pos)
     141              : {
     142          214 :     if (curve.empty()) {
     143            0 :         return real_t {};
     144              :     }
     145              : 
     146          214 :     auto it = findLessOrEqual(curve, pos);
     147          214 :     if (it == curve.end()) {
     148              :         //! NOTE: hold the first point's value backwards in time, matching standard envelope semantics
     149            1 :         return curve.begin()->second.outValue;
     150              :     }
     151              : 
     152          213 :     real_t normalized = it->second.outValue;
     153          213 :     const auto next = std::next(it);
     154          213 :     if (next != curve.end()) {
     155          180 :         const real_t t = static_cast<real_t>(pos - it->first) / static_cast<real_t>(next->first - it->first);
     156          180 :         normalized = evaluateAt(next->second, it->second.outValue, t);
     157              :     }
     158              : 
     159          213 :     return normalized;
     160              : }
     161              : 
     162              : //! NOTE: resamples a curve into discrete samples for consumers that can't evaluate on read
     163              : template<typename Key, typename Callback>
     164            0 : void resampleCurve(const AutomationCurve<Key>& curve, Key stepInterval, Callback&& onSample)
     165              : {
     166            0 :     for (auto it = curve.cbegin(); it != curve.cend(); ++it) {
     167            0 :         onSample(it->first, it->second.outValue);
     168              : 
     169            0 :         const auto next = std::next(it);
     170            0 :         if (next == curve.cend()) {
     171            0 :             continue;
     172              :         }
     173              : 
     174            0 :         const real_t nextArrival = resolveInValue(next->second, it->second.outValue);
     175            0 :         const std::optional<AutomationPoint::Ease> nextEase = ease(next->second);
     176            0 :         const bool isFlat = (!nextEase || nextEase->isNone()) && muse::RealIsEqual(nextArrival, it->second.outValue);
     177            0 :         if (isFlat) {
     178            0 :             continue;
     179              :         }
     180              : 
     181            0 :         const Key intervalDuration = next->first - it->first;
     182            0 :         const size_t steps = std::max(size_t((intervalDuration + stepInterval - 1) / stepInterval), size_t(1));
     183              : 
     184            0 :         for (size_t j = 1; j < steps; ++j) {
     185            0 :             const Key t = it->first + intervalDuration * j / steps;
     186            0 :             const real_t normalized = evaluateAt(next->second, it->second.outValue, real_t(j) / real_t(steps));
     187            0 :             onSample(t, normalized);
     188              :         }
     189              :     }
     190            0 : }
     191              : }
        

Generated by: LCOV version 2.0-1