6230 lines
261 KiB
C++
6230 lines
261 KiB
C++
//=============================================================================
|
|
// MusE Score
|
|
// Linux Music Score Editor
|
|
// $Id: importxml.cpp 5653 2012-05-19 20:19:58Z lvinken $
|
|
//
|
|
// Copyright (C) 2002-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.
|
|
//=============================================================================
|
|
|
|
// TODO LVI 2011-01-18: it seems brackets ending after the last note in a measure
|
|
// import OK but stop is not exported OK. Find out what causes this (import, export,
|
|
// structural issue).
|
|
|
|
// TODO LVI 2011-10-30: determine how to report import errors.
|
|
// Currently all output (both debug and error reports) are done using qDebug.
|
|
|
|
// TODO LVI 2012-04-26: read drumset definition from MusicXML
|
|
// Mapping Musescore DrumInstrument to MusicXML elements
|
|
// pitch part-list/score-part/midi-instrument/midi-unpitched
|
|
// name part-list/score-part/score-instrument/instrument-name
|
|
// notehead part/measure/note/notehead (once for each individual note)
|
|
// line part/measure/note/unpitched (once for each individual note) + current clef
|
|
// stemdirection part/measure/note/stem (once for each individual note)
|
|
// voice N/A (leave empty ?)
|
|
// shortcut N/A (leave empty ?)
|
|
|
|
/**
|
|
MusicXML import.
|
|
*/
|
|
|
|
#include "config.h"
|
|
// #include "musescore.h"
|
|
#include "musicxml.h"
|
|
#include "file.h"
|
|
#include "libmscore/score.h"
|
|
#include "libmscore/rest.h"
|
|
#include "libmscore/chord.h"
|
|
#include "libmscore/sig.h"
|
|
#include "libmscore/key.h"
|
|
#include "libmscore/clef.h"
|
|
#include "libmscore/note.h"
|
|
#include "libmscore/element.h"
|
|
#include "libmscore/sym.h"
|
|
#include "libmscore/slur.h"
|
|
#include "libmscore/tie.h"
|
|
#include "libmscore/hairpin.h"
|
|
#include "libmscore/tuplet.h"
|
|
#include "libmscore/segment.h"
|
|
#include "libmscore/dynamic.h"
|
|
#include "libmscore/page.h"
|
|
#include "libmscore/staff.h"
|
|
#include "libmscore/part.h"
|
|
#include "libmscore/measure.h"
|
|
#include "libmscore/style.h"
|
|
#include "libmscore/bracket.h"
|
|
#include "libmscore/timesig.h"
|
|
#include "libmscore/xml.h"
|
|
#include "libmscore/barline.h"
|
|
#include "libmscore/lyrics.h"
|
|
#include "libmscore/volta.h"
|
|
#include "libmscore/textline.h"
|
|
#include "libmscore/keysig.h"
|
|
#include "libmscore/pitchspelling.h"
|
|
#include "libmscore/utils.h"
|
|
#include "libmscore/layoutbreak.h"
|
|
#include "libmscore/tremolo.h"
|
|
#include "libmscore/box.h"
|
|
#include "libmscore/repeat.h"
|
|
#include "libmscore/ottava.h"
|
|
#include "libmscore/trill.h"
|
|
#include "libmscore/pedal.h"
|
|
#include "libmscore/harmony.h"
|
|
#include "libmscore/tempotext.h"
|
|
#include "libmscore/stafftext.h"
|
|
#include "libmscore/articulation.h"
|
|
#include "libmscore/arpeggio.h"
|
|
#include "libmscore/glissando.h"
|
|
#include "libmscore/breath.h"
|
|
#include "libmscore/tempo.h"
|
|
#include "libmscore/chordlist.h"
|
|
#include "libmscore/mscore.h"
|
|
#include "libmscore/accidental.h"
|
|
#include "libmscore/rehearsalmark.h"
|
|
#include "libmscore/fingering.h"
|
|
#include "preferences.h"
|
|
#include "musicxmlsupport.h"
|
|
#include "libmscore/chordline.h"
|
|
#include "libmscore/figuredbass.h"
|
|
#include "libmscore/fret.h"
|
|
#include "thirdparty/qzip/qzipreader_p.h"
|
|
#include "libmscore/stafftype.h"
|
|
#include "libmscore/stringdata.h"
|
|
#include "libmscore/drumset.h"
|
|
#include "libmscore/beam.h"
|
|
#include "libmscore/jump.h"
|
|
#include "libmscore/marker.h"
|
|
#include "importxmlfirstpass.h"
|
|
#include "libmscore/instrchange.h"
|
|
|
|
namespace Ms {
|
|
|
|
//---------------------------------------------------------
|
|
// local defines for debug output
|
|
//---------------------------------------------------------
|
|
|
|
//#define DEBUG_VOICE_MAPPER true
|
|
//#define DEBUG_TICK true
|
|
|
|
//---------------------------------------------------------
|
|
// MusicXMLStepAltOct2Pitch
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Convert MusicXML \a step / \a alter / \a octave to midi pitch.
|
|
Note: same code is also in libmscore/tablature.cpp.
|
|
TODO: combine ?
|
|
*/
|
|
|
|
static int MusicXMLStepAltOct2Pitch(char step, int alter, int octave)
|
|
{
|
|
int istep = step - 'A';
|
|
// a b c d e f g
|
|
static int table[7] = { 9, 11, 0, 2, 4, 5, 7 };
|
|
if (istep < 0 || istep > 6) {
|
|
qDebug("MusicXMLStepAltOct2Pitch: illegal step %d, <%c>", istep, step);
|
|
return -1;
|
|
}
|
|
int pitch = table[istep] + alter + (octave+1) * 12;
|
|
|
|
if (pitch < 0)
|
|
pitch = -1;
|
|
if (pitch > 127)
|
|
pitch = -1;
|
|
|
|
return pitch;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlSetPitch
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Convert MusicXML \a step / \a alter / \a octave to midi pitch,
|
|
set pitch and tpc.
|
|
Note that n's staff and track have not been set yet
|
|
*/
|
|
|
|
static void xmlSetPitch(Note* n, char step, int alter, int octave, Ottava* (&ottavas)[MAX_NUMBER_LEVEL], int track)
|
|
{
|
|
//qDebug("xmlSetPitch(n=%p, st=%c, alter=%d, octave=%d)",
|
|
// n, step, alter, octave);
|
|
|
|
const Staff* staff = n->score()->staff(track / VOICES);
|
|
const Instrument* instr = staff->part()->instr();
|
|
const Interval intval = instr->transpose();
|
|
//qDebug(" staff=%p instr=%p dia=%d chro=%d",
|
|
// staff, instr, (int) intval.diatonic, (int) intval.chromatic);
|
|
|
|
int pitch = MusicXMLStepAltOct2Pitch(step, alter, octave);
|
|
pitch += intval.chromatic; // assume not in concert pitch
|
|
// ensure sane values
|
|
pitch = limit(pitch, 0, 127);
|
|
|
|
for(int i = 0; i < MAX_NUMBER_LEVEL; ++i) {
|
|
if (ottavas[i] != 0 && ottavas[i]->track() == track)
|
|
pitch -= ottavas[i]->pitchShift();
|
|
}
|
|
|
|
// a b c d e f g
|
|
static int table1[7] = { 5, 6, 0, 1, 2, 3, 4 };
|
|
int istep = step - 'A';
|
|
int tpc2 = step2tpc(table1[istep], AccidentalVal(alter));
|
|
int tpc1 = Ms::transposeTpc(tpc2, intval, true);
|
|
n->setPitch(pitch, tpc1, tpc2);
|
|
//qDebug(" pitch=%d tpc1=%d tpc2=%d", n->pitch(), n->tpc1(), n->tpc2());
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// calcTicks
|
|
//---------------------------------------------------------
|
|
|
|
static int calcTicks(QString text, int divisions)
|
|
{
|
|
if (divisions <= 0) {
|
|
qDebug("MusicXml-Import: bad divisions value: <%d>", divisions);
|
|
return 0;
|
|
}
|
|
bool ok;
|
|
int val = MxmlSupport::stringToInt(text, &ok);
|
|
if (!ok) {
|
|
qDebug("MusicXml-Import: bad duration value: <%s>",
|
|
qPrintable(text));
|
|
}
|
|
if (val == 0) // neuratron scanner produces sometimes 0 !?
|
|
val = 1;
|
|
val *= MScore::division;
|
|
val /= divisions;
|
|
return val;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// noteDurationAsFraction
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Determine note duration as fraction. Prefer note type over duration.
|
|
Input e is the note element.
|
|
If chord or grace, duration is 0.
|
|
*/
|
|
|
|
static Fraction noteDurationAsFraction(const int divisions, const QDomElement e)
|
|
{
|
|
int actualNotes = 0;
|
|
bool chord = false;
|
|
int dots = 0;
|
|
int duration = 0;
|
|
bool grace = false;
|
|
int normalNotes = 0;
|
|
bool rest = false;
|
|
QString type;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "chord")
|
|
chord = true;
|
|
else if (ee.tagName() == "dot")
|
|
dots++;
|
|
else if (ee.tagName() == "duration") {
|
|
bool ok;
|
|
duration = MxmlSupport::stringToInt(ee.text(), &ok);
|
|
if (!ok)
|
|
qDebug("MusicXml-Import: bad duration value: <%s>",
|
|
qPrintable(ee.text()));
|
|
}
|
|
else if (ee.tagName() == "grace")
|
|
grace = true;
|
|
else if (ee.tagName() == "rest")
|
|
rest = true;
|
|
else if (ee.tagName() == "time-modification") {
|
|
for (QDomElement eee = ee.firstChildElement(); !eee.isNull(); eee = eee.nextSiblingElement()) {
|
|
if (eee.tagName() == "actual-notes") {
|
|
bool ok;
|
|
actualNotes = MxmlSupport::stringToInt(eee.text(), &ok);
|
|
if (!ok || divisions <= 0)
|
|
qDebug("MusicXml-Import: bad actual-notes value: <%s>",
|
|
qPrintable(eee.text()));
|
|
}
|
|
if (eee.tagName() == "normal-notes") {
|
|
bool ok;
|
|
normalNotes = MxmlSupport::stringToInt(eee.text(), &ok);
|
|
if (!ok || divisions <= 0)
|
|
qDebug("MusicXml-Import: bad normal-notes value: <%s>",
|
|
qPrintable(eee.text()));
|
|
}
|
|
}
|
|
}
|
|
else if (ee.tagName() == "type")
|
|
type = ee.text();
|
|
}
|
|
|
|
// if chord or grace, duration is 0
|
|
if (chord || grace)
|
|
return Fraction(0, 1);
|
|
|
|
// calculate note duration as fraction based on type, dots, normal and actual notes
|
|
// if that does not succeed, fallback to using the <duration> element
|
|
// note divisions = ticks / quarter note
|
|
Fraction f = MxmlSupport::calculateFraction(type, dots, normalNotes, actualNotes);
|
|
if (!f.isValid()) {
|
|
f = Fraction(duration, 4 * divisions);
|
|
}
|
|
|
|
// bug fix for rests in triplet
|
|
if (f.isValid() && rest && normalNotes == 0 && actualNotes == 0) {
|
|
if ((Fraction(duration, 4 * divisions) / f) == Fraction(2, 3)) {
|
|
// duration is exactly 2/3 of what is expected based on type
|
|
f = Fraction(duration, 4 * divisions);
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUG_TICK
|
|
qDebug("time-in-fraction: note type %s dots %d norm %d act %d"
|
|
" dur %d chord %d grace %d-> dt frac %s (ticks %d)",
|
|
qPrintable(type), dots, normalNotes, actualNotes, duration,
|
|
chord, grace, qPrintable(f.print()), f.ticks());
|
|
#endif
|
|
|
|
return f;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// moveTick
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Move tick and typFr by amount specified in the element e, which must be
|
|
a forward, backup or note.
|
|
*/
|
|
|
|
static void moveTick(const int mtick, int& tick, int& maxtick, Fraction& typFr, const int divisions, const QDomElement e)
|
|
{
|
|
if (divisions <= 0) {
|
|
qDebug("moveTick: invalid divisions %d", divisions);
|
|
return;
|
|
}
|
|
|
|
if (e.tagName() == "forward") {
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "duration") {
|
|
int val = calcTicks(ee.text(), divisions);
|
|
#ifdef DEBUG_TICK
|
|
qDebug("forward %d", val);
|
|
#endif
|
|
bool ok;
|
|
val = MxmlSupport::stringToInt(ee.text(), &ok);
|
|
if (!ok || divisions <= 0)
|
|
qDebug("MusicXml-Import: bad divisions value: <%s>",
|
|
qPrintable(ee.text()));
|
|
Fraction f(val, 4 * divisions); // note divisions = ticks / quarter note
|
|
typFr += f;
|
|
typFr.reduce();
|
|
tick = mtick + typFr.ticks();
|
|
if (tick > maxtick)
|
|
maxtick = tick;
|
|
}
|
|
else if (ee.tagName() == "voice")
|
|
;
|
|
else if (ee.tagName() == "staff")
|
|
;
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else if (e.tagName() == "backup") {
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "duration") {
|
|
int val = calcTicks(ee.text(), divisions);
|
|
#ifdef DEBUG_TICK
|
|
qDebug("backup %d", val);
|
|
#endif
|
|
bool ok;
|
|
val = MxmlSupport::stringToInt(ee.text(), &ok);
|
|
if (!ok || divisions <= 0)
|
|
qDebug("MusicXml-Import: bad divisions value: <%s>",
|
|
qPrintable(ee.text()));
|
|
Fraction f(val, 4 * divisions); // note divisions = ticks / quarter note
|
|
if (f < typFr) {
|
|
typFr -= f;
|
|
typFr.reduce();
|
|
}
|
|
else
|
|
typFr = Fraction(0, 1);
|
|
tick = mtick + typFr.ticks();
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else if (e.tagName() == "note") {
|
|
#ifdef DEBUG_TICK
|
|
int ticks = 0;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
QString tag(ee.tagName());
|
|
if (tag == "duration") {
|
|
ticks = calcTicks(ee.text(), divisions);
|
|
}
|
|
}
|
|
qDebug("note %d", ticks);
|
|
#endif
|
|
Fraction f = noteDurationAsFraction(divisions, e);
|
|
typFr += f;
|
|
typFr.reduce();
|
|
tick = mtick + typFr.ticks();
|
|
if (tick > maxtick)
|
|
maxtick = tick;
|
|
}
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// 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<int>& ms)
|
|
{
|
|
ms.resize(ml.size());
|
|
// first measure starts at tick = 0
|
|
ms[0] = 0;
|
|
// all others start at start tick previous measure plus length previous measure
|
|
for (int i = 1; i < ml.size(); i++)
|
|
ms[i] = ms.at(i - 1) + ml.at(i - 1).ticks();
|
|
#ifdef DEBUG_TICK
|
|
for (int i = 0; i < ms.size(); i++)
|
|
qDebug("measurelength ms[%d] %d", i + 1, ms.at(i));
|
|
#endif
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// MusicXml
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
MusicXml constructor.
|
|
*/
|
|
|
|
MusicXml::MusicXml(QDomDocument* d, MxmlReaderFirstPass const& p1)
|
|
:
|
|
lastVolta(0),
|
|
doc(d),
|
|
pass1(p1),
|
|
divisions(-1), // set to impossible value to detect usage without initialization
|
|
beamMode(Beam::Mode::NONE),
|
|
pageWidth(0),
|
|
pageHeight(0)
|
|
{
|
|
// Determine the start tick of each measure in the part
|
|
pass1.determineMeasureLength(measureLength);
|
|
determineMeasureStart(measureLength, measureStart);
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// initMusicXmlSchema
|
|
// return false on error
|
|
//---------------------------------------------------------
|
|
|
|
static bool initMusicXmlSchema(QXmlSchema& schema)
|
|
{
|
|
// read the MusicXML schema from the application resources
|
|
QFile schemaFile(":/schema/musicxml.xsd");
|
|
if (!schemaFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
qDebug("initMusicXmlSchema() could not open resource musicxml.xsd");
|
|
MScore::lastError = QObject::tr("Internal error: Could not open resource musicxml.xsd\n");
|
|
return false;
|
|
}
|
|
|
|
// copy the schema into a QByteArray and fixup xs:imports,
|
|
// using a path to the application resources instead of to www.musicxml.org
|
|
// to prevent downloading from the net
|
|
QByteArray schemaBa;
|
|
QTextStream schemaStream(&schemaFile);
|
|
while (!schemaStream.atEnd()) {
|
|
QString line = schemaStream.readLine();
|
|
if (line.contains("xs:import"))
|
|
line.replace("http://www.musicxml.org/xsd", "qrc:///schema");
|
|
schemaBa += line.toUtf8();
|
|
schemaBa += "\n";
|
|
}
|
|
|
|
// load and validate the schema
|
|
schema.load(schemaBa);
|
|
if (!schema.isValid()) {
|
|
qDebug("initMusicXmlSchema() internal error: MusicXML schema is invalid");
|
|
MScore::lastError = QObject::tr("Internal error: MusicXML schema is invalid\n");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// musicXMLValidationErrorDialog
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Show a dialog displaying the MusicXML validation error(s)
|
|
and asks the user if he wants to try to load the file anyway.
|
|
Return QMessageBox::Yes (try anyway) or QMessageBox::No (don't)
|
|
*/
|
|
|
|
static int musicXMLValidationErrorDialog(QString text, QString detailedText)
|
|
{
|
|
QMessageBox errorDialog;
|
|
errorDialog.setIcon(QMessageBox::Question);
|
|
errorDialog.setText(text);
|
|
errorDialog.setInformativeText("Do you want to try to load this file anyway?");
|
|
errorDialog.setDetailedText(detailedText);
|
|
errorDialog.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
|
|
errorDialog.setDefaultButton(QMessageBox::No);
|
|
return errorDialog.exec();
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// extractRootfile
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Extract rootfile from compressed MusicXML file \a qf, return true if OK and false on error.
|
|
*/
|
|
|
|
static bool extractRootfile(QFile* qf, QByteArray& data)
|
|
{
|
|
MQZipReader f(qf->fileName());
|
|
data = f.fileData("META-INF/container.xml");
|
|
|
|
QDomDocument container;
|
|
int line, column;
|
|
QString err;
|
|
if (!container.setContent(data, false, &err, &line, &column)) {
|
|
MScore::lastError = QObject::tr("Error reading container.xml at line %1 column %2: %3\n").arg(line).arg(column).arg(err);
|
|
return false;
|
|
}
|
|
|
|
// extract first rootfile
|
|
QString rootfile = "";
|
|
for (QDomElement e = container.documentElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
if (e.tagName() == "container") {
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "rootfiles") {
|
|
for (QDomElement eee = ee.firstChildElement(); !eee.isNull(); eee = eee.nextSiblingElement()) {
|
|
if (eee.tagName() == "rootfile") {
|
|
if (rootfile == "")
|
|
rootfile = eee.attribute(QString("full-path"));
|
|
}
|
|
else
|
|
domError(eee);
|
|
}
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else
|
|
domError(e);
|
|
}
|
|
|
|
if (rootfile == "") {
|
|
qDebug("can't find rootfile in: %s", qPrintable(qf->fileName()));
|
|
MScore::lastError = QObject::tr("Can't find rootfile\n%1").arg(qf->fileName());
|
|
return false;
|
|
}
|
|
|
|
// read the rootfile
|
|
data = f.fileData(rootfile);
|
|
return true;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// doValidate
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Validate MusicXML data from file \a name contained in QIODevice \a dev.
|
|
*/
|
|
|
|
static Score::FileError doValidate(const QString& name, QIODevice* dev)
|
|
{
|
|
QTime t;
|
|
t.start();
|
|
|
|
// initialize the schema
|
|
ValidatorMessageHandler messageHandler;
|
|
QXmlSchema schema;
|
|
schema.setMessageHandler(&messageHandler);
|
|
if (!initMusicXmlSchema(schema))
|
|
return Score::FileError::FILE_BAD_FORMAT; // appropriate error message has been printed by initMusicXmlSchema
|
|
|
|
// validate the data
|
|
QXmlSchemaValidator validator(schema);
|
|
bool valid = validator.validate(dev, QUrl::fromLocalFile(name));
|
|
qDebug("Validation time elapsed: %d ms", t.elapsed());
|
|
|
|
if (valid)
|
|
qDebug("importMusicXml() file '%s' is a valid MusicXML file", qPrintable(name));
|
|
else {
|
|
qDebug("importMusicXml() file '%s' is not a valid MusicXML file", qPrintable(name));
|
|
MScore::lastError = QObject::tr("File '%1' is not a valid MusicXML file").arg(name);
|
|
if (MScore::noGui)
|
|
return Score::FileError::FILE_NO_ERROR; // might as well try anyhow in converter mode
|
|
if (musicXMLValidationErrorDialog(MScore::lastError, messageHandler.getErrors()) != QMessageBox::Yes)
|
|
return Score::FileError::FILE_USER_ABORT;
|
|
}
|
|
|
|
// return OK
|
|
return Score::FileError::FILE_NO_ERROR;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// doImport
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Import MusicXML data from file \a name contained in QIODevice \a dev into score \a score.
|
|
*/
|
|
|
|
static Score::FileError doImport(Score* score, const QString& name, QIODevice* dev, MxmlReaderFirstPass const& pass1)
|
|
{
|
|
QTime t;
|
|
t.start();
|
|
QDomDocument doc;
|
|
int line;
|
|
int column;
|
|
QString err;
|
|
if (!doc.setContent(dev, false, &err, &line, &column)) {
|
|
MScore::lastError = QObject::tr("Error at line %1 column %2: %3\n").arg(line).arg(column).arg(err);
|
|
return Score::FileError::FILE_BAD_FORMAT;
|
|
}
|
|
docName = name; // set filename for domError
|
|
MusicXml musicxml(&doc, pass1);
|
|
musicxml.import(score);
|
|
score->fixTicks();
|
|
qDebug("Parsing time elapsed: %d ms", t.elapsed());
|
|
return Score::FileError::FILE_NO_ERROR;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// doValidateAndImport
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Validate and import MusicXML data from file \a name contained in QIODevice \a dev into score \a score.
|
|
*/
|
|
|
|
static Score::FileError doValidateAndImport(Score* score, const QString& name, QIODevice* dev)
|
|
{
|
|
// validate the file
|
|
Score::FileError res;
|
|
res = doValidate(name, dev);
|
|
if (res != Score::FileError::FILE_NO_ERROR)
|
|
return res;
|
|
|
|
// pass 1
|
|
dev->seek(0);
|
|
MxmlReaderFirstPass pass1;
|
|
res = pass1.setContent(dev);
|
|
if (res != Score::FileError::FILE_NO_ERROR)
|
|
return res;
|
|
pass1.parseFile();
|
|
|
|
// import the file
|
|
dev->seek(0);
|
|
res = doImport(score, name, dev, pass1);
|
|
qDebug("importMusicXml() return %hhd", res);
|
|
return res;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// importMusicXml
|
|
// return Score::FileError::FILE_* errors
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Import MusicXML file \a name into the Score.
|
|
*/
|
|
|
|
Score::FileError importMusicXml(Score* score, const QString& name)
|
|
{
|
|
qDebug("importMusicXml(%p, %s)", score, qPrintable(name));
|
|
|
|
// open the MusicXML file
|
|
QFile xmlFile(name);
|
|
if (!xmlFile.exists())
|
|
return Score::FileError::FILE_NOT_FOUND;
|
|
if (!xmlFile.open(QIODevice::ReadOnly)) {
|
|
qDebug("importMusicXml() could not open MusicXML file '%s'", qPrintable(name));
|
|
MScore::lastError = QObject::tr("Could not open MusicXML file\n%1").arg(name);
|
|
return Score::FileError::FILE_OPEN_ERROR;
|
|
}
|
|
|
|
// and import it
|
|
return doValidateAndImport(score, name, &xmlFile);
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// importCompressedMusicXml
|
|
// return false on error
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Import compressed MusicXML file \a name into the Score.
|
|
*/
|
|
|
|
Score::FileError importCompressedMusicXml(Score* score, const QString& name)
|
|
{
|
|
qDebug("importCompressedMusicXml(%p, %s)", score, qPrintable(name));
|
|
|
|
// open the compressed MusicXML file
|
|
QFile mxlFile(name);
|
|
if (!mxlFile.exists())
|
|
return Score::FileError::FILE_NOT_FOUND;
|
|
if (!mxlFile.open(QIODevice::ReadOnly)) {
|
|
qDebug("importCompressedMusicXml() could not open compressed MusicXML file '%s'", qPrintable(name));
|
|
MScore::lastError = QObject::tr("Could not open compressed MusicXML file\n%1").arg(name);
|
|
return Score::FileError::FILE_OPEN_ERROR;
|
|
}
|
|
|
|
// extract the root file
|
|
QByteArray data;
|
|
if (!extractRootfile(&mxlFile, data))
|
|
return Score::FileError::FILE_BAD_FORMAT; // appropriate error message has been printed by extractRootfile
|
|
QBuffer buffer(&data);
|
|
buffer.open(QIODevice::ReadOnly);
|
|
|
|
// and import it
|
|
return doValidateAndImport(score, name, &buffer);
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// import
|
|
// scorePartwise
|
|
// part-list
|
|
// part
|
|
// work
|
|
// identification
|
|
//---------------------------------------------------------
|
|
|
|
static void tupletAssert();
|
|
|
|
/**
|
|
Parse the MusicXML file, which must be in score-partwise format.
|
|
*/
|
|
|
|
void MusicXml::import(Score* s)
|
|
{
|
|
tupletAssert();
|
|
score = s;
|
|
|
|
// assume no multi-measure rests, will be set to true when encountering a multi-measure rest
|
|
// required as multi-measure rest is a meaure attribute in MusicXML instead of a style setting
|
|
score->style()->set(StyleIdx::createMultiMeasureRests, false);
|
|
|
|
for (QDomElement e = doc->documentElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
if (e.tagName() == "score-partwise")
|
|
scorePartwise(e.firstChildElement());
|
|
else
|
|
domError(e);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// initPartState
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Initialize members as required for reading the MusicXML part element.
|
|
TODO: factor out part reading into a separate
|
|
*/
|
|
|
|
void MusicXml::initPartState()
|
|
{
|
|
fractionTSig = Fraction(0, 1);
|
|
tick = 0;
|
|
maxtick = 0;
|
|
prevtick = 0;
|
|
lastMeasureLen = 0;
|
|
multiMeasureRestCount = -1;
|
|
startMultiMeasureRest = false;
|
|
tie = 0;
|
|
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i)
|
|
slur[i] = SlurDesc();
|
|
for (int i = 0; i < MAX_BRACKETS; ++i)
|
|
bracket[i] = 0;
|
|
for (int i = 0; i < MAX_DASHES; ++i)
|
|
dashes[i] = 0;
|
|
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i)
|
|
ottavas[i] = 0;
|
|
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i)
|
|
hairpins[i] = 0;
|
|
for (int i = 0; i < MAX_NUMBER_LEVEL; ++i)
|
|
trills[i] = 0;
|
|
pedal = 0;
|
|
pedalContinue = 0;
|
|
harmony = 0;
|
|
tremStart = 0;
|
|
figBass = 0;
|
|
figBassExtend = false;
|
|
glissandoText = "";
|
|
glissandoColor = "";
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addText
|
|
//---------------------------------------------------------
|
|
|
|
static void addText(VBox* vbx, Score* s, QString strTxt, TextStyleType stl)
|
|
{
|
|
if (!strTxt.isEmpty()) {
|
|
Text* text = new Text(s);
|
|
text->setTextStyleType(stl);
|
|
text->setText(strTxt);
|
|
vbx->add(text);
|
|
}
|
|
}
|
|
|
|
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->setText(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.
|
|
*/
|
|
|
|
void MusicXml::doCredits()
|
|
{
|
|
// IMPORT_LAYOUT
|
|
//qDebug("MusicXml::doCredits()");
|
|
const PageFormat* pf = score->pageFormat();
|
|
/*
|
|
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 pw = pf->width() * 10 * MScore::DPI / score->spatium();
|
|
const double ph = pf->height() * 10 * MScore::DPI / score->spatium();
|
|
//const double tm = pf->oddTopMargin() * 10 * MScore::DPI / score->spatium();
|
|
//const double tov = ph - tm;
|
|
const int pw1 = pageWidth / 3;
|
|
const int pw2 = pageWidth * 2 / 3;
|
|
const int ph2 = pageHeight / 2;
|
|
/*
|
|
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, TextStyleType::TITLE);
|
|
addText(vbox, score, strSubTitle, TextStyleType::SUBTITLE);
|
|
addText(vbox, score, strComposer, TextStyleType::COMPOSER);
|
|
addText(vbox, score, strPoet, TextStyleType::POET);
|
|
addText(vbox, score, strTranslator, 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);
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// 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.
|
|
*/
|
|
|
|
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()) {
|
|
qDebug("ImportMusicXml: time symbol <%s> not recognized with beats=%s and beat-type=%s",
|
|
qPrintable(timeSymbol), qPrintable(beats), qPrintable(beatType));
|
|
return false;
|
|
}
|
|
|
|
btp = beatType.toInt();
|
|
QStringList list = beats.split("+");
|
|
for (int i = 0; i < list.size(); i++)
|
|
bts += list.at(i).toInt();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// readPageFormat
|
|
//---------------------------------------------------------
|
|
|
|
void MusicXml::readPageFormat(PageFormat* pf, QDomElement de, qreal conversion)
|
|
{
|
|
qreal _oddRightMargin = 0.0;
|
|
qreal _evenRightMargin = 0.0;
|
|
QSizeF size;
|
|
|
|
for (QDomElement e = de.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
const QString& tag(e.tagName());
|
|
const QString& val(e.text());
|
|
if (tag == "page-margins") {
|
|
QString type = e.attribute("type","both");
|
|
qreal lm = 0.0, rm = 0.0, tm = 0.0, bm = 0.0;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
const QString& tag(ee.tagName());
|
|
qreal val = ee.text().toDouble() * conversion;
|
|
if (tag == "left-margin")
|
|
lm = val;
|
|
else if (tag == "right-margin")
|
|
rm = val;
|
|
else if (tag == "top-margin")
|
|
tm = val;
|
|
else if (tag == "bottom-margin")
|
|
bm = val;
|
|
else
|
|
domError(ee);
|
|
}
|
|
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 (tag == "page-height") {
|
|
size.rheight() = val.toDouble() * conversion;
|
|
// set pageHeight and pageWidth for use by doCredits()
|
|
pageHeight = static_cast<int>(val.toDouble() + 0.5);
|
|
}
|
|
else if (tag == "page-width") {
|
|
size.rwidth() = val.toDouble() * conversion;
|
|
// set pageHeight and pageWidth for use by doCredits()
|
|
pageWidth = static_cast<int>(val.toDouble() + 0.5);
|
|
}
|
|
else
|
|
domError(e);
|
|
}
|
|
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
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// 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;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// nextPartOfFormattedString
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read the next part of a MusicXML formatted string and convert to MuseScore internal encoding.
|
|
*/
|
|
|
|
static QString nextPartOfFormattedString(QDomElement e)
|
|
{
|
|
QString txt = e.text();
|
|
QString syms = text2syms(txt);
|
|
QString lang = e.attribute(QString("xml:lang"), "it");
|
|
QString fontWeight = e.attribute(QString("font-weight"));
|
|
QString fontSize = e.attribute(QString("font-size"));
|
|
QString fontStyle = e.attribute(QString("font-style"));
|
|
QString underline = e.attribute(QString("underline"));
|
|
QString fontFamily = e.attribute(QString("font-family"));
|
|
// TODO: color, enclosure, yoffset in only part of the text, ...
|
|
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;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// updateStyles
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
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);
|
|
}
|
|
|
|
/**
|
|
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);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// scorePartwise
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read the MusicXML score-partwise element.
|
|
*/
|
|
|
|
void MusicXml::scorePartwise(QDomElement ee)
|
|
{
|
|
// In a first pass collect all parts in case the part-list does not
|
|
// list them all. Incomplete part-list's are generated by some versions
|
|
// of Finale.
|
|
// Furthermore, determine the length in ticks of each measure in the part
|
|
for (QDomElement e = ee; !e.isNull(); e = e.nextSiblingElement()) {
|
|
if (e.tagName() == "part") {
|
|
QString id = e.attribute(QString("id"));
|
|
if (id == "")
|
|
qDebug("MusicXML import: part without id");
|
|
else {
|
|
Part* part = new Part(score);
|
|
part->setId(id);
|
|
score->appendPart(part);
|
|
Staff* staff = new Staff(score);
|
|
staff->setPart(part);
|
|
part->staves()->push_back(staff);
|
|
score->staves().push_back(staff);
|
|
tuplets.resize(VOICES); // part now contains one staff, thus VOICES voices
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read the score
|
|
for (QDomElement e = ee; !e.isNull(); e = e.nextSiblingElement()) {
|
|
QString tag(e.tagName());
|
|
if (tag == "part-list")
|
|
xmlPartList(e.firstChildElement());
|
|
else if (tag == "part")
|
|
xmlPart(e.firstChildElement(), e.attribute(QString("id")));
|
|
else if (tag == "work") {
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "work-number")
|
|
score->setMetaTag("workNumber", ee.text());
|
|
else if (ee.tagName() == "work-title")
|
|
score->setMetaTag("workTitle", ee.text());
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else if (tag == "identification") {
|
|
// read the metadata
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "creator") {
|
|
// type is an arbitrary label
|
|
score->setMetaTag(ee.attribute(QString("type")), ee.text());
|
|
}
|
|
else if (ee.tagName() == "rights")
|
|
score->setMetaTag("copyright", ee.text());
|
|
else if (ee.tagName() == "encoding")
|
|
score->setMetaTag("encoding", ee.text());
|
|
else if (ee.tagName() == "source")
|
|
score->setMetaTag("source", ee.text());
|
|
else if (ee.tagName() == "miscellaneous")
|
|
; // ignore
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else if (tag == "defaults") {
|
|
// IMPORT_LAYOUT
|
|
double millimeter = score->spatium()/10.0;
|
|
double tenths = 1.0;
|
|
QString lyricFontFamily;
|
|
QString lyricFontSize;
|
|
QString wordFontFamily;
|
|
QString wordFontSize;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
QString tag(ee.tagName());
|
|
if (tag == "scaling") {
|
|
for (QDomElement eee = ee.firstChildElement(); !eee.isNull(); eee = eee.nextSiblingElement()) {
|
|
QString tag(eee.tagName());
|
|
if (tag == "millimeters")
|
|
millimeter = eee.text().toDouble();
|
|
else if (tag == "tenths")
|
|
tenths = eee.text().toDouble();
|
|
else
|
|
domError(eee);
|
|
}
|
|
double _spatium = MScore::DPMM * (millimeter * 10.0 / tenths);
|
|
if (preferences.musicxmlImportLayout)
|
|
score->setSpatium(_spatium);
|
|
}
|
|
else if (tag == "page-layout") {
|
|
PageFormat pf;
|
|
readPageFormat(&pf, ee, millimeter / (tenths * INCH));
|
|
if (preferences.musicxmlImportLayout)
|
|
score->setPageFormat(pf);
|
|
}
|
|
else if (tag == "system-layout") {
|
|
for (QDomElement eee = ee.firstChildElement(); !eee.isNull(); eee = eee.nextSiblingElement()) {
|
|
QString tag(eee.tagName());
|
|
Spatium val(eee.text().toDouble() / 10.0);
|
|
if (tag == "system-margins")
|
|
;
|
|
else if (tag == "system-distance") {
|
|
if (preferences.musicxmlImportLayout) {
|
|
score->style()->set(StyleIdx::minSystemDistance, val);
|
|
qDebug("system distance %f", val.val());
|
|
}
|
|
}
|
|
else if (tag == "top-system-distance")
|
|
;
|
|
else
|
|
domError(eee);
|
|
}
|
|
}
|
|
else if (tag == "staff-layout") {
|
|
for (QDomElement eee = ee.firstChildElement(); !eee.isNull(); eee = eee.nextSiblingElement()) {
|
|
QString tag(eee.tagName());
|
|
Spatium val(eee.text().toDouble() / 10.0);
|
|
if (tag == "staff-distance") {
|
|
if (preferences.musicxmlImportLayout)
|
|
score->style()->set(StyleIdx::staffDistance, val);
|
|
}
|
|
else
|
|
domError(eee);
|
|
}
|
|
}
|
|
else if (tag == "music-font")
|
|
domNotImplemented(ee);
|
|
else if (tag == "word-font") {
|
|
wordFontFamily = ee.attribute("font-family");
|
|
wordFontSize = ee.attribute("font-size");
|
|
}
|
|
else if (tag == "lyric-font") {
|
|
lyricFontFamily = ee.attribute("font-family");
|
|
lyricFontSize = ee.attribute("font-size");
|
|
}
|
|
else if (tag == "appearance")
|
|
domNotImplemented(ee);
|
|
else if (tag == "lyric-language")
|
|
domNotImplemented(ee);
|
|
else
|
|
domError(ee);
|
|
}
|
|
|
|
/*
|
|
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 ?
|
|
// IMPORT_LAYOUT END
|
|
}
|
|
else if (tag == "movement-number")
|
|
score->setMetaTag("movementNumber", e.text());
|
|
else if (tag == "movement-title")
|
|
score->setMetaTag("movementTitle", e.text());
|
|
else if (tag == "credit") {
|
|
QString page = e.attribute(QString("page"), "1");
|
|
// handle only page 1 credits (to extract title etc.)
|
|
if (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;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
QString tag(ee.tagName());
|
|
if (tag == "credit-words") {
|
|
// IMPORT_LAYOUT
|
|
if (!creditWordsRead) {
|
|
defaultx = ee.attribute(QString("default-x")).toDouble();
|
|
defaulty = ee.attribute(QString("default-y")).toDouble();
|
|
justify = ee.attribute(QString("justify"));
|
|
halign = ee.attribute(QString("halign"));
|
|
valign = ee.attribute(QString("valign"));
|
|
creditWordsRead = true;
|
|
}
|
|
crwords += nextPartOfFormattedString(ee);
|
|
}
|
|
else if (tag == "credit-type")
|
|
domNotImplemented(ee); // TODO
|
|
else
|
|
domError(ee);
|
|
}
|
|
if (crwords != "") {
|
|
CreditWords* cw = new CreditWords(defaultx, defaulty, justify, halign, valign, crwords);
|
|
credits.append(cw);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
domError(e);
|
|
}
|
|
|
|
// 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 %d 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());
|
|
}
|
|
}
|
|
|
|
// having read all parts (meaning all segments have been created),
|
|
// now attach all jumps and markers to segments
|
|
// simply use the first SegChordRest in the measure
|
|
for (int i = 0; i < jumpsMarkers.size(); i++) {
|
|
Measure* meas = jumpsMarkers.at(i).meas();
|
|
qDebug("jumpsMarkers jm %p meas %p ",
|
|
jumpsMarkers.at(i).el(), meas);
|
|
qDebug("attach to measure %p", meas);
|
|
meas->add(jumpsMarkers.at(i).el());
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// 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("part-group number=%d start=%d span=%d type=%d",
|
|
// n, pgs[n]->start, pgs[n]->span, pgs[n]->type);
|
|
pgl.push_back(pgs[n]);
|
|
pgs.erase(n);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlPartList
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read the MusicXML part-list element.
|
|
*/
|
|
|
|
void MusicXml::xmlPartList(QDomElement e)
|
|
{
|
|
int scoreParts = 0;
|
|
MusicXmlPartGroupMap partGroups;
|
|
|
|
for (; !e.isNull(); e = e.nextSiblingElement()) {
|
|
if (e.tagName() == "score-part")
|
|
xmlScorePart(e.firstChildElement(), e.attribute(QString("id")), scoreParts);
|
|
else if (e.tagName() == "part-group") {
|
|
bool barlineSpan = true;
|
|
int number = e.attribute(QString("number")).toInt() - 1;
|
|
QString symbol = "";
|
|
QString type = e.attribute(QString("type"));
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "group-symbol")
|
|
symbol = ee.text();
|
|
else if (ee.tagName() == "group-barline") {
|
|
if (ee.text() == "no")
|
|
barlineSpan = false;
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
if (type == "start")
|
|
partGroupStart(partGroups, number, scoreParts, symbol, barlineSpan);
|
|
else if (type == "stop")
|
|
partGroupStop(partGroups, number, scoreParts, partGroupList);
|
|
else
|
|
qDebug("Import MusicXml:xmlPartList: part-group type '%s' not supported",
|
|
qPrintable(type));
|
|
}
|
|
else
|
|
domError(e);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlScorePart
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read the MusicXML score-part element.
|
|
*/
|
|
|
|
void MusicXml::xmlScorePart(QDomElement e, QString id, int& parts)
|
|
{
|
|
Part* part = 0;
|
|
foreach(Part* p, score->parts()) {
|
|
if (p->id() == id) {
|
|
part = p;
|
|
parts++;
|
|
break;
|
|
}
|
|
}
|
|
if (part == 0) {
|
|
// Some versions of Rosegarden (at least v11.02) mention parts
|
|
// in the <part-list>, but don't contain the corresponding <part>s.
|
|
// (i.e. the part-list is overcomplete).
|
|
// These parts are reported but can safely be ignored.
|
|
qDebug("Import MusicXml::xmlScorePart: cannot find part %s", qPrintable(id));
|
|
return;
|
|
}
|
|
|
|
drumsets.insert(id, MusicXMLDrumset());
|
|
|
|
for (; !e.isNull(); e = e.nextSiblingElement()) {
|
|
if (e.tagName() == "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
|
|
if (!(e.attribute("print-object") == "no"))
|
|
part->setLongName(e.text());
|
|
part->setPartName(e.text());
|
|
}
|
|
else if (e.tagName() == "part-name-display") {
|
|
// TODO
|
|
domNotImplemented(e);
|
|
}
|
|
else if (e.tagName() == "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
|
|
if (!(e.attribute("print-object") == "no"))
|
|
part->setShortName(e.text());
|
|
}
|
|
else if (e.tagName() == "part-abbreviation-display") {
|
|
// TODO
|
|
domNotImplemented(e);
|
|
}
|
|
else if (e.tagName() == "score-instrument") {
|
|
QString instrId = e.attribute("id");
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "ensemble")
|
|
domNotImplemented(e);
|
|
else if (ee.tagName() == "instrument-name") {
|
|
drumsets[id].insert(instrId, MusicXMLDrumInstrument(ee.text()));
|
|
// Element instrument-name is typically not displayed in the score,
|
|
// but used only internally
|
|
if (drumsets[id].contains(instrId))
|
|
drumsets[id][instrId].name = ee.text();
|
|
// try to prevent an empty track name
|
|
if (part->partName() == "")
|
|
part->setPartName(ee.text());
|
|
}
|
|
else if (ee.tagName() == "instrument-sound")
|
|
domNotImplemented(e);
|
|
else if (ee.tagName() == "solo")
|
|
domNotImplemented(e);
|
|
else if (ee.tagName() == "virtual-instrument")
|
|
domNotImplemented(e);
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else if (e.tagName() == "midi-instrument") {
|
|
QString instrId = e.attribute("id");
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "midi-bank")
|
|
domNotImplemented(e);
|
|
else if (ee.tagName() == "midi-channel") {
|
|
int channel = ee.text().toInt();
|
|
if (channel < 1) {
|
|
qDebug("MusicXml::xmlScorePart: incorrect midi-channel: %d", channel);
|
|
channel = 1;
|
|
}
|
|
else if (channel > 16) {
|
|
qDebug("MusicXml::xmlScorePart: incorrect midi-channel: %d", channel);
|
|
channel = 16;
|
|
}
|
|
if (drumsets[id].contains(instrId))
|
|
drumsets[id][instrId].midiChannel = channel - 1;
|
|
}
|
|
else if (ee.tagName() == "midi-program") {
|
|
int program = ee.text().toInt();
|
|
// Bug fix for Cubase 6.5.5 which generates <midi-program>0</midi-program>
|
|
// Check program number range
|
|
if (program < 1) {
|
|
qDebug("MusicXml::xmlScorePart: incorrect midi-program: %d", program);
|
|
program = 1;
|
|
}
|
|
else if (program > 128) {
|
|
qDebug("MusicXml::xmlScorePart: incorrect midi-program: %d", program);
|
|
program = 128;
|
|
}
|
|
if (drumsets[id].contains(instrId))
|
|
drumsets[id][instrId].midiProgram = program - 1;
|
|
}
|
|
else if (ee.tagName() == "midi-unpitched") {
|
|
if (drumsets[id].contains(instrId))
|
|
drumsets[id][instrId].pitch = ee.text().toInt() - 1;
|
|
}
|
|
else if (ee.tagName() == "volume") {
|
|
double vol = ee.text().toDouble();
|
|
if (vol >= 0 && vol <= 100) {
|
|
if (drumsets[id].contains(instrId))
|
|
drumsets[id][instrId].midiVolume = static_cast<int>((vol / 100) * 127);
|
|
}
|
|
else
|
|
qDebug("MusicXml::xmlScorePart: incorrect midi-volume: %g", vol);
|
|
}
|
|
else if (ee.tagName() == "pan") {
|
|
double pan = ee.text().toDouble();
|
|
if (pan >= -90 && pan <= 90) {
|
|
if (drumsets[id].contains(instrId))
|
|
drumsets[id][instrId].midiPan = static_cast<int>(((pan + 90) / 180) * 127);
|
|
}
|
|
else
|
|
qDebug("MusicXml::xmlScorePart: incorrect midi-volume: %g", pan);
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else if (e.tagName() == "midi-device") {
|
|
// TODO
|
|
domNotImplemented(e);
|
|
}
|
|
else
|
|
domError(e);
|
|
}
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// VoiceDesc
|
|
//---------------------------------------------------------
|
|
|
|
VoiceDesc::VoiceDesc() : _staff(-1), _voice(-1), _overlaps(false)
|
|
{
|
|
for (int i = 0; i < MAX_STAVES; ++i) {
|
|
_chordRests[i] = 0;
|
|
_staffAlloc[i] = -1;
|
|
_voices[i] = -1;
|
|
}
|
|
}
|
|
|
|
void VoiceDesc::incrChordRests(int s)
|
|
{
|
|
if (0 <= s && s < MAX_STAVES)
|
|
_chordRests[s]++;
|
|
}
|
|
|
|
int VoiceDesc::numberChordRests() const
|
|
{
|
|
int res = 0;
|
|
for (int i = 0; i < MAX_STAVES; ++i)
|
|
res += _chordRests[i];
|
|
return res;
|
|
}
|
|
|
|
int VoiceDesc::preferredStaff() const
|
|
{
|
|
int max = 0;
|
|
int res = 0;
|
|
for (int i = 0; i < MAX_STAVES; ++i)
|
|
if (_chordRests[i] > max) {
|
|
max = _chordRests[i];
|
|
res = i;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
QString VoiceDesc::toString() const
|
|
{
|
|
QString res = "[";
|
|
for (int i = 0; i < MAX_STAVES; ++i)
|
|
res += QString(" %1").arg(_chordRests[i]);
|
|
res += QString(" ] overlaps %1").arg(_overlaps);
|
|
if (_overlaps) {
|
|
res += " staffAlloc [";
|
|
for (int i = 0; i < MAX_STAVES; ++i)
|
|
res += QString(" %1").arg(_staffAlloc[i]);
|
|
res += " ] voices [";
|
|
for (int i = 0; i < MAX_STAVES; ++i)
|
|
res += QString(" %1").arg(_voices[i]);
|
|
res += " ]";
|
|
}
|
|
else
|
|
res += QString(" staff %1 voice %2").arg(_staff + 1).arg(_voice + 1);
|
|
return res;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// fillGap -- fill one gap (tstart - tend) in this track in this measure with rest(s)
|
|
//---------------------------------------------------------
|
|
|
|
static void fillGap(Measure* measure, int track, int tstart, int tend)
|
|
{
|
|
int ctick = tstart;
|
|
int restLen = tend - tstart;
|
|
// qDebug("\nfillGIFV fillGap(measure %p track %d tstart %d tend %d) restLen %d len",
|
|
// measure, track, tstart, tend, restLen);
|
|
// note: as MScore::division (#ticks in a quarter note) equals 480
|
|
// MScore::division / 64 (#ticks in a 256th note) uequals 7.5 but is rounded down to 7
|
|
while (restLen > MScore::division / 64) {
|
|
int len = restLen;
|
|
TDuration d(TDuration::DurationType::V_INVALID);
|
|
if (measure->ticks() == restLen)
|
|
d.setType(TDuration::DurationType::V_MEASURE);
|
|
else
|
|
d.setVal(len);
|
|
Rest* rest = new Rest(measure->score(), d);
|
|
rest->setDuration(Fraction::fromTicks(len));
|
|
rest->setTrack(track);
|
|
rest->setVisible(false);
|
|
Segment* s = measure->getSegment(rest, tstart);
|
|
s->add(rest);
|
|
len = rest->globalDuration().ticks();
|
|
// qDebug(" %d", len);
|
|
ctick += len;
|
|
restLen -= len;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// fillGapsInFirstVoices -- fill gaps in first voice of every staff in this measure for this part with rest(s)
|
|
//---------------------------------------------------------
|
|
|
|
static void fillGapsInFirstVoices(Measure* measure, Part* part)
|
|
{
|
|
int measTick = measure->tick();
|
|
int measLen = measure->ticks();
|
|
int nextMeasTick = measTick + measLen;
|
|
int staffIdx = part->score()->staffIdx(part);
|
|
/*
|
|
qDebug("fillGIFV measure %p part %p idx %d nstaves %d tick %d - %d (len %d)",
|
|
measure, part, staffIdx, part->nstaves(),
|
|
measTick, nextMeasTick, measLen);
|
|
*/
|
|
for (int st = 0; st < part->nstaves(); ++st) {
|
|
int track = (staffIdx + st) * VOICES;
|
|
int endOfLastCR = measTick;
|
|
for (Segment* s = measure->first(); s; s = s->next()) {
|
|
// qDebug("fillGIFV segment %p tp %s", s, s->subTypeName());
|
|
Element* el = s->element(track);
|
|
if (el) {
|
|
// qDebug(" el[%d] %p", track, el);
|
|
if (s->isChordRest()) {
|
|
ChordRest* cr = static_cast<ChordRest*>(el);
|
|
int crTick = cr->tick();
|
|
int crLen = cr->globalDuration().ticks();
|
|
int nextCrTick = crTick + crLen;
|
|
/*
|
|
qDebug(" chord/rest tick %d - %d (len %d)",
|
|
crTick, nextCrTick, crLen);
|
|
*/
|
|
if (crTick > endOfLastCR) {
|
|
/*
|
|
qDebug(" GAP: track %d tick %d - %d",
|
|
track, endOfLastCR, crTick);
|
|
*/
|
|
fillGap(measure, track, endOfLastCR, crTick);
|
|
}
|
|
endOfLastCR = nextCrTick;
|
|
}
|
|
}
|
|
}
|
|
if (nextMeasTick > endOfLastCR) {
|
|
/*
|
|
qDebug("fillGIFV measure end GAP: track %d tick %d - %d",
|
|
track, endOfLastCR, nextMeasTick);
|
|
*/
|
|
fillGap(measure, track, endOfLastCR, nextMeasTick);
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// findDeleteWords
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Find a non-empty staff text in \a s at \a track (which originates as MusicXML <words>).
|
|
If found, delete it and return its text.
|
|
*/
|
|
|
|
static QString findDeleteStaffText(Segment* s, int track)
|
|
{
|
|
//qDebug("findDeleteWords(s %p track %d)", s, track);
|
|
foreach (Element* e, s->annotations()) {
|
|
//qDebug("findDeleteWords e %p type %hhd track %d", e, e->type(), e->track());
|
|
if (e->type() != Element::Type::STAFF_TEXT || e->track() < track || e->track() >= track+VOICES)
|
|
continue;
|
|
Text* t = static_cast<Text*>(e);
|
|
//qDebug("findDeleteWords t %p text '%s'", t, qPrintable(t->text()));
|
|
QString res = t->text();
|
|
if (res != "") {
|
|
s->remove(t);
|
|
return res;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlPart
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read the MusicXML part element.
|
|
*/
|
|
|
|
void MusicXml::xmlPart(QDomElement e, QString id)
|
|
{
|
|
//qDebug("xmlPart(id='%s')", qPrintable(id));
|
|
if (id == "") {
|
|
qDebug("MusicXML import: part without id");
|
|
return;
|
|
}
|
|
Part* part = 0;
|
|
foreach(Part* p, score->parts()) {
|
|
if (p->id() == id) {
|
|
part = p;
|
|
break;
|
|
}
|
|
}
|
|
if (part == 0) {
|
|
qDebug("Import MusicXml:xmlPart: cannot find part %s", id.toLatin1().data());
|
|
return;
|
|
}
|
|
|
|
initPartState();
|
|
|
|
KeySigEvent ev;
|
|
KeySig currKeySig;
|
|
currKeySig.setKeySigEvent(ev);
|
|
|
|
// initVoiceMapperAndMapVoices(e);
|
|
voicelist = pass1.getVoiceList(id);
|
|
#ifdef DEBUG_VOICE_MAPPER
|
|
// debug: print voice mapper contents
|
|
qDebug("voiceMapperStats: new staff");
|
|
for (QMap<QString, Ms::VoiceDesc>::const_iterator i = voicelist.constBegin(); i != voicelist.constEnd(); ++i) {
|
|
qDebug("voiceMapperStats: voice %s staff data %s",
|
|
qPrintable(i.key()), qPrintable(i.value().toString()));
|
|
}
|
|
#endif
|
|
|
|
if (!score->measures()->first()) {
|
|
doCredits();
|
|
}
|
|
|
|
for (int measureNr = 0; !e.isNull(); e = e.nextSiblingElement(), measureNr++) {
|
|
if (e.tagName() == "measure") {
|
|
// set the correct start tick for the measure
|
|
tick = measureStart.at(measureNr);
|
|
Measure* measure = xmlMeasure(part, e, e.attribute(QString("number")).toInt()-1, measureLength.at(measureNr), &currKeySig);
|
|
if (measure)
|
|
fillGapsInFirstVoices(measure, part);
|
|
}
|
|
else
|
|
domError(e);
|
|
}
|
|
|
|
//qDebug("spanner list:");
|
|
auto i = spanners.constBegin();
|
|
while (i != spanners.constEnd()) {
|
|
Spanner* sp = i.key();
|
|
int tick1 = i.value().first;
|
|
int tick2 = i.value().second;
|
|
//qDebug("spanner %p tp %hhd tick1 %d tick2 %d track %d track2 %d",
|
|
// sp, sp->type(), tick1, tick2, sp->track(), sp->track2());
|
|
sp->setTick(tick1);
|
|
sp->setTick2(tick2);
|
|
sp->score()->addElement(sp);
|
|
++i;
|
|
}
|
|
spanners.clear();
|
|
|
|
// determine if the part contains a drumset
|
|
// this is the case if any instrument has a midi-unpitched element,
|
|
// (which stored in the MusicXMLDrumInstrument pitch field)
|
|
// if the part contains a drumset, Drumset drumset is intialized
|
|
// debug: also dump the drumset for this part
|
|
|
|
Drumset* drumset = new Drumset;
|
|
bool hasDrumset = false;
|
|
drumset->clear();
|
|
MusicXMLDrumsetIterator ii(drumsets[id]);
|
|
while (ii.hasNext()) {
|
|
ii.next();
|
|
//qDebug("xmlPart: instrument: %s %s", qPrintable(ii.key()), qPrintable(ii.value().toString()));
|
|
int pitch = ii.value().pitch;
|
|
if (0 <= pitch && pitch <= 127) {
|
|
hasDrumset = true;
|
|
drumset->drum(ii.value().pitch)
|
|
= DrumInstrument(ii.value().name.toLatin1().constData(),
|
|
ii.value().notehead, ii.value().line, ii.value().stemDirection);
|
|
}
|
|
}
|
|
//qDebug("xmlPart: hasDrumset %d", hasDrumset);
|
|
|
|
// debug: dump the instrument map
|
|
/*
|
|
{
|
|
auto il = pass1.getInstrList(id);
|
|
for (auto it = il.cbegin(); it != il.cend(); ++it) {
|
|
Fraction f = (*it).first;
|
|
qDebug("xmlPart: instrument map: tick %s (%d) instr '%s'", qPrintable(f.print()), f.ticks(), qPrintable((*it).second));
|
|
}
|
|
}
|
|
*/
|
|
|
|
// set the parts first instrument
|
|
if (drumsets[id].size() > 0) {
|
|
QString instrId = pass1.getInstrList(id).instrument(Fraction(0, 1));
|
|
//qDebug("xmlPart: initial instrument '%s'", qPrintable(instrId));
|
|
MusicXMLDrumInstrument instr;
|
|
if (instrId == "")
|
|
instr = drumsets[id].first();
|
|
else if (drumsets[id].contains(instrId))
|
|
instr = drumsets[id].value(instrId);
|
|
else {
|
|
qDebug("xmlPart: initial instrument '%s' not found in part '%s'", qPrintable(instrId), qPrintable(id));
|
|
instr = drumsets[id].first();
|
|
}
|
|
// part->setMidiChannel(instr.midiChannel); not required (is a NOP anyway)
|
|
part->setMidiProgram(instr.midiProgram);
|
|
part->setPan(instr.midiPan);
|
|
part->setVolume(instr.midiVolume);
|
|
part->instr()->setTrackName(instr.name);
|
|
}
|
|
else
|
|
qDebug("xmlPart: no instrument found for part '%s'", qPrintable(id));
|
|
|
|
if (hasDrumset) {
|
|
// set staff type to percussion if incorrectly imported as pitched staff
|
|
// Note: part has been read, staff type already set based on clef type and staff-details
|
|
// but may be incorrect for a percussion staff that does not use a percussion clef
|
|
for (int j = 0; j < part->nstaves(); ++j)
|
|
if (part->staff(j)->lines() == 5 && !part->staff(j)->isDrumStaff())
|
|
part->staff(j)->setStaffType(StaffType::preset(StaffTypes::PERC_DEFAULT));
|
|
// set drumset for instrument
|
|
part->instr()->setUseDrumset(DrumsetKind::DEFAULT_DRUMS);
|
|
part->instr()->setDrumset(drumset);
|
|
part->instr()->channel(0).bank = 128;
|
|
part->instr()->channel(0).updateInitList();
|
|
}
|
|
else {
|
|
// drumset is not needed
|
|
delete drumset;
|
|
// set the instruments for this part
|
|
MusicXmlInstrList il = pass1.getInstrList(id);
|
|
for (auto it = il.cbegin(); it != il.cend(); ++it) {
|
|
Fraction f = (*it).first;
|
|
if (f > Fraction(0, 1)) {
|
|
auto instrId = (*it).second;
|
|
int staff = score->staffIdx(part);
|
|
int track = staff * VOICES;
|
|
//qDebug("xmlPart: instrument change: tick %s (%d) track %d instr '%s'",
|
|
// qPrintable(f.print()), f.ticks(), track, qPrintable(instrId));
|
|
Segment* segment = score->tick2segment(f.ticks(), true, Segment::Type::ChordRest, true);
|
|
if (!segment)
|
|
qDebug("xmlPart: segment for instrument change at tick %d not found", f.ticks());
|
|
else if (!drumsets[id].contains(instrId))
|
|
qDebug("xmlPart: changed instrument '%s' at tick %d not found in part '%s'",
|
|
qPrintable(instrId), f.ticks(), qPrintable(id));
|
|
else {
|
|
MusicXMLDrumInstrument mxmlInstr = drumsets[id].value(instrId);
|
|
Instrument instr;
|
|
// part->setMidiChannel(instr.midiChannel); not required (is a NOP anyway)
|
|
instr.channel(0).program = mxmlInstr.midiProgram;
|
|
instr.channel(0).pan = mxmlInstr.midiPan;
|
|
instr.channel(0).volume = mxmlInstr.midiVolume;
|
|
instr.setTrackName(mxmlInstr.name);
|
|
InstrumentChange* ic = new InstrumentChange(score);
|
|
ic->setInstrument(instr);
|
|
ic->setTrack(track);
|
|
// if there is already a staff text at this tick / track,
|
|
// delete it and use its text here instead of "Instrument"
|
|
QString text = findDeleteStaffText(segment, track);
|
|
if (text == "")
|
|
ic->setText("Instrument");
|
|
else
|
|
ic->setText(text);
|
|
segment->add(ic);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//qDebug("xmlPart: end");
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// readFiguredBassItem
|
|
//---------------------------------------------------------
|
|
|
|
static void readFiguredBassItem(FiguredBassItem* fgi, const QDomElement& de,
|
|
bool paren, bool& extend)
|
|
{
|
|
// read the <figure> node de
|
|
for (QDomElement e = de.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
const QString& tag(e.tagName());
|
|
const QString& val(e.text());
|
|
int iVal = val.toInt();
|
|
if (tag == "extend")
|
|
extend = true;
|
|
else if (tag == "figure-number") {
|
|
// MusicXML spec states figure-number is a number
|
|
// MuseScore can only handle single digit
|
|
if (1 <= iVal && iVal <= 9)
|
|
fgi->setDigit(iVal);
|
|
}
|
|
else if (tag == "prefix")
|
|
fgi->setPrefix(fgi->MusicXML2Modifier(val));
|
|
else if (tag == "suffix")
|
|
fgi->setSuffix(fgi->MusicXML2Modifier(val));
|
|
else
|
|
domError(e);
|
|
}
|
|
// set parentheses
|
|
if (paren) {
|
|
// parenthesis open
|
|
if (fgi->prefix() != FiguredBassItem::Modifier::NONE)
|
|
fgi->setParenth1(FiguredBassItem::Parenthesis::ROUNDOPEN); // before prefix
|
|
else if (fgi->digit() != FBIDigitNone)
|
|
fgi->setParenth2(FiguredBassItem::Parenthesis::ROUNDOPEN); // before digit
|
|
else if (fgi->suffix() != FiguredBassItem::Modifier::NONE)
|
|
fgi->setParenth3(FiguredBassItem::Parenthesis::ROUNDOPEN); // before suffix
|
|
// parenthesis close
|
|
if (fgi->suffix() != FiguredBassItem::Modifier::NONE)
|
|
fgi->setParenth4(FiguredBassItem::Parenthesis::ROUNDCLOSED); // after suffix
|
|
else if (fgi->digit() != FBIDigitNone)
|
|
fgi->setParenth3(FiguredBassItem::Parenthesis::ROUNDCLOSED); // after digit
|
|
else if (fgi->prefix() != FiguredBassItem::Modifier::NONE)
|
|
fgi->setParenth2(FiguredBassItem::Parenthesis::ROUNDCLOSED); // after prefix
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// Read MusicXML
|
|
//
|
|
// Set the FiguredBass state based on the MusicXML <figured-bass> node de.
|
|
// Note that onNote and ticks must be set by the MusicXML importer,
|
|
// as the required context is not present in the items DOM tree.
|
|
// Exception: if a <duration> element is present, tick can be set.
|
|
// Return true if valid, non-empty figure(s) are found
|
|
// Set extend to true if extend elements were found
|
|
//---------------------------------------------------------
|
|
|
|
static bool readFigBass(FiguredBass* fb, const QDomElement& de, int divisions, bool& extend)
|
|
{
|
|
extend = false;
|
|
bool parentheses = (de.attribute("parentheses") == "yes");
|
|
QString normalizedText;
|
|
int idx = 0;
|
|
for (QDomElement e = de.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
const QString& tag(e.tagName());
|
|
const QString& val(e.text());
|
|
if (tag == "duration") {
|
|
bool ok = true;
|
|
int duration = val.toInt(&ok);
|
|
if (ok) {
|
|
duration *= MScore::division;
|
|
duration /= divisions;
|
|
fb->setTicks(duration);
|
|
}
|
|
else
|
|
qDebug("MusicXml-Import: bad duration value: <%s>",
|
|
qPrintable(val));
|
|
}
|
|
else if (tag == "figure") {
|
|
bool figureExtend = false;
|
|
FiguredBassItem* pItem = new FiguredBassItem(fb->score(), idx++);
|
|
pItem->setTrack(fb->track());
|
|
pItem->setParent(fb);
|
|
readFiguredBassItem(pItem, e, parentheses, figureExtend);
|
|
if (figureExtend)
|
|
extend = true;
|
|
fb->appendItem(pItem);
|
|
// add item normalized text
|
|
if (!normalizedText.isEmpty())
|
|
normalizedText.append('\n');
|
|
normalizedText.append(pItem->normalizedText());
|
|
}
|
|
else {
|
|
domError(e);
|
|
return false;
|
|
}
|
|
}
|
|
fb->setPlainText(normalizedText); // this is the text to show while editing
|
|
bool res = !normalizedText.isEmpty();
|
|
return res;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// removeBeam -- // beam mode for all elements and remove the beam
|
|
//---------------------------------------------------------
|
|
|
|
static void removeBeam(Beam*& beam)
|
|
{
|
|
for (int i = 0; i < beam->elements().size(); ++i)
|
|
beam->elements().at(i)->setBeamMode(Beam::Mode::NONE);
|
|
delete beam;
|
|
beam = 0;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// handleBeamAndStemDir
|
|
//---------------------------------------------------------
|
|
|
|
static void handleBeamAndStemDir(ChordRest* cr, const Beam::Mode bm, const MScore::Direction sd, Beam*& beam)
|
|
{
|
|
if (!cr) return;
|
|
// create a new beam
|
|
if (bm == Beam::Mode::BEGIN) {
|
|
// if currently in a beam, delete it
|
|
if (beam) {
|
|
qDebug("handleBeamAndStemDir() new beam, removing previous incomplete beam %p", beam);
|
|
removeBeam(beam);
|
|
}
|
|
// create a new beam
|
|
beam = new Beam(cr->score());
|
|
beam->setTrack(cr->track());
|
|
beam->setBeamDirection(sd);
|
|
}
|
|
// add ChordRest to beam
|
|
if (beam) {
|
|
// verify still in the same track (switching voices in the middle of a beam is not supported)
|
|
// and in a beam ...
|
|
// (note no check is done on correct order of beam begin/continue/end)
|
|
if (cr->track() == beam->track()
|
|
&& (bm == Beam::Mode::BEGIN || bm == Beam::Mode::MID || bm == Beam::Mode::END)) {
|
|
// ... and actually add cr to the beam
|
|
beam->add(cr);
|
|
}
|
|
else {
|
|
qDebug("handleBeamAndStemDir() from track %d to track %d bm %hhd -> abort beam",
|
|
beam->track(), cr->track(), bm);
|
|
// ... or reset beam mode for all elements and remove the beam
|
|
removeBeam(beam);
|
|
}
|
|
}
|
|
// if no beam, set stem direction on chord itself
|
|
if (!beam) {
|
|
static_cast<Chord*>(cr)->setStemDirection(sd);
|
|
cr->setBeamMode(Beam::Mode::NONE);
|
|
}
|
|
// terminate the currect beam and add to the score
|
|
if (beam && bm == Beam::Mode::END)
|
|
beam = 0;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlMeasure
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read the MusicXML measure element.
|
|
*/
|
|
|
|
Measure* MusicXml::xmlMeasure(Part* part, QDomElement e, int number, Fraction measureLen, KeySig* currKeySig)
|
|
{
|
|
#ifdef DEBUG_TICK
|
|
qDebug("xmlMeasure %d begin", number);
|
|
#endif
|
|
int staves = score->nstaves();
|
|
int staff = score->staffIdx(part);
|
|
|
|
// collect candidates for courtesy accidentals to work out at measure end
|
|
QList<Note*> courtAccNotes;
|
|
QList<int> alterList;
|
|
int alt = -10; // any number outside range of xml-tag "alter"
|
|
QList<bool> accTmp;
|
|
int i = 0;
|
|
while(i < 74){ // number of lines per stave rsp. length of array AccidentalState (no constant found)
|
|
accTmp.append(false);
|
|
i++;
|
|
}
|
|
|
|
// current "tick" within this measure as fraction
|
|
// calculated using note type, backup and forward
|
|
Fraction noteTypeTickFr;
|
|
// maximum "tick" within this measure as fraction
|
|
Fraction maxNoteTypeTickFr;
|
|
// current beam
|
|
Beam* beam = 0;
|
|
|
|
if (staves == 0) {
|
|
qDebug("no staves!");
|
|
return 0;
|
|
}
|
|
|
|
// search measure for tick
|
|
Measure* measure = 0;
|
|
Measure* lastMeasure = 0;
|
|
for (MeasureBase* mb = score->measures()->first(); mb; mb = mb->next()) {
|
|
if (mb->type() != Element::Type::MEASURE)
|
|
continue;
|
|
Measure* m = (Measure*)mb;
|
|
lastMeasure = m;
|
|
if (m->tick() == tick) {
|
|
measure = m;
|
|
break;
|
|
}
|
|
}
|
|
if (!measure) {
|
|
//
|
|
// DEBUG:
|
|
if (lastMeasure && lastMeasure->tick() > tick) {
|
|
qDebug("Measure at position %d not found!", tick);
|
|
}
|
|
measure = new Measure(score);
|
|
measure->setTick(tick);
|
|
measure->setLen(measureLen);
|
|
measure->setNo(number);
|
|
score->measures()->add(measure);
|
|
} else {
|
|
// ws:
|
|
// int pstaves = part->nstaves();
|
|
// for (int i = 0; i < pstaves; ++i) {
|
|
// Staff* reals = score->staff(staff+i);
|
|
// measure->mstaff(staff+i)->lines->setLines(reals->lines());
|
|
// }
|
|
}
|
|
|
|
QString implicit = e.attribute("implicit", "no");
|
|
if (implicit == "yes")
|
|
measure->setIrregular(true);
|
|
|
|
QString cv = "1"; // current voice for chords, default is 1
|
|
QList<Chord*> graceNotes;
|
|
for (e = e.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
if (e.tagName() == "attributes")
|
|
xmlAttributes(measure, staff, e.firstChildElement(), currKeySig);
|
|
else if (e.tagName() == "note") {
|
|
Note* note = xmlNote(measure, staff, part->id(), beam, cv, e, graceNotes, alt);
|
|
if(note) {
|
|
if(note->accidental()){
|
|
if(note->accidental()->accidentalType() != Accidental::Type::NONE){
|
|
courtAccNotes.append(note);
|
|
alterList.append(alt);
|
|
}
|
|
}
|
|
}
|
|
moveTick(measure->tick(), tick, maxtick, noteTypeTickFr, divisions, e);
|
|
#ifdef DEBUG_TICK
|
|
qDebug(" after inserting note tick=%d", tick);
|
|
#endif
|
|
}
|
|
else if (e.tagName() == "backup") {
|
|
// moveTick(tick, maxtick, divisions, e);
|
|
moveTick(measure->tick(), tick, maxtick, noteTypeTickFr, divisions, e);
|
|
}
|
|
else if (e.tagName() == "direction") {
|
|
direction(measure, staff, e);
|
|
}
|
|
else if (e.tagName() == "print") {
|
|
// IMPORT_LAYOUT
|
|
QString newSystem = e.attribute("new-system", "no");
|
|
QString newPage = e.attribute("new-page", "no");
|
|
int blankPage = e.attribute("blank-page", "0").toInt();
|
|
//
|
|
// in MScore the break happens _after_ the marked measure:
|
|
//
|
|
MeasureBase* pm = measure->prevMeasure(); // We insert VBox only for title, no HBox for the moment
|
|
if (pm == 0) {
|
|
qDebug("ImportXml: warning: break on first measure");
|
|
if (blankPage == 1) { // blank title page, insert a VBOX if needed
|
|
pm = measure->prev();
|
|
if (pm == 0) {
|
|
pm = score->insertMeasure(Element::Type::VBOX, measure);
|
|
}
|
|
}
|
|
}
|
|
if (pm) {
|
|
if (preferences.musicxmlImportBreaks
|
|
&& (newSystem == "yes" || newPage == "yes")) {
|
|
LayoutBreak* lb = new LayoutBreak(score);
|
|
lb->setLayoutBreakType(
|
|
newSystem == "yes" ? LayoutBreak::Type::LINE : LayoutBreak::Type::PAGE
|
|
);
|
|
pm->add(lb);
|
|
}
|
|
}
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "system-layout") {
|
|
}
|
|
else if (ee.tagName() == "staff-layout") {
|
|
}
|
|
else if (ee.tagName() == "measure-numbering") {
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
// IMPORT_LAYOUT END
|
|
}
|
|
else if (e.tagName() == "forward") {
|
|
// moveTick(tick, maxtick, divisions, e);
|
|
moveTick(measure->tick(), tick, maxtick, noteTypeTickFr, divisions, e);
|
|
}
|
|
else if (e.tagName() == "barline") {
|
|
QString loc = e.attribute("location", "right");
|
|
QString barStyle;
|
|
QString endingNumber;
|
|
QString endingType;
|
|
QString endingText;
|
|
QString repeat;
|
|
QString count;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "bar-style")
|
|
barStyle = ee.text();
|
|
else if (ee.tagName() == "ending") {
|
|
endingNumber = ee.attribute("number");
|
|
endingType = ee.attribute("type");
|
|
endingText = ee.text();
|
|
}
|
|
else if (ee.tagName() == "repeat")
|
|
{
|
|
repeat = ee.attribute("direction");
|
|
count = ee.attribute("times");
|
|
if (count.isEmpty()) {
|
|
count = "2";
|
|
}
|
|
measure->setRepeatCount(count.toInt());
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
if ((barStyle != "") || (repeat != "")) {
|
|
BarLine* barLine = new BarLine(score);
|
|
bool visible = true;
|
|
if (barStyle == "light-heavy" && repeat == "backward") {
|
|
barLine->setBarLineType(BarLineType::END_REPEAT);
|
|
}
|
|
else if (barStyle == "heavy-light" && repeat == "forward") {
|
|
barLine->setBarLineType(BarLineType::START_REPEAT);
|
|
}
|
|
else if (barStyle == "light-heavy" && repeat.isEmpty())
|
|
barLine->setBarLineType(BarLineType::END);
|
|
else if (barStyle == "regular")
|
|
barLine->setBarLineType(BarLineType::NORMAL);
|
|
else if (barStyle == "dashed")
|
|
barLine->setBarLineType(BarLineType::BROKEN);
|
|
else if (barStyle == "dotted")
|
|
barLine->setBarLineType(BarLineType::DOTTED);
|
|
else if (barStyle == "light-light")
|
|
barLine->setBarLineType(BarLineType::DOUBLE);
|
|
/*
|
|
else if (barStyle == "heavy-light")
|
|
;
|
|
else if (barStyle == "heavy-heavy")
|
|
;
|
|
*/
|
|
else if (barStyle == "none") {
|
|
barLine->setBarLineType(BarLineType::NORMAL);
|
|
visible = false;
|
|
}
|
|
else if (barStyle == "") {
|
|
if (repeat == "backward")
|
|
barLine->setBarLineType(BarLineType::END_REPEAT);
|
|
else if (repeat == "forward")
|
|
barLine->setBarLineType(BarLineType::START_REPEAT);
|
|
else
|
|
qDebug("ImportXml: warning: empty bar type");
|
|
}
|
|
else
|
|
qDebug("unsupported bar type <%s>", barStyle.toLatin1().data());
|
|
barLine->setTrack(staff * VOICES);
|
|
if (barLine->barLineType() == BarLineType::START_REPEAT) {
|
|
measure->setRepeatFlags(Repeat::START);
|
|
}
|
|
else if (barLine->barLineType() == BarLineType::END_REPEAT) {
|
|
measure->setRepeatFlags(Repeat::END);
|
|
}
|
|
else {
|
|
if (loc == "right")
|
|
measure->setEndBarLineType(barLine->barLineType(), false, visible);
|
|
else if (measure->prevMeasure())
|
|
measure->prevMeasure()->setEndBarLineType(barLine->barLineType(), false, visible);
|
|
}
|
|
}
|
|
if (!(endingNumber.isEmpty() && endingType.isEmpty())) {
|
|
if (endingNumber.isEmpty())
|
|
qDebug("ImportXml: warning: empty ending number");
|
|
else if (endingType.isEmpty())
|
|
qDebug("ImportXml: warning: empty ending type");
|
|
else {
|
|
QStringList sl = endingNumber.split(",", QString::SkipEmptyParts);
|
|
QList<int> iEndingNumbers;
|
|
bool unsupported = false;
|
|
foreach(const QString &s, sl) {
|
|
int iEndingNumber = s.toInt();
|
|
if (iEndingNumber <= 0) {
|
|
unsupported = true;
|
|
break;
|
|
}
|
|
iEndingNumbers.append(iEndingNumber);
|
|
}
|
|
|
|
if (unsupported)
|
|
qDebug("ImportXml: warning: unsupported ending number <%s>",
|
|
endingNumber.toLatin1().data());
|
|
else {
|
|
if (endingType == "start") {
|
|
Volta* volta = new Volta(score);
|
|
volta->setTrack(staff * VOICES);
|
|
volta->setText(endingText.isEmpty() ? endingNumber : endingText);
|
|
// LVIFIX TODO also support endings "1 - 3"
|
|
volta->endings().clear();
|
|
volta->endings().append(iEndingNumbers);
|
|
volta->setTick(measure->tick());
|
|
score->addElement(volta);
|
|
lastVolta = volta;
|
|
}
|
|
else if (endingType == "stop") {
|
|
if (lastVolta) {
|
|
lastVolta->setVoltaType(Volta::Type::CLOSED);
|
|
lastVolta->setTick2(measure->tick() + measure->ticks());
|
|
lastVolta = 0;
|
|
}
|
|
else {
|
|
qDebug("lastVolta == 0 on stop");
|
|
}
|
|
}
|
|
else if (endingType == "discontinue") {
|
|
if (lastVolta) {
|
|
lastVolta->setVoltaType(Volta::Type::OPEN);
|
|
lastVolta->setTick2(measure->tick() + measure->ticks());
|
|
lastVolta = 0;
|
|
}
|
|
else {
|
|
qDebug("lastVolta == 0 on discontinue");
|
|
}
|
|
}
|
|
else
|
|
qDebug("ImportXml: warning: unsupported ending type <%s>",
|
|
endingType.toLatin1().data());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (e.tagName() == "sound")
|
|
domNotImplemented(e);
|
|
else if (e.tagName() == "harmony")
|
|
xmlHarmony(e, tick, measure, staff);
|
|
else if (e.tagName() == "figured-bass") {
|
|
if (figBass) {
|
|
qDebug("more than one figured bass element on note not supported");
|
|
}
|
|
else {
|
|
// read figured bass element to attach to next note
|
|
figBassExtend = false;
|
|
bool mustkeep = false;
|
|
figBass = new FiguredBass(score);
|
|
// mustkeep = figBass->readMusicXML(e, divisions, figBassExtend);
|
|
mustkeep = readFigBass(figBass, e, divisions, figBassExtend);
|
|
// qDebug("xmlMeaure: fb mustkeep %d extend %d", mustkeep, figBassExtend);
|
|
if (!mustkeep) {
|
|
delete figBass;
|
|
figBass = 0;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
domError(e);
|
|
}
|
|
// grace notes appearing at the end of a measure are added as grace notes after to the last Chord
|
|
foreach (Chord* gc, graceNotes){
|
|
addGraceNoteAfter(gc, measure->last());
|
|
graceNotes.clear();
|
|
}
|
|
#ifdef DEBUG_TICK
|
|
qDebug("end_of_measure measure->tick()=%d maxtick=%d lastMeasureLen=%d measureLen=%d tsig=%d(%s)",
|
|
measure->tick(), maxtick, lastMeasureLen, measureLen.ticks(),
|
|
fractionTSig.ticks(), qPrintable(fractionTSig.print()));
|
|
#endif
|
|
|
|
// can't have beams extending into the next measure
|
|
if (beam)
|
|
removeBeam(beam);
|
|
|
|
// TODO:
|
|
// - how to handle fractionTSig.isZero (shouldn't happen ?)
|
|
// - how to handle unmetered music
|
|
if (fractionTSig.isValid() && !fractionTSig.isZero())
|
|
measure->setTimesig(fractionTSig);
|
|
#if 1 // previous code
|
|
// measure->setLen(Fraction::fromTicks(measureLen));
|
|
lastMeasureLen = measureLen.ticks();
|
|
tick = maxtick;
|
|
#endif
|
|
|
|
// Check for "superfluous" accidentals to mark them as USER accidentals.
|
|
// The candiadates list courtAccNotes is ordered voice after voice. Check it here segment after segment.
|
|
AccidentalState currAcc;
|
|
currAcc.init(currKeySig->keySigEvent().key());
|
|
Segment::Type st = Segment::Type::ChordRest;
|
|
for (Ms::Segment* segment = measure->first(st); segment; segment = segment->next(st)) {
|
|
for (int track = 0; track < staves * VOICES; ++track) {
|
|
Element* e = segment->element(track);
|
|
if (!e || e->type() != Ms::Element::Type::CHORD)
|
|
continue;
|
|
Chord* chord = static_cast<Chord*>(e);
|
|
foreach (Note* nt, chord->notes()){
|
|
int i = courtAccNotes.indexOf(nt);
|
|
if(i > -1){
|
|
int alter = alterList.value(i);
|
|
int ln = absStep(nt->tpc(), nt->pitch());
|
|
AccidentalVal currAccVal = currAcc.accidentalVal(ln);
|
|
if ((alter == -1 && currAccVal == AccidentalVal::FLAT && nt->accidental()->accidentalType() == Accidental::Type::FLAT && !accTmp.value(ln))
|
|
|| (alter == 0 && currAccVal == AccidentalVal::NATURAL && nt->accidental()->accidentalType() == Accidental::Type::NATURAL && !accTmp.value(ln))
|
|
|| (alter == 1 && currAccVal == AccidentalVal::SHARP && nt->accidental()->accidentalType() == Accidental::Type::SHARP && !accTmp.value(ln))) {
|
|
nt->accidental()->setRole(Accidental::Role::USER);
|
|
}
|
|
else if ((nt->accidental()->accidentalType() > Accidental::Type::NATURAL) && (nt->accidental()->accidentalType() < Accidental::Type::END)) { // microtonal accidental
|
|
alter = 0;
|
|
nt->accidental()->setRole(Accidental::Role::USER);
|
|
accTmp.replace(ln, false);
|
|
}
|
|
else {
|
|
accTmp.replace(ln, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// multi-measure rest handling:
|
|
// if any multi-measure rest is found, the "create multi-measure rest" style setting
|
|
// is enabled
|
|
// the first measure in a multi-measure rest gets setBreakMultiMeasureRest(true)
|
|
// and count down the remaining number of measures
|
|
// the first measure after a multi-measure rest gets setBreakMultiMeasureRest(true)
|
|
// for all other measures breakMultiMeasureRest is unchanged (stays default (false))
|
|
if (startMultiMeasureRest) {
|
|
score->style()->set(StyleIdx::createMultiMeasureRests, true);
|
|
measure->setBreakMultiMeasureRest(true);
|
|
startMultiMeasureRest = false;
|
|
}
|
|
else {
|
|
if (multiMeasureRestCount > 0) {
|
|
// measure is continuation of a multi-measure rest
|
|
--multiMeasureRestCount;
|
|
}
|
|
else if (multiMeasureRestCount == 0) {
|
|
// measure is first measure after a multi-measure rest
|
|
measure->setBreakMultiMeasureRest(true);
|
|
--multiMeasureRestCount;
|
|
}
|
|
}
|
|
|
|
return measure;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// setSLinePlacement -- helper for direction
|
|
//---------------------------------------------------------
|
|
|
|
// SLine placement is modified by changing the first segments user offset
|
|
// As the SLine has just been created, it does not have any segment yet
|
|
|
|
static void setSLinePlacement(SLine* sli, const QString placement)
|
|
{
|
|
/*
|
|
qDebug("setSLinePlacement sli %p type %d s=%g pl='%s'",
|
|
sli, sli->type(), sli->score()->spatium(), qPrintable(placement));
|
|
*/
|
|
|
|
// calc y offset assuming five line staff and default style
|
|
// note that required y offset is element type dependent
|
|
const qreal stafflines = 5; // assume five line staff, but works OK-ish for other sizes too
|
|
qreal offsAbove = 0;
|
|
qreal offsBelow = 0;
|
|
if (sli->type() == Element::Type::PEDAL || sli->type() == Element::Type::HAIRPIN) {
|
|
offsAbove = -6 - (stafflines - 1);
|
|
offsBelow = -1;
|
|
}
|
|
else if (sli->type() == Element::Type::TEXTLINE) {
|
|
offsAbove = 0;
|
|
offsBelow = 5 + 3 + (stafflines - 1);
|
|
}
|
|
else if (sli->type() == Element::Type::OTTAVA) {
|
|
// ignore
|
|
}
|
|
else
|
|
qDebug("setSLinePlacement sli %p unsupported type %hhd",
|
|
sli, sli->type());
|
|
|
|
// move to correct position
|
|
qreal y = 0;
|
|
if (placement == "above") y += offsAbove;
|
|
if (placement == "below") y += offsBelow;
|
|
// add linesegment containing the user offset
|
|
LineSegment* tls= sli->createLineSegment();
|
|
//qDebug(" y = %g", y);
|
|
y *= sli->score()->spatium();
|
|
tls->setUserOff(QPointF(0, y));
|
|
sli->add(tls);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addElem
|
|
//---------------------------------------------------------
|
|
|
|
static void addElem(Element* el, int track, QString& placement, Measure* measure, int tick)
|
|
{
|
|
/*
|
|
qDebug("addElem el %p track %d placement %s tick %d",
|
|
el, track, qPrintable(placement), tick);
|
|
*/
|
|
|
|
// calc y offset assuming five line staff and default style
|
|
// note that required y offset is element type dependent
|
|
const qreal stafflines = 5; // assume five line staff, but works OK-ish for other sizes too
|
|
qreal offsAbove = 0;
|
|
qreal offsBelow = 0;
|
|
if (el->type() == Element::Type::TEMPO_TEXT || el->type() == Element::Type::REHEARSAL_MARK) {
|
|
offsAbove = 0;
|
|
offsBelow = 8 + (stafflines - 1);
|
|
}
|
|
else if (el->type() == Element::Type::TEXT || el->type() == Element::Type::STAFF_TEXT) {
|
|
offsAbove = 0;
|
|
offsBelow = 6 + (stafflines - 1);
|
|
}
|
|
else if (el->type() == Element::Type::SYMBOL) {
|
|
offsAbove = -2;
|
|
offsBelow = 4 + (stafflines - 1);
|
|
}
|
|
else if (el->type() == Element::Type::DYNAMIC) {
|
|
offsAbove = -5.75 - (stafflines - 1);
|
|
offsBelow = -0.75;
|
|
}
|
|
else
|
|
qDebug("addElem el %p unsupported type %hhd",
|
|
el, el->type());
|
|
|
|
// move to correct position
|
|
// TODO: handle rx, ry
|
|
qreal y = 0;
|
|
if (placement == "above") y += offsAbove;
|
|
if (placement == "below") y += offsBelow;
|
|
//qDebug(" y = %g", y);
|
|
y *= el->score()->spatium();
|
|
el->setUserOff(QPoint(0, y));
|
|
el->setTrack(track);
|
|
Segment* s = measure->getSegment(Segment::Type::ChordRest, tick);
|
|
s->add(el);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// metronome
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read the MusicXML metronome element, convert to text and set r to calculated tempo.
|
|
*/
|
|
|
|
/*
|
|
<metronome parentheses="yes">
|
|
<beat-unit>quarter</beat-unit>
|
|
<beat-unit-dot/>
|
|
<per-minute>50</per-minute>
|
|
</metronome>
|
|
<metronome parentheses="yes">
|
|
<beat-unit>quarter</beat-unit>
|
|
<beat-unit-dot/>
|
|
<beat-unit>half</beat-unit>
|
|
<beat-unit-dot/>
|
|
</metronome>
|
|
*/
|
|
|
|
static QString metronome(QDomElement e, double& r)
|
|
{
|
|
r = 0;
|
|
QString tempoText;
|
|
QString perMinute;
|
|
|
|
QString parenth = e.attribute("parentheses");
|
|
if (parenth == "yes")
|
|
tempoText += "(";
|
|
TDuration dur1;
|
|
TDuration dur2;
|
|
for (e = e.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
QString txt = e.text();
|
|
if (e.tagName() == "beat-unit") {
|
|
// set first dur that is still invalid
|
|
if (!dur1.isValid()) dur1.setType(txt);
|
|
else if (!dur2.isValid()) dur2.setType(txt);
|
|
}
|
|
else if (e.tagName() == "beat-unit-dot") {
|
|
if (dur2.isValid()) dur2.setDots(1);
|
|
else if (dur1.isValid()) dur1.setDots(1);
|
|
}
|
|
else if (e.tagName() == "per-minute") {
|
|
perMinute = txt;
|
|
}
|
|
else
|
|
domError(e);
|
|
} // for (e = e.firstChildElement(); ...
|
|
|
|
if (dur1.isValid())
|
|
tempoText += TempoText::duration2tempoTextString(dur1);
|
|
if (dur2.isValid()) {
|
|
tempoText += " = ";
|
|
tempoText += TempoText::duration2tempoTextString(dur2);
|
|
}
|
|
else if (perMinute != "") {
|
|
tempoText += " = ";
|
|
tempoText += perMinute;
|
|
}
|
|
if (dur1.isValid() && !dur2.isValid() && perMinute != "") {
|
|
bool ok;
|
|
double d = perMinute.toDouble(&ok);
|
|
if (ok) {
|
|
// convert fraction to beats per minute
|
|
r = 4 * dur1.fraction().numerator() * d / dur1.fraction().denominator();
|
|
}
|
|
}
|
|
if (parenth == "yes")
|
|
tempoText += ")";
|
|
return tempoText;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// checkSpannerOverlap
|
|
//---------------------------------------------------------
|
|
|
|
// check for overlapping spanners
|
|
// if necessary, delete the (incorrectly allocated) new one
|
|
// return the right one to use
|
|
|
|
static SLine* checkSpannerOverlap(SLine* cur_sp, SLine* new_sp, QString type)
|
|
{
|
|
//qDebug("checkSpannerOverlap(cur_sp %p, new_sp %p, type %s)", cur_sp, new_sp, qPrintable(type));
|
|
if (cur_sp) {
|
|
qDebug("overlapping %s not supported", qPrintable(type));
|
|
delete new_sp;
|
|
return cur_sp;
|
|
}
|
|
else
|
|
return new_sp;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// handleSpannerStart
|
|
//---------------------------------------------------------
|
|
|
|
// note that in case of overlapping spanners, handleSpannerStart is called for every spanner
|
|
// as spanners QMap allows only one value per key, this does not hurt at all
|
|
|
|
static void handleSpannerStart(SLine* new_sp, QString /* type */, int track, QString& placement, int tick, MusicXmlSpannerMap& spanners)
|
|
{
|
|
new_sp->setTrack(track);
|
|
setSLinePlacement(new_sp, placement);
|
|
spanners[new_sp] = QPair<int, int>(tick, -1);
|
|
//qDebug("%s %p inserted at first tick %d", qPrintable(type), new_sp, tick);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// handleSpannerStop
|
|
//---------------------------------------------------------
|
|
|
|
static void handleSpannerStop(SLine* cur_sp, QString type, int track2, int tick, MusicXmlSpannerMap& spanners)
|
|
{
|
|
if (!cur_sp) {
|
|
qDebug("%s stop without start", qPrintable(type));
|
|
return;
|
|
}
|
|
|
|
cur_sp->setTrack2(track2);
|
|
spanners[cur_sp].second = tick;
|
|
//qDebug("pedal %p second tick %d", cur_sp, tick);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// matchRepeat
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Do a wild-card match with known repeat texts.
|
|
*/
|
|
|
|
static QString matchRepeat(const QString& lowerTxt)
|
|
{
|
|
QString repeat;
|
|
QRegExp daCapo("d\\.? *c\\.?|da *capo");
|
|
QRegExp daCapoAlFine("d\\.? *c\\.? *al *fine|da *capo *al *fine");
|
|
QRegExp daCapoAlCoda("d\\.? *c\\.? *al *coda|da *capo *al *coda");
|
|
QRegExp dalSegno("d\\.? *s\\.?|d[ae]l *segno");
|
|
QRegExp dalSegnoAlFine("d\\.? *s\\.? *al *fine|d[ae]l *segno *al *fine");
|
|
QRegExp dalSegnoAlCoda("d\\.? *s\\.? *al *coda|d[ae]l *segno *al *coda");
|
|
QRegExp fine("fine");
|
|
QRegExp toCoda("to *coda");
|
|
if (daCapo.exactMatch(lowerTxt)) repeat = "daCapo";
|
|
if (daCapoAlFine.exactMatch(lowerTxt)) repeat = "daCapoAlFine";
|
|
if (daCapoAlCoda.exactMatch(lowerTxt)) repeat = "daCapoAlCoda";
|
|
if (dalSegno.exactMatch(lowerTxt)) repeat = "dalSegno";
|
|
if (dalSegnoAlFine.exactMatch(lowerTxt)) repeat = "dalSegnoAlFine";
|
|
if (dalSegnoAlCoda.exactMatch(lowerTxt)) repeat = "dalSegnoAlCoda";
|
|
if (fine.exactMatch(lowerTxt)) repeat = "fine";
|
|
if (toCoda.exactMatch(lowerTxt)) repeat = "toCoda";
|
|
return repeat;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// findJump
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Try to find a Jump in \a repeat.
|
|
*/
|
|
|
|
static Jump* findJump(const QString& repeat, Score* score)
|
|
{
|
|
Jump* jp = 0;
|
|
if (repeat == "daCapo") {
|
|
jp = new Jump(score);
|
|
jp->setTextStyleType(TextStyleType::REPEAT_RIGHT);
|
|
jp->setJumpType(Jump::Type::DC);
|
|
}
|
|
else if (repeat == "daCapoAlCoda") {
|
|
jp = new Jump(score);
|
|
jp->setTextStyleType(TextStyleType::REPEAT_RIGHT);
|
|
jp->setJumpType(Jump::Type::DC_AL_CODA);
|
|
}
|
|
else if (repeat == "daCapoAlFine") {
|
|
jp = new Jump(score);
|
|
jp->setTextStyleType(TextStyleType::REPEAT_RIGHT);
|
|
jp->setJumpType(Jump::Type::DC_AL_FINE);
|
|
}
|
|
else if (repeat == "dalSegno") {
|
|
jp = new Jump(score);
|
|
jp->setTextStyleType(TextStyleType::REPEAT_RIGHT);
|
|
jp->setJumpType(Jump::Type::DS);
|
|
}
|
|
else if (repeat == "dalSegnoAlCoda") {
|
|
jp = new Jump(score);
|
|
jp->setTextStyleType(TextStyleType::REPEAT_RIGHT);
|
|
jp->setJumpType(Jump::Type::DS_AL_CODA);
|
|
}
|
|
else if (repeat == "dalSegnoAlFine") {
|
|
jp = new Jump(score);
|
|
jp->setTextStyleType(TextStyleType::REPEAT_RIGHT);
|
|
jp->setJumpType(Jump::Type::DS_AL_FINE);
|
|
}
|
|
return jp;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// findMarker
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Try to find a Marker in \a repeat.
|
|
*/
|
|
|
|
static Marker* findMarker(const QString& repeat, Score* score)
|
|
{
|
|
Marker* m = 0;
|
|
if (repeat == "segno") {
|
|
m = new Marker(score);
|
|
// note: Marker::read() also contains code to set text style based on type
|
|
// avoid duplicated code
|
|
m->setTextStyleType(TextStyleType::REPEAT_LEFT);
|
|
// apparently this MUST be after setTextStyle
|
|
m->setMarkerType(Marker::Type::SEGNO);
|
|
}
|
|
else if (repeat == "coda") {
|
|
m = new Marker(score);
|
|
m->setTextStyleType(TextStyleType::REPEAT_LEFT);
|
|
m->setMarkerType(Marker::Type::CODA);
|
|
}
|
|
else if (repeat == "fine") {
|
|
m = new Marker(score);
|
|
m->setTextStyleType(TextStyleType::REPEAT_RIGHT);
|
|
m->setMarkerType(Marker::Type::FINE);
|
|
}
|
|
else if (repeat == "toCoda") {
|
|
m = new Marker(score);
|
|
m->setTextStyleType(TextStyleType::REPEAT_RIGHT);
|
|
m->setMarkerType(Marker::Type::TOCODA);
|
|
}
|
|
return m;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// direction
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read the MusicXML direction element.
|
|
*/
|
|
|
|
// LVI FIXME: introduce offset concept to mscore.
|
|
// offset changes only the print position (not the tick), but unlike relative-x
|
|
// it is expressed in terms of divisions (MusicXML direction.dtd)
|
|
// even though the DTD does not mention it, practically speaking
|
|
// offset and relative-x are mutually exclusive
|
|
|
|
// Typical example:
|
|
// <direction placement="above">
|
|
// <direction-type>
|
|
// <words>Fine</words>
|
|
// </direction-type>
|
|
// <sound fine="yes"/>
|
|
// </direction>
|
|
|
|
// Note: multiple direction-type (and multiple words) elements may be present,
|
|
// and at least one direction-type must be present but sound is optional and
|
|
// at most one can be present.
|
|
|
|
void MusicXml::direction(Measure* measure, int staff, QDomElement e)
|
|
{
|
|
QString placement = e.attribute("placement");
|
|
|
|
QString dirType;
|
|
QString type;
|
|
QString niente = "no";
|
|
QString txt;
|
|
QString formattedText;
|
|
// int offset = 0; // not supported yet
|
|
//int track = 0;
|
|
int track = staff * VOICES;
|
|
QStringList dynamics;
|
|
// int spread;
|
|
// qreal rx = 0.0;
|
|
// qreal ry = 0.0;
|
|
qreal yoffset = 0.0; // actually this is default-y
|
|
// qreal xoffset;
|
|
bool hasYoffset = false;
|
|
QString dynaVelocity = "";
|
|
QString tempo = "";
|
|
QString sndCapo = "";
|
|
QString sndCoda = "";
|
|
QString sndDacapo = "";
|
|
QString sndDalsegno = "";
|
|
QString sndSegno = "";
|
|
QString sndFine = "";
|
|
bool coda = false;
|
|
bool segno = false;
|
|
int ottavasize = 0;
|
|
QString pedalLine;
|
|
QString pedalSign;
|
|
int number = 1;
|
|
QString lineEnd;
|
|
// qreal endLength;
|
|
QString lineType;
|
|
QDomElement metrEl;
|
|
QString enclosure = "none";
|
|
|
|
for (e = e.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
if (e.tagName() == "direction-type") {
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
// IMPORT_LAYOUT
|
|
dirType = ee.tagName();
|
|
if (preferences.musicxmlImportLayout) {
|
|
// ry = ee.attribute(QString("relative-y"), "0").toDouble() * -.1;
|
|
// rx = ee.attribute(QString("relative-x"), "0").toDouble() * .1;
|
|
yoffset = ee.attribute("default-y").toDouble(&hasYoffset) * -0.1;
|
|
// xoffset = ee.attribute("default-x", "0.0").toDouble() * 0.1;
|
|
}
|
|
if (dirType == "words") {
|
|
enclosure = ee.attribute(QString("enclosure"), "none");
|
|
txt = ee.text(); // support legacy code
|
|
formattedText += nextPartOfFormattedString(ee);
|
|
}
|
|
else if (dirType == "rehearsal") {
|
|
enclosure = ee.attribute(QString("enclosure"), "square");
|
|
formattedText += nextPartOfFormattedString(ee);
|
|
}
|
|
else if (dirType == "pedal") {
|
|
type = ee.attribute(QString("type"));
|
|
pedalLine = ee.attribute(QString("line"));
|
|
pedalSign = ee.attribute(QString("sign"));
|
|
}
|
|
else if (dirType == "dynamics") {
|
|
QDomElement eee = ee.firstChildElement();
|
|
if (!eee.isNull()) {
|
|
if (eee.tagName() == "other-dynamics")
|
|
dynamics.push_back(eee.text());
|
|
else
|
|
dynamics.push_back(eee.tagName());
|
|
}
|
|
}
|
|
else if (dirType == "wedge") {
|
|
type = ee.attribute(QString("type"));
|
|
number = ee.attribute(QString("number"), "1").toInt();
|
|
niente = ee.attribute(QString("niente"),"no");
|
|
// spread = ee.attribute(QString("spread"), "0").toInt();
|
|
}
|
|
else if (dirType == "dashes") {
|
|
type = ee.attribute(QString("type"));
|
|
number = ee.attribute(QString("number"), "1").toInt();
|
|
}
|
|
else if (dirType == "bracket") {
|
|
type = ee.attribute(QString("type"));
|
|
number = ee.attribute(QString("number"), "1").toInt();
|
|
lineEnd = ee.attribute(QString("line-end"), "none");
|
|
// endLength = ee.attribute(QString("end-length"), "0").toDouble() * 0.1;
|
|
lineType = ee.attribute(QString("line-type"), "solid");
|
|
}
|
|
else if (dirType == "metronome")
|
|
metrEl = ee;
|
|
else if (dirType == "octave-shift") {
|
|
type = ee.attribute(QString("type"));
|
|
number = ee.attribute(QString("number"), "1").toInt();
|
|
ottavasize = ee.attribute(QString("size"), "0").toInt();
|
|
}
|
|
else if (dirType == "coda")
|
|
coda = true;
|
|
else if (dirType == "segno")
|
|
segno = true;
|
|
else if (dirType == "other-direction")
|
|
; // application-specific, non-standard direction: ignore
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else if (e.tagName() == "sound") {
|
|
// attr: dynamics, tempo
|
|
// LVIFIX: TODO coda and segno should be numbered uniquely
|
|
sndCapo = e.attribute("capo");
|
|
sndCoda = e.attribute("coda");
|
|
sndDacapo = e.attribute("dacapo");
|
|
sndDalsegno = e.attribute("dalsegno");
|
|
sndFine = e.attribute("fine");
|
|
sndSegno = e.attribute("segno");
|
|
tempo = e.attribute("tempo");
|
|
dynaVelocity = e.attribute("dynamics");
|
|
}
|
|
else if (e.tagName() == "offset")
|
|
//offset = (e.text().toInt() * MScore::division)/divisions;
|
|
; // ignore, currently not supported
|
|
else if (e.tagName() == "staff") {
|
|
// DEBUG: <staff>0</staff>
|
|
int rstaff = e.text().toInt() - 1;
|
|
if (rstaff < 0) // ???
|
|
rstaff = 0;
|
|
track = (staff + rstaff) * VOICES;
|
|
}
|
|
else if (e.tagName() == "voice")
|
|
domNotImplemented(e);
|
|
else
|
|
domError(e);
|
|
} // for (e = e.firstChildElement(); ...
|
|
|
|
/*
|
|
qDebug(" tempo=%s txt=%s metrEl=%s coda=%d segno=%d sndCapo=%s sndCoda=%s"
|
|
" sndDacapo=%s sndDalsegno=%s sndFine=%s sndSegno=%s",
|
|
tempo.toLatin1().data(),
|
|
txt.toLatin1().data(),
|
|
qPrintable(metrEl.tagName()),
|
|
coda,
|
|
segno,
|
|
sndCapo.toLatin1().data(),
|
|
sndCoda.toLatin1().data(),
|
|
sndDacapo.toLatin1().data(),
|
|
sndDalsegno.toLatin1().data(),
|
|
sndFine.toLatin1().data(),
|
|
sndSegno.toLatin1().data()
|
|
);
|
|
*/
|
|
|
|
// Try to recognize the various repeats
|
|
QString repeat = "";
|
|
// Easy cases first
|
|
if (coda) repeat = "coda";
|
|
if (segno) repeat = "segno";
|
|
// As sound may be missing, next do a wild-card match with known repeat texts
|
|
if (repeat == "") repeat = matchRepeat(txt.toLower());
|
|
// If that did not work, try to recognize a sound attribute
|
|
if (repeat == "" && sndCoda != "") repeat = "coda";
|
|
if (repeat == "" && sndDacapo != "") repeat = "daCapo";
|
|
if (repeat == "" && sndDalsegno != "") repeat = "dalSegno";
|
|
if (repeat == "" && sndFine != "") repeat = "fine";
|
|
if (repeat == "" && sndSegno != "") repeat = "segno";
|
|
// If a repeat was found, assume words is no longer needed
|
|
if (repeat != "") txt = "";
|
|
|
|
/*
|
|
qDebug(" txt=%s repeat=%s",
|
|
txt.toLatin1().data(),
|
|
repeat.toLatin1().data()
|
|
);
|
|
*/
|
|
|
|
if (repeat != "") {
|
|
if (Jump* jp = findJump(repeat, score)) {
|
|
jp->setTrack(track);
|
|
qDebug("jumpsMarkers adding jm %p meas %p",jp, measure);
|
|
jumpsMarkers.append(JumpMarkerDesc(jp, measure));
|
|
}
|
|
if (Marker* m = findMarker(repeat, score)) {
|
|
m->setTrack(track);
|
|
qDebug("jumpsMarkers adding jm %p meas %p",m, measure);
|
|
jumpsMarkers.append(JumpMarkerDesc(m, measure));
|
|
}
|
|
}
|
|
|
|
if ((dirType == "words" && repeat == "") || dirType == "metronome") {
|
|
/*
|
|
qDebug("words txt='%s' metrEl='%s' tempo='%s' pl='%s' hasyoffs=%d fsz='%s' fst='%s' fw='%s'",
|
|
txt.toUtf8().data(),
|
|
qPrintable(metrEl.tagName()),
|
|
tempo.toUtf8().data(),
|
|
placement.toUtf8().data(),
|
|
hasYoffset,
|
|
fontSize.toUtf8().data(),
|
|
fontStyle.toUtf8().data(),
|
|
fontWeight.toUtf8().data()
|
|
);
|
|
*/
|
|
|
|
double tpoMetro = 0; // tempo according to metronome
|
|
// determine tempo text and calculate bpm according to metronome
|
|
if (metrEl.tagName() != "") formattedText += metronome(metrEl, tpoMetro);
|
|
double tpo = tempo.toDouble(); // tempo according to sound tempo=...
|
|
|
|
// fix for Sibelius 7.1.3 (direct export) which creates metronomes without <sound tempo="..."/>:
|
|
// if necessary, use the value calculated by metronome()
|
|
// note: no floating point comparisons with 0 ...
|
|
if (tpo < 0.1 && tpoMetro > 0.1)
|
|
tpo = tpoMetro;
|
|
|
|
Text* t;
|
|
if (tpo > 0.1) {
|
|
tpo /= 60;
|
|
t = new TempoText(score);
|
|
((TempoText*) t)->setTempo(tpo);
|
|
((TempoText*) t)->setFollowText(true);
|
|
score->setTempo(tick, tpo);
|
|
}
|
|
else {
|
|
t = new StaffText(score);
|
|
}
|
|
|
|
//qDebug("formatted words '%s'", qPrintable(formattedText));
|
|
t->setText(formattedText);
|
|
|
|
if (enclosure == "circle") {
|
|
t->textStyle().setHasFrame(true);
|
|
t->textStyle().setCircle(true);
|
|
}
|
|
else if (enclosure == "rectangle") {
|
|
t->textStyle().setHasFrame(true);
|
|
t->textStyle().setFrameRound(0);
|
|
}
|
|
|
|
if (hasYoffset) t->textStyle().setYoff(yoffset);
|
|
addElem(t, track, placement, measure, tick);
|
|
}
|
|
else if (dirType == "rehearsal") {
|
|
Text* t = new RehearsalMark(score);
|
|
if (!formattedText.contains("<b>"))
|
|
formattedText = "<b></b>" + formattedText; // explicitly turn bold off
|
|
t->setText(formattedText);
|
|
t->textStyle().setHasFrame(enclosure != "none");
|
|
if (hasYoffset) t->textStyle().setYoff(yoffset);
|
|
else t->setPlacement(placement == "above" ? Element::Placement::ABOVE : Element::Placement::BELOW);
|
|
addElem(t, track, placement, measure, tick);
|
|
}
|
|
else if (dirType == "pedal") {
|
|
if (pedalLine != "yes" && pedalSign == "") pedalSign = "yes"; // MusicXML 2.0 compatibility
|
|
if (pedalLine == "yes" && pedalSign == "") pedalSign = "no"; // MusicXML 2.0 compatibility
|
|
if (pedalLine == "yes") {
|
|
if (type == "start") {
|
|
pedal = static_cast<Pedal*>(checkSpannerOverlap(pedal, new Pedal(score), "pedal"));
|
|
if (pedalSign == "yes")
|
|
pedal->setBeginText("<sym>keyboardPedalPed</sym>");
|
|
else
|
|
pedal->setBeginHook(true);
|
|
pedal->setEndHook(true);
|
|
if (placement == "") placement = "below";
|
|
handleSpannerStart(pedal, "pedal", track, placement, tick, spanners);
|
|
}
|
|
else if (type == "stop") {
|
|
if (pedal) {
|
|
handleSpannerStop(pedal, "pedal", track, tick, spanners);
|
|
pedal = 0;
|
|
}
|
|
}
|
|
else if (type == "change") {
|
|
// pedal change is implemented as two separate pedals
|
|
// first stop the first one
|
|
if (pedal) {
|
|
pedal->setEndHookType(HookType::HOOK_45);
|
|
handleSpannerStop(pedal, "pedal", track, tick, spanners);
|
|
pedalContinue = pedal; // mark for later fixup
|
|
pedal = 0;
|
|
}
|
|
// then start a new one
|
|
pedal = static_cast<Pedal*>(checkSpannerOverlap(pedal, new Pedal(score), "pedal"));
|
|
pedal->setBeginHook(true);
|
|
pedal->setBeginHookType(HookType::HOOK_45);
|
|
pedal->setEndHook(true);
|
|
if (placement == "") placement = "below";
|
|
handleSpannerStart(pedal, "pedal", track, placement, tick, spanners);
|
|
}
|
|
else if (type == "continue") {
|
|
// ignore
|
|
}
|
|
else
|
|
qDebug("unknown pedal type %s", qPrintable(type));
|
|
}
|
|
else {
|
|
Symbol* s = new Symbol(score);
|
|
s->setAlign(AlignmentFlags::LEFT | AlignmentFlags::BASELINE);
|
|
s->setOffsetType(OffsetType::SPATIUM);
|
|
if (type == "start")
|
|
s->setSym(SymId::keyboardPedalPed);
|
|
else if (type == "stop")
|
|
s->setSym(SymId::keyboardPedalUp);
|
|
else
|
|
qDebug("unknown pedal type %s", qPrintable(type));
|
|
if (hasYoffset) s->setYoff(yoffset);
|
|
addElem(s, track, placement, measure, tick);
|
|
}
|
|
}
|
|
else if (dirType == "dynamics") {
|
|
// more than one dynamic ???
|
|
// LVIFIX: check import/export of <other-dynamics>unknown_text</...>
|
|
for (QStringList::Iterator it = dynamics.begin(); it != dynamics.end(); ++it ) {
|
|
Dynamic* dyn = new Dynamic(score);
|
|
dyn->setDynamicType(*it);
|
|
if (!dynaVelocity.isEmpty()) {
|
|
int dynaValue = round(dynaVelocity.toDouble() * 0.9);
|
|
if (dynaValue > 127)
|
|
dynaValue = 127;
|
|
else if (dynaValue < 0)
|
|
dynaValue = 0;
|
|
dyn->setVelocity( dynaValue );
|
|
}
|
|
if (hasYoffset) dyn->textStyle().setYoff(yoffset);
|
|
addElem(dyn, track, placement, measure, tick);
|
|
}
|
|
}
|
|
else if (dirType == "wedge") {
|
|
// qDebug("wedge type='%s' hairpin=%p", qPrintable(type), hairpin);
|
|
int n = number - 1;
|
|
Hairpin*& h = hairpins[n];
|
|
if (type == "crescendo" || type == "diminuendo") {
|
|
h = static_cast<Hairpin*>(checkSpannerOverlap(h, new Hairpin(score), "hairpin"));
|
|
h->setHairpinType(type == "crescendo"
|
|
? Hairpin::Type::CRESCENDO : Hairpin::Type::DECRESCENDO);
|
|
if (niente == "yes")
|
|
h->setHairpinCircledTip(true);
|
|
handleSpannerStart(h, QString("wedge %1").arg(number), track, placement, tick, spanners);
|
|
}
|
|
else if (type == "stop") {
|
|
if (h && niente == "yes")
|
|
h->setHairpinCircledTip(true);
|
|
handleSpannerStop(h, QString("wedge %1").arg(number), track, tick, spanners);
|
|
h = 0;
|
|
}
|
|
else
|
|
qDebug("unknown wedge type: %s", qPrintable(type));
|
|
}
|
|
else if (dirType == "bracket") {
|
|
int n = number - 1;
|
|
TextLine*& b = bracket[n];
|
|
if (type == "start") {
|
|
b = static_cast<TextLine*>(checkSpannerOverlap(b, new TextLine(score), "bracket"));
|
|
|
|
if (placement == "") placement = "above"; // set default
|
|
|
|
b->setBeginHook(lineEnd != "none");
|
|
if (lineEnd == "up")
|
|
b->setBeginHookHeight(-1 * b->beginHookHeight());
|
|
|
|
// hack: assume there was a words element before the bracket
|
|
if (!txt.isEmpty()) {
|
|
b->setBeginText(txt, TextStyleType::TEXTLINE);
|
|
}
|
|
|
|
if (lineType == "solid")
|
|
b->setLineStyle(Qt::SolidLine);
|
|
else if (lineType == "dashed")
|
|
b->setLineStyle(Qt::DashLine);
|
|
else if (lineType == "dotted")
|
|
b->setLineStyle(Qt::DotLine);
|
|
else
|
|
qDebug("unsupported line-type: %s", qPrintable(lineType));
|
|
|
|
handleSpannerStart(b, QString("bracket %1").arg(number), track, placement, tick, spanners);
|
|
//qDebug("bracket=%p inserted at first tick %d", b, tick);
|
|
}
|
|
else if (type == "stop") {
|
|
if (b) {
|
|
b->setEndHook(lineEnd != "none");
|
|
if (lineEnd == "up")
|
|
b->setEndHookHeight(-1 * b->endHookHeight());
|
|
}
|
|
handleSpannerStop(b, QString("bracket %1").arg(number), track, tick, spanners);
|
|
//qDebug("bracket=%p second tick %d", b, tick);
|
|
b = 0;
|
|
}
|
|
}
|
|
else if (dirType == "dashes") {
|
|
int n = number - 1;
|
|
TextLine*& b = dashes[n];
|
|
if (type == "start") {
|
|
b = static_cast<TextLine*>(checkSpannerOverlap(b, new TextLine(score), "dashes"));
|
|
|
|
if (placement == "") placement = "above"; // set default
|
|
|
|
// hack: assume there was a words element before the dashes
|
|
if (!txt.isEmpty()) {
|
|
b->setBeginText(txt, TextStyleType::TEXTLINE);
|
|
}
|
|
|
|
b->setBeginHook(false);
|
|
b->setEndHook(false);
|
|
b->setLineStyle(Qt::DashLine);
|
|
|
|
handleSpannerStart(b, QString("dashes %1").arg(number), track, placement, tick, spanners);
|
|
//qDebug("dashes=%p inserted at first tick %d", b, tick);
|
|
}
|
|
else if (type == "stop") {
|
|
// TODO: MuseScore doesn't support lines which start and end on different staves
|
|
handleSpannerStop(b, QString("dashes %1").arg(number), track, tick, spanners);
|
|
//qDebug("dashes=%p second tick %d", b, tick);
|
|
b = 0;
|
|
}
|
|
}
|
|
else if (dirType == "octave-shift") {
|
|
int n = number - 1;
|
|
Ottava*& o = ottavas[n];
|
|
if (type == "up" || type == "down") {
|
|
if (!(ottavasize == 8 || ottavasize == 15)) {
|
|
qDebug("unknown octave-shift size %d", ottavasize);
|
|
}
|
|
else {
|
|
o = static_cast<Ottava*>(checkSpannerOverlap(o, new Ottava(score), "octave-shift"));
|
|
|
|
if (placement == "") placement = "above"; // set default
|
|
|
|
if (type == "down" && ottavasize == 8) o->setOttavaType(Ottava::Type::OTTAVA_8VA);
|
|
if (type == "down" && ottavasize == 15) o->setOttavaType(Ottava::Type::OTTAVA_15MA);
|
|
if (type == "up" && ottavasize == 8) o->setOttavaType(Ottava::Type::OTTAVA_8VB);
|
|
if (type == "up" && ottavasize == 15) o->setOttavaType(Ottava::Type::OTTAVA_15MB);
|
|
|
|
handleSpannerStart(o, QString("octave-shift %1").arg(number), track, placement, tick, spanners);
|
|
//qDebug("ottava=%p inserted at first tick %d", o, tick);
|
|
}
|
|
}
|
|
else if (type == "stop") {
|
|
handleSpannerStop(o, QString("octave-shift %1").arg(number), track, tick, spanners);
|
|
//qDebug("ottava=%p second tick %d", o, tick);
|
|
o = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// 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);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// readStringData
|
|
//---------------------------------------------------------
|
|
|
|
static void readStringData(StringData* t, QDomElement de)
|
|
{
|
|
t->setFrets(25);
|
|
|
|
for (QDomElement e = de.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
const QString& tag(e.tagName());
|
|
int val = e.text().toInt();
|
|
if (tag == "staff-lines") {
|
|
if (val > 0) {
|
|
// resize the string table and init with zeroes
|
|
t->stringList() = QVector<instrString>(val).toList();
|
|
}
|
|
else
|
|
qDebug("Tablature::readMusicXML: illegal staff-lines %d", val);
|
|
}
|
|
else if (tag == "staff-tuning") {
|
|
int line = e.attribute("line").toInt();
|
|
QString step;
|
|
int alter = 0;
|
|
int octave = 0;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
const QString& tag(ee.tagName());
|
|
int val = ee.text().toInt();
|
|
if (tag == "tuning-alter")
|
|
alter = val;
|
|
else if (tag == "tuning-octave")
|
|
octave = val;
|
|
else if (tag == "tuning-step")
|
|
step = ee.text();
|
|
else
|
|
domError(ee);
|
|
}
|
|
if (0 < line && line <= t->stringList().size()) {
|
|
int pitch = MusicXMLStepAltOct2Pitch(step[0].toLatin1(), alter, octave);
|
|
if (pitch >= 0)
|
|
t->stringList()[line - 1].pitch = pitch;
|
|
else
|
|
qDebug("Tablature::readMusicXML invalid string %d tuning step/alter/oct %s/%d/%d",
|
|
line, qPrintable(step), alter, octave);
|
|
}
|
|
}
|
|
else if (tag == "capo") {
|
|
; // not supported: silently ignored
|
|
}
|
|
else {
|
|
; // others silently ignored
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlStaffDetails
|
|
//---------------------------------------------------------
|
|
|
|
static void xmlStaffDetails(Score* score, int staff, StringData* t, QDomElement e)
|
|
{
|
|
int number = e.attribute(QString("number"), "-1").toInt();
|
|
int staffIdx = staff;
|
|
if (number != -1)
|
|
staffIdx += number - 1;
|
|
bool hasStafflines = false;
|
|
int stafflines = 5;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "staff-lines") {
|
|
hasStafflines = true;
|
|
stafflines = ee.text().toInt();
|
|
}
|
|
else if (ee.tagName() == "staff-tuning")
|
|
; // ignore here (but prevent error message), handled by Tablature::readMusicXML
|
|
else
|
|
domNotImplemented(ee);
|
|
}
|
|
|
|
if (hasStafflines) {
|
|
if (number == -1) {
|
|
int staves = score->staff(staff)->part()->nstaves();
|
|
for (int i = 0; i < staves; ++i)
|
|
setStaffLines(score, staffIdx+i, stafflines);
|
|
}
|
|
else
|
|
setStaffLines(score, staffIdx, stafflines);
|
|
}
|
|
|
|
if (t) {
|
|
readStringData(t, e);
|
|
Instrument* i = score->staff(staff)->part()->instr();
|
|
i->setStringData(*t);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// isAppr
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Check if v approximately equals ref.
|
|
Used to prevent floating point comparison for equality from failing
|
|
*/
|
|
|
|
static bool isAppr(const double v, const double ref, const double epsilon)
|
|
{
|
|
return v > ref - epsilon && v < ref + epsilon;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// microtonalGuess
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Convert a MusicXML alter tag into a microtonal accidental in MuseScore enum Accidental::Type.
|
|
Works only for quarter tone, half tone, three-quarters tone and whole tone accidentals.
|
|
*/
|
|
|
|
static Accidental::Type microtonalGuess(double val)
|
|
{
|
|
const double eps = 0.001;
|
|
if (isAppr(val, -2, eps))
|
|
return Accidental::Type::FLAT2;
|
|
else if (isAppr(val, -1.5, eps))
|
|
return Accidental::Type::MIRRORED_FLAT2;
|
|
else if (isAppr(val, -1, eps))
|
|
return Accidental::Type::FLAT;
|
|
else if (isAppr(val, -0.5, eps))
|
|
return Accidental::Type::MIRRORED_FLAT;
|
|
else if (isAppr(val, 0, eps))
|
|
return Accidental::Type::NATURAL;
|
|
else if (isAppr(val, 0.5, eps))
|
|
return Accidental::Type::SHARP_SLASH;
|
|
else if (isAppr(val, 1, eps))
|
|
return Accidental::Type::SHARP;
|
|
else if (isAppr(val, 1.5, eps))
|
|
return Accidental::Type::SHARP_SLASH4;
|
|
else if (isAppr(val, 2, eps))
|
|
return Accidental::Type::SHARP2;
|
|
else
|
|
qDebug("Guess for microtonal accidental corresponding to value %f failed.", val);
|
|
|
|
// default
|
|
return Accidental::Type::NONE;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addSymToSig
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Add a symbol defined as key-step \a step , -alter \a alter and -accidental \a accid to \a sig.
|
|
*/
|
|
|
|
static void addSymToSig(KeySigEvent& sig, const QString& step, const QString& alter, const QString& accid)
|
|
{
|
|
//qDebug("addSymToSig(step '%s' alt '%s' acc '%s')",
|
|
// qPrintable(step), qPrintable(alter), qPrintable(accid));
|
|
|
|
SymId id = mxmlString2accSymId(accid);
|
|
if (id == SymId::noSym) {
|
|
bool ok;
|
|
double d;
|
|
d = alter.toDouble(&ok);
|
|
Accidental::Type accTpAlter = ok ? microtonalGuess(d) : Accidental::Type::NONE;
|
|
id = mxmlString2accSymId(accidentalType2MxmlString(accTpAlter));
|
|
}
|
|
|
|
if (step.size() == 1 && id != SymId::noSym) {
|
|
const QString table = "FEDCBAG";
|
|
const int line = table.indexOf(step);
|
|
// no auto layout for custom keysig, calculate xpos
|
|
// TODO: use symbol width ?
|
|
const qreal spread = 1.4; // assumed glyph width in space
|
|
const qreal x = sig.keySymbols().size() * spread;
|
|
if (line >= 0) {
|
|
KeySym ks;
|
|
ks.sym = id;
|
|
ks.spos = QPointF(x, qreal(line) * 0.5);
|
|
sig.keySymbols().append(ks);
|
|
sig.setCustom(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// flushAlteredTone
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
If a valid key-step, -alter, -accidental combination has been read,
|
|
convert it to a key symbol and add to the key.
|
|
Clear key-step, -alter, -accidental.
|
|
*/
|
|
|
|
static void flushAlteredTone(KeySigEvent& kse, QString& step, QString& alt, QString& acc)
|
|
{
|
|
//qDebug("flushAlteredTone(step '%s' alt '%s' acc '%s')",
|
|
// qPrintable(step), qPrintable(alt), qPrintable(acc));
|
|
|
|
if (step == "" && alt == "" && acc == "")
|
|
return; // nothing to do
|
|
|
|
// step and alt are required, but also accept step and acc
|
|
if (step != "" && (alt != "" || acc != "")) {
|
|
addSymToSig(kse, step, alt, acc);
|
|
}
|
|
else {
|
|
qDebug("flushAlteredTone invalid combination of step '%s' alt '%s' acc '%s')",
|
|
qPrintable(step), qPrintable(alt), qPrintable(acc));
|
|
}
|
|
|
|
// clean up
|
|
step = "";
|
|
alt = "";
|
|
acc = "";
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlAttributes
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read the MusicXML attributes element.
|
|
*/
|
|
|
|
// Order of attributes is divisions, key, time, staves and clef(s).
|
|
// For the first measure this means number of staves must be read first,
|
|
// as it determines how many key and time signatures must be inserted.
|
|
|
|
void MusicXml::xmlAttributes(Measure* measure, int staff, QDomElement e, KeySig* currKeySig)
|
|
{
|
|
QString beats = "";
|
|
QString beatType = "";
|
|
QString timeSymbol = "";
|
|
|
|
int staves = 1; // default is one staff
|
|
for (QDomElement e2 = e; !e2.isNull(); e2 = e2.nextSiblingElement()) {
|
|
if (e2.tagName() == "staves") {
|
|
staves = e2.text().toInt();
|
|
Part* part = score->staff(staff)->part();
|
|
part->setStaves(staves);
|
|
// grow tuplets size, do not shrink to prevent losing info
|
|
if (staves * VOICES > tuplets.size())
|
|
tuplets.resize(staves * VOICES);
|
|
}
|
|
}
|
|
|
|
for (; !e.isNull(); e = e.nextSiblingElement()) {
|
|
if (e.tagName() == "divisions") {
|
|
bool ok;
|
|
divisions = MxmlSupport::stringToInt(e.text(), &ok);
|
|
if (!ok) {
|
|
qDebug("MusicXml-Import: bad divisions value: <%s>",
|
|
qPrintable(e.text()));
|
|
divisions = 4;
|
|
}
|
|
}
|
|
else if (e.tagName() == "key") {
|
|
int number = e.attribute(QString("number"), "-1").toInt();
|
|
QString printObject(e.attribute("print-object", "yes"));
|
|
int staffIdx = staff;
|
|
if (number != -1)
|
|
staffIdx += number - 1;
|
|
|
|
// for custom keys, a single altered tone is described by
|
|
// key-step (required), key-alter (required) and key-accidental (optional)
|
|
// none, one or more altered tone may be present
|
|
// a simple state machine is required to detect them
|
|
KeySigEvent key;
|
|
QString keyStep;
|
|
QString keyAlter;
|
|
QString keyAccidental;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "fifths")
|
|
key.setKey(Key(ee.text().toInt()));
|
|
else if (ee.tagName() == "mode")
|
|
domNotImplemented(ee);
|
|
else if (ee.tagName() == "cancel")
|
|
domNotImplemented(ee); // TODO
|
|
else if (ee.tagName() == "key-step") {
|
|
flushAlteredTone(key, keyStep, keyAlter, keyAccidental);
|
|
keyStep = ee.text();
|
|
}
|
|
else if (ee.tagName() == "key-alter")
|
|
keyAlter = ee.text();
|
|
else if (ee.tagName() == "key-accidental")
|
|
keyAccidental = ee.text();
|
|
else
|
|
domError(ee);
|
|
}
|
|
flushAlteredTone(key, keyStep, keyAlter, keyAccidental);
|
|
|
|
if (number == -1) {
|
|
//
|
|
// apply key to all staves in the part
|
|
//
|
|
int staves = score->staff(staff)->part()->nstaves();
|
|
// apply to all staves in part
|
|
for (int i = 0; i < staves; ++i) {
|
|
Key oldkey = score->staff(staffIdx+i)->key(tick);
|
|
// TODO only if different custom key ?
|
|
if (oldkey != key.key() || key.custom()) {
|
|
// new key differs from key in effect at this tick
|
|
KeySig* keysig = new KeySig(score);
|
|
keysig->setTrack((staffIdx + i) * VOICES);
|
|
keysig->setKeySigEvent(key);
|
|
keysig->setVisible(printObject == "yes");
|
|
Segment* s = measure->getSegment(keysig, tick);
|
|
s->add(keysig);
|
|
currKeySig->setKeySigEvent(key);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
//
|
|
// apply key to staff(staffIdx) only
|
|
//
|
|
Key oldkey = score->staff(staffIdx)->key(tick);
|
|
// TODO only if different custom key ?
|
|
if (oldkey != key.key() || key.custom()) {
|
|
// new key differs from key in effect at this tick
|
|
KeySig* keysig = new KeySig(score);
|
|
keysig->setTrack(staffIdx * VOICES);
|
|
keysig->setKeySigEvent(key);
|
|
keysig->setVisible(printObject == "yes");
|
|
Segment* s = measure->getSegment(keysig, tick);
|
|
s->add(keysig);
|
|
currKeySig->setKeySigEvent(key);
|
|
}
|
|
}
|
|
}
|
|
else if (e.tagName() == "time") {
|
|
timeSymbol = e.attribute("symbol");
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "beats")
|
|
beats = ee.text();
|
|
else if (ee.tagName() == "beat-type") {
|
|
beatType = ee.text();
|
|
}
|
|
else if (ee.tagName() == "senza-misura")
|
|
;
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else if (e.tagName() == "clef") {
|
|
StaffTypes st = xmlClef(e, staff, measure);
|
|
int number = e.attribute(QString("number"), "-1").toInt();
|
|
int staffIdx = staff;
|
|
if (number != -1)
|
|
staffIdx += number - 1;
|
|
// qDebug("xmlAttributes clef score->staff(0) %p staffIdx %d score->staff(%d) %p",
|
|
// score->staff(0), staffIdx, staffIdx, score->staff(staffIdx));
|
|
if (st != StaffTypes::STANDARD)
|
|
score->staff(staffIdx)->setStaffType(StaffType::preset(st));
|
|
}
|
|
else if (e.tagName() == "staves")
|
|
; // ignore, already handled
|
|
else if (e.tagName() == "staff-details") {
|
|
int number = e.attribute(QString("number"), "-1").toInt();
|
|
int staffIdx = staff;
|
|
if (number != -1)
|
|
staffIdx += number - 1;
|
|
StringData* t = 0;
|
|
if (score->staff(staffIdx)->isTabStaff())
|
|
t = new StringData;
|
|
xmlStaffDetails(score, staff, t, e);
|
|
}
|
|
else if (e.tagName() == "instruments")
|
|
domNotImplemented(e);
|
|
else if (e.tagName() == "transpose") {
|
|
Interval interval;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
int i = ee.text().toInt();
|
|
if (ee.tagName() == "diatonic")
|
|
interval.diatonic = i;
|
|
else if (ee.tagName() == "chromatic")
|
|
interval.chromatic = i;
|
|
else if (ee.tagName() == "octave-change") {
|
|
interval.diatonic += i * 7;
|
|
interval.chromatic += i * 12;
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
score->staff(staff)->part()->instr()->setTranspose(interval);
|
|
}
|
|
else if (e.tagName() == "measure-style")
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "multiple-rest") {
|
|
int multipleRest = ee.text().toInt();
|
|
if (multipleRest > 1) {
|
|
multiMeasureRestCount = multipleRest - 1;
|
|
startMultiMeasureRest = true;
|
|
}
|
|
else
|
|
qDebug("ImportMusicXml: multiple-rest %d not supported",
|
|
multipleRest);
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
else
|
|
domError(e);
|
|
}
|
|
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)) {
|
|
fractionTSig = Fraction(bts, btp);
|
|
score->sigmap()->add(tick, fractionTSig);
|
|
Part* part = score->staff(staff)->part();
|
|
int staves = part->nstaves();
|
|
for (int i = 0; i < staves; ++i) {
|
|
TimeSig* timesig = new TimeSig(score);
|
|
timesig->setTrack((staff + i) * VOICES);
|
|
timesig->setSig(fractionTSig, st);
|
|
// handle simple compound time signature
|
|
if (beats.contains(QChar('+'))) {
|
|
timesig->setNumeratorString(beats);
|
|
timesig->setDenominatorString(beatType);
|
|
}
|
|
Segment* s = measure->getSegment(timesig, tick);
|
|
s->add(timesig);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addLyric -- add a single lyric to the score
|
|
// or delete it (if number too high)
|
|
//---------------------------------------------------------
|
|
|
|
static void addLyric(ChordRest* cr, Lyrics* l, int lyricNo)
|
|
{
|
|
if (lyricNo > MAX_LYRICS) {
|
|
qDebug("too much lyrics (>%d)", MAX_LYRICS);
|
|
delete l;
|
|
}
|
|
else {
|
|
l->setNo(lyricNo);
|
|
cr->add(l);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addLyrics -- add a notes lyrics to the score
|
|
//---------------------------------------------------------
|
|
|
|
static void addLyrics(ChordRest* cr,
|
|
QMap<int, Lyrics*>& numbrdLyrics,
|
|
QMap<int, Lyrics*>& defyLyrics,
|
|
QList<Lyrics*>& unNumbrdLyrics)
|
|
{
|
|
// first the lyrics with valid number
|
|
int lyricNo = -1;
|
|
for (QMap<int, Lyrics*>::const_iterator i = numbrdLyrics.constBegin(); i != numbrdLyrics.constEnd(); ++i) {
|
|
lyricNo = i.key(); // use number obtained from MusicXML file
|
|
Lyrics* l = i.value();
|
|
addLyric(cr, l, lyricNo);
|
|
}
|
|
|
|
// then the lyrics without valid number but with valid default-y
|
|
for (QMap<int, Lyrics*>::const_iterator i = defyLyrics.constBegin(); i != defyLyrics.constEnd(); ++i) {
|
|
lyricNo++; // use sequence number
|
|
Lyrics* l = i.value();
|
|
addLyric(cr, l, lyricNo);
|
|
}
|
|
|
|
// finally the remaining lyrics, which are simply added in order they appear in the MusicXML file
|
|
for (QList<Lyrics*>::const_iterator i = unNumbrdLyrics.constBegin(); i != unNumbrdLyrics.constEnd(); ++i) {
|
|
lyricNo++; // use sequence number
|
|
Lyrics* l = *i;
|
|
addLyric(cr, l, lyricNo);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlLyric -- parse a MusicXML lyric element
|
|
//---------------------------------------------------------
|
|
|
|
void MusicXml::xmlLyric(int trk, QDomElement e,
|
|
QMap<int, Lyrics*>& numbrdLyrics,
|
|
QMap<int, Lyrics*>& defyLyrics,
|
|
QList<Lyrics*>& unNumbrdLyrics)
|
|
{
|
|
Lyrics* l = new Lyrics(score);
|
|
//TODO-WS l->setTick(tick);
|
|
l->setTrack(trk);
|
|
|
|
bool ok = true;
|
|
int lyricNo = e.attribute(QString("number")).toInt(&ok) - 1;
|
|
if (ok) {
|
|
if (lyricNo < 0) {
|
|
qDebug("invalid lyrics number (<0)");
|
|
delete l;
|
|
return;
|
|
}
|
|
else if (lyricNo > MAX_LYRICS) {
|
|
qDebug("too much lyrics (>%d)", MAX_LYRICS);
|
|
delete l;
|
|
return;
|
|
}
|
|
else {
|
|
numbrdLyrics[lyricNo] = l;
|
|
}
|
|
}
|
|
else {
|
|
int defaultY = e.attribute(QString("default-y")).toInt(&ok);
|
|
if (ok)
|
|
// invert default-y as it decreases with increasing lyric number
|
|
defyLyrics[-defaultY] = l;
|
|
else
|
|
unNumbrdLyrics.append(l);
|
|
}
|
|
|
|
QString formattedText;
|
|
for (e = e.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
if (e.tagName() == "syllabic") {
|
|
if (e.text() == "single")
|
|
l->setSyllabic(Lyrics::Syllabic::SINGLE);
|
|
else if (e.text() == "begin")
|
|
l->setSyllabic(Lyrics::Syllabic::BEGIN);
|
|
else if (e.text() == "end")
|
|
l->setSyllabic(Lyrics::Syllabic::END);
|
|
else if (e.text() == "middle")
|
|
l->setSyllabic(Lyrics::Syllabic::MIDDLE);
|
|
else
|
|
qDebug("unknown syllabic %s", qPrintable(e.text()));
|
|
}
|
|
else if (e.tagName() == "text")
|
|
formattedText += nextPartOfFormattedString(e);
|
|
else if (e.tagName() == "elision")
|
|
if (e.text().isEmpty()) {
|
|
formattedText += " ";
|
|
}
|
|
else {
|
|
formattedText += nextPartOfFormattedString(e);
|
|
}
|
|
else if (e.tagName() == "extend")
|
|
;
|
|
else if (e.tagName() == "end-line")
|
|
;
|
|
else if (e.tagName() == "end-paragraph")
|
|
;
|
|
else
|
|
domError(e);
|
|
}
|
|
//qDebug("formatted lyric '%s'", qPrintable(formattedText));
|
|
l->setText(formattedText);
|
|
}
|
|
|
|
#if 0
|
|
//---------------------------------------------------------
|
|
// hasElem
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Determine if \a e has a child \a tagname.
|
|
*/
|
|
|
|
static bool hasElem(const QDomElement e, const QString& tagname)
|
|
{
|
|
return !e.elementsByTagName(tagname).isEmpty();
|
|
}
|
|
#endif
|
|
|
|
//---------------------------------------------------------
|
|
// tupletAssert -- check assertions for tuplet handling
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Check assertions for tuplet handling. If this fails, MusicXML
|
|
import will almost certainly break in non-obvious ways.
|
|
Should never happen, thus it is OK to quit the application.
|
|
*/
|
|
|
|
static void tupletAssert()
|
|
{
|
|
if (!(int(TDuration::DurationType::V_BREVE) == int(TDuration::DurationType::V_LONG) + 1
|
|
&& int(TDuration::DurationType::V_WHOLE) == int(TDuration::DurationType::V_BREVE) + 1
|
|
&& int(TDuration::DurationType::V_HALF) == int(TDuration::DurationType::V_WHOLE) + 1
|
|
&& int(TDuration::DurationType::V_QUARTER) == int(TDuration::DurationType::V_HALF) + 1
|
|
&& int(TDuration::DurationType::V_EIGHTH) == int(TDuration::DurationType::V_QUARTER) + 1
|
|
&& int(TDuration::DurationType::V_16TH) == int(TDuration::DurationType::V_EIGHTH) + 1
|
|
&& int(TDuration::DurationType::V_32ND) == int(TDuration::DurationType::V_16TH) + 1
|
|
&& int(TDuration::DurationType::V_64TH) == int(TDuration::DurationType::V_32ND) + 1
|
|
&& int(TDuration::DurationType::V_128TH) == int(TDuration::DurationType::V_64TH) + 1
|
|
&& int(TDuration::DurationType::V_256TH) == int(TDuration::DurationType::V_128TH) + 1
|
|
)) {
|
|
qFatal("tupletAssert() failed");
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// smallestTypeAndCount
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Determine the smallest note type and the number of those
|
|
present in a ChordRest.
|
|
For a note without dots the type equals the note type
|
|
and count is one.
|
|
For a single dotted note the type equals half the note type
|
|
and count is three.
|
|
A double dotted note is similar.
|
|
Note: code assumes when duration().type() is incremented,
|
|
the note length is divided by two, checked by tupletAssert().
|
|
*/
|
|
|
|
static void smallestTypeAndCount(ChordRest const* const cr, int& type, int& count)
|
|
{
|
|
type = int(cr->durationType().type());
|
|
count = 1;
|
|
switch (cr->durationType().dots()) {
|
|
case 0:
|
|
// nothing to do
|
|
break;
|
|
case 1:
|
|
type += 1; // next-smaller type
|
|
count = 3;
|
|
break;
|
|
case 2:
|
|
type += 2; // next-next-smaller type
|
|
count = 7;
|
|
break;
|
|
default:
|
|
qDebug("smallestTypeAndCount() does not support more than 2 dots");
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// matchTypeAndCount
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Given two note types and counts, if the types are not equal,
|
|
make them equal by successively doubling the count of the
|
|
largest type.
|
|
*/
|
|
|
|
static void matchTypeAndCount(int& type1, int& count1, int& type2, int& count2)
|
|
{
|
|
while (type1 < type2) {
|
|
type1++;
|
|
count1 *= 2;
|
|
}
|
|
while (type2 < type1) {
|
|
type2++;
|
|
count2 *= 2;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// determineTupletTypeAndCount
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Determine type and number of smallest notes in the tuplet
|
|
*/
|
|
|
|
static void determineTupletTypeAndCount(Tuplet* t, int& tupletType, int& tupletCount)
|
|
{
|
|
int elemCount = 0; // number of tuplet elements handled
|
|
|
|
foreach (DurationElement* de, t->elements()) {
|
|
if (de->type() == Element::Type::CHORD || de->type() == Element::Type::REST) {
|
|
ChordRest* cr = static_cast<ChordRest*>(de);
|
|
if (elemCount == 0) {
|
|
// first note: init variables
|
|
smallestTypeAndCount(cr, tupletType, tupletCount);
|
|
}
|
|
else {
|
|
int noteType = 0;
|
|
int noteCount = 0;
|
|
smallestTypeAndCount(cr, noteType, noteCount);
|
|
// match the types
|
|
matchTypeAndCount(tupletType, tupletCount, noteType, noteCount);
|
|
tupletCount += noteCount;
|
|
}
|
|
}
|
|
elemCount++;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// determineTupletBaseLen
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Determine tuplet baseLen as determined by the tuplet ratio,
|
|
and type and number of smallest notes in the tuplet.
|
|
|
|
Example: baselen of a 3:2 tuplet with 1/16, 1/8, 1/8 and 1/16
|
|
is 1/8. For this tuplet smalles note is 1/16, count is 6.
|
|
*/
|
|
|
|
TDuration determineTupletBaseLen(Tuplet* t)
|
|
{
|
|
int tupletType = 0; // smallest note type in the tuplet
|
|
int tupletCount = 0; // number of smallest notes in the tuplet
|
|
|
|
// first determine type and number of smallest notes in the tuplet
|
|
determineTupletTypeAndCount(t, tupletType, tupletCount);
|
|
|
|
// sanity check:
|
|
// for a 3:2 tuplet, count must be a multiple of 3
|
|
if (tupletCount % t->ratio().numerator()) {
|
|
qDebug("determineTupletBaseLen(%p) cannot divide count %d by %d", t, tupletCount, t->ratio().numerator());
|
|
return TDuration();
|
|
}
|
|
|
|
// calculate baselen in smallest notes
|
|
tupletCount /= t->ratio().numerator();
|
|
|
|
// normalize
|
|
while (tupletCount > 1 && (tupletCount % 2) == 0) {
|
|
tupletCount /= 2;
|
|
tupletType -= 1;
|
|
}
|
|
|
|
return TDuration(TDuration::DurationType(tupletType));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// isTupletFilled
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Determine if the tuplet contains the required number of notes,
|
|
either (1) of the specified normal type
|
|
or (2) the amount of the smallest notes in the tuplet equals
|
|
actual notes.
|
|
|
|
Example (1): a 3:2 tuplet with a 1/4 and a 1/8 note is filled
|
|
if normal type is 1/8, it is not filled if normal
|
|
type is 1/4.
|
|
|
|
Example (2): a 3:2 tuplet with a 1/4 and a 1/8 note is filled.
|
|
|
|
Use note types instead of duration to prevent errors due to rounding.
|
|
*/
|
|
|
|
bool isTupletFilled(Tuplet* t, TDuration normalType)
|
|
{
|
|
if (!t) return false;
|
|
|
|
int tupletType = 0; // smallest note type in the tuplet
|
|
int tupletCount = 0; // number of smallest notes in the tuplet
|
|
|
|
// first determine type and number of smallest notes in the tuplet
|
|
determineTupletTypeAndCount(t, tupletType, tupletCount);
|
|
|
|
// then compare ...
|
|
if (normalType.isValid()) {
|
|
int matchedNormalType = int(normalType.type());
|
|
int matchedNormalCount = t->ratio().numerator();
|
|
// match the types
|
|
matchTypeAndCount(tupletType, tupletCount, matchedNormalType, matchedNormalCount);
|
|
// ... result scenario (1)
|
|
return tupletCount >= matchedNormalCount;
|
|
}
|
|
else {
|
|
// ... result scenario (2)
|
|
return tupletCount >= t->ratio().numerator();
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlTuplet
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Parse and handle tuplet(s)
|
|
Tuplets with <actual-notes> and <normal-notes> but without <tuplet>
|
|
are handled correctly.
|
|
TODO Nested tuplets are not (yet) supported.
|
|
|
|
Note that cr must be initialized: fields measure, score, tick
|
|
and track are used.
|
|
*/
|
|
|
|
void xmlTuplet(Tuplet*& tuplet, ChordRest* cr, int ticks, QDomElement e)
|
|
{
|
|
int actualNotes = 1;
|
|
int normalNotes = 1;
|
|
TDuration normalType;
|
|
bool rest = false;
|
|
QString tupletType;
|
|
QString tupletPlacement;
|
|
QString tupletBracket;
|
|
QString tupletShowNumber;
|
|
|
|
// parse the elements required for tuplet handling
|
|
for (e = e.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
QString tag(e.tagName());
|
|
QString s(e.text());
|
|
if (tag == "notations") {
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "tuplet") {
|
|
tupletType = ee.attribute("type");
|
|
tupletPlacement = ee.attribute("placement");
|
|
tupletBracket = ee.attribute("bracket");
|
|
tupletShowNumber = ee.attribute("show-number");
|
|
}
|
|
}
|
|
}
|
|
else if (tag == "rest") {
|
|
rest = true;
|
|
}
|
|
else if (tag == "time-modification") { // tuplets
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "actual-notes")
|
|
actualNotes = ee.text().toInt();
|
|
else if (ee.tagName() == "normal-notes")
|
|
normalNotes = ee.text().toInt();
|
|
else if (ee.tagName() == "normal-type") {
|
|
// "measure" is not a valid normal-type,
|
|
// but would be accepted by setType()
|
|
if (ee.text() != "measure")
|
|
normalType.setType(ee.text());
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
}
|
|
|
|
// detect tremolo
|
|
if (actualNotes == 2 && normalNotes == 1)
|
|
return;
|
|
|
|
// Special case:
|
|
// Encore generates rests in tuplets w/o <tuplet> or <time-modification>.
|
|
// Detect this by comparing the actual duration with the expected duration
|
|
// based on note type. If actual is 2/3 of expected, the rest is part
|
|
// of a tuplet.
|
|
if (rest && actualNotes == 1 && normalNotes == 1 && cr->duration().ticks() != ticks) {
|
|
if (2 * cr->duration().ticks() == 3 * ticks) {
|
|
actualNotes = 3;
|
|
normalNotes = 2;
|
|
}
|
|
}
|
|
|
|
// check for obvious errors
|
|
if (tupletType == "start" && tuplet) {
|
|
qDebug("tuplet already started");
|
|
// TODO: how to recover ?
|
|
}
|
|
if (tupletType == "stop" && !tuplet) {
|
|
qDebug("tuplet stop but no tuplet started");
|
|
// TODO: how to recover ?
|
|
}
|
|
if (tupletType != "" && tupletType != "start" && tupletType != "stop") {
|
|
qDebug("unknown tuplet type %s", qPrintable(tupletType));
|
|
}
|
|
|
|
// Tuplet are either started by the tuplet start
|
|
// or when the time modification is first found.
|
|
if (!tuplet) {
|
|
if (tupletType == "start"
|
|
|| (!tuplet && (actualNotes != 1 || normalNotes != 1))) {
|
|
tuplet = new Tuplet(cr->score());
|
|
tuplet->setTrack(cr->track());
|
|
tuplet->setRatio(Fraction(actualNotes, normalNotes));
|
|
tuplet->setTick(cr->tick());
|
|
// set bracket, leave at default if unspecified
|
|
if (tupletBracket == "yes")
|
|
tuplet->setBracketType(Tuplet::BracketType::SHOW_BRACKET);
|
|
else if (tupletBracket == "no")
|
|
tuplet->setBracketType(Tuplet::BracketType::SHOW_NO_BRACKET);
|
|
// set number, default is "actual" (=NumberType::SHOW_NUMBER)
|
|
if (tupletShowNumber == "both")
|
|
tuplet->setNumberType(Tuplet::NumberType::SHOW_RELATION);
|
|
else if (tupletShowNumber == "none")
|
|
tuplet->setNumberType(Tuplet::NumberType::NO_TEXT);
|
|
else
|
|
tuplet->setNumberType(Tuplet::NumberType::SHOW_NUMBER);
|
|
// TODO type, placement, bracket
|
|
tuplet->setParent(cr->measure());
|
|
}
|
|
}
|
|
|
|
// Add chord to the current tuplet.
|
|
// Must also check for actual/normal notes to prevent
|
|
// adding one chord too much if tuplet stop is missing.
|
|
if (tuplet && !(actualNotes == 1 && normalNotes == 1)) {
|
|
cr->setTuplet(tuplet);
|
|
tuplet->add(cr);
|
|
}
|
|
|
|
// Tuplets are stopped by the tuplet stop
|
|
// or when the tuplet is filled completely
|
|
// (either with knowledge of the normal type
|
|
// or as a last resort calculated based on
|
|
// actual and normal notes plus total duration)
|
|
// or when the time-modification is not found.
|
|
if (tuplet) {
|
|
if (tupletType == "stop"
|
|
|| isTupletFilled(tuplet, normalType)
|
|
|| (actualNotes == 1 && normalNotes == 1)) {
|
|
// set baselen
|
|
TDuration td = determineTupletBaseLen(tuplet);
|
|
// qDebug("stop tuplet %p basetype %d", tuplet, tupletType);
|
|
tuplet->setBaseLen(td);
|
|
// TODO determine usefulness of following check
|
|
int totalDuration = 0;
|
|
foreach (DurationElement* de, tuplet->elements()) {
|
|
if (de->type() == Element::Type::CHORD || de->type() == Element::Type::REST) {
|
|
totalDuration+=de->globalDuration().ticks();
|
|
}
|
|
}
|
|
if (!(totalDuration && normalNotes)) {
|
|
qDebug("MusicXML::import: tuplet stop but bad duration");
|
|
}
|
|
tuplet = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addArticulationToChord
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Add Articulation to Chord.
|
|
*/
|
|
|
|
static void addArticulationToChord(ChordRest* cr, ArticulationType articSym, QString dir)
|
|
{
|
|
Articulation* na = new Articulation(cr->score());
|
|
na->setArticulationType(articSym);
|
|
if (dir == "up") {
|
|
na->setUp(true);
|
|
na->setAnchor(ArticulationAnchor::TOP_STAFF);
|
|
}
|
|
else if (dir == "down") {
|
|
na->setUp(false);
|
|
na->setAnchor(ArticulationAnchor::BOTTOM_STAFF);
|
|
}
|
|
cr->add(na);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addMordentToChord
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Add Mordent to Chord.
|
|
*/
|
|
|
|
static void addMordentToChord(ChordRest* cr, QString name, QString attrLong, QString attrAppr, QString attrDep)
|
|
{
|
|
ArticulationType articSym = ArticulationType::ARTICULATIONS; // legal but impossible ArticulationType value here indicating "not found"
|
|
if (name == "inverted-mordent") {
|
|
if ((attrLong == "" || attrLong == "no") && attrAppr == "" && attrDep == "") articSym = ArticulationType::Prall;
|
|
else if (attrLong == "yes" && attrAppr == "" && attrDep == "") articSym = ArticulationType::PrallPrall;
|
|
else if (attrLong == "yes" && attrAppr == "below" && attrDep == "") articSym = ArticulationType::UpPrall;
|
|
else if (attrLong == "yes" && attrAppr == "above" && attrDep == "") articSym = ArticulationType::DownPrall;
|
|
else if (attrLong == "yes" && attrAppr == "" && attrDep == "below") articSym = ArticulationType::PrallDown;
|
|
else if (attrLong == "yes" && attrAppr == "" && attrDep == "above") articSym = ArticulationType::PrallUp;
|
|
}
|
|
else if (name == "mordent") {
|
|
if ((attrLong == "" || attrLong == "no") && attrAppr == "" && attrDep == "") articSym = ArticulationType::Mordent;
|
|
else if (attrLong == "yes" && attrAppr == "" && attrDep == "") articSym = ArticulationType::PrallMordent;
|
|
else if (attrLong == "yes" && attrAppr == "below" && attrDep == "") articSym = ArticulationType::UpMordent;
|
|
else if (attrLong == "yes" && attrAppr == "above" && attrDep == "") articSym = ArticulationType::DownMordent;
|
|
}
|
|
if (articSym != ArticulationType::ARTICULATIONS) {
|
|
Articulation* na = new Articulation(cr->score());
|
|
na->setArticulationType(articSym);
|
|
cr->add(na);
|
|
}
|
|
else
|
|
qDebug("unknown ornament: name '%s' long '%s' approach '%s' departure '%s'",
|
|
qPrintable(name), qPrintable(attrLong), qPrintable(attrAppr), qPrintable(attrDep));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// readArticulations
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read a "simple" MuseScore articulation.
|
|
These are the articulations that can be
|
|
- represented by an enum ArticulationType
|
|
- added to a ChordRest
|
|
Return true (articulation recognized and handled)
|
|
or false (articulation not recognized).
|
|
Note simple implementation: MusicXML syntax is not strictly
|
|
checked, the articulations parent element does not matter.
|
|
*/
|
|
|
|
static bool readArticulations(ChordRest* cr, QString mxmlName)
|
|
{
|
|
QMap<QString, ArticulationType> map; // map MusicXML articulation name to MuseScore symbol
|
|
map["accent"] = ArticulationType::Sforzatoaccent;
|
|
map["staccatissimo"] = ArticulationType::Staccatissimo;
|
|
map["staccato"] = ArticulationType::Staccato;
|
|
map["tenuto"] = ArticulationType::Tenuto;
|
|
map["turn"] = ArticulationType::Turn;
|
|
map["inverted-turn"] = ArticulationType::Reverseturn;
|
|
map["stopped"] = ArticulationType::Plusstop;
|
|
map["up-bow"] = ArticulationType::Upbow;
|
|
map["down-bow"] = ArticulationType::Downbow;
|
|
map["detached-legato"] = ArticulationType::Portato;
|
|
map["spiccato"] = ArticulationType::Staccatissimo;
|
|
map["snap-pizzicato"] = ArticulationType::Snappizzicato;
|
|
map["schleifer"] = ArticulationType::Schleifer;
|
|
map["open-string"] = ArticulationType::Ouvert;
|
|
map["thumb-position"] = ArticulationType::ThumbPosition;
|
|
|
|
if (map.contains(mxmlName)) {
|
|
addArticulationToChord(cr, map.value(mxmlName), "");
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// convertNotehead
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Convert a MusicXML notehead name to a MuseScore headgroup.
|
|
*/
|
|
|
|
static NoteHead::Group convertNotehead(QString mxmlName)
|
|
{
|
|
QMap<QString, int> map; // map MusicXML notehead name to a MuseScore headgroup
|
|
map["slash"] = 5;
|
|
map["triangle"] = 3;
|
|
map["diamond"] = 2;
|
|
map["x"] = 1;
|
|
map["circle-x"] = 6;
|
|
map["do"] = 7;
|
|
map["re"] = 8;
|
|
map["mi"] = 4;
|
|
map["fa"] = 9;
|
|
map["so"] = 12;
|
|
map["la"] = 10;
|
|
map["ti"] = 11;
|
|
map["normal"] = 0;
|
|
|
|
if (map.contains(mxmlName))
|
|
return NoteHead::Group(map.value(mxmlName));
|
|
else
|
|
qDebug("unknown notehead %s", qPrintable(mxmlName));
|
|
// default: return 0
|
|
return NoteHead::Group::HEAD_NORMAL;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addTextToNote
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Add Text to Note.
|
|
*/
|
|
|
|
static void addTextToNote(QString txt, TextStyleType style, Score* score, Note* note)
|
|
{
|
|
if (!txt.isEmpty()) {
|
|
Text* t = new Fingering(score);
|
|
t->setTextStyleType(style);
|
|
t->setPlainText(txt);
|
|
note->add(t);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addFermata
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Add a MusicXML fermata.
|
|
Note: MusicXML common.mod: "The fermata type is upright if not specified."
|
|
*/
|
|
|
|
static void addFermata(ChordRest* cr, const QString type, const ArticulationType articSym)
|
|
{
|
|
if (type == "upright" || type == "")
|
|
addArticulationToChord(cr, articSym, "up");
|
|
else if (type == "inverted")
|
|
addArticulationToChord(cr, articSym, "down");
|
|
else
|
|
qDebug("unknown fermata type '%s'", qPrintable(type));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlFermata
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read a MusicXML fermata.
|
|
Note: MusicXML common.mod: "An empty fermata element represents a normal fermata."
|
|
*/
|
|
|
|
static void xmlFermata(ChordRest* cr, QDomElement e)
|
|
{
|
|
QString fermata = e.text();
|
|
QString fermataType = e.attribute(QString("type"));
|
|
|
|
if (fermata == "normal" || fermata == "")
|
|
addFermata(cr, fermataType, ArticulationType::Fermata);
|
|
else if (fermata == "angled")
|
|
addFermata(cr, fermataType, ArticulationType::Shortfermata);
|
|
else if (fermata == "square")
|
|
addFermata(cr, fermataType, ArticulationType::Longfermata);
|
|
else
|
|
qDebug("unknown fermata '%s'", qPrintable(fermata));
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlNotations
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read MusicXML notations.
|
|
*/
|
|
|
|
void MusicXml::xmlNotations(Note* note, ChordRest* cr, int trk, int tick, int ticks, QDomElement e)
|
|
{
|
|
Measure* measure = cr->measure();
|
|
int track = cr->track();
|
|
|
|
QString wavyLineType;
|
|
int wavyLineNo = 0;
|
|
QString arpeggioType;
|
|
QString glissandoType;
|
|
int breath = -1;
|
|
int tremolo = 0;
|
|
QString tremoloType;
|
|
QString placement;
|
|
QStringList dynamics;
|
|
// qreal rx = 0.0;
|
|
// qreal ry = 0.0;
|
|
qreal yoffset = 0.0; // actually this is default-y
|
|
// qreal xoffset = 0.0; // not used
|
|
bool hasYoffset = false;
|
|
QString chordLineType;
|
|
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "slur") {
|
|
int slurNo = ee.attribute(QString("number"), "1").toInt() - 1;
|
|
QString slurType = ee.attribute(QString("type"));
|
|
QString lineType = ee.attribute(QString("line-type"), "solid");
|
|
|
|
// PriMus Music-Notation by Columbussoft (build 10093) generates overlapping
|
|
// slurs that do not have a number attribute to distinguish them.
|
|
// The duplicates must be ignored, to prevent memory allocation issues,
|
|
// which caused a MuseScore crash
|
|
// Similar issues happen with Sibelius 7.1.3 (direct export)
|
|
|
|
if (slurType == "start") {
|
|
if (slur[slurNo].isStart())
|
|
// slur start when slur already started: report error
|
|
qDebug("ignoring duplicate slur start at line %d", e.lineNumber());
|
|
else if (slur[slurNo].isStop()) {
|
|
// slur start when slur already stopped: wrap up
|
|
Slur* newSlur = slur[slurNo].slur();
|
|
newSlur->setTick(tick);
|
|
newSlur->setStartElement(cr);
|
|
slur[slurNo] = SlurDesc();
|
|
}
|
|
else {
|
|
// slur start for new slur: init
|
|
Slur* newSlur = new Slur(score);
|
|
if(cr->isGrace())
|
|
newSlur->setAnchor(Spanner::Anchor::CHORD);
|
|
if (lineType == "dotted")
|
|
newSlur->setLineType(1);
|
|
else if (lineType == "dashed")
|
|
newSlur->setLineType(2);
|
|
newSlur->setTick(tick);
|
|
newSlur->setStartElement(cr);
|
|
QString pl = ee.attribute(QString("placement"));
|
|
if (pl == "above")
|
|
newSlur->setSlurDirection(MScore::Direction::UP);
|
|
else if (pl == "below")
|
|
newSlur->setSlurDirection(MScore::Direction::DOWN);
|
|
newSlur->setTrack(track);
|
|
newSlur->setTrack2(track);
|
|
slur[slurNo].start(newSlur);
|
|
score->addElement(newSlur);
|
|
}
|
|
}
|
|
else if (slurType == "stop") {
|
|
if (slur[slurNo].isStart()) {
|
|
// slur stop when slur already started: wrap up
|
|
Slur* newSlur = slur[slurNo].slur();
|
|
if(cr->isGrace()){
|
|
newSlur->setAnchor(Spanner::Anchor::CHORD);
|
|
newSlur->setEndElement(newSlur->startElement());
|
|
newSlur->setStartElement(cr);
|
|
}
|
|
else {
|
|
newSlur->setTick2(tick);
|
|
newSlur->setTrack2(track);
|
|
newSlur->setEndElement(cr);
|
|
}
|
|
slur[slurNo] = SlurDesc();
|
|
}
|
|
else if (slur[slurNo].isStop())
|
|
// slur stop when slur already stopped: report error
|
|
qDebug("ignoring duplicate slur stop at line %d", e.lineNumber());
|
|
else {
|
|
// slur stop for new slur: init
|
|
Slur* newSlur = new Slur(score);
|
|
newSlur->setTick2(tick);
|
|
newSlur->setTrack2(track);
|
|
newSlur->setEndElement(cr);
|
|
slur[slurNo].stop(newSlur);
|
|
}
|
|
}
|
|
else if (slurType == "continue")
|
|
; // ignore
|
|
else
|
|
qDebug("unknown slur type %s", qPrintable(slurType));
|
|
}
|
|
|
|
else if (ee.tagName() == "tied") {
|
|
QString tiedType = ee.attribute(QString("type"));
|
|
if (tiedType == "start") {
|
|
if (tie) {
|
|
qDebug("Tie already active");
|
|
}
|
|
else if (note) {
|
|
tie = new Tie(score);
|
|
// qDebug("use Tie %p", tie);
|
|
note->setTieFor(tie);
|
|
tie->setStartNote(note);
|
|
tie->setTrack(track);
|
|
QString tiedOrientation = ee.attribute("orientation", "auto");
|
|
if (tiedOrientation == "over")
|
|
tie->setSlurDirection(MScore::Direction::UP);
|
|
else if (tiedOrientation == "under")
|
|
tie->setSlurDirection(MScore::Direction::DOWN);
|
|
else if (tiedOrientation == "auto")
|
|
; // ignore
|
|
else
|
|
qDebug("unknown tied orientation: %s", tiedOrientation.toLatin1().data());
|
|
|
|
QString lineType = ee.attribute(QString("line-type"), "solid");
|
|
if (lineType == "dotted")
|
|
tie->setLineType(1);
|
|
else if (lineType == "dashed")
|
|
tie->setLineType(2);
|
|
tie = 0;
|
|
}
|
|
}
|
|
else if (tiedType == "stop")
|
|
; // ignore
|
|
else
|
|
qDebug("unknown tied type %s", tiedType.toLatin1().data());
|
|
}
|
|
else if (ee.tagName() == "tuplet") {
|
|
// needed for tuplets, handled in xmlTuplet
|
|
}
|
|
else if (ee.tagName() == "dynamics") {
|
|
// IMPORT_LAYOUT
|
|
placement = ee.attribute("placement");
|
|
if (preferences.musicxmlImportLayout) {
|
|
// ry = ee.attribute(QString("relative-y"), "0").toDouble() * -.1;
|
|
// rx = ee.attribute(QString("relative-x"), "0").toDouble() * .1;
|
|
yoffset = ee.attribute("default-y").toDouble(&hasYoffset) * -0.1;
|
|
// xoffset = ee.attribute("default-x", "0.0").toDouble() * 0.1;
|
|
}
|
|
QDomElement eee = ee.firstChildElement();
|
|
if (!eee.isNull()) {
|
|
if (eee.tagName() == "other-dynamics")
|
|
dynamics.push_back(eee.text());
|
|
else
|
|
dynamics.push_back(eee.tagName());
|
|
}
|
|
}
|
|
else if (ee.tagName() == "articulations") {
|
|
for (QDomElement eee = ee.firstChildElement(); !eee.isNull(); eee = eee.nextSiblingElement()) {
|
|
if (readArticulations(cr, eee.tagName()))
|
|
continue;
|
|
else if (eee.tagName() == "breath-mark")
|
|
breath = 0;
|
|
else if (eee.tagName() == "caesura")
|
|
breath = 3;
|
|
else if (eee.tagName() == "doit"
|
|
|| eee.tagName() == "falloff"
|
|
|| eee.tagName() == "plop"
|
|
|| eee.tagName() == "scoop")
|
|
chordLineType = eee.tagName();
|
|
else if (eee.tagName() == "strong-accent") {
|
|
QString strongAccentType = eee.attribute(QString("type"));
|
|
if (strongAccentType == "up" || strongAccentType == "")
|
|
addArticulationToChord(cr, ArticulationType::Marcato, "up");
|
|
else if (strongAccentType == "down")
|
|
addArticulationToChord(cr, ArticulationType::Marcato, "down");
|
|
else
|
|
qDebug("unknown mercato type %s", strongAccentType.toLatin1().data());
|
|
}
|
|
else
|
|
domError(eee);
|
|
}
|
|
}
|
|
else if (ee.tagName() == "fermata")
|
|
xmlFermata(cr, ee);
|
|
else if (ee.tagName() == "ornaments") {
|
|
bool trillMark = false;
|
|
// <trill-mark placement="above"/>
|
|
for (QDomElement eee = ee.firstChildElement(); !eee.isNull(); eee = eee.nextSiblingElement()) {
|
|
if (readArticulations(cr, eee.tagName()))
|
|
continue;
|
|
else if (eee.tagName() == "trill-mark")
|
|
trillMark = true;
|
|
else if (eee.tagName() == "wavy-line") {
|
|
wavyLineType = eee.attribute(QString("type"));
|
|
wavyLineNo = eee.attribute(QString("number"), "1").toInt() - 1;
|
|
}
|
|
else if (eee.tagName() == "tremolo") {
|
|
tremolo = eee.text().toInt();
|
|
tremoloType = eee.attribute(QString("type"));
|
|
}
|
|
else if (eee.tagName() == "accidental-mark")
|
|
domNotImplemented(eee);
|
|
else if (eee.tagName() == "delayed-turn")
|
|
// TODO: actually this should be offset a bit to the right
|
|
addArticulationToChord(cr, ArticulationType::Turn, "");
|
|
else if (eee.tagName() == "inverted-mordent"
|
|
|| eee.tagName() == "mordent")
|
|
addMordentToChord(cr, eee.tagName(), eee.attribute("long"), eee.attribute("approach"), eee.attribute("departure"));
|
|
else
|
|
domError(eee);
|
|
}
|
|
// note that mscore wavy line already implicitly includes a trillsym
|
|
// so don't add an additional one
|
|
if (trillMark && wavyLineType != "start")
|
|
addArticulationToChord(cr, ArticulationType::Trill, "");
|
|
}
|
|
else if (ee.tagName() == "technical") {
|
|
for (QDomElement eee = ee.firstChildElement(); !eee.isNull(); eee = eee.nextSiblingElement()) {
|
|
if (readArticulations(cr, eee.tagName()))
|
|
continue;
|
|
else if (eee.tagName() == "fingering")
|
|
// TODO: distinguish between keyboards (style TextStyleType::FINGERING)
|
|
// and (plucked) strings (style TextStyleType::LH_GUITAR_FINGERING)
|
|
addTextToNote(eee.text(), TextStyleType::FINGERING, score, note);
|
|
else if (eee.tagName() == "fret") {
|
|
if (note->staff()->isTabStaff())
|
|
note->setFret(eee.text().toInt());
|
|
}
|
|
else if (eee.tagName() == "pluck")
|
|
addTextToNote(eee.text(), TextStyleType::RH_GUITAR_FINGERING, score, note);
|
|
else if (eee.tagName() == "string") {
|
|
if (note->staff()->isTabStaff())
|
|
note->setString(eee.text().toInt() - 1);
|
|
else
|
|
addTextToNote(eee.text(), TextStyleType::STRING_NUMBER, score, note);
|
|
}
|
|
else if (eee.tagName() == "pull-off")
|
|
domNotImplemented(eee);
|
|
else
|
|
domError(eee);
|
|
}
|
|
}
|
|
else if (ee.tagName() == "arpeggiate") {
|
|
arpeggioType = ee.attribute(QString("direction"));
|
|
if (arpeggioType == "") arpeggioType = "none";
|
|
}
|
|
else if (ee.tagName() == "non-arpeggiate")
|
|
arpeggioType = "non-arpeggiate";
|
|
// glissando and slide are added to the "stop" chord only
|
|
// but text and color are read at start
|
|
else if (ee.tagName() == "glissando") {
|
|
if (ee.attribute("type") == "start") {
|
|
glissandoText = ee.text();
|
|
glissandoColor = ee.attribute("color");
|
|
}
|
|
else if (ee.attribute("type") == "stop") glissandoType = "glissando";
|
|
}
|
|
else if (ee.tagName() == "slide") {
|
|
if (ee.attribute("type") == "start") {
|
|
glissandoText = ee.text();
|
|
glissandoColor = ee.attribute("color");
|
|
}
|
|
else if (ee.attribute("type") == "stop") glissandoType = "slide";
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
// no support for arpeggio on rest
|
|
if (!arpeggioType.isEmpty() && cr->type() == Element::Type::CHORD) {
|
|
Arpeggio* a = new Arpeggio(score);
|
|
if (arpeggioType == "none")
|
|
a->setArpeggioType(ArpeggioType::NORMAL);
|
|
else if (arpeggioType == "up")
|
|
a->setArpeggioType(ArpeggioType::UP);
|
|
else if (arpeggioType == "down")
|
|
a->setArpeggioType(ArpeggioType::DOWN);
|
|
else if (arpeggioType == "non-arpeggiate")
|
|
a->setArpeggioType(ArpeggioType::BRACKET);
|
|
else {
|
|
qDebug("unknown arpeggio type %s", qPrintable(arpeggioType));
|
|
delete a;
|
|
a = 0;
|
|
}
|
|
if ((static_cast<Chord*>(cr))->arpeggio()) {
|
|
// there can be only one
|
|
delete a;
|
|
a = 0;
|
|
}
|
|
else
|
|
cr->add(a);
|
|
}
|
|
|
|
if (!glissandoType.isEmpty()) {
|
|
Glissando* g = new Glissando(score);
|
|
if (glissandoType == "slide")
|
|
g->setGlissandoType(Glissando::Type::STRAIGHT);
|
|
else if (glissandoType == "glissando")
|
|
g->setGlissandoType(Glissando::Type::WAVY);
|
|
else {
|
|
qDebug("unknown glissando type %s", glissandoType.toLatin1().data());
|
|
delete g;
|
|
g = 0;
|
|
}
|
|
if (g) {
|
|
if (glissandoText == "")
|
|
g->setShowText(false);
|
|
else {
|
|
g->setShowText(true);
|
|
g->setText(glissandoText);
|
|
glissandoText = "";
|
|
}
|
|
if (glissandoColor != "") {
|
|
QColor color(glissandoColor);
|
|
if (color.isValid())
|
|
g->setColor(color);
|
|
glissandoColor = "";
|
|
}
|
|
}
|
|
if ((static_cast<Chord*>(cr))->glissando()) {
|
|
// there can be only one
|
|
delete g;
|
|
g = 0;
|
|
}
|
|
else
|
|
cr->add(g);
|
|
}
|
|
|
|
if (!wavyLineType.isEmpty()) {
|
|
int n = wavyLineNo - 1;
|
|
Trill*& t = trills[n];
|
|
if (wavyLineType == "start") {
|
|
if (t) {
|
|
qDebug("overlapping wavy-line %d not supported", wavyLineNo);
|
|
delete t;
|
|
t = 0;
|
|
}
|
|
else {
|
|
t = new Trill(score);
|
|
t->setTrack(trk);
|
|
spanners[t] = QPair<int, int>(tick, -1);
|
|
// qDebug("wedge trill=%p inserted at first tick %d", trill, tick);
|
|
}
|
|
}
|
|
else if (wavyLineType == "stop") {
|
|
if (!t) {
|
|
qDebug("wavy-line %d stop without start", wavyLineNo);
|
|
}
|
|
else {
|
|
spanners[t].second = tick + ticks;
|
|
// qDebug("wedge trill=%p second tick %d", trill, tick);
|
|
t = 0;
|
|
}
|
|
}
|
|
else
|
|
qDebug("unknown wavy-line type %s", qPrintable(wavyLineType));
|
|
}
|
|
|
|
if (breath >= 0) {
|
|
Breath* b = new Breath(score);
|
|
// b->setTrack(trk + voice); TODO check next line
|
|
b->setTrack(track);
|
|
b->setBreathType(breath);
|
|
Segment* seg = measure->getSegment(Segment::Type::Breath, tick);
|
|
seg->add(b);
|
|
}
|
|
|
|
if (tremolo) {
|
|
if (tremolo == 1 || tremolo == 2 || tremolo == 3 || tremolo == 4) {
|
|
if (tremoloType == "" || tremoloType == "single") {
|
|
Tremolo* t = new Tremolo(score);
|
|
switch (tremolo) {
|
|
case 1: t->setTremoloType(TremoloType::R8); break;
|
|
case 2: t->setTremoloType(TremoloType::R16); break;
|
|
case 3: t->setTremoloType(TremoloType::R32); break;
|
|
case 4: t->setTremoloType(TremoloType::R64); break;
|
|
}
|
|
cr->add(t);
|
|
}
|
|
else if (tremoloType == "start") {
|
|
if (tremStart) qDebug("MusicXML::import: double tremolo start");
|
|
tremStart = static_cast<Chord*>(cr);
|
|
}
|
|
else if (tremoloType == "stop") {
|
|
if (tremStart) {
|
|
Tremolo* t = new Tremolo(score);
|
|
switch (tremolo) {
|
|
case 1: t->setTremoloType(TremoloType::C8); break;
|
|
case 2: t->setTremoloType(TremoloType::C16); break;
|
|
case 3: t->setTremoloType(TremoloType::C32); break;
|
|
case 4: t->setTremoloType(TremoloType::C64); break;
|
|
}
|
|
t->setChords(tremStart, static_cast<Chord*>(cr));
|
|
// fixup chord duration and type
|
|
t->chord1()->setDurationType(ticks);
|
|
t->chord1()->setDuration(ticks);
|
|
t->chord2()->setDurationType(ticks);
|
|
t->chord2()->setDuration(ticks);
|
|
// add tremolo to first chord (only)
|
|
tremStart->add(t);
|
|
}
|
|
else qDebug("MusicXML::import: double tremolo stop w/o start");
|
|
tremStart = 0;
|
|
}
|
|
}
|
|
else
|
|
qDebug("unknown tremolo type %d", tremolo);
|
|
}
|
|
|
|
if (chordLineType != "") {
|
|
ChordLine* cl = new ChordLine(score);
|
|
if (chordLineType == "falloff")
|
|
cl->setChordLineType(ChordLineType::FALL);
|
|
if (chordLineType == "doit")
|
|
cl->setChordLineType(ChordLineType::DOIT);
|
|
if (chordLineType == "plop")
|
|
cl->setChordLineType(ChordLineType::PLOP);
|
|
if (chordLineType == "scoop")
|
|
cl->setChordLineType(ChordLineType::SCOOP);
|
|
note->chord()->add(cl);
|
|
}
|
|
|
|
// more than one dynamic ???
|
|
// LVIFIX: check import/export of <other-dynamics>unknown_text</...>
|
|
// TODO remove duplicate code (see MusicXml::direction)
|
|
for (QStringList::Iterator it = dynamics.begin(); it != dynamics.end(); ++it ) {
|
|
Dynamic* dyn = new Dynamic(score);
|
|
dyn->setDynamicType(*it);
|
|
if (hasYoffset) dyn->textStyle().setYoff(yoffset);
|
|
addElem(dyn, track, placement, measure, tick);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// findLastFiguredBass
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
* Find last figured bass on \a track before \a seg
|
|
*/
|
|
|
|
static FiguredBass* findLastFiguredBass(int track, Segment* seg)
|
|
{
|
|
// qDebug("findLastFiguredBass(track %d seg %p)", track, seg);
|
|
while ((seg = seg->prev1(Segment::Type::ChordRest))) {
|
|
// qDebug("findLastFiguredBass seg %p", seg);
|
|
foreach(Element* e, seg->annotations()) {
|
|
if (e->track() == track && e->type() == Element::Type::FIGURED_BASS) {
|
|
FiguredBass* fb = static_cast<FiguredBass*>(e);
|
|
// qDebug("findLastFiguredBass found fb %p at seg %p", fb, seg);
|
|
return fb;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// graceNoteType
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
* convert duration and slash to grace note type
|
|
*/
|
|
|
|
NoteType graceNoteType(TDuration duration, QString graceSlash)
|
|
{
|
|
NoteType nt = NoteType::APPOGGIATURA;
|
|
if (graceSlash == "yes")
|
|
nt = NoteType::ACCIACCATURA;
|
|
if (duration.type() == TDuration::DurationType::V_QUARTER) {
|
|
nt = NoteType::GRACE4;
|
|
}
|
|
else if (duration.type() == TDuration::DurationType::V_16TH) {
|
|
nt = NoteType::GRACE16;
|
|
}
|
|
else if (duration.type() == TDuration::DurationType::V_32ND) {
|
|
nt = NoteType::GRACE32;
|
|
}
|
|
return nt;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// handleDisplayStep
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
* convert display-step and display-octave to staff line
|
|
*/
|
|
|
|
static void handleDisplayStep(ChordRest* cr, QString step, int octave, int tick, qreal spatium)
|
|
{
|
|
if (step != "" && 0 <= octave && octave <= 9) {
|
|
// qDebug("rest step=%s oct=%d", qPrintable(step), octave);
|
|
ClefType clef = cr->staff()->clef(tick);
|
|
int po = ClefInfo::pitchOffset(clef);
|
|
int istep = step[0].toLatin1() - 'A';
|
|
// qDebug(" clef=%d po=%d istep=%d", clef, po, istep);
|
|
if (istep < 0 || istep > 6) {
|
|
qDebug("rest: illegal display-step %d, <%s>", istep, qPrintable(step));
|
|
}
|
|
else {
|
|
// a b c d e f g
|
|
static int table2[7] = { 5, 6, 0, 1, 2, 3, 4 };
|
|
int dp = 7 * (octave + 2) + table2[istep];
|
|
// qDebug(" dp=%d po-dp=%d", dp, po-dp);
|
|
cr->setUserYoffset((po - dp + 3) * spatium / 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// setDuration
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
* Set \a cr duration
|
|
*/
|
|
|
|
static void setDuration(ChordRest* cr, bool rest, bool wholeMeasure, TDuration duration, int ticks)
|
|
{
|
|
if (rest) {
|
|
// By convention, whole measure rests do not have a "type" element
|
|
// As of MusicXML 3.0, this can be indicated by an attribute "measure",
|
|
// but for backwards compatibility the "old" convention still has to be supported.
|
|
if (duration.type() == TDuration::DurationType::V_INVALID) {
|
|
if (wholeMeasure)
|
|
duration.setType(TDuration::DurationType::V_MEASURE);
|
|
else
|
|
duration.setVal(ticks);
|
|
cr->setDurationType(duration);
|
|
cr->setDuration(Fraction::fromTicks(ticks));
|
|
}
|
|
else {
|
|
cr->setDurationType(duration);
|
|
cr->setDuration(cr->durationType().fraction());
|
|
}
|
|
}
|
|
else {
|
|
if (duration.type() == TDuration::DurationType::V_INVALID)
|
|
duration.setType(TDuration::DurationType::V_QUARTER);
|
|
cr->setDurationType(duration);
|
|
cr->setDuration(cr->durationType().fraction());
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlNote
|
|
//---------------------------------------------------------
|
|
|
|
/**
|
|
Read a MusicXML note.
|
|
|
|
\a Staff is the number of first staff of the part this note belongs to.
|
|
*/
|
|
Note* MusicXml::xmlNote(Measure* measure, int staff, const QString& partId, Beam*& beam,
|
|
QString& currentVoice, QDomElement e, QList<Chord*>& graceNotes, int& alt)
|
|
{
|
|
#ifdef DEBUG_TICK
|
|
qDebug("xmlNote start tick=%d (%d div) divisions=%d", tick, tick * divisions / MScore::division, divisions);
|
|
#endif
|
|
if (divisions <= 0) {
|
|
qDebug("xmlNote: invalid divisions %d", divisions);
|
|
return 0;
|
|
}
|
|
|
|
int ticks = 0;
|
|
QDomNode pn = e; // TODO remove pn
|
|
QDomElement org_e = e; // save e for later
|
|
QString strVoice = "1";
|
|
int voice = 0;
|
|
int move = 0;
|
|
|
|
bool rest = false;
|
|
int relStaff = 0;
|
|
Beam::Mode bm = Beam::Mode::NONE;
|
|
MScore::Direction sd = MScore::Direction::AUTO;
|
|
bool grace = false;
|
|
QString graceSlash;
|
|
QString step;
|
|
int alter = 0;
|
|
int octave = 4;
|
|
Accidental::Type accidental = Accidental::Type::NONE;
|
|
bool parentheses = false;
|
|
bool editorial = false;
|
|
bool cautionary = false;
|
|
TDuration duration(TDuration::DurationType::V_INVALID);
|
|
NoteHead::Group headGroup = NoteHead::Group::HEAD_NORMAL;
|
|
bool noStem = false;
|
|
QColor noteheadColor = QColor::Invalid;
|
|
bool noteheadParentheses = false;
|
|
bool chord = false;
|
|
int velocity = -1;
|
|
bool unpitched = false;
|
|
QString instrId;
|
|
QList<QDomElement> notations;
|
|
|
|
// first read all elements required for voice mapping
|
|
QDomElement e2 = e.firstChildElement();
|
|
for (; !e2.isNull(); e2 = e2.nextSiblingElement()) {
|
|
QString tag(e2.tagName());
|
|
QString s(e2.text());
|
|
if (tag == "voice")
|
|
strVoice = s;
|
|
else if (tag == "staff")
|
|
relStaff = s.toInt() - 1;
|
|
else if (tag == "grace") {
|
|
grace = true;
|
|
}
|
|
else if (tag == "duration") {
|
|
ticks = calcTicks(s, divisions);
|
|
}
|
|
else if (tag == "chord") {
|
|
chord = true;
|
|
}
|
|
// silently ignore others (will be handled later)
|
|
}
|
|
|
|
// Bug fix for Cubase 6.5.5 which generates <staff>2</staff> in a single staff part
|
|
// Same fix is required in MxmlReaderFirstPass::initVoiceMapperAndMapVoices
|
|
int nStavesInPart = score->staff(staff)->part()->nstaves();
|
|
if (relStaff < 0 || relStaff >= nStavesInPart) {
|
|
qDebug("ImportMusicXml: invalid staff %d (staves is %d) at line %d col %d",
|
|
relStaff + 1, nStavesInPart, e.lineNumber(), e.columnNumber());
|
|
relStaff = 0;
|
|
}
|
|
|
|
// Bug fix for Sibelius 7.1.3 which does not write <voice> for notes with <chord>
|
|
if (!chord)
|
|
// remember voice
|
|
currentVoice = strVoice;
|
|
else
|
|
// use voice from last note w/o <chord>
|
|
strVoice = currentVoice;
|
|
|
|
// 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: relStaff=%d voice=%d staff=%d", relStaff, voice, staff);
|
|
int s; // staff mapped by voice mapper
|
|
int v; // voice mapped by voice mapper
|
|
if (voicelist.value(strVoice).overlaps()) {
|
|
// for overlapping voices, the staff does not change
|
|
// and the voice is mapped and staff-dependent
|
|
s = relStaff;
|
|
v = voicelist.value(strVoice).voice(s);
|
|
}
|
|
else {
|
|
// for non-overlapping voices, both staff and voice are
|
|
// set by the voice mapper
|
|
s = voicelist.value(strVoice).staff();
|
|
v = voicelist.value(strVoice).voice();
|
|
}
|
|
|
|
// qDebug("voice mapper before: relStaff=%d voice=%d s=%d v=%d", relStaff, voice, s, v);
|
|
if (s < 0 || v < 0) {
|
|
qDebug("ImportMusicXml: too many voices (staff %d, relStaff %d, voice %s at line %d col %d)",
|
|
staff + 1, relStaff, qPrintable(strVoice), e.lineNumber(), e.columnNumber());
|
|
return 0;
|
|
}
|
|
else {
|
|
int d = relStaff - s;
|
|
relStaff = s;
|
|
move += d;
|
|
voice = v;
|
|
}
|
|
|
|
// qDebug("voice mapper after: relStaff=%d move=%d voice=%d", relStaff, move, voice);
|
|
// note: relStaff is the staff number relative to the parts first staff
|
|
// voice is the voice number in the staff
|
|
|
|
// determine tick for note
|
|
int loc_tick = chord ? prevtick : tick;
|
|
// qDebug("chord %d prevtick %d tick %d loc_tick %d", chord, prevtick, tick, loc_tick);
|
|
|
|
// trk is first track of staff
|
|
int trk = (staff + relStaff) * VOICES;
|
|
// track is current track in staff
|
|
int track = trk + voice;
|
|
|
|
QString printObject = "yes";
|
|
if (pn.isElement() && pn.nodeName() == "note") {
|
|
QDomElement pne = pn.toElement();
|
|
printObject = pne.attribute("print-object", "yes");
|
|
}
|
|
|
|
velocity = round(e.attribute("dynamics", "-1").toDouble() * 0.9);
|
|
|
|
// storage for xmlLyric
|
|
QMap<int, Lyrics*> numberedLyrics; // lyrics with valid number
|
|
QMap<int, Lyrics*> defaultyLyrics; // lyrics with valid default-y
|
|
QList<Lyrics*> unNumberedLyrics; // lyrics with neither
|
|
|
|
for (e = e.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
QString tag(e.tagName());
|
|
QString s(e.text());
|
|
|
|
if (tag == "pitch") {
|
|
step = "C";
|
|
alter = 0;
|
|
octave = 4;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "step") // A-G
|
|
step = ee.text();
|
|
else if (ee.tagName() == "alter") { // -1=flat 1=sharp (0.5=quarter sharp)
|
|
bool ok;
|
|
QString altertext = ee.text();
|
|
alter = MxmlSupport::stringToInt(altertext, &ok); // fractions not supported by mscore
|
|
if (!ok || alter < -2 || alter > 2) {
|
|
qDebug("ImportXml: bad 'alter' value: %s at line %d col %d",
|
|
qPrintable(altertext), ee.lineNumber(), ee.columnNumber());
|
|
bool ok2;
|
|
double altervalue = altertext.toDouble(&ok2);
|
|
if (ok2 && (qAbs(altervalue) < 2.0) && (accidental == Accidental::Type::NONE)) {
|
|
// try to see if a microtonal accidental is needed
|
|
accidental = microtonalGuess(altervalue);
|
|
}
|
|
alter = 0;
|
|
}
|
|
}
|
|
else if (ee.tagName() == "octave") // 0-9 4=middle C
|
|
octave = ee.text().toInt();
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else if (tag == "unpitched") {
|
|
//
|
|
// TODO: check semantic
|
|
//
|
|
unpitched = true;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "display-step") // A-G
|
|
step = ee.text();
|
|
else if (ee.tagName() == "display-octave") // 0-9 4=middle C
|
|
octave = ee.text().toInt();
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else if (tag == "type")
|
|
duration = TDuration(s);
|
|
else if (tag == "chord" || tag == "duration" || tag == "staff" || tag == "voice")
|
|
// already handled by voice mapper, ignore here but prevent
|
|
// spurious "Unknown Node <staff>" or "... <voice>" messages
|
|
;
|
|
else if (tag == "stem") {
|
|
if (s == "up")
|
|
sd = MScore::Direction::UP;
|
|
else if (s == "down")
|
|
sd = MScore::Direction::DOWN;
|
|
else if (s == "none")
|
|
noStem = true;
|
|
else if (s == "double")
|
|
;
|
|
else
|
|
qDebug("unknown stem direction %s", e.text().toLatin1().data());
|
|
}
|
|
else if (tag == "beam") {
|
|
int beamNo = e.attribute(QString("number"), "1").toInt();
|
|
if (beamNo == 1) {
|
|
if (s == "begin")
|
|
bm = Beam::Mode::BEGIN;
|
|
else if (s == "end")
|
|
bm = Beam::Mode::END;
|
|
else if (s == "continue")
|
|
bm = Beam::Mode::MID;
|
|
else if (s == "backward hook")
|
|
;
|
|
else if (s == "forward hook")
|
|
;
|
|
else
|
|
qDebug("unknown beam keyword <%s>", s.toLatin1().data());
|
|
}
|
|
}
|
|
else if (tag == "rest") {
|
|
rest = true;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "display-step") // A-G
|
|
step = ee.text();
|
|
else if (ee.tagName() == "display-octave") // 0-9 4=middle C
|
|
octave = ee.text().toInt();
|
|
else
|
|
domError(ee);
|
|
}
|
|
}
|
|
else if (tag == "lyric")
|
|
xmlLyric(track, e, numberedLyrics, defaultyLyrics, unNumberedLyrics);
|
|
else if (tag == "dot")
|
|
duration.setDots(duration.dots() + 1);
|
|
else if (tag == "accidental") {
|
|
if (s != "")
|
|
accidental = mxmlString2accidentalType(s);
|
|
else
|
|
qDebug("empty accidental");
|
|
if (e.attribute(QString("cautionary")) == "yes")
|
|
cautionary = true;
|
|
if (e.attribute(QString("editorial")) == "yes")
|
|
editorial = true;
|
|
if (e.attribute(QString("parentheses")) == "yes")
|
|
parentheses = true;
|
|
}
|
|
else if (tag == "notations") {
|
|
// save the QDomElement representing <notations> for later
|
|
// note that multiple notations elements may be present
|
|
notations << e;
|
|
}
|
|
else if (tag == "tie") {
|
|
QString tieType = e.attribute(QString("type"));
|
|
if (tieType == "start")
|
|
;
|
|
else if (tieType == "stop")
|
|
;
|
|
else
|
|
qDebug("unknown tie type %s", tieType.toLatin1().data());
|
|
}
|
|
else if (tag == "grace") {
|
|
graceSlash = e.attribute(QString("slash"));
|
|
}
|
|
else if (tag == "time-modification") {
|
|
// needed for tuplets, handled in xmlTuplet
|
|
}
|
|
else if (tag == "notehead") {
|
|
headGroup = convertNotehead(s);
|
|
QString color = e.attribute(QString("color"), 0);
|
|
if (color != 0)
|
|
noteheadColor = QColor(color);
|
|
if (e.attribute(QString("parentheses")) == "yes")
|
|
noteheadParentheses = true;
|
|
}
|
|
else if (tag == "instrument") {
|
|
instrId = e.attribute("id");
|
|
}
|
|
else if (tag == "cue")
|
|
domNotImplemented(e);
|
|
else
|
|
domError(e);
|
|
}
|
|
|
|
// qDebug("staff=%d relStaff=%d VOICES=%d voice=%d track=%d",
|
|
// staff, relStaff, VOICES, voice, track);
|
|
|
|
// qDebug("%s at %d voice %d dur = %d, beat %d/%d div %d pitch %d ticks %d",
|
|
// rest ? "Rest" : "Note", loc_tick, voice, duration, beats, beatType,
|
|
// divisions, 0 /* pitch */, ticks);
|
|
|
|
ChordRest* cr = 0;
|
|
Note* note = 0;
|
|
|
|
if (rest) {
|
|
cr = new Rest(score);
|
|
|
|
// Verify the rest fits exactly in the measure, as some programs
|
|
// (e.g. Cakewalk SONAR X2 Studio [Version: 19.0.0.306]) leave out
|
|
// the type for all rests.
|
|
bool wholeMeasure = (tick == measure->tick() && ticks == measure->ticks());
|
|
setDuration(cr, rest, wholeMeasure, duration, ticks);
|
|
|
|
if (beam) {
|
|
if (beam->track() == track) {
|
|
cr->setBeamMode(Beam::Mode::MID);
|
|
beam->add(cr);
|
|
}
|
|
else
|
|
removeBeam(beam);
|
|
}
|
|
else
|
|
cr->setBeamMode(Beam::Mode::NONE);
|
|
cr->setTrack(track);
|
|
cr->setStaffMove(move);
|
|
Segment* s = measure->getSegment(cr, loc_tick);
|
|
// Sibelius might export two rests at the same place, ignore the 2nd one
|
|
// <?DoletSibelius Two NoteRests in same voice at same position may be an error?>
|
|
if (!s->element(cr->track()))
|
|
s->add(cr);
|
|
cr->setVisible(printObject == "yes");
|
|
handleDisplayStep(cr, step, octave, loc_tick, score->spatium());
|
|
}
|
|
else {
|
|
if (grace) {
|
|
if (chord) {
|
|
// use last grace chord to add note to
|
|
if (!graceNotes.isEmpty())
|
|
cr = graceNotes.last();
|
|
}
|
|
else {
|
|
// create a new grace chord
|
|
Chord* ch = new Chord(score);
|
|
ch->setNoteType(graceNoteType(duration, graceSlash));
|
|
graceNotes.push_back(ch);
|
|
cr = ch;
|
|
//cr->setBeamMode(bm);
|
|
cr->setTrack(track);
|
|
setDuration(cr, false, false, duration, ticks);
|
|
ch->setNoteType(graceNoteType(duration, graceSlash));
|
|
// check whether grace is slured with prev. main note, then handle all grace
|
|
// notes until this as after
|
|
bool found = false;
|
|
if(!notations.empty()) {
|
|
foreach(QDomElement de, notations) {
|
|
for (QDomElement ee = de.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "slur") {
|
|
int slurNo = ee.attribute(QString("number"), "1").toInt() - 1;
|
|
QString slurType = ee.attribute(QString("type"));
|
|
if (slurType == "stop") {
|
|
QDomElement eLastNote = org_e.previousSiblingElement("note");
|
|
while(!eLastNote.isNull()){
|
|
if (eLastNote.elementsByTagName("grace").isEmpty()){
|
|
QList<QDomElement> eSlurs = findSlurElements(eLastNote);
|
|
foreach (QDomElement es, eSlurs){
|
|
if (!es.isNull() && slurNo == es.attribute(QString("number"), "1").toInt() - 1 && es.attribute(QString("type")) == "start"){
|
|
foreach (Chord* cg, graceNotes)
|
|
cg->toGraceAfter();
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found)
|
|
break;
|
|
}
|
|
eLastNote = eLastNote.previousSiblingElement("note");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (found)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// regular note
|
|
// if there is already a chord just add to it
|
|
// else create a new one
|
|
// this basically ignores <chord/> errors
|
|
cr = measure->findChord(loc_tick, track);
|
|
if (cr == 0) {
|
|
cr = new Chord(score);
|
|
cr->setBeamMode(bm);
|
|
cr->setTrack(track);
|
|
|
|
setDuration(cr, false, false, duration, ticks);
|
|
Segment* s = measure->getSegment(cr, loc_tick);
|
|
s->add(cr);
|
|
}
|
|
// append grace notes
|
|
// first excerpt grace notes after
|
|
QList<Chord*> toRemove;
|
|
if(graceNotes.length()){
|
|
for(int i = 0; i < graceNotes.length(); i++){
|
|
// grace notes from voice before, upcoming here must be grace after
|
|
if(graceNotes[i]->voice() != cr->voice()){
|
|
addGraceNoteAfter(graceNotes[i], measure->last());
|
|
toRemove.append(graceNotes[i]);
|
|
}
|
|
else if(graceNotes[i]->isGraceAfter()){
|
|
addGraceNoteAfter(graceNotes[i], cr->segment()->prev());
|
|
toRemove.append(graceNotes[i]);
|
|
}
|
|
}
|
|
}
|
|
foreach(Chord* cRem, toRemove)
|
|
graceNotes.removeOne(cRem);
|
|
toRemove.clear();
|
|
// append grace notes before
|
|
int ii = -1;
|
|
for (ii = graceNotes.size() - 1; ii >= 0; ii--) {
|
|
Chord* gc = graceNotes[ii];
|
|
if(gc->voice() == cr->voice()){
|
|
cr->add(gc);
|
|
}
|
|
}
|
|
graceNotes.clear();
|
|
}
|
|
|
|
if(cr)
|
|
cr->setStaffMove(move);
|
|
|
|
char c = step[0].toLatin1();
|
|
note = new Note(score);
|
|
note->setHeadGroup(headGroup);
|
|
if (noteheadColor != QColor::Invalid)
|
|
note->setColor(noteheadColor);
|
|
|
|
if (noteheadParentheses) {
|
|
Symbol* s = new Symbol(score);
|
|
s->setSym(SymId::noteheadParenthesisLeft);
|
|
s->setParent(note);
|
|
score->addElement(s);
|
|
s = new Symbol(score);
|
|
s->setSym(SymId::noteheadParenthesisRight);
|
|
s->setParent(note);
|
|
score->addElement(s);
|
|
}
|
|
|
|
if (velocity > 0) {
|
|
note->setVeloType(Note::ValueType::USER_VAL);
|
|
note->setVeloOffset(velocity);
|
|
}
|
|
|
|
// pitch must be set before adding note to chord as note
|
|
// is inserted into pitch sorted list (ws)
|
|
|
|
if (unpitched
|
|
&& drumsets.contains(partId)
|
|
&& drumsets[partId].contains(instrId)) {
|
|
// step and oct are display-step and ...-oct
|
|
// get pitch from instrument definition in drumset instead
|
|
int pitch = drumsets[partId][instrId].pitch;
|
|
note->setPitch(pitch);
|
|
// TODO - does this need to be key-aware?
|
|
note->setTpc(pitch2tpc(pitch, Key::C, Prefer::NEAREST)); // TODO: necessary ?
|
|
}
|
|
else
|
|
xmlSetPitch(note, c, alter, octave, ottavas, track);
|
|
|
|
cr->add(note);
|
|
|
|
static_cast<Chord*>(cr)->setNoStem(noStem);
|
|
|
|
// qDebug("staff for new note: %p (staff=%d, relStaff=%d)",
|
|
// score->staff(staff + relStaff), staff, relStaff);
|
|
|
|
if(accidental != Accidental::Type::NONE){
|
|
Accidental* a = new Accidental(score);
|
|
a->setAccidentalType(accidental);
|
|
if (editorial || cautionary || parentheses) {
|
|
a->setHasBracket(cautionary || parentheses);
|
|
a->setRole(Accidental::Role::USER);
|
|
}
|
|
else {
|
|
alt = alter;
|
|
}
|
|
note->add(a);
|
|
}
|
|
|
|
// LVIFIX: quarter tone accidentals support is "drawing only"
|
|
//WS-TODO if (accidental == 18
|
|
// || accidental == 19
|
|
// || accidental == 22
|
|
// || accidental == 25)
|
|
// note->setAccidentalType(accidental);
|
|
|
|
// remember beam mode last non-grace note
|
|
// bm == Beam::Mode::NONE means no <beam> was found
|
|
if (!grace && bm != Beam::Mode::NONE)
|
|
beamMode = bm;
|
|
|
|
// handle beam
|
|
if (!chord && !grace)
|
|
handleBeamAndStemDir(cr, bm, sd, beam);
|
|
|
|
note->setVisible(printObject == "yes");
|
|
|
|
// set drumset information
|
|
// note that in MuseScore, the drumset contains defaults for notehead,
|
|
// line and stem direction, while a MusicXML file contains actuals.
|
|
// the MusicXML values for each note are simply copied to the defaults
|
|
|
|
if (unpitched) {
|
|
// determine staff line based on display-step / -octave and clef type
|
|
ClefType clef = cr->staff()->clef(loc_tick);
|
|
int po = ClefInfo::pitchOffset(clef);
|
|
int pitch = MusicXMLStepAltOct2Pitch(step[0].toLatin1(), 0, octave);
|
|
int line = po - absStep(pitch);
|
|
|
|
// correct for number of staff lines
|
|
// see ExportMusicXml::unpitch2xml for explanation
|
|
// TODO handle other # staff lines ?
|
|
int staffLines = cr->staff()->lines();
|
|
if (staffLines == 1) line -= 8;
|
|
if (staffLines == 3) line -= 2;
|
|
|
|
// the drum palette cannot handle stem direction AUTO,
|
|
// overrule if necessary
|
|
if (sd == MScore::Direction::AUTO) {
|
|
if (line > 4)
|
|
sd = MScore::Direction::DOWN;
|
|
else
|
|
sd = MScore::Direction::UP;
|
|
}
|
|
|
|
if (drumsets.contains(partId)
|
|
&& drumsets[partId].contains(instrId)) {
|
|
drumsets[partId][instrId].notehead = headGroup;
|
|
drumsets[partId][instrId].line = line;
|
|
drumsets[partId][instrId].stemDirection = sd;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!chord && !grace) {
|
|
xmlTuplet(tuplets[voice + relStaff * VOICES], cr, ticks, org_e);
|
|
}
|
|
|
|
foreach(QDomElement de, notations) {
|
|
xmlNotations(note, cr, trk, loc_tick, ticks, de);
|
|
}
|
|
|
|
// add lyrics found by xmlLyric
|
|
addLyrics(cr, numberedLyrics, defaultyLyrics, unNumberedLyrics);
|
|
|
|
// add figured bass element
|
|
if (figBass) {
|
|
// qDebug("add figured bass %p at tick %d ticks %d trk %d", figBass, tick, ticks, trk);
|
|
figBass->setTrack(trk);
|
|
figBass->setTicks(ticks);
|
|
// TODO: set correct onNote value
|
|
Segment* s = measure->getSegment(Segment::Type::ChordRest, tick);
|
|
// TODO: use addelement() instead of Segment::add() ?
|
|
s->add(figBass);
|
|
figBass = 0;
|
|
}
|
|
else if (figBassExtend) {
|
|
// extend last figured bass to end of this chord
|
|
// qDebug("extend figured bass at tick %d ticks %d trk %d end %d", tick, ticks, trk, tick + ticks);
|
|
FiguredBass* fb = findLastFiguredBass((trk / VOICES) * VOICES, cr->segment());
|
|
if (fb)
|
|
fb->setTicks(tick + ticks - fb->segment()->tick());
|
|
}
|
|
figBassExtend = false;
|
|
|
|
// fixup pedal type="change" to end at the end of this note
|
|
// note tick is still at note start
|
|
if (pedalContinue) {
|
|
handleSpannerStop(pedalContinue, "pedal", track, tick + ticks, spanners);
|
|
pedalContinue = 0;
|
|
}
|
|
|
|
if (!chord)
|
|
prevtick = tick; // remember tick where last chordrest was inserted
|
|
|
|
#ifdef DEBUG_TICK
|
|
qDebug(" after inserting note tick=%d prevtick=%d", tick, prevtick);
|
|
#endif
|
|
return note;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// readFretDiagram
|
|
//---------------------------------------------------------
|
|
|
|
static void readFretDiagram(FretDiagram* fd, QDomElement de)
|
|
{
|
|
for (QDomElement e = de.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
const QString& tag(e.tagName());
|
|
int val = e.text().toInt();
|
|
if (tag == "frame-frets") {
|
|
if (val > 0)
|
|
fd->setFrets(val);
|
|
else
|
|
qDebug("FretDiagram::readMusicXML: illegal frame-fret %d", val);
|
|
}
|
|
else if (tag == "frame-note") {
|
|
int fret = -1;
|
|
int string = -1;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
const QString& tag(ee.tagName());
|
|
int val = ee.text().toInt();
|
|
if (tag == "fret")
|
|
fret = val;
|
|
else if (tag == "string")
|
|
string = val;
|
|
else
|
|
domError(ee);
|
|
}
|
|
qDebug("FretDiagram::readMusicXML string %d fret %d", string, fret);
|
|
if (string > 0) {
|
|
if (fret == 0)
|
|
fd->setMarker(fd->strings() - string, 79 /* ??? */);
|
|
else if (fret > 0)
|
|
fd->setDot(fd->strings() - string, fret);
|
|
}
|
|
}
|
|
else if (tag == "frame-strings") {
|
|
if (val > 0) {
|
|
fd->setStrings(val);
|
|
for (int i = 0; i < val; ++i)
|
|
fd->setMarker(i, 88 /* ??? */);
|
|
}
|
|
else
|
|
qDebug("FretDiagram::readMusicXML: illegal frame-strings %d", val);
|
|
}
|
|
else
|
|
domError(e);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlHarmony
|
|
//---------------------------------------------------------
|
|
|
|
void MusicXml::xmlHarmony(QDomElement e, int tick, Measure* measure, int staff)
|
|
{
|
|
// type:
|
|
|
|
// placement:
|
|
// in order to work correctly, this should probably be adjusted to account for spatium
|
|
// but in any case, we don't support import relative-x/y for other elements
|
|
// no reason to do so for chord symbols
|
|
double rx = 0.0; // 0.1 * e.attribute("relative-x", "0").toDouble();
|
|
double ry = 0.0; // -0.1 * e.attribute("relative-y", "0").toDouble();
|
|
|
|
double styleYOff = score->textStyle(TextStyleType::HARMONY).offset().y();
|
|
OffsetType offsetType = score->textStyle(TextStyleType::HARMONY).offsetType();
|
|
if (offsetType == OffsetType::ABS) {
|
|
styleYOff = styleYOff * MScore::DPMM / score->spatium();
|
|
}
|
|
|
|
double dy = -0.1 * e.attribute("default-y", QString::number(styleYOff* -10)).toDouble();
|
|
|
|
QString printObject(e.attribute("print-object", "yes"));
|
|
QString printFrame(e.attribute("print-frame"));
|
|
QString printStyle(e.attribute("print-style"));
|
|
|
|
QString kind, kindText, symbols, parens;
|
|
QList<HDegree> degreeList;
|
|
|
|
if (harmony) {
|
|
qDebug("MusicXML::import: more than one harmony");
|
|
return;
|
|
};
|
|
|
|
Harmony* ha = new Harmony(score);
|
|
ha->setUserOff(QPointF(rx, ry + dy - styleYOff));
|
|
int offset = 0;
|
|
for (e = e.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
QString tag(e.tagName());
|
|
if (tag == "root") {
|
|
QString step;
|
|
int alter = 0;
|
|
bool invalidRoot = false;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
QString tag(ee.tagName());
|
|
if (tag == "root-step") {
|
|
// attributes: print-style
|
|
step = ee.text();
|
|
if (ee.hasAttribute("text")) {
|
|
QString rtext = ee.attribute("text");
|
|
if (rtext == "") {
|
|
invalidRoot = true;
|
|
}
|
|
}
|
|
}
|
|
else if (tag == "root-alter") {
|
|
// attributes: print-object, print-style
|
|
// location (left-right)
|
|
alter = ee.text().toInt();
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
if (invalidRoot)
|
|
ha->setRootTpc(Tpc::TPC_INVALID);
|
|
else
|
|
ha->setRootTpc(step2tpc(step, AccidentalVal(alter)));
|
|
}
|
|
else if (tag == "function") {
|
|
// attributes: print-style
|
|
domNotImplemented(e);
|
|
}
|
|
else if (tag == "kind") {
|
|
// attributes: use-symbols yes-no
|
|
// text, stack-degrees, parentheses-degree, bracket-degrees,
|
|
// print-style, halign, valign
|
|
|
|
kindText = e.attribute("text");
|
|
symbols = e.attribute("use-symbols");
|
|
parens = e.attribute("parentheses-degrees");
|
|
kind = e.text();
|
|
}
|
|
else if (tag == "inversion") {
|
|
// attributes: print-style
|
|
}
|
|
else if (tag == "bass") {
|
|
QString step;
|
|
int alter = 0;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
QString tag(ee.tagName());
|
|
if (tag == "bass-step") {
|
|
// attributes: print-style
|
|
step = ee.text();
|
|
}
|
|
else if (tag == "bass-alter") {
|
|
// attributes: print-object, print-style
|
|
// location (left-right)
|
|
alter = ee.text().toInt();
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
ha->setBaseTpc(step2tpc(step, AccidentalVal(alter)));
|
|
}
|
|
else if (tag == "degree") {
|
|
int degreeValue = 0;
|
|
int degreeAlter = 0;
|
|
QString degreeType = "";
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
QString tag(ee.tagName());
|
|
if (tag == "degree-value") {
|
|
degreeValue = ee.text().toInt();
|
|
}
|
|
else if (tag == "degree-alter") {
|
|
degreeAlter = ee.text().toInt();
|
|
}
|
|
else if (tag == "degree-type") {
|
|
degreeType = ee.text();
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
if (degreeValue <= 0 || degreeValue > 13
|
|
|| degreeAlter < -2 || degreeAlter > 2
|
|
|| (degreeType != "add" && degreeType != "alter" && degreeType != "subtract")) {
|
|
qDebug("incorrect degree: degreeValue=%d degreeAlter=%d degreeType=%s",
|
|
degreeValue, degreeAlter, qPrintable(degreeType));
|
|
}
|
|
else {
|
|
if (degreeType == "add")
|
|
degreeList << HDegree(degreeValue, degreeAlter, HDegreeType::ADD);
|
|
else if (degreeType == "alter")
|
|
degreeList << HDegree(degreeValue, degreeAlter, HDegreeType::ALTER);
|
|
else if (degreeType == "subtract")
|
|
degreeList << HDegree(degreeValue, degreeAlter, HDegreeType::SUBTRACT);
|
|
}
|
|
}
|
|
else if (tag == "frame") {
|
|
qDebug("xmlHarmony: found harmony frame");
|
|
FretDiagram* fd = new FretDiagram(score);
|
|
fd->setTrack(staff * VOICES);
|
|
// read frame into FretDiagram
|
|
readFretDiagram(fd, e);
|
|
Segment* s = measure->getSegment(Segment::Type::ChordRest, tick);
|
|
s->add(fd);
|
|
}
|
|
else if (tag == "level")
|
|
domNotImplemented(e);
|
|
else if (tag == "offset")
|
|
offset = calcTicks(e.text(), divisions);
|
|
else
|
|
domError(e);
|
|
}
|
|
|
|
const ChordDescription* d = 0;
|
|
if (ha->rootTpc() != Tpc::TPC_INVALID)
|
|
d = ha->fromXml(kind, kindText, symbols, parens, degreeList);
|
|
if (d) {
|
|
ha->setId(d->id);
|
|
ha->setTextName(d->names.front());
|
|
}
|
|
else {
|
|
ha->setId(-1);
|
|
ha->setTextName(kindText);
|
|
}
|
|
ha->render();
|
|
|
|
ha->setVisible(printObject == "yes");
|
|
|
|
// TODO-LV: do this only if ha points to a valid harmony
|
|
// harmony = ha;
|
|
ha->setTrack(staff * VOICES);
|
|
Segment* s = measure->getSegment(Segment::Type::ChordRest, tick + offset);
|
|
s->add(ha);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// xmlClef
|
|
//---------------------------------------------------------
|
|
|
|
StaffTypes MusicXml::xmlClef(QDomElement e, int staffIdx, Measure* measure)
|
|
{
|
|
ClefType clef = ClefType::G;
|
|
StaffTypes res = StaffTypes::STANDARD;
|
|
int clefno = e.attribute(QString("number"), "1").toInt() - 1;
|
|
QString c;
|
|
int i = 0;
|
|
int line = -1;
|
|
for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
|
|
if (ee.tagName() == "sign")
|
|
c = ee.text();
|
|
else if (ee.tagName() == "line")
|
|
line = ee.text().toInt();
|
|
else if (ee.tagName() == "clef-octave-change") {
|
|
i = ee.text().toInt();
|
|
if (i && !(c == "F" || c == "G"))
|
|
qDebug("clef-octave-change only implemented for F and G key");
|
|
}
|
|
else
|
|
domError(ee);
|
|
}
|
|
|
|
//some software (Primus) don't include line and assume some default
|
|
// it's permitted by MusicXML 2.0 XSD
|
|
if (line == -1) {
|
|
if (c == "G")
|
|
line = 2;
|
|
else if (c == "F")
|
|
line = 4;
|
|
else if (c == "C")
|
|
line = 3;
|
|
}
|
|
|
|
if (c == "G" && i == 0 && line == 2)
|
|
clef = ClefType::G;
|
|
else if (c == "G" && i == 1 && line == 2)
|
|
clef = ClefType::G1;
|
|
else if (c == "G" && i == 2 && line == 2)
|
|
clef = ClefType::G2;
|
|
else if (c == "G" && i == -1 && line == 2)
|
|
clef = ClefType::G3;
|
|
else if (c == "G" && i == 0 && line == 1)
|
|
clef = ClefType::G4;
|
|
else if (c == "F" && i == 0 && line == 3)
|
|
clef = ClefType::F_B;
|
|
else if (c == "F" && i == 0 && line == 4)
|
|
clef = ClefType::F;
|
|
else if (c == "F" && i == 1 && line == 4)
|
|
clef = ClefType::F_8VA;
|
|
else if (c == "F" && i == 2 && line == 4)
|
|
clef = ClefType::F_15MA;
|
|
else if (c == "F" && i == -1 && line == 4)
|
|
clef = ClefType::F8;
|
|
else if (c == "F" && i == -2 && line == 4)
|
|
clef = ClefType::F15;
|
|
else if (c == "F" && i == 0 && line == 5)
|
|
clef = ClefType::F_C;
|
|
else if (c == "C") {
|
|
if (line == 5)
|
|
clef = ClefType::C5;
|
|
else if (line == 4)
|
|
clef = ClefType::C4;
|
|
else if (line == 3)
|
|
clef = ClefType::C3;
|
|
else if (line == 2)
|
|
clef = ClefType::C2;
|
|
else if (line == 1)
|
|
clef = ClefType::C1;
|
|
}
|
|
else if (c == "percussion") {
|
|
clef = ClefType::PERC;
|
|
res = StaffTypes::PERC_DEFAULT;
|
|
}
|
|
else if (c == "TAB") {
|
|
clef = ClefType::TAB;
|
|
res = StaffTypes::TAB_DEFAULT;
|
|
}
|
|
else
|
|
qDebug("ImportMusicXML: unknown clef <sign=%s line=%d oct ch=%d>", qPrintable(c), line, i);
|
|
// note: also generate symbol for tick 0
|
|
// was not necessary before 0.9.6
|
|
if (clefno < score->staff(staffIdx)->part()->staves()->size()) {
|
|
Clef* clefs = new Clef(score);
|
|
clefs->setClefType(clef);
|
|
clefs->setTrack((staffIdx + clefno) * VOICES);
|
|
Segment* s = measure->getSegment(clefs, tick);
|
|
s->add(clefs);
|
|
// clefs->staff()->setClef(tick, clefs->clefTypeList());
|
|
}
|
|
return res;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// findSlurElement
|
|
//---------------------------------------------------------
|
|
QList<QDomElement> MusicXml::findSlurElements(QDomElement e)
|
|
{
|
|
QList<QDomElement> slurs;
|
|
for (e = e.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
|
|
QString tag(e.tagName());
|
|
if (tag == "notations"){
|
|
for(e = e.firstChildElement(); !e.isNull(); e = e.nextSiblingElement())
|
|
if (e.tagName() == "slur")
|
|
slurs.append(e);
|
|
}
|
|
}
|
|
return slurs;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addGraceNoteAfter
|
|
//---------------------------------------------------------
|
|
|
|
void MusicXml::addGraceNoteAfter(Chord* graceNote, Segment* segm)
|
|
{
|
|
if(segm){
|
|
graceNote->toGraceAfter();
|
|
Element* el = segm->element(graceNote->track());
|
|
if (el && el->type() == Element::Type::CHORD) {
|
|
Chord* cr = static_cast<Chord*>(el);
|
|
cr->add(graceNote);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|