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:
Martin Pavelek
2019-11-21 14:44:18 +01:00
committed by Johannes Lorenz
parent 2f0010270e
commit da73ddd242
26 changed files with 1867 additions and 364 deletions

View File

@@ -39,7 +39,6 @@
#ifdef SA_DEBUG
#include <chrono>
#include <iostream>
#endif
@@ -68,7 +67,11 @@ SaSpectrumView::SaSpectrumView(SaControls *controls, SaProcessor *processor, QWi
m_logAmpTics = makeLogAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
m_linearAmpTics = makeLinearAmpTics(m_processor->getAmpRangeMin(), m_processor->getAmpRangeMax());
m_cursor = QPoint(0, 0);
m_cursor = QPointF(0, 0);
#ifdef SA_DEBUG
m_execution_avg = m_path_avg = m_draw_avg = 0;
#endif
}
@@ -134,12 +137,20 @@ void SaSpectrumView::paintEvent(QPaintEvent *event)
2.0, 2.0);
#ifdef SA_DEBUG
// display what FPS would be achieved if spectrum display ran in a loop
// display performance measurements if enabled
total_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - total_time;
m_execution_avg = 0.95 * m_execution_avg + 0.05 * total_time / 1000000.0;
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()));
painter.drawText(m_displayRight -150, 10, 130, 16, Qt::AlignLeft,
QString("Exec avg.: ").append(std::to_string(m_execution_avg).substr(0, 5).c_str()).append(" ms"));
painter.drawText(m_displayRight -150, 30, 130, 16, Qt::AlignLeft,
QString("Buff. upd. avg: ").append(std::to_string(m_refresh_avg).substr(0, 5).c_str()).append(" ms"));
painter.drawText(m_displayRight -150, 50, 130, 16, Qt::AlignLeft,
QString("Path build avg: ").append(std::to_string(m_path_avg).substr(0, 5).c_str()).append(" ms"));
painter.drawText(m_displayRight -150, 70, 130, 16, Qt::AlignLeft,
QString("Path draw avg: ").append(std::to_string(m_draw_avg).substr(0, 5).c_str()).append(" ms"));
#endif
}
@@ -148,22 +159,14 @@ void SaSpectrumView::paintEvent(QPaintEvent *event)
void SaSpectrumView::drawSpectrum(QPainter &painter)
{
#ifdef SA_DEBUG
int path_time = 0, draw_time = 0;
int 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))
if (m_decaySum > 0 || m_processor->spectrumNotEmpty())
{
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
@@ -199,17 +202,10 @@ void SaSpectrumView::drawSpectrum(QPainter &painter)
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()));
// save performance measurement result
m_draw_avg = 0.95 * m_draw_avg + 0.05 * draw_time / 1000000.0;
#endif
}
@@ -218,9 +214,9 @@ void SaSpectrumView::drawSpectrum(QPainter &painter)
// 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);
// Reallocation lock is required for the entire function, to keep display
// buffer size consistent with block size.
QMutexLocker reloc_lock(&m_processor->m_reallocationAccess);
// check if bin count changed and reallocate display buffers accordingly
if (m_processor->binCount() != m_displayBufferL.size())
@@ -240,8 +236,8 @@ void SaSpectrumView::refreshPaths()
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());
updateBuffers(m_processor->getSpectrumL(), m_displayBufferL.data(), m_peakBufferL.data());
updateBuffers(m_processor->getSpectrumR(), m_displayBufferR.data(), m_peakBufferR.data());
#ifdef SA_DEBUG
refresh_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - refresh_time;
#endif
@@ -254,41 +250,43 @@ void SaSpectrumView::refreshPaths()
}
#ifdef SA_DEBUG
int make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count();
int path_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);
m_pathL = makePath(m_displayBufferL, m_controls->m_spectrumResolutionModel.value());
if (m_controls->m_stereoModel.value())
{
m_pathR = makePath(m_displayBufferR, 1.5);
m_pathR = makePath(m_displayBufferR, m_controls->m_spectrumResolutionModel.value());
}
if (m_controls->m_peakHoldModel.value() || m_controls->m_refFreezeModel.value())
{
m_pathPeakL = makePath(m_peakBufferL, 0.25);
m_pathPeakL = makePath(m_peakBufferL, m_controls->m_envelopeResolutionModel.value());
if (m_controls->m_stereoModel.value())
{
m_pathPeakR = makePath(m_peakBufferR, 0.25);
m_pathPeakR = makePath(m_peakBufferR, m_controls->m_envelopeResolutionModel.value());
}
}
#ifdef SA_DEBUG
make_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - make_time;
path_time = std::chrono::high_resolution_clock::now().time_since_epoch().count() - path_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;
// save performance measurement results
m_refresh_avg = 0.95 * m_refresh_avg + 0.05 * refresh_time / 1000000.0;
m_path_avg = .95f * m_path_avg + .05f * path_time / 1000000.f;
#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)
// NOTE: The calling function is responsible for acquiring SaProcessor
// reallocation access lock! Data access lock is not needed: the final result
// buffer is updated very quickly and the worst case is that one frame will be
// part new, part old. At reasonable frame rate, such difference is invisible..
void SaSpectrumView::updateBuffers(const float *spectrum, float *displayBuffer, float *peakBuffer)
{
for (int n = 0; n < m_processor->binCount(); n++)
{
@@ -297,7 +295,8 @@ void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float
{
if (m_controls->m_smoothModel.value())
{
displayBuffer[n] = spectrum[n] * m_smoothFactor + displayBuffer[n] * (1 - m_smoothFactor);
const float smoothFactor = m_controls->m_averagingWeightModel.value();
displayBuffer[n] = spectrum[n] * smoothFactor + displayBuffer[n] * (1 - smoothFactor);
}
else
{
@@ -319,7 +318,7 @@ void SaSpectrumView::updateBuffers(float *spectrum, float *displayBuffer, float
}
else if (!m_controls->m_refFreezeModel.value())
{
peakBuffer[n] = peakBuffer[n] * m_peakDecayFactor;
peakBuffer[n] = peakBuffer[n] * m_controls->m_peakDecayFactorModel.value();
}
}
else if (!m_controls->m_refFreezeModel.value() && !m_controls->m_peakHoldModel.value())
@@ -539,38 +538,52 @@ void SaSpectrumView::drawGrid(QPainter &painter)
// Draw cursor and its coordinates if it is within display bounds.
void SaSpectrumView::drawCursor(QPainter &painter)
{
if( m_cursor.x() >= m_displayLeft
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());
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
// 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 HzdBFS").height() + box_margin);
unsigned int const box_width = fontMetrics.size(Qt::TextSingleLine, "-99.9 dBFS").width() + 2*box_margin;
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");
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 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);
tmps = QString("%1 Hz").arg(xFreq);
painter.drawText(m_displayLeft + box_left + box_margin,
m_displayTop + box_top + box_margin,
box_width, box_height / 2, 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());
tmps = QString(std::to_string(yAmp).substr(0, 5).c_str()).append(" dBFS");
}
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());
tmps = QString(std::to_string(0.0005f + yAmp).substr(0, 5).c_str());
}
painter.drawText(m_displayRight -60, 30, 100, 16, Qt::AlignLeft, tmps);
painter.drawText(m_displayLeft + box_left + box_margin,
m_displayTop + box_top + box_height / 2,
box_width, box_height / 2, Qt::AlignLeft, tmps);
}
}
@@ -774,14 +787,18 @@ void SaSpectrumView::periodicUpdate()
// 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 SaSpectrumView::mouseMoveEvent(QMouseEvent *event)
{
m_cursor = event->pos();
m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
}
void SaSpectrumView::mousePressEvent(QMouseEvent *event)
{
m_cursor = event->pos();
m_cursor = QPointF( event->localPos().x() - (event->windowPos().x() - (long)event->windowPos().x()),
event->localPos().y() - (event->windowPos().y() - (long)event->windowPos().y()));
}