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