SaWaterfallView.cpp 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  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. #include <cmath>
  25. #include <QImage>
  26. #include <QMutexLocker>
  27. #include <QPainter>
  28. #include <QSplitter>
  29. #include <QString>
  30. #include "EffectControlDialog.h"
  31. #include "GuiApplication.h"
  32. #include "MainWindow.h"
  33. #include "SaProcessor.h"
  34. SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, QWidget *_parent) :
  35. QWidget(_parent),
  36. m_controls(controls),
  37. m_processor(processor)
  38. {
  39. m_controlDialog = (EffectControlDialog*) _parent;
  40. setMinimumSize(300, 150);
  41. setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
  42. connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate()));
  43. m_timeTics = makeTimeTics();
  44. m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate();
  45. }
  46. // Compose and draw all the content; called by Qt.
  47. // Not as performance sensitive as SaSpectrumView, most of the processing is
  48. // done directly in SaProcessor.
  49. void SaWaterfallView::paintEvent(QPaintEvent *event)
  50. {
  51. #ifdef SA_DEBUG
  52. int start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
  53. #endif
  54. // all drawing done here, local variables are sufficient for the boundary
  55. const int displayTop = 1;
  56. const int displayBottom = height() -2;
  57. const int displayLeft = 26;
  58. const int displayRight = width() -26;
  59. const int displayWidth = displayRight - displayLeft;
  60. float label_width = 20;
  61. float label_height = 16;
  62. float margin = 2;
  63. QPainter painter(this);
  64. painter.setRenderHint(QPainter::Antialiasing, true);
  65. // check if time labels need to be rebuilt
  66. if ((float)m_processor->m_inBlockSize / m_processor->getSampleRate() != m_oldTimePerLine)
  67. {
  68. m_timeTics = makeTimeTics();
  69. m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate();
  70. }
  71. // print time labels
  72. float pos = 0;
  73. painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  74. for (auto & line: m_timeTics)
  75. {
  76. pos = timeToYPixel(line.first, displayBottom);
  77. // align first and last label to the edge if needed, otherwise center them
  78. if (line == m_timeTics.front() && pos < label_height / 2)
  79. {
  80. painter.drawText(displayLeft - label_width - margin, displayTop - 1,
  81. label_width, label_height, Qt::AlignRight | Qt::AlignTop | Qt::TextDontClip,
  82. QString(line.second.c_str()));
  83. painter.drawText(displayRight + margin, displayTop - 1,
  84. label_width, label_height, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip,
  85. QString(line.second.c_str()));
  86. }
  87. else if (line == m_timeTics.back() && pos > displayBottom - label_height + 2)
  88. {
  89. painter.drawText(displayLeft - label_width - margin, displayBottom - label_height,
  90. label_width, label_height, Qt::AlignRight | Qt::AlignBottom | Qt::TextDontClip,
  91. QString(line.second.c_str()));
  92. painter.drawText(displayRight + margin, displayBottom - label_height + 2,
  93. label_width, label_height, Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip,
  94. QString(line.second.c_str()));
  95. }
  96. else
  97. {
  98. painter.drawText(displayLeft - label_width - margin, pos - label_height / 2,
  99. label_width, label_height, Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip,
  100. QString(line.second.c_str()));
  101. painter.drawText(displayRight + margin, pos - label_height / 2,
  102. label_width, label_height, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip,
  103. QString(line.second.c_str()));
  104. }
  105. }
  106. // draw the spectrogram precomputed in SaProcessor
  107. if (m_processor->m_waterfallNotEmpty)
  108. {
  109. QMutexLocker lock(&m_processor->m_dataAccess);
  110. painter.drawImage(displayLeft, displayTop, // top left corner coordinates
  111. QImage(m_processor->m_history.data(), // raw pixel data to display
  112. m_processor->binCount(), // width = number of frequency bins
  113. m_processor->m_waterfallHeight, // height = number of history lines
  114. QImage::Format_RGB32
  115. ).scaled(displayWidth, // scale to fit view..
  116. displayBottom,
  117. Qt::IgnoreAspectRatio,
  118. Qt::SmoothTransformation));
  119. lock.unlock();
  120. }
  121. else
  122. {
  123. painter.fillRect(displayLeft, displayTop, displayWidth, displayBottom, QColor(0,0,0));
  124. }
  125. // always draw the outline
  126. painter.setPen(QPen(m_controls->m_colorGrid, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  127. painter.drawRoundedRect(displayLeft, displayTop, displayWidth, displayBottom, 2.0, 2.0);
  128. #ifdef SA_DEBUG
  129. // display what FPS would be achieved if waterfall ran in a loop
  130. start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - start_time;
  131. painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
  132. painter.drawText(displayRight -100, 10, 100, 16, Qt::AlignLeft,
  133. QString(std::string("Max FPS: " + std::to_string(1000000000.0 / start_time)).c_str()));
  134. #endif
  135. }
  136. // Convert time value to Y coordinate for display of given height.
  137. float SaWaterfallView::timeToYPixel(float time, int height)
  138. {
  139. float pixels_per_line = (float)height / m_processor->m_waterfallHeight;
  140. float seconds_per_line = ((float)m_processor->m_inBlockSize / m_processor->getSampleRate());
  141. return pixels_per_line * time / seconds_per_line;
  142. }
  143. // Generate labels for linear time scale.
  144. std::vector<std::pair<float, std::string>> SaWaterfallView::makeTimeTics()
  145. {
  146. std::vector<std::pair<float, std::string>> result;
  147. float i;
  148. // upper limit defined by number of lines * time per line
  149. float limit = m_processor->m_waterfallHeight * ((float)m_processor->m_inBlockSize / m_processor->getSampleRate());
  150. // set increment so that about 8 tics are generated
  151. float increment = std::round(10 * limit / 7) / 10;
  152. // NOTE: labels positions are rounded to match the (rounded) label value
  153. for (i = 0; i <= limit; i += increment)
  154. {
  155. if (i < 10)
  156. {
  157. result.emplace_back(std::round(i * 10) / 10, std::to_string(std::round(i * 10) / 10).substr(0, 3));
  158. }
  159. else
  160. {
  161. result.emplace_back(std::round(i), std::to_string(std::round(i)).substr(0, 2));
  162. }
  163. }
  164. return result;
  165. }
  166. // Periodically trigger repaint and check if the widget is visible.
  167. // If it is not, stop drawing and inform the processor.
  168. void SaWaterfallView::periodicUpdate()
  169. {
  170. m_processor->setWaterfallActive(isVisible());
  171. if (isVisible()) {update();}
  172. }
  173. // Adjust window size and widget visibility when waterfall is enabled or disabbled.
  174. void SaWaterfallView::updateVisibility()
  175. {
  176. // get container of the control dialog to be resized if needed
  177. QWidget *subWindow = m_controlDialog->parentWidget();
  178. if (m_controls->m_waterfallModel.value())
  179. {
  180. // clear old data before showing the waterfall
  181. QMutexLocker lock(&m_processor->m_dataAccess);
  182. std::fill(m_processor->m_history.begin(), m_processor->m_history.end(), 0);
  183. lock.unlock();
  184. setVisible(true);
  185. // increase window size if it is too small
  186. if (subWindow->size().height() < m_controlDialog->sizeHint().height())
  187. {
  188. subWindow->resize(subWindow->size().width(), m_controlDialog->sizeHint().height());
  189. }
  190. }
  191. else
  192. {
  193. setVisible(false);
  194. // decrease window size only if it does not violate sizeHint
  195. subWindow->resize(subWindow->size().width(), m_controlDialog->sizeHint().height());
  196. }
  197. }