diff --git a/src/engine/channels/enginechannel.cpp b/src/engine/channels/enginechannel.cpp index 161f2928115..04f3ba81a21 100644 --- a/src/engine/channels/enginechannel.cpp +++ b/src/engine/channels/enginechannel.cpp @@ -15,7 +15,8 @@ EngineChannel::EngineChannel(const ChannelHandleAndGroup& handle_group, m_pSampleRate(new ControlProxy("[Master]", "samplerate")), m_sampleBuffer(nullptr), m_bIsPrimaryDeck(isPrimaryDeck), - m_bIsTalkoverChannel(isTalkoverChannel) { + m_bIsTalkoverChannel(isTalkoverChannel), + m_channelIndex(-1) { m_pPFL = new ControlPushButton(ConfigKey(getGroup(), "pfl")); m_pPFL->setButtonMode(ControlPushButton::TOGGLE); m_pMaster = new ControlPushButton(ConfigKey(getGroup(), "master")); diff --git a/src/engine/channels/enginechannel.h b/src/engine/channels/enginechannel.h index 44faf50d536..c1ac3144049 100644 --- a/src/engine/channels/enginechannel.h +++ b/src/engine/channels/enginechannel.h @@ -49,6 +49,12 @@ class EngineChannel : public EngineObject { inline bool isPrimaryDeck() { return m_bIsPrimaryDeck; }; + int getChannelIndex() { + return m_channelIndex; + } + void setChannelIndex(int channelIndex) { + m_channelIndex = channelIndex; + } virtual void process(CSAMPLE* pOut, const int iBufferSize) = 0; virtual void collectFeatures(GroupFeatureState* pGroupFeatures) const = 0; @@ -85,4 +91,5 @@ class EngineChannel : public EngineObject { ControlPushButton* m_pOrientationCenter; ControlPushButton* m_pTalkover; bool m_bIsTalkoverChannel; + int m_channelIndex; }; diff --git a/src/engine/enginebuffer.cpp b/src/engine/enginebuffer.cpp index df81024f75b..802e69881be 100644 --- a/src/engine/enginebuffer.cpp +++ b/src/engine/enginebuffer.cpp @@ -1226,6 +1226,7 @@ void EngineBuffer::postProcess(const int iBufferSize) { double local_bpm = m_pBpmControl->updateLocalBpm(); double beat_distance = m_pBpmControl->updateBeatDistance(); m_pSyncControl->setLocalBpm(local_bpm); + m_pSyncControl->updateAudible(); SyncMode mode = m_pSyncControl->getSyncMode(); if (isMaster(mode)) { m_pEngineSync->notifyBeatDistanceChanged(m_pSyncControl, beat_distance); diff --git a/src/engine/enginebuffer.h b/src/engine/enginebuffer.h index 6185b199df7..ef7b21d921a 100644 --- a/src/engine/enginebuffer.h +++ b/src/engine/enginebuffer.h @@ -158,6 +158,10 @@ class EngineBuffer : public EngineObject { // has completed. void loadTrack(TrackPointer pTrack, bool play); + void setChannelIndex(int channelIndex) { + m_channelIndex = channelIndex; + } + public slots: void slotControlPlayRequest(double); void slotControlPlayFromStart(double); @@ -226,6 +230,8 @@ class EngineBuffer : public EngineObject { // Holds the name of the control group const QString m_group; + int m_channelIndex; + UserSettingsPointer m_pConfig; friend class CueControlTest; diff --git a/src/engine/enginemaster.cpp b/src/engine/enginemaster.cpp index 68cb0ec59dc..33cd29d5e42 100644 --- a/src/engine/enginemaster.cpp +++ b/src/engine/enginemaster.cpp @@ -792,6 +792,7 @@ void EngineMaster::processHeadphones(const CSAMPLE_GAIN masterMixGainInHeadphone void EngineMaster::addChannel(EngineChannel* pChannel) { ChannelInfo* pChannelInfo = new ChannelInfo(m_channels.size()); + pChannel->setChannelIndex(pChannelInfo->m_index); pChannelInfo->m_pChannel = pChannel; const QString& group = pChannel->getGroup(); pChannelInfo->m_handle = m_pChannelHandleFactory->getOrCreateHandle(group); @@ -836,6 +837,13 @@ EngineChannel* EngineMaster::getChannel(const QString& group) { return nullptr; } +CSAMPLE_GAIN EngineMaster::getMasterGain(int channelIndex) const { + if (channelIndex >= 0 && channelIndex < m_channelMasterGainCache.size()) { + return m_channelMasterGainCache[channelIndex].m_gain; + } + return CSAMPLE_GAIN_ZERO; +} + const CSAMPLE* EngineMaster::getDeckBuffer(unsigned int i) const { return getChannelBuffer(PlayerManager::groupForDeck(i)); } diff --git a/src/engine/enginemaster.h b/src/engine/enginemaster.h index 72f45fd17da..1f450fe8316 100644 --- a/src/engine/enginemaster.h +++ b/src/engine/enginemaster.h @@ -105,6 +105,8 @@ class EngineMaster : public QObject, public AudioSource { return m_pEngineSideChain; } + CSAMPLE_GAIN getMasterGain(int channelIndex) const; + struct ChannelInfo { ChannelInfo(int index) : m_pChannel(NULL), @@ -123,7 +125,7 @@ class EngineMaster : public QObject, public AudioSource { }; struct GainCache { - CSAMPLE m_gain; + CSAMPLE_GAIN m_gain; bool m_fadeout; }; diff --git a/src/engine/sync/enginesync.cpp b/src/engine/sync/enginesync.cpp index bf27fbd1a72..5d9045f6655 100644 --- a/src/engine/sync/enginesync.cpp +++ b/src/engine/sync/enginesync.cpp @@ -44,7 +44,7 @@ Syncable* EngineSync::pickMaster(Syncable* enabling_syncable) { } } - if (pSyncable->isPlaying()) { + if (pSyncable->isPlaying() && pSyncable->isAudible()) { if (playing_deck_count == 0) { first_playing_deck = pSyncable; } @@ -135,7 +135,7 @@ Syncable* EngineSync::findBpmMatchTarget(Syncable* requester) { // If the other deck is playing we stop looking immediately. Otherwise continue looking // for a playing deck with bpm > 0.0. - if (pOtherSyncable->isPlaying()) { + if (pOtherSyncable->isPlaying() && pOtherSyncable->isAudible()) { return pOtherSyncable; } @@ -237,10 +237,10 @@ void EngineSync::requestEnableSync(Syncable* pSyncable, bool bEnabled) { } } -void EngineSync::notifyPlaying(Syncable* pSyncable, bool playing) { - Q_UNUSED(playing); +void EngineSync::notifyPlayingAudible(Syncable* pSyncable, bool playingAudible) { if (kLogger.traceEnabled()) { - kLogger.trace() << "EngineSync::notifyPlaying" << pSyncable->getGroup() << playing; + kLogger.trace() << "EngineSync::notifyPlayingAudible" + << pSyncable->getGroup() << playingAudible; } // For now we don't care if the deck is now playing or stopping. if (!pSyncable->isSynchronized()) { @@ -248,7 +248,7 @@ void EngineSync::notifyPlaying(Syncable* pSyncable, bool playing) { } // similar to enablesync -- we pick a new master and maybe reinit. - Syncable* newMaster = pickMaster(playing ? pSyncable : nullptr); + Syncable* newMaster = pickMaster(playingAudible ? pSyncable : nullptr); if (newMaster != nullptr && newMaster != m_pMasterSyncable) { activateMaster(newMaster, false); diff --git a/src/engine/sync/enginesync.h b/src/engine/sync/enginesync.h index 86304cd73ed..b27c03bf23a 100644 --- a/src/engine/sync/enginesync.h +++ b/src/engine/sync/enginesync.h @@ -29,7 +29,7 @@ class EngineSync : public BaseSyncableListener { void requestBpmUpdate(Syncable* pSyncable, double bpm) override; void notifyInstantaneousBpmChanged(Syncable* pSyncable, double bpm) override; void notifyBeatDistanceChanged(Syncable* pSyncable, double beatDistance) override; - void notifyPlaying(Syncable* pSyncable, bool playing) override; + void notifyPlayingAudible(Syncable* pSyncable, bool playingAudible) override; void notifyScratching(Syncable* pSyncable, bool scratching) override; // Used to pick a sync target for cases where master sync mode is not sufficient. diff --git a/src/engine/sync/internalclock.h b/src/engine/sync/internalclock.h index 735bdd8536f..7b197486c2a 100644 --- a/src/engine/sync/internalclock.h +++ b/src/engine/sync/internalclock.h @@ -43,6 +43,9 @@ class InternalClock : public QObject, public Clock, public Syncable { bool isPlaying() const override { return false; } + bool isAudible() const override { + return false; + } double getBeatDistance() const override; void setMasterBeatDistance(double beatDistance) override; diff --git a/src/engine/sync/syncable.h b/src/engine/sync/syncable.h index 12d0c79ada1..eb83fc8f1b6 100644 --- a/src/engine/sync/syncable.h +++ b/src/engine/sync/syncable.h @@ -63,6 +63,7 @@ class Syncable { // Only relevant for player Syncables. virtual bool isPlaying() const = 0; + virtual bool isAudible() const = 0; // Gets the current speed of the syncable in bpm (bpm * rate slider), doesn't // include scratch or FF/REW values. @@ -127,7 +128,7 @@ class SyncableListener { virtual void notifyBeatDistanceChanged( Syncable* pSyncable, double beatDistance) = 0; - virtual void notifyPlaying(Syncable* pSyncable, bool playing) = 0; + virtual void notifyPlayingAudible(Syncable* pSyncable, bool playingAudible) = 0; virtual Syncable* getMasterSyncable() = 0; }; diff --git a/src/engine/sync/synccontrol.cpp b/src/engine/sync/synccontrol.cpp index 657e42deaa0..dc418c5500b 100644 --- a/src/engine/sync/synccontrol.cpp +++ b/src/engine/sync/synccontrol.cpp @@ -7,6 +7,7 @@ #include "engine/controls/bpmcontrol.h" #include "engine/controls/ratecontrol.h" #include "engine/enginebuffer.h" +#include "engine/enginemaster.h" #include "moc_synccontrol.cpp" #include "track/track.h" #include "util/assert.h" @@ -171,6 +172,10 @@ bool SyncControl::isPlaying() const { return m_pPlayButton->toBool(); } +bool SyncControl::isAudible() const { + return m_audible; +} + double SyncControl::adjustSyncBeatDistance(double beatDistance) const { // Similar to adjusting the target beat distance, when we report our beat // distance we need to adjust it by the master bpm adjustment factor. If @@ -353,7 +358,7 @@ void SyncControl::trackBeatsUpdated(mixxx::BeatsPointer pBeats) { } void SyncControl::slotControlPlay(double play) { - m_pEngineSync->notifyPlaying(this, play > 0.0); + m_pEngineSync->notifyPlayingAudible(this, play > 0.0 && m_audible); } void SyncControl::slotVinylControlChanged(double enabled) { @@ -438,6 +443,18 @@ void SyncControl::setLocalBpm(double local_bpm) { } } +void SyncControl::updateAudible() { + int channelIndex = m_pChannel->getChannelIndex(); + if (channelIndex >= 0) { + CSAMPLE_GAIN gain = getEngineMaster()->getMasterGain(channelIndex); + bool newAudible = gain > CSAMPLE_GAIN_ZERO; + if (m_audible != newAudible) { + m_audible = newAudible; + m_pEngineSync->notifyPlayingAudible(this, m_pPlayButton->toBool() && m_audible); + } + } +} + void SyncControl::slotRateChanged() { double bpm = m_pLocalBpm ? m_pLocalBpm->get() * m_pRateRatio->get() : 0.0; if (kLogger.traceEnabled()) { diff --git a/src/engine/sync/synccontrol.h b/src/engine/sync/synccontrol.h index 918a6dd892a..03e6e58c851 100644 --- a/src/engine/sync/synccontrol.h +++ b/src/engine/sync/synccontrol.h @@ -33,12 +33,14 @@ class SyncControl : public EngineControl, public Syncable { void notifyOnlyPlayingSyncable() override; void requestSync() override; bool isPlaying() const override; + bool isAudible() const override; double adjustSyncBeatDistance(double beatDistance) const; double getBeatDistance() const override; void updateTargetBeatDistance(); double getBaseBpm() const override; void setLocalBpm(double local_bpm); + void updateAudible(); // Must never result in a call to // SyncableListener::notifyBeatDistanceChanged or signal loops could occur. @@ -109,6 +111,7 @@ class SyncControl : public EngineControl, public Syncable { // multiplier changes and we need to recalculate the target distance. double m_unmultipliedTargetBeatDistance; ControlValueAtomic m_prevLocalBpm; + QAtomicInt m_audible; QScopedPointer m_pSyncMode; QScopedPointer m_pSyncMasterEnabled; diff --git a/src/test/enginesynctest.cpp b/src/test/enginesynctest.cpp index a1ace0478c4..5cfef5fb5e9 100644 --- a/src/test/enginesynctest.cpp +++ b/src/test/enginesynctest.cpp @@ -1087,6 +1087,7 @@ TEST_F(EngineSyncTest, SyncToNonSyncDeck) { ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0); ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0); + ProcessBuffer(); pButtonSyncEnabled2->set(1.0); pButtonSyncEnabled2->set(0.0); @@ -1168,6 +1169,7 @@ TEST_F(EngineSyncTest, MomentarySyncDependsOnPlayingStates) { ControlObject::set(ConfigKey(m_sGroup2, "rate"), getRateSliderValue(1.0)); ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0); ProcessBuffer(); + ProcessBuffer(); // Set channel 1 to be enabled momentarily. pButtonSyncEnabled1->set(1.0); @@ -1434,6 +1436,8 @@ TEST_F(EngineSyncTest, ZeroLatencyRateChangeQuant) { ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance")) ->get()); + ProcessBuffer(); + for (int i = 0; i < 50; ++i) { ProcessBuffer(); // Keep messing with the rate @@ -1484,6 +1488,8 @@ TEST_F(EngineSyncTest, ZeroLatencyRateDiffQuant) { ControlObject::getControl(ConfigKey(m_sGroup1, "beat_distance")) ->get()); + ProcessBuffer(); + for (int i = 0; i < 50; ++i) { ProcessBuffer(); // Keep messing with the rate @@ -1657,6 +1663,8 @@ TEST_F(EngineSyncTest, HalfDoubleThenPlay) { ControlObject::getControl(ConfigKey(m_sGroup1, "rate")) ->set(getRateSliderValue(1.0)); + ProcessBuffer(); + auto pButtonSyncEnabled1 = std::make_unique(m_sGroup1, "sync_enabled"); pButtonSyncEnabled1->slotSet(1.0); @@ -1689,9 +1697,13 @@ TEST_F(EngineSyncTest, HalfDoubleThenPlay) { ControlObject::getControl(ConfigKey(m_sGroup2, "local_bpm")) ->get()); + ProcessBuffer(); + ControlObject::getControl(ConfigKey(m_sGroup2, "play"))->set(1.0); ControlObject::getControl(ConfigKey(m_sGroup1, "play"))->set(1.0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(175.0, ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm")) ->get()); @@ -2186,6 +2198,8 @@ TEST_F(EngineSyncTest, QuantizeImpliesSyncPhase) { mixxx::BeatsPointer pBeats2 = BeatFactory::makeBeatGrid(m_pTrack2->getSampleRate(), 100, 0.0); m_pTrack2->trySetBeats(pBeats2); + ProcessBuffer(); + ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0); ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0); ProcessBuffer(); @@ -2535,3 +2549,39 @@ TEST_F(EngineSyncTest, BpmAdjustFactor) { ASSERT_TRUE(isFollower(m_sGroup2)); ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup)); } + +TEST_F(EngineSyncTest, ImpliciteMasterToInternalClock) { + m_pMixerDeck1->loadFakeTrack(false, 40.0); + m_pMixerDeck2->loadFakeTrack(false, 150.0); + ProcessBuffer(); + + EXPECT_DOUBLE_EQ(40.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm"))); + EXPECT_DOUBLE_EQ(150.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm"))); + + // During cue-ing volume is 0.0 + ControlObject::set(ConfigKey(m_sGroup1, "volume"), 0.0); + ControlObject::set(ConfigKey(m_sGroup1, "play"), 1.0); + ControlObject::set(ConfigKey(m_sGroup2, "play"), 1.0); + ProcessBuffer(); + + ControlObject::set(ConfigKey(m_sGroup1, "sync_enabled"), 1.0); + ControlObject::set(ConfigKey(m_sGroup2, "sync_enabled"), 1.0); + ProcessBuffer(); + + // group 2 should be synced to the first playing deck and becomes master + EXPECT_DOUBLE_EQ(75.0, ControlObject::get(ConfigKey(m_sGroup1, "bpm"))); + EXPECT_DOUBLE_EQ(150.0, ControlObject::get(ConfigKey(m_sGroup2, "bpm"))); + ASSERT_FALSE(isSoftMaster(m_sGroup1)); + ASSERT_TRUE(isSoftMaster(m_sGroup2)); + ASSERT_FALSE(isSoftMaster(m_sInternalClockGroup)); + ProcessBuffer(); + + // Drop Track + ControlObject::set(ConfigKey(m_sGroup1, "volume"), 1.0); + ProcessBuffer(); + ProcessBuffer(); + + ASSERT_FALSE(isSoftMaster(m_sGroup1)); + ASSERT_FALSE(isSoftMaster(m_sGroup2)); + ASSERT_TRUE(isSoftMaster(m_sInternalClockGroup)); +}