mirror of
https://github.com/cookiengineer/audacity
synced 2025-05-03 00:59:43 +02:00
... Although "Nyquist" is a proper name, still, translators should have discretion to transliterate it into another alphabet, and also make it consistent with longer translated strings containing it. Not so for debug messages mentioning actual keywords of Nyquist programming.
2475 lines
74 KiB
C++
2475 lines
74 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
Nyquist.cpp
|
|
|
|
Dominic Mazzoni
|
|
|
|
******************************************************************//**
|
|
|
|
\class NyquistEffect
|
|
\brief An Effect that calls up a Nyquist (XLISP) plug-in, i.e. many possible
|
|
effects from this one class.
|
|
|
|
*//****************************************************************//**
|
|
|
|
\class NyquistOutputDialog
|
|
\brief Dialog used with NyquistEffect
|
|
|
|
*//****************************************************************//**
|
|
|
|
\class NyqControl
|
|
\brief A control on a NyquistDialog.
|
|
|
|
*//*******************************************************************/
|
|
|
|
#include "../../Audacity.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
#include <locale.h>
|
|
|
|
#include <wx/checkbox.h>
|
|
#include <wx/choice.h>
|
|
#include <wx/datetime.h>
|
|
#include <wx/intl.h>
|
|
#include <wx/log.h>
|
|
#include <wx/msgdlg.h>
|
|
#include <wx/sstream.h>
|
|
#include <wx/textdlg.h>
|
|
#include <wx/txtstrm.h>
|
|
#include <wx/valgen.h>
|
|
#include <wx/wfstream.h>
|
|
#include <wx/numformatter.h>
|
|
|
|
#include "../../AudacityApp.h"
|
|
#include "../../FileException.h"
|
|
#include "../../FileNames.h"
|
|
#include "../../Internat.h"
|
|
#include "../../LabelTrack.h"
|
|
#include "../../prefs/SpectrogramSettings.h"
|
|
#include "../../Project.h"
|
|
#include "../../ShuttleGui.h"
|
|
#include "../../WaveClip.h"
|
|
#include "../../WaveTrack.h"
|
|
#include "../../widgets/valnum.h"
|
|
#include "../../Prefs.h"
|
|
#include "../../prefs/WaveformSettings.h"
|
|
|
|
#include "FileDialog.h"
|
|
|
|
#include "Nyquist.h"
|
|
|
|
#ifndef nyx_returns_start_and_end_time
|
|
#error You need to update lib-src/libnyquist
|
|
#endif
|
|
|
|
#include <locale.h>
|
|
#include <iostream>
|
|
#include <ostream>
|
|
#include <sstream>
|
|
#include <float.h>
|
|
|
|
#include "../../Experimental.h"
|
|
|
|
enum
|
|
{
|
|
ID_Editor = 10000,
|
|
ID_Version,
|
|
ID_Load,
|
|
ID_Save,
|
|
|
|
ID_Slider = 11000,
|
|
ID_Text = 12000,
|
|
ID_Choice = 13000
|
|
};
|
|
|
|
// Protect Nyquist from selections greater than 2^31 samples (bug 439)
|
|
#define NYQ_MAX_LEN (std::numeric_limits<long>::max())
|
|
|
|
#define UNINITIALIZED_CONTROL ((double)99999999.99)
|
|
|
|
static const wxChar *KEY_Version = XO("Version");
|
|
static const wxChar *KEY_Command = XO("Command");
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// NyquistEffect
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include <wx/arrimpl.cpp>
|
|
WX_DEFINE_OBJARRAY(NyqControlArray);
|
|
|
|
BEGIN_EVENT_TABLE(NyquistEffect, wxEvtHandler)
|
|
EVT_BUTTON(ID_Load, NyquistEffect::OnLoad)
|
|
EVT_BUTTON(ID_Save, NyquistEffect::OnSave)
|
|
|
|
EVT_COMMAND_RANGE(ID_Slider, ID_Slider+99,
|
|
wxEVT_COMMAND_SLIDER_UPDATED, NyquistEffect::OnSlider)
|
|
EVT_COMMAND_RANGE(ID_Text, ID_Text+99,
|
|
wxEVT_COMMAND_TEXT_UPDATED, NyquistEffect::OnText)
|
|
EVT_COMMAND_RANGE(ID_Choice, ID_Choice + 99,
|
|
wxEVT_COMMAND_CHOICE_SELECTED, NyquistEffect::OnChoice)
|
|
END_EVENT_TABLE()
|
|
|
|
NyquistEffect::NyquistEffect(const wxString &fName)
|
|
{
|
|
mOutputTrack[0] = mOutputTrack[1] = nullptr;
|
|
|
|
mAction = _("Applying Nyquist Effect...");
|
|
mInputCmd = wxEmptyString;
|
|
mCmd = wxEmptyString;
|
|
mIsPrompt = false;
|
|
mExternal = false;
|
|
mCompiler = false;
|
|
mTrace = false;
|
|
mRedirectOutput = false;
|
|
mDebug = false;
|
|
mIsSal = false;
|
|
mOK = false;
|
|
mAuthor = XO("n/a");
|
|
mCopyright = XO("n/a");
|
|
|
|
// set clip/split handling when applying over clip boundary.
|
|
mRestoreSplits = true; // Default: Restore split lines.
|
|
mMergeClips = -1; // Default (auto): Merge if length remains unchanged.
|
|
|
|
mDebugButton = true; // Debug button enabled by default.
|
|
|
|
mVersion = 4;
|
|
|
|
mStop = false;
|
|
mBreak = false;
|
|
mCont = false;
|
|
|
|
mMaxLen = NYQ_MAX_LEN;
|
|
|
|
// Interactive Nyquist
|
|
if (fName == NYQUIST_PROMPT_ID) {
|
|
/* i18n-hint: "Nyquist" is an embedded interpreted programming language in
|
|
Audacity, named in honor of the Swedish-American Harry Nyquist (or Nyqvist).
|
|
In the translations of this and other strings, you may transliterate the
|
|
name into another alphabet. */
|
|
mName = XO("Nyquist Prompt");
|
|
mType = EffectTypeProcess;
|
|
mOK = true;
|
|
mIsPrompt = true;
|
|
|
|
return;
|
|
}
|
|
|
|
if (fName == NYQUIST_WORKER_ID) {
|
|
// Effect spawned from Nyquist Prompt
|
|
/* i18n-hint: It is acceptable to translate this the same as for "Nyquist Prompt" */
|
|
mName = XO("Nyquist Worker");
|
|
return;
|
|
}
|
|
|
|
mFileName = fName;
|
|
mName = mFileName.GetName();
|
|
mFileModified = mFileName.GetModificationTime();
|
|
ParseFile();
|
|
}
|
|
|
|
NyquistEffect::~NyquistEffect()
|
|
{
|
|
}
|
|
|
|
// IdentInterface implementation
|
|
|
|
wxString NyquistEffect::GetPath()
|
|
{
|
|
if (mIsPrompt)
|
|
{
|
|
return NYQUIST_PROMPT_ID;
|
|
}
|
|
|
|
return mFileName.GetFullPath();
|
|
}
|
|
|
|
wxString NyquistEffect::GetSymbol()
|
|
{
|
|
if (mIsPrompt)
|
|
{
|
|
return XO("Nyquist Prompt");
|
|
}
|
|
|
|
return mName;
|
|
}
|
|
|
|
wxString NyquistEffect::GetName()
|
|
{
|
|
return GetSymbol();
|
|
}
|
|
|
|
wxString NyquistEffect::GetVendor()
|
|
{
|
|
if (mIsPrompt)
|
|
{
|
|
return _("Audacity");
|
|
}
|
|
|
|
return mAuthor;
|
|
}
|
|
|
|
wxString NyquistEffect::GetVersion()
|
|
{
|
|
return XO("n/a");
|
|
}
|
|
|
|
wxString NyquistEffect::GetDescription()
|
|
{
|
|
return mCopyright;
|
|
}
|
|
|
|
wxString NyquistEffect::ManualPage()
|
|
{
|
|
return mIsPrompt
|
|
? wxT("Nyquist_Prompt")
|
|
: mManPage;
|
|
}
|
|
|
|
wxString NyquistEffect::HelpPage()
|
|
{
|
|
wxArrayString paths = NyquistEffect::GetNyquistSearchPath();
|
|
wxString fileName;
|
|
|
|
for (size_t i = 0, cnt = paths.GetCount(); i < cnt; i++) {
|
|
fileName = wxFileName(paths[i] + wxT("/") + mHelpFile).GetFullPath();
|
|
if (wxFileExists(fileName)) {
|
|
mHelpFileExists = true;
|
|
return fileName;
|
|
}
|
|
}
|
|
return wxEmptyString;
|
|
}
|
|
|
|
// EffectIdentInterface implementation
|
|
|
|
EffectType NyquistEffect::GetType()
|
|
{
|
|
return mType;
|
|
}
|
|
|
|
wxString NyquistEffect::GetFamily()
|
|
{
|
|
return NYQUISTEFFECTS_FAMILY;
|
|
}
|
|
|
|
bool NyquistEffect::IsInteractive()
|
|
{
|
|
if (mIsPrompt)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return mControls.GetCount() != 0;
|
|
}
|
|
|
|
bool NyquistEffect::IsDefault()
|
|
{
|
|
return mIsPrompt;
|
|
}
|
|
|
|
// EffectClientInterface implementation
|
|
|
|
bool NyquistEffect::GetAutomationParameters(EffectAutomationParameters & parms)
|
|
{
|
|
if (mExternal)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (mIsPrompt)
|
|
{
|
|
parms.Write(KEY_Command, mInputCmd);
|
|
parms.Write(KEY_Version, mVersion);
|
|
|
|
return true;
|
|
}
|
|
|
|
for (size_t c = 0, cnt = mControls.GetCount(); c < cnt; c++)
|
|
{
|
|
NyqControl & ctrl = mControls[c];
|
|
double d = ctrl.val;
|
|
|
|
if (d == UNINITIALIZED_CONTROL && ctrl.type != NYQ_CTRL_STRING)
|
|
{
|
|
d = GetCtrlValue(ctrl.valStr);
|
|
}
|
|
|
|
if (ctrl.type == NYQ_CTRL_REAL || ctrl.type == NYQ_CTRL_FLOAT_TEXT)
|
|
{
|
|
parms.Write(ctrl.var, d);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_INT)
|
|
{
|
|
parms.Write(ctrl.var, (int) d);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_CHOICE)
|
|
{
|
|
wxArrayString choices = ParseChoice(ctrl);
|
|
parms.WriteEnum(ctrl.var, (int) d, choices);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_STRING)
|
|
{
|
|
parms.Write(ctrl.var, ctrl.valStr);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NyquistEffect::SetAutomationParameters(EffectAutomationParameters & parms)
|
|
{
|
|
if (mExternal)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (mIsPrompt)
|
|
{
|
|
parms.Read(KEY_Command, &mInputCmd, wxEmptyString);
|
|
parms.Read(KEY_Version, &mVersion, mVersion);
|
|
|
|
return true;
|
|
}
|
|
|
|
// First pass verifies values
|
|
for (size_t c = 0, cnt = mControls.GetCount(); c < cnt; c++)
|
|
{
|
|
NyqControl & ctrl = mControls[c];
|
|
bool good = false;
|
|
|
|
if (ctrl.type == NYQ_CTRL_REAL || ctrl.type == NYQ_CTRL_FLOAT_TEXT)
|
|
{
|
|
double val;
|
|
good = parms.Read(ctrl.var, &val) &&
|
|
val >= ctrl.low &&
|
|
val <= ctrl.high;
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_INT_TEXT)
|
|
{
|
|
int val;
|
|
good = parms.Read(ctrl.var, &val) &&
|
|
val >= ctrl.low &&
|
|
val <= ctrl.high;
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_CHOICE)
|
|
{
|
|
int val;
|
|
wxArrayString choices = ParseChoice(ctrl);
|
|
good = parms.ReadEnum(ctrl.var, &val, choices) &&
|
|
val != wxNOT_FOUND;
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_STRING)
|
|
{
|
|
wxString val;
|
|
good = parms.Read(ctrl.var, &val);
|
|
}
|
|
|
|
if (!good)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Second pass sets the variables
|
|
for (size_t c = 0, cnt = mControls.GetCount(); c < cnt; c++)
|
|
{
|
|
NyqControl & ctrl = mControls[c];
|
|
|
|
double d = ctrl.val;
|
|
if (d == UNINITIALIZED_CONTROL && ctrl.type != NYQ_CTRL_STRING)
|
|
{
|
|
d = GetCtrlValue(ctrl.valStr);
|
|
}
|
|
|
|
if (ctrl.type == NYQ_CTRL_REAL || ctrl.type == NYQ_CTRL_FLOAT_TEXT)
|
|
{
|
|
parms.Read(ctrl.var, &ctrl.val);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_INT_TEXT)
|
|
{
|
|
int val;
|
|
parms.Read(ctrl.var, &val);
|
|
ctrl.val = (double) val;
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_CHOICE)
|
|
{
|
|
int val {0};
|
|
wxArrayString choices = ParseChoice(ctrl);
|
|
parms.ReadEnum(ctrl.var, &val, choices);
|
|
ctrl.val = (double) val;
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_STRING)
|
|
{
|
|
parms.Read(ctrl.var, &ctrl.valStr);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Effect Implementation
|
|
|
|
bool NyquistEffect::Init()
|
|
{
|
|
// TODO: Document: Init() is called each time the effect is called but
|
|
// AFTER the UI (if any) has been created, so headers that affect the UI
|
|
// are only initialised at the start of the session.
|
|
|
|
// EffectType may not be defined in script, so
|
|
// reset each time we call the Nyquist Prompt.
|
|
if (mIsPrompt) {
|
|
mType = EffectTypeProcess;
|
|
mName = XO("Nyquist Prompt");
|
|
mDebugButton = true; // Debug button always enabled for Nyquist Prompt.
|
|
}
|
|
|
|
// As of Audacity 2.1.2 rc1, 'spectral' effects are allowed only if
|
|
// the selected track(s) are in a spectrogram view, and there is at
|
|
// least one frequency bound and Spectral Selection is enabled for the
|
|
// selected track(s) - (but don't apply to Nyquist Prompt).
|
|
|
|
if (!mIsPrompt && mIsSpectral) {
|
|
AudacityProject *project = GetActiveProject();
|
|
bool bAllowSpectralEditing = true;
|
|
|
|
SelectedTrackListOfKindIterator sel(Track::Wave, project->GetTracks());
|
|
for (WaveTrack *t = (WaveTrack *) sel.First(); t; t = (WaveTrack *) sel.Next()) {
|
|
if (t->GetDisplay() != WaveTrack::Spectrum ||
|
|
!(t->GetSpectrogramSettings().SpectralSelectionEnabled())) {
|
|
bAllowSpectralEditing = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bAllowSpectralEditing || ((mF0 < 0.0) && (mF1 < 0.0))) {
|
|
wxMessageBox(_("To use 'Spectral effects', enable 'Spectral Selection'\n"
|
|
"in the track Spectrogram settings and select the\n"
|
|
"frequency range for the effect to act on."),
|
|
_("Error"), wxOK | wxICON_EXCLAMATION | wxCENTRE);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!mIsPrompt && !mExternal)
|
|
{
|
|
//TODO: If we want to auto-add parameters from spectral selection,
|
|
//we will need to modify this test.
|
|
//Note that removing it stops the caching of parameter values,
|
|
//(during this session).
|
|
if (mFileName.GetModificationTime().IsLaterThan(mFileModified))
|
|
{
|
|
SaveUserPreset(GetCurrentSettingsGroup());
|
|
|
|
mMaxLen = NYQ_MAX_LEN;
|
|
ParseFile();
|
|
mFileModified = mFileName.GetModificationTime();
|
|
|
|
LoadUserPreset(GetCurrentSettingsGroup());
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NyquistEffect::CheckWhetherSkipEffect()
|
|
{
|
|
// If we're a prompt and we have controls, then we've already processed
|
|
// the audio, so skip further processing.
|
|
return (mIsPrompt && mControls.GetCount() > 0);
|
|
}
|
|
|
|
bool NyquistEffect::Process()
|
|
{
|
|
bool success = true;
|
|
mProjectChanged = false;
|
|
EffectManager & em = EffectManager::Get();
|
|
em.SetSkipStateFlag(false);
|
|
|
|
if (mExternal) {
|
|
mProgress->Hide();
|
|
}
|
|
|
|
// We must copy all the tracks, because Paste needs label tracks to ensure
|
|
// correct sync-lock group behavior when the timeline is affected; then we just want
|
|
// to operate on the selected wave tracks
|
|
CopyInputTracks(Track::All);
|
|
SelectedTrackListOfKindIterator iter(Track::Wave, mOutputTracks.get());
|
|
mCurTrack[0] = (WaveTrack *) iter.First();
|
|
mOutputTime = 0;
|
|
mCount = 0;
|
|
mProgressIn = 0;
|
|
mProgressOut = 0;
|
|
mProgressTot = 0;
|
|
mScale = (GetType() == EffectTypeProcess ? 0.5 : 1.0) / GetNumWaveGroups();
|
|
|
|
mStop = false;
|
|
mBreak = false;
|
|
mCont = false;
|
|
|
|
mTrackIndex = 0;
|
|
|
|
mNumSelectedChannels = 0;
|
|
SelectedTrackListOfKindIterator sel(Track::Wave, mOutputTracks.get());
|
|
for (WaveTrack *t = (WaveTrack *) sel.First(); t; t = (WaveTrack *) sel.Next()) {
|
|
mNumSelectedChannels++;
|
|
if (mT1 >= mT0) {
|
|
if (t->GetLinked()) {
|
|
mNumSelectedChannels++;
|
|
sel.Next();
|
|
}
|
|
}
|
|
}
|
|
|
|
mDebugOutput.Clear();
|
|
if (!mHelpFile.IsEmpty() && !mHelpFileExists) {
|
|
mDebugOutput = wxString::Format(wxT("error: File \"%s\" specified in header but not found in plug-in path.\n"), mHelpFile);
|
|
}
|
|
|
|
if (mVersion >= 4)
|
|
{
|
|
AudacityProject *project = GetActiveProject();
|
|
|
|
mProps = wxEmptyString;
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*AUDACITY* (list %d %d %d) 'VERSION)\n"), AUDACITY_VERSION, AUDACITY_RELEASE, AUDACITY_REVISION);
|
|
|
|
mProps += wxString::Format(wxT("(setf *DECIMAL-SEPARATOR* #\\%c)\n"), wxNumberFormatter::GetDecimalSeparator());
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'BASE)\n"), EscapeString(FileNames::BaseDir()).c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'DATA)\n"), EscapeString(FileNames::DataDir()).c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'HELP)\n"), EscapeString(FileNames::HtmlHelpDir().RemoveLast()).c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* \"%s\" 'TEMP)\n"), EscapeString(FileNames::TempDir()).c_str());
|
|
|
|
wxArrayString paths = NyquistEffect::GetNyquistSearchPath();
|
|
wxString list;
|
|
for (size_t i = 0, cnt = paths.GetCount(); i < cnt; i++)
|
|
{
|
|
list += wxT("\"") + EscapeString(paths[i]) + wxT("\" ");
|
|
}
|
|
list = list.RemoveLast();
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* (list %s) 'PLUGIN)\n"), list.c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-DIR* (list %s) 'PLUG-IN)\n"), list.c_str());
|
|
|
|
|
|
// Date and time:
|
|
wxDateTime now = wxDateTime::Now();
|
|
int year = now.GetYear();
|
|
int doy = now.GetDayOfYear();
|
|
int dom = now.GetDay();
|
|
// enumerated constants
|
|
wxDateTime::Month month = now.GetMonth();
|
|
wxDateTime::WeekDay day = now.GetWeekDay();
|
|
|
|
// Date/time as a list: year, day of year, hour, minute, seconds
|
|
mProps += wxString::Format(wxT("(setf *SYSTEM-TIME* (list %d %d %d %d %d))\n"),
|
|
year, doy, now.GetHour(), now.GetMinute(), now.GetSecond());
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'DATE)\n"), now.FormatDate().c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'TIME)\n"), now.FormatTime().c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'ISO-DATE)\n"), now.FormatISODate().c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'ISO-TIME)\n"), now.FormatISOTime().c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* %d 'YEAR)\n"), year);
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* %d 'DAY)\n"), dom); // day of month
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* %d 'MONTH)\n"), month);
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'MONTH-NAME)\n"), now.GetMonthName(month).c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SYSTEM-TIME* \"%s\" 'DAY-NAME)\n"), now.GetWeekDayName(day).c_str());
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'PROJECTS)\n"), (int) gAudacityProjects.size());
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* \"%s\" 'NAME)\n"), project->GetName().c_str());
|
|
|
|
TrackListIterator all(project->GetTracks());
|
|
Track *t;
|
|
int numTracks = 0;
|
|
int numWave = 0;
|
|
int numLabel = 0;
|
|
int numMidi = 0;
|
|
int numTime = 0;
|
|
wxString waveTrackList = wxT(""); // track positions of selected audio tracks.
|
|
|
|
for (t = all.First(); t; t = all.Next())
|
|
{
|
|
switch (t->GetKind())
|
|
{
|
|
case Track::Wave:
|
|
numWave++;
|
|
if (t->GetSelected()) {
|
|
waveTrackList += wxString::Format(wxT("%d "), 1 + numTracks);
|
|
}
|
|
break;
|
|
case Track::Label: numLabel++; break;
|
|
#if defined(USE_MIDI)
|
|
case Track::Note: numMidi++; break;
|
|
#endif
|
|
case Track::Time: numTime++; break;
|
|
default: break;
|
|
}
|
|
|
|
numTracks++;
|
|
if (t->GetLinked())
|
|
{
|
|
all.Next();
|
|
}
|
|
}
|
|
|
|
// We use Internat::ToString() rather than "%g" here because we
|
|
// always have to use the dot as decimal separator when giving
|
|
// numbers to Nyquist, whereas using "%g" will use the user's
|
|
// decimal separator which may be a comma in some countries.
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* (float %s) 'RATE)\n"),
|
|
Internat::ToString(project->GetRate()).c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'TRACKS)\n"), numTracks);
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'WAVETRACKS)\n"), numWave);
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'LABELTRACKS)\n"), numLabel);
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'MIDITRACKS)\n"), numMidi);
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* %d 'TIMETRACKS)\n"), numTime);
|
|
|
|
double previewLen = 6.0;
|
|
gPrefs->Read(wxT("/AudioIO/EffectsPreviewLen"), &previewLen);
|
|
mProps += wxString::Format(wxT("(putprop '*PROJECT* (float %s) 'PREVIEW-DURATION)\n"),
|
|
Internat::ToString(previewLen).c_str());
|
|
|
|
// *PREVIEWP* is true when previewing (better than relying on track view).
|
|
wxString isPreviewing = (this->IsPreviewing())? wxT("T") : wxT("NIL");
|
|
mProps += wxString::Format(wxT("(setf *PREVIEWP* %s)\n"), isPreviewing.c_str());
|
|
|
|
mProps += wxString::Format(wxT("(putprop '*SELECTION* (float %s) 'START)\n"),
|
|
Internat::ToString(mT0).c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SELECTION* (float %s) 'END)\n"),
|
|
Internat::ToString(mT1).c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SELECTION* (list %s) 'TRACKS)\n"), waveTrackList.c_str());
|
|
mProps += wxString::Format(wxT("(putprop '*SELECTION* %d 'CHANNELS)\n"), mNumSelectedChannels);
|
|
}
|
|
|
|
// Keep track of whether the current track is first selected in its sync-lock group
|
|
// (we have no idea what the length of the returned audio will be, so we have
|
|
// to handle sync-lock group behavior the "old" way).
|
|
mFirstInGroup = true;
|
|
Track *gtLast = NULL;
|
|
|
|
while (mCurTrack[0]) {
|
|
mCurNumChannels = 1;
|
|
if (mT1 >= mT0) {
|
|
if (mCurTrack[0]->GetLinked()) {
|
|
mCurNumChannels = 2;
|
|
|
|
mCurTrack[1] = (WaveTrack *)iter.Next();
|
|
if (mCurTrack[1]->GetRate() != mCurTrack[0]->GetRate()) {
|
|
wxMessageBox(_("Sorry, cannot apply effect on stereo tracks where the tracks don't match."),
|
|
wxT("Nyquist"),
|
|
wxOK | wxCENTRE, mUIParent);
|
|
success = false;
|
|
goto finish;
|
|
}
|
|
mCurStart[1] = mCurTrack[1]->TimeToLongSamples(mT0);
|
|
}
|
|
|
|
// Check whether we're in the same group as the last selected track
|
|
SyncLockedTracksIterator gIter(mOutputTracks.get());
|
|
Track *gt = gIter.StartWith(mCurTrack[0]);
|
|
mFirstInGroup = !gtLast || (gtLast != gt);
|
|
gtLast = gt;
|
|
|
|
mCurStart[0] = mCurTrack[0]->TimeToLongSamples(mT0);
|
|
auto end = mCurTrack[0]->TimeToLongSamples(mT1);
|
|
mCurLen = end - mCurStart[0];
|
|
|
|
if (mCurLen > NYQ_MAX_LEN) {
|
|
float hours = (float)NYQ_MAX_LEN / (44100 * 60 * 60);
|
|
const auto message = wxString::Format(
|
|
_("Selection too long for Nyquist code.\nMaximum allowed selection is %ld samples\n(about %.1f hours at 44100 Hz sample rate)."),
|
|
(long)NYQ_MAX_LEN, hours
|
|
);
|
|
wxMessageBox(message, _("Nyquist Error"), wxOK | wxCENTRE);
|
|
if (!mProjectChanged)
|
|
em.SetSkipStateFlag(true);
|
|
return false;
|
|
}
|
|
|
|
mCurLen = std::min(mCurLen, mMaxLen);
|
|
|
|
mProgressIn = 0.0;
|
|
mProgressOut = 0.0;
|
|
|
|
// libnyquist breaks except in LC_NUMERIC=="C".
|
|
//
|
|
// Note that we must set the locale to "C" even before calling
|
|
// nyx_init() because otherwise some effects will not work!
|
|
//
|
|
// MB: setlocale is not thread-safe. Should use uselocale()
|
|
// if available, or fix libnyquist to be locale-independent.
|
|
// See also http://bugzilla.audacityteam.org/show_bug.cgi?id=642#c9
|
|
// for further info about this thread safety question.
|
|
wxString prevlocale = wxSetlocale(LC_NUMERIC, NULL);
|
|
wxSetlocale(LC_NUMERIC, wxString(wxT("C")));
|
|
|
|
nyx_init();
|
|
nyx_set_os_callback(StaticOSCallback, (void *)this);
|
|
nyx_capture_output(StaticOutputCallback, (void *)this);
|
|
|
|
auto cleanup = finally( [&] {
|
|
nyx_capture_output(NULL, (void *)NULL);
|
|
nyx_set_os_callback(NULL, (void *)NULL);
|
|
nyx_cleanup();
|
|
} );
|
|
|
|
|
|
if (mVersion >= 4)
|
|
{
|
|
mPerTrackProps = wxEmptyString;
|
|
wxString lowHz = wxT("nil");
|
|
wxString highHz = wxT("nil");
|
|
wxString centerHz = wxT("nil");
|
|
wxString bandwidth = wxT("nil");
|
|
|
|
#if defined(EXPERIMENTAL_SPECTRAL_EDITING)
|
|
if (mF0 >= 0.0) {
|
|
lowHz.Printf(wxT("(float %s)"), Internat::ToString(mF0).c_str());
|
|
}
|
|
|
|
if (mF1 >= 0.0) {
|
|
highHz.Printf(wxT("(float %s)"), Internat::ToString(mF1).c_str());
|
|
}
|
|
|
|
if ((mF0 >= 0.0) && (mF1 >= 0.0)) {
|
|
centerHz.Printf(wxT("(float %s)"), Internat::ToString(sqrt(mF0 * mF1)).c_str());
|
|
}
|
|
|
|
if ((mF0 > 0.0) && (mF1 >= mF0)) {
|
|
// with very small values, bandwidth calculation may be inf.
|
|
// (Observed on Linux)
|
|
double bw = log(mF1 / mF0) / log(2.0);
|
|
if (!std::isinf(bw)) {
|
|
bandwidth.Printf(wxT("(float %s)"), Internat::ToString(bw).c_str());
|
|
}
|
|
}
|
|
|
|
#endif
|
|
mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'LOW-HZ)\n"), lowHz.c_str());
|
|
mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'CENTER-HZ)\n"), centerHz.c_str());
|
|
mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'HIGH-HZ)\n"), highHz.c_str());
|
|
mPerTrackProps += wxString::Format(wxT("(putprop '*SELECTION* %s 'BANDWIDTH)\n"), bandwidth.c_str());
|
|
}
|
|
|
|
success = ProcessOne();
|
|
|
|
// Reset previous locale
|
|
wxSetlocale(LC_NUMERIC, prevlocale);
|
|
|
|
if (!success) {
|
|
goto finish;
|
|
}
|
|
mProgressTot += mProgressIn + mProgressOut;
|
|
}
|
|
|
|
mCurTrack[0] = (WaveTrack *) iter.Next();
|
|
mCount += mCurNumChannels;
|
|
}
|
|
|
|
if (mOutputTime > 0.0) {
|
|
mT1 = mT0 + mOutputTime;
|
|
}
|
|
|
|
finish:
|
|
|
|
// Show debug window if trace set in plug-in header and something to show.
|
|
mDebug = (mTrace && !mDebugOutput.IsEmpty())? true : mDebug;
|
|
|
|
if (mDebug && !mRedirectOutput) {
|
|
NyquistOutputDialog dlog(mUIParent, -1,
|
|
mName,
|
|
_("Debug Output: "),
|
|
mDebugOutput.c_str());
|
|
dlog.CentreOnParent();
|
|
dlog.ShowModal();
|
|
}
|
|
|
|
ReplaceProcessedTracks(success);
|
|
|
|
if (!mProjectChanged)
|
|
em.SetSkipStateFlag(true);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool NyquistEffect::ShowInterface(wxWindow *parent, bool forceModal)
|
|
{
|
|
// Show the normal (prompt or effect) interface
|
|
bool res = Effect::ShowInterface(parent, forceModal);
|
|
|
|
// Remember if the user clicked debug
|
|
mDebug = (mUIResultID == eDebugID);
|
|
|
|
// We're done if the user clicked "Close", we are not the Nyquist Prompt,
|
|
// or the program currently loaded into the prompt doesn't have a UI.
|
|
if (!res || !mIsPrompt || mControls.GetCount() == 0)
|
|
{
|
|
return res;
|
|
}
|
|
|
|
NyquistEffect effect(NYQUIST_WORKER_ID);
|
|
|
|
effect.SetCommand(mInputCmd);
|
|
effect.mDebug = (mUIResultID == eDebugID);
|
|
|
|
SelectedRegion region(mT0, mT1);
|
|
#ifdef EXPERIMENTAL_SPECTRAL_EDITING
|
|
region.setF0(mF0);
|
|
region.setF1(mF1);
|
|
#endif
|
|
return Delegate(effect, parent, ®ion, true);
|
|
}
|
|
|
|
void NyquistEffect::PopulateOrExchange(ShuttleGui & S)
|
|
{
|
|
if (mIsPrompt)
|
|
{
|
|
BuildPromptWindow(S);
|
|
}
|
|
else
|
|
{
|
|
BuildEffectWindow(S);
|
|
}
|
|
|
|
EnableDebug(mDebugButton);
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataToWindow()
|
|
{
|
|
mUIParent->TransferDataToWindow();
|
|
|
|
bool success;
|
|
if (mIsPrompt)
|
|
{
|
|
success = TransferDataToPromptWindow();
|
|
}
|
|
else
|
|
{
|
|
success = TransferDataToEffectWindow();
|
|
}
|
|
|
|
if (success)
|
|
{
|
|
EnablePreview(mEnablePreview);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataFromWindow()
|
|
{
|
|
if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (mIsPrompt)
|
|
{
|
|
return TransferDataFromPromptWindow();
|
|
}
|
|
|
|
return TransferDataFromEffectWindow();
|
|
}
|
|
|
|
// NyquistEffect implementation
|
|
|
|
bool NyquistEffect::ProcessOne()
|
|
{
|
|
mError = false;
|
|
mFailedFileName.Clear();
|
|
|
|
nyx_rval rval;
|
|
|
|
wxString cmd;
|
|
|
|
// TODO: Document.
|
|
// Nyquist default latency is 300 ms, which is rather conservative and
|
|
// too long when playback set to ALSA (bug 570), so we'll use 100 ms like Audacity.
|
|
cmd += wxT("(snd-set-latency 0.1)");
|
|
|
|
if (mVersion >= 4) {
|
|
nyx_set_audio_name("*TRACK*");
|
|
cmd += wxT("(setf S 0.25)\n");
|
|
}
|
|
else {
|
|
nyx_set_audio_name("S");
|
|
cmd += wxT("(setf *TRACK* '*unbound*)\n");
|
|
}
|
|
|
|
if (mVersion >= 4) {
|
|
cmd += mProps;
|
|
cmd += mPerTrackProps;
|
|
|
|
// Set the track TYPE and VIEW properties
|
|
wxString type;
|
|
wxString view;
|
|
wxString bitFormat;
|
|
wxString spectralEditp;
|
|
|
|
switch (mCurTrack[0]->GetKind())
|
|
{
|
|
case Track::Wave:
|
|
type = wxT("wave");
|
|
spectralEditp = mCurTrack[0]->GetSpectrogramSettings().SpectralSelectionEnabled()? wxT("T") : wxT("NIL");
|
|
switch (((WaveTrack *) mCurTrack[0])->GetDisplay())
|
|
{
|
|
case WaveTrack::Waveform:
|
|
view = (mCurTrack[0]->GetWaveformSettings().scaleType == 0) ? wxT("\"Waveform\"") : wxT("\"Waveform (dB)\"");
|
|
break;
|
|
case WaveTrack::Spectrum:
|
|
view = wxT("\"Spectrogram\"");
|
|
break;
|
|
default: view = wxT("NIL"); break;
|
|
}
|
|
break;
|
|
#if defined(USE_MIDI)
|
|
case Track::Note:
|
|
type = wxT("midi");
|
|
view = wxT("\"Midi\"");
|
|
break;
|
|
#endif
|
|
case Track::Label:
|
|
type = wxT("label");
|
|
view = wxT("\"Label\"");
|
|
break;
|
|
case Track::Time:
|
|
type = wxT("time");
|
|
view = wxT("\"Time\"");
|
|
break;
|
|
}
|
|
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %d 'INDEX)\n"), ++mTrackIndex);
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* \"%s\" 'NAME)\n"), mCurTrack[0]->GetName().c_str());
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* \"%s\" 'TYPE)\n"), type.c_str());
|
|
// Note: "View" property may change when Audacity's choice of track views has stabilized.
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %s 'VIEW)\n"), view.c_str());
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %d 'CHANNELS)\n"), mCurNumChannels);
|
|
|
|
//NOTE: Audacity 2.1.3 True if spectral selection is enabled regardless of track view.
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %s 'SPECTRAL-EDIT-ENABLED)\n"), spectralEditp.c_str());
|
|
|
|
double startTime = 0.0;
|
|
double endTime = 0.0;
|
|
|
|
if (mCurTrack[0]->GetLinked()) {
|
|
startTime = std::min<double>(mCurTrack[0]->GetStartTime(), mCurTrack[0]->GetLink()->GetStartTime());
|
|
}
|
|
else {
|
|
startTime = mCurTrack[0]->GetStartTime();
|
|
}
|
|
|
|
if (mCurTrack[0]->GetLinked()) {
|
|
endTime = std::max<double>(mCurTrack[0]->GetEndTime(), mCurTrack[0]->GetLink()->GetEndTime());
|
|
}
|
|
else {
|
|
endTime = mCurTrack[0]->GetEndTime();
|
|
}
|
|
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'START-TIME)\n"),
|
|
Internat::ToString(startTime).c_str());
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'END-TIME)\n"),
|
|
Internat::ToString(endTime).c_str());
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'GAIN)\n"),
|
|
Internat::ToString(mCurTrack[0]->GetGain()).c_str());
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'PAN)\n"),
|
|
Internat::ToString(mCurTrack[0]->GetPan()).c_str());
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* (float %s) 'RATE)\n"),
|
|
Internat::ToString(mCurTrack[0]->GetRate()).c_str());
|
|
|
|
switch (mCurTrack[0]->GetSampleFormat())
|
|
{
|
|
case int16Sample:
|
|
bitFormat = wxT("16");
|
|
break;
|
|
case int24Sample:
|
|
bitFormat = wxT("24");
|
|
break;
|
|
case floatSample:
|
|
bitFormat = wxT("32.0");
|
|
break;
|
|
}
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %s 'FORMAT)\n"), bitFormat.c_str());
|
|
|
|
float maxPeakLevel = 0.0; // Deprecated as of 2.1.3
|
|
wxString clips, peakString, rmsString;
|
|
for (size_t i = 0; i < mCurNumChannels; i++) {
|
|
auto ca = mCurTrack[i]->SortedClipArray();
|
|
float maxPeak = 0.0;
|
|
|
|
// A list of clips for mono, or an array of lists for multi-channel.
|
|
if (mCurNumChannels > 1) {
|
|
clips += wxT("(list ");
|
|
}
|
|
// Each clip is a list (start-time, end-time)
|
|
for (const auto clip: ca) {
|
|
clips += wxString::Format(wxT("(list (float %s) (float %s))"),
|
|
Internat::ToString(clip->GetStartTime()).c_str(),
|
|
Internat::ToString(clip->GetEndTime()).c_str());
|
|
}
|
|
if (mCurNumChannels > 1) clips += wxT(" )");
|
|
|
|
float min, max;
|
|
auto pair = mCurTrack[i]->GetMinMax(mT0, mT1); // may throw
|
|
min = pair.first, max = pair.second;
|
|
maxPeak = wxMax(wxMax(fabs(min), fabs(max)), maxPeak);
|
|
maxPeakLevel = wxMax(maxPeakLevel, maxPeak);
|
|
|
|
// On Debian, NaN samples give maxPeak = 3.40282e+38 (FLT_MAX)
|
|
if (!std::isinf(maxPeak) && !std::isnan(maxPeak) && (maxPeak < FLT_MAX)) {
|
|
peakString += wxString::Format(wxT("(float %s) "), Internat::ToString(maxPeak).c_str());
|
|
} else {
|
|
peakString += wxT("nil ");
|
|
}
|
|
|
|
float rms = mCurTrack[i]->GetRMS(mT0, mT1); // may throw
|
|
if (!std::isinf(rms) && !std::isnan(rms)) {
|
|
rmsString += wxString::Format(wxT("(float %s) "), Internat::ToString(rms).c_str());
|
|
} else {
|
|
rmsString += wxT("nil ");
|
|
}
|
|
}
|
|
// A list of clips for mono, or an array of lists for multi-channel.
|
|
cmd += wxString::Format(wxT("(putprop '*TRACK* %s%s ) 'CLIPS)\n"),
|
|
(mCurNumChannels == 1) ? wxT("(list ") : wxT("(vector "),
|
|
clips.c_str());
|
|
|
|
(mCurNumChannels > 1)?
|
|
cmd += wxString::Format(wxT("(putprop '*SELECTION* (vector %s) 'PEAK)\n"), peakString) :
|
|
cmd += wxString::Format(wxT("(putprop '*SELECTION* %s 'PEAK)\n"), peakString);
|
|
|
|
if (!std::isinf(maxPeakLevel) && !std::isnan(maxPeakLevel) && (maxPeakLevel < FLT_MAX)) {
|
|
cmd += wxString::Format(wxT("(putprop '*SELECTION* (float %s) 'PEAK-LEVEL)\n"),
|
|
Internat::ToString(maxPeakLevel).c_str());
|
|
}
|
|
|
|
(mCurNumChannels > 1)?
|
|
cmd += wxString::Format(wxT("(putprop '*SELECTION* (vector %s) 'RMS)\n"), rmsString) :
|
|
cmd += wxString::Format(wxT("(putprop '*SELECTION* %s 'RMS)\n"), rmsString);
|
|
}
|
|
|
|
if (GetType() == EffectTypeGenerate) {
|
|
nyx_set_audio_params(mCurTrack[0]->GetRate(), 0);
|
|
}
|
|
else {
|
|
// UNSAFE_SAMPLE_COUNT_TRUNCATION
|
|
// Danger! Truncation of long long to long!
|
|
// Don't say we didn't warn you!
|
|
|
|
// Note mCurLen was elsewhere limited to mMaxLen, which is normally
|
|
// the greatest long value, and yet even mMaxLen may be experimentally
|
|
// increased with a nyquist comment directive.
|
|
// See the parsing of "maxlen"
|
|
|
|
auto curLen = long(mCurLen.as_long_long());
|
|
nyx_set_audio_params(mCurTrack[0]->GetRate(), curLen);
|
|
|
|
nyx_set_input_audio(StaticGetCallback, (void *)this,
|
|
(int)mCurNumChannels,
|
|
curLen, mCurTrack[0]->GetRate());
|
|
}
|
|
|
|
// Restore the Nyquist sixteenth note symbol for Generate plug-ins.
|
|
// See http://bugzilla.audacityteam.org/show_bug.cgi?id=490.
|
|
if (GetType() == EffectTypeGenerate) {
|
|
cmd += wxT("(setf s 0.25)\n");
|
|
}
|
|
|
|
if (mDebug || mTrace) {
|
|
cmd += wxT("(setf *tracenable* T)\n");
|
|
if (mExternal) {
|
|
cmd += wxT("(setf *breakenable* T)\n");
|
|
}
|
|
}
|
|
else {
|
|
// Explicitly disable backtrace and prevent values
|
|
// from being carried through to the output.
|
|
// This should be the final command before evaluating the Nyquist script.
|
|
cmd += wxT("(setf *tracenable* NIL)\n");
|
|
}
|
|
|
|
for (unsigned int j = 0; j < mControls.GetCount(); j++) {
|
|
if (mControls[j].type == NYQ_CTRL_REAL || mControls[j].type == NYQ_CTRL_FLOAT_TEXT) {
|
|
// We use Internat::ToString() rather than "%f" here because we
|
|
// always have to use the dot as decimal separator when giving
|
|
// numbers to Nyquist, whereas using "%f" will use the user's
|
|
// decimal separator which may be a comma in some countries.
|
|
cmd += wxString::Format(wxT("(setf %s %s)\n"),
|
|
mControls[j].var.c_str(),
|
|
Internat::ToString(mControls[j].val, 14).c_str());
|
|
}
|
|
else if (mControls[j].type == NYQ_CTRL_INT ||
|
|
mControls[j].type == NYQ_CTRL_INT_TEXT ||
|
|
mControls[j].type == NYQ_CTRL_CHOICE) {
|
|
cmd += wxString::Format(wxT("(setf %s %d)\n"),
|
|
mControls[j].var.c_str(),
|
|
(int)(mControls[j].val));
|
|
}
|
|
else if (mControls[j].type == NYQ_CTRL_STRING) {
|
|
cmd += wxT("(setf ");
|
|
// restrict variable names to 7-bit ASCII:
|
|
cmd += mControls[j].var.c_str();
|
|
cmd += wxT(" \"");
|
|
cmd += EscapeString(mControls[j].valStr); // unrestricted value will become quoted UTF-8
|
|
cmd += wxT("\")\n");
|
|
}
|
|
}
|
|
|
|
if (mIsSal) {
|
|
wxString str = EscapeString(mCmd);
|
|
// this is tricky: we need SAL to call main so that we can get a
|
|
// SAL traceback in the event of an error (sal-compile catches the
|
|
// error and calls sal-error-output), but SAL does not return values.
|
|
// We will catch the value in a special global aud:result and if no
|
|
// error occurs, we will grab the value with a LISP expression
|
|
str += wxT("\nset aud:result = main()\n");
|
|
|
|
if (mDebug || mTrace) {
|
|
// since we're about to evaluate SAL, remove LISP trace enable and
|
|
// break enable (which stops SAL processing) and turn on SAL stack
|
|
// trace
|
|
cmd += wxT("(setf *tracenable* nil)\n");
|
|
cmd += wxT("(setf *breakenable* nil)\n");
|
|
cmd += wxT("(setf *sal-traceback* t)\n");
|
|
}
|
|
|
|
if (mCompiler) {
|
|
cmd += wxT("(setf *sal-compiler-debug* t)\n");
|
|
}
|
|
|
|
cmd += wxT("(setf *sal-call-stack* nil)\n");
|
|
// if we do not set this here and an error occurs in main, another
|
|
// error will be raised when we try to return the value of aud:result
|
|
// which is unbound
|
|
cmd += wxT("(setf aud:result nil)\n");
|
|
cmd += wxT("(sal-compile-audacity \"") + str + wxT("\" t t nil)\n");
|
|
// Capture the value returned by main (saved in aud:result), but
|
|
// set aud:result to nil so sound results can be evaluated without
|
|
// retaining audio in memory
|
|
cmd += wxT("(prog1 aud:result (setf aud:result nil))\n");
|
|
}
|
|
else {
|
|
cmd += mCmd;
|
|
}
|
|
|
|
// Put the fetch buffers in a clean initial state
|
|
for (size_t i = 0; i < mCurNumChannels; i++)
|
|
mCurBuffer[i].Free();
|
|
|
|
// Guarantee release of memory when done
|
|
auto cleanup = finally( [&] {
|
|
for (size_t i = 0; i < mCurNumChannels; i++)
|
|
mCurBuffer[i].Free();
|
|
} );
|
|
|
|
// Evaluate the expression, which may invoke the get callback, but often does
|
|
// not, leaving that to delayed evaluation of the output sound
|
|
rval = nyx_eval_expression(cmd.mb_str(wxConvUTF8));
|
|
|
|
// If we're not showing debug window, log errors and warnings:
|
|
if (!mDebugOutput.IsEmpty() && !mDebug && !mTrace) {
|
|
/* i18n-hint: An effect "returned" a message.*/
|
|
wxLogMessage(_("\'%s\' returned:\n%s"), mName, mDebugOutput);
|
|
}
|
|
|
|
// Audacity has no idea how long Nyquist processing will take, but
|
|
// can monitor audio being returned.
|
|
// Anything other than audio should be returned almost instantly
|
|
// so notify the user that process has completed (bug 558)
|
|
if ((rval != nyx_audio) && ((mCount + mCurNumChannels) == mNumSelectedChannels)) {
|
|
if (mCurNumChannels == 1) {
|
|
TrackProgress(mCount, 1.0, _("Processing complete."));
|
|
}
|
|
else {
|
|
TrackGroupProgress(mCount, 1.0, _("Processing complete."));
|
|
}
|
|
}
|
|
|
|
if (rval == nyx_error) {
|
|
// Return value is not valid type.
|
|
// Show error in debug window if trace enabled, otherwise log.
|
|
if (mTrace) {
|
|
/* i18n-hint: "%s" is replaced by name of plug-in.*/
|
|
mDebugOutput = wxString::Format(_("nyx_error returned from %s.\n"),
|
|
mName.IsEmpty()? _("plug-in") : mName) + mDebugOutput;
|
|
mDebug = true;
|
|
return false;
|
|
}
|
|
else {
|
|
wxLogMessage(wxT("Nyquist returned nyx_error."));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (rval == nyx_string) {
|
|
wxString msg = NyquistToWxString(nyx_get_string());
|
|
if (!msg.IsEmpty()) // Not currently a documented feature, but could be useful as a No-Op.
|
|
wxMessageBox(msg, mName, wxOK | wxCENTRE, mUIParent);
|
|
|
|
// True if not process type.
|
|
// If not returning audio from process effect,
|
|
// return first reult then stop (disables preview)
|
|
// but allow all output from Nyquist Prompt.
|
|
return (GetType() != EffectTypeProcess || mIsPrompt);
|
|
}
|
|
|
|
if (rval == nyx_double) {
|
|
wxString str;
|
|
str.Printf(_("Nyquist returned the value:") + wxString(wxT(" %f")),
|
|
nyx_get_double());
|
|
wxMessageBox(str, wxT("Nyquist"),
|
|
wxOK | wxCENTRE, mUIParent);
|
|
return (GetType() != EffectTypeProcess || mIsPrompt);
|
|
}
|
|
|
|
if (rval == nyx_int) {
|
|
wxString str;
|
|
str.Printf(_("Nyquist returned the value:") + wxString(wxT(" %d")),
|
|
nyx_get_int());
|
|
wxMessageBox(str, wxT("Nyquist"),
|
|
wxOK | wxCENTRE, mUIParent);
|
|
return (GetType() != EffectTypeProcess || mIsPrompt);
|
|
}
|
|
|
|
if (rval == nyx_labels) {
|
|
mProjectChanged = true;
|
|
unsigned int numLabels = nyx_get_num_labels();
|
|
unsigned int l;
|
|
LabelTrack *ltrack = NULL;
|
|
|
|
TrackListIterator iter(mOutputTracks.get());
|
|
for (Track *t = iter.First(); t; t = iter.Next()) {
|
|
if (t->GetKind() == Track::Label) {
|
|
ltrack = (LabelTrack *)t;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ltrack) {
|
|
ltrack = static_cast<LabelTrack*>(AddToOutputTracks(mFactory->NewLabelTrack()));
|
|
}
|
|
|
|
for (l = 0; l < numLabels; l++) {
|
|
double t0, t1;
|
|
const char *str;
|
|
|
|
// PRL: to do:
|
|
// let Nyquist analyzers define more complicated selections
|
|
nyx_get_label(l, &t0, &t1, &str);
|
|
|
|
ltrack->AddLabel(SelectedRegion(t0 + mT0, t1 + mT0), UTF8CTOWX(str));
|
|
}
|
|
return (GetType() != EffectTypeProcess || mIsPrompt);
|
|
}
|
|
|
|
wxASSERT(rval == nyx_audio);
|
|
|
|
int outChannels = nyx_get_audio_num_channels();
|
|
if (outChannels > (int)mCurNumChannels) {
|
|
wxMessageBox(_("Nyquist returned too many audio channels.\n"),
|
|
wxT("Nyquist"),
|
|
wxOK | wxCENTRE, mUIParent);
|
|
return false;
|
|
}
|
|
|
|
if (outChannels == -1) {
|
|
wxMessageBox(_("Nyquist returned one audio channel as an array.\n"),
|
|
wxT("Nyquist"),
|
|
wxOK | wxCENTRE, mUIParent);
|
|
return false;
|
|
}
|
|
|
|
if (outChannels == 0) {
|
|
wxMessageBox(_("Nyquist returned an empty array.\n"),
|
|
wxT("Nyquist"),
|
|
wxOK | wxCENTRE, mUIParent);
|
|
return false;
|
|
}
|
|
|
|
std::unique_ptr<WaveTrack> outputTrack[2];
|
|
|
|
double rate = mCurTrack[0]->GetRate();
|
|
for (int i = 0; i < outChannels; i++) {
|
|
sampleFormat format = mCurTrack[i]->GetSampleFormat();
|
|
|
|
if (outChannels == (int)mCurNumChannels) {
|
|
rate = mCurTrack[i]->GetRate();
|
|
}
|
|
|
|
outputTrack[i] = mFactory->NewWaveTrack(format, rate);
|
|
|
|
// Clean the initial buffer states again for the get callbacks
|
|
// -- is this really needed?
|
|
mCurBuffer[i].Free();
|
|
}
|
|
|
|
// Now fully evaluate the sound
|
|
int success;
|
|
{
|
|
auto vr0 = valueRestorer( mOutputTrack[0], outputTrack[0].get() );
|
|
auto vr1 = valueRestorer( mOutputTrack[1], outputTrack[1].get() );
|
|
success = nyx_get_audio(StaticPutCallback, (void *)this);
|
|
}
|
|
|
|
// See if GetCallback found read errors
|
|
if (mFailedFileName.IsOk())
|
|
// re-construct an exception
|
|
// I wish I had std::exception_ptr instead
|
|
// and could re-throw any AudacityException
|
|
throw FileException{
|
|
FileException::Cause::Read, mFailedFileName };
|
|
else if (mError)
|
|
// what, then?
|
|
success = false;
|
|
|
|
if (!success)
|
|
return false;
|
|
|
|
for (int i = 0; i < outChannels; i++) {
|
|
outputTrack[i]->Flush();
|
|
mOutputTime = outputTrack[i]->GetEndTime();
|
|
|
|
if (mOutputTime <= 0) {
|
|
wxMessageBox(_("Nyquist returned nil audio.\n"),
|
|
wxT("Nyquist"),
|
|
wxOK | wxCENTRE, mUIParent);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < mCurNumChannels; i++) {
|
|
WaveTrack *out;
|
|
|
|
if (outChannels == (int)mCurNumChannels) {
|
|
out = outputTrack[i].get();
|
|
}
|
|
else {
|
|
out = outputTrack[0].get();
|
|
}
|
|
|
|
if (mMergeClips < 0) {
|
|
// Use sample counts to determine default behaviour - times will rarely be equal.
|
|
bool bMergeClips = (out->TimeToLongSamples(mT0) + out->TimeToLongSamples(mOutputTime) ==
|
|
out->TimeToLongSamples(mT1));
|
|
mCurTrack[i]->ClearAndPaste(mT0, mT1, out, mRestoreSplits, bMergeClips);
|
|
}
|
|
else {
|
|
mCurTrack[i]->ClearAndPaste(mT0, mT1, out, mRestoreSplits, mMergeClips != 0);
|
|
}
|
|
|
|
// If we were first in the group adjust non-selected group tracks
|
|
if (mFirstInGroup) {
|
|
SyncLockedTracksIterator git(mOutputTracks.get());
|
|
Track *t;
|
|
for (t = git.StartWith(mCurTrack[i]); t; t = git.Next())
|
|
{
|
|
if (!t->GetSelected() && t->IsSyncLockSelected()) {
|
|
t->SyncLockAdjust(mT1, mT0 + out->GetEndTime());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only the first channel can be first in its group
|
|
mFirstInGroup = false;
|
|
}
|
|
|
|
mProjectChanged = true;
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// NyquistEffect Implementation
|
|
// ============================================================================
|
|
|
|
wxString NyquistEffect::NyquistToWxString(const char *nyqString)
|
|
{
|
|
wxString str(nyqString, wxConvUTF8);
|
|
if (nyqString != NULL && nyqString[0] && str.IsEmpty()) {
|
|
// invalid UTF-8 string, convert as Latin-1
|
|
str = _("[Warning: Nyquist returned invalid UTF-8 string, converted here as Latin-1]");
|
|
// TODO: internationalization of strings from Nyquist effects, at least
|
|
// from those shipped with Audacity
|
|
str += LAT1CTOWX(nyqString);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
wxString NyquistEffect::EscapeString(const wxString & inStr)
|
|
{
|
|
wxString str = inStr;
|
|
|
|
str.Replace(wxT("\\"), wxT("\\\\"));
|
|
str.Replace(wxT("\""), wxT("\\\""));
|
|
|
|
return str;
|
|
}
|
|
|
|
wxArrayString NyquistEffect::ParseChoice(const NyqControl & ctrl)
|
|
{
|
|
wxArrayString choices = wxStringTokenize(ctrl.label, wxT(","));
|
|
|
|
for (size_t i = 0, cnt = choices.GetCount();i < cnt; i++)
|
|
{
|
|
choices[i] = choices[i].Trim(true).Trim(false);
|
|
}
|
|
|
|
return choices;
|
|
}
|
|
|
|
void NyquistEffect::RedirectOutput()
|
|
{
|
|
mRedirectOutput = true;
|
|
}
|
|
|
|
void NyquistEffect::SetCommand(const wxString &cmd)
|
|
{
|
|
mExternal = true;
|
|
|
|
ParseCommand(cmd);
|
|
}
|
|
|
|
void NyquistEffect::Break()
|
|
{
|
|
mBreak = true;
|
|
}
|
|
|
|
void NyquistEffect::Continue()
|
|
{
|
|
mCont = true;
|
|
}
|
|
|
|
void NyquistEffect::Stop()
|
|
{
|
|
mStop = true;
|
|
}
|
|
|
|
wxString NyquistEffect::UnQuote(const wxString &s)
|
|
{
|
|
wxString out;
|
|
int len = s.Length();
|
|
|
|
if (len >= 2 && s[0] == wxT('\"') && s[len - 1] == wxT('\"')) {
|
|
return s.Mid(1, len - 2);
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
double NyquistEffect::GetCtrlValue(const wxString &s)
|
|
{
|
|
/* For this to work correctly requires that the plug-in header is
|
|
* parsed on each run so that the correct value for "half-srate" may
|
|
* be determined.
|
|
*
|
|
AudacityProject *project = GetActiveProject();
|
|
double rate = INT_MAX;
|
|
if (project && s.IsSameAs(wxT("half-srate"), false)) {
|
|
SelectedTrackListOfKindIterator sel(Track::Wave, project->GetTracks());
|
|
for (WaveTrack *t = (WaveTrack *) sel.First(); t; t = (WaveTrack *) sel.Next()) {
|
|
rate = std::min(t->GetRate(), rate);
|
|
}
|
|
return (rate / 2.0);
|
|
}
|
|
*/
|
|
|
|
return Internat::CompatibleToDouble(s);
|
|
}
|
|
|
|
void NyquistEffect::Parse(const wxString &line)
|
|
{
|
|
wxArrayString tokens;
|
|
|
|
int i;
|
|
int len = line.Length();
|
|
bool sl = false;
|
|
bool q = false;
|
|
wxString tok = wxT("");
|
|
|
|
for (i = 1; i < len; i++) {
|
|
wxChar c = line[i];
|
|
|
|
if (c == wxT('\\')) {
|
|
sl = true;
|
|
}
|
|
else if (c == wxT('"')) {
|
|
q = !q;
|
|
}
|
|
else {
|
|
if ((!q && !sl && c == wxT(' ')) || c == wxT('\t')) {
|
|
tokens.Add(tok);
|
|
tok = wxT("");
|
|
}
|
|
else if (sl && c == wxT('n')) {
|
|
tok += wxT('\n');
|
|
}
|
|
else {
|
|
tok += c;
|
|
}
|
|
|
|
sl = false;
|
|
}
|
|
}
|
|
|
|
if (tok != wxT("")) {
|
|
tokens.Add(tok);
|
|
}
|
|
|
|
len = tokens.GetCount();
|
|
if (len < 1) {
|
|
return;
|
|
}
|
|
|
|
// Consistency decission is for "plug-in" as the correct spelling
|
|
// "plugin" (deprecated) is allowed as an undocumented convenience.
|
|
if (len == 2 && tokens[0] == wxT("nyquist") &&
|
|
(tokens[1] == wxT("plug-in") || tokens[1] == wxT("plugin"))) {
|
|
mOK = true;
|
|
return;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("type")) {
|
|
if (tokens[1] == wxT("process")) {
|
|
mType = EffectTypeProcess;
|
|
}
|
|
else if (tokens[1] == wxT("generate")) {
|
|
mType = EffectTypeGenerate;
|
|
}
|
|
else if (tokens[1] == wxT("analyze")) {
|
|
mType = EffectTypeAnalyze;
|
|
}
|
|
if (len >= 3 && tokens[2] == wxT("spectral")) {;
|
|
mIsSpectral = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (len == 2 && tokens[0] == wxT("codetype")) {
|
|
// This will stop ParseProgram() from doing a best guess as program type.
|
|
if (tokens[1] == wxT("lisp")) {
|
|
mIsSal = false;
|
|
mFoundType = true;
|
|
}
|
|
else if (tokens[1] == wxT("sal")) {
|
|
mIsSal = true;
|
|
mFoundType = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// TODO: Update documentation.
|
|
|
|
if (len >= 2 && tokens[0] == wxT("debugflags")) {
|
|
for (int i = 1; i < len; i++) {
|
|
// "trace" sets *tracenable* (LISP) or *sal-traceback* (SAL)
|
|
// and displays debug window IF there is anything to show.
|
|
if (tokens[i] == wxT("trace")) {
|
|
mTrace = true;
|
|
}
|
|
else if (tokens[i] == wxT("notrace")) {
|
|
mTrace = false;
|
|
}
|
|
else if (tokens[i] == wxT("compiler")) {
|
|
mCompiler = true;
|
|
}
|
|
else if (tokens[i] == wxT("nocompiler")) {
|
|
mCompiler = false;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// We support versions 1, 2 and 3
|
|
// (Version 2 added support for string parameters.)
|
|
// (Version 3 added support for choice parameters.)
|
|
// (Version 4 added support for project/track/selection information.)
|
|
if (len >= 2 && tokens[0] == wxT("version")) {
|
|
long v;
|
|
tokens[1].ToLong(&v);
|
|
if (v < 1 && v > 4) {
|
|
// This is an unsupported plug-in version
|
|
mOK = false;
|
|
return;
|
|
}
|
|
mVersion = (int) v;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("name")) {
|
|
mName = UnQuote(tokens[1]);
|
|
if (mName.EndsWith(wxT("...")))
|
|
{
|
|
mName = mName.RemoveLast(3);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("action")) {
|
|
mAction = UnQuote(tokens[1]);
|
|
return;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("info")) {
|
|
mInfo = UnQuote(tokens[1]);
|
|
return;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("preview")) {
|
|
if (tokens[1] == wxT("enabled") || tokens[1] == wxT("true")) {
|
|
mEnablePreview = true;
|
|
SetLinearEffectFlag(false);
|
|
}
|
|
else if (tokens[1] == wxT("linear")) {
|
|
mEnablePreview = true;
|
|
SetLinearEffectFlag(true);
|
|
}
|
|
else if (tokens[1] == wxT("selection")) {
|
|
mEnablePreview = true;
|
|
SetPreviewFullSelectionFlag(true);
|
|
}
|
|
else if (tokens[1] == wxT("disabled") || tokens[1] == wxT("false")) {
|
|
mEnablePreview = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Maximum number of samples to be processed. This can help the
|
|
// progress bar if effect does not process all of selection.
|
|
if (len >= 2 && tokens[0] == wxT("maxlen")) {
|
|
long long v; // Note that Nyquist may overflow at > 2^31 samples (bug 439)
|
|
tokens[1].ToLongLong(&v);
|
|
mMaxLen = (sampleCount) v;
|
|
}
|
|
|
|
#if defined(EXPERIMENTAL_NYQUIST_SPLIT_CONTROL)
|
|
if (len >= 2 && tokens[0] == wxT("mergeclips")) {
|
|
long v;
|
|
// -1 = auto (default), 0 = don't merge clips, 1 = do merge clips
|
|
tokens[1].ToLong(&v);
|
|
mMergeClips = v;
|
|
return;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("restoresplits")) {
|
|
long v;
|
|
// Splits are restored by default. Set to 0 to prevent.
|
|
tokens[1].ToLong(&v);
|
|
mRestoreSplits = !!v;
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (len >= 2 && tokens[0] == wxT("author")) {
|
|
mAuthor = UnQuote(tokens[1]);
|
|
return;
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("copyright")) {
|
|
mCopyright = UnQuote(tokens[1]);
|
|
return;
|
|
}
|
|
|
|
// TODO: Document.
|
|
// Page name in Audacity development manual
|
|
if (len >= 2 && tokens[0] == wxT("manpage")) {
|
|
mManPage = UnQuote(tokens[1]);
|
|
return;
|
|
}
|
|
|
|
// TODO: Document.
|
|
// Local Help file
|
|
if (len >= 2 && tokens[0] == wxT("helpfile")) {
|
|
mHelpFile = UnQuote(tokens[1]);
|
|
return;
|
|
}
|
|
|
|
// TODO: Document.
|
|
// Debug button may be disabled for release plug-ins.
|
|
if (len >= 2 && tokens[0] == wxT("debugbutton")) {
|
|
if (tokens[1] == wxT("disabled") || tokens[1] == wxT("false")) {
|
|
mDebugButton = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (len >= 6 && tokens[0] == wxT("control")) {
|
|
NyqControl ctrl;
|
|
|
|
ctrl.var = tokens[1];
|
|
ctrl.name = tokens[2];
|
|
ctrl.label = tokens[4];
|
|
ctrl.valStr = tokens[5];
|
|
ctrl.val = GetCtrlValue(ctrl.valStr);
|
|
|
|
if (tokens[3] == wxT("string")) {
|
|
ctrl.type = NYQ_CTRL_STRING;
|
|
}
|
|
else if (tokens[3] == wxT("choice")) {
|
|
ctrl.type = NYQ_CTRL_CHOICE;
|
|
}
|
|
else {
|
|
if (len < 8) {
|
|
return;
|
|
}
|
|
|
|
if ((tokens[3] == wxT("float")) ||
|
|
(tokens[3] == wxT("real"))) // Deprecated
|
|
ctrl.type = NYQ_CTRL_REAL;
|
|
else if (tokens[3] == wxT("int"))
|
|
ctrl.type = NYQ_CTRL_INT;
|
|
else if (tokens[3] == wxT("float-text"))
|
|
ctrl.type = NYQ_CTRL_FLOAT_TEXT;
|
|
else if (tokens[3] == wxT("int-text"))
|
|
ctrl.type = NYQ_CTRL_INT_TEXT;
|
|
else
|
|
{
|
|
wxString str;
|
|
str.Printf(_("Bad Nyquist 'control' type specification: '%s' in plug-in file '%s'.\nControl not created."),
|
|
tokens[3].c_str(), mFileName.GetFullPath().c_str());
|
|
|
|
// Too disturbing to show alert before Audacity frame is up.
|
|
// wxMessageBox(str, wxT("Nyquist Warning"), wxOK | wxICON_EXCLAMATION);
|
|
|
|
// Note that the AudacityApp's mLogger has not yet been created,
|
|
// so this brings up an alert box, but after the Audacity frame is up.
|
|
wxLogWarning(str);
|
|
return;
|
|
}
|
|
|
|
ctrl.lowStr = tokens[6];
|
|
if (ctrl.type == NYQ_CTRL_INT_TEXT && ctrl.lowStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl.low = INT_MIN;
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_FLOAT_TEXT && ctrl.lowStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl.low = -(FLT_MAX);
|
|
}
|
|
else {
|
|
ctrl.low = GetCtrlValue(ctrl.lowStr);
|
|
}
|
|
|
|
ctrl.highStr = tokens[7];
|
|
if (ctrl.type == NYQ_CTRL_INT_TEXT && ctrl.highStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl.high = INT_MAX;
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_FLOAT_TEXT && ctrl.highStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl.high = FLT_MAX;
|
|
}
|
|
else {
|
|
ctrl.high = GetCtrlValue(ctrl.highStr);
|
|
}
|
|
|
|
if (ctrl.high < ctrl.low) {
|
|
ctrl.high = ctrl.low;
|
|
}
|
|
|
|
if (ctrl.val < ctrl.low) {
|
|
ctrl.val = ctrl.low;
|
|
}
|
|
|
|
if (ctrl.val > ctrl.high) {
|
|
ctrl.val = ctrl.high;
|
|
}
|
|
|
|
ctrl.ticks = 1000;
|
|
if (ctrl.type == NYQ_CTRL_INT &&
|
|
(ctrl.high - ctrl.low < ctrl.ticks)) {
|
|
ctrl.ticks = (int)(ctrl.high - ctrl.low);
|
|
}
|
|
}
|
|
|
|
if( mPresetNames.Index( ctrl.var ) == wxNOT_FOUND )
|
|
{
|
|
mControls.Add(ctrl);
|
|
}
|
|
}
|
|
|
|
if (len >= 2 && tokens[0] == wxT("categories")) {
|
|
for (size_t i = 1; i < tokens.GetCount(); ++i) {
|
|
mCategories.Add(tokens[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool NyquistEffect::ParseProgram(wxInputStream & stream)
|
|
{
|
|
if (!stream.IsOk())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
wxTextInputStream pgm(stream);
|
|
|
|
mCmd = wxT("");
|
|
mIsSal = false;
|
|
mControls.Clear();
|
|
mCategories.Clear();
|
|
mIsSpectral = false;
|
|
mManPage = wxEmptyString; // If not wxEmptyString, must be a page in the Audacity manual.
|
|
mHelpFile = wxEmptyString; // If not wxEmptyString, must be a valid HTML help file.
|
|
mHelpFileExists = false;
|
|
mDebug = false;
|
|
mTrace = false;
|
|
|
|
mFoundType = false;
|
|
while (!stream.Eof() && stream.IsOk())
|
|
{
|
|
wxString line = pgm.ReadLine().Trim(false);
|
|
if (line.Length() > 1 && line[0] == wxT(';'))
|
|
{
|
|
Parse(line);
|
|
}
|
|
else if (!mFoundType && line.Length() > 0)
|
|
{
|
|
if (line[0] == wxT('(') ||
|
|
(line[0] == wxT('#') && line.Length() > 1 && line[1] == wxT('|')))
|
|
{
|
|
mIsSal = false;
|
|
mFoundType = true;
|
|
}
|
|
else if (line.Upper().Find(wxT("RETURN")) != wxNOT_FOUND)
|
|
{
|
|
mIsSal = true;
|
|
mFoundType = true;
|
|
}
|
|
}
|
|
// preserve comments so that SAL effects compile with proper line numbers
|
|
mCmd += line + wxT("\n");
|
|
}
|
|
if (!mFoundType && mIsPrompt)
|
|
{
|
|
/* i1n-hint: SAL and LISP are names for variant syntaxes for the
|
|
Nyquist programming language. Leave them, and 'return', untranslated. */
|
|
wxMessageBox(_("Your code looks like SAL syntax, but there is no \'return\' statement.\n\
|
|
For SAL, use a return statement such as:\n\treturn *track* * 0.1\n\
|
|
or for LISP, begin with an open parenthesis such as:\n\t(mult *track* 0.1)\n ."),
|
|
_("Error in Nyquist code"),
|
|
wxOK | wxCENTRE);
|
|
return false;
|
|
// Else just throw it at Nyquist to see what happens
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void NyquistEffect::ParseFile()
|
|
{
|
|
wxFileInputStream stream(mFileName.GetFullPath());
|
|
|
|
ParseProgram(stream);
|
|
}
|
|
|
|
bool NyquistEffect::ParseCommand(const wxString & cmd)
|
|
{
|
|
mEnablePreview = true;
|
|
|
|
wxStringInputStream stream(cmd + wxT(" "));
|
|
|
|
return ParseProgram(stream);
|
|
}
|
|
|
|
int NyquistEffect::StaticGetCallback(float *buffer, int channel,
|
|
long start, long len, long totlen,
|
|
void *userdata)
|
|
{
|
|
NyquistEffect *This = (NyquistEffect *)userdata;
|
|
return This->GetCallback(buffer, channel, start, len, totlen);
|
|
}
|
|
|
|
int NyquistEffect::GetCallback(float *buffer, int ch,
|
|
long start, long len, long WXUNUSED(totlen))
|
|
{
|
|
if (mCurBuffer[ch].ptr()) {
|
|
if ((mCurStart[ch] + start) < mCurBufferStart[ch] ||
|
|
(mCurStart[ch] + start)+len >
|
|
mCurBufferStart[ch]+mCurBufferLen[ch]) {
|
|
mCurBuffer[ch].Free();
|
|
}
|
|
}
|
|
|
|
if (!mCurBuffer[ch].ptr()) {
|
|
mCurBufferStart[ch] = (mCurStart[ch] + start);
|
|
mCurBufferLen[ch] = mCurTrack[ch]->GetBestBlockSize(mCurBufferStart[ch]);
|
|
|
|
if (mCurBufferLen[ch] < (size_t) len) {
|
|
mCurBufferLen[ch] = mCurTrack[ch]->GetIdealBlockSize();
|
|
}
|
|
|
|
mCurBufferLen[ch] =
|
|
limitSampleBufferSize( mCurBufferLen[ch],
|
|
mCurStart[ch] + mCurLen - mCurBufferStart[ch] );
|
|
|
|
mCurBuffer[ch].Allocate(mCurBufferLen[ch], floatSample);
|
|
try {
|
|
mCurTrack[ch]->Get(
|
|
mCurBuffer[ch].ptr(), floatSample,
|
|
mCurBufferStart[ch], mCurBufferLen[ch]);
|
|
}
|
|
catch ( const FileException& e ) {
|
|
if ( e.cause == FileException::Cause::Read )
|
|
mFailedFileName = e.fileName;
|
|
mError = true;
|
|
return -1;
|
|
}
|
|
catch ( ... ) {
|
|
mError = true;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// We have guaranteed above that this is nonnegative and bounded by
|
|
// mCurBufferLen[ch]:
|
|
auto offset = ( mCurStart[ch] + start - mCurBufferStart[ch] ).as_size_t();
|
|
CopySamples(mCurBuffer[ch].ptr() + offset*SAMPLE_SIZE(floatSample), floatSample,
|
|
(samplePtr)buffer, floatSample,
|
|
len);
|
|
|
|
if (ch == 0) {
|
|
double progress = mScale *
|
|
( (start+len)/ mCurLen.as_double() );
|
|
|
|
if (progress > mProgressIn) {
|
|
mProgressIn = progress;
|
|
}
|
|
|
|
if (TotalProgress(mProgressIn+mProgressOut+mProgressTot)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int NyquistEffect::StaticPutCallback(float *buffer, int channel,
|
|
long start, long len, long totlen,
|
|
void *userdata)
|
|
{
|
|
NyquistEffect *This = (NyquistEffect *)userdata;
|
|
return This->PutCallback(buffer, channel, start, len, totlen);
|
|
}
|
|
|
|
int NyquistEffect::PutCallback(float *buffer, int channel,
|
|
long start, long len, long totlen)
|
|
{
|
|
// Don't let C++ exceptions propagate through the Nyquist library
|
|
return GuardedCall<int>( [&] {
|
|
if (channel == 0) {
|
|
double progress = mScale*((float)(start+len)/totlen);
|
|
|
|
if (progress > mProgressOut) {
|
|
mProgressOut = progress;
|
|
}
|
|
|
|
if (TotalProgress(mProgressIn+mProgressOut+mProgressTot)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
mOutputTrack[channel]->Append((samplePtr)buffer, floatSample, len);
|
|
|
|
return 0; // success
|
|
}, MakeSimpleGuard( -1 ) ); // translate all exceptions into failure
|
|
}
|
|
|
|
void NyquistEffect::StaticOutputCallback(int c, void *This)
|
|
{
|
|
((NyquistEffect *)This)->OutputCallback(c);
|
|
}
|
|
|
|
void NyquistEffect::OutputCallback(int c)
|
|
{
|
|
// Always collect Nyquist error messages for normal plug-ins
|
|
if (!mRedirectOutput) {
|
|
mDebugOutput += (char)c;
|
|
return;
|
|
}
|
|
|
|
std::cout << (char)c;
|
|
}
|
|
|
|
void NyquistEffect::StaticOSCallback(void *This)
|
|
{
|
|
((NyquistEffect *)This)->OSCallback();
|
|
}
|
|
|
|
void NyquistEffect::OSCallback()
|
|
{
|
|
if (mStop) {
|
|
mStop = false;
|
|
nyx_stop();
|
|
}
|
|
else if (mBreak) {
|
|
mBreak = false;
|
|
nyx_break();
|
|
}
|
|
else if (mCont) {
|
|
mCont = false;
|
|
nyx_continue();
|
|
}
|
|
|
|
// LLL: STF figured out that yielding while the effect is being applied
|
|
// produces an EXTREME slowdown. It appears that yielding is not
|
|
// really necessary on Linux and Windows.
|
|
//
|
|
// However, on the Mac, the spinning cursor appears during longer
|
|
// Nyquist processing and that may cause the user to think Audacity
|
|
// has crashed or hung. In addition, yielding or not on the Mac
|
|
// doesn't seem to make much of a difference in execution time.
|
|
//
|
|
// So, yielding on the Mac only...
|
|
#if defined(__WXMAC__)
|
|
wxYieldIfNeeded();
|
|
#endif
|
|
}
|
|
|
|
wxArrayString NyquistEffect::GetNyquistSearchPath()
|
|
{
|
|
wxArrayString audacityPathList = wxGetApp().audacityPathList;
|
|
wxArrayString pathList;
|
|
|
|
for (size_t i = 0; i < audacityPathList.GetCount(); i++)
|
|
{
|
|
wxString prefix = audacityPathList[i] + wxFILE_SEP_PATH;
|
|
wxGetApp().AddUniquePathToPathList(prefix + wxT("nyquist"), pathList);
|
|
wxGetApp().AddUniquePathToPathList(prefix + wxT("plugins"), pathList);
|
|
wxGetApp().AddUniquePathToPathList(prefix + wxT("plug-ins"), pathList);
|
|
}
|
|
pathList.Add(FileNames::PlugInDir());
|
|
|
|
return pathList;
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataToPromptWindow()
|
|
{
|
|
mCommandText->ChangeValue(mInputCmd);
|
|
mVersionCheckBox->SetValue(mVersion <= 3);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataToEffectWindow()
|
|
{
|
|
for (size_t i = 0, cnt = mControls.GetCount(); i < cnt; i++)
|
|
{
|
|
NyqControl & ctrl = mControls[i];
|
|
|
|
if (ctrl.type == NYQ_CTRL_CHOICE)
|
|
{
|
|
wxArrayString choices = ParseChoice(ctrl);
|
|
|
|
int val = (int)ctrl.val;
|
|
if (val < 0 || val >= (int)choices.GetCount())
|
|
{
|
|
val = 0;
|
|
}
|
|
|
|
wxChoice *c = (wxChoice *) mUIParent->FindWindow(ID_Choice + i);
|
|
c->SetSelection(val);
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_REAL)
|
|
{
|
|
// wxTextCtrls are handled by the validators
|
|
double range = ctrl.high - ctrl.low;
|
|
int val = (int)(0.5 + ctrl.ticks * (ctrl.val - ctrl.low) / range);
|
|
wxSlider *s = (wxSlider *) mUIParent->FindWindow(ID_Slider + i);
|
|
s->SetValue(val);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataFromPromptWindow()
|
|
{
|
|
mInputCmd = mCommandText->GetValue();
|
|
mVersion = mVersionCheckBox->GetValue() ? 3 : 4;
|
|
|
|
return ParseCommand(mInputCmd);
|
|
}
|
|
|
|
bool NyquistEffect::TransferDataFromEffectWindow()
|
|
{
|
|
if (mControls.GetCount() == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
for (unsigned int i = 0; i < mControls.GetCount(); i++)
|
|
{
|
|
NyqControl *ctrl = &mControls[i];
|
|
|
|
if (ctrl->type == NYQ_CTRL_STRING)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (ctrl->val == UNINITIALIZED_CONTROL)
|
|
{
|
|
ctrl->val = GetCtrlValue(ctrl->valStr);
|
|
}
|
|
|
|
if (ctrl->type == NYQ_CTRL_CHOICE)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (ctrl->type == NYQ_CTRL_INT_TEXT && ctrl->lowStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl->low = INT_MIN;
|
|
}
|
|
else if (ctrl->type == NYQ_CTRL_FLOAT_TEXT && ctrl->lowStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl->low = -(FLT_MAX);
|
|
}
|
|
else {
|
|
ctrl->low = GetCtrlValue(ctrl->lowStr);
|
|
}
|
|
|
|
if (ctrl->type == NYQ_CTRL_INT_TEXT && ctrl->highStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl->high = INT_MAX;
|
|
}
|
|
else if (ctrl->type == NYQ_CTRL_FLOAT_TEXT && ctrl->highStr.IsSameAs(wxT("nil"), false)) {
|
|
ctrl->high = FLT_MAX;
|
|
}
|
|
else {
|
|
ctrl->high = GetCtrlValue(ctrl->highStr);
|
|
}
|
|
|
|
if (ctrl->high < ctrl->low)
|
|
{
|
|
ctrl->high = ctrl->low + 1;
|
|
}
|
|
|
|
if (ctrl->val < ctrl->low)
|
|
{
|
|
ctrl->val = ctrl->low;
|
|
}
|
|
|
|
if (ctrl->val > ctrl->high)
|
|
{
|
|
ctrl->val = ctrl->high;
|
|
}
|
|
|
|
ctrl->ticks = 1000;
|
|
if (ctrl->type == NYQ_CTRL_INT &&
|
|
(ctrl->high - ctrl->low < ctrl->ticks))
|
|
{
|
|
ctrl->ticks = (int)(ctrl->high - ctrl->low);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void NyquistEffect::BuildPromptWindow(ShuttleGui & S)
|
|
{
|
|
S.StartVerticalLay();
|
|
{
|
|
S.StartMultiColumn(3, wxEXPAND);
|
|
{
|
|
S.SetStretchyCol(1);
|
|
|
|
S.AddVariableText(_("Enter Nyquist Command: "));
|
|
|
|
S.AddSpace(1, 1);
|
|
|
|
mVersionCheckBox = S.AddCheckBox(_("&Use legacy (version 3) syntax."),
|
|
(mVersion == 3) ? wxT("true") : wxT("false"));
|
|
}
|
|
S.EndMultiColumn();
|
|
|
|
S.StartHorizontalLay(wxEXPAND, 1);
|
|
{
|
|
mCommandText = S.AddTextWindow(wxT(""));
|
|
mCommandText->SetMinSize(wxSize(500, 200));
|
|
}
|
|
S.EndHorizontalLay();
|
|
|
|
S.StartHorizontalLay(wxALIGN_CENTER, 0);
|
|
{
|
|
S.Id(ID_Load).AddButton(_("&Load"));
|
|
S.Id(ID_Save).AddButton(_("&Save"));
|
|
}
|
|
S.EndHorizontalLay();
|
|
}
|
|
S.EndVerticalLay();
|
|
|
|
mCommandText->SetFocus();
|
|
}
|
|
|
|
void NyquistEffect::BuildEffectWindow(ShuttleGui & S)
|
|
{
|
|
S.SetStyle(wxVSCROLL | wxTAB_TRAVERSAL);
|
|
wxScrolledWindow *scroller = S.StartScroller(2);
|
|
{
|
|
S.StartMultiColumn(4);
|
|
{
|
|
for (size_t i = 0; i < mControls.GetCount(); i++)
|
|
{
|
|
NyqControl & ctrl = mControls[i];
|
|
|
|
S.AddPrompt(ctrl.name + wxT(":"));
|
|
|
|
if (ctrl.type == NYQ_CTRL_STRING)
|
|
{
|
|
S.AddSpace(10, 10);
|
|
|
|
wxTextCtrl *item = S.Id(ID_Text + i).AddTextBox(wxT(""), wxT(""), 12);
|
|
item->SetValidator(wxGenericValidator(&ctrl.valStr));
|
|
}
|
|
else if (ctrl.type == NYQ_CTRL_CHOICE)
|
|
{
|
|
S.AddSpace(10, 10);
|
|
|
|
wxArrayString choices = wxStringTokenize(ctrl.label, wxT(","));
|
|
S.Id(ID_Choice + i).AddChoice(wxT(""), wxT(""), &choices);
|
|
}
|
|
else
|
|
{
|
|
// Integer or Real
|
|
if (ctrl.type == NYQ_CTRL_INT_TEXT || ctrl.type == NYQ_CTRL_FLOAT_TEXT)
|
|
{
|
|
S.AddSpace(10, 10);
|
|
}
|
|
|
|
wxTextCtrl *item = S.Id(ID_Text+i).AddTextBox(wxT(""), wxT(""),
|
|
(ctrl.type == NYQ_CTRL_INT_TEXT ||
|
|
ctrl.type == NYQ_CTRL_FLOAT_TEXT) ? 25 : 12);
|
|
|
|
double range = ctrl.high - ctrl.low;
|
|
|
|
if (ctrl.type == NYQ_CTRL_REAL || ctrl.type == NYQ_CTRL_FLOAT_TEXT)
|
|
{
|
|
// > 12 decimal places can cause rounding errors in display.
|
|
FloatingPointValidator<double> vld(12, &ctrl.val);
|
|
vld.SetRange(ctrl.low, ctrl.high);
|
|
|
|
// Set number of decimal places
|
|
int style = range < 10 ? NUM_VAL_THREE_TRAILING_ZEROES :
|
|
range < 100 ? NUM_VAL_TWO_TRAILING_ZEROES :
|
|
NUM_VAL_ONE_TRAILING_ZERO;
|
|
vld.SetStyle(style);
|
|
|
|
item->SetValidator(vld);
|
|
}
|
|
else
|
|
{
|
|
IntegerValidator<double> vld(&ctrl.val);
|
|
vld.SetRange((int) ctrl.low, (int) ctrl.high);
|
|
item->SetValidator(vld);
|
|
}
|
|
|
|
if (ctrl.type == NYQ_CTRL_INT || ctrl.type == NYQ_CTRL_REAL)
|
|
{
|
|
S.SetStyle(wxSL_HORIZONTAL);
|
|
S.Id(ID_Slider + i).AddSlider(wxT(""), 0, ctrl.ticks, 0);
|
|
S.SetSizeHints(150, -1);
|
|
}
|
|
}
|
|
|
|
if (ctrl.type == NYQ_CTRL_CHOICE || ctrl.label.IsEmpty())
|
|
{
|
|
S.AddSpace(10, 10);
|
|
}
|
|
else
|
|
{
|
|
S.AddUnits(ctrl.label);
|
|
}
|
|
}
|
|
}
|
|
S.EndMultiColumn();
|
|
}
|
|
S.EndScroller();
|
|
|
|
scroller->SetScrollRate(0, 20);
|
|
|
|
// This fools NVDA into not saying "Panel" when the dialog gets focus
|
|
scroller->SetName(wxT("\a"));
|
|
scroller->SetLabel(wxT("\a"));
|
|
}
|
|
|
|
// NyquistEffect implementation
|
|
|
|
bool NyquistEffect::IsOk()
|
|
{
|
|
return mOK;
|
|
}
|
|
|
|
void NyquistEffect::OnLoad(wxCommandEvent & WXUNUSED(evt))
|
|
{
|
|
if (mCommandText->IsModified())
|
|
{
|
|
if (wxMessageBox(_("Current program has been modified.\nDiscard changes?"),
|
|
GetName(),
|
|
wxYES_NO) == wxNO)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
FileDialog dlog(mUIParent,
|
|
_("Load Nyquist script"),
|
|
mFileName.GetPath(),
|
|
wxEmptyString,
|
|
_("Nyquist scripts (*.ny)|*.ny|Lisp scripts (*.lsp)|*.lsp|Text files (*.txt)|*.txt|All files|*"),
|
|
wxFD_OPEN | wxRESIZE_BORDER);
|
|
|
|
if (dlog.ShowModal() != wxID_OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
mFileName = dlog.GetPath();
|
|
|
|
if (!mCommandText->LoadFile(mFileName.GetFullPath()))
|
|
{
|
|
wxMessageBox(_("File could not be loaded"), GetName());
|
|
}
|
|
}
|
|
|
|
void NyquistEffect::OnSave(wxCommandEvent & WXUNUSED(evt))
|
|
{
|
|
FileDialog dlog(mUIParent,
|
|
_("Save Nyquist script"),
|
|
mFileName.GetPath(),
|
|
mFileName.GetFullName(),
|
|
_("Nyquist scripts (*.ny)|*.ny|Lisp scripts (*.lsp)|*.lsp|All files|*"),
|
|
wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxRESIZE_BORDER);
|
|
|
|
if (dlog.ShowModal() != wxID_OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
mFileName = dlog.GetPath();
|
|
|
|
if (!mCommandText->SaveFile(mFileName.GetFullPath()))
|
|
{
|
|
wxMessageBox(_("File could not be saved"), GetName());
|
|
}
|
|
}
|
|
|
|
void NyquistEffect::OnSlider(wxCommandEvent & evt)
|
|
{
|
|
int i = evt.GetId() - ID_Slider;
|
|
NyqControl & ctrl = mControls[i];
|
|
|
|
int val = evt.GetInt();
|
|
double range = ctrl.high - ctrl.low;
|
|
double newVal = (val / (double)ctrl.ticks) * range + ctrl.low;
|
|
|
|
// Determine precision for displayed number
|
|
int precision = range < 1.0 ? 3 :
|
|
range < 10.0 ? 2 :
|
|
range < 100.0 ? 1 :
|
|
0;
|
|
|
|
// If the value is at least one tick different from the current value
|
|
// change it (this prevents changes from manually entered values unless
|
|
// the slider actually moved)
|
|
if (fabs(newVal - ctrl.val) >= (1 / (double)ctrl.ticks) * range &&
|
|
fabs(newVal - ctrl.val) >= pow(0.1, precision) / 2)
|
|
{
|
|
// First round to the appropriate precision
|
|
newVal *= pow(10.0, precision);
|
|
newVal = floor(newVal + 0.5);
|
|
newVal /= pow(10.0, precision);
|
|
|
|
ctrl.val = newVal;
|
|
|
|
mUIParent->FindWindow(ID_Text + i)->GetValidator()->TransferToWindow();
|
|
}
|
|
}
|
|
|
|
void NyquistEffect::OnChoice(wxCommandEvent & evt)
|
|
{
|
|
mControls[evt.GetId() - ID_Choice].val = (double) evt.GetInt();
|
|
}
|
|
|
|
void NyquistEffect::OnText(wxCommandEvent & evt)
|
|
{
|
|
int i = evt.GetId() - ID_Text;
|
|
|
|
NyqControl & ctrl = mControls[i];
|
|
|
|
if (wxDynamicCast(evt.GetEventObject(), wxWindow)->GetValidator()->TransferFromWindow())
|
|
{
|
|
if (ctrl.type == NYQ_CTRL_REAL || ctrl.type == NYQ_CTRL_INT)
|
|
{
|
|
int pos = (int)floor((ctrl.val - ctrl.low) /
|
|
(ctrl.high - ctrl.low) * ctrl.ticks + 0.5);
|
|
|
|
wxSlider *slider = (wxSlider *)mUIParent->FindWindow(ID_Slider + i);
|
|
slider->SetValue(pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// NyquistOutputDialog
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
BEGIN_EVENT_TABLE(NyquistOutputDialog, wxDialogWrapper)
|
|
EVT_BUTTON(wxID_OK, NyquistOutputDialog::OnOk)
|
|
END_EVENT_TABLE()
|
|
|
|
NyquistOutputDialog::NyquistOutputDialog(wxWindow * parent, wxWindowID id,
|
|
const wxString & title,
|
|
const wxString & prompt,
|
|
const wxString &message)
|
|
: wxDialogWrapper{ parent, id, title, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER }
|
|
{
|
|
SetName(GetTitle());
|
|
|
|
wxBoxSizer *mainSizer;
|
|
{
|
|
auto uMainSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
|
|
mainSizer = uMainSizer.get();
|
|
wxButton *button;
|
|
wxControl *item;
|
|
|
|
item = safenew wxStaticText(this, -1, prompt);
|
|
item->SetName(prompt); // fix for bug 577 (NVDA/Narrator screen readers do not read static text in dialogs)
|
|
mainSizer->Add(item, 0, wxALIGN_LEFT | wxLEFT | wxTOP | wxRIGHT, 10);
|
|
|
|
// TODO: use ShowInfoDialog() instead.
|
|
// Beware this dialog MUST work with screen readers.
|
|
item = safenew wxTextCtrl(this, -1, message,
|
|
wxDefaultPosition, wxSize(480, 250),
|
|
wxTE_MULTILINE | wxTE_READONLY);
|
|
mainSizer->Add(item, 1, wxEXPAND | wxALL, 10);
|
|
|
|
{
|
|
auto hSizer = std::make_unique<wxBoxSizer>(wxHORIZONTAL);
|
|
|
|
/* i18n-hint: In most languages OK is to be translated as OK. It appears on a button.*/
|
|
button = safenew wxButton(this, wxID_OK, _("OK"));
|
|
button->SetDefault();
|
|
hSizer->Add(button, 0, wxALIGN_CENTRE | wxALL, 5);
|
|
|
|
mainSizer->Add(hSizer.release(), 0, wxALIGN_CENTRE | wxLEFT | wxBOTTOM | wxRIGHT, 5);
|
|
}
|
|
|
|
SetAutoLayout(true);
|
|
SetSizer(uMainSizer.release());
|
|
}
|
|
|
|
mainSizer->Fit(this);
|
|
mainSizer->SetSizeHints(this);
|
|
}
|
|
|
|
// ============================================================================
|
|
// NyquistOutputDialog implementation
|
|
// ============================================================================
|
|
|
|
void NyquistOutputDialog::OnOk(wxCommandEvent & /* event */)
|
|
{
|
|
EndModal(wxID_OK);
|
|
}
|
|
|