diff --git a/src/mixer/basetrackplayer.cpp b/src/mixer/basetrackplayer.cpp index c9f0aff6fee9..fc610b66cd20 100644 --- a/src/mixer/basetrackplayer.cpp +++ b/src/mixer/basetrackplayer.cpp @@ -192,6 +192,11 @@ BaseTrackPlayerImpl::BaseTrackPlayerImpl( m_pRateRatio = make_parented(getGroup(), "rate_ratio", this); m_pPitchAdjust = make_parented(getGroup(), "pitch_adjust", this); + + m_pUpdateReplayGainFromPregain = std::make_unique( + ConfigKey(getGroup(), "update_replaygain_from_pregain")); + m_pUpdateReplayGainFromPregain->connectValueChangeRequest(this, + &BaseTrackPlayerImpl::slotUpdateReplayGainFromPregain); } BaseTrackPlayerImpl::~BaseTrackPlayerImpl() { @@ -367,6 +372,10 @@ void BaseTrackPlayerImpl::connectLoadedTrack() { &Track::replayGainUpdated, this, &BaseTrackPlayerImpl::slotSetReplayGain); + connect(m_pLoadedTrack.get(), + &Track::replayGainAdjusted, + this, + &BaseTrackPlayerImpl::slotAdjustReplayGain); connect(m_pLoadedTrack.get(), &Track::colorUpdated, @@ -599,7 +608,7 @@ void BaseTrackPlayerImpl::slotCloneChannel(EngineChannel* pChannel) { void BaseTrackPlayerImpl::slotSetReplayGain(mixxx::ReplayGain replayGain) { // Do not change replay gain when track is playing because - // this may lead to an unexpected volume change + // this may lead to an unexpected volume change. if (m_pPlay->get() == 0.0) { setReplayGain(replayGain.getRatio()); } else { @@ -607,6 +616,22 @@ void BaseTrackPlayerImpl::slotSetReplayGain(mixxx::ReplayGain replayGain) { } } +void BaseTrackPlayerImpl::slotAdjustReplayGain(mixxx::ReplayGain replayGain) { + const double factor = m_pReplayGain->get() / replayGain.getRatio(); + const double newPregain = m_pPreGain->get() * factor; + + // There is a very slight chance that there will be a buffer call in between these sets. + // Therefore, we first adjust the control that is being lowered before the control + // that is being raised. Worst case, the volume goes down briefly before rectifying. + if (factor < 1.0) { + m_pPreGain->set(newPregain); + setReplayGain(replayGain.getRatio()); + } else { + setReplayGain(replayGain.getRatio()); + m_pPreGain->set(newPregain); + } +} + void BaseTrackPlayerImpl::slotSetTrackColor(const mixxx::RgbColor::optional_t& color) { m_pTrackColor->forceSet(trackColorToDouble(color)); } @@ -712,6 +737,21 @@ void BaseTrackPlayerImpl::slotShiftCuesMillisButton(double value, double millise slotShiftCuesMillis(milliseconds); } +void BaseTrackPlayerImpl::slotUpdateReplayGainFromPregain(double pressed) { + if (pressed <= 0) { + return; + } + if (!m_pLoadedTrack) { + return; + } + const double gain = m_pPreGain->get(); + // Gain is at unity already, ignore and return. + if (gain == 1.0) { + return; + } + m_pLoadedTrack->adjustReplayGainFromPregain(gain); +} + void BaseTrackPlayerImpl::setReplayGain(double value) { m_pReplayGain->set(value); m_replaygainPending = false; diff --git a/src/mixer/basetrackplayer.h b/src/mixer/basetrackplayer.h index 87bceec0b749..6746957cd589 100644 --- a/src/mixer/basetrackplayer.h +++ b/src/mixer/basetrackplayer.h @@ -82,6 +82,9 @@ class BaseTrackPlayerImpl : public BaseTrackPlayer { void slotTrackLoaded(TrackPointer pNewTrack, TrackPointer pOldTrack); void slotLoadFailed(TrackPointer pTrack, const QString& reason); void slotSetReplayGain(mixxx::ReplayGain replayGain); + // When the replaygain is adjusted, we modify the track pregain + // to compensate so there is no audible change in volume. + void slotAdjustReplayGain(mixxx::ReplayGain replayGain); void slotSetTrackColor(const mixxx::RgbColor::optional_t& color); void slotPlayToggled(double); @@ -97,6 +100,7 @@ class BaseTrackPlayerImpl : public BaseTrackPlayer { void slotWaveformZoomSetDefault(double pressed); void slotShiftCuesMillis(double milliseconds); void slotShiftCuesMillisButton(double value, double milliseconds); + void slotUpdateReplayGainFromPregain(double pressed); private: void setReplayGain(double value); @@ -142,6 +146,8 @@ class BaseTrackPlayerImpl : public BaseTrackPlayer { std::unique_ptr m_pShiftCuesLaterSmall; std::unique_ptr m_pShiftCues; + std::unique_ptr m_pUpdateReplayGainFromPregain; + parented_ptr m_pReplayGain; parented_ptr m_pPlay; parented_ptr m_pLowFilter; diff --git a/src/test/replaygaintest.cpp b/src/test/replaygaintest.cpp index 3696f0fd8e0c..c9407470eabb 100644 --- a/src/test/replaygaintest.cpp +++ b/src/test/replaygaintest.cpp @@ -1,9 +1,10 @@ #include -#include "track/replaygain.h" - #include +#include "test/mockedenginebackendtest.h" +#include "track/replaygain.h" + namespace { class ReplayGainTest : public testing::Test { @@ -156,4 +157,33 @@ TEST_F(ReplayGainTest, NormalizePeak) { normalizePeak(mixxx::ReplayGain::kPeakClip + mixxx::ReplayGain::kPeakClip); } +class AdjustReplayGainTest : public MockedEngineBackendTest {}; + +TEST_F(AdjustReplayGainTest, AdjustReplayGainUpdatesPregain) { + // Initialize fake track replaygain so it's not zero. + mixxx::ReplayGain replayGain; + replayGain.setRatio(1.0); + m_pTrack1->setReplayGain(replayGain); + // Load the same track in decks 1 and 2 so we can see that the pregain is adjusted on both + // decks. + m_pMixerDeck2->slotLoadTrack(m_pTrack1, false); + // Because of this artificial process we have to manually set the replaygain CO for the second + // deck. + m_pMixerDeck2->slotSetReplayGain(replayGain); + + ControlObject::getControl(ConfigKey(m_sGroup1, "pregain"))->set(1.2); + ControlObject::getControl(ConfigKey(m_sGroup1, "update_replaygain_from_pregain"))->set(1.0); + + // The pregain value is folded into the replaygain value, and all pregains for all decks that + // have the same track loaded are adjusted so that the audible volume of the track does not + // change. + EXPECT_DOUBLE_EQ(1.0, ControlObject::getControl(ConfigKey(m_sGroup1, "pregain"))->get()); + EXPECT_NEAR(0.83333333, + ControlObject::getControl(ConfigKey(m_sGroup2, "pregain"))->get(), + .005); + EXPECT_DOUBLE_EQ(1.2, ControlObject::getControl(ConfigKey(m_sGroup1, "replaygain"))->get()); + EXPECT_DOUBLE_EQ(1.2, ControlObject::getControl(ConfigKey(m_sGroup2, "replaygain"))->get()); + EXPECT_DOUBLE_EQ(1.2, m_pTrack1->getReplayGain().getRatio()); +} + } // anonymous namespace diff --git a/src/track/replaygain.h b/src/track/replaygain.h index 76c3e51fa52a..ea5360c2e552 100644 --- a/src/track/replaygain.h +++ b/src/track/replaygain.h @@ -99,7 +99,7 @@ class ReplayGain final { m_peak = normalizePeak(m_peak); } -private: + private: double m_ratio; CSAMPLE m_peak; }; @@ -119,7 +119,7 @@ QDebug operator<<(QDebug dbg, const ReplayGain& arg) { return dbg << "ratio =" << arg.getRatio() << "/" << "peak =" << arg.getPeak(); } -} +} // namespace mixxx Q_DECLARE_TYPEINFO(mixxx::ReplayGain, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(mixxx::ReplayGain) diff --git a/src/track/track.cpp b/src/track/track.cpp index 686f986ac91a..da3f4ccae869 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -328,6 +328,16 @@ void Track::setReplayGain(const mixxx::ReplayGain& replayGain) { } } +void Track::adjustReplayGainFromPregain(double gain) { + QMutexLocker lock(&m_qMutex); + mixxx::ReplayGain replayGain = m_record.getMetadata().getTrackInfo().getReplayGain(); + replayGain.setRatio(gain * replayGain.getRatio()); + if (compareAndSet(m_record.refMetadata().refTrackInfo().ptrReplayGain(), replayGain)) { + markDirtyAndUnlock(&lock); + emit replayGainAdjusted(replayGain); + } +} + mixxx::Bpm Track::getBpmWhileLocked() const { // BPM values must be synchronized at all times! DEBUG_ASSERT(m_record.getMetadata().getTrackInfo().getBpm() == getBeatsPointerBpm(m_pBeats)); diff --git a/src/track/track.h b/src/track/track.h index 8aa4c33f2b3d..83618a7ad39f 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -147,6 +147,8 @@ class Track : public QObject { // Set ReplayGain void setReplayGain(const mixxx::ReplayGain&); + // Adjust ReplayGain by multiplying the given gain amount. + void adjustReplayGainFromPregain(double); // Returns ReplayGain mixxx::ReplayGain getReplayGain() const; @@ -413,6 +415,9 @@ class Track : public QObject { void coverArtUpdated(); void beatsUpdated(); void replayGainUpdated(mixxx::ReplayGain replayGain); + // This signal indicates that ReplayGain is being adjusted, and pregains should be + // adjusted in the opposite direction to compensate (no audible change). + void replayGainAdjusted(const mixxx::ReplayGain&); void colorUpdated(const mixxx::RgbColor::optional_t& color); void cuesUpdated(); void analyzed(); diff --git a/src/widget/wtrackmenu.cpp b/src/widget/wtrackmenu.cpp index 9fb3fee4aa0b..cf7e0993a767 100644 --- a/src/widget/wtrackmenu.cpp +++ b/src/widget/wtrackmenu.cpp @@ -80,7 +80,7 @@ int WTrackMenu::getTrackCount() const { if (m_pTrackModel) { return m_trackIndexList.size(); } else { - return m_trackPointerList.size(); + return m_pTrack ? 1 : 0; } } @@ -340,6 +340,15 @@ void WTrackMenu::createActions() { &WTrackMenu::slotClearBeats); } + if (featureIsEnabled(Feature::UpdateReplayGain)) { + m_pUpdateReplayGain = + new QAction(tr("Update ReplayGain from Deck Gain"), m_pClearMetadataMenu); + connect(m_pUpdateReplayGain, + &QAction::triggered, + this, + &WTrackMenu::slotUpdateReplayGainFromPregain); + } + if (featureIsEnabled(Feature::Color)) { ColorPaletteSettings colorPaletteSettings(m_pConfig); m_pColorPickerAction = new WColorPickerAction(WColorPicker::Option::AllowNoColor, @@ -473,6 +482,10 @@ void WTrackMenu::setupActions() { addMenu(m_pClearMetadataMenu); } + if (featureIsEnabled(Feature::UpdateReplayGain)) { + addAction(m_pUpdateReplayGain); + } + addSeparator(); if (featureIsEnabled(Feature::HideUnhidePurge)) { if (m_pTrackModel->hasCapabilities(TrackModel::Capability::Hide)) { @@ -508,10 +521,8 @@ bool WTrackMenu::isAnyTrackBpmLocked() const { } } } else { - for (const auto& pTrack : m_trackPointerList) { - if (pTrack->isBpmLocked()) { - return true; - } + if (m_pTrack && m_pTrack->isBpmLocked()) { + return true; } } return false; @@ -536,13 +547,10 @@ std::optional> WTrackMenu::getCommonTrackColor() } } } else { - commonColor = m_trackPointerList.first()->getColor(); - for (const auto& pTrack : m_trackPointerList) { - if (commonColor != pTrack->getColor()) { - // Multiple, different colors - return std::nullopt; - } + if (!m_pTrack) { + return std::nullopt; } + commonColor = m_pTrack->getColor(); } return make_optional(commonColor); } @@ -605,7 +613,7 @@ CoverInfo WTrackMenu::getCoverInfoOfLastTrack() const { .toString(); return coverInfo; } else { - return m_trackPointerList.last()->getCoverInfoWithLocation(); + return m_pTrack->getCoverInfoWithLocation(); } } @@ -706,6 +714,10 @@ void WTrackMenu::updateMenus() { } } + if (featureIsEnabled(Feature::UpdateReplayGain)) { + m_pUpdateReplayGain->setEnabled(!m_deckGroup.isEmpty()); + } + if (featureIsEnabled(Feature::Color)) { m_pColorPickerAction->setColorPalette( ColorPaletteSettings(m_pConfig).getTrackColorPalette()); @@ -741,7 +753,7 @@ void WTrackMenu::updateMenus() { } void WTrackMenu::loadTrack( - const TrackPointer& pTrack) { + const TrackPointer& pTrack, const QString& deckGroup) { // This asserts that this function is only accessible when a track model is not set, // thus maintaining only the TrackPointerList in state and avoiding storing // duplicate state with TrackIdList and QModelIndexList. @@ -755,7 +767,8 @@ void WTrackMenu::loadTrack( if (!pTrack) { return; } - m_trackPointerList = TrackPointerList{pTrack}; + m_pTrack = pTrack; + m_deckGroup = deckGroup; updateMenus(); } @@ -788,9 +801,8 @@ TrackIdList WTrackMenu::getTrackIds() const { trackIds.push_back(trackId); } } else { - trackIds.reserve(m_trackPointerList.size()); - for (const auto& pTrack : m_trackPointerList) { - const auto trackId = pTrack->getId(); + if (m_pTrack) { + const auto trackId = m_pTrack->getId(); DEBUG_ASSERT(trackId.isValid()); trackIds.push_back(trackId); } @@ -812,15 +824,11 @@ QList WTrackMenu::getTrackRefs() const { } trackRefs.push_back(std::move(trackRef)); } - } else { - trackRefs.reserve(m_trackPointerList.size()); - for (const auto& pTrack : m_trackPointerList) { - DEBUG_ASSERT(pTrack); - auto trackRef = TrackRef::fromFileInfo( - pTrack->getFileInfo(), - pTrack->getId()); - trackRefs.push_back(std::move(trackRef)); - } + } else if (m_pTrack) { + auto trackRef = TrackRef::fromFileInfo( + m_pTrack->getFileInfo(), + m_pTrack->getId()); + trackRefs.push_back(std::move(trackRef)); } return trackRefs; } @@ -835,13 +843,8 @@ TrackPointer WTrackMenu::getFirstTrackPointer() const { // Skip unavailable tracks } return TrackPointer(); - } else { - if (m_trackPointerList.isEmpty()) { - return TrackPointer(); - } - DEBUG_ASSERT(m_trackPointerList.first()); - return m_trackPointerList.first(); } + return m_pTrack; } std::unique_ptr WTrackMenu::newTrackPointerIterator() const { @@ -854,13 +857,11 @@ std::unique_ptr WTrackMenu::newTrackPointerIterator return std::make_unique( m_pTrackModel, m_trackIndexList); - } else { - if (m_trackPointerList.isEmpty()) { - return nullptr; - } + } else if (m_pTrack) { return std::make_unique( - m_trackPointerList); + TrackPointerList{m_pTrack}); } + return nullptr; } int WTrackMenu::applyTrackPointerOperation( @@ -914,6 +915,22 @@ class ImportMetadataFromFileTagsTrackPointerOperation : public mixxx::TrackPoint } // anonymous namespace +void WTrackMenu::slotUpdateReplayGainFromPregain() { + VERIFY_OR_DEBUG_ASSERT(m_pTrack) { + return; + } + VERIFY_OR_DEBUG_ASSERT(!m_deckGroup.isEmpty()) { + return; + } + + const double gain = ControlObject::get(ConfigKey(m_deckGroup, "pregain")); + // Gain is at unity already, ignore and return. + if (gain == 1.0) { + return; + } + m_pTrack->adjustReplayGainFromPregain(gain); +} + void WTrackMenu::slotImportMetadataFromFileTags() { const auto progressLabelText = tr("Importing metadata of %n track(s) from file tags", "", getTrackCount()); @@ -1597,7 +1614,7 @@ void WTrackMenu::slotShowDlgTrackInfo() { if (m_pTrackModel) { m_pDlgTrackInfo->loadTrack(m_trackIndexList.at(0)); } else { - m_pDlgTrackInfo->loadTrack(m_trackPointerList.at(0)); + m_pDlgTrackInfo->loadTrack(m_pTrack); } m_pDlgTrackInfo->show(); } @@ -1621,7 +1638,7 @@ void WTrackMenu::slotShowDlgTagFetcher() { if (m_pTrackModel) { m_pDlgTagFetcher->loadTrack(m_trackIndexList.at(0)); } else { - m_pDlgTagFetcher->loadTrack(m_trackPointerList.at(0)); + m_pDlgTagFetcher->loadTrack(m_pTrack); } m_pDlgTagFetcher->show(); } @@ -1737,7 +1754,8 @@ void WTrackMenu::slotPurge() { } void WTrackMenu::clearTrackSelection() { - m_trackPointerList.clear(); + m_pTrack = nullptr; + m_deckGroup = QString(); m_trackIndexList.clear(); } diff --git a/src/widget/wtrackmenu.h b/src/widget/wtrackmenu.h index ced9c065546b..df627b32f4b8 100644 --- a/src/widget/wtrackmenu.h +++ b/src/widget/wtrackmenu.h @@ -47,6 +47,7 @@ class WTrackMenu : public QMenu { FileBrowser = 1 << 10, Properties = 1 << 11, SearchRelated = 1 << 12, + UpdateReplayGain = 1 << 13, TrackModelFeatures = Remove | HideUnhidePurge, All = AutoDJ | LoadTo | Playlist | Crate | Remove | Metadata | Reset | BPM | Color | HideUnhidePurge | FileBrowser | Properties | @@ -69,7 +70,7 @@ class WTrackMenu : public QMenu { const QModelIndexList& trackIndexList); void loadTrack( - const TrackPointer& pTrack); + const TrackPointer& pTrack, const QString& deckGroup); // WARNING: This function hides non-virtual QMenu::popup(). // This has been done on purpose to ensure menu doesn't popup without loaded track(s). @@ -106,6 +107,7 @@ class WTrackMenu : public QMenu { void slotScaleBpm(mixxx::Beats::BpmScale scale); // Info and metadata + void slotUpdateReplayGainFromPregain(); void slotShowDlgTagFetcher(); void slotImportMetadataFromFileTags(); void slotExportMetadataIntoFileTags(); @@ -186,8 +188,11 @@ class WTrackMenu : public QMenu { TrackModel* const m_pTrackModel; QModelIndexList m_trackIndexList; - // Source of track list when TrackModel is not set. - TrackPointerList m_trackPointerList; + /// Track being referenced when TrackModel is not set. + TrackPointer m_pTrack; + /// If the user right clicked on a track in a deck, this will record which + /// deck made the request. + QString m_deckGroup; const ControlProxy* m_pNumSamplers{}; const ControlProxy* m_pNumDecks{}; @@ -207,6 +212,9 @@ class WTrackMenu : public QMenu { WCoverArtMenu* m_pCoverMenu{}; parented_ptr m_pSearchRelatedMenu; + // Update ReplayGain from Track + QAction* m_pUpdateReplayGain{}; + // Reload Track Metadata Action: QAction* m_pImportMetadataFromFileAct{}; QAction* m_pImportMetadataFromMusicBrainzAct{}; diff --git a/src/widget/wtrackproperty.cpp b/src/widget/wtrackproperty.cpp index 67cea1b497b7..c4705610a387 100644 --- a/src/widget/wtrackproperty.cpp +++ b/src/widget/wtrackproperty.cpp @@ -19,7 +19,8 @@ constexpr WTrackMenu::Features kTrackMenuFeatures = WTrackMenu::Feature::BPM | WTrackMenu::Feature::Color | WTrackMenu::Feature::FileBrowser | - WTrackMenu::Feature::Properties; + WTrackMenu::Feature::Properties | + WTrackMenu::Feature::UpdateReplayGain; } // namespace WTrackProperty::WTrackProperty( @@ -91,7 +92,7 @@ void WTrackProperty::mouseMoveEvent(QMouseEvent* event) { void WTrackProperty::mouseDoubleClickEvent(QMouseEvent* event) { Q_UNUSED(event); if (m_pCurrentTrack) { - m_pTrackMenu->loadTrack(m_pCurrentTrack); + m_pTrackMenu->loadTrack(m_pCurrentTrack, m_group); m_pTrackMenu->slotShowDlgTrackInfo(); } } @@ -107,7 +108,7 @@ void WTrackProperty::dropEvent(QDropEvent* event) { void WTrackProperty::contextMenuEvent(QContextMenuEvent* event) { event->accept(); if (m_pCurrentTrack) { - m_pTrackMenu->loadTrack(m_pCurrentTrack); + m_pTrackMenu->loadTrack(m_pCurrentTrack, m_group); // Create the right-click menu m_pTrackMenu->popup(event->globalPos()); } diff --git a/src/widget/wtracktext.cpp b/src/widget/wtracktext.cpp index 3c1ddcf7a4cf..93db0d62ff9c 100644 --- a/src/widget/wtracktext.cpp +++ b/src/widget/wtracktext.cpp @@ -19,7 +19,8 @@ constexpr WTrackMenu::Features kTrackMenuFeatures = WTrackMenu::Feature::BPM | WTrackMenu::Feature::Color | WTrackMenu::Feature::FileBrowser | - WTrackMenu::Feature::Properties; + WTrackMenu::Feature::Properties | + WTrackMenu::Feature::UpdateReplayGain; } // namespace WTrackText::WTrackText(QWidget* pParent, @@ -82,7 +83,7 @@ void WTrackText::mouseMoveEvent(QMouseEvent *event) { void WTrackText::mouseDoubleClickEvent(QMouseEvent* event) { Q_UNUSED(event); if (m_pCurrentTrack) { - m_pTrackMenu->loadTrack(m_pCurrentTrack); + m_pTrackMenu->loadTrack(m_pCurrentTrack, m_group); m_pTrackMenu->slotShowDlgTrackInfo(); } } @@ -98,7 +99,7 @@ void WTrackText::dropEvent(QDropEvent *event) { void WTrackText::contextMenuEvent(QContextMenuEvent* event) { event->accept(); if (m_pCurrentTrack) { - m_pTrackMenu->loadTrack(m_pCurrentTrack); + m_pTrackMenu->loadTrack(m_pCurrentTrack, m_group); // Create the right-click menu m_pTrackMenu->popup(event->globalPos()); } diff --git a/src/widget/wtrackwidgetgroup.cpp b/src/widget/wtrackwidgetgroup.cpp index df6fc93dadc4..ebdb9adfd3a3 100644 --- a/src/widget/wtrackwidgetgroup.cpp +++ b/src/widget/wtrackwidgetgroup.cpp @@ -23,7 +23,8 @@ constexpr WTrackMenu::Features kTrackMenuFeatures = WTrackMenu::Feature::BPM | WTrackMenu::Feature::Color | WTrackMenu::Feature::FileBrowser | - WTrackMenu::Feature::Properties; + WTrackMenu::Feature::Properties | + WTrackMenu::Feature::UpdateReplayGain; } // anonymous namespace @@ -121,7 +122,7 @@ void WTrackWidgetGroup::dropEvent(QDropEvent* event) { void WTrackWidgetGroup::contextMenuEvent(QContextMenuEvent* event) { event->accept(); if (m_pCurrentTrack) { - m_pTrackMenu->loadTrack(m_pCurrentTrack); + m_pTrackMenu->loadTrack(m_pCurrentTrack, m_group); // Create the right-click menu m_pTrackMenu->popup(event->globalPos()); }