1330 lines
47 KiB
C++
1330 lines
47 KiB
C++
//=============================================================================
|
||
// MuseScore
|
||
// Music Composition & Notation
|
||
//
|
||
// Copyright (C) 2011-2012 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
|
||
// as published by the Free Software Foundation and appearing in
|
||
// the file LICENSE.GPL
|
||
//=============================================================================
|
||
|
||
#include "config.h"
|
||
#include "score.h"
|
||
#include "xml.h"
|
||
#include "element.h"
|
||
#include "measure.h"
|
||
#include "segment.h"
|
||
#include "slur.h"
|
||
#include "chordrest.h"
|
||
#include "chord.h"
|
||
#include "tuplet.h"
|
||
#include "beam.h"
|
||
#include "revisions.h"
|
||
#include "page.h"
|
||
#include "part.h"
|
||
#include "staff.h"
|
||
#include "system.h"
|
||
#include "keysig.h"
|
||
#include "clef.h"
|
||
#include "text.h"
|
||
#include "ottava.h"
|
||
#include "volta.h"
|
||
#include "excerpt.h"
|
||
#include "mscore.h"
|
||
#include "stafftype.h"
|
||
#include "sym.h"
|
||
|
||
#ifdef OMR
|
||
#include "omr/omr.h"
|
||
#include "omr/omrpage.h"
|
||
#endif
|
||
|
||
#include "sig.h"
|
||
#include "undo.h"
|
||
#include "imageStore.h"
|
||
#include "audio.h"
|
||
#include "barline.h"
|
||
#include "thirdparty/qzip/qzipreader_p.h"
|
||
#include "thirdparty/qzip/qzipwriter_p.h"
|
||
#ifdef Q_OS_WIN
|
||
#include <windows.h>
|
||
#endif
|
||
|
||
namespace Ms {
|
||
|
||
//---------------------------------------------------------
|
||
// writeMeasure
|
||
//---------------------------------------------------------
|
||
|
||
static void writeMeasure(XmlWriter& xml, MeasureBase* m, int staffIdx, bool writeSystemElements, bool forceTimeSig)
|
||
{
|
||
//
|
||
// special case multi measure rest
|
||
//
|
||
if (m->isMeasure() || staffIdx == 0)
|
||
m->write(xml, staffIdx, writeSystemElements, forceTimeSig);
|
||
|
||
if (m->score()->styleB(Sid::createMultiMeasureRests) && m->isMeasure() && toMeasure(m)->mmRest())
|
||
toMeasure(m)->mmRest()->write(xml, staffIdx, writeSystemElements, forceTimeSig);
|
||
|
||
xml.setCurTick(m->endTick());
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// writeMovement
|
||
//---------------------------------------------------------
|
||
|
||
void Score::writeMovement(XmlWriter& xml, bool selectionOnly)
|
||
{
|
||
// if we have multi measure rests and some parts are hidden,
|
||
// then some layout information is missing:
|
||
// relayout with all parts set visible
|
||
|
||
QList<Part*> hiddenParts;
|
||
bool unhide = false;
|
||
if (styleB(Sid::createMultiMeasureRests)) {
|
||
for (Part* part : _parts) {
|
||
if (!part->show()) {
|
||
if (!unhide) {
|
||
startCmd();
|
||
unhide = true;
|
||
}
|
||
part->undoChangeProperty(Pid::VISIBLE, true);
|
||
hiddenParts.append(part);
|
||
}
|
||
}
|
||
}
|
||
if (unhide) {
|
||
doLayout();
|
||
for (Part* p : hiddenParts)
|
||
p->setShow(false);
|
||
}
|
||
|
||
xml.stag(this);
|
||
if (excerpt()) {
|
||
Excerpt* e = excerpt();
|
||
QMultiMap<int, int> trackList = e->tracks();
|
||
QMapIterator<int, int> i(trackList);
|
||
if (!(trackList.size() == e->parts().size() * VOICES) && !trackList.isEmpty()) {
|
||
while (i.hasNext()) {
|
||
i.next();
|
||
xml.tagE(QString("Tracklist sTrack=\"%1\" dstTrack=\"%2\"").arg(i.key()).arg(i.value()));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (lineMode())
|
||
xml.tag("layoutMode", "line");
|
||
if (systemMode())
|
||
xml.tag("layoutMode", "system");
|
||
|
||
#ifdef OMR
|
||
if (masterScore()->omr() && xml.writeOmr())
|
||
masterScore()->omr()->write(xml);
|
||
#endif
|
||
if (isMaster() && masterScore()->showOmr() && xml.writeOmr())
|
||
xml.tag("showOmr", masterScore()->showOmr());
|
||
if (_audio && xml.writeOmr()) {
|
||
xml.tag("playMode", int(_playMode));
|
||
_audio->write(xml);
|
||
}
|
||
|
||
for (int i = 0; i < 32; ++i) {
|
||
if (!_layerTags[i].isEmpty()) {
|
||
xml.tag(QString("LayerTag id=\"%1\" tag=\"%2\"").arg(i).arg(_layerTags[i]),
|
||
_layerTagComments[i]);
|
||
}
|
||
}
|
||
int n = _layer.size();
|
||
for (int i = 1; i < n; ++i) { // don’t save default variant
|
||
const Layer& l = _layer[i];
|
||
xml.tagE(QString("Layer name=\"%1\" mask=\"%2\"").arg(l.name).arg(l.tags));
|
||
}
|
||
xml.tag("currentLayer", _currentLayer);
|
||
|
||
if (isTopScore() && !MScore::testMode)
|
||
_synthesizerState.write(xml);
|
||
|
||
if (pageNumberOffset())
|
||
xml.tag("page-offset", pageNumberOffset());
|
||
xml.tag("Division", MScore::division);
|
||
xml.setCurTrack(-1);
|
||
|
||
if (isTopScore()) // only top score
|
||
style().save(xml, true); // save only differences to buildin style
|
||
|
||
xml.tag("showInvisible", _showInvisible);
|
||
xml.tag("showUnprintable", _showUnprintable);
|
||
xml.tag("showFrames", _showFrames);
|
||
xml.tag("showMargins", _showPageborders);
|
||
|
||
QMapIterator<QString, QString> i(_metaTags);
|
||
while (i.hasNext()) {
|
||
i.next();
|
||
// do not output "platform" and "creationDate" in test and save template mode
|
||
if ((!MScore::testMode && !MScore::saveTemplateMode) || (i.key() != "platform" && i.key() != "creationDate"))
|
||
xml.tag(QString("metaTag name=\"%1\"").arg(i.key().toHtmlEscaped()), i.value());
|
||
}
|
||
|
||
xml.setCurTrack(0);
|
||
int staffStart;
|
||
int staffEnd;
|
||
MeasureBase* measureStart;
|
||
MeasureBase* measureEnd;
|
||
|
||
if (selectionOnly) {
|
||
staffStart = _selection.staffStart();
|
||
staffEnd = _selection.staffEnd();
|
||
// make sure we select full parts
|
||
Staff* sStaff = staff(staffStart);
|
||
Part* sPart = sStaff->part();
|
||
Staff* eStaff = staff(staffEnd - 1);
|
||
Part* ePart = eStaff->part();
|
||
staffStart = staffIdx(sPart);
|
||
staffEnd = staffIdx(ePart) + ePart->nstaves();
|
||
measureStart = _selection.startSegment()->measure();
|
||
if (measureStart->isMeasure() && toMeasure(measureStart)->isMMRest())
|
||
measureStart = toMeasure(measureStart)->mmRestFirst();
|
||
if (_selection.endSegment())
|
||
measureEnd = _selection.endSegment()->measure()->next();
|
||
else
|
||
measureEnd = 0;
|
||
}
|
||
else {
|
||
staffStart = 0;
|
||
staffEnd = nstaves();
|
||
measureStart = first();
|
||
measureEnd = 0;
|
||
}
|
||
|
||
// Let's decide: write midi mapping to a file or not
|
||
masterScore()->checkMidiMapping();
|
||
for (const Part* part : _parts) {
|
||
if (!selectionOnly || ((staffIdx(part) >= staffStart) && (staffEnd >= staffIdx(part) + part->nstaves())))
|
||
part->write(xml);
|
||
}
|
||
|
||
xml.setCurTrack(0);
|
||
xml.setTrackDiff(-staffStart * VOICES);
|
||
if (measureStart) {
|
||
for (int staffIdx = staffStart; staffIdx < staffEnd; ++staffIdx) {
|
||
xml.stag(staff(staffIdx), QString("id=\"%1\"").arg(staffIdx + 1 - staffStart));
|
||
xml.setCurTick(measureStart->tick());
|
||
xml.setTickDiff(xml.curTick());
|
||
xml.setCurTrack(staffIdx * VOICES);
|
||
bool writeSystemElements = (staffIdx == staffStart);
|
||
bool firstMeasureWritten = false;
|
||
bool forceTimeSig = false;
|
||
for (MeasureBase* m = measureStart; m != measureEnd; m = m->next()) {
|
||
// force timesig if first measure and selectionOnly
|
||
if (selectionOnly && m->isMeasure()) {
|
||
if (!firstMeasureWritten) {
|
||
forceTimeSig = true;
|
||
firstMeasureWritten = true;
|
||
}
|
||
else
|
||
forceTimeSig = false;
|
||
}
|
||
writeMeasure(xml, m, staffIdx, writeSystemElements, forceTimeSig);
|
||
}
|
||
xml.etag();
|
||
}
|
||
}
|
||
xml.setCurTrack(-1);
|
||
if (isMaster()) {
|
||
if (!selectionOnly) {
|
||
for (const Excerpt* excerpt : excerpts()) {
|
||
if (excerpt->partScore() != this)
|
||
excerpt->partScore()->write(xml, false); // recursion
|
||
}
|
||
}
|
||
}
|
||
else
|
||
xml.tag("name", excerpt()->title());
|
||
xml.etag();
|
||
|
||
if (unhide)
|
||
endCmd(true);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// write
|
||
//---------------------------------------------------------
|
||
|
||
void Score::write(XmlWriter& xml, bool selectionOnly)
|
||
{
|
||
if (isMaster()) {
|
||
MasterScore* score = static_cast<MasterScore*>(this);
|
||
while (score->prev())
|
||
score = score->prev();
|
||
while (score) {
|
||
score->writeMovement(xml, selectionOnly);
|
||
score = score->next();
|
||
}
|
||
}
|
||
else
|
||
writeMovement(xml, selectionOnly);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// Staff
|
||
//---------------------------------------------------------
|
||
|
||
void Score::readStaff(XmlReader& e)
|
||
{
|
||
int staff = e.intAttribute("id", 1) - 1;
|
||
int measureIdx = 0;
|
||
e.setCurrentMeasureIndex(0);
|
||
e.setTick(Fraction(0,1));
|
||
e.setTrack(staff * VOICES);
|
||
|
||
if (staff == 0) {
|
||
while (e.readNextStartElement()) {
|
||
const QStringRef& tag(e.name());
|
||
|
||
if (tag == "Measure") {
|
||
Measure* measure = 0;
|
||
measure = new Measure(this);
|
||
measure->setTick(e.tick());
|
||
e.setCurrentMeasureIndex(measureIdx++);
|
||
//
|
||
// inherit timesig from previous measure
|
||
//
|
||
Measure* m = e.lastMeasure(); // measure->prevMeasure();
|
||
Fraction f(m ? m->timesig() : Fraction(4,4));
|
||
measure->setTicks(f);
|
||
measure->setTimesig(f);
|
||
|
||
measure->read(e, staff);
|
||
measure->checkMeasure(staff);
|
||
if (!measure->isMMRest()) {
|
||
measures()->add(measure);
|
||
e.setLastMeasure(measure);
|
||
e.setTick(measure->tick() + measure->ticks());
|
||
}
|
||
else {
|
||
// this is a multi measure rest
|
||
// always preceded by the first measure it replaces
|
||
Measure* m1 = e.lastMeasure();
|
||
|
||
if (m1) {
|
||
m1->setMMRest(measure);
|
||
measure->setTick(m1->tick());
|
||
}
|
||
}
|
||
}
|
||
else if (tag == "HBox" || tag == "VBox" || tag == "TBox" || tag == "FBox") {
|
||
MeasureBase* mb = toMeasureBase(Element::name2Element(tag, this));
|
||
mb->read(e);
|
||
mb->setTick(e.tick());
|
||
measures()->add(mb);
|
||
}
|
||
else if (tag == "tick")
|
||
e.setTick(Fraction::fromTicks(fileDivision(e.readInt())));
|
||
else
|
||
e.unknown();
|
||
}
|
||
}
|
||
else {
|
||
Measure* measure = firstMeasure();
|
||
while (e.readNextStartElement()) {
|
||
const QStringRef& tag(e.name());
|
||
|
||
if (tag == "Measure") {
|
||
if (measure == 0) {
|
||
qDebug("Score::readStaff(): missing measure!");
|
||
measure = new Measure(this);
|
||
measure->setTick(e.tick());
|
||
measures()->add(measure);
|
||
}
|
||
e.setTick(measure->tick());
|
||
e.setCurrentMeasureIndex(measureIdx++);
|
||
measure->read(e, staff);
|
||
measure->checkMeasure(staff);
|
||
if (measure->isMMRest())
|
||
measure = e.lastMeasure()->nextMeasure();
|
||
else {
|
||
e.setLastMeasure(measure);
|
||
if (measure->mmRest())
|
||
measure = measure->mmRest();
|
||
else
|
||
measure = measure->nextMeasure();
|
||
}
|
||
}
|
||
else if (tag == "tick")
|
||
e.setTick(Fraction::fromTicks(fileDivision(e.readInt())));
|
||
else
|
||
e.unknown();
|
||
}
|
||
}
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveFile
|
||
/// If file has generated name, create a modal file save dialog
|
||
/// and ask filename.
|
||
/// Rename old file to backup file (.xxxx.msc?,).
|
||
/// Default is to save score in .mscz format,
|
||
/// Return true if OK and false on error.
|
||
//---------------------------------------------------------
|
||
|
||
bool MasterScore::saveFile()
|
||
{
|
||
if (readOnly())
|
||
return false;
|
||
QString suffix = info.suffix();
|
||
if (info.exists() && !info.isWritable()) {
|
||
MScore::lastError = tr("The following file is locked: \n%1 \n\nTry saving to a different location.").arg(info.filePath());
|
||
return false;
|
||
}
|
||
//
|
||
// step 1
|
||
// save into temporary file to prevent partially overwriting
|
||
// the original file in case of "disc full"
|
||
//
|
||
|
||
QString tempName = info.filePath() + QString(".temp");
|
||
QFile temp(tempName);
|
||
if (!temp.open(QIODevice::WriteOnly)) {
|
||
MScore::lastError = tr("Open Temp File\n%1\nfailed: %2").arg(tempName, strerror(errno));
|
||
return false;
|
||
}
|
||
bool rv = suffix == "mscx" ? Score::saveFile(&temp, false) : Score::saveCompressedFile(&temp, info, false);
|
||
if (!rv) {
|
||
return false;
|
||
}
|
||
|
||
if (temp.error() != QFile::NoError) {
|
||
MScore::lastError = tr("Save File failed: %1").arg(temp.errorString());
|
||
return false;
|
||
}
|
||
temp.close();
|
||
|
||
QString name(info.filePath());
|
||
QString basename(info.fileName());
|
||
QDir dir(info.path());
|
||
if (!saved()) {
|
||
// if file was already saved in this session
|
||
// save but don't overwrite backup again
|
||
|
||
//
|
||
// step 2
|
||
// remove old backup file if exists
|
||
//
|
||
QString backupName = QString(".") + info.fileName() + QString(",");
|
||
if (dir.exists(backupName)) {
|
||
if (!dir.remove(backupName)) {
|
||
// if (!MScore::noGui)
|
||
// QMessageBox::critical(0, QObject::tr("Save File"),
|
||
// tr("Removing old backup file %1 failed").arg(backupName));
|
||
}
|
||
}
|
||
|
||
//
|
||
// step 3
|
||
// rename old file into backup
|
||
//
|
||
if (dir.exists(basename)) {
|
||
if (!dir.rename(basename, backupName)) {
|
||
// if (!MScore::noGui)
|
||
// QMessageBox::critical(0, tr("Save File"),
|
||
// tr("Renaming old file <%1> to backup <%2> failed").arg(name, backupname);
|
||
}
|
||
}
|
||
|
||
QFileInfo fileBackup(dir, backupName);
|
||
_sessionStartBackupInfo = fileBackup;
|
||
|
||
#ifdef Q_OS_WIN
|
||
QString backupNativePath = QDir::toNativeSeparators(fileBackup.absoluteFilePath());
|
||
#if (defined (_MSCVER) || defined (_MSC_VER))
|
||
#if (defined (UNICODE))
|
||
SetFileAttributes((LPCTSTR)backupNativePath.unicode(), FILE_ATTRIBUTE_HIDDEN);
|
||
#else
|
||
// Use byte-based Windows function
|
||
SetFileAttributes((LPCTSTR)backupNativePath.toLocal8Bit(), FILE_ATTRIBUTE_HIDDEN);
|
||
#endif
|
||
#else
|
||
SetFileAttributes((LPCTSTR)backupNativePath.toLocal8Bit(), FILE_ATTRIBUTE_HIDDEN);
|
||
#endif
|
||
#endif
|
||
}
|
||
else {
|
||
// file has previously been saved - remove the old file
|
||
if (dir.exists(basename)) {
|
||
if (!dir.remove(basename)) {
|
||
// if (!MScore::noGui)
|
||
// QMessageBox::critical(0, tr("Save File"),
|
||
// tr("Removing old file %1 failed").arg(name));
|
||
}
|
||
}
|
||
}
|
||
|
||
//
|
||
// step 4
|
||
// rename temp name into file name
|
||
//
|
||
if (!QFile::rename(tempName, name)) {
|
||
MScore::lastError = tr("Renaming temp. file <%1> to <%2> failed:\n%3").arg(tempName, name, strerror(errno));
|
||
return false;
|
||
}
|
||
// make file readable by all
|
||
QFile::setPermissions(name, QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser
|
||
| QFile::ReadGroup | QFile::ReadOther);
|
||
|
||
undoStack()->setClean();
|
||
setSaved(true);
|
||
info.refresh();
|
||
update();
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveCompressedFile
|
||
//---------------------------------------------------------
|
||
|
||
bool Score::saveCompressedFile(QFileInfo& info, bool onlySelection)
|
||
{
|
||
if (readOnly() && info == *masterScore()->fileInfo())
|
||
return false;
|
||
QFile fp(info.filePath());
|
||
if (!fp.open(QIODevice::WriteOnly)) {
|
||
MScore::lastError = tr("Open File\n%1\nfailed: %2").arg(info.filePath(), strerror(errno));
|
||
return false;
|
||
}
|
||
return saveCompressedFile(&fp, info, onlySelection);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// createThumbnail
|
||
//---------------------------------------------------------
|
||
|
||
QImage Score::createThumbnail()
|
||
{
|
||
LayoutMode mode = layoutMode();
|
||
setLayoutMode(LayoutMode::PAGE);
|
||
doLayout();
|
||
|
||
Page* page = pages().at(0);
|
||
QRectF fr = page->abbox();
|
||
qreal mag = 256.0 / qMax(fr.width(), fr.height());
|
||
int w = int(fr.width() * mag);
|
||
int h = int(fr.height() * mag);
|
||
|
||
QImage pm(w, h, QImage::Format_ARGB32_Premultiplied);
|
||
|
||
int dpm = lrint(DPMM * 1000.0);
|
||
pm.setDotsPerMeterX(dpm);
|
||
pm.setDotsPerMeterY(dpm);
|
||
pm.fill(0xffffffff);
|
||
|
||
double pr = MScore::pixelRatio;
|
||
MScore::pixelRatio = 1.0;
|
||
|
||
QPainter p(&pm);
|
||
p.setRenderHint(QPainter::Antialiasing, true);
|
||
p.setRenderHint(QPainter::TextAntialiasing, true);
|
||
p.scale(mag, mag);
|
||
print(&p, 0);
|
||
p.end();
|
||
|
||
MScore::pixelRatio = pr;
|
||
|
||
if (layoutMode() != mode) {
|
||
setLayoutMode(mode);
|
||
doLayout();
|
||
}
|
||
return pm;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveCompressedFile
|
||
// file is already opened
|
||
//---------------------------------------------------------
|
||
|
||
bool Score::saveCompressedFile(QFileDevice* f, QFileInfo& info, bool onlySelection, bool doCreateThumbnail)
|
||
{
|
||
MQZipWriter uz(f);
|
||
|
||
QString fn = info.completeBaseName() + ".mscx";
|
||
QBuffer cbuf;
|
||
cbuf.open(QIODevice::ReadWrite);
|
||
XmlWriter xml(this, &cbuf);
|
||
xml << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
||
xml.stag("container");
|
||
xml.stag("rootfiles");
|
||
xml.stag(QString("rootfile full-path=\"%1\"").arg(XmlWriter::xmlString(fn)));
|
||
xml.etag();
|
||
for (ImageStoreItem* ip : imageStore) {
|
||
if (!ip->isUsed(this))
|
||
continue;
|
||
QString path = QString("Pictures/") + ip->hashName();
|
||
xml.tag("file", path);
|
||
}
|
||
|
||
xml.etag();
|
||
xml.etag();
|
||
cbuf.seek(0);
|
||
//uz.addDirectory("META-INF");
|
||
uz.addFile("META-INF/container.xml", cbuf.data());
|
||
|
||
QBuffer dbuf;
|
||
dbuf.open(QIODevice::ReadWrite);
|
||
saveFile(&dbuf, true, onlySelection);
|
||
dbuf.seek(0);
|
||
uz.addFile(fn, dbuf.data());
|
||
f->flush(); // flush to preserve score data in case of
|
||
// any failures on the further operations.
|
||
|
||
// save images
|
||
//uz.addDirectory("Pictures");
|
||
for (ImageStoreItem* ip : imageStore) {
|
||
if (!ip->isUsed(this))
|
||
continue;
|
||
QString path = QString("Pictures/") + ip->hashName();
|
||
uz.addFile(path, ip->buffer());
|
||
}
|
||
|
||
// create thumbnail
|
||
if (doCreateThumbnail && !pages().isEmpty()) {
|
||
QImage pm = createThumbnail();
|
||
|
||
QByteArray ba;
|
||
QBuffer b(&ba);
|
||
if (!b.open(QIODevice::WriteOnly))
|
||
qDebug("open buffer failed");
|
||
if (!pm.save(&b, "PNG"))
|
||
qDebug("save failed");
|
||
uz.addFile("Thumbnails/thumbnail.png", ba);
|
||
}
|
||
|
||
#ifdef OMR
|
||
//
|
||
// save OMR page images
|
||
//
|
||
if (masterScore()->omr()) {
|
||
int n = masterScore()->omr()->numPages();
|
||
for (int i = 0; i < n; ++i) {
|
||
QString path = QString("OmrPages/page%1.png").arg(i+1);
|
||
QBuffer cbuf1;
|
||
OmrPage* page = masterScore()->omr()->page(i);
|
||
const QImage& image = page->image();
|
||
if (!image.save(&cbuf1, "PNG")) {
|
||
MScore::lastError = tr("Save file: cannot save image (%1x%2)").arg(image.width(), image.height());
|
||
return false;
|
||
}
|
||
uz.addFile(path, cbuf1.data());
|
||
cbuf1.close();
|
||
}
|
||
}
|
||
#endif
|
||
//
|
||
// save audio
|
||
//
|
||
if (_audio)
|
||
uz.addFile("audio.ogg", _audio->data());
|
||
|
||
uz.close();
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveFile
|
||
// return true on success
|
||
//---------------------------------------------------------
|
||
|
||
bool Score::saveFile(QFileInfo& info)
|
||
{
|
||
if (readOnly() && info == *masterScore()->fileInfo())
|
||
return false;
|
||
if (info.suffix().isEmpty())
|
||
info.setFile(info.filePath() + ".mscx");
|
||
QFile fp(info.filePath());
|
||
if (!fp.open(QIODevice::WriteOnly)) {
|
||
MScore::lastError = tr("Open File\n%1\nfailed: %2").arg(info.filePath(), strerror(errno));
|
||
return false;
|
||
}
|
||
saveFile(&fp, false, false);
|
||
fp.close();
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// loadStyle
|
||
//---------------------------------------------------------
|
||
|
||
bool Score::loadStyle(const QString& fn, bool ignore)
|
||
{
|
||
QFile f(fn);
|
||
if (f.open(QIODevice::ReadOnly)) {
|
||
MStyle st = style();
|
||
if (st.load(&f, ignore)) {
|
||
undo(new ChangeStyle(this, st));
|
||
return true;
|
||
}
|
||
else {
|
||
MScore::lastError = tr("The style file is not compatible with this version of MuseScore.");
|
||
return false;
|
||
}
|
||
}
|
||
MScore::lastError = strerror(errno);
|
||
return false;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveStyle
|
||
//---------------------------------------------------------
|
||
|
||
bool Score::saveStyle(const QString& name)
|
||
{
|
||
QString ext(".mss");
|
||
QFileInfo info(name);
|
||
|
||
if (info.suffix().isEmpty())
|
||
info.setFile(info.filePath() + ext);
|
||
QFile f(info.filePath());
|
||
if (!f.open(QIODevice::WriteOnly)) {
|
||
MScore::lastError = tr("Open Style File\n%1\nfailed: %2").arg(info.filePath(), strerror(errno));
|
||
return false;
|
||
}
|
||
|
||
XmlWriter xml(this, &f);
|
||
xml.header();
|
||
xml.stag("museScore version=\"" MSC_VERSION "\"");
|
||
style().save(xml, false); // save complete style
|
||
xml.etag();
|
||
if (f.error() != QFile::NoError) {
|
||
MScore::lastError = tr("Write Style failed: %1").arg(f.errorString());
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// saveFile
|
||
// return true on success
|
||
//---------------------------------------------------------
|
||
|
||
extern QString revision;
|
||
|
||
bool Score::saveFile(QIODevice* f, bool msczFormat, bool onlySelection)
|
||
{
|
||
XmlWriter xml(this, f);
|
||
xml.setWriteOmr(msczFormat);
|
||
xml.header();
|
||
if (!MScore::testMode) {
|
||
xml.stag("museScore version=\"" MSC_VERSION "\"");
|
||
xml.tag("programVersion", VERSION);
|
||
xml.tag("programRevision", revision);
|
||
}
|
||
else
|
||
xml.stag("museScore version=\"3.01\"");
|
||
write(xml, onlySelection);
|
||
xml.etag();
|
||
if (isMaster())
|
||
masterScore()->revisions()->write(xml);
|
||
if (!onlySelection) {
|
||
//update version values for i.e. plugin access
|
||
_mscoreVersion = VERSION;
|
||
_mscoreRevision = revision.toInt(0, 16);
|
||
_mscVersion = MSCVERSION;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// readRootFile
|
||
//---------------------------------------------------------
|
||
|
||
QString readRootFile(MQZipReader* uz, QList<QString>& images)
|
||
{
|
||
QString rootfile;
|
||
|
||
QByteArray cbuf = uz->fileData("META-INF/container.xml");
|
||
if (cbuf.isEmpty()) {
|
||
qDebug("can't find container.xml");
|
||
return rootfile;
|
||
}
|
||
|
||
XmlReader e(cbuf);
|
||
|
||
while (e.readNextStartElement()) {
|
||
if (e.name() != "container") {
|
||
e.unknown();
|
||
continue;
|
||
}
|
||
while (e.readNextStartElement()) {
|
||
if (e.name() != "rootfiles") {
|
||
e.unknown();
|
||
continue;
|
||
}
|
||
while (e.readNextStartElement()) {
|
||
const QStringRef& tag(e.name());
|
||
|
||
if (tag == "rootfile") {
|
||
if (rootfile.isEmpty()) {
|
||
rootfile = e.attribute("full-path");
|
||
e.skipCurrentElement();
|
||
}
|
||
}
|
||
else if (tag == "file")
|
||
images.append(e.readElementText());
|
||
else
|
||
e.unknown();
|
||
}
|
||
}
|
||
}
|
||
return rootfile;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// loadCompressedMsc
|
||
// return false on error
|
||
//---------------------------------------------------------
|
||
|
||
Score::FileError MasterScore::loadCompressedMsc(QIODevice* io, bool ignoreVersionError)
|
||
{
|
||
MQZipReader uz(io);
|
||
|
||
QList<QString> sl;
|
||
QString rootfile = readRootFile(&uz, sl);
|
||
if (rootfile.isEmpty())
|
||
return FileError::FILE_NO_ROOTFILE;
|
||
|
||
//
|
||
// load images
|
||
//
|
||
if (!MScore::noImages) {
|
||
foreach(const QString& s, sl) {
|
||
QByteArray dbuf = uz.fileData(s);
|
||
imageStore.add(s, dbuf);
|
||
}
|
||
}
|
||
|
||
QByteArray dbuf = uz.fileData(rootfile);
|
||
if (dbuf.isEmpty()) {
|
||
QVector<MQZipReader::FileInfo> fil = uz.fileInfoList();
|
||
foreach(const MQZipReader::FileInfo& fi, fil) {
|
||
if (fi.filePath.endsWith(".mscx")) {
|
||
dbuf = uz.fileData(fi.filePath);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
XmlReader e(dbuf);
|
||
QBuffer readAheadBuf(&dbuf);
|
||
e.setReadAheadDevice(&readAheadBuf);
|
||
e.setDocName(masterScore()->fileInfo()->completeBaseName());
|
||
|
||
FileError retval = read1(e, ignoreVersionError);
|
||
|
||
#ifdef OMR
|
||
//
|
||
// load OMR page images
|
||
//
|
||
if (masterScore()->omr()) {
|
||
int n = masterScore()->omr()->numPages();
|
||
for (int i = 0; i < n; ++i) {
|
||
QString path = QString("OmrPages/page%1.png").arg(i+1);
|
||
QByteArray dbuf1 = uz.fileData(path);
|
||
OmrPage* page = masterScore()->omr()->page(i);
|
||
QImage image;
|
||
if (image.loadFromData(dbuf1, "PNG")) {
|
||
page->setImage(image);
|
||
}
|
||
else
|
||
qDebug("load image failed");
|
||
}
|
||
}
|
||
#endif
|
||
//
|
||
// read audio
|
||
//
|
||
if (audio()) {
|
||
QByteArray dbuf1 = uz.fileData("audio.ogg");
|
||
audio()->setData(dbuf1);
|
||
}
|
||
return retval;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// loadMsc
|
||
// return true on success
|
||
//---------------------------------------------------------
|
||
|
||
Score::FileError MasterScore::loadMsc(QString name, bool ignoreVersionError)
|
||
{
|
||
QFile f(name);
|
||
if (!f.open(QIODevice::ReadOnly)) {
|
||
MScore::lastError = f.errorString();
|
||
return FileError::FILE_OPEN_ERROR;
|
||
}
|
||
return loadMsc(name, &f, ignoreVersionError);
|
||
}
|
||
|
||
Score::FileError MasterScore::loadMsc(QString name, QIODevice* io, bool ignoreVersionError)
|
||
{
|
||
ScoreLoad sl;
|
||
fileInfo()->setFile(name);
|
||
|
||
if (name.endsWith(".mscz"))
|
||
return loadCompressedMsc(io, ignoreVersionError);
|
||
else {
|
||
XmlReader r(io);
|
||
r.setReadAheadDevice(io);
|
||
return read1(r, ignoreVersionError);
|
||
}
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// parseVersion
|
||
//---------------------------------------------------------
|
||
|
||
void MasterScore::parseVersion(const QString& val)
|
||
{
|
||
QRegExp re("(\\d+)\\.(\\d+)\\.(\\d+)");
|
||
int v1, v2, v3, rv1, rv2, rv3;
|
||
if (re.indexIn(VERSION) != -1) {
|
||
QStringList sl = re.capturedTexts();
|
||
if (sl.size() == 4) {
|
||
v1 = sl[1].toInt();
|
||
v2 = sl[2].toInt();
|
||
v3 = sl[3].toInt();
|
||
if (re.indexIn(val) != -1) {
|
||
sl = re.capturedTexts();
|
||
if (sl.size() == 4) {
|
||
rv1 = sl[1].toInt();
|
||
rv2 = sl[2].toInt();
|
||
rv3 = sl[3].toInt();
|
||
|
||
int currentVersion = v1 * 10000 + v2 * 100 + v3;
|
||
int readVersion = rv1 * 10000 + rv2 * 100 + rv3;
|
||
if (readVersion > currentVersion) {
|
||
qDebug("read future version");
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
QRegExp re1("(\\d+)\\.(\\d+)");
|
||
if (re1.indexIn(val) != -1) {
|
||
sl = re.capturedTexts();
|
||
if (sl.size() == 3) {
|
||
rv1 = sl[1].toInt();
|
||
rv2 = sl[2].toInt();
|
||
|
||
int currentVersion = v1 * 10000 + v2 * 100 + v3;
|
||
int readVersion = rv1 * 10000 + rv2 * 100;
|
||
if (readVersion > currentVersion) {
|
||
qDebug("read future version");
|
||
}
|
||
}
|
||
}
|
||
else
|
||
qDebug("1cannot parse <%s>", qPrintable(val));
|
||
}
|
||
}
|
||
}
|
||
else
|
||
qDebug("2cannot parse <%s>", VERSION);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// read1
|
||
// return true on success
|
||
//---------------------------------------------------------
|
||
|
||
Score::FileError MasterScore::read1(XmlReader& e, bool ignoreVersionError)
|
||
{
|
||
while (e.readNextStartElement()) {
|
||
if (e.name() == "museScore") {
|
||
const QString& version = e.attribute("version");
|
||
QStringList sl = version.split('.');
|
||
setMscVersion(sl[0].toInt() * 100 + sl[1].toInt());
|
||
|
||
if (!ignoreVersionError) {
|
||
QString message;
|
||
if (mscVersion() > MSCVERSION)
|
||
return FileError::FILE_TOO_NEW;
|
||
if (mscVersion() < 114)
|
||
return FileError::FILE_TOO_OLD;
|
||
if (mscVersion() == 300)
|
||
return FileError::FILE_OLD_300_FORMAT;
|
||
}
|
||
Score::FileError error;
|
||
if (mscVersion() <= 114)
|
||
error = read114(e);
|
||
else if (mscVersion() <= 207)
|
||
error = read206(e);
|
||
else
|
||
error = read301(e);
|
||
setExcerptsChanged(false);
|
||
return error;
|
||
}
|
||
else
|
||
e.unknown();
|
||
}
|
||
return FileError::FILE_CORRUPTED;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// print
|
||
//---------------------------------------------------------
|
||
|
||
void Score::print(QPainter* painter, int pageNo)
|
||
{
|
||
_printing = true;
|
||
MScore::pdfPrinting = true;
|
||
Page* page = pages().at(pageNo);
|
||
QRectF fr = page->abbox();
|
||
|
||
QList<Element*> ell = page->items(fr);
|
||
qStableSort(ell.begin(), ell.end(), elementLessThan);
|
||
for (const Element* e : ell) {
|
||
if (!e->visible())
|
||
continue;
|
||
painter->save();
|
||
painter->translate(e->pagePos());
|
||
e->draw(painter);
|
||
painter->restore();
|
||
}
|
||
MScore::pdfPrinting = false;
|
||
_printing = false;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// readCompressedToBuffer
|
||
//---------------------------------------------------------
|
||
|
||
QByteArray MasterScore::readCompressedToBuffer()
|
||
{
|
||
MQZipReader uz(info.filePath());
|
||
if (!uz.exists()) {
|
||
qDebug("Score::readCompressedToBuffer: cannot read zip file");
|
||
return QByteArray();
|
||
}
|
||
QList<QString> images;
|
||
QString rootfile = readRootFile(&uz, images);
|
||
|
||
//
|
||
// load images
|
||
//
|
||
foreach(const QString& s, images) {
|
||
QByteArray dbuf = uz.fileData(s);
|
||
imageStore.add(s, dbuf);
|
||
}
|
||
|
||
if (rootfile.isEmpty()) {
|
||
qDebug("=can't find rootfile in: %s", qPrintable(info.filePath()));
|
||
return QByteArray();
|
||
}
|
||
return uz.fileData(rootfile);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// readToBuffer
|
||
//---------------------------------------------------------
|
||
|
||
QByteArray MasterScore::readToBuffer()
|
||
{
|
||
QByteArray ba;
|
||
QString cs = info.suffix();
|
||
|
||
if (cs == "mscz") {
|
||
ba = readCompressedToBuffer();
|
||
}
|
||
if (cs.toLower() == "msc" || cs.toLower() == "mscx") {
|
||
QFile f(info.filePath());
|
||
if (f.open(QIODevice::ReadOnly)) {
|
||
ba = f.readAll();
|
||
f.close();
|
||
}
|
||
}
|
||
return ba;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// createRevision
|
||
//---------------------------------------------------------
|
||
|
||
void Score::createRevision()
|
||
{
|
||
#if 0
|
||
qDebug("createRevision");
|
||
QBuffer dbuf;
|
||
dbuf.open(QIODevice::ReadWrite);
|
||
saveFile(&dbuf, false, false);
|
||
dbuf.close();
|
||
|
||
QByteArray ba1 = readToBuffer();
|
||
|
||
QString os = QString::fromUtf8(ba1.data(), ba1.size());
|
||
QString ns = QString::fromUtf8(dbuf.buffer().data(), dbuf.buffer().size());
|
||
|
||
diff_match_patch dmp;
|
||
Revision* r = new Revision();
|
||
r->setDiff(dmp.patch_toText(dmp.patch_make(ns, os)));
|
||
r->setId("1");
|
||
_revisions->add(r);
|
||
|
||
// qDebug("patch:\n%s\n==========", qPrintable(patch));
|
||
//
|
||
#endif
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// writeVoiceMove
|
||
// write <move> and starting <voice> tags to denote
|
||
// change in position.
|
||
// Returns true if <voice> tag was written.
|
||
//---------------------------------------------------------
|
||
|
||
static bool writeVoiceMove(XmlWriter& xml, Segment* seg, const Fraction& startTick, int track, int* lastTrackWrittenPtr)
|
||
{
|
||
bool voiceTagWritten = false;
|
||
int& lastTrackWritten = *lastTrackWrittenPtr;
|
||
if ((lastTrackWritten < track) && !xml.clipboardmode()) {
|
||
while (lastTrackWritten < (track - 1)) {
|
||
xml.tagE("voice");
|
||
++lastTrackWritten;
|
||
}
|
||
xml.stag("voice");
|
||
xml.setCurTick(startTick);
|
||
xml.setCurTrack(track);
|
||
++lastTrackWritten;
|
||
voiceTagWritten = true;
|
||
}
|
||
|
||
if ((xml.curTick() != seg->tick()) || (track != xml.curTrack())) {
|
||
Location curr = Location::absolute();
|
||
Location dest = Location::absolute();
|
||
curr.setFrac(xml.curTick());
|
||
dest.setFrac(seg->tick());
|
||
curr.setTrack(xml.curTrack());
|
||
dest.setTrack(track);
|
||
|
||
dest.toRelative(curr);
|
||
dest.write(xml);
|
||
|
||
xml.setCurTick(seg->tick());
|
||
xml.setCurTrack(track);
|
||
}
|
||
|
||
return voiceTagWritten;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// writeSegments
|
||
// ls - write upto this segment (excluding)
|
||
// can be zero
|
||
//---------------------------------------------------------
|
||
|
||
void Score::writeSegments(XmlWriter& xml, int strack, int etrack,
|
||
Segment* fs, Segment* ls, bool writeSystemElements, bool forceTimeSig)
|
||
{
|
||
Fraction startTick = xml.curTick();
|
||
Fraction endTick = ls ? ls->tick() : lastMeasure()->endTick();
|
||
bool clip = xml.clipboardmode();
|
||
|
||
// in clipboard mode, ls might be in an mmrest
|
||
// since we are traversing regular measures,
|
||
// force them out of mmRest
|
||
if (clip) {
|
||
Measure* lm = ls ? ls->measure() : 0;
|
||
Measure* fm = fs ? fs->measure() : 0;
|
||
if (lm && lm->isMMRest()) {
|
||
lm = lm->mmRestLast();
|
||
if (lm)
|
||
ls = lm->nextMeasure() ? lm->nextMeasure()->first() : lastSegment();
|
||
else
|
||
qDebug("writeSegments: no measure for end segment in mmrest");
|
||
}
|
||
if (fm && fm->isMMRest()) {
|
||
fm = fm->mmRestFirst();
|
||
if (fm)
|
||
fs = fm->first();
|
||
}
|
||
}
|
||
|
||
QList<Spanner*> spanners;
|
||
#if 0
|
||
auto endIt = spanner().upper_bound(endTick);
|
||
for (auto i = spanner().begin(); i != endIt; ++i) {
|
||
Spanner* s = i->second;
|
||
#else
|
||
auto sl = spannerMap().findOverlapping(fs->tick().ticks(), endTick.ticks());
|
||
for (auto i : sl) {
|
||
Spanner* s = i.value;
|
||
#endif
|
||
if (s->generated() || !xml.canWrite(s))
|
||
continue;
|
||
// don't write voltas to clipboard
|
||
if (clip && s->isVolta())
|
||
continue;
|
||
spanners.push_back(s);
|
||
}
|
||
|
||
int lastTrackWritten = strack - 1; // for counting necessary <voice> tags
|
||
for (int track = strack; track < etrack; ++track) {
|
||
if (!xml.canWriteVoice(track))
|
||
continue;
|
||
|
||
bool voiceTagWritten = false;
|
||
|
||
bool timeSigWritten = false; // for forceTimeSig
|
||
bool crWritten = false; // for forceTimeSig
|
||
bool keySigWritten = false; // for forceTimeSig
|
||
|
||
for (Segment* segment = fs; segment && segment != ls; segment = segment->next1()) {
|
||
if (!segment->enabled())
|
||
continue;
|
||
if (track == 0)
|
||
segment->setWritten(false);
|
||
Element* e = segment->element(track);
|
||
|
||
//
|
||
// special case: - barline span > 1
|
||
// - part (excerpt) staff starts after
|
||
// barline element
|
||
bool needMove = (segment->tick() != xml.curTick() || (track > lastTrackWritten));
|
||
if ((segment->isEndBarLineType()) && !e && writeSystemElements && ((track % VOICES) == 0)) {
|
||
// search barline:
|
||
for (int idx = track - VOICES; idx >= 0; idx -= VOICES) {
|
||
if (segment->element(idx)) {
|
||
int oDiff = xml.trackDiff();
|
||
xml.setTrackDiff(idx); // staffIdx should be zero
|
||
segment->element(idx)->write(xml);
|
||
xml.setTrackDiff(oDiff);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
for (Element* e1 : segment->annotations()) {
|
||
if (e1->track() != track || e1->generated() || (e1->systemFlag() && !writeSystemElements))
|
||
continue;
|
||
if (needMove) {
|
||
voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
|
||
needMove = false;
|
||
}
|
||
e1->write(xml);
|
||
}
|
||
Measure* m = segment->measure();
|
||
// don't write spanners for multi measure rests
|
||
|
||
if ((!(m && m->isMMRest())) && segment->isChordRestType()) {
|
||
for (Spanner* s : spanners) {
|
||
if (s->track() == track) {
|
||
bool end = false;
|
||
if (s->anchor() == Spanner::Anchor::CHORD || s->anchor() == Spanner::Anchor::NOTE)
|
||
end = s->tick2() < endTick;
|
||
else
|
||
end = s->tick2() <= endTick;
|
||
if (s->tick() == segment->tick() && (!clip || end) && !s->isSlur()) {
|
||
if (needMove) {
|
||
voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
|
||
needMove = false;
|
||
}
|
||
s->writeSpannerStart(xml, segment, track);
|
||
}
|
||
}
|
||
if ((s->tick2() == segment->tick())
|
||
&& !s->isSlur()
|
||
&& (s->track2() == track || (s->track2() == -1 && s->track() == track))
|
||
&& (!clip || s->tick() >= fs->tick())
|
||
) {
|
||
if (needMove) {
|
||
voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
|
||
needMove = false;
|
||
}
|
||
s->writeSpannerEnd(xml, segment, track);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!e || !xml.canWrite(e))
|
||
continue;
|
||
if (e->generated())
|
||
continue;
|
||
if (forceTimeSig && track2voice(track) == 0 && segment->segmentType() == SegmentType::ChordRest && !timeSigWritten && !crWritten) {
|
||
// Ensure that <voice> tag is open
|
||
voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
|
||
// we will miss a key sig!
|
||
if (!keySigWritten) {
|
||
Key k = score()->staff(track2staff(track))->key(segment->tick());
|
||
KeySig* ks = new KeySig(this);
|
||
ks->setKey(k);
|
||
ks->write(xml);
|
||
delete ks;
|
||
keySigWritten = true;
|
||
}
|
||
// we will miss a time sig!
|
||
Fraction tsf = sigmap()->timesig(segment->tick()).timesig();
|
||
TimeSig* ts = new TimeSig(this);
|
||
ts->setSig(tsf);
|
||
ts->write(xml);
|
||
delete ts;
|
||
timeSigWritten = true;
|
||
}
|
||
if (needMove) {
|
||
voiceTagWritten |= writeVoiceMove(xml, segment, startTick, track, &lastTrackWritten);
|
||
needMove = false;
|
||
}
|
||
if (e->isChordRest()) {
|
||
ChordRest* cr = toChordRest(e);
|
||
cr->writeTupletStart(xml);
|
||
}
|
||
// if (segment->isEndBarLine() && (m->mmRestCount() < 0 || m->mmRest())) {
|
||
// BarLine* bl = toBarLine(e);
|
||
//TODO bl->setBarLineType(m->endBarLineType());
|
||
// bl->setVisible(m->endBarLineVisible());
|
||
// }
|
||
e->write(xml);
|
||
|
||
if (e->isChordRest()) {
|
||
ChordRest* cr = toChordRest(e);
|
||
cr->writeTupletEnd(xml);
|
||
}
|
||
|
||
segment->write(xml); // write only once
|
||
if (forceTimeSig) {
|
||
if (segment->segmentType() == SegmentType::KeySig)
|
||
keySigWritten = true;
|
||
if (segment->segmentType() == SegmentType::TimeSig)
|
||
timeSigWritten = true;
|
||
if (segment->segmentType() == SegmentType::ChordRest)
|
||
crWritten = true;
|
||
}
|
||
}
|
||
|
||
//write spanner ending after the last segment, on the last tick
|
||
if (clip || ls == 0) {
|
||
for (Spanner* s : spanners) {
|
||
if ((s->tick2() == endTick)
|
||
&& !s->isSlur()
|
||
&& (s->track2() == track || (s->track2() == -1 && s->track() == track))
|
||
&& (!clip || s->tick() >= fs->tick())
|
||
) {
|
||
s->writeSpannerEnd(xml, lastMeasure(), track, endTick);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (voiceTagWritten)
|
||
xml.etag(); // </voice>
|
||
}
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// searchTuplet
|
||
// search complete Dom for tuplet id
|
||
// last resort in case of error
|
||
//---------------------------------------------------------
|
||
|
||
Tuplet* Score::searchTuplet(XmlReader& /*e*/, int /*id*/)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
}
|
||
|