SaSpectrumView.cpp 29 KB

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