mirror of
https://github.com/cookiengineer/audacity
synced 2025-05-02 16:49:41 +02:00
Reimplement importation of .aup3 file more simply...
... Breaking dependency of ProjectFileIO on on TimeTrack, and reusing the same functions that implement cross-document copy and paste of tracks. Also changing behavior, so that if a TimeTrack exists but another is imported, then the import quietly replaces the existing completely.
This commit is contained in:
parent
15313a27f7
commit
92e36332f3
@ -27,7 +27,6 @@ Paul Licameli split from AudacityProject.cpp
|
|||||||
#include "SampleBlock.h"
|
#include "SampleBlock.h"
|
||||||
#include "Tags.h"
|
#include "Tags.h"
|
||||||
#include "TempDirectory.h"
|
#include "TempDirectory.h"
|
||||||
#include "TimeTrack.h"
|
|
||||||
#include "ViewInfo.h"
|
#include "ViewInfo.h"
|
||||||
#include "WaveTrack.h"
|
#include "WaveTrack.h"
|
||||||
#include "widgets/AudacityMessageBox.h"
|
#include "widgets/AudacityMessageBox.h"
|
||||||
@ -1677,357 +1676,6 @@ bool ProjectFileIO::WriteDoc(const char *table,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Importing an AUP3 project into an AUP3 project is a bit different than
|
|
||||||
// normal importing since we need to copy data from one DB to the other
|
|
||||||
// while adjusting the sample block IDs to represent the newly assigned
|
|
||||||
// IDs.
|
|
||||||
bool ProjectFileIO::ImportProject(const FilePath &fileName)
|
|
||||||
{
|
|
||||||
// Get access to the current project file
|
|
||||||
auto db = DB();
|
|
||||||
|
|
||||||
bool success = false;
|
|
||||||
bool restore = true;
|
|
||||||
int rc = SQLITE_OK;
|
|
||||||
|
|
||||||
// Ensure the inbound database gets detached
|
|
||||||
auto detach = finally([&]
|
|
||||||
{
|
|
||||||
// If this DETACH fails, subsequent project imports will fail until Audacity
|
|
||||||
// is relaunched.
|
|
||||||
auto result = sqlite3_exec(db, "DETACH DATABASE inbound;", nullptr, nullptr, nullptr);
|
|
||||||
|
|
||||||
// Only capture the error if there wasn't a previous error
|
|
||||||
if (result != SQLITE_OK && (rc == SQLITE_DONE || rc == SQLITE_OK))
|
|
||||||
{
|
|
||||||
SetDBError(
|
|
||||||
XO("Failed to detach project during import")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attach the inbound project file
|
|
||||||
wxString sql;
|
|
||||||
sql.Printf("ATTACH DATABASE 'file:%s?immutable=1&mode=ro' AS inbound;", fileName);
|
|
||||||
|
|
||||||
rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
|
|
||||||
if (rc != SQLITE_OK)
|
|
||||||
{
|
|
||||||
SetDBError(
|
|
||||||
XO("Unable to attach %s project file").Format(fileName)
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need either the autosave or project docs from the inbound AUP3
|
|
||||||
wxMemoryBuffer buffer;
|
|
||||||
|
|
||||||
// Get the autosave doc, if any
|
|
||||||
if (!GetBlob("SELECT dict || doc FROM inbound.project WHERE id = 1;", buffer))
|
|
||||||
{
|
|
||||||
// Error already set
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't have an autosave doc, load the project doc instead
|
|
||||||
if (buffer.GetDataLen() == 0)
|
|
||||||
{
|
|
||||||
if (!GetBlob("SELECT dict || doc FROM inbound.autosave WHERE id = 1;", buffer))
|
|
||||||
{
|
|
||||||
// Error already set
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Missing both the autosave and project docs. This can happen if the
|
|
||||||
// system were to crash before the first autosave into a temporary file.
|
|
||||||
if (buffer.GetDataLen() == 0)
|
|
||||||
{
|
|
||||||
SetError(XO("Unable to load project or autosave documents"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wxString project;
|
|
||||||
|
|
||||||
project = ProjectSerializer::Decode(buffer);
|
|
||||||
if (project.size() == 0)
|
|
||||||
{
|
|
||||||
SetError(XO("Unable to decode project document"));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the project doc
|
|
||||||
wxStringInputStream in(project);
|
|
||||||
wxXmlDocument doc;
|
|
||||||
if (!doc.Load(in))
|
|
||||||
{
|
|
||||||
SetError(XO("Unable to parse the project document"));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the root ("project") node
|
|
||||||
wxXmlNode *root = doc.GetRoot();
|
|
||||||
if (!root->GetName().IsSameAs(wxT("project")))
|
|
||||||
{
|
|
||||||
SetError(XO("Missing root node in project document"));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Soft delete all non-essential attributes to prevent updating the active
|
|
||||||
// project. This takes advantage of the knowledge that when a project is
|
|
||||||
// parsed, unrecognized attributes are simply ignored.
|
|
||||||
//
|
|
||||||
// This is necessary because we don't want any of the active project settings
|
|
||||||
// to be modified by the inbound project.
|
|
||||||
for (wxXmlAttribute *attr = root->GetAttributes(); attr; attr = attr->GetNext())
|
|
||||||
{
|
|
||||||
wxString name = attr->GetName();
|
|
||||||
if (!name.IsSameAs(wxT("version")) && !name.IsSameAs(wxT("audacityversion")))
|
|
||||||
{
|
|
||||||
attr->SetName(name + wxT("_deleted"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively find and collect all waveblock nodes
|
|
||||||
std::vector<wxXmlNode *> blocknodes;
|
|
||||||
std::function<void(wxXmlNode *)> findblocks = [&](wxXmlNode *node)
|
|
||||||
{
|
|
||||||
while (node)
|
|
||||||
{
|
|
||||||
if (node->GetName().IsSameAs(wxT("waveblock")))
|
|
||||||
{
|
|
||||||
blocknodes.push_back(node);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
findblocks(node->GetChildren());
|
|
||||||
}
|
|
||||||
|
|
||||||
node = node->GetNext();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get access to the active tracklist
|
|
||||||
auto pProject = &mProject;
|
|
||||||
auto &tracklist = TrackList::Get(*pProject);
|
|
||||||
|
|
||||||
// Search for a timetrack and remove it if the project already has one
|
|
||||||
if (*tracklist.Any<TimeTrack>().begin())
|
|
||||||
{
|
|
||||||
// Find a timetrack and remove it if it exists
|
|
||||||
for (wxXmlNode *node = doc.GetRoot()->GetChildren(); node; node = node->GetNext())
|
|
||||||
{
|
|
||||||
if (node->GetName().IsSameAs(wxT("timetrack")))
|
|
||||||
{
|
|
||||||
AudacityMessageBox(
|
|
||||||
XO("The active project already has a time track and one was encountered in the project being imported, bypassing imported time track."),
|
|
||||||
XO("Project Import"),
|
|
||||||
wxOK | wxICON_EXCLAMATION | wxCENTRE,
|
|
||||||
&GetProjectFrame(*pProject));
|
|
||||||
|
|
||||||
root->RemoveChild(node);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all waveblocks in all wavetracks
|
|
||||||
for (wxXmlNode *node = doc.GetRoot()->GetChildren(); node; node = node->GetNext())
|
|
||||||
{
|
|
||||||
if (node->GetName().IsSameAs(wxT("wavetrack")))
|
|
||||||
{
|
|
||||||
findblocks(node->GetChildren());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// Cleanup...
|
|
||||||
sqlite3_stmt *stmt = nullptr;
|
|
||||||
auto cleanup = finally([&]
|
|
||||||
{
|
|
||||||
// Ensure the prepared statement gets cleaned up
|
|
||||||
if (stmt)
|
|
||||||
{
|
|
||||||
// Ignore the return code since it will have already been captured below.
|
|
||||||
sqlite3_finalize(stmt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prepare the statement to copy the sample block from the inbound project to the
|
|
||||||
// active project. All columns other than the blockid column get copied.
|
|
||||||
wxString columns(wxT("sampleformat, summin, summax, sumrms, summary256, summary64k, samples"));
|
|
||||||
sql.Printf("INSERT INTO main.sampleblocks (%s)"
|
|
||||||
" SELECT %s"
|
|
||||||
" FROM inbound.sampleblocks"
|
|
||||||
" WHERE blockid = ?;",
|
|
||||||
columns,
|
|
||||||
columns);
|
|
||||||
|
|
||||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
|
|
||||||
if (rc != SQLITE_OK)
|
|
||||||
{
|
|
||||||
SetDBError(
|
|
||||||
XO("Unable to prepare project file command:\n\n%s").Format(sql)
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* i18n-hint: This title appears on a dialog that indicates the progress
|
|
||||||
in doing something.*/
|
|
||||||
ProgressDialog progress(XO("Progress"), XO("Importing project"), pdlgHideStopButton);
|
|
||||||
ProgressResult result = ProgressResult::Success;
|
|
||||||
|
|
||||||
wxLongLong_t count = 0;
|
|
||||||
wxLongLong_t total = blocknodes.size();
|
|
||||||
|
|
||||||
rc = sqlite3_exec(db, "BEGIN;", nullptr, nullptr, nullptr);
|
|
||||||
if (rc != SQLITE_OK)
|
|
||||||
{
|
|
||||||
SetDBError(
|
|
||||||
XO("Unable to start a transaction during import")
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy all the sample blocks from the inbound project file into
|
|
||||||
// the active one, while remembering which were copied.
|
|
||||||
for (auto node : blocknodes)
|
|
||||||
{
|
|
||||||
// Find the blockid attribute...it should always be there
|
|
||||||
wxXmlAttribute *attr = node->GetAttributes();
|
|
||||||
while (attr && !attr->GetName().IsSameAs(wxT("blockid")))
|
|
||||||
{
|
|
||||||
attr = attr->GetNext();
|
|
||||||
}
|
|
||||||
wxASSERT(attr != nullptr);
|
|
||||||
|
|
||||||
// And get the blockid
|
|
||||||
SampleBlockID blockid;
|
|
||||||
attr->GetValue().ToLongLong(&blockid);
|
|
||||||
|
|
||||||
if ( blockid <= 0 )
|
|
||||||
// silent block
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Bind statement parameters
|
|
||||||
rc = sqlite3_bind_int64(stmt, 1, blockid);
|
|
||||||
if (rc != SQLITE_OK)
|
|
||||||
{
|
|
||||||
SetDBError(
|
|
||||||
XO("Failed to bind SQL parameter")
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process it
|
|
||||||
rc = sqlite3_step(stmt);
|
|
||||||
if (rc != SQLITE_DONE)
|
|
||||||
{
|
|
||||||
SetDBError(
|
|
||||||
XO("Failed to import sample block.\nThe following command failed:\n\n%s").Format(sql)
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the original blockid with the new one
|
|
||||||
attr->SetValue(wxString::Format(wxT("%lld"), sqlite3_last_insert_rowid(db)));
|
|
||||||
|
|
||||||
// Reset the statement for the next iteration
|
|
||||||
if (sqlite3_reset(stmt) != SQLITE_OK)
|
|
||||||
{
|
|
||||||
THROW_INCONSISTENCY_EXCEPTION;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remember that we copied this node in case the user cancels
|
|
||||||
result = progress.Update(++count, total);
|
|
||||||
if (result != ProgressResult::Success)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bail if the import was cancelled or failed. If the user stopped the
|
|
||||||
// import or it completed, then we continue on.
|
|
||||||
if (rc != SQLITE_DONE || result == ProgressResult::Cancelled || result == ProgressResult::Failed)
|
|
||||||
{
|
|
||||||
// If this fails (probably due to memory or disk space), the transaction will
|
|
||||||
// (presumably) stil be active, so further updates to the project file will
|
|
||||||
// fail as well. Not really much we can do about it except tell the user.
|
|
||||||
auto result = sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
||||||
|
|
||||||
// Only capture the error if there wasn't a previous error
|
|
||||||
if (result != SQLITE_OK && rc == SQLITE_DONE)
|
|
||||||
{
|
|
||||||
SetDBError(
|
|
||||||
XO("Failed to rollback transaction during import")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go ahead and commit now. If this fails (probably due to memory or disk space),
|
|
||||||
// the transaction will (presumably) stil be active, so further updates to the
|
|
||||||
// project file will fail as well. Not really much we can do about it except tell
|
|
||||||
// the user.
|
|
||||||
rc = sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);
|
|
||||||
if (rc != SQLITE_OK)
|
|
||||||
{
|
|
||||||
SetDBError(
|
|
||||||
XO("Unable to commit transaction during import")
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy over tags...likely to produce duplicates...needs work once used
|
|
||||||
rc = sqlite3_exec(db,
|
|
||||||
"INSERT INTO main.tags SELECT * FROM inbound.tags;",
|
|
||||||
nullptr,
|
|
||||||
nullptr,
|
|
||||||
nullptr);
|
|
||||||
if (rc != SQLITE_OK)
|
|
||||||
{
|
|
||||||
SetDBError(
|
|
||||||
XO("Failed to import tags")
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recreate the project doc with the revisions we've made above. If this fails
|
|
||||||
// it's probably due to memory.
|
|
||||||
wxStringOutputStream output;
|
|
||||||
if (!doc.Save(output))
|
|
||||||
{
|
|
||||||
SetError(
|
|
||||||
XO("Unable to recreate project information.")
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now load the document as normal
|
|
||||||
XMLFileReader xmlFile;
|
|
||||||
if (!xmlFile.ParseString(this, output.GetString()))
|
|
||||||
{
|
|
||||||
SetError(
|
|
||||||
XO("Unable to parse project information."), xmlFile.GetErrorStr()
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ProjectFileIO::LoadProject(const FilePath &fileName, bool ignoreAutosave)
|
bool ProjectFileIO::LoadProject(const FilePath &fileName, bool ignoreAutosave)
|
||||||
{
|
{
|
||||||
bool success = false;
|
bool success = false;
|
||||||
|
@ -92,7 +92,6 @@ public:
|
|||||||
bool CloseProject();
|
bool CloseProject();
|
||||||
bool ReopenProject();
|
bool ReopenProject();
|
||||||
|
|
||||||
bool ImportProject(const FilePath &fileName);
|
|
||||||
bool LoadProject(const FilePath &fileName, bool ignoreAutosave);
|
bool LoadProject(const FilePath &fileName, bool ignoreAutosave);
|
||||||
bool UpdateSaved(const TrackList *tracks = nullptr);
|
bool UpdateSaved(const TrackList *tracks = nullptr);
|
||||||
bool SaveProject(const FilePath &fileName, const TrackList *lastSaved);
|
bool SaveProject(const FilePath &fileName, const TrackList *lastSaved);
|
||||||
|
@ -1137,6 +1137,29 @@ ProjectFileManager::AddImportedTracks(const FilePath &fileName,
|
|||||||
// HandleResize();
|
// HandleResize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool ImportProject(AudacityProject &dest, const FilePath &fileName)
|
||||||
|
{
|
||||||
|
InvisibleTemporaryProject temp;
|
||||||
|
auto &project = temp.Project();
|
||||||
|
|
||||||
|
auto &projectFileIO = ProjectFileIO::Get(project);
|
||||||
|
if (!projectFileIO.LoadProject(fileName, false))
|
||||||
|
return false;
|
||||||
|
auto &srcTracks = TrackList::Get(project);
|
||||||
|
auto &destTracks = TrackList::Get(dest);
|
||||||
|
for (const Track *pTrack : srcTracks.Any()) {
|
||||||
|
auto destTrack = pTrack->PasteInto(dest);
|
||||||
|
Track::FinishCopy(pTrack, destTrack.get());
|
||||||
|
if (destTrack.use_count() == 1)
|
||||||
|
destTracks.Add(destTrack);
|
||||||
|
}
|
||||||
|
Tags::Get(dest).Merge(Tags::Get(project));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If pNewTrackList is passed in non-NULL, it gets filled with the pointers to NEW tracks.
|
// If pNewTrackList is passed in non-NULL, it gets filled with the pointers to NEW tracks.
|
||||||
bool ProjectFileManager::Import(
|
bool ProjectFileManager::Import(
|
||||||
const FilePath &fileName,
|
const FilePath &fileName,
|
||||||
@ -1151,7 +1174,7 @@ bool ProjectFileManager::Import(
|
|||||||
|
|
||||||
// Handle AUP3 ("project") files directly
|
// Handle AUP3 ("project") files directly
|
||||||
if (fileName.AfterLast('.').IsSameAs(wxT("aup3"), false)) {
|
if (fileName.AfterLast('.').IsSameAs(wxT("aup3"), false)) {
|
||||||
if (projectFileIO.ImportProject(fileName)) {
|
if (ImportProject(project, fileName)) {
|
||||||
auto &history = ProjectHistory::Get(project);
|
auto &history = ProjectHistory::Get(project);
|
||||||
|
|
||||||
// If the project was clean and temporary (not permanently saved), then set
|
// If the project was clean and temporary (not permanently saved), then set
|
||||||
|
@ -52,6 +52,11 @@ static ProjectFileIORegistry::Entry registerFactory{
|
|||||||
TimeTrack::TimeTrack(const ZoomInfo *zoomInfo):
|
TimeTrack::TimeTrack(const ZoomInfo *zoomInfo):
|
||||||
Track()
|
Track()
|
||||||
, mZoomInfo(zoomInfo)
|
, mZoomInfo(zoomInfo)
|
||||||
|
{
|
||||||
|
CleanState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeTrack::CleanState()
|
||||||
{
|
{
|
||||||
mEnvelope = std::make_unique<BoundedEnvelope>(true, TIMETRACK_MIN, TIMETRACK_MAX, 1.0);
|
mEnvelope = std::make_unique<BoundedEnvelope>(true, TIMETRACK_MIN, TIMETRACK_MAX, 1.0);
|
||||||
|
|
||||||
@ -144,7 +149,15 @@ Track::Holder TimeTrack::PasteInto( AudacityProject &project ) const
|
|||||||
pNewTrack = pTrack->SharedPointer<TimeTrack>();
|
pNewTrack = pTrack->SharedPointer<TimeTrack>();
|
||||||
else
|
else
|
||||||
pNewTrack = std::make_shared<TimeTrack>( &ViewInfo::Get( project ) );
|
pNewTrack = std::make_shared<TimeTrack>( &ViewInfo::Get( project ) );
|
||||||
|
|
||||||
|
// Should come here only for .aup3 import, not for paste (because the
|
||||||
|
// track is skipped in cut/copy commands)
|
||||||
|
// And for import we agree to replace the track contents completely
|
||||||
|
pNewTrack->CleanState();
|
||||||
|
pNewTrack->Init(*this);
|
||||||
pNewTrack->Paste(0.0, this);
|
pNewTrack->Paste(0.0, this);
|
||||||
|
pNewTrack->SetRangeLower(this->GetRangeLower());
|
||||||
|
pNewTrack->SetRangeUpper(this->GetRangeUpper());
|
||||||
return pNewTrack;
|
return pNewTrack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +95,8 @@ class TimeTrack final : public Track {
|
|||||||
Ruler &GetRuler() const { return *mRuler; }
|
Ruler &GetRuler() const { return *mRuler; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void CleanState();
|
||||||
|
|
||||||
// Identifying the type of track
|
// Identifying the type of track
|
||||||
TrackKind GetKind() const override { return TrackKind::Time; }
|
TrackKind GetKind() const override { return TrackKind::Time; }
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user