/********************************************************************** 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 #include #include #include #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 // 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(""); const wxString Effect::kFactoryDefaultsIdent = wxT(""); 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(¶ms)) { 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(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 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(mNumAudioOut, mNumChannels); std::shared_ptr 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( FindProject() ) // how to remove this const_cast? ); auto trackRange = mTracks->Any() + [&] (const Track *pTrack) { return allSyncLockSelected ? pTrack->IsSelectedOrSyncLockSelected() : track_cast( 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 &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() }; 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 { return std::shared_ptr { 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(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(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(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 ); }