[Follow up] Improve performance when rendering sample waveforms (#7695)

A follow up to 786088baec to fix rendering issues and crashes.

---------

Co-authored-by: Sotonye Atemie <sakertooth@gmail.com>
This commit is contained in:
Cas Pascal
2025-04-16 10:40:16 +07:00
committed by GitHub
parent 795d513c7f
commit 64053342d8
6 changed files with 82 additions and 43 deletions

View File

@@ -67,6 +67,7 @@ private:
SampleClip * m_clip;
SampleThumbnail m_sampleThumbnail;
QPixmap m_paintPixmap;
long m_paintPixmapXPosition;
bool splitClip( const TimePos pos ) override;
} ;

View File

@@ -54,10 +54,8 @@ public:
{
QRect sampleRect; //!< A rectangle that covers the entire range of samples.
QRect drawRect; //!< Specifies the location in `sampleRect` where the waveform will be drawn. Equals
//!< `sampleRect` when null.
QRect viewportRect; //!< Clips `drawRect`. Equals `drawRect` when null.
QRect viewportRect; //!< Specifies the location in `sampleRect` where the waveform will be drawn. Equals
//!< `sampleRect` when null.
float amplification = 1.0f; //!< The amount of amplification to apply to the waveform.
@@ -95,8 +93,8 @@ private:
Peak operator+(const Peak& other) const { return Peak(std::min(min, other.min), std::max(max, other.max)); }
Peak operator+(const SampleFrame& frame) const { return *this + Peak{frame}; }
float min = std::numeric_limits<float>::max();
float max = std::numeric_limits<float>::min();
float min = std::numeric_limits<float>::infinity();
float max = -std::numeric_limits<float>::infinity();
};
Thumbnail() = default;
@@ -105,6 +103,7 @@ private:
Thumbnail zoomOut(float factor) const;
Peak* data() { return m_peaks.data(); }
Peak& operator[](size_t index) { return m_peaks[index]; }
const Peak& operator[](size_t index) const { return m_peaks[index]; }
@@ -134,7 +133,7 @@ private:
using ThumbnailCache = std::vector<Thumbnail>;
std::shared_ptr<ThumbnailCache> m_thumbnailCache = std::make_shared<ThumbnailCache>();
std::shared_ptr<const SampleBuffer> m_buffer = SampleBuffer::emptyBuffer();
inline static std::unordered_map<SampleThumbnailEntry, std::shared_ptr<ThumbnailCache>, Hash> s_sampleThumbnailCacheMap;
};

View File

@@ -70,6 +70,7 @@ SampleThumbnail::Thumbnail SampleThumbnail::Thumbnail::zoomOut(float factor) con
}
SampleThumbnail::SampleThumbnail(const Sample& sample)
: m_buffer(sample.buffer())
{
auto entry = SampleThumbnailEntry{sample.sampleFile(), QFileInfo{sample.sampleFile()}.lastModified()};
if (!entry.filePath.isEmpty())
@@ -91,11 +92,9 @@ SampleThumbnail::SampleThumbnail(const Sample& sample)
s_sampleThumbnailCacheMap[std::move(entry)] = m_thumbnailCache;
}
if (!sample.buffer()) { throw std::runtime_error{"Cannot create a sample thumbnail with no buffer"}; }
if (sample.sampleSize() == 0) { return; }
const auto fullResolutionWidth = sample.sampleSize() * DEFAULT_CHANNELS;
m_thumbnailCache->emplace_back(&sample.buffer()->data()->left(), fullResolutionWidth, fullResolutionWidth);
const auto flatBuffer = m_buffer->data()->data();
const auto flatBufferSize = m_buffer->size() * DEFAULT_CHANNELS;
m_thumbnailCache->emplace_back(flatBuffer, flatBufferSize, flatBufferSize / AggregationPerZoomStep);
while (m_thumbnailCache->back().width() >= AggregationPerZoomStep)
{
@@ -107,48 +106,71 @@ SampleThumbnail::SampleThumbnail(const Sample& sample)
void SampleThumbnail::visualize(VisualizeParameters parameters, QPainter& painter) const
{
const auto& sampleRect = parameters.sampleRect;
const auto& drawRect = parameters.drawRect.isNull() ? sampleRect : parameters.drawRect;
const auto& viewportRect = parameters.viewportRect.isNull() ? drawRect : parameters.viewportRect;
const auto& viewportRect = parameters.viewportRect.isNull() ? sampleRect : parameters.viewportRect;
const auto renderRect = sampleRect.intersected(drawRect).intersected(viewportRect);
const auto renderRect = sampleRect.intersected(viewportRect);
if (renderRect.isNull()) { return; }
const auto sampleRange = parameters.sampleEnd - parameters.sampleStart;
if (sampleRange <= 0 || sampleRange > 1) { return; }
if (sampleRange <= 0.0f || sampleRange > 1.0f) { return; }
const auto targetThumbnailWidth = static_cast<int>(static_cast<double>(sampleRect.width()) / sampleRange);
const auto targetThumbnailWidth = static_cast<int>(sampleRect.width() / sampleRange);
const auto finerThumbnail = std::find_if(m_thumbnailCache->rbegin(), m_thumbnailCache->rend(),
[&](const auto& thumbnail) { return thumbnail.width() >= targetThumbnailWidth; });
if (finerThumbnail == m_thumbnailCache->rend())
{
qDebug() << "Could not find closest finer thumbnail for a target width of" << targetThumbnailWidth;
return;
}
const auto useOriginalBuffer = finerThumbnail == m_thumbnailCache->rend();
const auto drawOriginalBuffer = static_cast<size_t>(targetThumbnailWidth) == m_buffer->size();
painter.save();
painter.setRenderHint(QPainter::Antialiasing, true);
const auto thumbnailBeginForward = std::max<int>(renderRect.x() - sampleRect.x(), static_cast<int>(parameters.sampleStart * targetThumbnailWidth));
const auto thumbnailEndForward = std::max<int>(renderRect.x() + renderRect.width() - sampleRect.x(), static_cast<int>(parameters.sampleEnd * targetThumbnailWidth));
const auto thumbnailBeginForward = std::max<int>(renderRect.x() - sampleRect.x(), parameters.sampleStart * targetThumbnailWidth);
const auto thumbnailEndForward = std::max<int>(renderRect.x() + renderRect.width() - sampleRect.x(), parameters.sampleEnd * targetThumbnailWidth);
const auto thumbnailBegin = parameters.reversed ? targetThumbnailWidth - thumbnailBeginForward - 1 : thumbnailBeginForward;
const auto thumbnailEnd = parameters.reversed ? targetThumbnailWidth - thumbnailEndForward : thumbnailEndForward;
const auto advanceThumbnailBy = parameters.reversed ? -1 : 1;
const auto finerThumbnailScaleFactor = static_cast<double>(finerThumbnail->width()) / targetThumbnailWidth;
const auto yScale = drawRect.height() / 2 * parameters.amplification;
const auto finerThumbnailWidth = useOriginalBuffer ? m_buffer->size() : finerThumbnail->width();
const auto finerThumbnailScaleFactor = static_cast<double>(finerThumbnailWidth) / targetThumbnailWidth;
const auto yScale = renderRect.height() / 2 * parameters.amplification;
for (auto x = renderRect.x(), i = thumbnailBegin; x < renderRect.x() + renderRect.width() && i != thumbnailEnd;
++x, i += advanceThumbnailBy)
++x, i += advanceThumbnailBy)
{
const auto beginAggregationAt = &(*finerThumbnail)[static_cast<int>(std::floor(i * finerThumbnailScaleFactor))];
const auto endAggregationAt = &(*finerThumbnail)[static_cast<int>(std::ceil((i + 1) * finerThumbnailScaleFactor))];
const auto peak = std::accumulate(beginAggregationAt, endAggregationAt, Thumbnail::Peak{});
if (useOriginalBuffer && drawOriginalBuffer)
{
const auto value = m_buffer->data()->data()[i];
painter.drawPoint(x, renderRect.center().y() - value * yScale);
continue;
}
else
{
const auto beginIndex = std::clamp<size_t>(std::floor(i * finerThumbnailScaleFactor), 0, finerThumbnail->width() - 1);
const auto endIndex = std::clamp<size_t>(std::ceil((i + 1) * finerThumbnailScaleFactor), 0, finerThumbnail->width() - 1);
const auto yMin = drawRect.center().y() - peak.min * yScale;
const auto yMax = drawRect.center().y() - peak.max * yScale;
auto minPeak = 0.f;
auto maxPeak = 0.f;
painter.drawLine(x, yMin, x, yMax);
if (useOriginalBuffer)
{
const auto flatBuffer = m_buffer->data()->data();
const auto [min, max] = std::minmax_element(flatBuffer + beginIndex, flatBuffer + endIndex);
minPeak = *min;
maxPeak = *max;
}
else
{
const auto beginAggregationAt = finerThumbnail->data() + beginIndex;
const auto endAggregationAt = finerThumbnail->data() + endIndex;
const auto peak = std::accumulate(beginAggregationAt, endAggregationAt, Thumbnail::Peak{});
minPeak = peak.min;
maxPeak = peak.max;
}
const auto yMin = renderRect.center().y() - minPeak * yScale;
const auto yMax = renderRect.center().y() - maxPeak * yScale;
painter.drawLine(x, yMin, x, yMax);
}
}
painter.restore();

View File

@@ -306,6 +306,8 @@ void ClipView::remove()
// as actually deleting the Clip with the deleteLater function. That being said, it shouldn't
// be possible to make a Clip without a Track (i.e., Clip::getTrack is never nullptr).
m_clip->deleteLater();
m_trackView->update();
}

View File

@@ -37,6 +37,7 @@
#include "SampleThumbnail.h"
#include "Song.h"
#include "StringPairDrag.h"
#include "TrackView.h"
namespace lmms::gui
{
@@ -45,7 +46,8 @@ namespace lmms::gui
SampleClipView::SampleClipView( SampleClip * _clip, TrackView * _tv ) :
ClipView( _clip, _tv ),
m_clip( _clip ),
m_paintPixmap()
m_paintPixmap(),
m_paintPixmapXPosition(0)
{
// update UI and tooltip
updateSample();
@@ -210,15 +212,22 @@ void SampleClipView::paintEvent( QPaintEvent * pe )
if( !needsUpdate() )
{
painter.drawPixmap( 0, 0, m_paintPixmap );
painter.drawPixmap(m_paintPixmapXPosition, 0, m_paintPixmap);
return;
}
setNeedsUpdate( false );
if (m_paintPixmap.isNull() || m_paintPixmap.size() != size())
const auto trackViewWidth = getTrackView()->rect().width();
// Use the clip's height to avoid artifacts when rendering while something else is overlaying the clip.
const auto viewPortRect = QRect(0, 0, trackViewWidth * 2, rect().height());
m_paintPixmapXPosition = std::max(0, pe->rect().x() - trackViewWidth);
if (m_paintPixmap.isNull() || m_paintPixmap.size() != viewPortRect.size())
{
m_paintPixmap = QPixmap(size());
m_paintPixmap = QPixmap(viewPortRect.size());
}
QPainter p( &m_paintPixmap );
@@ -274,12 +283,14 @@ void SampleClipView::paintEvent( QPaintEvent * pe )
float sampleLength = m_clip->sampleLength() * ppb / ticksPerBar;
const auto& sample = m_clip->m_sample;
const auto sampleRextX = static_cast<int>(offsetStart) - m_paintPixmapXPosition;
if (sample.sampleSize() > 0)
{
const auto param = SampleThumbnail::VisualizeParameters{
.sampleRect = QRect(offsetStart, spacing, sampleLength, height() - spacing),
.drawRect = QRect(0, spacing, width(), height() - spacing),
.viewportRect = pe->rect(),
.sampleRect = QRect(sampleRextX, spacing, sampleLength, height() - spacing),
.viewportRect = viewPortRect,
.amplification = sample.amplification(),
.reversed = sample.reversed()
};
@@ -295,12 +306,15 @@ void SampleClipView::paintEvent( QPaintEvent * pe )
// inner border
p.setPen( c.lighter( 135 ) );
p.drawRect( 1, 1, rect().right() - BORDER_WIDTH,
p.drawRect(
-m_paintPixmapXPosition + 1,
1,
rect().right() - BORDER_WIDTH,
rect().bottom() - BORDER_WIDTH );
// outer border
p.setPen( c.darker( 200 ) );
p.drawRect( 0, 0, rect().right(), rect().bottom() );
p.drawRect(-m_paintPixmapXPosition, 0, rect().right(), rect().bottom());
// draw the 'muted' pixmap only if the clip was manualy muted
if( m_clip->isMuted() )
@@ -332,7 +346,7 @@ void SampleClipView::paintEvent( QPaintEvent * pe )
p.end();
painter.drawPixmap( 0, 0, m_paintPixmap );
painter.drawPixmap(m_paintPixmapXPosition, 0, m_paintPixmap);
}

View File

@@ -1216,6 +1216,7 @@ void AutomationEditor::paintEvent(QPaintEvent * pe )
const auto param = SampleThumbnail::VisualizeParameters{
.sampleRect = QRect(startPos, yOffset, sampleWidth, sampleHeight),
.viewportRect = rect(),
.amplification = sample.amplification(),
.sampleStart = static_cast<float>(sample.startFrame()) / sample.sampleSize(),
.sampleEnd = static_cast<float>(sample.endFrame()) / sample.sampleSize(),