266 lines
9 KiB
C++
266 lines
9 KiB
C++
//=============================================================================
|
|
// MuseScore
|
|
// Music Composition & Notation
|
|
//
|
|
// Copyright (C) 2018 Werner Schweer and others
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License version 2
|
|
// as published by the Free Software Foundation and appearing in
|
|
// the file LICENCE.GPL
|
|
//=============================================================================
|
|
|
|
#include "libmscore/fraction.h"
|
|
|
|
#include "importmxmllogger.h"
|
|
#include "importmxmlnoteduration.h"
|
|
|
|
namespace Ms {
|
|
|
|
//---------------------------------------------------------
|
|
// noteTypeToFraction
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Convert MusicXML note type to fraction.
|
|
*/
|
|
|
|
static Fraction noteTypeToFraction(const QString& type)
|
|
{
|
|
if (type == "1024th")
|
|
return Fraction(1, 1024);
|
|
else if (type == "512th")
|
|
return Fraction(1, 512);
|
|
else if (type == "256th")
|
|
return Fraction(1, 256);
|
|
else if (type == "128th")
|
|
return Fraction(1, 128);
|
|
else if (type == "64th")
|
|
return Fraction(1, 64);
|
|
else if (type == "32nd")
|
|
return Fraction(1, 32);
|
|
else if (type == "16th")
|
|
return Fraction(1, 16);
|
|
else if (type == "eighth")
|
|
return Fraction(1, 8);
|
|
else if (type == "quarter")
|
|
return Fraction(1, 4);
|
|
else if (type == "half")
|
|
return Fraction(1, 2);
|
|
else if (type == "whole")
|
|
return Fraction(1, 1);
|
|
else if (type == "breve")
|
|
return Fraction(2, 1);
|
|
else if (type == "long")
|
|
return Fraction(4, 1);
|
|
else if (type == "maxima")
|
|
return Fraction(8, 1);
|
|
else
|
|
return Fraction(0, 0);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// calculateFraction
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Convert note type, number of dots and actual and normal notes into a duration
|
|
*/
|
|
|
|
static Fraction calculateFraction(const QString& type, const int dots, const Fraction timeMod)
|
|
{
|
|
// type
|
|
Fraction f = noteTypeToFraction(type);
|
|
if (f.isValid()) {
|
|
// dot(s)
|
|
Fraction f_no_dots = f;
|
|
for (int i = 0; i < dots; ++i)
|
|
f += (f_no_dots / (2 << i));
|
|
// tuplet
|
|
if (timeMod.isValid())
|
|
f *= timeMod;
|
|
// clean up (just in case)
|
|
f.reduce();
|
|
}
|
|
return f;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// checkTiming
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Do timing error checks.
|
|
Return empty string if OK, message in case of error.
|
|
*/
|
|
|
|
QString mxmlNoteDuration::checkTiming(const QString& type, const bool rest, const bool grace)
|
|
{
|
|
//qDebug("type %s rest %d grace %d", qPrintable(type), rest, grace);
|
|
QString errorStr;
|
|
|
|
// normalize duration
|
|
if (_dura.isValid())
|
|
_dura.reduce();
|
|
|
|
const auto calcDura = calculateFraction(type, _dots, _timeMod);
|
|
if (_dura.isValid() && calcDura.isValid()) {
|
|
if (_dura != calcDura) {
|
|
errorStr = QString("calculated duration (%1) not equal to specified duration (%2)")
|
|
.arg(calcDura.print()).arg(_dura.print());
|
|
//qDebug("rest %d type '%s' timemod %s", rest, qPrintable(type), qPrintable(_timeMod.print()));
|
|
|
|
if (rest && type == "whole" && _dura.isValid()) {
|
|
// Sibelius whole measure rest (not an error)
|
|
errorStr = "";
|
|
}
|
|
else if (grace && _dura == Fraction(0, 1)) {
|
|
// grace note (not an error)
|
|
errorStr = "";
|
|
}
|
|
else {
|
|
const int maxDiff = 3; // maximum difference considered a rounding error
|
|
if (qAbs(calcDura.ticks() - _dura.ticks()) <= maxDiff) {
|
|
errorStr += " -> assuming rounding error";
|
|
_dura = calcDura;
|
|
}
|
|
}
|
|
|
|
// Special case:
|
|
// Encore generates rests in tuplets w/o <tuplet> or <time-modification>.
|
|
// Detect this by comparing the actual duration with the expected duration
|
|
// based on note type. If actual is 2/3 of expected, the rest is part
|
|
// of a tuplet.
|
|
if (rest && !_timeMod.isValid()) {
|
|
if (2 * calcDura.ticks() == 3 * _dura.ticks()) {
|
|
_timeMod = Fraction(2, 3);
|
|
errorStr += " -> assuming triplet";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (_dura.isValid()) {
|
|
// do not report an error for typeless (whole measure) rests
|
|
if (!(rest && type == ""))
|
|
errorStr = "calculated duration invalid, using specified duration";
|
|
}
|
|
else if (calcDura.isValid()) {
|
|
if (!grace) {
|
|
errorStr = "specified duration invalid, using calculated duration";
|
|
_dura = calcDura; // overrule dura
|
|
}
|
|
}
|
|
else {
|
|
errorStr = "calculated and specified duration invalid, using 4/4";
|
|
_dura = Fraction(4, 4);
|
|
}
|
|
|
|
return errorStr;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// duration
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Parse the /score-partwise/part/measure/note/duration node.
|
|
*/
|
|
|
|
void mxmlNoteDuration::duration(QXmlStreamReader& e)
|
|
{
|
|
Q_ASSERT(e.isStartElement() && e.name() == "duration");
|
|
_logger->logDebugTrace("MusicXMLParserPass1::duration", &e);
|
|
|
|
_dura.set(0, 0); // invalid unless set correctly
|
|
int intDura = e.readElementText().toInt();
|
|
if (intDura > 0) {
|
|
if (_divs > 0) {
|
|
_dura.set(intDura, 4 * _divs);
|
|
_dura.reduce(); // prevent overflow in later Fraction operations
|
|
}
|
|
else
|
|
_logger->logError("illegal or uninitialized divisions", &e);
|
|
}
|
|
else
|
|
_logger->logError("illegal duration", &e);
|
|
//qDebug("duration %s valid %d", qPrintable(dura.print()), dura.isValid());
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// readProperties
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Handle selected child elements of the /score-partwise/part/measure/note/duration node.
|
|
Return true if handled.
|
|
*/
|
|
|
|
bool mxmlNoteDuration::readProperties(QXmlStreamReader& e)
|
|
{
|
|
const QStringRef& tag(e.name());
|
|
//qDebug("tag %s", qPrintable(tag.toString()));
|
|
if (tag == "dot") {
|
|
_dots++;
|
|
e.readNext();
|
|
return true;
|
|
}
|
|
else if (tag == "duration") {
|
|
duration(e);
|
|
return true;
|
|
}
|
|
else if (tag == "time-modification") {
|
|
timeModification(e);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// timeModification
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Parse the /score-partwise/part/measure/note/time-modification node.
|
|
*/
|
|
|
|
void mxmlNoteDuration::timeModification(QXmlStreamReader& e)
|
|
{
|
|
Q_ASSERT(e.isStartElement() && e.name() == "time-modification");
|
|
_logger->logDebugTrace("MusicXMLParserPass1::timeModification", &e);
|
|
|
|
int intActual = 0;
|
|
int intNormal = 0;
|
|
QString strActual;
|
|
QString strNormal;
|
|
|
|
while (e.readNextStartElement()) {
|
|
const QStringRef& tag(e.name());
|
|
if (tag == "actual-notes")
|
|
strActual = e.readElementText();
|
|
else if (tag == "normal-notes")
|
|
strNormal = e.readElementText();
|
|
else if (tag == "normal-type") {
|
|
// "measure" is not a valid normal-type,
|
|
// but would be accepted by setType()
|
|
QString strNormalType = e.readElementText();
|
|
if (strNormalType != "measure")
|
|
_normalType.setType(strNormalType);
|
|
}
|
|
else {
|
|
_logger->logDebugInfo(QString("skipping '%1'").arg(e.name().toString()), &e);
|
|
e.skipCurrentElement();
|
|
}
|
|
}
|
|
|
|
intActual = strActual.toInt();
|
|
intNormal = strNormal.toInt();
|
|
if (intActual > 0 && intNormal > 0)
|
|
_timeMod.set(intNormal, intActual);
|
|
else {
|
|
_timeMod.set(1, 1);
|
|
_logger->logError(QString("illegal time-modification: actual-notes %1 normal-notes %2")
|
|
.arg(strActual).arg(strNormal), &e);
|
|
}
|
|
}
|
|
|
|
}
|