text: /**********************************************************************
  
  Audacium: A Digital Audio Editor
  
  ProjectFileIO.cpp
  
  Paul Licameli split from AudacityProject.cpp
  
  **********************************************************************/
  
  #include "ProjectFileIO.h"
  
  #include <atomic>
  #include <sqlite3.h>
  #include <wx/crt.h>
  #include <wx/frame.h>
  #include <wx/progdlg.h>
  #include <wx/sstream.h>
  #include <wx/xml/xml.h>
  
  #include "ActiveProjects.h"
  #include "CodeConversions.h"
  #include "DBConnection.h"
  #include "Project.h"
  #include "ProjectFileIORegistry.h"
  #include "ProjectSerializer.h"
  #include "ProjectSettings.h"
  #include "SampleBlock.h"
  #include "Tags.h"
  #include "TempDirectory.h"
  #include "ViewInfo.h"
  #include "WaveTrack.h"
  #include "widgets/AudacityMessageBox.h"
  #include "widgets/ErrorDialog.h"
  #include "widgets/NumericTextCtrl.h"
  #include "widgets/ProgressDialog.h"
  #include "wxFileNameWrapper.h"
  #include "xml/XMLFileReader.h"
  
  // Don't change this unless the file format changes
  // in an irrevocable way
  #define AUDACITY_FILE_FORMAT_VERSION "1.3.0"
  
  #undef NO_SHM
  #if !defined(__WXMSW__)
     #define NO_SHM
  #endif
  
  wxDEFINE_EVENT(EVT_PROJECT_TITLE_CHANGE, wxCommandEvent);
  wxDEFINE_EVENT( EVT_CHECKPOINT_FAILURE, wxCommandEvent);
  wxDEFINE_EVENT( EVT_RECONNECTION_FAILURE, wxCommandEvent);
  
  // Used to convert 4 byte-sized values into an integer for use in SQLite
  // PRAGMA statements. These values will be store in the database header.
  //
  // Note that endianness is not an issue here since SQLite integers are
  // architecture independent.
  #define PACK(b1, b2, b3, b4) ((b1 << 24) | (b2 << 16) | (b3 << 8) | b4)
  
  // The ProjectFileID is stored in the SQLite database header to identify the file
  // as an Audacity project file. It can be used by applications that identify file
  // types, such as the Linux "file" command.
  static const int ProjectFileID = PACK('A', 'U', 'D', 'Y');
  
  // The "ProjectFileVersion" represents the version of Audacity at which a specific
  // database schema was used. It is assumed that any changes to the database schema
  // will require a new Audacity version so if schema changes are required set this
  // to the new release being produced.
  //
  // This version is checked before accessing any tables in the database since there's
  // no guarantee what tables exist. If it's found that the database is newer than the
  // currently running Audacity, an error dialog will be displayed informing the user
  // that they need a newer version of Audacity.
  //
  // Note that this is NOT the "schema_version" that SQLite maintains. The value
  // specified here is stored in the "user_version" field of the SQLite database
  // header.
  static const int ProjectFileVersion = PACK(3, 0, 0, 0);
  
  // Navigation:
  //
  // Bindings are marked out in the code by, e.g. 
  // BIND SQL sampleblocks
  // A search for "BIND SQL" will find all bindings.
  // A search for "SQL sampleblocks" will find all SQL related 
  // to sampleblocks.
  
  static const char *ProjectFileSchema =
     // These are persistent and not connection based
     //
     // See the CMakeList.txt for the SQLite lib for more
     // settings.
     "PRAGMA <schema>.application_id = %d;"
     "PRAGMA <schema>.user_version = %d;"
     ""
     // project is a binary representation of an XML file.
     // it's in binary for speed.
     // One instance only.  id is always 1.
     // dict is a dictionary of fieldnames.
     // doc is the binary representation of the XML
     // in the doc, fieldnames are replaced by 2 byte dictionary
     // index numbers.
     // This is all opaque to SQLite.  It just sees two
     // big binary blobs.
     // There is no limit to document blob size.
     // dict will be smallish, with an entry for each 
     // kind of field.
     "CREATE TABLE IF NOT EXISTS <schema>.project"
     "("
     "  id                   INTEGER PRIMARY KEY,"
     "  dict                 BLOB,"
     "  doc                  BLOB"
     ");"
     ""
     // CREATE SQL autosave
     // autosave is a binary representation of an XML file.
     // it's in binary for speed.
     // One instance only.  id is always 1.
     // dict is a dictionary of fieldnames.
     // doc is the binary representation of the XML
     // in the doc, fieldnames are replaced by 2 byte dictionary
     // index numbers.
     // This is all opaque to SQLite.  It just sees two
     // big binary blobs.
     // There is no limit to document blob size.
     // dict will be smallish, with an entry for each 
     // kind of field.
     "CREATE TABLE IF NOT EXISTS <schema>.autosave"
     "("
     "  id                   INTEGER PRIMARY KEY,"
     "  dict                 BLOB,"
     "  doc                  BLOB"
     ");"
     ""
     // CREATE SQL sampleblocks
     // 'samples' are fixed size blocks of int16, int32 or float32 numbers.
     // The blocks may be partially empty.
     // The quantity of valid data in the blocks is
     // provided in the project blob.
     // 
     // sampleformat specifies the format of the samples stored.
     //
     // blockID is a 64 bit number.
     //
     // Rows are immutable -- never updated after addition, but may be
     // deleted.
     //
     // summin to summary64K are summaries at 3 distance scales.
     "CREATE TABLE IF NOT EXISTS <schema>.sampleblocks"
     "("
     "  blockid              INTEGER PRIMARY KEY AUTOINCREMENT,"
     "  sampleformat         INTEGER,"
     "  summin               REAL,"
     "  summax               REAL,"
     "  sumrms               REAL,"
     "  summary256           BLOB,"
     "  summary64k           BLOB,"
     "  samples              BLOB"
     ");";
  
  // This singleton handles initialization/shutdown of the SQLite library.
  // It is needed because our local SQLite is built with SQLITE_OMIT_AUTOINIT
  // defined.
  //
  // It's safe to use even if a system version of SQLite is used that didn't
  // have SQLITE_OMIT_AUTOINIT defined.
  class SQLiteIniter
  {
  public:
     SQLiteIniter()
     {
        // Enable URI filenames for all connections
        mRc = sqlite3_config(SQLITE_CONFIG_URI, 1);
        if (mRc == SQLITE_OK)
        {
           mRc = sqlite3_config(SQLITE_CONFIG_LOG, LogCallback, nullptr);
           if (mRc == SQLITE_OK)
           {
              mRc = sqlite3_initialize();
           }
        }
  
  #ifdef NO_SHM
        if (mRc == SQLITE_OK)
        {
           // Use the "unix-excl" VFS to make access to the DB exclusive.  This gets
           // rid of the "<database name>-shm" shared memory file.
           //
           // Though it shouldn't, it doesn't matter if this fails.
           auto vfs = sqlite3_vfs_find("unix-excl");
           if (vfs)
           {
              sqlite3_vfs_register(vfs, 1);
           }
        }
  #endif
     }
     ~SQLiteIniter()
     {
        // This function must be called single-threaded only
        // It returns a value, but there's nothing we can do with it
        (void) sqlite3_shutdown();
     }
  
     static void LogCallback(void *WXUNUSED(arg), int code, const char *msg)
     {
        wxLogMessage("sqlite3 message: (%d) %s", code, msg);
     }
  
     int mRc;
  };
  
  bool ProjectFileIO::InitializeSQL()
  {
     static SQLiteIniter sqliteIniter;
     return sqliteIniter.mRc == SQLITE_OK;
  }
  
  static void RefreshAllTitles(bool bShowProjectNumbers )
  {
     for ( auto pProject : AllProjects{} ) {
        if ( !GetProjectFrame( *pProject ).IsIconized() ) {
           ProjectFileIO::Get( *pProject ).SetProjectTitle(
              bShowProjectNumbers ? pProject->GetProjectNumber() : -1 );
        }
     }
  }
  
  TitleRestorer::TitleRestorer(
     wxTopLevelWindow &window, AudacityProject &project )
  {
     if( window.IsIconized() )
        window.Restore();
     window.Raise(); // May help identifying the window on Mac
  
     // Construct this project's name and number.
     sProjName = project.GetProjectName();
     if ( sProjName.empty() ) {
        sProjName = _("<untitled>");
        UnnamedCount = std::count_if(
           AllProjects{}.begin(), AllProjects{}.end(),
           []( const AllProjects::value_type &ptr ){
              return ptr->GetProjectName().empty();
           }
        );
        if ( UnnamedCount > 1 ) {
           sProjNumber.Printf(
              _("[Project %02i] "), project.GetProjectNumber() + 1 );
           RefreshAllTitles( true );
        } 
     }
     else
        UnnamedCount = 0;
  }
  
  TitleRestorer::~TitleRestorer() {
     if( UnnamedCount > 1 )
        RefreshAllTitles( false );
  }
  
  static const AudacityProject::AttachedObjects::RegisteredFactory sFileIOKey{
     []( AudacityProject &parent ){
        auto result = std::make_shared< ProjectFileIO >( parent );
        return result;
     }
  };
  
  ProjectFileIO &ProjectFileIO::Get( AudacityProject &project )
  {
     auto &result = project.AttachedObjects::Get< ProjectFileIO >( sFileIOKey );
     return result;
  }
  
  const ProjectFileIO &ProjectFileIO::Get( const AudacityProject &project )
  {
     return Get( const_cast< AudacityProject & >( project ) );
  }
  
  ProjectFileIO::ProjectFileIO(AudacityProject &project)
     : mProject{ project }
     , mpErrors{ std::make_shared<DBConnectionErrors>() }
  {
     mPrevConn = nullptr;
  
     mRecovered = false;
     mModified = false;
     mTemporary = true;
  
     UpdatePrefs();
  }
  
  ProjectFileIO::~ProjectFileIO()
  {
  }
  
  bool ProjectFileIO::HasConnection() const
  {
     auto &connectionPtr = ConnectionPtr::Get( mProject );
     return connectionPtr.mpConnection != nullptr;
  }
  
  DBConnection &ProjectFileIO::GetConnection()
  {
     auto &curConn = CurrConn();
     if (!curConn)
     {
        if (!OpenConnection())
        {
           throw SimpleMessageBoxException
           {
              ExceptionType::Internal,
              XO("Failed to open the project's database"),
              XO("Warning"),
              "Error:_Disk_full_or_not_writable"
           };
        }
     }
  
     return *curConn;
  }
  
  wxString ProjectFileIO::GenerateDoc()
  {
     auto &trackList = TrackList::Get( mProject );
  
     XMLStringWriter doc;
     WriteXMLHeader(doc);
     WriteXML(doc, false, trackList.empty() ? nullptr : &trackList);
     return doc;
  }
  
  sqlite3 *ProjectFileIO::DB()
  {
     return GetConnection().DB();
  }
  
  /*!
   @pre *CurConn() does not exist
   @post *CurConn() exists or return value is false
   */
  bool ProjectFileIO::OpenConnection(FilePath fileName /* = {}  */)
  {
     auto &curConn = CurrConn();
     wxASSERT(!curConn);
     bool isTemp = false;
  
     if (fileName.empty())
     {
        fileName = GetFileName();
        if (fileName.empty())
        {
           fileName = TempDirectory::UnsavedProjectFileName();
           isTemp = true;
        }
     }
     else
     {
        // If this project resides in the temporary directory, then we'll mark it
        // as temporary.
        wxFileName temp(TempDirectory::TempDir(), wxT(""));
        wxFileName file(fileName);
        file.SetFullName(wxT(""));
        if (file == temp)
        {
           isTemp = true;
        }
     }
  
     // Pass weak_ptr to project into DBConnection constructor
     curConn = std::make_unique<DBConnection>(
        mProject.shared_from_this(), mpErrors, [this]{ OnCheckpointFailure(); } );
     auto rc = curConn->Open(fileName);
     if (rc != SQLITE_OK)
     {
        // Must use SetError() here since we do not have an active DB
        SetError(
           XO("Failed to open database file:\n\n%s").Format(fileName),
           {},
           rc
        );
        curConn.reset();
        return false;
     }
  
     if (!CheckVersion())
     {
        CloseConnection();
        curConn.reset();
        return false;
     }
  
     mTemporary = isTemp;
  
     SetFileName(fileName);
  
     return true;
  }
  
  bool ProjectFileIO::CloseConnection()
  {
     auto &curConn = CurrConn();
     if (!curConn)
        return false;
  
     if (!curConn->Close())
     {
        return false;
     }
     curConn.reset();
  
     SetFileName({});
  
     return true;
  }
  
  // Put the current database connection aside, keeping it open, so that
  // another may be opened with OpenConnection()
  void ProjectFileIO::SaveConnection()
  {
     // Should do nothing in proper usage, but be sure not to leak a connection:
     DiscardConnection();
  
     mPrevConn = std::move(CurrConn());
     mPrevFileName = mFileName;
     mPrevTemporary = mTemporary;
  
     SetFileName({});
  }
  
  // Close any set-aside connection
  void ProjectFileIO::DiscardConnection()
  {
     if (mPrevConn)
     {
        if (!mPrevConn->Close())
        {
           // Store an error message
           SetDBError(
              XO("Failed to discard connection")
           );
        }
  
        // If this is a temporary project, we no longer want to keep the
        // project file.
        if (mPrevTemporary)
        {
           // This is just a safety check.
           wxFileName temp(TempDirectory::TempDir(), wxT(""));
           wxFileName file(mPrevFileName);
           file.SetFullName(wxT(""));
           if (file == temp)
           {
              if (!RemoveProject(mPrevFileName))
              {
                 wxLogMessage("Failed to remove temporary project %s", mPrevFileName);
              }
           }
        }
        mPrevConn = nullptr;
        mPrevFileName.clear();
     }
  }
  
  // Close any current connection and switch back to using the saved
  void ProjectFileIO::RestoreConnection()
  {
     auto &curConn = CurrConn();
     if (curConn)
     {
        if (!curConn->Close())
        {
           // Store an error message
           SetDBError(
              XO("Failed to restore connection")
           );
        }
     }
  
     curConn = std::move(mPrevConn);
     SetFileName(mPrevFileName);
     mTemporary = mPrevTemporary;
  
     mPrevFileName.clear();
  }
  
  void ProjectFileIO::UseConnection(Connection &&conn, const FilePath &filePath)
  {
     auto &curConn = CurrConn();
     wxASSERT(!curConn);
  
     curConn = std::move(conn);
     SetFileName(filePath);
  }
  
  static int ExecCallback(void *data, int cols, char **vals, char **names)
  {
     auto &cb = *static_cast<const ProjectFileIO::ExecCB *>(data);
     // Be careful not to throw anything across sqlite3's stack frames.
     return GuardedCall<int>(
        [&]{ return cb(cols, vals, names); },
        MakeSimpleGuard( 1 )
     );
  }
  
  int ProjectFileIO::Exec(const char *query, const ExecCB &callback)
  {
     char *errmsg = nullptr;
  
     const void *ptr = &callback;
     int rc = sqlite3_exec(DB(), query, ExecCallback,
                           const_cast<void*>(ptr), &errmsg);
  
     if (rc != SQLITE_ABORT && errmsg)
     {
        SetDBError(
           XO("Failed to execute a project file command:\n\n%s").Format(query),
           Verbatim(errmsg),
           rc
        );
     }
     if (errmsg)
     {
        sqlite3_free(errmsg);
     }
  
     return rc;
  }
  
  bool ProjectFileIO::Query(const char *sql, const ExecCB &callback)
  {
     int rc = Exec(sql, callback);
     // SQLITE_ABORT is a non-error return only meaning the callback
     // stopped the iteration of rows early
     if ( !(rc == SQLITE_OK || rc == SQLITE_ABORT) )
     {
        return false;
     }
  
     return true;
  }
  
  bool ProjectFileIO::GetValue(const char *sql, wxString &result)
  {
     // Retrieve the first column in the first row, if any
     result.clear();
     auto cb = [&result](int cols, char **vals, char **){
        if (cols > 0)
           result = vals[0];
        // Stop after one row
        return 1;
     };
  
     return Query(sql, cb);
  }
  
  bool ProjectFileIO::GetBlob(const char *sql, wxMemoryBuffer &buffer)
  {
     auto db = DB();
     int rc;
  
     buffer.Clear();
  
     sqlite3_stmt *stmt = nullptr;
     auto cleanup = finally([&]
     {
        if (stmt)
        {
           sqlite3_finalize(stmt);
        }
     });
  
     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;
     }
  
     rc = sqlite3_step(stmt);
  
     // A row wasn't found...not an error
     if (rc == SQLITE_DONE)
     {
        return true;
     }
  
     if (rc != SQLITE_ROW)
     {
        SetDBError(
           XO("Failed to retrieve data from the project file.\nThe following command failed:\n\n%s").Format(sql)
        );
        // AUD TODO handle error
        return false;
     }
  
     const void *blob = sqlite3_column_blob(stmt, 0);
     int size = sqlite3_column_bytes(stmt, 0);
  
     buffer.AppendData(blob, size);
  
     return true;
  }
  
  bool ProjectFileIO::CheckVersion()
  {
     auto db = DB();
     int rc;
  
     // Install our schema if this is an empty DB
     wxString result;
     if (!GetValue("SELECT Count(*) FROM sqlite_master WHERE type='table';", result))
     {
        // Bug 2718 workaround for a better error message:
        // If at this point we get SQLITE_CANTOPEN, then the directory is read-only
        if (GetLastErrorCode() == SQLITE_CANTOPEN)
        {
            SetError(
                /* i18n-hint: An error message. */
                XO("Project is in a read only directory\n(Unable to create the required temporary files)"),
                GetLibraryError()
            );
        }
  
        return false;
     }
  
     // If the return count is zero, then there are no tables defined, so this
     // must be a new project file.
     if (wxStrtol<char **>(result, nullptr, 10) == 0)
     {
        return InstallSchema(db);
     }
  
     // Check for our application ID
     if (!GetValue("PRAGMA application_ID;", result))
     {
        return false;
     }
  
     // It's a database that SQLite recognizes, but it's not one of ours
     if (wxStrtoul<char **>(result, nullptr, 10) != ProjectFileID)
     {
        SetError(XO("This is not an Audacium project file"));
        return false;
     }
  
     // Get the project file version
     if (!GetValue("PRAGMA user_version;", result))
     {
        return false;
     }
  
     long version = wxStrtol<char **>(result, nullptr, 10);
  
     // Project file version is higher than ours. We will refuse to
     // process it since we can't trust anything about it.
     if (version > ProjectFileVersion)
     {
        SetError(
           XO("This project was created with a newer version of Audacium.\n\nYou will need to upgrade to open it.")
        );
        return false;
     }
     
     // Project file is older than ours, ask the user if it's okay to
     // upgrade.
     if (version < ProjectFileVersion)
     {
        return UpgradeSchema();
     }
  
     return true;
  }
  
  bool ProjectFileIO::InstallSchema(sqlite3 *db, const char *schema /* = "main" */)
  {
     int rc;
  
     wxString sql;
     sql.Printf(ProjectFileSchema, ProjectFileID, ProjectFileVersion);
     sql.Replace("<schema>", schema);
  
     rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
     if (rc != SQLITE_OK)
     {
        SetDBError(
           XO("Unable to initialize the project file")
        );
        return false;
     }
  
     return true;
  }
  
  bool ProjectFileIO::UpgradeSchema()
  {
     // To do
     return true;
  }
  
  // The orphan block handling should be removed once autosave and related
  // blocks become part of the same transaction.
  
  // An SQLite function that takes a blockid and looks it up in a set of
  // blockids captured during project load.  If the blockid isn't found
  // in the set, it will be deleted.
  void ProjectFileIO::InSet(sqlite3_context *context, int argc, sqlite3_value **argv)
  {
     BlockIDs *blockids = (BlockIDs *) sqlite3_user_data(context);
     SampleBlockID blockid = sqlite3_value_int64(argv[0]);
  
     sqlite3_result_int(context, blockids->find(blockid) != blockids->end());
  }
  
  bool ProjectFileIO::DeleteBlocks(const BlockIDs &blockids, bool complement)
  {
     auto db = DB();
     int rc;
  
     auto cleanup = finally([&]
     {
        // Remove our function, whether it was successfully defined or not.
        sqlite3_create_function(db, "inset", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, nullptr, nullptr, nullptr, nullptr);
     });
  
     // Add the function used to verify each row's blockid against the set of active blockids
     const void *p = &blockids;
     rc = sqlite3_create_function(db, "inset", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, const_cast<void*>(p), InSet, nullptr, nullptr);
     if (rc != SQLITE_OK)
     {
        /* i18n-hint: An error message.  Don't translate inset or blockids.*/
        SetDBError(XO("Unable to add 'inset' function (can't verify blockids)"));
        return false;
     }
  
     // Delete all rows in the set, or not in it
     // This is the first command that writes to the database, and so we
     // do more informative error reporting than usual, if it fails.
     auto sql = wxString::Format(
        "DELETE FROM sampleblocks WHERE %sinset(blockid);",
        complement ? "NOT " : "" );
     rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
     if (rc != SQLITE_OK)
     {
        if( rc==SQLITE_READONLY)
           /* i18n-hint: An error message.  Don't translate blockfiles.*/
           SetDBError(XO("Project is read only\n(Unable to work with the blockfiles)"));
        else if( rc==SQLITE_LOCKED)
           /* i18n-hint: An error message.  Don't translate blockfiles.*/
           SetDBError(XO("Project is locked\n(Unable to work with the blockfiles)"));
        else if( rc==SQLITE_BUSY)
           /* i18n-hint: An error message.  Don't translate blockfiles.*/
           SetDBError(XO("Project is busy\n(Unable to work with the blockfiles)"));
        else if( rc==SQLITE_CORRUPT)
           /* i18n-hint: An error message.  Don't translate blockfiles.*/
           SetDBError(XO("Project is corrupt\n(Unable to work with the blockfiles)"));
        else if( rc==SQLITE_PERM)
           /* i18n-hint: An error message.  Don't translate blockfiles.*/
           SetDBError(XO("Some permissions issue\n(Unable to work with the blockfiles)"));
        else if( rc==SQLITE_IOERR)
           /* i18n-hint: An error message.  Don't translate blockfiles.*/
           SetDBError(XO("A disk I/O error\n(Unable to work with the blockfiles)"));
        else if( rc==SQLITE_AUTH)
           /* i18n-hint: An error message.  Don't translate blockfiles.*/
           SetDBError(XO("Not authorized\n(Unable to work with the blockfiles)"));
        else
           /* i18n-hint: An error message.  Don't translate blockfiles.*/
           SetDBError(XO("Unable to work with the blockfiles"));
  
        return false;
     }
  
     // Mark the project recovered if we deleted any rows
     int changes = sqlite3_changes(db);
     if (changes > 0)
     {
        wxLogInfo(XO("Total orphan blocks deleted %d").Translation(), changes);
        mRecovered = true;
     }
  
     return true;
  }
  
  bool ProjectFileIO::CopyTo(const FilePath &destpath,
     const TranslatableString &msg,
     bool isTemporary,
     bool prune /* = false */,
     const std::vector<const TrackList *> &tracks /* = {} */)
  {
     auto pConn = CurrConn().get();
     if (!pConn)
        return false;
  
     // Get access to the active tracklist
     auto pProject = &mProject;
  
     SampleBlockIDSet blockids;
  
     // Collect all active blockids
     if (prune)
     {
        for (auto trackList : tracks)
           if (trackList)
              InspectBlocks( *trackList, {}, &blockids );
     }
     // Collect ALL blockids
     else
     {
        auto cb = [&blockids](int cols, char **vals, char **){
           SampleBlockID blockid;
           wxString{ vals[0] }.ToLongLong(&blockid);
           blockids.insert(blockid);
           return 0;
        };
  
        if (!Query("SELECT blockid FROM sampleblocks;", cb))
        {
           // Error message already captured.
           return false;
        }
     }
  
     // Create the project doc
     ProjectSerializer doc;
     WriteXMLHeader(doc);
     WriteXML(doc, false, tracks.empty() ? nullptr : tracks[0]);
  
     auto db = DB();
     Connection destConn = nullptr;
     bool success = false;
     int rc = SQLITE_OK;
     ProgressResult res = ProgressResult::Success;
  
     // Cleanup in case things go awry
     auto cleanup = finally([&]
     {
        if (!success)
        {
           if (destConn)
           {
              destConn->Close();
              destConn = nullptr;
           }
  
           // Rollback transaction in case one was active.
           // 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 || rc == SQLITE_OK))
           {
              SetDBError(
                 XO("Failed to rollback transaction during import")
              );
           }
  
           // And detach the outbound DB in case (if it's attached). Don't check for
           // errors since it may not be attached. But, if it is and the DETACH fails,
           // subsequent CopyTo() actions will fail until Audacity is relaunched.
           sqlite3_exec(db, "DETACH DATABASE outbound;", nullptr, nullptr, nullptr);
  
           // RemoveProject not necessary to clean up attached database
           wxRemoveFile(destpath);
        }
     });
  
     // Attach the destination database 
     wxString sql;
     wxString dbName = destpath;
     // Bug 2793: Quotes in name need escaping for sqlite3.
     dbName.Replace( "'", "''");
     sql.Printf("ATTACH DATABASE '%s' AS outbound;", dbName.ToUTF8());
  
     rc = sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
     if (rc != SQLITE_OK)
     {
        SetDBError(
           XO("Unable to attach destination database")
        );
        return false;
     }
  
     // Ensure attached DB connection gets configured
     //
     // NOTE:  Between the above attach and setting the mode here, a normal DELETE
     //        mode journal will be used and will briefly appear in the filesystem.
     if ( pConn->FastMode("outbound") != SQLITE_OK)
     {
        SetDBError(
           XO("Unable to switch to fast journaling mode")
        );
  
        return false;
     }
  
     // Install our schema into the new database
     if (!InstallSchema(db, "outbound"))
     {
        // Message already set
        return false;
     }
  
     {
        // Ensure statement gets cleaned up
        sqlite3_stmt *stmt = nullptr;
        auto cleanup = finally([&]
        {
           if (stmt)
           {
              // No need to check return code
              sqlite3_finalize(stmt);
           }
        });
  
        // Prepare the statement only once
        rc = sqlite3_prepare_v2(db,
                                "INSERT INTO outbound.sampleblocks"
                                "  SELECT * FROM main.sampleblocks"
                                "  WHERE blockid = ?;",
                                -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"), msg, pdlgHideStopButton);
        ProgressResult result = ProgressResult::Success;
  
        wxLongLong_t count = 0;
        wxLongLong_t total = blockids.size();
  
        // Start a transaction.  Since we're running without a journal,
        // this really doesn't provide rollback.  It just prevents SQLite
        // from auto committing after each step through the loop.
        //
        // Also note that we will have an open transaction if we fail
        // while copying the blocks. This is fine since we're just going
        // to delete the database anyway.
        sqlite3_exec(db, "BEGIN;", nullptr, nullptr, nullptr);
  
        // Copy sample blocks from the main DB to the outbound DB
        for (auto blockid : blockids)
        {
           // Bind statement parameters
           rc = sqlite3_bind_int64(stmt, 1, blockid);
           if (rc != SQLITE_OK)
           {
              SetDBError(
                 XO("Failed to bind SQL parameter")
              );
  
              return false;
           }
  
           // Process it
           rc = sqlite3_step(stmt);
           if (rc != SQLITE_DONE)
           {
              SetDBError(
                 XO("Failed to update the project file.\nThe following command failed:\n\n%s").Format(sql)
              );
              return false;
           }
  
           // Reset statement to beginning
           if (sqlite3_reset(stmt) != SQLITE_OK)
           {
              THROW_INCONSISTENCY_EXCEPTION;
           }
  
           result = progress.Update(++count, total);
           if (result != ProgressResult::Success)
           {
              // Note that we're not setting success, so the finally
              // block above will take care of cleaning up
              return false;
           }
        }
  
        // Write the doc.
        //
        // If we're compacting a temporary project (user initiated from the File
        // menu), then write the doc to the "autosave" table since temporary
        // projects do not have a "project" doc.
        if (!WriteDoc(isTemporary ? "autosave" : "project", doc, "outbound"))
        {
           return false;
        }
  
        // See BEGIN above...
        sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);
     }
  
     // Detach the destination database
     rc = sqlite3_exec(db, "DETACH DATABASE outbound;", nullptr, nullptr, nullptr);
     if (rc != SQLITE_OK)
     {
        SetDBError(
           XO("Destination project could not be detached")
        );
  
        return false;
     }
  
     // Tell cleanup everything is good to go
     success = true;
  
     return true;
  }
  
  bool ProjectFileIO::ShouldCompact(const std::vector<const TrackList *> &tracks)
  {
     SampleBlockIDSet active;
     unsigned long long current = 0;
  
     {
        auto fn = BlockSpaceUsageAccumulator( current );
        for (auto pTracks : tracks)
           if (pTracks)
              InspectBlocks( *pTracks, fn,
                 &active // Visit unique blocks only
              );
     }
  
     // Get the number of blocks and total length from the project file.
     unsigned long long total = GetTotalUsage();
     unsigned long long blockcount = 0;
     
     auto cb = [&blockcount](int cols, char **vals, char **)
     {
        // Convert
        wxString(vals[0]).ToULongLong(&blockcount);
        return 0;
     };
  
     if (!Query("SELECT Count(*) FROM sampleblocks;", cb) || blockcount == 0)
     {
        // Shouldn't compact since we don't have the full picture
        return false;
     }
  
     // Remember if we had unused blocks in the project file
     mHadUnused = (blockcount > active.size());
  
     // Let's make a percentage...should be plenty of head room
     current *= 100;
  
     wxLogDebug(wxT("used = %lld total = %lld %lld"), current, total, total ? current / total : 0);
     if (!total || current / total > 80)
     {
        wxLogDebug(wxT("not compacting"));
        return false;
     }
     wxLogDebug(wxT("compacting"));
  
     return true;
  }
  
  Connection &ProjectFileIO::CurrConn()
  {
     auto &connectionPtr = ConnectionPtr::Get( mProject );
     return connectionPtr.mpConnection;
  }
  
  const std::vector<wxString> &ProjectFileIO::AuxiliaryFileSuffixes()
  {
     static const std::vector<wxString> strings {
        "-wal",
  #ifndef NO_SHM
        "-shm",
  #endif
     };
     return strings;
  }
  
  FilePath ProjectFileIO::SafetyFileName(const FilePath &src)
  {
     wxFileNameWrapper fn{ src };
  
     // Extra characters inserted into filename before extension
     wxString extra =
  #ifdef __WXGTK__
        wxT("~")
  #else
        wxT(".bak")
  #endif
     ;
  
     int nn = 1;
     auto numberString = [](int num) -> wxString {
        return num == 1 ? wxString{} : wxString::Format(".%d", num);
     };
  
     auto suffixes = AuxiliaryFileSuffixes();
     suffixes.push_back({});
  
     // Find backup paths not already occupied; check all auxiliary suffixes
     const auto name = fn.GetName();
     FilePath result;
     do {
        fn.SetName( name + numberString(nn++) + extra );
        result = fn.GetFullPath();
     }
     while( std::any_of(suffixes.begin(), suffixes.end(), [&](auto &suffix){
        return wxFileExists(result + suffix);
     }) );
  
     return result;
  }
  
  bool ProjectFileIO::RenameOrWarn(const FilePath &src, const FilePath &dst)
  {
     std::atomic_bool done = {false};
     bool success = false;
     auto thread = std::thread([&]
     {
        success = wxRenameFile(src, dst);
        done = true;
     });
  
     auto &window = GetProjectFrame( mProject );
  
     // Provides a progress dialog with indeterminate mode
     wxGenericProgressDialog pd(XO("Copying Project").Translation(),
                                XO("This may take several seconds").Translation(),
                                300000,     // range
                                &window,     // parent
                                wxPD_APP_MODAL | wxPD_ELAPSED_TIME | wxPD_SMOOTH);
  
     // Wait for the checkpoints to end
     while (!done)
     {
        wxMilliSleep(50);
        pd.Pulse();
     }
     thread.join();
  
     if (!success)
     {
        ShowError(
           &window,
           XO("Error Writing to File"),
           XO("Audacium failed to write file %s.\n"
              "Perhaps disk is full or not writable.\n"
              "For tips on freeing up space, click the help button.")
              .Format(dst),
           "Error:_Disk_full_or_not_writable"
           );
        return false;
     }
  
     return true;
  }
  
  bool ProjectFileIO::MoveProject(const FilePath &src, const FilePath &dst)
  {
     // Assume the src database file is not busy.
     if (!RenameOrWarn(src, dst))
        return false;
  
     // So far so good, but the separate -wal and -shm files might yet exist,
     // as when checkpointing failed for limited space on the drive.
     // If so move them too or else lose data.
  
     std::vector< std::pair<FilePath, FilePath> > pairs{ { src, dst } };
     bool success = false;
     auto cleanup = finally([&]{
        if (!success) {
           // If any one of the renames failed, back out the previous ones.
           // This should be a no-fail recovery!  Not clear what to do if any
           // of these renames fails.
           for (auto &pair : pairs) {
              if (!(pair.first.empty() && pair.second.empty()))
                 wxRenameFile(pair.second, pair.first);
           }
        }
     });
  
     for (const auto &suffix : AuxiliaryFileSuffixes()) {
        auto srcName = src + suffix;
        if (wxFileExists(srcName)) {
           auto dstName = dst + suffix;
           if (!RenameOrWarn(srcName, dstName))
              return false;
           pairs.push_back({ srcName, dstName });
        }
     }
  
     return (success = true);
  }
  
  bool ProjectFileIO::RemoveProject(const FilePath &filename)
  {
     if (!wxFileExists(filename))
        return false;
  
     bool success = wxRemoveFile(filename);
     auto &suffixes = AuxiliaryFileSuffixes();
     for (const auto &suffix : suffixes) {
        auto file = filename + suffix;
        if (wxFileExists(file))
           success = wxRemoveFile(file) && success;
     }
     return success;
  }
  
  ProjectFileIO::BackupProject::BackupProject(
     ProjectFileIO &projectFileIO, const FilePath &path )
  {
     auto safety = SafetyFileName(path);
     if (!projectFileIO.MoveProject(path, safety))
        return;
  
     mPath = path;
     mSafety = safety;
  }
  
  void ProjectFileIO::BackupProject::Discard()
  {
     if (!mPath.empty()) {
        // Succeeded; don't need the safety files
        RemoveProject(mSafety);
        mSafety.clear();
     }
  }
  
  ProjectFileIO::BackupProject::~BackupProject()
  {
     if (!mPath.empty()) {
        if (!mSafety.empty()) {
           // Failed; restore from safety files
           auto suffixes = AuxiliaryFileSuffixes();
           suffixes.push_back({});
           for (const auto &suffix : suffixes) {
              auto path = mPath + suffix;
              if (wxFileExists(path))
                 wxRemoveFile(path);
              wxRenameFile(mSafety + suffix, mPath + suffix);
           }
        }
     }
  }
  
  void ProjectFileIO::Compact(
     const std::vector<const TrackList *> &tracks, bool force)
  {
     // Haven't compacted yet
     mWasCompacted = false;
  
     // Assume we have unused blocks until we find out otherwise. That way cleanup
     // at project close time will still occur.
     mHadUnused = true;
  
     // If forcing compaction, bypass inspection.
     if (!force)
     {
        // Don't compact if this is a temporary project or if it's determined there are not
        // enough unused blocks to make it worthwhile.
        if (IsTemporary() || !ShouldCompact(tracks))
        {
           // Delete the AutoSave doc it if exists
           if (IsModified())
           {
              // PRL:  not clear what to do if the following fails, but the worst should
              // be, the project may reopen in its present state as a recovery file, not
              // at the last saved state.
              // REVIEW: Could the autosave file be corrupt though at that point, and so 
              // prevent recovery?
              // LLL: I believe Paul is correct since it's deleted with a single SQLite
              // transaction. The next time the file opens will just invoke recovery.
              (void) AutoSaveDelete();
           }
  
           return;
        }
     }
  
     wxString origName = mFileName;
     wxString backName = origName + "_compact_back";
     wxString tempName = origName + "_compact_temp";
  
     // Copy the original database to a new database. Only prune sample blocks if
     // we have a tracklist.
     // REVIEW: Compact can fail on the CopyTo with no error messages.  That's OK?
     // LLL: We could display an error message or just ignore the failure and allow
     // the file to be compacted the next time it's saved.
     if (CopyTo(tempName, XO("Compacting project"), IsTemporary(), !tracks.empty(), tracks))
     {
        // Must close the database to rename it
        if (CloseConnection())
        {
           // Only use the new file if it is actually smaller than the original.
           //
           // If the original file doesn't have anything to compact (original and new
           // are basically identical), the file could grow by a few pages because of
           // differences in how SQLite constructs the b-tree.
           //
           // In this case, just toss the new file and continue to use the original.
           //
           // Also, do this after closing the connection so that the -wal file
           // gets cleaned up.
           if (wxFileName::GetSize(tempName) < wxFileName::GetSize(origName))
           {
              // Rename the original to backup
              if (wxRenameFile(origName, backName))
              {
                 // Rename the temporary to original
                 if (wxRenameFile(tempName, origName))
                 {
                    // Open the newly compacted original file
                    if (OpenConnection(origName))
                    {
                       // Remove the old original file
                       if (!wxRemoveFile(backName))
                       {
                          // Just log the error, nothing can be done to correct it
                          // and WX should have logged another message showing the
                          // system error code.
                          wxLogWarning(wxT("Compaction failed to delete backup %s"), backName);
                       }
  
                       // Remember that we compacted
                       mWasCompacted = true;
  
                       return;
                    }
                    else
                    {
                       wxLogWarning(wxT("Compaction failed to open new project %s"), origName);
                    }
  
                    if (!wxRenameFile(origName, tempName))
                    {
                       wxLogWarning(wxT("Compaction failed to rename orignal %s to temp %s"),
                                    origName, tempName);
                    }
                 }
                 else
                 {
                    wxLogWarning(wxT("Compaction failed to rename temp %s to orig %s"),
                                   origName, tempName);
                 }
  
                 if (!wxRenameFile(backName, origName))
                 {
                    wxLogWarning(wxT("Compaction failed to rename back %s to orig %s"),
                                 backName, origName);
                 }
              }
              else
              {
                 wxLogWarning(wxT("Compaction failed to rename orig %s to back %s"),
                                backName, origName);
              }
           }
  
           if (!OpenConnection(origName))
           {
              wxLogWarning(wxT("Compaction failed to reopen %s"), origName);
           }
        }
  
        // Did not achieve any real compaction
        // RemoveProject not needed for what was an attached database
        if (!wxRemoveFile(tempName))
        {
           // Just log the error, nothing can be done to correct it
           // and WX should have logged another message showing the
           // system error code.
           wxLogWarning(wxT("Failed to delete temporary file...ignoring"));
        }
     }
  
     return;
  }
  
  bool ProjectFileIO::WasCompacted()
  {
     return mWasCompacted;
  }
  
  bool ProjectFileIO::HadUnused()
  {
     return mHadUnused;
  }
  
  void ProjectFileIO::UpdatePrefs()
  {
     SetProjectTitle();
  }
  
  // Pass a number in to show project number, or -1 not to.
  void ProjectFileIO::SetProjectTitle(int number)
  {
     auto &project = mProject;
     auto pWindow = project.GetFrame();
     if (!pWindow)
     {
        return;
     }
     auto &window = *pWindow;
     wxString name = project.GetProjectName();
  
     // If we are showing project numbers, then we also explicitly show "<untitled>" if there
     // is none.
     if (number >= 0)
     {
        name =
        /* i18n-hint: The %02i is the project number, the %s is the project name.*/
        XO("[Project %02i] Audacium \"%s\"")
           .Format( number + 1,
                   name.empty() ? XO("<untitled>") : Verbatim((const char *)name))
           .Translation();
     }
     // If we are not showing numbers, then <untitled> shows as 'Audacity'.
     else if (name.empty())
     {
        name = "Audacium";
     }
  
     if (mRecovered)
     {
        name += wxT(" ");
        /* i18n-hint: E.g this is recovered audio that had been lost.*/
        name += _("(Recovered)");
     }
  
     if (name != window.GetTitle())
     {
        window.SetTitle( name );
        window.SetName(name);       // to make the nvda screen reader read the correct title
  
        project.QueueEvent(
           safenew wxCommandEvent{ EVT_PROJECT_TITLE_CHANGE } );
     }
  }
  
  const FilePath &ProjectFileIO::GetFileName() const
  {
     return mFileName;
  }
  
  void ProjectFileIO::SetFileName(const FilePath &fileName)
  {
     auto &project = mProject;
  
     if (!mFileName.empty())
     {
        ActiveProjects::Remove(mFileName);
     }
  
     mFileName = fileName;
  
     if (!mFileName.empty())
     {
        ActiveProjects::Add(mFileName);
     }
  
     if (IsTemporary())
     {
        project.SetProjectName({});
     }
     else
     {
        project.SetProjectName(wxFileName(mFileName).GetName());
     }
  
     SetProjectTitle();
  }
  
  bool ProjectFileIO::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
  {
     auto &project = mProject;
     auto &window = GetProjectFrame(project);
     auto &viewInfo = ViewInfo::Get(project);
     auto &settings = ProjectSettings::Get(project);
  
     wxString fileVersion;
     wxString audacityVersion;
     int requiredTags = 0;
     long longVpos = 0;
  
     // loop through attrs, which is a null-terminated list of
     // attribute-value pairs
     while (*attrs)
     {
        const wxChar *attr = *attrs++;
        const wxChar *value = *attrs++;
  
        if (!value || !XMLValueChecker::IsGoodString(value))
        {
           break;
        }
  
        if (viewInfo.ReadXMLAttribute(attr, value))
        {
           // We need to save vpos now and restore it below
           longVpos = std::max(longVpos, long(viewInfo.vpos));
           continue;
        }
  
        else if (!wxStrcmp(attr, wxT("version")))
        {
           fileVersion = value;
           requiredTags++;
        }
  
        else if (!wxStrcmp(attr, wxT("audacityversion")))
        {
           audacityVersion = value;
           requiredTags++;
        }
  
        else if (!wxStrcmp(attr, wxT("rate")))
        {
           double rate;
           Internat::CompatibleToDouble(value, &rate);
           settings.SetRate( rate );
        }
  
        else if (!wxStrcmp(attr, wxT("snapto")))
        {
           settings.SetSnapTo(wxString(value) == wxT("on") ? true : false);
        }
  
        else if (!wxStrcmp(attr, wxT("selectionformat")))
        {
           settings.SetSelectionFormat(
              NumericConverter::LookupFormat( NumericConverter::TIME, value) );
        }
  
        else if (!wxStrcmp(attr, wxT("audiotimeformat")))
        {
           settings.SetAudioTimeFormat(
              NumericConverter::LookupFormat( NumericConverter::TIME, value) );
        }
  
        else if (!wxStrcmp(attr, wxT("frequencyformat")))
        {
           settings.SetFrequencySelectionFormatName(
              NumericConverter::LookupFormat( NumericConverter::FREQUENCY, value ) );
        }
  
        else if (!wxStrcmp(attr, wxT("bandwidthformat")))
        {
           settings.SetBandwidthSelectionFormatName(
              NumericConverter::LookupFormat( NumericConverter::BANDWIDTH, value ) );
        }
     } // while
  
     if (longVpos != 0)
     {
        // PRL: It seems this must happen after SetSnapTo
        viewInfo.vpos = longVpos;
     }
  
     if (requiredTags < 2)
     {
        return false;
     }
  
     // Parse the file version from the project
     int fver;
     int frel;
     int frev;
     if (!wxSscanf(fileVersion, wxT("%i.%i.%i"), &fver, &frel, &frev))
     {
        return false;
     }
  
     // Parse the file version Audacity was build with
     int cver;
     int crel;
     int crev;
     wxSscanf(wxT(AUDACITY_FILE_FORMAT_VERSION), wxT("%i.%i.%i"), &cver, &crel, &crev);
  
     int fileVer = ((fver *100)+frel)*100+frev;
     int codeVer = ((cver *100)+crel)*100+crev;
  
     if (codeVer<fileVer)
     {
        /* i18n-hint: %s will be replaced by the version number.*/
        auto msg = XO("This file was saved using Audacium %s.\nYou are using Audacium %s. You may need to upgrade to a newer version to open this file.")
           .Format(audacityVersion, AUDACITY_VERSION_STRING);
  
        ShowError(
           &window,
           XO("Can't open project file"),
           msg, 
           "FAQ:Errors_opening_an_Audacity_project"
           );
  
        return false;
     }
  
     if (wxStrcmp(tag, wxT("project")))
     {
        return false;
     }
  
     // All other tests passed, so we succeed
     return true;
  }
  
  XMLTagHandler *ProjectFileIO::HandleXMLChild(const wxChar *tag)
  {
     auto &project = mProject;
     auto fn = ProjectFileIORegistry::Lookup(tag);
     if (fn)
     {
        return fn(project);
     }
  
     return nullptr;
  }
  
  void ProjectFileIO::OnCheckpointFailure()
  {
     wxCommandEvent evt{ EVT_CHECKPOINT_FAILURE };
     mProject.ProcessEvent(evt);
  }
  
  void ProjectFileIO::WriteXMLHeader(XMLWriter &xmlFile) const
  {
     xmlFile.Write(wxT("<?xml "));
     xmlFile.Write(wxT("version=\"1.0\" "));
     xmlFile.Write(wxT("standalone=\"no\" "));
     xmlFile.Write(wxT("?>\n"));
  
     xmlFile.Write(wxT("<!DOCTYPE "));
     xmlFile.Write(wxT("project "));
     xmlFile.Write(wxT("PUBLIC "));
     xmlFile.Write(wxT("\"-//audacityproject-1.3.0//DTD//EN\" "));
     xmlFile.Write(wxT("\"http://audacity.sourceforge.net/xml/audacityproject-1.3.0.dtd\" "));
     xmlFile.Write(wxT(">\n"));
  }
  
  void ProjectFileIO::WriteXML(XMLWriter &xmlFile,
                               bool recording /* = false */,
                               const TrackList *tracks /* = nullptr */)
  // may throw
  {
     auto &proj = mProject;
     auto &tracklist = tracks ? *tracks : TrackList::Get(proj);
     auto &viewInfo = ViewInfo::Get(proj);
     auto &tags = Tags::Get(proj);
     const auto &settings = ProjectSettings::Get(proj);
  
     //TIMER_START( "AudacityProject::WriteXML", xml_writer_timer );
  
     xmlFile.StartTag(wxT("project"));
     xmlFile.WriteAttr(wxT("xmlns"), wxT("http://audacity.sourceforge.net/xml/"));
  
     xmlFile.WriteAttr(wxT("version"), wxT(AUDACITY_FILE_FORMAT_VERSION));
     xmlFile.WriteAttr(wxT("audacityversion"), AUDACITY_VERSION_STRING);
  
     viewInfo.WriteXMLAttributes(xmlFile);
     xmlFile.WriteAttr(wxT("rate"), settings.GetRate());
     xmlFile.WriteAttr(wxT("snapto"), settings.GetSnapTo() ? wxT("on") : wxT("off"));
     xmlFile.WriteAttr(wxT("selectionformat"),
                       settings.GetSelectionFormat().Internal());
     xmlFile.WriteAttr(wxT("frequencyformat"),
                       settings.GetFrequencySelectionFormatName().Internal());
     xmlFile.WriteAttr(wxT("bandwidthformat"),
                       settings.GetBandwidthSelectionFormatName().Internal());
  
     tags.WriteXML(xmlFile);
  
     unsigned int ndx = 0;
     tracklist.Any().Visit([&](const Track *t)
     {
        auto useTrack = t;
        if ( recording ) {
           // When append-recording, there is a temporary "shadow" track accumulating
           // changes and displayed on the screen but it is not yet part of the
           // regular track list.  That is the one that we want to back up.
           // SubstitutePendingChangedTrack() fetches the shadow, if the track has
           // one, else it gives the same track back.
           useTrack = t->SubstitutePendingChangedTrack().get();
        }
        else if ( useTrack->GetId() == TrackId{} ) {
           // This is a track added during a non-appending recording that is
           // not yet in the undo history.  The UndoManager skips backing it up
           // when pushing.  Don't auto-save it.
           return;
        }
        useTrack->WriteXML(xmlFile);
     });
  
     xmlFile.EndTag(wxT("project"));
  
     //TIMER_STOP( xml_writer_timer );
  }
  
  bool ProjectFileIO::AutoSave(bool recording)
  {
     ProjectSerializer autosave;
     WriteXMLHeader(autosave);
     WriteXML(autosave, recording);
  
     if (WriteDoc("autosave", autosave))
     {
        mModified = true;
        return true;
     }
  
     return false;
  }
  
  bool ProjectFileIO::AutoSaveDelete(sqlite3 *db /* = nullptr */)
  {
     int rc;
  
     if (!db)
     {
        db = DB();
     }
  
     rc = sqlite3_exec(db, "DELETE FROM autosave;", nullptr, nullptr, nullptr);
     if (rc != SQLITE_OK)
     {
        SetDBError(
           XO("Failed to remove the autosave information from the project file.")
        );
        return false;
     }
  
     mModified = false;
  
     return true;
  }
  
  bool ProjectFileIO::WriteDoc(const char *table,
                               const ProjectSerializer &autosave,
                               const char *schema /* = "main" */)
  {
     auto db = DB();
     int rc;
  
     // For now, we always use an ID of 1. This will replace the previously
     // written row every time.
     char sql[256];
     sqlite3_snprintf(sizeof(sql),
                      sql,
                      "INSERT INTO %s.%s(id, dict, doc) VALUES(1, ?1, ?2)"
                      "       ON CONFLICT(id) DO UPDATE SET dict = ?1, doc = ?2;",
                      schema,
                      table);
  
     sqlite3_stmt *stmt = nullptr;
     auto cleanup = finally([&]
     {
        if (stmt)
        {
           sqlite3_finalize(stmt);
        }
     });
  
     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;
     }
  
     const wxMemoryBuffer &dict = autosave.GetDict();
     const wxMemoryBuffer &data = autosave.GetData();
  
     // Bind statement parameters
     // Might return SQL_MISUSE which means it's our mistake that we violated
     // preconditions; should return SQL_OK which is 0
     if (sqlite3_bind_blob(stmt, 1, dict.GetData(), dict.GetDataLen(), SQLITE_STATIC) ||
         sqlite3_bind_blob(stmt, 2, data.GetData(), data.GetDataLen(), SQLITE_STATIC))
     {
        SetDBError(
           XO("Unable to bind to blob")
        );
        return false;
     }
  
     rc = sqlite3_step(stmt);
     if (rc != SQLITE_DONE)
     {
        SetDBError(
           XO("Failed to update the project file.\nThe following command failed:\n\n%s").Format(sql)
        );
        return false;
     }
  
     return true;
  }
  
  bool ProjectFileIO::LoadProject(const FilePath &fileName, bool ignoreAutosave)
  {
     bool success = false;
  
     auto cleanup = finally([&]
     {
        if (!success)
        {
           RestoreConnection();
        }
     });
  
     SaveConnection();
  
     // Open the project file
     if (!OpenConnection(fileName))
     {
        return false;
     }
  
     wxString project;
     wxMemoryBuffer buffer;
     bool usedAutosave = true;
  
     // Get the autosave doc, if any
     if (!ignoreAutosave &&
         !GetBlob("SELECT dict || doc FROM autosave 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)
     {
        usedAutosave = false;
  
        if (!GetBlob("SELECT dict || doc FROM project 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.
     // This should be a recoverable scenario.
     if (buffer.GetDataLen() == 0)
     {
        mRecovered = true;
     }
     else
     {
        project = ProjectSerializer::Decode(buffer);
        if (project.empty())
        {
           SetError(XO("Unable to decode project document"));
  
           return false;
        }
  
        XMLFileReader xmlFile;
  
        // Load 'er up
        success = xmlFile.ParseString(this, project);
        if (!success)
        {
           SetError(
              XO("Unable to parse project information."),
              xmlFile.GetErrorStr()
           );
           return false;
        }
  
        // Check for orphans blocks...sets mRecovered if any were deleted
        
        auto blockids = WaveTrackFactory::Get( mProject )
           .GetSampleBlockFactory()
              ->GetActiveBlockIDs();
        if (blockids.size() > 0)
        {
           success = DeleteBlocks(blockids, true);
           if (!success)
           {
              return false;
           }
        }
     
        // Remember if we used autosave or not
        if (usedAutosave)
        {
           mRecovered = true;
        }
     }
  
     // Mark the project modified if we recovered it
     if (mRecovered)
     {
        mModified = true;
     }
  
     // A previously saved project will have a document in the project table, so
     // we use that knowledge to determine if this file is an unsaved/temporary
     // file or a permanent project file
     wxString result;
     success = GetValue("SELECT Count(*) FROM project;", result);
     if (!success)
     {
        return false;
     }
  
     mTemporary = !result.IsSameAs(wxT("1"));
  
     SetFileName(fileName);
  
     DiscardConnection();
  
     success = true;
  
     return true;
  }
  
  bool ProjectFileIO::UpdateSaved(const TrackList *tracks)
  {
     ProjectSerializer doc;
     WriteXMLHeader(doc);
     WriteXML(doc, false, tracks);
  
     if (!WriteDoc("project", doc))
     {
        return false;
     }
  
     // Autosave no longer needed
     if (!AutoSaveDelete())
     {
        return false;
     }
  
     return true;
  }
  
  // REVIEW: This function is believed to report an error to the user in all cases 
  // of failure.  Callers are believed not to need to do so if they receive 'false'.
  // LLL: All failures checks should now be displaying an error.
  bool ProjectFileIO::SaveProject(
     const FilePath &fileName, const TrackList *lastSaved)
  {
     // In the case where we're saving a temporary project to a permanent project,
     // we'll try to simply rename the project to save a bit of time. We then fall
     // through to the normal Save (not SaveAs) processing.
     if (IsTemporary() && mFileName != fileName)
     {
        FilePath savedName = mFileName;
        if (CloseConnection())
        {
           bool reopened = false;
           bool moved = false;
           if (true == (moved = MoveProject(savedName, fileName)))
           {
              if (OpenConnection(fileName))
                 reopened = true;
              else {
                 MoveProject(fileName, savedName);
                 moved = false; // No longer moved
  
                 reopened = OpenConnection(savedName);
              }
           }
           else {
              // Rename can fail -- if it's to a different device, requiring
              // real copy of contents, which might exhaust space
              reopened = OpenConnection(savedName);
           }
  
           // Warning issued in MoveProject()
           if (reopened && !moved) {
              return false;
           }
  
           if (!reopened) {
              wxTheApp->CallAfter([this]{
                 ShowError(nullptr,
                    XO("Warning"),
                    XO(
  "The project's database failed to reopen, "
  "possibly because of limited space on the storage device."),
                    "Error:_Disk_full_or_not_writable"
                 );
                 wxCommandEvent evt{ EVT_RECONNECTION_FAILURE };
                 mProject.ProcessEvent(evt);
              });
  
              return false;
           }
        }
     }
  
     // If we're saving to a different file than the current one, then copy the
     // current to the new file and make it the active file.
     if (mFileName != fileName)
     {
        // Do NOT prune here since we need to retain the Undo history
        // after we switch to the new file.
        if (!CopyTo(fileName, XO("Saving project"), false))
        {
           ShowError(
              nullptr,
              XO("Error Saving Project"),
              FileException::WriteFailureMessage(fileName),
              "Error:_Disk_full_or_not_writable"
              );
           return false;
        }
  
        // Open the newly created database
        Connection newConn = std::make_unique<DBConnection>(
           mProject.shared_from_this(), mpErrors,
           [this]{ OnCheckpointFailure(); });
  
        // NOTE: There is a noticeable delay here when dealing with large multi-hour
        //       projects that we just created. The delay occurs in Open() when it
        //       calls SafeMode() and is due to the switch from the NONE journal mode
        //       to the WAL journal mode.
        //
        //       So, we do the Open() in a thread and display a progress dialog. Since
        //       this is currently the only known instance where this occurs, we do the
        //       threading here. If more instances are identified, then the threading
        //       should be moved to DBConnection::Open(), wrapping the SafeMode() call
        //       there.
        {
           std::atomic_bool done = {false};
           bool success = true;
           auto thread = std::thread([&]
           {
              auto rc =  newConn->Open(fileName);
              if (rc != SQLITE_OK)
              {
                 // Capture the error string
                 SetError(Verbatim(sqlite3_errstr(rc)));
                 success = false;
              }
              done = true;
           });
  
           // Provides a progress dialog with indeterminate mode
           wxGenericProgressDialog pd(XO("Syncing").Translation(),
                                      XO("This may take several seconds").Translation(),
                                      300000,     // range
                                      nullptr,    // parent
                                      wxPD_APP_MODAL | wxPD_ELAPSED_TIME | wxPD_SMOOTH);
  
           // Wait for the checkpoints to end
           while (!done)
           {
              wxMilliSleep(50);
              pd.Pulse();
           }
           thread.join();
  
           if (!success)
           {
              // Additional help via a Help button links to the manual.
              ShowError(nullptr,
                        XO("Error Saving Project"),
                        XO("The project failed to open, possibly due to limited space\n"
                           "on the storage device.\n\n%s").Format(GetLastError()),
                        "Error:_Disk_full_or_not_writable");
  
              newConn = nullptr;
  
              // Clean up the destination project
              if (!wxRemoveFile(fileName))
              {
                 wxLogMessage("Failed to remove destination project after open failure: %s", fileName);
              }
  
              return false;
           }
        }
  
        // Autosave no longer needed in original project file.
        if (!AutoSaveDelete())
        {
           // Additional help via a Help button links to the manual.
           ShowError(nullptr,
                     XO("Error Saving Project"),
                     XO("Unable to remove autosave information, possibly due to limited space\n"
                        "on the storage device.\n\n%s").Format(GetLastError()),
                    "Error:_Disk_full_or_not_writable");
  
           newConn = nullptr;
  
           // Clean up the destination project
           if (!wxRemoveFile(fileName))
           {
              wxLogMessage("Failed to remove destination project after AutoSaveDelete failure: %s", fileName);
           }
  
           return false;
        }
  
        if (lastSaved) {
           // Bug2605: Be sure not to save orphan blocks
           bool recovered = mRecovered;
           SampleBlockIDSet blockids;
           InspectBlocks( *lastSaved, {}, &blockids );
           // TODO: Not sure what to do if the deletion fails
           DeleteBlocks(blockids, true);
           // Don't set mRecovered if any were deleted
           mRecovered = recovered;
        }
  
        // Try to compact the original project file.
        auto empty = TrackList::Create(&mProject);
        Compact( { lastSaved ? lastSaved : empty.get() }, true );
  
        // Safe to close the original project file now. Not much we can do if this fails,
        // but we should still be in good shape since we'll be switching to the newly
        // saved database below.
        CloseProject();
  
        // And make it the active project file 
        UseConnection(std::move(newConn), fileName);
     }
     else
     {
        if ( !UpdateSaved( nullptr ) ) {
           ShowError(
              nullptr,
              XO("Error Saving Project"),
              FileException::WriteFailureMessage(fileName),
              "Error:_Disk_full_or_not_writable"
              );
           return false;
        }
     }
  
     // Reaching this point defines success and all the rest are no-fail
     // operations:
  
     // No longer modified
     mModified = false;
  
     // No longer recovered
     mRecovered = false;
  
     // No longer a temporary project
     mTemporary = false;
  
     // Adjust the title
     SetProjectTitle();
  
     return true;
  }
  
  bool ProjectFileIO::SaveCopy(const FilePath& fileName)
  {
     return CopyTo(fileName, XO("Backing up project"), false, true,
        {&TrackList::Get(mProject)});
  }
  
  bool ProjectFileIO::OpenProject()
  {
     return OpenConnection();
  }
  
  bool ProjectFileIO::CloseProject()
  {
     auto &currConn = CurrConn();
     if (!currConn)
     {
        wxLogDebug("Closing project with no database connection");
        return true;
     }
  
     // Save the filename since CloseConnection() will clear it
     wxString filename = mFileName;
  
     // Not much we can do if this fails.  The user will simply get
     // the recovery dialog upon next restart.
     if (CloseConnection())
     {
        // If this is a temporary project, we no longer want to keep the
        // project file.
        if (IsTemporary())
        {
           // This is just a safety check.
           wxFileName temp(TempDirectory::TempDir(), wxT(""));
           wxFileName file(filename);
           file.SetFullName(wxT(""));
           if (file == temp)
              RemoveProject(filename);
        }
     }
  
     return true;
  }
  
  bool ProjectFileIO::ReopenProject()
  {
     FilePath fileName = mFileName;
     if (!CloseConnection())
     {
        return false;
     }
  
     return OpenConnection(fileName);
  }
  
  bool ProjectFileIO::IsModified() const
  {
     return mModified;
  }
  
  bool ProjectFileIO::IsTemporary() const
  {
     return mTemporary;
  }
  
  bool ProjectFileIO::IsRecovered() const
  {
     return mRecovered;
  }
  
  wxLongLong ProjectFileIO::GetFreeDiskSpace() const
  {
     wxLongLong freeSpace;
     if (wxGetDiskSpace(wxPathOnly(mFileName), NULL, &freeSpace))
     {
        if (FileNames::IsOnFATFileSystem(mFileName)) {
           // 4 GiB per-file maximum
           constexpr auto limit = 1ll << 32;
  
           // Opening a file only to find its length looks wasteful but
           // seems to be necessary at least on Windows with FAT filesystems.
           // I don't know if that is only a wxWidgets bug.
           auto length = wxFile{mFileName}.Length();
           // auto length = wxFileName::GetSize(mFileName);
  
           if (length == wxInvalidSize)
              length = 0;
           auto free = std::max<wxLongLong>(0, limit - length);
           freeSpace = std::min(freeSpace, free);
        }
        return freeSpace;
     }
  
     return -1;
  }
  
  /// Displays an error dialog with a button that offers help
  void ProjectFileIO::ShowError(wxWindow *parent,
                                const TranslatableString &dlogTitle,
                                const TranslatableString &message,
                                const wxString &helpPage)
  {
     ShowExceptionDialog(parent, dlogTitle, message, helpPage, true,
                     audacity::ToWString(GetLastLog()));
  }
  
  const TranslatableString &ProjectFileIO::GetLastError() const
  {
     return mpErrors->mLastError;
  }
  
  const TranslatableString &ProjectFileIO::GetLibraryError() const
  {
     return mpErrors->mLibraryError;
  }
  
  int ProjectFileIO::GetLastErrorCode() const
  {
      return mpErrors->mErrorCode;
  }
  
  const wxString &ProjectFileIO::GetLastLog() const
  {
      return mpErrors->mLog;
  }
  
  void ProjectFileIO::SetError(
      const TranslatableString& msg, const TranslatableString& libraryError, int errorCode)
  {
     auto &currConn = CurrConn();
     if (currConn)
        currConn->SetError(msg, libraryError, errorCode);
  }
  
  void ProjectFileIO::SetDBError(
     const TranslatableString &msg, const TranslatableString &libraryError, int errorCode)
  {
     auto &currConn = CurrConn();
     if (currConn)
        currConn->SetDBError(msg, libraryError, errorCode);
  }
  
  void ProjectFileIO::SetBypass()
  {
     auto &currConn = CurrConn();
     if (!currConn)
        return;
  
     // Determine if we can bypass sample block deletes during shutdown.
     //
     // IMPORTANT:
     // If the project was compacted, then we MUST bypass further
     // deletions since the new file doesn't have the blocks that the
     // Sequences expect to be there.
  
     currConn->SetBypass( true );
  
     // Only permanent project files need cleaning at shutdown
     if (!IsTemporary() && !WasCompacted())
     {
        // If we still have unused blocks, then we must not bypass deletions
        // during shutdown.  Otherwise, we would have orphaned blocks the next time
        // the project is opened.
        //
        // An example of when dead blocks will exist is when a user opens a permanent
        // project, adds a track (with samples) to it, and chooses not to save the
        // changes.
        if (HadUnused())
        {
           currConn->SetBypass( false );
        }
     }
  
     return;
  }
  
  int64_t ProjectFileIO::GetBlockUsage(SampleBlockID blockid)
  {
     auto pConn = CurrConn().get();
     if (!pConn)
        return 0;
     return GetDiskUsage(*pConn, blockid);
  }
  
  int64_t ProjectFileIO::GetCurrentUsage(
     const std::vector<const TrackList*> &trackLists) const
  {
     unsigned long long current = 0;
     const auto fn = BlockSpaceUsageAccumulator(current);
  
     // Must pass address of this set, even if not otherwise used, to avoid
     // possible multiple count of shared blocks
     SampleBlockIDSet seen;
     for (auto pTracks: trackLists)
        if (pTracks)
           InspectBlocks(*pTracks, fn, &seen);
  
     return current;
  }
  
  int64_t ProjectFileIO::GetTotalUsage()
  {
     auto pConn = CurrConn().get();
     if (!pConn)
        return 0;
     return GetDiskUsage(*pConn, 0);
  }
  
  //
  // Returns the amount of disk space used by the specified sample blockid or all
  // of the sample blocks if the blockid is 0.  It does this by using the raw SQLite
  // pages available from the "sqlite_dbpage" virtual table to traverse the SQLite
  // table b-tree described here:  https://www.sqlite.org/fileformat.html
  //
  int64_t ProjectFileIO::GetDiskUsage(DBConnection &conn, SampleBlockID blockid /* = 0 */)
  {
     // Information we need to track our travels through the b-tree
     typedef struct
     {
        int64_t pgno;
        int currentCell;
        int numCells;
        unsigned char data[65536];
     } page;
     std::vector<page> stack;
  
     int64_t total = 0;
     int64_t found = 0;
     int64_t right = 0;
     int rc;
  
     // Get the rootpage for the sampleblocks table.
     sqlite3_stmt *stmt =
        conn.Prepare(DBConnection::GetRootPage,
                      "SELECT rootpage FROM sqlite_master WHERE tbl_name = 'sampleblocks';");
     if (stmt == nullptr || sqlite3_step(stmt) != SQLITE_ROW)
     {
        return 0;
     }
  
     // And store it in our first stack frame
     stack.push_back({sqlite3_column_int64(stmt, 0)});
  
     // All done with the statement
     sqlite3_clear_bindings(stmt);
     sqlite3_reset(stmt);
  
     // Prepare/retrieve statement to read raw database page
     stmt = conn.Prepare(DBConnection::GetDBPage,
        "SELECT data FROM sqlite_dbpage WHERE pgno = ?1;");
     if (stmt == nullptr)
     {
        return 0;
     }
  
     // Traverse the b-tree until we've visited all of the leaf pages or until
     // we find the one corresponding to the passed in sample blockid. Because we
     // use an integer primary key for the sampleblocks table, the traversal will
     // be in ascending blockid sequence.
     do
     {
        // Acces the top stack frame
        page &pg = stack.back();
  
        // Read the page from the sqlite_dbpage table if it hasn't yet been loaded
        if (pg.numCells == 0)
        {
           // Bind the page number
           sqlite3_bind_int64(stmt, 1, pg.pgno);
  
           // And retrieve the page
           if (sqlite3_step(stmt) != SQLITE_ROW)
           {
              // REVIEW: Likely harmless failure - says size is zero on
              // this error.
              // LLL: Yea, but not much else we can do.
              return 0;
           }
  
           // Copy the page content to the stack frame
           memcpy(&pg.data,
                  sqlite3_column_blob(stmt, 0),
                  sqlite3_column_bytes(stmt, 0));
  
           // And retrieve the total number of cells within it
           pg.numCells = get2(&pg.data[3]);
  
           // Reset statement for next usage
           sqlite3_clear_bindings(stmt);
           sqlite3_reset(stmt);
        }
  
        //wxLogDebug("%*.*spgno %lld currentCell %d numCells %d", (stack.size() - 1) * 2, (stack.size() - 1) * 2, "", pg.pgno, pg.currentCell, pg.numCells);
  
        // Process an interior table b-tree page
        if (pg.data[0] == 0x05)
        {
           // Process the next cell if we haven't examined all of them yet
           if (pg.currentCell < pg.numCells)
           {
              // Remember the right-most leaf page number.
              right = get4(&pg.data[8]);
  
              // Iterate over the cells.
              //
              // If we're not looking for a specific blockid, then we always push the
              // target page onto the stack and leave the loop after a single iteration.
              //
              // Otherwise, we match the blockid against the highest integer key contained
              // within the cell and if the blockid falls within the cell, we stack the
              // page and stop the iteration.
              //
              // In theory, we could do a binary search for a specific blockid here, but
              // because our sample blocks are always large, we will get very few cells
              // per page...usually 6 or less.
              //
              // In both cases, the stacked page can be either an internal or leaf page.
              bool stacked = false;
              while (pg.currentCell < pg.numCells)
              {
                 // Get the offset to this cell using the offset in the cell pointer
                 // array.
                 //
                 // The cell pointer array starts immediately after the page header
                 // at offset 12 and the retrieved offset is from the beginning of
                 // the page.
                 int celloff = get2(&pg.data[12 + (pg.currentCell * 2)]);
  
                 // Bump to the next cell for the next iteration.
                 pg.currentCell++;
  
                 // Get the page number this cell describes
                 int pagenum = get4(&pg.data[celloff]);
  
                 // And the highest integer key, which starts at offset 4 within the cell.
                 int64_t intkey = 0;
                 get_varint(&pg.data[celloff + 4], &intkey);
  
                 //wxLogDebug("%*.*sinternal - right %lld celloff %d pagenum %d intkey %lld", (stack.size() - 1) * 2, (stack.size() - 1) * 2, " ", right, celloff, pagenum, intkey);
  
                 // Stack the described page if we're not looking for a specific blockid
                 // or if this page contains the given blockid.
                 if (!blockid || blockid <= intkey)
                 {
                    stack.push_back({pagenum, 0, 0});
                    stacked = true;
                    break;
                 }
              }
  
              // If we pushed a new page onto the stack, we need to jump back up
              // to read the page
              if (stacked)
              {
                 continue;
              }
           }
  
           // We've exhausted all the cells with this page, so we stack the right-most
           // leaf page.  Ensure we only process it once.
           if (right)
           {
              stack.push_back({right, 0, 0});
              right = 0;
              continue;
           }
        }
        // Process a leaf table b-tree page
        else if (pg.data[0] == 0x0d)
        {
           // Iterate over the cells
           //
           // If we're not looking for a specific blockid, then just accumulate the
           // payload sizes. We will be reading every leaf page in the sampleblocks
           // table.
           //
           // Otherwise we break out when we find the matching blockid. In this case,
           // we only ever look at 1 leaf page.
           bool stop = false;
           for (int i = 0; i < pg.numCells; i++)
           {
              // Get the offset to this cell using the offset in the cell pointer
              // array.
              //
              // The cell pointer array starts immediately after the page header
              // at offset 8 and the retrieved offset is from the beginning of
              // the page.
              int celloff = get2(&pg.data[8 + (i * 2)]);
  
              // Get the total payload size in bytes of the described row.
              int64_t payload = 0;
              int digits = get_varint(&pg.data[celloff], &payload);
  
              // Get the integer key for this row.
              int64_t intkey = 0;
              get_varint(&pg.data[celloff + digits], &intkey);
  
              //wxLogDebug("%*.*sleaf - celloff %4d intkey %lld payload %lld", (stack.size() - 1) * 2, (stack.size() - 1) * 2, " ", celloff, intkey, payload);
  
              // Add this payload size to the total if we're not looking for a specific
              // blockid
              if (!blockid)
              {
                 total += payload;
              }
              // Otherwise, return the payload size for a matching row
              else if (blockid == intkey)
              {
                 return payload;
              }
           }
        }
  
        // Done with the current branch, so pop back up to the previous one (if any)
        stack.pop_back();
     } while (!stack.empty());
  
     // Return the total used for all sample blocks
     return total;
  }
  
  // Retrieves a 2-byte big-endian integer from the page data
  unsigned int ProjectFileIO::get2(const unsigned char *ptr)
  {
     return (ptr[0] << 8) | ptr[1];
  }
  
  // Retrieves a 4-byte big-endian integer from the page data
  unsigned int ProjectFileIO::get4(const unsigned char *ptr)
  {
     return ((unsigned int) ptr[0] << 24) |
            ((unsigned int) ptr[1] << 16) |
            ((unsigned int) ptr[2] << 8)  |
            ((unsigned int) ptr[3]);
  }
  
  // Retrieves a variable length integer from the page data. Returns the
  // number of digits used to encode the integer and the stores the
  // value at the given location.
  int ProjectFileIO::get_varint(const unsigned char *ptr, int64_t *out)
  {
     int64_t val = 0;
     int i;
  
     for (i = 0; i < 8; ++i)
     {
        val = (val << 7) + (ptr[i] & 0x7f);
        if ((ptr[i] & 0x80) == 0)
        {
           *out = val;
           return i + 1;
        }
     }
  
     val = (val << 8) + (ptr[i] & 0xff);
     *out = val;
  
     return 9;
  }
  
  InvisibleTemporaryProject::InvisibleTemporaryProject()
     : mpProject{ std::make_shared< AudacityProject >() }
  {
  }
  
  InvisibleTemporaryProject::~InvisibleTemporaryProject()
  {
     auto &projectFileIO = ProjectFileIO::Get( Project() );
     projectFileIO.SetBypass();
     auto &tracks = TrackList::Get( Project() );
     tracks.Clear();
  
     // Consume some delayed track list related events before destroying the
     // temporary project
     try { wxTheApp->Yield(); } catch(...) {}
  
     // Destroy the project and yield again to let delayed window deletions happen
     projectFileIO.CloseProject();
     mpProject.reset();
     try { wxTheApp->Yield(); } catch(...) {}
  }
  