/*!******************************************************************** Audacity: A Digital Audio Editor @file ImportAUP.cpp @brief Upgrading project file formats from before version 3 *//****************************************************************//** \class AUPImportFileHandle \brief An ImportFileHandle for AUP files (pre-AUP3) *//****************************************************************//** \class AUPImportPlugin \brief An ImportPlugin for AUP files (pre-AUP3) *//*******************************************************************/ #include "../Audacity.h" // for USE_* macros #include "Import.h" #include "ImportPlugin.h" #include "../Envelope.h" #include "../FileFormats.h" #include "../FileNames.h" #include "../LabelTrack.h" #if defined(USE_MIDI) #include "../NoteTrack.h" #endif #include "../Prefs.h" #include "../Project.h" #include "../ProjectFileIO.h" #include "../ProjectFileManager.h" #include "../ProjectHistory.h" #include "../ProjectSelectionManager.h" #include "../ProjectSettings.h" #include "../Sequence.h" #include "../Tags.h" #include "../TimeTrack.h" #include "../ViewInfo.h" #include "../WaveClip.h" #include "../WaveTrack.h" #include "../toolbars/SelectionBar.h" #include "../widgets/AudacityMessageBox.h" #include "../widgets/NumericTextCtrl.h" #include "../widgets/ProgressDialog.h" #include "../xml/XMLFileReader.h" #include "../wxFileNameWrapper.h" #include #define DESC XO("AUP project files (*.aup)") static const auto exts = {wxT("aup")}; #include #include #include #include #include #include class AUPImportFileHandle; using ImportHandle = std::unique_ptr; using NewChannelGroup = std::vector>; class AUPImportPlugin final : public ImportPlugin { public: AUPImportPlugin(); ~AUPImportPlugin(); wxString GetPluginStringID() override; TranslatableString GetPluginFormatDescription() override; ImportHandle Open(const FilePath &fileName, AudacityProject *project) override; }; class AUPImportFileHandle final : public ImportFileHandle, public XMLTagHandler { public: AUPImportFileHandle(const FilePath &name, AudacityProject *project); ~AUPImportFileHandle(); TranslatableString GetFileDescription() override; ByteCount GetFileUncompressedBytes() override; ProgressResult Import(WaveTrackFactory *trackFactory, TrackHolders &outTracks, Tags *tags) override; wxInt32 GetStreamCount() override; const TranslatableStrings &GetStreamInfo() override; void SetStreamUsage(wxInt32 WXUNUSED(StreamID), bool WXUNUSED(Use)) override; bool Open(); private: struct node { wxString parent; wxString tag; XMLTagHandler *handler; }; using stack = std::vector; bool HandleXMLTag(const wxChar *tag, const wxChar **attrs) override; void HandleXMLEndTag(const wxChar *tag) override; XMLTagHandler *HandleXMLChild(const wxChar *tag) override; bool HandleProject(XMLTagHandler *&handle); bool HandleLabelTrack(XMLTagHandler *&handle); bool HandleNoteTrack(XMLTagHandler *&handle); bool HandleTimeTrack(XMLTagHandler *&handle); bool HandleWaveTrack(XMLTagHandler *&handle); bool HandleTags(XMLTagHandler *&handle); bool HandleTag(XMLTagHandler *&handle); bool HandleLabel(XMLTagHandler *&handle); bool HandleWaveClip(XMLTagHandler *&handle); bool HandleSequence(XMLTagHandler *&handle); bool HandleWaveBlock(XMLTagHandler *&handle); bool HandleEnvelope(XMLTagHandler *&handle); bool HandleControlPoint(XMLTagHandler *&handle); bool HandleSimpleBlockFile(XMLTagHandler *&handle); bool HandleSilentBlockFile(XMLTagHandler *&handle); bool HandlePCMAliasBlockFile(XMLTagHandler *&handle); bool HandleImport(XMLTagHandler *&handle); // Called in first pass to collect information about blocks void AddFile(sampleCount len, sampleFormat format, const FilePath &blockFilename = wxEmptyString, const FilePath &audioFilename = wxEmptyString, sampleCount origin = 0, int channel = 0); // These two use the collected file information in a second pass bool AddSilence(sampleCount len); bool AddSamples(const FilePath &blockFilename, const FilePath &audioFilename, sampleCount len, sampleFormat format, sampleCount origin = 0, int channel = 0); bool SetError(const TranslatableString &msg); bool SetWarning(const TranslatableString &msg); private: AudacityProject &mProject; Tags *mTags; // project tag values that will be set in the actual project if the // import is successful #define field(n, t) bool have##n; t n struct { field(vpos, int); field(h, double); field(zoom, double); field(sel0, double); field(sel1, double); #ifdef EXPERIMENTAL_SPECTRAL_EDITING field(selLow, double); field(selHigh, double); #endif field(rate, double); field(snapto, bool); field(selectionformat, wxString); field(audiotimeformat, wxString); field(frequencyformat, wxString); field(bandwidthformat, wxString); } mProjectAttrs; #undef field typedef struct { WaveTrack *track; WaveClip *clip; FilePath blockFile; FilePath audioFile; sampleCount len; sampleFormat format; sampleCount origin; int channel; } fileinfo; std::vector mFiles; sampleCount mTotalSamples; sampleFormat mFormat; unsigned long mNumChannels; stack mHandlers; wxString mParentTag; wxString mCurrentTag; const wxChar **mAttrs; wxFileName mProjDir; using BlockFileMap = std::map>>; BlockFileMap mFileMap; WaveTrack *mWaveTrack; WaveClip *mClip; std::vector mClips; ProgressResult mUpdateResult; TranslatableString mErrorMsg; }; AUPImportPlugin::AUPImportPlugin() : ImportPlugin(FileExtensions(exts.begin(), exts.end())) { } AUPImportPlugin::~AUPImportPlugin() { } wxString AUPImportPlugin::GetPluginStringID() { return wxT("legacyaup"); } TranslatableString AUPImportPlugin::GetPluginFormatDescription() { return DESC; } ImportHandle AUPImportPlugin::Open(const FilePath &fileName, AudacityProject *project) { auto handle = std::make_unique(fileName, project); if (!handle->Open()) { // Error or not something that we recognize return nullptr; } return handle; } static Importer::RegisteredImportPlugin registered { "AUP", std::make_unique() }; AUPImportFileHandle::AUPImportFileHandle(const FilePath &fileName, AudacityProject *project) : ImportFileHandle(fileName), mProject(*project) { } AUPImportFileHandle::~AUPImportFileHandle() { } TranslatableString AUPImportFileHandle::GetFileDescription() { return DESC; } auto AUPImportFileHandle::GetFileUncompressedBytes() -> ByteCount { // TODO: Get Uncompressed byte count. return 0; } ProgressResult AUPImportFileHandle::Import(WaveTrackFactory *WXUNUSED(trackFactory), TrackHolders &WXUNUSED(outTracks), Tags *tags) { auto &history = ProjectHistory::Get(mProject); auto &tracks = TrackList::Get(mProject); auto &viewInfo = ViewInfo::Get(mProject); auto &settings = ProjectSettings::Get(mProject); auto &selman = ProjectSelectionManager::Get(mProject); auto oldNumTracks = tracks.size(); auto cleanup = finally([this, &tracks, oldNumTracks]{ if (mUpdateResult != ProgressResult::Success) { // Revoke additions of tracks while (oldNumTracks < tracks.size()) { Track *lastTrack = *tracks.Any().rbegin(); tracks.Remove(lastTrack); } } }); bool isDirty = history.GetDirty() || !tracks.empty(); mTotalSamples = 0; mTags = tags; CreateProgress(); mUpdateResult = ProgressResult::Success; XMLFileReader xmlFile; bool success = xmlFile.Parse(this, mFilename); if (!success) { AudacityMessageBox( XO("Couldn't import the project:\n\n%s").Format(xmlFile.GetErrorStr()), XO("Import Project"), wxOK | wxCENTRE, &GetProjectFrame(mProject)); return ProgressResult::Failed; } if (!mErrorMsg.empty()) { // Error or warning AudacityMessageBox( mErrorMsg, XO("Import Project"), wxOK | wxCENTRE, &GetProjectFrame(mProject)); if (mUpdateResult == ProgressResult::Failed) { // Error return ProgressResult::Failed; } } // If mUpdateResult had been changed, we would have returned already wxASSERT( mUpdateResult == ProgressResult::Success ); sampleCount processed = 0; for (auto fi : mFiles) { mUpdateResult = mProgress->Update(processed.as_long_long(), mTotalSamples.as_long_long()); if (mUpdateResult != ProgressResult::Success) { return mUpdateResult; } mClip = fi.clip; mWaveTrack = fi.track; if (fi.blockFile.empty()) { AddSilence(fi.len); } else { AddSamples(fi.blockFile, fi.audioFile, fi.len, fi.format, fi.origin, fi.channel); } processed += fi.len; } for (auto pClip : mClips) pClip->UpdateEnvelopeTrackLen(); wxASSERT( mUpdateResult == ProgressResult::Success ); // If the active project is "dirty", then bypass the below updates as we don't // want to going changing things the user may have already set up. if (isDirty) { return mUpdateResult; } if (mProjectAttrs.haverate) { auto &bar = SelectionBar::Get(mProject); bar.SetRate(mProjectAttrs.rate); } if (mProjectAttrs.havesnapto) { selman.AS_SetSnapTo(mProjectAttrs.snapto ? SNAP_NEAREST : SNAP_OFF); } if (mProjectAttrs.haveselectionformat) { selman.AS_SetSelectionFormat(NumericConverter::LookupFormat(NumericConverter::TIME, mProjectAttrs.selectionformat)); } if (mProjectAttrs.haveaudiotimeformat) { selman.TT_SetAudioTimeFormat(NumericConverter::LookupFormat(NumericConverter::TIME, mProjectAttrs.audiotimeformat)); } if (mProjectAttrs.havefrequencyformat) { selman.SSBL_SetFrequencySelectionFormatName(NumericConverter::LookupFormat(NumericConverter::TIME, mProjectAttrs.frequencyformat)); } if (mProjectAttrs.havebandwidthformat) { selman.SSBL_SetBandwidthSelectionFormatName(NumericConverter::LookupFormat(NumericConverter::TIME, mProjectAttrs.bandwidthformat)); } // PRL: It seems this must happen after SetSnapTo if (mProjectAttrs.havevpos) { viewInfo.vpos = mProjectAttrs.vpos; } if (mProjectAttrs.haveh) { viewInfo.h = mProjectAttrs.h; } if (mProjectAttrs.havezoom) { viewInfo.SetZoom(mProjectAttrs.zoom); } if (mProjectAttrs.havesel0) { viewInfo.selectedRegion.setT0(mProjectAttrs.sel0); } if (mProjectAttrs.havesel1) { viewInfo.selectedRegion.setT1(mProjectAttrs.sel1); } #ifdef EXPERIMENTAL_SPECTRAL_EDITING if (mProjectAttrs.haveselLow) { viewInfo.selectedRegion.setF0(mProjectAttrs.selLow); } if (mProjectAttrs.haveselHigh) { viewInfo.selectedRegion.setF1(mProjectAttrs.selHigh); } #endif return mUpdateResult; } wxInt32 AUPImportFileHandle::GetStreamCount() { return 1; } const TranslatableStrings &AUPImportFileHandle::GetStreamInfo() { static TranslatableStrings empty; return empty; } void AUPImportFileHandle::SetStreamUsage(wxInt32 WXUNUSED(StreamID), bool WXUNUSED(Use)) { } bool AUPImportFileHandle::Open() { wxFFile ff(mFilename, wxT("rb")); if (ff.IsOpened()) { char buf[256]; int numRead = ff.Read(buf, sizeof(buf)); ff.Close(); buf[sizeof(buf) - 1] = '\0'; if (!wxStrncmp(buf, wxT("AudacityProject"), 15)) { AudacityMessageBox( XO("This project was saved by Audacity version 1.0 or earlier. The format has\n" "changed and this version of Audacity is unable to import the project.\n\n" "Use a version of Audacity prior to v3.0.0 to upgrade the project and then\n" "you may import it with this version of Audacity."), XO("Import Project"), wxOK | wxCENTRE, &GetProjectFrame(mProject)); return false; } if (wxStrncmp(buf, "HandleXMLEndTag(tag); } mHandlers.pop_back(); if (mHandlers.size()) { node = mHandlers.back(); mParentTag = node.parent; mCurrentTag = node.tag; } } bool AUPImportFileHandle::HandleXMLTag(const wxChar *tag, const wxChar **attrs) { if (mUpdateResult != ProgressResult::Success) { return false; } mParentTag = mCurrentTag; mCurrentTag = tag; mAttrs = attrs; XMLTagHandler *handler = nullptr; bool success = false; if (mCurrentTag.IsSameAs(wxT("project")) || mCurrentTag.IsSameAs(wxT("audacityproject"))) { success = HandleProject(handler); } else if (mCurrentTag.IsSameAs(wxT("labeltrack"))) { success = HandleLabelTrack(handler); } else if (mCurrentTag.IsSameAs(wxT("notetrack"))) { success = HandleNoteTrack(handler); } else if (mCurrentTag.IsSameAs(wxT("timetrack"))) { success = HandleTimeTrack(handler); } else if (mCurrentTag.IsSameAs(wxT("wavetrack"))) { success = HandleWaveTrack(handler); } else if (mCurrentTag.IsSameAs(wxT("tags"))) { success = HandleTags(handler); } else if (mCurrentTag.IsSameAs(wxT("tag"))) { success = HandleTag(handler); } else if (mCurrentTag.IsSameAs(wxT("label"))) { success = HandleLabel(handler); } else if (mCurrentTag.IsSameAs(wxT("waveclip"))) { success = HandleWaveClip(handler); } else if (mCurrentTag.IsSameAs(wxT("sequence"))) { success = HandleSequence(handler); } else if (mCurrentTag.IsSameAs(wxT("waveblock"))) { success = HandleWaveBlock(handler); } else if (mCurrentTag.IsSameAs(wxT("envelope"))) { success = HandleEnvelope(handler); } else if (mCurrentTag.IsSameAs(wxT("controlpoint"))) { success = HandleControlPoint(handler); } else if (mCurrentTag.IsSameAs(wxT("simpleblockfile"))) { success = HandleSimpleBlockFile(handler); } else if (mCurrentTag.IsSameAs(wxT("silentblockfile"))) { success = HandleSilentBlockFile(handler); } else if (mCurrentTag.IsSameAs(wxT("pcmaliasblockfile"))) { success = HandlePCMAliasBlockFile(handler); } else if (mCurrentTag.IsSameAs(wxT("import"))) { success = HandleImport(handler); } if (!success || (handler && !handler->HandleXMLTag(tag, attrs))) { return SetError(XO("Internal error in importer...tag not recognized")); } mHandlers.push_back({mParentTag, mCurrentTag, handler}); return true; } bool AUPImportFileHandle::HandleProject(XMLTagHandler *&handler) { auto &fileMan = ProjectFileManager::Get(mProject); auto &window = GetProjectFrame(mProject); int requiredTags = 0; while (*mAttrs) { const wxChar *attr = *mAttrs++; const wxChar *value = *mAttrs++; long lValue; long long llValue; double dValue; if (!value) { break; } if (!XMLValueChecker::IsGoodString(value)) { return SetError(XO("Invalid project '%s' attribute.").Format(attr)); } wxString strValue = value; #define set(f, v) (mProjectAttrs.have ## f = true, mProjectAttrs.f = v) // ViewInfo if (!wxStrcmp(attr, wxT("vpos"))) { if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&lValue) || (lValue < 0)) { return SetError(XO("Invalid project 'vpos' attribute.")); } set(vpos, (int) lValue); } else if (!wxStrcmp(attr, wxT("h"))) { if (!Internat::CompatibleToDouble(value, &dValue)) { return SetError(XO("Invalid project 'h' attribute.")); } set(h, dValue); } else if (!wxStrcmp(attr, wxT("zoom"))) { if (!Internat::CompatibleToDouble(value, &dValue) || (dValue < 0.0)) { return SetError(XO("Invalid project 'zoom' attribute.")); } set(zoom, dValue); } // Viewinfo.SelectedRegion else if (!wxStrcmp(attr, wxT("sel0"))) { if (!Internat::CompatibleToDouble(value, &dValue) || (dValue < 0.0)) { return SetError(XO("Invalid project 'sel0' attribute.")); } set(sel0, dValue); } else if (!wxStrcmp(attr, wxT("sel1"))) { if (!Internat::CompatibleToDouble(value, &dValue) || (dValue < 0.0)) { return SetError(XO("Invalid project 'sel1' attribute.")); } set(sel1, dValue); } #ifdef EXPERIMENTAL_SPECTRAL_EDITING else if (!wxStrcmp(attr, wxT("selLow"))) { if (!Internat::CompatibleToDouble(value, &dValue) || (dValue < 0.0)) { return SetError(XO("Invalid project 'selLow' attribute.")); } set(selLow, dValue); } else if (!wxStrcmp(attr, wxT("selHigh"))) { if (!Internat::CompatibleToDouble(value, &dValue) || (dValue < 0.0)) { return SetError(XO("Invalid project 'selHigh' attribute.")); } set(selHigh, dValue); } #endif else if (!wxStrcmp(attr, wxT("version"))) { requiredTags++; } else if (!wxStrcmp(attr, wxT("audacityversion"))) { requiredTags++; } else if (!wxStrcmp(attr, wxT("projname"))) { requiredTags++; mProjDir = mFilename; wxString altname = mProjDir.GetName() + wxT("-data"); mProjDir.SetFullName(wxEmptyString); wxString projName = value; bool found = false; // First try to load the data files based on the _data dir given in the .aup file if (!projName.empty()) { mProjDir.AppendDir(projName); if (!mProjDir.DirExists()) { mProjDir.RemoveLastDir(); projName.clear(); } } // If that fails then try to use the filename of the .aup as the base directory // This is because unzipped projects e.g. those that get transferred between mac-pc // may have encoding issues and end up expanding the wrong filenames for certain // international characters (such as capital 'A' with an umlaut.) if (projName.empty()) { projName = altname; mProjDir.AppendDir(projName); if (!mProjDir.DirExists()) { projName.clear(); } } // No luck...complain and bail if (projName.empty()) { AudacityMessageBox( XO("Couldn't find the project data folder: \"%s\"").Format(std::cref(value)), XO("Error Opening Project"), wxOK | wxCENTRE, &window); return false; } // Collect and hash the file names within the project directory wxArrayString files; size_t cnt = wxDir::GetAllFiles(mProjDir.GetFullPath(), &files, "*.*"); for (const auto &fn : files) { mFileMap[wxFileNameFromPath(fn)] = {fn, {}}; } } else if (!wxStrcmp(attr, wxT("rate"))) { if (!Internat::CompatibleToDouble(value, &dValue) || (dValue < 0.0)) { return SetError(XO("Invalid project 'selLow' attribute.")); } set(rate, dValue); } else if (!wxStrcmp(attr, wxT("snapto"))) { set(snapto, (strValue == wxT("on") ? true : false)); } else if (!wxStrcmp(attr, wxT("selectionformat"))) { set(selectionformat, strValue); } else if (!wxStrcmp(attr, wxT("audiotimeformat"))) { set(audiotimeformat, strValue); } else if (!wxStrcmp(attr, wxT("frequencyformat"))) { set(frequencyformat, strValue); } else if (!wxStrcmp(attr, wxT("bandwidthformat"))) { set(bandwidthformat, strValue); } #undef set } if (requiredTags < 3) { return false; } // Do not set the handler - already handled return true; } bool AUPImportFileHandle::HandleLabelTrack(XMLTagHandler *&handler) { handler = TrackList::Get(mProject).Add(std::make_shared()); return true; } bool AUPImportFileHandle::HandleNoteTrack(XMLTagHandler *&handler) { #if defined(USE_MIDI) handler = TrackList::Get(mProject).Add(std::make_shared()); return true; #else AudacityMessageBox( XO("MIDI tracks found in project file, but this build of Audacity does not include MIDI support, bypassing track."), XO("Project Import"), wxOK | wxICON_EXCLAMATION | wxCENTRE, &GetProjectFrame(mProject)); return false; #endif } bool AUPImportFileHandle::HandleTimeTrack(XMLTagHandler *&handler) { auto &tracks = TrackList::Get(mProject); // Bypass this timetrack if the project already has one // (See HandleTimeEnvelope and HandleControlPoint also) if (*tracks.Any().begin()) { 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(mProject)); return true; } auto &viewInfo = ViewInfo::Get( mProject ); handler = TrackList::Get(mProject).Add(std::make_shared(&viewInfo)); return true; } bool AUPImportFileHandle::HandleWaveTrack(XMLTagHandler *&handler) { auto &trackFactory = WaveTrackFactory::Get(mProject); handler = mWaveTrack = TrackList::Get(mProject).Add(trackFactory.NewWaveTrack()); // No active clip. In early versions of Audacity, there was a single // implied clip so we'll create a clip when the first "sequence" is // found. mClip = nullptr; return true; } bool AUPImportFileHandle::HandleTags(XMLTagHandler *&handler) { wxString n; wxString v; // Support for legacy tags while(*mAttrs) { const wxChar *attr = *mAttrs++; const wxChar *value = *mAttrs++; if (!value) { break; } // Ignore empty tags if (!*value) { continue; } if (!XMLValueChecker::IsGoodString(attr) || !XMLValueChecker::IsGoodString(value)) { // Log it??? return false; } if (!wxStrcmp(attr, "id3v2")) { continue; } else if (!wxStrcmp(attr, "track")) { n = wxT("TRACKNUMBER"); } else { n = attr; n.MakeUpper(); } mTags->SetTag(n, value); } // Do not set the handler - already handled return true; } bool AUPImportFileHandle::HandleTag(XMLTagHandler *&handler) { if (!mParentTag.IsSameAs(wxT("tags"))) { return false; } wxString n, v; while (*mAttrs) { wxString attr = *mAttrs++; if (attr.empty()) { break; } wxString value = *mAttrs++; if (!XMLValueChecker::IsGoodString(attr) || !XMLValueChecker::IsGoodString(value)) { break; } if (attr == wxT("name")) { n = value; } else if (attr == wxT("value")) { v = value; } } if (n == wxT("id3v2")) { // LLL: This is obsolete, but it must be handled and ignored. } else { mTags->SetTag(n, v); } // Do not set the handler - already handled return true; } bool AUPImportFileHandle::HandleLabel(XMLTagHandler *&handler) { if (!mParentTag.IsSameAs(wxT("labeltrack"))) { return false; } // The parent handler also handles this tag handler = mHandlers.back().handler; return true; } bool AUPImportFileHandle::HandleWaveClip(XMLTagHandler *&handler) { struct node node = mHandlers.back(); if (mParentTag.IsSameAs(wxT("wavetrack"))) { WaveTrack *wavetrack = static_cast(node.handler); handler = wavetrack->CreateClip(); } else if (mParentTag.IsSameAs(wxT("waveclip"))) { // Nested wave clips are cut lines WaveClip *waveclip = static_cast(node.handler); handler = waveclip->HandleXMLChild(mCurrentTag); } mClip = static_cast(handler); mClips.push_back(mClip); return true; } bool AUPImportFileHandle::HandleEnvelope(XMLTagHandler *&handler) { struct node node = mHandlers.back(); if (mParentTag.IsSameAs(wxT("timetrack"))) { // If an imported timetrack was bypassed, then we want to bypass the // envelope as well. (See HandleTimeTrack and HandleControlPoint) if (node.handler) { TimeTrack *timetrack = static_cast(node.handler); handler = timetrack->GetEnvelope(); } } // Earlier versions of Audacity had a single implied waveclip, so for // these versions, we get or create the only clip in the track. else if (mParentTag.IsSameAs(wxT("wavetrack"))) { handler = mWaveTrack->RightmostOrNewClip()->GetEnvelope(); } // Nested wave clips are cut lines else if (mParentTag.IsSameAs(wxT("waveclip"))) { WaveClip *waveclip = static_cast(node.handler); handler = waveclip->GetEnvelope(); } return true; } bool AUPImportFileHandle::HandleControlPoint(XMLTagHandler *&handler) { struct node node = mHandlers.back(); if (mParentTag.IsSameAs(wxT("envelope"))) { // If an imported timetrack was bypassed, then we want to bypass the // control points as well. (See HandleTimeTrack and HandleEnvelope) if (node.handler) { Envelope *envelope = static_cast(node.handler); handler = envelope->HandleXMLChild(mCurrentTag); } } return true; } bool AUPImportFileHandle::HandleSequence(XMLTagHandler *&handler) { struct node node = mHandlers.back(); WaveClip *waveclip = static_cast(node.handler); // Earlier versions of Audacity had a single implied waveclip, so for // these versions, we get or create the only clip in the track. if (mParentTag.IsSameAs(wxT("wavetrack"))) { XMLTagHandler *dummy; HandleWaveClip(dummy); waveclip = mClip; } while(*mAttrs) { const wxChar *attr = *mAttrs++; const wxChar *value = *mAttrs++; if (!value) { break; } long long nValue = 0; const wxString strValue = value; // promote string, we need this for all if (!wxStrcmp(attr, wxT("maxsamples"))) { // This attribute is a sample count, so can be 64bit if (!XMLValueChecker::IsGoodInt64(strValue) || !strValue.ToLongLong(&nValue) || (nValue < 0)) { return SetError(XO("Invalid sequence 'maxsamples' attribute.")); } // Dominic, 12/10/2006: // Let's check that maxsamples is >= 1024 and <= 64 * 1024 * 1024 // - that's a pretty wide range of reasonable values. if ((nValue < 1024) || (nValue > 64 * 1024 * 1024)) { return SetError(XO("Invalid sequence 'maxsamples' attribute.")); } } else if (!wxStrcmp(attr, wxT("sampleformat"))) { // This attribute is a sample format, normal int long fValue; if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&fValue) || (fValue < 0) || !XMLValueChecker::IsValidSampleFormat(fValue)) { return SetError(XO("Invalid sequence 'sampleformat' attribute.")); } mFormat = (sampleFormat) fValue; waveclip->GetSequence()->ConvertToSampleFormat( mFormat ); } else if (!wxStrcmp(attr, wxT("numsamples"))) { // This attribute is a sample count, so can be 64bit if (!XMLValueChecker::IsGoodInt64(strValue) || !strValue.ToLongLong(&nValue) || (nValue < 0)) { return SetError(XO("Invalid sequence 'numsamples' attribute.")); } } } // Do not set the handler - already handled return true; } bool AUPImportFileHandle::HandleWaveBlock(XMLTagHandler *&handler) { while(*mAttrs) { const wxChar *attr = *mAttrs++; const wxChar *value = *mAttrs++; long long nValue = 0; if (!value) { break; } const wxString strValue = value; if (!wxStrcmp(attr, wxT("start"))) { // making sure that values > 2^31 are OK because long clips will need them. if (!XMLValueChecker::IsGoodInt64(strValue) || !strValue.ToLongLong(&nValue) || (nValue < 0)) { return SetError(XO("Unable to parse the waveblock 'start' attribute")); } } } // Do not set the handler - already handled return true; } bool AUPImportFileHandle::HandleSimpleBlockFile(XMLTagHandler *&handler) { FilePath filename; sampleCount len = 0; while (*mAttrs) { const wxChar *attr = *mAttrs++; const wxChar *value = *mAttrs++; long long nValue; if (!value) { break; } const wxString strValue = value; // Can't use XMLValueChecker::IsGoodFileName here, but do part of its test. if (!wxStricmp(attr, wxT("filename"))) { if (XMLValueChecker::IsGoodFileString(strValue)) { if (mFileMap.find(strValue) != mFileMap.end()) { filename = mFileMap[strValue].first; } else { SetWarning(XO("Missing project file %s\n\nInserting silence instead.") .Format(strValue)); } } } else if (!wxStrcmp(attr, wxT("len"))) { if (!XMLValueChecker::IsGoodInt64(strValue) || !strValue.ToLongLong(&nValue) || (nValue <= 0)) { return SetError(XO("Missing or invalid simpleblockfile 'len' attribute.")); } len = nValue; } } // Do not set the handler - already handled AddFile(len, mFormat, filename, filename); return true; } bool AUPImportFileHandle::HandleSilentBlockFile(XMLTagHandler *&handler) { FilePath filename; sampleCount len = 0; while (*mAttrs) { const wxChar *attr = *mAttrs++; const wxChar *value = *mAttrs++; long long nValue; if (!value) { break; } const wxString strValue = value; if (!wxStrcmp(attr, wxT("len"))) { if (!XMLValueChecker::IsGoodInt64(value) || !strValue.ToLongLong(&nValue) || !(nValue > 0)) { return SetError(XO("Missing or invalid silentblockfile 'len' attribute.")); } len = nValue; } } // Do not set the handler - already handled AddFile(len, mFormat); return true; } bool AUPImportFileHandle::HandlePCMAliasBlockFile(XMLTagHandler *&handler) { wxString summaryFilename; wxFileName filename; sampleCount start = 0; sampleCount len = 0; int channel = 0; wxString name; while (*mAttrs) { const wxChar *attr = *mAttrs++; const wxChar *value = *mAttrs++; long long nValue; if (!value) { break; } const wxString strValue = value; if (!wxStricmp(attr, wxT("aliasfile"))) { if (XMLValueChecker::IsGoodPathName(strValue)) { filename.Assign(strValue); } else if (XMLValueChecker::IsGoodFileName(strValue, mProjDir.GetPath())) { // Allow fallback of looking for the file name, located in the data directory. filename.Assign(mProjDir.GetPath(), strValue); } else if (XMLValueChecker::IsGoodPathString(strValue)) { // If the aliased file is missing, we failed XMLValueChecker::IsGoodPathName() // and XMLValueChecker::IsGoodFileName, because both do existence tests. SetWarning(XO("Missing alias file %s\n\nInserting silence instead.") .Format(strValue)); } } else if (!wxStricmp(attr, wxT("summaryfile"))) { summaryFilename = strValue; } else if (!wxStricmp(attr, wxT("aliasstart"))) { if (!XMLValueChecker::IsGoodInt64(strValue) || !strValue.ToLongLong(&nValue) || (nValue < 0)) { return SetError(XO("Missing or invalid pcmaliasblockfile 'aliasstart' attribute.")); } start = nValue; } else if (!wxStricmp(attr, wxT("aliaslen"))) { if (!XMLValueChecker::IsGoodInt64(strValue) || !strValue.ToLongLong(&nValue) || (nValue <= 0)) { return SetError(XO("Missing or invalid pcmaliasblockfile 'aliaslen' attribute.")); } len = nValue; } else if (!wxStricmp(attr, wxT("aliaschannel"))) { long nValue; if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&nValue) || (nValue < 0)) { return SetError(XO("Missing or invalid pcmaliasblockfile 'aliaslen' attribute.")); } channel = nValue; } } // Do not set the handler - already handled if (filename.IsOk()) AddFile(len, mFormat, summaryFilename, filename.GetFullPath(), start, channel); else AddFile(len, mFormat); // will add silence instead return true; } bool AUPImportFileHandle::HandleImport(XMLTagHandler *&handler) { // Adapted from ImportXMLTagHandler::HandleXMLTag as in version 2.4.2 if (!mAttrs || !(*mAttrs) || wxStrcmp(*mAttrs++, wxT("filename"))) return false; wxString strAttr = *mAttrs; if (!XMLValueChecker::IsGoodPathName(strAttr)) { // Maybe strAttr is just a fileName, not the full path. Try the project data directory. wxFileNameWrapper fileName0{ mFilename }; fileName0.SetExt({}); wxFileNameWrapper fileName{ fileName0.GetFullPath() + wxT("_data"), strAttr }; if (XMLValueChecker::IsGoodFileName(strAttr, fileName.GetPath(wxPATH_GET_VOLUME))) strAttr = fileName.GetFullPath(); else { wxLogWarning(wxT("Could not import file: %s"), strAttr); return false; } } auto &tracks = TrackList::Get(mProject); auto oldNumTracks = tracks.size(); Track *pLast = nullptr; if (oldNumTracks > 0) pLast = *tracks.Any().rbegin(); // Guard this call so that C++ exceptions don't propagate through // the expat library GuardedCall( [&] { ProjectFileManager::Get( mProject ).Import(strAttr, false); }, [&] (AudacityException*) {} ); if (oldNumTracks == tracks.size()) return false; // Handle other attributes, now that we have the tracks. // Apply them to all new wave tracks. ++mAttrs; const wxChar** pAttr; bool bSuccess = true; auto range = tracks.Any(); if (pLast) { range = range.StartingWith(pLast); ++range.first; } for (auto pTrack: range.Filter()) { // Most of the "import" tag attributes are the same as for "wavetrack" tags, // so apply them via WaveTrack::HandleXMLTag(). bSuccess = pTrack->HandleXMLTag(wxT("wavetrack"), mAttrs); // "offset" tag is ignored in WaveTrack::HandleXMLTag except for legacy projects, // so handle it here. double dblValue; pAttr = mAttrs; while (*pAttr) { const wxChar *attr = *pAttr++; const wxChar *value = *pAttr++; const wxString strValue = value; if (!wxStrcmp(attr, wxT("offset")) && XMLValueChecker::IsGoodString(strValue) && Internat::CompatibleToDouble(strValue, &dblValue)) pTrack->SetOffset(dblValue); } } return bSuccess; } void AUPImportFileHandle::AddFile(sampleCount len, sampleFormat format, const FilePath &blockFilename /* = wxEmptyString */, const FilePath &audioFilename /* = wxEmptyString */, sampleCount origin /* = 0 */, int channel /* = 0 */) { fileinfo fi = {}; fi.track = mWaveTrack; fi.clip = mClip; fi.blockFile = blockFilename; fi.audioFile = audioFilename; fi.len = len; fi.format = format, fi.origin = origin, fi.channel = channel; mFiles.push_back(fi); mTotalSamples += len; } bool AUPImportFileHandle::AddSilence(sampleCount len) { wxASSERT(mClip || mWaveTrack); if (mClip) { mClip->InsertSilence(mClip->GetEndTime(), mWaveTrack->LongSamplesToTime(len)); } else if (mWaveTrack) { mWaveTrack->InsertSilence(mWaveTrack->GetEndTime(), mWaveTrack->LongSamplesToTime(len)); } return true; } // All errors that occur here will simply insert silence and allow the // import to continue. bool AUPImportFileHandle::AddSamples(const FilePath &blockFilename, const FilePath &audioFilename, sampleCount len, sampleFormat format, sampleCount origin /* = 0 */, int channel /* = 0 */) { auto pClip = mClip ? mClip : mWaveTrack->RightmostOrNewClip(); auto &pBlock = mFileMap[wxFileNameFromPath(blockFilename)].second; if (pBlock) { // Replicate the sharing of blocks pClip->AppendSharedBlock( pBlock ); return true; } // Third party library has its own type alias, check it before // adding origin + size_t static_assert(sizeof(sampleCount::type) <= sizeof(sf_count_t), "Type sf_count_t is too narrow to hold a sampleCount"); SF_INFO info; memset(&info, 0, sizeof(info)); wxFile f; // will be closed when it goes out of scope SNDFILE *sf = nullptr; bool success = false; auto cleanup = finally([&] { if (!success) { SetWarning(XO("Error while processing %s\n\nInserting silence.").Format(audioFilename)); // If we are unwinding for an exception, don't do another // potentially throwing operation if (!std::uncaught_exception()) // If this does throw, let that propagate, don't guard the call AddSilence(len); } if (sf) { SFCall(sf_close, sf); } }); if (!f.Open(audioFilename)) { SetWarning(XO("Failed to open %s").Format(audioFilename)); return true; } // Even though there is an sf_open() that takes a filename, use the one that // takes a file descriptor since wxWidgets can open a file with a Unicode name and // libsndfile can't (under Windows). sf = SFCall(sf_open_fd, f.fd(), SFM_READ, &info, FALSE); if (!sf) { SetWarning(XO("Failed to open %s").Format(audioFilename)); return true; } if (origin > 0) { if (SFCall(sf_seek, sf, origin.as_long_long(), SEEK_SET) < 0) { SetWarning(XO("Failed to seek to position %lld in %s") .Format(origin.as_long_long(), audioFilename)); return true; } } sf_count_t cnt = len.as_size_t(); int channels = info.channels; wxASSERT(channels >= 1); wxASSERT(channel < channels); SampleBuffer buffer(cnt, format); samplePtr bufptr = buffer.ptr(); size_t framesRead = 0; if (channels == 1 && format == int16Sample && sf_subtype_is_integer(info.format)) { // If both the src and dest formats are integer formats, // read integers directly from the file, conversions not needed framesRead = SFCall(sf_readf_short, sf, (short *) bufptr, cnt); } else if (channels == 1 && format == int24Sample && sf_subtype_is_integer(info.format)) { framesRead = SFCall(sf_readf_int, sf, (int *) bufptr, cnt); if (framesRead != cnt) { SetWarning(XO("Unable to read %lld samples from %s") .Format(cnt, audioFilename)); return true; } // libsndfile gave us the 3 byte sample in the 3 most // significant bytes -- we want it in the 3 least // significant bytes. int *intPtr = (int *) bufptr; for (size_t i = 0; i < framesRead; i++) { intPtr[i] = intPtr[i] >> 8; } } else if (format == int16Sample && !sf_subtype_more_than_16_bits(info.format)) { // Special case: if the file is in 16-bit (or less) format, // and the calling method wants 16-bit data, go ahead and // read 16-bit data directly. This is a pretty common // case, as most audio files are 16-bit. SampleBuffer temp(cnt * channels, int16Sample); short *tmpptr = (short *) temp.ptr(); framesRead = SFCall(sf_readf_short, sf, tmpptr, cnt); if (framesRead != cnt) { SetWarning(XO("Unable to read %lld samples from %s") .Format(cnt, audioFilename)); return true; } for (size_t i = 0; i < framesRead; i++) { ((short *)bufptr)[i] = tmpptr[(channels * i) + channel]; } } else { // Otherwise, let libsndfile handle the conversion and // scaling, and pass us normalized data as floats. We can // then convert to whatever format we want. SampleBuffer tmpbuf(cnt * channels, floatSample); float *tmpptr = (float *) tmpbuf.ptr(); framesRead = SFCall(sf_readf_float, sf, tmpptr, cnt); if (framesRead != cnt) { SetWarning(XO("Unable to read %lld samples from %s") .Format(cnt, audioFilename)); return true; } CopySamples((samplePtr)(tmpptr + channel), floatSample, bufptr, format, framesRead, true /* high quality by default */, channels /* source stride */); } wxASSERT(mClip || mWaveTrack); // Add the samples to the clip/track if (pClip) { pBlock = pClip->AppendNewBlock(bufptr, format, cnt); } // Let the finally block know everything is good success = true; return true; } bool AUPImportFileHandle::SetError(const TranslatableString &msg) { wxLogError(msg.Debug()); if (mErrorMsg.empty() || mUpdateResult == ProgressResult::Success) { mErrorMsg = msg; } // The only place where mUpdateResult is set during XML handling callbacks mUpdateResult = ProgressResult::Failed; return false; } bool AUPImportFileHandle::SetWarning(const TranslatableString &msg) { wxLogWarning(msg.Debug()); if (mErrorMsg.empty()) { mErrorMsg = msg; } return false; }