MuseScore/src/importexport/musicxml/internal/musicxml/exportxml.cpp

7599 lines
262 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-CLA-applies
*
* MuseScore
* Music Composition & Notation
*
* Copyright (C) 2021 MuseScore BVBA 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 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
MusicXML export.
*/
// TODO: trill lines need to be handled the same way as slurs
// in MuseScore they are measure level elements, while in MusicXML
// they are attached to notes (as ornaments)
//=========================================================
// LVI FIXME
//
// Evaluate parameter handling between the various classes, could be simplified
//=========================================================
// TODO LVI 2011-10-30: determine how to report export errors.
// Currently all output (both debug and error reports) are done using LOGD.
#include "exportxml.h"
#include <math.h>
#include <QBuffer>
#include <QDate>
#include <QRegularExpression>
#include "containers.h"
#include "io/iodevice.h"
#include "io/buffer.h"
#include "io/fileinfo.h"
#include "global/deprecated/qzipwriter_p.h"
#include "engraving/style/style.h"
#include "engraving/rw/xml.h"
#include "engraving/types/typesconv.h"
#include "engraving/types/symnames.h"
#include "libmscore/accidental.h"
#include "libmscore/arpeggio.h"
#include "libmscore/articulation.h"
#include "libmscore/barline.h"
#include "libmscore/beam.h"
#include "libmscore/bracket.h"
#include "libmscore/breath.h"
#include "libmscore/chord.h"
#include "libmscore/chordline.h"
#include "libmscore/clef.h"
#include "libmscore/drumset.h"
#include "libmscore/dynamic.h"
#include "libmscore/engravingitem.h"
#include "libmscore/factory.h"
#include "libmscore/fermata.h"
#include "libmscore/figuredbass.h"
#include "libmscore/fret.h"
#include "libmscore/glissando.h"
#include "libmscore/gradualtempochange.h"
#include "libmscore/hairpin.h"
#include "libmscore/harmonicmark.h"
#include "libmscore/harmony.h"
#include "libmscore/instrchange.h"
#include "libmscore/jump.h"
#include "libmscore/key.h"
#include "libmscore/keysig.h"
#include "libmscore/layoutbreak.h"
#include "libmscore/letring.h"
#include "libmscore/lyrics.h"
#include "libmscore/marker.h"
#include "libmscore/masterscore.h"
#include "libmscore/measure.h"
#include "libmscore/measurerepeat.h"
#include "libmscore/mscore.h"
#include "libmscore/note.h"
#include "libmscore/ottava.h"
#include "libmscore/page.h"
#include "libmscore/palmmute.h"
#include "libmscore/part.h"
#include "libmscore/pedal.h"
#include "libmscore/pickscrape.h"
#include "libmscore/pitchspelling.h"
#include "libmscore/rasgueado.h"
#include "libmscore/rehearsalmark.h"
#include "libmscore/rest.h"
#include "libmscore/segment.h"
#include "libmscore/sig.h"
#include "libmscore/slur.h"
#include "libmscore/spanner.h"
#include "libmscore/staff.h"
#include "libmscore/stringdata.h"
#include "libmscore/system.h"
#include "libmscore/tempotext.h"
#include "libmscore/text.h"
#include "libmscore/textframe.h"
#include "libmscore/textlinebase.h"
#include "libmscore/tie.h"
#include "libmscore/timesig.h"
#include "libmscore/tremolo.h"
#include "libmscore/trill.h"
#include "libmscore/tuplet.h"
#include "libmscore/undo.h"
#include "libmscore/utils.h"
#include "libmscore/volta.h"
#include "libmscore/whammybar.h"
#include "musicxml.h"
#include "musicxmlfonthandler.h"
#include "musicxmlsupport.h"
#include "modularity/ioc.h"
#include "../../imusicxmlconfiguration.h"
#include "engraving/iengravingconfiguration.h"
#include "log.h"
using namespace mu;
using namespace mu::io;
using namespace mu::iex::musicxml;
using namespace mu::engraving;
namespace mu::engraving {
//---------------------------------------------------------
// local defines for debug output
//---------------------------------------------------------
// #define DEBUG_CLEF true
// #define DEBUG_REPEATS true
// #define DEBUG_TICK true
#ifdef DEBUG_CLEF
#define clefDebug(...) LOGD(__VA_ARGS__)
#else
#define clefDebug(...) {}
#endif
//---------------------------------------------------------
// typedefs
//---------------------------------------------------------
typedef QMap<track_idx_t, const FiguredBass*> FigBassMap;
//---------------------------------------------------------
// attributes -- prints <attributes> tag when necessary
//---------------------------------------------------------
class Attributes
{
bool inAttributes;
public:
Attributes() { start(); }
void doAttr(XmlWriter& xml, bool attr);
void start();
void stop(XmlWriter& xml);
};
//---------------------------------------------------------
// doAttr - when necessary change state and print <attributes> tag
//---------------------------------------------------------
void Attributes::doAttr(XmlWriter& xml, bool attr)
{
if (!inAttributes && attr) {
xml.startElement("attributes");
inAttributes = true;
} else if (inAttributes && !attr) {
xml.endElement();
inAttributes = false;
}
}
//---------------------------------------------------------
// start -- initialize
//---------------------------------------------------------
void Attributes::start()
{
inAttributes = false;
}
//---------------------------------------------------------
// stop -- print </attributes> tag when necessary
//---------------------------------------------------------
void Attributes::stop(XmlWriter& xml)
{
if (inAttributes) {
xml.endElement();
inAttributes = false;
}
}
//---------------------------------------------------------
// notations -- prints <notations> tag when necessary
//---------------------------------------------------------
class Notations
{
bool notationsPrinted = false;
bool prevElementVisible = true;
public:
void tag(XmlWriter& xml, const EngravingItem* e);
void etag(XmlWriter& xml);
};
//---------------------------------------------------------
// articulations -- prints <articulations> tag when necessary
//---------------------------------------------------------
class Articulations
{
bool articulationsPrinted;
public:
Articulations() { articulationsPrinted = false; }
void tag(XmlWriter& xml);
void etag(XmlWriter& xml);
};
//---------------------------------------------------------
// ornaments -- prints <ornaments> tag when necessary
//---------------------------------------------------------
class Ornaments
{
bool ornamentsPrinted;
public:
Ornaments() { ornamentsPrinted = false; }
void tag(XmlWriter& xml);
void etag(XmlWriter& xml);
};
//---------------------------------------------------------
// technical -- prints <technical> tag when necessary
//---------------------------------------------------------
class Technical
{
bool technicalPrinted;
public:
Technical() { technicalPrinted = false; }
void tag(XmlWriter& xml);
void etag(XmlWriter& xml);
};
//---------------------------------------------------------
// slur handler -- prints <slur> tags
//---------------------------------------------------------
class SlurHandler
{
const Slur* slur[MAX_NUMBER_LEVEL];
bool started[MAX_NUMBER_LEVEL];
int findSlur(const Slur* s) const;
public:
SlurHandler();
void doSlurs(const ChordRest* chordRest, Notations& notations, XmlWriter& xml);
private:
void doSlurStart(const Slur* s, Notations& notations, XmlWriter& xml);
void doSlurStop(const Slur* s, Notations& notations, XmlWriter& xml);
};
//---------------------------------------------------------
// glissando handler -- prints <glissando> tags
//---------------------------------------------------------
class GlissandoHandler
{
const Note* glissNote[MAX_NUMBER_LEVEL];
const Note* slideNote[MAX_NUMBER_LEVEL];
int findNote(const Note* note, int type) const;
public:
GlissandoHandler();
void doGlissandoStart(Glissando* gliss, Notations& notations, XmlWriter& xml);
void doGlissandoStop(Glissando* gliss, Notations& notations, XmlWriter& xml);
};
//---------------------------------------------------------
// MeasureNumberStateHandler
//---------------------------------------------------------
/**
State handler used to calculate measure number including implicit flag.
To be called once at the start of each measure in a part.
*/
class MeasureNumberStateHandler final
{
public:
MeasureNumberStateHandler();
void updateForMeasure(const Measure* const m);
QString measureNumber() const;
bool isFirstActualMeasure() const;
private:
void init();
int _measureNo; // number of next regular measure
int _irregularMeasureNo; // number of next irregular measure
int _pickupMeasureNo; // number of next pickup measure
QString _cachedAttributes; // attributes calculated by updateForMeasure()
};
//---------------------------------------------------------
// MeasurePrintContext
//---------------------------------------------------------
struct MeasurePrintContext final
{
void measureWritten(const Measure* m);
bool scoreStart = true;
bool pageStart = true;
bool systemStart = true;
const Measure* prevMeasure = nullptr;
const System* prevSystem = nullptr;
const System* lastSystemPrevPage = nullptr;
};
//---------------------------------------------------------
// ExportMusicXml
//---------------------------------------------------------
typedef QHash<const ChordRest* const, const Trill*> TrillHash;
typedef QMap<const Instrument*, int> MxmlInstrumentMap;
class ExportMusicXml
{
INJECT_STATIC(iex_musicxml, mu::iex::musicxml::IMusicXmlConfiguration, configuration)
Score* _score;
XmlWriter _xml;
SlurHandler sh;
GlissandoHandler gh;
Fraction _tick;
Attributes _attr;
TextLineBase const* brackets[MAX_NUMBER_LEVEL];
TextLineBase const* dashes[MAX_NUMBER_LEVEL];
Hairpin const* hairpins[MAX_NUMBER_LEVEL];
Ottava const* ottavas[MAX_NUMBER_LEVEL];
Trill const* trills[MAX_NUMBER_LEVEL];
std::vector<const Jump*> _jumpElements;
int div;
double millimeters;
int tenths;
bool _tboxesAboveWritten;
bool _tboxesBelowWritten;
TrillHash _trillStart;
TrillHash _trillStop;
MxmlInstrumentMap instrMap;
int findBracket(const TextLineBase* tl) const;
int findDashes(const TextLineBase* tl) const;
int findHairpin(const Hairpin* tl) const;
int findOttava(const Ottava* tl) const;
int findTrill(const Trill* tl) const;
void chord(Chord* chord, staff_idx_t staff, const std::vector<Lyrics*>& ll, bool useDrumset);
void rest(Rest* chord, staff_idx_t staff);
void clef(staff_idx_t staff, const ClefType ct, const QString& extraAttributes = "");
void timesig(TimeSig* tsig);
void keysig(const KeySig* ks, ClefType ct, staff_idx_t staff = 0, bool visible = true);
void barlineLeft(const Measure* const m);
void barlineMiddle(const BarLine* bl);
void barlineRight(const Measure* const m, const track_idx_t strack, const track_idx_t etrack);
void lyrics(const std::vector<Lyrics*>& ll, const track_idx_t trk);
void work(const MeasureBase* measure);
void calcDivMoveToTick(const Fraction& t);
void calcDivisions();
void keysigTimesig(const Measure* m, const Part* p);
void chordAttributes(Chord* chord, Notations& notations, Technical& technical, TrillHash& trillStart, TrillHash& trillStop);
void wavyLineStartStop(const ChordRest* const cr, Notations& notations, Ornaments& ornaments, TrillHash& trillStart,
TrillHash& trillStop);
void print(const Measure* const m, const int partNr, const int firstStaffOfPart, const int nrStavesInPart,
const MeasurePrintContext& mpc);
void findAndExportClef(const Measure* const m, const int staves, const track_idx_t strack, const track_idx_t etrack);
void exportDefaultClef(const Part* const part, const Measure* const m);
void writeElement(EngravingItem* el, const Measure* m, staff_idx_t sstaff, bool useDrumset);
void writeMeasureTracks(const Measure* const m, const int partIndex, const staff_idx_t strack, const staff_idx_t partRelStaffNo,
const bool useDrumset, const bool isLastStaffOfPart, FigBassMap& fbMap, QSet<const Spanner*>& spannersStopped);
void writeMeasureStaves(const Measure* m, const int partIndex, const staff_idx_t startStaff, const size_t nstaves,
const bool useDrumset, FigBassMap& fbMap, QSet<const Spanner*>& spannersStopped);
void writeMeasure(const Measure* const m, const int idx, const int staffCount, MeasureNumberStateHandler& mnsh, FigBassMap& fbMap,
const MeasurePrintContext& mpc, QSet<const Spanner*>& spannersStopped);
void repeatAtMeasureStart(Attributes& attr, const Measure* const m, track_idx_t strack, track_idx_t etrack, track_idx_t track);
void repeatAtMeasureStop(const Measure* const m, track_idx_t strack, track_idx_t etrack, track_idx_t track);
void writeParts();
static QString fermataPosition(const Fermata* const fermata);
static QString elementPosition(const ExportMusicXml* const expMxml, const EngravingItem* const elm);
static QString positioningAttributes(EngravingItem const* const el, bool isSpanStart = true);
static QString positioningAttributesForTboxText(const QPointF position, float spatium);
static void identification(XmlWriter& xml, Score const* const score);
public:
ExportMusicXml(Score* s)
{
_score = s;
_tick = { 0, 1 };
div = 1;
tenths = 40;
millimeters = _score->spatium() * tenths / (10 * DPMM);
}
void write(mu::io::IODevice* dev);
void credits(XmlWriter& xml);
void moveToTick(const Fraction& t);
void words(TextBase const* const text, staff_idx_t staff);
void tboxTextAsWords(TextBase const* const text, const staff_idx_t staff, QPointF position);
void rehearsal(RehearsalMark const* const rmk, staff_idx_t staff);
void hairpin(Hairpin const* const hp, staff_idx_t staff, const Fraction& tick);
void ottava(Ottava const* const ot, staff_idx_t staff, const Fraction& tick);
void pedal(Pedal const* const pd, staff_idx_t staff, const Fraction& tick);
void textLine(TextLineBase const* const tl, staff_idx_t staff, const Fraction& tick);
void dynamic(Dynamic const* const dyn, staff_idx_t staff);
void symbol(Symbol const* const sym, staff_idx_t staff);
void tempoText(TempoText const* const text, staff_idx_t staff);
void harmony(Harmony const* const, FretDiagram const* const fd, int offset = 0);
Score* score() const { return _score; }
double getTenthsFromInches(double) const;
double getTenthsFromDots(double) const;
Fraction tick() const { return _tick; }
void writeInstrumentDetails(const Instrument* instrument);
static bool canWrite(const EngravingItem* e);
};
//---------------------------------------------------------
// positionToQString
//---------------------------------------------------------
static QString positionToQString(const QPointF def, const QPointF rel, const float spatium)
{
// minimum value to export
const float positionElipson = 0.1f;
// convert into tenths for MusicXML
const float defaultX = 10 * def.x() / spatium;
const float defaultY = -10 * def.y() / spatium;
const float relativeX = 10 * rel.x() / spatium;
const float relativeY = -10 * rel.y() / spatium;
// generate string representation
QString res;
if (fabsf(defaultX) > positionElipson) {
res += QString(" default-x=\"%1\"").arg(QString::number(defaultX, 'f', 2));
}
if (fabsf(defaultY) > positionElipson) {
res += QString(" default-y=\"%1\"").arg(QString::number(defaultY, 'f', 2));
}
if (fabsf(relativeX) > positionElipson) {
res += QString(" relative-x=\"%1\"").arg(QString::number(relativeX, 'f', 2));
}
if (fabsf(relativeY) > positionElipson) {
res += QString(" relative-y=\"%1\"").arg(QString::number(relativeY, 'f', 2));
}
return res;
}
//---------------------------------------------------------
// positioningAttributes
// According to the specs (common.dtd), all direction-type and note elements must be relative to the measure
// while all other elements are relative to their position or the nearest note.
//---------------------------------------------------------
QString ExportMusicXml::positioningAttributes(EngravingItem const* const el, bool isSpanStart)
{
if (!configuration()->musicxmlExportLayout()) {
return "";
}
//LOGD("single el %p _pos x,y %f %f _userOff x,y %f %f spatium %f",
// el, el->ipos().x(), el->ipos().y(), el->offset().x(), el->offset().y(), el->spatium());
QPointF def;
QPointF rel;
float spatium = el->spatium();
const SLine* span = nullptr;
if (el->isSLine()) {
span = static_cast<const SLine*>(el);
}
if (span && !span->segmentsEmpty()) {
if (isSpanStart) {
const auto seg = span->frontSegment();
const auto offset = seg->offset();
const auto p = seg->pos();
rel.setX(offset.x());
def.setY(p.y());
//LOGD("sline start seg %p seg->pos x,y %f %f seg->userOff x,y %f %f spatium %f",
// seg, p.x(), p.y(), seg->offset().x(), seg->offset().y(), seg->spatium());
} else {
const auto seg = span->backSegment();
const auto userOff = seg->offset(); // This is the offset accessible from the inspector
const auto userOff2 = seg->userOff2(); // Offset of the actual dragged anchor, which doesn't affect the inspector offset
//auto pos = seg->pos();
//auto pos2 = seg->pos2();
//LOGD("sline stop seg %p seg->pos2 x,y %f %f seg->userOff2 x,y %f %f spatium %f",
// seg, pos2.x(), pos2.y(), seg->userOff2().x(), seg->userOff2().y(), seg->spatium());
// For an SLine, the actual offset equals the sum of userOff and userOff2,
// as userOff moves the SLine as a whole
rel.setX(userOff.x() + userOff2.x());
// Following would probably required for non-horizontal SLines:
//defaultY = pos.y() + pos2.y();
}
} else {
def = el->ipos().toQPointF(); // Note: for some elements, Finale Notepad seems to work slightly better w/o default-x
rel = el->offset().toQPointF();
}
return positionToQString(def, rel, spatium);
}
//---------------------------------------------------------
// tag
//---------------------------------------------------------
void Notations::tag(XmlWriter& xml, const EngravingItem* e)
{
if (notationsPrinted && prevElementVisible != e->visible()) {
etag(xml);
}
if (!notationsPrinted) {
if (e->visible()) {
xml.startElement("notations");
} else {
xml.startElement("notations", { { "print-object", "no" } });
}
}
notationsPrinted = true;
prevElementVisible = e->visible();
}
//---------------------------------------------------------
// etag
//---------------------------------------------------------
void Notations::etag(XmlWriter& xml)
{
if (notationsPrinted) {
xml.endElement();
}
notationsPrinted = false;
}
//---------------------------------------------------------
// tag
//---------------------------------------------------------
void Articulations::tag(XmlWriter& xml)
{
if (!articulationsPrinted) {
xml.startElement("articulations");
}
articulationsPrinted = true;
}
//---------------------------------------------------------
// etag
//---------------------------------------------------------
void Articulations::etag(XmlWriter& xml)
{
if (articulationsPrinted) {
xml.endElement();
}
articulationsPrinted = false;
}
//---------------------------------------------------------
// tag
//---------------------------------------------------------
void Ornaments::tag(XmlWriter& xml)
{
if (!ornamentsPrinted) {
xml.startElement("ornaments");
}
ornamentsPrinted = true;
}
//---------------------------------------------------------
// etag
//---------------------------------------------------------
void Ornaments::etag(XmlWriter& xml)
{
if (ornamentsPrinted) {
xml.endElement();
}
ornamentsPrinted = false;
}
//---------------------------------------------------------
// tag
//---------------------------------------------------------
void Technical::tag(XmlWriter& xml)
{
if (!technicalPrinted) {
xml.startElement("technical");
}
technicalPrinted = true;
}
//---------------------------------------------------------
// etag
//---------------------------------------------------------
void Technical::etag(XmlWriter& xml)
{
if (technicalPrinted) {
xml.endElement();
}
technicalPrinted = false;
}
static std::shared_ptr<mu::engraving::IEngravingConfiguration> engravingConfiguration()
{
return mu::modularity::ioc()->resolve<mu::engraving::IEngravingConfiguration>("iex_musicxml");
}
//---------------------------------------------------------
// color2xml
//---------------------------------------------------------
/**
Return \a el color.
*/
static QString color2xml(const EngravingItem* el)
{
if (el->color() != engravingConfiguration()->defaultColor()) {
return QString(" color=\"%1\"").arg(QString::fromStdString(el->color().toString()).toUpper());
} else {
return "";
}
}
static void addColorAttr(const EngravingItem* el, XmlWriter::Attributes& attrs)
{
if (el->color() != engravingConfiguration()->defaultColor()) {
attrs.push_back({ "color", QString::fromStdString(el->color().toString()).toUpper() });
}
}
//---------------------------------------------------------
// fontStyleToXML
//---------------------------------------------------------
static QString fontStyleToXML(const FontStyle style, bool allowUnderline = true)
{
QString res;
if (style & FontStyle::Bold) {
res += " font-weight=\"bold\"";
}
if (style & FontStyle::Italic) {
res += " font-style=\"italic\"";
}
if (allowUnderline && style & FontStyle::Underline) {
res += " underline=\"1\"";
}
// at places where underline is not wanted (e.g. fingering, pluck), strike is not wanted too
if (allowUnderline && style & FontStyle::Strike) {
res += " line-through=\"1\"";
}
return res;
}
//---------------------------------------------------------
// slurHandler
//---------------------------------------------------------
SlurHandler::SlurHandler()
{
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
slur[i] = 0;
started[i] = false;
}
}
static QString slurTieLineStyle(const SlurTie* s)
{
QString lineType;
QString rest;
switch (s->styleType()) {
case SlurStyleType::Dotted:
lineType = "dotted";
break;
case SlurStyleType::Dashed:
lineType = "dashed";
break;
default:
lineType = "";
}
if (!lineType.isEmpty()) {
rest = QString(" line-type=\"%1\"").arg(lineType);
}
return rest;
}
//---------------------------------------------------------
// findSlur -- get index of slur in slur table
// return -1 if not found
//---------------------------------------------------------
int SlurHandler::findSlur(const Slur* s) const
{
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
if (slur[i] == s) {
return i;
}
}
return -1;
}
//---------------------------------------------------------
// findFirstChordRest -- find first chord or rest (in musical order) for slur s
// note that this is not necessarily the same as s->startElement()
//---------------------------------------------------------
static const ChordRest* findFirstChordRest(const Slur* s)
{
const EngravingItem* e1 = s->startElement();
if (!e1 || !(e1->isChordRest())) {
LOGD("no valid start element for slur %p", s);
return nullptr;
}
const EngravingItem* e2 = s->endElement();
if (!e2 || !(e2->isChordRest())) {
LOGD("no valid end element for slur %p", s);
return nullptr;
}
if (e1->tick() < e2->tick()) {
return static_cast<const ChordRest*>(e1);
} else if (e1->tick() > e2->tick()) {
return static_cast<const ChordRest*>(e2);
}
if (e1->isRest() || e2->isRest()) {
return nullptr;
}
const auto c1 = static_cast<const Chord*>(e1);
const auto c2 = static_cast<const Chord*>(e2);
// c1->tick() == c2->tick()
if (!c1->isGrace() && !c2->isGrace()) {
// slur between two regular notes at the same tick
// probably shouldn't happen but handle just in case
LOGD("invalid slur between chords %p and %p at tick %d", c1, c2, c1->tick().ticks());
return 0;
} else if (c1->isGraceBefore() && !c2->isGraceBefore()) {
return c1; // easy case: c1 first
} else if (c1->isGraceAfter() && !c2->isGraceAfter()) {
return c2; // easy case: c2 first
} else if (c2->isGraceBefore() && !c1->isGraceBefore()) {
return c2; // easy case: c2 first
} else if (c2->isGraceAfter() && !c1->isGraceAfter()) {
return c1; // easy case: c1 first
} else {
// both are grace before or both are grace after -> compare grace indexes
// (note: higher means closer to the non-grace chord it is attached to)
if ((c1->isGraceBefore() && c1->graceIndex() < c2->graceIndex())
|| (c1->isGraceAfter() && c1->graceIndex() > c2->graceIndex())) {
return c1;
} else {
return c2;
}
}
}
//---------------------------------------------------------
// doSlurs
//---------------------------------------------------------
void SlurHandler::doSlurs(const ChordRest* chordRest, Notations& notations, XmlWriter& xml)
{
// loop over all slurs twice, first to handle the stops, then the starts
for (int i = 0; i < 2; ++i) {
// search for slur(s) starting or stopping at this chord
for (const auto& it : chordRest->score()->spanner()) {
auto sp = it.second;
if (sp->generated() || sp->type() != ElementType::SLUR || !ExportMusicXml::canWrite(sp)) {
continue;
}
if (chordRest == sp->startElement() || chordRest == sp->endElement()) {
const auto s = static_cast<const Slur*>(sp);
const auto firstChordRest = findFirstChordRest(s);
if (firstChordRest) {
if (i == 0) {
// first time: do slur stops
if (firstChordRest != chordRest) {
doSlurStop(s, notations, xml);
}
} else {
// second time: do slur starts
if (firstChordRest == chordRest) {
doSlurStart(s, notations, xml);
}
}
}
}
}
}
}
//---------------------------------------------------------
// doSlurStart
//---------------------------------------------------------
void SlurHandler::doSlurStart(const Slur* s, Notations& notations, XmlWriter& xml)
{
// check if on slur list (i.e. stop already seen)
int i = findSlur(s);
// compose tag
QString tagName = "slur";
tagName += slurTieLineStyle(s); // define line type
tagName += color2xml(s);
tagName += QString(" type=\"start\" placement=\"%1\"")
.arg(s->up() ? "above" : "below");
tagName += ExportMusicXml::positioningAttributes(s, true);
if (i >= 0) {
// remove from list and print start
slur[i] = 0;
started[i] = false;
notations.tag(xml, s);
tagName += QString(" number=\"%1\"").arg(i + 1);
xml.tagRaw(tagName);
} else {
// find free slot to store it
i = findSlur(0);
if (i >= 0) {
slur[i] = s;
started[i] = true;
notations.tag(xml, s);
tagName += QString(" number=\"%1\"").arg(i + 1);
xml.tagRaw(tagName);
} else {
LOGD("no free slur slot");
}
}
}
//---------------------------------------------------------
// doSlurStop
//---------------------------------------------------------
// Note: a slur may start in a higher voice in the same measure.
// In that case it is not yet started (i.e. on the active slur list)
// when doSlurStop() is executed. Handle this slur as follows:
// - generate stop anyway and put it on the slur list
// - doSlurStart() starts slur but doesn't store it
void SlurHandler::doSlurStop(const Slur* s, Notations& notations, XmlWriter& xml)
{
// check if on slur list
int i = findSlur(s);
if (i < 0) {
// if not, find free slot to store it
i = findSlur(0);
if (i >= 0) {
slur[i] = s;
started[i] = false;
notations.tag(xml, s);
QString tagName = QString("slur type=\"stop\" number=\"%1\"").arg(i + 1);
tagName += ExportMusicXml::positioningAttributes(s, false);
xml.tagRaw(tagName);
} else {
LOGD("no free slur slot");
}
} else {
// found (already started), stop it and remove from list
slur[i] = 0;
started[i] = false;
notations.tag(xml, s);
QString tagName = QString("slur type=\"stop\" number=\"%1\"").arg(i + 1);
tagName += ExportMusicXml::positioningAttributes(s, false);
xml.tagRaw(tagName);
}
}
//---------------------------------------------------------
// glissando
//---------------------------------------------------------
// <notations>
// <slide line-type="solid" number="1" type="start"/>
// </notations>
// <notations>
// <glissando line-type="wavy" number="1" type="start"/>
// </notations>
static void glissando(const Glissando* gli, int number, bool start, Notations& notations, XmlWriter& xml)
{
GlissandoType st = gli->glissandoType();
QString tagName;
switch (st) {
case GlissandoType::STRAIGHT:
tagName = "slide line-type=\"solid\"";
break;
case GlissandoType::WAVY:
tagName = "glissando line-type=\"wavy\"";
break;
default:
LOGD("unknown glissando subtype %d", int(st));
return;
break;
}
tagName += QString(" number=\"%1\" type=\"%2\"").arg(number).arg(start ? "start" : "stop");
tagName += color2xml(gli);
tagName += ExportMusicXml::positioningAttributes(gli, start);
notations.tag(xml, gli);
if (start && gli->showText() && gli->text() != "") {
xml.tagRaw(tagName, gli->text());
} else {
xml.tagRaw(tagName);
}
}
//---------------------------------------------------------
// GlissandoHandler
//---------------------------------------------------------
GlissandoHandler::GlissandoHandler()
{
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
glissNote[i] = 0;
slideNote[i] = 0;
}
}
//---------------------------------------------------------
// findNote -- get index of Note in note table for subtype type
// return -1 if not found
//---------------------------------------------------------
int GlissandoHandler::findNote(const Note* note, int type) const
{
if (type != 0 && type != 1) {
LOGD("GlissandoHandler::findNote: unknown glissando subtype %d", type);
return -1;
}
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
if (type == 0 && slideNote[i] == note) {
return i;
}
if (type == 1 && glissNote[i] == note) {
return i;
}
}
return -1;
}
//---------------------------------------------------------
// doGlissandoStart
//---------------------------------------------------------
void GlissandoHandler::doGlissandoStart(Glissando* gliss, Notations& notations, XmlWriter& xml)
{
GlissandoType type = gliss->glissandoType();
if (type != GlissandoType::STRAIGHT && type != GlissandoType::WAVY) {
LOGD("doGlissandoStart: unknown glissando subtype %d", int(type));
return;
}
Note* note = static_cast<Note*>(gliss->startElement());
// check if on chord list
int i = findNote(note, int(type));
if (i >= 0) {
// print error and remove from list
LOGD("doGlissandoStart: note for glissando/slide %p already on list", gliss);
if (type == GlissandoType::STRAIGHT) {
slideNote[i] = 0;
}
if (type == GlissandoType::WAVY) {
glissNote[i] = 0;
}
}
// find free slot to store it
i = findNote(0, int(type));
if (i >= 0) {
if (type == GlissandoType::STRAIGHT) {
slideNote[i] = note;
}
if (type == GlissandoType::WAVY) {
glissNote[i] = note;
}
glissando(gliss, i + 1, true, notations, xml);
} else {
LOGD("doGlissandoStart: no free slot");
}
}
//---------------------------------------------------------
// doGlissandoStop
//---------------------------------------------------------
void GlissandoHandler::doGlissandoStop(Glissando* gliss, Notations& notations, XmlWriter& xml)
{
GlissandoType type = gliss->glissandoType();
if (type != GlissandoType::STRAIGHT && type != GlissandoType::WAVY) {
LOGD("doGlissandoStart: unknown glissando subtype %d", int(type));
return;
}
Note* note = static_cast<Note*>(gliss->startElement());
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
if (type == GlissandoType::STRAIGHT && slideNote[i] == note) {
slideNote[i] = 0;
glissando(gliss, i + 1, false, notations, xml);
return;
}
if (type == GlissandoType::WAVY && glissNote[i] == note) {
glissNote[i] = 0;
glissando(gliss, i + 1, false, notations, xml);
return;
}
}
LOGD("doGlissandoStop: glissando note %p not found", note);
}
//---------------------------------------------------------
// directions anchor -- anchor directions at another element or a specific tick
//---------------------------------------------------------
class DirectionsAnchor
{
EngravingItem* direct; // the element containing the direction
EngravingItem* anchor; // the element it is attached to
bool start; // whether it is attached to start or end
Fraction tick; // the timestamp
public:
DirectionsAnchor(EngravingItem* a, bool s, const Fraction& t) { direct = 0; anchor = a; start = s; tick = t; }
DirectionsAnchor(const Fraction& t) { direct = 0; anchor = 0; start = true; tick = t; }
EngravingItem* getDirect() { return direct; }
EngravingItem* getAnchor() { return anchor; }
bool getStart() { return start; }
Fraction getTick() { return tick; }
void setDirect(EngravingItem* d) { direct = d; }
};
//---------------------------------------------------------
// trill handling
//---------------------------------------------------------
// find all trills in this measure and this part
static void findTrills(const Measure* const measure, track_idx_t strack, track_idx_t etrack, TrillHash& trillStart, TrillHash& trillStop)
{
// loop over all spanners in this measure
auto stick = measure->tick();
auto etick = measure->tick() + measure->ticks();
for (auto it = measure->score()->spanner().lower_bound(stick.ticks());
it != measure->score()->spanner().upper_bound(etick.ticks()); ++it) {
auto e = it->second;
//LOGD("1 trill %p type %d track %d tick %s", e, e->type(), e->track(), qPrintable(e->tick().print()));
if (e->isTrill() && ExportMusicXml::canWrite(e) && strack <= e->track() && e->track() < etrack
&& e->tick() >= measure->tick() && e->tick() < (measure->tick() + measure->ticks())) {
//LOGD("2 trill %p", e);
// a trill is found starting in this segment, trill end time is known
// determine notes to write trill start and stop
const auto tr = toTrill(e);
auto elem1 = tr->startElement();
auto elem2 = tr->endElement();
if (elem1 && elem1->isChordRest() && elem2 && elem2->isChordRest()) {
trillStart.insert(toChordRest(elem1), tr);
trillStop.insert(toChordRest(elem2), tr);
}
}
}
}
//---------------------------------------------------------
// helpers for ::calcDivisions
//---------------------------------------------------------
typedef QList<int> IntVector;
static IntVector integers;
static IntVector primes;
// check if all integers can be divided by d
static bool canDivideBy(int d)
{
bool res = true;
for (int i = 0; i < integers.count(); i++) {
if ((integers[i] <= 1) || ((integers[i] % d) != 0)) {
res = false;
}
}
return res;
}
// divide all integers by d
static void divideBy(int d)
{
for (int i = 0; i < integers.count(); i++) {
integers[i] /= d;
}
}
static void addInteger(int len)
{
if (len > 0 && !integers.contains(len)) {
integers.append(len);
}
}
//---------------------------------------------------------
// calcDivMoveToTick
//---------------------------------------------------------
void ExportMusicXml::calcDivMoveToTick(const Fraction& t)
{
if (t < _tick) {
#ifdef DEBUG_TICK
LOGD("backup %d", (tick - t).ticks());
#endif
addInteger((_tick - t).ticks());
} else if (t > _tick) {
#ifdef DEBUG_TICK
LOGD("forward %d", (t - tick).ticks());
#endif
addInteger((t - _tick).ticks());
}
_tick = t;
}
//---------------------------------------------------------
// isTwoNoteTremolo - determine is chord is part of two note tremolo
//---------------------------------------------------------
static bool isTwoNoteTremolo(Chord* chord)
{
return chord->tremolo() && chord->tremolo()->twoNotes();
}
//---------------------------------------------------------
// calcDivisions
//---------------------------------------------------------
// Loop over all voices in all staves and determine a suitable value for divisions.
// Length of time in MusicXML is expressed in "units", which should allow expressing all time values
// as an integral number of units. Divisions contains the number of units in a quarter note.
// MuseScore uses division (480) midi ticks to represent a quarter note, which expresses all note values
// plus triplets and quintuplets as integer values. Solution is to collect all time values required,
// and divide them by the highest common denominator, which is implemented as a series of
// divisions by prime factors. Initialize the list with division to make sure a quarter note can always
// be written as an integral number of units.
/**
*/
void ExportMusicXml::calcDivisions()
{
// init
integers.clear();
primes.clear();
integers.append(Constants::division);
primes.append(2);
primes.append(3);
primes.append(5);
const std::vector<Part*>& il = _score->parts();
for (size_t idx = 0; idx < il.size(); ++idx) {
Part* part = il.at(idx);
_tick = { 0, 1 };
size_t staves = part->nstaves();
track_idx_t strack = _score->staffIdx(part) * VOICES;
track_idx_t etrack = strack + staves * VOICES;
for (MeasureBase* mb = _score->measures()->first(); mb; mb = mb->next()) {
if (mb->type() != ElementType::MEASURE) {
continue;
}
Measure* m = (Measure*)mb;
for (track_idx_t st = strack; st < etrack; ++st) {
for (Segment* seg = m->first(); seg; seg = seg->next()) {
for (const EngravingItem* e : seg->annotations()) {
if (e->track() == st && e->type() == ElementType::FIGURED_BASS) {
const FiguredBass* fb = toFiguredBass(e);
#ifdef DEBUG_TICK
LOGD("figuredbass tick %d duration %d", fb->tick().ticks(), fb->ticks().ticks());
#endif
addInteger(fb->ticks().ticks());
}
}
EngravingItem* el = seg->element(st);
if (!el) {
continue;
}
// must ignore start repeat to prevent spurious backup/forward
if (el->type() == ElementType::BAR_LINE && toBarLine(el)->barLineType() == BarLineType::START_REPEAT) {
continue;
}
if (_tick != seg->tick()) {
calcDivMoveToTick(seg->tick());
}
if (el->isChordRest()) {
Fraction l = toChordRest(el)->actualTicks();
if (el->isChord()) {
if (isTwoNoteTremolo(toChord(el))) {
l = l * Fraction(1, 2);
}
}
#ifdef DEBUG_TICK
LOGD("chordrest tick %d duration %d", _tick.ticks(), l.ticks());
#endif
addInteger(l.ticks());
_tick += l;
}
}
}
// move to end of measure (in case of incomplete last voice)
calcDivMoveToTick(m->endTick());
}
}
// do it: divide by all primes as often as possible
for (int u = 0; u < primes.count(); u++) {
while (canDivideBy(primes[u])) {
divideBy(primes[u]);
}
}
div = Constants::division / integers[0];
#ifdef DEBUG_TICK
LOGD("divisions=%d div=%d", integers[0], div);
#endif
}
//---------------------------------------------------------
// writePageFormat
//---------------------------------------------------------
static void writePageFormat(const Score* const s, XmlWriter& xml, double conversion)
{
xml.startElement("page-layout");
xml.tag("page-height", s->styleD(Sid::pageHeight) * conversion);
xml.tag("page-width", s->styleD(Sid::pageWidth) * conversion);
QString type("both");
if (s->styleB(Sid::pageTwosided)) {
type = "even";
xml.startElement("page-margins", { { "type", type } });
xml.tag("left-margin", s->styleD(Sid::pageEvenLeftMargin) * conversion);
xml.tag("right-margin", s->styleD(Sid::pageOddLeftMargin) * conversion);
xml.tag("top-margin", s->styleD(Sid::pageEvenTopMargin) * conversion);
xml.tag("bottom-margin", s->styleD(Sid::pageEvenBottomMargin) * conversion);
xml.endElement();
type = "odd";
}
xml.startElement("page-margins", { { "type", type } });
xml.tag("left-margin", s->styleD(Sid::pageOddLeftMargin) * conversion);
xml.tag("right-margin", s->styleD(Sid::pageEvenLeftMargin) * conversion);
xml.tag("top-margin", s->styleD(Sid::pageOddTopMargin) * conversion);
xml.tag("bottom-margin", s->styleD(Sid::pageOddBottomMargin) * conversion);
xml.endElement();
xml.endElement();
}
//---------------------------------------------------------
// defaults
//---------------------------------------------------------
// _spatium = DPMM * (millimeter * 10.0 / tenths);
static void defaults(XmlWriter& xml, const Score* const s, double& millimeters, const int& tenths)
{
xml.startElement("defaults");
{
xml.startElement("scaling");
xml.tag("millimeters", millimeters);
xml.tag("tenths", tenths);
xml.endElement();
}
writePageFormat(s, xml, INCH / millimeters * tenths);
// TODO: also write default system layout here
// when exporting only manual or no breaks, system-distance is not written at all
// font defaults
// as MuseScore supports dozens of different styles, while MusicXML only has defaults
// for music (TODO), words and lyrics, use Tid STAFF (typically used for words)
// and LYRIC1 to get MusicXML defaults
// TODO xml.tagE("music-font font-family=\"TBD\" font-size=\"TBD\"");
xml.tag("word-font", { { "font-family", s->styleSt(Sid::staffTextFontFace) }, { "font-size", s->styleD(Sid::staffTextFontSize) } });
xml.tag("lyric-font",
{ { "font-family", s->styleSt(Sid::lyricsOddFontFace) }, { "font-size", s->styleD(Sid::lyricsOddFontSize) } });
xml.endElement();
}
//---------------------------------------------------------
// formatForWords
//---------------------------------------------------------
static CharFormat formatForWords(const Score* const s)
{
CharFormat defFmt;
defFmt.setFontFamily(s->styleSt(Sid::staffTextFontFace));
defFmt.setFontSize(s->styleD(Sid::staffTextFontSize));
return defFmt;
}
//---------------------------------------------------------
// creditWords
//---------------------------------------------------------
static void creditWords(XmlWriter& xml, const Score* const s, const page_idx_t pageNr,
const double x, const double y, const QString& just, const QString& val,
const std::list<TextFragment>& words, const QString& creditType)
{
// prevent incorrect MusicXML for empty text
if (words.empty()) {
return;
}
const QString mtf = s->styleSt(Sid::MusicalTextFont);
const CharFormat defFmt = formatForWords(s);
// export formatted
xml.startElement("credit", { { "page", pageNr } });
if (creditType != "") {
xml.tag("credit-type", creditType);
}
QString attr = QString(" default-x=\"%1\"").arg(x);
attr += QString(" default-y=\"%1\"").arg(y);
attr += " justify=\"" + just + "\"";
attr += " valign=\"" + val + "\"";
MScoreTextToMXML mttm("credit-words", attr, defFmt, mtf);
mttm.writeTextFragments(words, xml);
xml.endElement();
}
//---------------------------------------------------------
// parentHeight
//---------------------------------------------------------
static double parentHeight(const EngravingItem* element)
{
const EngravingItem* parent = element->parentItem();
if (!parent) {
return 0;
}
if (parent->type() == ElementType::VBOX) {
return parent->height();
}
return 0;
}
//---------------------------------------------------------
// tidToCreditType
//---------------------------------------------------------
static QString tidToCreditType(const TextStyleType tid)
{
QString res;
switch (tid) {
case TextStyleType::COMPOSER:
res = "composer";
break;
case TextStyleType::POET:
res = "lyricist";
break;
case TextStyleType::SUBTITLE:
res = "subtitle";
break;
case TextStyleType::TITLE:
res = "title";
break;
default:
break;
}
return res;
}
//---------------------------------------------------------
// textAsCreditWords
//---------------------------------------------------------
// Refactor suggestion: make getTenthsFromInches static instead of ExportMusicXml member function
static void textAsCreditWords(const ExportMusicXml* const expMxml, XmlWriter& xml, const Score* const s, const int pageNr,
const Text* const text)
{
// determine page formatting
const double h = expMxml->getTenthsFromInches(s->styleD(Sid::pageHeight));
const double w = expMxml->getTenthsFromInches(s->styleD(Sid::pageWidth));
const double lm = expMxml->getTenthsFromInches(s->styleD(Sid::pageOddLeftMargin));
const double rm = expMxml->getTenthsFromInches(s->styleD(Sid::pageEvenLeftMargin));
const double ph = expMxml->getTenthsFromDots(parentHeight(text));
double tx = w / 2;
double ty = h - expMxml->getTenthsFromDots(text->pagePos().y());
Align al = text->align();
QString just;
QString val;
if (al == AlignH::RIGHT) {
just = "right";
tx = w - rm;
} else if (al == AlignH::HCENTER) {
just = "center";
// tx already set correctly
} else {
just = "left";
tx = lm;
}
if (al == AlignV::BOTTOM) {
val = "bottom";
ty -= ph;
} else if (al == AlignV::VCENTER) {
val = "middle";
ty -= ph / 2;
} else if (al == AlignV::BASELINE) {
val = "baseline";
ty -= ph / 2;
} else {
val = "top";
// ty already set correctly
}
const QString creditType= tidToCreditType(text->textStyleType());
creditWords(xml, s, pageNr, tx, ty, just, val, text->fragmentList(), creditType);
}
//---------------------------------------------------------
// credits
//---------------------------------------------------------
void ExportMusicXml::credits(XmlWriter& xml)
{
// find the vboxes in every page and write their elements as credit-words
for (const auto page : _score->pages()) {
const auto pageIdx = _score->pageIdx(page);
for (const auto system : page->systems()) {
for (const auto mb : system->measures()) {
if (mb->isVBox()) {
for (const EngravingItem* element : mb->el()) {
if (element->isText()) {
const Text* text = toText(element);
textAsCreditWords(this, xml, _score, static_cast<int>(pageIdx) + 1, text);
}
}
}
}
}
}
// put copyright at the bottom center of every page
// note: as the copyright metatag contains plain text, special XML characters must be escaped
// determine page formatting
const QString rights = _score->metaTag(u"copyright");
if (!rights.isEmpty()) {
const double bm = getTenthsFromInches(_score->styleD(Sid::pageOddBottomMargin));
const double w = getTenthsFromInches(_score->styleD(Sid::pageWidth));
/*
const double h = getTenthsFromInches(_score->styleD(Sid::pageHeight));
const double lm = getTenthsFromInches(_score->styleD(Sid::pageOddLeftMargin));
const double rm = getTenthsFromInches(_score->styleD(Sid::pageEvenLeftMargin));
const double tm = getTenthsFromInches(_score->styleD(Sid::pageOddTopMargin));
LOGD("page h=%g w=%g lm=%g rm=%g tm=%g bm=%g", h, w, lm, rm, tm, bm);
*/
TextFragment f(XmlWriter::xmlString(rights));
f.changeFormat(FormatId::FontFamily, _score->styleSt(Sid::footerFontFace).toQString());
f.changeFormat(FormatId::FontSize, _score->styleD(Sid::footerFontSize));
std::list<TextFragment> list;
list.push_back(f);
for (page_idx_t pageIdx = 0; pageIdx < _score->npages(); ++pageIdx) {
creditWords(xml, _score, pageIdx + 1, w / 2, bm, "center", "bottom", list, "rights");
}
}
}
//---------------------------------------------------------
// midipitch2xml
//---------------------------------------------------------
static int alterTab[12] = { 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0 };
static char noteTab[12] = { 'C', 'C', 'D', 'D', 'E', 'F', 'F', 'G', 'G', 'A', 'A', 'B' };
static void midipitch2xml(int pitch, char& c, int& alter, int& octave)
{
// 60 = C 4
c = noteTab[pitch % 12];
alter = alterTab[pitch % 12];
octave = pitch / 12 - 1;
//LOGD("midipitch2xml(pitch %d) step %c, alter %d, octave %d", pitch, c, alter, octave);
}
//---------------------------------------------------------
// tabpitch2xml
//---------------------------------------------------------
static void tabpitch2xml(const int pitch, const int tpc, QString& s, int& alter, int& octave)
{
s = tpc2stepName(tpc);
alter = tpc2alterByKey(tpc, Key::C);
octave = (pitch - alter) / 12 - 1;
if (alter < -2 || 2 < alter) {
LOGD("tabpitch2xml(pitch %d, tpc %d) problem: step %s, alter %d, octave %d",
pitch, tpc, qPrintable(s), alter, octave);
}
/*
else
LOGD("tabpitch2xml(pitch %d, tpc %d) step %s, alter %d, octave %d",
pitch, tpc, qPrintable(s), alter, octave);
*/
}
//---------------------------------------------------------
// pitch2xml
//---------------------------------------------------------
// TODO validation
static void pitch2xml(const Note* note, QString& s, int& alter, int& octave)
{
const auto st = note->staff();
const auto tick = note->tick();
const auto instr = st->part()->instrument(tick);
const auto intval = instr->transpose();
s = tpc2stepName(note->tpc());
alter = tpc2alterByKey(note->tpc(), Key::C);
// note that pitch must be converted to concert pitch
// in order to calculate the correct octave
octave = (note->pitch() - intval.chromatic - alter) / 12 - 1;
//
// HACK:
// On percussion clefs there is no relationship between
// note->pitch() and note->line()
// note->line() is determined by drumMap
//
ClefType ct = st->clef(tick);
if (ct == ClefType::PERC || ct == ClefType::PERC2) {
alter = 0;
octave = line2pitch(note->line(), ct, Key::C) / 12 - 1;
}
// correct for ottava lines
int ottava = 0;
switch (note->ppitch() - note->pitch()) {
case 24: ottava = 2;
break;
case 12: ottava = 1;
break;
case 0: ottava = 0;
break;
case -12: ottava = -1;
break;
case -24: ottava = -2;
break;
default: LOGD("pitch2xml() tick=%d pitch()=%d ppitch()=%d",
tick.ticks(), note->pitch(), note->ppitch());
}
octave += ottava;
//LOGD("pitch2xml(pitch %d, tpc %d, ottava %d clef %hhd) step %s, alter %d, octave %d",
// note->pitch(), note->tpc(), ottava, clef, qPrintable(s), alter, octave);
}
// unpitch2xml -- calculate display-step and display-octave for an unpitched note
// note:
// even though this produces the correct step/octave according to Recordare's tutorial
// Finale Notepad 2012 does not import a three line staff with percussion clef correctly
// Same goes for Sibelius 6 in case of three or five line staff with percussion clef
static void unpitch2xml(const Note* note, QString& s, int& octave)
{
static char table1[] = "FEDCBAG";
Fraction tick = note->chord()->tick();
Staff* st = note->staff();
ClefType ct = st->clef(tick);
// offset in lines between staff with current clef and with G clef
int clefOffset = ClefInfo::pitchOffset(ct) - ClefInfo::pitchOffset(ClefType::G);
// line note would be on on a five line staff with G clef
// note top line is line 0, bottom line is line 8
int line5g = note->line() - clefOffset;
// in MusicXML with percussion clef, step and octave are determined as if G clef is used
// when stafflines is not equal to five, in MusicXML the bottom line is still E4.
// in MuseScore assumes line 0 is F5
// MS line numbers (top to bottom) plus correction to get lowest line at E4 (line 8)
// 1 line staff: 0 -> correction 8
// 3 line staff: 2, 4, 6 -> correction 2
// 5 line staff: 0, 2, 4, 6, 8 -> correction 0
// TODO handle other # staff lines ?
if (st->lines(Fraction(0, 1)) == 1) {
line5g += 8;
}
if (st->lines(Fraction(0, 1)) == 3) {
line5g += 2;
}
// index in table1 to get step
int stepIdx = (line5g + 700) % 7;
// get step
s = table1[stepIdx];
// calculate octave, offset "3" correcting for the fact that an octave starts
// with C instead of F
octave =(3 - line5g + 700) / 7 + 5 - 100;
// LOGD("ExportMusicXml::unpitch2xml(%p) clef %d clef.po %d clefOffset %d staff.lines %d note.line %d line5g %d step %c oct %d",
// note, ct, clefTable[ct].pitchOffset, clefOffset, st->lines(), note->line(), line5g, step, octave);
}
//---------------------------------------------------------
// tick2xml
// set type + dots depending on tick len
//---------------------------------------------------------
static QString tick2xml(const Fraction& ticks, int* dots)
{
TDuration t(ticks);
*dots = t.dots();
if (ticks == Fraction(0, 1)) {
t.setType(DurationType::V_MEASURE);
*dots = 0;
}
return TConv::toXml(t.type()).ascii();
}
//---------------------------------------------------------
// findVolta -- find volta starting in measure m
//---------------------------------------------------------
static Volta* findVolta(const Measure* const m, bool left)
{
Fraction stick = m->tick();
Fraction etick = m->tick() + m->ticks();
auto spanners = m->score()->spannerMap().findOverlapping(stick.ticks(), etick.ticks());
for (auto i : spanners) {
Spanner* el = i.value;
if (el->type() != ElementType::VOLTA) {
continue;
}
if (left && el->tick() == stick) {
return (Volta*)el;
}
if (!left && el->tick2() == etick) {
return (Volta*)el;
}
}
return 0;
}
//---------------------------------------------------------
// ending
//---------------------------------------------------------
static void ending(XmlWriter& xml, Volta* v, bool left)
{
QString number = "";
QString type = "";
for (int i : v->endings()) {
if (!number.isEmpty()) {
number += ", ";
}
number += QString("%1").arg(i);
}
if (left) {
type = "start";
} else {
Volta::Type st = v->voltaType();
switch (st) {
case Volta::Type::OPEN:
type = "discontinue";
break;
case Volta::Type::CLOSED:
type = "stop";
break;
default:
LOGD("unknown volta subtype %d", int(st));
return;
}
}
QString voltaXml = QString("ending number=\"%1\" type=\"%2\"").arg(number, type);
voltaXml += ExportMusicXml::positioningAttributes(v, left);
if (left) {
xml.tagRaw(voltaXml, v->text().toXmlEscaped());
} else {
xml.tagRaw(voltaXml);
}
}
//---------------------------------------------------------
// barlineLeft -- search for and handle barline left
//---------------------------------------------------------
void ExportMusicXml::barlineLeft(const Measure* const m)
{
bool rs = m->repeatStart();
Volta* volta = findVolta(m, true);
if (!rs && !volta) {
return;
}
_attr.doAttr(_xml, false);
_xml.startElement("barline", { { "location", "left" } });
if (rs) {
_xml.tag("bar-style", QString("heavy-light"));
}
if (volta) {
ending(_xml, volta, true);
}
if (rs) {
_xml.tag("repeat", { { "direction", "forward" } });
}
_xml.endElement();
}
//---------------------------------------------------------
// shortBarlineStyle -- recognize normal but shorter barline styles
//---------------------------------------------------------
static QString shortBarlineStyle(const BarLine* bl)
{
if (bl->barLineType() == BarLineType::NORMAL && !bl->spanStaff()) {
if (bl->spanTo() < 0) {
// lowest point of barline above lowest staff line
if (bl->spanFrom() < 0) {
return "tick"; // highest point of barline above highest staff line
} else {
return "short"; // highest point of barline below highest staff line
}
}
}
return "";
}
//---------------------------------------------------------
// normalBarlineStyle -- recognize other barline styles
//---------------------------------------------------------
static QString normalBarlineStyle(const BarLine* bl)
{
const auto bst = bl->barLineType();
switch (bst) {
case BarLineType::NORMAL:
return "regular";
case BarLineType::DOUBLE:
return "light-light";
case BarLineType::END_REPEAT:
case BarLineType::REVERSE_END:
return "light-heavy";
case BarLineType::BROKEN:
return "dashed";
case BarLineType::DOTTED:
return "dotted";
case BarLineType::END:
case BarLineType::END_START_REPEAT:
return "light-heavy";
case BarLineType::HEAVY:
return "heavy";
case BarLineType::DOUBLE_HEAVY:
return "heavy-heavy";
default:
LOGD("bar subtype %d not supported", int(bst));
}
return "";
}
//---------------------------------------------------------
// barlineMiddle -- handle barline middle
//---------------------------------------------------------
void ExportMusicXml::barlineMiddle(const BarLine* bl)
{
auto vis = bl->visible();
auto shortStyle = shortBarlineStyle(bl);
auto normalStyle = normalBarlineStyle(bl);
QString barStyle;
if (!vis) {
barStyle = "none";
} else if (shortStyle != "") {
barStyle = shortStyle;
} else {
barStyle = normalStyle;
}
if (barStyle != "") {
_xml.startElement("barline", { { "location", "middle" } });
_xml.tag("bar-style", barStyle);
_xml.endElement();
}
}
//---------------------------------------------------------
// fermataPosition - return fermata y position as MusicXML string
//---------------------------------------------------------
QString ExportMusicXml::fermataPosition(const Fermata* const fermata)
{
QString res;
if (configuration()->musicxmlExportLayout()) {
constexpr qreal SPATIUM2TENTHS = 10;
constexpr qreal EPSILON = 0.01;
const auto spatium = fermata->spatium();
const auto defY = -1 * SPATIUM2TENTHS * fermata->ipos().y() / spatium;
const auto relY = -1 * SPATIUM2TENTHS * fermata->offset().y() / spatium;
if (qAbs(defY) >= EPSILON) {
res += QString(" default-y=\"%1\"").arg(QString::number(defY, 'f', 2));
}
if (qAbs(relY) >= EPSILON) {
res += QString(" relative-y=\"%1\"").arg(QString::number(relY, 'f', 2));
}
}
return res;
}
//---------------------------------------------------------
// fermata - write a fermata
//---------------------------------------------------------
static void fermata(const Fermata* const a, XmlWriter& xml)
{
QString tagName = "fermata";
tagName += QString(" type=\"%1\"").arg(a->placement() == PlacementV::ABOVE ? "upright" : "inverted");
tagName += ExportMusicXml::fermataPosition(a);
tagName += color2xml(a);
SymId id = a->symId();
if (id == SymId::fermataAbove || id == SymId::fermataBelow) {
xml.tagRaw(tagName);
} else if (id == SymId::fermataShortAbove || id == SymId::fermataShortBelow) {
xml.tagRaw(tagName, "angled");
} else if (id == SymId::fermataLongAbove || id == SymId::fermataLongBelow) {
xml.tagRaw(tagName, "square");
} else if (id == SymId::fermataVeryShortAbove || id == SymId::fermataVeryShortBelow) {
xml.tagRaw(tagName, "double-angled");
} else if (id == SymId::fermataVeryLongAbove || id == SymId::fermataVeryLongBelow) {
xml.tagRaw(tagName, "double-square");
} else if (id == SymId::fermataLongHenzeAbove || id == SymId::fermataLongHenzeBelow) {
xml.tagRaw(tagName, "double-dot");
} else if (id == SymId::fermataShortHenzeAbove || id == SymId::fermataShortHenzeBelow) {
xml.tagRaw(tagName, "half-curve");
} else {
LOGD("unknown fermata sim id %d", static_cast<int>(id));
}
}
//---------------------------------------------------------
// barlineHasFermata -- search for fermata on barline
//---------------------------------------------------------
static bool barlineHasFermata(const BarLine* const barline, const track_idx_t strack, const track_idx_t etrack) // TODO: track
{
const Segment* seg = barline ? barline->segment() : 0;
if (seg) {
for (const auto anno : seg->annotations()) {
if (anno->isFermata() && strack <= anno->track() && anno->track() < etrack) {
return true;
}
}
}
return false;
}
//---------------------------------------------------------
// writeBarlineFermata -- write fermata on barline
//---------------------------------------------------------
static void writeBarlineFermata(const BarLine* const barline, XmlWriter& xml, const track_idx_t strack, const track_idx_t etrack)
{
const Segment* seg = barline ? barline->segment() : 0;
if (seg) {
for (const auto anno : seg->annotations()) {
if (anno->isFermata() && strack <= anno->track() && anno->track() < etrack) {
fermata(toFermata(anno), xml);
}
}
}
}
//---------------------------------------------------------
// barlineRight -- search for and handle barline right
//---------------------------------------------------------
void ExportMusicXml::barlineRight(const Measure* const m, const track_idx_t strack, const track_idx_t etrack)
{
const Measure* mmR1 = m->mmRest1(); // the multi measure rest this measure is covered by
const Measure* mmRLst = mmR1->isMMRest() ? mmR1->mmRestLast() : 0; // last measure of replaced sequence of empty measures
// note: use barlinetype as found in multi measure rest for last measure of replaced sequence
BarLineType bst = m == mmRLst ? mmR1->endBarLineType() : m->endBarLineType();
bool visible = m->endBarLineVisible();
bool needBarStyle = (bst != BarLineType::NORMAL && bst != BarLineType::START_REPEAT) || !visible;
Volta* volta = findVolta(m, false);
// detect short and tick barlines
QString special = "";
if (bst == BarLineType::NORMAL) {
const BarLine* bl = m->endBarLine();
if (bl && !bl->spanStaff()) {
if (bl->spanFrom() == BARLINE_SPAN_TICK1_FROM && bl->spanTo() == BARLINE_SPAN_TICK1_TO) {
special = "tick";
}
if (bl->spanFrom() == BARLINE_SPAN_TICK2_FROM && bl->spanTo() == BARLINE_SPAN_TICK2_TO) {
special = "tick";
}
if (bl->spanFrom() == BARLINE_SPAN_SHORT1_FROM && bl->spanTo() == BARLINE_SPAN_SHORT1_TO) {
special = "short";
}
if (bl->spanFrom() == BARLINE_SPAN_SHORT2_FROM && bl->spanTo() == BARLINE_SPAN_SHORT2_FROM) {
special = "short";
}
}
}
// check fermata
// no need to take mmrest into account, MS does not create mmrests for measure with fermatas
const auto hasFermata = barlineHasFermata(m->endBarLine(), strack, etrack);
if (!needBarStyle && !volta && special.isEmpty() && !hasFermata) {
return;
}
_xml.startElement("barline", { { "location", "right" } });
if (needBarStyle) {
if (!visible) {
_xml.tag("bar-style", QString("none"));
} else {
switch (bst) {
case BarLineType::DOUBLE:
_xml.tag("bar-style", QString("light-light"));
break;
case BarLineType::END_REPEAT:
case BarLineType::REVERSE_END:
_xml.tag("bar-style", QString("light-heavy"));
break;
case BarLineType::BROKEN:
_xml.tag("bar-style", QString("dashed"));
break;
case BarLineType::DOTTED:
_xml.tag("bar-style", QString("dotted"));
break;
case BarLineType::END:
case BarLineType::END_START_REPEAT:
_xml.tag("bar-style", QString("light-heavy"));
break;
case BarLineType::HEAVY:
_xml.tag("bar-style", QString("heavy"));
break;
case BarLineType::DOUBLE_HEAVY:
_xml.tag("bar-style", QString("heavy-heavy"));
break;
default:
LOGD("ExportMusicXml::bar(): bar subtype %d not supported", int(bst));
break;
}
}
} else if (!special.isEmpty()) {
_xml.tag("bar-style", special);
}
writeBarlineFermata(m->endBarLine(), _xml, strack, etrack);
if (volta) {
ending(_xml, volta, false);
}
if (bst == BarLineType::END_REPEAT || bst == BarLineType::END_START_REPEAT) {
if (m->repeatCount() > 2) {
_xml.tag("repeat", { { "direction", "backward" }, { "times", m->repeatCount() } });
} else {
_xml.tag("repeat", { { "direction", "backward" } });
}
}
_xml.endElement();
}
//---------------------------------------------------------
// calculateTimeDeltaInDivisions
//---------------------------------------------------------
static int calculateTimeDeltaInDivisions(const Fraction& t1, const Fraction& t2, const int divisions)
{
return (t1 - t2).ticks() / divisions;
}
//---------------------------------------------------------
// moveToTick
//---------------------------------------------------------
void ExportMusicXml::moveToTick(const Fraction& t)
{
//LOGD("ExportMusicXml::moveToTick(t=%s) _tick=%s", qPrintable(t.print()), qPrintable(_tick.print()));
if (t < _tick) {
#ifdef DEBUG_TICK
LOGD(" -> backup");
#endif
_attr.doAttr(_xml, false);
_xml.startElement("backup");
_xml.tag("duration", calculateTimeDeltaInDivisions(_tick, t, div));
_xml.endElement();
} else if (t > _tick) {
#ifdef DEBUG_TICK
LOGD(" -> forward");
#endif
_attr.doAttr(_xml, false);
_xml.startElement("forward");
_xml.tag("duration", calculateTimeDeltaInDivisions(t, _tick, div));
_xml.endElement();
}
_tick = t;
}
//---------------------------------------------------------
// timesig
//---------------------------------------------------------
void ExportMusicXml::timesig(TimeSig* tsig)
{
TimeSigType st = tsig->timeSigType();
Fraction ts = tsig->sig();
int z = ts.numerator();
int n = ts.denominator();
QString ns = tsig->numeratorString();
_attr.doAttr(_xml, true);
XmlWriter::Attributes attrs;
if (st == TimeSigType::FOUR_FOUR) {
attrs = { { "symbol", "common" } };
} else if (st == TimeSigType::ALLA_BREVE) {
attrs = { { "symbol", "cut" } };
} else if (st == TimeSigType::CUT_BACH) {
attrs = { { "symbol", "cut2" } };
} else if (st == TimeSigType::CUT_TRIPLE) {
attrs = { { "symbol", "cut3" } };
}
if (!tsig->visible()) {
attrs.push_back({ "print-object", "no" });
}
addColorAttr(tsig, attrs);
_xml.startElement("time", attrs);
QRegularExpression regex(QRegularExpression::anchoredPattern("^\\d+(\\+\\d+)+$")); // matches a compound numerator
if (regex.match(ns).hasMatch()) {
// if compound numerator, exported as is
_xml.tag("beats", ns);
} else {
// else fall back and use the numerator as integer
_xml.tag("beats", z);
}
_xml.tag("beat-type", n);
_xml.endElement();
}
//---------------------------------------------------------
// accSymId2alter
//---------------------------------------------------------
static double accSymId2alter(SymId id)
{
double res = 0;
switch (id) {
case SymId::accidentalDoubleFlat: res = -2;
break;
case SymId::accidentalThreeQuarterTonesFlatZimmermann: res = -1.5;
break;
case SymId::accidentalFlat: res = -1;
break;
case SymId::accidentalQuarterToneFlatStein: res = -0.5;
break;
case SymId::accidentalNatural: res = 0;
break;
case SymId::accidentalQuarterToneSharpStein: res = 0.5;
break;
case SymId::accidentalSharp: res = 1;
break;
case SymId::accidentalThreeQuarterTonesSharpStein: res = 1.5;
break;
case SymId::accidentalDoubleSharp: res = 2;
break;
default: LOGD("accSymId2alter: unsupported sym %s", SymNames::nameForSymId(id).ascii());
}
return res;
}
//---------------------------------------------------------
// keysig
//---------------------------------------------------------
void ExportMusicXml::keysig(const KeySig* ks, ClefType ct, staff_idx_t staff, bool visible)
{
static char table2[] = "CDEFGAB";
int po = ClefInfo::pitchOffset(ct); // actually 7 * oct + step for topmost staff line
//LOGD("keysig st %d key %d custom %d ct %hhd st %d", staff, ks->key(), ks->isCustom(), ct, staff);
//LOGD(" pitch offset clef %d stp %d oct %d ", po, po % 7, po / 7);
XmlWriter::Attributes attrs;
if (staff) {
attrs.push_back({ "number", staff });
}
if (!visible) {
attrs.push_back({ "print-object", "no" });
}
addColorAttr(ks, attrs);
_attr.doAttr(_xml, true);
_xml.startElement("key", attrs);
const KeySigEvent kse = ks->keySigEvent();
const std::vector<KeySym>& keysyms = kse.keySymbols();
if (kse.custom() && !kse.isAtonal() && keysyms.size() > 0) {
// non-traditional key signature
// MusicXML order is left-to-right order, while KeySims in keySymbols()
// are in insertion order -> sorting required
// first put the KeySyms in a map
QMap<qreal, KeySym> map;
for (const KeySym& ksym : keysyms) {
map.insert(ksym.xPos, ksym);
}
// then write them (automatically sorted on key)
for (const KeySym& ksym : map) {
int step = (po - ksym.line) % 7;
//LOGD(" keysym sym %d -> line %d step %d", ksym.sym, ksym.line, step);
_xml.tag("key-step", QString(QChar(table2[step])));
_xml.tag("key-alter", accSymId2alter(ksym.sym));
_xml.tag("key-accidental", accSymId2MxmlString(ksym.sym));
}
} else {
// traditional key signature
_xml.tag("fifths", static_cast<int>(kse.key()));
switch (kse.mode()) {
case KeyMode::NONE: _xml.tag("mode", "none");
break;
case KeyMode::MAJOR: _xml.tag("mode", "major");
break;
case KeyMode::MINOR: _xml.tag("mode", "minor");
break;
case KeyMode::DORIAN: _xml.tag("mode", "dorian");
break;
case KeyMode::PHRYGIAN: _xml.tag("mode", "phrygian");
break;
case KeyMode::LYDIAN: _xml.tag("mode", "lydian");
break;
case KeyMode::MIXOLYDIAN: _xml.tag("mode", "mixolydian");
break;
case KeyMode::AEOLIAN: _xml.tag("mode", "aeolian");
break;
case KeyMode::IONIAN: _xml.tag("mode", "ionian");
break;
case KeyMode::LOCRIAN: _xml.tag("mode", "locrian");
break;
case KeyMode::UNKNOWN: // fall thru
default:
if (kse.custom()) {
_xml.tag("mode", "none");
}
}
}
_xml.endElement();
}
//---------------------------------------------------------
// clef
//---------------------------------------------------------
struct MusicXmlClefInfo
{
ClefType type;
const char* sign;
int octChng;
};
static const std::vector<MusicXmlClefInfo> CLEF_INFOS = {
{ ClefType::G, "G", 0 },
{ ClefType::G15_MB, "G", -2 },
{ ClefType::G8_VB, "G", -1 },
{ ClefType::G8_VA, "G", 1 },
{ ClefType::G15_MA, "G", 2 },
{ ClefType::G8_VB_O, "G", -1 },
{ ClefType::G8_VB_P, "G", 0 },
{ ClefType::G_1, "G", 0 },
{ ClefType::C1, "C", 0 },
{ ClefType::C2, "C", 0 },
{ ClefType::C3, "C", 0 },
{ ClefType::C4, "C", 0 },
{ ClefType::C5, "C", 0 },
{ ClefType::C_19C, "G", 0 },
{ ClefType::C1_F18C, "C", 0 },
{ ClefType::C3_F18C, "C", 0 },
{ ClefType::C4_F18C, "C", 0 },
{ ClefType::C1_F20C, "C", 0 },
{ ClefType::C3_F20C, "C", 0 },
{ ClefType::C4_F20C, "C", 0 },
{ ClefType::F, "F", 0 },
{ ClefType::F15_MB, "F", -2 },
{ ClefType::F8_VB, "F", -1 },
{ ClefType::F_8VA, "F", 1 },
{ ClefType::F_15MA, "F", 2 },
{ ClefType::F_B, "F", 0 },
{ ClefType::F_C, "F", 0 },
{ ClefType::F_F18C, "F", 0 },
{ ClefType::F_19C, "F", 0 },
{ ClefType::PERC, "percussion", 0 },
{ ClefType::PERC2, "percussion", 0 },
{ ClefType::TAB, "TAB", 0 },
{ ClefType::TAB4, "TAB", 0 },
{ ClefType::TAB_SERIF, "TAB", 0 },
{ ClefType::TAB4_SERIF, "TAB", 0 },
};
static const MusicXmlClefInfo findClefInfoByType(const ClefType& v)
{
auto it = std::find_if(CLEF_INFOS.cbegin(), CLEF_INFOS.cend(), [v](const MusicXmlClefInfo& i) {
return i.type == v;
});
IF_ASSERT_FAILED(it != CLEF_INFOS.cend()) {
static MusicXmlClefInfo dummy_;
return dummy_;
}
return *it;
}
void ExportMusicXml::clef(staff_idx_t staff, const ClefType ct, const QString& extraAttributes)
{
clefDebug("ExportMusicXml::clef(staff %zu, clef %hhd)", staff, ct);
QString tagName = "clef";
if (staff) {
tagName += QString(" number=\"%1\"").arg(static_cast<int>(staff));
}
tagName += extraAttributes;
_attr.doAttr(_xml, true);
_xml.startElementRaw(tagName);
MusicXmlClefInfo info = findClefInfoByType(ct);
int line = ClefInfo::line(ct);
_xml.tag("sign", info.sign);
_xml.tag("line", line);
if (info.octChng) {
_xml.tag("clef-octave-change", info.octChng);
}
_xml.endElement();
}
//---------------------------------------------------------
// tupletNesting
//---------------------------------------------------------
/*
* determine the tuplet nesting level for cr
* 0 = not part of tuplet
* 1 = part of a single tuplet
* 2 = part of two nested tuplets
* etc.
*/
static int tupletNesting(const ChordRest* const cr)
{
const DurationElement* el { cr->tuplet() };
int nesting { 0 };
while (el) {
nesting++;
el = el->tuplet();
}
return nesting;
}
//---------------------------------------------------------
// isSimpleTuplet
//---------------------------------------------------------
/*
* determine if t is simple, i.e. all its children are chords or rests
*/
static bool isSimpleTuplet(const Tuplet* const t)
{
if (t->tuplet()) {
return false;
}
for (const auto el : t->elements()) {
if (!el->isChordRest()) {
return false;
}
}
return true;
}
//---------------------------------------------------------
// isTupletStart
//---------------------------------------------------------
/*
* determine if t is the starting element of a tuplet
*/
static bool isTupletStart(const DurationElement* const el)
{
const auto t = el->tuplet();
if (!t) {
return false;
}
return el == t->elements().front();
}
//---------------------------------------------------------
// isTupletStop
//---------------------------------------------------------
/*
* determine if t is the stopping element of a tuplet
*/
static bool isTupletStop(const DurationElement* const el)
{
const auto t = el->tuplet();
if (!t) {
return false;
}
return el == t->elements().back();
}
//---------------------------------------------------------
// startTupletAtLevel
//---------------------------------------------------------
/*
* return the tuplet starting at tuplet nesting level, if any
*/
static const Tuplet* startTupletAtLevel(const DurationElement* const cr, const int level)
{
const DurationElement* el { cr };
if (!el->tuplet()) {
return nullptr;
}
for (int i = 0; i < level; ++i) {
if (!isTupletStart(el)) {
return nullptr;
}
el = el->tuplet();
}
return toTuplet(el);
}
//---------------------------------------------------------
// stopTupletAtLevel
//---------------------------------------------------------
/*
* return the tuplet stopping at tuplet nesting level, if any
*/
static const Tuplet* stopTupletAtLevel(const DurationElement* const cr, const int level)
{
const DurationElement* el { cr };
if (!el->tuplet()) {
return nullptr;
}
for (int i = 0; i < level; ++i) {
if (!isTupletStop(el)) {
return nullptr;
}
el = el->tuplet();
}
return toTuplet(el);
}
//---------------------------------------------------------
// tupletTypeAndDots
//---------------------------------------------------------
static void tupletTypeAndDots(const QString& type, const int dots, XmlWriter& xml)
{
xml.tag("tuplet-type", type);
for (int i = 0; i < dots; ++i) {
xml.tag("tuplet-dot");
}
}
//---------------------------------------------------------
// tupletActualAndNormal
//---------------------------------------------------------
static void tupletActualAndNormal(const Tuplet* const t, XmlWriter& xml)
{
xml.startElement("tuplet-actual");
xml.tag("tuplet-number", t->ratio().numerator());
int dots { 0 };
const auto s = tick2xml(t->baseLen().ticks(), &dots);
tupletTypeAndDots(s, dots, xml);
xml.endElement();
xml.startElement("tuplet-normal");
xml.tag("tuplet-number", t->ratio().denominator());
tupletTypeAndDots(s, dots, xml);
xml.endElement();
}
//---------------------------------------------------------
// tupletStart
//---------------------------------------------------------
// LVIFIX: add placement to tuplet support
// <notations>
// <tuplet type="start" placement="above" bracket="no"/>
// </notations>
static void tupletStart(const Tuplet* const t, const int number, const bool needActualAndNormal, Notations& notations, XmlWriter& xml)
{
notations.tag(xml, t);
QString tupletTag = "tuplet type=\"start\"";
if (!isSimpleTuplet(t)) {
tupletTag += QString(" number=\"%1\"").arg(number);
}
tupletTag += " bracket=";
tupletTag += t->hasBracket() ? "\"yes\"" : "\"no\"";
if (t->numberType() == TupletNumberType::SHOW_RELATION) {
tupletTag += " show-number=\"both\"";
}
if (t->numberType() == TupletNumberType::NO_TEXT) {
tupletTag += " show-number=\"none\"";
}
if (needActualAndNormal) {
xml.startElementRaw(tupletTag);
tupletActualAndNormal(t, xml);
xml.endElement();
} else {
xml.tagRaw(tupletTag);
}
}
//---------------------------------------------------------
// tupletStop
//---------------------------------------------------------
static void tupletStop(const Tuplet* const t, const int number, Notations& notations, XmlWriter& xml)
{
notations.tag(xml, t);
XmlWriter::Attributes tupletAttrs = { { "type", "stop" } };
if (!isSimpleTuplet(t)) {
tupletAttrs.push_back({ "number", number });
}
xml.tag("tuplet", tupletAttrs);
}
//---------------------------------------------------------
// tupletStartStop
//---------------------------------------------------------
static void tupletStartStop(ChordRest* cr, Notations& notations, XmlWriter& xml)
{
const auto nesting = tupletNesting(cr);
bool doActualAndNormal = (nesting > 1);
if (cr->isChord() && isTwoNoteTremolo(toChord(cr))) {
doActualAndNormal = true;
}
for (int level = nesting - 1; level >= 0; --level) {
const auto startTuplet = startTupletAtLevel(cr, level + 1);
if (startTuplet) {
tupletStart(startTuplet, nesting - level, doActualAndNormal, notations, xml);
}
const auto stopTuplet = stopTupletAtLevel(cr, level + 1);
if (stopTuplet) {
tupletStop(stopTuplet, nesting - level, notations, xml);
}
}
}
//---------------------------------------------------------
// findTrill -- get index of trill in trill table
// return -1 if not found
//---------------------------------------------------------
int ExportMusicXml::findTrill(const Trill* tr) const
{
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
if (trills[i] == tr) {
return i;
}
}
return -1;
}
//---------------------------------------------------------
// writeAccidental
//---------------------------------------------------------
static void writeAccidental(XmlWriter& xml, const QString& tagName, const Accidental* const acc)
{
if (acc) {
QString s = accidentalType2MxmlString(acc->accidentalType());
if (s != "") {
QString tag = tagName;
if (acc->bracket() != AccidentalBracket::NONE) {
tag += " parentheses=\"yes\"";
}
xml.tagRaw(tag, s);
}
}
}
//---------------------------------------------------------
// wavyLineStart
//---------------------------------------------------------
static void wavyLineStart(const Trill* tr, const int number, Notations& notations, Ornaments& ornaments, XmlWriter& xml)
{
// mscore only supports wavy-line with trill-mark
notations.tag(xml, tr);
ornaments.tag(xml);
xml.tag("trill-mark");
writeAccidental(xml, "accidental-mark", tr->accidental());
QString tagName = "wavy-line type=\"start\"";
tagName += QString(" number=\"%1\"").arg(number + 1);
tagName += color2xml(tr);
tagName += ExportMusicXml::positioningAttributes(tr, true);
xml.tagRaw(tagName);
}
//---------------------------------------------------------
// wavyLineStop
//---------------------------------------------------------
static void wavyLineStop(const Trill* tr, const int number, Notations& notations, Ornaments& ornaments, XmlWriter& xml)
{
notations.tag(xml, tr);
ornaments.tag(xml);
QString trillXml = QString("wavy-line type=\"stop\" number=\"%1\"").arg(number + 1);
trillXml += ExportMusicXml::positioningAttributes(tr, false);
xml.tagRaw(trillXml);
}
//---------------------------------------------------------
// wavyLineStartStop
//---------------------------------------------------------
void ExportMusicXml::wavyLineStartStop(const ChordRest* const cr, Notations& notations, Ornaments& ornaments,
TrillHash& trillStart, TrillHash& trillStop)
{
if (trillStart.contains(cr) && trillStop.contains(cr)) {
const auto tr = trillStart.value(cr);
auto n = findTrill(0);
if (n >= 0) {
wavyLineStart(tr, n, notations, ornaments, _xml);
wavyLineStop(tr, n, notations, ornaments, _xml);
} else {
LOGD("too many overlapping trills (cr %p staff %zu tick %d)",
cr, cr->staffIdx(), cr->tick().ticks());
}
} else {
if (trillStop.contains(cr)) {
const auto tr = trillStop.value(cr);
auto n = findTrill(tr);
if (n >= 0) {
// trill stop after trill start
trills[n] = 0;
} else {
// trill stop before trill start
n = findTrill(0);
if (n >= 0) {
trills[n] = tr;
} else {
LOGD("too many overlapping trills (cr %p staff %zu tick %d)",
cr, cr->staffIdx(), cr->tick().ticks());
}
}
if (n >= 0) {
wavyLineStop(tr, n, notations, ornaments, _xml);
}
trillStop.remove(cr);
}
if (trillStart.contains(cr)) {
const auto tr = trillStart.value(cr);
auto n = findTrill(tr);
if (n >= 0) {
LOGD("wavyLineStartStop error");
} else {
n = findTrill(0);
if (n >= 0) {
trills[n] = tr;
wavyLineStart(tr, n, notations, ornaments, _xml);
} else {
LOGD("too many overlapping trills (cr %p staff %zu tick %d)",
cr, cr->staffIdx(), cr->tick().ticks());
}
trillStart.remove(cr);
}
}
}
}
//---------------------------------------------------------
// tremoloSingleStartStop
//---------------------------------------------------------
static void tremoloSingleStartStop(Chord* chord, Notations& notations, Ornaments& ornaments, XmlWriter& xml)
{
Tremolo* tr = chord->tremolo();
if (tr && ExportMusicXml::canWrite(tr)) {
int count = 0;
TremoloType st = tr->tremoloType();
QString type = "";
if (chord->tremoloChordType() == TremoloChordType::TremoloSingle) {
type = "single";
switch (st) {
case TremoloType::R8: count = 1;
break;
case TremoloType::R16: count = 2;
break;
case TremoloType::R32: count = 3;
break;
case TremoloType::R64: count = 4;
break;
default: LOGD("unknown tremolo single %d", int(st));
break;
}
} else if (chord->tremoloChordType() == TremoloChordType::TremoloFirstNote) {
type = "start";
switch (st) {
case TremoloType::C8: count = 1;
break;
case TremoloType::C16: count = 2;
break;
case TremoloType::C32: count = 3;
break;
case TremoloType::C64: count = 4;
break;
default: LOGD("unknown tremolo double %d", int(st));
break;
}
} else if (chord->tremoloChordType() == TremoloChordType::TremoloSecondNote) {
type = "stop";
switch (st) {
case TremoloType::C8: count = 1;
break;
case TremoloType::C16: count = 2;
break;
case TremoloType::C32: count = 3;
break;
case TremoloType::C64: count = 4;
break;
default: LOGD("unknown tremolo double %d", int(st));
break;
}
} else {
LOGD("unknown tremolo subtype %d", int(st));
}
if (type != "" && count > 0) {
notations.tag(xml, tr);
ornaments.tag(xml);
XmlWriter::Attributes attrs = { { "type", type } };
if (type == "single" || type == "start") {
addColorAttr(tr, attrs);
}
xml.tag("tremolo", attrs, count);
}
}
}
//---------------------------------------------------------
// fermatas
//---------------------------------------------------------
static void fermatas(const QVector<EngravingItem*>& cra, XmlWriter& xml, Notations& notations)
{
for (const EngravingItem* e : cra) {
if (!e->isFermata() || !ExportMusicXml::canWrite(e)) {
continue;
}
notations.tag(xml, e);
fermata(toFermata(e), xml);
}
}
//---------------------------------------------------------
// symIdToArtic
//---------------------------------------------------------
static QString symIdToArtic(const SymId sid)
{
switch (sid) {
case SymId::articAccentAbove:
case SymId::articAccentBelow:
return "accent";
break;
case SymId::articStaccatoAbove:
case SymId::articStaccatoBelow:
case SymId::articAccentStaccatoAbove:
case SymId::articAccentStaccatoBelow:
case SymId::articMarcatoStaccatoAbove:
case SymId::articMarcatoStaccatoBelow:
return "staccato";
break;
case SymId::articStaccatissimoAbove:
case SymId::articStaccatissimoBelow:
case SymId::articStaccatissimoStrokeAbove:
case SymId::articStaccatissimoStrokeBelow:
case SymId::articStaccatissimoWedgeAbove:
case SymId::articStaccatissimoWedgeBelow:
return "staccatissimo";
break;
case SymId::articTenutoAbove:
case SymId::articTenutoBelow:
return "tenuto";
break;
case SymId::articMarcatoAbove:
case SymId::articMarcatoBelow:
return "strong-accent";
break;
case SymId::articTenutoStaccatoAbove:
case SymId::articTenutoStaccatoBelow:
return "detached-legato";
break;
case SymId::articSoftAccentAbove:
case SymId::articSoftAccentBelow:
return "soft-accent";
break;
case SymId::articStressAbove:
case SymId::articStressBelow:
return "stress";
break;
case SymId::articUnstressAbove:
case SymId::articUnstressBelow:
return "unstress";
break;
default:
; // nothing
break;
}
return "";
}
//---------------------------------------------------------
// symIdToOrnam
//---------------------------------------------------------
static QString symIdToOrnam(const SymId sid)
{
switch (sid) {
case SymId::ornamentTurnInverted:
case SymId::ornamentTurnSlash:
return "inverted-turn";
break;
case SymId::ornamentTurn:
return "turn";
break;
case SymId::ornamentTrill:
return "trill-mark";
break;
case SymId::ornamentMordent:
return "mordent";
break;
case SymId::ornamentShortTrill:
// return "short-trill";
return "inverted-mordent";
break;
case SymId::ornamentTremblement:
return "inverted-mordent long=\"yes\"";
break;
case SymId::ornamentPrallMordent:
return "mordent long=\"yes\"";
break;
case SymId::ornamentUpPrall:
return "inverted-mordent long=\"yes\" approach=\"below\"";
break;
case SymId::ornamentPrecompMordentUpperPrefix:
return "inverted-mordent long=\"yes\" approach=\"above\"";
break;
case SymId::ornamentUpMordent:
return "mordent long=\"yes\" approach=\"below\"";
break;
case SymId::ornamentDownMordent:
return "mordent long=\"yes\" approach=\"above\"";
break;
case SymId::ornamentPrallDown:
return "inverted-mordent long=\"yes\" departure=\"below\"";
break;
case SymId::ornamentPrallUp:
return "inverted-mordent long=\"yes\" departure=\"above\"";
break;
case SymId::ornamentLinePrall:
// MusicXML 3.0 does not distinguish between downprall and lineprall
return "inverted-mordent long=\"yes\" approach=\"above\"";
break;
case SymId::ornamentPrecompSlide:
return "schleifer";
break;
default:
; // nothing
break;
}
return "";
}
//---------------------------------------------------------
// symIdToTechn
//---------------------------------------------------------
static QString symIdToTechn(const SymId sid)
{
switch (sid) {
case SymId::brassMuteClosed:
return "stopped";
break;
case SymId::stringsHarmonic:
return "harmonic";
break;
case SymId::stringsUpBow:
return "up-bow";
break;
case SymId::stringsDownBow:
return "down-bow";
break;
case SymId::pluckedSnapPizzicatoAbove:
return "snap-pizzicato";
break;
case SymId::brassMuteOpen:
return "open-string";
break;
case SymId::stringsThumbPosition:
return "thumb-position";
break;
default:
; // nothing
break;
}
return "";
}
//---------------------------------------------------------
// writeChordLines
//---------------------------------------------------------
static void writeChordLines(const Chord* const chord, XmlWriter& xml, Notations& notations, Articulations& articulations)
{
for (EngravingItem* e : chord->el()) {
LOGD("writeChordLines: el %p type %d (%s)", e, int(e->type()), e->typeName());
if (e->type() == ElementType::CHORDLINE) {
ChordLine const* const cl = static_cast<ChordLine*>(e);
QString subtype;
switch (cl->chordLineType()) {
case ChordLineType::FALL:
subtype = "falloff";
break;
case ChordLineType::DOIT:
subtype = "doit";
break;
case ChordLineType::PLOP:
subtype = "plop";
break;
case ChordLineType::SCOOP:
subtype = "scoop";
break;
default:
LOGD("unknown ChordLine subtype %d", int(cl->chordLineType()));
}
if (subtype != "") {
notations.tag(xml, e);
articulations.tag(xml);
xml.tagRaw(subtype);
}
}
}
}
//---------------------------------------------------------
// chordAttributes
//---------------------------------------------------------
void ExportMusicXml::chordAttributes(Chord* chord, Notations& notations, Technical& technical,
TrillHash& trillStart, TrillHash& trillStop)
{
if (!chord->isGrace()) {
QVector<EngravingItem*> fl;
for (EngravingItem* e : chord->segment()->annotations()) {
if (e->track() == chord->track() && e->isFermata()) {
fl.push_back(e);
}
}
fermatas(fl, _xml, notations);
}
const std::vector<Articulation*> na = chord->articulations();
// first the attributes whose elements are children of <articulations>
Articulations articulations;
for (const Articulation* a : na) {
if (!ExportMusicXml::canWrite(a)) {
continue;
}
auto sid = a->symId();
auto mxmlArtic = symIdToArtic(sid);
if (mxmlArtic != "") {
if (sid == SymId::articMarcatoAbove || sid == SymId::articMarcatoBelow) {
if (a->up()) {
mxmlArtic += " type=\"up\"";
} else {
mxmlArtic += " type=\"down\"";
}
}
notations.tag(_xml, a);
articulations.tag(_xml);
_xml.tagRaw(mxmlArtic);
}
}
Breath* b = chord->hasBreathMark();
if (b && ExportMusicXml::canWrite(b)) {
notations.tag(_xml, b);
articulations.tag(_xml);
_xml.tag(b->isCaesura() ? "caesura" : "breath-mark");
}
writeChordLines(chord, _xml, notations, articulations);
articulations.etag(_xml);
// then the attributes whose elements are children of <ornaments>
Ornaments ornaments;
for (const Articulation* a : na) {
if (!ExportMusicXml::canWrite(a)) {
continue;
}
auto sid = a->symId();
auto mxmlOrnam = symIdToOrnam(sid);
if (mxmlOrnam != "") {
notations.tag(_xml, a);
ornaments.tag(_xml);
_xml.tagRaw(mxmlOrnam);
}
}
tremoloSingleStartStop(chord, notations, ornaments, _xml);
wavyLineStartStop(chord, notations, ornaments, trillStart, trillStop);
ornaments.etag(_xml);
// and finally the attributes whose elements are children of <technical>
for (const Articulation* a : na) {
if (!ExportMusicXml::canWrite(a)) {
continue;
}
auto sid = a->symId();
QString placement;
QString direction;
QString attr;
if (!a->isStyled(Pid::ARTICULATION_ANCHOR) && a->anchor() != ArticulationAnchor::CHORD) {
placement
= (a->anchor() == ArticulationAnchor::BOTTOM_STAFF || a->anchor() == ArticulationAnchor::BOTTOM_CHORD) ? "below" : "above";
} else if (!a->isStyled(Pid::DIRECTION) && a->direction() != DirectionV::AUTO) {
direction = (a->direction() == DirectionV::DOWN) ? "down" : "up";
}
/* For future use if/when implemented font details for articulation
if (!a->isStyled(Pid::FONT_FACE))
attr += QString(" font-family=\"%1\"").arg(a->getProperty(Pid::FONT_FACE).toString());
if (!a->isStyled(Pid::FONT_SIZE))
attr += QString(" font-size=\"%1\"").arg(a->getProperty(Pid::FONT_SIZE).toReal());
if (!a->isStyled(Pid::FONT_STYLE))
attr += fontStyleToXML(static_cast<FontStyle>(a->getProperty(Pid::FONT_STYLE).toInt()), false);
*/
auto mxmlTechn = symIdToTechn(sid);
if (mxmlTechn != "") {
notations.tag(_xml, a);
technical.tag(_xml);
if (sid == SymId::stringsHarmonic) {
if (placement != "") {
attr += QString(" placement=\"%1\"").arg(placement);
}
_xml.startElementRaw(mxmlTechn + attr);
_xml.tag("natural");
_xml.endElement();
} else { // TODO: check additional modifier (attr) for other symbols
_xml.tagRaw(mxmlTechn);
}
}
}
// check if all articulations were handled
for (const Articulation* a : na) {
if (!ExportMusicXml::canWrite(a)) {
continue;
}
auto sid = a->symId();
if (symIdToArtic(sid) == ""
&& symIdToOrnam(sid) == ""
&& symIdToTechn(sid) == ""
&& !isLaissezVibrer(sid)) {
LOGD("unknown chord attribute %d %s", static_cast<int>(sid), qPrintable(a->translatedTypeUserName()));
}
}
}
//---------------------------------------------------------
// arpeggiate
//---------------------------------------------------------
// <notations>
// <arpeggiate direction="up"/>
// </notations>
static void arpeggiate(Arpeggio* arp, bool front, bool back, XmlWriter& xml, Notations& notations)
{
if (!ExportMusicXml::canWrite(arp)) {
return;
}
QString tagName;
switch (arp->arpeggioType()) {
case ArpeggioType::NORMAL:
notations.tag(xml, arp);
tagName = "arpeggiate";
break;
case ArpeggioType::UP: // fall through
case ArpeggioType::UP_STRAIGHT: // not supported by MusicXML, export as normal arpeggio
notations.tag(xml, arp);
tagName = "arpeggiate direction=\"up\"";
break;
case ArpeggioType::DOWN: // fall through
case ArpeggioType::DOWN_STRAIGHT: // not supported by MusicXML, export as normal arpeggio
notations.tag(xml, arp);
tagName = "arpeggiate direction=\"down\"";
break;
case ArpeggioType::BRACKET:
if (front) {
notations.tag(xml, arp);
tagName = "non-arpeggiate type=\"bottom\"";
}
if (back) {
notations.tag(xml, arp);
tagName = "non-arpeggiate type=\"top\"";
}
break;
default:
LOGD("unknown arpeggio subtype %d", int(arp->arpeggioType()));
break;
}
if (tagName != "") {
tagName += ExportMusicXml::positioningAttributes(arp);
xml.tagRaw(tagName);
}
}
//---------------------------------------------------------
// determineTupletNormalTicks
//---------------------------------------------------------
/**
Determine the ticks in the normal type for the tuplet \a chord.
This is non-zero only if chord if part of a tuplet containing
different length duration elements.
TODO determine how to handle baselen with dots and verify correct behaviour.
TODO verify if baseLen should always be correctly set
(it seems after MusicXMLimport this is not the case)
*/
static int determineTupletNormalTicks(Tuplet const* const t)
{
if (!t) {
return 0;
}
/*
LOGD("determineTupletNormalTicks t %p baselen %s", t, qPrintable(t->baseLen().ticks().print()));
for (int i = 0; i < t->elements().size(); ++i)
LOGD("determineTupletNormalTicks t %p i %d ticks %s", t, i, qPrintable(t->elements().at(i)->ticks().print()));
*/
for (unsigned int i = 1; i < t->elements().size(); ++i) {
if (t->elements().at(0)->ticks() != t->elements().at(i)->ticks()) {
return t->baseLen().ticks().ticks();
}
}
if (t->elements().size() != (unsigned)(t->ratio().numerator())) {
return t->baseLen().ticks().ticks();
}
return 0;
}
//---------------------------------------------------------
// beamFanAttribute
//---------------------------------------------------------
static QString beamFanAttribute(const Beam* const b)
{
const qreal epsilon = 0.1;
QString fan;
if ((b->growRight() - b->growLeft() > epsilon)) {
fan = "accel";
}
if ((b->growLeft() - b->growRight() > epsilon)) {
fan = "rit";
}
if (fan != "") {
return QString(" fan=\"%1\"").arg(fan);
}
return "";
}
//---------------------------------------------------------
// writeBeam
//---------------------------------------------------------
// beaming
// <beam number="1">start</beam>
// <beam number="1">end</beam>
// <beam number="1">continue</beam>
// <beam number="1">backward hook</beam>
// <beam number="1">forward hook</beam>
static void writeBeam(XmlWriter& xml, ChordRest* const cr, Beam* const b)
{
const auto& elements = b->elements();
const size_t idx = mu::indexOf(elements, cr);
if (idx == mu::nidx) {
LOGD("Beam::writeMusicXml(): cannot find ChordRest");
return;
}
int blp = -1; // beam level previous chord
int blc = -1; // beam level current chord
int bln = -1; // beam level next chord
// find beam level previous chord
for (size_t i = idx - 1; blp == -1 && i != mu::nidx; --i) {
const auto crst = elements[i];
if (crst->isChord()) {
blp = toChord(crst)->beams();
}
}
// find beam level current chord
if (cr->isChord()) {
blc = toChord(cr)->beams();
}
// find beam level next chord
for (size_t i = idx + 1; bln == -1 && i < elements.size(); ++i) {
const auto crst = elements[i];
if (crst->isChord()) {
bln = toChord(crst)->beams();
}
}
// find beam type and write
for (int i = 1; i <= blc; ++i) {
QString text;
if (blp < i && bln >= i) {
text = "begin";
} else if (blp < i && bln < i) {
if (bln > 0) {
text = "forward hook";
} else if (blp > 0) {
text = "backward hook";
}
} else if (blp >= i && bln < i) {
text = "end";
} else if (blp >= i && bln >= i) {
text = "continue";
}
if (text != "") {
QString tag = "beam";
tag += QString(" number=\"%1\"").arg(i);
if (text == "begin") {
tag += beamFanAttribute(b);
}
xml.tagRaw(tag, text);
}
}
}
//---------------------------------------------------------
// instrId
//---------------------------------------------------------
static QString instrId(int partNr, int instrNr)
{
return QString("id=\"P%1-I%2\"").arg(partNr).arg(instrNr);
}
//---------------------------------------------------------
// isNoteheadParenthesis
//---------------------------------------------------------
static bool isNoteheadParenthesis(const Symbol* symbol)
{
const SymId sym = symbol->sym();
return sym == SymId::noteheadParenthesisLeft || sym == SymId::noteheadParenthesisRight;
}
//---------------------------------------------------------
// writeNotehead
//---------------------------------------------------------
static void writeNotehead(XmlWriter& xml, const Note* const note)
{
QString noteheadTagname = QString("notehead");
noteheadTagname += color2xml(note);
bool leftParenthesis = false, rightParenthesis = false;
for (EngravingItem* elem : note->el()) {
if (elem->type() == ElementType::SYMBOL) {
Symbol* s = static_cast<Symbol*>(elem);
if (s->sym() == SymId::noteheadParenthesisLeft) {
leftParenthesis = true;
} else if (s->sym() == SymId::noteheadParenthesisRight) {
rightParenthesis = true;
}
}
}
if (rightParenthesis && leftParenthesis) {
noteheadTagname += " parentheses=\"yes\"";
}
if (note->headType() == NoteHeadType::HEAD_QUARTER) {
noteheadTagname += " filled=\"yes\"";
} else if ((note->headType() == NoteHeadType::HEAD_HALF) || (note->headType() == NoteHeadType::HEAD_WHOLE)) {
noteheadTagname += " filled=\"no\"";
}
if (!note->visible()) {
// The notehead is invisible but other parts of the note might
// still be visible so don't export <note print-object="no">.
xml.tagRaw(noteheadTagname, "none");
} else if (note->headGroup() == NoteHeadGroup::HEAD_SLASH) {
xml.tagRaw(noteheadTagname, "slash");
} else if (note->headGroup() == NoteHeadGroup::HEAD_TRIANGLE_UP) {
xml.tagRaw(noteheadTagname, "triangle");
} else if (note->headGroup() == NoteHeadGroup::HEAD_DIAMOND) {
xml.tagRaw(noteheadTagname, "diamond");
} else if (note->headGroup() == NoteHeadGroup::HEAD_PLUS) {
xml.tagRaw(noteheadTagname, "cross");
} else if (note->headGroup() == NoteHeadGroup::HEAD_CROSS) {
xml.tagRaw(noteheadTagname, "x");
} else if (note->headGroup() == NoteHeadGroup::HEAD_XCIRCLE) {
xml.tagRaw(noteheadTagname, "circle-x");
} else if (note->headGroup() == NoteHeadGroup::HEAD_TRIANGLE_DOWN) {
xml.tagRaw(noteheadTagname, "inverted triangle");
} else if (note->headGroup() == NoteHeadGroup::HEAD_SLASHED1) {
xml.tagRaw(noteheadTagname, "slashed");
} else if (note->headGroup() == NoteHeadGroup::HEAD_SLASHED2) {
xml.tagRaw(noteheadTagname, "back slashed");
} else if (note->headGroup() == NoteHeadGroup::HEAD_DO) {
xml.tagRaw(noteheadTagname, "do");
} else if (note->headGroup() == NoteHeadGroup::HEAD_RE) {
xml.tagRaw(noteheadTagname, "re");
} else if (note->headGroup() == NoteHeadGroup::HEAD_MI) {
xml.tagRaw(noteheadTagname, "mi");
} else if (note->headGroup() == NoteHeadGroup::HEAD_FA && !note->chord()->up()) {
xml.tagRaw(noteheadTagname, "fa");
} else if (note->headGroup() == NoteHeadGroup::HEAD_FA && note->chord()->up()) {
xml.tagRaw(noteheadTagname, "fa up");
} else if (note->headGroup() == NoteHeadGroup::HEAD_LA) {
xml.tagRaw(noteheadTagname, "la");
} else if (note->headGroup() == NoteHeadGroup::HEAD_TI) {
xml.tagRaw(noteheadTagname, "ti");
} else if (note->headGroup() == NoteHeadGroup::HEAD_SOL) {
xml.tagRaw(noteheadTagname, "so");
} else if (note->color() != engravingConfiguration()->defaultColor()) {
xml.tagRaw(noteheadTagname, "normal");
} else if (rightParenthesis && leftParenthesis) {
xml.tagRaw(noteheadTagname, "normal");
} else if (note->headType() != NoteHeadType::HEAD_AUTO) {
xml.tagRaw(noteheadTagname, "normal");
}
}
//---------------------------------------------------------
// writeFingering
//---------------------------------------------------------
static void writeFingering(XmlWriter& xml, Notations& notations, Technical& technical, const Note* const note)
{
for (const EngravingItem* e : note->el()) {
if (!ExportMusicXml::canWrite(e)) {
continue;
}
if (e->type() == ElementType::FINGERING) {
const TextBase* f = toTextBase(e);
notations.tag(xml, e);
technical.tag(xml);
QString t = MScoreTextToMXML::toPlainText(f->xmlText());
QString attr;
if (!f->isStyled(Pid::PLACEMENT) || f->placement() == PlacementV::BELOW) {
attr = QString(" placement=\"%1\"").arg((f->placement() == PlacementV::BELOW) ? "below" : "above");
}
if (!f->isStyled(Pid::FONT_FACE)) {
attr += QString(" font-family=\"%1\"").arg(f->getProperty(Pid::FONT_FACE).value<String>());
}
if (!f->isStyled(Pid::FONT_SIZE)) {
attr += QString(" font-size=\"%1\"").arg(f->getProperty(Pid::FONT_SIZE).toReal());
}
if (!f->isStyled(Pid::FONT_STYLE)) {
attr += fontStyleToXML(static_cast<FontStyle>(f->getProperty(Pid::FONT_STYLE).toInt()), false);
}
if (f->textStyleType() == TextStyleType::RH_GUITAR_FINGERING) {
xml.tagRaw("pluck" + attr, t);
} else if (f->textStyleType() == TextStyleType::LH_GUITAR_FINGERING) {
xml.tagRaw("fingering" + attr, t);
} else if (f->textStyleType() == TextStyleType::FINGERING) {
// for generic fingering, try to detect plucking
// (backwards compatibility with MuseScore 1.x)
// p, i, m, a, c represent the plucking finger
if (t == "p" || t == "i" || t == "m" || t == "a" || t == "c") {
xml.tagRaw("pluck" + attr, t);
} else {
xml.tagRaw("fingering" + attr, t);
}
} else if (f->textStyleType() == TextStyleType::STRING_NUMBER) {
bool ok;
int i = t.toInt(&ok);
if (ok) {
if (i == 0) {
xml.tagRaw("open-string" + attr);
} else if (i > 0) {
xml.tagRaw("string" + attr, t);
}
}
if (!ok || i < 0) {
LOGD("invalid string number '%s'", qPrintable(t));
}
} else {
LOGD("unknown fingering style");
}
} else {
// TODO
}
}
}
//---------------------------------------------------------
// writeNotationSymbols
//---------------------------------------------------------
static void writeNotationSymbols(XmlWriter& xml, Notations& notations, const ElementList& elist, bool excludeParentheses)
{
for (const EngravingItem* e : elist) {
if (!e->isSymbol() || !ExportMusicXml::canWrite(e)) {
continue;
}
const Symbol* symbol = toSymbol(e);
if (excludeParentheses && isNoteheadParenthesis(symbol)) {
// Already handled when writing notehead properties.
continue;
}
notations.tag(xml, symbol);
xml.tag("other-notation", { { "type", "single" }, { "smufl", symbol->symName() } });
}
}
//---------------------------------------------------------
// stretchCorrActFraction
//---------------------------------------------------------
static Fraction stretchCorrActFraction(const Note* const note)
{
// time signature stretch factor
const Fraction str = note->chord()->staff()->timeStretch(note->chord()->tick());
// chord's actual ticks corrected for stretch
return note->chord()->actualTicks() * str;
}
//---------------------------------------------------------
// tremoloCorrection
//---------------------------------------------------------
// duration correction for two note tremolo
static int tremoloCorrection(const Note* const note)
{
int tremCorr = 1;
if (isTwoNoteTremolo(note->chord())) {
tremCorr = 2;
}
return tremCorr;
}
//---------------------------------------------------------
// isSmallNote
//---------------------------------------------------------
static bool isSmallNote(const Note* const note)
{
return note->isSmall() || note->chord()->isSmall();
}
//---------------------------------------------------------
// isCueNote
//---------------------------------------------------------
static bool isCueNote(const Note* const note)
{
return (!note->chord()->isGrace()) && isSmallNote(note) && !note->play();
}
//---------------------------------------------------------
// timeModification
//---------------------------------------------------------
static Fraction timeModification(const Tuplet* const tuplet, const int tremolo = 1)
{
int actNotes { tremolo };
int nrmNotes { 1 };
const Tuplet* t { tuplet };
while (t) {
// cannot use Fraction::operator*() as it contains a reduce(),
// which would change a 6:4 tuplet into 3:2
actNotes *= t->ratio().numerator();
nrmNotes *= t->ratio().denominator();
t = t->tuplet();
}
return { actNotes, nrmNotes };
}
//---------------------------------------------------------
// writeTypeAndDots
//---------------------------------------------------------
static void writeTypeAndDots(XmlWriter& xml, const Note* const note)
{
int dots = 0;
const auto ratio = timeModification(note->chord()->tuplet());
const auto strActFraction = stretchCorrActFraction(note);
const Fraction tt = strActFraction * ratio * tremoloCorrection(note);
const QString s { tick2xml(tt, &dots) };
if (s.isEmpty()) {
LOGD("no note type found for fraction %d / %d", strActFraction.numerator(), strActFraction.denominator());
}
// small notes are indicated by size=cue, but for grace and cue notes this is implicit
if (isSmallNote(note) && !isCueNote(note) && !note->chord()->isGrace()) {
xml.tag("type", { { "size", "cue" } }, s);
} else {
xml.tag("type", s);
}
for (int ni = dots; ni > 0; ni--) {
xml.tag("dot");
}
}
//---------------------------------------------------------
// writeTimeModification
//---------------------------------------------------------
static void writeTimeModification(XmlWriter& xml, const Tuplet* const tuplet, const int tremolo = 1)
{
const auto ratio = timeModification(tuplet, tremolo);
if (ratio != Fraction(1, 1)) {
const auto actNotes = ratio.numerator();
const auto nrmNotes = ratio.denominator();
const auto nrmTicks = determineTupletNormalTicks(tuplet);
xml.startElement("time-modification");
xml.tag("actual-notes", actNotes);
xml.tag("normal-notes", nrmNotes);
//LOGD("nrmTicks %d", nrmTicks);
if (nrmTicks > 0) {
int nrmDots { 0 };
const QString nrmType { tick2xml(Fraction::fromTicks(nrmTicks), &nrmDots) };
if (nrmType.isEmpty()) {
LOGD("no note type found for ticks %d", nrmTicks);
} else {
xml.tag("normal-type", nrmType);
for (int ni = nrmDots; ni > 0; ni--) {
xml.tag("normal-dot");
}
}
}
xml.endElement();
}
}
//---------------------------------------------------------
// writePitch
//---------------------------------------------------------
static void writePitch(XmlWriter& xml, const Note* const note, const bool useDrumset)
{
// step / alter / octave
QString step;
int alter = 0;
int octave = 0;
const auto chord = note->chord();
if (chord->staff() && chord->staff()->isTabStaff(Fraction(0, 1))) {
tabpitch2xml(note->pitch(), note->tpc(), step, alter, octave);
} else {
if (!useDrumset) {
pitch2xml(note, step, alter, octave);
} else {
unpitch2xml(note, step, octave);
}
}
xml.startElement(useDrumset ? "unpitched" : "pitch");
xml.tag(useDrumset ? "display-step" : "step", step);
// Check for microtonal accidentals and overwrite "alter" tag
auto acc = note->accidental();
double alter2 = 0.0;
if (acc) {
switch (acc->accidentalType()) {
case AccidentalType::MIRRORED_FLAT: alter2 = -0.5;
break;
case AccidentalType::SHARP_SLASH: alter2 = 0.5;
break;
case AccidentalType::MIRRORED_FLAT2: alter2 = -1.5;
break;
case AccidentalType::SHARP_SLASH4: alter2 = 1.5;
break;
default: break;
}
}
if (alter && !alter2) {
xml.tag("alter", alter);
}
if (!alter && alter2) {
xml.tag("alter", alter2);
}
// TODO what if both alter and alter2 are present? For Example: playing with transposing instruments
xml.tag(useDrumset ? "display-octave" : "octave", octave);
xml.endElement();
}
//---------------------------------------------------------
// elementPosition
//---------------------------------------------------------
QString ExportMusicXml::elementPosition(const ExportMusicXml* const expMxml, const EngravingItem* const elm)
{
QString res;
if (configuration()->musicxmlExportLayout()) {
const double pageHeight = expMxml->getTenthsFromInches(expMxml->score()->styleD(Sid::pageHeight));
const auto meas = elm->findMeasure();
IF_ASSERT_FAILED(meas) {
return res;
}
double measureX = expMxml->getTenthsFromDots(meas->pagePos().x());
double measureY = pageHeight - expMxml->getTenthsFromDots(meas->pagePos().y());
double noteX = expMxml->getTenthsFromDots(elm->pagePos().x());
double noteY = pageHeight - expMxml->getTenthsFromDots(elm->pagePos().y());
res += QString(" default-x=\"%1\"").arg(QString::number(noteX - measureX, 'f', 2));
res += QString(" default-y=\"%1\"").arg(QString::number(noteY - measureY, 'f', 2));
}
return res;
}
//---------------------------------------------------------
// chord
//---------------------------------------------------------
/**
Write \a chord on \a staff with lyriclist \a ll.
For a single-staff part, \a staff equals zero, suppressing the <staff> element.
*/
void ExportMusicXml::chord(Chord* chord, staff_idx_t staff, const std::vector<Lyrics*>& ll, bool useDrumset)
{
Part* part = chord->score()->staff(chord->track() / VOICES)->part();
size_t partNr = mu::indexOf(_score->parts(), part);
int instNr = instrMap.value(part->instrument(_tick), -1);
/*
LOGD("chord() %p parent %p isgrace %d #gracenotes %d graceidx %d",
chord, chord->parent(), chord->isGrace(), chord->graceNotes().size(), chord->graceIndex());
LOGD("track %d tick %d part %p nr %d instr %p nr %d",
chord->track(), chord->tick(), part, partNr, part->instrument(tick), instNr);
for (EngravingItem* e : chord->el())
LOGD("chord %p el %p", chord, e);
*/
std::vector<Note*> nl = chord->notes();
bool grace = chord->isGrace();
if (!grace) {
_tick += chord->actualTicks();
}
#ifdef DEBUG_TICK
LOGD("ExportMusicXml::chord() oldtick=%d", tick);
LOGD("notetype=%d grace=%d", gracen, grace);
LOGD(" newtick=%d", tick);
#endif
for (Note* note : nl) {
_attr.doAttr(_xml, false);
QString noteTag = QString("note");
noteTag += elementPosition(this, note);
int velo = note->userVelocity();
if (velo != 0) {
noteTag += QString(" dynamics=\"%1\"").arg(QString::number(velo * 100.0 / 90.0, 'f', 2));
}
_xml.startElementRaw(noteTag);
if (grace) {
if (note->noteType() == NoteType::ACCIACCATURA) {
_xml.tag("grace", { { "slash", "yes" } });
} else {
_xml.tag("grace");
}
}
if (isCueNote(note)) {
_xml.tag("cue");
}
if (note != nl.front()) {
_xml.tag("chord");
}
writePitch(_xml, note, useDrumset);
// duration
if (!grace) {
_xml.tag("duration", stretchCorrActFraction(note).ticks() / div);
}
if (!isCueNote(note)) {
if (note->tieBack()) {
_xml.tag("tie", { { "type", "stop" } });
}
if (note->tieFor()) {
_xml.tag("tie", { { "type", "start" } });
}
}
// instrument for multi-instrument or unpitched parts
if (!useDrumset) {
if (instrMap.size() > 1 && instNr >= 0) {
_xml.tagRaw(QString("instrument %1").arg(instrId(static_cast<int>(partNr) + 1, instNr + 1)));
}
} else {
_xml.tagRaw(QString("instrument %1").arg(instrId(static_cast<int>(partNr) + 1, note->pitch() + 1)));
}
// voice
// for a single-staff part, staff is 0, which needs to be corrected
// to calculate the correct voice number
voice_idx_t voice = (static_cast<int>(staff) - 1) * VOICES + note->chord()->voice() + 1;
if (staff == 0) {
voice += VOICES;
}
_xml.tag("voice", static_cast<int>(voice));
writeTypeAndDots(_xml, note);
writeAccidental(_xml, "accidental", note->accidental());
writeTimeModification(_xml, note->chord()->tuplet(), tremoloCorrection(note));
// no stem for whole notes and beyond
if (chord->noStem() || chord->measure()->stemless(chord->staffIdx())) {
_xml.tag("stem", QString("none"));
} else if (note->chord()->stem()) {
_xml.tag("stem", QString(note->chord()->up() ? "up" : "down"));
}
writeNotehead(_xml, note);
// LVIFIX: check move() handling
if (staff) {
_xml.tag("staff", static_cast<int>(staff + note->chord()->staffMove()));
}
if (note == nl.front() && chord->beam()) {
writeBeam(_xml, chord, chord->beam());
}
Notations notations;
Technical technical;
const Tie* tieBack = note->tieBack();
if (tieBack && ExportMusicXml::canWrite(tieBack)) {
notations.tag(_xml, tieBack);
_xml.tag("tied", { { "type", "stop" } });
}
const Tie* tieFor = note->tieFor();
if (tieFor && ExportMusicXml::canWrite(tieFor)) {
notations.tag(_xml, tieFor);
QString rest = slurTieLineStyle(tieFor);
_xml.tagRaw(QString("tied type=\"start\"%1").arg(rest));
}
const Articulation* laissezVibrer = findLaissezVibrer(chord);
if (laissezVibrer && ExportMusicXml::canWrite(laissezVibrer)) {
notations.tag(_xml, laissezVibrer);
_xml.tag("tied", { { "type", "let-ring" } });
}
if (note == nl.front()) {
if (!grace) {
tupletStartStop(chord, notations, _xml);
}
sh.doSlurs(chord, notations, _xml);
chordAttributes(chord, notations, technical, _trillStart, _trillStop);
}
writeFingering(_xml, notations, technical, note);
writeNotationSymbols(_xml, notations, note->el(), true);
// write tablature string / fret
if (chord->staff() && chord->staff()->isTabStaff(Fraction(0, 1))) {
if (note->fret() >= 0 && note->string() >= 0) {
notations.tag(_xml, note);
technical.tag(_xml);
_xml.tag("string", note->string() + 1);
_xml.tag("fret", note->fret());
}
}
technical.etag(_xml);
if (chord->arpeggio()) {
arpeggiate(chord->arpeggio(), note == nl.front(), note == nl.back(), _xml, notations);
}
for (Spanner* spanner : note->spannerFor()) {
if (spanner->type() == ElementType::GLISSANDO && ExportMusicXml::canWrite(spanner)) {
gh.doGlissandoStart(static_cast<Glissando*>(spanner), notations, _xml);
}
}
for (Spanner* spanner : note->spannerBack()) {
if (spanner->type() == ElementType::GLISSANDO && ExportMusicXml::canWrite(spanner)) {
gh.doGlissandoStop(static_cast<Glissando*>(spanner), notations, _xml);
}
}
// write glissando (only for last note)
/*
Chord* ch = nextChord(chord);
if ((note == nl.back()) && ch && ch->glissando()) {
gh.doGlissandoStart(ch, notations, xml);
}
if (chord->glissando()) {
gh.doGlissandoStop(chord, notations, xml);
}
*/
notations.etag(_xml);
// write lyrics (only for first note)
if (!grace && (note == nl.front())) {
lyrics(ll, chord->track());
}
_xml.endElement();
}
}
//---------------------------------------------------------
// rest
//---------------------------------------------------------
/**
Write \a rest on \a staff.
For a single-staff part, \a staff equals zero, suppressing the <staff> element.
*/
void ExportMusicXml::rest(Rest* rest, staff_idx_t staff)
{
static char table2[] = "CDEFGAB";
#ifdef DEBUG_TICK
LOGD("ExportMusicXml::rest() oldtick=%d", tick);
#endif
_attr.doAttr(_xml, false);
QString noteTag = QString("note");
noteTag += color2xml(rest);
noteTag += elementPosition(this, rest);
if (!rest->visible()) {
noteTag += " print-object=\"no\"";
}
_xml.startElementRaw(noteTag);
int yOffsSt = 0;
int oct = 0;
int stp = 0;
ClefType clef = rest->staff()->clef(rest->tick());
int po = ClefInfo::pitchOffset(clef);
// Determine y position, but leave at zero in case of tablature staff
// as no display-step or display-octave should be written for a tablature staff,
if (clef != ClefType::TAB && clef != ClefType::TAB_SERIF && clef != ClefType::TAB4 && clef != ClefType::TAB4_SERIF) {
double yOffsSp = rest->offset().y() / rest->spatium(); // y offset in spatium (negative = up)
yOffsSt = -2 * int(yOffsSp > 0.0 ? yOffsSp + 0.5 : yOffsSp - 0.5); // same rounded to int (positive = up)
po -= 4; // pitch middle staff line (two lines times two steps lower than top line)
po += yOffsSt; // rest "pitch"
oct = po / 7; // octave
stp = po % 7; // step
}
QString restTag { "rest" };
const TDuration d = rest->durationType();
if (d.type() == DurationType::V_MEASURE) {
restTag += " measure=\"yes\"";
}
// Either <rest/>
// or <rest><display-step>F</display-step><display-octave>5</display-octave></rest>
if (yOffsSt == 0) {
_xml.tagRaw(restTag);
} else {
_xml.startElementRaw(restTag);
_xml.tag("display-step", QString(QChar(table2[stp])));
_xml.tag("display-octave", oct - 1);
_xml.endElement();
}
Fraction tickLen = rest->actualTicks();
if (d.type() == DurationType::V_MEASURE) {
// to avoid forward since rest->ticklen=0 in this case.
tickLen = rest->measure()->ticks();
}
_tick += tickLen;
#ifdef DEBUG_TICK
LOGD(" tickLen=%d newtick=%d", tickLen, tick);
#endif
_xml.tag("duration", tickLen.ticks() / div);
// for a single-staff part, staff is 0, which needs to be corrected
// to calculate the correct voice number
voice_idx_t voice = (static_cast<int>(staff) - 1) * VOICES + rest->voice() + 1;
if (staff == 0) {
voice += VOICES;
}
_xml.tag("voice", static_cast<int>(voice));
// do not output a "type" element for whole measure rest
if (d.type() != DurationType::V_MEASURE) {
AsciiStringView s = TConv::toXml(d.type());
int dots = rest->dots();
if (rest->isSmall()) {
_xml.tag("type", { { "size", "cue" } }, s);
} else {
_xml.tag("type", s);
}
for (int i = dots; i > 0; i--) {
_xml.tag("dot");
}
}
writeTimeModification(_xml, rest->tuplet());
if (staff) {
_xml.tag("staff", static_cast<int>(staff));
}
Notations notations;
QVector<EngravingItem*> fl;
for (EngravingItem* e : rest->segment()->annotations()) {
if (e->isFermata() && e->track() == rest->track()) {
fl.push_back(e);
}
}
fermatas(fl, _xml, notations);
Articulations articulations;
Breath* b = rest->hasBreathMark();
if (b && ExportMusicXml::canWrite(b)) {
notations.tag(_xml, b);
articulations.tag(_xml);
_xml.tag(b->isCaesura() ? "caesura" : "breath-mark");
}
articulations.etag(_xml);
Ornaments ornaments;
wavyLineStartStop(rest, notations, ornaments, _trillStart, _trillStop);
ornaments.etag(_xml);
writeNotationSymbols(_xml, notations, rest->el(), false);
sh.doSlurs(rest, notations, _xml);
tupletStartStop(rest, notations, _xml);
notations.etag(_xml);
_xml.endElement();
}
//---------------------------------------------------------
// directionTag
//---------------------------------------------------------
static void directionTag(XmlWriter& xml, Attributes& attr, EngravingItem const* const el = 0)
{
attr.doAttr(xml, false);
QString tagname = QString("direction");
if (el) {
/*
LOGD("directionTag() spatium=%g elem=%p tp=%d (%s)\ndirectionTag() x=%g y=%g xsp,ysp=%g,%g w=%g h=%g userOff.y=%g",
el->spatium(),
el,
el->type(),
el->typeName(),
el->x(), el->y(),
el->x()/el->spatium(), el->y()/el->spatium(),
el->width(), el->height(),
el->offset().y()
);
*/
const EngravingItem* pel = 0;
const LineSegment* seg = 0;
if (el->type() == ElementType::HAIRPIN || el->type() == ElementType::OTTAVA
|| el->type() == ElementType::PEDAL || el->type() == ElementType::TEXTLINE
|| el->type() == ElementType::LET_RING || el->type() == ElementType::PALM_MUTE
|| el->type() == ElementType::WHAMMY_BAR || el->type() == ElementType::RASGUEADO
|| el->type() == ElementType::HARMONIC_MARK || el->type() == ElementType::PICK_SCRAPE
|| el->type() == ElementType::GRADUAL_TEMPO_CHANGE) {
// handle elements derived from SLine
// find the system containing the first linesegment
const SLine* sl = static_cast<const SLine*>(el);
if (!sl->segmentsEmpty()) {
seg = toLineSegment(sl->frontSegment());
/*
LOGD("directionTag() seg=%p x=%g y=%g w=%g h=%g cpx=%g cpy=%g userOff.y=%g",
seg, seg->x(), seg->y(),
seg->width(), seg->height(),
seg->pagePos().x(), seg->pagePos().y(),
seg->offset().y());
*/
pel = seg->parentItem();
}
} else if (el->type() == ElementType::DYNAMIC
|| el->type() == ElementType::INSTRUMENT_CHANGE
|| el->type() == ElementType::REHEARSAL_MARK
|| el->type() == ElementType::STAFF_TEXT
|| el->type() == ElementType::PLAYTECH_ANNOTATION
|| el->type() == ElementType::SYMBOL
|| el->type() == ElementType::TEXT) {
// handle other elements attached (e.g. via Segment / Measure) to a system
// find the system containing this element
for (const EngravingItem* e = el; e; e = e->parentItem()) {
if (e->type() == ElementType::SYSTEM) {
pel = e;
}
}
} else {
LOGD("directionTag() element %p tp=%d (%s) not supported",
el, int(el->type()), el->typeName());
}
/*
if (pel) {
LOGD("directionTag() prnt tp=%d (%s) x=%g y=%g w=%g h=%g userOff.y=%g",
pel->type(),
pel->typeName(),
pel->x(), pel->y(),
pel->width(), pel->height(),
pel->offset().y());
}
*/
if (pel && pel->type() == ElementType::SYSTEM) {
/*
const System* sys = static_cast<const System*>(pel);
QRectF bb = sys->staff(el->staffIdx())->bbox();
LOGD("directionTag() syst=%p sys x=%g y=%g cpx=%g cpy=%g",
sys, sys->pos().x(), sys->pos().y(),
sys->pagePos().x(),
sys->pagePos().y()
);
LOGD("directionTag() staff x=%g y=%g w=%g h=%g",
bb.x(), bb.y(),
bb.width(), bb.height());
// element is above the staff if center of bbox is above center of staff
LOGD("directionTag() center diff=%g", el->y() + el->height() / 2 - bb.y() - bb.height() / 2);
*/
if (el->isHairpin() || el->isOttava() || el->isPedal() || el->isTextLine()) {
// for the line type elements the reference point is vertically centered
// actual position info is in the segments
// compare the segment's canvas ypos with the staff's center height
// if (seg->pagePos().y() < sys->pagePos().y() + bb.y() + bb.height() / 2)
if (el->placement() == PlacementV::ABOVE) {
tagname += " placement=\"above\"";
} else {
tagname += " placement=\"below\"";
}
} else if (el->isDynamic()) {
tagname += " placement=\"";
tagname += el->placement() == PlacementV::ABOVE ? "above" : "below";
tagname += "\"";
} else {
/*
LOGD("directionTag() staff ely=%g elh=%g bby=%g bbh=%g",
el->y(), el->height(),
bb.y(), bb.height());
*/
// if (el->y() + el->height() / 2 < /*bb.y() +*/ bb.height() / 2)
if (el->placement() == PlacementV::ABOVE) {
tagname += " placement=\"above\"";
} else {
tagname += " placement=\"below\"";
}
}
} // if (pel && ...
}
xml.startElementRaw(tagname);
}
//---------------------------------------------------------
// directionETag
//---------------------------------------------------------
static void directionETag(XmlWriter& xml, staff_idx_t staff, int offs = 0)
{
if (offs) {
xml.tag("offset", offs);
}
if (staff) {
xml.tag("staff", static_cast<int>(staff));
}
xml.endElement();
}
//---------------------------------------------------------
// partGroupStart
//---------------------------------------------------------
static void partGroupStart(XmlWriter& xml, int number, BracketType bracket)
{
xml.startElement("part-group", { { "type", "start" }, { "number", number } });
QString br = "";
switch (bracket) {
case BracketType::NO_BRACKET:
br = "none";
break;
case BracketType::NORMAL:
br = "bracket";
break;
case BracketType::BRACE:
br = "brace";
break;
case BracketType::LINE:
br = "line";
break;
case BracketType::SQUARE:
br = "square";
break;
default:
LOGD("bracket subtype %d not understood", int(bracket));
}
if (br != "") {
xml.tag("group-symbol", br);
}
xml.endElement();
}
//---------------------------------------------------------
// findMetronome
//---------------------------------------------------------
static bool findMetronome(const std::list<TextFragment>& list,
std::list<TextFragment>& wordsLeft, // words left of metronome
bool& hasParen, // parenthesis
QString& metroLeft, // left part of metronome
QString& metroRight, // right part of metronome
std::list<TextFragment>& wordsRight // words right of metronome
)
{
QString words = MScoreTextToMXML::toPlainTextPlusSymbols(list);
//LOGD("findMetronome('%s')", qPrintable(words));
hasParen = false;
metroLeft = "";
metroRight = "";
int metroPos = -1; // metronome start position
int metroLen = 0; // metronome length
int indEq = words.indexOf('=');
if (indEq <= 0) {
return false;
}
int len1 = 0;
TDuration dur;
// find first note, limiting search to the part left of the first '=',
// to prevent matching the second note in a "note1 = note2" metronome
int pos1 = TempoText::findTempoDuration(words.left(indEq), len1, dur);
QRegularExpression equationRegEx("\\s*=\\s*");
QRegularExpressionMatch equationMatch;
int pos2 = words.indexOf(equationRegEx, pos1 + len1, &equationMatch);
if (pos1 != -1 && pos2 == pos1 + len1) {
int len2 = equationMatch.capturedLength();
if (words.length() > pos2 + len2) {
QString s1 = words.mid(0, pos1); // string to the left of metronome
QString s2 = words.mid(pos1, len1); // first note
QString s3 = words.mid(pos2, len2); // equals sign
QString s4 = words.mid(pos2 + len2); // string to the right of equals sign
/*
LOGD("found note and equals: '%s'%s'%s'%s'",
qPrintable(s1),
qPrintable(s2),
qPrintable(s3),
qPrintable(s4)
);
*/
// now determine what is to the right of the equals sign
// must have either a (dotted) note or a number at start of s4
int len3 = 0;
QRegularExpression numberRegEx("\\d+");
int pos3 = TempoText::findTempoDuration(s4, len3, dur);
if (pos3 == -1) {
// did not find note, try to find a number
QRegularExpressionMatch numberMatch;
pos3 = s4.indexOf(numberRegEx, 0, &numberMatch);
if (pos3 == 0) {
len3 = numberMatch.capturedLength();
}
}
if (pos3 == -1) {
// neither found
return false;
}
QString s5 = s4.mid(0, len3); // number or second note
QString s6 = s4.mid(len3); // string to the right of metronome
/*
LOGD("found right part: '%s'%s'",
qPrintable(s5),
qPrintable(s6)
);
*/
// determine if metronome has parentheses
// left part of string must end with parenthesis plus optional spaces
// right part of string must have parenthesis (but not in first pos)
int lparen = s1.indexOf("(");
int rparen = s6.indexOf(")");
hasParen = (lparen == s1.length() - 1 && rparen == 0);
metroLeft = s2;
metroRight = s5;
metroPos = pos1; // metronome position
metroLen = len1 + len2 + len3; // metronome length
if (hasParen) {
metroPos -= 1; // move left one position
metroLen += 2; // add length of '(' and ')'
}
// calculate starting position corrected for surrogate pairs
// (which were ignored by toPlainTextPlusSymbols())
int corrPos = metroPos;
for (int i = 0; i < metroPos; ++i) {
if (words.at(i).isHighSurrogate()) {
--corrPos;
}
}
metroPos = corrPos;
/*
LOGD("-> found '%s'%s' hasParen %d metro pos %d len %d",
qPrintable(metroLeft),
qPrintable(metroRight),
hasParen, metroPos, metroLen
);
*/
std::list<TextFragment> mid; // not used
MScoreTextToMXML::split(list, metroPos, metroLen, wordsLeft, mid, wordsRight);
return true;
}
}
return false;
}
//---------------------------------------------------------
// beatUnit
//---------------------------------------------------------
static void beatUnit(XmlWriter& xml, const TDuration dur)
{
int dots = dur.dots();
xml.tag("beat-unit", TConv::toXml(dur.type()));
while (dots > 0) {
xml.tag("beat-unit-dot");
--dots;
}
}
//---------------------------------------------------------
// wordsMetronome
//---------------------------------------------------------
static void wordsMetronome(XmlWriter& xml, Score* s, TextBase const* const text, const int offset)
{
//LOGD("wordsMetronome('%s')", qPrintable(text->xmlText()));
const std::list<TextFragment> list = text->fragmentList();
std::list<TextFragment> wordsLeft; // words left of metronome
bool hasParen; // parenthesis
QString metroLeft; // left part of metronome
QString metroRight; // right part of metronome
std::list<TextFragment> wordsRight; // words right of metronome
// set the default words format
const QString mtf = s->styleSt(Sid::MusicalTextFont);
const CharFormat defFmt = formatForWords(s);
if (findMetronome(list, wordsLeft, hasParen, metroLeft, metroRight, wordsRight)) {
if (wordsLeft.size() > 0) {
xml.startElement("direction-type");
QString attr = ExportMusicXml::positioningAttributes(text);
MScoreTextToMXML mttm("words", attr, defFmt, mtf);
mttm.writeTextFragments(wordsLeft, xml);
xml.endElement();
}
xml.startElement("direction-type");
QString tagName = QString("metronome parentheses=\"%1\"").arg(hasParen ? "yes" : "no");
tagName += ExportMusicXml::positioningAttributes(text);
xml.startElementRaw(tagName);
int len1 = 0;
TDuration dur;
TempoText::findTempoDuration(metroLeft, len1, dur);
beatUnit(xml, dur);
if (TempoText::findTempoDuration(metroRight, len1, dur) != -1) {
beatUnit(xml, dur);
} else {
xml.tag("per-minute", metroRight);
}
xml.endElement();
xml.endElement();
if (wordsRight.size() > 0) {
xml.startElement("direction-type");
QString attr = ExportMusicXml::positioningAttributes(text);
MScoreTextToMXML mttm("words", attr, defFmt, mtf);
mttm.writeTextFragments(wordsRight, xml);
xml.endElement();
}
} else {
xml.startElement("direction-type");
QString attr;
if (text->hasFrame()) {
if (text->circle()) {
attr = " enclosure=\"circle\"";
} else {
attr = " enclosure=\"rectangle\"";
}
}
attr += ExportMusicXml::positioningAttributes(text);
MScoreTextToMXML mttm("words", attr, defFmt, mtf);
//LOGD("words('%s')", qPrintable(text->text()));
mttm.writeTextFragments(text->fragmentList(), xml);
xml.endElement();
}
if (offset) {
xml.tag("offset", offset);
}
}
//---------------------------------------------------------
// tempoText
//---------------------------------------------------------
void ExportMusicXml::tempoText(TempoText const* const text, staff_idx_t staff)
{
const auto offset = calculateTimeDeltaInDivisions(text->tick(), tick(), div);
/*
LOGD("tick %s text->tick %s offset %d xmlText='%s')",
qPrintable(tick().print()),
qPrintable(text->tick().print()),
offset,
qPrintable(text->xmlText()));
*/
_attr.doAttr(_xml, false);
_xml.startElement("direction", { { "placement", (text->placement() == PlacementV::BELOW) ? "below" : "above" } });
wordsMetronome(_xml, _score, text, offset);
if (staff) {
_xml.tag("staff", static_cast<int>(staff));
}
// Format tempo with maximum 2 decimal places, because in some MuseScore files tempo is stored
// imprecisely and this could cause rounding errors (e.g. 92 BPM would be saved as 91.9998).
BeatsPerMinute bpm = text->tempo().toBPM();
qreal bpmRounded = round(bpm.val * 100) / 100;
_xml.tag("sound", { { "tempo", bpmRounded } });
_xml.endElement();
}
//---------------------------------------------------------
// words
//---------------------------------------------------------
void ExportMusicXml::words(TextBase const* const text, staff_idx_t staff)
{
const auto offset = calculateTimeDeltaInDivisions(text->tick(), tick(), div);
/*
LOGD("tick %s text->tick %s offset %d userOff.x=%f userOff.y=%f xmlText='%s' plainText='%s'",
qPrintable(tick().print()),
qPrintable(text->tick().print()),
offset,
text->offset().x(), text->offset().y(),
qPrintable(text->xmlText()),
qPrintable(text->plainText()));
*/
if (text->plainText() == "") {
// sometimes empty Texts are present, exporting would result
// in invalid MusicXML (as an empty direction-type would be created)
return;
}
directionTag(_xml, _attr, text);
wordsMetronome(_xml, _score, text, offset);
directionETag(_xml, staff);
}
//---------------------------------------------------------
// positioningAttributesForTboxText
//---------------------------------------------------------
QString ExportMusicXml::positioningAttributesForTboxText(const QPointF position, float spatium)
{
if (!configuration()->musicxmlExportLayout()) {
return "";
}
QPointF relative; // use zero relative position
return positionToQString(position, relative, spatium);
}
//---------------------------------------------------------
// tboxTextAsWords
//---------------------------------------------------------
void ExportMusicXml::tboxTextAsWords(TextBase const* const text, const staff_idx_t staff, const QPointF relativePosition)
{
if (text->plainText() == "") {
// sometimes empty Texts are present, exporting would result
// in invalid MusicXML (as an empty direction-type would be created)
return;
}
// set the default words format
const QString mtf = _score->styleSt(Sid::MusicalTextFont);
const CharFormat defFmt = formatForWords(_score);
_xml.startElement("direction", { { "placement", (relativePosition.y() < 0) ? "above" : "below" } });
_xml.startElement("direction-type");
QString attr;
if (text->hasFrame()) {
if (text->circle()) {
attr = " enclosure=\"circle\"";
} else {
attr = " enclosure=\"rectangle\"";
}
}
attr += ExportMusicXml::positioningAttributesForTboxText(relativePosition, text->spatium());
attr += " valign=\"top\"";
MScoreTextToMXML mttm("words", attr, defFmt, mtf);
mttm.writeTextFragments(text->fragmentList(), _xml);
_xml.endElement();
directionETag(_xml, staff);
}
//---------------------------------------------------------
// rehearsal
//---------------------------------------------------------
void ExportMusicXml::rehearsal(RehearsalMark const* const rmk, staff_idx_t staff)
{
if (rmk->plainText() == "") {
// sometimes empty Texts are present, exporting would result
// in invalid MusicXML (as an empty direction-type would be created)
return;
}
directionTag(_xml, _attr, rmk);
_xml.startElement("direction-type");
QString attr = positioningAttributes(rmk);
if (!rmk->hasFrame()) {
attr = " enclosure=\"none\"";
}
// set the default words format
const QString mtf = _score->styleSt(Sid::MusicalTextFont);
const CharFormat defFmt = formatForWords(_score);
// write formatted
MScoreTextToMXML mttm("rehearsal", attr, defFmt, mtf);
mttm.writeTextFragments(rmk->fragmentList(), _xml);
_xml.endElement();
const auto offset = calculateTimeDeltaInDivisions(rmk->tick(), tick(), div);
if (offset) {
_xml.tag("offset", offset);
}
directionETag(_xml, staff);
}
//---------------------------------------------------------
// findDashes -- get index of hairpin in dashes table
// return -1 if not found
//---------------------------------------------------------
int ExportMusicXml::findDashes(const TextLineBase* hp) const
{
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
if (dashes[i] == hp) {
return i;
}
}
return -1;
}
//---------------------------------------------------------
// findHairpin -- get index of hairpin in hairpin table
// return -1 if not found
//---------------------------------------------------------
int ExportMusicXml::findHairpin(const Hairpin* hp) const
{
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
if (hairpins[i] == hp) {
return i;
}
}
return -1;
}
//---------------------------------------------------------
// findInString
//---------------------------------------------------------
// find the longest first match of dynList's dynamic text in s
// used by the MusicXML export to correctly export dynamics embedded
// in spanner begin- or endtexts
// return match's position and length and the dynamic type
static int findDynamicInString(const QString& s, int& length, QString& type)
{
length = 0;
type = "";
int matchIndex { -1 };
const int n = static_cast<int>(DynamicType::LAST) - 1;
// for all dynamics, find their text in s
for (int i = 0; i < n; ++i) {
DynamicType t = static_cast<DynamicType>(i);
const QString dynamicText = Dynamic::dynamicText(t);
const int dynamicLength = dynamicText.length();
// note: skip entries with empty text
if (dynamicLength > 0) {
const auto index = s.indexOf(dynamicText);
if (index >= 0) {
// found a match, accept it if
// - it is the first one
// - or it starts a the same index but is longer ("pp" versus "p")
if (matchIndex == -1 || (index == matchIndex && dynamicLength > length)) {
matchIndex = index;
length = dynamicLength;
type = TConv::toXml(t).ascii();
}
}
}
}
return matchIndex;
}
//---------------------------------------------------------
// writeHairpinText
//---------------------------------------------------------
static void writeHairpinText(XmlWriter& xml, const TextLineBase* const tlb, bool isStart = true)
{
auto text = isStart ? tlb->beginText() : tlb->endText();
while (text != "") {
int dynamicLength { 0 };
QString dynamicsType;
auto dynamicPosition = findDynamicInString(text, dynamicLength, dynamicsType);
if (dynamicPosition == -1 || dynamicPosition > 0) {
// text remaining and either no dynamic of not at front of text
xml.startElement("direction-type");
QString tag = "words";
tag
+= QString(" font-family=\"%1\"").arg(tlb->getProperty(isStart ? Pid::BEGIN_FONT_FACE : Pid::END_FONT_FACE).value<String>());
tag += QString(" font-size=\"%1\"").arg(tlb->getProperty(isStart ? Pid::BEGIN_FONT_SIZE : Pid::END_FONT_SIZE).toReal());
tag += fontStyleToXML(static_cast<FontStyle>(tlb->getProperty(isStart ? Pid::BEGIN_FONT_STYLE : Pid::END_FONT_STYLE).toInt()));
tag += ExportMusicXml::positioningAttributes(tlb, isStart);
xml.tagRaw(tag, dynamicPosition == -1 ? text : text.left(dynamicPosition));
xml.endElement();
if (dynamicPosition == -1) {
text = "";
} else if (dynamicPosition > 0) {
text.remove(0, dynamicPosition);
dynamicPosition = 0;
}
}
if (dynamicPosition == 0) {
// dynamic at front of text
xml.startElement("direction-type");
QString tag = "dynamics";
tag += ExportMusicXml::positioningAttributes(tlb, isStart);
xml.startElementRaw(tag);
xml.tagRaw(dynamicsType);
xml.endElement();
xml.endElement();
text.remove(0, dynamicLength);
}
}
}
//---------------------------------------------------------
// hairpin
//---------------------------------------------------------
void ExportMusicXml::hairpin(Hairpin const* const hp, staff_idx_t staff, const Fraction& tick)
{
const auto isLineType = hp->isLineType();
int n;
if (isLineType) {
n = findDashes(hp);
if (n >= 0) {
dashes[n] = nullptr;
} else {
n = findDashes(nullptr);
if (n >= 0) {
dashes[n] = hp;
} else {
LOGD("too many overlapping dashes (hp %p staff %zu tick %d)", hp, staff, tick.ticks());
return;
}
}
} else {
n = findHairpin(hp);
if (n >= 0) {
hairpins[n] = nullptr;
} else {
n = findHairpin(nullptr);
if (n >= 0) {
hairpins[n] = hp;
} else {
LOGD("too many overlapping hairpins (hp %p staff %zu tick %d)", hp, staff, tick.ticks());
return;
}
}
}
directionTag(_xml, _attr, hp);
if (hp->tick() == tick) {
writeHairpinText(_xml, hp, hp->tick() == tick);
}
if (isLineType) {
if (hp->tick() == tick) {
_xml.startElement("direction-type");
QString tag = "dashes type=\"start\"";
tag += QString(" number=\"%1\"").arg(n + 1);
tag += positioningAttributes(hp, hp->tick() == tick);
_xml.tagRaw(tag);
_xml.endElement();
} else {
_xml.startElement("direction-type");
_xml.tagRaw(QString("dashes type=\"stop\" number=\"%1\"").arg(n + 1));
_xml.endElement();
}
} else {
_xml.startElement("direction-type");
QString tag = "wedge type=";
if (hp->tick() == tick) {
if (hp->hairpinType() == HairpinType::CRESC_HAIRPIN) {
tag += "\"crescendo\"";
if (hp->hairpinCircledTip()) {
tag += " niente=\"yes\"";
}
} else {
tag += "\"diminuendo\"";
}
} else {
tag += "\"stop\"";
if (hp->hairpinCircledTip() && hp->hairpinType() == HairpinType::DECRESC_HAIRPIN) {
tag += " niente=\"yes\"";
}
}
tag += QString(" number=\"%1\"").arg(n + 1);
tag += positioningAttributes(hp, hp->tick() == tick);
_xml.tagRaw(tag);
_xml.endElement();
}
if (hp->tick() != tick) {
writeHairpinText(_xml, hp, hp->tick() == tick);
}
directionETag(_xml, staff);
}
//---------------------------------------------------------
// findOttava -- get index of ottava in ottava table
// return -1 if not found
//---------------------------------------------------------
int ExportMusicXml::findOttava(const Ottava* ot) const
{
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
if (ottavas[i] == ot) {
return i;
}
}
return -1;
}
//---------------------------------------------------------
// ottava
// <octave-shift type="down" size="8" relative-y="14"/>
// <octave-shift type="stop" size="8"/>
//---------------------------------------------------------
void ExportMusicXml::ottava(Ottava const* const ot, staff_idx_t staff, const Fraction& tick)
{
auto n = findOttava(ot);
if (n >= 0) {
ottavas[n] = 0;
} else {
n = findOttava(0);
if (n >= 0) {
ottavas[n] = ot;
} else {
LOGD("too many overlapping ottavas (ot %p staff %zu tick %d)", ot, staff, tick.ticks());
return;
}
}
QString octaveShiftXml;
const auto st = ot->ottavaType();
if (ot->tick() == tick) {
const char* sz = 0;
const char* tp = 0;
switch (st) {
case OttavaType::OTTAVA_8VA:
sz = "8";
tp = "down";
break;
case OttavaType::OTTAVA_15MA:
sz = "15";
tp = "down";
break;
case OttavaType::OTTAVA_8VB:
sz = "8";
tp = "up";
break;
case OttavaType::OTTAVA_15MB:
sz = "15";
tp = "up";
break;
default:
LOGD("ottava subtype %d not understood", int(st));
}
if (sz && tp) {
octaveShiftXml = QString("octave-shift type=\"%1\" size=\"%2\" number=\"%3\"").arg(tp, sz).arg(n + 1);
}
} else {
if (st == OttavaType::OTTAVA_8VA || st == OttavaType::OTTAVA_8VB) {
octaveShiftXml = QString("octave-shift type=\"stop\" size=\"8\" number=\"%1\"").arg(n + 1);
} else if (st == OttavaType::OTTAVA_15MA || st == OttavaType::OTTAVA_15MB) {
octaveShiftXml = QString("octave-shift type=\"stop\" size=\"15\" number=\"%1\"").arg(n + 1);
} else {
LOGD("ottava subtype %d not understood", int(st));
}
}
if (octaveShiftXml != "") {
directionTag(_xml, _attr, ot);
_xml.startElement("direction-type");
octaveShiftXml += positioningAttributes(ot, ot->tick() == tick);
_xml.tagRaw(octaveShiftXml);
_xml.endElement();
directionETag(_xml, staff);
}
}
//---------------------------------------------------------
// pedal
//---------------------------------------------------------
void ExportMusicXml::pedal(Pedal const* const pd, staff_idx_t staff, const Fraction& tick)
{
directionTag(_xml, _attr, pd);
_xml.startElement("direction-type");
QString pedalXml;
if (pd->tick() == tick) {
pedalXml = "pedal type=\"start\" line=\"yes\"";
} else {
pedalXml = "pedal type=\"stop\" line=\"yes\"";
}
pedalXml += positioningAttributes(pd, pd->tick() == tick);
_xml.tagRaw(pedalXml);
_xml.endElement();
directionETag(_xml, staff);
}
//---------------------------------------------------------
// findBracket -- get index of bracket in bracket table
// return -1 if not found
//---------------------------------------------------------
int ExportMusicXml::findBracket(const TextLineBase* tl) const
{
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
if (brackets[i] == tl) {
return i;
}
}
return -1;
}
//---------------------------------------------------------
// textLine
//---------------------------------------------------------
void ExportMusicXml::textLine(TextLineBase const* const tl, staff_idx_t staff, const Fraction& tick)
{
using namespace mu::draw;
int n;
// special case: a dashed line w/o hooks is written as dashes
const auto isDashes = tl->lineStyle() == LineType::DASHED && (tl->beginHookType() == HookType::NONE)
&& (tl->endHookType() == HookType::NONE);
if (isDashes) {
n = findDashes(tl);
if (n >= 0) {
dashes[n] = nullptr;
} else {
n = findBracket(nullptr);
if (n >= 0) {
dashes[n] = tl;
} else {
LOGD("too many overlapping dashes (tl %p staff %zu tick %d)", tl, staff, tick.ticks());
return;
}
}
} else {
n = findBracket(tl);
if (n >= 0) {
brackets[n] = nullptr;
} else {
n = findBracket(nullptr);
if (n >= 0) {
brackets[n] = tl;
} else {
LOGD("too many overlapping textlines (tl %p staff %zu tick %d)", tl, staff, tick.ticks());
return;
}
}
}
QString rest;
QPointF p;
QString lineEnd = "none";
QString type;
bool hook = false;
double hookHeight = 0.0;
if (tl->tick() == tick) {
if (!isDashes) {
QString lineType;
switch (tl->lineStyle()) {
case LineType::SOLID:
lineType = "solid";
break;
case LineType::DASHED:
lineType = "dashed";
break;
case LineType::DOTTED:
lineType = "dotted";
break;
}
rest += QString(" line-type=\"%1\"").arg(lineType);
}
hook = tl->beginHookType() != HookType::NONE;
hookHeight = tl->beginHookHeight().val();
if (!tl->segmentsEmpty()) {
p = tl->frontSegment()->offset().toQPointF();
}
// offs = tl->mxmlOff();
type = "start";
} else {
hook = tl->endHookType() != HookType::NONE;
hookHeight = tl->endHookHeight().val();
if (!tl->segmentsEmpty()) {
p = (toLineSegment(tl->backSegment()))->userOff2().toQPointF();
}
// offs = tl->mxmlOff2();
type = "stop";
}
if (hook) {
if (hookHeight < 0.0) {
lineEnd = "up";
hookHeight *= -1.0;
} else {
lineEnd = "down";
}
rest += QString(" end-length=\"%1\"").arg(hookHeight * 10);
}
rest += positioningAttributes(tl, tl->tick() == tick);
directionTag(_xml, _attr, tl);
if (!tl->beginText().isEmpty() && tl->tick() == tick) {
_xml.startElement("direction-type");
_xml.tag("words", tl->beginText());
_xml.endElement();
}
_xml.startElement("direction-type");
if (isDashes) {
_xml.tag("dashes", { { "type", type }, { "number", n + 1 } });
} else {
_xml.tagRaw(QString("bracket type=\"%1\" number=\"%2\" line-end=\"%3\"%4").arg(type, QString::number(n + 1), lineEnd, rest));
}
_xml.endElement();
if (!tl->endText().isEmpty() && tl->tick() != tick) {
_xml.startElement("direction-type");
_xml.tag("words", tl->endText());
_xml.endElement();
}
/*
if (offs)
xml.tag("offset", offs);
*/
directionETag(_xml, staff);
}
//---------------------------------------------------------
// dynamic
//---------------------------------------------------------
// In MuseScore dynamics are essentially user-defined texts, therefore the ones
// supported by MusicXML need to be filtered out. Everything not recognized
// as MusicXML dynamics is written as other-dynamics.
void ExportMusicXml::dynamic(Dynamic const* const dyn, staff_idx_t staff)
{
QSet<QString> set; // the valid MusicXML dynamics
set << "f" << "ff" << "fff" << "ffff" << "fffff" << "ffffff"
<< "fp" << "fz"
<< "mf" << "mp"
<< "p" << "pp" << "ppp" << "pppp" << "ppppp" << "pppppp"
<< "rf" << "rfz"
<< "sf" << "sffz" << "sfp" << "sfpp" << "sfz";
directionTag(_xml, _attr, dyn);
_xml.startElement("direction-type");
QString tagName = "dynamics";
tagName += positioningAttributes(dyn);
_xml.startElementRaw(tagName);
const QString dynTypeName = TConv::toXml(dyn->dynamicType()).ascii();
if (set.contains(dynTypeName)) {
_xml.tagRaw(dynTypeName);
} else if (dynTypeName != "") {
std::map<ushort, QChar> map;
map[0xE520] = 'p';
map[0xE521] = 'm';
map[0xE522] = 'f';
map[0xE523] = 'r';
map[0xE524] = 's';
map[0xE525] = 'z';
map[0xE526] = 'n';
QString dynText = dynTypeName;
if (dyn->dynamicType() == DynamicType::OTHER) {
dynText = dyn->plainText();
}
// collect consecutive runs of either dynamics glyphs
// or other characters and write the runs.
QString text;
bool inDynamicsSym = false;
for (const auto& ch : qAsConst(dynText)) {
const auto it = map.find(ch.unicode());
if (it != map.end()) {
// found a SMUFL single letter dynamics glyph
if (!inDynamicsSym) {
if (text != "") {
_xml.tag("other-dynamics", text);
text = "";
}
inDynamicsSym = true;
}
text += it->second;
} else {
// found a non-dynamics character
if (inDynamicsSym) {
if (text != "") {
if (set.contains(text)) {
_xml.tagRaw(text);
} else {
_xml.tag("other-dynamics", text);
}
text = "";
}
inDynamicsSym = false;
}
text += ch;
}
}
if (text != "") {
if (inDynamicsSym && set.contains(text)) {
_xml.tagRaw(text);
} else {
_xml.tag("other-dynamics", text);
}
}
}
_xml.endElement();
_xml.endElement();
const auto offset = calculateTimeDeltaInDivisions(dyn->tick(), tick(), div);
if (offset) {
_xml.tag("offset", offset);
}
if (staff) {
_xml.tag("staff", static_cast<int>(staff));
}
if (dyn->velocity() > 0) {
_xml.tagRaw(QString("sound dynamics=\"%1\"").arg(QString::number(dyn->velocity() * 100.0 / 90.0, 'f', 2)));
}
_xml.endElement();
}
//---------------------------------------------------------
// symbol
//---------------------------------------------------------
// TODO: remove dependency on symbol name and replace by a more stable interface
// changes in sym.cpp r2494 broke MusicXML export of pedals (again)
void ExportMusicXml::symbol(Symbol const* const sym, staff_idx_t staff)
{
AsciiStringView name = SymNames::nameForSymId(sym->sym());
QString mxmlName = "";
if (name == "keyboardPedalPed") {
mxmlName = "pedal type=\"start\"";
} else if (name == "keyboardPedalUp") {
mxmlName = "pedal type=\"stop\"";
} else {
mxmlName = QString("other-direction smufl=\"%1\"").arg(name.ascii());
}
directionTag(_xml, _attr, sym);
mxmlName += positioningAttributes(sym);
_xml.startElement("direction-type");
_xml.tagRaw(mxmlName);
_xml.endElement();
const auto offset = calculateTimeDeltaInDivisions(sym->tick(), tick(), div);
if (offset) {
_xml.tag("offset", offset);
}
directionETag(_xml, staff);
}
//---------------------------------------------------------
// lyrics
//---------------------------------------------------------
void ExportMusicXml::lyrics(const std::vector<Lyrics*>& ll, const track_idx_t trk)
{
for (const Lyrics* l : ll) {
if (l && !l->xmlText().isEmpty()) {
if ((l)->track() == trk) {
QString lyricXml = QString("lyric number=\"%1\"").arg((l)->no() + 1);
lyricXml += color2xml(l);
lyricXml += positioningAttributes(l);
_xml.startElementRaw(lyricXml);
Lyrics::Syllabic syl = (l)->syllabic();
QString s = "";
switch (syl) {
case Lyrics::Syllabic::SINGLE: s = "single";
break;
case Lyrics::Syllabic::BEGIN: s = "begin";
break;
case Lyrics::Syllabic::END: s = "end";
break;
case Lyrics::Syllabic::MIDDLE: s = "middle";
break;
default:
LOGD("unknown syllabic %d", int(syl));
}
_xml.tag("syllabic", s);
QString attr; // TODO TBD
// set the default words format
const QString mtf = _score->styleSt(Sid::MusicalTextFont);
CharFormat defFmt;
defFmt.setFontFamily(_score->styleSt(Sid::lyricsEvenFontFace));
defFmt.setFontSize(_score->styleD(Sid::lyricsOddFontSize));
// write formatted
MScoreTextToMXML mttm("text", attr, defFmt, mtf);
mttm.writeTextFragments(l->fragmentList(), _xml);
if (l->ticks().isNotZero()) {
_xml.tag("extend");
}
_xml.endElement();
}
}
}
}
//---------------------------------------------------------
// directionJump -- write jump
//---------------------------------------------------------
// LVIFIX: TODO coda and segno should be numbered uniquely
static void directionJump(XmlWriter& xml, const Jump* const jp)
{
JumpType jtp = jp->jumpType();
String words;
String type;
String sound;
bool isDaCapo = false;
bool isDalSegno = false;
if (jtp == JumpType::DC) {
if (jp->xmlText() == "") {
words = u"D.C.";
} else {
words = jp->xmlText();
}
isDaCapo = true;
} else if (jtp == JumpType::DC_AL_FINE) {
if (jp->xmlText() == "") {
words = u"D.C. al Fine";
} else {
words = jp->xmlText();
}
isDaCapo = true;
} else if (jtp == JumpType::DC_AL_CODA) {
if (jp->xmlText() == "") {
words = u"D.C. al Coda";
} else {
words = jp->xmlText();
}
isDaCapo = true;
} else if (jtp == JumpType::DS_AL_CODA) {
if (jp->xmlText() == "") {
words = u"D.S. al Coda";
} else {
words = jp->xmlText();
}
isDalSegno = true;
} else if (jtp == JumpType::DS_AL_FINE) {
if (jp->xmlText() == "") {
words = u"D.S. al Fine";
} else {
words = jp->xmlText();
}
isDalSegno = true;
} else if (jtp == JumpType::DS) {
words = u"D.S.";
isDalSegno = true;
} else {
words = jp->xmlText();
if (jp->jumpTo() == "start") {
isDaCapo = true;
} else {
isDalSegno = true;
}
}
if (isDaCapo) {
sound = u"dacapo=\"yes\"";
} else if (isDalSegno) {
if (jp->jumpTo() == "") {
sound = u"dalsegno=\"1\"";
} else {
sound = u"dalsegno=\"" + jp->jumpTo() + u"\"";
}
}
if (sound != "") {
xml.startElement("direction", { { "placement", (jp->placement() == PlacementV::BELOW) ? "below" : "above" } });
xml.startElement("direction-type");
String positioning = ExportMusicXml::positioningAttributes(jp);
if (type != "") {
xml.tagRaw(type + positioning);
}
if (words != "") {
xml.tagRaw(u"words" + positioning, words);
}
xml.endElement();
if (sound != "") {
xml.tagRaw(u"sound " + sound);
}
xml.endElement();
}
}
//---------------------------------------------------------
// getEffectiveMarkerType
//---------------------------------------------------------
static MarkerType getEffectiveMarkerType(const Marker* const m, const std::vector<const Jump*>& jumps)
{
MarkerType mtp = m->markerType();
if (mtp != MarkerType::USER) {
return mtp;
}
// Try to guess marker type from its usage in jumps.
const QString label = m->label();
for (const Jump* j : jumps) {
MarkerType guessedMarkerType = mtp;
if (j->jumpTo() == label) {
guessedMarkerType = MarkerType::SEGNO;
} else if (j->playUntil() == label) {
guessedMarkerType = j->continueAt().isEmpty() ? MarkerType::FINE : MarkerType::TOCODA;
} else if (j->continueAt() == label) {
guessedMarkerType = MarkerType::CODA;
}
if (guessedMarkerType != mtp) {
if (mtp != MarkerType::USER) {
// Type guesses differ for different jump elements.
LOGD("Cannot guess type for marker with label=\"%s\"", qPrintable(label));
return MarkerType::USER;
}
mtp = guessedMarkerType;
}
}
return mtp;
}
//---------------------------------------------------------
// findCodaLabel
//---------------------------------------------------------
static QString findCodaLabel(const std::vector<const Jump*>& jumps, const QString& toCodaLabel)
{
for (const Jump* j : jumps) {
if (j->playUntil() == toCodaLabel) {
return j->continueAt();
}
}
return QString();
}
//---------------------------------------------------------
// directionMarker -- write marker
//---------------------------------------------------------
static void directionMarker(XmlWriter& xml, const Marker* const m, const std::vector<const Jump*>& jumps)
{
const MarkerType mtp = getEffectiveMarkerType(m, jumps);
String words;
String type;
String sound;
switch (mtp) {
case MarkerType::CODA:
case MarkerType::VARCODA:
case MarkerType::CODETTA:
type = u"coda";
if (m->label() == "") {
sound = u"coda=\"1\"";
} else {
sound = u"coda=\"" + m->label() + u"\"";
}
break;
case MarkerType::SEGNO:
case MarkerType::VARSEGNO:
type = u"segno";
if (m->label() == "") {
sound = "segno=\"1\"";
} else {
sound = u"segno=\"" + m->label() + u"\"";
}
break;
case MarkerType::FINE:
words = u"Fine";
sound = u"fine=\"yes\"";
break;
case MarkerType::TOCODA:
case MarkerType::TOCODASYM: {
if (m->xmlText() == "") {
words = "To Coda";
} else {
words = m->xmlText();
}
const String codaLabel = findCodaLabel(jumps, m->label());
if (codaLabel == "") {
sound = u"tocoda=\"1\"";
} else {
sound = u"tocoda=\"" + codaLabel + u"\"";
}
break;
}
case MarkerType::USER:
LOGD("marker type=%d not implemented", int(mtp));
break;
}
if (sound != u"") {
xml.startElement("direction", { { "placement", (m->placement() == PlacementV::BELOW) ? "below" : "above" } });
xml.startElement("direction-type");
String positioning = ExportMusicXml::positioningAttributes(m);
if (type != u"") {
xml.tagRaw(type + positioning);
}
if (words != u"") {
xml.tagRaw(u"words" + positioning, words);
}
xml.endElement();
if (sound != u"") {
xml.tagRaw(String("sound ") + sound);
}
xml.endElement();
}
}
//---------------------------------------------------------
// findTrackForAnnotations
//---------------------------------------------------------
// An annotation is attached to the staff, with track set
// to the lowest track in the staff. Find a track for it
// (the lowest track in this staff that has a chord or rest)
static track_idx_t findTrackForAnnotations(track_idx_t track, Segment* seg)
{
if (seg->segmentType() != SegmentType::ChordRest) {
return mu::nidx;
}
staff_idx_t staff = track / VOICES;
track_idx_t strack = staff * VOICES; // start track of staff containing track
track_idx_t etrack = strack + VOICES; // end track of staff containing track + 1
for (track_idx_t i = strack; i < etrack; i++) {
if (seg->element(i)) {
return i;
}
}
return mu::nidx;
}
//---------------------------------------------------------
// repeatAtMeasureStart -- write repeats at begin of measure
//---------------------------------------------------------
void ExportMusicXml::repeatAtMeasureStart(Attributes& attr, const Measure* const m, track_idx_t strack, track_idx_t etrack,
track_idx_t track)
{
// loop over all segments
for (EngravingItem* e : m->el()) {
track_idx_t wtrack = mu::nidx; // track to write jump
if (strack <= e->track() && e->track() < etrack) {
wtrack = findTrackForAnnotations(e->track(), m->first(SegmentType::ChordRest));
}
if (track != wtrack) {
continue;
}
switch (e->type()) {
case ElementType::MARKER:
{
// filter out the markers at measure Start
const Marker* const mk = toMarker(e);
const MarkerType mtp = getEffectiveMarkerType(mk, _jumpElements);
switch (mtp) {
case MarkerType::SEGNO:
case MarkerType::VARSEGNO:
case MarkerType::CODA:
case MarkerType::VARCODA:
case MarkerType::CODETTA:
LOGD(" -> handled");
attr.doAttr(_xml, false);
directionMarker(_xml, mk, _jumpElements);
break;
case MarkerType::FINE:
case MarkerType::TOCODA:
case MarkerType::TOCODASYM:
// ignore
break;
case MarkerType::USER:
LOGD("repeatAtMeasureStart: marker %d not implemented", int(mtp));
break;
}
}
break;
default:
LOGD("repeatAtMeasureStart: direction type %s at tick %d not implemented",
e->typeName(), m->tick().ticks());
break;
}
}
}
//---------------------------------------------------------
// repeatAtMeasureStop -- write repeats at end of measure
//---------------------------------------------------------
void ExportMusicXml::repeatAtMeasureStop(const Measure* const m, track_idx_t strack, track_idx_t etrack, track_idx_t track)
{
for (EngravingItem* e : m->el()) {
track_idx_t wtrack = mu::nidx; // track to write jump
if (strack <= e->track() && e->track() < etrack) {
wtrack = findTrackForAnnotations(e->track(), m->first(SegmentType::ChordRest));
}
if (track != wtrack) {
continue;
}
switch (e->type()) {
case ElementType::MARKER:
{
// filter out the markers at measure stop
const Marker* const mk = toMarker(e);
const MarkerType mtp = getEffectiveMarkerType(mk, _jumpElements);
switch (mtp) {
case MarkerType::FINE:
case MarkerType::TOCODA:
case MarkerType::TOCODASYM:
directionMarker(_xml, mk, _jumpElements);
break;
case MarkerType::SEGNO:
case MarkerType::VARSEGNO:
case MarkerType::CODA:
case MarkerType::VARCODA:
case MarkerType::CODETTA:
// ignore
break;
case MarkerType::USER:
LOGD("repeatAtMeasureStop: marker %d not implemented", int(mtp));
break;
}
}
break;
case ElementType::JUMP:
directionJump(_xml, toJump(e));
break;
default:
LOGD("repeatAtMeasureStop: direction type %s at tick %d not implemented",
e->typeName(), m->tick().ticks());
break;
}
}
}
//---------------------------------------------------------
// work -- write the <work> element
// note that order must be work-number, work-title
// also write <movement-number> and <movement-title>
// data is taken from the score metadata instead of the Text elements
//---------------------------------------------------------
void ExportMusicXml::work(const MeasureBase* /*measure*/)
{
QString workTitle = _score->metaTag(u"workTitle");
QString workNumber = _score->metaTag(u"workNumber");
if (!(workTitle.isEmpty() && workNumber.isEmpty())) {
_xml.startElement("work");
if (!workNumber.isEmpty()) {
_xml.tag("work-number", workNumber);
}
if (!workTitle.isEmpty()) {
_xml.tag("work-title", workTitle);
}
_xml.endElement();
}
if (!_score->metaTag(u"movementNumber").isEmpty()) {
_xml.tag("movement-number", _score->metaTag(u"movementNumber"));
}
if (!_score->metaTag(u"movementTitle").isEmpty()) {
_xml.tag("movement-title", _score->metaTag(u"movementTitle"));
}
}
//---------------------------------------------------------
// measureRepeat -- write measure-repeat
//---------------------------------------------------------
static void measureRepeat(XmlWriter& xml, Attributes& attr, const Measure* const m, const int partIndex)
{
Part* part = m->score()->parts().at(partIndex);
const staff_idx_t scoreRelStaff = m->score()->staffIdx(part);
for (size_t i = 0; i < part->nstaves(); ++i) {
staff_idx_t staffIdx = scoreRelStaff + i;
if (m->isMeasureRepeatGroup(staffIdx)
&& (!m->prevMeasure() || !m->prevMeasure()->isMeasureRepeatGroup(staffIdx)
|| (m->measureRepeatNumMeasures(staffIdx) != m->prevMeasure()->measureRepeatNumMeasures(staffIdx)))) {
attr.doAttr(xml, true);
xml.startElement("measure-style", { { "number", i + 1 } });
int numMeasures = m->measureRepeatNumMeasures(staffIdx);
if (numMeasures > 1) {
// slashes == numMeasures for everything MuseScore currently supports
xml.tag("measure-repeat", { { "slashes", numMeasures }, { "type", "start" } }, numMeasures);
} else {
// no need to include slashes
xml.tag("measure-repeat", { { "type", "start" } }, numMeasures);
}
xml.endElement();
} else if (
// no longer in measure repeats
m->prevMeasure() && ((m->prevMeasure()->isMeasureRepeatGroup(staffIdx) && !m->isMeasureRepeatGroup(staffIdx))
// or still in measure repeats, but now of different duration
|| (m->prevMeasure()->measureRepeatElement(staffIdx) && m->measureRepeatElement(staffIdx)
&& (m->measureRepeatNumMeasures(staffIdx) != m->prevMeasure()->measureRepeatNumMeasures(staffIdx))))) {
attr.doAttr(xml, true);
xml.startElement("measure-style", { { "number", i + 1 } });
xml.tag("measure-repeat", { { "type", "stop" } }, "");
xml.endElement();
}
}
}
//---------------------------------------------------------
// measureStyle -- write measure-style
//---------------------------------------------------------
// this is done at the first measure of a multimeasure rest
// note: for a normal measure, mmRest1 is the measure itself,
// for a multi-measure rest, it is the replacing measure
static void measureStyle(XmlWriter& xml, Attributes& attr, const Measure* const m, const int partIndex)
{
const Measure* mmR1 = m->mmRest1();
if (m != mmR1 && m == mmR1->mmRestFirst()) {
attr.doAttr(xml, true);
xml.startElement("measure-style");
xml.tag("multiple-rest", mmR1->mmRestCount());
xml.endElement();
} else {
// measure repeat can only possibly be present if mmrest was not
measureRepeat(xml, attr, m, partIndex);
}
}
//---------------------------------------------------------
// findFretDiagram
//---------------------------------------------------------
static const FretDiagram* findFretDiagram(track_idx_t strack, track_idx_t etrack, track_idx_t track, Segment* seg)
{
if (seg->segmentType() == SegmentType::ChordRest) {
for (const EngravingItem* e : seg->annotations()) {
track_idx_t wtrack = mu::nidx; // track to write annotation
if (strack <= e->track() && e->track() < etrack) {
wtrack = findTrackForAnnotations(e->track(), seg);
}
if (track == wtrack && e->type() == ElementType::FRET_DIAGRAM) {
return static_cast<const FretDiagram*>(e);
}
}
}
return 0;
}
//---------------------------------------------------------
// commonAnnotations
//---------------------------------------------------------
static bool commonAnnotations(ExportMusicXml* exp, const EngravingItem* e, staff_idx_t sstaff)
{
bool instrChangeHandled = false;
// note: write the changed instrument details (transposition) here,
// optionally writing the associated staff text is done below
if (e->isInstrumentChange()) {
const auto instrChange = toInstrumentChange(e);
exp->writeInstrumentDetails(instrChange->instrument());
instrChangeHandled = true;
}
if (e->isSymbol()) {
exp->symbol(toSymbol(e), sstaff);
} else if (e->isTempoText()) {
exp->tempoText(toTempoText(e), sstaff);
} else if (e->isPlayTechAnnotation() || e->isStaffText() || e->isSystemText() || e->isTripletFeel() || e->isText()
|| (e->isInstrumentChange() && e->visible())) {
exp->words(toTextBase(e), sstaff);
} else if (e->isDynamic()) {
exp->dynamic(toDynamic(e), sstaff);
} else if (e->isRehearsalMark()) {
exp->rehearsal(toRehearsalMark(e), sstaff);
} else {
return instrChangeHandled;
}
return true;
}
//---------------------------------------------------------
// annotations
//---------------------------------------------------------
/*
* Write annotations that are attached to chords or rests
*/
// In MuseScore, EngravingItem::FRET_DIAGRAM and EngravingItem::HARMONY are separate annotations,
// in MusicXML they are combined in the harmony element. This means they have to be matched.
// TODO: replace/repair current algorithm (which can only handle one FRET_DIAGRAM and one HARMONY)
static void annotations(ExportMusicXml* exp, track_idx_t strack, track_idx_t etrack, track_idx_t track, staff_idx_t sstaff, Segment* seg)
{
if (seg->segmentType() == SegmentType::ChordRest) {
const FretDiagram* fd = findFretDiagram(strack, etrack, track, seg);
// if (fd) LOGD("annotations seg %p found fretboard diagram %p", seg, fd);
for (const EngravingItem* e : seg->annotations()) {
if (!exp->canWrite(e)) {
continue;
}
track_idx_t wtrack = mu::nidx; // track to write annotation
if (strack <= e->track() && e->track() < etrack) {
wtrack = findTrackForAnnotations(e->track(), seg);
}
if (track == wtrack) {
if (commonAnnotations(exp, e, sstaff)) {
// already handled
} else if (e->isHarmony()) {
// LOGD("annotations seg %p found harmony %p", seg, e);
exp->harmony(toHarmony(e), fd);
fd = nullptr; // make sure to write only once ...
} else if (e->isFermata() || e->isFiguredBass() || e->isFretDiagram() || e->isJump()) {
// handled separately by chordAttributes(), figuredBass(), findFretDiagram() or ignored
} else {
LOGD("direction type %s at tick %d not implemented",
e->typeName(), seg->tick().ticks());
}
}
}
if (fd) {
// found fd but no harmony, cannot write (MusicXML would be invalid)
LOGD("seg %p found fretboard diagram %p w/o harmony: cannot write",
seg, fd);
}
}
}
//---------------------------------------------------------
// figuredBass
//---------------------------------------------------------
static void figuredBass(XmlWriter& xml, track_idx_t strack, track_idx_t etrack, track_idx_t track, const ChordRest* cr, FigBassMap& fbMap,
int divisions)
{
Segment* seg = cr->segment();
if (seg->segmentType() == SegmentType::ChordRest) {
for (const EngravingItem* e : seg->annotations()) {
track_idx_t wtrack = mu::nidx; // track to write annotation
if (strack <= e->track() && e->track() < etrack) {
wtrack = findTrackForAnnotations(e->track(), seg);
}
if (track == wtrack) {
if (e->type() == ElementType::FIGURED_BASS) {
const FiguredBass* fb = dynamic_cast<const FiguredBass*>(e);
//LOGD("figuredbass() track %d seg %p fb %p seg %p tick %d ticks %d cr %p tick %d ticks %d",
// track, seg, fb, fb->segment(), fb->segment()->tick(), fb->ticks(), cr, cr->tick(), cr->actualTicks());
bool extend = fb->ticks() > cr->actualTicks();
if (extend) {
//LOGD("figuredbass() extend to %d + %d = %d",
// cr->tick(), fb->ticks(), cr->tick() + fb->ticks());
fbMap.insert(strack, fb);
} else {
fbMap.remove(strack);
}
const Fraction crEndTick = cr->tick() + cr->actualTicks();
const Fraction fbEndTick = fb->segment()->tick() + fb->ticks();
const bool writeDuration = fb->ticks() < cr->actualTicks();
fb->writeMusicXML(xml, true, crEndTick.ticks(), fbEndTick.ticks(),
writeDuration, divisions);
// Check for changing figures under a single note (each figure stored in a separate segment)
for (Segment* segNext = seg->next(); segNext && segNext->element(track) == NULL; segNext = segNext->next()) {
for (EngravingItem* annot : segNext->annotations()) {
if (annot->type() == ElementType::FIGURED_BASS && annot->track() == track) {
fb = dynamic_cast<const FiguredBass*>(annot);
fb->writeMusicXML(xml, true, 0, 0, true, divisions);
}
}
}
// no extend can be pending
return;
}
}
}
// check for extend pending
if (fbMap.contains(strack)) {
const FiguredBass* fb = fbMap.value(strack);
Fraction crEndTick = cr->tick() + cr->actualTicks();
Fraction fbEndTick = fb->segment()->tick() + fb->ticks();
bool writeDuration = fb->ticks() < cr->actualTicks();
if (cr->tick() < fbEndTick) {
//LOGD("figuredbass() at tick %d extend only", cr->tick());
fb->writeMusicXML(xml, false, crEndTick.ticks(), fbEndTick.ticks(), writeDuration, divisions);
}
if (fbEndTick <= crEndTick) {
//LOGD("figuredbass() at tick %d extend done", cr->tick() + cr->actualTicks());
fbMap.remove(strack);
}
}
}
}
//---------------------------------------------------------
// spannerStart
//---------------------------------------------------------
// for each spanner start:
// find start track
// find stop track
// if stop track < start track
// get data from list of already stopped spanners
// else
// calculate data
// write start if in right track
static void spannerStart(ExportMusicXml* exp, track_idx_t strack, track_idx_t etrack, track_idx_t track, staff_idx_t sstaff, Segment* seg)
{
if (seg->segmentType() == SegmentType::ChordRest) {
Fraction stick = seg->tick();
for (auto it = exp->score()->spanner().lower_bound(stick.ticks()); it != exp->score()->spanner().upper_bound(stick.ticks()); ++it) {
Spanner* e = it->second;
if (!exp->canWrite(e)) {
continue;
}
track_idx_t wtrack = mu::nidx; // track to write spanner
if (strack <= e->track() && e->track() < etrack) {
wtrack = findTrackForAnnotations(e->track(), seg);
}
if (track == wtrack) {
switch (e->type()) {
case ElementType::HAIRPIN:
exp->hairpin(toHairpin(e), sstaff, seg->tick());
break;
case ElementType::OTTAVA:
exp->ottava(toOttava(e), sstaff, seg->tick());
break;
case ElementType::PEDAL:
exp->pedal(toPedal(e), sstaff, seg->tick());
break;
case ElementType::TEXTLINE:
exp->textLine(toTextLineBase(e), sstaff, seg->tick());
break;
case ElementType::LET_RING:
exp->textLine(toLetRing(e), sstaff, seg->tick());
break;
case ElementType::GRADUAL_TEMPO_CHANGE:
exp->textLine(toGradualTempoChange(e), sstaff, seg->tick());
break;
case ElementType::PALM_MUTE:
exp->textLine(toPalmMute(e), sstaff, seg->tick());
break;
case ElementType::WHAMMY_BAR:
exp->textLine(toWhammyBar(e), sstaff, seg->tick());
break;
case ElementType::RASGUEADO:
exp->textLine(toRasgueado(e), sstaff, seg->tick());
break;
case ElementType::HARMONIC_MARK:
exp->textLine(toHarmonicMark(e), sstaff, seg->tick());
break;
case ElementType::PICK_SCRAPE:
exp->textLine(toPickScrape(e), sstaff, seg->tick());
break;
case ElementType::TRILL:
// ignore (written as <note><notations><ornaments><wavy-line>)
break;
case ElementType::SLUR:
// ignore (written as <note><notations><slur>)
break;
default:
LOGD("spannerStart: direction type %d ('%s') at tick %d not implemented",
int(e->type()), e->typeName(), seg->tick().ticks());
break;
}
}
} // for
}
}
//---------------------------------------------------------
// spannerStop
//---------------------------------------------------------
// called after writing each chord or rest to check if a spanner must be stopped
// loop over all spanners and find spanners in strack ending at tick2
// note that more than one voice may contains notes ending at tick2,
// remember which spanners have already been stopped (the "stopped" set)
static void spannerStop(ExportMusicXml* exp, track_idx_t strack, track_idx_t etrack, const Fraction& tick2, staff_idx_t sstaff,
QSet<const Spanner*>& stopped)
{
for (auto it : exp->score()->spanner()) {
Spanner* e = it.second;
if (!exp->canWrite(e)) {
continue;
}
if (e->tick2() != tick2 || e->track() < strack || e->track() >= etrack) {
continue;
}
if (!stopped.contains(e)) {
stopped.insert(e);
switch (e->type()) {
case ElementType::HAIRPIN:
exp->hairpin(toHairpin(e), sstaff, Fraction(-1, 1));
break;
case ElementType::OTTAVA:
exp->ottava(toOttava(e), sstaff, Fraction(-1, 1));
break;
case ElementType::PEDAL:
exp->pedal(toPedal(e), sstaff, Fraction(-1, 1));
break;
case ElementType::TEXTLINE:
exp->textLine(toTextLineBase(e), sstaff, Fraction(-1, 1));
break;
case ElementType::LET_RING:
exp->textLine(toLetRing(e), sstaff, Fraction(-1, 1));
break;
case ElementType::PALM_MUTE:
exp->textLine(toPalmMute(e), sstaff, Fraction(-1, 1));
break;
case ElementType::WHAMMY_BAR:
exp->textLine(toWhammyBar(e), sstaff, Fraction(-1, 1));
break;
case ElementType::RASGUEADO:
exp->textLine(toRasgueado(e), sstaff, Fraction(-1, 1));
break;
case ElementType::HARMONIC_MARK:
exp->textLine(toHarmonicMark(e), sstaff, Fraction(-1, 1));
break;
case ElementType::PICK_SCRAPE:
exp->textLine(toPickScrape(e), sstaff, Fraction(-1, 1));
break;
case ElementType::TRILL:
// ignore (written as <note><notations><ornaments><wavy-line>
break;
case ElementType::SLUR:
// ignore (written as <note><notations><slur>)
break;
default:
LOGD("spannerStop: direction type %s at tick2 %d not implemented",
e->typeName(), tick2.ticks());
break;
}
}
} // for
}
//---------------------------------------------------------
// keysigTimesig
//---------------------------------------------------------
/**
Output attributes at start of measure: key, time
*/
void ExportMusicXml::keysigTimesig(const Measure* m, const Part* p)
{
track_idx_t strack = p->startTrack();
track_idx_t etrack = p->endTrack();
//LOGD("keysigTimesig m %p strack %d etrack %d", m, strack, etrack);
// search all staves for non-generated key signatures
std::map<staff_idx_t, KeySig*> keysigs; // map staff to key signature
for (Segment* seg = m->first(); seg; seg = seg->next()) {
if (seg->tick() > m->tick()) {
break;
}
for (track_idx_t t = strack; t < etrack; t += VOICES) {
EngravingItem* el = seg->element(t);
if (!el) {
continue;
}
if (el->type() == ElementType::KEYSIG) {
//LOGD(" found keysig %p track %d", el, el->track());
staff_idx_t st = (t - strack) / VOICES;
if (!el->generated()) {
keysigs[st] = static_cast<KeySig*>(el);
}
}
}
}
//ClefType ct = rest->staff()->clef(rest->tick());
// write the key signatues
if (!keysigs.empty()) {
// determine if all staves have a keysig and all keysigs are identical
// in that case a single <key> is written, without number=... attribute
size_t nstaves = p->nstaves();
bool singleKey = true;
// check if all staves have a keysig
for (staff_idx_t i = 0; i < nstaves; i++) {
if (!mu::contains(keysigs, i)) {
singleKey = false;
}
}
// check if all keysigs are identical
if (singleKey) {
for (staff_idx_t i = 1; i < nstaves; i++) {
if (!(keysigs.at(i)->key() == keysigs.at(0)->key())) {
singleKey = false;
}
}
}
// write the keysigs
//LOGD(" singleKey %d", singleKey);
if (singleKey) {
// keysig applies to all staves
keysig(keysigs.at(0), p->staff(0)->clef(m->tick()), 0, keysigs.at(0)->visible());
} else {
// staff-specific keysigs
for (staff_idx_t st : mu::keys(keysigs)) {
keysig(keysigs.at(st), p->staff(st)->clef(m->tick()), st + 1, keysigs.at(st)->visible());
}
}
} else {
// always write a keysig at tick = 0
if (m->tick().isZero()) {
//KeySigEvent kse;
//kse.setKey(Key::C);
KeySig* ks = Factory::createKeySig(_score->dummy()->segment());
ks->setKey(Key::C);
keysig(ks, p->staff(0)->clef(m->tick()));
delete ks;
}
}
TimeSig* tsig = 0;
for (Segment* seg = m->first(); seg; seg = seg->next()) {
if (seg->tick() > m->tick()) {
break;
}
EngravingItem* el = seg->element(strack);
if (el && el->type() == ElementType::TIMESIG) {
tsig = (TimeSig*)el;
}
}
if (tsig) {
timesig(tsig);
}
}
//---------------------------------------------------------
// identification -- write the identification
//---------------------------------------------------------
void ExportMusicXml::identification(XmlWriter& xml, Score const* const score)
{
xml.startElement("identification");
QStringList creators;
// the creator types commonly found in MusicXML
creators << "arranger" << "composer" << "lyricist" << "poet" << "translator";
for (const QString& type : qAsConst(creators)) {
QString creator = score->metaTag(type);
if (!creator.isEmpty()) {
xml.tag("creator", { { "type", type } }, creator);
}
}
if (!score->metaTag(u"copyright").isEmpty()) {
xml.tag("rights", score->metaTag(u"copyright"));
}
xml.startElement("encoding");
if (MScore::debugMode) {
xml.tag("software", QString("MuseScore 0.7.0"));
xml.tag("encoding-date", QString("2007-09-10"));
} else {
xml.tag("software", QString("MuseScore ") + QString(MUSESCORE_VERSION));
xml.tag("encoding-date", QDate::currentDate().toString(Qt::ISODate));
}
// specify supported elements
xml.tag("supports", { { "element", "accidental" }, { "type", "yes" } });
xml.tag("supports", { { "element", "beam" }, { "type", "yes" } });
// set support for print new-page and new-system to match user preference
// for MusicxmlExportBreaks::MANUAL support is "no" because "yes" breaks Finale NotePad import
IMusicXmlConfiguration::MusicxmlExportBreaksType breaksType = configuration()->musicxmlExportBreaksType();
if (configuration()->musicxmlExportLayout() && breaksType == IMusicXmlConfiguration::MusicxmlExportBreaksType::All) {
xml.tag("supports", { { "element", "print" }, { "attribute", "new-page" }, { "type", "yes" }, { "value", "yes" } });
xml.tag("supports", { { "element", "print" }, { "attribute", "new-system" }, { "type", "yes" }, { "value", "yes" } });
} else {
xml.tag("supports", { { "element", "print" }, { "attribute", "new-page" }, { "type", "no" } });
xml.tag("supports", { { "element", "print" }, { "attribute", "new-system" }, { "type", "no" } });
}
xml.tag("supports", { { "element", "stem" }, { "type", "yes" } });
xml.endElement();
if (!score->metaTag(u"source").isEmpty()) {
xml.tag("source", score->metaTag(u"source"));
}
xml.endElement();
}
//---------------------------------------------------------
// findPartGroupNumber
//---------------------------------------------------------
static int findPartGroupNumber(int* partGroupEnd)
{
// find part group number
for (int number = 0; number < MAX_PART_GROUPS; ++number) {
if (partGroupEnd[number] == -1) {
return number;
}
}
LOGD("no free part group number");
return MAX_PART_GROUPS;
}
//---------------------------------------------------------
// scoreInstrument
//---------------------------------------------------------
static void scoreInstrument(XmlWriter& xml, const int partNr, const int instrNr, const QString& instrName)
{
xml.startElementRaw(QString("score-instrument %1").arg(instrId(partNr, instrNr)));
xml.tag("instrument-name", instrName);
xml.endElement();
}
//---------------------------------------------------------
// midiInstrument
//---------------------------------------------------------
static void midiInstrument(XmlWriter& xml, const int partNr, const int instrNr,
const Instrument* instr, const Score* score, const int unpitched = 0)
{
xml.startElementRaw(QString("midi-instrument %1").arg(instrId(partNr, instrNr)));
int midiChannel = score->masterScore()->midiChannel(instr->channel(0)->channel());
if (midiChannel >= 0 && midiChannel < 16) {
xml.tag("midi-channel", midiChannel + 1);
}
int midiProgram = instr->channel(0)->program();
if (midiProgram >= 0 && midiProgram < 128) {
xml.tag("midi-program", midiProgram + 1);
}
if (unpitched > 0) {
xml.tag("midi-unpitched", unpitched);
}
xml.tag("volume", (instr->channel(0)->volume() / 127.0) * 100); //percent
xml.tag("pan", int(((instr->channel(0)->pan() - 63.5) / 63.5) * 90)); //-90 hard left, +90 hard right xml.etag();
xml.endElement();
}
//---------------------------------------------------------
// initInstrMap
//---------------------------------------------------------
/**
Initialize the Instrument* to number map for a Part
Used to generate instrument numbers for a multi-instrument part
*/
static void initInstrMap(MxmlInstrumentMap& im, const InstrumentList& il, const Score* /*score*/)
{
im.clear();
for (const auto& pair : il) {
const Instrument* instr = pair.second;
if (!im.contains(instr)) {
im.insert(instr, im.size());
}
}
}
//---------------------------------------------------------
// initReverseInstrMap
//---------------------------------------------------------
typedef QMap<int, const Instrument*> MxmlReverseInstrumentMap;
/**
Initialize the number t Instrument* map for a Part
Used to iterate in sequence over instrument numbers for a multi-instrument part
*/
static void initReverseInstrMap(MxmlReverseInstrumentMap& rim, const MxmlInstrumentMap& im)
{
rim.clear();
for (const Instrument* i : im.keys()) {
int instNr = im.value(i);
rim.insert(instNr, i);
}
}
//---------------------------------------------------------
// hasPageBreak
//---------------------------------------------------------
static MeasureBase* lastMeasureBase(const System* const system)
{
MeasureBase* mb = nullptr;
if (system) {
const auto& measures = system->measures();
IF_ASSERT_FAILED(!(measures.empty())) {
return nullptr;
}
mb = measures.back();
}
return mb;
}
//---------------------------------------------------------
// hasPageBreak
//---------------------------------------------------------
static bool hasPageBreak(const System* const system)
{
const MeasureBase* mb = nullptr;
if (system) {
const auto& measures = system->measures();
IF_ASSERT_FAILED(!(measures.empty())) {
return false;
}
mb = measures.back();
}
return mb && mb->pageBreak();
}
//---------------------------------------------------------
// print
//---------------------------------------------------------
/**
Handle the <print> element.
When exporting layout and all breaks, a <print> with layout information
is generated for the first measure in the score, in a system or on a page.
When exporting layout but only manual or no breaks, a <print> with
layout information is generated only for the first measure in the score,
as it is assumed the system layout is broken by the importing application
anyway and is thus useless.
a page break is explicit (manual) if:
- the last system on the previous page has a page break
a system break is explicit (manual) if:
- the previous system in the score has a system or layout break
- if the previous system in the score does not have measures
(i.e. only has (a) frame(s))
*/
void ExportMusicXml::print(const Measure* const m, const int partNr, const int firstStaffOfPart,
const int nrStavesInPart, const MeasurePrintContext& mpc)
{
const MeasureBase* const prevSysMB = lastMeasureBase(mpc.prevSystem);
const bool prevMeasLineBreak = prevSysMB ? prevSysMB->lineBreak() : false;
const bool prevMeasSectionBreak = prevSysMB ? prevSysMB->sectionBreak() : false;
const bool prevPageBreak = hasPageBreak(mpc.lastSystemPrevPage);
QString newSystemOrPage; // new-[system|page]="yes" or empty
if (!mpc.scoreStart) {
IMusicXmlConfiguration::MusicxmlExportBreaksType exportBreaksType = configuration()->musicxmlExportBreaksType();
if (exportBreaksType == IMusicXmlConfiguration::MusicxmlExportBreaksType::All) {
if (mpc.pageStart) {
newSystemOrPage = " new-page=\"yes\"";
} else if (mpc.systemStart) {
newSystemOrPage = " new-system=\"yes\"";
}
} else if (exportBreaksType == IMusicXmlConfiguration::MusicxmlExportBreaksType::Manual) {
if (mpc.pageStart && prevPageBreak) {
newSystemOrPage = " new-page=\"yes\"";
} else if (mpc.systemStart && (prevMeasLineBreak || prevMeasSectionBreak)) {
newSystemOrPage = " new-system=\"yes\"";
}
}
}
bool doBreak = mpc.scoreStart || (newSystemOrPage != "");
bool doLayout = configuration()->musicxmlExportLayout();
if (doBreak) {
if (doLayout) {
_xml.startElementRaw(QString("print%1").arg(newSystemOrPage));
const double pageWidth = getTenthsFromInches(score()->styleD(Sid::pageWidth));
const double lm = getTenthsFromInches(score()->styleD(Sid::pageOddLeftMargin));
const double rm = getTenthsFromInches(score()->styleD(Sid::pageWidth)
- score()->styleD(Sid::pagePrintableWidth)
- score()->styleD(Sid::pageOddLeftMargin));
const double tm = getTenthsFromInches(score()->styleD(Sid::pageOddTopMargin));
// System Layout
// For a multi-measure rest positioning is valid only
// in the replacing measure
// note: for a normal measure, mmRest1 is the measure itself,
// for a multi-measure rest, it is the replacing measure
const Measure* mmR1 = m->mmRest1();
const System* system = mmR1->system();
// Put the system print suggestions only for the first part in a score...
if (partNr == 0) {
// Find the right margin of the system.
double systemLM = getTenthsFromDots(mmR1->pagePos().x() - system->page()->pagePos().x()) - lm;
double systemRM = pageWidth - rm - (getTenthsFromDots(system->bbox().width()) + lm);
_xml.startElement("system-layout");
_xml.startElement("system-margins");
_xml.tag("left-margin", QString("%1").arg(QString::number(systemLM, 'f', 2)));
_xml.tag("right-margin", QString("%1").arg(QString::number(systemRM, 'f', 2)));
_xml.endElement();
if (mpc.systemStart && !mpc.pageStart) {
// see System::layout2() for the factor 2 * score()->spatium()
const double sysDist = getTenthsFromDots(mmR1->pagePos().y()
- mpc.prevMeasure->pagePos().y()
- mpc.prevMeasure->bbox().height()
+ 2 * score()->spatium()
);
_xml.tag("system-distance", QString("%1").arg(QString::number(sysDist, 'f', 2)));
}
if (mpc.pageStart || mpc.scoreStart) {
const double topSysDist = getTenthsFromDots(mmR1->pagePos().y()) - tm;
_xml.tag("top-system-distance", QString("%1").arg(QString::number(topSysDist, 'f', 2)));
}
_xml.endElement();
}
// Staff layout elements.
for (int staffIdx = (firstStaffOfPart == 0) ? 1 : 0; staffIdx < nrStavesInPart; staffIdx++) {
// calculate distance between this and previous staff using the bounding boxes
const auto staffNr = firstStaffOfPart + staffIdx;
const auto prevBbox = system->staff(staffNr - 1)->bbox();
const auto staffDist = system->staff(staffNr)->bbox().y() - prevBbox.y() - prevBbox.height();
_xml.startElement("staff-layout", { { "number", staffIdx + 1 } });
_xml.tag("staff-distance", QString("%1").arg(QString::number(getTenthsFromDots(staffDist), 'f', 2)));
_xml.endElement();
}
_xml.endElement();
} else if (newSystemOrPage != "") {
_xml.tagRaw(QString("print%1").arg(newSystemOrPage));
}
}
}
//---------------------------------------------------------
// exportDefaultClef
//---------------------------------------------------------
/**
In case no clef is found, export a default clef with type determined by staff type.
Note that a multi-measure rest starting in the first measure should be handled correctly.
*/
void ExportMusicXml::exportDefaultClef(const Part* const part, const Measure* const m)
{
const auto staves = part->nstaves();
if (m->tick() == Fraction(0, 1)) {
const auto clefSeg = m->findSegment(SegmentType::HeaderClef, Fraction(0, 1));
if (clefSeg) {
for (size_t i = 0; i < staves; ++i) {
// sstaff - xml staff number, counting from 1 for this
// instrument
// special number 0 -> dont show staff number in
// xml output (because there is only one staff)
auto sstaff = (staves > 1) ? i + 1 : 0;
auto track = part->startTrack() + VOICES * i;
if (clefSeg->element(track) == nullptr) {
ClefType ct { ClefType::G };
QString stafftype;
switch (part->staff(i)->staffType(Fraction(0, 1))->group()) {
case StaffGroup::TAB:
ct = ClefType::TAB;
stafftype = "tab";
break;
case StaffGroup::STANDARD:
ct = ClefType::G;
stafftype = "std";
break;
case StaffGroup::PERCUSSION:
ct = ClefType::PERC;
stafftype = "perc";
break;
}
LOGD("no clef found in first measure track %zu (stafftype %s)", track, qPrintable(stafftype));
clef(sstaff, ct, " print-object=\"no\"");
}
}
}
}
}
//---------------------------------------------------------
// findAndExportClef
//---------------------------------------------------------
/**
Make sure clefs at end of measure get exported at start of next measure.
*/
void ExportMusicXml::findAndExportClef(const Measure* const m, const int staves, const track_idx_t strack, const track_idx_t etrack)
{
Measure* prevMeasure = m->prevMeasure();
Measure* mmR = m->mmRest(); // the replacing measure in a multi-measure rest
Fraction tick = m->tick();
Segment* cs1;
Segment* cs2 = m->findSegment(SegmentType::Clef, tick);
Segment* cs3;
Segment* seg = 0;
if (prevMeasure) {
cs1 = prevMeasure->findSegment(SegmentType::Clef, tick);
} else {
cs1 = m->findSegment(SegmentType::HeaderClef, tick);
}
if (mmR) {
cs3 = mmR->findSegment(SegmentType::HeaderClef, tick);
if (!cs3) {
cs3 = mmR->findSegment(SegmentType::Clef, tick);
}
} else {
cs3 = 0;
}
if (cs1 && cs2) {
// should only happen at begin of new system
// when previous system ends with a non-generated clef
seg = cs1;
} else if (cs1) {
seg = cs1;
} else if (cs3) {
// happens when the first measure is a multi-measure rest
// containing a generated clef
seg = cs3;
} else {
seg = cs2;
}
clefDebug("exportxml: clef segments cs1=%p cs2=%p cs3=%p seg=%p", cs1, cs2, cs3, seg);
// output attribute at start of measure: clef
if (seg) {
for (track_idx_t st = strack; st < etrack; st += VOICES) {
// sstaff - xml staff number, counting from 1 for this
// instrument
// special number 0 -> dont show staff number in
// xml output (because there is only one staff)
staff_idx_t sstaff = (staves > 1) ? st - strack + VOICES : 0;
sstaff /= VOICES;
Clef* cle = static_cast<Clef*>(seg->element(st));
if (cle) {
clefDebug("exportxml: clef at start measure ti=%d ct=%d gen=%d", tick, int(cle->clefType()), cle->generated());
// output only clef changes, not generated clefs at line beginning
// exception: at tick=0, export clef anyway
if ((tick.isZero() || !cle->generated())
&& ((seg->measure() != m) || ((seg->segmentType() == SegmentType::HeaderClef) && !cle->otherClef()))) {
clefDebug("exportxml: clef exported");
clef(sstaff, cle->clefType(), color2xml(cle));
} else {
clefDebug("exportxml: clef not exported");
}
}
}
}
}
//---------------------------------------------------------
// findPitchesUsed
//---------------------------------------------------------
/**
Find the set of pitches actually used in a part.
*/
typedef QSet<int> pitchSet; // the set of pitches used
static void addChordPitchesToSet(const Chord* c, pitchSet& set)
{
for (const Note* note : c->notes()) {
LOGD("chord %p note %p pitch %d", c, note, note->pitch() + 1);
set.insert(note->pitch());
}
}
static void findPitchesUsed(const Part* part, pitchSet& set)
{
track_idx_t strack = part->startTrack();
track_idx_t etrack = part->endTrack();
// loop over all chords in the part
for (const MeasureBase* mb = part->score()->measures()->first(); mb; mb = mb->next()) {
if (mb->type() != ElementType::MEASURE) {
continue;
}
const Measure* m = static_cast<const Measure*>(mb);
for (track_idx_t st = strack; st < etrack; ++st) {
for (Segment* seg = m->first(); seg; seg = seg->next()) {
const EngravingItem* el = seg->element(st);
if (!el) {
continue;
}
if (el->type() == ElementType::CHORD) {
// add grace and non-grace note pitches to the result set
const Chord* c = static_cast<const Chord*>(el);
if (c) {
for (const Chord* g : c->graceNotesBefore()) {
addChordPitchesToSet(g, set);
}
addChordPitchesToSet(c, set);
for (const Chord* g : c->graceNotesAfter()) {
addChordPitchesToSet(g, set);
}
}
}
}
}
}
}
//---------------------------------------------------------
// partList
//---------------------------------------------------------
/**
Write the part list to \a xml.
*/
static void partList(XmlWriter& xml, Score* score, MxmlInstrumentMap& instrMap)
{
xml.startElement("part-list");
size_t staffCount = 0; // count sum of # staves in parts
const auto& parts = score->parts();
int partGroupEnd[MAX_PART_GROUPS]; // staff where part group ends (bracketSpan is in staves, not parts)
for (int i = 0; i < MAX_PART_GROUPS; i++) {
partGroupEnd[i] = -1;
}
for (size_t idx = 0; idx < parts.size(); ++idx) {
const auto part = parts.at(idx);
bool bracketFound = false;
// handle brackets
for (size_t i = 0; i < part->nstaves(); i++) {
Staff* st = part->staff(i);
if (st) {
for (size_t j = 0; j < st->bracketLevels() + 1; j++) {
if (st->bracketType(j) != BracketType::NO_BRACKET) {
bracketFound = true;
if (i == 0) {
// OK, found bracket in first staff of part
// filter out implicit brackets
if (!(st->bracketSpan(j) == part->nstaves()
&& st->bracketType(j) == BracketType::BRACE)) {
// add others
int number = findPartGroupNumber(partGroupEnd);
if (number < MAX_PART_GROUPS) {
partGroupStart(xml, number + 1, st->bracketType(j));
partGroupEnd[number] = static_cast<int>(staffCount + st->bracketSpan(j));
}
}
} else {
// bracket in other staff not supported in MusicXML
LOGD("bracket starting in staff %zu not supported", i + 1);
}
}
}
}
}
// handle bracket none
if (!bracketFound && part->nstaves() > 1) {
int number = findPartGroupNumber(partGroupEnd);
if (number < MAX_PART_GROUPS) {
partGroupStart(xml, number + 1, BracketType::NO_BRACKET);
partGroupEnd[number] = static_cast<int>(idx + part->nstaves());
}
}
xml.startElementRaw(QString("score-part id=\"P%1\"").arg(idx + 1));
initInstrMap(instrMap, part->instruments(), score);
// by default export the parts long name as part-name
if (part->longName() != "") {
xml.tag("part-name", MScoreTextToMXML::toPlainText(part->longName()));
} else {
if (part->partName() != "") {
// use the track name if no part long name
// to prevent an empty track name on import
xml.tag("part-name", { { "print-object", "no" } }, MScoreTextToMXML::toPlainText(part->partName()));
} else {
// part-name is required
xml.tag("part-name", "");
}
}
if (!part->shortName().isEmpty()) {
xml.tag("part-abbreviation", MScoreTextToMXML::toPlainText(part->shortName()));
}
if (part->instrument()->useDrumset()) {
const Drumset* drumset = part->instrument()->drumset();
pitchSet pitches;
findPitchesUsed(part, pitches);
for (int i = 0; i < 128; ++i) {
DrumInstrument di = drumset->drum(i);
if (di.notehead != NoteHeadGroup::HEAD_INVALID) {
scoreInstrument(xml, static_cast<int>(idx) + 1, i + 1, di.name);
} else if (pitches.contains(i)) {
scoreInstrument(xml, static_cast<int>(idx) + 1, i + 1, QString("Instrument %1").arg(i + 1));
}
}
int midiPort = part->midiPort() + 1;
if (midiPort >= 1 && midiPort <= 16) {
xml.tag("midi-device", { { "port", midiPort } }, "");
}
for (int i = 0; i < 128; ++i) {
DrumInstrument di = drumset->drum(i);
if (di.notehead != NoteHeadGroup::HEAD_INVALID || pitches.contains(i)) {
midiInstrument(xml, static_cast<int>(idx) + 1, i + 1, part->instrument(), score, i + 1);
}
}
} else {
MxmlReverseInstrumentMap rim;
initReverseInstrMap(rim, instrMap);
for (int instNr : rim.keys()) {
scoreInstrument(xml, static_cast<int>(idx) + 1, instNr + 1, MScoreTextToMXML::toPlainText(rim.value(instNr)->trackName()));
}
for (auto ii = rim.constBegin(); ii != rim.constEnd(); ii++) {
int instNr = ii.key();
int midiPort = part->midiPort() + 1;
if (ii.value()->channel().size() > 0) {
midiPort = score->masterScore()->midiMapping(ii.value()->channel(0)->channel())->port() + 1;
}
if (midiPort >= 1 && midiPort <= 16) {
xml.tagRaw(QString("midi-device %1 port=\"%2\"").arg(instrId(static_cast<int>(idx) + 1, instNr + 1)).arg(midiPort), "");
} else {
xml.tagRaw(QString("midi-device %1").arg(instrId(static_cast<int>(idx) + 1, instNr + 1)), "");
}
midiInstrument(xml, static_cast<int>(idx) + 1, instNr + 1, rim.value(instNr), score);
}
}
xml.endElement();
staffCount += part->nstaves();
for (int i = MAX_PART_GROUPS - 1; i >= 0; i--) {
int end = partGroupEnd[i];
if (end >= 0) {
if (static_cast<int>(staffCount) >= end) {
xml.tag("part-group", { { "type", "stop" }, { "number", i + 1 } });
partGroupEnd[i] = -1;
}
}
}
}
xml.endElement();
}
//---------------------------------------------------------
// tickIsInMiddleOfMeasure
//---------------------------------------------------------
static bool tickIsInMiddleOfMeasure(const Fraction ti, const Measure* m)
{
return ti != m->tick() && ti != m->endTick();
}
//---------------------------------------------------------
// writeElement
//---------------------------------------------------------
/**
Write \a el.
*/
void ExportMusicXml::writeElement(EngravingItem* el, const Measure* m, staff_idx_t sstaff, bool useDrumset)
{
if (el->isClef()) {
// output only clef changes, not generated clefs
// at line beginning
// also ignore clefs at the start of a measure,
// these have already been output
// also ignore clefs at the end of a measure
// these will be output at the start of the next measure
const auto cle = toClef(el);
const auto ti = cle->segment()->tick();
clefDebug("exportxml: clef in measure ti=%d ct=%d gen=%d", ti, int(cle->clefType()), el->generated());
if (el->generated()) {
clefDebug("exportxml: generated clef not exported");
} else if (!el->generated() && tickIsInMiddleOfMeasure(ti, m)) {
clef(sstaff, cle->clefType(), color2xml(cle));
} else if (!el->generated() && (ti == m->tick()) && (cle->segment()->segmentType() != SegmentType::HeaderClef)) {
clef(sstaff, cle->clefType(), color2xml(cle) + QString(" after-barline=\"yes\""));
} else {
clefDebug("exportxml: clef not exported");
}
} else if (el->isChord()) {
const auto c = toChord(el);
// ise grace after
if (c) {
const auto ll = c->lyrics();
for (const auto g : c->graceNotesBefore()) {
chord(g, sstaff, ll, useDrumset);
}
chord(c, sstaff, ll, useDrumset);
for (const auto g : c->graceNotesAfter()) {
chord(g, sstaff, ll, useDrumset);
}
}
} else if (el->isRest()) {
const auto r = toRest(el);
if (!(r->isGap())) {
rest(r, sstaff);
}
} else if (el->isBarLine()) {
const auto barln = toBarLine(el);
if (tickIsInMiddleOfMeasure(barln->tick(), m)) {
barlineMiddle(barln);
}
} else if (el->isKeySig() || el->isTimeSig() || el->isBreath()) {
// handled elsewhere
} else {
LOGD("ExportMusicXml::write unknown segment type %s", el->typeName());
}
}
//---------------------------------------------------------
// writeStaffDetails
//---------------------------------------------------------
/**
Write the staff details for \a part to \a xml.
*/
static void writeStaffDetails(XmlWriter& xml, const Part* part)
{
const Instrument* instrument = part->instrument();
size_t staves = part->nstaves();
// staff details
// TODO: decide how to handle linked regular / TAB staff
// currently exported as a two staff part ...
for (size_t i = 0; i < staves; i++) {
Staff* st = part->staff(i);
if (st->lines(Fraction(0, 1)) != 5 || st->isTabStaff(Fraction(0, 1)) || !st->show()) {
XmlWriter::Attributes attributes;
if (staves > 1) {
attributes.push_back({ "number", i + 1 });
}
if (!st->show()) {
attributes.push_back({ "print-object", "no" });
}
xml.startElement("staff-details", attributes);
xml.tag("staff-lines", st->lines(Fraction(0, 1)));
if (st->isTabStaff(Fraction(0, 1)) && instrument->stringData()) {
std::vector<instrString> l = instrument->stringData()->stringList();
for (size_t ii = 0; ii < l.size(); ii++) {
char step = ' ';
int alter = 0;
int octave = 0;
midipitch2xml(l.at(ii).pitch, step, alter, octave);
xml.startElement("staff-tuning", { { "line", ii + 1 } });
xml.tag("tuning-step", QString("%1").arg(step));
if (alter) {
xml.tag("tuning-alter", alter);
}
xml.tag("tuning-octave", octave);
xml.endElement();
}
}
xml.endElement();
}
}
}
//---------------------------------------------------------
// writeInstrumentDetails
//---------------------------------------------------------
/**
Write the instrument details for \a instrument.
*/
void ExportMusicXml::writeInstrumentDetails(const Instrument* instrument)
{
if (instrument->transpose().chromatic) {
_attr.doAttr(_xml, true);
_xml.startElement("transpose");
_xml.tag("diatonic", instrument->transpose().diatonic % 7);
_xml.tag("chromatic", instrument->transpose().chromatic % 12);
int octaveChange = instrument->transpose().chromatic / 12;
if (octaveChange != 0) {
_xml.tag("octave-change", octaveChange);
}
_xml.endElement();
_attr.doAttr(_xml, false);
}
}
//---------------------------------------------------------
// annotationsWithoutNote
//---------------------------------------------------------
/**
Write the annotations that could not be attached to notes.
*/
static void annotationsWithoutNote(ExportMusicXml* exp, const track_idx_t strack, const int staves, const Measure* const measure)
{
for (auto segment = measure->first(); segment; segment = segment->next()) {
if (segment->segmentType() == SegmentType::ChordRest) {
for (const auto element : segment->annotations()) {
if (!element->isFiguredBass() && !element->isHarmony()) { // handled elsewhere
if (!exp->canWrite(element)) {
continue;
}
const track_idx_t wtrack = findTrackForAnnotations(element->track(), segment); // track to write annotation
if (strack <= element->track() && element->track() < (strack + VOICES * staves) && wtrack == mu::nidx) {
commonAnnotations(exp, element, staves > 1 ? 1 : 0);
}
}
}
}
}
}
//---------------------------------------------------------
// MeasureNumberStateHandler
//---------------------------------------------------------
MeasureNumberStateHandler::MeasureNumberStateHandler()
{
init();
}
void MeasureNumberStateHandler::init()
{
_measureNo = 1;
_irregularMeasureNo = 1;
_pickupMeasureNo = 1;
}
void MeasureNumberStateHandler::updateForMeasure(const Measure* const m)
{
// restart measure numbering after a section break if startWithMeasureOne is set
// check the previous MeasureBase instead of Measure to catch breaks in frames too
const MeasureBase* previousMB = m->prev();
if (previousMB) {
previousMB = previousMB->findPotentialSectionBreak();
}
if (previousMB) {
const auto layoutSectionBreak = previousMB->sectionBreakElement();
if (layoutSectionBreak && layoutSectionBreak->startWithMeasureOne()) {
init();
}
}
// update measure numbers and cache result
_measureNo += m->noOffset();
_cachedAttributes = " number=";
if ((_irregularMeasureNo + _measureNo) == 2 && m->irregular()) {
_cachedAttributes += "\"0\" implicit=\"yes\"";
_pickupMeasureNo++;
} else if (m->irregular()) {
_cachedAttributes += QString("\"X%1\" implicit=\"yes\"").arg(_irregularMeasureNo++);
} else {
_cachedAttributes += QString("\"%1\"").arg(_measureNo++);
}
}
QString MeasureNumberStateHandler::measureNumber() const
{
return _cachedAttributes;
}
bool MeasureNumberStateHandler::isFirstActualMeasure() const
{
return (_irregularMeasureNo + _measureNo + _pickupMeasureNo) == 4;
}
//---------------------------------------------------------
// findLastSystemWithMeasures
//---------------------------------------------------------
static System* findLastSystemWithMeasures(const Page* const page)
{
for (int i = static_cast<int>(page->systems().size()) - 1; i >= 0; --i) {
const auto s = page->systems().at(i);
const auto m = s->firstMeasure();
if (m) {
return s;
}
}
return nullptr;
}
//---------------------------------------------------------
// isFirstMeasureInSystem
//---------------------------------------------------------
static bool isFirstMeasureInSystem(const Measure* const measure)
{
const auto system = measure->mmRest1()->system();
const auto firstMeasureInSystem = system->firstMeasure();
const auto realFirstMeasureInSystem = firstMeasureInSystem->isMMRest() ? firstMeasureInSystem->mmRestFirst() : firstMeasureInSystem;
return measure == realFirstMeasureInSystem;
}
//---------------------------------------------------------
// isFirstMeasureInLastSystem
//---------------------------------------------------------
static bool isFirstMeasureInLastSystem(const Measure* const measure)
{
const auto system = measure->mmRest1()->system();
const auto page = system->page();
/*
Notes on multi-measure rest handling:
Function mmRest1() returns either the measure itself (if not part of multi-measure rest)
or the replacing multi-measure rest measure.
Using this is required as a measure that is covered by a multi-measure rest has no system.
Furthermore, the first measure in a system starting with a multi-measure rest is the a multi-
measure rest itself instead of the first covered measure.
*/
const auto lastSystem = findLastSystemWithMeasures(page);
if (!lastSystem) {
return false; // degenerate case: no system with measures found
}
const auto firstMeasureInLastSystem = lastSystem->firstMeasure();
const auto realFirstMeasureInLastSystem
= firstMeasureInLastSystem->isMMRest() ? firstMeasureInLastSystem->mmRestFirst() : firstMeasureInLastSystem;
return measure == realFirstMeasureInLastSystem;
}
//---------------------------------------------------------
// systemHasMeasures
//---------------------------------------------------------
static bool systemHasMeasures(const System* const system)
{
return system->firstMeasure();
}
//---------------------------------------------------------
// findTextFramesToWriteAsWordsAbove
//---------------------------------------------------------
static std::vector<TBox*> findTextFramesToWriteAsWordsAbove(const Measure* const measure)
{
const auto system = measure->mmRest1()->system();
const auto page = system->page();
const size_t systemIndex = mu::indexOf(page->systems(), system);
std::vector<TBox*> tboxes;
if (isFirstMeasureInSystem(measure)) {
for (int idx = static_cast<int>(systemIndex - 1); idx >= 0 && !systemHasMeasures(page->system(idx)); --idx) {
const auto sys = page->system(idx);
for (const auto mb : sys->measures()) {
if (mb->isTBox()) {
auto tbox = toTBox(mb);
tboxes.insert(tboxes.begin(), tbox);
}
}
}
}
return tboxes;
}
//---------------------------------------------------------
// findTextFramesToWriteAsWordsBelow
//---------------------------------------------------------
static std::vector<TBox*> findTextFramesToWriteAsWordsBelow(const Measure* const measure)
{
const auto system = measure->mmRest1()->system();
const auto page = system->page();
const size_t systemIndex = static_cast<int>(mu::indexOf(page->systems(), system));
std::vector<TBox*> tboxes;
if (isFirstMeasureInLastSystem(measure)) {
for (size_t idx = systemIndex + 1; idx < page->systems().size() /* && !systemHasMeasures(page->system(idx))*/;
++idx) {
const auto sys = page->system(static_cast<int>(idx));
for (const auto mb : sys->measures()) {
if (mb->isTBox()) {
auto tbox = toTBox(mb);
tboxes.insert(tboxes.begin(), tbox);
}
}
}
}
return tboxes;
}
//---------------------------------------------------------
// writeMeasureTracks
//---------------------------------------------------------
/**
Write data contained in the measure's tracks.
*/
void ExportMusicXml::writeMeasureTracks(const Measure* const m,
const int partIndex,
const staff_idx_t strack,
const staff_idx_t partRelStaffNo,
const bool useDrumset,
const bool isLastStaffOfPart,
FigBassMap& fbMap,
QSet<const Spanner*>& spannersStopped)
{
const auto tboxesAbove = findTextFramesToWriteAsWordsAbove(m);
const auto tboxesBelow = findTextFramesToWriteAsWordsBelow(m);
track_idx_t etrack = strack + VOICES;
for (track_idx_t track = strack; track < etrack; ++track) {
for (auto seg = m->first(); seg; seg = seg->next()) {
const auto el = seg->element(track);
if (!el) {
continue;
}
// must ignore start repeat to prevent spurious backup/forward
if (el->isBarLine() && toBarLine(el)->barLineType() == BarLineType::START_REPEAT) {
continue;
}
// generate backup or forward to the start time of the element
if (_tick != seg->tick()) {
_attr.doAttr(_xml, false);
moveToTick(seg->tick());
}
// handle annotations and spanners (directions attached to this note or rest)
if (el->isChordRest()) {
_attr.doAttr(_xml, false);
const bool isFirstPart = (partIndex == 0);
const bool isLastPart = (partIndex == (static_cast<int>(_score->parts().size()) - 1));
if (!_tboxesAboveWritten && isFirstPart) {
for (const auto tbox : tboxesAbove) {
// note: use mmRest1() to get at a possible multi-measure rest,
// as the covered measure would be positioned at 0,0.
tboxTextAsWords(tbox->text(), 0, mu::PointF(tbox->text()->canvasPos() - m->mmRest1()->canvasPos()).toQPointF());
}
_tboxesAboveWritten = true;
}
if (!_tboxesBelowWritten && isLastPart && isLastStaffOfPart) {
for (const auto tbox : tboxesBelow) {
const auto lastStaffNr = track2staff(track);
const auto sys = m->mmRest1()->system();
auto textPos = tbox->text()->canvasPos() - m->mmRest1()->canvasPos();
if (lastStaffNr < sys->staves().size()) {
// convert to position relative to last staff of system
textPos.setY(textPos.y() - (sys->staffCanvasYpage(lastStaffNr) - sys->staffCanvasYpage(0)));
}
tboxTextAsWords(tbox->text(), partRelStaffNo, textPos.toQPointF());
}
_tboxesBelowWritten = true;
}
annotations(this, strack, etrack, track, partRelStaffNo, seg);
// look for more harmony
for (auto seg1 = seg->next(); seg1; seg1 = seg1->next()) {
if (seg1->isChordRestType()) {
const auto el1 = seg1->element(track);
if (el1) { // found a ChordRest, next harmony will be attach to this one
break;
}
for (auto annot : seg1->annotations()) {
if (annot->isHarmony() && annot->track() == track) {
harmony(toHarmony(annot), 0, (seg1->tick() - seg->tick()).ticks() / div);
}
}
}
}
figuredBass(_xml, strack, etrack, track, static_cast<const ChordRest*>(el), fbMap, div);
spannerStart(this, strack, etrack, track, partRelStaffNo, seg);
}
// write element el if necessary
writeElement(el, m, partRelStaffNo, useDrumset);
// handle annotations and spanners (directions attached to this note or rest)
if (el->isChordRest()) {
const staff_idx_t spannerStaff = track2staff(track);
const track_idx_t starttrack = staff2track(spannerStaff);
const track_idx_t endtrack = staff2track(spannerStaff + 1);
spannerStop(this, starttrack, endtrack, _tick, partRelStaffNo, spannersStopped);
}
} // for (Segment* seg = ...
_attr.stop(_xml);
} // for (int st = ...
}
//---------------------------------------------------------
// writeMeasureStaves
//---------------------------------------------------------
/**
Write each staff of a measure for a given part.
*/
void ExportMusicXml::writeMeasureStaves(const Measure* m,
const int partIndex,
const staff_idx_t startStaff,
const size_t nstaves,
const bool useDrumset,
FigBassMap& fbMap,
QSet<const Spanner*>& spannersStopped)
{
const staff_idx_t endStaff = startStaff + nstaves;
const Measure* const origM = m;
_tboxesAboveWritten = false;
_tboxesBelowWritten = false;
for (staff_idx_t staffIdx = startStaff; staffIdx < endStaff; ++staffIdx) {
// some staves may need to make m point somewhere else, so just in case, ensure start in same place
IF_ASSERT_FAILED(m == origM) {
return;
}
moveToTick(m->tick());
staff_idx_t partRelStaffNo = (nstaves > 1 ? staffIdx - startStaff + 1 : 0); // xml staff number, counting from 1 for this instrument
// special number 0 -> dont show staff number in xml output
// (because there is only one staff)
// in presence of a MeasureRepeat, adjust m to point to the actual content being repeated
if (m->isMeasureRepeatGroup(staffIdx)) {
MeasureRepeat* mr = nullptr;
while (m->isMeasureRepeatGroup(staffIdx) && m->prevMeasure()) { // keep going back until out of measure repeat groups
mr = m->measureRepeatElement(staffIdx);
IF_ASSERT_FAILED(mr) {
LOGE() << String("Could not find MeasureRepeat on measure %1, staff %2").arg(m->index()).arg(staffIdx);
break;
}
for (int i = 0; i < mr->numMeasures() && m->prevMeasure(); ++i) {
m = m->prevMeasure();
}
}
}
// in case m was changed, also rewind _tick so as not to generate unnecessary backup/forward tags
auto tickDelta = _tick - m->tick();
_tick -= tickDelta;
bool isLastStaffOfPart = (endStaff - 1 <= staffIdx); // for writing tboxes below
writeMeasureTracks(m, partIndex, staff2track(staffIdx), partRelStaffNo, useDrumset, isLastStaffOfPart, fbMap, spannersStopped);
// restore m and _tick before advancing to next staff in part
m = origM;
_tick += tickDelta;
}
}
//---------------------------------------------------------
// writeMeasure
//---------------------------------------------------------
/**
Write a measure.
*/
void ExportMusicXml::writeMeasure(const Measure* const m,
const int partIndex,
const int staffCount,
MeasureNumberStateHandler& mnsh,
FigBassMap& fbMap,
const MeasurePrintContext& mpc,
QSet<const Spanner*>& spannersStopped)
{
const auto part = _score->parts().at(partIndex);
const size_t staves = part->nstaves();
const track_idx_t strack = part->startTrack();
const track_idx_t etrack = part->endTrack();
// pickup and other irregular measures need special care
QString measureTag = "measure";
mnsh.updateForMeasure(m);
measureTag += mnsh.measureNumber();
const bool isFirstActualMeasure = mnsh.isFirstActualMeasure();
if (configuration()->musicxmlExportLayout()) {
measureTag += QString(" width=\"%1\"").arg(QString::number(m->bbox().width() / DPMM / millimeters * tenths, 'f', 2));
}
_xml.startElementRaw(measureTag);
print(m, partIndex, staffCount, static_cast<int>(staves), mpc);
_attr.start();
findTrills(m, strack, etrack, _trillStart, _trillStop);
// barline left must be the first element in a measure
barlineLeft(m);
// output attributes with the first actual measure (pickup or regular)
if (isFirstActualMeasure) {
_attr.doAttr(_xml, true);
_xml.tag("divisions", Constants::division / div);
}
// output attributes at start of measure: key, time
keysigTimesig(m, part);
// output attributes with the first actual measure (pickup or regular) only
if (isFirstActualMeasure) {
if (staves > 1) {
_xml.tag("staves", static_cast<int>(staves));
}
if (instrMap.size() > 1) {
_xml.tag("instruments", instrMap.size());
}
}
// make sure clefs at end of measure get exported at start of next measure
findAndExportClef(m, static_cast<int>(staves), strack, etrack);
// make sure a clef gets exported if none is found
exportDefaultClef(part, m);
// output attributes with the first actual measure (pickup or regular) only
if (isFirstActualMeasure) {
writeStaffDetails(_xml, part);
writeInstrumentDetails(part->instrument());
}
// output attribute at start of measure: measure-style
measureStyle(_xml, _attr, m, partIndex);
// MuseScore limitation: repeats are always in the first part
// and are implicitly placed at either measure start or stop
if (partIndex == 0) {
repeatAtMeasureStart(_attr, m, strack, etrack, strack);
}
// write data in the staves
writeMeasureStaves(m, partIndex, track2staff(strack), staves, part->instrument()->useDrumset(), fbMap, spannersStopped);
// write the annotations that could not be attached to notes
annotationsWithoutNote(this, strack, static_cast<int>(staves), m);
// move to end of measure (in case of incomplete last voice)
#ifdef DEBUG_TICK
LOGD("end of measure");
#endif
moveToTick(m->endTick());
if (partIndex == 0) {
repeatAtMeasureStop(m, strack, etrack, strack);
}
// note: don't use "m->repeatFlags() & Repeat::END" here, because more
// barline types need to be handled besides repeat end ("light-heavy")
barlineRight(m, strack, etrack);
_xml.endElement();
}
//---------------------------------------------------------
// measureWritten
//---------------------------------------------------------
void MeasurePrintContext::measureWritten(const Measure* m)
{
scoreStart = false;
pageStart = false;
systemStart = false;
prevMeasure = m;
}
//---------------------------------------------------------
// writeParts
//---------------------------------------------------------
/**
Write all parts.
*/
void ExportMusicXml::writeParts()
{
int staffCount = 0;
const auto& parts = _score->parts();
for (size_t partIndex = 0; partIndex < parts.size(); ++partIndex) {
const auto part = parts.at(partIndex);
_tick = { 0, 1 };
_xml.startElementRaw(QString("part id=\"P%1\"").arg(partIndex + 1));
_trillStart.clear();
_trillStop.clear();
initInstrMap(instrMap, part->instruments(), _score);
MeasureNumberStateHandler mnsh;
FigBassMap fbMap; // pending figured bass extends
// set of spanners already stopped in this part
// required to prevent multiple spanner stops for the same spanner
QSet<const Spanner*> spannersStopped;
const auto& pages = _score->pages();
MeasurePrintContext mpc;
for (size_t pageIndex = 0; pageIndex < pages.size(); ++pageIndex) {
const auto page = pages.at(pageIndex);
mpc.pageStart = true;
const auto& systems = page->systems();
for (int systemIndex = 0; systemIndex < static_cast<int>(systems.size()); ++systemIndex) {
const auto system = systems.at(systemIndex);
mpc.systemStart = true;
for (const auto mb : system->measures()) {
if (!mb->isMeasure()) {
continue;
}
const auto m = toMeasure(mb);
if (m->isMMRest()) {
// in case of a multimeasure rest (which is a single measure in MuseScore), write the measure range it replaces
const auto m2 = m->mmRestLast()->nextMeasure();
for (auto m1 = m->mmRestFirst(); m1 != m2; m1 = m1->nextMeasure()) {
if (m1->isMeasure()) {
writeMeasure(m1, static_cast<int>(partIndex), staffCount, mnsh, fbMap, mpc, spannersStopped);
mpc.measureWritten(m1);
}
}
} else {
// write the measure (or, if measure repeat, the "underlying" measure that it indicates for the musician to play)
writeMeasure(m, static_cast<int>(partIndex), staffCount, mnsh, fbMap, mpc, spannersStopped);
mpc.measureWritten(m);
}
}
mpc.prevSystem = system;
}
mpc.lastSystemPrevPage = mpc.prevSystem;
}
staffCount += static_cast<int>(part->nstaves());
_xml.endElement();
}
}
//---------------------------------------------------------
// findJumpElements
//---------------------------------------------------------
static std::vector<const Jump*> findJumpElements(const Score* score)
{
std::vector<const Jump*> jumps;
for (const MeasureBase* m = score->first(); m; m = m->next()) {
for (const EngravingItem* e : m->el()) {
if (e->isJump()) {
jumps.push_back(toJump(e));
}
}
}
return jumps;
}
//---------------------------------------------------------
// write
//---------------------------------------------------------
/**
Write the score to \a dev in MusicXML format.
*/
void ExportMusicXml::write(mu::io::IODevice* dev)
{
// must export in transposed pitch to prevent
// losing the transposition information
// if necessary, switch concert pitch mode off
// before export and restore it after export
bool concertPitch = score()->styleB(Sid::concertPitch);
if (concertPitch) {
score()->startCmd();
score()->undo(new ChangeStyleVal(score(), Sid::concertPitch, false));
score()->doLayout(); // this is only allowed in a cmd context to not corrupt the undo/redo stack
}
calcDivisions();
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
brackets[i] = nullptr;
dashes[i] = nullptr;
hairpins[i] = nullptr;
ottavas[i] = nullptr;
trills[i] = nullptr;
}
_jumpElements = findJumpElements(_score);
_xml.setDevice(dev);
_xml.startDocument();
_xml.writeDoctype(u"score-partwise PUBLIC \"-//Recordare//DTD MusicXML 4.0 Partwise//EN\" \"http://www.musicxml.org/dtds/partwise.dtd\"");
_xml.startElement("score-partwise", { { "version", "4.0" } });
work(_score->measures()->first());
identification(_xml, _score);
if (configuration()->musicxmlExportLayout()) {
defaults(_xml, _score, millimeters, tenths);
credits(_xml);
}
partList(_xml, _score, instrMap);
writeParts();
_xml.endElement();
if (concertPitch) {
// restore concert pitch
score()->endCmd(true); // rollback
}
}
//---------------------------------------------------------
// saveXml
// return false on error
//---------------------------------------------------------
/**
Save Score as MusicXML file \a name.
Return false on error.
*/
bool saveXml(Score* score, QIODevice* device)
{
mu::io::Buffer buf;
buf.open(mu::io::IODevice::WriteOnly);
ExportMusicXml em(score);
em.write(&buf);
device->write(buf.data().toQByteArrayNoCopy());
return true;
}
bool saveXml(Score* score, const QString& name)
{
QFile f(name);
if (!f.open(QIODevice::WriteOnly)) {
return false;
}
bool res = saveXml(score, &f) && (f.error() == QFile::NoError);
f.close();
return res;
}
//---------------------------------------------------------
// saveMxl
// return false on error
//---------------------------------------------------------
/**
Save Score as compressed MusicXML file \a name.
Return false on error.
*/
// META-INF/container.xml:
// <?xml version="1.0" encoding="UTF-8"?>
// <container>
// <rootfiles>
// <rootfile full-path="testHello.xml"/>
// </rootfiles>
// </container>
static void writeMxlArchive(Score* score, MQZipWriter& zipwriter, const QString& filename)
{
mu::io::Buffer cbuf;
cbuf.open(mu::io::IODevice::ReadWrite);
XmlWriter xml;
xml.setDevice(&cbuf);
xml.startDocument();
xml.startElement("container");
xml.startElement("rootfiles");
xml.startElement("rootfile", { { "full-path", filename } });
xml.endElement();
xml.endElement();
xml.endElement();
cbuf.seek(0);
zipwriter.addFile("META-INF/container.xml", cbuf.data().toQByteArrayNoCopy());
mu::io::Buffer dbuf;
dbuf.open(mu::io::IODevice::ReadWrite);
ExportMusicXml em(score);
em.write(&dbuf);
dbuf.seek(0);
zipwriter.addFile(filename, dbuf.data().toQByteArrayNoCopy());
}
bool saveMxl(Score* score, QIODevice* device)
{
MQZipWriter uz(device);
//anonymized filename since we don't know the actual one here
QString fn = "score.xml";
writeMxlArchive(score, uz, fn);
uz.close();
return true;
}
bool saveMxl(Score* score, const QString& name)
{
MQZipWriter uz(name);
FileInfo fi(name);
QString fn = fi.completeBaseName() + u".xml";
writeMxlArchive(score, uz, fn);
return true;
}
double ExportMusicXml::getTenthsFromInches(double inches) const
{
return inches * INCH / millimeters * tenths;
}
double ExportMusicXml::getTenthsFromDots(double dots) const
{
return dots / DPMM / millimeters * tenths;
}
//---------------------------------------------------------
// harmony
//---------------------------------------------------------
void ExportMusicXml::harmony(Harmony const* const h, FretDiagram const* const fd, int offset)
{
// this code was probably in place to allow chord symbols shifted *right* to export with offset
// since this was at once time the only way to get a chord to appear over beat 3 in an empty 4/4 measure
// but the value was calculated incorrectly (should be divided by spatium) and would be better off using offset anyhow
// since we now support placement of chord symbols over "empty" beats directly,
// and we don't generally export position info for other elements
// it's just as well to not bother doing so here
//double rx = h->offset().x()*10;
//QString relative;
//if (rx > 0) {
// relative = QString(" relative-x=\"%1\"").arg(QString::number(rx,'f',2));
// }
int rootTpc = h->rootTpc();
if (rootTpc != Tpc::TPC_INVALID) {
XmlWriter::Attributes harmonyAttrs;
bool frame = h->hasFrame();
harmonyAttrs.push_back({ "print-frame", frame ? "yes" : "no" }); // .append(relative));
addColorAttr(h, harmonyAttrs);
_xml.startElement("harmony", harmonyAttrs);
_xml.startElement("root");
_xml.tag("root-step", tpc2stepName(rootTpc));
int alter = int(tpc2alter(rootTpc));
if (alter) {
_xml.tag("root-alter", alter);
}
_xml.endElement();
if (!h->xmlKind().isEmpty()) {
QString s = "kind";
QString kindText = h->musicXmlText();
if (h->musicXmlText() != "") {
s += " text=\"" + kindText + "\"";
}
if (h->xmlSymbols() == "yes") {
s += " use-symbols=\"yes\"";
}
if (h->xmlParens() == "yes") {
s += " parentheses-degrees=\"yes\"";
}
_xml.tagRaw(s, h->xmlKind());
StringList l = h->xmlDegrees();
if (!l.empty()) {
for (const String& _tag : qAsConst(l)) {
QString tag = _tag.toQString();
QString degreeText;
if (h->xmlKind().startsWith(u"suspended")
&& tag.startsWith("add") && tag[3].isDigit()
&& !kindText.isEmpty() && kindText[0].isDigit()) {
// hack to correct text for suspended chords whose kind text has degree information baked in
// (required by some other applications)
int tagDegree = tag.midRef(3).toInt();
QString kindTextExtension;
for (int i = 0; i < kindText.length() && kindText[i].isDigit(); ++i) {
kindTextExtension[i] = kindText[i];
}
int kindExtension = kindTextExtension.toInt();
if (tagDegree <= kindExtension && (tagDegree & 1) && (kindExtension & 1)) {
degreeText = " text=\"\"";
}
}
_xml.startElement("degree");
alter = 0;
int idx = 3;
if (tag[idx] == '#') {
alter = 1;
++idx;
} else if (tag[idx] == 'b') {
alter = -1;
++idx;
}
_xml.tagRaw(QString("degree-value%1").arg(degreeText), tag.mid(idx));
_xml.tag("degree-alter", alter); // finale insists on this even if 0
if (tag.startsWith("add")) {
_xml.tagRaw(QString("degree-type%1").arg(degreeText), "add");
} else if (tag.startsWith("sub")) {
_xml.tag("degree-type", "subtract");
} else if (tag.startsWith("alt")) {
_xml.tag("degree-type", "alter");
}
_xml.endElement();
}
}
} else {
if (h->extensionName().empty()) {
_xml.tag("kind", "");
} else {
_xml.tag("kind", { { "text", h->extensionName() } }, "");
}
}
int baseTpc = h->baseTpc();
if (baseTpc != Tpc::TPC_INVALID) {
_xml.startElement("bass");
_xml.tag("bass-step", tpc2stepName(baseTpc));
alter = int(tpc2alter(baseTpc));
if (alter) {
_xml.tag("bass-alter", alter);
}
_xml.endElement();
}
if (offset > 0) {
_xml.tag("offset", offset);
}
if (fd) {
fd->writeMusicXML(_xml);
}
_xml.endElement();
} else {
//
// export an unrecognized Chord
// which may contain arbitrary text
//
_xml.startElement("harmony", { { "print-frame", h->hasFrame() ? "yes" : "no" } });
const String textName = h->hTextName();
switch (h->harmonyType()) {
case HarmonyType::NASHVILLE: {
_xml.tag("function", h->hFunction());
_xml.tag("kind", { { "text", textName } }, "none");
}
break;
case HarmonyType::ROMAN: {
// TODO: parse?
_xml.tag("function", h->hTextName()); // note: HTML escape done by tag()
_xml.tag("kind", { { "text", "" } }, "none");
}
break;
case HarmonyType::STANDARD:
default: {
_xml.startElement("root");
_xml.tag("root-step", { { "text", "" } }, "C");
_xml.endElement(); // root
_xml.tag("kind", { { "text", textName } }, "none");
}
break;
}
_xml.endElement(); // harmony
}
}
//---------------------------------------------------------
// canWrite
//---------------------------------------------------------
/**
Whether a tag corresponding to the given element \p e
should be included to the exported MusicXML file.
*/
bool ExportMusicXml::canWrite(const EngravingItem* e)
{
return e->visible() || configuration()->musicxmlExportInvisibleElements();
}
}