changeset 1301:e8368466fa34

Half-steps for level in level-pan widget
author Chris Cannam
date Thu, 21 Jun 2018 15:36:29 +0100
parents 1589bc7528b7
children f3d3fab250ac
files widgets/LevelPanWidget.cpp widgets/LevelPanWidget.h
diffstat 2 files changed, 207 insertions(+), 141 deletions(-) [+]
line wrap: on
line diff
--- a/widgets/LevelPanWidget.cpp	Thu Jun 21 10:43:14 2018 +0100
+++ b/widgets/LevelPanWidget.cpp	Thu Jun 21 15:36:29 2018 +0100
@@ -30,18 +30,51 @@
 using std::cerr;
 using std::endl;
 
-static const int maxLevel = 4; // min is 0, may be mute or not depending on m_includeMute
+/**
+ * Gain and pan scales:
+ *
+ * Gain: we have 5 circles vertically in the display, each of which
+ * has half-circle and full-circle versions, and we also have "no
+ * circles", so there are in total 11 distinct levels, which we refer
+ * to as "notches" and number 0-10. (We use "notch" because "level" is
+ * used by the external API to refer to audio gain.)
+ * 
+ * i.e. the levels are represented by these (schematic, rotated to
+ * horizontal) displays:
+ *
+ *  0  X
+ *  1  [
+ *  2  []
+ *  3  [][
+ *  ...
+ *  9  [][][][][
+ *  10 [][][][][]
+ * 
+ * If we have mute enabled, then we map the range 0-10 to gain using
+ * AudioLevel::fader_to_* with the ShortFader type, which treats fader
+ * 0 as muted. If mute is disabled, then we map the range 1-10.
+ * 
+ * We can also disable half-circles, which leaves the range unchanged
+ * but limits the notches to even values.
+ * 
+ * Pan: we have 5 columns with no finer resolution, so we only have 2
+ * possible pan values on each side of centre.
+ */
+
 static const int maxPan = 2; // range is -maxPan to maxPan
 
 LevelPanWidget::LevelPanWidget(QWidget *parent) :
     QWidget(parent),
-    m_level(maxLevel),
+    m_minNotch(0),
+    m_maxNotch(10),
+    m_notch(m_maxNotch),
     m_pan(0),
     m_monitorLeft(-1),
     m_monitorRight(-1),
     m_editable(true),
     m_editing(false),
-    m_includeMute(true)
+    m_includeMute(true),
+    m_includeHalfSteps(true)
 {
     setToolTip(tr("Drag vertically to adjust level, horizontally to adjust pan"));
     setLevel(1.0);
@@ -67,81 +100,55 @@
     return WidgetScale::scaleQSize(QSize(40, 40));
 }
 
-static int
-db_to_level(double db)
+int
+LevelPanWidget::clampNotch(int notch) const
 {
-    // Only if !m_includeMute, otherwise AudioLevel is used.
-    // Levels are: +6 0 -6 -12 -20
-    assert(maxLevel == 4);
-    if (db > 3.) return 4;
-    else if (db > -3.) return 3;
-    else if (db > -9.) return 2;
-    else if (db > -16.) return 1;
-    else return 0;
-}
-
-static double
-level_to_db(int level)
-{
-    // Only if !m_includeMute, otherwise AudioLevel is used.
-    // Levels are: +6 0 -6 -12 -20
-    assert(maxLevel == 4);
-    if (level >= 4) return 6.;
-    else if (level == 3) return 0.;
-    else if (level == 2) return -6.;
-    else if (level == 1) return -12.;
-    else return -20.;
+    if (notch < m_minNotch) notch = m_minNotch;
+    if (notch > m_maxNotch) notch = m_maxNotch;
+    if (!m_includeHalfSteps) {
+        notch = (notch / 2) * 2;
+    }
+    return notch;
 }
 
 int
-LevelPanWidget::audioLevelToLevel(float audioLevel, bool withMute)
+LevelPanWidget::audioLevelToNotch(float audioLevel) const
 {
-    int level;
-    if (withMute) {
-        level = AudioLevel::multiplier_to_fader
-            (audioLevel, maxLevel, AudioLevel::ShortFader);
-    } else {
-        level = db_to_level(AudioLevel::multiplier_to_dB(audioLevel));
-    }
-    if (level < 0) level = 0;
-    if (level > maxLevel) level = maxLevel;
-    return level;
+    int notch = AudioLevel::multiplier_to_fader
+        (audioLevel, m_maxNotch, AudioLevel::ShortFader);
+    return clampNotch(notch);
 }
 
 float
-LevelPanWidget::levelToAudioLevel(int level, bool withMute)
+LevelPanWidget::notchToAudioLevel(int notch) const
 {
-    if (withMute) {
-        return float(AudioLevel::fader_to_multiplier
-                     (level, maxLevel, AudioLevel::ShortFader));
-    } else {
-        return float(AudioLevel::dB_to_multiplier(level_to_db(level)));
-    }
+    return float(AudioLevel::fader_to_multiplier
+                 (notch, m_maxNotch, AudioLevel::ShortFader));
 }
 
 void
-LevelPanWidget::setLevel(float flevel)
+LevelPanWidget::setLevel(float level)
 {
-    int level = audioLevelToLevel(flevel, m_includeMute);
-    if (level != m_level) {
-        m_level = level;
+    int notch = audioLevelToNotch(level);
+    if (notch != m_notch) {
+        m_notch = notch;
         float convertsTo = getLevel();
-        if (fabsf(convertsTo - flevel) > 1e-5) {
+        if (fabsf(convertsTo - level) > 1e-5) {
             emitLevelChanged();
         }
         update();
     }
+    SVCERR << "setLevel: level " << level << " -> notch " << m_notch << " (which converts back to level " << getLevel() << ")" << endl;
 }
 
 float
 LevelPanWidget::getLevel() const
 {
-    float flevel = levelToAudioLevel(m_level, m_includeMute);
-    return flevel;
+    return notchToAudioLevel(m_notch);
 }
 
 int
-LevelPanWidget::audioPanToPan(float audioPan)
+LevelPanWidget::audioPanToPan(float audioPan) const
 {
     int pan = int(round(audioPan * maxPan));
     if (pan < -maxPan) pan = -maxPan;
@@ -150,7 +157,7 @@
 }
 
 float
-LevelPanWidget::panToAudioPan(int pan)
+LevelPanWidget::panToAudioPan(int pan) const
 {
     return float(pan) / float(maxPan);
 }
@@ -202,6 +209,11 @@
 LevelPanWidget::setIncludeMute(bool include)
 {
     m_includeMute = include;
+    if (m_includeMute) {
+        m_minNotch = 0;
+    } else {
+        m_minNotch = 1;
+    }
     emitLevelChanged();
     update();
 }
@@ -244,13 +256,14 @@
     if (!m_editable) return;
     if (!m_editing) return;
     
-    int level, pan;
-    toCell(rect(), e->pos(), level, pan);
-    if (level == m_level && pan == m_pan) {
+    int notch = coordsToNotch(rect(), e->pos());
+    int pan = coordsToPan(rect(), e->pos());
+
+    if (notch == m_notch && pan == m_pan) {
         return;
     }
-    if (level != m_level) {
-        m_level = level;
+    if (notch != m_notch) {
+        m_notch = notch;
         emitLevelChanged();
     }
     if (pan != m_pan) {
@@ -279,14 +292,14 @@
         }
     } else {
         if (e->delta() > 0) {
-            if (m_level < maxLevel) {
-                ++m_level;
+            if (m_notch < m_maxNotch) {
+                ++m_notch;
                 emitLevelChanged();
                 update();
             }
         } else {
-            if (m_level > 0) {
-                --m_level;
+            if (m_notch > m_minNotch) {
+                --m_notch;
                 emitLevelChanged();
                 update();
             }
@@ -294,41 +307,53 @@
     }
 }
 
-void
-LevelPanWidget::toCell(QRectF rect, QPointF loc, int &level, int &pan) const
+int
+LevelPanWidget::coordsToNotch(QRectF rect, QPointF loc) const
 {
-    double w = rect.width(), h = rect.height();
+    double h = rect.height();
+
+    int nnotch = m_maxNotch + 1;
+    double cell = h / nnotch;
+
+    int notch = int((h - (loc.y() - rect.y())) / cell);
+    notch = clampNotch(notch);
+
+    return notch;
+}    
+
+int
+LevelPanWidget::coordsToPan(QRectF rect, QPointF loc) const
+{
+    double w = rect.width();
 
     int npan = maxPan * 2 + 1;
-    int nlevel = maxLevel + 1;
+    double cell = w / npan;
 
-    double wcell = w / npan, hcell = h / nlevel;
-
-    level = int((h - (loc.y() - rect.y())) / hcell);
-    if (level < 0) level = 0;
-    if (level > maxLevel) level = maxLevel;
-
-    pan = int((loc.x() - rect.x()) / wcell) - maxPan;
+    int pan = int((loc.x() - rect.x()) / cell) - maxPan;
     if (pan < -maxPan) pan = -maxPan;
     if (pan > maxPan) pan = maxPan;
+
+    return pan;
 }
 
 QSizeF
 LevelPanWidget::cellSize(QRectF rect) const
 {
     double w = rect.width(), h = rect.height();
-    int npan = maxPan * 2 + 1;
-    int nlevel = maxLevel + 1;
-    double wcell = w / npan, hcell = h / nlevel;
+    int ncol = maxPan * 2 + 1;
+    int nrow = m_maxNotch/2;
+    double wcell = w / ncol, hcell = h / nrow;
     return QSizeF(wcell, hcell);
 }
 
 QPointF
-LevelPanWidget::cellCentre(QRectF rect, int level, int pan) const
+LevelPanWidget::cellCentre(QRectF rect, int row, int col) const
 {
     QSizeF cs = cellSize(rect);
-    return QPointF(rect.x() + cs.width() * (pan + maxPan) + cs.width() / 2.,
-                   rect.y() + rect.height() - cs.height() * (level + 1) + cs.height() / 2.);
+    return QPointF(rect.x() +
+                   cs.width() * (col + maxPan) + cs.width() / 2.,
+                   rect.y() + rect.height() -
+                   cs.height() * (row + 1) + cs.height() / 2.);
 }
 
 QSizeF
@@ -341,10 +366,10 @@
 }
 
 QRectF
-LevelPanWidget::cellLightRect(QRectF rect, int level, int pan) const
+LevelPanWidget::cellLightRect(QRectF rect, int row, int col) const
 {
     QSizeF cls = cellLightSize(rect);
-    QPointF cc = cellCentre(rect, level, pan);
+    QPointF cc = cellCentre(rect, row, col);
     return QRectF(cc.x() - cls.width() / 2., 
                   cc.y() - cls.height() / 2.,
                   cls.width(),
@@ -355,19 +380,26 @@
 LevelPanWidget::thinLineWidth(QRectF rect) const
 {
     double tw = ceil(rect.width() / (maxPan * 2. * 10.));
-    double th = ceil(rect.height() / (maxLevel * 10.));
+    double th = ceil(rect.height() / (m_maxNotch/2 * 10.));
     return std::min(th, tw);
 }
 
-static QColor
-level_to_colour(int level)
+QRectF
+LevelPanWidget::cellOutlineRect(QRectF rect, int row, int col) const
 {
-    assert(maxLevel == 4);
-    if (level == 0) return Qt::black;
-    else if (level == 1) return QColor(80, 0, 0);
-    else if (level == 2) return QColor(160, 0, 0);
-    else if (level == 3) return QColor(255, 0, 0);
-    else return QColor(255, 255, 0);
+    QRectF clr = cellLightRect(rect, row, col);
+    double adj = thinLineWidth(rect)/2;
+    return clr.adjusted(-adj, -adj, adj, adj);
+}
+
+QColor
+LevelPanWidget::notchToColour(int notch) const
+{
+    if (notch < 3) return Qt::black;
+    if (notch < 5) return QColor(80, 0, 0);
+    if (notch < 7) return QColor(160, 0, 0);
+    if (notch < 9) return QColor(255, 0, 0);
+    return QColor(255, 255, 0);
 }
 
 void
@@ -381,17 +413,69 @@
 
     double thin = thinLineWidth(rect);
 
-    pen.setColor(QColor(127, 127, 127, 127));
+    QColor columnBackground = QColor(180, 180, 180);
+    pen.setColor(columnBackground);
     pen.setWidthF(cellLightSize(rect).width() + thin);
     pen.setCapStyle(Qt::RoundCap);
     paint.setPen(pen);
     paint.setBrush(Qt::NoBrush);
 
     for (int pan = -maxPan; pan <= maxPan; ++pan) {
-        paint.drawLine(cellCentre(rect, 0, pan), cellCentre(rect, maxLevel, pan));
+        paint.drawLine(cellCentre(rect, 0, pan),
+                       cellCentre(rect, m_maxNotch/2 - 1, pan));
     }
 
-    if (m_monitorLeft > 0.f || m_monitorRight > 0.f) {
+    bool monitoring = (m_monitorLeft > 0.f || m_monitorRight > 0.f);
+    
+    if (isEnabled()) {
+        pen.setColor(Qt::black);
+    } else {
+        pen.setColor(Qt::darkGray);
+    }
+
+    if (!asIfEditable && m_includeMute && m_notch == 0) {
+        // The X for mute takes up the whole display when we're not
+        // being rendered in editable style
+        pen.setWidthF(thin * 2);
+        pen.setCapStyle(Qt::RoundCap);
+        paint.setPen(pen);
+        paint.drawLine(cellCentre(rect, 0, -maxPan),
+                       cellCentre(rect, m_maxNotch/2 - 1, maxPan));
+        paint.drawLine(cellCentre(rect, m_maxNotch/2 - 1, -maxPan),
+                       cellCentre(rect, 0, maxPan));
+    } else {
+        // the normal case
+        
+        // pen a bit less thin than in theory, so that we can erase
+        // semi-circles later without leaving a faint edge
+        pen.setWidthF(thin * 0.8);
+        pen.setCapStyle(Qt::FlatCap);
+        paint.setPen(pen);
+
+        if (m_includeMute && m_notch == 0) {
+            QRectF clr = cellLightRect(rect, 0, m_pan);
+            paint.drawLine(clr.topLeft(), clr.bottomRight());
+            paint.drawLine(clr.bottomLeft(), clr.topRight());
+        } else {
+            for (int notch = 1; notch <= m_notch; notch += 2) {
+                if (isEnabled() && !monitoring) {
+                    paint.setBrush(notchToColour(notch));
+                }
+                QRectF clr = cellLightRect(rect, notch/2, m_pan);
+                paint.drawEllipse(clr);
+            }
+            if (m_notch % 2 != 0) {
+                QRectF clr = cellOutlineRect(rect, (m_notch-1)/2, m_pan);
+                paint.save();
+                paint.setPen(Qt::NoPen);
+                paint.setBrush(columnBackground);
+                paint.drawPie(clr, 0, 180 * 16);
+                paint.restore();
+            }
+        }
+    }
+
+    if (monitoring) {
         paint.setPen(Qt::NoPen);
         for (int pan = -maxPan; pan <= maxPan; ++pan) {
             float audioPan = panToAudioPan(pan);
@@ -401,50 +485,22 @@
             } else {
                 audioLevel = m_monitorRight + m_monitorLeft * (1.f - audioPan);
             }
-            int levelHere = audioLevelToLevel(audioLevel, false);
-            for (int level = 0; level <= levelHere; ++level) {
-                paint.setBrush(level_to_colour(level));
-                QRectF clr = cellLightRect(rect, level, pan);
-                paint.drawEllipse(clr);
+            int notchHere = audioLevelToNotch(audioLevel);
+            for (int notch = 1; notch <= notchHere; notch += 2) {
+                paint.setBrush(notchToColour(notch));
+                QRectF clr = cellLightRect(rect, (notch-1)/2, pan);
+                double adj = thinLineWidth(rect)/2;
+                clr = clr.adjusted(adj, adj, -adj, -adj);
+                if (notch + 2 > notchHere && notchHere % 2 != 0) {
+                    paint.drawPie(clr, 180 * 16, 180 * 16);
+                } else {
+                    paint.drawEllipse(clr);
+                }
             }
         }
         paint.setPen(pen);
         paint.setBrush(Qt::NoBrush);
     }
-    
-    if (isEnabled()) {
-        pen.setColor(Qt::black);
-    } else {
-        pen.setColor(Qt::darkGray);
-    }
-
-    if (!asIfEditable && m_includeMute && m_level == 0) {
-        pen.setWidthF(thin * 2);
-        pen.setCapStyle(Qt::RoundCap);
-        paint.setPen(pen);
-        paint.drawLine(cellCentre(rect, 0, -maxPan),
-                       cellCentre(rect, maxLevel, maxPan));
-        paint.drawLine(cellCentre(rect, maxLevel, -maxPan),
-                       cellCentre(rect, 0, maxPan));
-        return;
-    }
-    
-    pen.setWidthF(thin);
-    pen.setCapStyle(Qt::FlatCap);
-    paint.setPen(pen);
-    
-    for (int level = 0; level <= m_level; ++level) {
-        if (isEnabled()) {
-            paint.setBrush(level_to_colour(level));
-        }
-        QRectF clr = cellLightRect(rect, level, m_pan);
-        if (m_includeMute && m_level == 0) {
-            paint.drawLine(clr.topLeft(), clr.bottomRight());
-            paint.drawLine(clr.bottomLeft(), clr.topRight());
-        } else {
-            paint.drawEllipse(clr);
-        }
-    }
 }
 
 void
--- a/widgets/LevelPanWidget.h	Thu Jun 21 10:43:14 2018 +0100
+++ b/widgets/LevelPanWidget.h	Thu Jun 21 15:36:29 2018 +0100
@@ -87,27 +87,37 @@
 
     void emitLevelChanged();
     void emitPanChanged();
-    
-    int m_level;
+
+    int m_minNotch;
+    int m_maxNotch;
+    int m_notch;
     int m_pan;
     float m_monitorLeft;
     float m_monitorRight;
     bool m_editable;
     bool m_editing;
     bool m_includeMute;
+    bool m_includeHalfSteps;
 
-    static int audioLevelToLevel(float audioLevel, bool withMute);
-    static float levelToAudioLevel(int level, bool withMute);
+    int clampNotch(int notch) const;
 
-    static int audioPanToPan(float audioPan);
-    static float panToAudioPan(int pan);
+    int audioLevelToNotch(float audioLevel) const;
+    float notchToAudioLevel(int notch) const;
+
+    int audioPanToPan(float audioPan) const;
+    float panToAudioPan(int pan) const;
+
+    int coordsToNotch(QRectF rect, QPointF pos) const;
+    int coordsToPan(QRectF rect, QPointF pos) const;
+
+    QColor notchToColour(int notch) const;
     
     QSizeF cellSize(QRectF) const;
-    QPointF cellCentre(QRectF, int level, int pan) const;
+    QPointF cellCentre(QRectF, int row, int col) const;
     QSizeF cellLightSize(QRectF) const;
-    QRectF cellLightRect(QRectF, int level, int pan) const;
+    QRectF cellLightRect(QRectF, int row, int col) const;
+    QRectF cellOutlineRect(QRectF, int row, int col) const;
     double thinLineWidth(QRectF) const;
-    void toCell(QRectF, QPointF loc, int &level, int &pan) const;
 };
 
 #endif