diff --git a/include/AutomationEditor.h b/include/AutomationEditor.h index 15f6dd22ec5..eb3d229a3c0 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 @@ protected slots: 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 4ff218fb0ef..10ce5b2f300 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 @@ public slots: 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 00000000000..b8e1e85af46 --- /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 ccfc9fb609e..00000000000 --- 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 b41af33e016..f120fbf255e 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 1251501b02d..6440570e660 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 @@ public slots: 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 17fab42e220..37f55572f4c 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 d22e83f5eaa..029b6932027 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 @@ public slots: 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 fe4a2c462b2..51f4638326a 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 00000000000..c31c0d93e9c --- /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 165ede4ee89..00000000000 --- 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 a7251be8de6..d5cfb211ee8 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 0e56b934ef8..5514e57395a 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" #ifndef __USE_XOPEN #define __USE_XOPEN @@ -1025,6 +1025,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 ) @@ -1217,12 +1218,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