From a4ce7f5f7d7ce7244761bf93f906c4890001315c Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:43:16 -0400 Subject: [PATCH 1/3] Initial Implementation --- include/PatternClipView.h | 2 + src/gui/clips/PatternClipView.cpp | 108 ++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/include/PatternClipView.h b/include/PatternClipView.h index ee313f32ab2..98c492d0fc6 100644 --- a/include/PatternClipView.h +++ b/include/PatternClipView.h @@ -61,6 +61,8 @@ protected slots: private: + void copySelectionToNewPatternTrack(); + PatternClip* m_patternClip; QPixmap m_paintPixmap; diff --git a/src/gui/clips/PatternClipView.cpp b/src/gui/clips/PatternClipView.cpp index bf12440c722..917a0701a6e 100644 --- a/src/gui/clips/PatternClipView.cpp +++ b/src/gui/clips/PatternClipView.cpp @@ -24,16 +24,24 @@ #include "PatternClipView.h" +#include + #include #include #include +#include "AutomationClip.h" #include "Engine.h" #include "GuiApplication.h" #include "MainWindow.h" +#include "MidiClip.h" #include "PatternClip.h" +#include "PatternTrack.h" +#include "PatternTrackView.h" #include "PatternStore.h" #include "RenameDialog.h" +#include "SampleClip.h" +#include "Song.h" namespace lmms::gui { @@ -63,10 +71,110 @@ void PatternClipView::constructContextMenu(QMenu* _cm) _cm->addAction( embed::getIconPixmap( "edit_rename" ), tr( "Change name" ), this, SLOT(changeName())); + _cm->addAction( + embed::getIconPixmap("pattern_track"), + tr("Copy Clip to new Pattern Track"), + this, + &PatternClipView::copySelectionToNewPatternTrack + ); } +void PatternClipView::copySelectionToNewPatternTrack() +{ + QVector clipvs = getClickedClips(); + // We check if the owner of the first Clip is a Pattern Track + bool isPatternTrack = dynamic_cast(clipvs.at(0)->getTrackView()); + if (!isPatternTrack) { return; } + // Then we create a set with all the Clips owners + std::set ownerTracks; + for (auto clipv: clipvs) { ownerTracks.insert(clipv->getTrackView()); } + + // Can merge if there's only one owner track + bool allSameTrack = ownerTracks.size() == 1; + if (!allSameTrack) { return; } + + // Find the first clip's start position in the selection + TimePos firstClipStartPos = m_patternClip->startPosition(); + for (auto clipv: clipvs) + { + firstClipStartPos = std::min(firstClipStartPos, clipv->getClip()->startPosition()); + } + + // Create new pattern track and clip + Track* new_track = Track::create(Track::Type::Pattern, Engine::getSong()); + PatternClip* new_clip = new PatternClip(new_track); + + new_clip->movePosition(firstClipStartPos); + const int oldPatternTrackIndex = static_cast(m_patternClip->getTrack())->patternIndex(); + const int newPatternTrackIndex = static_cast(new_track)->patternIndex(); + TimePos maxNotePos = 0; + + for (const auto& track : Engine::patternStore()->tracks()) + { + Clip* clip = track->getClip(oldPatternTrackIndex); + auto sClip = dynamic_cast(clip); + auto mClip = dynamic_cast(clip); + auto aClip = dynamic_cast(clip); + Clip* newClip = track->getClip(newPatternTrackIndex); + if (sClip) + { + // TODO + Clip::copyStateTo(clip, newClip); + } + else if (mClip) + { + MidiClip* newMidiClip = dynamic_cast(newClip); + + for (auto clipv: clipvs) + { + // Figure out how many times this clip repeats itself. At maximum it could touch (length roudned up + 1) bars + // when accounting for the fact that the start offset could make it play the end of a bar before starting the first full bar. + // Here we go the safe way and iterate through the maximum possible repetitions, and discard any notes outside of the range. + // First +1 for ceiling, second +1 for possible previous bar. + int maxPossibleRepetitions = clipv->getClip()->length() / mClip->length() + 1 + 1; + + TimePos clipRelativePos = clipv->getClip()->startPosition() - firstClipStartPos; + TimePos startTimeOffset = clipv->getClip()->startTimeOffset(); + + for (Note const* note : mClip->notes()) + { + // Start loop at i = -1 to get the bar touched by start offset + for (int i = -1; i < maxPossibleRepetitions - 1; i++) + { + auto newNote = Note{*note}; + + TimePos newNotePos = note->pos() + clipRelativePos + startTimeOffset + i * mClip->length().nextFullBar() * TimePos::ticksPerBar(); + TimePos newNotePosRelativeToClip = note->pos() + startTimeOffset + i * mClip->length().nextFullBar() * TimePos::ticksPerBar(); + + if (newNotePosRelativeToClip < 0 || newNotePosRelativeToClip >= clipv->getClip()->length()) { continue; } + + newNote.setPos(newNotePos); + newMidiClip->addNote(newNote, false); + maxNotePos = std::max(maxNotePos, newNotePos); + } + } + } + } + else if (aClip) + { + // TODO + Clip::copyStateTo(clip, newClip); + } + } + // Update the number of steps/bars for all tracks. For some reason addNote for midi clips does not update the length automatically. + for (const auto& track : Engine::patternStore()->tracks()) + { + for (int i = 0; i < maxNotePos.getBar(); i++) + { + static_cast(track->getClip(newPatternTrackIndex))->addSteps(); + } + } + + // Now that we know the maximum number of bars, set the length of the new pattern clip + new_clip->changeLength(maxNotePos.nextFullBar() * TimePos::ticksPerBar()); +} void PatternClipView::mouseDoubleClickEvent(QMouseEvent*) { From 0d64ef5bf3e79336ed741ec8842bdf7ef36d8da1 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 19 Oct 2024 22:51:11 -0400 Subject: [PATCH 2/3] Change action name --- src/gui/clips/PatternClipView.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/clips/PatternClipView.cpp b/src/gui/clips/PatternClipView.cpp index 917a0701a6e..f5401d7921c 100644 --- a/src/gui/clips/PatternClipView.cpp +++ b/src/gui/clips/PatternClipView.cpp @@ -73,7 +73,7 @@ void PatternClipView::constructContextMenu(QMenu* _cm) this, SLOT(changeName())); _cm->addAction( embed::getIconPixmap("pattern_track"), - tr("Copy Clip to new Pattern Track"), + tr("Copy to New Pattern Track"), this, &PatternClipView::copySelectionToNewPatternTrack ); From 8b624b41518951698e811cfc5fa874bda5d51373 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:39:26 -0400 Subject: [PATCH 3/3] Add support for merging clips from multiple tracks --- include/PatternClipView.h | 1 + src/gui/clips/PatternClipView.cpp | 125 +++++++++++++++++------------- 2 files changed, 70 insertions(+), 56 deletions(-) diff --git a/include/PatternClipView.h b/include/PatternClipView.h index 98c492d0fc6..f0147479f23 100644 --- a/include/PatternClipView.h +++ b/include/PatternClipView.h @@ -61,6 +61,7 @@ protected slots: private: + bool canCopySelectionToNewTrack(); void copySelectionToNewPatternTrack(); PatternClip* m_patternClip; diff --git a/src/gui/clips/PatternClipView.cpp b/src/gui/clips/PatternClipView.cpp index f5401d7921c..516bf9cff7e 100644 --- a/src/gui/clips/PatternClipView.cpp +++ b/src/gui/clips/PatternClipView.cpp @@ -71,33 +71,42 @@ void PatternClipView::constructContextMenu(QMenu* _cm) _cm->addAction( embed::getIconPixmap( "edit_rename" ), tr( "Change name" ), this, SLOT(changeName())); - _cm->addAction( - embed::getIconPixmap("pattern_track"), - tr("Copy to New Pattern Track"), - this, - &PatternClipView::copySelectionToNewPatternTrack - ); + if (canCopySelectionToNewTrack()) + { + _cm->addAction( + embed::getIconPixmap("pattern_track"), + tr("Copy to New Pattern Track"), + this, + &PatternClipView::copySelectionToNewPatternTrack + ); + } } -void PatternClipView::copySelectionToNewPatternTrack() +bool PatternClipView::canCopySelectionToNewTrack() { QVector clipvs = getClickedClips(); // We check if the owner of the first Clip is a Pattern Track bool isPatternTrack = dynamic_cast(clipvs.at(0)->getTrackView()); - if (!isPatternTrack) { return; } - // Then we create a set with all the Clips owners - std::set ownerTracks; - for (auto clipv: clipvs) { ownerTracks.insert(clipv->getTrackView()); } + if (!isPatternTrack) { return false; } + // Make sure every track is a pattern track + for (auto clipv: clipvs) { + if (!dynamic_cast(clipv->getTrackView())) { return false; } + } + return true; +} - // Can merge if there's only one owner track - bool allSameTrack = ownerTracks.size() == 1; - if (!allSameTrack) { return; } +void PatternClipView::copySelectionToNewPatternTrack() +{ + QVector clipvs = getClickedClips(); + std::set ownerTracks; // Find the first clip's start position in the selection TimePos firstClipStartPos = m_patternClip->startPosition(); for (auto clipv: clipvs) { firstClipStartPos = std::min(firstClipStartPos, clipv->getClip()->startPosition()); + // And build up a set of the affected pattern tracks + ownerTracks.insert(clipv->getTrackView()); } // Create new pattern track and clip @@ -106,61 +115,65 @@ void PatternClipView::copySelectionToNewPatternTrack() new_clip->movePosition(firstClipStartPos); - const int oldPatternTrackIndex = static_cast(m_patternClip->getTrack())->patternIndex(); + //const int oldPatternTrackIndex = static_cast(m_patternClip->getTrack())->patternIndex(); const int newPatternTrackIndex = static_cast(new_track)->patternIndex(); TimePos maxNotePos = 0; - for (const auto& track : Engine::patternStore()->tracks()) + for (const auto& patternTrack : ownerTracks) { - Clip* clip = track->getClip(oldPatternTrackIndex); - auto sClip = dynamic_cast(clip); - auto mClip = dynamic_cast(clip); - auto aClip = dynamic_cast(clip); - Clip* newClip = track->getClip(newPatternTrackIndex); - if (sClip) - { - // TODO - Clip::copyStateTo(clip, newClip); - } - else if (mClip) + for (const auto& track : Engine::patternStore()->tracks()) { - MidiClip* newMidiClip = dynamic_cast(newClip); - - for (auto clipv: clipvs) + int ownerPatternTrackIndex = static_cast(patternTrack->getTrack())->patternIndex(); + Clip* clip = track->getClip(ownerPatternTrackIndex); + auto sClip = dynamic_cast(clip); + auto mClip = dynamic_cast(clip); + auto aClip = dynamic_cast(clip); + Clip* newClip = track->getClip(newPatternTrackIndex); + if (sClip) { - // Figure out how many times this clip repeats itself. At maximum it could touch (length roudned up + 1) bars - // when accounting for the fact that the start offset could make it play the end of a bar before starting the first full bar. - // Here we go the safe way and iterate through the maximum possible repetitions, and discard any notes outside of the range. - // First +1 for ceiling, second +1 for possible previous bar. - int maxPossibleRepetitions = clipv->getClip()->length() / mClip->length() + 1 + 1; - - TimePos clipRelativePos = clipv->getClip()->startPosition() - firstClipStartPos; - TimePos startTimeOffset = clipv->getClip()->startTimeOffset(); - - for (Note const* note : mClip->notes()) + // TODO + Clip::copyStateTo(clip, newClip); + } + else if (mClip) + { + MidiClip* newMidiClip = dynamic_cast(newClip); + for (auto clipv: clipvs) { - // Start loop at i = -1 to get the bar touched by start offset - for (int i = -1; i < maxPossibleRepetitions - 1; i++) - { - auto newNote = Note{*note}; + if (clipv->getTrackView() != patternTrack) { continue; } + // Figure out how many times this clip repeats itself. At maximum it could touch (length roudned up + 1) bars + // when accounting for the fact that the start offset could make it play the end of a bar before starting the first full bar. + // Here we go the safe way and iterate through the maximum possible repetitions, and discard any notes outside of the range. + // First +1 for ceiling, second +1 for possible previous bar. + int maxPossibleRepetitions = clipv->getClip()->length() / mClip->length() + 1 + 1; - TimePos newNotePos = note->pos() + clipRelativePos + startTimeOffset + i * mClip->length().nextFullBar() * TimePos::ticksPerBar(); - TimePos newNotePosRelativeToClip = note->pos() + startTimeOffset + i * mClip->length().nextFullBar() * TimePos::ticksPerBar(); - - if (newNotePosRelativeToClip < 0 || newNotePosRelativeToClip >= clipv->getClip()->length()) { continue; } + TimePos clipRelativePos = clipv->getClip()->startPosition() - firstClipStartPos; + TimePos startTimeOffset = clipv->getClip()->startTimeOffset(); - newNote.setPos(newNotePos); - newMidiClip->addNote(newNote, false); - maxNotePos = std::max(maxNotePos, newNotePos); + for (Note const* note : mClip->notes()) + { + // Start loop at i = -1 to get the bar touched by start offset + for (int i = -1; i < maxPossibleRepetitions - 1; i++) + { + auto newNote = Note{*note}; + + TimePos newNotePos = note->pos() + clipRelativePos + startTimeOffset + i * mClip->length().nextFullBar() * TimePos::ticksPerBar(); + TimePos newNotePosRelativeToClip = note->pos() + startTimeOffset + i * mClip->length().nextFullBar() * TimePos::ticksPerBar(); + + if (newNotePosRelativeToClip < 0 || newNotePosRelativeToClip >= clipv->getClip()->length()) { continue; } + + newNote.setPos(newNotePos); + newMidiClip->addNote(newNote, false); + maxNotePos = std::max(maxNotePos, newNotePos); + } } } } - } - else if (aClip) - { - // TODO - Clip::copyStateTo(clip, newClip); + else if (aClip) + { + // TODO + Clip::copyStateTo(clip, newClip); + } } } // Update the number of steps/bars for all tracks. For some reason addNote for midi clips does not update the length automatically.