diff --git a/CMakeLists.txt b/CMakeLists.txt index e39d72cc133..5187dfad6ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1002,6 +1002,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/widget/wtracktext.cpp src/widget/wtrackwidgetgroup.cpp src/widget/wvumeter.cpp + src/widget/wvumetergl.cpp src/widget/wwaveformviewer.cpp src/widget/wwidget.cpp src/widget/wwidgetgroup.cpp diff --git a/res/skins/Shade/auxiliary.xml b/res/skins/Shade/auxiliary.xml index 521a9258232..7a5e3be3c52 100644 --- a/res/skins/Shade/auxiliary.xml +++ b/res/skins/Shade/auxiliary.xml @@ -24,19 +24,25 @@ Visual - Volume level display ********************************************** --> - - auxiliary_VuMeter - skin:/style/volume_display_microphone_over.png - skin:/style/volume_display_microphone.png + 99,14 - 5 - 500 - 50 - 2 - - [Auxiliary],VuMeter - - + 5f,35f + horizontal + + + auxiliary_VuMeter + skin:/style/volume_display_microphone_over.png + skin:/style/volume_display_microphone.png + 5 + 500 + 50 + 2 + + [Auxiliary],VuMeter + + + + auxiliary_pregain diff --git a/res/skins/Shade/microphone.xml b/res/skins/Shade/microphone.xml index 5c1606433fc..3a51a598b74 100644 --- a/res/skins/Shade/microphone.xml +++ b/res/skins/Shade/microphone.xml @@ -25,19 +25,25 @@ Visual - Volume level display ********************************************** --> - - microphone_VuMeter - skin:/style/volume_display_microphone_over.png - skin:/style/volume_display_microphone.png + 82,14 - 5 - 500 - 50 - 2 - - [Microphone],VuMeter - - + 5f,35f + horizontal + + + microphone_VuMeter + skin:/style/volume_display_microphone_over.png + skin:/style/volume_display_microphone.png + 5 + 500 + 50 + 2 + + [Microphone],VuMeter + + + + - - channel_VuMeter - skin:/style/volume_display_over.png - skin:/style/volume_display.png + 107,76 - false - 5 - 500 - 50 - 2 - - [Channel1],VuMeter - - - - channel_VuMeter - skin:/style/volume_display_over.png - skin:/style/volume_display.png + 5f,81f + horizontal + + + channel_VuMeter + skin:/style/volume_display_over.png + skin:/style/volume_display.png + false + 5 + 500 + 50 + 2 + 1 + vinyl_spinny_background.png + vinyl_spinny_foreground.png + vinyl_spinny_foreground_ghost.png + + [Channel1],VuMeter + + + + + 143,76 - false - 5 - 500 - 50 - 2 - - [Channel2],VuMeter - - - - - master_VuMeterL - skin:/style/volume_display_master_over.png - skin:/style/volume_display_master.png + 5f,81f + horizontal + + + channel_VuMeter + skin:/style/volume_display_over.png + skin:/style/volume_display.png + false + 5 + 500 + 50 + 2 + 1 + vinyl_spinny_background.png + vinyl_spinny_foreground.png + vinyl_spinny_foreground_ghost.png + + [Channel2],VuMeter + + + + + 122,76 - 5 - 500 - 50 - 2 - - [Master],VuMeterL - - - - master_VuMeterR - skin:/style/volume_display_master_over.png - skin:/style/volume_display_master.png + 5f,81f + horizontal + + + master_VuMeterL + skin:/style/volume_display_master_over.png + skin:/style/volume_display_master.png + 5 + 500 + 50 + 2 + 1 + vinyl_spinny_background.png + vinyl_spinny_foreground.png + vinyl_spinny_foreground_ghost.png + + [Master],VuMeterL + + + + + 128,76 - 5 - 500 - 50 - 2 - - [Master],VuMeterR - - - + 5f,81f + horizontal + + + master_VuMeterR + skin:/style/volume_display_master_over.png + skin:/style/volume_display_master.png + 5 + 500 + 50 + 2 + 1 + vinyl_spinny_background.png + vinyl_spinny_foreground.png + vinyl_spinny_foreground_ghost.png + + [Master],VuMeterR + + + + diff --git a/src/main.cpp b/src/main.cpp index 1393a3fde0f..13264af1995 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "errordialoghandler.h" #include "mixxx.h" diff --git a/src/skin/legacy/legacyskinparser.cpp b/src/skin/legacy/legacyskinparser.cpp index 23b04855836..3da36b6e51d 100644 --- a/src/skin/legacy/legacyskinparser.cpp +++ b/src/skin/legacy/legacyskinparser.cpp @@ -79,6 +79,7 @@ #include "widget/wtracktext.h" #include "widget/wtrackwidgetgroup.h" #include "widget/wvumeter.h" +#include "widget/wvumetergl.h" #include "widget/wwaveformviewer.h" #include "widget/wwidget.h" #include "widget/wwidgetgroup.h" @@ -527,9 +528,7 @@ QList LegacySkinParser::parseNode(const QDomElement& node) { } else if (nodeName == "StarRating") { result = wrapWidget(parseStarRating(node)); } else if (nodeName == "VuMeter") { - WVuMeter* pVuMeterWidget = parseStandardWidget(node); - WaveformWidgetFactory::instance()->addTimerListener(pVuMeterWidget); - result = wrapWidget(pVuMeterWidget); + result = wrapWidget(parseVuMeter(node)); } else if (nodeName == "StatusLight") { result = wrapWidget(parseStandardWidget(node)); } else if (nodeName == "Display") { @@ -1270,22 +1269,85 @@ QWidget* LegacySkinParser::parseSpinny(const QDomElement& node) { SKIN_WARNING(node, *m_pContext) << "No player found for group:" << group; return nullptr; } - WSpinny* spinny = new WSpinny(m_pParent, group, m_pConfig, m_pVCManager, pPlayer); - commonWidgetSetup(node, spinny); + // Note: For some reasons on X11 we need to create the widget without a parent to avoid to + // create two platform windows (QXcbWindow) in QWidget::create() for this widget. + // This happens, because the QWidget::create() of a parent() will populate all children + // with platform windows q_createNativeChildrenAndSetParent() while another window is already + // under construction. The ID for the first window is not cleared and leads to a segfault + // during on shutdown. This has been tested with Qt 5.12.8 and 5.15.3 + WSpinny* pSpinny; + if (qApp->platformName() == QLatin1String("xcb")) { + pSpinny = new WSpinny(nullptr, group, m_pConfig, m_pVCManager, pPlayer); + pSpinny->setParent(m_pParent); + } else { + pSpinny = new WSpinny(m_pParent, group, m_pConfig, m_pVCManager, pPlayer); + } + commonWidgetSetup(node, pSpinny); connect(waveformWidgetFactory, &WaveformWidgetFactory::renderSpinnies, - spinny, + pSpinny, &WSpinny::render); - connect(waveformWidgetFactory, &WaveformWidgetFactory::swapSpinnies, spinny, &WSpinny::swap); - connect(spinny, &WSpinny::trackDropped, m_pPlayerManager, &PlayerManager::slotLoadToPlayer); - connect(spinny, &WSpinny::cloneDeck, m_pPlayerManager, &PlayerManager::slotCloneDeck); - - spinny->setup(node, *m_pContext); - spinny->installEventFilter(m_pKeyboard); - spinny->installEventFilter(m_pControllerManager->getControllerLearningEventFilter()); - spinny->Init(); - return spinny; + connect(waveformWidgetFactory, &WaveformWidgetFactory::swapSpinnies, pSpinny, &WSpinny::swap); + connect(pSpinny, &WSpinny::trackDropped, m_pPlayerManager, &PlayerManager::slotLoadToPlayer); + connect(pSpinny, &WSpinny::cloneDeck, m_pPlayerManager, &PlayerManager::slotCloneDeck); + + pSpinny->setup(node, *m_pContext); + pSpinny->installEventFilter(m_pKeyboard); + pSpinny->installEventFilter(m_pControllerManager->getControllerLearningEventFilter()); + pSpinny->Init(); + return pSpinny; +} + +QWidget* LegacySkinParser::parseVuMeter(const QDomElement& node) { + if (!CmdlineArgs::Instance().getUseVuMeterGL()) { + // Standard WVuMeter + WVuMeter* pVuMeterWidget = parseStandardWidget(node); + WaveformWidgetFactory::instance()->addVuMeter(pVuMeterWidget); + return pVuMeterWidget; + } + + // QGLWidget derived WVuMeterGL + + if (CmdlineArgs::Instance().getSafeMode()) { + WLabel* dummy = new WLabel(m_pParent); + //: Shown when Mixxx is running in safe mode. + dummy->setText(tr("Safe Mode Enabled")); + return dummy; + } + + auto* waveformWidgetFactory = WaveformWidgetFactory::instance(); + if (!waveformWidgetFactory->isOpenGlAvailable() && + !waveformWidgetFactory->isOpenGlesAvailable()) { + WLabel* dummy = new WLabel(m_pParent); + //: Shown when VuMeter can not be displayed. Please keep \n unchanged + dummy->setText(tr("No OpenGL\nsupport.")); + return dummy; + } + // Note: For some reasons on X11 we need to create the widget without a parent to avoid to + // create two platform windows (QXcbWindow) in QWidget::create() for this widget. + // This happens, because the QWidget::create() of a parent() will populate all children + // with platform windows q_createNativeChildrenAndSetParent() while another window is already + // under construction. The ID for the first window is not cleared and leads to a segfault + // during on shutdown. This has been tested with Qt 5.12.8 and 5.15.3 + WVuMeterGL* pVuMeterWidget; + if (qApp->platformName() == QLatin1String("xcb")) { + pVuMeterWidget = new WVuMeterGL(); + pVuMeterWidget->setParent(m_pParent); + } else { + pVuMeterWidget = new WVuMeterGL(m_pParent); + } + commonWidgetSetup(node, pVuMeterWidget); + + waveformWidgetFactory->addVuMeter(pVuMeterWidget); + + pVuMeterWidget->setup(node, *m_pContext); + pVuMeterWidget->installEventFilter(m_pKeyboard); + pVuMeterWidget->installEventFilter( + m_pControllerManager->getControllerLearningEventFilter()); + pVuMeterWidget->Init(); + + return pVuMeterWidget; } QWidget* LegacySkinParser::parseSearchBox(const QDomElement& node) { diff --git a/src/skin/legacy/legacyskinparser.h b/src/skin/legacy/legacyskinparser.h index 69fc1393651..347cc3a82e6 100644 --- a/src/skin/legacy/legacyskinparser.h +++ b/src/skin/legacy/legacyskinparser.h @@ -110,6 +110,7 @@ class LegacySkinParser : public QObject, public SkinParser { QWidget* parseVisual(const QDomElement& node); QWidget* parseOverview(const QDomElement& node); QWidget* parseSpinny(const QDomElement& node); + QWidget* parseVuMeter(const QDomElement& node); // Library widgets. QWidget* parseTableView(const QDomElement& node); diff --git a/src/util/cmdlineargs.cpp b/src/util/cmdlineargs.cpp index 36ecb2f0185..b643174b9fa 100644 --- a/src/util/cmdlineargs.cpp +++ b/src/util/cmdlineargs.cpp @@ -14,6 +14,7 @@ CmdlineArgs::CmdlineArgs() m_midiDebug(false), m_developer(false), m_safeMode(false), + m_useVuMeterGL(true), m_debugAssertBreak(false), m_settingsPathSet(false), m_logLevel(mixxx::kLogLevelDefault), @@ -97,14 +98,22 @@ warnings and errors to the console unless this is set properly.\n", stdout); when a critical error occurs unless this is set properly.\n", stdout); } i++; - } else if (QString::fromLocal8Bit(argv[i]).contains("--midiDebug", Qt::CaseInsensitive) || - QString::fromLocal8Bit(argv[i]).contains("--controllerDebug", Qt::CaseInsensitive)) { + } else if (QString::fromLocal8Bit(argv[i]).contains( + "--disableVuMeterGL", Qt::CaseInsensitive)) { + m_useVuMeterGL = false; + } else if (QString::fromLocal8Bit(argv[i]).contains( + "--midiDebug", Qt::CaseInsensitive) || + QString::fromLocal8Bit(argv[i]).contains( + "--controllerDebug", Qt::CaseInsensitive)) { m_midiDebug = true; - } else if (QString::fromLocal8Bit(argv[i]).contains("--developer", Qt::CaseInsensitive)) { + } else if (QString::fromLocal8Bit(argv[i]).contains( + "--developer", Qt::CaseInsensitive)) { m_developer = true; - } else if (QString::fromLocal8Bit(argv[i]).contains("--safeMode", Qt::CaseInsensitive)) { + } else if (QString::fromLocal8Bit(argv[i]).contains( + "--safeMode", Qt::CaseInsensitive)) { m_safeMode = true; - } else if (QString::fromLocal8Bit(argv[i]).contains("--debugAssertBreak", Qt::CaseInsensitive)) { + } else if (QString::fromLocal8Bit(argv[i]).contains( + "--debugAssertBreak", Qt::CaseInsensitive)) { m_debugAssertBreak = true; } else if (i > 0) { // Don't try to load the program name to a deck @@ -152,6 +161,8 @@ void CmdlineArgs::printUsage() { and spinning vinyl widgets. Try this option if\n\ Mixxx is crashing on startup.\n\ \n\ +--disableVuMeterGL Do not use OpenGL vu meter.\n\ +\n\ --locale LOCALE Use a custom locale for loading translations\n\ (e.g 'fr')\n\ \n\ @@ -169,14 +180,15 @@ void CmdlineArgs::printUsage() { defined at --logLevel above.\n\ \n" #ifdef MIXXX_BUILD_DEBUG -"\ + "\ --debugAssertBreak Breaks (SIGINT) Mixxx, if a DEBUG_ASSERT\n\ evaluates to false. Under a debugger you can\n\ continue afterwards.\ \n" #endif -"\ --h, --help Display this help message and exit", stdout); + "\ +-h, --help Display this help message and exit", + stdout); fputs("\n\n(For more information, see " MIXXX_MANUAL_COMMANDLINEOPTIONS_URL ")\n", stdout); } diff --git a/src/util/cmdlineargs.h b/src/util/cmdlineargs.h index 1aa10034983..f98de23d163 100644 --- a/src/util/cmdlineargs.h +++ b/src/util/cmdlineargs.h @@ -28,6 +28,9 @@ class CmdlineArgs final { bool getMidiDebug() const { return m_midiDebug; } bool getDeveloper() const { return m_developer; } bool getSafeMode() const { return m_safeMode; } + bool getUseVuMeterGL() const { + return m_useVuMeterGL; + } bool getDebugAssertBreak() const { return m_debugAssertBreak; } bool getSettingsPathSet() const { return m_settingsPathSet; } mixxx::LogLevel getLogLevel() const { return m_logLevel; } @@ -47,6 +50,7 @@ class CmdlineArgs final { bool m_midiDebug; bool m_developer; // Developer Mode bool m_safeMode; + bool m_useVuMeterGL; bool m_debugAssertBreak; bool m_settingsPathSet; // has --settingsPath been set on command line ? mixxx::LogLevel m_logLevel; // Level of stderr logging message verbosity diff --git a/src/util/widgethelper.cpp b/src/util/widgethelper.cpp index d0fda107b17..72254788b71 100644 --- a/src/util/widgethelper.cpp +++ b/src/util/widgethelper.cpp @@ -47,6 +47,17 @@ QWindow* getWindow( return nullptr; } +// get the base color of a widget, or recursively search the parent tree for one +QColor findBaseColor(QWidget* pWidget) { + while (pWidget) { + if (pWidget->palette().isBrushSet(QPalette::Normal, QPalette::Base)) { + return pWidget->palette().color(QPalette::Base); + } + pWidget = qobject_cast(pWidget->parent()); + } + return QColor(0, 0, 0); +} + } // namespace widgethelper } // namespace mixxx diff --git a/src/util/widgethelper.h b/src/util/widgethelper.h index 7ede58dfb31..81a4009969a 100644 --- a/src/util/widgethelper.h +++ b/src/util/widgethelper.h @@ -49,6 +49,12 @@ inline QScreen* getScreen( #endif } +// Get the base color of a widget, or recursively search the parent tree for one. +// +// Returns QColor(0,0,0) when none is found. As the recursion can go quite deep, +// avoid calling repeatedly, but only when needed, e.g in a showEvent. +QColor findBaseColor(QWidget* pWidget); + } // namespace widgethelper } // namespace mixxx diff --git a/src/waveform/vsyncthread.cpp b/src/waveform/vsyncthread.cpp index 2105d9221a2..ebb54e9bf18 100644 --- a/src/waveform/vsyncthread.cpp +++ b/src/waveform/vsyncthread.cpp @@ -49,7 +49,7 @@ void VSyncThread::run() { emit vsyncSwap(); // swaps the new waveform to front m_semaVsyncSlot.acquire(); - m_timer.restart(); + m_sinceLastSwap = m_timer.restart(); m_waitToSwapMicros = 1000; usleep(1000); } else { // if (m_vSyncMode == ST_TIMER) { @@ -75,7 +75,8 @@ void VSyncThread::run() { m_semaVsyncSlot.acquire(); // <- Assume we are VSynced here -> - int lastSwapTime = static_cast(m_timer.restart().toIntegerMicros()); + m_sinceLastSwap = m_timer.restart(); + int lastSwapTime = static_cast(m_sinceLastSwap.toIntegerMicros()); if (remainingForSwap < 0) { // Our swapping call was already delayed // The real swap might happens on the following VSync, depending on driver settings @@ -162,3 +163,7 @@ void VSyncThread::getAvailableVSyncTypes(QList >* pList) { } } } + +mixxx::Duration VSyncThread::sinceLastSwap() const { + return m_sinceLastSwap; +} diff --git a/src/waveform/vsyncthread.h b/src/waveform/vsyncthread.h index f7310aee56d..4632fe0e79f 100644 --- a/src/waveform/vsyncthread.h +++ b/src/waveform/vsyncthread.h @@ -37,7 +37,7 @@ class VSyncThread : public QThread { void getAvailableVSyncTypes(QList >* list); void setupSync(QGLWidget* glw, int index); void waitUntilSwap(QGLWidget* glw); - + mixxx::Duration sinceLastSwap() const; signals: void vsyncRender(); void vsyncSwap(); @@ -55,4 +55,5 @@ class VSyncThread : public QThread { QSemaphore m_semaVsyncSlot; double m_displayFrameRate; int m_vSyncPerRendering; + mixxx::Duration m_sinceLastSwap; }; diff --git a/src/waveform/waveformwidgetfactory.cpp b/src/waveform/waveformwidgetfactory.cpp index 0ae08e33aeb..7d404b09b50 100644 --- a/src/waveform/waveformwidgetfactory.cpp +++ b/src/waveform/waveformwidgetfactory.cpp @@ -36,6 +36,7 @@ #include "waveform/widgets/softwarewaveformwidget.h" #include "waveform/widgets/waveformwidgetabstract.h" #include "widget/wvumeter.h" +#include "widget/wvumetergl.h" #include "widget/wwaveformviewer.h" namespace { @@ -353,17 +354,29 @@ void WaveformWidgetFactory::destroyWidgets() { m_waveformWidgetHolders.clear(); } -void WaveformWidgetFactory::addTimerListener(WVuMeter* pWidget) { +void WaveformWidgetFactory::addVuMeter(WVuMeter* pVuMeter) { // Do not hold the pointer to of timer listeners since they may be deleted. // We don't activate update() or repaint() directly so listener widgets // can decide whether to paint or not. connect(this, &WaveformWidgetFactory::waveformUpdateTick, - pWidget, + pVuMeter, &WVuMeter::maybeUpdate, Qt::DirectConnection); } +void WaveformWidgetFactory::addVuMeter(WVuMeterGL* pVuMeter) { + // WVuMeterGLs to be rendered and swapped from the vsync thread + connect(this, + &WaveformWidgetFactory::renderVuMeters, + pVuMeter, + &WVuMeterGL::render); + connect(this, + &WaveformWidgetFactory::swapVuMeters, + pVuMeter, + &WVuMeterGL::swap); +} + void WaveformWidgetFactory::slotSkinLoaded() { setWidgetTypeFromConfig(); #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && defined __WINDOWS__ @@ -673,6 +686,9 @@ void WaveformWidgetFactory::render() { // WSpinnys are also double-buffered QGLWidgets, like all the waveform // renderers. Render all the WSpinny widgets now. emit renderSpinnies(m_vsyncThread); + // Same for WVuMeterGL. Note that we are either using WVuMeter or WVuMeterGL. + // If we are using WVuMeter, this does nothing + emit renderVuMeters(m_vsyncThread); // Notify all other waveform-like widgets (e.g. WSpinny's) that they should // update. @@ -729,6 +745,9 @@ void WaveformWidgetFactory::swap() { // WSpinnys are also double-buffered QGLWidgets, like all the waveform // renderers. Swap all the WSpinny widgets now. emit swapSpinnies(); + // Same for WVuMeterGL. Note that we are either using WVuMeter or WVuMeterGL + // If we are using WVuMeter, this does nothing + emit swapVuMeters(); } //qDebug() << "swap end" << m_vsyncThread->elapsed(); m_vsyncThread->vsyncSlotFinished(); diff --git a/src/waveform/waveformwidgetfactory.h b/src/waveform/waveformwidgetfactory.h index 483ea4caad8..53961480a02 100644 --- a/src/waveform/waveformwidgetfactory.h +++ b/src/waveform/waveformwidgetfactory.h @@ -12,6 +12,7 @@ #include "waveform/widgets/waveformwidgettype.h" class WVuMeter; +class WVuMeterGL; class WWaveformViewer; class WaveformWidgetAbstract; class VSyncThread; @@ -122,7 +123,8 @@ class WaveformWidgetFactory : public QObject, public Singleton >* list); void destroyWidgets(); - void addTimerListener(WVuMeter* pWidget); + void addVuMeter(WVuMeter* pWidget); + void addVuMeter(WVuMeterGL* pWidget); void startVSync(GuiTick* pGuiTick, VisualsManager* pVisualsManager); void setVSyncType(int vsType); @@ -140,6 +142,8 @@ class WaveformWidgetFactory : public QObject, public Singleton -#include #include #include -#include #include "moc_wvumeter.cpp" #include "util/math.h" @@ -51,9 +48,10 @@ void WVuMeter::setup(const QDomNode& node, const SkinContext& context) { QDomElement vuNode = context.selectElement(node, "PathVu"); // The implicit default in <1.12.0 was FIXED so we keep it for backwards // compatibility. - setPixmaps(context.getPixmapSource(vuNode), bHorizontal, - context.selectScaleMode(vuNode, Paintable::FIXED), - context.getScaleFactor()); + setPixmaps(context.getPixmapSource(vuNode), + bHorizontal, + context.selectScaleMode(vuNode, Paintable::FIXED), + context.getScaleFactor()); m_iPeakHoldSize = context.selectInt(node, "PeakHoldSize"); if (m_iPeakHoldSize < 0 || m_iPeakHoldSize > 100) { @@ -154,7 +152,7 @@ void WVuMeter::maybeUpdate() { } } -void WVuMeter::paintEvent(QPaintEvent * /*unused*/) { +void WVuMeter::paintEvent(QPaintEvent* /*unused*/) { ScopedTimer t("WVuMeter::paintEvent"); QStyleOption option; @@ -176,12 +174,14 @@ void WVuMeter::paintEvent(QPaintEvent * /*unused*/) { // Draw (part of) vu if (m_bHorizontal) { const double widgetPosition = math_clamp(widgetWidth * m_dParameter, - 0.0, widgetWidth); + 0.0, + widgetWidth); QRectF targetRect(0, 0, widgetPosition, widgetHeight); const double pixmapPosition = math_clamp(pixmapWidth * m_dParameter, - 0.0, pixmapWidth); - QRectF sourceRect(0, 0, pixmapPosition, m_pPixmapVu->height()); + 0.0, + pixmapWidth); + QRectF sourceRect(0, 0, pixmapPosition, m_pPixmapVu->height()); m_pPixmapVu->draw(targetRect, &p, sourceRect); if (m_iPeakHoldSize > 0 && m_dPeakParameter > 0.0) { @@ -194,22 +194,26 @@ void WVuMeter::paintEvent(QPaintEvent * /*unused*/) { pixmapWidth * m_dPeakParameter, 0.0, pixmapWidth); const double pixmapPeakHoldSize = m_iPeakHoldSize; - targetRect = QRectF(widgetPeakPosition - widgetPeakHoldSize, 0, - widgetPeakHoldSize, widgetHeight); - sourceRect = QRectF(pixmapPeakPosition - pixmapPeakHoldSize, 0, - pixmapPeakHoldSize, pixmapHeight); + targetRect = QRectF(widgetPeakPosition - widgetPeakHoldSize, + 0, + widgetPeakHoldSize, + widgetHeight); + sourceRect = QRectF(pixmapPeakPosition - pixmapPeakHoldSize, + 0, + pixmapPeakHoldSize, + pixmapHeight); m_pPixmapVu->draw(targetRect, &p, sourceRect); } } else { const double widgetPosition = math_clamp(widgetHeight * m_dParameter, - 0.0, widgetHeight); - QRectF targetRect(0, widgetHeight - widgetPosition, - widgetWidth, widgetPosition); + 0.0, + widgetHeight); + QRectF targetRect(0, widgetHeight - widgetPosition, widgetWidth, widgetPosition); const double pixmapPosition = math_clamp(pixmapHeight * m_dParameter, - 0.0, pixmapHeight); - QRectF sourceRect(0, pixmapHeight - pixmapPosition, - pixmapWidth, pixmapPosition); + 0.0, + pixmapHeight); + QRectF sourceRect(0, pixmapHeight - pixmapPosition, pixmapWidth, pixmapPosition); m_pPixmapVu->draw(targetRect, &p, sourceRect); if (m_iPeakHoldSize > 0 && m_dPeakParameter > 0.0) { @@ -222,10 +226,14 @@ void WVuMeter::paintEvent(QPaintEvent * /*unused*/) { pixmapHeight * m_dPeakParameter, 0.0, pixmapHeight); const double pixmapPeakHoldSize = m_iPeakHoldSize; - targetRect = QRectF(0, widgetHeight - widgetPeakPosition, - widgetWidth, widgetPeakHoldSize); - sourceRect = QRectF(0, pixmapHeight - pixmapPeakPosition, - pixmapWidth, pixmapPeakHoldSize); + targetRect = QRectF(0, + widgetHeight - widgetPeakPosition, + widgetWidth, + widgetPeakHoldSize); + sourceRect = QRectF(0, + pixmapHeight - pixmapPeakPosition, + pixmapWidth, + pixmapPeakHoldSize); m_pPixmapVu->draw(targetRect, &p, sourceRect); } } diff --git a/src/widget/wvumeter.h b/src/widget/wvumeter.h index 988f0c463e3..df16bf482e8 100644 --- a/src/widget/wvumeter.h +++ b/src/widget/wvumeter.h @@ -1,20 +1,14 @@ #pragma once -#include -#include -#include -#include -#include - -#include "widget/wwidget.h" -#include "widget/wpixmapstore.h" #include "skin/legacy/skincontext.h" #include "util/performancetimer.h" +#include "widget/wpixmapstore.h" +#include "widget/wwidget.h" -class WVuMeter : public WWidget { - Q_OBJECT +class WVuMeter : public WWidget { + Q_OBJECT public: - explicit WVuMeter(QWidget *parent=nullptr); + explicit WVuMeter(QWidget* parent = nullptr); void setup(const QDomNode& node, const SkinContext& context); void setPixmapBackground( @@ -35,7 +29,7 @@ class WVuMeter : public WWidget { void updateState(mixxx::Duration elapsed); private: - void paintEvent(QPaintEvent * /*unused*/) override; + void paintEvent(QPaintEvent* /*unused*/) override; void setPeak(double parameter); // Current parameter and peak parameter. diff --git a/src/widget/wvumetergl.cpp b/src/widget/wvumetergl.cpp new file mode 100644 index 00000000000..09f2a29ca56 --- /dev/null +++ b/src/widget/wvumetergl.cpp @@ -0,0 +1,314 @@ +#include "widget/wvumetergl.h" + +#include "moc_wvumetergl.cpp" +#include "util/math.h" +#include "util/timer.h" +#include "util/widgethelper.h" +#include "waveform/sharedglcontext.h" +#include "waveform/vsyncthread.h" +#include "widget/wpixmapstore.h" + +#define DEFAULT_FALLTIME 20 +#define DEFAULT_FALLSTEP 1 +#define DEFAULT_HOLDTIME 400 +#define DEFAULT_HOLDSIZE 5 + +WVuMeterGL::WVuMeterGL(QWidget* parent) + : QGLWidget(parent, SharedGLContext::getWidget()), + WBaseWidget(this), + m_bHasRendered(false), + m_bSwapNeeded(false), + m_dParameter(0), + m_dPeakParameter(0), + m_dLastParameter(0), + m_dLastPeakParameter(0), + m_iPixmapLength(0), + m_bHorizontal(false), + m_iPeakHoldSize(0), + m_iPeakFallStep(0), + m_iPeakHoldTime(0), + m_iPeakFallTime(0), + m_dPeakHoldCountdownMs(0) { + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_OpaquePaintEvent); + + setAutoFillBackground(false); + setAutoBufferSwap(false); + + // Not interested in repaint or update calls, as we draw from the vsync thread + setUpdatesEnabled(false); +} + +void WVuMeterGL::setup(const QDomNode& node, const SkinContext& context) { + // Set pixmaps + bool bHorizontal = false; + (void)context.hasNodeSelectBool(node, "Horizontal", &bHorizontal); + + // Set background pixmap if available + QDomElement backPathNode = context.selectElement(node, "PathBack"); + if (!backPathNode.isNull()) { + // The implicit default in <1.12.0 was FIXED so we keep it for backwards + // compatibility. + setPixmapBackground( + context.getPixmapSource(backPathNode), + context.selectScaleMode(backPathNode, Paintable::FIXED), + context.getScaleFactor()); + } + + QDomElement vuNode = context.selectElement(node, "PathVu"); + // The implicit default in <1.12.0 was FIXED so we keep it for backwards + // compatibility. + setPixmaps(context.getPixmapSource(vuNode), + bHorizontal, + context.selectScaleMode(vuNode, Paintable::FIXED), + context.getScaleFactor()); + + m_iPeakHoldSize = context.selectInt(node, "PeakHoldSize"); + if (m_iPeakHoldSize < 0 || m_iPeakHoldSize > 100) { + m_iPeakHoldSize = DEFAULT_HOLDSIZE; + } + + m_iPeakFallStep = context.selectInt(node, "PeakFallStep"); + if (m_iPeakFallStep < 1 || m_iPeakFallStep > 1000) { + m_iPeakFallStep = DEFAULT_FALLSTEP; + } + + m_iPeakHoldTime = context.selectInt(node, "PeakHoldTime"); + if (m_iPeakHoldTime < 1 || m_iPeakHoldTime > 3000) { + m_iPeakHoldTime = DEFAULT_HOLDTIME; + } + + m_iPeakFallTime = context.selectInt(node, "PeakFallTime"); + if (m_iPeakFallTime < 1 || m_iPeakFallTime > 1000) { + m_iPeakFallTime = DEFAULT_FALLTIME; + } + + setFocusPolicy(Qt::NoFocus); +} + +void WVuMeterGL::setPixmapBackground( + const PixmapSource& source, + Paintable::DrawMode mode, + double scaleFactor) { + m_pPixmapBack = WPixmapStore::getPaintable(source, mode, scaleFactor); + if (m_pPixmapBack.isNull()) { + qDebug() << metaObject()->className() + << "Error loading background pixmap:" << source.getPath(); + } else if (mode == Paintable::FIXED) { + setFixedSize(m_pPixmapBack->size()); + } +} + +void WVuMeterGL::setPixmaps(const PixmapSource& source, + bool bHorizontal, + Paintable::DrawMode mode, + double scaleFactor) { + m_pPixmapVu = WPixmapStore::getPaintable(source, mode, scaleFactor); + if (m_pPixmapVu.isNull()) { + qDebug() << "WVuMeterGL: Error loading vu pixmap" << source.getPath(); + } else { + m_bHorizontal = bHorizontal; + if (m_bHorizontal) { + m_iPixmapLength = m_pPixmapVu->width(); + } else { + m_iPixmapLength = m_pPixmapVu->height(); + } + } +} + +void WVuMeterGL::onConnectedControlChanged(double dParameter, double dValue) { + Q_UNUSED(dValue); + m_dParameter = math_clamp(dParameter, 0.0, 1.0); + + if (dParameter > 0.0) { + setPeak(dParameter); + } else { + // A 0.0 value is very unlikely except when the VU Meter is disabled + m_dPeakParameter = 0; + } +} + +void WVuMeterGL::setPeak(double parameter) { + if (parameter > m_dPeakParameter) { + m_dPeakParameter = parameter; + m_dPeakHoldCountdownMs = m_iPeakHoldTime; + } +} + +void WVuMeterGL::updateState(mixxx::Duration elapsed) { + double msecsElapsed = elapsed.toDoubleMillis(); + // If we're holding at a peak then don't update anything + m_dPeakHoldCountdownMs -= msecsElapsed; + if (m_dPeakHoldCountdownMs > 0) { + return; + } else { + m_dPeakHoldCountdownMs = 0; + } + + // Otherwise, decrement the peak position by the fall step size times the + // milliseconds elapsed over the fall time multiplier. The peak will fall + // FallStep times (out of 128 steps) every FallTime milliseconds. + m_dPeakParameter -= static_cast(m_iPeakFallStep) * + msecsElapsed / + static_cast(m_iPeakFallTime * m_iPixmapLength); + m_dPeakParameter = math_clamp(m_dPeakParameter, 0.0, 1.0); +} + +void WVuMeterGL::paintEvent(QPaintEvent* e) { + Q_UNUSED(e); + // Force a rerender when render is called from the vsync thread, e.g. to + // git rid artifacts after hiding and showing the mixer or incomplete + // initial drawing. + m_bHasRendered = false; +} + +void WVuMeterGL::showEvent(QShowEvent* e) { + Q_UNUSED(e); + // Find the base color recursively in parent widget. + m_qBgColor = mixxx::widgethelper::findBaseColor(this); +} + +void WVuMeterGL::render(VSyncThread* vSyncThread) { + ScopedTimer t("WVuMeterGL::render"); + + updateState(vSyncThread->sinceLastSwap()); + + if (m_bHasRendered && m_dParameter == m_dLastParameter && + m_dPeakParameter == m_dLastPeakParameter) { + return; + } + + if (!isValid() || !isVisible()) { + return; + } + + auto* window = windowHandle(); + if (window == nullptr || !window->isExposed()) { + return; + } + + QPainter p(this); + // fill the background, in case the image contains transparency + p.fillRect(rect(), m_qBgColor); + + if (!m_pPixmapBack.isNull()) { + // Draw background. + QRectF sourceRect(0, 0, m_pPixmapBack->width(), m_pPixmapBack->height()); + m_pPixmapBack->draw(rect(), &p, sourceRect); + } + + const double widgetWidth = width(); + const double widgetHeight = height(); + const double pixmapWidth = m_pPixmapVu.isNull() ? 0 : m_pPixmapVu->width(); + const double pixmapHeight = m_pPixmapVu.isNull() ? 0 : m_pPixmapVu->height(); + + // Draw (part of) vu + if (m_bHorizontal) { + { + const double widgetPosition = math_clamp(widgetWidth * m_dParameter, 0.0, widgetWidth); + QRectF targetRect(0, 0, widgetPosition, widgetHeight); + + if (!m_pPixmapVu.isNull()) { + const double pixmapPosition = math_clamp( + pixmapWidth * m_dParameter, 0.0, pixmapWidth); + QRectF sourceRect(0, 0, pixmapPosition, pixmapHeight); + m_pPixmapVu->draw(targetRect, &p, sourceRect); + } else { + // fallback to green rectangle + p.fillRect(targetRect, QColor(0, 255, 0)); + } + } + + if (m_iPeakHoldSize > 0 && m_dPeakParameter > 0.0 && + m_dPeakParameter > m_dParameter) { + const double widgetPeakPosition = math_clamp( + widgetWidth * m_dPeakParameter, 0.0, widgetWidth); + const double pixmapPeakHoldSize = static_cast(m_iPeakHoldSize); + const double widgetPeakHoldSize = widgetWidth * pixmapPeakHoldSize / pixmapHeight; + + QRectF targetRect(widgetPeakPosition - widgetPeakHoldSize, + 0, + widgetPeakHoldSize, + widgetHeight); + + if (!m_pPixmapVu.isNull()) { + const double pixmapPeakPosition = math_clamp( + pixmapWidth * m_dPeakParameter, 0.0, pixmapWidth); + + QRectF sourceRect = + QRectF(pixmapPeakPosition - pixmapPeakHoldSize, + 0, + pixmapPeakHoldSize, + pixmapHeight); + m_pPixmapVu->draw(targetRect, &p, sourceRect); + } else { + // fallback to green rectangle + p.fillRect(targetRect, QColor(0, 255, 0)); + } + } + } else { + // vertical + { + const double widgetPosition = + math_clamp(widgetHeight * m_dParameter, 0.0, widgetHeight); + QRectF targetRect(0, widgetHeight - widgetPosition, widgetWidth, widgetPosition); + + if (!m_pPixmapVu.isNull()) { + const double pixmapPosition = math_clamp( + pixmapHeight * m_dParameter, 0.0, pixmapHeight); + QRectF sourceRect(0, pixmapHeight - pixmapPosition, pixmapWidth, pixmapPosition); + m_pPixmapVu->draw(targetRect, &p, sourceRect); + } else { + // fallback to green rectangle + p.fillRect(targetRect, QColor(0, 255, 0)); + } + } + + if (m_iPeakHoldSize > 0 && m_dPeakParameter > 0.0 && + m_dPeakParameter > m_dParameter) { + const double widgetPeakPosition = math_clamp( + widgetHeight * m_dPeakParameter, 0.0, widgetHeight); + const double pixmapPeakHoldSize = static_cast(m_iPeakHoldSize); + const double widgetPeakHoldSize = widgetHeight * pixmapPeakHoldSize / pixmapHeight; + + QRectF targetRect(0, + widgetHeight - widgetPeakPosition, + widgetWidth, + widgetPeakHoldSize); + + if (!m_pPixmapVu.isNull()) { + const double pixmapPeakPosition = math_clamp( + pixmapHeight * m_dPeakParameter, 0.0, pixmapHeight); + + QRectF sourceRect = QRectF(0, + pixmapHeight - pixmapPeakPosition, + pixmapWidth, + pixmapPeakHoldSize); + m_pPixmapVu->draw(targetRect, &p, sourceRect); + } else { + // fallback to green rectangle + p.fillRect(targetRect, QColor(0, 255, 0)); + } + } + } + + m_dLastParameter = m_dParameter; + m_dLastPeakParameter = m_dPeakParameter; + m_bHasRendered = true; + m_bSwapNeeded = true; +} + +void WVuMeterGL::swap() { + if (!isValid() || !isVisible() || !m_bSwapNeeded) { + return; + } + auto* window = windowHandle(); + if (window == nullptr || !window->isExposed()) { + return; + } + if (context() != QGLContext::currentContext()) { + makeCurrent(); + } + swapBuffers(); + m_bSwapNeeded = false; +} diff --git a/src/widget/wvumetergl.h b/src/widget/wvumetergl.h new file mode 100644 index 00000000000..43437489d80 --- /dev/null +++ b/src/widget/wvumetergl.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +#include "skin/legacy/skincontext.h" +#include "util/duration.h" +#include "widget/wpixmapstore.h" +#include "widget/wwidget.h" + +class VSyncThread; + +class WVuMeterGL : public QGLWidget, public WBaseWidget { + Q_OBJECT + public: + explicit WVuMeterGL(QWidget* parent = nullptr); + + void setup(const QDomNode& node, const SkinContext& context); + void setPixmapBackground( + const PixmapSource& source, + Paintable::DrawMode mode, + double scaleFactor); + void setPixmaps( + const PixmapSource& source, + bool bHorizontal, + Paintable::DrawMode mode, + double scaleFactor); + void onConnectedControlChanged(double dParameter, double dValue) override; + + public slots: + void render(VSyncThread* vSyncThread); + void swap(); + + protected slots: + void updateState(mixxx::Duration elapsed); + + private: + void paintEvent(QPaintEvent* /*unused*/) override; + void showEvent(QShowEvent* /*unused*/) override; + void setPeak(double parameter); + + // To make sure we render at least once even when we have no signal + bool m_bHasRendered; + // To indicate that we rendered so we need to swap + bool m_bSwapNeeded; + // Current parameter and peak parameter. + double m_dParameter; + double m_dPeakParameter; + + // The last parameter and peak parameter values at the time of + // rendering. Used to check whether the widget state has changed since the + // last render in maybeUpdate. + double m_dLastParameter; + double m_dLastPeakParameter; + + // Length of the VU-meter pixmap along the relevant axis. + int m_iPixmapLength; + + // Associated pixmaps + PaintablePointer m_pPixmapBack; + PaintablePointer m_pPixmapVu; + + // True if it's a horizontal vu meter + bool m_bHorizontal; + + int m_iPeakHoldSize; + int m_iPeakFallStep; + int m_iPeakHoldTime; + int m_iPeakFallTime; + + // The peak hold time remaining in milliseconds. + double m_dPeakHoldCountdownMs; + + QColor m_qBgColor; +};