SaWaterfallView.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. /* SaWaterfallViewView.cpp - implementation of SaWaterfallViewView class.
  2. *
  3. * Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
  4. *
  5. * This file is part of LMMS - https://lmms.io
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 2 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public
  17. * License along with this program (see COPYING); if not, write to the
  18. * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  19. * Boston, MA 02110-1301 USA.
  20. *
  21. */
  22. #include "SaWaterfallView.h"
  23. #include <algorithm>
  24. #ifdef SA_DEBUG
  25. #include <chrono>
  26. #endif
  27. #include <cmath>
  28. #include <QImage>
  29. #include <QMouseEvent>
  30. #include <QMutexLocker>
  31. #include <QPainter>
  32. #include <QSplitter>
  33. #include <QString>
  34. #include "EffectControlDialog.h"
  35. #include "GuiApplication.h"
  36. #include "MainWindow.h"
  37. #include "SaProcessor.h"
  38. SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, QWidget *_parent) :
  39. QWidget(_parent),
  40. m_controls(controls),
  41. m_processor(processor)
  42. {
  43. m_controlDialog = (EffectControlDialog*) _parent;
  44. setMinimumSize(300, 150);
  45. setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  46. connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate()));
  47. m_displayTop = 1;
  48. m_displayBottom = height() -2;
  49. m_displayLeft = 26;
  50. m_displayRight = width() -26;
  51. m_displayWidth = m_displayRight - m_displayLeft;
  52. m_displayHeight = m_displayBottom - m_displayTop;
  53. m_timeTics = makeTimeTics();
  54. m_oldSecondsPerLine = 0;
  55. m_oldHeight = 0;
  56. m_cursor = QPointF(0, 0);
  57. #ifdef SA_DEBUG
  58. m_execution_avg = 0;
  59. #endif
  60. }
  61. // Compose and draw all the content; called by Qt.
  62. // Not as performance sensitive as SaSpectrumView, most of the processing is
  63. // done directly in SaProcessor.
  64. void SaWaterfallView::paintEvent(QPaintEvent *event)
  65. {
  66. #ifdef SA_DEBUG
  67. unsigned int draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
  68. #endif
  69. // update boundary
  70. m_displayBottom = height() -2;
  71. m_displayRight = width() -26;
  72. m_displayWidth = m_displayRight - m_displayLeft;
  73. m_displayHeight = m_displayBottom - m_displayTop;
  74. float label_width = 20;
  75. float label_height = 16;
  76. float margin = 2;
  77. QPainter painter(this);
  78. painter.setRenderHint(QPainter::Antialiasing, true);
  79. // check if time labels need to be rebuilt
  80. if (secondsPerLine() != m_oldSecondsPerLine || m_processor->waterfallHeight() != m_oldHeight)
  81. {
  82. m_timeTics = makeTimeTics();
  83. m_oldSecondsPerLine = secondsPerLine();
  84. m_oldHeight = m_processor->waterfallHeight();
  85. }
  86. // print time labels
  87. float pos = 0;
  88. painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  89. for (auto & line: m_timeTics)
  90. {
  91. pos = timeToYPixel(line.first, m_displayHeight);
  92. // align first and last label to the edge if needed, otherwise center them
  93. if (line == m_timeTics.front() && pos < label_height / 2)
  94. {
  95. painter.drawText(m_displayLeft - label_width - margin, m_displayTop - 1,
  96. label_width, label_height, Qt::AlignRight | Qt::AlignTop | Qt::TextDontClip,
  97. QString(line.second.c_str()));
  98. painter.drawText(m_displayRight + margin, m_displayTop - 1,
  99. label_width, label_height, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip,
  100. QString(line.second.c_str()));
  101. }
  102. else if (line == m_timeTics.back() && pos > m_displayBottom - label_height + 2)
  103. {
  104. painter.drawText(m_displayLeft - label_width - margin, m_displayBottom - label_height,
  105. label_width, label_height, Qt::AlignRight | Qt::AlignBottom | Qt::TextDontClip,
  106. QString(line.second.c_str()));
  107. painter.drawText(m_displayRight + margin, m_displayBottom - label_height + 2,
  108. label_width, label_height, Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip,
  109. QString(line.second.c_str()));
  110. }
  111. else
  112. {
  113. painter.drawText(m_displayLeft - label_width - margin, pos - label_height / 2,
  114. label_width, label_height, Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip,
  115. QString(line.second.c_str()));
  116. painter.drawText(m_displayRight + margin, pos - label_height / 2,
  117. label_width, label_height, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip,
  118. QString(line.second.c_str()));
  119. }
  120. }
  121. // draw the spectrogram precomputed in SaProcessor
  122. if (m_processor->waterfallNotEmpty())
  123. {
  124. QMutexLocker lock(&m_processor->m_reallocationAccess);
  125. QImage temp = QImage(m_processor->getHistory(), // raw pixel data to display
  126. m_processor->waterfallWidth(), // width = number of frequency bins
  127. m_processor->waterfallHeight(), // height = number of history lines
  128. QImage::Format_RGB32);
  129. lock.unlock();
  130. temp.setDevicePixelRatio(devicePixelRatio()); // display at native resolution
  131. painter.drawImage(m_displayLeft, m_displayTop,
  132. temp.scaled(m_displayWidth * devicePixelRatio(),
  133. m_displayHeight * devicePixelRatio(),
  134. Qt::IgnoreAspectRatio,
  135. Qt::SmoothTransformation));
  136. m_processor->flipRequest();
  137. }
  138. else
  139. {
  140. painter.fillRect(m_displayLeft, m_displayTop, m_displayWidth, m_displayHeight, QColor(0,0,0));
  141. }
  142. // draw cursor (if it is within bounds)
  143. drawCursor(painter);
  144. // always draw the outline
  145. painter.setPen(QPen(m_controls->m_colorGrid, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  146. painter.drawRoundedRect(m_displayLeft, m_displayTop, m_displayWidth, m_displayHeight, 2.0, 2.0);
  147. #ifdef SA_DEBUG
  148. draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time;
  149. m_execution_avg = 0.95 * m_execution_avg + 0.05 * draw_time / 1000000.0;
  150. painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  151. painter.drawText(m_displayRight -150, 10, 100, 16, Qt::AlignLeft,
  152. QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).c_str()).append(" ms"));
  153. #endif
  154. }
  155. // Helper functions for time conversion
  156. float SaWaterfallView::samplesPerLine()
  157. {
  158. return (float)m_processor->inBlockSize() / m_controls->m_windowOverlapModel.value();
  159. }
  160. float SaWaterfallView::secondsPerLine()
  161. {
  162. return samplesPerLine() / m_processor->getSampleRate();
  163. }
  164. // Convert time value to Y coordinate for display of given height.
  165. float SaWaterfallView::timeToYPixel(float time, int height)
  166. {
  167. float pixels_per_line = (float)height / m_processor->waterfallHeight();
  168. return pixels_per_line * time / secondsPerLine();
  169. }
  170. // Convert Y coordinate on display of given height back to time value.
  171. float SaWaterfallView::yPixelToTime(float position, int height)
  172. {
  173. if (height == 0) {height = 1;}
  174. float pixels_per_line = (float)height / m_processor->waterfallHeight();
  175. return (position / pixels_per_line) * secondsPerLine();
  176. }
  177. // Generate labels for linear time scale.
  178. std::vector<std::pair<float, std::string>> SaWaterfallView::makeTimeTics()
  179. {
  180. std::vector<std::pair<float, std::string>> result;
  181. float i;
  182. // get time value of the last line
  183. float limit = yPixelToTime(m_displayBottom, m_displayHeight);
  184. // set increment to about 30 pixels (but min. 0.1 s)
  185. float increment = std::round(10 * limit / (m_displayHeight / 30)) / 10;
  186. if (increment < 0.1) {increment = 0.1;}
  187. // NOTE: labels positions are rounded to match the (rounded) label value
  188. for (i = 0; i <= limit; i += increment)
  189. {
  190. if (i > 99)
  191. {
  192. result.emplace_back(std::round(i), std::to_string(std::round(i)).substr(0, 3));
  193. }
  194. else if (i < 10)
  195. {
  196. result.emplace_back(std::round(i * 10) / 10, std::to_string(std::round(i * 10) / 10).substr(0, 3));
  197. }
  198. else
  199. {
  200. result.emplace_back(std::round(i), std::to_string(std::round(i)).substr(0, 2));
  201. }
  202. }
  203. return result;
  204. }
  205. // Periodically trigger repaint and check if the widget is visible.
  206. // If it is not, stop drawing and inform the processor.
  207. void SaWaterfallView::periodicUpdate()
  208. {
  209. m_processor->setWaterfallActive(isVisible());
  210. if (isVisible()) {update();}
  211. }
  212. // Adjust window size and widget visibility when waterfall is enabled or disabbled.
  213. void SaWaterfallView::updateVisibility()
  214. {
  215. // get container of the control dialog to be resized if needed
  216. QWidget *subWindow = m_controlDialog->parentWidget();
  217. if (m_controls->m_waterfallModel.value())
  218. {
  219. // clear old data before showing the waterfall
  220. m_processor->clearHistory();
  221. setVisible(true);
  222. // increase window size if it is too small
  223. if (subWindow->size().height() < m_controlDialog->sizeHint().height())
  224. {
  225. subWindow->resize(subWindow->size().width(), m_controlDialog->sizeHint().height());
  226. }
  227. }
  228. else
  229. {
  230. setVisible(false);
  231. // decrease window size only if it does not violate sizeHint
  232. subWindow->resize(subWindow->size().width(), m_controlDialog->sizeHint().height());
  233. }
  234. }
  235. // Draw cursor and its coordinates if it is within display bounds.
  236. void SaWaterfallView::drawCursor(QPainter &painter)
  237. {
  238. if ( m_cursor.x() >= m_displayLeft
  239. && m_cursor.x() <= m_displayRight
  240. && m_cursor.y() >= m_displayTop
  241. && m_cursor.y() <= m_displayBottom)
  242. {
  243. // cursor lines
  244. painter.setPen(QPen(m_controls->m_colorGrid.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  245. painter.drawLine(QPointF(m_cursor.x(), m_displayTop), QPointF(m_cursor.x(), m_displayBottom));
  246. painter.drawLine(QPointF(m_displayLeft, m_cursor.y()), QPointF(m_displayRight, m_cursor.y()));
  247. // coordinates: background box
  248. QFontMetrics fontMetrics = painter.fontMetrics();
  249. unsigned int const box_left = 5;
  250. unsigned int const box_top = 5;
  251. unsigned int const box_margin = 3;
  252. unsigned int const box_height = 2*(fontMetrics.size(Qt::TextSingleLine, "0 Hz").height() + box_margin);
  253. unsigned int const box_width = fontMetrics.size(Qt::TextSingleLine, "20000 Hz ").width() + 2*box_margin;
  254. painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  255. painter.fillRect(m_displayLeft + box_left, m_displayTop + box_top,
  256. box_width, box_height, QColor(0, 0, 0, 64));
  257. // coordinates: text
  258. painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  259. QString tmps;
  260. // frequency
  261. int freq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth);
  262. tmps = QString("%1 Hz").arg(freq);
  263. painter.drawText(m_displayLeft + box_left + box_margin,
  264. m_displayTop + box_top + box_margin,
  265. box_width, box_height / 2, Qt::AlignLeft, tmps);
  266. // time
  267. float time = yPixelToTime(m_cursor.y(), m_displayBottom);
  268. tmps = QString(std::to_string(time).substr(0, 5).c_str()).append(" s");
  269. painter.drawText(m_displayLeft + box_left + box_margin,
  270. m_displayTop + box_top + box_height / 2,
  271. box_width, box_height / 2, Qt::AlignLeft, tmps);
  272. }
  273. }
  274. // Handle mouse input: set new cursor position.
  275. // For some reason (a bug?), localPos() only returns integers. As a workaround
  276. // the fractional part is taken from windowPos() (which works correctly).
  277. void SaWaterfallView::mouseMoveEvent(QMouseEvent *event)
  278. {
  279. m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
  280. event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
  281. }
  282. void SaWaterfallView::mousePressEvent(QMouseEvent *event)
  283. {
  284. m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
  285. event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
  286. }
  287. // Handle resize event: rebuild time labels
  288. void SaWaterfallView::resizeEvent(QResizeEvent *event)
  289. {
  290. m_timeTics = makeTimeTics();
  291. }