Files
lmms/plugins/Vectorscope/VectorView.cpp
Michael Gregorius 501011e573 Vectorscope render fix (#7652)
* Fix rendering for Vectorscope

The rendering of the Vectorscope is broken on Wayland if the size of the
Vectorscope is increased. This is caused by using a `QImage` to render
the scope traces which is then scaled up.

Introduce a new way to paint the vector scope (goniometer) which simply
paints lines or points that progressively get dimmer and which does not
make use of a QImage anymore.

It supports the following features:
* Log mode
* Zooming
* Rendering the drawing performance
* Selecting a different color for the traces

It does not support:
* HQ Mode: The new implementation provides a performance that's
  equivalent to Non-HQ mode and look similar or better than the HQ mode.
* Blurring of old data
* Persistence: Might be implemented by using a factor for the dimming

Rendering of the samples/trances uses the composition mode "Plus" so
that overlapping elements will appear like adding brightness. Painting
the grid and lines is done using the normal composition mode "Source
Over" so that it simply replaces existing pixels.

Painting of the lines/points and the grids and lines is done in a
"signal space", i.e. a transform where elements in the interval of
[-1, 1] feel "natural". The text is painted in "widget space".

* Remove old implementation

Remove HQ mode and persistence. Also remove the legacy option again.
This removes the models, loading, saving and the GUI controls.

Remove all unnecessary members from `VectorView`, adjust the
constructor. Move the implementation of `paintLinesMode` into
`paintEvent`. Remove methods `paintLegacyMode` and `paintLinesMode`.

* Move colors into VectorView

Move the colors out of `VecControls` into `VectorView` as they are
related to presentation.

* Remove friend relationship to VectorView

Remove a friend relationship to `VectorView` from `VecControls` by
introducing  const getters for the models.

* Remove VectorView::m_visible

Remove the unnecessary member `m_visible` from `VectorView`. It was not
initialized and only written to but never read.

* Make Vectorscope themeable

Make the Vectorscope themeable by introducing Qt properties for the
relevant colors.

The default theme gets the values from the code whereas the classic
theme gets a trace with amber color.

* Rename m_colorFG

Rename `m_colorFG` to `m_colorTrace`. Adjust the Qt property
accordingly.

Remove local variable `traceColor` from paint method and use member
`m_colorTrace` directly.

* Remove m_colorOutline

Remove unused member `m_colorOutline`.

* Fix horizontal lines on silence

Fix the horizontal lines that are rendered on silence. They seem to be
produced when rendering lines that start and end at the same point.

Therefore we only draw a point if the current and last point are the
same.

* Add some margin to the VectorView

Add some margin to the rendering of the `VectorView` so that the circle
does not touch the bounary of the widget.

* Clean up the layout of the Vectorscope

Clean up the layout of the Vectorscope. The checkboxes are not put on
top of the vector view anymore but are organized in a horizontal layout
beneath it. This gives a much tidier look.
2025-01-22 22:40:17 +01:00

283 lines
9.2 KiB
C++

/* VectorView.cpp - implementation of VectorView class.
*
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
* Copyright (c) 2025- Michael Gregorius
*
* 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 "VectorView.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <QImage>
#include <QPainter>
#include "ColorChooser.h"
#include "GuiApplication.h"
#include "FontHelper.h"
#include "MainWindow.h"
#include "VecControls.h"
namespace lmms::gui
{
VectorView::VectorView(VecControls* controls, LocklessRingBuffer<SampleFrame>* inputBuffer, QWidget* parent) :
QWidget(parent),
m_controls(controls),
m_inputBuffer(inputBuffer),
m_bufferReader(*inputBuffer),
m_zoom(1.f),
m_zoomTimestamp(0)
{
setMinimumSize(200, 200);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
connect(getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate()));
#ifdef VEC_DEBUG
m_executionAvg = 0;
#endif
}
// Compose and draw all the content; called by Qt.
void VectorView::paintEvent(QPaintEvent *event)
{
#ifdef VEC_DEBUG
unsigned int drawTime = std::chrono::high_resolution_clock::now().time_since_epoch().count();
#endif
const bool logScale = m_controls->getLogarithmicModel().value();
const bool linesMode = m_controls->getLinesModel().value();
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
// Paint background
painter.fillRect(rect(), Qt::black);
const qreal widthF = qreal(width());
const qreal heightF = qreal(height());
const auto minOfWidthAndHeight = std::min(widthF, heightF);
// If we would divide by 4 then the circle would go to the boundaries
// of the widget. We increase the value at bit more to get some margin.
const auto scaleValue = minOfWidthAndHeight / 4.1;
// Compute several transforms that are used to paint various elements
// This transform moves the origin/center to the middle of the widget width and to the correct height
QTransform centerTransform;
centerTransform.translate(widthF / 2., minOfWidthAndHeight / 2.);
// This transform is used to center and scale the painting of data and of the grid and labels
QTransform gridAndLabelTransform(centerTransform);
// Invert the Y axis while we are at it so that we can paint in a "normal" coordinate system
gridAndLabelTransform.scale(scaleValue, -scaleValue);
// This transform is used to paint the traces. It takes the zoom factor as an "extra" scale into account as well.
QTransform tracePaintingTransform(gridAndLabelTransform);
tracePaintingTransform.scale(m_zoom, m_zoom);
const auto traceWidth = 2. / (scaleValue * m_zoom);
// This will add colors so that line intersections produce lighter colors/intensities
painter.setCompositionMode(QPainter::CompositionMode_Plus);
painter.setTransform(tracePaintingTransform);
// Get new samples from the lockless input FIFO buffer
const auto inBuffer = m_bufferReader.read_max(m_inputBuffer->capacity());
const std::size_t frameCount = inBuffer.size();
for (std::size_t frame = 0; frame < frameCount; ++frame)
{
auto sampleFrame = inBuffer[frame];
if (logScale)
{
const float distance = std::sqrt(sampleFrame.sumOfSquaredAmplitudes());
const float distanceLog = std::log10(1 + 9 * std::abs(distance));
if (distance != 0)
{
const float factor = distanceLog / distance;
sampleFrame *= factor;
}
}
// Perform a mid/side split which will potentially boost signals
//
// Represent the side by the x coordinate and the mid by the y coordinate.
//
// A mono signal which just contains a mid signal will just show as a line
// along the y axis because it carries the same information in the left and right channel.
// So we can say: left == right. So lets replace "right" with "left" in the formula below:
// (left - left, -(left + left)) = (0, -2*left).
// If two signals are completely out of phase the show as a line along the x axis. That's because
// each signal is the opposite of the other one, e.g. right = -left. Let's replace again:
// (left - (-left), -(left - left)) = (2*left, 0).
//
const auto mid = sampleFrame.left() + sampleFrame.right();
const auto side = sampleFrame.left() - sampleFrame.right();
// We negate the mid value of the coordinate so that it tilts correctly if we pan hard left and hard right
QPointF currentPoint(side, -mid);
const auto darkenedColor(m_colorTrace.darker(100 + frame));
painter.setPen(QPen(darkenedColor, traceWidth));
// Only draw a line if we can draw a line, i.e. if the point really changes.
// Otherwise just produce a point.
// Without this check Qt will draw horizontal lines when silence is processed.
if (linesMode && m_lastPoint != currentPoint)
{
painter.drawLine(QLineF(m_lastPoint, currentPoint));
}
else
{
painter.drawPoint(currentPoint);
}
m_lastPoint = currentPoint;
}
// Draw grid and labels overlay
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.setTransform(gridAndLabelTransform);
const QPointF origin(0, 0);
painter.setPen(QPen(m_colorGrid, 2.5 / scaleValue, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
painter.drawEllipse(origin, 2.f, 2.f);
const qreal root = std::sqrt(qreal(2.1));
painter.setPen(QPen(m_colorGrid, 2.5 / scaleValue, Qt::DotLine, Qt::RoundCap, Qt::BevelJoin));
painter.drawLine(origin, QPointF(-root, root));
painter.drawLine(origin, QPointF(root, root));
painter.resetTransform();
// Draw L/R text
const auto lText = QString("L");
const auto rText = QString("R");
QFont boldFont = adjustedToPixelSize(painter.font(), 26);
boldFont.setBold(true);
QFontMetrics fm(boldFont);
const auto boundingRectL = fm.boundingRect(lText);
const auto boundingRectR = fm.boundingRect(rText);
painter.setPen(QPen(m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
painter.setFont(boldFont);
QTransform transformL(centerTransform);
transformL.rotate(-45.);
transformL.translate(-boundingRectL.width() / 2, -(minOfWidthAndHeight / 2) - 10);
painter.setTransform(transformL);
painter.drawText(0, 0, lText);
QTransform transformR(centerTransform);
transformR.rotate(45.);
transformR.translate(-boundingRectR.width() / 2, -(minOfWidthAndHeight / 2) - 10);
painter.setTransform(transformR);
painter.drawText(0, 0, rText);
drawZoomInfo();
// Optionally measure drawing performance
#ifdef VEC_DEBUG
QPainter debugPainter(this);
drawTime = std::chrono::high_resolution_clock::now().time_since_epoch().count() - drawTime;
m_executionAvg = 0.95f * m_executionAvg + 0.05f * drawTime / 1000000.f;
QString debugText = tr("Exec avg.: %1 ms").arg(static_cast<int>(round(m_executionAvg)));
debugPainter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
debugPainter.drawText(0, height(), debugText);
#endif
}
// Periodically trigger repaint and check if the widget is visible
void VectorView::periodicUpdate()
{
if (isVisible())
{
update();
}
}
// Allow to change color on double-click.
// More of an Easter egg, to avoid cluttering the interface with non-essential functionality.
void VectorView::mouseDoubleClickEvent(QMouseEvent *event)
{
auto colorDialog = new ColorChooser(m_colorTrace, this);
if (colorDialog->exec())
{
m_colorTrace = colorDialog->currentColor();
}
}
// Change zoom level using the mouse wheel
void VectorView::wheelEvent(QWheelEvent *event)
{
// Go through integers to avoid accumulating errors
const unsigned short old_zoom = round(100 * m_zoom);
// Min-max bounds are 20 and 1000 %, step for 15°-increment mouse wheel is 20 %
const unsigned short new_zoom = qBound(20, old_zoom + event->angleDelta().y() / 6, 1000);
m_zoom = new_zoom / 100.f;
event->accept();
m_zoomTimestamp = std::chrono::duration_cast<std::chrono::milliseconds>
(
std::chrono::high_resolution_clock::now().time_since_epoch()
).count();
}
void VectorView::drawZoomInfo()
{
const unsigned int currentTimestamp = std::chrono::duration_cast<std::chrono::milliseconds>
(
std::chrono::high_resolution_clock::now().time_since_epoch()
).count();
if (currentTimestamp - m_zoomTimestamp < 1000)
{
QPainter painter(this);
const auto zoomValue = static_cast<int>(std::round(m_zoom * 100.));
const auto text = tr("Zoom: %1 %").arg(zoomValue);
// Measure text
const auto fm = painter.fontMetrics();
const auto boundingRect = fm.boundingRect(text);
const auto descent = fm.descent();
painter.setPen(QPen(m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
painter.drawText((width() - boundingRect.width()) / 2, height() - descent - 2, text);
}
}
} // namespace lmms::gui