Skip to content

Commit

Permalink
add controls to sort hotcues by position, optionally remove empty slo…
Browse files Browse the repository at this point in the history
…ts/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)
  • Loading branch information
ronso0 committed Feb 1, 2025
1 parent 65eb4ca commit 75d46a2
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 14 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/audio/frame.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <limits>

#include "engine/engine.h"
#include "util/compatibility/qhash.h"
#include "util/fpclassify.h"

namespace mixxx {
Expand Down Expand Up @@ -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<qhash_seed_t>(pos.value(), seed);
}

constexpr FramePos kInvalidFramePos = FramePos(FramePos::kInvalidValue);
constexpr FramePos kStartFramePos = FramePos(FramePos::kStartValue);
} // namespace audio
Expand Down
37 changes: 23 additions & 14 deletions src/controllers/controlpickermenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions src/engine/controls/cuecontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ CueControl::CueControl(const QString& group,
m_pPassthrough->connectValueChanged(this,
&CueControl::passthroughChanged,
Qt::DirectConnection);

m_pSortHotcuesByPos = std::make_unique<ControlPushButton>(ConfigKey(group, "sort_hotcues"));
connect(m_pSortHotcuesByPos.get(),
&ControlObject::valueChanged,
this,
&CueControl::setHotcueIndicesSortedByPosition,
Qt::DirectConnection);
m_pSortHotcuesByPosCompress = std::make_unique<ControlPushButton>(
ConfigKey(group, "sort_hotcues_remove_offsets"));
connect(m_pSortHotcuesByPosCompress.get(),
&ControlObject::valueChanged,
this,
&CueControl::setHotcueIndicesSortedByPositionCompress,
Qt::DirectConnection);
}

CueControl::~CueControl() {
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/engine/controls/cuecontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -362,6 +365,9 @@ class CueControl : public EngineControl {

parented_ptr<ControlProxy> m_pPassthrough;

std::unique_ptr<ControlPushButton> m_pSortHotcuesByPos;
std::unique_ptr<ControlPushButton> m_pSortHotcuesByPosCompress;

QAtomicPointer<HotcueControl> m_pCurrentSavedLoopControl;

// Tells us which controls map to which hotcue
Expand Down
80 changes: 80 additions & 0 deletions src/test/hotcueorderbyposition_test.cpp
Original file line number Diff line number Diff line change
@@ -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);
}
57 changes: 57 additions & 0 deletions src/track/track.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> indices;
QList<mixxx::audio::FramePos> 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<mixxx::audio::FramePos, int> 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();
}
Expand Down
5 changes: 5 additions & 0 deletions src/track/track.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
5 changes: 5 additions & 0 deletions src/track/track_decl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
57 changes: 57 additions & 0 deletions src/widget/wtrackmenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,23 @@ void WTrackMenu::createActions() {

m_pClearAllMetadataAction = make_parented<QAction>(tr("All"), m_pClearMetadataMenu);
connect(m_pClearAllMetadataAction, &QAction::triggered, this, &WTrackMenu::slotClearAllMetadata);

m_pSortHotcuesByPositionCompressAction = make_parented<QAction>(
tr("Sort hotcues by position (remove offsets)"), m_pClearMetadataMenu);
connect(m_pSortHotcuesByPositionCompressAction,
&QAction::triggered,
this,
[this]() {
slotSortHotcuesByPosition(HotcueSortMode::RemoveOffsets);
});
m_pSortHotcuesByPositionAction = make_parented<QAction>(
tr("Sort hotcues by position"), m_pClearMetadataMenu);
connect(m_pSortHotcuesByPositionAction,
&QAction::triggered,
this,
[this]() {
slotSortHotcuesByPosition(HotcueSortMode::KeepOffsets);
});
}

if (featureIsEnabled(Feature::BPM)) {
Expand Down Expand Up @@ -576,6 +593,11 @@ void WTrackMenu::createActions() {
}

void WTrackMenu::setupActions() {
addAction(m_pSortHotcuesByPositionCompressAction);
addAction(m_pSortHotcuesByPositionAction);

addSeparator();

if (featureIsEnabled(Feature::SearchRelated)) {
addMenu(m_pSearchRelatedMenu);
}
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 75d46a2

Please sign in to comment.