/* SaSpectrumView.cpp - implementation of SaSpectrumView class. * * Copyright (c) 2019 Martin Pavelek * * Based partially on Eq plugin code, * Copyright (c) 2014-2017, David French * * This file is part of LMMS - https://lmms.io * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program (see COPYING); if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301 USA. * */ #include "SaSpectrumView.h" #include #include #include #include #include #include #include "GuiApplication.h" #include "MainWindow.h" #include "SaProcessor.h" #ifdef SA_DEBUG #include #include #endif SaSpectrumView::SaSpectrumView(SaControls *controls, SaProcessor *processor, QWidget *_parent) : QWidget(_parent), m_controls(controls), m_processor(processor), m_freezeRequest(false), m_frozen(false) { setMinimumSize(360, 170); setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate())); m_displayBufferL.resize(m_processor->binCount(), 0); m_displayBufferR.resize(m_processor->binCount(), 0); m_peakBufferL.resize(m_processor->binCount(), 0); m_peakBufferR.resize(m_processor->binCount(), 0); m_freqRangeIndex = m_controls->m_freqRangeModel.value(); m_ampRangeIndex = m_controls->m_ampRangeModel.value(); m_logFreqTics = makeLogFreqTics(m_processor->getFreqRangeMin(), m_processor->getFreqRangeMax()); m_linearFreqTics = makeLinearFreqTics(m_processor->getFreqRangeMin(), m_processor->getFreqRangeMax()); m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax()); m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax()); m_cursor = QPoint(0, 0); } // Compose and draw all the content; periodically called by Qt. // NOTE: Performance sensitive! If the drawing takes too long, it will drag // the FPS down for the entire program! Use SA_DEBUG to display timings. void SaSpectrumView::paintEvent(QPaintEvent *event) { #ifdef SA_DEBUG int total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif // 0) Constants and init QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); // drawing and path-making are split into multiple methods for clarity; // display boundaries are updated here and shared as member variables m_displayTop = 1; m_displayBottom = height() -20; m_displayLeft = 26; m_displayRight = width() -26; m_displayWidth = m_displayRight - m_displayLeft; // recompute range labels if needed if (m_freqRangeIndex != m_controls->m_freqRangeModel.value()) { m_logFreqTics = makeLogFreqTics(m_processor->getFreqRangeMin(), m_processor->getFreqRangeMax()); m_linearFreqTics = makeLinearFreqTics(m_processor->getFreqRangeMin(true), m_processor->getFreqRangeMax()); m_freqRangeIndex = m_controls->m_freqRangeModel.value(); } if (m_ampRangeIndex != m_controls->m_ampRangeModel.value()) { m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax()); m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(true), m_processor->getAmpRangeMax()); m_ampRangeIndex = m_controls->m_ampRangeModel.value(); } // generate freeze request or clear "frozen" status based on freeze button if (!m_frozen && m_controls->m_refFreezeModel.value()) { m_freezeRequest = true; } else if (!m_controls->m_refFreezeModel.value()) { m_frozen = false; } // 1) Background, grid and labels drawGrid(painter); // 2) Spectrum display drawSpectrum(painter); // 3) Overlays // draw cursor (if it is within bounds) drawCursor(painter); // always draw the display outline painter.setPen(QPen(m_controls->m_colorGrid, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawRoundedRect(m_displayLeft, 1, m_displayWidth, m_displayBottom, 2.0, 2.0); #ifdef SA_DEBUG // display what FPS would be achieved if spectrum display ran in a loop total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time; painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawText(m_displayRight -100, 70, 100, 16, Qt::AlignLeft, QString(std::string("Max FPS: " + std::to_string(1000000000.0 / total_time)).c_str())); #endif } // Refresh data and draw the spectrum. void SaSpectrumView::drawSpectrum(QPainter &painter) { #ifdef SA_DEBUG int path_time = 0, draw_time = 0; #endif // draw the graph only if there is any input, averaging residue or peaks QMutexLocker lock(&m_processor->m_dataAccess); if (m_decaySum > 0 || notEmpty(m_processor->m_normSpectrumL) || notEmpty(m_processor->m_normSpectrumR)) { lock.unlock(); #ifdef SA_DEBUG path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif // update data buffers and reconstruct paths refreshPaths(); #ifdef SA_DEBUG path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - path_time; #endif // draw stored paths #ifdef SA_DEBUG draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif // in case stereo is disabled, mono data are stored in left channel structures if (m_controls->m_stereoModel.value()) { painter.fillPath(m_pathR, QBrush(m_controls->m_colorR)); painter.fillPath(m_pathL, QBrush(m_controls->m_colorL)); } else { painter.fillPath(m_pathL, QBrush(m_controls->m_colorMono)); } // draw the peakBuffer only if peak hold or reference freeze is active if (m_controls->m_peakHoldModel.value() || m_controls->m_refFreezeModel.value()) { if (m_controls->m_stereoModel.value()) { painter.setPen(QPen(m_controls->m_colorR, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawPath(m_pathPeakR); painter.setPen(QPen(m_controls->m_colorL, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawPath(m_pathPeakL); } else { painter.setPen(QPen(m_controls->m_colorL, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawPath(m_pathPeakL); } } #ifdef SA_DEBUG draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time; #endif } else { lock.unlock(); } #ifdef SA_DEBUG // display measurement results painter.drawText(m_displayRight -100, 90, 100, 16, Qt::AlignLeft, QString(std::string("Path ms: " + std::to_string(path_time / 1000000.0)).c_str())); painter.drawText(m_displayRight -100, 110, 100, 16, Qt::AlignLeft, QString(std::string("Draw ms: " + std::to_string(draw_time / 1000000.0)).c_str())); #endif } // Read newest FFT results from SaProcessor, update local display buffers // and build QPainter paths. void SaSpectrumView::refreshPaths() { // Lock is required for the entire function, mainly to prevent block size // changes from causing reallocation of data structures mid-way. QMutexLocker lock(&m_processor->m_dataAccess); // check if bin count changed and reallocate display buffers accordingly if (m_processor->binCount() != m_displayBufferL.size()) { m_displayBufferL.clear(); m_displayBufferR.clear(); m_peakBufferL.clear(); m_peakBufferR.clear(); m_displayBufferL.resize(m_processor->binCount(), 0); m_displayBufferR.resize(m_processor->binCount(), 0); m_peakBufferL.resize(m_processor->binCount(), 0); m_peakBufferR.resize(m_processor->binCount(), 0); } // update display buffers for left and right channel #ifdef SA_DEBUG int refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif m_decaySum = 0; updateBuffers(m_processor->m_normSpectrumL.data(), m_displayBufferL.data(), m_peakBufferL.data()); updateBuffers(m_processor->m_normSpectrumR.data(), m_displayBufferR.data(), m_peakBufferR.data()); #ifdef SA_DEBUG refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - refresh_time; #endif // if there was a freeze request, it was taken care of during the update if (m_controls->m_refFreezeModel.value() && m_freezeRequest) { m_freezeRequest = false; m_frozen = true; } #ifdef SA_DEBUG int make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count(); #endif // Use updated display buffers to prepare new paths for QPainter. // This is the second slowest action (first is the subsequent drawing); use // the resolution parameter to balance display quality and performance. m_pathL = makePath(m_displayBufferL, 1.5); if (m_controls->m_stereoModel.value()) { m_pathR = makePath(m_displayBufferR, 1.5); } if (m_controls->m_peakHoldModel.value() || m_controls->m_refFreezeModel.value()) { m_pathPeakL = makePath(m_peakBufferL, 0.25); if (m_controls->m_stereoModel.value()) { m_pathPeakR = makePath(m_peakBufferR, 0.25); } } #ifdef SA_DEBUG make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - make_time; #endif #ifdef SA_DEBUG // print measurement results std::cout << "Buffer update ms: " << std::to_string(refresh_time / 1000000.0) << ", "; std::cout << "Path-make ms: " << std::to_string(make_time / 1000000.0) << std::endl; #endif } // Update display buffers: add new data, update average and peaks / reference. // Output the sum of all displayed values -- draw only if it is non-zero. // NOTE: The calling function is responsible for acquiring SaProcessor data // access lock! void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float *peakBuffer) { for (int n = 0; n < m_processor->binCount(); n++) { // Update the exponential average if enabled, or simply copy the value. if (!m_controls->m_pauseModel.value()) { if (m_controls->m_smoothModel.value()) { displayBuffer[n] = spectrum[n] * m_smoothFactor + displayBuffer[n] * (1 - m_smoothFactor); } else { displayBuffer[n] = spectrum[n]; } } // Update peak-hold and reference freeze data (using a shared curve). // Peak hold and freeze can be combined: decay only if not frozen. // Ref. freeze operates on the (possibly averaged) display buffer. if (m_controls->m_refFreezeModel.value() && m_freezeRequest) { peakBuffer[n] = displayBuffer[n]; } else if (m_controls->m_peakHoldModel.value() && !m_controls->m_pauseModel.value()) { if (spectrum[n] > peakBuffer[n]) { peakBuffer[n] = spectrum[n]; } else if (!m_controls->m_refFreezeModel.value()) { peakBuffer[n] = peakBuffer[n] * m_peakDecayFactor; } } else if (!m_controls->m_refFreezeModel.value() && !m_controls->m_peakHoldModel.value()) { peakBuffer[n] = 0; } // take note if there was actually anything to display m_decaySum += displayBuffer[n] + peakBuffer[n]; } } // Use display buffer to build a path that can be drawn or filled by QPainter. // Resolution controls the performance / quality tradeoff; the value specifies // number of points in x axis per device pixel. Values over 1.0 still // contribute to quality and accuracy thanks to anti-aliasing. QPainterPath SaSpectrumView::makePath(std::vector &displayBuffer, float resolution = 1.0) { // convert resolution to number of path points per logical pixel float pixel_limit = resolution * window()->devicePixelRatio(); QPainterPath path; path.moveTo(m_displayLeft, m_displayBottom); // Translate frequency bins to path points. // Display is flipped: y values grow towards zero, initial max is bottom. // Bins falling to interval [x_start, x_next) contribute to a single point. float max = m_displayBottom; float x_start = -1; // lower bound of currently constructed point for (unsigned int n = 0; n < m_processor->binCount(); n++) { float x = freqToXPixel(binToFreq(n), m_displayWidth); float x_next = freqToXPixel(binToFreq(n + 1), m_displayWidth); float y = ampToYPixel(displayBuffer[n], m_displayBottom); // consider making a point only if x falls within display bounds if (0 < x && x < m_displayWidth) { if (x_start == -1) { x_start = x; // the first displayed bin is stretched to the left edge to prevent // creating a misleading slope leading to zero (at log. scale) path.lineTo(m_displayLeft, y + m_displayTop); } // Opt.: QPainter is very slow -- draw at most [pixel_limit] points // per logical pixel. As opposed to limiting the bin count, this // allows high resolution display if user resizes the analyzer. // Look at bins that share the pixel and use the highest value: max = y < max ? y : max; // And make the final point in the middle of current interval. if ((int)(x * pixel_limit) != (int)(x_next * pixel_limit)) { x = (x + x_start) / 2; path.lineTo(x + m_displayLeft, max + m_displayTop); max = m_displayBottom; x_start = x_next; } } else { // stop processing after a bin falls outside right edge // and align it to the edge to prevent a gap if (n > 0 && x > 0) { path.lineTo(m_displayRight, y + m_displayTop); break; } } } path.lineTo(m_displayRight, m_displayBottom); path.closeSubpath(); return path; } // Draw background, grid and associated frequency and amplitude labels. void SaSpectrumView::drawGrid(QPainter &painter) { std::vector> *freqTics = NULL; std::vector> *ampTics = NULL; float pos = 0; float label_width = 24; float label_height = 15; float margin = 5; // always draw the background painter.fillRect(m_displayLeft, m_displayTop, m_displayWidth, m_displayBottom, m_controls->m_colorBG); // select logarithmic or linear frequency grid and draw it if (m_controls->m_logXModel.value()) { freqTics = &m_logFreqTics; } else { freqTics = &m_linearFreqTics; } // draw frequency grid (line.first is display position) painter.setPen(QPen(m_controls->m_colorGrid, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); for (auto &line: *freqTics) { painter.drawLine(m_displayLeft + freqToXPixel(line.first, m_displayWidth), 2, m_displayLeft + freqToXPixel(line.first, m_displayWidth), m_displayBottom); } // print frequency labels (line.second is label) painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); for (auto & line: *freqTics) { pos = m_displayLeft + freqToXPixel(line.first, m_displayWidth); // align first and last label to the edge if needed, otherwise center them if (line == freqTics->front() && pos - label_width / 2 < m_displayLeft) { painter.drawText(m_displayLeft, m_displayBottom + margin, label_width, label_height, Qt::AlignLeft | Qt::TextDontClip, QString(line.second.c_str())); } else if (line == freqTics->back() && pos + label_width / 2 > m_displayRight) { painter.drawText(m_displayRight - label_width, m_displayBottom + margin, label_width, label_height, Qt::AlignRight | Qt::TextDontClip, QString(line.second.c_str())); } else { painter.drawText(pos - label_width / 2, m_displayBottom + margin, label_width, label_height, Qt::AlignHCenter | Qt::TextDontClip, QString(line.second.c_str())); } } margin = 2; // select logarithmic or linear amplitude grid and draw it if (m_controls->m_logYModel.value()) { ampTics = &m_logAmpTics; } else { ampTics = &m_linearAmpTics; } // draw amplitude grid painter.setPen(QPen(m_controls->m_colorGrid, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); for (auto & line: *ampTics) { painter.drawLine(m_displayLeft + 1, ampToYPixel(line.first, m_displayBottom), m_displayRight - 1, ampToYPixel(line.first, m_displayBottom)); } // print amplitude labels painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); bool stereo = m_controls->m_stereoModel.value(); for (auto & line: *ampTics) { pos = ampToYPixel(line.first, m_displayBottom); // align first and last labels to edge if needed, otherwise center them if (line == ampTics->back() && pos < 8) { if (stereo) { painter.setPen(QPen(m_controls->m_colorL.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); } painter.drawText(m_displayLeft - label_width - margin, m_displayTop - 2, label_width, label_height, Qt::AlignRight | Qt::AlignTop | Qt::TextDontClip, QString(line.second.c_str())); if (stereo) { painter.setPen(QPen(m_controls->m_colorR.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); } painter.drawText(m_displayRight + margin, m_displayTop - 2, label_width, label_height, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip, QString(line.second.c_str())); } else if (line == ampTics->front() && pos > m_displayBottom - label_height) { if (stereo) { painter.setPen(QPen(m_controls->m_colorL.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); } painter.drawText(m_displayLeft - label_width - margin, m_displayBottom - label_height + 2, label_width, label_height, Qt::AlignRight | Qt::AlignBottom | Qt::TextDontClip, QString(line.second.c_str())); if (stereo) { painter.setPen(QPen(m_controls->m_colorR.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); } painter.drawText(m_displayRight + margin, m_displayBottom - label_height + 2, label_width, label_height, Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip, QString(line.second.c_str())); } else { if (stereo) { painter.setPen(QPen(m_controls->m_colorL.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); } painter.drawText(m_displayLeft - label_width - margin, pos - label_height / 2, label_width, label_height, Qt::AlignRight | Qt::AlignVCenter | Qt::TextDontClip, QString(line.second.c_str())); if (stereo) { painter.setPen(QPen(m_controls->m_colorR.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); } painter.drawText(m_displayRight + margin, pos - label_height / 2, label_width, label_height, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextDontClip, QString(line.second.c_str())); } } } // Draw cursor and its coordinates if it is within display bounds. void SaSpectrumView::drawCursor(QPainter &painter) { if( m_cursor.x() >= m_displayLeft && m_cursor.x() <= m_displayRight && m_cursor.y() >= m_displayTop && m_cursor.y() <= m_displayBottom) { // cursor lines painter.setPen(QPen(m_controls->m_colorGrid.lighter(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawLine(m_cursor.x(), m_displayTop, m_cursor.x(), m_displayBottom); painter.drawLine(m_displayLeft, m_cursor.y(), m_displayRight, m_cursor.y()); // coordinates painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin)); painter.drawText(m_displayRight -60, 5, 100, 16, Qt::AlignLeft, "Cursor"); QString tmps; // frequency int xFreq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth); tmps = QString(std::string(std::to_string(xFreq) + " Hz").c_str()); painter.drawText(m_displayRight -60, 18, 100, 16, Qt::AlignLeft, tmps); // amplitude float yAmp = m_processor->yPixelToAmp(m_cursor.y(), m_displayBottom); if (m_controls->m_logYModel.value()) { tmps = QString(std::string(std::to_string(yAmp).substr(0, 5) + " dB").c_str()); } else { // add 0.0005 to get proper rounding to 3 decimal places tmps = QString(std::string(std::to_string(0.0005f + yAmp)).substr(0, 5).c_str()); } painter.drawText(m_displayRight -60, 30, 100, 16, Qt::AlignLeft, tmps); } } // Wrappers for most used SaProcessor helpers (to make local code more compact). float SaSpectrumView::binToFreq(unsigned int bin_index) { return m_processor->binToFreq(bin_index); } float SaSpectrumView::freqToXPixel(float frequency, unsigned int width) { return m_processor->freqToXPixel(frequency, width); } float SaSpectrumView::ampToYPixel(float amplitude, unsigned int height) { return m_processor->ampToYPixel(amplitude, height); } // Generate labels suitable for logarithmic frequency scale. // Low / high limits are in Hz. Lowest possible label is 10 Hz. std::vector> SaSpectrumView::makeLogFreqTics(int low, int high) { std::vector> result; int i, j; int a[] = {10, 20, 50}; // sparse series multipliers int b[] = {14, 30, 70}; // additional (denser) series // generate main steps (powers of 10); use the series to specify smaller steps for (i = 1; i <= high; i *= 10) { for (j = 0; j < 3; j++) { // insert a label from sparse series if it falls within bounds if (i * a[j] >= low && i * a[j] <= high) { if (i * a[j] < 1000) { result.emplace_back(i * a[j], std::to_string(i * a[j])); } else { result.emplace_back(i * a[j], std::to_string(i * a[j] / 1000) + "k"); } } // also insert denser series if high and low values are close if ((log10(high) - log10(low) < 2) && (i * b[j] >= low && i * b[j] <= high)) { if (i * b[j] < 1500) { result.emplace_back(i * b[j], std::to_string(i * b[j])); } else { result.emplace_back(i * b[j], std::to_string(i * b[j] / 1000) + "k"); } } } } return result; } // Generate labels suitable for linear frequency scale. // Low / high limits are in Hz. std::vector> SaSpectrumView::makeLinearFreqTics(int low, int high) { std::vector> result; int i, increment; // select a suitable increment based on zoom level if (high - low < 500) {increment = 50;} else if (high - low < 1000) {increment = 100;} else if (high - low < 5000) {increment = 1000;} else {increment = 2000;} // generate steps based on increment, starting at 0 for (i = 0; i <= high; i += increment) { if (i >= low) { if (i < 1000) { result.emplace_back(i, std::to_string(i)); } else { result.emplace_back(i, std::to_string(i/1000) + "k"); } } } return result; } // Generate labels suitable for logarithmic (dB) amplitude scale. // Low / high limits are in dB; 0 dB amplitude = 1.0 linear. // Treating results as power ratio, i.e., 3 dB should be about twice as loud. std::vector> SaSpectrumView::makeLogAmpTics(int low, int high) { std::vector> result; float i; double increment; // Base zoom level on selected range and how close is the current height // to the sizeHint() (denser scale for bigger window). if ((high - low) < 20 * ((float)height() / sizeHint().height())) { increment = pow(10, 0.3); // 3 dB steps when really zoomed in } else if (high - low < 45 * ((float)height() / sizeHint().height())) { increment = pow(10, 0.6); // 6 dB steps when sufficiently zoomed in } else { increment = 10; // 10 dB steps otherwise } // Generate n dB increments, start checking at -90 dB. Limits are tweaked // just a little bit to make sure float comparisons do not miss edges. for (i = 0.000000001; 10 * log10(i) <= (high + 0.001); i *= increment) { if (10 * log10(i) >= (low - 0.001)) { result.emplace_back(i, std::to_string((int)std::round(10 * log10(i)))); } } return result; } // Generate labels suitable for linear amplitude scale. // Low / high limits are in dB; 0 dB amplitude = 1.0 linear. // Smallest possible label is 0.001, largest is 999. This includes the majority // of useful labels; going lower or higher would require increasing margin size // so that the text can fit. That would be a waste of space -- the linear scale // would only make the experience worse for the main, logarithmic (dB) scale. std::vector> SaSpectrumView::makeLinearAmpTics(int low, int high) { std::vector> result; double i, nearest; // make about 5 labels when window is small, 10 if it is big float split = (float)height() / sizeHint().height() >= 1.5 ? 10.0 : 5.0; // convert limits to linear scale float lin_low = pow(10, low / 10.0); float lin_high = pow(10, high / 10.0); // Linear scale will vary widely, so instead of trying to craft extra nice // multiples, just generate a few evenly spaced increments across the range, // paying attention only to the decimal places to keep labels short. // Limits are shifted a bit so that float comparisons do not miss edges. for (i = 0; i <= (lin_high + 0.0001); i += (lin_high - lin_low) / split) { if (i >= (lin_low - 0.0001)) { if (i >= 9.99 && i < 99.9) { nearest = std::round(i); result.emplace_back(nearest, std::to_string(nearest).substr(0, 2)); } else if (i >= 0.099) { // also covers numbers above 100 nearest = std::round(i * 10) / 10; result.emplace_back(nearest, std::to_string(nearest).substr(0, 3)); } else if (i >= 0.0099) { nearest = std::round(i * 1000) / 1000; result.emplace_back(nearest, std::to_string(nearest).substr(0, 4)); } else if (i >= 0.00099) { nearest = std::round(i * 10000) / 10000; result.emplace_back(nearest, std::to_string(nearest).substr(1, 4)); } else if (i > -0.01 && i < 0.01) { result.emplace_back(i, "0"); // an exception, zero is short.. } } } return result; } // Periodic update is called by LMMS. void SaSpectrumView::periodicUpdate() { // check if the widget is visible; if it is not, processing can be paused m_processor->setSpectrumActive(isVisible()); // tell Qt it is time for repaint update(); } // Handle mouse input: set new cursor position. void SaSpectrumView::mouseMoveEvent(QMouseEvent *event) { m_cursor = event->pos(); } void SaSpectrumView::mousePressEvent(QMouseEvent *event) { m_cursor = event->pos(); } // Handle resize event: rebuild grid and labels void SaSpectrumView::resizeEvent(QResizeEvent *event) { // frequency does not change density with size // amplitude does: rebuild labels m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax()); m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax()); }