1 /**********************************************************************
2 
3    Audacity: A Digital Audio Editor
4    Audacity(R) is copyright (c) 1999-2013 Audacity Team.
5    License: GPL v2.  See License.txt.
6 
7   ChangePitch.cpp
8   Vaughan Johnson, Dominic Mazzoni, Steve Daulton
9 
10 ******************************************************************//**
11 
12 \file ChangePitch.cpp
13 \brief Change Pitch effect provides raising or lowering
14 the pitch without changing the tempo.
15 
16 *//*******************************************************************/
17 
18 
19 
20 #if USE_SOUNDTOUCH
21 #include "ChangePitch.h"
22 #include "LoadEffects.h"
23 
24 #if USE_SBSMS
25 #include <wx/valgen.h>
26 #endif
27 
28 #include <float.h>
29 #include <math.h>
30 
31 #include <wx/checkbox.h>
32 #include <wx/choice.h>
33 #include <wx/intl.h>
34 #include <wx/slider.h>
35 #include <wx/spinctrl.h>
36 #include <wx/valtext.h>
37 
38 #include "../PitchName.h"
39 #include "../Shuttle.h"
40 #include "../ShuttleGui.h"
41 #include "Spectrum.h"
42 #include "../WaveTrack.h"
43 #include "../widgets/valnum.h"
44 #include "TimeWarper.h"
45 
46 // Soundtouch defines these as well, which are also in generated configmac.h
47 // and configunix.h, so get rid of them before including,
48 // to avoid compiler warnings, and be sure to do this
49 // after all other #includes, to avoid any mischief that might result
50 // from doing the un-definitions before seeing any wx headers.
51 #undef PACKAGE_NAME
52 #undef PACKAGE_STRING
53 #undef PACKAGE_TARNAME
54 #undef PACKAGE_VERSION
55 #undef PACKAGE_BUGREPORT
56 #undef PACKAGE
57 #undef VERSION
58 #include "SoundTouch.h"
59 
60 enum {
61    ID_PercentChange = 10000,
62    ID_FromPitch,
63    ID_FromOctave,
64    ID_ToPitch,
65    ID_ToOctave,
66    ID_SemitonesChange,
67    ID_FromFrequency,
68    ID_ToFrequency
69 };
70 
71 // Soundtouch is not reasonable below -99% or above 3000%.
72 
73 // Define keys, defaults, minimums, and maximums for the effect parameters
74 //
75 //     Name          Type     Key               Def   Min      Max      Scale
76 Param( Percentage,   double,  wxT("Percentage"), 0.0,  -99.0,   3000.0,  1  );
77 Param( UseSBSMS,     bool,    wxT("SBSMS"),     false, false,   true,    1  );
78 
79 // We warp the slider to go up to 400%, but user can enter up to 3000%
80 static const double kSliderMax = 100.0;          // warped above zero to actually go up to 400%
81 static const double kSliderWarp = 1.30105;       // warp power takes max from 100 to 400.
82 
83 // EffectChangePitch
84 
85 const ComponentInterfaceSymbol EffectChangePitch::Symbol
86 { XO("Change Pitch") };
87 
88 namespace{ BuiltinEffectsModule::Registration< EffectChangePitch > reg; }
89 
BEGIN_EVENT_TABLE(EffectChangePitch,wxEvtHandler)90 BEGIN_EVENT_TABLE(EffectChangePitch, wxEvtHandler)
91    EVT_CHOICE(ID_FromPitch, EffectChangePitch::OnChoice_FromPitch)
92    EVT_TEXT(ID_FromOctave, EffectChangePitch::OnSpin_FromOctave)
93    EVT_CHOICE(ID_ToPitch, EffectChangePitch::OnChoice_ToPitch)
94    EVT_TEXT(ID_ToOctave, EffectChangePitch::OnSpin_ToOctave)
95 
96    EVT_TEXT(ID_SemitonesChange, EffectChangePitch::OnText_SemitonesChange)
97 
98    EVT_TEXT(ID_FromFrequency, EffectChangePitch::OnText_FromFrequency)
99    EVT_TEXT(ID_ToFrequency, EffectChangePitch::OnText_ToFrequency)
100 
101    EVT_TEXT(ID_PercentChange, EffectChangePitch::OnText_PercentChange)
102    EVT_SLIDER(ID_PercentChange, EffectChangePitch::OnSlider_PercentChange)
103 END_EVENT_TABLE()
104 
105 EffectChangePitch::EffectChangePitch()
106 {
107    m_dPercentChange = DEF_Percentage;
108    m_dSemitonesChange = 0.0;
109    m_dStartFrequency = 0.0; // 0.0 => uninitialized
110    m_bLoopDetect = false;
111 
112 #if USE_SBSMS
113    mUseSBSMS = DEF_UseSBSMS;
114 #else
115    mUseSBSMS = false;
116 #endif
117 
118    // NULL out these control members because there are some cases where the
119    // event table handlers get called during this method, and those handlers that
120    // can cause trouble check for NULL.
121    m_pChoice_FromPitch = NULL;
122    m_pSpin_FromOctave = NULL;
123    m_pChoice_ToPitch = NULL;
124    m_pSpin_ToOctave = NULL;
125 
126    m_pTextCtrl_SemitonesChange = NULL;
127 
128    m_pTextCtrl_FromFrequency = NULL;
129    m_pTextCtrl_ToFrequency = NULL;
130 
131    m_pTextCtrl_PercentChange = NULL;
132    m_pSlider_PercentChange = NULL;
133 
134    SetLinearEffectFlag(true);
135 }
136 
~EffectChangePitch()137 EffectChangePitch::~EffectChangePitch()
138 {
139 }
140 
141 // ComponentInterface implementation
142 
GetSymbol()143 ComponentInterfaceSymbol EffectChangePitch::GetSymbol()
144 {
145    return Symbol;
146 }
147 
GetDescription()148 TranslatableString EffectChangePitch::GetDescription()
149 {
150    return XO("Changes the pitch of a track without changing its tempo");
151 }
152 
ManualPage()153 ManualPageID EffectChangePitch::ManualPage()
154 {
155    return L"Change_Pitch";
156 }
157 
158 // EffectDefinitionInterface implementation
159 
GetType()160 EffectType EffectChangePitch::GetType()
161 {
162    return EffectTypeProcess;
163 }
164 
165 // EffectClientInterface implementation
DefineParams(ShuttleParams & S)166 bool EffectChangePitch::DefineParams( ShuttleParams & S ){
167    S.SHUTTLE_PARAM( m_dPercentChange, Percentage );
168    S.SHUTTLE_PARAM( mUseSBSMS, UseSBSMS );
169    return true;
170 }
171 
GetAutomationParameters(CommandParameters & parms)172 bool EffectChangePitch::GetAutomationParameters(CommandParameters & parms)
173 {
174    parms.Write(KEY_Percentage, m_dPercentChange);
175    parms.Write(KEY_UseSBSMS, mUseSBSMS);
176 
177    return true;
178 }
179 
SetAutomationParameters(CommandParameters & parms)180 bool EffectChangePitch::SetAutomationParameters(CommandParameters & parms)
181 {
182    // Vaughan, 2013-06: Long lost to history, I don't see why m_dPercentChange was chosen to be shuttled.
183    // Only m_dSemitonesChange is used in Process().
184    ReadAndVerifyDouble(Percentage);
185 
186    m_dPercentChange = Percentage;
187    Calc_SemitonesChange_fromPercentChange();
188 
189 #if USE_SBSMS
190    ReadAndVerifyBool(UseSBSMS);
191    mUseSBSMS = UseSBSMS;
192 #else
193    mUseSBSMS = false;
194 #endif
195 
196    return true;
197 }
198 
LoadFactoryDefaults()199 bool EffectChangePitch::LoadFactoryDefaults()
200 {
201    DeduceFrequencies();
202 
203    return Effect::LoadFactoryDefaults();
204 }
205 
206 // Effect implementation
207 
Init()208 bool EffectChangePitch::Init()
209 {
210    return true;
211 }
212 
Process()213 bool EffectChangePitch::Process()
214 {
215 #if USE_SBSMS
216    if (mUseSBSMS)
217    {
218       double pitchRatio = 1.0 + m_dPercentChange / 100.0;
219       EffectSBSMS proxy;
220       proxy.mProxyEffectName = XO("High Quality Pitch Change");
221       proxy.setParameters(1.0, pitchRatio);
222 
223       return Delegate(proxy, *mUIParent, nullptr);
224    }
225    else
226 #endif
227    {
228       // Macros save m_dPercentChange and not m_dSemitonesChange, so we must
229       // ensure that m_dSemitonesChange is set.
230       Calc_SemitonesChange_fromPercentChange();
231 
232       auto initer = [&](soundtouch::SoundTouch *soundtouch)
233       {
234          soundtouch->setPitchSemiTones((float)(m_dSemitonesChange));
235       };
236       IdentityTimeWarper warper;
237 #ifdef USE_MIDI
238       // Pitch shifting note tracks is currently only supported by SoundTouchEffect
239       // and non-real-time-preview effects require an audio track selection.
240       //
241       // Note: m_dSemitonesChange is private to ChangePitch because it only
242       // needs to pass it along to mSoundTouch (above). I added mSemitones
243       // to SoundTouchEffect (the super class) to convey this value
244       // to process Note tracks. This approach minimizes changes to existing
245       // code, but it would be cleaner to change all m_dSemitonesChange to
246       // mSemitones, make mSemitones exist with or without USE_MIDI, and
247       // eliminate the next line:
248       mSemitones = m_dSemitonesChange;
249 #endif
250       return EffectSoundTouch::ProcessWithTimeWarper(initer, warper, true);
251    }
252 }
253 
CheckWhetherSkipEffect()254 bool EffectChangePitch::CheckWhetherSkipEffect()
255 {
256    return (m_dPercentChange == 0.0);
257 }
258 
PopulateOrExchange(ShuttleGui & S)259 void EffectChangePitch::PopulateOrExchange(ShuttleGui & S)
260 {
261    DeduceFrequencies(); // Set frequency-related control values based on sample.
262 
263    TranslatableStrings pitch;
264    for (int ii = 0; ii < 12; ++ii)
265       pitch.push_back( PitchName( ii, PitchNameChoice::Both ) );
266 
267    S.SetBorder(5);
268 
269    S.StartVerticalLay(0);
270    {
271       S.StartVerticalLay();
272       {
273          S.AddTitle(XO("Change Pitch without Changing Tempo"));
274          S.AddTitle(
275             XO("Estimated Start Pitch: %s%d (%.3f Hz)")
276                .Format( pitch[m_nFromPitch], m_nFromOctave, m_FromFrequency) );
277       }
278       S.EndVerticalLay();
279 
280       /* i18n-hint: (noun) Musical pitch.*/
281       S.StartStatic(XO("Pitch"));
282       {
283          S.StartMultiColumn(6, wxALIGN_CENTER); // 6 controls, because each AddChoice adds a wxStaticText and a wxChoice.
284          {
285             m_pChoice_FromPitch = S.Id(ID_FromPitch)
286                /* i18n-hint: changing musical pitch "from" one value "to" another */
287                .Name(XC("from", "change pitch"))
288                .MinSize( { 80, -1 } )
289             /* i18n-hint: changing musical pitch "from" one value "to" another */
290                .AddChoice(XXC("&from", "change pitch"), pitch);
291 
292             m_pSpin_FromOctave = S.Id(ID_FromOctave)
293                .Name(XO("from Octave"))
294                .MinSize( { 50, -1 } )
295                .AddSpinCtrl( {}, m_nFromOctave, INT_MAX, INT_MIN);
296 
297             m_pChoice_ToPitch = S.Id(ID_ToPitch)
298                /* i18n-hint: changing musical pitch "from" one value "to" another */
299                .Name(XC("to", "change pitch"))
300                .MinSize( { 80, -1 } )
301                /* i18n-hint: changing musical pitch "from" one value "to" another */
302                .AddChoice(XXC("&to", "change pitch"), pitch);
303 
304             m_pSpin_ToOctave = S.Id(ID_ToOctave)
305                .Name(XO("to Octave"))
306                .MinSize( { 50, -1 } )
307                .AddSpinCtrl( {}, m_nToOctave, INT_MAX, INT_MIN);
308          }
309          S.EndMultiColumn();
310 
311          S.StartHorizontalLay(wxALIGN_CENTER);
312          {
313             m_pTextCtrl_SemitonesChange = S.Id(ID_SemitonesChange)
314                .Name(XO("Semitones (half-steps)"))
315                .Validator<FloatingPointValidator<double>>(
316                   2, &m_dSemitonesChange,
317                   NumValidatorStyle::TWO_TRAILING_ZEROES
318                )
319                .AddTextBox(XXO("&Semitones (half-steps):"), wxT(""), 12);
320          }
321          S.EndHorizontalLay();
322       }
323       S.EndStatic();
324 
325       S.StartStatic(XO("Frequency"));
326       {
327          S.StartMultiColumn(5, wxALIGN_CENTER); // 5, because AddTextBox adds a wxStaticText and a wxTextCtrl.
328          {
329             m_pTextCtrl_FromFrequency = S.Id(ID_FromFrequency)
330                .Name(XO("from (Hz)"))
331                .Validator<FloatingPointValidator<double>>(
332                   3, &m_FromFrequency,
333                   NumValidatorStyle::THREE_TRAILING_ZEROES,
334                   0.0
335                )
336                .AddTextBox(XXO("f&rom"), wxT(""), 12);
337 
338             m_pTextCtrl_ToFrequency = S.Id(ID_ToFrequency)
339                .Name(XO("to (Hz)"))
340                .Validator<FloatingPointValidator<double>>(
341                   3, &m_ToFrequency,
342                   NumValidatorStyle::THREE_TRAILING_ZEROES,
343                   0.0
344                )
345                .AddTextBox(XXO("t&o"), wxT(""), 12);
346 
347             S.AddUnits(XO("Hz"));
348          }
349          S.EndMultiColumn();
350 
351          S.StartHorizontalLay(wxALIGN_CENTER);
352          {
353             m_pTextCtrl_PercentChange = S.Id(ID_PercentChange)
354                .Validator<FloatingPointValidator<double>>(
355                   3, &m_dPercentChange,
356                   NumValidatorStyle::THREE_TRAILING_ZEROES,
357                   MIN_Percentage, MAX_Percentage
358                )
359                .AddTextBox(XXO("Percent C&hange:"), wxT(""), 12);
360          }
361          S.EndHorizontalLay();
362 
363          S.StartHorizontalLay(wxEXPAND);
364          {
365             m_pSlider_PercentChange = S.Id(ID_PercentChange)
366                .Name(XO("Percent Change"))
367                .Style(wxSL_HORIZONTAL)
368                .AddSlider( {}, 0, (int)kSliderMax, (int)MIN_Percentage);
369          }
370          S.EndHorizontalLay();
371       }
372       S.EndStatic();
373 
374 #if USE_SBSMS
375       S.StartMultiColumn(2);
376       {
377          mUseSBSMSCheckBox = S.Validator<wxGenericValidator>(&mUseSBSMS)
378             .AddCheckBox(XXO("&Use high quality stretching (slow)"),
379                                              mUseSBSMS);
380       }
381       S.EndMultiColumn();
382 #endif
383 
384    }
385    S.EndVerticalLay();
386    return;
387 }
388 
TransferDataToWindow()389 bool EffectChangePitch::TransferDataToWindow()
390 {
391    m_bLoopDetect = true;
392 
393    if (!mUIParent->TransferDataToWindow())
394    {
395       return false;
396    }
397 
398    Calc_SemitonesChange_fromPercentChange();
399    Calc_ToPitch(); // Call *after* m_dSemitonesChange is updated.
400    Calc_ToFrequency();
401    Calc_ToOctave(); // Call after Calc_ToFrequency().
402 
403    Update_Choice_FromPitch();
404    Update_Choice_ToPitch();
405    Update_Spin_FromOctave();
406    Update_Spin_ToOctave();
407    Update_Text_SemitonesChange();
408    Update_Text_FromFrequency();
409    Update_Text_ToFrequency();
410    Update_Text_PercentChange();
411    Update_Slider_PercentChange();
412 
413    m_bLoopDetect = false;
414 
415    return true;
416 }
417 
TransferDataFromWindow()418 bool EffectChangePitch::TransferDataFromWindow()
419 {
420    if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
421    {
422       return false;
423    }
424 
425    // from/to pitch controls
426    m_nFromPitch = m_pChoice_FromPitch->GetSelection();
427    m_nFromOctave = m_pSpin_FromOctave->GetValue();
428 
429    m_nToPitch = m_pChoice_ToPitch->GetSelection();
430 
431    // No need to update Slider_PercentChange here because TextCtrl_PercentChange
432    // always tracks it & is more precise (decimal points).
433 
434    return true;
435 }
436 
437 // EffectChangePitch implementation
438 
439 // Deduce m_FromFrequency from the samples at the beginning of
440 // the selection. Then set some other params accordingly.
DeduceFrequencies()441 void EffectChangePitch::DeduceFrequencies()
442 {
443     auto FirstTrack = [&]()->const WaveTrack *{
444       if( IsBatchProcessing() || !inputTracks() )
445          return nullptr;
446       return *( inputTracks()->Selected< const WaveTrack >() ).first;
447    };
448 
449    m_dStartFrequency = 261.265;// Middle C.
450 
451    // As a neat trick, attempt to get the frequency of the note at the
452    // beginning of the selection.
453    auto track = FirstTrack();
454    if (track ) {
455       double rate = track->GetRate();
456 
457       // Auto-size window -- high sample rates require larger windowSize.
458       // Aim for around 2048 samples at 44.1 kHz (good down to about 100 Hz).
459       // To detect single notes, analysis period should be about 0.2 seconds.
460       // windowSize must be a power of 2.
461       const size_t windowSize =
462          // windowSize < 256 too inaccurate
463          std::max(256, wxRound(pow(2.0, floor((log(rate / 20.0)/log(2.0)) + 0.5))));
464 
465       // we want about 0.2 seconds to catch the first note.
466       // number of windows rounded to nearest integer >= 1.
467       const unsigned numWindows =
468          std::max(1, wxRound((double)(rate / (5.0f * windowSize))));
469 
470       double trackStart = track->GetStartTime();
471       double t0 = mT0 < trackStart? trackStart: mT0;
472       auto start = track->TimeToLongSamples(t0);
473 
474       auto analyzeSize = windowSize * numWindows;
475       Floats buffer{ analyzeSize };
476 
477       Floats freq{ windowSize / 2 };
478       Floats freqa{ windowSize / 2, true };
479 
480       track->GetFloats(buffer.get(), start, analyzeSize);
481       for(unsigned i = 0; i < numWindows; i++) {
482          ComputeSpectrum(buffer.get() + i * windowSize, windowSize,
483                          windowSize, rate, freq.get(), true);
484          for(size_t j = 0; j < windowSize / 2; j++)
485             freqa[j] += freq[j];
486       }
487       size_t argmax = 0;
488       for(size_t j = 1; j < windowSize / 2; j++)
489          if (freqa[j] > freqa[argmax])
490             argmax = j;
491 
492       auto lag = (windowSize / 2 - 1) - argmax;
493       m_dStartFrequency = rate / lag;
494    }
495 
496    double dFromMIDInote = FreqToMIDInote(m_dStartFrequency);
497    double dToMIDInote = dFromMIDInote + m_dSemitonesChange;
498    m_nFromPitch = PitchIndex(dFromMIDInote);
499    m_nFromOctave = PitchOctave(dFromMIDInote);
500    m_nToPitch = PitchIndex(dToMIDInote);
501    m_nToOctave = PitchOctave(dToMIDInote);
502 
503    m_FromFrequency = m_dStartFrequency;
504    // Calc_PercentChange();  // This will reset m_dPercentChange
505    Calc_ToFrequency();
506 }
507 
508 // calculations
509 
Calc_ToPitch()510 void EffectChangePitch::Calc_ToPitch()
511 {
512    int nSemitonesChange =
513       (int)(m_dSemitonesChange + ((m_dSemitonesChange < 0.0) ? -0.5 : 0.5));
514    m_nToPitch = (m_nFromPitch + nSemitonesChange) % 12;
515    if (m_nToPitch < 0)
516       m_nToPitch += 12;
517 }
518 
Calc_ToOctave()519 void EffectChangePitch::Calc_ToOctave()
520 {
521    m_nToOctave = PitchOctave(FreqToMIDInote(m_ToFrequency));
522 }
523 
Calc_SemitonesChange_fromPitches()524 void EffectChangePitch::Calc_SemitonesChange_fromPitches()
525 {
526    m_dSemitonesChange =
527       PitchToMIDInote(m_nToPitch, m_nToOctave) - PitchToMIDInote(m_nFromPitch, m_nFromOctave);
528 }
529 
Calc_SemitonesChange_fromPercentChange()530 void EffectChangePitch::Calc_SemitonesChange_fromPercentChange()
531 {
532    // Use m_dPercentChange rather than m_FromFrequency & m_ToFrequency, because
533    // they start out uninitialized, but m_dPercentChange is always valid.
534    m_dSemitonesChange = (12.0 * log((100.0 + m_dPercentChange) / 100.0)) / log(2.0);
535 }
536 
Calc_ToFrequency()537 void EffectChangePitch::Calc_ToFrequency()
538 {
539    m_ToFrequency = (m_FromFrequency * (100.0 + m_dPercentChange)) / 100.0;
540 }
541 
Calc_PercentChange()542 void EffectChangePitch::Calc_PercentChange()
543 {
544    m_dPercentChange = 100.0 * (pow(2.0, (m_dSemitonesChange / 12.0)) - 1.0);
545 }
546 
547 
548 // handlers
OnChoice_FromPitch(wxCommandEvent & WXUNUSED (evt))549 void EffectChangePitch::OnChoice_FromPitch(wxCommandEvent & WXUNUSED(evt))
550 {
551    if (m_bLoopDetect)
552       return;
553 
554    m_nFromPitch = m_pChoice_FromPitch->GetSelection();
555    m_FromFrequency = PitchToFreq(m_nFromPitch, m_nFromOctave);
556 
557    Calc_ToPitch();
558    Calc_ToFrequency();
559    Calc_ToOctave(); // Call after Calc_ToFrequency().
560 
561    m_bLoopDetect = true;
562    {
563       Update_Choice_ToPitch();
564       Update_Spin_ToOctave();
565       Update_Text_FromFrequency();
566       Update_Text_ToFrequency();
567    }
568    m_bLoopDetect = false;
569 }
570 
OnSpin_FromOctave(wxCommandEvent & WXUNUSED (evt))571 void EffectChangePitch::OnSpin_FromOctave(wxCommandEvent & WXUNUSED(evt))
572 {
573    if (m_bLoopDetect)
574       return;
575 
576    m_nFromOctave = m_pSpin_FromOctave->GetValue();
577    //vvv If I change this code to not keep semitones and percent constant,
578    // will need validation code as in OnSpin_ToOctave.
579    m_FromFrequency = PitchToFreq(m_nFromPitch, m_nFromOctave);
580 
581    Calc_ToFrequency();
582    Calc_ToOctave(); // Call after Calc_ToFrequency().
583 
584    m_bLoopDetect = true;
585    {
586       Update_Spin_ToOctave();
587       Update_Text_FromFrequency();
588       Update_Text_ToFrequency();
589    }
590    m_bLoopDetect = false;
591 }
592 
OnChoice_ToPitch(wxCommandEvent & WXUNUSED (evt))593 void EffectChangePitch::OnChoice_ToPitch(wxCommandEvent & WXUNUSED(evt))
594 {
595    if (m_bLoopDetect)
596       return;
597 
598    m_nToPitch = m_pChoice_ToPitch->GetSelection();
599 
600    Calc_SemitonesChange_fromPitches();
601    Calc_PercentChange(); // Call *after* m_dSemitonesChange is updated.
602    Calc_ToFrequency(); // Call *after* m_dPercentChange is updated.
603 
604    m_bLoopDetect = true;
605    {
606       Update_Text_SemitonesChange();
607       Update_Text_ToFrequency();
608       Update_Text_PercentChange();
609       Update_Slider_PercentChange();
610    }
611    m_bLoopDetect = false;
612 }
613 
OnSpin_ToOctave(wxCommandEvent & WXUNUSED (evt))614 void EffectChangePitch::OnSpin_ToOctave(wxCommandEvent & WXUNUSED(evt))
615 {
616    if (m_bLoopDetect)
617       return;
618 
619    int nNewValue = m_pSpin_ToOctave->GetValue();
620    // Validation: Rather than set a range for octave numbers, enforce a range that
621    // keeps m_dPercentChange above -99%, per Soundtouch constraints.
622    if ((nNewValue + 3) < m_nFromOctave)
623    {
624       ::wxBell();
625       m_pSpin_ToOctave->SetValue(m_nFromOctave - 3);
626       return;
627    }
628    m_nToOctave = nNewValue;
629 
630    m_ToFrequency = PitchToFreq(m_nToPitch, m_nToOctave);
631 
632    Calc_SemitonesChange_fromPitches();
633    Calc_PercentChange(); // Call *after* m_dSemitonesChange is updated.
634 
635    m_bLoopDetect = true;
636    {
637       Update_Text_SemitonesChange();
638       Update_Text_ToFrequency();
639       Update_Text_PercentChange();
640       Update_Slider_PercentChange();
641    }
642    m_bLoopDetect = false;
643 }
644 
OnText_SemitonesChange(wxCommandEvent & WXUNUSED (evt))645 void EffectChangePitch::OnText_SemitonesChange(wxCommandEvent & WXUNUSED(evt))
646 {
647    if (m_bLoopDetect)
648       return;
649 
650    if (!m_pTextCtrl_SemitonesChange->GetValidator()->TransferFromWindow())
651    {
652       EnableApply(false);
653       return;
654    }
655 
656    Calc_PercentChange();
657    Calc_ToFrequency(); // Call *after* m_dPercentChange is updated.
658    Calc_ToPitch();
659    Calc_ToOctave(); // Call after Calc_ToFrequency().
660 
661    m_bLoopDetect = true;
662    {
663       Update_Choice_ToPitch();
664       Update_Spin_ToOctave();
665       Update_Text_ToFrequency();
666       Update_Text_PercentChange();
667       Update_Slider_PercentChange();
668    }
669    m_bLoopDetect = false;
670 
671    // If m_dSemitonesChange is a big enough negative, we can go to or below 0 freq.
672    // If m_dSemitonesChange is a big enough positive, we can go to 1.#INF (Windows) or inf (Linux).
673    // But practically, these are best limits for Soundtouch.
674    bool bIsGoodValue = (m_dSemitonesChange > -80.0) && (m_dSemitonesChange <= 60.0);
675    EnableApply(bIsGoodValue);
676 }
677 
OnText_FromFrequency(wxCommandEvent & WXUNUSED (evt))678 void EffectChangePitch::OnText_FromFrequency(wxCommandEvent & WXUNUSED(evt))
679 {
680    if (m_bLoopDetect)
681       return;
682 
683    // Empty string causes unpredictable results with ToDouble() and later calculations.
684    // Non-positive frequency makes no sense, but user might still be editing,
685    // so it's not an error, but we do not want to update the values/controls.
686    if (!m_pTextCtrl_FromFrequency->GetValidator()->TransferFromWindow())
687    {
688       EnableApply(false);
689       return;
690    }
691 
692    double newFromMIDInote = FreqToMIDInote(m_FromFrequency);
693    m_nFromPitch = PitchIndex(newFromMIDInote);
694    m_nFromOctave = PitchOctave(newFromMIDInote);
695    Calc_ToPitch();
696    Calc_ToFrequency();
697    Calc_ToOctave(); // Call after Calc_ToFrequency().
698 
699    m_bLoopDetect = true;
700    {
701       Update_Choice_FromPitch();
702       Update_Spin_FromOctave();
703       Update_Choice_ToPitch();
704       Update_Spin_ToOctave();
705       Update_Text_ToFrequency();
706    }
707    m_bLoopDetect = false;
708 
709    // Success. Make sure OK and Preview are enabled, in case we disabled above during editing.
710    EnableApply(true);
711 }
712 
OnText_ToFrequency(wxCommandEvent & WXUNUSED (evt))713 void EffectChangePitch::OnText_ToFrequency(wxCommandEvent & WXUNUSED(evt))
714 {
715    if (m_bLoopDetect)
716       return;
717 
718    // Empty string causes unpredictable results with ToDouble() and later calculations.
719    // Non-positive frequency makes no sense, but user might still be editing,
720    // so it's not an error, but we do not want to update the values/controls.
721    if (!m_pTextCtrl_ToFrequency->GetValidator()->TransferFromWindow())
722    {
723       EnableApply(false);
724       return;
725    }
726 
727    m_dPercentChange = ((m_ToFrequency * 100.0) / m_FromFrequency) - 100.0;
728 
729    Calc_ToOctave(); // Call after Calc_ToFrequency().
730    Calc_SemitonesChange_fromPercentChange();
731    Calc_ToPitch(); // Call *after* m_dSemitonesChange is updated.
732 
733    m_bLoopDetect = true;
734    {
735       Update_Choice_ToPitch();
736       Update_Spin_ToOctave();
737       Update_Text_SemitonesChange();
738       Update_Text_PercentChange();
739       Update_Slider_PercentChange();
740    }
741    m_bLoopDetect = false;
742 
743    // Success. Make sure OK and Preview are disabled if percent change is out of bounds.
744    // Can happen while editing.
745    // If the value is good, might also need to re-enable because of above clause.
746    bool bIsGoodValue = (m_dPercentChange > MIN_Percentage) && (m_dPercentChange <= MAX_Percentage);
747    EnableApply(bIsGoodValue);
748 }
749 
OnText_PercentChange(wxCommandEvent & WXUNUSED (evt))750 void EffectChangePitch::OnText_PercentChange(wxCommandEvent & WXUNUSED(evt))
751 {
752    if (m_bLoopDetect)
753       return;
754 
755    if (!m_pTextCtrl_PercentChange->GetValidator()->TransferFromWindow())
756    {
757       EnableApply(false);
758       return;
759    }
760 
761    Calc_SemitonesChange_fromPercentChange();
762    Calc_ToPitch(); // Call *after* m_dSemitonesChange is updated.
763    Calc_ToFrequency();
764    Calc_ToOctave(); // Call after Calc_ToFrequency().
765 
766    m_bLoopDetect = true;
767    {
768       Update_Choice_ToPitch();
769       Update_Spin_ToOctave();
770       Update_Text_SemitonesChange();
771       Update_Text_ToFrequency();
772       Update_Slider_PercentChange();
773    }
774    m_bLoopDetect = false;
775 
776    // Success. Make sure OK and Preview are enabled, in case we disabled above during editing.
777    EnableApply(true);
778 }
779 
OnSlider_PercentChange(wxCommandEvent & WXUNUSED (evt))780 void EffectChangePitch::OnSlider_PercentChange(wxCommandEvent & WXUNUSED(evt))
781 {
782    if (m_bLoopDetect)
783       return;
784 
785    m_dPercentChange = (double)(m_pSlider_PercentChange->GetValue());
786    // Warp positive values to actually go up faster & further than negatives.
787    if (m_dPercentChange > 0.0)
788       m_dPercentChange = pow(m_dPercentChange, kSliderWarp);
789 
790    Calc_SemitonesChange_fromPercentChange();
791    Calc_ToPitch(); // Call *after* m_dSemitonesChange is updated.
792    Calc_ToFrequency();
793    Calc_ToOctave(); // Call after Calc_ToFrequency().
794 
795    m_bLoopDetect = true;
796    {
797       Update_Choice_ToPitch();
798       Update_Spin_ToOctave();
799       Update_Text_SemitonesChange();
800       Update_Text_ToFrequency();
801       Update_Text_PercentChange();
802    }
803    m_bLoopDetect = false;
804 }
805 
806 // helper fns for controls
807 
Update_Choice_FromPitch()808 void EffectChangePitch::Update_Choice_FromPitch()
809 {
810    m_pChoice_FromPitch->SetSelection(m_nFromPitch);
811 }
812 
Update_Spin_FromOctave()813 void EffectChangePitch::Update_Spin_FromOctave()
814 {
815    m_pSpin_FromOctave->SetValue(m_nFromOctave);
816 }
817 
Update_Choice_ToPitch()818 void EffectChangePitch::Update_Choice_ToPitch()
819 {
820    m_pChoice_ToPitch->SetSelection(m_nToPitch);
821 }
822 
Update_Spin_ToOctave()823 void EffectChangePitch::Update_Spin_ToOctave()
824 {
825    m_pSpin_ToOctave->SetValue(m_nToOctave);
826 }
827 
Update_Text_SemitonesChange()828 void EffectChangePitch::Update_Text_SemitonesChange()
829 {
830    m_pTextCtrl_SemitonesChange->GetValidator()->TransferToWindow();
831 }
832 
Update_Text_FromFrequency()833 void EffectChangePitch::Update_Text_FromFrequency()
834 {
835    m_pTextCtrl_FromFrequency->GetValidator()->TransferToWindow();
836 }
837 
Update_Text_ToFrequency()838 void EffectChangePitch::Update_Text_ToFrequency()
839 {
840    m_pTextCtrl_ToFrequency->GetValidator()->TransferToWindow();
841 }
842 
Update_Text_PercentChange()843 void EffectChangePitch::Update_Text_PercentChange()
844 {
845    m_pTextCtrl_PercentChange->GetValidator()->TransferToWindow();
846 }
847 
Update_Slider_PercentChange()848 void EffectChangePitch::Update_Slider_PercentChange()
849 {
850    double unwarped = m_dPercentChange;
851    if (unwarped > 0.0)
852       // Un-warp values above zero to actually go up to kSliderMax.
853       unwarped = pow(m_dPercentChange, (1.0 / kSliderWarp));
854 
855    // Add 0.5 to unwarped so trunc -> round.
856    m_pSlider_PercentChange->SetValue((int)(unwarped + 0.5));
857 }
858 
859 #endif // USE_SOUNDTOUCH
860 
861