mirror of
https://github.com/LMMS/lmms.git
synced 2026-07-18 01:22:40 -04:00
Spectrum analyzer update (#5160)
* advanced config: expose hidden constants to user screen * advanced config: add support for FFT window overlapping * waterfall: display at native resolution on high-DPI screens * waterfall: add cursor and improve label density * FFT: fix normalization so that 0 dBFS matches full-scale sinewave * FFT: decouple data acquisition from processing and display * FFT: separate lock for reallocation (to avoid some needless waiting) * moved ranges and other constants to a separate file * debug: better performance measurements * minor fixes * build the ringbuffer library as part of LMMS core
This commit is contained in:
committed by
Johannes Lorenz
parent
2f0010270e
commit
da73ddd242
@@ -23,8 +23,12 @@
|
||||
#include "SaWaterfallView.h"
|
||||
|
||||
#include <algorithm>
|
||||
#ifdef SA_DEBUG
|
||||
#include <chrono>
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <QImage>
|
||||
#include <QMouseEvent>
|
||||
#include <QMutexLocker>
|
||||
#include <QPainter>
|
||||
#include <QSplitter>
|
||||
@@ -47,8 +51,22 @@ SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, Q
|
||||
|
||||
connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate()));
|
||||
|
||||
m_displayTop = 1;
|
||||
m_displayBottom = height() -2;
|
||||
m_displayLeft = 26;
|
||||
m_displayRight = width() -26;
|
||||
m_displayWidth = m_displayRight - m_displayLeft;
|
||||
m_displayHeight = m_displayBottom - m_displayTop;
|
||||
|
||||
m_timeTics = makeTimeTics();
|
||||
m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate();
|
||||
m_oldSecondsPerLine = 0;
|
||||
m_oldHeight = 0;
|
||||
|
||||
m_cursor = QPointF(0, 0);
|
||||
|
||||
#ifdef SA_DEBUG
|
||||
m_execution_avg = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -58,15 +76,14 @@ SaWaterfallView::SaWaterfallView(SaControls *controls, SaProcessor *processor, Q
|
||||
void SaWaterfallView::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
#ifdef SA_DEBUG
|
||||
int start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
|
||||
unsigned int draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
|
||||
#endif
|
||||
|
||||
// all drawing done here, local variables are sufficient for the boundary
|
||||
const int displayTop = 1;
|
||||
const int displayBottom = height() -2;
|
||||
const int displayLeft = 26;
|
||||
const int displayRight = width() -26;
|
||||
const int displayWidth = displayRight - displayLeft;
|
||||
// update boundary
|
||||
m_displayBottom = height() -2;
|
||||
m_displayRight = width() -26;
|
||||
m_displayWidth = m_displayRight - m_displayLeft;
|
||||
m_displayHeight = m_displayBottom - m_displayTop;
|
||||
float label_width = 20;
|
||||
float label_height = 16;
|
||||
float margin = 2;
|
||||
@@ -75,10 +92,11 @@ void SaWaterfallView::paintEvent(QPaintEvent *event)
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
// check if time labels need to be rebuilt
|
||||
if ((float)m_processor->m_inBlockSize / m_processor->getSampleRate() != m_oldTimePerLine)
|
||||
if (secondsPerLine() != m_oldSecondsPerLine || m_processor->waterfallHeight() != m_oldHeight)
|
||||
{
|
||||
m_timeTics = makeTimeTics();
|
||||
m_oldTimePerLine = (float)m_processor->m_inBlockSize / m_processor->getSampleRate();
|
||||
m_oldSecondsPerLine = secondsPerLine();
|
||||
m_oldHeight = m_processor->waterfallHeight();
|
||||
}
|
||||
|
||||
// print time labels
|
||||
@@ -86,78 +104,104 @@ void SaWaterfallView::paintEvent(QPaintEvent *event)
|
||||
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
||||
for (auto & line: m_timeTics)
|
||||
{
|
||||
pos = timeToYPixel(line.first, displayBottom);
|
||||
pos = timeToYPixel(line.first, m_displayHeight);
|
||||
// align first and last label to the edge if needed, otherwise center them
|
||||
if (line == m_timeTics.front() && pos < label_height / 2)
|
||||
{
|
||||
painter.drawText(displayLeft - label_width - margin, displayTop - 1,
|
||||
painter.drawText(m_displayLeft - label_width - margin, m_displayTop - 1,
|
||||
label_width, label_height, Qt::AlignRight | Qt::AlignTop | Qt::TextDontClip,
|
||||
QString(line.second.c_str()));
|
||||
painter.drawText(displayRight + margin, displayTop - 1,
|
||||
painter.drawText(m_displayRight + margin, m_displayTop - 1,
|
||||
label_width, label_height, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip,
|
||||
QString(line.second.c_str()));
|
||||
}
|
||||
else if (line == m_timeTics.back() && pos > displayBottom - label_height + 2)
|
||||
else if (line == m_timeTics.back() && pos > m_displayBottom - label_height + 2)
|
||||
{
|
||||
painter.drawText(displayLeft - label_width - margin, displayBottom - label_height,
|
||||
painter.drawText(m_displayLeft - label_width - margin, m_displayBottom - label_height,
|
||||
label_width, label_height, Qt::AlignRight | Qt::AlignBottom | Qt::TextDontClip,
|
||||
QString(line.second.c_str()));
|
||||
painter.drawText(displayRight + margin, displayBottom - label_height + 2,
|
||||
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
|
||||
{
|
||||
painter.drawText(displayLeft - label_width - margin, pos - label_height / 2,
|
||||
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()));
|
||||
painter.drawText(displayRight + margin, pos - label_height / 2,
|
||||
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 the spectrogram precomputed in SaProcessor
|
||||
if (m_processor->m_waterfallNotEmpty)
|
||||
if (m_processor->waterfallNotEmpty())
|
||||
{
|
||||
QMutexLocker lock(&m_processor->m_dataAccess);
|
||||
painter.drawImage(displayLeft, displayTop, // top left corner coordinates
|
||||
QImage(m_processor->m_history.data(), // raw pixel data to display
|
||||
m_processor->binCount(), // width = number of frequency bins
|
||||
m_processor->m_waterfallHeight, // height = number of history lines
|
||||
QImage::Format_RGB32
|
||||
).scaled(displayWidth, // scale to fit view..
|
||||
displayBottom,
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation));
|
||||
QMutexLocker lock(&m_processor->m_reallocationAccess);
|
||||
QImage temp = QImage(m_processor->getHistory(), // raw pixel data to display
|
||||
m_processor->waterfallWidth(), // width = number of frequency bins
|
||||
m_processor->waterfallHeight(), // height = number of history lines
|
||||
QImage::Format_RGB32);
|
||||
lock.unlock();
|
||||
temp.setDevicePixelRatio(devicePixelRatio()); // display at native resolution
|
||||
painter.drawImage(m_displayLeft, m_displayTop,
|
||||
temp.scaled(m_displayWidth * devicePixelRatio(),
|
||||
m_displayHeight * devicePixelRatio(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation));
|
||||
m_processor->flipRequest();
|
||||
}
|
||||
else
|
||||
{
|
||||
painter.fillRect(displayLeft, displayTop, displayWidth, displayBottom, QColor(0,0,0));
|
||||
painter.fillRect(m_displayLeft, m_displayTop, m_displayWidth, m_displayHeight, QColor(0,0,0));
|
||||
}
|
||||
|
||||
// draw cursor (if it is within bounds)
|
||||
drawCursor(painter);
|
||||
|
||||
// always draw the outline
|
||||
painter.setPen(QPen(m_controls->m_colorGrid, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
||||
painter.drawRoundedRect(displayLeft, displayTop, displayWidth, displayBottom, 2.0, 2.0);
|
||||
painter.drawRoundedRect(m_displayLeft, m_displayTop, m_displayWidth, m_displayHeight, 2.0, 2.0);
|
||||
|
||||
#ifdef SA_DEBUG
|
||||
// display what FPS would be achieved if waterfall ran in a loop
|
||||
start_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - start_time;
|
||||
draw_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - draw_time;
|
||||
m_execution_avg = 0.95 * m_execution_avg + 0.05 * draw_time / 1000000.0;
|
||||
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
||||
painter.drawText(displayRight -100, 10, 100, 16, Qt::AlignLeft,
|
||||
QString(std::string("Max FPS: " + std::to_string(1000000000.0 / start_time)).c_str()));
|
||||
painter.drawText(m_displayRight -150, 10, 100, 16, Qt::AlignLeft,
|
||||
QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).c_str()).append(" ms"));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
// Helper functions for time conversion
|
||||
float SaWaterfallView::samplesPerLine()
|
||||
{
|
||||
return (float)m_processor->inBlockSize() / m_controls->m_windowOverlapModel.value();
|
||||
}
|
||||
|
||||
float SaWaterfallView::secondsPerLine()
|
||||
{
|
||||
return samplesPerLine() / m_processor->getSampleRate();
|
||||
}
|
||||
|
||||
|
||||
// Convert time value to Y coordinate for display of given height.
|
||||
float SaWaterfallView::timeToYPixel(float time, int height)
|
||||
{
|
||||
float pixels_per_line = (float)height / m_processor->m_waterfallHeight;
|
||||
float seconds_per_line = ((float)m_processor->m_inBlockSize / m_processor->getSampleRate());
|
||||
float pixels_per_line = (float)height / m_processor->waterfallHeight();
|
||||
|
||||
return pixels_per_line * time / seconds_per_line;
|
||||
return pixels_per_line * time / secondsPerLine();
|
||||
}
|
||||
|
||||
|
||||
// Convert Y coordinate on display of given height back to time value.
|
||||
float SaWaterfallView::yPixelToTime(float position, int height)
|
||||
{
|
||||
if (height == 0) {height = 1;}
|
||||
float pixels_per_line = (float)height / m_processor->waterfallHeight();
|
||||
|
||||
return (position / pixels_per_line) * secondsPerLine();
|
||||
}
|
||||
|
||||
|
||||
@@ -167,16 +211,21 @@ std::vector<std::pair<float, std::string>> SaWaterfallView::makeTimeTics()
|
||||
std::vector<std::pair<float, std::string>> result;
|
||||
float i;
|
||||
|
||||
// upper limit defined by number of lines * time per line
|
||||
float limit = m_processor->m_waterfallHeight * ((float)m_processor->m_inBlockSize / m_processor->getSampleRate());
|
||||
// get time value of the last line
|
||||
float limit = yPixelToTime(m_displayBottom, m_displayHeight);
|
||||
|
||||
// set increment so that about 8 tics are generated
|
||||
float increment = std::round(10 * limit / 7) / 10;
|
||||
// set increment to about 30 pixels (but min. 0.1 s)
|
||||
float increment = std::round(10 * limit / (m_displayHeight / 30)) / 10;
|
||||
if (increment < 0.1) {increment = 0.1;}
|
||||
|
||||
// NOTE: labels positions are rounded to match the (rounded) label value
|
||||
for (i = 0; i <= limit; i += increment)
|
||||
{
|
||||
if (i < 10)
|
||||
if (i > 99)
|
||||
{
|
||||
result.emplace_back(std::round(i), std::to_string(std::round(i)).substr(0, 3));
|
||||
}
|
||||
else if (i < 10)
|
||||
{
|
||||
result.emplace_back(std::round(i * 10) / 10, std::to_string(std::round(i * 10) / 10).substr(0, 3));
|
||||
}
|
||||
@@ -208,10 +257,7 @@ void SaWaterfallView::updateVisibility()
|
||||
if (m_controls->m_waterfallModel.value())
|
||||
{
|
||||
// clear old data before showing the waterfall
|
||||
QMutexLocker lock(&m_processor->m_dataAccess);
|
||||
std::fill(m_processor->m_history.begin(), m_processor->m_history.end(), 0);
|
||||
lock.unlock();
|
||||
|
||||
m_processor->clearHistory();
|
||||
setVisible(true);
|
||||
|
||||
// increase window size if it is too small
|
||||
@@ -228,3 +274,70 @@ void SaWaterfallView::updateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Draw cursor and its coordinates if it is within display bounds.
|
||||
void SaWaterfallView::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(QPointF(m_cursor.x(), m_displayTop), QPointF(m_cursor.x(), m_displayBottom));
|
||||
painter.drawLine(QPointF(m_displayLeft, m_cursor.y()), QPointF(m_displayRight, m_cursor.y()));
|
||||
|
||||
// coordinates: background box
|
||||
QFontMetrics fontMetrics = painter.fontMetrics();
|
||||
unsigned int const box_left = 5;
|
||||
unsigned int const box_top = 5;
|
||||
unsigned int const box_margin = 3;
|
||||
unsigned int const box_height = 2*(fontMetrics.size(Qt::TextSingleLine, "0 Hz").height() + box_margin);
|
||||
unsigned int const box_width = fontMetrics.size(Qt::TextSingleLine, "20000 Hz ").width() + 2*box_margin;
|
||||
painter.setPen(QPen(m_controls->m_colorLabels.darker(), 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
||||
painter.fillRect(m_displayLeft + box_left, m_displayTop + box_top,
|
||||
box_width, box_height, QColor(0, 0, 0, 64));
|
||||
|
||||
// coordinates: text
|
||||
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
||||
QString tmps;
|
||||
|
||||
// frequency
|
||||
int freq = (int)m_processor->xPixelToFreq(m_cursor.x() - m_displayLeft, m_displayWidth);
|
||||
tmps = QString("%1 Hz").arg(freq);
|
||||
painter.drawText(m_displayLeft + box_left + box_margin,
|
||||
m_displayTop + box_top + box_margin,
|
||||
box_width, box_height / 2, Qt::AlignLeft, tmps);
|
||||
|
||||
// time
|
||||
float time = yPixelToTime(m_cursor.y(), m_displayBottom);
|
||||
tmps = QString(std::to_string(time).substr(0, 5).c_str()).append(" s");
|
||||
painter.drawText(m_displayLeft + box_left + box_margin,
|
||||
m_displayTop + box_top + box_height / 2,
|
||||
box_width, box_height / 2, Qt::AlignLeft, tmps);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle mouse input: set new cursor position.
|
||||
// For some reason (a bug?), localPos() only returns integers. As a workaround
|
||||
// the fractional part is taken from windowPos() (which works correctly).
|
||||
void SaWaterfallView::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
|
||||
event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
|
||||
}
|
||||
|
||||
void SaWaterfallView::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
|
||||
event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
|
||||
}
|
||||
|
||||
|
||||
// Handle resize event: rebuild time labels
|
||||
void SaWaterfallView::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
m_timeTics = makeTimeTics();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user