/**********************************************************************

  Audacity: A Digital Audio Editor

  LadspaEffect.cpp

  Dominic Mazzoni

  This class implements a LADSPA Plug-in effect.

*******************************************************************//**

\class LadspaEffect
\brief An Effect that calls up a LADSPA plug in, i.e. many possible
effects from this one class.

*//****************************************************************//**

\class LadspaEffectDialog
\brief Dialog used with Effect

*//*******************************************************************/


#include "../../Audacity.h"
#include "LadspaEffect.h"       // This class's header file

#include <float.h>

#if !defined(__WXMSW__)
#include <dlfcn.h>

#ifndef RTLD_DEEPBIND
#define RTLD_DEEPBIND 0
#endif
#endif

#include <wx/setup.h> // for wxUSE_* macros
#include <wx/wxprec.h>
#include <wx/button.h>
#include <wx/checkbox.h>
#include <wx/dcbuffer.h>
#include <wx/dcclient.h>
#include <wx/filename.h>
#include <wx/log.h>
#include <wx/menu.h>
#include <wx/sizer.h>
#include <wx/slider.h>
#include <wx/statbox.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/tokenzr.h>
#include <wx/intl.h>
#include <wx/scrolwin.h>
#include <wx/version.h>

#include "../../FileNames.h"
#include "../../ShuttleGui.h"
#include "../../widgets/NumericTextCtrl.h"
#include "../../widgets/valnum.h"
#include "../../widgets/wxPanelWrapper.h"

#if wxUSE_ACCESSIBILITY
#include "../../widgets/WindowAccessible.h"
#endif

// ============================================================================
// List of effects that ship with Audacity.  These will be autoregistered.
// ============================================================================
const static wxChar *kShippedEffects[] =
{
   wxT("sc4_1882.dll"),
};

// ============================================================================
// Module registration entry point
//
// This is the symbol that Audacity looks for when the module is built as a
// dynamic library.
//
// When the module is builtin to Audacity, we use the same function, but it is
// declared static so as not to clash with other builtin modules.
// ============================================================================
DECLARE_MODULE_ENTRY(AudacityModule)
{
   // Create and register the importer
   // Trust the module manager not to leak this
   return safenew LadspaEffectsModule(moduleManager, path);
}

// ============================================================================
// Register this as a builtin module
// ============================================================================
DECLARE_BUILTIN_MODULE(LadspaBuiltin);

///////////////////////////////////////////////////////////////////////////////
//
// LadspaEffectsModule
//
///////////////////////////////////////////////////////////////////////////////

LadspaEffectsModule::LadspaEffectsModule(ModuleManagerInterface *moduleManager,
                                     const wxString *path)
{
   mModMan = moduleManager;
   if (path)
   {
      mPath = *path;
   }
}

LadspaEffectsModule::~LadspaEffectsModule()
{
}

// ============================================================================
// ComponentInterface implementation
// ============================================================================

PluginPath LadspaEffectsModule::GetPath()
{
   return mPath;
}

ComponentInterfaceSymbol LadspaEffectsModule::GetSymbol()
{
   /* i18n-hint: abbreviates "Linux Audio Developer's Simple Plugin API"
      (Application programming interface)
    */
   return XO("LADSPA Effects");
}

VendorSymbol LadspaEffectsModule::GetVendor()
{
   return XO("The Audacity Team");
}

wxString LadspaEffectsModule::GetVersion()
{
   // This "may" be different if this were to be maintained as a separate DLL
   return LADSPAEFFECTS_VERSION;
}

TranslatableString LadspaEffectsModule::GetDescription()
{
   return XO("Provides LADSPA Effects");
}

// ============================================================================
// ModuleInterface implementation
// ============================================================================

bool LadspaEffectsModule::Initialize()
{
   // Nothing to do here
   return true;
}

void LadspaEffectsModule::Terminate()
{
   // Nothing to do here
   return;
}

EffectFamilySymbol LadspaEffectsModule::GetOptionalFamilySymbol()
{
#if USE_LADSPA
   return LADSPAEFFECTS_FAMILY;
#else
   return {};
#endif
}

const FileExtensions &LadspaEffectsModule::GetFileExtensions()
{
   static FileExtensions result{{

#ifdef __WXMSW__

      _T("dll")

#else

      _T("so")

   #ifdef __WXMAC__
   // Is it correct that these are candidate plug-in files too for macOs?
      , _T("dylib")
   #endif

#endif

   }};
   return result;
}

FilePath LadspaEffectsModule::InstallPath()
{
   // To do: better choice
   return FileNames::PlugInDir();
}

bool LadspaEffectsModule::AutoRegisterPlugins(PluginManagerInterface & pm)
{
   // Autoregister effects that we "think" are ones that have been shipped with
   // Audacity.  A little simplistic, but it should suffice for now.
   auto pathList = GetSearchPaths();
   FilePaths files;
   TranslatableString ignoredErrMsg;

   for (int i = 0; i < (int)WXSIZEOF(kShippedEffects); i++)
   {
      files.clear();
      pm.FindFilesInPathList(kShippedEffects[i], pathList, files);
      for (size_t j = 0, cnt = files.size(); j < cnt; j++)
      {
         if (!pm.IsPluginRegistered(files[j]))
         {
            // No checking for error ?
            DiscoverPluginsAtPath(files[j], ignoredErrMsg,
               PluginManagerInterface::DefaultRegistrationCallback);
         }
      }
   }

   // We still want to be called during the normal registration process
   return false;
}

PluginPaths LadspaEffectsModule::FindPluginPaths(PluginManagerInterface & pm)
{
   auto pathList = GetSearchPaths();
   FilePaths files;

#if defined(__WXMAC__)

   // Recursively scan for all shared objects
   pm.FindFilesInPathList(wxT("*.so"), pathList, files, true);

#elif defined(__WXMSW__)

   // Recursively scan for all DLLs
   pm.FindFilesInPathList(wxT("*.dll"), pathList, files, true);

#else
   
   // Recursively scan for all shared objects
   pm.FindFilesInPathList(wxT("*.so"), pathList, files, true);

#endif

   return { files.begin(), files.end() };
}

unsigned LadspaEffectsModule::DiscoverPluginsAtPath(
   const PluginPath & path, TranslatableString &errMsg,
   const RegistrationCallback &callback)
{
   errMsg = {};
   // Since we now have builtin VST support, ignore the VST bridge as it
   // causes duplicate menu entries to appear.
   wxFileName ff(path);
   if (ff.GetName().CmpNoCase(wxT("vst-bridge")) == 0) {
      errMsg = XO("Audacity no longer uses vst-bridge");
      return 0;
   }

   // As a courtesy to some plug-ins that might be bridges to
   // open other plug-ins, we set the current working
   // directory to be the plug-in's directory.
   wxString envpath;
   bool hadpath = wxGetEnv(wxT("PATH"), &envpath);
   wxSetEnv(wxT("PATH"), ff.GetPath() + wxFILE_SEP_PATH + envpath);
   wxString saveOldCWD = ff.GetCwd();
   ff.SetCwd();

   int index = 0;
   int nLoaded = 0;
   LADSPA_Descriptor_Function mainFn = NULL;
#if defined(__WXMSW__)
   wxDynamicLibrary lib;
   if (lib.Load(path, wxDL_NOW)) {
      wxLogNull logNo;

      mainFn = (LADSPA_Descriptor_Function) lib.GetSymbol(wxT("ladspa_descriptor"));
#else
   void *lib = dlopen((const char *)path.ToUTF8(), RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND);
   if (lib) {
      mainFn = (LADSPA_Descriptor_Function) dlsym(lib, "ladspa_descriptor");
#endif

      if (mainFn) {
         const LADSPA_Descriptor *data;

         for (data = mainFn(index); data; data = mainFn(++index)) {
            LadspaEffect effect(path, index);
            if (effect.SetHost(NULL)) {
               ++nLoaded;
               if (callback)
                  callback( this, &effect );
            }
            else
               errMsg = XO("Could not load the library");
         }
      }
   }
   else
      errMsg = XO("Could not load the library");

#if defined(__WXMSW__)
   if (lib.IsLoaded()) {
      // PRL:  I suspect Bug1257 -- Crash when enabling Amplio2 -- is the fault of a timing-
      // dependent multi-threading bug in the Amplio2 library itself, in case the unload of the .dll
      // comes too soon after the load.  I saw the bug in Release builds but not Debug.
      // A sleep of even 1 ms was enough to fix the problem for me, but let's be even more generous.
      ::wxMilliSleep(10);
      lib.Unload();
   }
#else
   if (lib) {
      dlclose(lib);
   }
#endif

   wxSetWorkingDirectory(saveOldCWD);
   hadpath ? wxSetEnv(wxT("PATH"), envpath) : wxUnsetEnv(wxT("PATH"));

   return nLoaded;
}

bool LadspaEffectsModule::IsPluginValid(const PluginPath & path, bool bFast)
{
   if( bFast )
      return true;
   wxString realPath = path.BeforeFirst(wxT(';'));
   return wxFileName::FileExists(realPath);
}

ComponentInterface *LadspaEffectsModule::CreateInstance(const PluginPath & path)
{
   // Acquires a resource for the application.
   // For us, the path is two words.
   // 1)  The library's path
   // 2)  The LADSPA descriptor index
   long index;
   wxString realPath = path.BeforeFirst(wxT(';'));
   path.AfterFirst(wxT(';')).ToLong(&index);

   // Safety of this depends on complementary calls to DeleteInstance on the module manager side.
   return safenew LadspaEffect(realPath, (int)index);
}

void LadspaEffectsModule::DeleteInstance(ComponentInterface *instance)
{
   std::unique_ptr < LadspaEffect > {
      dynamic_cast<LadspaEffect *>(instance)
   };
}

FilePaths LadspaEffectsModule::GetSearchPaths()
{
   FilePaths pathList;
   wxString pathVar;

   // Check for the LADSPA_PATH environment variable
   pathVar = wxString::FromUTF8(getenv("LADSPA_PATH"));
   if (!pathVar.empty())
   {
      wxStringTokenizer tok(pathVar);
      while (tok.HasMoreTokens())
      {
         pathList.push_back(tok.GetNextToken());
      }
   }

#if defined(__WXMAC__)
#define LADSPAPATH wxT("/Library/Audio/Plug-Ins/LADSPA")

   // Look in ~/Library/Audio/Plug-Ins/LADSPA and /Library/Audio/Plug-Ins/LADSPA
   pathList.push_back(wxGetHomeDir() + wxFILE_SEP_PATH + LADSPAPATH);
   pathList.push_back(LADSPAPATH);

#elif defined(__WXMSW__)

   // No special paths...probably should look in %CommonProgramFiles%\LADSPA

#else

   pathList.push_back(wxGetHomeDir() + wxFILE_SEP_PATH + wxT(".ladspa"));
#if defined(__LP64__)
   pathList.push_back(wxT("/usr/local/lib64/ladspa"));
   pathList.push_back(wxT("/usr/lib64/ladspa"));
#endif
   pathList.push_back(wxT("/usr/local/lib/ladspa"));
   pathList.push_back(wxT("/usr/lib/ladspa"));
   pathList.push_back(wxT(LIBDIR) wxT("/ladspa"));

#endif

   return pathList;
}

///////////////////////////////////////////////////////////////////////////////
//
// LadspaEffectOptionsDialog
//
///////////////////////////////////////////////////////////////////////////////

class LadspaEffectOptionsDialog final : public wxDialogWrapper
{
public:
   LadspaEffectOptionsDialog(wxWindow * parent, EffectHostInterface *host);
   virtual ~LadspaEffectOptionsDialog();

   void PopulateOrExchange(ShuttleGui & S);

   void OnOk(wxCommandEvent & evt);

private:
   EffectHostInterface *mHost;
   bool mUseLatency;

   DECLARE_EVENT_TABLE()
};

BEGIN_EVENT_TABLE(LadspaEffectOptionsDialog, wxDialogWrapper)
   EVT_BUTTON(wxID_OK, LadspaEffectOptionsDialog::OnOk)
END_EVENT_TABLE()

LadspaEffectOptionsDialog::LadspaEffectOptionsDialog(wxWindow * parent, EffectHostInterface *host)
:  wxDialogWrapper(parent, wxID_ANY, XO("LADSPA Effect Options"))
{
   mHost = host;

   mHost->GetSharedConfig(wxT("Options"), wxT("UseLatency"), mUseLatency, true);

   ShuttleGui S(this, eIsCreating);
   PopulateOrExchange(S);
}

LadspaEffectOptionsDialog::~LadspaEffectOptionsDialog()
{
}

void LadspaEffectOptionsDialog::PopulateOrExchange(ShuttleGui & S)
{
   S.SetBorder(5);
   S.StartHorizontalLay(wxEXPAND, 1);
   {
      S.StartVerticalLay(false);
      {
         S.StartStatic(XO("Latency Compensation"));
         {
            S.AddVariableText( XO(
"As part of their processing, some LADSPA effects must delay returning "
"audio to Audacity. When not compensating for this delay, you will "
"notice that small silences have been inserted into the audio. "
"Enabling this option will provide that compensation, but it may "
"not work for all LADSPA effects."),
               false, 0, 650);

            S.StartHorizontalLay(wxALIGN_LEFT);
            {
               S.TieCheckBox(XO("Enable &compensation"),
                             mUseLatency);
            }
            S.EndHorizontalLay();
         }
         S.EndStatic();
      }
      S.EndVerticalLay();
   }
   S.EndHorizontalLay();

   S.AddStandardButtons();

   Layout();
   Fit();
   Center();
}

void LadspaEffectOptionsDialog::OnOk(wxCommandEvent & WXUNUSED(evt))
{
   if (!Validate())
   {
      return;
   }

   ShuttleGui S(this, eIsGettingFromDialog);
   PopulateOrExchange(S);

   mHost->SetSharedConfig(wxT("Options"), wxT("UseLatency"), mUseLatency);

   EndModal(wxID_OK);
}

enum
{
   ID_Duration = 20000,
   ID_Toggles = 21000,
   ID_Sliders = 22000,
   ID_Texts = 23000,
};

///////////////////////////////////////////////////////////////////////////////
//
// LadspaEffectMeter
//
///////////////////////////////////////////////////////////////////////////////

class LadspaEffectMeter final : public wxWindow
{
public:
   LadspaEffectMeter(wxWindow *parent, const float & val, float min, float max);
   virtual ~LadspaEffectMeter();

private:
   void OnErase(wxEraseEvent & evt);
   void OnPaint(wxPaintEvent & evt);
   void OnIdle(wxIdleEvent & evt);
   void OnSize(wxSizeEvent & evt);

private:
   const float & mVal;
   float mMin;
   float mMax;
   float mLastValue;

   DECLARE_EVENT_TABLE()
};

BEGIN_EVENT_TABLE(LadspaEffectMeter, wxWindow)
   EVT_IDLE(LadspaEffectMeter::OnIdle)
   EVT_ERASE_BACKGROUND(LadspaEffectMeter::OnErase)
   EVT_PAINT(LadspaEffectMeter::OnPaint)
   EVT_SIZE(LadspaEffectMeter::OnSize)
END_EVENT_TABLE()

LadspaEffectMeter::LadspaEffectMeter(wxWindow *parent, const float & val, float min, float max)
:  wxWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxDEFAULT_CONTROL_BORDER),
   mVal(val)
{
   mMin = min;
   mMax = max;
   mLastValue = -mVal;

   SetBackgroundColour(*wxWHITE);
}

LadspaEffectMeter::~LadspaEffectMeter()
{
}

void LadspaEffectMeter::OnIdle(wxIdleEvent &evt)
{
   evt.Skip();
   if (mLastValue != mVal)
   {
      Refresh(false);
   }
}

void LadspaEffectMeter::OnErase(wxEraseEvent & WXUNUSED(evt))
{
   // Just ignore it to prevent flashing
}

void LadspaEffectMeter::OnPaint(wxPaintEvent & WXUNUSED(evt))
{
   wxPaintDC dc(this);

   // Cache some metrics
   wxRect r = GetClientRect();
   wxCoord x = r.GetLeft();
   wxCoord y = r.GetTop();
   wxCoord w = r.GetWidth();
   wxCoord h = r.GetHeight();

   // These use unscaled value, min, and max
   float val = mVal;
   if (val > mMax)
   {
      val = mMax;
   }
   if (val < mMin)
   {
      val = mMin;
   }
   val -= mMin;

   // Setup for erasing the background
   dc.SetPen(*wxTRANSPARENT_PEN);
   dc.SetBrush(wxColour(100, 100, 220));

   dc.Clear();
   dc.DrawRectangle(x, y, (w * (val / fabs(mMax - mMin))), h);

   mLastValue = mVal;
}

void LadspaEffectMeter::OnSize(wxSizeEvent & WXUNUSED(evt))
{
   Refresh(false);
}

///////////////////////////////////////////////////////////////////////////////
//
// LadspaEffect
//
///////////////////////////////////////////////////////////////////////////////

BEGIN_EVENT_TABLE(LadspaEffect, wxEvtHandler)
   EVT_COMMAND_RANGE(ID_Toggles, ID_Toggles + 999, wxEVT_COMMAND_CHECKBOX_CLICKED, LadspaEffect::OnCheckBox)
   EVT_COMMAND_RANGE(ID_Sliders, ID_Sliders + 999, wxEVT_COMMAND_SLIDER_UPDATED, LadspaEffect::OnSlider)
   EVT_COMMAND_RANGE(ID_Texts, ID_Texts + 999, wxEVT_COMMAND_TEXT_UPDATED, LadspaEffect::OnTextCtrl)
END_EVENT_TABLE()

LadspaEffect::LadspaEffect(const wxString & path, int index)
{
   mPath = path;
   mIndex = index;
   mData = NULL;

   mHost = NULL;
   mMaster = NULL;
   mReady = false;

   mInteractive = false;

   mAudioIns = 0;
   mAudioOuts = 0;
   mNumInputControls = 0;
   mNumOutputControls = 0;
   mSampleRate = 44100;
   mBlockSize = 0;

   mLatencyPort = -1;

   mDialog = NULL;
   mParent = NULL;
}

LadspaEffect::~LadspaEffect()
{
}

// ============================================================================
// ComponentInterface implementation
// ============================================================================

PluginPath LadspaEffect::GetPath()
{
   return wxString::Format(wxT("%s;%d"), mPath, mIndex);
}

ComponentInterfaceSymbol LadspaEffect::GetSymbol()
{
   return LAT1CTOWX(mData->Name);
}

VendorSymbol LadspaEffect::GetVendor()
{
   return { LAT1CTOWX(mData->Maker) };
}

wxString LadspaEffect::GetVersion()
{
   return _("n/a");
}

TranslatableString LadspaEffect::GetDescription()
{
   return Verbatim( LAT1CTOWX(mData->Copyright) );
}

// ============================================================================
// EffectDefinitionInterface implementation
// ============================================================================

EffectType LadspaEffect::GetType()
{
   if (mAudioIns == 0 && mAudioOuts == 0)
   {
      return EffectTypeTool;
   }

   if (mAudioIns == 0)
   {
      return EffectTypeGenerate;
   }

   if (mAudioOuts == 0)
   {
      return EffectTypeAnalyze;
   }

   return EffectTypeProcess;
}

EffectFamilySymbol LadspaEffect::GetFamily()
{
   return LADSPAEFFECTS_FAMILY;
}

bool LadspaEffect::IsInteractive()
{
   return mInteractive;
}

bool LadspaEffect::IsDefault()
{
   return false;
}

bool LadspaEffect::IsLegacy()
{
   return false;
}

bool LadspaEffect::SupportsRealtime()
{
   return GetType() != EffectTypeGenerate;
}

bool LadspaEffect::SupportsAutomation()
{
   return mNumInputControls > 0;
}

// ============================================================================
// EffectClientInterface Implementation
// ============================================================================

bool LadspaEffect::SetHost(EffectHostInterface *host)
{
   mHost = host;

   if (!Load())
   {
      return false;
   }

   mInputPorts.reinit( mData->PortCount );
   mOutputPorts.reinit( mData->PortCount );
   mInputControls.reinit( mData->PortCount );
   mOutputControls.reinit( mData->PortCount );

   for (unsigned long p = 0; p < mData->PortCount; p++)
   {
      LADSPA_PortDescriptor d = mData->PortDescriptors[p];

      // Collect the audio ports
      if (LADSPA_IS_PORT_AUDIO(d))
      {
         if (LADSPA_IS_PORT_INPUT(d)) 
         {
            mInputPorts[mAudioIns++] = p;
         }
         else if (LADSPA_IS_PORT_OUTPUT(d))
         {
            mOutputPorts[mAudioOuts++] = p;
         }
      }
      // Determine the port's default value
      else if (LADSPA_IS_PORT_CONTROL(d) && LADSPA_IS_PORT_INPUT(d))
      {
         mInteractive = true;

         LADSPA_PortRangeHint hint = mData->PortRangeHints[p];
         float val = float(1.0);
         float lower = hint.LowerBound;
         float upper = hint.UpperBound;

         if (LADSPA_IS_HINT_SAMPLE_RATE(hint.HintDescriptor))
         {
            lower *= mSampleRate;
            upper *= mSampleRate;
         }

         if (LADSPA_IS_HINT_BOUNDED_BELOW(hint.HintDescriptor) && val < lower)
         {
            val = lower;
         }

         if (LADSPA_IS_HINT_BOUNDED_ABOVE(hint.HintDescriptor) && val > upper)
         {
            val = upper;
         }

         if (LADSPA_IS_HINT_DEFAULT_MINIMUM(hint.HintDescriptor))
         {
            val = lower;
         }

         if (LADSPA_IS_HINT_DEFAULT_MAXIMUM(hint.HintDescriptor))
         {
            val = upper;
         }

         if (LADSPA_IS_HINT_DEFAULT_LOW(hint.HintDescriptor))
         {
            if (LADSPA_IS_HINT_LOGARITHMIC(hint.HintDescriptor))
            {
               val = exp(log(lower) * 0.75f + log(upper) * 0.25f);
            }
            else
            {
               val = lower * 0.75f + upper * 0.25f;
            }
         }

         if (LADSPA_IS_HINT_DEFAULT_MIDDLE(hint.HintDescriptor))
         {
            if (LADSPA_IS_HINT_LOGARITHMIC(hint.HintDescriptor))
            {
               val = exp(log(lower) * 0.5f + log(upper) * 0.5f);
            }
            else
            {
               val = lower * 0.5f + upper * 0.5f;
            }
         }

         if (LADSPA_IS_HINT_DEFAULT_HIGH(hint.HintDescriptor))
         {
            if (LADSPA_IS_HINT_LOGARITHMIC(hint.HintDescriptor))
            {
               val = exp(log(lower) * 0.25f + log(upper) * 0.75f);
            }
            else
            {
               val = lower * 0.25f + upper * 0.75f;
            }
         }

         if (LADSPA_IS_HINT_DEFAULT_0(hint.HintDescriptor))
         {
            val = 0.0f;
         }

         if (LADSPA_IS_HINT_DEFAULT_1(hint.HintDescriptor))
         {
            val = 1.0f;
         }

         if (LADSPA_IS_HINT_DEFAULT_100(hint.HintDescriptor))
         {
            val = 100.0f;
         }

         if (LADSPA_IS_HINT_DEFAULT_440(hint.HintDescriptor))
         {
            val = 440.0f;
         }

         mNumInputControls++;
         mInputControls[p] = val;
      }
      else if (LADSPA_IS_PORT_CONTROL(d) && LADSPA_IS_PORT_OUTPUT(d))
      {
         mOutputControls[p] = 0.0;

         // LADSPA effects have a convention of providing latency on an output
         // control port whose name is "latency".
         if (strcmp(mData->PortNames[p], "latency") == 0)
         {
            mLatencyPort = p;
         }
         else
         {
            mInteractive = true;
            mNumOutputControls++;
         }
      }
   }

   // mHost will be null during registration
   if (mHost)
   {
      mHost->GetSharedConfig(wxT("Options"), wxT("UseLatency"), mUseLatency, true);

      bool haveDefaults;
      mHost->GetPrivateConfig(mHost->GetFactoryDefaultsGroup(), wxT("Initialized"), haveDefaults, false);
      if (!haveDefaults)
      {
         SaveParameters(mHost->GetFactoryDefaultsGroup());
         mHost->SetPrivateConfig(mHost->GetFactoryDefaultsGroup(), wxT("Initialized"), true);
      }

      LoadParameters(mHost->GetCurrentSettingsGroup());
   }

   return true;
}

unsigned LadspaEffect::GetAudioInCount()
{
   return mAudioIns;
}

unsigned LadspaEffect::GetAudioOutCount()
{
   return mAudioOuts;
}

int LadspaEffect::GetMidiInCount()
{
   return 0;
}

int LadspaEffect::GetMidiOutCount()
{
   return 0;
}

void LadspaEffect::SetSampleRate(double rate)
{
   mSampleRate = rate;
}

size_t LadspaEffect::SetBlockSize(size_t maxBlockSize)
{
   mBlockSize = maxBlockSize;

   return mBlockSize;
}

size_t LadspaEffect::GetBlockSize() const
{
   return mBlockSize;
}

sampleCount LadspaEffect::GetLatency()
{
   if (mUseLatency && mLatencyPort >= 0 && !mLatencyDone)
   {
      mLatencyDone = true;
      return sampleCount ( mOutputControls[mLatencyPort] );
   }

   return 0;
}

size_t LadspaEffect::GetTailSize()
{
   return 0;
}

bool LadspaEffect::IsReady()
{
   return mReady;
}

bool LadspaEffect::ProcessInitialize(sampleCount WXUNUSED(totalLen), ChannelNames WXUNUSED(chanMap))
{
   /* Instantiate the plugin */
   if (!mReady)
   {
      mMaster = InitInstance(mSampleRate);
      if (!mMaster)
      {
         return false;
      }
      mReady = true;
   }

   mLatencyDone = false;

   return true;
}

bool LadspaEffect::ProcessFinalize()
{
   if (mReady)
   {
      mReady = false;

      FreeInstance(mMaster);
      mMaster = NULL;
   }

   return true;
}

size_t LadspaEffect::ProcessBlock(float **inBlock, float **outBlock, size_t blockLen)
{
   for (int i = 0; i < (int)mAudioIns; i++)
   {
      mData->connect_port(mMaster, mInputPorts[i], inBlock[i]);
   }

   for (int i = 0; i < (int)mAudioOuts; i++)
   {
      mData->connect_port(mMaster, mOutputPorts[i], outBlock[i]);
   }

   mData->run(mMaster, blockLen);

   RefreshControls(true);

   return blockLen;
}

bool LadspaEffect::RealtimeInitialize()
{
   return true;
}

bool LadspaEffect::RealtimeAddProcessor(unsigned WXUNUSED(numChannels), float sampleRate)
{
   LADSPA_Handle slave = InitInstance(sampleRate);
   if (!slave)
   {
      return false;
   }

   mSlaves.push_back(slave);

   return true;
}

bool LadspaEffect::RealtimeFinalize()
{
   for (size_t i = 0, cnt = mSlaves.size(); i < cnt; i++)
   {
      FreeInstance(mSlaves[i]);
   }
   mSlaves.clear();

   return true;
}

bool LadspaEffect::RealtimeSuspend()
{
   return true;
}

bool LadspaEffect::RealtimeResume()
{
   return true;
}

bool LadspaEffect::RealtimeProcessStart()
{
   return true;
}

size_t LadspaEffect::RealtimeProcess(int group,
                                          float **inbuf,
                                          float **outbuf,
                                          size_t numSamples)
{
   for (int i = 0; i < (int)mAudioIns; i++)
   {
      mData->connect_port(mSlaves[group], mInputPorts[i], inbuf[i]);
   }

   for (int i = 0; i < (int)mAudioOuts; i++)
   {
      mData->connect_port(mSlaves[group], mOutputPorts[i], outbuf[i]);
   }

   mData->run(mSlaves[group], numSamples);

   return numSamples;
}

bool LadspaEffect::RealtimeProcessEnd()
{
   return true;
}

bool LadspaEffect::ShowInterface(
   wxWindow &parent, const EffectDialogFactory &factory, bool forceModal)
{
   if (mDialog)
   {
      if ( mDialog->Close(true) )
         mDialog = nullptr;
      return false;
   }

   // mDialog is null
   auto cleanup = valueRestorer( mDialog );

   if ( factory )
      mDialog = factory(parent, mHost, this);
   if (!mDialog)
   {
      return false;
   }

   mDialog->Layout();
   mDialog->Fit();
   mDialog->SetMinSize(mDialog->GetSize());

   if ((SupportsRealtime() || GetType() == EffectTypeAnalyze) && !forceModal)
   {
      mDialog->Show();
      cleanup.release();

      return false;
   }

   bool res = mDialog->ShowModal() != 0;

   return res;
}

bool LadspaEffect::GetAutomationParameters(CommandParameters & parms)
{
   for (unsigned long p = 0; p < mData->PortCount; p++)
   {
      LADSPA_PortDescriptor d = mData->PortDescriptors[p];

      if (LADSPA_IS_PORT_CONTROL(d) && LADSPA_IS_PORT_INPUT(d))
      {
         if (!parms.Write(LAT1CTOWX(mData->PortNames[p]), mInputControls[p]))
         {
            return false;
         }
      }
   }

   return true;
}

bool LadspaEffect::SetAutomationParameters(CommandParameters & parms)
{
   for (unsigned long p = 0; p < mData->PortCount; p++)
   {
      LADSPA_PortDescriptor descriptor = mData->PortDescriptors[p];

      if (LADSPA_IS_PORT_CONTROL(descriptor) && LADSPA_IS_PORT_INPUT(descriptor))
      {
         wxString labelText = LAT1CTOWX(mData->PortNames[p]);
         double d = 0.0;
         if (!parms.Read(labelText, &d))
         {
            return false;
         }

         mInputControls[p] = d;
      }
   }

   return true;
}

bool LadspaEffect::LoadUserPreset(const RegistryPath & name)
{
   if (!LoadParameters(name))
   {
      return false;
   }

   RefreshControls();

   return true;
}

bool LadspaEffect::SaveUserPreset(const RegistryPath & name)
{
   return SaveParameters(name);
}

RegistryPaths LadspaEffect::GetFactoryPresets()
{
   return {};
}

bool LadspaEffect::LoadFactoryPreset(int WXUNUSED(id))
{
   return true;
}

bool LadspaEffect::LoadFactoryDefaults()
{
   if (!LoadParameters(mHost->GetFactoryDefaultsGroup()))
   {
      return false;
   }

   RefreshControls();

   return true;
}

// ============================================================================
// EffectUIClientInterface Implementation
// ============================================================================

void LadspaEffect::SetHostUI(EffectUIHostInterface *host)
{
   mUIHost = host;
}

bool LadspaEffect::PopulateUI(ShuttleGui &S)
{
   auto parent = S.GetParent();

   mParent = parent;

   mParent->PushEventHandler(this);

   mToggles.reinit( mData->PortCount );
   mSliders.reinit( mData->PortCount );
   mFields.reinit( mData->PortCount, true);
   mLabels.reinit( mData->PortCount );
   mMeters.reinit( mData->PortCount );

   wxASSERT(mParent); // To justify safenew
   wxScrolledWindow *const w = safenew wxScrolledWindow(mParent,
      wxID_ANY,
      wxDefaultPosition,
      wxDefaultSize,
      wxVSCROLL | wxTAB_TRAVERSAL);

   {
      auto mainSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
      w->SetScrollRate(0, 20);

      // This fools NVDA into not saying "Panel" when the dialog gets focus
      w->SetName(wxT("\a"));
      w->SetLabel(wxT("\a"));

      mainSizer->Add(w, 1, wxEXPAND);
      mParent->SetSizer(mainSizer.release());
   }

   wxSizer *marginSizer;
   {
      auto uMarginSizer = std::make_unique<wxBoxSizer>(wxVERTICAL);
      marginSizer = uMarginSizer.get();

      if (mNumInputControls)
      {
         auto paramSizer = std::make_unique<wxStaticBoxSizer>(wxVERTICAL, w, _("Effect Settings"));

         auto gridSizer = std::make_unique<wxFlexGridSizer>(5, 0, 0);
         gridSizer->AddGrowableCol(3);

         wxControl *item;

         // Add the duration control for generators
         if (GetType() == EffectTypeGenerate)
         {
            item = safenew wxStaticText(w, 0, _("Duration:"));
            gridSizer->Add(item, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, 5);
            mDuration = safenew
               NumericTextCtrl(w, ID_Duration,
                  NumericConverter::TIME,
                  mHost->GetDurationFormat(),
                  mHost->GetDuration(),
                  mSampleRate,
                  NumericTextCtrl::Options{}
                     .AutoPos(true));
            mDuration->SetName( XO("Duration") );
            gridSizer->Add(mDuration, 0, wxALIGN_CENTER_VERTICAL | wxALL, 5);
            gridSizer->Add(1, 1, 0);
            gridSizer->Add(1, 1, 0);
            gridSizer->Add(1, 1, 0);
         }

         for (unsigned long p = 0; p < mData->PortCount; p++)
         {
            LADSPA_PortDescriptor d = mData->PortDescriptors[p];
            if (LADSPA_IS_PORT_AUDIO(d) || LADSPA_IS_PORT_OUTPUT(d))
            {
               continue;
            }

            wxString labelText = LAT1CTOWX(mData->PortNames[p]);
            item = safenew wxStaticText(w, 0, wxString::Format(_("%s:"), labelText));
            gridSizer->Add(item, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, 5);

            wxString fieldText;
            LADSPA_PortRangeHint hint = mData->PortRangeHints[p];

            if (LADSPA_IS_HINT_TOGGLED(hint.HintDescriptor))
            {
               mToggles[p] = safenew wxCheckBox(w, ID_Toggles + p, wxT(""));
               mToggles[p]->SetName(labelText);
               mToggles[p]->SetValue(mInputControls[p] > 0);
               gridSizer->Add(mToggles[p], 0, wxALL, 5);

               gridSizer->Add(1, 1, 0);
               gridSizer->Add(1, 1, 0);
               gridSizer->Add(1, 1, 0);
               continue;
            }

            wxString bound;
            float lower = -FLT_MAX;
            float upper = FLT_MAX;
            bool haslo = false;
            bool hashi = false;
            bool forceint = false;

            if (LADSPA_IS_HINT_BOUNDED_BELOW(hint.HintDescriptor))
            {
               lower = hint.LowerBound;
               haslo = true;
            }

            if (LADSPA_IS_HINT_BOUNDED_ABOVE(hint.HintDescriptor))
            {
               upper = hint.UpperBound;
               hashi = true;
            }

            if (LADSPA_IS_HINT_SAMPLE_RATE(hint.HintDescriptor))
            {
               lower *= mSampleRate;
               upper *= mSampleRate;
               forceint = true;
            }

            // Limit to the UI precision
            lower = ceilf(lower * 1000000.0) / 1000000.0;
            upper = floorf(upper * 1000000.0) / 1000000.0;
            mInputControls[p] = roundf(mInputControls[p] * 1000000.0) / 1000000.0;

            if (haslo && mInputControls[p] < lower)
            {
               mInputControls[p] = lower;
            }

            if (hashi && mInputControls[p] > upper)
            {
               mInputControls[p] = lower;
            }

            // Don't specify a value at creation time.  This prevents unwanted events
            // being sent to the OnTextCtrl() handler before the associated slider
            // has been created.
            mFields[p] = safenew wxTextCtrl(w, ID_Texts + p);
            mFields[p]->SetName(labelText);
            gridSizer->Add(mFields[p], 0, wxALIGN_CENTER_VERTICAL | wxALL, 5);

            wxString str;
            if (haslo)
            {
               if (LADSPA_IS_HINT_INTEGER(hint.HintDescriptor) || forceint)
               {
                  str.Printf(wxT("%d"), (int)(lower + 0.5));
               }
               else
               {
                  str = Internat::ToDisplayString(lower);
               }
               item = safenew wxStaticText(w, 0, str);
               gridSizer->Add(item, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, 5);
            }
            else
            {
               gridSizer->Add(1, 1, 0);
            }

            mSliders[p] = safenew wxSliderWrapper(w, ID_Sliders + p,
               0, 0, 1000,
               wxDefaultPosition,
               wxSize(200, -1));
#if wxUSE_ACCESSIBILITY
            // so that name can be set on a standard control
            mSliders[p]->SetAccessible(safenew WindowAccessible(mSliders[p]));
#endif
            mSliders[p]->SetName(labelText);
            gridSizer->Add(mSliders[p], 0, wxALIGN_CENTER_VERTICAL | wxEXPAND | wxALL, 5);

            if (hashi)
            {
               if (LADSPA_IS_HINT_INTEGER(hint.HintDescriptor) || forceint)
               {
                  str.Printf(wxT("%d"), (int)(upper + 0.5));
               }
               else
               {
                  str = Internat::ToDisplayString(upper);
               }
               item = safenew wxStaticText(w, 0, str);
               gridSizer->Add(item, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, 5);
            }
            else
            {
               gridSizer->Add(1, 1, 0);
            }

            if (LADSPA_IS_HINT_INTEGER(hint.HintDescriptor) || forceint)
            {
               fieldText.Printf(wxT("%d"), (int)(mInputControls[p] + 0.5));

               IntegerValidator<float> vld(&mInputControls[p]);
               vld.SetRange(haslo ? lower : INT_MIN,
                  hashi ? upper : INT_MAX);
               mFields[p]->SetValidator(vld);
            }
            else
            {
               fieldText = Internat::ToDisplayString(mInputControls[p]);

               // > 12 decimal places can cause rounding errors in display.
               FloatingPointValidator<float> vld(6, &mInputControls[p]);
               vld.SetRange(lower, upper);

               // Set number of decimal places
               if (upper - lower < 10.0)
               {
                  vld.SetStyle(NumValidatorStyle::THREE_TRAILING_ZEROES);
               }
               else if (upper - lower < 100.0)
               {
                  vld.SetStyle(NumValidatorStyle::TWO_TRAILING_ZEROES);
               }
               else
               {
                  vld.SetStyle(NumValidatorStyle::ONE_TRAILING_ZERO);
               }

               mFields[p]->SetValidator(vld);
            }

            // Set the textctrl value.  This will trigger an event so OnTextCtrl()
            // can update the slider.
            mFields[p]->SetValue(fieldText);
         }

         paramSizer->Add(gridSizer.release(), 0, wxEXPAND | wxALL, 5);
         marginSizer->Add(paramSizer.release(), 0, wxEXPAND | wxALL, 5);
      }

      if (mNumOutputControls > 0)
      {
         auto paramSizer = std::make_unique<wxStaticBoxSizer>(wxVERTICAL, w, _("Effect Output"));

         auto gridSizer = std::make_unique<wxFlexGridSizer>(2, 0, 0);
         gridSizer->AddGrowableCol(1);

         wxControl *item;

         for (unsigned long p = 0; p < mData->PortCount; p++)
         {
            LADSPA_PortDescriptor d = mData->PortDescriptors[p];
            if (LADSPA_IS_PORT_AUDIO(d) || LADSPA_IS_PORT_INPUT(d))
            {
               continue;
            }

            wxString labelText = LAT1CTOWX(mData->PortNames[p]);
            item = safenew wxStaticText(w, 0, wxString::Format(_("%s:"), labelText));
            gridSizer->Add(item, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, 5);

            //LADSPA_PortRangeHint hint = mData->PortRangeHints[p];

            wxString bound;
            float lower = 0.0;
            float upper = 1.0;

            /*
            bool haslo = false;
            bool hashi = false;
            bool forceint = false;

            if (LADSPA_IS_HINT_BOUNDED_BELOW(hint.HintDescriptor))
            {
               lower = hint.LowerBound;
               haslo = true;
            }

            if (LADSPA_IS_HINT_BOUNDED_ABOVE(hint.HintDescriptor))
            {
               upper = hint.UpperBound;
               hashi = true;
            }

            if (LADSPA_IS_HINT_SAMPLE_RATE(hint.HintDescriptor))
            {
               lower *= mSampleRate;
               upper *= mSampleRate;
               forceint = true;
            }
            */

            // Limit to the UI precision
            lower = ceilf(lower * 1000000.0) / 1000000.0;
            upper = floorf(upper * 1000000.0) / 1000000.0;
            mInputControls[p] = roundf(mInputControls[p] * 1000000.0) / 1000000.0;

            mMeters[p] = safenew LadspaEffectMeter(w, mOutputControls[p], lower, upper);
            mMeters[p]->SetLabel(labelText);    // for screen readers
            gridSizer->Add(mMeters[p], 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxALL, 5);
         }

         paramSizer->Add(gridSizer.release(), 0, wxEXPAND | wxALL, 5);
         marginSizer->Add(paramSizer.release(), 0, wxEXPAND | wxALL, 5);

         RefreshControls(true);
      }

      w->SetSizer(uMarginSizer.release());
   }

   w->Layout();

   // Try to give the window a sensible default/minimum size
   wxSize sz1 = marginSizer->GetMinSize();
   wxSize sz2 = mParent->GetMinSize();
   w->SetMinSize( { std::min(sz1.x, sz2.x), std::min(sz1.y, sz2.y) } );

   // And let the parent reduce to the NEW minimum if possible
   mParent->SetMinSize({ -1, -1 });

   return true;
}

bool LadspaEffect::IsGraphicalUI()
{
   return false;
}

bool LadspaEffect::ValidateUI()
{
   if (!mParent->Validate())
   {
      return false;
   }

   if (GetType() == EffectTypeGenerate)
   {
      mHost->SetDuration(mDuration->GetValue());
   }

   return true;
}

bool LadspaEffect::HideUI()
{
   return true;
}

bool LadspaEffect::CloseUI()
{
   mParent->RemoveEventHandler(this);

   mToggles.reset();
   mSliders.reset();
   mFields.reset();
   mLabels.reset();

   mUIHost = NULL;
   mParent = NULL;
   mDialog = NULL;

   return true;
}

bool LadspaEffect::CanExportPresets()
{
   return false;
}

void LadspaEffect::ExportPresets()
{
}

void LadspaEffect::ImportPresets()
{
}

bool LadspaEffect::HasOptions()
{
   return true;
}

void LadspaEffect::ShowOptions()
{
   LadspaEffectOptionsDialog dlg(mParent, mHost);
   if (dlg.ShowModal())
   {
      // Reinitialize configuration options
      mHost->GetSharedConfig(wxT("Options"), wxT("UseLatency"), mUseLatency, true);
   }
}

// ============================================================================
// LadspaEffect Implementation
// ============================================================================

bool LadspaEffect::Load()
{
   if (mLib.IsLoaded())
   {
      return true;
   }

   wxFileName ff = mPath;
   wxString envpath;
   bool hadpath = wxGetEnv(wxT("PATH"), &envpath);
   wxSetEnv(wxT("PATH"), ff.GetPath() + wxFILE_SEP_PATH + envpath);
   wxString saveOldCWD = ff.GetCwd();
   ff.SetCwd();

   LADSPA_Descriptor_Function mainFn = NULL;

   if (mLib.Load(mPath, wxDL_NOW))
   {
      wxLogNull logNo;

      mainFn = (LADSPA_Descriptor_Function) mLib.GetSymbol(wxT("ladspa_descriptor"));
      if (mainFn)
      {
         mData = mainFn(mIndex);
         return true;
      }
   }

   if (mLib.IsLoaded())
   {
      mLib.Unload();
   }

   wxSetWorkingDirectory(saveOldCWD);
   hadpath ? wxSetEnv(wxT("PATH"), envpath) : wxUnsetEnv(wxT("PATH"));

   return false;
}

void LadspaEffect::Unload()
{
   if (mLib.IsLoaded())
   {
      mLib.Unload();
   }
}

bool LadspaEffect::LoadParameters(const RegistryPath & group)
{
   wxString parms;
   if (!mHost->GetPrivateConfig(group, wxT("Parameters"), parms, wxEmptyString))
   {
      return false;
   }

   CommandParameters eap;
   if (!eap.SetParameters(parms))
   {
      return false;
   }

   return SetAutomationParameters(eap);
}

bool LadspaEffect::SaveParameters(const RegistryPath & group)
{
   CommandParameters eap;
   if (!GetAutomationParameters(eap))
   {
      return false;
   }

   wxString parms;
   if (!eap.GetParameters(parms))
   {
      return false;
   }

   return mHost->SetPrivateConfig(group, wxT("Parameters"), parms);
}

LADSPA_Handle LadspaEffect::InitInstance(float sampleRate)
{
   /* Instantiate the plugin */
   LADSPA_Handle handle = mData->instantiate(mData, sampleRate);
   if (!handle)
   {
      return NULL;
   }

   for (unsigned long p = 0; p < mData->PortCount; p++)
   {
      LADSPA_PortDescriptor d = mData->PortDescriptors[p];
      if (LADSPA_IS_PORT_CONTROL(d))
      {
         if (LADSPA_IS_PORT_INPUT(d))
         {
            mData->connect_port(handle, p, &mInputControls[p]);
         }
         else
         {
            mData->connect_port(handle, p, &mOutputControls[p]);
         }
      }
   }

   if (mData->activate)
   {
      mData->activate(handle);
   }

   return handle;
}

void LadspaEffect::FreeInstance(LADSPA_Handle handle)
{
   if (mData->deactivate)
   {
      mData->deactivate(handle);
   }

   mData->cleanup(handle);
}

void LadspaEffect::OnCheckBox(wxCommandEvent & evt)
{
   int p = evt.GetId() - ID_Toggles;

   mInputControls[p] = mToggles[p]->GetValue();
}

void LadspaEffect::OnSlider(wxCommandEvent & evt)
{
   int p = evt.GetId() - ID_Sliders;

   float val;
   float lower = float(0.0);
   float upper = float(10.0);
   float range;
   bool forceint = false;

   LADSPA_PortRangeHint hint = mData->PortRangeHints[p];
   if (LADSPA_IS_HINT_BOUNDED_BELOW(hint.HintDescriptor))
      lower = hint.LowerBound;
   if (LADSPA_IS_HINT_BOUNDED_ABOVE(hint.HintDescriptor))
      upper = hint.UpperBound;
   if (LADSPA_IS_HINT_SAMPLE_RATE(hint.HintDescriptor)) {
      lower *= mSampleRate;
      upper *= mSampleRate;
      forceint = true;
   }

   range = upper - lower;

   val = (mSliders[p]->GetValue() / 1000.0) * range + lower;

   wxString str;
   if (LADSPA_IS_HINT_INTEGER(hint.HintDescriptor) || forceint)
      str.Printf(wxT("%d"), (int)(val + 0.5));
   else
      str = Internat::ToDisplayString(val);

   mFields[p]->SetValue(str);

   mInputControls[p] = val;
}

void LadspaEffect::OnTextCtrl(wxCommandEvent & evt)
{
   LadspaEffect *that = this;
   int p = evt.GetId() - ID_Texts;

   float val;
   float lower = float(0.0);
   float upper = float(10.0);
   float range;

   val = Internat::CompatibleToDouble(that->mFields[p]->GetValue());

   LADSPA_PortRangeHint hint = that->mData->PortRangeHints[p];
   if (LADSPA_IS_HINT_BOUNDED_BELOW(hint.HintDescriptor))
      lower = hint.LowerBound;
   if (LADSPA_IS_HINT_BOUNDED_ABOVE(hint.HintDescriptor))
      upper = hint.UpperBound;
   if (LADSPA_IS_HINT_SAMPLE_RATE(hint.HintDescriptor)) {
      lower *= mSampleRate;
      upper *= mSampleRate;
   }
   range = upper - lower;

   if (val < lower)
      val = lower;
   if (val > upper)
      val = upper;

   mInputControls[p] = val;

   that->mSliders[p]->SetValue((int)(((val-lower)/range) * 1000.0 + 0.5));
}

void LadspaEffect::RefreshControls(bool outputOnly)
{
   if (!mParent)
   {
      return;
   }

   for (unsigned long p = 0; p < mData->PortCount; p++)
   {
      LADSPA_PortDescriptor d = mData->PortDescriptors[p];
      if (!(LADSPA_IS_PORT_CONTROL(d)))
      {
         continue;
      }

      wxString fieldText;
      LADSPA_PortRangeHint hint = mData->PortRangeHints[p];

      bool forceint = false;
      if (LADSPA_IS_HINT_SAMPLE_RATE(hint.HintDescriptor))
      {
         forceint = true;
      }

      if (LADSPA_IS_PORT_OUTPUT(d)) 
      {
         continue;
      }

      if (outputOnly)
      {
         continue;
      }

      if (LADSPA_IS_HINT_TOGGLED(hint.HintDescriptor))
      {
         mToggles[p]->SetValue(mInputControls[p] > 0);
         continue;
      }

      if (LADSPA_IS_HINT_INTEGER(hint.HintDescriptor) || forceint)
      {
         fieldText.Printf(wxT("%d"), (int)(mInputControls[p] + 0.5));
      }
      else
      {
         fieldText = Internat::ToDisplayString(mInputControls[p]);
      }

      // Set the textctrl value.  This will trigger an event so OnTextCtrl()
      // can update the slider.
      mFields[p]->SetValue(fieldText);
   }
}
