mirror of
https://github.com/cookiengineer/audacity
synced 2025-05-01 16:19:43 +02:00
My code is for normal Nyquist effects. This fix also incorporates a fix from Leland for Nyquist Prompt. Thanks Leland.
3488 lines
108 KiB
C++
3488 lines
108 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
Nyquist.cpp
|
|
|
|
Dominic Mazzoni
|
|
|
|
******************************************************************//**
|
|
|
|
\class NyquistEffect
|
|
\brief An Effect that calls up a Nyquist (XLISP) plug-in, i.e. many possible
|
|
effects from this one class.
|
|
|
|
*//****************************************************************//**
|
|
|
|
\class NyquistOutputDialog
|
|
\brief Dialog used with NyquistEffect
|
|
|
|
*//****************************************************************//**
|
|
|
|
\class NyqControl
|
|
\brief A control on a NyquistDialog.
|
|
|
|
*//*******************************************************************/
|
|
|
|
#include "../../Audacity.h" // for USE_* macros
|
|
#include "Nyquist.h"
|
|
|
|
#include "../../Experimental.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
#include <locale.h>
|
|
|
|
#include <wx/button.h>
|
|
#include <wx/checkbox.h>
|
|
#include <wx/choice.h>
|
|
#include <wx/datetime.h>
|
|
#include <wx/intl.h>
|
|
#include <wx/log.h>
|
|
#include <wx/scrolwin.h>
|
|
#include <wx/sizer.h>
|
|
#include <wx/slider.h>
|
|
#include <wx/sstream.h>
|
|
#include <wx/stattext.h>
|
|
#include <wx/textdlg.h>
|
|
#include <wx/tokenzr.h>
|
|
#include <wx/txtstrm.h>
|
|
#include <wx/valgen.h>
|
|
#include <wx/wfstream.h>
|
|
#include <wx/numformatter.h>
|
|
#include <wx/stdpaths.h>
|
|
|
|
#include "../EffectManager.h"
|
|
#include "../../DirManager.h"
|
|
#include "../../FileNames.h"
|
|
#include "../../LabelTrack.h"
|
|
#include "../../NoteTrack.h"
|
|
#include "../../TimeTrack.h"
|
|
#include "../../prefs/SpectrogramSettings.h"
|
|
#include "../../Project.h"
|
|
#include "../../ProjectSettings.h"
|
|
#include "../../ShuttleGetDefinition.h"
|
|
#include "../../ShuttleGui.h"
|
|
#include "../../ViewInfo.h"
|
|
#include "../../WaveClip.h"
|
|
#include "../../WaveTrack.h"
|
|
#include "../../widgets/valnum.h"
|
|
#include "../../widgets/AudacityMessageBox.h"
|
|
#include "../../Prefs.h"
|
|
#include "../../wxFileNameWrapper.h"
|
|
#include "../../prefs/GUIPrefs.h"
|
|
#include "../../prefs/WaveformSettings.h"
|
|
#include "../../tracks/playabletrack/wavetrack/ui/WaveTrackView.h"
|
|
#include "../../tracks/playabletrack/wavetrack/ui/WaveTrackViewConstants.h"
|
|
#include "../../widgets/NumericTextCtrl.h"
|
|
#include "../../widgets/ProgressDialog.h"
|
|
|
|
#include "../../widgets/FileDialog/FileDialog.h"
|
|
|
|
#ifndef nyx_returns_start_and_end_time
|
|
#error You need to update lib-src/libnyquist
|
|
#endif
|
|
|
|
#include <locale.h>
|
|
#include <iostream>
|
|
#include <ostream>
|
|
#include <sstream>
|
|
#include <float.h>
|
|
|
|
int NyquistEffect::mReentryCount = 0;
|
|
|
|
enum
|
|
{
|
|
ID_Editor = 10000,
|
|
ID_Version,
|
|
ID_Load,
|
|
ID_Save,
|
|
|
|
ID_Slider = 11000,
|
|
ID_Text = 12000,
|
|
ID_Choice = 13000,
|
|
ID_Time = 14000,
|
|
ID_FILE = 15000
|
|
};
|
|
|
|
// Protect Nyquist from selections greater than 2^31 samples (bug 439)
|
|
#define NYQ_MAX_LEN (std::numeric_limits<long>::max())
|
|
|
|
#define UNINITIALIZED_CONTROL ((double)99999999.99)
|
|
|
|
static const wxChar *KEY_Version = wxT("Version");
|
|
static const wxChar *KEY_Command = wxT("Command");
|
|
static const wxChar *KEY_Parameters = wxT("Parameters");
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// NyquistEffect
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
BEGIN_EVENT_TABLE(NyquistEffect, wxEvtHandler)
|
|
EVT_BUTTON(ID_Load, NyquistEffect::OnLoad)
|
|
EVT_BUTTON(ID_Save, NyquistEffect::OnSave)
|
|
|
|
EVT_COMMAND_RANGE(ID_Slider, ID_Slider+99,
|
|
wxEVT_COMMAND_SLIDER_UPDATED, NyquistEffect::OnSlider)
|
|
EVT_COMMAND_RANGE(ID_Text, ID_Text+99,
|
|
wxEVT_COMMAND_TEXT_UPDATED, NyquistEffect::OnText)
|
|
EVT_COMMAND_RANGE(ID_Choice, ID_Choice + 99,
|
|
wxEVT_COMMAND_CHOICE_SELECTED, NyquistEffect::OnChoice)
|
|
EVT_COMMAND_RANGE(ID_Time, ID_Time + 99,
|
|
wxEVT_COMMAND_TEXT_UPDATED, NyquistEffect::OnTime)
|
|
EVT_COMMAND_RANGE(ID_FILE, ID_FILE + 99,
|
|
wxEVT_COMMAND_BUTTON_CLICKED, NyquistEffect::OnFileButton)
|
|
END_EVENT_TABLE()
|
|
|
|
NyquistEffect::NyquistEffect(const wxString &fName)
|
|
{
|
|
mOutputTrack[0] = mOutputTrack[1] = nullptr;
|
|
|
|
mAction = XO("Applying Nyquist Effect...");
|
|
mIsPrompt = false;
|
|
mExternal = false;
|
|
mCompiler = false;
|
|
mTrace = false;
|
|
mRedirectOutput = false;
|
|
mDebug = false;
|
|
mIsSal = false;
|
|
mOK = false;
|
|
mAuthor = XO("n/a");
|
|
mReleaseVersion = XO("n/a");
|
|
mCopyright = XO("n/a");
|
|
|
|
// set clip/split handling when applying over clip boundary.
|
|
mRestoreSplits = true; // Default: Restore split lines.
|
|
mMergeClips = -1; // Default (auto): Merge if length remains unchanged.
|
|
|
|
mVersion = 4;
|
|
|
|
mStop = false;
|
|
mBreak = false;
|
|
mCont = false;
|
|
mIsTool = false;
|
|
|
|
mMaxLen = NYQ_MAX_LEN;
|
|
|
|
// Interactive Nyquist
|
|
if (fName == NYQUIST_PROMPT_ID) {
|
|
mName = XO("Nyquist Prompt");
|
|
mType = EffectTypeTool;
|
|
mIsTool = true;
|
|
mPromptName = mName;
|
|
mPromptType = mType;
|
|
mOK = true;
|
|
mIsPrompt = true;
|
|
return;
|
|
}
|
|
|
|
if (fName == NYQUIST_WORKER_ID) {
|
|
// Effect spawned from Nyquist Prompt
|
|
/* i18n-hint: It is acceptable to translate this the same as for "Nyquist Prompt" */
|
|
mName = XO("Nyquist Worker");
|
|
return;
|
|
}
|
|
|
|
mFileName = fName;
|
|
// Use the file name verbatim as effect name.
|
|
// This is only a default name, overridden if we find a $name line:
|
|
mName = Verbatim( mFileName.GetName() );
|
|
mFileModified = mFileName.GetModificationTime();
|
|
ParseFile();
|
|
|
|
if (!mOK && mInitError.empty())
|
|
mInitError = XO("Ill-formed Nyquist plug-in header");
|
|
}
|
|
|
|
NyquistEffect::~NyquistEffect()
|
|
{
|
|
}
|
|
|
|
// ComponentInterface implementation
|
|
|
|
PluginPath NyquistEffect::GetPath()
|
|
{
|
|
if (mIsPrompt)
|
|
return NYQUIST_PROMPT_ID;
|
|
|
|
return mFileName.GetFullPath();
|
|
}
|
|
|
|
ComponentInterfaceSymbol NyquistEffect::GetSymbol()
|
|
{
|
|
if (mIsPrompt)
|
|
return XO("Nyquist Prompt");
|
|
|
|
return mName;
|
|
}
|
|
|
|
VendorSymbol NyquistEffect::GetVendor()
|
|
{
|
|
if (mIsPrompt)
|
|
{
|
|
return XO("Audacity");
|
|
}
|
|
|
|
return mAuthor;
|
|
}
|
|
|
|
wxString NyquistEffect::GetVersion()
|
|
{
|
|
// Are Nyquist version strings really supposed to be translatable?
|
|
// See commit a06e561 which used XO for at least one of them
|
|
return mReleaseVersion.Translation();
|
|
}
|
|
|
|
TranslatableString NyquistEffect::GetDescription()
|
|
{
|
|
return mCopyright;
|
|
}
|
|
|
|
wxString NyquistEffect::ManualPage()
|
|
{
|
|
return mIsPrompt
|
|
? wxT("Nyquist_Prompt")
|
|
: mManPage;
|
|
}
|
|
|
|
wxString NyquistEffect::HelpPage()
|
|
{
|
|
auto paths = NyquistEffect::GetNyquistSearchPath();
|
|
wxString fileName;
|
|
|
|
for (size_t i = 0, cnt = paths.size(); i < cnt; i++) {
|
|
fileName = wxFileName(paths[i] + wxT("/") + mHelpFile).GetFullPath();
|
|
if (wxFileExists(fileName)) {
|
|
mHelpFileExists = true;
|
|
return fileName;
|
|
}
|
|
}
|
|
return wxEmptyString;
|
|
}
|
|
|
|
// EffectDefinitionInterface implementation
|
|
|
|
EffectType NyquistEffect::GetType()
|
|
{
|
|
return mType;
|
|
}
|
|
|
|
EffectType NyquistEffect::GetClassification()
|
|
{
|
|
if (mIsTool)
|
|
return EffectTypeTool;
|
|
return mType;
|
|
}
|
|
|
|
EffectFamilySymbol NyquistEffect::GetFamily()
|
|
{
|
|
return NYQUISTEFFECTS_FAMILY;
|
|
}
|
|
|
|
bool NyquistEffect::IsInteractive()
|
|
{
|
|
if (mIsPrompt)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return mControls.size() != 0;
|
|
}
|
|
|
|
bool NyquistEffect::IsDefault()
|
|
{
|
|
return mIsPrompt;
|
|
}
|
|
|
|
// EffectClientInterface implementation
|
|
bool NyquistEffect::DefineParams( ShuttleParams & S )
|
|
{
|
|
// For now we assume Nyquist can do get and set better than DefineParams can,
|
|
// And so we ONLY use it for geting the signature.
|
|
auto pGa = dynamic_cast<ShuttleGetAutomation*>(&S);
|
|
if( pGa ){
|
|
GetAutomationParameters( *(pGa->mpEap) );
|
|
return true;
|
|
}
|
|
auto pSa = dynamic_cast<ShuttleSetAutomation*>(&S);
|
|
if( pSa ){
|
|
SetAutomationParameters( *(pSa->mpEap) );
|
|
return true;
|
|
}
|
|
auto pSd = dynamic_cast<ShuttleGetDefinition*>(&S);
|
|
if( pSd == nullptr )
|
|
return true;
|
|
//wxASSERT( pSd );
|
|
|
|
if (mExternal)
|
|
return true;
|
|
|
|
if (mIsPrompt)
|
|
{
|
|
S.Define( mInputCmd, KEY_Command, "" );
|
|
S.Define( mVersion, KEY_Version, 3 );
|
|
S.Define( mParameters, KEY_Parameters, "" );
|
|
return true;
|
|
}
|
|
|
|
for (size_t c = 0, cnt = mControls.size(); c < cnt; c++)
|
|
{
|
|
NyqControl & ctrl = mControls[c];
|
|
double d = ctrl.val;
|
|
|
|
if (d == UNINITIALIZED_CONTROL && ctrl.type != NYQ_CTRL_STRING)
|
|
{
|
|
d = GetCtrlValue(ctrl.valStr);
|
|
}
|
|
|
|
if (ctrl.type == NYQ_CTRL_FLOAT || ctrl.type == NYQ_CTRL_FLOAT_TEXT ||
|
|
ctrl.type == NYQ_CTRL_TIME)
|
|
{
|
|
S.Define( d, static_cast<const wxChar*>( ctrl.var.c_str() ), (double)0.0, ctrl.low, ctrl.high, 1.0);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_INT_TEXT)
|
|
{
|
|
int x=d;
|
|
S.Define( x, static_cast<const wxChar*>( ctrl.var.c_str() ), 0, ctrl.low, ctrl.high, 1);
|
|
//parms.Write(ctrl.var, (int) d);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_CHOICE)
|
|
{
|
|
// untranslated
|
|
int x=d;
|
|
//parms.WriteEnum(ctrl.var, (int) d, choices);
|
|
S.DefineEnum( x, static_cast<const wxChar*>( ctrl.var.c_str() ), 0,
|
|
ctrl.choices.data(), ctrl.choices.size() );
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_STRING || ctrl.type == NYQ_CTRL_FILE)
|
|
{
|
|
S.Define( ctrl.valStr, ctrl.var, "" , ctrl.lowStr, ctrl.highStr );
|
|
//parms.Write(ctrl.var, ctrl.valStr);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool NyquistEffect::GetAutomationParameters(CommandParameters & parms)
|
|
{
|
|
if (mIsPrompt)
|
|
{
|
|
parms.Write(KEY_Command, mInputCmd);
|
|
parms.Write(KEY_Version, mVersion);
|
|
parms.Write(KEY_Parameters, mParameters);
|
|
|
|
return true;
|
|
}
|
|
|
|
for (size_t c = 0, cnt = mControls.size(); c < cnt; c++)
|
|
{
|
|
NyqControl & ctrl = mControls[c];
|
|
double d = ctrl.val;
|
|
|
|
if (d == UNINITIALIZED_CONTROL && ctrl.type != NYQ_CTRL_STRING)
|
|
{
|
|
d = GetCtrlValue(ctrl.valStr);
|
|
}
|
|
|
|
if (ctrl.type == NYQ_CTRL_FLOAT || ctrl.type == NYQ_CTRL_FLOAT_TEXT ||
|
|
ctrl.type == NYQ_CTRL_TIME)
|
|
{
|
|
parms.Write(ctrl.var, d);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_INT_TEXT)
|
|
{
|
|
parms.Write(ctrl.var, (int) d);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_CHOICE)
|
|
{
|
|
// untranslated
|
|
parms.WriteEnum(ctrl.var, (int) d,
|
|
ctrl.choices.data(), ctrl.choices.size());
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_STRING)
|
|
{
|
|
parms.Write(ctrl.var, ctrl.valStr);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_FILE)
|
|
{
|
|
resolveFilePath(ctrl.valStr);
|
|
parms.Write(ctrl.var, ctrl.valStr);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NyquistEffect::SetAutomationParameters(CommandParameters & parms)
|
|
{
|
|
if (mIsPrompt)
|
|
{
|
|
parms.Read(KEY_Command, &mInputCmd, wxEmptyString);
|
|
parms.Read(KEY_Version, &mVersion, mVersion);
|
|
parms.Read(KEY_Parameters, &mParameters, wxEmptyString);
|
|
|
|
if (!mInputCmd.empty())
|
|
{
|
|
ParseCommand(mInputCmd);
|
|
}
|
|
|
|
if (!mParameters.empty())
|
|
{
|
|
parms.SetParameters(mParameters);
|
|
}
|
|
|
|
if (!IsBatchProcessing())
|
|
{
|
|
mType = EffectTypeTool;
|
|
}
|
|
|
|
mPromptType = mType;
|
|
mIsTool = (mPromptType == EffectTypeTool);
|
|
mExternal = true;
|
|
|
|
if (!IsBatchProcessing())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Constants to document what the true/false values mean.
|
|
const auto kTestOnly = true;
|
|
const auto kTestAndSet = false;
|
|
|
|
// badCount will encompass both actual bad values and missing values.
|
|
// We probably never actually have bad values when using the dialogs
|
|
// since the dialog validation will catch them.
|
|
int badCount;
|
|
// When batch processing, we just ignore missing/bad parameters.
|
|
// We'll end up using defaults in those cases.
|
|
if (!IsBatchProcessing()) {
|
|
badCount = SetLispVarsFromParameters(parms, kTestOnly);
|
|
if (badCount > 0)
|
|
return false;
|
|
}
|
|
|
|
badCount = SetLispVarsFromParameters(parms, kTestAndSet);
|
|
// We never do anything with badCount here.
|
|
// It might be non zero, for missing parameters, and we allow that,
|
|
// and don't distinguish that from an out-of-range value.
|
|
return true;
|
|
}
|
|
|
|
// Sets the lisp variables form the parameters.
|
|
// returns the number of bad settings.
|
|
// We can run this just testing for bad values, or actually setting when
|
|
// the values are good.
|
|
int NyquistEffect::SetLispVarsFromParameters(CommandParameters & parms, bool bTestOnly)
|
|
{
|
|
int badCount = 0;
|
|
// First pass verifies values
|
|
for (size_t c = 0, cnt = mControls.size(); c < cnt; c++)
|
|
{
|
|
NyqControl & ctrl = mControls[c];
|
|
bool good = false;
|
|
|
|
// This GetCtrlValue code is preserved from former code,
|
|
// but probably is pointless. The value d isn't used later,
|
|
// and GetCtrlValue does not appear to have important needed
|
|
// side effects.
|
|
if (!bTestOnly) {
|
|
double d = ctrl.val;
|
|
if (d == UNINITIALIZED_CONTROL && ctrl.type != NYQ_CTRL_STRING)
|
|
{
|
|
d = GetCtrlValue(ctrl.valStr);
|
|
}
|
|
}
|
|
|
|
if (ctrl.type == NYQ_CTRL_FLOAT || ctrl.type == NYQ_CTRL_FLOAT_TEXT ||
|
|
ctrl.type == NYQ_CTRL_TIME)
|
|
{
|
|
double val;
|
|
good = parms.Read(ctrl.var, &val) &&
|
|
val >= ctrl.low &&
|
|
val <= ctrl.high;
|
|
if (good && !bTestOnly)
|
|
ctrl.val = val;
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_INT_TEXT)
|
|
{
|
|
int val;
|
|
good = parms.Read(ctrl.var, &val) &&
|
|
val >= ctrl.low &&
|
|
val <= ctrl.high;
|
|
if (good && !bTestOnly)
|
|
ctrl.val = (double)val;
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_CHOICE)
|
|
{
|
|
int val;
|
|
// untranslated
|
|
good = parms.ReadEnum(ctrl.var, &val,
|
|
ctrl.choices.data(), ctrl.choices.size()) &&
|
|
val != wxNOT_FOUND;
|
|
if (good && !bTestOnly)
|
|
ctrl.val = (double)val;
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_STRING || ctrl.type == NYQ_CTRL_FILE)
|
|
{
|
|
wxString val;
|
|
good = parms.Read(ctrl.var, &val);
|
|
if (good && !bTestOnly)
|
|
ctrl.valStr = val;
|
|
}
|
|
/*
|
|
else if (ctrl.type == NYQ_CTRL_TEXT)
|
|
{
|
|
// This "control" is just fixed text (nothing to save or restore),
|
|
// Does not count for good/bad counting.
|
|
good = true;
|
|
}
|
|
*/
|
|
badCount += !good ? 1 : 0;
|
|
}
|
|
return badCount;
|
|
}
|
|
|
|
// Effect Implementation
|
|
bool NyquistEffect::Init()
|
|
{
|
|
// When Nyquist Prompt spawns an effect GUI, Init() is called for Nyquist Prompt,
|
|
// and then again for the spawned (mExternal) effect.
|
|
|
|
// EffectType may not be defined in script, so
|
|
// reset each time we call the Nyquist Prompt.
|
|
if (mIsPrompt) {
|
|
mName = mPromptName;
|
|
// Reset effect type each time we call the Nyquist Prompt.
|
|
mType = mPromptType;
|
|
mIsSpectral = false;
|
|
mDebugButton = true; // Debug button always enabled for Nyquist Prompt.
|
|
mEnablePreview = true; // Preview button always enabled for Nyquist Prompt.
|
|
}
|
|
|
|
// As of Audacity 2.1.2 rc1, 'spectral' effects are allowed only if
|
|
// the selected track(s) are in a spectrogram view, and there is at
|
|
// least one frequency bound and Spectral Selection is enabled for the
|
|
// selected track(s) - (but don't apply to Nyquist Prompt).
|
|
|
|
if (!mIsPrompt && mIsSpectral) {
|
|
auto *project = FindProject();
|
|
bool bAllowSpectralEditing = true;
|
|
|
|
for ( auto t :
|
|
TrackList::Get( *project ).Selected< const WaveTrack >() ) {
|
|
const auto displays = WaveTrackView::Get(*t).GetDisplays();
|
|
bool hasSpectral = (displays.end() != std::find(
|
|
displays.begin(), displays.end(),
|
|
WaveTrackSubView::Type{ WaveTrackViewConstants::Spectrum, {} }
|
|
) );
|
|
if ( !hasSpectral ||
|
|
!(t->GetSpectrogramSettings().SpectralSelectionEnabled())) {
|
|
bAllowSpectralEditing = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bAllowSpectralEditing || ((mF0 < 0.0) && (mF1 < 0.0))) {
|
|
Effect::MessageBox(
|
|
XO("To use 'Spectral effects', enable 'Spectral Selection'\n"
|
|
"in the track Spectrogram settings and select the\n"
|
|
"frequency range for the effect to act on."),
|
|
wxOK | wxICON_EXCLAMATION | wxCENTRE,
|
|
XO("Error") );
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!mIsPrompt && !mExternal)
|
|
{
|
|
//TODO: (bugs):
|
|
// 1) If there is more than one plug-in with the same name, GetModificationTime may pick the wrong one.
|
|
// 2) If the ;type is changed after the effect has been registered, the plug-in will appear in the wrong menu.
|
|
|
|
//TODO: If we want to auto-add parameters from spectral selection,
|
|
//we will need to modify this test.
|
|
//Note that removing it stops the caching of parameter values,
|
|
//(during this session).
|
|
if (mFileName.GetModificationTime().IsLaterThan(mFileModified))
|
|
{
|
|
SaveUserPreset(GetCurrentSettingsGroup());
|
|
|
|
mMaxLen = NYQ_MAX_LEN;
|
|
ParseFile();
|
|
mFileModified = mFileName.GetModificationTime();
|
|
|
|
LoadUserPreset(GetCurrentSettingsGroup());
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NyquistEffect::CheckWhetherSkipEffect()
|
|
{
|
|
// If we're a prompt and we have controls, then we've already processed
|
|
// the audio, so skip further processing.
|
|
return (mIsPrompt && mControls.size() > 0 && !IsBatchProcessing());
|
|
}
|
|
|
|
static void RegisterFunctions();
|
|
|
|
bool NyquistEffect::Process()
|
|
{
|
|
// Check for reentrant Nyquist commands.
|
|
// I'm choosing to mark skipped Nyquist commands as successful even though
|
|
// they are skipped. The reason is that when Nyquist calls out to a chain,
|
|
// and that chain contains Nyquist, it will be clearer if the chain completes
|
|
// skipping Nyquist, rather than doing nothing at all.
|
|
if( mReentryCount > 0 )
|
|
return true;
|
|
|
|
// Restore the reentry counter (to zero) when we exit.
|
|
auto countRestorer = valueRestorer( mReentryCount);
|
|
mReentryCount++;
|
|
RegisterFunctions();
|
|
|
|
bool success = true;
|
|
int nEffectsSoFar = nEffectsDone;
|
|
mProjectChanged = false;
|
|
EffectManager & em = EffectManager::Get();
|
|
em.SetSkipStateFlag(false);
|
|
|
|
// This code was added in a fix for bug 2392 (no preview for Nyquist)
|
|
// It was commented out in a fix for bug 2428 (no progress dialog from a macro)
|
|
//if (mExternal) {
|
|
// mProgress->Hide();
|
|
//}
|
|
|
|
mOutputTime = 0;
|
|
mCount = 0;
|
|
mProgressIn = 0;
|
|
mProgressOut = 0;
|
|
mProgressTot = 0;
|
|
mScale = (GetType() == EffectTypeProcess ? 0.5 : 1.0) / GetNumWaveGroups();
|
|
|
|
mStop = false;
|
|
mBreak = false;
|
|
mCont = false;
|
|
|
|
mTrackIndex = 0;
|
|
|
|
// If in tool mode, then we don't do anything with the track and selection.
|
|
const bool bOnePassTool = (GetType() == EffectTypeTool);
|
|
|
|
// We must copy all the tracks, because Paste needs label tracks to ensure
|
|
// correct sync-lock group behavior when the timeline is affected; then we just want
|
|
// to operate on the selected wave tracks
|
|
if ( !bOnePassTool )
|
|
CopyInputTracks(true);
|
|
|
|
mNumSelectedChannels = bOnePassTool
|
|
? 0
|
|
: mOutputTracks->Selected< const WaveTrack >().size();
|
|
|
|
mDebugOutput = {};
|
|
if (!mHelpFile.empty() && !mHelpFileExists) {
|
|
mDebugOutput = XO(
|
|
"error: File \"%s\" specified in header but not found in plug-in path.\n")
|
|
.Format( mHelpFile );
|
|
}
|
|
|
|
if (mVersion >= 4)
|
|
{
|
|
auto project = FindProject();
|
|
|
|
mProps = wxEmptyString;
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*AUDACITY* (list %d %d %d) 'VERSION)\n"), AUDACITY_VERSION, AUDACITY_RELEASE, AUDACITY_REVISION);
|
|
wxString lang = gPrefs->Read(wxT("/Locale/Language"), wxT(""));
|
|
lang = (lang.empty())? GUIPrefs::SetLang(lang) : lang;
|
|
mProps += wxString::Format(wxT("(putprop '*AUDACITY* \"%s\" 'LANGUAGE)\n"), lang);
|
|
|
|
mProps += wxString::Format(wxT("(setf *DECIMAL-SEPARATOR* #\\%c)\n"), wxNumberFormatter::GetDecimalSeparator());
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'BASE)\n"), EscapeString(FileNames::BaseDir()));
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'DATA)\n"), EscapeString(FileNames::DataDir()));
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'HELP)\n"), EscapeString(FileNames::HtmlHelpDir().RemoveLast()));
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'TEMP)\n"), EscapeString(FileNames::TempDir()));
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'SYS-TEMP)\n"), EscapeString(wxStandardPaths::Get().GetTempDir()));
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'DOCUMENTS)\n"), EscapeString(wxStandardPaths::Get().GetDocumentsDir()));
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'HOME)\n"), EscapeString(wxGetHomeDir()));
|
|
|
|
auto paths = NyquistEffect::GetNyquistSearchPath();
|
|
wxString list;
|
|
for (size_t i = 0, cnt = paths.size(); i < cnt; i++)
|
|
{
|
|
list += wxT("\"") + EscapeString(paths[i]) + wxT("\" ");
|
|
}
|
|
list = list.RemoveLast();
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* (list %s) 'PLUGIN)\n"), list);
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* (list %s) 'PLUG-IN)\n"), list);
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'USER-PLUG-IN)\n"),
|
|
EscapeString(FileNames::PlugInDir()));
|
|
|
|
// Date and time:
|
|
wxDateTime now = wxDateTime::Now();
|
|
int year = now.GetYear();
|
|
int doy = now.GetDayOfYear();
|
|
int dom = now.GetDay();
|
|
// enumerated constants
|
|
wxDateTime::Month month = now.GetMonth();
|
|
wxDateTime::WeekDay day = now.GetWeekDay();
|
|
|
|
// Date/time as a list: year, day of year, hour, minute, seconds
|
|
mProps += wxString::Format(wxT("(setf *SYSTEM-TIME* (list %d %d %d %d %d))\n"),
|
|
year, doy, now.GetHour(), now.GetMinute(), now.GetSecond());
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'DATE)\n"), now.FormatDate());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'TIME)\n"), now.FormatTime());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'ISO-DATE)\n"), now.FormatISODate());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'ISO-TIME)\n"), now.FormatISOTime());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* %d 'YEAR)\n"), year);
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* %d 'DAY)\n"), dom); // day of month
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* %d 'MONTH)\n"), month);
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'MONTH-NAME)\n"), now.GetMonthName(month));
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'DAY-NAME)\n"), now.GetWeekDayName(day));
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'PROJECTS)\n"),
|
|
(int) AllProjects{}.size());
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* \"%s\" 'NAME)\n"), project->GetProjectName());
|
|
|
|
int numTracks = 0;
|
|
int numWave = 0;
|
|
int numLabel = 0;
|
|
int numMidi = 0;
|
|
int numTime = 0;
|
|
wxString waveTrackList; // track positions of selected audio tracks.
|
|
|
|
{
|
|
auto countRange = TrackList::Get( *project ).Leaders();
|
|
for (auto t : countRange) {
|
|
t->TypeSwitch( [&](const WaveTrack *) {
|
|
numWave++;
|
|
if (t->GetSelected())
|
|
waveTrackList += wxString::Format(wxT("%d "), 1 + numTracks);
|
|
});
|
|
numTracks++;
|
|
}
|
|
numLabel = countRange.Filter<const LabelTrack>().size();
|
|
#if defined(USE_MIDI)
|
|
numMidi = countRange.Filter<const NoteTrack>().size();
|
|
#endif
|
|
numTime = countRange.Filter<const TimeTrack>().size();
|
|
}
|
|
|
|
// We use Internat::ToString() rather than "%g" here because we
|
|
// always have to use the dot as decimal separator when giving
|
|
// numbers to Nyquist, whereas using "%g" will use the user's
|
|
// decimal separator which may be a comma in some countries.
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* (float %s) 'RATE)\n"),
|
|
Internat::ToString(ProjectSettings::Get(*project).GetRate()));
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'TRACKS)\n"), numTracks);
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'WAVETRACKS)\n"), numWave);
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'LABELTRACKS)\n"), numLabel);
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'MIDITRACKS)\n"), numMidi);
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'TIMETRACKS)\n"), numTime);
|
|
|
|
double previewLen = 6.0;
|
|
gPrefs->Read(wxT("/AudioIO/EffectsPreviewLen"), &previewLen);
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* (float %s) 'PREVIEW-DURATION)\n"),
|
|
Internat::ToString(previewLen));
|
|
|
|
// *PREVIEWP* is true when previewing (better than relying on track view).
|
|
wxString isPreviewing = (this->IsPreviewing())? wxT("T") : wxT("NIL");
|
|
mProps += wxString::Format(wxT("(setf *PREVIEWP* %s)\n"), isPreviewing);
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*SELECTION* (float %s) 'START)\n"),
|
|
Internat::ToString(mT0));
|
|
mProps += wxString::Format(wxT("(putprop '*SELECTION* (float %s) 'END)\n"),
|
|
Internat::ToString(mT1));
|
|
mProps += wxString::Format(wxT("(putprop '*SELECTION* (list %s) 'TRACKS)\n"), waveTrackList);
|
|
mProps += wxString::Format(wxT("(putprop '*SELECTION* %d 'CHANNELS)\n"), mNumSelectedChannels);
|
|
}
|
|
|
|
// Nyquist Prompt does not require a selection, but effects do.
|
|
if (!bOnePassTool && (mNumSelectedChannels == 0)) {
|
|
auto message = XO("Audio selection required.");
|
|
Effect::MessageBox(
|
|
message,
|
|
wxOK | wxCENTRE | wxICON_EXCLAMATION,
|
|
XO("Nyquist Error") );
|
|
}
|
|
|
|
Optional<TrackIterRange<WaveTrack>> pRange;
|
|
if (!bOnePassTool)
|
|
pRange.emplace(mOutputTracks->Selected< WaveTrack >() + &Track::IsLeader);
|
|
|
|
// Keep track of whether the current track is first selected in its sync-lock group
|
|
// (we have no idea what the length of the returned audio will be, so we have
|
|
// to handle sync-lock group behavior the "old" way).
|
|
mFirstInGroup = true;
|
|
Track *gtLast = NULL;
|
|
|
|
for (;
|
|
bOnePassTool || pRange->first != pRange->second;
|
|
(void) (!pRange || (++pRange->first, true))
|
|
) {
|
|
// Prepare to accumulate more debug output in OutputCallback
|
|
mDebugOutputStr = mDebugOutput.Translation();
|
|
mDebugOutput = Verbatim( "%s" ).Format( std::cref( mDebugOutputStr ) );
|
|
|
|
mCurTrack[0] = pRange ? *pRange->first : nullptr;
|
|
mCurNumChannels = 1;
|
|
if ( (mT1 >= mT0) || bOnePassTool ) {
|
|
if (bOnePassTool) {
|
|
}
|
|
else {
|
|
auto channels = TrackList::Channels(mCurTrack[0]);
|
|
if (channels.size() > 1) {
|
|
// TODO: more-than-two-channels
|
|
// Pay attention to consistency of mNumSelectedChannels
|
|
// with the running tally made by this loop!
|
|
mCurNumChannels = 2;
|
|
|
|
mCurTrack[1] = * ++ channels.first;
|
|
if (mCurTrack[1]->GetRate() != mCurTrack[0]->GetRate()) {
|
|
Effect::MessageBox(
|
|
XO(
|
|
"Sorry, cannot apply effect on stereo tracks where the tracks don't match."),
|
|
wxOK | wxCENTRE );
|
|
success = false;
|
|
goto finish;
|
|
}
|
|
mCurStart[1] = mCurTrack[1]->TimeToLongSamples(mT0);
|
|
}
|
|
|
|
// Check whether we're in the same group as the last selected track
|
|
Track *gt = *TrackList::SyncLockGroup(mCurTrack[0]).first;
|
|
mFirstInGroup = !gtLast || (gtLast != gt);
|
|
gtLast = gt;
|
|
|
|
mCurStart[0] = mCurTrack[0]->TimeToLongSamples(mT0);
|
|
auto end = mCurTrack[0]->TimeToLongSamples(mT1);
|
|
mCurLen = end - mCurStart[0];
|
|
|
|
if (mCurLen > NYQ_MAX_LEN) {
|
|
float hours = (float)NYQ_MAX_LEN / (44100 * 60 * 60);
|
|
const auto message =
|
|
XO(
|
|
"Selection too long for Nyquist code.\nMaximum allowed selection is %ld samples\n(about %.1f hours at 44100 Hz sample rate).")
|
|
.Format((long)NYQ_MAX_LEN, hours);
|
|
Effect::MessageBox(
|
|
message,
|
|
wxOK | wxCENTRE,
|
|
XO("Nyquist Error") );
|
|
if (!mProjectChanged)
|
|
em.SetSkipStateFlag(true);
|
|
return false;
|
|
}
|
|
|
|
mCurLen = std::min(mCurLen, mMaxLen);
|
|
}
|
|
|
|
mProgressIn = 0.0;
|
|
mProgressOut = 0.0;
|
|
|
|
// libnyquist breaks except in LC_NUMERIC=="C".
|
|
//
|
|
// Note that we must set the locale to "C" even before calling
|
|
// nyx_init() because otherwise some effects will not work!
|
|
//
|
|
// MB: setlocale is not thread-safe. Should use uselocale()
|
|
// if available, or fix libnyquist to be locale-independent.
|
|
// See also http://bugzilla.audacityteam.org/show_bug.cgi?id=642#c9
|
|
// for further info about this thread safety question.
|
|
wxString prevlocale = wxSetlocale(LC_NUMERIC, NULL);
|
|
wxSetlocale(LC_NUMERIC, wxString(wxT("C")));
|
|
|
|
nyx_init();
|
|
nyx_set_os_callback(StaticOSCallback, (void *)this);
|
|
nyx_capture_output(StaticOutputCallback, (void *)this);
|
|
|
|
auto cleanup = finally( [&] {
|
|
nyx_capture_output(NULL, (void *)NULL);
|
|
nyx_set_os_callback(NULL, (void *)NULL);
|
|
nyx_cleanup();
|
|
} );
|
|
|
|
|
|
if (mVersion >= 4)
|
|
{
|
|
mPerTrackProps = wxEmptyString;
|
|
wxString lowHz = wxT("nil");
|
|
wxString highHz = wxT("nil");
|
|
wxString centerHz = wxT("nil");
|
|
wxString bandwidth = wxT("nil");
|
|
|
|
#if defined(EXPERIMENTAL_SPECTRAL_EDITING)
|
|
if (mF0 >= 0.0) {
|
|
lowHz.Printf(wxT("(float %s)"), Internat::ToString(mF0));
|
|
}
|
|
|
|
if (mF1 >= 0.0) {
|
|
highHz.Printf(wxT("(float %s)"), Internat::ToString(mF1));
|
|
}
|
|
|
|
if ((mF0 >= 0.0) && (mF1 >= 0.0)) {
|
|
centerHz.Printf(wxT("(float %s)"), Internat::ToString(sqrt(mF0 * mF1)));
|
|
}
|
|
|
|
if ((mF0 > 0.0) && (mF1 >= mF0)) {
|
|
// with very small values, bandwidth calculation may be inf.
|
|
// (Observed on Linux)
|
|
double bw = log(mF1 / mF0) / log(2.0);
|
|
if (!std::isinf(bw)) {
|
|
bandwidth.Printf(wxT("(float %s)"), Internat::ToString(bw));
|
|
}
|
|
}
|
|
|
|
#endif
|
|
mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'LOW-HZ)\n"), lowHz);
|
|
mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'CENTER-HZ)\n"), centerHz);
|
|
mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'HIGH-HZ)\n"), highHz);
|
|
mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'BANDWIDTH)\n"), bandwidth);
|
|
}
|
|
|
|
success = ProcessOne();
|
|
|
|
// Reset previous locale
|
|
wxSetlocale(LC_NUMERIC, prevlocale);
|
|
|
|
if (!success || bOnePassTool) {
|
|
goto finish;
|
|
}
|
|
mProgressTot += mProgressIn + mProgressOut;
|
|
}
|
|
|
|
mCount += mCurNumChannels;
|
|
}
|
|
|
|
if (mOutputTime > 0.0) {
|
|
mT1 = mT0 + mOutputTime;
|
|
}
|
|
|
|
finish:
|
|
|
|
// Show debug window if trace set in plug-in header and something to show.
|
|
mDebug = (mTrace && !mDebugOutput.Translation().empty())? true : mDebug;
|
|
|
|
if (mDebug && !mRedirectOutput) {
|
|
NyquistOutputDialog dlog(mUIParent, -1,
|
|
mName,
|
|
XO("Debug Output: "),
|
|
mDebugOutput);
|
|
dlog.CentreOnParent();
|
|
dlog.ShowModal();
|
|
}
|
|
|
|
// Has rug been pulled from under us by some effect done within Nyquist??
|
|
if( !bOnePassTool && ( nEffectsSoFar == nEffectsDone ))
|
|
ReplaceProcessedTracks(success);
|
|
else{
|
|
ReplaceProcessedTracks(false); // Do not use the results.
|
|
// Selection is to be set to whatever it is in the project.
|
|
auto project = FindProject();
|
|
if (project) {
|
|
auto &selectedRegion = ViewInfo::Get( *project ).selectedRegion;
|
|
mT0 = selectedRegion.t0();
|
|
mT1 = selectedRegion.t1();
|
|
}
|
|
else {
|
|
mT0 = 0;
|
|
mT1 = -1;
|
|
}
|
|
|
|
}
|
|
|
|
if (!mProjectChanged)
|
|
em.SetSkipStateFlag(true);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool NyquistEffect::ShowInterface(
|
|
wxWindow &parent, const EffectDialogFactory &factory, bool forceModal)
|
|
{
|
|
// Show the normal (prompt or effect) interface
|
|
bool res = Effect::ShowInterface(parent, factory, forceModal);
|
|
|
|
// Remember if the user clicked debug
|
|
mDebug = (mUIResultID == eDebugID);
|
|
|
|
// We're done if the user clicked "Close", we are not the Nyquist Prompt,
|
|
// or the program currently loaded into the prompt doesn't have a UI.
|
|
if (!res || !mIsPrompt || mControls.size() == 0)
|
|
{
|
|
return res;
|
|
}
|
|
|
|
NyquistEffect effect(NYQUIST_WORKER_ID);
|
|
|
|
if (IsBatchProcessing())
|
|
{
|
|
effect.SetBatchProcessing(true);
|
|
effect.SetCommand(mInputCmd);
|
|
|
|
CommandParameters cp;
|
|
cp.SetParameters(mParameters);
|
|
effect.SetAutomationParameters(cp);
|
|
|
|
// Show the normal (prompt or effect) interface
|
|
res = effect.ShowInterface(parent, factory, forceModal);
|
|
if (res)
|
|
{
|
|
CommandParameters cp;
|
|
effect.GetAutomationParameters(cp);
|
|
cp.GetParameters(mParameters);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
effect.SetCommand(mInputCmd);
|
|
effect.mDebug = (mUIResultID == eDebugID);
|
|
res = Delegate(effect, parent, factory);
|
|
mT0 = effect.mT0;
|
|
mT1 = effect.mT1;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
void NyquistEffect::PopulateOrExchange(ShuttleGui & S)
|
|
{
|
|
if (mIsPrompt)
|
|
{
|
|
BuildPromptWindow(S);
|
|
}
|
|
else
|
|
{
|
|
BuildEffectWindow(S);
|
|
}
|
|
|
|
EnableDebug(mDebugButton);
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataToWindow()
|
|
{
|
|
mUIParent->TransferDataToWindow();
|
|
|
|
bool success;
|
|
if (mIsPrompt)
|
|
{
|
|
success = TransferDataToPromptWindow();
|
|
}
|
|
else
|
|
{
|
|
success = TransferDataToEffectWindow();
|
|
}
|
|
|
|
if (success)
|
|
{
|
|
EnablePreview(mEnablePreview);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataFromWindow()
|
|
{
|
|
if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (mIsPrompt)
|
|
{
|
|
return TransferDataFromPromptWindow();
|
|
}
|
|
return TransferDataFromEffectWindow();
|
|
}
|
|
|
|
// NyquistEffect implementation
|
|
|
|
bool NyquistEffect::ProcessOne()
|
|
{
|
|
mpException = {};
|
|
|
|
nyx_rval rval;
|
|
|
|
wxString cmd;
|
|
cmd += wxT("(snd-set-latency 0.1)");
|
|
|
|
// A tool may be using AUD-DO which will potentially invalidate *TRACK*
|
|
// so tools do not get *TRACK*.
|
|
if (GetType() == EffectTypeTool)
|
|
cmd += wxT("(setf S 0.25)\n"); // No Track.
|
|
else if (mVersion >= 4) {
|
|
nyx_set_audio_name("*TRACK*");
|
|
cmd += wxT("(setf S 0.25)\n");
|
|
}
|
|
else {
|
|
nyx_set_audio_name("S");
|
|
cmd += wxT("(setf *TRACK* '*unbound*)\n");
|
|
}
|
|
|
|
if(mVersion >= 4) {
|
|
cmd += mProps;
|
|
cmd += mPerTrackProps;
|
|
}
|
|
|
|
if( (mVersion >= 4) && (GetType() != EffectTypeTool) ) {
|
|
// Set the track TYPE and VIEW properties
|
|
wxString type;
|
|
wxString view;
|
|
wxString bitFormat;
|
|
wxString spectralEditp;
|
|
|
|
mCurTrack[0]->TypeSwitch(
|
|
[&](const WaveTrack *wt) {
|
|
type = wxT("wave");
|
|
spectralEditp = mCurTrack[0]->GetSpectrogramSettings().SpectralSelectionEnabled()? wxT("T") : wxT("NIL");
|
|
auto displays = WaveTrackView::Get( *wt ).GetDisplays();
|
|
auto format = [&]( decltype(displays[0]) display ) {
|
|
// Get the English name of the view type, without menu codes,
|
|
// as a string that Lisp can examine
|
|
return wxString::Format( wxT("\"%s\""),
|
|
display.name.Stripped().Debug() );
|
|
};
|
|
if (displays.empty())
|
|
view = wxT("NIL");
|
|
else if (displays.size() == 1)
|
|
view = format( displays[0] );
|
|
else {
|
|
view = wxT("(list");
|
|
for ( auto display : displays )
|
|
view += wxString(wxT(" ")) + format( display );
|
|
view += wxT(")");
|
|
}
|
|
},
|
|
#if defined(USE_MIDI)
|
|
[&](const NoteTrack *) {
|
|
type = wxT("midi");
|
|
view = wxT("\"Midi\"");
|
|
},
|
|
#endif
|
|
[&](const LabelTrack *) {
|
|
type = wxT("label");
|
|
view = wxT("\"Label\"");
|
|
},
|
|
[&](const TimeTrack *) {
|
|
type = wxT("time");
|
|
view = wxT("\"Time\"");
|
|
}
|
|
);
|
|
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %d 'INDEX)\n"), ++mTrackIndex);
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* \"%s\" 'NAME)\n"), mCurTrack[0]->GetName());
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* \"%s\" 'TYPE)\n"), type);
|
|
// Note: "View" property may change when Audacity's choice of track views has stabilized.
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %s 'VIEW)\n"), view);
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %d 'CHANNELS)\n"), mCurNumChannels);
|
|
|
|
//NOTE: Audacity 2.1.3 True if spectral selection is enabled regardless of track view.
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %s 'SPECTRAL-EDIT-ENABLED)\n"), spectralEditp);
|
|
|
|
auto channels = TrackList::Channels( mCurTrack[0] );
|
|
double startTime = channels.min( &Track::GetStartTime );
|
|
double endTime = channels.max( &Track::GetEndTime );
|
|
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'START-TIME)\n"),
|
|
Internat::ToString(startTime));
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'END-TIME)\n"),
|
|
Internat::ToString(endTime));
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'GAIN)\n"),
|
|
Internat::ToString(mCurTrack[0]->GetGain()));
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'PAN)\n"),
|
|
Internat::ToString(mCurTrack[0]->GetPan()));
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'RATE)\n"),
|
|
Internat::ToString(mCurTrack[0]->GetRate()));
|
|
|
|
switch (mCurTrack[0]->GetSampleFormat())
|
|
{
|
|
case int16Sample:
|
|
bitFormat = wxT("16");
|
|
break;
|
|
case int24Sample:
|
|
bitFormat = wxT("24");
|
|
break;
|
|
case floatSample:
|
|
bitFormat = wxT("32.0");
|
|
break;
|
|
}
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %s 'FORMAT)\n"), bitFormat);
|
|
|
|
float maxPeakLevel = 0.0; // Deprecated as of 2.1.3
|
|
wxString clips, peakString, rmsString;
|
|
for (size_t i = 0; i < mCurNumChannels; i++) {
|
|
auto ca = mCurTrack[i]->SortedClipArray();
|
|
float maxPeak = 0.0;
|
|
|
|
// A list of clips for mono, or an array of lists for multi-channel.
|
|
if (mCurNumChannels > 1) {
|
|
clips += wxT("(list ");
|
|
}
|
|
// Each clip is a list (start-time, end-time)
|
|
// Limit number of clips added to avoid argument stack overflow error (bug 2300).
|
|
for (size_t i=0; i<ca.size(); i++) {
|
|
if (i < 1000) {
|
|
clips += wxString::Format(wxT("(list (float %s) (float %s))"),
|
|
Internat::ToString(ca[i]->GetStartTime()),
|
|
Internat::ToString(ca[i]->GetEndTime()));
|
|
} else if (i == 1000) {
|
|
// If final clip is NIL, plug-in developer knows there are more than 1000 clips in channel.
|
|
clips += "NIL";
|
|
} else if (i > 1000) {
|
|
break;
|
|
}
|
|
}
|
|
if (mCurNumChannels > 1) clips += wxT(" )");
|
|
|
|
float min, max;
|
|
auto pair = mCurTrack[i]->GetMinMax(mT0, mT1); // may throw
|
|
min = pair.first, max = pair.second;
|
|
maxPeak = wxMax(wxMax(fabs(min), fabs(max)), maxPeak);
|
|
maxPeakLevel = wxMax(maxPeakLevel, maxPeak);
|
|
|
|
// On Debian, NaN samples give maxPeak = 3.40282e+38 (FLT_MAX)
|
|
if (!std::isinf(maxPeak) && !std::isnan(maxPeak) && (maxPeak < FLT_MAX)) {
|
|
peakString += wxString::Format(wxT("(float %s) "), Internat::ToString(maxPeak));
|
|
} else {
|
|
peakString += wxT("nil ");
|
|
}
|
|
|
|
float rms = mCurTrack[i]->GetRMS(mT0, mT1); // may throw
|
|
if (!std::isinf(rms) && !std::isnan(rms)) {
|
|
rmsString += wxString::Format(wxT("(float %s) "), Internat::ToString(rms));
|
|
} else {
|
|
rmsString += wxT("NIL ");
|
|
}
|
|
}
|
|
// A list of clips for mono, or an array of lists for multi-channel.
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %s%s ) 'CLIPS)\n"),
|
|
(mCurNumChannels == 1) ? wxT("(list ") : wxT("(vector "),
|
|
clips);
|
|
|
|
(mCurNumChannels > 1)?
|
|
cmd += wxString::Format(wxT("(putprop '*SELECTION* (vector %s) 'PEAK)\n"), peakString) :
|
|
cmd += wxString::Format(wxT("(putprop '*SELECTION* %s 'PEAK)\n"), peakString);
|
|
|
|
if (!std::isinf(maxPeakLevel) && !std::isnan(maxPeakLevel) && (maxPeakLevel < FLT_MAX)) {
|
|
cmd += wxString::Format(wxT("(putprop '*SELECTION* (float %s) 'PEAK-LEVEL)\n"),
|
|
Internat::ToString(maxPeakLevel));
|
|
}
|
|
|
|
(mCurNumChannels > 1)?
|
|
cmd += wxString::Format(wxT("(putprop '*SELECTION* (vector %s) 'RMS)\n"), rmsString) :
|
|
cmd += wxString::Format(wxT("(putprop '*SELECTION* %s 'RMS)\n"), rmsString);
|
|
}
|
|
|
|
// If in tool mode, then we don't do anything with the track and selection.
|
|
if (GetType() == EffectTypeTool) {
|
|
nyx_set_audio_params(44100, 0);
|
|
}
|
|
else if (GetType() == EffectTypeGenerate) {
|
|
nyx_set_audio_params(mCurTrack[0]->GetRate(), 0);
|
|
}
|
|
else {
|
|
// UNSAFE_SAMPLE_COUNT_TRUNCATION
|
|
// Danger! Truncation of long long to long!
|
|
// Don't say we didn't warn you!
|
|
|
|
// Note mCurLen was elsewhere limited to mMaxLen, which is normally
|
|
// the greatest long value, and yet even mMaxLen may be experimentally
|
|
// increased with a nyquist comment directive.
|
|
// See the parsing of "maxlen"
|
|
|
|
auto curLen = long(mCurLen.as_long_long());
|
|
nyx_set_audio_params(mCurTrack[0]->GetRate(), curLen);
|
|
|
|
nyx_set_input_audio(StaticGetCallback, (void *)this,
|
|
(int)mCurNumChannels,
|
|
curLen, mCurTrack[0]->GetRate());
|
|
}
|
|
|
|
// Restore the Nyquist sixteenth note symbol for Generate plug-ins.
|
|
// See http://bugzilla.audacityteam.org/show_bug.cgi?id=490.
|
|
if (GetType() == EffectTypeGenerate) {
|
|
cmd += wxT("(setf s 0.25)\n");
|
|
}
|
|
|
|
if (mDebug || mTrace) {
|
|
cmd += wxT("(setf *tracenable* T)\n");
|
|
if (mExternal) {
|
|
cmd += wxT("(setf *breakenable* T)\n");
|
|
}
|
|
}
|
|
else {
|
|
// Explicitly disable backtrace and prevent values
|
|
// from being carried through to the output.
|
|
// This should be the final command before evaluating the Nyquist script.
|
|
cmd += wxT("(setf *tracenable* NIL)\n");
|
|
}
|
|
|
|
for (unsigned int j = 0; j < mControls.size(); j++) {
|
|
if (mControls[j].type == NYQ_CTRL_FLOAT || mControls[j].type == NYQ_CTRL_FLOAT_TEXT ||
|
|
mControls[j].type == NYQ_CTRL_TIME) {
|
|
// We use Internat::ToString() rather than "%f" here because we
|
|
// always have to use the dot as decimal separator when giving
|
|
// numbers to Nyquist, whereas using "%f" will use the user's
|
|
// decimal separator which may be a comma in some countries.
|
|
cmd += wxString::Format(wxT("(setf %s %s)\n"),
|
|
mControls[j].var,
|
|
Internat::ToString(mControls[j].val, 14));
|
|
}
|
|
else if (mControls[j].type == NYQ_CTRL_INT ||
|
|
mControls[j].type == NYQ_CTRL_INT_TEXT ||
|
|
mControls[j].type == NYQ_CTRL_CHOICE) {
|
|
cmd += wxString::Format(wxT("(setf %s %d)\n"),
|
|
mControls[j].var,
|
|
(int)(mControls[j].val));
|
|
}
|
|
else if (mControls[j].type == NYQ_CTRL_STRING || mControls[j].type == NYQ_CTRL_FILE) {
|
|
cmd += wxT("(setf ");
|
|
// restrict variable names to 7-bit ASCII:
|
|
cmd += mControls[j].var;
|
|
cmd += wxT(" \"");
|
|
cmd += EscapeString(mControls[j].valStr); // unrestricted value will become quoted UTF-8
|
|
cmd += wxT("\")\n");
|
|
}
|
|
}
|
|
|
|
if (mIsSal) {
|
|
wxString str = EscapeString(mCmd);
|
|
// this is tricky: we need SAL to call main so that we can get a
|
|
// SAL traceback in the event of an error (sal-compile catches the
|
|
// error and calls sal-error-output), but SAL does not return values.
|
|
// We will catch the value in a special global aud:result and if no
|
|
// error occurs, we will grab the value with a LISP expression
|
|
str += wxT("\nset aud:result = main()\n");
|
|
|
|
if (mDebug || mTrace) {
|
|
// since we're about to evaluate SAL, remove LISP trace enable and
|
|
// break enable (which stops SAL processing) and turn on SAL stack
|
|
// trace
|
|
cmd += wxT("(setf *tracenable* nil)\n");
|
|
cmd += wxT("(setf *breakenable* nil)\n");
|
|
cmd += wxT("(setf *sal-traceback* t)\n");
|
|
}
|
|
|
|
if (mCompiler) {
|
|
cmd += wxT("(setf *sal-compiler-debug* t)\n");
|
|
}
|
|
|
|
cmd += wxT("(setf *sal-call-stack* nil)\n");
|
|
// if we do not set this here and an error occurs in main, another
|
|
// error will be raised when we try to return the value of aud:result
|
|
// which is unbound
|
|
cmd += wxT("(setf aud:result nil)\n");
|
|
cmd += wxT("(sal-compile-audacity \"") + str + wxT("\" t t nil)\n");
|
|
// Capture the value returned by main (saved in aud:result), but
|
|
// set aud:result to nil so sound results can be evaluated without
|
|
// retaining audio in memory
|
|
cmd += wxT("(prog1 aud:result (setf aud:result nil))\n");
|
|
}
|
|
else {
|
|
cmd += mCmd;
|
|
}
|
|
|
|
// Put the fetch buffers in a clean initial state
|
|
for (size_t i = 0; i < mCurNumChannels; i++)
|
|
mCurBuffer[i].Free();
|
|
|
|
// Guarantee release of memory when done
|
|
auto cleanup = finally( [&] {
|
|
for (size_t i = 0; i < mCurNumChannels; i++)
|
|
mCurBuffer[i].Free();
|
|
} );
|
|
|
|
// Evaluate the expression, which may invoke the get callback, but often does
|
|
// not, leaving that to delayed evaluation of the output sound
|
|
rval = nyx_eval_expression(cmd.mb_str(wxConvUTF8));
|
|
|
|
// If we're not showing debug window, log errors and warnings:
|
|
const auto output = mDebugOutput.Translation();
|
|
if (!output.empty() && !mDebug && !mTrace) {
|
|
/* i18n-hint: An effect "returned" a message.*/
|
|
wxLogMessage(wxT("\'%s\' returned:\n%s"),
|
|
mName.Translation(), output);
|
|
}
|
|
|
|
// Audacity has no idea how long Nyquist processing will take, but
|
|
// can monitor audio being returned.
|
|
// Anything other than audio should be returned almost instantly
|
|
// so notify the user that process has completed (bug 558)
|
|
if ((rval != nyx_audio) && ((mCount + mCurNumChannels) == mNumSelectedChannels)) {
|
|
if (mCurNumChannels == 1) {
|
|
TrackProgress(mCount, 1.0, XO("Processing complete."));
|
|
}
|
|
else {
|
|
TrackGroupProgress(mCount, 1.0, XO("Processing complete."));
|
|
}
|
|
}
|
|
|
|
if ((rval == nyx_audio) && (GetType() == EffectTypeTool)) {
|
|
// Catch this first so that we can also handle other errors.
|
|
mDebugOutput =
|
|
/* i18n-hint: Don't translate ';type tool'. */
|
|
XO("';type tool' effects cannot return audio from Nyquist.\n")
|
|
+ mDebugOutput;
|
|
rval = nyx_error;
|
|
}
|
|
|
|
if ((rval == nyx_labels) && (GetType() == EffectTypeTool)) {
|
|
// Catch this first so that we can also handle other errors.
|
|
mDebugOutput =
|
|
/* i18n-hint: Don't translate ';type tool'. */
|
|
XO("';type tool' effects cannot return labels from Nyquist.\n")
|
|
+ mDebugOutput;
|
|
rval = nyx_error;
|
|
}
|
|
|
|
if (rval == nyx_error) {
|
|
// Return value is not valid type.
|
|
// Show error in debug window if trace enabled, otherwise log.
|
|
if (mTrace) {
|
|
/* i18n-hint: "%s" is replaced by name of plug-in.*/
|
|
mDebugOutput = XO("nyx_error returned from %s.\n")
|
|
.Format( mName.empty() ? XO("plug-in") : mName )
|
|
+ mDebugOutput;
|
|
mDebug = true;
|
|
}
|
|
else {
|
|
wxLogMessage(
|
|
"Nyquist returned nyx_error:\n%s", mDebugOutput.Translation());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (rval == nyx_list) {
|
|
wxLogMessage("Nyquist returned nyx_list");
|
|
if (GetType() == EffectTypeTool) {
|
|
mProjectChanged = true;
|
|
} else {
|
|
Effect::MessageBox(XO("Nyquist returned a list.") );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (rval == nyx_string) {
|
|
// Assume the string has already been translated within the Lisp runtime
|
|
// if necessary, by one of the gettext functions defined below, before it
|
|
// is communicated back to C++
|
|
auto msg = Verbatim( NyquistToWxString(nyx_get_string()) );
|
|
if (!msg.empty()) { // Empty string may be used as a No-Op return value.
|
|
Effect::MessageBox( msg );
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
|
|
// True if not process type.
|
|
// If not returning audio from process effect,
|
|
// return first result then stop (disables preview)
|
|
// but allow all output from Nyquist Prompt.
|
|
return (GetType() != EffectTypeProcess || mIsPrompt);
|
|
}
|
|
|
|
if (rval == nyx_double) {
|
|
auto str = XO("Nyquist returned the value: %f")
|
|
.Format(nyx_get_double());
|
|
Effect::MessageBox( str );
|
|
return (GetType() != EffectTypeProcess || mIsPrompt);
|
|
}
|
|
|
|
if (rval == nyx_int) {
|
|
auto str = XO("Nyquist returned the value: %d")
|
|
.Format(nyx_get_int());
|
|
Effect::MessageBox( str );
|
|
return (GetType() != EffectTypeProcess || mIsPrompt);
|
|
}
|
|
|
|
if (rval == nyx_labels) {
|
|
mProjectChanged = true;
|
|
unsigned int numLabels = nyx_get_num_labels();
|
|
unsigned int l;
|
|
auto ltrack = * mOutputTracks->Any< LabelTrack >().begin();
|
|
if (!ltrack) {
|
|
ltrack = static_cast<LabelTrack*>(AddToOutputTracks(mFactory->NewLabelTrack()));
|
|
}
|
|
|
|
for (l = 0; l < numLabels; l++) {
|
|
double t0, t1;
|
|
const char *str;
|
|
|
|
// PRL: to do:
|
|
// let Nyquist analyzers define more complicated selections
|
|
nyx_get_label(l, &t0, &t1, &str);
|
|
|
|
ltrack->AddLabel(SelectedRegion(t0 + mT0, t1 + mT0), UTF8CTOWX(str));
|
|
}
|
|
return (GetType() != EffectTypeProcess || mIsPrompt);
|
|
}
|
|
|
|
wxASSERT(rval == nyx_audio);
|
|
|
|
int outChannels = nyx_get_audio_num_channels();
|
|
if (outChannels > (int)mCurNumChannels) {
|
|
Effect::MessageBox( XO("Nyquist returned too many audio channels.\n") );
|
|
return false;
|
|
}
|
|
|
|
if (outChannels == -1) {
|
|
Effect::MessageBox(
|
|
XO("Nyquist returned one audio channel as an array.\n") );
|
|
return false;
|
|
}
|
|
|
|
if (outChannels == 0) {
|
|
Effect::MessageBox( XO("Nyquist returned an empty array.\n") );
|
|
return false;
|
|
}
|
|
|
|
std::shared_ptr<WaveTrack> outputTrack[2];
|
|
|
|
double rate = mCurTrack[0]->GetRate();
|
|
for (int i = 0; i < outChannels; i++) {
|
|
if (outChannels == (int)mCurNumChannels) {
|
|
rate = mCurTrack[i]->GetRate();
|
|
}
|
|
|
|
outputTrack[i] = mCurTrack[i]->EmptyCopy();
|
|
outputTrack[i]->SetRate( rate );
|
|
|
|
// Clean the initial buffer states again for the get callbacks
|
|
// -- is this really needed?
|
|
mCurBuffer[i].Free();
|
|
}
|
|
|
|
// Now fully evaluate the sound
|
|
int success;
|
|
{
|
|
auto vr0 = valueRestorer( mOutputTrack[0], outputTrack[0].get() );
|
|
auto vr1 = valueRestorer( mOutputTrack[1], outputTrack[1].get() );
|
|
success = nyx_get_audio(StaticPutCallback, (void *)this);
|
|
}
|
|
|
|
// See if GetCallback found read errors
|
|
{
|
|
auto pException = mpException;
|
|
mpException = {};
|
|
if (pException)
|
|
std::rethrow_exception( pException );
|
|
}
|
|
|
|
if (!success)
|
|
return false;
|
|
|
|
for (int i = 0; i < outChannels; i++) {
|
|
outputTrack[i]->Flush();
|
|
mOutputTime = outputTrack[i]->GetEndTime();
|
|
|
|
if (mOutputTime <= 0) {
|
|
Effect::MessageBox( XO("Nyquist returned nil audio.\n") );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < mCurNumChannels; i++) {
|
|
WaveTrack *out;
|
|
|
|
if (outChannels == (int)mCurNumChannels) {
|
|
out = outputTrack[i].get();
|
|
}
|
|
else {
|
|
out = outputTrack[0].get();
|
|
}
|
|
|
|
if (mMergeClips < 0) {
|
|
// Use sample counts to determine default behaviour - times will rarely be equal.
|
|
bool bMergeClips = (out->TimeToLongSamples(mT0) + out->TimeToLongSamples(mOutputTime) ==
|
|
out->TimeToLongSamples(mT1));
|
|
mCurTrack[i]->ClearAndPaste(mT0, mT1, out, mRestoreSplits, bMergeClips);
|
|
}
|
|
else {
|
|
mCurTrack[i]->ClearAndPaste(mT0, mT1, out, mRestoreSplits, mMergeClips != 0);
|
|
}
|
|
|
|
// If we were first in the group adjust non-selected group tracks
|
|
if (mFirstInGroup) {
|
|
for (auto t : TrackList::SyncLockGroup(mCurTrack[i]))
|
|
{
|
|
if (!t->GetSelected() && t->IsSyncLockSelected()) {
|
|
t->SyncLockAdjust(mT1, mT0 + out->GetEndTime());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only the first channel can be first in its group
|
|
mFirstInGroup = false;
|
|
}
|
|
|
|
mProjectChanged = true;
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// NyquistEffect Implementation
|
|
// ============================================================================
|
|
|
|
wxString NyquistEffect::NyquistToWxString(const char *nyqString)
|
|
{
|
|
wxString str(nyqString, wxConvUTF8);
|
|
if (nyqString != NULL && nyqString[0] && str.empty()) {
|
|
// invalid UTF-8 string, convert as Latin-1
|
|
str = _("[Warning: Nyquist returned invalid UTF-8 string, converted here as Latin-1]");
|
|
// TODO: internationalization of strings from Nyquist effects, at least
|
|
// from those shipped with Audacity
|
|
str += LAT1CTOWX(nyqString);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
wxString NyquistEffect::EscapeString(const wxString & inStr)
|
|
{
|
|
wxString str = inStr;
|
|
|
|
str.Replace(wxT("\\"), wxT("\\\\"));
|
|
str.Replace(wxT("\""), wxT("\\\""));
|
|
|
|
return str;
|
|
}
|
|
|
|
std::vector<EnumValueSymbol> NyquistEffect::ParseChoice(const wxString & text)
|
|
{
|
|
std::vector<EnumValueSymbol> results;
|
|
if (text[0] == wxT('(')) {
|
|
// New style: expecting a Lisp-like list of strings
|
|
Tokenizer tzer;
|
|
tzer.Tokenize(text, true, 1, 1);
|
|
auto &choices = tzer.tokens;
|
|
wxString extra;
|
|
for (auto &choice : choices) {
|
|
auto label = UnQuote(choice, true, &extra);
|
|
if (extra.empty())
|
|
results.push_back( TranslatableString{ label, {} } );
|
|
else
|
|
results.push_back(
|
|
{ extra, TranslatableString{ label, {} } } );
|
|
}
|
|
}
|
|
else {
|
|
// Old style: expecting a comma-separated list of
|
|
// un-internationalized names, ignoring leading and trailing spaces
|
|
// on each; and the whole may be quoted
|
|
auto choices = wxStringTokenize(
|
|
text[0] == wxT('"') ? text.Mid(1, text.length() - 2) : text,
|
|
wxT(",")
|
|
);
|
|
for (auto &choice : choices)
|
|
results.push_back( { choice.Trim(true).Trim(false) } );
|
|
}
|
|
return results;
|
|
}
|
|
|
|
FileExtensions NyquistEffect::ParseFileExtensions(const wxString & text)
|
|
{
|
|
// todo: error handling
|
|
FileExtensions results;
|
|
if (text[0] == wxT('(')) {
|
|
Tokenizer tzer;
|
|
tzer.Tokenize(text, true, 1, 1);
|
|
for (const auto &token : tzer.tokens)
|
|
results.push_back( UnQuote( token ) );
|
|
}
|
|
return results;
|
|
}
|
|
|
|
FileNames::FileType NyquistEffect::ParseFileType(const wxString & text)
|
|
{
|
|
// todo: error handling
|
|
FileNames::FileType result;
|
|
if (text[0] == wxT('(')) {
|
|
Tokenizer tzer;
|
|
tzer.Tokenize(text, true, 1, 1);
|
|
auto &tokens = tzer.tokens;
|
|
if ( tokens.size() == 2 )
|
|
result =
|
|
{ UnQuoteMsgid( tokens[0] ), ParseFileExtensions( tokens[1] ) };
|
|
}
|
|
return result;
|
|
}
|
|
|
|
FileNames::FileTypes NyquistEffect::ParseFileTypes(const wxString & text)
|
|
{
|
|
// todo: error handling
|
|
FileNames::FileTypes results;
|
|
if (text[0] == wxT('(')) {
|
|
Tokenizer tzer;
|
|
tzer.Tokenize(text, true, 1, 1);
|
|
auto &types = tzer.tokens;
|
|
if ( !types.empty() && types[0][0] == wxT('(') )
|
|
for (auto &type : types)
|
|
results.push_back( ParseFileType( type ) );
|
|
}
|
|
if ( results.empty() ) {
|
|
// Old-style is a specially formatted string, maybe translated
|
|
// Parse it for compatibility
|
|
auto str = UnQuote( text );
|
|
auto pieces = wxSplit( str, '|' );
|
|
// Should have an even number
|
|
auto size = pieces.size();
|
|
if ( size % 2 == 1 )
|
|
--size, pieces.pop_back();
|
|
for ( size_t ii = 0; ii < size; ii += 2 ) {
|
|
FileExtensions extensions;
|
|
auto extensionStrings = wxSplit( pieces[ii + 1], ';' );
|
|
for ( const auto &extensionString : extensionStrings )
|
|
if ( extensionString.StartsWith( wxT("*.") ) ) {
|
|
auto ext = extensionString.substr( 2 );
|
|
if (ext == wxT("*"))
|
|
// "*.*" to match all
|
|
ext.clear();
|
|
extensions.push_back( ext );
|
|
}
|
|
results.push_back( { Verbatim( pieces[ii] ), extensions } );
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
void NyquistEffect::RedirectOutput()
|
|
{
|
|
mRedirectOutput = true;
|
|
}
|
|
|
|
void NyquistEffect::SetCommand(const wxString &cmd)
|
|
{
|
|
mExternal = true;
|
|
|
|
if (cmd.size()) {
|
|
ParseCommand(cmd);
|
|
}
|
|
}
|
|
|
|
void NyquistEffect::Break()
|
|
{
|
|
mBreak = true;
|
|
}
|
|
|
|
void NyquistEffect::Continue()
|
|
{
|
|
mCont = true;
|
|
}
|
|
|
|
void NyquistEffect::Stop()
|
|
{
|
|
mStop = true;
|
|
}
|
|
|
|
TranslatableString NyquistEffect::UnQuoteMsgid(const wxString &s, bool allowParens,
|
|
wxString *pExtraString)
|
|
{
|
|
if (pExtraString)
|
|
*pExtraString = wxString{};
|
|
|
|
int len = s.length();
|
|
if (len >= 2 && s[0] == wxT('\"') && s[len - 1] == wxT('\"')) {
|
|
auto unquoted = s.Mid(1, len - 2);
|
|
// Sorry, no context strings, yet
|
|
return TranslatableString{ unquoted, {} };
|
|
}
|
|
else if (allowParens &&
|
|
len >= 2 && s[0] == wxT('(') && s[len - 1] == wxT(')')) {
|
|
Tokenizer tzer;
|
|
tzer.Tokenize(s, true, 1, 1);
|
|
auto &tokens = tzer.tokens;
|
|
if (tokens.size() > 1) {
|
|
if (pExtraString && tokens[1][0] == '(') {
|
|
// A choice with a distinct internal string form like
|
|
// ("InternalString" (_ "Visible string"))
|
|
// Recur to find the two strings
|
|
*pExtraString = UnQuote(tokens[0], false);
|
|
return UnQuoteMsgid(tokens[1]);
|
|
}
|
|
else {
|
|
// Assume the first token was _ -- we don't check that
|
|
// And the second is the string, which is internationalized
|
|
// Sorry, no context strings, yet
|
|
return UnQuoteMsgid( tokens[1], false );
|
|
}
|
|
}
|
|
else
|
|
return {};
|
|
}
|
|
else
|
|
// If string was not quoted, assume no translation exists
|
|
return Verbatim( s );
|
|
}
|
|
|
|
wxString NyquistEffect::UnQuote(const wxString &s, bool allowParens,
|
|
wxString *pExtraString)
|
|
{
|
|
return UnQuoteMsgid( s, allowParens, pExtraString ).Translation();
|
|
}
|
|
|
|
double NyquistEffect::GetCtrlValue(const wxString &s)
|
|
{
|
|
/* For this to work correctly requires that the plug-in header is
|
|
* parsed on each run so that the correct value for "half-srate" may
|
|
* be determined.
|
|
*
|
|
auto project = FindProject();
|
|
if (project && s.IsSameAs(wxT("half-srate"), false)) {
|
|
auto rate =
|
|
TrackList::Get( *project ).Selected< const WaveTrack >()
|
|
.min( &WaveTrack::GetRate );
|
|
return (rate / 2.0);
|
|
}
|
|
*/
|
|
|
|
return Internat::CompatibleToDouble(s);
|
|
}
|
|
|
|
bool NyquistEffect::Tokenizer::Tokenize(
|
|
const wxString &line, bool eof,
|
|
size_t trimStart, size_t trimEnd)
|
|
{
|
|
auto endToken = [&]{
|
|
if (!tok.empty()) {
|
|
tokens.push_back(tok);
|
|
tok = wxT("");
|
|
}
|
|
};
|
|
|
|
for (auto c :
|
|
make_iterator_range(line.begin() + trimStart, line.end() - trimEnd)) {
|
|
if (q && !sl && c == wxT('\\')) {
|
|
// begin escaped character, only within quotes
|
|
sl = true;
|
|
continue;
|
|
}
|
|
|
|
if (!sl && c == wxT('"')) {
|
|
// Unescaped quote
|
|
if (!q) {
|
|
// start of string
|
|
if (!paren)
|
|
// finish previous token
|
|
endToken();
|
|
// Include the delimiter in the token
|
|
tok += c;
|
|
q = true;
|
|
}
|
|
else {
|
|
// end of string
|
|
// Include the delimiter in the token
|
|
tok += c;
|
|
if (!paren)
|
|
endToken();
|
|
q = false;
|
|
}
|
|
}
|
|
else if (!q && !paren && (c == wxT(' ') || c == wxT('\t')))
|
|
// Unenclosed whitespace
|
|
// Separate tokens; don't accumulate this character
|
|
endToken();
|
|
else if (!q && c == wxT(';'))
|
|
// semicolon not in quotes, but maybe in parentheses
|
|
// Lisp style comments with ; (but not with #| ... |#) are allowed
|
|
// within a wrapped header multi-line, so that i18n hint comments may
|
|
// be placed before strings and found by xgettext
|
|
break;
|
|
else if (!q && c == wxT('(')) {
|
|
// Start of list or sublist
|
|
if (++paren == 1)
|
|
// finish previous token; begin list, including the delimiter
|
|
endToken(), tok += c;
|
|
else
|
|
// defer tokenizing of nested list to a later pass over the token
|
|
tok += c;
|
|
}
|
|
else if (!q && c == wxT(')')) {
|
|
// End of list or sublist
|
|
if (--paren == 0)
|
|
// finish list, including the delimiter
|
|
tok += c, endToken();
|
|
else if (paren < 0)
|
|
// forgive unbalanced right paren
|
|
paren = 0, endToken();
|
|
else
|
|
// nested list; deferred tokenizing
|
|
tok += c;
|
|
}
|
|
else {
|
|
if (sl && paren)
|
|
// Escaped character in string inside list, to be parsed again
|
|
// Put the escape back for the next pass
|
|
tok += wxT('\\');
|
|
if (sl && !paren && c == 'n')
|
|
// Convert \n to newline, the only special escape besides \\ or \"
|
|
// But this should not be used if a string needs to localize.
|
|
// Instead, simply put a line break in the string.
|
|
c = '\n';
|
|
tok += c;
|
|
}
|
|
|
|
sl = false;
|
|
}
|
|
|
|
if (eof || (!q && !paren)) {
|
|
endToken();
|
|
return true;
|
|
}
|
|
else {
|
|
// End of line but not of file, and a string or list is yet unclosed
|
|
// If a string, accumulate a newline character
|
|
if (q)
|
|
tok += wxT('\n');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool NyquistEffect::Parse(
|
|
Tokenizer &tzer, const wxString &line, bool eof, bool first)
|
|
{
|
|
if ( !tzer.Tokenize(line, eof, first ? 1 : 0, 0) )
|
|
return false;
|
|
|
|
const auto &tokens = tzer.tokens;
|
|
int len = tokens.size();
|
|
if (len < 1) {
|
|
return true;
|
|
}
|
|
|
|
// Consistency decision is for "plug-in" as the correct spelling
|
|
// "plugin" (deprecated) is allowed as an undocumented convenience.
|
|
if (len == 2 && tokens[0] == wxT("nyquist") &&
|
|
(tokens[1] == wxT("plug-in") || tokens[1] == wxT("plugin"))) {
|
|
mOK = true;
|
|
return true;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("type")) {
|
|
wxString tok = tokens[1];
|
|
mIsTool = false;
|
|
if (tok == wxT("tool")) {
|
|
mIsTool = true;
|
|
mType = EffectTypeTool;
|
|
// we allow
|
|
// ;type tool
|
|
// ;type tool process
|
|
// ;type tool generate
|
|
// ;type tool analyze
|
|
// The last three are placed in the tool menu, but are processed as
|
|
// process, generate or analyze.
|
|
if (len >= 3)
|
|
tok = tokens[2];
|
|
}
|
|
|
|
if (tok == wxT("process")) {
|
|
mType = EffectTypeProcess;
|
|
}
|
|
else if (tok == wxT("generate")) {
|
|
mType = EffectTypeGenerate;
|
|
}
|
|
else if (tok == wxT("analyze")) {
|
|
mType = EffectTypeAnalyze;
|
|
}
|
|
|
|
if (len >= 3 && tokens[2] == wxT("spectral")) {;
|
|
mIsSpectral = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (len == 2 && tokens[0] == wxT("codetype")) {
|
|
// This will stop ParseProgram() from doing a best guess as program type.
|
|
if (tokens[1] == wxT("lisp")) {
|
|
mIsSal = false;
|
|
mFoundType = true;
|
|
}
|
|
else if (tokens[1] == wxT("sal")) {
|
|
mIsSal = true;
|
|
mFoundType = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("debugflags")) {
|
|
for (int i = 1; i < len; i++) {
|
|
// "trace" sets *tracenable* (LISP) or *sal-traceback* (SAL)
|
|
// and displays debug window IF there is anything to show.
|
|
if (tokens[i] == wxT("trace")) {
|
|
mTrace = true;
|
|
}
|
|
else if (tokens[i] == wxT("notrace")) {
|
|
mTrace = false;
|
|
}
|
|
else if (tokens[i] == wxT("compiler")) {
|
|
mCompiler = true;
|
|
}
|
|
else if (tokens[i] == wxT("nocompiler")) {
|
|
mCompiler = false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// We support versions 1, 2 and 3
|
|
// (Version 2 added support for string parameters.)
|
|
// (Version 3 added support for choice parameters.)
|
|
// (Version 4 added support for project/track/selection information.)
|
|
if (len >= 2 && tokens[0] == wxT("version")) {
|
|
long v;
|
|
tokens[1].ToLong(&v);
|
|
if (v < 1 || v > 4) {
|
|
// This is an unsupported plug-in version
|
|
mOK = false;
|
|
mInitError = XO(
|
|
"This version of Audacity does not support Nyquist plug-in version %ld")
|
|
.Format( v );
|
|
return true;
|
|
}
|
|
mVersion = (int) v;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("name")) {
|
|
auto name = UnQuote(tokens[1]);
|
|
// Strip ... from name if it's present, perhaps in third party plug-ins
|
|
// Menu system puts ... back if there are any controls
|
|
// This redundant naming convention must NOT be followed for
|
|
// shipped Nyquist effects with internationalization. Else the msgid
|
|
// later looked up will lack the ... and will not be found.
|
|
if (name.EndsWith(wxT("...")))
|
|
name = name.RemoveLast(3);
|
|
mName = TranslatableString{ name, {} };
|
|
return true;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("action")) {
|
|
mAction = TranslatableString{ UnQuote(tokens[1]), {} };
|
|
return true;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("info")) {
|
|
mInfo = TranslatableString{ UnQuote(tokens[1]), {} };
|
|
return true;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("preview")) {
|
|
if (tokens[1] == wxT("enabled") || tokens[1] == wxT("true")) {
|
|
mEnablePreview = true;
|
|
SetLinearEffectFlag(false);
|
|
}
|
|
else if (tokens[1] == wxT("linear")) {
|
|
mEnablePreview = true;
|
|
SetLinearEffectFlag(true);
|
|
}
|
|
else if (tokens[1] == wxT("selection")) {
|
|
mEnablePreview = true;
|
|
SetPreviewFullSelectionFlag(true);
|
|
}
|
|
else if (tokens[1] == wxT("disabled") || tokens[1] == wxT("false")) {
|
|
mEnablePreview = false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Maximum number of samples to be processed. This can help the
|
|
// progress bar if effect does not process all of selection.
|
|
if (len >= 2 && tokens[0] == wxT("maxlen")) {
|
|
long long v; // Note that Nyquist may overflow at > 2^31 samples (bug 439)
|
|
tokens[1].ToLongLong(&v);
|
|
mMaxLen = (sampleCount) v;
|
|
}
|
|
|
|
#if defined(EXPERIMENTAL_NYQUIST_SPLIT_CONTROL)
|
|
if (len >= 2 && tokens[0] == wxT("mergeclips")) {
|
|
long v;
|
|
// -1 = auto (default), 0 = don't merge clips, 1 = do merge clips
|
|
tokens[1].ToLong(&v);
|
|
mMergeClips = v;
|
|
return true;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("restoresplits")) {
|
|
long v;
|
|
// Splits are restored by default. Set to 0 to prevent.
|
|
tokens[1].ToLong(&v);
|
|
mRestoreSplits = !!v;
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
if (len >= 2 && tokens[0] == wxT("author")) {
|
|
mAuthor = TranslatableString{ UnQuote(tokens[1]), {} };
|
|
return true;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("release")) {
|
|
// Value must be quoted if the release version string contains spaces.
|
|
mReleaseVersion =
|
|
TranslatableString{ UnQuote(tokens[1]), {} };
|
|
return true;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("copyright")) {
|
|
mCopyright = TranslatableString{ UnQuote(tokens[1]), {} };
|
|
return true;
|
|
}
|
|
|
|
// Page name in Audacity development manual
|
|
if (len >= 2 && tokens[0] == wxT("manpage")) {
|
|
// do not translate
|
|
mManPage = UnQuote(tokens[1], false);
|
|
return true;
|
|
}
|
|
|
|
// Local Help file
|
|
if (len >= 2 && tokens[0] == wxT("helpfile")) {
|
|
// do not translate
|
|
mHelpFile = UnQuote(tokens[1], false);
|
|
return true;
|
|
}
|
|
|
|
// Debug button may be disabled for release plug-ins.
|
|
if (len >= 2 && tokens[0] == wxT("debugbutton")) {
|
|
if (tokens[1] == wxT("disabled") || tokens[1] == wxT("false")) {
|
|
mDebugButton = false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
if (len >= 3 && tokens[0] == wxT("control")) {
|
|
NyqControl ctrl;
|
|
|
|
if (len == 3 && tokens[1] == wxT("text")) {
|
|
ctrl.var = tokens[1];
|
|
ctrl.label = UnQuote( tokens[2] );
|
|
ctrl.type = NYQ_CTRL_TEXT;
|
|
}
|
|
else if (len >= 5)
|
|
{
|
|
ctrl.var = tokens[1];
|
|
ctrl.name = UnQuote( tokens[2] );
|
|
// 3 is type, below
|
|
ctrl.label = tokens[4];
|
|
|
|
// valStr may or may not be a quoted string
|
|
ctrl.valStr = len > 5 ? tokens[5] : wxT("");
|
|
ctrl.val = GetCtrlValue(ctrl.valStr);
|
|
if (ctrl.valStr.length() > 0 &&
|
|
(ctrl.valStr[0] == wxT('(') ||
|
|
ctrl.valStr[0] == wxT('"')))
|
|
ctrl.valStr = UnQuote( ctrl.valStr );
|
|
|
|
// 6 is minimum, below
|
|
// 7 is maximum, below
|
|
|
|
if (tokens[3] == wxT("string")) {
|
|
ctrl.type = NYQ_CTRL_STRING;
|
|
ctrl.label = UnQuote( ctrl.label );
|
|
}
|
|
else if (tokens[3] == wxT("choice")) {
|
|
ctrl.type = NYQ_CTRL_CHOICE;
|
|
ctrl.choices = ParseChoice(ctrl.label);
|
|
ctrl.label = wxT("");
|
|
}
|
|
else if (tokens[3] == wxT("file")) {
|
|
ctrl.type = NYQ_CTRL_FILE;
|
|
ctrl.fileTypes = ParseFileTypes(tokens[6]);
|
|
// will determine file dialog styles:
|
|
ctrl.highStr = UnQuote( tokens[7] );
|
|
ctrl.label = wxT("");
|
|
}
|
|
else {
|
|
ctrl.label = UnQuote( ctrl.label );
|
|
|
|
if (len < 8) {
|
|
return true;
|
|
}
|
|
|
|
if ((tokens[3] == wxT("float")) ||
|
|
(tokens[3] == wxT("real"))) // Deprecated
|
|
ctrl.type = NYQ_CTRL_FLOAT;
|
|
else if (tokens[3] == wxT("int"))
|
|
ctrl.type = NYQ_CTRL_INT;
|
|
else if (tokens[3] == wxT("float-text"))
|
|
ctrl.type = NYQ_CTRL_FLOAT_TEXT;
|
|
else if (tokens[3] == wxT("int-text"))
|
|
ctrl.type = NYQ_CTRL_INT_TEXT;
|
|
else if (tokens[3] == wxT("time"))
|
|
ctrl.type = NYQ_CTRL_TIME;
|
|
else
|
|
{
|
|
wxString str;
|
|
str.Printf(wxT("Bad Nyquist 'control' type specification: '%s' in plug-in file '%s'.\nControl not created."),
|
|
tokens[3], mFileName.GetFullPath());
|
|
|
|
// Too disturbing to show alert before Audacity frame is up.
|
|
// Effect::MessageBox(
|
|
// str,
|
|
// wxOK | wxICON_EXCLAMATION,
|
|
// XO("Nyquist Warning") );
|
|
|
|
// Note that the AudacityApp's mLogger has not yet been created,
|
|
// so this brings up an alert box, but after the Audacity frame is up.
|
|
wxLogWarning(str);
|
|
return true;
|
|
}
|
|
|
|
ctrl.lowStr = UnQuote( tokens[6] );
|
|
if (ctrl.type == NYQ_CTRL_INT_TEXT && ctrl.lowStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl.low = INT_MIN;
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_FLOAT_TEXT && ctrl.lowStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl.low = -(FLT_MAX);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_TIME && ctrl.lowStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl.low = 0.0;
|
|
}
|
|
else {
|
|
ctrl.low = GetCtrlValue(ctrl.lowStr);
|
|
}
|
|
|
|
ctrl.highStr = UnQuote( tokens[7] );
|
|
if (ctrl.type == NYQ_CTRL_INT_TEXT && ctrl.highStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl.high = INT_MAX;
|
|
}
|
|
else if ((ctrl.type == NYQ_CTRL_FLOAT_TEXT || ctrl.type == NYQ_CTRL_TIME) &&
|
|
ctrl.highStr.IsSameAs(wxT("nil"), false))
|
|
{
|
|
ctrl.high = FLT_MAX;
|
|
}
|
|
else {
|
|
ctrl.high = GetCtrlValue(ctrl.highStr);
|
|
}
|
|
|
|
if (ctrl.high < ctrl.low) {
|
|
ctrl.high = ctrl.low;
|
|
}
|
|
|
|
if (ctrl.val < ctrl.low) {
|
|
ctrl.val = ctrl.low;
|
|
}
|
|
|
|
if (ctrl.val > ctrl.high) {
|
|
ctrl.val = ctrl.high;
|
|
}
|
|
|
|
ctrl.ticks = 1000;
|
|
if (ctrl.type == NYQ_CTRL_INT &&
|
|
(ctrl.high - ctrl.low < ctrl.ticks)) {
|
|
ctrl.ticks = (int)(ctrl.high - ctrl.low);
|
|
}
|
|
}
|
|
}
|
|
|
|
if( ! make_iterator_range( mPresetNames ).contains( ctrl.var ) )
|
|
{
|
|
mControls.push_back(ctrl);
|
|
}
|
|
}
|
|
|
|
// Deprecated
|
|
if (len >= 2 && tokens[0] == wxT("categories")) {
|
|
for (size_t i = 1; i < tokens.size(); ++i) {
|
|
mCategories.push_back(tokens[i]);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool NyquistEffect::ParseProgram(wxInputStream & stream)
|
|
{
|
|
if (!stream.IsOk())
|
|
{
|
|
mInitError = XO("Could not open file");
|
|
return false;
|
|
}
|
|
|
|
wxTextInputStream pgm(stream, wxT(" \t"), wxConvAuto());
|
|
|
|
mCmd = wxT("");
|
|
mCmd.Alloc(10000);
|
|
mIsSal = false;
|
|
mControls.clear();
|
|
mCategories.clear();
|
|
mIsSpectral = false;
|
|
mManPage = wxEmptyString; // If not wxEmptyString, must be a page in the Audacity manual.
|
|
mHelpFile = wxEmptyString; // If not wxEmptyString, must be a valid HTML help file.
|
|
mHelpFileExists = false;
|
|
mDebug = false;
|
|
mTrace = false;
|
|
mDebugButton = true; // Debug button enabled by default.
|
|
mEnablePreview = true; // Preview button enabled by default.
|
|
|
|
// Bug 1934.
|
|
// All Nyquist plug-ins should have a ';type' field, but if they don't we default to
|
|
// being an Effect.
|
|
mType = EffectTypeProcess;
|
|
|
|
mFoundType = false;
|
|
while (!stream.Eof() && stream.IsOk())
|
|
{
|
|
wxString line = pgm.ReadLine();
|
|
if (line.length() > 1 &&
|
|
// New in 2.3.0: allow magic comment lines to start with $
|
|
// The trick is that xgettext will not consider such lines comments
|
|
// and will extract the strings they contain
|
|
(line[0] == wxT(';') || line[0] == wxT('$')) )
|
|
{
|
|
Tokenizer tzer;
|
|
unsigned nLines = 1;
|
|
bool done;
|
|
// Allow continuations within control lines.
|
|
bool control =
|
|
line[0] == wxT('$') || line.StartsWith( wxT(";control") );
|
|
do
|
|
done = Parse(tzer, line, !control || stream.Eof(), nLines == 1);
|
|
while(!done &&
|
|
(line = pgm.ReadLine(), ++nLines, true));
|
|
|
|
// Don't pass these lines to the interpreter, so it doesn't get confused
|
|
// by $, but pass blanks,
|
|
// so that SAL effects compile with proper line numbers
|
|
while (nLines --)
|
|
mCmd += wxT('\n');
|
|
}
|
|
else
|
|
{
|
|
if(!mFoundType && line.length() > 0) {
|
|
if (line[0] == wxT('(') ||
|
|
(line[0] == wxT('#') && line.length() > 1 && line[1] == wxT('|')))
|
|
{
|
|
mIsSal = false;
|
|
mFoundType = true;
|
|
}
|
|
else if (line.Upper().Find(wxT("RETURN")) != wxNOT_FOUND)
|
|
{
|
|
mIsSal = true;
|
|
mFoundType = true;
|
|
}
|
|
}
|
|
mCmd += line + wxT("\n");
|
|
}
|
|
}
|
|
if (!mFoundType && mIsPrompt)
|
|
{
|
|
/* i1n-hint: SAL and LISP are names for variant syntaxes for the
|
|
Nyquist programming language. Leave them, and 'return', untranslated. */
|
|
Effect::MessageBox(
|
|
XO(
|
|
"Your code looks like SAL syntax, but there is no \'return\' statement.\n\
|
|
For SAL, use a return statement such as:\n\treturn *track* * 0.1\n\
|
|
or for LISP, begin with an open parenthesis such as:\n\t(mult *track* 0.1)\n ."),
|
|
Effect::DefaultMessageBoxStyle,
|
|
XO("Error in Nyquist code") );
|
|
/* i18n-hint: refers to programming "languages" */
|
|
mInitError = XO("Could not determine language");
|
|
return false;
|
|
// Else just throw it at Nyquist to see what happens
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void NyquistEffect::ParseFile()
|
|
{
|
|
wxFileInputStream rawStream(mFileName.GetFullPath());
|
|
wxBufferedInputStream stream(rawStream, 10000);
|
|
|
|
ParseProgram(stream);
|
|
}
|
|
|
|
bool NyquistEffect::ParseCommand(const wxString & cmd)
|
|
{
|
|
wxStringInputStream stream(cmd + wxT(" "));
|
|
|
|
return ParseProgram(stream);
|
|
}
|
|
|
|
int NyquistEffect::StaticGetCallback(float *buffer, int channel,
|
|
long start, long len, long totlen,
|
|
void *userdata)
|
|
{
|
|
NyquistEffect *This = (NyquistEffect *)userdata;
|
|
return This->GetCallback(buffer, channel, start, len, totlen);
|
|
}
|
|
|
|
int NyquistEffect::GetCallback(float *buffer, int ch,
|
|
long start, long len, long WXUNUSED(totlen))
|
|
{
|
|
if (mCurBuffer[ch].ptr()) {
|
|
if ((mCurStart[ch] + start) < mCurBufferStart[ch] ||
|
|
(mCurStart[ch] + start)+len >
|
|
mCurBufferStart[ch]+mCurBufferLen[ch]) {
|
|
mCurBuffer[ch].Free();
|
|
}
|
|
}
|
|
|
|
if (!mCurBuffer[ch].ptr()) {
|
|
mCurBufferStart[ch] = (mCurStart[ch] + start);
|
|
mCurBufferLen[ch] = mCurTrack[ch]->GetBestBlockSize(mCurBufferStart[ch]);
|
|
|
|
if (mCurBufferLen[ch] < (size_t) len) {
|
|
mCurBufferLen[ch] = mCurTrack[ch]->GetIdealBlockSize();
|
|
}
|
|
|
|
mCurBufferLen[ch] =
|
|
limitSampleBufferSize( mCurBufferLen[ch],
|
|
mCurStart[ch] + mCurLen - mCurBufferStart[ch] );
|
|
|
|
mCurBuffer[ch].Allocate(mCurBufferLen[ch], floatSample);
|
|
try {
|
|
mCurTrack[ch]->Get(
|
|
mCurBuffer[ch].ptr(), floatSample,
|
|
mCurBufferStart[ch], mCurBufferLen[ch]);
|
|
}
|
|
catch ( ... ) {
|
|
// Save the exception object for re-throw when out of the library
|
|
mpException = std::current_exception();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// We have guaranteed above that this is nonnegative and bounded by
|
|
// mCurBufferLen[ch]:
|
|
auto offset = ( mCurStart[ch] + start - mCurBufferStart[ch] ).as_size_t();
|
|
CopySamples(mCurBuffer[ch].ptr() + offset*SAMPLE_SIZE(floatSample), floatSample,
|
|
(samplePtr)buffer, floatSample,
|
|
len);
|
|
|
|
if (ch == 0) {
|
|
double progress = mScale *
|
|
( (start+len)/ mCurLen.as_double() );
|
|
|
|
if (progress > mProgressIn) {
|
|
mProgressIn = progress;
|
|
}
|
|
|
|
if (TotalProgress(mProgressIn+mProgressOut+mProgressTot)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int NyquistEffect::StaticPutCallback(float *buffer, int channel,
|
|
long start, long len, long totlen,
|
|
void *userdata)
|
|
{
|
|
NyquistEffect *This = (NyquistEffect *)userdata;
|
|
return This->PutCallback(buffer, channel, start, len, totlen);
|
|
}
|
|
|
|
int NyquistEffect::PutCallback(float *buffer, int channel,
|
|
long start, long len, long totlen)
|
|
{
|
|
// Don't let C++ exceptions propagate through the Nyquist library
|
|
return GuardedCall<int>( [&] {
|
|
if (channel == 0) {
|
|
double progress = mScale*((float)(start+len)/totlen);
|
|
|
|
if (progress > mProgressOut) {
|
|
mProgressOut = progress;
|
|
}
|
|
|
|
if (TotalProgress(mProgressIn+mProgressOut+mProgressTot)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
mOutputTrack[channel]->Append((samplePtr)buffer, floatSample, len);
|
|
|
|
return 0; // success
|
|
}, MakeSimpleGuard( -1 ) ); // translate all exceptions into failure
|
|
}
|
|
|
|
void NyquistEffect::StaticOutputCallback(int c, void *This)
|
|
{
|
|
((NyquistEffect *)This)->OutputCallback(c);
|
|
}
|
|
|
|
void NyquistEffect::OutputCallback(int c)
|
|
{
|
|
// Always collect Nyquist error messages for normal plug-ins
|
|
if (!mRedirectOutput) {
|
|
mDebugOutputStr += (char)c;
|
|
return;
|
|
}
|
|
|
|
std::cout << (char)c;
|
|
}
|
|
|
|
void NyquistEffect::StaticOSCallback(void *This)
|
|
{
|
|
((NyquistEffect *)This)->OSCallback();
|
|
}
|
|
|
|
void NyquistEffect::OSCallback()
|
|
{
|
|
if (mStop) {
|
|
mStop = false;
|
|
nyx_stop();
|
|
}
|
|
else if (mBreak) {
|
|
mBreak = false;
|
|
nyx_break();
|
|
}
|
|
else if (mCont) {
|
|
mCont = false;
|
|
nyx_continue();
|
|
}
|
|
|
|
// LLL: STF figured out that yielding while the effect is being applied
|
|
// produces an EXTREME slowdown. It appears that yielding is not
|
|
// really necessary on Linux and Windows.
|
|
//
|
|
// However, on the Mac, the spinning cursor appears during longer
|
|
// Nyquist processing and that may cause the user to think Audacity
|
|
// has crashed or hung. In addition, yielding or not on the Mac
|
|
// doesn't seem to make much of a difference in execution time.
|
|
//
|
|
// So, yielding on the Mac only...
|
|
#if defined(__WXMAC__)
|
|
wxYieldIfNeeded();
|
|
#endif
|
|
}
|
|
|
|
FilePaths NyquistEffect::GetNyquistSearchPath()
|
|
{
|
|
const auto &audacityPathList = FileNames::AudacityPathList();
|
|
FilePaths pathList;
|
|
|
|
for (size_t i = 0; i < audacityPathList.size(); i++)
|
|
{
|
|
wxString prefix = audacityPathList[i] + wxFILE_SEP_PATH;
|
|
FileNames::AddUniquePathToPathList(prefix + wxT("nyquist"), pathList);
|
|
FileNames::AddUniquePathToPathList(prefix + wxT("plugins"), pathList);
|
|
FileNames::AddUniquePathToPathList(prefix + wxT("plug-ins"), pathList);
|
|
}
|
|
pathList.push_back(FileNames::PlugInDir());
|
|
|
|
return pathList;
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataToPromptWindow()
|
|
{
|
|
mCommandText->ChangeValue(mInputCmd);
|
|
mVersionCheckBox->SetValue(mVersion <= 3);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataToEffectWindow()
|
|
{
|
|
for (size_t i = 0, cnt = mControls.size(); i < cnt; i++)
|
|
{
|
|
NyqControl & ctrl = mControls[i];
|
|
|
|
if (ctrl.type == NYQ_CTRL_CHOICE)
|
|
{
|
|
const auto count = ctrl.choices.size();
|
|
|
|
int val = (int)ctrl.val;
|
|
if (val < 0 || val >= (int)count)
|
|
{
|
|
val = 0;
|
|
}
|
|
|
|
wxChoice *c = (wxChoice *) mUIParent->FindWindow(ID_Choice + i);
|
|
c->SetSelection(val);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_FLOAT)
|
|
{
|
|
// wxTextCtrls are handled by the validators
|
|
double range = ctrl.high - ctrl.low;
|
|
int val = (int)(0.5 + ctrl.ticks * (ctrl.val - ctrl.low) / range);
|
|
wxSlider *s = (wxSlider *) mUIParent->FindWindow(ID_Slider + i);
|
|
s->SetValue(val);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataFromPromptWindow()
|
|
{
|
|
mInputCmd = mCommandText->GetValue();
|
|
|
|
// Un-correct smart quoting, bothersomely applied in wxTextCtrl by
|
|
// the native widget of MacOS 10.9 SDK
|
|
const wxString left = wxT("\u201c"), right = wxT("\u201d"), dumb = '"';
|
|
mInputCmd.Replace(left, dumb, true);
|
|
mInputCmd.Replace(right, dumb, true);
|
|
|
|
const wxString leftSingle = wxT("\u2018"), rightSingle = wxT("\u2019"),
|
|
dumbSingle = '\'';
|
|
mInputCmd.Replace(leftSingle, dumbSingle, true);
|
|
mInputCmd.Replace(rightSingle, dumbSingle, true);
|
|
|
|
mVersion = mVersionCheckBox->GetValue() ? 3 : 4;
|
|
|
|
return ParseCommand(mInputCmd);
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataFromEffectWindow()
|
|
{
|
|
if (mControls.size() == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
for (unsigned int i = 0; i < mControls.size(); i++)
|
|
{
|
|
NyqControl *ctrl = &mControls[i];
|
|
|
|
if (ctrl->type == NYQ_CTRL_STRING)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (ctrl->val == UNINITIALIZED_CONTROL)
|
|
{
|
|
ctrl->val = GetCtrlValue(ctrl->valStr);
|
|
}
|
|
|
|
if (ctrl->type == NYQ_CTRL_CHOICE)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (ctrl->type == NYQ_CTRL_FILE)
|
|
{
|
|
resolveFilePath(ctrl->valStr);
|
|
|
|
wxString path;
|
|
if (ctrl->valStr.StartsWith("\"", &path))
|
|
{
|
|
// Validate if a list of quoted paths.
|
|
if (path.EndsWith("\"", &path))
|
|
{
|
|
path.Replace("\"\"", "\"");
|
|
wxStringTokenizer tokenizer(path, "\"");
|
|
while (tokenizer.HasMoreTokens())
|
|
{
|
|
wxString token = tokenizer.GetNextToken();
|
|
if(!validatePath(token))
|
|
{
|
|
const auto message =
|
|
XO("\"%s\" is not a valid file path.").Format( token );
|
|
Effect::MessageBox(
|
|
message,
|
|
wxOK | wxICON_EXCLAMATION | wxCENTRE,
|
|
XO("Error") );
|
|
return false;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
const auto message =
|
|
/* i18n-hint: Warning that there is one quotation mark rather than a pair.*/
|
|
XO("Mismatched quotes in\n%s").Format( ctrl->valStr );
|
|
Effect::MessageBox(
|
|
message,
|
|
wxOK | wxICON_EXCLAMATION | wxCENTRE,
|
|
XO("Error") );
|
|
return false;
|
|
}
|
|
}
|
|
// Validate a single path.
|
|
else if (validatePath(ctrl->valStr))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Validation failed
|
|
const auto message =
|
|
XO("\"%s\" is not a valid file path.").Format( ctrl->valStr );
|
|
Effect::MessageBox(
|
|
message,
|
|
wxOK | wxICON_EXCLAMATION | wxCENTRE,
|
|
XO("Error") );
|
|
return false;
|
|
}
|
|
|
|
if (ctrl->type == NYQ_CTRL_TIME)
|
|
{
|
|
NumericTextCtrl *n = (NumericTextCtrl *) mUIParent->FindWindow(ID_Time + i);
|
|
ctrl->val = n->GetValue();
|
|
}
|
|
|
|
if (ctrl->type == NYQ_CTRL_INT_TEXT && ctrl->lowStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl->low = INT_MIN;
|
|
}
|
|
else if ((ctrl->type == NYQ_CTRL_FLOAT_TEXT || ctrl->type == NYQ_CTRL_TIME) &&
|
|
ctrl->lowStr.IsSameAs(wxT("nil"), false))
|
|
{
|
|
ctrl->low = -(FLT_MAX);
|
|
}
|
|
else
|
|
{
|
|
ctrl->low = GetCtrlValue(ctrl->lowStr);
|
|
}
|
|
|
|
if (ctrl->type == NYQ_CTRL_INT_TEXT && ctrl->highStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl->high = INT_MAX;
|
|
}
|
|
else if ((ctrl->type == NYQ_CTRL_FLOAT_TEXT || ctrl->type == NYQ_CTRL_TIME) &&
|
|
ctrl->highStr.IsSameAs(wxT("nil"), false))
|
|
{
|
|
ctrl->high = FLT_MAX;
|
|
}
|
|
else
|
|
{
|
|
ctrl->high = GetCtrlValue(ctrl->highStr);
|
|
}
|
|
|
|
if (ctrl->high < ctrl->low)
|
|
{
|
|
ctrl->high = ctrl->low + 1;
|
|
}
|
|
|
|
if (ctrl->val < ctrl->low)
|
|
{
|
|
ctrl->val = ctrl->low;
|
|
}
|
|
|
|
if (ctrl->val > ctrl->high)
|
|
{
|
|
ctrl->val = ctrl->high;
|
|
}
|
|
|
|
ctrl->ticks = 1000;
|
|
if (ctrl->type == NYQ_CTRL_INT &&
|
|
(ctrl->high - ctrl->low < ctrl->ticks))
|
|
{
|
|
ctrl->ticks = (int)(ctrl->high - ctrl->low);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void NyquistEffect::BuildPromptWindow(ShuttleGui & S)
|
|
{
|
|
S.StartVerticalLay();
|
|
{
|
|
S.StartMultiColumn(3, wxEXPAND);
|
|
{
|
|
S.SetStretchyCol(1);
|
|
|
|
S.AddVariableText(XO("Enter Nyquist Command: "));
|
|
|
|
S.AddSpace(1, 1);
|
|
|
|
mVersionCheckBox = S.AddCheckBox(XXO("&Use legacy (version 3) syntax."),
|
|
(mVersion == 3));
|
|
}
|
|
S.EndMultiColumn();
|
|
|
|
S.StartHorizontalLay(wxEXPAND, 1);
|
|
{
|
|
mCommandText = S.Focus()
|
|
.MinSize( { 500, 200 } )
|
|
.AddTextWindow(wxT(""));
|
|
}
|
|
S.EndHorizontalLay();
|
|
|
|
S.StartHorizontalLay(wxALIGN_CENTER, 0);
|
|
{
|
|
S.Id(ID_Load).AddButton(XXO("&Load"));
|
|
S.Id(ID_Save).AddButton(XXO("&Save"));
|
|
}
|
|
S.EndHorizontalLay();
|
|
}
|
|
S.EndVerticalLay();
|
|
}
|
|
|
|
void NyquistEffect::BuildEffectWindow(ShuttleGui & S)
|
|
{
|
|
wxScrolledWindow *scroller = S.Style(wxVSCROLL | wxTAB_TRAVERSAL)
|
|
.StartScroller(2);
|
|
{
|
|
S.StartMultiColumn(4);
|
|
{
|
|
for (size_t i = 0; i < mControls.size(); i++)
|
|
{
|
|
NyqControl & ctrl = mControls[i];
|
|
|
|
if (ctrl.type == NYQ_CTRL_TEXT)
|
|
{
|
|
S.EndMultiColumn();
|
|
S.StartHorizontalLay(wxALIGN_LEFT, 0);
|
|
{
|
|
S.AddSpace(0, 10);
|
|
S.AddFixedText( Verbatim( ctrl.label ), false );
|
|
}
|
|
S.EndHorizontalLay();
|
|
S.StartMultiColumn(4);
|
|
}
|
|
else
|
|
{
|
|
auto prompt = XXO("%s:").Format( ctrl.name );
|
|
S.AddPrompt( prompt );
|
|
|
|
if (ctrl.type == NYQ_CTRL_STRING)
|
|
{
|
|
S.AddSpace(10, 10);
|
|
|
|
auto item = S.Id(ID_Text + i)
|
|
.Validator<wxGenericValidator>(&ctrl.valStr)
|
|
.Name( prompt )
|
|
.AddTextBox( {}, wxT(""), 50);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_CHOICE)
|
|
{
|
|
S.AddSpace(10, 10);
|
|
|
|
S.Id(ID_Choice + i).AddChoice( {},
|
|
Msgids( ctrl.choices.data(), ctrl.choices.size() ) );
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_TIME)
|
|
{
|
|
S.AddSpace(10, 10);
|
|
|
|
const auto options = NumericTextCtrl::Options{}
|
|
.AutoPos(true)
|
|
.MenuEnabled(true)
|
|
.ReadOnly(false);
|
|
|
|
NumericTextCtrl *time = safenew
|
|
NumericTextCtrl(S.GetParent(), (ID_Time + i),
|
|
NumericConverter::TIME,
|
|
GetSelectionFormat(),
|
|
ctrl.val,
|
|
mProjectRate,
|
|
options);
|
|
S
|
|
.Name( prompt )
|
|
.Position(wxALIGN_LEFT | wxALL)
|
|
.AddWindow(time);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_FILE)
|
|
{
|
|
S.AddSpace(10, 10);
|
|
|
|
// Get default file extension if specified in wildcards
|
|
FileExtension defaultExtension;
|
|
if (!ctrl.fileTypes.empty()) {
|
|
const auto &type = ctrl.fileTypes[0];
|
|
if ( !type.extensions.empty() )
|
|
defaultExtension = type.extensions[0];
|
|
}
|
|
resolveFilePath(ctrl.valStr, defaultExtension);
|
|
|
|
wxTextCtrl *item = S.Id(ID_Text+i)
|
|
.Name( prompt )
|
|
.AddTextBox( {}, wxT(""), 40);
|
|
item->SetValidator(wxGenericValidator(&ctrl.valStr));
|
|
|
|
if (ctrl.label.empty())
|
|
// We'd expect wxFileSelectorPromptStr to already be translated, but apparently not.
|
|
ctrl.label = wxGetTranslation( wxFileSelectorPromptStr );
|
|
S.Id(ID_FILE + i).AddButton(
|
|
Verbatim(ctrl.label), wxALIGN_LEFT);
|
|
}
|
|
else
|
|
{
|
|
// Integer or Real
|
|
if (ctrl.type == NYQ_CTRL_INT_TEXT || ctrl.type == NYQ_CTRL_FLOAT_TEXT)
|
|
{
|
|
S.AddSpace(10, 10);
|
|
}
|
|
|
|
S.Id(ID_Text+i);
|
|
if (ctrl.type == NYQ_CTRL_FLOAT || ctrl.type == NYQ_CTRL_FLOAT_TEXT)
|
|
{
|
|
double range = ctrl.high - ctrl.low;
|
|
S.Validator<FloatingPointValidator<double>>(
|
|
// > 12 decimal places can cause rounding errors in display.
|
|
12, &ctrl.val,
|
|
// Set number of decimal places
|
|
(range < 10
|
|
? NumValidatorStyle::THREE_TRAILING_ZEROES
|
|
: range < 100
|
|
? NumValidatorStyle::TWO_TRAILING_ZEROES
|
|
: NumValidatorStyle::ONE_TRAILING_ZERO),
|
|
ctrl.low, ctrl.high
|
|
);
|
|
}
|
|
else
|
|
{
|
|
S.Validator<IntegerValidator<double>>(
|
|
&ctrl.val, NumValidatorStyle::DEFAULT,
|
|
(int) ctrl.low, (int) ctrl.high);
|
|
}
|
|
wxTextCtrl *item = S
|
|
.Name( prompt )
|
|
.AddTextBox( {}, wxT(""),
|
|
(ctrl.type == NYQ_CTRL_INT_TEXT ||
|
|
ctrl.type == NYQ_CTRL_FLOAT_TEXT) ? 25 : 12);
|
|
|
|
if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_FLOAT)
|
|
{
|
|
S.Id(ID_Slider + i)
|
|
.Style(wxSL_HORIZONTAL)
|
|
.MinSize( { 150, -1 } )
|
|
.AddSlider( {}, 0, ctrl.ticks, 0);
|
|
}
|
|
}
|
|
|
|
if (ctrl.type != NYQ_CTRL_FILE)
|
|
{
|
|
if (ctrl.type == NYQ_CTRL_CHOICE || ctrl.label.empty())
|
|
{
|
|
S.AddSpace(10, 10);
|
|
}
|
|
else
|
|
{
|
|
S.AddUnits( Verbatim( ctrl.label ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
S.EndMultiColumn();
|
|
}
|
|
S.EndScroller();
|
|
|
|
scroller->SetScrollRate(0, 20);
|
|
|
|
// This fools NVDA into not saying "Panel" when the dialog gets focus
|
|
scroller->SetName(wxT("\a"));
|
|
scroller->SetLabel(wxT("\a"));
|
|
}
|
|
|
|
// NyquistEffect implementation
|
|
|
|
bool NyquistEffect::IsOk()
|
|
{
|
|
return mOK;
|
|
}
|
|
|
|
static const FileNames::FileType
|
|
/* i18n-hint: Nyquist is the name of a programming language */
|
|
NyquistScripts = { XO("Nyquist scripts"), { wxT("ny") }, true }
|
|
/* i18n-hint: Lisp is the name of a programming language */
|
|
, LispScripts = { XO("Lisp scripts"), { wxT("lsp") }, true }
|
|
;
|
|
|
|
void NyquistEffect::OnLoad(wxCommandEvent & WXUNUSED(evt))
|
|
{
|
|
if (mCommandText->IsModified())
|
|
{
|
|
if (wxNO == Effect::MessageBox(
|
|
XO("Current program has been modified.\nDiscard changes?"),
|
|
wxYES_NO ) )
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
FileDialogWrapper dlog(
|
|
mUIParent,
|
|
XO("Load Nyquist script"),
|
|
mFileName.GetPath(),
|
|
wxEmptyString,
|
|
{
|
|
NyquistScripts,
|
|
LispScripts,
|
|
FileNames::TextFiles,
|
|
FileNames::AllFiles
|
|
},
|
|
wxFD_OPEN | wxRESIZE_BORDER);
|
|
|
|
if (dlog.ShowModal() != wxID_OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
mFileName = dlog.GetPath();
|
|
|
|
if (!mCommandText->LoadFile(mFileName.GetFullPath()))
|
|
{
|
|
Effect::MessageBox( XO("File could not be loaded") );
|
|
}
|
|
}
|
|
|
|
void NyquistEffect::OnSave(wxCommandEvent & WXUNUSED(evt))
|
|
{
|
|
FileDialogWrapper dlog(
|
|
mUIParent,
|
|
XO("Save Nyquist script"),
|
|
mFileName.GetPath(),
|
|
mFileName.GetFullName(),
|
|
{
|
|
NyquistScripts,
|
|
LispScripts,
|
|
FileNames::AllFiles
|
|
},
|
|
wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxRESIZE_BORDER);
|
|
|
|
if (dlog.ShowModal() != wxID_OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
mFileName = dlog.GetPath();
|
|
|
|
if (!mCommandText->SaveFile(mFileName.GetFullPath()))
|
|
{
|
|
Effect::MessageBox( XO("File could not be saved") );
|
|
}
|
|
}
|
|
|
|
void NyquistEffect::OnSlider(wxCommandEvent & evt)
|
|
{
|
|
int i = evt.GetId() - ID_Slider;
|
|
NyqControl & ctrl = mControls[i];
|
|
|
|
int val = evt.GetInt();
|
|
double range = ctrl.high - ctrl.low;
|
|
double newVal = (val / (double)ctrl.ticks) * range + ctrl.low;
|
|
|
|
// Determine precision for displayed number
|
|
int precision = range < 1.0 ? 3 :
|
|
range < 10.0 ? 2 :
|
|
range < 100.0 ? 1 :
|
|
0;
|
|
|
|
// If the value is at least one tick different from the current value
|
|
// change it (this prevents changes from manually entered values unless
|
|
// the slider actually moved)
|
|
if (fabs(newVal - ctrl.val) >= (1 / (double)ctrl.ticks) * range &&
|
|
fabs(newVal - ctrl.val) >= pow(0.1, precision) / 2)
|
|
{
|
|
// First round to the appropriate precision
|
|
newVal *= pow(10.0, precision);
|
|
newVal = floor(newVal + 0.5);
|
|
newVal /= pow(10.0, precision);
|
|
|
|
ctrl.val = newVal;
|
|
|
|
mUIParent->FindWindow(ID_Text + i)->GetValidator()->TransferToWindow();
|
|
}
|
|
}
|
|
|
|
void NyquistEffect::OnChoice(wxCommandEvent & evt)
|
|
{
|
|
mControls[evt.GetId() - ID_Choice].val = (double) evt.GetInt();
|
|
}
|
|
|
|
void NyquistEffect::OnTime(wxCommandEvent& evt)
|
|
{
|
|
int i = evt.GetId() - ID_Time;
|
|
static double value = 0.0;
|
|
NyqControl & ctrl = mControls[i];
|
|
|
|
NumericTextCtrl *n = (NumericTextCtrl *) mUIParent->FindWindow(ID_Time + i);
|
|
double val = n->GetValue();
|
|
|
|
// Observed that two events transmitted on each control change (Linux)
|
|
// so skip if value has not changed.
|
|
if (val != value) {
|
|
if (val < ctrl.low || val > ctrl.high) {
|
|
const auto message = XO("Value range:\n%s to %s")
|
|
.Format( ToTimeFormat(ctrl.low), ToTimeFormat(ctrl.high) );
|
|
Effect::MessageBox(
|
|
message,
|
|
wxOK | wxCENTRE,
|
|
XO("Value Error") );
|
|
}
|
|
|
|
if (val < ctrl.low)
|
|
val = ctrl.low;
|
|
else if (val > ctrl.high)
|
|
val = ctrl.high;
|
|
|
|
n->SetValue(val);
|
|
value = val;
|
|
}
|
|
}
|
|
|
|
void NyquistEffect::OnFileButton(wxCommandEvent& evt)
|
|
{
|
|
int i = evt.GetId() - ID_FILE;
|
|
NyqControl & ctrl = mControls[i];
|
|
|
|
// Get style flags:
|
|
// Ensure legal combinations so that wxWidgets does not throw an assert error.
|
|
unsigned int flags = 0;
|
|
if (!ctrl.highStr.empty())
|
|
{
|
|
wxStringTokenizer tokenizer(ctrl.highStr, ",");
|
|
while ( tokenizer.HasMoreTokens() )
|
|
{
|
|
wxString token = tokenizer.GetNextToken().Trim(true).Trim(false);
|
|
if (token.IsSameAs("open", false))
|
|
{
|
|
flags |= wxFD_OPEN;
|
|
flags &= ~wxFD_SAVE;
|
|
flags &= ~wxFD_OVERWRITE_PROMPT;
|
|
}
|
|
else if (token.IsSameAs("save", false))
|
|
{
|
|
flags |= wxFD_SAVE;
|
|
flags &= ~wxFD_OPEN;
|
|
flags &= ~wxFD_MULTIPLE;
|
|
flags &= ~wxFD_FILE_MUST_EXIST;
|
|
}
|
|
else if (token.IsSameAs("overwrite", false) && !(flags & wxFD_OPEN))
|
|
{
|
|
flags |= wxFD_OVERWRITE_PROMPT;
|
|
}
|
|
else if (token.IsSameAs("exists", false) && !(flags & wxFD_SAVE))
|
|
{
|
|
flags |= wxFD_FILE_MUST_EXIST;
|
|
}
|
|
else if (token.IsSameAs("multiple", false) && !(flags & wxFD_SAVE))
|
|
{
|
|
flags |= wxFD_MULTIPLE;
|
|
}
|
|
}
|
|
}
|
|
|
|
resolveFilePath(ctrl.valStr);
|
|
|
|
wxFileName fname = ctrl.valStr;
|
|
wxString defaultDir = fname.GetPath();
|
|
wxString defaultFile = fname.GetName();
|
|
auto message = XO("Select a file");
|
|
|
|
if (flags & wxFD_MULTIPLE)
|
|
message = XO("Select one or more files");
|
|
else if (flags & wxFD_SAVE)
|
|
message = XO("Save file as");
|
|
|
|
FileDialogWrapper openFileDialog(mUIParent->FindWindow(ID_FILE + i),
|
|
message,
|
|
defaultDir,
|
|
defaultFile,
|
|
ctrl.fileTypes,
|
|
flags); // styles
|
|
|
|
if (openFileDialog.ShowModal() == wxID_CANCEL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
wxString path;
|
|
// When multiple files selected, return file paths as a list of quoted strings.
|
|
if (flags & wxFD_MULTIPLE)
|
|
{
|
|
wxArrayString selectedFiles;
|
|
openFileDialog.GetPaths(selectedFiles);
|
|
|
|
for (size_t sf = 0; sf < selectedFiles.size(); sf++) {
|
|
path += "\"";
|
|
path += selectedFiles[sf];
|
|
path += "\"";
|
|
}
|
|
ctrl.valStr = path;
|
|
}
|
|
else
|
|
{
|
|
ctrl.valStr = openFileDialog.GetPath();
|
|
}
|
|
|
|
mUIParent->FindWindow(ID_Text + i)->GetValidator()->TransferToWindow();
|
|
}
|
|
|
|
void NyquistEffect::resolveFilePath(wxString& path, FileExtension extension /* empty string */)
|
|
{
|
|
#if defined(__WXMSW__)
|
|
path.Replace("/", wxFileName::GetPathSeparator());
|
|
#endif
|
|
|
|
path.Trim(true).Trim(false);
|
|
|
|
typedef std::unordered_map<wxString, FilePath> map;
|
|
map pathKeys = {
|
|
{"*home*", wxGetHomeDir()},
|
|
{"~", wxGetHomeDir()},
|
|
{"*default*", FileNames::DefaultToDocumentsFolder("").GetPath()},
|
|
{"*export*", FileNames::DefaultToDocumentsFolder(wxT("/Export/Path")).GetPath()},
|
|
{"*save*", FileNames::DefaultToDocumentsFolder(wxT("/SaveAs/Path")).GetPath()},
|
|
{"*config*", FileNames::DataDir()}
|
|
};
|
|
|
|
int characters = path.Find(wxFileName::GetPathSeparator());
|
|
if(characters == wxNOT_FOUND) // Just a path or just a file name
|
|
{
|
|
if (path.empty())
|
|
path = "*default*";
|
|
|
|
if (pathKeys.find(path) != pathKeys.end())
|
|
{
|
|
// Keyword found, so assume this is the intended directory.
|
|
path = pathKeys[path] + wxFileName::GetPathSeparator();
|
|
}
|
|
else // Just a file name
|
|
{
|
|
path = pathKeys["*default*"] + wxFileName::GetPathSeparator() + path;
|
|
}
|
|
}
|
|
else // path + file name
|
|
{
|
|
wxString firstDir = path.Left(characters);
|
|
wxString rest = path.Mid(characters);
|
|
|
|
if (pathKeys.find(firstDir) != pathKeys.end())
|
|
{
|
|
path = pathKeys[firstDir] + rest;
|
|
}
|
|
}
|
|
|
|
wxFileName fname = path;
|
|
|
|
// If the directory is invalid, better to leave it as is (invalid) so that
|
|
// the user sees the error rather than an unexpected file path.
|
|
if (fname.wxFileName::IsOk() && fname.GetFullName().empty())
|
|
{
|
|
path = fname.GetPathWithSep() + _("untitled");
|
|
if (!extension.empty())
|
|
path = path + '.' + extension;
|
|
}
|
|
}
|
|
|
|
|
|
bool NyquistEffect::validatePath(wxString path)
|
|
{
|
|
wxFileName fname = path;
|
|
wxString dir = fname.GetPath();
|
|
|
|
return (fname.wxFileName::IsOk() &&
|
|
wxFileName::DirExists(dir) &&
|
|
!fname.GetFullName().empty());
|
|
}
|
|
|
|
|
|
wxString NyquistEffect::ToTimeFormat(double t)
|
|
{
|
|
int seconds = static_cast<int>(t);
|
|
int hh = seconds / 3600;
|
|
int mm = seconds % 3600;
|
|
mm = mm / 60;
|
|
return wxString::Format("%d:%d:%.3f", hh, mm, t - (hh * 3600 + mm * 60));
|
|
}
|
|
|
|
|
|
void NyquistEffect::OnText(wxCommandEvent & evt)
|
|
{
|
|
int i = evt.GetId() - ID_Text;
|
|
|
|
NyqControl & ctrl = mControls[i];
|
|
|
|
if (wxDynamicCast(evt.GetEventObject(), wxWindow)->GetValidator()->TransferFromWindow())
|
|
{
|
|
if (ctrl.type == NYQ_CTRL_FLOAT || ctrl.type == NYQ_CTRL_INT)
|
|
{
|
|
int pos = (int)floor((ctrl.val - ctrl.low) /
|
|
(ctrl.high - ctrl.low) * ctrl.ticks + 0.5);
|
|
|
|
wxSlider *slider = (wxSlider *)mUIParent->FindWindow(ID_Slider + i);
|
|
slider->SetValue(pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// NyquistOutputDialog
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
BEGIN_EVENT_TABLE(NyquistOutputDialog, wxDialogWrapper)
|
|
EVT_BUTTON(wxID_OK, NyquistOutputDialog::OnOk)
|
|
END_EVENT_TABLE()
|
|
|
|
NyquistOutputDialog::NyquistOutputDialog(wxWindow * parent, wxWindowID id,
|
|
const TranslatableString & title,
|
|
const TranslatableString & prompt,
|
|
const TranslatableString &message)
|
|
: wxDialogWrapper{ parent, id, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER }
|
|
{
|
|
SetName();
|
|
|
|
ShuttleGui S{ this, eIsCreating };
|
|
{
|
|
S.SetBorder(10);
|
|
|
|
S.AddVariableText( prompt, false, wxALIGN_LEFT | wxLEFT | wxTOP | wxRIGHT );
|
|
|
|
// TODO: use ShowInfoDialog() instead.
|
|
// Beware this dialog MUST work with screen readers.
|
|
S.Prop( 1 )
|
|
.Position(wxEXPAND | wxALL)
|
|
.MinSize( { 480, 250 } )
|
|
.Style(wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH)
|
|
.AddTextWindow( message.Translation() );
|
|
|
|
S.SetBorder( 5 );
|
|
|
|
S.StartHorizontalLay(wxALIGN_CENTRE | wxLEFT | wxBOTTOM | wxRIGHT, 0 );
|
|
{
|
|
/* i18n-hint: In most languages OK is to be translated as OK. It appears on a button.*/
|
|
S.Id(wxID_OK).AddButton( XXO("OK"), wxALIGN_CENTRE, true );
|
|
}
|
|
S.EndHorizontalLay();
|
|
|
|
}
|
|
|
|
SetAutoLayout(true);
|
|
GetSizer()->Fit(this);
|
|
GetSizer()->SetSizeHints(this);
|
|
}
|
|
|
|
// ============================================================================
|
|
// NyquistOutputDialog implementation
|
|
// ============================================================================
|
|
|
|
void NyquistOutputDialog::OnOk(wxCommandEvent & /* event */)
|
|
{
|
|
EndModal(wxID_OK);
|
|
}
|
|
|
|
// Registration of extra functions in XLisp.
|
|
#include "../../../lib-src/libnyquist/nyquist/xlisp/xlisp.h"
|
|
|
|
static LVAL gettext()
|
|
{
|
|
auto string = UTF8CTOWX(getstring(xlgastring()));
|
|
#if !HAS_I18N_CONTEXTS
|
|
// allow ignored context argument
|
|
if ( moreargs() )
|
|
nextarg();
|
|
#endif
|
|
xllastarg();
|
|
return cvstring(GetCustomTranslation(string).mb_str(wxConvUTF8));
|
|
}
|
|
|
|
static LVAL gettextc()
|
|
{
|
|
#if HAS_I18N_CONTEXTS
|
|
auto string = UTF8CTOWX(getstring(xlgastring()));
|
|
auto context = UTF8CTOWX(getstring(xlgastring()));
|
|
xllastarg();
|
|
return cvstring(wxGetTranslation( string, "", 0, "", context )
|
|
.mb_str(wxConvUTF8));
|
|
#else
|
|
return gettext();
|
|
#endif
|
|
}
|
|
|
|
static LVAL ngettext()
|
|
{
|
|
auto string1 = UTF8CTOWX(getstring(xlgastring()));
|
|
auto string2 = UTF8CTOWX(getstring(xlgastring()));
|
|
auto number = getfixnum(xlgafixnum());
|
|
#if !HAS_I18N_CONTEXTS
|
|
// allow ignored context argument
|
|
if ( moreargs() )
|
|
nextarg();
|
|
#endif
|
|
xllastarg();
|
|
return cvstring(
|
|
wxGetTranslation(string1, string2, number).mb_str(wxConvUTF8));
|
|
}
|
|
|
|
static LVAL ngettextc()
|
|
{
|
|
#if HAS_I18N_CONTEXTS
|
|
auto string1 = UTF8CTOWX(getstring(xlgastring()));
|
|
auto string2 = UTF8CTOWX(getstring(xlgastring()));
|
|
auto number = getfixnum(xlgafixnum());
|
|
auto context = UTF8CTOWX(getstring(xlgastring()));
|
|
xllastarg();
|
|
return cvstring(wxGetTranslation( string1, string2, number, "", context )
|
|
.mb_str(wxConvUTF8));
|
|
#else
|
|
return ngettext();
|
|
#endif
|
|
}
|
|
|
|
void * nyq_make_opaque_string( int size, unsigned char *src ){
|
|
LVAL dst;
|
|
unsigned char * dstp;
|
|
dst = new_string((int)(size+2));
|
|
dstp = getstring(dst);
|
|
|
|
/* copy the source to the destination */
|
|
while (size-- > 0)
|
|
*dstp++ = *src++;
|
|
*dstp = '\0';
|
|
|
|
return (void*)dst;
|
|
}
|
|
|
|
void * nyq_reformat_aud_do_response(const wxString & Str) {
|
|
LVAL dst;
|
|
LVAL message;
|
|
LVAL success;
|
|
wxString Left = Str.BeforeLast('\n').BeforeLast('\n').ToAscii();
|
|
wxString Right = Str.BeforeLast('\n').AfterLast('\n').ToAscii();
|
|
message = cvstring(Left);
|
|
success = Right.EndsWith("OK") ? s_true : nullptr;
|
|
dst = cons(message, success);
|
|
return (void *)dst;
|
|
}
|
|
|
|
#include "../../commands/ScriptCommandRelay.h"
|
|
|
|
|
|
/* xlc_aud_do -- interface to C routine aud_do */
|
|
/**/
|
|
LVAL xlc_aud_do(void)
|
|
{
|
|
// Based on string-trim...
|
|
unsigned char *leftp;
|
|
LVAL src,dst;
|
|
|
|
/* get the string */
|
|
src = xlgastring();
|
|
xllastarg();
|
|
|
|
/* setup the string pointer */
|
|
leftp = getstring(src);
|
|
|
|
// Go call my real function here...
|
|
dst = (LVAL)ExecForLisp( (char *)leftp );
|
|
|
|
//dst = cons(dst, (LVAL)1);
|
|
/* return the new string */
|
|
return (dst);
|
|
}
|
|
|
|
static void RegisterFunctions()
|
|
{
|
|
// Add functions to XLisp. Do this only once,
|
|
// before the first call to nyx_init.
|
|
static bool firstTime = true;
|
|
if (firstTime) {
|
|
firstTime = false;
|
|
|
|
// All function names must be UP-CASED
|
|
static const FUNDEF functions[] = {
|
|
{ "_", SUBR, gettext },
|
|
{ "_C", SUBR, gettextc },
|
|
{ "NGETTEXT", SUBR, ngettext },
|
|
{ "NGETTEXTC", SUBR, ngettextc },
|
|
{ "AUD-DO", SUBR, xlc_aud_do },
|
|
};
|
|
|
|
xlbindfunctions( functions, WXSIZEOF( functions ) );
|
|
}
|
|
}
|