Ghost notes for the automation editor (#6940)

Show ghost notes or sample track as a visual aid in the Automation Editor.

---------

Co-authored-by: IanCaio <iancaio_dev@hotmail.com>
This commit is contained in:
DanielKauss
2023-11-25 12:16:56 +01:00
committed by GitHub
parent 3a928d80b2
commit c2811aebef
11 changed files with 201 additions and 25 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -33,6 +33,10 @@ lmms--gui--AutomationEditor {
qproperty-scaleColor: qlineargradient(spread:reflect,
x1:0, y1:0.5, x2:1, y2:0.5,
stop:0 #333, stop:1 #202020);
qproperty-ghostNoteColor: rgba(248, 248, 255, 125);
qproperty-detuningNoteColor: rgba(248, 11, 11, 125);
qproperty-ghostSampleColor: rgba(125, 125, 125, 125);
}
/* text box */

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -66,6 +66,9 @@ lmms--gui--AutomationEditor {
qproperty-graphColor: rgba(69,42,153,180);
qproperty-scaleColor: #262b30;
qproperty-ghostNoteColor: rgba(248, 248, 255, 125);
qproperty-detuningNoteColor: rgba(248, 11, 11, 125);
qproperty-ghostSampleColor: rgba(125, 125, 125, 125);
}
/* text box */

View File

@@ -26,16 +26,18 @@
#ifndef LMMS_GUI_AUTOMATION_EDITOR_H
#define LMMS_GUI_AUTOMATION_EDITOR_H
#include <QPushButton>
#include <QWidget>
#include <array>
#include "Editor.h"
#include "lmms_basics.h"
#include "JournallingObject.h"
#include "TimePos.h"
#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"
class QPainter;
class QPixmap;
@@ -68,8 +70,13 @@ class AutomationEditor : public QWidget, public JournallingObject
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
{
@@ -159,6 +166,13 @@ protected slots:
/// 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
@@ -183,6 +197,12 @@ private:
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;
@@ -211,6 +231,10 @@ private:
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();
@@ -261,6 +285,9 @@ private:
QBrush m_scaleColor;
QColor m_crossColor;
QColor m_backgroundShade;
QColor m_ghostNoteColor;
QColor m_detuningNoteColor;
QColor m_ghostSampleColor;
friend class AutomationEditorWindow;
@@ -284,6 +311,9 @@ public:
~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;
@@ -337,6 +367,8 @@ private:
ComboBox * m_zoomingXComboBox;
ComboBox * m_zoomingYComboBox;
ComboBox * m_quantizeComboBox;
QPushButton* m_resetGhostNotes;
};
} // namespace gui

View File

@@ -71,6 +71,7 @@ public slots:
protected slots:
void openInPianoRoll();
void setGhostInPianoRoll();
void setGhostInAutomationEditor();
void resetName();
void changeName();
@@ -100,7 +101,7 @@ private:
QColor m_mutedNoteBorderColor;
QStaticText m_staticTextName;
bool m_legacySEPattern;
} ;

View File

@@ -47,6 +47,7 @@ public:
public slots:
void updateSample();
void reverseSample();
void setAutomationGhost();

View File

@@ -25,13 +25,16 @@
#include "MidiClipView.h"
#include <algorithm>
#include <cmath>
#include <QApplication>
#include <QInputDialog>
#include <QMenu>
#include <QPainter>
#include <cmath>
#include "AutomationEditor.h"
#include "ConfigManager.h"
#include "DeprecationHelper.h"
#include "GuiApplication.h"
@@ -85,10 +88,11 @@ void MidiClipView::update()
void MidiClipView::openInPianoRoll()
{
getGUI()->pianoRoll()->setCurrentMidiClip( m_clip );
getGUI()->pianoRoll()->parentWidget()->show();
getGUI()->pianoRoll()->show();
getGUI()->pianoRoll()->setFocus();
auto pRoll = getGUI()->pianoRoll();
pRoll->setCurrentMidiClip(m_clip);
pRoll->parentWidget()->show();
pRoll->show();
pRoll->setFocus();
}
@@ -97,14 +101,21 @@ void MidiClipView::openInPianoRoll()
void MidiClipView::setGhostInPianoRoll()
{
getGUI()->pianoRoll()->setGhostMidiClip( m_clip );
getGUI()->pianoRoll()->parentWidget()->show();
getGUI()->pianoRoll()->show();
getGUI()->pianoRoll()->setFocus();
auto pRoll = getGUI()->pianoRoll();
pRoll->setGhostMidiClip(m_clip);
pRoll->parentWidget()->show();
pRoll->show();
pRoll->setFocus();
}
void MidiClipView::setGhostInAutomationEditor()
{
auto aEditor = getGUI()->automationEditor();
aEditor->setGhostMidiClip(m_clip);
aEditor->parentWidget()->show();
aEditor->show();
aEditor->setFocus();
}
void MidiClipView::resetName() { m_clip->setName(""); }
@@ -192,7 +203,13 @@ void MidiClipView::constructContextMenu( QMenu * _cm )
_cm->insertAction( _cm->actions()[1], b );
connect( b, SIGNAL(triggered(bool)),
this, SLOT(setGhostInPianoRoll()));
_cm->insertSeparator( _cm->actions()[2] );
auto c = new QAction(embed::getIconPixmap("automation_ghost_note"), tr("Set as ghost in automation editor"), _cm);
if (m_clip->empty()) { c->setEnabled(false); }
_cm->insertAction(_cm->actions()[2], c);
connect(c, &QAction::triggered, this, &MidiClipView::setGhostInAutomationEditor);
_cm->insertSeparator(_cm->actions()[3]);
_cm->addSeparator();
_cm->addAction( embed::getIconPixmap( "edit_erase" ),

View File

@@ -28,6 +28,8 @@
#include <QMenu>
#include <QPainter>
#include "GuiApplication.h"
#include "AutomationEditor.h"
#include "embed.h"
#include "PathUtil.h"
#include "SampleBuffer.h"
@@ -83,6 +85,12 @@ void SampleClipView::constructContextMenu(QMenu* cm)
SLOT(reverseSample())
);
cm->addAction(
embed::getIconPixmap("automation_ghost_note"),
tr("Set as ghost in automation editor"),
this,
SLOT(setAutomationGhost())
);
}
@@ -321,6 +329,14 @@ void SampleClipView::reverseSample()
void SampleClipView::setAutomationGhost()
{
auto aEditor = gui::getGUI()->automationEditor();
aEditor->setGhostSample(m_clip);
aEditor->parentWidget()->show();
aEditor->show();
aEditor->setFocus();
}
//! Split this Clip.
/*! \param pos the position of the split, relative to the start of the clip */

View File

@@ -27,8 +27,6 @@
#include "AutomationEditor.h"
#include <cmath>
#include <QApplication>
#include <QInputDialog>
#include <QKeyEvent>
@@ -38,6 +36,9 @@
#include <QScrollBar>
#include <QStyleOption>
#include <QToolTip>
#include <cmath>
#include "SampleClip.h"
#ifndef __USE_XOPEN
#define __USE_XOPEN
@@ -46,20 +47,23 @@
#include "ActionGroup.h"
#include "AutomationNode.h"
#include "ComboBox.h"
#include "debug.h"
#include "DeprecationHelper.h"
#include "embed.h"
#include "DetuningHelper.h"
#include "Engine.h"
#include "GuiApplication.h"
#include "gui_templates.h"
#include "Knob.h"
#include "MainWindow.h"
#include "MidiClip.h"
#include "PatternStore.h"
#include "PianoRoll.h"
#include "ProjectJournal.h"
#include "SampleBuffer.h"
#include "StringPairDrag.h"
#include "TextFloat.h"
#include "TimeLineWidget.h"
#include "debug.h"
#include "embed.h"
#include "gui_templates.h"
namespace lmms::gui
@@ -101,7 +105,8 @@ AutomationEditor::AutomationEditor() :
m_nodeTangentLineColor(0, 0, 0),
m_scaleColor(Qt::SolidPattern),
m_crossColor(0, 0, 0),
m_backgroundShade(0, 0, 0)
m_backgroundShade(0, 0, 0),
m_ghostNoteColor(0, 0, 0)
{
connect( this, SIGNAL(currentClipChanged()),
this, SLOT(updateAfterClipChange()),
@@ -1032,8 +1037,19 @@ inline void AutomationEditor::drawAutomationTangents(QPainter& p, timeMap::itera
p.drawEllipse(tx - 3, ty - 3, 6, 6);
}
void AutomationEditor::setGhostMidiClip(MidiClip* newMidiClip)
{
// Expects a pointer to a MIDI clip or nullptr.
m_ghostNotes = newMidiClip;
m_renderSample = false;
}
void AutomationEditor::setGhostSample(SampleClip* newGhostSample)
{
// Expects a pointer to a Sample buffer or nullptr.
m_ghostSample = newGhostSample;
m_renderSample = true;
}
void AutomationEditor::paintEvent(QPaintEvent * pe )
{
@@ -1219,6 +1235,81 @@ void AutomationEditor::paintEvent(QPaintEvent * pe )
p.drawLine( x, grid_bottom, x, x_line_end );
}
// draw ghost sample
if (m_ghostSample != nullptr && m_ghostSample->sampleBuffer()->frames() > 1 && m_renderSample)
{
int sampleFrames = m_ghostSample->sampleBuffer()->frames();
int length = static_cast<float>(sampleFrames) / Engine::framesPerTick();
int editorHeight = grid_bottom - TOP_MARGIN;
int startPos = xCoordOfTick(0);
int sampleWidth = xCoordOfTick(length) - startPos;
int sampleHeight = std::min(editorHeight - SAMPLE_MARGIN, MAX_SAMPLE_HEIGHT);
int yOffset = (editorHeight - sampleHeight) / 2.0f + TOP_MARGIN;
p.setPen(m_ghostSampleColor);
m_ghostSample->sampleBuffer()->visualize(p, QRect(startPos, yOffset, sampleWidth, sampleHeight), 0, sampleFrames);
}
// draw ghost notes
if (m_ghostNotes != nullptr && !m_renderSample)
{
const NoteVector& notes = m_ghostNotes->notes();
int minKey = 128;
int maxKey = 0;
int detuningOffset = 0;
const Note* detuningNote = nullptr;
for (const Note* note : notes)
{
int noteKey = note->key();
if (note->detuning()->automationClip() == m_clip) {
detuningOffset = note->pos();
detuningNote = note;
}
maxKey = std::max(maxKey, noteKey);
minKey = std::min(minKey, noteKey);
}
for (const Note* note : notes)
{
int lenTicks = note->length();
int notePos = note->pos();
// offset note if detuning
if (notePos+lenTicks < detuningOffset) { continue; }
notePos -= detuningOffset;
// remove/change after #5902
if (lenTicks == 0) { continue; }
else if (lenTicks < 0) { lenTicks = 4; }
int note_width = lenTicks * m_ppb / TimePos::ticksPerBar();
int keyRange = maxKey - minKey;
if (keyRange < MIN_NOTE_RANGE)
{
int padding = (MIN_NOTE_RANGE - keyRange) / 2.0f;
maxKey += padding;
minKey -= padding;
keyRange = MIN_NOTE_RANGE;
}
float absNoteHeight = static_cast<float>(note->key() - minKey) / (maxKey - minKey);
int graphHeight = grid_bottom - NOTE_HEIGHT - NOTE_MARGIN - TOP_MARGIN;
const int y = (graphHeight - graphHeight * absNoteHeight) + NOTE_HEIGHT / 2.0f + TOP_MARGIN;
const int x = xCoordOfTick(notePos);
if (note == detuningNote) {
p.fillRect(x, y, note_width, NOTE_HEIGHT, m_detuningNoteColor);
} else {
p.fillRect(x, y, note_width, NOTE_HEIGHT, m_ghostNoteColor);
}
}
}
// and finally bars
for( tick = m_currentPosition - m_currentPosition % TimePos::ticksPerBar(),
x = xCoordOfTick( tick );
@@ -2117,8 +2208,18 @@ AutomationEditorWindow::AutomationEditorWindow() :
quantizationActionsToolBar->addWidget( quantize_lbl );
quantizationActionsToolBar->addWidget( m_quantizeComboBox );
m_resetGhostNotes = new QPushButton(m_toolBar);
m_resetGhostNotes->setIcon(embed::getIconPixmap("clear_ghost_note"));
m_resetGhostNotes->setToolTip(tr("Clear ghost notes"));
m_resetGhostNotes->setEnabled(true);
connect(m_resetGhostNotes, &QPushButton::pressed, m_editor, &AutomationEditor::resetGhostNotes);
quantizationActionsToolBar->addSeparator();
quantizationActionsToolBar->addWidget(m_resetGhostNotes);
// Setup our actual window
setFocusPolicy( Qt::StrongFocus );
setFocusPolicy(Qt::StrongFocus);
setFocus();
setWindowIcon( embed::getIconPixmap( "automation" ) );
setAcceptDrops( true );

View File

@@ -1635,6 +1635,7 @@ void PianoRoll::mousePressEvent(QMouseEvent * me )
}
detuningClip = n->detuning()->automationClip();
connect(detuningClip.data(), SIGNAL(dataChanged()), this, SLOT(update()));
getGUI()->automationEditor()->setGhostMidiClip(m_midiClip);
getGUI()->automationEditor()->open(detuningClip);
return;
}