audacia/src/import/ImportAUP.cpp

1718 lines
45 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 "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 <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) = SelectedRegion::UndefinedFrequency;
field(selHigh, double) = SelectedRegion::UndefinedFrequency;
#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()))
{
static_assert(
sizeof(long long) >= sizeof(uint64_t) &&
sizeof(long) >= sizeof(uint32_t),
"Assumptions about sizes in XMLValueChecker calls are invalid!");
}
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++;
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")))
{
long lValue;
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(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);
// 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;
}
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
long long llvalue;
if (!XMLValueChecker::IsGoodInt64(strValue) || !strValue.ToLongLong(&llvalue) || (llvalue < 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 ((llvalue < 1024) || (llvalue > 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
long long llvalue;
if (!XMLValueChecker::IsGoodInt64(strValue) || !strValue.ToLongLong(&llvalue) || (llvalue < 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++;
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.
long long llvalue;
if (!XMLValueChecker::IsGoodInt64(strValue) || !strValue.ToLongLong(&llvalue) || (llvalue < 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;
size_t len = 0;
while (*mAttrs)
{
const wxChar *attr = *mAttrs++;
const wxChar *value = *mAttrs++;
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")))
{
long lValue;
if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&lValue) || (lValue <= 0))
{
return SetError(XO("Missing or invalid simpleblockfile 'len' attribute."));
}
len = lValue;
}
}
// Do not set the handler - already handled
AddFile(len, mFormat, filename, filename);
return true;
}
bool AUPImportFileHandle::HandleSilentBlockFile(XMLTagHandler *&handler)
{
FilePath filename;
size_t len = 0;
while (*mAttrs)
{
const wxChar *attr = *mAttrs++;
const wxChar *value = *mAttrs++;
if (!value)
{
break;
}
const wxString strValue = value;
if (!wxStrcmp(attr, wxT("len")))
{
long lValue;
if (!XMLValueChecker::IsGoodInt(value) || !strValue.ToLong(&lValue) || !(lValue > 0))
{
return SetError(XO("Missing or invalid silentblockfile 'len' attribute."));
}
len = lValue;
}
}
// Do not set the handler - already handled
AddFile(len, mFormat);
return true;
}
bool AUPImportFileHandle::HandlePCMAliasBlockFile(XMLTagHandler *&handler)
{
wxString summaryFilename;
wxFileName filename;
sampleCount start = 0;
size_t len = 0;
int channel = 0;
wxString name;
while (*mAttrs)
{
const wxChar *attr = *mAttrs++;
const wxChar *value = *mAttrs++;
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")))
{
long long llValue;
if (!XMLValueChecker::IsGoodInt64(strValue) || !strValue.ToLongLong(&llValue) || (llValue < 0))
{
return SetError(XO("Missing or invalid pcmaliasblockfile 'aliasstart' attribute."));
}
start = llValue;
}
else if (!wxStricmp(attr, wxT("aliaslen")))
{
long lValue;
if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&lValue) || (lValue <= 0))
{
return SetError(XO("Missing or invalid pcmaliasblockfile 'aliaslen' attribute."));
}
len = lValue;
}
else if (!wxStricmp(attr, wxT("aliaschannel")))
{
long lValue;
if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&lValue) || (lValue < 0))
{
return SetError(XO("Missing or invalid pcmaliasblockfile 'aliaslen' attribute."));
}
channel = lValue;
}
}
// 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([&]
{
// Do this before any throwing might happen
if (sf)
{
SFCall<int>(sf_close, sf);
}
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 (!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;
// These cases preserve the logic formerly in BlockFile.cpp,
// which was deleted at commit 98d1468.
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
{
/*
Therefore none of the three cases above:
!(channels == 1 && format == int16Sample && sf_subtype_is_integer(info.format))
&&
!(channels == 1 && format == int24Sample && sf_subtype_is_integer(info.format))
&&
!(format == int16Sample && !sf_subtype_more_than_16_bits(info.format))
So format is not 16 bits with wider file format (third conjunct),
but still maybe it is 24 bits with float file format (second conjunct).
*/
// 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;
}
/*
Dithering will happen in CopySamples if format is 24 bits.
Should that be done?
Either the file is an ordinary simple block file -- and presumably the
track was saved specifying a matching format, so format is float and
there is no dithering.
Or else this is the very unusual case of an .auf file, importing PCM data
on demand. The destination format is narrower, requiring dither, only
if the user also specified a narrow format for the track. In such a
case, dithering is right.
*/
CopySamples((samplePtr)(tmpptr + channel),
floatSample,
bufptr,
format,
framesRead,
gHighQualityDither /* 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;
}