diff --git a/include/Fader.h b/include/Fader.h index 53e353a3dc5..9d6e21590c2 100644 --- a/include/Fader.h +++ b/include/Fader.h @@ -74,8 +74,8 @@ class LMMS_EXPORT Fader : public QWidget, public FloatModelView 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 @@ class LMMS_EXPORT Fader : public QWidget, public FloatModelView 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 @@ class LMMS_EXPORT Fader : public QWidget, public FloatModelView void paintEvent(QPaintEvent* ev) override; void paintLevels(QPaintEvent* ev, QPainter& painter, bool linear = false); - - int knobPosY() const - { - float fRange = model()->maxValue() - model()->minValue(); - float realVal = model()->value() - model()->minValue(); - - return height() - ((height() - m_knob.height()) * (realVal / fRange)); - } + void paintFaderTicks(QPainter& painter); + + float determineAdjustmentDelta(const Qt::KeyboardModifiers & modifiers) const; + void adjustModelByDBDelta(float value); + + 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 @@ class LMMS_EXPORT Fader : public QWidget, public FloatModelView 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 6716aee094b..3d5f4ffb6f1 100644 --- a/include/MixerChannelView.h +++ b/include/MixerChannelView.h @@ -82,6 +82,8 @@ class MixerChannelView : public QWidget 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 871a80bcd4b..23589f91ac6 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 slots: 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 a4f44f5d305..e7202556b15 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 3185d08794b..5c9aa5e5d2c 100644 --- a/plugins/Eq/EqFader.h +++ b/plugins/Eq/EqFader.h @@ -43,7 +43,7 @@ class EqFader : public Fader 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 48d2ddb3025..225a3f0e93a 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 0ba893be062..d05ff097db5 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 d71ede03f53..06f228ab7e3 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 a647df416cf..6806d98e5d1 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -57,14 +57,22 @@ #include "ConfigManager.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()) - { - updateTextFloat(); - s_textFloat->show(); + const int localY = mouseEvent->y(); + const auto knobLowerPosY = calculateKnobPosYFromModel(); + const auto knobUpperPosY = knobLowerPosY - m_knob.height(); - m_moveStartPoint = mouseEvent->globalY(); - m_startValue = model()->value(); + const auto clickedOnKnob = localY >= knobUpperPosY && localY <= knobLowerPosY; - mouseEvent->accept(); + if (clickedOnKnob) + { + // 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; + + // 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,21 +256,198 @@ 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); +} + /// /// Set peak value (0.0 .. 1.0) @@ -246,29 +493,46 @@ 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) + { + s_textFloat->setText(getModelValueAsDbString()); + } + else { - QString label(tr("Volume: %1 dBFS")); + s_textFloat->setText(m_description + " " + QString("%1 ").arg(model()->value() * m_conversionFactor) + " " + m_unit); + } - auto const modelValue = model()->value(); - if (modelValue <= 0.) + 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.) { - s_textFloat->setText(label.arg("-∞")); + return label.arg(tr("-inf")); } else { - s_textFloat->setText(label.arg(ampToDbfs(modelValue), 3, 'f', 2)); + return label.arg(ampToDbfs(value), 3, 'f', 2); } } else { - s_textFloat->setText(m_description + " " + QString("%1 ").arg(model()->value() * m_conversionFactor) + " " + m_unit); + return label.arg(value, 3, 'f', 2); } - - s_textFloat->moveGlobal(this, QPoint(width() + 2, knobPosY() - s_textFloat->height() / 2)); } - void Fader::paintEvent(QPaintEvent* ev) { QPainter painter(this); @@ -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 ebd0d3d9dc7..e85ca1a5097 100644 --- a/src/gui/widgets/FloatModelEditorBase.cpp +++ b/src/gui/widgets/FloatModelEditorBase.cpp @@ -390,8 +390,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; @@ -444,8 +443,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() + (