From e8544eb271b1f6dd1b69977f5964df575f2dd6be Mon Sep 17 00:00:00 2001 From: ronso0 Date: Wed, 15 Jan 2025 00:06:58 +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 --- src/audio/frame.h | 7 ++++ src/engine/controls/cuecontrol.cpp | 26 +++++++++++++ src/engine/controls/cuecontrol.h | 6 +++ src/track/track.cpp | 61 ++++++++++++++++++++++++++++++ src/track/track.h | 5 +++ src/track/track_decl.h | 5 +++ 6 files changed, 110 insertions(+) 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/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/track/track.cpp b/src/track/track.cpp index 6eae0749c5a..b025fe84cc5 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -989,6 +989,67 @@ void Track::shiftCuePositionsMillis(double milliseconds) { markDirtyAndUnlock(&locked); } +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 && + pCue->getType() != mixxx::CueType::Loop && + pCue->getType() != mixxx::CueType::Jump) { + 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 && + pCue->getType() != mixxx::CueType::Loop && + pCue->getType() != mixxx::CueType::Jump) { + 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 af398438ff7..e6e59fa3bc8 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -296,6 +296,11 @@ class Track : public QObject { void setMainCuePosition(mixxx::audio::FramePos position); /// Shift all cues by a constant offset void shiftCuePositionsMillis(mixxx::audio::FrameDiff_t 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");