mirror of
https://github.com/LMMS/lmms.git
synced 2026-05-24 06:37:52 -04:00
Add the vectorscope plugin (#5328)
The credit for the `ColorChooser` class goes to CYBERDEViLNL.
This commit is contained in:
@@ -64,6 +64,7 @@ SET(LMMS_PLUGIN_LIST
|
||||
VstEffect
|
||||
watsyn
|
||||
waveshaper
|
||||
Vectorscope
|
||||
vibed
|
||||
Xpressive
|
||||
zynaddsubfx
|
||||
|
||||
41
include/ColorChooser.h
Normal file
41
include/ColorChooser.h
Normal file
@@ -0,0 +1,41 @@
|
||||
/* ColorChooser.h - declaration and definition of ColorChooser class.
|
||||
*
|
||||
* Copyright (c) 2019 CYBERDEViLNL <cyberdevilnl/at/protonmail/dot/ch>
|
||||
*
|
||||
* 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 <QColorDialog>
|
||||
#include <QApplication>
|
||||
#include <QKeyEvent>
|
||||
|
||||
class ColorChooser: public QColorDialog
|
||||
{
|
||||
public:
|
||||
ColorChooser(const QColor &initial, QWidget *parent): QColorDialog(initial, parent) {};
|
||||
ColorChooser(QWidget *parent): QColorDialog(parent) {};
|
||||
|
||||
protected:
|
||||
// Forward key events to the parent to prevent stuck notes when the dialog gets focus
|
||||
void keyReleaseEvent(QKeyEvent *event) override
|
||||
{
|
||||
QKeyEvent ke(*event);
|
||||
QApplication::sendEvent(parentWidget(), &ke);
|
||||
}
|
||||
};
|
||||
@@ -81,13 +81,13 @@ private:
|
||||
FloatModel m_zeroPaddingModel;
|
||||
|
||||
// colors (hard-coded, values must add up to specific numbers)
|
||||
QColor m_colorL; //!< color of the left channel
|
||||
QColor m_colorR; //!< color of the right channel
|
||||
QColor m_colorMono; //!< mono color for spectrum display
|
||||
QColor m_colorMonoW; //!< mono color for waterfall display
|
||||
QColor m_colorBG; //!< spectrum display background color
|
||||
QColor m_colorGrid; //!< color of grid lines
|
||||
QColor m_colorLabels; //!< color of axis labels
|
||||
QColor m_colorL; //!< color of the left channel
|
||||
QColor m_colorR; //!< color of the right channel
|
||||
QColor m_colorMono; //!< mono color for spectrum display
|
||||
QColor m_colorMonoW; //!< mono color for waterfall display
|
||||
QColor m_colorBG; //!< spectrum display background color
|
||||
QColor m_colorGrid; //!< color of grid lines
|
||||
QColor m_colorLabels; //!< color of axis labels
|
||||
|
||||
friend class SaControlsDialog;
|
||||
friend class SaSpectrumView;
|
||||
|
||||
3
plugins/Vectorscope/CMakeLists.txt
Normal file
3
plugins/Vectorscope/CMakeLists.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
INCLUDE(BuildPlugin)
|
||||
BUILD_PLUGIN(vectorscope Vectorscope.cpp VecControls.cpp VecControlsDialog.cpp VectorView.cpp
|
||||
MOCFILES VecControls.h VecControlsDialog.h VectorView.h EMBEDDED_RESOURCES logo.png)
|
||||
14
plugins/Vectorscope/README.md
Normal file
14
plugins/Vectorscope/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Vectorscope plugin
|
||||
|
||||
## Overview
|
||||
|
||||
Vectorscope is a simple stereo field visualizer. Samples are plotted into a graph, with left and right channels providing the coordinates. Previously drawn samples quickly fade away and are continuously replaced by new samples, creating a real-time plot of the most recently played samples.
|
||||
|
||||
Similar to other effect plugins, the top-level widget is VecControlDialog. It displays configuration knobs and the main VectorView widget. The back-end configuration class is VecControls, which holds all models and configuration values.
|
||||
|
||||
VectorView computes and shows the plot. It gets data for processing from the Vectorscope class, which handles the interface with LMMS. In order to avoid any stalling of the realtime-sensitive audio thread, data are exchanged through a lockless ring buffer.
|
||||
|
||||
## Changelog
|
||||
|
||||
1.0.0 2019-11-21
|
||||
- initial release
|
||||
70
plugins/Vectorscope/VecControls.cpp
Normal file
70
plugins/Vectorscope/VecControls.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* VecControls.cpp - definition of VecControls class.
|
||||
*
|
||||
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
|
||||
*
|
||||
* 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 "VecControls.h"
|
||||
|
||||
#include <QtXml/QDomElement>
|
||||
|
||||
#include "VecControlsDialog.h"
|
||||
#include "Vectorscope.h"
|
||||
|
||||
|
||||
VecControls::VecControls(Vectorscope *effect) :
|
||||
EffectControls(effect),
|
||||
m_effect(effect),
|
||||
|
||||
// initialize models and set default values
|
||||
m_persistenceModel(0.5f, 0.0f, 1.0f, 0.05f, this, tr("Display persistence amount")),
|
||||
m_logarithmicModel(false, this, tr("Logarithmic scale")),
|
||||
m_highQualityModel(false, this, tr("High quality"))
|
||||
{
|
||||
// Colors (percentages include sRGB gamma correction)
|
||||
m_colorFG = QColor(60, 255, 130, 255); // ~LMMS green
|
||||
m_colorGrid = QColor(76, 80, 84, 128); // ~60 % gray (slightly cold / blue), 50 % transparent
|
||||
m_colorLabels = QColor(76, 80, 84, 255); // ~60 % gray (slightly cold / blue)
|
||||
m_colorOutline = QColor(30, 34, 38, 255); // ~40 % gray (slightly cold / blue)
|
||||
}
|
||||
|
||||
|
||||
// Create the VecControlDialog widget which handles display of GUI elements.
|
||||
EffectControlDialog* VecControls::createView()
|
||||
{
|
||||
return new VecControlsDialog(this);
|
||||
}
|
||||
|
||||
|
||||
void VecControls::loadSettings(const QDomElement &element)
|
||||
{
|
||||
m_persistenceModel.loadSettings(element, "Persistence");
|
||||
m_logarithmicModel.loadSettings(element, "Logarithmic");
|
||||
m_highQualityModel.loadSettings(element, "HighQuality");
|
||||
}
|
||||
|
||||
|
||||
void VecControls::saveSettings(QDomDocument &document, QDomElement &element)
|
||||
{
|
||||
m_persistenceModel.saveSettings(document, element, "Persistence");
|
||||
m_logarithmicModel.saveSettings(document, element, "Logarithmic");
|
||||
m_highQualityModel.saveSettings(document, element, "HighQuality");
|
||||
}
|
||||
66
plugins/Vectorscope/VecControls.h
Normal file
66
plugins/Vectorscope/VecControls.h
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* VecControls.h - declaration of VecControls class.
|
||||
*
|
||||
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef VECCONTROLS_H
|
||||
#define VECCONTROLS_H
|
||||
|
||||
#include <QColor>
|
||||
|
||||
#include "EffectControls.h"
|
||||
|
||||
|
||||
class Vectorscope;
|
||||
|
||||
// Holds all the configuration values
|
||||
class VecControls : public EffectControls
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VecControls(Vectorscope *effect);
|
||||
virtual ~VecControls() {}
|
||||
|
||||
EffectControlDialog *createView() override;
|
||||
|
||||
void saveSettings (QDomDocument &document, QDomElement &element) override;
|
||||
void loadSettings (const QDomElement &element) override;
|
||||
|
||||
QString nodeName() const override {return "Vectorscope";}
|
||||
int controlCount() override {return 3;}
|
||||
|
||||
private:
|
||||
Vectorscope *m_effect;
|
||||
|
||||
FloatModel m_persistenceModel;
|
||||
BoolModel m_logarithmicModel;
|
||||
BoolModel m_highQualityModel;
|
||||
|
||||
QColor m_colorFG;
|
||||
QColor m_colorGrid;
|
||||
QColor m_colorLabels;
|
||||
QColor m_colorOutline;
|
||||
|
||||
friend class VecControlsDialog;
|
||||
friend class VectorView;
|
||||
};
|
||||
#endif // VECCONTROLS_H
|
||||
94
plugins/Vectorscope/VecControlsDialog.cpp
Normal file
94
plugins/Vectorscope/VecControlsDialog.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* VecControlsDialog.cpp - definition of VecControlsDialog class.
|
||||
*
|
||||
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
|
||||
*
|
||||
* 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 "VecControlsDialog.h"
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QResizeEvent>
|
||||
#include <QSizePolicy>
|
||||
#include <QWidget>
|
||||
|
||||
#include "embed.h"
|
||||
#include "LedCheckbox.h"
|
||||
#include "VecControls.h"
|
||||
#include "Vectorscope.h"
|
||||
#include "VectorView.h"
|
||||
|
||||
|
||||
// The entire GUI layout is built here.
|
||||
VecControlsDialog::VecControlsDialog(VecControls *controls) :
|
||||
EffectControlDialog(controls),
|
||||
m_controls(controls)
|
||||
{
|
||||
QVBoxLayout *master_layout = new QVBoxLayout;
|
||||
master_layout->setContentsMargins(0, 2, 0, 0);
|
||||
setLayout(master_layout);
|
||||
|
||||
// Visualizer widget
|
||||
// The size of 768 pixels seems to offer a good balance of speed, accuracy and trace thickness.
|
||||
VectorView *display = new VectorView(controls, m_controls->m_effect->getBuffer(), 768, this);
|
||||
master_layout->addWidget(display);
|
||||
|
||||
// Config area located inside visualizer
|
||||
QVBoxLayout *internal_layout = new QVBoxLayout(display);
|
||||
QHBoxLayout *config_layout = new QHBoxLayout();
|
||||
QVBoxLayout *switch_layout = new QVBoxLayout();
|
||||
internal_layout->addStretch();
|
||||
internal_layout->addLayout(config_layout);
|
||||
config_layout->addLayout(switch_layout);
|
||||
|
||||
// High-quality switch
|
||||
LedCheckBox *highQualityButton = new LedCheckBox(tr("HQ"), this);
|
||||
highQualityButton->setToolTip(tr("Double the resolution and simulate continuous analog-like trace."));
|
||||
highQualityButton->setCheckable(true);
|
||||
highQualityButton->setMinimumSize(70, 12);
|
||||
highQualityButton->setModel(&controls->m_highQualityModel);
|
||||
switch_layout->addWidget(highQualityButton);
|
||||
|
||||
// Log. scale switch
|
||||
LedCheckBox *logarithmicButton = new LedCheckBox(tr("Log. scale"), this);
|
||||
logarithmicButton->setToolTip(tr("Display amplitude on logarithmic scale to better see small values."));
|
||||
logarithmicButton->setCheckable(true);
|
||||
logarithmicButton->setMinimumSize(70, 12);
|
||||
logarithmicButton->setModel(&controls->m_logarithmicModel);
|
||||
switch_layout->addWidget(logarithmicButton);
|
||||
|
||||
config_layout->addStretch();
|
||||
|
||||
// Persistence knob
|
||||
Knob *persistenceKnob = new Knob(knobSmall_17, this);
|
||||
persistenceKnob->setModel(&controls->m_persistenceModel);
|
||||
persistenceKnob->setLabel(tr("Persist."));
|
||||
persistenceKnob->setToolTip(tr("Trace persistence: higher amount means the trace will stay bright for longer time."));
|
||||
persistenceKnob->setHintText(tr("Trace persistence"), "");
|
||||
config_layout->addWidget(persistenceKnob);
|
||||
}
|
||||
|
||||
|
||||
// Suggest the best widget size.
|
||||
QSize VecControlsDialog::sizeHint() const
|
||||
{
|
||||
return QSize(275, 300);
|
||||
}
|
||||
47
plugins/Vectorscope/VecControlsDialog.h
Normal file
47
plugins/Vectorscope/VecControlsDialog.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* VecControlsDialog.h - declatation of VecControlsDialog class.
|
||||
*
|
||||
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef VECCONTROLSDIALOG_H
|
||||
#define VECCONTROLSDIALOG_H
|
||||
|
||||
#include "EffectControlDialog.h"
|
||||
|
||||
class VecControls;
|
||||
|
||||
//! Top-level widget holding the configuration GUI and vector display
|
||||
class VecControlsDialog : public EffectControlDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VecControlsDialog(VecControls *controls);
|
||||
virtual ~VecControlsDialog() {}
|
||||
|
||||
bool isResizable() const override {return true;}
|
||||
QSize sizeHint() const override;
|
||||
|
||||
private:
|
||||
VecControls *m_controls;
|
||||
};
|
||||
|
||||
#endif // VECCONTROLSDIALOG_H
|
||||
328
plugins/Vectorscope/VectorView.cpp
Normal file
328
plugins/Vectorscope/VectorView.cpp
Normal file
@@ -0,0 +1,328 @@
|
||||
/* VectorView.cpp - implementation of VectorView class.
|
||||
*
|
||||
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
|
||||
*
|
||||
* 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 "MainWindow.h"
|
||||
|
||||
|
||||
VectorView::VectorView(VecControls *controls, LocklessRingBuffer<sampleFrame> *inputBuffer, unsigned short displaySize, QWidget *parent) :
|
||||
QWidget(parent),
|
||||
m_controls(controls),
|
||||
m_inputBuffer(inputBuffer),
|
||||
m_bufferReader(*inputBuffer),
|
||||
m_displaySize(displaySize),
|
||||
m_zoom(1.f),
|
||||
m_persistTimestamp(0),
|
||||
m_zoomTimestamp(0),
|
||||
m_oldHQ(m_controls->m_highQualityModel.value()),
|
||||
m_oldX(m_displaySize / 2),
|
||||
m_oldY(m_displaySize / 2)
|
||||
{
|
||||
setMinimumSize(200, 200);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
|
||||
connect(gui->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate()));
|
||||
|
||||
m_displayBuffer.resize(sizeof qRgb(0,0,0) * m_displaySize * m_displaySize, 0);
|
||||
|
||||
#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
|
||||
|
||||
// All drawing done in this method, local variables are sufficient for the boundary
|
||||
const int displayTop = 2;
|
||||
const int displayBottom = height() - 2;
|
||||
const int displayLeft = 2;
|
||||
const int displayRight = width() - 2;
|
||||
const int displayWidth = displayRight - displayLeft;
|
||||
const int displayHeight = displayBottom - displayTop;
|
||||
|
||||
const float centerX = displayLeft + (displayWidth / 2.f);
|
||||
const float centerY = displayTop + (displayWidth / 2.f);
|
||||
|
||||
const int margin = 4;
|
||||
const int gridCorner = 30;
|
||||
|
||||
// Setup QPainter and font sizes
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
QFont normalFont, boldFont;
|
||||
boldFont.setPixelSize(26);
|
||||
boldFont.setBold(true);
|
||||
const int labelWidth = 26;
|
||||
const int labelHeight = 26;
|
||||
|
||||
bool hq = m_controls->m_highQualityModel.value();
|
||||
|
||||
// Clear display buffer if quality setting was changed
|
||||
if (hq != m_oldHQ)
|
||||
{
|
||||
m_oldHQ = hq;
|
||||
for (std::size_t i = 0; i < m_displayBuffer.size(); i++)
|
||||
{
|
||||
m_displayBuffer.data()[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Dim stored image based on persistence setting and elapsed time.
|
||||
// Update period is limited to 50 ms (20 FPS) for non-HQ mode and 10 ms (100 FPS) for HQ mode.
|
||||
const unsigned int currentTimestamp = std::chrono::duration_cast<std::chrono::milliseconds>
|
||||
(
|
||||
std::chrono::high_resolution_clock::now().time_since_epoch()
|
||||
).count();
|
||||
const unsigned int elapsed = currentTimestamp - m_persistTimestamp;
|
||||
const unsigned int threshold = hq ? 10 : 50;
|
||||
if (elapsed > threshold)
|
||||
{
|
||||
m_persistTimestamp = currentTimestamp;
|
||||
// Non-HQ mode uses half the resolution → use limited buffer space.
|
||||
const std::size_t useableBuffer = hq ? m_displayBuffer.size() : m_displayBuffer.size() / 4;
|
||||
// The knob value is interpreted on log. scale, otherwise the effect would ramp up too slowly.
|
||||
// Persistence value specifies fraction of light intensity that remains after 10 ms.
|
||||
// → Compensate it based on elapsed time (exponential decay).
|
||||
const float persist = log10(1 + 9 * m_controls->m_persistenceModel.value());
|
||||
const float persistPerFrame = pow(persist, elapsed / 10.f);
|
||||
// Note that for simplicity and performance reasons, this implementation only dims all stored
|
||||
// values by a given factor. A true simulation would also do the inverse of desaturation that
|
||||
// occurs in high-intensity traces in HQ mode.
|
||||
for (std::size_t i = 0; i < useableBuffer; i++)
|
||||
{
|
||||
m_displayBuffer.data()[i] *= persistPerFrame;
|
||||
}
|
||||
}
|
||||
|
||||
// Get new samples from the lockless input FIFO buffer
|
||||
auto inBuffer = m_bufferReader.read_max(m_inputBuffer->capacity());
|
||||
std::size_t frameCount = inBuffer.size();
|
||||
|
||||
// Draw new points on top
|
||||
float left, right;
|
||||
int x, y;
|
||||
|
||||
const bool logScale = m_controls->m_logarithmicModel.value();
|
||||
const unsigned short activeSize = hq ? m_displaySize : m_displaySize / 2;
|
||||
|
||||
// Helper lambda functions for better readability
|
||||
// Make sure pixel stays within display bounds:
|
||||
auto saturate = [=](short pixelPos) {return qBound((short)0, pixelPos, (short)(activeSize - 1));};
|
||||
// Take existing pixel and brigthen it. Very bright light should reduce saturation and become
|
||||
// white. This effect is easily approximated by capping elementary colors to 255 individually.
|
||||
auto updatePixel = [&](unsigned short x, unsigned short y, QColor addedColor)
|
||||
{
|
||||
QColor currentColor = ((QRgb*)m_displayBuffer.data())[x + y * activeSize];
|
||||
currentColor.setRed(std::min(currentColor.red() + addedColor.red(), 255));
|
||||
currentColor.setGreen(std::min(currentColor.green() + addedColor.green(), 255));
|
||||
currentColor.setBlue(std::min(currentColor.blue() + addedColor.blue(), 255));
|
||||
((QRgb*)m_displayBuffer.data())[x + y * activeSize] = currentColor.rgb();
|
||||
};
|
||||
|
||||
if (hq)
|
||||
{
|
||||
// High quality mode: check distance between points and draw a line.
|
||||
// The longer the line is, the dimmer, simulating real electron trace on luminescent screen.
|
||||
for (std::size_t frame = 0; frame < frameCount; frame++)
|
||||
{
|
||||
float inLeft = inBuffer[frame][0] * m_zoom;
|
||||
float inRight = inBuffer[frame][1] * m_zoom;
|
||||
// Scale left and right channel from (-1.0, 1.0) to display range
|
||||
if (logScale)
|
||||
{
|
||||
// To better preserve shapes, the log scale is applied to the distance from origin,
|
||||
// not the individual channels.
|
||||
const float distance = sqrt(inLeft * inLeft + inRight * inRight);
|
||||
const float distanceLog = log10(1 + 9 * abs(distance));
|
||||
const float angleCos = inLeft / distance;
|
||||
const float angleSin = inRight / distance;
|
||||
left = distanceLog * angleCos * (activeSize - 1) / 4;
|
||||
right = distanceLog * angleSin * (activeSize - 1) / 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
left = inLeft * (activeSize - 1) / 4;
|
||||
right = inRight * (activeSize - 1) / 4;
|
||||
}
|
||||
|
||||
// Rotate display coordinates 45 degrees, flip Y axis and make sure the result stays within bounds
|
||||
x = saturate(right - left + activeSize / 2.f);
|
||||
y = saturate(activeSize - (right + left + activeSize / 2.f));
|
||||
|
||||
// Estimate number of points needed to fill space between the old and new pixel. Cap at 100.
|
||||
unsigned char points = std::min((int)sqrt((m_oldX - x) * (m_oldX - x) + (m_oldY - y) * (m_oldY - y)), 100);
|
||||
|
||||
// Large distance = dim trace. The curve for darker() is choosen so that:
|
||||
// - no movement (0 points) actually _increases_ brightness slightly,
|
||||
// - one point between samples = returns exactly the specified color,
|
||||
// - one to 99 points between samples = follows a sharp "1/x" decaying curve,
|
||||
// - 100 points between samples = returns approximately 5 % brightness.
|
||||
// Everything else is discarded (by the 100 point cap) because there is not much to see anyway.
|
||||
QColor addedColor = m_controls->m_colorFG.darker(75 + 20 * points).rgb();
|
||||
|
||||
// Draw the new pixel: the beam sweeps across area that may have been excited before
|
||||
// → add new value to existing pixel state.
|
||||
updatePixel(x, y, addedColor);
|
||||
|
||||
// Draw interpolated points between the old pixel and the new one
|
||||
int newX = right - left + activeSize / 2.f;
|
||||
int newY = activeSize - (right + left + activeSize / 2.f);
|
||||
for (unsigned char i = 1; i < points; i++)
|
||||
{
|
||||
x = saturate(((points - i) * m_oldX + i * newX) / points);
|
||||
y = saturate(((points - i) * m_oldY + i * newY) / points);
|
||||
updatePixel(x, y, addedColor);
|
||||
}
|
||||
m_oldX = newX;
|
||||
m_oldY = newY;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// To improve performance, non-HQ mode uses smaller display size and only
|
||||
// one full-color pixel per sample.
|
||||
for (std::size_t frame = 0; frame < frameCount; frame++)
|
||||
{
|
||||
float inLeft = inBuffer[frame][0] * m_zoom;
|
||||
float inRight = inBuffer[frame][1] * m_zoom;
|
||||
if (logScale) {
|
||||
const float distance = sqrt(inLeft * inLeft + inRight * inRight);
|
||||
const float distanceLog = log10(1 + 9 * abs(distance));
|
||||
const float angleCos = inLeft / distance;
|
||||
const float angleSin = inRight / distance;
|
||||
left = distanceLog * angleCos * (activeSize - 1) / 4;
|
||||
right = distanceLog * angleSin * (activeSize - 1) / 4;
|
||||
} else {
|
||||
left = inLeft * (activeSize - 1) / 4;
|
||||
right = inRight * (activeSize - 1) / 4;
|
||||
}
|
||||
x = saturate(right - left + activeSize / 2.f);
|
||||
y = saturate(activeSize - (right + left + activeSize / 2.f));
|
||||
((QRgb*)m_displayBuffer.data())[x + y * activeSize] = m_controls->m_colorFG.rgb();
|
||||
}
|
||||
}
|
||||
|
||||
// Draw background
|
||||
painter.fillRect(displayLeft, displayTop, displayWidth, displayHeight, QColor(0,0,0));
|
||||
|
||||
// Draw the final image
|
||||
QImage temp = QImage(m_displayBuffer.data(),
|
||||
activeSize,
|
||||
activeSize,
|
||||
QImage::Format_RGB32);
|
||||
temp.setDevicePixelRatio(devicePixelRatio());
|
||||
painter.drawImage(displayLeft, displayTop,
|
||||
temp.scaledToWidth(displayWidth * devicePixelRatio(),
|
||||
Qt::SmoothTransformation));
|
||||
|
||||
// Draw the grid and labels
|
||||
painter.setPen(QPen(m_controls->m_colorGrid, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
||||
painter.drawEllipse(QPointF(centerX, centerY), displayWidth / 2.f, displayWidth / 2.f);
|
||||
painter.setPen(QPen(m_controls->m_colorGrid, 1.5, Qt::DotLine, Qt::RoundCap, Qt::BevelJoin));
|
||||
painter.drawLine(QPointF(centerX, centerY), QPointF(displayLeft + gridCorner, displayTop + gridCorner));
|
||||
painter.drawLine(QPointF(centerX, centerY), QPointF(displayRight - gridCorner, displayTop + gridCorner));
|
||||
|
||||
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
||||
painter.setFont(boldFont);
|
||||
painter.drawText(displayLeft + margin, displayTop,
|
||||
labelWidth, labelHeight, Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip,
|
||||
QString("L"));
|
||||
painter.drawText(displayRight - margin - labelWidth, displayTop,
|
||||
labelWidth, labelHeight, Qt::AlignRight| Qt::AlignTop | Qt::TextDontClip,
|
||||
QString("R"));
|
||||
|
||||
// Draw the outline
|
||||
painter.setPen(QPen(m_controls->m_colorOutline, 2, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
||||
painter.drawRoundedRect(1, 1, width() - 2, height() - 2, 2.f, 2.f);
|
||||
|
||||
// Draw zoom info if changed within last second (re-using timestamp acquired for dimming)
|
||||
if (currentTimestamp - m_zoomTimestamp < 1000)
|
||||
{
|
||||
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
||||
painter.setFont(normalFont);
|
||||
painter.drawText(displayWidth / 2 - 50, displayBottom - 20, 100, 16, Qt::AlignCenter,
|
||||
QString("Zoom: ").append(std::to_string((int)round(m_zoom * 100)).c_str()).append(" %"));
|
||||
}
|
||||
|
||||
// Optionally measure drawing performance
|
||||
#ifdef VEC_DEBUG
|
||||
drawTime = std::chrono::high_resolution_clock::now().time_since_epoch().count() - drawTime;
|
||||
m_executionAvg = 0.95f * m_executionAvg + 0.05f * drawTime / 1000000.f;
|
||||
painter.setPen(QPen(m_controls->m_colorLabels, 1, Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
|
||||
painter.setFont(normalFont);
|
||||
painter.drawText(displayWidth / 2 - 50, displayBottom - 16, 100, 16, Qt::AlignLeft,
|
||||
QString("Exec avg.: ").append(std::to_string(m_executionAvg).substr(0, 5).c_str()).append(" ms"));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
// Periodically trigger repaint and check if the widget is visible
|
||||
void VectorView::periodicUpdate()
|
||||
{
|
||||
m_visible = isVisible();
|
||||
if (m_visible) {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)
|
||||
{
|
||||
ColorChooser *colorDialog = new ColorChooser(m_controls->m_colorFG, this);
|
||||
if (colorDialog->exec())
|
||||
{
|
||||
m_controls->m_colorFG = 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();
|
||||
|
||||
}
|
||||
80
plugins/Vectorscope/VectorView.h
Normal file
80
plugins/Vectorscope/VectorView.h
Normal file
@@ -0,0 +1,80 @@
|
||||
/* VectorView.h - declaration of VectorView class.
|
||||
*
|
||||
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
#ifndef VECTORVIEW_H
|
||||
#define VECTORVIEW_H
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <QWidget>
|
||||
|
||||
#include "Knob.h"
|
||||
#include "LedCheckbox.h"
|
||||
#include "LocklessRingBuffer.h"
|
||||
#include "VecControls.h"
|
||||
|
||||
//#define VEC_DEBUG
|
||||
|
||||
|
||||
// Widget that displays a vectorscope visualization of stereo signal.
|
||||
class VectorView : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VectorView(VecControls *controls, LocklessRingBuffer<sampleFrame> *inputBuffer, unsigned short displaySize, QWidget *parent = 0);
|
||||
virtual ~VectorView() {}
|
||||
|
||||
QSize sizeHint() const override {return QSize(300, 300);}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void periodicUpdate();
|
||||
|
||||
private:
|
||||
VecControls *m_controls;
|
||||
|
||||
LocklessRingBuffer<sampleFrame> *m_inputBuffer;
|
||||
LocklessRingBufferReader<sampleFrame> m_bufferReader;
|
||||
|
||||
std::vector<uchar> m_displayBuffer;
|
||||
const unsigned short m_displaySize;
|
||||
|
||||
bool m_visible;
|
||||
|
||||
float m_zoom;
|
||||
|
||||
// State variables for comparison with previous repaint
|
||||
unsigned int m_persistTimestamp;
|
||||
unsigned int m_zoomTimestamp;
|
||||
bool m_oldHQ;
|
||||
int m_oldX;
|
||||
int m_oldY;
|
||||
|
||||
#ifdef VEC_DEBUG
|
||||
float m_executionAvg = 0;
|
||||
#endif
|
||||
};
|
||||
#endif // VECTORVIEW_H
|
||||
80
plugins/Vectorscope/Vectorscope.cpp
Normal file
80
plugins/Vectorscope/Vectorscope.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Vectorscope.cpp - definition of Vectorscope class.
|
||||
*
|
||||
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
|
||||
*
|
||||
* 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 "Vectorscope.h"
|
||||
|
||||
#include "embed.h"
|
||||
#include "plugin_export.h"
|
||||
|
||||
|
||||
extern "C" {
|
||||
Plugin::Descriptor PLUGIN_EXPORT vectorscope_plugin_descriptor =
|
||||
{
|
||||
STRINGIFY(PLUGIN_NAME),
|
||||
"Vectorscope",
|
||||
QT_TRANSLATE_NOOP("pluginBrowser", "A stereo field visualizer."),
|
||||
"Martin Pavelek <he29/dot/HS/at/gmail/dot/com>",
|
||||
0x0100,
|
||||
Plugin::Effect,
|
||||
new PluginPixmapLoader("logo"),
|
||||
NULL,
|
||||
NULL
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Vectorscope::Vectorscope(Model *parent, const Plugin::Descriptor::SubPluginFeatures::Key *key) :
|
||||
Effect(&vectorscope_plugin_descriptor, parent, key),
|
||||
m_controls(this),
|
||||
// Buffer is sized to cover 4* the current maximum LMMS audio buffer size,
|
||||
// so that it has some reserve space in case GUI thresd is busy.
|
||||
m_inputBuffer(4 * m_maxBufferSize)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
// Take audio data and store them for processing and display in the GUI thread.
|
||||
bool Vectorscope::processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count)
|
||||
{
|
||||
if (!isEnabled() || !isRunning ()) {return false;}
|
||||
|
||||
// Skip processing if the controls dialog isn't visible, it would only waste CPU cycles.
|
||||
if (m_controls.isViewVisible())
|
||||
{
|
||||
// To avoid processing spikes on audio thread, data are stored in
|
||||
// a lockless ringbuffer and processed in a separate thread.
|
||||
m_inputBuffer.write(buffer, frame_count);
|
||||
}
|
||||
return isRunning();
|
||||
}
|
||||
|
||||
|
||||
extern "C" {
|
||||
// needed for getting plugin out of shared lib
|
||||
PLUGIN_EXPORT Plugin *lmms_plugin_main(Model *parent, void *data)
|
||||
{
|
||||
return new Vectorscope(parent, static_cast<const Plugin::Descriptor::SubPluginFeatures::Key *>(data));
|
||||
}
|
||||
}
|
||||
|
||||
52
plugins/Vectorscope/Vectorscope.h
Normal file
52
plugins/Vectorscope/Vectorscope.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/* Vectorscope.h - declaration of Vectorscope class.
|
||||
*
|
||||
* Copyright (c) 2019 Martin Pavelek <he29/dot/HS/at/gmail/dot/com>
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef VECTORSCOPE_H
|
||||
#define VECTORSCOPE_H
|
||||
|
||||
#include "Effect.h"
|
||||
#include "LocklessRingBuffer.h"
|
||||
#include "VecControls.h"
|
||||
|
||||
|
||||
//! Top level class; handles LMMS interface and accumulates data for processing.
|
||||
class Vectorscope : public Effect
|
||||
{
|
||||
public:
|
||||
Vectorscope(Model *parent, const Descriptor::SubPluginFeatures::Key *key);
|
||||
virtual ~Vectorscope() {};
|
||||
|
||||
bool processAudioBuffer(sampleFrame *buffer, const fpp_t frame_count) override;
|
||||
EffectControls *controls() override {return &m_controls;}
|
||||
LocklessRingBuffer<sampleFrame> *getBuffer() {return &m_inputBuffer;}
|
||||
|
||||
private:
|
||||
VecControls m_controls;
|
||||
|
||||
// Maximum LMMS buffer size (hard coded, the actual constant is hard to get)
|
||||
const unsigned int m_maxBufferSize = 4096;
|
||||
LocklessRingBuffer<sampleFrame> m_inputBuffer;
|
||||
};
|
||||
|
||||
#endif // VECTORSCOPE_H
|
||||
|
||||
BIN
plugins/Vectorscope/logo.png
Normal file
BIN
plugins/Vectorscope/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 774 B |
Reference in New Issue
Block a user