MuseScore/mscore/importmxmlpass1.cpp

2814 lines
105 KiB
C++

//=============================================================================
// MuseScore
// Linux Music Score Editor
//
// Copyright (C) 2015 Werner Schweer and others
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2.
//
// 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, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//=============================================================================
#include "libmscore/box.h"
#include "libmscore/instrtemplate.h"
#include "libmscore/measure.h"
#include "libmscore/page.h"
#include "libmscore/part.h"
#include "libmscore/staff.h"
#include "libmscore/stringdata.h"
#include "libmscore/sym.h"
#include "libmscore/symbol.h"
#include "libmscore/timesig.h"
#include "libmscore/style.h"
#include "libmscore/spanner.h"
#include "libmscore/bracketItem.h"
#include "importmxmllogger.h"
#include "importmxmlnoteduration.h"
#include "importmxmlpass1.h"
#include "importmxmlpass2.h"
#include "preferences.h"
namespace Ms {
//---------------------------------------------------------
// allocateStaves
//---------------------------------------------------------
/**
Allocate MuseScore staff to MusicXML voices.
For each staff, allocate at most VOICES voices to the staff.
*/
// for regular (non-overlapping) voices:
// 1) assign voice to a staff (allocateStaves)
// 2) assign voice numbers (allocateVoices)
// due to cross-staving, it is not a priori clear to which staff
// a voice has to be assigned
// allocate ordered by number of chordrests in the MusicXML voice
//
// for overlapping voices:
// 1) assign voice to staves it is found in (allocateStaves)
// 2) assign voice numbers (allocateVoices)
static void allocateStaves(VoiceList& vcLst)
{
// initialize
int voicesAllocated[MAX_STAVES]; // number of voices allocated on each staff
for (int i = 0; i < MAX_STAVES; ++i)
voicesAllocated[i] = 0;
// handle regular (non-overlapping) voices
// note: outer loop executed vcLst.size() times, as each inner loop handles exactly one item
for (int i = 0; i < vcLst.size(); ++i) {
// find the regular voice containing the highest number of chords and rests that has not been handled yet
int max = 0;
QString key;
for (VoiceList::const_iterator j = vcLst.constBegin(); j != vcLst.constEnd(); ++j) {
if (!j.value().overlaps() && j.value().numberChordRests() > max && j.value().staff() == -1) {
max = j.value().numberChordRests();
key = j.key();
}
}
if (key != "") {
int prefSt = vcLst.value(key).preferredStaff();
if (voicesAllocated[prefSt] < VOICES) {
vcLst[key].setStaff(prefSt);
voicesAllocated[prefSt]++;
}
else
// out of voices: mark as used but not allocated
vcLst[key].setStaff(-2);
}
}
// handle overlapping voices
// for every staff allocate remaining voices (if space allows)
// the ones with the highest number of chords and rests get allocated first
for (int h = 0; h < MAX_STAVES; ++h) {
// note: middle loop executed vcLst.size() times, as each inner loop handles exactly one item
for (int i = 0; i < vcLst.size(); ++i) {
// find the overlapping voice containing the highest number of chords and rests that has not been handled yet
int max = 0;
QString key;
for (VoiceList::const_iterator j = vcLst.constBegin(); j != vcLst.constEnd(); ++j) {
if (j.value().overlaps() && j.value().numberChordRests(h) > max && j.value().staffAlloc(h) == -1) {
max = j.value().numberChordRests(h);
key = j.key();
}
}
if (key != "") {
int prefSt = h;
if (voicesAllocated[prefSt] < VOICES) {
vcLst[key].setStaffAlloc(prefSt, 1);
voicesAllocated[prefSt]++;
}
else
// out of voices: mark as used but not allocated
vcLst[key].setStaffAlloc(prefSt, -2);
}
}
}
}
//---------------------------------------------------------
// allocateVoices
//---------------------------------------------------------
/**
Allocate MuseScore voice to MusicXML voices.
For each staff, the voices are number 1, 2, 3, 4
in the same order they are numbered in the MusicXML file.
*/
static void allocateVoices(VoiceList& vcLst)
{
int nextVoice[MAX_STAVES]; // number of voices allocated on each staff
for (int i = 0; i < MAX_STAVES; ++i)
nextVoice[i] = 0;
// handle regular (non-overlapping) voices
// a voice is allocated on one specific staff
for (VoiceList::const_iterator i = vcLst.constBegin(); i != vcLst.constEnd(); ++i) {
int staff = i.value().staff();
QString key = i.key();
if (staff >= 0) {
vcLst[key].setVoice(nextVoice[staff]);
nextVoice[staff]++;
}
}
// handle overlapping voices
// each voice may be in every staff
for (VoiceList::const_iterator i = vcLst.constBegin(); i != vcLst.constEnd(); ++i) {
for (int j = 0; j < MAX_STAVES; ++j) {
int staffAlloc = i.value().staffAlloc(j);
QString key = i.key();
if (staffAlloc >= 0) {
vcLst[key].setVoice(j, nextVoice[j]);
nextVoice[j]++;
}
}
}
}
//---------------------------------------------------------
// copyOverlapData
//---------------------------------------------------------
/**
Copy the overlap data from the overlap detector to the voice list.
*/
static void copyOverlapData(VoiceOverlapDetector& vod, VoiceList& vcLst)
{
for (VoiceList::const_iterator i = vcLst.constBegin(); i != vcLst.constEnd(); ++i) {
QString key = i.key();
if (vod.stavesOverlap(key))
vcLst[key].setOverlap(true);
}
}
//---------------------------------------------------------
// MusicXMLParserPass1
//---------------------------------------------------------
MusicXMLParserPass1::MusicXMLParserPass1(Score* score, MxmlLogger* logger)
: _divs(0), _score(score), _logger(logger)
{
// nothing
}
//---------------------------------------------------------
// initPartState
//---------------------------------------------------------
/**
Initialize members as required for reading the MusicXML part element.
TODO: factor out part reading into a separate class
TODO: preferably use automatically initialized variables
Note that Qt automatically initializes new elements in QVector (tuplets).
*/
void MusicXMLParserPass1::initPartState(const QString& /* partId */)
{
_timeSigDura = Fraction(0, 0); // invalid
_octaveShifts.clear();
_firstInstrSTime = Fraction(0, 1);
_firstInstrId = "";
}
//---------------------------------------------------------
// determineMeasureLength
//---------------------------------------------------------
/**
Determine the length in ticks of each measure in all parts.
Return false on error.
*/
bool MusicXMLParserPass1::determineMeasureLength(QVector<Fraction>& ml) const
{
ml.clear();
// determine number of measures: max number of measures in any part
int nMeasures = 0;
foreach (const MusicXmlPart &part, _parts) {
if (part.nMeasures() > nMeasures)
nMeasures = part.nMeasures();
}
// determine max length of a specific measure in all parts
for (int i = 0; i < nMeasures; ++i) {
Fraction maxMeasDur;
foreach (const MusicXmlPart &part, _parts) {
if (i < part.nMeasures()) {
Fraction measDurPartJ = part.measureDuration(i);
if (measDurPartJ > maxMeasDur)
maxMeasDur = measDurPartJ;
}
}
//qDebug("determineMeasureLength() measure %d %s (%d)", i, qPrintable(maxMeasDur.print()), maxMeasDur.ticks());
ml.append(maxMeasDur);
}
return true;
}
//---------------------------------------------------------
// getVoiceList
//---------------------------------------------------------
/**
Get the VoiceList for part \a id.
Return an empty VoiceList on error.
*/
VoiceList MusicXMLParserPass1::getVoiceList(const QString id) const
{
if (_parts.contains(id))
return _parts.value(id).voicelist;
return VoiceList();
}
//---------------------------------------------------------
// getInstrList
//---------------------------------------------------------
/**
Get the MusicXmlInstrList for part \a id.
Return an empty MusicXmlInstrList on error.
*/
MusicXmlInstrList MusicXMLParserPass1::getInstrList(const QString id) const
{
if (_parts.contains(id))
return _parts.value(id)._instrList;
return MusicXmlInstrList();
}
//---------------------------------------------------------
// determineMeasureLength
//---------------------------------------------------------
/**
Set default notehead, line and stem direction
for instrument \a instrId in part \a id.
*/
void MusicXMLParserPass1::setDrumsetDefault(const QString& id,
const QString& instrId,
const NoteHead::Group hg,
const int line,
const Direction sd)
{
if (_drumsets.contains(id)
&& _drumsets[id].contains(instrId)) {
_drumsets[id][instrId].notehead = hg;
_drumsets[id][instrId].line = line;
_drumsets[id][instrId].stemDirection = sd;
}
}
//---------------------------------------------------------
// determineStaffMoveVoice
//---------------------------------------------------------
/**
For part \a id, determine MuseScore (ms) staffmove, track and voice from MusicXML (mx) staff and voice
MusicXML staff is 0 for the first staff, 1 for the second.
Note: track is the first track of the ms staff in the score, add ms voice for elements in a voice
Return true if OK, false on error
TODO: finalize
*/
bool MusicXMLParserPass1::determineStaffMoveVoice(const QString& id, const int mxStaff, const QString& mxVoice,
int& msMove, int& msTrack, int& msVoice) const
{
VoiceList voicelist = getVoiceList(id);
msMove = 0; // TODO
msTrack = 0; // TODO
msVoice = 0; // TODO
// Musicxml voices are counted for all staffs of an
// instrument. They are not limited. In mscore voices are associated
// with a staff. Every staff can have at most VOICES voices.
// The following lines map musicXml voices to mscore voices.
// If a voice crosses two staffs, this is expressed with the
// "move" parameter in mscore.
// Musicxml voices are unique within a part, but not across parts.
//qDebug("voice mapper before: voice='%s' staff=%d", qPrintable(mxVoice), mxStaff);
int s; // staff mapped by voice mapper
int v; // voice mapped by voice mapper
if (voicelist.value(mxVoice).overlaps()) {
// for overlapping voices, the staff does not change
// and the voice is mapped and staff-dependent
s = mxStaff;
v = voicelist.value(mxVoice).voice(s);
}
else {
// for non-overlapping voices, both staff and voice are
// set by the voice mapper
s = voicelist.value(mxVoice).staff();
v = voicelist.value(mxVoice).voice();
}
//qDebug("voice mapper mapped: s=%d v=%d", s, v);
if (s < 0 || v < 0) {
qDebug("too many voices (staff=%d voice='%s' -> s=%d v=%d)",
mxStaff + 1, qPrintable(mxVoice), s, v);
return false;
}
msMove = mxStaff - s;
msVoice = v;
// make score-relative instead on part-relative
Part* part = _partMap.value(id);
Q_ASSERT(part);
int scoreRelStaff = _score->staffIdx(part); // zero-based number of parts first staff in the score
msTrack = (scoreRelStaff + s) * VOICES;
//qDebug("voice mapper after: scoreRelStaff=%d partRelStaff=%d msMove=%d msTrack=%d msVoice=%d",
// scoreRelStaff, s, msMove, msTrack, msVoice);
// note: relStaff is the staff number relative to the parts first staff
// voice is the voice number in the staff
return true;
}
//---------------------------------------------------------
// hasPart
//---------------------------------------------------------
/**
Check if part \a id is found.
*/
bool MusicXMLParserPass1::hasPart(const QString& id) const
{
return _parts.contains(id);
}
//---------------------------------------------------------
// trackForPart
//---------------------------------------------------------
/**
Return the (score relative) track number for the first staff of part \a id.
*/
int MusicXMLParserPass1::trackForPart(const QString& id) const
{
Part* part = _partMap.value(id);
Q_ASSERT(part);
int scoreRelStaff = _score->staffIdx(part); // zero-based number of parts first staff in the score
return scoreRelStaff * VOICES;
}
//---------------------------------------------------------
// getMeasureStart
//---------------------------------------------------------
/**
Return the measure start time for measure \a i.
*/
Fraction MusicXMLParserPass1::getMeasureStart(const int i) const
{
if (0 <= i && i < _measureStart.size())
return _measureStart.at(i);
else
return Fraction(0, 0); // invalid
}
//---------------------------------------------------------
// octaveShift
//---------------------------------------------------------
/**
Return the octave shift for part \a id in \a staff at \a f.
*/
int MusicXMLParserPass1::octaveShift(const QString& id, const int staff, const Fraction f) const
{
if (_parts.contains(id))
return _parts.value(id).octaveShift(staff, f);
return 0;
}
//---------------------------------------------------------
// skipLogCurrElem
//---------------------------------------------------------
/**
Skip the current element, log debug as info.
*/
void MusicXMLParserPass1::skipLogCurrElem()
{
_logger->logDebugInfo(QString("skipping '%1'").arg(_e.name().toString()), &_e);
_e.skipCurrentElement();
}
//---------------------------------------------------------
// createMeasures
//---------------------------------------------------------
/**
Create required measures with correct number, start tick and length for Score \a score.
*/
static void createMeasures(Score* score, const QVector<Fraction>& ml, const QVector<Fraction>& ms)
{
for (int i = 0; i < ml.size(); ++i) {
Measure* measure = new Measure(score);
measure->setTick(ms.at(i).ticks());
measure->setLen(ml.at(i));
measure->setNo(i);
score->measures()->add(measure);
}
}
//---------------------------------------------------------
// determineMeasureStart
//---------------------------------------------------------
/**
Determine the start ticks of each measure
i.e. the sum of all previous measures length
or start tick measure equals start tick previous measure plus length previous measure
*/
static void determineMeasureStart(const QVector<Fraction>& ml, QVector<Fraction>& ms)
{
ms.resize(ml.size());
if (!(ms.size() > 0))
return; // no parts read
// first measure starts at t = 0
ms[0] = Fraction(0, 1);
// all others start at start time previous measure plus length previous measure
for (int i = 1; i < ml.size(); i++)
ms[i] = ms.at(i - 1) + ml.at(i - 1);
//for (int i = 0; i < ms.size(); i++)
// qDebug("measurestart ms[%d] %s", i + 1, qPrintable(ms.at(i).print()));
}
//---------------------------------------------------------
// addText
//---------------------------------------------------------
/**
Add text \a strTxt to VBox \a vbx using SubStyleId \a stl.
*/
static void addText(VBox* vbx, Score* s, QString strTxt, SubStyleId stl)
{
if (!strTxt.isEmpty()) {
Text* text = new Text(stl, s);
text->setXmlText(strTxt);
vbx->add(text);
}
}
//---------------------------------------------------------
// addText
//---------------------------------------------------------
/**
Add text \a strTxt to VBox \a vbx using SubStyleId \a stl.
Also sets Align and Yoff.
*/
static void addText2(VBox* vbx, Score* s, QString strTxt, SubStyleId stl, Align v, double yoffs)
{
if (!strTxt.isEmpty()) {
Text* text = new Text(stl, s);
text->setXmlText(strTxt);
text->setAlign(v);
text->setOffset(QPointF(0.0, yoffs));
vbx->add(text);
}
}
//---------------------------------------------------------
// doCredits
//---------------------------------------------------------
/**
Create Text elements for the credits read from MusicXML credit-words elements.
Apply simple heuristics using only default x and y to recognize the meaning of credit words
If no credits are found, create credits from meta data.
*/
static void doCredits(Score* score, const CreditWordsList& credits, const int pageWidth, const int pageHeight)
{
/*
qDebug("MusicXml::doCredits()");
qDebug("page format set (inch) w=%g h=%g tm=%g spatium=%g DPMM=%g DPI=%g",
pf->width(), pf->height(), pf->oddTopMargin(), score->spatium(), DPMM, DPI);
*/
// page width, height and odd top margin in tenths
const double ph = score->styleD(Sid::pageHeight) * 10 * DPI / score->spatium();
const int pw1 = pageWidth / 3;
const int pw2 = pageWidth * 2 / 3;
const int ph2 = pageHeight / 2;
/*
const double pw = pf->width() * 10 * DPI / score->spatium();
const double tm = pf->oddTopMargin() * 10 * DPI / score->spatium();
const double tov = ph - tm;
qDebug("page format set (tenths) w=%g h=%g tm=%g tov=%g", pw, ph, tm, tov);
qDebug("page format (xml, tenths) w=%d h=%d", pageWidth, pageHeight);
qDebug("page format pw1=%d pw2=%d ph2=%d", pw1, pw2, ph2);
*/
// dump the credits
/*
for (ciCreditWords ci = credits.begin(); ci != credits.end(); ++ci) {
CreditWords* w = *ci;
qDebug("credit-words defx=%g defy=%g just=%s hal=%s val=%s words='%s'",
w->defaultX,
w->defaultY,
qPrintable(w->justify),
qPrintable(w->hAlign),
qPrintable(w->vAlign),
qPrintable(w->words));
}
*/
int nWordsHeader = 0; // number of credit-words in the header
int nWordsFooter = 0; // number of credit-words in the footer
for (ciCreditWords ci = credits.begin(); ci != credits.end(); ++ci) {
CreditWords* w = *ci;
double defy = w->defaultY;
// and count #words in header and footer
if (defy > ph2)
nWordsHeader++;
else
nWordsFooter++;
} // end for (ciCreditWords ...
// if there are any credit words in the header, use these
// else use the credit words in the footer (if any)
bool useHeader = nWordsHeader > 0;
bool useFooter = nWordsHeader == 0 && nWordsFooter > 0;
//qDebug("header %d footer %d useHeader %d useFooter %d",
// nWordsHeader, nWordsFooter, useHeader, useFooter);
// determine credits height and create vbox to contain them
qreal vboxHeight = 10; // default height in spatium
double miny = pageHeight;
double maxy = 0;
if (pageWidth > 1 && pageHeight > 1) {
for (ciCreditWords ci = credits.begin(); ci != credits.end(); ++ci) {
CreditWords* w = *ci;
double defy = w->defaultY;
if ((useHeader && defy > ph2) || (useFooter && defy < ph2)) {
if (defy > maxy) maxy = defy;
if (defy < miny) miny = defy;
}
}
//qDebug("miny=%g maxy=%g", miny, maxy);
if (miny < (ph - 1) && maxy > 1) { // if both miny and maxy set
double diff = maxy - miny; // calculate height in tenths
if (diff > 1 && diff < ph2) { // and size is reasonable
vboxHeight = diff;
vboxHeight /= 10; // height in spatium
vboxHeight += 2.5; // guesstimated correction for last line
}
}
}
//qDebug("vbox height %g sp", vboxHeight);
VBox* vbox = new VBox(score);
vbox->setBoxHeight(Spatium(vboxHeight));
QMap<int, CreditWords*> creditMap; // store credit-words sorted on y pos
bool creditWordsUsed = false;
for (ciCreditWords ci = credits.begin(); ci != credits.end(); ++ci) {
CreditWords* w = *ci;
double defx = w->defaultX;
double defy = w->defaultY;
// handle all credit words in the box
if ((useHeader && defy > ph2) || (useFooter && defy < ph2)) {
creditWordsUsed = true;
// composer is in the right column
if (pw2 < defx) {
// found composer
addText2(vbox, score, w->words,
SubStyleId::COMPOSER, Align::RIGHT | Align::BOTTOM,
(miny - w->defaultY) * score->spatium() / (10 * DPI));
}
// poet is in the left column
else if (defx < pw1) {
// found poet/lyricist
addText2(vbox, score, w->words,
SubStyleId::POET, Align::LEFT | Align::BOTTOM,
(miny - w->defaultY) * score->spatium() / (10 * DPI));
}
// save others (in the middle column) to be handled later
else {
creditMap.insert(defy, w);
}
}
// keep remaining footer text for possible use as copyright
else if (useHeader && defy < ph2) {
// found credit words in both header and footer
// header was used to create a vbox at the top of the first page
// footer is ignored as it conflicts with the default MuseScore footer style
//qDebug("add to copyright: '%s'", qPrintable(w->words));
}
} // end for (ciCreditWords ...
/*
QMap<int, CreditWords*>::const_iterator ci = creditMap.constBegin();
while (ci != creditMap.constEnd()) {
CreditWords* w = ci.value();
qDebug("creditMap %d credit-words defx=%g defy=%g just=%s hal=%s val=%s words=%s",
ci.key(),
w->defaultX,
w->defaultY,
qPrintable(w->justify),
qPrintable(w->hAlign),
qPrintable(w->vAlign),
qPrintable(w->words));
++ci;
}
*/
// assign title, subtitle and copyright
QList<int> keys = creditMap.uniqueKeys(); // note: ignoring credit-words at the same y pos
// if any credit-words present, the highest is the title
// note that the keys are sorted in ascending order
// -> use the last key
if (keys.size() >= 1) {
CreditWords* w = creditMap.value(keys.at(keys.size() - 1));
//qDebug("title='%s'", qPrintable(w->words));
addText2(vbox, score, w->words,
SubStyleId::TITLE, Align::HCENTER | Align::TOP,
(maxy - w->defaultY) * score->spatium() / (10 * DPI));
}
// add remaining credit-words as subtitles
for (int i = 0; i < (keys.size() - 1); i++) {
CreditWords* w = creditMap.value(keys.at(i));
//qDebug("subtitle='%s'", qPrintable(w->words));
addText2(vbox, score, w->words,
SubStyleId::SUBTITLE, Align::HCENTER | Align::TOP,
(maxy - w->defaultY) * score->spatium() / (10 * DPI));
}
// use metadata if no workable credit-words found
if (!creditWordsUsed) {
QString strTitle;
QString strSubTitle;
QString strComposer;
QString strPoet;
QString strTranslator;
if (!(score->metaTag("movementTitle").isEmpty() && score->metaTag("workTitle").isEmpty())) {
strTitle = score->metaTag("movementTitle");
if (strTitle.isEmpty())
strTitle = score->metaTag("workTitle");
}
if (!(score->metaTag("movementNumber").isEmpty() && score->metaTag("workNumber").isEmpty())) {
strSubTitle = score->metaTag("movementNumber");
if (strSubTitle.isEmpty())
strSubTitle = score->metaTag("workNumber");
}
QString metaComposer = score->metaTag("composer");
QString metaPoet = score->metaTag("poet");
QString metaTranslator = score->metaTag("translator");
if (!metaComposer.isEmpty()) strComposer = metaComposer;
if (metaPoet.isEmpty()) metaPoet = score->metaTag("lyricist");
if (!metaPoet.isEmpty()) strPoet = metaPoet;
if (!metaTranslator.isEmpty()) strTranslator = metaTranslator;
addText(vbox, score, strTitle.toHtmlEscaped(), SubStyleId::TITLE);
addText(vbox, score, strSubTitle.toHtmlEscaped(), SubStyleId::SUBTITLE);
addText(vbox, score, strComposer.toHtmlEscaped(), SubStyleId::COMPOSER);
addText(vbox, score, strPoet.toHtmlEscaped(), SubStyleId::POET);
addText(vbox, score, strTranslator.toHtmlEscaped(), SubStyleId::TRANSLATOR);
}
if (vbox) {
vbox->setTick(0);
score->measures()->add(vbox);
}
}
//---------------------------------------------------------
// fixupSigmap
//---------------------------------------------------------
/**
To enable error handling in pass2, ensure sigmap contains a valid entry at tick = 0.
Required by TimeSigMap::tickValues(), called (indirectly) by Segment::add().
*/
static void fixupSigmap(MxmlLogger* logger, Score* score, const QVector<Fraction>& measureLength)
{
auto it = score->sigmap()->find(0);
if (it == score->sigmap()->end()) {
// no valid timesig at tick = 0
logger->logDebugInfo("no valid time signature at tick = 0");
// use length of first measure instead time signature.
// if there is no first measure, we probably don't care,
// but set a default anyway.
Fraction tsig = measureLength.isEmpty() ? Fraction(4, 4) : measureLength.at(0);
score->sigmap()->add(0, tsig);
}
}
//---------------------------------------------------------
// parse
//---------------------------------------------------------
/**
Parse MusicXML in \a device and extract pass 1 data.
*/
Score::FileError MusicXMLParserPass1::parse(QIODevice* device)
{
_logger->logDebugTrace("MusicXMLParserPass1::parse device");
_parts.clear();
_e.setDevice(device);
auto res = parse();
if (res != Score::FileError::FILE_NO_ERROR)
return res;
// Determine the start tick of each measure in the part
determineMeasureLength(_measureLength);
determineMeasureStart(_measureLength, _measureStart);
// Fixup timesig at tick = 0 if necessary
fixupSigmap(_logger, _score, _measureLength);
// Create the measures
createMeasures(_score, _measureLength, _measureStart);
return res;
}
//---------------------------------------------------------
// parse
//---------------------------------------------------------
/**
Start the parsing process, after verifying the top-level node is score-partwise
*/
Score::FileError MusicXMLParserPass1::parse()
{
_logger->logDebugTrace("MusicXMLParserPass1::parse");
bool found = false;
while (_e.readNextStartElement()) {
if (_e.name() == "score-partwise") {
found = true;
scorePartwise();
}
else {
_logger->logError(QString("this is not a MusicXML score-partwise file (top-level node '%1')")
.arg(_e.name().toString()), &_e);
_e.skipCurrentElement();
return Score::FileError::FILE_BAD_FORMAT;
}
}
if (!found) {
_logger->logError("this is not a MusicXML score-partwise file, node <score-partwise> not found", &_e);
return Score::FileError::FILE_BAD_FORMAT;
}
return Score::FileError::FILE_NO_ERROR;
}
//---------------------------------------------------------
// allStaffGroupsIdentical
//---------------------------------------------------------
/**
Return true if all staves in Part \a p have the same staff group
*/
static bool allStaffGroupsIdentical(Part const* const p)
{
for (int i = 1; i < p->nstaves(); ++i) {
if (p->staff(0)->staffType(0)->group() != p->staff(i)->staffType(0)->group())
return false;
}
return true;
}
//---------------------------------------------------------
// scorePartwise
//---------------------------------------------------------
/**
Parse the MusicXML top-level (XPath /score-partwise) node.
*/
void MusicXMLParserPass1::scorePartwise()
{
Q_ASSERT(_e.isStartElement() && _e.name() == "score-partwise");
_logger->logDebugTrace("MusicXMLParserPass1::scorePartwise", &_e);
MusicXmlPartGroupList partGroupList;
CreditWordsList credits;
int pageWidth = 0; ///< Page width read from defaults
int pageHeight = 0; ///< Page height read from defaults
while (_e.readNextStartElement()) {
if (_e.name() == "part")
part();
else if (_e.name() == "part-list") {
// if any credits are present, they have been read now
// add the credits to the score before adding any measure
// note that a part-list element must always be present
doCredits(_score, credits, pageWidth, pageHeight);
// and read the part list
partList(partGroupList);
}
else if (_e.name() == "work") {
while (_e.readNextStartElement()) {
if (_e.name() == "work-number")
_score->setMetaTag("workNumber", _e.readElementText());
else if (_e.name() == "work-title")
_score->setMetaTag("workTitle", _e.readElementText());
else
skipLogCurrElem();
}
}
else if (_e.name() == "identification")
identification();
else if (_e.name() == "defaults")
defaults(pageWidth, pageHeight);
else if (_e.name() == "movement-number")
_score->setMetaTag("movementNumber", _e.readElementText());
else if (_e.name() == "movement-title")
_score->setMetaTag("movementTitle", _e.readElementText());
else if (_e.name() == "credit")
credit(credits);
else
skipLogCurrElem();
}
// add brackets where required
/*
qDebug("partGroupList");
for (int i = 0; i < (int) partGroupList.size(); i++) {
MusicXmlPartGroup* pg = partGroupList[i];
qDebug("part-group span %d start %d type %hhd barlinespan %d",
pg->span, pg->start, pg->type, pg->barlineSpan);
}
*/
// set of (typically multi-staff) parts containing one or more explicit brackets
// spanning only that part: these won't get an implicit brace later
// e.g. a two-staff piano part with an explicit brace
QSet<Part const* const> partSet;
// handle the explicit brackets
const QList<Part*>& il = _score->parts();
for (int i = 0; i < (int) partGroupList.size(); i++) {
MusicXmlPartGroup* pg = partGroupList[i];
// add part to set
if (pg->span == 1)
partSet << il.at(pg->start);
// determine span in staves
int stavesSpan = 0;
for (int j = 0; j < pg->span; j++)
stavesSpan += il.at(pg->start + j)->nstaves();
// add bracket and set the span
// TODO: use group-symbol default-x to determine horizontal order of brackets
Staff* staff = il.at(pg->start)->staff(0);
if (pg->type == BracketType::NO_BRACKET)
staff->setBracketType(0, BracketType::NO_BRACKET);
else {
staff->addBracket(new BracketItem(staff->score(), pg->type, stavesSpan));
}
if (pg->barlineSpan)
staff->setBarLineSpan(pg->span);
}
// handle the implicit brackets:
// multi-staff parts w/o explicit brackets get a brace
foreach(Part const* const p, il) {
if (p->nstaves() > 1 && !partSet.contains(p)) {
p->staff(0)->addBracket(new BracketItem(p->score(), BracketType::BRACE, p->nstaves()));
if (allStaffGroupsIdentical(p)) {
// span only if the same types
p->staff(0)->setBarLineSpan(p->nstaves());
}
}
}
}
//---------------------------------------------------------
// identification
//---------------------------------------------------------
/**
Parse the /score-partwise/identification node:
read the metadata.
*/
void MusicXMLParserPass1::identification()
{
Q_ASSERT(_e.isStartElement() && _e.name() == "identification");
_logger->logDebugTrace("MusicXMLParserPass1::identification", &_e);
while (_e.readNextStartElement()) {
if (_e.name() == "creator") {
// type is an arbitrary label
QString strType = _e.attributes().value("type").toString();
_score->setMetaTag(strType, _e.readElementText());
}
else if (_e.name() == "rights")
_score->setMetaTag("copyright", _e.readElementText());
else if (_e.name() == "encoding") {
// TODO
_e.skipCurrentElement(); // skip but don't log
// _score->setMetaTag("encoding", _e.readElementText()); works with DOM but not with pull parser
// temporarily fake the encoding tag (compliant with DOM parser) to help the autotester
if (MScore::debugMode)
_score->setMetaTag("encoding", "MuseScore 0.7.02007-09-10");
}
else if (_e.name() == "source")
_score->setMetaTag("source", _e.readElementText());
else if (_e.name() == "miscellaneous")
// TODO
_e.skipCurrentElement(); // skip but don't log
else
skipLogCurrElem();
}
}
//---------------------------------------------------------
// text2syms
//---------------------------------------------------------
/**
Convert SMuFL code points to MuseScore <sym>...</sym>
*/
static QString text2syms(const QString& t)
{
//QTime time;
//time.start();
// first create a map from symbol (Unicode) text to symId
// note that this takes about 1 msec on a Core i5,
// caching does not gain much
ScoreFont* sf = ScoreFont::fallbackFont();
QMap<QString, SymId> map;
int maxStringSize = 0; // maximum string size found
for (int i = int(SymId::noSym); i < int(SymId::lastSym); ++i) {
SymId id((SymId(i)));
QString string(sf->toString(id));
// insert all syms except space to prevent matching all regular spaces
if (id != SymId::space)
map.insert(string, id);
if (string.size() > maxStringSize)
maxStringSize = string.size();
}
//qDebug("text2syms map count %d maxsz %d filling time elapsed: %d ms",
// map.size(), maxStringSize, time.elapsed());
// then look for matches
QString in = t;
QString res;
while (in != "") {
// try to find the largest match possible
int maxMatch = qMin(in.size(), maxStringSize);
QString sym;
while (maxMatch > 0) {
QString toBeMatched = in.left(maxMatch);
if (map.contains(toBeMatched)) {
sym = Sym::id2name(map.value(toBeMatched));
break;
}
maxMatch--;
}
if (maxMatch > 0) {
// found a match, add sym to res and remove match from string in
res += "<sym>";
res += sym;
res += "</sym>";
in.remove(0, maxMatch);
}
else {
// not found, move one char from res to in
res += in.left(1);
in.remove(0, 1);
}
}
//qDebug("text2syms total time elapsed: %d ms, res '%s'", time.elapsed(), qPrintable(res));
return res;
}
//---------------------------------------------------------
// decodeEntities
//---------------------------------------------------------
/**
Decode &#...; in string \a src into UNICODE (utf8) character.
*/
static QString decodeEntities( const QString& src )
{
QString ret(src);
QRegExp re("&#([0-9]+);");
re.setMinimal(true);
int pos = 0;
while ( (pos = re.indexIn(src, pos)) != -1 ) {
ret = ret.replace(re.cap(0), QChar(re.cap(1).toInt(0,10)));
pos += re.matchedLength();
}
return ret;
}
//---------------------------------------------------------
// nextPartOfFormattedString
//---------------------------------------------------------
// TODO: probably should be shared between pass 1 and 2
/**
Read the next part of a MusicXML formatted string and convert to MuseScore internal encoding.
*/
static QString nextPartOfFormattedString(QXmlStreamReader& e)
{
//QString lang = e.attribute(QString("xml:lang"), "it");
QString fontWeight = e.attributes().value("font-weight").toString();
QString fontSize = e.attributes().value("font-size").toString();
QString fontStyle = e.attributes().value("font-style").toString();
QString underline = e.attributes().value("underline").toString();
QString fontFamily = e.attributes().value("font-family").toString();
// TODO: color, enclosure, yoffset in only part of the text, ...
QString txt = e.readElementText();
// replace HTML entities
txt = decodeEntities(txt);
QString syms = text2syms(txt);
QString importedtext;
if (!fontSize.isEmpty()) {
bool ok = true;
float size = fontSize.toFloat(&ok);
if (ok)
importedtext += QString("<font size=\"%1\"/>").arg(size);
}
if (!fontFamily.isEmpty() && txt == syms) {
// add font family only if no <sym> replacement made
importedtext += QString("<font face=\"%1\"/>").arg(fontFamily);
}
if (fontWeight == "bold")
importedtext += "<b>";
if (fontStyle == "italic")
importedtext += "<i>";
if (!underline.isEmpty()) {
bool ok = true;
int lines = underline.toInt(&ok);
if (ok && (lines > 0)) // 1,2, or 3 underlines are imported as single underline
importedtext += "<u>";
else
underline = "";
}
if (txt == syms) {
txt.replace(QString("\r"), QString("")); // convert Windows line break \r\n -> \n
importedtext += txt.toHtmlEscaped();
}
else {
// <sym> replacement made, should be no need for line break or other conversions
importedtext += syms;
}
if (underline != "")
importedtext += "</u>";
if (fontStyle == "italic")
importedtext += "</i>";
if (fontWeight == "bold")
importedtext += "</b>";
//qDebug("importedtext '%s'", qPrintable(importedtext));
return importedtext;
}
//---------------------------------------------------------
// credit
//---------------------------------------------------------
/**
Parse the /score-partwise/credit node:
read the credits for later handling by doCredits().
*/
void MusicXMLParserPass1::credit(CreditWordsList& credits)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "credit");
_logger->logDebugTrace("MusicXMLParserPass1::credit", &_e);
QString page = _e.attributes().value("page").toString();
// handle only page 1 credits (to extract title etc.)
// assume no page attribute means page 1
if (page == "" || page == "1") {
// multiple credit-words elements may be present,
// which are appended
// use the position info from the first one
// font information is ignored, credits will be styled
bool creditWordsRead = false;
double defaultx = 0;
double defaulty = 0;
QString justify;
QString halign;
QString valign;
QString crwords;
while (_e.readNextStartElement()) {
if (_e.name() == "credit-words") {
// IMPORT_LAYOUT
if (!creditWordsRead) {
defaultx = _e.attributes().value("default-x").toString().toDouble();
defaulty = _e.attributes().value("default-y").toString().toDouble();
justify = _e.attributes().value("justify").toString();
halign = _e.attributes().value("halign").toString();
valign = _e.attributes().value("valign").toString();
creditWordsRead = true;
}
crwords += nextPartOfFormattedString(_e);
}
else if (_e.name() == "credit-type")
_e.skipCurrentElement(); // skip but don't log
else
skipLogCurrElem();
}
if (crwords != "") {
CreditWords* cw = new CreditWords(defaultx, defaulty, justify, halign, valign, crwords);
credits.append(cw);
}
}
else
_e.skipCurrentElement(); // skip but don't log
Q_ASSERT(_e.isEndElement() && _e.name() == "credit");
}
//---------------------------------------------------------
// mustSetSize
//---------------------------------------------------------
/**
Determine if i is a style type for which the default size must be set
*/
// The MusicXML specification does not specify to which kinds of text
// the word-font setting applies. Setting all sizes to the size specified
// gives bad results, e.g. for measure numbers, so a selection is made.
// Some tweaking may still be required.
#if 0
static bool mustSetSize(const int i)
{
return
i == int(SubStyleId::TITLE)
|| i == int(SubStyleId::SUBTITLE)
|| i == int(SubStyleId::COMPOSER)
|| i == int(SubStyleId::POET)
|| i == int(SubStyleId::INSTRUMENT_LONG)
|| i == int(SubStyleId::INSTRUMENT_SHORT)
|| i == int(SubStyleId::INSTRUMENT_EXCERPT)
|| i == int(SubStyleId::TEMPO)
|| i == int(SubStyleId::METRONOME)
|| i == int(SubStyleId::TRANSLATOR)
|| i == int(SubStyleId::SYSTEM)
|| i == int(SubStyleId::STAFF)
|| i == int(SubStyleId::REPEAT_LEFT)
|| i == int(SubStyleId::REPEAT_RIGHT)
|| i == int(SubStyleId::TEXTLINE)
|| i == int(SubStyleId::GLISSANDO)
|| i == int(SubStyleId::INSTRUMENT_CHANGE);
}
#endif
//---------------------------------------------------------
// updateStyles
//---------------------------------------------------------
/**
Update the style definitions to match the MusicXML word-font and lyric-font.
*/
static void updateStyles(Score* score,
const QString& /*wordFamily*/, const QString& /*wordSize*/,
const QString& lyricFamily, const QString& lyricSize)
{
//TODO:ws const float fWordSize = wordSize.toFloat(); // note conversion error results in value 0.0
const float fLyricSize = lyricSize.toFloat(); // but avoid comparing float with exact value later
// loop over all text styles (except the empty, always hidden, first one)
// set all text styles to the MusicXML defaults
#if 0 // TODO:ws
for (int i = int(SubStyleId::DEFAULT) + 1; i < int(SubStyleId::TEXT_STYLES); ++i) {
TextStyle ts = score->style().textStyle(TextStyleType(i));
if (i == int(SubStyleId::LYRIC1) || i == int(SubStyleId::LYRIC2)) {
if (lyricFamily != "")
ts.setFamily(lyricFamily);
if (fLyricSize > 0.001)
ts.setSize(fLyricSize);
}
else {
if (wordFamily != "")
ts.setFamily(wordFamily);
if (fWordSize > 0.001 && mustSetSize(i))
ts.setSize(fWordSize);
}
score->style().setTextStyle(ts);
}
#endif
if (lyricFamily != "") {
score->style().set(Sid::lyricsOddFontFace, lyricFamily);
score->style().set(Sid::lyricsEvenFontFace, lyricFamily);
}
if (fLyricSize > 0.001) {
score->style().set(Sid::lyricsOddFontSize, QVariant(fLyricSize));
score->style().set(Sid::lyricsEvenFontSize, QVariant(fLyricSize));
}
}
//---------------------------------------------------------
// defaults
//---------------------------------------------------------
/**
Parse the /score-partwise/defaults node:
read the general score layout settings.
*/
void MusicXMLParserPass1::defaults(int& pageWidth, int& pageHeight)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "defaults");
//_logger->logDebugTrace("MusicXMLParserPass1::defaults", &_e);
double millimeter = _score->spatium()/10.0;
double tenths = 1.0;
QString lyricFontFamily;
QString lyricFontSize;
QString wordFontFamily;
QString wordFontSize;
while (_e.readNextStartElement()) {
if (_e.name() == "appearance")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "scaling") {
while (_e.readNextStartElement()) {
if (_e.name() == "millimeters")
millimeter = _e.readElementText().toDouble();
else if (_e.name() == "tenths")
tenths = _e.readElementText().toDouble();
else
skipLogCurrElem();
}
double _spatium = DPMM * (millimeter * 10.0 / tenths);
if (preferences.getBool(PREF_IMPORT_MUSICXML_IMPORTLAYOUT))
_score->setSpatium(_spatium);
}
else if (_e.name() == "page-layout") {
PageFormat pf;
pageLayout(pf, millimeter / (tenths * INCH), pageWidth, pageHeight);
//TODO:ws if (preferences.musicxmlImportLayout)
// _score->setPageFormat(pf);
}
else if (_e.name() == "system-layout") {
while (_e.readNextStartElement()) {
if (_e.name() == "system-dividers")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "system-margins")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "system-distance") {
Spatium val(_e.readElementText().toDouble() / 10.0);
if (preferences.getBool(PREF_IMPORT_MUSICXML_IMPORTLAYOUT)) {
_score->style().set(Sid::minSystemDistance, val);
//qDebug("system distance %f", val.val());
}
}
else if (_e.name() == "top-system-distance")
_e.skipCurrentElement(); // skip but don't log
else
skipLogCurrElem();
}
}
else if (_e.name() == "staff-layout") {
while (_e.readNextStartElement()) {
if (_e.name() == "staff-distance") {
Spatium val(_e.readElementText().toDouble() / 10.0);
if (preferences.getBool(PREF_IMPORT_MUSICXML_IMPORTLAYOUT))
_score->style().set(Sid::staffDistance, val);
}
else
skipLogCurrElem();
}
}
else if (_e.name() == "music-font")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "word-font") {
wordFontFamily = _e.attributes().value("font-family").toString();
wordFontSize = _e.attributes().value("font-size").toString();
_e.skipCurrentElement();
}
else if (_e.name() == "lyric-font") {
lyricFontFamily = _e.attributes().value("font-family").toString();
lyricFontSize = _e.attributes().value("font-size").toString();
_e.skipCurrentElement();
}
else if (_e.name() == "lyric-language")
_e.skipCurrentElement(); // skip but don't log
else
skipLogCurrElem();
}
/*
qDebug("word font family '%s' size '%s' lyric font family '%s' size '%s'",
qPrintable(wordFontFamily), qPrintable(wordFontSize),
qPrintable(lyricFontFamily), qPrintable(lyricFontSize));
*/
updateStyles(_score, wordFontFamily, wordFontSize, lyricFontFamily, lyricFontSize);
_score->setDefaultsRead(true); // TODO only if actually succeeded ?
}
//---------------------------------------------------------
// pageLayout
//---------------------------------------------------------
/**
Parse the /score-partwise/defaults/page-layout node:
read the page layout.
*/
void MusicXMLParserPass1::pageLayout(PageFormat& pf, const qreal conversion,
int& pageWidth, int& pageHeight)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "page-layout");
_logger->logDebugTrace("MusicXMLParserPass1::pageLayout", &_e);
qreal _oddRightMargin = 0.0;
qreal _evenRightMargin = 0.0;
QSizeF size;
while (_e.readNextStartElement()) {
if (_e.name() == "page-margins") {
QString type = _e.attributes().value("type").toString();
if (type == "")
type = "both";
qreal lm = 0.0, rm = 0.0, tm = 0.0, bm = 0.0;
while (_e.readNextStartElement()) {
if (_e.name() == "left-margin")
lm = _e.readElementText().toDouble() * conversion;
else if (_e.name() == "right-margin")
rm = _e.readElementText().toDouble() * conversion;
else if (_e.name() == "top-margin")
tm = _e.readElementText().toDouble() * conversion;
else if (_e.name() == "bottom-margin")
bm = _e.readElementText().toDouble() * conversion;
else
skipLogCurrElem();
}
pf.twosided = type == "odd" || type == "even";
if (type == "odd" || type == "both") {
pf.oddLeftMargin = lm;
_oddRightMargin = rm;
pf.oddTopMargin = tm;
pf.oddBottomMargin = bm;
}
if (type == "even" || type == "both") {
pf.evenLeftMargin = lm;
_evenRightMargin = rm;
pf.evenTopMargin = tm;
pf.evenBottomMargin = bm;
}
}
else if (_e.name() == "page-height") {
double val = _e.readElementText().toDouble();
size.rheight() = val * conversion;
// set pageHeight and pageWidth for use by doCredits()
pageHeight = static_cast<int>(val + 0.5);
}
else if (_e.name() == "page-width") {
double val = _e.readElementText().toDouble();
size.rwidth() = val * conversion;
// set pageHeight and pageWidth for use by doCredits()
pageWidth = static_cast<int>(val + 0.5);
}
else
skipLogCurrElem();
}
pf.size = size;
qreal w1 = size.width() - pf.oddLeftMargin - _oddRightMargin;
qreal w2 = size.width() - pf.evenLeftMargin - _evenRightMargin;
pf.printableWidth = qMax(w1, w2); // silently adjust right margins
}
//---------------------------------------------------------
// partList
//---------------------------------------------------------
/**
Parse the /score-partwise/part-list:
create the parts and for each part set id and name.
Also handle the part-groups.
*/
void MusicXMLParserPass1::partList(MusicXmlPartGroupList& partGroupList)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "part-list");
_logger->logDebugTrace("MusicXMLParserPass1::partList", &_e);
int scoreParts = 0; // number of score-parts read sofar
MusicXmlPartGroupMap partGroups;
while (_e.readNextStartElement()) {
if (_e.name() == "part-group")
partGroup(scoreParts, partGroupList, partGroups);
else if (_e.name() == "score-part") {
scorePart();
scoreParts++;
}
else
skipLogCurrElem();
}
}
//---------------------------------------------------------
// createPart
//---------------------------------------------------------
/**
Create the part, set its \a id and insert it in PartMap \a pm.
Part name (if any) will be set later.
*/
static void createPart(Score* score, const QString& id, PartMap& pm)
{
Part* part = new Part(score);
pm.insert(id, part);
part->setId(id);
score->appendPart(part);
Staff* staff = new Staff(score);
staff->setPart(part);
part->staves()->push_back(staff);
score->staves().push_back(staff);
// TODO TBD tuplets.resize(VOICES); // part now contains one staff, thus VOICES voices
}
//---------------------------------------------------------
// partGroupStart
//---------------------------------------------------------
typedef std::map<int,MusicXmlPartGroup*> MusicXmlPartGroupMap;
/**
Store part-group start with number \a n, first part \a p and symbol / \a s in the partGroups
map \a pgs for later reference, as at this time insufficient information is available to be able
to generate the brackets.
*/
static void partGroupStart(MusicXmlPartGroupMap& pgs, int n, int p, QString s, bool barlineSpan)
{
//qDebug("partGroupStart number=%d part=%d symbol=%s", n, p, qPrintable(s));
if (pgs.count(n) > 0) {
qDebug("part-group number=%d already active", n);
return;
}
BracketType bracketType = BracketType::NO_BRACKET;
if (s == "")
; // ignore (handle as NO_BRACKET)
else if (s == "none")
; // already set to NO_BRACKET
else if (s == "brace")
bracketType = BracketType::BRACE;
else if (s == "bracket")
bracketType = BracketType::NORMAL;
else if (s == "line")
bracketType = BracketType::LINE;
else if (s == "square")
bracketType = BracketType::SQUARE;
else {
qDebug("part-group symbol=%s not supported", qPrintable(s));
return;
}
MusicXmlPartGroup* pg = new MusicXmlPartGroup;
pg->span = 0;
pg->start = p;
pg->barlineSpan = barlineSpan,
pg->type = bracketType;
pgs[n] = pg;
}
//---------------------------------------------------------
// partGroupStop
//---------------------------------------------------------
/**
Handle part-group stop with number \a n and part \a p.
For part group n, the start part, span (in parts) and type are now known.
To generate brackets, the span in staves must also be known.
*/
static void partGroupStop(MusicXmlPartGroupMap& pgs, int n, int p,
MusicXmlPartGroupList& pgl)
{
if (pgs.count(n) == 0) {
qDebug("part-group number=%d not active", n);
return;
}
pgs[n]->span = p - pgs[n]->start;
//qDebug("partgroupstop number=%d start=%d span=%d type=%hhd",
// n, pgs[n]->start, pgs[n]->span, pgs[n]->type);
pgl.push_back(pgs[n]);
pgs.erase(n);
}
//---------------------------------------------------------
// partGroup
//---------------------------------------------------------
/**
Parse the /score-partwise/part-list/part-group node.
*/
void MusicXMLParserPass1::partGroup(const int scoreParts,
MusicXmlPartGroupList& partGroupList,
MusicXmlPartGroupMap& partGroups)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "part-group");
_logger->logDebugTrace("MusicXMLParserPass1::partGroup", &_e);
bool barlineSpan = true;
int number = _e.attributes().value("number").toInt();
if (number > 0) number--;
QString symbol = "";
QString type = _e.attributes().value("type").toString();
while (_e.readNextStartElement()) {
if (_e.name() == "group-name")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "group-abbreviation")
symbol = _e.readElementText();
else if (_e.name() == "group-symbol")
symbol = _e.readElementText();
else if (_e.name() == "group-barline") {
if (_e.readElementText() == "no")
barlineSpan = false;
}
else
skipLogCurrElem();
}
if (type == "start")
partGroupStart(partGroups, number, scoreParts, symbol, barlineSpan);
else if (type == "stop")
partGroupStop(partGroups, number, scoreParts, partGroupList);
else
_logger->logError(QString("part-group type '%1' not supported").arg(type), &_e);
}
//---------------------------------------------------------
// findInstrument
//---------------------------------------------------------
/**
Find the first InstrumentTemplate with musicXMLid instrSound
and a non-empty set of channels.
*/
#if 0 // not used
static const InstrumentTemplate* findInstrument(const QString& instrSound)
{
const InstrumentTemplate* instr = nullptr;
for (const InstrumentGroup* group : instrumentGroups) {
for (const InstrumentTemplate* templ : group->instrumentTemplates) {
if (templ->musicXMLid == instrSound && !templ->channel.isEmpty()) {
return templ;
}
}
}
return instr;
}
#endif
//---------------------------------------------------------
// scorePart
//---------------------------------------------------------
/**
Parse the /score-partwise/part-list/score-part node:
create the part and sets id and name.
Note that a part is created even if no part-name is present
which is invalid MusicXML but is (sometimes ?) generated by NWC2MusicXML.
*/
void MusicXMLParserPass1::scorePart()
{
Q_ASSERT(_e.isStartElement() && _e.name() == "score-part");
_logger->logDebugTrace("MusicXMLParserPass1::scorePart", &_e);
QString id = _e.attributes().value("id").toString();
if (_parts.contains(id)) {
_logger->logError(QString("duplicate part id '%1'").arg(id), &_e);
skipLogCurrElem();
return;
}
else {
_parts.insert(id, MusicXmlPart(id));
_drumsets.insert(id, MusicXMLDrumset());
createPart(_score, id, _partMap);
}
while (_e.readNextStartElement()) {
if (_e.name() == "part-name") {
// Element part-name contains the displayed (full) part name
// It is displayed by default, but can be suppressed (print-object=”no”)
// As of MusicXML 3.0, formatting is deprecated, with part-name in plain text
// and the formatted version in the part-name-display element
_parts[id].setPrintName(!(_e.attributes().value("print-object") == "no"));
QString name = _e.readElementText();
_parts[id].setName(name);
}
else if (_e.name() == "part-name-display") {
// TODO
_e.skipCurrentElement(); // skip but don't log
}
else if (_e.name() == "part-abbreviation") {
// Element part-name contains the displayed (abbreviated) part name
// It is displayed by default, but can be suppressed (print-object=”no”)
// As of MusicXML 3.0, formatting is deprecated, with part-name in plain text
// and the formatted version in the part-abbreviation-display element
_parts[id].setPrintAbbr(!(_e.attributes().value("print-object") == "no"));
QString name = _e.readElementText();
_parts[id].setAbbr(name);
}
else if (_e.name() == "part-abbreviation-display")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "score-instrument")
scoreInstrument(id);
else if (_e.name() == "midi-device") {
if (!_e.attributes().hasAttribute("port")) {
_e.readElementText(); // empty string
continue;
}
QString instrId = _e.attributes().value("id").toString();
QString port = _e.attributes().value("port").toString();
// If instrId is missing, the device assignment affects all
// score-instrument elements in the score-part
if (instrId.isEmpty()) {
for (auto it = _drumsets[id].cbegin(); it != _drumsets[id].cend(); ++it)
_drumsets[id][it.key()].midiPort = port.toInt() - 1;
}
else if (_drumsets[id].contains(instrId))
_drumsets[id][instrId].midiPort = port.toInt() - 1;
_e.readElementText(); // empty string
}
else if (_e.name() == "midi-instrument")
midiInstrument(id);
else
skipLogCurrElem();
}
Q_ASSERT(_e.isEndElement() && _e.name() == "score-part");
}
//---------------------------------------------------------
// scoreInstrument
//---------------------------------------------------------
/**
Parse the /score-partwise/part-list/score-part/score-instrument node.
*/
void MusicXMLParserPass1::scoreInstrument(const QString& partId)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "score-instrument");
_logger->logDebugTrace("MusicXMLParserPass1::scoreInstrument", &_e);
QString instrId = _e.attributes().value("id").toString();
while (_e.readNextStartElement()) {
if (_e.name() == "ensemble")
skipLogCurrElem();
else if (_e.name() == "instrument-name") {
QString instrName = _e.readElementText();
/*
qDebug("partId '%s' instrId '%s' instrName '%s'",
qPrintable(partId),
qPrintable(instrId),
qPrintable(instrName)
);
*/
_drumsets[partId].insert(instrId, MusicXMLDrumInstrument(instrName));
// Element instrument-name is typically not displayed in the score,
// but used only internally
if (_drumsets[partId].contains(instrId))
_drumsets[partId][instrId].name = instrName;
}
else if (_e.name() == "instrument-sound") {
QString instrSound = _e.readElementText();
if (_drumsets[partId].contains(instrId))
_drumsets[partId][instrId].sound = instrSound;
}
else if (_e.name() == "virtual-instrument") {
while (_e.readNextStartElement()) {
if (_e.name() == "virtual-library") {
QString virtualLibrary = _e.readElementText();
if (_drumsets[partId].contains(instrId))
_drumsets[partId][instrId].virtLib = virtualLibrary;
}
else if (_e.name() == "virtual-name") {
QString virtualName = _e.readElementText();
if (_drumsets[partId].contains(instrId))
_drumsets[partId][instrId].virtName = virtualName;
}
else
skipLogCurrElem();
}
}
else
skipLogCurrElem();
}
Q_ASSERT(_e.isEndElement() && _e.name() == "score-instrument");
}
//---------------------------------------------------------
// midiInstrument
//---------------------------------------------------------
/**
Parse the /score-partwise/part-list/score-part/midi-instrument node.
*/
void MusicXMLParserPass1::midiInstrument(const QString& partId)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "midi-instrument");
_logger->logDebugTrace("MusicXMLParserPass1::midiInstrument", &_e);
QString instrId = _e.attributes().value("id").toString();
while (_e.readNextStartElement()) {
if (_e.name() == "midi-bank")
skipLogCurrElem();
else if (_e.name() == "midi-channel") {
int channel = _e.readElementText().toInt();
if (channel < 1) {
_logger->logError(QString("incorrect midi-channel: %1").arg(channel), &_e);
channel = 1;
}
else if (channel > 16) {
_logger->logError(QString("incorrect midi-channel: %1").arg(channel), &_e);
channel = 16;
}
if (_drumsets[partId].contains(instrId))
_drumsets[partId][instrId].midiChannel = channel - 1;
}
else if (_e.name() == "midi-program") {
int program = _e.readElementText().toInt();
// Bug fix for Cubase 6.5.5 which generates <midi-program>0</midi-program>
// Check program number range
if (program < 1) {
_logger->logError(QString("incorrect midi-program: %1").arg(program), &_e);
program = 1;
}
else if (program > 128) {
_logger->logError(QString("incorrect midi-program: %1").arg(program), &_e);
program = 128;
}
if (_drumsets[partId].contains(instrId))
_drumsets[partId][instrId].midiProgram = program - 1;
}
else if (_e.name() == "midi-unpitched") {
if (_drumsets[partId].contains(instrId))
_drumsets[partId][instrId].pitch = _e.readElementText().toInt() - 1;
}
else if (_e.name() == "volume") {
double vol = _e.readElementText().toDouble();
if (vol >= 0 && vol <= 100) {
if (_drumsets[partId].contains(instrId))
_drumsets[partId][instrId].midiVolume = static_cast<int>((vol / 100) * 127);
}
else
_logger->logError(QString("incorrect midi-volume: %1").arg(vol), &_e);
}
else if (_e.name() == "pan") {
double pan = _e.readElementText().toDouble();
if (pan >= -90 && pan <= 90) {
if (_drumsets[partId].contains(instrId))
_drumsets[partId][instrId].midiPan = static_cast<int>(((pan + 90) / 180) * 127);
}
else
_logger->logError(QString("incorrect midi-volume: %g1").arg(pan), &_e);
}
else
skipLogCurrElem();
}
Q_ASSERT(_e.isEndElement() && _e.name() == "midi-instrument");
}
//---------------------------------------------------------
// setNumberOfStavesForPart
//---------------------------------------------------------
/**
Set number of staves for part \a partId to the max value
of the current value \a staves.
*/
static void setNumberOfStavesForPart(Part* const part, const int staves)
{
Q_ASSERT(part);
if (staves > part->nstaves())
part->setStaves(staves);
}
//---------------------------------------------------------
// part
//---------------------------------------------------------
/**
Parse the /score-partwise/part node:
read the parts data to determine measure timing and octave shifts.
Assign voices and staves.
*/
void MusicXMLParserPass1::part()
{
Q_ASSERT(_e.isStartElement() && _e.name() == "part");
_logger->logDebugTrace("MusicXMLParserPass1::part", &_e);
const QString id = _e.attributes().value("id").toString();
if (!_parts.contains(id)) {
_logger->logError(QString("cannot find part '%1'").arg(id), &_e);
skipLogCurrElem();
}
initPartState(id);
VoiceOverlapDetector vod;
Fraction time; // current time within part
Fraction mdur; // measure duration
while (_e.readNextStartElement()) {
if (_e.name() == "measure") {
measure(id, time, mdur, vod);
time += mdur;
}
else
skipLogCurrElem();
}
// Bug fix for Cubase 6.5.5..9.5.10 which generate <staff>2</staff> in a single staff part
setNumberOfStavesForPart(_partMap.value(id), _parts[id].maxStaff());
// allocate MuseScore staff to MusicXML voices
allocateStaves(_parts[id].voicelist);
// allocate MuseScore voice to MusicXML voices
allocateVoices(_parts[id].voicelist);
// calculate the octave shifts
_parts[id].calcOctaveShifts();
// set first instrument for multi-instrument part starting with rest
if (_firstInstrId != "" && _firstInstrSTime > Fraction(0, 1))
_parts[id]._instrList.setInstrument(_firstInstrId, Fraction(0, 1));
// determine the lyric numbers for this part
_parts[id].lyricNumberHandler().determineLyricNos();
// debug: print results
//qDebug("%s", qPrintable(_parts[id].toString()));
//qDebug("lyric numbers: %s", qPrintable(_parts[id].lyricNumberHandler().toString()));
/*
qDebug("instrument map:");
for (auto& instr: _parts[id]._instrList) {
qDebug("%s %s", qPrintable(instr.first.print()), qPrintable(instr.second));
}
*/
/*
qDebug("voiceMapperStats: new staff");
VoiceList& vl = _parts[id].voicelist;
for (auto i = vl.constBegin(); i != vl.constEnd(); ++i) {
qDebug("voiceMapperStats: voice %s staff data %s",
qPrintable(i.key()), qPrintable(i.value().toString()));
}
*/
}
//---------------------------------------------------------
// measureDurationAsFraction
//---------------------------------------------------------
/**
Determine a suitable measure duration value given the time signature
by setting the duration denominator to be greater than or equal
to the time signature denominator
*/
static Fraction measureDurationAsFraction(const Fraction length, const int tsigtype)
{
if (tsigtype <= 0)
// invalid tsigtype
return length;
Fraction res = length;
while (res.denominator() < tsigtype) {
res.setNumerator(res.numerator() * 2);
res.setDenominator(res.denominator() * 2);
}
return res;
}
//---------------------------------------------------------
// measure
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure node:
read the measures data as required to determine measure timing, octave shifts
and assign voices and staves.
*/
void MusicXMLParserPass1::measure(const QString& partId,
const Fraction cTime,
Fraction& mdur,
VoiceOverlapDetector& vod)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "measure");
_logger->logDebugTrace("MusicXMLParserPass1::measure", &_e);
QString number = _e.attributes().value("number").toString();
Fraction mTime; // current time stamp within measure
Fraction mDura; // current total measure duration
vod.newMeasure();
while (_e.readNextStartElement()) {
if (_e.name() == "attributes")
attributes(partId, cTime + mTime);
else if (_e.name() == "barline")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "note") {
Fraction dura;
// note: chord and grace note handling done in note()
note(partId, cTime + mTime, dura, vod);
if (dura.isValid()) {
mTime += dura;
if (mTime > mDura)
mDura = mTime;
}
}
else if (_e.name() == "forward") {
Fraction dura;
forward(dura);
if (dura.isValid()) {
mTime += dura;
if (mTime > mDura)
mDura = mTime;
}
}
else if (_e.name() == "backup") {
Fraction dura;
backup(dura);
if (dura.isValid()) {
if (dura <= mTime)
mTime -= dura;
else {
_logger->logError("backup beyond measure start", &_e);
mTime.set(0, 1);
}
}
}
else if (_e.name() == "direction")
direction(partId, cTime + mTime);
else if (_e.name() == "harmony")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "print")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "sound")
_e.skipCurrentElement(); // skip but don't log
else
skipLogCurrElem();
/*
qDebug("mTime %s (%s) mDura %s (%s)",
qPrintable(mTime.print()),
qPrintable(mTime.reduced().print()),
qPrintable(mDura.print()),
qPrintable(mDura.reduced().print()));
*/
}
// debug vod
// vod.dump();
// copy overlap data from vod to voicelist
copyOverlapData(vod, _parts[partId].voicelist);
// measure duration fixups
mDura.reduce();
// fix for PDFtoMusic Pro v1.3.0d Build BF4E (which sometimes generates empty measures)
// if no valid length found and length according to time signature is known,
// use length according to time signature
if (mDura.isZero() && _timeSigDura.isValid() && _timeSigDura > Fraction(0, 1))
mDura = _timeSigDura;
// if necessary, round up to an integral number of 1/64s,
// to comply with MuseScores actual measure length constraints
// TODO: calculate in fraction
int length = mDura.ticks();
int correctedLength = length;
if ((length % (MScore::division/16)) != 0) {
correctedLength = ((length / (MScore::division/16)) + 1) * (MScore::division/16);
mDura = Fraction::fromTicks(correctedLength);
}
// set measure duration to a suitable value given the time signature
if (_timeSigDura.isValid() && _timeSigDura > Fraction(0, 1)) {
int btp = _timeSigDura.denominator();
if (btp > 0)
mDura = measureDurationAsFraction(mDura, btp);
}
// set return value(s)
mdur = mDura;
// set measure number and duration
/*
qDebug("part %s measure %s dura %s (%d)",
qPrintable(partId), qPrintable(number), qPrintable(mdur.print()), mdur.ticks());
*/
_parts[partId].addMeasureNumberAndDuration(number, mdur);
}
//---------------------------------------------------------
// attributes
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/attributes node.
*/
void MusicXMLParserPass1::attributes(const QString& partId, const Fraction cTime)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "attributes");
_logger->logDebugTrace("MusicXMLParserPass1::attributes", &_e);
while (_e.readNextStartElement()) {
if (_e.name() == "clef")
clef(partId);
else if (_e.name() == "divisions")
divisions();
else if (_e.name() == "key")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "instruments")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "staff-details")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "staves")
staves(partId);
else if (_e.name() == "time")
time(cTime);
else if (_e.name() == "transpose")
_e.skipCurrentElement(); // skip but don't log
else
skipLogCurrElem();
}
}
//---------------------------------------------------------
// clef
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/attributes/clef node.
Set the staff type based on clef type
TODO: check if staff type setting could be simplified
*/
void MusicXMLParserPass1::clef(const QString& partId)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "clef");
_logger->logDebugTrace("MusicXMLParserPass1::clef", &_e);
QString number = _e.attributes().value("number").toString();
int n = 0;
if (number != "") {
n = number.toInt();
if (n <= 0) {
_logger->logError(QString("invalid number %1").arg(number), &_e);
n = 0;
}
else
n--; // make zero-based
}
StaffTypes staffType = StaffTypes::STANDARD;
while (_e.readNextStartElement()) {
if (_e.name() == "line")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "sign") {
QString sign = _e.readElementText();
if (sign == "TAB")
staffType = StaffTypes::TAB_DEFAULT;
else if (sign == "percussion")
staffType = StaffTypes::PERC_DEFAULT;
}
else
skipLogCurrElem();
}
Part* part = getPart(partId);
Q_ASSERT(part);
int staves = part->nstaves();
int staffIdx = _score->staffIdx(part);
// TODO: changed for #55501, but now staff type init is shared between pass 1 and 2
// old code: if (0 <= n && n < staves && staffType != StaffTypes::STANDARD)
if (0 <= n && n < staves && staffType == StaffTypes::TAB_DEFAULT)
_score->staff(staffIdx + n)->setStaffType(0, StaffType::preset(staffType));
}
//---------------------------------------------------------
// determineTimeSig
//---------------------------------------------------------
/**
Determine the time signature based on \a beats, \a beatType and \a timeSymbol.
Sets return parameters \a st, \a bts, \a btp.
Return true if OK, false on error.
*/
// TODO: share between pass 1 and pass 2
static bool determineTimeSig(MxmlLogger* logger, const QXmlStreamReader* const xmlreader,
const QString beats, const QString beatType, const QString timeSymbol,
TimeSigType& st, int& bts, int& btp)
{
// initialize
st = TimeSigType::NORMAL;
bts = 0; // the beats (max 4 separated by "+") as integer
btp = 0; // beat-type as integer
// determine if timesig is valid
if (beats == "2" && beatType == "2" && timeSymbol == "cut") {
st = TimeSigType::ALLA_BREVE;
bts = 2;
btp = 2;
return true;
}
else if (beats == "4" && beatType == "4" && timeSymbol == "common") {
st = TimeSigType::FOUR_FOUR;
bts = 4;
btp = 4;
return true;
}
else {
if (!timeSymbol.isEmpty() && timeSymbol != "normal") {
logger->logError(QString("time symbol '%1' not recognized with beats=%2 and beat-type=%3")
.arg(timeSymbol).arg(beats).arg(beatType), xmlreader);
return false;
}
btp = beatType.toInt();
QStringList list = beats.split("+");
for (int i = 0; i < list.size(); i++)
bts += list.at(i).toInt();
}
// determine if bts and btp are valid
if (bts <= 0 || btp <=0) {
logger->logError(QString("beats=%1 and/or beat-type=%2 not recognized")
.arg(beats).arg(beatType), xmlreader);
return false;
}
return true;
}
//---------------------------------------------------------
// time
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/attributes/time node.
*/
void MusicXMLParserPass1::time(const Fraction cTime)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "time");
QString beats;
QString beatType;
QString timeSymbol = _e.attributes().value("symbol").toString();
while (_e.readNextStartElement()) {
if (_e.name() == "beats")
beats = _e.readElementText();
else if (_e.name() == "beat-type")
beatType = _e.readElementText();
else
skipLogCurrElem();
}
if (beats != "" && beatType != "") {
// determine if timesig is valid
TimeSigType st = TimeSigType::NORMAL;
int bts = 0; // total beats as integer (beats may contain multiple numbers, separated by "+")
int btp = 0; // beat-type as integer
if (determineTimeSig(_logger, &_e, beats, beatType, timeSymbol, st, bts, btp)) {
_timeSigDura = Fraction(bts, btp);
_score->sigmap()->add(cTime.ticks(), _timeSigDura);
}
}
}
//---------------------------------------------------------
// divisions
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/attributes/divisions node.
*/
void MusicXMLParserPass1::divisions()
{
Q_ASSERT(_e.isStartElement() && _e.name() == "divisions");
_divs = _e.readElementText().toInt();
if (!(_divs > 0))
_logger->logError("illegal divisions", &_e);
}
//---------------------------------------------------------
// staves
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/attributes/staves node.
*/
void MusicXMLParserPass1::staves(const QString& partId)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "staves");
_logger->logDebugTrace("MusicXMLParserPass1::staves", &_e);
int staves = _e.readElementText().toInt();
if (!(staves > 0 && staves <= MAX_STAVES)) {
_logger->logError("illegal staves", &_e);
return;
}
setNumberOfStavesForPart(_partMap.value(partId), staves);
}
//---------------------------------------------------------
// direction
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/direction node
to be able to handle octave-shifts, as these must be interpreted
in musical order instead of in MusicXML file order.
*/
void MusicXMLParserPass1::direction(const QString& partId, const Fraction cTime)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "direction");
// note: file order is direction-type first, then staff
// this means staff is still unknown when direction-type is handled
QList<MxmlOctaveShiftDesc> starts;
QList<MxmlOctaveShiftDesc> stops;
int staff = 0;
while (_e.readNextStartElement()) {
if (_e.name() == "direction-type")
directionType(cTime, starts, stops);
else if (_e.name() == "staff") {
int nstaves = getPart(partId)->nstaves();
QString strStaff = _e.readElementText();
staff = strStaff.toInt() - 1;
if (0 <= staff && staff < nstaves)
; //qDebug("direction staff %d", staff + 1);
else {
_logger->logError(QString("invalid staff %1").arg(strStaff), &_e);
staff = 0;
}
}
else
_e.skipCurrentElement();
}
// handle the stops first
foreach (auto desc, stops) {
if (_octaveShifts.contains(desc.num)) {
MxmlOctaveShiftDesc prevDesc = _octaveShifts.value(desc.num);
if (prevDesc.tp == MxmlOctaveShiftDesc::Type::UP
|| prevDesc.tp == MxmlOctaveShiftDesc::Type::DOWN) {
// a complete pair
_parts[partId].addOctaveShift(staff, prevDesc.size, prevDesc.time);
_parts[partId].addOctaveShift(staff, -prevDesc.size, desc.time);
}
else
_logger->logError("double octave-shift stop", &_e);
_octaveShifts.remove(desc.num);
}
else
_octaveShifts.insert(desc.num, desc);
}
// then handle the starts
foreach (auto desc, starts) {
if (_octaveShifts.contains(desc.num)) {
MxmlOctaveShiftDesc prevDesc = _octaveShifts.value(desc.num);
if (prevDesc.tp == MxmlOctaveShiftDesc::Type::STOP) {
// a complete pair
_parts[partId].addOctaveShift(staff, desc.size, desc.time);
_parts[partId].addOctaveShift(staff, -desc.size, prevDesc.time);
}
else
_logger->logError("double octave-shift start", &_e);
_octaveShifts.remove(desc.num);
}
else
_octaveShifts.insert(desc.num, desc);
}
}
//---------------------------------------------------------
// directionType
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/direction/direction-type node.
*/
void MusicXMLParserPass1::directionType(const Fraction cTime,
QList<MxmlOctaveShiftDesc>& starts,
QList<MxmlOctaveShiftDesc>& stops)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "direction-type");
while (_e.readNextStartElement()) {
if (_e.name() == "octave-shift") {
QString number = _e.attributes().value("number").toString();
int n = 0;
if (number != "") {
n = number.toInt();
if (n <= 0)
_logger->logError(QString("invalid number %1").arg(number), &_e);
else
n--; // make zero-based
}
if (0 <= n && n < MAX_NUMBER_LEVEL) {
short size = _e.attributes().value("size").toShort();
QString type = _e.attributes().value("type").toString();
//qDebug("octave-shift type '%s' size %d number %d", qPrintable(type), size, n);
MxmlOctaveShiftDesc osDesc;
handleOctaveShift(cTime, type, size, osDesc);
osDesc.num = n;
if (osDesc.tp == MxmlOctaveShiftDesc::Type::UP
|| osDesc.tp == MxmlOctaveShiftDesc::Type::DOWN)
starts.append(osDesc);
else if (osDesc.tp == MxmlOctaveShiftDesc::Type::STOP)
stops.append(osDesc);
}
else {
_logger->logError(QString("invalid octave-shift number %1").arg(number), &_e);
}
_e.skipCurrentElement();
}
else
_e.skipCurrentElement();
}
Q_ASSERT(_e.isEndElement() && _e.name() == "direction-type");
}
//---------------------------------------------------------
// handleOctaveShift
//---------------------------------------------------------
void MusicXMLParserPass1::handleOctaveShift(const Fraction cTime,
const QString& type, short size,
MxmlOctaveShiftDesc& desc)
{
MxmlOctaveShiftDesc::Type tp = MxmlOctaveShiftDesc::Type::NONE;
short sz = 0;
switch (size) {
case 8: sz = 1; break;
case 15: sz = 2; break;
default:
_logger->logError(QString("invalid octave-shift size %1").arg(size), &_e);
return;
}
if (!cTime.isValid() || cTime < Fraction(0, 1))
_logger->logError("invalid current time", &_e);
if (type == "up")
tp = MxmlOctaveShiftDesc::Type::UP;
else if (type == "down") {
tp = MxmlOctaveShiftDesc::Type::DOWN;
sz *= -1;
}
else if (type == "stop")
tp = MxmlOctaveShiftDesc::Type::STOP;
else {
_logger->logError(QString("invalid octave-shift type '%1'").arg(type), &_e);
return;
}
desc = MxmlOctaveShiftDesc(tp, sz, cTime);
}
//---------------------------------------------------------
// setFirstInstr
//---------------------------------------------------------
void MusicXMLParserPass1::setFirstInstr(const QString& id, const Fraction stime)
{
// check for valid arguments
if (id == "" || !stime.isValid() || stime < Fraction(0, 1))
return;
// check for no instrument found yet or new earliest start time
// note: compare using <= to catch instrument at t=0
if (_firstInstrId == "" || stime <= _firstInstrSTime) {
_firstInstrId = id;
_firstInstrSTime = stime;
}
}
//---------------------------------------------------------
// note
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/note node.
*/
void MusicXMLParserPass1::note(const QString& partId,
const Fraction sTime,
Fraction& dura,
VoiceOverlapDetector& vod)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "note");
//_logger->logDebugTrace("MusicXMLParserPass1::note", &_e);
if (_e.attributes().value("print-spacing") == "no") {
notePrintSpacingNo(dura);
return;
}
//float alter = 0;
bool chord = false;
bool grace = false;
//int octave = -1;
bool bRest = false;
int staff = 1;
//int step = 0;
QString type;
QString voice = "1";
QString instrId;
mxmlNoteDuration mnd(_divs, _logger);
while (_e.readNextStartElement()) {
if (mnd.readProperties(_e)) {
// element handled
}
else if (_e.name() == "accidental")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "beam")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "chord") {
chord = true;
_e.readNext();
}
else if (_e.name() == "cue")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "grace") {
grace = true;
_e.readNext();
}
else if (_e.name() == "instrument") {
instrId = _e.attributes().value("id").toString();
_e.readNext();
}
else if (_e.name() == "lyric") {
const auto number = _e.attributes().value("number").toString();
_parts[partId].lyricNumberHandler().addNumber(number);
_e.skipCurrentElement();
}
else if (_e.name() == "notations")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "notehead")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "pitch")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "rest") {
bRest = true;
rest();
}
else if (_e.name() == "staff") {
auto ok = false;
auto strStaff = _e.readElementText();
staff = strStaff.toInt(&ok);
_parts[partId].setMaxStaff(staff);
Part* part = _partMap.value(partId);
Q_ASSERT(part);
if (!ok || staff <= 0 || staff > part->nstaves())
_logger->logError(QString("illegal staff '%1'").arg(strStaff), &_e);
}
else if (_e.name() == "stem")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "tie")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "type")
type = _e.readElementText();
else if (_e.name() == "unpitched")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "voice")
voice = _e.readElementText();
else
skipLogCurrElem();
}
// convert staff to zero-based
staff--;
// multi-instrument handling
setFirstInstr(instrId, sTime);
QString prevInstrId = _parts[partId]._instrList.instrument(sTime);
bool mustInsert = instrId != prevInstrId;
/*
qDebug("tick %s (%d) staff %d voice '%s' previnst='%s' instrument '%s' mustInsert %d",
qPrintable(sTime.print()),
sTime.ticks(),
staff + 1,
qPrintable(voice),
qPrintable(prevInstrId),
qPrintable(instrId),
mustInsert
);
*/
if (mustInsert)
_parts[partId]._instrList.setInstrument(instrId, sTime);
// check for timing error(s) and set dura
// keep in this order as checkTiming() might change dura
auto errorStr = mnd.checkTiming(type, bRest, grace);
dura = mnd.dura();
if (errorStr != "")
_logger->logError(errorStr, &_e);
// don't count chord or grace note duration
// note that this does not check the MusicXML requirement that notes in a chord
// cannot have a duration longer than the first note in the chord
if (chord || grace)
dura.set(0, 1);
// store result
if (dura.isValid() && dura > Fraction(0, 1)) {
// count the chords
if (!_parts.value(partId).voicelist.contains(voice)) {
VoiceDesc vs;
_parts[partId].voicelist.insert(voice, vs);
}
_parts[partId].voicelist[voice].incrChordRests(staff);
// determine note length for voice overlap detection
// TODO
vod.addNote(sTime.ticks(), (sTime + dura).ticks(), voice, staff);
}
Q_ASSERT(_e.isEndElement() && _e.name() == "note");
}
//---------------------------------------------------------
// notePrintSpacingNo
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/note node for a note with print-spacing="no".
These are handled like a forward: only moving the time forward.
*/
void MusicXMLParserPass1::notePrintSpacingNo(Fraction& dura)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "note");
//_logger->logDebugTrace("MusicXMLParserPass1::notePrintSpacingNo", &_e);
bool chord = false;
bool grace = false;
while (_e.readNextStartElement()) {
if (_e.name() == "chord") {
chord = true;
_e.readNext();
}
else if (_e.name() == "duration")
duration(dura);
else if (_e.name() == "grace") {
grace = true;
_e.readNext();
}
else
_e.skipCurrentElement(); // skip but don't log
}
// don't count chord or grace note duration
// note that this does not check the MusicXML requirement that notes in a chord
// cannot have a duration longer than the first note in the chord
if (chord || grace)
dura.set(0, 1);
Q_ASSERT(_e.isEndElement() && _e.name() == "note");
}
//---------------------------------------------------------
// duration
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/note/duration node.
*/
void MusicXMLParserPass1::duration(Fraction& dura)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "duration");
//_logger->logDebugTrace("MusicXMLParserPass1::duration", &_e);
dura.set(0, 0); // invalid unless set correctly
int intDura = _e.readElementText().toInt();
if (intDura > 0) {
if (_divs > 0) {
dura.set(intDura, 4 * _divs);
dura.reduce(); // prevent overflow in later Fraction operations
}
else
_logger->logError("illegal or uninitialized divisions", &_e);
}
else
_logger->logError("illegal duration", &_e);
//qDebug("duration %s valid %d", qPrintable(dura.print()), dura.isValid());
}
//---------------------------------------------------------
// forward
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/note/forward node.
*/
void MusicXMLParserPass1::forward(Fraction& dura)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "forward");
//_logger->logDebugTrace("MusicXMLParserPass1::forward", &_e);
while (_e.readNextStartElement()) {
if (_e.name() == "duration")
duration(dura);
else if (_e.name() == "staff")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "voice")
_e.skipCurrentElement(); // skip but don't log
else
skipLogCurrElem();
}
}
//---------------------------------------------------------
// backup
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/note/backup node.
*/
void MusicXMLParserPass1::backup(Fraction& dura)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "backup");
//_logger->logDebugTrace("MusicXMLParserPass1::backup", &_e);
while (_e.readNextStartElement()) {
if (_e.name() == "duration")
duration(dura);
else
skipLogCurrElem();
}
}
//---------------------------------------------------------
// timeModification
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/note/time-modification node.
*/
void MusicXMLParserPass1::timeModification(Fraction& timeMod)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "time-modification");
//_logger->logDebugTrace("MusicXMLParserPass1::timeModification", &_e);
int intActual = 0;
int intNormal = 0;
QString strActual;
QString strNormal;
while (_e.readNextStartElement()) {
if (_e.name() == "actual-notes")
strActual = _e.readElementText();
else if (_e.name() == "normal-notes")
strNormal = _e.readElementText();
else
skipLogCurrElem();
}
intActual = strActual.toInt();
intNormal = strNormal.toInt();
if (intActual > 0 && intNormal > 0)
timeMod.set(intNormal, intActual);
else {
timeMod.set(1, 1);
_logger->logError(QString("illegal time-modification: actual-notes %1 normal-notes %2")
.arg(strActual).arg(strNormal), &_e);
}
}
//---------------------------------------------------------
// rest
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/note/rest node.
*/
void MusicXMLParserPass1::rest()
{
Q_ASSERT(_e.isStartElement() && _e.name() == "rest");
//_logger->logDebugTrace("MusicXMLParserPass1::rest", &_e);
while (_e.readNextStartElement()) {
if (_e.name() == "display-octave")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "display-step")
_e.skipCurrentElement(); // skip but don't log
else
skipLogCurrElem();
}
}
} // namespace Ms