Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion src/library/autodj/autodjprocessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -791,12 +791,50 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes,
}
}

// Start the toDeck early so it is already playing (and not cold-starting)
// by the time the crossfader transition begins. We trigger kEarlyPlaySeconds
// before fadeBeginPos and seek the toDeck back by however much time actually
// remain until fadeBeginPos at that moment.
if (m_eState == ADJ_IDLE && thisDeck->isFromDeck && !otherDeck->loading &&
Comment thread
ywwg marked this conversation as resolved.
thisDeckPlaying && !otherDeckPlaying &&
otherDeck->startPos != kKeepPosition) {
Comment thread
ywwg marked this conversation as resolved.
const double fromDeckDurationSec = getEndSecond(thisDeck);
if (fromDeckDurationSec > 0.0) {
constexpr double kEarlyPlaySeconds = 1.0;
const double earlyPlayPos =
thisDeck->fadeBeginPos - (kEarlyPlaySeconds / fromDeckDurationSec);
Comment thread
ywwg marked this conversation as resolved.
Outdated
if (thisPlayPosition >= earlyPlayPos &&
thisPlayPosition < thisDeck->fadeBeginPos) {
Comment thread
ywwg marked this conversation as resolved.
Outdated
const double toDeckDurationSec = getEndSecond(otherDeck);
if (toDeckDurationSec > 0.0) {
// Seek the toDeck back by the exact number of seconds remaining
// until fadeBeginPos so that startPos aligns with the fade start,
// regardless of where in the buffer window this callback fired.
const double secondsUntilFade =
(thisDeck->fadeBeginPos - thisPlayPosition) * fromDeckDurationSec;
const double earlyStartPos =
otherDeck->startPos - (secondsUntilFade / toDeckDurationSec);
Comment thread
ywwg marked this conversation as resolved.
Outdated
otherDeck->setPlayPosition(earlyStartPos);
otherDeck->play();
Comment thread
ywwg marked this conversation as resolved.
Outdated
Comment thread
ywwg marked this conversation as resolved.
Outdated
m_eState = ADJ_PREROLLING;

if constexpr (sDebug) {
qDebug() << this << "playerPositionChanged"
<< "early play toDeck at" << earlyStartPos
<< "(" << secondsUntilFade << "s before startPos"
<< otherDeck->startPos << ")";
}
}
}
}
}

// If we are past this deck's posThreshold then:
// - transition into fading mode, play the other deck and fade to it.
// - check if fading is done and stop the deck
// - update the crossfader
if (thisPlayPosition >= thisDeck->fadeBeginPos && thisDeck->isFromDeck && !otherDeck->loading) {
if (m_eState == ADJ_IDLE) {
if (m_eState == ADJ_PREROLLING || m_eState == ADJ_IDLE) {
if (thisDeckPlaying || thisPlayPosition >= 1.0) {
// Set the state as FADING.
m_eState = thisDeck->isLeft() ? ADJ_LEFT_FADING : ADJ_RIGHT_FADING;
Expand All @@ -817,6 +855,8 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes,
setCrossfader(thisDeck->isLeft() ? 1.0 : -1.0);
}

// There's a chance the user loaded a track during the preroll -- just start it now,
// it's not worth trying to recover the correct preroll position.
if (!otherDeckPlaying) {
otherDeck->play();
}
Expand Down Expand Up @@ -1623,6 +1663,8 @@ void AutoDJProcessor::playerTrackLoaded(DeckAttributes* pDeck, TrackPointer pTra
pDeck->play();
}
}
// If the state is prerolling, playerPositionChanged will tell the other deck to start playing
// at the correct fade time.
}

void AutoDJProcessor::playerLoadingTrack(DeckAttributes* pDeck,
Expand Down
2 changes: 1 addition & 1 deletion src/library/autodj/autodjprocessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ class AutoDJProcessor : public QObject {
ADJ_LEFT_FADING,
ADJ_RIGHT_FADING,
ADJ_ENABLE_P1LOADED,
ADJ_ENABLE_P1PLAYING,
ADJ_PREROLLING,
ADJ_DISABLED
};

Expand Down
79 changes: 79 additions & 0 deletions src/test/autodjprocessor_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1855,6 +1855,85 @@ TEST_F(AutoDJProcessorTest, FadeToDeck2_SeekBeforeTransition) {
EXPECT_DOUBLE_EQ(0, deck2.playposition.get());
}

// Verify that the toDeck is started ~1 second before the fade threshold so it
// is already spinning when the crossfader begins to move, eliminating the
// cold-start gap.
//
// With a 100-second track and 10-second transition time in FixedFullTrack mode:
// fadeBeginPos = 0.9 (90 s / 100 s)
// earlyPlayPos = 0.89 (fadeBeginPos - 1s/100s)
// earlyStartPos = -0.01 (startPos 0.0 - 1s/100s)
TEST_F(AutoDJProcessorTest, FadeToDeck2_EarlyPlay_PreRoll) {
pProcessor->setTransitionMode(AutoDJProcessor::TransitionMode::FixedFullTrack);

TrackId testId = addTrackToCollection(kTrackLocationTest);
ASSERT_TRUE(testId.isValid());

// Crossfader starts on the left.
mixer.crossfader.set(-1.0);
// Use a 100-second track so positions are simple round numbers.
TrackPointer pTrack = newTestTrack();
pTrack->setDuration(100);
deck1.slotLoadTrack(pTrack,
#ifdef __STEM__
mixxx::StemChannelSelection(),
#endif
true);
deck1.fakeTrackLoadedEvent(pTrack);

PlaylistTableModel* pAutoDJTableModel = pProcessor->getTableModel();
pAutoDJTableModel->appendTrack(testId);

EXPECT_CALL(*pProcessor, emitAutoDJStateChanged(AutoDJProcessor::ADJ_IDLE));
EXPECT_CALL(*pProcessor, emitLoadTrackToPlayer(_, QString("[Channel2]"), false));

AutoDJProcessor::AutoDJError err = pProcessor->toggleAutoDJ(true);
EXPECT_EQ(AutoDJProcessor::ADJ_OK, err);
EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState());

// Pretend the track load succeeds; deck2 is cued to startPos = 0.0.
deck2.slotLoadTrack(pTrack,
#ifdef __STEM__
mixxx::StemChannelSelection(),
#endif
false);
deck2.fakeTrackLoadedEvent(pTrack);

EXPECT_DOUBLE_EQ(0.0, deck2.playposition.get());
EXPECT_DOUBLE_EQ(0.0, deck2.play.get());
EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState());
EXPECT_DOUBLE_EQ(-1.0, mixer.crossfader.get());
EXPECT_DOUBLE_EQ(1.0, deck1.play.get());

// Seek deck1 to just before the early-play window (< 0.89). Deck2 should
// not start playing yet.
deck1.playposition.set(0.88);
EXPECT_DOUBLE_EQ(0.0, deck2.play.get());
EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState());

// Seek deck1 to the early-play trigger point (fadeBeginPos - 1s = 0.89).
// Deck2 should now be playing and seeked 1 second before its startPos,
// i.e. at -0.01. State remains IDLE – the crossfader has not moved yet.
deck1.playposition.set(0.89);
EXPECT_DOUBLE_EQ(1.0, deck2.play.get());
EXPECT_NEAR(-0.01, deck2.playposition.get(), 1e-9);
Comment thread
ywwg marked this conversation as resolved.
EXPECT_EQ(AutoDJProcessor::ADJ_IDLE, pProcessor->getState());
EXPECT_DOUBLE_EQ(-1.0, mixer.crossfader.get());

// Advance deck1 to fadeBeginPos (0.9). The transition starts; deck2 was
// already playing so no redundant play() call is made.
EXPECT_CALL(*pProcessor, emitAutoDJStateChanged(AutoDJProcessor::ADJ_LEFT_FADING));
deck1.playposition.set(0.9);
EXPECT_EQ(AutoDJProcessor::ADJ_LEFT_FADING, pProcessor->getState());
EXPECT_DOUBLE_EQ(1.0, deck1.play.get());
EXPECT_DOUBLE_EQ(1.0, deck2.play.get());

// Advance deck1 further into the fade; crossfader should have moved toward
// the right.
deck1.playposition.set(0.95);
EXPECT_LT(-1.0, mixer.crossfader.get());
}

TEST_F(AutoDJProcessorTest, TrackZeroLength) {
TrackId testId = addTrackToCollection(kTrackLocationTest);
ASSERT_TRUE(testId.isValid());
Expand Down
Loading