SaSpectrumView.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. /* SaSpectrumView.cpp - implementation of SaSpectrumView class.
  2. *
  3. * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
  4. *
  5. * Based partially on Eq plugin code,
  6. * Copyright (c) 2014-2017, David French <dave/dot/french3/at/googlemail/dot/com>
  7. *
  8. * This file is part of LMMS - https://lmms.io
  9. *
  10. * This program is free software; you can redistribute it and/or
  11. * modify it under the terms of the GNU General Public
  12. * License as published by the Free Software Foundation; either
  13. * version 2 of the License, or (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  18. * General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU General Public
  21. * License along with this program (see COPYING); if not, write to the
  22. * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  23. * Boston, MA 02110-1301 USA.
  24. *
  25. */
  26. #include "SaSpectrumView.h"
  27. #include <algorithm>
  28. #include <cmath>
  29. #include <QMouseEvent>
  30. #include <QMutexLocker>
  31. #include <QPainter>
  32. #include <QString>
  33. #include "GuiApplication.h"
  34. #include "MainWindow.h"
  35. #include "SaProcessor.h"
  36. #ifdef SA_DEBUG
  37. #include <chrono>
  38. #include <iostream>
  39. #endif
  40. SaSpectrumView::SaSpectrumView(SaControls *controls, SaProcessor *processor, QWidget *_parent) :
  41. QWidget(_parent),
  42. m_controls(controls),
  43. m_processor(processor),
  44. m_freezeRequest(false),
  45. m_frozen(false)
  46. {
  47. setMinimumSize(360, 170);
  48. setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
  49. connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate()));
  50. m_displayBufferL.resize(m_processor->binCount(), 0);
  51. m_displayBufferR.resize(m_processor->binCount(), 0);
  52. m_peakBufferL.resize(m_processor->binCount(), 0);
  53. m_peakBufferR.resize(m_processor->binCount(), 0);
  54. m_freqRangeIndex = m_controls->m_freqRangeModel.value();
  55. m_ampRangeIndex = m_controls->m_ampRangeModel.value();
  56. m_logFreqTics = makeLogFreqTics(m_processor->getFreqRangeMin(), m_processor->getFreqRangeMax());
  57. m_linearFreqTics = makeLinearFreqTics(m_processor->getFreqRangeMin(), m_processor->getFreqRangeMax());
  58. m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
  59. m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
  60. m_cursor = QPoint(0, 0);
  61. }
  62. // Compose and draw all the content; periodically called by Qt.
  63. // NOTE: Performance sensitive! If the drawing takes too long, it will drag
  64. // the FPS down for the entire program! Use SA_DEBUG to display timings.
  65. void SaSpectrumView::paintEvent(QPaintEvent *event)
  66. {
  67. #ifdef SA_DEBUG
  68. int total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
  69. #endif
  70. // 0) Constants and init
  71. QPainter painter(this);
  72. painter.setRenderHint(QPainter::Antialiasing, true);
  73. // drawing and path-making are split into multiple methods for clarity;
  74. // display boundaries are updated here and shared as member variables
  75. m_displayTop = 1;
  76. m_displayBottom = height() -20;
  77. m_displayLeft = 26;
  78. m_displayRight = width() -26;
  79. m_displayWidth = m_displayRight - m_displayLeft;
  80. // recompute range labels if needed
  81. if (m_freqRangeIndex != m_controls->m_freqRangeModel.value())
  82. {
  83. m_logFreqTics = makeLogFreqTics(m_processor->getFreqRangeMin(), m_processor->getFreqRangeMax());
  84. m_linearFreqTics = makeLinearFreqTics(m_processor->getFreqRangeMin(true), m_processor->getFreqRangeMax());
  85. m_freqRangeIndex = m_controls->m_freqRangeModel.value();
  86. }
  87. if (m_ampRangeIndex != m_controls->m_ampRangeModel.value())
  88. {
  89. m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
  90. m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(true), m_processor->getAmpRangeMax());
  91. m_ampRangeIndex = m_controls->m_ampRangeModel.value();
  92. }
  93. // generate freeze request or clear "frozen" status based on freeze button
  94. if (!m_frozen && m_controls->m_refFreezeModel.value())
  95. {
  96. m_freezeRequest = true;
  97. }
  98. else if (!m_controls->m_refFreezeModel.value())
  99. {
  100. m_frozen = false;
  101. }
  102. // 1) Background, grid and labels
  103. drawGrid(painter);
  104. // 2) Spectrum display
  105. drawSpectrum(painter);
  106. // 3) Overlays
  107. // draw cursor (if it is within bounds)
  108. drawCursor(painter);
  109. // always draw the display outline
  110. painter.setPen(QPen(m_controls->m_colorGrid, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  111. painter.drawRoundedRect(m_displayLeft, 1,
  112. m_displayWidth, m_displayBottom,
  113. 2.0, 2.0);
  114. #ifdef SA_DEBUG
  115. // display what FPS would be achieved if spectrum display ran in a loop
  116. total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time;
  117. painter.setPen(QPen(m_controls->m_colorLabels, 1,
  118. Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  119. painter.drawText(m_displayRight -100, 70, 100, 16, Qt::AlignLeft,
  120. QString(std::string("Max FPS: " + std::to_string(1000000000.0 / total_time)).c_str()));
  121. #endif
  122. }
  123. // Refresh data and draw the spectrum.
  124. void SaSpectrumView::drawSpectrum(QPainter &painter)
  125. {
  126. #ifdef SA_DEBUG
  127. int path_time = 0, draw_time = 0;
  128. #endif
  129. // draw the graph only if there is any input, averaging residue or peaks
  130. QMutexLocker lock(&m_processor->m_dataAccess);
  131. if (m_decaySum > 0 || notEmpty(m_processor->m_normSpectrumL) || notEmpty(m_processor->m_normSpectrumR))
  132. {
  133. lock.unlock();
  134. #ifdef SA_DEBUG
  135. path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
  136. #endif
  137. // update data buffers and reconstruct paths
  138. refreshPaths();
  139. #ifdef SA_DEBUG
  140. path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - path_time;
  141. #endif
  142. // draw stored paths
  143. #ifdef SA_DEBUG
  144. draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
  145. #endif
  146. // in case stereo is disabled, mono data are stored in left channel structures
  147. if (m_controls->m_stereoModel.value())
  148. {
  149. painter.fillPath(m_pathR, QBrush(m_controls->m_colorR));
  150. painter.fillPath(m_pathL, QBrush(m_controls->m_colorL));
  151. }
  152. else
  153. {
  154. painter.fillPath(m_pathL, QBrush(m_controls->m_colorMono));
  155. }
  156. // draw the peakBuffer only if peak hold or reference freeze is active
  157. if (m_controls->m_peakHoldModel.value() || m_controls->m_refFreezeModel.value())
  158. {
  159. if (m_controls->m_stereoModel.value())
  160. {
  161. painter.setPen(QPen(m_controls->m_colorR, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  162. painter.drawPath(m_pathPeakR);
  163. painter.setPen(QPen(m_controls->m_colorL, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  164. painter.drawPath(m_pathPeakL);
  165. }
  166. else
  167. {
  168. painter.setPen(QPen(m_controls->m_colorL, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  169. painter.drawPath(m_pathPeakL);
  170. }
  171. }
  172. #ifdef SA_DEBUG
  173. draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time;
  174. #endif
  175. }
  176. else
  177. {
  178. lock.unlock();
  179. }
  180. #ifdef SA_DEBUG
  181. // display measurement results
  182. painter.drawText(m_displayRight -100, 90, 100, 16, Qt::AlignLeft,
  183. QString(std::string("Path ms: " + std::to_string(path_time / 1000000.0)).c_str()));
  184. painter.drawText(m_displayRight -100, 110, 100, 16, Qt::AlignLeft,
  185. QString(std::string("Draw ms: " + std::to_string(draw_time / 1000000.0)).c_str()));
  186. #endif
  187. }
  188. // Read newest FFT results from SaProcessor, update local display buffers
  189. // and build QPainter paths.
  190. void SaSpectrumView::refreshPaths()
  191. {
  192. // Lock is required for the entire function, mainly to prevent block size
  193. // changes from causing reallocation of data structures mid-way.
  194. QMutexLocker lock(&m_processor->m_dataAccess);
  195. // check if bin count changed and reallocate display buffers accordingly
  196. if (m_processor->binCount() != m_displayBufferL.size())
  197. {
  198. m_displayBufferL.clear();
  199. m_displayBufferR.clear();
  200. m_peakBufferL.clear();
  201. m_peakBufferR.clear();
  202. m_displayBufferL.resize(m_processor->binCount(), 0);
  203. m_displayBufferR.resize(m_processor->binCount(), 0);
  204. m_peakBufferL.resize(m_processor->binCount(), 0);
  205. m_peakBufferR.resize(m_processor->binCount(), 0);
  206. }
  207. // update display buffers for left and right channel
  208. #ifdef SA_DEBUG
  209. int refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
  210. #endif
  211. m_decaySum = 0;
  212. updateBuffers(m_processor->m_normSpectrumL.data(), m_displayBufferL.data(), m_peakBufferL.data());
  213. updateBuffers(m_processor->m_normSpectrumR.data(), m_displayBufferR.data(), m_peakBufferR.data());
  214. #ifdef SA_DEBUG
  215. refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - refresh_time;
  216. #endif
  217. // if there was a freeze request, it was taken care of during the update
  218. if (m_controls->m_refFreezeModel.value() && m_freezeRequest)
  219. {
  220. m_freezeRequest = false;
  221. m_frozen = true;
  222. }
  223. #ifdef SA_DEBUG
  224. int make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
  225. #endif
  226. // Use updated display buffers to prepare new paths for QPainter.
  227. // This is the second slowest action (first is the subsequent drawing); use
  228. // the resolution parameter to balance display quality and performance.
  229. m_pathL = makePath(m_displayBufferL, 1.5);
  230. if (m_controls->m_stereoModel.value())
  231. {
  232. m_pathR = makePath(m_displayBufferR, 1.5);
  233. }
  234. if (m_controls->m_peakHoldModel.value() || m_controls->m_refFreezeModel.value())
  235. {
  236. m_pathPeakL = makePath(m_peakBufferL, 0.25);
  237. if (m_controls->m_stereoModel.value())
  238. {
  239. m_pathPeakR = makePath(m_peakBufferR, 0.25);
  240. }
  241. }
  242. #ifdef SA_DEBUG
  243. make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - make_time;
  244. #endif
  245. #ifdef SA_DEBUG
  246. // print measurement results
  247. std::cout << "Buffer update ms: " << std::to_string(refresh_time / 1000000.0) << ", ";
  248. std::cout << "Path-make ms: " << std::to_string(make_time / 1000000.0) << std::endl;
  249. #endif
  250. }
  251. // Update display buffers: add new data, update average and peaks / reference.
  252. // Output the sum of all displayed values -- draw only if it is non-zero.
  253. // NOTE: The calling function is responsible for acquiring SaProcessor data
  254. // access lock!
  255. void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float *peakBuffer)
  256. {
  257. for (int n = 0; n < m_processor->binCount(); n++)
  258. {
  259. // Update the exponential average if enabled, or simply copy the value.
  260. if (!m_controls->m_pauseModel.value())
  261. {
  262. if (m_controls->m_smoothModel.value())
  263. {
  264. displayBuffer[n] = spectrum[n] * m_smoothFactor + displayBuffer[n] * (1 - m_smoothFactor);
  265. }
  266. else
  267. {
  268. displayBuffer[n] = spectrum[n];
  269. }
  270. }
  271. // Update peak-hold and reference freeze data (using a shared curve).
  272. // Peak hold and freeze can be combined: decay only if not frozen.
  273. // Ref. freeze operates on the (possibly averaged) display buffer.
  274. if (m_controls->m_refFreezeModel.value() && m_freezeRequest)
  275. {
  276. peakBuffer[n] = displayBuffer[n];
  277. }
  278. else if (m_controls->m_peakHoldModel.value() && !m_controls->m_pauseModel.value())
  279. {
  280. if (spectrum[n] > peakBuffer[n])
  281. {
  282. peakBuffer[n] = spectrum[n];
  283. }
  284. else if (!m_controls->m_refFreezeModel.value())
  285. {
  286. peakBuffer[n] = peakBuffer[n] * m_peakDecayFactor;
  287. }
  288. }
  289. else if (!m_controls->m_refFreezeModel.value() && !m_controls->m_peakHoldModel.value())
  290. {
  291. peakBuffer[n] = 0;
  292. }
  293. // take note if there was actually anything to display
  294. m_decaySum += displayBuffer[n] + peakBuffer[n];
  295. }
  296. }
  297. // Use display buffer to build a path that can be drawn or filled by QPainter.
  298. // Resolution controls the performance / quality tradeoff; the value specifies
  299. // number of points in x axis per device pixel. Values over 1.0 still
  300. // contribute to quality and accuracy thanks to anti-aliasing.
  301. QPainterPath SaSpectrumView::makePath(std::vector<float> &displayBuffer, float resolution = 1.0)
  302. {
  303. // convert resolution to number of path points per logical pixel
  304. float pixel_limit = resolution * window()->devicePixelRatio();
  305. QPainterPath path;
  306. path.moveTo(m_displayLeft, m_displayBottom);
  307. // Translate frequency bins to path points.
  308. // Display is flipped: y values grow towards zero, initial max is bottom.
  309. // Bins falling to interval [x_start, x_next) contribute to a single point.
  310. float max = m_displayBottom;
  311. float x_start = -1; // lower bound of currently constructed point
  312. for (unsigned int n = 0; n < m_processor->binCount(); n++)
  313. {
  314. float x = freqToXPixel(binToFreq(n), m_displayWidth);
  315. float x_next = freqToXPixel(binToFreq(n + 1), m_displayWidth);
  316. float y = ampToYPixel(displayBuffer[n], m_displayBottom);
  317. // consider making a point only if x falls within display bounds
  318. if (0 < x && x < m_displayWidth)
  319. {
  320. if (x_start == -1)
  321. {
  322. x_start = x;
  323. // the first displayed bin is stretched to the left edge to prevent
  324. // creating a misleading slope leading to zero (at log. scale)
  325. path.lineTo(m_displayLeft, y + m_displayTop);
  326. }
  327. // Opt.: QPainter is very slow -- draw at most [pixel_limit] points
  328. // per logical pixel. As opposed to limiting the bin count, this
  329. // allows high resolution display if user resizes the analyzer.
  330. // Look at bins that share the pixel and use the highest value:
  331. max = y < max ? y : max;
  332. // And make the final point in the middle of current interval.
  333. if ((int)(x * pixel_limit) != (int)(x_next * pixel_limit))
  334. {
  335. x = (x + x_start) / 2;
  336. path.lineTo(x + m_displayLeft, max + m_displayTop);
  337. max = m_displayBottom;
  338. x_start = x_next;
  339. }
  340. }
  341. else
  342. {
  343. // stop processing after a bin falls outside right edge
  344. // and align it to the edge to prevent a gap
  345. if (n > 0 && x > 0)
  346. {
  347. path.lineTo(m_displayRight, y + m_displayTop);
  348. break;
  349. }
  350. }
  351. }
  352. path.lineTo(m_displayRight, m_displayBottom);
  353. path.closeSubpath();
  354. return path;
  355. }
  356. // Draw background, grid and associated frequency and amplitude labels.
  357. void SaSpectrumView::drawGrid(QPainter &painter)
  358. {
  359. std::vector<std::pair<int, std::string>> *freqTics = NULL;
  360. std::vector<std::pair<float, std::string>> *ampTics = NULL;
  361. float pos = 0;
  362. float label_width = 24;
  363. float label_height = 15;
  364. float margin = 5;
  365. // always draw the background
  366. painter.fillRect(m_displayLeft, m_displayTop,
  367. m_displayWidth, m_displayBottom,
  368. m_controls->m_colorBG);
  369. // select logarithmic or linear frequency grid and draw it
  370. if (m_controls->m_logXModel.value())
  371. {
  372. freqTics = &m_logFreqTics;
  373. }
  374. else
  375. {
  376. freqTics = &m_linearFreqTics;
  377. }
  378. // draw frequency grid (line.first is display position)
  379. painter.setPen(QPen(m_controls->m_colorGrid, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  380. for (auto &line: *freqTics)
  381. {
  382. painter.drawLine(m_displayLeft + freqToXPixel(line.first, m_displayWidth),
  383. 2,
  384. m_displayLeft + freqToXPixel(line.first, m_displayWidth),
  385. m_displayBottom);
  386. }
  387. // print frequency labels (line.second is label)
  388. painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  389. for (auto & line: *freqTics)
  390. {
  391. pos = m_displayLeft + freqToXPixel(line.first, m_displayWidth);
  392. // align first and last label to the edge if needed, otherwise center them
  393. if (line == freqTics->front() && pos - label_width / 2 < m_displayLeft)
  394. {
  395. painter.drawText(m_displayLeft, m_displayBottom + margin,
  396. label_width, label_height, Qt::AlignLeft | Qt::TextDontClip,
  397. QString(line.second.c_str()));
  398. }
  399. else if (line == freqTics->back() && pos + label_width / 2 > m_displayRight)
  400. {
  401. painter.drawText(m_displayRight - label_width, m_displayBottom + margin,
  402. label_width, label_height, Qt::AlignRight | Qt::TextDontClip,
  403. QString(line.second.c_str()));
  404. }
  405. else
  406. {
  407. painter.drawText(pos - label_width / 2, m_displayBottom + margin,
  408. label_width, label_height, Qt::AlignHCenter | Qt::TextDontClip,
  409. QString(line.second.c_str()));
  410. }
  411. }
  412. margin = 2;
  413. // select logarithmic or linear amplitude grid and draw it
  414. if (m_controls->m_logYModel.value())
  415. {
  416. ampTics = &m_logAmpTics;
  417. }
  418. else
  419. {
  420. ampTics = &m_linearAmpTics;
  421. }
  422. // draw amplitude grid
  423. painter.setPen(QPen(m_controls->m_colorGrid, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  424. for (auto & line: *ampTics)
  425. {
  426. painter.drawLine(m_displayLeft + 1,
  427. ampToYPixel(line.first, m_displayBottom),
  428. m_displayRight - 1,
  429. ampToYPixel(line.first, m_displayBottom));
  430. }
  431. // print amplitude labels
  432. painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  433. bool stereo = m_controls->m_stereoModel.value();
  434. for (auto & line: *ampTics)
  435. {
  436. pos = ampToYPixel(line.first, m_displayBottom);
  437. // align first and last labels to edge if needed, otherwise center them
  438. if (line == ampTics->back() && pos < 8)
  439. {
  440. if (stereo)
  441. {
  442. painter.setPen(QPen(m_controls->m_colorL.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  443. }
  444. painter.drawText(m_displayLeft - label_width - margin, m_displayTop - 2,
  445. label_width, label_height, Qt::AlignRight | Qt::AlignTop | Qt::TextDontClip,
  446. QString(line.second.c_str()));
  447. if (stereo)
  448. {
  449. painter.setPen(QPen(m_controls->m_colorR.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  450. }
  451. painter.drawText(m_displayRight + margin, m_displayTop - 2,
  452. label_width, label_height, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip,
  453. QString(line.second.c_str()));
  454. }
  455. else if (line == ampTics->front() && pos > m_displayBottom - label_height)
  456. {
  457. if (stereo)
  458. {
  459. painter.setPen(QPen(m_controls->m_colorL.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  460. }
  461. painter.drawText(m_displayLeft - label_width - margin, m_displayBottom - label_height + 2,
  462. label_width, label_height, Qt::AlignRight | Qt::AlignBottom | Qt::TextDontClip,
  463. QString(line.second.c_str()));
  464. if (stereo)
  465. {
  466. painter.setPen(QPen(m_controls->m_colorR.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  467. }
  468. painter.drawText(m_displayRight + margin, m_displayBottom - label_height + 2,
  469. label_width, label_height, Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip,
  470. QString(line.second.c_str()));
  471. }
  472. else
  473. {
  474. if (stereo)
  475. {
  476. painter.setPen(QPen(m_controls->m_colorL.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  477. }
  478. painter.drawText(m_displayLeft - label_width - margin, pos - label_height / 2,
  479. label_width, label_height, Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip,
  480. QString(line.second.c_str()));
  481. if (stereo)
  482. {
  483. painter.setPen(QPen(m_controls->m_colorR.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  484. }
  485. painter.drawText(m_displayRight + margin, pos - label_height / 2,
  486. label_width, label_height, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip,
  487. QString(line.second.c_str()));
  488. }
  489. }
  490. }
  491. // Draw cursor and its coordinates if it is within display bounds.
  492. void SaSpectrumView::drawCursor(QPainter &painter)
  493. {
  494. if( m_cursor.x() >= m_displayLeft
  495. && m_cursor.x() <= m_displayRight
  496. && m_cursor.y() >= m_displayTop
  497. && m_cursor.y() <= m_displayBottom)
  498. {
  499. // cursor lines
  500. painter.setPen(QPen(m_controls->m_colorGrid.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  501. painter.drawLine(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom);
  502. painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y());
  503. // coordinates
  504. painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  505. painter.drawText(m_displayRight -60, 5, 100, 16, Qt::AlignLeft, "Cursor");
  506. QString tmps;
  507. // frequency
  508. int xFreq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth);
  509. tmps = QString(std::string(std::to_string(xFreq) + " Hz").c_str());
  510. painter.drawText(m_displayRight -60, 18, 100, 16, Qt::AlignLeft, tmps);
  511. // amplitude
  512. float yAmp = m_processor->yPixelToAmp(m_cursor.y(), m_displayBottom);
  513. if (m_controls->m_logYModel.value())
  514. {
  515. tmps = QString(std::string(std::to_string(yAmp).substr(0, 5) + " dB").c_str());
  516. }
  517. else
  518. {
  519. // add 0.0005 to get proper rounding to 3 decimal places
  520. tmps = QString(std::string(std::to_string(0.0005f + yAmp)).substr(0, 5).c_str());
  521. }
  522. painter.drawText(m_displayRight -60, 30, 100, 16, Qt::AlignLeft, tmps);
  523. }
  524. }
  525. // Wrappers for most used SaProcessor helpers (to make local code more compact).
  526. float SaSpectrumView::binToFreq(unsigned int bin_index)
  527. {
  528. return m_processor->binToFreq(bin_index);
  529. }
  530. float SaSpectrumView::freqToXPixel(float frequency, unsigned int width)
  531. {
  532. return m_processor->freqToXPixel(frequency, width);
  533. }
  534. float SaSpectrumView::ampToYPixel(float amplitude, unsigned int height)
  535. {
  536. return m_processor->ampToYPixel(amplitude, height);
  537. }
  538. // Generate labels suitable for logarithmic frequency scale.
  539. // Low / high limits are in Hz. Lowest possible label is 10 Hz.
  540. std::vector<std::pair<int, std::string>> SaSpectrumView::makeLogFreqTics(int low, int high)
  541. {
  542. std::vector<std::pair<int, std::string>> result;
  543. int i, j;
  544. int a[] = {10, 20, 50}; // sparse series multipliers
  545. int b[] = {14, 30, 70}; // additional (denser) series
  546. // generate main steps (powers of 10); use the series to specify smaller steps
  547. for (i = 1; i <= high; i *= 10)
  548. {
  549. for (j = 0; j < 3; j++)
  550. {
  551. // insert a label from sparse series if it falls within bounds
  552. if (i * a[j] >= low && i * a[j] <= high)
  553. {
  554. if (i * a[j] < 1000)
  555. {
  556. result.emplace_back(i * a[j], std::to_string(i * a[j]));
  557. }
  558. else
  559. {
  560. result.emplace_back(i * a[j], std::to_string(i * a[j] / 1000) + "k");
  561. }
  562. }
  563. // also insert denser series if high and low values are close
  564. if ((log10(high) - log10(low) < 2) && (i * b[j] >= low && i * b[j] <= high))
  565. {
  566. if (i * b[j] < 1500)
  567. {
  568. result.emplace_back(i * b[j], std::to_string(i * b[j]));
  569. }
  570. else
  571. {
  572. result.emplace_back(i * b[j], std::to_string(i * b[j] / 1000) + "k");
  573. }
  574. }
  575. }
  576. }
  577. return result;
  578. }
  579. // Generate labels suitable for linear frequency scale.
  580. // Low / high limits are in Hz.
  581. std::vector<std::pair<int, std::string>> SaSpectrumView::makeLinearFreqTics(int low, int high)
  582. {
  583. std::vector<std::pair<int, std::string>> result;
  584. int i, increment;
  585. // select a suitable increment based on zoom level
  586. if (high - low < 500) {increment = 50;}
  587. else if (high - low < 1000) {increment = 100;}
  588. else if (high - low < 5000) {increment = 1000;}
  589. else {increment = 2000;}
  590. // generate steps based on increment, starting at 0
  591. for (i = 0; i <= high; i += increment)
  592. {
  593. if (i >= low)
  594. {
  595. if (i < 1000)
  596. {
  597. result.emplace_back(i, std::to_string(i));
  598. }
  599. else
  600. {
  601. result.emplace_back(i, std::to_string(i/1000) + "k");
  602. }
  603. }
  604. }
  605. return result;
  606. }
  607. // Generate labels suitable for logarithmic (dB) amplitude scale.
  608. // Low / high limits are in dB; 0 dB amplitude = 1.0 linear.
  609. // Treating results as power ratio, i.e., 3 dB should be about twice as loud.
  610. std::vector<std::pair<float, std::string>> SaSpectrumView::makeLogAmpTics(int low, int high)
  611. {
  612. std::vector<std::pair<float, std::string>> result;
  613. float i;
  614. double increment;
  615. // Base zoom level on selected range and how close is the current height
  616. // to the sizeHint() (denser scale for bigger window).
  617. if ((high - low) < 20 * ((float)height() / sizeHint().height()))
  618. {
  619. increment = pow(10, 0.3); // 3 dB steps when really zoomed in
  620. }
  621. else if (high - low < 45 * ((float)height() / sizeHint().height()))
  622. {
  623. increment = pow(10, 0.6); // 6 dB steps when sufficiently zoomed in
  624. }
  625. else
  626. {
  627. increment = 10; // 10 dB steps otherwise
  628. }
  629. // Generate n dB increments, start checking at -90 dB. Limits are tweaked
  630. // just a little bit to make sure float comparisons do not miss edges.
  631. for (i = 0.000000001; 10 * log10(i) <= (high + 0.001); i *= increment)
  632. {
  633. if (10 * log10(i) >= (low - 0.001))
  634. {
  635. result.emplace_back(i, std::to_string((int)std::round(10 * log10(i))));
  636. }
  637. }
  638. return result;
  639. }
  640. // Generate labels suitable for linear amplitude scale.
  641. // Low / high limits are in dB; 0 dB amplitude = 1.0 linear.
  642. // Smallest possible label is 0.001, largest is 999. This includes the majority
  643. // of useful labels; going lower or higher would require increasing margin size
  644. // so that the text can fit. That would be a waste of space -- the linear scale
  645. // would only make the experience worse for the main, logarithmic (dB) scale.
  646. std::vector<std::pair<float, std::string>> SaSpectrumView::makeLinearAmpTics(int low, int high)
  647. {
  648. std::vector<std::pair<float, std::string>> result;
  649. double i, nearest;
  650. // make about 5 labels when window is small, 10 if it is big
  651. float split = (float)height() / sizeHint().height() >= 1.5 ? 10.0 : 5.0;
  652. // convert limits to linear scale
  653. float lin_low = pow(10, low / 10.0);
  654. float lin_high = pow(10, high / 10.0);
  655. // Linear scale will vary widely, so instead of trying to craft extra nice
  656. // multiples, just generate a few evenly spaced increments across the range,
  657. // paying attention only to the decimal places to keep labels short.
  658. // Limits are shifted a bit so that float comparisons do not miss edges.
  659. for (i = 0; i <= (lin_high + 0.0001); i += (lin_high - lin_low) / split)
  660. {
  661. if (i >= (lin_low - 0.0001))
  662. {
  663. if (i >= 9.99 && i < 99.9)
  664. {
  665. nearest = std::round(i);
  666. result.emplace_back(nearest, std::to_string(nearest).substr(0, 2));
  667. }
  668. else if (i >= 0.099)
  669. { // also covers numbers above 100
  670. nearest = std::round(i * 10) / 10;
  671. result.emplace_back(nearest, std::to_string(nearest).substr(0, 3));
  672. }
  673. else if (i >= 0.0099)
  674. {
  675. nearest = std::round(i * 1000) / 1000;
  676. result.emplace_back(nearest, std::to_string(nearest).substr(0, 4));
  677. }
  678. else if (i >= 0.00099)
  679. {
  680. nearest = std::round(i * 10000) / 10000;
  681. result.emplace_back(nearest, std::to_string(nearest).substr(1, 4));
  682. }
  683. else if (i > -0.01 && i < 0.01)
  684. {
  685. result.emplace_back(i, "0"); // an exception, zero is short..
  686. }
  687. }
  688. }
  689. return result;
  690. }
  691. // Periodic update is called by LMMS.
  692. void SaSpectrumView::periodicUpdate()
  693. {
  694. // check if the widget is visible; if it is not, processing can be paused
  695. m_processor->setSpectrumActive(isVisible());
  696. // tell Qt it is time for repaint
  697. update();
  698. }
  699. // Handle mouse input: set new cursor position.
  700. void SaSpectrumView::mouseMoveEvent(QMouseEvent *event)
  701. {
  702. m_cursor = event->pos();
  703. }
  704. void SaSpectrumView::mousePressEvent(QMouseEvent *event)
  705. {
  706. m_cursor = event->pos();
  707. }
  708. // Handle resize event: rebuild grid and labels
  709. void SaSpectrumView::resizeEvent(QResizeEvent *event)
  710. {
  711. // frequency does not change density with size
  712. // amplitude does: rebuild labels
  713. m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
  714. m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
  715. }