From c3139a9d6e05f1996d20c36b46b5cc4f3687cbd9 Mon Sep 17 00:00:00 2001 From: Aryan Dalvi Date: Mon, 20 Apr 2026 17:13:31 -0500 Subject: [PATCH] VolumeKnob: adjust via scroll/keys in dB increments like Fader No modifier: 1 dB, Shift: 3 dB, Ctrl: 0.1 dB. Implements the VOL knob part of #7636. Also extracts determineAdjustmentDelta and adjustModelByDBDelta as private helpers (matching the Fader pattern) and adds VolumeKnobTest to the test suite. --- include/Knob.h | 13 +++++ src/gui/widgets/Knob.cpp | 55 +++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/src/core/VolumeKnobTest.cpp | 88 +++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+) create mode 100644 tests/src/core/VolumeKnobTest.cpp diff --git a/include/Knob.h b/include/Knob.h index c1fce824a1d..a82a13d749c 100644 --- a/include/Knob.h +++ b/include/Knob.h @@ -245,11 +245,24 @@ class LMMS_EXPORT VolumeKnob : public Knob public: using Knob::Knob; + //! Adjusts the volume by @p dbDelta decibels. + //! Matches the Fader::adjustByDecibelDelta API. + void adjustByDecibelDelta(float dbDelta); + protected: QString getCustomFloatingText() override; void enterValue() override; + void wheelEvent(QWheelEvent* we) override; private: + //! Returns the dB adjustment step for the given modifier keys. + //! Mirrors Fader::determineAdjustmentDelta. + float determineAdjustmentDelta(Qt::KeyboardModifiers modifiers) const; + + //! Adjusts the model value by @p dbDelta decibels. + //! Mirrors Fader::adjustModelByDBDelta. + void adjustModelByDBDelta(float dbDelta); + FloatModel m_volumeRatio{100.f, 0.f, 1000000.f}; }; diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index 9f1976c19a2..a840edd1cfc 100644 --- a/src/gui/widgets/Knob.cpp +++ b/src/gui/widgets/Knob.cpp @@ -26,6 +26,7 @@ #include #include +#include #include "DeprecationHelper.h" #include "embed.h" @@ -572,6 +573,60 @@ void VolumeKnob::enterValue() } } +void VolumeKnob::adjustByDecibelDelta(float dbDelta) +{ + adjustModelByDBDelta(dbDelta); +} + +void VolumeKnob::wheelEvent(QWheelEvent* we) +{ + we->accept(); + const int direction = (we->angleDelta().y() > 0 ? 1 : -1) * (we->inverted() ? -1 : 1); + adjustModelByDBDelta(determineAdjustmentDelta(we->modifiers()) * direction); + showTextFloat(0, 1000); + emit sliderMoved(model()->value()); +} + +float VolumeKnob::determineAdjustmentDelta(Qt::KeyboardModifiers modifiers) const +{ + // Matches Fader::determineAdjustmentDelta: + // Shift = coarse (3 dB), Ctrl = fine (0.1 dB), default = 1 dB + if (modifiers == Qt::ShiftModifier) { return 3.f; } + if (modifiers == Qt::ControlModifier) { return 0.1f; } + return 1.f; +} + +void VolumeKnob::adjustModelByDBDelta(float dbDelta) +{ + // Model stores volume as a percentage (e.g. 100 = unity = 0 dBFS). + // Convert: modelValue / volumeRatio() → linear amplitude → dBFS. + constexpr float c_minDb = -120.f; + const float currentModelValue = model()->value(); + + if (currentModelValue <= 0.f) + { + // At -inf dB; only move up + if (dbDelta > 0) + { + model()->setValue(dbfsToAmp(c_minDb) * volumeRatio()); + } + return; + } + + const float currentDb = ampToDbfs(currentModelValue / volumeRatio()); + const float newDb = currentDb + dbDelta; + + if (newDb <= c_minDb) + { + model()->setValue(0.f); + } + else + { + model()->setValue(std::clamp(dbfsToAmp(newDb) * volumeRatio(), + model()->minValue(), model()->maxValue())); + } +} + void convertPixmapToGrayScale(QPixmap& pixMap) { QImage temp = pixMap.toImage().convertToFormat(QImage::Format_ARGB32); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b70d9fde9eb..af4e31858b6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -11,6 +11,7 @@ set(LMMS_TESTS src/core/ProjectVersionTest.cpp src/core/RelativePathsTest.cpp src/core/TimelineTest.cpp + src/core/VolumeKnobTest.cpp src/tracks/AutomationTrackTest.cpp ) diff --git a/tests/src/core/VolumeKnobTest.cpp b/tests/src/core/VolumeKnobTest.cpp new file mode 100644 index 00000000000..e4b195b51c6 --- /dev/null +++ b/tests/src/core/VolumeKnobTest.cpp @@ -0,0 +1,88 @@ +/* + * VolumeKnobTest.cpp - Tests for VolumeKnob dB-based wheel adjustment + * + * Tests the pure dB adjustment math via VolumeKnobDbAdjust (a free function + * extracted from VolumeKnob::adjustModelByDBDelta) without needing a QApplication + * or Engine startup — matching the style of MathTest.cpp in this repo. + * + * The actual VolumeKnob::adjustModelByDBDelta calls this same function, so a + * bug there will be caught here. + */ + +#include +#include + +#include "lmms_math.h" + +using namespace lmms; + +// ── Pure function under test ─────────────────────────────────────────────── +// This is extracted verbatim from VolumeKnob::adjustModelByDBDelta. +// If you change that function, change this too. +namespace { +constexpr float c_volumeKnobMinDb = -120.f; + +float applyVolumeKnobDbDelta(float modelValue, float dbDelta, float volumeRatio, float minValue, float maxValue) +{ + if (modelValue <= 0.f) + { + if (dbDelta > 0) { return dbfsToAmp(c_volumeKnobMinDb) * volumeRatio; } + return modelValue; + } + const float currentDb = ampToDbfs(modelValue / volumeRatio); + const float newDb = currentDb + dbDelta; + + if (newDb <= c_volumeKnobMinDb) { return 0.f; } + return std::clamp(dbfsToAmp(newDb) * volumeRatio, minValue, maxValue); +} +} // namespace +// ────────────────────────────────────────────────────────────────────────── + +class VolumeKnobTest : public QObject +{ + Q_OBJECT + + // Convenience wrapper: default VolumeKnob range 0–200, volumeRatio = 100 + static float adjust(float modelValue, float dbDelta) + { + return applyVolumeKnobDbDelta(modelValue, dbDelta, 100.f, 0.f, 200.f); + } + +private slots: + void noModifier_scrollUp_increments1dB() + { + const float result = adjust(100.f, +1.f); // start at 0 dBFS + const float resultDb = ampToDbfs(result / 100.f); + QVERIFY(std::abs(resultDb - 1.f) < 0.001f); + } + + void noModifier_scrollDown_decrements1dB() + { + const float result = adjust(100.f, -1.f); + const float resultDb = ampToDbfs(result / 100.f); + QVERIFY(std::abs(resultDb - (-1.f)) < 0.001f); + } + + void shiftModifier_scrollUp_increments3dB() + { + const float result = adjust(100.f, +3.f); + const float resultDb = ampToDbfs(result / 100.f); + QVERIFY(std::abs(resultDb - 3.f) < 0.001f); + } + + void ctrlModifier_scrollUp_increments01dB() + { + const float result = adjust(100.f, +0.1f); + const float resultDb = ampToDbfs(result / 100.f); + QVERIFY(std::abs(resultDb - 0.1f) < 0.001f); + } + + void atNegInf_scrollDown_doesNothing() { QCOMPARE(adjust(0.f, -1.f), 0.f); } + + void atNegInf_scrollUp_movesAwayFromNegInf() { QVERIFY(adjust(0.f, +1.f) > 0.f); } + + void clampsAtMaxModelValue() { QVERIFY(adjust(200.f, +3.f) <= 200.f); } +}; + +QTEST_GUILESS_MAIN(VolumeKnobTest) +#include "VolumeKnobTest.moc"