changeset 1549:2fec0d9bd7ac zoom

Merge from default branch
author Chris Cannam
date Fri, 05 Oct 2018 10:25:25 +0100 (2018-10-05)
parents 32400727bcbd (diff) 51d6551d5244 (current diff)
children 4de4284d0596 5f9c9d8c3de6
files
diffstat 19 files changed, 1042 insertions(+), 143 deletions(-) [+]
line wrap: on
line diff
--- a/base/RealTime.h	Wed Oct 03 15:45:57 2018 +0100
+++ b/base/RealTime.h	Fri Oct 05 10:25:25 2018 +0100
@@ -57,7 +57,8 @@
         sec(r.sec), nsec(r.nsec) { }
 
     static RealTime fromSeconds(double sec);
-    static RealTime fromMilliseconds(int msec);
+    static RealTime fromMilliseconds(int64_t msec);
+    static RealTime fromMicroseconds(int64_t usec);
     static RealTime fromTimeval(const struct timeval &);
     static RealTime fromXsdDuration(std::string xsdd);
 
@@ -171,7 +172,8 @@
      * Unlike toText, this function does not depend on the application
      * preferences.
      */
-    std::string toFrameText(int fps, bool hms) const;
+    std::string toFrameText(int fps, bool hms,
+                            std::string frameDelimiter = ":") const;
 
     /**
      * Return a user-readable string to the nearest second, in H:M:S
--- a/base/RealTimeSV.cpp	Wed Oct 03 15:45:57 2018 +0100
+++ b/base/RealTimeSV.cpp	Fri Oct 05 10:25:25 2018 +0100
@@ -61,9 +61,29 @@
 }
 
 RealTime
-RealTime::fromMilliseconds(int msec)
+RealTime::fromMilliseconds(int64_t msec)
 {
-    return RealTime(msec / 1000, (msec % 1000) * 1000000);
+    int64_t sec = msec / 1000;
+    if (sec > INT_MAX || sec < INT_MIN) {
+        cerr << "WARNING: millisecond value out of range for RealTime, "
+             << "returning zero instead: " << msec << endl;
+        return RealTime::zeroTime;
+    }
+        
+    return RealTime(int(sec), int(msec % 1000) * 1000000);
+}
+
+RealTime
+RealTime::fromMicroseconds(int64_t usec)
+{
+    int64_t sec = usec / 1000000;
+    if (sec > INT_MAX || sec < INT_MIN) {
+        cerr << "WARNING: microsecond value out of range for RealTime, "
+             << "returning zero instead: " << usec << endl;
+        return RealTime::zeroTime;
+    }
+    
+    return RealTime(int(sec), int(usec % 1000000) * 1000);
 }
 
 RealTime
@@ -258,20 +278,27 @@
 
     Preferences *p = Preferences::getInstance();
     bool hms = true;
+    std::string frameDelimiter = ":";
     
     if (p) {
         hms = p->getShowHMS();
         int fps = 0;
         switch (p->getTimeToTextMode()) {
-        case Preferences::TimeToTextMs: break;
-        case Preferences::TimeToTextUs: fps = 1000000; break;
+        case Preferences::TimeToTextMs:
+            break;
+        case Preferences::TimeToTextUs:
+            fps = 1000000;
+            frameDelimiter = ".";
+            break;
         case Preferences::TimeToText24Frame: fps = 24; break;
         case Preferences::TimeToText25Frame: fps = 25; break;
         case Preferences::TimeToText30Frame: fps = 30; break;
         case Preferences::TimeToText50Frame: fps = 50; break;
         case Preferences::TimeToText60Frame: fps = 60; break;
         }
-        if (fps != 0) return toFrameText(fps, hms);
+        if (fps != 0) {
+            return toFrameText(fps, hms, frameDelimiter);
+        }
     }
 
     return toMSText(fixedDp, hms);
@@ -338,9 +365,11 @@
 }
 
 std::string
-RealTime::toFrameText(int fps, bool hms) const
+RealTime::toFrameText(int fps, bool hms, std::string frameDelimiter) const
 {
-    if (*this < RealTime::zeroTime) return "-" + (-*this).toFrameText(fps, hms);
+    if (*this < RealTime::zeroTime) {
+        return "-" + (-*this).toFrameText(fps, hms);
+    }
 
     std::stringstream out;
 
@@ -357,7 +386,7 @@
         div *= 10;
     }
 
-    out << ":";
+    out << frameDelimiter;
 
 //    cerr << "div = " << div << ", f =  "<< f << endl;
 
--- a/base/ZoomConstraint.h	Wed Oct 03 15:45:57 2018 +0100
+++ b/base/ZoomConstraint.h	Fri Oct 05 10:25:25 2018 +0100
@@ -18,6 +18,8 @@
 
 #include <stdlib.h>
 
+#include "ZoomLevel.h"
+
 /**
  * ZoomConstraint is a simple interface that describes a limitation on
  * the available zoom sizes for a view, for example based on cache
@@ -39,30 +41,42 @@
     };
 
     /**
-     * Given the "ideal" block size (frames per pixel) for a given
-     * zoom level, return the nearest viable block size for this
-     * constraint.
+     * Given an "ideal" zoom level (frames per pixel or pixels per
+     * frame) for a given zoom level, return the nearest viable block
+     * size for this constraint.
      *
      * For example, if a block size of 1523 frames per pixel is
      * requested but the underlying model only supports value
      * summaries at powers-of-two block sizes, return 1024 or 2048
      * depending on the rounding direction supplied.
      */
-    virtual int getNearestBlockSize(int requestedBlockSize,
-                                    RoundingDirection = RoundNearest)
+    virtual ZoomLevel getNearestZoomLevel(ZoomLevel requestedZoomLevel,
+                                          RoundingDirection = RoundNearest)
         const
     {
-        if (requestedBlockSize > getMaxZoomLevel()) return getMaxZoomLevel();
-        else return requestedBlockSize;
+        if (getMaxZoomLevel() < requestedZoomLevel) return getMaxZoomLevel();
+	else return requestedZoomLevel;
     }
 
     /**
+     * Return the minimum zoom level within range for this constraint.
+     * Individual views will probably want to limit this, for example
+     * in order to ensure that at least one or two samples fit in the
+     * current window size, or in order to save on interpolation cost.
+     */
+    virtual ZoomLevel getMinZoomLevel() const {
+        return { ZoomLevel::PixelsPerFrame, 512 };
+    }
+    
+    /**
      * Return the maximum zoom level within range for this constraint.
      * This is quite large -- individual views will probably want to
      * limit how far a user might reasonably zoom out based on other
      * factors such as the duration of the file.
      */
-    virtual int getMaxZoomLevel() const { return 4194304; } // 2^22, arbitrarily
+    virtual ZoomLevel getMaxZoomLevel() const {
+        return { ZoomLevel::FramesPerPixel, 4194304 }; // 2^22, arbitrarily
+    }
 };
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/ZoomLevel.cpp	Fri Oct 05 10:25:25 2018 +0100
@@ -0,0 +1,25 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ZoomLevel.h"
+
+std::ostream &operator<<(std::ostream &s, const ZoomLevel &z) {
+    if (z.zone == ZoomLevel::PixelsPerFrame) {
+        s << "1/" << z.level;
+    } else {
+        s << z.level;
+    }
+    return s;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/ZoomLevel.h	Fri Oct 05 10:25:25 2018 +0100
@@ -0,0 +1,124 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_ZOOM_LEVEL_H
+#define SV_ZOOM_LEVEL_H
+
+#include "BaseTypes.h"
+
+#include <ostream>
+#include <cmath>
+
+/** Display zoom level. Can be an integer number of samples per pixel,
+ *  or an integer number of pixels per sample.
+ */
+struct ZoomLevel {
+
+    enum Zone {
+        FramesPerPixel, // zoomed out (as in classic SV)
+        PixelsPerFrame  // zoomed in beyond 1-1 (interpolating the waveform)
+    };
+
+    Zone zone;
+    int level;
+
+    ZoomLevel() : zone(FramesPerPixel), level(1) { }
+    ZoomLevel(Zone z, int lev) : zone(z), level(lev) { }
+    
+    bool operator<(const ZoomLevel &other) const {
+        if (zone == FramesPerPixel) {
+            if (other.zone == zone) {
+                return level < other.level;
+            } else {
+                return false;
+            }
+        } else {
+            if (other.zone == zone) {
+                return level > other.level;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    bool operator==(const ZoomLevel &other) const {
+        return (zone == other.zone && level == other.level);
+    }
+
+    ZoomLevel incremented() const {
+        if (zone == FramesPerPixel) {
+            return { zone, level + 1 };
+        } else if (level == 1) {
+            return { FramesPerPixel, 2 };
+        } else if (level == 2) {
+            return { FramesPerPixel, 1 };
+        } else {
+            return { zone, level - 1 };
+        }
+    }
+
+    ZoomLevel decremented() const {
+        if (zone == PixelsPerFrame) {
+            return { zone, level + 1 };
+        } else if (level == 1) {
+            return { PixelsPerFrame, 2 };
+        } else {
+            return { zone, level - 1 };
+        }
+    }
+
+    /** Inexact conversion. The result is a whole number if we are
+     *  zoomed in enough (in PixelsPerFrame zone), a fraction
+     *  otherwise.
+     */
+    double framesToPixels(double frames) const {
+        if (zone == PixelsPerFrame) {
+            return frames * level;
+        } else {
+            return frames / level;
+        }
+    }
+
+    /** Inexact conversion. The result is a whole number if we are
+     *  zoomed out enough (in FramesPerPixel zone), a fraction
+     *  otherwise.
+     */
+    double pixelsToFrames(double pixels) const {
+        if (zone == PixelsPerFrame) {
+            return pixels / level;
+        } else {
+            return pixels * level;
+        }
+    }
+
+    /** Return a ZoomLevel that approximates the given ratio of pixels
+     *  to frames.
+     */
+    static ZoomLevel fromRatio(int pixels, sv_frame_t frames) {
+        if (pixels < frames) {
+            return { FramesPerPixel, int(round(double(frames)/pixels)) };
+        } else {
+            int r = int(round(pixels/double(frames)));
+            if (r > 1) {
+                return { PixelsPerFrame, r };
+            } else {
+                return { FramesPerPixel, 1 };
+            }
+        }
+    }
+};
+
+std::ostream &operator<<(std::ostream &s, const ZoomLevel &z);
+
+#endif
--- a/base/test/TestOurRealTime.h	Wed Oct 03 15:45:57 2018 +0100
+++ b/base/test/TestOurRealTime.h	Fri Oct 05 10:25:25 2018 +0100
@@ -138,6 +138,20 @@
         QCOMPARE(RealTime::fromMilliseconds(-1000), RealTime(-1, 0));
         QCOMPARE(RealTime::fromMilliseconds(-1500), RealTime(-1, -ONE_BILLION/2));
     }
+
+    void fromMicroseconds()
+    {
+        QCOMPARE(RealTime::fromMicroseconds(0), RealTime(0, 0));
+        QCOMPARE(RealTime::fromMicroseconds(500000), RealTime(0, ONE_BILLION/2));
+        QCOMPARE(RealTime::fromMicroseconds(1000000), RealTime(1, 0));
+        QCOMPARE(RealTime::fromMicroseconds(1500000), RealTime(1, ONE_BILLION/2));
+
+        QCOMPARE(RealTime::fromMicroseconds(-0), RealTime(0, 0));
+        QCOMPARE(RealTime::fromMicroseconds(-500000), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime::fromMicroseconds(-1000000), RealTime(-1, 0));
+        QCOMPARE(RealTime::fromMicroseconds(-1500000), RealTime(-1, -ONE_BILLION/2));
+        QCOMPARE(RealTime::fromMicroseconds(13500000), RealTime(13, ONE_BILLION/2));
+    }
     
     void fromTimeval()
     {
--- a/data/model/PowerOfSqrtTwoZoomConstraint.cpp	Wed Oct 03 15:45:57 2018 +0100
+++ b/data/model/PowerOfSqrtTwoZoomConstraint.cpp	Fri Oct 05 10:25:25 2018 +0100
@@ -21,13 +21,30 @@
 #include "base/Debug.h"
 
 
-int
-PowerOfSqrtTwoZoomConstraint::getNearestBlockSize(int blockSize,
+ZoomLevel
+PowerOfSqrtTwoZoomConstraint::getNearestZoomLevel(ZoomLevel requested,
                                                   RoundingDirection dir) const
 {
     int type, power;
-    int rv = getNearestBlockSize(blockSize, type, power, dir);
-    return rv;
+    int blockSize;
+
+    if (requested.zone == ZoomLevel::FramesPerPixel) {
+        blockSize = getNearestBlockSize(requested.level, type, power, dir);
+        return { requested.zone, blockSize };
+    } else {
+        RoundingDirection opposite = dir;
+        if (dir == RoundUp) opposite = RoundDown;
+        else if (dir == RoundDown) opposite = RoundUp;
+        blockSize = getNearestBlockSize(requested.level, type, power, opposite);
+        if (blockSize > getMinZoomLevel().level) {
+            blockSize = getMinZoomLevel().level;
+        }
+        if (blockSize == 1) {
+            return { ZoomLevel::FramesPerPixel, 1 };
+        } else {
+            return { requested.zone, blockSize };
+        }
+    }
 }
 
 int
@@ -113,6 +130,9 @@
         prevBase = base;
     }
 
-    if (result > getMaxZoomLevel()) result = getMaxZoomLevel();
+    if (result > getMaxZoomLevel().level) {
+        result = getMaxZoomLevel().level;
+    }
+
     return result;
 }   
--- a/data/model/PowerOfSqrtTwoZoomConstraint.h	Wed Oct 03 15:45:57 2018 +0100
+++ b/data/model/PowerOfSqrtTwoZoomConstraint.h	Fri Oct 05 10:25:25 2018 +0100
@@ -21,17 +21,17 @@
 class PowerOfSqrtTwoZoomConstraint : virtual public ZoomConstraint
 {
 public:
-    virtual int getNearestBlockSize(int requestedBlockSize,
-                                    RoundingDirection dir = RoundNearest)
-        const;
-    
+    virtual ZoomLevel getNearestZoomLevel(ZoomLevel requested,
+                                          RoundingDirection dir = RoundNearest)
+	const override;
+	
+    virtual int getMinCachePower() const { return 6; }
+
     virtual int getNearestBlockSize(int requestedBlockSize,
                                     int &type,
                                     int &power,
                                     RoundingDirection dir = RoundNearest)
         const;
-        
-    virtual int getMinCachePower() const { return 6; }
 };
 
 #endif
--- a/data/model/PowerOfTwoZoomConstraint.cpp	Wed Oct 03 15:45:57 2018 +0100
+++ b/data/model/PowerOfTwoZoomConstraint.cpp	Fri Oct 05 10:25:25 2018 +0100
@@ -15,11 +15,39 @@
 
 #include "PowerOfTwoZoomConstraint.h"
 
+ZoomLevel
+PowerOfTwoZoomConstraint::getNearestZoomLevel(ZoomLevel requested,
+                                              RoundingDirection dir) const
+{
+    int blockSize;
+
+    if (requested.zone == ZoomLevel::FramesPerPixel) {
+        blockSize = getNearestBlockSize(requested.level, dir);
+        if (blockSize > getMaxZoomLevel().level) {
+            blockSize = getMaxZoomLevel().level;
+        }
+        return { requested.zone, blockSize };
+    } else {
+        RoundingDirection opposite = dir;
+        if (dir == RoundUp) opposite = RoundDown;
+        else if (dir == RoundDown) opposite = RoundUp;
+        blockSize = getNearestBlockSize(requested.level, opposite);
+        if (blockSize > getMinZoomLevel().level) {
+            blockSize = getMinZoomLevel().level;
+        }
+        if (blockSize == 1) {
+            return { ZoomLevel::FramesPerPixel, 1 };
+        } else {
+            return { requested.zone, blockSize };
+        }
+    }
+}
+
 int
 PowerOfTwoZoomConstraint::getNearestBlockSize(int req,
                                               RoundingDirection dir) const
 {
-    int max = getMaxZoomLevel();
+    int max = getMaxZoomLevel().level;
 
     if (req > max) {
         return max;
--- a/data/model/PowerOfTwoZoomConstraint.h	Wed Oct 03 15:45:57 2018 +0100
+++ b/data/model/PowerOfTwoZoomConstraint.h	Fri Oct 05 10:25:25 2018 +0100
@@ -21,7 +21,12 @@
 class PowerOfTwoZoomConstraint : virtual public ZoomConstraint
 {
 public:
-    virtual int getNearestBlockSize(int requestedBlockSize,
+    virtual ZoomLevel getNearestZoomLevel(ZoomLevel requested,
+                                          RoundingDirection dir = RoundNearest)
+	const override;
+
+protected:
+    virtual int getNearestBlockSize(int requested,
                                     RoundingDirection dir = RoundNearest)
         const;
 };
--- a/data/model/ReadOnlyWaveFileModel.cpp	Wed Oct 03 15:45:57 2018 +0100
+++ b/data/model/ReadOnlyWaveFileModel.cpp	Fri Oct 05 10:25:25 2018 +0100
@@ -341,6 +341,7 @@
     int power = m_zoomConstraint.getMinCachePower();
     int roundedBlockSize = m_zoomConstraint.getNearestBlockSize
         (desired, cacheType, power, ZoomConstraint::RoundDown);
+
     if (cacheType != 0 && cacheType != 1) {
         // We will be reading directly from file, so can satisfy any
         // blocksize requirement
--- a/data/model/WaveFileModel.h	Wed Oct 03 15:45:57 2018 +0100
+++ b/data/model/WaveFileModel.h	Fri Oct 05 10:25:25 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef WAVE_FILE_MODEL_H
-#define WAVE_FILE_MODEL_H
+#ifndef SV_WAVE_FILE_MODEL_H
+#define SV_WAVE_FILE_MODEL_H
 
 #include "RangeSummarisableTimeValueModel.h"
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/WaveformOversampler.cpp	Fri Oct 05 10:25:25 2018 +0100
@@ -0,0 +1,295 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+   
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "WaveformOversampler.h"
+
+#include "base/Profiler.h"
+
+#include "data/model/DenseTimeValueModel.h"
+
+floatvec_t
+WaveformOversampler::getOversampledData(const DenseTimeValueModel *source,
+                                        int channel,
+                                        sv_frame_t sourceStartFrame,
+                                        sv_frame_t sourceFrameCount,
+                                        int oversampleBy)
+{
+    Profiler profiler("WaveformOversampler::getOversampledData");
+    
+    // Oversampled at a fixed ratio of m_filterRatio
+    floatvec_t fixedRatio = getFixedRatioData(source, channel,
+                                              sourceStartFrame,
+                                              sourceFrameCount);
+    sv_frame_t fixedCount = fixedRatio.size();
+    sv_frame_t targetCount = (fixedCount / m_filterRatio) * oversampleBy;
+
+    // And apply linear interpolation to the desired factor
+    
+    floatvec_t result(targetCount, 0.f);
+
+    for (int i = 0; i < targetCount; ++i) {
+        double pos = (double(i) / oversampleBy) * m_filterRatio;
+        double diff = pos - floor(pos);
+        int ix = int(floor(pos));
+        double interpolated = (1.0 - diff) * fixedRatio[ix];
+        if (in_range_for(fixedRatio, ix + 1)) {
+            interpolated += diff * fixedRatio[ix + 1];
+        }
+        result[i] = float(interpolated);
+    }
+
+    return result;
+}
+
+floatvec_t
+WaveformOversampler::getFixedRatioData(const DenseTimeValueModel *source,
+                                       int channel,
+                                       sv_frame_t sourceStartFrame,
+                                       sv_frame_t sourceFrameCount)
+{
+    Profiler profiler("WaveformOversampler::getFixedRatioData");
+    
+    sv_frame_t sourceLength = source->getEndFrame();
+    
+    if (sourceStartFrame + sourceFrameCount > sourceLength) {
+        sourceFrameCount = sourceLength - sourceStartFrame;
+        if (sourceFrameCount <= 0) return {};
+    }
+
+    sv_frame_t targetFrameCount = sourceFrameCount * m_filterRatio;
+    
+    sv_frame_t filterLength = m_filter.size(); // NB this is known to be odd
+    sv_frame_t filterTailOut = (filterLength - 1) / 2;
+    sv_frame_t filterTailIn = filterTailOut / m_filterRatio;
+
+    floatvec_t oversampled(targetFrameCount, 0.f);
+
+    sv_frame_t i0 = sourceStartFrame - filterTailIn;
+    if (i0 < 0) {
+        i0 = 0;
+    }
+    sv_frame_t i1 = sourceStartFrame + sourceFrameCount + filterTailIn;
+    if (i1 > sourceLength) {
+        i1 = sourceLength;
+    }
+    
+    floatvec_t sourceData = source->getData(channel, i0, i1 - i0);
+    
+    for (sv_frame_t i = i0; i < i1; ++i) {
+        float v = sourceData[i - i0];
+        sv_frame_t outOffset =
+            (i - sourceStartFrame) * m_filterRatio - filterTailOut;
+        for (sv_frame_t j = 0; j < filterLength; ++j) {
+            sv_frame_t outIndex = outOffset + j;
+            if (outIndex < 0 || outIndex >= targetFrameCount) {
+                continue;
+            }
+            oversampled[outIndex] += v * m_filter[j];
+        }
+    }
+
+    return oversampled;
+}
+
+int
+WaveformOversampler::m_filterRatio = 8;
+
+/// Precalculated windowed sinc FIR filter for oversampling ratio of 8
+floatvec_t
+WaveformOversampler::m_filter {
+    2.0171043153063023E-4, 2.887198196326776E-4,
+    3.410439309101285E-4, 3.4267123819805857E-4,
+    2.843462511901066E-4, 1.6636986363946504E-4,
+    -4.5940658605786285E-18, -1.9299665002484582E-4,
+    -3.8279951732549946E-4, -5.357990649609105E-4,
+    -6.201170748425957E-4, -6.11531555444137E-4,
+    -4.987822892899791E-4, -2.872272251922189E-4,
+    -7.822991648518709E-19, 3.2382854144162815E-4,
+    6.341027017666046E-4, 8.769331519465396E-4,
+    0.001003535186382615, 9.791732608026272E-4,
+    7.906678783612421E-4, 4.5101220009813206E-4,
+    -3.039648514978356E-18, -4.996532051508215E-4,
+    -9.704877518513666E-4, -0.0013318083550190923,
+    -0.0015128911871247466, -0.0014658111457301016,
+    -0.0011756800671747431, -6.663214707558645E-4,
+    1.0713650598357415E-17, 7.292959363289514E-4,
+    0.0014084768982220279, 0.0019222969998680237,
+    0.0021721723797956524, 0.0020938999751673173,
+    0.0016712330766289326, 9.427050283841188E-4,
+    -5.656965938821965E-18, -0.0010225643654040554,
+    -0.001966437013513757, -0.002672722670880038,
+    -0.00300806671037164, -0.00288843624179131,
+    -0.0022967244980623574, -0.0012908081494665458,
+    -5.1499690577098E-18, 0.0013904094522721,
+    0.0026648961861419334, 0.0036103002009868065,
+    0.004050469159316014, 0.0038774554290217484,
+    0.0030739396559265075, 0.001722603817632299,
+    -9.130030250503607E-18, -0.0018451873718735516,
+    -0.0035270571169279162, -0.004765847116110058,
+    -0.0053332982334767624, -0.005092831604550132,
+    -0.00402770012894082, -0.0022517645624319594,
+    3.2752446397299053E-17, 0.0024010765839506923,
+    0.004579613038976446, 0.006174912111845945,
+    0.00689578873526276, 0.006571541332393174,
+    0.005186887306036285, 0.002894248521447605,
+    -1.336645565990815E-17, -0.0030747336558684963,
+    -0.0058540294958507235, -0.007879546416595632,
+    -0.008784519668338507, -0.008357645279493864,
+    -0.006586046547485615, -0.003669217725935383,
+    -1.9348975378752276E-17, 0.0038863208094135626,
+    0.007388553022623823, 0.009931080628244226,
+    0.011056594746806033, 0.010505398026651453,
+    0.008267906564613223, 0.00460048159140493,
+    -1.816145184081109E-17, -0.004861124757802925,
+    -0.009231379891669668, -0.012394511669674028,
+    -0.01378467517229709, -0.013084177592430083,
+    -0.010287380585427207, -0.00571879407588959,
+    7.520535431851951E-17, 0.006032144534828161,
+    0.011445734103106982, 0.015355551390625357,
+    0.017065088242935025, 0.016186450815185452,
+    0.012718051439340603, 0.0070655888687995785,
+    -2.3209664144996714E-17, -0.007444328311482942,
+    -0.01411821163125819, -0.018932253281654043,
+    -0.02103125585328301, -0.019941019333653123,
+    -0.015663002335303704, -0.008699245445932525,
+    2.5712475624993567E-17, 0.009161748270723635,
+    0.0173729814451255, 0.023294901939595228,
+    0.02587678878709242, 0.02453592568963366,
+    0.01927365323131565, 0.010706050935569809,
+    -2.8133472199037193E-17, -0.011280308241551094,
+    -0.02139710071477064, -0.02870170641615764,
+    -0.031897218249350504, -0.030260140480986304,
+    -0.023784294156618507, -0.013220449772289724,
+    3.042099156841831E-17, 0.013951594368737923,
+    0.0264884258371512, 0.03556693609945249,
+    0.03957036852169639, 0.0375845888664677,
+    0.029579845398822833, 0.016465167405787955,
+    -3.2524514488155654E-17, -0.017431115375410273,
+    -0.03315356952091943, -0.04460179422099746,
+    -0.0497244634100025, -0.04733366619358394,
+    -0.037341081614037944, -0.020838316594689998,
+    3.439626695384943E-17, 0.022185914535618936,
+    0.04232958202159685, 0.05713801867687856,
+    0.06393033000280622, 0.06109191933191721,
+    0.04839482380906132, 0.027127167584840003,
+    -3.5992766927138734E-17, -0.029168755716052385,
+    -0.055960213335110184, -0.07598693477350407,
+    -0.08556575102599769, -0.08233350786406181,
+    -0.06571046000158454, -0.03713224848702707,
+    3.727625511036616E-17, 0.04066438975848791,
+    0.07882920057770397, 0.10826166115536123,
+    0.12343378955977465, 0.12040455825217859,
+    0.09755344650130694, 0.056053367635801106,
+    -3.8215953158245473E-17, -0.0638435745677513,
+    -0.12667849902789644, -0.17861887575594584,
+    -0.20985333136704623, -0.21188193950868073,
+    -0.17867464086818077, -0.10760048593620072,
+    3.8789099095340224E-17, 0.13868670259490817,
+    0.29927055936918734, 0.46961864377510765,
+    0.6358321371992203, 0.7836674214332147,
+    0.9000377382311825, 0.9744199685311685,
+    1.0000000000000004, 0.9744199685311685,
+    0.9000377382311825, 0.7836674214332147,
+    0.6358321371992203, 0.46961864377510765,
+    0.29927055936918734, 0.13868670259490817,
+    3.8789099095340224E-17, -0.10760048593620072,
+    -0.17867464086818077, -0.21188193950868073,
+    -0.20985333136704623, -0.17861887575594584,
+    -0.12667849902789644, -0.0638435745677513,
+    -3.8215953158245473E-17, 0.056053367635801106,
+    0.09755344650130694, 0.12040455825217859,
+    0.12343378955977465, 0.10826166115536123,
+    0.07882920057770397, 0.04066438975848791,
+    3.727625511036616E-17, -0.03713224848702707,
+    -0.06571046000158454, -0.08233350786406181,
+    -0.08556575102599769, -0.07598693477350407,
+    -0.055960213335110184, -0.029168755716052385,
+    -3.5992766927138734E-17, 0.027127167584840003,
+    0.04839482380906132, 0.06109191933191721,
+    0.06393033000280622, 0.05713801867687856,
+    0.04232958202159685, 0.022185914535618936,
+    3.439626695384943E-17, -0.020838316594689998,
+    -0.037341081614037944, -0.04733366619358394,
+    -0.0497244634100025, -0.04460179422099746,
+    -0.03315356952091943, -0.017431115375410273,
+    -3.2524514488155654E-17, 0.016465167405787955,
+    0.029579845398822833, 0.0375845888664677,
+    0.03957036852169639, 0.03556693609945249,
+    0.0264884258371512, 0.013951594368737923,
+    3.042099156841831E-17, -0.013220449772289724,
+    -0.023784294156618507, -0.030260140480986304,
+    -0.031897218249350504, -0.02870170641615764,
+    -0.02139710071477064, -0.011280308241551094,
+    -2.8133472199037193E-17, 0.010706050935569809,
+    0.01927365323131565, 0.02453592568963366,
+    0.02587678878709242, 0.023294901939595228,
+    0.0173729814451255, 0.009161748270723635,
+    2.5712475624993567E-17, -0.008699245445932525,
+    -0.015663002335303704, -0.019941019333653123,
+    -0.02103125585328301, -0.018932253281654043,
+    -0.01411821163125819, -0.007444328311482942,
+    -2.3209664144996714E-17, 0.0070655888687995785,
+    0.012718051439340603, 0.016186450815185452,
+    0.017065088242935025, 0.015355551390625357,
+    0.011445734103106982, 0.006032144534828161,
+    7.520535431851951E-17, -0.00571879407588959,
+    -0.010287380585427207, -0.013084177592430083,
+    -0.01378467517229709, -0.012394511669674028,
+    -0.009231379891669668, -0.004861124757802925,
+    -1.816145184081109E-17, 0.00460048159140493,
+    0.008267906564613223, 0.010505398026651453,
+    0.011056594746806033, 0.009931080628244226,
+    0.007388553022623823, 0.0038863208094135626,
+    -1.9348975378752276E-17, -0.003669217725935383,
+    -0.006586046547485615, -0.008357645279493864,
+    -0.008784519668338507, -0.007879546416595632,
+    -0.0058540294958507235, -0.0030747336558684963,
+    -1.336645565990815E-17, 0.002894248521447605,
+    0.005186887306036285, 0.006571541332393174,
+    0.00689578873526276, 0.006174912111845945,
+    0.004579613038976446, 0.0024010765839506923,
+    3.2752446397299053E-17, -0.0022517645624319594,
+    -0.00402770012894082, -0.005092831604550132,
+    -0.0053332982334767624, -0.004765847116110058,
+    -0.0035270571169279162, -0.0018451873718735516,
+    -9.130030250503607E-18, 0.001722603817632299,
+    0.0030739396559265075, 0.0038774554290217484,
+    0.004050469159316014, 0.0036103002009868065,
+    0.0026648961861419334, 0.0013904094522721,
+    -5.1499690577098E-18, -0.0012908081494665458,
+    -0.0022967244980623574, -0.00288843624179131,
+    -0.00300806671037164, -0.002672722670880038,
+    -0.001966437013513757, -0.0010225643654040554,
+    -5.656965938821965E-18, 9.427050283841188E-4,
+    0.0016712330766289326, 0.0020938999751673173,
+    0.0021721723797956524, 0.0019222969998680237,
+    0.0014084768982220279, 7.292959363289514E-4,
+    1.0713650598357415E-17, -6.663214707558645E-4,
+    -0.0011756800671747431, -0.0014658111457301016,
+    -0.0015128911871247466, -0.0013318083550190923,
+    -9.704877518513666E-4, -4.996532051508215E-4,
+    -3.039648514978356E-18, 4.5101220009813206E-4,
+    7.906678783612421E-4, 9.791732608026272E-4,
+    0.001003535186382615, 8.769331519465396E-4,
+    6.341027017666046E-4, 3.2382854144162815E-4,
+    -7.822991648518709E-19, -2.872272251922189E-4,
+    -4.987822892899791E-4, -6.11531555444137E-4,
+    -6.201170748425957E-4, -5.357990649609105E-4,
+    -3.8279951732549946E-4, -1.9299665002484582E-4,
+    -4.5940658605786285E-18, 1.6636986363946504E-4,
+    2.843462511901066E-4, 3.4267123819805857E-4,
+    3.410439309101285E-4, 2.887198196326776E-4,
+    2.0171043153063023E-4
+};
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/WaveformOversampler.h	Fri Oct 05 10:25:25 2018 +0100
@@ -0,0 +1,57 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+   
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_WAVEFORM_OVERSAMPLER_H
+#define SV_WAVEFORM_OVERSAMPLER_H
+
+#include "base/BaseTypes.h"
+
+class DenseTimeValueModel;
+
+/** Oversample the sample data from a DenseTimeValueModel by an
+ *  integer factor, on the assumption that the model represents
+ *  audio. Oversampling is carried out using a windowed sinc filter
+ *  for a fixed 8x ratio with further linear interpolation to handle
+ *  other ratios. The aim is not to provide the "best-sounding"
+ *  interpolation, but to provide accurate and predictable projections
+ *  of the theoretical waveform shape for display rendering without
+ *  leaving decisions about interpolation up to a resampler library.
+ */
+class WaveformOversampler
+{
+public:
+    /** Return an oversampled version of the audio data from the given
+     *  source sample range. Will query sufficient source audio before
+     *  and after the requested range (where available) to ensure an
+     *  accurate-looking result after filtering. The returned vector
+     *  will have sourceFrameCount * oversampleBy samples, except when
+     *  truncated because the end of the model was reached.
+     */
+    static floatvec_t getOversampledData(const DenseTimeValueModel *source,
+                                         int channel,
+                                         sv_frame_t sourceStartFrame,
+                                         sv_frame_t sourceFrameCount,
+                                         int oversampleBy);
+
+private:
+    static floatvec_t getFixedRatioData(const DenseTimeValueModel *source,
+                                        int channel,
+                                        sv_frame_t sourceStartFrame,
+                                        sv_frame_t sourceFrameCount);
+    
+    static int m_filterRatio;
+    static floatvec_t m_filter;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/test/TestWaveformOversampler.h	Fri Oct 05 10:25:25 2018 +0100
@@ -0,0 +1,251 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+  Sonic Visualiser
+  An audio file viewer and annotation editor.
+  Centre for Digital Music, Queen Mary, University of London.
+    
+  This program is free software; you can redistribute it and/or
+  modify it under the terms of the GNU General Public License as
+  published by the Free Software Foundation; either version 2 of the
+  License, or (at your option) any later version.  See the file
+  COPYING included with this distribution for more information.
+*/
+
+#ifndef TEST_WAVEFORM_OVERSAMPLER_H
+#define TEST_WAVEFORM_OVERSAMPLER_H
+
+#include "../WaveformOversampler.h"
+#include "../WritableWaveFileModel.h"
+
+#include "../../../base/BaseTypes.h"
+
+#include <QObject>
+#include <QtTest>
+
+class TestWaveformOversampler : public QObject
+{
+    Q_OBJECT
+
+public:
+    TestWaveformOversampler() {
+        m_source = floatvec_t(5000, 0.f);
+        m_source[0] = 1.f;
+        m_source[2500] = 0.5f;
+        m_source[2501] = -0.5f;
+        m_source[4999] = -1.f;
+        for (int i = 3000; i < 3900; ++i) {
+            m_source[i] = float(sin(double(i - 3000) * M_PI / 50.0));
+        }
+        m_sourceModel = new WritableWaveFileModel(8000, 1);
+        const float *d = m_source.data();
+        QVERIFY(m_sourceModel->addSamples(&d, m_source.size()));
+        m_sourceModel->writeComplete();
+    }
+
+    ~TestWaveformOversampler() {
+        delete m_sourceModel;
+    }
+
+private:
+    floatvec_t m_source;
+    WritableWaveFileModel *m_sourceModel;
+
+    void compareStrided(floatvec_t obtained, floatvec_t expected, int stride) {
+        QCOMPARE(obtained.size(), expected.size() * stride);
+        float threshold = 1e-10f;
+        for (int i = 0; in_range_for(expected, i); ++i) {
+            if (fabsf(obtained[i * stride] - expected[i]) > threshold) {
+                std::cerr << "At position " << i * stride << ": "
+                          << obtained[i * stride] << " != " << expected[i]
+                          << std::endl;
+                QCOMPARE(obtained, expected);
+            }
+        }
+    }
+
+    void compareVecs(floatvec_t obtained, floatvec_t expected) {
+        compareStrided(obtained, expected, 1);
+    }
+
+    floatvec_t get(sv_frame_t sourceStartFrame,
+                   sv_frame_t sourceFrameCount,
+                   int oversampleBy) {
+        return WaveformOversampler::getOversampledData
+            (m_sourceModel, 0,
+             sourceStartFrame, sourceFrameCount, oversampleBy);
+    }
+    
+    void testVerbatim(sv_frame_t sourceStartFrame,
+                      sv_frame_t sourceFrameCount,
+                      int oversampleBy,
+                      floatvec_t expected) {
+        floatvec_t output =
+            get(sourceStartFrame, sourceFrameCount, oversampleBy);
+        compareVecs(output, expected);
+    }
+
+    void testStrided(sv_frame_t sourceStartFrame,
+                     sv_frame_t sourceFrameCount,
+                     int oversampleBy,
+                     floatvec_t expected) {
+        // check only the values that are expected to be precisely the
+        // original samples
+        floatvec_t output =
+            get(sourceStartFrame, sourceFrameCount, oversampleBy);
+        compareStrided(output, expected, oversampleBy);
+    }
+
+    floatvec_t sourceSubset(sv_frame_t start, sv_frame_t length) {
+        return floatvec_t(m_source.begin() + start,
+                          m_source.begin() + start + length);
+    }
+
+private slots:
+    void testWholeVerbatim() {
+        testVerbatim(0, 5000, 1, m_source);
+    }
+
+    void testSubsetsVerbatim() {
+        testVerbatim(0, 500, 1, sourceSubset(0, 500));
+        testVerbatim(4500, 500, 1, sourceSubset(4500, 500));
+        testVerbatim(2000, 1000, 1, sourceSubset(2000, 1000));
+    }
+
+    void testOverlapsVerbatim() {
+        // overlapping the start -> result should be zero-padded to
+        // preserve start frame
+        floatvec_t expected = sourceSubset(0, 400);
+        expected.insert(expected.begin(), 100, 0.f);
+        testVerbatim(-100, 500, 1, expected);
+
+        // overlapping the end -> result should be truncated to
+        // preserve source length
+        expected = sourceSubset(4600, 400);
+        testVerbatim(4600, 500, 1, expected);
+    }
+
+    void testWhole2x() {
+        testStrided(0, 5000, 2, m_source);
+
+        // check for windowed sinc values between the original samples
+        floatvec_t output = get(0, 5000, 2);
+        QVERIFY(output[1] - 0.6358 < 0.0001);
+        QVERIFY(output[3] + 0.2099 < 0.0001);
+    }
+    
+    void testWhole3x() {
+        testStrided(0, 5000, 3, m_source);
+
+        // check for windowed sinc values between the original samples
+        floatvec_t output = get(0, 5000, 3);
+        QVERIFY(output[1] > 0.7);
+        QVERIFY(output[2] > 0.4);
+        QVERIFY(output[4] < -0.1);
+        QVERIFY(output[5] < -0.1);
+    }
+    
+    void testWhole4x() {
+        testStrided(0, 5000, 4, m_source);
+
+        // check for windowed sinc values between the original samples
+        floatvec_t output = get(0, 5000, 4);
+        QVERIFY(output[1] - 0.9000 < 0.0001);
+        QVERIFY(output[2] - 0.6358 < 0.0001);
+        QVERIFY(output[3] - 0.2993 < 0.0001);
+        QVERIFY(output[5] + 0.1787 < 0.0001);
+        QVERIFY(output[6] + 0.2099 < 0.0001);
+        QVERIFY(output[7] + 0.1267 < 0.0001);
+
+        // alternate values at 2n should equal all values at n
+        output = get(0, 5000, 4);
+        floatvec_t half = get(0, 5000, 2);
+        compareStrided(output, half, 2);
+    }
+    
+    void testWhole8x() {
+        testStrided(0, 5000, 8, m_source);
+
+        // alternate values at 2n should equal all values at n
+        floatvec_t output = get(0, 5000, 8);
+        floatvec_t half = get(0, 5000, 4);
+        compareStrided(output, half, 2);
+    }
+    
+    void testWhole10x() {
+        testStrided(0, 5000, 10, m_source);
+
+        // alternate values at 2n should equal all values at n
+        floatvec_t output = get(0, 5000, 10);
+        floatvec_t half = get(0, 5000, 5);
+        compareStrided(output, half, 2);
+    }
+    
+    void testWhole16x() {
+        testStrided(0, 5000, 16, m_source);
+
+        // alternate values at 2n should equal all values at n
+        floatvec_t output = get(0, 5000, 16);
+        floatvec_t half = get(0, 5000, 8);
+        compareStrided(output, half, 2);
+    }
+    
+    void testSubsets4x() {
+        testStrided(0, 500, 4, sourceSubset(0, 500));
+        testStrided(4500, 500, 4, sourceSubset(4500, 500));
+        testStrided(2000, 1000, 4, sourceSubset(2000, 1000));
+
+        // check for windowed sinc values between the original
+        // samples, even when the original sample that was the source
+        // of this sinc kernel is not within the requested range
+        floatvec_t output = get(1, 10, 4);
+        QVERIFY(output[0] < 0.0001);
+        QVERIFY(output[1] + 0.1787 < 0.0001);
+        QVERIFY(output[2] + 0.2099 < 0.0001);
+        QVERIFY(output[3] + 0.1267 < 0.0001);
+
+        // and again at the end
+        output = get(4989, 10, 4);
+        QVERIFY(output[39] + 0.9000 < 0.0001);
+        QVERIFY(output[38] + 0.6358 < 0.0001);
+        QVERIFY(output[37] + 0.2993 < 0.0001);
+        QVERIFY(output[35] - 0.1787 < 0.0001);
+        QVERIFY(output[34] - 0.2099 < 0.0001);
+        QVERIFY(output[33] - 0.1267 < 0.0001);
+    }
+    
+    void testOverlaps4x() {
+        // overlapping the start -> result should be zero-padded to
+        // preserve start frame
+        floatvec_t expected = sourceSubset(0, 400);
+        expected.insert(expected.begin(), 100, 0.f);
+        testStrided(-100, 500, 4, expected);
+
+        // overlapping the end -> result should be truncated to
+        // preserve source length
+        expected = sourceSubset(4600, 400);
+        testStrided(4600, 500, 4, expected);
+    }
+
+    void testSubsets15x() {
+        testStrided(0, 500, 15, sourceSubset(0, 500));
+        testStrided(4500, 500, 15, sourceSubset(4500, 500));
+        testStrided(2000, 1000, 15, sourceSubset(2000, 1000));
+    }
+    
+    void testOverlaps15x() {
+        // overlapping the start -> result should be zero-padded to
+        // preserve start frame
+        floatvec_t expected = sourceSubset(0, 400);
+        expected.insert(expected.begin(), 100, 0.f);
+        testStrided(-100, 500, 15, expected);
+
+        // overlapping the end -> result should be truncated to
+        // preserve source length
+        expected = sourceSubset(4600, 400);
+        testStrided(4600, 500, 15, expected);
+    }
+};
+
+
+#endif
--- a/data/model/test/TestZoomConstraints.h	Wed Oct 03 15:45:57 2018 +0100
+++ b/data/model/test/TestZoomConstraints.h	Fri Oct 05 10:25:25 2018 +0100
@@ -1,15 +1,15 @@
 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
 
 /*
-    Sonic Visualiser
-    An audio file viewer and annotation editor.
-    Centre for Digital Music, Queen Mary, University of London.
+  Sonic Visualiser
+  An audio file viewer and annotation editor.
+  Centre for Digital Music, Queen Mary, University of London.
     
-    This program is free software; you can redistribute it and/or
-    modify it under the terms of the GNU General Public License as
-    published by the Free Software Foundation; either version 2 of the
-    License, or (at your option) any later version.  See the file
-    COPYING included with this distribution for more information.
+  This program is free software; you can redistribute it and/or
+  modify it under the terms of the GNU General Public License as
+  published by the Free Software Foundation; either version 2 of the
+  License, or (at your option) any later version.  See the file
+  COPYING included with this distribution for more information.
 */
 
 #ifndef TEST_ZOOM_CONSTRAINTS_H
@@ -30,149 +30,171 @@
 {
     Q_OBJECT
 
+    void checkFpp(const ZoomConstraint &c,
+                  ZoomConstraint::RoundingDirection dir,
+                  int n,
+                  int expected) {
+        QCOMPARE(c.getNearestZoomLevel(ZoomLevel(ZoomLevel::FramesPerPixel, n),
+                                       dir),
+                 ZoomLevel(ZoomLevel::FramesPerPixel, expected));
+    }
+    
 private slots:
     void unconstrainedNearest() {
         ZoomConstraint c;
-        QCOMPARE(c.getNearestBlockSize(1), 1);
-        QCOMPARE(c.getNearestBlockSize(2), 2);
-        QCOMPARE(c.getNearestBlockSize(3), 3);
-        QCOMPARE(c.getNearestBlockSize(4), 4);
-        QCOMPARE(c.getNearestBlockSize(20), 20);
-        QCOMPARE(c.getNearestBlockSize(23), 23);
-        int max = c.getMaxZoomLevel();
-        QCOMPARE(c.getNearestBlockSize(max), max);
-        QCOMPARE(c.getNearestBlockSize(max+1), max);
+        checkFpp(c, ZoomConstraint::RoundNearest, 1, 1);
+        checkFpp(c, ZoomConstraint::RoundNearest, 2, 2);
+        checkFpp(c, ZoomConstraint::RoundNearest, 3, 3);
+        checkFpp(c, ZoomConstraint::RoundNearest, 4, 4);
+        checkFpp(c, ZoomConstraint::RoundNearest, 20, 20);
+        checkFpp(c, ZoomConstraint::RoundNearest, 32, 32);
+        auto max = c.getMaxZoomLevel();
+        QCOMPARE(c.getNearestZoomLevel(max), max);
+        QCOMPARE(c.getNearestZoomLevel(max.incremented()), max);
     }
     
     void unconstrainedUp() {
         ZoomConstraint c;
-        QCOMPARE(c.getNearestBlockSize(1, ZoomConstraint::RoundUp), 1);
-        QCOMPARE(c.getNearestBlockSize(2, ZoomConstraint::RoundUp), 2);
-        QCOMPARE(c.getNearestBlockSize(3, ZoomConstraint::RoundUp), 3);
-        QCOMPARE(c.getNearestBlockSize(4, ZoomConstraint::RoundUp), 4);
-        QCOMPARE(c.getNearestBlockSize(20, ZoomConstraint::RoundUp), 20);
-        QCOMPARE(c.getNearestBlockSize(32, ZoomConstraint::RoundUp), 32);
-        int max = c.getMaxZoomLevel();
-        QCOMPARE(c.getNearestBlockSize(max, ZoomConstraint::RoundUp), max);
-        QCOMPARE(c.getNearestBlockSize(max+1, ZoomConstraint::RoundUp), max);
+        checkFpp(c, ZoomConstraint::RoundUp, 1, 1);
+        checkFpp(c, ZoomConstraint::RoundUp, 2, 2);
+        checkFpp(c, ZoomConstraint::RoundUp, 3, 3);
+        checkFpp(c, ZoomConstraint::RoundUp, 4, 4);
+        checkFpp(c, ZoomConstraint::RoundUp, 20, 20);
+        checkFpp(c, ZoomConstraint::RoundUp, 32, 32);
+        auto max = c.getMaxZoomLevel();
+        QCOMPARE(c.getNearestZoomLevel(max,
+                                       ZoomConstraint::RoundUp), max);
+        QCOMPARE(c.getNearestZoomLevel(max.incremented(),
+                                       ZoomConstraint::RoundUp), max);
     }
     
     void unconstrainedDown() {
         ZoomConstraint c;
-        QCOMPARE(c.getNearestBlockSize(1, ZoomConstraint::RoundDown), 1);
-        QCOMPARE(c.getNearestBlockSize(2, ZoomConstraint::RoundDown), 2);
-        QCOMPARE(c.getNearestBlockSize(3, ZoomConstraint::RoundDown), 3);
-        QCOMPARE(c.getNearestBlockSize(4, ZoomConstraint::RoundDown), 4);
-        QCOMPARE(c.getNearestBlockSize(20, ZoomConstraint::RoundDown), 20);
-        QCOMPARE(c.getNearestBlockSize(32, ZoomConstraint::RoundDown), 32);
-        int max = c.getMaxZoomLevel();
-        QCOMPARE(c.getNearestBlockSize(max, ZoomConstraint::RoundDown), max);
-        QCOMPARE(c.getNearestBlockSize(max+1, ZoomConstraint::RoundDown), max);
+        checkFpp(c, ZoomConstraint::RoundDown, 1, 1);
+        checkFpp(c, ZoomConstraint::RoundDown, 2, 2);
+        checkFpp(c, ZoomConstraint::RoundDown, 3, 3);
+        checkFpp(c, ZoomConstraint::RoundDown, 4, 4);
+        checkFpp(c, ZoomConstraint::RoundDown, 20, 20);
+        checkFpp(c, ZoomConstraint::RoundDown, 32, 32);
+        auto max = c.getMaxZoomLevel();
+        QCOMPARE(c.getNearestZoomLevel(max,
+                                       ZoomConstraint::RoundDown), max);
+        QCOMPARE(c.getNearestZoomLevel(max.incremented(),
+                                       ZoomConstraint::RoundDown), max);
     }
 
     void powerOfTwoNearest() {
         PowerOfTwoZoomConstraint c;
-        QCOMPARE(c.getNearestBlockSize(1), 1);
-        QCOMPARE(c.getNearestBlockSize(2), 2);
-        QCOMPARE(c.getNearestBlockSize(3), 2);
-        QCOMPARE(c.getNearestBlockSize(4), 4);
-        QCOMPARE(c.getNearestBlockSize(20), 16);
-        QCOMPARE(c.getNearestBlockSize(23), 16);
-        QCOMPARE(c.getNearestBlockSize(24), 16);
-        QCOMPARE(c.getNearestBlockSize(25), 32);
-        int max = c.getMaxZoomLevel();
-        QCOMPARE(c.getNearestBlockSize(max), max);
-        QCOMPARE(c.getNearestBlockSize(max+1), max);
+        checkFpp(c, ZoomConstraint::RoundNearest, 1, 1);
+        checkFpp(c, ZoomConstraint::RoundNearest, 2, 2);
+        checkFpp(c, ZoomConstraint::RoundNearest, 3, 2);
+        checkFpp(c, ZoomConstraint::RoundNearest, 4, 4);
+        checkFpp(c, ZoomConstraint::RoundNearest, 20, 16);
+        checkFpp(c, ZoomConstraint::RoundNearest, 23, 16);
+        checkFpp(c, ZoomConstraint::RoundNearest, 24, 16);
+        checkFpp(c, ZoomConstraint::RoundNearest, 25, 32);
+        auto max = c.getMaxZoomLevel();
+        QCOMPARE(c.getNearestZoomLevel(max), max);
+        QCOMPARE(c.getNearestZoomLevel(max.incremented()), max);
     }
     
     void powerOfTwoUp() {
         PowerOfTwoZoomConstraint c;
-        QCOMPARE(c.getNearestBlockSize(1, ZoomConstraint::RoundUp), 1);
-        QCOMPARE(c.getNearestBlockSize(2, ZoomConstraint::RoundUp), 2);
-        QCOMPARE(c.getNearestBlockSize(3, ZoomConstraint::RoundUp), 4);
-        QCOMPARE(c.getNearestBlockSize(4, ZoomConstraint::RoundUp), 4);
-        QCOMPARE(c.getNearestBlockSize(20, ZoomConstraint::RoundUp), 32);
-        QCOMPARE(c.getNearestBlockSize(32, ZoomConstraint::RoundUp), 32);
-        QCOMPARE(c.getNearestBlockSize(33, ZoomConstraint::RoundUp), 64);
-        int max = c.getMaxZoomLevel();
-        QCOMPARE(c.getNearestBlockSize(max, ZoomConstraint::RoundUp), max);
-        QCOMPARE(c.getNearestBlockSize(max+1, ZoomConstraint::RoundUp), max);
+        checkFpp(c, ZoomConstraint::RoundUp, 1, 1);
+        checkFpp(c, ZoomConstraint::RoundUp, 2, 2);
+        checkFpp(c, ZoomConstraint::RoundUp, 3, 4);
+        checkFpp(c, ZoomConstraint::RoundUp, 4, 4);
+        checkFpp(c, ZoomConstraint::RoundUp, 20, 32);
+        checkFpp(c, ZoomConstraint::RoundUp, 32, 32);
+        checkFpp(c, ZoomConstraint::RoundUp, 33, 64);
+        auto max = c.getMaxZoomLevel();
+        QCOMPARE(c.getNearestZoomLevel(max,
+                                       ZoomConstraint::RoundUp), max);
+        QCOMPARE(c.getNearestZoomLevel(max.incremented(),
+                                       ZoomConstraint::RoundUp), max);
     }
     
     void powerOfTwoDown() {
         PowerOfTwoZoomConstraint c;
-        QCOMPARE(c.getNearestBlockSize(1, ZoomConstraint::RoundDown), 1);
-        QCOMPARE(c.getNearestBlockSize(2, ZoomConstraint::RoundDown), 2);
-        QCOMPARE(c.getNearestBlockSize(3, ZoomConstraint::RoundDown), 2);
-        QCOMPARE(c.getNearestBlockSize(4, ZoomConstraint::RoundDown), 4);
-        QCOMPARE(c.getNearestBlockSize(20, ZoomConstraint::RoundDown), 16);
-        QCOMPARE(c.getNearestBlockSize(32, ZoomConstraint::RoundDown), 32);
-        QCOMPARE(c.getNearestBlockSize(33, ZoomConstraint::RoundDown), 32);
-        int max = c.getMaxZoomLevel();
-        QCOMPARE(c.getNearestBlockSize(max, ZoomConstraint::RoundDown), max);
-        QCOMPARE(c.getNearestBlockSize(max+1, ZoomConstraint::RoundDown), max);
+        checkFpp(c, ZoomConstraint::RoundDown, 1, 1);
+        checkFpp(c, ZoomConstraint::RoundDown, 2, 2);
+        checkFpp(c, ZoomConstraint::RoundDown, 3, 2);
+        checkFpp(c, ZoomConstraint::RoundDown, 4, 4);
+        checkFpp(c, ZoomConstraint::RoundDown, 20, 16);
+        checkFpp(c, ZoomConstraint::RoundDown, 32, 32);
+        checkFpp(c, ZoomConstraint::RoundDown, 33, 32);
+        auto max = c.getMaxZoomLevel();
+        QCOMPARE(c.getNearestZoomLevel(max,
+                                       ZoomConstraint::RoundDown), max);
+        QCOMPARE(c.getNearestZoomLevel(max.incremented(),
+                                       ZoomConstraint::RoundDown), max);
     }
 
     void powerOfSqrtTwoNearest() {
         PowerOfSqrtTwoZoomConstraint c;
-        QCOMPARE(c.getNearestBlockSize(1), 1);
-        QCOMPARE(c.getNearestBlockSize(2), 2);
-        QCOMPARE(c.getNearestBlockSize(3), 2);
-        QCOMPARE(c.getNearestBlockSize(4), 4);
-        QCOMPARE(c.getNearestBlockSize(18), 16);
-        QCOMPARE(c.getNearestBlockSize(19), 16);
-        QCOMPARE(c.getNearestBlockSize(20), 22);
-        QCOMPARE(c.getNearestBlockSize(23), 22);
-        QCOMPARE(c.getNearestBlockSize(28), 32);
+        checkFpp(c, ZoomConstraint::RoundNearest, 1, 1);
+        checkFpp(c, ZoomConstraint::RoundNearest, 2, 2);
+        checkFpp(c, ZoomConstraint::RoundNearest, 3, 2);
+        checkFpp(c, ZoomConstraint::RoundNearest, 4, 4);
+        checkFpp(c, ZoomConstraint::RoundNearest, 18, 16);
+        checkFpp(c, ZoomConstraint::RoundNearest, 19, 16);
+        checkFpp(c, ZoomConstraint::RoundNearest, 20, 22);
+        checkFpp(c, ZoomConstraint::RoundNearest, 23, 22);
+        checkFpp(c, ZoomConstraint::RoundNearest, 28, 32);
         // PowerOfSqrtTwoZoomConstraint makes an effort to ensure
         // bigger numbers get rounded to a multiple of something
         // simple (64 or 90 depending on whether they are power-of-two
         // or power-of-sqrt-two types)
-        QCOMPARE(c.getNearestBlockSize(800), 720);
-        QCOMPARE(c.getNearestBlockSize(1023), 1024);
-        QCOMPARE(c.getNearestBlockSize(1024), 1024);
-        QCOMPARE(c.getNearestBlockSize(1025), 1024);
-        int max = c.getMaxZoomLevel();
-        QCOMPARE(c.getNearestBlockSize(max), max);
-        QCOMPARE(c.getNearestBlockSize(max+1), max);
+        checkFpp(c, ZoomConstraint::RoundNearest, 800, 720);
+        checkFpp(c, ZoomConstraint::RoundNearest, 1023, 1024);
+        checkFpp(c, ZoomConstraint::RoundNearest, 1024, 1024);
+        checkFpp(c, ZoomConstraint::RoundNearest, 1024, 1024);
+        checkFpp(c, ZoomConstraint::RoundNearest, 1025, 1024);
+        auto max = c.getMaxZoomLevel();
+        QCOMPARE(c.getNearestZoomLevel(max), max);
+        QCOMPARE(c.getNearestZoomLevel(max.incremented()), max);
     }
     
     void powerOfSqrtTwoUp() {
         PowerOfSqrtTwoZoomConstraint c;
-        QCOMPARE(c.getNearestBlockSize(1, ZoomConstraint::RoundUp), 1);
-        QCOMPARE(c.getNearestBlockSize(2, ZoomConstraint::RoundUp), 2);
-        QCOMPARE(c.getNearestBlockSize(3, ZoomConstraint::RoundUp), 4);
-        QCOMPARE(c.getNearestBlockSize(4, ZoomConstraint::RoundUp), 4);
-        QCOMPARE(c.getNearestBlockSize(18, ZoomConstraint::RoundUp), 22);
-        QCOMPARE(c.getNearestBlockSize(22, ZoomConstraint::RoundUp), 22);
-        QCOMPARE(c.getNearestBlockSize(23, ZoomConstraint::RoundUp), 32);
-        QCOMPARE(c.getNearestBlockSize(800, ZoomConstraint::RoundUp), 1024);
-        QCOMPARE(c.getNearestBlockSize(1023, ZoomConstraint::RoundUp), 1024);
-        QCOMPARE(c.getNearestBlockSize(1024, ZoomConstraint::RoundUp), 1024);
+        checkFpp(c, ZoomConstraint::RoundUp, 1, 1);
+        checkFpp(c, ZoomConstraint::RoundUp, 2, 2);
+        checkFpp(c, ZoomConstraint::RoundUp, 3, 4);
+        checkFpp(c, ZoomConstraint::RoundUp, 4, 4);
+        checkFpp(c, ZoomConstraint::RoundUp, 18, 22);
+        checkFpp(c, ZoomConstraint::RoundUp, 22, 22);
+        checkFpp(c, ZoomConstraint::RoundUp, 23, 32);
+        checkFpp(c, ZoomConstraint::RoundUp, 800, 1024);
+        checkFpp(c, ZoomConstraint::RoundUp, 1023, 1024);
+        checkFpp(c, ZoomConstraint::RoundUp, 1024, 1024);
         // see comment above
-        QCOMPARE(c.getNearestBlockSize(1025, ZoomConstraint::RoundUp), 1440);
-        int max = c.getMaxZoomLevel();
-        QCOMPARE(c.getNearestBlockSize(max, ZoomConstraint::RoundUp), max);
-        QCOMPARE(c.getNearestBlockSize(max+1, ZoomConstraint::RoundUp), max);
+        checkFpp(c, ZoomConstraint::RoundUp, 1025, 1440);
+        auto max = c.getMaxZoomLevel();
+        QCOMPARE(c.getNearestZoomLevel(max,
+                                       ZoomConstraint::RoundUp), max);
+        QCOMPARE(c.getNearestZoomLevel(max.incremented(),
+                                       ZoomConstraint::RoundUp), max);
     }
     
     void powerOfSqrtTwoDown() {
         PowerOfSqrtTwoZoomConstraint c;
-        QCOMPARE(c.getNearestBlockSize(1, ZoomConstraint::RoundDown), 1);
-        QCOMPARE(c.getNearestBlockSize(2, ZoomConstraint::RoundDown), 2);
-        QCOMPARE(c.getNearestBlockSize(3, ZoomConstraint::RoundDown), 2);
-        QCOMPARE(c.getNearestBlockSize(4, ZoomConstraint::RoundDown), 4);
-        QCOMPARE(c.getNearestBlockSize(18, ZoomConstraint::RoundDown), 16);
-        QCOMPARE(c.getNearestBlockSize(22, ZoomConstraint::RoundDown), 22);
-        QCOMPARE(c.getNearestBlockSize(23, ZoomConstraint::RoundDown), 22);
+        checkFpp(c, ZoomConstraint::RoundDown, 1, 1);
+        checkFpp(c, ZoomConstraint::RoundDown, 2, 2);
+        checkFpp(c, ZoomConstraint::RoundDown, 3, 2);
+        checkFpp(c, ZoomConstraint::RoundDown, 4, 4);
+        checkFpp(c, ZoomConstraint::RoundDown, 18, 16);
+        checkFpp(c, ZoomConstraint::RoundDown, 22, 22);
+        checkFpp(c, ZoomConstraint::RoundDown, 23, 22);
         // see comment above
-        QCOMPARE(c.getNearestBlockSize(800, ZoomConstraint::RoundDown), 720);
-        QCOMPARE(c.getNearestBlockSize(1023, ZoomConstraint::RoundDown), 720);
-        QCOMPARE(c.getNearestBlockSize(1024, ZoomConstraint::RoundDown), 1024);
-        QCOMPARE(c.getNearestBlockSize(1025, ZoomConstraint::RoundDown), 1024);
-        int max = c.getMaxZoomLevel();
-        QCOMPARE(c.getNearestBlockSize(max, ZoomConstraint::RoundDown), max);
-        QCOMPARE(c.getNearestBlockSize(max+1, ZoomConstraint::RoundDown), max);
+        checkFpp(c, ZoomConstraint::RoundDown, 800, 720);
+        checkFpp(c, ZoomConstraint::RoundDown, 1023, 720);
+        checkFpp(c, ZoomConstraint::RoundDown, 1024, 1024);
+        checkFpp(c, ZoomConstraint::RoundDown, 1025, 1024);
+        auto max = c.getMaxZoomLevel();
+        QCOMPARE(c.getNearestZoomLevel(max,
+                                       ZoomConstraint::RoundDown), max);
+        QCOMPARE(c.getNearestZoomLevel(max.incremented(),
+                                       ZoomConstraint::RoundDown), max);
     }
 };
 
--- a/data/model/test/files.pri	Wed Oct 03 15:45:57 2018 +0100
+++ b/data/model/test/files.pri	Fri Oct 05 10:25:25 2018 +0100
@@ -2,6 +2,7 @@
 	Compares.h \
 	MockWaveModel.h \
 	TestFFTModel.h \
+        TestWaveformOversampler.h \
         TestZoomConstraints.h
 	
 TEST_SOURCES += \
--- a/data/model/test/svcore-data-model-test.cpp	Wed Oct 03 15:45:57 2018 +0100
+++ b/data/model/test/svcore-data-model-test.cpp	Fri Oct 05 10:25:25 2018 +0100
@@ -13,6 +13,7 @@
 
 #include "TestFFTModel.h"
 #include "TestZoomConstraints.h"
+#include "TestWaveformOversampler.h"
 
 #include <QtTest>
 
@@ -40,6 +41,12 @@
         else ++bad;
     }
 
+    {
+        TestWaveformOversampler t;
+        if (QTest::qExec(&t, argc, argv) == 0) ++good;
+        else ++bad;
+    }
+
     if (bad > 0) {
         SVCERR << "\n********* " << bad << " test suite(s) failed!\n" << endl;
         return 1;
--- a/files.pri	Wed Oct 03 15:45:57 2018 +0100
+++ b/files.pri	Fri Oct 05 10:25:25 2018 +0100
@@ -43,6 +43,7 @@
            base/Window.h \
            base/XmlExportable.h \
            base/ZoomConstraint.h \
+           base/ZoomLevel.h \
            data/fileio/AudioFileReader.h \
            data/fileio/AudioFileReaderFactory.h \
            data/fileio/AudioFileSizeEstimator.h \
@@ -95,6 +96,7 @@
            data/model/SparseValueModel.h \
            data/model/TabularModel.h \
            data/model/TextModel.h \
+           data/model/WaveformOversampler.h \
            data/model/WaveFileModel.h \
            data/model/ReadOnlyWaveFileModel.h \
            data/model/WritableWaveFileModel.h \
@@ -175,6 +177,7 @@
            base/UnitDatabase.cpp \
            base/ViewManagerBase.cpp \
            base/XmlExportable.cpp \
+           base/ZoomLevel.cpp \
            data/fileio/AudioFileReader.cpp \
            data/fileio/AudioFileReaderFactory.cpp \
            data/fileio/AudioFileSizeEstimator.cpp \
@@ -209,6 +212,7 @@
            data/model/PowerOfSqrtTwoZoomConstraint.cpp \
            data/model/PowerOfTwoZoomConstraint.cpp \
            data/model/RangeSummarisableTimeValueModel.cpp \
+           data/model/WaveformOversampler.cpp \
            data/model/WaveFileModel.cpp \
            data/model/ReadOnlyWaveFileModel.cpp \
            data/model/WritableWaveFileModel.cpp \