1 /*
2  * Spectral Analyzer audio effect based on DISTRHO Plugin Framework (DPF)
3  *
4  * SPDX-License-Identifier: MIT
5  *
6  * Copyright (C) 2019 Jean Pierre Cimalando <jpcima@protonmail.com>
7  *
8  * Permission is hereby granted, free of charge, to any person obtaining a copy
9  * of this software and associated documentation files (the "Software"), to
10  * deal in the Software without restriction, including without limitation the
11  * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12  * sell copies of the Software, and to permit persons to whom the Software is
13  * furnished to do so, subject to the following conditions:
14  *
15  * The above copyright notice and this permission notice shall be included in
16  * all copies or substantial portions of the Software.
17  *
18  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24  * IN THE SOFTWARE.
25  */
26 
27 #include "UISpectralAnalyzer.hpp"
28 #include "Parameters.h"
29 #include "ColorPalette.h"
30 #include "Config.h"
31 #include "ui/components/SpectrumView.h"
32 #include "ui/components/FloatingWindow.h"
33 #include "ui/components/SpinBoxChooser.h"
34 #include "ui/components/Slider.h"
35 #include "ui/components/TextLabel.h"
36 #include "ui/components/SelectionRectangle.h"
37 #include "ui/components/ResizeHandle.h"
38 #include "ui/FontEngine.h"
39 #include "dsp/AnalyzerDefs.h"
40 #include "util/format_string.h"
41 #include "Window.hpp"
42 #include "Color.hpp"
43 #include <sys/stat.h>
44 #include <cmath>
45 
46 enum {
47     kToolBarIdSetup = 1,
48     kToolBarIdScale,
49     kToolBarIdFreeze,
50     kToolBarIdSelect,
51     kToolBarIdHide,
52     kToolBarIdColor,
53 };
54 
55 // -----------------------------------------------------------------------
56 // Init / Deinit
57 
UISpectralAnalyzer()58 UISpectralAnalyzer::UISpectralAnalyzer()
59     : UI(1000, 350),
60       fPalette(new ColorPalette(ColorPalette::create_default()))
61 {
62     ColorPalette &palette = *fPalette;
63 
64     {
65         std::unique_ptr<CSimpleIniA> ini = create_theme();
66         ini->SetFileComment(
67             ";; This document describes the default theme." "\n"
68             ";; Important: do not edit this file directly! changes will be lost." "\n"
69             ";; If you want to design a theme, duplicate this file with a different name.");
70         ColorPalette::save_defaults(*ini, "color", true);
71         save_theme("default", *ini);
72     }
73 
74     fUiConfig = load_configuration("ui");
75     if (!fUiConfig)
76         fUiConfig = create_configuration();
77 
78     CSimpleIniA &uiConfig = *fUiConfig;
79     bool updateUiConfig = false;
80     if (!uiConfig.GetFileComment()) {
81         uiConfig.SetFileComment(";; This is the configuration of the user interface.");
82         updateUiConfig = true;
83     }
84     if (!uiConfig.GetValue("ui", "theme")) {
85         uiConfig.SetValue("ui", "theme", "default", "; Identifier of the theme which is active on program startup");
86         updateUiConfig = true;
87     }
88     if (updateUiConfig)
89         save_configuration("ui", uiConfig);
90 
91     loadTheme(uiConfig.GetValue("ui", "theme", "default"));
92 
93     FontEngine fe(*this, palette);
94 
95     fSpectrumView = makeSubwidget<SpectrumView>(this, palette);
96 
97     fMainToolBar = makeSubwidget<MainToolBar>(this, palette);
98     fMainToolBar->addButton(kToolBarIdSetup, "Setup", "\uf085");
99     fMainToolBar->addButton(kToolBarIdScale, "Scale", "\uf0b2");
100     fMainToolBar->addButton(kToolBarIdFreeze, "Freeze", "\uf256");
101     fMainToolBar->addButton(kToolBarIdSelect, "Select", "\uf05b");
102     fMainToolBar->addButton(kToolBarIdHide, "Hide", "\uf0d0");
103     fMainToolBar->addButton(kToolBarIdColor, "Color", "\uf53f");
104     fMainToolBar->setListener(this);
105 
106     Font fontAwesome;
107     fontAwesome.name = "awesome";
108     fontAwesome.size = 16.0;
109     fontAwesome.colorRef = Colors::text_normal;
110     Font fontLabel;
111     fontLabel.name = "regular";
112     fontLabel.size = 12.0;
113     fontLabel.colorRef = Colors::text_normal;
114     Font fontChNLabel[kNumChannels];
115     Font fontChNAwesome[kNumChannels];
116     for (uint32_t c = 0; c < kNumChannels; ++c) {
117         fontChNLabel[c] = fontLabel;
118         fontChNAwesome[c] = fontAwesome;
119         fontChNLabel[c].colorRef = Colors::text_channel1 + c;
120         fontChNAwesome[c].colorRef = Colors::text_channel1 + c;
121     }
122 
123     fSetupWindow = makeSubwidget<FloatingWindow>(this, palette);
124     fSetupWindow->setVisible(false);
125     fSetupWindow->setSize(260, 160);
126     {
127         int y = 10;
128 
129         TextLabel *label;
130 
131 
132         label = makeSubwidget<TextLabel>(fSetupWindow, palette);
133         label->setText("Algorithm");
134         label->setFont(fontLabel);
135         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
136         label->setAbsolutePos(10, y);
137         label->setSize(100, 20);
138         fSetupWindow->moveAlong(label);
139 
140         fAlgorithmChooser = makeSubwidget<SpinBoxChooser>(fSetupWindow, palette);
141         fAlgorithmChooser->setSize(150, 20);
142         fAlgorithmChooser->setAbsolutePos(100, y);
143         for (uint32_t algo = 0; algo < kNumAlgorithms; ++algo)
144             fAlgorithmChooser->addChoice(algo, getAlgorithmName((Algorithm)algo));
145         fAlgorithmChooser->ValueChangedCallback = [this](int32_t value)
146             { setParameterValue(kPidAlgorithm, value); };
147         fSetupWindow->moveAlong(fAlgorithmChooser);
148 
149         y += 30;
150 
151         label = makeSubwidget<TextLabel>(fSetupWindow, palette);
152         label->setText("Resolution");
153         label->setFont(fontLabel);
154         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
155         label->setAbsolutePos(10, y);
156         label->setSize(100, 20);
157         fSetupWindow->moveAlong(label);
158 
159         fFftSizeChooser = makeSubwidget<SpinBoxChooser>(fSetupWindow, palette);
160         fFftSizeChooser->setSize(150, 20);
161         fFftSizeChooser->setAbsolutePos(100, y);
162         for (uint32_t sizeLog2 = kStftMinSizeLog2; sizeLog2 <= kStftMaxSizeLog2; ++sizeLog2)
163             fFftSizeChooser->addChoice(sizeLog2, std::to_string(1u << sizeLog2).c_str());
164         fFftSizeChooser->ValueChangedCallback = [this](int32_t value)
165             { setParameterValue(kPidFftSize, value); };
166         fSetupWindow->moveAlong(fFftSizeChooser);
167 
168         y += 30;
169 
170         label = makeSubwidget<TextLabel>(fSetupWindow, palette);
171         label->setText("Step");
172         label->setFont(fontLabel);
173         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
174         label->setAbsolutePos(10, y);
175         label->setSize(100, 20);
176         fSetupWindow->moveAlong(label);
177 
178         fStepSizeChooser = makeSubwidget<SpinBoxChooser>(fSetupWindow, palette);
179         fStepSizeChooser->setSize(150, 20);
180         fStepSizeChooser->setAbsolutePos(100, y);
181         for (uint32_t stepLog2 = kStftMinStepLog2; stepLog2 <= kStftMaxStepLog2; ++stepLog2)
182             fStepSizeChooser->addChoice(stepLog2, std::to_string(1u << stepLog2).c_str());
183         fStepSizeChooser->ValueChangedCallback = [this](int32_t value)
184             { setParameterValue(kPidStepSize, value); };
185         fSetupWindow->moveAlong(fStepSizeChooser);
186 
187         y += 30;
188 
189         label = makeSubwidget<TextLabel>(fSetupWindow, palette);
190         label->setText("Attack time");
191         label->setFont(fontLabel);
192         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
193         label->setAbsolutePos(10, y);
194         label->setSize(100, 20);
195         fSetupWindow->moveAlong(label);
196 
197         fAttackTimeSlider = makeSubwidget<Slider>(fSetupWindow, palette);
198         fAttackTimeSlider->setSize(150, 20);
199         fAttackTimeSlider->setAbsolutePos(100, y);
200         fAttackTimeSlider->setValueBounds(kStftMinAttackTime, kStftMaxAttackTime);
201         fAttackTimeSlider->ValueChangedCallback = [this](double value)
202             { setParameterValue(kPidAttackTime, value); };
203         fAttackTimeSlider->FormatCallback = [](double value) -> std::string
204             { return std::to_string(std::lround(value * 1e3)) + " ms"; };
205         fSetupWindow->moveAlong(fAttackTimeSlider);
206 
207         y += 30;
208 
209         label = makeSubwidget<TextLabel>(fSetupWindow, palette);
210         label->setText("Release time");
211         label->setFont(fontLabel);
212         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
213         label->setAbsolutePos(10, y);
214         label->setSize(100, 20);
215         fSetupWindow->moveAlong(label);
216 
217         fReleaseTimeSlider = makeSubwidget<Slider>(fSetupWindow, palette);
218         fReleaseTimeSlider->setSize(150, 20);
219         fReleaseTimeSlider->setAbsolutePos(100, y);
220         fReleaseTimeSlider->setValueBounds(kStftMinReleaseTime, kStftMaxReleaseTime);
221         fReleaseTimeSlider->ValueChangedCallback = [this](double value)
222             { setParameterValue(kPidReleaseTime, value); };
223         fReleaseTimeSlider->FormatCallback = [](double value) -> std::string
224             { return std::to_string(std::lround(value * 1e3)) + " ms"; };
225         fSetupWindow->moveAlong(fReleaseTimeSlider);
226     }
227 
228     fScaleWindow = makeSubwidget<FloatingWindow>(this, palette);
229     fScaleWindow->setVisible(false);
230     fScaleWindow->setSize(180, 70);
231     {
232         int y = 10;
233 
234         TextLabel *label;
235 
236         label = makeSubwidget<TextLabel>(fScaleWindow, palette);
237         label->setText("\uf8cc");
238         label->setFont(fontAwesome);
239         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
240         label->setAbsolutePos(10, y);
241         label->setSize(130, 20);
242         fScaleWindow->moveAlong(label);
243 
244         label = makeSubwidget<TextLabel>(fScaleWindow, palette);
245         label->setText("left: select zoom region");
246         label->setFont(fontLabel);
247         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
248         label->setAbsolutePos(30, y);
249         label->setSize(130, 20);
250         fScaleWindow->moveAlong(label);
251 
252         y += 30;
253 
254         label = makeSubwidget<TextLabel>(fScaleWindow, palette);
255         label->setText("\uf8cc");
256         label->setFont(fontAwesome);
257         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
258         label->setAbsolutePos(10, y);
259         label->setSize(130, 20);
260         fScaleWindow->moveAlong(label);
261 
262         label = makeSubwidget<TextLabel>(fScaleWindow, palette);
263         label->setText("right: reset zoom");
264         label->setFont(fontLabel);
265         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
266         label->setAbsolutePos(30, y);
267         label->setSize(130, 20);
268         fScaleWindow->moveAlong(label);
269     }
270 
271     fSelectWindow = makeSubwidget<FloatingWindow>(this, palette);
272     fSelectWindow->setVisible(false);
273     fSelectWindow->setSize(420, 100);
274     {
275         int x = 0;
276         int y = 10;
277 
278         TextLabel *label;
279 
280         label = makeSubwidget<TextLabel>(fSelectWindow, palette);
281         label->setText("Cursor");
282         label->setFont(fontLabel);
283         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
284         label->setAbsolutePos(x + 10, y);
285         label->setSize(100, 20);
286         fSelectWindow->moveAlong(label);
287 
288         y += 30;
289 
290         label = makeSubwidget<TextLabel>(fSelectWindow, palette);
291         label->setText("\uf337");
292         label->setFont(fontAwesome);
293         label->setAlignment(kAlignCenter|kAlignInside);
294         label->setAbsolutePos(x + 5, y);
295         label->setSize(20, 20);
296         fSelectWindow->moveAlong(label);
297 
298         label = makeSubwidget<TextLabel>(fSelectWindow, palette);
299         fSelectLabelX = label;
300         //label->setText("");
301         label->setFont(fontLabel);
302         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
303         label->setAbsolutePos(x + 30, y);
304         label->setSize(100, 20);
305         fSelectWindow->moveAlong(label);
306 
307         y += 30;
308 
309         label = makeSubwidget<TextLabel>(fSelectWindow, palette);
310         label->setText("\uf338");
311         label->setFont(fontAwesome);
312         label->setAlignment(kAlignCenter|kAlignInside);
313         label->setAbsolutePos(x + 5, y);
314         label->setSize(20, 20);
315         fSelectWindow->moveAlong(label);
316 
317         label = makeSubwidget<TextLabel>(fSelectWindow, palette);
318         fSelectLabelY = label;
319         //label->setText("");
320         label->setFont(fontLabel);
321         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
322         label->setAbsolutePos(x + 30, y);
323         label->setSize(100, 20);
324         fSelectWindow->moveAlong(label);
325 
326         y = 10;
327         x += 100;
328 
329         for (unsigned c = 0; c < kNumChannels; ++c) {
330             y += 30;
331 
332             label = makeSubwidget<TextLabel>(fSelectWindow, palette);
333             label->setText("\uf140");
334             label->setFont(fontChNAwesome[c]);
335             label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
336             label->setAbsolutePos(x + 10, y);
337             label->setSize(100, 20);
338             fSelectWindow->moveAlong(label);
339 
340             label = makeSubwidget<TextLabel>(fSelectWindow, palette);
341             fSelectChannelY[c] = label;
342             //label->setText("");
343             label->setFont(fontChNLabel[c]);
344             label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
345             label->setAbsolutePos(x + 30, y);
346             label->setSize(100, 20);
347             fSelectWindow->moveAlong(label);
348         }
349 
350         y = 10;
351         x += 120;
352 
353         for (unsigned c = 0; c < kNumChannels; ++c) {
354             y = 10;
355 
356             if (c == 0) {
357                 label = makeSubwidget<TextLabel>(fSelectWindow, palette);
358                 label->setText("Nearby peak");
359                 label->setFont(fontLabel);
360                 label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
361                 label->setAbsolutePos(x + 10, y);
362                 label->setSize(100, 20);
363                 fSelectWindow->moveAlong(label);
364             }
365 
366             y += 30;
367 
368             label = makeSubwidget<TextLabel>(fSelectWindow, palette);
369             label->setText("\uf140");
370             label->setFont(fontChNAwesome[c]);
371             label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
372             label->setAbsolutePos(x + 10, y);
373             label->setSize(100, 20);
374             fSelectWindow->moveAlong(label);
375 
376             label = makeSubwidget<TextLabel>(fSelectWindow, palette);
377             fSelectNearPeakX[c] = label;
378             //label->setText("");
379             label->setFont(fontChNLabel[c]);
380             label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
381             label->setAbsolutePos(x + 30, y);
382             label->setSize(100, 20);
383             fSelectWindow->moveAlong(label);
384 
385             y += 30;
386 
387             label = makeSubwidget<TextLabel>(fSelectWindow, palette);
388             label->setText("\uf140");
389             label->setFont(fontChNAwesome[c]);
390             label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
391             label->setAbsolutePos(x + 10, y);
392             label->setSize(100, 20);
393             fSelectWindow->moveAlong(label);
394 
395             label = makeSubwidget<TextLabel>(fSelectWindow, palette);
396             fSelectNearPeakY[c] = label;
397             //label->setText("");
398             label->setFont(fontChNLabel[c]);
399             label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
400             label->setAbsolutePos(x + 30, y);
401             label->setSize(100, 20);
402             fSelectWindow->moveAlong(label);
403 
404             x += 100;
405         }
406     }
407 
408     fColorWindow = makeSubwidget<FloatingWindow>(this, palette);
409     fColorWindow->setVisible(false);
410     fColorWindow->setSize(260, 70);
411     {
412         int y = 10;
413 
414         TextLabel *label;
415 
416         label = makeSubwidget<TextLabel>(fColorWindow, palette);
417         label->setText("Theme");
418         label->setFont(fontLabel);
419         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
420         label->setAbsolutePos(10, y);
421         label->setSize(100, 20);
422         fColorWindow->moveAlong(label);
423 
424         fThemeChooser = makeSubwidget<SpinBoxChooser>(fColorWindow, palette);
425         fThemeChooser->setSize(150, 20);
426         fThemeChooser->setAbsolutePos(100, y);
427         fColorWindow->moveAlong(fThemeChooser);
428 
429         y += 30;
430 
431         label = makeSubwidget<TextLabel>(fColorWindow, palette);
432         label->setText("Edit mode");
433         label->setFont(fontLabel);
434         label->setAlignment(kAlignLeft|kAlignCenter|kAlignInside);
435         label->setAbsolutePos(10, y);
436         label->setSize(100, 20);
437         fColorWindow->moveAlong(label);
438 
439         fThemeEditChooser = makeSubwidget<SpinBoxChooser>(fColorWindow, palette);
440         fThemeEditChooser->setSize(150, 20);
441         fThemeEditChooser->setAbsolutePos(100, y);
442         fThemeEditChooser->addChoice(0, "off");
443         fThemeEditChooser->addChoice(1, "on");
444         fColorWindow->moveAlong(fThemeEditChooser);
445     }
446 
447     fSelectionRectangle = makeSubwidget<SelectionRectangle>(this, palette);
448     fSelectionRectangle->setVisible(false);
449 
450     fResizeHandle = makeSubwidget<ResizeHandle>(this, palette);
451     fResizeHandle->setSize(20, 20);
452     fResizeHandle->RequestToResizeCallback = [this](DGL::Size<uint> newSize) {
453         setSize(newSize);
454     };
455 
456     uiReshape(getWidth(), getHeight());
457 }
458 
~UISpectralAnalyzer()459 UISpectralAnalyzer::~UISpectralAnalyzer()
460 {
461     getPluginInstance()->setEditorIsVisible(false);
462 }
463 
getPluginInstance()464 PluginSpectralAnalyzer *UISpectralAnalyzer::getPluginInstance()
465 {
466     return static_cast<PluginSpectralAnalyzer *>(getPluginInstancePointer());
467 }
468 
469 // -----------------------------------------------------------------------
470 // DSP/Plugin callbacks
471 
472 /**
473   A parameter has changed on the plugin side.
474   This is called by the host to inform the UI about parameter changes.
475 */
parameterChanged(uint32_t index,float value)476 void UISpectralAnalyzer::parameterChanged(uint32_t index, float value)
477 {
478     switch (index) {
479     case kPidFftSize:
480         fFftSizeChooser->setValue(value);
481         break;
482     case kPidStepSize:
483         fStepSizeChooser->setValue(value);
484         break;
485     case kPidAttackTime:
486         fAttackTimeSlider->setValue(value);
487         break;
488     case kPidReleaseTime:
489         fReleaseTimeSlider->setValue(value);
490         break;
491     case kPidAlgorithm:
492         fAlgorithmChooser->setValue(value);
493         break;
494     }
495 }
496 
497 /**
498   A program has been loaded on the plugin side.
499   This is called by the host to inform the UI about program changes.
500 */
programLoaded(uint32_t index)501 void UISpectralAnalyzer::programLoaded(uint32_t index)
502 {
503     (void)index;
504     DISTRHO_SAFE_ASSERT(false);
505 }
506 
507 /**
508   Optional callback to inform the UI about a sample rate change on the plugin side.
509 */
sampleRateChanged(double newSampleRate)510 void UISpectralAnalyzer::sampleRateChanged(double newSampleRate)
511 {
512     (void)newSampleRate;
513 }
514 
515 // -----------------------------------------------------------------------
516 // Optional UI callbacks
517 
518 /**
519   Idle callback.
520   This function is called at regular intervals.
521 */
uiIdle()522 void UISpectralAnalyzer::uiIdle()
523 {
524     getPluginInstance()->setEditorIsVisible(isVisible());
525 
526     updateSpectrum();
527 
528     // under theme developer mode, recheck periodically for edits
529     if (fThemeEditChooser->value()) {
530         const std::string path = get_theme_file(fCurrentTheme);
531         bool update = false;
532 
533         struct stat st;
534         if (::stat(path.c_str(), &st) == 0) {
535 #if !defined(_WIN32)
536             update = fCurrentThemeMtime.tv_sec != st.st_mtim.tv_sec ||
537                 fCurrentThemeMtime.tv_nsec != st.st_mtim.tv_nsec;
538 #else
539             update = fCurrentThemeMtime != st.st_mtime;
540 #endif
541         }
542 
543         if (update)
544             loadTheme(fCurrentTheme.c_str());
545     }
546 }
547 
548 /**
549   Window reshape function, called when the parent window is resized.
550 */
uiReshape(uint width,uint height)551 void UISpectralAnalyzer::uiReshape(uint width, uint height)
552 {
553     fSpectrumView->setAbsolutePos(0, 0);
554     fSpectrumView->setSize(width, height);
555 
556     fMainToolBar->setAbsolutePos(0, 0);
557     fMainToolBar->setSize(fMainToolBar->getIdealWidth(), 40);
558 
559     FloatingWindow *floats[] = {fSetupWindow, fScaleWindow, fSelectWindow, fColorWindow};
560 
561     if (!fInitializedFloatingWindowPos) {
562         const int floatingPosX = 4;
563         const int floatingPosY = fMainToolBar->getAbsoluteY() + fMainToolBar->getHeight() + 4;
564         for (FloatingWindow *win : floats)
565             win->setAbsolutePos(floatingPosX, floatingPosY);
566         fInitializedFloatingWindowPos = true;
567     }
568 
569     for (FloatingWindow *win : floats) {
570         win->setMoveLimits(getAbsolutePos(), getSize());
571         // TODO(jpc) I'd rather move the window relative to nearest corner
572         win->repositionWithinLimits();
573     }
574 
575     fResizeHandle->setAbsolutePos(
576         width - fResizeHandle->getWidth(), height - fResizeHandle->getHeight());
577 }
578 
579 // -----------------------------------------------------------------------
580 
onNanoDisplay()581 void UISpectralAnalyzer::onNanoDisplay()
582 {
583 }
584 
onMouse(const MouseEvent & ev)585 bool UISpectralAnalyzer::onMouse(const MouseEvent &ev)
586 {
587     if (fMode == kModeScale && ev.press && ev.button == 1) {
588         fSelectionRectangle->setVisible(true);
589         fSelectionRectangle->setAbsolutePos(ev.pos);
590         fSelectionRectangle->setSize(0, 0);
591         fSelectionOrigin = ::Point(ev.pos.getX(), ev.pos.getY());
592         fScaleRectDragging = true;
593         return true;
594     }
595     else if (fMode == kModeScale && fScaleRectDragging && !ev.press && ev.button == 1) {
596         fSelectionRectangle->setVisible(false);
597         fScaleRectDragging = false;
598         int x1 = fSelectionRectangle->getAbsoluteX() - fSpectrumView->getAbsoluteX();
599         int x2 = x1 + (int)fSelectionRectangle->getWidth();
600         int y1 = fSelectionRectangle->getAbsoluteY() - fSpectrumView->getAbsoluteY();
601         int y2 = y1 + (int)fSelectionRectangle->getHeight();
602         if (x1 != x2 && y1 != y2) {
603             double key1 = fSpectrumView->keyOfX(x1);
604             double key2 = fSpectrumView->keyOfX(x2);
605             double db1 = fSpectrumView->dbMagOfY(y1);
606             double db2 = fSpectrumView->dbMagOfY(y2);
607             if (key1 > key2)
608                 std::swap(key1, key2);
609             if (db1 > db2)
610                 std::swap(db1, db2);
611             fSpectrumView->setKeyScale(key1, key2);
612             fSpectrumView->setDbScale(db1, db2);
613         }
614         return true;
615     }
616     else if (fMode == kModeScale && ev.press && ev.button == 3) {
617         fSpectrumView->setDefaultScales();
618         return true;
619     }
620     else if (fMode == kModeSelect && ev.press && ev.button == 1) {
621         fSelectPositionFixed = !fSelectPositionFixed;
622         if (!fSelectPositionFixed)
623             setNewSelectionPositionByMouse(ev.pos);
624         return true;
625     }
626     else if (fMode == kModeHide && ev.press && ev.button == 1) {
627         switchMode(kModeNormal);
628         return true;
629     }
630 
631     return false;
632 }
633 
onMotion(const MotionEvent & ev)634 bool UISpectralAnalyzer::onMotion(const MotionEvent &ev)
635 {
636     switch (fMode) {
637     case kModeScale:
638         if (fScaleRectDragging) {
639             int x1 = ev.pos.getX();
640             int y1 = ev.pos.getY();
641             int x2 = fSelectionOrigin.x;
642             int y2 = fSelectionOrigin.y;
643             if (x1 > x2)
644                 std::swap(x1, x2);
645             if (y1 > y2)
646                 std::swap(y1, y2);
647             fSelectionRectangle->setAbsolutePos(x1, y1);
648             fSelectionRectangle->setSize(x2 - x1, y2 - y1);
649         }
650         break;
651     case kModeSelect:
652         if (!fSelectPositionFixed)
653             setNewSelectionPositionByMouse(ev.pos);
654         break;
655     }
656 
657     return false;
658 }
659 
660 // -----------------------------------------------------------------------
661 
onToolBarItemClicked(int id)662 void UISpectralAnalyzer::onToolBarItemClicked(int id)
663 {
664     switch (id) {
665     case kToolBarIdSetup:
666         switchMode((fMode == kModeSetup) ? kModeNormal : kModeSetup);
667         break;
668     case kToolBarIdScale:
669         switchMode((fMode == kModeScale) ? kModeNormal : kModeScale);
670         break;
671     case kToolBarIdFreeze:
672         fSpectrumView->toggleFreeze();
673         fMainToolBar->setSelected(kToolBarIdFreeze, fSpectrumView->isFrozen());
674         break;
675     case kToolBarIdSelect:
676         switchMode((fMode == kModeSelect) ? kModeNormal : kModeSelect);
677         break;
678     case kToolBarIdHide:
679         switchMode(kModeHide);
680         break;
681     case kToolBarIdColor:
682         switchMode((fMode == kModeColor) ? kModeNormal : kModeColor);
683         break;
684     }
685 }
686 
687 // -----------------------------------------------------------------------
688 
switchMode(int mode)689 void UISpectralAnalyzer::switchMode(int mode)
690 {
691     int prevMode = fMode;
692     if (mode == prevMode)
693         return;
694 
695     fMode = mode;
696 
697     fSetupWindow->setAllVisible(false);
698     fScaleWindow->setAllVisible(false);
699     fSelectWindow->setAllVisible(false);
700     fColorWindow->setAllVisible(false);
701     fMainToolBar->setSelected(kToolBarIdSetup, false);
702     fMainToolBar->setSelected(kToolBarIdScale, false);
703     fMainToolBar->setSelected(kToolBarIdSelect, false);
704     fMainToolBar->setSelected(kToolBarIdColor, false);
705 
706     switch (prevMode) {
707     case kModeScale:
708         fScaleRectDragging = false;
709         break;
710     case kModeSelect:
711         fSpectrumView->clearReferenceLine();
712         break;
713     case kModeHide:
714         fMainToolBar->setVisible(true);
715         fResizeHandle->setVisible(true);
716         break;
717     }
718 
719     switch (mode) {
720     case kModeSetup:
721         fSetupWindow->setAllVisible(true);
722         fMainToolBar->setSelected(kToolBarIdSetup, fMode == kModeSetup);
723         break;
724     case kModeScale:
725         fScaleWindow->setAllVisible(true);
726         fMainToolBar->setSelected(kToolBarIdScale, fMode == kModeScale);
727         fScaleRectDragging = false;
728         break;
729     case kModeSelect:
730         fSelectWindow->setAllVisible(true);
731         fMainToolBar->setSelected(kToolBarIdSelect, fMode == kModeSelect);
732         fSelectPositionFixed = false;
733         fSelectLastCursorKey = 0.0;
734         fSelectLastCursorFreq = 0.0;
735         fSelectLastCursorMag = 0.0;
736         fSpectrumView->clearReferenceLine();
737         break;
738     case kModeHide:
739         fMainToolBar->setVisible(false);
740         fResizeHandle->setVisible(false);
741         break;
742     case kModeColor:
743         fColorWindow->setAllVisible(true);
744         fMainToolBar->setSelected(kToolBarIdColor, fMode == kModeColor);
745         reloadThemeList();
746         break;
747     }
748 }
749 
setNewSelectionPositionByMouse(DGL::Point<int> pos)750 void UISpectralAnalyzer::setNewSelectionPositionByMouse(DGL::Point<int> pos)
751 {
752     const double key = fSpectrumView->keyOfX(pos.getX() - fSpectrumView->getAbsoluteX());
753     const double mag = fSpectrumView->dbMagOfY(pos.getY() - fSpectrumView->getAbsoluteY());
754     const double freq = 440.0 * std::exp2((key - 69.0) * (1.0 / 12.0));
755     fSelectLastCursorKey = key;
756     fSelectLastCursorFreq = freq;
757     fSelectLastCursorMag = mag;
758     updateSelectModeDisplays();
759 }
760 
updateSpectrum()761 void UISpectralAnalyzer::updateSpectrum()
762 {
763     PluginSpectralAnalyzer *plugin = getPluginInstance();
764     DISTRHO_SAFE_ASSERT_RETURN(plugin, );
765 
766     std::unique_lock<SpinMutex> lock(plugin->fSendMutex);
767     fFrequencies = plugin->fSendFrequencies;
768     fMagnitudes = plugin->fSendMagnitudes;
769     fSize = plugin->fSendSize;
770     lock.unlock();
771 
772     fSpectrumView->setData(fFrequencies.data(), fMagnitudes.data(), fSize, kNumChannels);
773 
774     if (fMode == kModeSelect)
775         updateSelectModeDisplays();
776 }
777 
updateSelectModeDisplays()778 void UISpectralAnalyzer::updateSelectModeDisplays()
779 {
780     auto toHzString = [](double hz) -> std::string {
781         return format_string("%.2f", hz) + " Hz";
782     };
783     auto toDbString = [](double dB) -> std::string {
784         if (dB > kStftFloorMagnitudeInDB + kNegligibleDB)
785             return format_string("%.2f", dB) + " dB";
786         else
787             return "-\u221e dB";
788     };
789 
790     fSpectrumView->setReferenceLine(fSelectLastCursorKey, fSelectLastCursorMag);
791     fSelectLabelX->setText(toHzString(fSelectLastCursorFreq));
792     fSelectLabelY->setText(toDbString(fSelectLastCursorMag));
793 
794     for (unsigned c = 0; c < kNumChannels; ++c) {
795         double mag = fSpectrumView->evalMagnitudeOnDisplay(c, fSelectLastCursorFreq);
796         fSelectChannelY[c]->setText(toDbString(mag));
797 
798         SpectrumView::Peak pk = fSpectrumView->findNearbyPeakOnDisplay(c, fSelectLastCursorFreq);
799         fSelectNearPeakX[c]->setText(toHzString(pk.frequency));
800         fSelectNearPeakY[c]->setText(toDbString(pk.magnitude));
801     }
802 }
803 
loadTheme(const char * theme)804 void UISpectralAnalyzer::loadTheme(const char *theme)
805 {
806     ColorPalette &palette = *fPalette;
807     palette = ColorPalette::create_default();
808 
809     std::unique_ptr<CSimpleIniA> ini;
810     const std::string path = get_theme_file(theme);
811 
812     if (!path.empty()) {
813         ini = create_theme();
814         if (ini->LoadFile(path.c_str()) != SI_OK)
815             ini.reset();
816     }
817 
818     fCurrentThemeMtime = {};
819 
820     if (!ini)
821         fprintf(stderr, "Cannot load theme: %s\n", theme);
822     else {
823         palette.load(*ini, "color");
824 
825         struct stat st;
826         if (::stat(path.c_str(), &st) == 0) {
827 #if !defined(_WIN32)
828             fCurrentThemeMtime = st.st_mtim;
829 #else
830             fCurrentThemeMtime = st.st_mtime;
831 #endif
832         }
833     }
834 
835     ///
836     CSimpleIniA &uiConfig = *fUiConfig;
837     const char *themeInConfig = uiConfig.GetValue("ui", "theme");
838     if (!themeInConfig || std::strcmp(themeInConfig, theme) != 0) {
839         uiConfig.SetValue("ui", "theme", theme, "; Identifier of the theme which is active on program startup", true);
840         save_configuration("ui", uiConfig);
841     }
842 
843     ///
844     fCurrentTheme = theme;
845     repaint();
846 }
847 
reloadThemeList()848 void UISpectralAnalyzer::reloadThemeList()
849 {
850     fThemeChooser->ValueChangedCallback = {};
851     fThemeChooser->clearChoices();
852     {
853         int32_t index = -1;
854         std::vector<std::string> themes = list_themes();
855         for (uint32_t i = 0, n = themes.size(); i < n; ++i) {
856             fThemeChooser->addChoice(i, themes[i].c_str());
857             if (themes[i] == fCurrentTheme)
858                 index = i;
859         }
860         if (index != -1)
861             fThemeChooser->setValueIndex(index);
862     }
863     fThemeChooser->ValueChangedCallback = [this](int32_t index) {
864         loadTheme(fThemeChooser->textAtIndex(index).c_str());
865     };
866 }
867 
868 // -----------------------------------------------------------------------
869 
createUI()870 UI *DISTRHO::createUI()
871 {
872     return new UISpectralAnalyzer();
873 }
874