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

  Audacity: A Digital Audio Editor

  ChangeSpeed.cpp

  Vaughan Johnson, Dominic Mazzoni

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

\class EffectChangeSpeed
\brief An Effect that affects both pitch & speed.

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


#include "ChangeSpeed.h"
#include "LoadEffects.h"

#include <math.h>

#include <wx/choice.h>
#include <wx/intl.h>
#include <wx/slider.h>

#include "../LabelTrack.h"
#include "Prefs.h"
#include "Resample.h"
#include "../Shuttle.h"
#include "../ShuttleGui.h"
#include "../widgets/NumericTextCtrl.h"
#include "../widgets/valnum.h"

#include "TimeWarper.h"
#include "../WaveClip.h"
#include "../WaveTrack.h"

enum
{
   ID_PercentChange = 10000,
   ID_Multiplier,
   ID_FromVinyl,
   ID_ToVinyl,
   ID_ToLength
};

// the standard vinyl rpm choices
// If the percent change is not one of these ratios, the choice control gets "n/a".
enum kVinyl
{
   kVinyl_33AndAThird = 0,
   kVinyl_45,
   kVinyl_78,
   kVinyl_NA
};

static const TranslatableStrings kVinylStrings{
   XO("33\u2153"),
   XO("45"),
   XO("78"),
   /* i18n-hint: n/a is an English abbreviation meaning "not applicable". */
   XO("n/a"),
};

// Soundtouch is not reasonable below -99% or above 3000%.

// Define keys, defaults, minimums, and maximums for the effect parameters
//
//     Name          Type     Key               Def   Min      Max      Scale
Param( Percentage,   double,  wxT("Percentage"), 0.0,  -99.0,   4900.0,  1  );

// We warp the slider to go up to 400%, but user can enter higher values
static const double kSliderMax = 100.0;         // warped above zero to actually go up to 400%
static const double kSliderWarp = 1.30105;      // warp power takes max from 100 to 400.

//
// EffectChangeSpeed
//

const ComponentInterfaceSymbol EffectChangeSpeed::Symbol
{ XO("Change Speed") };

namespace{ BuiltinEffectsModule::Registration< EffectChangeSpeed > reg; }

BEGIN_EVENT_TABLE(EffectChangeSpeed, wxEvtHandler)
    EVT_TEXT(ID_PercentChange, EffectChangeSpeed::OnText_PercentChange)
    EVT_TEXT(ID_Multiplier, EffectChangeSpeed::OnText_Multiplier)
    EVT_SLIDER(ID_PercentChange, EffectChangeSpeed::OnSlider_PercentChange)
    EVT_CHOICE(ID_FromVinyl, EffectChangeSpeed::OnChoice_Vinyl)
    EVT_CHOICE(ID_ToVinyl, EffectChangeSpeed::OnChoice_Vinyl)
    EVT_TEXT(ID_ToLength, EffectChangeSpeed::OnTimeCtrl_ToLength)
    EVT_COMMAND(ID_ToLength, EVT_TIMETEXTCTRL_UPDATED, EffectChangeSpeed::OnTimeCtrlUpdate)
END_EVENT_TABLE()

EffectChangeSpeed::EffectChangeSpeed()
{
   // effect parameters
   m_PercentChange = DEF_Percentage;

   mFromVinyl = kVinyl_33AndAThird;
   mToVinyl = kVinyl_33AndAThird;
   mFromLength = 0.0;
   mToLength = 0.0;
   mFormat = NumericConverter::DefaultSelectionFormat();
   mbLoopDetect = false;

   SetLinearEffectFlag(true);
}

EffectChangeSpeed::~EffectChangeSpeed()
{
}

// ComponentInterface implementation

ComponentInterfaceSymbol EffectChangeSpeed::GetSymbol()
{
   return Symbol;
}

TranslatableString EffectChangeSpeed::GetDescription()
{
   return XO("Changes the speed of a track, also changing its pitch");
}

ManualPageID EffectChangeSpeed::ManualPage()
{
   return L"Change_Speed";
}


// EffectDefinitionInterface implementation

EffectType EffectChangeSpeed::GetType()
{
   return EffectTypeProcess;
}

// EffectClientInterface implementation
bool EffectChangeSpeed::DefineParams( ShuttleParams & S ){
   S.SHUTTLE_PARAM( m_PercentChange, Percentage );
   return true;
}

bool EffectChangeSpeed::GetAutomationParameters(CommandParameters & parms)
{
   parms.Write(KEY_Percentage, m_PercentChange);

   return true;
}

bool EffectChangeSpeed::SetAutomationParameters(CommandParameters & parms)
{
   ReadAndVerifyDouble(Percentage);

   m_PercentChange = Percentage;

   return true;
}

bool EffectChangeSpeed::LoadFactoryDefaults()
{
   mFromVinyl = kVinyl_33AndAThird;
   mFormat = NumericConverter::DefaultSelectionFormat();

   return Effect::LoadFactoryDefaults();
}

// Effect implementation

bool EffectChangeSpeed::CheckWhetherSkipEffect()
{
   return (m_PercentChange == 0.0);
}

double EffectChangeSpeed::CalcPreviewInputLength(double previewLength)
{
   return previewLength * (100.0 + m_PercentChange) / 100.0;
}

bool EffectChangeSpeed::Startup()
{
   wxString base = wxT("/Effects/ChangeSpeed/");

   // Migrate settings from 2.1.0 or before

   // Already migrated, so bail
   if (gPrefs->Exists(base + wxT("Migrated")))
   {
      return true;
   }

   // Load the old "current" settings
   if (gPrefs->Exists(base))
   {
      // Retrieve last used control values
      gPrefs->Read(base + wxT("PercentChange"), &m_PercentChange, 0);

      wxString format;
      gPrefs->Read(base + wxT("TimeFormat"), &format, wxString{});
      mFormat = NumericConverter::LookupFormat( NumericConverter::TIME, format );

      gPrefs->Read(base + wxT("VinylChoice"), &mFromVinyl, 0);
      if (mFromVinyl == kVinyl_NA)
      {
         mFromVinyl = kVinyl_33AndAThird;
      }

      SetPrivateConfig(GetCurrentSettingsGroup(), wxT("TimeFormat"), mFormat.Internal());
      SetPrivateConfig(GetCurrentSettingsGroup(), wxT("VinylChoice"), mFromVinyl);

      SaveUserPreset(GetCurrentSettingsGroup());

      // Do not migrate again
      gPrefs->Write(base + wxT("Migrated"), true);
      gPrefs->Flush();
   }

   return true;
}

bool EffectChangeSpeed::Init()
{
   // The selection might have changed since the last time EffectChangeSpeed
   // was invoked, so recalculate the Length parameters.
   mFromLength = mT1 - mT0;
   return true;
}

bool EffectChangeSpeed::Process()
{
   // Similar to EffectSoundTouch::Process()

   // Iterate over each track.
   // All needed because this effect needs to introduce
   // silence in the sync-lock group tracks to keep sync
   CopyInputTracks(true); // Set up mOutputTracks.
   bool bGoodResult = true;

   mCurTrackNum = 0;
   mMaxNewLength = 0.0;

   mFactor = 100.0 / (100.0 + m_PercentChange);

   mOutputTracks->Any().VisitWhile( bGoodResult,
      [&](LabelTrack *lt) {
         if (lt->GetSelected() || lt->IsSyncLockSelected())
         {
            if (!ProcessLabelTrack(lt))
               bGoodResult = false;
         }
      },
      [&](WaveTrack *pOutWaveTrack, const Track::Fallthrough &fallthrough) {
         if (!pOutWaveTrack->GetSelected())
            return fallthrough();

         //Get start and end times from track
         mCurT0 = pOutWaveTrack->GetStartTime();
         mCurT1 = pOutWaveTrack->GetEndTime();

         //Set the current bounds to whichever left marker is
         //greater and whichever right marker is less:
         mCurT0 = wxMax(mT0, mCurT0);
         mCurT1 = wxMin(mT1, mCurT1);

         // Process only if the right marker is to the right of the left marker
         if (mCurT1 > mCurT0) {
            //Transform the marker timepoints to samples
            auto start = pOutWaveTrack->TimeToLongSamples(mCurT0);
            auto end = pOutWaveTrack->TimeToLongSamples(mCurT1);

            //ProcessOne() (implemented below) processes a single track
            if (!ProcessOne(pOutWaveTrack, start, end))
               bGoodResult = false;
         }
         mCurTrackNum++;
      },
      [&](Track *t) {
         if (t->IsSyncLockSelected())
            t->SyncLockAdjust(mT1, mT0 + (mT1 - mT0) * mFactor);
      }
   );

   if (bGoodResult)
      ReplaceProcessedTracks(bGoodResult);

   // Update selection.
   mT1 = mT0 + (((mT1 - mT0) * 100.0) / (100.0 + m_PercentChange));

   return bGoodResult;
}

void EffectChangeSpeed::PopulateOrExchange(ShuttleGui & S)
{
   {
      wxString formatId;
      GetPrivateConfig(GetCurrentSettingsGroup(), wxT("TimeFormat"),
                       formatId, mFormat.Internal());
      mFormat = NumericConverter::LookupFormat(
         NumericConverter::TIME, formatId );
   }
   GetPrivateConfig(GetCurrentSettingsGroup(), wxT("VinylChoice"), mFromVinyl, mFromVinyl);

   S.SetBorder(5);

   S.StartVerticalLay(0);
   {
      S.AddSpace(0, 5);
      S.AddTitle(XO("Change Speed, affecting both Tempo and Pitch"));
      S.AddSpace(0, 10);

      // Speed multiplier and percent change controls.
      S.StartMultiColumn(4, wxCENTER);
      {
         mpTextCtrl_Multiplier = S.Id(ID_Multiplier)
            .Validator<FloatingPointValidator<double>>(
               3, &mMultiplier,
               NumValidatorStyle::THREE_TRAILING_ZEROES,
               MIN_Percentage / 100.0, ((MAX_Percentage / 100.0) + 1)
            )
            .AddTextBox(XXO("&Speed Multiplier:"), wxT(""), 12);

         mpTextCtrl_PercentChange = S.Id(ID_PercentChange)
            .Validator<FloatingPointValidator<double>>(
               3, &m_PercentChange,
               NumValidatorStyle::THREE_TRAILING_ZEROES,
               MIN_Percentage, MAX_Percentage
            )
            .AddTextBox(XXO("Percent C&hange:"), wxT(""), 12);
      }
      S.EndMultiColumn();

      // Percent change slider.
      S.StartHorizontalLay(wxEXPAND);
      {
         mpSlider_PercentChange = S.Id(ID_PercentChange)
            .Name(XO("Percent Change"))
            .Style(wxSL_HORIZONTAL)
            .AddSlider( {}, 0, (int)kSliderMax, (int)MIN_Percentage);
      }
      S.EndHorizontalLay();

      // Vinyl rpm controls.
      S.StartMultiColumn(5, wxCENTER);
      {
         /* i18n-hint: "rpm" is an English abbreviation meaning "revolutions per minute".
            "vinyl" refers to old-fashioned phonograph records */
         S.AddUnits(XO("Standard Vinyl rpm:"));

         mpChoice_FromVinyl = S.Id(ID_FromVinyl)
            /* i18n-hint: changing speed of audio "from" one value "to" another
             "rpm" means "revolutions per minute" as on a vinyl record turntable
             */
            .Name(XO("From rpm"))
            .MinSize( { 100, -1 } )
            /* i18n-hint: changing speed of audio "from" one value "to" another */
            .AddChoice(XXC("&from", "change speed"), kVinylStrings);

         mpChoice_ToVinyl = S.Id(ID_ToVinyl)
            /* i18n-hint: changing speed of audio "from" one value "to" another
             "rpm" means "revolutions per minute" as on a vinyl record turntable
             */
            .Name(XO("To rpm"))
            .MinSize( { 100, -1 } )
            /* i18n-hint: changing speed of audio "from" one value "to" another */
            .AddChoice(XXC("&to", "change speed"), kVinylStrings);
      }
      S.EndMultiColumn();

      // From/To time controls.
      S.StartStatic(XO("Selection Length"), 0);
      {
         S.StartMultiColumn(2, wxALIGN_LEFT);
         {
            S.AddPrompt(XXO("C&urrent Length:"));

            mpFromLengthCtrl = safenew
                  NumericTextCtrl(S.GetParent(), wxID_ANY,
                                 NumericConverter::TIME,
                                 mFormat,
                                 mFromLength,
                                 mProjectRate,
                                 NumericTextCtrl::Options{}
                                  .ReadOnly(true)
                                  .MenuEnabled(false));

            S.ToolTip(XO("Current length of selection."))
               /* i18n-hint: changing speed of audio "from" one value "to" another */
               .Name(XC("from", "change speed"))
               .Position(wxALIGN_LEFT)
               .AddWindow(mpFromLengthCtrl);

            S.AddPrompt(XXO("&New Length:"));

            mpToLengthCtrl = safenew
                  NumericTextCtrl(S.GetParent(), ID_ToLength,
                                 NumericConverter::TIME,
                                 mFormat,
                                 mToLength,
                                 mProjectRate);

            /* i18n-hint: changing speed of audio "from" one value "to" another */
            S.Name(XC("to", "change speed"))
               .Position(wxALIGN_LEFT)
               .AddWindow(mpToLengthCtrl);
         }
         S.EndMultiColumn();
      }
      S.EndStatic();
   }
   S.EndVerticalLay();
}

bool EffectChangeSpeed::TransferDataToWindow()
{
   mbLoopDetect = true;

   if (!mUIParent->TransferDataToWindow())
   {
      return false;
   }

   if (mFromVinyl == kVinyl_NA)
   {
      mFromVinyl = kVinyl_33AndAThird;
   }

   Update_Text_PercentChange();
   Update_Text_Multiplier();
   Update_Slider_PercentChange();
   Update_TimeCtrl_ToLength();

   // Set from/to Vinyl controls - mFromVinyl must be set first.
   mpChoice_FromVinyl->SetSelection(mFromVinyl);
   // Then update to get correct mToVinyl.
   Update_Vinyl();
   // Then update ToVinyl control.
   mpChoice_ToVinyl->SetSelection(mToVinyl);

   // Set From Length control.
   // Set the format first so we can get sample accuracy.
   mpFromLengthCtrl->SetFormatName(mFormat);
   mpFromLengthCtrl->SetValue(mFromLength);

   mbLoopDetect = false;

   return true;
}

bool EffectChangeSpeed::TransferDataFromWindow()
{
   // mUIParent->TransferDataFromWindow() loses some precision, so save and restore it.
   double exactPercent = m_PercentChange;
   if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
   {
      return false;
   }
   m_PercentChange = exactPercent;

   SetPrivateConfig(GetCurrentSettingsGroup(), wxT("TimeFormat"), mFormat.Internal());
   SetPrivateConfig(GetCurrentSettingsGroup(), wxT("VinylChoice"), mFromVinyl);

   return true;
}

// EffectChangeSpeed implementation

// Labels are time-scaled linearly inside the affected region, and labels after
// the region are shifted along according to how the region size changed.
bool EffectChangeSpeed::ProcessLabelTrack(LabelTrack *lt)
{
   RegionTimeWarper warper { mT0, mT1,
      std::make_unique<LinearTimeWarper>(mT0, mT0,
                                         mT1, mT0 + (mT1-mT0)*mFactor) };
   lt->WarpLabels(warper);
   return true;
}

// ProcessOne() takes a track, transforms it to bunch of buffer-blocks,
// and calls libsamplerate code on these blocks.
bool EffectChangeSpeed::ProcessOne(WaveTrack * track,
                           sampleCount start, sampleCount end)
{
   if (track == NULL)
      return false;

   // initialization, per examples of Mixer::Mixer and
   // EffectSoundTouch::ProcessOne

   
   auto outputTrack = track->EmptyCopy();

   //Get the length of the selection (as double). len is
   //used simple to calculate a progress meter, so it is easier
   //to make it a double now than it is to do it later
   auto len = (end - start).as_double();

   // Initiate processing buffers, most likely shorter than
   // the length of the selection being processed.
   auto inBufferSize = track->GetMaxBlockSize();

   Floats inBuffer{ inBufferSize };

   // mFactor is at most 100-fold so this shouldn't overflow size_t
   auto outBufferSize = size_t( mFactor * inBufferSize + 10 );
   Floats outBuffer{ outBufferSize };

   // Set up the resampling stuff for this track.
   Resample resample(true, mFactor, mFactor); // constant rate resampling

   //Go through the track one buffer at a time. samplePos counts which
   //sample the current buffer starts at.
   bool bResult = true;
   auto samplePos = start;
   while (samplePos < end) {
      //Get a blockSize of samples (smaller than the size of the buffer)
      auto blockSize = limitSampleBufferSize(
         track->GetBestBlockSize(samplePos),
         end - samplePos
      );

      //Get the samples from the track and put them in the buffer
      track->GetFloats(inBuffer.get(), samplePos, blockSize);

      const auto results = resample.Process(mFactor,
                                    inBuffer.get(),
                                    blockSize,
                                    ((samplePos + blockSize) >= end),
                                    outBuffer.get(),
                                    outBufferSize);
      const auto outgen = results.second;

      if (outgen > 0)
         outputTrack->Append((samplePtr)outBuffer.get(), floatSample,
                             outgen);

      // Increment samplePos
      samplePos += results.first;

      // Update the Progress meter
      if (TrackProgress(mCurTrackNum, (samplePos - start).as_double() / len)) {
         bResult = false;
         break;
      }
   }

   // Flush the output WaveTrack (since it's buffered, too)
   outputTrack->Flush();

   // Take the output track and insert it in place of the original
   // sample data
   double newLength = outputTrack->GetEndTime();
   if (bResult)
   {
      // Silenced samples will be inserted in gaps between clips, so capture where these
      // gaps are for later deletion
      std::vector<std::pair<double, double>> gaps;
      double last = mCurT0;
      auto clips = track->SortedClipArray();
      auto front = clips.front();
      auto back = clips.back();
      for (auto &clip : clips) {
         auto st = clip->GetPlayStartTime();
         auto et = clip->GetPlayEndTime();

         if (st >= mCurT0 || et < mCurT1) {
            if (mCurT0 < st && clip == front) {
               gaps.push_back(std::make_pair(mCurT0, st));
            }
            else if (last < st && mCurT0 <= last ) {
               gaps.push_back(std::make_pair(last, st));
            }

            if (et < mCurT1 && clip == back) {
               gaps.push_back(std::make_pair(et, mCurT1));
            }
         }
         last = et;
      }

      LinearTimeWarper warper { mCurT0, mCurT0, mCurT1, mCurT0 + newLength };

      // Take the output track and insert it in place of the original sample data
      track->ClearAndPaste(mCurT0, mCurT1, outputTrack.get(), true, true, &warper);

      // Finally, recreate the gaps
      for (auto gap : gaps) {
         auto st = track->LongSamplesToTime(track->TimeToLongSamples(gap.first));
         auto et = track->LongSamplesToTime(track->TimeToLongSamples(gap.second));
         if (st >= mCurT0 && et <= mCurT1 && st != et)
         {
            track->SplitDelete(warper.Warp(st), warper.Warp(et));
         }
      }
   }

   if (newLength > mMaxNewLength)
      mMaxNewLength = newLength;

   return bResult;
}

// handler implementations for EffectChangeSpeed

void EffectChangeSpeed::OnText_PercentChange(wxCommandEvent & WXUNUSED(evt))
{
   if (mbLoopDetect)
      return;

   mpTextCtrl_PercentChange->GetValidator()->TransferFromWindow();
   UpdateUI();

   mbLoopDetect = true;
   Update_Text_Multiplier();
   Update_Slider_PercentChange();
   Update_Vinyl();
   Update_TimeCtrl_ToLength();
   mbLoopDetect = false;
}

void EffectChangeSpeed::OnText_Multiplier(wxCommandEvent & WXUNUSED(evt))
{
   if (mbLoopDetect)
      return;

   mpTextCtrl_Multiplier->GetValidator()->TransferFromWindow();
   m_PercentChange = 100 * (mMultiplier - 1);
   UpdateUI();

   mbLoopDetect = true;
   Update_Text_PercentChange();
   Update_Slider_PercentChange();
   Update_Vinyl();
   Update_TimeCtrl_ToLength();
   mbLoopDetect = false;
}

void EffectChangeSpeed::OnSlider_PercentChange(wxCommandEvent & WXUNUSED(evt))
{
   if (mbLoopDetect)
      return;

   m_PercentChange = (double)(mpSlider_PercentChange->GetValue());
   // Warp positive values to actually go up faster & further than negatives.
   if (m_PercentChange > 0.0)
      m_PercentChange = pow(m_PercentChange, kSliderWarp);
   UpdateUI();

   mbLoopDetect = true;
   Update_Text_PercentChange();
   Update_Text_Multiplier();
   Update_Vinyl();
   Update_TimeCtrl_ToLength();
   mbLoopDetect = false;
}

void EffectChangeSpeed::OnChoice_Vinyl(wxCommandEvent & WXUNUSED(evt))
{
   // Treat mpChoice_FromVinyl and mpChoice_ToVinyl as one control since we need
   // both to calculate Percent Change.
   mFromVinyl = mpChoice_FromVinyl->GetSelection();
   mToVinyl = mpChoice_ToVinyl->GetSelection();
   // Use this as the 'preferred' choice.
   if (mFromVinyl != kVinyl_NA) {
      SetPrivateConfig(GetCurrentSettingsGroup(), wxT("VinylChoice"), mFromVinyl);
   }

   // If mFromVinyl & mToVinyl are set, then there's a NEW percent change.
   if ((mFromVinyl != kVinyl_NA) && (mToVinyl != kVinyl_NA))
   {
      double fromRPM;
      double toRPM;
      switch (mFromVinyl) {
      default:
      case kVinyl_33AndAThird:   fromRPM = 33.0 + (1.0 / 3.0); break;
      case kVinyl_45:            fromRPM = 45.0; break;
      case kVinyl_78:            fromRPM = 78; break;
      }
      switch (mToVinyl) {
      default:
      case kVinyl_33AndAThird:   toRPM = 33.0 + (1.0 / 3.0); break;
      case kVinyl_45:            toRPM = 45.0; break;
      case kVinyl_78:            toRPM = 78; break;
      }
      m_PercentChange = ((toRPM * 100.0) / fromRPM) - 100.0;
      UpdateUI();

      mbLoopDetect = true;
      Update_Text_PercentChange();
      Update_Text_Multiplier();
      Update_Slider_PercentChange();
      Update_TimeCtrl_ToLength();
   }
   mbLoopDetect = false;
}

void EffectChangeSpeed::OnTimeCtrl_ToLength(wxCommandEvent & WXUNUSED(evt))
{
   if (mbLoopDetect)
      return;

   mToLength = mpToLengthCtrl->GetValue();
   // Division by (double) 0.0 is not an error and we want to show "infinite" in
   // text controls, so take care that we handle infinite values when they occur.
   m_PercentChange = ((mFromLength * 100.0) / mToLength) - 100.0;
   UpdateUI();

   mbLoopDetect = true;

   Update_Text_PercentChange();
   Update_Text_Multiplier();
   Update_Slider_PercentChange();
   Update_Vinyl();

   mbLoopDetect = false;
}

void EffectChangeSpeed::OnTimeCtrlUpdate(wxCommandEvent & evt)
{
   mFormat = NumericConverter::LookupFormat(
      NumericConverter::TIME, evt.GetString() );

   mpFromLengthCtrl->SetFormatName(mFormat);
   // Update From/To Length controls (precision has changed).
   mpToLengthCtrl->SetValue(mToLength);
   mpFromLengthCtrl->SetValue(mFromLength);
}

// helper functions

void EffectChangeSpeed::Update_Text_PercentChange()
// Update Text Percent control from percent change.
{
   mpTextCtrl_PercentChange->GetValidator()->TransferToWindow();
}

void EffectChangeSpeed::Update_Text_Multiplier()
// Update Multiplier control from percent change.
{
   mMultiplier =  1 + (m_PercentChange) / 100.0;
   mpTextCtrl_Multiplier->GetValidator()->TransferToWindow();
}

void EffectChangeSpeed::Update_Slider_PercentChange()
// Update Slider Percent control from percent change.
{
   auto unwarped = std::min<double>(m_PercentChange, MAX_Percentage);
   if (unwarped > 0.0)
      // Un-warp values above zero to actually go up to kSliderMax.
      unwarped = pow(m_PercentChange, (1.0 / kSliderWarp));

   // Caution: m_PercentChange could be infinite.
   int unwarpedi = (int)(unwarped + 0.5);
   unwarpedi = std::min<int>(unwarpedi, (int)kSliderMax);

   mpSlider_PercentChange->SetValue(unwarpedi);
}

void EffectChangeSpeed::Update_Vinyl()
// Update Vinyl controls from percent change.
{
   // Match Vinyl rpm when within 0.01% of a standard ratio.
   // Ratios calculated as: ((toRPM / fromRPM) - 1) * 100 * 100

   // Caution: m_PercentChange could be infinite
   int ratio = (int)((m_PercentChange * 100) + 0.5);

   switch (ratio)
   {
      case 0: // toRPM is the same as fromRPM
         if (mFromVinyl != kVinyl_NA) {
            mpChoice_ToVinyl->SetSelection(mpChoice_FromVinyl->GetSelection());
         } else {
            // Use the last saved option.
            GetPrivateConfig(GetCurrentSettingsGroup(), wxT("VinylChoice"), mFromVinyl, 0);
            mpChoice_FromVinyl->SetSelection(mFromVinyl);
            mpChoice_ToVinyl->SetSelection(mFromVinyl);
         }
         break;
      case 3500:
         mpChoice_FromVinyl->SetSelection(kVinyl_33AndAThird);
         mpChoice_ToVinyl->SetSelection(kVinyl_45);
         break;
      case 13400:
         mpChoice_FromVinyl->SetSelection(kVinyl_33AndAThird);
         mpChoice_ToVinyl->SetSelection(kVinyl_78);
         break;
      case -2593:
         mpChoice_FromVinyl->SetSelection(kVinyl_45);
         mpChoice_ToVinyl->SetSelection(kVinyl_33AndAThird);
         break;
      case 7333:
         mpChoice_FromVinyl->SetSelection(kVinyl_45);
         mpChoice_ToVinyl->SetSelection(kVinyl_78);
         break;
      case -5727:
         mpChoice_FromVinyl->SetSelection(kVinyl_78);
         mpChoice_ToVinyl->SetSelection(kVinyl_33AndAThird);
         break;
      case -4231:
         mpChoice_FromVinyl->SetSelection(kVinyl_78);
         mpChoice_ToVinyl->SetSelection(kVinyl_45);
         break;
      default:
         mpChoice_ToVinyl->SetSelection(kVinyl_NA);
   }
   // and update variables.
   mFromVinyl = mpChoice_FromVinyl->GetSelection();
   mToVinyl = mpChoice_ToVinyl->GetSelection();
}

void EffectChangeSpeed::Update_TimeCtrl_ToLength()
// Update ToLength control from percent change.
{
   mToLength = (mFromLength * 100.0) / (100.0 + m_PercentChange);

   // Set the format first so we can get sample accuracy.
   mpToLengthCtrl->SetFormatName(mFormat);
   // Negative times do not make sense.
   // 359999 = 99h:59m:59s which is a little less disturbing than overflow characters
   // though it may still look a bit strange with some formats.
   mToLength = TrapDouble(mToLength, 0.0, 359999.0);
   mpToLengthCtrl->SetValue(mToLength);
}

void EffectChangeSpeed::UpdateUI()
// Disable OK and Preview if not in sensible range.
{
   EnableApply(m_PercentChange >= MIN_Percentage && m_PercentChange <= MAX_Percentage);
}
