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 : }
|