Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache and interpolate resolutions when drawing sample waveforms #7574

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/AutomationEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "JournallingObject.h"
#include "MidiClip.h"
#include "SampleClip.h"
#include "SampleWaveform.h"
#include "TimePos.h"
#include "lmms_basics.h"

Expand Down Expand Up @@ -233,6 +234,7 @@ protected slots:

MidiClip* m_ghostNotes = nullptr;
QPointer<SampleClip> m_ghostSample = nullptr; // QPointer to set to nullptr on deletion
SampleWaveform m_waveform;
bool m_renderSample = false;

void centerTopBottomScroll();
Expand Down
2 changes: 2 additions & 0 deletions include/SampleClipView.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#define LMMS_GUI_SAMPLE_CLIP_VIEW_H

#include "ClipView.h"
#include "SampleWaveform.h"

namespace lmms
{
Expand Down Expand Up @@ -63,6 +64,7 @@ public slots:

private:
SampleClip * m_clip;
SampleWaveform m_waveform;
QPixmap m_paintPixmap;
bool splitClip( const TimePos pos ) override;
} ;
Expand Down
25 changes: 16 additions & 9 deletions include/SampleWaveform.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,30 @@
#define LMMS_GUI_SAMPLE_WAVEFORM_H

#include <QPainter>
#include <optional>

#include "Sample.h"
#include "SampleFrame.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;
};
SampleWaveform() = default;
SampleWaveform(const SampleFrame* buffer, size_t size);

static void visualize(Parameters parameters, QPainter& painter, const QRect& rect);
void generate();
void visualize(QPainter& painter, const QRect& rect,
float amplification = 1.0f, bool reversed = false,
std::optional<size_t> from = std::nullopt,
std::optional<size_t> to = std::nullopt);
void reset(const SampleFrame* buffer, size_t size);

private:
const SampleFrame* m_buffer = nullptr;
size_t m_size = 0;
std::unordered_map<int, std::vector<float>> m_min;
std::unordered_map<int, std::vector<float>> m_max;
};
} // namespace lmms::gui

Expand Down
2 changes: 2 additions & 0 deletions plugins/AudioFileProcessor/AudioFileProcessorView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ void AudioFileProcessorView::dropEvent(QDropEvent* de)
}

m_waveView->updateSampleRange();
m_waveView->updateWaveform();
Engine::getSong()->setModified();
de->accept();
}
Expand Down Expand Up @@ -263,6 +264,7 @@ void AudioFileProcessorView::openAudioFile()
castModel<AudioFileProcessor>()->setAudioFile(af);
Engine::getSong()->setModified();
m_waveView->updateSampleRange();
m_waveView->updateWaveform();
}

void AudioFileProcessorView::modelChanged()
Expand Down
13 changes: 8 additions & 5 deletions plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

#include "ConfigManager.h"
#include "FontHelper.h"
#include "Sample.h"
#include "SampleWaveform.h"

#include <QPainter>
Expand All @@ -50,6 +51,11 @@ void AudioFileProcessorWaveView::updateSampleRange()
}
}

void AudioFileProcessorWaveView::updateWaveform()
{
m_waveform.reset(m_sample->data(), m_sample->sampleSize());
}

void AudioFileProcessorWaveView::setTo(int to)
{
m_to = std::min(to, static_cast<int>(m_sample->sampleSize()));
Expand Down Expand Up @@ -89,6 +95,7 @@ AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget* parent, int w, i
configureKnobRelationsAndWaveViews();

updateSampleRange();
updateWaveform();

m_graph.fill(Qt::transparent);
update();
Expand Down Expand Up @@ -339,12 +346,8 @@ void AudioFileProcessorWaveView::updateGraph()
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<size_t>(range()), m_sample->amplification(), m_sample->reversed()};
SampleWaveform::visualize(waveform, p, rect);
m_waveform.visualize(p, rect, m_sample->amplification(), m_sample->reversed(), m_from, m_to);
}

void AudioFileProcessorWaveView::zoom(const bool out)
Expand Down
3 changes: 3 additions & 0 deletions plugins/AudioFileProcessor/AudioFileProcessorWaveView.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@


#include "Knob.h"
#include "SampleWaveform.h"


namespace lmms
Expand Down Expand Up @@ -126,6 +127,7 @@ public slots:
} ;

Sample const* m_sample;
SampleWaveform m_waveform;
QPixmap m_graph;
int m_from;
int m_to;
Expand Down Expand Up @@ -153,6 +155,7 @@ public slots:


void updateSampleRange();
void updateWaveform();
private:
void setTo(int to);
void setFrom(int from);
Expand Down
17 changes: 10 additions & 7 deletions plugins/SlicerT/SlicerTWaveform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ SlicerTWaveform::SlicerTWaveform(int totalWidth, int totalHeight, SlicerT* instr

connect(instrument, &SlicerT::isPlaying, this, &SlicerTWaveform::isPlaying);
connect(instrument, &SlicerT::dataChanged, this, &SlicerTWaveform::updateUI);
connect(instrument, &SlicerT::dataChanged, this, &SlicerTWaveform::updateWaveform);

m_emptySampleIcon = m_emptySampleIcon.createMaskFromColor(QColor(255, 255, 255), Qt::MaskMode::MaskOutColor);

Expand Down Expand Up @@ -115,10 +116,8 @@ 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_waveform.visualize(brush, rect, sample.amplification(), sample.reversed());

// increase brightness in inner color
QBitmap innerMask = m_seekerWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor);
Expand Down Expand Up @@ -173,11 +172,9 @@ void SlicerTWaveform::drawEditorWaveform()
brush.setPen(s_waveformColor);
float 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);
const auto& sample = m_slicerTParent->m_originalSample;
m_waveform.visualize(brush, rect, sample.amplification(), sample.reversed(), startFrame, endFrame);

// increase brightness in inner color
QBitmap innerMask = m_editorWaveform.createMaskFromColor(s_waveformMaskColor, Qt::MaskMode::MaskOutColor);
Expand Down Expand Up @@ -275,6 +272,12 @@ void SlicerTWaveform::updateUI()
update();
}

void SlicerTWaveform::updateWaveform()
{
const auto& sample = m_slicerTParent->m_originalSample;
m_waveform.reset(sample.data(), sample.sampleSize());
}

// updates the closest object and changes the cursor respectivly
void SlicerTWaveform::updateClosest(QMouseEvent* me)
{
Expand Down
5 changes: 5 additions & 0 deletions plugins/SlicerT/SlicerTWaveform.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include <QInputDialog>
#include <QMouseEvent>
#include <QPainter>
#include "SampleWaveform.h"

namespace lmms {

Expand All @@ -44,6 +45,7 @@ class SlicerTWaveform : public QWidget

public slots:
void updateUI();
void updateWaveform();
void isPlaying(float current, float start, float end);

public:
Expand Down Expand Up @@ -112,6 +114,9 @@ public slots:
SlicerT* m_slicerTParent;

QElapsedTimer m_updateTimer;

SampleWaveform m_waveform;

void drawSeekerWaveform();
void drawSeeker();
void drawEditorWaveform();
Expand Down
120 changes: 70 additions & 50 deletions src/gui/SampleWaveform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,73 +23,93 @@
*/

#include "SampleWaveform.h"
#include "interpolation.h"

namespace lmms::gui {

void SampleWaveform::visualize(Parameters parameters, QPainter& painter, const QRect& rect)
SampleWaveform::SampleWaveform(const SampleFrame* buffer, size_t size)
: m_buffer(buffer)
, m_size(size)
{
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;
void SampleWaveform::generate()
{
if (!m_buffer || m_size == 0) { return; }

const auto color = painter.pen().color();
const auto rmsColor = color.lighter(123);
m_min.clear();
m_max.clear();

const float framesPerPixel = std::max(1.0f, static_cast<float>(parameters.size) / width);
const auto maxLevel = static_cast<int>(std::log2(m_size));
for (auto level = 1; level <= maxLevel; ++level)
{
const auto downsampledSize = m_size / static_cast<int>(std::exp2(level));
m_min[level].resize(downsampledSize);
m_max[level].resize(downsampledSize);

if (level == 1)
{
for (auto i = std::size_t{0}; i < downsampledSize; ++i)
{
static auto peakComp = [](const SampleFrame& a, const SampleFrame& b) { return a.average() < b.average(); };
m_min[level][i] = std::min_element(&m_buffer[i * 2], &m_buffer[i * 2 + 2], peakComp)->average();
m_max[level][i] = std::max_element(&m_buffer[i * 2], &m_buffer[i * 2 + 2], peakComp)->average();
}
}
else
{
for (auto i = std::size_t{0}; i < downsampledSize; ++i)
{
m_min[level][i] = *std::min_element(&m_min[level - 1][i * 2], &m_min[level - 1][i * 2 + 2]);
m_max[level][i] = *std::max_element(&m_max[level - 1][i * 2], &m_max[level - 1][i * 2 + 2]);
}
}
}
}

constexpr float maxFramesPerPixel = 512.0f;
const float resolution = std::max(1.0f, framesPerPixel / maxFramesPerPixel);
const float framesPerResolution = framesPerPixel / resolution;
void SampleWaveform::visualize(QPainter& painter, const QRect& rect, float amplification, bool reversed,
std::optional<size_t> from, std::optional<size_t> to)
{
if (!m_buffer || m_size == 0) { return; }

size_t numPixels = std::min(parameters.size, static_cast<size_t>(width));
auto min = std::vector<float>(numPixels, 1);
auto max = std::vector<float>(numPixels, -1);
auto squared = std::vector<float>(numPixels, 0);
const auto sampleBegin = from.value_or(0);
const auto sampleEnd = to.value_or(m_size);
const auto samplesPerPixel = std::max(1, static_cast<int>(sampleEnd - sampleBegin) / rect.width());

const size_t maxFrames = static_cast<size_t>(numPixels * framesPerPixel);
const auto downsampledLevelLow = static_cast<int>(std::log2(samplesPerPixel));
const auto downsampledLevelHigh = static_cast<int>(std::ceil(std::log2(samplesPerPixel)));

auto pixelIndex = std::size_t{0};
const auto downsampledFactorLow = static_cast<int>(std::exp2(downsampledLevelLow));
const auto downsampledFactorHigh = static_cast<int>(std::exp2(downsampledLevelHigh));

for (auto i = std::size_t{0}; i < maxFrames; i += static_cast<std::size_t>(resolution))
{
pixelIndex = i / framesPerPixel;
const auto frameIndex = !parameters.reversed ? i : maxFrames - i;
const auto ratio = downsampledFactorHigh == downsampledFactorLow
? 1.0f
: static_cast<float>(samplesPerPixel - downsampledFactorLow) / (downsampledFactorHigh - downsampledFactorLow);

const auto& frame = parameters.buffer[frameIndex];
const auto value = frame.average();
const auto centerY = rect.center().y();
const auto centerHeight = rect.height() / 2.0f;

if (value > max[pixelIndex]) { max[pixelIndex] = value; }
if (value < min[pixelIndex]) { min[pixelIndex] = value; }
for (auto i = 0; i < rect.width(); ++i)
{
const auto index = static_cast<float>(i * samplesPerPixel);
const auto lowIndex = static_cast<int>(std::floor(index / downsampledFactorLow));
const auto highIndex = static_cast<int>(std::ceil(index / downsampledFactorHigh));

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<int>(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 auto minPeak = linearInterpolate(m_min[downsampledLevelLow][lowIndex], m_min[downsampledLevelHigh][highIndex], ratio);
const auto maxPeak = linearInterpolate(m_max[downsampledLevelLow][lowIndex], m_max[downsampledLevelHigh][highIndex], ratio);

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);
const auto lineMin = centerY - minPeak * centerHeight * amplification;
const auto lineMax = centerY - maxPeak * centerHeight * amplification;
const auto lineX = rect.x() + (reversed ? rect.width() - i : i);
painter.drawLine(lineX, lineMax, lineX, lineMin);
}
}

void SampleWaveform::reset(const SampleFrame* buffer, size_t size)
{
m_buffer = buffer;
m_size = size;
generate();
}

} // namespace lmms::gui
7 changes: 4 additions & 3 deletions src/gui/clips/SampleClipView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ SampleClipView::SampleClipView( SampleClip * _clip, TrackView * _tv ) :
void SampleClipView::updateSample()
{
update();
m_waveform.reset(m_clip->m_sample.data(), m_clip->m_sample.sampleSize());

// set tooltip to filename so that user can see what sample this
// sample-clip contains
setToolTip(
Expand Down Expand Up @@ -272,9 +274,8 @@ void SampleClipView::paintEvent( QPaintEvent * pe )
QRect r = QRect( offset, spacing,
qMax( static_cast<int>( m_clip->sampleLength() * ppb / ticksPerBar ), 1 ), rect().bottom() - 2 * spacing );

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);
const auto& sample = m_clip->sample();
m_waveform.visualize(p, r, sample.amplification(), sample.reversed());

QString name = PathUtil::cleanName(m_clip->m_sample.sampleFile());
paintTextLabel(name, p);
Expand Down
Loading
Loading