LCOV - code coverage report
Current view: top level - src/importexport/musicxml/internal/import - musicxmltupletstate.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 93.8 % 112 105
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-Studio-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2024 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              : #include "musicxmltupletstate.h"
      23              : 
      24              : #include "engraving/dom/tuplet.h"
      25              : 
      26              : #include "internal/shared/musicxmltypes.h"
      27              : 
      28              : using namespace mu::engraving;
      29              : using namespace mu::iex::musicxml;
      30              : 
      31              : //---------------------------------------------------------
      32              : //   determineTupletFractionAndFullDuration
      33              : //---------------------------------------------------------
      34              : 
      35              : /**
      36              :  Split duration into two factors where fullDuration is note sized
      37              :  (i.e. the denominator is a power of 2), 1/2 < fraction <= 1/1
      38              :  and fraction * fullDuration equals duration.
      39              :  */
      40              : 
      41          124 : void MusicXmlTupletState::determineTupletFractionAndFullDuration(const Fraction duration, Fraction& fraction, Fraction& fullDuration)
      42              : {
      43          124 :     fraction = duration;
      44          124 :     fullDuration = Fraction(1, 1);
      45              :     // move denominator's powers of 2 from fraction to fullDuration
      46          407 :     while (fraction.denominator() % 2 == 0) {
      47          283 :         fraction *= 2;
      48          283 :         fraction.reduce();
      49          283 :         fullDuration *= Fraction(1, 2);
      50              :     }
      51              :     // move numerator's powers of 2 from fraction to fullDuration
      52          131 :     while (fraction.numerator() % 2 == 0) {
      53            7 :         fraction *= Fraction(1, 2);
      54            7 :         fraction.reduce();
      55            7 :         fullDuration *= 2;
      56            7 :         fullDuration.reduce();
      57              :     }
      58              :     // make sure 1/2 < fraction <= 1/1
      59          131 :     while (fraction <= Fraction(1, 2)) {
      60            7 :         fullDuration *= Fraction(1, 2);
      61            7 :         fraction *= 2;
      62              :     }
      63          124 :     fullDuration.reduce();
      64          124 :     fraction.reduce();
      65              : 
      66              :     /*
      67              :     Examples (note result when denominator is not a power of two):
      68              :     3:2 tuplet of 1/4 results in fraction 1/1 and fullDuration 1/2
      69              :     2:3 tuplet of 1/4 results in fraction 3/1 and fullDuration 1/4
      70              :     4:3 tuplet of 1/4 results in fraction 3/1 and fullDuration 1/4
      71              :     3:4 tuplet of 1/4 results in fraction 1/1 and fullDuration 1/1
      72              : 
      73              :      Bring back fraction in 1/2 .. 1/1 range.
      74              :      */
      75              : 
      76          124 :     if (fraction > Fraction(1, 1) && fraction.denominator() == 1) {
      77            3 :         fullDuration *= fraction;
      78            3 :         fullDuration.reduce();
      79            3 :         fraction = Fraction(1, 1);
      80              :     }
      81              : 
      82              :     /*
      83              :     LOGD("duration %s fraction %s fullDuration %s",
      84              :            muPrintable(duration.toString()),
      85              :            muPrintable(fraction.toString()),
      86              :            muPrintable(fullDuration.toString())
      87              :            );
      88              :     */
      89          124 : }
      90              : 
      91              : //---------------------------------------------------------
      92              : //   missingTupletDuration
      93              : //---------------------------------------------------------
      94              : 
      95            9 : Fraction MusicXmlTupletState::missingTupletDuration(const Fraction duration)
      96              : {
      97            9 :     Fraction tupletFraction;
      98            9 :     Fraction tupletFullDuration;
      99              : 
     100            9 :     determineTupletFractionAndFullDuration(duration, tupletFraction, tupletFullDuration);
     101            9 :     Fraction missing = (Fraction(1, 1) - tupletFraction) * tupletFullDuration;
     102              : 
     103            9 :     return missing;
     104              : }
     105              : 
     106              : //---------------------------------------------------------
     107              : //   isTupletFilled
     108              : //---------------------------------------------------------
     109              : 
     110              : /**
     111              :  Determine if the tuplet is completely filled,
     112              :  because either (1) it is at least the same duration
     113              :  as the specified number of the specified normal type notes
     114              :  or (2) the duration adds up to a normal note duration.
     115              : 
     116              :  Example (1): a 3:2 tuplet with a 1/4 and a 1/8 note
     117              :  is filled if normal type is 1/8,
     118              :  it is not filled if normal type is 1/4.
     119              : 
     120              :  Example (2): a 3:2 tuplet with a 1/4 and a 1/8 note is filled.
     121              :  */
     122              : 
     123          278 : bool MusicXmlTupletState::isTupletFilled(const TDuration normalType, const Fraction timeMod)
     124              : {
     125              :     UNUSED(timeMod);
     126          278 :     bool res = false;
     127              :     /*
     128              :     const auto normalNotes = state.m_normalNotes;
     129              :     LOGD("duration %s normalType %s timeMod %s normalNotes %d actualNotes %d",
     130              :            muPrintable(state.m_duration.toString()),
     131              :            muPrintable(normalType.fraction().toString()),
     132              :            muPrintable(timeMod.toString()),
     133              :            normalNotes,
     134              :            actualNotes
     135              :            );
     136              :     */
     137              : 
     138          278 :     if (normalType.isValid()) {
     139           18 :         int matchedNormalType  = int(normalType.type());
     140           18 :         int matchedNormalCount = actualNotes;
     141              :         // match the types
     142           18 :         matchTypeAndCount(matchedNormalType, matchedNormalCount);
     143              :         // ... result scenario (1)
     144           18 :         res = smallestNoteCount >= matchedNormalCount;
     145              :         /*
     146              :         LOGD("normalType valid tupletType %d tupletCount %d matchedNormalType %d matchedNormalCount %d res %d",
     147              :                tupletType,
     148              :                tupletCount,
     149              :                matchedNormalType,
     150              :                matchedNormalCount,
     151              :                res
     152              :                );
     153              :          */
     154              :     } else {
     155              :         // ... result scenario (2)
     156          260 :         res = smallestNoteCount >= actualNotes;
     157              :         /*
     158              :         LOGD("normalType not valid tupletCount %d actualNotes %d res %d",
     159              :                tupletCount,
     160              :                actualNotes,
     161              :                res
     162              :                );
     163              :          */
     164              :     }
     165          278 :     return res;
     166              : }
     167              : 
     168              : //---------------------------------------------------------
     169              : //   smallestTypeAndCount
     170              : //---------------------------------------------------------
     171              : 
     172              : /**
     173              :  Determine the smallest note type and the number of those
     174              :  present in a ChordRest.
     175              :  For a note without dots the type equals the note type
     176              :  and count is one.
     177              :  For a single dotted note the type equals half the note type
     178              :  and count is three.
     179              :  A double dotted note is similar.
     180              :  Note: code assumes when duration().type() is incremented,
     181              :  the note length is divided by two, checked by tupletAssert().
     182              :  */
     183              : 
     184          692 : void MusicXmlTupletState::smallestTypeAndCount(const TDuration durType, int& type, int& count)
     185              : {
     186          692 :     type = int(durType.type());
     187          692 :     count = 1;
     188          692 :     switch (durType.dots()) {
     189          670 :     case 0:
     190              :         // nothing to do
     191          670 :         break;
     192           22 :     case 1:
     193           22 :         type += 1;                 // next-smaller type
     194           22 :         count = 3;
     195           22 :         break;
     196            0 :     case 2:
     197            0 :         type += 2;                 // next-next-smaller type
     198            0 :         count = 7;
     199            0 :         break;
     200            0 :     default:
     201            0 :         LOGD("smallestTypeAndCount() does not support more than 2 dots");
     202              :     }
     203          692 : }
     204              : 
     205              : //---------------------------------------------------------
     206              : //   matchTypeAndCount
     207              : //---------------------------------------------------------
     208              : 
     209              : /**
     210              :  Given two note types and counts, if the types are not equal,
     211              :  make them equal by successively doubling the count of the
     212              :  largest type.
     213              :  */
     214              : 
     215          474 : void MusicXmlTupletState::matchTypeAndCount(int& noteType, int& noteCount)
     216              : {
     217          494 :     while (smallestNoteType < noteType) {
     218           20 :         smallestNoteType++;
     219           20 :         smallestNoteCount *= 2;
     220              :     }
     221          498 :     while (noteType < smallestNoteType) {
     222           24 :         noteType++;
     223           24 :         noteCount *= 2;
     224              :     }
     225          474 : }
     226              : 
     227              : //---------------------------------------------------------
     228              : //   addDurationToTuplet
     229              : //---------------------------------------------------------
     230              : 
     231              : /**
     232              :  Add duration to tuplet duration
     233              :  Determine type and number of smallest notes in the tuplet
     234              :  */
     235              : 
     236          692 : void MusicXmlTupletState::addDurationToTuplet(const Fraction dur, const Fraction timeMod)
     237              : {
     238              :     /*
     239              :     LOGD("1 duration %s timeMod %s -> state.tupletType %d state.tupletCount %d state.actualNotes %d state.normalNotes %d",
     240              :            muPrintable(duration.print()),
     241              :            muPrintable(timeMod.print()),
     242              :            m_tupletType,
     243              :            m_tupletCount,
     244              :            m_actualNotes,
     245              :            m_normalNotes
     246              :            );
     247              :     */
     248          692 :     if (duration <= Fraction(0, 1)) {
     249              :         // first note: init variables
     250          236 :         actualNotes = timeMod.denominator();
     251          236 :         normalNotes = timeMod.numerator();
     252          236 :         smallestTypeAndCount(dur / timeMod, smallestNoteType, smallestNoteCount);
     253              :     } else {
     254          456 :         int noteType = 0;
     255          456 :         int noteCount = 0;
     256          456 :         smallestTypeAndCount(dur / timeMod, noteType, noteCount);
     257              :         // match the types
     258          456 :         matchTypeAndCount(noteType, noteCount);
     259          456 :         smallestNoteCount += noteCount;
     260              :     }
     261          692 :     duration += dur;
     262              :     /*
     263              :     LOGD("2 duration %s -> state.tupletType %d state.tupletCount %d state.actualNotes %d state.normalNotes %d",
     264              :            muPrintable(duration.print()),
     265              :            m_tupletType,
     266              :            m_tupletCount,
     267              :            m_actualNotes,
     268              :            m_normalNotes
     269              :            );
     270              :     */
     271          692 : }
     272              : 
     273              : //---------------------------------------------------------
     274              : //   determineTupletAction
     275              : //---------------------------------------------------------
     276              : 
     277              : /**
     278              :  Update tuplet state using parse result tupletDesc.
     279              :  Tuplets with <actual-notes> and <normal-notes> but without <tuplet>
     280              :  are handled correctly.
     281              :  TODO Nested tuplets are not (yet) supported.
     282              :  */
     283              : 
     284        13959 : MusicXmlTupletFlags MusicXmlTupletState::determineTupletAction(const Fraction noteDuration,
     285              :                                                                const Fraction timeMod,
     286              :                                                                const MusicXmlStartStop tupletStartStop,
     287              :                                                                const TDuration normalType,
     288              :                                                                Fraction& missingPreviousDuration,
     289              :                                                                Fraction& missingCurrentDuration)
     290              : {
     291        13959 :     const int actNotes = timeMod.denominator();
     292        13959 :     const int norNotes = timeMod.numerator();
     293        13959 :     MusicXmlTupletFlags res = MusicXmlTupletFlag::NONE;
     294              : 
     295              :     // check for unexpected termination of previous tuplet
     296        13959 :     if (inTuplet && timeMod == Fraction(1, 1)) {
     297              :         // recover by simply stopping the current tuplet first
     298            8 :         if (!isTupletFilled(normalType, timeMod)) {
     299            2 :             missingPreviousDuration = missingTupletDuration(duration);
     300              :             //LOGD("tuplet incomplete, missing %s", muPrintable(missingPreviousDuration.print()));
     301              :         }
     302            8 :         *this = {};
     303            8 :         res |= MusicXmlTupletFlag::STOP_PREVIOUS;
     304              :     }
     305              : 
     306              :     // check for obvious errors
     307        13959 :     if (inTuplet && tupletStartStop == MusicXmlStartStop::START) {
     308            2 :         LOGD("tuplet already started");
     309              :         // recover by simply stopping the current tuplet first
     310            2 :         if (!isTupletFilled(normalType, timeMod)) {
     311            0 :             missingPreviousDuration = missingTupletDuration(duration);
     312              :             //LOGD("tuplet incomplete, missing %s", muPrintable(missingPreviousDuration.print()));
     313              :         }
     314            2 :         *this = {};
     315            2 :         res |= MusicXmlTupletFlag::STOP_PREVIOUS;
     316              :     }
     317        13959 :     if (tupletStartStop == MusicXmlStartStop::STOP && !inTuplet) {
     318            8 :         LOGD("tuplet stop but no tuplet started");           // TODO
     319              :         // recovery handled later (automatically, no special case needed)
     320              :     }
     321              : 
     322              :     // Tuplet are either started by the tuplet start
     323              :     // or when the time modification is first found.
     324        13959 :     if (!inTuplet) {
     325        13503 :         if (tupletStartStop == MusicXmlStartStop::START
     326        13301 :             || (!inTuplet && (actNotes != 1 || norNotes != 1))) {
     327          236 :             if (tupletStartStop != MusicXmlStartStop::START) {
     328           34 :                 implicit = true;
     329              :             } else {
     330          202 :                 implicit = false;
     331              :             }
     332              :             // create a new tuplet
     333          236 :             inTuplet = true;
     334          236 :             res |= MusicXmlTupletFlag::START_NEW;
     335              :         }
     336              :     }
     337              : 
     338              :     // Add chord to the current tuplet.
     339              :     // Must also check for actual/normal notes to prevent
     340              :     // adding one chord too much if tuplet stop is missing.
     341        13959 :     if (inTuplet && !(actNotes == 1 && norNotes == 1)) {
     342          692 :         addDurationToTuplet(noteDuration, timeMod);
     343          692 :         res |= MusicXmlTupletFlag::ADD_CHORD;
     344              :     }
     345              : 
     346              :     // Tuplets are stopped by the tuplet stop
     347              :     // or when the tuplet is filled completely
     348              :     // (either with knowledge of the normal type
     349              :     // or as a last resort calculated based on
     350              :     // actual and normal notes plus total duration)
     351              :     // or when the time-modification is not found.
     352              : 
     353        13959 :     if (inTuplet) {
     354          692 :         if (tupletStartStop == MusicXmlStartStop::STOP
     355          498 :             || (implicit && isTupletFilled(normalType, timeMod))
     356         1190 :             || (actNotes == 1 && norNotes == 1)) {           // incorrect ??? check scenario incomplete tuplet w/o start
     357          220 :             if (actNotes > norNotes && !isTupletFilled(normalType, timeMod)) {
     358            4 :                 missingCurrentDuration = missingTupletDuration(duration);
     359            4 :                 LOGD("current tuplet incomplete, missing %s", muPrintable(missingCurrentDuration.toString()));
     360              :             }
     361              : 
     362          220 :             *this = {};
     363          220 :             res |= MusicXmlTupletFlag::STOP_CURRENT;
     364              :         }
     365              :     }
     366              : 
     367        13959 :     return res;
     368              : }
        

Generated by: LCOV version 2.0-1