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