From 9705c31773fb81d423b4b0bf8a7cf27e723911a9 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 4 Jul 2022 03:16:22 +0200 Subject: [PATCH] Transpose midi clips in song editor (#6409) * Transpose midi clips in song editor * Fix undo stupidity * Check boundries when transposing clips and move transpose function to NoteVector * Avoid update if nothing has changed * Make getNoteBounds a separate function * Rename getNoteBounds to boundsForNotes * bool operator instead of optional + qobject_cast * Revert "bool operator instead of optional + qobject_cast" This reverts commit 98c56a96cf1538dcdce481c0a1c045b2660a59ee. * qobject_cast and nullopt --- include/MidiClipView.h | 1 + include/Note.h | 13 +++++++ src/core/Note.cpp | 29 +++++++++++++++ src/gui/clips/MidiClipView.cpp | 68 +++++++++++++++++++++++++++++++++- 4 files changed, 109 insertions(+), 2 deletions(-) diff --git a/include/MidiClipView.h b/include/MidiClipView.h index 278e49443..1a9809733 100644 --- a/include/MidiClipView.h +++ b/include/MidiClipView.h @@ -73,6 +73,7 @@ protected slots: void resetName(); void changeName(); + void transposeSelection(); protected: diff --git a/include/Note.h b/include/Note.h index f1c10c0ac..01338be49 100644 --- a/include/Note.h +++ b/include/Note.h @@ -26,6 +26,7 @@ #ifndef NOTE_H #define NOTE_H +#include #include #include "volume.h" @@ -252,6 +253,18 @@ private: typedef QVector NoteVector; +struct NoteBounds +{ + TimePos start; + TimePos end; + int lowest; + int highest; +}; + + +std::optional boundsForNotes(const NoteVector& notes); + + } // namespace lmms #endif diff --git a/src/core/Note.cpp b/src/core/Note.cpp index b730ec1cc..e4e5f0e5d 100644 --- a/src/core/Note.cpp +++ b/src/core/Note.cpp @@ -237,4 +237,33 @@ bool Note::withinRange(int tickStart, int tickEnd) const } + + +/*! \brief Get the start/end/bottom/top positions of notes in a vector + * + * Returns no value if there are no notes + */ +std::optional boundsForNotes(const NoteVector& notes) +{ + if (notes.empty()) { return std::nullopt; } + + TimePos start = notes.front()->pos(); + TimePos end = start; + int lower = notes.front()->key(); + int upper = lower; + + for (const Note* note: notes) + { + // TODO should we assume that NoteVector is always sorted correctly, + // so first() always has the lowest time position? + start = std::min(start, note->pos()); + end = std::max(end, note->endPos()); + lower = std::min(lower, note->key()); + upper = std::max(upper, note->key()); + } + + return NoteBounds{start, end, lower, upper}; +} + + } // namespace lmms diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index 675b350db..720999574 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include @@ -36,6 +37,7 @@ #include "MidiClip.h" #include "PianoRoll.h" #include "RenameDialog.h" +#include "TrackView.h" namespace lmms::gui { @@ -144,8 +146,66 @@ void MidiClipView::changeName() +void MidiClipView::transposeSelection() +{ + const auto selection = getClickedClips(); + + // Calculate the key boundries for all clips + int highest = 0; + int lowest = NumKeys - 1; + for (ClipView* clipview: selection) + { + if (auto mcv = qobject_cast(clipview)) + { + if (auto bounds = boundsForNotes(mcv->getMidiClip()->notes())) + { + lowest = std::min(bounds->lowest, lowest); + highest = std::max(bounds->highest, highest); + } + } + } + + int semitones = QInputDialog::getInt(this, tr("Transpose"), tr("Semitones to transpose by:"), + /*start*/ 0, /*min*/ -lowest, /*max*/ (NumKeys - 1 - highest)); + + if (semitones == 0) { return; } + + // TODO make this not crash + // Engine::getSong()->addJournalCheckPoint(); + + QSet m_changedTracks; + for (ClipView* clipview: selection) + { + auto mcv = qobject_cast(clipview); + if (!mcv) { continue; } + + auto clip = mcv->getMidiClip(); + if (clip->notes().empty()) { continue; } + + auto track = clipview->getTrackView()->getTrack(); + if (!m_changedTracks.contains(track)) + { + track->addJournalCheckPoint(); + m_changedTracks.insert(track); + } + + for (Note* note: clip->notes()) + { + note->setKey(note->key() + semitones); + } + emit clip->dataChanged(); + } + // At least one clip must have notes to show the transpose dialog, so something *has* changed + Engine::getSong()->setModified(); +} + + + + void MidiClipView::constructContextMenu( QMenu * _cm ) { + bool isBeat = m_clip->type() == MidiClip::BeatClip; + QAction * a = new QAction( embed::getIconPixmap( "piano" ), tr( "Open in piano-roll" ), _cm ); _cm->insertAction( _cm->actions()[0], a ); @@ -163,6 +223,10 @@ void MidiClipView::constructContextMenu( QMenu * _cm ) _cm->addAction( embed::getIconPixmap( "edit_erase" ), tr( "Clear all notes" ), m_clip, SLOT(clear())); + if (!isBeat) + { + _cm->addAction(embed::getIconPixmap("scale"), tr("Transpose"), this, &MidiClipView::transposeSelection); + } _cm->addSeparator(); _cm->addAction( embed::getIconPixmap( "reload" ), tr( "Reset name" ), @@ -171,7 +235,7 @@ void MidiClipView::constructContextMenu( QMenu * _cm ) tr( "Change name" ), this, SLOT(changeName())); - if ( m_clip->type() == MidiClip::BeatClip ) + if (isBeat) { _cm->addSeparator(); @@ -622,4 +686,4 @@ void MidiClipView::paintEvent( QPaintEvent * ) } -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui