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

  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.

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

#include "../Audacity.h" // for USE_SOUNDTOUCH

#if USE_SOUNDTOUCH
#include "ChangeTempo.h"

#if USE_SBSMS
#include "../../../lib-src/header-substitutes/sbsms.h"
#include <wx/valgen.h>
#endif

#include <math.h>

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

#include "../Shuttle.h"
#include "../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");
}

wxString EffectChangeTempo::ManualPage()
{
   return wxT("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);

   mSoundTouch.reset();

   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
   {
      mSoundTouch = std::make_unique<soundtouch::SoundTouch>();
      mSoundTouch->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(warper);
   }

   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(XO("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 a quantity "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 a quantity "from" one value "to" another */
               .AddTextBox(XO("&from"), wxT(""), 12);

            m_pTextCtrl_ToBPM = S.Id(ID_ToBPM)
               /* i18n-hint: changing a quantity "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 a quantity "from" one value "to" another */
               .AddTextBox(XO("&to"), 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 a quantity "from" one value "to" another */
               .AddTextBox(XO("from"), 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 a quantity "from" one value "to" another */
               .AddTextBox(XO("t&o"), wxT(""), 12);
         }
         S.EndHorizontalLay();
      }
      S.EndStatic();

#if USE_SBSMS
      S.StartMultiColumn(2);
      {
         mUseSBSMSCheckBox = S.Validator<wxGenericValidator>(&mUseSBSMS)
            .AddCheckBox(XO("&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
