1 /**********************************************************************
2 
3 Audacity: A Digital Audio Editor
4 
5 SpectrumView.cpp
6 
7 Paul Licameli split from WaveTrackView.cpp
8 
9 **********************************************************************/
10 
11 
12 #include "SpectrumView.h"
13 
14 #include "SpectralDataManager.h" // Cycle :-(
15 #include "SpectrumVRulerControls.h"
16 #include "WaveTrackView.h"
17 #include "WaveTrackViewConstants.h"
18 
19 #include "../../../ui/BrushHandle.h"
20 
21 #include "AColor.h"
22 #include "Prefs.h"
23 #include "NumberScale.h"
24 #include "../../../../TrackArtist.h"
25 #include "../../../../TrackPanelDrawingContext.h"
26 #include "ViewInfo.h"
27 #include "../../../../WaveClip.h"
28 #include "../../../../WaveTrack.h"
29 #include "../../../../prefs/SpectrogramSettings.h"
30 #include "../../../../ProjectSettings.h"
31 
32 #include <wx/dcmemory.h>
33 #include <wx/graphics.h>
34 
35 #include "float_cast.h"
36 
37 class BrushHandle;
38 class SpectralData;
39 
40 static WaveTrackSubView::Type sType{
41    WaveTrackViewConstants::Spectrum,
42    { wxT("Spectrogram"), XXO("&Spectrogram") }
43 };
44 
45 static WaveTrackSubViewType::RegisteredType reg{ sType };
46 
SpectrumView(WaveTrackView & waveTrackView)47 SpectrumView::SpectrumView(WaveTrackView &waveTrackView) : WaveTrackSubView(waveTrackView) {
48    auto wt = static_cast<WaveTrack*>( FindTrack().get() );
49    mpSpectralData = std::make_shared<SpectralData>(wt->GetRate());
50    mOnBrushTool = false;
51 }
52 
53 SpectrumView::~SpectrumView() = default;
54 
IsSpectral() const55 bool SpectrumView::IsSpectral() const
56 {
57    return true;
58 }
59 
60 class SpectrumView::SpectralDataSaver : public BrushHandle::StateSaver {
61 public:
SpectralDataSaver(SpectrumView & view)62    explicit SpectralDataSaver( SpectrumView &view )
63       : mView{ view }
64    {}
65 
Init(AudacityProject & project,bool clearAll)66    void Init( AudacityProject &project, bool clearAll ) override
67    {
68       mpProject = &project;
69       ForAll( project, [this, clearAll](SpectrumView &view){
70          auto pOldData = view.mpSpectralData;
71          if (clearAll) {
72             auto &pNewData = view.mpBackupSpectralData =
73                std::make_shared<SpectralData>(pOldData->GetSR());
74             pNewData->CopyFrom(*pOldData);
75             pOldData->clearAllData();
76          }
77          else {
78             // Back up one view only
79             if (&mView == &view) {
80                auto &pNewData = view.mpBackupSpectralData =
81                   std::make_shared<SpectralData>(pOldData->GetSR());
82                pNewData->CopyFrom( *pOldData );
83             }
84             else
85                view.mpBackupSpectralData = {};
86          }
87       });
88    }
89 
~SpectralDataSaver()90    ~SpectralDataSaver() override
91    {
92       if (mpProject)
93          ForAll( *mpProject, [this](SpectrumView &view){
94             if (mCommitted) {
95                // Discard all backups
96                view.mpBackupSpectralData = {};
97             }
98             else {
99                // Restore all
100                if (auto &pBackupData = view.mpBackupSpectralData) {
101                   view.mpSpectralData->CopyFrom(*pBackupData);
102                   pBackupData.reset();
103                }
104             }
105          });
106    }
107 
108 private:
109    SpectrumView &mView;
110    AudacityProject *mpProject = nullptr;
111 };
112 
113 // This always hits, but details of the hit vary with mouse position and
114 // key state.
BrushHandleHitTest(std::weak_ptr<BrushHandle> & holder,const TrackPanelMouseState & st,const AudacityProject * pProject,const std::shared_ptr<SpectrumView> & pTrackView,const std::shared_ptr<SpectralData> & mpData)115 static UIHandlePtr BrushHandleHitTest(
116    std::weak_ptr<BrushHandle> &holder,
117    const TrackPanelMouseState &st, const AudacityProject *pProject,
118    const std::shared_ptr<SpectrumView> &pTrackView,
119    const std::shared_ptr<SpectralData> &mpData)
120 {
121    const auto &viewInfo = ViewInfo::Get( *pProject );
122    auto &projectSettings = ProjectSettings::Get( *pProject );
123    auto result = std::make_shared<BrushHandle>(
124       std::make_shared<SpectrumView::SpectralDataSaver>(*pTrackView),
125       pTrackView, TrackList::Get( *pProject ),
126       st, viewInfo, mpData, projectSettings);
127 
128    result = AssignUIHandlePtr(holder, result);
129 
130    //Make sure we are within the selected track
131    // Adjusting the selection edges can be turned off in
132    // the preferences...
133    auto pTrack = pTrackView->FindTrack();
134    if (!pTrack->GetSelected() || !viewInfo.bAdjustSelectionEdges)
135    {
136       return result;
137    }
138 
139    return result;
140 }
141 
ForAll(AudacityProject & project,std::function<void (SpectrumView & view)> fn)142 void SpectrumView::ForAll( AudacityProject &project,
143    std::function<void(SpectrumView &view)> fn )
144 {
145    if (!fn)
146       return;
147    for ( const auto wt : TrackList::Get(project).Any< WaveTrack >() ) {
148       if (auto pWaveTrackView =
149           dynamic_cast<WaveTrackView*>( &TrackView::Get(*wt)) ) {
150          for (const auto &pSubView : pWaveTrackView->GetAllSubViews()) {
151             if (const auto sView = dynamic_cast<SpectrumView*>(pSubView.get()))
152                fn( *sView );
153          }
154       }
155    }
156 }
157 
DetailedHitTest(const TrackPanelMouseState & state,const AudacityProject * pProject,int currentTool,bool bMultiTool)158 std::vector<UIHandlePtr> SpectrumView::DetailedHitTest(
159    const TrackPanelMouseState &state,
160    const AudacityProject *pProject, int currentTool, bool bMultiTool )
161 {
162    const auto wt = std::static_pointer_cast< WaveTrack >( FindTrack() );
163    std::vector<UIHandlePtr> results;
164 
165 #ifdef EXPERIMENTAL_BRUSH_TOOL
166    mOnBrushTool = (currentTool == ToolCodes::brushTool);
167    if(mOnBrushTool){
168       const auto result = BrushHandleHitTest(
169          mBrushHandle, state,
170          pProject, std::static_pointer_cast<SpectrumView>(shared_from_this()),
171          mpSpectralData);
172       results.push_back(result);
173       return results;
174    }
175 #endif
176 
177    return WaveTrackSubView::DoDetailedHitTest(
178       state, pProject, currentTool, bMultiTool, wt
179    ).second;
180 }
181 
DoSetMinimized(bool minimized)182 void SpectrumView::DoSetMinimized( bool minimized )
183 {
184    auto wt = static_cast<WaveTrack*>( FindTrack().get() );
185 
186 #ifdef EXPERIMENTAL_HALF_WAVE
187    bool bHalfWave;
188    gPrefs->Read(wxT("/GUI/CollapseToHalfWave"), &bHalfWave, false);
189    if( bHalfWave && minimized)
190    {
191       // It is all right to set the top of scale to a huge number,
192       // not knowing the track rate here -- because when retrieving the
193       // value, then we pass in a sample rate and clamp it above to the
194       // Nyquist frequency.
195       constexpr auto max = std::numeric_limits<float>::max();
196       const bool spectrumLinear =
197          (wt->GetSpectrogramSettings().scaleType ==
198             SpectrogramSettings::stLinear);
199       // Zoom out full
200       wt->SetSpectrumBounds( spectrumLinear ? 0.0f : 1.0f, max );
201    }
202 #endif
203 
204    TrackView::DoSetMinimized( minimized );
205 }
206 
SubViewType() const207 auto SpectrumView::SubViewType() const -> const Type &
208 {
209    return sType;
210 }
211 
DoGetVRulerControls()212 std::shared_ptr<TrackVRulerControls> SpectrumView::DoGetVRulerControls()
213 {
214    return std::make_shared<SpectrumVRulerControls>( shared_from_this() );
215 }
216 
GetSpectralData()217 std::shared_ptr<SpectralData> SpectrumView::GetSpectralData(){
218    return mpSpectralData;
219 }
220 
CopyToSubView(WaveTrackSubView * destSubView) const221 void SpectrumView::CopyToSubView(WaveTrackSubView *destSubView) const{
222    if ( const auto pDest = dynamic_cast< SpectrumView* >( destSubView ) ) {
223       pDest->mpSpectralData =  std::make_shared<SpectralData>(mpSpectralData->GetSR());
224       pDest->mpSpectralData->CopyFrom(*mpSpectralData);
225    }
226 }
227 
228 namespace
229 {
230 
findValue(const float * spectrum,float bin0,float bin1,unsigned nBins,bool autocorrelation,int gain,int range)231 static inline float findValue
232 (const float *spectrum, float bin0, float bin1, unsigned nBins,
233  bool autocorrelation, int gain, int range)
234 {
235    float value;
236 
237 
238 #if 0
239    // Averaging method
240    if ((int)(bin1) == (int)(bin0)) {
241       value = spectrum[(int)(bin0)];
242    } else {
243       float binwidth= bin1 - bin0;
244       value = spectrum[(int)(bin0)] * (1.f - bin0 + (int)bin0);
245 
246       bin0 = 1 + (int)(bin0);
247       while (bin0 < (int)(bin1)) {
248          value += spectrum[(int)(bin0)];
249          bin0 += 1.0;
250       }
251       // Do not reference past end of freq array.
252       if ((int)(bin1) >= (int)nBins) {
253          bin1 -= 1.0;
254       }
255 
256       value += spectrum[(int)(bin1)] * (bin1 - (int)(bin1));
257       value /= binwidth;
258    }
259 #else
260    // Maximum method, and no apportionment of any single bins over multiple pixel rows
261    // See Bug971
262    int index, limitIndex;
263    if (autocorrelation) {
264       // bin = 2 * nBins / (nBins - 1 - array_index);
265       // Solve for index
266       index = std::max(0.0f, std::min(float(nBins - 1),
267          (nBins - 1) - (2 * nBins) / (std::max(1.0f, bin0))
268       ));
269       limitIndex = std::max(0.0f, std::min(float(nBins - 1),
270          (nBins - 1) - (2 * nBins) / (std::max(1.0f, bin1))
271       ));
272    }
273    else {
274       index = std::min<int>(nBins - 1, (int)(floor(0.5 + bin0)));
275       limitIndex = std::min<int>(nBins, (int)(floor(0.5 + bin1)));
276    }
277    value = spectrum[index];
278    while (++index < limitIndex)
279       value = std::max(value, spectrum[index]);
280 #endif
281    if (!autocorrelation) {
282       // Last step converts dB to a 0.0-1.0 range
283       value = (value + range + gain) / (double)range;
284    }
285    value = std::min(1.0f, std::max(0.0f, value));
286    return value;
287 }
288 
289 // dashCount counts both dashes and the spaces between them.
290 inline AColor::ColorGradientChoice
ChooseColorSet(float bin0,float bin1,float selBinLo,float selBinCenter,float selBinHi,int dashCount,bool isSpectral)291 ChooseColorSet( float bin0, float bin1, float selBinLo,
292    float selBinCenter, float selBinHi, int dashCount, bool isSpectral )
293 {
294    if (!isSpectral)
295       return  AColor::ColorGradientTimeSelected;
296    if ((selBinCenter >= 0) && (bin0 <= selBinCenter) &&
297        (selBinCenter < bin1))
298       return AColor::ColorGradientEdge;
299    if ((0 == dashCount % 2) &&
300        (((selBinLo >= 0) && (bin0 <= selBinLo) && ( selBinLo < bin1))  ||
301         ((selBinHi >= 0) && (bin0 <= selBinHi) && ( selBinHi < bin1))))
302       return AColor::ColorGradientEdge;
303    if ((selBinLo < 0 || selBinLo < bin1) && (selBinHi < 0 || selBinHi > bin0))
304       return  AColor::ColorGradientTimeAndFrequencySelected;
305 
306    return  AColor::ColorGradientTimeSelected;
307 }
308 
309 
DrawClipSpectrum(TrackPanelDrawingContext & context,WaveTrackCache & waveTrackCache,const WaveClip * clip,const wxRect & rect,const std::shared_ptr<SpectralData> & mpSpectralData,bool selected)310 void DrawClipSpectrum(TrackPanelDrawingContext &context,
311                                    WaveTrackCache &waveTrackCache,
312                                    const WaveClip *clip,
313                                    const wxRect &rect,
314                                    const std::shared_ptr<SpectralData> &mpSpectralData,
315                                    bool selected)
316 {
317    auto &dc = context.dc;
318    const auto artist = TrackArtist::Get( context );
319    bool onBrushTool = artist->onBrushTool;
320    const auto &selectedRegion = *artist->pSelectedRegion;
321    const auto &zoomInfo = *artist->pZoomInfo;
322 
323 #ifdef PROFILE_WAVEFORM
324    Profiler profiler;
325 #endif
326 
327    //If clip is "too small" draw a placeholder instead of
328    //attempting to fit the contents into a few pixels
329    if (!WaveTrackView::ClipDetailsVisible(*clip, zoomInfo, rect))
330    {
331       auto clipRect = ClipParameters::GetClipRect(*clip, zoomInfo, rect);
332       TrackArt::DrawClipFolded(dc, clipRect);
333       return;
334    }
335 
336    const WaveTrack *const track = waveTrackCache.GetTrack().get();
337    const SpectrogramSettings &settings = track->GetSpectrogramSettings();
338    const bool autocorrelation = (settings.algorithm == SpectrogramSettings::algPitchEAC);
339 
340    enum { DASH_LENGTH = 10 /* pixels */ };
341 
342    const ClipParameters params{
343       true, track, clip, rect, selectedRegion, zoomInfo };
344    const wxRect &hiddenMid = params.hiddenMid;
345    // The "hiddenMid" rect contains the part of the display actually
346    // containing the waveform, as it appears without the fisheye.  If it's empty, we're done.
347    if (hiddenMid.width <= 0) {
348       return;
349    }
350 
351    const double &t0 = params.t0;
352    const double &tOffset = params.tOffset;
353    const auto &ssel0 = params.ssel0;
354    const auto &ssel1 = params.ssel1;
355    const double &averagePixelsPerSample = params.averagePixelsPerSample;
356    const double &rate = params.rate;
357    const double &hiddenLeftOffset = params.hiddenLeftOffset;
358    const double &leftOffset = params.leftOffset;
359    const wxRect &mid = params.mid;
360 
361    double freqLo = SelectedRegion::UndefinedFrequency;
362    double freqHi = SelectedRegion::UndefinedFrequency;
363 #ifdef EXPERIMENTAL_SPECTRAL_EDITING
364    freqLo = selectedRegion.f0();
365    freqHi = selectedRegion.f1();
366 #endif
367 
368    const int &colorScheme = settings.colorScheme;
369    const int &range = settings.range;
370    const int &gain = settings.gain;
371 
372 #ifdef EXPERIMENTAL_FIND_NOTES
373    const bool &fftFindNotes = settings.fftFindNotes;
374    const double &findNotesMinA = settings.findNotesMinA;
375    const int &numberOfMaxima = settings.numberOfMaxima;
376    const bool &findNotesQuantize = settings.findNotesQuantize;
377 #endif
378 #ifdef EXPERIMENTAL_FFT_Y_GRID
379    const bool &fftYGrid = settings.fftYGrid;
380 #endif
381 
382    dc.SetPen(*wxTRANSPARENT_PEN);
383 
384    // We draw directly to a bit image in memory,
385    // and then paint this directly to our offscreen
386    // bitmap.  Note that this could be optimized even
387    // more, but for now this is not bad.  -dmazzoni
388    wxImage image((int)mid.width, (int)mid.height);
389    if (!image.IsOk())
390       return;
391 #ifdef EXPERIMENTAL_SPECTROGRAM_OVERLAY
392    image.SetAlpha();
393    unsigned char *alpha = image.GetAlpha();
394 #endif
395    unsigned char *data = image.GetData();
396 
397    const auto half = settings.GetFFTLength() / 2;
398    const double binUnit = rate / (2 * half);
399    const float *freq = 0;
400    const sampleCount *where = 0;
401    bool updated;
402    {
403       const double pps = averagePixelsPerSample * rate;
404       updated = clip->GetSpectrogram(waveTrackCache, freq, where,
405                                      (size_t)hiddenMid.width,
406          t0, pps);
407    }
408    auto nBins = settings.NBins();
409 
410    float minFreq, maxFreq;
411    track->GetSpectrumBounds(&minFreq, &maxFreq);
412 
413    const SpectrogramSettings::ScaleType scaleType = settings.scaleType;
414 
415    // nearest frequency to each pixel row from number scale, for selecting
416    // the desired fft bin(s) for display on that row
417    float *bins = (float*)alloca(sizeof(*bins)*(hiddenMid.height + 1));
418    {
419       const NumberScale numberScale( settings.GetScale( minFreq, maxFreq ) );
420 
421       NumberScale::Iterator it = numberScale.begin(mid.height);
422       float nextBin = std::max( 0.0f, std::min( float(nBins - 1),
423          settings.findBin( *it, binUnit ) ) );
424 
425       int yy;
426       for (yy = 0; yy < hiddenMid.height; ++yy) {
427          bins[yy] = nextBin;
428          nextBin = std::max( 0.0f, std::min( float(nBins - 1),
429             settings.findBin( *++it, binUnit ) ) );
430       }
431       bins[yy] = nextBin;
432    }
433 
434 #ifdef EXPERIMENTAL_FFT_Y_GRID
435    const float
436       log2 = logf(2.0f),
437       scale2 = (lmax - lmin) / log2,
438       lmin2 = lmin / log2;
439 
440    ArrayOf<bool> yGrid{size_t(mid.height)};
441    for (int yy = 0; yy < mid.height; ++yy) {
442       float n = (float(yy) / mid.height*scale2 - lmin2) * 12;
443       float n2 = (float(yy + 1) / mid.height*scale2 - lmin2) * 12;
444       float f = float(minFreq) / (fftSkipPoints + 1)*powf(2.0f, n / 12.0f + lmin2);
445       float f2 = float(minFreq) / (fftSkipPoints + 1)*powf(2.0f, n2 / 12.0f + lmin2);
446       n = logf(f / 440) / log2 * 12;
447       n2 = logf(f2 / 440) / log2 * 12;
448       if (floor(n) < floor(n2))
449          yGrid[yy] = true;
450       else
451          yGrid[yy] = false;
452    }
453 #endif //EXPERIMENTAL_FFT_Y_GRID
454 
455    if (!updated && clip->mSpecPxCache->valid &&
456       ((int)clip->mSpecPxCache->len == hiddenMid.height * hiddenMid.width)
457       && scaleType == clip->mSpecPxCache->scaleType
458       && gain == clip->mSpecPxCache->gain
459       && range == clip->mSpecPxCache->range
460       && minFreq == clip->mSpecPxCache->minFreq
461       && maxFreq == clip->mSpecPxCache->maxFreq
462 #ifdef EXPERIMENTAL_FFT_Y_GRID
463    && fftYGrid==fftYGridOld
464 #endif //EXPERIMENTAL_FFT_Y_GRID
465 #ifdef EXPERIMENTAL_FIND_NOTES
466    && fftFindNotes == artist->fftFindNotesOld
467    && findNotesMinA == artist->findNotesMinAOld
468    && numberOfMaxima == artist->findNotesNOld
469    && findNotesQuantize == artist->findNotesQuantizeOld
470 #endif
471    ) {
472       // Wave clip's spectrum cache is up to date,
473       // and so is the spectrum pixel cache
474    }
475    else {
476       // Update the spectrum pixel cache
477       clip->mSpecPxCache = std::make_unique<SpecPxCache>(hiddenMid.width * hiddenMid.height);
478       clip->mSpecPxCache->valid = true;
479       clip->mSpecPxCache->scaleType = scaleType;
480       clip->mSpecPxCache->gain = gain;
481       clip->mSpecPxCache->range = range;
482       clip->mSpecPxCache->minFreq = minFreq;
483       clip->mSpecPxCache->maxFreq = maxFreq;
484 #ifdef EXPERIMENTAL_FIND_NOTES
485       artist->fftFindNotesOld = fftFindNotes;
486       artist->findNotesMinAOld = findNotesMinA;
487       artist->findNotesNOld = numberOfMaxima;
488       artist->findNotesQuantizeOld = findNotesQuantize;
489 #endif
490 
491 #ifdef EXPERIMENTAL_FIND_NOTES
492       float log2 = logf( 2.0f ),
493          lmin = logf( minFreq ), lmax = logf( maxFreq ), scale = lmax - lmin,
494          lmins = lmin,
495          lmaxs = lmax
496          ;
497 #endif //EXPERIMENTAL_FIND_NOTES
498 
499 #ifdef EXPERIMENTAL_FIND_NOTES
500       int maxima[128];
501       float maxima0[128], maxima1[128];
502       const float
503          f2bin = half / (rate / 2.0f),
504          bin2f = 1.0f / f2bin,
505          minDistance = powf(2.0f, 2.0f / 12.0f),
506          i0 = expf(lmin) / binUnit,
507          i1 = expf(scale + lmin) / binUnit,
508          minColor = 0.0f;
509       const size_t maxTableSize = 1024;
510       ArrayOf<int> indexes{ maxTableSize };
511 #endif //EXPERIMENTAL_FIND_NOTES
512 
513 #ifdef _OPENMP
514 #pragma omp parallel for
515 #endif
516       for (int xx = 0; xx < hiddenMid.width; ++xx) {
517 #ifdef EXPERIMENTAL_FIND_NOTES
518          int maximas = 0;
519          const int x0 = nBins * xx;
520          if (fftFindNotes) {
521             for (int i = maxTableSize - 1; i >= 0; i--)
522                indexes[i] = -1;
523 
524             // Build a table of (most) values, put the index in it.
525             for (int i = (int)(i0); i < (int)(i1); i++) {
526                float freqi = freq[x0 + (int)(i)];
527                int value = (int)((freqi + gain + range) / range*(maxTableSize - 1));
528                if (value < 0)
529                   value = 0;
530                if (value >= maxTableSize)
531                   value = maxTableSize - 1;
532                indexes[value] = i;
533             }
534             // Build from the indices an array of maxima.
535             for (int i = maxTableSize - 1; i >= 0; i--) {
536                int index = indexes[i];
537                if (index >= 0) {
538                   float freqi = freq[x0 + index];
539                   if (freqi < findNotesMinA)
540                      break;
541 
542                   bool ok = true;
543                   for (int m = 0; m < maximas; m++) {
544                      // Avoid to store very close maxima.
545                      float maxm = maxima[m];
546                      if (maxm / index < minDistance && index / maxm < minDistance) {
547                         ok = false;
548                         break;
549                      }
550                   }
551                   if (ok) {
552                      maxima[maximas++] = index;
553                      if (maximas >= numberOfMaxima)
554                         break;
555                   }
556                }
557             }
558 
559 // The f2pix helper macro converts a frequency into a pixel coordinate.
560 #define f2pix(f) (logf(f)-lmins)/(lmaxs-lmins)*hiddenMid.height
561 
562             // Possibly quantize the maxima frequencies and create the pixel block limits.
563             for (int i = 0; i < maximas; i++) {
564                int index = maxima[i];
565                float f = float(index)*bin2f;
566                if (findNotesQuantize)
567                {
568                   f = expf((int)(log(f / 440) / log2 * 12 - 0.5) / 12.0f*log2) * 440;
569                   maxima[i] = f*f2bin;
570                }
571                float f0 = expf((log(f / 440) / log2 * 24 - 1) / 24.0f*log2) * 440;
572                maxima0[i] = f2pix(f0);
573                float f1 = expf((log(f / 440) / log2 * 24 + 1) / 24.0f*log2) * 440;
574                maxima1[i] = f2pix(f1);
575             }
576          }
577 
578          int it = 0;
579          bool inMaximum = false;
580 #endif //EXPERIMENTAL_FIND_NOTES
581 
582          for (int yy = 0; yy < hiddenMid.height; ++yy) {
583             const float bin     = bins[yy];
584             const float nextBin = bins[yy+1];
585 
586             if (settings.scaleType != SpectrogramSettings::stLogarithmic) {
587                const float value = findValue
588                   (freq + nBins * xx, bin, nextBin, nBins, autocorrelation, gain, range);
589                clip->mSpecPxCache->values[xx * hiddenMid.height + yy] = value;
590             }
591             else {
592                float value;
593 
594 #ifdef EXPERIMENTAL_FIND_NOTES
595                if (fftFindNotes) {
596                   if (it < maximas) {
597                      float i0 = maxima0[it];
598                      if (yy >= i0)
599                         inMaximum = true;
600 
601                      if (inMaximum) {
602                         float i1 = maxima1[it];
603                         if (yy + 1 <= i1) {
604                            value = findValue(freq + x0, bin, nextBin, nBins, autocorrelation, gain, range);
605                            if (value < findNotesMinA)
606                               value = minColor;
607                         }
608                         else {
609                            it++;
610                            inMaximum = false;
611                            value = minColor;
612                         }
613                      }
614                      else {
615                         value = minColor;
616                      }
617                   }
618                   else
619                      value = minColor;
620                }
621                else
622 #endif //EXPERIMENTAL_FIND_NOTES
623                {
624                   value = findValue
625                      (freq + nBins * xx, bin, nextBin, nBins, autocorrelation, gain, range);
626                }
627                clip->mSpecPxCache->values[xx * hiddenMid.height + yy] = value;
628             } // logF
629          } // each yy
630       } // each xx
631    } // updating cache
632 
633    float selBinLo = settings.findBin( freqLo, binUnit);
634    float selBinHi = settings.findBin( freqHi, binUnit);
635    float selBinCenter = (freqLo < 0 || freqHi < 0)
636       ? -1
637       : settings.findBin( sqrt(freqLo * freqHi), binUnit );
638 
639    const bool isSpectral = settings.SpectralSelectionEnabled();
640    const bool hidden = (ZoomInfo::HIDDEN == zoomInfo.GetFisheyeState());
641    const int begin = hidden
642       ? 0
643       : std::max(0, (int)(zoomInfo.GetFisheyeLeftBoundary(-leftOffset)));
644    const int end = hidden
645       ? 0
646       : std::min(mid.width, (int)(zoomInfo.GetFisheyeRightBoundary(-leftOffset)));
647    const size_t numPixels = std::max(0, end - begin);
648 
649    SpecCache specCache;
650 
651    // need explicit resize since specCache.where[] accessed before Populate()
652    specCache.Grow(numPixels, settings, -1, t0);
653 
654    if (numPixels > 0) {
655       for (int ii = begin; ii < end; ++ii) {
656          const double time = zoomInfo.PositionToTime(ii, -leftOffset) - tOffset;
657          specCache.where[ii - begin] = sampleCount(0.5 + rate * time);
658       }
659       specCache.Populate
660          (settings, waveTrackCache,
661           0, 0, numPixels,
662           clip->GetPlaySamplesCount(),
663           tOffset, rate,
664           0 // FIXME: PRL -- make reassignment work with fisheye
665        );
666    }
667 
668    // build color gradient tables (not thread safe)
669    if (!AColor::gradient_inited)
670       AColor::PreComputeGradient();
671 
672    // left pixel column of the fisheye
673    int fisheyeLeft = zoomInfo.GetFisheyeLeftBoundary(-leftOffset);
674 
675    // Bug 2389 - always draw at least one pixel of selection.
676    int selectedX = zoomInfo.TimeToPosition(selectedRegion.t0(), -leftOffset);
677 
678 #ifdef _OPENMP
679 #pragma omp parallel for
680 #endif
681 
682    const NumberScale numberScale(settings.GetScale(minFreq, maxFreq));
683    int windowSize = mpSpectralData->GetWindowSize();
684    int hopSize = mpSpectralData->GetHopSize();
685    double sr = mpSpectralData->GetSR();
686    auto &dataHistory = mpSpectralData->dataHistory;
687 
688    // Lazy way to add all hops and bins required for rendering
689    dataHistory.push_back(mpSpectralData->dataBuffer);
690 
691    // Generate combined hops and bins map for rendering
692    std::map<long long, std::set<int>> hopBinMap;
693    for(auto vecIter = dataHistory.begin(); vecIter != dataHistory.end(); ++vecIter){
694       for(const auto &hopMap: *vecIter){
695          for(const auto &binNum: hopMap.second)
696             hopBinMap[hopMap.first].insert(binNum);
697       }
698    }
699 
700    // Lambda for converting yy (not mouse coord!) to respective freq. bins
701    auto yyToFreqBin = [&](int yy){
702       const double p = double(yy) / hiddenMid.height;
703       float convertedFreq = numberScale.PositionToValue(p);
704       float convertedFreqBinNum = convertedFreq / (sr / windowSize);
705 
706       // By default lrintf will round to nearest by default, rounding to even on tie.
707       // std::round that was used here before rounds halfway cases away from zero.
708       // However, we can probably tolerate rounding issues here, as this will only slightly affect
709       // the visuals.
710       return static_cast<int>(lrintf(convertedFreqBinNum));
711    };
712 
713    for (int xx = 0; xx < mid.width; ++xx) {
714       int correctedX = xx + leftOffset - hiddenLeftOffset;
715 
716       // in fisheye mode the time scale has changed, so the row values aren't cached
717       // in the loop above, and must be fetched from fft cache
718       float* uncached;
719       if (!zoomInfo.InFisheye(xx, -leftOffset)) {
720           uncached = 0;
721       }
722       else {
723           int specIndex = (xx - fisheyeLeft) * nBins;
724           wxASSERT(specIndex >= 0 && specIndex < (int)specCache.freq.size());
725           uncached = &specCache.freq[specIndex];
726       }
727 
728       // zoomInfo must be queried for each column since with fisheye enabled
729       // time between columns is variable
730       auto w0 = sampleCount(0.5 + rate *
731                    (zoomInfo.PositionToTime(xx, -leftOffset) - tOffset));
732 
733       auto w1 = sampleCount(0.5 + rate *
734                     (zoomInfo.PositionToTime(xx+1, -leftOffset) - tOffset));
735 
736       bool maybeSelected = ssel0 <= w0 && w1 < ssel1;
737       maybeSelected = maybeSelected || (xx == selectedX);
738 
739       // In case the xx matches the hop number, it will be used as iterator for frequency bins
740       std::set<int> *pSelectedBins = nullptr;
741       std::set<int>::iterator freqBinIter;
742       auto advanceFreqBinIter = [&](int nextBinRounded){
743          while (freqBinIter != pSelectedBins->end() &&
744          *freqBinIter < nextBinRounded)
745             ++freqBinIter;
746       };
747 
748       bool hitHopNum = false;
749       if (onBrushTool) {
750          int convertedHopNum = (w0.as_long_long() + hopSize / 2) / hopSize;
751          hitHopNum = (hopBinMap.find(convertedHopNum) != hopBinMap.end());
752          if(hitHopNum) {
753             pSelectedBins = &hopBinMap[convertedHopNum];
754             freqBinIter = pSelectedBins->begin();
755             advanceFreqBinIter(yyToFreqBin(0));
756          }
757       }
758 
759       for (int yy = 0; yy < hiddenMid.height; ++yy) {
760          if(onBrushTool)
761             maybeSelected = false;
762          const float bin     = bins[yy];
763          const float nextBin = bins[yy+1];
764          auto binRounded = yyToFreqBin(yy);
765          auto nextBinRounded = yyToFreqBin(yy + 1);
766 
767          if(hitHopNum
768             && freqBinIter != pSelectedBins->end()
769             && binRounded == *freqBinIter)
770             maybeSelected = true;
771 
772          if (hitHopNum)
773             advanceFreqBinIter(nextBinRounded);
774 
775          // For spectral selection, determine what colour
776          // set to use.  We use a darker selection if
777          // in both spectral range and time range.
778 
779          AColor::ColorGradientChoice selected = AColor::ColorGradientUnselected;
780 
781          // If we are in the time selected range, then we may use a different color set.
782          if (maybeSelected) {
783             selected =
784                ChooseColorSet(bin, nextBin, selBinLo, selBinCenter, selBinHi,
785                   (xx + leftOffset - hiddenLeftOffset) / DASH_LENGTH, isSpectral);
786             if ( onBrushTool && selected != AColor::ColorGradientUnselected )
787                // use only two sets of colors
788                selected = AColor::ColorGradientTimeAndFrequencySelected;
789          }
790 
791          const float value = uncached
792             ? findValue(uncached, bin, nextBin, nBins, autocorrelation, gain, range)
793             : clip->mSpecPxCache->values[correctedX * hiddenMid.height + yy];
794 
795          unsigned char rv, gv, bv;
796          GetColorGradient(value, selected, colorScheme, &rv, &gv, &bv);
797 
798 #ifdef EXPERIMENTAL_FFT_Y_GRID
799          if (fftYGrid && yGrid[yy]) {
800             rv /= 1.1f;
801             gv /= 1.1f;
802             bv /= 1.1f;
803          }
804 #endif //EXPERIMENTAL_FFT_Y_GRID
805          int px = ((mid.height - 1 - yy) * mid.width + xx);
806 #ifdef EXPERIMENTAL_SPECTROGRAM_OVERLAY
807          // More transparent the closer to zero intensity.
808          alpha[px]= wxMin( 200, (value+0.3) * 500) ;
809 #endif
810          px *=3;
811          data[px++] = rv;
812          data[px++] = gv;
813          data[px] = bv;
814       } // each yy
815    } // each xx
816 
817    dataHistory.pop_back();
818    wxBitmap converted = wxBitmap(image);
819 
820    wxMemoryDC memDC;
821 
822    memDC.SelectObject(converted);
823 
824    dc.Blit(mid.x, mid.y, mid.width, mid.height, &memDC, 0, 0, wxCOPY, FALSE);
825 
826    // Draw clip edges, as also in waveform view, which improves the appearance
827    // of split views
828    {
829       auto clipRect = ClipParameters::GetClipRect(*clip, zoomInfo, rect);
830       TrackArt::DrawClipEdges(dc, clipRect, selected);
831    }
832 }
833 
834 }
835 
DoDraw(TrackPanelDrawingContext & context,const WaveTrack * track,const WaveClip * selectedClip,const wxRect & rect)836 void SpectrumView::DoDraw(TrackPanelDrawingContext& context,
837                                 const WaveTrack* track,
838                                 const WaveClip* selectedClip,
839                                 const wxRect & rect )
840 {
841    const auto artist = TrackArtist::Get( context );
842    const auto &blankSelectedBrush = artist->blankSelectedBrush;
843    const auto &blankBrush = artist->blankBrush;
844    TrackArt::DrawBackgroundWithSelection(
845       context, rect, track, blankSelectedBrush, blankBrush );
846 
847    WaveTrackCache cache(track->SharedPointer<const WaveTrack>());
848    for (const auto &clip: track->GetClips()){
849       DrawClipSpectrum( context, cache, clip.get(), rect,
850                         mpSpectralData, clip.get() == selectedClip);
851    }
852 
853    DrawBoldBoundaries( context, track, rect );
854 }
855 
Draw(TrackPanelDrawingContext & context,const wxRect & rect,unsigned iPass)856 void SpectrumView::Draw(
857    TrackPanelDrawingContext &context, const wxRect &rect, unsigned iPass )
858 {
859    if ( iPass == TrackArtist::PassTracks ) {
860       auto &dc = context.dc;
861       // Update cache for locations, e.g. cutlines and merge points
862       // Bug2588: do this for both channels, even if one is not drawn, so that
863       // cut-line editing (which depends on the locations cache) works properly.
864       // If both channels are visible, we will duplicate this effort, but that
865       // matters little.
866       for( auto channel:
867           TrackList::Channels(static_cast<WaveTrack*>(FindTrack().get())) )
868          channel->UpdateLocationsCache();
869 
870       const auto wt = std::static_pointer_cast<const WaveTrack>(
871          FindTrack()->SubstitutePendingChangedTrack());
872 
873       const auto artist = TrackArtist::Get( context );
874 
875 #if defined(__WXMAC__)
876       wxAntialiasMode aamode = dc.GetGraphicsContext()->GetAntialiasMode();
877       dc.GetGraphicsContext()->SetAntialiasMode(wxANTIALIAS_NONE);
878 #endif
879 
880       auto waveTrackView = GetWaveTrackView().lock();
881       wxASSERT(waveTrackView.use_count());
882 
883       auto seletedClip = waveTrackView->GetSelectedClip().lock();
884       DoDraw( context, wt.get(), seletedClip.get(), rect );
885 
886 #if defined(__WXMAC__)
887       dc.GetGraphicsContext()->SetAntialiasMode(aamode);
888 #endif
889    }
890    WaveTrackSubView::Draw( context, rect, iPass );
891 }
892 
893 static const WaveTrackSubViews::RegisteredFactory key{
__anon776fe5110702( )894    []( WaveTrackView &view ){
895       return std::make_shared< SpectrumView >( view );
896    }
897 };
898 
899 // The following attaches the spectrogram settings item to the wave track popup
900 // menu.  It is appropriate only to spectrum view and so is kept in this
901 // source file with the rest of the spectrum view implementation.
902 #include "WaveTrackControls.h"
903 #include "AudioIOBase.h"
904 #include "../../../../Menus.h"
905 #include "../../../../ProjectHistory.h"
906 #include "../../../../RefreshCode.h"
907 #include "../../../../prefs/PrefsDialog.h"
908 #include "../../../../prefs/SpectrumPrefs.h"
909 #include "../../../../widgets/AudacityMessageBox.h"
910 #include "../../../../widgets/PopupMenuTable.h"
911 
912 namespace {
913 struct SpectrogramSettingsHandler : PopupMenuHandler {
914 
915    PlayableTrackControls::InitMenuData *mpData{};
Instance__anon776fe5110811::SpectrogramSettingsHandler916    static SpectrogramSettingsHandler &Instance()
917    {
918       static SpectrogramSettingsHandler instance;
919       return instance;
920    }
921 
922    void OnSpectrogramSettings(wxCommandEvent &);
923 
InitUserData__anon776fe5110811::SpectrogramSettingsHandler924    void InitUserData(void *pUserData) override
925    {
926       mpData = static_cast< PlayableTrackControls::InitMenuData* >(pUserData);
927    }
928 };
929 
OnSpectrogramSettings(wxCommandEvent &)930 void SpectrogramSettingsHandler::OnSpectrogramSettings(wxCommandEvent &)
931 {
932    class ViewSettingsDialog final : public PrefsDialog
933    {
934    public:
935       ViewSettingsDialog(wxWindow *parent, AudacityProject &project,
936          const TranslatableString &title, PrefsPanel::Factories &factories,
937          int page)
938          : PrefsDialog(parent, &project, title, factories)
939          , mPage(page)
940       {
941       }
942 
943       long GetPreferredPage() override
944       {
945          return mPage;
946       }
947 
948       void SavePreferredPage() override
949       {
950       }
951 
952    private:
953       const int mPage;
954    };
955 
956    auto gAudioIO = AudioIOBase::Get();
957    if (gAudioIO->IsBusy()){
958       AudacityMessageBox(
959          XO(
960 "To change Spectrogram Settings, stop any\n playing or recording first."),
961          XO("Stop the Audio First"),
962          wxOK | wxICON_EXCLAMATION | wxCENTRE);
963       return;
964    }
965 
966    WaveTrack *const pTrack = static_cast<WaveTrack*>(mpData->pTrack);
967 
968    PrefsPanel::Factories factories;
969    // factories.push_back(WaveformPrefsFactory( pTrack ));
970    factories.push_back(SpectrumPrefsFactory( pTrack ));
971    const int page =
972       // (pTrack->GetDisplay() == WaveTrackViewConstants::Spectrum) ? 1 :
973       0;
974 
975    auto title = XO("%s:").Format( pTrack->GetName() );
976    ViewSettingsDialog dialog(
977       mpData->pParent, mpData->project, title, factories, page);
978 
979    if (0 != dialog.ShowModal()) {
980       // Redraw
981       AudacityProject *const project = &mpData->project;
982       ProjectHistory::Get( *project ).ModifyState(true);
983       //Bug 1725 Toolbar was left greyed out.
984       //This solution is overkill, but does fix the problem and is what the
985       //prefs dialog normally does.
986       MenuCreator::RebuildAllMenuBars();
987       mpData->result = RefreshCode::RefreshAll;
988    }
989 }
990 
991 PopupMenuTable::AttachedItem sAttachment{
992    GetWaveTrackMenuTable(),
993    { "SubViews/Extra" },
994    std::make_unique<PopupMenuSection>( "SpectrogramSettings",
995       // Conditionally add menu item for settings, if showing spectrum
996       PopupMenuTable::Computed< WaveTrackPopupMenuTable >(
__anon776fe5110902( ) 997          []( WaveTrackPopupMenuTable &table ) -> Registry::BaseItemPtr {
998             using Entry = PopupMenuTable::Entry;
999             static const int OnSpectrogramSettingsID =
1000             GetWaveTrackMenuTable().ReserveId();
1001 
1002             const auto pTrack = &table.FindWaveTrack();
1003             const auto &view = WaveTrackView::Get( *pTrack );
1004             const auto displays = view.GetDisplays();
1005             bool hasSpectrum = (displays.end() != std::find(
1006                displays.begin(), displays.end(),
1007                WaveTrackSubView::Type{ WaveTrackViewConstants::Spectrum, {} }
1008             ) );
1009             if( hasSpectrum )
1010                // In future, we might move this to the context menu of the
1011                // Spectrum vertical ruler.
1012                // (But the latter won't be satisfactory without a means to
1013                // open that other context menu with keystrokes only, and that
1014                // would require some notion of a focused sub-view.)
1015                return std::make_unique<Entry>( "SpectrogramSettings",
1016                   Entry::Item,
1017                   OnSpectrogramSettingsID,
1018                   XXO("S&pectrogram Settings..."),
1019                   (wxCommandEventFunction)
1020                      (&SpectrogramSettingsHandler::OnSpectrogramSettings),
1021                   SpectrogramSettingsHandler::Instance(),
1022                   []( PopupMenuHandler &handler, wxMenu &menu, int id ){
1023                      // Bug 1253.  Shouldn't open preferences if audio is busy.
1024                      // We can't change them on the fly yet anyway.
1025                      auto gAudioIO = AudioIOBase::Get();
1026                      menu.Enable(id, !gAudioIO->IsBusy());
1027                   } );
1028             else
1029                return nullptr;
1030          } ) )
1031 };
1032 }
1033 
ShouldCaptureEvent(wxKeyEvent & event,SpectralData * pData)1034 static bool ShouldCaptureEvent(wxKeyEvent& event, SpectralData *pData)
1035 {
1036    const auto keyCode = event.GetKeyCode();
1037    return
1038       (keyCode == WXK_BACK || keyCode == WXK_DELETE ||
1039        keyCode == WXK_NUMPAD_DELETE)
1040       && pData && !pData->dataHistory.empty();
1041 }
1042 
CaptureKey(wxKeyEvent & event,ViewInfo &,wxWindow *,AudacityProject *)1043 unsigned SpectrumView::CaptureKey(
1044    wxKeyEvent& event, ViewInfo&, wxWindow*, AudacityProject*)
1045 {
1046    bool capture = ShouldCaptureEvent(event, mpSpectralData.get());
1047    event.Skip(!capture);
1048    return RefreshCode::RefreshNone;
1049 }
1050 
KeyDown(wxKeyEvent & event,ViewInfo & viewInfo,wxWindow *,AudacityProject * project)1051 unsigned SpectrumView::KeyDown(wxKeyEvent& event, ViewInfo& viewInfo, wxWindow*, AudacityProject* project)
1052 {
1053    bool capture = ShouldCaptureEvent(event, mpSpectralData.get());
1054    event.Skip(!capture);
1055    if (capture && SpectralDataManager::ProcessTracks(*project))
1056       // Not RefreshCell, because there might be effects in multiple tracks
1057       return RefreshCode::RefreshAll;
1058    return RefreshCode::RefreshNone;
1059 }
1060 
Char(wxKeyEvent & event,ViewInfo &,wxWindow *,AudacityProject *)1061 unsigned SpectrumView::Char(
1062    wxKeyEvent &event, ViewInfo&, wxWindow*, AudacityProject* )
1063 {
1064    bool capture = ShouldCaptureEvent(event, mpSpectralData.get());
1065    event.Skip(!capture);
1066    return RefreshCode::RefreshNone;
1067 }
1068