Chris@923: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@923: Chris@923: /* Chris@923: Sonic Visualiser Chris@923: An audio file viewer and annotation editor. Chris@923: Centre for Digital Music, Queen Mary, University of London. Chris@923: Chris@923: This program is free software; you can redistribute it and/or Chris@923: modify it under the terms of the GNU General Public License as Chris@923: published by the Free Software Foundation; either version 2 of the Chris@923: License, or (at your option) any later version. See the file Chris@923: COPYING included with this distribution for more information. Chris@923: */ Chris@923: Chris@923: #include "LevelPanWidget.h" Chris@923: Chris@923: #include Chris@923: #include Chris@923: #include Chris@923: Chris@923: #include "layer/ColourMapper.h" Chris@925: #include "base/AudioLevel.h" Chris@923: Chris@1176: #include "WidgetScale.h" Chris@1176: Chris@923: #include Chris@926: #include Chris@940: #include Chris@923: Chris@923: using std::cerr; Chris@923: using std::endl; Chris@923: Chris@1301: /** Chris@1301: * Gain and pan scales: Chris@1301: * Chris@1301: * Gain: we have 5 circles vertically in the display, each of which Chris@1301: * has half-circle and full-circle versions, and we also have "no Chris@1301: * circles", so there are in total 11 distinct levels, which we refer Chris@1301: * to as "notches" and number 0-10. (We use "notch" because "level" is Chris@1301: * used by the external API to refer to audio gain.) Chris@1301: * Chris@1301: * i.e. the levels are represented by these (schematic, rotated to Chris@1301: * horizontal) displays: Chris@1301: * Chris@1301: * 0 X Chris@1301: * 1 [ Chris@1301: * 2 [] Chris@1301: * 3 [][ Chris@1301: * ... Chris@1301: * 9 [][][][][ Chris@1301: * 10 [][][][][] Chris@1301: * Chris@1301: * If we have mute enabled, then we map the range 0-10 to gain using Chris@1301: * AudioLevel::fader_to_* with the ShortFader type, which treats fader Chris@1301: * 0 as muted. If mute is disabled, then we map the range 1-10. Chris@1301: * Chris@1301: * We can also disable half-circles, which leaves the range unchanged Chris@1301: * but limits the notches to even values. Chris@1301: * Chris@1301: * Pan: we have 5 columns with no finer resolution, so we only have 2 Chris@1301: * possible pan values on each side of centre. Chris@1301: */ Chris@1301: Chris@923: static const int maxPan = 2; // range is -maxPan to maxPan Chris@923: Chris@923: LevelPanWidget::LevelPanWidget(QWidget *parent) : Chris@923: QWidget(parent), Chris@1301: m_minNotch(0), Chris@1301: m_maxNotch(10), Chris@1301: m_notch(m_maxNotch), Chris@923: m_pan(0), Chris@1177: m_monitorLeft(-1), Chris@1177: m_monitorRight(-1), Chris@940: m_editable(true), Chris@1249: m_editing(false), Chris@1301: m_includeMute(true), Chris@1303: m_includeHalfSteps(true) Chris@923: { Chris@1191: setToolTip(tr("Drag vertically to adjust level, horizontally to adjust pan")); Chris@1201: setLevel(1.0); Chris@1201: setPan(0.0); Chris@923: } Chris@923: Chris@923: LevelPanWidget::~LevelPanWidget() Chris@923: { Chris@923: } Chris@923: Chris@1249: void Chris@1249: LevelPanWidget::setToDefault() Chris@1249: { Chris@1249: setLevel(1.0); Chris@1249: setPan(0.0); Chris@1250: emitLevelChanged(); Chris@1250: emitPanChanged(); Chris@1249: } Chris@1249: Chris@929: QSize Chris@929: LevelPanWidget::sizeHint() const Chris@929: { Chris@1176: return WidgetScale::scaleQSize(QSize(40, 40)); Chris@929: } Chris@929: Chris@1301: int Chris@1301: LevelPanWidget::clampNotch(int notch) const Chris@940: { Chris@1301: if (notch < m_minNotch) notch = m_minNotch; Chris@1301: if (notch > m_maxNotch) notch = m_maxNotch; Chris@1301: if (!m_includeHalfSteps) { Chris@1301: notch = (notch / 2) * 2; Chris@1301: } Chris@1301: return notch; Chris@940: } Chris@940: Chris@1177: int Chris@1302: LevelPanWidget::clampPan(int pan) const Chris@1302: { Chris@1302: if (pan < -maxPan) pan = -maxPan; Chris@1302: if (pan > maxPan) pan = maxPan; Chris@1302: return pan; Chris@1302: } Chris@1302: Chris@1302: int Chris@1301: LevelPanWidget::audioLevelToNotch(float audioLevel) const Chris@1177: { Chris@1301: int notch = AudioLevel::multiplier_to_fader Chris@1301: (audioLevel, m_maxNotch, AudioLevel::ShortFader); Chris@1301: return clampNotch(notch); Chris@1177: } Chris@1177: Chris@1177: float Chris@1301: LevelPanWidget::notchToAudioLevel(int notch) const Chris@1177: { Chris@1301: return float(AudioLevel::fader_to_multiplier Chris@1301: (notch, m_maxNotch, AudioLevel::ShortFader)); Chris@1177: } Chris@1177: Chris@923: void Chris@1301: LevelPanWidget::setLevel(float level) Chris@923: { Chris@1301: int notch = audioLevelToNotch(level); Chris@1301: if (notch != m_notch) { Chris@1301: m_notch = notch; Chris@1266: float convertsTo = getLevel(); Chris@1301: if (fabsf(convertsTo - level) > 1e-5) { Chris@1266: emitLevelChanged(); Chris@1266: } Chris@1266: update(); Chris@925: } Chris@923: } Chris@923: Chris@940: float Chris@940: LevelPanWidget::getLevel() const Chris@940: { Chris@1301: return notchToAudioLevel(m_notch); Chris@1177: } Chris@1177: Chris@1177: int Chris@1301: LevelPanWidget::audioPanToPan(float audioPan) const Chris@1177: { Chris@1177: int pan = int(round(audioPan * maxPan)); Chris@1302: pan = clampPan(pan); Chris@1177: return pan; Chris@1177: } Chris@1177: Chris@1177: float Chris@1301: LevelPanWidget::panToAudioPan(int pan) const Chris@1177: { Chris@1177: return float(pan) / float(maxPan); Chris@1177: } Chris@1177: Chris@1177: void Chris@1177: LevelPanWidget::setPan(float fpan) Chris@1177: { Chris@1177: int pan = audioPanToPan(fpan); Chris@1177: if (pan != m_pan) { Chris@1177: m_pan = pan; Chris@1177: update(); Chris@940: } Chris@940: } Chris@940: Chris@1177: float Chris@1177: LevelPanWidget::getPan() const Chris@1177: { Chris@1177: return panToAudioPan(m_pan); Chris@1177: } Chris@1177: Chris@923: void Chris@1177: LevelPanWidget::setMonitoringLevels(float left, float right) Chris@923: { Chris@1177: m_monitorLeft = left; Chris@1177: m_monitorRight = right; Chris@923: update(); Chris@923: } Chris@923: Chris@940: bool Chris@940: LevelPanWidget::isEditable() const Chris@940: { Chris@940: return m_editable; Chris@940: } Chris@940: Chris@940: bool Chris@940: LevelPanWidget::includesMute() const Chris@940: { Chris@940: return m_includeMute; Chris@940: } Chris@940: Chris@923: void Chris@923: LevelPanWidget::setEditable(bool editable) Chris@923: { Chris@923: m_editable = editable; Chris@923: update(); Chris@923: } Chris@923: Chris@940: void Chris@940: LevelPanWidget::setIncludeMute(bool include) Chris@923: { Chris@940: m_includeMute = include; Chris@1301: if (m_includeMute) { Chris@1301: m_minNotch = 0; Chris@1301: } else { Chris@1301: m_minNotch = 1; Chris@1301: } Chris@940: emitLevelChanged(); Chris@940: update(); Chris@923: } Chris@923: Chris@923: void Chris@923: LevelPanWidget::emitLevelChanged() Chris@923: { Chris@923: emit levelChanged(getLevel()); Chris@923: } Chris@923: Chris@923: void Chris@923: LevelPanWidget::emitPanChanged() Chris@923: { Chris@923: emit panChanged(getPan()); Chris@923: } Chris@923: Chris@923: void Chris@923: LevelPanWidget::mousePressEvent(QMouseEvent *e) Chris@923: { Chris@1249: if (e->button() == Qt::MidButton || Chris@1249: ((e->button() == Qt::LeftButton) && Chris@1249: (e->modifiers() & Qt::ControlModifier))) { Chris@1249: setToDefault(); Chris@1249: } else if (e->button() == Qt::LeftButton) { Chris@1249: m_editing = true; Chris@1249: mouseMoveEvent(e); Chris@1249: } Chris@1249: } Chris@1249: Chris@1249: void Chris@1249: LevelPanWidget::mouseReleaseEvent(QMouseEvent *e) Chris@1249: { Chris@923: mouseMoveEvent(e); Chris@1249: m_editing = false; Chris@923: } Chris@923: Chris@923: void Chris@923: LevelPanWidget::mouseMoveEvent(QMouseEvent *e) Chris@923: { Chris@923: if (!m_editable) return; Chris@1249: if (!m_editing) return; Chris@923: Chris@1301: int notch = coordsToNotch(rect(), e->pos()); Chris@1301: int pan = coordsToPan(rect(), e->pos()); Chris@1301: Chris@1301: if (notch == m_notch && pan == m_pan) { Chris@1266: return; Chris@923: } Chris@1301: if (notch != m_notch) { Chris@1301: m_notch = notch; Chris@1266: emitLevelChanged(); Chris@923: } Chris@923: if (pan != m_pan) { Chris@1266: m_pan = pan; Chris@1266: emitPanChanged(); Chris@923: } Chris@923: update(); Chris@923: } Chris@923: Chris@923: void Chris@923: LevelPanWidget::wheelEvent(QWheelEvent *e) Chris@923: { Chris@1303: int delta = m_wheelCounter.count(e); Chris@1303: Chris@1303: if (delta == 0) { Chris@1302: return; Chris@1302: } Chris@1302: Chris@1303: if (e->modifiers() & Qt::ControlModifier) { Chris@1303: m_pan = clampPan(m_pan + delta); Chris@1303: emitPanChanged(); Chris@1303: update(); Chris@1302: } else { Chris@1303: m_notch = clampNotch(m_notch + delta); Chris@1303: emitLevelChanged(); Chris@1303: update(); Chris@923: } Chris@923: } Chris@923: Chris@1301: int Chris@1301: LevelPanWidget::coordsToNotch(QRectF rect, QPointF loc) const Chris@923: { Chris@1301: double h = rect.height(); Chris@1301: Chris@1301: int nnotch = m_maxNotch + 1; Chris@1301: double cell = h / nnotch; Chris@1301: Chris@1301: int notch = int((h - (loc.y() - rect.y())) / cell); Chris@1301: notch = clampNotch(notch); Chris@1301: Chris@1301: return notch; Chris@1301: } Chris@1301: Chris@1301: int Chris@1301: LevelPanWidget::coordsToPan(QRectF rect, QPointF loc) const Chris@1301: { Chris@1301: double w = rect.width(); Chris@929: Chris@923: int npan = maxPan * 2 + 1; Chris@1301: double cell = w / npan; Chris@929: Chris@1301: int pan = int((loc.x() - rect.x()) / cell) - maxPan; Chris@1302: pan = clampPan(pan); Chris@1301: Chris@1301: return pan; Chris@923: } Chris@923: Chris@923: QSizeF Chris@929: LevelPanWidget::cellSize(QRectF rect) const Chris@923: { Chris@929: double w = rect.width(), h = rect.height(); Chris@1301: int ncol = maxPan * 2 + 1; Chris@1301: int nrow = m_maxNotch/2; Chris@1301: double wcell = w / ncol, hcell = h / nrow; Chris@923: return QSizeF(wcell, hcell); Chris@923: } Chris@923: Chris@923: QPointF Chris@1301: LevelPanWidget::cellCentre(QRectF rect, int row, int col) const Chris@923: { Chris@929: QSizeF cs = cellSize(rect); Chris@1301: return QPointF(rect.x() + Chris@1301: cs.width() * (col + maxPan) + cs.width() / 2., Chris@1301: rect.y() + rect.height() - Chris@1301: cs.height() * (row + 1) + cs.height() / 2.); Chris@923: } Chris@923: Chris@923: QSizeF Chris@929: LevelPanWidget::cellLightSize(QRectF rect) const Chris@923: { cannam@1307: double extent = 0.7; Chris@929: QSizeF cs = cellSize(rect); Chris@923: double m = std::min(cs.width(), cs.height()); Chris@923: return QSizeF(m * extent, m * extent); Chris@923: } Chris@923: Chris@923: QRectF Chris@1301: LevelPanWidget::cellLightRect(QRectF rect, int row, int col) const Chris@923: { Chris@929: QSizeF cls = cellLightSize(rect); Chris@1301: QPointF cc = cellCentre(rect, row, col); Chris@923: return QRectF(cc.x() - cls.width() / 2., Chris@1266: cc.y() - cls.height() / 2., Chris@1266: cls.width(), Chris@1266: cls.height()); Chris@923: } Chris@923: Chris@923: double Chris@929: LevelPanWidget::thinLineWidth(QRectF rect) const Chris@923: { Chris@929: double tw = ceil(rect.width() / (maxPan * 2. * 10.)); Chris@1301: double th = ceil(rect.height() / (m_maxNotch/2 * 10.)); Chris@923: return std::min(th, tw); Chris@923: } Chris@923: Chris@1304: double Chris@1304: LevelPanWidget::cornerRadius(QRectF rect) const Chris@1304: { Chris@1304: QSizeF cs = cellSize(rect); Chris@1304: double m = std::min(cs.width(), cs.height()); Chris@1304: return m / 5; Chris@1304: } Chris@1304: Chris@1301: QRectF Chris@1301: LevelPanWidget::cellOutlineRect(QRectF rect, int row, int col) const Chris@941: { Chris@1301: QRectF clr = cellLightRect(rect, row, col); cannam@1307: double adj = thinLineWidth(rect)/2 + 0.5; Chris@1301: return clr.adjusted(-adj, -adj, adj, adj); Chris@1301: } Chris@1301: Chris@1301: QColor Chris@1306: LevelPanWidget::cellToColour(int cell) const Chris@1301: { Chris@1306: if (cell < 1) return Qt::black; Chris@1306: if (cell < 2) return QColor(80, 0, 0); Chris@1306: if (cell < 3) return QColor(160, 0, 0); Chris@1306: if (cell < 4) return QColor(255, 0, 0); Chris@1301: return QColor(255, 255, 0); Chris@941: } Chris@941: Chris@923: void Chris@929: LevelPanWidget::renderTo(QPaintDevice *dev, QRectF rect, bool asIfEditable) const Chris@923: { Chris@929: QPainter paint(dev); Chris@923: Chris@923: paint.setRenderHint(QPainter::Antialiasing, true); Chris@923: Chris@929: double thin = thinLineWidth(rect); Chris@1304: double radius = cornerRadius(rect); Chris@938: Chris@1301: QColor columnBackground = QColor(180, 180, 180); Chris@1306: Chris@1306: bool monitoring = (m_monitorLeft > 0.f || m_monitorRight > 0.f); Chris@1306: Chris@1306: QPen pen; Chris@1306: if (isEnabled()) { Chris@1306: pen.setColor(Qt::black); Chris@1306: } else { Chris@1306: pen.setColor(Qt::darkGray); Chris@1306: } Chris@1306: pen.setWidthF(thin); Chris@1306: pen.setCapStyle(Qt::FlatCap); Chris@1306: pen.setJoinStyle(Qt::MiterJoin); Chris@923: Chris@923: for (int pan = -maxPan; pan <= maxPan; ++pan) { Chris@1306: Chris@1306: paint.setPen(Qt::NoPen); Chris@1306: paint.setBrush(columnBackground); Chris@1306: Chris@1304: QRectF top = cellOutlineRect(rect, m_maxNotch/2 - 1, pan); Chris@1304: QRectF bottom = cellOutlineRect(rect, 0, pan); Chris@1304: paint.drawRoundedRect(QRectF(top.x(), Chris@1304: top.y(), Chris@1304: top.width(), Chris@1304: bottom.y() + bottom.height() - top.y()), Chris@1304: radius, radius); Chris@924: Chris@1306: if (!asIfEditable && m_includeMute && m_notch == 0) { Chris@1306: // We will instead be drawing a single big X for mute, Chris@1306: // after this loop Chris@1306: continue; Chris@1306: } Chris@1306: Chris@1306: if (!monitoring && m_pan != pan) { Chris@1306: continue; Chris@1306: } Chris@1306: Chris@1306: int monitorNotch = 0; Chris@1306: if (monitoring) { Chris@1306: float rprop = float(pan - (-maxPan)) / float(maxPan * 2); Chris@1306: float lprop = float(maxPan - pan) / float(maxPan * 2); Chris@1306: float monitorLevel = Chris@1306: lprop * m_monitorLeft * m_monitorLeft + Chris@1306: rprop * m_monitorRight * m_monitorRight; Chris@1306: monitorNotch = audioLevelToNotch(monitorLevel); Chris@1306: } Chris@1306: Chris@1306: int firstCell = 0; Chris@1306: int lastCell = m_maxNotch / 2 - 1; Chris@1306: Chris@1306: for (int cell = firstCell; cell <= lastCell; ++cell) { Chris@1306: Chris@1306: QRectF clr = cellLightRect(rect, cell, pan); Chris@1306: Chris@1306: if (m_includeMute && m_pan == pan && m_notch == 0) { Chris@1306: // X for mute in the bottom cell Chris@1306: paint.setPen(pen); Chris@1306: paint.drawLine(clr.topLeft(), clr.bottomRight()); Chris@1306: paint.drawLine(clr.bottomLeft(), clr.topRight()); Chris@1306: break; Chris@1306: } Chris@1306: Chris@1306: const int none = 0, half = 1, full = 2; Chris@1306: Chris@1306: int fill = none; Chris@1306: Chris@1306: int outline = none; Chris@1306: if (m_pan == pan && m_notch > cell * 2 + 1) { Chris@1306: outline = full; Chris@1306: } else if (m_pan == pan && m_notch == cell * 2 + 1) { Chris@1306: outline = half; Chris@1306: } Chris@1306: Chris@1306: if (monitoring) { Chris@1306: if (monitorNotch > cell * 2 + 1) { Chris@1306: fill = full; Chris@1306: } else if (monitorNotch == cell * 2 + 1) { Chris@1306: fill = half; Chris@1306: } Chris@1306: } else { Chris@1306: if (isEnabled()) { Chris@1306: fill = outline; Chris@1306: } Chris@1306: } Chris@1306: Chris@1306: // If one of {fill, outline} is "full" and the other is Chris@1306: // "half", then we draw the "half" one first (because we Chris@1306: // need to erase half of it) Chris@1306: Chris@1306: if (fill == half || outline == half) { Chris@1306: if (fill == half) { Chris@1306: paint.setBrush(cellToColour(cell)); Chris@1306: } else { Chris@1306: paint.setBrush(Qt::NoBrush); Chris@1306: } Chris@1306: if (outline == half) { Chris@1306: paint.setPen(pen); Chris@1306: } else { Chris@1306: paint.setPen(Qt::NoPen); Chris@1306: } Chris@1306: Chris@1306: paint.drawRoundedRect(clr, radius, radius); Chris@1306: Chris@1306: paint.setBrush(columnBackground); Chris@1306: Chris@1306: if (cell == lastCell) { Chris@1306: QPen bgpen(pen); Chris@1306: bgpen.setColor(columnBackground); Chris@1306: paint.setPen(bgpen); Chris@1306: paint.drawRoundedRect(QRectF(clr.x(), Chris@1306: clr.y(), Chris@1306: clr.width(), Chris@1306: clr.height()/4), Chris@1306: radius, radius); Chris@1306: paint.drawRect(QRectF(clr.x(), Chris@1306: clr.y() + clr.height()/4, Chris@1306: clr.width(), Chris@1306: clr.height()/4)); Chris@1306: } else { Chris@1306: paint.setPen(Qt::NoPen); cannam@1307: QRectF cor = cellOutlineRect(rect, cell, pan); cannam@1307: paint.drawRect(QRectF(cor.x(), cannam@1307: cor.y() - 0.5, cannam@1307: cor.width(), cannam@1307: cor.height()/2)); Chris@1306: } Chris@1306: } Chris@1306: Chris@1306: if (outline == full || fill == full) { Chris@1306: Chris@1306: if (fill == full) { Chris@1306: paint.setBrush(cellToColour(cell)); Chris@1306: } else { Chris@1306: paint.setBrush(Qt::NoBrush); Chris@1306: } Chris@1306: if (outline == full) { Chris@1306: paint.setPen(pen); Chris@1306: } else { Chris@1306: paint.setPen(Qt::NoPen); Chris@1306: } Chris@1306: Chris@1306: paint.drawRoundedRect(clr, radius, radius); Chris@1306: } Chris@1306: } Chris@1301: } Chris@1301: Chris@1301: if (!asIfEditable && m_includeMute && m_notch == 0) { Chris@1301: // The X for mute takes up the whole display when we're not Chris@1301: // being rendered in editable style Chris@1306: pen.setColor(Qt::black); Chris@1301: pen.setWidthF(thin * 2); Chris@1301: pen.setCapStyle(Qt::RoundCap); Chris@1301: paint.setPen(pen); Chris@1301: paint.drawLine(cellCentre(rect, 0, -maxPan), Chris@1301: cellCentre(rect, m_maxNotch/2 - 1, maxPan)); Chris@1301: paint.drawLine(cellCentre(rect, m_maxNotch/2 - 1, -maxPan), Chris@1301: cellCentre(rect, 0, maxPan)); Chris@1177: } Chris@923: } Chris@923: Chris@929: void Chris@929: LevelPanWidget::paintEvent(QPaintEvent *) Chris@929: { Chris@929: renderTo(this, rect(), m_editable); Chris@929: } Chris@923: Chris@1180: void Chris@1180: LevelPanWidget::enterEvent(QEvent *e) Chris@1180: { Chris@1180: QWidget::enterEvent(e); Chris@1180: emit mouseEntered(); Chris@1180: } Chris@929: Chris@1180: void Chris@1180: LevelPanWidget::leaveEvent(QEvent *e) Chris@1180: { Chris@1180: QWidget::enterEvent(e); Chris@1180: emit mouseLeft(); Chris@1180: } Chris@929: