mirror of
https://github.com/LMMS/lmms.git
synced 2026-05-17 11:18:30 -04:00
* Adds a baseDir for the local path
Adds a new Base for paths called "local:", which will translate to the dir where the currently opened project file is at. In the future this will allow us to make project bundles and make it easier to export and transfer projects to other people without breaking the paths to samples, presets, plugins and others.
* Starts implementing the makeBundle functionality
For now, to make a bundle LMMS has to be run through CLI with the makeBundle/--makeBundle command, followed by an input file and an output file ('lmms --makeBundle input.mmp output.mmp'). DataFile::writeBundle() is then called. For now, it only saves the mmp/mmpz file normally and also creates a "resources" folder if it doesn't exists. Later it will also manipulate the DataFile so all paths are local and copy all files to the resources folder.
TODO:
-Remove warnings.
-Implement the logic to manipulate the DataFile and copy files.
* Starts implementing logic to go through resources
Starts implementing the logic that will go through all the resources of the project file and add them to the bundle. We use a std::map of QString to std::vector<QString>: The first string is the DOM element tagname that is going to be searched for. The vector of strings holds all attributes this element can have that accesses resources. For now we just print those to the screen.
* Adds logic to copy files and update the project
The raw logic for creating the bundle is finished. It now copies the resource files and update the project to use "local:" paths to the new file now.
Now it's a matter of organizing things and adding safety checks for file operation errors basically.
* Makes the writeBundle method more organized
Improves comments and debugging warnings to make the writeBundle a bit more organized for review.
* Adds a project bundle folder
Adds a project bundle folder, inside which the bundles will be created. Instead of receiving an output project file name, the makeBundle command now receives a bundle name that will be used as the name of the bundle's folder.
Uses a typedef for the std::map with the tags and attributes with resources.
TODO:
- Fix the local: prefix so it works when we don't have the project file open (for CLI usage, or find another way to deal with it).
- Sanitize the bundle name.
- Allow overwriting bundles?
* Handles local paths when a project isn't open
The PathUtil base prefix conversion for "local:" uses the loaded song file name. When we are running the makebundle command from the CLI there isn't a loaded project, so those prefixes aren't converted properly. Now, when there isn't a project open PathUtil will return "local:" again when it tries to convert this base prefix. DataFile can then check if the base prefix is still there, and if it is it knows the conversion wasn't possible, so it does the conversion itself. To do that, a member called m_fileName was added to DataFile, which will hold the file being manipulated if that's where the DataFile originated from, and the local path can be retrieved from it.
* Sanitizes the bundle name
The bundle name is now sanitized. Since it's going to be used as a folder name, we need to keep the user from giving invalid folder names as a bundle's name. The rules for the name are:
1) It must start with a word character (either a digit or letter)
2) It can be followed by any number of letters, digits, whitespaces or hyphens
3) It must end with a word character (either a digit or letter)
A Regexp is used to check for the name validity.
* Moves away from projectbundle folder concept
This commit regresses some functionality. Project bundles will be saved just as any other project, except they will also have the resources folder. It will be up to the user to organize the bundles on their own folders. It's currently not allowed to save a bundle on a folder where there's one already though (if there's a resources folder already). Later it might be allowed to overwrite bundles in that case.
The projectbundles folder was dropped. The user can save project bundles anywhere in the system.
The DataFile::writeBundle was removed. It's functionality was merged into the DataFile::writeFile method, by adding a boolean on the parameters defining whether it should be saved with resources. The logic of copying the resource files and changing the paths inside the project DataFile was moved to DataFile::copyResources, making the methods a little bit less dense.
* Adds an option to save project as bundle
The "Save As" dialog now has an option to save project as a project bundle (with resources), which will save the file as a bundle.
Known bug:
- Because the "local:" base prefix is translated to the filename from the Engine::getSong(), it breaks when Song::guiSaveProjectAs is called, because that method changes the project name before saving. Urgent fix!
* Fix local: prefix saving bug
There was a bug where "local:" prefixes weren't resolved properly during saving because Song::guiSaveProjectAs() changed the project name to the destiny file name before saving. This resulted in the local paths using the destination file as a reference. Both Song::guiSaveProject() and Song::guiSaveProjectAs() were rewritten, and now they only rename the project after it's saved.
* Adds a warning message box
When the user tries to save a project bundle on a folder that already has a project bundle (contains a resources folder) a message box pops up telling the user it's not permitted and that another path should be chosen.
* Removes unused header
Forgot to remove <QRegExp> header when I removed the code that used it.
* Removes Vestige plugins bundling
For safety reasons, remove the possibility to bundle VSTs loaded
through vestige. Also runs a safety check during the project being
loaded (Song::loadProject) to check if either Vestige plugins or effect
plugins are using local paths, and abort the project load if so. That is
to avoid malicious code being run because of bad DLLs being shipped with
a project file.
* Extracts code from loadProject to another method
Extracts code that checks if a DataFile contains local paths to
plugins to another method inside DataFile.
* Removes debug warnings
Removes warnings previously used for debugging. Improves a
warning message on PathUtil.
* Fixes small bug with error logging
Fixes small bug, where a QMessageBox was being used to prompt an
error without checking if the gui is loaded first. Now we check for the
GUI and if we are in CLI mode we use a QTextStream instead.
* Saves the bundle in a newly created folder
Now a folder with the project name is created inside which the
bundle will be saved. This makes the process more convenient.
Some save errors that previously only triggered qWarnings now
trigger message boxes to warn the user of what happened (using a lambda
function that either shows message boxes or trigger qWarnings depending
whether a gui is present).
Makes it so saving a bundle doesn't change the loaded project
path, that way the user won't be able to accidentally "Save" over a
bundle which should not be done for now.
* Enhances the name conflict workaround
Now, instead of replacing the resource names with meaningless
numbers, the bundle save will just append a counter to the end of
filenames that have been repeated.
* Starts addressing Johannes review
* Adds makebundle action to bash completion file
Adds the bash completion code for the made bundle action.
* Improves safety check on project files
Now, instead of checking certain XML tags for local paths,
DataFile::hasLocalPlugin() will return true if ANY tag that isn't on the
RESOURCE_ELEMENTS list contains an attribute that starts with "local:".
The method is now recursive so it can go through all XML tags
during this check.
* Addresses Spekular change request
Uses basePrefix(Base::LocalDir) instead of "local:" on the
return of unresolved local paths.
* Makes hasLocalPlugins method const
* Replaces literal uses of "local:"
Instead of using "local:" we are now retrieving the base prefix
from PathUtil, so if we change the prefix on the future we don't need to
replace every mention to it as well.
* Fix some comments on the header and cpp file
* Changes variable on PathUtil to const
Changes the retrieved pointer to the song object to a const
pointer.
* Leave doxygen comment on CPP file only
There was 2 doxygen comments for the same method, on the header
and CPP file. The latter was kept since it goes into more details about
the functionality of the method.
* Fix doxygen comment @param
Fixes the doxygen comment from hasLocalPlugin().
* Remove assert statements
Some assert statements were being done wrong and are probably
even unnecessary for that piece of code, so they were removed.
* Skips local paths when looking for shortest path
PathUtil::toShortestRelative() was including the local paths on
the candidate paths, which could lead to a unallowed resource (i.e.:
vst plugin) to be assigned a local path even on a regular save.
The local paths are now skipped when looking for the shortest
relative path, since they should only be used by the bundle save on the
allowed resources.
* Address Spekular's review
Changes some of the PathUtil methods to allow a boolean pointer
to be used to return the status of the method, setting it to false if it
failed somewhere.
Also adds a parameter to toShortestRelative to either allow or
forbid local paths in the search for the shortest relative path.
* Replaces "ok" with "error"
1802 lines
47 KiB
C++
1802 lines
47 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 <math.h>
|
|
#include <map>
|
|
|
|
#include <QDebug>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QDir>
|
|
#include <QMessageBox>
|
|
|
|
#include "base64.h"
|
|
#include "ConfigManager.h"
|
|
#include "Effect.h"
|
|
#include "embed.h"
|
|
#include "GuiApplication.h"
|
|
#include "LocaleHelper.h"
|
|
#include "PluginFactory.h"
|
|
#include "ProjectVersion.h"
|
|
#include "SongEditor.h"
|
|
#include "TextFloat.h"
|
|
#include "PathUtil.h"
|
|
|
|
#include "lmmsversion.h"
|
|
|
|
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 = {
|
|
{ "sampletco", {"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
|
|
};
|
|
|
|
// 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"
|
|
};
|
|
|
|
DataFile::typeDescStruct
|
|
DataFile::s_types[DataFile::TypeCount] =
|
|
{
|
|
{ DataFile::UnknownType, "unknown" },
|
|
{ DataFile::SongProject, "song" },
|
|
{ DataFile::SongProjectTemplate, "songtemplate" },
|
|
{ DataFile::InstrumentTrackSettings, "instrumenttracksettings" },
|
|
{ DataFile::DragNDropData, "dnddata" },
|
|
{ DataFile::ClipboardData, "clipboard-data" },
|
|
{ DataFile::JournalData, "journaldata" },
|
|
{ DataFile::EffectSettings, "effectsettings" },
|
|
{ DataFile::NotePattern, "pattern" }
|
|
} ;
|
|
|
|
|
|
|
|
|
|
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 )
|
|
{
|
|
QMessageBox::critical( NULL,
|
|
SongEditor::tr( "Could not open file" ),
|
|
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>" );
|
|
}
|
|
|
|
|
|
|
|
|
|
DataFile::~DataFile()
|
|
{
|
|
}
|
|
|
|
|
|
|
|
|
|
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::NotePattern:
|
|
if (extension == "xpt" || extension == "xptz")
|
|
{
|
|
return true;
|
|
}
|
|
break;
|
|
case Type::UnknownType:
|
|
if (! ( extension == "mmp" || extension == "mpt" || extension == "mmpz" ||
|
|
extension == "xpf" || extension == "xml" ||
|
|
( extension == "xiz" && ! pluginFactory->pluginSupportingExtension(extension).isNull()) ||
|
|
extension == "sf2" || extension == "sf3" || extension == "pat" || extension == "mid" ||
|
|
extension == "dll"
|
|
#ifdef LMMS_HAVE_LV2
|
|
|| extension == "lv2"
|
|
#endif
|
|
) )
|
|
{
|
|
return true;
|
|
}
|
|
if( extension == "wav" || extension == "ogg" ||
|
|
extension == "ds" )
|
|
{
|
|
return true;
|
|
}
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
|
|
QString DataFile::nameWithExtension( const QString & _fn ) const
|
|
{
|
|
switch( type() )
|
|
{
|
|
case SongProject:
|
|
if( _fn.section( '.', -1 ) != "mmp" &&
|
|
_fn.section( '.', -1 ) != "mpt" &&
|
|
_fn.section( '.', -1 ) != "mmpz" )
|
|
{
|
|
if( ConfigManager::inst()->value( "app",
|
|
"nommpz" ).toInt() == 0 )
|
|
{
|
|
return _fn + ".mmpz";
|
|
}
|
|
return _fn + ".mmp";
|
|
}
|
|
break;
|
|
case SongProjectTemplate:
|
|
if( _fn.section( '.',-1 ) != "mpt" )
|
|
{
|
|
return _fn + ".mpt";
|
|
}
|
|
break;
|
|
case InstrumentTrackSettings:
|
|
if( _fn.section( '.', -1 ) != "xpf" )
|
|
{
|
|
return _fn + ".xpf";
|
|
}
|
|
break;
|
|
default: ;
|
|
}
|
|
return _fn;
|
|
}
|
|
|
|
|
|
|
|
|
|
void DataFile::write( QTextStream & _strm )
|
|
{
|
|
if( type() == SongProject || type() == SongProjectTemplate
|
|
|| type() == InstrumentTrackSettings )
|
|
{
|
|
cleanMetaNodes( documentElement() );
|
|
}
|
|
|
|
save(_strm, 2);
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DataFile::writeFile(const QString& filename, bool withResources)
|
|
{
|
|
// Small lambda function for displaying errors
|
|
auto showError = [this](QString title, QString body){
|
|
if (gui)
|
|
{
|
|
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";
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
QFile 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 );
|
|
}
|
|
|
|
outfile.close();
|
|
|
|
// make sure the file has been written correctly
|
|
if( QFileInfo( outfile.fileName() ).size() > 0 )
|
|
{
|
|
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;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DataFile::copyResources(const QString& resourcesDir)
|
|
{
|
|
// List of filenames used so we can append a counter to any
|
|
// repeating filenames
|
|
std::list<QString> namesList;
|
|
|
|
ResourcesMap::const_iterator 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();
|
|
|
|
std::vector<QString>::const_iterator 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
|
|
(
|
|
ResourcesMap::const_iterator it = ELEMENTS_WITH_RESOURCES.begin();
|
|
it != ELEMENTS_WITH_RESOURCES.end();
|
|
++it
|
|
)
|
|
{
|
|
if (childElement.tagName() == it->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 )
|
|
{
|
|
for( int i = 0; i < TypeCount; ++i )
|
|
{
|
|
if( s_types[i].m_name == typeName )
|
|
{
|
|
return static_cast<DataFile::Type>( i );
|
|
}
|
|
}
|
|
|
|
// compat code
|
|
if( typeName == "channelsettings" )
|
|
{
|
|
return DataFile::InstrumentTrackSettings;
|
|
}
|
|
|
|
return UnknownType;
|
|
}
|
|
|
|
|
|
|
|
|
|
QString DataFile::typeName( Type type )
|
|
{
|
|
if( type >= UnknownType && type < TypeCount )
|
|
{
|
|
return s_types[type].m_name;
|
|
}
|
|
|
|
return s_types[UnknownType].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( QRegExp( "^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( NULL, 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( QRegExp("/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 )
|
|
{
|
|
int patternLength, steps;
|
|
QDomElement el = patterns.item( j ).toElement();
|
|
if( el.attribute( "len" ) != "" )
|
|
{
|
|
patternLength = el.attribute( "len" ).toInt();
|
|
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();
|
|
if( attribute.attribute( "name" ) == "file" &&
|
|
( attribute.attribute( "value" ) == "calf" ||
|
|
attribute.attribute( "value" ) == "calf.so" ) )
|
|
{
|
|
attribute.setAttribute( "value", "veal" );
|
|
}
|
|
else if( attribute.attribute( "name" ) == "plugin" &&
|
|
attribute.attribute( "value" ) == "Sidechaincompressor" )
|
|
{
|
|
attribute.setAttribute( "value", "SidechainCompressor" );
|
|
}
|
|
else if( attribute.attribute( "name" ) == "plugin" &&
|
|
attribute.attribute( "value" ) == "Sidechaingate" )
|
|
{
|
|
attribute.setAttribute( "value", "SidechainGate" );
|
|
}
|
|
else if( attribute.attribute( "name" ) == "plugin" &&
|
|
attribute.attribute( "value" ) == "Multibandcompressor" )
|
|
{
|
|
attribute.setAttribute( "value", "MultibandCompressor" );
|
|
}
|
|
else if( attribute.attribute( "name" ) == "plugin" &&
|
|
attribute.attribute( "value" ) == "Multibandgate" )
|
|
{
|
|
attribute.setAttribute( "value", "MultibandGate" );
|
|
}
|
|
else if( attribute.attribute( "name" ) == "plugin" &&
|
|
attribute.attribute( "value" ) == "Multibandlimiter" )
|
|
{
|
|
attribute.setAttribute( "value", "MultibandLimiter" );
|
|
}
|
|
|
|
// Handle port changes
|
|
|
|
if( attribute.attribute( "name" ) == "plugin" &&
|
|
( attribute.attribute( "value" ) == "MultibandLimiter" ||
|
|
attribute.attribute( "value" ) == "MultibandCompressor" ||
|
|
attribute.attribute( "value" ) == "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);
|
|
}
|
|
|
|
if( attribute.attribute( "name" ) == "plugin" &&
|
|
( attribute.attribute( "value" ) == "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);
|
|
}
|
|
|
|
|
|
if( attribute.attribute( "name" ) == "plugin" &&
|
|
( attribute.attribute( "value" ) == "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);
|
|
}
|
|
|
|
if( attribute.attribute( "name" ) == "plugin" &&
|
|
( ( attribute.attribute( "value" ) == "Equalizer5Band" )
|
|
|| ( attribute.attribute( "value" ) == "Equalizer8Band" )
|
|
|| ( attribute.attribute( "value" ) == "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 = ( attribute.attribute( "value" ) == "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;
|
|
if(attribute.attribute( "value" ) == "Equalizer5Band")
|
|
zoom_port = 36;
|
|
else if(attribute.attribute( "value" ) == "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);
|
|
}
|
|
|
|
if( attribute.attribute( "name" ) == "plugin" &&
|
|
attribute.attribute( "value" ) == "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);
|
|
}
|
|
|
|
if( attribute.attribute( "name" ) == "plugin" &&
|
|
attribute.attribute( "value" ) == "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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void DataFile::upgrade()
|
|
{
|
|
// Runs all necessary upgrade methods
|
|
std::for_each( UPGRADE_METHODS.begin() + m_fileVersion, 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() == SongProject || 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 )
|
|
{
|
|
qWarning() << "at line" << line << "column" << errorMsg;
|
|
if( gui )
|
|
{
|
|
QMessageBox::critical( NULL,
|
|
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"))
|
|
{
|
|
// compareType defaults to All, so it doesn't have to be set here
|
|
ProjectVersion createdWith = root.attribute("creatorversion");
|
|
ProjectVersion openedWith = LMMS_VERSION;
|
|
|
|
if (createdWith < openedWith) { upgrade(); }
|
|
|
|
if (createdWith.setCompareType(ProjectVersion::Minor)
|
|
!= openedWith.setCompareType(ProjectVersion::Minor)
|
|
&& gui != nullptr && root.attribute("type") == "song"
|
|
){
|
|
auto projectType = _sourceFile.endsWith(".mpt") ?
|
|
SongEditor::tr("template") : SongEditor::tr("project");
|
|
|
|
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
|
|
);
|
|
}
|
|
}
|
|
|
|
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 );
|
|
}
|