Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MU3 Backend] ENG-55: Infer tempo text #8412

Merged
merged 7 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
164 changes: 137 additions & 27 deletions importexport/musicxml/importmxmlpass2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2320,6 +2320,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 @@ -2595,6 +2596,20 @@ static void preventNegativeTick(const Fraction& tick, Fraction& offset, MxmlLogg
}
}

//---------------------------------------------------------
// isTempoOrphanCandidate
//---------------------------------------------------------

bool MusicXMLDelayedDirectionElement::isTempoOrphanCandidate() const
{
return _element->isStaffText()
&& _placement == "above"
&& isBold();
iveshenry18 marked this conversation as resolved.
Show resolved Hide resolved
}

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

void MusicXMLDelayedDirectionElement::addElem()
{
Expand All @@ -2608,6 +2623,38 @@ QString MusicXMLParserDirection::placement() const
else return _placement;
}

//---------------------------------------------------------
// combineTempoText
//---------------------------------------------------------
/**
Combine potentially separated tempo text.
*/

void DelayedDirectionsList::combineTempoText()
{
// Iterate through candidates
for (auto ddi1 = rbegin(), ddi1Next = ddi1; ddi1 != rend(); ddi1 = ddi1Next) {
ddi1Next = std::next(ddi1);
if ((*ddi1)->isTempoOrphanCandidate()) {
for (auto ddi2 = rbegin(), ddi2Next = ddi2; ddi2 != rend(); ddi2 = ddi2Next) {
ddi2Next = std::next(ddi2);
// Combine with tempo text if present
if (ddi1 != ddi2
&& (*ddi2)->tick() == (*ddi1)->tick()
&& (*ddi2)->element()->isTempoText()) {
TempoText* tt = toTempoText((*ddi2)->element());
StaffText* st = toStaffText((*ddi1)->element());
QString sep = tt->plainText().endsWith(' ') || st->plainText().startsWith(' ') ? "" : " ";
tt->setXmlText(tt->xmlText() + sep + st->xmlText());
delete st;
iveshenry18 marked this conversation as resolved.
Show resolved Hide resolved
ddi1Next = decltype(ddi1){ erase(std::next(ddi1).base()) };
break;
}
}
}
}
}

//---------------------------------------------------------
// direction
//---------------------------------------------------------
Expand Down Expand Up @@ -2692,41 +2739,44 @@ void MusicXMLParserDirection::direction(const QString& partId,
}
else if (_wordsText != "" || _rehearsalText != "" || _metroText != "") {
TextBase* t = 0;
if (_tpoSound > 0.1) {
if (_tpoSound > 0.1 || attemptTempoTextCoercion(tick)) {
// 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("(<.*?>)"));
QString sep = _metroText != "" && _wordsText != "" && rawWordsText.back() != ' ' ? " " : "";
t->setXmlText(_wordsText + sep + _metroText);
((TempoText*) t)->setTempo(_tpoSound);
if (_tpoSound > 0.1) {
_tpoSound /= 60;
((TempoText*) t)->setTempo(_tpoSound);
_score->setTempo(tick, _tpoSound);
}
else {
((TempoText*) t)->setTempo(_score->tempo(tick)); // Maintain tempo (somewhat hacky)
}
((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 @@ -2756,7 +2806,7 @@ void MusicXMLParserDirection::direction(const QString& partId,
else {
// Add element to score later, after collecting all the others and sorting by default-y
// This allows default-y to be at least respected by the order of elements
MusicXMLDelayedDirectionElement* delayedDirection = new MusicXMLDelayedDirectionElement(totalY(), t, track, wordsPlacement, measure, tick + _offset);
MusicXMLDelayedDirectionElement* delayedDirection = new MusicXMLDelayedDirectionElement(totalY(), t, track, wordsPlacement, measure, tick + _offset, _isBold);
delayedDirections.push_back(delayedDirection);
}
}
Expand Down Expand Up @@ -2803,15 +2853,15 @@ void MusicXMLParserDirection::direction(const QString& partId,

// Add element to score later, after collecting all the others and sorting by default-y
// This allows default-y to be at least respected by the order of elements
MusicXMLDelayedDirectionElement* delayedDirection = new MusicXMLDelayedDirectionElement(totalY(), dyn, track, dynamicsPlacement, measure, tick + _offset);
MusicXMLDelayedDirectionElement* delayedDirection = new MusicXMLDelayedDirectionElement(totalY(), dyn, track, dynamicsPlacement, measure, tick + _offset, _isBold);
delayedDirections.push_back(delayedDirection);
}

// handle the elems
foreach( auto elem, _elems) {
for (auto elem : _elems) {
// Add element to score later, after collecting all the others and sorting by default-y
// This allows default-y to be at least respected by the order of elements
MusicXMLDelayedDirectionElement* delayedDirection = new MusicXMLDelayedDirectionElement(totalY(), elem, track, placement(), measure, tick + _offset);
MusicXMLDelayedDirectionElement* delayedDirection = new MusicXMLDelayedDirectionElement(totalY(), elem, track, placement(), measure, tick + _offset, _isBold);
delayedDirections.push_back(delayedDirection);
}

Expand Down Expand Up @@ -2889,6 +2939,7 @@ void MusicXMLParserDirection::directionType(QList<MusicXmlSpannerDesc>& starts,
_relativeY = relativeYCandidate;
_hasDefaultY |= hasDefaultYCandidate;
_hasRelativeY |= hasRelativeYCandidate;
_isBold &= _e.attributes().value("font-weight").toString() == "bold";
QString number = _e.attributes().value("number").toString();
int n = 0;
if (number != "") {
Expand Down Expand Up @@ -3262,10 +3313,70 @@ void MusicXMLInferredFingering::addToNotes(std::vector<Note*>& notes) const

MusicXMLDelayedDirectionElement* MusicXMLInferredFingering::toDelayedDirection()
{
auto dd = new MusicXMLDelayedDirectionElement(_totalY, _element, _track, _placement, _measure, _tick);
auto dd = new MusicXMLDelayedDirectionElement(_totalY, _element, _track, _placement, _measure, _tick, false);
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;
}

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

bool MusicXMLParserDirection::attemptTempoTextCoercion(const Fraction& tick)
{
QList<QString> tempoWords{"rit", "rall", "accel", "tempo", "allegr", "poco", "molto", "più", "meno", "mosso", "rubato"};
iveshenry18 marked this conversation as resolved.
Show resolved Hide resolved
if (_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;
}
else if (placement() == "above" && _isBold) {
if (tick == Fraction(0, 1)) return true;
for (auto tempoWord : tempoWords)
if (_wordsText.contains(tempoWord, Qt::CaseInsensitive))
return true;
}
return false;
}

//---------------------------------------------------------
// handleRepeats
Expand Down Expand Up @@ -5871,7 +5982,7 @@ void MusicXMLParserPass2::harmony(const QString& partId, Measure* measure, const
}
// Add element to score later, after collecting all the others and sorting by default-y
// This allows default-y to be at least respected by the order of elements
MusicXMLDelayedDirectionElement* delayedDirection = new MusicXMLDelayedDirectionElement(totalY, se, track, placement, measure, sTime + offset);
MusicXMLDelayedDirectionElement* delayedDirection = new MusicXMLDelayedDirectionElement(totalY, se, track, placement, measure, sTime + offset, false);
delayedDirections.push_back(delayedDirection);
}

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

/**
Combine any eligible articulations.
i.e. accent + staccato = staccato accent
Expand Down Expand Up @@ -7220,7 +7330,7 @@ MusicXMLParserDirection::MusicXMLParserDirection(QXmlStreamReader& e,
MusicXMLParserPass2& pass2,
MxmlLogger* logger)
: _e(e), _score(score), _pass1(pass1), _pass2(pass2), _logger(logger),
_hasDefaultY(false), _defaultY(0.0), _hasRelativeY(false), _relativeY(0.0),
_hasDefaultY(false), _defaultY(0.0), _hasRelativeY(false), _relativeY(0.0), _isBold(true),
_tpoMetro(0), _tpoSound(0), _offset(0, 1)
{
// nothing
Expand Down
29 changes: 24 additions & 5 deletions 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 @@ -382,6 +390,7 @@ class MusicXMLParserDirection {
bool _hasRelativeY;
qreal _relativeY;
bool hasTotalY() const { return _hasRelativeY || _hasDefaultY; }
bool _isBold;
double _tpoMetro; // tempo according to metronome
double _tpoSound; // tempo according to sound
QList<Element*> _elems;
Expand All @@ -403,6 +412,9 @@ class MusicXMLParserDirection {
bool isLyricBracket() const;
void textToDynamic(QString& text) const;
bool directionToDynamic();
bool isLikelyTempoText();
bool attemptTempoTextCoercion(const Fraction& tick);
double convertTextToNotes();
void skipLogCurrElem();
};

Expand All @@ -411,17 +423,23 @@ class MusicXMLParserDirection {
//---------------------------------------------------------
/**
Helper class to allow Direction elements to be sorted by _totalY
before being added to the score.
before being added to the score. TODO: merge into MusicXMLParserDirection.
*/

class MusicXMLDelayedDirectionElement {
public:
MusicXMLDelayedDirectionElement(qreal totalY, Element* element, int track,
QString placement, Measure* measure, Fraction tick) :
QString placement, Measure* measure, Fraction tick, bool isBold) :
_totalY(totalY), _element(element), _track(track), _placement(placement),
_measure(measure), _tick(tick) {}
void addElem();
_measure(measure), _tick(tick), _isBold(isBold) {}

qreal totalY() const { return _totalY; }
Element* element() { return _element; }
Fraction tick() const { return _tick; }

void addElem();
bool isBold() const { return _isBold; }
bool isTempoOrphanCandidate() const;

private:
qreal _totalY;
Expand All @@ -430,6 +448,7 @@ class MusicXMLDelayedDirectionElement {
QString _placement;
Measure* _measure;
Fraction _tick;
bool _isBold;
};

//---------------------------------------------------------
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