1 /**********************************************************************
2 
3   Audacity: A Digital Audio Editor
4 
5   Nyquist.cpp
6 
7   Dominic Mazzoni
8 
9 ******************************************************************//**
10 
11 \class NyquistEffect
12 \brief An Effect that calls up a Nyquist (XLISP) plug-in, i.e. many possible
13 effects from this one class.
14 
15 *//****************************************************************//**
16 
17 \class NyquistOutputDialog
18 \brief Dialog used with NyquistEffect
19 
20 *//****************************************************************//**
21 
22 \class NyqControl
23 \brief A control on a NyquistDialog.
24 
25 *//*******************************************************************/
26 
27 
28 #include "Nyquist.h"
29 
30 #include <algorithm>
31 #include <cmath>
32 #include <cstring>
33 
34 #include <locale.h>
35 
36 #include <wx/button.h>
37 #include <wx/checkbox.h>
38 #include <wx/choice.h>
39 #include <wx/datetime.h>
40 #include <wx/intl.h>
41 #include <wx/log.h>
42 #include <wx/scrolwin.h>
43 #include <wx/sizer.h>
44 #include <wx/slider.h>
45 #include <wx/sstream.h>
46 #include <wx/stattext.h>
47 #include <wx/textdlg.h>
48 #include <wx/tokenzr.h>
49 #include <wx/txtstrm.h>
50 #include <wx/valgen.h>
51 #include <wx/wfstream.h>
52 #include <wx/numformatter.h>
53 #include <wx/stdpaths.h>
54 
55 #include "../EffectManager.h"
56 #include "FileNames.h"
57 #include "../../LabelTrack.h"
58 #include "Languages.h"
59 #include "../../NoteTrack.h"
60 #include "../../TimeTrack.h"
61 #include "../../prefs/SpectrogramSettings.h"
62 #include "../../PluginManager.h"
63 #include "Project.h"
64 #include "ProjectRate.h"
65 #include "../../ShuttleGetDefinition.h"
66 #include "../../ShuttleGui.h"
67 #include "TempDirectory.h"
68 #include "ViewInfo.h"
69 #include "../../WaveClip.h"
70 #include "../../WaveTrack.h"
71 #include "../../widgets/valnum.h"
72 #include "../../widgets/AudacityMessageBox.h"
73 #include "Prefs.h"
74 #include "wxFileNameWrapper.h"
75 #include "../../prefs/GUIPrefs.h"
76 #include "../../tracks/playabletrack/wavetrack/ui/WaveTrackView.h"
77 #include "../../tracks/playabletrack/wavetrack/ui/WaveTrackViewConstants.h"
78 #include "../../widgets/NumericTextCtrl.h"
79 #include "../../widgets/ProgressDialog.h"
80 
81 #include "../../widgets/FileDialog/FileDialog.h"
82 
83 #ifndef nyx_returns_start_and_end_time
84 #error You need to update lib-src/libnyquist
85 #endif
86 
87 #include <locale.h>
88 #include <iostream>
89 #include <ostream>
90 #include <sstream>
91 #include <float.h>
92 
93 int NyquistEffect::mReentryCount = 0;
94 
95 enum
96 {
97    ID_Editor = 10000,
98    ID_Load,
99    ID_Save,
100 
101    ID_Slider = 11000,
102    ID_Text = 12000,
103    ID_Choice = 13000,
104    ID_Time = 14000,
105    ID_FILE = 15000
106 };
107 
108 // Protect Nyquist from selections greater than 2^31 samples (bug 439)
109 #define NYQ_MAX_LEN (std::numeric_limits<long>::max())
110 
111 #define UNINITIALIZED_CONTROL ((double)99999999.99)
112 
113 static const wxChar *KEY_Command = wxT("Command");
114 static const wxChar *KEY_Parameters = wxT("Parameters");
115 
116 ///////////////////////////////////////////////////////////////////////////////
117 //
118 // NyquistEffect
119 //
120 ///////////////////////////////////////////////////////////////////////////////
121 
BEGIN_EVENT_TABLE(NyquistEffect,wxEvtHandler)122 BEGIN_EVENT_TABLE(NyquistEffect, wxEvtHandler)
123    EVT_BUTTON(ID_Load, NyquistEffect::OnLoad)
124    EVT_BUTTON(ID_Save, NyquistEffect::OnSave)
125 
126    EVT_COMMAND_RANGE(ID_Slider, ID_Slider+99,
127                      wxEVT_COMMAND_SLIDER_UPDATED, NyquistEffect::OnSlider)
128    EVT_COMMAND_RANGE(ID_Text, ID_Text+99,
129                      wxEVT_COMMAND_TEXT_UPDATED, NyquistEffect::OnText)
130    EVT_COMMAND_RANGE(ID_Choice, ID_Choice + 99,
131                      wxEVT_COMMAND_CHOICE_SELECTED, NyquistEffect::OnChoice)
132    EVT_COMMAND_RANGE(ID_Time, ID_Time + 99,
133                      wxEVT_COMMAND_TEXT_UPDATED, NyquistEffect::OnTime)
134    EVT_COMMAND_RANGE(ID_FILE, ID_FILE + 99,
135                      wxEVT_COMMAND_BUTTON_CLICKED, NyquistEffect::OnFileButton)
136 END_EVENT_TABLE()
137 
138 NyquistEffect::NyquistEffect(const wxString &fName)
139 {
140    mOutputTrack[0] = mOutputTrack[1] = nullptr;
141 
142    mAction = XO("Applying Nyquist Effect...");
143    mIsPrompt = false;
144    mExternal = false;
145    mCompiler = false;
146    mTrace = false;
147    mRedirectOutput = false;
148    mDebug = false;
149    mIsSal = false;
150    mOK = false;
151    mAuthor = XO("n/a");
152    mReleaseVersion = XO("n/a");
153    mCopyright = XO("n/a");
154 
155    // set clip/split handling when applying over clip boundary.
156    mRestoreSplits = true;  // Default: Restore split lines.
157    mMergeClips = -1;       // Default (auto):  Merge if length remains unchanged.
158 
159    mVersion = 4;
160 
161    mStop = false;
162    mBreak = false;
163    mCont = false;
164    mIsTool = false;
165 
166    mMaxLen = NYQ_MAX_LEN;
167 
168    // Interactive Nyquist
169    if (fName == NYQUIST_PROMPT_ID) {
170       mName = NYQUIST_PROMPT_NAME;
171       mType = EffectTypeTool;
172       mIsTool = true;
173       mPromptName = mName;
174       mPromptType = mType;
175       mOK = true;
176       mIsPrompt = true;
177       return;
178    }
179 
180    if (fName == NYQUIST_WORKER_ID) {
181       // Effect spawned from Nyquist Prompt
182 /* i18n-hint: It is acceptable to translate this the same as for "Nyquist Prompt" */
183       mName = XO("Nyquist Worker");
184       return;
185    }
186 
187    mFileName = fName;
188    // Use the file name verbatim as effect name.
189    // This is only a default name, overridden if we find a $name line:
190    mName = Verbatim( mFileName.GetName() );
191    mFileModified = mFileName.GetModificationTime();
192    ParseFile();
193 
194    if (!mOK && mInitError.empty())
195       mInitError = XO("Ill-formed Nyquist plug-in header");
196 }
197 
~NyquistEffect()198 NyquistEffect::~NyquistEffect()
199 {
200 }
201 
202 // ComponentInterface implementation
203 
GetPath()204 PluginPath NyquistEffect::GetPath()
205 {
206    if (mIsPrompt)
207       return NYQUIST_PROMPT_ID;
208 
209    return mFileName.GetFullPath();
210 }
211 
GetSymbol()212 ComponentInterfaceSymbol NyquistEffect::GetSymbol()
213 {
214    if (mIsPrompt)
215       return { NYQUIST_PROMPT_ID, NYQUIST_PROMPT_NAME };
216 
217    return mName;
218 }
219 
GetVendor()220 VendorSymbol NyquistEffect::GetVendor()
221 {
222    if (mIsPrompt)
223    {
224       return XO("Audacity");
225    }
226 
227    return mAuthor;
228 }
229 
GetVersion()230 wxString NyquistEffect::GetVersion()
231 {
232    // Are Nyquist version strings really supposed to be translatable?
233    // See commit a06e561 which used XO for at least one of them
234    return mReleaseVersion.Translation();
235 }
236 
GetDescription()237 TranslatableString NyquistEffect::GetDescription()
238 {
239    return mCopyright;
240 }
241 
ManualPage()242 ManualPageID NyquistEffect::ManualPage()
243 {
244       return mIsPrompt
245          ? wxString("Nyquist_Prompt")
246          : mManPage;
247 }
248 
HelpPage()249 FilePath NyquistEffect::HelpPage()
250 {
251    auto paths = NyquistEffect::GetNyquistSearchPath();
252    wxString fileName;
253 
254    for (size_t i = 0, cnt = paths.size(); i < cnt; i++) {
255       fileName = wxFileName(paths[i] + wxT("/") + mHelpFile).GetFullPath();
256       if (wxFileExists(fileName)) {
257          mHelpFileExists = true;
258          return fileName;
259       }
260    }
261    return wxEmptyString;
262 }
263 
264 // EffectDefinitionInterface implementation
265 
GetType()266 EffectType NyquistEffect::GetType()
267 {
268    return mType;
269 }
270 
GetClassification()271 EffectType NyquistEffect::GetClassification()
272 {
273    if (mIsTool)
274       return EffectTypeTool;
275    return mType;
276 }
277 
GetFamily()278 EffectFamilySymbol NyquistEffect::GetFamily()
279 {
280    return NYQUISTEFFECTS_FAMILY;
281 }
282 
IsInteractive()283 bool NyquistEffect::IsInteractive()
284 {
285    if (mIsPrompt)
286    {
287       return true;
288    }
289 
290    return mControls.size() != 0;
291 }
292 
IsDefault()293 bool NyquistEffect::IsDefault()
294 {
295    return mIsPrompt;
296 }
297 
298 // EffectClientInterface implementation
DefineParams(ShuttleParams & S)299 bool NyquistEffect::DefineParams( ShuttleParams & S )
300 {
301    // For now we assume Nyquist can do get and set better than DefineParams can,
302    // And so we ONLY use it for getting the signature.
303    auto pGa = dynamic_cast<ShuttleGetAutomation*>(&S);
304    if( pGa ){
305       GetAutomationParameters( *(pGa->mpEap) );
306       return true;
307    }
308    auto pSa = dynamic_cast<ShuttleSetAutomation*>(&S);
309    if( pSa ){
310       SetAutomationParameters( *(pSa->mpEap) );
311       return true;
312    }
313    auto pSd  = dynamic_cast<ShuttleGetDefinition*>(&S);
314    if( pSd == nullptr )
315       return true;
316    //wxASSERT( pSd );
317 
318    if (mExternal)
319       return true;
320 
321    if (mIsPrompt)
322    {
323       S.Define( mInputCmd, KEY_Command, "" );
324       S.Define( mParameters, KEY_Parameters, "" );
325       return true;
326    }
327 
328    for (size_t c = 0, cnt = mControls.size(); c < cnt; c++)
329    {
330       NyqControl & ctrl = mControls[c];
331       double d = ctrl.val;
332 
333       if (d == UNINITIALIZED_CONTROL && ctrl.type != NYQ_CTRL_STRING)
334       {
335          d = GetCtrlValue(ctrl.valStr);
336       }
337 
338       if (ctrl.type == NYQ_CTRL_FLOAT || ctrl.type == NYQ_CTRL_FLOAT_TEXT ||
339           ctrl.type == NYQ_CTRL_TIME)
340       {
341          S.Define( d, static_cast<const wxChar*>( ctrl.var.c_str() ), (double)0.0, ctrl.low, ctrl.high, 1.0);
342       }
343       else if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_INT_TEXT)
344       {
345          int x=d;
346          S.Define( x, static_cast<const wxChar*>( ctrl.var.c_str() ), 0, ctrl.low, ctrl.high, 1);
347          //parms.Write(ctrl.var, (int) d);
348       }
349       else if (ctrl.type == NYQ_CTRL_CHOICE)
350       {
351          // untranslated
352          int x=d;
353          //parms.WriteEnum(ctrl.var, (int) d, choices);
354          S.DefineEnum( x, static_cast<const wxChar*>( ctrl.var.c_str() ), 0,
355                        ctrl.choices.data(), ctrl.choices.size() );
356       }
357       else if (ctrl.type == NYQ_CTRL_STRING || ctrl.type == NYQ_CTRL_FILE)
358       {
359          S.Define( ctrl.valStr, ctrl.var, "" , ctrl.lowStr, ctrl.highStr );
360          //parms.Write(ctrl.var, ctrl.valStr);
361       }
362    }
363    return true;
364 }
365 
GetAutomationParameters(CommandParameters & parms)366 bool NyquistEffect::GetAutomationParameters(CommandParameters & parms)
367 {
368    if (mIsPrompt)
369    {
370       parms.Write(KEY_Command, mInputCmd);
371       parms.Write(KEY_Parameters, mParameters);
372 
373       return true;
374    }
375 
376    for (size_t c = 0, cnt = mControls.size(); c < cnt; c++)
377    {
378       NyqControl & ctrl = mControls[c];
379       double d = ctrl.val;
380 
381       if (d == UNINITIALIZED_CONTROL && ctrl.type != NYQ_CTRL_STRING)
382       {
383          d = GetCtrlValue(ctrl.valStr);
384       }
385 
386       if (ctrl.type == NYQ_CTRL_FLOAT || ctrl.type == NYQ_CTRL_FLOAT_TEXT ||
387           ctrl.type == NYQ_CTRL_TIME)
388       {
389          parms.Write(ctrl.var, d);
390       }
391       else if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_INT_TEXT)
392       {
393          parms.Write(ctrl.var, (int) d);
394       }
395       else if (ctrl.type == NYQ_CTRL_CHOICE)
396       {
397          // untranslated
398          parms.WriteEnum(ctrl.var, (int) d,
399                          ctrl.choices.data(), ctrl.choices.size());
400       }
401       else if (ctrl.type == NYQ_CTRL_STRING)
402       {
403          parms.Write(ctrl.var, ctrl.valStr);
404       }
405       else if (ctrl.type == NYQ_CTRL_FILE)
406       {
407          resolveFilePath(ctrl.valStr);
408          parms.Write(ctrl.var, ctrl.valStr);
409       }
410    }
411 
412    return true;
413 }
414 
SetAutomationParameters(CommandParameters & parms)415 bool NyquistEffect::SetAutomationParameters(CommandParameters & parms)
416 {
417    if (mIsPrompt)
418    {
419       parms.Read(KEY_Command, &mInputCmd, wxEmptyString);
420       parms.Read(KEY_Parameters, &mParameters, wxEmptyString);
421 
422       if (!mInputCmd.empty())
423       {
424          ParseCommand(mInputCmd);
425       }
426 
427       if (!mParameters.empty())
428       {
429          parms.SetParameters(mParameters);
430       }
431 
432       if (!IsBatchProcessing())
433       {
434          mType = EffectTypeTool;
435       }
436 
437       mPromptType = mType;
438       mIsTool = (mPromptType == EffectTypeTool);
439       mExternal = true;
440 
441       if (!IsBatchProcessing())
442       {
443          return true;
444       }
445    }
446 
447    // Constants to document what the true/false values mean.
448    const auto kTestOnly = true;
449    const auto kTestAndSet = false;
450 
451    // badCount will encompass both actual bad values and missing values.
452    // We probably never actually have bad values when using the dialogs
453    // since the dialog validation will catch them.
454    int badCount;
455    // When batch processing, we just ignore missing/bad parameters.
456    // We'll end up using defaults in those cases.
457    if (!IsBatchProcessing()) {
458       badCount = SetLispVarsFromParameters(parms, kTestOnly);
459       if (badCount > 0)
460          return false;
461    }
462 
463    badCount = SetLispVarsFromParameters(parms, kTestAndSet);
464    // We never do anything with badCount here.
465    // It might be non zero, for missing parameters, and we allow that,
466    // and don't distinguish that from an out-of-range value.
467    return true;
468 }
469 
470 // Sets the lisp variables form the parameters.
471 // returns the number of bad settings.
472 // We can run this just testing for bad values, or actually setting when
473 // the values are good.
SetLispVarsFromParameters(CommandParameters & parms,bool bTestOnly)474 int NyquistEffect::SetLispVarsFromParameters(CommandParameters & parms, bool bTestOnly)
475 {
476    int badCount = 0;
477    // First pass verifies values
478    for (size_t c = 0, cnt = mControls.size(); c < cnt; c++)
479    {
480       NyqControl & ctrl = mControls[c];
481       bool good = false;
482 
483       // This GetCtrlValue code is preserved from former code,
484       // but probably is pointless.  The value d isn't used later,
485       // and GetCtrlValue does not appear to have important needed
486       // side effects.
487       if (!bTestOnly) {
488          double d = ctrl.val;
489          if (d == UNINITIALIZED_CONTROL && ctrl.type != NYQ_CTRL_STRING)
490          {
491             d = GetCtrlValue(ctrl.valStr);
492          }
493       }
494 
495       if (ctrl.type == NYQ_CTRL_FLOAT || ctrl.type == NYQ_CTRL_FLOAT_TEXT ||
496          ctrl.type == NYQ_CTRL_TIME)
497       {
498          double val;
499          good = parms.Read(ctrl.var, &val) &&
500             val >= ctrl.low &&
501             val <= ctrl.high;
502          if (good && !bTestOnly)
503             ctrl.val = val;
504       }
505       else if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_INT_TEXT)
506       {
507          int val;
508          good = parms.Read(ctrl.var, &val) &&
509             val >= ctrl.low &&
510             val <= ctrl.high;
511          if (good && !bTestOnly)
512             ctrl.val = (double)val;
513       }
514       else if (ctrl.type == NYQ_CTRL_CHOICE)
515       {
516          int val;
517          // untranslated
518          good = parms.ReadEnum(ctrl.var, &val,
519             ctrl.choices.data(), ctrl.choices.size()) &&
520             val != wxNOT_FOUND;
521          if (good && !bTestOnly)
522             ctrl.val = (double)val;
523       }
524       else if (ctrl.type == NYQ_CTRL_STRING || ctrl.type == NYQ_CTRL_FILE)
525       {
526          wxString val;
527          good = parms.Read(ctrl.var, &val);
528          if (good && !bTestOnly)
529             ctrl.valStr = val;
530       }
531       else if (ctrl.type == NYQ_CTRL_TEXT)
532       {
533          // This "control" is just fixed text (nothing to save or restore),
534          // Does not count for good/bad counting.
535          good = true;
536       }
537       badCount += !good ? 1 : 0;
538    }
539    return badCount;
540 }
541 
542 // Effect Implementation
Init()543 bool NyquistEffect::Init()
544 {
545    // When Nyquist Prompt spawns an effect GUI, Init() is called for Nyquist Prompt,
546    // and then again for the spawned (mExternal) effect.
547 
548    // EffectType may not be defined in script, so
549    // reset each time we call the Nyquist Prompt.
550    if (mIsPrompt) {
551       mName = mPromptName;
552       // Reset effect type each time we call the Nyquist Prompt.
553       mType = mPromptType;
554       mIsSpectral = false;
555       mDebugButton = true;    // Debug button always enabled for Nyquist Prompt.
556       mEnablePreview = true;  // Preview button always enabled for Nyquist Prompt.
557       mVersion = 4;
558    }
559 
560    // As of Audacity 2.1.2 rc1, 'spectral' effects are allowed only if
561    // the selected track(s) are in a spectrogram view, and there is at
562    // least one frequency bound and Spectral Selection is enabled for the
563    // selected track(s) - (but don't apply to Nyquist Prompt).
564 
565    if (!mIsPrompt && mIsSpectral) {
566       auto *project = FindProject();
567       bool bAllowSpectralEditing = false;
568       bool hasSpectral = false;
569 
570       for ( auto t :
571                TrackList::Get( *project ).Selected< const WaveTrack >() ) {
572          const auto displays = WaveTrackView::Get(*t).GetDisplays();
573          if (displays.end() != std::find(
574             displays.begin(), displays.end(),
575             WaveTrackSubView::Type{ WaveTrackViewConstants::Spectrum, {} }))
576             hasSpectral = true;
577          if ( hasSpectral &&
578              (t->GetSpectrogramSettings().SpectralSelectionEnabled())) {
579             bAllowSpectralEditing = true;
580             break;
581          }
582       }
583 
584       if (!bAllowSpectralEditing || ((mF0 < 0.0) && (mF1 < 0.0))) {
585          if (!hasSpectral) {
586             Effect::MessageBox(
587             XO("Enable track spectrogram view before\n"
588             "applying 'Spectral' effects."),
589             wxOK | wxICON_EXCLAMATION | wxCENTRE,
590             XO("Error") );
591          } else {
592             Effect::MessageBox(
593                XO("To use 'Spectral effects', enable 'Spectral Selection'\n"
594                            "in the track Spectrogram settings and select the\n"
595                            "frequency range for the effect to act on."),
596                wxOK | wxICON_EXCLAMATION | wxCENTRE,
597                XO("Error") );
598          }
599          return false;
600       }
601    }
602 
603    if (!mIsPrompt && !mExternal)
604    {
605       //TODO: (bugs):
606       // 1) If there is more than one plug-in with the same name, GetModificationTime may pick the wrong one.
607       // 2) If the ;type is changed after the effect has been registered, the plug-in will appear in the wrong menu.
608 
609       //TODO: If we want to auto-add parameters from spectral selection,
610       //we will need to modify this test.
611       //Note that removing it stops the caching of parameter values,
612       //(during this session).
613       if (mFileName.GetModificationTime().IsLaterThan(mFileModified))
614       {
615          SaveUserPreset(GetCurrentSettingsGroup());
616 
617          mMaxLen = NYQ_MAX_LEN;
618          ParseFile();
619          mFileModified = mFileName.GetModificationTime();
620 
621          LoadUserPreset(GetCurrentSettingsGroup());
622       }
623    }
624 
625    return true;
626 }
627 
CheckWhetherSkipEffect()628 bool NyquistEffect::CheckWhetherSkipEffect()
629 {
630    // If we're a prompt and we have controls, then we've already processed
631    // the audio, so skip further processing.
632    return (mIsPrompt && mControls.size() > 0 && !IsBatchProcessing());
633 }
634 
635 static void RegisterFunctions();
636 
Process()637 bool NyquistEffect::Process()
638 {
639    // Check for reentrant Nyquist commands.
640    // I'm choosing to mark skipped Nyquist commands as successful even though
641    // they are skipped.  The reason is that when Nyquist calls out to a chain,
642    // and that chain contains Nyquist,  it will be clearer if the chain completes
643    // skipping Nyquist, rather than doing nothing at all.
644    if( mReentryCount > 0 )
645       return true;
646 
647    // Restore the reentry counter (to zero) when we exit.
648    auto countRestorer = valueRestorer( mReentryCount);
649    mReentryCount++;
650    RegisterFunctions();
651 
652    bool success = true;
653    int nEffectsSoFar = nEffectsDone;
654    mProjectChanged = false;
655    EffectManager & em = EffectManager::Get();
656    em.SetSkipStateFlag(false);
657 
658    // This code was added in a fix for bug 2392 (no preview for Nyquist)
659    // It was commented out in a fix for bug 2428 (no progress dialog from a macro)
660    //if (mExternal) {
661    //  mProgress->Hide();
662    //}
663 
664    mOutputTime = 0;
665    mCount = 0;
666    mProgressIn = 0;
667    mProgressOut = 0;
668    mProgressTot = 0;
669    mScale = (GetType() == EffectTypeProcess ? 0.5 : 1.0) / GetNumWaveGroups();
670 
671    mStop = false;
672    mBreak = false;
673    mCont = false;
674 
675    mTrackIndex = 0;
676 
677    // If in tool mode, then we don't do anything with the track and selection.
678    const bool bOnePassTool = (GetType() == EffectTypeTool);
679 
680    // We must copy all the tracks, because Paste needs label tracks to ensure
681    // correct sync-lock group behavior when the timeline is affected; then we just want
682    // to operate on the selected wave tracks
683    if ( !bOnePassTool )
684       CopyInputTracks(true);
685 
686    mNumSelectedChannels = bOnePassTool
687       ? 0
688       : mOutputTracks->Selected< const WaveTrack >().size();
689 
690    mDebugOutput = {};
691    if (!mHelpFile.empty() && !mHelpFileExists) {
692       mDebugOutput = XO(
693 "error: File \"%s\" specified in header but not found in plug-in path.\n")
694          .Format( mHelpFile );
695    }
696 
697    if (mVersion >= 4)
698    {
699       auto project = FindProject();
700 
701       mProps = wxEmptyString;
702 
703       mProps += wxString::Format(wxT("(putprop '*AUDACITY* (list %d %d %d) 'VERSION)\n"), AUDACITY_VERSION, AUDACITY_RELEASE, AUDACITY_REVISION);
704       wxString lang = gPrefs->Read(wxT("/Locale/Language"), wxT(""));
705       lang = (lang.empty())
706          ? Languages::GetSystemLanguageCode(FileNames::AudacityPathList())
707          : lang;
708       mProps += wxString::Format(wxT("(putprop '*AUDACITY* \"%s\" 'LANGUAGE)\n"), lang);
709 
710       mProps += wxString::Format(wxT("(setf *DECIMAL-SEPARATOR* #\\%c)\n"), wxNumberFormatter::GetDecimalSeparator());
711 
712       mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'BASE)\n"), EscapeString(FileNames::BaseDir()));
713       mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'DATA)\n"), EscapeString(FileNames::DataDir()));
714       mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'HELP)\n"), EscapeString(FileNames::HtmlHelpDir().RemoveLast()));
715       mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'TEMP)\n"), EscapeString(TempDirectory::TempDir()));
716       mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'SYS-TEMP)\n"), EscapeString(wxStandardPaths::Get().GetTempDir()));
717       mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'DOCUMENTS)\n"), EscapeString(wxStandardPaths::Get().GetDocumentsDir()));
718       mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'HOME)\n"), EscapeString(wxGetHomeDir()));
719 
720       auto paths = NyquistEffect::GetNyquistSearchPath();
721       wxString list;
722       for (size_t i = 0, cnt = paths.size(); i < cnt; i++)
723       {
724          list += wxT("\"") + EscapeString(paths[i]) + wxT("\" ");
725       }
726       list = list.RemoveLast();
727 
728       mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* (list %s) 'PLUGIN)\n"), list);
729       mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* (list %s) 'PLUG-IN)\n"), list);
730       mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'USER-PLUG-IN)\n"),
731                                  EscapeString(FileNames::PlugInDir()));
732 
733       // Date and time:
734       wxDateTime now = wxDateTime::Now();
735       int year = now.GetYear();
736       int doy = now.GetDayOfYear();
737       int dom = now.GetDay();
738       // enumerated constants
739       wxDateTime::Month month = now.GetMonth();
740       wxDateTime::WeekDay day = now.GetWeekDay();
741 
742       // Date/time as a list: year, day of year, hour, minute, seconds
743       mProps += wxString::Format(wxT("(setf *SYSTEM-TIME* (list %d %d %d %d %d))\n"),
744                                  year, doy, now.GetHour(), now.GetMinute(), now.GetSecond());
745 
746       mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'DATE)\n"), now.FormatDate());
747       mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'TIME)\n"), now.FormatTime());
748       mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'ISO-DATE)\n"), now.FormatISODate());
749       mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'ISO-TIME)\n"), now.FormatISOTime());
750       mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* %d 'YEAR)\n"), year);
751       mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* %d 'DAY)\n"), dom);   // day of month
752       mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* %d 'MONTH)\n"), month);
753       mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'MONTH-NAME)\n"), now.GetMonthName(month));
754       mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'DAY-NAME)\n"), now.GetWeekDayName(day));
755 
756       mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'PROJECTS)\n"),
757          (int) AllProjects{}.size());
758       mProps += wxString::Format(wxT("(putprop '*PROJECT* \"%s\" 'NAME)\n"), EscapeString(project->GetProjectName()));
759 
760       int numTracks = 0;
761       int numWave = 0;
762       int numLabel = 0;
763       int numMidi = 0;
764       int numTime = 0;
765       wxString waveTrackList;   // track positions of selected audio tracks.
766 
767       {
768          auto countRange = TrackList::Get( *project ).Leaders();
769          for (auto t : countRange) {
770             t->TypeSwitch( [&](const WaveTrack *) {
771                numWave++;
772                if (t->GetSelected())
773                   waveTrackList += wxString::Format(wxT("%d "), 1 + numTracks);
774             });
775             numTracks++;
776          }
777          numLabel = countRange.Filter<const LabelTrack>().size();
778    #if defined(USE_MIDI)
779          numMidi = countRange.Filter<const NoteTrack>().size();
780    #endif
781          numTime = countRange.Filter<const TimeTrack>().size();
782       }
783 
784       // We use Internat::ToString() rather than "%g" here because we
785       // always have to use the dot as decimal separator when giving
786       // numbers to Nyquist, whereas using "%g" will use the user's
787       // decimal separator which may be a comma in some countries.
788       mProps += wxString::Format(wxT("(putprop '*PROJECT* (float %s) 'RATE)\n"),
789          Internat::ToString(ProjectRate::Get(*project).GetRate()));
790       mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'TRACKS)\n"), numTracks);
791       mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'WAVETRACKS)\n"), numWave);
792       mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'LABELTRACKS)\n"), numLabel);
793       mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'MIDITRACKS)\n"), numMidi);
794       mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'TIMETRACKS)\n"), numTime);
795 
796       double previewLen = 6.0;
797       gPrefs->Read(wxT("/AudioIO/EffectsPreviewLen"), &previewLen);
798       mProps += wxString::Format(wxT("(putprop '*PROJECT* (float %s) 'PREVIEW-DURATION)\n"),
799                                  Internat::ToString(previewLen));
800 
801       // *PREVIEWP* is true when previewing (better than relying on track view).
802       wxString isPreviewing = (this->IsPreviewing())? wxT("T") : wxT("NIL");
803       mProps += wxString::Format(wxT("(setf *PREVIEWP* %s)\n"), isPreviewing);
804 
805       mProps += wxString::Format(wxT("(putprop '*SELECTION* (float %s) 'START)\n"),
806                                  Internat::ToString(mT0));
807       mProps += wxString::Format(wxT("(putprop '*SELECTION* (float %s) 'END)\n"),
808                                  Internat::ToString(mT1));
809       mProps += wxString::Format(wxT("(putprop '*SELECTION* (list %s) 'TRACKS)\n"), waveTrackList);
810       mProps += wxString::Format(wxT("(putprop '*SELECTION* %d 'CHANNELS)\n"), mNumSelectedChannels);
811    }
812 
813    // Nyquist Prompt does not require a selection, but effects do.
814    if (!bOnePassTool && (mNumSelectedChannels == 0)) {
815       auto message = XO("Audio selection required.");
816       Effect::MessageBox(
817          message,
818          wxOK | wxCENTRE | wxICON_EXCLAMATION,
819          XO("Nyquist Error") );
820    }
821 
822    Optional<TrackIterRange<WaveTrack>> pRange;
823    if (!bOnePassTool)
824       pRange.emplace(mOutputTracks->Selected< WaveTrack >() + &Track::IsLeader);
825 
826    // Keep track of whether the current track is first selected in its sync-lock group
827    // (we have no idea what the length of the returned audio will be, so we have
828    // to handle sync-lock group behavior the "old" way).
829    mFirstInGroup = true;
830    Track *gtLast = NULL;
831 
832    for (;
833         bOnePassTool || pRange->first != pRange->second;
834         (void) (!pRange || (++pRange->first, true))
835    ) {
836       // Prepare to accumulate more debug output in OutputCallback
837       mDebugOutputStr = mDebugOutput.Translation();
838       mDebugOutput = Verbatim( "%s" ).Format( std::cref( mDebugOutputStr ) );
839 
840       mCurTrack[0] = pRange ? *pRange->first : nullptr;
841       mCurNumChannels = 1;
842       if ( (mT1 >= mT0) || bOnePassTool ) {
843          if (bOnePassTool) {
844          }
845          else {
846             auto channels = TrackList::Channels(mCurTrack[0]);
847             if (channels.size() > 1) {
848                // TODO: more-than-two-channels
849                // Pay attention to consistency of mNumSelectedChannels
850                // with the running tally made by this loop!
851                mCurNumChannels = 2;
852 
853                mCurTrack[1] = * ++ channels.first;
854                if (mCurTrack[1]->GetRate() != mCurTrack[0]->GetRate()) {
855                   Effect::MessageBox(
856                      XO(
857 "Sorry, cannot apply effect on stereo tracks where the tracks don't match."),
858                      wxOK | wxCENTRE );
859                   success = false;
860                   goto finish;
861                }
862                mCurStart[1] = mCurTrack[1]->TimeToLongSamples(mT0);
863             }
864 
865             // Check whether we're in the same group as the last selected track
866             Track *gt = *TrackList::SyncLockGroup(mCurTrack[0]).first;
867             mFirstInGroup = !gtLast || (gtLast != gt);
868             gtLast = gt;
869 
870             mCurStart[0] = mCurTrack[0]->TimeToLongSamples(mT0);
871             auto end = mCurTrack[0]->TimeToLongSamples(mT1);
872             mCurLen = end - mCurStart[0];
873 
874             if (mCurLen > NYQ_MAX_LEN) {
875                float hours = (float)NYQ_MAX_LEN / (44100 * 60 * 60);
876                const auto message =
877                   XO(
878 "Selection too long for Nyquist code.\nMaximum allowed selection is %ld samples\n(about %.1f hours at 44100 Hz sample rate).")
879                      .Format((long)NYQ_MAX_LEN, hours);
880                Effect::MessageBox(
881                   message,
882                   wxOK | wxCENTRE,
883                   XO("Nyquist Error") );
884                if (!mProjectChanged)
885                   em.SetSkipStateFlag(true);
886                return false;
887             }
888 
889             mCurLen = std::min(mCurLen, mMaxLen);
890          }
891 
892          mProgressIn = 0.0;
893          mProgressOut = 0.0;
894 
895          // libnyquist breaks except in LC_NUMERIC=="C".
896          //
897          // Note that we must set the locale to "C" even before calling
898          // nyx_init() because otherwise some effects will not work!
899          //
900          // MB: setlocale is not thread-safe.  Should use uselocale()
901          //     if available, or fix libnyquist to be locale-independent.
902          // See also http://bugzilla.audacityteam.org/show_bug.cgi?id=642#c9
903          // for further info about this thread safety question.
904          wxString prevlocale = wxSetlocale(LC_NUMERIC, NULL);
905          wxSetlocale(LC_NUMERIC, wxString(wxT("C")));
906 
907          nyx_init();
908          nyx_set_os_callback(StaticOSCallback, (void *)this);
909          nyx_capture_output(StaticOutputCallback, (void *)this);
910 
911          auto cleanup = finally( [&] {
912             nyx_capture_output(NULL, (void *)NULL);
913             nyx_set_os_callback(NULL, (void *)NULL);
914             nyx_cleanup();
915          } );
916 
917 
918          if (mVersion >= 4)
919          {
920             mPerTrackProps = wxEmptyString;
921             wxString lowHz = wxT("nil");
922             wxString highHz = wxT("nil");
923             wxString centerHz = wxT("nil");
924             wxString bandwidth = wxT("nil");
925 
926 #if defined(EXPERIMENTAL_SPECTRAL_EDITING)
927             if (mF0 >= 0.0) {
928                lowHz.Printf(wxT("(float %s)"), Internat::ToString(mF0));
929             }
930 
931             if (mF1 >= 0.0) {
932                highHz.Printf(wxT("(float %s)"), Internat::ToString(mF1));
933             }
934 
935             if ((mF0 >= 0.0) && (mF1 >= 0.0)) {
936                centerHz.Printf(wxT("(float %s)"), Internat::ToString(sqrt(mF0 * mF1)));
937             }
938 
939             if ((mF0 > 0.0) && (mF1 >= mF0)) {
940                // with very small values, bandwidth calculation may be inf.
941                // (Observed on Linux)
942                double bw = log(mF1 / mF0) / log(2.0);
943                if (!std::isinf(bw)) {
944                   bandwidth.Printf(wxT("(float %s)"), Internat::ToString(bw));
945                }
946             }
947 
948 #endif
949             mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'LOW-HZ)\n"), lowHz);
950             mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'CENTER-HZ)\n"), centerHz);
951             mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'HIGH-HZ)\n"), highHz);
952             mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'BANDWIDTH)\n"), bandwidth);
953          }
954 
955          success = ProcessOne();
956 
957          // Reset previous locale
958          wxSetlocale(LC_NUMERIC, prevlocale);
959 
960          if (!success || bOnePassTool) {
961             goto finish;
962          }
963          mProgressTot += mProgressIn + mProgressOut;
964       }
965 
966       mCount += mCurNumChannels;
967    }
968 
969    if (mOutputTime > 0.0) {
970       mT1 = mT0 + mOutputTime;
971    }
972 
973 finish:
974 
975    // Show debug window if trace set in plug-in header and something to show.
976    mDebug = (mTrace && !mDebugOutput.Translation().empty())? true : mDebug;
977 
978    if (mDebug && !mRedirectOutput) {
979       NyquistOutputDialog dlog(mUIParent, -1,
980                                mName,
981                                XO("Debug Output: "),
982                                mDebugOutput);
983       dlog.CentreOnParent();
984       dlog.ShowModal();
985    }
986 
987    // Has rug been pulled from under us by some effect done within Nyquist??
988    if( !bOnePassTool && ( nEffectsSoFar == nEffectsDone ))
989       ReplaceProcessedTracks(success);
990    else{
991       ReplaceProcessedTracks(false); // Do not use the results.
992       // Selection is to be set to whatever it is in the project.
993       auto project = FindProject();
994       if (project) {
995          auto &selectedRegion = ViewInfo::Get( *project ).selectedRegion;
996          mT0 = selectedRegion.t0();
997          mT1 = selectedRegion.t1();
998       }
999       else {
1000          mT0 = 0;
1001          mT1 = -1;
1002       }
1003 
1004    }
1005 
1006    if (!mProjectChanged)
1007       em.SetSkipStateFlag(true);
1008 
1009    return success;
1010 }
1011 
ShowInterface(wxWindow & parent,const EffectDialogFactory & factory,bool forceModal)1012 bool NyquistEffect::ShowInterface(
1013    wxWindow &parent, const EffectDialogFactory &factory, bool forceModal)
1014 {
1015    bool res = true;
1016    if (!(Effect::TestUIFlags(EffectManager::kRepeatNyquistPrompt) && mIsPrompt)) {
1017       // Show the normal (prompt or effect) interface
1018       res = Effect::ShowInterface(parent, factory, forceModal);
1019    }
1020 
1021 
1022    // Remember if the user clicked debug
1023    mDebug = (mUIResultID == eDebugID);
1024 
1025    // We're done if the user clicked "Close", we are not the Nyquist Prompt,
1026    // or the program currently loaded into the prompt doesn't have a UI.
1027    if (!res || !mIsPrompt || mControls.size() == 0)
1028    {
1029       return res;
1030    }
1031 
1032    NyquistEffect effect(NYQUIST_WORKER_ID);
1033 
1034    if (IsBatchProcessing())
1035    {
1036       effect.SetBatchProcessing(true);
1037       effect.SetCommand(mInputCmd);
1038 
1039       CommandParameters cp;
1040       cp.SetParameters(mParameters);
1041       effect.SetAutomationParameters(cp);
1042 
1043       // Show the normal (prompt or effect) interface
1044       res = effect.ShowInterface(parent, factory, forceModal);
1045       if (res)
1046       {
1047          CommandParameters cp;
1048          effect.GetAutomationParameters(cp);
1049          cp.GetParameters(mParameters);
1050       }
1051    }
1052    else
1053    {
1054       effect.SetCommand(mInputCmd);
1055       effect.mDebug = (mUIResultID == eDebugID);
1056       res = Delegate(effect, parent, factory);
1057       mT0 = effect.mT0;
1058       mT1 = effect.mT1;
1059    }
1060 
1061    return res;
1062 }
1063 
PopulateOrExchange(ShuttleGui & S)1064 void NyquistEffect::PopulateOrExchange(ShuttleGui & S)
1065 {
1066    if (mIsPrompt)
1067    {
1068       BuildPromptWindow(S);
1069    }
1070    else
1071    {
1072       BuildEffectWindow(S);
1073    }
1074 
1075    EnableDebug(mDebugButton);
1076 }
1077 
TransferDataToWindow()1078 bool NyquistEffect::TransferDataToWindow()
1079 {
1080    mUIParent->TransferDataToWindow();
1081 
1082    bool success;
1083    if (mIsPrompt)
1084    {
1085       success = TransferDataToPromptWindow();
1086    }
1087    else
1088    {
1089       success = TransferDataToEffectWindow();
1090    }
1091 
1092    if (success)
1093    {
1094       EnablePreview(mEnablePreview);
1095    }
1096 
1097    return success;
1098 }
1099 
TransferDataFromWindow()1100 bool NyquistEffect::TransferDataFromWindow()
1101 {
1102    if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
1103    {
1104       return false;
1105    }
1106 
1107    if (mIsPrompt)
1108    {
1109       return TransferDataFromPromptWindow();
1110    }
1111    return TransferDataFromEffectWindow();
1112 }
1113 
1114 // NyquistEffect implementation
1115 
ProcessOne()1116 bool NyquistEffect::ProcessOne()
1117 {
1118    mpException = {};
1119 
1120    nyx_rval rval;
1121 
1122    wxString cmd;
1123    cmd += wxT("(snd-set-latency  0.1)");
1124 
1125    // A tool may be using AUD-DO which will potentially invalidate *TRACK*
1126    // so tools do not get *TRACK*.
1127    if (GetType() == EffectTypeTool)
1128       cmd += wxT("(setf S 0.25)\n");  // No Track.
1129    else if (mVersion >= 4) {
1130       nyx_set_audio_name("*TRACK*");
1131       cmd += wxT("(setf S 0.25)\n");
1132    }
1133    else {
1134       nyx_set_audio_name("S");
1135       cmd += wxT("(setf *TRACK* '*unbound*)\n");
1136    }
1137 
1138    if(mVersion >= 4) {
1139       cmd += mProps;
1140       cmd += mPerTrackProps;
1141    }
1142 
1143    if( (mVersion >= 4) && (GetType() != EffectTypeTool) ) {
1144       // Set the track TYPE and VIEW properties
1145       wxString type;
1146       wxString view;
1147       wxString bitFormat;
1148       wxString spectralEditp;
1149 
1150       mCurTrack[0]->TypeSwitch(
1151          [&](const WaveTrack *wt) {
1152             type = wxT("wave");
1153             spectralEditp = mCurTrack[0]->GetSpectrogramSettings().SpectralSelectionEnabled()? wxT("T") : wxT("NIL");
1154             auto displays = WaveTrackView::Get( *wt ).GetDisplays();
1155             auto format = [&]( decltype(displays[0]) display ) {
1156                // Get the English name of the view type, without menu codes,
1157                // as a string that Lisp can examine
1158                return wxString::Format( wxT("\"%s\""),
1159                   display.name.Stripped().Debug() );
1160             };
1161             if (displays.empty())
1162                view = wxT("NIL");
1163             else if (displays.size() == 1)
1164                view = format( displays[0] );
1165             else {
1166                view = wxT("(list");
1167                for ( auto display : displays )
1168                   view += wxString(wxT(" ")) + format( display );
1169                view += wxT(")");
1170             }
1171          },
1172 #if defined(USE_MIDI)
1173          [&](const NoteTrack *) {
1174             type = wxT("midi");
1175             view = wxT("\"Midi\"");
1176          },
1177 #endif
1178          [&](const LabelTrack *) {
1179             type = wxT("label");
1180             view = wxT("\"Label\"");
1181          },
1182          [&](const TimeTrack *) {
1183             type = wxT("time");
1184             view = wxT("\"Time\"");
1185          }
1186       );
1187 
1188       cmd += wxString::Format(wxT("(putprop '*TRACK* %d 'INDEX)\n"), ++mTrackIndex);
1189       cmd += wxString::Format(wxT("(putprop '*TRACK* \"%s\" 'NAME)\n"), EscapeString(mCurTrack[0]->GetName()));
1190       cmd += wxString::Format(wxT("(putprop '*TRACK* \"%s\" 'TYPE)\n"), type);
1191       // Note: "View" property may change when Audacity's choice of track views has stabilized.
1192       cmd += wxString::Format(wxT("(putprop '*TRACK* %s 'VIEW)\n"), view);
1193       cmd += wxString::Format(wxT("(putprop '*TRACK* %d 'CHANNELS)\n"), mCurNumChannels);
1194 
1195       //NOTE: Audacity 2.1.3 True if spectral selection is enabled regardless of track view.
1196       cmd += wxString::Format(wxT("(putprop '*TRACK* %s 'SPECTRAL-EDIT-ENABLED)\n"), spectralEditp);
1197 
1198       auto channels = TrackList::Channels( mCurTrack[0] );
1199       double startTime = channels.min( &Track::GetStartTime );
1200       double endTime = channels.max( &Track::GetEndTime );
1201 
1202       cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'START-TIME)\n"),
1203                               Internat::ToString(startTime));
1204       cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'END-TIME)\n"),
1205                               Internat::ToString(endTime));
1206       cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'GAIN)\n"),
1207                               Internat::ToString(mCurTrack[0]->GetGain()));
1208       cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'PAN)\n"),
1209                               Internat::ToString(mCurTrack[0]->GetPan()));
1210       cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'RATE)\n"),
1211                               Internat::ToString(mCurTrack[0]->GetRate()));
1212 
1213       switch (mCurTrack[0]->GetSampleFormat())
1214       {
1215          case int16Sample:
1216             bitFormat = wxT("16");
1217             break;
1218          case int24Sample:
1219             bitFormat = wxT("24");
1220             break;
1221          case floatSample:
1222             bitFormat = wxT("32.0");
1223             break;
1224       }
1225       cmd += wxString::Format(wxT("(putprop '*TRACK* %s 'FORMAT)\n"), bitFormat);
1226 
1227       float maxPeakLevel = 0.0;  // Deprecated as of 2.1.3
1228       wxString clips, peakString, rmsString;
1229       for (size_t i = 0; i < mCurNumChannels; i++) {
1230          auto ca = mCurTrack[i]->SortedClipArray();
1231          float maxPeak = 0.0;
1232 
1233          // A list of clips for mono, or an array of lists for multi-channel.
1234          if (mCurNumChannels > 1) {
1235             clips += wxT("(list ");
1236          }
1237          // Each clip is a list (start-time, end-time)
1238          // Limit number of clips added to avoid argument stack overflow error (bug 2300).
1239          for (size_t i=0; i<ca.size(); i++) {
1240             if (i < 1000) {
1241                clips += wxString::Format(wxT("(list (float %s) (float %s))"),
1242                                          Internat::ToString(ca[i]->GetPlayStartTime()),
1243                                          Internat::ToString(ca[i]->GetPlayEndTime()));
1244             } else if (i == 1000) {
1245                // If final clip is NIL, plug-in developer knows there are more than 1000 clips in channel.
1246                clips += "NIL";
1247             } else if (i > 1000) {
1248                break;
1249             }
1250          }
1251          if (mCurNumChannels > 1) clips += wxT(" )");
1252 
1253          float min, max;
1254          auto pair = mCurTrack[i]->GetMinMax(mT0, mT1); // may throw
1255          min = pair.first, max = pair.second;
1256          maxPeak = wxMax(wxMax(fabs(min), fabs(max)), maxPeak);
1257          maxPeakLevel = wxMax(maxPeakLevel, maxPeak);
1258 
1259          // On Debian, NaN samples give maxPeak = 3.40282e+38 (FLT_MAX)
1260          if (!std::isinf(maxPeak) && !std::isnan(maxPeak) && (maxPeak < FLT_MAX)) {
1261             peakString += wxString::Format(wxT("(float %s) "), Internat::ToString(maxPeak));
1262          } else {
1263             peakString += wxT("nil ");
1264          }
1265 
1266          float rms = mCurTrack[i]->GetRMS(mT0, mT1); // may throw
1267          if (!std::isinf(rms) && !std::isnan(rms)) {
1268             rmsString += wxString::Format(wxT("(float %s) "), Internat::ToString(rms));
1269          } else {
1270             rmsString += wxT("NIL ");
1271          }
1272       }
1273       // A list of clips for mono, or an array of lists for multi-channel.
1274       cmd += wxString::Format(wxT("(putprop '*TRACK* %s%s ) 'CLIPS)\n"),
1275                               (mCurNumChannels == 1) ? wxT("(list ") : wxT("(vector "),
1276                               clips);
1277 
1278       (mCurNumChannels > 1)?
1279          cmd += wxString::Format(wxT("(putprop '*SELECTION* (vector %s) 'PEAK)\n"), peakString) :
1280          cmd += wxString::Format(wxT("(putprop '*SELECTION* %s 'PEAK)\n"), peakString);
1281 
1282       if (!std::isinf(maxPeakLevel) && !std::isnan(maxPeakLevel) && (maxPeakLevel < FLT_MAX)) {
1283          cmd += wxString::Format(wxT("(putprop '*SELECTION* (float %s) 'PEAK-LEVEL)\n"),
1284                                  Internat::ToString(maxPeakLevel));
1285       }
1286 
1287       (mCurNumChannels > 1)?
1288          cmd += wxString::Format(wxT("(putprop '*SELECTION* (vector %s) 'RMS)\n"), rmsString) :
1289          cmd += wxString::Format(wxT("(putprop '*SELECTION* %s 'RMS)\n"), rmsString);
1290    }
1291 
1292    // If in tool mode, then we don't do anything with the track and selection.
1293    if (GetType() == EffectTypeTool) {
1294       nyx_set_audio_params(44100, 0);
1295    }
1296    else if (GetType() == EffectTypeGenerate) {
1297       nyx_set_audio_params(mCurTrack[0]->GetRate(), 0);
1298    }
1299    else {
1300       auto curLen = mCurLen.as_long_long();
1301       nyx_set_audio_params(mCurTrack[0]->GetRate(), curLen);
1302 
1303       nyx_set_input_audio(StaticGetCallback, (void *)this,
1304                           (int)mCurNumChannels,
1305                           curLen, mCurTrack[0]->GetRate());
1306    }
1307 
1308    // Restore the Nyquist sixteenth note symbol for Generate plug-ins.
1309    // See http://bugzilla.audacityteam.org/show_bug.cgi?id=490.
1310    if (GetType() == EffectTypeGenerate) {
1311       cmd += wxT("(setf s 0.25)\n");
1312    }
1313 
1314    if (mDebug || mTrace) {
1315       cmd += wxT("(setf *tracenable* T)\n");
1316       if (mExternal) {
1317          cmd += wxT("(setf *breakenable* T)\n");
1318       }
1319    }
1320    else {
1321       // Explicitly disable backtrace and prevent values
1322       // from being carried through to the output.
1323       // This should be the final command before evaluating the Nyquist script.
1324       cmd += wxT("(setf *tracenable* NIL)\n");
1325    }
1326 
1327    for (unsigned int j = 0; j < mControls.size(); j++) {
1328       if (mControls[j].type == NYQ_CTRL_FLOAT || mControls[j].type == NYQ_CTRL_FLOAT_TEXT ||
1329           mControls[j].type == NYQ_CTRL_TIME) {
1330          // We use Internat::ToString() rather than "%f" here because we
1331          // always have to use the dot as decimal separator when giving
1332          // numbers to Nyquist, whereas using "%f" will use the user's
1333          // decimal separator which may be a comma in some countries.
1334          cmd += wxString::Format(wxT("(setf %s %s)\n"),
1335                                  mControls[j].var,
1336                                  Internat::ToString(mControls[j].val, 14));
1337       }
1338       else if (mControls[j].type == NYQ_CTRL_INT ||
1339             mControls[j].type == NYQ_CTRL_INT_TEXT ||
1340             mControls[j].type == NYQ_CTRL_CHOICE) {
1341          cmd += wxString::Format(wxT("(setf %s %d)\n"),
1342                                  mControls[j].var,
1343                                  (int)(mControls[j].val));
1344       }
1345       else if (mControls[j].type == NYQ_CTRL_STRING || mControls[j].type == NYQ_CTRL_FILE) {
1346          cmd += wxT("(setf ");
1347          // restrict variable names to 7-bit ASCII:
1348          cmd += mControls[j].var;
1349          cmd += wxT(" \"");
1350          cmd += EscapeString(mControls[j].valStr); // unrestricted value will become quoted UTF-8
1351          cmd += wxT("\")\n");
1352       }
1353    }
1354 
1355    if (mIsSal) {
1356       wxString str = EscapeString(mCmd);
1357       // this is tricky: we need SAL to call main so that we can get a
1358       // SAL traceback in the event of an error (sal-compile catches the
1359       // error and calls sal-error-output), but SAL does not return values.
1360       // We will catch the value in a special global aud:result and if no
1361       // error occurs, we will grab the value with a LISP expression
1362       str += wxT("\nset aud:result = main()\n");
1363 
1364       if (mDebug || mTrace) {
1365          // since we're about to evaluate SAL, remove LISP trace enable and
1366          // break enable (which stops SAL processing) and turn on SAL stack
1367          // trace
1368          cmd += wxT("(setf *tracenable* nil)\n");
1369          cmd += wxT("(setf *breakenable* nil)\n");
1370          cmd += wxT("(setf *sal-traceback* t)\n");
1371       }
1372 
1373       if (mCompiler) {
1374          cmd += wxT("(setf *sal-compiler-debug* t)\n");
1375       }
1376 
1377       cmd += wxT("(setf *sal-call-stack* nil)\n");
1378       // if we do not set this here and an error occurs in main, another
1379       // error will be raised when we try to return the value of aud:result
1380       // which is unbound
1381       cmd += wxT("(setf aud:result nil)\n");
1382       cmd += wxT("(sal-compile-audacity \"") + str + wxT("\" t t nil)\n");
1383       // Capture the value returned by main (saved in aud:result), but
1384       // set aud:result to nil so sound results can be evaluated without
1385       // retaining audio in memory
1386       cmd += wxT("(prog1 aud:result (setf aud:result nil))\n");
1387    }
1388    else {
1389       cmd += mCmd;
1390    }
1391 
1392    // Put the fetch buffers in a clean initial state
1393    for (size_t i = 0; i < mCurNumChannels; i++)
1394       mCurBuffer[i].reset();
1395 
1396    // Guarantee release of memory when done
1397    auto cleanup = finally( [&] {
1398       for (size_t i = 0; i < mCurNumChannels; i++)
1399          mCurBuffer[i].reset();
1400    } );
1401 
1402    // Evaluate the expression, which may invoke the get callback, but often does
1403    // not, leaving that to delayed evaluation of the output sound
1404    rval = nyx_eval_expression(cmd.mb_str(wxConvUTF8));
1405 
1406    // If we're not showing debug window, log errors and warnings:
1407    const auto output = mDebugOutput.Translation();
1408    if (!output.empty() && !mDebug && !mTrace) {
1409       /* i18n-hint: An effect "returned" a message.*/
1410       wxLogMessage(wxT("\'%s\' returned:\n%s"),
1411          mName.Translation(), output);
1412    }
1413 
1414    // Audacity has no idea how long Nyquist processing will take, but
1415    // can monitor audio being returned.
1416    // Anything other than audio should be returned almost instantly
1417    // so notify the user that process has completed (bug 558)
1418    if ((rval != nyx_audio) && ((mCount + mCurNumChannels) == mNumSelectedChannels)) {
1419       if (mCurNumChannels == 1) {
1420          TrackProgress(mCount, 1.0, XO("Processing complete."));
1421       }
1422       else {
1423          TrackGroupProgress(mCount, 1.0, XO("Processing complete."));
1424       }
1425    }
1426 
1427    if ((rval == nyx_audio) && (GetType() == EffectTypeTool)) {
1428       // Catch this first so that we can also handle other errors.
1429       mDebugOutput =
1430          /* i18n-hint: Don't translate ';type tool'.  */
1431          XO("';type tool' effects cannot return audio from Nyquist.\n")
1432          + mDebugOutput;
1433       rval = nyx_error;
1434    }
1435 
1436    if ((rval == nyx_labels) && (GetType() == EffectTypeTool)) {
1437       // Catch this first so that we can also handle other errors.
1438       mDebugOutput =
1439          /* i18n-hint: Don't translate ';type tool'.  */
1440          XO("';type tool' effects cannot return labels from Nyquist.\n")
1441          + mDebugOutput;
1442       rval = nyx_error;
1443    }
1444 
1445    if (rval == nyx_error) {
1446       // Return value is not valid type.
1447       // Show error in debug window if trace enabled, otherwise log.
1448       if (mTrace) {
1449          /* i18n-hint: "%s" is replaced by name of plug-in.*/
1450          mDebugOutput = XO("nyx_error returned from %s.\n")
1451             .Format( mName.empty() ? XO("plug-in") : mName )
1452          + mDebugOutput;
1453          mDebug = true;
1454       }
1455       else {
1456          wxLogMessage(
1457             "Nyquist returned nyx_error:\n%s", mDebugOutput.Translation());
1458       }
1459       return false;
1460    }
1461 
1462    if (rval == nyx_list) {
1463       wxLogMessage("Nyquist returned nyx_list");
1464       if (GetType() == EffectTypeTool) {
1465          mProjectChanged = true;
1466       } else {
1467          Effect::MessageBox(XO("Nyquist returned a list.") );
1468       }
1469       return true;
1470    }
1471 
1472    if (rval == nyx_string) {
1473       // Assume the string has already been translated within the Lisp runtime
1474       // if necessary, by one of the gettext functions defined below, before it
1475       // is communicated back to C++
1476       auto msg = Verbatim( NyquistToWxString(nyx_get_string()) );
1477       if (!msg.empty()) { // Empty string may be used as a No-Op return value.
1478          Effect::MessageBox( msg );
1479       }
1480       else if (GetType() == EffectTypeTool) {
1481          // ;tools may change the project with aud-do commands so
1482          // it is essential that the state is added to history.
1483          mProjectChanged = true;
1484          return true;
1485       }
1486       else {
1487          // A true no-op.
1488          return true;
1489       }
1490 
1491       // True if not process type.
1492       // If not returning audio from process effect,
1493       // return first result then stop (disables preview)
1494       // but allow all output from Nyquist Prompt.
1495       return (GetType() != EffectTypeProcess || mIsPrompt);
1496    }
1497 
1498    if (rval == nyx_double) {
1499       auto str = XO("Nyquist returned the value: %f")
1500          .Format(nyx_get_double());
1501       Effect::MessageBox( str );
1502       return (GetType() != EffectTypeProcess || mIsPrompt);
1503    }
1504 
1505    if (rval == nyx_int) {
1506       auto str = XO("Nyquist returned the value: %d")
1507          .Format(nyx_get_int());
1508       Effect::MessageBox( str );
1509       return (GetType() != EffectTypeProcess || mIsPrompt);
1510    }
1511 
1512    if (rval == nyx_labels) {
1513       mProjectChanged = true;
1514       unsigned int numLabels = nyx_get_num_labels();
1515       unsigned int l;
1516       auto ltrack = * mOutputTracks->Any< LabelTrack >().begin();
1517       if (!ltrack) {
1518          ltrack = static_cast<LabelTrack*>(
1519             AddToOutputTracks(std::make_shared<LabelTrack>()));
1520       }
1521 
1522       for (l = 0; l < numLabels; l++) {
1523          double t0, t1;
1524          const char *str;
1525 
1526          // PRL:  to do:
1527          // let Nyquist analyzers define more complicated selections
1528          nyx_get_label(l, &t0, &t1, &str);
1529 
1530          ltrack->AddLabel(SelectedRegion(t0 + mT0, t1 + mT0), UTF8CTOWX(str));
1531       }
1532       return (GetType() != EffectTypeProcess || mIsPrompt);
1533    }
1534 
1535    wxASSERT(rval == nyx_audio);
1536 
1537    int outChannels = nyx_get_audio_num_channels();
1538    if (outChannels > (int)mCurNumChannels) {
1539       Effect::MessageBox( XO("Nyquist returned too many audio channels.\n") );
1540       return false;
1541    }
1542 
1543    if (outChannels == -1) {
1544       Effect::MessageBox(
1545          XO("Nyquist returned one audio channel as an array.\n") );
1546       return false;
1547    }
1548 
1549    if (outChannels == 0) {
1550       Effect::MessageBox( XO("Nyquist returned an empty array.\n") );
1551       return false;
1552    }
1553 
1554    std::shared_ptr<WaveTrack> outputTrack[2];
1555 
1556    double rate = mCurTrack[0]->GetRate();
1557    for (int i = 0; i < outChannels; i++) {
1558       if (outChannels == (int)mCurNumChannels) {
1559          rate = mCurTrack[i]->GetRate();
1560       }
1561 
1562       outputTrack[i] = mCurTrack[i]->EmptyCopy();
1563       outputTrack[i]->SetRate( rate );
1564 
1565       // Clean the initial buffer states again for the get callbacks
1566       // -- is this really needed?
1567       mCurBuffer[i].reset();
1568    }
1569 
1570    // Now fully evaluate the sound
1571    int success;
1572    {
1573       auto vr0 = valueRestorer( mOutputTrack[0], outputTrack[0].get() );
1574       auto vr1 = valueRestorer( mOutputTrack[1], outputTrack[1].get() );
1575       success = nyx_get_audio(StaticPutCallback, (void *)this);
1576    }
1577 
1578    // See if GetCallback found read errors
1579    {
1580       auto pException = mpException;
1581       mpException = {};
1582       if (pException)
1583          std::rethrow_exception( pException );
1584    }
1585 
1586    if (!success)
1587       return false;
1588 
1589    for (int i = 0; i < outChannels; i++) {
1590       outputTrack[i]->Flush();
1591       mOutputTime = outputTrack[i]->GetEndTime();
1592 
1593       if (mOutputTime <= 0) {
1594          Effect::MessageBox( XO("Nyquist returned nil audio.\n") );
1595          return false;
1596       }
1597    }
1598 
1599    for (size_t i = 0; i < mCurNumChannels; i++) {
1600       WaveTrack *out;
1601 
1602       if (outChannels == (int)mCurNumChannels) {
1603          out = outputTrack[i].get();
1604       }
1605       else {
1606          out = outputTrack[0].get();
1607       }
1608 
1609       if (mMergeClips < 0) {
1610          // Use sample counts to determine default behaviour - times will rarely be equal.
1611          bool bMergeClips = (out->TimeToLongSamples(mT0) + out->TimeToLongSamples(mOutputTime) ==
1612                                                                      out->TimeToLongSamples(mT1));
1613          mCurTrack[i]->ClearAndPaste(mT0, mT1, out, mRestoreSplits, bMergeClips);
1614       }
1615       else {
1616          mCurTrack[i]->ClearAndPaste(mT0, mT1, out, mRestoreSplits, mMergeClips != 0);
1617       }
1618 
1619       // If we were first in the group adjust non-selected group tracks
1620       if (mFirstInGroup) {
1621          for (auto t : TrackList::SyncLockGroup(mCurTrack[i]))
1622          {
1623             if (!t->GetSelected() && t->IsSyncLockSelected()) {
1624                t->SyncLockAdjust(mT1, mT0 + out->GetEndTime());
1625             }
1626          }
1627       }
1628 
1629       // Only the first channel can be first in its group
1630       mFirstInGroup = false;
1631    }
1632 
1633    mProjectChanged = true;
1634    return true;
1635 }
1636 
1637 // ============================================================================
1638 // NyquistEffect Implementation
1639 // ============================================================================
1640 
NyquistToWxString(const char * nyqString)1641 wxString NyquistEffect::NyquistToWxString(const char *nyqString)
1642 {
1643     wxString str(nyqString, wxConvUTF8);
1644     if (nyqString != NULL && nyqString[0] && str.empty()) {
1645         // invalid UTF-8 string, convert as Latin-1
1646         str = _("[Warning: Nyquist returned invalid UTF-8 string, converted here as Latin-1]");
1647        // TODO: internationalization of strings from Nyquist effects, at least
1648        // from those shipped with Audacity
1649         str += LAT1CTOWX(nyqString);
1650     }
1651     return str;
1652 }
1653 
EscapeString(const wxString & inStr)1654 wxString NyquistEffect::EscapeString(const wxString & inStr)
1655 {
1656    wxString str = inStr;
1657 
1658    str.Replace(wxT("\\"), wxT("\\\\"));
1659    str.Replace(wxT("\""), wxT("\\\""));
1660 
1661    return str;
1662 }
1663 
ParseChoice(const wxString & text)1664 std::vector<EnumValueSymbol> NyquistEffect::ParseChoice(const wxString & text)
1665 {
1666    std::vector<EnumValueSymbol> results;
1667    if (text[0] == wxT('(')) {
1668       // New style:  expecting a Lisp-like list of strings
1669       Tokenizer tzer;
1670       tzer.Tokenize(text, true, 1, 1);
1671       auto &choices = tzer.tokens;
1672       wxString extra;
1673       for (auto &choice : choices) {
1674          auto label = UnQuote(choice, true, &extra);
1675          if (extra.empty())
1676             results.push_back( TranslatableString{ label, {} } );
1677          else
1678             results.push_back(
1679                { extra, TranslatableString{ label, {} } } );
1680       }
1681    }
1682    else {
1683       // Old style: expecting a comma-separated list of
1684       // un-internationalized names, ignoring leading and trailing spaces
1685       // on each; and the whole may be quoted
1686       auto choices = wxStringTokenize(
1687          text[0] == wxT('"') ? text.Mid(1, text.length() - 2) : text,
1688          wxT(",")
1689       );
1690       for (auto &choice : choices)
1691          results.push_back( { choice.Trim(true).Trim(false) } );
1692    }
1693    return results;
1694 }
1695 
ParseFileExtensions(const wxString & text)1696 FileExtensions NyquistEffect::ParseFileExtensions(const wxString & text)
1697 {
1698    // todo: error handling
1699    FileExtensions results;
1700    if (text[0] == wxT('(')) {
1701       Tokenizer tzer;
1702       tzer.Tokenize(text, true, 1, 1);
1703       for (const auto &token : tzer.tokens)
1704          results.push_back( UnQuote( token ) );
1705    }
1706    return results;
1707 }
1708 
ParseFileType(const wxString & text)1709 FileNames::FileType NyquistEffect::ParseFileType(const wxString & text)
1710 {
1711    // todo: error handling
1712    FileNames::FileType result;
1713    if (text[0] == wxT('(')) {
1714       Tokenizer tzer;
1715       tzer.Tokenize(text, true, 1, 1);
1716       auto &tokens = tzer.tokens;
1717       if ( tokens.size() == 2 )
1718          result =
1719             { UnQuoteMsgid( tokens[0] ), ParseFileExtensions( tokens[1] ) };
1720    }
1721    return result;
1722 }
1723 
ParseFileTypes(const wxString & text)1724 FileNames::FileTypes NyquistEffect::ParseFileTypes(const wxString & text)
1725 {
1726    // todo: error handling
1727    FileNames::FileTypes results;
1728    if (text[0] == wxT('(')) {
1729       Tokenizer tzer;
1730       tzer.Tokenize(text, true, 1, 1);
1731       auto &types = tzer.tokens;
1732       if ( !types.empty() && types[0][0] == wxT('(') )
1733          for (auto &type : types)
1734             results.push_back( ParseFileType( type ) );
1735    }
1736    if ( results.empty() ) {
1737       // Old-style is a specially formatted string, maybe translated
1738       // Parse it for compatibility
1739       auto str = UnQuote( text );
1740       auto pieces = wxSplit( str, '|' );
1741       // Should have an even number
1742       auto size = pieces.size();
1743       if ( size % 2 == 1 )
1744          --size, pieces.pop_back();
1745       for ( size_t ii = 0; ii < size; ii += 2 ) {
1746          FileExtensions extensions;
1747          auto extensionStrings = wxSplit( pieces[ii + 1], ';' );
1748          for ( const auto &extensionString : extensionStrings )
1749             if ( extensionString.StartsWith( wxT("*.") ) ) {
1750                auto ext = extensionString.substr( 2 );
1751                if (ext == wxT("*"))
1752                   // "*.*" to match all
1753                   ext.clear();
1754                extensions.push_back( ext );
1755             }
1756          results.push_back( { Verbatim( pieces[ii] ), extensions } );
1757       }
1758    }
1759    return results;
1760 }
1761 
RedirectOutput()1762 void NyquistEffect::RedirectOutput()
1763 {
1764    mRedirectOutput = true;
1765 }
1766 
SetCommand(const wxString & cmd)1767 void NyquistEffect::SetCommand(const wxString &cmd)
1768 {
1769    mExternal = true;
1770 
1771    if (cmd.size()) {
1772       ParseCommand(cmd);
1773    }
1774 }
1775 
Break()1776 void NyquistEffect::Break()
1777 {
1778    mBreak = true;
1779 }
1780 
Continue()1781 void NyquistEffect::Continue()
1782 {
1783    mCont = true;
1784 }
1785 
Stop()1786 void NyquistEffect::Stop()
1787 {
1788    mStop = true;
1789 }
1790 
UnQuoteMsgid(const wxString & s,bool allowParens,wxString * pExtraString)1791 TranslatableString NyquistEffect::UnQuoteMsgid(const wxString &s, bool allowParens,
1792                                 wxString *pExtraString)
1793 {
1794    if (pExtraString)
1795       *pExtraString = wxString{};
1796 
1797    int len = s.length();
1798    if (len >= 2 && s[0] == wxT('\"') && s[len - 1] == wxT('\"')) {
1799       auto unquoted = s.Mid(1, len - 2);
1800       // Sorry, no context strings, yet
1801       // (See also comments in NyquistEffectsModule::AutoRegisterPlugins)
1802       return TranslatableString{ unquoted, {} };
1803    }
1804    else if (allowParens &&
1805             len >= 2 && s[0] == wxT('(') && s[len - 1] == wxT(')')) {
1806       Tokenizer tzer;
1807       tzer.Tokenize(s, true, 1, 1);
1808       auto &tokens = tzer.tokens;
1809       if (tokens.size() > 1) {
1810          if (pExtraString && tokens[1][0] == '(') {
1811             // A choice with a distinct internal string form like
1812             // ("InternalString" (_ "Visible string"))
1813             // Recur to find the two strings
1814             *pExtraString = UnQuote(tokens[0], false);
1815             return UnQuoteMsgid(tokens[1]);
1816          }
1817          else {
1818             // Assume the first token was _ -- we don't check that
1819             // And the second is the string, which is internationalized
1820             // Sorry, no context strings, yet
1821             return UnQuoteMsgid( tokens[1], false );
1822          }
1823       }
1824       else
1825          return {};
1826    }
1827    else
1828       // If string was not quoted, assume no translation exists
1829       return Verbatim( s );
1830 }
1831 
UnQuote(const wxString & s,bool allowParens,wxString * pExtraString)1832 wxString NyquistEffect::UnQuote(const wxString &s, bool allowParens,
1833                                 wxString *pExtraString)
1834 {
1835    return UnQuoteMsgid( s, allowParens, pExtraString ).Translation();
1836 }
1837 
GetCtrlValue(const wxString & s)1838 double NyquistEffect::GetCtrlValue(const wxString &s)
1839 {
1840    /* For this to work correctly requires that the plug-in header is
1841     * parsed on each run so that the correct value for "half-srate" may
1842     * be determined.
1843     *
1844    auto project = FindProject();
1845    if (project && s.IsSameAs(wxT("half-srate"), false)) {
1846       auto rate =
1847          TrackList::Get( *project ).Selected< const WaveTrack >()
1848             .min( &WaveTrack::GetRate );
1849       return (rate / 2.0);
1850    }
1851    */
1852 
1853    return Internat::CompatibleToDouble(s);
1854 }
1855 
Tokenize(const wxString & line,bool eof,size_t trimStart,size_t trimEnd)1856 bool NyquistEffect::Tokenizer::Tokenize(
1857    const wxString &line, bool eof,
1858    size_t trimStart, size_t trimEnd)
1859 {
1860    auto endToken = [&]{
1861       if (!tok.empty()) {
1862          tokens.push_back(tok);
1863          tok = wxT("");
1864       }
1865    };
1866 
1867    for (auto c :
1868         make_iterator_range(line.begin() + trimStart, line.end() - trimEnd)) {
1869       if (q && !sl && c == wxT('\\')) {
1870          // begin escaped character, only within quotes
1871          sl = true;
1872          continue;
1873       }
1874 
1875       if (!sl && c == wxT('"')) {
1876          // Unescaped quote
1877          if (!q) {
1878             // start of string
1879             if (!paren)
1880                // finish previous token
1881                endToken();
1882             // Include the delimiter in the token
1883             tok += c;
1884             q = true;
1885          }
1886          else {
1887             // end of string
1888             // Include the delimiter in the token
1889             tok += c;
1890             if (!paren)
1891                endToken();
1892             q = false;
1893          }
1894       }
1895       else if (!q && !paren && (c == wxT(' ') || c == wxT('\t')))
1896          // Unenclosed whitespace
1897          // Separate tokens; don't accumulate this character
1898          endToken();
1899       else if (!q && c == wxT(';'))
1900          // semicolon not in quotes, but maybe in parentheses
1901          // Lisp style comments with ; (but not with #| ... |#) are allowed
1902          // within a wrapped header multi-line, so that i18n hint comments may
1903          // be placed before strings and found by xgettext
1904          break;
1905       else if (!q && c == wxT('(')) {
1906          // Start of list or sublist
1907          if (++paren == 1)
1908             // finish previous token; begin list, including the delimiter
1909             endToken(), tok += c;
1910          else
1911             // defer tokenizing of nested list to a later pass over the token
1912             tok += c;
1913       }
1914       else if (!q && c == wxT(')')) {
1915          // End of list or sublist
1916          if (--paren == 0)
1917             // finish list, including the delimiter
1918             tok += c, endToken();
1919          else if (paren < 0)
1920             // forgive unbalanced right paren
1921             paren = 0, endToken();
1922          else
1923             // nested list; deferred tokenizing
1924             tok += c;
1925       }
1926       else {
1927          if (sl && paren)
1928             // Escaped character in string inside list, to be parsed again
1929             // Put the escape back for the next pass
1930             tok += wxT('\\');
1931          if (sl && !paren && c == 'n')
1932             // Convert \n to newline, the only special escape besides \\ or \"
1933             // But this should not be used if a string needs to localize.
1934             // Instead, simply put a line break in the string.
1935             c = '\n';
1936          tok += c;
1937       }
1938 
1939       sl = false;
1940    }
1941 
1942    if (eof || (!q && !paren)) {
1943       endToken();
1944       return true;
1945    }
1946    else {
1947       // End of line but not of file, and a string or list is yet unclosed
1948       // If a string, accumulate a newline character
1949       if (q)
1950          tok += wxT('\n');
1951       return false;
1952    }
1953 }
1954 
Parse(Tokenizer & tzer,const wxString & line,bool eof,bool first)1955 bool NyquistEffect::Parse(
1956    Tokenizer &tzer, const wxString &line, bool eof, bool first)
1957 {
1958    if ( !tzer.Tokenize(line, eof, first ? 1 : 0, 0) )
1959       return false;
1960 
1961    const auto &tokens = tzer.tokens;
1962    int len = tokens.size();
1963    if (len < 1) {
1964       return true;
1965    }
1966 
1967    // Consistency decision is for "plug-in" as the correct spelling
1968    // "plugin" (deprecated) is allowed as an undocumented convenience.
1969    if (len == 2 && tokens[0] == wxT("nyquist") &&
1970       (tokens[1] == wxT("plug-in") || tokens[1] == wxT("plugin"))) {
1971       mOK = true;
1972       return true;
1973    }
1974 
1975    if (len >= 2 && tokens[0] == wxT("type")) {
1976       wxString tok = tokens[1];
1977       mIsTool = false;
1978       if (tok == wxT("tool")) {
1979          mIsTool = true;
1980          mType = EffectTypeTool;
1981          // we allow
1982          // ;type tool
1983          // ;type tool process
1984          // ;type tool generate
1985          // ;type tool analyze
1986          // The last three are placed in the tool menu, but are processed as
1987          // process, generate or analyze.
1988          if (len >= 3)
1989             tok = tokens[2];
1990       }
1991 
1992       if (tok == wxT("process")) {
1993          mType = EffectTypeProcess;
1994       }
1995       else if (tok == wxT("generate")) {
1996          mType = EffectTypeGenerate;
1997       }
1998       else if (tok == wxT("analyze")) {
1999          mType = EffectTypeAnalyze;
2000       }
2001 
2002       if (len >= 3 && tokens[2] == wxT("spectral")) {;
2003          mIsSpectral = true;
2004       }
2005       return true;
2006    }
2007 
2008    if (len == 2 && tokens[0] == wxT("codetype")) {
2009       // This will stop ParseProgram() from doing a best guess as program type.
2010       if (tokens[1] == wxT("lisp")) {
2011          mIsSal = false;
2012          mFoundType = true;
2013       }
2014       else if (tokens[1] == wxT("sal")) {
2015          mIsSal = true;
2016          mFoundType = true;
2017       }
2018       return true;
2019    }
2020 
2021    if (len >= 2 && tokens[0] == wxT("debugflags")) {
2022       for (int i = 1; i < len; i++) {
2023          // "trace" sets *tracenable* (LISP) or *sal-traceback* (SAL)
2024          // and displays debug window IF there is anything to show.
2025          if (tokens[i] == wxT("trace")) {
2026             mTrace = true;
2027          }
2028          else if (tokens[i] == wxT("notrace")) {
2029             mTrace = false;
2030          }
2031          else if (tokens[i] == wxT("compiler")) {
2032             mCompiler = true;
2033          }
2034          else if (tokens[i] == wxT("nocompiler")) {
2035             mCompiler = false;
2036          }
2037       }
2038       return true;
2039    }
2040 
2041    // We support versions 1, 2 and 3
2042    // (Version 2 added support for string parameters.)
2043    // (Version 3 added support for choice parameters.)
2044    // (Version 4 added support for project/track/selection information.)
2045    if (len >= 2 && tokens[0] == wxT("version")) {
2046       long v;
2047       tokens[1].ToLong(&v);
2048       if (v < 1 || v > 4) {
2049          // This is an unsupported plug-in version
2050          mOK = false;
2051          mInitError = XO(
2052 "This version of Audacity does not support Nyquist plug-in version %ld")
2053             .Format( v );
2054          return true;
2055       }
2056       mVersion = (int) v;
2057    }
2058 
2059    if (len >= 2 && tokens[0] == wxT("name")) {
2060       // Names do not yet support context strings for translations, or
2061       // internal names distinct from visible English names.
2062       // (See also comments in NyquistEffectsModule::AutoRegisterPlugins)
2063       auto name = UnQuote(tokens[1]);
2064       // Strip ... from name if it's present, perhaps in third party plug-ins
2065       // Menu system puts ... back if there are any controls
2066       // This redundant naming convention must NOT be followed for
2067       // shipped Nyquist effects with internationalization.  Else the msgid
2068       // later looked up will lack the ... and will not be found.
2069       if (name.EndsWith(wxT("...")))
2070          name = name.RemoveLast(3);
2071       mName = TranslatableString{ name, {} };
2072       return true;
2073    }
2074 
2075    if (len >= 2 && tokens[0] == wxT("action")) {
2076       mAction = TranslatableString{ UnQuote(tokens[1]), {} };
2077       return true;
2078    }
2079 
2080    if (len >= 2 && tokens[0] == wxT("info")) {
2081       mInfo = TranslatableString{ UnQuote(tokens[1]), {} };
2082       return true;
2083    }
2084 
2085    if (len >= 2 && tokens[0] == wxT("preview")) {
2086       if (tokens[1] == wxT("enabled") || tokens[1] == wxT("true")) {
2087          mEnablePreview = true;
2088          SetLinearEffectFlag(false);
2089       }
2090       else if (tokens[1] == wxT("linear")) {
2091          mEnablePreview = true;
2092          SetLinearEffectFlag(true);
2093       }
2094       else if (tokens[1] == wxT("selection")) {
2095          mEnablePreview = true;
2096          SetPreviewFullSelectionFlag(true);
2097       }
2098       else if (tokens[1] == wxT("disabled") || tokens[1] == wxT("false")) {
2099          mEnablePreview = false;
2100       }
2101       return true;
2102    }
2103 
2104    // Maximum number of samples to be processed. This can help the
2105    // progress bar if effect does not process all of selection.
2106    if (len >= 2 && tokens[0] == wxT("maxlen")) {
2107       long long v; // Note that Nyquist may overflow at > 2^31 samples (bug 439)
2108       tokens[1].ToLongLong(&v);
2109       mMaxLen = (sampleCount) v;
2110    }
2111 
2112 #if defined(EXPERIMENTAL_NYQUIST_SPLIT_CONTROL)
2113    if (len >= 2 && tokens[0] == wxT("mergeclips")) {
2114       long v;
2115       // -1 = auto (default), 0 = don't merge clips, 1 = do merge clips
2116       tokens[1].ToLong(&v);
2117       mMergeClips = v;
2118       return true;
2119    }
2120 
2121    if (len >= 2 && tokens[0] == wxT("restoresplits")) {
2122       long v;
2123       // Splits are restored by default. Set to 0 to prevent.
2124       tokens[1].ToLong(&v);
2125       mRestoreSplits = !!v;
2126       return true;
2127    }
2128 #endif
2129 
2130    if (len >= 2 && tokens[0] == wxT("author")) {
2131       mAuthor = TranslatableString{ UnQuote(tokens[1]), {} };
2132       return true;
2133    }
2134 
2135    if (len >= 2 && tokens[0] == wxT("release")) {
2136       // Value must be quoted if the release version string contains spaces.
2137       mReleaseVersion =
2138          TranslatableString{ UnQuote(tokens[1]), {} };
2139       return true;
2140    }
2141 
2142    if (len >= 2 && tokens[0] == wxT("copyright")) {
2143       mCopyright = TranslatableString{ UnQuote(tokens[1]), {} };
2144       return true;
2145    }
2146 
2147    // Page name in Audacity development manual
2148    if (len >= 2 && tokens[0] == wxT("manpage")) {
2149       // do not translate
2150       mManPage = UnQuote(tokens[1], false);
2151       return true;
2152    }
2153 
2154    // Local Help file
2155    if (len >= 2 && tokens[0] == wxT("helpfile")) {
2156       // do not translate
2157       mHelpFile = UnQuote(tokens[1], false);
2158       return true;
2159    }
2160 
2161    // Debug button may be disabled for release plug-ins.
2162    if (len >= 2 && tokens[0] == wxT("debugbutton")) {
2163       if (tokens[1] == wxT("disabled") || tokens[1] == wxT("false")) {
2164          mDebugButton = false;
2165       }
2166       return true;
2167    }
2168 
2169 
2170    if (len >= 3 && tokens[0] == wxT("control")) {
2171       NyqControl ctrl;
2172 
2173       if (len == 3 && tokens[1] == wxT("text")) {
2174          ctrl.var = tokens[1];
2175          ctrl.label = UnQuote( tokens[2] );
2176          ctrl.type = NYQ_CTRL_TEXT;
2177       }
2178       else if (len >= 5)
2179       {
2180          ctrl.var = tokens[1];
2181          ctrl.name = UnQuote( tokens[2] );
2182          // 3 is type, below
2183          ctrl.label = tokens[4];
2184 
2185          // valStr may or may not be a quoted string
2186          ctrl.valStr = len > 5 ? tokens[5] : wxString{};
2187          ctrl.val = GetCtrlValue(ctrl.valStr);
2188          if (ctrl.valStr.length() > 0 &&
2189                (ctrl.valStr[0] == wxT('(') ||
2190                ctrl.valStr[0] == wxT('"')))
2191             ctrl.valStr = UnQuote( ctrl.valStr );
2192 
2193          // 6 is minimum, below
2194          // 7 is maximum, below
2195 
2196          if (tokens[3] == wxT("string")) {
2197             ctrl.type = NYQ_CTRL_STRING;
2198             ctrl.label = UnQuote( ctrl.label );
2199          }
2200          else if (tokens[3] == wxT("choice")) {
2201             ctrl.type = NYQ_CTRL_CHOICE;
2202             ctrl.choices = ParseChoice(ctrl.label);
2203             ctrl.label = wxT("");
2204          }
2205          else if (tokens[3] == wxT("file")) {
2206             ctrl.type = NYQ_CTRL_FILE;
2207             ctrl.fileTypes = ParseFileTypes(tokens[6]);
2208             // will determine file dialog styles:
2209             ctrl.highStr = UnQuote( tokens[7] );
2210             ctrl.label = UnQuote(ctrl.label);
2211          }
2212          else {
2213             ctrl.label = UnQuote( ctrl.label );
2214 
2215             if (len < 8) {
2216                return true;
2217             }
2218 
2219             if ((tokens[3] == wxT("float")) ||
2220                   (tokens[3] == wxT("real"))) // Deprecated
2221                ctrl.type = NYQ_CTRL_FLOAT;
2222             else if (tokens[3] == wxT("int"))
2223                ctrl.type = NYQ_CTRL_INT;
2224             else if (tokens[3] == wxT("float-text"))
2225                ctrl.type = NYQ_CTRL_FLOAT_TEXT;
2226             else if (tokens[3] == wxT("int-text"))
2227                ctrl.type = NYQ_CTRL_INT_TEXT;
2228             else if (tokens[3] == wxT("time"))
2229                 ctrl.type = NYQ_CTRL_TIME;
2230             else
2231             {
2232                wxString str;
2233                str.Printf(wxT("Bad Nyquist 'control' type specification: '%s' in plug-in file '%s'.\nControl not created."),
2234                         tokens[3], mFileName.GetFullPath());
2235 
2236                // Too disturbing to show alert before Audacity frame is up.
2237                //    Effect::MessageBox(
2238                //       str,
2239                //       wxOK | wxICON_EXCLAMATION,
2240                //       XO("Nyquist Warning") );
2241 
2242                // Note that the AudacityApp's mLogger has not yet been created,
2243                // so this brings up an alert box, but after the Audacity frame is up.
2244                wxLogWarning(str);
2245                return true;
2246             }
2247 
2248             ctrl.lowStr = UnQuote( tokens[6] );
2249             if (ctrl.type == NYQ_CTRL_INT_TEXT && ctrl.lowStr.IsSameAs(wxT("nil"), false)) {
2250                ctrl.low = INT_MIN;
2251             }
2252             else if (ctrl.type == NYQ_CTRL_FLOAT_TEXT && ctrl.lowStr.IsSameAs(wxT("nil"), false)) {
2253                ctrl.low = -(FLT_MAX);
2254             }
2255             else if (ctrl.type == NYQ_CTRL_TIME && ctrl.lowStr.IsSameAs(wxT("nil"), false)) {
2256                 ctrl.low = 0.0;
2257             }
2258             else {
2259                ctrl.low = GetCtrlValue(ctrl.lowStr);
2260             }
2261 
2262             ctrl.highStr = UnQuote( tokens[7] );
2263             if (ctrl.type == NYQ_CTRL_INT_TEXT && ctrl.highStr.IsSameAs(wxT("nil"), false)) {
2264                ctrl.high = INT_MAX;
2265             }
2266             else if ((ctrl.type == NYQ_CTRL_FLOAT_TEXT || ctrl.type == NYQ_CTRL_TIME) &&
2267                       ctrl.highStr.IsSameAs(wxT("nil"), false))
2268             {
2269                ctrl.high = FLT_MAX;
2270             }
2271             else {
2272                ctrl.high = GetCtrlValue(ctrl.highStr);
2273             }
2274 
2275             if (ctrl.high < ctrl.low) {
2276                ctrl.high = ctrl.low;
2277             }
2278 
2279             if (ctrl.val < ctrl.low) {
2280                ctrl.val = ctrl.low;
2281             }
2282 
2283             if (ctrl.val > ctrl.high) {
2284                ctrl.val = ctrl.high;
2285             }
2286 
2287             ctrl.ticks = 1000;
2288             if (ctrl.type == NYQ_CTRL_INT &&
2289                (ctrl.high - ctrl.low < ctrl.ticks)) {
2290                ctrl.ticks = (int)(ctrl.high - ctrl.low);
2291             }
2292          }
2293       }
2294 
2295       if( ! make_iterator_range( mPresetNames ).contains( ctrl.var ) )
2296       {
2297          mControls.push_back(ctrl);
2298       }
2299    }
2300 
2301    // Deprecated
2302    if (len >= 2 && tokens[0] == wxT("categories")) {
2303       for (size_t i = 1; i < tokens.size(); ++i) {
2304          mCategories.push_back(tokens[i]);
2305       }
2306    }
2307    return true;
2308 }
2309 
ParseProgram(wxInputStream & stream)2310 bool NyquistEffect::ParseProgram(wxInputStream & stream)
2311 {
2312    if (!stream.IsOk())
2313    {
2314       mInitError = XO("Could not open file");
2315       return false;
2316    }
2317 
2318    wxTextInputStream pgm(stream, wxT(" \t"), wxConvAuto());
2319 
2320    mCmd = wxT("");
2321    mCmd.Alloc(10000);
2322    mIsSal = false;
2323    mControls.clear();
2324    mCategories.clear();
2325    mIsSpectral = false;
2326    mManPage = wxEmptyString; // If not wxEmptyString, must be a page in the Audacity manual.
2327    mHelpFile = wxEmptyString; // If not wxEmptyString, must be a valid HTML help file.
2328    mHelpFileExists = false;
2329    mDebug = false;
2330    mTrace = false;
2331    mDebugButton = true;    // Debug button enabled by default.
2332    mEnablePreview = true;  // Preview button enabled by default.
2333 
2334    // Bug 1934.
2335    // All Nyquist plug-ins should have a ';type' field, but if they don't we default to
2336    // being an Effect.
2337    mType = EffectTypeProcess;
2338 
2339    mFoundType = false;
2340    while (!stream.Eof() && stream.IsOk())
2341    {
2342       wxString line = pgm.ReadLine();
2343       if (line.length() > 1 &&
2344           // New in 2.3.0:  allow magic comment lines to start with $
2345           // The trick is that xgettext will not consider such lines comments
2346           // and will extract the strings they contain
2347           (line[0] == wxT(';') || line[0] == wxT('$')) )
2348       {
2349          Tokenizer tzer;
2350          unsigned nLines = 1;
2351          bool done;
2352          // Allow continuations within control lines.
2353          bool control =
2354             line[0] == wxT('$') || line.StartsWith( wxT(";control") );
2355          do
2356             done = Parse(tzer, line, !control || stream.Eof(), nLines == 1);
2357          while(!done &&
2358             (line = pgm.ReadLine(), ++nLines, true));
2359 
2360          // Don't pass these lines to the interpreter, so it doesn't get confused
2361          // by $, but pass blanks,
2362          // so that SAL effects compile with proper line numbers
2363          while (nLines --)
2364             mCmd += wxT('\n');
2365       }
2366       else
2367       {
2368          if(!mFoundType && line.length() > 0) {
2369             if (line[0] == wxT('(') ||
2370                 (line[0] == wxT('#') && line.length() > 1 && line[1] == wxT('|')))
2371             {
2372                mIsSal = false;
2373                mFoundType = true;
2374             }
2375             else if (line.Upper().Find(wxT("RETURN")) != wxNOT_FOUND)
2376             {
2377                mIsSal = true;
2378                mFoundType = true;
2379             }
2380          }
2381          mCmd += line + wxT("\n");
2382       }
2383    }
2384    if (!mFoundType && mIsPrompt)
2385    {
2386       /* i1n-hint: SAL and LISP are names for variant syntaxes for the
2387        Nyquist programming language.  Leave them, and 'return', untranslated. */
2388       Effect::MessageBox(
2389          XO(
2390 "Your code looks like SAL syntax, but there is no \'return\' statement.\n\
2391 For SAL, use a return statement such as:\n\treturn *track* * 0.1\n\
2392 or for LISP, begin with an open parenthesis such as:\n\t(mult *track* 0.1)\n ."),
2393          Effect::DefaultMessageBoxStyle,
2394          XO("Error in Nyquist code") );
2395       /* i18n-hint: refers to programming "languages" */
2396       mInitError = XO("Could not determine language");
2397       return false;
2398       // Else just throw it at Nyquist to see what happens
2399    }
2400 
2401    return true;
2402 }
2403 
ParseFile()2404 void NyquistEffect::ParseFile()
2405 {
2406    wxFileInputStream rawStream(mFileName.GetFullPath());
2407    wxBufferedInputStream stream(rawStream, 10000);
2408 
2409    ParseProgram(stream);
2410 }
2411 
ParseCommand(const wxString & cmd)2412 bool NyquistEffect::ParseCommand(const wxString & cmd)
2413 {
2414    wxStringInputStream stream(cmd + wxT(" "));
2415 
2416    return ParseProgram(stream);
2417 }
2418 
StaticGetCallback(float * buffer,int channel,int64_t start,int64_t len,int64_t totlen,void * userdata)2419 int NyquistEffect::StaticGetCallback(float *buffer, int channel,
2420                                      int64_t start, int64_t len, int64_t totlen,
2421                                      void *userdata)
2422 {
2423    NyquistEffect *This = (NyquistEffect *)userdata;
2424    return This->GetCallback(buffer, channel, start, len, totlen);
2425 }
2426 
GetCallback(float * buffer,int ch,int64_t start,int64_t len,int64_t WXUNUSED (totlen))2427 int NyquistEffect::GetCallback(float *buffer, int ch,
2428                                int64_t start, int64_t len, int64_t WXUNUSED(totlen))
2429 {
2430    if (mCurBuffer[ch]) {
2431       if ((mCurStart[ch] + start) < mCurBufferStart[ch] ||
2432           (mCurStart[ch] + start)+len >
2433           mCurBufferStart[ch]+mCurBufferLen[ch]) {
2434          mCurBuffer[ch].reset();
2435       }
2436    }
2437 
2438    if (!mCurBuffer[ch]) {
2439       mCurBufferStart[ch] = (mCurStart[ch] + start);
2440       mCurBufferLen[ch] = mCurTrack[ch]->GetBestBlockSize(mCurBufferStart[ch]);
2441 
2442       if (mCurBufferLen[ch] < (size_t) len) {
2443          mCurBufferLen[ch] = mCurTrack[ch]->GetIdealBlockSize();
2444       }
2445 
2446       mCurBufferLen[ch] =
2447          limitSampleBufferSize( mCurBufferLen[ch],
2448                                 mCurStart[ch] + mCurLen - mCurBufferStart[ch] );
2449 
2450       // C++20
2451       // mCurBuffer[ch] = std::make_unique_for_overwrite(mCurBufferLen[ch]);
2452       mCurBuffer[ch] = Buffer{ safenew float[ mCurBufferLen[ch] ] };
2453       try {
2454          mCurTrack[ch]->GetFloats( mCurBuffer[ch].get(),
2455             mCurBufferStart[ch], mCurBufferLen[ch]);
2456       }
2457       catch ( ... ) {
2458          // Save the exception object for re-throw when out of the library
2459          mpException = std::current_exception();
2460          return -1;
2461       }
2462    }
2463 
2464    // We have guaranteed above that this is nonnegative and bounded by
2465    // mCurBufferLen[ch]:
2466    auto offset = ( mCurStart[ch] + start - mCurBufferStart[ch] ).as_size_t();
2467    const void *src = &mCurBuffer[ch][offset];
2468    std::memcpy(buffer, src, len * sizeof(float));
2469 
2470    if (ch == 0) {
2471       double progress = mScale *
2472          ( (start+len)/ mCurLen.as_double() );
2473 
2474       if (progress > mProgressIn) {
2475          mProgressIn = progress;
2476       }
2477 
2478       if (TotalProgress(mProgressIn+mProgressOut+mProgressTot)) {
2479          return -1;
2480       }
2481    }
2482 
2483    return 0;
2484 }
2485 
StaticPutCallback(float * buffer,int channel,int64_t start,int64_t len,int64_t totlen,void * userdata)2486 int NyquistEffect::StaticPutCallback(float *buffer, int channel,
2487                                      int64_t start, int64_t len, int64_t totlen,
2488                                      void *userdata)
2489 {
2490    NyquistEffect *This = (NyquistEffect *)userdata;
2491    return This->PutCallback(buffer, channel, start, len, totlen);
2492 }
2493 
PutCallback(float * buffer,int channel,int64_t start,int64_t len,int64_t totlen)2494 int NyquistEffect::PutCallback(float *buffer, int channel,
2495                                int64_t start, int64_t len, int64_t totlen)
2496 {
2497    // Don't let C++ exceptions propagate through the Nyquist library
2498    return GuardedCall<int>( [&] {
2499       if (channel == 0) {
2500          double progress = mScale*((float)(start+len)/totlen);
2501 
2502          if (progress > mProgressOut) {
2503             mProgressOut = progress;
2504          }
2505 
2506          if (TotalProgress(mProgressIn+mProgressOut+mProgressTot)) {
2507             return -1;
2508          }
2509       }
2510 
2511       mOutputTrack[channel]->Append((samplePtr)buffer, floatSample, len);
2512 
2513       return 0; // success
2514    }, MakeSimpleGuard( -1 ) ); // translate all exceptions into failure
2515 }
2516 
StaticOutputCallback(int c,void * This)2517 void NyquistEffect::StaticOutputCallback(int c, void *This)
2518 {
2519    ((NyquistEffect *)This)->OutputCallback(c);
2520 }
2521 
OutputCallback(int c)2522 void NyquistEffect::OutputCallback(int c)
2523 {
2524    // Always collect Nyquist error messages for normal plug-ins
2525    if (!mRedirectOutput) {
2526       mDebugOutputStr += (wxChar)c;
2527       return;
2528    }
2529 
2530    std::cout << (char)c;
2531 }
2532 
StaticOSCallback(void * This)2533 void NyquistEffect::StaticOSCallback(void *This)
2534 {
2535    ((NyquistEffect *)This)->OSCallback();
2536 }
2537 
OSCallback()2538 void NyquistEffect::OSCallback()
2539 {
2540    if (mStop) {
2541       mStop = false;
2542       nyx_stop();
2543    }
2544    else if (mBreak) {
2545       mBreak = false;
2546       nyx_break();
2547    }
2548    else if (mCont) {
2549       mCont = false;
2550       nyx_continue();
2551    }
2552 
2553    // LLL:  STF figured out that yielding while the effect is being applied
2554    //       produces an EXTREME slowdown.  It appears that yielding is not
2555    //       really necessary on Linux and Windows.
2556    //
2557    //       However, on the Mac, the spinning cursor appears during longer
2558    //       Nyquist processing and that may cause the user to think Audacity
2559    //       has crashed or hung.  In addition, yielding or not on the Mac
2560    //       doesn't seem to make much of a difference in execution time.
2561    //
2562    //       So, yielding on the Mac only...
2563 #if defined(__WXMAC__)
2564    wxYieldIfNeeded();
2565 #endif
2566 }
2567 
GetNyquistSearchPath()2568 FilePaths NyquistEffect::GetNyquistSearchPath()
2569 {
2570    const auto &audacityPathList = FileNames::AudacityPathList();
2571    FilePaths pathList;
2572 
2573    for (size_t i = 0; i < audacityPathList.size(); i++)
2574    {
2575       wxString prefix = audacityPathList[i] + wxFILE_SEP_PATH;
2576       FileNames::AddUniquePathToPathList(prefix + wxT("nyquist"), pathList);
2577       FileNames::AddUniquePathToPathList(prefix + wxT("plugins"), pathList);
2578       FileNames::AddUniquePathToPathList(prefix + wxT("plug-ins"), pathList);
2579    }
2580    pathList.push_back(FileNames::PlugInDir());
2581 
2582    return pathList;
2583 }
2584 
TransferDataToPromptWindow()2585 bool NyquistEffect::TransferDataToPromptWindow()
2586 {
2587    mCommandText->ChangeValue(mInputCmd);
2588 
2589    return true;
2590 }
2591 
TransferDataToEffectWindow()2592 bool NyquistEffect::TransferDataToEffectWindow()
2593 {
2594    for (size_t i = 0, cnt = mControls.size(); i < cnt; i++)
2595    {
2596       NyqControl & ctrl = mControls[i];
2597 
2598       if (ctrl.type == NYQ_CTRL_CHOICE)
2599       {
2600          const auto count = ctrl.choices.size();
2601 
2602          int val = (int)ctrl.val;
2603          if (val < 0 || val >= (int)count)
2604          {
2605             val = 0;
2606          }
2607 
2608          wxChoice *c = (wxChoice *) mUIParent->FindWindow(ID_Choice + i);
2609          c->SetSelection(val);
2610       }
2611       else if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_FLOAT)
2612       {
2613          // wxTextCtrls are handled by the validators
2614          double range = ctrl.high - ctrl.low;
2615          int val = (int)(0.5 + ctrl.ticks * (ctrl.val - ctrl.low) / range);
2616          wxSlider *s = (wxSlider *) mUIParent->FindWindow(ID_Slider + i);
2617          s->SetValue(val);
2618       }
2619       else if (ctrl.type == NYQ_CTRL_TIME)
2620       {
2621          NumericTextCtrl *n = (NumericTextCtrl *) mUIParent->FindWindow(ID_Time + i);
2622          n->SetValue(ctrl.val);
2623       }
2624    }
2625 
2626    return true;
2627 }
2628 
TransferDataFromPromptWindow()2629 bool NyquistEffect::TransferDataFromPromptWindow()
2630 {
2631    mInputCmd = mCommandText->GetValue();
2632 
2633    // Un-correct smart quoting, bothersomely applied in wxTextCtrl by
2634    // the native widget of MacOS 10.9 SDK
2635    const wxString left = wxT("\u201c"), right = wxT("\u201d"), dumb = '"';
2636    mInputCmd.Replace(left, dumb, true);
2637    mInputCmd.Replace(right, dumb, true);
2638 
2639    const wxString leftSingle = wxT("\u2018"), rightSingle = wxT("\u2019"),
2640       dumbSingle = '\'';
2641    mInputCmd.Replace(leftSingle, dumbSingle, true);
2642    mInputCmd.Replace(rightSingle, dumbSingle, true);
2643 
2644    return ParseCommand(mInputCmd);
2645 }
2646 
TransferDataFromEffectWindow()2647 bool NyquistEffect::TransferDataFromEffectWindow()
2648 {
2649    if (mControls.size() == 0)
2650    {
2651       return true;
2652    }
2653 
2654    for (unsigned int i = 0; i < mControls.size(); i++)
2655    {
2656       NyqControl *ctrl = &mControls[i];
2657 
2658       if (ctrl->type == NYQ_CTRL_STRING || ctrl->type == NYQ_CTRL_TEXT)
2659       {
2660          continue;
2661       }
2662 
2663       if (ctrl->val == UNINITIALIZED_CONTROL)
2664       {
2665          ctrl->val = GetCtrlValue(ctrl->valStr);
2666       }
2667 
2668       if (ctrl->type == NYQ_CTRL_CHOICE)
2669       {
2670          continue;
2671       }
2672 
2673       if (ctrl->type == NYQ_CTRL_FILE)
2674       {
2675          resolveFilePath(ctrl->valStr);
2676 
2677          wxString path;
2678          if (ctrl->valStr.StartsWith("\"", &path))
2679          {
2680             // Validate if a list of quoted paths.
2681             if (path.EndsWith("\"", &path))
2682             {
2683                path.Replace("\"\"", "\"");
2684                wxStringTokenizer tokenizer(path, "\"");
2685                while (tokenizer.HasMoreTokens())
2686                {
2687                   wxString token = tokenizer.GetNextToken();
2688                   if(!validatePath(token))
2689                   {
2690                      const auto message =
2691                         XO("\"%s\" is not a valid file path.").Format( token );
2692                      Effect::MessageBox(
2693                         message,
2694                         wxOK | wxICON_EXCLAMATION | wxCENTRE,
2695                         XO("Error") );
2696                      return false;
2697                   }
2698                }
2699                continue;
2700             }
2701             else
2702             {
2703                const auto message =
2704                   /* i18n-hint: Warning that there is one quotation mark rather than a pair.*/
2705                   XO("Mismatched quotes in\n%s").Format( ctrl->valStr );
2706                Effect::MessageBox(
2707                   message,
2708                   wxOK | wxICON_EXCLAMATION | wxCENTRE,
2709                   XO("Error") );
2710                return false;
2711             }
2712          }
2713          // Validate a single path.
2714          else if (validatePath(ctrl->valStr))
2715          {
2716             continue;
2717          }
2718 
2719          // Validation failed
2720          const auto message =
2721             XO("\"%s\" is not a valid file path.").Format( ctrl->valStr );
2722          Effect::MessageBox(
2723             message,
2724             wxOK | wxICON_EXCLAMATION | wxCENTRE,
2725             XO("Error") );
2726          return false;
2727       }
2728 
2729       if (ctrl->type == NYQ_CTRL_TIME)
2730       {
2731          NumericTextCtrl *n = (NumericTextCtrl *) mUIParent->FindWindow(ID_Time + i);
2732          ctrl->val = n->GetValue();
2733       }
2734 
2735       if (ctrl->type == NYQ_CTRL_INT_TEXT && ctrl->lowStr.IsSameAs(wxT("nil"), false)) {
2736          ctrl->low = INT_MIN;
2737       }
2738       else if ((ctrl->type == NYQ_CTRL_FLOAT_TEXT || ctrl->type == NYQ_CTRL_TIME) &&
2739                ctrl->lowStr.IsSameAs(wxT("nil"), false))
2740       {
2741          ctrl->low = -(FLT_MAX);
2742       }
2743       else
2744       {
2745          ctrl->low = GetCtrlValue(ctrl->lowStr);
2746       }
2747 
2748       if (ctrl->type == NYQ_CTRL_INT_TEXT && ctrl->highStr.IsSameAs(wxT("nil"), false)) {
2749          ctrl->high = INT_MAX;
2750       }
2751       else if ((ctrl->type == NYQ_CTRL_FLOAT_TEXT || ctrl->type == NYQ_CTRL_TIME) &&
2752                ctrl->highStr.IsSameAs(wxT("nil"), false))
2753       {
2754          ctrl->high = FLT_MAX;
2755       }
2756       else
2757       {
2758          ctrl->high = GetCtrlValue(ctrl->highStr);
2759       }
2760 
2761       if (ctrl->high < ctrl->low)
2762       {
2763          ctrl->high = ctrl->low + 1;
2764       }
2765 
2766       if (ctrl->val < ctrl->low)
2767       {
2768          ctrl->val = ctrl->low;
2769       }
2770 
2771       if (ctrl->val > ctrl->high)
2772       {
2773          ctrl->val = ctrl->high;
2774       }
2775 
2776       ctrl->ticks = 1000;
2777       if (ctrl->type == NYQ_CTRL_INT &&
2778           (ctrl->high - ctrl->low < ctrl->ticks))
2779       {
2780          ctrl->ticks = (int)(ctrl->high - ctrl->low);
2781       }
2782    }
2783 
2784    return true;
2785 }
2786 
BuildPromptWindow(ShuttleGui & S)2787 void NyquistEffect::BuildPromptWindow(ShuttleGui & S)
2788 {
2789    S.StartVerticalLay();
2790    {
2791       S.StartMultiColumn(3, wxEXPAND);
2792       {
2793          S.SetStretchyCol(1);
2794 
2795          S.AddVariableText(XO("Enter Nyquist Command: "));
2796 
2797          S.AddSpace(1, 1);
2798       }
2799       S.EndMultiColumn();
2800 
2801       S.StartHorizontalLay(wxEXPAND, 1);
2802       {
2803           mCommandText = S.Focus()
2804             .MinSize( { 500, 200 } )
2805             .AddTextWindow(wxT(""));
2806       }
2807       S.EndHorizontalLay();
2808 
2809       S.StartHorizontalLay(wxALIGN_CENTER, 0);
2810       {
2811          S.Id(ID_Load).AddButton(XXO("&Load"));
2812          S.Id(ID_Save).AddButton(XXO("&Save"));
2813       }
2814       S.EndHorizontalLay();
2815    }
2816    S.EndVerticalLay();
2817 }
2818 
BuildEffectWindow(ShuttleGui & S)2819 void NyquistEffect::BuildEffectWindow(ShuttleGui & S)
2820 {
2821    wxScrolledWindow *scroller = S.Style(wxVSCROLL | wxTAB_TRAVERSAL)
2822       .StartScroller(2);
2823    {
2824       S.StartMultiColumn(4);
2825       {
2826          for (size_t i = 0; i < mControls.size(); i++)
2827          {
2828             NyqControl & ctrl = mControls[i];
2829 
2830             if (ctrl.type == NYQ_CTRL_TEXT)
2831             {
2832                S.EndMultiColumn();
2833                S.StartHorizontalLay(wxALIGN_LEFT, 0);
2834                {
2835                   S.AddSpace(0, 10);
2836                   S.AddFixedText( Verbatim( ctrl.label ), false );
2837                }
2838                S.EndHorizontalLay();
2839                S.StartMultiColumn(4);
2840             }
2841             else
2842             {
2843                auto prompt = XXO("%s:").Format( ctrl.name );
2844                S.AddPrompt( prompt );
2845 
2846                if (ctrl.type == NYQ_CTRL_STRING)
2847                {
2848                   S.AddSpace(10, 10);
2849 
2850                   auto item = S.Id(ID_Text + i)
2851                      .Validator<wxGenericValidator>(&ctrl.valStr)
2852                      .Name( prompt )
2853                      .AddTextBox( {}, wxT(""), 50);
2854                }
2855                else if (ctrl.type == NYQ_CTRL_CHOICE)
2856                {
2857                   S.AddSpace(10, 10);
2858 
2859                   S.Id(ID_Choice + i).AddChoice( {},
2860                      Msgids( ctrl.choices.data(), ctrl.choices.size() ) );
2861                }
2862                else if (ctrl.type == NYQ_CTRL_TIME)
2863                {
2864                   S.AddSpace(10, 10);
2865 
2866                   const auto options = NumericTextCtrl::Options{}
2867                                           .AutoPos(true)
2868                                           .MenuEnabled(true)
2869                                           .ReadOnly(false);
2870 
2871                   NumericTextCtrl *time = safenew
2872                      NumericTextCtrl(S.GetParent(), (ID_Time + i),
2873                                      NumericConverter::TIME,
2874                                      GetSelectionFormat(),
2875                                      ctrl.val,
2876                                      mProjectRate,
2877                                      options);
2878                   S
2879                      .Name( prompt )
2880                      .Position(wxALIGN_LEFT | wxALL)
2881                      .AddWindow(time);
2882                }
2883                else if (ctrl.type == NYQ_CTRL_FILE)
2884                {
2885                   S.AddSpace(10, 10);
2886 
2887                   // Get default file extension if specified in wildcards
2888                   FileExtension defaultExtension;
2889                   if (!ctrl.fileTypes.empty()) {
2890                      const auto &type = ctrl.fileTypes[0];
2891                      if ( !type.extensions.empty() )
2892                         defaultExtension = type.extensions[0];
2893                   }
2894                   resolveFilePath(ctrl.valStr, defaultExtension);
2895 
2896                   wxTextCtrl *item = S.Id(ID_Text+i)
2897                      .Name( prompt )
2898                      .AddTextBox( {}, wxT(""), 40);
2899                   item->SetValidator(wxGenericValidator(&ctrl.valStr));
2900 
2901                   if (ctrl.label.empty())
2902                      // We'd expect wxFileSelectorPromptStr to already be translated, but apparently not.
2903                      ctrl.label = wxGetTranslation( wxFileSelectorPromptStr );
2904                   S.Id(ID_FILE + i).AddButton(
2905                      Verbatim(ctrl.label), wxALIGN_LEFT);
2906                }
2907                else
2908                {
2909                   // Integer or Real
2910                   if (ctrl.type == NYQ_CTRL_INT_TEXT || ctrl.type == NYQ_CTRL_FLOAT_TEXT)
2911                   {
2912                      S.AddSpace(10, 10);
2913                   }
2914 
2915                   S.Id(ID_Text+i);
2916                   if (ctrl.type == NYQ_CTRL_FLOAT || ctrl.type == NYQ_CTRL_FLOAT_TEXT)
2917                   {
2918                      double range = ctrl.high - ctrl.low;
2919                      S.Validator<FloatingPointValidator<double>>(
2920                         // > 12 decimal places can cause rounding errors in display.
2921                         12, &ctrl.val,
2922                         // Set number of decimal places
2923                         (range < 10
2924                            ? NumValidatorStyle::THREE_TRAILING_ZEROES
2925                            : range < 100
2926                               ? NumValidatorStyle::TWO_TRAILING_ZEROES
2927                               : NumValidatorStyle::ONE_TRAILING_ZERO),
2928                         ctrl.low, ctrl.high
2929                      );
2930                   }
2931                   else
2932                   {
2933                      S.Validator<IntegerValidator<double>>(
2934                         &ctrl.val, NumValidatorStyle::DEFAULT,
2935                         (int) ctrl.low, (int) ctrl.high);
2936                   }
2937                   wxTextCtrl *item = S
2938                      .Name( prompt )
2939                      .AddTextBox( {}, wxT(""),
2940                         (ctrl.type == NYQ_CTRL_INT_TEXT ||
2941                          ctrl.type == NYQ_CTRL_FLOAT_TEXT) ? 25 : 12);
2942 
2943                   if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_FLOAT)
2944                   {
2945                      S.Id(ID_Slider + i)
2946                         .Style(wxSL_HORIZONTAL)
2947                         .MinSize( { 150, -1 } )
2948                         .AddSlider( {}, 0, ctrl.ticks, 0);
2949                   }
2950                }
2951 
2952                if (ctrl.type != NYQ_CTRL_FILE)
2953                {
2954                   if (ctrl.type == NYQ_CTRL_CHOICE || ctrl.label.empty())
2955                   {
2956                      S.AddSpace(10, 10);
2957                   }
2958                   else
2959                   {
2960                      S.AddUnits( Verbatim( ctrl.label ) );
2961                   }
2962                }
2963             }
2964          }
2965       }
2966       S.EndMultiColumn();
2967    }
2968    S.EndScroller();
2969 
2970    scroller->SetScrollRate(0, 20);
2971 
2972    // This fools NVDA into not saying "Panel" when the dialog gets focus
2973    scroller->SetName(wxT("\a"));
2974    scroller->SetLabel(wxT("\a"));
2975 }
2976 
2977 // NyquistEffect implementation
2978 
IsOk()2979 bool NyquistEffect::IsOk()
2980 {
2981    return mOK;
2982 }
2983 
2984 static const FileNames::FileType
2985    /* i18n-hint: Nyquist is the name of a programming language */
2986      NyquistScripts = { XO("Nyquist scripts"), { wxT("ny") }, true }
2987    /* i18n-hint: Lisp is the name of a programming language */
2988    , LispScripts = { XO("Lisp scripts"), { wxT("lsp") }, true }
2989 ;
2990 
OnLoad(wxCommandEvent & WXUNUSED (evt))2991 void NyquistEffect::OnLoad(wxCommandEvent & WXUNUSED(evt))
2992 {
2993    if (mCommandText->IsModified())
2994    {
2995       if (wxNO == Effect::MessageBox(
2996          XO("Current program has been modified.\nDiscard changes?"),
2997          wxYES_NO ) )
2998       {
2999          return;
3000       }
3001    }
3002 
3003    FileDialogWrapper dlog(
3004       mUIParent,
3005       XO("Load Nyquist script"),
3006       mFileName.GetPath(),
3007       wxEmptyString,
3008       {
3009          NyquistScripts,
3010          LispScripts,
3011          FileNames::TextFiles,
3012          FileNames::AllFiles
3013       },
3014       wxFD_OPEN | wxRESIZE_BORDER);
3015 
3016    if (dlog.ShowModal() != wxID_OK)
3017    {
3018       return;
3019    }
3020 
3021    mFileName = dlog.GetPath();
3022 
3023    if (!mCommandText->LoadFile(mFileName.GetFullPath()))
3024    {
3025       Effect::MessageBox( XO("File could not be loaded") );
3026    }
3027 }
3028 
OnSave(wxCommandEvent & WXUNUSED (evt))3029 void NyquistEffect::OnSave(wxCommandEvent & WXUNUSED(evt))
3030 {
3031    FileDialogWrapper dlog(
3032       mUIParent,
3033       XO("Save Nyquist script"),
3034       mFileName.GetPath(),
3035       mFileName.GetFullName(),
3036       {
3037          NyquistScripts,
3038          LispScripts,
3039          FileNames::AllFiles
3040       },
3041       wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxRESIZE_BORDER);
3042 
3043    if (dlog.ShowModal() != wxID_OK)
3044    {
3045       return;
3046    }
3047 
3048    mFileName = dlog.GetPath();
3049 
3050    if (!mCommandText->SaveFile(mFileName.GetFullPath()))
3051    {
3052       Effect::MessageBox( XO("File could not be saved") );
3053    }
3054 }
3055 
OnSlider(wxCommandEvent & evt)3056 void NyquistEffect::OnSlider(wxCommandEvent & evt)
3057 {
3058    int i = evt.GetId() - ID_Slider;
3059    NyqControl & ctrl = mControls[i];
3060 
3061    int val = evt.GetInt();
3062    double range = ctrl.high - ctrl.low;
3063    double newVal = (val / (double)ctrl.ticks) * range + ctrl.low;
3064 
3065    // Determine precision for displayed number
3066    int precision = range < 1.0 ? 3 :
3067                    range < 10.0 ? 2 :
3068                    range < 100.0 ? 1 :
3069                    0;
3070 
3071    // If the value is at least one tick different from the current value
3072    // change it (this prevents changes from manually entered values unless
3073    // the slider actually moved)
3074    if (fabs(newVal - ctrl.val) >= (1 / (double)ctrl.ticks) * range &&
3075        fabs(newVal - ctrl.val) >= pow(0.1, precision) / 2)
3076    {
3077       // First round to the appropriate precision
3078       newVal *= pow(10.0, precision);
3079       newVal = floor(newVal + 0.5);
3080       newVal /= pow(10.0, precision);
3081 
3082       ctrl.val = newVal;
3083 
3084       mUIParent->FindWindow(ID_Text + i)->GetValidator()->TransferToWindow();
3085    }
3086 }
3087 
OnChoice(wxCommandEvent & evt)3088 void NyquistEffect::OnChoice(wxCommandEvent & evt)
3089 {
3090    mControls[evt.GetId() - ID_Choice].val = (double) evt.GetInt();
3091 }
3092 
OnTime(wxCommandEvent & evt)3093 void NyquistEffect::OnTime(wxCommandEvent& evt)
3094 {
3095    int i = evt.GetId() - ID_Time;
3096    static double value = 0.0;
3097    NyqControl & ctrl = mControls[i];
3098 
3099    NumericTextCtrl *n = (NumericTextCtrl *) mUIParent->FindWindow(ID_Time + i);
3100    double val = n->GetValue();
3101 
3102    // Observed that two events transmitted on each control change (Linux)
3103    // so skip if value has not changed.
3104    if (val != value) {
3105       if (val < ctrl.low || val > ctrl.high) {
3106          const auto message = XO("Value range:\n%s to %s")
3107             .Format( ToTimeFormat(ctrl.low), ToTimeFormat(ctrl.high) );
3108          Effect::MessageBox(
3109             message,
3110             wxOK | wxCENTRE,
3111             XO("Value Error") );
3112       }
3113 
3114       if (val < ctrl.low)
3115          val = ctrl.low;
3116       else if (val > ctrl.high)
3117          val = ctrl.high;
3118 
3119       n->SetValue(val);
3120       value = val;
3121    }
3122 }
3123 
OnFileButton(wxCommandEvent & evt)3124 void NyquistEffect::OnFileButton(wxCommandEvent& evt)
3125 {
3126    int i = evt.GetId() - ID_FILE;
3127    NyqControl & ctrl = mControls[i];
3128 
3129    // Get style flags:
3130    // Ensure legal combinations so that wxWidgets does not throw an assert error.
3131    unsigned int flags = 0;
3132    if (!ctrl.highStr.empty())
3133    {
3134       wxStringTokenizer tokenizer(ctrl.highStr, ",");
3135       while ( tokenizer.HasMoreTokens() )
3136       {
3137          wxString token = tokenizer.GetNextToken().Trim(true).Trim(false);
3138          if (token.IsSameAs("open", false))
3139          {
3140             flags |= wxFD_OPEN;
3141             flags &= ~wxFD_SAVE;
3142             flags &= ~wxFD_OVERWRITE_PROMPT;
3143          }
3144          else if (token.IsSameAs("save", false))
3145          {
3146             flags |= wxFD_SAVE;
3147             flags &= ~wxFD_OPEN;
3148             flags &= ~wxFD_MULTIPLE;
3149             flags &= ~wxFD_FILE_MUST_EXIST;
3150          }
3151          else if (token.IsSameAs("overwrite", false) && !(flags & wxFD_OPEN))
3152          {
3153             flags |= wxFD_OVERWRITE_PROMPT;
3154          }
3155          else if (token.IsSameAs("exists", false) && !(flags & wxFD_SAVE))
3156          {
3157             flags |= wxFD_FILE_MUST_EXIST;
3158          }
3159          else if (token.IsSameAs("multiple", false) && !(flags & wxFD_SAVE))
3160          {
3161             flags |= wxFD_MULTIPLE;
3162          }
3163       }
3164    }
3165 
3166    resolveFilePath(ctrl.valStr);
3167 
3168    wxFileName fname = ctrl.valStr;
3169    wxString defaultDir = fname.GetPath();
3170    wxString defaultFile = fname.GetName();
3171    auto message = XO("Select a file");
3172 
3173    if (flags & wxFD_MULTIPLE)
3174       message = XO("Select one or more files");
3175    else if (flags & wxFD_SAVE)
3176       message = XO("Save file as");
3177 
3178    FileDialogWrapper openFileDialog(mUIParent->FindWindow(ID_FILE + i),
3179                                message,
3180                                defaultDir,
3181                                defaultFile,
3182                                ctrl.fileTypes,
3183                                flags);       // styles
3184 
3185    if (openFileDialog.ShowModal() == wxID_CANCEL)
3186    {
3187       return;
3188    }
3189 
3190    wxString path;
3191    // When multiple files selected, return file paths as a list of quoted strings.
3192    if (flags & wxFD_MULTIPLE)
3193    {
3194       wxArrayString selectedFiles;
3195       openFileDialog.GetPaths(selectedFiles);
3196 
3197       for (size_t sf = 0; sf < selectedFiles.size(); sf++) {
3198          path += "\"";
3199          path += selectedFiles[sf];
3200          path += "\"";
3201       }
3202       ctrl.valStr = path;
3203    }
3204    else
3205    {
3206       ctrl.valStr = openFileDialog.GetPath();
3207    }
3208 
3209    mUIParent->FindWindow(ID_Text + i)->GetValidator()->TransferToWindow();
3210 }
3211 
resolveFilePath(wxString & path,FileExtension extension)3212 void NyquistEffect::resolveFilePath(wxString& path, FileExtension extension /* empty string */)
3213 {
3214 #if defined(__WXMSW__)
3215    path.Replace("/", wxFileName::GetPathSeparator());
3216 #endif
3217 
3218    path.Trim(true).Trim(false);
3219 
3220    typedef std::unordered_map<wxString, FilePath> map;
3221    map pathKeys = {
3222       {"*home*", wxGetHomeDir()},
3223       {"~", wxGetHomeDir()},
3224       {"*default*", FileNames::DefaultToDocumentsFolder("").GetPath()},
3225       {"*export*", FileNames::FindDefaultPath(FileNames::Operation::Export)},
3226       {"*save*", FileNames::FindDefaultPath(FileNames::Operation::Save)},
3227       {"*config*", FileNames::DataDir()}
3228    };
3229 
3230    int characters = path.Find(wxFileName::GetPathSeparator());
3231    if(characters == wxNOT_FOUND) // Just a path or just a file name
3232    {
3233       if (path.empty())
3234          path = "*default*";
3235 
3236       if (pathKeys.find(path) != pathKeys.end())
3237       {
3238          // Keyword found, so assume this is the intended directory.
3239          path = pathKeys[path] + wxFileName::GetPathSeparator();
3240       }
3241       else  // Just a file name
3242       {
3243          path = pathKeys["*default*"] + wxFileName::GetPathSeparator() + path;
3244       }
3245    }
3246    else  // path + file name
3247    {
3248       wxString firstDir = path.Left(characters);
3249       wxString rest = path.Mid(characters);
3250 
3251       if (pathKeys.find(firstDir) != pathKeys.end())
3252       {
3253          path = pathKeys[firstDir] + rest;
3254       }
3255    }
3256 
3257    wxFileName fname = path;
3258 
3259    // If the directory is invalid, better to leave it as is (invalid) so that
3260    // the user sees the error rather than an unexpected file path.
3261    if (fname.wxFileName::IsOk() && fname.GetFullName().empty())
3262    {
3263       path = fname.GetPathWithSep() + _("untitled");
3264       if (!extension.empty())
3265          path = path + '.' + extension;
3266    }
3267 }
3268 
3269 
validatePath(wxString path)3270 bool NyquistEffect::validatePath(wxString path)
3271 {
3272    wxFileName fname = path;
3273    wxString dir = fname.GetPath();
3274 
3275    return (fname.wxFileName::IsOk() &&
3276            wxFileName::DirExists(dir) &&
3277            !fname.GetFullName().empty());
3278 }
3279 
3280 
ToTimeFormat(double t)3281 wxString NyquistEffect::ToTimeFormat(double t)
3282 {
3283    int seconds = static_cast<int>(t);
3284    int hh = seconds / 3600;
3285    int mm = seconds % 3600;
3286    mm = mm / 60;
3287    return wxString::Format("%d:%d:%.3f", hh, mm, t - (hh * 3600 + mm * 60));
3288 }
3289 
3290 
OnText(wxCommandEvent & evt)3291 void NyquistEffect::OnText(wxCommandEvent & evt)
3292 {
3293    int i = evt.GetId() - ID_Text;
3294 
3295    NyqControl & ctrl = mControls[i];
3296 
3297    if (wxDynamicCast(evt.GetEventObject(), wxWindow)->GetValidator()->TransferFromWindow())
3298    {
3299       if (ctrl.type == NYQ_CTRL_FLOAT || ctrl.type == NYQ_CTRL_INT)
3300       {
3301          int pos = (int)floor((ctrl.val - ctrl.low) /
3302                               (ctrl.high - ctrl.low) * ctrl.ticks + 0.5);
3303 
3304          wxSlider *slider = (wxSlider *)mUIParent->FindWindow(ID_Slider + i);
3305          slider->SetValue(pos);
3306       }
3307    }
3308 }
3309 
3310 ///////////////////////////////////////////////////////////////////////////////
3311 //
3312 // NyquistOutputDialog
3313 //
3314 ///////////////////////////////////////////////////////////////////////////////
3315 
3316 
BEGIN_EVENT_TABLE(NyquistOutputDialog,wxDialogWrapper)3317 BEGIN_EVENT_TABLE(NyquistOutputDialog, wxDialogWrapper)
3318    EVT_BUTTON(wxID_OK, NyquistOutputDialog::OnOk)
3319 END_EVENT_TABLE()
3320 
3321 NyquistOutputDialog::NyquistOutputDialog(wxWindow * parent, wxWindowID id,
3322                                        const TranslatableString & title,
3323                                        const TranslatableString & prompt,
3324                                        const TranslatableString &message)
3325 : wxDialogWrapper{ parent, id, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER }
3326 {
3327    SetName();
3328 
3329    ShuttleGui S{ this, eIsCreating };
3330    {
3331       S.SetBorder(10);
3332 
3333       S.AddVariableText( prompt, false, wxALIGN_LEFT | wxLEFT | wxTOP | wxRIGHT );
3334 
3335       // TODO: use ShowInfoDialog() instead.
3336       // Beware this dialog MUST work with screen readers.
3337       S.Prop( 1 )
3338          .Position(wxEXPAND | wxALL)
3339          .MinSize( { 480, 250 } )
3340          .Style(wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH)
3341          .AddTextWindow( message.Translation() );
3342 
3343       S.SetBorder( 5 );
3344 
3345       S.StartHorizontalLay(wxALIGN_CENTRE | wxLEFT | wxBOTTOM | wxRIGHT, 0 );
3346       {
3347          /* i18n-hint: In most languages OK is to be translated as OK.  It appears on a button.*/
3348          S.Id(wxID_OK).AddButton( XXO("OK"), wxALIGN_CENTRE, true );
3349       }
3350       S.EndHorizontalLay();
3351 
3352    }
3353 
3354    SetAutoLayout(true);
3355    GetSizer()->Fit(this);
3356    GetSizer()->SetSizeHints(this);
3357 }
3358 
3359 // ============================================================================
3360 // NyquistOutputDialog implementation
3361 // ============================================================================
3362 
OnOk(wxCommandEvent &)3363 void NyquistOutputDialog::OnOk(wxCommandEvent & /* event */)
3364 {
3365    EndModal(wxID_OK);
3366 }
3367 
3368 // Registration of extra functions in XLisp.
3369 #include "../../../lib-src/libnyquist/nyquist/xlisp/xlisp.h"
3370 
gettext()3371 static LVAL gettext()
3372 {
3373    auto string = UTF8CTOWX(getstring(xlgastring()));
3374 #if !HAS_I18N_CONTEXTS
3375    // allow ignored context argument
3376    if ( moreargs() )
3377       nextarg();
3378 #endif
3379    xllastarg();
3380    return cvstring(GetCustomTranslation(string).mb_str(wxConvUTF8));
3381 }
3382 
gettextc()3383 static LVAL gettextc()
3384 {
3385 #if HAS_I18N_CONTEXTS
3386    auto string = UTF8CTOWX(getstring(xlgastring()));
3387    auto context = UTF8CTOWX(getstring(xlgastring()));
3388    xllastarg();
3389    return cvstring(wxGetTranslation( string, "", 0, "", context )
3390       .mb_str(wxConvUTF8));
3391 #else
3392    return gettext();
3393 #endif
3394 }
3395 
ngettext()3396 static LVAL ngettext()
3397 {
3398    auto string1 = UTF8CTOWX(getstring(xlgastring()));
3399    auto string2 = UTF8CTOWX(getstring(xlgastring()));
3400    auto number = getfixnum(xlgafixnum());
3401 #if !HAS_I18N_CONTEXTS
3402    // allow ignored context argument
3403    if ( moreargs() )
3404       nextarg();
3405 #endif
3406    xllastarg();
3407    return cvstring(
3408       wxGetTranslation(string1, string2, number).mb_str(wxConvUTF8));
3409 }
3410 
ngettextc()3411 static LVAL ngettextc()
3412 {
3413 #if HAS_I18N_CONTEXTS
3414    auto string1 = UTF8CTOWX(getstring(xlgastring()));
3415    auto string2 = UTF8CTOWX(getstring(xlgastring()));
3416    auto number = getfixnum(xlgafixnum());
3417    auto context = UTF8CTOWX(getstring(xlgastring()));
3418    xllastarg();
3419    return cvstring(wxGetTranslation( string1, string2, number, "", context )
3420       .mb_str(wxConvUTF8));
3421 #else
3422    return ngettext();
3423 #endif
3424 }
3425 
nyq_make_opaque_string(int size,unsigned char * src)3426 void * nyq_make_opaque_string( int size, unsigned char *src ){
3427     LVAL dst;
3428     unsigned char * dstp;
3429     dst = new_string((int)(size+2));
3430     dstp = getstring(dst);
3431 
3432     /* copy the source to the destination */
3433     while (size-- > 0)
3434         *dstp++ = *src++;
3435     *dstp = '\0';
3436 
3437     return (void*)dst;
3438 }
3439 
nyq_reformat_aud_do_response(const wxString & Str)3440 void * nyq_reformat_aud_do_response(const wxString & Str) {
3441    LVAL dst;
3442    LVAL message;
3443    LVAL success;
3444    wxString Left = Str.BeforeLast('\n').BeforeLast('\n').ToAscii();
3445    wxString Right = Str.BeforeLast('\n').AfterLast('\n').ToAscii();
3446    message = cvstring(Left);
3447    success = Right.EndsWith("OK") ? s_true : nullptr;
3448    dst = cons(message, success);
3449    return (void *)dst;
3450 }
3451 
3452 #include "../../commands/ScriptCommandRelay.h"
3453 
3454 
3455 /* xlc_aud_do -- interface to C routine aud_do */
3456 /**/
xlc_aud_do(void)3457 LVAL xlc_aud_do(void)
3458 {
3459 // Based on string-trim...
3460     unsigned char *leftp;
3461     LVAL src,dst;
3462 
3463     /* get the string */
3464     src = xlgastring();
3465     xllastarg();
3466 
3467     /* setup the string pointer */
3468     leftp = getstring(src);
3469 
3470     // Go call my real function here...
3471     dst = (LVAL)ExecForLisp( (char *)leftp );
3472 
3473     //dst = cons(dst, (LVAL)1);
3474     /* return the new string */
3475     return (dst);
3476 }
3477 
RegisterFunctions()3478 static void RegisterFunctions()
3479 {
3480    // Add functions to XLisp.  Do this only once,
3481    // before the first call to nyx_init.
3482    static bool firstTime = true;
3483    if (firstTime) {
3484       firstTime = false;
3485 
3486       // All function names must be UP-CASED
3487       static const FUNDEF functions[] = {
3488          { "_", SUBR, gettext },
3489          { "_C", SUBR, gettextc },
3490          { "NGETTEXT", SUBR, ngettext },
3491          { "NGETTEXTC", SUBR, ngettextc },
3492          { "AUD-DO",  SUBR, xlc_aud_do },
3493        };
3494 
3495       xlbindfunctions( functions, WXSIZEOF( functions ) );
3496    }
3497 }
3498