From ae7079212c7dfe3a28b07b0998e0865da853c796 Mon Sep 17 00:00:00 2001
From: Lukas Waslowski <cr7pt0gr4ph7@gmail.com>
Date: Fri, 3 May 2024 13:45:09 +0000
Subject: [PATCH] AutoDJProcessor: Add option to reset the crossfader to
 neutral

---
 src/library/autodj/autodjprocessor.cpp     | 86 +++++++++++++++++-----
 src/library/autodj/autodjprocessor.h       |  1 +
 src/preferences/dialog/dlgprefautodj.cpp   | 13 ++++
 src/preferences/dialog/dlgprefautodj.h     |  1 +
 src/preferences/dialog/dlgprefautodjdlg.ui | 59 +++++++++++++++
 5 files changed, 141 insertions(+), 19 deletions(-)

diff --git a/src/library/autodj/autodjprocessor.cpp b/src/library/autodj/autodjprocessor.cpp
index 0910d6f9b89..7f46593380b 100644
--- a/src/library/autodj/autodjprocessor.cpp
+++ b/src/library/autodj/autodjprocessor.cpp
@@ -14,12 +14,17 @@
 namespace {
 const char* kTransitionPreferenceName = "Transition";
 const char* kTransitionModePreferenceName = "TransitionMode";
+const char* kResetFaderPreferenceName = "ResetFaderToNeutralOnIdle";
 constexpr double kTransitionPreferenceDefault = 10.0;
 constexpr double kKeepPosition = -1.0;
 
 // A track needs to be longer than two callbacks to not stop AutoDJ
 constexpr double kMinimumTrackDurationSec = 0.2;
 
+constexpr double kCrossfaderLeftOnly = -1.0;
+constexpr double kCrossfaderNeutral = 0.0;
+constexpr double kCrossfaderRightOnly = 1.0;
+
 constexpr bool sDebug = false;
 } // anonymous namespace
 
@@ -209,6 +214,26 @@ void AutoDJProcessor::setCrossfader(double value) {
     m_pCOCrossfader->set(value);
 }
 
+void AutoDJProcessor::setCrossfaderToIdle(double value) {
+    DEBUG_ASSERT(value == kCrossfaderLeftOnly || value == kCrossfaderRightOnly);
+
+    // Depending on the user's preferences, the idle position
+    // of the crossfader is either fully to the left/right,
+    // or in the middle.
+    const bool resetFaderToNeutralOnIdle = m_pConfig->getValue<bool>(
+            ConfigKey(kConfigKey, kResetFaderPreferenceName),
+            false);
+
+    if (resetFaderToNeutralOnIdle) {
+        // Move crossfader to neutral. Crossfader will be moved
+        // to the left/right just before starting a crossfade.
+        setCrossfader(kCrossfaderNeutral);
+    } else {
+        // Move crossfader fully to the left/right
+        setCrossfader(value);
+    }
+}
+
 AutoDJProcessor::AutoDJError AutoDJProcessor::shufflePlaylist(
         const QModelIndexList& selectedIndices) {
     QModelIndex exclude;
@@ -543,8 +568,11 @@ AutoDJProcessor::AutoDJError AutoDJProcessor::toggleAutoDJ(bool enable) {
             // playerPositionChanged for deck1 after the track is loaded.
             m_eState = ADJ_ENABLE_P1LOADED;
 
-            // Move crossfader to the left.
-            setCrossfader(-1.0);
+            // Move crossfader to its idle position (either to the left,
+            // or in the middle, depending on the user's preferences).
+            // We will set it fully to the left just before starting
+            // a crossfade anyway.
+            setCrossfaderToIdle(kCrossfaderLeftOnly);
 
             // Load track into the left deck and play. Once it starts playing,
             // we will receive a playerPositionChanged update for deck 1 which
@@ -557,13 +585,21 @@ AutoDJProcessor::AutoDJError AutoDJProcessor::toggleAutoDJ(bool enable) {
             if (leftDeckPlaying) {
                 // Load track into the right deck.
                 emitLoadTrackToPlayer(nextTrack, pRightDeck->group, false);
-                // Move crossfader to the left.
-                setCrossfader(-1.0);
+
+                // Move crossfader to its idle position (either to the left,
+                // or in the middle, depending on the user's preferences).
+                // We will set it fully to the left just before starting
+                // a crossfade anyway.
+                setCrossfaderToIdle(kCrossfaderLeftOnly);
             } else {
                 // Load track into the left deck.
                 emitLoadTrackToPlayer(nextTrack, pLeftDeck->group, false);
-                // Move crossfader to the right.
-                setCrossfader(1.0);
+
+                // Move crossfader to its idle position (either to the right,
+                // or in the middle, depending on the user's preferences).
+                // We will set it fully to the right just before starting
+                // a crossfade anyway.
+                setCrossfaderToIdle(kCrossfaderRightOnly);
             }
         }
         emitAutoDJStateChanged(m_eState);
@@ -742,12 +778,11 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes,
         // If the user stops the toDeck during a fade, let the fade continue
         // and do not load the next track.
         if (!otherDeckPlaying && otherDeck->isFromDeck) {
-            // Force crossfader all the way to the (non fading) toDeck.
-            if (m_eState == ADJ_RIGHT_FADING) {
-                setCrossfader(-1.0);
-            } else {
-                setCrossfader(1.0);
-            }
+            // Force crossfader all the way to the (non fading) toDeck,
+            // or to the middle, depending on the user's preferences.
+            setCrossfaderToIdle(m_eState == ADJ_RIGHT_FADING
+                            ? kCrossfaderLeftOnly
+                            : kCrossfaderRightOnly);
             m_eState = ADJ_IDLE;
             // Invalidate threshold calculated for the old otherDeck
             // This avoids starting a fade back before the new track is
@@ -804,12 +839,25 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes,
                     otherDeck->setPlayPosition(otherDeck->startPos);
                 }
 
-                if (!otherDeckPlaying) {
-                    otherDeck->play();
-                }
+                const bool resetFaderToNeutralOnIdle = m_pConfig->getValue<bool>(
+                        ConfigKey(kConfigKey, kResetFaderPreferenceName),
+                        false);
 
                 if (thisDeck->fadeBeginPos >= thisDeck->fadeEndPos) {
-                    setCrossfader(thisDeck->isLeft() ? 1.0 : -1.0);
+                    // This deck has an invalid fade position, so we
+                    // immediately switch over to the other deck.
+                    setCrossfader(thisDeck->isLeft() ? kCrossfaderRightOnly : kCrossfaderLeftOnly);
+                } else if (!otherDeckPlaying && resetFaderToNeutralOnIdle) {
+                    // The user has requested the crossfader to be reset to
+                    // neutral as long as no automatic crossfade is in progress
+                    // (which is handled by setCrossfaderToIdle), so we need
+                    // to set up the crossfader here instead, right before
+                    // starting the fade.
+                    setCrossfader(thisDeck->isLeft() ? kCrossfaderLeftOnly : kCrossfaderRightOnly);
+                }
+
+                if (!otherDeckPlaying) {
+                    otherDeck->play();
                 }
 
                 // Now that we have started the other deck playing, remove the track
@@ -827,9 +875,9 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes,
 
         double crossfaderTarget;
         if (m_eState == ADJ_LEFT_FADING) {
-            crossfaderTarget = 1.0;
+            crossfaderTarget = kCrossfaderRightOnly;
         } else if (m_eState == ADJ_RIGHT_FADING) {
-            crossfaderTarget = -1.0;
+            crossfaderTarget = kCrossfaderLeftOnly;
         } else {
             // this happens if the not playing track is cued into the outro region,
             // calculated for the swapped roles.
@@ -847,7 +895,7 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes,
             m_transitionProgress = 1.0;
             // Note: If the user has stopped the toDeck during the transition.
             // this deck just stops as well. In this case a stopped AutoDJ is accepted
-            // because the use did it intentionally
+            // because the user did it intentionally
         } else {
             // We are in Fading state.
             // Calculate the current transitionProgress, the place between begin
diff --git a/src/library/autodj/autodjprocessor.h b/src/library/autodj/autodjprocessor.h
index 600cf7bfb70..87efdaa266b 100644
--- a/src/library/autodj/autodjprocessor.h
+++ b/src/library/autodj/autodjprocessor.h
@@ -242,6 +242,7 @@ class AutoDJProcessor : public QObject {
     // every time)
     double getCrossfader() const;
     void setCrossfader(double value);
+    void setCrossfaderToIdle(double value);
 
     // Following functions return seconds computed from samples or -1 if
     // track in deck has invalid sample rate (<= 0)
diff --git a/src/preferences/dialog/dlgprefautodj.cpp b/src/preferences/dialog/dlgprefautodj.cpp
index 7842318bf60..1761d0873e9 100644
--- a/src/preferences/dialog/dlgprefautodj.cpp
+++ b/src/preferences/dialog/dlgprefautodj.cpp
@@ -8,6 +8,14 @@ DlgPrefAutoDJ::DlgPrefAutoDJ(QWidget* pParent,
           m_pConfig(pConfig) {
     setupUi(this);
 
+    // Whether to reset the crossfader to neutral when not fading
+    ResetFaderToNeutralOnIdleCheckBox->setChecked(m_pConfig->getValue(
+            ConfigKey("[Auto DJ]", "ResetFaderToNeutralOnIdle"), false));
+    connect(ResetFaderToNeutralOnIdleCheckBox,
+            &QCheckBox::stateChanged,
+            this,
+            &DlgPrefAutoDJ::slotToggleResetFaderToNeutralOnIdle);
+
     // The minimum available for randomly-selected tracks
     MinimumAvailableSpinBox->setValue(
             m_pConfig->getValue(
@@ -154,6 +162,11 @@ void DlgPrefAutoDJ::slotToggleRequeueIgnore(int buttonState) {
     RequeueIgnoreTimeEdit->setEnabled(checked);
 }
 
+void DlgPrefAutoDJ::slotToggleResetFaderToNeutralOnIdle(int buttonState) {
+    bool checked = buttonState == Qt::Checked;
+    m_pConfig->setValue(ConfigKey("[Auto DJ]", "ResetFaderToNeutralOnIdle"), checked);
+}
+
 void DlgPrefAutoDJ::slotSetRequeueIgnoreTime(const QTime& a_rTime) {
     QString str = a_rTime.toString(RequeueIgnoreTimeEdit->displayFormat());
     m_pConfig->set(ConfigKey("[Auto DJ]", "IgnoreTimeBuff"), str);
diff --git a/src/preferences/dialog/dlgprefautodj.h b/src/preferences/dialog/dlgprefautodj.h
index cc5e28fde06..054cf304f36 100644
--- a/src/preferences/dialog/dlgprefautodj.h
+++ b/src/preferences/dialog/dlgprefautodj.h
@@ -20,6 +20,7 @@ class DlgPrefAutoDJ : public DlgPreferencePage, public Ui::DlgPrefAutoDJDlg {
   private slots:
     void slotSetMinimumAvailable(int);
     void slotToggleRequeueIgnore(int buttonState);
+    void slotToggleResetFaderToNeutralOnIdle(int buttonState);
     void slotSetRequeueIgnoreTime(const QTime& a_rTime);
     void slotSetRandomQueueMin(int);
     void slotConsiderRepeatPlaylistState(bool);
diff --git a/src/preferences/dialog/dlgprefautodjdlg.ui b/src/preferences/dialog/dlgprefautodjdlg.ui
index c84dab445dd..585555bac82 100644
--- a/src/preferences/dialog/dlgprefautodjdlg.ui
+++ b/src/preferences/dialog/dlgprefautodjdlg.ui
@@ -15,6 +15,65 @@
   </property>
   <layout class="QVBoxLayout" name="AutoDJGridLayout">
 
+    <item>
+    <widget class="QGroupBox" name="CrossfaderOptions">
+      <property name="title">
+       <string>Crossfader</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+      </property>
+      <layout class="QGridLayout" name="CrossfaderGridLayout">
+
+       <item row="0" column="0">
+        <widget class="QLabel" name="ResetFaderToNeutralOnIdleLabel">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="toolTip">
+          <string>Check to always reset the crossfader to neutral when no fade is in progress.</string>
+         </property>
+         <property name="text">
+          <string>Reset fader to neutral after crossfading</string>
+         </property>
+          <property name="buddy">
+           <cstring>ResetFaderToNeutralOnIdleCheckBox</cstring>
+          </property>
+        </widget>
+       </item>
+
+       <item row="0" column="1">
+        <widget class="QCheckBox" name="ResetFaderToNeutralOnIdleCheckBox">
+         <property name="toolTip">
+          <string>Check to always reset the crossfader to neutral when no fade is in progress.</string>
+         </property>
+         <property name="text">
+          <string/>
+         </property>
+        </widget>
+       </item>
+
+       <item row="1" column="2">
+        <spacer name="horizontalSpacerCrossfader">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Expanding" vsizetype="Minimum">
+            <horstretch>1</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+        </spacer>
+       </item>
+
+      </layout>
+    </widget>
+   </item>
+
    <item>
     <widget class="QGroupBox" name="RequeueOptions">
       <property name="title">