1 /**********************************************************************
2 
3   Audacity: A Digital Audio Editor
4 
5   ChangeSpeed.cpp
6 
7   Vaughan Johnson, Dominic Mazzoni
8 
9 *******************************************************************//**
10 
11 \class EffectChangeSpeed
12 \brief An Effect that affects both pitch & speed.
13 
14 *//*******************************************************************/
15 
16 
17 #include "ChangeSpeed.h"
18 #include "LoadEffects.h"
19 
20 #include <math.h>
21 
22 #include <wx/choice.h>
23 #include <wx/intl.h>
24 #include <wx/slider.h>
25 
26 #include "../LabelTrack.h"
27 #include "Prefs.h"
28 #include "Resample.h"
29 #include "../Shuttle.h"
30 #include "../ShuttleGui.h"
31 #include "../widgets/NumericTextCtrl.h"
32 #include "../widgets/valnum.h"
33 
34 #include "TimeWarper.h"
35 #include "../WaveClip.h"
36 #include "../WaveTrack.h"
37 
38 enum
39 {
40    ID_PercentChange = 10000,
41    ID_Multiplier,
42    ID_FromVinyl,
43    ID_ToVinyl,
44    ID_ToLength
45 };
46 
47 // the standard vinyl rpm choices
48 // If the percent change is not one of these ratios, the choice control gets "n/a".
49 enum kVinyl
50 {
51    kVinyl_33AndAThird = 0,
52    kVinyl_45,
53    kVinyl_78,
54    kVinyl_NA
55 };
56 
57 static const TranslatableStrings kVinylStrings{
58    XO("33\u2153"),
59    XO("45"),
60    XO("78"),
61    /* i18n-hint: n/a is an English abbreviation meaning "not applicable". */
62    XO("n/a"),
63 };
64 
65 // Soundtouch is not reasonable below -99% or above 3000%.
66 
67 // Define keys, defaults, minimums, and maximums for the effect parameters
68 //
69 //     Name          Type     Key               Def   Min      Max      Scale
70 Param( Percentage,   double,  wxT("Percentage"), 0.0,  -99.0,   4900.0,  1  );
71 
72 // We warp the slider to go up to 400%, but user can enter higher values
73 static const double kSliderMax = 100.0;         // warped above zero to actually go up to 400%
74 static const double kSliderWarp = 1.30105;      // warp power takes max from 100 to 400.
75 
76 //
77 // EffectChangeSpeed
78 //
79 
80 const ComponentInterfaceSymbol EffectChangeSpeed::Symbol
81 { XO("Change Speed") };
82 
83 namespace{ BuiltinEffectsModule::Registration< EffectChangeSpeed > reg; }
84 
BEGIN_EVENT_TABLE(EffectChangeSpeed,wxEvtHandler)85 BEGIN_EVENT_TABLE(EffectChangeSpeed, wxEvtHandler)
86     EVT_TEXT(ID_PercentChange, EffectChangeSpeed::OnText_PercentChange)
87     EVT_TEXT(ID_Multiplier, EffectChangeSpeed::OnText_Multiplier)
88     EVT_SLIDER(ID_PercentChange, EffectChangeSpeed::OnSlider_PercentChange)
89     EVT_CHOICE(ID_FromVinyl, EffectChangeSpeed::OnChoice_Vinyl)
90     EVT_CHOICE(ID_ToVinyl, EffectChangeSpeed::OnChoice_Vinyl)
91     EVT_TEXT(ID_ToLength, EffectChangeSpeed::OnTimeCtrl_ToLength)
92     EVT_COMMAND(ID_ToLength, EVT_TIMETEXTCTRL_UPDATED, EffectChangeSpeed::OnTimeCtrlUpdate)
93 END_EVENT_TABLE()
94 
95 EffectChangeSpeed::EffectChangeSpeed()
96 {
97    // effect parameters
98    m_PercentChange = DEF_Percentage;
99 
100    mFromVinyl = kVinyl_33AndAThird;
101    mToVinyl = kVinyl_33AndAThird;
102    mFromLength = 0.0;
103    mToLength = 0.0;
104    mFormat = NumericConverter::DefaultSelectionFormat();
105    mbLoopDetect = false;
106 
107    SetLinearEffectFlag(true);
108 }
109 
~EffectChangeSpeed()110 EffectChangeSpeed::~EffectChangeSpeed()
111 {
112 }
113 
114 // ComponentInterface implementation
115 
GetSymbol()116 ComponentInterfaceSymbol EffectChangeSpeed::GetSymbol()
117 {
118    return Symbol;
119 }
120 
GetDescription()121 TranslatableString EffectChangeSpeed::GetDescription()
122 {
123    return XO("Changes the speed of a track, also changing its pitch");
124 }
125 
ManualPage()126 ManualPageID EffectChangeSpeed::ManualPage()
127 {
128    return L"Change_Speed";
129 }
130 
131 
132 // EffectDefinitionInterface implementation
133 
GetType()134 EffectType EffectChangeSpeed::GetType()
135 {
136    return EffectTypeProcess;
137 }
138 
139 // EffectClientInterface implementation
DefineParams(ShuttleParams & S)140 bool EffectChangeSpeed::DefineParams( ShuttleParams & S ){
141    S.SHUTTLE_PARAM( m_PercentChange, Percentage );
142    return true;
143 }
144 
GetAutomationParameters(CommandParameters & parms)145 bool EffectChangeSpeed::GetAutomationParameters(CommandParameters & parms)
146 {
147    parms.Write(KEY_Percentage, m_PercentChange);
148 
149    return true;
150 }
151 
SetAutomationParameters(CommandParameters & parms)152 bool EffectChangeSpeed::SetAutomationParameters(CommandParameters & parms)
153 {
154    ReadAndVerifyDouble(Percentage);
155 
156    m_PercentChange = Percentage;
157 
158    return true;
159 }
160 
LoadFactoryDefaults()161 bool EffectChangeSpeed::LoadFactoryDefaults()
162 {
163    mFromVinyl = kVinyl_33AndAThird;
164    mFormat = NumericConverter::DefaultSelectionFormat();
165 
166    return Effect::LoadFactoryDefaults();
167 }
168 
169 // Effect implementation
170 
CheckWhetherSkipEffect()171 bool EffectChangeSpeed::CheckWhetherSkipEffect()
172 {
173    return (m_PercentChange == 0.0);
174 }
175 
CalcPreviewInputLength(double previewLength)176 double EffectChangeSpeed::CalcPreviewInputLength(double previewLength)
177 {
178    return previewLength * (100.0 + m_PercentChange) / 100.0;
179 }
180 
Startup()181 bool EffectChangeSpeed::Startup()
182 {
183    wxString base = wxT("/Effects/ChangeSpeed/");
184 
185    // Migrate settings from 2.1.0 or before
186 
187    // Already migrated, so bail
188    if (gPrefs->Exists(base + wxT("Migrated")))
189    {
190       return true;
191    }
192 
193    // Load the old "current" settings
194    if (gPrefs->Exists(base))
195    {
196       // Retrieve last used control values
197       gPrefs->Read(base + wxT("PercentChange"), &m_PercentChange, 0);
198 
199       wxString format;
200       gPrefs->Read(base + wxT("TimeFormat"), &format, wxString{});
201       mFormat = NumericConverter::LookupFormat( NumericConverter::TIME, format );
202 
203       gPrefs->Read(base + wxT("VinylChoice"), &mFromVinyl, 0);
204       if (mFromVinyl == kVinyl_NA)
205       {
206          mFromVinyl = kVinyl_33AndAThird;
207       }
208 
209       SetPrivateConfig(GetCurrentSettingsGroup(), wxT("TimeFormat"), mFormat.Internal());
210       SetPrivateConfig(GetCurrentSettingsGroup(), wxT("VinylChoice"), mFromVinyl);
211 
212       SaveUserPreset(GetCurrentSettingsGroup());
213 
214       // Do not migrate again
215       gPrefs->Write(base + wxT("Migrated"), true);
216       gPrefs->Flush();
217    }
218 
219    return true;
220 }
221 
Init()222 bool EffectChangeSpeed::Init()
223 {
224    // The selection might have changed since the last time EffectChangeSpeed
225    // was invoked, so recalculate the Length parameters.
226    mFromLength = mT1 - mT0;
227    return true;
228 }
229 
Process()230 bool EffectChangeSpeed::Process()
231 {
232    // Similar to EffectSoundTouch::Process()
233 
234    // Iterate over each track.
235    // All needed because this effect needs to introduce
236    // silence in the sync-lock group tracks to keep sync
237    CopyInputTracks(true); // Set up mOutputTracks.
238    bool bGoodResult = true;
239 
240    mCurTrackNum = 0;
241    mMaxNewLength = 0.0;
242 
243    mFactor = 100.0 / (100.0 + m_PercentChange);
244 
245    mOutputTracks->Any().VisitWhile( bGoodResult,
246       [&](LabelTrack *lt) {
247          if (lt->GetSelected() || lt->IsSyncLockSelected())
248          {
249             if (!ProcessLabelTrack(lt))
250                bGoodResult = false;
251          }
252       },
253       [&](WaveTrack *pOutWaveTrack, const Track::Fallthrough &fallthrough) {
254          if (!pOutWaveTrack->GetSelected())
255             return fallthrough();
256 
257          //Get start and end times from track
258          mCurT0 = pOutWaveTrack->GetStartTime();
259          mCurT1 = pOutWaveTrack->GetEndTime();
260 
261          //Set the current bounds to whichever left marker is
262          //greater and whichever right marker is less:
263          mCurT0 = wxMax(mT0, mCurT0);
264          mCurT1 = wxMin(mT1, mCurT1);
265 
266          // Process only if the right marker is to the right of the left marker
267          if (mCurT1 > mCurT0) {
268             //Transform the marker timepoints to samples
269             auto start = pOutWaveTrack->TimeToLongSamples(mCurT0);
270             auto end = pOutWaveTrack->TimeToLongSamples(mCurT1);
271 
272             //ProcessOne() (implemented below) processes a single track
273             if (!ProcessOne(pOutWaveTrack, start, end))
274                bGoodResult = false;
275          }
276          mCurTrackNum++;
277       },
278       [&](Track *t) {
279          if (t->IsSyncLockSelected())
280             t->SyncLockAdjust(mT1, mT0 + (mT1 - mT0) * mFactor);
281       }
282    );
283 
284    if (bGoodResult)
285       ReplaceProcessedTracks(bGoodResult);
286 
287    // Update selection.
288    mT1 = mT0 + (((mT1 - mT0) * 100.0) / (100.0 + m_PercentChange));
289 
290    return bGoodResult;
291 }
292 
PopulateOrExchange(ShuttleGui & S)293 void EffectChangeSpeed::PopulateOrExchange(ShuttleGui & S)
294 {
295    {
296       wxString formatId;
297       GetPrivateConfig(GetCurrentSettingsGroup(), wxT("TimeFormat"),
298                        formatId, mFormat.Internal());
299       mFormat = NumericConverter::LookupFormat(
300          NumericConverter::TIME, formatId );
301    }
302    GetPrivateConfig(GetCurrentSettingsGroup(), wxT("VinylChoice"), mFromVinyl, mFromVinyl);
303 
304    S.SetBorder(5);
305 
306    S.StartVerticalLay(0);
307    {
308       S.AddSpace(0, 5);
309       S.AddTitle(XO("Change Speed, affecting both Tempo and Pitch"));
310       S.AddSpace(0, 10);
311 
312       // Speed multiplier and percent change controls.
313       S.StartMultiColumn(4, wxCENTER);
314       {
315          mpTextCtrl_Multiplier = S.Id(ID_Multiplier)
316             .Validator<FloatingPointValidator<double>>(
317                3, &mMultiplier,
318                NumValidatorStyle::THREE_TRAILING_ZEROES,
319                MIN_Percentage / 100.0, ((MAX_Percentage / 100.0) + 1)
320             )
321             .AddTextBox(XXO("&Speed Multiplier:"), wxT(""), 12);
322 
323          mpTextCtrl_PercentChange = S.Id(ID_PercentChange)
324             .Validator<FloatingPointValidator<double>>(
325                3, &m_PercentChange,
326                NumValidatorStyle::THREE_TRAILING_ZEROES,
327                MIN_Percentage, MAX_Percentage
328             )
329             .AddTextBox(XXO("Percent C&hange:"), wxT(""), 12);
330       }
331       S.EndMultiColumn();
332 
333       // Percent change slider.
334       S.StartHorizontalLay(wxEXPAND);
335       {
336          mpSlider_PercentChange = S.Id(ID_PercentChange)
337             .Name(XO("Percent Change"))
338             .Style(wxSL_HORIZONTAL)
339             .AddSlider( {}, 0, (int)kSliderMax, (int)MIN_Percentage);
340       }
341       S.EndHorizontalLay();
342 
343       // Vinyl rpm controls.
344       S.StartMultiColumn(5, wxCENTER);
345       {
346          /* i18n-hint: "rpm" is an English abbreviation meaning "revolutions per minute".
347             "vinyl" refers to old-fashioned phonograph records */
348          S.AddUnits(XO("Standard Vinyl rpm:"));
349 
350          mpChoice_FromVinyl = S.Id(ID_FromVinyl)
351             /* i18n-hint: changing speed of audio "from" one value "to" another
352              "rpm" means "revolutions per minute" as on a vinyl record turntable
353              */
354             .Name(XO("From rpm"))
355             .MinSize( { 100, -1 } )
356             /* i18n-hint: changing speed of audio "from" one value "to" another */
357             .AddChoice(XXC("&from", "change speed"), kVinylStrings);
358 
359          mpChoice_ToVinyl = S.Id(ID_ToVinyl)
360             /* i18n-hint: changing speed of audio "from" one value "to" another
361              "rpm" means "revolutions per minute" as on a vinyl record turntable
362              */
363             .Name(XO("To rpm"))
364             .MinSize( { 100, -1 } )
365             /* i18n-hint: changing speed of audio "from" one value "to" another */
366             .AddChoice(XXC("&to", "change speed"), kVinylStrings);
367       }
368       S.EndMultiColumn();
369 
370       // From/To time controls.
371       S.StartStatic(XO("Selection Length"), 0);
372       {
373          S.StartMultiColumn(2, wxALIGN_LEFT);
374          {
375             S.AddPrompt(XXO("C&urrent Length:"));
376 
377             mpFromLengthCtrl = safenew
378                   NumericTextCtrl(S.GetParent(), wxID_ANY,
379                                  NumericConverter::TIME,
380                                  mFormat,
381                                  mFromLength,
382                                  mProjectRate,
383                                  NumericTextCtrl::Options{}
384                                   .ReadOnly(true)
385                                   .MenuEnabled(false));
386 
387             S.ToolTip(XO("Current length of selection."))
388                /* i18n-hint: changing speed of audio "from" one value "to" another */
389                .Name(XC("from", "change speed"))
390                .Position(wxALIGN_LEFT)
391                .AddWindow(mpFromLengthCtrl);
392 
393             S.AddPrompt(XXO("&New Length:"));
394 
395             mpToLengthCtrl = safenew
396                   NumericTextCtrl(S.GetParent(), ID_ToLength,
397                                  NumericConverter::TIME,
398                                  mFormat,
399                                  mToLength,
400                                  mProjectRate);
401 
402             /* i18n-hint: changing speed of audio "from" one value "to" another */
403             S.Name(XC("to", "change speed"))
404                .Position(wxALIGN_LEFT)
405                .AddWindow(mpToLengthCtrl);
406          }
407          S.EndMultiColumn();
408       }
409       S.EndStatic();
410    }
411    S.EndVerticalLay();
412 }
413 
TransferDataToWindow()414 bool EffectChangeSpeed::TransferDataToWindow()
415 {
416    mbLoopDetect = true;
417 
418    if (!mUIParent->TransferDataToWindow())
419    {
420       return false;
421    }
422 
423    if (mFromVinyl == kVinyl_NA)
424    {
425       mFromVinyl = kVinyl_33AndAThird;
426    }
427 
428    Update_Text_PercentChange();
429    Update_Text_Multiplier();
430    Update_Slider_PercentChange();
431    Update_TimeCtrl_ToLength();
432 
433    // Set from/to Vinyl controls - mFromVinyl must be set first.
434    mpChoice_FromVinyl->SetSelection(mFromVinyl);
435    // Then update to get correct mToVinyl.
436    Update_Vinyl();
437    // Then update ToVinyl control.
438    mpChoice_ToVinyl->SetSelection(mToVinyl);
439 
440    // Set From Length control.
441    // Set the format first so we can get sample accuracy.
442    mpFromLengthCtrl->SetFormatName(mFormat);
443    mpFromLengthCtrl->SetValue(mFromLength);
444 
445    mbLoopDetect = false;
446 
447    return true;
448 }
449 
TransferDataFromWindow()450 bool EffectChangeSpeed::TransferDataFromWindow()
451 {
452    // mUIParent->TransferDataFromWindow() loses some precision, so save and restore it.
453    double exactPercent = m_PercentChange;
454    if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
455    {
456       return false;
457    }
458    m_PercentChange = exactPercent;
459 
460    SetPrivateConfig(GetCurrentSettingsGroup(), wxT("TimeFormat"), mFormat.Internal());
461    SetPrivateConfig(GetCurrentSettingsGroup(), wxT("VinylChoice"), mFromVinyl);
462 
463    return true;
464 }
465 
466 // EffectChangeSpeed implementation
467 
468 // Labels are time-scaled linearly inside the affected region, and labels after
469 // the region are shifted along according to how the region size changed.
ProcessLabelTrack(LabelTrack * lt)470 bool EffectChangeSpeed::ProcessLabelTrack(LabelTrack *lt)
471 {
472    RegionTimeWarper warper { mT0, mT1,
473       std::make_unique<LinearTimeWarper>(mT0, mT0,
474                                          mT1, mT0 + (mT1-mT0)*mFactor) };
475    lt->WarpLabels(warper);
476    return true;
477 }
478 
479 // ProcessOne() takes a track, transforms it to bunch of buffer-blocks,
480 // and calls libsamplerate code on these blocks.
ProcessOne(WaveTrack * track,sampleCount start,sampleCount end)481 bool EffectChangeSpeed::ProcessOne(WaveTrack * track,
482                            sampleCount start, sampleCount end)
483 {
484    if (track == NULL)
485       return false;
486 
487    // initialization, per examples of Mixer::Mixer and
488    // EffectSoundTouch::ProcessOne
489 
490 
491    auto outputTrack = track->EmptyCopy();
492 
493    //Get the length of the selection (as double). len is
494    //used simple to calculate a progress meter, so it is easier
495    //to make it a double now than it is to do it later
496    auto len = (end - start).as_double();
497 
498    // Initiate processing buffers, most likely shorter than
499    // the length of the selection being processed.
500    auto inBufferSize = track->GetMaxBlockSize();
501 
502    Floats inBuffer{ inBufferSize };
503 
504    // mFactor is at most 100-fold so this shouldn't overflow size_t
505    auto outBufferSize = size_t( mFactor * inBufferSize + 10 );
506    Floats outBuffer{ outBufferSize };
507 
508    // Set up the resampling stuff for this track.
509    Resample resample(true, mFactor, mFactor); // constant rate resampling
510 
511    //Go through the track one buffer at a time. samplePos counts which
512    //sample the current buffer starts at.
513    bool bResult = true;
514    auto samplePos = start;
515    while (samplePos < end) {
516       //Get a blockSize of samples (smaller than the size of the buffer)
517       auto blockSize = limitSampleBufferSize(
518          track->GetBestBlockSize(samplePos),
519          end - samplePos
520       );
521 
522       //Get the samples from the track and put them in the buffer
523       track->GetFloats(inBuffer.get(), samplePos, blockSize);
524 
525       const auto results = resample.Process(mFactor,
526                                     inBuffer.get(),
527                                     blockSize,
528                                     ((samplePos + blockSize) >= end),
529                                     outBuffer.get(),
530                                     outBufferSize);
531       const auto outgen = results.second;
532 
533       if (outgen > 0)
534          outputTrack->Append((samplePtr)outBuffer.get(), floatSample,
535                              outgen);
536 
537       // Increment samplePos
538       samplePos += results.first;
539 
540       // Update the Progress meter
541       if (TrackProgress(mCurTrackNum, (samplePos - start).as_double() / len)) {
542          bResult = false;
543          break;
544       }
545    }
546 
547    // Flush the output WaveTrack (since it's buffered, too)
548    outputTrack->Flush();
549 
550    // Take the output track and insert it in place of the original
551    // sample data
552    double newLength = outputTrack->GetEndTime();
553    if (bResult)
554    {
555       // Silenced samples will be inserted in gaps between clips, so capture where these
556       // gaps are for later deletion
557       std::vector<std::pair<double, double>> gaps;
558       double last = mCurT0;
559       auto clips = track->SortedClipArray();
560       auto front = clips.front();
561       auto back = clips.back();
562       for (auto &clip : clips) {
563          auto st = clip->GetPlayStartTime();
564          auto et = clip->GetPlayEndTime();
565 
566          if (st >= mCurT0 || et < mCurT1) {
567             if (mCurT0 < st && clip == front) {
568                gaps.push_back(std::make_pair(mCurT0, st));
569             }
570             else if (last < st && mCurT0 <= last ) {
571                gaps.push_back(std::make_pair(last, st));
572             }
573 
574             if (et < mCurT1 && clip == back) {
575                gaps.push_back(std::make_pair(et, mCurT1));
576             }
577          }
578          last = et;
579       }
580 
581       LinearTimeWarper warper { mCurT0, mCurT0, mCurT1, mCurT0 + newLength };
582 
583       // Take the output track and insert it in place of the original sample data
584       track->ClearAndPaste(mCurT0, mCurT1, outputTrack.get(), true, true, &warper);
585 
586       // Finally, recreate the gaps
587       for (auto gap : gaps) {
588          auto st = track->LongSamplesToTime(track->TimeToLongSamples(gap.first));
589          auto et = track->LongSamplesToTime(track->TimeToLongSamples(gap.second));
590          if (st >= mCurT0 && et <= mCurT1 && st != et)
591          {
592             track->SplitDelete(warper.Warp(st), warper.Warp(et));
593          }
594       }
595    }
596 
597    if (newLength > mMaxNewLength)
598       mMaxNewLength = newLength;
599 
600    return bResult;
601 }
602 
603 // handler implementations for EffectChangeSpeed
604 
OnText_PercentChange(wxCommandEvent & WXUNUSED (evt))605 void EffectChangeSpeed::OnText_PercentChange(wxCommandEvent & WXUNUSED(evt))
606 {
607    if (mbLoopDetect)
608       return;
609 
610    mpTextCtrl_PercentChange->GetValidator()->TransferFromWindow();
611    UpdateUI();
612 
613    mbLoopDetect = true;
614    Update_Text_Multiplier();
615    Update_Slider_PercentChange();
616    Update_Vinyl();
617    Update_TimeCtrl_ToLength();
618    mbLoopDetect = false;
619 }
620 
OnText_Multiplier(wxCommandEvent & WXUNUSED (evt))621 void EffectChangeSpeed::OnText_Multiplier(wxCommandEvent & WXUNUSED(evt))
622 {
623    if (mbLoopDetect)
624       return;
625 
626    mpTextCtrl_Multiplier->GetValidator()->TransferFromWindow();
627    m_PercentChange = 100 * (mMultiplier - 1);
628    UpdateUI();
629 
630    mbLoopDetect = true;
631    Update_Text_PercentChange();
632    Update_Slider_PercentChange();
633    Update_Vinyl();
634    Update_TimeCtrl_ToLength();
635    mbLoopDetect = false;
636 }
637 
OnSlider_PercentChange(wxCommandEvent & WXUNUSED (evt))638 void EffectChangeSpeed::OnSlider_PercentChange(wxCommandEvent & WXUNUSED(evt))
639 {
640    if (mbLoopDetect)
641       return;
642 
643    m_PercentChange = (double)(mpSlider_PercentChange->GetValue());
644    // Warp positive values to actually go up faster & further than negatives.
645    if (m_PercentChange > 0.0)
646       m_PercentChange = pow(m_PercentChange, kSliderWarp);
647    UpdateUI();
648 
649    mbLoopDetect = true;
650    Update_Text_PercentChange();
651    Update_Text_Multiplier();
652    Update_Vinyl();
653    Update_TimeCtrl_ToLength();
654    mbLoopDetect = false;
655 }
656 
OnChoice_Vinyl(wxCommandEvent & WXUNUSED (evt))657 void EffectChangeSpeed::OnChoice_Vinyl(wxCommandEvent & WXUNUSED(evt))
658 {
659    // Treat mpChoice_FromVinyl and mpChoice_ToVinyl as one control since we need
660    // both to calculate Percent Change.
661    mFromVinyl = mpChoice_FromVinyl->GetSelection();
662    mToVinyl = mpChoice_ToVinyl->GetSelection();
663    // Use this as the 'preferred' choice.
664    if (mFromVinyl != kVinyl_NA) {
665       SetPrivateConfig(GetCurrentSettingsGroup(), wxT("VinylChoice"), mFromVinyl);
666    }
667 
668    // If mFromVinyl & mToVinyl are set, then there's a NEW percent change.
669    if ((mFromVinyl != kVinyl_NA) && (mToVinyl != kVinyl_NA))
670    {
671       double fromRPM;
672       double toRPM;
673       switch (mFromVinyl) {
674       default:
675       case kVinyl_33AndAThird:   fromRPM = 33.0 + (1.0 / 3.0); break;
676       case kVinyl_45:            fromRPM = 45.0; break;
677       case kVinyl_78:            fromRPM = 78; break;
678       }
679       switch (mToVinyl) {
680       default:
681       case kVinyl_33AndAThird:   toRPM = 33.0 + (1.0 / 3.0); break;
682       case kVinyl_45:            toRPM = 45.0; break;
683       case kVinyl_78:            toRPM = 78; break;
684       }
685       m_PercentChange = ((toRPM * 100.0) / fromRPM) - 100.0;
686       UpdateUI();
687 
688       mbLoopDetect = true;
689       Update_Text_PercentChange();
690       Update_Text_Multiplier();
691       Update_Slider_PercentChange();
692       Update_TimeCtrl_ToLength();
693    }
694    mbLoopDetect = false;
695 }
696 
OnTimeCtrl_ToLength(wxCommandEvent & WXUNUSED (evt))697 void EffectChangeSpeed::OnTimeCtrl_ToLength(wxCommandEvent & WXUNUSED(evt))
698 {
699    if (mbLoopDetect)
700       return;
701 
702    mToLength = mpToLengthCtrl->GetValue();
703    // Division by (double) 0.0 is not an error and we want to show "infinite" in
704    // text controls, so take care that we handle infinite values when they occur.
705    m_PercentChange = ((mFromLength * 100.0) / mToLength) - 100.0;
706    UpdateUI();
707 
708    mbLoopDetect = true;
709 
710    Update_Text_PercentChange();
711    Update_Text_Multiplier();
712    Update_Slider_PercentChange();
713    Update_Vinyl();
714 
715    mbLoopDetect = false;
716 }
717 
OnTimeCtrlUpdate(wxCommandEvent & evt)718 void EffectChangeSpeed::OnTimeCtrlUpdate(wxCommandEvent & evt)
719 {
720    mFormat = NumericConverter::LookupFormat(
721       NumericConverter::TIME, evt.GetString() );
722 
723    mpFromLengthCtrl->SetFormatName(mFormat);
724    // Update From/To Length controls (precision has changed).
725    mpToLengthCtrl->SetValue(mToLength);
726    mpFromLengthCtrl->SetValue(mFromLength);
727 }
728 
729 // helper functions
730 
Update_Text_PercentChange()731 void EffectChangeSpeed::Update_Text_PercentChange()
732 // Update Text Percent control from percent change.
733 {
734    mpTextCtrl_PercentChange->GetValidator()->TransferToWindow();
735 }
736 
Update_Text_Multiplier()737 void EffectChangeSpeed::Update_Text_Multiplier()
738 // Update Multiplier control from percent change.
739 {
740    mMultiplier =  1 + (m_PercentChange) / 100.0;
741    mpTextCtrl_Multiplier->GetValidator()->TransferToWindow();
742 }
743 
Update_Slider_PercentChange()744 void EffectChangeSpeed::Update_Slider_PercentChange()
745 // Update Slider Percent control from percent change.
746 {
747    auto unwarped = std::min<double>(m_PercentChange, MAX_Percentage);
748    if (unwarped > 0.0)
749       // Un-warp values above zero to actually go up to kSliderMax.
750       unwarped = pow(m_PercentChange, (1.0 / kSliderWarp));
751 
752    // Caution: m_PercentChange could be infinite.
753    int unwarpedi = (int)(unwarped + 0.5);
754    unwarpedi = std::min<int>(unwarpedi, (int)kSliderMax);
755 
756    mpSlider_PercentChange->SetValue(unwarpedi);
757 }
758 
Update_Vinyl()759 void EffectChangeSpeed::Update_Vinyl()
760 // Update Vinyl controls from percent change.
761 {
762    // Match Vinyl rpm when within 0.01% of a standard ratio.
763    // Ratios calculated as: ((toRPM / fromRPM) - 1) * 100 * 100
764 
765    // Caution: m_PercentChange could be infinite
766    int ratio = (int)((m_PercentChange * 100) + 0.5);
767 
768    switch (ratio)
769    {
770       case 0: // toRPM is the same as fromRPM
771          if (mFromVinyl != kVinyl_NA) {
772             mpChoice_ToVinyl->SetSelection(mpChoice_FromVinyl->GetSelection());
773          } else {
774             // Use the last saved option.
775             GetPrivateConfig(GetCurrentSettingsGroup(), wxT("VinylChoice"), mFromVinyl, 0);
776             mpChoice_FromVinyl->SetSelection(mFromVinyl);
777             mpChoice_ToVinyl->SetSelection(mFromVinyl);
778          }
779          break;
780       case 3500:
781          mpChoice_FromVinyl->SetSelection(kVinyl_33AndAThird);
782          mpChoice_ToVinyl->SetSelection(kVinyl_45);
783          break;
784       case 13400:
785          mpChoice_FromVinyl->SetSelection(kVinyl_33AndAThird);
786          mpChoice_ToVinyl->SetSelection(kVinyl_78);
787          break;
788       case -2593:
789          mpChoice_FromVinyl->SetSelection(kVinyl_45);
790          mpChoice_ToVinyl->SetSelection(kVinyl_33AndAThird);
791          break;
792       case 7333:
793          mpChoice_FromVinyl->SetSelection(kVinyl_45);
794          mpChoice_ToVinyl->SetSelection(kVinyl_78);
795          break;
796       case -5727:
797          mpChoice_FromVinyl->SetSelection(kVinyl_78);
798          mpChoice_ToVinyl->SetSelection(kVinyl_33AndAThird);
799          break;
800       case -4231:
801          mpChoice_FromVinyl->SetSelection(kVinyl_78);
802          mpChoice_ToVinyl->SetSelection(kVinyl_45);
803          break;
804       default:
805          mpChoice_ToVinyl->SetSelection(kVinyl_NA);
806    }
807    // and update variables.
808    mFromVinyl = mpChoice_FromVinyl->GetSelection();
809    mToVinyl = mpChoice_ToVinyl->GetSelection();
810 }
811 
Update_TimeCtrl_ToLength()812 void EffectChangeSpeed::Update_TimeCtrl_ToLength()
813 // Update ToLength control from percent change.
814 {
815    mToLength = (mFromLength * 100.0) / (100.0 + m_PercentChange);
816 
817    // Set the format first so we can get sample accuracy.
818    mpToLengthCtrl->SetFormatName(mFormat);
819    // Negative times do not make sense.
820    // 359999 = 99h:59m:59s which is a little less disturbing than overflow characters
821    // though it may still look a bit strange with some formats.
822    mToLength = TrapDouble(mToLength, 0.0, 359999.0);
823    mpToLengthCtrl->SetValue(mToLength);
824 }
825 
UpdateUI()826 void EffectChangeSpeed::UpdateUI()
827 // Disable OK and Preview if not in sensible range.
828 {
829    EnableApply(m_PercentChange >= MIN_Percentage && m_PercentChange <= MAX_Percentage);
830 }
831