mirror of
https://github.com/cookiengineer/audacity
synced 2025-05-02 08:39:46 +02:00
... though harmlessly so. They called to nonstatic member functions of classes with improper this pointers; though the functions did not use this. Bind events to nonmember funtions instead. Sometimes just to empty lambdas to consume the event and ignore it, blocking other handlers.
3970 lines
97 KiB
C++
3970 lines
97 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
VSTEffect.cpp
|
|
|
|
Dominic Mazzoni
|
|
|
|
This class implements a VST Plug-in effect. The plug-in must be
|
|
loaded in a platform-specific way and passed into the constructor,
|
|
but from here this class handles the interfacing.
|
|
|
|
********************************************************************//**
|
|
|
|
\class AEffect
|
|
\brief VST Effects class, conforming to VST layout.
|
|
|
|
*//********************************************************************/
|
|
|
|
//#define VST_DEBUG
|
|
//#define DEBUG_VST
|
|
|
|
// *******************************************************************
|
|
// WARNING: This is NOT 64-bit safe
|
|
// *******************************************************************
|
|
|
|
#include "../../Audacity.h"
|
|
|
|
#if 0
|
|
#if defined(BUILDING_AUDACITY)
|
|
#include "../../Audacity.h"
|
|
#include "../../PlatformCompatibility.h"
|
|
|
|
// Make the main function private
|
|
#define MODULEMAIN_SCOPE static
|
|
#else
|
|
#define MODULEMAIN_SCOPE
|
|
#define USE_VST 1
|
|
#endif
|
|
#endif
|
|
|
|
#if USE_VST
|
|
|
|
#include <limits.h>
|
|
#include <stdio.h>
|
|
|
|
#include <wx/dynlib.h>
|
|
#include <wx/app.h>
|
|
#include <wx/defs.h>
|
|
#include <wx/buffer.h>
|
|
#include <wx/busyinfo.h>
|
|
#include <wx/button.h>
|
|
#include <wx/combobox.h>
|
|
#include <wx/dcclient.h>
|
|
#include <wx/file.h>
|
|
#include <wx/filename.h>
|
|
#include <wx/frame.h>
|
|
#include <wx/imaglist.h>
|
|
#include <wx/listctrl.h>
|
|
#include <wx/log.h>
|
|
#include <wx/module.h>
|
|
#include <wx/process.h>
|
|
#include <wx/progdlg.h>
|
|
#include <wx/recguard.h>
|
|
#include <wx/sizer.h>
|
|
#include <wx/slider.h>
|
|
#include <wx/scrolwin.h>
|
|
#include <wx/sstream.h>
|
|
#include <wx/statbox.h>
|
|
#include <wx/stattext.h>
|
|
#include <wx/timer.h>
|
|
#include <wx/tokenzr.h>
|
|
#include <wx/utils.h>
|
|
|
|
#if defined(__WXMSW__)
|
|
#include <shlwapi.h>
|
|
#pragma comment(lib, "shlwapi")
|
|
#else
|
|
#include <dlfcn.h>
|
|
#endif
|
|
|
|
// TODO: Unfortunately we have some dependencies on Audacity provided
|
|
// dialogs, widgets and other stuff. This will need to be cleaned up.
|
|
|
|
#include "../../FileNames.h"
|
|
#include "../../Internat.h"
|
|
#include "../../PlatformCompatibility.h"
|
|
#include "../../ShuttleGui.h"
|
|
#include "../../effects/Effect.h"
|
|
#include "../../widgets/NumericTextCtrl.h"
|
|
#include "../../widgets/wxPanelWrapper.h"
|
|
#include "../../widgets/valnum.h"
|
|
#include "../../widgets/ErrorDialog.h"
|
|
#include "../../xml/XMLFileReader.h"
|
|
#include "../../xml/XMLWriter.h"
|
|
|
|
#include "audacity/ConfigInterface.h"
|
|
|
|
#include "VSTEffect.h"
|
|
#include "../../MemoryX.h"
|
|
|
|
// NOTE: To debug the subprocess, use wxLogDebug and, on Windows, Debugview
|
|
// from TechNet (Sysinternals).
|
|
|
|
// ============================================================================
|
|
//
|
|
// Module registration entry point
|
|
//
|
|
// This is the symbol that Audacity looks for when the module is built as a
|
|
// dynamic library.
|
|
//
|
|
// When the module is builtin to Audacity, we use the same function, but it is
|
|
// declared static so as not to clash with other builtin modules.
|
|
//
|
|
// ============================================================================
|
|
DECLARE_MODULE_ENTRY(AudacityModule)
|
|
{
|
|
// Create our effects module and register
|
|
// Trust the module manager not to leak this
|
|
return safenew VSTEffectsModule(moduleManager, path);
|
|
}
|
|
|
|
// ============================================================================
|
|
//
|
|
// Register this as a builtin module
|
|
//
|
|
// We also take advantage of the fact that wxModules are initialized before
|
|
// the wxApp::OnInit() method is called. We check to see if Audacity was
|
|
// executed to scan a VST effect in a different process.
|
|
//
|
|
// ============================================================================
|
|
DECLARE_BUILTIN_MODULE(VSTBuiltin);
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///
|
|
/// Auto created at program start up, this initialises VST.
|
|
///
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
class VSTSubEntry final : public wxModule
|
|
{
|
|
public:
|
|
bool OnInit()
|
|
{
|
|
// Have we been started to check a plugin?
|
|
if (wxTheApp && wxTheApp->argc == 3 && wxStrcmp(wxTheApp->argv[1], VSTCMDKEY) == 0)
|
|
{
|
|
// NOTE: This can really hide failures, which is what we want for those pesky
|
|
// VSTs that are bad or that our support isn't currect. But, it can also
|
|
// hide Audacity failures in the subprocess, so if you're having an unruley
|
|
// VST or odd Audacity failures, comment it out and you might get more info.
|
|
//wxHandleFatalExceptions();
|
|
VSTEffectsModule::Check(wxTheApp->argv[2]);
|
|
|
|
// Returning false causes default processing to display a message box, but we don't
|
|
// want that so disable logging.
|
|
wxLog::EnableLogging(false);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
void OnExit() {};
|
|
|
|
DECLARE_DYNAMIC_CLASS(VSTSubEntry)
|
|
};
|
|
IMPLEMENT_DYNAMIC_CLASS(VSTSubEntry, wxModule);
|
|
|
|
//----------------------------------------------------------------------------
|
|
// VSTSubProcess
|
|
//----------------------------------------------------------------------------
|
|
#define OUTPUTKEY wxT("<VSTLOADCHK>-")
|
|
enum InfoKeys
|
|
{
|
|
kKeySubIDs,
|
|
kKeyBegin,
|
|
kKeyName,
|
|
kKeyPath,
|
|
kKeyVendor,
|
|
kKeyVersion,
|
|
kKeyDescription,
|
|
kKeyEffectType,
|
|
kKeyInteractive,
|
|
kKeyAutomatable,
|
|
kKeyEnd
|
|
};
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///
|
|
/// Information about one VST effect.
|
|
///
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
class VSTSubProcess final : public wxProcess,
|
|
public EffectIdentInterface
|
|
{
|
|
public:
|
|
VSTSubProcess()
|
|
{
|
|
Redirect();
|
|
}
|
|
|
|
// EffectClientInterface implementation
|
|
|
|
wxString GetPath() override
|
|
{
|
|
return mPath;
|
|
}
|
|
|
|
wxString GetSymbol() override
|
|
{
|
|
return mName;
|
|
}
|
|
|
|
wxString GetName() override
|
|
{
|
|
return GetSymbol();
|
|
}
|
|
|
|
wxString GetVendor() override
|
|
{
|
|
return mVendor;
|
|
}
|
|
|
|
wxString GetVersion() override
|
|
{
|
|
return mVersion;
|
|
}
|
|
|
|
wxString GetDescription() override
|
|
{
|
|
return mDescription;
|
|
}
|
|
|
|
wxString GetFamily() override
|
|
{
|
|
return VSTPLUGINTYPE;
|
|
}
|
|
|
|
EffectType GetType() override
|
|
{
|
|
return mType;
|
|
}
|
|
|
|
bool IsInteractive() override
|
|
{
|
|
return mInteractive;
|
|
}
|
|
|
|
bool IsDefault() override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool IsLegacy() override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool SupportsRealtime() override
|
|
{
|
|
return mType == EffectTypeProcess;
|
|
}
|
|
|
|
bool SupportsAutomation() override
|
|
{
|
|
return mAutomatable;
|
|
}
|
|
|
|
public:
|
|
wxString mPath;
|
|
wxString mName;
|
|
wxString mVendor;
|
|
wxString mVersion;
|
|
wxString mDescription;
|
|
EffectType mType;
|
|
bool mInteractive;
|
|
bool mAutomatable;
|
|
};
|
|
|
|
// ============================================================================
|
|
//
|
|
// VSTEffectsModule
|
|
//
|
|
// ============================================================================
|
|
VSTEffectsModule::VSTEffectsModule(ModuleManagerInterface *moduleManager,
|
|
const wxString *path)
|
|
{
|
|
mModMan = moduleManager;
|
|
if (path)
|
|
{
|
|
mPath = *path;
|
|
}
|
|
}
|
|
|
|
VSTEffectsModule::~VSTEffectsModule()
|
|
{
|
|
mPath = wxEmptyString;
|
|
}
|
|
|
|
// ============================================================================
|
|
// IdentInterface implementation
|
|
// ============================================================================
|
|
|
|
wxString VSTEffectsModule::GetPath()
|
|
{
|
|
return mPath;
|
|
}
|
|
|
|
wxString VSTEffectsModule::GetSymbol()
|
|
{
|
|
return XO("VST Effects");
|
|
}
|
|
|
|
wxString VSTEffectsModule::GetName()
|
|
{
|
|
return GetSymbol();
|
|
}
|
|
|
|
wxString VSTEffectsModule::GetVendor()
|
|
{
|
|
return XO("The Audacity Team");
|
|
}
|
|
|
|
wxString VSTEffectsModule::GetVersion()
|
|
{
|
|
// This "may" be different if this were to be maintained as a separate DLL
|
|
return AUDACITY_VERSION_STRING;
|
|
}
|
|
|
|
wxString VSTEffectsModule::GetDescription()
|
|
{
|
|
return _("Adds the ability to use VST effects in Audacity.");
|
|
}
|
|
|
|
// ============================================================================
|
|
// ModuleInterface implementation
|
|
// ============================================================================
|
|
|
|
bool VSTEffectsModule::Initialize()
|
|
{
|
|
// Nothing to do here
|
|
return true;
|
|
}
|
|
|
|
void VSTEffectsModule::Terminate()
|
|
{
|
|
// Nothing to do here
|
|
return;
|
|
}
|
|
|
|
wxArrayString VSTEffectsModule::FileExtensions()
|
|
{
|
|
static const wxString ext[] = { _T("vst") };
|
|
static const wxArrayString result{ sizeof(ext)/sizeof(*ext), ext };
|
|
return result;
|
|
}
|
|
|
|
wxString VSTEffectsModule::InstallPath()
|
|
{
|
|
// Not yet ready for VST drag-and-drop...
|
|
// return FileNames::PlugInDir();
|
|
|
|
return {};
|
|
}
|
|
|
|
bool VSTEffectsModule::AutoRegisterPlugins(PluginManagerInterface & WXUNUSED(pm))
|
|
{
|
|
// We don't auto-register
|
|
return true;
|
|
}
|
|
|
|
wxArrayString VSTEffectsModule::FindPluginPaths(PluginManagerInterface & pm)
|
|
{
|
|
wxArrayString pathList;
|
|
wxArrayString files;
|
|
|
|
// Check for the VST_PATH environment variable
|
|
wxString vstpath = wxString::FromUTF8(getenv("VST_PATH"));
|
|
if (!vstpath.empty())
|
|
{
|
|
wxStringTokenizer tok(vstpath);
|
|
while (tok.HasMoreTokens())
|
|
{
|
|
pathList.Add(tok.GetNextToken());
|
|
}
|
|
}
|
|
|
|
#if defined(__WXMAC__)
|
|
#define VSTPATH wxT("/Library/Audio/Plug-Ins/VST")
|
|
|
|
// Look in ~/Library/Audio/Plug-Ins/VST and /Library/Audio/Plug-Ins/VST
|
|
pathList.Add(wxGetHomeDir() + wxFILE_SEP_PATH + VSTPATH);
|
|
pathList.Add(VSTPATH);
|
|
|
|
// Recursively search all paths for Info.plist files. This will identify all
|
|
// bundles.
|
|
pm.FindFilesInPathList(wxT("Info.plist"), pathList, files, true);
|
|
|
|
// Remove the 'Contents/Info.plist' portion of the names
|
|
for (size_t i = 0; i < files.GetCount(); i++)
|
|
{
|
|
files[i] = wxPathOnly(wxPathOnly(files[i]));
|
|
if (!files[i].EndsWith(wxT(".vst")))
|
|
{
|
|
files.RemoveAt(i--);
|
|
}
|
|
}
|
|
|
|
#elif defined(__WXMSW__)
|
|
|
|
TCHAR dpath[MAX_PATH];
|
|
TCHAR tpath[MAX_PATH];
|
|
DWORD len;
|
|
|
|
// Try HKEY_CURRENT_USER registry key first
|
|
len = WXSIZEOF(tpath);
|
|
if (SHRegGetUSValue(wxT("Software\\VST"),
|
|
wxT("VSTPluginsPath"),
|
|
NULL,
|
|
tpath,
|
|
&len,
|
|
FALSE,
|
|
NULL,
|
|
0) == ERROR_SUCCESS)
|
|
{
|
|
tpath[len] = 0;
|
|
dpath[0] = 0;
|
|
ExpandEnvironmentStrings(tpath, dpath, WXSIZEOF(dpath));
|
|
pathList.Add(dpath);
|
|
}
|
|
|
|
// Then try HKEY_LOCAL_MACHINE registry key
|
|
len = WXSIZEOF(tpath);
|
|
if (SHRegGetUSValue(wxT("Software\\VST"),
|
|
wxT("VSTPluginsPath"),
|
|
NULL,
|
|
tpath,
|
|
&len,
|
|
TRUE,
|
|
NULL,
|
|
0) == ERROR_SUCCESS)
|
|
{
|
|
tpath[len] = 0;
|
|
dpath[0] = 0;
|
|
ExpandEnvironmentStrings(tpath, dpath, WXSIZEOF(dpath));
|
|
pathList.Add(dpath);
|
|
}
|
|
|
|
// Add the default path last
|
|
dpath[0] = 0;
|
|
ExpandEnvironmentStrings(wxT("%ProgramFiles%\\Steinberg\\VSTPlugins"),
|
|
dpath,
|
|
WXSIZEOF(dpath));
|
|
pathList.Add(dpath);
|
|
|
|
// Recursively scan for all DLLs
|
|
pm.FindFilesInPathList(wxT("*.dll"), pathList, files, true);
|
|
|
|
#else
|
|
|
|
// Nothing specified in the VST_PATH environment variable...provide defaults
|
|
if (vstpath.IsEmpty())
|
|
{
|
|
// We add this "non-default" one
|
|
pathList.Add(wxT(LIBDIR) wxT("/vst"));
|
|
|
|
// These are the defaults used by other hosts
|
|
pathList.Add(wxT("/usr/lib/vst"));
|
|
pathList.Add(wxT("/usr/local/lib/vst"));
|
|
pathList.Add(wxGetHomeDir() + wxFILE_SEP_PATH + wxT(".vst"));
|
|
}
|
|
|
|
// Recursively scan for all shared objects
|
|
pm.FindFilesInPathList(wxT("*.so"), pathList, files, true);
|
|
|
|
#endif
|
|
|
|
return files;
|
|
}
|
|
|
|
unsigned VSTEffectsModule::DiscoverPluginsAtPath(
|
|
const wxString & path, wxString &errMsg,
|
|
const RegistrationCallback &callback)
|
|
{
|
|
bool error = false;
|
|
unsigned nFound = 0;
|
|
errMsg.clear();
|
|
// TODO: Fix this for external usage
|
|
const wxString &cmdpath = PlatformCompatibility::GetExecutablePath();
|
|
|
|
wxString effectIDs = wxT("0;");
|
|
wxStringTokenizer effectTzr(effectIDs, wxT(";"));
|
|
|
|
Maybe<wxProgressDialog> progress{};
|
|
size_t idCnt = 0;
|
|
size_t idNdx = 0;
|
|
|
|
bool valid = false;
|
|
bool cont = true;
|
|
|
|
while (effectTzr.HasMoreTokens() && cont)
|
|
{
|
|
wxString effectID = effectTzr.GetNextToken();
|
|
|
|
wxString cmd;
|
|
cmd.Printf(wxT("\"%s\" %s \"%s;%s\""), cmdpath, VSTCMDKEY, path, effectID);
|
|
|
|
VSTSubProcess proc;
|
|
try
|
|
{
|
|
int flags = wxEXEC_SYNC | wxEXEC_NODISABLE;
|
|
#if defined(__WXMSW__)
|
|
flags += wxEXEC_NOHIDE;
|
|
#endif
|
|
wxExecute(cmd, flags, &proc);
|
|
}
|
|
catch (...)
|
|
{
|
|
wxLogMessage(_("VST plugin registration failed for %s\n"), path);
|
|
error = true;
|
|
valid = false;
|
|
}
|
|
|
|
wxString output;
|
|
wxStringOutputStream ss(&output);
|
|
proc.GetInputStream()->Read(ss);
|
|
|
|
int keycount = 0;
|
|
bool haveBegin = false;
|
|
wxStringTokenizer tzr(output, wxT("\n"));
|
|
while (tzr.HasMoreTokens())
|
|
{
|
|
wxString line = tzr.GetNextToken();
|
|
|
|
// Our output may follow any output the plugin may have written.
|
|
if (!line.StartsWith(OUTPUTKEY))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
long key;
|
|
if (!line.Mid(wxStrlen(OUTPUTKEY)).BeforeFirst(wxT('=')).ToLong(&key))
|
|
{
|
|
continue;
|
|
}
|
|
wxString val = line.AfterFirst(wxT('=')).BeforeFirst(wxT('\r'));
|
|
|
|
switch (key)
|
|
{
|
|
case kKeySubIDs:
|
|
effectIDs = val;
|
|
effectTzr.Reinit(effectIDs);
|
|
idCnt = effectTzr.CountTokens();
|
|
if (idCnt > 3)
|
|
{
|
|
progress.create( _("Scanning Shell VST"),
|
|
wxString::Format(_("Registering %d of %d: %-64.64s"), 0, idCnt, proc.GetName()),
|
|
static_cast<int>(idCnt),
|
|
nullptr,
|
|
wxPD_APP_MODAL |
|
|
wxPD_AUTO_HIDE |
|
|
wxPD_CAN_ABORT |
|
|
wxPD_ELAPSED_TIME |
|
|
wxPD_ESTIMATED_TIME |
|
|
wxPD_REMAINING_TIME );
|
|
progress->Show();
|
|
}
|
|
break;
|
|
|
|
case kKeyBegin:
|
|
haveBegin = true;
|
|
keycount++;
|
|
break;
|
|
|
|
case kKeyName:
|
|
proc.mName = val;
|
|
keycount++;
|
|
break;
|
|
|
|
case kKeyPath:
|
|
proc.mPath = val;
|
|
keycount++;
|
|
break;
|
|
|
|
case kKeyVendor:
|
|
proc.mVendor = val;
|
|
keycount++;
|
|
break;
|
|
|
|
case kKeyVersion:
|
|
proc.mVersion = val;
|
|
keycount++;
|
|
break;
|
|
|
|
case kKeyDescription:
|
|
proc.mDescription = val;
|
|
keycount++;
|
|
break;
|
|
|
|
case kKeyEffectType:
|
|
long type;
|
|
val.ToLong(&type);
|
|
proc.mType = (EffectType) type;
|
|
keycount++;
|
|
break;
|
|
|
|
case kKeyInteractive:
|
|
proc.mInteractive = val.IsSameAs(wxT("1"));
|
|
keycount++;
|
|
break;
|
|
|
|
case kKeyAutomatable:
|
|
proc.mAutomatable = val.IsSameAs(wxT("1"));
|
|
keycount++;
|
|
break;
|
|
|
|
case kKeyEnd:
|
|
{
|
|
if (!haveBegin || ++keycount != kKeyEnd)
|
|
{
|
|
keycount = 0;
|
|
haveBegin = false;
|
|
continue;
|
|
}
|
|
|
|
bool skip = false;
|
|
if (progress)
|
|
{
|
|
idNdx++;
|
|
cont = progress->Update(idNdx,
|
|
wxString::Format(_("Registering %d of %d: %-64.64s"), idNdx, idCnt, proc.GetName()));
|
|
}
|
|
|
|
if (!skip && cont)
|
|
{
|
|
valid = true;
|
|
if (callback)
|
|
callback( this, &proc );
|
|
++nFound;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
keycount = 0;
|
|
haveBegin = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (error)
|
|
errMsg = _("Could not load the library");
|
|
|
|
return nFound;
|
|
}
|
|
|
|
bool VSTEffectsModule::IsPluginValid(const wxString & path, bool bFast)
|
|
{
|
|
if( bFast )
|
|
return true;
|
|
wxString realPath = path.BeforeFirst(wxT(';'));
|
|
return wxFileName::FileExists(realPath) || wxFileName::DirExists(realPath);
|
|
}
|
|
|
|
IdentInterface *VSTEffectsModule::CreateInstance(const wxString & path)
|
|
{
|
|
// Acquires a resource for the application.
|
|
// For us, the ID is simply the path to the effect
|
|
// Safety of this depends on complementary calls to DeleteInstance on the module manager side.
|
|
return safenew VSTEffect(path);
|
|
}
|
|
|
|
void VSTEffectsModule::DeleteInstance(IdentInterface *instance)
|
|
{
|
|
std::unique_ptr < VSTEffect > {
|
|
dynamic_cast<VSTEffect *>(instance)
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// ModuleEffectInterface implementation
|
|
// ============================================================================
|
|
|
|
// ============================================================================
|
|
// VSTEffectsModule implementation
|
|
// ============================================================================
|
|
|
|
// static
|
|
//
|
|
// Called from reinvokation of Audacity or DLL to check in a separate process
|
|
void VSTEffectsModule::Check(const wxChar *path)
|
|
{
|
|
VSTEffect effect(path);
|
|
if (effect.SetHost(NULL))
|
|
{
|
|
wxArrayInt effectIDs = effect.GetEffectIDs();
|
|
wxString out;
|
|
|
|
if (effectIDs.GetCount() > 0)
|
|
{
|
|
wxString subids;
|
|
|
|
for (size_t i = 0, cnt = effectIDs.GetCount(); i < cnt; i++)
|
|
{
|
|
subids += wxString::Format(wxT("%d;"), effectIDs[i]);
|
|
}
|
|
|
|
out = wxString::Format(wxT("%s%d=%s\n"), OUTPUTKEY, kKeySubIDs, subids.RemoveLast());
|
|
}
|
|
else
|
|
{
|
|
out += wxString::Format(wxT("%s%d=%s\n"), OUTPUTKEY, kKeyBegin, wxEmptyString);
|
|
out += wxString::Format(wxT("%s%d=%s\n"), OUTPUTKEY, kKeyPath, effect.GetPath());
|
|
out += wxString::Format(wxT("%s%d=%s\n"), OUTPUTKEY, kKeyName, effect.GetName());
|
|
out += wxString::Format(wxT("%s%d=%s\n"), OUTPUTKEY, kKeyVendor, effect.GetVendor());
|
|
out += wxString::Format(wxT("%s%d=%s\n"), OUTPUTKEY, kKeyVersion, effect.GetVersion());
|
|
out += wxString::Format(wxT("%s%d=%s\n"), OUTPUTKEY, kKeyDescription, effect.GetDescription());
|
|
out += wxString::Format(wxT("%s%d=%d\n"), OUTPUTKEY, kKeyEffectType, effect.GetType());
|
|
out += wxString::Format(wxT("%s%d=%d\n"), OUTPUTKEY, kKeyInteractive, effect.IsInteractive());
|
|
out += wxString::Format(wxT("%s%d=%d\n"), OUTPUTKEY, kKeyAutomatable, effect.SupportsAutomation());
|
|
out += wxString::Format(wxT("%s%d=%s\n"), OUTPUTKEY, kKeyEnd, wxEmptyString);
|
|
}
|
|
|
|
// We want to output info in one chunk to prevent output
|
|
// from the effect intermixing with the info
|
|
const wxCharBuffer buf = out.ToUTF8();
|
|
fwrite(buf, 1, strlen(buf), stdout);
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Dialog for configuring latency, buffer size and graphics mode for a
|
|
// VST effect.
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
class VSTEffectOptionsDialog final : public wxDialogWrapper
|
|
{
|
|
public:
|
|
VSTEffectOptionsDialog(wxWindow * parent, EffectHostInterface *host);
|
|
virtual ~VSTEffectOptionsDialog();
|
|
|
|
void PopulateOrExchange(ShuttleGui & S);
|
|
|
|
void OnOk(wxCommandEvent & evt);
|
|
|
|
private:
|
|
EffectHostInterface *mHost;
|
|
int mBufferSize;
|
|
bool mUseLatency;
|
|
bool mUseGUI;
|
|
|
|
DECLARE_EVENT_TABLE()
|
|
};
|
|
|
|
BEGIN_EVENT_TABLE(VSTEffectOptionsDialog, wxDialogWrapper)
|
|
EVT_BUTTON(wxID_OK, VSTEffectOptionsDialog::OnOk)
|
|
END_EVENT_TABLE()
|
|
|
|
VSTEffectOptionsDialog::VSTEffectOptionsDialog(wxWindow * parent, EffectHostInterface *host)
|
|
: wxDialogWrapper(parent, wxID_ANY, wxString(_("VST Effect Options")))
|
|
{
|
|
mHost = host;
|
|
|
|
mHost->GetSharedConfig(wxT("Options"), wxT("BufferSize"), mBufferSize, 8192);
|
|
mHost->GetSharedConfig(wxT("Options"), wxT("UseLatency"), mUseLatency, true);
|
|
mHost->GetSharedConfig(wxT("Options"), wxT("UseGUI"), mUseGUI, true);
|
|
|
|
ShuttleGui S(this, eIsCreating);
|
|
PopulateOrExchange(S);
|
|
}
|
|
|
|
VSTEffectOptionsDialog::~VSTEffectOptionsDialog()
|
|
{
|
|
}
|
|
|
|
void VSTEffectOptionsDialog::PopulateOrExchange(ShuttleGui & S)
|
|
{
|
|
S.SetBorder(5);
|
|
S.StartHorizontalLay(wxEXPAND, 1);
|
|
{
|
|
S.StartVerticalLay(false);
|
|
{
|
|
S.StartStatic(_("Buffer Size"));
|
|
{
|
|
IntegerValidator<int> vld(&mBufferSize);
|
|
vld.SetRange(8, 1048576 * 1);
|
|
|
|
S.AddVariableText(wxString() +
|
|
_("The buffer size controls the number of samples sent to the effect ") +
|
|
_("on each iteration. Smaller values will cause slower processing and ") +
|
|
_("some effects require 8192 samples or less to work properly. However ") +
|
|
_("most effects can accept large buffers and using them will greatly ") +
|
|
_("reduce processing time."))->Wrap(650);
|
|
|
|
S.StartHorizontalLay(wxALIGN_LEFT);
|
|
{
|
|
wxTextCtrl *t;
|
|
t = S.TieNumericTextBox(_("&Buffer Size (8 to 1048576 samples):"),
|
|
mBufferSize,
|
|
12);
|
|
t->SetMinSize(wxSize(100, -1));
|
|
t->SetValidator(vld);
|
|
}
|
|
S.EndHorizontalLay();
|
|
}
|
|
S.EndStatic();
|
|
|
|
S.StartStatic(_("Latency Compensation"));
|
|
{
|
|
S.AddVariableText(wxString() +
|
|
_("As part of their processing, some VST effects must delay returning ") +
|
|
_("audio to Audacity. When not compensating for this delay, you will ") +
|
|
_("notice that small silences have been inserted into the audio. ") +
|
|
_("Enabling this option will provide that compensation, but it may ") +
|
|
_("not work for all VST effects."))->Wrap(650);
|
|
|
|
S.StartHorizontalLay(wxALIGN_LEFT);
|
|
{
|
|
S.TieCheckBox(_("Enable &compensation"),
|
|
mUseLatency);
|
|
}
|
|
S.EndHorizontalLay();
|
|
}
|
|
S.EndStatic();
|
|
|
|
S.StartStatic(_("Graphical Mode"));
|
|
{
|
|
S.AddVariableText(wxString() +
|
|
_("Most VST effects have a graphical interface for setting parameter values.") +
|
|
_(" A basic text-only method is also available. ") +
|
|
_(" Reopen the effect for this to take effect."))->Wrap(650);
|
|
S.TieCheckBox(_("Enable &graphical interface"),
|
|
mUseGUI);
|
|
}
|
|
S.EndStatic();
|
|
}
|
|
S.EndVerticalLay();
|
|
}
|
|
S.EndHorizontalLay();
|
|
|
|
S.AddStandardButtons();
|
|
|
|
Layout();
|
|
Fit();
|
|
Center();
|
|
}
|
|
|
|
void VSTEffectOptionsDialog::OnOk(wxCommandEvent & WXUNUSED(evt))
|
|
{
|
|
if (!Validate())
|
|
{
|
|
return;
|
|
}
|
|
|
|
ShuttleGui S(this, eIsGettingFromDialog);
|
|
PopulateOrExchange(S);
|
|
|
|
mHost->SetSharedConfig(wxT("Options"), wxT("BufferSize"), mBufferSize);
|
|
mHost->SetSharedConfig(wxT("Options"), wxT("UseLatency"), mUseLatency);
|
|
mHost->SetSharedConfig(wxT("Options"), wxT("UseGUI"), mUseGUI);
|
|
|
|
EndModal(wxID_OK);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
///
|
|
/// Wrapper for wxTimer that calls a VST effect at regular intervals.
|
|
///
|
|
/// \todo should there be tests for no timer available?
|
|
///
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
class VSTEffectTimer final : public wxTimer
|
|
{
|
|
public:
|
|
VSTEffectTimer(VSTEffect *effect)
|
|
: wxTimer(),
|
|
mEffect(effect)
|
|
{
|
|
}
|
|
|
|
~VSTEffectTimer()
|
|
{
|
|
}
|
|
|
|
void Notify()
|
|
{
|
|
mEffect->OnTimer();
|
|
}
|
|
|
|
private:
|
|
VSTEffect *mEffect;
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// VSTEffect
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
enum
|
|
{
|
|
ID_Duration = 20000,
|
|
ID_Sliders = 21000,
|
|
};
|
|
|
|
DEFINE_LOCAL_EVENT_TYPE(EVT_SIZEWINDOW);
|
|
DEFINE_LOCAL_EVENT_TYPE(EVT_UPDATEDISPLAY);
|
|
|
|
BEGIN_EVENT_TABLE(VSTEffect, wxEvtHandler)
|
|
EVT_COMMAND_RANGE(ID_Sliders, ID_Sliders + 999, wxEVT_COMMAND_SLIDER_UPDATED, VSTEffect::OnSlider)
|
|
|
|
// Events from the audioMaster callback
|
|
EVT_COMMAND(wxID_ANY, EVT_SIZEWINDOW, VSTEffect::OnSizeWindow)
|
|
END_EVENT_TABLE()
|
|
|
|
// Needed to support shell plugins...sucks, but whatcha gonna do???
|
|
intptr_t VSTEffect::mCurrentEffectID;
|
|
|
|
typedef AEffect *(*vstPluginMain)(audioMasterCallback audioMaster);
|
|
|
|
intptr_t VSTEffect::AudioMaster(AEffect * effect,
|
|
int32_t opcode,
|
|
int32_t index,
|
|
intptr_t value,
|
|
void * ptr,
|
|
float opt)
|
|
{
|
|
VSTEffect *vst = (effect ? (VSTEffect *) effect->ptr2 : NULL);
|
|
|
|
// Handles operations during initialization...before VSTEffect has had a
|
|
// chance to set its instance pointer.
|
|
switch (opcode)
|
|
{
|
|
case audioMasterVersion:
|
|
return (intptr_t) 2400;
|
|
|
|
case audioMasterCurrentId:
|
|
return mCurrentEffectID;
|
|
|
|
case audioMasterGetVendorString:
|
|
strcpy((char *) ptr, "Audacity Team"); // Do not translate, max 64 + 1 for null terminator
|
|
return 1;
|
|
|
|
case audioMasterGetProductString:
|
|
strcpy((char *) ptr, "Audacity"); // Do not translate, max 64 + 1 for null terminator
|
|
return 1;
|
|
|
|
case audioMasterGetVendorVersion:
|
|
return (intptr_t) (AUDACITY_VERSION << 24 |
|
|
AUDACITY_RELEASE << 16 |
|
|
AUDACITY_REVISION << 8 |
|
|
AUDACITY_MODLEVEL);
|
|
|
|
// Some (older) effects depend on an effIdle call when requested. An
|
|
// example is the Antress Modern plugins which uses the call to update
|
|
// the editors display when the program (preset) changes.
|
|
case audioMasterNeedIdle:
|
|
if (vst)
|
|
{
|
|
vst->NeedIdle();
|
|
return 1;
|
|
}
|
|
return 0;
|
|
|
|
// We would normally get this if the effect editor is dipslayed and something "major"
|
|
// has changed (like a program change) instead of multiple automation calls.
|
|
// Since we don't do anything with the parameters while the editor is displayed,
|
|
// there's no need for us to do anything.
|
|
case audioMasterUpdateDisplay:
|
|
if (vst)
|
|
{
|
|
vst->UpdateDisplay();
|
|
return 1;
|
|
}
|
|
return 0;
|
|
|
|
// Return the current time info.
|
|
case audioMasterGetTime:
|
|
if (vst)
|
|
{
|
|
return (intptr_t) vst->GetTimeInfo();
|
|
}
|
|
return 0;
|
|
|
|
// Inputs, outputs, or initial delay has changed...all we care about is initial delay.
|
|
case audioMasterIOChanged:
|
|
if (vst)
|
|
{
|
|
vst->SetBufferDelay(effect->initialDelay);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
|
|
case audioMasterGetSampleRate:
|
|
if (vst)
|
|
{
|
|
return (intptr_t) vst->GetSampleRate();
|
|
}
|
|
return 0;
|
|
|
|
case audioMasterIdle:
|
|
wxYieldIfNeeded();
|
|
return 1;
|
|
|
|
case audioMasterGetCurrentProcessLevel:
|
|
if (vst)
|
|
{
|
|
return vst->GetProcessLevel();
|
|
}
|
|
return 0;
|
|
|
|
case audioMasterGetLanguage:
|
|
return kVstLangEnglish;
|
|
|
|
// We always replace, never accumulate
|
|
case audioMasterWillReplaceOrAccumulate:
|
|
return 1;
|
|
|
|
// Resize the window to accommodate the effect size
|
|
case audioMasterSizeWindow:
|
|
if (vst)
|
|
{
|
|
vst->SizeWindow(index, value);
|
|
}
|
|
return 1;
|
|
|
|
case audioMasterCanDo:
|
|
{
|
|
char *s = (char *) ptr;
|
|
if (strcmp(s, "acceptIOChanges") == 0 ||
|
|
strcmp(s, "sendVstTimeInfo") == 0 ||
|
|
strcmp(s, "startStopProcess") == 0 ||
|
|
strcmp(s, "shellCategory") == 0 ||
|
|
strcmp(s, "sizeWindow") == 0)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
#if defined(VST_DEBUG)
|
|
#if defined(__WXMSW__)
|
|
wxLogDebug(wxT("VST canDo: %s"), wxString::FromAscii((char *)ptr));
|
|
#else
|
|
wxPrintf(wxT("VST canDo: %s\n"), wxString::FromAscii((char *)ptr));
|
|
#endif
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
case audioMasterBeginEdit:
|
|
case audioMasterEndEdit:
|
|
return 0;
|
|
|
|
case audioMasterAutomate:
|
|
if (vst)
|
|
{
|
|
vst->Automate(index, opt);
|
|
}
|
|
return 0;
|
|
|
|
// We're always connected (sort of)
|
|
case audioMasterPinConnected:
|
|
|
|
// We don't do MIDI yet
|
|
case audioMasterWantMidi:
|
|
case audioMasterProcessEvents:
|
|
|
|
// Don't need to see any messages about these
|
|
return 0;
|
|
}
|
|
|
|
#if defined(VST_DEBUG)
|
|
#if defined(__WXMSW__)
|
|
wxLogDebug(wxT("vst: %p opcode: %d index: %d value: %p ptr: %p opt: %f user: %p"),
|
|
effect, (int) opcode, (int) index, (void *) value, ptr, opt, vst);
|
|
#else
|
|
wxPrintf(wxT("vst: %p opcode: %d index: %d value: %p ptr: %p opt: %f user: %p\n"),
|
|
effect, (int) opcode, (int) index, (void *) value, ptr, opt, vst);
|
|
#endif
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
#if !defined(__WXMSW__)
|
|
void VSTEffect::ModuleDeleter::operator() (void* p) const
|
|
{
|
|
if (p)
|
|
dlclose(p);
|
|
}
|
|
#endif
|
|
|
|
#if defined(__WXMAC__)
|
|
void VSTEffect::BundleDeleter::operator() (void* p) const
|
|
{
|
|
if (p)
|
|
CFRelease(static_cast<CFBundleRef>(p));
|
|
}
|
|
|
|
void VSTEffect::ResourceDeleter::operator() (void *p) const
|
|
{
|
|
if (mpHandle) {
|
|
int resource = (int)p;
|
|
CFBundleCloseBundleResourceMap(mpHandle->get(), resource);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
VSTEffect::VSTEffect(const wxString & path, VSTEffect *master)
|
|
: mPath(path),
|
|
mMaster(master)
|
|
{
|
|
mHost = NULL;
|
|
mModule = NULL;
|
|
mAEffect = NULL;
|
|
mDialog = NULL;
|
|
|
|
mTimer = std::make_unique<VSTEffectTimer>(this);
|
|
mTimerGuard = 0;
|
|
|
|
mInteractive = false;
|
|
mAudioIns = 0;
|
|
mAudioOuts = 0;
|
|
mMidiIns = 0;
|
|
mMidiOuts = 0;
|
|
mSampleRate = 44100;
|
|
mBlockSize = mUserBlockSize = 8192;
|
|
mBufferDelay = 0;
|
|
mProcessLevel = 1; // in GUI thread
|
|
mHasPower = false;
|
|
mWantsIdle = false;
|
|
mWantsEditIdle = false;
|
|
mUseLatency = true;
|
|
mReady = false;
|
|
|
|
memset(&mTimeInfo, 0, sizeof(mTimeInfo));
|
|
mTimeInfo.samplePos = 0.0;
|
|
mTimeInfo.sampleRate = 44100.0; // this is a bogus value, but it's only for the display
|
|
mTimeInfo.nanoSeconds = wxGetLocalTimeMillis().ToDouble();
|
|
mTimeInfo.tempo = 120.0;
|
|
mTimeInfo.timeSigNumerator = 4;
|
|
mTimeInfo.timeSigDenominator = 4;
|
|
mTimeInfo.flags = kVstTempoValid | kVstNanosValid;
|
|
|
|
// UI
|
|
|
|
mGui = false;
|
|
mContainer = NULL;
|
|
|
|
// If we're a slave then go ahead a load immediately
|
|
if (mMaster)
|
|
{
|
|
Load();
|
|
}
|
|
}
|
|
|
|
VSTEffect::~VSTEffect()
|
|
{
|
|
if (mDialog)
|
|
{
|
|
mDialog->Close();
|
|
}
|
|
|
|
Unload();
|
|
}
|
|
|
|
// ============================================================================
|
|
// IdentInterface Implementation
|
|
// ============================================================================
|
|
|
|
wxString VSTEffect::GetPath()
|
|
{
|
|
return mPath;
|
|
}
|
|
|
|
wxString VSTEffect::GetSymbol()
|
|
{
|
|
return mName;
|
|
}
|
|
|
|
wxString VSTEffect::GetName()
|
|
{
|
|
return GetSymbol();
|
|
}
|
|
|
|
wxString VSTEffect::GetVendor()
|
|
{
|
|
return mVendor;
|
|
}
|
|
|
|
wxString VSTEffect::GetVersion()
|
|
{
|
|
wxString version;
|
|
|
|
bool skipping = true;
|
|
for (int i = 0, s = 0; i < 4; i++, s += 8)
|
|
{
|
|
int dig = (mVersion >> s) & 0xff;
|
|
if (dig != 0 || !skipping)
|
|
{
|
|
version += !skipping ? wxT(".") : wxT("");
|
|
version += wxString::Format(wxT("%d"), dig);
|
|
skipping = false;
|
|
}
|
|
}
|
|
|
|
return version;
|
|
}
|
|
|
|
wxString VSTEffect::GetDescription()
|
|
{
|
|
// VST does have a product string opcode and some effects return a short
|
|
// description, but most do not or they just return the name again. So,
|
|
// try to provide some sort of useful information.
|
|
return wxString::Format( _("Audio In: %d, Audio Out: %d"),
|
|
mAudioIns, mAudioOuts);
|
|
}
|
|
|
|
// ============================================================================
|
|
// EffectIdentInterface Implementation
|
|
// ============================================================================
|
|
|
|
EffectType VSTEffect::GetType()
|
|
{
|
|
if (mAudioIns == 0 && mAudioOuts == 0 && mMidiIns == 0 && mMidiOuts == 0)
|
|
{
|
|
return EffectTypeNone;
|
|
}
|
|
|
|
if (mAudioIns == 0 && mMidiIns == 0)
|
|
{
|
|
return EffectTypeGenerate;
|
|
}
|
|
|
|
if (mAudioOuts == 0 && mMidiOuts == 0)
|
|
{
|
|
return EffectTypeAnalyze;
|
|
}
|
|
|
|
return EffectTypeProcess;
|
|
}
|
|
|
|
|
|
wxString VSTEffect::GetFamily()
|
|
{
|
|
return VSTPLUGINTYPE;
|
|
}
|
|
|
|
bool VSTEffect::IsInteractive()
|
|
{
|
|
return mInteractive;
|
|
}
|
|
|
|
bool VSTEffect::IsDefault()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool VSTEffect::IsLegacy()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool VSTEffect::SupportsRealtime()
|
|
{
|
|
return GetType() == EffectTypeProcess;
|
|
}
|
|
|
|
bool VSTEffect::SupportsAutomation()
|
|
{
|
|
return mAutomatable;
|
|
}
|
|
|
|
// ============================================================================
|
|
// EffectClientInterface Implementation
|
|
// ============================================================================
|
|
|
|
bool VSTEffect::SetHost(EffectHostInterface *host)
|
|
{
|
|
mHost = host;
|
|
|
|
if (!mAEffect)
|
|
{
|
|
Load();
|
|
}
|
|
|
|
if (!mAEffect)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If we have a master then there's no need to load settings since the master will feed
|
|
// us everything we need.
|
|
if (mMaster)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (mHost)
|
|
{
|
|
int userBlockSize;
|
|
mHost->GetSharedConfig(wxT("Options"), wxT("BufferSize"), userBlockSize, 8192);
|
|
mUserBlockSize = std::max( 1, userBlockSize );
|
|
mHost->GetSharedConfig(wxT("Options"), wxT("UseLatency"), mUseLatency, true);
|
|
|
|
mBlockSize = mUserBlockSize;
|
|
|
|
bool haveDefaults;
|
|
mHost->GetPrivateConfig(mHost->GetFactoryDefaultsGroup(), wxT("Initialized"), haveDefaults, false);
|
|
if (!haveDefaults)
|
|
{
|
|
SaveParameters(mHost->GetFactoryDefaultsGroup());
|
|
mHost->SetPrivateConfig(mHost->GetFactoryDefaultsGroup(), wxT("Initialized"), true);
|
|
}
|
|
|
|
LoadParameters(mHost->GetCurrentSettingsGroup());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
unsigned VSTEffect::GetAudioInCount()
|
|
{
|
|
return mAudioIns;
|
|
}
|
|
|
|
unsigned VSTEffect::GetAudioOutCount()
|
|
{
|
|
return mAudioOuts;
|
|
}
|
|
|
|
int VSTEffect::GetMidiInCount()
|
|
{
|
|
return mMidiIns;
|
|
}
|
|
|
|
int VSTEffect::GetMidiOutCount()
|
|
{
|
|
return mMidiOuts;
|
|
}
|
|
|
|
size_t VSTEffect::SetBlockSize(size_t maxBlockSize)
|
|
{
|
|
mBlockSize = std::min( maxBlockSize, mUserBlockSize );
|
|
return mBlockSize;
|
|
}
|
|
|
|
void VSTEffect::SetSampleRate(double rate)
|
|
{
|
|
mSampleRate = (float) rate;
|
|
}
|
|
|
|
sampleCount VSTEffect::GetLatency()
|
|
{
|
|
if (mUseLatency)
|
|
{
|
|
// ??? Threading issue ???
|
|
auto delay = mBufferDelay;
|
|
mBufferDelay = 0;
|
|
return delay;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
size_t VSTEffect::GetTailSize()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
bool VSTEffect::IsReady()
|
|
{
|
|
return mReady;
|
|
}
|
|
|
|
bool VSTEffect::ProcessInitialize(sampleCount WXUNUSED(totalLen), ChannelNames WXUNUSED(chanMap))
|
|
{
|
|
// Initialize time info
|
|
memset(&mTimeInfo, 0, sizeof(mTimeInfo));
|
|
mTimeInfo.sampleRate = mSampleRate;
|
|
mTimeInfo.nanoSeconds = wxGetLocalTimeMillis().ToDouble();
|
|
mTimeInfo.tempo = 120.0;
|
|
mTimeInfo.timeSigNumerator = 4;
|
|
mTimeInfo.timeSigDenominator = 4;
|
|
mTimeInfo.flags = kVstTempoValid | kVstNanosValid | kVstTransportPlaying;
|
|
|
|
// Set processing parameters...power must be off for this
|
|
callDispatcher(effSetSampleRate, 0, 0, NULL, mSampleRate);
|
|
callDispatcher(effSetBlockSize, 0, mBlockSize, NULL, 0.0);
|
|
|
|
// Turn on the power
|
|
PowerOn();
|
|
|
|
// Set the initial buffer delay
|
|
SetBufferDelay(mAEffect->initialDelay);
|
|
|
|
mReady = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VSTEffect::ProcessFinalize()
|
|
{
|
|
mReady = false;
|
|
|
|
PowerOff();
|
|
|
|
return true;
|
|
}
|
|
|
|
size_t VSTEffect::ProcessBlock(float **inBlock, float **outBlock, size_t blockLen)
|
|
{
|
|
// Only call the effect if there's something to do...some do not like zero-length block
|
|
if (blockLen)
|
|
{
|
|
// Go let the plugin moleste the samples
|
|
callProcessReplacing(inBlock, outBlock, blockLen);
|
|
|
|
// And track the position
|
|
mTimeInfo.samplePos += ((double) blockLen / mTimeInfo.sampleRate);
|
|
}
|
|
|
|
return blockLen;
|
|
}
|
|
|
|
unsigned VSTEffect::GetChannelCount()
|
|
{
|
|
return mNumChannels;
|
|
}
|
|
|
|
void VSTEffect::SetChannelCount(unsigned numChannels)
|
|
{
|
|
mNumChannels = numChannels;
|
|
}
|
|
|
|
bool VSTEffect::RealtimeInitialize()
|
|
{
|
|
mMasterIn.reinit( mAudioIns, mBlockSize, true );
|
|
mMasterOut.reinit( mAudioOuts, mBlockSize );
|
|
|
|
return ProcessInitialize(0, NULL);
|
|
}
|
|
|
|
bool VSTEffect::RealtimeAddProcessor(unsigned numChannels, float sampleRate)
|
|
{
|
|
mSlaves.push_back(make_movable<VSTEffect>(mPath, this));
|
|
VSTEffect *const slave = mSlaves.back().get();
|
|
|
|
slave->SetBlockSize(mBlockSize);
|
|
slave->SetChannelCount(numChannels);
|
|
slave->SetSampleRate(sampleRate);
|
|
|
|
int clen = 0;
|
|
if (mAEffect->flags & effFlagsProgramChunks)
|
|
{
|
|
void *chunk = NULL;
|
|
|
|
clen = (int) callDispatcher(effGetChunk, 1, 0, &chunk, 0.0); // get master's chunk, for the program only
|
|
if (clen != 0)
|
|
{
|
|
slave->callSetChunk(true, clen, chunk); // copy state to slave, for the program only
|
|
}
|
|
}
|
|
|
|
if (clen == 0)
|
|
{
|
|
callDispatcher(effBeginSetProgram, 0, 0, NULL, 0.0);
|
|
|
|
for (int i = 0; i < mAEffect->numParams; i++)
|
|
{
|
|
slave->callSetParameter(i, callGetParameter(i));
|
|
}
|
|
|
|
callDispatcher(effEndSetProgram, 0, 0, NULL, 0.0);
|
|
}
|
|
|
|
return slave->ProcessInitialize(0, NULL);
|
|
}
|
|
|
|
bool VSTEffect::RealtimeFinalize()
|
|
{
|
|
for (const auto &slave : mSlaves)
|
|
slave->ProcessFinalize();
|
|
mSlaves.clear();
|
|
|
|
mMasterIn.reset();
|
|
|
|
mMasterOut.reset();
|
|
|
|
return ProcessFinalize();
|
|
}
|
|
|
|
bool VSTEffect::RealtimeSuspend()
|
|
{
|
|
PowerOff();
|
|
|
|
for (const auto &slave : mSlaves)
|
|
slave->PowerOff();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VSTEffect::RealtimeResume()
|
|
{
|
|
PowerOn();
|
|
|
|
for (const auto &slave : mSlaves)
|
|
slave->PowerOn();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VSTEffect::RealtimeProcessStart()
|
|
{
|
|
for (unsigned int i = 0; i < mAudioIns; i++)
|
|
memset(mMasterIn[i].get(), 0, mBlockSize * sizeof(float));
|
|
|
|
mNumSamples = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
size_t VSTEffect::RealtimeProcess(int group, float **inbuf, float **outbuf, size_t numSamples)
|
|
{
|
|
wxASSERT(numSamples <= mBlockSize);
|
|
|
|
for (unsigned int c = 0; c < mAudioIns; c++)
|
|
{
|
|
for (decltype(numSamples) s = 0; s < numSamples; s++)
|
|
{
|
|
mMasterIn[c][s] += inbuf[c][s];
|
|
}
|
|
}
|
|
mNumSamples = std::max(numSamples, mNumSamples);
|
|
|
|
return mSlaves[group]->ProcessBlock(inbuf, outbuf, numSamples);
|
|
}
|
|
|
|
bool VSTEffect::RealtimeProcessEnd()
|
|
{
|
|
// These casts to float** should be safe...
|
|
ProcessBlock(
|
|
reinterpret_cast <float**> (mMasterIn.get()),
|
|
reinterpret_cast <float**> (mMasterOut.get()),
|
|
mNumSamples);
|
|
|
|
return true;
|
|
}
|
|
|
|
///
|
|
/// Some history...
|
|
///
|
|
/// Before we ran into the Antress plugin problem with buffer size limitations,
|
|
/// (see below) we just had a plain old effect loop...get the input samples, pass
|
|
/// them to the effect, save the output samples.
|
|
///
|
|
/// But, the hack I put in to limit the buffer size to only 8k (normally 512k or so)
|
|
/// severely impacted performance. So, Michael C. added some intermediate buffering
|
|
/// that sped things up quite a bit and this is how things have worked for quite a
|
|
/// while. It still didn't get the performance back to the pre-hack stage, but it
|
|
/// was a definite benefit.
|
|
///
|
|
/// History over...
|
|
///
|
|
/// I've recently (May 2014) tried newer versions of the Antress effects and they
|
|
/// no longer seem to have a problem with buffer size. So, I've made a bit of a
|
|
/// compromise...I've made the buffer size user configurable. Should have done this
|
|
/// from the beginning. I've left the default 8k, just in case, but now the user
|
|
/// can set the buffering based on their specific setup and needs.
|
|
///
|
|
/// And at the same time I added buffer delay compensation, which allows Audacity
|
|
/// to account for latency introduced by some effects. This is based on information
|
|
/// provided by the effect, so it will not work with all effects since they don't
|
|
/// all provide the information (kn0ck0ut is one).
|
|
///
|
|
bool VSTEffect::ShowInterface(wxWindow *parent, bool forceModal)
|
|
{
|
|
if (mDialog)
|
|
{
|
|
if ( mDialog->Close(true) )
|
|
mDialog = nullptr;
|
|
return false;
|
|
}
|
|
|
|
// mDialog is null
|
|
auto cleanup = valueRestorer( mDialog );
|
|
|
|
// mProcessLevel = 1; // in GUI thread
|
|
|
|
// Set some defaults since some VSTs need them...these will be reset when
|
|
// normal or realtime processing begins
|
|
if (!IsReady())
|
|
{
|
|
mSampleRate = 44100;
|
|
mBlockSize = 8192;
|
|
ProcessInitialize(0, NULL);
|
|
}
|
|
|
|
mDialog = mHost->CreateUI(parent, this);
|
|
if (!mDialog)
|
|
{
|
|
return false;
|
|
}
|
|
mDialog->CentreOnParent();
|
|
|
|
if (SupportsRealtime() && !forceModal)
|
|
{
|
|
mDialog->Show();
|
|
cleanup.release();
|
|
|
|
return false;
|
|
}
|
|
|
|
bool res = mDialog->ShowModal() != 0;
|
|
|
|
return res;
|
|
}
|
|
|
|
bool VSTEffect::GetAutomationParameters(EffectAutomationParameters & parms)
|
|
{
|
|
for (int i = 0; i < mAEffect->numParams; i++)
|
|
{
|
|
wxString name = GetString(effGetParamName, i);
|
|
if (name.IsEmpty())
|
|
{
|
|
name.Printf(wxT("parm_%d"), i);
|
|
}
|
|
|
|
float value = callGetParameter(i);
|
|
if (!parms.Write(name, value))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VSTEffect::SetAutomationParameters(EffectAutomationParameters & parms)
|
|
{
|
|
callDispatcher(effBeginSetProgram, 0, 0, NULL, 0.0);
|
|
for (int i = 0; i < mAEffect->numParams; i++)
|
|
{
|
|
wxString name = GetString(effGetParamName, i);
|
|
if (name.IsEmpty())
|
|
{
|
|
name.Printf(wxT("parm_%d"), i);
|
|
}
|
|
|
|
double d = 0.0;
|
|
if (!parms.Read(name, &d))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (d >= -1.0 && d <= 1.0)
|
|
{
|
|
callSetParameter(i, d);
|
|
for (const auto &slave : mSlaves)
|
|
slave->callSetParameter(i, d);
|
|
}
|
|
}
|
|
callDispatcher(effEndSetProgram, 0, 0, NULL, 0.0);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool VSTEffect::LoadUserPreset(const wxString & name)
|
|
{
|
|
if (!LoadParameters(name))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
RefreshParameters();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VSTEffect::SaveUserPreset(const wxString & name)
|
|
{
|
|
return SaveParameters(name);
|
|
}
|
|
|
|
wxArrayString VSTEffect::GetFactoryPresets()
|
|
{
|
|
wxArrayString progs;
|
|
|
|
// Some plugins, like Guitar Rig 5, only report 128 programs while they have hundreds. While
|
|
// I was able to come up with a hack in the Guitar Rig case to gather all of the program names
|
|
// it would not let me set a program outside of the first 128.
|
|
if (mVstVersion >= 2)
|
|
{
|
|
for (int i = 0; i < mAEffect->numPrograms; i++)
|
|
{
|
|
progs.Add(GetString(effGetProgramNameIndexed, i));
|
|
}
|
|
}
|
|
|
|
return progs;
|
|
}
|
|
|
|
bool VSTEffect::LoadFactoryPreset(int id)
|
|
{
|
|
callSetProgram(id);
|
|
|
|
RefreshParameters();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VSTEffect::LoadFactoryDefaults()
|
|
{
|
|
if (!LoadParameters(mHost->GetFactoryDefaultsGroup()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
RefreshParameters();
|
|
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// EffectUIClientInterface implementation
|
|
// ============================================================================
|
|
|
|
void VSTEffect::SetHostUI(EffectUIHostInterface *host)
|
|
{
|
|
mUIHost = host;
|
|
}
|
|
|
|
bool VSTEffect::PopulateUI(wxWindow *parent)
|
|
{
|
|
mDialog = static_cast<wxDialog *>(wxGetTopLevelParent(parent));
|
|
mParent = parent;
|
|
|
|
mParent->PushEventHandler(this);
|
|
|
|
// Determine if the VST editor is supposed to be used or not
|
|
mHost->GetSharedConfig(wxT("Options"),
|
|
wxT("UseGUI"),
|
|
mGui,
|
|
true);
|
|
mGui = mAEffect->flags & effFlagsHasEditor ? mGui : false;
|
|
|
|
// Must use the GUI editor if parameters aren't provided
|
|
if (mAEffect->numParams == 0)
|
|
{
|
|
mGui = true;
|
|
}
|
|
|
|
// Build the appropriate dialog type
|
|
if (mGui)
|
|
{
|
|
BuildFancy();
|
|
}
|
|
else
|
|
{
|
|
BuildPlain();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VSTEffect::IsGraphicalUI()
|
|
{
|
|
return mGui;
|
|
}
|
|
|
|
bool VSTEffect::ValidateUI()
|
|
{
|
|
if (!mParent->Validate() || !mParent->TransferDataFromWindow())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (GetType() == EffectTypeGenerate)
|
|
{
|
|
mHost->SetDuration(mDuration->GetValue());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VSTEffect::HideUI()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool VSTEffect::CloseUI()
|
|
{
|
|
#ifdef __WXMAC__
|
|
#ifdef __WX_EVTLOOP_BUSY_WAITING__
|
|
wxEventLoop::SetBusyWaiting(false);
|
|
#endif
|
|
mControl->Close();
|
|
#endif
|
|
|
|
mParent->RemoveEventHandler(this);
|
|
|
|
PowerOff();
|
|
|
|
NeedEditIdle(false);
|
|
|
|
RemoveHandler();
|
|
|
|
mNames.reset();
|
|
mSliders.reset();
|
|
mDisplays.reset();
|
|
mLabels.reset();
|
|
|
|
mUIHost = NULL;
|
|
mParent = NULL;
|
|
mDialog = NULL;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VSTEffect::CanExportPresets()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Throws exceptions rather than reporting errors.
|
|
void VSTEffect::ExportPresets()
|
|
{
|
|
wxString path;
|
|
|
|
// Ask the user for the real name
|
|
//
|
|
// Passing a valid parent will cause some effects dialogs to malfunction
|
|
// upon returning from the FileNames::SelectFile().
|
|
path = FileNames::SelectFile(FileNames::Operation::_None,
|
|
_("Save VST Preset As:"),
|
|
FileNames::DataDir(),
|
|
wxEmptyString,
|
|
wxT("xml"),
|
|
wxT("Standard VST bank file (*.fxb)|*.fxb|Standard VST program file (*.fxp)|*.fxp|Audacity VST preset file (*.xml)|*.xml"),
|
|
wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxRESIZE_BORDER,
|
|
NULL);
|
|
|
|
// User canceled...
|
|
if (path.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
wxFileName fn(path);
|
|
wxString ext = fn.GetExt();
|
|
if (ext.CmpNoCase(wxT("fxb")) == 0)
|
|
{
|
|
SaveFXB(fn);
|
|
}
|
|
else if (ext.CmpNoCase(wxT("fxp")) == 0)
|
|
{
|
|
SaveFXP(fn);
|
|
}
|
|
else if (ext.CmpNoCase(wxT("xml")) == 0)
|
|
{
|
|
// may throw
|
|
SaveXML(fn);
|
|
}
|
|
else
|
|
{
|
|
// This shouldn't happen, but complain anyway
|
|
AudacityMessageBox(_("Unrecognized file extension."),
|
|
_("Error Saving VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Load an "fxb", "fxp" or Audacuty "xml" file
|
|
//
|
|
// Based on work by Sven Giermann
|
|
//
|
|
void VSTEffect::ImportPresets()
|
|
{
|
|
wxString path;
|
|
|
|
// Ask the user for the real name
|
|
path = FileNames::SelectFile(FileNames::Operation::_None,
|
|
_("Load VST Preset:"),
|
|
FileNames::DataDir(),
|
|
wxEmptyString,
|
|
wxT("xml"),
|
|
wxT("VST preset files (*.fxb; *.fxp; *.xml)|*.fxb;*.fxp;*.xml"),
|
|
wxFD_OPEN | wxRESIZE_BORDER,
|
|
mParent);
|
|
|
|
// User canceled...
|
|
if (path.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
wxFileName fn(path);
|
|
wxString ext = fn.GetExt();
|
|
bool success = false;
|
|
if (ext.CmpNoCase(wxT("fxb")) == 0)
|
|
{
|
|
success = LoadFXB(fn);
|
|
}
|
|
else if (ext.CmpNoCase(wxT("fxp")) == 0)
|
|
{
|
|
success = LoadFXP(fn);
|
|
}
|
|
else if (ext.CmpNoCase(wxT("xml")) == 0)
|
|
{
|
|
success = LoadXML(fn);
|
|
}
|
|
else
|
|
{
|
|
// This shouldn't happen, but complain anyway
|
|
AudacityMessageBox(_("Unrecognized file extension."),
|
|
_("Error Loading VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
|
|
return;
|
|
}
|
|
|
|
if (!success)
|
|
{
|
|
AudacityMessageBox(_("Unable to load presets file."),
|
|
_("Error Loading VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
|
|
return;
|
|
}
|
|
|
|
RefreshParameters();
|
|
|
|
return;
|
|
}
|
|
|
|
bool VSTEffect::HasOptions()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void VSTEffect::ShowOptions()
|
|
{
|
|
VSTEffectOptionsDialog dlg(mParent, mHost);
|
|
if (dlg.ShowModal())
|
|
{
|
|
// Reinitialize configuration settings
|
|
int userBlockSize;
|
|
mHost->GetSharedConfig(wxT("Options"), wxT("BufferSize"), userBlockSize, 8192);
|
|
mUserBlockSize = std::max( 1, userBlockSize );
|
|
mHost->GetSharedConfig(wxT("Options"), wxT("UseLatency"), mUseLatency, true);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// VSTEffect implementation
|
|
// ============================================================================
|
|
|
|
bool VSTEffect::Load()
|
|
{
|
|
vstPluginMain pluginMain;
|
|
bool success = false;
|
|
|
|
long effectID = 0;
|
|
wxString realPath = mPath.BeforeFirst(wxT(';'));
|
|
mPath.AfterFirst(wxT(';')).ToLong(&effectID);
|
|
mCurrentEffectID = (intptr_t) effectID;
|
|
|
|
mModule = NULL;
|
|
mAEffect = NULL;
|
|
|
|
#if defined(__WXMAC__)
|
|
// Start clean
|
|
mBundleRef.reset();
|
|
|
|
mResource = ResourceHandle{};
|
|
|
|
// Convert the path to a CFSTring
|
|
wxCFStringRef path(realPath);
|
|
|
|
// Convert the path to a URL
|
|
CFURLRef urlRef =
|
|
CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
|
|
path,
|
|
kCFURLPOSIXPathStyle,
|
|
true);
|
|
if (urlRef == NULL)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Create the bundle using the URL
|
|
BundleHandle bundleRef{ CFBundleCreate(kCFAllocatorDefault, urlRef) };
|
|
|
|
// Done with the URL
|
|
CFRelease(urlRef);
|
|
|
|
// Bail if the bundle wasn't created
|
|
if (!bundleRef)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Retrieve a reference to the executable
|
|
CFURLRef exeRef = CFBundleCopyExecutableURL(bundleRef.get());
|
|
if (!exeRef)
|
|
return false;
|
|
|
|
// Convert back to path
|
|
UInt8 exePath[PLATFORM_MAX_PATH];
|
|
Boolean good = CFURLGetFileSystemRepresentation(exeRef, true, exePath, sizeof(exePath));
|
|
|
|
// Done with the executable reference
|
|
CFRelease(exeRef);
|
|
|
|
// Bail if we couldn't resolve the executable path
|
|
if (good == FALSE)
|
|
return false;
|
|
|
|
// Attempt to open it
|
|
mModule.reset((char*)dlopen((char *) exePath, RTLD_NOW | RTLD_LOCAL));
|
|
if (!mModule)
|
|
return false;
|
|
|
|
// Try to locate the NEW plugin entry point
|
|
pluginMain = (vstPluginMain) dlsym(mModule.get(), "VSTPluginMain");
|
|
|
|
// If not found, try finding the old entry point
|
|
if (pluginMain == NULL)
|
|
{
|
|
pluginMain = (vstPluginMain) dlsym(mModule.get(), "main_macho");
|
|
}
|
|
|
|
// Must not be a VST plugin
|
|
if (pluginMain == NULL)
|
|
{
|
|
mModule.reset();
|
|
return false;
|
|
}
|
|
|
|
// Need to keep the bundle reference around so we can map the
|
|
// resources.
|
|
mBundleRef = std::move(bundleRef);
|
|
|
|
// Open the resource map ... some plugins (like GRM Tools) need this.
|
|
mResource = ResourceHandle {
|
|
reinterpret_cast<char*>(
|
|
CFBundleOpenBundleResourceMap(mBundleRef.get())),
|
|
ResourceDeleter{&mBundleRef}
|
|
};
|
|
|
|
#elif defined(__WXMSW__)
|
|
|
|
{
|
|
wxLogNull nolog;
|
|
|
|
// Try to load the library
|
|
auto lib = std::make_unique<wxDynamicLibrary>(realPath);
|
|
if (!lib)
|
|
return false;
|
|
|
|
// Bail if it wasn't successful
|
|
if (!lib->IsLoaded())
|
|
return false;
|
|
|
|
// Try to find the entry point, while suppressing error messages
|
|
pluginMain = (vstPluginMain) lib->GetSymbol(wxT("VSTPluginMain"));
|
|
if (pluginMain == NULL)
|
|
{
|
|
pluginMain = (vstPluginMain) lib->GetSymbol(wxT("main"));
|
|
if (pluginMain == NULL)
|
|
return false;
|
|
}
|
|
|
|
// Save the library reference
|
|
mModule = std::move(lib);
|
|
}
|
|
|
|
#else
|
|
|
|
// Attempt to load it
|
|
//
|
|
// Spent a few days trying to figure out why some VSTs where running okay and
|
|
// others were hit or miss. The cause was that we export all of Audacity's
|
|
// symbols and some of the loaded libraries were picking up Audacity's and
|
|
// not their own.
|
|
//
|
|
// So far, I've only seen this issue on Linux, but we might just be getting
|
|
// lucky on the Mac and Windows. The sooner we stop exporting everything
|
|
// the better.
|
|
//
|
|
// To get around the problem, I just added the RTLD_DEEPBIND flag to the load
|
|
// and that "basically" puts Audacity last when the loader needs to resolve
|
|
// symbols.
|
|
//
|
|
// Once we define a proper external API, the flags can be removed.
|
|
#ifndef RTLD_DEEPBIND
|
|
#define RTLD_DEEPBIND 0
|
|
#endif
|
|
ModuleHandle lib {
|
|
(char*) dlopen((const char *)wxString(realPath).ToUTF8(),
|
|
RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND)
|
|
};
|
|
if (!lib)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Try to find the entry point, while suppressing error messages
|
|
pluginMain = (vstPluginMain) dlsym(lib.get(), "VSTPluginMain");
|
|
if (pluginMain == NULL)
|
|
{
|
|
pluginMain = (vstPluginMain) dlsym(lib.get(), "main");
|
|
if (pluginMain == NULL)
|
|
return false;
|
|
}
|
|
|
|
// Save the library reference
|
|
mModule = std::move(lib);
|
|
|
|
#endif
|
|
|
|
// Initialize the plugin
|
|
try
|
|
{
|
|
mAEffect = pluginMain(VSTEffect::AudioMaster);
|
|
}
|
|
catch (...)
|
|
{
|
|
wxLogMessage(_("VST plugin initialization failed\n"));
|
|
mAEffect = NULL;
|
|
}
|
|
|
|
// Was it successful?
|
|
if (mAEffect)
|
|
{
|
|
// Save a reference to ourselves
|
|
//
|
|
// Note: Some hosts use "user" and some use "ptr2/resvd2". It might
|
|
// be worthwhile to check if user is NULL before using it and
|
|
// then falling back to "ptr2/resvd2".
|
|
mAEffect->ptr2 = this;
|
|
|
|
// Give the plugin an initial sample rate and blocksize
|
|
callDispatcher(effSetSampleRate, 0, 0, NULL, 48000.0);
|
|
callDispatcher(effSetBlockSize, 0, 512, NULL, 0);
|
|
|
|
// Ask the plugin to identify itself...might be needed for older plugins
|
|
callDispatcher(effIdentify, 0, 0, NULL, 0);
|
|
|
|
// Open the plugin
|
|
callDispatcher(effOpen, 0, 0, NULL, 0.0);
|
|
|
|
// Get the VST version the plugin understands
|
|
mVstVersion = callDispatcher(effGetVstVersion, 0, 0, NULL, 0);
|
|
|
|
// Set it again in case plugin ignored it before the effOpen
|
|
callDispatcher(effSetSampleRate, 0, 0, NULL, 48000.0);
|
|
callDispatcher(effSetBlockSize, 0, 512, NULL, 0);
|
|
|
|
// Ensure that it looks like a plugin and can deal with ProcessReplacing
|
|
// calls. Also exclude synths for now.
|
|
if (mAEffect->magic == kEffectMagic &&
|
|
!(mAEffect->flags & effFlagsIsSynth) &&
|
|
mAEffect->flags & effFlagsCanReplacing)
|
|
{
|
|
if (mVstVersion >= 2)
|
|
{
|
|
mName = GetString(effGetEffectName);
|
|
if (mName.length() == 0)
|
|
{
|
|
mName = GetString(effGetProductString);
|
|
}
|
|
}
|
|
if (mName.length() == 0)
|
|
{
|
|
mName = wxFileName{realPath}.GetName();
|
|
}
|
|
|
|
if (mVstVersion >= 2)
|
|
{
|
|
mVendor = GetString(effGetVendorString);
|
|
mVersion = wxINT32_SWAP_ON_LE(callDispatcher(effGetVendorVersion, 0, 0, NULL, 0));
|
|
}
|
|
if (mVersion == 0)
|
|
{
|
|
mVersion = wxINT32_SWAP_ON_LE(mAEffect->version);
|
|
}
|
|
|
|
if (mAEffect->flags & effFlagsHasEditor || mAEffect->numParams != 0)
|
|
{
|
|
mInteractive = true;
|
|
}
|
|
|
|
mAudioIns = mAEffect->numInputs;
|
|
mAudioOuts = mAEffect->numOutputs;
|
|
|
|
mMidiIns = 0;
|
|
mMidiOuts = 0;
|
|
|
|
// Check to see if parameters can be automated. This isn't a gaurantee
|
|
// since it could be that the effect simply doesn't support the opcode.
|
|
mAutomatable = false;
|
|
for (int i = 0; i < mAEffect->numParams; i++)
|
|
{
|
|
if (callDispatcher(effCanBeAutomated, 0, i, NULL, 0.0))
|
|
{
|
|
mAutomatable = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Make sure we start out with a valid program selection
|
|
// I've found one plugin (SoundHack +morphfilter) that will
|
|
// crash Audacity when saving the initial default parameters
|
|
// with this.
|
|
callSetProgram(0);
|
|
|
|
// Pretty confident that we're good to go
|
|
success = true;
|
|
}
|
|
}
|
|
|
|
if (!success)
|
|
{
|
|
Unload();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
void VSTEffect::Unload()
|
|
{
|
|
if (mDialog)
|
|
{
|
|
CloseUI();
|
|
}
|
|
|
|
if (mAEffect)
|
|
{
|
|
// Turn the power off
|
|
PowerOff();
|
|
|
|
// Finally, close the plugin
|
|
callDispatcher(effClose, 0, 0, NULL, 0.0);
|
|
mAEffect = NULL;
|
|
}
|
|
|
|
if (mModule)
|
|
{
|
|
#if defined(__WXMAC__)
|
|
mResource = ResourceHandle{};
|
|
mBundleRef.reset();
|
|
#endif
|
|
|
|
mModule.reset();
|
|
mAEffect = NULL;
|
|
}
|
|
}
|
|
|
|
wxArrayInt VSTEffect::GetEffectIDs()
|
|
{
|
|
wxArrayInt effectIDs;
|
|
|
|
// Are we a shell?
|
|
if (mVstVersion >= 2 && (VstPlugCategory) callDispatcher(effGetPlugCategory, 0, 0, NULL, 0) == kPlugCategShell)
|
|
{
|
|
char name[64];
|
|
int effectID;
|
|
|
|
effectID = (int) callDispatcher(effShellGetNextPlugin, 0, 0, &name, 0);
|
|
while (effectID)
|
|
{
|
|
effectIDs.Add(effectID);
|
|
effectID = (int) callDispatcher(effShellGetNextPlugin, 0, 0, &name, 0);
|
|
}
|
|
}
|
|
|
|
return effectIDs;
|
|
}
|
|
|
|
bool VSTEffect::LoadParameters(const wxString & group)
|
|
{
|
|
wxString value;
|
|
|
|
VstPatchChunkInfo info = {1, mAEffect->uniqueID, mAEffect->version, mAEffect->numParams, ""};
|
|
mHost->GetPrivateConfig(group, wxT("UniqueID"), info.pluginUniqueID, info.pluginUniqueID);
|
|
mHost->GetPrivateConfig(group, wxT("Version"), info.pluginVersion, info.pluginVersion);
|
|
mHost->GetPrivateConfig(group, wxT("Elements"), info.numElements, info.numElements);
|
|
|
|
if ((info.pluginUniqueID != mAEffect->uniqueID) ||
|
|
(info.pluginVersion != mAEffect->version) ||
|
|
(info.numElements != mAEffect->numParams))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (mHost->GetPrivateConfig(group, wxT("Chunk"), value, wxEmptyString))
|
|
{
|
|
ArrayOf<char> buf{ value.length() / 4 * 3 };
|
|
|
|
int len = VSTEffect::b64decode(value, buf.get());
|
|
if (len)
|
|
{
|
|
callSetChunk(true, len, buf.get(), &info);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
wxString parms;
|
|
if (!mHost->GetPrivateConfig(group, wxT("Parameters"), parms, wxEmptyString))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
EffectAutomationParameters eap;
|
|
if (!eap.SetParameters(parms))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return SetAutomationParameters(eap);
|
|
}
|
|
|
|
bool VSTEffect::SaveParameters(const wxString & group)
|
|
{
|
|
mHost->SetPrivateConfig(group, wxT("UniqueID"), mAEffect->uniqueID);
|
|
mHost->SetPrivateConfig(group, wxT("Version"), mAEffect->version);
|
|
mHost->SetPrivateConfig(group, wxT("Elements"), mAEffect->numParams);
|
|
|
|
if (mAEffect->flags & effFlagsProgramChunks)
|
|
{
|
|
void *chunk = NULL;
|
|
int clen = (int) callDispatcher(effGetChunk, 1, 0, &chunk, 0.0);
|
|
if (clen <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
mHost->SetPrivateConfig(group, wxT("Chunk"), VSTEffect::b64encode(chunk, clen));
|
|
return true;
|
|
}
|
|
|
|
EffectAutomationParameters eap;
|
|
if (!GetAutomationParameters(eap))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
wxString parms;
|
|
if (!eap.GetParameters(parms))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return mHost->SetPrivateConfig(group, wxT("Parameters"), parms);
|
|
}
|
|
|
|
void VSTEffect::OnTimer()
|
|
{
|
|
wxRecursionGuard guard(mTimerGuard);
|
|
|
|
// Ignore it if we're recursing
|
|
if (guard.IsInside())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (mVstVersion >= 2 && mWantsIdle)
|
|
{
|
|
int ret = callDispatcher(effIdle, 0, 0, NULL, 0.0);
|
|
if (!ret)
|
|
{
|
|
mWantsIdle = false;
|
|
}
|
|
}
|
|
|
|
if (mWantsEditIdle)
|
|
{
|
|
callDispatcher(effEditIdle, 0, 0, NULL, 0.0);
|
|
}
|
|
}
|
|
|
|
void VSTEffect::NeedIdle()
|
|
{
|
|
mWantsIdle = true;
|
|
mTimer->Start(100);
|
|
}
|
|
|
|
void VSTEffect::NeedEditIdle(bool state)
|
|
{
|
|
mWantsEditIdle = state;
|
|
mTimer->Start(100);
|
|
}
|
|
|
|
VstTimeInfo *VSTEffect::GetTimeInfo()
|
|
{
|
|
mTimeInfo.nanoSeconds = wxGetLocalTimeMillis().ToDouble();
|
|
return &mTimeInfo;
|
|
}
|
|
|
|
float VSTEffect::GetSampleRate()
|
|
{
|
|
return mTimeInfo.sampleRate;
|
|
}
|
|
|
|
int VSTEffect::GetProcessLevel()
|
|
{
|
|
return mProcessLevel;
|
|
}
|
|
|
|
void VSTEffect::PowerOn()
|
|
{
|
|
if (!mHasPower)
|
|
{
|
|
// Turn the power on
|
|
callDispatcher(effMainsChanged, 0, 1, NULL, 0.0);
|
|
|
|
// Tell the effect we're going to start processing
|
|
if (mVstVersion >= 2)
|
|
{
|
|
callDispatcher(effStartProcess, 0, 0, NULL, 0.0);
|
|
}
|
|
|
|
// Set state
|
|
mHasPower = true;
|
|
}
|
|
}
|
|
|
|
void VSTEffect::PowerOff()
|
|
{
|
|
if (mHasPower)
|
|
{
|
|
// Tell the effect we're going to stop processing
|
|
if (mVstVersion >= 2)
|
|
{
|
|
callDispatcher(effStopProcess, 0, 0, NULL, 0.0);
|
|
}
|
|
|
|
// Turn the power off
|
|
callDispatcher(effMainsChanged, 0, 0, NULL, 0.0);
|
|
|
|
// Set state
|
|
mHasPower = false;
|
|
}
|
|
}
|
|
|
|
void VSTEffect::SizeWindow(int w, int h)
|
|
{
|
|
// Queue the event to make the resizes smoother
|
|
if (mParent)
|
|
{
|
|
wxCommandEvent sw(EVT_SIZEWINDOW);
|
|
sw.SetInt(w);
|
|
sw.SetExtraLong(h);
|
|
mParent->GetEventHandler()->AddPendingEvent(sw);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
void VSTEffect::UpdateDisplay()
|
|
{
|
|
#if 0
|
|
// Tell the dialog to refresh effect information
|
|
if (mParent)
|
|
{
|
|
wxCommandEvent ud(EVT_UPDATEDISPLAY);
|
|
mParent->GetEventHandler()->AddPendingEvent(ud);
|
|
}
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
void VSTEffect::Automate(int index, float value)
|
|
{
|
|
// Just ignore it if we're a slave
|
|
if (mMaster)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (const auto &slave : mSlaves)
|
|
slave->callSetParameter(index, value);
|
|
|
|
return;
|
|
}
|
|
|
|
void VSTEffect::SetBufferDelay(int samples)
|
|
{
|
|
// We do not support negative delay
|
|
if (samples >= 0 && mUseLatency)
|
|
{
|
|
mBufferDelay = samples;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
int VSTEffect::GetString(wxString & outstr, int opcode, int index)
|
|
{
|
|
char buf[256];
|
|
|
|
memset(buf, 0, sizeof(buf));
|
|
|
|
callDispatcher(opcode, index, 0, buf, 0.0);
|
|
|
|
outstr = wxString::FromUTF8(buf);
|
|
|
|
return 0;
|
|
}
|
|
|
|
wxString VSTEffect::GetString(int opcode, int index)
|
|
{
|
|
wxString str;
|
|
|
|
GetString(str, opcode, index);
|
|
|
|
return str;
|
|
}
|
|
|
|
void VSTEffect::SetString(int opcode, const wxString & str, int index)
|
|
{
|
|
char buf[256];
|
|
strcpy(buf, str.Left(255).ToUTF8());
|
|
|
|
callDispatcher(opcode, index, 0, buf, 0.0);
|
|
}
|
|
|
|
intptr_t VSTEffect::callDispatcher(int opcode,
|
|
int index, intptr_t value, void *ptr, float opt)
|
|
{
|
|
// Needed since we might be in the dispatcher when the timer pops
|
|
wxCRIT_SECT_LOCKER(locker, mDispatcherLock);
|
|
return mAEffect->dispatcher(mAEffect, opcode, index, value, ptr, opt);
|
|
}
|
|
|
|
void VSTEffect::callProcessReplacing(float **inputs,
|
|
float **outputs, int sampleframes)
|
|
{
|
|
mAEffect->processReplacing(mAEffect, inputs, outputs, sampleframes);
|
|
}
|
|
|
|
float VSTEffect::callGetParameter(int index)
|
|
{
|
|
return mAEffect->getParameter(mAEffect, index);
|
|
}
|
|
|
|
void VSTEffect::callSetParameter(int index, float value)
|
|
{
|
|
if (mVstVersion == 0 || callDispatcher(effCanBeAutomated, 0, index, NULL, 0.0))
|
|
{
|
|
mAEffect->setParameter(mAEffect, index, value);
|
|
|
|
for (const auto &slave : mSlaves)
|
|
slave->callSetParameter(index, value);
|
|
}
|
|
}
|
|
|
|
void VSTEffect::callSetProgram(int index)
|
|
{
|
|
callDispatcher(effBeginSetProgram, 0, 0, NULL, 0.0);
|
|
|
|
callDispatcher(effSetProgram, 0, index, NULL, 0.0);
|
|
for (const auto &slave : mSlaves)
|
|
slave->callSetProgram(index);
|
|
|
|
callDispatcher(effEndSetProgram, 0, 0, NULL, 0.0);
|
|
}
|
|
|
|
void VSTEffect::callSetChunk(bool isPgm, int len, void *buf)
|
|
{
|
|
VstPatchChunkInfo info;
|
|
|
|
memset(&info, 0, sizeof(info));
|
|
info.version = 1;
|
|
info.pluginUniqueID = mAEffect->uniqueID;
|
|
info.pluginVersion = mAEffect->version;
|
|
info.numElements = isPgm ? mAEffect->numParams : mAEffect->numPrograms;
|
|
|
|
callSetChunk(isPgm, len, buf, &info);
|
|
}
|
|
|
|
void VSTEffect::callSetChunk(bool isPgm, int len, void *buf, VstPatchChunkInfo *info)
|
|
{
|
|
if (isPgm)
|
|
{
|
|
// Ask the effect if this is an acceptable program
|
|
if (callDispatcher(effBeginLoadProgram, 0, 0, info, 0.0) == -1)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Ask the effect if this is an acceptable bank
|
|
if (callDispatcher(effBeginLoadBank, 0, 0, info, 0.0) == -1)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
callDispatcher(effBeginSetProgram, 0, 0, NULL, 0.0);
|
|
callDispatcher(effSetChunk, isPgm ? 1 : 0, len, buf, 0.0);
|
|
callDispatcher(effEndSetProgram, 0, 0, NULL, 0.0);
|
|
|
|
for (const auto &slave : mSlaves)
|
|
slave->callSetChunk(isPgm, len, buf, info);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Base64 en/decoding
|
|
//
|
|
// Original routines marked as public domain and found at:
|
|
//
|
|
// http://en.wikibooks.org/wiki/Algorithm_implementation/Miscellaneous/Base64
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Lookup table for encoding
|
|
const static wxChar cset[] = wxT("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
|
|
const static char padc = wxT('=');
|
|
|
|
wxString VSTEffect::b64encode(const void *in, int len)
|
|
{
|
|
unsigned char *p = (unsigned char *) in;
|
|
wxString out;
|
|
|
|
unsigned long temp;
|
|
for (int i = 0; i < len / 3; i++)
|
|
{
|
|
temp = (*p++) << 16; //Convert to big endian
|
|
temp += (*p++) << 8;
|
|
temp += (*p++);
|
|
out += cset[(temp & 0x00FC0000) >> 18];
|
|
out += cset[(temp & 0x0003F000) >> 12];
|
|
out += cset[(temp & 0x00000FC0) >> 6];
|
|
out += cset[(temp & 0x0000003F)];
|
|
}
|
|
|
|
switch (len % 3)
|
|
{
|
|
case 1:
|
|
temp = (*p++) << 16; //Convert to big endian
|
|
out += cset[(temp & 0x00FC0000) >> 18];
|
|
out += cset[(temp & 0x0003F000) >> 12];
|
|
out += padc;
|
|
out += padc;
|
|
break;
|
|
|
|
case 2:
|
|
temp = (*p++) << 16; //Convert to big endian
|
|
temp += (*p++) << 8;
|
|
out += cset[(temp & 0x00FC0000) >> 18];
|
|
out += cset[(temp & 0x0003F000) >> 12];
|
|
out += cset[(temp & 0x00000FC0) >> 6];
|
|
out += padc;
|
|
break;
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
int VSTEffect::b64decode(const wxString &in, void *out)
|
|
{
|
|
int len = in.length();
|
|
unsigned char *p = (unsigned char *) out;
|
|
|
|
if (len % 4) //Sanity check
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
int padding = 0;
|
|
if (len)
|
|
{
|
|
if (in[len - 1] == padc)
|
|
{
|
|
padding++;
|
|
}
|
|
|
|
if (in[len - 2] == padc)
|
|
{
|
|
padding++;
|
|
}
|
|
}
|
|
|
|
//const char *a = in.mb_str();
|
|
//Setup a vector to hold the result
|
|
unsigned long temp = 0; //Holds decoded quanta
|
|
int i = 0;
|
|
while (i < len)
|
|
{
|
|
for (int quantumPosition = 0; quantumPosition < 4; quantumPosition++)
|
|
{
|
|
unsigned char c = in[i];
|
|
temp <<= 6;
|
|
|
|
if (c >= 0x41 && c <= 0x5A)
|
|
{
|
|
temp |= c - 0x41;
|
|
}
|
|
else if (c >= 0x61 && c <= 0x7A)
|
|
{
|
|
temp |= c - 0x47;
|
|
}
|
|
else if (c >= 0x30 && c <= 0x39)
|
|
{
|
|
temp |= c + 0x04;
|
|
}
|
|
else if (c == 0x2B)
|
|
{
|
|
temp |= 0x3E;
|
|
}
|
|
else if (c == 0x2F)
|
|
{
|
|
temp |= 0x3F;
|
|
}
|
|
else if (c == padc)
|
|
{
|
|
switch (len - i)
|
|
{
|
|
case 1: //One pad character
|
|
*p++ = (temp >> 16) & 0x000000FF;
|
|
*p++ = (temp >> 8) & 0x000000FF;
|
|
return p - (unsigned char *) out;
|
|
case 2: //Two pad characters
|
|
*p++ = (temp >> 10) & 0x000000FF;
|
|
return p - (unsigned char *) out;
|
|
}
|
|
}
|
|
i++;
|
|
}
|
|
*p++ = (temp >> 16) & 0x000000FF;
|
|
*p++ = (temp >> 8) & 0x000000FF;
|
|
*p++ = temp & 0x000000FF;
|
|
}
|
|
|
|
return p - (unsigned char *) out;
|
|
}
|
|
|
|
void VSTEffect::RemoveHandler()
|
|
{
|
|
}
|
|
|
|
static void OnSize(wxSizeEvent & evt)
|
|
{
|
|
evt.Skip();
|
|
|
|
// Once the parent dialog reaches its final size as indicated by
|
|
// a non-default minimum size, we set the maximum size to match.
|
|
// This is a bit of a hack to prevent VSTs GUI windows from resizing
|
|
// there's no real reason to allow it. But, there should be a better
|
|
// way of handling it.
|
|
wxWindow *w = (wxWindow *) evt.GetEventObject();
|
|
wxSize sz = w->GetMinSize();
|
|
|
|
if (sz != wxDefaultSize)
|
|
{
|
|
w->SetMaxSize(sz);
|
|
}
|
|
}
|
|
|
|
void VSTEffect::BuildFancy()
|
|
{
|
|
// Turn the power on...some effects need this when the editor is open
|
|
PowerOn();
|
|
|
|
auto control = Destroy_ptr<VSTControl>{ safenew VSTControl };
|
|
if (!control)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!control->Create(mParent, this))
|
|
{
|
|
return;
|
|
}
|
|
|
|
{
|
|
auto mainSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
|
|
|
|
mainSizer->Add((mControl = control.release()), 0, wxALIGN_CENTER);
|
|
|
|
mParent->SetMinSize(wxDefaultSize);
|
|
mParent->SetSizer(mainSizer.release());
|
|
}
|
|
|
|
NeedEditIdle(true);
|
|
|
|
mDialog->Bind(wxEVT_SIZE, OnSize);
|
|
|
|
#ifdef __WXMAC__
|
|
#ifdef __WX_EVTLOOP_BUSY_WAITING__
|
|
wxEventLoop::SetBusyWaiting(true);
|
|
#endif
|
|
#endif
|
|
|
|
return;
|
|
}
|
|
|
|
void VSTEffect::BuildPlain()
|
|
{
|
|
wxASSERT(mParent); // To justify safenew
|
|
wxScrolledWindow *const scroller = safenew wxScrolledWindow(mParent,
|
|
wxID_ANY,
|
|
wxDefaultPosition,
|
|
wxDefaultSize,
|
|
wxVSCROLL | wxTAB_TRAVERSAL);
|
|
|
|
{
|
|
auto mainSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
|
|
|
|
// Try to give the window a sensible default/minimum size
|
|
scroller->SetMinSize(wxSize(wxMax(600, mParent->GetSize().GetWidth() * 2 / 3),
|
|
mParent->GetSize().GetHeight() / 2));
|
|
scroller->SetScrollRate(0, 20);
|
|
|
|
// This fools NVDA into not saying "Panel" when the dialog gets focus
|
|
scroller->SetName(wxT("\a"));
|
|
scroller->SetLabel(wxT("\a"));
|
|
|
|
mainSizer->Add(scroller, 1, wxEXPAND | wxALL, 5);
|
|
mParent->SetSizer(mainSizer.release());
|
|
}
|
|
|
|
mNames.reinit(static_cast<size_t>(mAEffect->numParams));
|
|
mSliders.reinit(static_cast<size_t>(mAEffect->numParams));
|
|
mDisplays.reinit(static_cast<size_t>(mAEffect->numParams));
|
|
mLabels.reinit(static_cast<size_t>(mAEffect->numParams));
|
|
|
|
{
|
|
auto paramSizer = std::make_unique<wxStaticBoxSizer>(wxVERTICAL, scroller, _("Effect Settings"));
|
|
|
|
{
|
|
auto gridSizer = std::make_unique<wxFlexGridSizer>(4, 0, 0);
|
|
gridSizer->AddGrowableCol(1);
|
|
|
|
// Add the duration control for generators
|
|
if (GetType() == EffectTypeGenerate)
|
|
{
|
|
wxControl *item = safenew wxStaticText(scroller, 0, _("Duration:"));
|
|
gridSizer->Add(item, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, 5);
|
|
mDuration = safenew
|
|
NumericTextCtrl(scroller, ID_Duration,
|
|
NumericConverter::TIME,
|
|
mHost->GetDurationFormat(),
|
|
mHost->GetDuration(),
|
|
mSampleRate,
|
|
NumericTextCtrl::Options{}
|
|
.AutoPos(true));
|
|
mDuration->SetName(_("Duration"));
|
|
gridSizer->Add(mDuration, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5);
|
|
gridSizer->Add(1, 1, 0);
|
|
gridSizer->Add(1, 1, 0);
|
|
}
|
|
|
|
// Find the longest parameter name.
|
|
int namew = 0;
|
|
int w;
|
|
int h;
|
|
for (int i = 0; i < mAEffect->numParams; i++)
|
|
{
|
|
wxString text = GetString(effGetParamName, i);
|
|
|
|
if (text.Right(1) != wxT(':'))
|
|
{
|
|
text += wxT(':');
|
|
}
|
|
|
|
scroller->GetTextExtent(text, &w, &h);
|
|
if (w > namew)
|
|
{
|
|
namew = w;
|
|
}
|
|
}
|
|
|
|
scroller->GetTextExtent(wxT("HHHHHHHH"), &w, &h);
|
|
|
|
for (int i = 0; i < mAEffect->numParams; i++)
|
|
{
|
|
mNames[i] = safenew wxStaticText(scroller,
|
|
wxID_ANY,
|
|
wxEmptyString,
|
|
wxDefaultPosition,
|
|
wxSize(namew, -1),
|
|
wxALIGN_RIGHT | wxST_NO_AUTORESIZE);
|
|
gridSizer->Add(mNames[i], 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, 5);
|
|
|
|
mSliders[i] = safenew wxSlider(scroller,
|
|
ID_Sliders + i,
|
|
0,
|
|
0,
|
|
1000,
|
|
wxDefaultPosition,
|
|
wxSize(200, -1));
|
|
gridSizer->Add(mSliders[i], 0, wxALIGN_CENTER_VERTICAL | wxEXPAND | wxALL, 5);
|
|
|
|
mDisplays[i] = safenew wxStaticText(scroller,
|
|
wxID_ANY,
|
|
wxEmptyString,
|
|
wxDefaultPosition,
|
|
wxSize(w, -1),
|
|
wxALIGN_RIGHT | wxST_NO_AUTORESIZE);
|
|
gridSizer->Add(mDisplays[i], 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, 5);
|
|
|
|
mLabels[i] = safenew wxStaticText(scroller,
|
|
wxID_ANY,
|
|
wxEmptyString,
|
|
wxDefaultPosition,
|
|
wxSize(w, -1),
|
|
wxALIGN_LEFT | wxST_NO_AUTORESIZE);
|
|
gridSizer->Add(mLabels[i], 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, 5);
|
|
}
|
|
|
|
paramSizer->Add(gridSizer.release(), 1, wxEXPAND | wxALL, 5);
|
|
}
|
|
scroller->SetSizer(paramSizer.release());
|
|
}
|
|
|
|
RefreshParameters();
|
|
|
|
mSliders[0]->SetFocus();
|
|
}
|
|
|
|
void VSTEffect::RefreshParameters(int skip)
|
|
{
|
|
if (!mNames)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < mAEffect->numParams; i++)
|
|
{
|
|
wxString text = GetString(effGetParamName, i);
|
|
|
|
text = text.Trim(true).Trim(false);
|
|
|
|
wxString name = text;
|
|
|
|
if (text.Right(1) != wxT(':'))
|
|
{
|
|
text += wxT(':');
|
|
}
|
|
mNames[i]->SetLabel(text);
|
|
|
|
// For some parameters types like on/off, setting the slider value has
|
|
// a side effect that causes it to only move when the parameter changes
|
|
// from off to on. However, this prevents changing the value using the
|
|
// keyboard, so we skip the active slider if any.
|
|
if (i != skip)
|
|
{
|
|
mSliders[i]->SetValue(callGetParameter(i) * 1000);
|
|
}
|
|
name = text;
|
|
|
|
text = GetString(effGetParamDisplay, i);
|
|
if (text.IsEmpty())
|
|
{
|
|
text.Printf(wxT("%.5g"),callGetParameter(i));
|
|
}
|
|
mDisplays[i]->SetLabel(wxString::Format(wxT("%8s"), text));
|
|
name += wxT(' ') + text;
|
|
|
|
text = GetString(effGetParamDisplay, i);
|
|
if (!text.IsEmpty())
|
|
{
|
|
text.Printf(wxT("%-8s"), GetString(effGetParamLabel, i));
|
|
mLabels[i]->SetLabel(wxString::Format(wxT("%8s"), text));
|
|
name += wxT(' ') + text;
|
|
}
|
|
|
|
mSliders[i]->SetName(name);
|
|
}
|
|
}
|
|
|
|
void VSTEffect::OnSizeWindow(wxCommandEvent & evt)
|
|
{
|
|
if (!mControl)
|
|
{
|
|
return;
|
|
}
|
|
|
|
mControl->SetMinSize(wxSize(evt.GetInt(), (int) evt.GetExtraLong()));
|
|
mControl->SetSize(wxSize(evt.GetInt(), (int) evt.GetExtraLong()));
|
|
|
|
// DO NOT CHANGE THE ORDER OF THESE
|
|
//
|
|
// Guitar Rig (and possibly others) Cocoa VSTs can resize too large
|
|
// if the bounds are unlimited.
|
|
mDialog->SetMinSize(wxDefaultSize);
|
|
mDialog->SetMaxSize(wxDefaultSize);
|
|
mDialog->Layout();
|
|
mDialog->SetMinSize(mDialog->GetBestSize());
|
|
mDialog->SetMaxSize(mDialog->GetBestSize());
|
|
mDialog->Fit();
|
|
}
|
|
|
|
void VSTEffect::OnSlider(wxCommandEvent & evt)
|
|
{
|
|
wxSlider *s = (wxSlider *) evt.GetEventObject();
|
|
int i = s->GetId() - ID_Sliders;
|
|
|
|
callSetParameter(i, s->GetValue() / 1000.0);
|
|
|
|
RefreshParameters(i);
|
|
}
|
|
|
|
bool VSTEffect::LoadFXB(const wxFileName & fn)
|
|
{
|
|
bool ret = false;
|
|
|
|
// Try to open the file...will be closed automatically when method returns
|
|
wxFFile f(fn.GetFullPath(), wxT("rb"));
|
|
if (!f.IsOpened())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Allocate memory for the contents
|
|
ArrayOf<unsigned char> data{ size_t(f.Length()) };
|
|
if (!data)
|
|
{
|
|
AudacityMessageBox(_("Unable to allocate memory when loading presets file."),
|
|
_("Error Loading VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
return false;
|
|
}
|
|
unsigned char *bptr = data.get();
|
|
|
|
do
|
|
{
|
|
// Read in the whole file
|
|
ssize_t len = f.Read((void *) bptr, f.Length());
|
|
if (f.Error())
|
|
{
|
|
AudacityMessageBox(_("Unable to read presets file."),
|
|
_("Error Loading VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
break;
|
|
}
|
|
|
|
// Most references to the data are via an "int" array
|
|
int32_t *iptr = (int32_t *) bptr;
|
|
|
|
// Verify that we have at least enough for the header
|
|
if (len < 156)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Verify that we probably have an FX file
|
|
if (wxINT32_SWAP_ON_LE(iptr[0]) != CCONST('C', 'c', 'n', 'K'))
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Ignore the size...sometimes it's there, other times it's zero
|
|
|
|
// Get the version and verify
|
|
int version = wxINT32_SWAP_ON_LE(iptr[3]);
|
|
if (version != 1 && version != 2)
|
|
{
|
|
break;
|
|
}
|
|
|
|
VstPatchChunkInfo info =
|
|
{
|
|
1,
|
|
wxINT32_SWAP_ON_LE(iptr[4]),
|
|
wxINT32_SWAP_ON_LE(iptr[5]),
|
|
wxINT32_SWAP_ON_LE(iptr[6]),
|
|
""
|
|
};
|
|
|
|
// Ensure this program looks to belong to the current plugin
|
|
if ((info.pluginUniqueID != mAEffect->uniqueID) &&
|
|
(info.pluginVersion != mAEffect->version) &&
|
|
(info.numElements != mAEffect->numPrograms))
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Get the number of programs
|
|
int numProgs = info.numElements;
|
|
|
|
// Get the current program index
|
|
int curProg = 0;
|
|
if (version >= 2)
|
|
{
|
|
curProg = wxINT32_SWAP_ON_LE(iptr[7]);
|
|
if (curProg < 0 || curProg >= numProgs)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Is it a bank of programs?
|
|
if (wxINT32_SWAP_ON_LE(iptr[2]) == CCONST('F', 'x', 'B', 'k'))
|
|
{
|
|
// Drop the header
|
|
bptr += 156;
|
|
len -= 156;
|
|
|
|
unsigned char *tempPtr = bptr;
|
|
ssize_t tempLen = len;
|
|
|
|
// Validate all of the programs
|
|
for (int i = 0; i < numProgs; i++)
|
|
{
|
|
if (!LoadFXProgram(&tempPtr, tempLen, i, true))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Ask the effect if this is an acceptable bank
|
|
if (callDispatcher(effBeginLoadBank, 0, 0, &info, 0.0) == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Start loading the individual programs
|
|
for (int i = 0; i < numProgs; i++)
|
|
{
|
|
ret = LoadFXProgram(&bptr, len, i, false);
|
|
}
|
|
}
|
|
// Or maybe a bank chunk?
|
|
else if (wxINT32_SWAP_ON_LE(iptr[2]) == CCONST('F', 'B', 'C', 'h'))
|
|
{
|
|
// Can't load programs chunks if the plugin doesn't support it
|
|
if (!(mAEffect->flags & effFlagsProgramChunks))
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Verify that we have enough to grab the chunk size
|
|
if (len < 160)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Get the chunk size
|
|
int size = wxINT32_SWAP_ON_LE(iptr[39]);
|
|
|
|
// We finally know the full length of the program
|
|
int proglen = 160 + size;
|
|
|
|
// Verify that we have enough for the entire program
|
|
if (len < proglen)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Set the entire bank in one shot
|
|
callSetChunk(false, size, &iptr[40], &info);
|
|
|
|
// Success
|
|
ret = true;
|
|
}
|
|
// Unrecognizable type
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Set the active program
|
|
if (ret && version >= 2)
|
|
{
|
|
callSetProgram(curProg);
|
|
}
|
|
} while (false);
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool VSTEffect::LoadFXP(const wxFileName & fn)
|
|
{
|
|
bool ret = false;
|
|
|
|
// Try to open the file...will be closed automatically when method returns
|
|
wxFFile f(fn.GetFullPath(), wxT("rb"));
|
|
if (!f.IsOpened())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Allocate memory for the contents
|
|
ArrayOf<unsigned char> data{ size_t(f.Length()) };
|
|
if (!data)
|
|
{
|
|
AudacityMessageBox(_("Unable to allocate memory when loading presets file."),
|
|
_("Error Loading VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
return false;
|
|
}
|
|
unsigned char *bptr = data.get();
|
|
|
|
do
|
|
{
|
|
// Read in the whole file
|
|
ssize_t len = f.Read((void *) bptr, f.Length());
|
|
if (f.Error())
|
|
{
|
|
AudacityMessageBox(_("Unable to read presets file."),
|
|
_("Error Loading VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
break;
|
|
}
|
|
|
|
// Get (or default) currently selected program
|
|
int i = 0; //mProgram->GetCurrentSelection();
|
|
if (i < 0)
|
|
{
|
|
i = 0; // default to first program
|
|
}
|
|
|
|
// Go verify and set the program
|
|
ret = LoadFXProgram(&bptr, len, i, false);
|
|
} while (false);
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool VSTEffect::LoadFXProgram(unsigned char **bptr, ssize_t & len, int index, bool dryrun)
|
|
{
|
|
// Most references to the data are via an "int" array
|
|
int32_t *iptr = (int32_t *) *bptr;
|
|
|
|
// Verify that we have at least enough for a program without parameters
|
|
if (len < 28)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Verify that we probably have an FX file
|
|
if (wxINT32_SWAP_ON_LE(iptr[0]) != CCONST('C', 'c', 'n', 'K'))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Ignore the size...sometimes it's there, other times it's zero
|
|
|
|
// Get the version and verify
|
|
#if defined(IS_THIS_AN_FXP_ARTIFICAL_LIMITATION)
|
|
int version = wxINT32_SWAP_ON_LE(iptr[3]);
|
|
if (version != 1)
|
|
{
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
VstPatchChunkInfo info =
|
|
{
|
|
1,
|
|
wxINT32_SWAP_ON_LE(iptr[4]),
|
|
wxINT32_SWAP_ON_LE(iptr[5]),
|
|
wxINT32_SWAP_ON_LE(iptr[6]),
|
|
""
|
|
};
|
|
|
|
// Ensure this program looks to belong to the current plugin
|
|
if ((info.pluginUniqueID != mAEffect->uniqueID) &&
|
|
(info.pluginVersion != mAEffect->version) &&
|
|
(info.numElements != mAEffect->numParams))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Get the number of parameters
|
|
int numParams = info.numElements;
|
|
|
|
// At this point, we have to have enough to include the program name as well
|
|
if (len < 56)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Get the program name
|
|
wxString progName(wxString::From8BitData((char *)&iptr[7]));
|
|
|
|
// Might be a regular program
|
|
if (wxINT32_SWAP_ON_LE(iptr[2]) == CCONST('F', 'x', 'C', 'k'))
|
|
{
|
|
// We finally know the full length of the program
|
|
int proglen = 56 + (numParams * sizeof(float));
|
|
|
|
// Verify that we have enough for all of the parameter values
|
|
if (len < proglen)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Validate all of the parameter values
|
|
for (int i = 0; i < numParams; i++)
|
|
{
|
|
uint32_t ival = wxUINT32_SWAP_ON_LE(iptr[14 + i]);
|
|
float val = *((float *) &ival);
|
|
if (val < 0.0 || val > 1.0)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// They look okay...time to start changing things
|
|
if (!dryrun)
|
|
{
|
|
// Ask the effect if this is an acceptable program
|
|
if (callDispatcher(effBeginLoadProgram, 0, 0, &info, 0.0) == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Load all of the parameters
|
|
callDispatcher(effBeginSetProgram, 0, 0, NULL, 0.0);
|
|
for (int i = 0; i < numParams; i++)
|
|
{
|
|
wxUint32 val = wxUINT32_SWAP_ON_LE(iptr[14 + i]);
|
|
callSetParameter(i, *((float *) &val));
|
|
}
|
|
callDispatcher(effEndSetProgram, 0, 0, NULL, 0.0);
|
|
}
|
|
|
|
// Update in case we're loading an "FxBk" format bank file
|
|
*bptr += proglen;
|
|
len -= proglen;
|
|
}
|
|
// Maybe we have a program chunk
|
|
else if (wxINT32_SWAP_ON_LE(iptr[2]) == CCONST('F', 'P', 'C', 'h'))
|
|
{
|
|
// Can't load programs chunks if the plugin doesn't support it
|
|
if (!(mAEffect->flags & effFlagsProgramChunks))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Verify that we have enough to grab the chunk size
|
|
if (len < 60)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Get the chunk size
|
|
int size = wxINT32_SWAP_ON_LE(iptr[14]);
|
|
|
|
// We finally know the full length of the program
|
|
int proglen = 60 + size;
|
|
|
|
// Verify that we have enough for the entire program
|
|
if (len < proglen)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Set the entire program in one shot
|
|
if (!dryrun)
|
|
{
|
|
callSetChunk(true, size, &iptr[15], &info);
|
|
}
|
|
|
|
// Update in case we're loading an "FxBk" format bank file
|
|
*bptr += proglen;
|
|
len -= proglen;
|
|
}
|
|
else
|
|
{
|
|
// Unknown type
|
|
return false;
|
|
}
|
|
|
|
if (!dryrun)
|
|
{
|
|
SetString(effSetProgramName, wxString(progName), index);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VSTEffect::LoadXML(const wxFileName & fn)
|
|
{
|
|
mInChunk = false;
|
|
mInSet = false;
|
|
|
|
// default to read as XML file
|
|
// Load the program
|
|
XMLFileReader reader;
|
|
bool ok = reader.Parse(this, fn.GetFullPath());
|
|
|
|
// Something went wrong with the file, clean up
|
|
if (mInSet)
|
|
{
|
|
callDispatcher(effEndSetProgram, 0, 0, NULL, 0.0);
|
|
|
|
mInSet = false;
|
|
}
|
|
|
|
if (!ok)
|
|
{
|
|
// Inform user of load failure
|
|
AudacityMessageBox(reader.GetErrorStr(),
|
|
_("Error Loading VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void VSTEffect::SaveFXB(const wxFileName & fn)
|
|
{
|
|
// Create/Open the file
|
|
const wxString fullPath{fn.GetFullPath()};
|
|
wxFFile f(fullPath, wxT("wb"));
|
|
if (!f.IsOpened())
|
|
{
|
|
AudacityMessageBox(wxString::Format(_("Could not open file: \"%s\""), fullPath),
|
|
_("Error Saving VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
return;
|
|
}
|
|
|
|
wxMemoryBuffer buf;
|
|
wxInt32 subType;
|
|
void *chunkPtr;
|
|
int chunkSize;
|
|
int dataSize = 148;
|
|
wxInt32 tab[8];
|
|
int curProg = 0 ; //mProgram->GetCurrentSelection();
|
|
|
|
if (mAEffect->flags & effFlagsProgramChunks)
|
|
{
|
|
subType = CCONST('F', 'B', 'C', 'h');
|
|
|
|
chunkSize = callDispatcher(effGetChunk, 0, 0, &chunkPtr, 0.0);
|
|
dataSize += 4 + chunkSize;
|
|
}
|
|
else
|
|
{
|
|
subType = CCONST('F', 'x', 'B', 'k');
|
|
|
|
for (int i = 0; i < mAEffect->numPrograms; i++)
|
|
{
|
|
SaveFXProgram(buf, i);
|
|
}
|
|
|
|
dataSize += buf.GetDataLen();
|
|
}
|
|
|
|
tab[0] = wxINT32_SWAP_ON_LE(CCONST('C', 'c', 'n', 'K'));
|
|
tab[1] = wxINT32_SWAP_ON_LE(dataSize);
|
|
tab[2] = wxINT32_SWAP_ON_LE(subType);
|
|
tab[3] = wxINT32_SWAP_ON_LE(curProg >= 0 ? 2 : 1);
|
|
tab[4] = wxINT32_SWAP_ON_LE(mAEffect->uniqueID);
|
|
tab[5] = wxINT32_SWAP_ON_LE(mAEffect->version);
|
|
tab[6] = wxINT32_SWAP_ON_LE(mAEffect->numPrograms);
|
|
tab[7] = wxINT32_SWAP_ON_LE(curProg >= 0 ? curProg : 0);
|
|
|
|
f.Write(tab, sizeof(tab));
|
|
if (!f.Error())
|
|
{
|
|
char padding[124];
|
|
memset(padding, 0, sizeof(padding));
|
|
f.Write(padding, sizeof(padding));
|
|
|
|
if (!f.Error())
|
|
{
|
|
if (mAEffect->flags & effFlagsProgramChunks)
|
|
{
|
|
wxInt32 size = wxINT32_SWAP_ON_LE(chunkSize);
|
|
f.Write(&size, sizeof(size));
|
|
f.Write(chunkPtr, chunkSize);
|
|
}
|
|
else
|
|
{
|
|
f.Write(buf.GetData(), buf.GetDataLen());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (f.Error())
|
|
{
|
|
AudacityMessageBox(wxString::Format(_("Error writing to file: \"%s\""), fullPath),
|
|
_("Error Saving VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
}
|
|
|
|
f.Close();
|
|
|
|
return;
|
|
}
|
|
|
|
void VSTEffect::SaveFXP(const wxFileName & fn)
|
|
{
|
|
// Create/Open the file
|
|
const wxString fullPath{ fn.GetFullPath() };
|
|
wxFFile f(fullPath, wxT("wb"));
|
|
if (!f.IsOpened())
|
|
{
|
|
AudacityMessageBox(wxString::Format(_("Could not open file: \"%s\""), fullPath),
|
|
_("Error Saving VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
return;
|
|
}
|
|
|
|
wxMemoryBuffer buf;
|
|
|
|
int ndx = callDispatcher(effGetProgram, 0, 0, NULL, 0.0);
|
|
SaveFXProgram(buf, ndx);
|
|
|
|
f.Write(buf.GetData(), buf.GetDataLen());
|
|
if (f.Error())
|
|
{
|
|
AudacityMessageBox(wxString::Format(_("Error writing to file: \"%s\""), fullPath),
|
|
_("Error Saving VST Presets"),
|
|
wxOK | wxCENTRE,
|
|
mParent);
|
|
}
|
|
|
|
f.Close();
|
|
|
|
return;
|
|
}
|
|
|
|
void VSTEffect::SaveFXProgram(wxMemoryBuffer & buf, int index)
|
|
{
|
|
wxInt32 subType;
|
|
void *chunkPtr;
|
|
int chunkSize;
|
|
int dataSize = 48;
|
|
char progName[28];
|
|
wxInt32 tab[7];
|
|
|
|
callDispatcher(effGetProgramNameIndexed, index, 0, &progName, 0.0);
|
|
progName[27] = '\0';
|
|
chunkSize = strlen(progName);
|
|
memset(&progName[chunkSize], 0, sizeof(progName) - chunkSize);
|
|
|
|
if (mAEffect->flags & effFlagsProgramChunks)
|
|
{
|
|
subType = CCONST('F', 'P', 'C', 'h');
|
|
|
|
chunkSize = callDispatcher(effGetChunk, 1, 0, &chunkPtr, 0.0);
|
|
dataSize += 4 + chunkSize;
|
|
}
|
|
else
|
|
{
|
|
subType = CCONST('F', 'x', 'C', 'k');
|
|
|
|
dataSize += (mAEffect->numParams << 2);
|
|
}
|
|
|
|
tab[0] = wxINT32_SWAP_ON_LE(CCONST('C', 'c', 'n', 'K'));
|
|
tab[1] = wxINT32_SWAP_ON_LE(dataSize);
|
|
tab[2] = wxINT32_SWAP_ON_LE(subType);
|
|
tab[3] = wxINT32_SWAP_ON_LE(1);
|
|
tab[4] = wxINT32_SWAP_ON_LE(mAEffect->uniqueID);
|
|
tab[5] = wxINT32_SWAP_ON_LE(mAEffect->version);
|
|
tab[6] = wxINT32_SWAP_ON_LE(mAEffect->numParams);
|
|
|
|
buf.AppendData(tab, sizeof(tab));
|
|
buf.AppendData(progName, sizeof(progName));
|
|
|
|
if (mAEffect->flags & effFlagsProgramChunks)
|
|
{
|
|
wxInt32 size = wxINT32_SWAP_ON_LE(chunkSize);
|
|
buf.AppendData(&size, sizeof(size));
|
|
buf.AppendData(chunkPtr, chunkSize);
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < mAEffect->numParams; i++)
|
|
{
|
|
float val = callGetParameter(i);
|
|
wxUint32 ival = wxUINT32_SWAP_ON_LE(*((wxUint32 *) &val));
|
|
buf.AppendData(&ival, sizeof(ival));
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Throws exceptions rather than giving error return.
|
|
void VSTEffect::SaveXML(const wxFileName & fn)
|
|
// may throw
|
|
{
|
|
XMLFileWriter xmlFile{ fn.GetFullPath(), _("Error Saving Effect Presets") };
|
|
|
|
xmlFile.StartTag(wxT("vstprogrampersistence"));
|
|
xmlFile.WriteAttr(wxT("version"), wxT("2"));
|
|
|
|
xmlFile.StartTag(wxT("effect"));
|
|
xmlFile.WriteAttr(wxT("name"), GetName());
|
|
xmlFile.WriteAttr(wxT("uniqueID"), mAEffect->uniqueID);
|
|
xmlFile.WriteAttr(wxT("version"), mAEffect->version);
|
|
xmlFile.WriteAttr(wxT("numParams"), mAEffect->numParams);
|
|
|
|
xmlFile.StartTag(wxT("program"));
|
|
xmlFile.WriteAttr(wxT("name"), wxEmptyString); //mProgram->GetValue());
|
|
|
|
int clen = 0;
|
|
if (mAEffect->flags & effFlagsProgramChunks)
|
|
{
|
|
void *chunk = NULL;
|
|
|
|
clen = (int) callDispatcher(effGetChunk, 1, 0, &chunk, 0.0);
|
|
if (clen != 0)
|
|
{
|
|
xmlFile.StartTag(wxT("chunk"));
|
|
xmlFile.WriteSubTree(VSTEffect::b64encode(chunk, clen) + wxT('\n'));
|
|
xmlFile.EndTag(wxT("chunk"));
|
|
}
|
|
}
|
|
|
|
if (clen == 0)
|
|
{
|
|
for (int i = 0; i < mAEffect->numParams; i++)
|
|
{
|
|
xmlFile.StartTag(wxT("param"));
|
|
|
|
xmlFile.WriteAttr(wxT("index"), i);
|
|
xmlFile.WriteAttr(wxT("name"),
|
|
GetString(effGetParamName, i));
|
|
xmlFile.WriteAttr(wxT("value"),
|
|
wxString::Format(wxT("%f"),
|
|
callGetParameter(i)));
|
|
|
|
xmlFile.EndTag(wxT("param"));
|
|
}
|
|
}
|
|
|
|
xmlFile.EndTag(wxT("program"));
|
|
|
|
xmlFile.EndTag(wxT("effect"));
|
|
|
|
xmlFile.EndTag(wxT("vstprogrampersistence"));
|
|
|
|
xmlFile.Commit();
|
|
}
|
|
|
|
bool VSTEffect::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
|
|
{
|
|
if (wxStrcmp(tag, wxT("vstprogrampersistence")) == 0)
|
|
{
|
|
while (*attrs)
|
|
{
|
|
const wxChar *attr = *attrs++;
|
|
const wxChar *value = *attrs++;
|
|
|
|
if (!value)
|
|
{
|
|
break;
|
|
}
|
|
|
|
const wxString strValue = value;
|
|
|
|
if (wxStrcmp(attr, wxT("version")) == 0)
|
|
{
|
|
if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&mXMLVersion))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (mXMLVersion < 1 || mXMLVersion > 2)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (wxStrcmp(tag, wxT("effect")) == 0)
|
|
{
|
|
memset(&mXMLInfo, 0, sizeof(mXMLInfo));
|
|
mXMLInfo.version = 1;
|
|
mXMLInfo.pluginUniqueID = mAEffect->uniqueID;
|
|
mXMLInfo.pluginVersion = mAEffect->version;
|
|
mXMLInfo.numElements = mAEffect->numParams;
|
|
|
|
while (*attrs)
|
|
{
|
|
const wxChar *attr = *attrs++;
|
|
const wxChar *value = *attrs++;
|
|
|
|
if (!value)
|
|
{
|
|
break;
|
|
}
|
|
|
|
const wxString strValue = value;
|
|
|
|
if (wxStrcmp(attr, wxT("name")) == 0)
|
|
{
|
|
if (!XMLValueChecker::IsGoodString(strValue))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (value != GetName())
|
|
{
|
|
wxString msg;
|
|
msg.Printf(_("This parameter file was saved from %s. Continue?"), value);
|
|
int result = AudacityMessageBox(msg, wxT("Confirm"), wxYES_NO, mParent);
|
|
if (result == wxNO)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else if (wxStrcmp(attr, wxT("version")) == 0)
|
|
{
|
|
long version;
|
|
if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&version))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
mXMLInfo.pluginVersion = (int) version;
|
|
}
|
|
else if (mXMLVersion > 1 && wxStrcmp(attr, wxT("uniqueID")) == 0)
|
|
{
|
|
long uniqueID;
|
|
if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&uniqueID))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
mXMLInfo.pluginUniqueID = (int) uniqueID;
|
|
}
|
|
else if (mXMLVersion > 1 && wxStrcmp(attr, wxT("numParams")) == 0)
|
|
{
|
|
long numParams;
|
|
if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&numParams))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
mXMLInfo.numElements = (int) numParams;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (wxStrcmp(tag, wxT("program")) == 0)
|
|
{
|
|
while (*attrs)
|
|
{
|
|
const wxChar *attr = *attrs++;
|
|
const wxChar *value = *attrs++;
|
|
|
|
if (!value)
|
|
{
|
|
break;
|
|
}
|
|
|
|
const wxString strValue = value;
|
|
|
|
if (wxStrcmp(attr, wxT("name")) == 0)
|
|
{
|
|
if (!XMLValueChecker::IsGoodString(strValue))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (strValue.length() > 24)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int ndx = 0; //mProgram->GetCurrentSelection();
|
|
if (ndx == wxNOT_FOUND)
|
|
{
|
|
ndx = 0;
|
|
}
|
|
|
|
SetString(effSetProgramName, strValue, ndx);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
mInChunk = false;
|
|
|
|
if (callDispatcher(effBeginLoadProgram, 0, 0, &mXMLInfo, 0.0) == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
callDispatcher(effBeginSetProgram, 0, 0, NULL, 0.0);
|
|
|
|
mInSet = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
if (wxStrcmp(tag, wxT("param")) == 0)
|
|
{
|
|
long ndx = -1;
|
|
double val = -1.0;
|
|
while (*attrs)
|
|
{
|
|
const wxChar *attr = *attrs++;
|
|
const wxChar *value = *attrs++;
|
|
|
|
if (!value)
|
|
{
|
|
break;
|
|
}
|
|
|
|
const wxString strValue = value;
|
|
|
|
if (wxStrcmp(attr, wxT("index")) == 0)
|
|
{
|
|
if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&ndx))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ndx < 0 || ndx >= mAEffect->numParams)
|
|
{
|
|
// Could be a different version of the effect...probably should
|
|
// tell the user
|
|
return false;
|
|
}
|
|
}
|
|
else if (wxStrcmp(attr, wxT("name")) == 0)
|
|
{
|
|
if (!XMLValueChecker::IsGoodString(strValue))
|
|
{
|
|
return false;
|
|
}
|
|
// Nothing to do with it for now
|
|
}
|
|
else if (wxStrcmp(attr, wxT("value")) == 0)
|
|
{
|
|
if (!XMLValueChecker::IsGoodInt(strValue) ||
|
|
!Internat::CompatibleToDouble(strValue, &val))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (val < 0.0 || val > 1.0)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ndx == -1 || val == -1.0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
callSetParameter(ndx, val);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (wxStrcmp(tag, wxT("chunk")) == 0)
|
|
{
|
|
mInChunk = true;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void VSTEffect::HandleXMLEndTag(const wxChar *tag)
|
|
{
|
|
if (wxStrcmp(tag, wxT("chunk")) == 0)
|
|
{
|
|
if (mChunk.length())
|
|
{
|
|
ArrayOf<char> buf{ mChunk.length() / 4 * 3 };
|
|
|
|
int len = VSTEffect::b64decode(mChunk, buf.get());
|
|
if (len)
|
|
{
|
|
callSetChunk(true, len, buf.get(), &mXMLInfo);
|
|
}
|
|
|
|
mChunk.clear();
|
|
}
|
|
mInChunk = false;
|
|
}
|
|
|
|
if (wxStrcmp(tag, wxT("program")) == 0)
|
|
{
|
|
if (mInSet)
|
|
{
|
|
callDispatcher(effEndSetProgram, 0, 0, NULL, 0.0);
|
|
|
|
mInSet = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void VSTEffect::HandleXMLContent(const wxString & content)
|
|
{
|
|
if (mInChunk)
|
|
{
|
|
mChunk += wxString(content).Trim(true).Trim(false);
|
|
}
|
|
}
|
|
|
|
XMLTagHandler *VSTEffect::HandleXMLChild(const wxChar *tag)
|
|
{
|
|
if (wxStrcmp(tag, wxT("vstprogrampersistence")) == 0)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
if (wxStrcmp(tag, wxT("effect")) == 0)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
if (wxStrcmp(tag, wxT("program")) == 0)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
if (wxStrcmp(tag, wxT("param")) == 0)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
if (wxStrcmp(tag, wxT("chunk")) == 0)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
#endif // USE_VST
|