audacia/src/import/ImportAUP.cpp

1679 lines
43 KiB
C++

/*!********************************************************************
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 "../ProjectFileIORegistry.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 <map>
#define DESC XO("AUP project files (*.aup)")
static const auto exts = {wxT("aup")};
#include <wx/dir.h>
#include <wx/ffile.h>
#include <wx/file.h>
#include <wx/frame.h>
#include <wx/string.h>
#include <wx/utils.h>
class AUPImportFileHandle;
using ImportHandle = std::unique_ptr<ImportFileHandle>;
using NewChannelGroup = std::vector<std::shared_ptr<WaveTrack>>;
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<struct node>;
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<fileinfo> mFiles;
sampleCount mTotalSamples;
sampleFormat mFormat;
unsigned long mNumChannels;
stack mHandlers;
wxString mParentTag;
wxString mCurrentTag;
const wxChar **mAttrs;
wxFileName mProjDir;
using BlockFileMap =
std::map<wxString, std::pair<FilePath, std::shared_ptr<SampleBlock>>>;
BlockFileMap mFileMap;
WaveTrack *mWaveTrack;
WaveClip *mClip;
std::vector<WaveClip *> 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<AUPImportFileHandle>(fileName, project);
if (!handle->Open())
{
// Error or not something that we recognize
return nullptr;
}
return handle;
}
static Importer::RegisteredImportPlugin registered
{
"AUP", std::make_unique<AUPImportPlugin>()
};
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, "<?xml", 5) == 0 &&
(wxStrstr(buf, "<audacityproject") ||
wxStrstr(buf, "<project") ))
{
return true;
}
}
return false;
}
XMLTagHandler *AUPImportFileHandle::HandleXMLChild(const wxChar *tag)
{
return this;
}
void AUPImportFileHandle::HandleXMLEndTag(const wxChar *tag)
{
if (mUpdateResult != ProgressResult::Success)
{
return;
}
struct node node = mHandlers.back();
if (wxStrcmp(tag, wxT("waveclip")) == 0)
{
mClip = nullptr;
}
if (node.handler)
{
node.handler->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) || (dValue < 0.0))
{
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(*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<LabelTrack>());
return true;
}
bool AUPImportFileHandle::HandleNoteTrack(XMLTagHandler *&handler)
{
#if defined(USE_MIDI)
handler = TrackList::Get(mProject).Add(std::make_shared<NoteTrack>());
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<TimeTrack>().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<TimeTrack>(&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<WaveTrack *>(node.handler);
handler = wavetrack->CreateClip();
}
else if (mParentTag.IsSameAs(wxT("waveclip")))
{
// Nested wave clips are cut lines
WaveClip *waveclip = static_cast<WaveClip *>(node.handler);
handler = waveclip->HandleXMLChild(mCurrentTag);
}
mClip = static_cast<WaveClip *>(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<TimeTrack *>(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<WaveClip *>(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<Envelope *>(node.handler);
handler = envelope->HandleXMLChild(mCurrentTag);
}
}
return true;
}
bool AUPImportFileHandle::HandleSequence(XMLTagHandler *&handler)
{
struct node node = mHandlers.back();
WaveClip *waveclip = static_cast<WaveClip *>(node.handler);
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<WaveTrack>())
{
// 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<int>(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<SNDFILE*>(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_count_t>(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_count_t>(sf_readf_short, sf, (short *) bufptr, cnt);
}
else if (channels == 1 && format == int24Sample && sf_subtype_is_integer(info.format))
{
framesRead = SFCall<sf_count_t>(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_count_t>(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_count_t>(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;
}