changeset 1353:86429ff00f05

Merge from branch zoom
author Chris Cannam
date Wed, 10 Oct 2018 08:44:27 +0100
parents 9fb7133dd818 (current diff) 4949061fcb8c (diff)
children 40b9a495a0e0
files view/View.cpp
diffstat 20 files changed, 926 insertions(+), 607 deletions(-) [+]
line wrap: on
line diff
--- a/files.pri	Wed Oct 03 12:59:55 2018 +0100
+++ b/files.pri	Wed Oct 10 08:44:27 2018 +0100
@@ -1,9 +1,9 @@
 
 SVGUI_HEADERS += \
            layer/Colour3DPlotLayer.h \
-	   layer/Colour3DPlotRenderer.h \
-	   layer/ColourDatabase.h \
-	   layer/ColourMapper.h \
+           layer/Colour3DPlotRenderer.h \
+           layer/ColourDatabase.h \
+           layer/ColourMapper.h \
            layer/ColourScale.h \
            layer/ColourScaleLayer.h \
            layer/FlexiNoteLayer.h \
@@ -36,14 +36,14 @@
            layer/TimeValueLayer.h \
            layer/VerticalScaleLayer.h \
            layer/WaveformLayer.h \
-	   view/AlignmentView.h \
+           view/AlignmentView.h \
            view/Overview.h \
            view/Pane.h \
            view/PaneStack.h \
            view/View.h \
            view/ViewManager.h \
            view/ViewProxy.h \
-	   widgets/ActivityLog.h \
+           widgets/ActivityLog.h \
            widgets/AudioDial.h \
            widgets/ClickableLabel.h \
            widgets/ColourComboBox.h \
@@ -95,10 +95,10 @@
 
 SVGUI_SOURCES += \
            layer/Colour3DPlotLayer.cpp \
-	   layer/Colour3DPlotRenderer.cpp \
-	   layer/ColourDatabase.cpp \
-	   layer/ColourMapper.cpp \
-	   layer/ColourScale.cpp \
+           layer/Colour3DPlotRenderer.cpp \
+           layer/ColourDatabase.cpp \
+           layer/ColourMapper.cpp \
+           layer/ColourScale.cpp \
            layer/FlexiNoteLayer.cpp \
            layer/HorizontalFrequencyScale.cpp \
            layer/ImageLayer.cpp \
@@ -124,13 +124,13 @@
            layer/TimeRulerLayer.cpp \
            layer/TimeValueLayer.cpp \
            layer/WaveformLayer.cpp \
-	   view/AlignmentView.cpp \
+           view/AlignmentView.cpp \
            view/Overview.cpp \
            view/Pane.cpp \
            view/PaneStack.cpp \
            view/View.cpp \
            view/ViewManager.cpp \
-	   widgets/ActivityLog.cpp \
+           widgets/ActivityLog.cpp \
            widgets/AudioDial.cpp \
            widgets/ColourComboBox.cpp \
            widgets/ColourMapComboBox.cpp \
--- a/layer/Colour3DPlotRenderer.cpp	Wed Oct 03 12:59:55 2018 +0100
+++ b/layer/Colour3DPlotRenderer.cpp	Wed Oct 10 08:44:27 2018 +0100
@@ -32,7 +32,10 @@
 
 #include <vector>
 
-//#define DEBUG_COLOUR_PLOT_REPAINT 1
+#include <utility>
+using namespace std::rel_ops;
+
+#define DEBUG_COLOUR_PLOT_REPAINT 1
 
 using namespace std;
 
@@ -315,7 +318,7 @@
     }
 
     int binResolution = model->getResolution();
-    int zoomLevel = v->getZoomLevel();
+    ZoomLevel zoomLevel = v->getZoomLevel();
     sv_samplerate_t modelRate = model->getSampleRate();
 
     double rateRatio = v->getViewManager()->getMainModelSampleRate() / modelRate;
@@ -332,12 +335,14 @@
         // explicitly requested opaque & sufficiently zoomed-in
         
         if (model->getHeight() * 3 < v->getPaintHeight() &&
-            relativeBinResolution >= 3 * zoomLevel) {
+            zoomLevel < ZoomLevel(ZoomLevel::FramesPerPixel,
+                                  int(round(relativeBinResolution / 3)))) {
             return DirectTranslucent;
         }
     }
 
-    if (relativeBinResolution > zoomLevel) {
+    if (ZoomLevel(ZoomLevel::FramesPerPixel,
+                  int(round(relativeBinResolution))) > zoomLevel) {
         return DrawBufferBinResolution;
     } else {
         return DrawBufferPixelResolution;
@@ -555,12 +560,12 @@
     if (m_params.binDisplay == BinDisplay::PeakFrequencies) return;
     if (m_params.colourScale.getScale() == ColourScaleType::Phase) return;
     
-    int zoomLevel = v->getZoomLevel();
+    ZoomLevel zoomLevel = v->getZoomLevel();
     int binResolution = model->getResolution();
     
     for (int ix = 0; in_range_for(m_sources.peakCaches, ix); ++ix) {
         int bpp = m_sources.peakCaches[ix]->getColumnsPerPeak();
-        int equivZoom = binResolution * bpp;
+        ZoomLevel equivZoom(ZoomLevel::FramesPerPixel, binResolution * bpp);
         if (zoomLevel >= equivZoom) {
             // this peak cache would work, though it might not be best
             if (bpp > binsPerPeak) {
@@ -759,6 +764,11 @@
     int drawBufferWidth;
     int binResolution = model->getResolution();
 
+    // These loops should eventually terminate provided that
+    // getFrameForX always returns a multiple of the zoom level,
+    // i.e. there is some x for which getFrameForX(x) == 0 and
+    // subsequent return values are equally spaced
+    
     for (int x = x0; ; --x) {
         sv_frame_t f = v->getFrameForX(x);
         if ((f / binResolution) * binResolution == f) {
@@ -769,6 +779,7 @@
             }
         }
     }
+    
     for (int x = x0 + repaintWidth; ; ++x) {
         sv_frame_t f = v->getFrameForX(x);
         if ((f / binResolution) * binResolution == f) {
@@ -779,6 +790,7 @@
             }
         }
     }
+
     drawBufferWidth = int
         ((rightBoundaryFrame - leftBoundaryFrame) / binResolution);
     
--- a/layer/LayerGeometryProvider.h	Wed Oct 03 12:59:55 2018 +0100
+++ b/layer/LayerGeometryProvider.h	Wed Oct 10 08:44:27 2018 +0100
@@ -12,10 +12,11 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef LAYER_GEOMETRY_PROVIDER_H
-#define LAYER_GEOMETRY_PROVIDER_H
+#ifndef SV_LAYER_GEOMETRY_PROVIDER_H
+#define SV_LAYER_GEOMETRY_PROVIDER_H
 
 #include "base/BaseTypes.h"
+#include "base/ZoomLevel.h"
 
 #include <QMutex>
 #include <QMutexLocker>
@@ -147,9 +148,10 @@
                                  bool &log) const = 0;
 
     /**
-     * Return the zoom level, i.e. the number of frames per pixel
+     * Return the zoom level, i.e. the number of frames per pixel or
+     * pixels per frame
      */
-    virtual int getZoomLevel() const = 0;
+    virtual ZoomLevel getZoomLevel() const = 0;
 
     /**
      * To be called from a layer, to obtain the extent of the surface
--- a/layer/ScrollableImageCache.h	Wed Oct 03 12:59:55 2018 +0100
+++ b/layer/ScrollableImageCache.h	Wed Oct 10 08:44:27 2018 +0100
@@ -40,8 +40,7 @@
     ScrollableImageCache() :
         m_validLeft(0),
         m_validWidth(0),
-        m_startFrame(0),
-        m_zoomLevel(0)
+        m_startFrame(0)
     {}
 
     void invalidate() {
@@ -83,7 +82,7 @@
         return QRect(m_validLeft, 0, m_validWidth, m_image.height());
     }
     
-    int getZoomLevel() const {
+    ZoomLevel getZoomLevel() const {
         return m_zoomLevel;
     }
 
@@ -93,7 +92,8 @@
      * invalidate the cache here is the only thing the zoom level is
      * used for.)
      */
-    void setZoomLevel(int zoom) {
+    void setZoomLevel(ZoomLevel zoom) {
+        using namespace std::rel_ops;
         if (m_zoomLevel != zoom) {
             m_zoomLevel = zoom;
             invalidate();
@@ -157,7 +157,7 @@
     int m_validLeft;
     int m_validWidth;
     sv_frame_t m_startFrame;
-    int m_zoomLevel;
+    ZoomLevel m_zoomLevel;
 };
 
 #endif
--- a/layer/ScrollableMagRangeCache.h	Wed Oct 03 12:59:55 2018 +0100
+++ b/layer/ScrollableMagRangeCache.h	Wed Oct 10 08:44:27 2018 +0100
@@ -36,8 +36,7 @@
 {
 public:
     ScrollableMagRangeCache() :
-        m_startFrame(0),
-        m_zoomLevel(0)
+        m_startFrame(0)
     {}
 
     void invalidate() {
@@ -58,7 +57,7 @@
         }
     }
         
-    int getZoomLevel() const {
+    ZoomLevel getZoomLevel() const {
         return m_zoomLevel;
     }
 
@@ -68,7 +67,8 @@
      * invalidate the cache here is the only thing the zoom level is
      * used for.)
      */
-    void setZoomLevel(int zoom) {
+    void setZoomLevel(ZoomLevel zoom) {
+        using namespace std::rel_ops;
         if (m_zoomLevel != zoom) {
             m_zoomLevel = zoom;
             invalidate();
@@ -133,7 +133,7 @@
 private:
     std::vector<MagnitudeRange> m_ranges;
     sv_frame_t m_startFrame;
-    int m_zoomLevel;
+    ZoomLevel m_zoomLevel;
 };
 
 #endif
--- a/layer/TimeRulerLayer.cpp	Wed Oct 03 12:59:55 2018 +0100
+++ b/layer/TimeRulerLayer.cpp	Wed Oct 10 08:44:27 2018 +0100
@@ -19,6 +19,7 @@
 
 #include "data/model/Model.h"
 #include "base/RealTime.h"
+#include "base/Preferences.h"
 #include "view/View.h"
 
 #include "ColourDatabase.h"
@@ -59,8 +60,8 @@
     }
 
     bool q;
-    int tick = getMajorTickSpacing(v, q);
-    RealTime rtick = RealTime::fromMilliseconds(tick);
+    int64_t tickUSec = getMajorTickUSec(v, q);
+    RealTime rtick = RealTime::fromMicroseconds(tickUSec);
     sv_samplerate_t rate = m_model->getSampleRate();
     
     RealTime rt = RealTime::frame2RealTime(frame, rate);
@@ -141,20 +142,20 @@
     return true;
 }
 
-int
-TimeRulerLayer::getMajorTickSpacing(LayerGeometryProvider *v, bool &quarterTicks) const
+int64_t
+TimeRulerLayer::getMajorTickUSec(LayerGeometryProvider *v,
+                                 bool &quarterTicks) const
 {
-    // return value is in milliseconds
-
-    if (!m_model || !v) return 1000;
+    // return value is in microseconds
+    if (!m_model || !v) return 1000 * 1000;
 
     sv_samplerate_t sampleRate = m_model->getSampleRate();
-    if (!sampleRate) return 1000;
+    if (!sampleRate) return 1000 * 1000;
 
     sv_frame_t startFrame = v->getStartFrame();
     sv_frame_t endFrame = v->getEndFrame();
 
-    int minPixelSpacing = 50;
+    int minPixelSpacing = ViewManager::scalePixelSize(50);
 
     RealTime rtStart = RealTime::frame2RealTime(startFrame, sampleRate);
     RealTime rtEnd = RealTime::frame2RealTime(endFrame, sampleRate);
@@ -163,43 +164,82 @@
     if (count < 1) count = 1;
     RealTime rtGap = (rtEnd - rtStart) / count;
 
-    int incms;
+    int64_t incus;
     quarterTicks = false;
 
     if (rtGap.sec > 0) {
-        incms = 1000;
+        incus = 1000 * 1000;
         int s = rtGap.sec;
-        if (s > 0) { incms *= 5; s /= 5; }
-        if (s > 0) { incms *= 2; s /= 2; }
-        if (s > 0) { incms *= 6; s /= 6; quarterTicks = true; }
-        if (s > 0) { incms *= 5; s /= 5; quarterTicks = false; }
-        if (s > 0) { incms *= 2; s /= 2; }
-        if (s > 0) { incms *= 6; s /= 6; quarterTicks = true; }
+        if (s > 0) { incus *= 5; s /= 5; }
+        if (s > 0) { incus *= 2; s /= 2; }
+        if (s > 0) { incus *= 6; s /= 6; quarterTicks = true; }
+        if (s > 0) { incus *= 5; s /= 5; quarterTicks = false; }
+        if (s > 0) { incus *= 2; s /= 2; }
+        if (s > 0) { incus *= 6; s /= 6; quarterTicks = true; }
         while (s > 0) {
-            incms *= 10;
+            incus *= 10;
             s /= 10;
             quarterTicks = false;
         }
+    } else if (rtGap.msec() > 0) {
+        incus = 1000;
+        int ms = rtGap.msec();
+        if (ms > 0) { incus *= 10; ms /= 10; }
+        if (ms > 0) { incus *= 10; ms /= 10; }
+        if (ms > 0) { incus *= 5; ms /= 5; }
+        if (ms > 0) { incus *= 2; ms /= 2; }
     } else {
-        incms = 1;
-        int ms = rtGap.msec();
-//        cerr << "rtGap.msec = " << ms << ", rtGap = " << rtGap << ", count = " << count << endl;
-//        cerr << "startFrame = " << startFrame << ", endFrame = " << endFrame << " rtStart = " << rtStart << ", rtEnd = " << rtEnd << endl;
-        if (ms > 0) { incms *= 10; ms /= 10; }
-        if (ms > 0) { incms *= 10; ms /= 10; }
-        if (ms > 0) { incms *= 5; ms /= 5; }
-        if (ms > 0) { incms *= 2; ms /= 2; }
+        incus = 1;
+        int us = rtGap.usec();
+        if (us > 0) { incus *= 10; us /= 10; }
+        if (us > 0) { incus *= 10; us /= 10; }
+        if (us > 0) { incus *= 5; us /= 5; }
+        if (us > 0) { incus *= 2; us /= 2; }
     }
 
-    return incms;
+    return incus;
+}
+
+int
+TimeRulerLayer::getXForUSec(LayerGeometryProvider *v, double us) const
+{
+    sv_samplerate_t sampleRate = m_model->getSampleRate();
+    double dframe = (us * sampleRate) / 1000000.0;
+    double eps = 1e-7;
+    sv_frame_t frame = sv_frame_t(floor(dframe + eps));
+    int x;
+
+    ZoomLevel zoom = v->getZoomLevel();
+
+    if (zoom.zone == ZoomLevel::FramesPerPixel) {
+            
+        frame /= zoom.level;
+        frame *= zoom.level; // so frame corresponds to an exact pixel
+        
+        x = v->getXForFrame(frame);
+        
+    } else {
+
+        double off = dframe - double(frame);
+        int x0 = v->getXForFrame(frame);
+        int x1 = v->getXForFrame(frame + 1);
+        
+        x = int(x0 + off * (x1 - x0));
+    }
+
+#ifdef DEBUG_TIME_RULER_LAYER
+    cerr << "Considering frame = " << frame << ", x = " << x << endl;
+#endif
+        
+    return x;
 }
 
 void
 TimeRulerLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
 #ifdef DEBUG_TIME_RULER_LAYER
-    SVDEBUG << "TimeRulerLayer::paint (" << rect.x() << "," << rect.y()
-              << ") [" << rect.width() << "x" << rect.height() << "]" << endl;
+    SVCERR << "TimeRulerLayer::paint (" << rect.x() << "," << rect.y()
+           << ") [" << rect.width() << "x" << rect.height() << "]" << endl;
 #endif
     
     if (!m_model || !m_model->isOK()) return;
@@ -210,27 +250,36 @@
     sv_frame_t startFrame = v->getFrameForX(rect.x() - 50);
 
 #ifdef DEBUG_TIME_RULER_LAYER
-    cerr << "start frame = " << startFrame << endl;
+    SVCERR << "start frame = " << startFrame << endl;
 #endif
 
     bool quarter = false;
-    int incms = getMajorTickSpacing(v, quarter);
-
-    int ms = int(lrint(1000.0 * (double(startFrame) / double(sampleRate))));
-    ms = (ms / incms) * incms - incms;
+    int64_t incus = getMajorTickUSec(v, quarter);
+    int64_t us = int64_t(floor(1000.0 * 1000.0 * (double(startFrame) /
+                                                  double(sampleRate))));
+    us = (us / incus) * incus - incus;
 
 #ifdef DEBUG_TIME_RULER_LAYER
-    cerr << "start ms = " << ms << " at step " << incms << endl;
+    SVCERR << "start us = " << us << " at step " << incus << endl;
 #endif
 
+    Preferences *prefs = Preferences::getInstance();
+    auto origTimeTextMode = prefs->getTimeToTextMode();
+    if (incus < 1000) {
+        // Temporarily switch to usec display mode (if we aren't using
+        // it already)
+        prefs->blockSignals(true);
+        prefs->setTimeToTextMode(Preferences::TimeToTextUs);
+    }
+    
     // Calculate the number of ticks per increment -- approximate
     // values for x and frame counts here will do, no rounding issue.
-    // We always use the exact incms in our calculations for where to
+    // We always use the exact incus in our calculations for where to
     // draw the actual ticks or lines.
 
     int minPixelSpacing = 50;
-    sv_frame_t incFrame = lrint((incms * sampleRate) / 1000);
-    int incX = int(incFrame / v->getZoomLevel());
+    sv_frame_t incFrame = lrint((double(incus) * sampleRate) / 1000000);
+    int incX = int(round(v->getZoomLevel().framesToPixels(double(incFrame))));
     int ticks = 10;
     if (incX < minPixelSpacing * 2) {
         ticks = quarter ? 4 : 5;
@@ -242,10 +291,7 @@
 
     // Do not label time zero - we now overlay an opaque area over
     // time < 0 which would cut it in half
-    int minlabel = 1; // ms
-
-    // used for a sanity check
-    sv_frame_t prevframe = 0;
+    int minlabel = 1; // us
     
     while (1) {
 
@@ -254,40 +300,27 @@
         // a different pixel when scrolling a small amount and
         // re-drawing with a different start frame).
 
-        double dms = ms;
-        sv_frame_t frame = lrint((dms * sampleRate) / 1000.0);
-        frame /= v->getZoomLevel();
-        frame *= v->getZoomLevel(); // so frame corresponds to an exact pixel
+        double dus = double(us);
 
-        if (frame == prevframe && prevframe != 0) {
-            cerr << "ERROR: frame == prevframe (== " << frame
-                 << ") in TimeRulerLayer::paint" << endl;
-            throw std::logic_error("frame == prevframe in TimeRulerLayer::paint");
-        }
-        prevframe = frame;
-        
-        int x = v->getXForFrame(frame);
-
-#ifdef DEBUG_TIME_RULER_LAYER
-        cerr << "Considering frame = " << frame << ", x = " << x << endl;
-#endif
+        int x = getXForUSec(v, dus);
 
         if (x >= rect.x() + rect.width() + 50) {
 #ifdef DEBUG_TIME_RULER_LAYER
-            cerr << "X well out of range, ending here" << endl;
+            SVCERR << "X well out of range, ending here" << endl;
 #endif
             break;
         }
 
-        if (x >= rect.x() - 50 && ms >= minlabel) {
+        if (x >= rect.x() - 50 && us >= minlabel) {
 
-            RealTime rt = RealTime::fromMilliseconds(ms);
+            RealTime rt = RealTime::fromMicroseconds(us);
 
 #ifdef DEBUG_TIME_RULER_LAYER
-            cerr << "X in range, drawing line here for time " << rt.toText() << endl;
+            SVCERR << "X in range, drawing line here for time " << rt.toText() << " (usec = " << us << ")" << endl;
 #endif
 
             QString text(QString::fromStdString(rt.toText()));
+            
             QFontMetrics metrics = paint.fontMetrics();
             int tw = metrics.width(text);
 
@@ -295,7 +328,7 @@
                 (x < rect.x() - tw/2 ||
                  x >= rect.x() + rect.width() + tw/2)) {
 #ifdef DEBUG_TIME_RULER_LAYER
-                cerr << "hm, maybe X isn't in range after all (x = " << x << ", tw = " << tw << ", rect.x() = " << rect.x() << ", rect.width() = " << rect.width() << ")" << endl;
+                SVCERR << "hm, maybe X isn't in range after all (x = " << x << ", tw = " << tw << ", rect.x() = " << rect.x() << ", rect.width() = " << rect.width() << ")" << endl;
 #endif
             }
 
@@ -335,22 +368,19 @@
 
         for (int i = 1; i < ticks; ++i) {
 
-            dms = ms + (i * double(incms)) / ticks;
-            frame = lrint((dms * sampleRate) / 1000.0);
-            frame /= v->getZoomLevel();
-            frame *= v->getZoomLevel(); // exact pixel as above
+            dus = double(us) + (i * double(incus)) / ticks;
 
-            x = v->getXForFrame(frame);
+            x = getXForUSec(v, dus);
 
             if (x < rect.x() || x >= rect.x() + rect.width()) {
 #ifdef DEBUG_TIME_RULER_LAYER
-//                cerr << "tick " << i << ": X out of range, going on to next tick" << endl;
+//                SVCERR << "tick " << i << ": X out of range, going on to next tick" << endl;
 #endif
                 continue;
             }
 
 #ifdef DEBUG_TIME_RULER_LAYER
-            cerr << "tick " << i << " in range, drawing at " << x << endl;
+            SVCERR << "tick " << i << " in range, drawing at " << x << endl;
 #endif
 
             int sz = 5;
@@ -367,8 +397,11 @@
             paint.drawLine(x, v->getPaintHeight() - sz - 1, x, v->getPaintHeight() - 1);
         }
 
-        ms += incms;
+        us += incus;
     }
+    
+    prefs->setTimeToTextMode(origTimeTextMode);
+    prefs->blockSignals(false);
 
     paint.restore();
 }
--- a/layer/TimeRulerLayer.h	Wed Oct 03 12:59:55 2018 +0100
+++ b/layer/TimeRulerLayer.h	Wed Oct 10 08:44:27 2018 +0100
@@ -68,7 +68,8 @@
 
     virtual int getDefaultColourHint(bool dark, bool &impose);
 
-    int getMajorTickSpacing(LayerGeometryProvider *, bool &quarterTicks) const;
+    int64_t getMajorTickUSec(LayerGeometryProvider *, bool &quarterTicks) const;
+    int getXForUSec(LayerGeometryProvider *, double usec) const;
 };
 
 #endif
--- a/layer/WaveformLayer.cpp	Wed Oct 03 12:59:55 2018 +0100
+++ b/layer/WaveformLayer.cpp	Wed Oct 10 08:44:27 2018 +0100
@@ -24,6 +24,8 @@
 #include "ColourDatabase.h"
 #include "PaintAssistant.h"
 
+#include "data/model/WaveformOversampler.h"
+
 #include <QPainter>
 #include <QPixmap>
 #include <QTextStream>
@@ -32,8 +34,9 @@
 #include <cmath>
 
 //#define DEBUG_WAVEFORM_PAINT 1
+//#define DEBUG_WAVEFORM_PAINT_BY_PIXEL 1
 
-
+using std::vector;
 
 
 WaveformLayer::WaveformLayer() :
@@ -49,10 +52,8 @@
     m_middleLineHeight(0.5),
     m_aggressive(false),
     m_cache(0),
-    m_cacheValid(false),
-    m_cacheZoomLevel(0)
+    m_cacheValid(false)
 {
-    
 }
 
 WaveformLayer::~WaveformLayer()
@@ -411,7 +412,8 @@
                             -5, -3, -2, -1, -0.5, 0 };
 
 bool
-WaveformLayer::getSourceFramesForX(LayerGeometryProvider *v, int x, int modelZoomLevel,
+WaveformLayer::getSourceFramesForX(LayerGeometryProvider *v,
+                                   int x, int modelZoomLevel,
                                    sv_frame_t &f0, sv_frame_t &f1) const
 {
     sv_frame_t viewFrame = v->getFrameForX(x);
@@ -422,15 +424,17 @@
     }
 
     f0 = viewFrame;
-    
     f0 = f0 / modelZoomLevel;
     f0 = f0 * modelZoomLevel;
 
-    viewFrame = v->getFrameForX(x + 1);
-    
-    f1 = viewFrame;
-    f1 = f1 / modelZoomLevel;
-    f1 = f1 * modelZoomLevel;
+    if (v->getZoomLevel().zone == ZoomLevel::PixelsPerFrame) {
+        f1 = f0 + 1;
+    } else {
+        viewFrame = v->getFrameForX(x + 1);
+        f1 = viewFrame;
+        f1 = f1 / modelZoomLevel;
+        f1 = f1 * modelZoomLevel;
+    }
     
     return (f0 < m_model->getEndFrame());
 }
@@ -482,11 +486,11 @@
         return;
     }
   
-    int zoomLevel = v->getZoomLevel();
+    ZoomLevel zoomLevel = v->getZoomLevel();
 
 #ifdef DEBUG_WAVEFORM_PAINT
     Profiler profiler("WaveformLayer::paint", true);
-    cerr << "WaveformLayer::paint (" << rect.x() << "," << rect.y()
+    SVCERR << "WaveformLayer::paint (" << rect.x() << "," << rect.y()
               << ") [" << rect.width() << "x" << rect.height() << "]: zoom " << zoomLevel << endl;
 #endif
 
@@ -500,15 +504,16 @@
     int w = v->getPaintWidth();
     int h = v->getPaintHeight();
 
-    bool ready = m_model->isReady();
     QPainter *paint;
 
     if (m_aggressive) {
 
 #ifdef DEBUG_WAVEFORM_PAINT
-        cerr << "WaveformLayer::paint: aggressive is true" << endl;
+        SVCERR << "WaveformLayer::paint: aggressive is true" << endl;
 #endif
 
+        using namespace std::rel_ops;
+        
         if (m_cacheValid && (zoomLevel != m_cacheZoomLevel)) {
             m_cacheValid = false;
         }
@@ -516,7 +521,7 @@
         if (!m_cache || m_cache->width() != w || m_cache->height() != h) {
 #ifdef DEBUG_WAVEFORM_PAINT
             if (m_cache) {
-                cerr << "WaveformLayer::paint: cache size " << m_cache->width() << "x" << m_cache->height() << " differs from view size " << w << "x" << h << ": regenerating aggressive cache" << endl;
+                SVCERR << "WaveformLayer::paint: cache size " << m_cache->width() << "x" << m_cache->height() << " differs from view size " << w << "x" << h << ": regenerating aggressive cache" << endl;
             }
 #endif
             delete m_cache;
@@ -554,15 +559,19 @@
     }
 
     int x0 = 0, x1 = w - 1;
-    int y0 = 0, y1 = h - 1;
 
     x0 = rect.left();
     x1 = rect.right();
-    y0 = rect.top();
-    y1 = rect.bottom();
 
-    if (x0 > 0) --x0;
-    if (x1 < w) ++x1;
+    if (x0 > 0) {
+        rect.adjust(-1, 0, 0, 0);
+        x0 = rect.left();
+    }
+
+    if (x1 < w) {
+        rect.adjust(0, 0, 1, 0);
+        x1 = rect.right();
+    }
 
     // Our zoom level may differ from that at which the underlying
     // model has its blocks.
@@ -572,28 +581,179 @@
     // the range being drawn is.  And that set of underlying frames
     // must remain the same when we scroll one or more pixels left or
     // right.
-            
-    int modelZoomLevel = m_model->getSummaryBlockSize(zoomLevel);
+
+    int desiredBlockSize = 1;
+    if (zoomLevel.zone == ZoomLevel::FramesPerPixel) {
+        desiredBlockSize = zoomLevel.level;
+    }
+    int blockSize = m_model->getSummaryBlockSize(desiredBlockSize);
 
     sv_frame_t frame0;
     sv_frame_t frame1;
     sv_frame_t spare;
 
-    getSourceFramesForX(v, x0, modelZoomLevel, frame0, spare);
-    getSourceFramesForX(v, x1, modelZoomLevel, spare, frame1);
+    getSourceFramesForX(v, x0, blockSize, frame0, spare);
+    getSourceFramesForX(v, x1, blockSize, spare, frame1);
     
 #ifdef DEBUG_WAVEFORM_PAINT
-    cerr << "Painting waveform from " << frame0 << " to " << frame1 << " (" << (x1-x0+1) << " pixels at zoom " << zoomLevel << " and model zoom " << modelZoomLevel << ")" <<  endl;
+    SVCERR << "Painting waveform from " << frame0 << " to " << frame1 << " (" << (x1-x0+1) << " pixels at zoom " << zoomLevel << " and model zoom " << blockSize << ")" <<  endl;
 #endif
 
-    RangeSummarisableTimeValueModel::RangeBlock *ranges = 
-        new RangeSummarisableTimeValueModel::RangeBlock;
+    m_effectiveGains.clear();
+    while ((int)m_effectiveGains.size() <= maxChannel) {
+        m_effectiveGains.push_back(m_gain);
+    }
+    if (m_autoNormalize) {
+        for (int ch = minChannel; ch <= maxChannel; ++ch) {
+            m_effectiveGains[ch] = getNormalizeGain(v, ch);
+        }
+    }
 
-    RangeSummarisableTimeValueModel::RangeBlock *otherChannelRanges = 0;
-    RangeSummarisableTimeValueModel::Range range;
+    RangeVec ranges;
+
+    if (v->getZoomLevel().zone == ZoomLevel::FramesPerPixel) {
+        getSummaryRanges(minChannel, maxChannel,
+                         mixingChannels || mergingChannels,
+                         frame0, frame1,
+                         blockSize, ranges);
+    } else {
+        getOversampledRanges(minChannel, maxChannel,
+                             mixingChannels || mergingChannels,
+                             frame0, frame1,
+                             v->getZoomLevel().level, ranges);
+    }
+
+    if (!ranges.empty()) {
+        for (int ch = minChannel; ch <= maxChannel; ++ch) {
+            paintChannel(v, paint, rect, ch, ranges, blockSize,
+                         frame0, frame1);
+        }
+    }
+    
+    if (m_middleLineHeight != 0.5) {
+        paint->restore();
+    }
+
+    if (m_aggressive) {
+        if (m_model->isReady() && rect == v->getPaintRect()) {
+            m_cacheValid = true;
+            m_cacheZoomLevel = zoomLevel;
+        }
+        paint->end();
+        delete paint;
+        viewPainter.drawPixmap(rect, *m_cache, rect);
+    }
+}
+
+void
+WaveformLayer::getSummaryRanges(int minChannel, int maxChannel,
+                                bool mixingOrMerging,
+                                sv_frame_t frame0, sv_frame_t frame1,
+                                int blockSize, RangeVec &ranges)
+    const
+{
+    for (int ch = minChannel; ch <= maxChannel; ++ch) {
+        ranges.push_back({});
+        m_model->getSummaries(ch, frame0, frame1 - frame0,
+                              ranges[ch - minChannel], blockSize);
+#ifdef DEBUG_WAVEFORM_PAINT
+            SVCERR << "channel " << ch << ": " << ranges[ch - minChannel].size() << " ranges from " << frame0 << " to " << frame1 << " at zoom level " << blockSize << endl;
+#endif
+    }
+    
+    if (mixingOrMerging) {
+        if (minChannel != 0 || maxChannel != 0) {
+            SVCERR << "Internal error: min & max channels should be 0 when merging or mixing all channels" << endl;
+        } else if (m_model->getChannelCount() > 1) {
+            ranges.push_back({});
+            m_model->getSummaries
+                (1, frame0, frame1 - frame0, ranges[1], blockSize);
+        }
+    }
+}
+
+void
+WaveformLayer::getOversampledRanges(int minChannel, int maxChannel,
+                                    bool /* mixingOrMerging */,
+                                    sv_frame_t frame0, sv_frame_t frame1,
+                                    int oversampleBy, RangeVec &ranges)
+    const
+{
+    // These frame values, tail length, etc variables are at the model
+    // sample rate, not the oversampled rate
+
+    sv_frame_t tail = 16;
+    sv_frame_t startFrame = m_model->getStartFrame();
+    sv_frame_t endFrame = m_model->getEndFrame();
+
+    sv_frame_t rf0 = frame0 - tail;
+    if (rf0 < startFrame) {
+        rf0 = 0;
+    }
+
+    sv_frame_t rf1 = frame1 + tail;
+    if (rf1 >= endFrame) {
+        rf1 = endFrame - 1;
+    }
+    if (rf1 <= rf0) {
+        SVCERR << "WARNING: getOversampledRanges: rf1 (" << rf1 << ") <= rf0 ("
+               << rf0 << ")" << endl;
+        return;
+    }
+    
+    for (int ch = minChannel; ch <= maxChannel; ++ch) {
+        floatvec_t oversampled = WaveformOversampler::getOversampledData
+            (m_model, ch, frame0, frame1 - frame0, oversampleBy);
+        RangeSummarisableTimeValueModel::RangeBlock rr;
+        for (float v: oversampled) {
+            RangeSummarisableTimeValueModel::Range r;
+            r.sample(v);
+            rr.push_back(r);
+        }
+        ranges.push_back(rr);
+
+#ifdef DEBUG_WAVEFORM_PAINT
+        SVCERR << "getOversampledRanges: " << frame0 << " -> " << frame1
+               << " (" << frame1 - frame0 << "-frame range) at ratio "
+               << oversampleBy << " with tail " << tail
+               << " -> got " << oversampled.size()
+               << " oversampled values for channel " << ch
+               << ", from which returning " << rr.size() << " ranges" << endl;
+#endif    
+    }
+
+    //!!! + channel modes
+    
+    return;
+}
+
+void
+WaveformLayer::paintChannel(LayerGeometryProvider *v,
+                            QPainter *paint,
+                            QRect rect, int ch,
+                            const RangeVec &ranges,
+                            int blockSize,
+                            sv_frame_t frame0,
+                            sv_frame_t frame1)
+    const
+{
+    int x0 = rect.left();
+    int y0 = rect.top();
+
+    int x1 = rect.right();
+    int y1 = rect.bottom();
+
+    int h = v->getPaintHeight();
+
+    int channels = 0, minChannel = 0, maxChannel = 0;
+    bool mergingChannels = false, mixingChannels = false;
+
+    channels = getChannelArrangement(minChannel, maxChannel,
+                                     mergingChannels, mixingChannels);
+    if (channels == 0) return;
 
     QColor baseColour = getBaseQColor();
-    std::vector<QColor> greys = getPartialShades(v);
+    vector<QColor> greys = getPartialShades(v);
         
     QColor midColour = baseColour;
     if (midColour == Qt::black) {
@@ -604,361 +764,344 @@
         midColour = midColour.light(50);
     }
 
-    while ((int)m_effectiveGains.size() <= maxChannel) {
-        m_effectiveGains.push_back(m_gain);
+    int prevRangeBottom = -1, prevRangeTop = -1;
+    QColor prevRangeBottomColour = baseColour, prevRangeTopColour = baseColour;
+
+    double gain = m_effectiveGains[ch];
+
+    int m = (h / channels) / 2;
+    int my = m + (((ch - minChannel) * h) / channels);
+
+#ifdef DEBUG_WAVEFORM_PAINT        
+    SVCERR << "ch = " << ch << ", channels = " << channels << ", m = " << m << ", my = " << my << ", h = " << h << endl;
+#endif
+
+    if (my - m > y1 || my + m < y0) return;
+
+    if ((m_scale == dBScale || m_scale == MeterScale) &&
+        m_channelMode != MergeChannels) {
+        m = (h / channels);
+        my = m + (((ch - minChannel) * h) / channels);
     }
 
-    for (int ch = minChannel; ch <= maxChannel; ++ch) {
+    paint->setPen(greys[1]);
+    paint->drawLine(x0, my, x1, my);
 
-        int prevRangeBottom = -1, prevRangeTop = -1;
-        QColor prevRangeBottomColour = baseColour, prevRangeTopColour = baseColour;
+    paintChannelScaleGuides(v, paint, rect, ch);
+  
+    int rangeix = ch - minChannel;
 
-        m_effectiveGains[ch] = m_gain;
+#ifdef DEBUG_WAVEFORM_PAINT
+    SVCERR << "paint channel " << ch << ": frame0 = " << frame0 << ", frame1 = " << frame1 << ", blockSize = " << blockSize << ", have " << ranges.size() << " range blocks of which ours is index " << rangeix << " with " << ranges[rangeix].size() << " ranges in it" << endl;
+#else
+    (void)frame1; // not actually used
+#endif
 
-        if (m_autoNormalize) {
-            m_effectiveGains[ch] = getNormalizeGain(v, ch);
+    for (int x = x0; x <= x1; ++x) {
+
+        sv_frame_t f0, f1;
+        sv_frame_t i0, i1;
+
+        bool showIndividualSample = false;
+        
+        if (v->getZoomLevel().zone == ZoomLevel::FramesPerPixel) {
+            if (!getSourceFramesForX(v, x, blockSize, f0, f1)) {
+                continue;
+            }
+            f1 = f1 - 1;
+            i0 = (f0 - frame0) / blockSize;
+            i1 = (f1 - frame0) / blockSize;
+        } else {
+            int oversampleBy = v->getZoomLevel().level;
+            f0 = f1 = v->getFrameForX(x);
+            int xf0 = v->getXForFrame(f0);
+            showIndividualSample = (x == xf0);
+            i0 = i1 = (f0 - frame0) * oversampleBy + (x - xf0);
         }
 
-        double gain = m_effectiveGains[ch];
+        if (f0 < frame0) {
+            SVCERR << "ERROR: WaveformLayer::paint: pixel " << x << " has f0 = " << f0 << " which is less than range frame0 " << frame0 << " for x0 = " << x0 << endl;
+            continue;
+        }
 
-        int m = (h / channels) / 2;
-        int my = m + (((ch - minChannel) * h) / channels);
-
-#ifdef DEBUG_WAVEFORM_PAINT        
-        cerr << "ch = " << ch << ", channels = " << channels << ", m = " << m << ", my = " << my << ", h = " << h << endl;
+#ifdef DEBUG_WAVEFORM_PAINT_BY_PIXEL
+        SVCERR << "WaveformLayer::paint: pixel " << x << ": i0 " << i0 << " (f " << f0 << "), i1 " << i1 << " (f " << f1 << ")" << endl;
 #endif
 
-        if (my - m > y1 || my + m < y0) continue;
-
-        if ((m_scale == dBScale || m_scale == MeterScale) &&
-            m_channelMode != MergeChannels) {
-            m = (h / channels);
-            my = m + (((ch - minChannel) * h) / channels);
+        if (i1 > i0 + 1) {
+            SVCERR << "WaveformLayer::paint: ERROR: i1 " << i1 << " > i0 " << i0 << " plus one (zoom = " << v->getZoomLevel() << ", model zoom = " << blockSize << ")" << endl;
         }
 
-        paint->setPen(greys[1]);
-        paint->drawLine(x0, my, x1, my);
+        const auto &r = ranges[rangeix];
+        RangeSummarisableTimeValueModel::Range range;
+            
+        if (in_range_for(r, i0)) {
 
-        int n = 10;
-        int py = -1;
+            range = r[i0];
+
+            if (i1 > i0 && in_range_for(r, i1)) {
+                range.setMax(std::max(range.max(), r[i1].max()));
+                range.setMin(std::min(range.min(), r[i1].min()));
+                range.setAbsmean((range.absmean() + r[i1].absmean()) / 2);
+            }
+
+        } else {
+#ifdef DEBUG_WAVEFORM_PAINT
+            SVCERR << "No (or not enough) ranges for index i0 = " << i0 << " (there are " << r.size() << " range(s))" << endl;
+#endif
+            continue;
+        }
+
+        int rangeBottom = 0, rangeTop = 0, meanBottom = 0, meanTop = 0;
+
+        if (mergingChannels && ranges.size() > 1) {
+
+            const auto &other = ranges[1];
+            
+            if (in_range_for(other, i0)) {
+
+                range.setMax(fabsf(range.max()));
+                range.setMin(-fabsf(other[i0].max()));
+                range.setAbsmean
+                    ((range.absmean() + other[i0].absmean()) / 2);
+
+                if (i1 > i0 && in_range_for(other, i1)) {
+                    // let's not concern ourselves about the mean
+                    range.setMin(std::min(range.min(),
+                                          -fabsf(other[i1].max())));
+                }
+            }
+
+        } else if (mixingChannels && ranges.size() > 1) {
+
+            const auto &other = ranges[1];
+            
+            if (in_range_for(other, i0)) {
+
+                range.setMax((range.max() + other[i0].max()) / 2);
+                range.setMin((range.min() + other[i0].min()) / 2);
+                range.setAbsmean((range.absmean() + other[i0].absmean()) / 2);
+            }
+        }
+
+        int greyLevels = 1;
+        if (m_greyscale && (m_scale == LinearScale)) greyLevels = 4;
+
+        switch (m_scale) {
+
+        case LinearScale:
+            rangeBottom = int(double(m * greyLevels) * range.min() * gain);
+            rangeTop    = int(double(m * greyLevels) * range.max() * gain);
+            meanBottom  = int(double(-m) * range.absmean() * gain);
+            meanTop     = int(double(m) * range.absmean() * gain);
+            break;
+
+        case dBScale:
+            if (!mergingChannels) {
+                int db0 = dBscale(range.min() * gain, m);
+                int db1 = dBscale(range.max() * gain, m);
+                rangeTop    = std::max(db0, db1);
+                meanTop     = std::min(db0, db1);
+                if (mixingChannels) rangeBottom = meanTop;
+                else rangeBottom = dBscale(range.absmean() * gain, m);
+                meanBottom  = rangeBottom;
+            } else {
+                rangeBottom = -dBscale(range.min() * gain, m * greyLevels);
+                rangeTop    =  dBscale(range.max() * gain, m * greyLevels);
+                meanBottom  = -dBscale(range.absmean() * gain, m);
+                meanTop     =  dBscale(range.absmean() * gain, m);
+            }
+            break;
+
+        case MeterScale:
+            if (!mergingChannels) {
+                int r0 = abs(AudioLevel::multiplier_to_preview(range.min() * gain, m));
+                int r1 = abs(AudioLevel::multiplier_to_preview(range.max() * gain, m));
+                rangeTop    = std::max(r0, r1);
+                meanTop     = std::min(r0, r1);
+                if (mixingChannels) rangeBottom = meanTop;
+                else rangeBottom = AudioLevel::multiplier_to_preview(range.absmean() * gain, m);
+                meanBottom  = rangeBottom;
+            } else {
+                rangeBottom = -AudioLevel::multiplier_to_preview(range.min() * gain, m * greyLevels);
+                rangeTop    =  AudioLevel::multiplier_to_preview(range.max() * gain, m * greyLevels);
+                meanBottom  = -AudioLevel::multiplier_to_preview(range.absmean() * gain, m);
+                meanTop     =  AudioLevel::multiplier_to_preview(range.absmean() * gain, m);
+            }
+            break;
+        }
+
+        rangeBottom = my * greyLevels - rangeBottom;
+        rangeTop    = my * greyLevels - rangeTop;
+        meanBottom  = my - meanBottom;
+        meanTop     = my - meanTop;
+
+        int topFill = (rangeTop % greyLevels);
+        if (topFill > 0) topFill = greyLevels - topFill;
+
+        int bottomFill = (rangeBottom % greyLevels);
+
+        rangeTop = rangeTop / greyLevels;
+        rangeBottom = rangeBottom / greyLevels;
+
+        bool clipped = false;
+
+        if (rangeTop < my - m) { rangeTop = my - m; }
+        if (rangeTop > my + m) { rangeTop = my + m; }
+        if (rangeBottom < my - m) { rangeBottom = my - m; }
+        if (rangeBottom > my + m) { rangeBottom = my + m; }
+
+        if (range.max() <= -1.0 ||
+            range.max() >= 1.0) clipped = true;
+            
+        if (meanBottom > rangeBottom) meanBottom = rangeBottom;
+        if (meanTop < rangeTop) meanTop = rangeTop;
+
+        bool drawMean = m_showMeans;
+        if (meanTop == rangeTop) {
+            if (meanTop < meanBottom) ++meanTop;
+            else drawMean = false;
+        }
+        if (meanBottom == rangeBottom && m_scale == LinearScale) {
+            if (meanBottom > meanTop) --meanBottom;
+            else drawMean = false;
+        }
+
+        if (showIndividualSample) {
+            paint->setPen(baseColour);
+            paint->drawRect(x-1, rangeTop-1, 2, 2);
+        }
         
-        if (v->hasLightBackground() &&
-            v->getViewManager() &&
-            v->getViewManager()->shouldShowScaleGuides()) {
+        if (x != x0 && prevRangeBottom != -1) {
+            if (prevRangeBottom > rangeBottom + 1 &&
+                prevRangeTop    > rangeBottom + 1) {
+//                    paint->setPen(midColour);
+                paint->setPen(baseColour);
+                paint->drawLine(x-1, prevRangeTop, x, rangeBottom + 1);
+                paint->setPen(prevRangeTopColour);
+                paint->drawPoint(x-1, prevRangeTop);
+            } else if (prevRangeBottom < rangeTop - 1 &&
+                       prevRangeTop    < rangeTop - 1) {
+//                    paint->setPen(midColour);
+                paint->setPen(baseColour);
+                paint->drawLine(x-1, prevRangeBottom, x, rangeTop - 1);
+                paint->setPen(prevRangeBottomColour);
+                paint->drawPoint(x-1, prevRangeBottom);
+            }
+        }
 
-            paint->setPen(QColor(240, 240, 240));
+        if (m_model->isReady()) {
+            if (clipped /*!!! ||
+                          range.min() * gain <= -1.0 ||
+                          range.max() * gain >=  1.0 */) {
+                paint->setPen(Qt::red); //!!! getContrastingColour
+            } else {
+                paint->setPen(baseColour);
+            }
+        } else {
+            paint->setPen(midColour);
+        }
 
-            for (int i = 1; i < n; ++i) {
-                
-                double val = 0.0, nval = 0.0;
+#ifdef DEBUG_WAVEFORM_PAINT_BY_PIXEL
+        SVCERR << "range " << rangeBottom << " -> " << rangeTop << ", means " << meanBottom << " -> " << meanTop << ", raw range " << range.min() << " -> " << range.max() << endl;
+#endif
 
-                switch (m_scale) {
+        if (rangeTop == rangeBottom) {
+            paint->drawPoint(x, rangeTop);
+        } else {
+            paint->drawLine(x, rangeBottom, x, rangeTop);
+        }
 
-                case LinearScale:
-                    val = (i * gain) / n;
-                    if (i > 0) nval = -val;
-                    break;
+        prevRangeTopColour = baseColour;
+        prevRangeBottomColour = baseColour;
 
-                case MeterScale:
-                    val = AudioLevel::dB_to_multiplier(meterdbs[i]) * gain;
-                    break;
-
-                case dBScale:
-                    val = AudioLevel::dB_to_multiplier(-(10*n) + i * 10) * gain;
-                    break;
-                }
-
-                if (val < -1.0 || val > 1.0) continue;
-
-                int y = getYForValue(v, val, ch);
-
-                if (py >= 0 && abs(y - py) < 10) continue;
-                else py = y;
-
-                int ny = y;
-                if (nval != 0.0) {
-                    ny = getYForValue(v, nval, ch);
-                }
-
-                paint->drawLine(x0, y, x1, y);
-                if (ny != y) {
-                    paint->drawLine(x0, ny, x1, ny);
+        if (m_greyscale && (m_scale == LinearScale) && m_model->isReady()) {
+            if (!clipped) {
+                if (rangeTop < rangeBottom) {
+                    if (topFill > 0 &&
+                        (!drawMean || (rangeTop < meanTop - 1))) {
+                        paint->setPen(greys[topFill - 1]);
+                        paint->drawPoint(x, rangeTop);
+                        prevRangeTopColour = greys[topFill - 1];
+                    }
+                    if (bottomFill > 0 && 
+                        (!drawMean || (rangeBottom > meanBottom + 1))) {
+                        paint->setPen(greys[bottomFill - 1]);
+                        paint->drawPoint(x, rangeBottom);
+                        prevRangeBottomColour = greys[bottomFill - 1];
+                    }
                 }
             }
         }
-  
-        m_model->getSummaries(ch, frame0, frame1 - frame0,
-                              *ranges, modelZoomLevel);
+        
+        if (drawMean) {
+            paint->setPen(midColour);
+            paint->drawLine(x, meanBottom, x, meanTop);
+        }
+        
+        prevRangeBottom = rangeBottom;
+        prevRangeTop = rangeTop;
+    }
+}
 
-#ifdef DEBUG_WAVEFORM_PAINT
-        cerr << "channel " << ch << ": " << ranges->size() << " ranges from " << frame0 << " to " << frame1 << " at zoom level " << modelZoomLevel << endl;
-#endif
+void
+WaveformLayer::paintChannelScaleGuides(LayerGeometryProvider *v,
+                                       QPainter *paint,
+                                       QRect rect,
+                                       int ch) const
+{
+    int x0 = rect.left();
+    int x1 = rect.right();
 
-        if (mergingChannels || mixingChannels) {
-            if (m_model->getChannelCount() > 1) {
-                if (!otherChannelRanges) {
-                    otherChannelRanges =
-                        new RangeSummarisableTimeValueModel::RangeBlock;
-                }
-                m_model->getSummaries
-                    (1, frame0, frame1 - frame0, *otherChannelRanges,
-                     modelZoomLevel);
-            } else {
-                if (otherChannelRanges != ranges) delete otherChannelRanges;
-                otherChannelRanges = ranges;
-            }
-        }
+    int n = 10;
+    int py = -1;
 
-        for (int x = x0; x <= x1; ++x) {
+    double gain = m_effectiveGains[ch];
+        
+    if (v->hasLightBackground() &&
+        v->getViewManager() &&
+        v->getViewManager()->shouldShowScaleGuides()) {
 
-            range = RangeSummarisableTimeValueModel::Range();
+        paint->setPen(QColor(240, 240, 240));
 
-            sv_frame_t f0, f1;
-            if (!getSourceFramesForX(v, x, modelZoomLevel, f0, f1)) continue;
-            f1 = f1 - 1;
-
-            if (f0 < frame0) {
-                cerr << "ERROR: WaveformLayer::paint: pixel " << x << " has f0 = " << f0 << " which is less than range frame0 " << frame0 << " for x0 = " << x0 << endl;
-                continue;
-            }
-
-            sv_frame_t i0 = (f0 - frame0) / modelZoomLevel;
-            sv_frame_t i1 = (f1 - frame0) / modelZoomLevel;
-
-#ifdef DEBUG_WAVEFORM_PAINT
-            cerr << "WaveformLayer::paint: pixel " << x << ": i0 " << i0 << " (f " << f0 << "), i1 " << i1 << " (f " << f1 << ")" << endl;
-#endif
-
-            if (i1 > i0 + 1) {
-                cerr << "WaveformLayer::paint: ERROR: i1 " << i1 << " > i0 " << i0 << " plus one (zoom = " << zoomLevel << ", model zoom = " << modelZoomLevel << ")" << endl;
-            }
-
-            if (ranges && i0 < (sv_frame_t)ranges->size()) {
-
-                range = (*ranges)[size_t(i0)];
-
-                if (i1 > i0 && i1 < (int)ranges->size()) {
-                    range.setMax(std::max(range.max(),
-                                          (*ranges)[size_t(i1)].max()));
-                    range.setMin(std::min(range.min(),
-                                          (*ranges)[size_t(i1)].min()));
-                    range.setAbsmean((range.absmean()
-                                      + (*ranges)[size_t(i1)].absmean()) / 2);
-                }
-
-            } else {
-#ifdef DEBUG_WAVEFORM_PAINT
-                cerr << "No (or not enough) ranges for i0 = " << i0 << endl;
-#endif
-                continue;
-            }
-
-            int rangeBottom = 0, rangeTop = 0, meanBottom = 0, meanTop = 0;
-
-            if (mergingChannels) {
-
-                if (otherChannelRanges && i0 < (sv_frame_t)otherChannelRanges->size()) {
-
-                    range.setMax(fabsf(range.max()));
-                    range.setMin(-fabsf((*otherChannelRanges)[size_t(i0)].max()));
-                    range.setAbsmean
-                        ((range.absmean() +
-                          (*otherChannelRanges)[size_t(i0)].absmean()) / 2);
-
-                    if (i1 > i0 && i1 < (sv_frame_t)otherChannelRanges->size()) {
-                        // let's not concern ourselves about the mean
-                        range.setMin
-                            (std::min
-                             (range.min(),
-                              -fabsf((*otherChannelRanges)[size_t(i1)].max())));
-                    }
-                }
-
-            } else if (mixingChannels) {
-
-                if (otherChannelRanges && i0 < (sv_frame_t)otherChannelRanges->size()) {
-
-                    range.setMax((range.max()
-                                  + (*otherChannelRanges)[size_t(i0)].max()) / 2);
-                    range.setMin((range.min()
-                                  + (*otherChannelRanges)[size_t(i0)].min()) / 2);
-                    range.setAbsmean((range.absmean()
-                                      + (*otherChannelRanges)[size_t(i0)].absmean()) / 2);
-                }
-            }
-
-            int greyLevels = 1;
-            if (m_greyscale && (m_scale == LinearScale)) greyLevels = 4;
+        for (int i = 1; i < n; ++i) {
+                
+            double val = 0.0, nval = 0.0;
 
             switch (m_scale) {
 
             case LinearScale:
-                rangeBottom = int(double(m * greyLevels) * range.min() * gain);
-                rangeTop    = int(double(m * greyLevels) * range.max() * gain);
-                meanBottom  = int(double(-m) * range.absmean() * gain);
-                meanTop     = int(double(m) * range.absmean() * gain);
+                val = (i * gain) / n;
+                if (i > 0) nval = -val;
+                break;
+
+            case MeterScale:
+                val = AudioLevel::dB_to_multiplier(meterdbs[i]) * gain;
                 break;
 
             case dBScale:
-                if (!mergingChannels) {
-                    int db0 = dBscale(range.min() * gain, m);
-                    int db1 = dBscale(range.max() * gain, m);
-                    rangeTop    = std::max(db0, db1);
-                    meanTop     = std::min(db0, db1);
-                    if (mixingChannels) rangeBottom = meanTop;
-                    else rangeBottom = dBscale(range.absmean() * gain, m);
-                    meanBottom  = rangeBottom;
-                } else {
-                    rangeBottom = -dBscale(range.min() * gain, m * greyLevels);
-                    rangeTop    =  dBscale(range.max() * gain, m * greyLevels);
-                    meanBottom  = -dBscale(range.absmean() * gain, m);
-                    meanTop     =  dBscale(range.absmean() * gain, m);
-                }
-                break;
-
-            case MeterScale:
-                if (!mergingChannels) {
-                    int r0 = abs(AudioLevel::multiplier_to_preview(range.min() * gain, m));
-                    int r1 = abs(AudioLevel::multiplier_to_preview(range.max() * gain, m));
-                    rangeTop    = std::max(r0, r1);
-                    meanTop     = std::min(r0, r1);
-                    if (mixingChannels) rangeBottom = meanTop;
-                    else rangeBottom = AudioLevel::multiplier_to_preview(range.absmean() * gain, m);
-                    meanBottom  = rangeBottom;
-                } else {
-                    rangeBottom = -AudioLevel::multiplier_to_preview(range.min() * gain, m * greyLevels);
-                    rangeTop    =  AudioLevel::multiplier_to_preview(range.max() * gain, m * greyLevels);
-                    meanBottom  = -AudioLevel::multiplier_to_preview(range.absmean() * gain, m);
-                    meanTop     =  AudioLevel::multiplier_to_preview(range.absmean() * gain, m);
-                }
+                val = AudioLevel::dB_to_multiplier(-(10*n) + i * 10) * gain;
                 break;
             }
 
-            rangeBottom = my * greyLevels - rangeBottom;
-            rangeTop    = my * greyLevels - rangeTop;
-            meanBottom  = my - meanBottom;
-            meanTop     = my - meanTop;
+            if (val < -1.0 || val > 1.0) continue;
 
-            int topFill = (rangeTop % greyLevels);
-            if (topFill > 0) topFill = greyLevels - topFill;
+            int y = getYForValue(v, val, ch);
 
-            int bottomFill = (rangeBottom % greyLevels);
+            if (py >= 0 && abs(y - py) < 10) continue;
+            else py = y;
 
-            rangeTop = rangeTop / greyLevels;
-            rangeBottom = rangeBottom / greyLevels;
-
-            bool clipped = false;
-
-            if (rangeTop < my - m) { rangeTop = my - m; }
-            if (rangeTop > my + m) { rangeTop = my + m; }
-            if (rangeBottom < my - m) { rangeBottom = my - m; }
-            if (rangeBottom > my + m) { rangeBottom = my + m; }
-
-            if (range.max() <= -1.0 ||
-                range.max() >= 1.0) clipped = true;
-            
-            if (meanBottom > rangeBottom) meanBottom = rangeBottom;
-            if (meanTop < rangeTop) meanTop = rangeTop;
-
-            bool drawMean = m_showMeans;
-            if (meanTop == rangeTop) {
-                if (meanTop < meanBottom) ++meanTop;
-                else drawMean = false;
-            }
-            if (meanBottom == rangeBottom && m_scale == LinearScale) {
-                if (meanBottom > meanTop) --meanBottom;
-                else drawMean = false;
+            int ny = y;
+            if (nval != 0.0) {
+                ny = getYForValue(v, nval, ch);
             }
 
-            if (x != x0 && prevRangeBottom != -1) {
-                if (prevRangeBottom > rangeBottom + 1 &&
-                    prevRangeTop    > rangeBottom + 1) {
-//                    paint->setPen(midColour);
-                    paint->setPen(baseColour);
-                    paint->drawLine(x-1, prevRangeTop, x, rangeBottom + 1);
-                    paint->setPen(prevRangeTopColour);
-                    paint->drawPoint(x-1, prevRangeTop);
-                } else if (prevRangeBottom < rangeTop - 1 &&
-                           prevRangeTop    < rangeTop - 1) {
-//                    paint->setPen(midColour);
-                    paint->setPen(baseColour);
-                    paint->drawLine(x-1, prevRangeBottom, x, rangeTop - 1);
-                    paint->setPen(prevRangeBottomColour);
-                    paint->drawPoint(x-1, prevRangeBottom);
-                }
+            paint->drawLine(x0, y, x1, y);
+            if (ny != y) {
+                paint->drawLine(x0, ny, x1, ny);
             }
-
-            if (ready) {
-                if (clipped /*!!! ||
-                    range.min() * gain <= -1.0 ||
-                    range.max() * gain >=  1.0 */) {
-                    paint->setPen(Qt::red); //!!! getContrastingColour
-                } else {
-                    paint->setPen(baseColour);
-                }
-            } else {
-                paint->setPen(midColour);
-            }
-
-#ifdef DEBUG_WAVEFORM_PAINT
-            cerr << "range " << rangeBottom << " -> " << rangeTop << ", means " << meanBottom << " -> " << meanTop << ", raw range " << range.min() << " -> " << range.max() << endl;
-#endif
-
-            if (rangeTop == rangeBottom) {
-                paint->drawPoint(x, rangeTop);
-            } else {
-                paint->drawLine(x, rangeBottom, x, rangeTop);
-            }
-
-            prevRangeTopColour = baseColour;
-            prevRangeBottomColour = baseColour;
-
-            if (m_greyscale && (m_scale == LinearScale) && ready) {
-                if (!clipped) {
-                    if (rangeTop < rangeBottom) {
-                        if (topFill > 0 &&
-                            (!drawMean || (rangeTop < meanTop - 1))) {
-                            paint->setPen(greys[topFill - 1]);
-                            paint->drawPoint(x, rangeTop);
-                            prevRangeTopColour = greys[topFill - 1];
-                        }
-                        if (bottomFill > 0 && 
-                            (!drawMean || (rangeBottom > meanBottom + 1))) {
-                            paint->setPen(greys[bottomFill - 1]);
-                            paint->drawPoint(x, rangeBottom);
-                            prevRangeBottomColour = greys[bottomFill - 1];
-                        }
-                    }
-                }
-            }
-        
-            if (drawMean) {
-                paint->setPen(midColour);
-                paint->drawLine(x, meanBottom, x, meanTop);
-            }
-        
-            prevRangeBottom = rangeBottom;
-            prevRangeTop = rangeTop;
         }
     }
-
-    if (m_middleLineHeight != 0.5) {
-        paint->restore();
-    }
-
-    if (m_aggressive) {
-
-        if (ready && rect == v->getPaintRect()) {
-            m_cacheValid = true;
-            m_cacheZoomLevel = zoomLevel;
-        }
-        paint->end();
-        delete paint;
-        viewPainter.drawPixmap(rect, *m_cache, rect);
-    }
-
-    if (otherChannelRanges != ranges) delete otherChannelRanges;
-    delete ranges;
 }
 
 QString
@@ -968,12 +1111,17 @@
 
     if (!m_model || !m_model->isOK()) return "";
 
-    int zoomLevel = v->getZoomLevel();
+    ZoomLevel zoomLevel = v->getZoomLevel();
 
-    int modelZoomLevel = m_model->getSummaryBlockSize(zoomLevel);
+    int desiredBlockSize = 1;
+    if (zoomLevel.zone == ZoomLevel::FramesPerPixel) {
+        desiredBlockSize = zoomLevel.level;
+    }
+
+    int blockSize = m_model->getSummaryBlockSize(desiredBlockSize);
 
     sv_frame_t f0, f1;
-    if (!getSourceFramesForX(v, x, modelZoomLevel, f0, f1)) return "";
+    if (!getSourceFramesForX(v, x, blockSize, f0, f1)) return "";
     
     QString text;
 
@@ -998,7 +1146,6 @@
 
     for (int ch = minChannel; ch <= maxChannel; ++ch) {
 
-        int blockSize = v->getZoomLevel();
         RangeSummarisableTimeValueModel::RangeBlock ranges;
         m_model->getSummaries(ch, f0, f1 - f0, ranges, blockSize);
 
@@ -1082,7 +1229,7 @@
         break;
     }
 
-//    cerr << "mergingChannels= " << mergingChannels << ", channel  = " << channel << ", value = " << value << ", vy = " << vy << endl;
+//    SVCERR << "mergingChannels= " << mergingChannels << ", channel  = " << channel << ", value = " << value << ", vy = " << vy << endl;
 
     return my - vy;
 }
--- a/layer/WaveformLayer.h	Wed Oct 03 12:59:55 2018 +0100
+++ b/layer/WaveformLayer.h	Wed Oct 10 08:44:27 2018 +0100
@@ -209,10 +209,30 @@
 
     const RangeSummarisableTimeValueModel *m_model; // I do not own this
 
+    typedef std::vector<RangeSummarisableTimeValueModel::RangeBlock> RangeVec;
+
     /// Return value is number of channels displayed
     int getChannelArrangement(int &min, int &max,
-                                 bool &merging, bool &mixing) const;
+                              bool &merging, bool &mixing) const;
 
+    void paintChannel
+    (LayerGeometryProvider *, QPainter *paint, QRect rect, int channel,
+     const RangeVec &ranges,
+     int blockSize, sv_frame_t frame0, sv_frame_t frame1) const;
+    
+    void paintChannelScaleGuides(LayerGeometryProvider *, QPainter *paint,
+                                 QRect rect, int channel) const;
+
+    void getSummaryRanges(int minChannel, int maxChannel,
+                          bool mixingOrMerging,
+                          sv_frame_t f0, sv_frame_t f1,
+                          int blockSize, RangeVec &ranges) const;
+
+    void getOversampledRanges(int minChannel, int maxChannel,
+                              bool mixingOrMerging,
+                              sv_frame_t f0, sv_frame_t f1,
+                              int oversampleBy, RangeVec &ranges) const;
+    
     int getYForValue(const LayerGeometryProvider *v, double value, int channel) const;
 
     double getValueForY(const LayerGeometryProvider *v, int y, int &channel) const;
@@ -238,7 +258,7 @@
 
     mutable QPixmap *m_cache;
     mutable bool m_cacheValid;
-    mutable int m_cacheZoomLevel;
+    mutable ZoomLevel m_cacheZoomLevel;
 };
 
 #endif
--- a/view/AlignmentView.cpp	Wed Oct 03 12:59:55 2018 +0100
+++ b/view/AlignmentView.cpp	Wed Oct 10 08:44:27 2018 +0100
@@ -57,14 +57,14 @@
 }
 
 void
-AlignmentView::viewAboveZoomLevelChanged(int level, bool)
+AlignmentView::viewAboveZoomLevelChanged(ZoomLevel level, bool)
 {
     m_zoomLevel = level;
     update();
 }
 
 void
-AlignmentView::viewBelowZoomLevelChanged(int, bool)
+AlignmentView::viewBelowZoomLevelChanged(ZoomLevel, bool)
 {
     update();
 }
@@ -80,9 +80,9 @@
 
     if (m_above) {
         connect(m_above,
-                SIGNAL(zoomLevelChanged(int, bool)),
+		SIGNAL(zoomLevelChanged(ZoomLevel, bool)),
                 this, 
-                SLOT(viewAboveZoomLevelChanged(int, bool)));
+		SLOT(viewAboveZoomLevelChanged(ZoomLevel, bool)));
     }
 }
 
@@ -97,9 +97,9 @@
 
     if (m_below) {
         connect(m_below,
-                SIGNAL(zoomLevelChanged(int, bool)),
+		SIGNAL(zoomLevelChanged(ZoomLevel, bool)),
                 this, 
-                SLOT(viewBelowZoomLevelChanged(int, bool)));
+		SLOT(viewBelowZoomLevelChanged(ZoomLevel, bool)));
     }
 }
 
--- a/view/AlignmentView.h	Wed Oct 03 12:59:55 2018 +0100
+++ b/view/AlignmentView.h	Wed Oct 10 08:44:27 2018 +0100
@@ -32,8 +32,8 @@
 public slots:
     virtual void globalCentreFrameChanged(sv_frame_t);
     virtual void viewCentreFrameChanged(View *, sv_frame_t);
-    virtual void viewAboveZoomLevelChanged(int, bool);
-    virtual void viewBelowZoomLevelChanged(int, bool);
+    virtual void viewAboveZoomLevelChanged(ZoomLevel, bool);
+    virtual void viewBelowZoomLevelChanged(ZoomLevel, bool);
     virtual void viewManagerPlaybackFrameChanged(sv_frame_t);
 
 protected:
--- a/view/Overview.cpp	Wed Oct 03 12:59:55 2018 +0100
+++ b/view/Overview.cpp	Wed Oct 10 08:44:27 2018 +0100
@@ -44,13 +44,14 @@
 void
 Overview::modelChangedWithin(sv_frame_t startFrame, sv_frame_t endFrame)
 {
+    using namespace std::rel_ops;
+    
     bool zoomChanged = false;
 
     sv_frame_t frameCount = getModelsEndFrame() - getModelsStartFrame();
-    int zoomLevel = int(frameCount / width());
-    if (zoomLevel < 1) zoomLevel = 1;
-    zoomLevel = getZoomConstraintBlockSize(zoomLevel,
-                                           ZoomConstraint::RoundUp);
+    ZoomLevel zoomLevel { ZoomLevel::FramesPerPixel, int(frameCount / width()) };
+    if (zoomLevel.level < 1) zoomLevel.level = 1;
+    zoomLevel = getZoomConstraintLevel(zoomLevel, ZoomConstraint::RoundUp);
     if (zoomLevel != m_zoomLevel) {
         zoomChanged = true;
     }
@@ -123,7 +124,7 @@
 }    
 
 void
-Overview::viewZoomLevelChanged(View *v, int, bool)
+Overview::viewZoomLevelChanged(View *v, ZoomLevel, bool)
 {
     if (v == this) return;
     if (m_views.find(v) != m_views.end()) {
@@ -171,6 +172,8 @@
 void
 Overview::paintEvent(QPaintEvent *e)
 {
+    using namespace std::rel_ops;
+    
     // Recalculate zoom in case the size of the widget has changed.
 
 #ifdef DEBUG_OVERVIEW
@@ -179,16 +182,17 @@
 
     sv_frame_t startFrame = getModelsStartFrame();
     sv_frame_t frameCount = getModelsEndFrame() - getModelsStartFrame();
-    int zoomLevel = int(frameCount / width());
-    if (zoomLevel < 1) zoomLevel = 1;
-    zoomLevel = getZoomConstraintBlockSize(zoomLevel,
-                                           ZoomConstraint::RoundUp);
+    ZoomLevel zoomLevel { ZoomLevel::FramesPerPixel, int(frameCount / width()) };
+    if (zoomLevel.level < 1) zoomLevel.level = 1;
+    zoomLevel = getZoomConstraintLevel(zoomLevel, ZoomConstraint::RoundUp);
     if (zoomLevel != m_zoomLevel) {
         m_zoomLevel = zoomLevel;
         emit zoomLevelChanged(m_zoomLevel, m_followZoom);
     }
 
-    sv_frame_t centreFrame = startFrame + m_zoomLevel * (width() / 2);
+    sv_frame_t centreFrame = startFrame +
+        sv_frame_t(round(m_zoomLevel.pixelsToFrames(width()/2)));
+    
     if (centreFrame > (startFrame + getModelsEndFrame())/2) {
         centreFrame = (startFrame + getModelsEndFrame())/2;
     }
@@ -309,7 +313,7 @@
     if (!m_clickedInRange) return;
 
     int xoff = int(e->x()) - int(m_clickPos.x());
-    sv_frame_t frameOff = xoff * m_zoomLevel;
+    sv_frame_t frameOff = sv_frame_t(round(m_zoomLevel.pixelsToFrames(xoff)));
     
     sv_frame_t newCentreFrame = m_dragCentreFrame;
     if (frameOff > 0) {
@@ -325,8 +329,11 @@
         if (newCentreFrame > 0) --newCentreFrame;
     }
     
+    sv_frame_t pixel = sv_frame_t(round(m_zoomLevel.pixelsToFrames(1)));
+    
     if (std::max(m_centreFrame, newCentreFrame) -
-        std::min(m_centreFrame, newCentreFrame) > m_zoomLevel) {
+        std::min(m_centreFrame, newCentreFrame) >
+        pixel) {
         sv_frame_t rf = alignToReference(newCentreFrame);
 #ifdef DEBUG_OVERVIEW
         cerr << "Overview::mouseMoveEvent: x " << e->x() << " and click x " << m_clickPos.x() << " -> frame " << newCentreFrame << " -> rf " << rf << endl;
--- a/view/Overview.h	Wed Oct 03 12:59:55 2018 +0100
+++ b/view/Overview.h	Wed Oct 10 08:44:27 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _OVERVIEW_H_
-#define _OVERVIEW_H_
+#ifndef SV_OVERVIEW_H
+#define SV_OVERVIEW_H
 
 #include "View.h"
 
@@ -46,7 +46,7 @@
 
     virtual void globalCentreFrameChanged(sv_frame_t);
     virtual void viewCentreFrameChanged(View *, sv_frame_t);
-    virtual void viewZoomLevelChanged(View *, int, bool);
+    virtual void viewZoomLevelChanged(View *, ZoomLevel, bool);
     virtual void viewManagerPlaybackFrameChanged(sv_frame_t);
 
     virtual void setBoxColour(QColor);
--- a/view/Pane.cpp	Wed Oct 03 12:59:55 2018 +0100
+++ b/view/Pane.cpp	Wed Oct 10 08:44:27 2018 +0100
@@ -201,7 +201,7 @@
 
     int count = 0;
     int current = 0;
-    int level = 1;
+    ZoomLevel level;
 
     //!!! pull out into function (presumably in View)
     bool haveConstraint = false;
@@ -213,20 +213,26 @@
         }
     }
 
+    SVCERR << "haveConstraint = " << haveConstraint << endl;
+            
     if (haveConstraint) {
         while (true) {
+            //!!! this won't terminate if level is in the PixelsPerFrame zone
             if (getZoomLevel() == level) current = count;
-            int newLevel = getZoomConstraintBlockSize(level + 1,
-                                                      ZoomConstraint::RoundUp);
+            ZoomLevel newLevel = getZoomConstraintLevel(level.incremented(),
+                                                        ZoomConstraint::RoundUp);
+            SVCERR << "newLevel = " << newLevel << endl;
             if (newLevel == level) break;
             level = newLevel;
             if (++count == 50) break;
         }
     } else {
         // if we have no particular constraints, we can really spread out
+        //!!! this is nonsense in PixelsPerFrame zone
         while (true) {
+            using namespace std::rel_ops;
             if (getZoomLevel() >= level) current = count;
-            int step = level / 10;
+            int step = level.level / 10;
             int pwr = 0;
             while (step > 0) {
                 ++pwr;
@@ -237,13 +243,14 @@
                 step *= 2;
                 --pwr;
             }
-//            cerr << level << endl;
-            level += step;
-            if (++count == 100 || level > 262144) break;
+            cerr << level.level << ", step " << step << endl;
+            level.level += step;
+            if (++count == 100 || level.level > 262144) break;
         }
     }
 
-//    cerr << "Have " << count << " zoom levels" << endl;
+    //!!!
+    SVCERR << "Have " << count << " zoom levels" << endl;
 
     m_hthumb->setMinimumValue(0);
     m_hthumb->setMaximumValue(count);
@@ -1145,8 +1152,8 @@
 QImage *
 Pane::renderPartToNewImage(sv_frame_t f0, sv_frame_t f1)
 {
-    int x0 = int(f0 / getZoomLevel());
-    int x1 = int(f1 / getZoomLevel());
+    int x0 = int(round(getZoomLevel().framesToPixels(double(f0))));
+    int x1 = int(round(getZoomLevel().framesToPixels(double(f1))));
 
     QImage *image = new QImage(x1 - x0 + m_scaleWidth,
                                height(), QImage::Format_RGB32);
@@ -1881,9 +1888,9 @@
     int x1 = r.x() + r.width();
     int y1 = r.y() + r.height();
 
-    int w = x1 - x0;
-        
     sv_frame_t newStartFrame = getFrameForX(x0);
+    sv_frame_t newEndFrame = getFrameForX(x1);
+    sv_frame_t dist = newEndFrame - newStartFrame;
         
     sv_frame_t visibleFrames = getEndFrame() - getStartFrame();
     if (newStartFrame <= -visibleFrames) {
@@ -1893,14 +1900,9 @@
     if (newStartFrame >= getModelsEndFrame()) {
         newStartFrame  = getModelsEndFrame() - 1;
     }
-        
-    double ratio = double(w) / double(width());
-//        cerr << "ratio: " << ratio << endl;
-    int newZoomLevel = (int)nearbyint(m_zoomLevel * ratio);
-    if (newZoomLevel < 1) newZoomLevel = 1;
-
-//        cerr << "start: " << m_startFrame << ", level " << m_zoomLevel << endl;
-    setZoomLevel(getZoomConstraintBlockSize(newZoomLevel));
+
+    ZoomLevel newZoomLevel = ZoomLevel::fromRatio(width(), dist);
+    setZoomLevel(getZoomConstraintLevel(newZoomLevel));
     setStartFrame(newStartFrame);
 
     QString unit;
@@ -2379,21 +2381,18 @@
         }
 
     } else {
+        using namespace std::rel_ops;
 
         // Zoom in or out
 
-        int newZoomLevel = m_zoomLevel;
+        ZoomLevel newZoomLevel = m_zoomLevel;
   
         if (sign > 0) {
-            if (newZoomLevel <= 2) {
-                newZoomLevel = 1;
-            } else {
-                newZoomLevel = getZoomConstraintBlockSize
-                    (newZoomLevel - 1, ZoomConstraint::RoundDown);
-            }
-        } else { // sign < 0
-            newZoomLevel = getZoomConstraintBlockSize
-                (newZoomLevel + 1, ZoomConstraint::RoundUp);
+            newZoomLevel = getZoomConstraintLevel(newZoomLevel.decremented(),
+                                                  ZoomConstraint::RoundDown);
+        } else {
+            newZoomLevel = getZoomConstraintLevel(newZoomLevel.incremented(),
+                                                  ZoomConstraint::RoundUp);
         }
     
         if (newZoomLevel != m_zoomLevel) {
@@ -2422,9 +2421,11 @@
     // Scroll left or right by a fixed number of pixels
 
     if (getStartFrame() < 0 && 
-        getEndFrame() >= getModelsEndFrame()) return;
-
-    int delta = (pixels * m_zoomLevel);
+        getEndFrame() >= getModelsEndFrame()) {
+        return;
+    }
+
+    int delta = int(round(m_zoomLevel.pixelsToFrames(pixels)));
 
     if (m_centreFrame < delta) {
         setCentreFrame(0);
@@ -2443,8 +2444,7 @@
     //!!! dupe with updateHeadsUpDisplay
 
     int count = 0;
-    int level = 1;
-
+    ZoomLevel level;
 
     //!!! pull out into function (presumably in View)
     bool haveConstraint = false;
@@ -2458,17 +2458,19 @@
 
     if (haveConstraint) {
         while (true) {
+            //!!! this won't terminate if level is in the PixelsPerFrame zone
             if (m_hthumb->getMaximumValue() - value == count) break;
-            int newLevel = getZoomConstraintBlockSize(level + 1,
-                                                      ZoomConstraint::RoundUp);
+            ZoomLevel newLevel = getZoomConstraintLevel(level.incremented(),
+                                                        ZoomConstraint::RoundUp);
             if (newLevel == level) break;
             level = newLevel;
             if (++count == 50) break;
         }
     } else {
+        //!!! this is nonsense in PixelsPerFrame zone
         while (true) {
             if (m_hthumb->getMaximumValue() - value == count) break;
-            int step = level / 10;
+            int step = level.level / 10;
             int pwr = 0;
             while (step > 0) {
                 ++pwr;
@@ -2480,8 +2482,8 @@
                 --pwr;
             }
 //            cerr << level << endl;
-            level += step;
-            if (++count == 100 || level > 262144) break;
+            level.level += step;
+            if (++count == 100 || level.level > 262144) break;
         }
     }
         
@@ -2747,7 +2749,7 @@
 }
 
 void
-Pane::viewZoomLevelChanged(View *v, int z, bool locked)
+Pane::viewZoomLevelChanged(View *v, ZoomLevel z, bool locked)
 {
 //    cerr << "Pane[" << this << "]::zoomLevelChanged (global now "
 //              << (m_manager ? m_manager->getGlobalZoom() : 0) << ")" << endl;
--- a/view/Pane.h	Wed Oct 03 12:59:55 2018 +0100
+++ b/view/Pane.h	Wed Oct 10 08:44:27 2018 +0100
@@ -87,7 +87,7 @@
     // view slots
     virtual void toolModeChanged() override;
     virtual void zoomWheelsEnabledChanged() override;
-    virtual void viewZoomLevelChanged(View *v, int z, bool locked) override;
+    virtual void viewZoomLevelChanged(View *, ZoomLevel, bool locked) override;
     virtual void modelAlignmentCompletionChanged() override;
 
     // local slots, not overrides
--- a/view/View.cpp	Wed Oct 03 12:59:55 2018 +0100
+++ b/view/View.cpp	Wed Oct 10 08:44:27 2018 +0100
@@ -54,7 +54,7 @@
     QFrame(w),
     m_id(getNextId()),
     m_centreFrame(0),
-    m_zoomLevel(1024),
+    m_zoomLevel(ZoomLevel::FramesPerPixel, 1024),
     m_followPan(true),
     m_followZoom(true),
     m_followPlay(PlaybackScrollPageWithCentre),
@@ -64,7 +64,7 @@
     m_cache(0),
     m_buffer(0),
     m_cacheCentreFrame(0),
-    m_cacheZoomLevel(1024),
+    m_cacheZoomLevel(ZoomLevel::FramesPerPixel, 1024),
     m_selectionCached(false),
     m_deleting(false),
     m_haveSelectedLayer(false),
@@ -319,7 +319,8 @@
 void
 View::setStartFrame(sv_frame_t f)
 {
-    setCentreFrame(f + m_zoomLevel * (width() / 2));
+    setCentreFrame(f + sv_frame_t(round
+                                  (m_zoomLevel.pixelsToFrames(width() / 2))));
 }
 
 bool
@@ -327,22 +328,40 @@
 {
     bool changeVisible = false;
 
+#ifdef DEBUG_VIEW
+    SVCERR << "View::setCentreFrame: from " << m_centreFrame
+           << " to " << f << endl;
+#endif
+
     if (m_centreFrame != f) {
 
-        int formerPixel = int(m_centreFrame / m_zoomLevel);
-
+        sv_frame_t formerCentre = m_centreFrame;
         m_centreFrame = f;
-
-        int newPixel = int(m_centreFrame / m_zoomLevel);
         
-        if (newPixel != formerPixel) {
+        if (m_zoomLevel.zone == ZoomLevel::PixelsPerFrame) {
 
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-            cout << "View(" << this << ")::setCentreFrame: newPixel " << newPixel << ", formerPixel " << formerPixel << endl;
+            SVCERR << "View(" << this << ")::setCentreFrame: in PixelsPerFrame zone, so change must be visible" << endl;
 #endif
             update();
-
             changeVisible = true;
+
+        } else {
+        
+            int formerPixel = int(formerCentre / m_zoomLevel.level);
+            int newPixel = int(m_centreFrame / m_zoomLevel.level);
+        
+            if (newPixel != formerPixel) {
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+                SVCERR << "View(" << this << ")::setCentreFrame: newPixel " << newPixel << ", formerPixel " << formerPixel << endl;
+#endif
+                // ensure the centre frame is a multiple of the zoom level
+                m_centreFrame = sv_frame_t(newPixel) * m_zoomLevel.level;
+                
+                update();
+                changeVisible = true;
+            }
         }
 
         if (e) {
@@ -362,23 +381,65 @@
 int
 View::getXForFrame(sv_frame_t frame) const
 {
-    return int((frame - getStartFrame()) / m_zoomLevel);
+    // In FramesPerPixel mode, the pixel should be the one "covering"
+    // the given frame, i.e. to the "left" of it - not necessarily the
+    // nearest boundary.
+    
+    sv_frame_t level = m_zoomLevel.level;
+    sv_frame_t fdiff = frame - getCentreFrame();
+    int diff, result;
+
+    if (m_zoomLevel.zone == ZoomLevel::FramesPerPixel) {
+        diff = int(fdiff / level);
+        if ((fdiff < 0) && ((fdiff % level) != 0)) {
+            --diff; // round to the left
+        }
+    } else {
+        diff = int(fdiff * level);
+    }
+
+    result = int(diff + (width()/2));
+    return result;
 }
 
 sv_frame_t
 View::getFrameForX(int x) const
 {
-    sv_frame_t z = m_zoomLevel; // nb not just int, or multiplication may overflow
-    sv_frame_t frame = m_centreFrame - (width()/2) * z;
-
-    frame = (frame / z) * z; // this is start frame
-    frame = frame + x * z;
-
-#ifdef DEBUG_VIEW_WIDGET_PAINT
-    cerr << "View::getFrameForX(" << x << "): z = " << z << ", m_centreFrame = " << m_centreFrame << ", width() = " << width() << ", frame = " << frame << endl;
+    // Note, this must always return a value that is on a zoom-level
+    // boundary - regardless of whether the nominal centre frame is on
+    // such a boundary or not.
+    
+    // In PixelsPerFrame mode, the frame should be the one immediately
+    // left of the given pixel, not necessarily the nearest.
+
+    int diff = x - (width()/2);
+    sv_frame_t level = m_zoomLevel.level;
+    sv_frame_t fdiff, result;
+    
+    if (m_zoomLevel.zone == ZoomLevel::FramesPerPixel) {
+        fdiff = diff * level;
+        result = ((fdiff + m_centreFrame) / level) * level;
+    } else {
+        fdiff = diff / level;
+        if ((diff < 0) && ((diff % level) != 0)) {
+            --fdiff; // round to the left
+        }
+        result = fdiff + m_centreFrame;
+    }
+
+#ifdef DEBUG_VIEW
+    if (x == 0) {
+        SVCERR << "getFrameForX(" << x << "): diff = " << diff << ", fdiff = "
+               << fdiff << ", m_centreFrame = " << m_centreFrame
+               << ", level = " << m_zoomLevel.level
+               << ", diff % level = " << (diff % m_zoomLevel.level)
+               << ", nominal " << fdiff + m_centreFrame
+               << ", will return " << result
+               << endl;
+    }
 #endif
-
-    return frame;
+    
+    return result;
 }
 
 double
@@ -447,7 +508,7 @@
     }
 }
 
-int
+ZoomLevel
 View::getZoomLevel() const
 {
 #ifdef DEBUG_VIEW_WIDGET_PAINT
@@ -476,16 +537,17 @@
 }
 
 void
-View::setZoomLevel(int z)
+View::setZoomLevel(ZoomLevel z)
 {
-    int dpratio = effectiveDevicePixelRatio();
-    if (z < dpratio) return;
-    if (z < 1) z = 1;
-    if (m_zoomLevel != int(z)) {
-        m_zoomLevel = z;
-        emit zoomLevelChanged(z, m_followZoom);
-        update();
+//!!!    int dpratio = effectiveDevicePixelRatio();
+//    if (z < dpratio) return;
+//    if (z < 1) z = 1;
+    if (m_zoomLevel == z) {
+        return;
     }
+    m_zoomLevel = z;
+    emit zoomLevelChanged(z, m_followZoom);
+    update();
 }
 
 bool
@@ -725,13 +787,13 @@
         m_manager->disconnect(this, SLOT(globalCentreFrameChanged(sv_frame_t)));
         m_manager->disconnect(this, SLOT(viewCentreFrameChanged(View *, sv_frame_t)));
         m_manager->disconnect(this, SLOT(viewManagerPlaybackFrameChanged(sv_frame_t)));
-        m_manager->disconnect(this, SLOT(viewZoomLevelChanged(View *, int, bool)));
+        m_manager->disconnect(this, SLOT(viewZoomLevelChanged(View *, ZoomLevel, bool)));
         m_manager->disconnect(this, SLOT(toolModeChanged()));
         m_manager->disconnect(this, SLOT(selectionChanged()));
         m_manager->disconnect(this, SLOT(overlayModeChanged()));
         m_manager->disconnect(this, SLOT(zoomWheelsEnabledChanged()));
         disconnect(m_manager, SLOT(viewCentreFrameChanged(sv_frame_t, bool, PlaybackFollowMode)));
-        disconnect(m_manager, SLOT(zoomLevelChanged(int, bool)));
+        disconnect(m_manager, SLOT(zoomLevelChanged(ZoomLevel, bool)));
     }
 
     m_manager = manager;
@@ -743,8 +805,8 @@
     connect(m_manager, SIGNAL(playbackFrameChanged(sv_frame_t)),
             this, SLOT(viewManagerPlaybackFrameChanged(sv_frame_t)));
 
-    connect(m_manager, SIGNAL(viewZoomLevelChanged(View *, int, bool)),
-            this, SLOT(viewZoomLevelChanged(View *, int, bool)));
+    connect(m_manager, SIGNAL(viewZoomLevelChanged(View *, ZoomLevel, bool)),
+            this, SLOT(viewZoomLevelChanged(View *, ZoomLevel, bool)));
 
     connect(m_manager, SIGNAL(toolModeChanged()),
             this, SLOT(toolModeChanged()));
@@ -764,8 +826,8 @@
             m_manager, SLOT(viewCentreFrameChanged(sv_frame_t, bool,
                                                    PlaybackFollowMode)));
 
-    connect(this, SIGNAL(zoomLevelChanged(int, bool)),
-            m_manager, SLOT(viewZoomLevelChanged(int, bool)));
+    connect(this, SIGNAL(zoomLevelChanged(ZoomLevel, bool)),
+            m_manager, SLOT(viewZoomLevelChanged(ZoomLevel, bool)));
 
     switch (m_followPlay) {
         
@@ -1127,7 +1189,7 @@
 }
 
 void
-View::viewZoomLevelChanged(View *p, int z, bool locked)
+View::viewZoomLevelChanged(View *p, ZoomLevel z, bool locked)
 {
 #ifdef DEBUG_VIEW_WIDGET_PAINT
     cerr  << "View[" << this << "]: viewZoomLevelChanged(" << p << ", " << z << ", " << locked << ")" << endl;
@@ -1400,30 +1462,32 @@
     return nonScrollables;
 }
 
-int
-View::getZoomConstraintBlockSize(int blockSize,
-                                 ZoomConstraint::RoundingDirection dir)
+ZoomLevel
+View::getZoomConstraintLevel(ZoomLevel zoomLevel,
+                             ZoomConstraint::RoundingDirection dir)
     const
 {
-    int candidate = blockSize;
+    using namespace std::rel_ops;
+    
+    ZoomLevel candidate = zoomLevel;
     bool haveCandidate = false;
 
     PowerOfSqrtTwoZoomConstraint defaultZoomConstraint;
 
-    for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end(); ++i) {
+    for (auto i = m_layerStack.begin(); i != m_layerStack.end(); ++i) {
 
         const ZoomConstraint *zoomConstraint = (*i)->getZoomConstraint();
         if (!zoomConstraint) zoomConstraint = &defaultZoomConstraint;
 
-        int thisBlockSize =
-            zoomConstraint->getNearestBlockSize(blockSize, dir);
+        ZoomLevel thisLevel =
+            zoomConstraint->getNearestZoomLevel(zoomLevel, dir);
 
         // Go for the block size that's furthest from the one
         // passed in.  Most of the time, that's what we want.
         if (!haveCandidate ||
-            (thisBlockSize > blockSize && thisBlockSize > candidate) ||
-            (thisBlockSize < blockSize && thisBlockSize < candidate)) {
-            candidate = thisBlockSize;
+            (thisLevel > zoomLevel && thisLevel > candidate) ||
+            (thisLevel < zoomLevel && thisLevel < candidate)) {
+            candidate = thisLevel;
             haveCandidate = true;
         }
     }
@@ -1454,16 +1518,18 @@
 void
 View::zoom(bool in)
 {
-    int newZoomLevel = m_zoomLevel;
+    ZoomLevel newZoomLevel = m_zoomLevel;
 
     if (in) {
-        newZoomLevel = getZoomConstraintBlockSize(newZoomLevel - 1, 
-                                                  ZoomConstraint::RoundDown);
+        newZoomLevel = getZoomConstraintLevel(m_zoomLevel.decremented(),
+                                              ZoomConstraint::RoundDown);
     } else {
-        newZoomLevel = getZoomConstraintBlockSize(newZoomLevel + 1,
-                                                  ZoomConstraint::RoundUp);
+        newZoomLevel = getZoomConstraintLevel(m_zoomLevel.incremented(),
+                                              ZoomConstraint::RoundUp);
     }
 
+    using namespace std::rel_ops;
+    
     if (newZoomLevel != m_zoomLevel) {
         setZoomLevel(newZoomLevel);
     }
@@ -1765,6 +1831,8 @@
                   << m_cacheZoomLevel << ", zoom " << m_zoomLevel << endl;
 #endif
 
+        using namespace std::rel_ops;
+    
         if (!m_cache ||
             m_cacheZoomLevel != m_zoomLevel ||
             scaledCacheSize != m_cache->size()) {
@@ -2370,8 +2438,8 @@
 bool
 View::render(QPainter &paint, int xorigin, sv_frame_t f0, sv_frame_t f1)
 {
-    int x0 = int(f0 / m_zoomLevel);
-    int x1 = int(f1 / m_zoomLevel);
+    int x0 = int(round(m_zoomLevel.framesToPixels(double(f0))));
+    int x1 = int(round(m_zoomLevel.framesToPixels(double(f1))));
 
     int w = x1 - x0;
 
@@ -2433,7 +2501,8 @@
             return false;
         }
 
-        m_centreFrame = f0 + (x + width()/2) * m_zoomLevel;
+        m_centreFrame = f0 + sv_frame_t(round(m_zoomLevel.pixelsToFrames
+                                              (x + width()/2)));
         
         QRect chunk(0, 0, width(), height());
 
@@ -2484,8 +2553,8 @@
 QImage *
 View::renderPartToNewImage(sv_frame_t f0, sv_frame_t f1)
 {
-    int x0 = int(f0 / getZoomLevel());
-    int x1 = int(f1 / getZoomLevel());
+    int x0 = int(round(getZoomLevel().framesToPixels(double(f0))));
+    int x1 = int(round(getZoomLevel().framesToPixels(double(f1))));
     
     QImage *image = new QImage(x1 - x0, height(), QImage::Format_RGB32);
 
@@ -2512,8 +2581,8 @@
 QSize
 View::getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1)
 {
-    int x0 = int(f0 / getZoomLevel());
-    int x1 = int(f1 / getZoomLevel());
+    int x0 = int(round(getZoomLevel().framesToPixels(double(f0))));
+    int x1 = int(round(getZoomLevel().framesToPixels(double(f1))));
 
     return QSize(x1 - x0, height());
 }
@@ -2530,8 +2599,8 @@
 bool
 View::renderPartToSvgFile(QString filename, sv_frame_t f0, sv_frame_t f1)
 {
-    int x0 = int(f0 / getZoomLevel());
-    int x1 = int(f1 / getZoomLevel());
+    int x0 = int(round(getZoomLevel().framesToPixels(double(f0))));
+    int x1 = int(round(getZoomLevel().framesToPixels(double(f1))));
 
     QSvgGenerator generator;
     generator.setFileName(filename);
@@ -2553,15 +2622,27 @@
 {
     stream << indent;
 
+    int classicZoomValue, deepZoomValue;
+
+    if (m_zoomLevel.zone == ZoomLevel::FramesPerPixel) {
+        classicZoomValue = m_zoomLevel.level;
+        deepZoomValue = 1;
+    } else {
+        classicZoomValue = 1;
+        deepZoomValue = m_zoomLevel.level;
+    }
+
     stream << QString("<view "
                       "centre=\"%1\" "
                       "zoom=\"%2\" "
-                      "followPan=\"%3\" "
-                      "followZoom=\"%4\" "
-                      "tracking=\"%5\" "
-                      " %6>\n")
+                      "deepZoom=\"%3\" "
+                      "followPan=\"%4\" "
+                      "followZoom=\"%5\" "
+                      "tracking=\"%6\" "
+                      " %7>\n")
         .arg(m_centreFrame)
-        .arg(m_zoomLevel)
+        .arg(classicZoomValue)
+        .arg(deepZoomValue)
         .arg(m_followPan)
         .arg(m_followZoom)
         .arg(m_followPlay == PlaybackScrollContinuous ? "scroll" :
--- a/view/View.h	Wed Oct 03 12:59:55 2018 +0100
+++ b/view/View.h	Wed Oct 10 08:44:27 2018 +0100
@@ -148,16 +148,17 @@
                             bool logarithmic) const;
 
     /**
-     * Return the zoom level, i.e. the number of frames per pixel
+     * Return the zoom level, i.e. the number of frames per pixel or
+     * pixels per frame
      */
-    int getZoomLevel() const;
+    ZoomLevel getZoomLevel() const;
 
     /**
-     * Set the zoom level, i.e. the number of frames per pixel.  The
-     * centre frame will be unchanged; the start and end frames will
-     * change.
+     * Set the zoom level, i.e. the number of frames per pixel or
+     * pixels per frame.  The centre frame will be unchanged; the
+     * start and end frames will change.
      */
-    virtual void setZoomLevel(int z);
+    virtual void setZoomLevel(ZoomLevel z);
 
     /**
      * Zoom in or out.
@@ -400,7 +401,7 @@
                             bool globalScroll,
                             PlaybackFollowMode followMode);
 
-    void zoomLevelChanged(int level, bool locked);
+    void zoomLevelChanged(ZoomLevel level, bool locked);
 
     void contextHelpChanged(const QString &);
 
@@ -418,7 +419,7 @@
     virtual void globalCentreFrameChanged(sv_frame_t);
     virtual void viewCentreFrameChanged(View *, sv_frame_t);
     virtual void viewManagerPlaybackFrameChanged(sv_frame_t);
-    virtual void viewZoomLevelChanged(View *, int, bool);
+    virtual void viewZoomLevelChanged(View *, ZoomLevel, bool);
 
     virtual void propertyContainerSelected(View *, PropertyContainer *pc);
 
@@ -456,9 +457,9 @@
     bool areLayersScrollable() const;
     LayerList getScrollableBackLayers(bool testChanged, bool &changed) const;
     LayerList getNonScrollableFrontLayers(bool testChanged, bool &changed) const;
-    int getZoomConstraintBlockSize(int blockSize,
-                                      ZoomConstraint::RoundingDirection dir =
-                                      ZoomConstraint::RoundNearest) const;
+    ZoomLevel getZoomConstraintLevel(ZoomLevel level,
+                                     ZoomConstraint::RoundingDirection dir =
+                                     ZoomConstraint::RoundNearest) const;
 
     // True if the top layer(s) use colours for meaningful things.  If
     // this is the case, selections will be shown using unfilled boxes
@@ -481,7 +482,7 @@
     int effectiveDevicePixelRatio() const;
 
     sv_frame_t          m_centreFrame;
-    int                 m_zoomLevel;
+    ZoomLevel           m_zoomLevel;
     bool                m_followPan;
     bool                m_followZoom;
     PlaybackFollowMode  m_followPlay;
@@ -493,7 +494,7 @@
     QPixmap            *m_cache;  // I own this
     QPixmap            *m_buffer; // I own this
     sv_frame_t          m_cacheCentreFrame;
-    int                 m_cacheZoomLevel;
+    ZoomLevel           m_cacheZoomLevel;
     bool                m_selectionCached;
 
     bool                m_deleting;
--- a/view/ViewManager.cpp	Wed Oct 03 12:59:55 2018 +0100
+++ b/view/ViewManager.cpp	Wed Oct 10 08:44:27 2018 +0100
@@ -33,7 +33,7 @@
     m_playSource(0),
     m_recordTarget(0),
     m_globalCentreFrame(0),
-    m_globalZoom(1024),
+    m_globalZoom(ZoomLevel::FramesPerPixel, 1024),
     m_playbackFrame(0),
     m_playbackModel(0),
     m_mainModelSampleRate(0),
@@ -147,7 +147,7 @@
     emit globalCentreFrameChanged(f);
 }
 
-int
+ZoomLevel
 ViewManager::getGlobalZoom() const
 {
 #ifdef DEBUG_VIEW_MANAGER
@@ -687,7 +687,7 @@
 }
 
 void
-ViewManager::viewZoomLevelChanged(int z, bool locked)
+ViewManager::viewZoomLevelChanged(ZoomLevel z, bool locked)
 {
     View *v = dynamic_cast<View *>(sender());
 
@@ -709,7 +709,11 @@
     emit viewZoomLevelChanged(v, z, locked);
 
     if (!dynamic_cast<Overview *>(v)) {
-        emit activity(tr("Zoom to %n sample(s) per pixel", "", z));
+        if (z.zone == ZoomLevel::FramesPerPixel) {
+            emit activity(tr("Zoom to %n sample(s) per pixel", "", z.level));
+        } else {
+            emit activity(tr("Zoom to %n pixels per sample", "", z.level));
+        }
     }
 }
 
--- a/view/ViewManager.h	Wed Oct 03 12:59:55 2018 +0100
+++ b/view/ViewManager.h	Wed Oct 10 08:44:27 2018 +0100
@@ -27,6 +27,7 @@
 #include "base/Command.h"
 #include "base/Clipboard.h"
 #include "base/BaseTypes.h"
+#include "base/ZoomLevel.h"
 
 class AudioPlaySource;
 class AudioRecordTarget;
@@ -87,7 +88,7 @@
     bool isRecording() const;
 
     sv_frame_t getGlobalCentreFrame() const; // the set method is a slot
-    int getGlobalZoom() const;
+    ZoomLevel getGlobalZoom() const;
 
     sv_frame_t getPlaybackFrame() const; // the set method is a slot
 
@@ -255,7 +256,7 @@
     void viewCentreFrameChanged(View *v, sv_frame_t frame);
 
     /** Emitted when a view zooms. */
-    void viewZoomLevelChanged(View *v, int zoom, bool locked);
+    void viewZoomLevelChanged(View *v, ZoomLevel zoom, bool locked);
 
     /** Emitted when the playback frame changes. */
     void playbackFrameChanged(sv_frame_t frame);
@@ -307,7 +308,7 @@
 
 public slots:
     void viewCentreFrameChanged(sv_frame_t, bool, PlaybackFollowMode);
-    void viewZoomLevelChanged(int, bool);
+    void viewZoomLevelChanged(ZoomLevel, bool);
     void setGlobalCentreFrame(sv_frame_t);
     void setPlaybackFrame(sv_frame_t);
     void playStatusChanged(bool playing);
@@ -323,7 +324,7 @@
     AudioRecordTarget *m_recordTarget;
     
     sv_frame_t m_globalCentreFrame;
-    int m_globalZoom;
+    ZoomLevel m_globalZoom;
     mutable sv_frame_t m_playbackFrame;
     Model *m_playbackModel; //!!!
     sv_samplerate_t m_mainModelSampleRate;
--- a/view/ViewProxy.h	Wed Oct 03 12:59:55 2018 +0100
+++ b/view/ViewProxy.h	Wed Oct 10 08:44:27 2018 +0100
@@ -75,12 +75,20 @@
                                  bool &log) const {
         return m_view->getValueExtents(unit, min, max, log);
     }
-    virtual int getZoomLevel() const {
-        int z = m_view->getZoomLevel();
+    virtual ZoomLevel getZoomLevel() const {
+        ZoomLevel z = m_view->getZoomLevel();
+        //!!!
 //        cerr << "getZoomLevel: from " << z << " to ";
-        z = z / m_scaleFactor;
+        if (z.zone == ZoomLevel::FramesPerPixel) {
+            z.level /= m_scaleFactor;
+            if (z.level < 1) {
+                z.level = 1;
+            }
+        } else {
+            //!!!???
+            z.level *= m_scaleFactor;
+        }
 //        cerr << z << endl;
-        if (z < 1) z = 1;
         return z;
     }
     virtual QRect getPaintRect() const {