From 3c3441bb0ca11b00af2d06e011c229caea3077c4 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 1 Mar 2025 20:08:04 +0100 Subject: [PATCH] Faders with a dB scale (instead of linear/percentage) (#7636) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use dbFS scale for the faders Make the faders use a linear dbFS scale. They now also change position on first click and can then be dragged. The `Fader` class now has a new property called `m_faderMinDb`. It's the minimum value before the amplification drops down fully to 0. It is needed because we cannot represent a scale from -inf to maxDB. Rename `knobPosY` to `calculateKnobPosYFromModel` and move the implementation into the cpp file. The method now first converts the model's current amplification value to dbFS and then computes a ratio based on that values and the minimum and maximum dbFS values. Add the method `setVolumeByLocalPixelValue` which takes a local y coordinate of the widget and sets the amplification of the model based on a dbFS scale. Adjust `mousePressEvent` and `mouseMoveEvent` so that they mostly take the local y coordinate of the event and use that to adjust the model via `setVolumeByLocalPixelValue`. Remove `m_moveStartPoint` and `m_startValue` and they are not needed anymore. * Apply curve to faders Apply a curve, i.e. the cube function and its inverse, to the fader positions to that we have more space to work with in the "interesting" areas around 0 dB and less space in the area where we tend to "-inf dB". Set the minimum dB value of the fader to -120 dB to increase the potential headroom. * Support for dB models Add support for models that are in dB. There's a new member `m_modelIsLinear` which can be set via the constructor. The default value in the constructor is `true`. Add the method `modelIsLinear` which can be used to query the parameter. It is used in `calculateKnobPosYFromModel` and `setVolumeByLocalPixelValue`. Both methods got extended to deal with models in dB. They were also refactored to extract code that is common for both cases. Ensure that the constructor of `Fader` is called with `false` for `CrossoverEQControlDialog` and `EqFader` because these use models that are in dB. * Show current dB value of fader in tool tip Show the current value of the fader in its tool tip. Please note that this value is always shown in dB because the goal should be to get rid of the linear values for the faders. Some of the code is extracted into the new method `Fader::getModelValueAsDbString` which is shared by `Fader::modelValueChanged` (also new) and `Fader::updateTextFloat`. Please note that `getModelValueAsDbString` will use "dB" instead of "dBFS" as the unit and that it will format "-inf dB" instead of "-∞ dB". These changes will also be visible in the text float that is used to show the new values as the fader is being dragged. * Let users enter values in dB Let users enter values in dB in the dialog that opens with a double click. The minimum value that can be entered is the minimum value that the fader allows to set, i.e. -120 dB. The maximum value is the maximum value of the model converted to dB. As of now this is ~6 dB. The current value is converted to dB. If it corresponds to "-inf dB", i.e. if the amplification is 0 then the minimum value is used. * Remove option "Display volume as dBFS" Remove the option "Display volume as dBFS" from the settings dialog and the check box option "Volume as dBFS" from the "View" menu. Volumes are now always treated as dB, i.e. all evaluations of the property "displaydbfs" have been removed which results in assuming that this option is always true. The upgrade code in `ConfigManager::upgrade_1_1_91` was left as is. However, a note was added which informs the reader that the value of "displaydbfs" is not evaluated anymore. * Extend Fader::wheelEvent Extend `Fader::wheelEvent` for the case where the model is not a dB model (`modelIsLinear() == true`), e.g. for the mixer faders. In that case it works as follows. Each step increments or decrements by 1 dB if no modifier is pressed. With "Shift" pressed the increment value is 3 dB. With "STRG" ("CTRL") pressed the increment value is reduced to 0.1 dB. If the value goes below the minimum positive dB value that is allowed by the fader then the fader is set to "-inf dB", i.e. zero amplification. If the fader is set to "-inf dB" and the users want to increase the value then it is first set to the minimum positive value that is allowed by the fader. If the model is a dB model then the same behavior as before is used. Although it should be considered to adjust this case as well. These models are used by the faders of the Crossover Equalizer, Equalizer, Compressor and Delay and they are not very usable with the mouse wheel. * Adjust the wheel behavior for faders with dB models Make the faders of the Crossover Equalizer, Equalizer, Compressor and Delay behave like the mixer faders, i.e. step in sizes of 3 dB, 1dB and 0.1 dB depending on whether a modifier key is pressed or not. Extract some common code to do so and add some `const` keywords. * Less "jumpy" knobs Implement a more stable knob behavior. Remove the jumping behavior if the users directly click on a volume knob. By storing the offset from the knob center and taking it into account during the changes it now also feels like the users are dragging the knob. Changes to the amplification are now only applied when the mouse is moved. This makes the double click behavior much more stable, i.e. if users click on the knob when it is at 0 dB the dialog will also show 0 dB and not something like 0.3 dB because the first click is already registered as a change of volume. If the users click next to the knob the amplification will still be changed immediately to that value. ## Technical details To make the knobs more stable a variable called `m_knobCenterOffset` was introduced. It stores the offset of the click from the knob center so that this value can be taken into account for in the method `setVolumeByLocalPixelValue`. * Make MSVC happy Add an indication that a float value is assigned to a float variable. * Introduce constexpr for scaling exponent Introduce the `constexpr c_dBScalingExponent` which describes the scaling exponent that's used to scale the dB scale in both directions. This will simplify potential adjustments by keeping the values consistent everywhere. * Draw fader ticks Draw fader ticks in case the model is a linear one. This means that for now they should only be painted for the mixer faders but not for the faders of the Compressor, Delay, etc. Extract the computation of the scaled ratio between the maximum model dB value and the minimum supported fader dB value into the new private method `computeScaledRatio`. This is necessary because it is needed to paint the fader knob at the correct position (using the knob bottom as the reference) and to paint the fader ticks at the correct position (using the knob center). Introduce the private method `paintFaderTicks` which paints the fader ticks. Note: to paint some non-evenly spaced fader ticks replace the `for` expression in `paintFaderTicks` with something like the following: ``` for (auto & i : {6.f, 0.f, -6.f, -12.f, -24.f, -36.f, -48.f, -60.f, -72.f, -84.f, -96.f, -108.f, -120.f}) ``` * Fader adjustments via keyboard Allow the adjustment of the faders via the keyboard. Using the up or plus key will increment the fader value whereas the down or minus key will decrement it. The same key modifiers as for the wheel event apply: * No modifier: adjust by 1 dB * Shift: adjust by 3 dB * Control: adjust by 0.1 dB Due to the very similar behavior of the mouse wheel and key press handling some common functionality was factored out: * Determinination of the (absolute) adjustment delta value by insprecting the modifier keys of an event. Factored into `determineAdjustmentDelta`. * Adjustment of the model by a given dB delta value. Factored into `adjustModelByDBDelta`. * Move the fader of the selected channel Move the fader of the selected channel instead of the fader that has focus when the up/plus or down/minus keys are pressed. Doing so also feels more natural because users can already change the selected channel via the left and right keys and now they can immediately adjust the volume of the currently selected channel while doing so. Key events are now handled in `MixerView::keyPressEvent` instead of `Fader::keyPressEvent` and the latter is removed. `MixerChannelView` now has a method called `fader` which provides the associated fader. This is needed so that the event handler of `MixerView` can act upon the currently selected fader. ## Changes in Fader The `Fader` class provides two new public methods. The `adjust` method takes the modifier key(s) and the adjustment direction and then decides internally how the modifier keys are mapped to increment values. This is done to keep the mapping between modifier keys and increment values consistent across different clients, e.g. the key event of the `MixerView` and the wheel event of the `Fader` itself. The direction is provided by the client because the means to determine the direction can differ between clients and cases, e.g. a wheel event determines the direction differently than a key event does. The method `adjustByDecibelDelta` simply adjusts the fader by the given delta amount. It currently is not really used in a public way but it still makes sense to provide this functionality in case a parent class or client wants to manipulate the faders by its very own logic. Because the `Fader` class does not react itself to key press events anymore the call to `setFocusPolicy` is removed again. * Enter fader value when space key pressed Let the users enter the fader value via dialog when the space key is pressed. Extract the dialog logic into the new method `adjustByDialog` and call it from `MixerView::keyPressEvent`. Make `Fader::mouseDoubleClickEvent` delegate to `adjustByDialog` and also fix the behavior by accepting the event. * More prominent fader ticks around 0 dB Make the fader ticks around 0 dB more prominent but drawing them slightly wider and with a more opaque color. * Work around a Qt bug Work around a Qt bug in conjunction with the scroll wheel and the Alt key. Simply return 0 for the fader delta as soon as Alt is pressed. * Fix wheel events without any modifier Fix the handling of wheel events without any modifier key being pressed. Commit ff435d551b5 accidentally tested against Alt using a logical OR instead of an AND. * Code review changes First set of code review changes: * Use Doxygen style documentation comments * Remove comment about `displaydbfs` from upgrade routine * White-space changes for touched lines. * Make minimum dB value a constexpr Make the minimum dB value a constexpr in the implementation file because currently it's an implementation detail that should not be of any interest to any other client. So `m_faderMinDb` becomes `c_faderMinDb`. * More flexible painting of fader ticks Paint the fader ticks in a more systematic and flexible way. This also removes some "magic numbers", e.g. by using `c_faderMinDb` instead of `-120.f` as the lower limit. The upper limit, i.e. the "starting point" is now also computed using the maximum value of the model so that the fader will still paint correctly if it ever changes. * Make the zero indicator bolder Make the zero indicator tick of the fader bolder. * Make rendering of fader ticks a preference Make rendering of fader ticks a preference which is off by default. Introduce the new option "Show fader ticks" to the setup dialog and save it to the UI attribute `showfaderticks`. The configuration value is currently evaluated in `Fader::paintEvent`. If this leads to performance problems it might be better to introduce a boolean member to the `Fader` class which caches that value. * Move constexprs to anonymous namespace --- include/Fader.h | 55 ++- include/MixerChannelView.h | 2 + include/SetupDialog.h | 4 +- .../CrossoverEQ/CrossoverEQControlDialog.cpp | 8 +- plugins/Eq/EqFader.h | 2 +- src/gui/MainWindow.cpp | 15 +- src/gui/MixerView.cpp | 28 ++ src/gui/modals/SetupDialog.cpp | 22 +- src/gui/widgets/Fader.cpp | 405 +++++++++++++++--- src/gui/widgets/FloatModelEditorBase.cpp | 6 +- 10 files changed, 450 insertions(+), 97 deletions(-) diff --git a/include/Fader.h b/include/Fader.h index 53e353a3d..9d6e21590 100644 --- a/include/Fader.h +++ b/include/Fader.h @@ -74,8 +74,8 @@ public: Q_PROPERTY(bool renderUnityLine READ getRenderUnityLine WRITE setRenderUnityLine) Q_PROPERTY(QColor unityMarker MEMBER m_unityMarker) - Fader(FloatModel* model, const QString& name, QWidget* parent); - Fader(FloatModel* model, const QString& name, QWidget* parent, const QPixmap& knob); + Fader(FloatModel* model, const QString& name, QWidget* parent, bool modelIsLinear = true); + Fader(FloatModel* model, const QString& name, QWidget* parent, const QPixmap& knob, bool modelIsLinear = true); ~Fader() override = default; void setPeak_L(float fPeak); @@ -93,6 +93,17 @@ public: inline bool getRenderUnityLine() const { return m_renderUnityLine; } inline void setRenderUnityLine(bool value = true) { m_renderUnityLine = value; } + enum class AdjustmentDirection + { + Up, + Down + }; + + void adjust(const Qt::KeyboardModifiers & modifiers, AdjustmentDirection direction); + void adjustByDecibelDelta(float value); + + void adjustByDialog(); + void setDisplayConversion(bool b) { m_conversionFactor = b ? 100.0 : 1.0; @@ -118,18 +129,34 @@ private: void paintEvent(QPaintEvent* ev) override; void paintLevels(QPaintEvent* ev, QPainter& painter, bool linear = false); + void paintFaderTicks(QPainter& painter); - int knobPosY() const - { - float fRange = model()->maxValue() - model()->minValue(); - float realVal = model()->value() - model()->minValue(); + float determineAdjustmentDelta(const Qt::KeyboardModifiers & modifiers) const; + void adjustModelByDBDelta(float value); - return height() - ((height() - m_knob.height()) * (realVal / fRange)); - } + int calculateKnobPosYFromModel() const; + void setVolumeByLocalPixelValue(int y); + + /** + * @brief Computes the scaled ratio between the maximum dB value supported by the model and the minimum + * dB value that's supported by the fader from the given actual dB value. + * + * If the provided input value lies inside the aforementioned interval then the result will be + * a value between 0 (value == minimum value) and 1 (value == maximum model value). + * If you look at the graphical representation of the fader then 0 represents a point at the bottom + * of the fader and 1 a point at the top of the fader. + * The ratio is scaled by an internal exponent which is an implementation detail that cannot be + * changed for now. + */ + float computeScaledRatio(float dBValue) const; void setPeak(float fPeak, float& targetPeak, float& persistentPeak, QElapsedTimer& lastPeakTimer); void updateTextFloat(); + void modelValueChanged(); + QString getModelValueAsDbString() const; + + bool modelIsLinear() const { return m_modelIsLinear; } // Private members private: @@ -145,10 +172,16 @@ private: QPixmap m_knob {embed::getIconPixmap("fader_knob")}; - bool m_levelsDisplayedInDBFS {true}; + /** + * @brief Stores the offset to the knob center when the user drags the fader knob + * + * This is needed to make it feel like the users drag the knob without it + * jumping immediately to the click position. + */ + int m_knobCenterOffset {0}; - int m_moveStartPoint {-1}; - float m_startValue {0.}; + bool m_levelsDisplayedInDBFS {true}; + bool m_modelIsLinear {false}; static SimpleTextFloat* s_textFloat; diff --git a/include/MixerChannelView.h b/include/MixerChannelView.h index 6716aee09..3d5f4ffb6 100644 --- a/include/MixerChannelView.h +++ b/include/MixerChannelView.h @@ -82,6 +82,8 @@ public: QColor strokeInnerInactive() const { return m_strokeInnerInactive; } void setStrokeInnerInactive(const QColor& c) { m_strokeInnerInactive = c; } + Fader* fader() const { return m_fader; } + public slots: void renameChannel(); void resetColor(); diff --git a/include/SetupDialog.h b/include/SetupDialog.h index 871a80bcd..23589f91a 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -72,10 +72,10 @@ protected slots: private slots: // General settings widget. - void toggleDisplaydBFS(bool enabled); void toggleTooltips(bool enabled); void toggleDisplayWaveform(bool enabled); void toggleNoteLabels(bool enabled); + void toggleShowFaderTicks(bool enabled); void toggleCompactTrackButtons(bool enabled); void toggleOneInstrumentTrackWindow(bool enabled); void toggleSideBarOnRight(bool enabled); @@ -134,10 +134,10 @@ private: TabBar * m_tabBar; // General settings widgets. - bool m_displaydBFS; bool m_tooltips; bool m_displayWaveform; bool m_printNoteLabels; + bool m_showFaderTicks; bool m_compactTrackButtons; bool m_oneInstrumentTrackWindow; bool m_sideBarOnRight; diff --git a/plugins/CrossoverEQ/CrossoverEQControlDialog.cpp b/plugins/CrossoverEQ/CrossoverEQControlDialog.cpp index a4f44f5d3..e7202556b 100644 --- a/plugins/CrossoverEQ/CrossoverEQControlDialog.cpp +++ b/plugins/CrossoverEQ/CrossoverEQControlDialog.cpp @@ -70,25 +70,25 @@ CrossoverEQControlDialog::CrossoverEQControlDialog( CrossoverEQControls * contro QPixmap const fader_knob(PLUGIN_NAME::getIconPixmap("fader_knob2")); // faders - auto gain1 = new Fader(&controls->m_gain1, tr("Band 1 gain"), this, fader_knob); + auto gain1 = new Fader(&controls->m_gain1, tr("Band 1 gain"), this, fader_knob, false); gain1->move( 7, 56 ); gain1->setDisplayConversion( false ); gain1->setHintText( tr( "Band 1 gain:" ), " dBFS" ); gain1->setRenderUnityLine(false); - auto gain2 = new Fader(&controls->m_gain2, tr("Band 2 gain"), this, fader_knob); + auto gain2 = new Fader(&controls->m_gain2, tr("Band 2 gain"), this, fader_knob, false); gain2->move( 47, 56 ); gain2->setDisplayConversion( false ); gain2->setHintText( tr( "Band 2 gain:" ), " dBFS" ); gain2->setRenderUnityLine(false); - auto gain3 = new Fader(&controls->m_gain3, tr("Band 3 gain"), this, fader_knob); + auto gain3 = new Fader(&controls->m_gain3, tr("Band 3 gain"), this, fader_knob, false); gain3->move( 87, 56 ); gain3->setDisplayConversion( false ); gain3->setHintText( tr( "Band 3 gain:" ), " dBFS" ); gain3->setRenderUnityLine(false); - auto gain4 = new Fader(&controls->m_gain4, tr("Band 4 gain"), this, fader_knob); + auto gain4 = new Fader(&controls->m_gain4, tr("Band 4 gain"), this, fader_knob, false); gain4->move( 127, 56 ); gain4->setDisplayConversion( false ); gain4->setHintText( tr( "Band 4 gain:" ), " dBFS" ); diff --git a/plugins/Eq/EqFader.h b/plugins/Eq/EqFader.h index 3185d0879..5c9aa5e5d 100644 --- a/plugins/Eq/EqFader.h +++ b/plugins/Eq/EqFader.h @@ -43,7 +43,7 @@ public: Q_OBJECT public: EqFader( FloatModel * model, const QString & name, QWidget * parent, float* lPeak, float* rPeak ) : - Fader( model, name, parent ) + Fader(model, name, parent, false) { setMinimumSize( 23, 116 ); setMaximumSize( 23, 116 ); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 48d2ddb30..225a3f0e9 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -1099,13 +1099,7 @@ void MainWindow::updateViewMenu() // Here we should put all look&feel -stuff from configmanager // that is safe to change on the fly. There is probably some // more elegant way to do this. - auto qa = new QAction(tr("Volume as dBFS"), this); - qa->setData("displaydbfs"); - qa->setCheckable( true ); - qa->setChecked( ConfigManager::inst()->value( "app", "displaydbfs" ).toInt() ); - m_viewMenu->addAction(qa); - - qa = new QAction(tr( "Smooth scroll" ), this); + auto qa = new QAction(tr("Smooth scroll"), this); qa->setData("smoothscroll"); qa->setCheckable( true ); qa->setChecked( ConfigManager::inst()->value( "ui", "smoothscroll" ).toInt() ); @@ -1135,12 +1129,7 @@ void MainWindow::updateConfig( QAction * _who ) QString tag = _who->data().toString(); bool checked = _who->isChecked(); - if( tag == "displaydbfs" ) - { - ConfigManager::inst()->setValue( "app", "displaydbfs", - QString::number(checked) ); - } - else if ( tag == "tooltips" ) + if (tag == "tooltips") { ConfigManager::inst()->setValue( "tooltips", "disabled", QString::number(!checked) ); diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index 0ba893be0..d05ff097d 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -481,6 +481,16 @@ void MixerView::renameChannel(int index) void MixerView::keyPressEvent(QKeyEvent * e) { + auto adjustCurrentFader = [this](const Qt::KeyboardModifiers& modifiers, Fader::AdjustmentDirection direction) + { + auto* mixerChannel = currentMixerChannel(); + + if (mixerChannel) + { + mixerChannel->fader()->adjust(modifiers, direction); + } + }; + switch(e->key()) { case Qt::Key_Delete: @@ -508,6 +518,14 @@ void MixerView::keyPressEvent(QKeyEvent * e) setCurrentMixerChannel(m_currentMixerChannel->channelIndex() + 1); } break; + case Qt::Key_Up: + case Qt::Key_Plus: + adjustCurrentFader(e->modifiers(), Fader::AdjustmentDirection::Up); + break; + case Qt::Key_Down: + case Qt::Key_Minus: + adjustCurrentFader(e->modifiers(), Fader::AdjustmentDirection::Down); + break; case Qt::Key_Insert: if (e->modifiers() & Qt::ShiftModifier) { @@ -519,6 +537,16 @@ void MixerView::keyPressEvent(QKeyEvent * e) case Qt::Key_F2: renameChannel(m_currentMixerChannel->channelIndex()); break; + case Qt::Key_Space: + { + auto* mixerChannel = currentMixerChannel(); + + if (mixerChannel) + { + mixerChannel->fader()->adjustByDialog(); + } + } + break; } } diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index d71ede03f..06f228ab7 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -91,14 +91,14 @@ inline void labelWidget(QWidget * w, const QString & txt) SetupDialog::SetupDialog(ConfigTab tab_to_open) : - m_displaydBFS(ConfigManager::inst()->value( - "app", "displaydbfs").toInt()), m_tooltips(!ConfigManager::inst()->value( "tooltips", "disabled").toInt()), m_displayWaveform(ConfigManager::inst()->value( "ui", "displaywaveform").toInt()), m_printNoteLabels(ConfigManager::inst()->value( "ui", "printnotelabels").toInt()), + m_showFaderTicks(ConfigManager::inst()->value( + "ui", "showfaderticks").toInt()), m_compactTrackButtons(ConfigManager::inst()->value( "ui", "compacttrackbuttons").toInt()), m_oneInstrumentTrackWindow(ConfigManager::inst()->value( @@ -231,14 +231,14 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : QGroupBox * guiGroupBox = new QGroupBox(tr("Graphical user interface (GUI)"), generalControls); QVBoxLayout * guiGroupLayout = new QVBoxLayout(guiGroupBox); - addCheckBox(tr("Display volume as dBFS "), guiGroupBox, guiGroupLayout, - m_displaydBFS, SLOT(toggleDisplaydBFS(bool)), true); addCheckBox(tr("Enable tooltips"), guiGroupBox, guiGroupLayout, m_tooltips, SLOT(toggleTooltips(bool)), true); addCheckBox(tr("Enable master oscilloscope by default"), guiGroupBox, guiGroupLayout, m_displayWaveform, SLOT(toggleDisplayWaveform(bool)), true); addCheckBox(tr("Enable all note labels in piano roll"), guiGroupBox, guiGroupLayout, m_printNoteLabels, SLOT(toggleNoteLabels(bool)), false); + addCheckBox(tr("Show fader ticks"), guiGroupBox, guiGroupLayout, + m_showFaderTicks, SLOT(toggleShowFaderTicks(bool)), false); addCheckBox(tr("Enable compact track buttons"), guiGroupBox, guiGroupLayout, m_compactTrackButtons, SLOT(toggleCompactTrackButtons(bool)), true); addCheckBox(tr("Enable one instrument-track-window mode"), guiGroupBox, guiGroupLayout, @@ -913,14 +913,14 @@ void SetupDialog::accept() from taking mouse input, rendering the application unusable. */ QDialog::accept(); - ConfigManager::inst()->setValue("app", "displaydbfs", - QString::number(m_displaydBFS)); ConfigManager::inst()->setValue("tooltips", "disabled", QString::number(!m_tooltips)); ConfigManager::inst()->setValue("ui", "displaywaveform", QString::number(m_displayWaveform)); ConfigManager::inst()->setValue("ui", "printnotelabels", QString::number(m_printNoteLabels)); + ConfigManager::inst()->setValue("ui", "showfaderticks", + QString::number(m_showFaderTicks)); ConfigManager::inst()->setValue("ui", "compacttrackbuttons", QString::number(m_compactTrackButtons)); ConfigManager::inst()->setValue("ui", "oneinstrumenttrackwindow", @@ -1003,12 +1003,6 @@ void SetupDialog::accept() // General settings slots. -void SetupDialog::toggleDisplaydBFS(bool enabled) -{ - m_displaydBFS = enabled; -} - - void SetupDialog::toggleTooltips(bool enabled) { m_tooltips = enabled; @@ -1026,6 +1020,10 @@ void SetupDialog::toggleNoteLabels(bool enabled) m_printNoteLabels = enabled; } +void SetupDialog::toggleShowFaderTicks(bool enabled) +{ + m_showFaderTicks = enabled; +} void SetupDialog::toggleCompactTrackButtons(bool enabled) { diff --git a/src/gui/widgets/Fader.cpp b/src/gui/widgets/Fader.cpp index e8560c3c0..153d8ca1a 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -58,14 +58,22 @@ #include "KeyboardShortcuts.h" #include "SimpleTextFloat.h" +namespace +{ + constexpr auto c_dBScalingExponent = 3.f; + //! The dbFS amount after which we drop down to -inf dbFS + constexpr auto c_faderMinDb = -120.f; +} + namespace lmms::gui { SimpleTextFloat* Fader::s_textFloat = nullptr; -Fader::Fader(FloatModel* model, const QString& name, QWidget* parent) : +Fader::Fader(FloatModel* model, const QString& name, QWidget* parent, bool modelIsLinear) : QWidget(parent), - FloatModelView(model, this) + FloatModelView(model, this), + m_modelIsLinear(modelIsLinear) { if (s_textFloat == nullptr) { @@ -82,15 +90,74 @@ Fader::Fader(FloatModel* model, const QString& name, QWidget* parent) : setHintText("Volume:", "%"); m_conversionFactor = 100.0; + + if (model) + { + // We currently assume that the model is not changed later on and only connect here once + + // This is for example used to update the tool tip which shows the current value of the fader + connect(model, &FloatModel::dataChanged, this, &Fader::modelValueChanged); + + // Trigger manually so that the tool tip is initialized correctly + modelValueChanged(); + } } -Fader::Fader(FloatModel* model, const QString& name, QWidget* parent, const QPixmap& knob) : - Fader(model, name, parent) +Fader::Fader(FloatModel* model, const QString& name, QWidget* parent, const QPixmap& knob, bool modelIsLinear) : + Fader(model, name, parent, modelIsLinear) { m_knob = knob; } +void Fader::adjust(const Qt::KeyboardModifiers & modifiers, AdjustmentDirection direction) +{ + const auto adjustmentDb = determineAdjustmentDelta(modifiers) * (direction == AdjustmentDirection::Down ? -1. : 1.); + adjustByDecibelDelta(adjustmentDb); +} + +void Fader::adjustByDecibelDelta(float value) +{ + adjustModelByDBDelta(value); + + updateTextFloat(); + s_textFloat->setVisibilityTimeOut(1000); +} + +void Fader::adjustByDialog() +{ + bool ok; + + if (modelIsLinear()) + { + auto maxDB = ampToDbfs(model()->maxValue()); + const auto currentValue = model()->value() <= 0. ? c_faderMinDb : ampToDbfs(model()->value()); + + float enteredValue = QInputDialog::getDouble(this, tr("Set value"), + tr("Please enter a new value between %1 and %2:").arg(c_faderMinDb).arg(maxDB), + currentValue, c_faderMinDb, maxDB, model()->getDigitCount(), &ok); + + if (ok) + { + model()->setValue(dbfsToAmp(enteredValue)); + } + return; + } + else + { + // The model already is in dB + auto minv = model()->minValue() * m_conversionFactor; + auto maxv = model()->maxValue() * m_conversionFactor; + float enteredValue = QInputDialog::getDouble(this, tr("Set value"), + tr("Please enter a new value between %1 and %2:").arg(minv).arg(maxv), + model()->getRoundedValue() * m_conversionFactor, minv, maxv, model()->getDigitCount(), &ok); + + if (ok) + { + model()->setValue(enteredValue / m_conversionFactor); + } + } +} void Fader::contextMenuEvent(QContextMenuEvent* ev) { @@ -105,18 +172,13 @@ void Fader::contextMenuEvent(QContextMenuEvent* ev) void Fader::mouseMoveEvent(QMouseEvent* mouseEvent) { - if (m_moveStartPoint >= 0) - { - int dy = m_moveStartPoint - mouseEvent->globalY(); + const int localY = mouseEvent->y(); - float delta = dy * (model()->maxValue() - model()->minValue()) / (float)(height() - (m_knob).height()); + setVolumeByLocalPixelValue(localY); - const auto step = model()->step(); - float newValue = static_cast(static_cast((m_startValue + delta) / step + 0.5)) * step; - model()->setValue(newValue); + updateTextFloat(); - updateTextFloat(); - } + mouseEvent->accept(); } @@ -134,20 +196,37 @@ void Fader::mousePressEvent(QMouseEvent* mouseEvent) thisModel->saveJournallingState(false); } - if (mouseEvent->y() >= knobPosY() - (m_knob).height() && mouseEvent->y() < knobPosY()) + const int localY = mouseEvent->y(); + const auto knobLowerPosY = calculateKnobPosYFromModel(); + const auto knobUpperPosY = knobLowerPosY - m_knob.height(); + + const auto clickedOnKnob = localY >= knobUpperPosY && localY <= knobLowerPosY; + + if (clickedOnKnob) { - updateTextFloat(); - s_textFloat->show(); + // If the users clicked on the knob we want to compensate for the offset to the center line + // of the knob when dealing with mouse move events. + // This will make it feel like the users have grabbed the knob where they clicked. + const auto knobCenterPos = knobLowerPosY - (m_knob.height() / 2); + m_knobCenterOffset = localY - knobCenterPos; - m_moveStartPoint = mouseEvent->globalY(); - m_startValue = model()->value(); - - mouseEvent->accept(); + // In this case we also will not call setVolumeByLocalPixelValue, i.e. we do not make any immediate + // changes. This should only happen if the users actually move the mouse while grabbing the knob. + // This makes the knobs less "jumpy". } else { - m_moveStartPoint = -1; + // If the users did not click on the knob then we assume that the fader knob's center should move to + // the position of the click. We do not compensate for any offset. + m_knobCenterOffset = 0; + + setVolumeByLocalPixelValue(localY); } + + updateTextFloat(); + s_textFloat->show(); + + mouseEvent->accept(); } else { @@ -159,18 +238,9 @@ void Fader::mousePressEvent(QMouseEvent* mouseEvent) void Fader::mouseDoubleClickEvent(QMouseEvent* mouseEvent) { - bool ok; - // TODO: dbFS handling - auto minv = model()->minValue() * m_conversionFactor; - auto maxv = model()->maxValue() * m_conversionFactor; - float enteredValue = QInputDialog::getDouble(this, tr("Set value"), - tr("Please enter a new value between %1 and %2:").arg(minv).arg(maxv), - model()->getRoundedValue() * m_conversionFactor, minv, maxv, model()->getDigitCount(), &ok); + adjustByDialog(); - if (ok) - { - model()->setValue(enteredValue / m_conversionFactor); - } + mouseEvent->accept(); } @@ -186,20 +256,197 @@ void Fader::mouseReleaseEvent(QMouseEvent* mouseEvent) } } + // Always reset the offset to 0 regardless of which mouse button is pressed + m_knobCenterOffset = 0; + s_textFloat->hide(); } void Fader::wheelEvent (QWheelEvent* ev) { - ev->accept(); const int direction = (ev->angleDelta().y() > 0 ? 1 : -1) * (ev->inverted() ? -1 : 1); - model()->incValue(direction); - updateTextFloat(); - s_textFloat->setVisibilityTimeOut(1000); + const float increment = determineAdjustmentDelta(ev->modifiers()) * direction; + + adjustByDecibelDelta(increment); + + ev->accept(); } +float Fader::determineAdjustmentDelta(const Qt::KeyboardModifiers & modifiers) const +{ + if (modifiers == Qt::ShiftModifier) + { + // The shift is intended to go through the values in very coarse steps as in: + // "Shift into overdrive" + return 3.f; + } + else if (modifiers == Qt::ControlModifier) + { + // The control key gives more control, i.e. it enables more fine-grained adjustments + return 0.1f; + } + else if (modifiers & Qt::AltModifier) + { + // Work around a Qt bug in conjunction with the scroll wheel and the Alt key + return 0.f; + } + + return 1.f; +} + +void Fader::adjustModelByDBDelta(float value) +{ + if (modelIsLinear()) + { + const auto modelValue = model()->value(); + + if (modelValue <= 0.) + { + // We are at -inf dB. Do nothing if we user wishes to decrease. + if (value > 0) + { + // Otherwise set the model to the minimum value supported by the fader. + model()->setValue(dbfsToAmp(c_faderMinDb)); + } + } + else + { + // We can safely compute the dB value as the value is greater than 0 + const auto valueInDB = ampToDbfs(modelValue); + + const auto adjustedValue = valueInDB + value; + + model()->setValue(adjustedValue < c_faderMinDb ? 0. : dbfsToAmp(adjustedValue)); + } + } + else + { + const auto adjustedValue = std::clamp(model()->value() + value, model()->minValue(), model()->maxValue()); + + model()->setValue(adjustedValue); + } +} + +int Fader::calculateKnobPosYFromModel() const +{ + auto* m = model(); + + auto const minV = m->minValue(); + auto const maxV = m->maxValue(); + auto const value = m->value(); + + if (modelIsLinear()) + { + // This method calculates the pixel position where the lower end of + // the fader knob should be for the amplification value in the model. + // + // The following assumes that the model describes an amplification, + // i.e. that values are in [0, max] and that 1 is unity, i.e. 0 dbFS. + + auto const distanceToMin = value - minV; + + // Prevent dbFS calculations with zero or negative values + if (distanceToMin <= 0) + { + return height(); + } + else + { + // Make sure that we do not get values less that the minimum fader dbFS + // for the calculations that will follow. + auto const actualDb = std::max(c_faderMinDb, ampToDbfs(value)); + + const auto scaledRatio = computeScaledRatio(actualDb); + + // This returns results between: + // * m_knob.height() for a ratio of 1 + // * height() for a ratio of 0 + return height() - (height() - m_knob.height()) * scaledRatio; + } + } + else + { + // The model is in dB so we just show that in a linear fashion + + auto const clampedValue = std::clamp(value, minV, maxV); + + auto const ratio = (clampedValue - minV) / (maxV - minV); + + // This returns results between: + // * m_knob.height() for a ratio of 1 + // * height() for a ratio of 0 + return height() - (height() - m_knob.height()) * ratio; + } +} + + +void Fader::setVolumeByLocalPixelValue(int y) +{ + auto* m = model(); + + // Compensate the offset where users have actually clicked + y -= m_knobCenterOffset; + + // The y parameter gives us where the mouse click went. + // Assume that the middle of the fader should go there. + int const lowerFaderKnob = y + (m_knob.height() / 2); + + // In some cases we need the clamped lower position of the fader knob so we can ensure + // that we only set allowed values in the model range. + int const clampedLowerFaderKnob = std::clamp(lowerFaderKnob, m_knob.height(), height()); + + if (modelIsLinear()) + { + if (lowerFaderKnob >= height()) + { + // Check the non-clamped value because otherwise we wouldn't be able to set -inf dB! + model()->setValue(0); + } + else + { + // We are in the case where we set a value that's different from -inf dB so we use the clamped value + // of the lower knob position so that we only set allowed values in the model range. + + // First map the lower knob position to [0, 1] so that we can apply some curve mapping, e.g. + // square, cube, etc. + LinearMap knobMap(float(m_knob.height()), 1., float(height()), 0.); + + // Apply the inverse of what is done in calculateKnobPosYFromModel + auto const knobPos = std::pow(knobMap.map(clampedLowerFaderKnob), 1./c_dBScalingExponent); + + float const maxDb = ampToDbfs(m->maxValue()); + + LinearMap dbMap(1., maxDb, 0., c_faderMinDb); + + float const dbValue = dbMap.map(knobPos); + + // Pull everything that's quieter than the minimum fader dbFS value down to 0 amplification. + // This should not happen due to the steps above but let's be sure. + // Otherwise compute the amplification value from the mapped dbFS value but make sure that we + // do not exceed the maximum dbValue of the model + float ampValue = dbValue < c_faderMinDb ? 0. : dbfsToAmp(std::min(maxDb, dbValue)); + + model()->setValue(ampValue); + } + } + else + { + LinearMap valueMap(float(m_knob.height()), model()->maxValue(), float(height()), model()->minValue()); + + model()->setValue(valueMap.map(clampedLowerFaderKnob)); + } +} + +float Fader::computeScaledRatio(float dBValue) const +{ + const auto maxDb = ampToDbfs(model()->maxValue()); + + const auto ratio = (dBValue - c_faderMinDb) / (maxDb - c_faderMinDb); + + return std::pow(ratio, c_dBScalingExponent); +} /// @@ -246,28 +493,45 @@ void Fader::setPeak_R(float fPeak) // update tooltip showing value and adjust position while changing fader value void Fader::updateTextFloat() { - if (ConfigManager::inst()->value("app", "displaydbfs").toInt() && m_conversionFactor == 100.0) + if (m_conversionFactor == 100.0) { - QString label(tr("Volume: %1 dBFS")); - - auto const modelValue = model()->value(); - if (modelValue <= 0.) - { - s_textFloat->setText(label.arg("-∞")); - } - else - { - s_textFloat->setText(label.arg(ampToDbfs(modelValue), 3, 'f', 2)); - } + s_textFloat->setText(getModelValueAsDbString()); } else { s_textFloat->setText(m_description + " " + QString("%1 ").arg(model()->value() * m_conversionFactor) + " " + m_unit); } - s_textFloat->moveGlobal(this, QPoint(width() + 2, knobPosY() - s_textFloat->height() / 2)); + s_textFloat->moveGlobal(this, QPoint(width() + 2, calculateKnobPosYFromModel() - s_textFloat->height() / 2)); } +void Fader::modelValueChanged() +{ + setToolTip(getModelValueAsDbString()); +} + +QString Fader::getModelValueAsDbString() const +{ + const auto value = model()->value(); + + QString label(tr("Volume: %1 dB")); + + if (modelIsLinear()) + { + if (value <= 0.) + { + return label.arg(tr("-inf")); + } + else + { + return label.arg(ampToDbfs(value), 3, 'f', 2); + } + } + else + { + return label.arg(value, 3, 'f', 2); + } +} void Fader::paintEvent(QPaintEvent* ev) { @@ -276,8 +540,13 @@ void Fader::paintEvent(QPaintEvent* ev) // Draw the levels with peaks paintLevels(ev, painter, !m_levelsDisplayedInDBFS); + if (ConfigManager::inst()->value( "ui", "showfaderticks" ).toInt() && modelIsLinear()) + { + paintFaderTicks(painter); + } + // Draw the knob - painter.drawPixmap((width() - m_knob.width()) / 2, knobPosY() - m_knob.height(), m_knob); + painter.drawPixmap((width() - m_knob.width()) / 2, calculateKnobPosYFromModel() - m_knob.height(), m_knob); } void Fader::paintLevels(QPaintEvent* ev, QPainter& painter, bool linear) @@ -433,4 +702,40 @@ void Fader::paintLevels(QPaintEvent* ev, QPainter& painter, bool linear) painter.restore(); } +void Fader::paintFaderTicks(QPainter& painter) +{ + painter.save(); + + const QPen zeroPen(QColor(255, 255, 255, 216), 2.5); + const QPen nonZeroPen(QColor(255, 255, 255, 128), 1.); + + // We use the maximum dB value of the model to calculate the nearest multiple + // of the step size that we use to paint the ticks so that we know the start point. + // This code will paint ticks with steps that are defined by the step size around + // the 0 dB marker. + const auto maxDB = ampToDbfs(model()->maxValue()); + const auto stepSize = 6.f; + const auto startValue = std::floor(maxDB / stepSize) * stepSize; + + for (float i = startValue; i >= c_faderMinDb; i-= stepSize) + { + const auto scaledRatio = computeScaledRatio(i); + const auto maxHeight = height() - (height() - m_knob.height()) * scaledRatio - (m_knob.height() / 2); + + if (approximatelyEqual(i, 0.)) + { + painter.setPen(zeroPen); + } + else + { + painter.setPen(nonZeroPen); + } + + painter.drawLine(QPointF(0, maxHeight), QPointF(1, maxHeight)); + painter.drawLine(QPointF(width() - 1, maxHeight), QPointF(width(), maxHeight)); + } + + painter.restore(); +} + } // namespace lmms::gui diff --git a/src/gui/widgets/FloatModelEditorBase.cpp b/src/gui/widgets/FloatModelEditorBase.cpp index 81f121a84..3c7fe93c7 100644 --- a/src/gui/widgets/FloatModelEditorBase.cpp +++ b/src/gui/widgets/FloatModelEditorBase.cpp @@ -376,8 +376,7 @@ void FloatModelEditorBase::enterValue() bool ok; float new_val; - if (isVolumeKnob() && - ConfigManager::inst()->value("app", "displaydbfs").toInt()) + if (isVolumeKnob()) { auto const initalValue = model()->getRoundedValue() / 100.0; auto const initialDbValue = initalValue > 0. ? ampToDbfs(initalValue) : -96; @@ -430,8 +429,7 @@ void FloatModelEditorBase::friendlyUpdate() QString FloatModelEditorBase::displayValue() const { - if (isVolumeKnob() && - ConfigManager::inst()->value("app", "displaydbfs").toInt()) + if (isVolumeKnob()) { auto const valueToVolumeRatio = model()->getRoundedValue() / volumeRatio(); return m_description.trimmed() + (