From ef0a06629a9d4652b3a91d85af768e7e5797fe2a Mon Sep 17 00:00:00 2001 From: "Tim E. Real" Date: Mon, 17 Sep 2012 07:49:10 +0000 Subject: Introducing Copy On Write for waves. See ChangeLog. --- muse2/ChangeLog | 6 ++ muse2/muse/wave.cpp | 64 ++++++++++++ muse2/muse/wave.h | 8 +- muse2/muse/waveedit/wavecanvas.cpp | 169 +++++++++++++++++++++++++++---- muse2/muse/waveedit/wavecanvas.h | 2 +- muse2/muse/widgets/CMakeLists.txt | 3 + muse2/muse/widgets/copy_on_write.cpp | 39 +++++++ muse2/muse/widgets/copy_on_write.h | 42 ++++++++ muse2/muse/widgets/copy_on_write_base.ui | 102 +++++++++++++++++++ muse2/muse/widgets/utils.cpp | 33 ++++++ muse2/muse/widgets/utils.h | 4 +- 11 files changed, 451 insertions(+), 21 deletions(-) create mode 100644 muse2/muse/widgets/copy_on_write.cpp create mode 100644 muse2/muse/widgets/copy_on_write.h create mode 100644 muse2/muse/widgets/copy_on_write_base.ui diff --git a/muse2/ChangeLog b/muse2/ChangeLog index 6177d2c0..8c2a1751 100644 --- a/muse2/ChangeLog +++ b/muse2/ChangeLog @@ -1,3 +1,9 @@ +17.09.2012: + * Introducing: Copy On Write for waves. (Tim) + It asks to copy, to the Project Directory, any wave to be modified which is either + not writable or is shared with another independent (non-clone) wave event. + TODO: Improve by asking even for writable waves outside the Project Directory + (add a 'copy' checkbox column to the dialog, settable only under certain conditions). 16.09.2012: - Fixed allocation error with drag&drop of plugins (rj) - Fixed close menu alternative in Instrument editor (rj) diff --git a/muse2/muse/wave.cpp b/muse2/muse/wave.cpp index 0657bbe0..c2969f6d 100644 --- a/muse2/muse/wave.cpp +++ b/muse2/muse/wave.cpp @@ -42,6 +42,8 @@ #include "audio.h" ///#include "sig.h" #include "al/sig.h" +#include "part.h" +#include "track.h" //#define WAVE_DEBUG //#define WAVE_DEBUG_PRC @@ -393,11 +395,21 @@ QString SndFile::path() const return finfo->filePath(); } +QString SndFile::canonicalPath() const + { + return finfo->canonicalFilePath(); + } + QString SndFile::dirPath() const { return finfo->absolutePath(); } +QString SndFile::canonicalDirPath() const + { + return finfo->canonicalPath(); + } + QString SndFile::name() const { return finfo->fileName(); @@ -789,6 +801,58 @@ void SndFile::applyUndoFile(const QString& original, const QString& tmpfile, uns MusEGlobal::audio->msgIdle(false); } +//--------------------------------------------------------- +// checkCopyOnWrite +//--------------------------------------------------------- + +bool SndFile::checkCopyOnWrite() +{ + QString path_this = canonicalPath(); + if(path_this.isEmpty()) + return false; + + bool fwrite = finfo->isWritable(); + + // No exceptions: Even if this wave event is a clone, if it ain't writeable we gotta copy the wave. + if(!fwrite) + return true; + + // Count the number of non-clone part wave events (including possibly this one) using this file. + // Not much choice but to search all active wave events - the sndfile ref count is not the solution for this... + int use_count = 0; + WaveTrackList* wtl = MusEGlobal::song->waves(); + for(ciTrack it = wtl->begin(); it != wtl->end(); ++it) + { + PartList* pl = (*it)->parts(); + for(ciPart ip = pl->begin(); ip != pl->end(); ++ip) + { + EventList* el = ip->second->events(); + // We are looking for active independent non-clone parts + if(el->arefCount() > 1) + continue; + for(ciEvent ie = el->begin(); ie != el->end(); ++ie) + { + if(ie->second.type() != Wave) + continue; + const Event& ev = ie->second; + if(ev.empty()) + continue; + const SndFileR sf = ev.sndFile(); + QString path = sf.canonicalPath(); + if(path.isEmpty()) + continue; + if(path == path_this) + ++use_count; + // If more than one non-clone part wave event is using the file, signify that the caller should make a copy of it. + if(use_count > 1) + return true; + } + } + } + + return false; +} + // DELETETHIS 170 #if 0 //--------------------------------------------------------- diff --git a/muse2/muse/wave.h b/muse2/muse/wave.h index c9c7061d..d144d342 100644 --- a/muse2/muse/wave.h +++ b/muse2/muse/wave.h @@ -64,7 +64,7 @@ class SndFile { bool openFlag; bool writeFlag; size_t readInternal(int srcChannels, float** dst, size_t n, bool overwrite, float *buffer); - + protected: int refCount; @@ -86,10 +86,13 @@ class SndFile { bool isOpen() const { return openFlag; } bool isWritable() const { return writeFlag; } void update(); + bool checkCopyOnWrite(); //!< check if the file should be copied before writing to it QString basename() const; //!< filename without extension QString dirPath() const; //!< path + QString canonicalDirPath() const; //!< path, resolved (no symlinks or . .. etc) QString path() const; //!< path with filename + QString canonicalPath() const; //!< path with filename, resolved (no symlinks or . .. etc) QString name() const; //!< filename unsigned samples() const; @@ -143,10 +146,13 @@ class SndFileR { bool isOpen() const { return sf->isOpen(); } bool isWritable() const { return sf->isWritable(); } void update() { sf->update(); } + bool checkCopyOnWrite() { return sf->checkCopyOnWrite(); }; QString basename() const { return sf->basename(); } QString dirPath() const { return sf->dirPath(); } + QString canonicalDirPath() const { return sf->canonicalDirPath(); } QString path() const { return sf->path(); } + QString canonicalPath() const { return sf->canonicalPath(); } QString name() const { return sf->name(); } unsigned samples() const { return sf->samples(); } diff --git a/muse2/muse/waveedit/wavecanvas.cpp b/muse2/muse/waveedit/wavecanvas.cpp index 2139b647..8c1b0012 100644 --- a/muse2/muse/waveedit/wavecanvas.cpp +++ b/muse2/muse/waveedit/wavecanvas.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,7 @@ #include #include +#include "app.h" #include "xml.h" #include "wavecanvas.h" #include "event.h" @@ -64,6 +66,7 @@ #include "fastlog.h" #include "utils.h" #include "tools.h" +#include "copy_on_write.h" namespace MusEGui { @@ -1885,8 +1888,7 @@ void WaveCanvas::cmd(int cmd) // modifyWarnedYet = true; // if(QMessageBox::warning(this, QString("Muse"), // tr("Warning! Muse currently operates directly on the sound file.\n" - // "Undo is supported, but NOT after exit, WITH OR WITHOUT A SAVE!\n" - // "If you are stuck, try deleting the associated .wca file and reloading."), tr("&Ok"), tr("&Cancel"), + // "Undo is supported, but NOT after exit, WITH OR WITHOUT A SAVE!"), tr("&Ok"), tr("&Cancel"), // QString::null, 0, 1 ) != 0) // return; //} @@ -1947,7 +1949,7 @@ MusECore::WaveSelectionList WaveCanvas::getSelection(unsigned startpos, unsigned //printf("Event data affected: %d->%d filename:%s\n", sx, ex, file.name().toLatin1().constData()); MusECore::WaveEventSelection s; - s.file = file; + s.event = event; s.startframe = sx; s.endframe = ex+1; //printf("sx=%d ex=%d\n",sx,ex); @@ -1964,24 +1966,155 @@ MusECore::WaveSelectionList WaveCanvas::getSelection(unsigned startpos, unsigned //--------------------------------------------------------- void WaveCanvas::modifySelection(int operation, unsigned startpos, unsigned stoppos, double paramA) { - MusEGlobal::song->startUndo(); + if (operation == PASTE) { + // we need to redefine startpos and stoppos + if (copiedPart =="") + return; + MusECore::SndFile pasteFile(copiedPart); + pasteFile.openRead(); + startpos = pos[0]; + stoppos = startpos+ pasteFile.samples(); // possibly this is wrong if there are tempo changes + pasteFile.close(); + pos[0]=stoppos; + } - if (operation == PASTE) { - // we need to redefine startpos and stoppos - if (copiedPart =="") - return; - MusECore::SndFile pasteFile(copiedPart); - pasteFile.openRead(); - startpos = pos[0]; - stoppos = startpos+ pasteFile.samples(); // possibly this is wrong if there are tempo changes - pasteFile.close(); - pos[0]=stoppos; - } - - MusECore::WaveSelectionList selection = getSelection(startpos, stoppos); + // + // Copy on Write: Check if some files need to be copied, either because they are not + // writable, or more than one independent (non-clone) wave event shares a wave file. + // + + MusECore::WaveSelectionList selection = getSelection(startpos, stoppos); + std::vector copy_files_proj_dir; + for(MusECore::iWaveSelection i = selection.begin(); i != selection.end(); i++) + { + MusECore::WaveEventSelection w = *i; + MusECore::SndFileR file = w.event.sndFile(); + if(file.checkCopyOnWrite()) + { + std::vector::iterator i = copy_files_proj_dir.begin(); + for( ; i != copy_files_proj_dir.end(); ++i) + { + if(i->canonicalPath() == file.canonicalPath()) + break; + } + if(i == copy_files_proj_dir.end()) + copy_files_proj_dir.push_back(file); + } + } + if(!copy_files_proj_dir.empty()) + { + CopyOnWriteDialog* dlg = new CopyOnWriteDialog(); + for(std::vector::iterator i = copy_files_proj_dir.begin(); i != copy_files_proj_dir.end(); ++i) + { + qint64 sz = QFile(i->canonicalPath()).size(); + QString s; + if(sz > 1048576) + s += QString::number(sz / 1048576) + "MB "; + else + if(sz > 1024) + s += QString::number(sz / 1024) + "KB "; + else + s += QString::number(sz) + "B "; + s += i->canonicalPath(); + dlg->addProjDirFile(s); + } + int rv = dlg->exec(); + delete dlg; + if(rv != QDialog::Accepted) + return; + // Has a project been created yet? + if(MusEGlobal::museProject == MusEGlobal::museProjectInitPath) // && MusEGlobal::config.useProjectSaveDialog + { + // No project, we need to create one. + if(!MusEGlobal::muse->saveAs()) + return; // No project, don't want to copy without one. + //setFocus(); // For some reason focus is given away to Arranger + } + for(MusECore::iWaveSelection i = selection.begin(); i != selection.end(); i++) + { + MusECore::WaveEventSelection w = *i; + MusECore::SndFileR file = w.event.sndFile(); + if(!file.checkCopyOnWrite()) // Make sure to re-check + continue; + QString filePath = MusEGlobal::museProject + QString("/") + file.name(); + QString newFilePath; + if(MusECore::getUniqueFileName(filePath, newFilePath)) + { + { + QFile qf(file.canonicalPath()); + if(!qf.copy(newFilePath)) // Copy the file + { + printf("MusE Error: Could not copy to new sound file (file exists?): %s\n", newFilePath.toLatin1().constData()); + continue; // Let's not overwrite an existing file + } + } + QFile nqf(newFilePath); + // Need to make sure some permissions are set... + QFile::Permissions pm = nqf.permissions(); + if(!(pm & QFile::ReadOwner)) + { + pm |= QFile::ReadOwner; + if(!nqf.setPermissions(pm)) + { + printf("MusE Error: Could not set read owner permissions on new sound file: %s\n", newFilePath.toLatin1().constData()); + continue; + } + } + if(!(pm & QFile::WriteOwner)) + { + pm |= QFile::WriteOwner; + if(!nqf.setPermissions(pm)) + { + printf("MusE Error: Could not set write owner permissions on new sound file: %s\n", newFilePath.toLatin1().constData()); + continue; + } + } + if(!(pm & QFile::ReadUser)) + { + pm |= QFile::ReadUser; + if(!nqf.setPermissions(pm)) + { + printf("MusE Error: Could not set read user permissions on new sound file: %s\n", newFilePath.toLatin1().constData()); + continue; + } + } + if(!(pm & QFile::WriteUser)) + { + pm |= QFile::WriteUser; + if(!nqf.setPermissions(pm)) + { + printf("MusE Error: Could not set write user permissions on new sound file: %s\n", newFilePath.toLatin1().constData()); + continue; + } + } + MusECore::SndFile* newSF = new MusECore::SndFile(newFilePath); + MusECore::SndFileR newSFR(newSF); // Create a sndFileR for the new file + if(newSFR.openRead()) + { + printf("MusE Error: Could not open new sound file: %s\n", newSFR.canonicalPath().toLatin1().constData()); + continue; // newSF will be deleted when newSFR goes out of scope and is deleted + } + MusEGlobal::audio->msgIdle(true); + w.event.sndFile().close(); // Close the old file. + // NOTE: For now, don't bother providing undo for this. Reason: If the user undoes + // and then modifies again, it will prompt to create new copies each time. There is + // no mechanism ("touched"?) to tell if an existing copy would be suitable to just 'drop in'. + // It would help if we deleted the wave file copies upon undo, but not too crazy about that. + // So since the copy has already been created and "there it is", we might as well use it. + // It means that events and even undo items BEFORE this operation will point to this + // NEW wave file (as if they always did). It also means the user CANNOT change back + // to the old file... Oh well, this IS Copy On Write. + // FIXME: Find a conceptual way to make undo work with or without deleting the copies. + w.event.setSndFile(newSFR); // Set the new file. + MusEGlobal::audio->msgIdle(false); + } + } + } + + MusEGlobal::song->startUndo(); for (MusECore::iWaveSelection i = selection.begin(); i != selection.end(); i++) { MusECore::WaveEventSelection w = *i; - MusECore::SndFileR& file = w.file; + MusECore::SndFileR file = w.event.sndFile(); unsigned sx = w.startframe; unsigned ex = w.endframe; unsigned file_channels = file.channels(); diff --git a/muse2/muse/waveedit/wavecanvas.h b/muse2/muse/waveedit/wavecanvas.h index 4a6ae9f9..f9553845 100644 --- a/muse2/muse/waveedit/wavecanvas.h +++ b/muse2/muse/waveedit/wavecanvas.h @@ -46,7 +46,7 @@ class WavePart; class WaveTrack; struct WaveEventSelection { - SndFileR file; + Event event; unsigned startframe; unsigned endframe; }; diff --git a/muse2/muse/widgets/CMakeLists.txt b/muse2/muse/widgets/CMakeLists.txt index 88706339..0e9d369d 100644 --- a/muse2/muse/widgets/CMakeLists.txt +++ b/muse2/muse/widgets/CMakeLists.txt @@ -40,6 +40,7 @@ QT4_WRAP_CPP (widget_mocs comboQuant.h combobox.h comment.h + copy_on_write.h ctrlcombo.h custom_widget_actions.h dentry.h @@ -114,6 +115,7 @@ file (GLOB widgets_ui_files cliplisteditorbase.ui commentbase.ui configmidifilebase.ui + copy_on_write_base.ui didyouknow.ui editnotedialogbase.ui editsysexdialogbase.ui @@ -153,6 +155,7 @@ file (GLOB widgets_source_files comboQuant.cpp combobox.cpp comment.cpp + copy_on_write.cpp ctrlcombo.cpp custom_widget_actions.cpp dentry.cpp diff --git a/muse2/muse/widgets/copy_on_write.cpp b/muse2/muse/widgets/copy_on_write.cpp new file mode 100644 index 00000000..53e79a91 --- /dev/null +++ b/muse2/muse/widgets/copy_on_write.cpp @@ -0,0 +1,39 @@ +//========================================================= +// MusE +// Linux Music Editor +// copy_on_write.cpp +// (C) Copyright 2012 Tim E. Real (terminator356 on users dot sourceforge dot net) +// +// 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; 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; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +//========================================================= + +#include "copy_on_write.h" + +namespace MusEGui { + +CopyOnWriteDialog::CopyOnWriteDialog(QWidget* parent): QDialog(parent) +{ + setupUi(this); + +} + +void CopyOnWriteDialog::addProjDirFile(const QString& s) +{ + projDirList->addItem(s); +} + + +} // namespace MusEGui \ No newline at end of file diff --git a/muse2/muse/widgets/copy_on_write.h b/muse2/muse/widgets/copy_on_write.h new file mode 100644 index 00000000..f06274eb --- /dev/null +++ b/muse2/muse/widgets/copy_on_write.h @@ -0,0 +1,42 @@ +//========================================================= +// MusE +// Linux Music Editor +// copy_on_write.h +// (C) Copyright 2012 Tim E. Real (terminator356 on users dot sourceforge dot net) +// +// 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; 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; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +//========================================================= + +#ifndef __COPY_ON_WRITE_H__ +#define __COPY_ON_WRITE_H__ + +#include "ui_copy_on_write_base.h" + +namespace MusEGui { + +class CopyOnWriteDialog : public QDialog, public Ui::CopyOnWriteDialogBase +{ + Q_OBJECT + +public: + CopyOnWriteDialog(QWidget* parent = 0); + void addProjDirFile(const QString&); +}; + +} // namespace MusEGui + + +#endif diff --git a/muse2/muse/widgets/copy_on_write_base.ui b/muse2/muse/widgets/copy_on_write_base.ui new file mode 100644 index 00000000..d59b6325 --- /dev/null +++ b/muse2/muse/widgets/copy_on_write_base.ui @@ -0,0 +1,102 @@ + + + CopyOnWriteDialogBase + + + + 0 + 0 + 431 + 344 + + + + Copy Wave Files + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Some wave files will be copied to the Project Directory, +either because they are not writable or because more +than one independent Wave Event shares them. +Multiple copies will be made in some cases. + +If no Project has been created yet, you will be asked to, +giving another chance to cancel. + + + true + + + + + + + + + + These files will be copied to the Project Directory: + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + CopyOnWriteDialogBase + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + CopyOnWriteDialogBase + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/muse2/muse/widgets/utils.cpp b/muse2/muse/widgets/utils.cpp index e9fbc9e3..ab1d74fd 100644 --- a/muse2/muse/widgets/utils.cpp +++ b/muse2/muse/widgets/utils.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include "audio.h" #include "audiodev.h" @@ -566,4 +567,36 @@ int get_paste_len() return end_tick - begin_tick; } +//--------------------------------------------------------- +// getUniqueFileName +// Sets newAbsFilePath to origFilepath or a new version if found +// Return true if success +//--------------------------------------------------------- + +bool getUniqueFileName(const QString& origFilepath, QString& newAbsFilePath) + { + QFileInfo fi(origFilepath); + if(!fi.exists()) + { + newAbsFilePath = fi.absoluteFilePath(); + return true; + } + + QString pre = fi.absolutePath() + QString('/') + fi.baseName() + QString('_'); + QString post = QString('.') + fi.completeSuffix(); + for(int i = 1; i < 100000; ++i) + { + fi.setFile(pre + QString::number(i) + post); + if(!fi.exists()) + { + newAbsFilePath = fi.absoluteFilePath(); + return true; + } + } + + printf("Could not find a suitable filename (more than 100000 files based on %s - clean up!\n", origFilepath.toLatin1().constData()); + return false; + } + + } // namespace MusECore diff --git a/muse2/muse/widgets/utils.h b/muse2/muse/widgets/utils.h index 07899a84..a558e73c 100644 --- a/muse2/muse/widgets/utils.h +++ b/muse2/muse/widgets/utils.h @@ -58,7 +58,9 @@ extern QPainterPath roundedPath(int x, int y, int w, int h, int xrad, int yrad, extern QIcon colorRect(const QColor& color, int width, int height); extern int get_paste_len(); -} // namespace MusECores +extern bool getUniqueFileName(const QString& filename, QString& newAbsFilePath); + +} // namespace MusECore #endif -- cgit v1.2.3