1 /**********************************************************************
2 
3   Audacity: A Digital Audio Editor
4 
5   SpectrumPrefs.cpp
6 
7   Dominic Mazzoni
8   James Crook
9 
10 *******************************************************************//**
11 
12 \class SpectrumPrefs
13 \brief A PrefsPanel for spectrum settings.
14 
15 *//*******************************************************************/
16 
17 
18 #include "SpectrumPrefs.h"
19 
20 #include <wx/choice.h>
21 #include <wx/defs.h>
22 #include <wx/intl.h>
23 #include <wx/checkbox.h>
24 #include <wx/textctrl.h>
25 
26 #include "FFT.h"
27 #include "Project.h"
28 #include "../ShuttleGui.h"
29 
30 #include "../TrackPanel.h"
31 #include "../WaveTrack.h"
32 #include "../tracks/playabletrack/wavetrack/ui/WaveTrackView.h"
33 
34 #include <algorithm>
35 
36 #include "../widgets/AudacityMessageBox.h"
37 
SpectrumPrefs(wxWindow * parent,wxWindowID winid,AudacityProject * pProject,WaveTrack * wt)38 SpectrumPrefs::SpectrumPrefs(wxWindow * parent, wxWindowID winid,
39    AudacityProject *pProject, WaveTrack *wt)
40 :  PrefsPanel(parent, winid, wt ? XO("Spectrogram Settings") : XO("Spectrograms"))
41 , mProject{ pProject }
42 , mWt(wt)
43 , mPopulating(false)
44 {
45    if (mWt) {
46       SpectrogramSettings &settings = wt->GetSpectrogramSettings();
47       mOrigDefaulted = mDefaulted = (&SpectrogramSettings::defaults() == &settings);
48       mTempSettings = mOrigSettings = settings;
49       wt->GetSpectrumBounds(&mOrigMin, &mOrigMax);
50       mTempSettings.maxFreq = mOrigMax;
51       mTempSettings.minFreq = mOrigMin;
52       mOrigPlacements = WaveTrackView::Get( *mWt ).SavePlacements();
53    }
54    else  {
55       mTempSettings = mOrigSettings = SpectrogramSettings::defaults();
56       mOrigDefaulted = mDefaulted = false;
57    }
58 
59    const auto windowSize = mTempSettings.WindowSize();
60    mTempSettings.ConvertToEnumeratedWindowSizes();
61    Populate(windowSize);
62 }
63 
~SpectrumPrefs()64 SpectrumPrefs::~SpectrumPrefs()
65 {
66    if (!mCommitted)
67       Rollback();
68 }
69 
GetSymbol()70 ComponentInterfaceSymbol SpectrumPrefs::GetSymbol()
71 {
72    return SPECTRUM_PREFS_PLUGIN_SYMBOL;
73 }
74 
GetDescription()75 TranslatableString SpectrumPrefs::GetDescription()
76 {
77    return XO("Preferences for Spectrum");
78 }
79 
HelpPageName()80 ManualPageID SpectrumPrefs::HelpPageName()
81 {
82    // Currently (May2017) Spectrum Settings is the only preferences
83    // we ever display in a dialog on its own without others.
84    // We do so when it is configuring spectrums for a track.
85    // Because this happens, we want to visit a different help page.
86    // So we change the page name in the case of a page on its own.
87    return mWt
88       ? "Spectrogram_Settings"
89       : "Spectrograms_Preferences";
90 }
91 
92 enum {
93    ID_WINDOW_SIZE = 10001,
94    ID_WINDOW_TYPE,
95    ID_PADDING_SIZE,
96    ID_SCALE,
97    ID_ALGORITHM,
98    ID_MINIMUM,
99    ID_MAXIMUM,
100    ID_GAIN,
101    ID_RANGE,
102    ID_FREQUENCY_GAIN,
103    ID_COLOR_SCHEME,
104    ID_SPECTRAL_SELECTION,
105    ID_DEFAULTS,
106 };
107 
Populate(size_t windowSize)108 void SpectrumPrefs::Populate(size_t windowSize)
109 {
110    PopulatePaddingChoices(windowSize);
111 
112    for (int i = 0; i < NumWindowFuncs(); i++) {
113       mTypeChoices.push_back( WindowFuncName(i) );
114    }
115 
116    //------------------------- Main section --------------------
117    // Now construct the GUI itself.
118    ShuttleGui S(this, eIsCreatingFromPrefs);
119    PopulateOrExchange(S);
120    // ----------------------- End of main section --------------
121 }
122 
PopulatePaddingChoices(size_t windowSize)123 void SpectrumPrefs::PopulatePaddingChoices(size_t windowSize)
124 {
125    mZeroPaddingChoice = 1;
126 
127    // The choice of window size restricts the choice of padding.
128    // So the padding menu might grow or shrink.
129 
130    // If pPaddingSizeControl is NULL, we have not yet tied the choice control.
131    // If it is not NULL, we rebuild the control by hand.
132    // I don't yet know an easier way to do this with ShuttleGUI functions.
133    // PRL
134    wxChoice *const pPaddingSizeControl =
135       static_cast<wxChoice*>(wxWindow::FindWindowById(ID_PADDING_SIZE, this));
136 
137    if (pPaddingSizeControl) {
138       mZeroPaddingChoice = pPaddingSizeControl->GetSelection();
139       pPaddingSizeControl->Clear();
140    }
141 
142    unsigned padding = 1;
143    int numChoices = 0;
144    const size_t maxWindowSize = 1 << (SpectrogramSettings::LogMaxWindowSize);
145    while (windowSize <= maxWindowSize) {
146       const auto numeral = wxString::Format(wxT("%d"), padding);
147       mZeroPaddingChoices.push_back( Verbatim( numeral ) );
148       if (pPaddingSizeControl)
149          pPaddingSizeControl->Append(numeral);
150       windowSize <<= 1;
151       padding <<= 1;
152       ++numChoices;
153    }
154 
155    mZeroPaddingChoice = std::min(mZeroPaddingChoice, numChoices - 1);
156 
157    if (pPaddingSizeControl)
158       pPaddingSizeControl->SetSelection(mZeroPaddingChoice);
159 }
160 
PopulateOrExchange(ShuttleGui & S)161 void SpectrumPrefs::PopulateOrExchange(ShuttleGui & S)
162 {
163    mPopulating = true;
164    S.SetBorder(2);
165    S.StartScroller(); {
166 
167    // S.StartStatic(XO("Track Settings"));
168    // {
169 
170 
171    mDefaultsCheckbox = 0;
172    if (mWt) {
173       /* i18n-hint: use is a verb */
174       mDefaultsCheckbox = S.Id(ID_DEFAULTS).TieCheckBox(XXO("&Use Preferences"), mDefaulted);
175    }
176 
177    S.StartMultiColumn(2,wxEXPAND);
178    {
179       S.SetStretchyCol( 0 );
180       S.SetStretchyCol( 1 );
181       S.StartStatic(XO("Scale"),1);
182       {
183          S.StartMultiColumn(2,wxEXPAND);
184          {
185             S.SetStretchyCol( 0 );
186             S.SetStretchyCol( 1 );
187             S.Id(ID_SCALE).TieChoice(XXO("S&cale:"),
188                mTempSettings.scaleType,
189                Msgids( SpectrogramSettings::GetScaleNames() ) );
190             mMinFreq =
191                S.Id(ID_MINIMUM).TieNumericTextBox(XXO("Mi&n Frequency (Hz):"),
192                mTempSettings.minFreq,
193                12);
194             mMaxFreq =
195                S.Id(ID_MAXIMUM).TieNumericTextBox(XXO("Ma&x Frequency (Hz):"),
196                mTempSettings.maxFreq,
197                12);
198          }
199          S.EndMultiColumn();
200       }
201       S.EndStatic();
202 
203       S.StartStatic(XO("Colors"),1);
204       {
205          S.StartMultiColumn(2,wxEXPAND);
206          {
207             S.SetStretchyCol( 0 );
208             S.SetStretchyCol( 1 );
209             mGain =
210                S.Id(ID_GAIN).TieNumericTextBox(XXO("&Gain (dB):"),
211                mTempSettings.gain,
212                8);
213             mRange =
214                S.Id(ID_RANGE).TieNumericTextBox(XXO("&Range (dB):"),
215                mTempSettings.range,
216                8);
217 
218             mFrequencyGain =
219                S.Id(ID_FREQUENCY_GAIN).TieNumericTextBox(XXO("High &boost (dB/dec):"),
220                mTempSettings.frequencyGain,
221                8);
222 
223             // i18n-hint Scheme refers to a color scheme for spectrogram colors
224             S.Id(ID_COLOR_SCHEME).TieChoice(XC("Sche&me", "spectrum prefs"),
225                (int&)mTempSettings.colorScheme,
226                Msgids( SpectrogramSettings::GetColorSchemeNames() ) );
227          }
228          S.EndMultiColumn();
229       }
230       S.EndStatic();
231    }
232    S.EndMultiColumn();
233 
234    S.StartStatic(XO("Algorithm"));
235    {
236       S.StartMultiColumn(2);
237       {
238          mAlgorithmChoice =
239             S.Id(ID_ALGORITHM).TieChoice(XXO("A&lgorithm:"),
240             mTempSettings.algorithm,
241             SpectrogramSettings::GetAlgorithmNames() );
242 
243          S.Id(ID_WINDOW_SIZE).TieChoice(XXO("Window &size:"),
244             mTempSettings.windowSize,
245             {
246                XO("8 - most wideband"),
247                XO("16"),
248                XO("32"),
249                XO("64"),
250                XO("128"),
251                XO("256"),
252                XO("512"),
253                XO("1024 - default"),
254                XO("2048"),
255                XO("4096"),
256                XO("8192"),
257                XO("16384"),
258                XO("32768 - most narrowband"),
259             }
260          );
261 
262          S.Id(ID_WINDOW_TYPE).TieChoice(XXO("Window &type:"),
263             mTempSettings.windowType,
264             mTypeChoices);
265 
266          mZeroPaddingChoiceCtrl =
267             S.Id(ID_PADDING_SIZE).TieChoice(XXO("&Zero padding factor:"),
268             mTempSettings.zeroPaddingFactor,
269             mZeroPaddingChoices);
270       }
271       S.EndMultiColumn();
272    }
273    S.EndStatic();
274 
275 #ifndef SPECTRAL_SELECTION_GLOBAL_SWITCH
276    S.Id(ID_SPECTRAL_SELECTION).TieCheckBox(XXO("Ena&ble Spectral Selection"),
277       mTempSettings.spectralSelection);
278 #endif
279 
280 #ifdef EXPERIMENTAL_FFT_Y_GRID
281          S.TieCheckBox(XO("Show a grid along the &Y-axis"),
282             mTempSettings.fftYGrid);
283 #endif //EXPERIMENTAL_FFT_Y_GRID
284 
285 #ifdef EXPERIMENTAL_FIND_NOTES
286       /* i18n-hint: FFT stands for Fast Fourier Transform and probably shouldn't be translated*/
287       S.StartStatic(XO("FFT Find Notes"));
288       {
289          S.StartTwoColumn();
290          {
291             mFindNotesMinA =
292                S.TieNumericTextBox(XXO("Minimum Amplitude (dB):"),
293                mTempSettings.findNotesMinA,
294                8);
295 
296             mFindNotesN =
297                S.TieNumericTextBox(XXO("Max. Number of Notes (1..128):"),
298                mTempSettings.numberOfMaxima,
299                8);
300          }
301          S.EndTwoColumn();
302 
303          S.TieCheckBox(XXO("&Find Notes"),
304             mTempSettings.fftFindNotes);
305 
306          S.TieCheckBox(XXO("&Quantize Notes"),
307             mTempSettings.findNotesQuantize);
308       }
309       S.EndStatic();
310 #endif //EXPERIMENTAL_FIND_NOTES
311    // S.EndStatic();
312 
313 #ifdef SPECTRAL_SELECTION_GLOBAL_SWITCH
314    S.StartStatic(XO("Global settings"));
315    {
316       S.TieCheckBox(XXO("Ena&ble spectral selection"),
317          SpectrogramSettings::Globals::Get().spectralSelection);
318    }
319    S.EndStatic();
320 #endif
321 
322    } S.EndScroller();
323 
324    // Enabling and disabling belongs outside this function.
325    if( S.GetMode() != eIsGettingMetadata )
326       EnableDisableSTFTOnlyControls();
327 
328    mPopulating = false;
329 }
330 
Validate()331 bool SpectrumPrefs::Validate()
332 {
333    // Do checking for whole numbers
334 
335    // ToDo: use wxIntegerValidator<unsigned> when available
336 
337    long maxFreq;
338    if (!mMaxFreq->GetValue().ToLong(&maxFreq)) {
339       AudacityMessageBox( XO("The maximum frequency must be an integer") );
340       return false;
341    }
342 
343    long minFreq;
344    if (!mMinFreq->GetValue().ToLong(&minFreq)) {
345       AudacityMessageBox( XO("The minimum frequency must be an integer") );
346       return false;
347    }
348 
349    long gain;
350    if (!mGain->GetValue().ToLong(&gain)) {
351       AudacityMessageBox( XO("The gain must be an integer") );
352       return false;
353    }
354 
355    long range;
356    if (!mRange->GetValue().ToLong(&range)) {
357       AudacityMessageBox( XO("The range must be a positive integer") );
358       return false;
359    }
360 
361    long frequencygain;
362    if (!mFrequencyGain->GetValue().ToLong(&frequencygain)) {
363       AudacityMessageBox( XO("The frequency gain must be an integer") );
364       return false;
365    }
366 
367 #ifdef EXPERIMENTAL_FIND_NOTES
368    long findNotesMinA;
369    if (!mFindNotesMinA->GetValue().ToLong(&findNotesMinA)) {
370       AudacityMessageBox( XO("The minimum amplitude (dB) must be an integer") );
371       return false;
372    }
373 
374    long findNotesN;
375    if (!mFindNotesN->GetValue().ToLong(&findNotesN)) {
376       AudacityMessageBox( XO("The maximum number of notes must be an integer") );
377       return false;
378    }
379    if (findNotesN < 1 || findNotesN > 128) {
380       AudacityMessageBox( XO(
381 "The maximum number of notes must be in the range 1..128") );
382       return false;
383    }
384 #endif //EXPERIMENTAL_FIND_NOTES
385 
386    ShuttleGui S(this, eIsSavingToPrefs);
387    PopulateOrExchange(S);
388 
389    // Delegate range checking to SpectrogramSettings class
390    mTempSettings.ConvertToActualWindowSizes();
391    const bool result = mTempSettings.Validate(false);
392    mTempSettings.ConvertToEnumeratedWindowSizes();
393    return result;
394 }
395 
Rollback()396 void SpectrumPrefs::Rollback()
397 {
398    if (mWt) {
399       auto channels = TrackList::Channels(mWt);
400 
401       for (auto channel : channels) {
402          if (mOrigDefaulted) {
403             channel->SetSpectrogramSettings({});
404             channel->SetSpectrumBounds(-1, -1);
405          }
406          else {
407             auto &settings =
408                channel->GetIndependentSpectrogramSettings();
409             channel->SetSpectrumBounds(mOrigMin, mOrigMax);
410             settings = mOrigSettings;
411          }
412       }
413    }
414 
415    if (!mWt || mOrigDefaulted) {
416       SpectrogramSettings *const pSettings = &SpectrogramSettings::defaults();
417       *pSettings = mOrigSettings;
418    }
419 
420    const bool isOpenPage = this->IsShown();
421    if (mWt && isOpenPage) {
422       auto channels = TrackList::Channels(mWt);
423       for (auto channel : channels)
424          WaveTrackView::Get( *channel ).RestorePlacements( mOrigPlacements );
425    }
426 
427    if (isOpenPage) {
428       if ( mProject ) {
429          auto &tp = TrackPanel::Get ( *mProject );
430          tp.UpdateVRulers();
431          tp.Refresh(false);
432       }
433    }
434 }
435 
Preview()436 void SpectrumPrefs::Preview()
437 {
438    if (!Validate())
439       return;
440 
441    const bool isOpenPage = this->IsShown();
442 
443    ShuttleGui S(this, eIsSavingToPrefs);
444    PopulateOrExchange(S);
445 
446 
447    mTempSettings.ConvertToActualWindowSizes();
448 
449    if (mWt) {
450       for (auto channel : TrackList::Channels(mWt)) {
451          if (mDefaulted) {
452             channel->SetSpectrogramSettings({});
453             // ... and so that the vertical scale also defaults:
454             channel->SetSpectrumBounds(-1, -1);
455          }
456          else {
457             SpectrogramSettings &settings =
458                channel->GetIndependentSpectrogramSettings();
459             channel->SetSpectrumBounds(mTempSettings.minFreq, mTempSettings.maxFreq);
460             settings = mTempSettings;
461          }
462       }
463    }
464 
465    if (!mWt || mDefaulted) {
466       SpectrogramSettings *const pSettings = &SpectrogramSettings::defaults();
467       *pSettings = mTempSettings;
468    }
469    mTempSettings.ConvertToEnumeratedWindowSizes();
470 
471    // Bug 2278
472    // This code destroys any Multi-view we had.
473    // Commenting it out as it seems not to be needed.
474    /*
475    if (mWt && isOpenPage) {
476       for (auto channel : TrackList::Channels(mWt))
477          WaveTrackView::Get( *channel )
478             .SetDisplay( WaveTrackViewConstants::Spectrum );
479    }
480    */
481 
482    if (isOpenPage) {
483       if ( mProject ) {
484          auto &tp = TrackPanel::Get( *mProject );
485          tp.UpdateVRulers();
486          tp.Refresh(false);
487       }
488    }
489 }
490 
Commit()491 bool SpectrumPrefs::Commit()
492 {
493    if (!Validate())
494       return false;
495 
496    mCommitted = true;
497    SpectrogramSettings::Globals::Get().SavePrefs(); // always
498    SpectrogramSettings *const pSettings = &SpectrogramSettings::defaults();
499    if (!mWt || mDefaulted) {
500       pSettings->SavePrefs();
501    }
502    pSettings->LoadPrefs(); // always; in case Globals changed
503 
504    return true;
505 }
506 
ShowsPreviewButton()507 bool SpectrumPrefs::ShowsPreviewButton()
508 {
509    return mProject != nullptr;
510 }
511 
OnControl(wxCommandEvent &)512 void SpectrumPrefs::OnControl(wxCommandEvent&)
513 {
514    // Common routine for most controls
515    // If any per-track setting is changed, break the association with defaults
516    // Skip this, and View Settings... will be able to change defaults instead
517    // when the checkbox is on, as in the original design.
518 
519    if (mDefaultsCheckbox && !mPopulating) {
520       mDefaulted = false;
521       mDefaultsCheckbox->SetValue(false);
522    }
523 }
524 
OnWindowSize(wxCommandEvent & evt)525 void SpectrumPrefs::OnWindowSize(wxCommandEvent &evt)
526 {
527    // Restrict choice of zero padding, so that product of window
528    // size and padding may not exceed the largest window size.
529    wxChoice *const pWindowSizeControl =
530       static_cast<wxChoice*>(wxWindow::FindWindowById(ID_WINDOW_SIZE, this));
531    size_t windowSize = 1 <<
532       (pWindowSizeControl->GetSelection() + SpectrogramSettings::LogMinWindowSize);
533    PopulatePaddingChoices(windowSize);
534 
535    // Do the common part
536    OnControl(evt);
537 }
538 
OnDefaults(wxCommandEvent &)539 void SpectrumPrefs::OnDefaults(wxCommandEvent &)
540 {
541    if (mDefaultsCheckbox->IsChecked()) {
542       mTempSettings = SpectrogramSettings::defaults();
543       mTempSettings.ConvertToEnumeratedWindowSizes();
544       mDefaulted = true;
545       ShuttleGui S(this, eIsSettingToDialog);
546       PopulateOrExchange(S);
547    }
548 }
549 
OnAlgorithm(wxCommandEvent & evt)550 void SpectrumPrefs::OnAlgorithm(wxCommandEvent &evt)
551 {
552    EnableDisableSTFTOnlyControls();
553    OnControl(evt);
554 }
555 
EnableDisableSTFTOnlyControls()556 void SpectrumPrefs::EnableDisableSTFTOnlyControls()
557 {
558    // Enable or disable other controls that are applicable only to STFT.
559    const bool STFT =
560       (mAlgorithmChoice->GetSelection() != SpectrogramSettings::algPitchEAC);
561    mGain->Enable(STFT);
562    mRange->Enable(STFT);
563    mFrequencyGain->Enable(STFT);
564    mZeroPaddingChoiceCtrl->Enable(STFT);
565 }
566 
BEGIN_EVENT_TABLE(SpectrumPrefs,PrefsPanel)567 BEGIN_EVENT_TABLE(SpectrumPrefs, PrefsPanel)
568    EVT_CHOICE(ID_WINDOW_SIZE, SpectrumPrefs::OnWindowSize)
569    EVT_CHECKBOX(ID_DEFAULTS, SpectrumPrefs::OnDefaults)
570    EVT_CHOICE(ID_ALGORITHM, SpectrumPrefs::OnAlgorithm)
571 
572    // Several controls with common routine that unchecks the default box
573    EVT_CHOICE(ID_WINDOW_TYPE, SpectrumPrefs::OnControl)
574    EVT_CHOICE(ID_PADDING_SIZE, SpectrumPrefs::OnControl)
575    EVT_CHOICE(ID_SCALE, SpectrumPrefs::OnControl)
576    EVT_TEXT(ID_MINIMUM, SpectrumPrefs::OnControl)
577    EVT_TEXT(ID_MAXIMUM, SpectrumPrefs::OnControl)
578    EVT_TEXT(ID_GAIN, SpectrumPrefs::OnControl)
579    EVT_TEXT(ID_RANGE, SpectrumPrefs::OnControl)
580    EVT_TEXT(ID_FREQUENCY_GAIN, SpectrumPrefs::OnControl)
581    EVT_CHOICE(ID_COLOR_SCHEME, SpectrumPrefs::OnControl)
582    EVT_CHECKBOX(ID_SPECTRAL_SELECTION, SpectrumPrefs::OnControl)
583 
584 END_EVENT_TABLE()
585 
586 PrefsPanel::Factory
587 SpectrumPrefsFactory( WaveTrack *wt )
588 {
589    return [=](wxWindow *parent, wxWindowID winid, AudacityProject *pProject)
590    {
591       wxASSERT(parent); // to justify safenew
592       return safenew SpectrumPrefs(parent, winid, pProject, wt);
593    };
594 }
595 
596 namespace{
597 PrefsPanel::Registration sAttachment{ "Spectrum",
598    SpectrumPrefsFactory( nullptr ),
599    false,
600    // Place it at a lower tree level
601    { "Tracks" }
602 };
603 }
604