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