changeset 1238:4d0ca1ab4cd0

Some work to make spectrum layers (and slice layers generally) zoomable in the frequency axis. Also fixes a number of view id mixups in SliceLayer which broke offset calculations for the x axis scale.
author Chris Cannam
date Tue, 07 Feb 2017 14:55:19 +0000
parents 2cc9e0e5df51
children a496986aa61b
files layer/Colour3DPlotLayer.cpp layer/PianoScale.cpp layer/PianoScale.h layer/SliceLayer.cpp layer/SliceLayer.h layer/SpectrumLayer.cpp layer/SpectrumLayer.h widgets/Panner.cpp
diffstat 8 files changed, 342 insertions(+), 288 deletions(-) [+]
line wrap: on
line diff
--- a/layer/Colour3DPlotLayer.cpp	Fri Jan 27 13:19:21 2017 +0000
+++ b/layer/Colour3DPlotLayer.cpp	Tue Feb 07 14:55:19 2017 +0000
@@ -640,7 +640,7 @@
     min = 0;
     max = double(m_model->getHeight());
 
-    logarithmic = false;
+    logarithmic = (m_binScale == BinScale::Log);
     unit = "";
 
     return true;
--- a/layer/PianoScale.cpp	Fri Jan 27 13:19:21 2017 +0000
+++ b/layer/PianoScale.cpp	Tue Feb 07 14:55:19 2017 +0000
@@ -23,6 +23,9 @@
 
 #include "LayerGeometryProvider.h"
 
+#include <iostream>
+using namespace std;
+
 void
 PianoScale::paintPianoVertical(LayerGeometryProvider *v,
 			       QPainter &paint,
@@ -83,3 +86,75 @@
     }
 }
 
+void
+PianoScale::paintPianoHorizontal(LayerGeometryProvider *v,
+                                 const HorizontalScaleProvider *p,
+                                 QPainter &paint,
+                                 QRect r)
+{
+    int x0 = r.x(), y0 = r.y(), x1 = r.x() + r.width(), y1 = r.y() + r.height();
+
+    paint.drawLine(x0, y0, x1, y0);
+
+    int px = x0, ppx = x0;
+    paint.setBrush(paint.pen().color());
+
+    for (int i = 0; i < 128; ++i) {
+
+        double f = Pitch::getFrequencyForPitch(i);
+        int x = int(lrint(p->getXForFrequency(v, f)));
+
+        if (i == 0) {
+            px = ppx = x;
+        }
+        if (i == 1) {
+            ppx = px - (x - px);
+        }
+        
+        if (x < x0) {
+            ppx = px;
+            px = x;
+            continue;
+        }
+        
+        if (x > x1) {
+            break;
+        }
+
+        int n = (i % 12);
+
+        if (n == 1) {
+            // C# -- fill the C from here
+            QColor col = Qt::gray;
+            if (i == 61) { // filling middle C
+                col = Qt::blue;
+                col = col.light(150);
+            }
+            if (x - ppx > 2) {
+                paint.fillRect((px + ppx) / 2 + 1,
+                               y0 + 1,
+                               x - (px + ppx) / 2 - 1,
+                               y1 - y0,
+                               col);
+            }
+        }
+
+        if (n == 1 || n == 3 || n == 6 || n == 8 || n == 10) {
+            // black notes
+            paint.drawLine(x, y0, x, y1);
+            int rw = int(lrint(double(x - px) / 4) * 2);
+            if (rw < 2) rw = 2;
+            paint.drawRect(x - rw/2, (y0 + y1) / 2, rw, (y1 - y0) / 2);
+        } else if (n == 0 || n == 5) {
+            // C, F
+            if (px < x1) {
+                paint.drawLine((x + px) / 2, y0, (x + px) / 2, y1);
+            }
+        }
+        
+        ppx = px;
+        px = x;
+    }
+}
+
+
--- a/layer/PianoScale.h	Fri Jan 27 13:19:21 2017 +0000
+++ b/layer/PianoScale.h	Tue Feb 07 14:55:19 2017 +0000
@@ -26,6 +26,16 @@
 public:
     void paintPianoVertical
     (LayerGeometryProvider *v, QPainter &paint, QRect rect, double minf, double maxf);
+
+    class HorizontalScaleProvider {
+    public:
+        virtual double getFrequencyForX(const LayerGeometryProvider *, double x) const = 0;
+        virtual double getXForFrequency(const LayerGeometryProvider *, double freq) const = 0;
+    };
+    
+    void paintPianoHorizontal
+    (LayerGeometryProvider *v, const HorizontalScaleProvider *p,
+     QPainter &paint, QRect rect);
 };
 
 #endif
--- a/layer/SliceLayer.cpp	Fri Jan 27 13:19:21 2017 +0000
+++ b/layer/SliceLayer.cpp	Tue Feb 07 14:55:19 2017 +0000
@@ -40,6 +40,8 @@
     m_threshold(0.0),
     m_initialThreshold(0.0),
     m_gain(1.0),
+    m_minbin(0),
+    m_maxbin(0),
     m_currentf0(0),
     m_currentf1(0)
 {
@@ -67,7 +69,11 @@
 
     connectSignals(m_sliceableModel);
 
+    m_minbin = 0;
+    m_maxbin = m_sliceableModel->getHeight();
+    
     emit modelReplaced();
+    emit layerParametersChanged();
 }
 
 void
@@ -107,13 +113,10 @@
     maxbin = 0;
     if (!m_sliceableModel) return "";
 
-    int xorigin = m_xorigins[v];
-    int w = v->getPaintWidth() - xorigin - 1;
-    
+    minbin = int(round(getBinForX(v, p.x())));
+    maxbin = int(round(getBinForX(v, p.x() + 1)));
+
     int mh = m_sliceableModel->getHeight();
-    minbin = getBinForX(p.x() - xorigin, mh, w);
-    maxbin = getBinForX(p.x() - xorigin + 1, mh, w);
-
     if (minbin >= mh) minbin = mh - 1;
     if (maxbin >= mh) maxbin = mh - 1;
     if (minbin < 0) minbin = 0;
@@ -133,12 +136,15 @@
 
     if (includeBinDescription) {
 
+        int i0 = minbin - m_minbin;
+        int i1 = maxbin - m_minbin;
+        
         float minvalue = 0.0;
-        if (minbin < int(m_values.size())) minvalue = m_values[minbin];
+        if (in_range_for(m_values, i0)) minvalue = m_values[i0];
 
         float maxvalue = minvalue;
-        if (maxbin < int(m_values.size())) maxvalue = m_values[maxbin];
-        
+        if (in_range_for(m_values, i1)) maxvalue = m_values[i1];
+
         if (minvalue > maxvalue) std::swap(minvalue, maxvalue);
         
         QString binstr;
@@ -180,10 +186,21 @@
 }
 
 double
-SliceLayer::getXForBin(int bin, int count, double w) const
+SliceLayer::getXForBin(const LayerGeometryProvider *v, double bin) 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) {
 
     case LinearBins:
@@ -198,15 +215,27 @@
         x = w - (w * log10(count - bin - 1)) / log10(count);
         break;
     }
-
-    return x;
+    
+    return x + origin;
 }
 
-int
-SliceLayer::getBinForX(double x, int count, double w) const
+double
+SliceLayer::getBinForX(const LayerGeometryProvider *v, double x) const
 {
-    int bin = 0;
+    double 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;
+
+    x = x - origin;
+    if (x < 0) x = 0;
+    
     switch (m_binScale) {
 
     case LinearBins:
@@ -222,20 +251,20 @@
         break;
     }
 
-    return bin;
+    return bin + m_minbin;
 }
 
 double
-SliceLayer::getYForValue(double value, const LayerGeometryProvider *v, double &norm) const
+SliceLayer::getYForValue(const LayerGeometryProvider *v, double value, double &norm) const
 {
     norm = 0.0;
 
-    if (m_yorigins.find(v) == m_yorigins.end()) return 0;
+    if (m_yorigins.find(v->getId()) == m_yorigins.end()) return 0;
 
     value *= m_gain;
 
-    int yorigin = m_yorigins[v];
-    int h = m_heights[v];
+    int yorigin = m_yorigins[v->getId()];
+    int h = m_heights[v->getId()];
     double thresh = getThresholdDb();
 
     double y = 0.0;
@@ -276,14 +305,14 @@
 }
 
 double
-SliceLayer::getValueForY(double y, const LayerGeometryProvider *v) const
+SliceLayer::getValueForY(const LayerGeometryProvider *v, double y) const
 {
     double value = 0.0;
 
-    if (m_yorigins.find(v) == m_yorigins.end()) return value;
+    if (m_yorigins.find(v->getId()) == m_yorigins.end()) return value;
 
-    int yorigin = m_yorigins[v];
-    int h = m_heights[v];
+    int yorigin = m_yorigins[v->getId()];
+    int h = m_heights[v->getId()];
     double thresh = getThresholdDb();
 
     if (h <= 0) return value;
@@ -341,20 +370,26 @@
     int xorigin = getVerticalScaleWidth(v, true, paint) + 1;
     int w = v->getPaintWidth() - xorigin - 1;
 
-    m_xorigins[v] = xorigin; // for use in getFeatureDescription
+    m_xorigins[v->getId()] = xorigin; // for use in getFeatureDescription
     
     int yorigin = v->getPaintHeight() - 20 - paint.fontMetrics().height() - 7;
     int h = yorigin - paint.fontMetrics().height() - 8;
 
-    m_yorigins[v] = yorigin; // for getYForValue etc
-    m_heights[v] = h;
+    m_yorigins[v->getId()] = yorigin; // for getYForValue etc
+    m_heights[v->getId()] = h;
 
     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;
 
     m_values.clear();
@@ -388,7 +423,7 @@
 
     for (int col = col0; col <= col1; ++col) {
         for (int bin = 0; bin < mh; ++bin) {
-            float value = m_sliceableModel->getValueAt(col, bin);
+            float value = m_sliceableModel->getValueAt(col, bin0 + bin);
             if (bin < cs) value *= curve[bin];
             if (m_samplingMode == SamplePeak) {
                 if (value > m_values[bin]) m_values[bin] = value;
@@ -412,18 +447,18 @@
         }
     }
 
-    double nx = xorigin;
+    double nx = getXForBin(v, bin0);
 
     ColourMapper mapper(m_colourMap, 0, 1);
 
     for (int bin = 0; bin < mh; ++bin) {
 
         double x = nx;
-        nx = xorigin + getXForBin(bin + 1, mh, w);
+        nx = getXForBin(v, bin + bin0 + 1);
 
         double value = m_values[bin];
         double norm = 0.0;
-        double y = getYForValue(value, v, norm);
+        double y = getYForValue(v, value, norm);
 
         if (m_plotStyle == PlotLines) {
 
@@ -461,60 +496,20 @@
         paint.drawPath(path);
     }
     paint.restore();
-/*
-    QPoint discard;
-
-    if (v->getViewManager() && v->getViewManager()->shouldShowFrameCount() &&
-        v->shouldIlluminateLocalFeatures(this, discard)) {
-
-        int sampleRate = m_sliceableModel->getSampleRate();
-
-        QString startText = QString("%1 / %2")
-            .arg(QString::fromStdString
-                 (RealTime::frame2RealTime
-                  (f0, sampleRate).toText(true)))
-            .arg(f0);
-
-        QString endText = QString(" %1 / %2")
-            .arg(QString::fromStdString
-                 (RealTime::frame2RealTime
-                  (f1, sampleRate).toText(true)))
-            .arg(f1);
-
-        QString durationText = QString("(%1 / %2) ")
-            .arg(QString::fromStdString
-                 (RealTime::frame2RealTime
-                  (f1 - f0 + 1, sampleRate).toText(true)))
-            .arg(f1 - f0 + 1);
-
-        v->drawVisibleText
-            (paint, xorigin + 5,
-             paint.fontMetrics().ascent() + 5,
-             startText, PaintAssistant::OutlinedText);
-        
-        v->drawVisibleText
-            (paint, xorigin + 5,
-             paint.fontMetrics().ascent() + paint.fontMetrics().height() + 10,
-             endText, PaintAssistant::OutlinedText);
-        
-        v->drawVisibleText
-            (paint, xorigin + 5,
-             paint.fontMetrics().ascent() + 2*paint.fontMetrics().height() + 15,
-             durationText, PaintAssistant::OutlinedText);
-    }
-*/
 }
 
 int
 SliceLayer::getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &paint) const
 {
+    int width;
     if (m_energyScale == LinearScale || m_energyScale == AbsoluteScale) {
-	return std::max(paint.fontMetrics().width("0.0") + 13,
-                        paint.fontMetrics().width("x10-10"));
+	width = std::max(paint.fontMetrics().width("0.0") + 13,
+                         paint.fontMetrics().width("x10-10"));
     } else {
-	return std::max(paint.fontMetrics().width(tr("0dB")),
-			paint.fontMetrics().width(tr("-Inf"))) + 13;
+        width = std::max(paint.fontMetrics().width(tr("0dB")),
+                         paint.fontMetrics().width(tr("-Inf"))) + 13;
     }
+    return width;
 }
 
 void
@@ -630,7 +625,7 @@
 	*max = 50;
         *deflt = 0;
 
-        cerr << "gain is " << m_gain << ", mode is " << m_samplingMode << endl;
+//        cerr << "gain is " << m_gain << ", mode is " << m_samplingMode << endl;
 
 	val = int(lrint(log10(m_gain) * 20.0));
 	if (val < *min) val = *min;
@@ -897,7 +892,7 @@
                  "binScale=\"%5\" "
                  "gain=\"%6\" "
                  "threshold=\"%7\" "
-                 "normalize=\"%8\"")
+                 "normalize=\"%8\" %9")
         .arg(m_colourMap)
 	.arg(m_energyScale)
         .arg(m_samplingMode)
@@ -905,7 +900,11 @@
         .arg(m_binScale)
         .arg(m_gain)
         .arg(m_threshold)
-        .arg(m_normalize ? "true" : "false");
+        .arg(m_normalize ? "true" : "false")
+        .arg(QString("minbin=\"%1\" "
+                     "maxbin=\"%2\"")
+             .arg(m_minbin)
+             .arg(m_maxbin));
 
     SingleColourLayer::toXml(stream, indent, extraAttributes + " " + s);
 }
@@ -944,11 +943,105 @@
 
     bool normalize = (attributes.value("normalize").trimmed() == "true");
     setNormalize(normalize);
+
+    bool alsoOk = false;
+    
+    float min = attributes.value("minbin").toFloat(&ok);
+    float max = attributes.value("maxbin").toFloat(&alsoOk);
+    if (ok && alsoOk) setDisplayExtents(min, max);
 }
 
 bool
-SliceLayer::getValueExtents(double &, double &, bool &, QString &) const
+SliceLayer::getValueExtents(double &min, double &max, bool &logarithmic,
+                            QString &unit) const
 {
-    return false;
+    if (!m_sliceableModel) return false;
+    
+    min = 0;
+    max = double(m_sliceableModel->getHeight());
+
+    logarithmic = (m_binScale == BinScale::LogBins);
+    unit = "";
+
+    return true;
 }
 
+bool
+SliceLayer::getDisplayExtents(double &min, double &max) const
+{
+    if (!m_sliceableModel) return false;
+
+    double hmax = double(m_sliceableModel->getHeight());
+    
+    min = m_minbin;
+    max = m_maxbin;
+    if (max <= min) {
+        min = 0;
+        max = hmax;
+    }
+    if (min < 0) min = 0;
+    if (max > hmax) max = hmax;
+
+    return true;
+}
+
+bool
+SliceLayer::setDisplayExtents(double min, double max)
+{
+    if (!m_sliceableModel) return false;
+
+    m_minbin = int(lrint(min));
+    m_maxbin = int(lrint(max));
+    
+    emit layerParametersChanged();
+    return true;
+}
+
+int
+SliceLayer::getVerticalZoomSteps(int &defaultStep) const
+{
+    if (!m_sliceableModel) return 0;
+
+    defaultStep = 0;
+    int h = m_sliceableModel->getHeight();
+    return h;
+}
+
+int
+SliceLayer::getCurrentVerticalZoomStep() const
+{
+    if (!m_sliceableModel) return 0;
+
+    double min, max;
+    getDisplayExtents(min, max);
+    return m_sliceableModel->getHeight() - int(lrint(max - min));
+}
+
+void
+SliceLayer::setVerticalZoomStep(int step)
+{
+    if (!m_sliceableModel) return;
+
+//    SVDEBUG << "SliceLayer::setVerticalZoomStep(" <<step <<"): before: minbin = " << m_minbin << ", maxbin = " << m_maxbin << endl;
+
+    int dist = m_sliceableModel->getHeight() - step;
+    if (dist < 1) dist = 1;
+    double centre = m_minbin + (m_maxbin - m_minbin) / 2.0;
+    m_minbin = int(lrint(centre - dist/2.0));
+    if (m_minbin < 0) m_minbin = 0;
+    m_maxbin = m_minbin + dist;
+    if (m_maxbin > m_sliceableModel->getHeight()) m_maxbin = m_sliceableModel->getHeight();
+
+//    SVDEBUG << "SliceLayer::setVerticalZoomStep(" <<step <<"):  after: minbin = " << m_minbin << ", maxbin = " << m_maxbin << endl;
+    
+    emit layerParametersChanged();
+}
+
+RangeMapper *
+SliceLayer::getNewVerticalZoomRangeMapper() const
+{
+    if (!m_sliceableModel) return 0;
+
+    return new LinearRangeMapper(0, m_sliceableModel->getHeight(),
+                                 0, m_sliceableModel->getHeight(), "");
+}
--- a/layer/SliceLayer.h	Fri Jan 27 13:19:21 2017 +0000
+++ b/layer/SliceLayer.h	Tue Feb 07 14:55:19 2017 +0000
@@ -65,6 +65,14 @@
     virtual bool getValueExtents(double &min, double &max,
                                  bool &logarithmic, QString &unit) const;
 
+    virtual bool getDisplayExtents(double &min, double &max) const;
+    virtual bool setDisplayExtents(double min, double max);
+
+    virtual int getVerticalZoomSteps(int &defaultStep) const;
+    virtual int getCurrentVerticalZoomStep() const;
+    virtual void setVerticalZoomStep(int);
+    virtual RangeMapper *getNewVerticalZoomRangeMapper() const;
+
     virtual bool hasTimeXAxis() const { return false; }
 
     virtual bool isLayerScrollable(const LayerGeometryProvider *) const { return false; }
@@ -109,11 +117,11 @@
     void modelAboutToBeDeleted(Model *);
 
 protected:
-    virtual double getXForBin(int bin, int totalBins, double w) const;
-    virtual int getBinForX(double x, int totalBins, double w) const;
+    virtual double getXForBin(const LayerGeometryProvider *, double bin) const;
+    virtual double getBinForX(const LayerGeometryProvider *, double x) const;
 
-    virtual double getYForValue(double value, const LayerGeometryProvider *v, double &norm) const;
-    virtual double getValueForY(double y, const LayerGeometryProvider *v) const;
+    virtual double getYForValue(const LayerGeometryProvider *v, double value, double &norm) const;
+    virtual double getValueForY(const LayerGeometryProvider *v, double y) const;
     
     virtual QString getFeatureDescriptionAux(LayerGeometryProvider *v, QPoint &,
                                              bool includeBinDescription,
@@ -140,10 +148,12 @@
     float                             m_threshold;
     float                             m_initialThreshold;
     float                             m_gain;
+    int                               m_minbin;
+    int                               m_maxbin;
     mutable std::vector<int>          m_scalePoints;
-    mutable std::map<const LayerGeometryProvider *, int> m_xorigins;
-    mutable std::map<const LayerGeometryProvider *, int> m_yorigins;
-    mutable std::map<const LayerGeometryProvider *, int> m_heights;
+    mutable std::map<int, int>        m_xorigins; // LayerGeometryProvider id -> x
+    mutable std::map<int, int>        m_yorigins; // LayerGeometryProvider id -> y
+    mutable std::map<int, int>        m_heights;  // LayerGeometryProvider id -> h
     mutable sv_frame_t                m_currentf0;
     mutable sv_frame_t                m_currentf1;
     mutable std::vector<float>        m_values;
--- a/layer/SpectrumLayer.cpp	Fri Jan 27 13:19:21 2017 +0000
+++ b/layer/SpectrumLayer.cpp	Tue Feb 07 14:55:19 2017 +0000
@@ -297,98 +297,29 @@
     }
 }
 
-bool
-SpectrumLayer::getValueExtents(double &, double &, bool &, QString &) const
+double
+SpectrumLayer::getFrequencyForX(const LayerGeometryProvider *v, double x) const
 {
-    return false;
+    if (!m_sliceableModel) return 0;
+    double bin = getBinForX(v, x);
+    return (m_sliceableModel->getSampleRate() * bin) /
+        (m_sliceableModel->getHeight() * 2);
 }
 
 double
-SpectrumLayer::getXForBin(int bin, int totalBins, double w) const
+SpectrumLayer::getXForFrequency(const LayerGeometryProvider *v, double freq) const
 {
-    if (!m_sliceableModel) return SliceLayer::getXForBin(bin, totalBins, w);
-
-    sv_samplerate_t sampleRate = m_sliceableModel->getSampleRate();
-    double binfreq = (sampleRate * bin) / (totalBins * 2);
-    
-    return getXForFrequency(binfreq, w);
-}
-
-int
-SpectrumLayer::getBinForX(double x, int totalBins, double w) const
-{
-    if (!m_sliceableModel) return SliceLayer::getBinForX(x, totalBins, w);
-
-    sv_samplerate_t sampleRate = m_sliceableModel->getSampleRate();
-    double binfreq = getFrequencyForX(x, w);
-
-    return int((binfreq * totalBins * 2) / sampleRate);
-}
-
-double
-SpectrumLayer::getFrequencyForX(double x, double w) const
-{
-    double freq = 0;
     if (!m_sliceableModel) return 0;
-
-    sv_samplerate_t sampleRate = m_sliceableModel->getSampleRate();
-
-    double maxfreq = double(sampleRate) / 2;
-
-    switch (m_binScale) {
-
-    case LinearBins:
-        freq = ((x * maxfreq) / w);
-        break;
-        
-    case LogBins:
-        freq = pow(10.0, (x * log10(maxfreq)) / w);
-        break;
-
-    case InvertedLogBins:
-        freq = maxfreq - pow(10.0, ((w - x) * log10(maxfreq)) / w);
-        break;
-    }
-
-    return freq;
-}
-
-double
-SpectrumLayer::getXForFrequency(double freq, double w) const
-{
-    double x = 0;
-    if (!m_sliceableModel) return x;
-
-    sv_samplerate_t sampleRate = m_sliceableModel->getSampleRate();
-
-    double maxfreq = double(sampleRate) / 2;
-
-    switch (m_binScale) {
-
-    case LinearBins:
-        x = (freq * w) / maxfreq;
-        break;
-        
-    case LogBins:
-        x = (log10(freq) * w) / log10(maxfreq);
-        break;
-
-    case InvertedLogBins:
-        if (maxfreq == freq) x = w;
-        else x = w - (log10(maxfreq - freq) * w) / log10(maxfreq);
-        break;
-    }
-
-    return x;
+    double bin = (freq * m_sliceableModel->getHeight() * 2) /
+        m_sliceableModel->getSampleRate();
+    return getXForBin(v, bin);
 }
 
 bool
 SpectrumLayer::getXScaleValue(const LayerGeometryProvider *v, int x, 
                               double &value, QString &unit) const
 {
-    if (m_xorigins.find(v) == m_xorigins.end()) return false;
-    int xorigin = m_xorigins.find(v)->second;
-    value = getFrequencyForX(x - xorigin, v->getPaintWidth() - xorigin - 1);
+    value = getFrequencyForX(v, x);
     unit = "Hz";
     return true;
 }
@@ -397,7 +328,7 @@
 SpectrumLayer::getYScaleValue(const LayerGeometryProvider *v, int y,
                               double &value, QString &unit) const
 {
-    value = getValueForY(y, v);
+    value = getValueForY(v, y);
 
     if (m_energyScale == dBScale || m_energyScale == MeterScale) {
 
@@ -483,33 +414,34 @@
     ColourMapper mapper(m_colourMap, 0, 1);
     paint.setPen(mapper.getContrastingColour());
 
-    int xorigin = m_xorigins[v];
+    int xorigin = m_xorigins[v->getId()];
     int w = v->getPaintWidth() - xorigin - 1;
     
     paint.drawLine(xorigin, cursorPos.y(), v->getPaintWidth(), cursorPos.y());
     paint.drawLine(cursorPos.x(), cursorPos.y(), cursorPos.x(), v->getPaintHeight());
     
-    double fundamental = getFrequencyForX(cursorPos.x() - xorigin, w);
+    double fundamental = getFrequencyForX(v, cursorPos.x());
 
     int hoffset = 2;
     if (m_binScale == LogBins) hoffset = 13;
 
     PaintAssistant::drawVisibleText(v, paint,
-                       cursorPos.x() + 2,
-                       v->getPaintHeight() - 2 - hoffset,
-                       QString("%1 Hz").arg(fundamental),
-                       PaintAssistant::OutlinedText);
+                                    cursorPos.x() + 2,
+                                    v->getPaintHeight() - 2 - hoffset,
+                                    QString("%1 Hz").arg(fundamental),
+                                    PaintAssistant::OutlinedText);
 
     if (Pitch::isFrequencyInMidiRange(fundamental)) {
         QString pitchLabel = Pitch::getPitchLabelForFrequency(fundamental);
         PaintAssistant::drawVisibleText(v, paint,
-                           cursorPos.x() - paint.fontMetrics().width(pitchLabel) - 2,
-                           v->getPaintHeight() - 2 - hoffset,
-                           pitchLabel,
-                           PaintAssistant::OutlinedText);
+                                        cursorPos.x() -
+                                        paint.fontMetrics().width(pitchLabel) - 2,
+                                        v->getPaintHeight() - 2 - hoffset,
+                                        pitchLabel,
+                                        PaintAssistant::OutlinedText);
     }
 
-    double value = getValueForY(cursorPos.y(), v);
+    double value = getValueForY(v, cursorPos.y());
     double thresh = m_threshold;
     double db = thresh;
     if (value > 0.0) db = 10.0 * log10(value);
@@ -531,7 +463,7 @@
 
     while (harmonic < 100) {
 
-        int hx = int(lrint(getXForFrequency(fundamental * harmonic, w)));
+        int hx = int(lrint(getXForFrequency(v, fundamental * harmonic)));
         hx += xorigin;
 
         if (hx < xorigin || hx > v->getPaintWidth()) break;
@@ -568,12 +500,15 @@
 
     if (genericDesc == "") return "";
 
-    double minvalue = 0.f;
-    if (minbin < int(m_values.size())) minvalue = m_values[minbin];
+    int i0 = minbin - m_minbin;
+    int i1 = maxbin - m_minbin;
+        
+    float minvalue = 0.0;
+    if (in_range_for(m_values, i0)) minvalue = m_values[i0];
 
-    double maxvalue = minvalue;
-    if (maxbin < int(m_values.size())) maxvalue = m_values[maxbin];
-        
+    float maxvalue = minvalue;
+    if (in_range_for(m_values, i1)) maxvalue = m_values[i1];
+    
     if (minvalue > maxvalue) std::swap(minvalue, maxvalue);
     
     QString binstr;
@@ -718,10 +653,10 @@
             
             double freq = i->second;
           
-            int x = int(lrint(getXForFrequency(freq, w)));
+            int x = int(lrint(getXForFrequency(v, freq)));
 
             double norm = 0.f;
-            (void)getYForValue(values[bin], v, norm); // don't need return value, need norm
+            (void)getYForValue(v, values[bin], norm); // don't need return value, need norm
 
             paint.setPen(mapper.map(norm));
             paint.drawLine(xorigin + x, 0, xorigin + x, v->getPaintHeight() - pkh - 1);
@@ -733,87 +668,18 @@
     SliceLayer::paint(v, paint, rect);
 
     //!!! All of this stuff relating to depicting frequencies
-    //(keyboard, crosshairs etc) should be applicable to any slice
-    //layer whose model has a vertical scale unit of Hz.  However, the
-    //dense 3d model at the moment doesn't record its vertical scale
-    //unit -- we need to fix that and hoist this code as appropriate.
-    //Same really goes for any code in SpectrogramLayer that could be
-    //relevant to Colour3DPlotLayer with unit Hz, but that's a bigger
-    //proposition.
+    // (keyboard, crosshairs etc) should be applicable to any slice
+    // layer whose model has a vertical scale unit of Hz.  However,
+    // the dense 3d model at the moment doesn't record its vertical
+    // scale unit -- we need to fix that and hoist this code as
+    // appropriate.  Same really goes for any code in SpectrogramLayer
+    // that could be relevant to Colour3DPlotLayer with unit Hz, but
+    // that's a bigger proposition.
 
-//    if (m_binScale == LogBins) {
+    int h = v->getPaintHeight();
 
-//        int pkh = 10;
-        int h = v->getPaintHeight();
-
-        // piano keyboard
-
-        //!!! todo: move to PianoScale::paintPianoHorizontal
-        
-	paint.drawLine(xorigin, h - pkh - 1, w + xorigin, h - pkh - 1);
-
-	int px = xorigin, ppx = xorigin;
-	paint.setBrush(paint.pen().color());
-
-	for (int i = 0; i < 128; ++i) {
-
-	    double f = Pitch::getFrequencyForPitch(i);
-	    int x = int(lrint(getXForFrequency(f, w)));
-                           
-            x += xorigin;
-
-            if (i == 0) {
-                px = ppx = x;
-            }
-            if (i == 1) {
-                ppx = px - (x - px);
-            }
-
-            if (x < xorigin) {
-                ppx = px;
-                px = x;
-                continue;
-            }
-                
-            if (x > w) {
-                break;
-            }
-
-	    int n = (i % 12);
-
-            if (n == 1) {
-                // C# -- fill the C from here
-                QColor col = Qt::gray;
-                if (i == 61) { // filling middle C
-                    col = Qt::blue;
-                    col = col.light(150);
-                }
-                if (x - ppx > 2) {
-                    paint.fillRect((px + ppx) / 2 + 1,
-                                   h - pkh,
-                                   x - (px + ppx) / 2 - 1,
-                                   pkh,
-                                   col);
-                }
-            }
-
-	    if (n == 1 || n == 3 || n == 6 || n == 8 || n == 10) {
-		// black notes
-		paint.drawLine(x, h - pkh, x, h);
-		int rw = int(lrint(double(x - px) / 4) * 2);
-		if (rw < 2) rw = 2;
-		paint.drawRect(x - rw/2, h - pkh, rw, pkh/2);
-	    } else if (n == 0 || n == 5) {
-		// C, F
-		if (px < w) {
-		    paint.drawLine((x + px) / 2, h - pkh, (x + px) / 2, h);
-		}
-	    }
-
-            ppx = px;
-	    px = x;
-	}
-//    }
+    PianoScale().paintPianoHorizontal
+        (v, this, paint, QRect(xorigin, h - pkh - 1, w + xorigin, pkh));
 
     paint.restore();
 }
--- a/layer/SpectrumLayer.h	Fri Jan 27 13:19:21 2017 +0000
+++ b/layer/SpectrumLayer.h	Tue Feb 07 14:55:19 2017 +0000
@@ -14,8 +14,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _SPECTRUM_LAYER_H_
-#define _SPECTRUM_LAYER_H_
+#ifndef SV_SPECTRUM_LAYER_H
+#define SV_SPECTRUM_LAYER_H
 
 #include "SliceLayer.h"
 
@@ -23,12 +23,15 @@
 
 #include "data/model/DenseTimeValueModel.h"
 
+#include "PianoScale.h"
+
 #include <QColor>
 #include <QMutex>
 
 class FFTModel;
 
-class SpectrumLayer : public SliceLayer
+class SpectrumLayer : public SliceLayer,
+                      public PianoScale::HorizontalScaleProvider
 {
     Q_OBJECT
 
@@ -64,9 +67,6 @@
     virtual void setProperty(const PropertyName &, int value);
     virtual void setProperties(const QXmlAttributes &);
 
-    virtual bool getValueExtents(double &min, double &max,
-                                 bool &logarithmic, QString &unit) const;
-
     virtual bool getXScaleValue(const LayerGeometryProvider *v, int x,
                                 double &value, QString &unit) const;
 
@@ -96,15 +96,15 @@
     virtual void toXml(QTextStream &stream, QString indent = "",
                        QString extraAttributes = "") const;
 
+    virtual double getFrequencyForX(const LayerGeometryProvider *,
+                                    double x) const override;
+    virtual double getXForFrequency(const LayerGeometryProvider *,
+                                    double freq) const override;
+
 protected slots:
     void preferenceChanged(PropertyContainer::PropertyName name);
 
 protected:
-    // make this SliceLayer method unavailable to the general public
-//    virtual void setModel(DenseThreeDimensionalModel *model) {
-//        SliceLayer::setModel(model);
-//    }
-
     DenseTimeValueModel    *m_originModel;
     int                     m_channel;
     bool                    m_channelSet;
@@ -121,12 +121,6 @@
     virtual void getBiasCurve(BiasCurve &) const;
     BiasCurve m_biasCurve;
 
-    virtual double getXForBin(int bin, int totalBins, double w) const;
-    virtual int getBinForX(double x, int totalBins, double w) const;
-
-    double getFrequencyForX(double x, double w) const;
-    double getXForFrequency(double freq, double w) const;
-
     int getWindowIncrement() const {
         if (m_windowHopLevel == 0) return m_windowSize;
         else if (m_windowHopLevel == 1) return (m_windowSize * 3) / 4;
--- a/widgets/Panner.cpp	Fri Jan 27 13:19:21 2017 +0000
+++ b/widgets/Panner.cpp	Tue Feb 07 14:55:19 2017 +0000
@@ -20,6 +20,8 @@
 #include <QWheelEvent>
 #include <QPainter>
 
+#include "WidgetScale.h"
+
 #include <iostream>
 #include <cmath>
 
@@ -156,9 +158,13 @@
     QColor bg(palette().background().color());
     bg.setAlpha(m_backgroundAlpha);
 
-    paint.setPen(palette().dark().color());
+    int penWidth = WidgetScale::scalePixelSize(1);
+    if (penWidth < 1) penWidth = 1;
+    paint.setPen(QPen(palette().dark().color(), penWidth));
+    
     paint.setBrush(bg);
-    paint.drawRect(0, 0, width()-1, height()-1);
+    paint.drawRect(penWidth/2, penWidth/2,
+                   width()-penWidth/2-1, height()-penWidth/2-1);
 
     QColor hl(m_thumbColour);
     hl.setAlpha(m_thumbAlpha);