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