Mercurial > hg > svgui
changeset 1395:32bbb86094c3
Merge from branch spectrogramparam
author | Chris Cannam |
---|---|
date | Wed, 14 Nov 2018 14:23:17 +0000 |
parents | 78eecb19e688 (current diff) 4a36f6130056 (diff) |
children | 2e316a724336 |
files | |
diffstat | 15 files changed, 627 insertions(+), 187 deletions(-) [+] |
line wrap: on
line diff
--- a/layer/FlexiNoteLayer.cpp Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/FlexiNoteLayer.cpp Wed Nov 14 14:23:17 2018 +0000 @@ -439,7 +439,7 @@ } if (!usePoints.empty()) { - int fuzz = 2; + int fuzz = ViewManager::scalePixelSize(2); int px = v->getXForFrame(usePoints.begin()->frame); if ((px > x && px - x > fuzz) || (px < x && x - px > fuzz + 1)) {
--- a/layer/Layer.h Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/Layer.h Wed Nov 14 14:23:17 2018 +0000 @@ -136,6 +136,8 @@ virtual void paintVerticalScale(LayerGeometryProvider *, bool /* detailed */, QPainter &, QRect) const { } + virtual int getHorizontalScaleHeight(LayerGeometryProvider *, QPainter &) const { return 0; } + virtual bool getCrosshairExtents(LayerGeometryProvider *, QPainter &, QPoint /* cursorPos */, std::vector<QRect> &) const { return false; @@ -412,11 +414,32 @@ virtual PlayParameters *getPlayParameters(); + /** + * True if this layer will need to place text labels when it is + * painted. The view will take into account how many layers are + * requesting this, and will provide a distinct y-coord to each + * layer on request via View::getTextLabelHeight(). + */ virtual bool needsTextLabelHeight() const { return false; } + /** + * Return true if the X axis on the layer is time proportional to + * audio frames, false otherwise. Almost all layer types return + * true here: the exceptions are spectrum and slice layers. + */ virtual bool hasTimeXAxis() const { return true; } /** + * Update the X and Y axis scales, where appropriate, to focus on + * the given rectangular region. This should *only* be overridden + * by layers whose hasTimeXAxis() returns false - the pane handles + * zooming appropriately in every "normal" case. + */ + virtual void zoomToRegion(const LayerGeometryProvider *, QRect) { + return; + } + + /** * Return the minimum and maximum values for the y axis of the * model in this layer, as well as whether the layer is configured * to use a logarithmic y axis display. Also return the unit for
--- a/layer/NoteLayer.cpp Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/NoteLayer.cpp Wed Nov 14 14:23:17 2018 +0000 @@ -421,7 +421,7 @@ } if (!usePoints.empty()) { - int fuzz = 2; + int fuzz = ViewManager::scalePixelSize(2); int px = v->getXForFrame(usePoints.begin()->frame); if ((px > x && px - x > fuzz) || (px < x && x - px > fuzz + 1)) {
--- a/layer/RegionLayer.cpp Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/RegionLayer.cpp Wed Nov 14 14:23:17 2018 +0000 @@ -335,7 +335,7 @@ } if (!usePoints.empty()) { - int fuzz = 2; + int fuzz = ViewManager::scalePixelSize(2); int px = v->getXForFrame(usePoints.begin()->frame); if ((px > x && px - x > fuzz) || (px < x && x - px > fuzz + 1)) {
--- a/layer/SliceLayer.cpp Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/SliceLayer.cpp Wed Nov 14 14:23:17 2018 +0000 @@ -24,6 +24,8 @@ #include "PaintAssistant.h" +#include "base/Profiler.h" + #include <QPainter> #include <QPainterPath> #include <QTextStream> @@ -72,8 +74,10 @@ connectSignals(m_sliceableModel); - m_minbin = 0; - m_maxbin = m_sliceableModel->getHeight(); + if (m_minbin == 0 && m_maxbin == 0) { + m_minbin = 0; + m_maxbin = m_sliceableModel->getHeight(); + } emit modelReplaced(); emit layerParametersChanged(); @@ -191,26 +195,32 @@ double SliceLayer::getXForBin(const LayerGeometryProvider *v, double bin) const { + return getXForScalePoint(v, bin, m_minbin, m_maxbin); +} + +double +SliceLayer::getXForScalePoint(const LayerGeometryProvider *v, + double p, double pmin, double pmax) const +{ double x = 0; - bin -= m_minbin; - if (bin < 0) bin = 0; - - double count = m_maxbin - m_minbin; - if (count < 0) count = 0; - int pw = v->getPaintWidth(); int origin = m_xorigins[v->getId()]; int w = pw - origin; if (w < 1) w = 1; - switch (m_binScale) { + if (p < pmin) p = pmin; + if (p > pmax) p = pmax; + + if (m_binScale == LinearBins) { + x = (w * (p - pmin)) / (pmax - pmin); + } else { - case LinearBins: - x = (w * bin) / count; - break; + if (m_binScale == InvertedLogBins) { + // stoopid + p = pmax - p; + } - case LogBins: // The 0.8 here is an awkward compromise. Our x-coord is // proportional to log of bin number, with the x-coord "of a // bin" being that of the left edge of the bin range. We can't @@ -223,12 +233,33 @@ // as "a bit less than 1", so that most of it is visible but a // bit is tactfully cropped at the left edge so it doesn't // take up so much space. - x = (w * log10(bin + 0.8)) / log10(count + 0.8); - break; + const double origin = 0.8; - case InvertedLogBins: - x = w - (w * log10(count - bin - 1)) / log10(count); - break; + // sometimes we are called with a pmin/pmax range that begins + // before 0: in that situation, we shift everything along by + // the difference between 0 and pmin before doing any other + // calculations + double reqdshift = 0.0; + if (pmin < 0) reqdshift = -pmin; + + double pminlog = log10(pmin + reqdshift + origin); + double pmaxlog = log10(pmax + reqdshift + origin); + double plog = log10(p + reqdshift + origin); + x = (w * (plog - pminlog)) / (pmaxlog - pminlog); + +/* + cerr << "getXForScalePoint(" << p << "): pmin = " << pmin + << ", pmax = " << pmax << ", w = " << w + << ", reqdshift = " << reqdshift + << ", pminlog = " << pminlog << ", pmaxlog = " << pmaxlog + << ", plog = " << plog + << " -> x = " << x << endl; +*/ + + if (m_binScale == InvertedLogBins) { + // still stoopid + x = w - x; + } } return x + origin; @@ -237,10 +268,14 @@ double SliceLayer::getBinForX(const LayerGeometryProvider *v, double x) const { - double bin = 0; + return getScalePointForX(v, x, m_minbin, m_maxbin); +} - double count = m_maxbin - m_minbin; - if (count < 0) count = 0; +double +SliceLayer::getScalePointForX(const LayerGeometryProvider *v, + double x, double pmin, double pmax) const +{ + double p = 0; int pw = v->getPaintWidth(); int origin = m_xorigins[v->getId()]; @@ -252,24 +287,33 @@ if (x < 0) x = 0; double eps = 1e-10; - - switch (m_binScale) { - case LinearBins: - bin = (x * count) / w + eps; - break; - - case LogBins: - // See comment in getXForBin - bin = pow(10.0, (x * log10(count + 0.8)) / w) - 0.8 + eps; - break; + if (m_binScale == LinearBins) { + p = pmin + eps + (x * (pmax - pmin)) / w; + } else { - case InvertedLogBins: - bin = count + 1 - pow(10.0, (log10(count) * (w - x)) / double(w)) + eps; - break; + if (m_binScale == InvertedLogBins) { + x = w - x; + } + + // See comments in getXForScalePoint + + const double origin = 0.8; + double reqdshift = 0.0; + if (pmin < 0) reqdshift = -pmin; + + double pminlog = log10(pmin + reqdshift + origin); + double pmaxlog = log10(pmax + reqdshift + origin); + + double plog = pminlog + eps + (x * (pmaxlog - pminlog)) / w; + p = pow(10.0, plog) - reqdshift - origin; + + if (m_binScale == InvertedLogBins) { + p = pmax - p; + } } - return bin + m_minbin; + return p; } double @@ -364,11 +408,14 @@ void SliceLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const { - if (!m_sliceableModel || !m_sliceableModel->isOK() || + if (!m_sliceableModel || + !m_sliceableModel->isOK() || !m_sliceableModel->isReady()) return; + Profiler profiler("SliceLayer::paint()"); + paint.save(); - paint.setRenderHint(QPainter::Antialiasing, false); + paint.setRenderHint(QPainter::Antialiasing, true); paint.setBrush(Qt::NoBrush); if (v->getViewManager() && v->getViewManager()->shouldShowScaleGuides()) { @@ -383,17 +430,31 @@ } } + int mh = m_sliceableModel->getHeight(); + int bin0 = 0; + if (m_maxbin > m_minbin) { + mh = m_maxbin - m_minbin; + bin0 = m_minbin; + } + if (m_plotStyle == PlotBlocks) { // Must use actual zero-width pen, too slow otherwise paint.setPen(QPen(getBaseQColor(), 0)); } else { - paint.setPen(PaintAssistant::scalePen(getBaseQColor())); + // Similarly, if there are very many bins here, we use a + // thinner pen + QPen pen(getBaseQColor(), 1); + if (mh < 10000) { + pen = PaintAssistant::scalePen(pen); + } + paint.setPen(pen); } int xorigin = getVerticalScaleWidth(v, true, paint) + 1; m_xorigins[v->getId()] = xorigin; // for use in getFeatureDescription - int yorigin = v->getPaintHeight() - 20 - paint.fontMetrics().height() - 7; + int yorigin = v->getPaintHeight() - getHorizontalScaleHeight(v, paint) - + paint.fontMetrics().height(); int h = yorigin - paint.fontMetrics().height() - 8; m_yorigins[v->getId()] = yorigin; // for getYForValue etc @@ -402,14 +463,6 @@ if (h <= 0) return; QPainterPath path; - - int mh = m_sliceableModel->getHeight(); - int bin0 = 0; - - if (m_maxbin > m_minbin) { - mh = m_maxbin - m_minbin; - bin0 = m_minbin; - } int divisor = 0; @@ -434,6 +487,7 @@ f1 = (col1 + 1) * res - 1; // cerr << "resolution " << res << ", col0 " << col0 << ", col1 " << col1 << ", f0 " << f0 << ", f1 " << f1 << endl; +// cerr << "mh = " << mh << endl; m_currentf0 = f0; m_currentf1 = f1; @@ -443,8 +497,10 @@ int cs = int(curve.size()); for (int col = col0; col <= col1; ++col) { + DenseThreeDimensionalModel::Column column = + m_sliceableModel->getColumn(col); for (int bin = 0; bin < mh; ++bin) { - float value = m_sliceableModel->getValueAt(col, bin0 + bin); + float value = column[bin0 + bin]; if (bin < cs) value *= curve[bin]; if (m_samplingMode == SamplePeak) { if (value > m_values[bin]) m_values[bin] = value; @@ -472,6 +528,13 @@ ColourMapper mapper(m_colourMap, m_colourInverted, 0, 1); + double ytop = 0, ybottom = 0; + bool firstBinOfPixel = true; + + QColor prevColour = v->getBackground(); + double prevPx = 0; + double prevYtop = 0; + for (int bin = 0; bin < mh; ++bin) { double x = nx; @@ -481,36 +544,105 @@ double norm = 0.0; double y = getYForValue(v, value, norm); - if (m_plotStyle == PlotLines) { + if (y < ytop || firstBinOfPixel) { + ytop = y; + } + if (y > ybottom || firstBinOfPixel) { + ybottom = y; + } - if (bin == 0) { - path.moveTo((x + nx) / 2, y); - } else { - path.lineTo((x + nx) / 2, y); + if (int(nx) != int(x) || bin+1 == mh) { + + if (m_plotStyle == PlotLines) { + + double px = (x + nx) / 2; + + if (bin == 0) { + path.moveTo(px, y); + } else { + if (ytop != ybottom) { + path.lineTo(px, ybottom); + path.lineTo(px, ytop); + path.moveTo(px, ybottom); + } else { + path.lineTo(px, ytop); + } + } + + } else if (m_plotStyle == PlotSteps) { + + if (bin == 0) { + path.moveTo(x, y); + } else { + path.lineTo(x, ytop); + } + path.lineTo(nx, ytop); + + } else if (m_plotStyle == PlotBlocks) { + + // work in pixel coords here, as we don't want the + // vertical edges to be antialiased + + path.moveTo(QPoint(int(x), int(yorigin))); + path.lineTo(QPoint(int(x), int(ytop))); + path.lineTo(QPoint(int(nx), int(ytop))); + path.lineTo(QPoint(int(nx), int(yorigin))); + path.lineTo(QPoint(int(x), int(yorigin))); + + } else if (m_plotStyle == PlotFilledBlocks) { + + QColor c = mapper.map(norm); + paint.setPen(Qt::NoPen); + + // work in pixel coords here, as we don't want the + // vertical edges to be antialiased + + if (nx > x + 1) { + + double px = (x + nx) / 2; + + QVector<QPoint> pp; + + if (bin > 0) { + paint.setBrush(prevColour); + pp.clear(); + pp << QPoint(int(prevPx), int(yorigin)); + pp << QPoint(int(prevPx), int(prevYtop)); + pp << QPoint(int((px + prevPx) / 2), + int((ytop + prevYtop) / 2)); + pp << QPoint(int((px + prevPx) / 2), + int(yorigin)); + paint.drawConvexPolygon(QPolygon(pp)); + + paint.setBrush(c); + pp.clear(); + pp << QPoint(int((px + prevPx) / 2), + int(yorigin)); + pp << QPoint(int((px + prevPx) / 2), + int((ytop + prevYtop) / 2)); + pp << QPoint(int(px), int(ytop)); + pp << QPoint(int(px), int(yorigin)); + paint.drawConvexPolygon(QPolygon(pp)); + } + + prevPx = px; + prevColour = c; + prevYtop = ytop; + + } else { + + paint.fillRect(QRect(int(x), int(ytop), + int(nx) - int(x), + int(yorigin) - int(ytop)), + c); + } } - } else if (m_plotStyle == PlotSteps) { + firstBinOfPixel = true; - if (bin == 0) { - path.moveTo(x, y); - } else { - path.lineTo(x, y); - } - path.lineTo(nx, y); - - } else if (m_plotStyle == PlotBlocks) { - - path.moveTo(x, yorigin); - path.lineTo(x, y); - path.lineTo(nx, y); - path.lineTo(nx, yorigin); - path.lineTo(x, yorigin); - - } else if (m_plotStyle == PlotFilledBlocks) { - - paint.fillRect(QRectF(x, y, nx - x, yorigin - y), mapper.map(norm)); + } else { + firstBinOfPixel = false; } - } if (m_plotStyle != PlotFilledBlocks) { @@ -544,7 +676,8 @@ // int h = (rect.height() * 3) / 4; // int y = (rect.height() / 2) - (h / 2); - int yorigin = v->getPaintHeight() - 20 - paint.fontMetrics().height() - 6; + int yorigin = v->getPaintHeight() - getHorizontalScaleHeight(v, paint) - + paint.fontMetrics().height(); int h = yorigin - paint.fontMetrics().height() - 8; if (h < 0) return; @@ -1103,3 +1236,18 @@ return new LinearRangeMapper(0, m_sliceableModel->getHeight(), 0, m_sliceableModel->getHeight(), ""); } + +void +SliceLayer::zoomToRegion(const LayerGeometryProvider *v, QRect rect) +{ + double bin0 = getBinForX(v, rect.x()); + double bin1 = getBinForX(v, rect.x() + rect.width()); + + // ignore y for now... + + SVDEBUG << "SliceLayer::zoomToRegion: zooming to bin range " + << bin0 << " -> " << bin1 << endl; + + setDisplayExtents(floor(bin0), ceil(bin1)); +} +
--- a/layer/SliceLayer.h Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/SliceLayer.h Wed Nov 14 14:23:17 2018 +0000 @@ -73,7 +73,9 @@ virtual void setVerticalZoomStep(int); virtual RangeMapper *getNewVerticalZoomRangeMapper() const; - virtual bool hasTimeXAxis() const { return false; } + virtual bool hasTimeXAxis() const override { return false; } + + virtual void zoomToRegion(const LayerGeometryProvider *, QRect) override; virtual bool isLayerScrollable(const LayerGeometryProvider *) const { return false; } @@ -119,9 +121,22 @@ void modelAboutToBeDeleted(Model *); protected: + /// Convert a (possibly non-integral) bin into x-coord. May be overridden virtual double getXForBin(const LayerGeometryProvider *, double bin) const; + + /// Convert an x-coord into (possibly non-integral) bin. May be overridden virtual double getBinForX(const LayerGeometryProvider *, double x) const; + /// Convert a point such as a bin number into x-coord, given max & + /// min. For use by getXForBin etc + double getXForScalePoint(const LayerGeometryProvider *, + double p, double pmin, double pmax) const; + + /// Convert an x-coord into a point such as a bin number, given + /// max & min. For use by getBinForX etc + double getScalePointForX(const LayerGeometryProvider *, + double x, double pmin, double pmax) const; + virtual double getYForValue(const LayerGeometryProvider *v, double value, double &norm) const; virtual double getValueForY(const LayerGeometryProvider *v, double y) const;
--- a/layer/SpectrogramLayer.cpp Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/SpectrogramLayer.cpp Wed Nov 14 14:23:17 2018 +0000 @@ -61,6 +61,7 @@ m_windowSize(1024), m_windowType(HanningWindow), m_windowHopLevel(2), + m_oversampling(1), m_gain(1.0), m_initialGain(1.0), m_threshold(1.0e-8f), @@ -236,6 +237,7 @@ list.push_back("Colour Scale"); list.push_back("Window Size"); list.push_back("Window Increment"); + list.push_back("Oversampling"); list.push_back("Normalization"); list.push_back("Bin Display"); list.push_back("Threshold"); @@ -254,6 +256,7 @@ if (name == "Colour Scale") return tr("Colour Scale"); if (name == "Window Size") return tr("Window Size"); if (name == "Window Increment") return tr("Window Overlap"); + if (name == "Oversampling") return tr("Oversampling"); if (name == "Normalization") return tr("Normalization"); if (name == "Bin Display") return tr("Bin Display"); if (name == "Threshold") return tr("Threshold"); @@ -287,7 +290,8 @@ if (name == "Bin Display" || name == "Frequency Scale") return tr("Bins"); if (name == "Window Size" || - name == "Window Increment") return tr("Window"); + name == "Window Increment" || + name == "Oversampling") return tr("Window"); if (name == "Colour" || name == "Threshold" || name == "Colour Rotation") return tr("Colour"); @@ -376,7 +380,17 @@ *deflt = 2; val = m_windowHopLevel; - + + } else if (name == "Oversampling") { + + *min = 0; + *max = 3; + *deflt = 0; + + val = 0; + int ov = m_oversampling; + while (ov > 1) { ov >>= 1; val ++; } + } else if (name == "Min Frequency") { *min = 0; @@ -485,6 +499,15 @@ case 5: return tr("93.75 %"); } } + if (name == "Oversampling") { + switch (value) { + default: + case 0: return tr("1x"); + case 1: return tr("2x"); + case 2: return tr("4x"); + case 3: return tr("8x"); + } + } if (name == "Min Frequency") { switch (value) { default: @@ -578,6 +601,8 @@ setWindowSize(32 << value); } else if (name == "Window Increment") { setWindowHopLevel(value); + } else if (name == "Oversampling") { + setOversampling(1 << value); } else if (name == "Min Frequency") { switch (value) { default: @@ -707,8 +732,58 @@ } int -SpectrogramLayer::getFFTOversampling() const +SpectrogramLayer::getFFTSize() const { + return m_windowSize * m_oversampling; +} + +void +SpectrogramLayer::setWindowSize(int ws) +{ + if (m_windowSize == ws) return; + invalidateRenderers(); + m_windowSize = ws; + recreateFFTModel(); + emit layerParametersChanged(); +} + +int +SpectrogramLayer::getWindowSize() const +{ + return m_windowSize; +} + +void +SpectrogramLayer::setWindowHopLevel(int v) +{ + if (m_windowHopLevel == v) return; + invalidateRenderers(); + m_windowHopLevel = v; + recreateFFTModel(); + emit layerParametersChanged(); +} + +int +SpectrogramLayer::getWindowHopLevel() const +{ + return m_windowHopLevel; +} + +void +SpectrogramLayer::setOversampling(int oversampling) +{ + if (m_oversampling == oversampling) return; + invalidateRenderers(); + m_oversampling = oversampling; + recreateFFTModel(); + emit layerParametersChanged(); +} + +int +SpectrogramLayer::getOversampling() const +{ + return m_oversampling; + /*!!! if (m_binDisplay != BinDisplay::AllBins) { return 1; } @@ -722,52 +797,7 @@ } return 4; -} - -int -SpectrogramLayer::getFFTSize() const -{ - return m_windowSize * getFFTOversampling(); -} - -void -SpectrogramLayer::setWindowSize(int ws) -{ - if (m_windowSize == ws) return; - - invalidateRenderers(); - - m_windowSize = ws; - - recreateFFTModel(); - - emit layerParametersChanged(); -} - -int -SpectrogramLayer::getWindowSize() const -{ - return m_windowSize; -} - -void -SpectrogramLayer::setWindowHopLevel(int v) -{ - if (m_windowHopLevel == v) return; - - invalidateRenderers(); - - m_windowHopLevel = v; - - recreateFFTModel(); - - emit layerParametersChanged(); -} - -int -SpectrogramLayer::getWindowHopLevel() const -{ - return m_windowHopLevel; + */ } void @@ -2500,11 +2530,13 @@ s += QString("channel=\"%1\" " "windowSize=\"%2\" " "windowHopLevel=\"%3\" " - "gain=\"%4\" " - "threshold=\"%5\" ") + "oversampling=\"%4\" " + "gain=\"%5\" " + "threshold=\"%6\" ") .arg(m_channel) .arg(m_windowSize) .arg(m_windowHopLevel) + .arg(m_oversampling) .arg(m_gain) .arg(m_threshold); @@ -2583,6 +2615,9 @@ } } + int oversampling = attributes.value("oversampling").toUInt(&ok); + if (ok) setOversampling(oversampling); + float gain = attributes.value("gain").toFloat(&ok); if (ok) setGain(gain);
--- a/layer/SpectrogramLayer.h Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/SpectrogramLayer.h Wed Nov 14 14:23:17 2018 +0000 @@ -111,6 +111,9 @@ void setWindowHopLevel(int level); int getWindowHopLevel() const; + void setOversampling(int oversampling); + int getOversampling() const; + void setWindowType(WindowType type); WindowType getWindowType() const; @@ -246,6 +249,7 @@ int m_windowSize; WindowType m_windowType; int m_windowHopLevel; + int m_oversampling; float m_gain; float m_initialGain; float m_threshold; @@ -300,8 +304,7 @@ else return m_windowSize / (1 << (m_windowHopLevel - 1)); } - int getFFTOversampling() const; - int getFFTSize() const; // m_windowSize * getFFTOversampling() + int getFFTSize() const; // m_windowSize * getOversampling() FFTModel *m_fftModel; FFTModel *getFFTModel() const { return m_fftModel; }
--- a/layer/SpectrumLayer.cpp Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/SpectrumLayer.cpp Wed Nov 14 14:23:17 2018 +0000 @@ -39,6 +39,7 @@ m_windowSize(4096), m_windowType(HanningWindow), m_windowHopLevel(3), + m_oversampling(1), m_showPeaks(false), m_newFFTNeeded(true) { @@ -112,18 +113,20 @@ return; } + int fftSize = getFFTSize(); + FFTModel *newFFT = new FFTModel(m_originModel, m_channel, m_windowType, m_windowSize, getWindowIncrement(), - m_windowSize); + fftSize); setSliceableModel(newFFT); m_biasCurve.clear(); - for (int i = 0; i < m_windowSize; ++i) { - m_biasCurve.push_back(1.f / (float(m_windowSize)/2.f)); + for (int i = 0; i < fftSize; ++i) { + m_biasCurve.push_back(1.f / (float(fftSize)/2.f)); } m_newFFTNeeded = false; @@ -135,6 +138,7 @@ PropertyList list = SliceLayer::getProperties(); list.push_back("Window Size"); list.push_back("Window Increment"); + list.push_back("Oversampling"); list.push_back("Show Peak Frequencies"); return list; } @@ -144,6 +148,7 @@ { if (name == "Window Size") return tr("Window Size"); if (name == "Window Increment") return tr("Window Overlap"); + if (name == "Oversampling") return tr("Oversampling"); if (name == "Show Peak Frequencies") return tr("Show Peak Frequencies"); return SliceLayer::getPropertyLabel(name); } @@ -160,6 +165,7 @@ { if (name == "Window Size") return ValueProperty; if (name == "Window Increment") return ValueProperty; + if (name == "Oversampling") return ValueProperty; if (name == "Show Peak Frequencies") return ToggleProperty; return SliceLayer::getPropertyType(name); } @@ -168,7 +174,8 @@ SpectrumLayer::getPropertyGroupName(const PropertyName &name) const { if (name == "Window Size" || - name == "Window Increment") return tr("Window"); + name == "Window Increment" || + name == "Oversampling") return tr("Window"); if (name == "Show Peak Frequencies") return tr("Bins"); return SliceLayer::getPropertyGroupName(name); } @@ -202,6 +209,16 @@ val = m_windowHopLevel; + } else if (name == "Oversampling") { + + *min = 0; + *max = 3; + *deflt = 0; + + val = 0; + int ov = m_oversampling; + while (ov > 1) { ov >>= 1; val ++; } + } else if (name == "Show Peak Frequencies") { return m_showPeaks ? 1 : 0; @@ -232,6 +249,15 @@ case 5: return tr("93.75 %"); } } + if (name == "Oversampling") { + switch (value) { + default: + case 0: return tr("1x"); + case 1: return tr("2x"); + case 2: return tr("4x"); + case 3: return tr("8x"); + } + } return SliceLayer::getPropertyValueLabel(name, value); } @@ -248,6 +274,8 @@ setWindowSize(32 << value); } else if (name == "Window Increment") { setWindowHopLevel(value); + } else if (name == "Oversampling") { + setOversampling(1 << value); } else if (name == "Show Peak Frequencies") { setShowPeaks(value ? true : false); } else { @@ -259,6 +287,16 @@ SpectrumLayer::setWindowSize(int ws) { if (m_windowSize == ws) return; + + SVDEBUG << "setWindowSize: from " << m_windowSize + << " to " << ws << ": updating min and max bins from " + << m_minbin << " and " << m_maxbin << " to "; + + m_minbin = int(round((double(m_minbin) / m_windowSize) * ws)); + m_maxbin = int(round((double(m_maxbin) / m_windowSize) * ws)); + + SVDEBUG << m_minbin << " and " << m_maxbin << endl; + m_windowSize = ws; m_newFFTNeeded = true; emit layerParametersChanged(); @@ -283,6 +321,32 @@ } void +SpectrumLayer::setOversampling(int oversampling) +{ + if (m_oversampling == oversampling) return; + + SVDEBUG << "setOversampling: from " << m_oversampling + << " to " << oversampling << ": updating min and max bins from " + << m_minbin << " and " << m_maxbin << " to "; + + m_minbin = int(round((double(m_minbin) / m_oversampling) * oversampling)); + m_maxbin = int(round((double(m_maxbin) / m_oversampling) * oversampling)); + + SVDEBUG << m_minbin << " and " << m_maxbin << endl; + + m_oversampling = oversampling; + m_newFFTNeeded = true; + + emit layerParametersChanged(); +} + +int +SpectrumLayer::getOversampling() const +{ + return m_oversampling; +} + +void SpectrumLayer::setShowPeaks(bool show) { if (m_showPeaks == show) return; @@ -294,32 +358,91 @@ SpectrumLayer::preferenceChanged(PropertyContainer::PropertyName name) { if (name == "Window Type") { - setWindowType(Preferences::getInstance()->getWindowType()); + auto type = Preferences::getInstance()->getWindowType(); + SVDEBUG << "SpectrumLayer::preferenceChanged: Window type changed to " + << type << endl; + setWindowType(type); return; } } double +SpectrumLayer::getBinForFrequency(double freq) const +{ + if (!m_sliceableModel) return 0; + double bin = (freq * getFFTSize()) / m_sliceableModel->getSampleRate(); + // we assume the frequency of a bin corresponds to the centre of + // its visual range + bin += 0.5; + return bin; +} + +double +SpectrumLayer::getBinForX(const LayerGeometryProvider *v, double x) const +{ + if (!m_sliceableModel) return 0; + double bin = getBinForFrequency(getFrequencyForX(v, x)); + return bin; +} + +double SpectrumLayer::getFrequencyForX(const LayerGeometryProvider *v, double x) const { if (!m_sliceableModel) return 0; - double bin = getBinForX(v, x); + + double fmin = getFrequencyForBin(m_minbin); + + if (m_binScale == LogBins && m_minbin == 0) { + // Avoid too much space going to the first bin, but do so in a + // way that usually avoids us shifting left/right as the + // window size or oversampling ratio change - i.e. base this + // on frequency rather than bin number unless we have a lot of + // very low-resolution content + fmin = getFrequencyForBin(0.8); + if (fmin > 6.0) fmin = 6.0; + } + + double fmax = getFrequencyForBin(m_maxbin); + + double freq = getScalePointForX(v, x, fmin, fmax); + return freq; +} + +double +SpectrumLayer::getFrequencyForBin(double bin) const +{ + if (!m_sliceableModel) return 0; // we assume the frequency of a bin corresponds to the centre of // its visual range bin -= 0.5; - return (m_sliceableModel->getSampleRate() * bin) / - (m_sliceableModel->getHeight() * 2); + double freq = (bin * m_sliceableModel->getSampleRate()) / getFFTSize(); + return freq; +} + +double +SpectrumLayer::getXForBin(const LayerGeometryProvider *v, double bin) const +{ + if (!m_sliceableModel) return 0; + double x = getXForFrequency(v, getFrequencyForBin(bin)); + return x; } double SpectrumLayer::getXForFrequency(const LayerGeometryProvider *v, double freq) const { if (!m_sliceableModel) return 0; - double bin = (freq * m_sliceableModel->getHeight() * 2) / - m_sliceableModel->getSampleRate(); - // we want the centre of the bin range - bin += 0.5; - return getXForBin(v, bin); + + double fmin = getFrequencyForBin(m_minbin); + if (m_binScale == LogBins && m_minbin == 0) { + // See comment in getFrequencyForX above + fmin = getFrequencyForBin(0.8); + if (fmin > 6.0) fmin = 6.0; + } + + double fmax = getFrequencyForBin(m_maxbin); + + double x = getXForScalePoint(v, freq, fmin, fmax); + return x; } bool @@ -427,13 +550,13 @@ double fundamental = getFrequencyForX(v, cursorPos.x()); - int hoffset = 2; - if (m_binScale == LogBins) hoffset = 13; + int hoffset = getHorizontalScaleHeight(v, paint) + + 2 * paint.fontMetrics().height(); PaintAssistant::drawVisibleText(v, paint, cursorPos.x() + 2, v->getPaintHeight() - 2 - hoffset, - QString("%1 Hz").arg(fundamental), + tr("%1 Hz").arg(fundamental), PaintAssistant::OutlinedText); if (Pitch::isFrequencyInMidiRange(fundamental)) { @@ -447,10 +570,6 @@ } double value = getValueForY(v, cursorPos.y()); - double thresh = m_threshold; - double db = thresh; - if (value > 0.0) db = 10.0 * log10(value); - if (db < thresh) db = thresh; PaintAssistant::drawVisibleText(v, paint, xorigin + 2, @@ -458,11 +577,15 @@ QString("%1 V").arg(value), PaintAssistant::OutlinedText); - PaintAssistant::drawVisibleText(v, paint, - xorigin + 2, - cursorPos.y() + 2 + paint.fontMetrics().ascent(), - QString("%1 dBV").arg(db), - PaintAssistant::OutlinedText); + if (value > m_threshold) { + double db = 10.0 * log10(value); + PaintAssistant::drawVisibleText(v, paint, + xorigin + 2, + cursorPos.y() + 2 + + paint.fontMetrics().ascent(), + QString("%1 dBV").arg(db), + PaintAssistant::OutlinedText); + } int harmonic = 2; @@ -518,10 +641,10 @@ QString binstr; QString hzstr; int minfreq = int(lrint((minbin * m_sliceableModel->getSampleRate()) / - m_windowSize)); + getFFTSize())); int maxfreq = int(lrint((std::max(maxbin, minbin) * m_sliceableModel->getSampleRate()) / - m_windowSize)); + getFFTSize())); if (maxbin != minbin) { binstr = tr("%1 - %2").arg(minbin+1).arg(maxbin+1); @@ -602,11 +725,16 @@ FFTModel *fft = dynamic_cast<FFTModel *> (const_cast<DenseThreeDimensionalModel *>(m_sliceableModel)); - double thresh = (pow(10, -6) / m_gain) * (m_windowSize / 2.0); // -60dB adj + double thresh = (pow(10, -6) / m_gain) * (getFFTSize() / 2.0); // -60dB adj int xorigin = getVerticalScaleWidth(v, false, paint) + 1; int scaleHeight = getHorizontalScaleHeight(v, paint); + QPoint localPos; + bool shouldIlluminate = v->shouldIlluminateLocalFeatures(this, localPos); + +// cerr << "shouldIlluminate = " << shouldIlluminate << ", localPos = " << localPos.x() << "," << localPos.y() << endl; + if (fft && m_showPeaks) { // draw peak lines @@ -624,7 +752,8 @@ int peakminbin = 0; int peakmaxbin = fft->getHeight() - 1; double peakmaxfreq = Pitch::getFrequencyForPitch(128); - peakmaxbin = int(((peakmaxfreq * fft->getHeight() * 2) / fft->getSampleRate())); + peakmaxbin = int(((peakmaxfreq * fft->getHeight() * 2) / + fft->getSampleRate())); FFTModel::PeakSet peaks = fft->getPeakFrequencies (FFTModel::MajorPitchAdaptivePeaks, col, peakminbin, peakmaxbin); @@ -633,32 +762,73 @@ getBiasCurve(curve); int cs = int(curve.size()); - std::vector<double> values; + int px = -1; + + int fuzz = ViewManager::scalePixelSize(3); + bool illuminatedSomething = false; - for (int bin = 0; bin < fft->getHeight(); ++bin) { - double value = m_sliceableModel->getValueAt(col, bin); - if (bin < cs) value *= curve[bin]; - values.push_back(value); - } - for (FFTModel::PeakSet::iterator i = peaks.begin(); i != peaks.end(); ++i) { + double freq = i->second; + int x = int(lrint(getXForFrequency(v, freq))); + if (x == px) { + continue; + } + int bin = i->first; // cerr << "bin = " << bin << ", thresh = " << thresh << ", value = " << fft->getMagnitudeAt(col, bin) << endl; - if (!fft->isOverThreshold(col, bin, float(thresh))) continue; + double value = fft->getValueAt(col, bin); + if (value < thresh) continue; + if (bin < cs) value *= curve[bin]; - double freq = i->second; - - int x = int(lrint(getXForFrequency(v, freq))); + double norm = 0.f; + // we need the norm here for colour map; the y coord is + // only used to pick a label height if illuminating the + // local point + double y = getYForValue(v, value, norm); - double norm = 0.f; - (void)getYForValue(v, values[bin], norm); // don't need return value, need norm + QColor colour = mapper.map(norm); + + paint.setPen(QPen(colour, 1)); + paint.drawLine(x, 0, x, v->getPaintHeight() - scaleHeight - 1); - paint.setPen(mapper.map(norm)); - paint.drawLine(x, 0, x, v->getPaintHeight() - scaleHeight - 1); + bool illuminateThis = false; + if (shouldIlluminate && !illuminatedSomething && + std::abs(localPos.x() - x) <= fuzz) { + illuminateThis = true; + } + + if (illuminateThis) { + int labelY = v->getPaintHeight() - + getHorizontalScaleHeight(v, paint) - + paint.fontMetrics().height() * 3; + QString text = tr("%1 Hz").arg(freq); + int lw = paint.fontMetrics().width(text); + int gap = ViewManager::scalePixelSize(3); + double half = double(gap)/2.0; + int labelX = x - lw - gap; + if (labelX < getVerticalScaleWidth(v, false, paint)) { + labelX = x + gap; + } + PaintAssistant::drawVisibleText + (v, paint, labelX, labelY, + text, PaintAssistant::OutlinedText); + if (Pitch::isFrequencyInMidiRange(freq)) { + QString pitchLabel = Pitch::getPitchLabelForFrequency(freq); + PaintAssistant::drawVisibleText + (v, paint, + labelX, labelY + paint.fontMetrics().ascent() + gap, + pitchLabel, PaintAssistant::OutlinedText); + } + paint.fillRect(QRectF(x - half, labelY + gap, gap, gap), + colour); + illuminatedSomething = true; + } + + px = x; } paint.restore(); @@ -746,9 +916,11 @@ { QString s = QString("windowSize=\"%1\" " "windowHopLevel=\"%2\" " - "showPeaks=\"%3\" ") + "oversampling=\"%3\" " + "showPeaks=\"%4\" ") .arg(m_windowSize) .arg(m_windowHopLevel) + .arg(m_oversampling) .arg(m_showPeaks ? "true" : "false"); SliceLayer::toXml(stream, indent, extraAttributes + " " + s); @@ -767,6 +939,9 @@ int windowHopLevel = attributes.value("windowHopLevel").toUInt(&ok); if (ok) setWindowHopLevel(windowHopLevel); + int oversampling = attributes.value("oversampling").toUInt(&ok); + if (ok) setOversampling(oversampling); + bool showPeaks = (attributes.value("showPeaks").trimmed() == "true"); setShowPeaks(showPeaks); }
--- a/layer/SpectrumLayer.h Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/SpectrumLayer.h Wed Nov 14 14:23:17 2018 +0000 @@ -28,8 +28,6 @@ #include <QColor> #include <QMutex> -class FFTModel; - class SpectrumLayer : public SliceLayer, public HorizontalScaleProvider { @@ -46,7 +44,7 @@ std::vector<QRect> &extents) const override; virtual void paintCrosshairs(LayerGeometryProvider *, QPainter &, QPoint) const override; - virtual int getHorizontalScaleHeight(LayerGeometryProvider *, QPainter &) const; + virtual int getHorizontalScaleHeight(LayerGeometryProvider *, QPainter &) const override; virtual void paintHorizontalScale(LayerGeometryProvider *, QPainter &, int xorigin) const; virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const override; @@ -90,12 +88,19 @@ void setWindowHopLevel(int level); int getWindowHopLevel() const { return m_windowHopLevel; } + void setOversampling(int oversampling); + int getOversampling() const; + + int getFFTSize() const { return getWindowSize() * getOversampling(); } + void setWindowType(WindowType type); WindowType getWindowType() const { return m_windowType; } - + void setShowPeaks(bool); bool getShowPeaks() const { return m_showPeaks; } + virtual bool needsTextLabelHeight() const { return true; } + virtual void toXml(QTextStream &stream, QString indent = "", QString extraAttributes = "") const override; @@ -114,6 +119,7 @@ int m_windowSize; WindowType m_windowType; int m_windowHopLevel; + int m_oversampling; bool m_showPeaks; mutable bool m_newFFTNeeded; @@ -121,6 +127,14 @@ void setupFFT(); + virtual double getBinForFrequency(double freq) const; + virtual double getFrequencyForBin(double bin) const; + + virtual double getXForBin(const LayerGeometryProvider *, double bin) + const override; + virtual double getBinForX(const LayerGeometryProvider *, double x) + const override; + virtual void getBiasCurve(BiasCurve &) const override; BiasCurve m_biasCurve;
--- a/layer/TimeInstantLayer.cpp Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/TimeInstantLayer.cpp Wed Nov 14 14:23:17 2018 +0000 @@ -191,7 +191,7 @@ } if (!usePoints.empty()) { - int fuzz = 2; + int fuzz = ViewManager::scalePixelSize(2); int px = v->getXForFrame(usePoints.begin()->frame); if ((px > x && px - x > fuzz) || (px < x && x - px > fuzz + 1)) {
--- a/layer/TimeRulerLayer.cpp Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/TimeRulerLayer.cpp Wed Nov 14 14:23:17 2018 +0000 @@ -113,7 +113,7 @@ dr = abs(v->getXForFrame(right) - x); } - int fuzz = 2; + int fuzz = ViewManager::scalePixelSize(2); if (dl >= 0 && dr >= 0) { if (dl < dr) {
--- a/layer/TimeValueLayer.cpp Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/TimeValueLayer.cpp Wed Nov 14 14:23:17 2018 +0000 @@ -565,7 +565,7 @@ } if (!usePoints.empty()) { - int fuzz = 2; + int fuzz = ViewManager::scalePixelSize(2); int px = v->getXForFrame(usePoints.begin()->frame); if ((px > x && px - x > fuzz) || (px < x && x - px > fuzz + 3)) {
--- a/layer/WaveformLayer.cpp Tue Nov 06 15:42:06 2018 +0000 +++ b/layer/WaveformLayer.cpp Wed Nov 14 14:23:17 2018 +0000 @@ -783,7 +783,7 @@ // Horizontal axis along middle paint->setPen(QPen(midColour, 0)); - paint->drawLine(x0, my, x1, my); + paint->drawLine(QPointF(x0, my + 0.5), QPointF(x1, my + 0.5)); paintChannelScaleGuides(v, paint, rect, ch);
--- a/view/Pane.cpp Tue Nov 06 15:42:06 2018 +0000 +++ b/view/Pane.cpp Wed Nov 14 14:23:17 2018 +0000 @@ -27,6 +27,7 @@ #include "layer/WaveformLayer.h" #include "layer/TimeRulerLayer.h" #include "layer/PaintAssistant.h" +#include "ViewProxy.h" // GF: added so we can propagate the mouse move event to the note layer for context handling. #include "layer/LayerFactory.h" @@ -299,8 +300,8 @@ bool Pane::shouldIlluminateLocalSelection(QPoint &pos, - bool &closeToLeft, - bool &closeToRight) const + bool &closeToLeft, + bool &closeToRight) const { if (m_identifyFeatures && m_manager && @@ -880,6 +881,8 @@ void Pane::drawLayerNames(QRect r, QPainter &paint) { + ViewProxy proxy(this, effectiveDevicePixelRatio()); + int fontHeight = paint.fontMetrics().height(); int fontAscent = paint.fontMetrics().ascent(); @@ -888,6 +891,18 @@ lly -= m_manager->scalePixelSize(20); } + for (LayerList::iterator i = m_layerStack.end(); i != m_layerStack.begin();) { + --i; + int hsh = (*i)->getHorizontalScaleHeight(&proxy, paint); + if (hsh > 0) { + lly -= hsh; + break; + } + if ((*i)->isLayerOpaque()) { + break; + } + } + if (r.y() + r.height() < lly - int(m_layerStack.size()) * fontHeight) { return; } @@ -1822,6 +1837,18 @@ int x1 = r.x() + r.width(); int y1 = r.y() + r.height(); + SVDEBUG << "Pane::zoomToRegion: region defined by pixel rect (" + << r.x() << "," << r.y() << "), " << r.width() << "x" << r.height() + << endl; + + Layer *interactionLayer = getInteractionLayer(); + if (interactionLayer && !(interactionLayer->hasTimeXAxis())) { + SVDEBUG << "Interaction layer does not have time X axis - delegating to it to decide what to do" << endl; + ViewProxy proxy(this, effectiveDevicePixelRatio()); + interactionLayer->zoomToRegion(&proxy, r); + return; + } + sv_frame_t newStartFrame = getFrameForX(x0); sv_frame_t newEndFrame = getFrameForX(x1); sv_frame_t dist = newEndFrame - newStartFrame; @@ -2718,7 +2745,7 @@ if (mode == ViewManager::NavigateMode) { - help = tr("Click and drag to navigate"); + help = tr("Click and drag to navigate; use mouse-wheel or trackpad-scroll to zoom; hold Shift and drag to zoom to an area"); } else if (mode == ViewManager::SelectMode) {