/********************************************************************** 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 #endif #include #include #include #include #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"); } 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(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>( 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>( 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>( 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>( 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>( 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(&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