# HG changeset patch # User Chris Cannam # Date 1529591789 -3600 # Node ID e8368466fa3407937aa22c1e35492bb4ca7fd43c # Parent 1589bc7528b7b9bdf33352155eacf5c7cf329a9f Half-steps for level in level-pan widget diff -r 1589bc7528b7 -r e8368466fa34 widgets/LevelPanWidget.cpp --- 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 diff -r 1589bc7528b7 -r e8368466fa34 widgets/LevelPanWidget.h --- 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