2908 lines
106 KiB
C++
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
|