From 75d46a244bc7792c2573f47a6673762a0b4870b0 Mon Sep 17 00:00:00 2001 From: ronso0 Date: Fri, 31 Jan 2025 01:54:32 +0100 Subject: [PATCH] add controls to sort hotcues by position, optionally remove empty slots/offsets `sort_hotcues`: by pos.: 3, 7, 2 -> 2, 3, 7 `sort_hotcues_compress`: 3, 7, 2 -> 1, 2, 3 add actions to track menu (top) --- CMakeLists.txt | 1 + src/audio/frame.h | 7 +++ src/controllers/controlpickermenu.cpp | 37 +++++++----- src/engine/controls/cuecontrol.cpp | 26 ++++++++ src/engine/controls/cuecontrol.h | 6 ++ src/test/hotcueorderbyposition_test.cpp | 80 +++++++++++++++++++++++++ src/track/track.cpp | 57 ++++++++++++++++++ src/track/track.h | 5 ++ src/track/track_decl.h | 5 ++ src/widget/wtrackmenu.cpp | 57 ++++++++++++++++++ src/widget/wtrackmenu.h | 6 ++ 11 files changed, 273 insertions(+), 14 deletions(-) create mode 100644 src/test/hotcueorderbyposition_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 06b24d877c2..979f3d38008 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2460,6 +2460,7 @@ add_executable( src/test/frametest.cpp src/test/globaltrackcache_test.cpp src/test/hotcuecontrol_test.cpp + src/test/hotcueorderbyposition_test.cpp src/test/imageutils_test.cpp src/test/indexrange_test.cpp src/test/itunesxmlimportertest.cpp diff --git a/src/audio/frame.h b/src/audio/frame.h index 6e921c4dea8..61eab19ac47 100644 --- a/src/audio/frame.h +++ b/src/audio/frame.h @@ -5,6 +5,7 @@ #include #include "engine/engine.h" +#include "util/compatibility/qhash.h" #include "util/fpclassify.h" namespace mixxx { @@ -247,6 +248,12 @@ inline bool operator!=(FramePos frame1, FramePos frame2) { QDebug operator<<(QDebug dbg, FramePos arg); +inline qhash_seed_t qHash( + FramePos pos, + qhash_seed_t seed = 0) { + return static_cast(pos.value(), seed); +} + constexpr FramePos kInvalidFramePos = FramePos(FramePos::kInvalidValue); constexpr FramePos kStartFramePos = FramePos(FramePos::kStartValue); } // namespace audio diff --git a/src/controllers/controlpickermenu.cpp b/src/controllers/controlpickermenu.cpp index dd636fbbc69..b4bd5e34664 100644 --- a/src/controllers/controlpickermenu.cpp +++ b/src/controllers/controlpickermenu.cpp @@ -515,20 +515,6 @@ ControlPickerMenu::ControlPickerMenu(QWidget* pParent) // Hotcues QMenu* pHotcueMainMenu = addSubmenu(tr("Hotcues")); - QString hotcueActivateTitle = tr("Hotcue %1"); - QString hotcueClearTitle = tr("Clear Hotcue %1"); - QString hotcueSetTitle = tr("Set Hotcue %1"); - QString hotcueGotoTitle = tr("Jump To Hotcue %1"); - QString hotcueGotoAndStopTitle = tr("Jump To Hotcue %1 And Stop"); - QString hotcueGotoAndPlayTitle = tr("Jump To Hotcue %1 And Play"); - QString hotcuePreviewTitle = tr("Preview Hotcue %1"); - QString hotcueActivateDescription = tr("Set, preview from or jump to hotcue %1"); - QString hotcueClearDescription = tr("Clear hotcue %1"); - QString hotcueSetDescription = tr("Set hotcue %1"); - QString hotcueGotoDescription = tr("Jump to hotcue %1"); - QString hotcueGotoAndStopDescription = tr("Jump to hotcue %1 and stop"); - QString hotcueGotoAndPlayDescription = tr("Jump to hotcue %1 and play"); - QString hotcuePreviewDescription = tr("Preview from hotcue %1"); addDeckControl("shift_cues_earlier", tr("Shift cue points earlier"), tr("Shift cue points 10 milliseconds earlier"), @@ -545,6 +531,29 @@ ControlPickerMenu::ControlPickerMenu(QWidget* pParent) tr("Shift cue points later (fine)"), tr("Shift cue points 1 millisecond later"), pHotcueMainMenu); + addDeckControl("sort_hotcues", + tr("Sort hotcues by position"), + tr("Sort hotcues by position"), + pHotcueMainMenu); + addDeckControl("sort_hotcues_remove_offsets", + tr("Sort hotcues by position (remove offsets)"), + tr("Sort hotcues by position (remove offsets)"), + pHotcueMainMenu); + + const QString hotcueActivateTitle = tr("Hotcue %1"); + const QString hotcueClearTitle = tr("Clear Hotcue %1"); + const QString hotcueSetTitle = tr("Set Hotcue %1"); + const QString hotcueGotoTitle = tr("Jump To Hotcue %1"); + const QString hotcueGotoAndStopTitle = tr("Jump To Hotcue %1 And Stop"); + const QString hotcueGotoAndPlayTitle = tr("Jump To Hotcue %1 And Play"); + const QString hotcuePreviewTitle = tr("Preview Hotcue %1"); + const QString hotcueActivateDescription = tr("Set, preview from or jump to hotcue %1"); + const QString hotcueClearDescription = tr("Clear hotcue %1"); + const QString hotcueSetDescription = tr("Set hotcue %1"); + const QString hotcueGotoDescription = tr("Jump to hotcue %1"); + const QString hotcueGotoAndStopDescription = tr("Jump to hotcue %1 and stop"); + const QString hotcueGotoAndPlayDescription = tr("Jump to hotcue %1 and play"); + const QString hotcuePreviewDescription = tr("Preview from hotcue %1"); // add menus for hotcues 1-16. // though, keep the menu small put additional hotcues in a separate menu, // but don't create that submenu for less than 4 additional hotcues. diff --git a/src/engine/controls/cuecontrol.cpp b/src/engine/controls/cuecontrol.cpp index 80c2c08121f..0ec33756d47 100644 --- a/src/engine/controls/cuecontrol.cpp +++ b/src/engine/controls/cuecontrol.cpp @@ -128,6 +128,20 @@ CueControl::CueControl(const QString& group, m_pPassthrough->connectValueChanged(this, &CueControl::passthroughChanged, Qt::DirectConnection); + + m_pSortHotcuesByPos = std::make_unique(ConfigKey(group, "sort_hotcues")); + connect(m_pSortHotcuesByPos.get(), + &ControlObject::valueChanged, + this, + &CueControl::setHotcueIndicesSortedByPosition, + Qt::DirectConnection); + m_pSortHotcuesByPosCompress = std::make_unique( + ConfigKey(group, "sort_hotcues_remove_offsets")); + connect(m_pSortHotcuesByPosCompress.get(), + &ControlObject::valueChanged, + this, + &CueControl::setHotcueIndicesSortedByPositionCompress, + Qt::DirectConnection); } CueControl::~CueControl() { @@ -448,6 +462,18 @@ void CueControl::passthroughChanged(double enabled) { } } +void CueControl::setHotcueIndicesSortedByPosition(double v) { + if (v > 0 && m_pLoadedTrack) { + m_pLoadedTrack->setHotcueIndicesSortedByPosition(HotcueSortMode::KeepOffsets); + } +} + +void CueControl::setHotcueIndicesSortedByPositionCompress(double v) { + if (v > 0 && m_pLoadedTrack) { + m_pLoadedTrack->setHotcueIndicesSortedByPosition(HotcueSortMode::RemoveOffsets); + } +} + void CueControl::attachCue(const CuePointer& pCue, HotcueControl* pControl) { VERIFY_OR_DEBUG_ASSERT(pControl) { return; diff --git a/src/engine/controls/cuecontrol.h b/src/engine/controls/cuecontrol.h index 64bdbb51ef5..d9864cf1d85 100644 --- a/src/engine/controls/cuecontrol.h +++ b/src/engine/controls/cuecontrol.h @@ -240,6 +240,9 @@ class CueControl : public EngineControl { void passthroughChanged(double v); + void setHotcueIndicesSortedByPosition(double v); + void setHotcueIndicesSortedByPositionCompress(double v); + void cueSet(double v); void cueClear(double v); void cueGoto(double v); @@ -362,6 +365,9 @@ class CueControl : public EngineControl { parented_ptr m_pPassthrough; + std::unique_ptr m_pSortHotcuesByPos; + std::unique_ptr m_pSortHotcuesByPosCompress; + QAtomicPointer m_pCurrentSavedLoopControl; // Tells us which controls map to which hotcue diff --git a/src/test/hotcueorderbyposition_test.cpp b/src/test/hotcueorderbyposition_test.cpp new file mode 100644 index 00000000000..5648758c2d4 --- /dev/null +++ b/src/test/hotcueorderbyposition_test.cpp @@ -0,0 +1,80 @@ +// #include "library/coverart.h" +// #include "sources/soundsourceproxy.h" +// #include "test/soundsourceproviderregistration.h" +#include "test/mixxxtest.h" +#include "track/cue.h" +#include "track/track.h" + +// Test for updating track metadata and cover art from files. +class TrackHotcueOrderByPosTest : public MixxxTest { + protected: + static TrackPointer newTestTrack() { + return Track::newTemporary( + QDir(MixxxTest::getOrInitTestDir().filePath(QStringLiteral("track-test-data"))), + "THOBP.mp3"); + } +}; + +TEST_F(TrackHotcueOrderByPosTest, orderHotcuesKeepOffsets) { + auto pTrack = newTestTrack(); + pTrack->markClean(); + + // create hotcues with ascending position but unordered indices + CuePointer pHotcue1 = pTrack->createAndAddCue(mixxx::CueType::HotCue, + 2, + mixxx::audio::FramePos(100), + mixxx::audio::kInvalidFramePos); + CuePointer pHotcue2 = pTrack->createAndAddCue(mixxx::CueType::HotCue, + 1, + mixxx::audio::FramePos(200), + mixxx::audio::kInvalidFramePos); + CuePointer pHotcue3 = pTrack->createAndAddCue(mixxx::CueType::HotCue, + 7, + mixxx::audio::FramePos(300), + mixxx::audio::kInvalidFramePos, + mixxx::PredefinedColorPalettes::kDefaultCueColor); + CuePointer pHotcue4 = pTrack->createAndAddCue(mixxx::CueType::HotCue, + 5, + mixxx::audio::FramePos(400), + mixxx::audio::kInvalidFramePos); + + pTrack->setHotcueIndicesSortedByPosition(HotcueSortMode::KeepOffsets); + + // Hotcues indices by position should now be 1 2 5 7 + EXPECT_EQ(pHotcue1->getHotCue(), 1); + EXPECT_EQ(pHotcue2->getHotCue(), 2); + EXPECT_EQ(pHotcue3->getHotCue(), 5); + EXPECT_EQ(pHotcue4->getHotCue(), 7); +} + +TEST_F(TrackHotcueOrderByPosTest, orderHotcuesRemoveOffsets) { + auto pTrack = newTestTrack(); + pTrack->markClean(); + + // create hotcues with ascending position but unordered indices + CuePointer pHotcue1 = pTrack->createAndAddCue(mixxx::CueType::HotCue, + 2, + mixxx::audio::FramePos(100), + mixxx::audio::kInvalidFramePos); + CuePointer pHotcue2 = pTrack->createAndAddCue(mixxx::CueType::HotCue, + 1, + mixxx::audio::FramePos(200), + mixxx::audio::kInvalidFramePos); + CuePointer pHotcue3 = pTrack->createAndAddCue(mixxx::CueType::HotCue, + 7, + mixxx::audio::FramePos(300), + mixxx::audio::kInvalidFramePos, + mixxx::PredefinedColorPalettes::kDefaultCueColor); + CuePointer pHotcue4 = pTrack->createAndAddCue(mixxx::CueType::HotCue, + 5, + mixxx::audio::FramePos(400), + mixxx::audio::kInvalidFramePos); + + pTrack->setHotcueIndicesSortedByPosition(HotcueSortMode::RemoveOffsets); + + // Hotcues indices by position should now be 0 1 2 3 + EXPECT_EQ(pHotcue1->getHotCue(), 0); + EXPECT_EQ(pHotcue2->getHotCue(), 1); + EXPECT_EQ(pHotcue3->getHotCue(), 2); + EXPECT_EQ(pHotcue4->getHotCue(), 3); +} diff --git a/src/track/track.cpp b/src/track/track.cpp index ad53892533f..e01a37996aa 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -1004,6 +1004,63 @@ void Track::shiftBeatsMillis(double milliseconds) { } } +void Track::setHotcueIndicesSortedByPosition(HotcueSortMode sortMode) { + auto locked = lockMutex(&m_qMutex); + + // Populate lists of positions and indices + QList indices; + QList positions; + indices.reserve(m_cuePoints.size()); + positions.reserve(m_cuePoints.size()); + for (const CuePointer& pCue : std::as_const(m_cuePoints)) { + if (pCue->getType() != mixxx::CueType::HotCue) { + continue; + } + const auto pos = pCue->getPosition(); + positions.append(pos); + if (sortMode == HotcueSortMode::KeepOffsets) { + // We shall keep empty hotcues (start offset, gaps), so we need + // to store the indices + indices.append(pCue->getHotCue()); + } + } + + std::sort(positions.begin(), positions.end()); + if (sortMode == HotcueSortMode::KeepOffsets) { + DEBUG_ASSERT(positions.size() == indices.size()); + std::sort(indices.begin(), indices.end()); + } + + // The actual sorting: + // re-map hotcue positions to indices in ascending order + QHash posIndexHash; + if (sortMode == HotcueSortMode::RemoveOffsets) { + // Assign new indices, start with 0 + int index = mixxx::kFirstHotCueIndex; + for (int i = 0; i < positions.size(); i++) { + posIndexHash.insert(positions[i], index); + index++; + } + } else { // HotcueSortMode::KeepOffsets + // Assign sorted indices + for (int i = 0; i < positions.size(); i++) { + posIndexHash.insert(positions[i], indices[i]); + } + } + + // Finally set new indices on hotcues + for (CuePointer& pCue : m_cuePoints) { + if (pCue->getType() != mixxx::CueType::HotCue) { + continue; + } + int newIndex = posIndexHash.take(pCue->getPosition()); + pCue->setHotCue(newIndex); + } + + markDirtyAndUnlock(&locked); + emit cuesUpdated(); +} + void Track::analysisFinished() { emit analyzed(); } diff --git a/src/track/track.h b/src/track/track.h index 446347914a9..d97204860f7 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -299,6 +299,11 @@ class Track : public QObject { /// Shift beatgrid by a constant offset void shiftBeatsMillis(double milliseconds); + /// Set hoctues' indices sorted by their frame position. + /// If compress is true, indices are consecutive and start at 0. + /// Set false to sort only, ie. keep empty hotcues before and in between. + void setHotcueIndicesSortedByPosition(HotcueSortMode sortMode); + // Call when analysis is done. void analysisFinished(); diff --git a/src/track/track_decl.h b/src/track/track_decl.h index bcc5d66e783..c3edb26daec 100644 --- a/src/track/track_decl.h +++ b/src/track/track_decl.h @@ -28,6 +28,11 @@ enum class ExportTrackMetadataResult { Skipped, }; +enum class HotcueSortMode { + KeepOffsets, + RemoveOffsets, +}; + // key for control to open/close the decks' track menus const QString kShowTrackMenuKey = QStringLiteral("show_track_menu"); diff --git a/src/widget/wtrackmenu.cpp b/src/widget/wtrackmenu.cpp index b20c18fe584..db9f48aa40d 100644 --- a/src/widget/wtrackmenu.cpp +++ b/src/widget/wtrackmenu.cpp @@ -476,6 +476,23 @@ void WTrackMenu::createActions() { m_pClearAllMetadataAction = make_parented(tr("All"), m_pClearMetadataMenu); connect(m_pClearAllMetadataAction, &QAction::triggered, this, &WTrackMenu::slotClearAllMetadata); + + m_pSortHotcuesByPositionCompressAction = make_parented( + tr("Sort hotcues by position (remove offsets)"), m_pClearMetadataMenu); + connect(m_pSortHotcuesByPositionCompressAction, + &QAction::triggered, + this, + [this]() { + slotSortHotcuesByPosition(HotcueSortMode::RemoveOffsets); + }); + m_pSortHotcuesByPositionAction = make_parented( + tr("Sort hotcues by position"), m_pClearMetadataMenu); + connect(m_pSortHotcuesByPositionAction, + &QAction::triggered, + this, + [this]() { + slotSortHotcuesByPosition(HotcueSortMode::KeepOffsets); + }); } if (featureIsEnabled(Feature::BPM)) { @@ -576,6 +593,11 @@ void WTrackMenu::createActions() { } void WTrackMenu::setupActions() { + addAction(m_pSortHotcuesByPositionCompressAction); + addAction(m_pSortHotcuesByPositionAction); + + addSeparator(); + if (featureIsEnabled(Feature::SearchRelated)) { addMenu(m_pSearchRelatedMenu); } @@ -2147,6 +2169,19 @@ class ResetOutroTrackPointerOperation : public mixxx::TrackPointerOperation { } }; +class SortHotcuesByPositionTrackPointerOperation : public mixxx::TrackPointerOperation { + public: + explicit SortHotcuesByPositionTrackPointerOperation(HotcueSortMode sortMode) + : m_sortMode(sortMode) { + } + + private: + void doApply(const TrackPointer& pTrack) const override { + pTrack->setHotcueIndicesSortedByPosition(m_sortMode); + } + HotcueSortMode m_sortMode; +}; + } // anonymous namespace void WTrackMenu::slotResetMainCue() { @@ -2199,6 +2234,28 @@ void WTrackMenu::slotClearHotCues() { &trackOperator); } +void WTrackMenu::slotSortHotcuesByPosition(HotcueSortMode sortMode) { + QString progressLabelText; + switch (sortMode) { + case HotcueSortMode::RemoveOffsets: + progressLabelText = tr( + "Sorting hotcues of %n track(s) by position (remove offsets)", + "", + getTrackCount()); + break; + case HotcueSortMode::KeepOffsets: + default: + progressLabelText = + tr("Sorting hotcues of %n track(s) by position", "", getTrackCount()); + break; + } + const auto trackOperator = + SortHotcuesByPositionTrackPointerOperation(sortMode); + applyTrackPointerOperation( + progressLabelText, + &trackOperator); +} + namespace { class ResetKeysTrackPointerOperation : public mixxx::TrackPointerOperation { diff --git a/src/widget/wtrackmenu.h b/src/widget/wtrackmenu.h index 4ff3678f3ea..f2f7234aeb7 100644 --- a/src/widget/wtrackmenu.h +++ b/src/widget/wtrackmenu.h @@ -12,6 +12,7 @@ #include "library/trackprocessing.h" #include "preferences/usersettings.h" #include "track/beats.h" +#include "track/track_decl.h" #include "track/trackref.h" #include "util/color/rgbcolor.h" #include "util/parented_ptr.h" @@ -168,6 +169,9 @@ class WTrackMenu : public QMenu { void slotScaleBpm(mixxx::Beats::BpmScale scale); void slotUndoBeatsChange(); + // Hotcues + void slotSortHotcuesByPosition(HotcueSortMode sortMode); + // Info and metadata void slotUpdateReplayGainFromPregain(); void slotShowDlgTagFetcher(); @@ -364,6 +368,8 @@ class WTrackMenu : public QMenu { parented_ptr m_pClearKeyAction; parented_ptr m_pClearReplayGainAction; parented_ptr m_pClearAllMetadataAction; + parented_ptr m_pSortHotcuesByPositionAction{}; + parented_ptr m_pSortHotcuesByPositionCompressAction{}; const UserSettingsPointer m_pConfig; Library* const m_pLibrary;