audacia/src/effects/ChangeSpeed.cpp
Paul Licameli e4a7c9ba5b Uses of TranslatableString as value of XO macro...
... It is not implicitly convertible from wxString, compelling many uses of
the new type to fix compilation.
2019-12-01 18:05:20 -05:00

788 lines
23 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
ChangeSpeed.cpp
Vaughan Johnson, Dominic Mazzoni
*******************************************************************//**
\class EffectChangeSpeed
\brief An Effect that affects both pitch & speed.
*//*******************************************************************/
#include "../Audacity.h"
#include "ChangeSpeed.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 "../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,
nVinyl
};
static const TranslatableString kVinylStrings[nVinyl] =
{
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
//
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 CHANGESPEED_PLUGIN_SYMBOL;
}
wxString EffectChangeSpeed::GetDescription()
{
return _("Changes the speed of a track, also changing its pitch");
}
wxString EffectChangeSpeed::ManualPage()
{
return wxT("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);
wxASSERT(nVinyl == WXSIZEOF(kVinylStrings));
wxArrayStringEx vinylChoices;
for (int i = 0; i < nVinyl; i++)
{
if (i == kVinyl_NA)
{
vinylChoices.push_back(wxGetTranslation(kVinylStrings[i]));
}
else
{
vinylChoices.push_back(kVinylStrings[i]);
}
}
S.SetBorder(5);
S.StartVerticalLay(0);
{
S.AddSpace(0, 5);
S.AddTitle(_("Change Speed, affecting both Tempo and Pitch"));
S.AddSpace(0, 10);
// Speed multiplier and percent change controls.
S.StartMultiColumn(4, wxCENTER);
{
FloatingPointValidator<double> vldMultiplier(3, &mMultiplier, NumValidatorStyle::THREE_TRAILING_ZEROES);
vldMultiplier.SetRange(MIN_Percentage / 100.0, ((MAX_Percentage / 100.0) + 1));
mpTextCtrl_Multiplier =
S.Id(ID_Multiplier).AddTextBox(_("Speed Multiplier:"), wxT(""), 12);
mpTextCtrl_Multiplier->SetValidator(vldMultiplier);
FloatingPointValidator<double> vldPercentage(3, &m_PercentChange, NumValidatorStyle::THREE_TRAILING_ZEROES);
vldPercentage.SetRange(MIN_Percentage, MAX_Percentage);
mpTextCtrl_PercentChange =
S.Id(ID_PercentChange).AddTextBox(_("Percent Change:"), wxT(""), 12);
mpTextCtrl_PercentChange->SetValidator(vldPercentage);
}
S.EndMultiColumn();
// Percent change slider.
S.StartHorizontalLay(wxEXPAND);
{
S.SetStyle(wxSL_HORIZONTAL);
mpSlider_PercentChange =
S.Id(ID_PercentChange).AddSlider( {}, 0, (int)kSliderMax, (int)MIN_Percentage);
mpSlider_PercentChange->SetName(_("Percent Change"));
}
S.EndHorizontalLay();
// Vinyl rpm controls.
S.StartMultiColumn(5, wxCENTER);
{
/* i18n-hint: "rpm" is an English abbreviation meaning "revolutions per minute". */
S.AddUnits(_("Standard Vinyl rpm:"));
mpChoice_FromVinyl = S.Id(ID_FromVinyl)
/* i18n-hint: changing a quantity "from" one value "to" another */
.AddChoice(_("from"), vinylChoices);
mpChoice_FromVinyl->SetName(_("From rpm"));
mpChoice_FromVinyl->SetSizeHints(100, -1);
mpChoice_ToVinyl =
S.Id(ID_ToVinyl).AddChoice(_("to"), vinylChoices);
mpChoice_ToVinyl->SetName(_("To rpm"));
mpChoice_ToVinyl->SetSizeHints(100, -1);
}
S.EndMultiColumn();
// From/To time controls.
S.StartStatic(_("Selection Length"), 0);
{
S.StartMultiColumn(2, wxALIGN_LEFT);
{
S.AddPrompt(_("Current Length:"));
mpFromLengthCtrl = safenew
NumericTextCtrl(S.GetParent(), wxID_ANY,
NumericConverter::TIME,
mFormat,
mFromLength,
mProjectRate,
NumericTextCtrl::Options{}
.ReadOnly(true)
.MenuEnabled(false));
mpFromLengthCtrl->SetName(_("from"));
mpFromLengthCtrl->SetToolTip(_("Current length of selection."));
S.AddWindow(mpFromLengthCtrl, wxALIGN_LEFT);
S.AddPrompt(_("New Length:"));
mpToLengthCtrl = safenew
NumericTextCtrl(S.GetParent(), ID_ToLength,
NumericConverter::TIME,
mFormat,
mToLength,
mProjectRate);
mpToLengthCtrl->SetName(_("to"));
S.AddWindow(mpToLengthCtrl, wxALIGN_LEFT);
}
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 = mFactory->NewWaveTrack(track->GetSampleFormat(),
track->GetRate());
//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->Get((samplePtr) inBuffer.get(), floatSample, 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)
{
LinearTimeWarper warper { mCurT0, mCurT0, mCurT1, mCurT0 + newLength };
track->ClearAndPaste(
mCurT0, mCurT1, outputTrack.get(), true, false, &warper);
}
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);
}