LCOV - code coverage report
Current view: top level - src/engraving/compat/midi - pitchwheelrenderer.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 88.4 % 112 99
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : #include "pitchwheelrenderer.h"
       2              : 
       3              : #include <algorithm>
       4              : 
       5              : #include "log.h"
       6              : 
       7              : using namespace mu::engraving;
       8              : 
       9           88 : PitchWheelRenderer::PitchWheelRenderer(PitchWheelSpecs wheelSpec)
      10           88 :     : _wheelSpec(wheelSpec)
      11           88 : {}
      12              : 
      13          107 : void PitchWheelRenderer::addPitchWheelFunction(const PitchWheelFunction& function, uint32_t channel, staff_idx_t staffIdx,
      14              :                                                MidiInstrumentEffect effect)
      15              : {
      16          107 :     PitchWheelFunctions& functions =  _functions[channel];
      17          107 :     _effectByChannel[channel] = effect;
      18          107 :     _staffIdxByChannel[channel] = staffIdx;
      19              : 
      20          107 :     if (function.mStartTick < functions.startTick) {
      21           30 :         functions.startTick = function.mStartTick;
      22              :     }
      23          107 :     if (function.mEndTick > functions.endTick) {
      24          104 :         functions.endTick = function.mEndTick;
      25              :     }
      26              : 
      27          107 :     functions.functions.push_back(function);
      28          107 : }
      29              : 
      30           88 : EventsHolder PitchWheelRenderer::renderPitchWheel() const noexcept
      31              : {
      32           88 :     EventsHolder pitchWheelEvents;
      33              : 
      34          118 :     for (const auto& function : _functions) {
      35           30 :         renderChannelPitchWheel(pitchWheelEvents, function.second, function.first);
      36              :     }
      37              : 
      38           88 :     return pitchWheelEvents;
      39              : }
      40              : 
      41              : //! MARK: PRIVATE METHODS
      42              : 
      43           30 : void PitchWheelRenderer::renderChannelPitchWheel(EventsHolder& pitchWheelEvents,
      44              :                                                  const PitchWheelFunctions& functions,
      45              :                                                  uint32_t channel) const noexcept
      46              : {
      47           30 :     if (functions.endTick < functions.startTick) {
      48            0 :         return;
      49              :     }
      50              : 
      51           30 :     MidiInstrumentEffect effect = MidiInstrumentEffect::NONE;
      52           30 :     if (_effectByChannel.find(channel) != _effectByChannel.end()) {
      53           30 :         effect = _effectByChannel.at(channel);
      54              :     }
      55              : 
      56           30 :     bool staffInfoValid = false;
      57           30 :     staff_idx_t staffIdx = 0;
      58              : 
      59           30 :     if (_staffIdxByChannel.find(channel) != _staffIdxByChannel.end()) {
      60           30 :         staffInfoValid = true;
      61           30 :         staffIdx = _staffIdxByChannel.at(channel);
      62              :     }
      63              : 
      64           30 :     std::map<int, int, std::greater<> > ranges;
      65           30 :     generateRanges(functions.functions, ranges);
      66              : 
      67           79 :     for (auto rit = ranges.crbegin(); rit != ranges.crend(); ++rit) {
      68           49 :         int32_t start = rit->first;
      69           49 :         int32_t end = rit->second;
      70           49 :         int32_t tick = start;
      71              : 
      72           49 :         std::vector<PitchWheelFunction> functionsToProcess;
      73           49 :         functionsToProcess.reserve(functions.functions.size());
      74          261 :         for (const auto& func : functions.functions) {
      75          212 :             if (func.mEndTick <= end) {
      76          158 :                 functionsToProcess.push_back(func);
      77              :             }
      78              :         }
      79           49 :         std::vector<int> pitches;
      80           49 :         pitches.reserve(functionsToProcess.size());
      81          207 :         for (size_t i = 0; i < functionsToProcess.size(); ++i) {
      82          158 :             pitches.push_back(0);
      83              :         }
      84           49 :         int prevPitch = _wheelSpec.mLimit;
      85           49 :         bool forceUpdate = false;
      86         1475 :         while (tick < end) {
      87         1426 :             auto funcIt = functionsToProcess.begin();
      88         7014 :             for (size_t i = 0; i < functionsToProcess.size(); ++i) {
      89         5588 :                 if (tick >= funcIt->mEndTick) {
      90              :                     // Function exceeds its max range
      91              :                     // don't need its value anymore
      92         2772 :                     pitches.at(i) = 0;
      93         2772 :                     ++funcIt;
      94         2772 :                     forceUpdate = true;
      95         2772 :                     continue;
      96              :                 }
      97         2816 :                 if (tick < funcIt->mStartTick) {
      98         1278 :                     ++funcIt;
      99         1278 :                     continue;
     100              :                 }
     101         1538 :                 int pitch = funcIt->func(tick);
     102         1538 :                 pitches.at(i) = pitch;
     103         1538 :                 ++funcIt;
     104              :             }
     105         1426 :             int finalPitch = _wheelSpec.mLimit;
     106         7014 :             for (const auto& pitch : pitches) {
     107         5588 :                 finalPitch += pitch;
     108              :             }
     109         1426 :             finalPitch = std::clamp(finalPitch, 0, 2 * _wheelSpec.mLimit - 1);
     110         1426 :             if (forceUpdate || finalPitch != prevPitch || tick == start) {
     111         1345 :                 NPlayEvent evb(ME_PITCHBEND, channel, finalPitch % 128, finalPitch / 128);
     112         1345 :                 evb.setEffect(effect);
     113         1345 :                 if (staffInfoValid) {
     114         1345 :                     evb.setOriginatingStaff(staffIdx);
     115              :                 }
     116         1345 :                 pitchWheelEvents[channel].emplace_hint(pitchWheelEvents[channel].end(), std::make_pair(tick, evb));
     117         1345 :                 forceUpdate = false;
     118              :             }
     119              : 
     120         1426 :             prevPitch = finalPitch;
     121         1426 :             tick += _wheelSpec.mStep;
     122              :         }
     123           49 :     }
     124           30 : }
     125              : 
     126            0 : int32_t PitchWheelRenderer::findNextStartTick(const std::vector<PitchWheelFunction>& functions) const noexcept
     127              : {
     128            0 :     int32_t tick = std::numeric_limits<int32_t>::max();
     129            0 :     for (const auto& func : functions) {
     130            0 :         tick = std::min(tick, func.mStartTick);
     131              :     }
     132              : 
     133            0 :     return tick;
     134              : }
     135              : 
     136            0 : int32_t PitchWheelRenderer::calculatePitchBend(const std::vector<PitchWheelFunction>& functions, int32_t tick) const noexcept
     137              : {
     138            0 :     int pitchValue = _wheelSpec.mLimit;
     139              : 
     140            0 :     for (const auto& func : functions) {
     141            0 :         if (tick < func.mStartTick || tick >= func.mEndTick) {
     142            0 :             continue;
     143              :         }
     144              : 
     145            0 :         pitchValue += func.func(tick);
     146              :     }
     147              : 
     148            0 :     return pitchValue;
     149              : }
     150              : 
     151              : // 1                |------|
     152              : // 2                   |-----------| handleStartTick();
     153              : // result           |--------------|
     154              : // 3           |--------|            handleEndTick();
     155              : // result      |-------------------|
     156           35 : void PitchWheelRenderer::generateRanges(const std::vector<PitchWheelFunction>& functions, std::map<int, int, std::greater<> >& ranges)
     157              : {
     158              :     // !NOTE ranges map is reversed. Use reverse iterators
     159           32 :     auto handleEndTick = [&](const PitchWheelFunction& func) {
     160           32 :         auto lowerRange = ranges.upper_bound(func.mEndTick);
     161           32 :         if (lowerRange == ranges.end()) {
     162           31 :             return false;
     163              :         }
     164            1 :         ranges.insert({ func.mStartTick, lowerRange->second });
     165            1 :         ranges.erase(lowerRange);
     166            1 :         return true;
     167           35 :     };
     168              : 
     169          110 :     auto handleStartTick = [&](const PitchWheelFunction& func) {
     170          110 :         auto lowerRange = ranges.upper_bound(func.mStartTick);
     171          110 :         if (lowerRange == ranges.end()) {
     172              :             // We are getting PW events sorted by startTick
     173              :             // So in ideal world handleEndTick always return false
     174           32 :             return handleEndTick(func);
     175              :         }
     176           78 :         if (lowerRange->second >= func.mStartTick) {
     177           58 :             lowerRange->second = std::max(lowerRange->second, func.mEndTick);
     178           58 :             return true;
     179              :         }
     180           20 :         return false;
     181           35 :     };
     182              : 
     183          147 :     for (const auto& func : functions) {
     184          112 :         if (auto key = ranges.find(func.mStartTick); key != ranges.end()) {
     185            2 :             key->second = std::max(key->second, func.mEndTick);
     186            2 :             continue;
     187              :         }
     188          110 :         if (!handleStartTick(func)) {
     189           51 :             ranges.insert({ func.mStartTick, func.mEndTick });
     190              :         }
     191              :     }
     192           35 : }
        

Generated by: LCOV version 2.0-1