diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index c82bba3291a..2d853038873 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -64,6 +64,7 @@ SET(LMMS_PLUGIN_LIST VstEffect watsyn waveshaper + Vectorscope vibed Xpressive zynaddsubfx diff --git a/include/ColorChooser.h b/include/ColorChooser.h new file mode 100644 index 00000000000..fe5b7a22a4e --- /dev/null +++ b/include/ColorChooser.h @@ -0,0 +1,41 @@ +/* ColorChooser.h - declaration and definition of ColorChooser class. + * + * Copyright (c) 2019 CYBERDEViLNL + * + * 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 +#include +#include + +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); + } +}; diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index 3b18dd475d6..d313fd72288 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -122,6 +122,7 @@ class LocklessRingBufferReader : public ringbuffer_reader_t void waitForData() { QMutex useless_lock; + useless_lock.lock(); m_notifier->wait(&useless_lock); useless_lock.unlock(); } diff --git a/plugins/SpectrumAnalyzer/SaControls.h b/plugins/SpectrumAnalyzer/SaControls.h index 4673416bc20..ee8a9e001fe 100644 --- a/plugins/SpectrumAnalyzer/SaControls.h +++ b/plugins/SpectrumAnalyzer/SaControls.h @@ -81,13 +81,13 @@ class SaControls : public EffectControls 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; diff --git a/plugins/Vectorscope/CMakeLists.txt b/plugins/Vectorscope/CMakeLists.txt new file mode 100644 index 00000000000..b73ff76d589 --- /dev/null +++ b/plugins/Vectorscope/CMakeLists.txt @@ -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) diff --git a/plugins/Vectorscope/README.md b/plugins/Vectorscope/README.md new file mode 100644 index 00000000000..18b218f6d9d --- /dev/null +++ b/plugins/Vectorscope/README.md @@ -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 diff --git a/plugins/Vectorscope/VecControls.cpp b/plugins/Vectorscope/VecControls.cpp new file mode 100644 index 00000000000..0e7a2d06160 --- /dev/null +++ b/plugins/Vectorscope/VecControls.cpp @@ -0,0 +1,70 @@ +/* + * VecControls.cpp - definition of VecControls class. + * + * Copyright (c) 2019 Martin Pavelek + * + * 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 + +#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"); +} diff --git a/plugins/Vectorscope/VecControls.h b/plugins/Vectorscope/VecControls.h new file mode 100644 index 00000000000..04b688e5a5b --- /dev/null +++ b/plugins/Vectorscope/VecControls.h @@ -0,0 +1,66 @@ +/* + * VecControls.h - declaration of VecControls class. + * + * Copyright (c) 2019 Martin Pavelek + * + * 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 + +#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 diff --git a/plugins/Vectorscope/VecControlsDialog.cpp b/plugins/Vectorscope/VecControlsDialog.cpp new file mode 100644 index 00000000000..9916d775605 --- /dev/null +++ b/plugins/Vectorscope/VecControlsDialog.cpp @@ -0,0 +1,94 @@ +/* + * VecControlsDialog.cpp - definition of VecControlsDialog class. + * + * Copyright (c) 2019 Martin Pavelek + * + * 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 +#include +#include +#include +#include + +#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); +} diff --git a/plugins/Vectorscope/VecControlsDialog.h b/plugins/Vectorscope/VecControlsDialog.h new file mode 100644 index 00000000000..b76c06ad001 --- /dev/null +++ b/plugins/Vectorscope/VecControlsDialog.h @@ -0,0 +1,47 @@ +/* + * VecControlsDialog.h - declatation of VecControlsDialog class. + * + * Copyright (c) 2019 Martin Pavelek + * + * 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 diff --git a/plugins/Vectorscope/VectorView.cpp b/plugins/Vectorscope/VectorView.cpp new file mode 100644 index 00000000000..9a3f855eb98 --- /dev/null +++ b/plugins/Vectorscope/VectorView.cpp @@ -0,0 +1,328 @@ +/* VectorView.cpp - implementation of VectorView class. + * + * Copyright (c) 2019 Martin Pavelek + * + * 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 +#include +#include +#include +#include + +#include "ColorChooser.h" +#include "GuiApplication.h" +#include "MainWindow.h" + + +VectorView::VectorView(VecControls *controls, LocklessRingBuffer *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::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::high_resolution_clock::now().time_since_epoch() + ).count(); + +} diff --git a/plugins/Vectorscope/VectorView.h b/plugins/Vectorscope/VectorView.h new file mode 100644 index 00000000000..066e306a0a0 --- /dev/null +++ b/plugins/Vectorscope/VectorView.h @@ -0,0 +1,80 @@ +/* VectorView.h - declaration of VectorView class. + * + * Copyright (c) 2019 Martin Pavelek + * + * 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 +#include +#include + +#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 *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 *m_inputBuffer; + LocklessRingBufferReader m_bufferReader; + + std::vector 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 diff --git a/plugins/Vectorscope/Vectorscope.cpp b/plugins/Vectorscope/Vectorscope.cpp new file mode 100644 index 00000000000..f8bc30c40df --- /dev/null +++ b/plugins/Vectorscope/Vectorscope.cpp @@ -0,0 +1,80 @@ +/* + * Vectorscope.cpp - definition of Vectorscope class. + * + * Copyright (c) 2019 Martin Pavelek + * + * 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 ", + 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(data)); + } +} + diff --git a/plugins/Vectorscope/Vectorscope.h b/plugins/Vectorscope/Vectorscope.h new file mode 100644 index 00000000000..b45ff6de4ab --- /dev/null +++ b/plugins/Vectorscope/Vectorscope.h @@ -0,0 +1,52 @@ +/* Vectorscope.h - declaration of Vectorscope class. + * + * Copyright (c) 2019 Martin Pavelek + * + * 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 *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 m_inputBuffer; +}; + +#endif // VECTORSCOPE_H + diff --git a/plugins/Vectorscope/logo.png b/plugins/Vectorscope/logo.png new file mode 100644 index 00000000000..9340da708dd Binary files /dev/null and b/plugins/Vectorscope/logo.png differ