audacia/src/effects/Effect.cpp

2487 lines
60 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
Effect.cpp
Dominic Mazzoni
Vaughan Johnson
Martyn Shaw
*******************************************************************//**
\class Effect
\brief Base class for many of the effects in Audacity.
*//*******************************************************************/
#include "Effect.h"
#include "TimeWarper.h"
#include <algorithm>
#include <wx/defs.h>
#include <wx/sizer.h>
#include <wx/tokenzr.h>
#include "../AudioIO.h"
#include "../DBConnection.h"
#include "../LabelTrack.h"
#include "../Mix.h"
#include "../PluginManager.h"
#include "../ProjectAudioManager.h"
#include "../ProjectFileIO.h"
#include "../ProjectSettings.h"
#include "../prefs/QualitySettings.h"
#include "../ShuttleGui.h"
#include "../Shuttle.h"
#include "../ViewInfo.h"
#include "../WaveTrack.h"
#include "../wxFileNameWrapper.h"
#include "../widgets/ProgressDialog.h"
#include "../tracks/playabletrack/wavetrack/ui/WaveTrackView.h"
#include "../tracks/playabletrack/wavetrack/ui/WaveTrackViewConstants.h"
#include "../widgets/NumericTextCtrl.h"
#include "../widgets/AudacityMessageBox.h"
#include "../widgets/ErrorDialog.h"
#include <unordered_map>
// Effect application counter
int Effect::nEffectsDone=0;
static const int kPlayID = 20102;
static const int kRewindID = 20103;
static const int kFFwdID = 20104;
const wxString Effect::kUserPresetIdent = wxT("User Preset:");
const wxString Effect::kFactoryPresetIdent = wxT("Factory Preset:");
const wxString Effect::kCurrentSettingsIdent = wxT("<Current Settings>");
const wxString Effect::kFactoryDefaultsIdent = wxT("<Factory Defaults>");
using t2bHash = std::unordered_map< void*, bool >;
namespace {
Effect::VetoDialogHook &GetVetoDialogHook()
{
static Effect::VetoDialogHook sHook = nullptr;
return sHook;
}
}
auto Effect::SetVetoDialogHook( VetoDialogHook hook )
-> VetoDialogHook
{
auto &theHook = GetVetoDialogHook();
auto result = theHook;
theHook = hook;
return result;
}
Effect::Effect()
{
mClient = NULL;
mTracks = NULL;
mT0 = 0.0;
mT1 = 0.0;
mDuration = 0.0;
mIsPreview = false;
mIsLinearEffect = false;
mPreviewWithNotSelected = false;
mPreviewFullSelection = false;
mNumTracks = 0;
mNumGroups = 0;
mProgress = NULL;
mUIParent = NULL;
mUIDialog = NULL;
mUIFlags = 0;
mNumAudioIn = 0;
mNumAudioOut = 0;
mBufferSize = 0;
mBlockSize = 0;
mNumChannels = 0;
mUIDebug = false;
// PRL: I think this initialization of mProjectRate doesn't matter
// because it is always reassigned in DoEffect before it is used
// STF: but can't call AudioIOBase::GetOptimalSupportedSampleRate() here.
// (Which is called to compute the default-default value.) (Bug 2280)
mProjectRate = QualitySettings::DefaultSampleRate.ReadWithDefault(44100);
mIsBatch = false;
}
Effect::~Effect()
{
if (mUIDialog)
{
mUIDialog->Close();
}
}
// EffectDefinitionInterface implementation
EffectType Effect::GetType()
{
if (mClient)
{
return mClient->GetType();
}
return EffectTypeNone;
}
PluginPath Effect::GetPath()
{
if (mClient)
{
return mClient->GetPath();
}
return BUILTIN_EFFECT_PREFIX + GetSymbol().Internal();
}
ComponentInterfaceSymbol Effect::GetSymbol()
{
if (mClient)
{
return mClient->GetSymbol();
}
return {};
}
VendorSymbol Effect::GetVendor()
{
if (mClient)
{
return mClient->GetVendor();
}
return XO("Audacity");
}
wxString Effect::GetVersion()
{
if (mClient)
{
return mClient->GetVersion();
}
return AUDACITY_VERSION_STRING;
}
TranslatableString Effect::GetDescription()
{
if (mClient)
{
return mClient->GetDescription();
}
return {};
}
EffectFamilySymbol Effect::GetFamily()
{
if (mClient)
{
return mClient->GetFamily();
}
// Unusually, the internal and visible strings differ for the built-in
// effect family.
return { wxT("Audacity"), XO("Built-in") };
}
bool Effect::IsInteractive()
{
if (mClient)
{
return mClient->IsInteractive();
}
return true;
}
bool Effect::IsDefault()
{
if (mClient)
{
return mClient->IsDefault();
}
return true;
}
bool Effect::IsLegacy()
{
if (mClient)
{
return false;
}
return true;
}
bool Effect::SupportsRealtime()
{
if (mClient)
{
return mClient->SupportsRealtime();
}
return false;
}
bool Effect::SupportsAutomation()
{
if (mClient)
{
return mClient->SupportsAutomation();
}
return true;
}
// EffectClientInterface implementation
bool Effect::SetHost(EffectHostInterface *host)
{
if (mClient)
{
return mClient->SetHost(host);
}
return true;
}
unsigned Effect::GetAudioInCount()
{
if (mClient)
{
return mClient->GetAudioInCount();
}
return 0;
}
unsigned Effect::GetAudioOutCount()
{
if (mClient)
{
return mClient->GetAudioOutCount();
}
return 0;
}
int Effect::GetMidiInCount()
{
if (mClient)
{
return mClient->GetMidiInCount();
}
return 0;
}
int Effect::GetMidiOutCount()
{
if (mClient)
{
return mClient->GetMidiOutCount();
}
return 0;
}
void Effect::SetSampleRate(double rate)
{
if (mClient)
{
mClient->SetSampleRate(rate);
}
mSampleRate = rate;
}
size_t Effect::SetBlockSize(size_t maxBlockSize)
{
if (mClient)
{
return mClient->SetBlockSize(maxBlockSize);
}
mBlockSize = maxBlockSize;
return mBlockSize;
}
size_t Effect::GetBlockSize() const
{
if (mClient)
{
return mClient->GetBlockSize();
}
return mBlockSize;
}
sampleCount Effect::GetLatency()
{
if (mClient)
{
return mClient->GetLatency();
}
return 0;
}
size_t Effect::GetTailSize()
{
if (mClient)
{
return mClient->GetTailSize();
}
return 0;
}
bool Effect::IsReady()
{
if (mClient)
{
return mClient->IsReady();
}
return true;
}
bool Effect::ProcessInitialize(sampleCount totalLen, ChannelNames chanMap)
{
if (mClient)
{
return mClient->ProcessInitialize(totalLen, chanMap);
}
return true;
}
bool Effect::ProcessFinalize()
{
if (mClient)
{
return mClient->ProcessFinalize();
}
return true;
}
size_t Effect::ProcessBlock(float **inBlock, float **outBlock, size_t blockLen)
{
if (mClient)
{
return mClient->ProcessBlock(inBlock, outBlock, blockLen);
}
return 0;
}
bool Effect::RealtimeInitialize()
{
if (mClient)
{
mBlockSize = mClient->SetBlockSize(512);
return mClient->RealtimeInitialize();
}
mBlockSize = 512;
return false;
}
bool Effect::RealtimeAddProcessor(unsigned numChannels, float sampleRate)
{
if (mClient)
{
return mClient->RealtimeAddProcessor(numChannels, sampleRate);
}
return true;
}
bool Effect::RealtimeFinalize()
{
if (mClient)
{
return mClient->RealtimeFinalize();
}
return false;
}
bool Effect::RealtimeSuspend()
{
if (mClient)
return mClient->RealtimeSuspend();
return true;
}
bool Effect::RealtimeResume()
{
if (mClient)
return mClient->RealtimeResume();
return true;
}
bool Effect::RealtimeProcessStart()
{
if (mClient)
{
return mClient->RealtimeProcessStart();
}
return true;
}
size_t Effect::RealtimeProcess(int group,
float **inbuf,
float **outbuf,
size_t numSamples)
{
if (mClient)
{
return mClient->RealtimeProcess(group, inbuf, outbuf, numSamples);
}
return 0;
}
bool Effect::RealtimeProcessEnd()
{
if (mClient)
{
return mClient->RealtimeProcessEnd();
}
return true;
}
bool Effect::ShowInterface(wxWindow &parent,
const EffectDialogFactory &factory, bool forceModal)
{
if (!IsInteractive())
{
return true;
}
if (mUIDialog)
{
if ( mUIDialog->Close(true) )
mUIDialog = nullptr;
return false;
}
if (mClient)
{
return mClient->ShowInterface(parent, factory, forceModal);
}
// mUIDialog is null
auto cleanup = valueRestorer( mUIDialog );
if ( factory )
mUIDialog = factory(parent, this, this);
if (!mUIDialog)
{
return false;
}
mUIDialog->Layout();
mUIDialog->Fit();
mUIDialog->SetMinSize(mUIDialog->GetSize());
auto hook = GetVetoDialogHook();
if( hook && hook( mUIDialog ) )
return false;
if( SupportsRealtime() && !forceModal )
{
mUIDialog->Show();
cleanup.release();
// Return false to bypass effect processing
return false;
}
bool res = mUIDialog->ShowModal() != 0;
return res;
}
bool Effect::GetAutomationParameters(CommandParameters & parms)
{
if (mClient)
{
return mClient->GetAutomationParameters(parms);
}
return true;
}
bool Effect::SetAutomationParameters(CommandParameters & parms)
{
if (mClient)
{
return mClient->SetAutomationParameters(parms);
}
return true;
}
bool Effect::LoadUserPreset(const RegistryPath & name)
{
if (mClient)
{
return mClient->LoadUserPreset(name);
}
wxString parms;
if (!GetPrivateConfig(name, wxT("Parameters"), parms))
{
return false;
}
return SetAutomationParameters(parms);
}
bool Effect::SaveUserPreset(const RegistryPath & name)
{
if (mClient)
{
return mClient->SaveUserPreset(name);
}
wxString parms;
if (!GetAutomationParameters(parms))
{
return false;
}
return SetPrivateConfig(name, wxT("Parameters"), parms);
}
RegistryPaths Effect::GetFactoryPresets()
{
if (mClient)
{
return mClient->GetFactoryPresets();
}
return {};
}
bool Effect::LoadFactoryPreset(int id)
{
if (mClient)
{
return mClient->LoadFactoryPreset(id);
}
return true;
}
bool Effect::LoadFactoryDefaults()
{
if (mClient)
{
return mClient->LoadFactoryDefaults();
}
return LoadUserPreset(GetFactoryDefaultsGroup());
}
// EffectUIClientInterface implementation
void Effect::SetHostUI(EffectUIHostInterface *WXUNUSED(host))
{
}
bool Effect::PopulateUI(ShuttleGui &S)
{
auto parent = S.GetParent();
mUIParent = parent;
mUIParent->PushEventHandler(this);
// LoadUserPreset(GetCurrentSettingsGroup());
PopulateOrExchange(S);
mUIParent->SetMinSize(mUIParent->GetSizer()->GetMinSize());
return true;
}
bool Effect::IsGraphicalUI()
{
return false;
}
bool Effect::ValidateUI()
{
return mUIParent->Validate();
}
bool Effect::HideUI()
{
return true;
}
bool Effect::CloseUI()
{
if (mUIParent)
mUIParent->RemoveEventHandler(this);
mUIParent = NULL;
mUIDialog = NULL;
return true;
}
bool Effect::CanExportPresets()
{
return true;
}
static const FileNames::FileTypes &PresetTypes()
{
static const FileNames::FileTypes result {
{ XO("Presets"), { wxT("txt") }, true },
FileNames::AllFiles
};
return result;
};
void Effect::ExportPresets()
{
wxString params;
GetAutomationParameters(params);
wxString commandId = GetSquashedName(GetSymbol().Internal()).GET();
params = commandId + ":" + params;
auto path = FileNames::SelectFile(FileNames::Operation::Presets,
XO("Export Effect Parameters"),
wxEmptyString,
wxEmptyString,
wxEmptyString,
PresetTypes(),
wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxRESIZE_BORDER,
nullptr);
if (path.empty()) {
return;
}
// Create/Open the file
wxFFile f(path, wxT("wb"));
if (!f.IsOpened())
{
AudacityMessageBox(
XO("Could not open file: \"%s\"").Format( path ),
XO("Error Saving Effect Presets"),
wxICON_EXCLAMATION,
NULL);
return;
}
f.Write(params);
if (f.Error())
{
AudacityMessageBox(
XO("Error writing to file: \"%s\"").Format( path ),
XO("Error Saving Effect Presets"),
wxICON_EXCLAMATION,
NULL);
}
f.Close();
//SetWindowTitle();
}
void Effect::ImportPresets()
{
wxString params;
auto path = FileNames::SelectFile(FileNames::Operation::Presets,
XO("Import Effect Parameters"),
wxEmptyString,
wxEmptyString,
wxEmptyString,
PresetTypes(),
wxFD_OPEN | wxRESIZE_BORDER,
nullptr);
if (path.empty()) {
return;
}
wxFFile f(path);
if (f.IsOpened()) {
if (f.ReadAll(&params)) {
wxString ident = params.BeforeFirst(':');
params = params.AfterFirst(':');
wxString commandId = GetSquashedName(GetSymbol().Internal()).GET();
if (ident != commandId) {
// effect identifiers are a sensible length!
// must also have some params.
if ((params.Length() < 2 ) || (ident.Length() < 2) || (ident.Length() > 30))
{
Effect::MessageBox(
/* i18n-hint %s will be replaced by a file name */
XO("%s: is not a valid presets file.\n")
.Format(wxFileNameFromPath(path)));
}
else
{
Effect::MessageBox(
/* i18n-hint %s will be replaced by a file name */
XO("%s: is for a different Effect, Generator or Analyzer.\n")
.Format(wxFileNameFromPath(path)));
}
return;
}
SetAutomationParameters(params);
}
}
//SetWindowTitle();
}
CommandID Effect::GetSquashedName(wxString name)
{
// Get rid of leading and trailing white space
name.Trim(true).Trim(false);
if (name.empty())
{
return name;
}
wxStringTokenizer st(name, wxT(" "));
wxString id;
// CamelCase the name
while (st.HasMoreTokens())
{
wxString tok = st.GetNextToken();
id += tok.Left(1).MakeUpper() + tok.Mid(1).MakeLower();
}
return id;
}
bool Effect::HasOptions()
{
return false;
}
void Effect::ShowOptions()
{
}
// EffectHostInterface implementation
double Effect::GetDefaultDuration()
{
return 30.0;
}
double Effect::GetDuration()
{
if (mDuration < 0.0)
{
mDuration = 0.0;
}
return mDuration;
}
NumericFormatSymbol Effect::GetDurationFormat()
{
return mDurationFormat;
}
NumericFormatSymbol Effect::GetSelectionFormat()
{
if( !IsBatchProcessing() && FindProject() )
return ProjectSettings::Get( *FindProject() ).GetSelectionFormat();
return NumericConverter::HoursMinsSecondsFormat();
}
void Effect::SetDuration(double seconds)
{
if (seconds < 0.0)
{
seconds = 0.0;
}
if (GetType() == EffectTypeGenerate)
{
SetPrivateConfig(GetCurrentSettingsGroup(), wxT("LastUsedDuration"), seconds);
}
mDuration = seconds;
return;
}
RegistryPath Effect::GetUserPresetsGroup(const RegistryPath & name)
{
RegistryPath group = wxT("UserPresets");
if (!name.empty())
{
group += wxCONFIG_PATH_SEPARATOR + name;
}
return group;
}
RegistryPath Effect::GetCurrentSettingsGroup()
{
return wxT("CurrentSettings");
}
RegistryPath Effect::GetFactoryDefaultsGroup()
{
return wxT("FactoryDefaults");
}
wxString Effect::GetSavedStateGroup()
{
return wxT("SavedState");
}
// ConfigClientInterface implementation
bool Effect::HasSharedConfigGroup(const RegistryPath & group)
{
return PluginManager::Get().HasSharedConfigGroup(GetID(), group);
}
bool Effect::GetSharedConfigSubgroups(const RegistryPath & group, RegistryPaths &subgroups)
{
return PluginManager::Get().GetSharedConfigSubgroups(GetID(), group, subgroups);
}
bool Effect::GetSharedConfig(const RegistryPath & group, const RegistryPath & key, wxString & value, const wxString & defval)
{
return PluginManager::Get().GetSharedConfig(GetID(), group, key, value, defval);
}
bool Effect::GetSharedConfig(const RegistryPath & group, const RegistryPath & key, int & value, int defval)
{
return PluginManager::Get().GetSharedConfig(GetID(), group, key, value, defval);
}
bool Effect::GetSharedConfig(const RegistryPath & group, const RegistryPath & key, bool & value, bool defval)
{
return PluginManager::Get().GetSharedConfig(GetID(), group, key, value, defval);
}
bool Effect::GetSharedConfig(const RegistryPath & group, const RegistryPath & key, float & value, float defval)
{
return PluginManager::Get().GetSharedConfig(GetID(), group, key, value, defval);
}
bool Effect::GetSharedConfig(const RegistryPath & group, const RegistryPath & key, double & value, double defval)
{
return PluginManager::Get().GetSharedConfig(GetID(), group, key, value, defval);
}
bool Effect::SetSharedConfig(const RegistryPath & group, const RegistryPath & key, const wxString & value)
{
return PluginManager::Get().SetSharedConfig(GetID(), group, key, value);
}
bool Effect::SetSharedConfig(const RegistryPath & group, const RegistryPath & key, const int & value)
{
return PluginManager::Get().SetSharedConfig(GetID(), group, key, value);
}
bool Effect::SetSharedConfig(const RegistryPath & group, const RegistryPath & key, const bool & value)
{
return PluginManager::Get().SetSharedConfig(GetID(), group, key, value);
}
bool Effect::SetSharedConfig(const RegistryPath & group, const RegistryPath & key, const float & value)
{
return PluginManager::Get().SetSharedConfig(GetID(), group, key, value);
}
bool Effect::SetSharedConfig(const RegistryPath & group, const RegistryPath & key, const double & value)
{
return PluginManager::Get().SetSharedConfig(GetID(), group, key, value);
}
bool Effect::RemoveSharedConfigSubgroup(const RegistryPath & group)
{
return PluginManager::Get().RemoveSharedConfigSubgroup(GetID(), group);
}
bool Effect::RemoveSharedConfig(const RegistryPath & group, const RegistryPath & key)
{
return PluginManager::Get().RemoveSharedConfig(GetID(), group, key);
}
bool Effect::HasPrivateConfigGroup(const RegistryPath & group)
{
return PluginManager::Get().HasPrivateConfigGroup(GetID(), group);
}
bool Effect::GetPrivateConfigSubgroups(const RegistryPath & group, RegistryPaths & subgroups)
{
return PluginManager::Get().GetPrivateConfigSubgroups(GetID(), group, subgroups);
}
bool Effect::GetPrivateConfig(const RegistryPath & group, const RegistryPath & key, wxString & value, const wxString & defval)
{
return PluginManager::Get().GetPrivateConfig(GetID(), group, key, value, defval);
}
bool Effect::GetPrivateConfig(const RegistryPath & group, const RegistryPath & key, int & value, int defval)
{
return PluginManager::Get().GetPrivateConfig(GetID(), group, key, value, defval);
}
bool Effect::GetPrivateConfig(const RegistryPath & group, const RegistryPath & key, bool & value, bool defval)
{
return PluginManager::Get().GetPrivateConfig(GetID(), group, key, value, defval);
}
bool Effect::GetPrivateConfig(const RegistryPath & group, const RegistryPath & key, float & value, float defval)
{
return PluginManager::Get().GetPrivateConfig(GetID(), group, key, value, defval);
}
bool Effect::GetPrivateConfig(const RegistryPath & group, const RegistryPath & key, double & value, double defval)
{
return PluginManager::Get().GetPrivateConfig(GetID(), group, key, value, defval);
}
bool Effect::SetPrivateConfig(const RegistryPath & group, const RegistryPath & key, const wxString & value)
{
return PluginManager::Get().SetPrivateConfig(GetID(), group, key, value);
}
bool Effect::SetPrivateConfig(const RegistryPath & group, const RegistryPath & key, const int & value)
{
return PluginManager::Get().SetPrivateConfig(GetID(), group, key, value);
}
bool Effect::SetPrivateConfig(const RegistryPath & group, const RegistryPath & key, const bool & value)
{
return PluginManager::Get().SetPrivateConfig(GetID(), group, key, value);
}
bool Effect::SetPrivateConfig(const RegistryPath & group, const RegistryPath & key, const float & value)
{
return PluginManager::Get().SetPrivateConfig(GetID(), group, key, value);
}
bool Effect::SetPrivateConfig(const RegistryPath & group, const RegistryPath & key, const double & value)
{
return PluginManager::Get().SetPrivateConfig(GetID(), group, key, value);
}
bool Effect::RemovePrivateConfigSubgroup(const RegistryPath & group)
{
return PluginManager::Get().RemovePrivateConfigSubgroup(GetID(), group);
}
bool Effect::RemovePrivateConfig(const RegistryPath & group, const RegistryPath & key)
{
return PluginManager::Get().RemovePrivateConfig(GetID(), group, key);
}
// Effect implementation
PluginID Effect::GetID()
{
if (mClient)
{
return PluginManager::GetID(mClient);
}
return PluginManager::GetID(this);
}
bool Effect::Startup(EffectClientInterface *client)
{
// Let destructor know we need to be shutdown
mClient = client;
// Set host so client startup can use our services
if (!SetHost(this))
{
// Bail if the client startup fails
mClient = NULL;
return false;
}
mNumAudioIn = GetAudioInCount();
mNumAudioOut = GetAudioOutCount();
bool haveDefaults;
GetPrivateConfig(GetFactoryDefaultsGroup(), wxT("Initialized"), haveDefaults, false);
if (!haveDefaults)
{
SaveUserPreset(GetFactoryDefaultsGroup());
SetPrivateConfig(GetFactoryDefaultsGroup(), wxT("Initialized"), true);
}
LoadUserPreset(GetCurrentSettingsGroup());
return Startup();
}
bool Effect::Startup()
{
return true;
}
bool Effect::GetAutomationParameters(wxString & parms)
{
CommandParameters eap;
if (mUIDialog && !TransferDataFromWindow())
{
return false;
}
ShuttleGetAutomation S;
S.mpEap = &eap;
if( DefineParams( S ) ){
;// got eap value using DefineParams.
}
// Won't be needed in future
else if (!GetAutomationParameters(eap))
{
return false;
}
return eap.GetParameters(parms);
}
bool Effect::SetAutomationParameters(const wxString & parms)
{
wxString preset = parms;
bool success = false;
if (preset.StartsWith(kUserPresetIdent))
{
preset.Replace(kUserPresetIdent, wxEmptyString, false);
success = LoadUserPreset(GetUserPresetsGroup(preset));
}
else if (preset.StartsWith(kFactoryPresetIdent))
{
preset.Replace(kFactoryPresetIdent, wxEmptyString, false);
auto presets = GetFactoryPresets();
success = LoadFactoryPreset( make_iterator_range( presets ).index( preset ) );
}
else if (preset.StartsWith(kCurrentSettingsIdent))
{
preset.Replace(kCurrentSettingsIdent, wxEmptyString, false);
success = LoadUserPreset(GetCurrentSettingsGroup());
}
else if (preset.StartsWith(kFactoryDefaultsIdent))
{
preset.Replace(kFactoryDefaultsIdent, wxEmptyString, false);
success = LoadUserPreset(GetFactoryDefaultsGroup());
}
else
{
CommandParameters eap(parms);
ShuttleSetAutomation S;
S.SetForValidating( &eap );
// DefineParams returns false if not defined for this effect.
if( !DefineParams( S ) )
// the old method...
success = SetAutomationParameters(eap);
else if( !S.bOK )
success = false;
else{
success = true;
S.SetForWriting( &eap );
DefineParams( S );
}
}
if (!success)
{
Effect::MessageBox(
XO("%s: Could not load settings below. Default settings will be used.\n\n%s")
.Format( GetName(), preset ) );
// We are using default settings and we still wish to continue.
return true;
//return false;
}
if (!mUIDialog)
{
return true;
}
return TransferDataToWindow();
}
RegistryPaths Effect::GetUserPresets()
{
RegistryPaths presets;
GetPrivateConfigSubgroups(GetUserPresetsGroup(wxEmptyString), presets);
std::sort( presets.begin(), presets.end() );
return presets;
}
bool Effect::HasCurrentSettings()
{
return HasPrivateConfigGroup(GetCurrentSettingsGroup());
}
bool Effect::HasFactoryDefaults()
{
return HasPrivateConfigGroup(GetFactoryDefaultsGroup());
}
ManualPageID Effect::ManualPage()
{
return {};
}
FilePath Effect::HelpPage()
{
return {};
}
void Effect::SetUIFlags(unsigned flags) {
mUIFlags = flags;
}
unsigned Effect::TestUIFlags(unsigned mask) {
return mask & mUIFlags;
}
bool Effect::IsBatchProcessing()
{
return mIsBatch;
}
void Effect::SetBatchProcessing(bool start)
{
mIsBatch = start;
if (start)
{
SaveUserPreset(GetSavedStateGroup());
}
else
{
LoadUserPreset(GetSavedStateGroup());
}
}
bool Effect::DoEffect(double projectRate,
TrackList *list,
WaveTrackFactory *factory,
NotifyingSelectedRegion &selectedRegion,
wxWindow *pParent,
const EffectDialogFactory &dialogFactory)
{
wxASSERT(selectedRegion.duration() >= 0.0);
mOutputTracks.reset();
mpSelectedRegion = &selectedRegion;
mFactory = factory;
mProjectRate = projectRate;
mTracks = list;
// This is for performance purposes only, no additional recovery implied
auto &pProject = *const_cast<AudacityProject*>(FindProject()); // how to remove this const_cast?
auto &pIO = ProjectFileIO::Get(pProject);
TransactionScope trans(pIO.GetConnection(), "Effect");
// Update track/group counts
CountWaveTracks();
bool isSelection = false;
mDuration = 0.0;
if (GetType() == EffectTypeGenerate)
{
GetPrivateConfig(GetCurrentSettingsGroup(), wxT("LastUsedDuration"), mDuration, GetDefaultDuration());
}
WaveTrack *newTrack{};
bool success = false;
auto oldDuration = mDuration;
auto cleanup = finally( [&] {
if (!success) {
if (newTrack) {
mTracks->Remove(newTrack);
}
// LastUsedDuration may have been modified by Preview.
SetDuration(oldDuration);
}
else
trans.Commit();
End();
ReplaceProcessedTracks( false );
} );
// We don't yet know the effect type for code in the Nyquist Prompt, so
// assume it requires a track and handle errors when the effect runs.
if ((GetType() == EffectTypeGenerate || GetPath() == NYQUIST_PROMPT_ID) && (mNumTracks == 0)) {
newTrack = mTracks->Add(mFactory->NewWaveTrack());
newTrack->SetSelected(true);
}
mT0 = selectedRegion.t0();
mT1 = selectedRegion.t1();
if (mT1 > mT0)
{
// there is a selection: let's fit in there...
// MJS: note that this is just for the TTC and is independent of the track rate
// but we do need to make sure we have the right number of samples at the project rate
double quantMT0 = QUANTIZED_TIME(mT0, mProjectRate);
double quantMT1 = QUANTIZED_TIME(mT1, mProjectRate);
mDuration = quantMT1 - quantMT0;
isSelection = true;
mT1 = mT0 + mDuration;
}
mDurationFormat = isSelection
? NumericConverter::TimeAndSampleFormat()
: NumericConverter::DefaultSelectionFormat();
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
mF0 = selectedRegion.f0();
mF1 = selectedRegion.f1();
wxArrayString Names;
if( mF0 != SelectedRegion::UndefinedFrequency )
Names.push_back(wxT("control-f0"));
if( mF1 != SelectedRegion::UndefinedFrequency )
Names.push_back(wxT("control-f1"));
SetPresetParameters( &Names, NULL );
#endif
CountWaveTracks();
// Note: Init may read parameters from preferences
if (!Init())
{
return false;
}
// Prompting will be bypassed when applying an effect that has already
// been configured, e.g. repeating the last effect on a different selection.
// Prompting may call Effect::Preview
if ( pParent && dialogFactory &&
IsInteractive() &&
!ShowInterface( *pParent, dialogFactory, IsBatchProcessing() ) )
{
return false;
}
bool returnVal = true;
bool skipFlag = CheckWhetherSkipEffect();
if (skipFlag == false)
{
auto name = GetName();
ProgressDialog progress{
name,
XO("Applying %s...").Format( name ),
pdlgHideStopButton
};
auto vr = valueRestorer( mProgress, &progress );
{
returnVal = Process();
}
}
if (returnVal && (mT1 >= mT0 ))
{
selectedRegion.setTimes(mT0, mT1);
}
success = returnVal;
return returnVal;
}
bool Effect::Delegate(
Effect &delegate, wxWindow &parent, const EffectDialogFactory &factory )
{
NotifyingSelectedRegion region;
region.setTimes( mT0, mT1 );
return delegate.DoEffect( mProjectRate, mTracks, mFactory,
region, &parent, factory );
}
// All legacy effects should have this overridden
bool Effect::Init()
{
return true;
}
int Effect::GetPass()
{
return mPass;
}
bool Effect::InitPass1()
{
return true;
}
bool Effect::InitPass2()
{
return false;
}
bool Effect::Process()
{
CopyInputTracks(true);
bool bGoodResult = true;
// It's possible that the number of channels the effect expects changed based on
// the parameters (the Audacity Reverb effect does when the stereo width is 0).
mNumAudioIn = GetAudioInCount();
mNumAudioOut = GetAudioOutCount();
mPass = 1;
if (InitPass1())
{
bGoodResult = ProcessPass();
mPass = 2;
if (bGoodResult && InitPass2())
{
bGoodResult = ProcessPass();
}
}
ReplaceProcessedTracks(bGoodResult);
return bGoodResult;
}
bool Effect::ProcessPass()
{
bool bGoodResult = true;
bool isGenerator = GetType() == EffectTypeGenerate;
FloatBuffers inBuffer, outBuffer;
ArrayOf<float *> inBufPos, outBufPos;
ChannelName map[3];
mBufferSize = 0;
mBlockSize = 0;
int count = 0;
bool clear = false;
const bool multichannel = mNumAudioIn > 1;
auto range = multichannel
? mOutputTracks->Leaders()
: mOutputTracks->Any();
range.VisitWhile( bGoodResult,
[&](WaveTrack *left, const Track::Fallthrough &fallthrough) {
if (!left->GetSelected())
return fallthrough();
sampleCount len = 0;
sampleCount start = 0;
mNumChannels = 0;
WaveTrack *right{};
// Iterate either over one track which could be any channel,
// or if multichannel, then over all channels of left,
// which is a leader.
for (auto channel :
TrackList::Channels(left).StartingWith(left)) {
if (channel->GetChannel() == Track::LeftChannel)
map[mNumChannels] = ChannelNameFrontLeft;
else if (channel->GetChannel() == Track::RightChannel)
map[mNumChannels] = ChannelNameFrontRight;
else
map[mNumChannels] = ChannelNameMono;
++ mNumChannels;
map[mNumChannels] = ChannelNameEOL;
if (! multichannel)
break;
if (mNumChannels == 2) {
// TODO: more-than-two-channels
right = channel;
clear = false;
// Ignore other channels
break;
}
}
if (!isGenerator)
{
GetBounds(*left, right, &start, &len);
mSampleCnt = len;
}
else
mSampleCnt = left->TimeToLongSamples(mDuration);
// Let the client know the sample rate
SetSampleRate(left->GetRate());
// Get the block size the client wants to use
auto max = left->GetMaxBlockSize() * 2;
mBlockSize = SetBlockSize(max);
// Calculate the buffer size to be at least the max rounded up to the clients
// selected block size.
const auto prevBufferSize = mBufferSize;
mBufferSize = ((max + (mBlockSize - 1)) / mBlockSize) * mBlockSize;
// If the buffer size has changed, then (re)allocate the buffers
if (prevBufferSize != mBufferSize)
{
// Always create the number of input buffers the client expects even if we don't have
// the same number of channels.
inBufPos.reinit( mNumAudioIn );
inBuffer.reinit( mNumAudioIn, mBufferSize );
// We won't be using more than the first 2 buffers, so clear the rest (if any)
for (size_t i = 2; i < mNumAudioIn; i++)
{
for (size_t j = 0; j < mBufferSize; j++)
{
inBuffer[i][j] = 0.0;
}
}
// Always create the number of output buffers the client expects even if we don't have
// the same number of channels.
outBufPos.reinit( mNumAudioOut );
// Output buffers get an extra mBlockSize worth to give extra room if
// the plugin adds latency
outBuffer.reinit( mNumAudioOut, mBufferSize + mBlockSize );
}
// (Re)Set the input buffer positions
for (size_t i = 0; i < mNumAudioIn; i++)
{
inBufPos[i] = inBuffer[i].get();
}
// (Re)Set the output buffer positions
for (size_t i = 0; i < mNumAudioOut; i++)
{
outBufPos[i] = outBuffer[i].get();
}
// Clear unused input buffers
if (!right && !clear && mNumAudioIn > 1)
{
for (size_t j = 0; j < mBufferSize; j++)
{
inBuffer[1][j] = 0.0;
}
clear = true;
}
// Go process the track(s)
bGoodResult = ProcessTrack(
count, map, left, right, start, len,
inBuffer, outBuffer, inBufPos, outBufPos);
if (!bGoodResult)
return;
count++;
},
[&](Track *t) {
if (t->IsSyncLockSelected())
t->SyncLockAdjust(mT1, mT0 + mDuration);
}
);
if (bGoodResult && GetType() == EffectTypeGenerate)
{
mT1 = mT0 + mDuration;
}
return bGoodResult;
}
bool Effect::ProcessTrack(int count,
ChannelNames map,
WaveTrack *left,
WaveTrack *right,
sampleCount start,
sampleCount len,
FloatBuffers &inBuffer,
FloatBuffers &outBuffer,
ArrayOf< float * > &inBufPos,
ArrayOf< float *> &outBufPos)
{
bool rc = true;
// Give the plugin a chance to initialize
if (!ProcessInitialize(len, map))
{
return false;
}
{ // Start scope for cleanup
auto cleanup = finally( [&] {
// Allow the plugin to cleanup
if (!ProcessFinalize())
{
// In case of non-exceptional flow of control, set rc
rc = false;
}
} );
// For each input block of samples, we pass it to the effect along with a
// variable output location. This output location is simply a pointer into a
// much larger buffer. This reduces the number of calls required to add the
// samples to the output track.
//
// Upon return from the effect, the output samples are "moved to the left" by
// the number of samples in the current latency setting, effectively removing any
// delay introduced by the effect.
//
// At the same time the total number of delayed samples are gathered and when
// there is no further input data to process, the loop continues to call the
// effect with an empty input buffer until the effect has had a chance to
// return all of the remaining delayed samples.
auto inPos = start;
auto outPos = start;
auto inputRemaining = len;
decltype(GetLatency()) curDelay = 0, delayRemaining = 0;
decltype(mBlockSize) curBlockSize = 0;
decltype(mBufferSize) inputBufferCnt = 0;
decltype(mBufferSize) outputBufferCnt = 0;
bool cleared = false;
auto chans = std::min<unsigned>(mNumAudioOut, mNumChannels);
std::shared_ptr<WaveTrack> genLeft, genRight;
decltype(len) genLength = 0;
bool isGenerator = GetType() == EffectTypeGenerate;
bool isProcessor = GetType() == EffectTypeProcess;
double genDur = 0;
if (isGenerator)
{
if (mIsPreview) {
gPrefs->Read(wxT("/AudioIO/EffectsPreviewLen"), &genDur, 6.0);
genDur = wxMin(mDuration, CalcPreviewInputLength(genDur));
}
else {
genDur = mDuration;
}
genLength = sampleCount((left->GetRate() * genDur) + 0.5); // round to nearest sample
delayRemaining = genLength;
cleared = true;
// Create temporary tracks
genLeft = left->EmptyCopy();
if (right)
genRight = right->EmptyCopy();
}
// Call the effect until we run out of input or delayed samples
while (inputRemaining != 0 || delayRemaining != 0)
{
// Still working on the input samples
if (inputRemaining != 0)
{
// Need to refill the input buffers
if (inputBufferCnt == 0)
{
// Calculate the number of samples to get
inputBufferCnt =
limitSampleBufferSize( mBufferSize, inputRemaining );
// Fill the input buffers
left->GetFloats(inBuffer[0].get(), inPos, inputBufferCnt);
if (right)
{
right->GetFloats(inBuffer[1].get(), inPos, inputBufferCnt);
}
// Reset the input buffer positions
for (size_t i = 0; i < mNumChannels; i++)
{
inBufPos[i] = inBuffer[i].get();
}
}
// Calculate the number of samples to process
curBlockSize = mBlockSize;
if (curBlockSize > inputRemaining)
{
// We've reached the last block...set current block size to what's left
// inputRemaining is positive and bounded by a size_t
curBlockSize = inputRemaining.as_size_t();
inputRemaining = 0;
// Clear the remainder of the buffers so that a full block can be passed
// to the effect
auto cnt = mBlockSize - curBlockSize;
for (size_t i = 0; i < mNumChannels; i++)
{
for (decltype(cnt) j = 0 ; j < cnt; j++)
{
inBufPos[i][j + curBlockSize] = 0.0;
}
}
// Might be able to use up some of the delayed samples
if (delayRemaining != 0)
{
// Don't use more than needed
cnt = limitSampleBufferSize(cnt, delayRemaining);
delayRemaining -= cnt;
curBlockSize += cnt;
}
}
}
// We've exhausted the input samples and are now working on the delay
else if (delayRemaining != 0)
{
// Calculate the number of samples to process
curBlockSize = limitSampleBufferSize( mBlockSize, delayRemaining );
delayRemaining -= curBlockSize;
// From this point on, we only want to feed zeros to the plugin
if (!cleared)
{
// Reset the input buffer positions
for (size_t i = 0; i < mNumChannels; i++)
{
inBufPos[i] = inBuffer[i].get();
// And clear
for (size_t j = 0; j < mBlockSize; j++)
{
inBuffer[i][j] = 0.0;
}
}
cleared = true;
}
}
// Finally call the plugin to process the block
decltype(curBlockSize) processed;
try
{
processed = ProcessBlock(inBufPos.get(), outBufPos.get(), curBlockSize);
}
catch( const AudacityException & WXUNUSED(e) )
{
// PRL: Bug 437:
// Pass this along to our application-level handler
throw;
}
catch(...)
{
// PRL:
// Exceptions for other reasons, maybe in third-party code...
// Continue treating them as we used to, but I wonder if these
// should now be treated the same way.
return false;
}
wxASSERT(processed == curBlockSize);
wxUnusedVar(processed);
// Bump to next input buffer position
if (inputRemaining != 0)
{
for (size_t i = 0; i < mNumChannels; i++)
{
inBufPos[i] += curBlockSize;
}
inputRemaining -= curBlockSize;
inputBufferCnt -= curBlockSize;
}
// "ls" and "rs" serve as the input sample index for the left and
// right channels when processing the input samples. If we flip
// over to processing delayed samples, they simply become counters
// for the progress display.
inPos += curBlockSize;
// Get the current number of delayed samples and accumulate
if (isProcessor)
{
{
auto delay = GetLatency();
curDelay += delay;
delayRemaining += delay;
}
// If the plugin has delayed the output by more samples than our current
// block size, then we leave the output pointers alone. This effectively
// removes those delayed samples from the output buffer.
if (curDelay >= curBlockSize)
{
curDelay -= curBlockSize;
curBlockSize = 0;
}
// We have some delayed samples, at the beginning of the output samples,
// so overlay them by shifting the remaining output samples.
else if (curDelay > 0)
{
// curDelay is bounded by curBlockSize:
auto delay = curDelay.as_size_t();
curBlockSize -= delay;
for (size_t i = 0; i < chans; i++)
{
memmove(outBufPos[i], outBufPos[i] + delay, sizeof(float) * curBlockSize);
}
curDelay = 0;
}
}
// Adjust the number of samples in the output buffers
outputBufferCnt += curBlockSize;
// Still have room in the output buffers
if (outputBufferCnt < mBufferSize)
{
// Bump to next output buffer position
for (size_t i = 0; i < chans; i++)
{
outBufPos[i] += curBlockSize;
}
}
// Output buffers have filled
else
{
if (isProcessor)
{
// Write them out
left->Set((samplePtr) outBuffer[0].get(), floatSample, outPos, outputBufferCnt);
if (right)
{
if (chans >= 2)
{
right->Set((samplePtr) outBuffer[1].get(), floatSample, outPos, outputBufferCnt);
}
else
{
right->Set((samplePtr) outBuffer[0].get(), floatSample, outPos, outputBufferCnt);
}
}
}
else if (isGenerator)
{
genLeft->Append((samplePtr) outBuffer[0].get(), floatSample, outputBufferCnt);
if (genRight)
{
genRight->Append((samplePtr) outBuffer[1].get(), floatSample, outputBufferCnt);
}
}
// Reset the output buffer positions
for (size_t i = 0; i < chans; i++)
{
outBufPos[i] = outBuffer[i].get();
}
// Bump to the next track position
outPos += outputBufferCnt;
outputBufferCnt = 0;
}
if (mNumChannels > 1)
{
if (TrackGroupProgress(count,
(inPos - start).as_double() /
(isGenerator ? genLength : len).as_double()))
{
rc = false;
break;
}
}
else
{
if (TrackProgress(count,
(inPos - start).as_double() /
(isGenerator ? genLength : len).as_double()))
{
rc = false;
break;
}
}
}
// Put any remaining output
if (rc && outputBufferCnt)
{
if (isProcessor)
{
left->Set((samplePtr) outBuffer[0].get(), floatSample, outPos, outputBufferCnt);
if (right)
{
if (chans >= 2)
{
right->Set((samplePtr) outBuffer[1].get(), floatSample, outPos, outputBufferCnt);
}
else
{
right->Set((samplePtr) outBuffer[0].get(), floatSample, outPos, outputBufferCnt);
}
}
}
else if (isGenerator)
{
genLeft->Append((samplePtr) outBuffer[0].get(), floatSample, outputBufferCnt);
if (genRight)
{
genRight->Append((samplePtr) outBuffer[1].get(), floatSample, outputBufferCnt);
}
}
}
if (rc && isGenerator)
{
auto pProject = FindProject();
// Transfer the data from the temporary tracks to the actual ones
genLeft->Flush();
// mT1 gives us the NEW selection. We want to replace up to GetSel1().
auto &selectedRegion = ViewInfo::Get( *pProject ).selectedRegion;
auto t1 = selectedRegion.t1();
PasteTimeWarper warper{ t1, mT0 + genLeft->GetEndTime() };
left->ClearAndPaste(mT0, t1, genLeft.get(), true, true,
&warper);
if (genRight)
{
genRight->Flush();
right->ClearAndPaste(mT0, selectedRegion.t1(),
genRight.get(), true, true, nullptr /* &warper */);
}
}
} // End scope for cleanup
return rc;
}
void Effect::End()
{
}
void Effect::PopulateOrExchange(ShuttleGui & WXUNUSED(S))
{
return;
}
bool Effect::TransferDataToWindow()
{
return true;
}
bool Effect::TransferDataFromWindow()
{
return true;
}
bool Effect::EnableApply(bool enable)
{
// May be called during initialization, so try to find the dialog
wxWindow *dlg = mUIDialog;
if (!dlg && mUIParent)
{
dlg = wxGetTopLevelParent(mUIParent);
}
if (dlg)
{
wxWindow *apply = dlg->FindWindow(wxID_APPLY);
// Don't allow focus to get trapped
if (!enable)
{
wxWindow *focus = dlg->FindFocus();
if (focus == apply)
{
dlg->FindWindow(wxID_CLOSE)->SetFocus();
}
}
apply->Enable(enable);
}
EnablePreview(enable);
return enable;
}
bool Effect::EnablePreview(bool enable)
{
// May be called during initialization, so try to find the dialog
wxWindow *dlg = mUIDialog;
if (!dlg && mUIParent)
{
dlg = wxGetTopLevelParent(mUIParent);
}
if (dlg)
{
wxWindow *play = dlg->FindWindow(kPlayID);
if (play)
{
wxWindow *rewind = dlg->FindWindow(kRewindID);
wxWindow *ffwd = dlg->FindWindow(kFFwdID);
// Don't allow focus to get trapped
if (!enable)
{
wxWindow *focus = dlg->FindFocus();
if (focus && (focus == play || focus == rewind || focus == ffwd))
{
dlg->FindWindow(wxID_CLOSE)->SetFocus();
}
}
play->Enable(enable);
if (SupportsRealtime())
{
rewind->Enable(enable);
ffwd->Enable(enable);
}
}
}
return enable;
}
void Effect::EnableDebug(bool enable)
{
mUIDebug = enable;
}
void Effect::SetLinearEffectFlag(bool linearEffectFlag)
{
mIsLinearEffect = linearEffectFlag;
}
void Effect::SetPreviewFullSelectionFlag(bool previewDurationFlag)
{
mPreviewFullSelection = previewDurationFlag;
}
void Effect::IncludeNotSelectedPreviewTracks(bool includeNotSelected)
{
mPreviewWithNotSelected = includeNotSelected;
}
bool Effect::TotalProgress(double frac, const TranslatableString &msg)
{
auto updateResult = (mProgress ?
mProgress->Update(frac, msg) :
ProgressResult::Success);
return (updateResult != ProgressResult::Success);
}
bool Effect::TrackProgress(int whichTrack, double frac, const TranslatableString &msg)
{
auto updateResult = (mProgress ?
mProgress->Update(whichTrack + frac, (double) mNumTracks, msg) :
ProgressResult::Success);
return (updateResult != ProgressResult::Success);
}
bool Effect::TrackGroupProgress(int whichGroup, double frac, const TranslatableString &msg)
{
auto updateResult = (mProgress ?
mProgress->Update(whichGroup + frac, (double) mNumGroups, msg) :
ProgressResult::Success);
return (updateResult != ProgressResult::Success);
}
void Effect::GetBounds(
const WaveTrack &track, const WaveTrack *pRight,
sampleCount *start, sampleCount *len)
{
auto t0 = std::max( mT0, track.GetStartTime() );
auto t1 = std::min( mT1, track.GetEndTime() );
if ( pRight ) {
t0 = std::min( t0, std::max( mT0, pRight->GetStartTime() ) );
t1 = std::max( t1, std::min( mT1, pRight->GetEndTime() ) );
}
if (t1 > t0) {
*start = track.TimeToLongSamples(t0);
auto end = track.TimeToLongSamples(t1);
*len = end - *start;
}
else {
*start = 0;
*len = 0;
}
}
//
// private methods
//
// Use this method to copy the input tracks to mOutputTracks, if
// doing the processing on them, and replacing the originals only on success (and not cancel).
// Copy the group tracks that have tracks selected
// If not all sync-locked selected, then only selected wave tracks.
void Effect::CopyInputTracks(bool allSyncLockSelected)
{
// Reset map
mIMap.clear();
mOMap.clear();
mOutputTracks = TrackList::Create(
const_cast<AudacityProject*>( FindProject() ) // how to remove this const_cast?
);
auto trackRange = mTracks->Any() +
[&] (const Track *pTrack) {
return allSyncLockSelected
? pTrack->IsSelectedOrSyncLockSelected()
: track_cast<const WaveTrack*>( pTrack ) && pTrack->GetSelected();
};
t2bHash added;
for (auto aTrack : trackRange)
{
Track *o = mOutputTracks->Add(aTrack->Duplicate());
mIMap.push_back(aTrack);
mOMap.push_back(o);
}
}
Track *Effect::AddToOutputTracks(const std::shared_ptr<Track> &t)
{
mIMap.push_back(NULL);
mOMap.push_back(t.get());
return mOutputTracks->Add(t);
}
Effect::AddedAnalysisTrack::AddedAnalysisTrack(Effect *pEffect, const wxString &name)
: mpEffect(pEffect)
{
LabelTrack::Holder pTrack{ std::make_shared<LabelTrack>() };
mpTrack = pTrack.get();
if (!name.empty())
pTrack->SetName(name);
pEffect->mTracks->Add( pTrack );
}
Effect::AddedAnalysisTrack::AddedAnalysisTrack(AddedAnalysisTrack &&that)
{
mpEffect = that.mpEffect;
mpTrack = that.mpTrack;
that.Commit();
}
void Effect::AddedAnalysisTrack::Commit()
{
mpEffect = nullptr;
}
Effect::AddedAnalysisTrack::~AddedAnalysisTrack()
{
if (mpEffect) {
// not committed -- DELETE the label track
mpEffect->mTracks->Remove(mpTrack);
}
}
auto Effect::AddAnalysisTrack(const wxString &name) -> std::shared_ptr<AddedAnalysisTrack>
{
return std::shared_ptr<AddedAnalysisTrack>
{ safenew AddedAnalysisTrack{ this, name } };
}
Effect::ModifiedAnalysisTrack::ModifiedAnalysisTrack
(Effect *pEffect, const LabelTrack *pOrigTrack, const wxString &name)
: mpEffect(pEffect)
{
// copy LabelTrack here, so it can be undone on cancel
auto newTrack = pOrigTrack->Copy(pOrigTrack->GetStartTime(), pOrigTrack->GetEndTime());
mpTrack = static_cast<LabelTrack*>(newTrack.get());
// Why doesn't LabelTrack::Copy complete the job? :
mpTrack->SetOffset(pOrigTrack->GetStartTime());
if (!name.empty())
mpTrack->SetName(name);
// mpOrigTrack came from mTracks which we own but expose as const to subclasses
// So it's okay that we cast it back to const
mpOrigTrack =
pEffect->mTracks->Replace(const_cast<LabelTrack*>(pOrigTrack),
newTrack );
}
Effect::ModifiedAnalysisTrack::ModifiedAnalysisTrack(ModifiedAnalysisTrack &&that)
{
mpEffect = that.mpEffect;
mpTrack = that.mpTrack;
mpOrigTrack = std::move(that.mpOrigTrack);
that.Commit();
}
void Effect::ModifiedAnalysisTrack::Commit()
{
mpEffect = nullptr;
}
Effect::ModifiedAnalysisTrack::~ModifiedAnalysisTrack()
{
if (mpEffect) {
// not committed -- DELETE the label track
// mpOrigTrack came from mTracks which we own but expose as const to subclasses
// So it's okay that we cast it back to const
mpEffect->mTracks->Replace(mpTrack, mpOrigTrack);
}
}
auto Effect::ModifyAnalysisTrack
(const LabelTrack *pOrigTrack, const wxString &name) -> ModifiedAnalysisTrack
{
return{ this, pOrigTrack, name };
}
// If bGoodResult, replace mTracks tracks with successfully processed mOutputTracks copies.
// Else clear and DELETE mOutputTracks copies.
void Effect::ReplaceProcessedTracks(const bool bGoodResult)
{
if (!bGoodResult) {
// Free resources, unless already freed.
// Processing failed or was cancelled so throw away the processed tracks.
if ( mOutputTracks )
mOutputTracks->Clear();
// Reset map
mIMap.clear();
mOMap.clear();
//TODO:undo the non-gui ODTask transfer
return;
}
// Assume resources need to be freed.
wxASSERT(mOutputTracks); // Make sure we at least did the CopyInputTracks().
auto iterOut = mOutputTracks->ListOfTracks::begin(),
iterEnd = mOutputTracks->ListOfTracks::end();
size_t cnt = mOMap.size();
size_t i = 0;
for (; iterOut != iterEnd; ++i) {
ListOfTracks::value_type o = *iterOut;
// If tracks were removed from mOutputTracks, then there will be
// tracks in the map that must be removed from mTracks.
while (i < cnt && mOMap[i] != o.get()) {
const auto t = mIMap[i];
if (t) {
mTracks->Remove(t);
}
i++;
}
// This should never happen
wxASSERT(i < cnt);
// Remove the track from the output list...don't DELETE it
iterOut = mOutputTracks->erase(iterOut);
const auto t = mIMap[i];
if (t == NULL)
{
// This track is a NEW addition to output tracks; add it to mTracks
mTracks->Add( o );
}
else
{
// Replace mTracks entry with the NEW track
mTracks->Replace(t, o);
}
}
// If tracks were removed from mOutputTracks, then there may be tracks
// left at the end of the map that must be removed from mTracks.
while (i < cnt) {
const auto t = mIMap[i];
if (t) {
mTracks->Remove(t);
}
i++;
}
// Reset map
mIMap.clear();
mOMap.clear();
// Make sure we processed everything
wxASSERT(mOutputTracks->empty());
// The output list is no longer needed
mOutputTracks.reset();
nEffectsDone++;
}
const AudacityProject *Effect::FindProject() const
{
if (!inputTracks())
return nullptr;
return inputTracks()->GetOwner();
}
void Effect::CountWaveTracks()
{
mNumTracks = mTracks->Selected< const WaveTrack >().size();
mNumGroups = mTracks->SelectedLeaders< const WaveTrack >().size();
}
double Effect::CalcPreviewInputLength(double previewLength)
{
return previewLength;
}
bool Effect::IsHidden()
{
return false;
}
void Effect::Preview(bool dryOnly)
{
if (mNumTracks == 0) { // nothing to preview
return;
}
auto gAudioIO = AudioIO::Get();
if (gAudioIO->IsBusy()) {
return;
}
wxWindow *FocusDialog = wxWindow::FindFocus();
double previewDuration;
bool isNyquist = GetFamily() == NYQUISTEFFECTS_FAMILY;
bool isGenerator = GetType() == EffectTypeGenerate;
// Mix a few seconds of audio from all of the tracks
double previewLen;
gPrefs->Read(wxT("/AudioIO/EffectsPreviewLen"), &previewLen, 6.0);
const double rate = mProjectRate;
if (isNyquist && isGenerator) {
previewDuration = CalcPreviewInputLength(previewLen);
}
else {
previewDuration = wxMin(mDuration, CalcPreviewInputLength(previewLen));
}
double t1 = mT0 + previewDuration;
if ((t1 > mT1) && !isGenerator) {
t1 = mT1;
}
if (t1 <= mT0)
return;
bool success = true;
auto cleanup = finally( [&] {
// Effect is already inited; we will call Process, End, and then Init
// again, so the state is exactly the way it was before Preview
// was called.
if (!dryOnly) {
End();
GuardedCall( [&]{ Init(); } );
}
// In case any dialog control depends on mT1 or mDuration:
if ( mUIDialog )
mUIDialog->TransferDataToWindow();
} );
auto vr0 = valueRestorer( mT0 );
auto vr1 = valueRestorer( mT1 );
// Most effects should stop at t1.
if (!mPreviewFullSelection)
mT1 = t1;
// In case any dialog control depends on mT1 or mDuration:
if ( mUIDialog ) {
mUIDialog->TransferDataToWindow();
}
// Save the original track list
TrackList *saveTracks = mTracks;
auto cleanup2 = finally( [&] {
mTracks = saveTracks;
if (FocusDialog) {
FocusDialog->SetFocus();
}
// In case of failed effect, be sure to free memory.
ReplaceProcessedTracks( false );
} );
// Build NEW tracklist from rendering tracks
// Set the same owning project, so FindProject() can see it within Process()
const auto pProject = saveTracks->GetOwner();
auto uTracks = TrackList::Create( pProject );
mTracks = uTracks.get();
// Linear Effect preview optimised by pre-mixing to one track.
// Generators need to generate per track.
if (mIsLinearEffect && !isGenerator) {
WaveTrack::Holder mixLeft, mixRight;
MixAndRender(saveTracks, mFactory, rate, floatSample, mT0, t1, mixLeft, mixRight);
if (!mixLeft)
return;
mixLeft->Offset(-mixLeft->GetStartTime());
mixLeft->SetSelected(true);
WaveTrackView::Get( *mixLeft )
.SetDisplay(WaveTrackViewConstants::NoDisplay);
auto pLeft = mTracks->Add( mixLeft );
Track *pRight{};
if (mixRight) {
mixRight->Offset(-mixRight->GetStartTime());
mixRight->SetSelected(true);
pRight = mTracks->Add( mixRight );
}
mTracks->GroupChannels(*pLeft, pRight ? 2 : 1);
}
else {
for (auto src : saveTracks->Any< const WaveTrack >()) {
if (src->GetSelected() || mPreviewWithNotSelected) {
auto dest = src->Copy(mT0, t1);
dest->SetSelected(src->GetSelected());
WaveTrackView::Get( *static_cast<WaveTrack*>(dest.get()) )
.SetDisplay(WaveTrackViewConstants::NoDisplay);
mTracks->Add( dest );
}
}
}
// NEW tracks start at time zero.
// Adjust mT0 and mT1 to be the times to process, and to
// play back in these tracks
mT1 -= mT0;
mT0 = 0.0;
// Update track/group counts
CountWaveTracks();
// Apply effect
if (!dryOnly) {
ProgressDialog progress{
GetName(),
XO("Preparing preview"),
pdlgHideCancelButton
}; // Have only "Stop" button.
auto vr = valueRestorer( mProgress, &progress );
auto vr2 = valueRestorer( mIsPreview, true );
success = Process();
}
if (success)
{
auto tracks = ProjectAudioManager::GetAllPlaybackTracks(*mTracks, true);
// Some effects (Paulstretch) may need to generate more
// than previewLen, so take the min.
t1 = std::min(mT0 + previewLen, mT1);
// Start audio playing
auto options = DefaultPlayOptions(*pProject);
int token =
gAudioIO->StartStream(tracks, mT0, t1, options);
if (token) {
auto previewing = ProgressResult::Success;
// The progress dialog must be deleted before stopping the stream
// to allow events to flow to the app during StopStream processing.
// The progress dialog blocks these events.
{
ProgressDialog progress
(GetName(), XO("Previewing"), pdlgHideCancelButton);
while (gAudioIO->IsStreamActive(token) && previewing == ProgressResult::Success) {
::wxMilliSleep(100);
previewing = progress.Update(gAudioIO->GetStreamTime() - mT0, t1 - mT0);
}
}
gAudioIO->StopStream();
while (gAudioIO->IsBusy()) {
::wxMilliSleep(100);
}
}
else {
ShowExceptionDialog(FocusDialog, XO("Error"),
XO("Error opening sound device.\nTry changing the audio host, playback device and the project sample rate."),
wxT("Error_opening_sound_device"));
}
}
}
int Effect::MessageBox( const TranslatableString& message,
long style, const TranslatableString &titleStr)
{
auto title = titleStr.empty()
? GetName()
: XO("%s: %s").Format( GetName(), titleStr );
return AudacityMessageBox( message, title, style, mUIParent );
}