Skip to content

Commit

Permalink
ENG-55: Infer tempo text
Browse files Browse the repository at this point in the history
Sometimes tempo text is incorrectly exported as staff text, often in the
form "q = 60". This commit adds regex-based inferencing for this case,
converting the left side of the equals sign to the proper symbol, and
calculating the resulting tempo from the resulting symbol and the value
of the right side.

Duplicate of musescore#8412, part 1
  • Loading branch information
iveshenry18 authored and Jojo-Schmitz committed Mar 5, 2023
1 parent f6b4244 commit 571c9eb
Show file tree
Hide file tree
Showing 8 changed files with 2,887 additions and 22 deletions.
125 changes: 104 additions & 21 deletions importexport/musicxml/importmxmlpass2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2326,6 +2326,7 @@ void MusicXMLParserPass2::measure(const QString& partId,
}

// Sort and add delayed directions
// delayedDirections.combineTempoText();
std::sort(delayedDirections.begin(), delayedDirections.end(),
// Lambda: sort by absolute value of totalY
[](const MusicXMLDelayedDirectionElement* a, const MusicXMLDelayedDirectionElement* b) -> bool {
Expand Down Expand Up @@ -2602,6 +2603,10 @@ static void preventNegativeTick(const Fraction& tick, Fraction& offset, MxmlLogg
}


//---------------------------------------------------------
// addElem
//---------------------------------------------------------

void MusicXMLDelayedDirectionElement::addElem()
{
addElemOffset(_element, _track, _placement, _measure, _tick);
Expand All @@ -2614,6 +2619,32 @@ QString MusicXMLParserDirection::placement() const
else return _placement;
}

//---------------------------------------------------------
// combineTempoText
//---------------------------------------------------------
/**
Combine potentially separated tempo text.
TODO: Use flags (hasTempoCandidate, etc.) to make more efficient
*/

void DelayedDirectionsList::combineTempoText()
{
// Iterate through each distinct pair (backwards, to allow for deletions)
for (auto ddi1 = rbegin(), ddi1Next = ddi1; ddi1 != rend(); ddi1 = ddi1Next) {
ddi1Next = std::next(ddi1);
if (false)
continue;
for (auto ddi2 = ddi1 + 1, ddi2Next = ddi1; ddi2 != rend(); ddi2 = ddi2Next) {
ddi2Next = std::next(ddi2);
// Combine and remove articulations if present in map
if (false) {
ddi1Next = decltype(ddi1){ erase(std::next(ddi1).base()) };
ddi2Next = decltype(ddi2){ erase(std::next(ddi2).base()) };
}
}
}
}

//---------------------------------------------------------
// direction
//---------------------------------------------------------
Expand Down Expand Up @@ -2698,13 +2729,12 @@ void MusicXMLParserDirection::direction(const QString& partId,
}
else if (_wordsText != "" || _rehearsalText != "" || _metroText != "") {
TextBase* t = 0;
if (_tpoSound > 0.1) {
if (_tpoSound > 0.1 || isLikelyTempoText()) {
// to prevent duplicates, only create a TempoText if none is present yet
if (hasTempoTextAtTick(_score->tempomap(), tick.ticks())) {
_logger->logError(QString("duplicate tempo at tick %1").arg(tick.ticks()), &_e);
}
else {
_tpoSound /= 60;
t = new TempoText(_score);
QString rawWordsText = _wordsText;
rawWordsText.remove(QRegularExpression("(<.*?>)"));
Expand All @@ -2714,29 +2744,30 @@ void MusicXMLParserDirection::direction(const QString& partId,
QString sep = _metroText != "" && _wordsText != "" && rawWordsText.at(rawWordsText.size() - 1) != ' ' ? " " : "";
#endif
t->setXmlText(_wordsText + sep + _metroText);
((TempoText*) t)->setTempo(_tpoSound);
((TempoText*) t)->setFollowText(true);
_score->setTempo(tick, _tpoSound);
if (_tpoSound > 0.1) {
_tpoSound /= 60;
((TempoText*) t)->setTempo(_tpoSound);
((TempoText*) t)->setFollowText(true);
_score->setTempo(tick, _tpoSound);
}
}
}
else if (_wordsText != "" || _metroText != "") {
t = new StaffText(_score);
t->setXmlText(_wordsText + _metroText);
isExpressionText = _wordsText.contains("<i>") && _metroText.isEmpty();
}
else {
if (_wordsText != "" || _metroText != "") {
t = new StaffText(_score);
t->setXmlText(_wordsText + _metroText);
isExpressionText = _wordsText.contains("<i>") && _metroText.isEmpty();
}
else {
t = new RehearsalMark(_score);
if (!_rehearsalText.contains("<b>"))
_rehearsalText = "<b></b>" + _rehearsalText; // explicitly turn bold off
t->setXmlText(_rehearsalText);
if (!_hasDefaultY) {
t->setPlacement(Placement::ABOVE); // crude way to force placement TODO improve ?
t->setPropertyFlags(Pid::PLACEMENT, PropertyFlags::UNSTYLED);
}
t = new RehearsalMark(_score);
if (!_rehearsalText.contains("<b>"))
_rehearsalText = "<b></b>" + _rehearsalText; // explicitly turn bold off
t->setXmlText(_rehearsalText);
if (!_hasDefaultY) {
t->setPlacement(Placement::ABOVE); // crude way to force placement TODO improve ?
t->setPropertyFlags(Pid::PLACEMENT, PropertyFlags::UNSTYLED);
}
}

if (t) {
if (_enclosure == "circle") {
t->setFrameType(FrameType::CIRCLE);
Expand Down Expand Up @@ -3277,6 +3308,59 @@ MusicXMLDelayedDirectionElement* MusicXMLInferredFingering::toDelayedDirection()
return dd;
}

//---------------------------------------------------------
// convertTextToNotes
//---------------------------------------------------------
/**
Converts note characters in _wordsText to proper symbols and
returns the tempo value of the resulting notes
*/

double MusicXMLParserDirection::convertTextToNotes()
{
QRegularExpression notesRegex("(?<note>[yxeqhwW]\\.{0,2})(\\s*=)");
QString notesSubstring = notesRegex.match(_wordsText).captured("note");

QList<QPair<QString, QString>> noteSyms{{"q", QString("<sym>metNoteQuarterUp</sym>")}, // note4_Sym
{"e", QString("<sym>metNote8thUp</sym>")}, // note8_Sym
{"h", QString("<sym>metNoteHalfUp</sym>")}, // note2_Sym
{"y", QString("<sym>metNote32ndUp</sym>")}, // note32_Sym
{"x", QString("<sym>metNote16thUp</sym>")}, // note16_Sym
{"w", QString("<sym>metNoteWhole</sym>")},
{"W", QString("<sym>metNoteDoubleWhole</sym>")}};
for (auto noteSym : noteSyms) {
if (notesSubstring.contains(noteSym.first)) {
notesSubstring.replace(noteSym.first, noteSym.second);
break;
}
}
notesSubstring.replace(".", QString("<sym>metAugmentationDot</sym>")); // dot
_wordsText.replace(notesRegex, notesSubstring + "\\2");

double tempoValue = TempoText::findTempoValue(_wordsText);
if (!tempoValue) tempoValue = 1.0 / 60.0; // default to quarter note
return tempoValue;
}

//---------------------------------------------------------
// isLikelyTempoText
//---------------------------------------------------------
/**
Infers if a direction is likely tempo text, possibly changing
the _wordsText to the appropriate note symbol and inferring the _tpoSound.
*/

bool MusicXMLParserDirection::isLikelyTempoText()
{
if (_tpoSound < 0.1 && _wordsText.contains(QRegularExpression("[yxeqhwW.]+\\s*=\\s*\\d+"))) {
QRegularExpression tempoValRegex("=\\s*(?<tempo>\\d+)");
double tempoVal = tempoValRegex.match(_wordsText).captured("tempo").toDouble();
double noteVal = convertTextToNotes() * 60.0;
_tpoSound = tempoVal / noteVal;
return true;
}
return false;
}

//---------------------------------------------------------
// handleRepeats
Expand Down Expand Up @@ -6897,7 +6981,6 @@ bool MusicXMLParserNotations::skipCombine(const Notation& n1, const Notation& n2
//---------------------------------------------------------
// combineArticulations
//---------------------------------------------------------

/**
Combine any eligible articulations.
i.e. accent + staccato = staccato accent
Expand Down
12 changes: 11 additions & 1 deletion importexport/musicxml/importmxmlpass2.h
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ class MxmlLogger;
class MusicXMLDelayedDirectionElement;
class MusicXMLInferredFingering;

using DelayedDirectionsList = QList<MusicXMLDelayedDirectionElement*>;
using InferredFingeringsList = QList<MusicXMLInferredFingering*>;
using SlurStack = std::array<SlurDesc, MAX_NUMBER_LEVEL>;
using TrillStack = std::array<Trill*, MAX_NUMBER_LEVEL>;
Expand All @@ -195,6 +194,15 @@ using HairpinsStack = std::array<MusicXmlExtendedSpannerDesc, MAX_NUMBER_LEVEL>;
using SpannerStack = std::array<MusicXmlExtendedSpannerDesc, MAX_NUMBER_LEVEL>;
using SpannerSet = std::set<Spanner*>;

//---------------------------------------------------------
// DelayedDirectionsList
//---------------------------------------------------------

class DelayedDirectionsList : public QList<MusicXMLDelayedDirectionElement*> {
public:
void combineTempoText();
};

//---------------------------------------------------------
// MusicXMLParserNotations
//---------------------------------------------------------
Expand Down Expand Up @@ -403,6 +411,8 @@ class MusicXMLParserDirection {
bool isLyricBracket() const;
void textToDynamic(QString& text) const;
bool directionToDynamic();
bool isLikelyTempoText();
double convertTextToNotes();
void skipLogCurrElem();
};

Expand Down
17 changes: 17 additions & 0 deletions libmscore/tempotext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,23 @@ static const TempoPattern tpSym[] = {
TempoPattern("<sym>metNote1024thUp</sym>", 1.0/15360.0,TDuration::DurationType::V_1024TH), // 1/1024
};

//---------------------------------------------------------
// findTempoValue
// find the value (fraction of a minute) of the symbols
// in a string.
//---------------------------------------------------------

double TempoText::findTempoValue(const QString& s)
{
for (const auto& i : tpSym) {
QRegularExpression re(i.pattern);
if (s.contains(re)) {
return i.f;
}
}
return 0;
}

//---------------------------------------------------------
// duration2tempoTextString
// find the tempoText string representation for duration
Expand Down
1 change: 1 addition & 0 deletions libmscore/tempotext.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class TempoText final : public TextBase {
void layout() override;

static int findTempoDuration(const QString& s, int& len, TDuration& dur);
static double findTempoValue(const QString& s);
static QString duration2tempoTextString(const TDuration dur);
static QString duration2userName(const TDuration t);

Expand Down

0 comments on commit 571c9eb

Please sign in to comment.