diff --git a/src/analyzer/analyzersilence.cpp b/src/analyzer/analyzersilence.cpp index a1b463a1218..0cfe9b24b9e 100644 --- a/src/analyzer/analyzersilence.cpp +++ b/src/analyzer/analyzersilence.cpp @@ -10,33 +10,56 @@ namespace { // verify that the track samples have not changed since the last analysis constexpr CSAMPLE kSilenceThreshold = 0.001f; // -60 dB // TODO: Change the above line to: -//constexpr CSAMPLE kSilenceThreshold = db2ratio(-60.0f); +// constexpr CSAMPLE kSilenceThreshold = db2ratio(-60.0f); + +// This comment can be deleted for full release. +// Some other values in case. These are in dBV expressed as Volts RMS +// (which seems, sensibly, the way Mixxx works). +// N_10DB_FADE_THRESHOLD 0.3162f +// N_12DB_FADE_THRESHOLD 0.2511f +// N_15DB_FADE_THRESHOLD 0.1778f +// N_18DB_FADE_THRESHOLD 0.1259f +// N_20DB_FADE_THRESHOLD 0.1f +// N_24DB_FADE_THRESHOLD 0.0631f +// N_25DB_FADE_THRESHOLD 0.0562f +// N_27DB_FADE_THRESHOLD 0.0447f +// N_30DB_FADE_THRESHOLD 0.0316f +// N_40DB_FADE_THRESHOLD 0.01f + +constexpr CSAMPLE kFadeInThreshold = 0.0447f; // -27 dBV +constexpr CSAMPLE kFadeOutThreshold = 0.2511f; // -12 dBV bool shouldAnalyze(TrackPointer pTrack) { CuePointer pIntroCue = pTrack->findCueByType(mixxx::CueType::Intro); CuePointer pOutroCue = pTrack->findCueByType(mixxx::CueType::Outro); CuePointer pN60dBSound = pTrack->findCueByType(mixxx::CueType::N60dBSound); + CuePointer pFadeIn = pTrack->findCueByType(mixxx::CueType::FadeIn); + CuePointer pFadeOut = pTrack->findCueByType(mixxx::CueType::FadeOut); - if (!pIntroCue || !pOutroCue || !pN60dBSound || pN60dBSound->getLengthFrames() <= 0) { + if (!pFadeIn || !pFadeOut || !pIntroCue || !pOutroCue || !pN60dBSound || + pN60dBSound->getLengthFrames() <= 0) { return true; } return false; } -template -Iterator first_sound(Iterator begin, Iterator end) { - return std::find_if(begin, end, [](const auto elem) { - return fabs(elem) >= kSilenceThreshold; +template +Iterator find_first_above_threshold(Iterator begin, + Iterator end, + Threshold threshold) { + return std::find_if(begin, end, [threshold](const auto elem) { + return fabs(elem) >= threshold; }); } - } // anonymous namespace AnalyzerSilence::AnalyzerSilence(UserSettingsPointer pConfig) : m_pConfig(pConfig), m_framesProcessed(0), m_signalStart(-1), - m_signalEnd(-1) { + m_signalEnd(-1), + m_fadeThresholdFadeInEnd(-1), + m_fadeThresholdFadeOutStart(-1) { } bool AnalyzerSilence::initialize(const AnalyzerTrack& track, @@ -53,6 +76,8 @@ bool AnalyzerSilence::initialize(const AnalyzerTrack& track, m_framesProcessed = 0; m_signalStart = -1; m_signalEnd = -1; + m_fadeThresholdFadeInEnd = -1; + m_fadeThresholdFadeOutStart = -1; m_channelCount = channelCount; return true; @@ -60,13 +85,38 @@ bool AnalyzerSilence::initialize(const AnalyzerTrack& track, // static SINT AnalyzerSilence::findFirstSoundInChunk(std::span samples) { - return std::distance(samples.begin(), first_sound(samples.begin(), samples.end())); + return std::distance(samples.begin(), + find_first_above_threshold( + samples.begin(), samples.end(), kSilenceThreshold)); } // static SINT AnalyzerSilence::findLastSoundInChunk(std::span samples) { // -1 is required, because the distance from the fist sample index (0) to crend() is 1, - SINT ret = std::distance(first_sound(samples.rbegin(), samples.rend()), samples.rend()) - 1; + SINT ret = std::distance(find_first_above_threshold(samples.rbegin(), + samples.rend(), + kSilenceThreshold), + samples.rend()) - + 1; + return ret; +} + +// Find the index of first sound sample where the sound is above kFadeInThreshold (-27db) +SINT AnalyzerSilence::findLastFadeInChunk(std::span samples) { + SINT ret = std::distance(samples.begin(), + find_first_above_threshold( + samples.begin(), samples.end(), kFadeInThreshold)); + return ret; +} + +// Find the index of last sound sample where the sound is above kFadeOutThreshold (-12db) +SINT AnalyzerSilence::findFirstFadeOutChunk(std::span samples) { + // Note we are searching backwards from the end here. + SINT ret = std::distance(find_first_above_threshold(samples.rbegin(), + samples.rend(), + kFadeOutThreshold), + samples.rend()) - + 1; return ret; } @@ -93,10 +143,24 @@ bool AnalyzerSilence::processSamples(const CSAMPLE* pIn, SINT count) { m_signalStart = m_framesProcessed + firstSoundSample / m_channelCount; } } - if (m_signalStart >= 0) { + + if (m_fadeThresholdFadeInEnd < 0) { + const SINT lastSampleOfFadeIn = findLastFadeInChunk(samples); + if (lastSampleOfFadeIn < count) { + m_fadeThresholdFadeInEnd = m_framesProcessed + (lastSampleOfFadeIn / m_channelCount); + } + } + if (m_fadeThresholdFadeInEnd >= 0) { + const SINT lasttSampleBeforeFadeOut = findFirstFadeOutChunk(samples); + if (lasttSampleBeforeFadeOut >= 0) { + m_fadeThresholdFadeOutStart = m_framesProcessed + + (lasttSampleBeforeFadeOut / m_channelCount) + 1; + } + } + if (m_fadeThresholdFadeOutStart >= 0) { const SINT lastSoundSample = findLastSoundInChunk(samples); if (lastSoundSample >= 0) { - m_signalEnd = m_framesProcessed + lastSoundSample / m_channelCount + 1; + m_signalEnd = m_framesProcessed + (lastSoundSample / m_channelCount) + 1; } } @@ -136,6 +200,36 @@ void AnalyzerSilence::storeResults(TrackPointer pTrack) { setupMainAndIntroCue(pTrack.get(), firstSoundPosition, m_pConfig.data()); setupOutroCue(pTrack.get(), lastSoundPosition); + + if (m_fadeThresholdFadeInEnd < 0) { + m_fadeThresholdFadeInEnd = 0; + } + if (m_fadeThresholdFadeOutStart < 0) { + m_fadeThresholdFadeOutStart = m_framesProcessed; + } + const auto fadeInEndPosition = mixxx::audio::FramePos(m_fadeThresholdFadeInEnd); + CuePointer pFadeIn = pTrack->findCueByType(mixxx::CueType::FadeIn); + if (pFadeIn == nullptr) { + pFadeIn = pTrack->createAndAddCue( + mixxx::CueType::FadeIn, + Cue::kNoHotCue, + firstSoundPosition, + fadeInEndPosition); + } else { + pFadeIn->setStartAndEndPosition(firstSoundPosition, fadeInEndPosition); + } + + const auto fadeOutStartPosition = mixxx::audio::FramePos(m_fadeThresholdFadeOutStart); + CuePointer pFadeOut = pTrack->findCueByType(mixxx::CueType::FadeOut); + if (pFadeOut == nullptr) { + pFadeOut = pTrack->createAndAddCue( + mixxx::CueType::FadeOut, + Cue::kNoHotCue, + fadeOutStartPosition, + lastSoundPosition); + } else { + pFadeOut->setStartAndEndPosition(fadeOutStartPosition, lastSoundPosition); + } } // static diff --git a/src/analyzer/analyzersilence.h b/src/analyzer/analyzersilence.h index baca4e54e99..0595db82901 100644 --- a/src/analyzer/analyzersilence.h +++ b/src/analyzer/analyzersilence.h @@ -34,6 +34,15 @@ class AnalyzerSilence : public Analyzer { UserSettings* pConfig); static void setupOutroCue(Track* pTrack, mixxx::audio::FramePos lastSoundPosition); + /// returns the index of the first sample in the buffer that is above the + /// fade in threshold (e.g. -27 dB). + static SINT findLastFadeInChunk(std::span samples); + + /// returns the index of the last sample in the buffer that is + /// above the fade out threshold (e.g. -12 dB) or samples.size() if no + /// sample is found + static SINT findFirstFadeOutChunk(std::span samples); + /// returns the index of the first sample in the buffer that is above -60 dB /// or samples.size() if no sample is found static SINT findFirstSoundInChunk(std::span samples); @@ -56,4 +65,6 @@ class AnalyzerSilence : public Analyzer { SINT m_framesProcessed; SINT m_signalStart; SINT m_signalEnd; + SINT m_fadeThresholdFadeInEnd; + SINT m_fadeThresholdFadeOutStart; }; diff --git a/src/library/autodj/autodjprocessor.cpp b/src/library/autodj/autodjprocessor.cpp index 473663eeb69..91e8add4ff6 100644 --- a/src/library/autodj/autodjprocessor.cpp +++ b/src/library/autodj/autodjprocessor.cpp @@ -44,12 +44,9 @@ DeckAttributes::DeckAttributes(int index, m_sampleRate(group, "track_samplerate"), m_rateRatio(group, "rate_ratio"), m_pPlayer(pPlayer) { - connect(m_pPlayer, &BaseTrackPlayer::newTrackLoaded, - this, &DeckAttributes::slotTrackLoaded); - connect(m_pPlayer, &BaseTrackPlayer::loadingTrack, - this, &DeckAttributes::slotLoadingTrack); - connect(m_pPlayer, &BaseTrackPlayer::playerEmpty, - this, &DeckAttributes::slotPlayerEmpty); + connect(m_pPlayer, &BaseTrackPlayer::newTrackLoaded, this, &DeckAttributes::slotTrackLoaded); + connect(m_pPlayer, &BaseTrackPlayer::loadingTrack, this, &DeckAttributes::slotLoadingTrack); + connect(m_pPlayer, &BaseTrackPlayer::playerEmpty, this, &DeckAttributes::slotPlayerEmpty); m_playPos.connectValueChanged(this, &DeckAttributes::slotPlayPosChanged); m_play.connectValueChanged(this, &DeckAttributes::slotPlayChanged); m_introStartPos.connectValueChanged(this, &DeckAttributes::slotIntroStartPositionChanged); @@ -91,7 +88,7 @@ void DeckAttributes::slotTrackLoaded(TrackPointer pTrack) { } void DeckAttributes::slotLoadingTrack(TrackPointer pNewTrack, TrackPointer pOldTrack) { - //qDebug() << "DeckAttributes::slotLoadingTrack"; + // qDebug() << "DeckAttributes::slotLoadingTrack"; emit loadingTrack(this, pNewTrack, pOldTrack); } @@ -134,8 +131,7 @@ AutoDJProcessor::AutoDJProcessor( m_pSkipNext = new ControlPushButton( ConfigKey("[AutoDJ]", "skip_next")); - connect(m_pSkipNext, &ControlObject::valueChanged, - this, &AutoDJProcessor::controlSkipNext); + connect(m_pSkipNext, &ControlObject::valueChanged, this, &AutoDJProcessor::controlSkipNext); m_pAddRandomTrack = new ControlPushButton( ConfigKey("[AutoDJ]", "add_random_track")); @@ -146,8 +142,7 @@ AutoDJProcessor::AutoDJProcessor( m_pFadeNow = new ControlPushButton( ConfigKey("[AutoDJ]", "fade_now")); - connect(m_pFadeNow, &ControlObject::valueChanged, - this, &AutoDJProcessor::controlFadeNow); + connect(m_pFadeNow, &ControlObject::valueChanged, this, &AutoDJProcessor::controlFadeNow); m_pEnabledAutoDJ = new ControlPushButton( ConfigKey("[AutoDJ]", "enabled")); @@ -334,7 +329,8 @@ void AutoDJProcessor::fadeNow() { pFromDeck->fadeBeginPos /= fromDeckDuration; pFromDeck->fadeEndPos /= fromDeckDuration; pToDeck->startPos /= toDeckDuration; - + pToDeck->playNextPos /= toDeckDuration; + pFromDeck->playNextPos /= toDeckDuration; VERIFY_OR_DEBUG_ASSERT(pFromDeck->fadeBeginPos <= 1) { pFromDeck->fadeBeginPos = 1; } @@ -367,7 +363,7 @@ AutoDJProcessor::AutoDJError AutoDJProcessor::skipNext() { TrackId leftId = pLeftDeck->getLoadedTrack()->getId(); TrackId rightId = pRightDeck->getLoadedTrack()->getId(); if (nextId == leftId || nextId == rightId) { - // One of the playing tracks is still on top of playlist, remove second item + // One of the playing tracks is still on top of playlist, remove second item m_pAutoDJTableModel->removeTrack(m_pAutoDJTableModel->index(1, 0)); } else { m_pAutoDJTableModel->removeTrack(m_pAutoDJTableModel->index(0, 0)); @@ -567,6 +563,7 @@ AutoDJProcessor::AutoDJError AutoDJProcessor::toggleAutoDJ(bool enable) { // One of the two decks is playing. Switch into IDLE mode and wait // until the playing deck crosses posThreshold to start fading. m_eState = ADJ_IDLE; + m_isNowStartingEarly = false; if (leftDeckPlaying) { // Load track into the right deck. emitLoadTrackToPlayer(nextTrack, pRightDeck->group, false); @@ -673,7 +670,7 @@ void AutoDJProcessor::crossfaderChanged(double value) { } void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes, - double thisPlayPosition) { + double thisPlayPosition) { // qDebug() << "player" << pAttributes->group << "PositionChanged(" << value << ")"; if (m_eState == ADJ_DISABLED) { // nothing to do @@ -720,6 +717,7 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes, // sure our thresholds are configured (by calling calculateFadeThresholds // for the playing deck). m_eState = ADJ_IDLE; + m_isNowStartingEarly = false; if (!rightDeckPlaying) { // Only left deck playing! @@ -766,6 +764,7 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes, setCrossfader(1.0); } m_eState = ADJ_IDLE; + m_isNowStartingEarly = false; // Invalidate threshold calculated for the old otherDeck // This avoids starting a fade back before the new track is // loaded into the otherDeck @@ -801,11 +800,42 @@ void AutoDJProcessor::playerPositionChanged(DeckAttributes* pAttributes, } } + // We use thisDeck->playNextPos to start the next track earlier than thisDeck->fadeEndPos + // so in RadioLanewayCrossfade we can start a track with a slow fade in earlier, but with out + // fading the current track, so we won't bork cold endings. + if (!m_isNowStartingEarly && + (thisPlayPosition >= thisDeck->playNextPos) && + (thisPlayPosition < thisDeck->fadeBeginPos) && + thisDeck->isFromDeck && !otherDeck->loading) { + if (m_eState == ADJ_IDLE) { + m_isNowStartingEarly = true; + if (thisDeckPlaying || thisPlayPosition >= 1.0) { + const double toDeckFadeDistance = + (thisDeck->fadeEndPos - thisDeck->fadeBeginPos) * + getEndSecond(thisDeck) / getEndSecond(otherDeck); + // Re-cue the track if the user has sought forward and will miss the fadeBeginPos + if (otherDeck->playPosition() >= otherDeck->fadeBeginPos - toDeckFadeDistance) { + otherDeck->setPlayPosition(otherDeck->startPos); + } + if (m_crossfaderStartCenter) { + setCrossfader(0.0); + } else if (thisDeck->fadeBeginPos >= thisDeck->fadeEndPos) { + setCrossfader(thisDeck->isLeft() ? 1.0 : -1.0); + } + + if (!otherDeckPlaying) { + otherDeck->play(); + } + } + } + } + // 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 ((thisPlayPosition >= thisDeck->fadeBeginPos) && + thisDeck->isFromDeck && !otherDeck->loading) { if (m_eState == ADJ_IDLE) { if (thisDeckPlaying || thisPlayPosition >= 1.0) { // Set the state as FADING. @@ -899,7 +929,8 @@ TrackPointer AutoDJProcessor::getNextTrackFromQueue() { bool randomQueueEnabled = m_pConfig->getValue( ConfigKey("[Auto DJ]", "EnableRandomQueue")); int minAutoDJCrateTracks = m_pConfig->getValueString( - ConfigKey(kConfigKey, "RandomQueueMinimumAllowed")).toInt(); + ConfigKey(kConfigKey, "RandomQueueMinimumAllowed")) + .toInt(); int tracksToAdd = minAutoDJCrateTracks - m_pAutoDJTableModel->rowCount(); // In case we start off with < minimum tracks if (randomQueueEnabled && (tracksToAdd > 0)) { @@ -908,7 +939,7 @@ TrackPointer AutoDJProcessor::getNextTrackFromQueue() { while (true) { TrackPointer nextTrack = m_pAutoDJTableModel->getTrack( - m_pAutoDJTableModel->index(0, 0)); + m_pAutoDJTableModel->index(0, 0)); if (nextTrack) { if (nextTrack->getFileInfo().checkFileExists()) { @@ -987,9 +1018,11 @@ bool AutoDJProcessor::removeTrackFromTopOfQueue(TrackPointer pTrack) { void AutoDJProcessor::maybeFillRandomTracks() { int minAutoDJCrateTracks = m_pConfig->getValueString( - ConfigKey(kConfigKey, "RandomQueueMinimumAllowed")).toInt(); + ConfigKey(kConfigKey, "RandomQueueMinimumAllowed")) + .toInt(); bool randomQueueEnabled = (((m_pConfig->getValueString( - ConfigKey("[Auto DJ]", "EnableRandomQueue")).toInt())) == 1); + ConfigKey("[Auto DJ]", "EnableRandomQueue")) + .toInt())) == 1); int tracksToAdd = minAutoDJCrateTracks - m_pAutoDJTableModel->rowCount(); if (randomQueueEnabled && (tracksToAdd > 0)) { @@ -1182,6 +1215,72 @@ double AutoDJProcessor::getOutroEndSecond(DeckAttributes* pDeck) { return framePositionToSeconds(outroEndPosition, pDeck); } +double AutoDJProcessor::getMainCueSecond(DeckAttributes* pDeck) { + TrackPointer pTrack = pDeck->getLoadedTrack(); + if (!pTrack) { + return 0.0; + } + + const mixxx::audio::FramePos mainCue = pTrack->getMainCuePosition(); + if (mainCue.isValid()) { + const mixxx::audio::FramePos trackEndPosition = pDeck->trackEndPosition(); + if (mainCue <= trackEndPosition) { + return framePositionToSeconds(mainCue, pDeck); + } else { + qWarning() << "Main Cue starts after track end in:" + << pTrack->getLocation() + << "Using the first sample instead."; + } + } + return 0.0; +} + +double AutoDJProcessor::getFadeInSecond(DeckAttributes* pDeck) { + TrackPointer pTrack = pDeck->getLoadedTrack(); + if (!pTrack) { + return 0.0; + } + + CuePointer pFromTrackFadeIn = pTrack->findCueByType(mixxx::CueType::FadeIn); + if (pFromTrackFadeIn) { + const mixxx::audio::FramePos fadeInEnd = pFromTrackFadeIn->getEndPosition(); + if (fadeInEnd.isValid()) { + const mixxx::audio::FramePos trackEndPosition = pDeck->trackEndPosition(); + if (fadeInEnd <= trackEndPosition) { + return framePositionToSeconds(fadeInEnd, pDeck); + } else { + qWarning() << "Fade Sound Cue starts after track end in:" + << pTrack->getLocation() + << "Using the first sample instead."; + } + } + } + return 0.0; +} + +double AutoDJProcessor::getFadeOutSecond(DeckAttributes* pDeck) { + TrackPointer pTrack = pDeck->getLoadedTrack(); + if (!pTrack) { + return 0.0; + } + + const mixxx::audio::FramePos trackEndPosition = pDeck->trackEndPosition(); + CuePointer pFromTrackFadeOut = pTrack->findCueByType(mixxx::CueType::FadeOut); + if (pFromTrackFadeOut && pFromTrackFadeOut->getLengthFrames() > 0.0) { + const mixxx::audio::FramePos fadeOutStart = pFromTrackFadeOut->getPosition(); + if (fadeOutStart > mixxx::audio::FramePos(0.0)) { + if (fadeOutStart <= trackEndPosition) { + return framePositionToSeconds(fadeOutStart, pDeck); + } else { + qWarning() << "Fade Sound Cue ends after track end in:" + << pTrack->getLocation() + << "Using the last sample instead."; + } + } + } + return framePositionToSeconds(trackEndPosition, pDeck); +} + double AutoDJProcessor::getFirstSoundSecond(DeckAttributes* pDeck) { TrackPointer pTrack = pDeck->getLoadedTrack(); if (!pTrack) { @@ -1268,10 +1367,14 @@ void AutoDJProcessor::calculateTransition(DeckAttributes* pFromDeck, const double fromDeckEndPosition = getEndSecond(pFromDeck); const double toDeckEndPosition = getEndSecond(pToDeck); + // Make sure playNextPos is higher than fadeBeginPos by default + pFromDeck->playNextPos = fromDeckEndPosition; // Since the end position is measured in seconds from 0:00 it is also // the track duration. Use this alias for better readability. const double fromDeckDuration = fromDeckEndPosition; const double toDeckDuration = toDeckEndPosition; + // Make sure playNextPos is higher than fadeBeginPos by default + pToDeck->playNextPos = toDeckEndPosition; VERIFY_OR_DEBUG_ASSERT(fromDeckDuration >= kMinimumTrackDurationSec) { // Track has no duration or too short. This should not happen, because short @@ -1359,6 +1462,7 @@ void AutoDJProcessor::calculateTransition(DeckAttributes* pFromDeck, } m_crossfaderStartCenter = false; + pToDeck->fadeBeginPos = toDeckEndPosition; switch (m_transitionMode) { case TransitionMode::FullIntroOutro: { // Use the outro or intro length for the transition time, whichever is @@ -1481,6 +1585,168 @@ void AutoDJProcessor::calculateTransition(DeckAttributes* pFromDeck, getLastSoundSecond(pFromDeck), toDeckStartSecond); } break; + case TransitionMode::RadioLanewayCrossfade: { + // This transition mode does the following: + // + // (1) If a playing (from) track has reached the FadeOut cue start, + // then a crossfade starting center is initiated, with the + // transmission time taking the fadeOutLength, or value + // from the spin box (fixed value), whichever is smaller. + // This fits the majority of cases an provides a very smooth + // crossfade on tracks with a fadeout, and excellent timing + // when a track has a sharp or cold ending. + // + // (2) If the next (to) track has a fadeInLength greater than + // 0.25 seconds then the next (to) track is started a + // fadeInLength value (playNextPos) earlier than the + // current (from) deck fadeOutStart. If the fadeOutLength + + // playNextPos difference is greater than the spin box, + // then revert to item 1 above using (fixed value) calculated + // from fadeOutLength. This covers the case where we have a + // slow fade-in of a song, for example, when Boston's "More + // Than A Feeling", comes next after a song with a cold start, + // removing the perception of a gap, while preserving the + // cold ending. (In this example it is about 3 seconds + // earlier, though the new track is not really perceptible + // during a cold ending). Note that the crossfader holds + // in the center while the next (to) track starts, and + // starts moving the fader once fadeOutStart is reached. + // + // (3) If the next (to) track has a duration of less than 15 + // seconds then this track is likely a jingle or sample. + // In which case the fadeInLength is 0 and the fadeOutLength + // is 1. This way we have a fast neat transition while + // maximizing the time for the next track to load. + + pToDeck->fadeBeginPos = toDeckEndPosition; + m_crossfaderStartCenter = true; + // Make sure we start at the center of the crossfader so the track starts at full volume + + // some safety in case user changes transition mode. + pToDeck->fadeEndPos = getLastSoundSecond(pToDeck); + pToDeck->fadeBeginPos = getFadeOutSecond(pToDeck); + if (pToDeck->fadeBeginPos == 0.0) { + pToDeck->fadeBeginPos = pToDeck->fadeEndPos; + } + + // Calculate fade in values + double fadeInStart = getMainCueSecond(pToDeck); + // Fix spurious values from incomplete data from early calls to calculateTransition + // where the track length/ratio has not been established correctly. + if (fadeInStart > (fromDeckEndPosition / 1.2)) { + // if we are most of the way to the end then something is broken. + fadeInStart = getFirstSoundSecond(pToDeck); + } + double fadeInEnd = getFadeInSecond(pToDeck); + if (fadeInStart > fadeInEnd) { + fadeInEnd = fadeInStart; + } + + toDeckStartSeconds = toDeckPositionSeconds; // Where the user might have positioned the cue + if (seekToStartPoint || toDeckPositionSeconds >= pToDeck->fadeBeginPos) { + // toDeckPosition >= pToDeck->fadeBeginPos happens when the + // user has sought or played the pToDeck track past the fadeBeginPos. + // In this case we re-cue the new track to the main cue position. + toDeckStartSeconds = getMainCueSecond(pToDeck); + if (toDeckPositionSeconds >= pToDeck->fadeBeginPos) { + // If this is still too far (was it just changed?) + // make the start at the first sound. + toDeckStartSeconds = fadeInStart; + } + } + + // If our fadeInEnd is near the end of the track.. then... + // we have a pathologically long fade in... + // it might be the track... it might be the db... + // ... but this happens on most calculateTransition save the last: + // There are multiple calls to calculateTransition it seems that fadeInEnd + // collects spurious data about the fade in position + // until the lat call (is pDeck->rateRatio() correct all the time?). + // Placing this here seems to prevent anything weird happening for a short + // next (to) tracks, except that we have no roll in period, of course. + if (fadeInEnd > (fromDeckEndPosition / 1.2)) { + fadeInEnd = fadeInStart; + } + + // If we are still early (it might be a very short current track) then + // the FadeInLength is likely to end up as 0 which on a short track + // is probably OK in any event. + double fadeInLength = fadeInEnd - toDeckStartSeconds; + if (fadeInLength < 0.0) { + fadeInLength = 0.0; + } + + // Calculate fade out values + double fadeOutStart = getFadeOutSecond(pFromDeck); + double fadeOutEnd = getLastSoundSecond(pFromDeck); + if (fadeOutStart == 0) { + fadeOutStart = fadeOutEnd; + } + double fadeOutLength = fadeOutEnd - fadeOutStart; + + // getMainCueSecond(pToDeck) is also returning + // spurious values until the last calculateTransition call. + if (toDeckStartSeconds > pToDeck->fadeBeginPos) { + toDeckStartSeconds = getFirstSoundSecond(pToDeck); + } + + // Note as we get here pFromDeck->playNextPos == fromDeckEndPosition. + // We use this default to make sure that playNextPos is always greater than + // pFromDeck->fadeBeginPos unless we really intend to start early. + + // If we have a long(ish) fade in then let's start the next track early. + if ((fadeInLength > 0) && ((fadeOutStart - fadeInLength) > 0.0)) { + pFromDeck->playNextPos = fadeOutStart - fadeInLength; + } + if (fromDeckPosition > fadeOutStart) { + // We have already passed fadeOutStart + // This can happen if we have just enabled auto DJ + fadeOutStart = fromDeckPosition; + // and make sure we don't start early twice because + // we need do it with fadeOutStart only now. + pFromDeck->playNextPos = fromDeckEndPosition; + } + pToDeck->startPos = toDeckStartSeconds; + pFromDeck->fadeBeginPos = fadeOutStart; + + if (fadeOutLength <= m_transitionTime) { + if ((toDeckDuration < fadeOutLength) || + (toDeckDuration < 15)) { + // make sure that the transition time is less than the next track duration. + // and make sure that tracks under 15 seconds will be preceded by very + // short transition. + if (toDeckDuration > 1.0) { + // if the next track is very short (say a few seconds long jingle or sweeper) + // make is a very fast fade (so we can get on with loading the next track). + if ((pFromDeck->playNextPos < (fadeOutStart - (toDeckDuration - 1))) && + (!pToDeck->isPlaying())) { + pFromDeck->playNextPos = (fadeOutStart - (toDeckDuration - 1)); + } + pFromDeck->fadeEndPos = fadeOutStart + 1; + } else { + // Just in case it is a pathologically short track - + // that really shouldn't be here by now anyway. + pFromDeck->playNextPos = fromDeckEndPosition; + pFromDeck->fadeEndPos = fadeOutStart + kMinimumTrackDurationSec; + } + } else { + // This is the general case where we want to do a fadeout when + // after (sound level last goes below) Cue:FadOut start + // and/or we start early until (the sound level is above) + // Cue:FadeIn end. + pFromDeck->fadeEndPos = fadeOutEnd; + } + } else { + // if the fade out is longer than the max the user specified, + // just do a fixed fade out and don't start early. + pFromDeck->playNextPos = fromDeckEndPosition; + useFixedFadeTime(pFromDeck, + pToDeck, + fadeOutStart, + fadeOutStart + m_transitionTime, + toDeckStartSeconds); + } + } break; case TransitionMode::FixedFullTrack: default: { double startPoint; @@ -1495,15 +1761,18 @@ void AutoDJProcessor::calculateTransition(DeckAttributes* pFromDeck, startPoint = toDeckPositionSeconds; } useFixedFadeTime(pFromDeck, pToDeck, fromDeckPosition, fromDeckEndPosition, startPoint); - } + } } // These are expected to be a fraction of the track length. pFromDeck->fadeBeginPos /= fromDeckDuration; pFromDeck->fadeEndPos /= fromDeckDuration; + pFromDeck->playNextPos /= fromDeckEndPosition; + pToDeck->startPos /= toDeckDuration; pToDeck->fadeBeginPos /= toDeckDuration; pToDeck->fadeEndPos /= toDeckDuration; + pToDeck->playNextPos /= toDeckDuration; pFromDeck->isFromDeck = true; pToDeck->isFromDeck = false; @@ -1626,7 +1895,8 @@ void AutoDJProcessor::playerTrackLoaded(DeckAttributes* pDeck, TrackPointer pTra } void AutoDJProcessor::playerLoadingTrack(DeckAttributes* pDeck, - TrackPointer pNewTrack, TrackPointer pOldTrack) { + TrackPointer pNewTrack, + TrackPointer pOldTrack) { if constexpr (sDebug) { qDebug() << this << "playerLoadingTrack" << pDeck->group << "new:" << (pNewTrack ? pNewTrack->getLocation() : "(null)") @@ -1712,7 +1982,7 @@ void AutoDJProcessor::setTransitionTime(int time) { // Update the transition time first. m_pConfig->set(ConfigKey(kConfigKey, kTransitionPreferenceName), - ConfigValue(time)); + ConfigValue(time)); m_transitionTime = time; // Then re-calculate fade thresholds for the decks. diff --git a/src/library/autodj/autodjprocessor.h b/src/library/autodj/autodjprocessor.h index 654d77429f7..0acf5e5115b 100644 --- a/src/library/autodj/autodjprocessor.h +++ b/src/library/autodj/autodjprocessor.h @@ -118,6 +118,7 @@ class DeckAttributes : public QObject { int index; QString group; double startPos; // Set in toDeck nature + double playNextPos; // Needed for Radio Laneway Crossfade double fadeBeginPos; // set in fromDeck nature double fadeEndPos; // set in fromDeck nature bool isFromDeck; @@ -164,7 +165,8 @@ class AutoDJProcessor : public QObject { FadeAtOutroStart, FixedFullTrack, FixedSkipSilence, - FixedStartCenterSkipSilence + FixedStartCenterSkipSilence, + RadioLanewayCrossfade }; AutoDJProcessor(QObject* pParent, @@ -254,6 +256,9 @@ class AutoDJProcessor : public QObject { double getFirstSoundSecond(DeckAttributes* pDeck); double getLastSoundSecond(DeckAttributes* pDeck); double getEndSecond(DeckAttributes* pDeck); + double getMainCueSecond(DeckAttributes* pDeck); + double getFadeInSecond(DeckAttributes* pDeck); + double getFadeOutSecond(DeckAttributes* pDeck); double framePositionToSeconds(mixxx::audio::FramePos position, DeckAttributes* pDeck); TrackPointer getNextTrackFromQueue(); @@ -284,6 +289,7 @@ class AutoDJProcessor : public QObject { PlaylistTableModel* m_pAutoDJTableModel; AutoDJState m_eState; + bool m_isNowStartingEarly; double m_transitionProgress; double m_transitionTime; // the desired value set by the user TransitionMode m_transitionMode; diff --git a/src/library/autodj/dlgautodj.cpp b/src/library/autodj/dlgautodj.cpp index 52e02e07b73..9c115986128 100644 --- a/src/library/autodj/dlgautodj.cpp +++ b/src/library/autodj/dlgautodj.cpp @@ -70,8 +70,9 @@ DlgAutoDJ::DlgAutoDJ(WLibrary* parent, &WTrackTableView::setSelectedClick); QBoxLayout* box = qobject_cast(layout()); - VERIFY_OR_DEBUG_ASSERT(box) { //Assumes the form layout is a QVBox/QHBoxLayout! - } else { + VERIFY_OR_DEBUG_ASSERT(box) { // Assumes the form layout is a QVBox/QHBoxLayout! + } + else { box->removeWidget(m_pTrackTablePlaceholder); m_pTrackTablePlaceholder->hide(); box->insertWidget(1, m_pTrackTableView); @@ -82,7 +83,7 @@ DlgAutoDJ::DlgAutoDJ(WLibrary* parent, m_pTrackTableView->loadTrackModel(m_pAutoDJTableModel); // Do not set this because it disables auto-scrolling - //m_pTrackTableView->setDragDropMode(QAbstractItemView::InternalMove); + // m_pTrackTableView->setDragDropMode(QAbstractItemView::InternalMove); connect(pushButtonAutoDJ, &QPushButton::clicked, @@ -150,7 +151,13 @@ DlgAutoDJ::DlgAutoDJ(WLibrary* parent, "\n" "Skip Silence Start Full Volume:\n" "The same as Skip Silence, but starting transitions with a centered\n" - "crossfader, so that the intro starts at full volume.\n"); + "crossfader, so that the intro starts at full volume.\n" + "\n" + "Radio Laneway Crossfade:\n" + "Starts the next track at full volume. Starts the crossfade when the\n" + "volume last falls below -12Db or at the spin box setting which ever\n" + "is lower, and potentially starts the next earlier if it starts below\n" + "-27Db."); pushButtonFadeNow->setToolTip(fadeBtnTooltip); pushButtonSkipNext->setToolTip(skipBtnTooltip); @@ -185,6 +192,8 @@ DlgAutoDJ::DlgAutoDJ(WLibrary* parent, static_cast(AutoDJProcessor::TransitionMode::FixedSkipSilence)); fadeModeCombobox->addItem(tr("Skip Silence Start Full Volume"), static_cast(AutoDJProcessor::TransitionMode::FixedStartCenterSkipSilence)); + fadeModeCombobox->addItem(tr("Radio Laneway Crossfade"), + static_cast(AutoDJProcessor::TransitionMode::RadioLanewayCrossfade)); fadeModeCombobox->setCurrentIndex( fadeModeCombobox->findData(static_cast(m_pAutoDJProcessor->getTransitionMode()))); connect(fadeModeCombobox, diff --git a/src/track/cueinfo.cpp b/src/track/cueinfo.cpp index 20d7ea11a0b..6f0ec79997a 100644 --- a/src/track/cueinfo.cpp +++ b/src/track/cueinfo.cpp @@ -23,6 +23,8 @@ void assertEndPosition( case CueType::Intro: case CueType::Outro: case CueType::Invalid: + case CueType::FadeIn: + case CueType::FadeOut: break; case CueType::Beat: // unused default: @@ -148,6 +150,12 @@ QDebug operator<<(QDebug debug, const CueType& cueType) { case CueType::N60dBSound: debug << "CueType::N60dBSound"; break; + case CueType::FadeIn: + debug << "CueType::FadeIn"; + break; + case CueType::FadeOut: + debug << "CueType::FadeOut"; + break; } return debug; } diff --git a/src/track/cueinfo.h b/src/track/cueinfo.h index c73eadcc1d0..b31f0d59f90 100644 --- a/src/track/cueinfo.h +++ b/src/track/cueinfo.h @@ -19,6 +19,14 @@ enum class CueType { Outro = 7, N60dBSound = 8, // range that covers beginning and end of audible // sound; not shown to user + FadeIn = 9, // Range from start of track to the first sample in + // the track where the sound volume was above the + // kFadeInThreshold (cf analyzersilence.cpp); + // not shown to user + FadeOut = 10 // Range from the last sample in the track where + // the sound volume was above the kFadeOutThreshold + // to the end of the track (cf analyzersilence.cpp); + // not shown to user }; enum class CueFlag { diff --git a/src/widget/wtrackmenu.cpp b/src/widget/wtrackmenu.cpp index ef1b52b35fc..4998a43fe3c 100644 --- a/src/widget/wtrackmenu.cpp +++ b/src/widget/wtrackmenu.cpp @@ -2156,6 +2156,10 @@ class ResetWaveformTrackPointerOperation : public mixxx::TrackPointerOperation { // same reasons that apply for reanalyze of the waveforms applies also // for the AudibleSound cue. pTrack->removeCuesOfType(mixxx::CueType::N60dBSound); + // FIX/TODO: + // Assumed this Should also apply? + pTrack->removeCuesOfType(mixxx::CueType::FadeIn); + pTrack->removeCuesOfType(mixxx::CueType::FadeOut); } AnalysisDao& m_analysisDao;