Mercurial > hg > svgui
view widgets/LevelPanWidget.cpp @ 1386:fc3d89f88690 spectrogramparam
Use log-frequency rather than log-bin for calculating x coord in spectrum. This has the advantage that frequency positions don't move when we change the window size or oversampling ratio, but it does give us an unhelpfully large amount of space for very low frequencies - to be considered
author | Chris Cannam |
---|---|
date | Mon, 12 Nov 2018 11:34:34 +0000 |
parents | 5af5c611f4cb |
children |
line wrap: on
line source
/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ /* Sonic Visualiser An audio file viewer and annotation editor. Centre for Digital Music, Queen Mary, University of London. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. See the file COPYING included with this distribution for more information. */ #include "LevelPanWidget.h" #include <QPainter> #include <QMouseEvent> #include <QWheelEvent> #include "layer/ColourMapper.h" #include "base/AudioLevel.h" #include "WidgetScale.h" #include <iostream> #include <cmath> #include <cassert> using std::cerr; using std::endl; /** * 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_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_includeHalfSteps(true) { setToolTip(tr("Drag vertically to adjust level, horizontally to adjust pan")); setLevel(1.0); setPan(0.0); } LevelPanWidget::~LevelPanWidget() { } void LevelPanWidget::setToDefault() { setLevel(1.0); setPan(0.0); emitLevelChanged(); emitPanChanged(); } QSize LevelPanWidget::sizeHint() const { return WidgetScale::scaleQSize(QSize(40, 40)); } int LevelPanWidget::clampNotch(int notch) const { 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::clampPan(int pan) const { if (pan < -maxPan) pan = -maxPan; if (pan > maxPan) pan = maxPan; return pan; } int LevelPanWidget::audioLevelToNotch(float audioLevel) const { int notch = AudioLevel::multiplier_to_fader (audioLevel, m_maxNotch, AudioLevel::ShortFader); return clampNotch(notch); } float LevelPanWidget::notchToAudioLevel(int notch) const { return float(AudioLevel::fader_to_multiplier (notch, m_maxNotch, AudioLevel::ShortFader)); } void LevelPanWidget::setLevel(float level) { int notch = audioLevelToNotch(level); if (notch != m_notch) { m_notch = notch; float convertsTo = getLevel(); if (fabsf(convertsTo - level) > 1e-5) { emitLevelChanged(); } update(); } } float LevelPanWidget::getLevel() const { return notchToAudioLevel(m_notch); } int LevelPanWidget::audioPanToPan(float audioPan) const { int pan = int(round(audioPan * maxPan)); pan = clampPan(pan); return pan; } float LevelPanWidget::panToAudioPan(int pan) const { return float(pan) / float(maxPan); } void LevelPanWidget::setPan(float fpan) { int pan = audioPanToPan(fpan); if (pan != m_pan) { m_pan = pan; update(); } } float LevelPanWidget::getPan() const { return panToAudioPan(m_pan); } void LevelPanWidget::setMonitoringLevels(float left, float right) { m_monitorLeft = left; m_monitorRight = right; update(); } bool LevelPanWidget::isEditable() const { return m_editable; } bool LevelPanWidget::includesMute() const { return m_includeMute; } void LevelPanWidget::setEditable(bool editable) { m_editable = editable; update(); } void LevelPanWidget::setIncludeMute(bool include) { m_includeMute = include; if (m_includeMute) { m_minNotch = 0; } else { m_minNotch = 1; } emitLevelChanged(); update(); } void LevelPanWidget::emitLevelChanged() { emit levelChanged(getLevel()); } void LevelPanWidget::emitPanChanged() { emit panChanged(getPan()); } void LevelPanWidget::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::MidButton || ((e->button() == Qt::LeftButton) && (e->modifiers() & Qt::ControlModifier))) { setToDefault(); } else if (e->button() == Qt::LeftButton) { m_editing = true; mouseMoveEvent(e); } } void LevelPanWidget::mouseReleaseEvent(QMouseEvent *e) { mouseMoveEvent(e); m_editing = false; } void LevelPanWidget::mouseMoveEvent(QMouseEvent *e) { if (!m_editable) return; if (!m_editing) return; int notch = coordsToNotch(rect(), e->pos()); int pan = coordsToPan(rect(), e->pos()); if (notch == m_notch && pan == m_pan) { return; } if (notch != m_notch) { m_notch = notch; emitLevelChanged(); } if (pan != m_pan) { m_pan = pan; emitPanChanged(); } update(); } void LevelPanWidget::wheelEvent(QWheelEvent *e) { int delta = m_wheelCounter.count(e); if (delta == 0) { return; } if (e->modifiers() & Qt::ControlModifier) { m_pan = clampPan(m_pan + delta); emitPanChanged(); update(); } else { m_notch = clampNotch(m_notch + delta); emitLevelChanged(); update(); } } int LevelPanWidget::coordsToNotch(QRectF rect, QPointF loc) const { 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; double cell = w / npan; int pan = int((loc.x() - rect.x()) / cell) - maxPan; pan = clampPan(pan); return pan; } QSizeF LevelPanWidget::cellSize(QRectF rect) const { double w = rect.width(), h = rect.height(); 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 row, int col) const { QSizeF cs = cellSize(rect); return QPointF(rect.x() + cs.width() * (col + maxPan) + cs.width() / 2., rect.y() + rect.height() - cs.height() * (row + 1) + cs.height() / 2.); } QSizeF LevelPanWidget::cellLightSize(QRectF rect) const { double extent = 0.7; QSizeF cs = cellSize(rect); double m = std::min(cs.width(), cs.height()); return QSizeF(m * extent, m * extent); } QRectF LevelPanWidget::cellLightRect(QRectF rect, int row, int col) const { QSizeF cls = cellLightSize(rect); QPointF cc = cellCentre(rect, row, col); return QRectF(cc.x() - cls.width() / 2., cc.y() - cls.height() / 2., cls.width(), cls.height()); } double LevelPanWidget::thinLineWidth(QRectF rect) const { double tw = ceil(rect.width() / (maxPan * 2. * 10.)); double th = ceil(rect.height() / (m_maxNotch/2 * 10.)); return std::min(th, tw); } double LevelPanWidget::cornerRadius(QRectF rect) const { QSizeF cs = cellSize(rect); double m = std::min(cs.width(), cs.height()); return m / 5; } QRectF LevelPanWidget::cellOutlineRect(QRectF rect, int row, int col) const { QRectF clr = cellLightRect(rect, row, col); double adj = thinLineWidth(rect)/2 + 0.5; return clr.adjusted(-adj, -adj, adj, adj); } QColor LevelPanWidget::cellToColour(int cell) const { if (cell < 1) return Qt::black; if (cell < 2) return QColor(80, 0, 0); if (cell < 3) return QColor(160, 0, 0); if (cell < 4) return QColor(255, 0, 0); return QColor(255, 255, 0); } void LevelPanWidget::renderTo(QPaintDevice *dev, QRectF rect, bool asIfEditable) const { QPainter paint(dev); paint.setRenderHint(QPainter::Antialiasing, true); double thin = thinLineWidth(rect); double radius = cornerRadius(rect); QColor columnBackground = QColor(180, 180, 180); bool monitoring = (m_monitorLeft > 0.f || m_monitorRight > 0.f); QPen pen; if (isEnabled()) { pen.setColor(Qt::black); } else { pen.setColor(Qt::darkGray); } pen.setWidthF(thin); pen.setCapStyle(Qt::FlatCap); pen.setJoinStyle(Qt::MiterJoin); for (int pan = -maxPan; pan <= maxPan; ++pan) { paint.setPen(Qt::NoPen); paint.setBrush(columnBackground); QRectF top = cellOutlineRect(rect, m_maxNotch/2 - 1, pan); QRectF bottom = cellOutlineRect(rect, 0, pan); paint.drawRoundedRect(QRectF(top.x(), top.y(), top.width(), bottom.y() + bottom.height() - top.y()), radius, radius); if (!asIfEditable && m_includeMute && m_notch == 0) { // We will instead be drawing a single big X for mute, // after this loop continue; } if (!monitoring && m_pan != pan) { continue; } int monitorNotch = 0; if (monitoring) { float rprop = float(pan - (-maxPan)) / float(maxPan * 2); float lprop = float(maxPan - pan) / float(maxPan * 2); float monitorLevel = lprop * m_monitorLeft * m_monitorLeft + rprop * m_monitorRight * m_monitorRight; monitorNotch = audioLevelToNotch(monitorLevel); } int firstCell = 0; int lastCell = m_maxNotch / 2 - 1; for (int cell = firstCell; cell <= lastCell; ++cell) { QRectF clr = cellLightRect(rect, cell, pan); if (m_includeMute && m_pan == pan && m_notch == 0) { // X for mute in the bottom cell paint.setPen(pen); paint.drawLine(clr.topLeft(), clr.bottomRight()); paint.drawLine(clr.bottomLeft(), clr.topRight()); break; } const int none = 0, half = 1, full = 2; int fill = none; int outline = none; if (m_pan == pan && m_notch > cell * 2 + 1) { outline = full; } else if (m_pan == pan && m_notch == cell * 2 + 1) { outline = half; } if (monitoring) { if (monitorNotch > cell * 2 + 1) { fill = full; } else if (monitorNotch == cell * 2 + 1) { fill = half; } } else { if (isEnabled()) { fill = outline; } } // If one of {fill, outline} is "full" and the other is // "half", then we draw the "half" one first (because we // need to erase half of it) if (fill == half || outline == half) { if (fill == half) { paint.setBrush(cellToColour(cell)); } else { paint.setBrush(Qt::NoBrush); } if (outline == half) { paint.setPen(pen); } else { paint.setPen(Qt::NoPen); } paint.drawRoundedRect(clr, radius, radius); paint.setBrush(columnBackground); if (cell == lastCell) { QPen bgpen(pen); bgpen.setColor(columnBackground); paint.setPen(bgpen); paint.drawRoundedRect(QRectF(clr.x(), clr.y(), clr.width(), clr.height()/4), radius, radius); paint.drawRect(QRectF(clr.x(), clr.y() + clr.height()/4, clr.width(), clr.height()/4)); } else { paint.setPen(Qt::NoPen); QRectF cor = cellOutlineRect(rect, cell, pan); paint.drawRect(QRectF(cor.x(), cor.y() - 0.5, cor.width(), cor.height()/2)); } } if (outline == full || fill == full) { if (fill == full) { paint.setBrush(cellToColour(cell)); } else { paint.setBrush(Qt::NoBrush); } if (outline == full) { paint.setPen(pen); } else { paint.setPen(Qt::NoPen); } paint.drawRoundedRect(clr, radius, radius); } } } 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.setColor(Qt::black); 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)); } } void LevelPanWidget::paintEvent(QPaintEvent *) { renderTo(this, rect(), m_editable); } void LevelPanWidget::enterEvent(QEvent *e) { QWidget::enterEvent(e); emit mouseEntered(); } void LevelPanWidget::leaveEvent(QEvent *e) { QWidget::enterEvent(e); emit mouseLeft(); }