text: /**********************************************************************
  
    Audacity: A Digital Audio Editor
  
    ChangeTempo.cpp
  
    Vaughan Johnson,
    Dominic Mazzoni
  
  *******************************************************************//**
  
  \class EffectChangeTempo
  \brief An EffectSoundTouch provides speeding up or
    slowing down tempo without changing pitch.
  
  *//*******************************************************************/
  
  
  
  #if USE_SOUNDTOUCH
  #include "ChangeTempo.h"
  
  #if USE_SBSMS
  #include <wx/valgen.h>
  #endif
  
  #include <math.h>
  
  #include <wx/intl.h>
  #include <wx/checkbox.h>
  #include <wx/slider.h>
  
  #include "../shuttle/Shuttle.h"
  #include "../shuttle/ShuttleGui.h"
  #include "../widgets/valnum.h"
  #include "TimeWarper.h"
  
  #include "LoadEffects.h"
  
  // Soundtouch defines these as well, which are also in generated configmac.h
  // and configunix.h, so get rid of them before including,
  // to avoid compiler warnings, and be sure to do this
  // after all other #includes, to avoid any mischief that might result
  // from doing the un-definitions before seeing any wx headers.
  #undef PACKAGE_NAME
  #undef PACKAGE_STRING
  #undef PACKAGE_TARNAME
  #undef PACKAGE_VERSION
  #undef PACKAGE_BUGREPORT
  #undef PACKAGE
  #undef VERSION
  #include "SoundTouch.h"
  
  enum
  {
     ID_PercentChange = 10000,
     ID_FromBPM,
     ID_ToBPM,
     ID_FromLength,
     ID_ToLength
  };
  
  // 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,  -95.0,   3000.0,  1  );
  Param( UseSBSMS,     bool,    wxT("SBSMS"),     false, false,   true,    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.
  
  //
  // EffectChangeTempo
  //
  
  const ComponentInterfaceSymbol EffectChangeTempo::Symbol
  { XO("Change Tempo") };
  
  namespace{ BuiltinEffectsModule::Registration< EffectChangeTempo > reg; }
  
  BEGIN_EVENT_TABLE(EffectChangeTempo, wxEvtHandler)
      EVT_TEXT(ID_PercentChange, EffectChangeTempo::OnText_PercentChange)
      EVT_SLIDER(ID_PercentChange, EffectChangeTempo::OnSlider_PercentChange)
      EVT_TEXT(ID_FromBPM, EffectChangeTempo::OnText_FromBPM)
      EVT_TEXT(ID_ToBPM, EffectChangeTempo::OnText_ToBPM)
      EVT_TEXT(ID_ToLength, EffectChangeTempo::OnText_ToLength)
  END_EVENT_TABLE()
  
  EffectChangeTempo::EffectChangeTempo()
  {
     m_PercentChange = DEF_Percentage;
     m_FromBPM = 0.0; // indicates not yet set
     m_ToBPM = 0.0; // indicates not yet set
     m_FromLength = 0.0;
     m_ToLength = 0.0;
  
     m_bLoopDetect = false;
  
  #if USE_SBSMS
     mUseSBSMS = DEF_UseSBSMS;
  #else
     mUseSBSMS = false;
  #endif
  
     SetLinearEffectFlag(true);
  }
  
  EffectChangeTempo::~EffectChangeTempo()
  {
  }
  
  // ComponentInterface implementation
  
  ComponentInterfaceSymbol EffectChangeTempo::GetSymbol()
  {
     return Symbol;
  }
  
  TranslatableString EffectChangeTempo::GetDescription()
  {
     return XO("Changes the tempo of a selection without changing its pitch");
  }
  
  ManualPageID EffectChangeTempo::ManualPage()
  {
     return L"Change_Tempo";
  }
  
  // EffectDefinitionInterface implementation
  
  EffectType EffectChangeTempo::GetType()
  {
     return EffectTypeProcess;
  }
  
  bool EffectChangeTempo::SupportsAutomation()
  {
     return true;
  }
  
  // EffectClientInterface implementation
  bool EffectChangeTempo::DefineParams( ShuttleParams & S ){
     S.SHUTTLE_PARAM( m_PercentChange, Percentage );
     S.SHUTTLE_PARAM( mUseSBSMS, UseSBSMS );
     return true;
  }
  
  bool EffectChangeTempo::GetAutomationParameters(CommandParameters & parms)
  {
     parms.Write(KEY_Percentage, m_PercentChange);
     parms.Write(KEY_UseSBSMS, mUseSBSMS);
  
     return true;
  }
  
  bool EffectChangeTempo::SetAutomationParameters(CommandParameters & parms)
  {
     ReadAndVerifyDouble(Percentage);
     m_PercentChange = Percentage;
  
  #if USE_SBSMS
     ReadAndVerifyBool(UseSBSMS);
     mUseSBSMS = UseSBSMS;
  #else
     mUseSBSMS = false;
  #endif
  
     return true;
  }
  
  // Effect implementation
  
  double EffectChangeTempo::CalcPreviewInputLength(double previewLength)
  {
     return previewLength * (100.0 + m_PercentChange) / 100.0;
  }
  
  bool EffectChangeTempo::CheckWhetherSkipEffect()
  {
     return (m_PercentChange == 0.0);
  }
  
  bool EffectChangeTempo::Init()
  {
     // The selection might have changed since the last time EffectChangeTempo
     // was invoked, so recalculate the Length parameters.
     m_FromLength = mT1 - mT0;
     m_ToLength = (m_FromLength * 100.0) / (100.0 + m_PercentChange);
  
     return true;
  }
  
  bool EffectChangeTempo::Process()
  {
     bool success = false;
  
  #if USE_SBSMS
     if (mUseSBSMS)
     {
        double tempoRatio = 1.0 + m_PercentChange / 100.0;
        EffectSBSMS proxy;
        proxy.mProxyEffectName = XO("High Quality Tempo Change");
        proxy.setParameters(tempoRatio, 1.0);
        success = Delegate(proxy, *mUIParent, nullptr);
     }
     else
  #endif
     {
        auto initer = [&](soundtouch::SoundTouch *soundtouch)
        {
           soundtouch->setTempoChange(m_PercentChange);
        };
        double mT1Dashed = mT0 + (mT1 - mT0)/(m_PercentChange/100.0 + 1.0);
        RegionTimeWarper warper{ mT0, mT1,
           std::make_unique<LinearTimeWarper>(mT0, mT0, mT1, mT1Dashed )  };
        success = EffectSoundTouch::ProcessWithTimeWarper(initer, warper, false);
     }
  
     if(success)
        mT1 = mT0 + (mT1 - mT0)/(m_PercentChange/100 + 1.);
  
     return success;
  }
  
  void EffectChangeTempo::PopulateOrExchange(ShuttleGui & S)
  {
     enum { precision = 2 };
  
     S.StartVerticalLay(0);
     {
        S.AddSpace(0, 5);
        S.AddTitle(XO("Change Tempo without Changing Pitch"));
        S.SetBorder(5);
  
        //
        S.StartMultiColumn(2, wxCENTER);
        {
           m_pTextCtrl_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();
  
        //
        S.StartHorizontalLay(wxEXPAND);
        {
           m_pSlider_PercentChange = S.Id(ID_PercentChange)
              .Name(XO("Percent Change"))
              .Style(wxSL_HORIZONTAL)
              .AddSlider( {}, 0, (int)kSliderMax, (int)MIN_Percentage);
        }
        S.EndHorizontalLay();
  
        S.StartStatic(XO("Beats per minute"));
        {
           S.StartHorizontalLay(wxALIGN_CENTER);
           {
              m_pTextCtrl_FromBPM = S.Id(ID_FromBPM)
                 /* i18n-hint: changing tempo "from" one value "to" another */
                 .Name(XO("Beats per minute, from"))
                 .Validator<FloatingPointValidator<double>>(
                    3, &m_FromBPM,
                    NumValidatorStyle::THREE_TRAILING_ZEROES
                       | NumValidatorStyle::ZERO_AS_BLANK)
                 /* i18n-hint: changing tempo "from" one value "to" another */
                 .AddTextBox(XXC("&from", "change tempo"), wxT(""), 12);
  
              m_pTextCtrl_ToBPM = S.Id(ID_ToBPM)
                 /* i18n-hint: changing tempo "from" one value "to" another */
                 .Name(XO("Beats per minute, to"))
                 .Validator<FloatingPointValidator<double>>(
                    3, &m_ToBPM,
                    NumValidatorStyle::THREE_TRAILING_ZEROES
                       | NumValidatorStyle::ZERO_AS_BLANK)
                 /* i18n-hint: changing tempo "from" one value "to" another */
                 .AddTextBox(XXC("&to", "change tempo"), wxT(""), 12);
           }
           S.EndHorizontalLay();
        }
        S.EndStatic();
  
        //
        S.StartStatic(XO("Length (seconds)"));
        {
           S.StartHorizontalLay(wxALIGN_CENTER);
           {
              m_pTextCtrl_FromLength = S.Id(ID_FromLength)
                 .Disable() // Disable because the value comes from the
                         // user selection.
                 .Validator<FloatingPointValidator<double>>(
                    precision, &m_FromLength,
                    NumValidatorStyle::TWO_TRAILING_ZEROES)
                 /* i18n-hint: changing tempo "from" one value "to" another */
                 .AddTextBox(XXC("from", "change tempo"), wxT(""), 12);
              m_pTextCtrl_ToLength = S.Id(ID_ToLength)
                 .Validator<FloatingPointValidator<double>>(
                    2, &m_ToLength, NumValidatorStyle::TWO_TRAILING_ZEROES,
                    // min and max need same precision as what we're validating (bug 963)
                    RoundValue( precision,
                       (m_FromLength * 100.0) / (100.0 + MAX_Percentage) ),
                    RoundValue( precision,
                       (m_FromLength * 100.0) / (100.0 + MIN_Percentage) )
                 )
                 /* i18n-hint: changing tempo "from" one value "to" another */
                 .AddTextBox(XXC("t&o", "change tempo"), wxT(""), 12);
           }
           S.EndHorizontalLay();
        }
        S.EndStatic();
  
  #if USE_SBSMS
        S.StartMultiColumn(2);
        {
           mUseSBSMSCheckBox = S.Validator<wxGenericValidator>(&mUseSBSMS)
              .AddCheckBox(XXO("&Use high quality stretching (slow)"),
                                               mUseSBSMS);
        }
        S.EndMultiColumn();
  #endif
  
     }
     S.EndVerticalLay();
  
     return;
  }
  
  bool EffectChangeTempo::TransferDataToWindow()
  {
     // Reset from length because it can be changed by Preview
     m_FromLength = mT1 - mT0;
  
     m_bLoopDetect = true;
  
     if (!mUIParent->TransferDataToWindow())
     {
        return false;
     }
  
     // percent change controls
     Update_Slider_PercentChange();
     Update_Text_ToBPM();
     Update_Text_ToLength();
  
     m_bLoopDetect = false;
  
     // Set the accessibility name here because we need m_pTextCtrl_FromLength to have had its value set
     m_pTextCtrl_ToLength->SetName(
        wxString::Format( _("Length in seconds from %s, to"),
           m_pTextCtrl_FromLength->GetValue() ) );
  
     return true;
  }
  
  bool EffectChangeTempo::TransferDataFromWindow()
  {
     if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
     {
        return false;
     }
  
     return true;
  }
  
  // handler implementations for EffectChangeTempo
  
  void EffectChangeTempo::OnText_PercentChange(wxCommandEvent & WXUNUSED(evt))
  {
     if (m_bLoopDetect)
        return;
  
     m_pTextCtrl_PercentChange->GetValidator()->TransferFromWindow();
  
     m_bLoopDetect = true;
     Update_Slider_PercentChange();
     Update_Text_ToBPM();
     Update_Text_ToLength();
     m_bLoopDetect = false;
  }
  
  void EffectChangeTempo::OnSlider_PercentChange(wxCommandEvent & WXUNUSED(evt))
  {
     if (m_bLoopDetect)
        return;
  
     m_PercentChange = (double)(m_pSlider_PercentChange->GetValue());
     // Warp positive values to actually go up faster & further than negatives.
     if (m_PercentChange > 0.0)
        m_PercentChange = pow(m_PercentChange, kSliderWarp);
  
     m_bLoopDetect = true;
     Update_Text_PercentChange();
     Update_Text_ToBPM();
     Update_Text_ToLength();
     m_bLoopDetect = false;
  }
  
  void EffectChangeTempo::OnText_FromBPM(wxCommandEvent & WXUNUSED(evt))
  {
     if (m_bLoopDetect)
        return;
  
     m_pTextCtrl_FromBPM->GetValidator()->TransferFromWindow();
  
     m_bLoopDetect = true;
  
     Update_Text_ToBPM();
  
     m_bLoopDetect = false;
  }
  
  void EffectChangeTempo::OnText_ToBPM(wxCommandEvent & WXUNUSED(evt))
  {
     if (m_bLoopDetect)
        return;
  
     m_pTextCtrl_ToBPM->GetValidator()->TransferFromWindow();
  
     m_bLoopDetect = true;
  
     // If FromBPM has already been set, then there's a NEW percent change.
     if (m_FromBPM != 0.0 && m_ToBPM != 0.0)
     {
        m_PercentChange = ((m_ToBPM * 100.0) / m_FromBPM) - 100.0;
  
        Update_Text_PercentChange();
        Update_Slider_PercentChange();
  
        Update_Text_ToLength();
     }
  
     m_bLoopDetect = false;
  }
  
  void EffectChangeTempo::OnText_ToLength(wxCommandEvent & WXUNUSED(evt))
  {
     if (m_bLoopDetect)
        return;
  
     m_pTextCtrl_ToLength->GetValidator()->TransferFromWindow();
  
     if (m_ToLength != 0.0)
     {
        m_PercentChange = ((m_FromLength * 100.0) / m_ToLength) - 100.0;
     }
  
     m_bLoopDetect = true;
  
     Update_Text_PercentChange();
     Update_Slider_PercentChange();
  
     Update_Text_ToBPM();
  
     m_bLoopDetect = false;
  }
  
  // helper fns
  
  void EffectChangeTempo::Update_Text_PercentChange()
  {
     m_pTextCtrl_PercentChange->GetValidator()->TransferToWindow();
  }
  
  void EffectChangeTempo::Update_Slider_PercentChange()
  {
     double unwarped = m_PercentChange;
     if (unwarped > 0.0)
        // Un-warp values above zero to actually go up to kSliderMax.
        unwarped = pow(m_PercentChange, (1.0 / kSliderWarp));
  
     // Add 0.5 to unwarped so trunc -> round.
     m_pSlider_PercentChange->SetValue((int)(unwarped + 0.5));
  }
  
  void EffectChangeTempo::Update_Text_ToBPM()
  // Use m_FromBPM & m_PercentChange to set NEW m_ToBPM & control.
  {
     m_ToBPM = (((m_FromBPM * (100.0 + m_PercentChange)) / 100.0));
     m_pTextCtrl_ToBPM->GetValidator()->TransferToWindow();
  }
  
  void EffectChangeTempo::Update_Text_ToLength()
  // Use m_FromLength & m_PercentChange to set NEW m_ToLength & control.
  {
     m_ToLength = (m_FromLength * 100.0) / (100.0 + m_PercentChange);
     m_pTextCtrl_ToLength->GetValidator()->TransferToWindow();
  }
  
  #endif // USE_SOUNDTOUCH
  