From c2811aebefbbaa33f70924c59cbd5d5e3b98db18 Mon Sep 17 00:00:00 2001 From: DanielKauss Date: Sat, 25 Nov 2023 12:16:56 +0100 Subject: [PATCH] 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 --- data/themes/classic/automation_ghost_note.png | Bin 0 -> 4467 bytes data/themes/classic/style.css | 4 + data/themes/default/automation_ghost_note.png | Bin 0 -> 4467 bytes data/themes/default/style.css | 3 + include/AutomationEditor.h | 40 +++++- include/MidiClipView.h | 3 +- include/SampleClipView.h | 1 + src/gui/clips/MidiClipView.cpp | 39 ++++-- src/gui/clips/SampleClipView.cpp | 16 +++ src/gui/editors/AutomationEditor.cpp | 117 ++++++++++++++++-- src/gui/editors/PianoRoll.cpp | 1 + 11 files changed, 200 insertions(+), 24 deletions(-) create mode 100644 data/themes/classic/automation_ghost_note.png create mode 100644 data/themes/default/automation_ghost_note.png diff --git a/data/themes/classic/automation_ghost_note.png b/data/themes/classic/automation_ghost_note.png new file mode 100644 index 0000000000000000000000000000000000000000..d14c047d7aed311ea74bb7e725bc2b1681556b4e GIT binary patch literal 4467 zcmeHKdsGzH8J}Gca2qJ(sR2(n6K#YzJCA*jECks_R$QdA5~4OR%-mT9cXyWE0Ty#G zMn#dfrcE)8N=;5o4T(fat26~q#cGb2m}uK$Vnq$bM6D$$Hi?>8rFUkRSDSNsIH&oK z*>m^4e)o63?{~lZ?at<+!i9;N2Q>(SBs%l$OJRM#ToU5o_tE{{`LH?#N}asZnSmt0 z4n(SA5tWRAMK!LiR>dGOaBL$icf+=fk;{XsX-fTL*iMbMTVQ)`6eF)8Rca&=)~&Em zuzfGAUx!856xr+duq{6c%IAGGg4_dpBlSY3t4K@fNJ6JK!1V;Br%8e)jat$~>&-M_ zfcvXn8nZ}dT36Ct1?}4)+!kV>jby?E8@;46` zXt0V*%V)>BpL({tZtbZy;`Cs%>UDnR-|Q<-Tq=2c=i{--3$iy78D#N`1;2W3D6Q&joIZLjIOlhyg@rRn`o$L-zUdFU*{XFK=(_CRk+^4U1u-oEVz<5zx$ ztY6Z_ zc_XH2M{wV}N$$fi2E&58R4R2XrdiRaV>qz_=)%5$d|*gsb~wPWtAM1f03N|_L;ulz z2-OOl4Sn3`B3yx7P$}fE4uayH8BD%Q%O*;$&*FbxTOKw`9EpVuFv!!}gmrD0nh z#!;;TkyhEzQW!_MVi0HzI)jeD9ATlBLbEj5%pk|pOYM143fQxum68;oalEdsPFJVb zi9rueTCG-`pm2)9AOZ{3`z0of`9lxO6cG+P2(dvSAPJ&hD|0dxVvS@&Q8=$1vCkK9 zxkl;zp{NQ_4?N5Sa8gI$J|8}QMM!egLXxOK-?$>=t`7iwDF}%*K^8b_fnR!fJcWFN zMj2$6Jc2KvI0eO%=h(cGLbzb@=_fW1UVk4Bh(6pr}7J>ovWN+`b2bHC6JE?3WkvcMjHQ9Fg)ro zF6WF9k2CTA(j-#>j7u_bU1SU|YimquEr0d-^P6Tp6HE45G7IeAJB}vq>3iTD{=>lVi8b@* z-%7rga0Su#-*-lJ;CAN+JJhp|rx5BjO@B`f7d95Iefm$!VlK|uufEg!VagTZ`MzC^ zP15=4KOuL&mi$TfnKv&qn_ZurDLqlPsppZcH&tIe^GJOE=Sv>y_@L*W&%fHRdT>YJ zg^R~B?!?CRB;Q;#v@gS?EA_WL6>RQ&Y;Uuv{1;oVhO6;;zZ*Dpcj$W0E3GXnmM=T^ zoq{SyYES!)5AVPE-u0^82X{5O(WTA3IbY$X8@A3L9QyijT*>v$_qUo4Tv$~9Y)@9~ k&iTW0I=sCr)Sz;3CfYZ2w*Tk9mQ_WZjzat11?B7h4YN`c=>Px# literal 0 HcmV?d00001 diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 9b50851a369..505ab348340 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -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 */ diff --git a/data/themes/default/automation_ghost_note.png b/data/themes/default/automation_ghost_note.png new file mode 100644 index 0000000000000000000000000000000000000000..d14c047d7aed311ea74bb7e725bc2b1681556b4e GIT binary patch literal 4467 zcmeHKdsGzH8J}Gca2qJ(sR2(n6K#YzJCA*jECks_R$QdA5~4OR%-mT9cXyWE0Ty#G zMn#dfrcE)8N=;5o4T(fat26~q#cGb2m}uK$Vnq$bM6D$$Hi?>8rFUkRSDSNsIH&oK z*>m^4e)o63?{~lZ?at<+!i9;N2Q>(SBs%l$OJRM#ToU5o_tE{{`LH?#N}asZnSmt0 z4n(SA5tWRAMK!LiR>dGOaBL$icf+=fk;{XsX-fTL*iMbMTVQ)`6eF)8Rca&=)~&Em zuzfGAUx!856xr+duq{6c%IAGGg4_dpBlSY3t4K@fNJ6JK!1V;Br%8e)jat$~>&-M_ zfcvXn8nZ}dT36Ct1?}4)+!kV>jby?E8@;46` zXt0V*%V)>BpL({tZtbZy;`Cs%>UDnR-|Q<-Tq=2c=i{--3$iy78D#N`1;2W3D6Q&joIZLjIOlhyg@rRn`o$L-zUdFU*{XFK=(_CRk+^4U1u-oEVz<5zx$ ztY6Z_ zc_XH2M{wV}N$$fi2E&58R4R2XrdiRaV>qz_=)%5$d|*gsb~wPWtAM1f03N|_L;ulz z2-OOl4Sn3`B3yx7P$}fE4uayH8BD%Q%O*;$&*FbxTOKw`9EpVuFv!!}gmrD0nh z#!;;TkyhEzQW!_MVi0HzI)jeD9ATlBLbEj5%pk|pOYM143fQxum68;oalEdsPFJVb zi9rueTCG-`pm2)9AOZ{3`z0of`9lxO6cG+P2(dvSAPJ&hD|0dxVvS@&Q8=$1vCkK9 zxkl;zp{NQ_4?N5Sa8gI$J|8}QMM!egLXxOK-?$>=t`7iwDF}%*K^8b_fnR!fJcWFN zMj2$6Jc2KvI0eO%=h(cGLbzb@=_fW1UVk4Bh(6pr}7J>ovWN+`b2bHC6JE?3WkvcMjHQ9Fg)ro zF6WF9k2CTA(j-#>j7u_bU1SU|YimquEr0d-^P6Tp6HE45G7IeAJB}vq>3iTD{=>lVi8b@* z-%7rga0Su#-*-lJ;CAN+JJhp|rx5BjO@B`f7d95Iefm$!VlK|uufEg!VagTZ`MzC^ zP15=4KOuL&mi$TfnKv&qn_ZurDLqlPsppZcH&tIe^GJOE=Sv>y_@L*W&%fHRdT>YJ zg^R~B?!?CRB;Q;#v@gS?EA_WL6>RQ&Y;Uuv{1;oVhO6;;zZ*Dpcj$W0E3GXnmM=T^ zoq{SyYES!)5AVPE-u0^82X{5O(WTA3IbY$X8@A3L9QyijT*>v$_qUo4Tv$~9Y)@9~ k&iTW0I=sCr)Sz;3CfYZ2w*Tk9mQ_WZjzat11?B7h4YN`c=>Px# literal 0 HcmV?d00001 diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 172a67d8e6d..e05d526533b 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -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 */ diff --git a/include/AutomationEditor.h b/include/AutomationEditor.h index da6bdcaaf32..1110e8e4c9e 100644 --- a/include/AutomationEditor.h +++ b/include/AutomationEditor.h @@ -26,16 +26,18 @@ #ifndef LMMS_GUI_AUTOMATION_EDITOR_H #define LMMS_GUI_AUTOMATION_EDITOR_H +#include #include #include +#include "AutomationClip.h" +#include "ComboBoxModel.h" #include "Editor.h" - -#include "lmms_basics.h" #include "JournallingObject.h" +#include "MidiClip.h" +#include "SampleClip.h" #include "TimePos.h" -#include "AutomationClip.h" -#include "ComboBoxModel.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 @@ protected slots: 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 @@ protected slots: float m_bottomLevel; float m_topLevel; + MidiClip* m_ghostNotes = nullptr; + QPointer m_ghostSample = nullptr; // QPointer to set to nullptr on deletion + bool m_renderSample = false; + void centerTopBottomScroll(); void updateTopBottomLevels(); @@ -261,6 +285,9 @@ protected slots: 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 @@ class AutomationEditorWindow : public Editor ~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 slots: ComboBox * m_zoomingXComboBox; ComboBox * m_zoomingYComboBox; ComboBox * m_quantizeComboBox; + + QPushButton* m_resetGhostNotes; }; } // namespace gui diff --git a/include/MidiClipView.h b/include/MidiClipView.h index a32be956a2c..4285bf9da0f 100644 --- a/include/MidiClipView.h +++ b/include/MidiClipView.h @@ -71,6 +71,7 @@ public slots: protected slots: void openInPianoRoll(); void setGhostInPianoRoll(); + void setGhostInAutomationEditor(); void resetName(); void changeName(); @@ -100,7 +101,7 @@ protected slots: QColor m_mutedNoteBorderColor; QStaticText m_staticTextName; - + bool m_legacySEPattern; } ; diff --git a/include/SampleClipView.h b/include/SampleClipView.h index b3f53d79092..4ff218fb0ef 100644 --- a/include/SampleClipView.h +++ b/include/SampleClipView.h @@ -47,6 +47,7 @@ class SampleClipView : public ClipView public slots: void updateSample(); void reverseSample(); + void setAutomationGhost(); diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index 669f0ae60e5..a9ee1cb4008 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -25,13 +25,16 @@ #include "MidiClipView.h" + #include #include #include #include #include #include +#include +#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" ), diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index d0c08987980..81bbd271d54 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -28,6 +28,8 @@ #include #include +#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 */ diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index bd9566a057d..c8ef19b79bd 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -27,8 +27,6 @@ #include "AutomationEditor.h" -#include - #include #include #include @@ -38,6 +36,9 @@ #include #include #include +#include + +#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(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(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 ); diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 5ec8c323cfe..67fb940aa52 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -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; }