Files
lmms/plugins/Oscilloscope/OscilloscopeGraph.cpp
Héctor Sebastián Trejo Luna a1126aaee2 Oscilloscope: Add missing QApplication and QThread includes (#8377)
Adds two missing includes which are needed for Debug builds.

Fixes #8376
2026-05-09 15:13:46 -04:00

177 lines
6.8 KiB
C++

/*
* OscilloscopeGraph.cpp - Oscilloscope graph widget to handle drawing the waveform and user scrolling/zooming
*
* Copyright (c) 2025-2026 Keratin
*
* 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 "OscilloscopeGraph.h"
#include "OscilloscopeControls.h"
#include "Oscilloscope.h"
#include "embed.h"
#include "GuiApplication.h"
#include "MainWindow.h"
#include <QApplication>
#include <QPainter>
#include <QSizePolicy>
#include <QThread>
#include <QWheelEvent>
#include <functional>
namespace lmms::gui
{
OscilloscopeGraph::OscilloscopeGraph(QWidget* parent, OscilloscopeControls* controls):
QWidget(parent),
m_controls(controls),
m_inputBufferReader(static_cast<Oscilloscope*>(m_controls->effect())->inputBuffer())
{
setAutoFillBackground(true);
setMinimumSize(400, 200);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
connect(getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(update()));
connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, this, &OscilloscopeGraph::changeSampleRate);
changeSampleRate();
}
void OscilloscopeGraph::changeSampleRate()
{
// Must run on main thread to prevent race condition with ring buffer usage in paintEvent()
assert(QApplication::instance()->thread() == QThread::currentThread());
const auto newBufferSize = static_cast<std::size_t>(Engine::audioEngine()->outputSampleRate() * MaxBufferLengthSeconds);
m_ringBuffer.resize(newBufferSize);
m_ringBufferIndex = m_ringBufferIndex % newBufferSize;
}
void OscilloscopeGraph::paintEvent(QPaintEvent* pe)
{
const auto effect = static_cast<Oscilloscope*>(m_controls->effect());
// Update the ring buffer with any new data from the audio thread
auto incomingBuffer = m_inputBufferReader.read_max(effect->inputBuffer().capacity());
for (f_cnt_t f = 0; f < incomingBuffer.size(); ++f)
{
m_ringBuffer[m_ringBufferIndex] = incomingBuffer[f];
m_ringBufferIndex = (m_ringBufferIndex + 1) % m_ringBuffer.size();
}
QPainter p(this);
p.fillRect(0, 0, width(), height(), m_backgroundColor);
const float amp = m_controls->m_ampModel.value() * 0.01f;
int windowSizeFrames = m_ringBuffer.size()
* m_controls->m_lengthModel.value()
/ m_controls->m_lengthModel.maxValue();
const int framesPerPixel = std::max(1, windowSizeFrames / width());
int windowStartIndex = m_ringBufferIndex
+ (m_ringBuffer.size() - windowSizeFrames)
+ m_controls->m_phaseModel.value() * m_ringBuffer.size();
// Quantize start index to prevent flickering (round up to prevent going below the current ring buffer index)
const int frameOffsetWithinPixel = windowStartIndex % framesPerPixel;
windowStartIndex += framesPerPixel - frameOffsetWithinPixel;
// And subtract that amount from the window size to prevent going past off the right
windowSizeFrames -= framesPerPixel - frameOffsetWithinPixel;
p.setPen(m_minorLineColor);
p.drawLine(0, height() / 2, width(), height() / 2);
p.setPen(m_clippingLineColor);
// Subtracting/adding 1 pixel from the clipping line vertical positions to ensure they "round up" and
// don't appear when at the default zoom/scale.
p.drawLine(0, height() / 2 * (1 - amp) - 1, width(), height() / 2 * (1 - amp) - 1);
p.drawLine(0, height() / 2 * (1 + amp) + 1, width(), height() / 2 * (1 + amp) + 1);
const bool hq = framesPerPixel > 1;
const int xoffset = !hq * framesPerPixel;
const float xscale = 1.f / (windowSizeFrames - framesPerPixel);
auto drawWaveform = [&](QColor& color, auto&& getChannel)
{
p.setCompositionMode(QPainter::CompositionMode_Plus);
p.setPen(color);
for (int f = 0; f < windowSizeFrames - framesPerPixel; f += framesPerPixel)
{
const int currentIndex = (windowStartIndex + f) % m_ringBuffer.size();
const int nextIndex = (windowStartIndex + f + framesPerPixel) % m_ringBuffer.size();
float maxValue = getChannel(m_ringBuffer[hq ? currentIndex : nextIndex]);
float minValue = getChannel(m_ringBuffer[currentIndex]);
for (int i = currentIndex + 1; hq && i <= nextIndex; ++i)
{
maxValue = std::max(maxValue, getChannel(m_ringBuffer[i]));
minValue = std::min(minValue, getChannel(m_ringBuffer[i]));
}
p.drawLine(
width() * f * xscale,
height() * (1 - minValue * amp) / 2,
width() * (f + xoffset) * xscale,
height() * (1 - maxValue * amp) / 2
);
}
};
if (!m_controls->m_stereoModel.value())
{
drawWaveform(m_monoColor, [](const SampleFrame& f){ return f.average(); });
}
else
{
drawWaveform(m_leftColor, [](const SampleFrame& f){ return f.left(); });
drawWaveform(m_rightColor, [](const SampleFrame& f){ return f.right(); });
}
}
void OscilloscopeGraph::wheelEvent(QWheelEvent* we)
{
const float windowSizeMilliseconds = m_controls->m_lengthModel.value();
const float mouseOffset = (1.0f - we->position().x() / width())
* (windowSizeMilliseconds / m_controls->m_lengthModel.maxValue());
const float zoomAmount = std::clamp(
std::exp2(-we->angleDelta().y() / 240.0f),
m_controls->m_lengthModel.minValue() / m_controls->m_lengthModel.value(),
m_controls->m_lengthModel.maxValue() / m_controls->m_lengthModel.value()
);
if ((zoomAmount > 1.0f && windowSizeMilliseconds >= m_controls->m_lengthModel.maxValue()) || (zoomAmount < 1.0f && windowSizeMilliseconds <= m_controls->m_lengthModel.minValue())) { return; }
const float newWindowSizeMilliseconds = windowSizeMilliseconds * zoomAmount;
const float newPhase = m_controls->m_phaseModel.value() - mouseOffset * (1.0f - zoomAmount);
m_controls->m_lengthModel.setValue(newWindowSizeMilliseconds);
// Clamp to prevent the user from accidentally bringing the write position discontinuity into view
m_controls->m_phaseModel.setValue(std::clamp(newPhase, newWindowSizeMilliseconds / m_controls->m_lengthModel.maxValue(), 1.0f));
}
void OscilloscopeGraph::mousePressEvent(QMouseEvent* me) { m_mousePos = me->x(); }
void OscilloscopeGraph::mouseMoveEvent(QMouseEvent* me)
{
float newPhase = m_controls->m_phaseModel.value() + static_cast<float>(m_mousePos - me->x()) / width() * (m_controls->m_lengthModel.value() / m_controls->m_lengthModel.maxValue());
m_controls->m_phaseModel.setValue(newPhase - std::floor(newPhase));
m_mousePos = me->x();
}
} // namespace lmms::gui