MuseScore/mscore/importmxmlpass1.cpp

2908 lines
106 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/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 "importmxmlpass1.h"
#include "importmxmlpass2.h"
#include "preferences.h"
namespace Ms {
//---------------------------------------------------------
// noteTypeToFraction
//---------------------------------------------------------
/**
Convert MusicXML note type to fraction.
*/
static Fraction noteTypeToFraction(const QString& type)
{
if (type == "1024th")
return Fraction(1, 1024);
else if (type == "512th")
return Fraction(1, 512);
else if (type == "256th")
return Fraction(1, 256);
else if (type == "128th")
return Fraction(1, 128);
else if (type == "64th")
return Fraction(1, 64);
else if (type == "32nd")
return Fraction(1, 32);
else if (type == "16th")
return Fraction(1, 16);
else if (type == "eighth")
return Fraction(1, 8);
else if (type == "quarter")
return Fraction(1, 4);
else if (type == "half")
return Fraction(1, 2);
else if (type == "whole")
return Fraction(1, 1);
else if (type == "breve")
return Fraction(2, 1);
else if (type == "long")
return Fraction(4, 1);
else if (type == "maxima")
return Fraction(8, 1);
else
return Fraction(0, 0);
}
//---------------------------------------------------------
// calculateFraction
//---------------------------------------------------------
/**
Convert note type, number of dots and actual and normal notes into a duration
*/
static Fraction calculateFraction(const QString& type, const int dots, const Fraction timeMod)
{
// type
Fraction f = noteTypeToFraction(type);
if (f.isValid()) {
// dot(s)
Fraction f_no_dots = f;
for (int i = 0; i < dots; ++i)
f += (f_no_dots / (2 << i));
// tuplet
if (timeMod.isValid())
f *= timeMod;
// clean up (just in case)
f.reduce();
}
return f;
}
//---------------------------------------------------------
// 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)
: _divs(0), _score(score)
{
// 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();
}
//---------------------------------------------------------
// 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 MScore::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("ImportMusicXml: 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;
}
//---------------------------------------------------------
// logDebugTrace
//---------------------------------------------------------
/**
Log debug (function) trace.
*/
void MusicXMLParserPass1::logDebugTrace(const QString& /*info*/)
{
//qDebug("Trace %s", qPrintable(info));
}
//---------------------------------------------------------
// logDebugInfo
//---------------------------------------------------------
/**
Log debug \a info (non-fatal events relevant for debugging).
*/
void MusicXMLParserPass1::logDebugInfo(const QString& /*info*/)
{
//qDebug("Info at line %lld col %lld: %s",
// _e.lineNumber(), _e.columnNumber(), qPrintable(info));
}
//---------------------------------------------------------
// logError
//---------------------------------------------------------
/**
Log \a error (possibly non-fatal but to be reported to the user anyway).
*/
void MusicXMLParserPass1::logError(const QString& error)
{
QString err;
err = QString("Error at line %1 col %2: %3").arg(_e.lineNumber()).arg(_e.columnNumber()).arg(error);
qDebug("%s", qPrintable(err));
_parseStatus += err;
}
//---------------------------------------------------------
// skipLogCurrElem
//---------------------------------------------------------
/**
Skip the current element, log debug as info.
*/
void MusicXMLParserPass1::skipLogCurrElem()
{
logDebugInfo(QString("skipping '%1'").arg(_e.name().toString()));
_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 TextStyleType \a stl.
*/
static void addText(VBox* vbx, Score* s, QString strTxt, TextStyleType stl)
{
if (!strTxt.isEmpty()) {
Text* text = new Text(s);
text->setTextStyleType(stl);
text->setXmlText(strTxt);
vbx->add(text);
}
}
//---------------------------------------------------------
// addText
//---------------------------------------------------------
/**
Add text \a strTxt to VBox \a vbx using TextStyleType \a stl.
Also sets Align and Yoff.
*/
static void addText2(VBox* vbx, Score* s, QString strTxt, TextStyleType stl, Align v, double yoffs)
{
if (!strTxt.isEmpty()) {
Text* text = new Text(s);
text->setTextStyleType(stl);
text->setXmlText(strTxt);
text->textStyle().setAlign(v);
text->textStyle().setYoff(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)
{
const PageFormat* pf = score->pageFormat();
/*
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(), MScore::DPMM, MScore::DPI);
*/
// page width, height and odd top margin in tenths
const double ph = pf->height() * 10 * MScore::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 * MScore::DPI / score->spatium();
const double tm = pf->oddTopMargin() * 10 * MScore::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));
QString remainingFooterText;
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,
TextStyleType::COMPOSER, AlignmentFlags::RIGHT | AlignmentFlags::BOTTOM,
(miny - w->defaultY) * score->spatium() / (10 * MScore::DPI));
}
// poet is in the left column
else if (defx < pw1) {
// found poet
addText2(vbox, score, w->words,
TextStyleType::POET, AlignmentFlags::LEFT | AlignmentFlags::BOTTOM,
(miny - w->defaultY) * score->spatium() / (10 * MScore::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) {
//qDebug("add to copyright: '%s'", qPrintable(w->words));
remainingFooterText += 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,
TextStyleType::TITLE, AlignmentFlags::HCENTER | AlignmentFlags::TOP,
(maxy - w->defaultY) * score->spatium() / (10 * MScore::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,
TextStyleType::SUBTITLE, AlignmentFlags::HCENTER | AlignmentFlags::TOP,
(maxy - w->defaultY) * score->spatium() / (10 * MScore::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()) strPoet = metaPoet;
if (!metaTranslator.isEmpty()) strTranslator = metaTranslator;
addText(vbox, score, strTitle.toHtmlEscaped(), TextStyleType::TITLE);
addText(vbox, score, strSubTitle.toHtmlEscaped(), TextStyleType::SUBTITLE);
addText(vbox, score, strComposer.toHtmlEscaped(), TextStyleType::COMPOSER);
addText(vbox, score, strPoet.toHtmlEscaped(), TextStyleType::POET);
addText(vbox, score, strTranslator.toHtmlEscaped(), TextStyleType::TRANSLATOR);
}
if (vbox) {
vbox->setTick(0);
score->measures()->add(vbox);
}
// if no <rights> element was read and some text was found in the footer
// set the rights metadata to the value found
// TODO: remove formatting
// note that MusicXML files can contain at least two different copyright statements:
// - in the <rights> element (metadata)
// - in the <credit-words> (the printed version)
// while MuseScore supports only the first one
if (score->metaTag("copyright") == "" && remainingFooterText != "")
score->setMetaTag("copyright", remainingFooterText);
}
//---------------------------------------------------------
// parse
//---------------------------------------------------------
/**
Parse MusicXML in \a device and extract pass 1 data.
*/
Score::FileError MusicXMLParserPass1::parse(QIODevice* device)
{
logDebugTrace("MusicXMLParserPass1::parse device");
_parts.clear();
_e.setDevice(device);
Score::FileError 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);
createMeasures(_score, _measureLength, _measureStart);
return res;
}
//---------------------------------------------------------
// parse
//---------------------------------------------------------
/**
Start the parsing process, after verifying the top-level node is score-partwise
*/
Score::FileError MusicXMLParserPass1::parse()
{
logDebugTrace("MusicXMLParserPass1::parse");
bool found = false;
while (_e.readNextStartElement()) {
if (_e.name() == "score-partwise") {
found = true;
scorePartwise();
}
else {
logError(QString("this is not a MusicXML score-partwise file (top-level node '%1')")
.arg(_e.name().toString()));
_e.skipCurrentElement();
return Score::FileError::FILE_BAD_FORMAT;
}
}
if (!found) {
logError("this is not a MusicXML score-partwise file, node <score-partwise> not found");
return Score::FileError::FILE_BAD_FORMAT;
}
return Score::FileError::FILE_NO_ERROR;
}
//---------------------------------------------------------
// scorePartwise
//---------------------------------------------------------
/**
Parse the MusicXML top-level (XPath /score-partwise) node.
*/
void MusicXMLParserPass1::scorePartwise()
{
Q_ASSERT(_e.isStartElement() && _e.name() == "score-partwise");
logDebugTrace("MusicXMLParserPass1::scorePartwise");
MusicXmlPartGroupList partGroupList;
CreditWordsList credits;
int pageWidth; ///< Page width read from defaults
int pageHeight; ///< 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
if (pg->type == BracketType::NO_BRACKET)
il.at(pg->start)->staff(0)->setBracket(0, BracketType::NO_BRACKET);
else
il.at(pg->start)->staff(0)->addBracket(BracketItem(pg->type, stavesSpan));
if (pg->barlineSpan)
il.at(pg->start)->staff(0)->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(BracketItem(BracketType::BRACE, p->nstaves()));
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");
logDebugTrace("MusicXMLParserPass1::identification");
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");
logDebugTrace("MusicXMLParserPass1::credit");
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")
skipLogCurrElem();
else
skipLogCurrElem();
}
if (crwords != "") {
CreditWords* cw = new CreditWords(defaultx, defaulty, justify, halign, valign, crwords);
credits.append(cw);
}
}
else
skipLogCurrElem();
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.
static bool mustSetSize(const int i)
{
return
i == int(TextStyleType::TITLE)
|| i == int(TextStyleType::SUBTITLE)
|| i == int(TextStyleType::COMPOSER)
|| i == int(TextStyleType::POET)
|| i == int(TextStyleType::INSTRUMENT_LONG)
|| i == int(TextStyleType::INSTRUMENT_SHORT)
|| i == int(TextStyleType::INSTRUMENT_EXCERPT)
|| i == int(TextStyleType::TEMPO)
|| i == int(TextStyleType::METRONOME)
|| i == int(TextStyleType::TRANSLATOR)
|| i == int(TextStyleType::SYSTEM)
|| i == int(TextStyleType::STAFF)
|| i == int(TextStyleType::REPEAT_LEFT)
|| i == int(TextStyleType::REPEAT_RIGHT)
|| i == int(TextStyleType::TEXTLINE)
|| i == int(TextStyleType::GLISSANDO)
|| i == int(TextStyleType::INSTRUMENT_CHANGE);
}
//---------------------------------------------------------
// 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)
{
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
for (int i = int(TextStyleType::DEFAULT) + 1; i < int(TextStyleType::TEXT_STYLES); ++i) {
TextStyle ts = score->style()->textStyle(TextStyleType(i));
if (i == int(TextStyleType::LYRIC1) || i == int(TextStyleType::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);
}
}
//---------------------------------------------------------
// 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");
logDebugTrace("MusicXMLParserPass1::defaults");
double millimeter = _score->spatium()/10.0;
double tenths = 1.0;
QString lyricFontFamily;
QString lyricFontSize;
QString wordFontFamily;
QString wordFontSize;
while (_e.readNextStartElement()) {
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 = MScore::DPMM * (millimeter * 10.0 / tenths);
if (preferences.musicxmlImportLayout)
_score->setSpatium(_spatium);
}
else if (_e.name() == "page-layout") {
PageFormat pf;
pageLayout(pf, millimeter / (tenths * INCH), pageWidth, pageHeight);
if (preferences.musicxmlImportLayout)
_score->setPageFormat(pf);
}
else if (_e.name() == "system-layout") {
while (_e.readNextStartElement()) {
if (_e.name() == "system-margins")
skipLogCurrElem();
else if (_e.name() == "system-distance") {
Spatium val(_e.readElementText().toDouble() / 10.0);
if (preferences.musicxmlImportLayout) {
_score->style()->set(StyleIdx::minSystemDistance, val);
qDebug("system distance %f", val.val());
}
}
else if (_e.name() == "top-system-distance")
skipLogCurrElem();
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.musicxmlImportLayout)
_score->style()->set(StyleIdx::staffDistance, val);
}
else
skipLogCurrElem();
}
}
else if (_e.name() == "music-font")
skipLogCurrElem();
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() == "appearance")
skipLogCurrElem();
else if (_e.name() == "lyric-language")
skipLogCurrElem();
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");
logDebugTrace("MusicXMLParserPass1::pageLayout");
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.setTwosided(type == "odd" || type == "even");
if (type == "odd" || type == "both") {
pf.setOddLeftMargin(lm);
_oddRightMargin = rm;
pf.setOddTopMargin(tm);
pf.setOddBottomMargin(bm);
}
if (type == "even" || type == "both") {
pf.setEvenLeftMargin(lm);
_evenRightMargin = rm;
pf.setEvenTopMargin(tm);
pf.setEvenBottomMargin(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.setSize(size);
qreal w1 = size.width() - pf.oddLeftMargin() - _oddRightMargin;
qreal w2 = size.width() - pf.evenLeftMargin() - _evenRightMargin;
pf.setPrintableWidth(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");
logDebugTrace("MusicXMLParserPass1::partList");
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, s.toLatin1().data());
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", s.toLatin1().data());
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");
logDebugTrace("MusicXMLParserPass1::partGroup");
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-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
qDebug("MusicXMLParserPass1::partGroup: part-group type '%s' not supported",
qPrintable(type)); // TODO
}
//---------------------------------------------------------
// 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");
logDebugTrace("MusicXMLParserPass1::scorePart");
QString id = _e.attributes().value("id").toString();
if (_parts.contains(id)) {
logError(QString("duplicate part id '%1'").arg(id));
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
bool doLong = !(_e.attributes().value("print-object") == "no");
QString name = _e.readElementText();
_partMap[id]->setPartName(name);
if (doLong)
_partMap[id]->setLongName(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
QString name = _e.readElementText();
if (!(_e.attributes().value("print-object") == "no"))
_partMap[id]->setPlainShortName(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")
_e.skipCurrentElement(); // skip but don't log
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");
logDebugTrace("MusicXMLParserPass1::scoreInstrument");
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;
// try to prevent an empty track name
if (_partMap[partId]->partName() == "")
_partMap[partId]->setPartName(instrName);
}
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");
logDebugTrace("MusicXMLParserPass1::midiInstrument");
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) {
logError(QString("MusicXml::xmlScorePart: incorrect midi-channel: %1").arg(channel));
channel = 1;
}
else if (channel > 16) {
logError(QString("MusicXml::xmlScorePart: incorrect midi-channel: %1").arg(channel));
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) {
logError(QString("MusicXml::xmlScorePart: incorrect midi-program: %1").arg(program));
program = 1;
}
else if (program > 128) {
logError(QString("MusicXml::xmlScorePart: incorrect midi-program: %1").arg(program));
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
logError(QString("MusicXml::xmlScorePart: incorrect midi-volume: %1").arg(vol));
}
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
logError(QString("MusicXml::xmlScorePart: incorrect midi-volume: %g1").arg(pan));
}
else
skipLogCurrElem();
}
Q_ASSERT(_e.isEndElement() && _e.name() == "midi-instrument");
}
//---------------------------------------------------------
// 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");
logDebugTrace("MusicXMLParserPass1::part");
const QString id = _e.attributes().value("id").toString();
if (!_parts.contains(id)) {
logError(QString("MusicXMLParserPass1::part cannot find part '%1'").arg(id));
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();
}
// 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();
// debug: print results
/*
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 time,
Fraction& mdur,
VoiceOverlapDetector& vod)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "measure");
logDebugTrace("MusicXMLParserPass1::measure");
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);
else if (_e.name() == "note") {
Fraction dura;
// note: chord and grace note handling done in note()
note(partId, time + 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 {
logError("backup beyond measure start");
mTime.set(0, 1);
}
}
}
else if (_e.name() == "direction")
direction(partId, time + mTime);
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)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "attributes");
logDebugTrace("MusicXMLParserPass1::attributes");
while (_e.readNextStartElement()) {
if (_e.name() == "clef")
clef(partId);
else if (_e.name() == "divisions")
divisions();
else if (_e.name() == "staff-details")
staffDetails(partId);
else if (_e.name() == "staves")
staves(partId);
else if (_e.name() == "time")
time();
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");
logDebugTrace("MusicXMLParserPass1::clef");
QString number = _e.attributes().value("number").toString();
int n = 0;
if (number != "") {
n = number.toInt();
if (n <= 0) {
logError(QString("invalid number %1").arg(number));
n = 0;
}
else
n--; // make zero-based
}
StaffTypes staffType = StaffTypes::STANDARD;
while (_e.readNextStartElement()) {
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(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(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") {
qDebug("ImportMusicXml: time symbol <%s> not recognized with beats=%s and beat-type=%s",
qPrintable(timeSymbol), qPrintable(beats), qPrintable(beatType)); // TODO
return false;
}
btp = beatType.toInt();
QStringList list = beats.split("+");
for (int i = 0; i < list.size(); i++)
bts += list.at(i).toInt();
}
return true;
}
//---------------------------------------------------------
// time
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/attributes/time node.
*/
void MusicXMLParserPass1::time()
{
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(beats, beatType, timeSymbol, st, bts, btp)) {
_timeSigDura = Fraction(bts, btp);
}
}
}
//---------------------------------------------------------
// 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))
logError("illegal divisions");
}
//---------------------------------------------------------
// setStaffLines
//---------------------------------------------------------
/**
Set stafflines and barline span for a single staff
*/
static void setStaffLines(Score* score, int staffIdx, int stafflines)
{
score->staff(staffIdx)->setLines(stafflines);
score->staff(staffIdx)->setBarLineTo((stafflines - 1) * 2);
}
//---------------------------------------------------------
// staffDetails
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/attributes/staff-details node.
*/
void MusicXMLParserPass1::staffDetails(const QString& partId)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "staff-details");
logDebugTrace("MusicXMLParserPass1::staffDetails");
QString number = _e.attributes().value("number").toString();
int n = -1; // invalid
if (number != "") {
n = number.toInt();
if (n <= 0) {
logError(QString("invalid number %1").arg(number));
n = -1;
}
else
n--; // make zero-based
}
Part* part = getPart(partId);
Q_ASSERT(part);
int staves = part->nstaves();
int staffIdx = _score->staffIdx(part);
StringData* t = 0;
if (_score->staff(staffIdx)->isTabStaff()) {
t = new StringData;
t->setFrets(25); // sensible default
}
int staffLines = 0;
while (_e.readNextStartElement()) {
if (_e.name() == "staff-lines") {
// save staff lines for later
staffLines = _e.readElementText().toInt();
// for a TAB staff also resize the string table and init with zeroes
if (t) {
if (0 < staffLines)
t->stringList() = QVector<instrString>(staffLines).toList();
else
logError(QString("illegal staff-lines %1").arg(staffLines));
}
}
else if (_e.name() == "staff-tuning")
staffTuning(t);
else
skipLogCurrElem();
}
if (staffLines > 0) {
if (n == -1) {
for (int i = 0; i < staves; ++i)
setStaffLines(_score, staffIdx+i, staffLines);
}
else
setStaffLines(_score, staffIdx, staffLines);
}
if (t) {
Instrument* i = part->instrument();
i->setStringData(*t);
}
}
//---------------------------------------------------------
// MusicXMLStepAltOct2Pitch
//---------------------------------------------------------
/**
Convert MusicXML \a step (0=C, 1=D, etc.) / \a alter / \a octave to midi pitch.
Note: same code is in pass 1 and in pass 2.
TODO: combine
*/
static int MusicXMLStepAltOct2Pitch(int step, int alter, int octave)
{
// c d e f g a b
static int table[7] = { 0, 2, 4, 5, 7, 9, 11 };
if (step < 0 || step > 6) {
qDebug("MusicXMLStepAltOct2Pitch: illegal step %d", step);
return -1;
}
int pitch = table[step] + alter + (octave+1) * 12;
if (pitch < 0)
pitch = -1;
if (pitch > 127)
pitch = -1;
return pitch;
}
//---------------------------------------------------------
// staffTuning
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/attributes/staff-details/staff-tuning node.
*/
void MusicXMLParserPass1::staffTuning(StringData* t)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "staff-tuning");
logDebugTrace("MusicXMLParserPass1::staffTuning");
// ignore <staff-tuning> if not a TAB staff
if (!t) {
logError("<staff-tuning> on non-TAB staff");
skipLogCurrElem();
return;
}
int line = _e.attributes().value("line").toInt();
int step = 0;
int alter = 0;
int octave = 0;
while (_e.readNextStartElement()) {
if (_e.name() == "tuning-alter")
alter = _e.readElementText().toInt();
else if (_e.name() == "tuning-octave")
octave = _e.readElementText().toInt();
else if (_e.name() == "tuning-step") {
QString strStep = _e.readElementText();
int pos = QString("CDEFGAB").indexOf(strStep);
if (strStep.size() == 1 && pos >=0 && pos < 7)
step = pos;
else
logError(QString("invalid step '%1'").arg(strStep));
}
else
skipLogCurrElem();
}
if (0 < line && line <= t->stringList().size()) {
int pitch = MusicXMLStepAltOct2Pitch(step, alter, octave);
if (pitch >= 0)
t->stringList()[line - 1].pitch = pitch;
else
logError(QString("invalid string %1 tuning step/alter/oct %2/%3/%4")
.arg(line).arg(step).arg(alter).arg(octave));
}
}
//---------------------------------------------------------
// staves
//---------------------------------------------------------
/**
Set number of staves for part \a partId to the max value of the current value
and the value in the <staves> element.
*/
void MusicXMLParserPass1::staves(const QString& partId)
{
Q_ASSERT(_e.isStartElement() && _e.name() == "staves");
logDebugTrace("MusicXMLParserPass1::staves");
int staves = _e.readElementText().toInt();
if (!(staves > 0 && staves <= MAX_STAVES)) {
logError("illegal staves");
return;
}
Part* part = _partMap.value(partId);
Q_ASSERT(part);
if (staves > part->nstaves())
part->setStaves(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 {
logError(QString("invalid staff %1").arg(strStaff));
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
qDebug("octave-shift start %s delta %d",
qPrintable(prevDesc.time.print()), prevDesc.size);
_parts[partId].addOctaveShift(staff, prevDesc.size, prevDesc.time);
qDebug("octave-shift stop %s delta %d",
qPrintable(desc.time.print()), -prevDesc.size);
_parts[partId].addOctaveShift(staff, -prevDesc.size, desc.time);
}
else
logError("double octave-shift stop");
_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
qDebug("octave-shift start %s delta %d",
qPrintable(desc.time.print()), desc.size);
_parts[partId].addOctaveShift(staff, desc.size, desc.time);
qDebug("octave-shift stop %s delta %d",
qPrintable(prevDesc.time.print()), -desc.size);
_parts[partId].addOctaveShift(staff, -desc.size, prevDesc.time);
}
else
logError("double octave-shift start");
_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)
logError(QString("invalid number %1").arg(number));
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 {
logError(QString("invalid octave-shift number %1").arg(number));
}
_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:
logError(QString("invalid octave-shift size %1").arg(size));
return;
}
if (!cTime.isValid() || cTime < Fraction(0, 1))
logError("invalid current time");
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 {
logError(QString("invalid octave-shift type '%1'").arg(type));
return;
}
desc = MxmlOctaveShiftDesc(tp, sz, cTime);
}
//---------------------------------------------------------
// 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");
//logDebugTrace("MusicXMLParserPass1::note");
if (_e.attributes().value("print-spacing") == "no") {
notePrintSpacingNo(dura);
return;
}
//float alter = 0;
bool chord = false;
int dots = 0;
bool grace = false;
//int octave = -1;
bool bRest = false;
int staff = 1;
//int step = 0;
Fraction timeMod(1, 1);
QString type;
QString voice = "1";
QString instrId;
while (_e.readNextStartElement()) {
if (_e.name() == "chord") {
chord = true;
_e.readNext();
}
else if (_e.name() == "dot") {
dots++;
_e.readNext();
}
else if (_e.name() == "duration")
duration(dura);
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() == "pitch")
_e.skipCurrentElement(); // skip but don't log
else if (_e.name() == "rest") {
bRest = true;
rest();
}
else if (_e.name() == "staff") {
QString strStaff = _e.readElementText();
staff = strStaff.toInt();
// Bug fix for Cubase 6.5.5 which generates <staff>2</staff> in a single staff part
// Same fix is required in pass 1 and pass 2
Part* part = _partMap.value(partId);
Q_ASSERT(part);
if (staff <= 0 || staff > part->nstaves()) {
logError(QString("illegal staff '%1'").arg(strStaff));
staff = 1;
}
}
else if (_e.name() == "time-modification")
timeModification(timeMod);
else if (_e.name() == "type")
type = _e.readElementText();
else if (_e.name() == "voice")
voice = _e.readElementText();
else
skipLogCurrElem();
}
// convert staff to zero-based
staff--;
// multi-instrument handling
QString prevInstrId = _parts[partId]._instrList.instrument(sTime);
bool mustInsert = instrId != prevInstrId;
/*
qDebug("tick %s (%d) staff %d voice '%s' previnst='%s' instrument '%s' insert %d",
qPrintable(sTime.print()),
sTime.ticks(),
staff + 1,
qPrintable(voice),
qPrintable(prevInstrId),
qPrintable(instrId),
mustInsert
);
*/
if (mustInsert)
_parts[partId]._instrList.setInstrument(instrId, sTime);
// normalize duration
if (dura.isValid())
dura.reduce();
// timing error check(s)
QString errorStr;
Fraction calcDura = calculateFraction(type, dots, timeMod);
if (dura.isValid() && calcDura.isValid()) {
if (dura != calcDura) {
errorStr = "calculated duration not equal to specified duration";
if (bRest && type == "whole" && dura.isValid()) {
// Sibelius whole measure rest (not an error)
errorStr = "";
}
else {
const int maxDiff = 3; // maximum difference considered a rounding error
if (qAbs(calcDura.ticks() - dura.ticks()) <= maxDiff) {
errorStr += " -> assuming rounding error";
dura = calcDura;
}
}
}
}
else if (dura.isValid()) {
// do not report an error for typeless (whole measure) rests
if (!(bRest && type == ""))
errorStr = "calculated duration invalid, using specified duration";
}
else if (calcDura.isValid()) {
if (!grace) {
errorStr = "specified duration invalid, using calculated duration";
dura = calcDura; // overrule dura
}
}
else
errorStr = "calculated and specified duration invalid";
if (errorStr != "")
logError(errorStr);
// 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);
}
}
//---------------------------------------------------------
// 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");
//logDebugTrace("MusicXMLParserPass1::notePrintSpacingNo");
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");
//logDebugTrace("MusicXMLParserPass1::duration");
dura.set(0, 0); // invalid unless set correctly
int intDura = _e.readElementText().toInt();
if (intDura > 0) {
if (_divs > 0)
dura.set(intDura, 4 * _divs);
else
logError("illegal or uninitialized divisions");
}
else
logError("illegal duration");
//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");
//logDebugTrace("MusicXMLParserPass1::forward");
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");
//logDebugTrace("MusicXMLParserPass1::backup");
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");
//logDebugTrace("MusicXMLParserPass1::timeModification");
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);
logError(QString("illegal time-modification: actual-notes %1 normal-notes %2")
.arg(strActual).arg(strNormal));
}
}
//---------------------------------------------------------
// rest
//---------------------------------------------------------
/**
Parse the /score-partwise/part/measure/note/rest node.
*/
void MusicXMLParserPass1::rest()
{
Q_ASSERT(_e.isStartElement() && _e.name() == "rest");
//logDebugTrace("MusicXMLParserPass1::rest");
while (_e.readNextStartElement()) {
skipLogCurrElem();
}
}
} // namespace Ms