mirror of
https://github.com/cookiengineer/audacity
synced 2025-07-17 17:17:40 +02:00
Problem: Validation of the length fails, because what should be the min and max are in fact the max and min. Introduced by commit 1d32824, which got the min and max the wrong way round.
496 lines
13 KiB
C++
496 lines
13 KiB
C++
/**********************************************************************
|
|
|
|
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 );
|
|
}
|
|
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 Change:"), 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("to"), 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
|