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

Add sample thumbnails #7366

Open
wants to merge 76 commits into
base: master
Choose a base branch
from
Open

Conversation

khoidauminh
Copy link
Contributor

@khoidauminh khoidauminh commented Jul 4, 2024

This is an attempt in adding sample thumbnails in order to speed up rendering (mainly) in the song editor and other places (currently being Audio File Processor, SlicerT and the Automation editor).

Brief summary of the PR:

  • A Bit is a sample thumbnail, which is a struct containing max, min and rms.
  • A Thumbnail is a vector of Bits and a ThumbnailCache is a vector of Thumbnails.
  • The class SampleThumbnail contains a non-static member that is a shared_ptr to a ThumbnailCache, and the static member s_sampleThumbnailCacheMap. Both are private.
How this implementation originally avoided duplicates

s_sampleThumbnailCacheMap is an std::map with keys being the sample file path and the value being a shared_ptr to ThumbnailCache. All thumbnail caches are stored here.

This implementation assumes that a unique sample has a unique full file path. If this is violated or the file path is just the file name, the implementation may break completely.

When a sample is loaded into the song editor, the constructor of SampleThumbnail looks into s_sampleThumbnailCacheMap to find the thumbnail cache for this sample. If the sample is new, it proceeds to generate a new thumbnail cache for this sample and insert it into the map. In other places like AFP, SlicerT, when loading a preexisting preset or project, the thumbnail caches might not be generated until you open the plugin GUI, which triggers the paintEvents.

The SampleThumbnail class attaches itself to classes that uses thumbnails, such as SampleClipView. This ensures that thumbnail cache that is being used will have a use_count() of 2 or more (one in the std::map and others in the objects). A use_count() of 1 indicates the sample is out of use and the thumbnail list will be deleted. Cleaning operations is done when loading new samples or closing LMMS.

Currently the global map behavior, explained in the collapsed section above has been deprecated in favor of the (in development) sample cache PR #7058.

As of current, the the thumbnail size divisor is exactly 10.

This PR now also makes use of partial rendering, which offloads some computation cost from zooming to other UI updates, such as scrolling, resizing the window, as it now only renders the areas not yet drawn.

Fixes #7576.
Fixes #7546.

@Rossmaxx
Copy link
Contributor

Rossmaxx commented Jul 4, 2024

This implementation assumes that a unique sample has a unique full file path. If this is violated or the file path is relative, the implementation may break completely.

Factory samples would like to have a word with you

@khoidauminh
Copy link
Contributor Author

khoidauminh commented Jul 4, 2024

This implementation assumes that a unique sample has a unique full file path. If this is violated or the file path is relative, the implementation may break completely.

Factory samples would like to have a word with you

As long as the factory sample paths remain unique for each sample (which I believe they do) then it won't break.

I'm mostly concerned with when the sample name is just the filename.

(also changed the PR description a bit)

@zachtyll
Copy link

You've got some code that is commented out, is it supposed to stay or to be erased?

Since this change is also visual, could you provide a screenshot of what it looks like?

@khoidauminh
Copy link
Contributor Author

@zachtyll Oh right they were supposed to be removed but I forgot to. Will make a commit to clear them out

Here are some pictures and videos (left is the old code and right is the new code):

image

image
image
image

slicert.mp4
samples2.mp4

The sample in this video is 11 minutes long:

automation.mp4

@sakertooth
Copy link
Contributor

sakertooth commented Jul 17, 2024

I see a number of style issues/some code that can be cleaned up. Instead of making a huge review for style, do I have permission to go in and fix it myself? I want to focus on the implementation here (the design/visualization code), not too much of the style, but its an important problem here still.

Edit: But if you would prefer a review, then I don't mind.

@khoidauminh
Copy link
Contributor Author

@sakertooth Feel free to push commits to adjust the style. For the implementation adjustments, I think it can benefit from a bit of discussion/reviewing before commits are pushed

@sakertooth
Copy link
Contributor

sakertooth commented Sep 10, 2024

Hey @khoidauminh, I just pushed some style changes, along with some fixes and rearrangements. Let me know if you have any objections with them. I plan to look into this PR more closely soon.

Edit: Forgot to mention, one thing I noticed is that there is still a lot of lag when the number of sample clips get around 20 and up and each are around 2 minute long. I think another optimization we might consider is just drawing the necessary region, and not the entire thumbnail (not sure if that will fix the issue, but it was something I had in mind).

src/gui/SampleThumbnail.cpp Outdated Show resolved Hide resolved
src/gui/SampleThumbnail.cpp Outdated Show resolved Hide resolved
@sakertooth
Copy link
Contributor

Pushed two commits: one fixes formatting, and the other changes the use of cbegin to use begin instead (begin can work in const contexts)

@khoidauminh
Copy link
Contributor Author

I'm eyeing these two lines:

if (parameters.reversed) { thumbnail.reverse(); }

thumbnail.clip(finerThumbnailBegin, finerThumbnailEnd);

Is there a performance impact on these? I think if we take an iterator-based approach, we could have almost zero performance cost. I know exactly how to do this cleanly in Rust but in C++ I have no idea. LMMS just moved to C++20 so I think this should be possible. Will take a look at the C++ references.

@sakertooth
Copy link
Contributor

Is there a performance impact on these? I think if we take an iterator-based approach, we could have almost zero performance cost. I know exactly how to do this cleanly in Rust but in C++ I have no idea. LMMS just moved to C++20 so I think this should be possible. Will take a look at the C++ references.

You're right. We don't need to copy anything at all and we can take an iterator-based approach to represent the range of samples to consider. Iterators in C++ are the begin and end functions in Containers (see what classifies as a Container here).

In most cases however, it is simple to consider begin and end iterators as just pointers to the first element and one past the last element in a container respectively.

Sidebar, but just as another good C++ reference, the guidelines are really good.

sakertooth and others added 9 commits January 3, 2025 16:07
…nail width to display width

To improve performance with especially long
samples (~20 minutes)
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.
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.
Comment on lines +1224 to +1229
auto param = SampleThumbnail::VisualizeParameters{};
param.amplification = sample.amplification();
param.reversed = sample.reversed();
param.sampleRect = QRect(startPos, yOffset, sampleWidth, sampleHeight);
param.sampleStart = static_cast<float>(sample.startFrame()) / sample.sampleSize();
param.sampleEnd = static_cast<float>(sample.endFrame()) / sample.sampleSize();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use C++20's designated initializers here if you'd like. And also in a few other places where you create a VisualizeParameters object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the immediate benefit in doing that, but I agree we could use it here probably.

Comment on lines +44 to +45
const auto beginSample = buffer + static_cast<size_t>(std::floor(peakIndex * m_samplesPerPeak));
const auto endSample = buffer + static_cast<size_t>(std::ceil((peakIndex + 1) * m_samplesPerPeak));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar enough with the logic to know whether the beginSample and endSample pointers will always be within the bounds of the array. Maybe you could add an assert to ensure that endSample < buffer + size?

Copy link
Contributor

@sakertooth sakertooth Jan 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its worth understanding it so we know for sure. The maximum number peakIndex will reach is width - 1. Therefore, endSample = buffer + ceil(width * m_samplesPerPeak) = buffer + ceil(width * size / width). width is an integer type so they cancel out safely, making endSample = buffer + ceil(size). Since size is an integer type as well, endSample = buffer + size when peakIndex = width - 1.

The problem here is that I shouldn't be accessing buffer viewing it as a bunch of SampleFrames. In the scenario I explained above, endSample points to at most one after the last SampleFrame in the buffer, making endSample->left() invalid. Instead, I need to be viewing the buffer in a flat manner (i.e., disregarding the notion of a left and right channel and accessing the float values directly.)

Or, just minus endSample by 1 and change endSample->left() to endSample->right() + 1.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In summary, the assert(endSample <= buffer + size) will always pass, but std::minmax_element can (and probably will) access invalid data.

Comment on lines +76 to +90
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The s_sampleThumbnailCacheMap accesses aren't thread-safe. Could that be a problem?

Copy link
Contributor

@sakertooth sakertooth Jan 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not at all. Visualization only happens on the UI thread. We also only create SampleThumbnails on the main thread.

Peak* peaks() { return m_peaks.data(); }
const Peak* peaks() const { return m_peaks.data(); }

int width() const { return m_peaks.size(); }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this return std::size_t?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the math involved uses int quite often (stuff like std::max(rect->x(), thumbnail->width()), making width a size_t is more of an inconvenience than anything else, though one could argue that returning size_t is more correct here. Using std::max<int> is an option, but I don't like the possible implicit conversion (which had caused bugs in the past relating to sample waveform visualization)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants