diff --git a/src/engine/channels/enginedeck.cpp b/src/engine/channels/enginedeck.cpp index a6645cd51379..7dee899a8563 100644 --- a/src/engine/channels/enginedeck.cpp +++ b/src/engine/channels/enginedeck.cpp @@ -68,6 +68,7 @@ EngineDeck::EngineDeck( m_stemGain.reserve(mixxx::kMaxSupportedStems); m_stemMute.reserve(mixxx::kMaxSupportedStems); + m_stemVuMeter.reserve(mixxx::kMaxSupportedStems); for (int stemIdx = 0; stemIdx < mixxx::kMaxSupportedStems; stemIdx++) { m_stemGain.emplace_back(std::make_unique( ConfigKey(getGroupForStem(getGroup(), stemIdx), QStringLiteral("volume")))); @@ -80,6 +81,9 @@ EngineDeck::EngineDeck( ConfigKey(getGroupForStem(getGroup(), stemIdx), QStringLiteral("mute"))); pMuteButton->setButtonMode(mixxx::control::ButtonMode::PowerWindow); m_stemMute.push_back(std::move(pMuteButton)); + + m_stemVuMeter.emplace_back(std::make_unique( + getGroupForStem(getGroup(), stemIdx), QString(), false)); } #endif } @@ -126,7 +130,8 @@ void EngineDeck::addStemHandle(const ChannelHandleAndGroup& stemHandleGroup) { void EngineDeck::processStem(CSAMPLE* pOut, const std::size_t bufferSize) { mixxx::audio::ChannelCount chCount = m_pBuffer->getChannelCount(); VERIFY_OR_DEBUG_ASSERT(m_stems.size() <= chCount && - m_stemMute.size() <= chCount && m_stemGain.size() <= chCount) { + m_stemMute.size() <= chCount && m_stemGain.size() <= chCount && + m_stemVuMeter.size() <= chCount) { return; }; mixxx::audio::SampleRate sampleRate = mixxx::audio::SampleRate::fromDouble(m_sampleRate.get()); @@ -184,6 +189,8 @@ void EngineDeck::processStem(CSAMPLE* pOut, const std::size_t bufferSize) { // gain) gain changes will yield to audio cracks. m_stemsGainCache[stemIdx] = stemGain; + m_stemVuMeter[stemIdx]->process(pOut, bufferSize); + // Put back the stem frames into the steam buffer (LRLR -> LR......LR......) SampleUtil::insertStereoToMulti( pIn, @@ -299,6 +306,11 @@ EngineChannel::ActiveState EngineDeck::updateActiveState() { } if (m_active) { m_vuMeter.reset(); +#ifdef __STEM__ + for (auto& stemVuMeter : m_stemVuMeter) { + stemVuMeter->reset(); + } +#endif m_active = false; return ActiveState::WasActive; } diff --git a/src/engine/channels/enginedeck.h b/src/engine/channels/enginedeck.h index 5121ff7f3fa6..26dae1449937 100644 --- a/src/engine/channels/enginedeck.h +++ b/src/engine/channels/enginedeck.h @@ -103,6 +103,7 @@ class EngineDeck : public EngineChannel, public AudioDestination { std::unique_ptr m_pStemCount; std::vector> m_stemGain; std::vector> m_stemMute; + std::vector> m_stemVuMeter; bool m_stemClonedState; #endif diff --git a/src/engine/enginevumeter.cpp b/src/engine/enginevumeter.cpp index 7383df3b8f7a..15e563a3c144 100644 --- a/src/engine/enginevumeter.cpp +++ b/src/engine/enginevumeter.cpp @@ -8,7 +8,7 @@ namespace { // Rate at which the vumeter is updated (using a sample rate of 44100 Hz): constexpr unsigned int kVuUpdateRate = 30; // in Hz (1/s), fits to display frame rate -constexpr int kPeakDuration = 500; // in ms +constexpr int kPeakDuration = 500; // in ms // Smoothing Factors // Must be from 0-1 the lower the factor, the more smoothing that is applied @@ -17,21 +17,27 @@ constexpr CSAMPLE kDecaySmoothing = 0.1f; //.16//.4 } // namespace -EngineVuMeter::EngineVuMeter(const QString& group, const QString& legacyGroup) +EngineVuMeter::EngineVuMeter(const QString& group, + const QString& legacyGroup, + bool createLegacyAliases) : m_vuMeter(ConfigKey(group, QStringLiteral("vu_meter"))), m_vuMeterLeft(ConfigKey(group, QStringLiteral("vu_meter_left"))), m_vuMeterRight(ConfigKey(group, QStringLiteral("vu_meter_right"))), m_peakIndicator(ConfigKey(group, QStringLiteral("peak_indicator"))), - m_peakIndicatorLeft(ConfigKey(group, QStringLiteral("peak_indicator_left"))), - m_peakIndicatorRight(ConfigKey(group, QStringLiteral("peak_indicator_right"))), + m_peakIndicatorLeft( + ConfigKey(group, QStringLiteral("peak_indicator_left"))), + m_peakIndicatorRight( + ConfigKey(group, QStringLiteral("peak_indicator_right"))), m_sampleRate(QStringLiteral("[App]"), QStringLiteral("samplerate")) { - const QString& aliasGroup = legacyGroup.isEmpty() ? group : legacyGroup; - m_vuMeter.addAlias(ConfigKey(aliasGroup, QStringLiteral("VuMeter"))); - m_vuMeterLeft.addAlias(ConfigKey(aliasGroup, QStringLiteral("VuMeterL"))); - m_vuMeterRight.addAlias(ConfigKey(aliasGroup, QStringLiteral("VuMeterR"))); - m_peakIndicator.addAlias(ConfigKey(aliasGroup, QStringLiteral("PeakIndicator"))); - m_peakIndicatorLeft.addAlias(ConfigKey(aliasGroup, QStringLiteral("PeakIndicatorL"))); - m_peakIndicatorRight.addAlias(ConfigKey(aliasGroup, QStringLiteral("PeakIndicatorR"))); + if (createLegacyAliases) { + const QString& aliasGroup = legacyGroup.isEmpty() ? group : legacyGroup; + m_vuMeter.addAlias(ConfigKey(aliasGroup, QStringLiteral("VuMeter"))); + m_vuMeterLeft.addAlias(ConfigKey(aliasGroup, QStringLiteral("VuMeterL"))); + m_vuMeterRight.addAlias(ConfigKey(aliasGroup, QStringLiteral("VuMeterR"))); + m_peakIndicator.addAlias(ConfigKey(aliasGroup, QStringLiteral("PeakIndicator"))); + m_peakIndicatorLeft.addAlias(ConfigKey(aliasGroup, QStringLiteral("PeakIndicatorL"))); + m_peakIndicatorRight.addAlias(ConfigKey(aliasGroup, QStringLiteral("PeakIndicatorR"))); + } // Initialize the calculation: reset(); } @@ -105,18 +111,17 @@ void EngineVuMeter::process(CSAMPLE* pIn, const std::size_t bufferSize) { : 0.0); } -void EngineVuMeter::doSmooth(CSAMPLE ¤tVolume, CSAMPLE newVolume) -{ +void EngineVuMeter::doSmooth(CSAMPLE& currentVolume, CSAMPLE newVolume) { if (currentVolume > newVolume) { currentVolume -= kDecaySmoothing * (currentVolume - newVolume); } else { currentVolume += kAttackSmoothing * (newVolume - currentVolume); } if (currentVolume < 0) { - currentVolume=0; + currentVolume = 0; } if (currentVolume > 1.0) { - currentVolume=1.0; + currentVolume = 1.0; } } diff --git a/src/engine/enginevumeter.h b/src/engine/enginevumeter.h index 53845ed909e8..91c6c1d32db2 100644 --- a/src/engine/enginevumeter.h +++ b/src/engine/enginevumeter.h @@ -7,14 +7,16 @@ class EngineVuMeter : public EngineObject { Q_OBJECT public: - EngineVuMeter(const QString& group, const QString& legacyGroup = QString()); + EngineVuMeter(const QString& group, + const QString& legacyGroup = QString(), + bool createLegacyAliases = true); virtual void process(CSAMPLE* pInOut, const std::size_t bufferSize); void reset(); private: - void doSmooth(CSAMPLE ¤tVolume, CSAMPLE newVolume); + void doSmooth(CSAMPLE& currentVolume, CSAMPLE newVolume); ControlObject m_vuMeter; ControlObject m_vuMeterLeft; diff --git a/src/test/stemcontrolobjecttest.cpp b/src/test/stemcontrolobjecttest.cpp index f26c43e7fbbe..490a016da007 100644 --- a/src/test/stemcontrolobjecttest.cpp +++ b/src/test/stemcontrolobjecttest.cpp @@ -95,6 +95,15 @@ class StemControlFixture : public BaseSignalPathTest, m_pStem3FXEnabled->set(0.0); m_pStem4FXEnabled->set(0.0); + m_pStem1VuMeter = std::make_unique( + getGroupForStem(m_sGroup1, 1), "vu_meter"); + m_pStem2VuMeter = std::make_unique( + getGroupForStem(m_sGroup1, 2), "vu_meter"); + m_pStem3VuMeter = std::make_unique( + getGroupForStem(m_sGroup1, 3), "vu_meter"); + m_pStem4VuMeter = std::make_unique( + getGroupForStem(m_sGroup1, 4), "vu_meter"); + m_pStemCount = std::make_unique(m_sGroup1, "stem_count"); } @@ -151,6 +160,10 @@ class StemControlFixture : public BaseSignalPathTest, std::unique_ptr m_pStem2FXEnabled; std::unique_ptr m_pStem3FXEnabled; std::unique_ptr m_pStem4FXEnabled; + std::unique_ptr m_pStem1VuMeter; + std::unique_ptr m_pStem2VuMeter; + std::unique_ptr m_pStem3VuMeter; + std::unique_ptr m_pStem4VuMeter; std::unique_ptr m_pStemCount; }; @@ -329,6 +342,40 @@ TEST_P(StemControlFixture, Mute) { QStringLiteral("StemMuteControlFull")); } +TEST_P(StemControlFixture, VuMeter) { + m_pChannel1->getEngineBuffer()->queueNewPlaypos( + mixxx::audio::FramePos{0}, EngineBuffer::SEEK_STANDARD); + m_pPlay->set(1.0); + + // Initial check: silence + EXPECT_EQ(m_pStem1VuMeter->get(), 0.0); + + // Process buffer to play sound + // Run enough cycles to trigger VU meter update (30Hz update rate vs ~44kHz/buffer) + for (int i = 0; i < 50; ++i) { + m_pEngineMixer->process(kProcessBufferSize); + } + + // Check if VU meters picked up the signal + EXPECT_GT(m_pStem1VuMeter->get(), 0.0); + EXPECT_GT(m_pStem2VuMeter->get(), 0.0); + + // Mute Stem 1 + m_pStem1Mute->set(1.0); + + // Process enough buffers to allow VU meter to decay to 0 + // Decay is exponential, so it takes time. + for (int i = 0; i < 600; ++i) { + m_pEngineMixer->process(kProcessBufferSize); + } + + // VU Meter should be near zero (allow small epsilon for imperfect decay) + EXPECT_NEAR(m_pStem1VuMeter->get(), 0.0, 0.001); + + // Stem 2 should still be playing + EXPECT_GT(m_pStem2VuMeter->get(), 0.0); +} + INSTANTIATE_TEST_SUITE_P( StemControlTest, StemControlFixture,