Files
lmms/include/AutomationEditor.h
Cas Pascal 786088baec 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 <sakertooth@gmail.com>
2025-02-08 20:05:19 -05:00

384 lines
9.8 KiB
C++

/*
* AutomationEditor.h - declaration of class AutomationEditor which is a window
* where you can edit dynamic values in an easy way
*
* Copyright (c) 2006-2008 Javier Serrano Polo <jasp00/at/users.sourceforge.net>
*
* 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_AUTOMATION_EDITOR_H
#define LMMS_GUI_AUTOMATION_EDITOR_H
#include <QPushButton>
#include <QWidget>
#include <array>
#include "AutomationClip.h"
#include "ComboBoxModel.h"
#include "Editor.h"
#include "JournallingObject.h"
#include "MidiClip.h"
#include "SampleClip.h"
#include "TimePos.h"
#include "lmms_basics.h"
#include "SampleThumbnail.h"
class QPainter;
class QPixmap;
class QScrollBar;
namespace lmms
{
class NotePlayHandle;
namespace gui
{
class Knob;
class ComboBox;
class TimeLineWidget;
class AutomationEditor : public QWidget, public JournallingObject
{
Q_OBJECT
Q_PROPERTY(QColor barLineColor MEMBER m_barLineColor)
Q_PROPERTY(QColor beatLineColor MEMBER m_beatLineColor)
Q_PROPERTY(QColor lineColor MEMBER m_lineColor)
Q_PROPERTY(QColor nodeInValueColor MEMBER m_nodeInValueColor)
Q_PROPERTY(QColor nodeOutValueColor MEMBER m_nodeOutValueColor)
Q_PROPERTY(QColor nodeTangentLineColor MEMBER m_nodeTangentLineColor)
Q_PROPERTY(QBrush scaleColor MEMBER m_scaleColor)
Q_PROPERTY(QBrush graphColor MEMBER m_graphColor)
Q_PROPERTY(QColor crossColor MEMBER m_crossColor)
Q_PROPERTY(QColor backgroundShade MEMBER m_backgroundShade)
Q_PROPERTY(QColor ghostNoteColor MEMBER m_ghostNoteColor)
Q_PROPERTY(QColor detuningNoteColor MEMBER m_detuningNoteColor)
Q_PROPERTY(QColor ghostSampleColor MEMBER m_ghostSampleColor)
public:
void setCurrentClip(AutomationClip * new_clip);
void setGhostMidiClip(MidiClip* newMidiClip);
void setGhostSample(SampleClip* newSample);
inline const AutomationClip * currentClip() const
{
return m_clip;
}
inline bool validClip() const
{
return m_clip != nullptr;
}
void saveSettings(QDomDocument & doc, QDomElement & parent) override;
void loadSettings(const QDomElement & parent) override;
QString nodeName() const override
{
return "automationeditor";
}
enum class EditMode
{
Draw,
Erase,
DrawOutValues,
EditTangents
};
public slots:
void update();
void updateAfterClipChange();
protected:
using timeMap = AutomationClip::timeMap;
void keyPressEvent(QKeyEvent * ke) override;
void leaveEvent(QEvent * e) override;
void mousePressEvent(QMouseEvent * mouseEvent) override;
void mouseDoubleClickEvent(QMouseEvent * mouseEvent) override;
void mouseReleaseEvent(QMouseEvent * mouseEvent) override;
void mouseMoveEvent(QMouseEvent * mouseEvent) override;
void paintEvent(QPaintEvent * pe) override;
void resizeEvent(QResizeEvent * re) override;
void wheelEvent(QWheelEvent * we) override;
float getLevel( int y );
int xCoordOfTick( int tick );
float yCoordOfLevel( float level );
inline void drawLevelTick(QPainter & p, int tick, float value);
timeMap::iterator getNodeAt(int x, int y, bool outValue = false, int r = 5);
/**
* @brief Given a mouse X coordinate, returns a timeMap::iterator that points to
* the closest node.
* @param Int X coordinate
* @return timeMap::iterator with the closest node or timeMap.end() if there are no nodes.
*/
timeMap::iterator getClosestNode(int x);
void drawLine( int x0, float y0, int x1, float y1 );
bool fineTuneValue(timeMap::iterator node, bool editingOutValue);
protected slots:
void play();
void stop();
void horScrolled( int new_pos );
void verScrolled( int new_pos );
void setEditMode(AutomationEditor::EditMode mode);
void setEditMode(int mode);
void setProgressionType(AutomationClip::ProgressionType type);
/**
* @brief This method handles the AutomationEditorWindow event of changing
* progression types. After that, it calls updateEditTanButton so the edit
* tangents button is updated accordingly
* @param Int New progression type
*/
void setProgressionType(int type);
void setTension();
void updatePosition( const lmms::TimePos & t );
void zoomingXChanged();
void zoomingYChanged();
/// Updates the clip's quantization using the current user selected value.
void setQuantization();
void resetGhostNotes()
{
m_ghostNotes = nullptr;
m_ghostSample = nullptr;
update();
}
private:
enum class Action
{
None,
MoveValue,
EraseValues,
MoveOutValue,
ResetOutValues,
DrawLine,
MoveTangent,
ResetTangents
} ;
// some constants...
static const int SCROLLBAR_SIZE = 12;
static const int TOP_MARGIN = 16;
static const int DEFAULT_Y_DELTA = 6;
static const int DEFAULT_STEPS_PER_BAR = 16;
static const int DEFAULT_PPB = 12 * DEFAULT_STEPS_PER_BAR;
static const int VALUES_WIDTH = 64;
static const int NOTE_HEIGHT = 10; // height of individual notes
static const int NOTE_MARGIN = 40; // total border margin for notes
static const int MIN_NOTE_RANGE = 20; // min number of keys for fixed size
static const int SAMPLE_MARGIN = 40;
static constexpr int MAX_SAMPLE_HEIGHT = 400; // constexpr for use in min
AutomationEditor();
AutomationEditor( const AutomationEditor & );
~AutomationEditor() override;
QPixmap m_toolDraw = embed::getIconPixmap("edit_draw");
QPixmap m_toolErase = embed::getIconPixmap("edit_erase");
QPixmap m_toolDrawOut = embed::getIconPixmap("edit_draw_outvalue");
QPixmap m_toolEditTangents = embed::getIconPixmap("edit_tangent");
QPixmap m_toolMove = embed::getIconPixmap("edit_move");
QPixmap m_toolYFlip = embed::getIconPixmap("flip_y");
QPixmap m_toolXFlip = embed::getIconPixmap("flip_x");
ComboBoxModel m_zoomingXModel;
ComboBoxModel m_zoomingYModel;
ComboBoxModel m_quantizeModel;
static const std::array<float, 7> m_zoomXLevels;
FloatModel * m_tensionModel;
AutomationClip * m_clip;
float m_minLevel;
float m_maxLevel;
float m_step;
float m_scrollLevel;
float m_bottomLevel;
float m_topLevel;
MidiClip* m_ghostNotes = nullptr;
QPointer<SampleClip> m_ghostSample = nullptr; // QPointer to set to nullptr on deletion
bool m_renderSample = false;
void centerTopBottomScroll();
void updateTopBottomLevels();
QScrollBar * m_leftRightScroll;
QScrollBar * m_topBottomScroll;
void adjustLeftRightScoll(int value);
TimePos m_currentPosition;
Action m_action;
int m_moveXOffset;
float m_drawLastLevel;
tick_t m_drawLastTick;
int m_ppb;
int m_y_delta;
bool m_y_auto;
// Time position (key) of automation node whose outValue is being dragged
int m_draggedOutValueKey;
// The tick from the node whose tangent is being dragged
int m_draggedTangentTick;
// Whether the tangent being dragged is the InTangent or OutTangent
bool m_draggedOutTangent;
EditMode m_editMode;
bool m_mouseDownLeft;
bool m_mouseDownRight; //true if right click is being held down
TimeLineWidget * m_timeLine;
bool m_scrollBack;
void drawCross(QPainter & p );
void drawAutomationPoint( QPainter & p, timeMap::iterator it );
void drawAutomationTangents(QPainter& p, timeMap::iterator it);
bool inPatternEditor();
QColor m_barLineColor;
QColor m_beatLineColor;
QColor m_lineColor;
QBrush m_graphColor;
QColor m_nodeInValueColor;
QColor m_nodeOutValueColor;
QColor m_nodeTangentLineColor;
QBrush m_scaleColor;
QColor m_crossColor;
QColor m_backgroundShade;
QColor m_ghostNoteColor;
QColor m_detuningNoteColor;
QColor m_ghostSampleColor;
SampleThumbnail m_sampleThumbnail;
friend class AutomationEditorWindow;
signals:
void currentClipChanged();
void positionChanged( const lmms::TimePos & );
} ;
class AutomationEditorWindow : public Editor
{
Q_OBJECT
static const int INITIAL_WIDTH = 860;
static const int INITIAL_HEIGHT = 480;
public:
AutomationEditorWindow();
~AutomationEditorWindow() override = default;
void setCurrentClip(AutomationClip* clip);
void setGhostMidiClip(MidiClip* clip) { m_editor->setGhostMidiClip(clip); };
void setGhostSample(SampleClip* newSample) { m_editor->setGhostSample(newSample); };
const AutomationClip* currentClip();
void dropEvent( QDropEvent * _de ) override;
void dragEnterEvent( QDragEnterEvent * _dee ) override;
void open(AutomationClip* clip);
AutomationEditor* m_editor;
QSize sizeHint() const override;
public slots:
void clearCurrentClip();
signals:
void currentClipChanged();
protected:
void focusInEvent(QFocusEvent * event) override;
protected slots:
void play() override;
void stop() override;
private slots:
void updateWindowTitle();
void setProgressionType(int progType);
/**
* @brief The Edit Tangent edit mode should only be available for
* Cubic Hermite progressions, so this method is responsable for disabling it
* for other edit modes and reenabling it when it changes back to the Edit Tangent
* mode.
*/
void updateEditTanButton();
private:
QAction* m_drawAction;
QAction* m_eraseAction;
QAction* m_drawOutAction;
QAction* m_editTanAction;
QAction* m_discreteAction;
QAction* m_linearAction;
QAction* m_cubicHermiteAction;
QAction* m_flipYAction;
QAction* m_flipXAction;
Knob * m_tensionKnob;
ComboBox * m_zoomingXComboBox;
ComboBox * m_zoomingYComboBox;
ComboBox * m_quantizeComboBox;
QPushButton* m_resetGhostNotes;
};
} // namespace gui
} // namespace lmms
#endif // LMMS_GUI_AUTOMATION_EDITOR_H