Files
lmms/src/core/DataFile.cpp
Michael Gregorius 9ca9143f5b Fix upgrade routine for BPM renamings (#7235)
Fix the upgrade routine that was introduced with pull request #6747 which added the BPM value to some file names.

This also simplifies the implementation by using a map.

Note: this also removes the code about the prefix `factorysample:`. If it is used in some files these entries will also have to be added to the map.
2024-05-01 18:03:35 +02:00

2111 lines
56 KiB
C++

/*
* DataFile.cpp - implementation of class DataFile
*
* Copyright (c) 2004-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
* Copyright (c) 2012-2013 Paul Giblock <p/at/pgiblock.net>
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* 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 (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#include "DataFile.h"
#include <algorithm>
#include <cmath>
#include <map>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QMessageBox>
#include <QRegularExpression>
#include <QSaveFile>
#include "base64.h"
#include "ConfigManager.h"
#include "Effect.h"
#include "embed.h"
#include "GuiApplication.h"
#include "LocaleHelper.h"
#include "Note.h"
#include "PluginFactory.h"
#include "ProjectVersion.h"
#include "SongEditor.h"
#include "TextFloat.h"
#include "Track.h"
#include "PathUtil.h"
#include "UpgradeExtendedNoteRange.h"
#include "lmmsversion.h"
namespace lmms
{
static void findIds(const QDomElement& elem, QList<jo_id_t>& idList);
// QMap with the DOM elements that access file resources
const DataFile::ResourcesMap DataFile::ELEMENTS_WITH_RESOURCES = {
{ "sampleclip", {"src"} },
{ "audiofileprocessor", {"src"} },
};
// Vector with all the upgrade methods
const std::vector<DataFile::UpgradeMethod> DataFile::UPGRADE_METHODS = {
&DataFile::upgrade_0_2_1_20070501 , &DataFile::upgrade_0_2_1_20070508,
&DataFile::upgrade_0_3_0_rc2 , &DataFile::upgrade_0_3_0,
&DataFile::upgrade_0_4_0_20080104 , &DataFile::upgrade_0_4_0_20080118,
&DataFile::upgrade_0_4_0_20080129 , &DataFile::upgrade_0_4_0_20080409,
&DataFile::upgrade_0_4_0_20080607 , &DataFile::upgrade_0_4_0_20080622,
&DataFile::upgrade_0_4_0_beta1 , &DataFile::upgrade_0_4_0_rc2,
&DataFile::upgrade_1_0_99 , &DataFile::upgrade_1_1_0,
&DataFile::upgrade_1_1_91 , &DataFile::upgrade_1_2_0_rc3,
&DataFile::upgrade_1_3_0 , &DataFile::upgrade_noHiddenClipNames,
&DataFile::upgrade_automationNodes , &DataFile::upgrade_extendedNoteRange,
&DataFile::upgrade_defaultTripleOscillatorHQ,
&DataFile::upgrade_mixerRename , &DataFile::upgrade_bbTcoRename,
&DataFile::upgrade_sampleAndHold , &DataFile::upgrade_midiCCIndexing,
&DataFile::upgrade_loopsRename , &DataFile::upgrade_noteTypes,
&DataFile::upgrade_fixCMTDelays
};
// Vector of all versions that have upgrade routines.
const std::vector<ProjectVersion> DataFile::UPGRADE_VERSIONS = {
"0.2.1-20070501" , "0.2.1-20070508" , "0.3.0-rc2",
"0.3.0" , "0.4.0-20080104" , "0.4.0-20080118",
"0.4.0-20080129" , "0.4.0-20080409" , "0.4.0-20080607",
"0.4.0-20080622" , "0.4.0-beta1" , "0.4.0-rc2",
"1.0.99-0" , "1.1.0-0" , "1.1.91-0",
"1.2.0-rc3" , "1.3.0"
};
namespace
{
struct TypeDescStruct
{
DataFile::Type m_type;
QString m_name;
};
const auto s_types = std::array{
TypeDescStruct{ DataFile::Type::Unknown, "unknown" },
TypeDescStruct{ DataFile::Type::SongProject, "song" },
TypeDescStruct{ DataFile::Type::SongProjectTemplate, "songtemplate" },
TypeDescStruct{ DataFile::Type::InstrumentTrackSettings, "instrumenttracksettings" },
TypeDescStruct{ DataFile::Type::DragNDropData, "dnddata" },
TypeDescStruct{ DataFile::Type::ClipboardData, "clipboard-data" },
TypeDescStruct{ DataFile::Type::JournalData, "journaldata" },
TypeDescStruct{ DataFile::Type::EffectSettings, "effectsettings" },
TypeDescStruct{ DataFile::Type::MidiClip, "midiclip" }
};
}
DataFile::DataFile( Type type ) :
QDomDocument( "lmms-project" ),
m_fileName(""),
m_content(),
m_head(),
m_type( type ),
m_fileVersion( UPGRADE_METHODS.size() )
{
appendChild( createProcessingInstruction("xml", "version=\"1.0\""));
QDomElement root = createElement( "lmms-project" );
root.setAttribute( "version", m_fileVersion );
root.setAttribute( "type", typeName( type ) );
root.setAttribute( "creator", "LMMS" );
root.setAttribute( "creatorversion", LMMS_VERSION );
appendChild( root );
m_head = createElement( "head" );
root.appendChild( m_head );
m_content = createElement( typeName( type ) );
root.appendChild( m_content );
}
DataFile::DataFile( const QString & _fileName ) :
QDomDocument(),
m_fileName(_fileName),
m_content(),
m_head(),
m_fileVersion( UPGRADE_METHODS.size() )
{
QFile inFile( _fileName );
if( !inFile.open( QIODevice::ReadOnly ) )
{
if (gui::getGUI() != nullptr)
{
QMessageBox::critical( nullptr,
gui::SongEditor::tr( "Could not open file" ),
gui::SongEditor::tr( "Could not open file %1. You probably "
"have no permissions to read this "
"file.\n Please make sure to have at "
"least read permissions to the file "
"and try again." ).arg( _fileName ) );
}
return;
}
loadData( inFile.readAll(), _fileName );
}
DataFile::DataFile( const QByteArray & _data ) :
QDomDocument(),
m_fileName(""),
m_content(),
m_head(),
m_fileVersion( UPGRADE_METHODS.size() )
{
loadData( _data, "<internal data>" );
}
bool DataFile::validate( QString extension )
{
switch( m_type )
{
case Type::SongProject:
if( extension == "mmp" || extension == "mmpz" )
{
return true;
}
break;
case Type::SongProjectTemplate:
if( extension == "mpt" )
{
return true;
}
break;
case Type::InstrumentTrackSettings:
if ( extension == "xpf" || extension == "xml" )
{
return true;
}
break;
case Type::MidiClip:
if (extension == "xpt" || extension == "xptz")
{
return true;
}
break;
case Type::Unknown:
if (! ( extension == "mmp" || extension == "mpt" || extension == "mmpz" ||
extension == "xpf" || extension == "xml" ||
( extension == "xiz" && ! getPluginFactory()->pluginSupportingExtension(extension).isNull()) ||
extension == "sf2" || extension == "sf3" || extension == "pat" || extension == "mid" ||
extension == "dll"
#ifdef LMMS_BUILD_LINUX
|| extension == "so"
#endif
#ifdef LMMS_HAVE_LV2
|| extension == "lv2"
#endif
) )
{
return true;
}
if( extension == "wav" || extension == "ogg" || extension == "ds"
#ifdef LMMS_HAVE_SNDFILE_MP3
|| extension == "mp3"
#endif
)
{
return true;
}
break;
default:
return false;
}
return false;
}
QString DataFile::nameWithExtension( const QString & _fn ) const
{
const QString extension = _fn.section( '.', -1 );
switch( type() )
{
case Type::SongProject:
if( extension != "mmp" &&
extension != "mpt" &&
extension != "mmpz" )
{
if( ConfigManager::inst()->value( "app",
"nommpz" ).toInt() == 0 )
{
return _fn + ".mmpz";
}
return _fn + ".mmp";
}
break;
case Type::SongProjectTemplate:
if( extension != "mpt" )
{
return _fn + ".mpt";
}
break;
case Type::InstrumentTrackSettings:
if( extension != "xpf" )
{
return _fn + ".xpf";
}
break;
default: ;
}
return _fn;
}
void DataFile::write( QTextStream & _strm )
{
if( type() == Type::SongProject || type() == Type::SongProjectTemplate
|| type() == Type::InstrumentTrackSettings )
{
cleanMetaNodes( documentElement() );
}
save(_strm, 2);
}
bool DataFile::writeFile(const QString& filename, bool withResources)
{
// Small lambda function for displaying errors
auto showError = [](QString title, QString body){
if (gui::getGUI() != nullptr)
{
QMessageBox mb;
mb.setWindowTitle(title);
mb.setText(body);
mb.setIcon(QMessageBox::Warning);
mb.setStandardButtons(QMessageBox::Ok);
mb.exec();
}
else
{
qWarning() << body;
}
};
// If we are saving without resources, filename is just the file we are
// saving to. If we are saving with resources (project bundle), filename
// will be used (discarding extensions) to create a folder where the
// bundle will be saved in
QFileInfo fInfo(filename);
const QString bundleDir = fInfo.path() + "/" + fInfo.fileName().section('.', 0, 0);
const QString resourcesDir = bundleDir + "/resources";
const QString fullName = withResources
? nameWithExtension(bundleDir + "/" + fInfo.fileName())
: nameWithExtension(filename);
const QString fullNameTemp = fullName + ".new";
const QString fullNameBak = fullName + ".bak";
using gui::SongEditor;
// If we are saving with resources, setup the bundle folder first
if (withResources)
{
// First check if there's a bundle folder with the same name in
// the path already. If so, warns user that we can't overwrite a
// project bundle.
if (QDir(bundleDir).exists())
{
showError(SongEditor::tr("Operation denied"),
SongEditor::tr("A bundle folder with that name already eists on the "
"selected path. Can't overwrite a project bundle. Please select a different "
"name."));
return false;
}
// Create bundle folder
if (!QDir().mkdir(bundleDir))
{
showError(SongEditor::tr("Error"),
SongEditor::tr("Couldn't create bundle folder."));
return false;
}
// Create resources folder
if (!QDir().mkdir(resourcesDir))
{
showError(SongEditor::tr("Error"),
SongEditor::tr("Couldn't create resources folder."));
return false;
}
// Copy resources to folder and update paths
if (!copyResources(resourcesDir))
{
showError(SongEditor::tr("Error"),
SongEditor::tr("Failed to copy resources."));
return false;
}
}
QSaveFile outfile(fullNameTemp);
if (!outfile.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
showError(SongEditor::tr("Could not write file"),
SongEditor::tr("Could not open %1 for writing. You probably are not permitted to "
"write to this file. Please make sure you have write-access to "
"the file and try again.").arg(fullName));
return false;
}
const QString extension = fullName.section('.', -1);
if (extension == "mmpz" || extension == "xptz")
{
QString xml;
QTextStream ts( &xml );
write( ts );
outfile.write( qCompress( xml.toUtf8() ) );
}
else
{
QTextStream ts( &outfile );
write( ts );
}
if (!outfile.commit())
{
showError(SongEditor::tr("Could not write file"),
SongEditor::tr("An unknown error has occured and the file could not be saved."));
return false;
}
if (ConfigManager::inst()->value("app", "disablebackup").toInt())
{
// remove current file
QFile::remove(fullName);
}
else
{
// remove old backup file
QFile::remove(fullNameBak);
// move current file to backup file
QFile::rename(fullName, fullNameBak);
}
// move temporary file to current file
QFile::rename(fullNameTemp, fullName);
return true;
}
bool DataFile::copyResources(const QString& resourcesDir)
{
// List of filenames used so we can append a counter to any
// repeating filenames
std::list<QString> namesList;
auto it = ELEMENTS_WITH_RESOURCES.begin();
// Copy resources and manipulate the DataFile to have local paths to them
while (it != ELEMENTS_WITH_RESOURCES.end())
{
QDomNodeList list = elementsByTagName(it->first);
// Go through all elements with the tagname from our map
for (int i = 0; !list.item(i).isNull(); ++i)
{
QDomElement el = list.item(i).toElement();
auto res = it->second.begin();
// Search for attributes that point to resources
while (res != it->second.end())
{
// If the element has that attribute
if (el.hasAttribute(*res))
{
// Get absolute path to resource
bool error;
QString resPath = PathUtil::toAbsolute(el.attribute(*res), &error);
// If we are running without the project loaded (from CLI), "local:" base
// prefixes aren't converted, so we need to convert it ourselves
if (error)
{
resPath = QFileInfo(m_fileName).path() + "/" + resPath.remove(0,
PathUtil::basePrefix(PathUtil::Base::LocalDir).length());
}
// Check if we need to add a counter to the filename
QString finalFileName = QFileInfo(resPath).fileName();
QString extension = resPath.section('.', -1);
int repeatedNames = 0;
for (QString name : namesList)
{
if (finalFileName == name)
{
++repeatedNames;
}
}
// Add the name to the list before modifying it
namesList.push_back(finalFileName);
if (repeatedNames)
{
// Remove the extension, add the counter and add the
// extension again to get the final file name
finalFileName.truncate(finalFileName.lastIndexOf('.'));
finalFileName = finalFileName + "-" + QString::number(repeatedNames) + "." + extension;
}
// Final path is our resources dir + the new file name
QString finalPath = resourcesDir + "/" + finalFileName;
// Copy resource file to the resources folder
if(!QFile::copy(resPath, finalPath))
{
qWarning("ERROR: Failed to copy resource");
return false;
}
// Update attribute path to point to the bundle file
QString newAtt = PathUtil::basePrefix(PathUtil::Base::LocalDir) + "resources/" + finalFileName;
el.setAttribute(*res, newAtt);
}
++res;
}
}
++it;
}
return true;
}
/**
* @brief This recursive method will go through all XML nodes of the DataFile
* and check whether any of them have local paths. If they are not on
* our list of elements that can have local paths we return true,
* indicating that we potentially have plugins with local paths that
* would be a security issue. The Song class can then abort loading
* this project.
* @param parent The parent node being iterated. When called
* without arguments, this will be an empty element that will be
* ignored (since the second parameter will be true).
* @param firstCall Defaults to true, and indicates to this recursive
* method whether this is the first call. If it is it will use the
* root element as the parent.
*/
bool DataFile::hasLocalPlugins(QDomElement parent /* = QDomElement()*/, bool firstCall /* = true*/) const
{
// If this is the first iteration of the recursion we use the root element
if (firstCall) { parent = documentElement(); }
auto children = parent.childNodes();
for (int i = 0; i < children.size(); ++i)
{
QDomNode child = children.at(i);
QDomElement childElement = child.toElement();
bool skipNode = false;
// Skip the nodes allowed to have "local:" attributes, but
// still check its children
for (const auto& element : ELEMENTS_WITH_RESOURCES)
{
if (childElement.tagName() == element.first)
{
skipNode = true;
break;
}
}
// Check if they have "local:" attribute (unless they are allowed to
// and skipNode is true)
if (!skipNode)
{
auto attributes = childElement.attributes();
for (int i = 0; i < attributes.size(); ++i)
{
QDomNode attribute = attributes.item(i);
QDomAttr attr = attribute.toAttr();
if (attr.value().startsWith(PathUtil::basePrefix(PathUtil::Base::LocalDir),
Qt::CaseInsensitive))
{
return true;
}
}
}
// Now we check the children of this node (recursively)
// and if any return true we return true.
if (hasLocalPlugins(childElement, false))
{
return true;
}
}
// If we got here none of the nodes had the "local:" path.
return false;
}
DataFile::Type DataFile::type( const QString& typeName )
{
const auto it = std::find_if(s_types.begin(), s_types.end(),
[&typeName](const TypeDescStruct& type) { return type.m_name == typeName; });
if (it != s_types.end()) { return it->m_type; }
// compat code
if( typeName == "channelsettings" )
{
return Type::InstrumentTrackSettings;
}
return Type::Unknown;
}
QString DataFile::typeName( Type type )
{
return s_types[static_cast<std::size_t>(type)].m_name;
}
void DataFile::cleanMetaNodes( QDomElement _de )
{
QDomNode node = _de.firstChild();
while( !node.isNull() )
{
if( node.isElement() )
{
if( node.toElement().attribute( "metadata" ).toInt() )
{
QDomNode ns = node.nextSibling();
_de.removeChild( node );
node = ns;
continue;
}
if( node.hasChildNodes() )
{
cleanMetaNodes( node.toElement() );
}
}
node = node.nextSibling();
}
}
void DataFile::upgrade_0_2_1_20070501()
{
// Upgrade to version 0.2.1-20070501
QDomNodeList list = elementsByTagName( "arpandchords" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
if( el.hasAttribute( "arpdir" ) )
{
int arpdir = el.attribute( "arpdir" ).toInt();
if( arpdir > 0 )
{
el.setAttribute( "arpdir", arpdir - 1 );
}
else
{
el.setAttribute( "arpdisabled", "1" );
}
}
}
list = elementsByTagName( "sampletrack" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
if( el.attribute( "vol" ) != "" )
{
el.setAttribute( "vol", LocaleHelper::toFloat(
el.attribute( "vol" ) ) * 100.0f );
}
else
{
QDomNode node = el.namedItem(
"automation-pattern" );
if( !node.isElement() ||
!node.namedItem( "vol" ).isElement() )
{
el.setAttribute( "vol", 100.0f );
}
}
}
list = elementsByTagName( "ladspacontrols" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
QDomNode anode = el.namedItem( "automation-pattern" );
QDomNode node = anode.firstChild();
while( !node.isNull() )
{
if( node.isElement() )
{
QString name = node.nodeName();
if( name.endsWith( "link" ) )
{
el.setAttribute( name,
node.namedItem( "time" )
.toElement()
.attribute( "value" ) );
QDomNode oldNode = node;
node = node.nextSibling();
anode.removeChild( oldNode );
continue;
}
}
node = node.nextSibling();
}
}
QDomNode node = m_head.firstChild();
while( !node.isNull() )
{
if( node.isElement() )
{
if( node.nodeName() == "bpm" )
{
int value = node.toElement().attribute(
"value" ).toInt();
if( value > 0 )
{
m_head.setAttribute( "bpm",
value );
QDomNode oldNode = node;
node = node.nextSibling();
m_head.removeChild( oldNode );
continue;
}
}
else if( node.nodeName() == "mastervol" )
{
int value = node.toElement().attribute(
"value" ).toInt();
if( value > 0 )
{
m_head.setAttribute(
"mastervol", value );
QDomNode oldNode = node;
node = node.nextSibling();
m_head.removeChild( oldNode );
continue;
}
}
else if( node.nodeName() == "masterpitch" )
{
m_head.setAttribute( "masterpitch",
-node.toElement().attribute(
"value" ).toInt() );
QDomNode oldNode = node;
node = node.nextSibling();
m_head.removeChild( oldNode );
continue;
}
}
node = node.nextSibling();
}
}
void DataFile::upgrade_0_2_1_20070508()
{
// Upgrade to version 0.2.1-20070508 from some version greater than or equal to 0.2.1-20070501
QDomNodeList list = elementsByTagName( "arpandchords" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
if( el.hasAttribute( "chorddisabled" ) )
{
el.setAttribute( "chord-enabled",
!el.attribute( "chorddisabled" )
.toInt() );
el.setAttribute( "arp-enabled",
!el.attribute( "arpdisabled" )
.toInt() );
}
else if( !el.hasAttribute( "chord-enabled" ) )
{
el.setAttribute( "chord-enabled", true );
el.setAttribute( "arp-enabled",
el.attribute( "arpdir" ).toInt() != 0 );
}
}
while( !( list = elementsByTagName( "channeltrack" ) ).isEmpty() )
{
QDomElement el = list.item( 0 ).toElement();
el.setTagName( "instrumenttrack" );
}
list = elementsByTagName( "instrumenttrack" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
if( el.hasAttribute( "vol" ) )
{
float value = LocaleHelper::toFloat( el.attribute( "vol" ) );
value = roundf( value * 0.585786438f );
el.setAttribute( "vol", value );
}
else
{
QDomNodeList vol_list = el.namedItem(
"automation-pattern" )
.namedItem( "vol" ).toElement()
.elementsByTagName( "time" );
for( int j = 0; !vol_list.item( j ).isNull();
++j )
{
QDomElement timeEl = list.item( j )
.toElement();
int value = timeEl.attribute( "value" )
.toInt();
value = (int)roundf( value *
0.585786438f );
timeEl.setAttribute( "value", value );
}
}
}
}
void DataFile::upgrade_0_3_0_rc2()
{
// Upgrade to version 0.3.0-rc2 from some version greater than or equal to 0.2.1-20070508
QDomNodeList list = elementsByTagName( "arpandchords" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
if( el.attribute( "arpdir" ).toInt() > 0 )
{
el.setAttribute( "arpdir",
el.attribute( "arpdir" ).toInt() - 1 );
}
}
}
void DataFile::upgrade_0_3_0()
{
// Upgrade to version 0.3.0 (final) from some version greater than or equal to 0.3.0-rc2
QDomNodeList list;
while( !( list = elementsByTagName(
"pluckedstringsynth" ) ).isEmpty() )
{
QDomElement el = list.item( 0 ).toElement();
el.setTagName( "vibedstrings" );
el.setAttribute( "active0", 1 );
}
while( !( list = elementsByTagName( "lb303" ) ).isEmpty() )
{
QDomElement el = list.item( 0 ).toElement();
el.setTagName( "lb302" );
}
while( !( list = elementsByTagName( "channelsettings" ) ).
isEmpty() )
{
QDomElement el = list.item( 0 ).toElement();
el.setTagName( "instrumenttracksettings" );
}
}
void DataFile::upgrade_0_4_0_20080104()
{
// Upgrade to version 0.4.0-20080104 from some version greater than or equal to 0.3.0 (final)
QDomNodeList list = elementsByTagName( "fx" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
if( el.hasAttribute( "fxdisabled" ) &&
el.attribute( "fxdisabled" ).toInt() == 0 )
{
el.setAttribute( "enabled", 1 );
}
}
}
void DataFile::upgrade_0_4_0_20080118()
{
// Upgrade to version 0.4.0-20080118 from some version greater than or equal to 0.4.0-20080104
QDomNodeList list;
while( !( list = elementsByTagName( "fx" ) ).isEmpty() )
{
QDomElement fxchain = list.item( 0 ).toElement();
fxchain.setTagName( "fxchain" );
QDomNode rack = list.item( 0 ).firstChild();
QDomNodeList effects = rack.childNodes();
// move items one level up
while( effects.count() )
{
fxchain.appendChild( effects.at( 0 ) );
}
fxchain.setAttribute( "numofeffects",
rack.toElement().attribute( "numofeffects" ) );
fxchain.removeChild( rack );
}
}
void DataFile::upgrade_0_4_0_20080129()
{
// Upgrade to version 0.4.0-20080129 from some version greater than or equal to 0.4.0-20080118
QDomNodeList list;
while( !( list =
elementsByTagName( "arpandchords" ) ).isEmpty() )
{
QDomElement aac = list.item( 0 ).toElement();
aac.setTagName( "arpeggiator" );
QDomNode cloned = aac.cloneNode();
cloned.toElement().setTagName( "chordcreator" );
aac.parentNode().appendChild( cloned );
}
}
void DataFile::upgrade_0_4_0_20080409()
{
// Upgrade to version 0.4.0-20080409 from some version greater than or equal to 0.4.0-20080129
QStringList s;
s << "note" << "pattern" << "bbtco" << "sampletco" << "time";
for( QStringList::iterator it = s.begin(); it < s.end(); ++it )
{
QDomNodeList list = elementsByTagName( *it );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
el.setAttribute( "pos",
el.attribute( "pos" ).toInt()*3 );
el.setAttribute( "len",
el.attribute( "len" ).toInt()*3 );
}
}
QDomNodeList list = elementsByTagName( "timeline" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
el.setAttribute( "lp0pos",
el.attribute( "lp0pos" ).toInt()*3 );
el.setAttribute( "lp1pos",
el.attribute( "lp1pos" ).toInt()*3 );
}
}
void DataFile::upgrade_0_4_0_20080607()
{
// Upgrade to version 0.4.0-20080607 from some version greater than or equal to 0.3.0-20080409
QDomNodeList list;
while( !( list = elementsByTagName( "midi" ) ).isEmpty() )
{
QDomElement el = list.item( 0 ).toElement();
el.setTagName( "midiport" );
}
}
void DataFile::upgrade_0_4_0_20080622()
{
// Upgrade to version 0.4.0-20080622 from some version greater than or equal to 0.3.0-20080607
QDomNodeList list;
while( !( list = elementsByTagName(
"automation-pattern" ) ).isEmpty() )
{
QDomElement el = list.item( 0 ).toElement();
el.setTagName( "automationpattern" );
}
list = elementsByTagName( "bbtrack" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
QString s = el.attribute( "name" );
s.replace(QRegularExpression("^Beat/Baseline "), "Beat/Bassline");
el.setAttribute( "name", s );
}
}
void DataFile::upgrade_0_4_0_beta1()
{
// Upgrade to version 0.4.0-beta1 from some version greater than or equal to 0.4.0-20080622
// convert binary effect-key-blobs to XML
QDomNodeList list;
list = elementsByTagName( "effect" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
QString k = el.attribute( "key" );
if( !k.isEmpty() )
{
const QList<QVariant> l =
base64::decode( k, QVariant::List ).toList();
if( !l.isEmpty() )
{
QString name = l[0].toString();
QVariant u = l[1];
EffectKey::AttributeMap m;
// VST-effect?
if( u.type() == QVariant::String )
{
m["file"] = u.toString();
}
// LADSPA-effect?
else if( u.type() == QVariant::StringList )
{
const QStringList sl = u.toStringList();
m["plugin"] = sl.value( 0 );
m["file"] = sl.value( 1 );
}
EffectKey key( nullptr, name, m );
el.appendChild( key.saveXML( *this ) );
}
}
}
}
void DataFile::upgrade_0_4_0_rc2()
{
// Upgrade to version 0.4.0-rc2 from some version greater than or equal to 0.4.0-beta1
QDomNodeList list = elementsByTagName( "audiofileprocessor" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
QString s = el.attribute( "src" );
s.replace( "drumsynth/misc ", "drumsynth/misc_" );
s.replace( "drumsynth/r&b", "drumsynth/r_n_b" );
s.replace( "drumsynth/r_b", "drumsynth/r_n_b" );
el.setAttribute( "src", s );
}
list = elementsByTagName( "lb302" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
int s = el.attribute( "shape" ).toInt();
if( s >= 1 )
{
s--;
}
el.setAttribute( "shape", QString("%1").arg(s) );
}
}
void DataFile::upgrade_1_0_99()
{
jo_id_t last_assigned_id = 0;
QList<jo_id_t> idList;
findIds(documentElement(), idList);
QDomNodeList list = elementsByTagName("ladspacontrols");
for(int i = 0; !list.item(i).isNull(); ++i)
{
for(QDomNode node = list.item(i).firstChild(); !node.isNull();
node = node.nextSibling())
{
QDomElement el = node.toElement();
QDomNode data_child = el.namedItem("data");
if(!data_child.isElement())
{
if (el.attribute("scale_type") == "log")
{
QDomElement me = createElement("data");
me.setAttribute("value", el.attribute("data"));
me.setAttribute("scale_type", "log");
jo_id_t id;
for(id = last_assigned_id + 1;
idList.contains(id); id++)
{
}
last_assigned_id = id;
idList.append(id);
me.setAttribute("id", id);
el.appendChild(me);
}
}
}
}
}
void DataFile::upgrade_1_1_0()
{
QDomNodeList list = elementsByTagName("fxchannel");
for (int i = 1; !list.item(i).isNull(); ++i)
{
QDomElement el = list.item(i).toElement();
QDomElement send = createElement("send");
send.setAttribute("channel", "0");
send.setAttribute("amount", "1");
el.appendChild(send);
}
}
void DataFile::upgrade_1_1_91()
{
// Upgrade to version 1.1.91 from some version less than 1.1.91
QDomNodeList list = elementsByTagName( "audiofileprocessor" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
QString s = el.attribute( "src" );
s.replace(QRegularExpression("/samples/bassloopes/"), "/samples/bassloops/");
el.setAttribute( "src", s );
}
list = elementsByTagName( "attribute" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
if( el.attribute( "name" ) == "plugin" && el.attribute( "value" ) == "vocoder-lmms" ) {
el.setAttribute( "value", "vocoder" );
}
}
list = elementsByTagName( "crossoevereqcontrols" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
// invert the mute LEDs
for( int j = 1; j <= 4; ++j ){
QString a = QString( "mute%1" ).arg( j );
el.setAttribute( a, ( el.attribute( a ) == "0" ) ? "1" : "0" );
}
}
list = elementsByTagName( "arpeggiator" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
// Swap elements ArpDirRandom and ArpDirDownAndUp
if( el.attribute( "arpdir" ) == "3" )
{
el.setAttribute( "arpdir", "4" );
}
else if( el.attribute( "arpdir" ) == "4" )
{
el.setAttribute( "arpdir", "3" );
}
}
}
static void upgradeElement_1_2_0_rc2_42( QDomElement & el )
{
if( el.hasAttribute( "syncmode" ) )
{
int syncmode = el.attribute( "syncmode" ).toInt();
QStringList names;
QDomNamedNodeMap atts = el.attributes();
for( uint i = 0; i < atts.length(); i++ )
{
QString name = atts.item( i ).nodeName();
if( name.endsWith( "_numerator" ) )
{
names << name.remove( "_numerator" )
+ "_syncmode";
}
}
for( QStringList::iterator it = names.begin(); it < names.end();
++it )
{
el.setAttribute( *it, syncmode );
}
}
QDomElement child = el.firstChildElement();
while ( !child.isNull() )
{
upgradeElement_1_2_0_rc2_42( child );
child = child.nextSiblingElement();
}
}
void DataFile::upgrade_1_2_0_rc3()
{
// Upgrade from earlier bbtrack beat note behaviour of adding
// steps if a note is placed after the last step.
QDomNodeList bbtracks = elementsByTagName( "bbtrack" );
for( int i = 0; !bbtracks.item( i ).isNull(); ++i )
{
QDomNodeList patterns = bbtracks.item( i
).toElement().elementsByTagName(
"pattern" );
for( int j = 0; !patterns.item( j ).isNull(); ++j )
{
QDomElement el = patterns.item( j ).toElement();
if( el.attribute( "len" ) != "" )
{
int patternLength = el.attribute( "len" ).toInt();
int steps = patternLength / 12;
el.setAttribute( "steps", steps );
}
}
}
// DataFile::upgrade_1_2_0_rc2_42
QDomElement el = firstChildElement();
while ( !el.isNull() )
{
upgradeElement_1_2_0_rc2_42( el );
el = el.nextSiblingElement();
}
}
/**
* Helper function to call a functor for all effect ports' DomElements,
* providing the functor with lists to add and remove DomElements. Helpful for
* patching port values from savefiles.
*/
template<class Ftor>
void iterate_ladspa_ports(QDomElement& effect, Ftor& ftor)
{
// Head back up the DOM to upgrade ports
QDomNodeList ladspacontrols = effect.elementsByTagName( "ladspacontrols" );
for( int m = 0; !ladspacontrols.item( m ).isNull(); ++m )
{
QList<QDomElement> addList, removeList;
QDomElement ladspacontrol = ladspacontrols.item( m ).toElement();
for( QDomElement port = ladspacontrol.firstChild().toElement();
!port.isNull(); port = port.nextSibling().toElement() )
{
QStringList parts = port.tagName().split("port");
// Not a "port"
if ( parts.size() < 2 )
{
continue;
}
int num = parts[1].toInt();
// From Qt's docs of QDomNode:
// * copying a QDomNode is OK, they still have the same
// pointer to the "internal" QDomNodePrivate.
// * Also, they are using linked lists, which means
// deleting or appending QDomNode does not invalidate
// any other pointers.
// => Inside ftor, you can (and should) push back the
// QDomElements by value, not references
// => The loops below for adding and removing don't
// invalidate any other QDomElements
ftor(port, num, addList, removeList);
}
// Add ports marked for adding
for ( QDomElement e : addList )
{
ladspacontrol.appendChild( e );
}
// Remove ports marked for removal
for ( QDomElement e : removeList )
{
ladspacontrol.removeChild( e );
}
}
}
// helper function if you need to print a QDomNode
QDebug operator<<(QDebug dbg, const QDomNode& node)
{
QString s;
QTextStream str(&s, QIODevice::WriteOnly);
node.save(str, 2);
dbg << qPrintable(s);
return dbg;
}
void DataFile::upgrade_1_3_0()
{
QDomNodeList list = elementsByTagName( "instrument" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement el = list.item( i ).toElement();
if( el.attribute( "name" ) == "papu" )
{
el.setAttribute( "name", "freeboy" );
QDomNodeList children = el.elementsByTagName( "papu" );
for( int j = 0; !children.item( j ).isNull(); ++j )
{
QDomElement child = children.item( j ).toElement();
child.setTagName( "freeboy" );
}
}
else if( el.attribute( "name" ) == "OPL2" )
{
el.setAttribute( "name", "opulenz" );
QDomNodeList children = el.elementsByTagName( "OPL2" );
for( int j = 0; !children.item( j ).isNull(); ++j )
{
QDomElement child = children.item( j ).toElement();
child.setTagName( "opulenz" );
}
}
}
list = elementsByTagName( "effect" );
for( int i = 0; !list.item( i ).isNull(); ++i )
{
QDomElement effect = list.item( i ).toElement();
if( effect.attribute( "name" ) == "ladspaeffect" )
{
QDomNodeList keys = effect.elementsByTagName( "key" );
for( int j = 0; !keys.item( j ).isNull(); ++j )
{
QDomElement key = keys.item( j ).toElement();
QDomNodeList attributes = key.elementsByTagName( "attribute" );
for( int k = 0; !attributes.item( k ).isNull(); ++k )
{
// Effect name changes
QDomElement attribute = attributes.item( k ).toElement();
const QString attrName = attribute.attribute("name");
const QString attrVal = attribute.attribute("value");
const QString plugin = attrName == "plugin" ? attrVal : "";
static const std::map<QString, QString> pluginNames = {
{"Sidechaincompressor", "SidechainCompressor"},
{"Sidechaingate", "SidechainGate"},
{"Multibandcompressor", "MultibandCompressor"},
{"Multibandgate", "MultibandGate"},
{"Multibandlimiter", "MultibandLimiter"},
};
if (attrName == "file" && (attrVal == "calf" || attrVal == "calf.so" ))
{
attribute.setAttribute( "value", "veal" );
}
const auto newName = pluginNames.find(plugin);
if (newName != pluginNames.end())
{
attribute.setAttribute("value", newName->second);
}
// Handle port changes
if (plugin == "MultibandLimiter" || plugin == "MultibandCompressor" || plugin == "MultibandGate")
{
auto fn = [&](QDomElement& port, int num, QList<QDomElement>&, QList<QDomElement>& removeList)
{
// Mark ports for removal
if ( num >= 18 && num <= 23 )
{
removeList << port;
}
// Bump higher ports up 6 positions
else if ( num >= 24 )
{
// port01...port010, etc
QString name( "port0" );
name.append( QString::number( num -6 ) );
port.setTagName( name );
}
};
iterate_ladspa_ports(effect, fn);
}
else if (plugin == "Pulsator")
{
auto fn = [&](QDomElement& port, int num, QList<QDomElement>& addList, QList<QDomElement>& removeList)
{
switch(num)
{
case 16:
{
// old freq is now at port 25
QDomElement portCopy = createElement("port025");
portCopy.setAttribute("data", port.attribute("data"));
addList << portCopy;
// remove old freq port
removeList << port;
// set the "timing" port to choose port23+2=port25 (timing in Hz)
QDomElement timing = createElement("port022");
timing.setAttribute("data", 2);
addList << timing;
break;
}
// port 18 (modulation) => 17
case 17:
port.setTagName("port016");
break;
case 18:
{
// leave port 18 (offsetr), but add port 17 (offsetl)
QDomElement offsetl = createElement("port017");
offsetl.setAttribute("data", 0.0f);
addList << offsetl;
// additional: bash port 21 to 1
QDomElement pulsewidth = createElement("port021");
pulsewidth.setAttribute("data", 1.0f);
addList << pulsewidth;
break;
}
}
};
iterate_ladspa_ports(effect, fn);
}
else if (plugin == "VintageDelay")
{
auto fn = [&](QDomElement& port, int num, QList<QDomElement>& addList, QList<QDomElement>& )
{
switch(num)
{
case 4:
{
// BPM is now port028
port.setTagName("port028");
// bash timing to BPM
QDomElement timing = createElement("port027");
timing.setAttribute("data", 0);
addList << timing;
// port 5 and 6 (in, out gain) need to be bashed to 1:
QDomElement input = createElement("port05");
input.setAttribute("data", 1.0f);
addList << input;
QDomElement output = createElement("port06");
output.setAttribute("data", 1.0f);
addList << output;
break;
}
default:
// all other ports increase by 10
QString name( "port0" );
name.append( QString::number( num + 10 ) );
port.setTagName( name );
}
};
iterate_ladspa_ports(effect, fn);
}
else if (plugin == "Equalizer5Band" || plugin == "Equalizer8Band" || plugin == "Equalizer12Band")
{
// NBand equalizers got 4 q nobs inserted. We need to shift everything else...
// HOWEVER: 5 band eq has only 2 q nobs inserted (no LS/HS filters)
bool band5 = plugin == "Equalizer5Band";
auto fn = [&](QDomElement& port, int num, QList<QDomElement>& addList, QList<QDomElement>& )
{
if(num == 4)
{
// don't modify port 4, but some other ones:
int zoom_port = 0;
if (plugin == "Equalizer5Band")
zoom_port = 36;
else if (plugin == "Equalizer8Band")
zoom_port = 48;
else // 12 band
zoom_port = 64;
// bash zoom to 0.25
QString name( "port0" );
name.append( QString::number( zoom_port ) );
QDomElement timing = createElement(name);
timing.setAttribute("data", 0.25f);
addList << timing;
}
// the following code could be refactored, but I did careful code-reading
// to prevent copy-paste-errors
if(num == 18)
{
// 18 => 19
port.setTagName("port019");
// insert port 18 (q)
QDomElement q = createElement("port018");
q.setAttribute("data", 0.707f);
addList << q;
}
else if(num >= 19 && num <= 20)
{
// num += 1
QString name( "port0" );
name.append( QString::number( num + 1 ) );
port.setTagName( name );
}
else if(num == 21)
{
// 21 => 23
port.setTagName("port023");
// insert port 22 (q)
QDomElement q = createElement("port022");
q.setAttribute("data", 0.707f);
addList << q;
}
else if(num >= 22 && (num <= 23 || band5))
{
// num += 2
QString name( "port0" );
name.append( QString::number( num + 2 ) );
port.setTagName( name );
}
else if(num == 24 && !band5)
{
// 24 => 27
port.setTagName("port027");
// insert port 26 (q)
QDomElement q = createElement("port026");
q.setAttribute("data", 0.707f);
addList << q;
}
else if(num >= 25 && num <= 26 && !band5)
{
// num += 3
QString name( "port0" );
name.append( QString::number( num + 3 ) );
port.setTagName( name );
}
else if(num == 27 && !band5)
{
// 27 => 31
port.setTagName("port031");
// insert port 30 (q)
QDomElement q = createElement("port030");
q.setAttribute("data", 0.707f);
addList << q;
}
else if(num >= 28 && !band5)
{
// num += 4
QString name( "port0" );
name.append( QString::number( num + 4 ) );
port.setTagName( name );
}
};
iterate_ladspa_ports(effect, fn);
}
else if (plugin == "Saturator")
{
auto fn = [&](QDomElement& port, int num, QList<QDomElement>&, QList<QDomElement>& )
{
// These ports have been shifted a bit weird...
if( num == 7 )
{
port.setTagName("port015");
}
else if(num == 12)
{
port.setTagName("port016");
}
else if(num == 13)
{
port.setTagName("port017");
}
else if ( num >= 15 )
{
QString name( "port0" );
name.append( QString::number( num + 3 ) );
port.setTagName( name );
}
};
iterate_ladspa_ports(effect, fn);
}
else if (plugin == "StereoTools")
{
auto fn = [&](QDomElement& port, int num, QList<QDomElement>&, QList<QDomElement>& )
{
// This effect can not be back-ported due to bugs in the old version,
// or due to different behaviour. We thus port all parameters we can,
// and bash all new parameters (in this case, s.level and m.level) to
// their new defaults (both 1.0f in this case)
if( num == 23 || num == 25 )
{
port.setAttribute("data", 1.0f);
}
};
iterate_ladspa_ports(effect, fn);
}
else if (plugin == "amPitchshift")
{
auto fn = [&](QDomElement& port, int num, QList<QDomElement>&, QList<QDomElement>& removeList)
{
switch (num)
{
case 0:
port.setTagName("port01");
break;
case 1:
port.setTagName("port03");
break;
case 10:
port.setTagName("port11");
break;
case 11:
port.setTagName("port13");
break;
}
};
iterate_ladspa_ports(effect, fn);
}
}
}
}
}
}
void DataFile::upgrade_noHiddenClipNames()
{
QDomNodeList tracks = elementsByTagName("track");
auto clearDefaultNames = [](QDomNodeList clips, QString trackName)
{
for (int j = 0; j < clips.size(); ++j)
{
QDomElement clip = clips.item(j).toElement();
QString clipName = clip.attribute("name", "");
if (clipName == trackName) { clip.setAttribute("name", ""); }
}
};
for (int i = 0; i < tracks.size(); ++i)
{
QDomElement track = tracks.item(i).toElement();
QString trackName = track.attribute("name", "");
QDomNodeList instClips = track.elementsByTagName("pattern");
QDomNodeList autoClips = track.elementsByTagName("automationpattern");
QDomNodeList bbClips = track.elementsByTagName("bbtco");
clearDefaultNames(instClips, trackName);
clearDefaultNames(autoClips, trackName);
clearDefaultNames(bbClips, trackName);
}
}
void DataFile::upgrade_automationNodes()
{
QDomNodeList autoPatterns = elementsByTagName("automationpattern");
// Go through all automation patterns
for (int i = 0; i < autoPatterns.size(); ++i)
{
QDomElement autoPattern = autoPatterns.item(i).toElement();
// On each automation pattern, get all <time> elements
QDomNodeList times = autoPattern.elementsByTagName("time");
// Loop through all <time> elements and change what we need
for (int j=0; j < times.size(); ++j)
{
QDomElement el = times.item(j).toElement();
float value = LocaleHelper::toFloat(el.attribute("value"));
// inValue will be equal to "value" and outValue will
// be set to the same
el.setAttribute("outValue", value);
}
}
}
// Convert the negative length notes to StepNotes
void DataFile::upgrade_noteTypes()
{
const auto notes = elementsByTagName("note");
for (int i = 0; i < notes.size(); ++i)
{
auto note = notes.item(i).toElement();
const auto noteSize = note.attribute("len").toInt();
if (noteSize < 0)
{
note.setAttribute("len", DefaultTicksPerBar / 16);
note.setAttribute("type", static_cast<int>(Note::Type::Step));
}
}
}
void DataFile::upgrade_fixCMTDelays()
{
static const QMap<QString, QString> nameMap {
{ "delay_0,01s", "delay_0.01s" },
{ "delay_0,1s", "delay_0.1s" },
{ "fbdelay_0,01s", "fbdelay_0.01s" },
{ "fbdelay_0,1s", "fbdelay_0.1s" }
};
const auto effects = elementsByTagName("effect");
for (int i = 0; i < effects.size(); ++i)
{
auto effect = effects.item(i).toElement();
// We are only interested in LADSPA plugins
if (effect.attribute("name") != "ladspaeffect") { continue; }
// Fetch all attributes (LMMS) beneath the LADSPA effect so that we can check the value of the plugin attribute (XML)
auto attributes = effect.elementsByTagName("attribute");
for (int j = 0; j < attributes.size(); ++j)
{
auto attribute = attributes.item(j).toElement();
if (attribute.attribute("name") == "plugin")
{
const auto attributeValue = attribute.attribute("value");
const auto it = nameMap.constFind(attributeValue);
if (it != nameMap.constEnd())
{
attribute.setAttribute("value", *it);
}
}
}
}
}
/** \brief Note range has been extended to match MIDI specification
*
* The non-standard note range previously affected all MIDI-based instruments
* except OpulenZ, and made them sound an octave lower than they should (#1857).
*/
void DataFile::upgrade_extendedNoteRange()
{
auto root = documentElement();
UpgradeExtendedNoteRange upgradeExtendedNoteRange(root);
upgradeExtendedNoteRange.upgrade();
}
/** \brief TripleOscillator switched to using high-quality, alias-free oscillators by default
*
* Older projects were made without this feature and would sound differently if loaded
* with the new default setting. This upgrade routine preserves their old behavior.
*/
void DataFile::upgrade_defaultTripleOscillatorHQ()
{
QDomNodeList tripleoscillators = elementsByTagName("tripleoscillator");
for (int i = 0; !tripleoscillators.item(i).isNull(); i++)
{
for (int j = 1; j <= 3; j++)
{
// Only set the attribute if it does not exist (default template has it but reports as 1.2.0)
if (tripleoscillators.item(i).toElement().attribute("useWaveTable" + QString::number(j)) == "")
{
tripleoscillators.item(i).toElement().setAttribute("useWaveTable" + QString::number(j), 0);
}
}
}
}
// Remove FX prefix from mixer and related nodes
void DataFile::upgrade_mixerRename()
{
// Change nodename <fxmixer> to <mixer>
QDomNodeList fxmixer = elementsByTagName("fxmixer");
for (int i = 0; i < fxmixer.length(); ++i)
{
auto item = fxmixer.item(i).toElement();
if (item.isNull())
{
continue;
}
item.setTagName("mixer");
}
// Change nodename <fxchannel> to <mixerchannel>
QDomNodeList fxchannel = elementsByTagName("fxchannel");
for (int i = 0; i < fxchannel.length(); ++i)
{
auto item = fxchannel.item(i).toElement();
if (item.isNull())
{
continue;
}
item.setTagName("mixerchannel");
}
// Change the attribute fxch of element <instrumenttrack> to mixch
QDomNodeList fxch = elementsByTagName("instrumenttrack");
for (int i = 0; i < fxch.length(); ++i)
{
auto item = fxch.item(i).toElement();
if (item.isNull())
{
continue;
}
if (item.hasAttribute("fxch"))
{
item.setAttribute("mixch", item.attribute("fxch"));
item.removeAttribute("fxch");
}
}
// Change the attribute fxch of element <sampletrack> to mixch
fxch = elementsByTagName("sampletrack");
for (int i = 0; i < fxch.length(); ++i)
{
auto item = fxch.item(i).toElement();
if (item.isNull())
{
continue;
}
if (item.hasAttribute("fxch"))
{
item.setAttribute("mixch", item.attribute("fxch"));
item.removeAttribute("fxch");
}
}
}
// Rename BB to pattern and TCO to clip
void DataFile::upgrade_bbTcoRename()
{
std::vector<std::pair<const char *, const char *>> names {
{"automationpattern", "automationclip"},
{"bbtco", "patternclip"},
{"pattern", "midiclip"},
{"sampletco", "sampleclip"},
{"bbtrack", "patterntrack"},
{"bbtrackcontainer", "patternstore"},
};
// Replace names of XML tags
for (auto name : names)
{
QDomNodeList elements = elementsByTagName(name.first);
for (int i = 0; !elements.item(i).isNull(); ++i)
{
elements.item(i).toElement().setTagName(name.second);
}
}
// Replace "Beat/Bassline" with "Pattern" in track names
QDomNodeList elements = elementsByTagName("track");
for (int i = 0; !elements.item(i).isNull(); ++i)
{
auto e = elements.item(i).toElement();
static_assert(Track::Type::Pattern == static_cast<Track::Type>(1), "Must be type=1 for backwards compatibility");
if (static_cast<Track::Type>(e.attribute("type").toInt()) == Track::Type::Pattern)
{
e.setAttribute("name", e.attribute("name").replace("Beat/Bassline", "Pattern"));
}
}
}
// Set LFO speed to 0.01 on projects made before sample-and-hold PR
void DataFile::upgrade_sampleAndHold()
{
QDomNodeList elements = elementsByTagName("lfocontroller");
for (int i = 0; i < elements.length(); ++i)
{
if (elements.item(i).isNull()) { continue; }
auto e = elements.item(i).toElement();
// Correct old random wave LFO speeds
if (e.attribute("wave").toInt() == 6)
{
e.setAttribute("speed", 0.01f);
}
}
}
// Change loops' filenames in <sampleclip>s
void DataFile::upgrade_loopsRename()
{
auto createEntry = [](const QString& originalName, const QString& bpm, const QString& extension = "ogg")
{
const QString replacement = originalName + " - " + bpm + " BPM." + extension;
return std::pair{originalName + "." + extension, replacement};
};
static const QMap<QString, QString> namesToNamesWithBPMsMap {
{ createEntry("bassloops/briff01", "140") },
{ createEntry("bassloops/rave_bass01", "180") },
{ createEntry("bassloops/rave_bass02", "180") },
{ createEntry("bassloops/tb303_01", "123") },
{ createEntry("bassloops/techno_bass01", "140") },
{ createEntry("bassloops/techno_bass02", "140") },
{ createEntry("bassloops/techno_synth01", "140") },
{ createEntry("bassloops/techno_synth02", "140") },
{ createEntry("bassloops/techno_synth03", "130") },
{ createEntry("bassloops/techno_synth04", "140") },
{ createEntry("beats/909beat01", "122") },
{ createEntry("beats/break01", "168") },
{ createEntry("beats/break02", "141") },
{ createEntry("beats/break03", "168") },
{ createEntry("beats/electro_beat01", "120") },
{ createEntry("beats/electro_beat02", "119") },
{ createEntry("beats/house_loop01", "142") },
{ createEntry("beats/jungle01", "168") },
{ createEntry("beats/rave_hihat01", "180") },
{ createEntry("beats/rave_hihat02", "180") },
{ createEntry("beats/rave_kick01", "180") },
{ createEntry("beats/rave_kick02", "180") },
{ createEntry("beats/rave_snare01", "180") },
{ createEntry("latin/latin_brass01", "140") },
{ createEntry("latin/latin_guitar01", "126") },
{ createEntry("latin/latin_guitar02", "140") },
{ createEntry("latin/latin_guitar03", "120") }
};
// Replace loop sample names
for (const auto& [elem, srcAttrs] : ELEMENTS_WITH_RESOURCES)
{
auto elements = elementsByTagName(elem);
for (const auto& srcAttr : srcAttrs)
{
for (int i = 0; i < elements.length(); ++i)
{
auto item = elements.item(i).toElement();
if (item.isNull() || !item.hasAttribute(srcAttr)) { continue; }
const QString srcVal = item.attribute(srcAttr);
const auto it = namesToNamesWithBPMsMap.constFind(srcVal);
if (it != namesToNamesWithBPMsMap.constEnd())
{
item.setAttribute(srcAttr, *it);
}
}
}
}
}
//! Update MIDI CC indexes, so that they are counted from 0. Older releases of LMMS
//! count the CCs from 1.
void DataFile::upgrade_midiCCIndexing()
{
static constexpr std::array attributesToUpdate{"inputcontroller", "outputcontroller"};
QDomNodeList elements = elementsByTagName("Midicontroller");
for(int i = 0; i < elements.length(); i++)
{
if (elements.item(i).isNull()) { continue; }
auto element = elements.item(i).toElement();
for (const char* attrName : attributesToUpdate)
{
if (element.hasAttribute(attrName))
{
int cc = element.attribute(attrName).toInt();
element.setAttribute(attrName, cc - 1);
}
}
}
}
void DataFile::upgrade()
{
// Runs all necessary upgrade methods
std::size_t max = std::min(static_cast<std::size_t>(m_fileVersion), UPGRADE_METHODS.size());
std::for_each( UPGRADE_METHODS.begin() + max, UPGRADE_METHODS.end(),
[this](UpgradeMethod um)
{
(this->*um)();
}
);
// Bump the file version (which should be the size of the upgrade methods vector)
m_fileVersion = UPGRADE_METHODS.size();
// update document meta data
documentElement().setAttribute( "version", m_fileVersion );
documentElement().setAttribute( "type", typeName( type() ) );
documentElement().setAttribute( "creator", "LMMS" );
documentElement().setAttribute( "creatorversion", LMMS_VERSION );
if( type() == Type::SongProject || type() == Type::SongProjectTemplate )
{
// Time-signature
if ( !m_head.hasAttribute( "timesig_numerator" ) )
{
m_head.setAttribute( "timesig_numerator", 4 );
m_head.setAttribute( "timesig_denominator", 4 );
}
if( !m_head.hasAttribute( "mastervol" ) )
{
m_head.setAttribute( "mastervol", 100 );
}
}
}
void DataFile::loadData( const QByteArray & _data, const QString & _sourceFile )
{
QString errorMsg;
int line = -1, col = -1;
if( !setContent( _data, &errorMsg, &line, &col ) )
{
// parsing failed? then try to uncompress data
QByteArray uncompressed = qUncompress( _data );
if( !uncompressed.isEmpty() )
{
if( setContent( uncompressed, &errorMsg, &line, &col ) )
{
line = col = -1;
}
}
if( line >= 0 && col >= 0 )
{
using gui::SongEditor;
qWarning() << "at line" << line << "column" << errorMsg;
if (gui::getGUI() != nullptr)
{
QMessageBox::critical( nullptr,
SongEditor::tr( "Error in file" ),
SongEditor::tr( "The file %1 seems to contain "
"errors and therefore can't be "
"loaded." ).
arg( _sourceFile ) );
}
return;
}
}
QDomElement root = documentElement();
m_type = type( root.attribute( "type" ) );
m_head = root.elementsByTagName( "head" ).item( 0 ).toElement();
if (!root.hasAttribute("version") || root.attribute("version")=="1.0")
{
// The file versioning is now a unsigned int, not maj.min, so we use
// legacyFileVersion() to retrieve the appropriate version
m_fileVersion = legacyFileVersion();
}
else
{
bool success;
m_fileVersion = root.attribute( "version" ).toUInt( &success );
if( !success ) qWarning("File Version conversion failure.");
}
if (root.hasAttribute("creatorversion"))
{
using gui::SongEditor;
// compareType defaults to All, so it doesn't have to be set here
ProjectVersion createdWith = root.attribute("creatorversion");
ProjectVersion openedWith = LMMS_VERSION;
if (createdWith.setCompareType(ProjectVersion::CompareType::Minor)
!= openedWith.setCompareType(ProjectVersion::CompareType::Minor)
&& gui::getGUI() != nullptr && root.attribute("type") == "song"
){
auto projectType = _sourceFile.endsWith(".mpt") ?
SongEditor::tr("template") : SongEditor::tr("project");
gui::TextFloat::displayMessage(
SongEditor::tr("Version difference"),
SongEditor::tr("This %1 was created with LMMS %2")
.arg(projectType).arg(createdWith.getVersion()),
embed::getIconPixmap("whatsthis", 24, 24),
2500
);
}
}
// Perform upgrade routines
if (m_fileVersion < UPGRADE_METHODS.size()) { upgrade(); }
m_content = root.elementsByTagName(typeName(m_type)).item(0).toElement();
}
void findIds(const QDomElement& elem, QList<jo_id_t>& idList)
{
if(elem.hasAttribute("id"))
{
idList.append(elem.attribute("id").toInt());
}
QDomElement child = elem.firstChildElement();
while(!child.isNull())
{
findIds(child, idList);
child = child.nextSiblingElement();
}
}
unsigned int DataFile::legacyFileVersion()
{
// Version of LMMs that created this project
ProjectVersion creator =
documentElement().attribute( "creatorversion" ).
replace( "svn", "" );
// Get an iterator pointing at the first upgrade we need to run (or at the end if there is no such upgrade)
auto firstRequiredUpgrade = std::upper_bound( UPGRADE_VERSIONS.begin(), UPGRADE_VERSIONS.end(), creator );
// Convert the iterator to an index, which is our file version (starting at 0)
return std::distance( UPGRADE_VERSIONS.begin(), firstRequiredUpgrade );
}
} // namespace lmms