From 786088baec64bec452cbf7fc736582c0a4ac9cb2 Mon Sep 17 00:00:00 2001 From: Cas Pascal <81458575+khoidauminh@users.noreply.github.com> Date: Sun, 9 Feb 2025 08:05:19 +0700 Subject: [PATCH] Improve performance when rendering sample waveforms (#7366) This PR attempts a number of improvements to the sample rendering (in the song editor, automation editor, AudioFileProcessor, SlicerT, etc) in LMMS: Thumbnails: Samples are aggregated into a set of thumbnails of multiple resolutions. The smallest larger thumbnail is then chosen for rendering during sample drawing. Each set of thumbnails is stored with its sample file metadata in an unordered_map, so that duplicate samples will use the same set of thumbnails. Partial rendering: additionally, this PR only renders the portions of the sample clips visible to the viewer to save rendering time. * Experimental sample thumbnail * Rename some classes and type aliases. Make some type declarations explicit * Use a combination of audioFile name and shared_ptrs to track samples; refactor some loops * That weird line at the end of the sample is now gone * Small changes to the code; Add comments * Add missing word to comment; Fix typo * Track `SharedSampleThumbnailList`s instead * Major refactor; implement thumbnailing for SlicerT, AFP and Automation editor * Code clean up, renames and documenting * Add the namespace lmms comments * More code updates and documentation * Fix error in comment * Comment out `qDebug`s * Fix formatting * Use alternative initialization of `SampleThumbnailVisualizeParameters` * Remove commented code * Fix style and simplify code * Use auto * Draw lines using floating point * Merge the classes into one nested class + Replace while loop with std::find_if * Fix comparison of different signedness * Include memory header * Fix a logic error when selecting samples; Rename a const * Fix more issues with signedness * Fix sample drawing error in `visualizeOriginal` * Only render regions in view of the sample * Allow partial repaints of sample clips * Remove unused variable * Limit most of the painting to the visible region * Revert back to using rect() in some places * Partial rendering for AutomationEditor * Don't redraw painted regions; allowHighResolution; remove `visualizeOriginal`; Remove s_sampleThumbnailCacheMap * Add s_sampleThumbnailCacheMap back for testing convenience * Minor change to the way `thumbnailSizeDivisor` is calculated * Extend update region by an amount * forgot about this * Adapt to master; Redesign VisualizeParameters; Don't rely entirely on needsUpdate() * Don't try to preserve painted regions * Allow for a bit more thumbnails; Fix incorrect rendering when vertically scrolling * Fix missing include statement * Remove the unused variable * Code optimization; Remove RMS member from Bit; Rename viewRect to drawRect * More code optimizations * Fix formatting * Use begin instead of cbegin * Improve generation of thumbnails * Improve expressiveness of the draw code * Add support for reversing * Fix drawing code * Fix draw code (part 2) * Apply more fixes and simplifications * Undo some out of scope changes * Remove SampleWaveform * Improve documentation * Use size_t for some counters * Change width parameter to be size_t * Remove temporary aggregated peak variable * Bump up AggregationPerZoomStep to 10 * Zoom out only requested range of thumbnail instead of clipping it separately * Rename targetSampleWidth to targetThumbnailWidth * Handle reversing for AFP; Iterate in reverse instead of reversing the entire thumbnail * Change names to be more accurate * Improve implementation of sample thumbnail cache map * Move AggregationPerZoomStep back down to 2, do not cap smallest thumbnail width to display width To improve performance with especially long samples (~20 minutes) * Simplify sample thumbnail cache handling in constructor * Call drawLines instead of drawLine in a loop QPainter::drawLine calls drawLines with a line count of 1. Therefore, calling drawLine in a loop means we are simply calling drawLines a lot more times than necessary. * Bump up AggregationPerZoomStep to 10 again Thought using 2 would help performance (when rendering). Maybe it does, but it's still quite slow when rendering a bunch of thumbnails at once. * Fix off-by-one error when constructing Thumbnail from buffer * Fix crash when viewport is not in bounds * Apply performance improvements Performance in the zoomOut function was bad because of the dynamic memory allocation. Huge chunks of memory were being allocated quite often, casuing a ton of cache misses and all around slower performance. To fix this, all the necessary downsampling is now done within the for loop and handled one pixel after another, instead of all at once. To further avoid allocations in the draw call, the change to use drawLines has been reverted. We now pass VisualizeParameters by value (it is only 64 bytes, which will fit quite nicely in a cache line, which is more benefical than reaching for that data by reference to some other part of the code). After applying these changes, we are now practically only bounded by the painting done by Qt (according to profiling done by Valgrind). Proper use of OpenGL could resolve this issue, which should finally make the performance quite optimal in variety of situations. * Apply minor changes Update copyright and unused functions. Move in newly created thumbnail into the cache instead of copying it. * Use C++20's designated initializers * Create param right before visualizing * Fix regressions with reversing * Fix incorrect rendering in AFP and SlicerT * Move MaxSampleThumbnailCacheSize and AggregationPerZoomStep into implementation file * Remove static keyword * Remove getter and setter for peak data --------- Co-authored-by: Sotonye Atemie --- include/AutomationEditor.h | 3 + include/SampleClipView.h | 3 + include/SampleThumbnail.h | 143 ++++++++++++++++ include/SampleWaveform.h | 49 ------ .../AudioFileProcessorWaveView.cpp | 24 ++- .../AudioFileProcessorWaveView.h | 2 + plugins/SlicerT/SlicerTWaveform.cpp | 37 +++-- plugins/SlicerT/SlicerTWaveform.h | 4 + src/gui/CMakeLists.txt | 2 +- src/gui/SampleThumbnail.cpp | 157 ++++++++++++++++++ src/gui/SampleWaveform.cpp | 95 ----------- src/gui/clips/SampleClipView.cpp | 25 ++- src/gui/editors/AutomationEditor.cpp | 19 ++- 13 files changed, 386 insertions(+), 177 deletions(-) create mode 100644 include/SampleThumbnail.h delete mode 100644 include/SampleWaveform.h create mode 100644 src/gui/SampleThumbnail.cpp delete mode 100644 src/gui/SampleWaveform.cpp diff --git a/include/AutomationEditor.h b/include/AutomationEditor.h index 15f6dd22ec..eb3d229a3c 100644 --- a/include/AutomationEditor.h +++ b/include/AutomationEditor.h @@ -38,6 +38,7 @@ #include "SampleClip.h" #include "TimePos.h" #include "lmms_basics.h" +#include "SampleThumbnail.h" class QPainter; class QPixmap; @@ -290,6 +291,8 @@ private: QColor m_ghostNoteColor; QColor m_detuningNoteColor; QColor m_ghostSampleColor; + + SampleThumbnail m_sampleThumbnail; friend class AutomationEditorWindow; diff --git a/include/SampleClipView.h b/include/SampleClipView.h index 4ff218fb0e..10ce5b2f30 100644 --- a/include/SampleClipView.h +++ b/include/SampleClipView.h @@ -27,6 +27,8 @@ #include "ClipView.h" +#include "SampleThumbnail.h" + namespace lmms { @@ -63,6 +65,7 @@ protected: private: SampleClip * m_clip; + SampleThumbnail m_sampleThumbnail; QPixmap m_paintPixmap; bool splitClip( const TimePos pos ) override; } ; diff --git a/include/SampleThumbnail.h b/include/SampleThumbnail.h new file mode 100644 index 0000000000..b8e1e85af4 --- /dev/null +++ b/include/SampleThumbnail.h @@ -0,0 +1,143 @@ +/* + * SampleThumbnail.h + * + * Copyright (c) 2024 Khoi Dau + * Copyright (c) 2024 Sotonye Atemie + * + * 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 LMMS_SAMPLE_THUMBNAIL_H +#define LMMS_SAMPLE_THUMBNAIL_H + +#include +#include +#include +#include + +#include "Sample.h" +#include "lmms_export.h" + +namespace lmms { + +/** + Allows for visualizing sample data. + + On construction, thumbnails will be generated + at logarathmic intervals of downsampling. Those cached thumbnails will then be further downsampled on the fly and + transformed in various ways to create the desired waveform. + + Given that we are dealing with far less data to generate + the visualization however (i.e., we are not reading from original sample data when drawing), this provides a + significant performance boost that wouldn't be possible otherwise. + */ +class LMMS_EXPORT SampleThumbnail +{ +public: + struct VisualizeParameters + { + 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. + + float amplification = 1.0f; //!< The amount of amplification to apply to the waveform. + + float sampleStart = 0.0f; //!< Where the sample begins for drawing. + + float sampleEnd = 1.0f; //!< Where the sample ends for drawing. + + bool reversed = false; //!< Determines if the waveform is drawn in reverse or not. + }; + + SampleThumbnail() = default; + SampleThumbnail(const Sample& sample); + void visualize(VisualizeParameters parameters, QPainter& painter) const; + +private: + class Thumbnail + { + public: + struct Peak + { + Peak() = default; + + Peak(float min, float max) + : min(min) + , max(max) + { + } + + Peak(const SampleFrame& frame) + : min(std::min(frame.left(), frame.right())) + , max(std::max(frame.left(), frame.right())) + { + } + + 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::max(); + float max = std::numeric_limits::min(); + }; + + Thumbnail() = default; + Thumbnail(std::vector peaks, double samplesPerPeak); + Thumbnail(const float* buffer, size_t size, size_t width); + + Thumbnail zoomOut(float factor) const; + + Peak& operator[](size_t index) { return m_peaks[index]; } + const Peak& operator[](size_t index) const { return m_peaks[index]; } + + int width() const { return m_peaks.size(); } + double samplesPerPeak() const { return m_samplesPerPeak; } + + private: + std::vector m_peaks; + double m_samplesPerPeak = 0.0; + }; + + struct SampleThumbnailEntry + { + QString filePath; + QDateTime lastModified; + + friend bool operator==(const SampleThumbnailEntry& first, const SampleThumbnailEntry& second) + { + return first.filePath == second.filePath && first.lastModified == second.lastModified; + } + }; + + struct Hash + { + std::size_t operator()(const SampleThumbnailEntry& entry) const noexcept { return qHash(entry.filePath); } + }; + + using ThumbnailCache = std::vector; + std::shared_ptr m_thumbnailCache = std::make_shared(); + + inline static std::unordered_map, Hash> s_sampleThumbnailCacheMap; +}; + +} // namespace lmms + +#endif // LMMS_SAMPLE_THUMBNAIL_H diff --git a/include/SampleWaveform.h b/include/SampleWaveform.h deleted file mode 100644 index ccfc9fb609..0000000000 --- a/include/SampleWaveform.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SampleWaveform.h - * - * Copyright (c) 2023 saker - * - * 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 LMMS_GUI_SAMPLE_WAVEFORM_H -#define LMMS_GUI_SAMPLE_WAVEFORM_H - -#include - -#include "Sample.h" -#include "lmms_export.h" - -namespace lmms::gui { -class LMMS_EXPORT SampleWaveform -{ -public: - struct Parameters - { - const SampleFrame* buffer; - size_t size; - float amplification; - bool reversed; - }; - - static void visualize(Parameters parameters, QPainter& painter, const QRect& rect); -}; -} // namespace lmms::gui - -#endif // LMMS_GUI_SAMPLE_WAVEFORM_H diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp index b41af33e01..f120fbf255 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp @@ -24,16 +24,16 @@ #include "AudioFileProcessorWaveView.h" +#include "Sample.h" #include "ConfigManager.h" +#include "SampleThumbnail.h" #include "FontHelper.h" -#include "SampleWaveform.h" #include #include #include - namespace lmms { @@ -81,7 +81,8 @@ AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget* parent, int w, i m_isDragging(false), m_reversed(false), m_framesPlayed(0), - m_animation(ConfigManager::inst()->value("ui", "animateafp").toInt()) + m_animation(ConfigManager::inst()->value("ui", "animateafp").toInt()), + m_sampleThumbnail(*buf) { setFixedSize(w, h); setMouseTracking(true); @@ -338,13 +339,18 @@ void AudioFileProcessorWaveView::updateGraph() m_graph.fill(Qt::transparent); QPainter p(&m_graph); p.setPen(QColor(255, 255, 255)); - - const auto dataOffset = m_reversed ? m_sample->sampleSize() - m_to : m_from; - const auto rect = QRect{0, 0, m_graph.width(), m_graph.height()}; - const auto waveform = SampleWaveform::Parameters{ - m_sample->data() + dataOffset, static_cast(range()), m_sample->amplification(), m_sample->reversed()}; - SampleWaveform::visualize(waveform, p, rect); + m_sampleThumbnail = SampleThumbnail{*m_sample}; + + const auto param = SampleThumbnail::VisualizeParameters{ + .sampleRect = m_graph.rect(), + .amplification = m_sample->amplification(), + .sampleStart = static_cast(m_from) / m_sample->sampleSize(), + .sampleEnd = static_cast(m_to) / m_sample->sampleSize(), + .reversed = m_sample->reversed(), + }; + + m_sampleThumbnail.visualize(param, p); } void AudioFileProcessorWaveView::zoom(const bool out) diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h index 1251501b02..6440570e66 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h @@ -27,6 +27,7 @@ #include "Knob.h" +#include "SampleThumbnail.h" namespace lmms @@ -144,6 +145,7 @@ private: bool m_reversed; f_cnt_t m_framesPlayed; bool m_animation; + SampleThumbnail m_sampleThumbnail; friend class AudioFileProcessorView; diff --git a/plugins/SlicerT/SlicerTWaveform.cpp b/plugins/SlicerT/SlicerTWaveform.cpp index 17fab42e22..37f55572f4 100644 --- a/plugins/SlicerT/SlicerTWaveform.cpp +++ b/plugins/SlicerT/SlicerTWaveform.cpp @@ -27,7 +27,7 @@ #include #include -#include "SampleWaveform.h" +#include "SampleThumbnail.h" #include "SlicerT.h" #include "SlicerTView.h" #include "embed.h" @@ -115,10 +115,19 @@ void SlicerTWaveform::drawSeekerWaveform() brush.setPen(s_waveformColor); const auto& sample = m_slicerTParent->m_originalSample; - const auto waveform - = SampleWaveform::Parameters{sample.data(), sample.sampleSize(), sample.amplification(), sample.reversed()}; - const auto rect = QRect(0, 0, m_seekerWaveform.width(), m_seekerWaveform.height()); - SampleWaveform::visualize(waveform, brush, rect); + + m_sampleThumbnail = SampleThumbnail{sample}; + + const auto param = SampleThumbnail::VisualizeParameters{ + .sampleRect = m_seekerWaveform.rect(), + .amplification = sample.amplification(), + .sampleStart = static_cast(sample.startFrame()) / sample.sampleSize(), + .sampleEnd = static_cast(sample.endFrame()) / sample.sampleSize(), + .reversed = sample.reversed() + }; + + m_sampleThumbnail.visualize(param, brush); + // increase brightness in inner color QBitmap innerMask = m_seekerWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); @@ -171,13 +180,21 @@ void SlicerTWaveform::drawEditorWaveform() size_t endFrame = m_seekerEnd * m_slicerTParent->m_originalSample.sampleSize(); brush.setPen(s_waveformColor); - float zoomOffset = (m_editorHeight - m_zoomLevel * m_editorHeight) / 2; + long zoomOffset = (m_editorHeight - m_zoomLevel * m_editorHeight) / 2; const auto& sample = m_slicerTParent->m_originalSample; - const auto waveform = SampleWaveform::Parameters{ - sample.data() + startFrame, endFrame - startFrame, sample.amplification(), sample.reversed()}; - const auto rect = QRect(0, zoomOffset, m_editorWidth, m_zoomLevel * m_editorHeight); - SampleWaveform::visualize(waveform, brush, rect); + + m_sampleThumbnail = SampleThumbnail{sample}; + + const auto param = SampleThumbnail::VisualizeParameters{ + .sampleRect = QRect(0, zoomOffset, m_editorWidth, static_cast(m_zoomLevel * m_editorHeight)), + .amplification = sample.amplification(), + .sampleStart = static_cast(startFrame) / sample.sampleSize(), + .sampleEnd = static_cast(endFrame) / sample.sampleSize(), + .reversed = sample.reversed(), + }; + + m_sampleThumbnail.visualize(param, brush); // increase brightness in inner color QBitmap innerMask = m_editorWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor); diff --git a/plugins/SlicerT/SlicerTWaveform.h b/plugins/SlicerT/SlicerTWaveform.h index d22e83f5ea..029b693202 100644 --- a/plugins/SlicerT/SlicerTWaveform.h +++ b/plugins/SlicerT/SlicerTWaveform.h @@ -32,6 +32,8 @@ #include #include +#include "SampleThumbnail.h" + namespace lmms { class SlicerT; @@ -108,6 +110,8 @@ private: QPixmap m_editorWaveform; QPixmap m_sliceEditor; QPixmap m_emptySampleIcon; + + SampleThumbnail m_sampleThumbnail; SlicerT* m_slicerTParent; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index fe4a2c462b..51f4638326 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -35,7 +35,7 @@ SET(LMMS_SRCS gui/RowTableView.cpp gui/SampleLoader.cpp gui/SampleTrackWindow.cpp - gui/SampleWaveform.cpp + gui/SampleThumbnail.cpp gui/SendButtonIndicator.cpp gui/SideBar.cpp gui/SideBarWidget.cpp diff --git a/src/gui/SampleThumbnail.cpp b/src/gui/SampleThumbnail.cpp new file mode 100644 index 0000000000..c31c0d93e9 --- /dev/null +++ b/src/gui/SampleThumbnail.cpp @@ -0,0 +1,157 @@ +/* + * SampleThumbnail.cpp + * + * Copyright (c) 2024 Khoi Dau + * Copyright (c) 2024 Sotonye Atemie + * + * 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 "SampleThumbnail.h" + +#include +#include + +namespace { + constexpr auto MaxSampleThumbnailCacheSize = 32; + constexpr auto AggregationPerZoomStep = 10; +} + +namespace lmms { + +SampleThumbnail::Thumbnail::Thumbnail(std::vector peaks, double samplesPerPeak) + : m_peaks(std::move(peaks)) + , m_samplesPerPeak(samplesPerPeak) +{ +} + +SampleThumbnail::Thumbnail::Thumbnail(const float* buffer, size_t size, size_t width) + : m_peaks(width) + , m_samplesPerPeak(std::max(static_cast(size) / width, 1.0)) +{ + for (auto peakIndex = std::size_t{0}; peakIndex < width; ++peakIndex) + { + const auto beginSample = buffer + static_cast(std::floor(peakIndex * m_samplesPerPeak)); + const auto endSample = buffer + static_cast(std::ceil((peakIndex + 1) * m_samplesPerPeak)); + const auto [min, max] = std::minmax_element(beginSample, endSample); + m_peaks[peakIndex] = Peak{*min, *max}; + } +} + +SampleThumbnail::Thumbnail SampleThumbnail::Thumbnail::zoomOut(float factor) const +{ + assert(factor >= 1 && "Invalid zoom out factor"); + + auto peaks = std::vector(m_peaks.size() / factor); + for (auto peakIndex = std::size_t{0}; peakIndex < peaks.size(); ++peakIndex) + { + const auto beginAggregationAt = m_peaks.begin() + static_cast(std::floor(peakIndex * factor)); + const auto endAggregationAt = m_peaks.begin() + static_cast(std::ceil((peakIndex + 1) * factor)); + peaks[peakIndex] = std::accumulate(beginAggregationAt, endAggregationAt, Peak{}); + } + + return Thumbnail{std::move(peaks), m_samplesPerPeak * factor}; +} + +SampleThumbnail::SampleThumbnail(const Sample& sample) +{ + auto entry = SampleThumbnailEntry{sample.sampleFile(), QFileInfo{sample.sampleFile()}.lastModified()}; + if (!entry.filePath.isEmpty()) + { + const auto it = s_sampleThumbnailCacheMap.find(entry); + if (it != s_sampleThumbnailCacheMap.end()) + { + m_thumbnailCache = it->second; + return; + } + + if (s_sampleThumbnailCacheMap.size() == MaxSampleThumbnailCacheSize) + { + const auto leastUsed = std::min_element(s_sampleThumbnailCacheMap.begin(), s_sampleThumbnailCacheMap.end(), + [](const auto& a, const auto& b) { return a.second.use_count() < b.second.use_count(); }); + s_sampleThumbnailCacheMap.erase(leastUsed->first); + } + + 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); + + while (m_thumbnailCache->back().width() >= AggregationPerZoomStep) + { + auto zoomedOutThumbnail = m_thumbnailCache->back().zoomOut(AggregationPerZoomStep); + m_thumbnailCache->emplace_back(std::move(zoomedOutThumbnail)); + } +} + +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 renderRect = sampleRect.intersected(drawRect).intersected(viewportRect); + if (renderRect.isNull()) { return; } + + const auto sampleRange = parameters.sampleEnd - parameters.sampleStart; + if (sampleRange <= 0 || sampleRange > 1) { return; } + + const auto targetThumbnailWidth = static_cast(static_cast(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; + } + + painter.save(); + painter.setRenderHint(QPainter::Antialiasing, true); + + const auto thumbnailBeginForward = std::max(renderRect.x() - sampleRect.x(), static_cast(parameters.sampleStart * targetThumbnailWidth)); + const auto thumbnailEndForward = std::max(renderRect.x() + renderRect.width() - sampleRect.x(), static_cast(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(finerThumbnail->width()) / targetThumbnailWidth; + const auto yScale = drawRect.height() / 2 * parameters.amplification; + + for (auto x = renderRect.x(), i = thumbnailBegin; x < renderRect.x() + renderRect.width() && i != thumbnailEnd; + ++x, i += advanceThumbnailBy) + { + const auto beginAggregationAt = &(*finerThumbnail)[static_cast(std::floor(i * finerThumbnailScaleFactor))]; + const auto endAggregationAt = &(*finerThumbnail)[static_cast(std::ceil((i + 1) * finerThumbnailScaleFactor))]; + const auto peak = std::accumulate(beginAggregationAt, endAggregationAt, Thumbnail::Peak{}); + + const auto yMin = drawRect.center().y() - peak.min * yScale; + const auto yMax = drawRect.center().y() - peak.max * yScale; + + painter.drawLine(x, yMin, x, yMax); + } + + painter.restore(); +} + +} // namespace lmms diff --git a/src/gui/SampleWaveform.cpp b/src/gui/SampleWaveform.cpp deleted file mode 100644 index 165ede4ee8..0000000000 --- a/src/gui/SampleWaveform.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SampleWaveform.cpp - * - * Copyright (c) 2023 saker - * - * 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 "SampleWaveform.h" - -namespace lmms::gui { - -void SampleWaveform::visualize(Parameters parameters, QPainter& painter, const QRect& rect) -{ - const int x = rect.x(); - const int height = rect.height(); - const int width = rect.width(); - const int centerY = rect.center().y(); - - const int halfHeight = height / 2; - - const auto color = painter.pen().color(); - const auto rmsColor = color.lighter(123); - - const float framesPerPixel = std::max(1.0f, static_cast(parameters.size) / width); - - constexpr float maxFramesPerPixel = 512.0f; - const float resolution = std::max(1.0f, framesPerPixel / maxFramesPerPixel); - const float framesPerResolution = framesPerPixel / resolution; - - size_t numPixels = std::min(parameters.size, static_cast(width)); - auto min = std::vector(numPixels, 1); - auto max = std::vector(numPixels, -1); - auto squared = std::vector(numPixels, 0); - - const size_t maxFrames = static_cast(numPixels * framesPerPixel); - - auto pixelIndex = std::size_t{0}; - - for (auto i = std::size_t{0}; i < maxFrames; i += static_cast(resolution)) - { - pixelIndex = i / framesPerPixel; - const auto frameIndex = !parameters.reversed ? i : maxFrames - i; - - const auto& frame = parameters.buffer[frameIndex]; - const auto value = frame.average(); - - if (value > max[pixelIndex]) { max[pixelIndex] = value; } - if (value < min[pixelIndex]) { min[pixelIndex] = value; } - - squared[pixelIndex] += value * value; - } - - if (pixelIndex < numPixels) - { - numPixels = pixelIndex; - } - - for (auto i = std::size_t{0}; i < numPixels; i++) - { - const int lineY1 = centerY - max[i] * halfHeight * parameters.amplification; - const int lineY2 = centerY - min[i] * halfHeight * parameters.amplification; - const int lineX = static_cast(i) + x; - painter.drawLine(lineX, lineY1, lineX, lineY2); - - const float rms = std::sqrt(squared[i] / framesPerResolution); - const float maxRMS = std::clamp(rms, min[i], max[i]); - const float minRMS = std::clamp(-rms, min[i], max[i]); - - const int rmsLineY1 = centerY - maxRMS * halfHeight * parameters.amplification; - const int rmsLineY2 = centerY - minRMS * halfHeight * parameters.amplification; - - painter.setPen(rmsColor); - painter.drawLine(lineX, rmsLineY1, lineX, rmsLineY2); - painter.setPen(color); - } -} - -} // namespace lmms::gui diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index a7251be8de..d5cfb211ee 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -34,7 +34,7 @@ #include "PathUtil.h" #include "SampleClip.h" #include "SampleLoader.h" -#include "SampleWaveform.h" +#include "SampleThumbnail.h" #include "Song.h" #include "StringPairDrag.h" @@ -61,6 +61,9 @@ SampleClipView::SampleClipView( SampleClip * _clip, TrackView * _tv ) : void SampleClipView::updateSample() { update(); + + m_sampleThumbnail = SampleThumbnail{m_clip->m_sample}; + // set tooltip to filename so that user can see what sample this // sample-clip contains setToolTip( @@ -267,14 +270,22 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) float nom = Engine::getSong()->getTimeSigModel().getNumerator(); float den = Engine::getSong()->getTimeSigModel().getDenominator(); float ticksPerBar = DefaultTicksPerBar * nom / den; - - float offset = m_clip->startTimeOffset() / ticksPerBar * pixelsPerBar(); - QRect r = QRect( offset, spacing, - qMax( static_cast( m_clip->sampleLength() * ppb / ticksPerBar ), 1 ), rect().bottom() - 2 * spacing ); + float offsetStart = m_clip->startTimeOffset() / ticksPerBar * pixelsPerBar(); + float sampleLength = m_clip->sampleLength() * ppb / ticksPerBar; const auto& sample = m_clip->m_sample; - const auto waveform = SampleWaveform::Parameters{sample.data(), sample.sampleSize(), sample.amplification(), sample.reversed()}; - SampleWaveform::visualize(waveform, p, r); + 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(), + .amplification = sample.amplification(), + .reversed = sample.reversed() + }; + + m_sampleThumbnail.visualize(param, p); + } QString name = PathUtil::cleanName(m_clip->m_sample.sampleFile()); paintTextLabel(name, p); diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index 39016d54d6..ad7d11bd01 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -39,7 +39,7 @@ #include #include "SampleClip.h" -#include "SampleWaveform.h" +#include "SampleThumbnail.h" #include "ActionGroup.h" #include "AutomationNode.h" @@ -1021,6 +1021,7 @@ void AutomationEditor::setGhostSample(SampleClip* newGhostSample) // Expects a pointer to a Sample buffer or nullptr. m_ghostSample = newGhostSample; m_renderSample = true; + m_sampleThumbnail = SampleThumbnail{newGhostSample->sample()}; } void AutomationEditor::paintEvent(QPaintEvent * pe ) @@ -1213,12 +1214,18 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) int yOffset = (editorHeight - sampleHeight) / 2.0f + TOP_MARGIN; p.setPen(m_ghostSampleColor); - + const auto& sample = m_ghostSample->sample(); - const auto waveform = SampleWaveform::Parameters{ - sample.data(), sample.sampleSize(), sample.amplification(), sample.reversed()}; - const auto rect = QRect(startPos, yOffset, sampleWidth, sampleHeight); - SampleWaveform::visualize(waveform, p, rect); + + const auto param = SampleThumbnail::VisualizeParameters{ + .sampleRect = QRect(startPos, yOffset, sampleWidth, sampleHeight), + .amplification = sample.amplification(), + .sampleStart = static_cast(sample.startFrame()) / sample.sampleSize(), + .sampleEnd = static_cast(sample.endFrame()) / sample.sampleSize(), + .reversed = sample.reversed() + }; + + m_sampleThumbnail.visualize(param, p); } // draw ghost notes