changeset 1365:3382d914e110

Merge from branch 3.0-integration
author Chris Cannam
date Fri, 13 Jan 2017 10:29:44 +0000
parents 6a7ea3bd0e10 (current diff) b812df0351d9 (diff)
children bbc4e4ee15d5
files base/RealTime.cpp base/Resampler.cpp base/Resampler.h base/ResizeableBitset.h base/test/TestRealTime.h base/test/main.cpp base/test/test.pro data/fft/FFTCacheReader.h data/fft/FFTCacheStorageType.h data/fft/FFTCacheWriter.h data/fft/FFTDataServer.cpp data/fft/FFTDataServer.h data/fft/FFTFileCacheReader.cpp data/fft/FFTFileCacheReader.h data/fft/FFTFileCacheWriter.cpp data/fft/FFTFileCacheWriter.h data/fft/FFTMemoryCache.cpp data/fft/FFTMemoryCache.h data/fft/FFTapi.cpp data/fft/FFTapi.h data/fileio/MatrixFile.cpp data/fileio/MatrixFile.h data/fileio/QuickTimeFileReader.cpp data/fileio/QuickTimeFileReader.h data/fileio/test/main.cpp data/fileio/test/test.pro data/fileio/test/testfiles/12000-6-16.aiff data/fileio/test/testfiles/32000-1-16.wav data/fileio/test/testfiles/32000-1.m4a data/fileio/test/testfiles/32000-1.mp3 data/fileio/test/testfiles/32000-1.ogg data/fileio/test/testfiles/44100-1-32.wav data/fileio/test/testfiles/44100-2-16.wav data/fileio/test/testfiles/44100-2-8.wav data/fileio/test/testfiles/44100-2.flac data/fileio/test/testfiles/44100-2.m4a data/fileio/test/testfiles/44100-2.mp3 data/fileio/test/testfiles/44100-2.ogg data/fileio/test/testfiles/48000-1-16.wav data/fileio/test/testfiles/48000-1-24.aiff data/fileio/test/testfiles/8000-1-8.wav data/fileio/test/testfiles/8000-2-16.wav data/fileio/test/testfiles/8000-6-16.wav data/model/test/main.cpp data/model/test/test.pro
diffstat 209 files changed, 8881 insertions(+), 9617 deletions(-) [+]
line wrap: on
line diff
--- a/base/AudioPlaySource.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/AudioPlaySource.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _AUDIO_PLAY_SOURCE_H_
-#define _AUDIO_PLAY_SOURCE_H_
+#ifndef SV_AUDIO_PLAY_SOURCE_H
+#define SV_AUDIO_PLAY_SOURCE_H
 
 #include "BaseTypes.h"
 
@@ -59,7 +59,9 @@
 
     /**
      * Return the current (or thereabouts) output levels in the range
-     * 0.0 -> 1.0, for metering purposes.
+     * 0.0 -> 1.0, for metering purposes.  The values returned are
+     * peak values since the last call to this function was made
+     * (i.e. calling this function also resets them).
      */
     virtual bool getOutputLevels(float &left, float &right) = 0;
 
@@ -70,11 +72,16 @@
     virtual sv_samplerate_t getSourceSampleRate() const = 0;
 
     /**
-     * Return the sample rate set by the target audio device (or the
-     * source sample rate if the target hasn't set one).  If the
-     * source and target sample rates differ, resampling will occur.
+     * Return the sample rate set by the target audio device (or 0 if
+     * the target hasn't told us yet).  If the source and target
+     * sample rates differ, resampling will occur.
+     *
+     * Note that we don't actually do any processing at the device
+     * sample rate. All processing happens at the source sample rate,
+     * and then a resampler is applied if necessary at the interface
+     * between application and driver layer.
      */
-    virtual sv_samplerate_t getTargetSampleRate() const = 0;
+    virtual sv_samplerate_t getDeviceSampleRate() const = 0;
 
     /**
      * Get the block size of the target audio device.  This may be an
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/AudioRecordTarget.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,50 @@
+/* -*- 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_AUDIO_RECORD_TARGET_H
+#define SV_AUDIO_RECORD_TARGET_H
+
+#include "BaseTypes.h"
+
+/**
+ * The record target API used by the view manager. See also AudioPlaySource.
+ */
+class AudioRecordTarget
+{
+public:
+    virtual ~AudioRecordTarget() { }
+
+    /**
+     * Return whether recording is currently happening.
+     */
+    virtual bool isRecording() const = 0;
+
+    /**
+     * Return the approximate duration of the audio recording so far.
+     */
+    virtual sv_frame_t getRecordDuration() const = 0;
+
+    /**
+     * Return the current (or thereabouts) input levels in the range
+     * 0.0 -> 1.0, for metering purposes. Only valid while recording.
+     * The values returned are peak values since the last call to this
+     * function was made (i.e. calling this function also resets them).
+     */
+    virtual bool getInputLevels(float &left, float &right) = 0;
+};
+
+#endif
+
+
+
--- a/base/BaseTypes.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/BaseTypes.h	Fri Jan 13 10:29:44 2017 +0000
@@ -16,6 +16,10 @@
 #define BASE_TYPES_H
 
 #include <cstdint>
+#include <complex>
+#include <vector>
+
+#include <bqvec/Allocators.h>
 
 /** Frame index, the unit of our time axis. This is signed because the
     axis conceptually extends below zero: zero represents the start of
@@ -46,5 +50,10 @@
 */
 typedef double sv_samplerate_t;
 
+typedef std::vector<float, breakfastquay::StlAllocator<float>> floatvec_t;
+
+typedef std::vector<std::complex<float>,
+                    breakfastquay::StlAllocator<std::complex<float>>> complexvec_t;
+
 #endif
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/ColumnOp.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,185 @@
+/* -*- 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 file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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 "ColumnOp.h"
+
+#include <cmath>
+#include <algorithm>
+#include <iostream>
+
+#include "base/Debug.h"
+
+using namespace std;
+
+ColumnOp::Column
+ColumnOp::fftScale(const Column &in, int fftSize)
+{
+    return applyGain(in, 2.0 / fftSize);
+}
+
+ColumnOp::Column
+ColumnOp::peakPick(const Column &in)
+{
+    vector<float> out(in.size(), 0.f);
+
+    for (int i = 0; in_range_for(in, i); ++i) {
+        if (isPeak(in, i)) {
+            out[i] = in[i];
+        }
+    }
+    
+    return out;
+}
+
+ColumnOp::Column
+ColumnOp::normalize(const Column &in, ColumnNormalization n) {
+
+    if (n == ColumnNormalization::None || in.empty()) {
+        return in;
+    }
+
+    float scale = 1.f;
+        
+    if (n == ColumnNormalization::Sum1) {
+
+        float sum = 0.f;
+
+        for (auto v: in) {
+            sum += fabsf(v);
+        }
+
+        if (sum != 0.f) {
+            scale = 1.f / sum;
+        }
+
+    } else {
+
+        float max = 0.f;
+
+        for (auto v: in) {
+            v = fabsf(v);
+            if (v > max) {
+                max = v;
+            }
+        }
+
+        if (n == ColumnNormalization::Max1) {
+            if (max != 0.f) {
+                scale = 1.f / max;
+            }
+        } else if (n == ColumnNormalization::Hybrid) {
+            if (max > 0.f) {
+                scale = log10f(max + 1.f) / max;
+            }
+        }
+    }
+
+    return applyGain(in, scale);
+}
+
+ColumnOp::Column
+ColumnOp::distribute(const Column &in,
+                     int h,
+                     const vector<double> &binfory,
+                     int minbin,
+                     bool interpolate)
+{
+    vector<float> out(h, 0.f);
+    int bins = int(in.size());
+
+    if (interpolate) {
+        // If the bins are all closer together than the target y
+        // coordinate increments, then we don't want to interpolate
+        // after all. But because the binfory mapping isn't
+        // necessarily linear, just checking e.g. whether bins > h is
+        // not enough -- the bins could still be spaced more widely at
+        // either end of the scale. We are prepared to assume however
+        // that if the bins are closer at both ends of the scale, they
+        // aren't going to diverge mysteriously in the middle.
+        if (h > 1 &&
+            fabs(binfory[1] - binfory[0]) >= 1.0 &&
+            fabs(binfory[h-1] - binfory[h-2]) >= 1.0) {
+            interpolate = false;
+        }
+    }
+    
+    for (int y = 0; y < h; ++y) {
+
+        if (interpolate) {
+
+            double sy = binfory[y] - minbin - 0.5;
+            double syf = floor(sy);
+
+            int mainbin = int(syf);
+            int other = mainbin;
+            if (sy > syf) {
+                other = mainbin + 1;
+            } else if (sy < syf) {
+                other = mainbin - 1;
+            }
+
+            if (mainbin < 0) {
+                mainbin = 0;
+            }
+            if (mainbin >= bins) {
+                mainbin = bins - 1;
+            }
+
+            if (other < 0) {
+                other = 0;
+            }
+            if (other >= bins) {
+                other = bins - 1;
+            }
+
+            double prop = 1.0 - fabs(sy - syf);
+            
+            double v0 = in[mainbin];
+            double v1 = in[other];
+                
+            out[y] = float(prop * v0 + (1.0 - prop) * v1);
+
+        } else {
+            
+            double sy0 = binfory[y] - minbin;
+
+            double sy1;
+            if (y+1 < h) {
+                sy1 = binfory[y+1] - minbin;
+            } else {
+                sy1 = bins;
+            }
+
+            int by0 = int(sy0 + 0.0001);
+            int by1 = int(sy1 + 0.0001);
+
+            if (by0 < 0 || by0 >= bins || by1 > bins) {
+                SVCERR << "ERROR: bin index out of range in ColumnOp::distribute: by0 = " << by0 << ", by1 = " << by1 << ", sy0 = " << sy0 << ", sy1 = " << sy1 << ", y = " << y << ", binfory[y] = " << binfory[y] << ", minbin = " << minbin << ", bins = " << bins << endl;
+                continue;
+            }
+                
+            for (int bin = by0; bin == by0 || bin < by1; ++bin) {
+
+                float value = in[bin];
+
+                if (bin == by0 || value > out[y]) {
+                    out[y] = value;
+                }
+            }
+        }
+    }
+
+    return out;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/ColumnOp.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,126 @@
+/* -*- 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 file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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 COLUMN_OP_H
+#define COLUMN_OP_H
+
+#include "BaseTypes.h"
+
+#include <vector>
+
+/**
+ * Display normalization types for columns in e.g. grid plots.
+ *
+ * Max1 means to normalize to max value = 1.0.
+ * Sum1 means to normalize to sum of values = 1.0.
+ *
+ * Hybrid means normalize to max = 1.0 and then multiply by
+ * log10 of the max value, to retain some difference between
+ * levels of neighbouring columns.
+ *
+ * Area normalization is handled separately.
+ */
+enum class ColumnNormalization {
+    None,
+    Max1,
+    Sum1,
+    Hybrid
+};
+
+/**
+ * Class containing static functions for simple operations on data
+ * columns, for use by display layers.
+ */
+class ColumnOp
+{
+public:
+    /** 
+     * Column type. 
+     */
+    typedef std::vector<float> Column;
+
+    /**
+     * Scale the given column using the given gain multiplier.
+     */
+    static Column applyGain(const Column &in, double gain) {
+        if (gain == 1.0) return in;
+	Column out;
+	out.reserve(in.size());
+	for (auto v: in) out.push_back(float(v * gain));
+	return out;
+    }
+
+    /**
+     * Scale an FFT output downward by half the FFT size.
+     */
+    static Column fftScale(const Column &in, int fftSize);
+
+    /**
+     * Determine whether an index points to a local peak.
+     */
+    static bool isPeak(const Column &in, int ix) {
+        if (!in_range_for(in, ix)) {
+            return false;
+        }
+        if (ix == 0) {
+            return in[0] >= in[1];
+        }
+        if (!in_range_for(in, ix+1)) {
+            return in[ix] > in[ix-1];
+        }
+	if (in[ix] < in[ix+1]) {
+            return false;
+        }
+	if (in[ix] <= in[ix-1]) {
+            return false;
+        }
+	return true;
+    }
+
+    /**
+     * Return a column containing only the local peak values (all
+     * others zero).
+     */
+    static Column peakPick(const Column &in);
+
+    /**
+     * Return a column normalized from the input column according to
+     * the given normalization scheme.
+     *
+     * Note that the sum or max (as appropriate) used for
+     * normalisation will be calculated from the absolute values of
+     * the column elements, should any of them be negative.
+     */
+    static Column normalize(const Column &in, ColumnNormalization n);
+    
+    /**
+     * Distribute the given column into a target vector of a different
+     * size, optionally using linear interpolation. The binfory vector
+     * contains a mapping from y coordinate (i.e. index into the
+     * target vector) to bin (i.e. index into the source column). The
+     * source column ("in") may be a partial column; it's assumed to
+     * contain enough bins to span the destination range, starting
+     * with the bin of index minbin.
+     */
+    static Column distribute(const Column &in,
+			     int h,
+			     const std::vector<double> &binfory,
+			     int minbin,
+			     bool interpolate);
+
+};
+
+#endif
+
--- a/base/Debug.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/Debug.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -21,25 +21,48 @@
 #include <QUrl>
 #include <QCoreApplication>
 
-#ifndef NDEBUG
+#include <stdexcept>
 
-static SVDebug *debug = 0;
+static SVDebug *svdebug = 0;
+static SVCerr *svcerr = 0;
 static QMutex mutex;
 
 SVDebug &getSVDebug() {
     mutex.lock();
-    if (!debug) {
-        debug = new SVDebug();
+    if (!svdebug) {
+        svdebug = new SVDebug();
     }
     mutex.unlock();
-    return *debug;
+    return *svdebug;
 }
 
+SVCerr &getSVCerr() {
+    mutex.lock();
+    if (!svcerr) {
+        if (!svdebug) {
+            svdebug = new SVDebug();
+        }
+        svcerr = new SVCerr(*svdebug);
+    }
+    mutex.unlock();
+    return *svcerr;
+}
+
+bool SVDebug::m_silenced = false;
+bool SVCerr::m_silenced = false;
+
 SVDebug::SVDebug() :
     m_prefix(0),
     m_ok(false),
-    m_eol(false)
+    m_eol(true)
 {
+    if (m_silenced) return;
+    
+    if (qApp->applicationName() == "") {
+        cerr << "ERROR: Can't use SVDEBUG before setting application name" << endl;
+        throw std::logic_error("Can't use SVDEBUG before setting application name");
+    }
+    
     QString pfx = ResourceFinder().getUserResourcePrefix();
     QDir logdir(QString("%1/%2").arg(pfx).arg("log"));
 
@@ -59,14 +82,14 @@
                              << "Failed to open debug log file "
                              << fileName << " for writing";
     } else {
-        cerr << m_prefix << ": Log file is " << fileName << endl;
+//        cerr << m_prefix << ": Log file is " << fileName << endl;
         m_ok = true;
     }
 }
 
 SVDebug::~SVDebug()
 {
-    m_stream.close();
+    if (m_stream) m_stream.close();
 }
 
 QDebug &
@@ -76,8 +99,6 @@
     return dbg;
 }
 
-#endif
-
 std::ostream &
 operator<<(std::ostream &target, const QString &str)
 {
--- a/base/Debug.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/Debug.h	Fri Jan 13 10:29:44 2017 +0000
@@ -36,8 +36,6 @@
 using std::cerr;
 using std::endl;
 
-#ifndef NDEBUG
-
 class SVDebug {
 public:
     SVDebug();
@@ -45,6 +43,7 @@
 
     template <typename T>
     inline SVDebug &operator<<(const T &t) {
+        if (m_silenced) return *this;
         if (m_ok) {
             if (m_eol) {
                 m_stream << m_prefix << " ";
@@ -56,39 +55,56 @@
     }
 
     inline SVDebug &operator<<(QTextStreamFunction) {
+        if (m_silenced) return *this;
         m_stream << std::endl;
         m_eol = true;
         return *this;
     }
 
+    static void silence() { m_silenced = true; }
+    
 private:
     std::fstream m_stream;
     char *m_prefix;
     bool m_ok;
     bool m_eol;
+    static bool m_silenced;
+};
+
+class SVCerr {
+public:
+    SVCerr(SVDebug &d) : m_d(d) { }
+    
+    template <typename T>
+    inline SVCerr &operator<<(const T &t) {
+        if (m_silenced) return *this;
+        m_d << t;
+        cerr << t;
+        return *this;
+    }
+
+    inline SVCerr &operator<<(QTextStreamFunction f) {
+        if (m_silenced) return *this;
+        m_d << f;
+        cerr << std::endl;
+        return *this;
+    }
+
+    static void silence() { m_silenced = true; }
+    
+private:
+    SVDebug &m_d;
+    static bool m_silenced;
 };
 
 extern SVDebug &getSVDebug();
+extern SVCerr &getSVCerr();
 
+// Writes to debug log only
 #define SVDEBUG getSVDebug()
 
-#else
-
-class NoDebug
-{
-public:
-    inline NoDebug() {}
-    inline ~NoDebug(){}
-
-    template <typename T>
-    inline NoDebug &operator<<(const T &) { return *this; }
-
-    inline NoDebug &operator<<(QTextStreamFunction) { return *this; }
-};
-
-#define SVDEBUG NoDebug()
-
-#endif /* !NDEBUG */
+// Writes to both SVDEBUG and cerr
+#define SVCERR getSVCerr()
 
 #endif /* !_DEBUG_H_ */
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/HelperExecPath.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,105 @@
+/* -*- 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 file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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 "HelperExecPath.h"
+
+#include <QCoreApplication>
+#include <QFile>
+#include <QDir>
+#include <QFileInfo>
+
+QStringList
+HelperExecPath::getTags()
+{
+    if (sizeof(void *) == 8) {
+        if (m_type == NativeArchitectureOnly) {
+            return { "64", "" };
+        } else {
+            return { "64", "", "32" };
+        }
+    } else {
+        return { "", "32" };
+    }
+}
+
+static bool
+isGood(QString path)
+{
+    return QFile(path).exists() && QFileInfo(path).isExecutable();
+}
+
+QList<HelperExecPath::HelperExec>
+HelperExecPath::getHelperExecutables(QString basename)
+{
+    QStringList dummy;
+    return search(basename, dummy);
+}
+
+QString
+HelperExecPath::getHelperExecutable(QString basename)
+{
+    auto execs = getHelperExecutables(basename);
+    if (execs.empty()) return "";
+    else return execs[0].executable;
+}
+
+QStringList
+HelperExecPath::getHelperDirPaths()
+{
+    QStringList dirs;
+    QString myDir = QCoreApplication::applicationDirPath();
+    dirs.push_back(myDir + "/helpers");
+    dirs.push_back(myDir);
+    return dirs;
+}
+
+QStringList
+HelperExecPath::getHelperCandidatePaths(QString basename)
+{
+    QStringList candidates;
+    (void)search(basename, candidates);
+    return candidates;
+}
+
+QList<HelperExecPath::HelperExec>
+HelperExecPath::search(QString basename, QStringList &candidates)
+{
+    // Helpers are expected to exist either in the same directory as
+    // this executable was found, or in a subdirectory called helpers.
+
+    QString extension = "";
+#ifdef _WIN32
+    extension = ".exe";
+#endif
+
+    QList<HelperExec> executables;
+    QStringList dirs = getHelperDirPaths();
+    
+    for (QString t: getTags()) {
+        for (QString d: dirs) {
+            QString path = d + QDir::separator() + basename;
+            if (t != QString()) path += "-" + t;
+            path += extension;
+            candidates.push_back(path);
+            if (isGood(path)) {
+                executables.push_back({ path, t });
+                break;
+            }
+        }
+    }
+
+    return executables;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/HelperExecPath.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,84 @@
+/* -*- 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 file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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_HELPER_EXEC_PATH_H
+#define SV_HELPER_EXEC_PATH_H
+
+#include <QStringList>
+
+/**
+ * Class to find helper executables that have been installed alongside
+ * the application. There may be more than one executable available
+ * with a given base name, because it's possible to have more than one
+ * implementation of a given service. For example, a plugin helper or
+ * scanner may exist in both 32-bit and 64-bit variants.
+ *
+ * This class encodes both the expected locations of helper
+ * executables, and the expected priority between different
+ * implementations (e.g. preferring the architecture that matches that
+ * of the host).
+ */
+class HelperExecPath
+{
+public:
+    enum SearchType {
+        NativeArchitectureOnly,
+        AllInstalled
+    };
+    
+    HelperExecPath(SearchType type) : m_type(type) { }
+    
+    /**
+     * Find a helper executable with the given base name in the bundle
+     * directory or installation location, if one exists, and return
+     * its full path. Equivalent to calling getHelperExecutables() and
+     * taking the first result from the returned list (or "" if empty).
+     */
+    QString getHelperExecutable(QString basename);
+
+    struct HelperExec {
+        QString executable;
+        QString tag;
+    };
+    
+    /**
+     * Find all helper executables with the given base name in the
+     * bundle directory or installation location, and return their
+     * full paths in order of priority. The "tag" string contains an
+     * identifier for the location or architecture of the helper, for
+     * example "32", "64", "js" etc. An empty tag signifies a default
+     * helper that matches the application's architecture.
+     */
+    QList<HelperExec> getHelperExecutables(QString basename);
+
+    /**
+     * Return the list of directories searched for helper
+     * executables.
+     */
+    QStringList getHelperDirPaths();
+    
+    /**
+     * Return the list of executable paths examined in the search for
+     * the helper executable with the given basename.
+     */
+    QStringList getHelperCandidatePaths(QString basename);
+
+private:
+    SearchType m_type;
+    QList<HelperExec> search(QString, QStringList &);
+    QStringList getTags();
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/HitCount.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,70 @@
+/* -*- 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 HIT_COUNT_H
+#define HIT_COUNT_H
+
+#include <string>
+#include <iostream>
+
+/**
+ * Profile class for counting cache hits and the like.
+ */
+class HitCount
+{
+public:
+    HitCount(std::string name) :
+	m_name(name),
+	m_hit(0),
+	m_partial(0),
+	m_miss(0)
+    { }
+    
+    ~HitCount() {
+#ifndef NO_HIT_COUNTS
+	using namespace std;
+	int total = m_hit + m_partial + m_miss;
+	cerr << "Hit count: " << m_name << ": ";
+	if (m_partial > 0) {
+	    cerr << m_hit << " hits, " << m_partial << " partial, "
+		 << m_miss << " misses";
+	} else {
+	    cerr << m_hit << " hits, " << m_miss << " misses";
+	}
+	if (total > 0) {
+	    if (m_partial > 0) {
+		cerr << " (" << ((m_hit * 100.0) / total) << "%, "
+		     << ((m_partial * 100.0) / total) << "%, "
+		     << ((m_miss * 100.0) / total) << "%)";
+	    } else {
+		cerr << " (" << ((m_hit * 100.0) / total) << "%, "
+		     << ((m_miss * 100.0) / total) << "%)";
+	    }
+	}
+	cerr << endl;
+#endif
+    }
+
+    void hit() { ++m_hit; }
+    void partial() { ++m_partial; }
+    void miss() { ++m_miss; }
+
+private:
+    std::string m_name;
+    int m_hit;
+    int m_partial;
+    int m_miss;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/MagnitudeRange.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,83 @@
+/* -*- 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 file copyright 2006 Chris Cannam and QMUL.
+    
+    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 MAGNITUDE_RANGE_H
+#define MAGNITUDE_RANGE_H
+
+#include <vector>
+
+/**
+ * Maintain a min and max value, and update them when supplied a new
+ * data point.
+ */
+class MagnitudeRange
+{
+public:
+    MagnitudeRange() : m_min(0), m_max(0) { }
+    MagnitudeRange(float min, float max) : m_min(min), m_max(max) { }
+    
+    bool operator==(const MagnitudeRange &r) {
+	return r.m_min == m_min && r.m_max == m_max;
+    }
+    bool operator!=(const MagnitudeRange &r) {
+        return !(*this == r);
+    }
+    
+    bool isSet() const { return (m_min != 0.f || m_max != 0.f); }
+    void set(float min, float max) {
+	m_min = min;
+	m_max = max;
+	if (m_max < m_min) m_max = m_min;
+    }
+    bool sample(float f) {
+	bool changed = false;
+	if (isSet()) {
+	    if (f < m_min) { m_min = f; changed = true; }
+	    if (f > m_max) { m_max = f; changed = true; }
+	} else {
+	    m_max = m_min = f;
+	    changed = true;
+	}
+	return changed;
+    }
+    bool sample(const std::vector<float> &ff) {
+        bool changed = false;
+        for (auto f: ff) {
+            if (sample(f)) {
+                changed = true;
+            }
+        }
+        return changed;
+    }
+    bool sample(const MagnitudeRange &r) {
+	bool changed = false;
+	if (isSet()) {
+	    if (r.m_min < m_min) { m_min = r.m_min; changed = true; }
+	    if (r.m_max > m_max) { m_max = r.m_max; changed = true; }
+	} else {
+	    m_min = r.m_min;
+	    m_max = r.m_max;
+	    changed = true;
+	}
+	return changed;
+    }            
+    float getMin() const { return m_min; }
+    float getMax() const { return m_max; }
+private:
+    float m_min;
+    float m_max;
+};
+
+#endif
--- a/base/PlayParameterRepository.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/PlayParameterRepository.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -35,14 +35,14 @@
 void
 PlayParameterRepository::addPlayable(const Playable *playable)
 {
-    cerr << "PlayParameterRepository:addPlayable playable = " << playable <<  endl;
+//    cerr << "PlayParameterRepository:addPlayable playable = " << playable <<  endl;
 
     if (!getPlayParameters(playable)) {
 
 	// Give all playables the same type of play parameters for the
 	// moment
 
-        cerr << "PlayParameterRepository:addPlayable: Adding play parameters for " << playable << endl;
+//        cerr << "PlayParameterRepository:addPlayable: Adding play parameters for " << playable << endl;
 
         PlayParameters *params = new PlayParameters;
         m_playParameters[playable] = params;
@@ -59,8 +59,8 @@
         connect(params, SIGNAL(playClipIdChanged(QString)),
                 this, SLOT(playClipIdChanged(QString)));
 
-        cerr << "Connected play parameters " << params << " for playable "
-                     << playable << " to this " << this << endl;
+//        cerr << "Connected play parameters " << params << " for playable "
+//                     << playable << " to this " << this << endl;
     }
 }    
 
--- a/base/Preferences.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/Preferences.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -40,11 +40,12 @@
     m_tuningFrequency(440),
     m_propertyBoxLayout(VerticallyStacked),
     m_windowType(HanningWindow),
-    m_resampleQuality(1),
+    m_runPluginsInProcess(true),
     m_omitRecentTemps(true),
     m_tempDirRoot(""),
     m_fixedSampleRate(0),
     m_resampleOnLoad(false),
+    m_gapless(true),
     m_normaliseAudio(false),
     m_viewFontSize(10),
     m_backgroundMode(BackgroundFromTheme),
@@ -64,9 +65,10 @@
         (settings.value("property-box-layout", int(VerticallyStacked)).toInt());
     m_windowType = WindowType
         (settings.value("window-type", int(HanningWindow)).toInt());
-    m_resampleQuality = settings.value("resample-quality", 1).toInt();
+    m_runPluginsInProcess = settings.value("run-vamp-plugins-in-process", true).toBool();
     m_fixedSampleRate = settings.value("fixed-sample-rate", 0).toDouble();
     m_resampleOnLoad = settings.value("resample-on-load", false).toBool();
+    m_gapless = settings.value("gapless", true).toBool();
     m_normaliseAudio = settings.value("normalise-audio", false).toBool();
     m_backgroundMode = BackgroundMode
         (settings.value("background-mode", int(BackgroundFromTheme)).toInt());
@@ -99,6 +101,7 @@
     props.push_back("Resample Quality");
     props.push_back("Omit Temporaries from Recent Files");
     props.push_back("Resample On Load");
+    props.push_back("Use Gapless Mode");
     props.push_back("Normalise Audio");
     props.push_back("Fixed Sample Rate");
     props.push_back("Temporary Directory Root");
@@ -141,6 +144,9 @@
     if (name == "Resample On Load") {
         return tr("Resample mismatching files on import");
     }
+    if (name == "Use Gapless Mode") {
+        return tr("Load mp3 files in gapless mode");
+    }
     if (name == "Fixed Sample Rate") {
         return tr("Single fixed sample rate to resample all files to");
     }
@@ -198,6 +204,9 @@
     if (name == "Resample On Load") {
         return ToggleProperty;
     }
+    if (name == "Use Gapless Mode") {
+        return ToggleProperty;
+    }
     if (name == "Fixed Sample Rate") {
         return ValueProperty;
     }
@@ -259,13 +268,10 @@
         return int(m_windowType);
     }
 
-    if (name == "Resample Quality") {
-        if (min) *min = 0;
-        if (max) *max = 2;
-        if (deflt) *deflt = 1;
-        return m_resampleQuality;
+    if (name == "Run Vamp Plugins In Process") {
+        return m_runPluginsInProcess;
     }
-
+    
     if (name == "Omit Temporaries from Recent Files") {
         if (deflt) *deflt = 1;
         return m_omitRecentTemps ? 1 : 0;
@@ -412,8 +418,8 @@
         setPropertyBoxLayout(value == 0 ? VerticallyStacked : Layered);
     } else if (name == "Window Type") {
         setWindowType(WindowType(value));
-    } else if (name == "Resample Quality") {
-        setResampleQuality(value);
+    } else if (name == "Run Vamp Plugins In Process") {
+        setRunPluginsInProcess(value ? true : false);
     } else if (name == "Omit Temporaries from Recent Files") {
         setOmitTempsFromRecentFiles(value ? true : false);
     } else if (name == "Background Mode") {
@@ -506,15 +512,15 @@
 }
 
 void
-Preferences::setResampleQuality(int q)
+Preferences::setRunPluginsInProcess(bool run)
 {
-    if (m_resampleQuality != q) {
-        m_resampleQuality = q;
+    if (m_runPluginsInProcess != run) {
+        m_runPluginsInProcess = run;
         QSettings settings;
         settings.beginGroup("Preferences");
-        settings.setValue("resample-quality", q);
+        settings.setValue("run-vamp-plugins-in-process", run);
         settings.endGroup();
-        emit propertyChanged("Resample Quality");
+        emit propertyChanged("Run Vamp Plugins In Process");
     }
 }
 
@@ -561,6 +567,19 @@
 }
 
 void
+Preferences::setUseGaplessMode(bool gapless)
+{
+    if (m_gapless != gapless) {
+        m_gapless = gapless;
+        QSettings settings;
+        settings.beginGroup("Preferences");
+        settings.setValue("gapless", gapless);
+        settings.endGroup();
+        emit propertyChanged("Use Gapless Mode");
+    }
+}
+
+void
 Preferences::setFixedSampleRate(sv_samplerate_t rate)
 {
     if (m_fixedSampleRate != rate) {
--- a/base/Preferences.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/Preferences.h	Fri Jan 13 10:29:44 2017 +0000
@@ -51,8 +51,9 @@
     SpectrogramXSmoothing getSpectrogramXSmoothing() const { return m_spectrogramXSmoothing; }
     double getTuningFrequency() const { return m_tuningFrequency; }
     WindowType getWindowType() const { return m_windowType; }
-    int getResampleQuality() const { return m_resampleQuality; }
 
+    bool getRunPluginsInProcess() const { return m_runPluginsInProcess; }
+    
     //!!! harmonise with PaneStack
     enum PropertyBoxLayout {
         VerticallyStacked,
@@ -70,7 +71,10 @@
     sv_samplerate_t getFixedSampleRate() const { return m_fixedSampleRate; }
 
     /// True if we should resample second or subsequent audio file to match first audio file's rate
-    bool getResampleOnLoad() const { return m_resampleOnLoad; }    
+    bool getResampleOnLoad() const { return m_resampleOnLoad; }
+
+    /// True if mp3 files should be loaded "gaplessly", i.e. compensating for encoder/decoder delay and padding
+    bool getUseGaplessMode() const { return m_gapless; }
     
     /// True if audio files should be loaded with normalisation (max == 1)
     bool getNormaliseAudio() const { return m_normaliseAudio; }
@@ -113,11 +117,12 @@
     void setTuningFrequency(double freq);
     void setPropertyBoxLayout(PropertyBoxLayout layout);
     void setWindowType(WindowType type);
-    void setResampleQuality(int quality);
+    void setRunPluginsInProcess(bool r);
     void setOmitTempsFromRecentFiles(bool omit);
     void setTemporaryDirectoryRoot(QString tempDirRoot);
     void setFixedSampleRate(sv_samplerate_t);
     void setResampleOnLoad(bool);
+    void setUseGaplessMode(bool);
     void setNormaliseAudio(bool);
     void setBackgroundMode(BackgroundMode mode);
     void setTimeToTextMode(TimeToTextMode mode);
@@ -150,11 +155,12 @@
     double m_tuningFrequency;
     PropertyBoxLayout m_propertyBoxLayout;
     WindowType m_windowType;
-    int m_resampleQuality;
+    bool m_runPluginsInProcess;
     bool m_omitRecentTemps;
     QString m_tempDirRoot;
     sv_samplerate_t m_fixedSampleRate;
     bool m_resampleOnLoad;
+    bool m_gapless;
     bool m_normaliseAudio;
     int m_viewFontSize;
     BackgroundMode m_backgroundMode;
--- a/base/Profiler.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/Profiler.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -47,15 +47,11 @@
     dump();
 }
 
+#ifndef NO_TIMING
 void Profiles::accumulate(
-#ifndef NO_TIMING
     const char* id, clock_t time, RealTime rt
-#else
-    const char*, clock_t, RealTime
-#endif
 )
 {
-#ifndef NO_TIMING    
     ProfilePair &pair(m_profiles[id]);
     ++pair.first;
     pair.second.first += time;
@@ -72,8 +68,8 @@
     if (rt > worstPair.second) {
         worstPair.second = rt;
     }
+}
 #endif
-}
 
 void Profiles::dump() const
 {
--- a/base/Profiler.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/Profiler.h	Fri Jan 13 10:29:44 2017 +0000
@@ -25,8 +25,6 @@
 
 #include "system/System.h"
 
-#include <ctime>
-#include <sys/time.h>
 #include <map>
 
 #include "RealTime.h"
@@ -41,6 +39,11 @@
 #endif
 #endif
 
+#ifndef NO_TIMING
+#include <ctime>
+#include <sys/time.h>
+#endif
+
 /**
  * Profiling classes
  */
@@ -56,12 +59,15 @@
     static Profiles* getInstance();
     ~Profiles();
 
+#ifndef NO_TIMING
     void accumulate(const char* id, clock_t time, RealTime rt);
+#endif
     void dump() const;
 
 protected:
     Profiles();
 
+#ifndef NO_TIMING
     typedef std::pair<clock_t, RealTime> TimePair;
     typedef std::pair<int, TimePair> ProfilePair;
     typedef std::map<const char *, ProfilePair> ProfileMap;
@@ -70,6 +76,7 @@
     ProfileMap m_profiles;
     LastCallMap m_lastCalls;
     WorstCallMap m_worstCalls;
+#endif
 
     static Profiles* m_instance;
 };
--- a/base/PropertyContainer.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/PropertyContainer.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -59,6 +59,12 @@
     return QString();
 }
 
+QString
+PropertyContainer::getPropertyValueIconName(const PropertyName &, int) const
+{
+    return QString();
+}
+
 RangeMapper *
 PropertyContainer::getNewPropertyRangeMapper(const PropertyName &) const
 {
@@ -172,6 +178,7 @@
 
     case ValueProperty:
     case ColourProperty:
+    case ColourMapProperty:
     {
         int min, max;
         getPropertyRangeAndValue(name, &min, &max, 0);
--- a/base/PropertyContainer.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/PropertyContainer.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _PROPERTY_CONTAINER_H_
-#define _PROPERTY_CONTAINER_H_
+#ifndef SV_PROPERTY_CONTAINER_H
+#define SV_PROPERTY_CONTAINER_H
 
 #include "Command.h"
 
@@ -40,6 +40,7 @@
 	RangeProperty, // range of integers
 	ValueProperty, // range of integers given string labels
 	ColourProperty, // colours, get/set as ColourDatabase indices
+        ColourMapProperty, // colour maps, get/set as ColourMapper::StandardMap enum
         UnitsProperty, // unit from UnitDatabase, get/set unit id
 	InvalidProperty, // property not found!
     };
@@ -91,6 +92,13 @@
 					  int value) const;
 
     /**
+     * If the given property is a ValueProperty, return the icon to be
+     * used for the given value for that property, if any.
+     */
+    virtual QString getPropertyValueIconName(const PropertyName &,
+                                             int value) const;
+
+    /**
      * If the given property is a RangeProperty, return a new
      * RangeMapper object mapping its integer range onto an underlying
      * floating point value range for human-intelligible display, if
--- a/base/RangeMapper.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/RangeMapper.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -23,13 +23,15 @@
 
 LinearRangeMapper::LinearRangeMapper(int minpos, int maxpos,
 				     double minval, double maxval,
-                                     QString unit, bool inverted) :
+                                     QString unit, bool inverted,
+                                     std::map<int, QString> labels) :
     m_minpos(minpos),
     m_maxpos(maxpos),
     m_minval(minval),
     m_maxval(maxval),
     m_unit(unit),
-    m_inverted(inverted)
+    m_inverted(inverted),
+    m_labels(labels)
 {
     assert(m_maxval != m_minval);
     assert(m_maxpos != m_minpos);
@@ -70,9 +72,20 @@
     double value = m_minval +
         ((double(position - m_minpos) / double(m_maxpos - m_minpos))
          * (m_maxval - m_minval));
+//    cerr << "getValueForPositionUnclamped(" << position << "): minval " << m_minval << ", maxval " << m_maxval << ", value " << value << endl;
     return value;
 }
 
+QString
+LinearRangeMapper::getLabel(int position) const
+{
+    if (m_labels.find(position) != m_labels.end()) {
+        return m_labels.at(position);
+    } else {
+        return "";
+    }
+}
+
 LogRangeMapper::LogRangeMapper(int minpos, int maxpos,
                                double minval, double maxval,
                                QString unit, bool inverted) :
--- a/base/RangeMapper.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/RangeMapper.h	Fri Jan 13 10:29:44 2017 +0000
@@ -50,7 +50,7 @@
     virtual double getValueForPosition(int position) const = 0;
 
     /**
-     * Return the value mapped from the given positionq, without
+     * Return the value mapped from the given position, without
      * clamping. That is, whatever mapping function is in use will be
      * projected even outside the minimum and maximum extents of the
      * mapper's value range. (The mapping outside that range is not
@@ -62,6 +62,16 @@
      * Get the unit of the mapper's value range.
      */
     virtual QString getUnit() const { return ""; }
+
+    /**
+     * The mapper may optionally provide special labels for one or
+     * more individual positions (such as the minimum position, the
+     * default, or indeed all positions). These should be used in any
+     * display context in preference to just showing the numerical
+     * value for the position. If a position has such a label, return
+     * it here.
+     */
+    virtual QString getLabel(int /* position */) const { return ""; }
 };
 
 
@@ -76,7 +86,8 @@
      */
     LinearRangeMapper(int minpos, int maxpos,
                       double minval, double maxval,
-                      QString unit = "", bool inverted = false);
+                      QString unit = "", bool inverted = false,
+                      std::map<int, QString> labels = {});
     
     virtual int getPositionForValue(double value) const;
     virtual int getPositionForValueUnclamped(double value) const;
@@ -85,6 +96,7 @@
     virtual double getValueForPositionUnclamped(int position) const;
 
     virtual QString getUnit() const { return m_unit; }
+    virtual QString getLabel(int position) const;
 
 protected:
     int m_minpos;
@@ -93,6 +105,7 @@
     double m_maxval;
     QString m_unit;
     bool m_inverted;
+    std::map<int, QString> m_labels;
 };
 
 class LogRangeMapper : public RangeMapper
--- a/base/RealTime.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,481 +0,0 @@
-/* -*- 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.
-*/
-
-/*
-   This is a modified version of a source file from the 
-   Rosegarden MIDI and audio sequencer and notation editor.
-   This file copyright 2000-2006 Chris Cannam.
-*/
-
-#include <iostream>
-
-#include <cstdlib>
-#include <sstream>
-
-#include "RealTime.h"
-#include "sys/time.h"
-
-#include "Debug.h"
-
-#include "Preferences.h"
-
-// A RealTime consists of two ints that must be at least 32 bits each.
-// A signed 32-bit int can store values exceeding +/- 2 billion.  This
-// means we can safely use our lower int for nanoseconds, as there are
-// 1 billion nanoseconds in a second and we need to handle double that
-// because of the implementations of addition etc that we use.
-//
-// The maximum valid RealTime on a 32-bit system is somewhere around
-// 68 years: 999999999 nanoseconds longer than the classic Unix epoch.
-
-#define ONE_BILLION 1000000000
-
-RealTime::RealTime(int s, int n) :
-    sec(s), nsec(n)
-{
-    if (sec == 0) {
-	while (nsec <= -ONE_BILLION) { nsec += ONE_BILLION; --sec; }
-	while (nsec >=  ONE_BILLION) { nsec -= ONE_BILLION; ++sec; }
-    } else if (sec < 0) {
-	while (nsec <= -ONE_BILLION) { nsec += ONE_BILLION; --sec; }
-	while (nsec > 0)             { nsec -= ONE_BILLION; ++sec; }
-    } else { 
-	while (nsec >=  ONE_BILLION) { nsec -= ONE_BILLION; ++sec; }
-	while (nsec < 0)             { nsec += ONE_BILLION; --sec; }
-    }
-}
-
-RealTime
-RealTime::fromSeconds(double sec)
-{
-    if (sec >= 0) {
-        return RealTime(int(sec), int((sec - int(sec)) * ONE_BILLION + 0.5));
-    } else {
-        return -fromSeconds(-sec);
-    }
-}
-
-RealTime
-RealTime::fromMilliseconds(int msec)
-{
-    return RealTime(msec / 1000, (msec % 1000) * 1000000);
-}
-
-RealTime
-RealTime::fromTimeval(const struct timeval &tv)
-{
-    return RealTime(int(tv.tv_sec), int(tv.tv_usec * 1000));
-}
-
-RealTime
-RealTime::fromXsdDuration(std::string xsdd)
-{
-    RealTime t;
-
-    int year = 0, month = 0, day = 0, hour = 0, minute = 0;
-    double second = 0.0;
-
-    int i = 0;
-
-    const char *s = xsdd.c_str();
-    int len = int(xsdd.length());
-
-    bool negative = false, afterT = false;
-
-    while (i < len) {
-
-        if (s[i] == '-') {
-            if (i == 0) negative = true;
-            ++i;
-            continue;
-        }
-
-        double value = 0.0;
-        char *eptr = 0;
-
-        if (isdigit(s[i]) || s[i] == '.') {
-            value = strtod(&s[i], &eptr);
-            i = int(eptr - s);
-        }
-
-        if (i == len) break;
-
-        switch (s[i]) {
-        case 'Y': year = int(value + 0.1); break;
-        case 'D': day  = int(value + 0.1); break;
-        case 'H': hour = int(value + 0.1); break;
-        case 'M':
-            if (afterT) minute = int(value + 0.1);
-            else month = int(value + 0.1);
-            break;
-        case 'S':
-            second = value;
-            break;
-        case 'T': afterT = true; break;
-        };
-
-        ++i;
-    }
-
-    if (year > 0) {
-        cerr << "WARNING: This xsd:duration (\"" << xsdd << "\") contains a non-zero year.\nWith no origin and a limited data size, I will treat a year as exactly 31556952\nseconds and you should expect overflow and/or poor results." << endl;
-        t = t + RealTime(year * 31556952, 0);
-    }
-
-    if (month > 0) {
-        cerr << "WARNING: This xsd:duration (\"" << xsdd << "\") contains a non-zero month.\nWith no origin and a limited data size, I will treat a month as exactly 2629746\nseconds and you should expect overflow and/or poor results." << endl;
-        t = t + RealTime(month * 2629746, 0);
-    }
-
-    if (day > 0) {
-        t = t + RealTime(day * 86400, 0);
-    }
-
-    if (hour > 0) {
-        t = t + RealTime(hour * 3600, 0);
-    }
-
-    if (minute > 0) {
-        t = t + RealTime(minute * 60, 0);
-    }
-
-    t = t + fromSeconds(second);
-
-    if (negative) {
-        return -t;
-    } else {
-        return t;
-    }
-}
-
-double
-RealTime::toDouble() const
-{
-    double d = sec;
-    d += double(nsec) / double(ONE_BILLION);
-    return d;
-}
-
-std::ostream &operator<<(std::ostream &out, const RealTime &rt)
-{
-    if (rt < RealTime::zeroTime) {
-	out << "-";
-    } else {
-	out << " ";
-    }
-
-    int s = (rt.sec < 0 ? -rt.sec : rt.sec);
-    int n = (rt.nsec < 0 ? -rt.nsec : rt.nsec);
-
-    out << s << ".";
-
-    int nn(n);
-    if (nn == 0) out << "00000000";
-    else while (nn < (ONE_BILLION / 10)) {
-	out << "0";
-	nn *= 10;
-    }
-    
-    out << n << "R";
-    return out;
-}
-
-std::string
-RealTime::toString(bool align) const
-{
-    std::stringstream out;
-    out << *this;
-    
-    std::string s = out.str();
-
-    if (!align && *this >= RealTime::zeroTime) {
-        // remove leading " "
-        s = s.substr(1, s.length() - 1);
-    }
-
-    // remove trailing R
-    return s.substr(0, s.length() - 1);
-}
-
-RealTime
-RealTime::fromString(std::string s)
-{
-    bool negative = false;
-    int section = 0;
-    std::string ssec, snsec;
-
-    for (size_t i = 0; i < s.length(); ++i) {
-
-        char c = s[i];
-        if (isspace(c)) continue;
-
-        if (section == 0) {
-
-            if (c == '-') negative = true;
-            else if (isdigit(c)) { section = 1; ssec += c; }
-            else if (c == '.') section = 2;
-            else break;
-
-        } else if (section == 1) {
-
-            if (c == '.') section = 2;
-            else if (isdigit(c)) ssec += c;
-            else break;
-
-        } else if (section == 2) {
-
-            if (isdigit(c)) snsec += c;
-            else break;
-        }
-    }
-
-    while (snsec.length() < 8) snsec += '0';
-
-    int sec = atoi(ssec.c_str());
-    int nsec = atoi(snsec.c_str());
-    if (negative) sec = -sec;
-
-//    SVDEBUG << "RealTime::fromString: string " << s << " -> "
-//              << sec << " sec, " << nsec << " nsec" << endl;
-
-    return RealTime(sec, nsec);
-}
-
-std::string
-RealTime::toText(bool fixedDp) const
-{
-    if (*this < RealTime::zeroTime) return "-" + (-*this).toText(fixedDp);
-
-    Preferences *p = Preferences::getInstance();
-    bool hms = true;
-    
-    if (p) {
-        hms = p->getShowHMS();
-        int fps = 0;
-        switch (p->getTimeToTextMode()) {
-        case Preferences::TimeToTextMs: break;
-        case Preferences::TimeToTextUs: fps = 1000000; 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);
-    }
-
-    return toMSText(fixedDp, hms);
-}
-
-static void
-writeSecPart(std::stringstream &out, bool hms, int sec)
-{
-    if (hms) {
-        if (sec >= 3600) {
-            out << (sec / 3600) << ":";
-        }
-
-        if (sec >= 60) {
-            int minutes = (sec % 3600) / 60;
-            if (sec >= 3600 && minutes < 10) out << "0";
-            out << minutes << ":";
-        }
-
-        if (sec >= 10) {
-            out << ((sec % 60) / 10);
-        }
-
-        out << (sec % 10);
-
-    } else {
-        out << sec;
-    }
-}
-
-std::string
-RealTime::toMSText(bool fixedDp, bool hms) const
-{
-    if (*this < RealTime::zeroTime) return "-" + (-*this).toMSText(fixedDp, hms);
-
-    std::stringstream out;
-
-    writeSecPart(out, hms, sec);
-    
-    int ms = msec();
-
-    if (ms != 0) {
-	out << ".";
-	out << (ms / 100);
-	ms = ms % 100;
-	if (ms != 0) {
-	    out << (ms / 10);
-	    ms = ms % 10;
-	} else if (fixedDp) {
-	    out << "0";
-	}
-	if (ms != 0) {
-	    out << ms;
-	} else if (fixedDp) {
-	    out << "0";
-	}
-    } else if (fixedDp) {
-	out << ".000";
-    }
-	
-    std::string s = out.str();
-
-    return s;
-}
-
-std::string
-RealTime::toFrameText(int fps, bool hms) const
-{
-    if (*this < RealTime::zeroTime) return "-" + (-*this).toFrameText(fps, hms);
-
-    std::stringstream out;
-
-    writeSecPart(out, hms, sec);
-
-    // avoid rounding error if fps does not divide into ONE_BILLION
-    int64_t fbig = nsec;
-    fbig *= fps;
-    int f = int(fbig / ONE_BILLION);
-
-    int div = 1;
-    int n = fps - 1;
-    while ((n = n / 10)) {
-        div *= 10;
-    }
-
-    out << ":";
-
-//    cerr << "div = " << div << ", f =  "<< f << endl;
-
-    while (div) {
-        int d = (f / div) % 10;
-        out << d;
-        div /= 10;
-    }
-	
-    std::string s = out.str();
-
-//    cerr << "converted " << toString() << " to " << s << endl;
-
-    return s;
-}
-
-std::string
-RealTime::toSecText() const
-{
-    if (*this < RealTime::zeroTime) return "-" + (-*this).toSecText();
-
-    std::stringstream out;
-
-    writeSecPart(out, true, sec);
-    
-    if (sec < 60) {
-        out << "s";
-    }
-
-    std::string s = out.str();
-
-    return s;
-}
-
-std::string
-RealTime::toXsdDuration() const
-{
-    std::string s = "PT" + toString(false) + "S";
-    return s;
-}
-
-RealTime
-RealTime::operator*(int m) const
-{
-    double t = (double(nsec) / ONE_BILLION) * m;
-    t += sec * m;
-    return fromSeconds(t);
-}
-
-RealTime
-RealTime::operator/(int d) const
-{
-    int secdiv = sec / d;
-    int secrem = sec % d;
-
-    double nsecdiv = (double(nsec) + ONE_BILLION * double(secrem)) / d;
-    
-    return RealTime(secdiv, int(nsecdiv + 0.5));
-}
-
-RealTime
-RealTime::operator*(double m) const
-{
-    double t = (double(nsec) / ONE_BILLION) * m;
-    t += sec * m;
-    return fromSeconds(t);
-}
-
-RealTime
-RealTime::operator/(double d) const
-{
-    double t = (double(nsec) / ONE_BILLION) / d;
-    t += sec / d;
-    return fromSeconds(t);
-}
-
-double 
-RealTime::operator/(const RealTime &r) const
-{
-    double lTotal = double(sec) * ONE_BILLION + double(nsec);
-    double rTotal = double(r.sec) * ONE_BILLION + double(r.nsec);
-    
-    if (rTotal == 0) return 0.0;
-    else return lTotal/rTotal;
-}
-
-static RealTime
-frame2RealTime_i(sv_frame_t frame, sv_frame_t iSampleRate)
-{
-    if (frame < 0) return -frame2RealTime_i(-frame, iSampleRate);
-
-    RealTime rt;
-    sv_frame_t sec = frame / iSampleRate;
-    rt.sec = int(sec);
-    frame -= sec * iSampleRate;
-    rt.nsec = (int)(((double(frame) * 1000000.0) / double(iSampleRate)) * 1000.0);
-    return rt;
-}
-
-sv_frame_t
-RealTime::realTime2Frame(const RealTime &time, sv_samplerate_t sampleRate)
-{
-    if (time < zeroTime) return -realTime2Frame(-time, sampleRate);
-    double s = time.sec + double(time.nsec + 1) / 1000000000.0;
-    return sv_frame_t(s * sampleRate);
-}
-
-RealTime
-RealTime::frame2RealTime(sv_frame_t frame, sv_samplerate_t sampleRate)
-{
-    if (sampleRate == double(int(sampleRate))) {
-        return frame2RealTime_i(frame, int(sampleRate));
-    }
-
-    double sec = double(frame) / sampleRate;
-    return fromSeconds(sec);
-}
-
-const RealTime RealTime::zeroTime(0,0);
-
--- a/base/RealTime.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/RealTime.h	Fri Jan 13 10:29:44 2017 +0000
@@ -28,7 +28,11 @@
 
 #include <vamp-hostsdk/RealTime.h>
 
-struct timeval;
+#ifdef _MSC_VER
+#include "winsock.h" // struct timeval is in here
+#else
+#include "sys/time.h"
+#endif
 
 /**
  * RealTime represents time values to nanosecond precision
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/RealTimeSV.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,486 @@
+/* -*- 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.
+*/
+
+/*
+   This is a modified version of a source file from the 
+   Rosegarden MIDI and audio sequencer and notation editor.
+   This file copyright 2000-2006 Chris Cannam.
+*/
+
+#include <iostream>
+
+#include <cstdlib>
+#include <sstream>
+
+#include "RealTime.h"
+
+#include "Debug.h"
+
+#include "Preferences.h"
+
+// A RealTime consists of two ints that must be at least 32 bits each.
+// A signed 32-bit int can store values exceeding +/- 2 billion.  This
+// means we can safely use our lower int for nanoseconds, as there are
+// 1 billion nanoseconds in a second and we need to handle double that
+// because of the implementations of addition etc that we use.
+//
+// The maximum valid RealTime on a 32-bit system is somewhere around
+// 68 years: 999999999 nanoseconds longer than the classic Unix epoch.
+
+#define ONE_BILLION 1000000000
+
+RealTime::RealTime(int s, int n) :
+    sec(s), nsec(n)
+{
+    if (sec == 0) {
+	while (nsec <= -ONE_BILLION) { nsec += ONE_BILLION; --sec; }
+	while (nsec >=  ONE_BILLION) { nsec -= ONE_BILLION; ++sec; }
+    } else if (sec < 0) {
+	while (nsec <= -ONE_BILLION) { nsec += ONE_BILLION; --sec; }
+	while (nsec > 0 && sec < 0)  { nsec -= ONE_BILLION; ++sec; }
+    } else { 
+	while (nsec >=  ONE_BILLION) { nsec -= ONE_BILLION; ++sec; }
+	while (nsec < 0 && sec > 0)  { nsec += ONE_BILLION; --sec; }
+    }
+}
+
+RealTime
+RealTime::fromSeconds(double sec)
+{
+    if (sec >= 0) {
+        return RealTime(int(sec), int((sec - int(sec)) * ONE_BILLION + 0.5));
+    } else {
+        return -fromSeconds(-sec);
+    }
+}
+
+RealTime
+RealTime::fromMilliseconds(int msec)
+{
+    return RealTime(msec / 1000, (msec % 1000) * 1000000);
+}
+
+RealTime
+RealTime::fromTimeval(const struct timeval &tv)
+{
+    return RealTime(int(tv.tv_sec), int(tv.tv_usec * 1000));
+}
+
+RealTime
+RealTime::fromXsdDuration(std::string xsdd)
+{
+    RealTime t;
+
+    int year = 0, month = 0, day = 0, hour = 0, minute = 0;
+    double second = 0.0;
+
+    char *loc = setlocale(LC_NUMERIC, 0);
+    (void)setlocale(LC_NUMERIC, "C"); // avoid strtod expecting ,-separator in DE
+
+    int i = 0;
+
+    const char *s = xsdd.c_str();
+    int len = int(xsdd.length());
+
+    bool negative = false, afterT = false;
+
+    while (i < len) {
+
+        if (s[i] == '-') {
+            if (i == 0) negative = true;
+            ++i;
+            continue;
+        }
+
+        double value = 0.0;
+        char *eptr = 0;
+
+        if (isdigit(s[i]) || s[i] == '.') {
+            value = strtod(&s[i], &eptr);
+            i = int(eptr - s);
+        }
+
+        if (i == len) break;
+
+        switch (s[i]) {
+        case 'Y': year = int(value + 0.1); break;
+        case 'D': day  = int(value + 0.1); break;
+        case 'H': hour = int(value + 0.1); break;
+        case 'M':
+            if (afterT) minute = int(value + 0.1);
+            else month = int(value + 0.1);
+            break;
+        case 'S':
+            second = value;
+            break;
+        case 'T': afterT = true; break;
+        };
+
+        ++i;
+    }
+
+    if (year > 0) {
+        cerr << "WARNING: This xsd:duration (\"" << xsdd << "\") contains a non-zero year.\nWith no origin and a limited data size, I will treat a year as exactly 31556952\nseconds and you should expect overflow and/or poor results." << endl;
+        t = t + RealTime(year * 31556952, 0);
+    }
+
+    if (month > 0) {
+        cerr << "WARNING: This xsd:duration (\"" << xsdd << "\") contains a non-zero month.\nWith no origin and a limited data size, I will treat a month as exactly 2629746\nseconds and you should expect overflow and/or poor results." << endl;
+        t = t + RealTime(month * 2629746, 0);
+    }
+
+    if (day > 0) {
+        t = t + RealTime(day * 86400, 0);
+    }
+
+    if (hour > 0) {
+        t = t + RealTime(hour * 3600, 0);
+    }
+
+    if (minute > 0) {
+        t = t + RealTime(minute * 60, 0);
+    }
+
+    t = t + fromSeconds(second);
+
+    setlocale(LC_NUMERIC, loc);
+    
+    if (negative) {
+        return -t;
+    } else {
+        return t;
+    }
+}
+
+double
+RealTime::toDouble() const
+{
+    double d = sec;
+    d += double(nsec) / double(ONE_BILLION);
+    return d;
+}
+
+std::ostream &operator<<(std::ostream &out, const RealTime &rt)
+{
+    if (rt < RealTime::zeroTime) {
+	out << "-";
+    } else {
+	out << " ";
+    }
+
+    int s = (rt.sec < 0 ? -rt.sec : rt.sec);
+    int n = (rt.nsec < 0 ? -rt.nsec : rt.nsec);
+
+    out << s << ".";
+
+    int nn(n);
+    if (nn == 0) out << "00000000";
+    else while (nn < (ONE_BILLION / 10)) {
+	out << "0";
+	nn *= 10;
+    }
+    
+    out << n << "R";
+    return out;
+}
+
+std::string
+RealTime::toString(bool align) const
+{
+    std::stringstream out;
+    out << *this;
+    
+    std::string s = out.str();
+
+    if (!align && *this >= RealTime::zeroTime) {
+        // remove leading " "
+        s = s.substr(1, s.length() - 1);
+    }
+
+    // remove trailing R
+    return s.substr(0, s.length() - 1);
+}
+
+RealTime
+RealTime::fromString(std::string s)
+{
+    bool negative = false;
+    int section = 0;
+    std::string ssec, snsec;
+
+    for (size_t i = 0; i < s.length(); ++i) {
+
+        char c = s[i];
+        if (isspace(c)) continue;
+
+        if (section == 0) {
+
+            if (c == '-') negative = true;
+            else if (isdigit(c)) { section = 1; ssec += c; }
+            else if (c == '.') section = 2;
+            else break;
+
+        } else if (section == 1) {
+
+            if (c == '.') section = 2;
+            else if (isdigit(c)) ssec += c;
+            else break;
+
+        } else if (section == 2) {
+
+            if (isdigit(c)) snsec += c;
+            else break;
+        }
+    }
+
+    while (snsec.length() < 8) snsec += '0';
+
+    int sec = atoi(ssec.c_str());
+    int nsec = atoi(snsec.c_str());
+    if (negative) sec = -sec;
+
+//    SVDEBUG << "RealTime::fromString: string " << s << " -> "
+//              << sec << " sec, " << nsec << " nsec" << endl;
+
+    return RealTime(sec, nsec);
+}
+
+std::string
+RealTime::toText(bool fixedDp) const
+{
+    if (*this < RealTime::zeroTime) return "-" + (-*this).toText(fixedDp);
+
+    Preferences *p = Preferences::getInstance();
+    bool hms = true;
+    
+    if (p) {
+        hms = p->getShowHMS();
+        int fps = 0;
+        switch (p->getTimeToTextMode()) {
+        case Preferences::TimeToTextMs: break;
+        case Preferences::TimeToTextUs: fps = 1000000; 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);
+    }
+
+    return toMSText(fixedDp, hms);
+}
+
+static void
+writeSecPart(std::stringstream &out, bool hms, int sec)
+{
+    if (hms) {
+        if (sec >= 3600) {
+            out << (sec / 3600) << ":";
+        }
+
+        if (sec >= 60) {
+            int minutes = (sec % 3600) / 60;
+            if (sec >= 3600 && minutes < 10) out << "0";
+            out << minutes << ":";
+        }
+
+        if (sec >= 10) {
+            out << ((sec % 60) / 10);
+        }
+
+        out << (sec % 10);
+
+    } else {
+        out << sec;
+    }
+}
+
+std::string
+RealTime::toMSText(bool fixedDp, bool hms) const
+{
+    if (*this < RealTime::zeroTime) return "-" + (-*this).toMSText(fixedDp, hms);
+
+    std::stringstream out;
+
+    writeSecPart(out, hms, sec);
+    
+    int ms = msec();
+
+    if (ms != 0) {
+	out << ".";
+	out << (ms / 100);
+	ms = ms % 100;
+	if (ms != 0) {
+	    out << (ms / 10);
+	    ms = ms % 10;
+	} else if (fixedDp) {
+	    out << "0";
+	}
+	if (ms != 0) {
+	    out << ms;
+	} else if (fixedDp) {
+	    out << "0";
+	}
+    } else if (fixedDp) {
+	out << ".000";
+    }
+	
+    std::string s = out.str();
+
+    return s;
+}
+
+std::string
+RealTime::toFrameText(int fps, bool hms) const
+{
+    if (*this < RealTime::zeroTime) return "-" + (-*this).toFrameText(fps, hms);
+
+    std::stringstream out;
+
+    writeSecPart(out, hms, sec);
+
+    // avoid rounding error if fps does not divide into ONE_BILLION
+    int64_t fbig = nsec;
+    fbig *= fps;
+    int f = int(fbig / ONE_BILLION);
+
+    int div = 1;
+    int n = fps - 1;
+    while ((n = n / 10)) {
+        div *= 10;
+    }
+
+    out << ":";
+
+//    cerr << "div = " << div << ", f =  "<< f << endl;
+
+    while (div) {
+        int d = (f / div) % 10;
+        out << d;
+        div /= 10;
+    }
+	
+    std::string s = out.str();
+
+//    cerr << "converted " << toString() << " to " << s << endl;
+
+    return s;
+}
+
+std::string
+RealTime::toSecText() const
+{
+    if (*this < RealTime::zeroTime) return "-" + (-*this).toSecText();
+
+    std::stringstream out;
+
+    writeSecPart(out, true, sec);
+    
+    if (sec < 60) {
+        out << "s";
+    }
+
+    std::string s = out.str();
+
+    return s;
+}
+
+std::string
+RealTime::toXsdDuration() const
+{
+    std::string s = "PT" + toString(false) + "S";
+    return s;
+}
+
+RealTime
+RealTime::operator*(int m) const
+{
+    double t = (double(nsec) / ONE_BILLION) * m;
+    t += sec * m;
+    return fromSeconds(t);
+}
+
+RealTime
+RealTime::operator/(int d) const
+{
+    int secdiv = sec / d;
+    int secrem = sec % d;
+
+    double nsecdiv = (double(nsec) + ONE_BILLION * double(secrem)) / d;
+    
+    return RealTime(secdiv, int(nsecdiv + 0.5));
+}
+
+RealTime
+RealTime::operator*(double m) const
+{
+    double t = (double(nsec) / ONE_BILLION) * m;
+    t += sec * m;
+    return fromSeconds(t);
+}
+
+RealTime
+RealTime::operator/(double d) const
+{
+    double t = (double(nsec) / ONE_BILLION) / d;
+    t += sec / d;
+    return fromSeconds(t);
+}
+
+double 
+RealTime::operator/(const RealTime &r) const
+{
+    double lTotal = double(sec) * ONE_BILLION + double(nsec);
+    double rTotal = double(r.sec) * ONE_BILLION + double(r.nsec);
+    
+    if (rTotal == 0) return 0.0;
+    else return lTotal/rTotal;
+}
+
+static RealTime
+frame2RealTime_i(sv_frame_t frame, sv_frame_t iSampleRate)
+{
+    if (frame < 0) return -frame2RealTime_i(-frame, iSampleRate);
+
+    int sec = int(frame / iSampleRate);
+    frame -= sec * iSampleRate;
+    int nsec = int((double(frame) / double(iSampleRate)) * ONE_BILLION + 0.5);
+    // Use ctor here instead of setting data members directly to
+    // ensure nsec > ONE_BILLION is handled properly.  It's extremely
+    // unlikely, but not impossible.
+    return RealTime(sec, nsec);
+}
+
+sv_frame_t
+RealTime::realTime2Frame(const RealTime &time, sv_samplerate_t sampleRate)
+{
+    if (time < zeroTime) return -realTime2Frame(-time, sampleRate);
+    double s = time.sec + double(time.nsec) / 1000000000.0;
+    return sv_frame_t(s * sampleRate + 0.5);
+}
+
+RealTime
+RealTime::frame2RealTime(sv_frame_t frame, sv_samplerate_t sampleRate)
+{
+    if (sampleRate == double(int(sampleRate))) {
+        return frame2RealTime_i(frame, int(sampleRate));
+    }
+
+    double sec = double(frame) / sampleRate;
+    return fromSeconds(sec);
+}
+
+const RealTime RealTime::zeroTime(0,0);
+
--- a/base/Resampler.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,196 +0,0 @@
-/* -*- 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.
-*/
-
-/*
-   This is a modified version of a source file from the 
-   Rubber Band audio timestretcher library.
-   This file copyright 2007 Chris Cannam.
-*/
-
-#include "Resampler.h"
-
-#include <cstdlib>
-#include <cmath>
-
-#include <iostream>
-
-#include <samplerate.h>
-
-#include "Debug.h"
-
-class Resampler::D
-{
-public:
-    D(Quality quality, int channels, sv_frame_t chunkSize);
-    ~D();
-
-    sv_frame_t resample(float **in, float **out,
-                 sv_frame_t incount, double ratio,
-                 bool final);
-
-    sv_frame_t resampleInterleaved(float *in, float *out,
-                            sv_frame_t incount, double ratio,
-                            bool final);
-
-    void reset();
-
-protected:
-    SRC_STATE *m_src;
-    float *m_iin;
-    float *m_iout;
-    int m_channels;
-    sv_frame_t m_iinsize;
-    sv_frame_t m_ioutsize;
-};
-
-Resampler::D::D(Quality quality, int channels, sv_frame_t chunkSize) :
-    m_src(0),
-    m_iin(0),
-    m_iout(0),
-    m_channels(channels),
-    m_iinsize(0),
-    m_ioutsize(0)
-{
-    int err = 0;
-    m_src = src_new(quality == Best ? SRC_SINC_BEST_QUALITY :
-                    quality == Fastest ? SRC_LINEAR :
-                    SRC_SINC_FASTEST,
-                    channels, &err);
-
-    //!!! check err, throw
-
-    if (chunkSize > 0 && m_channels > 1) {
-        //!!! alignment?
-        m_iinsize = chunkSize * m_channels;
-        m_ioutsize = chunkSize * m_channels * 2;
-        m_iin = (float *)malloc(m_iinsize * sizeof(float));
-        m_iout = (float *)malloc(m_ioutsize * sizeof(float));
-    }
-}
-
-Resampler::D::~D()
-{
-    src_delete(m_src);
-    if (m_iinsize > 0) {
-        free(m_iin);
-    }
-    if (m_ioutsize > 0) {
-        free(m_iout);
-    }
-}
-
-sv_frame_t
-Resampler::D::resample(float **in, float **out,
-                       sv_frame_t incount, double ratio,
-                       bool final)
-{
-    if (m_channels == 1) {
-        return resampleInterleaved(*in, *out, incount, ratio, final);
-    }
-
-    sv_frame_t outcount = lrint(ceil(double(incount) * ratio));
-
-    if (incount * m_channels > m_iinsize) {
-        m_iinsize = incount * m_channels;
-        m_iin = (float *)realloc(m_iin, m_iinsize * sizeof(float));
-    }
-    if (outcount * m_channels > m_ioutsize) {
-        m_ioutsize = outcount * m_channels;
-        m_iout = (float *)realloc(m_iout, m_ioutsize * sizeof(float));
-    }
-    for (sv_frame_t i = 0; i < incount; ++i) {
-        for (int c = 0; c < m_channels; ++c) {
-            m_iin[i * m_channels + c] = in[c][i];
-        }
-    }
-    
-    sv_frame_t gen = resampleInterleaved(m_iin, m_iout, incount, ratio, final);
-
-    for (sv_frame_t i = 0; i < gen; ++i) {
-        for (int c = 0; c < m_channels; ++c) {
-            out[c][i] = m_iout[i * m_channels + c];
-        }
-    }
-
-    return gen;
-}
-
-sv_frame_t
-Resampler::D::resampleInterleaved(float *in, float *out,
-                                  sv_frame_t incount, double ratio,
-                                  bool final)
-{
-    SRC_DATA data;
-
-    sv_frame_t outcount = lrint(ceil(double(incount) * ratio));
-
-    data.data_in = in;
-    data.data_out = out;
-    data.input_frames = incount;
-    data.output_frames = outcount;
-    data.src_ratio = ratio;
-    data.end_of_input = (final ? 1 : 0);
-
-    int err = src_process(m_src, &data);
-
-    if (err) {
-        cerr << "Resampler: ERROR: src_process returned error: " <<
-            src_strerror(err) << endl;
-        return 0;
-    }
-
-    if (data.input_frames_used != incount) {
-        cerr << "Resampler: NOTE: input_frames_used == " << data.input_frames_used << " (while incount = " << incount << ")" << endl;
-    }
-
-    return data.output_frames_gen;
-}
-
-void
-Resampler::D::reset()
-{
-    src_reset(m_src);
-}
-
-Resampler::Resampler(Quality quality, int channels, sv_frame_t chunkSize)
-{
-    m_d = new D(quality, channels, chunkSize);
-}
-
-Resampler::~Resampler()
-{
-    delete m_d;
-}
-
-sv_frame_t 
-Resampler::resample(float **in, float **out,
-                    sv_frame_t incount, double ratio,
-                    bool final)
-{
-    return m_d->resample(in, out, incount, ratio, final);
-}
-
-sv_frame_t 
-Resampler::resampleInterleaved(float *in, float *out,
-                               sv_frame_t incount, double ratio,
-                               bool final)
-{
-    return m_d->resampleInterleaved(in, out, incount, ratio, final);
-}
-
-void
-Resampler::reset()
-{
-    m_d->reset();
-}
-
--- a/base/Resampler.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-/* -*- 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.
-*/
-
-/*
-   This is a modified version of a source file from the 
-   Rubber Band audio timestretcher library.
-   This file copyright 2007 Chris Cannam.
-*/
-
-#ifndef _RESAMPLER_H_
-#define _RESAMPLER_H_
-
-#include "BaseTypes.h"
-
-#include <sys/types.h>
-
-class Resampler
-{
-public:
-    enum Quality { Best, FastestTolerable, Fastest };
-
-    Resampler(Quality quality, int channels, sv_frame_t chunkSize = 0);
-    ~Resampler();
-
-    sv_frame_t resample(float **in, float **out,
-                        sv_frame_t incount, double ratio,
-                        bool final = false);
-
-    sv_frame_t resampleInterleaved(float *in, float *out,
-                                   sv_frame_t incount, double ratio,
-                                   bool final = false);
-
-    void reset();
-
-protected:
-    class D;
-    D *m_d;
-};
-
-#endif
--- a/base/ResizeableBitset.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,107 +0,0 @@
-/* -*- 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 file copyright 2006 Chris Cannam.
-    
-    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 _RESIZEABLE_BITMAP_H_
-#define _RESIZEABLE_BITMAP_H_
-
-#include <vector>
-#include <stdint.h>
-#include <stddef.h>
-#include <stdlib.h>
-
-class ResizeableBitset {
-
-public:
-    ResizeableBitset() : m_bits(0), m_size(0) {
-    }
-    ResizeableBitset(size_t size) : m_bits(new std::vector<uint8_t>), m_size(size) {
-        m_bits->assign((size >> 3) + 1, 0);
-    }
-    ResizeableBitset(const ResizeableBitset &b) {
-        m_bits = new std::vector<uint8_t>(*b.m_bits);
-    }
-    ResizeableBitset &operator=(const ResizeableBitset &b) {
-        if (&b != this) return *this;
-        delete m_bits;
-        m_bits = new std::vector<uint8_t>(*b.m_bits);
-        return *this;
-    }
-    ~ResizeableBitset() {
-        delete m_bits;
-    }
-    
-    void resize(size_t size) { // retaining existing data; not thread safe
-        size_t bytes = (size >> 3) + 1;
-        if (m_bits && bytes == m_bits->size()) return;
-        std::vector<uint8_t> *newbits = new std::vector<uint8_t>(bytes);
-        newbits->assign(bytes, 0);
-        if (m_bits) {
-            for (size_t i = 0; i < bytes && i < m_bits->size(); ++i) {
-                (*newbits)[i] = (*m_bits)[i];
-            }
-            delete m_bits;
-        }
-        m_bits = newbits;
-        m_size = size;
-    }
-    
-    bool get(size_t column) const {
-        return ((*m_bits)[column >> 3]) & (1u << (column & 0x07));
-    }
-    
-    void set(size_t column) {
-        size_t ix = (column >> 3);
-        uint8_t prior = (*m_bits)[ix];
-        uint8_t extra = ((1u << (column & 0x07)) & 0xff);
-        (*m_bits)[ix] = uint8_t(prior | extra);
-    }
-
-    void reset(size_t column) {
-        ((*m_bits)[column >> 3]) &= uint8_t((~(1u << (column & 0x07))) & 0xff);
-    }
-
-    void copy(size_t source, size_t dest) {
-        get(source) ? set(dest) : reset(dest);
-    }
-
-    bool isAllOff() const {
-        for (size_t i = 0; i < m_bits->size(); ++i) {
-            if ((*m_bits)[i]) return false;
-        }
-        return true;
-    }
-
-    bool isAllOn() const {
-        for (size_t i = 0; i + 1 < m_bits->size(); ++i) {
-            if ((*m_bits)[i] != 0xff) return false;
-        }
-        for (size_t i = (m_size / 8) * 8; i < m_size; ++i) {
-            if (!get(i)) return false;
-        }
-        return true;
-    }
-
-    size_t size() const {
-        return m_size;
-    }
-    
-private:
-    std::vector<uint8_t> *m_bits;
-    size_t m_size;
-};
-
-
-#endif
-
--- a/base/ResourceFinder.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/ResourceFinder.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -33,6 +33,7 @@
 
 #include <cstdlib>
 #include <iostream>
+#include <stdexcept>
 
 /**
    Resource files may be found in three places:
@@ -126,6 +127,11 @@
 static QString
 getNewStyleUserResourcePrefix()
 {
+    if (qApp->applicationName() == "") {
+        cerr << "ERROR: Can't use ResourceFinder before setting application name" << endl;
+        throw std::logic_error("Can't use ResourceFinder before setting application name");
+    }
+
 #if QT_VERSION >= 0x050000
     // This is expected to be much more reliable than
     // getOldStyleUserResourcePrefix(), but it returns a different
@@ -133,7 +139,7 @@
     // fair enough). Hence migrateOldStyleResources() which moves
     // across any resources found in the old-style path the first time
     // we look for the new-style one
-    return QStandardPaths::writableLocation(QStandardPaths::DataLocation);
+    return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
 #else
     return getOldStyleUserResourcePrefix();
 #endif
--- a/base/Scavenger.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/Scavenger.h	Fri Jan 13 10:29:44 2017 +0000
@@ -26,7 +26,6 @@
 
 #include <vector>
 #include <list>
-#include <sys/time.h>
 #include <QMutex>
 #include <iostream>
 
--- a/base/StorageAdviser.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/StorageAdviser.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -22,7 +22,40 @@
 
 #include <iostream>
 
-//#define DEBUG_STORAGE_ADVISER 1
+QString
+StorageAdviser::criteriaToString(int criteria)
+{
+    QStringList labels;
+    if (criteria & SpeedCritical) labels.push_back("SpeedCritical");
+    if (criteria & PrecisionCritical) labels.push_back("PrecisionCritical");
+    if (criteria & LongRetentionLikely) labels.push_back("LongRetentionLikely");
+    if (criteria & FrequentLookupLikely) labels.push_back("FrequentLookupLikely");
+    if (labels.empty()) return "None";
+    else return labels.join("+");
+}
+
+QString
+StorageAdviser::recommendationToString(int recommendation)
+{
+    QStringList labels;
+    if (recommendation & UseMemory) labels.push_back("UseMemory");
+    if (recommendation & PreferMemory) labels.push_back("PreferMemory");
+    if (recommendation & PreferDisc) labels.push_back("PreferDisc");
+    if (recommendation & UseDisc) labels.push_back("UseDisc");
+    if (recommendation & ConserveSpace) labels.push_back("ConserveSpace");
+    if (recommendation & UseAsMuchAsYouLike) labels.push_back("UseAsMuchAsYouLike");
+    if (labels.empty()) return "None";
+    else return labels.join("+");
+}
+
+QString
+StorageAdviser::storageStatusToString(StorageStatus status)
+{
+    if (status == Insufficient) return "Insufficient";
+    if (status == Marginal) return "Marginal";
+    if (status == Sufficient) return "Sufficient";
+    return "Unknown";
+}
 
 size_t StorageAdviser::m_discPlanned = 0;
 size_t StorageAdviser::m_memoryPlanned = 0;
@@ -35,13 +68,15 @@
 			  size_t minimumSize,
 			  size_t maximumSize)
 {
-#ifdef DEBUG_STORAGE_ADVISER
-    SVDEBUG << "StorageAdviser::recommend: Criteria " << criteria 
-              << ", minimumSize " << minimumSize
-              << ", maximumSize " << maximumSize << endl;
-#endif
+    SVDEBUG << "StorageAdviser::recommend: criteria " << criteria
+            << " (" + criteriaToString(criteria) + ")"
+            << ", minimumSize " << minimumSize
+            << ", maximumSize " << maximumSize << endl;
 
     if (m_baseRecommendation != NoRecommendation) {
+        SVDEBUG << "StorageAdviser::recommend: Returning fixed recommendation "
+                << m_baseRecommendation << " ("
+                << recommendationToString(m_baseRecommendation) << ")" << endl;
         return m_baseRecommendation; // for now
     }
 
@@ -49,13 +84,24 @@
     try {
         path = TempDirectory::getInstance()->getPath();
     } catch (std::exception e) {
-        cerr << "StorageAdviser::recommend: ERROR: Failed to get temporary directory path: " << e.what() << endl;
-        return Recommendation(UseMemory | ConserveSpace);
+        SVDEBUG << "StorageAdviser::recommend: ERROR: Failed to get temporary directory path: " << e.what() << endl;
+        int r = UseMemory | ConserveSpace;
+        SVDEBUG << "StorageAdviser: returning fallback " << r
+                << " (" << recommendationToString(r) << ")" << endl;
+        return Recommendation(r);
     }
     ssize_t discFree = GetDiscSpaceMBAvailable(path.toLocal8Bit());
     ssize_t memoryFree, memoryTotal;
     GetRealMemoryMBAvailable(memoryFree, memoryTotal);
 
+    SVDEBUG << "StorageAdviser: disc space: " << discFree
+            << "M, memory free: " << memoryFree
+            << "M, memory total: " << memoryTotal << "M" << endl;
+    SVDEBUG << "StorageAdviser: disc planned: " << (m_discPlanned / 1024)
+            << "K, memory planned: " << (m_memoryPlanned / 1024) << "K" << endl;
+    SVDEBUG << "StorageAdviser: min requested: " << minimumSize
+            << "K, max requested: " << maximumSize << "K" << endl;
+
     if (discFree > ssize_t(m_discPlanned / 1024 + 1)) {
         discFree -= m_discPlanned / 1024 + 1;
     } else if (discFree > 0) { // can also be -1 for unknown
@@ -68,22 +114,11 @@
         memoryFree = 0;
     }
 
-#ifdef DEBUG_STORAGE_ADVISER
-    cerr << "Disc space: " << discFree << ", memory free: " << memoryFree << ", memory total: " << memoryTotal << ", min " << minimumSize << ", max " << maximumSize << endl;
-#endif
-
     //!!! We have a potentially serious problem here if multiple
     //recommendations are made in advance of any of the resulting
     //allocations, as the allocations that have been recommended for
     //won't be taken into account in subsequent recommendations.
 
-    enum StorageStatus {
-        Unknown,
-        Insufficient,
-        Marginal,
-        Sufficient
-    };
-
     StorageStatus memoryStatus = Unknown;
     StorageStatus discStatus = Unknown;
 
@@ -91,7 +126,7 @@
     ssize_t maxmb = ssize_t(maximumSize / 1024 + 1);
 
     if (memoryFree == -1) memoryStatus = Unknown;
-    else if (memoryFree < memoryTotal / 3) memoryStatus = Insufficient;
+    else if (memoryFree < memoryTotal / 3 && memoryFree < 512) memoryStatus = Insufficient;
     else if (minmb > (memoryFree * 3) / 4) memoryStatus = Insufficient;
     else if (maxmb > (memoryFree * 3) / 4) memoryStatus = Marginal;
     else if (minmb > (memoryFree / 3)) memoryStatus = Marginal;
@@ -105,10 +140,10 @@
     else if (minmb > (discFree / 10)) discStatus = Marginal;
     else discStatus = Sufficient;
 
-#ifdef DEBUG_STORAGE_ADVISER
-    cerr << "Memory status: " << memoryStatus << ", disc status "
-              << discStatus << endl;
-#endif
+    SVDEBUG << "StorageAdviser: memory status: " << memoryStatus
+            << " (" << storageStatusToString(memoryStatus) << ")"
+            << ", disc status " << discStatus
+            << " (" << storageStatusToString(discStatus) << ")" << endl;
 
     int recommendation = NoRecommendation;
 
@@ -181,6 +216,9 @@
         }
     }
 
+    SVDEBUG << "StorageAdviser: returning recommendation " << recommendation
+            << " (" << recommendationToString(recommendation) << ")" << endl;
+    
     return Recommendation(recommendation);
 }
 
@@ -189,8 +227,8 @@
 {
     if (area == MemoryAllocation) m_memoryPlanned += size;
     else if (area == DiscAllocation) m_discPlanned += size;
-//    cerr << "storage planned up: memory: " << m_memoryPlanned << ", disc "
-//              << m_discPlanned << endl;
+    SVDEBUG << "StorageAdviser: storage planned up: now memory: " << m_memoryPlanned << ", disc "
+            << m_discPlanned << endl;
 }
 
 void
@@ -203,8 +241,8 @@
         if (m_discPlanned > size) m_discPlanned -= size; 
         else m_discPlanned = 0;
     }
-//    cerr << "storage planned down: memory: " << m_memoryPlanned << ", disc "
-//              << m_discPlanned << endl;
+    SVDEBUG << "StorageAdviser: storage planned down: now memory: " << m_memoryPlanned << ", disc "
+            << m_discPlanned << endl;
 }
 
 void
--- a/base/StorageAdviser.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/StorageAdviser.h	Fri Jan 13 10:29:44 2017 +0000
@@ -14,11 +14,13 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _STORAGE_ADVISER_H_
-#define _STORAGE_ADVISER_H_
+#ifndef SV_STORAGE_ADVISER_H
+#define SV_STORAGE_ADVISER_H
 
 #include <cstdlib>
 
+#include <QString>
+
 /**
  * A utility class designed to help decide whether to store cache data
  * (for example FFT outputs) in memory or on disk in the TempDirectory.
@@ -91,6 +93,17 @@
     static size_t m_discPlanned;
     static size_t m_memoryPlanned;
     static Recommendation m_baseRecommendation;
+
+    enum StorageStatus {
+        Unknown,
+        Insufficient,
+        Marginal,
+        Sufficient
+    };
+
+    static QString criteriaToString(int);
+    static QString recommendationToString(int);
+    static QString storageStatusToString(StorageStatus);
 };
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/Strings.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,28 @@
+/* -*- 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 "Strings.h"
+
+QString
+Strings::pi = QChar(0x3c0);
+
+QString
+Strings::minus_pi = "-" + pi;
+
+QString
+Strings::infinity = QChar(0x221e);
+
+QString
+Strings::minus_infinity = "-" + infinity;
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/Strings.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,30 @@
+/* -*- 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_STRINGS_H
+#define SV_STRINGS_H
+
+#include <QString>
+
+class Strings
+{
+public:
+    static QString pi;
+    static QString minus_pi;
+
+    static QString infinity;
+    static QString minus_infinity;
+};
+
+#endif
--- a/base/TempDirectory.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/TempDirectory.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -25,7 +25,7 @@
 
 #include <iostream>
 #include <cassert>
-#include <unistd.h>
+
 #include <time.h>
 
 TempDirectory *
--- a/base/TempWriteFile.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/TempWriteFile.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -27,7 +27,7 @@
     temp.setAutoRemove(false);
     temp.open(); // creates the file and opens it atomically
     if (temp.error()) {
-	cerr << "TempWriteFile: Failed to create temporary file in directory of " << m_target << ": " << temp.errorString() << endl;
+	SVCERR << "TempWriteFile: Failed to create temporary file in directory of " << m_target << ": " << temp.errorString() << endl;
 	throw FileOperationFailed(temp.fileName(), "creation");
     }
     
@@ -54,13 +54,17 @@
 {
     if (m_temp == "") return;
 
-    QDir dir(QFileInfo(m_temp).dir());
-    // According to  http://doc.trolltech.com/4.4/qdir.html#rename
-    // some systems fail, if renaming over an existing file.
-    // Therefore, delete first the existing file.
-    if (dir.exists(m_target)) dir.remove(m_target);
-    if (!dir.rename(m_temp, m_target)) {
-	cerr << "TempWriteFile: Failed to rename temporary file " << m_temp << " to target " << m_target << endl;
+    QFile tempFile(m_temp);
+    QFile targetFile(m_target);
+    
+    if (targetFile.exists()) {
+        if (!targetFile.remove()) {
+            SVCERR << "TempWriteFile: WARNING: Failed to remove existing target file " << m_target << " prior to moving temporary file " << m_temp << " to it" << endl;
+        }
+    }
+    
+    if (!tempFile.rename(m_target)) {
+        SVCERR << "TempWriteFile: Failed to rename temporary file " << m_temp << " to target " << m_target << endl;
 	throw FileOperationFailed(m_temp, "rename");
     }
 
--- a/base/TempWriteFile.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/TempWriteFile.h	Fri Jan 13 10:29:44 2017 +0000
@@ -12,8 +12,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _TEMP_WRITE_FILE_H_
-#define _TEMP_WRITE_FILE_H_
+#ifndef SV_TEMP_WRITE_FILE_H
+#define SV_TEMP_WRITE_FILE_H
 
 #include <QTemporaryFile>
 
@@ -23,7 +23,6 @@
  * use when saving a file over an existing one, to avoid clobbering
  * the original before the save is complete.
  */
-
 class TempWriteFile
 {
 public:
--- a/base/ViewManagerBase.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/ViewManagerBase.h	Fri Jan 13 10:29:44 2017 +0000
@@ -21,6 +21,7 @@
 #include "Selection.h"
 
 class AudioPlaySource;
+class AudioRecordTarget;
 
 /**
  * Base class for ViewManager, with no GUI content.  This should
@@ -36,6 +37,7 @@
     virtual ~ViewManagerBase();
 
     virtual void setAudioPlaySource(AudioPlaySource *source) = 0;
+    virtual void setAudioRecordTarget(AudioRecordTarget *target) = 0;
 
     virtual sv_frame_t alignPlaybackFrameToReference(sv_frame_t) const = 0;
     virtual sv_frame_t alignReferenceToPlaybackFrame(sv_frame_t) const = 0;
--- a/base/Window.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/Window.h	Fri Jan 13 10:29:44 2017 +0000
@@ -22,6 +22,9 @@
 #include <map>
 #include <cstdlib>
 
+#include <bqvec/VectorOps.h>
+#include <bqvec/Allocators.h>
+
 #include "system/System.h"
 
 enum WindowType {
@@ -47,8 +50,12 @@
      * than symmetrical. (A window of size N is equivalent to a
      * symmetrical window of size N+1 with the final element missing.)
      */
-    Window(WindowType type, int size) : m_type(type), m_size(size) { encache(); }
-    Window(const Window &w) : m_type(w.m_type), m_size(w.m_size) { encache(); }
+    Window(WindowType type, int size) : m_type(type), m_size(size), m_cache(0) {
+        encache();
+    }
+    Window(const Window &w) : m_type(w.m_type), m_size(w.m_size), m_cache(0) {
+        encache();
+    }
     Window &operator=(const Window &w) {
 	if (&w == this) return *this;
 	m_type = w.m_type;
@@ -56,11 +63,16 @@
 	encache();
 	return *this;
     }
-    virtual ~Window() { delete[] m_cache; }
+    virtual ~Window() {
+        breakfastquay::deallocate(m_cache);
+    }
     
-    void cut(T *src) const { cut(src, src); }
-    void cut(T *src, T *dst) const {
-	for (int i = 0; i < m_size; ++i) dst[i] = src[i] * m_cache[i];
+    inline void cut(T *const BQ_R__ block) const {
+        breakfastquay::v_multiply(block, m_cache, m_size);
+    }
+
+    inline void cut(const T *const BQ_R__ src, T *const BQ_R__ dst) const {
+        breakfastquay::v_multiply(dst, src, m_cache, m_size);
     }
 
     T getArea() { return m_area; }
@@ -78,7 +90,7 @@
 protected:
     WindowType m_type;
     int m_size;
-    T *m_cache;
+    T *BQ_R__ m_cache;
     T m_area;
     
     void encache();
@@ -88,41 +100,42 @@
 template <typename T>
 void Window<T>::encache()
 {
+    if (!m_cache) m_cache = breakfastquay::allocate<T>(m_size);
+
     const int n = m_size;
-    T *mult = new T[n];
+    breakfastquay::v_set(m_cache, T(1.0), n);
     int i;
-    for (i = 0; i < n; ++i) mult[i] = 1.0;
 
     switch (m_type) {
 		
     case RectangularWindow:
 	for (i = 0; i < n; ++i) {
-	    mult[i] *= T(0.5);
+	    m_cache[i] *= T(0.5);
 	}
 	break;
 	    
     case BartlettWindow:
 	for (i = 0; i < n/2; ++i) {
-	    mult[i] *= T(i) / T(n/2);
-	    mult[i + n/2] *= T(1.0) - T(i) / T(n/2);
+	    m_cache[i] *= T(i) / T(n/2);
+	    m_cache[i + n/2] *= T(1.0) - T(i) / T(n/2);
 	}
 	break;
 	    
     case HammingWindow:
-        cosinewin(mult, 0.54, 0.46, 0.0, 0.0);
+        cosinewin(m_cache, 0.54, 0.46, 0.0, 0.0);
 	break;
 	    
     case HanningWindow:
-        cosinewin(mult, 0.50, 0.50, 0.0, 0.0);
+        cosinewin(m_cache, 0.50, 0.50, 0.0, 0.0);
 	break;
 	    
     case BlackmanWindow:
-        cosinewin(mult, 0.42, 0.50, 0.08, 0.0);
+        cosinewin(m_cache, 0.42, 0.50, 0.08, 0.0);
 	break;
 	    
     case GaussianWindow:
 	for (i = 0; i < n; ++i) {
-            mult[i] *= T(pow(2, - pow((i - (n-1)/2.0) / ((n-1)/2.0 / 3), 2)));
+            m_cache[i] *= T(pow(2, - pow((i - (n-1)/2.0) / ((n-1)/2.0 / 3), 2)));
 	}
 	break;
 	    
@@ -131,29 +144,27 @@
         int N = n-1;
         for (i = 0; i < N/4; ++i) {
             T m = T(2 * pow(1.0 - (T(N)/2 - T(i)) / (T(N)/2), 3));
-            mult[i] *= m;
-            mult[N-i] *= m;
+            m_cache[i] *= m;
+            m_cache[N-i] *= m;
         }
         for (i = N/4; i <= N/2; ++i) {
             int wn = i - N/2;
             T m = T(1.0 - 6 * pow(T(wn) / (T(N)/2), 2) * (1.0 - T(abs(wn)) / (T(N)/2)));
-            mult[i] *= m;
-            mult[N-i] *= m;
+            m_cache[i] *= m;
+            m_cache[N-i] *= m;
         }            
         break;
     }
 
     case NuttallWindow:
-        cosinewin(mult, 0.3635819, 0.4891775, 0.1365995, 0.0106411);
+        cosinewin(m_cache, 0.3635819, 0.4891775, 0.1365995, 0.0106411);
 	break;
 
     case BlackmanHarrisWindow:
-        cosinewin(mult, 0.35875, 0.48829, 0.14128, 0.01168);
+        cosinewin(m_cache, 0.35875, 0.48829, 0.14128, 0.01168);
         break;
     }
 	
-    m_cache = mult;
-
     m_area = 0;
     for (int i = 0; i < n; ++i) {
         m_area += m_cache[i];
--- a/base/ZoomConstraint.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/ZoomConstraint.h	Fri Jan 13 10:29:44 2017 +0000
@@ -58,8 +58,11 @@
 
     /**
      * 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 262144; }
+    virtual int getMaxZoomLevel() const { return 4194304; } // 2^22, arbitrarily
 };
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/TestColumnOp.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,276 @@
+/* -*- 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_COLUMN_OP_H
+#define TEST_COLUMN_OP_H
+
+#include "../ColumnOp.h"
+
+#include <QObject>
+#include <QtTest>
+#include <QDir>
+
+#include <iostream>
+
+//#define REPORT 1
+
+using namespace std;
+
+class TestColumnOp : public QObject
+{
+    Q_OBJECT
+
+    typedef ColumnOp C;
+    typedef ColumnOp::Column Column;
+    typedef vector<double> BinMapping;
+
+#ifdef REPORT
+    template <typename T>
+    void report(vector<T> v) {
+        cerr << "Vector is: [ ";
+        for (int i = 0; i < int(v.size()); ++i) {
+            if (i > 0) cerr << ", ";
+            cerr << v[i];
+        }
+        cerr << " ]\n";
+    }
+#else
+    template <typename T>
+    void report(vector<T> ) { }
+#endif
+                                     
+private slots:
+    void applyGain() {
+        QCOMPARE(C::applyGain({}, 1.0), Column());
+        Column c { 1, 2, 3, -4, 5, 6 };
+        Column actual(C::applyGain(c, 1.5));
+        Column expected { 1.5, 3, 4.5, -6, 7.5, 9 };
+        QCOMPARE(actual, expected);
+        actual = C::applyGain(c, 1.0);
+        QCOMPARE(actual, c);
+        actual = C::applyGain(c, 0.0);
+        expected = { 0, 0, 0, 0, 0, 0 };
+        QCOMPARE(actual, expected);
+    }
+
+    void fftScale() {
+        QCOMPARE(C::fftScale({}, 2.0), Column());
+        Column c { 1, 2, 3, -4, 5 };
+        Column actual(C::fftScale(c, 8));
+        Column expected { 0.25, 0.5, 0.75, -1, 1.25 };
+        QCOMPARE(actual, expected);
+    }
+
+    void isPeak_null() {
+        QVERIFY(!C::isPeak({}, 0));
+        QVERIFY(!C::isPeak({}, 1));
+        QVERIFY(!C::isPeak({}, -1));
+    }
+
+    void isPeak_obvious() {
+        Column c { 0.4, 0.5, 0.3 };
+        QVERIFY(!C::isPeak(c, 0));
+        QVERIFY(C::isPeak(c, 1));
+        QVERIFY(!C::isPeak(c, 2));
+    }
+
+    void isPeak_edges() {
+        Column c { 0.5, 0.4, 0.3 };
+        QVERIFY(C::isPeak(c, 0));
+        QVERIFY(!C::isPeak(c, 1));
+        QVERIFY(!C::isPeak(c, 2));
+        QVERIFY(!C::isPeak(c, 3));
+        QVERIFY(!C::isPeak(c, -1));
+        c = { 1.4, 1.5 };
+        QVERIFY(!C::isPeak(c, 0));
+        QVERIFY(C::isPeak(c, 1));
+    }
+
+    void isPeak_flat() {
+        Column c { 0.0, 0.0, 0.0 };
+        QVERIFY(C::isPeak(c, 0));
+        QVERIFY(!C::isPeak(c, 1));
+        QVERIFY(!C::isPeak(c, 2));
+    }
+
+    void isPeak_mixedSign() {
+        Column c { 0.4, -0.5, -0.3, -0.6, 0.1, -0.3 };
+        QVERIFY(C::isPeak(c, 0));
+        QVERIFY(!C::isPeak(c, 1));
+        QVERIFY(C::isPeak(c, 2));
+        QVERIFY(!C::isPeak(c, 3));
+        QVERIFY(C::isPeak(c, 4));
+        QVERIFY(!C::isPeak(c, 5));
+    }
+
+    void isPeak_duplicate() {
+        Column c({ 0.5, 0.5, 0.4, 0.4 });
+        QVERIFY(C::isPeak(c, 0));
+        QVERIFY(!C::isPeak(c, 1));
+        QVERIFY(!C::isPeak(c, 2));
+        QVERIFY(!C::isPeak(c, 3));
+        c = { 0.4, 0.4, 0.5, 0.5 };
+        QVERIFY(C::isPeak(c, 0)); // counterintuitive but necessary
+        QVERIFY(!C::isPeak(c, 1));
+        QVERIFY(C::isPeak(c, 2));
+        QVERIFY(!C::isPeak(c, 3));
+    }
+
+    void peakPick() {
+        QCOMPARE(C::peakPick({}), Column());
+        Column c({ 0.5, 0.5, 0.4, 0.4 });
+        QCOMPARE(C::peakPick(c), Column({ 0.5, 0.0, 0.0, 0.0 }));
+        c = Column({ 0.4, -0.5, -0.3, -0.6, 0.1, -0.3 });
+        QCOMPARE(C::peakPick(c), Column({ 0.4, 0.0, -0.3, 0.0, 0.1, 0.0 }));
+    }
+
+    void normalize_null() {
+        QCOMPARE(C::normalize({}, ColumnNormalization::None), Column());
+        QCOMPARE(C::normalize({}, ColumnNormalization::Sum1), Column());
+        QCOMPARE(C::normalize({}, ColumnNormalization::Max1), Column());
+        QCOMPARE(C::normalize({}, ColumnNormalization::Hybrid), Column());
+    }
+
+    void normalize_none() {
+        Column c { 1, 2, 3, 4 };
+        QCOMPARE(C::normalize(c, ColumnNormalization::None), c);
+    }
+
+    void normalize_none_mixedSign() {
+        Column c { 1, 2, -3, -4 };
+        QCOMPARE(C::normalize(c, ColumnNormalization::None), c);
+    }
+
+    void normalize_sum1() {
+        Column c { 1, 2, 4, 3 };
+        QCOMPARE(C::normalize(c, ColumnNormalization::Sum1),
+                 Column({ 0.1, 0.2, 0.4, 0.3 }));
+    }
+
+    void normalize_sum1_mixedSign() {
+        Column c { 1, 2, -4, -3 };
+        QCOMPARE(C::normalize(c, ColumnNormalization::Sum1),
+                 Column({ 0.1, 0.2, -0.4, -0.3 }));
+    }
+
+    void normalize_max1() {
+        Column c { 4, 3, 2, 1 };
+        QCOMPARE(C::normalize(c, ColumnNormalization::Max1),
+                 Column({ 1.0, 0.75, 0.5, 0.25 }));
+    }
+
+    void normalize_max1_mixedSign() {
+        Column c { -4, -3, 2, 1 };
+        QCOMPARE(C::normalize(c, ColumnNormalization::Max1),
+                 Column({ -1.0, -0.75, 0.5, 0.25 }));
+    }
+
+    void normalize_hybrid() {
+        // with max == 99, log10(max+1) == 2 so scale factor will be 2/99
+        Column c { 22, 44, 99, 66 };
+        QCOMPARE(C::normalize(c, ColumnNormalization::Hybrid),
+                 Column({ 44.0/99.0, 88.0/99.0, 2.0, 132.0/99.0 }));
+    }
+
+    void normalize_hybrid_mixedSign() {
+        // with max == 99, log10(max+1) == 2 so scale factor will be 2/99
+        Column c { 22, 44, -99, -66 };
+        QCOMPARE(C::normalize(c, ColumnNormalization::Hybrid),
+                 Column({ 44.0/99.0, 88.0/99.0, -2.0, -132.0/99.0 }));
+    }
+    
+    void distribute_simple() {
+        Column in { 1, 2, 3 };
+        BinMapping binfory { 0.0, 0.5, 1.0, 1.5, 2.0, 2.5 };
+        Column expected { 1, 1, 2, 2, 3, 3 };
+        Column actual(C::distribute(in, 6, binfory, 0, false));
+        report(actual);
+        QCOMPARE(actual, expected);
+    }
+    
+    void distribute_simple_interpolated() {
+        Column in { 1, 2, 3 };
+        BinMapping binfory { 0.0, 0.5, 1.0, 1.5, 2.0, 2.5 };
+        // There is a 0.5-bin offset from the distribution you might
+        // expect, because this corresponds visually to the way that
+        // bin values are duplicated upwards in simple_distribution.
+        // It means that switching between interpolated and
+        // non-interpolated views retains the visual position of each
+        // bin peak as somewhere in the middle of the scale area for
+        // that bin.
+        Column expected { 1, 1, 1.5, 2, 2.5, 3 };
+        Column actual(C::distribute(in, 6, binfory, 0, true));
+        report(actual);
+        QCOMPARE(actual, expected);
+    }
+    
+    void distribute_nonlinear() {
+        Column in { 1, 2, 3 };
+        BinMapping binfory { 0.0, 0.2, 0.5, 1.0, 2.0, 2.5 };
+        Column expected { 1, 1, 1, 2, 3, 3 };
+        Column actual(C::distribute(in, 6, binfory, 0, false));
+        report(actual);
+        QCOMPARE(actual, expected);
+    }
+    
+    void distribute_nonlinear_interpolated() {
+        // See distribute_simple_interpolated
+        Column in { 1, 2, 3 };
+        BinMapping binfory { 0.0, 0.2, 0.5, 1.0, 2.0, 2.5 };
+        Column expected { 1, 1, 1, 1.5, 2.5, 3 };
+        Column actual(C::distribute(in, 6, binfory, 0, true));
+        report(actual);
+        QCOMPARE(actual, expected);
+    }
+    
+    void distribute_shrinking() {
+        Column in { 4, 1, 2, 3, 5, 6 };
+        BinMapping binfory { 0.0, 2.0, 4.0 };
+        Column expected { 4, 3, 6 };
+        Column actual(C::distribute(in, 3, binfory, 0, false));
+        report(actual);
+        QCOMPARE(actual, expected);
+    }
+    
+    void distribute_shrinking_interpolated() {
+        // should be same as distribute_shrinking, we don't
+        // interpolate when resizing down
+        Column in { 4, 1, 2, 3, 5, 6 };
+        BinMapping binfory { 0.0, 2.0, 4.0 };
+        Column expected { 4, 3, 6 };
+        Column actual(C::distribute(in, 3, binfory, 0, true));
+        report(actual);
+        QCOMPARE(actual, expected);
+    }
+    
+    void distribute_nonlinear_someshrinking_interpolated() {
+        // But we *should* interpolate if the mapping involves
+        // shrinking some bins but expanding others.  See
+        // distribute_simple_interpolated for note on 0.5 offset
+        Column in { 4, 1, 2, 3, 5, 6 };
+        BinMapping binfory { 0.0, 3.0, 4.0, 4.5 };
+        Column expected { 4.0, 2.5, 4.0, 5.0 };
+        Column actual(C::distribute(in, 4, binfory, 0, true));
+        report(actual);
+        QCOMPARE(actual, expected);
+        binfory = BinMapping { 0.5, 1.0, 2.0, 5.0 };
+        expected = { 4.0, 2.5, 1.5, 5.5 };
+        actual = (C::distribute(in, 4, binfory, 0, true));
+        report(actual);
+        QCOMPARE(actual, expected);
+    }
+};
+    
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/TestOurRealTime.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,469 @@
+/* -*- 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_OUR_REALTIME_H
+#define TEST_OUR_REALTIME_H
+
+#include <QObject>
+#include <QtTest>
+#include <QDir>
+
+#include <iostream>
+
+#include "../RealTime.h"
+
+using namespace std;
+
+#define ONE_MILLION 1000000
+#define ONE_BILLION 1000000000
+
+class TestOurRealTime : public QObject
+{
+    Q_OBJECT
+
+    void compareTexts(string s, const char *e) {
+        QCOMPARE(QString(s.c_str()), QString(e));
+    }
+
+    typedef sv_frame_t frame_type;
+                                 
+private slots:
+
+    void zero()
+    {
+        QCOMPARE(RealTime(0, 0), RealTime::zeroTime);
+        QCOMPARE(RealTime(0, 0).sec, 0);
+        QCOMPARE(RealTime(0, 0).nsec, 0);
+        QCOMPARE(RealTime(0, 0).msec(), 0);
+        QCOMPARE(RealTime(0, 0).usec(), 0);
+    }
+
+    void ctor()
+    {
+        QCOMPARE(RealTime(0, 0), RealTime(0, 0));
+
+        // wraparounds
+        QCOMPARE(RealTime(0, ONE_BILLION/2), RealTime(1, -ONE_BILLION/2));
+        QCOMPARE(RealTime(0, -ONE_BILLION/2), RealTime(-1, ONE_BILLION/2));
+
+        QCOMPARE(RealTime(1, ONE_BILLION), RealTime(2, 0));
+        QCOMPARE(RealTime(1, -ONE_BILLION), RealTime(0, 0));
+        QCOMPARE(RealTime(-1, ONE_BILLION), RealTime(0, 0));
+        QCOMPARE(RealTime(-1, -ONE_BILLION), RealTime(-2, 0));
+
+        QCOMPARE(RealTime(1, -ONE_BILLION-ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime(-1, ONE_BILLION+ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
+            
+        QCOMPARE(RealTime(2, -ONE_BILLION*2), RealTime(0, 0));
+        QCOMPARE(RealTime(2, -ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
+
+        QCOMPARE(RealTime(-2, ONE_BILLION*2), RealTime(0, 0));
+        QCOMPARE(RealTime(-2, ONE_BILLION/2), RealTime(-1, -ONE_BILLION/2));
+        
+        QCOMPARE(RealTime(0, 1).sec, 0);
+        QCOMPARE(RealTime(0, 1).nsec, 1);
+        QCOMPARE(RealTime(0, -1).sec, 0);
+        QCOMPARE(RealTime(0, -1).nsec, -1);
+        QCOMPARE(RealTime(1, -1).sec, 0);
+        QCOMPARE(RealTime(1, -1).nsec, ONE_BILLION-1);
+        QCOMPARE(RealTime(-1, 1).sec, 0);
+        QCOMPARE(RealTime(-1, 1).nsec, -ONE_BILLION+1);
+        QCOMPARE(RealTime(-1, -1).sec, -1);
+        QCOMPARE(RealTime(-1, -1).nsec, -1);
+        
+        QCOMPARE(RealTime(2, -ONE_BILLION*2).sec, 0);
+        QCOMPARE(RealTime(2, -ONE_BILLION*2).nsec, 0);
+        QCOMPARE(RealTime(2, -ONE_BILLION/2).sec, 1);
+        QCOMPARE(RealTime(2, -ONE_BILLION/2).nsec, ONE_BILLION/2);
+
+        QCOMPARE(RealTime(-2, ONE_BILLION*2).sec, 0);
+        QCOMPARE(RealTime(-2, ONE_BILLION*2).nsec, 0);
+        QCOMPARE(RealTime(-2, ONE_BILLION/2).sec, -1);
+        QCOMPARE(RealTime(-2, ONE_BILLION/2).nsec, -ONE_BILLION/2);
+    }
+    
+    void fromSeconds()
+    {
+        QCOMPARE(RealTime::fromSeconds(0), RealTime(0, 0));
+
+        QCOMPARE(RealTime::fromSeconds(0.5).sec, 0);
+        QCOMPARE(RealTime::fromSeconds(0.5).nsec, ONE_BILLION/2);
+        QCOMPARE(RealTime::fromSeconds(0.5).usec(), ONE_MILLION/2);
+        QCOMPARE(RealTime::fromSeconds(0.5).msec(), 500);
+        
+        QCOMPARE(RealTime::fromSeconds(0.5), RealTime(0, ONE_BILLION/2));
+        QCOMPARE(RealTime::fromSeconds(1), RealTime(1, 0));
+        QCOMPARE(RealTime::fromSeconds(1.5), RealTime(1, ONE_BILLION/2));
+
+        QCOMPARE(RealTime::fromSeconds(-0.5).sec, 0);
+        QCOMPARE(RealTime::fromSeconds(-0.5).nsec, -ONE_BILLION/2);
+        QCOMPARE(RealTime::fromSeconds(-0.5).usec(), -ONE_MILLION/2);
+        QCOMPARE(RealTime::fromSeconds(-0.5).msec(), -500);
+        
+        QCOMPARE(RealTime::fromSeconds(-1.5).sec, -1);
+        QCOMPARE(RealTime::fromSeconds(-1.5).nsec, -ONE_BILLION/2);
+        QCOMPARE(RealTime::fromSeconds(-1.5).usec(), -ONE_MILLION/2);
+        QCOMPARE(RealTime::fromSeconds(-1.5).msec(), -500);
+        
+        QCOMPARE(RealTime::fromSeconds(-0.5), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime::fromSeconds(-1), RealTime(-1, 0));
+        QCOMPARE(RealTime::fromSeconds(-1.5), RealTime(-1, -ONE_BILLION/2));
+    }
+
+    void fromMilliseconds()
+    {
+        QCOMPARE(RealTime::fromMilliseconds(0), RealTime(0, 0));
+        QCOMPARE(RealTime::fromMilliseconds(500), RealTime(0, ONE_BILLION/2));
+        QCOMPARE(RealTime::fromMilliseconds(1000), RealTime(1, 0));
+        QCOMPARE(RealTime::fromMilliseconds(1500), RealTime(1, ONE_BILLION/2));
+
+        QCOMPARE(RealTime::fromMilliseconds(-0), RealTime(0, 0));
+        QCOMPARE(RealTime::fromMilliseconds(-500), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime::fromMilliseconds(-1000), RealTime(-1, 0));
+        QCOMPARE(RealTime::fromMilliseconds(-1500), RealTime(-1, -ONE_BILLION/2));
+    }
+    
+    void fromTimeval()
+    {
+        struct timeval tv;
+
+        tv.tv_sec = 0; tv.tv_usec = 0;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(0, 0));
+        tv.tv_sec = 0; tv.tv_usec = ONE_MILLION/2;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(0, ONE_BILLION/2));
+        tv.tv_sec = 1; tv.tv_usec = 0;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(1, 0));
+        tv.tv_sec = 1; tv.tv_usec = ONE_MILLION/2;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(1, ONE_BILLION/2));
+
+        tv.tv_sec = 0; tv.tv_usec = -ONE_MILLION/2;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(0, -ONE_BILLION/2));
+        tv.tv_sec = -1; tv.tv_usec = 0;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(-1, 0));
+        tv.tv_sec = -1; tv.tv_usec = -ONE_MILLION/2;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(-1, -ONE_BILLION/2));
+    }
+
+    void fromXsdDuration()
+    {
+        QCOMPARE(RealTime::fromXsdDuration("PT0"), RealTime::zeroTime);
+        QCOMPARE(RealTime::fromXsdDuration("PT0S"), RealTime::zeroTime);
+        QCOMPARE(RealTime::fromXsdDuration("PT10S"), RealTime(10, 0));
+        QCOMPARE(RealTime::fromXsdDuration("PT10.5S"), RealTime(10, ONE_BILLION/2));
+        QCOMPARE(RealTime::fromXsdDuration("PT1.5S").sec, 1);
+        QCOMPARE(RealTime::fromXsdDuration("PT1.5S").msec(), 500);
+        QCOMPARE(RealTime::fromXsdDuration("-PT1.5S").sec, -1);
+        QCOMPARE(RealTime::fromXsdDuration("-PT1.5S").msec(), -500);
+        QCOMPARE(RealTime::fromXsdDuration("PT1M30.5S"), RealTime(90, ONE_BILLION/2));
+        QCOMPARE(RealTime::fromXsdDuration("PT1H2M30.5S"), RealTime(3750, ONE_BILLION/2));
+    }
+
+    void toDouble()
+    {
+        QCOMPARE(RealTime(0, 0).toDouble(), 0.0);
+        QCOMPARE(RealTime(0, ONE_BILLION/2).toDouble(), 0.5);
+        QCOMPARE(RealTime(1, 0).toDouble(), 1.0);
+        QCOMPARE(RealTime(1, ONE_BILLION/2).toDouble(), 1.5);
+
+        QCOMPARE(RealTime(0, -ONE_BILLION/2).toDouble(), -0.5);
+        QCOMPARE(RealTime(-1, 0).toDouble(), -1.0);
+        QCOMPARE(RealTime(-1, -ONE_BILLION/2).toDouble(), -1.5);
+    }
+
+    void assign()
+    {
+        RealTime r;
+        r = RealTime(0, 0);
+        QCOMPARE(r, RealTime::zeroTime);
+        r = RealTime(0, ONE_BILLION/2);
+        QCOMPARE(r.sec, 0);
+        QCOMPARE(r.nsec, ONE_BILLION/2);
+        r = RealTime(-1, -ONE_BILLION/2);
+        QCOMPARE(r.sec, -1);
+        QCOMPARE(r.nsec, -ONE_BILLION/2);
+    }
+
+    void plus()
+    {
+        QCOMPARE(RealTime(0, 0) + RealTime(0, 0), RealTime(0, 0));
+
+        QCOMPARE(RealTime(0, 0) + RealTime(0, ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
+        QCOMPARE(RealTime(0, ONE_BILLION/2) + RealTime(0, ONE_BILLION/2), RealTime(1, 0));
+        QCOMPARE(RealTime(1, 0) + RealTime(0, ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
+
+        QCOMPARE(RealTime(0, 0) + RealTime(0, -ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2), RealTime(-1, 0));
+        QCOMPARE(RealTime(-1, 0) + RealTime(0, -ONE_BILLION/2), RealTime(-1, -ONE_BILLION/2));
+
+        QCOMPARE(RealTime(1, 0) + RealTime(0, -ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
+        QCOMPARE(RealTime(1, 0) + RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2), RealTime(0, 0));
+        QCOMPARE(RealTime(1, 0) + RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
+
+        QCOMPARE(RealTime(0, ONE_BILLION/2) + RealTime(-1, 0), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime(0, -ONE_BILLION/2) + RealTime(1, 0), RealTime(0, ONE_BILLION/2));
+    }
+    
+    void minus()
+    {
+        QCOMPARE(RealTime(0, 0) - RealTime(0, 0), RealTime(0, 0));
+
+        QCOMPARE(RealTime(0, 0) - RealTime(0, ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime(0, ONE_BILLION/2) - RealTime(0, ONE_BILLION/2), RealTime(0, 0));
+        QCOMPARE(RealTime(1, 0) - RealTime(0, ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
+
+        QCOMPARE(RealTime(0, 0) - RealTime(0, -ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
+        QCOMPARE(RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2), RealTime(0, 0));
+        QCOMPARE(RealTime(-1, 0) - RealTime(0, -ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
+
+        QCOMPARE(RealTime(1, 0) - RealTime(0, -ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
+        QCOMPARE(RealTime(1, 0) - RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2), RealTime(2, 0));
+        QCOMPARE(RealTime(1, 0) - RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2), RealTime(2, ONE_BILLION/2));
+
+        QCOMPARE(RealTime(0, ONE_BILLION/2) - RealTime(-1, 0), RealTime(1, ONE_BILLION/2));
+        QCOMPARE(RealTime(0, -ONE_BILLION/2) - RealTime(1, 0), RealTime(-1, -ONE_BILLION/2));
+    }
+
+    void negate()
+    {
+        QCOMPARE(-RealTime(0, 0), RealTime(0, 0));
+        QCOMPARE(-RealTime(1, 0), RealTime(-1, 0));
+        QCOMPARE(-RealTime(1, ONE_BILLION/2), RealTime(-1, -ONE_BILLION/2));
+        QCOMPARE(-RealTime(-1, -ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
+    }
+
+    void compare()
+    {
+        int sec, nsec;
+        for (sec = -2; sec <= 2; sec += 2) {
+            for (nsec = -1; nsec <= 1; nsec += 1) {
+                QCOMPARE(RealTime(sec, nsec) < RealTime(sec, nsec), false);
+                QCOMPARE(RealTime(sec, nsec) > RealTime(sec, nsec), false);
+                QCOMPARE(RealTime(sec, nsec) == RealTime(sec, nsec), true);
+                QCOMPARE(RealTime(sec, nsec) != RealTime(sec, nsec), false);
+                QCOMPARE(RealTime(sec, nsec) <= RealTime(sec, nsec), true);
+                QCOMPARE(RealTime(sec, nsec) >= RealTime(sec, nsec), true);
+            }
+        }
+        RealTime prev(-3, 0);
+        for (sec = -2; sec <= 2; sec += 2) {
+            for (nsec = -1; nsec <= 1; nsec += 1) {
+
+                RealTime curr(sec, nsec);
+
+                QCOMPARE(prev < curr, true);
+                QCOMPARE(prev > curr, false);
+                QCOMPARE(prev == curr, false);
+                QCOMPARE(prev != curr, true);
+                QCOMPARE(prev <= curr, true);
+                QCOMPARE(prev >= curr, false);
+
+                QCOMPARE(curr < prev, false);
+                QCOMPARE(curr > prev, true);
+                QCOMPARE(curr == prev, false);
+                QCOMPARE(curr != prev, true);
+                QCOMPARE(curr <= prev, false);
+                QCOMPARE(curr >= prev, true);
+
+                prev = curr;
+            }
+        }
+    }
+
+    void frame()
+    {
+        int frames[] = {
+            0, 1, 2047, 2048, 6656,
+            32767, 32768, 44100, 44101,
+            999999999, 2000000000
+        };
+        int n = sizeof(frames)/sizeof(frames[0]);
+
+        int rates[] = {
+            1, 2, 8000, 22050,
+            44100, 44101, 192000, 2000000001
+        };
+        int m = sizeof(rates)/sizeof(rates[0]);
+
+        vector<vector<RealTime>> realTimes = {
+            { { 0, 0 }, { 1, 0 }, { 2047, 0 }, { 2048, 0 },
+              { 6656, 0 }, { 32767, 0 }, { 32768, 0 }, { 44100, 0 },
+              { 44101, 0 }, { 999999999, 0 }, { 2000000000, 0 } },
+            { { 0, 0 }, { 0, 500000000 }, { 1023, 500000000 }, { 1024, 0 },
+              { 3328, 0 }, { 16383, 500000000 }, { 16384, 0 }, { 22050, 0 },
+              { 22050, 500000000 }, { 499999999, 500000000 }, { 1000000000, 0 } },
+            { { 0, 0 }, { 0, 125000 }, { 0, 255875000 }, { 0, 256000000 },
+              { 0, 832000000 }, { 4, 95875000 }, { 4, 96000000 }, { 5, 512500000 },
+              { 5, 512625000 }, { 124999, 999875000 }, { 250000, 0 } },
+            { { 0, 0 }, { 0, 45351 }, { 0, 92834467 }, { 0, 92879819 },
+              { 0, 301859410 }, { 1, 486031746 }, { 1, 486077098 }, { 2, 0 },
+              { 2, 45351 }, { 45351, 473877551 }, { 90702, 947845805 } },
+            { { 0, 0 }, { 0, 22676 }, { 0, 46417234 }, { 0, 46439909 },
+              { 0, 150929705 }, { 0, 743015873 }, { 0, 743038549 }, { 1, 0 },
+              { 1, 22676 }, { 22675, 736938776 }, { 45351, 473922902 } },
+            { { 0, 0 }, { 0, 22675 }, { 0, 46416181 }, { 0, 46438856 },
+              { 0, 150926283 }, { 0, 742999025 }, { 0, 743021700 }, { 0, 999977325 },
+              { 1, 0 }, { 22675, 222761389 }, { 45350, 445568128 } },
+            { { 0, 0 }, { 0, 5208 }, { 0, 10661458 }, { 0, 10666667 },
+              { 0, 34666667 }, { 0, 170661458 }, { 0, 170666667 }, { 0, 229687500 },
+              { 0, 229692708 }, { 5208, 333328125 }, { 10416, 666666667 } },
+            { { 0, 0 }, { 0, 0 }, { 0, 1023 }, { 0, 1024 },
+              { 0, 3328 }, { 0, 16383 }, { 0, 16384 }, { 0, 22050 },
+              { 0, 22050 }, { 0, 499999999 }, { 1, 0 } }
+        };
+        
+        for (int i = 0; i < n; ++i) {
+            frame_type frame = frames[i];
+            for (int j = 0; j < m; ++j) {
+                int rate = rates[j];
+
+                RealTime rt = RealTime::frame2RealTime(frame, rate);
+                QCOMPARE(rt.sec, realTimes[j][i].sec);
+                QCOMPARE(rt.nsec, realTimes[j][i].nsec);
+
+                frame_type conv = RealTime::realTime2Frame(rt, rate);
+                
+                rt = RealTime::frame2RealTime(-frame, rate);
+                frame_type negconv = RealTime::realTime2Frame(rt, rate);
+
+                if (rate > ONE_BILLION) {
+                    // We don't have enough precision in RealTime
+                    // for this absurd sample rate, so a round trip
+                    // conversion may round
+                    QVERIFY(abs(frame - conv) < 2);
+                    QVERIFY(abs(-frame - negconv) < 2);
+                } else {
+                    QCOMPARE(conv, frame);
+                    QCOMPARE(negconv, -frame);
+                }
+            }
+        }
+    }
+
+    // Our own RealTime has toMSText, toFrameText, toSecText
+    
+    void toText()
+    {
+        // we want to use QStrings, because then the Qt test library
+        // will print out any conflicts. The compareTexts function
+        // does this for us
+
+        int halfSec = ONE_BILLION/2; // nsec
+        
+        RealTime rt = RealTime(0, 0);
+        compareTexts(rt.toMSText(false, false), "0");
+        compareTexts(rt.toMSText(true, false), "0.000");
+        compareTexts(rt.toMSText(false, true), "0");
+        compareTexts(rt.toMSText(true, true), "0.000");
+        compareTexts(rt.toFrameText(24, false), "0:00");
+        compareTexts(rt.toFrameText(24, true), "0:00");
+        compareTexts(rt.toSecText(), "0s");
+
+        rt = RealTime(1, halfSec);
+        compareTexts(rt.toMSText(false, false), "1.5");
+        compareTexts(rt.toMSText(true, false), "1.500");
+        compareTexts(rt.toMSText(false, true), "1.5");
+        compareTexts(rt.toMSText(true, true), "1.500");
+        compareTexts(rt.toFrameText(24, false), "1:12");
+        compareTexts(rt.toFrameText(24, true), "1:12");
+        compareTexts(rt.toFrameText(25, false), "1:12");
+        compareTexts(rt.toFrameText(25, true), "1:12");
+        compareTexts(rt.toSecText(), "1s");
+
+        rt = RealTime::fromSeconds(-1.5);
+        compareTexts(rt.toMSText(false, false), "-1.5");
+        compareTexts(rt.toMSText(true, false), "-1.500");
+        compareTexts(rt.toMSText(false, true), "-1.5");
+        compareTexts(rt.toMSText(true, true), "-1.500");
+        compareTexts(rt.toFrameText(24, false), "-1:12");
+        compareTexts(rt.toFrameText(24, true), "-1:12");
+        compareTexts(rt.toSecText(), "-1s");
+
+        rt = RealTime(1, 1000);
+        compareTexts(rt.toMSText(false, false), "1");
+        compareTexts(rt.toFrameText(24, false), "1:00");
+        compareTexts(rt.toFrameText(ONE_MILLION, false), "1:000001");
+        compareTexts(rt.toSecText(), "1s");
+
+        rt = RealTime(1, 100000);
+        compareTexts(rt.toFrameText(ONE_MILLION, false), "1:000100");
+        compareTexts(rt.toSecText(), "1s");
+
+        rt = RealTime::fromSeconds(60);
+        compareTexts(rt.toMSText(false, false), "60");
+        compareTexts(rt.toMSText(true, false), "60.000");
+        compareTexts(rt.toMSText(false, true), "1:00");
+        compareTexts(rt.toMSText(true, true), "1:00.000");
+        compareTexts(rt.toFrameText(24, false), "60:00");
+        compareTexts(rt.toFrameText(24, true), "1:00:00");
+        compareTexts(rt.toSecText(), "1:00");
+
+        rt = RealTime::fromSeconds(61.05);
+        compareTexts(rt.toMSText(false, false), "61.05");
+        compareTexts(rt.toMSText(true, false), "61.050");
+        compareTexts(rt.toMSText(false, true), "1:01.05");
+        compareTexts(rt.toMSText(true, true), "1:01.050");
+        compareTexts(rt.toFrameText(24, false), "61:01");
+        compareTexts(rt.toFrameText(24, true), "1:01:01");
+        compareTexts(rt.toSecText(), "1:01");
+        
+        rt = RealTime::fromSeconds(601.05);
+        compareTexts(rt.toMSText(false, false), "601.05");
+        compareTexts(rt.toMSText(true, false), "601.050");
+        compareTexts(rt.toMSText(false, true), "10:01.05");
+        compareTexts(rt.toMSText(true, true), "10:01.050");
+        compareTexts(rt.toFrameText(24, false), "601:01");
+        compareTexts(rt.toFrameText(24, true), "10:01:01");
+        compareTexts(rt.toSecText(), "10:01");
+        
+        rt = RealTime::fromSeconds(3600);
+        compareTexts(rt.toMSText(false, false), "3600");
+        compareTexts(rt.toMSText(true, false), "3600.000");
+        compareTexts(rt.toMSText(false, true), "1:00:00");
+        compareTexts(rt.toMSText(true, true), "1:00:00.000");
+        compareTexts(rt.toFrameText(24, false), "3600:00");
+        compareTexts(rt.toFrameText(24, true), "1:00:00:00");
+        compareTexts(rt.toSecText(), "1:00:00");
+
+        // For practical reasons our time display always rounds down
+        rt = RealTime(3599, ONE_BILLION-1);
+        compareTexts(rt.toMSText(false, false), "3599.999");
+        compareTexts(rt.toMSText(true, false), "3599.999");
+        compareTexts(rt.toMSText(false, true), "59:59.999");
+        compareTexts(rt.toMSText(true, true), "59:59.999");
+        compareTexts(rt.toFrameText(24, false), "3599:23");
+        compareTexts(rt.toFrameText(24, true), "59:59:23");
+        compareTexts(rt.toSecText(), "59:59");
+
+        rt = RealTime::fromSeconds(3600 * 4 + 60 * 5 + 3 + 0.01);
+        compareTexts(rt.toMSText(false, false), "14703.01");
+        compareTexts(rt.toMSText(true, false), "14703.010");
+        compareTexts(rt.toMSText(false, true), "4:05:03.01");
+        compareTexts(rt.toMSText(true, true), "4:05:03.010");
+        compareTexts(rt.toFrameText(24, false), "14703:00");
+        compareTexts(rt.toFrameText(24, true), "4:05:03:00");
+        compareTexts(rt.toSecText(), "4:05:03");
+
+        rt = RealTime::fromSeconds(-(3600 * 4 + 60 * 5 + 3 + 0.01));
+        compareTexts(rt.toMSText(false, false), "-14703.01");
+        compareTexts(rt.toMSText(true, false), "-14703.010");
+        compareTexts(rt.toMSText(false, true), "-4:05:03.01");
+        compareTexts(rt.toMSText(true, true), "-4:05:03.010");
+        compareTexts(rt.toFrameText(24, false), "-14703:00");
+        compareTexts(rt.toFrameText(24, true), "-4:05:03:00");
+        compareTexts(rt.toSecText(), "-4:05:03");
+    }
+};
+
+#endif
+
--- a/base/test/TestRangeMapper.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/test/TestRangeMapper.h	Fri Jan 13 10:29:44 2017 +0000
@@ -77,7 +77,7 @@
 	QCOMPARE(rm.getValueForPosition(0), rm.getValueForPosition(1));
 	QCOMPARE(rm.getValueForPosition(9), rm.getValueForPosition(8));
 	QCOMPARE(rm.getValueForPositionUnclamped(6), 3.0);
-	QCOMPARE(rm.getValueForPositionUnclamped(0), 0.0);
+	QCOMPARE(rm.getValueForPositionUnclamped(0) + 1.0, 0.0 + 1.0);
 	QCOMPARE(rm.getValueForPositionUnclamped(-24), -12.0);
 	QCOMPARE(rm.getValueForPositionUnclamped(12), 6.0);
     }
@@ -93,7 +93,7 @@
 	QCOMPARE(rm.getValueForPosition(0), rm.getValueForPosition(1));
 	QCOMPARE(rm.getValueForPosition(9), rm.getValueForPosition(8));
 	QCOMPARE(rm.getValueForPositionUnclamped(3), 3.0);
-	QCOMPARE(rm.getValueForPositionUnclamped(9), 0.0);
+	QCOMPARE(rm.getValueForPositionUnclamped(9) + 1.0, 0.0 + 1.0);
 	QCOMPARE(rm.getValueForPositionUnclamped(33), -12.0);
 	QCOMPARE(rm.getValueForPositionUnclamped(-3), 6.0);
     }
--- a/base/test/TestRealTime.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,417 +0,0 @@
-/* -*- 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_REALTIME_H
-#define TEST_REALTIME_H
-
-#include "../RealTime.h"
-
-#include <QObject>
-#include <QtTest>
-#include <QDir>
-
-#include <iostream>
-
-using namespace std;
-
-class TestRealTime : public QObject
-{
-    Q_OBJECT
-
-    void compareTexts(string s, const char *e) {
-        QCOMPARE(QString(s.c_str()), QString(e));
-    }
-
-private slots:
-
-#define ONE_MILLION 1000000
-#define ONE_BILLION 1000000000
-    
-    void zero()
-    {
-	QCOMPARE(RealTime(0, 0), RealTime::zeroTime);
-	QCOMPARE(RealTime(0, 0).sec, 0);
-	QCOMPARE(RealTime(0, 0).nsec, 0);
-	QCOMPARE(RealTime(0, 0).msec(), 0);
-	QCOMPARE(RealTime(0, 0).usec(), 0);
-    }
-
-    void ctor()
-    {
-	QCOMPARE(RealTime(0, 0), RealTime(0, 0));
-
-	// wraparounds
-	QCOMPARE(RealTime(0, ONE_BILLION/2), RealTime(1, -ONE_BILLION/2));
-	QCOMPARE(RealTime(0, -ONE_BILLION/2), RealTime(-1, ONE_BILLION/2));
-
-	QCOMPARE(RealTime(1, ONE_BILLION), RealTime(2, 0));
-	QCOMPARE(RealTime(1, -ONE_BILLION), RealTime(0, 0));
-	QCOMPARE(RealTime(-1, ONE_BILLION), RealTime(0, 0));
-	QCOMPARE(RealTime(-1, -ONE_BILLION), RealTime(-2, 0));
-
-	QCOMPARE(RealTime(2, -ONE_BILLION*2), RealTime(0, 0));
-	QCOMPARE(RealTime(2, -ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
-
-	QCOMPARE(RealTime(-2, ONE_BILLION*2), RealTime(0, 0));
-	QCOMPARE(RealTime(-2, ONE_BILLION/2), RealTime(-1, -ONE_BILLION/2));
-	
-	QCOMPARE(RealTime(0, 1).sec, 0);
-	QCOMPARE(RealTime(0, 1).nsec, 1);
-	QCOMPARE(RealTime(0, -1).sec, 0);
-	QCOMPARE(RealTime(0, -1).nsec, -1);
-	QCOMPARE(RealTime(1, -1).sec, 0);
-	QCOMPARE(RealTime(1, -1).nsec, ONE_BILLION-1);
-	QCOMPARE(RealTime(-1, 1).sec, 0);
-	QCOMPARE(RealTime(-1, 1).nsec, -ONE_BILLION+1);
-	QCOMPARE(RealTime(-1, -1).sec, -1);
-	QCOMPARE(RealTime(-1, -1).nsec, -1);
-	
-	QCOMPARE(RealTime(2, -ONE_BILLION*2).sec, 0);
-	QCOMPARE(RealTime(2, -ONE_BILLION*2).nsec, 0);
-	QCOMPARE(RealTime(2, -ONE_BILLION/2).sec, 1);
-	QCOMPARE(RealTime(2, -ONE_BILLION/2).nsec, ONE_BILLION/2);
-
-	QCOMPARE(RealTime(-2, ONE_BILLION*2).sec, 0);
-	QCOMPARE(RealTime(-2, ONE_BILLION*2).nsec, 0);
-	QCOMPARE(RealTime(-2, ONE_BILLION/2).sec, -1);
-	QCOMPARE(RealTime(-2, ONE_BILLION/2).nsec, -ONE_BILLION/2);
-    }
-    
-    void fromSeconds()
-    {
-	QCOMPARE(RealTime::fromSeconds(0), RealTime(0, 0));
-
-	QCOMPARE(RealTime::fromSeconds(0.5).sec, 0);
-	QCOMPARE(RealTime::fromSeconds(0.5).nsec, ONE_BILLION/2);
-	QCOMPARE(RealTime::fromSeconds(0.5).usec(), ONE_MILLION/2);
-	QCOMPARE(RealTime::fromSeconds(0.5).msec(), 500);
-	
-	QCOMPARE(RealTime::fromSeconds(0.5), RealTime(0, ONE_BILLION/2));
-	QCOMPARE(RealTime::fromSeconds(1), RealTime(1, 0));
-	QCOMPARE(RealTime::fromSeconds(1.5), RealTime(1, ONE_BILLION/2));
-
-	QCOMPARE(RealTime::fromSeconds(-0.5).sec, 0);
-	QCOMPARE(RealTime::fromSeconds(-0.5).nsec, -ONE_BILLION/2);
-	QCOMPARE(RealTime::fromSeconds(-0.5).usec(), -ONE_MILLION/2);
-	QCOMPARE(RealTime::fromSeconds(-0.5).msec(), -500);
-	
-	QCOMPARE(RealTime::fromSeconds(-1.5).sec, -1);
-	QCOMPARE(RealTime::fromSeconds(-1.5).nsec, -ONE_BILLION/2);
-	QCOMPARE(RealTime::fromSeconds(-1.5).usec(), -ONE_MILLION/2);
-	QCOMPARE(RealTime::fromSeconds(-1.5).msec(), -500);
-	
-	QCOMPARE(RealTime::fromSeconds(-0.5), RealTime(0, -ONE_BILLION/2));
-	QCOMPARE(RealTime::fromSeconds(-1), RealTime(-1, 0));
-	QCOMPARE(RealTime::fromSeconds(-1.5), RealTime(-1, -ONE_BILLION/2));
-    }
-
-    void fromMilliseconds()
-    {
-	QCOMPARE(RealTime::fromMilliseconds(0), RealTime(0, 0));
-	QCOMPARE(RealTime::fromMilliseconds(500), RealTime(0, ONE_BILLION/2));
-	QCOMPARE(RealTime::fromMilliseconds(1000), RealTime(1, 0));
-	QCOMPARE(RealTime::fromMilliseconds(1500), RealTime(1, ONE_BILLION/2));
-
-    	QCOMPARE(RealTime::fromMilliseconds(-0), RealTime(0, 0));
-	QCOMPARE(RealTime::fromMilliseconds(-500), RealTime(0, -ONE_BILLION/2));
-	QCOMPARE(RealTime::fromMilliseconds(-1000), RealTime(-1, 0));
-	QCOMPARE(RealTime::fromMilliseconds(-1500), RealTime(-1, -ONE_BILLION/2));
-    }
-    
-    void fromTimeval()
-    {
-	struct timeval tv;
-
-	tv.tv_sec = 0; tv.tv_usec = 0;
-	QCOMPARE(RealTime::fromTimeval(tv), RealTime(0, 0));
-	tv.tv_sec = 0; tv.tv_usec = ONE_MILLION/2;
-	QCOMPARE(RealTime::fromTimeval(tv), RealTime(0, ONE_BILLION/2));
-	tv.tv_sec = 1; tv.tv_usec = 0;
-	QCOMPARE(RealTime::fromTimeval(tv), RealTime(1, 0));
-	tv.tv_sec = 1; tv.tv_usec = ONE_MILLION/2;
-	QCOMPARE(RealTime::fromTimeval(tv), RealTime(1, ONE_BILLION/2));
-
-	tv.tv_sec = 0; tv.tv_usec = -ONE_MILLION/2;
-	QCOMPARE(RealTime::fromTimeval(tv), RealTime(0, -ONE_BILLION/2));
-	tv.tv_sec = -1; tv.tv_usec = 0;
-	QCOMPARE(RealTime::fromTimeval(tv), RealTime(-1, 0));
-	tv.tv_sec = -1; tv.tv_usec = -ONE_MILLION/2;
-	QCOMPARE(RealTime::fromTimeval(tv), RealTime(-1, -ONE_BILLION/2));
-    }
-
-    void fromXsdDuration()
-    {
-	QCOMPARE(RealTime::fromXsdDuration("PT0"), RealTime::zeroTime);
-	QCOMPARE(RealTime::fromXsdDuration("PT0S"), RealTime::zeroTime);
-	QCOMPARE(RealTime::fromXsdDuration("PT10S"), RealTime(10, 0));
-	QCOMPARE(RealTime::fromXsdDuration("PT10.5S"), RealTime(10, ONE_BILLION/2));
-	QCOMPARE(RealTime::fromXsdDuration("PT1.5S").sec, 1);
-	QCOMPARE(RealTime::fromXsdDuration("PT1.5S").msec(), 500);
-	QCOMPARE(RealTime::fromXsdDuration("-PT1.5S").sec, -1);
-	QCOMPARE(RealTime::fromXsdDuration("-PT1.5S").msec(), -500);
-	QCOMPARE(RealTime::fromXsdDuration("PT1M30.5S"), RealTime(90, ONE_BILLION/2));
-	QCOMPARE(RealTime::fromXsdDuration("PT1H2M30.5S"), RealTime(3750, ONE_BILLION/2));
-    }
-
-    void toDouble()
-    {
-	QCOMPARE(RealTime(0, 0).toDouble(), 0.0);
-	QCOMPARE(RealTime(0, ONE_BILLION/2).toDouble(), 0.5);
-	QCOMPARE(RealTime(1, 0).toDouble(), 1.0);
-	QCOMPARE(RealTime(1, ONE_BILLION/2).toDouble(), 1.5);
-
-	QCOMPARE(RealTime(0, -ONE_BILLION/2).toDouble(), -0.5);
-	QCOMPARE(RealTime(-1, 0).toDouble(), -1.0);
-	QCOMPARE(RealTime(-1, -ONE_BILLION/2).toDouble(), -1.5);
-    }
-
-    void assign()
-    {
-	RealTime r;
-	r = RealTime(0, 0);
-	QCOMPARE(r, RealTime::zeroTime);
-	r = RealTime(0, ONE_BILLION/2);
-	QCOMPARE(r.toDouble(), 0.5);
-	r = RealTime(-1, -ONE_BILLION/2);
-	QCOMPARE(r.toDouble(), -1.5);
-    }
-
-    void plus()
-    {
-	QCOMPARE(RealTime(0, 0) + RealTime(0, 0), RealTime(0, 0));
-
-	QCOMPARE(RealTime(0, 0) + RealTime(0, ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
-	QCOMPARE(RealTime(0, ONE_BILLION/2) + RealTime(0, ONE_BILLION/2), RealTime(1, 0));
-	QCOMPARE(RealTime(1, 0) + RealTime(0, ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
-
-	QCOMPARE(RealTime(0, 0) + RealTime(0, -ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
-	QCOMPARE(RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2), RealTime(-1, 0));
-	QCOMPARE(RealTime(-1, 0) + RealTime(0, -ONE_BILLION/2), RealTime(-1, -ONE_BILLION/2));
-
-    	QCOMPARE(RealTime(1, 0) + RealTime(0, -ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
-	QCOMPARE(RealTime(1, 0) + RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2), RealTime(0, 0));
-	QCOMPARE(RealTime(1, 0) + RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
-
-	QCOMPARE(RealTime(0, ONE_BILLION/2) + RealTime(-1, 0), RealTime(0, -ONE_BILLION/2));
-	QCOMPARE(RealTime(0, -ONE_BILLION/2) + RealTime(1, 0), RealTime(0, ONE_BILLION/2));
-    }
-    
-    void minus()
-    {
-	QCOMPARE(RealTime(0, 0) - RealTime(0, 0), RealTime(0, 0));
-
-	QCOMPARE(RealTime(0, 0) - RealTime(0, ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
-	QCOMPARE(RealTime(0, ONE_BILLION/2) - RealTime(0, ONE_BILLION/2), RealTime(0, 0));
-	QCOMPARE(RealTime(1, 0) - RealTime(0, ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
-
-	QCOMPARE(RealTime(0, 0) - RealTime(0, -ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
-	QCOMPARE(RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2), RealTime(0, 0));
-	QCOMPARE(RealTime(-1, 0) - RealTime(0, -ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
-
-    	QCOMPARE(RealTime(1, 0) - RealTime(0, -ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
-	QCOMPARE(RealTime(1, 0) - RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2), RealTime(2, 0));
-	QCOMPARE(RealTime(1, 0) - RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2), RealTime(2, ONE_BILLION/2));
-
-	QCOMPARE(RealTime(0, ONE_BILLION/2) - RealTime(-1, 0), RealTime(1, ONE_BILLION/2));
-	QCOMPARE(RealTime(0, -ONE_BILLION/2) - RealTime(1, 0), RealTime(-1, -ONE_BILLION/2));
-    }
-
-    void negate()
-    {
-	QCOMPARE(-RealTime(0, 0), RealTime(0, 0));
-	QCOMPARE(-RealTime(1, 0), RealTime(-1, 0));
-	QCOMPARE(-RealTime(1, ONE_BILLION/2), RealTime(-1, -ONE_BILLION/2));
-	QCOMPARE(-RealTime(-1, -ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
-    }
-
-    void compare()
-    {
-	int sec, nsec;
-	for (sec = -2; sec <= 2; sec += 2) {
-	    for (nsec = -1; nsec <= 1; nsec += 1) {
-		QCOMPARE(RealTime(sec, nsec) < RealTime(sec, nsec), false);
-		QCOMPARE(RealTime(sec, nsec) > RealTime(sec, nsec), false);
-		QCOMPARE(RealTime(sec, nsec) == RealTime(sec, nsec), true);
-		QCOMPARE(RealTime(sec, nsec) != RealTime(sec, nsec), false);
-		QCOMPARE(RealTime(sec, nsec) <= RealTime(sec, nsec), true);
-		QCOMPARE(RealTime(sec, nsec) >= RealTime(sec, nsec), true);
-	    }
-	}
-	RealTime prev(-3, 0);
-	for (sec = -2; sec <= 2; sec += 2) {
-	    for (nsec = -1; nsec <= 1; nsec += 1) {
-
-		RealTime curr(sec, nsec);
-
-		QCOMPARE(prev < curr, true);
-		QCOMPARE(prev > curr, false);
-		QCOMPARE(prev == curr, false);
-		QCOMPARE(prev != curr, true);
-		QCOMPARE(prev <= curr, true);
-		QCOMPARE(prev >= curr, false);
-
-		QCOMPARE(curr < prev, false);
-		QCOMPARE(curr > prev, true);
-		QCOMPARE(curr == prev, false);
-		QCOMPARE(curr != prev, true);
-		QCOMPARE(curr <= prev, false);
-		QCOMPARE(curr >= prev, true);
-
-		prev = curr;
-	    }
-	}
-    }
-
-    void frame()
-    {
-        int frames[] = {
-            0, 1, 2047, 2048, 6656, 32767, 32768, 44100, 44101, 999999999
-        };
-        int n = sizeof(frames)/sizeof(frames[0]);
-
-        int rates[] = {
-            1, 2, 8000, 22050, 44100, 44101, 192000
-        };
-        int m = sizeof(rates)/sizeof(rates[0]);
-
-        for (int i = 0; i < n; ++i) {
-            sv_frame_t frame = frames[i];
-            for (int j = 0; j < m; ++j) {
-                int rate = rates[j];
-
-                RealTime rt = RealTime::frame2RealTime(frame, rate);
-                sv_frame_t conv = RealTime::realTime2Frame(rt, rate);
-                QCOMPARE(frame, conv);
-
-                rt = RealTime::frame2RealTime(-frame, rate);
-                conv = RealTime::realTime2Frame(rt, rate);
-                QCOMPARE(-frame, conv);
-            }
-        }
-    }
-    
-    void toText()
-    {
-        // we want to use QStrings, because then the Qt test library
-        // will print out any conflicts. The compareTexts function
-        // does this for us
-
-        int halfSec = ONE_BILLION/2; // nsec
-        
-        RealTime rt = RealTime(0, 0);
-        compareTexts(rt.toMSText(false, false), "0");
-        compareTexts(rt.toMSText(true, false), "0.000");
-        compareTexts(rt.toMSText(false, true), "0");
-        compareTexts(rt.toMSText(true, true), "0.000");
-        compareTexts(rt.toFrameText(24, false), "0:00");
-        compareTexts(rt.toFrameText(24, true), "0:00");
-        compareTexts(rt.toSecText(), "0s");
-
-        rt = RealTime(1, halfSec);
-        compareTexts(rt.toMSText(false, false), "1.5");
-        compareTexts(rt.toMSText(true, false), "1.500");
-        compareTexts(rt.toMSText(false, true), "1.5");
-        compareTexts(rt.toMSText(true, true), "1.500");
-        compareTexts(rt.toFrameText(24, false), "1:12");
-        compareTexts(rt.toFrameText(24, true), "1:12");
-        compareTexts(rt.toFrameText(25, false), "1:12");
-        compareTexts(rt.toFrameText(25, true), "1:12");
-        compareTexts(rt.toSecText(), "1s");
-
-        rt = RealTime::fromSeconds(-1.5);
-        compareTexts(rt.toMSText(false, false), "-1.5");
-        compareTexts(rt.toMSText(true, false), "-1.500");
-        compareTexts(rt.toMSText(false, true), "-1.5");
-        compareTexts(rt.toMSText(true, true), "-1.500");
-        compareTexts(rt.toFrameText(24, false), "-1:12");
-        compareTexts(rt.toFrameText(24, true), "-1:12");
-        compareTexts(rt.toSecText(), "-1s");
-
-        rt = RealTime(1, 1000);
-        compareTexts(rt.toMSText(false, false), "1");
-        compareTexts(rt.toFrameText(24, false), "1:00");
-        compareTexts(rt.toFrameText(ONE_MILLION, false), "1:000001");
-        compareTexts(rt.toSecText(), "1s");
-
-        rt = RealTime(1, 100000);
-        compareTexts(rt.toFrameText(ONE_MILLION, false), "1:000100");
-        compareTexts(rt.toSecText(), "1s");
-
-        rt = RealTime::fromSeconds(60);
-        compareTexts(rt.toMSText(false, false), "60");
-        compareTexts(rt.toMSText(true, false), "60.000");
-        compareTexts(rt.toMSText(false, true), "1:00");
-        compareTexts(rt.toMSText(true, true), "1:00.000");
-        compareTexts(rt.toFrameText(24, false), "60:00");
-        compareTexts(rt.toFrameText(24, true), "1:00:00");
-        compareTexts(rt.toSecText(), "1:00");
-
-        rt = RealTime::fromSeconds(61.05);
-        compareTexts(rt.toMSText(false, false), "61.05");
-        compareTexts(rt.toMSText(true, false), "61.050");
-        compareTexts(rt.toMSText(false, true), "1:01.05");
-        compareTexts(rt.toMSText(true, true), "1:01.050");
-        compareTexts(rt.toFrameText(24, false), "61:01");
-        compareTexts(rt.toFrameText(24, true), "1:01:01");
-        compareTexts(rt.toSecText(), "1:01");
-        
-        rt = RealTime::fromSeconds(601.05);
-        compareTexts(rt.toMSText(false, false), "601.05");
-        compareTexts(rt.toMSText(true, false), "601.050");
-        compareTexts(rt.toMSText(false, true), "10:01.05");
-        compareTexts(rt.toMSText(true, true), "10:01.050");
-        compareTexts(rt.toFrameText(24, false), "601:01");
-        compareTexts(rt.toFrameText(24, true), "10:01:01");
-        compareTexts(rt.toSecText(), "10:01");
-        
-        rt = RealTime::fromSeconds(3600);
-        compareTexts(rt.toMSText(false, false), "3600");
-        compareTexts(rt.toMSText(true, false), "3600.000");
-        compareTexts(rt.toMSText(false, true), "1:00:00");
-        compareTexts(rt.toMSText(true, true), "1:00:00.000");
-        compareTexts(rt.toFrameText(24, false), "3600:00");
-        compareTexts(rt.toFrameText(24, true), "1:00:00:00");
-        compareTexts(rt.toSecText(), "1:00:00");
-
-        // For practical reasons our time display always rounds down
-        rt = RealTime(3599, ONE_BILLION-1);
-        compareTexts(rt.toMSText(false, false), "3599.999");
-        compareTexts(rt.toMSText(true, false), "3599.999");
-        compareTexts(rt.toMSText(false, true), "59:59.999");
-        compareTexts(rt.toMSText(true, true), "59:59.999");
-        compareTexts(rt.toFrameText(24, false), "3599:23");
-        compareTexts(rt.toFrameText(24, true), "59:59:23");
-        compareTexts(rt.toSecText(), "59:59");
-
-        rt = RealTime::fromSeconds(3600 * 4 + 60 * 5 + 3 + 0.01);
-        compareTexts(rt.toMSText(false, false), "14703.01");
-        compareTexts(rt.toMSText(true, false), "14703.010");
-        compareTexts(rt.toMSText(false, true), "4:05:03.01");
-        compareTexts(rt.toMSText(true, true), "4:05:03.010");
-        compareTexts(rt.toFrameText(24, false), "14703:00");
-        compareTexts(rt.toFrameText(24, true), "4:05:03:00");
-        compareTexts(rt.toSecText(), "4:05:03");
-
-        rt = RealTime::fromSeconds(-(3600 * 4 + 60 * 5 + 3 + 0.01));
-        compareTexts(rt.toMSText(false, false), "-14703.01");
-        compareTexts(rt.toMSText(true, false), "-14703.010");
-        compareTexts(rt.toMSText(false, true), "-4:05:03.01");
-        compareTexts(rt.toMSText(true, true), "-4:05:03.010");
-        compareTexts(rt.toFrameText(24, false), "-14703:00");
-        compareTexts(rt.toFrameText(24, true), "-4:05:03:00");
-        compareTexts(rt.toSecText(), "-4:05:03");
-    }
-};
-
-#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/TestVampRealTime.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,383 @@
+/* -*- 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_VAMP_REALTIME_H
+
+#include <QObject>
+#include <QtTest>
+#include <QDir>
+
+#include <iostream>
+
+#include <vamp-hostsdk/RealTime.h>
+
+using namespace std;
+
+#define ONE_MILLION 1000000
+#define ONE_BILLION 1000000000
+
+class TestVampRealTime : public QObject
+{
+    Q_OBJECT
+
+    void compareTexts(string s, const char *e) {
+        QString actual(s.c_str());
+        QString expected(e);
+        QCOMPARE(actual, expected);
+    }
+
+    typedef Vamp::RealTime RealTime;
+    typedef long frame_type;
+                           
+private slots:
+
+    void zero()
+    {
+        QCOMPARE(RealTime(0, 0), RealTime::zeroTime);
+        QCOMPARE(RealTime(0, 0).sec, 0);
+        QCOMPARE(RealTime(0, 0).nsec, 0);
+        QCOMPARE(RealTime(0, 0).msec(), 0);
+        QCOMPARE(RealTime(0, 0).usec(), 0);
+    }
+
+    void ctor()
+    {
+        QCOMPARE(RealTime(0, 0), RealTime(0, 0));
+
+        // wraparounds
+        QCOMPARE(RealTime(0, ONE_BILLION/2), RealTime(1, -ONE_BILLION/2));
+        QCOMPARE(RealTime(0, -ONE_BILLION/2), RealTime(-1, ONE_BILLION/2));
+
+        QCOMPARE(RealTime(1, ONE_BILLION), RealTime(2, 0));
+        QCOMPARE(RealTime(1, -ONE_BILLION), RealTime(0, 0));
+        QCOMPARE(RealTime(-1, ONE_BILLION), RealTime(0, 0));
+        QCOMPARE(RealTime(-1, -ONE_BILLION), RealTime(-2, 0));
+
+        QCOMPARE(RealTime(2, -ONE_BILLION*2), RealTime(0, 0));
+        QCOMPARE(RealTime(2, -ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
+
+        QCOMPARE(RealTime(-2, ONE_BILLION*2), RealTime(0, 0));
+        QCOMPARE(RealTime(-2, ONE_BILLION/2), RealTime(-1, -ONE_BILLION/2));
+        
+        QCOMPARE(RealTime(0, 1).sec, 0);
+        QCOMPARE(RealTime(0, 1).nsec, 1);
+        QCOMPARE(RealTime(0, -1).sec, 0);
+        QCOMPARE(RealTime(0, -1).nsec, -1);
+        QCOMPARE(RealTime(1, -1).sec, 0);
+        QCOMPARE(RealTime(1, -1).nsec, ONE_BILLION-1);
+        QCOMPARE(RealTime(-1, 1).sec, 0);
+        QCOMPARE(RealTime(-1, 1).nsec, -ONE_BILLION+1);
+        QCOMPARE(RealTime(-1, -1).sec, -1);
+        QCOMPARE(RealTime(-1, -1).nsec, -1);
+        
+        QCOMPARE(RealTime(2, -ONE_BILLION*2).sec, 0);
+        QCOMPARE(RealTime(2, -ONE_BILLION*2).nsec, 0);
+        QCOMPARE(RealTime(2, -ONE_BILLION/2).sec, 1);
+        QCOMPARE(RealTime(2, -ONE_BILLION/2).nsec, ONE_BILLION/2);
+
+        QCOMPARE(RealTime(-2, ONE_BILLION*2).sec, 0);
+        QCOMPARE(RealTime(-2, ONE_BILLION*2).nsec, 0);
+        QCOMPARE(RealTime(-2, ONE_BILLION/2).sec, -1);
+        QCOMPARE(RealTime(-2, ONE_BILLION/2).nsec, -ONE_BILLION/2);
+    }
+    
+    void fromSeconds()
+    {
+        QCOMPARE(RealTime::fromSeconds(0), RealTime(0, 0));
+
+        QCOMPARE(RealTime::fromSeconds(0.5).sec, 0);
+        QCOMPARE(RealTime::fromSeconds(0.5).nsec, ONE_BILLION/2);
+        QCOMPARE(RealTime::fromSeconds(0.5).usec(), ONE_MILLION/2);
+        QCOMPARE(RealTime::fromSeconds(0.5).msec(), 500);
+        
+        QCOMPARE(RealTime::fromSeconds(0.5), RealTime(0, ONE_BILLION/2));
+        QCOMPARE(RealTime::fromSeconds(1), RealTime(1, 0));
+        QCOMPARE(RealTime::fromSeconds(1.5), RealTime(1, ONE_BILLION/2));
+
+        QCOMPARE(RealTime::fromSeconds(-0.5).sec, 0);
+        QCOMPARE(RealTime::fromSeconds(-0.5).nsec, -ONE_BILLION/2);
+        QCOMPARE(RealTime::fromSeconds(-0.5).usec(), -ONE_MILLION/2);
+        QCOMPARE(RealTime::fromSeconds(-0.5).msec(), -500);
+        
+        QCOMPARE(RealTime::fromSeconds(-1.5).sec, -1);
+        QCOMPARE(RealTime::fromSeconds(-1.5).nsec, -ONE_BILLION/2);
+        QCOMPARE(RealTime::fromSeconds(-1.5).usec(), -ONE_MILLION/2);
+        QCOMPARE(RealTime::fromSeconds(-1.5).msec(), -500);
+        
+        QCOMPARE(RealTime::fromSeconds(-0.5), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime::fromSeconds(-1), RealTime(-1, 0));
+        QCOMPARE(RealTime::fromSeconds(-1.5), RealTime(-1, -ONE_BILLION/2));
+    }
+
+    void fromMilliseconds()
+    {
+        QCOMPARE(RealTime::fromMilliseconds(0), RealTime(0, 0));
+        QCOMPARE(RealTime::fromMilliseconds(500), RealTime(0, ONE_BILLION/2));
+        QCOMPARE(RealTime::fromMilliseconds(1000), RealTime(1, 0));
+        QCOMPARE(RealTime::fromMilliseconds(1500), RealTime(1, ONE_BILLION/2));
+
+        QCOMPARE(RealTime::fromMilliseconds(-0), RealTime(0, 0));
+        QCOMPARE(RealTime::fromMilliseconds(-500), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime::fromMilliseconds(-1000), RealTime(-1, 0));
+        QCOMPARE(RealTime::fromMilliseconds(-1500), RealTime(-1, -ONE_BILLION/2));
+    }
+
+#ifndef Q_OS_WIN
+    void fromTimeval()
+    {
+        struct timeval tv;
+
+        tv.tv_sec = 0; tv.tv_usec = 0;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(0, 0));
+        tv.tv_sec = 0; tv.tv_usec = ONE_MILLION/2;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(0, ONE_BILLION/2));
+        tv.tv_sec = 1; tv.tv_usec = 0;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(1, 0));
+        tv.tv_sec = 1; tv.tv_usec = ONE_MILLION/2;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(1, ONE_BILLION/2));
+
+        tv.tv_sec = 0; tv.tv_usec = -ONE_MILLION/2;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(0, -ONE_BILLION/2));
+        tv.tv_sec = -1; tv.tv_usec = 0;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(-1, 0));
+        tv.tv_sec = -1; tv.tv_usec = -ONE_MILLION/2;
+        QCOMPARE(RealTime::fromTimeval(tv), RealTime(-1, -ONE_BILLION/2));
+    }
+#endif
+
+    void assign()
+    {
+        RealTime r;
+        r = RealTime(0, 0);
+        QCOMPARE(r, RealTime::zeroTime);
+        r = RealTime(0, ONE_BILLION/2);
+        QCOMPARE(r.sec, 0);
+        QCOMPARE(r.nsec, ONE_BILLION/2);
+        r = RealTime(-1, -ONE_BILLION/2);
+        QCOMPARE(r.sec, -1);
+        QCOMPARE(r.nsec, -ONE_BILLION/2);
+    }
+
+    void plus()
+    {
+        QCOMPARE(RealTime(0, 0) + RealTime(0, 0), RealTime(0, 0));
+
+        QCOMPARE(RealTime(0, 0) + RealTime(0, ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
+        QCOMPARE(RealTime(0, ONE_BILLION/2) + RealTime(0, ONE_BILLION/2), RealTime(1, 0));
+        QCOMPARE(RealTime(1, 0) + RealTime(0, ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
+
+        QCOMPARE(RealTime(0, 0) + RealTime(0, -ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2), RealTime(-1, 0));
+        QCOMPARE(RealTime(-1, 0) + RealTime(0, -ONE_BILLION/2), RealTime(-1, -ONE_BILLION/2));
+
+        QCOMPARE(RealTime(1, 0) + RealTime(0, -ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
+        QCOMPARE(RealTime(1, 0) + RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2), RealTime(0, 0));
+        QCOMPARE(RealTime(1, 0) + RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2) + RealTime(0, -ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
+
+        QCOMPARE(RealTime(0, ONE_BILLION/2) + RealTime(-1, 0), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime(0, -ONE_BILLION/2) + RealTime(1, 0), RealTime(0, ONE_BILLION/2));
+    }
+    
+    void minus()
+    {
+        QCOMPARE(RealTime(0, 0) - RealTime(0, 0), RealTime(0, 0));
+
+        QCOMPARE(RealTime(0, 0) - RealTime(0, ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
+        QCOMPARE(RealTime(0, ONE_BILLION/2) - RealTime(0, ONE_BILLION/2), RealTime(0, 0));
+        QCOMPARE(RealTime(1, 0) - RealTime(0, ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
+
+        QCOMPARE(RealTime(0, 0) - RealTime(0, -ONE_BILLION/2), RealTime(0, ONE_BILLION/2));
+        QCOMPARE(RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2), RealTime(0, 0));
+        QCOMPARE(RealTime(-1, 0) - RealTime(0, -ONE_BILLION/2), RealTime(0, -ONE_BILLION/2));
+
+        QCOMPARE(RealTime(1, 0) - RealTime(0, -ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
+        QCOMPARE(RealTime(1, 0) - RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2), RealTime(2, 0));
+        QCOMPARE(RealTime(1, 0) - RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2) - RealTime(0, -ONE_BILLION/2), RealTime(2, ONE_BILLION/2));
+
+        QCOMPARE(RealTime(0, ONE_BILLION/2) - RealTime(-1, 0), RealTime(1, ONE_BILLION/2));
+        QCOMPARE(RealTime(0, -ONE_BILLION/2) - RealTime(1, 0), RealTime(-1, -ONE_BILLION/2));
+    }
+
+    void negate()
+    {
+        QCOMPARE(-RealTime(0, 0), RealTime(0, 0));
+        QCOMPARE(-RealTime(1, 0), RealTime(-1, 0));
+        QCOMPARE(-RealTime(1, ONE_BILLION/2), RealTime(-1, -ONE_BILLION/2));
+        QCOMPARE(-RealTime(-1, -ONE_BILLION/2), RealTime(1, ONE_BILLION/2));
+    }
+
+    void compare()
+    {
+        int sec, nsec;
+        for (sec = -2; sec <= 2; sec += 2) {
+            for (nsec = -1; nsec <= 1; nsec += 1) {
+                QCOMPARE(RealTime(sec, nsec) < RealTime(sec, nsec), false);
+                QCOMPARE(RealTime(sec, nsec) > RealTime(sec, nsec), false);
+                QCOMPARE(RealTime(sec, nsec) == RealTime(sec, nsec), true);
+                QCOMPARE(RealTime(sec, nsec) != RealTime(sec, nsec), false);
+                QCOMPARE(RealTime(sec, nsec) <= RealTime(sec, nsec), true);
+                QCOMPARE(RealTime(sec, nsec) >= RealTime(sec, nsec), true);
+            }
+        }
+        RealTime prev(-3, 0);
+        for (sec = -2; sec <= 2; sec += 2) {
+            for (nsec = -1; nsec <= 1; nsec += 1) {
+
+                RealTime curr(sec, nsec);
+
+                QCOMPARE(prev < curr, true);
+                QCOMPARE(prev > curr, false);
+                QCOMPARE(prev == curr, false);
+                QCOMPARE(prev != curr, true);
+                QCOMPARE(prev <= curr, true);
+                QCOMPARE(prev >= curr, false);
+
+                QCOMPARE(curr < prev, false);
+                QCOMPARE(curr > prev, true);
+                QCOMPARE(curr == prev, false);
+                QCOMPARE(curr != prev, true);
+                QCOMPARE(curr <= prev, false);
+                QCOMPARE(curr >= prev, true);
+
+                prev = curr;
+            }
+        }
+    }
+
+    void frame()
+    {
+        int frames[] = {
+            0, 1, 2047, 2048, 6656,
+            32767, 32768, 44100, 44101,
+            999999999, 2000000000
+        };
+        int n = sizeof(frames)/sizeof(frames[0]);
+
+        int rates[] = {
+            1, 2, 8000, 22050,
+            44100, 44101, 192000, 2000000001
+        };
+        int m = sizeof(rates)/sizeof(rates[0]);
+
+        vector<vector<RealTime>> realTimes = {
+            { { 0, 0 }, { 1, 0 }, { 2047, 0 }, { 2048, 0 },
+              { 6656, 0 }, { 32767, 0 }, { 32768, 0 }, { 44100, 0 },
+              { 44101, 0 }, { 999999999, 0 }, { 2000000000, 0 } },
+            { { 0, 0 }, { 0, 500000000 }, { 1023, 500000000 }, { 1024, 0 },
+              { 3328, 0 }, { 16383, 500000000 }, { 16384, 0 }, { 22050, 0 },
+              { 22050, 500000000 }, { 499999999, 500000000 }, { 1000000000, 0 } },
+            { { 0, 0 }, { 0, 125000 }, { 0, 255875000 }, { 0, 256000000 },
+              { 0, 832000000 }, { 4, 95875000 }, { 4, 96000000 }, { 5, 512500000 },
+              { 5, 512625000 }, { 124999, 999875000 }, { 250000, 0 } },
+            { { 0, 0 }, { 0, 45351 }, { 0, 92834467 }, { 0, 92879819 },
+              { 0, 301859410 }, { 1, 486031746 }, { 1, 486077098 }, { 2, 0 },
+              { 2, 45351 }, { 45351, 473877551 }, { 90702, 947845805 } },
+            { { 0, 0 }, { 0, 22676 }, { 0, 46417234 }, { 0, 46439909 },
+              { 0, 150929705 }, { 0, 743015873 }, { 0, 743038549 }, { 1, 0 },
+              { 1, 22676 }, { 22675, 736938776 }, { 45351, 473922902 } },
+            { { 0, 0 }, { 0, 22675 }, { 0, 46416181 }, { 0, 46438856 },
+              { 0, 150926283 }, { 0, 742999025 }, { 0, 743021700 }, { 0, 999977325 },
+              { 1, 0 }, { 22675, 222761389 }, { 45350, 445568128 } },
+            { { 0, 0 }, { 0, 5208 }, { 0, 10661458 }, { 0, 10666667 },
+              { 0, 34666667 }, { 0, 170661458 }, { 0, 170666667 }, { 0, 229687500 },
+              { 0, 229692708 }, { 5208, 333328125 }, { 10416, 666666667 } },
+            { { 0, 0 }, { 0, 0 }, { 0, 1023 }, { 0, 1024 },
+              { 0, 3328 }, { 0, 16383 }, { 0, 16384 }, { 0, 22050 },
+              { 0, 22050 }, { 0, 499999999 }, { 1, 0 } }
+        };
+        
+        for (int i = 0; i < n; ++i) {
+            frame_type frame = frames[i];
+            for (int j = 0; j < m; ++j) {
+                int rate = rates[j];
+
+                RealTime rt = RealTime::frame2RealTime(frame, rate);
+                QCOMPARE(rt.sec, realTimes[j][i].sec);
+                QCOMPARE(rt.nsec, realTimes[j][i].nsec);
+
+                frame_type conv = RealTime::realTime2Frame(rt, rate);
+                
+                rt = RealTime::frame2RealTime(-frame, rate);
+                frame_type negconv = RealTime::realTime2Frame(rt, rate);
+
+                if (rate > ONE_BILLION) {
+                    // We don't have enough precision in RealTime
+                    // for this absurd sample rate, so a round trip
+                    // conversion may round
+                    QVERIFY(abs(frame - conv) < 2);
+                    QVERIFY(abs(-frame - negconv) < 2);
+                } else {
+                    QCOMPARE(conv, frame);
+                    QCOMPARE(negconv, -frame);
+                }
+            }
+        }
+    }
+
+    // Vamp SDK version just has toText, which is like our own
+    // toMSText with true for its second arg
+    
+    void toText()
+    {
+        // we want to use QStrings, because then the Qt test library
+        // will print out any conflicts. The compareTexts function
+        // does this for us
+
+        int halfSec = ONE_BILLION/2; // nsec
+        
+        RealTime rt = RealTime(0, 0);
+        compareTexts(rt.toText(false), "0");
+        compareTexts(rt.toText(true), "0.000");
+
+        rt = RealTime(1, halfSec);
+        compareTexts(rt.toText(false), "1.5");
+        compareTexts(rt.toText(true), "1.500");
+
+        rt = RealTime::fromSeconds(-1.5);
+        compareTexts(rt.toText(false), "-1.5");
+        compareTexts(rt.toText(true), "-1.500");
+
+        rt = RealTime::fromSeconds(60);
+        compareTexts(rt.toText(false), "1:00");
+        compareTexts(rt.toText(true), "1:00.000");
+
+        rt = RealTime::fromSeconds(61.05);
+        compareTexts(rt.toText(false), "1:01.05");
+        compareTexts(rt.toText(true), "1:01.050");
+        
+        rt = RealTime::fromSeconds(601.05);
+        compareTexts(rt.toText(false), "10:01.05");
+        compareTexts(rt.toText(true), "10:01.050");
+        
+        rt = RealTime::fromSeconds(3600);
+        compareTexts(rt.toText(false), "1:00:00");
+        compareTexts(rt.toText(true), "1:00:00.000");
+
+        // For practical reasons our time display always rounds down
+        rt = RealTime(3599, ONE_BILLION-1);
+        compareTexts(rt.toText(false), "59:59.999");
+        compareTexts(rt.toText(true), "59:59.999");
+
+        rt = RealTime::fromSeconds(3600 * 4 + 60 * 5 + 3 + 0.01);
+        compareTexts(rt.toText(false), "4:05:03.01");
+        compareTexts(rt.toText(true), "4:05:03.010");
+
+        rt = RealTime::fromSeconds(-(3600 * 4 + 60 * 5 + 3 + 0.01));
+        compareTexts(rt.toText(false), "-4:05:03.01");
+        compareTexts(rt.toText(true), "-4:05:03.010");
+    }
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/files.pri	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,10 @@
+TEST_HEADERS = \
+	     TestRangeMapper.h \
+	     TestPitch.h \
+	     TestOurRealTime.h \
+	     TestVampRealTime.h \
+	     TestStringBits.h \
+	     TestColumnOp.h
+	     
+TEST_SOURCES += \
+	     svcore-base-test.cpp
--- a/base/test/main.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-/* -*- 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 "TestRangeMapper.h"
-#include "TestPitch.h"
-#include "TestRealTime.h"
-#include "TestStringBits.h"
-
-#include <QtTest>
-
-#include <iostream>
-
-int main(int argc, char *argv[])
-{
-    int good = 0, bad = 0;
-
-    QCoreApplication app(argc, argv);
-    app.setOrganizationName("Sonic Visualiser");
-    app.setApplicationName("test-svcore-base");
-
-    {
-	TestRangeMapper t;
-	if (QTest::qExec(&t, argc, argv) == 0) ++good;
-	else ++bad;
-    }
-    {
-	TestPitch t;
-	if (QTest::qExec(&t, argc, argv) == 0) ++good;
-	else ++bad;
-    }
-    {
-	TestRealTime t;
-	if (QTest::qExec(&t, argc, argv) == 0) ++good;
-	else ++bad;
-    }
-    {
-	TestStringBits t;
-	if (QTest::qExec(&t, argc, argv) == 0) ++good;
-	else ++bad;
-    }
-
-    if (bad > 0) {
-	cerr << "\n********* " << bad << " test suite(s) failed!\n" << endl;
-	return 1;
-    } else {
-	cerr << "All tests passed" << endl;
-	return 0;
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/svcore-base-test.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,71 @@
+/* -*- 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 "TestRangeMapper.h"
+#include "TestPitch.h"
+#include "TestStringBits.h"
+#include "TestOurRealTime.h"
+#include "TestVampRealTime.h"
+#include "TestColumnOp.h"
+
+#include <QtTest>
+
+#include <iostream>
+
+int main(int argc, char *argv[])
+{
+    int good = 0, bad = 0;
+
+    QCoreApplication app(argc, argv);
+    app.setOrganizationName("Sonic Visualiser");
+    app.setApplicationName("test-svcore-base");
+
+    {
+	TestRangeMapper t;
+	if (QTest::qExec(&t, argc, argv) == 0) ++good;
+	else ++bad;
+    }
+    {
+	TestPitch t;
+	if (QTest::qExec(&t, argc, argv) == 0) ++good;
+	else ++bad;
+    }
+    {
+        TestOurRealTime t;
+	if (QTest::qExec(&t, argc, argv) == 0) ++good;
+	else ++bad;
+    }
+    {
+        TestVampRealTime t;
+	if (QTest::qExec(&t, argc, argv) == 0) ++good;
+	else ++bad;
+    }
+    {
+	TestStringBits t;
+	if (QTest::qExec(&t, argc, argv) == 0) ++good;
+	else ++bad;
+    }
+    {
+	TestColumnOp t;
+	if (QTest::qExec(&t, argc, argv) == 0) ++good;
+	else ++bad;
+    }
+
+    if (bad > 0) {
+	cerr << "\n********* " << bad << " test suite(s) failed!\n" << endl;
+	return 1;
+    } else {
+	cerr << "All tests passed" << endl;
+	return 0;
+    }
+}
--- a/base/test/test.pro	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-
-TEMPLATE = app
-
-LIBS += -L../.. -L../../../dataquay -L../../release -L../../../dataquay/release -lsvcore -ldataquay
-
-win32-g++ {
-    INCLUDEPATH += ../../../sv-dependency-builds/win32-mingw/include
-    LIBS += -L../../../sv-dependency-builds/win32-mingw/lib
-}
-win32-msvc* {
-    INCLUDEPATH += ../../../sv-dependency-builds/win32-msvc/include
-    LIBS += -L../../../sv-dependency-builds/win32-msvc/lib
-}
-mac* {
-    INCLUDEPATH += ../../../sv-dependency-builds/osx/include
-    LIBS += -L../../../sv-dependency-builds/osx/lib
-}
-
-exists(../../config.pri) {
-    include(../../config.pri)
-}
-
-!exists(../../config.pri) {
-
-    CONFIG += release
-    DEFINES += NDEBUG BUILD_RELEASE NO_TIMING
-
-    DEFINES += HAVE_BZ2 HAVE_FFTW3 HAVE_FFTW3F HAVE_SNDFILE HAVE_SAMPLERATE HAVE_VAMP HAVE_VAMPHOSTSDK HAVE_DATAQUAY HAVE_LIBLO HAVE_MAD HAVE_ID3TAG HAVE_PORTAUDIO_2_0
-
-    LIBS += -lbz2 -lvamp-hostsdk -lfftw3 -lfftw3f -lsndfile -lFLAC -logg -lvorbis -lvorbisenc -lvorbisfile -logg -lmad -lid3tag -lportaudio -lsamplerate -lz -lsord-0 -lserd-0
-
-    win* {
-        LIBS += -llo -lwinmm -lws2_32
-    }
-    macx* {
-        DEFINES += HAVE_COREAUDIO
-        LIBS += -framework CoreAudio -framework CoreMidi -framework AudioUnit -framework AudioToolbox -framework CoreFoundation -framework CoreServices -framework Accelerate
-    }
-}
-
-CONFIG += qt thread warn_on stl rtti exceptions console c++11
-QT += network xml testlib
-QT -= gui
-
-TARGET = svcore-base-test
-
-DEPENDPATH += ../..
-INCLUDEPATH += ../..
-OBJECTS_DIR = o
-MOC_DIR = o
-
-HEADERS += TestRangeMapper.h TestPitch.h TestRealTime.h TestStringBits.h
-SOURCES += main.cpp
-
-win* {
-//PRE_TARGETDEPS += ../../svcore.lib
-}
-!win* {
-PRE_TARGETDEPS += ../../libsvcore.a
-}
-
-!win32 {
-    !macx* {
-        QMAKE_POST_LINK=./$${TARGET}
-    }
-    macx* {
-        QMAKE_POST_LINK=./$${TARGET}.app/Contents/MacOS/$${TARGET}
-    }
-}
-
-win32:QMAKE_POST_LINK=./release/$${TARGET}.exe
-
--- a/configure.ac	Mon Nov 21 16:32:58 2016 +0000
+++ b/configure.ac	Fri Jan 13 10:29:44 2017 +0000
@@ -85,7 +85,7 @@
 SV_MODULE_REQUIRED([samplerate],[samplerate >= 0.1.2],[samplerate.h],[samplerate],[src_new])
 
 SV_MODULE_OPTIONAL([liblo],[],[lo/lo.h],[lo],[lo_address_new])
-SV_MODULE_OPTIONAL([portaudio_2_0],[portaudio-2.0 >= 19],[portaudio.h],[portaudio],[Pa_IsFormatSupported])
+SV_MODULE_OPTIONAL([portaudio],[portaudio-2.0 >= 19],[portaudio.h],[portaudio],[Pa_IsFormatSupported])
 SV_MODULE_OPTIONAL([JACK],[jack >= 0.100],[jack/jack.h],[jack],[jack_client_open])
 SV_MODULE_OPTIONAL([libpulse],[libpulse >= 0.9],[pulse/pulseaudio.h],[pulse],[pa_stream_new])
 SV_MODULE_OPTIONAL([lrdf],[lrdf >= 0.2],[lrdf.h],[lrdf],[lrdf_init])
--- a/data/fft/FFTCacheReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-/* -*- 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 file copyright 2006-2009 Chris Cannam and QMUL.
-    
-    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 _FFT_CACHE_READER_H_
-#define _FFT_CACHE_READER_H_
-
-#include "FFTCacheStorageType.h"
-#include <stddef.h>
-
-class FFTCacheReader
-{
-public:
-    virtual ~FFTCacheReader() { }
-
-    virtual int getWidth() const = 0;
-    virtual int getHeight() const = 0;
-	
-    virtual float getMagnitudeAt(int x, int y) const = 0;
-    virtual float getNormalizedMagnitudeAt(int x, int y) const = 0;
-    virtual float getMaximumMagnitudeAt(int x) const = 0;
-    virtual float getPhaseAt(int x, int y) const = 0;
-
-    virtual void getValuesAt(int x, int y, float &real, float &imag) const = 0;
-    virtual void getMagnitudesAt(int x, float *values, int minbin, int count, int step) const = 0;
-
-    virtual bool haveSetColumnAt(int x) const = 0;
-
-    virtual FFTCache::StorageType getStorageType() const = 0;
-};
-
-#endif
--- a/data/fft/FFTCacheStorageType.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-/* -*- 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 file copyright 2006-2009 Chris Cannam and QMUL.
-    
-    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 _FFT_CACHE_STORAGE_TYPE_H_
-#define _FFT_CACHE_STORAGE_TYPE_H_
-
-namespace FFTCache {
-enum StorageType { //!!! dup
-    Compact, // 16 bits normalized polar
-    Rectangular, // floating point real+imag
-    Polar // floating point mag+phase
-};
-}
-
-#endif
--- a/data/fft/FFTCacheWriter.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-/* -*- 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 file copyright 2006-2009 Chris Cannam and QMUL.
-    
-    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 _FFT_CACHE_WRITER_H_
-#define _FFT_CACHE_WRITER_H_
-
-#include <stddef.h>
-
-class FFTCacheWriter
-{
-public:
-    virtual ~FFTCacheWriter() { }
-
-    virtual int getWidth() const = 0;
-    virtual int getHeight() const = 0;
-
-    virtual void setColumnAt(int x, float *mags, float *phases, float factor) = 0;
-    virtual void setColumnAt(int x, float *reals, float *imags) = 0;
-
-    virtual bool haveSetColumnAt(int x) const = 0;
-
-    virtual void allColumnsWritten() = 0; // notify cache to close
-
-    virtual FFTCache::StorageType getStorageType() const = 0;
-};
-
-#endif
-
--- a/data/fft/FFTDataServer.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1601 +0,0 @@
-/* -*- 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 file copyright 2006 Chris Cannam and QMUL.
-    
-    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 "FFTDataServer.h"
-
-#include "FFTFileCacheReader.h"
-#include "FFTFileCacheWriter.h"
-#include "FFTMemoryCache.h"
-
-#include "model/DenseTimeValueModel.h"
-
-#include "system/System.h"
-
-#include "base/StorageAdviser.h"
-#include "base/Exceptions.h"
-#include "base/Profiler.h"
-#include "base/Thread.h" // for debug mutex locker
-
-#include <QWriteLocker>
-
-#include <stdexcept>
-
-//#define DEBUG_FFT_SERVER 1
-//#define DEBUG_FFT_SERVER_FILL 1
-
-#ifdef DEBUG_FFT_SERVER_FILL
-#ifndef DEBUG_FFT_SERVER
-#define DEBUG_FFT_SERVER 1
-#endif
-#endif
-
-
-FFTDataServer::ServerMap FFTDataServer::m_servers;
-FFTDataServer::ServerQueue FFTDataServer::m_releasedServers;
-QMutex FFTDataServer::m_serverMapMutex;
-
-FFTDataServer *
-FFTDataServer::getInstance(const DenseTimeValueModel *model,
-                           int channel,
-                           WindowType windowType,
-                           int windowSize,
-                           int windowIncrement,
-                           int fftSize,
-                           bool polar,
-                           StorageAdviser::Criteria criteria,
-                           sv_frame_t fillFromFrame)
-{
-    QString n = generateFileBasename(model,
-                                     channel,
-                                     windowType,
-                                     windowSize,
-                                     windowIncrement,
-                                     fftSize,
-                                     polar);
-
-    FFTDataServer *server = 0;
-    
-    MutexLocker locker(&m_serverMapMutex, "FFTDataServer::getInstance::m_serverMapMutex");
-
-    if ((server = findServer(n))) {
-        return server;
-    }
-
-    QString npn = generateFileBasename(model,
-                                       channel,
-                                       windowType,
-                                       windowSize,
-                                       windowIncrement,
-                                       fftSize,
-                                       !polar);
-
-    if ((server = findServer(npn))) {
-        return server;
-    }
-
-    try {
-        server = new FFTDataServer(n,
-                                   model,
-                                   channel,
-                                   windowType,
-                                   windowSize,
-                                   windowIncrement,
-                                   fftSize,
-                                   polar,
-                                   criteria,
-                                   fillFromFrame);
-    } catch (InsufficientDiscSpace) {
-        delete server;
-        server = 0;
-    }
-
-    if (server) {
-        m_servers[n] = ServerCountPair(server, 1);
-    }
-
-    return server;
-}
-
-FFTDataServer *
-FFTDataServer::getFuzzyInstance(const DenseTimeValueModel *model,
-                                int channel,
-                                WindowType windowType,
-                                int windowSize,
-                                int windowIncrement,
-                                int fftSize,
-                                bool polar,
-                                StorageAdviser::Criteria criteria,
-                                sv_frame_t fillFromFrame)
-{
-    // Fuzzy matching:
-    // 
-    // -- if we're asked for polar and have non-polar, use it (and
-    // vice versa).  This one is vital, and we do it for non-fuzzy as
-    // well (above).
-    //
-    // -- if we're asked for an instance with a given fft size and we
-    // have one already with a multiple of that fft size but the same
-    // window size and type (and model), we can draw the results from
-    // it (e.g. the 1st, 2nd, 3rd etc bins of a 512-sample FFT are the
-    // same as the the 1st, 5th, 9th etc of a 2048-sample FFT of the
-    // same window plus zero padding).
-    //
-    // -- if we're asked for an instance with a given window type and
-    // size and fft size and we have one already the same but with a
-    // smaller increment, we can draw the results from it (provided
-    // our increment is a multiple of its)
-    //
-    // The FFTModel knows how to interpret these things.  In
-    // both cases we require that the larger one is a power-of-two
-    // multiple of the smaller (e.g. even though in principle you can
-    // draw the results at increment 256 from those at increment 768
-    // or 1536, the model doesn't support this).
-
-    {
-        MutexLocker locker(&m_serverMapMutex, "FFTDataServer::getFuzzyInstance::m_serverMapMutex");
-
-        ServerMap::iterator best = m_servers.end();
-        int bestdist = -1;
-    
-        for (ServerMap::iterator i = m_servers.begin(); i != m_servers.end(); ++i) {
-
-            FFTDataServer *server = i->second.first;
-
-            if (server->getModel() == model &&
-                (server->getChannel() == channel || model->getChannelCount() == 1) &&
-                server->getWindowType() == windowType &&
-                server->getWindowSize() == windowSize &&
-                server->getWindowIncrement() <= windowIncrement &&
-                server->getFFTSize() >= fftSize) {
-                
-                if ((windowIncrement % server->getWindowIncrement()) != 0) continue;
-                int ratio = windowIncrement / server->getWindowIncrement();
-                bool poweroftwo = true;
-                while (ratio > 1) {
-                    if (ratio & 0x1) {
-                        poweroftwo = false;
-                        break;
-                    }
-                    ratio >>= 1;
-                }
-                if (!poweroftwo) continue;
-
-                if ((server->getFFTSize() % fftSize) != 0) continue;
-                ratio = server->getFFTSize() / fftSize;
-                while (ratio > 1) {
-                    if (ratio & 0x1) {
-                        poweroftwo = false;
-                        break;
-                    }
-                    ratio >>= 1;
-                }
-                if (!poweroftwo) continue;
-                
-                int distance = 0;
-                
-                if (server->getPolar() != polar) distance += 1;
-                
-                distance += ((windowIncrement / server->getWindowIncrement()) - 1) * 15;
-                distance += ((server->getFFTSize() / fftSize) - 1) * 10;
-                
-                if (server->getFillCompletion() < 50) distance += 100;
-
-#ifdef DEBUG_FFT_SERVER
-                std::cerr << "FFTDataServer::getFuzzyInstance: Distance for server " << server << " is " << distance << ", best is " << bestdist << std::endl;
-#endif
-                
-                if (bestdist == -1 || distance < bestdist) {
-                    bestdist = distance;
-                    best = i;
-                }
-            }
-        }
-
-        if (bestdist >= 0) {
-            FFTDataServer *server = best->second.first;
-#ifdef DEBUG_FFT_SERVER
-            std::cerr << "FFTDataServer::getFuzzyInstance: We like server " << server << " (with distance " << bestdist << ")" << std::endl;
-#endif
-            claimInstance(server, false);
-            return server;
-        }
-    }
-
-    // Nothing found, make a new one
-
-    return getInstance(model,
-                       channel,
-                       windowType,
-                       windowSize,
-                       windowIncrement,
-                       fftSize,
-                       polar,
-                       criteria,
-                       fillFromFrame);
-}
-
-FFTDataServer *
-FFTDataServer::findServer(QString n)
-{    
-#ifdef DEBUG_FFT_SERVER
-    std::cerr << "FFTDataServer::findServer(\"" << n << "\")" << std::endl;
-#endif
-
-    if (m_servers.find(n) != m_servers.end()) {
-
-        FFTDataServer *server = m_servers[n].first;
-
-#ifdef DEBUG_FFT_SERVER
-        std::cerr << "FFTDataServer::findServer(\"" << n << "\"): found " << server << std::endl;
-#endif
-
-        claimInstance(server, false);
-
-        return server;
-    }
-
-#ifdef DEBUG_FFT_SERVER
-        std::cerr << "FFTDataServer::findServer(\"" << n << "\"): not found" << std::endl;
-#endif
-
-    return 0;
-}
-
-void
-FFTDataServer::claimInstance(FFTDataServer *server)
-{
-    claimInstance(server, true);
-}
-
-void
-FFTDataServer::claimInstance(FFTDataServer *server, bool needLock)
-{
-    MutexLocker locker(needLock ? &m_serverMapMutex : 0,
-                       "FFTDataServer::claimInstance::m_serverMapMutex");
-
-#ifdef DEBUG_FFT_SERVER
-    std::cerr << "FFTDataServer::claimInstance(" << server << ")" << std::endl;
-#endif
-
-    for (ServerMap::iterator i = m_servers.begin(); i != m_servers.end(); ++i) {
-        if (i->second.first == server) {
-
-            for (ServerQueue::iterator j = m_releasedServers.begin();
-                 j != m_releasedServers.end(); ++j) {
-
-                if (*j == server) {
-#ifdef DEBUG_FFT_SERVER
-    std::cerr << "FFTDataServer::claimInstance: found in released server list, removing from it" << std::endl;
-#endif
-                    m_releasedServers.erase(j);
-                    break;
-                }
-            }
-
-            ++i->second.second;
-
-#ifdef DEBUG_FFT_SERVER
-            std::cerr << "FFTDataServer::claimInstance: new refcount is " << i->second.second << std::endl;
-#endif
-
-            return;
-        }
-    }
-    
-    cerr << "ERROR: FFTDataServer::claimInstance: instance "
-              << server << " unknown!" << endl;
-}
-
-void
-FFTDataServer::releaseInstance(FFTDataServer *server)
-{
-    releaseInstance(server, true);
-}
-
-void
-FFTDataServer::releaseInstance(FFTDataServer *server, bool needLock)
-{    
-    MutexLocker locker(needLock ? &m_serverMapMutex : 0,
-                       "FFTDataServer::releaseInstance::m_serverMapMutex");
-
-#ifdef DEBUG_FFT_SERVER
-    std::cerr << "FFTDataServer::releaseInstance(" << server << ")" << std::endl;
-#endif
-
-    // -- if ref count > 0, decrement and return
-    // -- if the instance hasn't been used at all, delete it immediately 
-    // -- if fewer than N instances (N = e.g. 3) remain with zero refcounts,
-    //    leave them hanging around
-    // -- if N instances with zero refcounts remain, delete the one that
-    //    was last released first
-    // -- if we run out of disk space when allocating an instance, go back
-    //    and delete the spare N instances before trying again
-    // -- have an additional method to indicate that a model has been
-    //    destroyed, so that we can delete all of its fft server instances
-
-    for (ServerMap::iterator i = m_servers.begin(); i != m_servers.end(); ++i) {
-        if (i->second.first == server) {
-            if (i->second.second == 0) {
-                cerr << "ERROR: FFTDataServer::releaseInstance("
-                          << server << "): instance not allocated" << endl;
-            } else if (--i->second.second == 0) {
-/*!!!
-                if (server->m_lastUsedCache == -1) { // never used
-#ifdef DEBUG_FFT_SERVER
-                    std::cerr << "FFTDataServer::releaseInstance: instance "
-                              << server << " has never been used, erasing"
-                              << std::endl;
-#endif
-                    delete server;
-                    m_servers.erase(i);
-                } else {
-*/
-#ifdef DEBUG_FFT_SERVER
-                    std::cerr << "FFTDataServer::releaseInstance: instance "
-                              << server << " no longer in use, marking for possible collection"
-                              << std::endl;
-#endif
-                    bool found = false;
-                    for (ServerQueue::iterator j = m_releasedServers.begin();
-                         j != m_releasedServers.end(); ++j) {
-                        if (*j == server) {
-                            cerr << "ERROR: FFTDataServer::releaseInstance("
-                                      << server << "): server is already in "
-                                      << "released servers list" << endl;
-                            found = true;
-                        }
-                    }
-                    if (!found) m_releasedServers.push_back(server);
-                    server->suspend();
-                    purgeLimbo();
-//!!!                }
-            } else {
-#ifdef DEBUG_FFT_SERVER
-                    std::cerr << "FFTDataServer::releaseInstance: instance "
-                              << server << " now has refcount " << i->second.second
-                              << std::endl;
-#endif
-            }
-            return;
-        }
-    }
-
-    cerr << "ERROR: FFTDataServer::releaseInstance(" << server << "): "
-              << "instance not found" << endl;
-}
-
-void
-FFTDataServer::purgeLimbo(int maxSize)
-{
-#ifdef DEBUG_FFT_SERVER
-    std::cerr << "FFTDataServer::purgeLimbo(" << maxSize << "): "
-              << m_releasedServers.size() << " candidates" << std::endl;
-#endif
-
-    while (int(m_releasedServers.size()) > maxSize) {
-
-        FFTDataServer *server = *m_releasedServers.begin();
-
-        bool found = false;
-
-#ifdef DEBUG_FFT_SERVER
-        std::cerr << "FFTDataServer::purgeLimbo: considering candidate "
-                  << server << std::endl;
-#endif
-
-        for (ServerMap::iterator i = m_servers.begin(); i != m_servers.end(); ++i) {
-
-            if (i->second.first == server) {
-                found = true;
-                if (i->second.second > 0) {
-                    cerr << "ERROR: FFTDataServer::purgeLimbo: Server "
-                              << server << " is in released queue, but still has non-zero refcount "
-                              << i->second.second << endl;
-                    // ... so don't delete it
-                    break;
-                }
-#ifdef DEBUG_FFT_SERVER
-                std::cerr << "FFTDataServer::purgeLimbo: looks OK, erasing it"
-                          << std::endl;
-#endif
-
-                m_servers.erase(i);
-                delete server;
-                break;
-            }
-        }
-
-        if (!found) {
-            cerr << "ERROR: FFTDataServer::purgeLimbo: Server "
-                      << server << " is in released queue, but not in server map!"
-                      << endl;
-            delete server;
-        }
-
-        m_releasedServers.pop_front();
-    }
-
-#ifdef DEBUG_FFT_SERVER
-    std::cerr << "FFTDataServer::purgeLimbo(" << maxSize << "): "
-              << m_releasedServers.size() << " remain" << std::endl;
-#endif
-
-}
-
-void
-FFTDataServer::modelAboutToBeDeleted(Model *model)
-{
-    MutexLocker locker(&m_serverMapMutex,
-                       "FFTDataServer::modelAboutToBeDeleted::m_serverMapMutex");
-
-#ifdef DEBUG_FFT_SERVER
-    std::cerr << "FFTDataServer::modelAboutToBeDeleted(" << model << ")"
-              << std::endl;
-#endif
-
-    for (ServerMap::iterator i = m_servers.begin(); i != m_servers.end(); ++i) {
-        
-        FFTDataServer *server = i->second.first;
-
-        if (server->getModel() == model) {
-
-#ifdef DEBUG_FFT_SERVER
-            std::cerr << "FFTDataServer::modelAboutToBeDeleted: server is "
-                      << server << std::endl;
-#endif
-
-            if (i->second.second > 0) {
-                cerr << "WARNING: FFTDataServer::modelAboutToBeDeleted: Model " << model << " (\"" << model->objectName() << "\") is about to be deleted, but is still being referred to by FFT server " << server << " with non-zero refcount " << i->second.second << endl;
-                server->suspendWrites();
-                return;
-            }
-            for (ServerQueue::iterator j = m_releasedServers.begin();
-                 j != m_releasedServers.end(); ++j) {
-                if (*j == server) {
-#ifdef DEBUG_FFT_SERVER
-                    std::cerr << "FFTDataServer::modelAboutToBeDeleted: erasing from released servers" << std::endl;
-#endif
-                    m_releasedServers.erase(j);
-                    break;
-                }
-            }
-#ifdef DEBUG_FFT_SERVER
-            std::cerr << "FFTDataServer::modelAboutToBeDeleted: erasing server" << std::endl;
-#endif
-            m_servers.erase(i);
-            delete server;
-            return;
-        }
-    }
-}
-
-FFTDataServer::FFTDataServer(QString fileBaseName,
-                             const DenseTimeValueModel *model,
-                             int channel,
-			     WindowType windowType,
-			     int windowSize,
-			     int windowIncrement,
-			     int fftSize,
-                             bool polar,
-                             StorageAdviser::Criteria criteria,
-                             sv_frame_t fillFromFrame) :
-    m_fileBaseName(fileBaseName),
-    m_model(model),
-    m_channel(channel),
-    m_windower(windowType, windowSize),
-    m_windowSize(windowSize),
-    m_windowIncrement(windowIncrement),
-    m_fftSize(fftSize),
-    m_polar(polar),
-    m_width(0),
-    m_height(0),
-    m_cacheWidth(0),
-    m_cacheWidthPower(0),
-    m_cacheWidthMask(0),
-    m_criteria(criteria),
-    m_fftInput(0),
-    m_exiting(false),
-    m_suspended(true), //!!! or false?
-    m_fillThread(0)
-{
-#ifdef DEBUG_FFT_SERVER
-    cerr << "FFTDataServer(" << this << " [" << (void *)QThread::currentThreadId() << "])::FFTDataServer" << endl;
-#endif
-
-    //!!! end is not correct until model finished reading -- what to do???
-
-    sv_frame_t start = m_model->getStartFrame();
-    sv_frame_t end = m_model->getEndFrame();
-
-    m_width = int((end - start) / m_windowIncrement) + 1;
-    m_height = m_fftSize / 2 + 1; // DC == 0, Nyquist == fftsize/2
-
-#ifdef DEBUG_FFT_SERVER 
-    cerr << "FFTDataServer(" << this << "): dimensions are "
-              << m_width << "x" << m_height << endl;
-#endif
-
-    int maxCacheSize = 20 * 1024 * 1024;
-    int columnSize = int(m_height * sizeof(fftsample) * 2 + sizeof(fftsample));
-    if (m_width < ((maxCacheSize * 2) / columnSize)) m_cacheWidth = m_width;
-    else m_cacheWidth = maxCacheSize / columnSize;
-    
-#ifdef DEBUG_FFT_SERVER
-    cerr << "FFTDataServer(" << this << "): cache width nominal "
-              << m_cacheWidth << ", actual ";
-#endif
-    
-    int bits = 0;
-    while (m_cacheWidth > 1) { m_cacheWidth >>= 1; ++bits; }
-    m_cacheWidthPower = bits + 1;
-    m_cacheWidth = 2;
-    while (bits) { m_cacheWidth <<= 1; --bits; }
-    m_cacheWidthMask = m_cacheWidth - 1;
-
-#ifdef DEBUG_FFT_SERVER
-    cerr << m_cacheWidth << " (power " << m_cacheWidthPower << ", mask "
-              << m_cacheWidthMask << ")" << endl;
-#endif
-
-    if (m_criteria == StorageAdviser::NoCriteria) {
-
-        // assume "spectrogram" criteria for polar ffts, and "feature
-        // extraction" criteria for rectangular ones.
-
-        if (m_polar) {
-            m_criteria = StorageAdviser::Criteria
-                (StorageAdviser::SpeedCritical |
-                 StorageAdviser::LongRetentionLikely);
-        } else {
-            m_criteria = StorageAdviser::Criteria
-                (StorageAdviser::PrecisionCritical);
-        }
-    }
-
-    for (int i = 0; i <= m_width / m_cacheWidth; ++i) {
-        m_caches.push_back(0);
-    }
-
-    m_fftInput = (fftsample *)
-        fftf_malloc(fftSize * sizeof(fftsample));
-
-    m_fftOutput = (fftf_complex *)
-        fftf_malloc((fftSize/2 + 1) * sizeof(fftf_complex));
-
-    m_workbuffer = (float *)
-        fftf_malloc((fftSize+2) * sizeof(float));
-
-    m_fftPlan = fftf_plan_dft_r2c_1d(m_fftSize,
-                                     m_fftInput,
-                                     m_fftOutput,
-                                     FFTW_MEASURE);
-
-    if (!m_fftPlan) {
-        cerr << "ERROR: fftf_plan_dft_r2c_1d(" << m_windowSize << ") failed!" << endl;
-        throw(0);
-    }
-
-    m_fillThread = new FillThread(*this, fillFromFrame);
-}
-
-FFTDataServer::~FFTDataServer()
-{
-#ifdef DEBUG_FFT_SERVER
-    cerr << "FFTDataServer(" << this << " [" << (void *)QThread::currentThreadId() << "])::~FFTDataServer()" << endl;
-#endif
-
-    m_suspended = false;
-    m_exiting = true;
-    m_condition.wakeAll();
-    if (m_fillThread) {
-        m_fillThread->wait();
-        delete m_fillThread;
-    }
-
-//    MutexLocker locker(&m_writeMutex,
-//                       "FFTDataServer::~FFTDataServer::m_writeMutex");
-
-    QMutexLocker mlocker(&m_fftBuffersLock);
-    QWriteLocker wlocker(&m_cacheVectorLock);
-
-    for (CacheVector::iterator i = m_caches.begin(); i != m_caches.end(); ++i) {
-        if (*i) {
-            delete *i;
-        }
-    }
-
-    deleteProcessingData();
-}
-
-void
-FFTDataServer::deleteProcessingData()
-{
-#ifdef DEBUG_FFT_SERVER
-    cerr << "FFTDataServer(" << this << " [" << (void *)QThread::currentThreadId() << "]): deleteProcessingData" << endl;
-#endif
-    if (m_fftInput) {
-        fftf_destroy_plan(m_fftPlan);
-        fftf_free(m_fftInput);
-        fftf_free(m_fftOutput);
-        fftf_free(m_workbuffer);
-    }
-    m_fftInput = 0;
-}
-
-void
-FFTDataServer::suspend()
-{
-#ifdef DEBUG_FFT_SERVER
-    cerr << "FFTDataServer(" << this << " [" << (void *)QThread::currentThreadId() << "]): suspend" << endl;
-#endif
-    Profiler profiler("FFTDataServer::suspend", false);
-
-    QMutexLocker locker(&m_fftBuffersLock);
-    m_suspended = true;
-}
-
-void
-FFTDataServer::suspendWrites()
-{
-#ifdef DEBUG_FFT_SERVER
-    cerr << "FFTDataServer(" << this << " [" << (void *)QThread::currentThreadId() << "]): suspendWrites" << endl;
-#endif
-    Profiler profiler("FFTDataServer::suspendWrites", false);
-
-    m_suspended = true;
-}
-
-void
-FFTDataServer::resume()
-{
-#ifdef DEBUG_FFT_SERVER
-    cerr << "FFTDataServer(" << this << " [" << (void *)QThread::currentThreadId() << "]): resume" << endl;
-#endif
-    Profiler profiler("FFTDataServer::resume", false);
-
-    m_suspended = false;
-    if (m_fillThread) {
-        if (m_fillThread->isFinished()) {
-            delete m_fillThread;
-            m_fillThread = 0;
-            deleteProcessingData();
-        } else if (!m_fillThread->isRunning()) {
-            m_fillThread->start();
-        } else {
-            m_condition.wakeAll();
-        }
-    }
-}
-
-void
-FFTDataServer::getStorageAdvice(int w, int h,
-                                bool &memoryCache, bool &compactCache)
-{
-    if (w < 0 || h < 0) throw std::domain_error("width & height must be non-negative");
-    size_t cells = size_t(w) * h;
-    size_t minimumSize = (cells / 1024) * sizeof(uint16_t); // kb
-    size_t maximumSize = (cells / 1024) * sizeof(float); // kb
-
-    // We don't have a compact rectangular representation, and compact
-    // of course is never precision-critical
-
-    bool canCompact = true;
-    if ((m_criteria & StorageAdviser::PrecisionCritical) || !m_polar) {
-        canCompact = false;
-        minimumSize = maximumSize; // don't use compact
-    }
-    
-    StorageAdviser::Recommendation recommendation;
-
-    try {
-
-        recommendation =
-            StorageAdviser::recommend(m_criteria, minimumSize, maximumSize);
-
-    } catch (InsufficientDiscSpace s) {
-
-        // Delete any unused servers we may have been leaving around
-        // in case we wanted them again
-
-        purgeLimbo(0);
-
-        // This time we don't catch InsufficientDiscSpace -- we
-        // haven't allocated anything yet and can safely let the
-        // exception out to indicate to the caller that we can't
-        // handle it.
-
-        recommendation =
-            StorageAdviser::recommend(m_criteria, minimumSize, maximumSize);
-    }
-
-//    cerr << "Recommendation was: " << recommendation << endl;
-
-    memoryCache = false;
-
-    if ((recommendation & StorageAdviser::UseMemory) ||
-        (recommendation & StorageAdviser::PreferMemory)) {
-        memoryCache = true;
-    }
-
-    compactCache = canCompact &&
-        (recommendation & StorageAdviser::ConserveSpace);
-
-#ifdef DEBUG_FFT_SERVER
-    cerr << "FFTDataServer: memory cache = " << memoryCache << ", compact cache = " << compactCache << endl;
-    
-    cerr << "Width " << w << " of " << m_width << ", height " << h << ", size " << w * h << endl;
-#endif
-}
-
-bool
-FFTDataServer::makeCache(int c)
-{
-    // Creating the cache could take a significant amount of time.  We
-    // don't want to block readers on m_cacheVectorLock while this is
-    // happening, but we do want to block any further calls to
-    // makeCache.  So we use this lock solely to serialise this
-    // particular function -- it isn't used anywhere else.
-
-    QMutexLocker locker(&m_cacheCreationMutex);
-
-    m_cacheVectorLock.lockForRead();
-    if (m_caches[c]) {
-        // someone else must have created the cache between our
-        // testing for it and taking the mutex
-        m_cacheVectorLock.unlock();
-        return true;
-    }
-    m_cacheVectorLock.unlock();
-
-    // Now m_cacheCreationMutex is held, but m_cacheVectorLock is not
-    // -- readers can proceed, but callers to this function will block
-
-    CacheBlock *cb = new CacheBlock;
-
-    QString name = QString("%1-%2").arg(m_fileBaseName).arg(c);
-
-    int width = m_cacheWidth;
-    if (c * m_cacheWidth + width > m_width) {
-        width = m_width - c * m_cacheWidth;
-    }
-
-    bool memoryCache = false;
-    bool compactCache = false;
-
-    getStorageAdvice(width, m_height, memoryCache, compactCache);
-
-    bool success = false;
-
-    if (memoryCache) {
-
-        try {
-
-            cb->memoryCache = new FFTMemoryCache
-                (compactCache ? FFTCache::Compact :
-                      m_polar ? FFTCache::Polar :
-                                FFTCache::Rectangular,
-                 width, m_height);
-
-            success = true;
-
-        } catch (std::bad_alloc) {
-
-            delete cb->memoryCache;
-            cb->memoryCache = 0;
-            
-            cerr << "WARNING: Memory allocation failed when creating"
-                      << " FFT memory cache no. " << c << " of " << width 
-                      << "x" << m_height << " (of total width " << m_width
-                      << "): falling back to disc cache" << endl;
-
-            memoryCache = false;
-        }
-    }
-
-    if (!memoryCache) {
-
-        try {
-        
-            cb->fileCacheWriter = new FFTFileCacheWriter
-                (name,
-                 compactCache ? FFTCache::Compact :
-                      m_polar ? FFTCache::Polar :
-                                FFTCache::Rectangular,
-                 width, m_height);
-
-            success = true;
-
-        } catch (std::exception &e) {
-
-            delete cb->fileCacheWriter;
-            cb->fileCacheWriter = 0;
-            
-            cerr << "ERROR: Failed to construct disc cache for FFT data: "
-                      << e.what() << endl;
-
-            throw;
-        }
-    }
-
-    m_cacheVectorLock.lockForWrite();
-
-    m_caches[c] = cb;
-
-    m_cacheVectorLock.unlock();
-
-    return success;
-}
- 
-bool
-FFTDataServer::makeCacheReader(int c)
-{
-    // preconditions: m_caches[c] exists and contains a file writer;
-    // m_cacheVectorLock is not locked by this thread
-#ifdef DEBUG_FFT_SERVER
-    std::cerr << "FFTDataServer::makeCacheReader(" << c << ")" << std::endl;
-#endif
-
-    QThread *me = QThread::currentThread();
-    QWriteLocker locker(&m_cacheVectorLock);
-    CacheBlock *cb(m_caches.at(c));
-    if (!cb || !cb->fileCacheWriter) return false;
-
-    try {
-        
-        cb->fileCacheReader[me] = new FFTFileCacheReader(cb->fileCacheWriter);
-
-    } catch (std::exception &e) {
-
-        delete cb->fileCacheReader[me];
-        cb->fileCacheReader.erase(me);
-            
-        cerr << "ERROR: Failed to construct disc cache reader for FFT data: "
-                  << e.what() << endl;
-        return false;
-    }
-
-    // erase a reader that looks like it may no longer going to be
-    // used by this thread for a while (leaving alone the current
-    // and previous cache readers)
-    int deleteCandidate = c - 2;
-    if (deleteCandidate < 0) deleteCandidate = c + 2;
-    if (deleteCandidate >= (int)m_caches.size()) {
-        return true;
-    }
-
-    cb = m_caches.at(deleteCandidate);
-    if (cb && cb->fileCacheReader.find(me) != cb->fileCacheReader.end()) {
-#ifdef DEBUG_FFT_SERVER
-        std::cerr << "FFTDataServer::makeCacheReader: Deleting probably unpopular reader " << deleteCandidate << " for this thread (as I create reader " << c << ")" << std::endl;
-#endif
-        delete cb->fileCacheReader[me];
-        cb->fileCacheReader.erase(me);
-    }
-            
-    return true;
-}
-       
-float
-FFTDataServer::getMagnitudeAt(int x, int y)
-{
-    Profiler profiler("FFTDataServer::getMagnitudeAt", false);
-
-    if (x >= m_width || y >= m_height) return 0;
-
-    float val = 0;
-
-    try {
-        int col;
-        FFTCacheReader *cache = getCacheReader(x, col);
-        if (!cache) return 0;
-
-        if (!cache->haveSetColumnAt(col)) {
-            if (getError() != "") return false;
-            Profiler profiler("FFTDataServer::getMagnitudeAt: filling");
-#ifdef DEBUG_FFT_SERVER
-            std::cerr << "FFTDataServer::getMagnitudeAt: calling fillColumn("
-                  << x << ")" << std::endl;
-#endif
-            fillColumn(x);
-        }
-
-        val = cache->getMagnitudeAt(col, y);
-
-    } catch (std::exception &e) {
-        m_error = e.what();
-    }
-
-    return val;
-}
-
-bool
-FFTDataServer::getMagnitudesAt(int x, float *values, int minbin, int count, int step)
-{
-    Profiler profiler("FFTDataServer::getMagnitudesAt", false);
-
-    if (x >= m_width) return false;
-
-    if (minbin >= m_height) minbin = m_height - 1;
-    if (count == 0) count = (m_height - minbin) / step;
-    else if (minbin + count * step > m_height) {
-        count = (m_height - minbin) / step;
-    }
-
-    try {
-        int col;
-        FFTCacheReader *cache = getCacheReader(x, col);
-        if (!cache) return false;
-
-        if (!cache->haveSetColumnAt(col)) {
-            if (getError() != "") return false;
-            Profiler profiler("FFTDataServer::getMagnitudesAt: filling");
-            fillColumn(x);
-        }
-
-        cache->getMagnitudesAt(col, values, minbin, count, step);
-
-    } catch (std::exception &e) {
-        m_error = e.what();
-        return false;
-    }
-
-    return true;
-}
-
-float
-FFTDataServer::getNormalizedMagnitudeAt(int x, int y)
-{
-    Profiler profiler("FFTDataServer::getNormalizedMagnitudeAt", false);
-
-    if (x >= m_width || y >= m_height) return 0;
-
-    float val = 0;
-
-    try {
-
-        int col;
-        FFTCacheReader *cache = getCacheReader(x, col);
-        if (!cache) return 0;
-
-        if (!cache->haveSetColumnAt(col)) {
-            if (getError() != "") return false;
-            Profiler profiler("FFTDataServer::getNormalizedMagnitudeAt: filling");
-            fillColumn(x);
-        }
-        val = cache->getNormalizedMagnitudeAt(col, y);
-
-    } catch (std::exception &e) {
-        m_error = e.what();
-    }
-
-    return val;
-}
-
-bool
-FFTDataServer::getNormalizedMagnitudesAt(int x, float *values, int minbin, int count, int step)
-{
-    Profiler profiler("FFTDataServer::getNormalizedMagnitudesAt", false);
-
-    if (x >= m_width) return false;
-
-    if (minbin >= m_height) minbin = m_height - 1;
-    if (count == 0) count = (m_height - minbin) / step;
-    else if (minbin + count * step > m_height) {
-        count = (m_height - minbin) / step;
-    }
-
-    try {
-
-        int col;
-        FFTCacheReader *cache = getCacheReader(x, col);
-        if (!cache) return false;
-
-        if (!cache->haveSetColumnAt(col)) {
-            if (getError() != "") return false;
-            Profiler profiler("FFTDataServer::getNormalizedMagnitudesAt: filling");
-            fillColumn(x);
-        }
-        
-        for (int i = 0; i < count; ++i) {
-            values[i] = cache->getNormalizedMagnitudeAt(col, i * step + minbin);
-        }
-        
-    } catch (std::exception &e) {
-        m_error = e.what();
-        return false;
-    }
-
-    return true;
-}
-
-float
-FFTDataServer::getMaximumMagnitudeAt(int x)
-{
-    Profiler profiler("FFTDataServer::getMaximumMagnitudeAt", false);
-
-    if (x >= m_width) return 0;
-
-    float val = 0;
-
-    try {
-
-        int col;
-        FFTCacheReader *cache = getCacheReader(x, col);
-        if (!cache) return 0;
-
-        if (!cache->haveSetColumnAt(col)) {
-            if (getError() != "") return false;
-            Profiler profiler("FFTDataServer::getMaximumMagnitudeAt: filling");
-            fillColumn(x);
-        }
-        val = cache->getMaximumMagnitudeAt(col);
-
-    } catch (std::exception &e) {
-        m_error = e.what();
-    }
-
-    return val;
-}
-
-float
-FFTDataServer::getPhaseAt(int x, int y)
-{
-    Profiler profiler("FFTDataServer::getPhaseAt", false);
-
-    if (x >= m_width || y >= m_height) return 0;
-
-    float val = 0;
-
-    try {
-
-        int col;
-        FFTCacheReader *cache = getCacheReader(x, col);
-        if (!cache) return 0;
-
-        if (!cache->haveSetColumnAt(col)) {
-            if (getError() != "") return false;
-            Profiler profiler("FFTDataServer::getPhaseAt: filling");
-            fillColumn(x);
-        }
-        val = cache->getPhaseAt(col, y);
-
-    } catch (std::exception &e) {
-        m_error = e.what();
-    }
-
-    return val;
-}
-
-bool
-FFTDataServer::getPhasesAt(int x, float *values, int minbin, int count, int step)
-{
-    Profiler profiler("FFTDataServer::getPhasesAt", false);
-
-    if (x >= m_width) return false;
-
-    if (minbin >= m_height) minbin = m_height - 1;
-    if (count == 0) count = (m_height - minbin) / step;
-    else if (minbin + count * step > m_height) {
-        count = (m_height - minbin) / step;
-    }
-
-    try {
-
-        int col;
-        FFTCacheReader *cache = getCacheReader(x, col);
-        if (!cache) return false;
-
-        if (!cache->haveSetColumnAt(col)) {
-            if (getError() != "") return false;
-            Profiler profiler("FFTDataServer::getPhasesAt: filling");
-            fillColumn(x);
-        }
-        
-        for (int i = 0; i < count; ++i) {
-            values[i] = cache->getPhaseAt(col, i * step + minbin);
-        }
-
-    } catch (std::exception &e) {
-        m_error = e.what();
-        return false;
-    }
-
-    return true;
-}
-
-void
-FFTDataServer::getValuesAt(int x, int y, float &real, float &imaginary)
-{
-    Profiler profiler("FFTDataServer::getValuesAt", false);
-
-    if (x >= m_width || y >= m_height) {
-        real = 0;
-        imaginary = 0;
-        return;
-    }
-
-    try {
-
-        int col;
-        FFTCacheReader *cache = getCacheReader(x, col);
-
-        if (!cache) {
-            real = 0;
-            imaginary = 0;
-            return;
-        }
-
-        if (!cache->haveSetColumnAt(col)) {
-            if (getError() != "") {
-                real = 0;
-                imaginary = 0;
-                return;
-            }
-            Profiler profiler("FFTDataServer::getValuesAt: filling");
-#ifdef DEBUG_FFT_SERVER
-            std::cerr << "FFTDataServer::getValuesAt(" << x << ", " << y << "): filling" << std::endl;
-#endif
-            fillColumn(x);
-        }        
-
-        cache->getValuesAt(col, y, real, imaginary);
-
-    } catch (std::exception &e) {
-        m_error = e.what();
-    }
-}
-
-bool
-FFTDataServer::getValuesAt(int x, float *reals, float *imaginaries, int minbin, int count, int step)
-{
-    Profiler profiler("FFTDataServer::getValuesAt", false);
-
-    if (x >= m_width) return false;
-
-    if (minbin >= m_height) minbin = m_height - 1;
-    if (count == 0) count = (m_height - minbin) / step;
-    else if (minbin + count * step > m_height) {
-        count = (m_height - minbin) / step;
-    }
-
-    try {
-
-        int col;
-        FFTCacheReader *cache = getCacheReader(x, col);
-        if (!cache) return false;
-
-        if (!cache->haveSetColumnAt(col)) {
-            if (getError() != "") return false;
-            Profiler profiler("FFTDataServer::getValuesAt: filling");
-            fillColumn(x);
-        }
-
-        for (int i = 0; i < count; ++i) {
-            cache->getValuesAt(col, i * step + minbin, reals[i], imaginaries[i]);
-        }
-
-    } catch (std::exception &e) {
-        m_error = e.what();
-        return false;
-    }
-
-    return true;
-}
-
-bool
-FFTDataServer::isColumnReady(int x)
-{
-    Profiler profiler("FFTDataServer::isColumnReady", false);
-
-    if (x >= m_width) return true;
-
-    if (!haveCache(x)) {
-/*!!!
-        if (m_lastUsedCache == -1) {
-            if (m_suspended) {
-                std::cerr << "FFTDataServer::isColumnReady(" << x << "): no cache, calling resume" << std::endl;
-                resume();
-            }
-            m_fillThread->start();
-        }
-*/
-        return false;
-    }
-
-    try {
-
-        int col;
-        FFTCacheReader *cache = getCacheReader(x, col);
-        if (!cache) return true;
-
-        return cache->haveSetColumnAt(col);
-
-    } catch (std::exception &e) {
-        m_error = e.what();
-        return false;
-    }
-}    
-
-void
-FFTDataServer::fillColumn(int x)
-{
-    Profiler profiler("FFTDataServer::fillColumn", false);
-
-    if (!m_model->isReady()) {
-        cerr << "WARNING: FFTDataServer::fillColumn(" 
-                  << x << "): model not yet ready" << endl;
-        return;
-    }
-/*
-    if (!m_fftInput) {
-        cerr << "WARNING: FFTDataServer::fillColumn(" << x << "): "
-                  << "input has already been completed and discarded?"
-                  << endl;
-        return;
-    }
-*/
-    if (x >= m_width) {
-        cerr << "WARNING: FFTDataServer::fillColumn(" << x << "): "
-                  << "x > width (" << x << " > " << m_width << ")"
-                  << endl;
-        return;
-    }
-
-    int col;
-#ifdef DEBUG_FFT_SERVER_FILL
-    cout << "FFTDataServer::fillColumn(" << x << ")" << endl;
-#endif
-    FFTCacheWriter *cache = getCacheWriter(x, col);
-    if (!cache) return;
-
-    int winsize = m_windowSize;
-    int fftsize = m_fftSize;
-    int hs = fftsize/2;
-
-    sv_frame_t pfx = 0;
-    int off = (fftsize - winsize) / 2;
-
-    sv_frame_t startFrame = m_windowIncrement * sv_frame_t(x);
-    sv_frame_t endFrame = startFrame + m_windowSize;
-
-    // FFT windows are centred at the respective audio sample frame,
-    // so the first one is centred at 0
-    startFrame -= winsize / 2;
-    endFrame   -= winsize / 2;
-
-#ifdef DEBUG_FFT_SERVER_FILL
-    std::cerr << "FFTDataServer::fillColumn: requesting frames "
-              << startFrame + pfx << " -> " << endFrame << " ( = "
-              << endFrame - (startFrame + pfx) << ") at index "
-              << off + pfx << " in buffer of size " << m_fftSize
-              << " with window size " << m_windowSize 
-              << " from channel " << m_channel << std::endl;
-#endif
-
-    QMutexLocker locker(&m_fftBuffersLock);
-
-    // We may have been called from a function that wanted to obtain a
-    // column using an FFTCacheReader.  Before calling us, it checked
-    // whether the column was available already, and the reader
-    // reported that it wasn't.  Now we test again, with the mutex
-    // held, to avoid a race condition in case another thread has
-    // called fillColumn at the same time.
-    if (cache->haveSetColumnAt(x & m_cacheWidthMask)) {
-        return;
-    }
-
-    if (!m_fftInput) {
-        cerr << "WARNING: FFTDataServer::fillColumn(" << x << "): "
-                  << "input has already been completed and discarded?"
-                  << endl;
-        return;
-    }
-
-    for (int i = 0; i < off; ++i) {
-        m_fftInput[i] = 0.0;
-    }
-
-    for (int i = 0; i < off; ++i) {
-        m_fftInput[fftsize - i - 1] = 0.0;
-    }
-
-    if (startFrame < 0) {
-	pfx = -startFrame;
-	for (int i = 0; i < pfx; ++i) {
-	    m_fftInput[off + i] = 0.0;
-	}
-    }
-
-    sv_frame_t count = 0;
-    if (endFrame > startFrame + pfx) count = endFrame - (startFrame + pfx);
-
-    sv_frame_t got = m_model->getData(m_channel, startFrame + pfx,
-                                      count, m_fftInput + off + pfx);
-
-    while (got + pfx < winsize) {
-	m_fftInput[off + got + pfx] = 0.0;
-	++got;
-    }
-
-    if (m_channel == -1) {
-	int channels = m_model->getChannelCount();
-	if (channels > 1) {
-	    for (int i = 0; i < winsize; ++i) {
-		m_fftInput[off + i] /= float(channels);
-	    }
-	}
-    }
-
-    m_windower.cut(m_fftInput + off);
-
-    for (int i = 0; i < hs; ++i) {
-	fftsample temp = m_fftInput[i];
-	m_fftInput[i] = m_fftInput[i + hs];
-	m_fftInput[i + hs] = temp;
-    }
-
-    fftf_execute(m_fftPlan);
-
-    float factor = 0.f;
-
-    if (cache->getStorageType() == FFTCache::Compact ||
-        cache->getStorageType() == FFTCache::Polar) {
-
-        for (int i = 0; i <= hs; ++i) {
-            fftsample real = m_fftOutput[i][0];
-            fftsample imag = m_fftOutput[i][1];
-            float mag = sqrtf(real * real + imag * imag);
-            m_workbuffer[i] = mag;
-            m_workbuffer[i + hs + 1] = atan2f(imag, real);
-            if (mag > factor) factor = mag;
-        }
-
-    } else {
-
-        for (int i = 0; i <= hs; ++i) {
-            m_workbuffer[i] = m_fftOutput[i][0];
-            m_workbuffer[i + hs + 1] = m_fftOutput[i][1];
-        }
-    }
-
-    Profiler subprof("FFTDataServer::fillColumn: set to cache");
-
-    if (cache->getStorageType() == FFTCache::Compact ||
-        cache->getStorageType() == FFTCache::Polar) {
-            
-        cache->setColumnAt(col,
-                           m_workbuffer,
-                           m_workbuffer + hs + 1,
-                           factor);
-
-    } else {
-
-        cache->setColumnAt(col,
-                           m_workbuffer,
-                           m_workbuffer + hs + 1);
-    }
-
-    if (m_suspended) {
-//        std::cerr << "FFTDataServer::fillColumn(" << x << "): calling resume" << std::endl;
-//        resume();
-    }
-}    
-
-void
-FFTDataServer::fillComplete()
-{
-    for (int i = 0; i < int(m_caches.size()); ++i) {
-        if (!m_caches[i]) continue;
-        if (m_caches[i]->memoryCache) {
-            m_caches[i]->memoryCache->allColumnsWritten();
-        }
-        if (m_caches[i]->fileCacheWriter) {
-            m_caches[i]->fileCacheWriter->allColumnsWritten();
-        }
-    }
-}
-
-QString
-FFTDataServer::getError() const
-{
-    QString err;
-    if (m_error != "") {
-        err = m_error;
-//        cerr << "FFTDataServer::getError: err (server " << this << ") = " << err << endl;
-    } else {
-        MutexLocker locker(&m_fftBuffersLock, "FFTDataServer::getError");
-        if (m_fillThread) {
-            err = m_fillThread->getError();
-//            cerr << "FFTDataServer::getError: err (server " << this << ", from thread " << m_fillThread
-//                 << ") = " << err << endl;
-        }
-    }
-    return err;
-}
-
-int
-FFTDataServer::getFillCompletion() const 
-{
-    if (m_fillThread) return m_fillThread->getCompletion();
-    else return 100;
-}
-
-sv_frame_t
-FFTDataServer::getFillExtent() const
-{
-    if (m_fillThread) return m_fillThread->getExtent();
-    else return m_model->getEndFrame();
-}
-
-QString
-FFTDataServer::generateFileBasename() const
-{
-    return generateFileBasename(m_model, m_channel, m_windower.getType(),
-                                m_windowSize, m_windowIncrement, m_fftSize,
-                                m_polar);
-}
-
-QString
-FFTDataServer::generateFileBasename(const DenseTimeValueModel *model,
-                                    int channel,
-                                    WindowType windowType,
-                                    int windowSize,
-                                    int windowIncrement,
-                                    int fftSize,
-                                    bool polar)
-{
-    return QString("%1-%2-%3-%4-%5-%6%7")
-        .arg(XmlExportable::getObjectExportId(model))
-        .arg(channel + 1)
-        .arg((int)windowType)
-        .arg(windowSize)
-        .arg(windowIncrement)
-        .arg(fftSize)
-        .arg(polar ? "-p" : "-r");
-}
-
-void
-FFTDataServer::FillThread::run()
-{
-#ifdef DEBUG_FFT_SERVER_FILL
-    std::cerr << "FFTDataServer::FillThread::run()" << std::endl;
-#endif
-    
-    m_extent = 0;
-    m_completion = 0;
-    
-    while (!m_server.m_model->isReady() && !m_server.m_exiting) {
-#ifdef DEBUG_FFT_SERVER_FILL
-        std::cerr << "FFTDataServer::FillThread::run(): waiting for model " << m_server.m_model << " to be ready" << std::endl;
-#endif
-        sleep(1);
-    }
-    if (m_server.m_exiting) return;
-
-    sv_frame_t start = m_server.m_model->getStartFrame();
-    sv_frame_t end = m_server.m_model->getEndFrame();
-    sv_frame_t remainingEnd = end;
-
-    int counter = 0;
-    int updateAt = 1;
-    int maxUpdateAt = int(end / m_server.m_windowIncrement) / 20;
-    if (maxUpdateAt < 100) maxUpdateAt = 100;
-
-    if (m_fillFrom > start) {
-
-        for (sv_frame_t f = m_fillFrom; f < end; f += m_server.m_windowIncrement) {
-	    
-            try {
-                m_server.fillColumn(int((f - start) / m_server.m_windowIncrement));
-            } catch (std::exception &e) {
-                MutexLocker locker(&m_server.m_fftBuffersLock,
-                                   "FFTDataServer::run::m_fftBuffersLock [err]");
-                m_threadError = e.what();
-                std::cerr << "FFTDataServer::FillThread::run: exception: " << m_threadError << " (thread = " << this << " from server " << &m_server << ")" << std::endl;
-                m_server.fillComplete();
-                m_completion = 100;
-                m_extent = end;
-                return;
-            }
-
-            if (m_server.m_exiting) return;
-
-            while (m_server.m_suspended) {
-#ifdef DEBUG_FFT_SERVER
-                cerr << "FFTDataServer(" << this << " [" << (void *)QThread::currentThreadId() << "]): suspended, waiting..." << endl;
-#endif
-                MutexLocker locker(&m_server.m_fftBuffersLock,
-                                   "FFTDataServer::run::m_fftBuffersLock [1]");
-                if (m_server.m_suspended && !m_server.m_exiting) {
-                    m_server.m_condition.wait(&m_server.m_fftBuffersLock, 10000);
-                }
-#ifdef DEBUG_FFT_SERVER
-                cerr << "FFTDataServer(" << this << " [" << (void *)QThread::currentThreadId() << "]): waited" << endl;
-#endif
-                if (m_server.m_exiting) return;
-            }
-
-            if (++counter == updateAt) {
-                m_extent = f;
-                m_completion = int(100 * fabsf(float(f - m_fillFrom) /
-                                                  float(end - start)));
-                counter = 0;
-                if (updateAt < maxUpdateAt) {
-                    updateAt *= 2;
-                    if (updateAt > maxUpdateAt) updateAt = maxUpdateAt;
-                }
-            }
-        }
-
-        remainingEnd = m_fillFrom;
-        if (remainingEnd > start) --remainingEnd;
-        else remainingEnd = start;
-    }
-
-    int baseCompletion = m_completion;
-
-    for (sv_frame_t f = start; f < remainingEnd; f += m_server.m_windowIncrement) {
-
-        try {
-            m_server.fillColumn(int((f - start) / m_server.m_windowIncrement));
-        } catch (std::exception &e) {
-            MutexLocker locker(&m_server.m_fftBuffersLock,
-                               "FFTDataServer::run::m_fftBuffersLock [err]");
-            m_threadError = e.what();
-                std::cerr << "FFTDataServer::FillThread::run: exception: " << m_threadError << " (thread = " << this << " from server " << &m_server << ")" << std::endl;
-            m_server.fillComplete();
-            m_completion = 100;
-            m_extent = end;
-            return;
-        }
-
-        if (m_server.m_exiting) return;
-
-        while (m_server.m_suspended) {
-#ifdef DEBUG_FFT_SERVER
-            cerr << "FFTDataServer(" << this << " [" << (void *)QThread::currentThreadId() << "]): suspended, waiting..." << endl;
-#endif
-            {
-                MutexLocker locker(&m_server.m_fftBuffersLock,
-                                   "FFTDataServer::run::m_fftBuffersLock [2]");
-                if (m_server.m_suspended && !m_server.m_exiting) {
-                    m_server.m_condition.wait(&m_server.m_fftBuffersLock, 10000);
-                }
-            }
-            if (m_server.m_exiting) return;
-        }
-		    
-        if (++counter == updateAt) {
-            m_extent = f;
-            m_completion = baseCompletion +
-                int(100 * fabsf(float(f - start) /
-                                   float(end - start)));
-            counter = 0;
-            if (updateAt < maxUpdateAt) {
-                updateAt *= 2;
-                if (updateAt > maxUpdateAt) updateAt = maxUpdateAt;
-            }
-        }
-    }
-
-    m_server.fillComplete();
-    m_completion = 100;
-    m_extent = end;
-
-#ifdef DEBUG_FFT_SERVER
-    std::cerr << "FFTDataServer::FillThread::run exiting" << std::endl;
-#endif
-}
-
--- a/data/fft/FFTDataServer.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,295 +0,0 @@
-/* -*- 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 file copyright 2006 Chris Cannam and QMUL.
-    
-    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 _FFT_DATA_SERVER_H_
-#define _FFT_DATA_SERVER_H_
-
-#include "base/Window.h"
-#include "base/Thread.h"
-#include "base/StorageAdviser.h"
-
-#include "FFTapi.h"
-#include "FFTFileCacheReader.h"
-#include "FFTFileCacheWriter.h"
-#include "FFTMemoryCache.h"
-
-#include <QMutex>
-#include <QReadWriteLock>
-#include <QReadLocker>
-#include <QWaitCondition>
-#include <QString>
-
-#include <vector>
-#include <deque>
-
-class DenseTimeValueModel;
-class Model;
-
-class FFTDataServer
-{
-public:
-    static FFTDataServer *getInstance(const DenseTimeValueModel *model,
-                                      int channel,
-                                      WindowType windowType,
-                                      int windowSize,
-                                      int windowIncrement,
-                                      int fftSize,
-                                      bool polar,
-                                      StorageAdviser::Criteria criteria =
-                                          StorageAdviser::NoCriteria,
-                                      sv_frame_t fillFromFrame = 0);
-
-    static FFTDataServer *getFuzzyInstance(const DenseTimeValueModel *model,
-                                           int channel,
-                                           WindowType windowType,
-                                           int windowSize,
-                                           int windowIncrement,
-                                           int fftSize,
-                                           bool polar,
-                                           StorageAdviser::Criteria criteria =
-                                               StorageAdviser::NoCriteria,
-                                           sv_frame_t fillFromFrame = 0);
-
-    static void claimInstance(FFTDataServer *);
-    static void releaseInstance(FFTDataServer *);
-
-    static void modelAboutToBeDeleted(Model *);
-
-    const DenseTimeValueModel *getModel() const { return m_model; }
-    int        getChannel() const { return m_channel; }
-    WindowType getWindowType() const { return m_windower.getType(); }
-    int        getWindowSize() const { return m_windowSize; }
-    int        getWindowIncrement() const { return m_windowIncrement; }
-    int        getFFTSize() const { return m_fftSize; }
-    bool       getPolar() const { return m_polar; }
-
-    int        getWidth() const  { return m_width;  }
-    int        getHeight() const { return m_height; }
-
-    float      getMagnitudeAt(int x, int y);
-    float      getNormalizedMagnitudeAt(int x, int y);
-    float      getMaximumMagnitudeAt(int x);
-    float      getPhaseAt(int x, int y);
-    void       getValuesAt(int x, int y, float &real, float &imaginary);
-    bool       isColumnReady(int x);
-
-    bool       getMagnitudesAt(int x, float *values, int minbin = 0, int count = 0, int step = 1);
-    bool       getNormalizedMagnitudesAt(int x, float *values, int minbin = 0, int count = 0, int step = 1);
-    bool       getPhasesAt(int x, float *values, int minbin = 0, int count = 0, int step = 1);
-    bool       getValuesAt(int x, float *reals, float *imaginaries, int minbin = 0, int count = 0, int step = 1);
-
-    void       suspend();
-    void       suspendWrites();
-    void       resume(); // also happens automatically if new data needed
-
-    // Convenience functions:
-
-    bool isLocalPeak(int x, int y) {
-        float mag = getMagnitudeAt(x, y);
-        if (y > 0 && mag < getMagnitudeAt(x, y - 1)) return false;
-        if (y < getHeight()-1 && mag < getMagnitudeAt(x, y + 1)) return false;
-        return true;
-    }
-    bool isOverThreshold(int x, int y, float threshold) {
-        return getMagnitudeAt(x, y) > threshold;
-    }
-
-    QString getError() const;
-    int getFillCompletion() const;
-    sv_frame_t getFillExtent() const;
-
-private:
-    FFTDataServer(QString fileBaseName,
-                  const DenseTimeValueModel *model,
-                  int channel,
-                  WindowType windowType,
-                  int windowSize,
-                  int windowIncrement,
-                  int fftSize,
-                  bool polar,
-                  StorageAdviser::Criteria criteria,
-                  sv_frame_t fillFromFrame = 0);
-
-    virtual ~FFTDataServer();
-
-    FFTDataServer(const FFTDataServer &); // not implemented
-    FFTDataServer &operator=(const FFTDataServer &); // not implemented
-
-    typedef float fftsample;
-
-    QString m_fileBaseName;
-    const DenseTimeValueModel *m_model;
-    int m_channel;
-
-    Window<fftsample> m_windower;
-
-    int m_windowSize;
-    int m_windowIncrement;
-    int m_fftSize;
-    bool m_polar;
-
-    int m_width;
-    int m_height;
-    int m_cacheWidth;
-    int m_cacheWidthPower;
-    int m_cacheWidthMask;
-
-    struct CacheBlock {
-        FFTMemoryCache *memoryCache;
-        typedef std::map<QThread *, FFTFileCacheReader *> ThreadReaderMap;
-        ThreadReaderMap fileCacheReader;
-        FFTFileCacheWriter *fileCacheWriter;
-        CacheBlock() : memoryCache(0), fileCacheWriter(0) { }
-        ~CacheBlock() {
-            delete memoryCache; 
-            while (!fileCacheReader.empty()) {
-                delete fileCacheReader.begin()->second;
-                fileCacheReader.erase(fileCacheReader.begin());
-            }
-            delete fileCacheWriter;
-        }
-    };
-
-    typedef std::vector<CacheBlock *> CacheVector;
-    CacheVector m_caches;
-    QReadWriteLock m_cacheVectorLock; // locks cache lookup, not use
-    QMutex m_cacheCreationMutex; // solely to serialise makeCache() calls
-
-    FFTCacheReader *getCacheReader(int x, int &col) {
-        Profiler profiler("FFTDataServer::getCacheReader");
-        col = x & m_cacheWidthMask;
-        int c = x >> m_cacheWidthPower;
-        m_cacheVectorLock.lockForRead();
-        CacheBlock *cb(m_caches.at(c));
-        if (cb) {
-            if (cb->memoryCache) {
-                m_cacheVectorLock.unlock();
-                return cb->memoryCache;
-            }
-            if (cb->fileCacheWriter) {
-                QThread *me = QThread::currentThread();
-                CacheBlock::ThreadReaderMap &map = cb->fileCacheReader;
-                if (map.find(me) == map.end()) {
-                    m_cacheVectorLock.unlock();
-                    if (!makeCacheReader(c)) return 0;
-                    return getCacheReader(x, col);
-                }
-                FFTCacheReader *reader = cb->fileCacheReader[me];
-                m_cacheVectorLock.unlock();
-                return reader;
-            }
-            // if cb exists but cb->fileCacheWriter doesn't, creation
-            // must have failed: don't try again
-            m_cacheVectorLock.unlock();
-            return 0;
-        }
-        m_cacheVectorLock.unlock();
-        if (getError() != "") return 0;
-        if (!makeCache(c)) return 0;
-        return getCacheReader(x, col);
-    }
-    
-    FFTCacheWriter *getCacheWriter(int x, int &col) {
-        Profiler profiler("FFTDataServer::getCacheWriter");
-        col = x & m_cacheWidthMask;
-        int c = x >> m_cacheWidthPower;
-        {
-            QReadLocker locker(&m_cacheVectorLock);
-            CacheBlock *cb(m_caches.at(c));
-            if (cb) {
-                if (cb->memoryCache) return cb->memoryCache;
-                if (cb->fileCacheWriter) return cb->fileCacheWriter;
-                // if cb exists, creation must have failed: don't try again
-                return 0;
-            }
-        }
-        if (!makeCache(c)) return 0;
-        return getCacheWriter(x, col);
-    }
-
-    bool haveCache(int x) {
-        int c = x >> m_cacheWidthPower;
-        return (m_caches.at(c) != 0);
-    }
-    
-    bool makeCache(int c);
-    bool makeCacheReader(int c);
-    
-    StorageAdviser::Criteria m_criteria;
-
-    void getStorageAdvice(int w, int h, bool &memory, bool &compact);
-        
-    mutable QMutex m_fftBuffersLock;
-    QWaitCondition m_condition;
-
-    fftsample *m_fftInput;
-    fftf_complex *m_fftOutput;
-    float *m_workbuffer;
-    fftf_plan m_fftPlan;
-
-    class FillThread : public Thread
-    {
-    public:
-        FillThread(FFTDataServer &server, sv_frame_t fillFromFrame) :
-            m_server(server), m_extent(0), m_completion(0),
-            m_fillFrom(fillFromFrame) { }
-
-        sv_frame_t getExtent() const { return m_extent; }
-        int getCompletion() const { return m_completion ? m_completion : 1; }
-        QString getError() const { return m_threadError; }
-        virtual void run();
-
-    protected:
-        FFTDataServer &m_server;
-        sv_frame_t m_extent;
-        int m_completion;
-        sv_frame_t m_fillFrom;
-        QString m_threadError;
-    };
-
-    bool m_exiting;
-    bool m_suspended;
-    FillThread *m_fillThread;
-    QString m_error;
-
-    void deleteProcessingData();
-    void fillColumn(int x);
-    void fillComplete();
-
-    QString generateFileBasename() const;
-    static QString generateFileBasename(const DenseTimeValueModel *model,
-                                        int channel,
-                                        WindowType windowType,
-                                        int windowSize,
-                                        int windowIncrement,
-                                        int fftSize,
-                                        bool polar);
-
-    typedef std::pair<FFTDataServer *, int> ServerCountPair;
-    typedef std::map<QString, ServerCountPair> ServerMap;
-    typedef std::deque<FFTDataServer *> ServerQueue;
-
-    static ServerMap m_servers;
-    static ServerQueue m_releasedServers; // these are still in m_servers as well, with zero refcount
-    static QMutex m_serverMapMutex;
-    static FFTDataServer *findServer(QString); // call with serverMapMutex held
-    static void purgeLimbo(int maxSize = 3); // call with serverMapMutex held
-
-    static void claimInstance(FFTDataServer *, bool needLock);
-    static void releaseInstance(FFTDataServer *, bool needLock);
-
-};
-
-#endif
--- a/data/fft/FFTFileCacheReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,281 +0,0 @@
-/* -*- 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 file copyright 2006-2009 Chris Cannam and QMUL.
-    
-    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 "FFTFileCacheReader.h"
-#include "FFTFileCacheWriter.h"
-
-#include "fileio/MatrixFile.h"
-
-#include "base/Profiler.h"
-#include "base/Thread.h"
-#include "base/Exceptions.h"
-
-#include <iostream>
-
-//#define DEBUG_FFT_FILE_CACHE_READER 1
-
-// The underlying matrix has height (m_height * 2 + 1).  In each
-// column we store magnitude at [0], [2] etc and phase at [1], [3]
-// etc, and then store the normalization factor (maximum magnitude) at
-// [m_height * 2].  In compact mode, the factor takes two cells.
-
-FFTFileCacheReader::FFTFileCacheReader(FFTFileCacheWriter *writer) :
-    m_readbuf(0),
-    m_readbufCol(0),
-    m_readbufWidth(0),
-    m_readbufGood(false),
-    m_storageType(writer->getStorageType()),
-    m_factorSize(m_storageType == FFTCache::Compact ? 2 : 1),
-    m_mfc(new MatrixFile
-          (writer->getFileBase(),
-           MatrixFile::ReadOnly,
-           int((m_storageType == FFTCache::Compact) ? sizeof(uint16_t) : sizeof(float)),
-           writer->getWidth(),
-           writer->getHeight() * 2 + m_factorSize))
-{
-#ifdef DEBUG_FFT_FILE_CACHE_READER
-    cerr << "FFTFileCacheReader: storage type is " << (m_storageType == FFTCache::Compact ? "Compact" : m_storageType == FFTCache::Polar ? "Polar" : "Rectangular") << endl;
-#endif
-}
-
-FFTFileCacheReader::~FFTFileCacheReader()
-{
-    if (m_readbuf) delete[] m_readbuf;
-    delete m_mfc;
-}
-
-int
-FFTFileCacheReader::getWidth() const
-{
-    return m_mfc->getWidth();
-}
-
-int
-FFTFileCacheReader::getHeight() const
-{
-    int mh = m_mfc->getHeight();
-    if (mh > m_factorSize) return (mh - m_factorSize) / 2;
-    else return 0;
-}
-
-float
-FFTFileCacheReader::getMagnitudeAt(int x, int y) const
-{
-    Profiler profiler("FFTFileCacheReader::getMagnitudeAt", false);
-
-    float value = 0.f;
-
-    switch (m_storageType) {
-
-    case FFTCache::Compact:
-        value = (getFromReadBufCompactUnsigned(x, y * 2) / 65535.f)
-            * getNormalizationFactor(x);
-        break;
-
-    case FFTCache::Rectangular:
-    {
-        float real, imag;
-        getValuesAt(x, y, real, imag);
-        value = sqrtf(real * real + imag * imag);
-        break;
-    }
-
-    case FFTCache::Polar:
-        value = getFromReadBufStandard(x, y * 2);
-        break;
-    }
-
-    return value;
-}
-
-float
-FFTFileCacheReader::getNormalizedMagnitudeAt(int x, int y) const
-{
-    float value = 0.f;
-
-    switch (m_storageType) {
-
-    case FFTCache::Compact:
-        value = getFromReadBufCompactUnsigned(x, y * 2) / 65535.f;
-        break;
-
-    case FFTCache::Rectangular:
-    case FFTCache::Polar:
-    {
-        float mag = getMagnitudeAt(x, y);
-        float factor = getNormalizationFactor(x);
-        if (factor != 0) value = mag / factor;
-        else value = 0.f;
-        break;
-    }
-    }
-
-    return value;
-}
-
-float
-FFTFileCacheReader::getMaximumMagnitudeAt(int x) const
-{
-    return getNormalizationFactor(x);
-}
-
-float
-FFTFileCacheReader::getPhaseAt(int x, int y) const
-{
-    float value = 0.f;
-    
-    switch (m_storageType) {
-
-    case FFTCache::Compact:
-        value = (getFromReadBufCompactSigned(x, y * 2 + 1) / 32767.f) * float(M_PI);
-        break;
-
-    case FFTCache::Rectangular:
-    {
-        float real, imag;
-        getValuesAt(x, y, real, imag);
-        value = atan2f(imag, real);
-        break;
-    }
-
-    case FFTCache::Polar:
-        value = getFromReadBufStandard(x, y * 2 + 1);
-        break;
-    }
-
-    return value;
-}
-
-void
-FFTFileCacheReader::getValuesAt(int x, int y, float &real, float &imag) const
-{
-//    SVDEBUG << "FFTFileCacheReader::getValuesAt(" << x << "," << y << ")" << endl;
-
-    switch (m_storageType) {
-
-    case FFTCache::Rectangular:
-        real = getFromReadBufStandard(x, y * 2);
-        imag = getFromReadBufStandard(x, y * 2 + 1);
-        return;
-
-    case FFTCache::Compact:
-    case FFTCache::Polar:
-        float mag = getMagnitudeAt(x, y);
-        float phase = getPhaseAt(x, y);
-        real = mag * cosf(phase);
-        imag = mag * sinf(phase);
-        return;
-    }
-}
-
-void
-FFTFileCacheReader::getMagnitudesAt(int x, float *values, int minbin, int count, int step) const
-{
-    Profiler profiler("FFTFileCacheReader::getMagnitudesAt");
-
-    switch (m_storageType) {
-
-    case FFTCache::Compact:
-        for (int i = 0; i < count; ++i) {
-            int y = minbin + i * step;
-            values[i] = (getFromReadBufCompactUnsigned(x, y * 2) / 65535.f)
-                * getNormalizationFactor(x);
-        }
-        break;
-
-    case FFTCache::Rectangular:
-    {
-        float real, imag;
-        for (int i = 0; i < count; ++i) {
-            int y = minbin + i * step;
-            real = getFromReadBufStandard(x, y * 2);
-            imag = getFromReadBufStandard(x, y * 2 + 1);
-            values[i] = sqrtf(real * real + imag * imag);
-        }
-        break;
-    }
-
-    case FFTCache::Polar:
-        for (int i = 0; i < count; ++i) {
-            int y = minbin + i * step;
-            values[i] = getFromReadBufStandard(x, y * 2);
-        }
-        break;
-    }
-}
-
-bool
-FFTFileCacheReader::haveSetColumnAt(int x) const
-{
-    if (m_readbuf && m_readbufGood &&
-        (m_readbufCol == x || (m_readbufWidth > 1 && m_readbufCol+1 == x))) {
-//        SVDEBUG << "FFTFileCacheReader::haveSetColumnAt: short-circuiting; we know about this one" << endl;
-        return true;
-    }
-    return m_mfc->haveSetColumnAt(x);
-}
-
-size_t
-FFTFileCacheReader::getCacheSize(int width, int height,
-                                 FFTCache::StorageType type)
-{
-    return (height * 2 + (type == FFTCache::Compact ? 2 : 1)) * width *
-        (type == FFTCache::Compact ? sizeof(uint16_t) : sizeof(float)) +
-        2 * sizeof(int); // matrix file header size
-}
-
-void
-FFTFileCacheReader::populateReadBuf(int x) const
-{
-    Profiler profiler("FFTFileCacheReader::populateReadBuf", false);
-
-//    SVDEBUG << "FFTFileCacheReader::populateReadBuf(" << x << ")" << endl;
-
-    if (!m_readbuf) {
-        m_readbuf = new char[m_mfc->getHeight() * 2 * m_mfc->getCellSize()];
-    }
-
-    m_readbufGood = false;
-
-    try {
-        bool good = false;
-        if (m_mfc->haveSetColumnAt(x)) {
-            // If the column is not available, we have no obligation
-            // to do anything with the readbuf -- we can cheerfully
-            // return garbage.  It's the responsibility of the caller
-            // to check haveSetColumnAt before trusting any retrieved
-            // data.  However, we do record whether the data in the
-            // readbuf is good or not, because we can use that to
-            // return an immediate result for haveSetColumnAt if the
-            // column is right.
-            good = true;
-            m_mfc->getColumnAt(x, m_readbuf);
-        }
-        if (m_mfc->haveSetColumnAt(x + 1)) {
-            m_mfc->getColumnAt
-                (x + 1, m_readbuf + m_mfc->getCellSize() * m_mfc->getHeight());
-            m_readbufWidth = 2;
-        } else {
-            m_readbufWidth = 1;
-        }
-        m_readbufGood = good;
-    } catch (FileReadFailed f) {
-        cerr << "ERROR: FFTFileCacheReader::populateReadBuf: File read failed: "
-                  << f.what() << endl;
-        memset(m_readbuf, 0, m_mfc->getHeight() * 2 * m_mfc->getCellSize());
-    }
-    m_readbufCol = x;
-}
-
--- a/data/fft/FFTFileCacheReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,123 +0,0 @@
-/* -*- 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 file copyright 2006-2009 Chris Cannam and QMUL.
-    
-    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 _FFT_FILE_CACHE_READER_H_
-#define _FFT_FILE_CACHE_READER_H_
-
-#include "data/fileio/MatrixFile.h"
-#include "FFTCacheReader.h"
-#include "FFTCacheStorageType.h"
-
-class FFTFileCacheWriter;
-
-class FFTFileCacheReader : public FFTCacheReader
-{
-public:
-    FFTFileCacheReader(FFTFileCacheWriter *);
-    ~FFTFileCacheReader();
-
-    int getWidth() const;
-    int getHeight() const;
-	
-    float getMagnitudeAt(int x, int y) const;
-    float getNormalizedMagnitudeAt(int x, int y) const;
-    float getMaximumMagnitudeAt(int x) const;
-    float getPhaseAt(int x, int y) const;
-
-    void getValuesAt(int x, int y, float &real, float &imag) const;
-    void getMagnitudesAt(int x, float *values, int minbin, int count, int step) const;
-
-    bool haveSetColumnAt(int x) const;
-
-    static size_t getCacheSize(int width, int height,
-                               FFTCache::StorageType type);
-
-    FFTCache::StorageType getStorageType() const { return m_storageType; }
-
-protected:
-    mutable char *m_readbuf;
-    mutable int m_readbufCol;
-    mutable int m_readbufWidth;
-    mutable bool m_readbufGood;
-
-    float getFromReadBufStandard(int x, int y) const {
-        float v;
-        if (m_readbuf &&
-            (m_readbufCol == x || (m_readbufWidth > 1 && m_readbufCol+1 == x))) {
-            v = ((float *)m_readbuf)[(x - m_readbufCol) * m_mfc->getHeight() + y];
-            return v;
-        } else {
-            populateReadBuf(x);
-            v = getFromReadBufStandard(x, y);
-            return v;
-        }
-    }
-
-    float getFromReadBufCompactUnsigned(int x, int y) const {
-        float v;
-        if (m_readbuf &&
-            (m_readbufCol == x || (m_readbufWidth > 1 && m_readbufCol+1 == x))) {
-            v = ((uint16_t *)m_readbuf)[(x - m_readbufCol) * m_mfc->getHeight() + y];
-            return v;
-        } else {
-            populateReadBuf(x);
-            v = getFromReadBufCompactUnsigned(x, y);
-            return v;
-        }
-    }
-
-    float getFromReadBufCompactSigned(int x, int y) const {
-        float v;
-        if (m_readbuf &&
-            (m_readbufCol == x || (m_readbufWidth > 1 && m_readbufCol+1 == x))) {
-            v = ((int16_t *)m_readbuf)[(x - m_readbufCol) * m_mfc->getHeight() + y];
-            return v;
-        } else {
-            populateReadBuf(x);
-            v = getFromReadBufCompactSigned(x, y);
-            return v;
-        }
-    }
-
-    void populateReadBuf(int x) const;
-
-    float getNormalizationFactor(int col) const {
-        int h = m_mfc->getHeight();
-        if (h < m_factorSize) return 0;
-        if (m_storageType != FFTCache::Compact) {
-            return getFromReadBufStandard(col, h - 1);
-        } else {
-            union {
-                float f;
-                uint16_t u[2];
-            } factor;
-            if (!m_readbuf ||
-                !(m_readbufCol == col ||
-                  (m_readbufWidth > 1 && m_readbufCol+1 == col))) {
-                populateReadBuf(col);
-            }
-            int ix = (col - m_readbufCol) * m_mfc->getHeight() + h;
-            factor.u[0] = ((uint16_t *)m_readbuf)[ix - 2];
-            factor.u[1] = ((uint16_t *)m_readbuf)[ix - 1];
-            return factor.f;
-        }
-    }
- 
-    FFTCache::StorageType m_storageType;
-    int m_factorSize;
-    MatrixFile *m_mfc;
-};
-
-#endif
--- a/data/fft/FFTFileCacheWriter.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,195 +0,0 @@
-/* -*- 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 file copyright 2006-2009 Chris Cannam and QMUL.
-    
-    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 "FFTFileCacheWriter.h"
-
-#include "fileio/MatrixFile.h"
-
-#include "base/Profiler.h"
-#include "base/Thread.h"
-#include "base/Exceptions.h"
-
-#include <iostream>
-
-//#define DEBUG_FFT_FILE_CACHE_WRITER 1
-
-
-// The underlying matrix has height (m_height * 2 + 1).  In each
-// column we store magnitude at [0], [2] etc and phase at [1], [3]
-// etc, and then store the normalization factor (maximum magnitude) at
-// [m_height * 2].  In compact mode, the factor takes two cells.
-
-FFTFileCacheWriter::FFTFileCacheWriter(QString fileBase,
-                                       FFTCache::StorageType storageType,
-                                       int width, int height) :
-    m_writebuf(0),
-    m_fileBase(fileBase),
-    m_storageType(storageType),
-    m_factorSize(storageType == FFTCache::Compact ? 2 : 1),
-    m_mfc(new MatrixFile
-          (fileBase, MatrixFile::WriteOnly, 
-           int((storageType == FFTCache::Compact) ? sizeof(uint16_t) : sizeof(float)),
-           width, height * 2 + m_factorSize))
-{
-#ifdef DEBUG_FFT_FILE_CACHE_WRITER
-    cerr << "FFTFileCacheWriter: storage type is " << (storageType == FFTCache::Compact ? "Compact" : storageType == FFTCache::Polar ? "Polar" : "Rectangular") << ", size " << width << "x" << height << endl;
-#endif
-    m_mfc->setAutoClose(true);
-    m_writebuf = new char[(height * 2 + m_factorSize) * m_mfc->getCellSize()];
-}
-
-FFTFileCacheWriter::~FFTFileCacheWriter()
-{
-    if (m_writebuf) delete[] m_writebuf;
-    delete m_mfc;
-}
-
-QString
-FFTFileCacheWriter::getFileBase() const
-{
-    return m_fileBase;
-}
-
-int
-FFTFileCacheWriter::getWidth() const
-{
-    return m_mfc->getWidth();
-}
-
-int
-FFTFileCacheWriter::getHeight() const
-{
-    int mh = m_mfc->getHeight();
-    if (mh > m_factorSize) return (mh - m_factorSize) / 2;
-    else return 0;
-}
-
-bool
-FFTFileCacheWriter::haveSetColumnAt(int x) const
-{
-    return m_mfc->haveSetColumnAt(x);
-}
-
-void
-FFTFileCacheWriter::setColumnAt(int x, float *mags, float *phases, float factor)
-{
-    int h = getHeight();
-
-    switch (m_storageType) {
-
-    case FFTCache::Compact:
-        for (int y = 0; y < h; ++y) {
-            ((uint16_t *)m_writebuf)[y * 2] = uint16_t((mags[y] / factor) * 65535.0);
-            ((uint16_t *)m_writebuf)[y * 2 + 1] = uint16_t(int16_t((phases[y] * 32767) / M_PI));
-        }
-        break;
-
-    case FFTCache::Rectangular:
-        for (int y = 0; y < h; ++y) {
-            ((float *)m_writebuf)[y * 2] = mags[y] * cosf(phases[y]);
-            ((float *)m_writebuf)[y * 2 + 1] = mags[y] * sinf(phases[y]);
-        }
-        break;
-
-    case FFTCache::Polar:
-        for (int y = 0; y < h; ++y) {
-            ((float *)m_writebuf)[y * 2] = mags[y];
-            ((float *)m_writebuf)[y * 2 + 1] = phases[y];
-        }
-        break;
-    }
-
-    static float maxFactor = 0;
-    if (factor > maxFactor) maxFactor = factor;
-#ifdef DEBUG_FFT_FILE_CACHE_WRITER
-    cerr << "Column " << x << ": normalization factor: " << factor << ", max " << maxFactor << " (height " << getHeight() << ")" << endl;
-#endif
-
-    setNormalizationFactorToWritebuf(factor);
-
-    m_mfc->setColumnAt(x, m_writebuf);
-}
-
-void
-FFTFileCacheWriter::setColumnAt(int x, float *real, float *imag)
-{
-    int h = getHeight();
-
-    float factor = 0.0f;
-
-    switch (m_storageType) {
-
-    case FFTCache::Compact:
-        for (int y = 0; y < h; ++y) {
-            float mag = sqrtf(real[y] * real[y] + imag[y] * imag[y]);
-            if (mag > factor) factor = mag;
-        }
-        for (int y = 0; y < h; ++y) {
-            float mag = sqrtf(real[y] * real[y] + imag[y] * imag[y]);
-            float phase = atan2f(imag[y], real[y]);
-            ((uint16_t *)m_writebuf)[y * 2] = uint16_t((mag / factor) * 65535.0);
-            ((uint16_t *)m_writebuf)[y * 2 + 1] = uint16_t(int16_t((phase * 32767) / M_PI));
-        }
-        break;
-
-    case FFTCache::Rectangular:
-        for (int y = 0; y < h; ++y) {
-            ((float *)m_writebuf)[y * 2] = real[y];
-            ((float *)m_writebuf)[y * 2 + 1] = imag[y];
-            float mag = sqrtf(real[y] * real[y] + imag[y] * imag[y]);
-            if (mag > factor) factor = mag;
-        }
-        break;
-
-    case FFTCache::Polar:
-        for (int y = 0; y < h; ++y) {
-            float mag = sqrtf(real[y] * real[y] + imag[y] * imag[y]);
-            if (mag > factor) factor = mag;
-            ((float *)m_writebuf)[y * 2] = mag;
-            float phase = atan2f(imag[y], real[y]);
-            ((float *)m_writebuf)[y * 2 + 1] = phase;
-        }
-        break;
-    }
-
-    static float maxFactor = 0;
-    if (factor > maxFactor) maxFactor = factor;
-#ifdef DEBUG_FFT_FILE_CACHE_WRITER
-    cerr << "[RI] Column " << x << ": normalization factor: " << factor << ", max " << maxFactor << " (height " << getHeight() << ")" << endl;
-#endif
-
-    setNormalizationFactorToWritebuf(factor);
-
-    m_mfc->setColumnAt(x, m_writebuf);
-}
-
-size_t
-FFTFileCacheWriter::getCacheSize(int width, int height,
-                                 FFTCache::StorageType type)
-{
-    return (height * 2 + (type == FFTCache::Compact ? 2 : 1)) * width *
-        (type == FFTCache::Compact ? sizeof(uint16_t) : sizeof(float)) +
-        2 * sizeof(int); // matrix file header size
-}
-
-void
-FFTFileCacheWriter::allColumnsWritten()
-{
-#ifdef DEBUG_FFT_FILE_CACHE_WRITER
-    SVDEBUG << "FFTFileCacheWriter::allColumnsWritten" << endl;
-#endif
-    m_mfc->close();
-}
-
--- a/data/fft/FFTFileCacheWriter.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-/* -*- 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 file copyright 2006-2009 Chris Cannam and QMUL.
-    
-    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 _FFT_FILE_CACHE_WRITER_H_
-#define _FFT_FILE_CACHE_WRITER_H_
-
-#include "FFTCacheStorageType.h"
-#include "FFTCacheWriter.h"
-#include "data/fileio/MatrixFile.h"
-
-class FFTFileCacheWriter : public FFTCacheWriter
-{
-public:
-    FFTFileCacheWriter(QString fileBase,
-                       FFTCache::StorageType storageType,
-                       int width, int height);
-    ~FFTFileCacheWriter();
-
-    int getWidth() const;
-    int getHeight() const;
-
-    void setColumnAt(int x, float *mags, float *phases, float factor);
-    void setColumnAt(int x, float *reals, float *imags);
-
-    static size_t getCacheSize(int width, int height,
-                               FFTCache::StorageType type);
-
-    bool haveSetColumnAt(int x) const;
-
-    void allColumnsWritten();
-
-    QString getFileBase() const;
-    FFTCache::StorageType getStorageType() const { return m_storageType; }
-
-protected:
-    char *m_writebuf;
-
-    void setNormalizationFactorToWritebuf(float newfactor) {
-        int h = m_mfc->getHeight();
-        if (h < m_factorSize) return;
-        if (m_storageType != FFTCache::Compact) {
-            ((float *)m_writebuf)[h - 1] = newfactor;
-        } else {
-            union {
-                float f;
-                uint16_t u[2];
-            } factor;
-            factor.f = newfactor;
-            ((uint16_t *)m_writebuf)[h - 2] = factor.u[0];
-            ((uint16_t *)m_writebuf)[h - 1] = factor.u[1];
-        }
-    }            
-
-    QString m_fileBase;
-    FFTCache::StorageType m_storageType;
-    int m_factorSize;
-    MatrixFile *m_mfc;
-};
-
-#endif
--- a/data/fft/FFTMemoryCache.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,218 +0,0 @@
-/* -*- 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 file copyright 2006 Chris Cannam.
-    
-    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 "FFTMemoryCache.h"
-#include "system/System.h"
-
-#include <iostream>
-#include <cstdlib>
-
-//#define DEBUG_FFT_MEMORY_CACHE 1
-
-FFTMemoryCache::FFTMemoryCache(FFTCache::StorageType storageType,
-                               int width, int height) :
-    m_width(width),
-    m_height(height),
-    m_magnitude(0),
-    m_phase(0),
-    m_fmagnitude(0),
-    m_fphase(0),
-    m_freal(0),
-    m_fimag(0),
-    m_factor(0),
-    m_storageType(storageType)
-{
-#ifdef DEBUG_FFT_MEMORY_CACHE
-    cerr << "FFTMemoryCache[" << this << "]::FFTMemoryCache (type "
-              << m_storageType << "), size " << m_width << "x" << m_height << endl;
-#endif
-
-    initialise();
-}
-
-FFTMemoryCache::~FFTMemoryCache()
-{
-#ifdef DEBUG_FFT_MEMORY_CACHE
-    cerr << "FFTMemoryCache[" << this << "]::~FFTMemoryCache" << endl;
-#endif
-
-    for (int i = 0; i < m_width; ++i) {
-	if (m_magnitude && m_magnitude[i]) free(m_magnitude[i]);
-	if (m_phase && m_phase[i]) free(m_phase[i]);
-	if (m_fmagnitude && m_fmagnitude[i]) free(m_fmagnitude[i]);
-	if (m_fphase && m_fphase[i]) free(m_fphase[i]);
-        if (m_freal && m_freal[i]) free(m_freal[i]);
-        if (m_fimag && m_fimag[i]) free(m_fimag[i]);
-    }
-
-    if (m_magnitude) free(m_magnitude);
-    if (m_phase) free(m_phase);
-    if (m_fmagnitude) free(m_fmagnitude);
-    if (m_fphase) free(m_fphase);
-    if (m_freal) free(m_freal);
-    if (m_fimag) free(m_fimag);
-    if (m_factor) free(m_factor);
-}
-
-void
-FFTMemoryCache::initialise()
-{
-    Profiler profiler("FFTMemoryCache::initialise");
-
-    int width = m_width, height = m_height;
-
-#ifdef DEBUG_FFT_MEMORY_CACHE
-    cerr << "FFTMemoryCache[" << this << "]::initialise(" << width << "x" << height << " = " << width*height << ")" << endl;
-#endif
-
-    if (m_storageType == FFTCache::Compact) {
-        initialise(m_magnitude);
-        initialise(m_phase);
-    } else if (m_storageType == FFTCache::Polar) {
-        initialise(m_fmagnitude);
-        initialise(m_fphase);
-    } else {
-        initialise(m_freal);
-        initialise(m_fimag);
-    }
-
-    m_colset.resize(width);
-
-    m_factor = (float *)realloc(m_factor, width * sizeof(float));
-
-    m_width = width;
-    m_height = height;
-
-#ifdef DEBUG_FFT_MEMORY_CACHE
-    cerr << "done, width = " << m_width << " height = " << m_height << endl;
-#endif
-}
-
-void
-FFTMemoryCache::initialise(uint16_t **&array)
-{
-    array = (uint16_t **)malloc(m_width * sizeof(uint16_t *));
-    if (!array) throw std::bad_alloc();
-    MUNLOCK(array, m_width * sizeof(uint16_t *));
-
-    for (int i = 0; i < m_width; ++i) {
-	array[i] = (uint16_t *)malloc(m_height * sizeof(uint16_t));
-	if (!array[i]) throw std::bad_alloc();
-	MUNLOCK(array[i], m_height * sizeof(uint16_t));
-    }
-}
-
-void
-FFTMemoryCache::initialise(float **&array)
-{
-    array = (float **)malloc(m_width * sizeof(float *));
-    if (!array) throw std::bad_alloc();
-    MUNLOCK(array, m_width * sizeof(float *));
-
-    for (int i = 0; i < m_width; ++i) {
-	array[i] = (float *)malloc(m_height * sizeof(float));
-	if (!array[i]) throw std::bad_alloc();
-	MUNLOCK(array[i], m_height * sizeof(float));
-    }
-}
-
-void
-FFTMemoryCache::setColumnAt(int x, float *mags, float *phases, float factor)
-{
-    Profiler profiler("FFTMemoryCache::setColumnAt: from polar");
-
-    setNormalizationFactor(x, factor);
-
-    if (m_storageType == FFTCache::Rectangular) {
-        Profiler subprof("FFTMemoryCache::setColumnAt: polar to cart");
-        for (int y = 0; y < m_height; ++y) {
-            m_freal[x][y] = mags[y] * cosf(phases[y]);
-            m_fimag[x][y] = mags[y] * sinf(phases[y]);
-        }
-    } else {
-        for (int y = 0; y < m_height; ++y) {
-            setMagnitudeAt(x, y, mags[y]);
-            setPhaseAt(x, y, phases[y]);
-        }
-    }
-
-    m_colsetLock.lockForWrite();
-    m_colset.set(x);
-    m_colsetLock.unlock();
-}
-
-void
-FFTMemoryCache::setColumnAt(int x, float *reals, float *imags)
-{
-    Profiler profiler("FFTMemoryCache::setColumnAt: from cart");
-
-    float max = 0.0;
-
-    switch (m_storageType) {
-
-    case FFTCache::Rectangular:
-        for (int y = 0; y < m_height; ++y) {
-            m_freal[x][y] = reals[y];
-            m_fimag[x][y] = imags[y];
-            float mag = sqrtf(reals[y] * reals[y] + imags[y] * imags[y]);
-            if (mag > max) max = mag;
-        }
-        break;
-
-    case FFTCache::Compact:
-    case FFTCache::Polar:
-    {
-        Profiler subprof("FFTMemoryCache::setColumnAt: cart to polar");
-        for (int y = 0; y < m_height; ++y) {
-            float mag = sqrtf(reals[y] * reals[y] + imags[y] * imags[y]);
-            float phase = atan2f(imags[y], reals[y]);
-            reals[y] = mag;
-            imags[y] = phase;
-            if (mag > max) max = mag;
-        }
-        break;
-    }
-    };
-
-    if (m_storageType == FFTCache::Rectangular) {
-        m_factor[x] = max;
-        m_colsetLock.lockForWrite();
-        m_colset.set(x);
-        m_colsetLock.unlock();
-    } else {
-        setColumnAt(x, reals, imags, max);
-    }
-}
-
-size_t
-FFTMemoryCache::getCacheSize(int width, int height, FFTCache::StorageType type)
-{
-    size_t sz = 0;
-
-    switch (type) {
-
-    case FFTCache::Compact:
-        sz = (height * 2 + 1) * width * sizeof(uint16_t);
-        break;
-
-    case FFTCache::Polar:
-    case FFTCache::Rectangular:
-        sz = (height * 2 + 1) * width * sizeof(float);
-        break;
-    }
-
-    return sz;
-}
-
--- a/data/fft/FFTMemoryCache.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,186 +0,0 @@
-/* -*- 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 file copyright 2006 Chris Cannam.
-    
-    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 _FFT_MEMORY_CACHE_H_
-#define _FFT_MEMORY_CACHE_H_
-
-#include "FFTCacheReader.h"
-#include "FFTCacheWriter.h"
-#include "FFTCacheStorageType.h"
-#include "base/ResizeableBitset.h"
-#include "base/Profiler.h"
-
-#include <QReadWriteLock>
-
-/**
- * In-memory FFT cache.  For this we want to cache magnitude with
- * enough resolution to have gain applied afterwards and determine
- * whether something is a peak or not, and also cache phase rather
- * than only phase-adjusted frequency so that we don't have to
- * recalculate if switching between phase and magnitude displays.  At
- * the same time, we don't want to take up too much memory.  It's not
- * expected to be accurate enough to be used as input for DSP or
- * resynthesis code.
- *
- * This implies probably 16 bits for a normalized magnitude and at
- * most 16 bits for phase.
- *
- * Each column's magnitudes are expected to be stored normalized
- * to [0,1] with respect to the column, so the normalization
- * factor should be calculated before all values in a column, and
- * set appropriately.
- */
-
-class FFTMemoryCache : public FFTCacheReader, public FFTCacheWriter
-{
-public:
-    FFTMemoryCache(FFTCache::StorageType storageType,
-                   int width, int height);
-    ~FFTMemoryCache();
-	
-    int getWidth() const { return m_width; }
-    int getHeight() const { return m_height; }
-	
-    float getMagnitudeAt(int x, int y) const {
-        if (m_storageType == FFTCache::Rectangular) {
-            Profiler profiler("FFTMemoryCache::getMagnitudeAt: cart to polar");
-            return sqrtf(m_freal[x][y] * m_freal[x][y] +
-                         m_fimag[x][y] * m_fimag[x][y]);
-        } else {
-            return getNormalizedMagnitudeAt(x, y) * m_factor[x];
-        }
-    }
-    
-    float getNormalizedMagnitudeAt(int x, int y) const {
-        if (m_storageType == FFTCache::Rectangular) return getMagnitudeAt(x, y) / m_factor[x];
-        else if (m_storageType == FFTCache::Polar) return m_fmagnitude[x][y];
-        else return float(m_magnitude[x][y]) / 65535.f;
-    }
-    
-    float getMaximumMagnitudeAt(int x) const {
-        return m_factor[x];
-    }
-    
-    float getPhaseAt(int x, int y) const {
-        if (m_storageType == FFTCache::Rectangular) {
-            Profiler profiler("FFTMemoryCache::getValuesAt: cart to polar");
-            return atan2f(m_fimag[x][y], m_freal[x][y]);
-        } else if (m_storageType == FFTCache::Polar) {
-            return m_fphase[x][y];
-        } else {
-            int16_t i = (int16_t)m_phase[x][y];
-            return float(i / 32767.0 * M_PI);
-        }
-    }
-    
-    void getValuesAt(int x, int y, float &real, float &imag) const {
-        if (m_storageType == FFTCache::Rectangular) {
-            real = m_freal[x][y];
-            imag = m_fimag[x][y];
-        } else {
-            Profiler profiler("FFTMemoryCache::getValuesAt: polar to cart");
-            float mag = getMagnitudeAt(x, y);
-            float phase = getPhaseAt(x, y);
-            real = mag * cosf(phase);
-            imag = mag * sinf(phase);
-        }
-    }
-
-    void getMagnitudesAt(int x, float *values, int minbin, int count, int step) const
-    {
-        if (m_storageType == FFTCache::Rectangular) {
-            for (int i = 0; i < count; ++i) {
-                int y = i * step + minbin;
-                values[i] = sqrtf(m_freal[x][y] * m_freal[x][y] +
-                                  m_fimag[x][y] * m_fimag[x][y]);
-            }
-        } else if (m_storageType == FFTCache::Polar) {
-            for (int i = 0; i < count; ++i) {
-                int y = i * step + minbin;
-                values[i] = m_fmagnitude[x][y] * m_factor[x];
-            }
-        } else {
-            for (int i = 0; i < count; ++i) {
-                int y = i * step + minbin;
-                values[i] = float(double(m_magnitude[x][y]) * m_factor[x] / 65535.0);
-            }
-        }
-    }
-
-    bool haveSetColumnAt(int x) const {
-        m_colsetLock.lockForRead();
-        bool have = m_colset.get(x);
-        m_colsetLock.unlock();
-        return have;
-    }
-
-    void setColumnAt(int x, float *mags, float *phases, float factor);
-
-    void setColumnAt(int x, float *reals, float *imags);
-
-    void allColumnsWritten() { } 
-
-    static size_t getCacheSize(int width, int height,
-                               FFTCache::StorageType type);
-
-    FFTCache::StorageType getStorageType() const { return m_storageType; }
-
-private:
-    int m_width;
-    int m_height;
-    uint16_t **m_magnitude;
-    uint16_t **m_phase;
-    float **m_fmagnitude;
-    float **m_fphase;
-    float **m_freal;
-    float **m_fimag;
-    float *m_factor;
-    FFTCache::StorageType m_storageType;
-    ResizeableBitset m_colset;
-    mutable QReadWriteLock m_colsetLock;
-
-    void initialise();
-
-    void setNormalizationFactor(int x, float factor) {
-        if (x < m_width) m_factor[x] = factor;
-    }
-    
-    void setMagnitudeAt(int x, int y, float mag) {
-         // norm factor must already be set
-        setNormalizedMagnitudeAt(x, y, mag / m_factor[x]);
-    }
-    
-    void setNormalizedMagnitudeAt(int x, int y, float norm) {
-        if (x < m_width && y < m_height) {
-            if (m_storageType == FFTCache::Polar) m_fmagnitude[x][y] = norm;
-            else m_magnitude[x][y] = uint16_t(norm * 65535.0);
-        }
-    }
-    
-    void setPhaseAt(int x, int y, float phase) {
-        // phase in range -pi -> pi
-        if (x < m_width && y < m_height) {
-            if (m_storageType == FFTCache::Polar) m_fphase[x][y] = phase;
-            else m_phase[x][y] = uint16_t(int16_t((phase * 32767) / M_PI));
-        }
-    }
-
-    void initialise(uint16_t **&);
-    void initialise(float **&);
-};
-
-
-#endif
-
--- a/data/fft/FFTapi.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,224 +0,0 @@
-/* -*- 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 file copyright 2006 Chris Cannam and QMUL.
-    FFT code from Don Cross's public domain FFT implementation.
-    
-    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 "FFTapi.h"
-
-#ifndef HAVE_FFTW3F
-
-#include <cmath>
-#include <iostream>
-
-void
-fft(unsigned int n, bool inverse, double *ri, double *ii, double *ro, double *io)
-{
-    if (!ri || !ro || !io) return;
-
-    unsigned int bits;
-    unsigned int i, j, k, m;
-    unsigned int blockSize, blockEnd;
-
-    double tr, ti;
-
-    if (n < 2) return;
-    if (n & (n-1)) return;
-
-    double angle = 2.0 * M_PI;
-    if (inverse) angle = -angle;
-
-    for (i = 0; ; ++i) {
-	if (n & (1 << i)) {
-	    bits = i;
-	    break;
-	}
-    }
-
-    int *table = new int[n];
-
-    for (i = 0; i < n; ++i) {
-	
-        m = i;
-        
-        for (j = k = 0; j < bits; ++j) {
-            k = (k << 1) | (m & 1);
-            m >>= 1;
-        }
-        
-        table[i] = k;
-    }
-
-    if (ii) {
-	for (i = 0; i < n; ++i) {
-	    ro[table[i]] = ri[i];
-	    io[table[i]] = ii[i];
-	}
-    } else {
-	for (i = 0; i < n; ++i) {
-	    ro[table[i]] = ri[i];
-	    io[table[i]] = 0.0;
-	}
-    }
-
-    blockEnd = 1;
-
-    for (blockSize = 2; blockSize <= n; blockSize <<= 1) {
-
-	double delta = angle / (double)blockSize;
-	double sm2 = -sin(-2 * delta);
-	double sm1 = -sin(-delta);
-	double cm2 = cos(-2 * delta);
-	double cm1 = cos(-delta);
-	double w = 2 * cm1;
-	double ar[3], ai[3];
-
-	for (i = 0; i < n; i += blockSize) {
-
-	    ar[2] = cm2;
-	    ar[1] = cm1;
-
-	    ai[2] = sm2;
-	    ai[1] = sm1;
-
-	    for (j = i, m = 0; m < blockEnd; j++, m++) {
-
-		ar[0] = w * ar[1] - ar[2];
-		ar[2] = ar[1];
-		ar[1] = ar[0];
-
-		ai[0] = w * ai[1] - ai[2];
-		ai[2] = ai[1];
-		ai[1] = ai[0];
-
-		k = j + blockEnd;
-		tr = ar[0] * ro[k] - ai[0] * io[k];
-		ti = ar[0] * io[k] + ai[0] * ro[k];
-
-		ro[k] = ro[j] - tr;
-		io[k] = io[j] - ti;
-
-		ro[j] += tr;
-		io[j] += ti;
-	    }
-	}
-
-	blockEnd = blockSize;
-    }
-
-/* fftw doesn't normalise, so nor will we
-
-    if (inverse) {
-
-	double denom = (double)n;
-
-	for (i = 0; i < n; i++) {
-	    ro[i] /= denom;
-	    io[i] /= denom;
-	}
-    }
-*/
-    delete[] table;
-}
-
-struct fftf_plan_ {
-    int size;
-    int inverse;
-    float *real;
-    fftf_complex *cplx;
-};
-
-fftf_plan
-fftf_plan_dft_r2c_1d(int n, float *in, fftf_complex *out, unsigned)
-{
-    if (n < 2) return 0;
-    if (n & (n-1)) return 0;
-    
-    fftf_plan_ *plan = new fftf_plan_;
-    plan->size = n;
-    plan->inverse = 0;
-    plan->real = in;
-    plan->cplx = out;
-    return plan;
-}
-
-fftf_plan
-fftf_plan_dft_c2r_1d(int n, fftf_complex *in, float *out, unsigned)
-{
-    if (n < 2) return 0;
-    if (n & (n-1)) return 0;
-    
-    fftf_plan_ *plan = new fftf_plan_;
-    plan->size = n;
-    plan->inverse = 1;
-    plan->real = out;
-    plan->cplx = in;
-    return plan;
-}
-
-void
-fftf_destroy_plan(fftf_plan p)
-{
-    delete p;
-}
-
-void
-fftf_execute(const fftf_plan p)
-{
-    float *real = p->real;
-    fftf_complex *cplx = p->cplx;
-    int n = p->size;
-    int forward = !p->inverse;
-
-    double *ri = new double[n];
-    double *ro = new double[n];
-    double *io = new double[n];
-
-    double *ii = 0;
-    if (!forward) ii = new double[n];
-
-    if (forward) {
-        for (int i = 0; i < n; ++i) {
-            ri[i] = real[i];
-        }
-    } else {
-        for (int i = 0; i < n/2+1; ++i) {
-            ri[i] = cplx[i][0];
-            ii[i] = cplx[i][1];
-            if (i > 0) {
-                ri[n-i] = ri[i];
-                ii[n-i] = -ii[i];
-            }
-        }
-    }
-
-    fft(n, !forward, ri, ii, ro, io);
-
-    if (forward) {
-        for (int i = 0; i < n/2+1; ++i) {
-            cplx[i][0] = ro[i];
-            cplx[i][1] = io[i];
-        }
-    } else {
-        for (int i = 0; i < n; ++i) {
-            real[i] = ro[i];
-        }
-    }
-
-    delete[] ri;
-    delete[] ro;
-    delete[] io;
-    if (ii) delete[] ii;
-}
-
-#endif
--- a/data/fft/FFTapi.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-/* -*- 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 file copyright 2006 Chris Cannam and QMUL.
-    
-    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 _FFT_API_H_
-#define _FFT_API_H_
-
-#ifdef HAVE_FFTW3F
-
-#include <fftw3.h>
-
-#define fftf_complex fftwf_complex
-#define fftf_malloc fftwf_malloc
-#define fftf_free fftwf_free
-#define fftf_plan fftwf_plan
-#define fftf_plan_dft_r2c_1d fftwf_plan_dft_r2c_1d
-#define fftf_plan_dft_c2r_1d fftwf_plan_dft_c2r_1d
-#define fftf_execute fftwf_execute
-#define fftf_destroy_plan fftwf_destroy_plan
-
-#else
-
-// Provide a fallback FFT implementation if FFTW3f is not available.
-
-typedef float fftf_complex[2];
-#define fftf_malloc malloc
-#define fftf_free free
-
-struct fftf_plan_;
-typedef fftf_plan_ *fftf_plan;
-
-fftf_plan fftf_plan_dft_r2c_1d(int n, float *in, fftf_complex *out, unsigned);
-fftf_plan fftf_plan_dft_c2r_1d(int n, fftf_complex *in, float *out, unsigned);
-void fftf_execute(const fftf_plan p);
-void fftf_destroy_plan(fftf_plan p);
-
-#define FFTW_ESTIMATE 0
-#define FFTW_MEASURE 0
-
-#endif
-
-#endif
-
--- a/data/fileio/AudioFileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/AudioFileReader.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -17,15 +17,17 @@
 
 using std::vector;
 
-vector<SampleBlock>
+vector<floatvec_t>
 AudioFileReader::getDeInterleavedFrames(sv_frame_t start, sv_frame_t count) const
 {
-    SampleBlock interleaved = getInterleavedFrames(start, count);
+    floatvec_t interleaved = getInterleavedFrames(start, count);
     
     int channels = getChannelCount();
+    if (channels == 1) return { interleaved };
+    
     sv_frame_t rc = interleaved.size() / channels;
 
-    vector<SampleBlock> frames(channels, SampleBlock(rc, 0.f));
+    vector<floatvec_t> frames(channels, floatvec_t(rc, 0.f));
     
     for (int c = 0; c < channels; ++c) {
         for (sv_frame_t i = 0; i < rc; ++i) {
--- a/data/fileio/AudioFileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/AudioFileReader.h	Fri Jan 13 10:29:44 2017 +0000
@@ -24,8 +24,6 @@
 #include <vector>
 #include <map>
 
-typedef std::vector<float> SampleBlock;
-
 class AudioFileReader : public QObject
 {
     Q_OBJECT
@@ -33,15 +31,42 @@
 public:
     virtual ~AudioFileReader() { }
 
+    /**
+     * Return true if the file was opened successfully and no error
+     * has subsequently occurred.
+     */
     bool isOK() const { return (m_channelCount > 0); }
 
+    /**
+     * If isOK() is false, return an error string.
+     */
     virtual QString getError() const { return ""; }
 
+    /**
+     * Return the number of audio sample frames (i.e. samples per
+     * channel) in the file.
+     */
     sv_frame_t getFrameCount() const { return m_frameCount; }
+
+    /**
+     * Return the number of channels in the file.
+     */
     int getChannelCount() const { return m_channelCount; }
+
+    /**
+     * Return the samplerate at which the file is being read. This is
+     * the rate requested when the file was opened, which may differ
+     * from the native rate of the file (in which case the file will
+     * be resampled as it is read).
+     */
     sv_samplerate_t getSampleRate() const { return m_sampleRate; }
 
-    virtual sv_samplerate_t getNativeRate() const { return m_sampleRate; } // if resampled
+    /**
+     * Return the native samplerate of the file. This will differ from
+     * getSampleRate() if the file is being resampled because it was
+     * requested to open at a different rate from native.
+     */
+    virtual sv_samplerate_t getNativeRate() const { return m_sampleRate; }
 
     /**
      * Return the location of the audio data in the reader (as passed
@@ -85,16 +110,15 @@
 
     /** 
      * Return interleaved samples for count frames from index start.
-     * The resulting sample block will contain count *
-     * getChannelCount() samples (or fewer if end of file is
-     * reached). The caller does not need to allocate space and any
-     * existing content in the SampleBlock will be erased.
+     * The resulting vector will contain count * getChannelCount()
+     * samples (or fewer if end of file is reached).
      *
      * The subclass implementations of this function must be
      * thread-safe -- that is, safe to call from multiple threads with
      * different arguments on the same object at the same time.
      */
-    virtual SampleBlock getInterleavedFrames(sv_frame_t start, sv_frame_t count) const = 0;
+    virtual floatvec_t getInterleavedFrames(sv_frame_t start,
+                                            sv_frame_t count) const = 0;
 
     /**
      * Return de-interleaved samples for count frames from index
@@ -103,7 +127,8 @@
      * will contain getChannelCount() sample blocks of count samples
      * each (or fewer if end of file is reached).
      */
-    virtual std::vector<SampleBlock> getDeInterleavedFrames(sv_frame_t start, sv_frame_t count) const;
+    virtual std::vector<floatvec_t> getDeInterleavedFrames(sv_frame_t start,
+                                                           sv_frame_t count) const;
 
     // only subclasses that do not know exactly how long the audio
     // file is until it's been completely decoded should implement this
--- a/data/fileio/AudioFileReaderFactory.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/AudioFileReaderFactory.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -19,15 +19,15 @@
 #include "DecodingWavFileReader.h"
 #include "OggVorbisFileReader.h"
 #include "MP3FileReader.h"
-#include "QuickTimeFileReader.h"
 #include "CoreAudioFileReader.h"
+#include "AudioFileSizeEstimator.h"
+
+#include "base/StorageAdviser.h"
 
 #include <QString>
 #include <QFileInfo>
 #include <iostream>
 
-//#define DEBUG_AUDIO_FILE_READER_FACTORY 1
-
 QString
 AudioFileReaderFactory::getKnownExtensions()
 {
@@ -42,9 +42,6 @@
     OggVorbisFileReader::getSupportedExtensions(extensions);
 #endif
 #endif
-#ifdef HAVE_QUICKTIME
-    QuickTimeFileReader::getSupportedExtensions(extensions);
-#endif
 #ifdef HAVE_COREAUDIO
     CoreAudioFileReader::getSupportedExtensions(extensions);
 #endif
@@ -60,300 +57,171 @@
 }
 
 AudioFileReader *
-AudioFileReaderFactory::createReader(FileSource source, 
-                                     sv_samplerate_t targetRate,
-                                     bool normalised,
+AudioFileReaderFactory::createReader(FileSource source,
+                                     Parameters params,
                                      ProgressReporter *reporter)
 {
-    return create(source, targetRate, normalised, false, reporter);
-}
-
-AudioFileReader *
-AudioFileReaderFactory::createThreadingReader(FileSource source, 
-                                              sv_samplerate_t targetRate,
-                                              bool normalised,
-                                              ProgressReporter *reporter)
-{
-    return create(source, targetRate, normalised, true, reporter);
-}
-
-AudioFileReader *
-AudioFileReaderFactory::create(FileSource source, 
-                               sv_samplerate_t targetRate, 
-                               bool normalised,
-                               bool threading,
-                               ProgressReporter *reporter)
-{
     QString err;
 
-#ifdef DEBUG_AUDIO_FILE_READER_FACTORY
-    cerr << "AudioFileReaderFactory::createReader(\"" << source.getLocation() << "\"): Requested rate: " << targetRate << endl;
-#endif
+    SVDEBUG << "AudioFileReaderFactory: url \"" << source.getLocation() << "\": requested rate: " << params.targetRate << (params.targetRate == 0 ? " (use source rate)" : "") << endl;
+    SVDEBUG << "AudioFileReaderFactory: local filename \"" << source.getLocalFilename() << "\", content type \"" << source.getContentType() << "\"" << endl;
 
     if (!source.isOK()) {
-        cerr << "AudioFileReaderFactory::createReader(\"" << source.getLocation() << "\": Failed to retrieve source (transmission error?): " << source.getErrorString() << endl;
+        SVCERR << "AudioFileReaderFactory::createReader(\"" << source.getLocation() << "\": Failed to retrieve source (transmission error?): " << source.getErrorString() << endl;
         return 0;
     }
 
     if (!source.isAvailable()) {
-        cerr << "AudioFileReaderFactory::createReader(\"" << source.getLocation() << "\": Source not found" << endl;
+        SVCERR << "AudioFileReaderFactory::createReader(\"" << source.getLocation() << "\": Source not found" << endl;
         return 0;
     }
 
     AudioFileReader *reader = 0;
 
-    // Try to construct a preferred reader based on the extension or
-    // MIME type.
+    sv_samplerate_t targetRate = params.targetRate;
+    bool normalised = (params.normalisation == Normalisation::Peak);
+  
+    sv_frame_t estimatedSamples = 
+        AudioFileSizeEstimator::estimate(source, targetRate);
+    
+    CodedAudioFileReader::CacheMode cacheMode =
+        CodedAudioFileReader::CacheInTemporaryFile;
 
-    if (WavFileReader::supports(source)) {
-
-        reader = new WavFileReader(source);
-
-        sv_samplerate_t fileRate = reader->getSampleRate();
-
-        if (reader->isOK() &&
-            (!reader->isQuicklySeekable() ||
-             normalised ||
-             (targetRate != 0 && fileRate != targetRate))) {
-
-#ifdef DEBUG_AUDIO_FILE_READER_FACTORY
-            cerr << "AudioFileReaderFactory::createReader: WAV file rate: " << reader->getSampleRate() << ", normalised " << normalised << ", seekable " << reader->isQuicklySeekable() << ", creating decoding reader" << endl;
-#endif
-            
-            delete reader;
-            reader = new DecodingWavFileReader
-                (source,
-                 threading ?
-                 DecodingWavFileReader::ResampleThreaded :
-                 DecodingWavFileReader::ResampleAtOnce,
-                 DecodingWavFileReader::CacheInTemporaryFile,
-                 targetRate ? targetRate : fileRate,
-                 normalised,
-                 reporter);
-            if (!reader->isOK()) {
-                delete reader;
-                reader = 0;
-            }
+    if (estimatedSamples > 0) {
+        size_t kb = (estimatedSamples * sizeof(float)) / 1024;
+        SVDEBUG << "AudioFileReaderFactory: checking where to potentially cache "
+                << kb << "K of sample data" << endl;
+        StorageAdviser::Recommendation rec =
+            StorageAdviser::recommend(StorageAdviser::SpeedCritical, kb, kb);
+        if ((rec & StorageAdviser::UseMemory) ||
+            (rec & StorageAdviser::PreferMemory)) {
+            SVDEBUG << "AudioFileReaderFactory: cacheing (if at all) in memory" << endl;
+            cacheMode = CodedAudioFileReader::CacheInMemory;
+        } else {
+            SVDEBUG << "AudioFileReaderFactory: cacheing (if at all) on disc" << endl;
         }
     }
     
+    CodedAudioFileReader::DecodeMode decodeMode =
+        (params.threadingMode == ThreadingMode::Threaded ?
+         CodedAudioFileReader::DecodeThreaded :
+         CodedAudioFileReader::DecodeAtOnce);
+
+    // We go through the set of supported readers at most twice: once
+    // picking out only the readers that claim to support the given
+    // file's extension or MIME type, and (if that fails) again
+    // providing the file to every reader in turn regardless of
+    // extension or type. (If none of the readers claim to support a
+    // file, that may just mean its extension is missing or
+    // misleading. We have to be confident that the reader won't open
+    // just any old text file or whatever and pretend it's succeeded.)
+
+    for (int any = 0; any <= 1; ++any) {
+
+        bool anyReader = (any > 0);
+
+        if (!anyReader) {
+            SVDEBUG << "AudioFileReaderFactory: Checking whether any reader officially handles this source" << endl;
+        } else {
+            SVDEBUG << "AudioFileReaderFactory: Source not officially handled by any reader, trying again with each reader in turn"
+                    << endl;
+        }
+    
 #ifdef HAVE_OGGZ
 #ifdef HAVE_FISHSOUND
-    if (!reader) {
-        if (OggVorbisFileReader::supports(source)) {
+        // If we have the "real" Ogg reader, use that first. Otherwise
+        // the WavFileReader will likely accept Ogg files (as
+        // libsndfile supports them) but it has no ability to return
+        // file metadata, so we get a slightly less useful result.
+        if (anyReader || OggVorbisFileReader::supports(source)) {
+
             reader = new OggVorbisFileReader
-                (source,
-                 threading ?
-                 OggVorbisFileReader::DecodeThreaded :
-                 OggVorbisFileReader::DecodeAtOnce,
-                 OggVorbisFileReader::CacheInTemporaryFile,
-                 targetRate,
-                 normalised,
-                 reporter);
-            if (!reader->isOK()) {
+                (source, decodeMode, cacheMode, targetRate, normalised, reporter);
+
+            if (reader->isOK()) {
+                SVDEBUG << "AudioFileReaderFactory: Ogg file reader is OK, returning it" << endl;
+                return reader;
+            } else {
                 delete reader;
-                reader = 0;
             }
         }
-    }
 #endif
 #endif
 
-#ifdef HAVE_MAD
-    if (!reader) {
-        if (MP3FileReader::supports(source)) {
-            reader = new MP3FileReader
-                (source,
-                 threading ?
-                 MP3FileReader::DecodeThreaded :
-                 MP3FileReader::DecodeAtOnce,
-                 MP3FileReader::CacheInTemporaryFile,
-                 targetRate,
-                 normalised,
-                 reporter);
-            if (!reader->isOK()) {
+        if (anyReader || WavFileReader::supports(source)) {
+
+            reader = new WavFileReader(source);
+
+            sv_samplerate_t fileRate = reader->getSampleRate();
+
+            if (reader->isOK() &&
+                (!reader->isQuicklySeekable() ||
+                 normalised ||
+                 (cacheMode == CodedAudioFileReader::CacheInMemory) ||
+                 (targetRate != 0 && fileRate != targetRate))) {
+
+                SVDEBUG << "AudioFileReaderFactory: WAV file reader rate: " << reader->getSampleRate() << ", normalised " << normalised << ", seekable " << reader->isQuicklySeekable() << ", in memory " << (cacheMode == CodedAudioFileReader::CacheInMemory) << ", creating decoding reader" << endl;
+            
                 delete reader;
-                reader = 0;
+                reader = new DecodingWavFileReader
+                    (source,
+                     decodeMode, cacheMode,
+                     targetRate ? targetRate : fileRate,
+                     normalised,
+                     reporter);
+            }
+
+            if (reader->isOK()) {
+                SVDEBUG << "AudioFileReaderFactory: WAV file reader is OK, returning it" << endl;
+                return reader;
+            } else {
+                delete reader;
             }
         }
-    }
-#endif
 
-#ifdef HAVE_QUICKTIME
-    if (!reader) {
-        if (QuickTimeFileReader::supports(source)) {
-            reader = new QuickTimeFileReader
-                (source,
-                 threading ?
-                 QuickTimeFileReader::DecodeThreaded : 
-                 QuickTimeFileReader::DecodeAtOnce,
-                 QuickTimeFileReader::CacheInTemporaryFile,
-                 targetRate,
-                 normalised,
-                 reporter);
-            if (!reader->isOK()) {
+#ifdef HAVE_MAD
+        if (anyReader || MP3FileReader::supports(source)) {
+
+            MP3FileReader::GaplessMode gapless =
+                params.gaplessMode == GaplessMode::Gapless ?
+                MP3FileReader::GaplessMode::Gapless :
+                MP3FileReader::GaplessMode::Gappy;
+            
+            reader = new MP3FileReader
+                (source, decodeMode, cacheMode, gapless,
+                 targetRate, normalised, reporter);
+
+            if (reader->isOK()) {
+                SVDEBUG << "AudioFileReaderFactory: MP3 file reader is OK, returning it" << endl;
+                return reader;
+            } else {
                 delete reader;
-                reader = 0;
             }
         }
-    }
 #endif
 
 #ifdef HAVE_COREAUDIO
-    if (!reader) {
-        if (CoreAudioFileReader::supports(source)) {
+        if (anyReader || CoreAudioFileReader::supports(source)) {
+
             reader = new CoreAudioFileReader
-                (source,
-                 threading ?
-                 CoreAudioFileReader::DecodeThreaded :
-                 CoreAudioFileReader::DecodeAtOnce,
-                 CoreAudioFileReader::CacheInTemporaryFile,
-                 targetRate,
-                 normalised,
-                 reporter);
-            if (!reader->isOK()) {
+                (source, decodeMode, cacheMode, targetRate, normalised, reporter);
+
+            if (reader->isOK()) {
+                SVDEBUG << "AudioFileReaderFactory: CoreAudio reader is OK, returning it" << endl;
+                return reader;
+            } else {
                 delete reader;
-                reader = 0;
             }
         }
-    }
 #endif
 
-
-    // If none of the readers claimed to support this file extension,
-    // perhaps the extension is missing or misleading.  Try again,
-    // ignoring it.  We have to be confident that the reader won't
-    // open just any old text file or whatever and pretend it's
-    // succeeded
-
-    if (!reader) {
-
-        reader = new WavFileReader(source);
-
-        sv_samplerate_t fileRate = reader->getSampleRate();
-
-        if (reader->isOK() &&
-            (!reader->isQuicklySeekable() ||
-             normalised ||
-             (targetRate != 0 && fileRate != targetRate))) {
-
-#ifdef DEBUG_AUDIO_FILE_READER_FACTORY
-            cerr << "AudioFileReaderFactory::createReader: WAV file rate: " << reader->getSampleRate() << ", normalised " << normalised << ", seekable " << reader->isQuicklySeekable() << ", creating decoding reader" << endl;
-#endif
-
-            delete reader;
-            reader = new DecodingWavFileReader
-                (source,
-                 threading ?
-                 DecodingWavFileReader::ResampleThreaded :
-                 DecodingWavFileReader::ResampleAtOnce,
-                 DecodingWavFileReader::CacheInTemporaryFile,
-                 targetRate ? targetRate : fileRate,
-                 normalised,
-                 reporter);
-        }
-
-        if (!reader->isOK()) {
-            delete reader;
-            reader = 0;
-        }
     }
     
-#ifdef HAVE_OGGZ
-#ifdef HAVE_FISHSOUND
-    if (!reader) {
-        reader = new OggVorbisFileReader
-            (source,
-             threading ?
-             OggVorbisFileReader::DecodeThreaded :
-             OggVorbisFileReader::DecodeAtOnce,
-             OggVorbisFileReader::CacheInTemporaryFile,
-             targetRate,
-             reporter);
-
-        if (!reader->isOK()) {
-            delete reader;
-            reader = 0;
-        }
-    }
-#endif
-#endif
-
-#ifdef HAVE_MAD
-    if (!reader) {
-        reader = new MP3FileReader
-            (source,
-             threading ?
-             MP3FileReader::DecodeThreaded :
-             MP3FileReader::DecodeAtOnce,
-             MP3FileReader::CacheInTemporaryFile,
-             targetRate,
-             reporter);
-
-        if (!reader->isOK()) {
-            delete reader;
-            reader = 0;
-        }
-    }
-#endif
-
-#ifdef HAVE_QUICKTIME
-    if (!reader) {
-        reader = new QuickTimeFileReader
-            (source,
-             threading ?
-             QuickTimeFileReader::DecodeThreaded : 
-             QuickTimeFileReader::DecodeAtOnce,
-             QuickTimeFileReader::CacheInTemporaryFile,
-             targetRate,
-             reporter);
-
-        if (!reader->isOK()) {
-            delete reader;
-            reader = 0;
-        }
-    }
-#endif
-
-#ifdef HAVE_COREAUDIO
-    if (!reader) {
-        reader = new CoreAudioFileReader
-            (source,
-             threading ?
-             CoreAudioFileReader::DecodeThreaded :
-             CoreAudioFileReader::DecodeAtOnce,
-             CoreAudioFileReader::CacheInTemporaryFile,
-             targetRate,
-             reporter);
-
-        if (!reader->isOK()) {
-            delete reader;
-            reader = 0;
-        }
-    }
-#endif
-
-    if (reader) {
-        if (reader->isOK()) {
-#ifdef DEBUG_AUDIO_FILE_READER_FACTORY
-            cerr << "AudioFileReaderFactory: Reader is OK" << endl;
-#endif
-            return reader;
-        }
-        cerr << "AudioFileReaderFactory: Preferred reader for "
-                  << "url \"" << source.getLocation()
-                  << "\" (content type \""
-                  << source.getContentType() << "\") failed";
-
-        if (reader->getError() != "") {
-            cerr << ": \"" << reader->getError() << "\"";
-        }
-        cerr << endl;
-        delete reader;
-        reader = 0;
-    }
-
-    cerr << "AudioFileReaderFactory: No reader" << endl;
-    return reader;
+    SVCERR << "AudioFileReaderFactory::Failed to create a reader for "
+           << "url \"" << source.getLocation()
+           << "\" (local filename \"" << source.getLocalFilename()
+           << "\", content type \""
+           << source.getContentType() << "\")" << endl;
+    return nullptr;
 }
 
--- a/data/fileio/AudioFileReaderFactory.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/AudioFileReaderFactory.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _AUDIO_FILE_READER_FACTORY_H_
-#define _AUDIO_FILE_READER_FACTORY_H_
+#ifndef AUDIO_FILE_READER_FACTORY_H
+#define AUDIO_FILE_READER_FACTORY_H
 
 #include <QString>
 
@@ -34,65 +34,113 @@
      */
     static QString getKnownExtensions();
 
+    enum class Normalisation {
+
+        /**
+         * Do not normalise file data.
+         */
+        None,
+
+        /**
+         * Normalise file data to abs(max) == 1.0.
+         */
+        Peak
+    };
+
+    enum class GaplessMode {
+
+        /** 
+         * Any encoder delay and padding found in file metadata will
+         * be compensated for, giving gapless decoding (assuming the
+         * metadata are correct). This is currently only applicable to
+         * mp3 files: all other supported files are always gapless
+         * where the file metadata provides for it. See documentation
+         * for MP3FileReader::GaplessMode for details of the specific
+         * implementation.
+         */
+        Gapless,
+
+        /**
+         * No delay compensation will happen and the results will be
+         * equivalent to the behaviour of audio readers before the
+         * compensation logic was implemented. This is currently only
+         * applicable to mp3 files: all other supported files are
+         * always gapless where the file metadata provides for it. See
+         * documentation for MP3FileReader::GaplessMode for details of
+         * the specific implementation.
+         */
+        Gappy
+    };
+
+    enum class ThreadingMode {
+        
+        /** 
+         * Any necessary decoding will happen synchronously when the
+         * reader is created.
+         */
+        NotThreaded,
+        
+        /**        
+         * If the reader supports threaded decoding, it will be used
+         * and the file will be decoded in a background thread. If the
+         * reader does not support threaded decoding, behaviour will
+         * be as for NotThreaded.
+         */
+        Threaded
+    };
+
+    struct Parameters {
+
+        /**
+         * Sample rate to open the file at. If zero (the default), the
+         * file's native rate will be used. If non-zero, the file will
+         * be automatically resampled to that rate.  You can query
+         * reader->getNativeRate() if you want to find out whether the
+         * file needed to be resampled.
+         */
+        sv_samplerate_t targetRate;
+
+        /**
+         * Normalisation to use. The default is Normalisation::None.
+         */
+        Normalisation normalisation;
+
+        /**
+         * Gapless mode to use. The default is GaplessMode::Gapless.
+         */
+        GaplessMode gaplessMode;
+
+        /**
+         * Threading mode. The default is ThreadingMode::NotThreaded.
+         */
+        ThreadingMode threadingMode;
+        
+        Parameters() :
+            targetRate(0),
+            normalisation(Normalisation::None),
+            gaplessMode(GaplessMode::Gapless),
+            threadingMode(ThreadingMode::NotThreaded)
+        { }
+    };
+    
     /**
      * Return an audio file reader initialised to the file at the
      * given path, or NULL if no suitable reader for this path is
      * available or the file cannot be opened.
      *
-     * If targetRate is non-zero, the file will be resampled to that
-     * rate (transparently).  You can query reader->getNativeRate()
-     * if you want to find out whether the file is being resampled
-     * or not.
-     *
-     * If normalised is true, the file data will be normalised to
-     * abs(max) == 1.0. Otherwise the file will not be normalised.
-     *
      * If a ProgressReporter is provided, it will be updated with
-     * progress status.  Caller retains ownership of the reporter
-     * object.
+     * progress status. This will only be meaningful if decoding is
+     * being carried out in non-threaded mode (either because the
+     * threaded parameter was not supplied or because the specific
+     * file reader used does not support it); in threaded mode,
+     * reported progress will jump straight to 100% before threading
+     * takes over. Caller retains ownership of the reporter object.
      *
      * Caller owns the returned object and must delete it after use.
      */
     static AudioFileReader *createReader(FileSource source,
-                                         sv_samplerate_t targetRate = 0,
-                                         bool normalised = false,
+                                         Parameters parameters,
                                          ProgressReporter *reporter = 0);
-
-    /**
-     * Return an audio file reader initialised to the file at the
-     * given path, or NULL if no suitable reader for this path is
-     * available or the file cannot be opened.  If the reader supports
-     * threaded decoding, it will be used and the file decoded in a
-     * background thread.
-     *
-     * If targetRate is non-zero, the file will be resampled to that
-     * rate (transparently).  You can query reader->getNativeRate()
-     * if you want to find out whether the file is being resampled
-     * or not.
-     *
-     * If normalised is true, the file data will be normalised to
-     * abs(max) == 1.0. Otherwise the file will not be normalised.
-     *
-     * If a ProgressReporter is provided, it will be updated with
-     * progress status.  This will only be meaningful if threading
-     * mode is not used because the file reader in use does not
-     * support it; otherwise progress as reported will jump straight
-     * to 100% before threading mode takes over.  Caller retains
-     * ownership of the reporter object.
-     *
-     * Caller owns the returned object and must delete it after use.
-     */
-    static AudioFileReader *createThreadingReader(FileSource source,
-                                                  sv_samplerate_t targetRate = 0,
-                                                  bool normalised = false,
-                                                  ProgressReporter *reporter = 0);
-
-protected:
-    static AudioFileReader *create(FileSource source,
-                                   sv_samplerate_t targetRate,
-                                   bool normalised,
-                                   bool threading,
-                                   ProgressReporter *reporter);
 };
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/AudioFileSizeEstimator.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,111 @@
+/* -*- 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 "AudioFileSizeEstimator.h"
+
+#include "WavFileReader.h"
+
+#include <QFile>
+
+#include "base/Debug.h"
+
+sv_frame_t
+AudioFileSizeEstimator::estimate(FileSource source,
+                                 sv_samplerate_t targetRate)
+{
+    sv_frame_t estimate = 0;
+    
+    SVDEBUG << "AudioFileSizeEstimator: Sample count estimate requested for file \""
+            << source.getLocalFilename() << "\"" << endl;
+
+    // Most of our file readers don't know the sample count until
+    // after they've finished decoding. This is an exception:
+
+    WavFileReader *reader = new WavFileReader(source);
+    if (reader->isOK() &&
+        reader->getChannelCount() > 0 &&
+        reader->getFrameCount() > 0) {
+        sv_frame_t samples =
+            reader->getFrameCount() * reader->getChannelCount();
+        sv_samplerate_t rate = reader->getSampleRate();
+        if (targetRate != 0.0 && targetRate != rate) {
+            samples = sv_frame_t(double(samples) * targetRate / rate);
+        }
+        delete reader;
+        SVDEBUG << "AudioFileSizeEstimator: WAV file reader accepts this file, reports "
+                << samples << " samples" << endl;
+        estimate = samples;
+    } else {
+        SVDEBUG << "AudioFileSizeEstimator: WAV file reader doesn't like this file, "
+                << "estimating from file size and extension instead" << endl;
+    }
+
+    if (estimate == 0) {
+
+        // The remainder just makes an estimate based on the file size
+        // and extension. We don't even know its sample rate at this
+        // point, so the following is a wild guess.
+        
+        double rateRatio = 1.0;
+        if (targetRate != 0.0) {
+            rateRatio = targetRate / 44100.0;
+        }
+    
+        QString extension = source.getExtension();
+
+        source.waitForData();
+        if (!source.isOK()) return 0;
+
+        sv_frame_t sz = 0;
+
+        {
+            QFile f(source.getLocalFilename());
+            if (f.open(QFile::ReadOnly)) {
+                SVDEBUG << "AudioFileSizeEstimator: opened file, size is "
+                        << f.size() << endl;
+                sz = f.size();
+                f.close();
+            }
+        }
+
+        if (extension == "ogg" || extension == "oga" ||
+            extension == "m4a" || extension == "mp3" ||
+            extension == "wma") {
+
+            // Usually a lossy file. Compression ratios can vary
+            // dramatically, but don't usually exceed about 20x compared
+            // to 16-bit PCM (e.g. a 128kbps mp3 has 11x ratio over WAV at
+            // 44.1kHz). We can estimate the number of samples to be file
+            // size x 20, divided by 2 as we're comparing with 16-bit PCM.
+
+            estimate = sv_frame_t(double(sz) * 10 * rateRatio);
+        }
+
+        if (extension == "flac") {
+        
+            // FLAC usually takes up a bit more than half the space of
+            // 16-bit PCM. So the number of 16-bit samples is roughly the
+            // same as the file size in bytes. As above, let's be
+            // conservative.
+
+            estimate = sv_frame_t(double(sz) * 1.2 * rateRatio);
+        }
+
+        SVDEBUG << "AudioFileSizeEstimator: for extension \""
+                << extension << "\", estimate = " << estimate << " samples" << endl;
+    }
+    
+    return estimate;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/AudioFileSizeEstimator.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,49 @@
+/* -*- 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 AUDIO_FILE_SIZE_ESTIMATOR_H
+#define AUDIO_FILE_SIZE_ESTIMATOR_H
+
+#include "base/BaseTypes.h"
+#include "data/fileio/FileSource.h"
+
+/**
+ * Estimate the number of samples in an audio file. For many
+ * compressed files this returns only a very approximate estimate,
+ * based on a rough estimate of compression ratio. Initially we're
+ * only aiming for a conservative estimate for purposes like "will
+ * this file fit in memory?" (and if unsure, say no).
+ */
+class AudioFileSizeEstimator
+{
+public:
+    /**
+     * Return an estimate of the number of samples (across all
+     * channels) in the given audio file, once it has been decoded and
+     * (if applicable) resampled to the given rate.
+     *
+     * This function is intended to be reasonably fast -- it may open
+     * the file, but it should not do any decoding. (However, if the
+     * file source is remote, it will probably be downloaded in its
+     * entirety before anything can be estimated.)
+     *
+     * The returned value is an estimate, and is deliberately usually
+     * on the high side. If the estimator has no idea at all, this
+     * will return 0.
+     */
+    static sv_frame_t estimate(FileSource source,
+                               sv_samplerate_t targetRate = 0);
+};
+
+#endif
--- a/data/fileio/BZipFileDevice.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/BZipFileDevice.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -23,6 +23,7 @@
 
 BZipFileDevice::BZipFileDevice(QString fileName) :
     m_fileName(fileName),
+    m_qfile(fileName),
     m_file(0),
     m_bzFile(0),
     m_atEnd(true),
@@ -72,9 +73,16 @@
 
     if (mode & WriteOnly) {
 
-        m_file = fopen(m_fileName.toLocal8Bit().data(), "wb");
+        if (!m_qfile.open(QIODevice::WriteOnly)) {
+            setErrorString(tr("Failed to open file for writing"));
+            m_ok = false;
+            return false;
+        }
+        
+        m_file = fdopen(m_qfile.handle(), "wb");
         if (!m_file) {
-            setErrorString(tr("Failed to open file for writing"));
+            setErrorString(tr("Failed to open file handle for writing"));
+            m_qfile.close();
             m_ok = false;
             return false;
         }
@@ -85,6 +93,7 @@
         if (!m_bzFile) {
             fclose(m_file);
             m_file = 0;
+            m_qfile.close();
             setErrorString(tr("Failed to open bzip2 stream for writing"));
             m_ok = false;
             return false;
@@ -99,9 +108,15 @@
 
     if (mode & ReadOnly) {
 
-        m_file = fopen(m_fileName.toLocal8Bit().data(), "rb");
+        if (!m_qfile.open(QIODevice::ReadOnly)) {
+            setErrorString(tr("Failed to open file for reading"));
+            m_ok = false;
+            return false;
+        }
+        
+        m_file = fdopen(m_qfile.handle(), "rb");
         if (!m_file) {
-            setErrorString(tr("Failed to open file for reading"));
+            setErrorString(tr("Failed to open file handle for reading"));
             m_ok = false;
             return false;
         }
@@ -112,6 +127,7 @@
         if (!m_bzFile) {
             fclose(m_file);
             m_file = 0;
+            m_qfile.close();
             setErrorString(tr("Failed to open bzip2 stream for reading"));
             m_ok = false;
             return false;
@@ -150,6 +166,7 @@
 	    setErrorString(tr("bzip2 stream write close error"));
 	}
         fclose(m_file);
+        m_qfile.close();
         m_bzFile = 0;
         m_file = 0;
         m_ok = false;
@@ -162,6 +179,7 @@
             setErrorString(tr("bzip2 stream read close error"));
         }
         fclose(m_file);
+        m_qfile.close();
         m_bzFile = 0;
         m_file = 0;
         m_ok = false;
--- a/data/fileio/BZipFileDevice.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/BZipFileDevice.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,10 +13,11 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _BZIP_FILE_DEVICE_H_
-#define _BZIP_FILE_DEVICE_H_
+#ifndef SV_BZIP_FILE_DEVICE_H
+#define SV_BZIP_FILE_DEVICE_H
 
 #include <QIODevice>
+#include <QFile>
 
 #include <bzlib.h>
 
@@ -41,6 +42,7 @@
 
     QString m_fileName;
 
+    QFile m_qfile;
     FILE *m_file;
     BZFILE *m_bzFile;
     bool m_atEnd;
--- a/data/fileio/CSVFileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/CSVFileReader.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _CSV_FILE_READER_H_
-#define _CSV_FILE_READER_H_
+#ifndef SV_CSV_FILE_READER_H
+#define SV_CSV_FILE_READER_H
 
 #include "DataFileReader.h"
 
--- a/data/fileio/CSVFormat.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/CSVFormat.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -25,6 +25,8 @@
 
 #include <iostream>
 
+#include "base/Debug.h"
+
 CSVFormat::CSVFormat(QString path) :
     m_separator(""),
     m_sampleRate(44100),
@@ -92,7 +94,6 @@
             return;
         }
     }
-    m_separator = " ";
 }
 
 void
@@ -100,7 +101,7 @@
 {
     if (m_separator == "") guessSeparator(line);
 
-    QStringList list = StringBits::split(line, m_separator[0], m_allowQuoting);
+    QStringList list = StringBits::split(line, getSeparator(), m_allowQuoting);
 
     int cols = list.size();
     if (lineno == 0 || (cols > m_columnCount)) m_columnCount = cols;
@@ -182,11 +183,13 @@
         }
     }
 
-//    cerr << "Estimated column qualities: ";
-//    for (int i = 0; i < m_columnCount; ++i) {
-//        cerr << int(m_columnQualities[i]) << " ";
-//    }
-//    cerr << endl;
+    if (lineno < 10) {
+        SVDEBUG << "Estimated column qualities for line " << lineno << " (reporting up to first 10): ";
+        for (int i = 0; i < m_columnCount; ++i) {
+            SVDEBUG << int(m_columnQualities[i]) << " ";
+        }
+        SVDEBUG << endl;
+    }
 }
 
 void
@@ -314,15 +317,15 @@
         }
     }
 
-//    cerr << "Estimated column purposes: ";
-//    for (int i = 0; i < m_columnCount; ++i) {
-//        cerr << int(m_columnPurposes[i]) << " ";
-//    }
-//    cerr << endl;
+    SVDEBUG << "Estimated column purposes: ";
+    for (int i = 0; i < m_columnCount; ++i) {
+        SVDEBUG << int(m_columnPurposes[i]) << " ";
+    }
+    SVDEBUG << endl;
 
-//    cerr << "Estimated model type: " << m_modelType << endl;
-//    cerr << "Estimated timing type: " << m_timingType << endl;
-//    cerr << "Estimated units: " << m_timeUnits << endl;
+    SVDEBUG << "Estimated model type: " << m_modelType << endl;
+    SVDEBUG << "Estimated timing type: " << m_timingType << endl;
+    SVDEBUG << "Estimated units: " << m_timeUnits << endl;
 }
 
 CSVFormat::ColumnPurpose
--- a/data/fileio/CSVFormat.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/CSVFormat.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _CSV_FORMAT_H_
-#define _CSV_FORMAT_H_
+#ifndef SV_CSV_FORMAT_H
+#define SV_CSV_FORMAT_H
 
 #include <QString>
 #include <QStringList>
--- a/data/fileio/CodedAudioFileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/CodedAudioFileReader.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -20,13 +20,17 @@
 #include "base/Exceptions.h"
 #include "base/Profiler.h"
 #include "base/Serialiser.h"
-#include "base/Resampler.h"
+#include "base/StorageAdviser.h"
+
+#include <bqresample/Resampler.h>
 
 #include <stdint.h>
 #include <iostream>
 #include <QDir>
 #include <QMutexLocker>
 
+using namespace std;
+
 CodedAudioFileReader::CodedAudioFileReader(CacheMode cacheMode,
                                            sv_samplerate_t targetRate,
                                            bool normalised) :
@@ -38,15 +42,26 @@
     m_cacheFileReader(0),
     m_cacheWriteBuffer(0),
     m_cacheWriteBufferIndex(0),
-    m_cacheWriteBufferSize(16384),
+    m_cacheWriteBufferFrames(65536),
     m_resampler(0),
     m_resampleBuffer(0),
+    m_resampleBufferFrames(0),
     m_fileFrameCount(0),
     m_normalised(normalised),
     m_max(0.f),
-    m_gain(1.f)
+    m_gain(1.f),
+    m_trimFromStart(0),
+    m_trimFromEnd(0),
+    m_clippedCount(0),
+    m_firstNonzero(0),
+    m_lastNonzero(0)
 {
-    SVDEBUG << "CodedAudioFileReader::CodedAudioFileReader: rate " << targetRate << ", normalised = " << normalised << endl;
+    SVDEBUG << "CodedAudioFileReader:: cache mode: " << cacheMode
+            << " (" << (cacheMode == CacheInTemporaryFile
+                        ? "CacheInTemporaryFile" : "CacheInMemory") << ")"
+            << ", rate: " << targetRate
+            << (targetRate == 0 ? " (use source rate)" : "")
+            << ", normalised: " << normalised << endl;
 
     m_frameCount = 0;
     m_sampleRate = targetRate;
@@ -56,29 +71,43 @@
 {
     QMutexLocker locker(&m_cacheMutex);
 
-    endSerialised();
-
+    if (m_serialiser) endSerialised();
+    
     if (m_cacheFileWritePtr) sf_close(m_cacheFileWritePtr);
 
     SVDEBUG << "CodedAudioFileReader::~CodedAudioFileReader: deleting cache file reader" << endl;
 
     delete m_cacheFileReader;
     delete[] m_cacheWriteBuffer;
-
+    
     if (m_cacheFileName != "") {
+        SVDEBUG << "CodedAudioFileReader::~CodedAudioFileReader: deleting cache file " << m_cacheFileName << endl;
         if (!QFile(m_cacheFileName).remove()) {
-            cerr << "WARNING: CodedAudioFileReader::~CodedAudioFileReader: Failed to delete cache file \"" << m_cacheFileName << "\"" << endl;
+            SVDEBUG << "WARNING: CodedAudioFileReader::~CodedAudioFileReader: Failed to delete cache file \"" << m_cacheFileName << "\"" << endl;
         }
     }
 
     delete m_resampler;
     delete[] m_resampleBuffer;
+
+    if (!m_data.empty()) {
+        StorageAdviser::notifyDoneAllocation
+            (StorageAdviser::MemoryAllocation,
+             (m_data.size() * sizeof(float)) / 1024);
+    }
+}
+
+void
+CodedAudioFileReader::setFramesToTrim(sv_frame_t fromStart, sv_frame_t fromEnd)
+{
+    m_trimFromStart = fromStart;
+    m_trimFromEnd = fromEnd;
 }
 
 void
 CodedAudioFileReader::startSerialised(QString id)
 {
-    SVDEBUG << "CodedAudioFileReader::startSerialised(" << id << ")" << endl;
+    SVDEBUG << "CodedAudioFileReader(" << this << ")::startSerialised: id = " << id << endl;
 
     delete m_serialiser;
     m_serialiser = new Serialiser(id);
@@ -100,9 +129,14 @@
 
     SVDEBUG << "CodedAudioFileReader::initialiseDecodeCache: file rate = " << m_fileRate << endl;
 
+    if (m_channelCount == 0) {
+        SVCERR << "CodedAudioFileReader::initialiseDecodeCache: No channel count set!" << endl;
+        throw std::logic_error("No channel count set");
+    }
+    
     if (m_fileRate == 0) {
-        cerr << "CodedAudioFileReader::initialiseDecodeCache: ERROR: File sample rate unknown (bug in subclass implementation?)" << endl;
-        throw FileOperationFailed("(coded file)", "File sample rate unknown (bug in subclass implementation?)");
+        SVDEBUG << "CodedAudioFileReader::initialiseDecodeCache: ERROR: File sample rate unknown (bug in subclass implementation?)" << endl;
+        throw FileOperationFailed("(coded file)", "sample rate unknown (bug in subclass implementation?)");
     }
     if (m_sampleRate == 0) {
         m_sampleRate = m_fileRate;
@@ -110,28 +144,33 @@
     }
     if (m_fileRate != m_sampleRate) {
         SVDEBUG << "CodedAudioFileReader: resampling " << m_fileRate << " -> " <<  m_sampleRate << endl;
-        m_resampler = new Resampler(Resampler::FastestTolerable,
-                                    m_channelCount,
-                                    m_cacheWriteBufferSize);
+
+        breakfastquay::Resampler::Parameters params;
+        params.quality = breakfastquay::Resampler::FastestTolerable;
+        params.maxBufferSize = int(m_cacheWriteBufferFrames);
+        params.initialSampleRate = m_fileRate;
+        m_resampler = new breakfastquay::Resampler(params, m_channelCount);
+
         double ratio = m_sampleRate / m_fileRate;
-        m_resampleBuffer = new float
-            [lrint(ceil(double(m_cacheWriteBufferSize) * m_channelCount * ratio + 1))];
+        m_resampleBufferFrames = int(ceil(double(m_cacheWriteBufferFrames) *
+                                          ratio + 1));
+        m_resampleBuffer = new float[m_resampleBufferFrames * m_channelCount];
     }
 
-    m_cacheWriteBuffer = new float[m_cacheWriteBufferSize * m_channelCount];
+    m_cacheWriteBuffer = new float[m_cacheWriteBufferFrames * m_channelCount];
     m_cacheWriteBufferIndex = 0;
 
     if (m_cacheMode == CacheInTemporaryFile) {
 
         try {
             QDir dir(TempDirectory::getInstance()->getPath());
-            m_cacheFileName = dir.filePath(QString("decoded_%1.wav")
+            m_cacheFileName = dir.filePath(QString("decoded_%1.w64")
                                            .arg((intptr_t)this));
 
             SF_INFO fileInfo;
             int fileRate = int(round(m_sampleRate));
             if (m_sampleRate != sv_samplerate_t(fileRate)) {
-                cerr << "CodedAudioFileReader: WARNING: Non-integer sample rate "
+                SVDEBUG << "CodedAudioFileReader: WARNING: Non-integer sample rate "
                      << m_sampleRate << " presented for writing, rounding to " << fileRate
                      << endl;
             }
@@ -157,10 +196,15 @@
             // tests.)
             //
             // So: now we write floats.
-            fileInfo.format = SF_FORMAT_WAV | SF_FORMAT_FLOAT;
-    
-            m_cacheFileWritePtr = sf_open(m_cacheFileName.toLocal8Bit(),
-                                          SFM_WRITE, &fileInfo);
+            fileInfo.format = SF_FORMAT_W64 | SF_FORMAT_FLOAT;
+
+#ifdef Q_OS_WIN
+            m_cacheFileWritePtr = sf_wchar_open
+                ((LPCWSTR)m_cacheFileName.utf16(), SFM_WRITE, &fileInfo);
+#else
+            m_cacheFileWritePtr = sf_open
+                (m_cacheFileName.toLocal8Bit(), SFM_WRITE, &fileInfo);
+#endif
 
             if (m_cacheFileWritePtr) {
 
@@ -172,7 +216,7 @@
                 m_cacheFileReader = new WavFileReader(m_cacheFileName);
 
                 if (!m_cacheFileReader->isOK()) {
-                    cerr << "ERROR: CodedAudioFileReader::initialiseDecodeCache: Failed to construct WAV file reader for temporary file: " << m_cacheFileReader->getError() << endl;
+                    SVDEBUG << "ERROR: CodedAudioFileReader::initialiseDecodeCache: Failed to construct WAV file reader for temporary file: " << m_cacheFileReader->getError() << endl;
                     delete m_cacheFileReader;
                     m_cacheFileReader = 0;
                     m_cacheMode = CacheInMemory;
@@ -180,12 +224,12 @@
                 }
 
             } else {
-                cerr << "CodedAudioFileReader::initialiseDecodeCache: failed to open cache file \"" << m_cacheFileName << "\" (" << m_channelCount << " channels, sample rate " << m_sampleRate << " for writing, falling back to in-memory cache" << endl;
+                SVDEBUG << "CodedAudioFileReader::initialiseDecodeCache: failed to open cache file \"" << m_cacheFileName << "\" (" << m_channelCount << " channels, sample rate " << m_sampleRate << " for writing, falling back to in-memory cache" << endl;
                 m_cacheMode = CacheInMemory;
             }
 
         } catch (DirectoryCreationFailed f) {
-            cerr << "CodedAudioFileReader::initialiseDecodeCache: failed to create temporary directory! Falling back to in-memory cache" << endl;
+            SVDEBUG << "CodedAudioFileReader::initialiseDecodeCache: failed to create temporary directory! Falling back to in-memory cache" << endl;
             m_cacheMode = CacheInMemory;
         }
     }
@@ -194,6 +238,11 @@
         m_data.clear();
     }
 
+    if (m_trimFromEnd >= (m_cacheWriteBufferFrames * m_channelCount)) {
+        SVCERR << "WARNING: CodedAudioFileReader::setSamplesToTrim: Can't handle trimming more frames from end (" << m_trimFromEnd << ") than can be stored in cache-write buffer (" << (m_cacheWriteBufferFrames * m_channelCount) << "), won't trim anything from the end after all";
+        m_trimFromEnd = 0;
+    }
+
     m_initialised = true;
 }
 
@@ -205,25 +254,20 @@
     if (!m_initialised) return;
 
     for (sv_frame_t i = 0; i < nframes; ++i) {
+
+        if (m_trimFromStart > 0) {
+            --m_trimFromStart;
+            continue;
+        }
         
         for (int c = 0; c < m_channelCount; ++c) {
 
             float sample = samples[c][i];
-        
             m_cacheWriteBuffer[m_cacheWriteBufferIndex++] = sample;
 
-            if (m_cacheWriteBufferIndex ==
-                m_cacheWriteBufferSize * m_channelCount) {
+        }
 
-                pushBuffer(m_cacheWriteBuffer, m_cacheWriteBufferSize, false);
-                m_cacheWriteBufferIndex = 0;
-            }
-
-            if (m_cacheWriteBufferIndex % 10240 == 0 &&
-                m_cacheFileReader) {
-                m_cacheFileReader->updateFrameCount();
-            }
-        }
+        pushCacheWriteBufferMaybe(false);
     }
 }
 
@@ -235,50 +279,40 @@
     if (!m_initialised) return;
 
     for (sv_frame_t i = 0; i < nframes; ++i) {
+
+        if (m_trimFromStart > 0) {
+            --m_trimFromStart;
+            continue;
+        }
         
         for (int c = 0; c < m_channelCount; ++c) {
 
             float sample = samples[i * m_channelCount + c];
         
             m_cacheWriteBuffer[m_cacheWriteBufferIndex++] = sample;
+        }
 
-            if (m_cacheWriteBufferIndex ==
-                m_cacheWriteBufferSize * m_channelCount) {
-
-                pushBuffer(m_cacheWriteBuffer, m_cacheWriteBufferSize, false);
-                m_cacheWriteBufferIndex = 0;
-            }
-
-            if (m_cacheWriteBufferIndex % 10240 == 0 &&
-                m_cacheFileReader) {
-                m_cacheFileReader->updateFrameCount();
-            }
-        }
+        pushCacheWriteBufferMaybe(false);
     }
 }
 
 void
-CodedAudioFileReader::addSamplesToDecodeCache(const SampleBlock &samples)
+CodedAudioFileReader::addSamplesToDecodeCache(const floatvec_t &samples)
 {
     QMutexLocker locker(&m_cacheMutex);
 
     if (!m_initialised) return;
 
     for (float sample: samples) {
+
+        if (m_trimFromStart > 0) {
+            --m_trimFromStart;
+            continue;
+        }
         
         m_cacheWriteBuffer[m_cacheWriteBufferIndex++] = sample;
 
-        if (m_cacheWriteBufferIndex ==
-            m_cacheWriteBufferSize * m_channelCount) {
-
-            pushBuffer(m_cacheWriteBuffer, m_cacheWriteBufferSize, false);
-            m_cacheWriteBufferIndex = 0;
-        }
-
-        if (m_cacheWriteBufferIndex % 10240 == 0 &&
-            m_cacheFileReader) {
-            m_cacheFileReader->updateFrameCount();
-        }
+        pushCacheWriteBufferMaybe(false);
     }
 }
 
@@ -287,16 +321,14 @@
 {
     QMutexLocker locker(&m_cacheMutex);
 
-    Profiler profiler("CodedAudioFileReader::finishDecodeCache", true);
+    Profiler profiler("CodedAudioFileReader::finishDecodeCache");
 
     if (!m_initialised) {
-        cerr << "WARNING: CodedAudioFileReader::finishDecodeCache: Cache was never initialised!" << endl;
+        SVDEBUG << "WARNING: CodedAudioFileReader::finishDecodeCache: Cache was never initialised!" << endl;
         return;
     }
 
-    pushBuffer(m_cacheWriteBuffer,
-               m_cacheWriteBufferIndex / m_channelCount,
-               true);
+    pushCacheWriteBufferMaybe(true);
 
     delete[] m_cacheWriteBuffer;
     m_cacheWriteBuffer = 0;
@@ -308,13 +340,76 @@
     m_resampler = 0;
 
     if (m_cacheMode == CacheInTemporaryFile) {
+
         sf_close(m_cacheFileWritePtr);
         m_cacheFileWritePtr = 0;
         if (m_cacheFileReader) m_cacheFileReader->updateFrameCount();
+
+    } else {
+        // I know, I know, we already allocated it...
+        StorageAdviser::notifyPlannedAllocation
+            (StorageAdviser::MemoryAllocation,
+             (m_data.size() * sizeof(float)) / 1024);
+    }
+
+    SVDEBUG << "CodedAudioFileReader: File decodes to " << m_fileFrameCount
+            << " frames" << endl;
+    if (m_fileFrameCount != m_frameCount) {
+        SVDEBUG << "CodedAudioFileReader: Resampled to " << m_frameCount
+                << " frames" << endl;
+    }
+    SVDEBUG << "CodedAudioFileReader: Signal abs max is " << m_max
+            << ", " << m_clippedCount
+            << " samples clipped, first non-zero frame is at "
+            << m_firstNonzero << ", last at " << m_lastNonzero << endl;
+    if (m_normalised) {
+        SVDEBUG << "CodedAudioFileReader: Normalising, gain is " << m_gain << endl;
     }
 }
 
 void
+CodedAudioFileReader::pushCacheWriteBufferMaybe(bool final)
+{
+    if (final ||
+        (m_cacheWriteBufferIndex ==
+         m_cacheWriteBufferFrames * m_channelCount)) {
+
+        if (m_trimFromEnd > 0) {
+        
+            sv_frame_t framesToPush =
+                (m_cacheWriteBufferIndex / m_channelCount) - m_trimFromEnd;
+
+            if (framesToPush <= 0 && !final) {
+                // This won't do, the buffer is full so we have to push
+                // something. Should have checked for this earlier
+                throw std::logic_error("Buffer full but nothing to push");
+            }
+
+            pushBuffer(m_cacheWriteBuffer, framesToPush, final);
+            
+            m_cacheWriteBufferIndex -= framesToPush * m_channelCount;
+
+            for (sv_frame_t i = 0; i < m_cacheWriteBufferIndex; ++i) {
+                m_cacheWriteBuffer[i] =
+                    m_cacheWriteBuffer[framesToPush * m_channelCount + i];
+            }
+
+        } else {
+
+            pushBuffer(m_cacheWriteBuffer,
+                       m_cacheWriteBufferIndex / m_channelCount,
+                       final);
+
+            m_cacheWriteBufferIndex = 0;
+        }
+
+        if (m_cacheFileReader) {
+            m_cacheFileReader->updateFrameCount();
+        }
+    }
+}
+
+sv_frame_t
 CodedAudioFileReader::pushBuffer(float *buffer, sv_frame_t sz, bool final)
 {
     m_fileFrameCount += sz;
@@ -329,6 +424,8 @@
     } else {
         pushBufferNonResampling(buffer, sz);
     }
+
+    return sz;
 }
 
 void
@@ -337,24 +434,37 @@
     float clip = 1.0;
     sv_frame_t count = sz * m_channelCount;
 
-    if (m_normalised) {
-        for (sv_frame_t i = 0; i < count; ++i) {
-            float v = fabsf(buffer[i]);
-            if (v > m_max) {
-                m_max = v;
-                m_gain = 1.f / m_max;
+    // statistics
+    for (sv_frame_t j = 0; j < sz; ++j) {
+        for (int c = 0; c < m_channelCount; ++c) {
+            sv_frame_t i = j * m_channelCount + c;
+            float v = buffer[i];
+            if (!m_normalised) {
+                if (v > clip) {
+                    buffer[i] = clip;
+                    ++m_clippedCount;
+                } else if (v < -clip) {
+                    buffer[i] = -clip;
+                    ++m_clippedCount;
+                }
+            }
+            v = fabsf(v);
+            if (v != 0.f) {
+                if (m_firstNonzero == 0) {
+                    m_firstNonzero = m_frameCount;
+                }
+                m_lastNonzero = m_frameCount;
+                if (v > m_max) {
+                    m_max = v;
+                }
             }
         }
-    } else {
-        for (sv_frame_t i = 0; i < count; ++i) {
-            if (buffer[i] >  clip) buffer[i] =  clip;
-        }
-        for (sv_frame_t i = 0; i < count; ++i) {
-            if (buffer[i] < -clip) buffer[i] = -clip;
-        }
+        ++m_frameCount;
     }
 
-    m_frameCount += sz;
+    if (m_max > 0.f) {
+        m_gain = 1.f / m_max; // used when normalising only
+    }
 
     switch (m_cacheMode) {
 
@@ -367,10 +477,8 @@
         break;
 
     case CacheInMemory:
-        m_dataLock.lockForWrite();
-        for (sv_frame_t s = 0; s < count; ++s) {
-            m_data.push_back(buffer[s]);
-        }
+        m_dataLock.lock();
+        m_data.insert(m_data.end(), buffer, buffer + count);
         m_dataLock.unlock();
         break;
     }
@@ -380,14 +488,15 @@
 CodedAudioFileReader::pushBufferResampling(float *buffer, sv_frame_t sz,
                                            double ratio, bool final)
 {
-    SVDEBUG << "pushBufferResampling: ratio = " << ratio << ", sz = " << sz << ", final = " << final << endl;
+//    SVDEBUG << "pushBufferResampling: ratio = " << ratio << ", sz = " << sz << ", final = " << final << endl;
 
     if (sz > 0) {
 
         sv_frame_t out = m_resampler->resampleInterleaved
-            (buffer,
-             m_resampleBuffer,
-             sz,
+            (m_resampleBuffer,
+             m_resampleBufferFrames,
+             buffer,
+             int(sz),
              ratio,
              false);
 
@@ -403,15 +512,16 @@
 
         sv_frame_t padSamples = padFrames * m_channelCount;
 
-        SVDEBUG << "frameCount = " << m_frameCount << ", equivFileFrames = " << double(m_frameCount) / ratio << ", m_fileFrameCount = " << m_fileFrameCount << ", padFrames= " << padFrames << ", padSamples = " << padSamples << endl;
+        SVDEBUG << "CodedAudioFileReader::pushBufferResampling: frameCount = " << m_frameCount << ", equivFileFrames = " << double(m_frameCount) / ratio << ", m_fileFrameCount = " << m_fileFrameCount << ", padFrames = " << padFrames << ", padSamples = " << padSamples << endl;
 
         float *padding = new float[padSamples];
         for (sv_frame_t i = 0; i < padSamples; ++i) padding[i] = 0.f;
 
         sv_frame_t out = m_resampler->resampleInterleaved
-            (padding,
-             m_resampleBuffer,
-             padFrames,
+            (m_resampleBuffer,
+             m_resampleBufferFrames,
+             padding,
+             int(padFrames),
              ratio,
              true);
 
@@ -424,7 +534,7 @@
     }
 }
 
-SampleBlock
+floatvec_t
 CodedAudioFileReader::getInterleavedFrames(sv_frame_t start, sv_frame_t count) const
 {
     // Lock is only required in CacheInMemory mode (the cache file
@@ -433,10 +543,10 @@
 
     if (!m_initialised) {
         SVDEBUG << "CodedAudioFileReader::getInterleavedFrames: not initialised" << endl;
-        return SampleBlock();
+        return {};
     }
 
-    SampleBlock frames;
+    floatvec_t frames;
     
     switch (m_cacheMode) {
 
@@ -448,22 +558,23 @@
 
     case CacheInMemory:
     {
-        if (!isOK()) return SampleBlock();
-        if (count == 0) return SampleBlock();
+        if (!isOK()) return {};
+        if (count == 0) return {};
 
-        sv_frame_t idx = start * m_channelCount;
-        sv_frame_t i = 0;
-        sv_frame_t n = count * m_channelCount;
+        sv_frame_t ix0 = start * m_channelCount;
+        sv_frame_t ix1 = ix0 + (count * m_channelCount);
 
-        frames.resize(size_t(n));
-
-        m_dataLock.lockForRead();
-        while (i < n && in_range_for(m_data, idx)) {
-            frames[size_t(i++)] = m_data[size_t(idx++)];
-        }
+        // This lock used to be a QReadWriteLock, but it appears that
+        // its lock mechanism is significantly slower than QMutex so
+        // it's not a good idea in cases like this where we don't
+        // really have threads taking a long time to read concurrently
+        m_dataLock.lock();
+        sv_frame_t n = sv_frame_t(m_data.size());
+        if (ix0 > n) ix0 = n;
+        if (ix1 > n) ix1 = n;
+        frames = floatvec_t(m_data.begin() + ix0, m_data.begin() + ix1);
         m_dataLock.unlock();
-
-        frames.resize(size_t(i));
+        break;
     }
     }
 
--- a/data/fileio/CodedAudioFileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/CodedAudioFileReader.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,18 +13,27 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _CODED_AUDIO_FILE_READER_H_
-#define _CODED_AUDIO_FILE_READER_H_
+#ifndef SV_CODED_AUDIO_FILE_READER_H
+#define SV_CODED_AUDIO_FILE_READER_H
 
 #include "AudioFileReader.h"
 
-#include <sndfile.h>
 #include <QMutex>
 #include <QReadWriteLock>
 
+#ifdef Q_OS_WIN
+#include <windows.h>
+#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1
+#endif
+
+#include <sndfile.h>
+
 class WavFileReader;
 class Serialiser;
-class Resampler;
+
+namespace breakfastquay {
+    class Resampler;
+}
 
 class CodedAudioFileReader : public AudioFileReader
 {
@@ -38,7 +47,12 @@
         CacheInMemory
     };
 
-    virtual SampleBlock getInterleavedFrames(sv_frame_t start, sv_frame_t count) const;
+    enum DecodeMode {
+        DecodeAtOnce, // decode the file on construction, with progress 
+        DecodeThreaded // decode in a background thread after construction
+    };
+
+    virtual floatvec_t getInterleavedFrames(sv_frame_t start, sv_frame_t count) const;
 
     virtual sv_samplerate_t getNativeRate() const { return m_fileRate; }
 
@@ -57,10 +71,13 @@
 
     void initialiseDecodeCache(); // samplerate, channels must have been set
 
+    // compensation for encoder delays:
+    void setFramesToTrim(sv_frame_t fromStart, sv_frame_t fromEnd);
+    
     // may throw InsufficientDiscSpace:
     void addSamplesToDecodeCache(float **samples, sv_frame_t nframes);
     void addSamplesToDecodeCache(float *samplesInterleaved, sv_frame_t nframes);
-    void addSamplesToDecodeCache(const SampleBlock &interleaved);
+    void addSamplesToDecodeCache(const floatvec_t &interleaved);
 
     // may throw InsufficientDiscSpace:
     void finishDecodeCache();
@@ -71,15 +88,21 @@
     void endSerialised();
 
 private:
-    void pushBuffer(float *interleaved, sv_frame_t sz, bool final);
+    void pushCacheWriteBufferMaybe(bool final);
+    
+    sv_frame_t pushBuffer(float *interleaved, sv_frame_t sz, bool final);
+
+    // to be called only by pushBuffer
     void pushBufferResampling(float *interleaved, sv_frame_t sz, double ratio, bool final);
+
+    // to be called only by pushBuffer and pushBufferResampling
     void pushBufferNonResampling(float *interleaved, sv_frame_t sz);
 
 protected:
     QMutex m_cacheMutex;
     CacheMode m_cacheMode;
-    SampleBlock m_data;
-    mutable QReadWriteLock m_dataLock;
+    floatvec_t m_data;
+    mutable QMutex m_dataLock;
     bool m_initialised;
     Serialiser *m_serialiser;
     sv_samplerate_t m_fileRate;
@@ -88,16 +111,24 @@
     SNDFILE *m_cacheFileWritePtr;
     WavFileReader *m_cacheFileReader;
     float *m_cacheWriteBuffer;
-    sv_frame_t m_cacheWriteBufferIndex;
-    sv_frame_t m_cacheWriteBufferSize; // frames
+    sv_frame_t m_cacheWriteBufferIndex;  // buffer write pointer in samples
+    sv_frame_t m_cacheWriteBufferFrames; // buffer size in frames
 
-    Resampler *m_resampler;
+    breakfastquay::Resampler *m_resampler;
     float *m_resampleBuffer;
+    int m_resampleBufferFrames;
     sv_frame_t m_fileFrameCount;
 
     bool m_normalised;
     float m_max;
     float m_gain;
+
+    sv_frame_t m_trimFromStart;
+    sv_frame_t m_trimFromEnd;
+    
+    sv_frame_t m_clippedCount;
+    sv_frame_t m_firstNonzero;
+    sv_frame_t m_lastNonzero;
 };
 
 #endif
--- a/data/fileio/CoreAudioFileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/CoreAudioFileReader.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -71,6 +71,8 @@
     m_completion(0),
     m_decodeThread(0)
 {
+    SVDEBUG << "CoreAudioFileReader: local path: \"" << m_path << "\"" << endl;
+
     m_channelCount = 0;
     m_fileRate = 0;
 
@@ -78,8 +80,6 @@
 
     Profiler profiler("CoreAudioFileReader::CoreAudioFileReader", true);
 
-    SVDEBUG << "CoreAudioFileReader: path is \"" << m_path << "\"" << endl;
-
     QByteArray ba = m_path.toLocal8Bit();
 
     CFURLRef url = CFURLCreateFromFileSystemRepresentation
@@ -114,18 +114,18 @@
     
     UInt32 propsize = sizeof(AudioStreamBasicDescription);
     m_d->err = ExtAudioFileGetProperty
-	(m_d->file, kExtAudioFileProperty_FileDataFormat, &propsize, &m_d->asbd);
+        (m_d->file, kExtAudioFileProperty_FileDataFormat, &propsize, &m_d->asbd);
     
     if (m_d->err) {
         m_error = "CoreAudioReadStream: Error in getting basic description: code " + codestr(m_d->err);
         ExtAudioFileDispose(m_d->file);
         return;
     }
-	
+        
     m_channelCount = m_d->asbd.mChannelsPerFrame;
     m_fileRate = m_d->asbd.mSampleRate;
 
-    cerr << "CoreAudioReadStream: " << m_channelCount << " channels, " << m_fileRate << " Hz" << endl;
+    SVDEBUG << "CoreAudioFileReader: " << m_channelCount << " channels, " << m_fileRate << " Hz" << endl;
 
     m_d->asbd.mFormatID = kAudioFormatLinearPCM;
     m_d->asbd.mFormatFlags =
@@ -137,9 +137,9 @@
     m_d->asbd.mBytesPerPacket = sizeof(float) * m_channelCount;
     m_d->asbd.mFramesPerPacket = 1;
     m_d->asbd.mReserved = 0;
-	
+        
     m_d->err = ExtAudioFileSetProperty
-	(m_d->file, kExtAudioFileProperty_ClientDataFormat, propsize, &m_d->asbd);
+        (m_d->file, kExtAudioFileProperty_ClientDataFormat, propsize, &m_d->asbd);
     
     if (m_d->err) {
         m_error = "CoreAudioReadStream: Error in setting client format: code " + codestr(m_d->err);
@@ -192,7 +192,7 @@
 
 CoreAudioFileReader::~CoreAudioFileReader()
 {
-    cerr << "CoreAudioFileReader::~CoreAudioFileReader" << endl;
+    SVDEBUG << "CoreAudioFileReader::~CoreAudioFileReader" << endl;
 
     if (m_d->valid) {
         ExtAudioFileDispose(m_d->file);
--- a/data/fileio/CoreAudioFileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/CoreAudioFileReader.h	Fri Jan 13 10:29:44 2017 +0000
@@ -31,17 +31,12 @@
     Q_OBJECT
 
 public:
-    enum DecodeMode {
-        DecodeAtOnce, // decode the file on construction, with progress
-        DecodeThreaded // decode in a background thread after construction
-    };
-
     CoreAudioFileReader(FileSource source,
                         DecodeMode decodeMode,
                         CacheMode cacheMode,
                         sv_samplerate_t targetRate = 0,
                         bool normalised = false,
-                        ProgressReporter *reporter = 0);
+                        ProgressReporter *reporter = nullptr);
     virtual ~CoreAudioFileReader();
 
     virtual QString getError() const { return m_error; }
--- a/data/fileio/DecodingWavFileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/DecodingWavFileReader.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -21,8 +21,10 @@
 
 #include <QFileInfo>
 
+using namespace std;
+
 DecodingWavFileReader::DecodingWavFileReader(FileSource source,
-                                             ResampleMode resampleMode,
+                                             DecodeMode decodeMode,
                                              CacheMode mode,
                                              sv_samplerate_t targetRate,
                                              bool normalised,
@@ -37,13 +39,15 @@
     m_reporter(reporter),
     m_decodeThread(0)
 {
+    SVDEBUG << "DecodingWavFileReader: local path: \"" << m_path
+            << "\", decode mode: " << decodeMode << " ("
+            << (decodeMode == DecodeAtOnce ? "DecodeAtOnce" : "DecodeThreaded")
+            << ")" << endl;
+
     m_channelCount = 0;
     m_fileRate = 0;
 
-    SVDEBUG << "DecodingWavFileReader::DecodingWavFileReader(\""
-              << m_path << "\"): rate " << targetRate << endl;
-
-    Profiler profiler("DecodingWavFileReader::DecodingWavFileReader", true);
+    Profiler profiler("DecodingWavFileReader::DecodingWavFileReader");
 
     m_original = new WavFileReader(m_path);
     if (!m_original->isOK()) {
@@ -56,7 +60,7 @@
 
     initialiseDecodeCache();
 
-    if (resampleMode == ResampleAtOnce) {
+    if (decodeMode == DecodeAtOnce) {
 
         if (m_reporter) {
             connect(m_reporter, SIGNAL(cancelled()), this, SLOT(cancelled()));
@@ -67,7 +71,7 @@
         sv_frame_t blockSize = 16384;
         sv_frame_t total = m_original->getFrameCount();
 
-        SampleBlock block;
+        floatvec_t block;
 
         for (sv_frame_t i = 0; i < total; i += blockSize) {
 
@@ -124,7 +128,7 @@
     sv_frame_t blockSize = 16384;
     sv_frame_t total = m_reader->m_original->getFrameCount();
     
-    SampleBlock block;
+    floatvec_t block;
     
     for (sv_frame_t i = 0; i < total; i += blockSize) {
         
@@ -147,7 +151,7 @@
 } 
 
 void
-DecodingWavFileReader::addBlock(const SampleBlock &frames)
+DecodingWavFileReader::addBlock(const floatvec_t &frames)
 {
     addSamplesToDecodeCache(frames);
 
@@ -167,7 +171,7 @@
 }
 
 void
-DecodingWavFileReader::getSupportedExtensions(std::set<QString> &extensions)
+DecodingWavFileReader::getSupportedExtensions(set<QString> &extensions)
 {
     WavFileReader::getSupportedExtensions(extensions);
 }
--- a/data/fileio/DecodingWavFileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/DecodingWavFileReader.h	Fri Jan 13 10:29:44 2017 +0000
@@ -29,13 +29,8 @@
 {
     Q_OBJECT
 public:
-    enum ResampleMode {
-        ResampleAtOnce, // resample the file on construction, with progress dialog
-        ResampleThreaded // resample in a background thread after construction
-    };
-
     DecodingWavFileReader(FileSource source,
-                          ResampleMode resampleMode,
+                          DecodeMode decodeMode, // determines when to resample
                           CacheMode cacheMode,
                           sv_samplerate_t targetRate = 0,
                           bool normalised = false,
@@ -69,7 +64,7 @@
     WavFileReader *m_original;
     ProgressReporter *m_reporter;
 
-    void addBlock(const SampleBlock &frames);
+    void addBlock(const floatvec_t &frames);
     
     class DecodeThread : public Thread
     {
--- a/data/fileio/FileFinder.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/FileFinder.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _FILE_FINDER_H_
-#define _FILE_FINDER_H_
+#ifndef SV_FILE_FINDER_H
+#define SV_FILE_FINDER_H
 
 #include <QString>
 
@@ -28,6 +28,7 @@
         LayerFileNoMidi,
         SessionOrAudioFile,
         ImageFile,
+        SVGFile,
         AnyFile,
         CSVFile,
         LayerFileNonSV,
--- a/data/fileio/FileReadThread.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/FileReadThread.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -19,7 +19,14 @@
 #include "base/Thread.h"
 
 #include <iostream>
+
+#ifdef _MSC_VER
+#include <io.h>
+#define _lseek lseek
+#else
 #include <unistd.h>
+#endif
+
 #include <cstdio>
 
 //#define DEBUG_FILE_READ_THREAD 1
--- a/data/fileio/FileSource.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/FileSource.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -30,8 +30,6 @@
 #include <iostream>
 #include <cstdlib>
 
-#include <unistd.h>
-
 //#define DEBUG_FILE_SOURCE 1
 
 int
--- a/data/fileio/FileSource.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/FileSource.h	Fri Jan 13 10:29:44 2017 +0000
@@ -167,7 +167,8 @@
     QString getContentType() const;
 
     /**
-     * Return the file extension for this file, if any.
+     * Return the file extension for this file, if any. The returned
+     * extension is always lower-case.
      */
     QString getExtension() const;
 
--- a/data/fileio/MIDIFileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/MIDIFileReader.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -53,7 +53,7 @@
 
 using namespace MIDIConstants;
 
-//#define MIDI_SVDEBUG 1
+//#define MIDI_DEBUG 1
 
 
 MIDIFileReader::MIDIFileReader(QString path,
@@ -323,7 +323,7 @@
 
 	    if (!skipToNextTrack()) {
 #ifdef MIDI_DEBUG
-		cerr << "Couldn't find Track " << j << endl;
+		SVDEBUG << "Couldn't find Track " << j << endl;
 #endif
 		m_error = "File corrupted or in non-standard format?";
 		m_format = MIDI_FILE_BAD_FORMAT;
@@ -331,14 +331,14 @@
 	    }
 
 #ifdef MIDI_DEBUG
-	    cerr << "Track has " << m_trackByteCount << " bytes" << endl;
+	    SVDEBUG << "Track has " << m_trackByteCount << " bytes" << endl;
 #endif
 
 	    // Run through the events taking them into our internal
 	    // representation.
 	    if (!parseTrack(i)) {
 #ifdef MIDI_DEBUG
-		cerr << "Track " << j << " parsing failed" << endl;
+		SVDEBUG << "Track " << j << " parsing failed" << endl;
 #endif
 		m_error = "File corrupted or in non-standard format?";
 		m_format = MIDI_FILE_BAD_FORMAT;
@@ -476,7 +476,7 @@
 
 	if (eventCode < 0x80) {
 #ifdef MIDI_DEBUG
-	    cerr << "WARNING: Invalid event code " << eventCode
+	    SVDEBUG << "WARNING: Invalid event code " << eventCode
 		 << " in MIDI file" << endl;
 #endif
 	    throw MIDIException(tr("Invalid event code %1 found").arg(int(eventCode)));
@@ -485,7 +485,7 @@
         deltaTime = getNumberFromMIDIBytes();
 
 #ifdef MIDI_DEBUG
-	cerr << "read delta time " << deltaTime << endl;
+	SVDEBUG << "read delta time " << deltaTime << endl;
 #endif
 
         // Get a single byte
@@ -505,7 +505,7 @@
 #endif
         } else {
 #ifdef MIDI_DEBUG
-	    cerr << "have new event code " << int(midiByte) << endl;
+	    SVDEBUG << "have new event code " << int(midiByte) << endl;
 #endif
             eventCode = midiByte;
 	    data1 = getMIDIByte();
@@ -517,7 +517,7 @@
             messageLength = getNumberFromMIDIBytes();
 
 //#ifdef MIDI_DEBUG
-		cerr << "Meta event of type " << int(metaEventCode) << " and " << messageLength << " bytes found, putting on track " << metaTrack << endl;
+		SVDEBUG << "Meta event of type " << int(metaEventCode) << " and " << messageLength << " bytes found, putting on track " << metaTrack << endl;
 //#endif
             metaMessage = getMIDIBytes(messageLength);
 
@@ -572,7 +572,7 @@
                 midiEvent = new MIDIEvent(deltaTime, eventCode, data1, data2);
 
                 /*
-		cerr << "MIDI event for channel " << channel << " (track "
+		SVDEBUG << "MIDI event for channel " << channel << " (track "
 			  << trackNum << ")" << endl;
 		midiEvent->print();
                           */
@@ -605,7 +605,7 @@
                 messageLength = getNumberFromMIDIBytes(data1);
 
 #ifdef MIDI_DEBUG
-		cerr << "SysEx of " << messageLength << " bytes found" << endl;
+		SVDEBUG << "SysEx of " << messageLength << " bytes found" << endl;
 #endif
 
                 metaMessage= getMIDIBytes(messageLength);
@@ -709,7 +709,7 @@
 void
 MIDIFileReader::updateTempoMap(unsigned int track)
 {
-    cerr << "updateTempoMap for track " << track << " (" << m_midiComposition[track].size() << " events)" << endl;
+    SVDEBUG << "updateTempoMap for track " << track << " (" << m_midiComposition[track].size() << " events)" << endl;
 
     for (MIDITrack::iterator i = m_midiComposition[track].begin();
 	 i != m_midiComposition[track].end(); ++i) {
@@ -723,7 +723,7 @@
 	    
 	    long tempo = (((m0 << 8) + m1) << 8) + m2;
 
-	    cerr << "updateTempoMap: have tempo, it's " << tempo << " at " << (*i)->getTime() << endl;
+	    SVDEBUG << "updateTempoMap: have tempo, it's " << tempo << " at " << (*i)->getTime() << endl;
 
 	    if (tempo != 0) {
 		double qpm = 60000000.0 / double(tempo);
@@ -786,11 +786,11 @@
     SVDEBUG << "MIDIFileReader::getTimeForMIDITime(" << midiTime << ")"
 	      << endl;
     SVDEBUG << "timing division = " << td << endl;
-    cerr << "nearest tempo event (of " << m_tempoMap.size() << ") is at " << tempoMIDITime << " ("
+    SVDEBUG << "nearest tempo event (of " << m_tempoMap.size() << ") is at " << tempoMIDITime << " ("
 	      << tempoRealTime << ")" << endl;
-    cerr << "quarters since then = " << quarters << endl;
-    cerr << "tempo = " << tempo << " quarters per minute" << endl;
-    cerr << "seconds since then = " << seconds << endl;
+    SVDEBUG << "quarters since then = " << quarters << endl;
+    SVDEBUG << "tempo = " << tempo << " quarters per minute" << endl;
+    SVDEBUG << "seconds since then = " << seconds << endl;
     SVDEBUG << "resulting time = " << (tempoRealTime + RealTime::fromSeconds(seconds)) << endl;
 */
 
@@ -928,7 +928,7 @@
     if (existingModel) {
 	model = dynamic_cast<NoteModel *>(existingModel);
 	if (!model) {
-	    cerr << "WARNING: MIDIFileReader::loadTrack: Existing model given, but it isn't a NoteModel -- ignoring it" << endl;
+	    SVDEBUG << "WARNING: MIDIFileReader::loadTrack: Existing model given, but it isn't a NoteModel -- ignoring it" << endl;
 	}
     }
 
--- a/data/fileio/MIDIFileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/MIDIFileReader.h	Fri Jan 13 10:29:44 2017 +0000
@@ -12,15 +12,14 @@
     COPYING included with this distribution for more information.
 */
 
-
 /*
    This is a modified version of a source file from the 
    Rosegarden MIDI and audio sequencer and notation editor.
    This file copyright 2000-2006 Richard Bown and Chris Cannam.
 */
 
-#ifndef _MIDI_FILE_READER_H_
-#define _MIDI_FILE_READER_H_
+#ifndef SV_MIDI_FILE_READER_H
+#define SV_MIDI_FILE_READER_H
 
 #include "DataFileReader.h"
 #include "base/RealTime.h"
@@ -61,7 +60,7 @@
 
 public:
     MIDIFileReader(QString path,
-                   MIDIFileImportPreferenceAcquirer *pref,
+                   MIDIFileImportPreferenceAcquirer *pref, // may be null
                    sv_samplerate_t mainModelSampleRate);
     virtual ~MIDIFileReader();
 
--- a/data/fileio/MP3FileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/MP3FileReader.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -27,87 +27,102 @@
 #include <iostream>
 
 #include <cstdlib>
-#include <unistd.h>
 
 #ifdef HAVE_ID3TAG
 #include <id3tag.h>
 #endif
 
-//#define DEBUG_ID3TAG 1
+#ifdef _WIN32
+#include <io.h>
+#include <fcntl.h>
+#else
+#include <fcntl.h>
+#include <unistd.h>
+#endif
 
 #include <QFileInfo>
 
+#include <QTextCodec>
+
+using std::string;
+
+static sv_frame_t DEFAULT_DECODER_DELAY = 529;
+
 MP3FileReader::MP3FileReader(FileSource source, DecodeMode decodeMode, 
-                             CacheMode mode, sv_samplerate_t targetRate,
+                             CacheMode mode, GaplessMode gaplessMode,
+                             sv_samplerate_t targetRate,
                              bool normalised,
                              ProgressReporter *reporter) :
     CodedAudioFileReader(mode, targetRate, normalised),
     m_source(source),
     m_path(source.getLocalFilename()),
+    m_gaplessMode(gaplessMode),
+    m_decodeErrorShown(false),
     m_decodeThread(0)
 {
+    SVDEBUG << "MP3FileReader: local path: \"" << m_path
+            << "\", decode mode: " << decodeMode << " ("
+            << (decodeMode == DecodeAtOnce ? "DecodeAtOnce" : "DecodeThreaded")
+            << ")" << endl;
+    
     m_channelCount = 0;
     m_fileRate = 0;
     m_fileSize = 0;
     m_bitrateNum = 0;
     m_bitrateDenom = 0;
     m_cancelled = false;
+    m_mp3FrameCount = 0;
     m_completion = 0;
     m_done = false;
     m_reporter = reporter;
 
-    struct stat stat;
-    if (::stat(m_path.toLocal8Bit().data(), &stat) == -1 || stat.st_size == 0) {
-	m_error = QString("File %1 does not exist.").arg(m_path);
-	return;
+    if (m_gaplessMode == GaplessMode::Gapless) {
+        CodedAudioFileReader::setFramesToTrim(DEFAULT_DECODER_DELAY, 0);
+    }
+    
+    m_fileSize = 0;
+
+    m_fileBuffer = 0;
+    m_fileBufferSize = 0;
+
+    m_sampleBuffer = 0;
+    m_sampleBufferSize = 0;
+
+    QFile qfile(m_path);
+    if (!qfile.open(QIODevice::ReadOnly)) {
+        m_error = QString("Failed to open file %1 for reading.").arg(m_path);
+        SVDEBUG << "MP3FileReader: " << m_error << endl;
+        return;
+    }   
+
+    m_fileSize = qfile.size();
+    
+    try {
+        // We need a mysterious MAD_BUFFER_GUARD (== 8) zero bytes at
+        // end of input, to ensure libmad decodes the last frame
+        // correctly. Otherwise the decoded audio is truncated.
+        m_fileBufferSize = m_fileSize + MAD_BUFFER_GUARD;
+        m_fileBuffer = new unsigned char[m_fileBufferSize];
+        memset(m_fileBuffer + m_fileSize, 0, MAD_BUFFER_GUARD);
+    } catch (...) {
+        m_error = QString("Out of memory");
+        SVDEBUG << "MP3FileReader: " << m_error << endl;
+        return;
     }
 
-    m_fileSize = stat.st_size;
+    auto amountRead = qfile.read(reinterpret_cast<char *>(m_fileBuffer),
+                                 m_fileSize);
 
-    m_filebuffer = 0;
-    m_samplebuffer = 0;
-    m_samplebuffersize = 0;
+    if (amountRead < m_fileSize) {
+        SVCERR << QString("MP3FileReader::MP3FileReader: Warning: reached EOF after only %1 of %2 bytes")
+            .arg(amountRead).arg(m_fileSize) << endl;
+        memset(m_fileBuffer + amountRead, 0, m_fileSize - amountRead);
+        m_fileSize = amountRead;
+    }
+        
+    loadTags(qfile.handle());
 
-    int fd = -1;
-    if ((fd = ::open(m_path.toLocal8Bit().data(), O_RDONLY
-#ifdef _WIN32
-                     | O_BINARY
-#endif
-                     , 0)) < 0) {
-	m_error = QString("Failed to open file %1 for reading.").arg(m_path);
-	return;
-    }	
-
-    try {
-        m_filebuffer = new unsigned char[m_fileSize];
-    } catch (...) {
-        m_error = QString("Out of memory");
-        ::close(fd);
-	return;
-    }
-    
-    ssize_t sz = 0;
-    ssize_t offset = 0;
-    while (offset < m_fileSize) {
-        sz = ::read(fd, m_filebuffer + offset, m_fileSize - offset);
-        if (sz < 0) {
-            m_error = QString("Read error for file %1 (after %2 bytes)")
-                .arg(m_path).arg(offset);
-            delete[] m_filebuffer;
-            ::close(fd);
-            return;
-        } else if (sz == 0) {
-            cerr << QString("MP3FileReader::MP3FileReader: Warning: reached EOF after only %1 of %2 bytes")
-                .arg(offset).arg(m_fileSize) << endl;
-            m_fileSize = offset;
-            break;
-        }
-        offset += sz;
-    }
-
-    ::close(fd);
-
-    loadTags();
+    qfile.close();
 
     if (decodeMode == DecodeAtOnce) {
 
@@ -117,12 +132,12 @@
                 (tr("Decoding %1...").arg(QFileInfo(m_path).fileName()));
         }
 
-        if (!decode(m_filebuffer, m_fileSize)) {
+        if (!decode(m_fileBuffer, m_fileBufferSize)) {
             m_error = QString("Failed to decode file %1.").arg(m_path);
         }
         
-        delete[] m_filebuffer;
-        m_filebuffer = 0;
+        delete[] m_fileBuffer;
+        m_fileBuffer = 0;
 
         if (isDecodeCacheInitialised()) finishDecodeCache();
         endSerialised();
@@ -139,11 +154,11 @@
             usleep(10);
         }
         
-        cerr << "MP3FileReader ctor: exiting with file rate = " << m_fileRate << endl;
+        SVDEBUG << "MP3FileReader: decoding startup complete, file rate = " << m_fileRate << endl;
     }
 
     if (m_error != "") {
-        cerr << "MP3FileReader::MP3FileReader(\"" << m_path << "\"): ERROR: " << m_error << endl;
+        SVDEBUG << "MP3FileReader::MP3FileReader(\"" << m_path << "\"): ERROR: " << m_error << endl;
     }
 }
 
@@ -163,14 +178,19 @@
 }
 
 void
-MP3FileReader::loadTags()
+MP3FileReader::loadTags(int fd)
 {
     m_title = "";
 
 #ifdef HAVE_ID3TAG
 
-    id3_file *file = id3_file_open(m_path.toLocal8Bit().data(),
-                                   ID3_FILE_MODE_READONLY);
+#ifdef _WIN32
+    int id3fd = _dup(fd);
+#else
+    int id3fd = dup(fd);
+#endif
+
+    id3_file *file = id3_file_fdopen(id3fd, ID3_FILE_MODE_READONLY);
     if (!file) return;
 
     // We can do this a lot more elegantly, but we'll leave that for
@@ -178,33 +198,32 @@
     
     id3_tag *tag = id3_file_tag(file);
     if (!tag) {
-#ifdef DEBUG_ID3TAG
-        cerr << "MP3FileReader::loadTags: No ID3 tag found" << endl;
-#endif
-        id3_file_close(file);
+        SVDEBUG << "MP3FileReader::loadTags: No ID3 tag found" << endl;
+        id3_file_close(file); // also closes our dup'd fd
         return;
     }
 
     m_title = loadTag(tag, "TIT2"); // work title
     if (m_title == "") m_title = loadTag(tag, "TIT1");
+    if (m_title == "") SVDEBUG << "MP3FileReader::loadTags: No title found" << endl;
 
     m_maker = loadTag(tag, "TPE1"); // "lead artist"
     if (m_maker == "") m_maker = loadTag(tag, "TPE2");
+    if (m_maker == "") SVDEBUG << "MP3FileReader::loadTags: No artist/maker found" << endl;
 
     for (unsigned int i = 0; i < tag->nframes; ++i) {
         if (tag->frames[i]) {
             QString value = loadTag(tag, tag->frames[i]->id);
-            if (value != "") m_tags[tag->frames[i]->id] = value;
+            if (value != "") {
+                m_tags[tag->frames[i]->id] = value;
+            }
         }
     }
 
-    id3_file_close(file);
+    id3_file_close(file); // also closes our dup'd fd
 
 #else
-#ifdef DEBUG_ID3TAG
-    cerr << "MP3FileReader::loadTags: ID3 tag support not compiled in"
-              << endl;
-#endif
+    SVDEBUG << "MP3FileReader::loadTags: ID3 tag support not compiled in" << endl;
 #endif
 }
 
@@ -216,47 +235,38 @@
 
     id3_frame *frame = id3_tag_findframe(tag, name, 0);
     if (!frame) {
-#ifdef DEBUG_ID3TAG
-        cerr << "MP3FileReader::loadTags: No \"" << name << "\" in ID3 tag" << endl;
-#endif
+        SVDEBUG << "MP3FileReader::loadTag: No \"" << name << "\" frame found in ID3 tag" << endl;
         return "";
     }
         
     if (frame->nfields < 2) {
-        cerr << "MP3FileReader::loadTags: WARNING: Not enough fields (" << frame->nfields << ") for \"" << name << "\" in ID3 tag" << endl;
+        cerr << "MP3FileReader::loadTag: WARNING: Not enough fields (" << frame->nfields << ") for \"" << name << "\" in ID3 tag" << endl;
         return "";
     }
 
     unsigned int nstrings = id3_field_getnstrings(&frame->fields[1]);
     if (nstrings == 0) {
-#ifdef DEBUG_ID3TAG
-        cerr << "MP3FileReader::loadTags: No strings for \"" << name << "\" in ID3 tag" << endl;
-#endif
+        SVDEBUG << "MP3FileReader::loadTag: No strings for \"" << name << "\" in ID3 tag" << endl;
         return "";
     }
 
     id3_ucs4_t const *ustr = id3_field_getstrings(&frame->fields[1], 0);
     if (!ustr) {
-#ifdef DEBUG_ID3TAG
-        cerr << "MP3FileReader::loadTags: Invalid or absent data for \"" << name << "\" in ID3 tag" << endl;
-#endif
+        SVDEBUG << "MP3FileReader::loadTag: Invalid or absent data for \"" << name << "\" in ID3 tag" << endl;
         return "";
     }
         
     id3_utf8_t *u8str = id3_ucs4_utf8duplicate(ustr);
     if (!u8str) {
-        cerr << "MP3FileReader::loadTags: ERROR: Internal error: Failed to convert UCS4 to UTF8 in ID3 title" << endl;
+        SVDEBUG << "MP3FileReader::loadTag: ERROR: Internal error: Failed to convert UCS4 to UTF8 in ID3 tag" << endl;
         return "";
     }
         
     QString rv = QString::fromUtf8((const char *)u8str);
     free(u8str);
 
-#ifdef DEBUG_ID3TAG
-	cerr << "MP3FileReader::loadTags: tag \"" << name << "\" -> \""
-	<< rv << "\"" << endl;
-#endif
-
+    SVDEBUG << "MP3FileReader::loadTag: Tag \"" << name << "\" -> \""
+            << rv << "\"" << endl;
 
     return rv;
 
@@ -268,19 +278,19 @@
 void
 MP3FileReader::DecodeThread::run()
 {
-    if (!m_reader->decode(m_reader->m_filebuffer, m_reader->m_fileSize)) {
+    if (!m_reader->decode(m_reader->m_fileBuffer, m_reader->m_fileBufferSize)) {
         m_reader->m_error = QString("Failed to decode file %1.").arg(m_reader->m_path);
     }
 
-    delete[] m_reader->m_filebuffer;
-    m_reader->m_filebuffer = 0;
+    delete[] m_reader->m_fileBuffer;
+    m_reader->m_fileBuffer = 0;
 
-    if (m_reader->m_samplebuffer) {
+    if (m_reader->m_sampleBuffer) {
         for (int c = 0; c < m_reader->m_channelCount; ++c) {
-            delete[] m_reader->m_samplebuffer[c];
+            delete[] m_reader->m_sampleBuffer[c];
         }
-        delete[] m_reader->m_samplebuffer;
-        m_reader->m_samplebuffer = 0;
+        delete[] m_reader->m_sampleBuffer;
+        m_reader->m_sampleBuffer = 0;
     }
 
     if (m_reader->isDecodeCacheInitialised()) m_reader->finishDecodeCache();
@@ -298,35 +308,51 @@
     struct mad_decoder decoder;
 
     data.start = (unsigned char const *)mm;
-    data.length = (unsigned long)sz;
+    data.length = sz;
+    data.finished = false;
     data.reader = this;
 
-    mad_decoder_init(&decoder, &data, input, 0, 0, output, error, 0);
+    mad_decoder_init(&decoder,          // decoder to initialise
+                     &data,             // our own data block for callbacks
+                     input_callback,    // provides (entire) input to mad
+                     0,                 // checks header
+                     filter_callback,   // filters frame before decoding
+                     output_callback,   // receives decoded output
+                     error_callback,    // handles decode errors
+                     0);                // "message_func"
+
     mad_decoder_run(&decoder, MAD_DECODER_MODE_SYNC);
     mad_decoder_finish(&decoder);
 
+    SVDEBUG << "MP3FileReader: Decoding complete, decoded " << m_mp3FrameCount
+            << " mp3 frames" << endl;
+    
     m_done = true;
     return true;
 }
 
 enum mad_flow
-MP3FileReader::input(void *dp, struct mad_stream *stream)
+MP3FileReader::input_callback(void *dp, struct mad_stream *stream)
 {
     DecoderData *data = (DecoderData *)dp;
 
-    if (!data->length) return MAD_FLOW_STOP;
+    if (!data->length) {
+        data->finished = true;
+        return MAD_FLOW_STOP;
+    }
 
     unsigned char const *start = data->start;
-    unsigned long length = data->length;
+    sv_frame_t length = data->length;
 
 #ifdef HAVE_ID3TAG
-    if (length > ID3_TAG_QUERYSIZE) {
+    while (length > ID3_TAG_QUERYSIZE) {
         ssize_t taglen = id3_tag_query(start, ID3_TAG_QUERYSIZE);
-        if (taglen > 0) {
-//            cerr << "ID3 tag length to skip: " << taglen << endl;
-            start += taglen;
-            length -= taglen;
+        if (taglen <= 0) {
+            break;
         }
+        SVDEBUG << "MP3FileReader: ID3 tag length to skip: " << taglen << endl;
+        start += taglen;
+        length -= taglen;
     }
 #endif
 
@@ -337,9 +363,107 @@
 }
 
 enum mad_flow
-MP3FileReader::output(void *dp,
-		      struct mad_header const *header,
-		      struct mad_pcm *pcm)
+MP3FileReader::filter_callback(void *dp,
+                               struct mad_stream const *stream,
+                               struct mad_frame *frame)
+{
+    DecoderData *data = (DecoderData *)dp;
+    return data->reader->filter(stream, frame);
+}
+
+static string toMagic(unsigned long fourcc)
+{
+    string magic("....");
+    for (int i = 0; i < 4; ++i) {
+        magic[3-i] = char((fourcc >> (8*i)) & 0xff);
+    }
+    return magic;
+}
+
+enum mad_flow
+MP3FileReader::filter(struct mad_stream const *stream,
+                      struct mad_frame *)
+{
+    if (m_mp3FrameCount > 0) {
+        // only handle info frame if it appears as first mp3 frame
+        return MAD_FLOW_CONTINUE;
+    }
+
+    if (m_gaplessMode == GaplessMode::Gappy) {
+        // Our non-gapless mode does not even filter out the Xing/LAME
+        // frame. That's because the main reason non-gapless mode
+        // exists is for backward compatibility with MP3FileReader
+        // behaviour before the gapless support was added, so we even
+        // need to keep the spurious 1152 samples resulting from
+        // feeding Xing/LAME frame to the decoder as otherwise we'd
+        // have different output from before.
+        SVDEBUG << "MP3FileReader: Not gapless mode, not checking Xing/LAME frame"
+                << endl;
+        return MAD_FLOW_CONTINUE;
+    }
+    
+    struct mad_bitptr ptr = stream->anc_ptr;
+    string magic = toMagic(mad_bit_read(&ptr, 32));
+
+    if (magic == "Xing" || magic == "Info") {
+
+        SVDEBUG << "MP3FileReader: Found Xing/LAME metadata frame (magic = \""
+                << magic << "\")" << endl;
+
+        // All we want at this point is the LAME encoder delay and
+        // padding values. We expect to see the Xing/Info magic (which
+        // we've already read), then 116 bytes of Xing data, then LAME
+        // magic, 5 byte version string, 12 bytes of LAME data that we
+        // aren't currently interested in, then the delays encoded as
+        // two 12-bit numbers into three bytes.
+        //
+        // (See gabriel.mp3-tech.org/mp3infotag.html)
+        
+        for (int skip = 0; skip < 116; ++skip) {
+            (void)mad_bit_read(&ptr, 8);
+        }
+
+        magic = toMagic(mad_bit_read(&ptr, 32));
+
+        if (magic == "LAME") {
+
+            SVDEBUG << "MP3FileReader: Found LAME-specific metadata" << endl;
+
+            for (int skip = 0; skip < 5 + 12; ++skip) {
+                (void)mad_bit_read(&ptr, 8);
+            }
+
+            auto delay = mad_bit_read(&ptr, 12);
+            auto padding = mad_bit_read(&ptr, 12);
+
+            sv_frame_t delayToDrop = DEFAULT_DECODER_DELAY + delay;
+            sv_frame_t paddingToDrop = padding - DEFAULT_DECODER_DELAY;
+            if (paddingToDrop < 0) paddingToDrop = 0;
+
+            SVDEBUG << "MP3FileReader: LAME encoder delay = " << delay
+                    << ", padding = " << padding << endl;
+
+            SVDEBUG << "MP3FileReader: Will be trimming " << delayToDrop
+                    << " samples from start and " << paddingToDrop
+                    << " from end" << endl;
+
+            CodedAudioFileReader::setFramesToTrim(delayToDrop, paddingToDrop);
+            
+        } else {
+            SVDEBUG << "MP3FileReader: Xing frame has no LAME metadata" << endl;
+        }
+            
+        return MAD_FLOW_IGNORE;
+        
+    } else {
+        return MAD_FLOW_CONTINUE;
+    }
+}
+
+enum mad_flow
+MP3FileReader::output_callback(void *dp,
+                               struct mad_header const *header,
+                               struct mad_pcm *pcm)
 {
     DecoderData *data = (DecoderData *)dp;
     return data->reader->accept(header, pcm);
@@ -347,11 +471,11 @@
 
 enum mad_flow
 MP3FileReader::accept(struct mad_header const *header,
-		      struct mad_pcm *pcm)
+                      struct mad_pcm *pcm)
 {
     int channels = pcm->channels;
     int frames = pcm->length;
-
+    
     if (header) {
         m_bitrateNum = m_bitrateNum + double(header->bitrate);
         m_bitrateDenom ++;
@@ -364,6 +488,10 @@
         m_fileRate = pcm->samplerate;
         m_channelCount = channels;
 
+        SVDEBUG << "MP3FileReader::accept: file rate = " << pcm->samplerate
+                << ", channel count = " << channels << ", about to init "
+                << "decode cache" << endl;
+
         initialiseDecodeCache();
 
         if (m_cacheMode == CacheInTemporaryFile) {
@@ -387,24 +515,30 @@
         }
     }
 
-    if (m_cancelled) return MAD_FLOW_STOP;
+    if (m_cancelled) {
+        SVDEBUG << "MP3FileReader: Decoding cancelled" << endl;
+        return MAD_FLOW_STOP;
+    }
 
     if (!isDecodeCacheInitialised()) {
+        SVDEBUG << "MP3FileReader::accept: fallback case: file rate = " << pcm->samplerate
+                << ", channel count = " << channels << ", about to init "
+                << "decode cache" << endl;
         initialiseDecodeCache();
     }
 
-    if (int(m_samplebuffersize) < frames) {
-        if (!m_samplebuffer) {
-            m_samplebuffer = new float *[channels];
+    if (m_sampleBufferSize < size_t(frames)) {
+        if (!m_sampleBuffer) {
+            m_sampleBuffer = new float *[channels];
             for (int c = 0; c < channels; ++c) {
-                m_samplebuffer[c] = 0;
+                m_sampleBuffer[c] = 0;
             }
         }
         for (int c = 0; c < channels; ++c) {
-            delete[] m_samplebuffer[c];
-            m_samplebuffer[c] = new float[frames];
+            delete[] m_sampleBuffer[c];
+            m_sampleBuffer[c] = new float[frames];
         }
-        m_samplebuffersize = frames;
+        m_sampleBufferSize = frames;
     }
 
     int activeChannels = int(sizeof(pcm->samples) / sizeof(pcm->samples[0]));
@@ -413,31 +547,48 @@
 
         for (int i = 0; i < frames; ++i) {
 
-	    mad_fixed_t sample = 0;
-	    if (ch < activeChannels) {
-		sample = pcm->samples[ch][i];
-	    }
-	    float fsample = float(sample) / float(MAD_F_ONE);
+            mad_fixed_t sample = 0;
+            if (ch < activeChannels) {
+                sample = pcm->samples[ch][i];
+            }
+            float fsample = float(sample) / float(MAD_F_ONE);
             
-            m_samplebuffer[ch][i] = fsample;
-	}
+            m_sampleBuffer[ch][i] = fsample;
+        }
     }
 
-    addSamplesToDecodeCache(m_samplebuffer, frames);
+    addSamplesToDecodeCache(m_sampleBuffer, frames);
+
+    ++m_mp3FrameCount;
 
     return MAD_FLOW_CONTINUE;
 }
 
 enum mad_flow
-MP3FileReader::error(void * /* dp */,
-		     struct mad_stream * /* stream */,
-		     struct mad_frame *)
+MP3FileReader::error_callback(void *dp,
+                              struct mad_stream *stream,
+                              struct mad_frame *)
 {
-//    DecoderData *data = (DecoderData *)dp;
+    DecoderData *data = (DecoderData *)dp;
 
-//    fprintf(stderr, "decoding error 0x%04x (%s) at byte offset %lu\n",
-//	    stream->error, mad_stream_errorstr(stream),
-//	    (unsigned long)(stream->this_frame - data->start));
+    sv_frame_t ix = stream->this_frame - data->start;
+    
+    if (stream->error == MAD_ERROR_LOSTSYNC &&
+        (data->finished || ix >= data->length)) {
+        // We are at end of file, losing sync is expected behaviour,
+        // don't report it
+        return MAD_FLOW_CONTINUE;
+    }
+    
+    if (!data->reader->m_decodeErrorShown) {
+        char buffer[256];
+        snprintf(buffer, 255,
+                 "MP3 decoding error 0x%04x (%s) at byte offset %lld",
+                 stream->error, mad_stream_errorstr(stream), (long long int)ix);
+        SVCERR << "Warning: in file \"" << data->reader->m_path << "\": "
+               << buffer << " (continuing; will not report any further decode errors for this file)" << endl;
+        data->reader->m_decodeErrorShown = true;
+    }
 
     return MAD_FLOW_CONTINUE;
 }
--- a/data/fileio/MP3FileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/MP3FileReader.h	Fri Jan 13 10:29:44 2017 +0000
@@ -32,14 +32,43 @@
     Q_OBJECT
 
 public:
-    enum DecodeMode {
-        DecodeAtOnce, // decode the file on construction, with progress
-        DecodeThreaded // decode in a background thread after construction
+    /**
+     * How the MP3FileReader should handle leading and trailing gaps.
+     * See http://lame.sourceforge.net/tech-FAQ.txt for a technical
+     * explanation of the numbers here.
+     */
+    enum class GaplessMode {
+        /**
+         * Trim unwanted samples from the start and end of the decoded
+         * audio. From the start, trim a number of samples equal to
+         * the decoder delay (a fixed 529 samples) plus any encoder
+         * delay that may be specified in Xing/LAME metadata. From the
+         * end, trim any padding specified in Xing/LAME metadata, less
+         * the fixed decoder delay. This usually results in "gapless"
+         * audio, i.e. with no spurious zero padding at either end.
+         */
+        Gapless,
+
+        /**
+         * Do not trim any samples. Also do not suppress any frames
+         * from being passed to the mp3 decoder, even Xing/LAME
+         * metadata frames. This will result in the audio being padded
+         * with zeros at either end: at the start, typically
+         * 529+576+1152 = 2257 samples for LAME-encoded mp3s; at the
+         * end an unknown number depending on the fill ratio of the
+         * final coded frame, but typically less than 1152-529 = 623.
+         *
+         * This mode produces the same output as produced by older
+         * versions of this code before the gapless option was added,
+         * and is present mostly for backward compatibility.
+         */
+        Gappy
     };
-
+    
     MP3FileReader(FileSource source,
                   DecodeMode decodeMode,
                   CacheMode cacheMode,
+                  GaplessMode gaplessMode,
                   sv_samplerate_t targetRate = 0,
                   bool normalised = false,
                   ProgressReporter *reporter = 0);
@@ -73,32 +102,43 @@
     QString m_title;
     QString m_maker;
     TagMap m_tags;
+    GaplessMode m_gaplessMode;
     sv_frame_t m_fileSize;
     double m_bitrateNum;
     int m_bitrateDenom;
+    int m_mp3FrameCount;
     int m_completion;
     bool m_done;
 
-    unsigned char *m_filebuffer;
-    float **m_samplebuffer;
-    int m_samplebuffersize;
+    unsigned char *m_fileBuffer;
+    size_t m_fileBufferSize;
+    
+    float **m_sampleBuffer;
+    size_t m_sampleBufferSize;
 
     ProgressReporter *m_reporter;
     bool m_cancelled;
 
-    struct DecoderData
-    {
+    bool m_decodeErrorShown;
+
+    struct DecoderData {
 	unsigned char const *start;
-	unsigned long length;
+	sv_frame_t length;
+        bool finished;
 	MP3FileReader *reader;
     };
 
     bool decode(void *mm, sv_frame_t sz);
+    enum mad_flow filter(struct mad_stream const *, struct mad_frame *);
     enum mad_flow accept(struct mad_header const *, struct mad_pcm *);
 
-    static enum mad_flow input(void *, struct mad_stream *);
-    static enum mad_flow output(void *, struct mad_header const *, struct mad_pcm *);
-    static enum mad_flow error(void *, struct mad_stream *, struct mad_frame *);
+    static enum mad_flow input_callback(void *, struct mad_stream *);
+    static enum mad_flow output_callback(void *, struct mad_header const *,
+                                         struct mad_pcm *);
+    static enum mad_flow filter_callback(void *, struct mad_stream const *,
+                                         struct mad_frame *);
+    static enum mad_flow error_callback(void *, struct mad_stream *,
+                                        struct mad_frame *);
 
     class DecodeThread : public Thread
     {
@@ -112,7 +152,7 @@
 
     DecodeThread *m_decodeThread;
 
-    void loadTags();
+    void loadTags(int fd);
     QString loadTag(void *vtag, const char *name);
 };
 
--- a/data/fileio/MatrixFile.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,451 +0,0 @@
-/* -*- 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 file copyright 2006-2009 Chris Cannam and QMUL.
-    
-    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 "MatrixFile.h"
-#include "base/TempDirectory.h"
-#include "system/System.h"
-#include "base/Profiler.h"
-#include "base/Exceptions.h"
-#include "base/Thread.h"
-
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <fcntl.h>
-#include <unistd.h>
-
-#include <iostream>
-
-#include <cstdio>
-#include <cassert>
-
-#include <cstdlib>
-
-#include <QFileInfo>
-#include <QDir>
-
-//#define DEBUG_MATRIX_FILE 1
-//#define DEBUG_MATRIX_FILE_READ_SET 1
-
-#ifdef DEBUG_MATRIX_FILE_READ_SET
-#ifndef DEBUG_MATRIX_FILE
-#define DEBUG_MATRIX_FILE 1
-#endif
-#endif
-
-std::map<QString, int> MatrixFile::m_refcount;
-QMutex MatrixFile::m_createMutex;
-
-static size_t totalStorage = 0;
-static size_t totalCount = 0;
-static size_t openCount = 0;
-
-MatrixFile::MatrixFile(QString fileBase, Mode mode,
-                       int cellSize, int width, int height) :
-    m_fd(-1),
-    m_mode(mode),
-    m_flags(0),
-    m_fmode(0),
-    m_cellSize(cellSize),
-    m_width(width),
-    m_height(height),
-    m_headerSize(2 * sizeof(int)),
-    m_setColumns(0),
-    m_autoClose(false),
-    m_readyToReadColumn(-1)
-{
-    Profiler profiler("MatrixFile::MatrixFile", true);
-
-#ifdef DEBUG_MATRIX_FILE
-    SVDEBUG << "MatrixFile::MatrixFile(" << fileBase << ", " << int(mode) << ", " << cellSize << ", " << width << ", " << height << ")" << endl;
-#endif
-
-    m_createMutex.lock();
-
-    QDir tempDir(TempDirectory::getInstance()->getPath());
-    QString fileName(tempDir.filePath(QString("%1.mfc").arg(fileBase)));
-    bool newFile = !QFileInfo(fileName).exists();
-
-    if (newFile && m_mode == ReadOnly) {
-        cerr << "ERROR: MatrixFile::MatrixFile: Read-only mode "
-                  << "specified, but cache file does not exist" << endl;
-        throw FileNotFound(fileName);
-    }
-
-    if (!newFile && m_mode == WriteOnly) {
-        cerr << "ERROR: MatrixFile::MatrixFile: Write-only mode "
-                  << "specified, but file already exists" << endl;
-        throw FileOperationFailed(fileName, "create");
-    }
-
-    // Use floating-point here to avoid integer overflow. We can be
-    // approximate so long as we are on the cautious side
-    if ((double(m_width) * m_height) * m_cellSize + m_headerSize + m_width >=
-        pow(2, 31) - 10.0) { // bit of slack there
-        cerr << "ERROR: MatrixFile::MatrixFile: width " << m_width
-             << " is too large for height " << m_height << " and cell size "
-             << m_cellSize << " (should be using multiple files)" << endl;
-        throw FileOperationFailed(fileName, "size");
-    }
-    
-    m_flags = 0;
-    m_fmode = S_IRUSR | S_IWUSR;
-
-    if (m_mode == WriteOnly) {
-        m_flags = O_WRONLY | O_CREAT;
-    } else {
-        m_flags = O_RDONLY;
-    }
-
-#ifdef _WIN32
-    m_flags |= O_BINARY;
-#endif
-
-#ifdef DEBUG_MATRIX_FILE
-    cerr << "MatrixFile(" << this << ")::MatrixFile: opening " << fileName << "..." << endl;
-#endif
-
-    if ((m_fd = ::open(fileName.toLocal8Bit(), m_flags, m_fmode)) < 0) {
-        ::perror("Open failed");
-        cerr << "ERROR: MatrixFile::MatrixFile: "
-                  << "Failed to open cache file \""
-                  << fileName << "\"";
-        if (m_mode == WriteOnly) cerr << " for writing";
-        cerr << endl;
-        throw FailedToOpenFile(fileName);
-    }
-
-    m_createMutex.unlock();
-
-#ifdef DEBUG_MATRIX_FILE
-    cerr << "MatrixFile(" << this << ")::MatrixFile: fd is " << m_fd << endl;
-#endif
-
-    if (newFile) {
-        initialise(); // write header and "unwritten" column tags
-    } else {
-        int header[2];
-        if (::read(m_fd, header, 2 * sizeof(int)) < 0) {
-            ::perror("MatrixFile::MatrixFile: read failed");
-            cerr << "ERROR: MatrixFile::MatrixFile: "
-                      << "Failed to read header (fd " << m_fd << ", file \""
-                      << fileName << "\")" << endl;
-            throw FileReadFailed(fileName);
-        }
-        if (header[0] != m_width || header[1] != m_height) {
-            cerr << "ERROR: MatrixFile::MatrixFile: "
-                      << "Dimensions in file header (" << header[0] << "x"
-                      << header[1] << ") differ from expected dimensions "
-                      << m_width << "x" << m_height << endl;
-            throw FailedToOpenFile(fileName);
-        }
-    }
-
-    m_fileName = fileName;
-    ++m_refcount[fileName];
-
-#ifdef DEBUG_MATRIX_FILE
-    cerr << "MatrixFile[" << m_fd << "]::MatrixFile: File " << fileName << ", ref " << m_refcount[fileName] << endl;
-
-    cerr << "MatrixFile[" << m_fd << "]::MatrixFile: Done, size is " << "(" << m_width << ", " << m_height << ")" << endl;
-#endif
-
-    ++totalCount;
-    ++openCount;
-}
-
-MatrixFile::~MatrixFile()
-{
-    if (m_fd >= 0) {
-        if (::close(m_fd) < 0) {
-            ::perror("MatrixFile::~MatrixFile: close failed");
-        }
-        openCount --;
-    }
-
-    QMutexLocker locker(&m_createMutex);
-
-    delete m_setColumns;
-
-    if (m_fileName != "") {
-
-        if (--m_refcount[m_fileName] == 0) {
-
-            if (::unlink(m_fileName.toLocal8Bit())) {
-                cerr << "WARNING: MatrixFile::~MatrixFile: reference count reached 0, but failed to unlink file \"" << m_fileName << "\"" << endl;
-            } else {
-                cerr << "deleted " << m_fileName << endl;
-            }
-        }
-    }
-
-    if (m_mode == WriteOnly) {
-        totalStorage -= (m_headerSize + (m_width * m_height * m_cellSize) + m_width);
-    }
-    totalCount --;
-
-#ifdef DEBUG_MATRIX_FILE
-    cerr << "MatrixFile[" << m_fd << "]::~MatrixFile: " << endl;
-    cerr << "MatrixFile: Total storage now " << totalStorage/1024 << "K in " << totalCount << " instances (" << openCount << " open)" << endl;
-#endif
-}
-
-void
-MatrixFile::initialise()
-{
-    Profiler profiler("MatrixFile::initialise", true);
-
-    assert(m_mode == WriteOnly);
-
-    m_setColumns = new ResizeableBitset(m_width);
-    
-    off_t off = m_headerSize + (m_width * m_height * m_cellSize) + m_width;
-
-#ifdef DEBUG_MATRIX_FILE
-    cerr << "MatrixFile[" << m_fd << "]::initialise(" << m_width << ", " << m_height << "): cell size " << m_cellSize << ", header size " << m_headerSize << ", resizing fd " << m_fd << " to " << off << endl;
-#endif
-
-    if (::lseek(m_fd, off - 1, SEEK_SET) < 0) {
-        ::perror("ERROR: MatrixFile::initialise: seek to end failed");
-        throw FileOperationFailed(m_fileName, "lseek");
-    }
-
-    unsigned char byte = 0;
-    if (::write(m_fd, &byte, 1) != 1) {
-        ::perror("ERROR: MatrixFile::initialise: write at end failed");
-        throw FileOperationFailed(m_fileName, "write");
-    }
-
-    if (::lseek(m_fd, 0, SEEK_SET) < 0) {
-        ::perror("ERROR: MatrixFile::initialise: Seek to write header failed");
-        throw FileOperationFailed(m_fileName, "lseek");
-    }
-
-    int header[2];
-    header[0] = m_width;
-    header[1] = m_height;
-    if (::write(m_fd, header, 2 * sizeof(int)) != 2 * sizeof(int)) {
-        ::perror("ERROR: MatrixFile::initialise: Failed to write header");
-        throw FileOperationFailed(m_fileName, "write");
-    }
-
-    if (m_mode == WriteOnly) {
-        totalStorage += (m_headerSize + (m_width * m_height * m_cellSize) + m_width);
-    }
-
-#ifdef DEBUG_MATRIX_FILE
-    cerr << "MatrixFile[" << m_fd << "]::initialise(" << m_width << ", " << m_height << "): storage "
-              << (m_headerSize + m_width * m_height * m_cellSize + m_width) << endl;
-
-    cerr << "MatrixFile: Total storage " << totalStorage/1024 << "K" << endl;
-#endif
-
-    seekTo(0);
-}
-
-void
-MatrixFile::close()
-{
-#ifdef DEBUG_MATRIX_FILE
-    SVDEBUG << "MatrixFile::close()" << endl;
-#endif
-    if (m_fd >= 0) {
-        if (::close(m_fd) < 0) {
-            ::perror("MatrixFile::close: close failed");
-        }
-        m_fd = -1;
-        -- openCount;
-#ifdef DEBUG_MATRIX_FILE
-        cerr << "MatrixFile: Now " << openCount << " open instances" << endl;
-#endif
-    }
-}
-
-void
-MatrixFile::getColumnAt(int x, void *data)
-{
-    assert(m_mode == ReadOnly);
-    
-#ifdef DEBUG_MATRIX_FILE_READ_SET
-    cerr << "MatrixFile[" << m_fd << "]::getColumnAt(" << x << ")" << endl;
-#endif
-
-    Profiler profiler("MatrixFile::getColumnAt");
-
-    ssize_t r = -1;
-
-    if (m_readyToReadColumn < 0 ||
-        m_readyToReadColumn != x) {
-
-        unsigned char set = 0;
-        if (!seekTo(x)) {
-            cerr << "ERROR: MatrixFile::getColumnAt(" << x << "): Seek failed" << endl;
-            throw FileOperationFailed(m_fileName, "seek");
-        }
-
-        r = ::read(m_fd, &set, 1);
-        if (r < 0) {
-            ::perror("MatrixFile::getColumnAt: read failed");
-            throw FileReadFailed(m_fileName);
-        }
-        if (!set) {
-            cerr << "MatrixFile[" << m_fd << "]::getColumnAt(" << x << "): Column has not been set" << endl;
-            return;
-        }
-    }
-
-    r = ::read(m_fd, data, m_height * m_cellSize);
-    if (r < 0) {
-        ::perror("MatrixFile::getColumnAt: read failed");
-        throw FileReadFailed(m_fileName);
-    }
-}
-
-bool
-MatrixFile::haveSetColumnAt(int x) const
-{
-    if (m_mode == WriteOnly) {
-        return m_setColumns->get(x);
-    }
-
-    if (m_readyToReadColumn >= 0 &&
-        int(m_readyToReadColumn) == x) return true;
-    
-    Profiler profiler("MatrixFile::haveSetColumnAt");
-
-#ifdef DEBUG_MATRIX_FILE_READ_SET
-    cerr << "MatrixFile[" << m_fd << "]::haveSetColumnAt(" << x << ")" << endl;
-//    cerr << ".";
-#endif
-
-    unsigned char set = 0;
-    if (!seekTo(x)) {
-        cerr << "ERROR: MatrixFile::haveSetColumnAt(" << x << "): Seek failed" << endl;
-        throw FileOperationFailed(m_fileName, "seek");
-    }
-
-    ssize_t r = -1;
-    r = ::read(m_fd, &set, 1);
-    if (r < 0) {
-        ::perror("MatrixFile::haveSetColumnAt: read failed");
-        throw FileReadFailed(m_fileName);
-    }
-
-    if (set) m_readyToReadColumn = int(x);
-
-    return set;
-}
-
-void
-MatrixFile::setColumnAt(int x, const void *data)
-{
-    assert(m_mode == WriteOnly);
-    if (m_fd < 0) return; // closed
-
-#ifdef DEBUG_MATRIX_FILE_READ_SET
-    cerr << "MatrixFile[" << m_fd << "]::setColumnAt(" << x << ")" << endl;
-//    cerr << ".";
-#endif
-
-    ssize_t w = 0;
-
-    if (!seekTo(x)) {
-        cerr << "ERROR: MatrixFile::setColumnAt(" << x << "): Seek failed" << endl;
-        throw FileOperationFailed(m_fileName, "seek");
-    }
-
-    unsigned char set = 0;
-    w = ::write(m_fd, &set, 1);
-    if (w != 1) {
-        ::perror("WARNING: MatrixFile::setColumnAt: write failed (1)");
-        throw FileOperationFailed(m_fileName, "write");
-    }
-
-    w = ::write(m_fd, data, m_height * m_cellSize);
-    if (w != ssize_t(m_height * m_cellSize)) {
-        ::perror("WARNING: MatrixFile::setColumnAt: write failed (2)");
-        throw FileOperationFailed(m_fileName, "write");
-    }
-/*
-    if (x == 0) {
-        cerr << "Wrote " << m_height * m_cellSize << " bytes, as follows:" << endl;
-        for (int i = 0; i < m_height * m_cellSize; ++i) {
-            cerr << (int)(((char *)data)[i]) << " ";
-        }
-        cerr << endl;
-    }
-*/
-    if (!seekTo(x)) {
-        cerr << "MatrixFile[" << m_fd << "]::setColumnAt(" << x << "): Seek failed" << endl;
-        throw FileOperationFailed(m_fileName, "seek");
-    }
-
-    set = 1;
-    w = ::write(m_fd, &set, 1);
-    if (w != 1) {
-        ::perror("WARNING: MatrixFile::setColumnAt: write failed (3)");
-        throw FileOperationFailed(m_fileName, "write");
-    }
-
-    m_setColumns->set(x);
-    if (m_autoClose) {
-        if (m_setColumns->isAllOn()) {
-#ifdef DEBUG_MATRIX_FILE
-            cerr << "MatrixFile[" << m_fd << "]::setColumnAt(" << x << "): All columns set: auto-closing" << endl;
-#endif
-            close();
-/*
-        } else {
-            int set = 0;
-            for (int i = 0; i < m_width; ++i) {
-                if (m_setColumns->get(i)) ++set;
-            }
-            cerr << "MatrixFile[" << m_fd << "]::setColumnAt(" << x << "): Auto-close on, but not all columns set yet (" << set << " of " << m_width << ")" << endl;
-*/
-        }
-    }
-}
-
-bool
-MatrixFile::seekTo(int x) const
-{
-    if (m_fd < 0) {
-        cerr << "ERROR: MatrixFile::seekTo: File not open" << endl;
-        return false;
-    }
-
-    m_readyToReadColumn = -1; // not ready, unless this is subsequently re-set
-
-    off_t off = m_headerSize + x * m_height * m_cellSize + x;
-
-#ifdef DEBUG_MATRIX_FILE_READ_SET
-    if (m_mode == ReadOnly) {
-        cerr << "MatrixFile[" << m_fd << "]::seekTo(" << x << "): off = " << off << endl;
-    }
-#endif
-
-#ifdef DEBUG_MATRIX_FILE_READ_SET
-    cerr << "MatrixFile[" << m_fd << "]::seekTo(" << x << "): off = " << off << endl;
-#endif
-
-    if (::lseek(m_fd, off, SEEK_SET) == (off_t)-1) {
-        ::perror("Seek failed");
-        cerr << "ERROR: MatrixFile::seekTo(" << x 
-                  << ") = " << off << " failed" << endl;
-        return false;
-    }
-
-    return true;
-}
-
--- a/data/fileio/MatrixFile.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,109 +0,0 @@
-/* -*- 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 file copyright 2006-2009 Chris Cannam and QMUL.
-    
-    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 _MATRIX_FILE_CACHE_H_
-#define _MATRIX_FILE_CACHE_H_
-
-#include "base/ResizeableBitset.h"
-
-#include "FileReadThread.h"
-
-#include <sys/types.h>
-#include <QString>
-#include <QMutex>
-#include <map>
-
-class MatrixFile : public QObject
-{
-    Q_OBJECT
-
-public:
-    enum Mode { ReadOnly, WriteOnly };
-
-    /**
-     * Construct a MatrixFile object reading from and/or writing to
-     * the matrix file with the given base name in the application's
-     * temporary directory.
-     *
-     * If mode is ReadOnly, the file must exist and be readable.
-     *
-     * If mode is WriteOnly, the file must not exist.
-     *
-     * cellSize specifies the size in bytes of the object type stored
-     * in the matrix.  For example, use cellSize = sizeof(float) for a
-     * matrix of floats.  The MatrixFile object doesn't care about the
-     * objects themselves, it just deals with raw data of a given size.
-     *
-     * width and height specify the dimensions of the file.  These
-     * cannot be changed after construction.
-     *
-     * MatrixFiles are reference counted by name.  When the last
-     * MatrixFile with a given name is destroyed, the file is removed.
-     * These are temporary files; the normal usage is to have one
-     * MatrixFile of WriteOnly type creating the file and then
-     * persisting until all readers are complete.
-     *
-     * MatrixFile has no built-in cache and is not thread-safe.  Use a
-     * separate MatrixFile in each thread.
-     */
-    MatrixFile(QString fileBase, Mode mode, int cellSize,
-               int width, int height);
-    virtual ~MatrixFile();
-
-    Mode getMode() const { return m_mode; }
-
-    int getWidth() const { return m_width; }
-    int getHeight() const { return m_height; }
-    int getCellSize() const { return m_cellSize; }
-
-    /**
-     * If this is set true on a write-mode MatrixFile, then the file
-     * will close() itself when all columns have been written.
-     */
-    void setAutoClose(bool a) { m_autoClose = a; }
-
-    void close(); // does not decrement ref count; that happens in dtor
-
-    bool haveSetColumnAt(int x) const;
-    void getColumnAt(int x, void *data); // may throw FileReadFailed
-    void setColumnAt(int x, const void *data);
-
-protected:
-    int     m_fd;
-    Mode    m_mode;
-    int     m_flags;
-    mode_t  m_fmode;
-    int     m_cellSize;
-    int     m_width;
-    int     m_height;
-    int     m_headerSize;
-    QString m_fileName;
-
-    ResizeableBitset *m_setColumns; // only in writer
-    bool m_autoClose;
-
-    // In reader: if this is >= 0, we can read that column directly
-    // without seeking (and we know that the column exists)
-    mutable int m_readyToReadColumn;
-
-    static std::map<QString, int> m_refcount;
-    static QMutex m_createMutex;
-
-    void initialise();
-    bool seekTo(int col) const;
-};
-
-#endif
-
--- a/data/fileio/OggVorbisFileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/OggVorbisFileReader.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -40,6 +40,10 @@
     CodedAudioFileReader(mode, targetRate, normalised),
     m_source(source),
     m_path(source.getLocalFilename()),
+    m_qfile(0),
+    m_ffile(0),
+    m_oggz(0),
+    m_fishSound(0),
     m_reporter(reporter),
     m_fileSize(0),
     m_bytesRead(0),
@@ -48,19 +52,48 @@
     m_completion(0),
     m_decodeThread(0)
 {
+    SVDEBUG << "OggVorbisFileReader: local path: \"" << m_path
+            << "\", decode mode: " << decodeMode << " ("
+            << (decodeMode == DecodeAtOnce ? "DecodeAtOnce" : "DecodeThreaded")
+            << ")" << endl;
+
     m_channelCount = 0;
     m_fileRate = 0;
 
 //    SVDEBUG << "OggVorbisFileReader::OggVorbisFileReader(" << m_path << "): now have " << (++instances) << " instances" << endl;
 
-    Profiler profiler("OggVorbisFileReader::OggVorbisFileReader", true);
+    Profiler profiler("OggVorbisFileReader::OggVorbisFileReader");
 
-    QFileInfo info(m_path);
-    m_fileSize = info.size();
+    // These shenanigans are to avoid using oggz_open(..) with a local
+    // codepage on Windows (make sure proper filename encoding is used)
+    
+    m_qfile = new QFile(m_path);
+    if (!m_qfile->open(QIODevice::ReadOnly)) {
+        m_error = QString("Failed to open file %1 for reading.").arg(m_path);
+        SVDEBUG << "OggVorbisFileReader: " << m_error << endl;
+        delete m_qfile;
+        m_qfile = 0;
+        return;
+    }
+    
+    m_fileSize = m_qfile->size();
 
-    if (!(m_oggz = oggz_open(m_path.toLocal8Bit().data(), OGGZ_READ))) {
-	m_error = QString("File %1 is not an OGG file.").arg(m_path);
-	return;
+    m_ffile = fdopen(dup(m_qfile->handle()), "r");
+    if (!m_ffile) {
+        m_error = QString("Failed to open file pointer for file %1").arg(m_path);
+        SVDEBUG << "OggVorbisFileReader: " << m_error << endl;
+        delete m_qfile;
+        m_qfile = 0;
+        return;
+    }
+    
+    if (!(m_oggz = oggz_open_stdio(m_ffile, OGGZ_READ))) {
+        m_error = QString("File %1 is not an OGG file.").arg(m_path);
+        fclose(m_ffile);
+        m_ffile = 0;
+        delete m_qfile;
+        m_qfile = 0;
+        return;
     }
 
     FishSoundInfo fsinfo;
@@ -109,6 +142,11 @@
         m_decodeThread->wait();
         delete m_decodeThread;
     }
+    if (m_qfile) {
+        // don't fclose m_ffile; oggz_close did that
+        delete m_qfile;
+        m_qfile = 0;
+    }
 }
 
 void
@@ -129,8 +167,14 @@
         
     fish_sound_delete(m_reader->m_fishSound);
     m_reader->m_fishSound = 0;
+
     oggz_close(m_reader->m_oggz);
     m_reader->m_oggz = 0;
+
+    // don't fclose m_ffile; oggz_close did that
+
+    delete m_reader->m_qfile;
+    m_reader->m_qfile = 0;
     
     if (m_reader->isDecodeCacheInitialised()) m_reader->finishDecodeCache();
     m_reader->m_completion = 100;
@@ -167,7 +211,7 @@
 
 int
 OggVorbisFileReader::acceptFrames(FishSound *fs, float **frames, long nframes,
-				  void *data)
+                                  void *data)
 {
     OggVorbisFileReader *reader = (OggVorbisFileReader *)data;
 
@@ -191,11 +235,11 @@
     }
 
     if (reader->m_channelCount == 0) {
-	FishSoundInfo fsinfo;
-	fish_sound_command(fs, FISH_SOUND_GET_INFO,
-			   &fsinfo, sizeof(FishSoundInfo));
-	reader->m_fileRate = fsinfo.samplerate;
-	reader->m_channelCount = fsinfo.channels;
+        FishSoundInfo fsinfo;
+        fish_sound_command(fs, FISH_SOUND_GET_INFO,
+                           &fsinfo, sizeof(FishSoundInfo));
+        reader->m_fileRate = fsinfo.samplerate;
+        reader->m_channelCount = fsinfo.channels;
         reader->initialiseDecodeCache();
     }
 
--- a/data/fileio/OggVorbisFileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/OggVorbisFileReader.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _OGG_VORBIS_FILE_READER_H_
-#define _OGG_VORBIS_FILE_READER_H_
+#ifndef SV_OGG_VORBIS_FILE_READER_H
+#define SV_OGG_VORBIS_FILE_READER_H
 
 #ifdef HAVE_OGGZ
 #ifdef HAVE_FISHSOUND
@@ -25,6 +25,8 @@
 #include <oggz/oggz.h>
 #include <fishsound/fishsound.h>
 
+#include <cstdio>
+
 #include <set>
 
 class ProgressReporter;
@@ -34,17 +36,12 @@
     Q_OBJECT
 
 public:
-    enum DecodeMode {
-        DecodeAtOnce, // decode the file on construction, with progress 
-        DecodeThreaded // decode in a background thread after construction
-    };
-
     OggVorbisFileReader(FileSource source,
                         DecodeMode decodeMode,
                         CacheMode cacheMode,
                         sv_samplerate_t targetRate = 0,
                         bool normalised = false,
-                        ProgressReporter *reporter = 0);
+                        ProgressReporter *reporter = nullptr);
     virtual ~OggVorbisFileReader();
 
     virtual QString getError() const { return m_error; }
@@ -76,6 +73,8 @@
     QString m_maker;
     TagMap m_tags;
 
+    QFile *m_qfile;
+    FILE *m_ffile;
     OGGZ *m_oggz;
     FishSound *m_fishSound;
     ProgressReporter *m_reporter;
--- a/data/fileio/QuickTimeFileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,380 +0,0 @@
-/* -*- 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 file copyright 2006-2007 Chris Cannam and QMUL.
-    
-    Based on QTAudioFile.cpp from SoundBite, copyright 2006
-    Chris Sutton and Mark Levy.
-    
-    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.
-*/
-
-#ifdef HAVE_QUICKTIME
-
-#include "QuickTimeFileReader.h"
-#include "base/Profiler.h"
-#include "base/ProgressReporter.h"
-#include "system/System.h"
-
-#include <QFileInfo>
-
-#ifdef _WIN32
-#include <QTML.h>
-#include <Movies.h>
-#else
-#include <QuickTime/QuickTime.h>
-#endif
-
-class QuickTimeFileReader::D
-{
-public:
-    D() : data(0), blockSize(1024) { }
-
-    MovieAudioExtractionRef      extractionSessionRef;
-    AudioBufferList              buffer;
-    float                       *data;
-    OSErr                        err; 
-    AudioStreamBasicDescription  asbd;
-    Movie                        movie;
-    int                          blockSize;
-};
-
-
-QuickTimeFileReader::QuickTimeFileReader(FileSource source,
-                                         DecodeMode decodeMode,
-                                         CacheMode mode,
-                                         sv_samplerate_t targetRate,
-                                         bool normalised,
-                                         ProgressReporter *reporter) :
-    CodedAudioFileReader(mode, targetRate, normalised),
-    m_source(source),
-    m_path(source.getLocalFilename()),
-    m_d(new D),
-    m_reporter(reporter),
-    m_cancelled(false),
-    m_completion(0),
-    m_decodeThread(0)
-{
-    m_channelCount = 0;
-    m_fileRate = 0;
-
-    Profiler profiler("QuickTimeFileReader::QuickTimeFileReader", true);
-
-SVDEBUG << "QuickTimeFileReader: path is \"" << m_path << "\"" << endl;
-
-    long QTversion;
-
-#ifdef WIN32
-    InitializeQTML(0); // FIXME should check QT version
-#else
-    m_d->err = Gestalt(gestaltQuickTime,&QTversion);
-    if ((m_d->err != noErr) || (QTversion < 0x07000000)) {
-        m_error = QString("Failed to find compatible version of QuickTime (version 7 or above required)");
-        return;
-    }
-#endif 
-
-    EnterMovies();
-	
-    Handle dataRef; 
-    OSType dataRefType;
-
-//    CFStringRef URLString = CFStringCreateWithCString
- //       (0, m_path.toLocal8Bit().data(), 0);
-
-
-    QByteArray ba = m_path.toLocal8Bit();
-
-    CFURLRef url = CFURLCreateFromFileSystemRepresentation
-        (kCFAllocatorDefault,
-         (const UInt8 *)ba.data(),
-         (CFIndex)ba.length(),
-         false);
-
-
-//    m_d->err = QTNewDataReferenceFromURLCFString
-    m_d->err = QTNewDataReferenceFromCFURL
-        (url, 0, &dataRef, &dataRefType);
-
-    if (m_d->err) { 
-        m_error = QString("Error creating data reference for QuickTime decoder: code %1").arg(m_d->err);
-        return;
-    }
-    
-    short fileID = movieInDataForkResID; 
-    short flags = 0; 
-    m_d->err = NewMovieFromDataRef
-        (&m_d->movie, flags, &fileID, dataRef, dataRefType);
-
-    DisposeHandle(dataRef);
-    if (m_d->err) { 
-        m_error = QString("Error creating new movie for QuickTime decoder: code %1").arg(m_d->err); 
-        return;
-    }
-
-    Boolean isProtected = 0;
-    Track aTrack = GetMovieIndTrackType
-        (m_d->movie, 1, SoundMediaType,
-         movieTrackMediaType | movieTrackEnabledOnly);
-
-    if (aTrack) {
-        Media aMedia = GetTrackMedia(aTrack);	// get the track media
-        if (aMedia) {
-            MediaHandler mh = GetMediaHandler(aMedia);	// get the media handler we can query
-            if (mh) {
-                m_d->err = QTGetComponentProperty(mh,
-                                                  kQTPropertyClass_DRM,
-                                                  kQTDRMPropertyID_IsProtected,
-                                                  sizeof(Boolean), &isProtected,nil);
-            } else {
-                m_d->err = 1;
-            }
-        } else {
-            m_d->err = 1;
-        }
-    } else {
-        m_d->err = 1;
-    }
-	
-    if (m_d->err && m_d->err != kQTPropertyNotSupportedErr) { 
-        m_error = QString("Error checking for DRM in QuickTime decoder: code %1").arg(m_d->err);
-        return;
-    } else if (!m_d->err && isProtected) { 
-        m_error = QString("File is protected with DRM");
-        return;
-    } else if (m_d->err == kQTPropertyNotSupportedErr && !isProtected) {
-        cerr << "QuickTime: File is not protected with DRM" << endl;
-    }
-
-    if (m_d->movie) {
-        SetMovieActive(m_d->movie, TRUE);
-        m_d->err = GetMoviesError();
-        if (m_d->err) {
-            m_error = QString("Error in QuickTime decoder activation: code %1").arg(m_d->err);
-            return;
-        }
-    } else {
-	m_error = QString("Error in QuickTime decoder: Movie object not valid");
-	return;
-    }
-    
-    m_d->err = MovieAudioExtractionBegin
-        (m_d->movie, 0, &m_d->extractionSessionRef);
-    if (m_d->err) {
-        m_error = QString("Error in QuickTime decoder extraction init: code %1").arg(m_d->err);
-        return;
-    }
-
-    m_d->err = MovieAudioExtractionGetProperty
-        (m_d->extractionSessionRef,
-         kQTPropertyClass_MovieAudioExtraction_Audio, kQTMovieAudioExtractionAudioPropertyID_AudioStreamBasicDescription,
-         sizeof(m_d->asbd),
-         &m_d->asbd,
-         nil);
-
-    if (m_d->err) {
-        m_error = QString("Error in QuickTime decoder property get: code %1").arg(m_d->err);
-        return;
-    }
-	
-    m_channelCount = m_d->asbd.mChannelsPerFrame;
-    m_fileRate = m_d->asbd.mSampleRate;
-
-    cerr << "QuickTime: " << m_channelCount << " channels, " << m_fileRate << " kHz" << endl;
-
-    m_d->asbd.mFormatFlags =
-        kAudioFormatFlagIsFloat |
-        kAudioFormatFlagIsPacked |
-        kAudioFormatFlagsNativeEndian;
-    m_d->asbd.mBitsPerChannel = sizeof(float) * 8;
-    m_d->asbd.mBytesPerFrame = sizeof(float) * m_d->asbd.mChannelsPerFrame;
-    m_d->asbd.mBytesPerPacket = m_d->asbd.mBytesPerFrame;
-	
-    m_d->err = MovieAudioExtractionSetProperty
-        (m_d->extractionSessionRef,
-         kQTPropertyClass_MovieAudioExtraction_Audio,
-         kQTMovieAudioExtractionAudioPropertyID_AudioStreamBasicDescription,
-         sizeof(m_d->asbd),
-         &m_d->asbd);
-
-    if (m_d->err) {
-        m_error = QString("Error in QuickTime decoder property set: code %1").arg(m_d->err);
-        m_channelCount = 0;
-        return;
-    }
-    m_d->buffer.mNumberBuffers = 1;
-    m_d->buffer.mBuffers[0].mNumberChannels = m_channelCount;
-    m_d->buffer.mBuffers[0].mDataByteSize =
-        sizeof(float) * m_channelCount * m_d->blockSize;
-    m_d->data = new float[m_channelCount * m_d->blockSize];
-    m_d->buffer.mBuffers[0].mData = m_d->data;
-
-    initialiseDecodeCache();
-
-    if (decodeMode == DecodeAtOnce) {
-
-        if (m_reporter) {
-            connect(m_reporter, SIGNAL(cancelled()), this, SLOT(cancelled()));
-            m_reporter->setMessage
-                (tr("Decoding %1...").arg(QFileInfo(m_path).fileName()));
-        }
-
-        while (1) {
-            
-            UInt32 framesRead = m_d->blockSize;
-            UInt32 extractionFlags = 0;
-            m_d->err = MovieAudioExtractionFillBuffer
-                (m_d->extractionSessionRef, &framesRead, &m_d->buffer,
-                 &extractionFlags);
-            if (m_d->err) {
-                m_error = QString("Error in QuickTime decoding: code %1")
-                    .arg(m_d->err);
-                break;
-            }
-
-            //!!! progress?
-
-//    cerr << "Read " << framesRead << " frames (block size " << m_d->blockSize << ")" << endl;
-
-            // QuickTime buffers are interleaved unless specified otherwise
-            addSamplesToDecodeCache(m_d->data, framesRead);
-
-            if (framesRead < m_d->blockSize) break;
-        }
-        
-        finishDecodeCache();
-        endSerialised();
-
-        m_d->err = MovieAudioExtractionEnd(m_d->extractionSessionRef);
-        if (m_d->err) {
-            m_error = QString("Error ending QuickTime extraction session: code %1").arg(m_d->err);
-        }
-
-        m_completion = 100;
-
-    } else {
-        if (m_reporter) m_reporter->setProgress(100);
-
-        if (m_channelCount > 0) {
-            m_decodeThread = new DecodeThread(this);
-            m_decodeThread->start();
-        }
-    }
-
-    cerr << "QuickTimeFileReader::QuickTimeFileReader: frame count is now " << getFrameCount() << ", error is \"\"" << m_error << "\"" << endl;
-}
-
-QuickTimeFileReader::~QuickTimeFileReader()
-{
-    SVDEBUG << "QuickTimeFileReader::~QuickTimeFileReader" << endl;
-
-    if (m_decodeThread) {
-        m_cancelled = true;
-        m_decodeThread->wait();
-        delete m_decodeThread;
-    }
-
-    SetMovieActive(m_d->movie, FALSE);
-    DisposeMovie(m_d->movie);
-
-    delete[] m_d->data;
-    delete m_d;
-}
-
-void
-QuickTimeFileReader::cancelled()
-{
-    m_cancelled = true;
-}
-
-void
-QuickTimeFileReader::DecodeThread::run()
-{
-    if (m_reader->m_cacheMode == CacheInTemporaryFile) {
-        m_reader->m_completion = 1;
-        m_reader->startSerialised("QuickTimeFileReader::Decode");
-    }
-
-    while (1) {
-            
-        UInt32 framesRead = m_reader->m_d->blockSize;
-        UInt32 extractionFlags = 0;
-        m_reader->m_d->err = MovieAudioExtractionFillBuffer
-            (m_reader->m_d->extractionSessionRef, &framesRead,
-             &m_reader->m_d->buffer, &extractionFlags);
-        if (m_reader->m_d->err) {
-            m_reader->m_error = QString("Error in QuickTime decoding: code %1")
-                .arg(m_reader->m_d->err);
-            break;
-        }
-       
-        // QuickTime buffers are interleaved unless specified otherwise
-        m_reader->addSamplesToDecodeCache(m_reader->m_d->data, framesRead);
-        
-        if (framesRead < m_reader->m_d->blockSize) break;
-    }
-        
-    m_reader->finishDecodeCache();
-    
-    m_reader->m_d->err = MovieAudioExtractionEnd(m_reader->m_d->extractionSessionRef);
-    if (m_reader->m_d->err) {
-        m_reader->m_error = QString("Error ending QuickTime extraction session: code %1").arg(m_reader->m_d->err);
-    }
-    
-    m_reader->m_completion = 100;
-    m_reader->endSerialised();
-} 
-
-void
-QuickTimeFileReader::getSupportedExtensions(std::set<QString> &extensions)
-{
-    extensions.insert("aiff");
-    extensions.insert("aif");
-    extensions.insert("au");
-    extensions.insert("avi");
-    extensions.insert("m4a");
-    extensions.insert("m4b");
-    extensions.insert("m4p");
-    extensions.insert("m4v");
-    extensions.insert("mov");
-    extensions.insert("mp3");
-    extensions.insert("mp4");
-    extensions.insert("wav");
-}
-
-bool
-QuickTimeFileReader::supportsExtension(QString extension)
-{
-    std::set<QString> extensions;
-    getSupportedExtensions(extensions);
-    return (extensions.find(extension.toLower()) != extensions.end());
-}
-
-bool
-QuickTimeFileReader::supportsContentType(QString type)
-{
-    return (type == "audio/x-aiff" ||
-            type == "audio/x-wav" ||
-            type == "audio/mpeg" ||
-            type == "audio/basic" ||
-            type == "audio/x-aac" ||
-            type == "video/mp4" ||
-            type == "video/quicktime");
-}
-
-bool
-QuickTimeFileReader::supports(FileSource &source)
-{
-    return (supportsExtension(source.getExtension()) ||
-            supportsContentType(source.getContentType()));
-}
-
-#endif
-
--- a/data/fileio/QuickTimeFileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-/* -*- 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 file copyright 2006-2007 Chris Cannam and QMUL.
-
-    Based in part on QTAudioFile.h from SoundBite, copyright 2006
-    Chris Sutton and Mark Levy.
-    
-    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 _QUICKTIME_FILE_READER_H_
-#define _QUICKTIME_FILE_READER_H_
-
-#ifdef HAVE_QUICKTIME
-
-#include "CodedAudioFileReader.h"
-
-#include "base/Thread.h"
-
-#include <set>
-
-class ProgressReporter;
-
-class QuickTimeFileReader : public CodedAudioFileReader
-{
-    Q_OBJECT
-
-public:
-    enum DecodeMode {
-        DecodeAtOnce, // decode the file on construction, with progress
-        DecodeThreaded // decode in a background thread after construction
-    };
-
-    QuickTimeFileReader(FileSource source,
-                        DecodeMode decodeMode,
-                        CacheMode cacheMode,
-                        sv_samplerate_t targetRate = 0,
-                        bool normalised = false,
-                        ProgressReporter *reporter = 0);
-    virtual ~QuickTimeFileReader();
-
-    virtual QString getError() const { return m_error; }
-    virtual QString getLocation() const { return m_source.getLocation(); }
-    virtual QString getTitle() const { return m_title; }
-    
-    static void getSupportedExtensions(std::set<QString> &extensions);
-    static bool supportsExtension(QString ext);
-    static bool supportsContentType(QString type);
-    static bool supports(FileSource &source);
-
-    virtual int getDecodeCompletion() const { return m_completion; }
-
-    virtual bool isUpdating() const {
-        return m_decodeThread && m_decodeThread->isRunning();
-    }
-
-public slots:
-    void cancelled();
-
-protected:
-    FileSource m_source;
-    QString m_path;
-    QString m_error;
-    QString m_title;
-
-    class D;
-    D *m_d;
-
-    ProgressReporter *m_reporter;
-    bool m_cancelled;
-    int m_completion;
-
-    class DecodeThread : public Thread
-    {
-    public:
-        DecodeThread(QuickTimeFileReader *reader) : m_reader(reader) { }
-        virtual void run();
-
-    protected:
-        QuickTimeFileReader *m_reader; 
-    };
-
-    DecodeThread *m_decodeThread;
-};
-
-#endif
-
-#endif
--- a/data/fileio/WavFileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/WavFileReader.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -15,11 +15,16 @@
 
 #include "WavFileReader.h"
 
+#include "base/HitCount.h"
+#include "base/Profiler.h"
+
 #include <iostream>
 
 #include <QMutexLocker>
 #include <QFileInfo>
 
+using namespace std;
+
 WavFileReader::WavFileReader(FileSource source, bool fileUpdating) :
     m_file(0),
     m_source(source),
@@ -35,21 +40,26 @@
 
     m_fileInfo.format = 0;
     m_fileInfo.frames = 0;
+
+#ifdef Q_OS_WIN
+    m_file = sf_wchar_open((LPCWSTR)m_path.utf16(), SFM_READ, &m_fileInfo);
+#else
     m_file = sf_open(m_path.toLocal8Bit(), SFM_READ, &m_fileInfo);
+#endif
 
     if (!m_file || (!fileUpdating && m_fileInfo.channels <= 0)) {
-	cerr << "WavFileReader::initialize: Failed to open file at \""
-                  << m_path << "\" ("
-		  << sf_strerror(m_file) << ")" << endl;
+        SVDEBUG << "WavFileReader::initialize: Failed to open file at \""
+                << m_path << "\" ("
+                << sf_strerror(m_file) << ")" << endl;
 
-	if (m_file) {
-	    m_error = QString("Couldn't load audio file '%1':\n%2")
-		.arg(m_path).arg(sf_strerror(m_file));
-	} else {
-	    m_error = QString("Failed to open audio file '%1'")
-		.arg(m_path);
-	}
-	return;
+        if (m_file) {
+            m_error = QString("Couldn't load audio file '%1':\n%2")
+                .arg(m_path).arg(sf_strerror(m_file));
+        } else {
+            m_error = QString("Failed to open audio file '%1'")
+                .arg(m_path);
+        }
+        return;
     }
 
     if (m_fileInfo.channels > 0) {
@@ -79,7 +89,7 @@
         }
     }
 
-//    cerr << "WavFileReader: Filename " << m_path << ", frame count " << m_frameCount << ", channel count " << m_channelCount << ", sample rate " << m_sampleRate << ", format " << m_fileInfo.format << ", seekable " << m_fileInfo.seekable << " adjusted to " << m_seekable << endl;
+    SVDEBUG << "WavFileReader: Filename " << m_path << ", frame count " << m_frameCount << ", channel count " << m_channelCount << ", sample rate " << m_sampleRate << ", format " << m_fileInfo.format << ", seekable " << m_fileInfo.seekable << " adjusted to " << m_seekable << endl;
 }
 
 WavFileReader::~WavFileReader()
@@ -96,10 +106,14 @@
 
     if (m_file) {
         sf_close(m_file);
+#ifdef Q_OS_WIN
+        m_file = sf_wchar_open((LPCWSTR)m_path.utf16(), SFM_READ, &m_fileInfo);
+#else
         m_file = sf_open(m_path.toLocal8Bit(), SFM_READ, &m_fileInfo);
+#endif
         if (!m_file || m_fileInfo.channels <= 0) {
-            cerr << "WavFileReader::updateFrameCount: Failed to open file at \"" << m_path << "\" ("
-                      << sf_strerror(m_file) << ")" << endl;
+            SVDEBUG << "WavFileReader::updateFrameCount: Failed to open file at \"" << m_path << "\" ("
+                    << sf_strerror(m_file) << ")" << endl;
         }
     }
 
@@ -113,7 +127,6 @@
     }
 
     if (m_frameCount != prevCount) {
-//        cerr << "frameCountChanged" << endl;
         emit frameCountChanged();
     }
 }
@@ -125,53 +138,70 @@
     m_updating = false;
 }
 
-SampleBlock
+floatvec_t
 WavFileReader::getInterleavedFrames(sv_frame_t start, sv_frame_t count) const
 {
-    if (count == 0) return SampleBlock();
+    static HitCount lastRead("WavFileReader: last read");
+
+    if (count == 0) return {};
 
     QMutexLocker locker(&m_mutex);
 
+    Profiler profiler("WavFileReader::getInterleavedFrames");
+    
     if (!m_file || !m_channelCount) {
-        return SampleBlock();
+        return {};
     }
 
     if (start >= m_fileInfo.frames) {
 //        SVDEBUG << "WavFileReader::getInterleavedFrames: " << start
 //                  << " > " << m_fileInfo.frames << endl;
-	return SampleBlock();
+        return {};
     }
 
     if (start + count > m_fileInfo.frames) {
-	count = m_fileInfo.frames - start;
+        count = m_fileInfo.frames - start;
     }
 
-    if (start != m_lastStart || count != m_lastCount) {
-
-	if (sf_seek(m_file, start, SEEK_SET) < 0) {
-	    return SampleBlock();
-	}
-
-        sv_frame_t n = count * m_fileInfo.channels;
-        m_buffer.resize(size_t(n));
-	
-        sf_count_t readCount = 0;
-
-	if ((readCount = sf_readf_float(m_file, m_buffer.data(), count)) < 0) {
-	    return SampleBlock();
-	}
-
-        m_buffer.resize(size_t(readCount * m_fileInfo.channels));
-        
-	m_lastStart = start;
-	m_lastCount = readCount;
+    // Because WaveFileModel::getSummaries() is called separately for
+    // individual channels, it's quite common for us to be called
+    // repeatedly for the same data. So this is worth cacheing.
+    if (start == m_lastStart && count == m_lastCount) {
+        lastRead.hit();
+        return m_buffer;
     }
 
-    return m_buffer;
+    // We don't actually support partial cache reads, but let's use
+    // the term partial to refer to any forward seek and consider a
+    // backward seek to be a miss
+    if (start >= m_lastStart) {
+        lastRead.partial();
+    } else {
+        lastRead.miss();
+    }
+    
+    if (sf_seek(m_file, start, SEEK_SET) < 0) {
+        return {};
+    }
+
+    floatvec_t data;
+    sv_frame_t n = count * m_fileInfo.channels;
+    data.resize(n);
+
+    m_lastStart = start;
+    m_lastCount = count;
+    
+    sf_count_t readCount = 0;
+    if ((readCount = sf_readf_float(m_file, data.data(), count)) < 0) {
+        return {};
+    }
+
+    m_buffer = data;
+    return data;
 }
 
 void
-WavFileReader::getSupportedExtensions(std::set<QString> &extensions)
+WavFileReader::getSupportedExtensions(set<QString> &extensions)
 {
     int count;
 
@@ -202,7 +232,7 @@
 bool
 WavFileReader::supportsExtension(QString extension)
 {
-    std::set<QString> extensions;
+    set<QString> extensions;
     getSupportedExtensions(extensions);
     return (extensions.find(extension.toLower()) != extensions.end());
 }
--- a/data/fileio/WavFileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/WavFileReader.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,11 +13,16 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _WAV_FILE_READER_H_
-#define _WAV_FILE_READER_H_
+#ifndef SV_WAV_FILE_READER_H
+#define SV_WAV_FILE_READER_H
 
 #include "AudioFileReader.h"
 
+#ifdef Q_OS_WIN
+#include <windows.h>
+#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1
+#endif
+
 #include <sndfile.h>
 #include <QMutex>
 
@@ -50,7 +55,7 @@
      * Must be safe to call from multiple threads with different
      * arguments on the same object at the same time.
      */
-    virtual SampleBlock getInterleavedFrames(sv_frame_t start, sv_frame_t count) const;
+    virtual floatvec_t getInterleavedFrames(sv_frame_t start, sv_frame_t count) const;
     
     static void getSupportedExtensions(std::set<QString> &extensions);
     static bool supportsExtension(QString ext);
@@ -75,8 +80,7 @@
     bool m_seekable;
 
     mutable QMutex m_mutex;
-    mutable SampleBlock m_buffer;
-    mutable sv_frame_t m_bufsiz;
+    mutable floatvec_t m_buffer;
     mutable sv_frame_t m_lastStart;
     mutable sv_frame_t m_lastCount;
 
--- a/data/fileio/WavFileWriter.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/WavFileWriter.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -25,8 +25,10 @@
 #include <iostream>
 #include <cmath>
 
+using namespace std;
+
 WavFileWriter::WavFileWriter(QString path,
-			     sv_samplerate_t sampleRate,
+                             sv_samplerate_t sampleRate,
                              int channels,
                              FileWriteMode mode) :
     m_path(path),
@@ -48,25 +50,22 @@
     fileInfo.format = SF_FORMAT_WAV | SF_FORMAT_FLOAT;
 
     try {
+        QString writePath = m_path;
         if (mode == WriteToTemporary) {
             m_temp = new TempWriteFile(m_path);
-            m_file = sf_open(m_temp->getTemporaryFilename().toLocal8Bit(),
-                             SFM_WRITE, &fileInfo);
-            if (!m_file) {
-                cerr << "WavFileWriter: Failed to open file ("
-                          << sf_strerror(m_file) << ")" << endl;
-                m_error = QString("Failed to open audio file '%1' for writing")
-                    .arg(m_temp->getTemporaryFilename());
-            }
-        } else {
-            m_file = sf_open(m_path.toLocal8Bit(), SFM_WRITE, &fileInfo);
-            if (!m_file) {
-                cerr << "WavFileWriter: Failed to open file ("
-                          << sf_strerror(m_file) << ")" << endl;
-                m_error = QString("Failed to open audio file '%1' for writing")
-                    .arg(m_path);
-            }
-        }            
+            writePath = m_temp->getTemporaryFilename();
+        }
+#ifdef Q_OS_WIN
+        m_file = sf_wchar_open((LPCWSTR)writePath.utf16(), SFM_WRITE, &fileInfo);
+#else
+        m_file = sf_open(writePath.toLocal8Bit(), SFM_WRITE, &fileInfo);
+#endif
+        if (!m_file) {
+            cerr << "WavFileWriter: Failed to open file ("
+                 << sf_strerror(m_file) << ")" << endl;
+            m_error = QString("Failed to open audio file '%1' for writing")
+                .arg(writePath);
+        }
     } catch (FileOperationFailed &f) {
         m_error = f.what();
         m_temp = 0;
@@ -117,62 +116,59 @@
     if (!m_file) {
         m_error = QString("Failed to write model to audio file '%1': File not open")
             .arg(getWriteFilename());
-	return false;
+        return false;
     }
 
     bool ownSelection = false;
     if (!selection) {
-	selection = new MultiSelection;
-	selection->setSelection(Selection(source->getStartFrame(),
-					  source->getEndFrame()));
+        selection = new MultiSelection;
+        selection->setSelection(Selection(source->getStartFrame(),
+                                          source->getEndFrame()));
         ownSelection = true;
     }
 
     sv_frame_t bs = 2048;
-    float *ub = new float[bs]; // uninterleaved buffer (one channel)
-    float *ib = new float[bs * m_channels]; // interleaved buffer
 
     for (MultiSelection::SelectionList::iterator i =
-	     selection->getSelections().begin();
-	 i != selection->getSelections().end(); ++i) {
+         selection->getSelections().begin();
+         i != selection->getSelections().end(); ++i) {
 	
-	sv_frame_t f0(i->getStartFrame()), f1(i->getEndFrame());
+        sv_frame_t f0(i->getStartFrame()), f1(i->getEndFrame());
 
-	for (sv_frame_t f = f0; f < f1; f += bs) {
+        for (sv_frame_t f = f0; f < f1; f += bs) {
 	    
-	    sv_frame_t n = std::min(bs, f1 - f);
+            sv_frame_t n = min(bs, f1 - f);
+            floatvec_t interleaved(n * m_channels, 0.f);
 
-	    for (int c = 0; c < int(m_channels); ++c) {
-		source->getData(c, f, n, ub);
-		for (int i = 0; i < n; ++i) {
-		    ib[i * m_channels + c] = ub[i];
-		}
-	    }	    
+            for (int c = 0; c < int(m_channels); ++c) {
+                auto chanbuf = source->getData(c, f, n);
+                for (int i = 0; in_range_for(chanbuf, i); ++i) {
+                    interleaved[i * m_channels + c] = chanbuf[i];
+                }
+            }
 
-	    sf_count_t written = sf_writef_float(m_file, ib, n);
+            sf_count_t written = sf_writef_float(m_file, interleaved.data(), n);
 
-	    if (written < n) {
-		m_error = QString("Only wrote %1 of %2 frames at file frame %3")
-		    .arg(written).arg(n).arg(f);
-		break;
-	    }
-	}
+            if (written < n) {
+                m_error = QString("Only wrote %1 of %2 frames at file frame %3")
+                        .arg(written).arg(n).arg(f);
+                break;
+            }
+        }
     }
 
-    delete[] ub;
-    delete[] ib;
     if (ownSelection) delete selection;
 
     return isOK();
 }
 	
 bool
-WavFileWriter::writeSamples(float **samples, sv_frame_t count)
+WavFileWriter::writeSamples(const float *const *samples, sv_frame_t count)
 {
     if (!m_file) {
         m_error = QString("Failed to write model to audio file '%1': File not open")
             .arg(getWriteFilename());
-	return false;
+        return false;
     }
 
     float *b = new float[count * m_channels];
--- a/data/fileio/WavFileWriter.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/WavFileWriter.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,11 +13,16 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _WAV_FILE_WRITER_H_
-#define _WAV_FILE_WRITER_H_
+#ifndef SV_WAV_FILE_WRITER_H
+#define SV_WAV_FILE_WRITER_H
 
 #include <QString>
 
+#ifdef Q_OS_WIN
+#include <windows.h>
+#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1
+#endif
+
 #include <sndfile.h>
 
 #include "base/BaseTypes.h"
@@ -59,7 +64,7 @@
     bool writeModel(DenseTimeValueModel *source,
                     MultiSelection *selection = 0);
 
-    bool writeSamples(float **samples, sv_frame_t count); // count per channel
+    bool writeSamples(const float *const *samples, sv_frame_t count); // count per channel
 
     bool close();
 
--- a/data/fileio/test/AudioFileReaderTest.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/test/AudioFileReaderTest.h	Fri Jan 13 10:29:44 2017 +0000
@@ -18,6 +18,7 @@
 
 #include "../AudioFileReaderFactory.h"
 #include "../AudioFileReader.h"
+#include "../WavFileWriter.h"
 
 #include "AudioTestData.h"
 
@@ -31,52 +32,232 @@
 
 using namespace std;
 
-static QString audioDir = "testfiles";
-
 class AudioFileReaderTest : public QObject
 {
     Q_OBJECT
 
+private:
+    QString testDirBase;
+    QString audioDir;
+    QString diffDir;
+
+public:
+    AudioFileReaderTest(QString base) {
+        if (base == "") {
+            base = "svcore/data/fileio/test";
+        }
+        testDirBase = base;
+        audioDir = base + "/audio";
+        diffDir = base + "/diffs";
+    }
+
+private:
     const char *strOf(QString s) {
         return strdup(s.toLocal8Bit().data());
     }
 
+    void getFileMetadata(QString filename,
+                         QString &extension,
+                         sv_samplerate_t &rate,
+                         int &channels,
+                         int &bitdepth) {
+
+        QStringList fileAndExt = filename.split(".");
+        QStringList bits = fileAndExt[0].split("-");
+
+        extension = fileAndExt[1];
+        rate = bits[0].toInt();
+        channels = bits[1].toInt();
+        bitdepth = 16;
+        if (bits.length() > 2) {
+            bitdepth = bits[2].toInt();
+        }
+    }
+    
+    void getExpectedThresholds(QString format,
+                               QString filename,
+                               bool resampled,
+                               bool gapless,
+                               bool normalised,
+                               double &maxLimit,
+                               double &rmsLimit) {
+
+        QString extension;
+        sv_samplerate_t fileRate;
+        int channels;
+        int bitdepth;
+        getFileMetadata(filename, extension, fileRate, channels, bitdepth);
+        
+        if (normalised) {
+
+            if (format == "ogg") {
+
+                // Our ogg is not especially high quality and is
+                // actually further from the original if normalised
+
+                maxLimit = 0.1;
+                rmsLimit = 0.03;
+
+            } else if (format == "aac") {
+
+                // Terrible performance for this test, load of spill
+                // from one channel to the other. I guess they know
+                // what they're doing, it's perceptual after all, but
+                // it does make this check a bit superfluous, you
+                // could probably pass it with a signal that sounds
+                // nothing like the original
+                maxLimit = 0.2;
+                rmsLimit = 0.1;
+
+            } else if (format == "mp3") {
+
+                if (resampled && !gapless) {
+
+                    // We expect worse figures here, because the
+                    // combination of uncompensated encoder delay +
+                    // resampling results in a fractional delay which
+                    // means the decoded signal is slightly out of
+                    // phase compared to the test signal
+
+                    maxLimit = 0.1;
+                    rmsLimit = 0.05;
+
+                } else {
+
+                    maxLimit = 0.05;
+                    rmsLimit = 0.01;
+                }
+
+            } else {
+
+                // lossless formats (wav, aiff, flac, apple_lossless)
+                
+                if (bitdepth >= 16 && !resampled) {
+                    maxLimit = 1e-3;
+                    rmsLimit = 3e-4;
+                } else {
+                    maxLimit = 0.01;
+                    rmsLimit = 5e-3;
+                }
+            }
+            
+        } else { // !normalised
+            
+            if (format == "ogg") {
+
+                maxLimit = 0.06;
+                rmsLimit = 0.03;
+
+            } else if (format == "aac") {
+
+                maxLimit = 0.1;
+                rmsLimit = 0.1;
+
+            } else if (format == "mp3") {
+
+                // all mp3 figures are worse when not normalising
+                maxLimit = 0.1;
+                rmsLimit = 0.05;
+
+            } else {
+
+                // lossless formats (wav, aiff, flac, apple_lossless)
+                
+                if (bitdepth >= 16 && !resampled) {
+                    maxLimit = 1e-3;
+                    rmsLimit = 3e-4;
+                } else {
+                    maxLimit = 0.02;
+                    rmsLimit = 0.01;
+                }
+            }
+        }
+    }
+
+    QString testName(QString format, QString filename, int rate, bool norm, bool gapless) {
+        return QString("%1/%2 at %3%4%5")
+            .arg(format)
+            .arg(filename)
+            .arg(rate)
+            .arg(norm ? " normalised": "")
+            .arg(gapless ? "" : " non-gapless");
+    }
+
 private slots:
     void init()
     {
         if (!QDir(audioDir).exists()) {
-            cerr << "ERROR: Audio test file directory \"" << audioDir << "\" does not exist" << endl;
+            QString cwd = QDir::currentPath();
+            cerr << "ERROR: Audio test file directory \"" << audioDir << "\" does not exist (cwd = " << cwd << ")" << endl;
             QVERIFY2(QDir(audioDir).exists(), "Audio test file directory not found");
         }
+        if (!QDir(diffDir).exists() && !QDir().mkpath(diffDir)) {
+            cerr << "ERROR: Audio diff directory \"" << diffDir << "\" does not exist and could not be created" << endl;
+            QVERIFY2(QDir(diffDir).exists(), "Audio diff directory not found and could not be created");
+        }
     }
 
     void read_data()
     {
+        QTest::addColumn<QString>("format");
         QTest::addColumn<QString>("audiofile");
-        QStringList files = QDir(audioDir).entryList(QDir::Files);
-        foreach (QString filename, files) {
-            QTest::newRow(strOf(filename)) << filename;
+        QTest::addColumn<int>("rate");
+        QTest::addColumn<bool>("normalised");
+        QTest::addColumn<bool>("gapless");
+        QStringList dirs = QDir(audioDir).entryList(QDir::Dirs |
+                                                    QDir::NoDotAndDotDot);
+        for (QString format: dirs) {
+            QStringList files = QDir(QDir(audioDir).filePath(format))
+                .entryList(QDir::Files);
+            int readRates[] = { 44100, 48000 };
+            bool norms[] = { false, true };
+            bool gaplesses[] = { true, false };
+            foreach (QString filename, files) {
+                for (int rate: readRates) {
+                    for (bool norm: norms) {
+                        for (bool gapless: gaplesses) {
+
+                            if (format != "mp3" && !gapless) {
+                                continue;
+                            }
+                        
+                            QString desc = testName
+                                (format, filename, rate, norm, gapless);
+
+                            QTest::newRow(strOf(desc))
+                                << format << filename << rate << norm << gapless;
+                        }
+                    }
+                }
+            }
         }
     }
 
     void read()
     {
+        QFETCH(QString, format);
         QFETCH(QString, audiofile);
+        QFETCH(int, rate);
+        QFETCH(bool, normalised);
+        QFETCH(bool, gapless);
 
-        sv_samplerate_t readRate = 48000;
+        sv_samplerate_t readRate(rate);
+        
+//        cerr << "\naudiofile = " << audiofile << endl;
+
+        AudioFileReaderFactory::Parameters params;
+        params.targetRate = readRate;
+        params.normalisation = (normalised ?
+                                AudioFileReaderFactory::Normalisation::Peak :
+                                AudioFileReaderFactory::Normalisation::None);
+        params.gaplessMode = (gapless ?
+                              AudioFileReaderFactory::GaplessMode::Gapless :
+                              AudioFileReaderFactory::GaplessMode::Gappy);
 
 	AudioFileReader *reader =
 	    AudioFileReaderFactory::createReader
-	    (audioDir + "/" + audiofile, readRate);
-
-        QStringList fileAndExt = audiofile.split(".");
-        QStringList bits = fileAndExt[0].split("-");
-        QString extension = fileAndExt[1];
-        sv_samplerate_t nominalRate = bits[0].toInt();
-        int nominalChannels = bits[1].toInt();
-        int nominalDepth = 16;
-        if (bits.length() > 2) nominalDepth = bits[2].toInt();
-
+	    (audioDir + "/" + format + "/" + audiofile, params);
+        
 	if (!reader) {
 #if ( QT_VERSION >= 0x050000 )
 	    QSKIP("Unsupported file, skipping");
@@ -85,11 +266,16 @@
 #endif
 	}
 
-        QCOMPARE((int)reader->getChannelCount(), nominalChannels);
-        QCOMPARE(reader->getNativeRate(), nominalRate);
+        QString extension;
+        sv_samplerate_t fileRate;
+        int channels;
+        int fileBitdepth;
+        getFileMetadata(audiofile, extension, fileRate, channels, fileBitdepth);
+        
+        QCOMPARE((int)reader->getChannelCount(), channels);
+        QCOMPARE(reader->getNativeRate(), fileRate);
         QCOMPARE(reader->getSampleRate(), readRate);
 
-	int channels = reader->getChannelCount();
 	AudioTestData tdata(readRate, channels);
 	
 	float *reference = tdata.getInterleavedData();
@@ -100,95 +286,200 @@
 	// more, though, so we can (a) check that we only get the
 	// expected number back (if this is not mp3/aac) or (b) take
 	// into account silence at beginning and end (if it is).
-	vector<float> test = reader->getInterleavedFrames(0, refFrames + 5000);
+	floatvec_t test = reader->getInterleavedFrames(0, refFrames + 5000);
 	sv_frame_t read = test.size() / channels;
 
-        if (extension == "mp3" || extension == "aac" || extension == "m4a") {
-            // mp3s and aacs can have silence at start and end
+        bool perceptual = (extension == "mp3" ||
+                           extension == "aac" ||
+                           extension == "m4a");
+        
+        if (perceptual && !gapless) {
+            // allow silence at start and end
             QVERIFY(read >= refFrames);
         } else {
             QCOMPARE(read, refFrames);
         }
 
-        // Our limits are pretty relaxed -- we're not testing decoder
-        // or resampler quality here, just whether the results are
-        // plainly wrong (e.g. at wrong samplerate or with an offset)
-
-	double limit = 0.01;
-        double edgeLimit = limit * 10; // in first or final edgeSize frames
+        bool resampled = readRate != fileRate;
+        double maxLimit, rmsLimit;
+        getExpectedThresholds(format,
+                              audiofile,
+                              resampled,
+                              gapless,
+                              normalised,
+                              maxLimit, rmsLimit);
+        
+        double edgeLimit = maxLimit * 3; // in first or final edgeSize frames
+        if (resampled && edgeLimit < 0.1) edgeLimit = 0.1;
         int edgeSize = 100; 
 
-        if (nominalDepth < 16) {
-            limit = 0.02;
-        }
-        if (extension == "ogg" || extension == "mp3" ||
-            extension == "aac" || extension == "m4a") {
-            limit = 0.2;
-            edgeLimit = limit * 3;
-        }
-
         // And we ignore completely the last few frames when upsampling
-        int discard = 1 + int(round(readRate / nominalRate));
+        int discard = 1 + int(round(readRate / fileRate));
 
         int offset = 0;
 
-        if (extension == "aac" || extension == "m4a") {
-            // our m4a file appears to have a fixed offset of 1024 (at
-            // file sample rate)
-            offset = int(round((1024 / nominalRate) * readRate));
-        }
+        if (perceptual) {
 
-        if (extension == "mp3") {
-            // while mp3s appear to vary
-            for (int i = 0; i < read; ++i) {
-                bool any = false;
-                double thresh = 0.01;
-                for (int c = 0; c < channels; ++c) {
-                    if (fabs(test[i * channels + c]) > thresh) {
-                        any = true;
+            // Look for an initial offset.
+            //
+            // We know the first channel has a sinusoid in it. It
+            // should have a peak at 0.4ms (see AudioTestData.h) but
+            // that might have been clipped, which would make it
+            // imprecise. We can tell if it's clipped, though, as
+            // there will be samples having exactly identical
+            // values. So what we look for is the peak if it's not
+            // clipped and, if it is, the first zero crossing after
+            // the peak, which should be at 0.8ms.
+
+            int expectedPeak = int(0.0004 * readRate);
+            int expectedZC = int(0.0008 * readRate);
+            bool foundPeak = false;
+            for (int i = 1; i+1 < read; ++i) {
+                float prevSample = test[(i-1) * channels];
+                float thisSample = test[i * channels];
+                float nextSample = test[(i+1) * channels];
+                if (thisSample > 0.8 && nextSample < thisSample) {
+                    foundPeak = true;
+                    if (thisSample > prevSample) {
+                        // not clipped
+                        offset = i - expectedPeak - 1;
                         break;
                     }
                 }
-                if (any) {
-                    offset = i;
+                if (foundPeak && (thisSample >= 0.0 && nextSample < 0.0)) {
+//                    cerr << "thisSample = " << thisSample << ", nextSample = "
+//                         << nextSample << endl;
+                    offset = i - expectedZC - 1;
                     break;
                 }
             }
+
+//            int fileRateEquivalent = int((offset / readRate) * fileRate);
 //            std::cerr << "offset = " << offset << std::endl;
+//            std::cerr << "at file rate would be " << fileRateEquivalent << std::endl;
+
+            // Previously our m4a test file had a fixed offset of 1024
+            // at the file sample rate -- this may be because it was
+            // produced by FAAC which did not write in the delay as
+            // metadata? We now have an m4a produced by Core Audio
+            // which gives a 0 offset. What to do...
+
+            // Anyway, mp3s should have 0 offset in gapless mode and
+            // "something else" otherwise.
+            
+            if (gapless) {
+                if (format == "aac") {
+                    // ouch!
+                    if (offset == -1) offset = 0;
+                }
+                QCOMPARE(offset, 0);
+            }
         }
 
-	for (int c = 0; c < channels; ++c) {
-	    float maxdiff = 0.f;
-	    int maxAt = 0;
-	    float totdiff = 0.f;
-	    for (int i = 0; i < read - offset - discard && i < refFrames; ++i) {
-		float diff = fabsf(test[(i + offset) * channels + c] -
-				   reference[i * channels + c]);
-		totdiff += diff;
+        {
+            // Write the diff file now, so that it's already been written
+            // even if the comparison fails. We aren't checking anything
+            // here except as necessary to avoid buffer overruns etc
+
+            QString diffFile =
+                testName(format, audiofile, rate, normalised, gapless);
+            diffFile.replace("/", "_");
+            diffFile.replace(".", "_");
+            diffFile.replace(" ", "_");
+            diffFile += ".wav";
+            diffFile = QDir(diffDir).filePath(diffFile);
+            WavFileWriter diffWriter(diffFile, readRate, channels,
+                                     WavFileWriter::WriteToTemporary);
+            QVERIFY(diffWriter.isOK());
+
+            vector<vector<float>> diffs(channels);
+            for (int c = 0; c < channels; ++c) {
+                for (int i = 0; i < refFrames; ++i) {
+                    int ix = i + offset;
+                    if (ix < read) {
+                        float signeddiff =
+                            test[ix * channels + c] -
+                            reference[i * channels + c];
+                        diffs[c].push_back(signeddiff);
+                    }
+                }
+            }
+            float **ptrs = new float*[channels];
+            for (int c = 0; c < channels; ++c) {
+                ptrs[c] = diffs[c].data();
+            }
+            diffWriter.writeSamples(ptrs, refFrames);
+            delete[] ptrs;
+        }
+            
+        for (int c = 0; c < channels; ++c) {
+
+            double maxDiff = 0.0;
+            double totalDiff = 0.0;
+            double totalSqrDiff = 0.0;
+            int maxIndex = 0;
+
+            for (int i = 0; i < refFrames; ++i) {
+                int ix = i + offset;
+                if (ix >= read) {
+                    cerr << "ERROR: audiofile " << audiofile << " reads truncated (read-rate reference frames " << i << " onward, of " << refFrames << ", are lost)" << endl;
+                    QVERIFY(ix < read);
+                }
+
+                if (ix + discard >= read) {
+                    // we forgive the very edge samples when
+                    // resampling (discard > 0)
+                    continue;
+                }
+                
+                double diff = fabs(test[ix * channels + c] -
+                                   reference[i * channels + c]);
+
+                totalDiff += diff;
+                totalSqrDiff += diff * diff;
+                
                 // in edge areas, record this only if it exceeds edgeLimit
-                if (i < edgeSize || i + edgeSize >= read - offset) {
-                    if (diff > edgeLimit && diff > maxdiff) {
-                        maxdiff = diff;
-                        maxAt = i;
+                if (i < edgeSize || i + edgeSize >= refFrames) {
+                    if (diff > edgeLimit && diff > maxDiff) {
+                        maxDiff = diff;
+                        maxIndex = i;
                     }
                 } else {
-                    if (diff > maxdiff) {
-                        maxdiff = diff;
-                        maxAt = i;
+                    if (diff > maxDiff) {
+                        maxDiff = diff;
+                        maxIndex = i;
                     }
-		}
-	    }
-	    float meandiff = totdiff / float(read);
-//	    cerr << "meandiff on channel " << c << ": " << meandiff << endl;
-//	    cerr << "maxdiff on channel " << c << ": " << maxdiff << " at " << maxAt << endl;
-            if (meandiff >= limit) {
-		cerr << "ERROR: for audiofile " << audiofile << ": mean diff = " << meandiff << " for channel " << c << endl;
-                QVERIFY(meandiff < limit);
+                }
             }
-	    if (maxdiff >= limit) {
-		cerr << "ERROR: for audiofile " << audiofile << ": max diff = " << maxdiff << " at frame " << maxAt << " of " << read << " on channel " << c << " (mean diff = " << meandiff << ")" << endl;
-		QVERIFY(maxdiff < limit);
-	    }
+                
+            double meanDiff = totalDiff / double(refFrames);
+            double rmsDiff = sqrt(totalSqrDiff / double(refFrames));
+
+            /*
+        cerr << "channel " << c << ": mean diff " << meanDiff << endl;
+	    cerr << "channel " << c << ":  rms diff " << rmsDiff << endl;
+	    cerr << "channel " << c << ":  max diff " << maxDiff << " at " << maxIndex << endl;
+            */            
+            if (rmsDiff >= rmsLimit) {
+                cerr << "ERROR: for audiofile " << audiofile << ": RMS diff = " << rmsDiff << " for channel " << c << " (limit = " << rmsLimit << ")" << endl;
+                QVERIFY(rmsDiff < rmsLimit);
+            }
+            if (maxDiff >= maxLimit) {
+                cerr << "ERROR: for audiofile " << audiofile << ": max diff = " << maxDiff << " at frame " << maxIndex << " of " << read << " on channel " << c << " (limit = " << maxLimit << ", edge limit = " << edgeLimit << ", mean diff = " << meanDiff << ", rms = " << rmsDiff << ")" << endl;
+                QVERIFY(maxDiff < maxLimit);
+            }
+
+            // and check for spurious material at end
+            
+            for (sv_frame_t i = refFrames; i + offset < read; ++i) {
+                sv_frame_t ix = i + offset;
+                float quiet = 0.1f; //!!! allow some ringing - but let's come back to this, it should tail off
+                float mag = fabsf(test[ix * channels + c]);
+                if (mag > quiet) {
+                    cerr << "ERROR: audiofile " << audiofile << " contains spurious data after end of reference (found sample " << test[ix * channels + c] << " at index " << ix << " of channel " << c << " after reference+offset ended at " << refFrames+offset << ")" << endl;
+                    QVERIFY(mag < quiet);
+                }
+            }
 	}
     }
 };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/AudioFileWriterTest.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,135 @@
+/* -*- 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_AUDIO_FILE_WRITER_H
+#define TEST_AUDIO_FILE_WRITER_H
+
+#include "../AudioFileReaderFactory.h"
+#include "../AudioFileReader.h"
+#include "../WavFileWriter.h"
+
+#include "AudioTestData.h"
+
+#include "bqvec/VectorOps.h"
+#include "bqvec/Allocators.h"
+
+#include <cmath>
+
+#include <QObject>
+#include <QtTest>
+#include <QDir>
+
+#include <iostream>
+
+using namespace std;
+using namespace breakfastquay;
+
+class AudioFileWriterTest : public QObject
+{
+    Q_OBJECT
+
+private:
+    QString testDirBase;
+    QString outDir;
+
+    static const int rate = 44100;
+    
+public:
+    AudioFileWriterTest(QString base) {
+        if (base == "") {
+            base = "svcore/data/fileio/test";
+        }
+        testDirBase = base;
+        outDir = base + "/outfiles";
+    }
+
+    const char *strOf(QString s) {
+        return strdup(s.toLocal8Bit().data());
+    }
+    
+    QString testName(bool direct, int channels) {
+        return QString("%1 %2 %3")
+            .arg(channels)
+            .arg(channels > 1 ? "channels" : "channel")
+            .arg(direct ? "direct" : "via temporary");
+    }
+
+private slots:
+    void init()
+    {
+        if (!QDir(outDir).exists() && !QDir().mkpath(outDir)) {
+            cerr << "ERROR: Audio out directory \"" << outDir << "\" does not exist and could not be created" << endl;
+            QVERIFY2(QDir(outDir).exists(), "Audio out directory not found and could not be created");
+        }
+    }
+
+    void write_data()
+    {
+        QTest::addColumn<bool>("direct");
+        QTest::addColumn<int>("channels");
+        for (int direct = 0; direct <= 1; ++direct) {
+            for (int channels = 1; channels < 8; ++channels) {
+                if (channels == 1 || channels == 2 ||
+                    channels == 5 || channels == 8) {
+                    QString desc = testName(direct, channels);
+                    QTest::newRow(strOf(desc)) << (bool)direct << channels;
+                }
+            }
+        }
+    }
+    
+    void write()
+    {
+        QFETCH(bool, direct);
+        QFETCH(int, channels);
+
+        QString outfile = QString("%1/out-%2ch-%3.wav")
+            .arg(outDir).arg(channels).arg(direct ? "direct" : "via-temporary");
+        
+        WavFileWriter writer(outfile,
+                             rate,
+                             channels,
+                             direct ?
+                             WavFileWriter::WriteToTarget :
+                             WavFileWriter::WriteToTemporary);
+        QVERIFY(writer.isOK());
+
+        AudioTestData data(rate, channels);
+        data.generate();
+
+        sv_frame_t frameCount = data.getFrameCount();
+        float *interleaved = data.getInterleavedData();
+        float **nonInterleaved = allocate_channels<float>(channels, frameCount);
+        v_deinterleave(nonInterleaved, interleaved, channels, int(frameCount));
+        bool ok = writer.writeSamples(nonInterleaved, frameCount);
+        deallocate_channels(nonInterleaved, channels);
+        QVERIFY(ok);
+        
+        ok = writer.close();
+        QVERIFY(ok);
+
+        AudioFileReaderFactory::Parameters params;
+        AudioFileReader *rereader =
+            AudioFileReaderFactory::createReader(outfile, params);
+        QVERIFY(rereader != nullptr);
+        
+        floatvec_t readFrames = rereader->getInterleavedFrames(0, frameCount);
+        floatvec_t expected(interleaved, interleaved + frameCount * channels);
+        QCOMPARE(readFrames, expected);
+
+        delete rereader;
+    }
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/EncodingTest.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,257 @@
+/* -*- 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_AUDIO_ENCODINGS_H
+#define TEST_AUDIO_ENCODINGS_H
+
+// Quick tests for filename encodings and encoding of ID3 data. Not a
+// test of audio codecs.
+
+#include "../AudioFileReaderFactory.h"
+#include "../AudioFileReader.h"
+#include "../WavFileWriter.h"
+
+#include <cmath>
+
+#include <QObject>
+#include <QtTest>
+#include <QDir>
+
+#include <iostream>
+
+using namespace std;
+
+const char utf8_name_cdp_1[] = "Caf\303\251 de Paris";
+const char utf8_name_cdp_2[] = "Caf\303\251 de \351\207\215\345\272\206";
+const char utf8_name_tsprk[] = "T\303\253mple of Sp\303\266rks";
+const char utf8_name_sprkt[] = "\343\202\271\343\203\235\343\203\274\343\202\257\343\201\256\345\257\272\351\231\242";
+
+// Mapping between filename and expected title metadata field
+static const char *mapping[][2] = {
+    { "id3v2-iso-8859-1", utf8_name_cdp_1 },
+    { "id3v2-ucs-2", utf8_name_cdp_2 },
+    { utf8_name_tsprk, utf8_name_tsprk },
+    { utf8_name_sprkt, utf8_name_sprkt },
+};
+static const int mappingCount = 4;
+
+class EncodingTest : public QObject
+{
+    Q_OBJECT
+
+private:
+    QString testDirBase;
+    QString encodingDir;
+    QString outDir;
+
+public:
+    EncodingTest(QString base) {
+        if (base == "") {
+            base = "svcore/data/fileio/test";
+        }
+        testDirBase = base;
+        encodingDir = base + "/encodings";
+        outDir = base + "/outfiles";
+    }
+
+private:
+    const char *strOf(QString s) {
+        return strdup(s.toLocal8Bit().data());
+    }
+
+    void addAudioFiles() {
+        QTest::addColumn<QString>("audiofile");
+        QStringList files = QDir(encodingDir).entryList(QDir::Files);
+        foreach (QString filename, files) {
+            QTest::newRow(strOf(filename)) << filename;
+        }
+    }
+
+private slots:
+    void init()
+    {
+        if (!QDir(encodingDir).exists()) {
+            cerr << "ERROR: Audio encoding file directory \"" << encodingDir << "\" does not exist" << endl;
+            QVERIFY2(QDir(encodingDir).exists(), "Audio encoding file directory not found");
+        }
+        if (!QDir(outDir).exists() && !QDir().mkpath(outDir)) {
+            cerr << "ERROR: Audio out directory \"" << outDir << "\" does not exist and could not be created" << endl;
+            QVERIFY2(QDir(outDir).exists(), "Audio out directory not found and could not be created");
+        }
+    }
+
+    void readAudio_data() {
+        addAudioFiles();
+    }
+
+    void readAudio() {
+
+        // Ensure that we can open all the files
+        
+        QFETCH(QString, audiofile);
+
+        AudioFileReaderFactory::Parameters params;
+        AudioFileReader *reader =
+            AudioFileReaderFactory::createReader
+            (encodingDir + "/" + audiofile, params);
+
+        QVERIFY(reader != nullptr);
+    }
+
+    void readMetadata_data() {
+        addAudioFiles();
+    }
+    
+    void readMetadata() {
+        
+        // All files other than WAVs should have title metadata; check
+        // that the title matches whatever is in our mapping structure
+        // defined at the top
+        
+        QFETCH(QString, audiofile);
+
+        AudioFileReaderFactory::Parameters params;
+        AudioFileReader *reader =
+            AudioFileReaderFactory::createReader
+            (encodingDir + "/" + audiofile, params);
+
+        QVERIFY(reader != nullptr);
+
+        QStringList fileAndExt = audiofile.split(".");
+        QString file = fileAndExt[0];
+        QString extension = fileAndExt[1];
+
+        if (extension != "wav") {
+
+#if (!defined (HAVE_OGGZ) || !defined(HAVE_FISHSOUND))
+            if (extension == "ogg") {
+                QSKIP("Lack native Ogg Vorbis reader, so won't be getting metadata");
+            }
+#endif
+            
+            auto blah = reader->getInterleavedFrames(0, 10);
+            
+            QString title = reader->getTitle();
+            QVERIFY(title != QString());
+
+            bool found = false;
+            for (int m = 0; m < mappingCount; ++m) {
+                if (file == QString::fromUtf8(mapping[m][0])) {
+                    found = true;
+                    QString expected = QString::fromUtf8(mapping[m][1]);
+                    if (title != expected) {
+                        cerr << "Title does not match expected: codepoints are" << endl;
+                        cerr << "Title (" << title.length() << "ch): ";
+                        for (int i = 0; i < title.length(); ++i) {
+                            cerr << title[i].unicode() << " ";
+                        }
+                        cerr << endl;
+                        cerr << "Expected (" << expected.length() << "ch): ";
+                        for (int i = 0; i < expected.length(); ++i) {
+                            cerr << expected[i].unicode() << " ";
+                        }
+                        cerr << endl;
+                    }
+                    QCOMPARE(title, expected);
+                    break;
+                }
+            }
+
+            if (!found) {
+                // Note that this can happen legitimately on Windows,
+                // where (for annoying VCS-related reasons) the test
+                // files may have a different filename encoding from
+                // the expected UTF-16. We check this properly in
+                // readWriteAudio below, by saving out the file to a
+                // name matching the metadata
+                cerr << "Couldn't find filename \""
+                     << file << "\" in title mapping array" << endl;
+                QSKIP("Couldn't find filename in title mapping array");
+            }
+        }
+    }
+
+    void readWriteAudio_data() {
+        addAudioFiles();
+    }
+
+    void readWriteAudio()
+    {
+        // For those files that have title metadata (i.e. all of them
+        // except the WAVs), read the title metadata and write a wav
+        // file (of arbitrary content) whose name matches that.  Then
+        // check that we can re-read it. This is intended to exercise
+        // systems on which the original test filename is miscoded (as
+        // can happen on Windows).
+        
+        QFETCH(QString, audiofile);
+
+        QStringList fileAndExt = audiofile.split(".");
+        QString file = fileAndExt[0];
+        QString extension = fileAndExt[1];
+
+        if (extension == "wav") {
+            return;
+        }
+
+#if (!defined (HAVE_OGGZ) || !defined(HAVE_FISHSOUND))
+        if (extension == "ogg") {
+            QSKIP("Lack native Ogg Vorbis reader, so won't be getting metadata");
+        }
+#endif
+
+        AudioFileReaderFactory::Parameters params;
+        AudioFileReader *reader =
+            AudioFileReaderFactory::createReader
+            (encodingDir + "/" + audiofile, params);
+        QVERIFY(reader != nullptr);
+
+        QString title = reader->getTitle();
+        QVERIFY(title != QString());
+
+        for (int useTemporary = 0; useTemporary <= 1; ++useTemporary) {
+        
+            QString outfile = outDir + "/" + file + ".wav";
+            WavFileWriter writer(outfile,
+                                 reader->getSampleRate(),
+                                 1,
+                                 useTemporary ?
+                                 WavFileWriter::WriteToTemporary :
+                                 WavFileWriter::WriteToTarget);
+
+            QVERIFY(writer.isOK());
+
+            floatvec_t data { 0.0, 1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0 };
+            const float *samples = data.data();
+            bool ok = writer.writeSamples(&samples, 8);
+            QVERIFY(ok);
+
+            ok = writer.close();
+            QVERIFY(ok);
+
+            AudioFileReader *rereader =
+                AudioFileReaderFactory::createReader(outfile, params);
+            QVERIFY(rereader != nullptr);
+
+            floatvec_t readFrames = rereader->getInterleavedFrames(0, 8);
+            QCOMPARE(readFrames, data);
+
+            delete rereader;
+        }
+
+        delete reader;
+    }
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/MIDIFileReaderTest.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,88 @@
+/* -*- 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 file copyright 2013 Chris Cannam.
+    
+    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_MIDI_FILE_READER_H
+#define TEST_MIDI_FILE_READER_H
+
+#include "../MIDIFileReader.h"
+
+#include <cmath>
+
+#include <QObject>
+#include <QtTest>
+#include <QDir>
+
+#include "base/Debug.h"
+
+#include <iostream>
+
+using namespace std;
+
+class MIDIFileReaderTest : public QObject
+{
+    Q_OBJECT
+
+private:
+    QString testDirBase;
+    QString midiDir;
+
+    const char *strOf(QString s) {
+        return strdup(s.toLocal8Bit().data());
+    }
+
+public:
+    MIDIFileReaderTest(QString base) {
+        if (base == "") {
+            base = "svcore/data/fileio/test";
+        }
+        testDirBase = base;
+        midiDir = base + "/midi";
+    }
+
+private slots:
+    void init()
+    {
+        if (!QDir(midiDir).exists()) {
+            cerr << "ERROR: MIDI file directory \"" << midiDir << "\" does not exist" << endl;
+            QVERIFY2(QDir(midiDir).exists(), "MIDI file directory not found");
+        }
+    }
+
+    void read_data()
+    {
+        QTest::addColumn<QString>("filename");
+        QStringList files = QDir(midiDir).entryList(QDir::Files);
+        foreach (QString filename, files) {
+            QTest::newRow(strOf(filename)) << filename;
+        }
+    }
+    
+    void read()
+    {
+        QFETCH(QString, filename);
+        QString path = midiDir + "/" + filename;
+        MIDIFileReader reader(path, nullptr, 44100);
+        Model *m = reader.load();
+        if (!m) {
+            cerr << "MIDI load failed for path: \"" << path << "\"" << endl;
+        }
+        QVERIFY(m != nullptr);
+        //!!! Ah, now here we could do something a bit more informative
+    }
+
+};
+
+#endif
+
Binary file data/fileio/test/audio/aac/32000-1.m4a has changed
Binary file data/fileio/test/audio/aac/44100-2.m4a has changed
Binary file data/fileio/test/audio/aiff/12000-6-16.aiff has changed
Binary file data/fileio/test/audio/aiff/48000-1-24.aiff has changed
Binary file data/fileio/test/audio/apple_lossless/32000-1.m4a has changed
Binary file data/fileio/test/audio/apple_lossless/44100-2.m4a has changed
Binary file data/fileio/test/audio/flac/44100-2.flac has changed
Binary file data/fileio/test/audio/mp3/32000-1.mp3 has changed
Binary file data/fileio/test/audio/mp3/44100-2.mp3 has changed
Binary file data/fileio/test/audio/ogg/32000-1.ogg has changed
Binary file data/fileio/test/audio/ogg/44100-2.ogg has changed
Binary file data/fileio/test/audio/wav/32000-1-16.wav has changed
Binary file data/fileio/test/audio/wav/44100-1-32.wav has changed
Binary file data/fileio/test/audio/wav/44100-2-16.wav has changed
Binary file data/fileio/test/audio/wav/44100-2-8.wav has changed
Binary file data/fileio/test/audio/wav/48000-1-16.wav has changed
Binary file data/fileio/test/audio/wav/8000-1-8.wav has changed
Binary file data/fileio/test/audio/wav/8000-2-16.wav has changed
Binary file data/fileio/test/audio/wav/8000-6-16.wav has changed
Binary file data/fileio/test/encodings/Tëmple of Spörks.mp3 has changed
Binary file data/fileio/test/encodings/Tëmple of Spörks.ogg has changed
Binary file data/fileio/test/encodings/id3v2-iso-8859-1.mp3 has changed
Binary file data/fileio/test/encodings/id3v2-ucs-2.mp3 has changed
Binary file data/fileio/test/encodings/スポークの寺院.mp3 has changed
Binary file data/fileio/test/encodings/スポークの寺院.ogg has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/files.pri	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,10 @@
+
+TEST_HEADERS += \
+	     AudioFileReaderTest.h \
+	     AudioFileWriterTest.h \
+	     AudioTestData.h \
+             EncodingTest.h \
+             MIDIFileReaderTest.h
+	     
+TEST_SOURCES += \
+	     svcore-data-fileio-test.cpp
--- a/data/fileio/test/main.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-/* -*- 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 file copyright 2013 Chris Cannam.
-    
-    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 "AudioFileReaderTest.h"
-
-#include <QtTest>
-
-#include <iostream>
-
-int main(int argc, char *argv[])
-{
-    int good = 0, bad = 0;
-
-    QCoreApplication app(argc, argv);
-    app.setOrganizationName("Sonic Visualiser");
-    app.setApplicationName("test-fileio");
-
-    {
-	AudioFileReaderTest t;
-	if (QTest::qExec(&t, argc, argv) == 0) ++good;
-	else ++bad;
-    }
-
-    if (bad > 0) {
-	cerr << "\n********* " << bad << " test suite(s) failed!\n" << endl;
-	return 1;
-    } else {
-	cerr << "All tests passed" << endl;
-	return 0;
-    }
-}
-
Binary file data/fileio/test/midi/scale.mid has changed
Binary file data/fileio/test/midi/아브라카다브라.mid has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/svcore-data-fileio-test.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,81 @@
+/* -*- 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 file copyright 2013 Chris Cannam.
+    
+    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 "AudioFileReaderTest.h"
+#include "AudioFileWriterTest.h"
+#include "EncodingTest.h"
+#include "MIDIFileReaderTest.h"
+
+#include <QtTest>
+
+#include <iostream>
+
+int main(int argc, char *argv[])
+{
+    int good = 0, bad = 0;
+
+    QString testDir;
+
+#ifdef Q_OS_WIN
+    // incredible to have to hardcode this, but I just can't figure out how to
+    // get QMAKE_POST_LINK to add an arg to its command successfully on Windows
+    testDir = "../sonic-visualiser/svcore/data/fileio/test";
+#endif
+
+    if (argc > 1) {
+        cerr << "argc = " << argc << endl;
+        testDir = argv[1];
+    }
+
+    if (testDir != "") {
+        cerr << "Setting test directory base path to \"" << testDir << "\"" << endl;
+    }
+
+    QCoreApplication app(argc, argv);
+    app.setOrganizationName("Sonic Visualiser");
+    app.setApplicationName("test-fileio");
+
+    {
+        AudioFileReaderTest t(testDir);
+        if (QTest::qExec(&t, argc, argv) == 0) ++good;
+        else ++bad;
+    }
+
+    {
+        AudioFileWriterTest t(testDir);
+        if (QTest::qExec(&t, argc, argv) == 0) ++good;
+        else ++bad;
+    }
+
+    {
+        EncodingTest t(testDir);
+        if (QTest::qExec(&t, argc, argv) == 0) ++good;
+        else ++bad;
+    }
+
+    {
+        MIDIFileReaderTest t(testDir);
+        if (QTest::qExec(&t, argc, argv) == 0) ++good;
+        else ++bad;
+    }
+
+    if (bad > 0) {
+	cerr << "\n********* " << bad << " test suite(s) failed!\n" << endl;
+	return 1;
+    } else {
+	cerr << "All tests passed" << endl;
+	return 0;
+    }
+}
+
--- a/data/fileio/test/test.pro	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-
-TEMPLATE = app
-
-LIBS += -L../../.. -L../../../../dataquay -L../../../release -L../../../../dataquay/release -lsvcore -ldataquay
-
-win32-g++ {
-    INCLUDEPATH += ../../../../sv-dependency-builds/win32-mingw/include
-    LIBS += -L../../../../sv-dependency-builds/win32-mingw/lib
-}
-win32-msvc* {
-    INCLUDEPATH += ../../../../sv-dependency-builds/win32-msvc/include
-    LIBS += -L../../../../sv-dependency-builds/win32-msvc/lib
-}
-mac* {
-    INCLUDEPATH += ../../../../sv-dependency-builds/osx/include
-    LIBS += -L../../../../sv-dependency-builds/osx/lib
-}
-
-exists(../../../config.pri) {
-    include(../../../config.pri)
-}
-
-!exists(../../../config.pri) {
-
-    CONFIG += release
-    DEFINES += NDEBUG BUILD_RELEASE NO_TIMING
-
-    DEFINES += HAVE_BZ2 HAVE_FFTW3 HAVE_FFTW3F HAVE_SNDFILE HAVE_SAMPLERATE HAVE_VAMP HAVE_VAMPHOSTSDK HAVE_DATAQUAY HAVE_LIBLO HAVE_MAD HAVE_ID3TAG HAVE_PORTAUDIO_2_0
-
-    LIBS += -lbz2 -lfftw3 -lfftw3f -lsndfile -lFLAC -logg -lvorbis -lvorbisenc -lvorbisfile -logg -lmad -lid3tag -lportaudio -lsamplerate -lz -lsord-0 -lserd-0
-
-    win* {
-        LIBS += -llo -lwinmm -lws2_32
-    }
-    macx* {
-        DEFINES += HAVE_COREAUDIO
-        LIBS += -framework CoreAudio -framework CoreMidi -framework AudioUnit -framework AudioToolbox -framework CoreFoundation -framework CoreServices -framework Accelerate
-    }
-    linux* {
-        LIBS += -ldl
-    }
-}
-
-CONFIG += qt thread warn_on stl rtti exceptions console c++11
-QT += network xml testlib
-QT -= gui
-
-TARGET = svcore-data-fileio-test
-
-DEPENDPATH += ../../..
-INCLUDEPATH += ../../..
-OBJECTS_DIR = o
-MOC_DIR = o
-
-HEADERS += AudioFileReaderTest.h \
-           AudioTestData.h
-SOURCES += main.cpp
-
-win* {
-//PRE_TARGETDEPS += ../../../svcore.lib
-}
-!win* {
-PRE_TARGETDEPS += ../../../libsvcore.a
-}
-
-!win32 {
-    !macx* {
-        QMAKE_POST_LINK=./$${TARGET}
-    }
-    macx* {
-        QMAKE_POST_LINK=./$${TARGET}.app/Contents/MacOS/$${TARGET}
-    }
-}
-
-win32:QMAKE_POST_LINK=./release/$${TARGET}.exe
-
Binary file data/fileio/test/testfiles/12000-6-16.aiff has changed
Binary file data/fileio/test/testfiles/32000-1-16.wav has changed
Binary file data/fileio/test/testfiles/32000-1.m4a has changed
Binary file data/fileio/test/testfiles/32000-1.mp3 has changed
Binary file data/fileio/test/testfiles/32000-1.ogg has changed
Binary file data/fileio/test/testfiles/44100-1-32.wav has changed
Binary file data/fileio/test/testfiles/44100-2-16.wav has changed
Binary file data/fileio/test/testfiles/44100-2-8.wav has changed
Binary file data/fileio/test/testfiles/44100-2.flac has changed
Binary file data/fileio/test/testfiles/44100-2.m4a has changed
Binary file data/fileio/test/testfiles/44100-2.mp3 has changed
Binary file data/fileio/test/testfiles/44100-2.ogg has changed
Binary file data/fileio/test/testfiles/48000-1-16.wav has changed
Binary file data/fileio/test/testfiles/48000-1-24.aiff has changed
Binary file data/fileio/test/testfiles/8000-1-8.wav has changed
Binary file data/fileio/test/testfiles/8000-2-16.wav has changed
Binary file data/fileio/test/testfiles/8000-6-16.wav has changed
--- a/data/midi/MIDIInput.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/midi/MIDIInput.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -17,7 +17,7 @@
 
 #include "rtmidi/RtMidi.h"
 
-#include <unistd.h>
+#include "system/System.h"
 
 MIDIInput::MIDIInput(QString name, FrameTimer *timer) :
     m_rtmidi(),
--- a/data/midi/rtmidi/RtMidi.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/midi/rtmidi/RtMidi.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -63,7 +63,7 @@
 #endif
   }
   else {
-    cerr << '\n' << errorString_ << "\n\n";
+    cerr << "\nRtMidi error: " << errorString_ << "\n\n";
     throw RtError( errorString_, type );
   }
 }
--- a/data/model/AggregateWaveModel.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/AggregateWaveModel.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -19,6 +19,8 @@
 
 #include <QTextStream>
 
+using namespace std;
+
 PowerOfSqrtTwoZoomConstraint
 AggregateWaveModel::m_zoomConstraint;
 
@@ -92,65 +94,55 @@
     return m_components.begin()->model->getSampleRate();
 }
 
-sv_frame_t
-AggregateWaveModel::getData(int channel, sv_frame_t start, sv_frame_t count,
-                            float *buffer) const
+floatvec_t
+AggregateWaveModel::getData(int channel, sv_frame_t start, sv_frame_t count) const
 {
     int ch0 = channel, ch1 = channel;
-    bool mixing = false;
     if (channel == -1) {
         ch0 = 0;
         ch1 = getChannelCount()-1;
-        mixing = true;
     }
 
-    float *readbuf = buffer;
-    if (mixing) {
-        readbuf = new float[count];
-        for (sv_frame_t i = 0; i < count; ++i) {
-            buffer[i] = 0.f;
+    floatvec_t result(count, 0.f);
+    sv_frame_t longest = 0;
+    
+    for (int c = ch0; c <= ch1; ++c) {
+
+        auto here = m_components[c].model->getData(m_components[c].channel,
+                                                   start, count);
+        if (sv_frame_t(here.size()) > longest) {
+            longest = sv_frame_t(here.size());
+        }
+        for (sv_frame_t i = 0; in_range_for(here, i); ++i) {
+            result[i] += here[i];
         }
     }
 
-    sv_frame_t longest = 0;
-    
-    for (int c = ch0; c <= ch1; ++c) {
-        sv_frame_t here = 
-            m_components[c].model->getData(m_components[c].channel,
-                                           start, count,
-                                           readbuf);
-        if (here > longest) {
-            longest = here;
-        }
-        if (here < count) {
-            for (sv_frame_t i = here; i < count; ++i) {
-                readbuf[i] = 0.f;
-            }
-        }
-        if (mixing) {
-            for (sv_frame_t i = 0; i < count; ++i) {
-                buffer[i] += readbuf[i];
-            }
-        }
-    }
-
-    if (mixing) delete[] readbuf;
-    return longest;
+    result.resize(longest);
+    return result;
 }
 
-sv_frame_t
+vector<floatvec_t>
 AggregateWaveModel::getMultiChannelData(int fromchannel, int tochannel,
-                                        sv_frame_t start, sv_frame_t count,
-                                        float **buffer) const
+                                        sv_frame_t start, sv_frame_t count) const
 {
     sv_frame_t min = count;
 
+    vector<floatvec_t> result;
+
     for (int c = fromchannel; c <= tochannel; ++c) {
-        sv_frame_t here = getData(c, start, count, buffer[c - fromchannel]);
-        if (here < min) min = here;
+        auto here = getData(c, start, count);
+        if (sv_frame_t(here.size()) < min) {
+            min = sv_frame_t(here.size());
+        }
+        result.push_back(here);
+    }
+
+    if (min < count) {
+        for (auto &v : result) v.resize(min);
     }
     
-    return min;
+    return result;
 }
 
 int
--- a/data/model/AggregateWaveModel.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/AggregateWaveModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -59,12 +59,9 @@
     virtual sv_frame_t getStartFrame() const { return 0; }
     virtual sv_frame_t getEndFrame() const { return getFrameCount(); }
 
-    virtual sv_frame_t getData(int channel, sv_frame_t start, sv_frame_t count,
-                               float *buffer) const;
+    virtual floatvec_t getData(int channel, sv_frame_t start, sv_frame_t count) const;
 
-    virtual sv_frame_t getMultiChannelData(int fromchannel, int tochannel,
-                                           sv_frame_t start, sv_frame_t count,
-                                           float **buffer) const;
+    virtual std::vector<floatvec_t> getMultiChannelData(int fromchannel, int tochannel, sv_frame_t start, sv_frame_t count) const;
 
     virtual int getSummaryBlockSize(int desired) const;
 
--- a/data/model/Dense3DModelPeakCache.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/Dense3DModelPeakCache.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -17,13 +17,13 @@
 
 #include "base/Profiler.h"
 
-Dense3DModelPeakCache::Dense3DModelPeakCache(DenseThreeDimensionalModel *source,
+#include "base/HitCount.h"
+
+Dense3DModelPeakCache::Dense3DModelPeakCache(const DenseThreeDimensionalModel *source,
 					     int columnsPerPeak) :
     m_source(source),
-    m_resolution(columnsPerPeak)
+    m_columnsPerPeak(columnsPerPeak)
 {
-    m_coverage.resize(1); // otherwise it is simply invalid
-
     m_cache = new EditableDenseThreeDimensionalModel
         (source->getSampleRate(),
          getResolution(),
@@ -43,24 +43,9 @@
     delete m_cache;
 }
 
-bool
-Dense3DModelPeakCache::isColumnAvailable(int column) const
-{
-    if (!m_source) return false;
-    if (haveColumn(column)) return true;
-    for (int i = m_resolution; i > 0; ) {
-        --i;
-        if (!m_source->isColumnAvailable(column * m_resolution + i)) {
-            return false;
-        }
-    }
-    return true;
-}
-
 Dense3DModelPeakCache::Column
 Dense3DModelPeakCache::getColumn(int column) const
 {
-    Profiler profiler("Dense3DModelPeakCache::getColumn");
     if (!m_source) return Column();
     if (!haveColumn(column)) fillColumn(column);
     return m_cache->getColumn(column);
@@ -81,9 +66,9 @@
     if (m_coverage.size() > 0) {
         // The last peak may have come from an incomplete read, which
         // may since have been filled, so reset it
-        m_coverage.reset(m_coverage.size()-1);
+        m_coverage[m_coverage.size()-1] = false;
     }
-    m_coverage.resize(getWidth()); // retaining data
+    m_coverage.resize(getWidth(), false); // retaining data
 }
 
 void
@@ -95,7 +80,14 @@
 bool
 Dense3DModelPeakCache::haveColumn(int column) const
 {
-    return column < (int)m_coverage.size() && m_coverage.get(column);
+    static HitCount count("Dense3DModelPeakCache");
+    if (in_range_for(m_coverage, column) && m_coverage[column]) {
+        count.hit();
+        return true;
+    } else {
+        count.miss();
+        return false;
+    }
 }
 
 void
@@ -103,26 +95,43 @@
 {
     Profiler profiler("Dense3DModelPeakCache::fillColumn");
 
-    if (column >= (int)m_coverage.size()) {
-        // see note in sourceModelChanged
-        if (m_coverage.size() > 0) m_coverage.reset(m_coverage.size()-1);
-        m_coverage.resize(column + 1);
+    if (!in_range_for(m_coverage, column)) {
+        if (m_coverage.size() > 0) {
+            // The last peak may have come from an incomplete read, which
+            // may since have been filled, so reset it
+            m_coverage[m_coverage.size()-1] = false;
+        }
+        m_coverage.resize(column + 1, false);
     }
 
+    int sourceWidth = m_source->getWidth();
+    
     Column peak;
-    for (int i = 0; i < int(m_resolution); ++i) {
-        Column here = m_source->getColumn(column * m_resolution + i);
+    int n = 0;
+    for (int i = 0; i < m_columnsPerPeak; ++i) {
+
+        int sourceColumn = column * m_columnsPerPeak + i;
+        if (sourceColumn >= sourceWidth) break;
+        
+        Column here = m_source->getColumn(sourceColumn);
+
+//        cerr << "Dense3DModelPeakCache::fillColumn(" << column << "): source col "
+//             << sourceColumn << " of " << sourceWidth
+//             << " returned " << here.size() << " elts" << endl;
+        
         if (i == 0) {
             peak = here;
+            n = int(peak.size());
         } else {
-            for (int j = 0; j < (int)peak.size() && j < (int)here.size(); ++j) {
-                if (here[j] > peak[j]) peak[j] = here[j];
+            int m = std::min(n, int(here.size()));
+            for (int j = 0; j < m; ++j) {
+                peak[j] = std::max(here[j], peak[j]);
             }
         }
     }
 
     m_cache->setColumn(column, peak);
-    m_coverage.set(column);
+    m_coverage[column] = true;
 }
 
 
--- a/data/model/Dense3DModelPeakCache.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/Dense3DModelPeakCache.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,19 +13,18 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _DENSE_3D_MODEL_PEAK_CACHE_H_
-#define _DENSE_3D_MODEL_PEAK_CACHE_H_
+#ifndef DENSE_3D_MODEL_PEAK_CACHE_H
+#define DENSE_3D_MODEL_PEAK_CACHE_H
 
 #include "DenseThreeDimensionalModel.h"
 #include "EditableDenseThreeDimensionalModel.h"
-#include "base/ResizeableBitset.h"
 
 class Dense3DModelPeakCache : public DenseThreeDimensionalModel
 {
     Q_OBJECT
 
 public:
-    Dense3DModelPeakCache(DenseThreeDimensionalModel *source,
+    Dense3DModelPeakCache(const DenseThreeDimensionalModel *source,
                           int columnsPerPeak);
     ~Dense3DModelPeakCache();
 
@@ -46,11 +45,20 @@
     }
 
     virtual int getResolution() const {
-        return m_source->getResolution() * m_resolution;
+        return m_source->getResolution() * m_columnsPerPeak;
     }
 
+    virtual int getColumnsPerPeak() const {
+        return m_columnsPerPeak;
+    }
+    
     virtual int getWidth() const {
-        return m_source->getWidth() / m_resolution + 1;
+        int sourceWidth = m_source->getWidth();
+        if ((sourceWidth % m_columnsPerPeak) == 0) {
+            return sourceWidth / m_columnsPerPeak;
+        } else {
+            return sourceWidth / m_columnsPerPeak + 1;
+        }
     }
 
     virtual int getHeight() const {
@@ -65,11 +73,15 @@
         return m_source->getMaximumLevel();
     }
 
-    virtual bool isColumnAvailable(int column) const;
+    /**
+     * Retrieve the peaks column at peak-cache column number col. This
+     * will consist of the peak values in the underlying model from
+     * columns (col * getColumnsPerPeak()) to ((col+1) *
+     * getColumnsPerPeak() - 1) inclusive.
+     */
+    virtual Column getColumn(int col) const;
 
-    virtual Column getColumn(int column) const;
-
-    virtual float getValueAt(int column, int n) const;
+    virtual float getValueAt(int col, int n) const;
 
     virtual QString getBinName(int n) const {
         return m_source->getBinName(n);
@@ -90,10 +102,11 @@
     void sourceModelAboutToBeDeleted();
 
 private:
-    DenseThreeDimensionalModel *m_source;
+    const DenseThreeDimensionalModel *m_source;
     mutable EditableDenseThreeDimensionalModel *m_cache;
-    mutable ResizeableBitset m_coverage;
-    int m_resolution;
+    mutable std::vector<bool> m_coverage; // must be bool, for space efficiency
+                                          // (vector of bool uses 1-bit elements)
+    int m_columnsPerPeak;
 
     bool haveColumn(int column) const;
     void fillColumn(int column) const;
--- a/data/model/DenseThreeDimensionalModel.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/DenseThreeDimensionalModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -18,6 +18,7 @@
 
 #include "Model.h"
 #include "TabularModel.h"
+#include "base/ColumnOp.h"
 #include "base/ZoomConstraint.h"
 #include "base/RealTime.h"
 
@@ -55,16 +56,7 @@
      */
     virtual float getMaximumLevel() const = 0;
 
-    /**
-     * Return true if there are data available for the given column.
-     * This should return true only if getColumn(column) would not
-     * have to do any substantial work to calculate its return values.
-     * If this function returns false, it may still be possible to
-     * retrieve the column, but its values may have to be calculated.
-     */
-    virtual bool isColumnAvailable(int column) const = 0;
-
-    typedef QVector<float> Column;
+    typedef ColumnOp::Column Column;
 
     /**
      * Get data from the given column of bin values.
--- a/data/model/DenseTimeValueModel.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/DenseTimeValueModel.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -37,27 +37,19 @@
 
     if (f1 <= f0) return "";
 
-    float **all = new float *[ch];
-    for (int c = 0; c < ch; ++c) {
-        all[c] = new float[f1 - f0];
-    }
+    auto data = getMultiChannelData(0, ch - 1, f0, f1 - f0);
 
-    sv_frame_t n = getMultiChannelData(0, ch - 1, f0, f1 - f0, all);
-
+    if (data.empty() || data[0].empty()) return "";
+    
     QStringList list;
-    for (sv_frame_t i = 0; i < n; ++i) {
+    for (sv_frame_t i = 0; in_range_for(data[0], i); ++i) {
         QStringList parts;
         parts << QString("%1").arg(f0 + i);
-        for (int c = 0; c < ch; ++c) {
-            parts << QString("%1").arg(all[c][i]);
+        for (int c = 0; in_range_for(data, c); ++c) {
+            parts << QString("%1").arg(data[c][i]);
         }
         list << parts.join(delimiter);
     }
 
-    for (int c = 0; c < ch; ++c) {
-        delete[] all[c];
-    }
-    delete[] all;
-
     return list.join("\n");
 }
--- a/data/model/DenseTimeValueModel.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/DenseTimeValueModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _DENSE_TIME_VALUE_MODEL_H_
-#define _DENSE_TIME_VALUE_MODEL_H_
+#ifndef SV_DENSE_TIME_VALUE_MODEL_H
+#define SV_DENSE_TIME_VALUE_MODEL_H
 
 #include <QObject>
 
@@ -57,27 +57,34 @@
 
     /**
      * Get the specified set of samples from the given channel of the
-     * model in single-precision floating-point format.  Return the
-     * number of samples actually retrieved.
+     * model in single-precision floating-point format. Returned
+     * vector may have fewer samples than requested, if the end of
+     * file was reached.
+     *
      * If the channel is given as -1, mix all available channels and
      * return the result.
      */
-    virtual sv_frame_t getData(int channel, sv_frame_t start, sv_frame_t count,
-                               float *buffer) const = 0;
+    virtual floatvec_t getData(int channel, sv_frame_t start, sv_frame_t count)
+        const = 0;
 
     /**
-     * Get the specified set of samples from given contiguous range
-     * of channels of the model in single-precision floating-point
-     * format.  Return the number of sample frames actually retrieved.
+     * Get the specified set of samples from given contiguous range of
+     * channels of the model in single-precision floating-point
+     * format. Returned vector may have fewer samples than requested,
+     * if the end of file was reached.
      */
-    virtual sv_frame_t getMultiChannelData(int fromchannel, int tochannel,
-                                           sv_frame_t start, sv_frame_t count,
-                                           float **buffers) const = 0;
+    virtual std::vector<floatvec_t> getMultiChannelData(int fromchannel,
+                                                        int tochannel,
+                                                        sv_frame_t start,
+                                                        sv_frame_t count)
+        const = 0;
 
     virtual bool canPlay() const { return true; }
     virtual QString getDefaultPlayClipId() const { return ""; }
 
-    virtual QString toDelimitedDataStringSubset(QString delimiter, sv_frame_t f0, sv_frame_t f1) const;
+    virtual QString toDelimitedDataStringSubset(QString delimiter,
+                                                sv_frame_t f0, sv_frame_t f1)
+        const;
 
     QString getTypeName() const { return tr("Dense Time-Value"); }
 };
--- a/data/model/EditableDenseThreeDimensionalModel.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/EditableDenseThreeDimensionalModel.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -96,7 +96,7 @@
 int
 EditableDenseThreeDimensionalModel::getWidth() const
 {
-    return m_data.size();
+    return int(m_data.size());
 }
 
 int
@@ -139,15 +139,15 @@
 EditableDenseThreeDimensionalModel::getColumn(int index) const
 {
     QReadLocker locker(&m_lock);
-    if (index < 0 || index >= m_data.size()) return Column();
-    return expandAndRetrieve(index);
+    if (in_range_for(m_data, index)) return expandAndRetrieve(index);
+    else return Column();
 }
 
 float
 EditableDenseThreeDimensionalModel::getValueAt(int index, int n) const
 {
     Column c = getColumn(index);
-    if (int(n) < c.size()) return c.at(n);
+    if (in_range_for(c, n)) return c.at(n);
     return m_minimum;
 }
 
@@ -157,7 +157,7 @@
 EditableDenseThreeDimensionalModel::truncateAndStore(int index,
                                                      const Column &values)
 {
-    assert(int(index) < m_data.size());
+    assert(in_range_for(m_data, index));
 
     //cout << "truncateAndStore(" << index << ", " << values.size() << ")" << endl;
 
@@ -169,7 +169,7 @@
     m_trunc[index] = 0;
     if (index == 0 ||
         m_compression == NoCompression ||
-        values.size() != int(m_yBinCount)) {
+        int(values.size()) != m_yBinCount) {
 //        given += values.size();
 //        stored += values.size();
         m_data[index] = values;
@@ -206,7 +206,7 @@
     Column p = expandAndRetrieve(index - tdist);
     int h = m_yBinCount;
 
-    if (p.size() == h && tdist <= maxdist) {
+    if (int(p.size()) == h && tdist <= maxdist) {
 
         int bcount = 0, tcount = 0;
         if (!known || !top) {
@@ -265,6 +265,17 @@
 }
 
 EditableDenseThreeDimensionalModel::Column
+EditableDenseThreeDimensionalModel::rightHeight(const Column &c) const
+{
+    if (int(c.size()) == m_yBinCount) return c;
+    else {
+        Column cc(c);
+        cc.resize(m_yBinCount, 0.0);
+        return cc;
+    }
+}
+
+EditableDenseThreeDimensionalModel::Column
 EditableDenseThreeDimensionalModel::expandAndRetrieve(int index) const
 {
     // See comment above m_trunc declaration in header
@@ -272,17 +283,17 @@
     assert(index >= 0 && index < int(m_data.size()));
     Column c = m_data.at(index);
     if (index == 0) {
-        return c;
+        return rightHeight(c);
     }
     int trunc = (int)m_trunc[index];
     if (trunc == 0) {
-        return c;
+        return rightHeight(c);
     }
     bool top = true;
     int tdist = trunc;
     if (trunc < 0) { top = false; tdist = -trunc; }
     Column p = expandAndRetrieve(index - tdist);
-    int psize = p.size(), csize = c.size();
+    int psize = int(p.size()), csize = int(c.size());
     if (psize != m_yBinCount) {
         cerr << "WARNING: EditableDenseThreeDimensionalModel::expandAndRetrieve: Trying to expand from incorrectly sized column" << endl;
     }
@@ -291,10 +302,6 @@
             c.push_back(p.at(i));
         }
     } else {
-        // push_front is very slow on QVector -- but not enough to
-        // make it desirable to choose a different container, since
-        // QVector has all the other advantages for us.  easier to
-        // write the whole array out to a new vector
         Column cc(psize);
         for (int i = 0; i < psize - csize; ++i) {
             cc[i] = p.at(i);
@@ -313,16 +320,14 @@
 {
     QWriteLocker locker(&m_lock);
 
-    while (int(index) >= m_data.size()) {
+    while (index >= int(m_data.size())) {
 	m_data.push_back(Column());
         m_trunc.push_back(0);
     }
 
     bool allChange = false;
 
-//    if (values.size() > m_yBinCount) m_yBinCount = values.size();
-
-    for (int i = 0; i < values.size(); ++i) {
+    for (int i = 0; in_range_for(values, i); ++i) {
         float value = values[i];
         if (ISNAN(value) || ISINF(value)) {
             continue;
@@ -432,13 +437,13 @@
     
     for (int i = 0; i < 10; ++i) {
         int index = i * 10;
-        if (index < m_data.size()) {
+        if (in_range_for(m_data, index)) {
             const Column &c = m_data.at(index);
-            while (c.size() > int(sample.size())) {
+            while (c.size() > sample.size()) {
                 sample.push_back(0.0);
                 n.push_back(0);
             }
-            for (int j = 0; j < c.size(); ++j) {
+            for (int j = 0; in_range_for(c, j); ++j) {
                 sample[j] += c.at(j);
                 ++n[j];
             }
@@ -486,9 +491,9 @@
 {
     QReadLocker locker(&m_lock);
     QString s;
-    for (int i = 0; i < m_data.size(); ++i) {
+    for (int i = 0; in_range_for(m_data, i); ++i) {
         QStringList list;
-	for (int j = 0; j < m_data.at(i).size(); ++j) {
+	for (int j = 0; in_range_for(m_data.at(i), j); ++j) {
             list << QString("%1").arg(m_data.at(i).at(j));
         }
         s += list.join(delimiter) + "\n";
@@ -501,11 +506,11 @@
 {
     QReadLocker locker(&m_lock);
     QString s;
-    for (int i = 0; i < m_data.size(); ++i) {
+    for (int i = 0; in_range_for(m_data, i); ++i) {
         sv_frame_t fr = m_startFrame + i * m_resolution;
         if (fr >= f0 && fr < f1) {
             QStringList list;
-            for (int j = 0; j < m_data.at(i).size(); ++j) {
+            for (int j = 0; in_range_for(m_data.at(i), j); ++j) {
                 list << QString("%1").arg(m_data.at(i).at(j));
             }
             s += list.join(delimiter) + "\n";
--- a/data/model/EditableDenseThreeDimensionalModel.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/EditableDenseThreeDimensionalModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -44,7 +44,7 @@
 
     EditableDenseThreeDimensionalModel(sv_samplerate_t sampleRate,
 				       int resolution,
-				       int yBinCount,
+				       int height,
                                        CompressionType compression,
 				       bool notifyOnAdd = true);
 
@@ -75,12 +75,19 @@
     virtual int getWidth() const;
 
     /**
-     * Return the number of bins in each set of bins.
+     * Return the number of bins in each column.
      */
     virtual int getHeight() const; 
 
     /**
-     * Set the number of bins in each set of bins.
+     * Set the number of bins in each column.
+     *
+     * You can set (via setColumn) a vector of any length as a column,
+     * but any column being retrieved will be resized to this height
+     * (or the height that was supplied to the constructor, if this is
+     * never called) on retrieval. That is, the model owner determines
+     * the height of the model at a single stroke; the columns
+     * themselves don't have any effect on the height of the model.
      */
     virtual void setHeight(int sz);
 
@@ -105,11 +112,6 @@
     virtual void setMaximumLevel(float sz);
 
     /**
-     * Return true if there are data available for the given column.
-     */
-    virtual bool isColumnAvailable(int x) const { return x < getWidth(); }
-
-    /**
      * Get the set of bin values at the given column.
      */
     virtual Column getColumn(int x) const;
@@ -194,7 +196,7 @@
                        QString extraAttributes = "") const;
 
 protected:
-    typedef QVector<Column> ValueMatrix;
+    typedef std::vector<Column> ValueMatrix;
     ValueMatrix m_data;
 
     // m_trunc is used for simple compression.  If at least the top N
@@ -208,6 +210,7 @@
     std::vector<signed char> m_trunc;
     void truncateAndStore(int index, const Column & values);
     Column expandAndRetrieve(int index) const;
+    Column rightHeight(const Column &c) const;
 
     std::vector<QString> m_binNames;
     std::vector<float> m_binValues;
--- a/data/model/FFTModel.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/FFTModel.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -15,184 +15,75 @@
 
 #include "FFTModel.h"
 #include "DenseTimeValueModel.h"
-#include "AggregateWaveModel.h"
 
 #include "base/Profiler.h"
 #include "base/Pitch.h"
+#include "base/HitCount.h"
 
 #include <algorithm>
 
 #include <cassert>
+#include <deque>
 
-#ifndef __GNUC__
-#include <alloca.h>
-#endif
+using namespace std;
+
+static HitCount inSmallCache("FFTModel: Small FFT cache");
+static HitCount inSourceCache("FFTModel: Source data cache");
 
 FFTModel::FFTModel(const DenseTimeValueModel *model,
                    int channel,
                    WindowType windowType,
                    int windowSize,
                    int windowIncrement,
-                   int fftSize,
-                   bool polar,
-                   StorageAdviser::Criteria criteria,
-                   sv_frame_t fillFromFrame) :
-    //!!! ZoomConstraint!
-    m_server(0),
-    m_xshift(0),
-    m_yshift(0)
+                   int fftSize) :
+    m_model(model),
+    m_channel(channel),
+    m_windowType(windowType),
+    m_windowSize(windowSize),
+    m_windowIncrement(windowIncrement),
+    m_fftSize(fftSize),
+    m_windower(windowType, windowSize),
+    m_fft(fftSize),
+    m_cacheSize(3)
 {
-    setSourceModel(const_cast<DenseTimeValueModel *>(model)); //!!! hmm.
-
-    m_server = getServer(model,
-                         channel,
-                         windowType,
-                         windowSize,
-                         windowIncrement,
-                         fftSize,
-                         polar,
-                         criteria,
-                         fillFromFrame);
-
-    if (!m_server) return; // caller should check isOK()
-
-    int xratio = windowIncrement / m_server->getWindowIncrement();
-    int yratio = m_server->getFFTSize() / fftSize;
-
-    while (xratio > 1) {
-        if (xratio & 0x1) {
-            cerr << "ERROR: FFTModel: Window increment ratio "
-                      << windowIncrement << " / "
-                      << m_server->getWindowIncrement()
-                      << " must be a power of two" << endl;
-            assert(!(xratio & 0x1));
-        }
-        ++m_xshift;
-        xratio >>= 1;
+    if (m_windowSize > m_fftSize) {
+        cerr << "ERROR: FFTModel::FFTModel: window size (" << m_windowSize
+             << ") must be at least FFT size (" << m_fftSize << ")" << endl;
+        throw invalid_argument("FFTModel window size must be at least FFT size");
     }
 
-    while (yratio > 1) {
-        if (yratio & 0x1) {
-            cerr << "ERROR: FFTModel: FFT size ratio "
-                      << m_server->getFFTSize() << " / " << fftSize
-                      << " must be a power of two" << endl;
-            assert(!(yratio & 0x1));
-        }
-        ++m_yshift;
-        yratio >>= 1;
-    }
+    m_fft.initFloat();
+
+    connect(model, SIGNAL(modelChanged()), this, SIGNAL(modelChanged()));
+    connect(model, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
+            this, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)));
 }
 
 FFTModel::~FFTModel()
 {
-    if (m_server) FFTDataServer::releaseInstance(m_server);
 }
 
 void
 FFTModel::sourceModelAboutToBeDeleted()
 {
-    if (m_sourceModel) {
-        cerr << "FFTModel[" << this << "]::sourceModelAboutToBeDeleted(" << m_sourceModel << ")" << endl;
-        if (m_server) {
-            FFTDataServer::releaseInstance(m_server);
-            m_server = 0;
-        }
-        FFTDataServer::modelAboutToBeDeleted(m_sourceModel);
+    if (m_model) {
+        cerr << "FFTModel[" << this << "]::sourceModelAboutToBeDeleted(" << m_model << ")" << endl;
+        m_model = 0;
     }
 }
 
-FFTDataServer *
-FFTModel::getServer(const DenseTimeValueModel *model,
-                    int channel,
-                    WindowType windowType,
-                    int windowSize,
-                    int windowIncrement,
-                    int fftSize,
-                    bool polar,
-                    StorageAdviser::Criteria criteria,
-                    sv_frame_t fillFromFrame)
+int
+FFTModel::getWidth() const
 {
-    // Obviously, an FFT model of channel C (where C != -1) of an
-    // aggregate model is the same as the FFT model of the appropriate
-    // channel of whichever model that aggregate channel is drawn
-    // from.  We should use that model here, in case we already have
-    // the data for it or will be wanting the same data again later.
-
-    // If the channel is -1 (i.e. mixture of all channels), then we
-    // can't do this shortcut unless the aggregate model only has one
-    // channel or contains exactly all of the channels of a single
-    // other model.  That isn't very likely -- if it were the case,
-    // why would we be using an aggregate model?
-
-    if (channel >= 0) {
-
-        const AggregateWaveModel *aggregate =
-            dynamic_cast<const AggregateWaveModel *>(model);
-
-        if (aggregate && channel < aggregate->getComponentCount()) {
-
-            AggregateWaveModel::ModelChannelSpec spec =
-                aggregate->getComponent(channel);
-
-            return getServer(spec.model,
-                             spec.channel,
-                             windowType,
-                             windowSize,
-                             windowIncrement,
-                             fftSize,
-                             polar,
-                             criteria,
-                             fillFromFrame);
-        }
-    }
-
-    // The normal case
-
-    return FFTDataServer::getFuzzyInstance(model,
-                                           channel,
-                                           windowType,
-                                           windowSize,
-                                           windowIncrement,
-                                           fftSize,
-                                           polar,
-                                           criteria,
-                                           fillFromFrame);
+    if (!m_model) return 0;
+    return int((m_model->getEndFrame() - m_model->getStartFrame())
+               / m_windowIncrement) + 1;
 }
 
-sv_samplerate_t
-FFTModel::getSampleRate() const
+int
+FFTModel::getHeight() const
 {
-    return isOK() ? m_server->getModel()->getSampleRate() : 0;
-}
-
-FFTModel::Column
-FFTModel::getColumn(int x) const
-{
-    Profiler profiler("FFTModel::getColumn", false);
-
-    Column result;
-
-    result.clear();
-    int h = getHeight();
-    result.reserve(h);
-
-#ifdef __GNUC__
-    float magnitudes[h];
-#else
-    float *magnitudes = (float *)alloca(h * sizeof(float));
-#endif
-
-    if (m_server->getMagnitudesAt(x << m_xshift, magnitudes)) {
-
-        for (int y = 0; y < h; ++y) {
-            result.push_back(magnitudes[y]);
-        }
-
-    } else {
-        for (int i = 0; i < h; ++i) result.push_back(0.f);
-    }
-
-    return result;
+    return m_fftSize / 2 + 1;
 }
 
 QString
@@ -204,15 +95,250 @@
     return name;
 }
 
+FFTModel::Column
+FFTModel::getColumn(int x) const
+{
+    auto cplx = getFFTColumn(x);
+    Column col;
+    col.reserve(cplx.size());
+    for (auto c: cplx) col.push_back(abs(c));
+    return col;
+}
+
+FFTModel::Column
+FFTModel::getPhases(int x) const
+{
+    auto cplx = getFFTColumn(x);
+    Column col;
+    col.reserve(cplx.size());
+    for (auto c: cplx) {
+        col.push_back(arg(c));
+    }
+    return col;
+}
+
+float
+FFTModel::getMagnitudeAt(int x, int y) const
+{
+    if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) return 0.f;
+    auto col = getFFTColumn(x);
+    return abs(col[y]);
+}
+
+float
+FFTModel::getMaximumMagnitudeAt(int x) const
+{
+    Column col(getColumn(x));
+    float max = 0.f;
+    int n = int(col.size());
+    for (int i = 0; i < n; ++i) {
+        if (col[i] > max) max = col[i];
+    }
+    return max;
+}
+
+float
+FFTModel::getPhaseAt(int x, int y) const
+{
+    if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) return 0.f;
+    return arg(getFFTColumn(x)[y]);
+}
+
+void
+FFTModel::getValuesAt(int x, int y, float &re, float &im) const
+{
+    auto col = getFFTColumn(x);
+    re = col[y].real();
+    im = col[y].imag();
+}
+
+bool
+FFTModel::getMagnitudesAt(int x, float *values, int minbin, int count) const
+{
+    if (count == 0) count = getHeight();
+    auto col = getFFTColumn(x);
+    for (int i = 0; i < count; ++i) {
+        values[i] = abs(col[minbin + i]);
+    }
+    return true;
+}
+
+bool
+FFTModel::getPhasesAt(int x, float *values, int minbin, int count) const
+{
+    if (count == 0) count = getHeight();
+    auto col = getFFTColumn(x);
+    for (int i = 0; i < count; ++i) {
+        values[i] = arg(col[minbin + i]);
+    }
+    return true;
+}
+
+bool
+FFTModel::getValuesAt(int x, float *reals, float *imags, int minbin, int count) const
+{
+    if (count == 0) count = getHeight();
+    auto col = getFFTColumn(x);
+    for (int i = 0; i < count; ++i) {
+        reals[i] = col[minbin + i].real();
+    }
+    for (int i = 0; i < count; ++i) {
+        imags[i] = col[minbin + i].imag();
+    }
+    return true;
+}
+
+FFTModel::fvec
+FFTModel::getSourceSamples(int column) const
+{
+    // m_fftSize may be greater than m_windowSize, but not the reverse
+
+//    cerr << "getSourceSamples(" << column << ")" << endl;
+    
+    auto range = getSourceSampleRange(column);
+    auto data = getSourceData(range);
+
+    int off = (m_fftSize - m_windowSize) / 2;
+
+    if (off == 0) {
+        return data;
+    } else {
+        vector<float> pad(off, 0.f);
+        fvec padded;
+        padded.reserve(m_fftSize);
+        padded.insert(padded.end(), pad.begin(), pad.end());
+        padded.insert(padded.end(), data.begin(), data.end());
+        padded.insert(padded.end(), pad.begin(), pad.end());
+        return padded;
+    }
+}
+
+FFTModel::fvec
+FFTModel::getSourceData(pair<sv_frame_t, sv_frame_t> range) const
+{
+//    cerr << "getSourceData(" << range.first << "," << range.second
+//         << "): saved range is (" << m_savedData.range.first
+//         << "," << m_savedData.range.second << ")" << endl;
+
+    if (m_savedData.range == range) {
+        inSourceCache.hit();
+        return m_savedData.data;
+    }
+
+    Profiler profiler("FFTModel::getSourceData (cache miss)");
+    
+    if (range.first < m_savedData.range.second &&
+        range.first >= m_savedData.range.first &&
+        range.second > m_savedData.range.second) {
+
+        inSourceCache.partial();
+        
+        sv_frame_t discard = range.first - m_savedData.range.first;
+
+        fvec acc(m_savedData.data.begin() + discard, m_savedData.data.end());
+
+        fvec rest = getSourceDataUncached({ m_savedData.range.second, range.second });
+
+        acc.insert(acc.end(), rest.begin(), rest.end());
+        
+        m_savedData = { range, acc };
+        return acc;
+
+    } else {
+
+        inSourceCache.miss();
+        
+        auto data = getSourceDataUncached(range);
+        m_savedData = { range, data };
+        return data;
+    }
+}
+
+FFTModel::fvec
+FFTModel::getSourceDataUncached(pair<sv_frame_t, sv_frame_t> range) const
+{
+    decltype(range.first) pfx = 0;
+    if (range.first < 0) {
+        pfx = -range.first;
+        range = { 0, range.second };
+    }
+
+    auto data = m_model->getData(m_channel,
+                                 range.first,
+                                 range.second - range.first);
+
+    if (data.empty()) {
+        SVDEBUG << "NOTE: empty source data for range (" << range.first << ","
+                << range.second << ") (model end frame "
+                << m_model->getEndFrame() << ")" << endl;
+    }
+    
+    // don't return a partial frame
+    data.resize(range.second - range.first, 0.f);
+
+    if (pfx > 0) {
+        vector<float> pad(pfx, 0.f);
+        data.insert(data.begin(), pad.begin(), pad.end());
+    }
+    
+    if (m_channel == -1) {
+	int channels = m_model->getChannelCount();
+	if (channels > 1) {
+            int n = int(data.size());
+            float factor = 1.f / float(channels);
+            // use mean instead of sum for fft model input
+	    for (int i = 0; i < n; ++i) {
+		data[i] *= factor;
+	    }
+	}
+    }
+    
+    return data;
+}
+
+FFTModel::cvec
+FFTModel::getFFTColumn(int n) const
+{
+    // The small cache (i.e. the m_cached deque) is for cases where
+    // values are looked up individually, and for e.g. peak-frequency
+    // spectrograms where values from two consecutive columns are
+    // needed at once. This cache gets essentially no hits when
+    // scrolling through a magnitude spectrogram, but 95%+ hits with a
+    // peak-frequency spectrogram.
+    for (const auto &incache : m_cached) {
+        if (incache.n == n) {
+            inSmallCache.hit();
+            return incache.col;
+        }
+    }
+    inSmallCache.miss();
+
+    Profiler profiler("FFTModel::getFFTColumn (cache miss)");
+    
+    auto samples = getSourceSamples(n);
+    m_windower.cut(samples.data());
+    breakfastquay::v_fftshift(samples.data(), m_fftSize);
+
+    cvec col(m_fftSize/2 + 1);
+    
+    m_fft.forwardInterleaved(samples.data(),
+                             reinterpret_cast<float *>(col.data()));
+
+    SavedColumn sc { n, col };
+    if (m_cached.size() >= m_cacheSize) {
+        m_cached.pop_front();
+    }
+    m_cached.push_back(sc);
+
+    return col;
+}
+
 bool
 FFTModel::estimateStableFrequency(int x, int y, double &frequency)
 {
     if (!isOK()) return false;
 
-    sv_samplerate_t sampleRate = m_server->getModel()->getSampleRate();
-
-    int fftSize = m_server->getFFTSize() >> m_yshift;
-    frequency = double(y * sampleRate) / fftSize;
+    frequency = double(y * getSampleRate()) / m_fftSize;
 
     if (x+1 >= getWidth()) return false;
 
@@ -230,24 +356,22 @@
 
     int incr = getResolution();
 
-    double expectedPhase = oldPhase + (2.0 * M_PI * y * incr) / fftSize;
+    double expectedPhase = oldPhase + (2.0 * M_PI * y * incr) / m_fftSize;
 
     double phaseError = princarg(newPhase - expectedPhase);
 
-//    bool stable = (fabsf(phaseError) < (1.1f * (m_windowIncrement * M_PI) / m_fftSize));
-
     // The new frequency estimate based on the phase error resulting
     // from assuming the "native" frequency of this bin
 
     frequency =
-        (sampleRate * (expectedPhase + phaseError - oldPhase)) /
+        (getSampleRate() * (expectedPhase + phaseError - oldPhase)) /
         (2.0 * M_PI * incr);
 
     return true;
 }
 
 FFTModel::PeakLocationSet
-FFTModel::getPeaks(PeakPickType type, int x, int ymin, int ymax)
+FFTModel::getPeaks(PeakPickType type, int x, int ymin, int ymax) const
 {
     Profiler profiler("FFTModel::getPeaks");
 
@@ -264,11 +388,7 @@
         int maxbin = ymax;
         if (maxbin < getHeight() - 1) maxbin = maxbin + 1;
         const int n = maxbin - minbin + 1;
-#ifdef __GNUC__
-        float values[n];
-#else
-        float *values = (float *)alloca(n * sizeof(float));
-#endif
+        float *values = new float[n];
         getMagnitudesAt(x, values, minbin, maxbin - minbin + 1);
         for (int bin = ymin; bin <= ymax; ++bin) {
             if (bin == minbin || bin == maxbin) continue;
@@ -277,14 +397,16 @@
                 peaks.insert(bin);
             }
         }
+        delete[] values;
         return peaks;
     }
 
     Column values = getColumn(x);
+    int nv = int(values.size());
 
     float mean = 0.f;
-    for (int i = 0; i < values.size(); ++i) mean += values[i];
-    if (values.size() > 0) mean = mean / float(values.size());
+    for (int i = 0; i < nv; ++i) mean += values[i];
+    if (nv > 0) mean = mean / float(values.size());
     
     // For peak picking we use a moving median window, picking the
     // highest value within each continuous region of values that
@@ -293,8 +415,8 @@
 
     sv_samplerate_t sampleRate = getSampleRate();
 
-    std::deque<float> window;
-    std::vector<int> inrange;
+    deque<float> window;
+    vector<int> inrange;
     float dist = 0.5;
 
     int medianWinSize = getPeakPickWindowSize(type, sampleRate, ymin, dist);
@@ -305,8 +427,8 @@
     else binmin = 0;
 
     int binmax;
-    if (ymax + halfWin < values.size()) binmax = ymax + halfWin;
-    else binmax = values.size()-1;
+    if (ymax + halfWin < nv) binmax = ymax + halfWin;
+    else binmax = nv - 1;
 
     int prevcentre = 0;
 
@@ -327,12 +449,12 @@
         int actualSize = int(window.size());
 
         if (type == MajorPitchAdaptivePeaks) {
-            if (ymax + halfWin < values.size()) binmax = ymax + halfWin;
-            else binmax = values.size()-1;
+            if (ymax + halfWin < nv) binmax = ymax + halfWin;
+            else binmax = nv - 1;
         }
 
-        std::deque<float> sorted(window);
-        std::sort(sorted.begin(), sorted.end());
+        deque<float> sorted(window);
+        sort(sorted.begin(), sorted.end());
         float median = sorted[int(float(sorted.size()) * dist)];
 
         int centrebin = 0;
@@ -348,7 +470,7 @@
                 inrange.push_back(centrebin);
             }
 
-            if (centre <= median || centrebin+1 == values.size()) {
+            if (centre <= median || centrebin+1 == nv) {
                 if (!inrange.empty()) {
                     int peakbin = 0;
                     float peakval = 0.f;
@@ -380,11 +502,10 @@
     if (type == MajorPeaks) return 10;
     if (bin == 0) return 3;
 
-    int fftSize = m_server->getFFTSize() >> m_yshift;
-    double binfreq = (sampleRate * bin) / fftSize;
+    double binfreq = (sampleRate * bin) / m_fftSize;
     double hifreq = Pitch::getFrequencyForPitch(73, 0, binfreq);
 
-    int hibin = int(lrint((hifreq * fftSize) / sampleRate));
+    int hibin = int(lrint((hifreq * m_fftSize) / sampleRate));
     int medianWinSize = hibin - bin;
     if (medianWinSize < 3) medianWinSize = 3;
 
@@ -395,7 +516,7 @@
 
 FFTModel::PeakSet
 FFTModel::getPeakFrequencies(PeakPickType type, int x,
-                             int ymin, int ymax)
+                             int ymin, int ymax) const
 {
     Profiler profiler("FFTModel::getPeakFrequencies");
 
@@ -404,7 +525,6 @@
     PeakLocationSet locations = getPeaks(type, x, ymin, ymax);
 
     sv_samplerate_t sampleRate = getSampleRate();
-    int fftSize = m_server->getFFTSize() >> m_yshift;
     int incr = getResolution();
 
     // This duplicates some of the work of estimateStableFrequency to
@@ -412,7 +532,7 @@
     // columns, instead of jumping back and forth between columns x and
     // x+1, which may be significantly slower if re-seeking is needed
 
-    std::vector<float> phases;
+    vector<float> phases;
     for (PeakLocationSet::iterator i = locations.begin();
          i != locations.end(); ++i) {
         phases.push_back(getPhaseAt(x, *i));
@@ -423,13 +543,11 @@
          i != locations.end(); ++i) {
         double oldPhase = phases[phaseIndex];
         double newPhase = getPhaseAt(x+1, *i);
-        double expectedPhase = oldPhase + (2.0 * M_PI * *i * incr) / fftSize;
+        double expectedPhase = oldPhase + (2.0 * M_PI * *i * incr) / m_fftSize;
         double phaseError = princarg(newPhase - expectedPhase);
         double frequency =
             (sampleRate * (expectedPhase + phaseError - oldPhase))
             / (2 * M_PI * incr);
-//        bool stable = (fabsf(phaseError) < (1.1f * (incr * M_PI) / fftSize));
-//        if (stable)
         peaks[*i] = frequency;
         ++phaseIndex;
     }
@@ -437,12 +555,3 @@
     return peaks;
 }
 
-FFTModel::FFTModel(const FFTModel &model) :
-    DenseThreeDimensionalModel(),
-    m_server(model.m_server),
-    m_xshift(model.m_xshift),
-    m_yshift(model.m_yshift)
-{
-    FFTDataServer::claimInstance(m_server);
-}
-
--- a/data/model/FFTModel.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/FFTModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -16,24 +16,31 @@
 #ifndef FFT_MODEL_H
 #define FFT_MODEL_H
 
-#include "data/fft/FFTDataServer.h"
 #include "DenseThreeDimensionalModel.h"
+#include "DenseTimeValueModel.h"
+
+#include "base/Window.h"
+
+#include <bqfft/FFT.h>
+#include <bqvec/Allocators.h>
 
 #include <set>
-#include <map>
+#include <vector>
+#include <complex>
+#include <deque>
 
 /**
  * An implementation of DenseThreeDimensionalModel that makes FFT data
  * derived from a DenseTimeValueModel available as a generic data
- * grid.  The FFT data is acquired using FFTDataServer.  Note that any
- * of the accessor functions may throw AllocationFailed if a cache
- * resize fails.
+ * grid.
  */
-
 class FFTModel : public DenseThreeDimensionalModel
 {
     Q_OBJECT
 
+    //!!! threading requirements?
+    //!!! doubles? since we're not caching much
+
 public:
     /**
      * Construct an FFT model derived from the given
@@ -43,111 +50,63 @@
      * If the model has multiple channels use only the given channel,
      * unless the channel is -1 in which case merge all available
      * channels.
-     * 
-     * If polar is true, the data will normally be retrieved from the
-     * FFT model in magnitude/phase form; otherwise it will normally
-     * be retrieved in "cartesian" real/imaginary form.  The results
-     * should be the same either way, but a "polar" model addressed in
-     * "cartesian" form or vice versa may suffer a performance
-     * penalty.
-     *
-     * The fillFromColumn argument gives a hint that the FFT data
-     * server should aim to start calculating FFT data at that column
-     * number if possible, as that is likely to be requested first.
      */
     FFTModel(const DenseTimeValueModel *model,
              int channel,
              WindowType windowType,
              int windowSize,
              int windowIncrement,
-             int fftSize,
-             bool polar,
-             StorageAdviser::Criteria criteria = StorageAdviser::NoCriteria,
-             sv_frame_t fillFromFrame = 0);
+             int fftSize);
     ~FFTModel();
 
-    inline float getMagnitudeAt(int x, int y) {
-        return m_server->getMagnitudeAt(x << m_xshift, y << m_yshift);
-    }
-    inline float getNormalizedMagnitudeAt(int x, int y) {
-        return m_server->getNormalizedMagnitudeAt(x << m_xshift, y << m_yshift);
-    }
-    inline float getMaximumMagnitudeAt(int x) {
-        return m_server->getMaximumMagnitudeAt(x << m_xshift);
-    }
-    inline float getPhaseAt(int x, int y) {
-        return m_server->getPhaseAt(x << m_xshift, y << m_yshift);
-    }
-    inline void getValuesAt(int x, int y, float &real, float &imaginary) {
-        m_server->getValuesAt(x << m_xshift, y << m_yshift, real, imaginary);
-    }
-    inline bool isColumnAvailable(int x) const {
-        return m_server->isColumnReady(x << m_xshift);
-    }
-
-    inline bool getMagnitudesAt(int x, float *values, int minbin = 0, int count = 0) {
-        return m_server->getMagnitudesAt(x << m_xshift, values, minbin << m_yshift, count, getYRatio());
-    }
-    inline bool getNormalizedMagnitudesAt(int x, float *values, int minbin = 0, int count = 0) {
-        return m_server->getNormalizedMagnitudesAt(x << m_xshift, values, minbin << m_yshift, count, getYRatio());
-    }
-    inline bool getPhasesAt(int x, float *values, int minbin = 0, int count = 0) {
-        return m_server->getPhasesAt(x << m_xshift, values, minbin << m_yshift, count, getYRatio());
-    }
-    inline bool getValuesAt(int x, float *reals, float *imaginaries, int minbin = 0, int count = 0) {
-        return m_server->getValuesAt(x << m_xshift, reals, imaginaries, minbin << m_yshift, count, getYRatio());
-    }
-
-    inline sv_frame_t getFillExtent() const { return m_server->getFillExtent(); }
-
     // DenseThreeDimensionalModel and Model methods:
     //
-    inline virtual int getWidth() const {
-        return m_server->getWidth() >> m_xshift;
-    }
-    inline virtual int getHeight() const {
-        // If there is no y-shift, the server's height (based on its
-        // fftsize/2 + 1) is correct.  If there is a shift, then the
-        // server is using a larger fft size than we want, so we shift
-        // it right as many times as necessary, but then we need to
-        // re-add the "+1" part (because ((fftsize*2)/2 + 1) / 2 !=
-        // fftsize/2 + 1).
-        return (m_server->getHeight() >> m_yshift) + (m_yshift > 0 ? 1 : 0);
-    }
-    virtual float getValueAt(int x, int y) const {
-        return const_cast<FFTModel *>(this)->getMagnitudeAt(x, y);
-    }
-    virtual bool isOK() const {
-        // Return true if the model was constructed successfully (not
-        // necessarily whether an error has occurred since
-        // construction, use getError for that)
-        return m_server && m_server->getModel();
-    }
-    virtual sv_frame_t getStartFrame() const {
-        return 0;
-    }
+    virtual int getWidth() const;
+    virtual int getHeight() const;
+    virtual float getValueAt(int x, int y) const { return getMagnitudeAt(x, y); }
+    virtual bool isOK() const { return m_model && m_model->isOK(); }
+    virtual sv_frame_t getStartFrame() const { return 0; }
     virtual sv_frame_t getEndFrame() const {
         return sv_frame_t(getWidth()) * getResolution() + getResolution();
     }
-    virtual sv_samplerate_t getSampleRate() const;
-    virtual int getResolution() const {
-        return m_server->getWindowIncrement() << m_xshift;
+    virtual sv_samplerate_t getSampleRate() const {
+        return isOK() ? m_model->getSampleRate() : 0;
     }
-    virtual int getYBinCount() const {
-        return getHeight();
+    virtual int getResolution() const { return m_windowIncrement; }
+    virtual int getYBinCount() const { return getHeight(); }
+    virtual float getMinimumLevel() const { return 0.f; } // Can't provide
+    virtual float getMaximumLevel() const { return 1.f; } // Can't provide
+    virtual Column getColumn(int x) const; // magnitudes
+    virtual Column getPhases(int x) const;
+    virtual QString getBinName(int n) const;
+    virtual bool shouldUseLogValueScale() const { return true; }
+    virtual int getCompletion() const {
+        int c = 100;
+        if (m_model) {
+            if (m_model->isReady(&c)) return 100;
+        }
+        return c;
     }
-    virtual float getMinimumLevel() const {
-        return 0.f; // Can't provide
-    }
-    virtual float getMaximumLevel() const {
-        return 1.f; // Can't provide
-    }
-    virtual Column getColumn(int x) const;
-    virtual QString getBinName(int n) const;
+    virtual QString getError() const { return ""; } //!!!???
+    virtual sv_frame_t getFillExtent() const { return getEndFrame(); }
 
-    virtual bool shouldUseLogValueScale() const {
-        return true; // Although obviously it's up to the user...
-    }
+    // FFTModel methods:
+    //
+    int getChannel() const { return m_channel; }
+    WindowType getWindowType() const { return m_windowType; }
+    int getWindowSize() const { return m_windowSize; }
+    int getWindowIncrement() const { return m_windowIncrement; }
+    int getFFTSize() const { return m_fftSize; }
+
+//!!! review which of these are ever actually called
+    
+    float getMagnitudeAt(int x, int y) const;
+    float getMaximumMagnitudeAt(int x) const;
+    float getPhaseAt(int x, int y) const;
+    void getValuesAt(int x, int y, float &real, float &imaginary) const;
+    bool getMagnitudesAt(int x, float *values, int minbin = 0, int count = 0) const;
+    bool getPhasesAt(int x, float *values, int minbin = 0, int count = 0) const;
+    bool getValuesAt(int x, float *reals, float *imaginaries, int minbin = 0, int count = 0) const;
 
     /**
      * Calculate an estimated frequency for a stable signal in this
@@ -171,20 +130,13 @@
      * ymax is zero, getHeight()-1 will be used.
      */
     virtual PeakLocationSet getPeaks(PeakPickType type, int x,
-                                     int ymin = 0, int ymax = 0);
+                                     int ymin = 0, int ymax = 0) const;
 
     /**
      * Return locations and estimated stable frequencies of peak bins.
      */
     virtual PeakSet getPeakFrequencies(PeakPickType type, int x,
-                                       int ymin = 0, int ymax = 0);
-
-    virtual int getCompletion() const { return m_server->getFillCompletion(); }
-    virtual QString getError() const { return m_server->getError(); }
-
-    virtual void suspend() { m_server->suspend(); }
-    virtual void suspendWrites() { m_server->suspendWrites(); }
-    virtual void resume() { m_server->resume(); }
+                                       int ymin = 0, int ymax = 0) const;
 
     QString getTypeName() const { return tr("FFT"); }
 
@@ -195,23 +147,48 @@
     FFTModel(const FFTModel &); // not implemented
     FFTModel &operator=(const FFTModel &); // not implemented
 
-    FFTDataServer *m_server;
-    int m_xshift;
-    int m_yshift;
-
-    FFTDataServer *getServer(const DenseTimeValueModel *,
-                             int, WindowType, int, int, int,
-                             bool, StorageAdviser::Criteria, sv_frame_t);
-
+    const DenseTimeValueModel *m_model;
+    int m_channel;
+    WindowType m_windowType;
+    int m_windowSize;
+    int m_windowIncrement;
+    int m_fftSize;
+    Window<float> m_windower;
+    mutable breakfastquay::FFT m_fft;
+    
     int getPeakPickWindowSize(PeakPickType type, sv_samplerate_t sampleRate,
                               int bin, float &percentile) const;
 
-    int getYRatio() {
-        int ys = m_yshift;
-        int r = 1;
-        while (ys) { --ys; r <<= 1; }
-        return r;
+    std::pair<sv_frame_t, sv_frame_t> getSourceSampleRange(int column) const {
+        sv_frame_t startFrame = m_windowIncrement * sv_frame_t(column);
+        sv_frame_t endFrame = startFrame + m_windowSize;
+        // Cols are centred on the audio sample (e.g. col 0 is centred at sample 0)
+        startFrame -= m_windowSize / 2;
+        endFrame -= m_windowSize / 2;
+        return { startFrame, endFrame };
     }
+
+    typedef std::vector<float, breakfastquay::StlAllocator<float>> fvec;
+    typedef std::vector<std::complex<float>,
+                        breakfastquay::StlAllocator<std::complex<float>>> cvec;
+    
+    cvec getFFTColumn(int column) const;
+    fvec getSourceSamples(int column) const;
+    fvec getSourceData(std::pair<sv_frame_t, sv_frame_t>) const;
+    fvec getSourceDataUncached(std::pair<sv_frame_t, sv_frame_t>) const;
+
+    struct SavedSourceData {
+        std::pair<sv_frame_t, sv_frame_t> range;
+        fvec data;
+    };
+    mutable SavedSourceData m_savedData;
+    
+    struct SavedColumn {
+        int n;
+        cvec col;
+    };
+    mutable std::deque<SavedColumn> m_cached;
+    size_t m_cacheSize;
 };
 
 #endif
--- a/data/model/Labeller.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/Labeller.h	Fri Jan 13 10:29:44 2017 +0000
@@ -171,37 +171,35 @@
         }
     }
         
+    /**
+     * Relabel all points in the given model that lie within the given
+     * multi-selection, according to the labelling properties of this
+     * labeller.  Return a command that has been executed but not yet
+     * added to the history.
+     */
     template <typename PointType>
-    void labelAll(SparseModel<PointType> &model, MultiSelection *ms) {
+    Command *labelAll(SparseModel<PointType> &model, MultiSelection *ms) {
 
-        typename SparseModel<PointType>::PointList::iterator i;
-        typename SparseModel<PointType>::PointList pl(model.getPoints());
-
-        typename SparseModel<PointType>::EditCommand *command =
-            new typename SparseModel<PointType>::EditCommand
+        auto points(model.getPoints());
+        auto command = new typename SparseModel<PointType>::EditCommand
             (&model, tr("Label Points"));
 
         PointType prevPoint(0);
+        bool havePrevPoint(false);
 
-        for (i = pl.begin(); i != pl.end(); ++i) {
+        for (auto p: points) {
 
-            bool inRange = true;
             if (ms) {
-                Selection s(ms->getContainingSelection(i->frame, false));
-                if (s.isEmpty() || !s.contains(i->frame)) {
-                    inRange = false;
+                Selection s(ms->getContainingSelection(p.frame, false));
+                if (!s.contains(p.frame)) {
+                    prevPoint = p;
+                    havePrevPoint = true;
+                    continue;
                 }
             }
 
-            PointType p(*i);
-
-            if (!inRange) {
-                prevPoint = p;
-                continue;
-            }
-
             if (actingOnPrevPoint()) {
-                if (i != pl.begin()) {
+                if (havePrevPoint) {
                     command->deletePoint(prevPoint);
                     label<PointType>(p, &prevPoint);
                     command->addPoint(prevPoint);
@@ -213,9 +211,94 @@
             }
 
             prevPoint = p;
+            havePrevPoint = true;
         }
 
-        command->finish();
+        return command->finish();
+    }
+
+    /**
+     * For each point in the given model (except the last), if that
+     * point lies within the given multi-selection, add n-1 new points
+     * at equally spaced intervals between it and the following point.
+     * Return a command that has been executed but not yet added to
+     * the history.
+     */
+    template <typename PointType>
+    Command *subdivide(SparseModel<PointType> &model, MultiSelection *ms, int n) {
+        
+        auto points(model.getPoints());
+        auto command = new typename SparseModel<PointType>::EditCommand
+            (&model, tr("Subdivide Points"));
+
+        for (auto i = points.begin(); i != points.end(); ++i) {
+
+            auto j = i;
+            // require a "next point" even if it's not in selection
+            if (++j == points.end()) {
+                break;
+            }
+
+            if (ms) {
+                Selection s(ms->getContainingSelection(i->frame, false));
+                if (!s.contains(i->frame)) {
+                    continue;
+                }
+            }
+
+            PointType p(*i);
+            PointType nextP(*j);
+
+            // n is the number of subdivisions, so we add n-1 new
+            // points equally spaced between p and nextP
+
+            for (int m = 1; m < n; ++m) {
+                sv_frame_t f = p.frame + (m * (nextP.frame - p.frame)) / n;
+                PointType newPoint(p);
+                newPoint.frame = f;
+                newPoint.label = tr("%1.%2").arg(p.label).arg(m+1);
+                command->addPoint(newPoint);
+            }
+        }
+
+        return command->finish();
+    }
+
+    /**
+     * Return a command that has been executed but not yet added to
+     * the history.
+     */
+    template <typename PointType>
+    Command *winnow(SparseModel<PointType> &model, MultiSelection *ms, int n) {
+        
+        auto points(model.getPoints());
+        auto command = new typename SparseModel<PointType>::EditCommand
+            (&model, tr("Winnow Points"));
+
+        int counter = 0;
+        
+        for (auto p: points) {
+
+            if (ms) {
+                Selection s(ms->getContainingSelection(p.frame, false));
+                if (!s.contains(p.frame)) {
+                    counter = 0;
+                    continue;
+                }
+            }
+
+            ++counter;
+
+            if (counter == n+1) counter = 1;
+            if (counter == 1) {
+                // this is an Nth instant, don't remove it
+                continue;
+            }
+            
+            command->deletePoint(p);
+        }
+
+        return command->finish();
     }
 
     template <typename PointType>
--- a/data/model/Model.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/Model.h	Fri Jan 13 10:29:44 2017 +0000
@@ -24,8 +24,6 @@
 #include "base/BaseTypes.h"
 #include "base/DataExportOptions.h"
 
-typedef std::vector<float> SampleBlock;
-
 class ZoomConstraint;
 class AlignmentModel;
 
@@ -123,7 +121,8 @@
      * through it an estimated percentage value showing how far
      * through the background operation it thinks it is (for progress
      * reporting).  If it has no way to calculate progress, it may
-     * return the special value COMPLETION_UNKNOWN.
+     * return the special value COMPLETION_UNKNOWN.  See also
+     * getCompletion().
      */
     virtual bool isReady(int *completion = 0) const {
 	bool ok = isOK();
--- a/data/model/ModelDataTableModel.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/ModelDataTableModel.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -47,7 +47,8 @@
     if (!m_model) return QVariant();
     if (role != Qt::EditRole && role != Qt::DisplayRole) return QVariant();
     if (!index.isValid()) return QVariant();
-    return m_model->getData(getUnsorted(index.row()), index.column(), role);
+    QVariant d = m_model->getData(getUnsorted(index.row()), index.column(), role);
+    return d;
 }
 
 bool
--- a/data/model/RangeSummarisableTimeValueModel.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/RangeSummarisableTimeValueModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _RANGE_SUMMARISABLE_TIME_VALUE_MODEL_H_
-#define _RANGE_SUMMARISABLE_TIME_VALUE_MODEL_H_
+#ifndef SV_RANGE_SUMMARISABLE_TIME_VALUE_MODEL_H
+#define SV_RANGE_SUMMARISABLE_TIME_VALUE_MODEL_H
 
 #include <QObject>
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/ReadOnlyWaveFileModel.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,738 @@
+/* -*- 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 file copyright 2006 Chris Cannam and QMUL.
+    
+    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 "ReadOnlyWaveFileModel.h"
+
+#include "fileio/AudioFileReader.h"
+#include "fileio/AudioFileReaderFactory.h"
+
+#include "system/System.h"
+
+#include "base/Preferences.h"
+
+#include <QFileInfo>
+#include <QTextStream>
+
+#include <iostream>
+//#include <unistd.h>
+#include <cmath>
+#include <sndfile.h>
+
+#include <cassert>
+
+using namespace std;
+
+//#define DEBUG_WAVE_FILE_MODEL 1
+
+PowerOfSqrtTwoZoomConstraint
+ReadOnlyWaveFileModel::m_zoomConstraint;
+
+ReadOnlyWaveFileModel::ReadOnlyWaveFileModel(FileSource source, sv_samplerate_t targetRate) :
+    m_source(source),
+    m_path(source.getLocation()),
+    m_reader(0),
+    m_myReader(true),
+    m_startFrame(0),
+    m_fillThread(0),
+    m_updateTimer(0),
+    m_lastFillExtent(0),
+    m_exiting(false),
+    m_lastDirectReadStart(0),
+    m_lastDirectReadCount(0)
+{
+    m_source.waitForData();
+
+    if (m_source.isOK()) {
+
+        Preferences *prefs = Preferences::getInstance();
+        
+        AudioFileReaderFactory::Parameters params;
+        params.targetRate = targetRate;
+
+        params.normalisation = prefs->getNormaliseAudio() ?
+            AudioFileReaderFactory::Normalisation::Peak :
+            AudioFileReaderFactory::Normalisation::None;
+
+        params.gaplessMode = prefs->getUseGaplessMode() ?
+            AudioFileReaderFactory::GaplessMode::Gapless :
+            AudioFileReaderFactory::GaplessMode::Gappy;
+        
+        params.threadingMode = AudioFileReaderFactory::ThreadingMode::Threaded;
+
+        m_reader = AudioFileReaderFactory::createReader(m_source, params);
+        if (m_reader) {
+            SVDEBUG << "ReadOnlyWaveFileModel::ReadOnlyWaveFileModel: reader rate: "
+                      << m_reader->getSampleRate() << endl;
+        }
+    }
+    
+    if (m_reader) setObjectName(m_reader->getTitle());
+    if (objectName() == "") setObjectName(QFileInfo(m_path).fileName());
+    if (isOK()) fillCache();
+}
+
+ReadOnlyWaveFileModel::ReadOnlyWaveFileModel(FileSource source, AudioFileReader *reader) :
+    m_source(source),
+    m_path(source.getLocation()),
+    m_reader(0),
+    m_myReader(false),
+    m_startFrame(0),
+    m_fillThread(0),
+    m_updateTimer(0),
+    m_lastFillExtent(0),
+    m_exiting(false)
+{
+    m_reader = reader;
+    if (m_reader) setObjectName(m_reader->getTitle());
+    if (objectName() == "") setObjectName(QFileInfo(m_path).fileName());
+    fillCache();
+}
+
+ReadOnlyWaveFileModel::~ReadOnlyWaveFileModel()
+{
+    m_exiting = true;
+    if (m_fillThread) m_fillThread->wait();
+    if (m_myReader) delete m_reader;
+    m_reader = 0;
+}
+
+bool
+ReadOnlyWaveFileModel::isOK() const
+{
+    return m_reader && m_reader->isOK();
+}
+
+bool
+ReadOnlyWaveFileModel::isReady(int *completion) const
+{
+    bool ready = (isOK() && (m_fillThread == 0));
+    double c = double(m_lastFillExtent) / double(getEndFrame() - getStartFrame());
+    static int prevCompletion = 0;
+    if (completion) {
+        *completion = int(c * 100.0 + 0.01);
+        if (m_reader) {
+            int decodeCompletion = m_reader->getDecodeCompletion();
+            if (decodeCompletion < 90) *completion = decodeCompletion;
+            else *completion = min(*completion, decodeCompletion);
+        }
+        if (*completion != 0 &&
+            *completion != 100 &&
+            prevCompletion != 0 &&
+            prevCompletion > *completion) {
+            // just to avoid completion going backwards
+            *completion = prevCompletion;
+        }
+        prevCompletion = *completion;
+    }
+#ifdef DEBUG_WAVE_FILE_MODEL
+    SVDEBUG << "ReadOnlyWaveFileModel::isReady(): ready = " << ready << ", completion = " << (completion ? *completion : -1) << endl;
+#endif
+    return ready;
+}
+
+sv_frame_t
+ReadOnlyWaveFileModel::getFrameCount() const
+{
+    if (!m_reader) return 0;
+    return m_reader->getFrameCount();
+}
+
+int
+ReadOnlyWaveFileModel::getChannelCount() const
+{
+    if (!m_reader) return 0;
+    return m_reader->getChannelCount();
+}
+
+sv_samplerate_t
+ReadOnlyWaveFileModel::getSampleRate() const 
+{
+    if (!m_reader) return 0;
+    return m_reader->getSampleRate();
+}
+
+sv_samplerate_t
+ReadOnlyWaveFileModel::getNativeRate() const 
+{
+    if (!m_reader) return 0;
+    sv_samplerate_t rate = m_reader->getNativeRate();
+    if (rate == 0) rate = getSampleRate();
+    return rate;
+}
+
+QString
+ReadOnlyWaveFileModel::getTitle() const
+{
+    QString title;
+    if (m_reader) title = m_reader->getTitle();
+    if (title == "") title = objectName();
+    return title;
+}
+
+QString
+ReadOnlyWaveFileModel::getMaker() const
+{
+    if (m_reader) return m_reader->getMaker();
+    return "";
+}
+
+QString
+ReadOnlyWaveFileModel::getLocation() const
+{
+    if (m_reader) return m_reader->getLocation();
+    return "";
+}
+
+QString
+ReadOnlyWaveFileModel::getLocalFilename() const
+{
+    if (m_reader) return m_reader->getLocalFilename();
+    return "";
+}
+    
+floatvec_t
+ReadOnlyWaveFileModel::getData(int channel, sv_frame_t start, sv_frame_t count) const
+{
+    // Read directly from the file.  This is used for e.g. audio
+    // playback or input to transforms.
+
+#ifdef DEBUG_WAVE_FILE_MODEL
+    cout << "ReadOnlyWaveFileModel::getData[" << this << "]: " << channel << ", " << start << ", " << count << endl;
+#endif
+
+    int channels = getChannelCount();
+
+    if (channel >= channels) {
+        cerr << "ERROR: WaveFileModel::getData: channel ("
+             << channel << ") >= channel count (" << channels << ")"
+             << endl;
+        return {};
+    }
+
+    if (!m_reader || !m_reader->isOK() || count == 0) {
+        return {};
+    }
+
+    if (start >= m_startFrame) {
+        start -= m_startFrame;
+    } else {
+        if (count <= m_startFrame - start) {
+            return {};
+        } else {
+            count -= (m_startFrame - start);
+            start = 0;
+        }
+    }
+
+    floatvec_t interleaved = m_reader->getInterleavedFrames(start, count);
+    if (channels == 1) return interleaved;
+
+    sv_frame_t obtained = interleaved.size() / channels;
+    
+    floatvec_t result(obtained, 0.f);
+    
+    if (channel != -1) {
+        // get a single channel
+        for (int i = 0; i < obtained; ++i) {
+            result[i] = interleaved[i * channels + channel];
+        }
+    } else {
+        // channel == -1, mix down all channels
+        for (int i = 0; i < obtained; ++i) {
+            for (int c = 0; c < channels; ++c) {
+                result[i] += interleaved[i * channels + c];
+            }
+        }
+    }
+
+    return result;
+}
+
+vector<floatvec_t>
+ReadOnlyWaveFileModel::getMultiChannelData(int fromchannel, int tochannel,
+                                           sv_frame_t start, sv_frame_t count) const
+{
+    // Read directly from the file.  This is used for e.g. audio
+    // playback or input to transforms.
+
+#ifdef DEBUG_WAVE_FILE_MODEL
+    cout << "ReadOnlyWaveFileModel::getData[" << this << "]: " << fromchannel << "," << tochannel << ", " << start << ", " << count << endl;
+#endif
+
+    int channels = getChannelCount();
+
+    if (fromchannel > tochannel) {
+        cerr << "ERROR: ReadOnlyWaveFileModel::getData: fromchannel ("
+                  << fromchannel << ") > tochannel (" << tochannel << ")"
+                  << endl;
+        return {};
+    }
+
+    if (tochannel >= channels) {
+        cerr << "ERROR: ReadOnlyWaveFileModel::getData: tochannel ("
+                  << tochannel << ") >= channel count (" << channels << ")"
+                  << endl;
+        return {};
+    }
+
+    if (!m_reader || !m_reader->isOK() || count == 0) {
+        return {};
+    }
+
+    int reqchannels = (tochannel - fromchannel) + 1;
+
+    if (start >= m_startFrame) {
+        start -= m_startFrame;
+    } else {
+        if (count <= m_startFrame - start) {
+            return {};
+        } else {
+            count -= (m_startFrame - start);
+            start = 0;
+        }
+    }
+
+    floatvec_t interleaved = m_reader->getInterleavedFrames(start, count);
+    if (channels == 1) return { interleaved };
+
+    sv_frame_t obtained = interleaved.size() / channels;
+    vector<floatvec_t> result(reqchannels, floatvec_t(obtained, 0.f));
+
+    for (int c = fromchannel; c <= tochannel; ++c) {
+        int destc = c - fromchannel;
+        for (int i = 0; i < obtained; ++i) {
+            result[destc][i] = interleaved[i * channels + c];
+        }
+    }
+    
+    return result;
+}
+
+int
+ReadOnlyWaveFileModel::getSummaryBlockSize(int desired) const
+{
+    int cacheType = 0;
+    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
+        return desired;
+    } else {
+        return roundedBlockSize;
+    }
+}    
+
+void
+ReadOnlyWaveFileModel::getSummaries(int channel, sv_frame_t start, sv_frame_t count,
+                                    RangeBlock &ranges, int &blockSize) const
+{
+    ranges.clear();
+    if (!isOK()) return;
+    ranges.reserve((count / blockSize) + 1);
+
+    if (start > m_startFrame) start -= m_startFrame;
+    else if (count <= m_startFrame - start) return;
+    else {
+        count -= (m_startFrame - start);
+        start = 0;
+    }
+
+    int cacheType = 0;
+    int power = m_zoomConstraint.getMinCachePower();
+    int roundedBlockSize = m_zoomConstraint.getNearestBlockSize
+        (blockSize, cacheType, power, ZoomConstraint::RoundDown);
+
+    int channels = getChannelCount();
+
+    if (cacheType != 0 && cacheType != 1) {
+
+	// We need to read directly from the file.  We haven't got
+	// this cached.  Hope the requested area is small.  This is
+	// not optimal -- we'll end up reading the same frames twice
+	// for stereo files, in two separate calls to this method.
+	// We could fairly trivially handle this for most cases that
+	// matter by putting a single cache in getInterleavedFrames
+	// for short queries.
+
+        m_directReadMutex.lock();
+
+        if (m_lastDirectReadStart != start ||
+            m_lastDirectReadCount != count ||
+            m_directRead.empty()) {
+
+            m_directRead = m_reader->getInterleavedFrames(start, count);
+            m_lastDirectReadStart = start;
+            m_lastDirectReadCount = count;
+        }
+
+	float max = 0.0, min = 0.0, total = 0.0;
+	sv_frame_t i = 0, got = 0;
+
+	while (i < count) {
+
+	    sv_frame_t index = i * channels + channel;
+	    if (index >= (sv_frame_t)m_directRead.size()) break;
+            
+	    float sample = m_directRead[index];
+            if (sample > max || got == 0) max = sample;
+	    if (sample < min || got == 0) min = sample;
+            total += fabsf(sample);
+
+	    ++i;
+            ++got;
+            
+            if (got == blockSize) {
+                ranges.push_back(Range(min, max, total / float(got)));
+                min = max = total = 0.0f;
+                got = 0;
+	    }
+	}
+
+        m_directReadMutex.unlock();
+
+	if (got > 0) {
+            ranges.push_back(Range(min, max, total / float(got)));
+	}
+
+	return;
+
+    } else {
+
+	QMutexLocker locker(&m_mutex);
+    
+	const RangeBlock &cache = m_cache[cacheType];
+
+        blockSize = roundedBlockSize;
+
+	sv_frame_t cacheBlock, div;
+
+        cacheBlock = (sv_frame_t(1) << m_zoomConstraint.getMinCachePower());
+	if (cacheType == 1) {
+	    cacheBlock = sv_frame_t(double(cacheBlock) * sqrt(2.) + 0.01);
+	}
+        div = blockSize / cacheBlock;
+
+	sv_frame_t startIndex = start / cacheBlock;
+	sv_frame_t endIndex = (start + count) / cacheBlock;
+
+	float max = 0.0, min = 0.0, total = 0.0;
+	sv_frame_t i = 0, got = 0;
+
+#ifdef DEBUG_WAVE_FILE_MODEL
+	cerr << "blockSize is " << blockSize << ", cacheBlock " << cacheBlock << ", start " << start << ", count " << count << " (frame count " << getFrameCount() << "), power is " << power << ", div is " << div << ", startIndex " << startIndex << ", endIndex " << endIndex << endl;
+#endif
+
+	for (i = 0; i <= endIndex - startIndex; ) {
+        
+	    sv_frame_t index = (i + startIndex) * channels + channel;
+	    if (!in_range_for(cache, index)) break;
+            
+            const Range &range = cache[index];
+            if (range.max() > max || got == 0) max = range.max();
+            if (range.min() < min || got == 0) min = range.min();
+            total += range.absmean();
+            
+	    ++i;
+            ++got;
+            
+	    if (got == div) {
+		ranges.push_back(Range(min, max, total / float(got)));
+                min = max = total = 0.0f;
+                got = 0;
+	    }
+	}
+		
+	if (got > 0) {
+            ranges.push_back(Range(min, max, total / float(got)));
+	}
+    }
+
+#ifdef DEBUG_WAVE_FILE_MODEL
+    cerr << "returning " << ranges.size() << " ranges" << endl;
+#endif
+    return;
+}
+
+ReadOnlyWaveFileModel::Range
+ReadOnlyWaveFileModel::getSummary(int channel, sv_frame_t start, sv_frame_t count) const
+{
+    Range range;
+    if (!isOK()) return range;
+
+    if (start > m_startFrame) start -= m_startFrame;
+    else if (count <= m_startFrame - start) return range;
+    else {
+        count -= (m_startFrame - start);
+        start = 0;
+    }
+
+    int blockSize;
+    for (blockSize = 1; blockSize <= count; blockSize *= 2);
+    if (blockSize > 1) blockSize /= 2;
+
+    bool first = false;
+
+    sv_frame_t blockStart = (start / blockSize) * blockSize;
+    sv_frame_t blockEnd = ((start + count) / blockSize) * blockSize;
+
+    if (blockStart < start) blockStart += blockSize;
+        
+    if (blockEnd > blockStart) {
+        RangeBlock ranges;
+        getSummaries(channel, blockStart, blockEnd - blockStart, ranges, blockSize);
+        for (int i = 0; i < (int)ranges.size(); ++i) {
+            if (first || ranges[i].min() < range.min()) range.setMin(ranges[i].min());
+            if (first || ranges[i].max() > range.max()) range.setMax(ranges[i].max());
+            if (first || ranges[i].absmean() < range.absmean()) range.setAbsmean(ranges[i].absmean());
+            first = false;
+        }
+    }
+
+    if (blockStart > start) {
+        Range startRange = getSummary(channel, start, blockStart - start);
+        range.setMin(min(range.min(), startRange.min()));
+        range.setMax(max(range.max(), startRange.max()));
+        range.setAbsmean(min(range.absmean(), startRange.absmean()));
+    }
+
+    if (blockEnd < start + count) {
+        Range endRange = getSummary(channel, blockEnd, start + count - blockEnd);
+        range.setMin(min(range.min(), endRange.min()));
+        range.setMax(max(range.max(), endRange.max()));
+        range.setAbsmean(min(range.absmean(), endRange.absmean()));
+    }
+
+    return range;
+}
+
+void
+ReadOnlyWaveFileModel::fillCache()
+{
+    m_mutex.lock();
+
+    m_updateTimer = new QTimer(this);
+    connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(fillTimerTimedOut()));
+    m_updateTimer->start(100);
+
+    m_fillThread = new RangeCacheFillThread(*this);
+    connect(m_fillThread, SIGNAL(finished()), this, SLOT(cacheFilled()));
+
+    m_mutex.unlock();
+    m_fillThread->start();
+
+#ifdef DEBUG_WAVE_FILE_MODEL
+    SVDEBUG << "ReadOnlyWaveFileModel::fillCache: started fill thread" << endl;
+#endif
+}   
+
+void
+ReadOnlyWaveFileModel::fillTimerTimedOut()
+{
+    if (m_fillThread) {
+	sv_frame_t fillExtent = m_fillThread->getFillExtent();
+#ifdef DEBUG_WAVE_FILE_MODEL
+        SVDEBUG << "ReadOnlyWaveFileModel::fillTimerTimedOut: extent = " << fillExtent << endl;
+#endif
+	if (fillExtent > m_lastFillExtent) {
+	    emit modelChangedWithin(m_lastFillExtent, fillExtent);
+	    m_lastFillExtent = fillExtent;
+	}
+    } else {
+#ifdef DEBUG_WAVE_FILE_MODEL
+        SVDEBUG << "ReadOnlyWaveFileModel::fillTimerTimedOut: no thread" << endl;
+#endif
+	emit modelChanged();
+    }
+}
+
+void
+ReadOnlyWaveFileModel::cacheFilled()
+{
+    m_mutex.lock();
+    delete m_fillThread;
+    m_fillThread = 0;
+    delete m_updateTimer;
+    m_updateTimer = 0;
+    m_mutex.unlock();
+    if (getEndFrame() > m_lastFillExtent) {
+        emit modelChangedWithin(m_lastFillExtent, getEndFrame());
+    }
+    emit modelChanged();
+    emit ready();
+#ifdef DEBUG_WAVE_FILE_MODEL
+    SVDEBUG << "ReadOnlyWaveFileModel::cacheFilled" << endl;
+#endif
+}
+
+void
+ReadOnlyWaveFileModel::RangeCacheFillThread::run()
+{
+    int cacheBlockSize[2];
+    cacheBlockSize[0] = (1 << m_model.m_zoomConstraint.getMinCachePower());
+    cacheBlockSize[1] = (int((1 << m_model.m_zoomConstraint.getMinCachePower()) *
+                                        sqrt(2.) + 0.01));
+    
+    sv_frame_t frame = 0;
+    const sv_frame_t readBlockSize = 32768;
+    floatvec_t block;
+
+    if (!m_model.isOK()) return;
+    
+    int channels = m_model.getChannelCount();
+    bool updating = m_model.m_reader->isUpdating();
+
+    if (updating) {
+        while (channels == 0 && !m_model.m_exiting) {
+#ifdef DEBUG_WAVE_FILE_MODEL
+            cerr << "ReadOnlyWaveFileModel::fill: Waiting for channels..." << endl;
+#endif
+            sleep(1);
+            channels = m_model.getChannelCount();
+        }
+    }
+
+    Range *range = new Range[2 * channels];
+    float *means = new float[2 * channels];
+    int count[2];
+    count[0] = count[1] = 0;
+    for (int i = 0; i < 2 * channels; ++i) {
+        means[i] = 0.f;
+    }
+
+    bool first = true;
+
+    while (first || updating) {
+
+        updating = m_model.m_reader->isUpdating();
+        m_frameCount = m_model.getFrameCount();
+
+        m_model.m_mutex.lock();
+
+        while (frame < m_frameCount) {
+
+            m_model.m_mutex.unlock();
+
+#ifdef DEBUG_WAVE_FILE_MODEL
+            cerr << "ReadOnlyWaveFileModel::fill inner loop: frame = " << frame << ", count = " << m_frameCount << ", blocksize " << readBlockSize << endl;
+#endif
+
+            if (updating && (frame + readBlockSize > m_frameCount)) {
+                m_model.m_mutex.lock(); // must be locked on exiting loop
+                break;
+            }
+
+            block = m_model.m_reader->getInterleavedFrames(frame, readBlockSize);
+
+            sv_frame_t gotBlockSize = block.size() / channels;
+
+            m_model.m_mutex.lock();
+
+            for (sv_frame_t i = 0; i < gotBlockSize; ++i) {
+		
+                for (int ch = 0; ch < channels; ++ch) {
+
+                    sv_frame_t index = channels * i + ch;
+                    float sample = block[index];
+                    
+                    for (int cacheType = 0; cacheType < 2; ++cacheType) {
+                        sv_frame_t rangeIndex = ch * 2 + cacheType;
+                        range[rangeIndex].sample(sample);
+                        means[rangeIndex] += fabsf(sample);
+                    }
+                }
+
+                for (int cacheType = 0; cacheType < 2; ++cacheType) {
+
+                    if (++count[cacheType] == cacheBlockSize[cacheType]) {
+                        
+                        for (int ch = 0; ch < int(channels); ++ch) {
+                            int rangeIndex = ch * 2 + cacheType;
+                            means[rangeIndex] = means[rangeIndex] / float(count[cacheType]);
+                            range[rangeIndex].setAbsmean(means[rangeIndex]);
+                            m_model.m_cache[cacheType].push_back(range[rangeIndex]);
+                            range[rangeIndex] = Range();
+                            means[rangeIndex] = 0.f;
+                        }
+
+                        count[cacheType] = 0;
+                    }
+                }
+                
+                ++frame;
+            }
+
+            if (m_model.m_exiting) break;
+            m_fillExtent = frame;
+        }
+
+        m_model.m_mutex.unlock();
+            
+        first = false;
+        if (m_model.m_exiting) break;
+        if (updating) {
+            sleep(1);
+        }
+    }
+
+    if (!m_model.m_exiting) {
+
+        QMutexLocker locker(&m_model.m_mutex);
+
+        for (int cacheType = 0; cacheType < 2; ++cacheType) {
+
+            if (count[cacheType] > 0) {
+
+                for (int ch = 0; ch < int(channels); ++ch) {
+                    int rangeIndex = ch * 2 + cacheType;
+                    means[rangeIndex] = means[rangeIndex] / float(count[cacheType]);
+                    range[rangeIndex].setAbsmean(means[rangeIndex]);
+                    m_model.m_cache[cacheType].push_back(range[rangeIndex]);
+                    range[rangeIndex] = Range();
+                    means[rangeIndex] = 0.f;
+                }
+
+                count[cacheType] = 0;
+            }
+            
+            const Range &rr = *m_model.m_cache[cacheType].begin();
+            MUNLOCK(&rr, m_model.m_cache[cacheType].capacity() * sizeof(Range));
+        }
+    }
+    
+    delete[] means;
+    delete[] range;
+
+    m_fillExtent = m_frameCount;
+
+#ifdef DEBUG_WAVE_FILE_MODEL        
+    for (int cacheType = 0; cacheType < 2; ++cacheType) {
+        cerr << "Cache type " << cacheType << " now contains " << m_model.m_cache[cacheType].size() << " ranges" << endl;
+    }
+#endif
+}
+
+void
+ReadOnlyWaveFileModel::toXml(QTextStream &out,
+                     QString indent,
+                     QString extraAttributes) const
+{
+    Model::toXml(out, indent,
+                 QString("type=\"wavefile\" file=\"%1\" %2")
+                 .arg(encodeEntities(m_path)).arg(extraAttributes));
+}
+
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/ReadOnlyWaveFileModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,131 @@
+/* -*- 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 file copyright 2006 Chris Cannam and QMUL.
+    
+    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 READ_ONLY_WAVE_FILE_MODEL_H
+#define READ_ONLY_WAVE_FILE_MODEL_H
+
+#include "WaveFileModel.h"
+
+#include "base/Thread.h"
+#include <QMutex>
+#include <QTimer>
+
+#include "data/fileio/FileSource.h"
+
+#include "RangeSummarisableTimeValueModel.h"
+#include "PowerOfSqrtTwoZoomConstraint.h"
+
+#include <stdlib.h>
+
+class AudioFileReader;
+
+class ReadOnlyWaveFileModel : public WaveFileModel
+{
+    Q_OBJECT
+
+public:
+    ReadOnlyWaveFileModel(FileSource source, sv_samplerate_t targetRate = 0);
+    ReadOnlyWaveFileModel(FileSource source, AudioFileReader *reader);
+    ~ReadOnlyWaveFileModel();
+
+    bool isOK() const;
+    bool isReady(int *) const;
+
+    const ZoomConstraint *getZoomConstraint() const { return &m_zoomConstraint; }
+
+    sv_frame_t getFrameCount() const;
+    int getChannelCount() const;
+    sv_samplerate_t getSampleRate() const;
+    sv_samplerate_t getNativeRate() const;
+
+    QString getTitle() const;
+    QString getMaker() const;
+    QString getLocation() const;
+
+    QString getLocalFilename() const;
+
+    float getValueMinimum() const { return -1.0f; }
+    float getValueMaximum() const { return  1.0f; }
+
+    virtual sv_frame_t getStartFrame() const { return m_startFrame; }
+    virtual sv_frame_t getEndFrame() const { return m_startFrame + getFrameCount(); }
+
+    void setStartFrame(sv_frame_t startFrame) { m_startFrame = startFrame; }
+
+    virtual floatvec_t getData(int channel, sv_frame_t start, sv_frame_t count) const;
+
+    virtual std::vector<floatvec_t> getMultiChannelData(int fromchannel, int tochannel, sv_frame_t start, sv_frame_t count) const;
+
+    virtual int getSummaryBlockSize(int desired) const;
+
+    virtual void getSummaries(int channel, sv_frame_t start, sv_frame_t count,
+                              RangeBlock &ranges,
+                              int &blockSize) const;
+
+    virtual Range getSummary(int channel, sv_frame_t start, sv_frame_t count) const;
+
+    QString getTypeName() const { return tr("Wave File"); }
+
+    virtual void toXml(QTextStream &out,
+                       QString indent = "",
+                       QString extraAttributes = "") const;
+
+protected slots:
+    void fillTimerTimedOut();
+    void cacheFilled();
+    
+protected:
+    void initialize();
+
+    class RangeCacheFillThread : public Thread
+    {
+    public:
+        RangeCacheFillThread(ReadOnlyWaveFileModel &model) :
+	    m_model(model), m_fillExtent(0),
+            m_frameCount(model.getFrameCount()) { }
+    
+	sv_frame_t getFillExtent() const { return m_fillExtent; }
+        virtual void run();
+
+    protected:
+        ReadOnlyWaveFileModel &m_model;
+	sv_frame_t m_fillExtent;
+        sv_frame_t m_frameCount;
+    };
+         
+    void fillCache();
+
+    FileSource m_source;
+    QString m_path;
+    AudioFileReader *m_reader;
+    bool m_myReader;
+
+    sv_frame_t m_startFrame;
+
+    RangeBlock m_cache[2]; // interleaved at two base resolutions
+    mutable QMutex m_mutex;
+    RangeCacheFillThread *m_fillThread;
+    QTimer *m_updateTimer;
+    sv_frame_t m_lastFillExtent;
+    bool m_exiting;
+    static PowerOfSqrtTwoZoomConstraint m_zoomConstraint;
+
+    mutable floatvec_t m_directRead;
+    mutable sv_frame_t m_lastDirectReadStart;
+    mutable sv_frame_t m_lastDirectReadCount;
+    mutable QMutex m_directReadMutex;
+};    
+
+#endif
--- a/data/model/SparseModel.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/SparseModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -20,6 +20,7 @@
 #include "TabularModel.h"
 #include "base/Command.h"
 #include "base/RealTime.h"
+#include "system/System.h"
 
 #include <iostream>
 
@@ -331,8 +332,13 @@
     virtual QVariant getData(int row, int column, int role) const
     {
         PointListConstIterator i = getPointListIteratorForRow(row);
-        if (i == m_points.end()) return QVariant();
+        if (i == m_points.end()) {
+//            cerr << "no iterator for row " << row << " (have " << getRowCount() << " rows)" << endl;
+            return QVariant();
+        }
 
+//        cerr << "returning data for row " << row << " col " << column << endl;
+        
         switch (column) {
         case 0: {
             if (role == SortRole) return int(i->frame);
@@ -410,6 +416,7 @@
     // This is only used if the model is called on to act in
     // TabularModel mode
     mutable std::vector<sv_frame_t> m_rows; // map from row number to frame
+
     void rebuildRowVector() const
     {
         m_rows.clear();
@@ -457,7 +464,7 @@
         while (ri > 0 && m_rows[ri-1] == m_rows[row]) { --ri; ++indexAtFrame; }
         int initialIndexAtFrame = indexAtFrame;
 
-//        std::cerr << "getPointListIteratorForRow " << row << ": initialIndexAtFrame = " << initialIndexAtFrame << std::endl;
+//        std::cerr << "getPointListIteratorForRow " << row << ": initialIndexAtFrame = " << initialIndexAtFrame << " for frame " << frame << std::endl;
 
         PointListConstIterator i0, i1;
         getPointIterators(frame, i0, i1);
@@ -470,9 +477,13 @@
             if (indexAtFrame > 0) { --indexAtFrame; continue; }
             return i;
         }
-
-//        std::cerr << "returning i with i->frame = " << i->frame << std::endl;
-
+/*
+        if (i == m_points.end()) {
+            std::cerr << "returning i at end" << std::endl;
+        } else {
+            std::cerr << "returning i with i->frame = " << i->frame << std::endl;
+        }
+*/
         if (indexAtFrame > 0) {
             std::cerr << "WARNING: SparseModel::getPointListIteratorForRow: No iterator available for row " << row << " (frame = " << frame << ", index at frame = " << initialIndexAtFrame << ", leftover index " << indexAtFrame << ")" << std::endl;
         }
@@ -729,8 +740,8 @@
     {
 	QMutexLocker locker(&m_mutex);
 	m_resolution = resolution;
+        m_rows.clear();
     }
-    m_rows.clear();
     emit modelChanged();
 }
 
@@ -742,8 +753,8 @@
 	QMutexLocker locker(&m_mutex);
 	m_points.clear();
         m_pointCount = 0;
+        m_rows.clear();
     }
-    m_rows.clear();
     emit modelChanged();
 }
 
@@ -751,12 +762,11 @@
 void
 SparseModel<PointType>::addPoint(const PointType &point)
 {
-    {
-	QMutexLocker locker(&m_mutex);
-	m_points.insert(point);
-        m_pointCount++;
-        if (point.getLabel() != "") m_hasTextLabels = true;
-    }
+    QMutexLocker locker(&m_mutex);
+
+    m_points.insert(point);
+    m_pointCount++;
+    if (point.getLabel() != "") m_hasTextLabels = true;
 
     // Even though this model is nominally sparse, there may still be
     // too many signals going on here (especially as they'll probably
@@ -783,18 +793,16 @@
 bool
 SparseModel<PointType>::containsPoint(const PointType &point)
 {
-    {
-	QMutexLocker locker(&m_mutex);
+    QMutexLocker locker(&m_mutex);
 
-	PointListIterator i = m_points.lower_bound(point);
-	typename PointType::Comparator comparator;
-	while (i != m_points.end()) {
-	    if (i->frame > point.frame) break;
-	    if (!comparator(*i, point) && !comparator(point, *i)) {
-                return true;
-	    }
-	    ++i;
-	}
+    PointListIterator i = m_points.lower_bound(point);
+    typename PointType::Comparator comparator;
+    while (i != m_points.end()) {
+        if (i->frame > point.frame) break;
+        if (!comparator(*i, point) && !comparator(point, *i)) {
+            return true;
+        }
+        ++i;
     }
 
     return false;
@@ -804,21 +812,20 @@
 void
 SparseModel<PointType>::deletePoint(const PointType &point)
 {
-    {
-	QMutexLocker locker(&m_mutex);
+    QMutexLocker locker(&m_mutex);
 
-	PointListIterator i = m_points.lower_bound(point);
-	typename PointType::Comparator comparator;
-	while (i != m_points.end()) {
-	    if (i->frame > point.frame) break;
-	    if (!comparator(*i, point) && !comparator(point, *i)) {
-		m_points.erase(i);
-                m_pointCount--;
-		break;
+    PointListIterator i = m_points.lower_bound(point);
+    typename PointType::Comparator comparator;
+    while (i != m_points.end()) {
+        if (i->frame > point.frame) break;
+        if (!comparator(*i, point) && !comparator(point, *i)) {
+            m_points.erase(i);
+            m_pointCount--;
+            break;
 	    }
-	    ++i;
-	}
+        ++i;
     }
+
 //    std::cout << "SparseOneDimensionalModel: emit modelChanged("
 //	      << point.frame << ")" << std::endl;
     m_rows.clear(); //!!! inefficient
@@ -831,6 +838,8 @@
 {
 //    std::cerr << "SparseModel::setCompletion(" << completion << ")" << std::endl;
 
+    QMutexLocker locker(&m_mutex);
+
     if (m_completion != completion) {
 	m_completion = completion;
 
--- a/data/model/WaveFileModel.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/WaveFileModel.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -15,738 +15,7 @@
 
 #include "WaveFileModel.h"
 
-#include "fileio/AudioFileReader.h"
-#include "fileio/AudioFileReaderFactory.h"
-
-#include "system/System.h"
-
-#include "base/Preferences.h"
-
-#include <QFileInfo>
-#include <QTextStream>
-
-#include <iostream>
-#include <unistd.h>
-#include <cmath>
-#include <sndfile.h>
-
-#include <cassert>
-
-//#define DEBUG_WAVE_FILE_MODEL 1
-
-PowerOfSqrtTwoZoomConstraint
-WaveFileModel::m_zoomConstraint;
-
-WaveFileModel::WaveFileModel(FileSource source, sv_samplerate_t targetRate) :
-    m_source(source),
-    m_path(source.getLocation()),
-    m_reader(0),
-    m_myReader(true),
-    m_startFrame(0),
-    m_fillThread(0),
-    m_updateTimer(0),
-    m_lastFillExtent(0),
-    m_exiting(false),
-    m_lastDirectReadStart(0),
-    m_lastDirectReadCount(0)
+WaveFileModel::~WaveFileModel()
 {
-    m_source.waitForData();
-    if (m_source.isOK()) {
-        bool normalise = Preferences::getInstance()->getNormaliseAudio();
-        m_reader = AudioFileReaderFactory::createThreadingReader
-            (m_source, targetRate, normalise);
-        if (m_reader) {
-            SVDEBUG << "WaveFileModel::WaveFileModel: reader rate: "
-                      << m_reader->getSampleRate() << endl;
-        }
-    }
-    if (m_reader) setObjectName(m_reader->getTitle());
-    if (objectName() == "") setObjectName(QFileInfo(m_path).fileName());
-    if (isOK()) fillCache();
 }
 
-WaveFileModel::WaveFileModel(FileSource source, AudioFileReader *reader) :
-    m_source(source),
-    m_path(source.getLocation()),
-    m_reader(0),
-    m_myReader(false),
-    m_startFrame(0),
-    m_fillThread(0),
-    m_updateTimer(0),
-    m_lastFillExtent(0),
-    m_exiting(false)
-{
-    m_reader = reader;
-    if (m_reader) setObjectName(m_reader->getTitle());
-    if (objectName() == "") setObjectName(QFileInfo(m_path).fileName());
-    fillCache();
-}
-
-WaveFileModel::~WaveFileModel()
-{
-    m_exiting = true;
-    if (m_fillThread) m_fillThread->wait();
-    if (m_myReader) delete m_reader;
-    m_reader = 0;
-}
-
-bool
-WaveFileModel::isOK() const
-{
-    return m_reader && m_reader->isOK();
-}
-
-bool
-WaveFileModel::isReady(int *completion) const
-{
-    bool ready = (isOK() && (m_fillThread == 0));
-    double c = double(m_lastFillExtent) / double(getEndFrame() - getStartFrame());
-    static int prevCompletion = 0;
-    if (completion) {
-        *completion = int(c * 100.0 + 0.01);
-        if (m_reader) {
-            int decodeCompletion = m_reader->getDecodeCompletion();
-            if (decodeCompletion < 90) *completion = decodeCompletion;
-            else *completion = std::min(*completion, decodeCompletion);
-        }
-        if (*completion != 0 &&
-            *completion != 100 &&
-            prevCompletion != 0 &&
-            prevCompletion > *completion) {
-            // just to avoid completion going backwards
-            *completion = prevCompletion;
-        }
-        prevCompletion = *completion;
-    }
-#ifdef DEBUG_WAVE_FILE_MODEL
-    SVDEBUG << "WaveFileModel::isReady(): ready = " << ready << ", completion = " << (completion ? *completion : -1) << endl;
-#endif
-    return ready;
-}
-
-sv_frame_t
-WaveFileModel::getFrameCount() const
-{
-    if (!m_reader) return 0;
-    return m_reader->getFrameCount();
-}
-
-int
-WaveFileModel::getChannelCount() const
-{
-    if (!m_reader) return 0;
-    return m_reader->getChannelCount();
-}
-
-sv_samplerate_t
-WaveFileModel::getSampleRate() const 
-{
-    if (!m_reader) return 0;
-    return m_reader->getSampleRate();
-}
-
-sv_samplerate_t
-WaveFileModel::getNativeRate() const 
-{
-    if (!m_reader) return 0;
-    sv_samplerate_t rate = m_reader->getNativeRate();
-    if (rate == 0) rate = getSampleRate();
-    return rate;
-}
-
-QString
-WaveFileModel::getTitle() const
-{
-    QString title;
-    if (m_reader) title = m_reader->getTitle();
-    if (title == "") title = objectName();
-    return title;
-}
-
-QString
-WaveFileModel::getMaker() const
-{
-    if (m_reader) return m_reader->getMaker();
-    return "";
-}
-
-QString
-WaveFileModel::getLocation() const
-{
-    if (m_reader) return m_reader->getLocation();
-    return "";
-}
-
-QString
-WaveFileModel::getLocalFilename() const
-{
-    if (m_reader) return m_reader->getLocalFilename();
-    return "";
-}
-    
-sv_frame_t
-WaveFileModel::getData(int channel, sv_frame_t start, sv_frame_t count,
-                       float *buffer) const
-{
-    // Always read these directly from the file. 
-    // This is used for e.g. audio playback.
-    // Could be much more efficient (although compiler optimisation will help)
-
-#ifdef DEBUG_WAVE_FILE_MODEL
-    cout << "WaveFileModel::getData[" << this << "]: " << channel << ", " << start << ", " << count << ", " << buffer << endl;
-#endif
-
-    if (start >= m_startFrame) {
-        start -= m_startFrame;
-    } else {
-        for (sv_frame_t i = 0; i < count; ++i) {
-            buffer[i] = 0.f;
-        }
-        if (count <= m_startFrame - start) {
-            return 0;
-        } else {
-            count -= (m_startFrame - start);
-            start = 0;
-        }
-    }
-
-    if (!m_reader || !m_reader->isOK() || count == 0) {
-        for (sv_frame_t i = 0; i < count; ++i) buffer[i] = 0.f;
-        return 0;
-    }
-
-#ifdef DEBUG_WAVE_FILE_MODEL
-//    SVDEBUG << "WaveFileModel::getValues(" << channel << ", "
-//              << start << ", " << end << "): calling reader" << endl;
-#endif
-
-    int channels = getChannelCount();
-
-    SampleBlock frames = m_reader->getInterleavedFrames(start, count);
-
-    sv_frame_t i = 0;
-
-    int ch0 = channel, ch1 = channel;
-    if (channel == -1) {
-	ch0 = 0;
-	ch1 = channels - 1;
-    }
-    
-    while (i < count) {
-
-	buffer[i] = 0.0;
-
-	for (int ch = ch0; ch <= ch1; ++ch) {
-
-	    sv_frame_t index = i * channels + ch;
-	    if (index >= (sv_frame_t)frames.size()) break;
-            
-	    float sample = frames[index];
-	    buffer[i] += sample;
-	}
-
-	++i;
-    }
-
-    return i;
-}
-
-sv_frame_t
-WaveFileModel::getMultiChannelData(int fromchannel, int tochannel,
-                                   sv_frame_t start, sv_frame_t count,
-                                   float **buffer) const
-{
-#ifdef DEBUG_WAVE_FILE_MODEL
-    cout << "WaveFileModel::getData[" << this << "]: " << fromchannel << "," << tochannel << ", " << start << ", " << count << ", " << buffer << endl;
-#endif
-
-    int channels = getChannelCount();
-
-    if (fromchannel > tochannel) {
-        cerr << "ERROR: WaveFileModel::getData: fromchannel ("
-                  << fromchannel << ") > tochannel (" << tochannel << ")"
-                  << endl;
-        return 0;
-    }
-
-    if (tochannel >= channels) {
-        cerr << "ERROR: WaveFileModel::getData: tochannel ("
-                  << tochannel << ") >= channel count (" << channels << ")"
-                  << endl;
-        return 0;
-    }
-
-    if (fromchannel == tochannel) {
-        return getData(fromchannel, start, count, buffer[0]);
-    }
-
-    int reqchannels = (tochannel - fromchannel) + 1;
-
-    // Always read these directly from the file. 
-    // This is used for e.g. audio playback.
-    // Could be much more efficient (although compiler optimisation will help)
-
-    if (start >= m_startFrame) {
-        start -= m_startFrame;
-    } else {
-        for (int c = 0; c < reqchannels; ++c) {
-            for (sv_frame_t i = 0; i < count; ++i) buffer[c][i] = 0.f;
-        }
-        if (count <= m_startFrame - start) {
-            return 0;
-        } else {
-            count -= (m_startFrame - start);
-            start = 0;
-        }
-    }
-
-    if (!m_reader || !m_reader->isOK() || count == 0) {
-        for (int c = 0; c < reqchannels; ++c) {
-            for (sv_frame_t i = 0; i < count; ++i) buffer[c][i] = 0.f;
-        }
-        return 0;
-    }
-
-    SampleBlock frames = m_reader->getInterleavedFrames(start, count);
-
-    sv_frame_t i = 0;
-
-    sv_frame_t index = 0, available = frames.size();
-
-    while (i < count) {
-
-        if (index >= available) break;
-
-        int destc = 0;
-
-        for (int c = 0; c < channels; ++c) {
-            
-            if (c >= fromchannel && c <= tochannel) {
-                buffer[destc][i] = frames[index];
-                ++destc;
-            }
-
-            ++index;
-        }
-
-        ++i;
-    }
-
-    return i;
-}
-
-int
-WaveFileModel::getSummaryBlockSize(int desired) const
-{
-    int cacheType = 0;
-    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
-        return desired;
-    } else {
-        return roundedBlockSize;
-    }
-}    
-
-void
-WaveFileModel::getSummaries(int channel, sv_frame_t start, sv_frame_t count,
-                            RangeBlock &ranges, int &blockSize) const
-{
-    ranges.clear();
-    if (!isOK()) return;
-    ranges.reserve((count / blockSize) + 1);
-
-    if (start > m_startFrame) start -= m_startFrame;
-    else if (count <= m_startFrame - start) return;
-    else {
-        count -= (m_startFrame - start);
-        start = 0;
-    }
-
-    int cacheType = 0;
-    int power = m_zoomConstraint.getMinCachePower();
-    int roundedBlockSize = m_zoomConstraint.getNearestBlockSize
-        (blockSize, cacheType, power, ZoomConstraint::RoundDown);
-
-    int channels = getChannelCount();
-
-    if (cacheType != 0 && cacheType != 1) {
-
-	// We need to read directly from the file.  We haven't got
-	// this cached.  Hope the requested area is small.  This is
-	// not optimal -- we'll end up reading the same frames twice
-	// for stereo files, in two separate calls to this method.
-	// We could fairly trivially handle this for most cases that
-	// matter by putting a single cache in getInterleavedFrames
-	// for short queries.
-
-        m_directReadMutex.lock();
-
-        if (m_lastDirectReadStart != start ||
-            m_lastDirectReadCount != count ||
-            m_directRead.empty()) {
-
-            m_directRead = m_reader->getInterleavedFrames(start, count);
-            m_lastDirectReadStart = start;
-            m_lastDirectReadCount = count;
-        }
-
-	float max = 0.0, min = 0.0, total = 0.0;
-	sv_frame_t i = 0, got = 0;
-
-	while (i < count) {
-
-	    sv_frame_t index = i * channels + channel;
-	    if (index >= (sv_frame_t)m_directRead.size()) break;
-            
-	    float sample = m_directRead[index];
-            if (sample > max || got == 0) max = sample;
-	    if (sample < min || got == 0) min = sample;
-            total += fabsf(sample);
-
-	    ++i;
-            ++got;
-            
-            if (got == blockSize) {
-                ranges.push_back(Range(min, max, total / float(got)));
-                min = max = total = 0.0f;
-                got = 0;
-	    }
-	}
-
-        m_directReadMutex.unlock();
-
-	if (got > 0) {
-            ranges.push_back(Range(min, max, total / float(got)));
-	}
-
-	return;
-
-    } else {
-
-	QMutexLocker locker(&m_mutex);
-    
-	const RangeBlock &cache = m_cache[cacheType];
-
-        blockSize = roundedBlockSize;
-
-	sv_frame_t cacheBlock, div;
-        
-	if (cacheType == 0) {
-	    cacheBlock = (1 << m_zoomConstraint.getMinCachePower());
-            div = (1 << power) / cacheBlock;
-	} else {
-	    cacheBlock = sv_frame_t((1 << m_zoomConstraint.getMinCachePower()) * sqrt(2.) + 0.01);
-            div = sv_frame_t(((1 << power) * sqrt(2.) + 0.01) / double(cacheBlock));
-	}
-
-	sv_frame_t startIndex = start / cacheBlock;
-	sv_frame_t endIndex = (start + count) / cacheBlock;
-
-	float max = 0.0, min = 0.0, total = 0.0;
-	sv_frame_t i = 0, got = 0;
-
-#ifdef DEBUG_WAVE_FILE_MODEL
-	cerr << "blockSize is " << blockSize << ", cacheBlock " << cacheBlock << ", start " << start << ", count " << count << " (frame count " << getFrameCount() << "), power is " << power << ", div is " << div << ", startIndex " << startIndex << ", endIndex " << endIndex << endl;
-#endif
-
-	for (i = 0; i <= endIndex - startIndex; ) {
-        
-	    sv_frame_t index = (i + startIndex) * channels + channel;
-	    if (index >= (sv_frame_t)cache.size()) break;
-            
-            const Range &range = cache[index];
-            if (range.max() > max || got == 0) max = range.max();
-            if (range.min() < min || got == 0) min = range.min();
-            total += range.absmean();
-            
-	    ++i;
-            ++got;
-            
-	    if (got == div) {
-		ranges.push_back(Range(min, max, total / float(got)));
-                min = max = total = 0.0f;
-                got = 0;
-	    }
-	}
-		
-	if (got > 0) {
-            ranges.push_back(Range(min, max, total / float(got)));
-	}
-    }
-
-#ifdef DEBUG_WAVE_FILE_MODEL
-    SVDEBUG << "returning " << ranges.size() << " ranges" << endl;
-#endif
-    return;
-}
-
-WaveFileModel::Range
-WaveFileModel::getSummary(int channel, sv_frame_t start, sv_frame_t count) const
-{
-    Range range;
-    if (!isOK()) return range;
-
-    if (start > m_startFrame) start -= m_startFrame;
-    else if (count <= m_startFrame - start) return range;
-    else {
-        count -= (m_startFrame - start);
-        start = 0;
-    }
-
-    int blockSize;
-    for (blockSize = 1; blockSize <= count; blockSize *= 2);
-    if (blockSize > 1) blockSize /= 2;
-
-    bool first = false;
-
-    sv_frame_t blockStart = (start / blockSize) * blockSize;
-    sv_frame_t blockEnd = ((start + count) / blockSize) * blockSize;
-
-    if (blockStart < start) blockStart += blockSize;
-        
-    if (blockEnd > blockStart) {
-        RangeBlock ranges;
-        getSummaries(channel, blockStart, blockEnd - blockStart, ranges, blockSize);
-        for (int i = 0; i < (int)ranges.size(); ++i) {
-            if (first || ranges[i].min() < range.min()) range.setMin(ranges[i].min());
-            if (first || ranges[i].max() > range.max()) range.setMax(ranges[i].max());
-            if (first || ranges[i].absmean() < range.absmean()) range.setAbsmean(ranges[i].absmean());
-            first = false;
-        }
-    }
-
-    if (blockStart > start) {
-        Range startRange = getSummary(channel, start, blockStart - start);
-        range.setMin(std::min(range.min(), startRange.min()));
-        range.setMax(std::max(range.max(), startRange.max()));
-        range.setAbsmean(std::min(range.absmean(), startRange.absmean()));
-    }
-
-    if (blockEnd < start + count) {
-        Range endRange = getSummary(channel, blockEnd, start + count - blockEnd);
-        range.setMin(std::min(range.min(), endRange.min()));
-        range.setMax(std::max(range.max(), endRange.max()));
-        range.setAbsmean(std::min(range.absmean(), endRange.absmean()));
-    }
-
-    return range;
-}
-
-void
-WaveFileModel::fillCache()
-{
-    m_mutex.lock();
-
-    m_updateTimer = new QTimer(this);
-    connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(fillTimerTimedOut()));
-    m_updateTimer->start(100);
-
-    m_fillThread = new RangeCacheFillThread(*this);
-    connect(m_fillThread, SIGNAL(finished()), this, SLOT(cacheFilled()));
-
-    m_mutex.unlock();
-    m_fillThread->start();
-
-#ifdef DEBUG_WAVE_FILE_MODEL
-    SVDEBUG << "WaveFileModel::fillCache: started fill thread" << endl;
-#endif
-}   
-
-void
-WaveFileModel::fillTimerTimedOut()
-{
-    if (m_fillThread) {
-	sv_frame_t fillExtent = m_fillThread->getFillExtent();
-#ifdef DEBUG_WAVE_FILE_MODEL
-        SVDEBUG << "WaveFileModel::fillTimerTimedOut: extent = " << fillExtent << endl;
-#endif
-	if (fillExtent > m_lastFillExtent) {
-	    emit modelChangedWithin(m_lastFillExtent, fillExtent);
-	    m_lastFillExtent = fillExtent;
-	}
-    } else {
-#ifdef DEBUG_WAVE_FILE_MODEL
-        SVDEBUG << "WaveFileModel::fillTimerTimedOut: no thread" << endl;
-#endif
-	emit modelChanged();
-    }
-}
-
-void
-WaveFileModel::cacheFilled()
-{
-    m_mutex.lock();
-    delete m_fillThread;
-    m_fillThread = 0;
-    delete m_updateTimer;
-    m_updateTimer = 0;
-    m_mutex.unlock();
-    if (getEndFrame() > m_lastFillExtent) {
-        emit modelChangedWithin(m_lastFillExtent, getEndFrame());
-    }
-    emit modelChanged();
-    emit ready();
-#ifdef DEBUG_WAVE_FILE_MODEL
-    SVDEBUG << "WaveFileModel::cacheFilled" << endl;
-#endif
-}
-
-void
-WaveFileModel::RangeCacheFillThread::run()
-{
-    int cacheBlockSize[2];
-    cacheBlockSize[0] = (1 << m_model.m_zoomConstraint.getMinCachePower());
-    cacheBlockSize[1] = (int((1 << m_model.m_zoomConstraint.getMinCachePower()) *
-                                        sqrt(2.) + 0.01));
-    
-    sv_frame_t frame = 0;
-    const sv_frame_t readBlockSize = 16384;
-    SampleBlock block;
-
-    if (!m_model.isOK()) return;
-    
-    int channels = m_model.getChannelCount();
-    bool updating = m_model.m_reader->isUpdating();
-
-    if (updating) {
-        while (channels == 0 && !m_model.m_exiting) {
-//            SVDEBUG << "WaveFileModel::fill: Waiting for channels..." << endl;
-            sleep(1);
-            channels = m_model.getChannelCount();
-        }
-    }
-
-    Range *range = new Range[2 * channels];
-    float *means = new float[2 * channels];
-    int count[2];
-    count[0] = count[1] = 0;
-    for (int i = 0; i < 2 * channels; ++i) {
-        means[i] = 0.f;
-    }
-
-    bool first = true;
-
-    while (first || updating) {
-
-        updating = m_model.m_reader->isUpdating();
-        m_frameCount = m_model.getFrameCount();
-
-//        SVDEBUG << "WaveFileModel::fill: frame = " << frame << ", count = " << m_frameCount << endl;
-
-        while (frame < m_frameCount) {
-
-//            SVDEBUG << "WaveFileModel::fill inner loop: frame = " << frame << ", count = " << m_frameCount << ", blocksize " << readBlockSize << endl;
-
-            if (updating && (frame + readBlockSize > m_frameCount)) break;
-
-            block = m_model.m_reader->getInterleavedFrames(frame, readBlockSize);
-
-//            cerr << "block is " << block.size() << endl;
-
-            for (sv_frame_t i = 0; i < readBlockSize; ++i) {
-		
-                if (channels * i + channels > (int)block.size()) break;
-
-                for (int ch = 0; ch < channels; ++ch) {
-
-                    sv_frame_t index = channels * i + ch;
-                    float sample = block[index];
-                    
-                    for (int cacheType = 0; cacheType < 2; ++cacheType) { // cache type
-                        
-                        sv_frame_t rangeIndex = ch * 2 + cacheType;
-                        range[rangeIndex].sample(sample);
-                        means[rangeIndex] += fabsf(sample);
-                    }
-                }
-
-                //!!! this looks like a ludicrous way to do synchronisation
-                QMutexLocker locker(&m_model.m_mutex);
-
-                for (int cacheType = 0; cacheType < 2; ++cacheType) {
-
-                    if (++count[cacheType] == cacheBlockSize[cacheType]) {
-                        
-                        for (int ch = 0; ch < int(channels); ++ch) {
-                            int rangeIndex = ch * 2 + cacheType;
-                            means[rangeIndex] = means[rangeIndex] / float(count[cacheType]);
-                            range[rangeIndex].setAbsmean(means[rangeIndex]);
-                            m_model.m_cache[cacheType].push_back(range[rangeIndex]);
-                            range[rangeIndex] = Range();
-                            means[rangeIndex] = 0.f;
-                        }
-
-                        count[cacheType] = 0;
-                    }
-                }
-                
-                ++frame;
-            }
-            
-            if (m_model.m_exiting) break;
-            
-            m_fillExtent = frame;
-        }
-
-//        cerr << "WaveFileModel: inner loop ended" << endl;
-
-        first = false;
-        if (m_model.m_exiting) break;
-        if (updating) {
-//            cerr << "sleeping..." << endl;
-            sleep(1);
-        }
-    }
-
-    if (!m_model.m_exiting) {
-
-        QMutexLocker locker(&m_model.m_mutex);
-
-        for (int cacheType = 0; cacheType < 2; ++cacheType) {
-
-            if (count[cacheType] > 0) {
-
-                for (int ch = 0; ch < int(channels); ++ch) {
-                    int rangeIndex = ch * 2 + cacheType;
-                    means[rangeIndex] = means[rangeIndex] / float(count[cacheType]);
-                    range[rangeIndex].setAbsmean(means[rangeIndex]);
-                    m_model.m_cache[cacheType].push_back(range[rangeIndex]);
-                    range[rangeIndex] = Range();
-                    means[rangeIndex] = 0.f;
-                }
-
-                count[cacheType] = 0;
-            }
-            
-            const Range &rr = *m_model.m_cache[cacheType].begin();
-            MUNLOCK(&rr, m_model.m_cache[cacheType].capacity() * sizeof(Range));
-        }
-    }
-    
-    delete[] means;
-    delete[] range;
-
-    m_fillExtent = m_frameCount;
-
-#ifdef DEBUG_WAVE_FILE_MODEL        
-    for (int cacheType = 0; cacheType < 2; ++cacheType) {
-        cerr << "Cache type " << cacheType << " now contains " << m_model.m_cache[cacheType].size() << " ranges" << endl;
-    }
-#endif
-}
-
-void
-WaveFileModel::toXml(QTextStream &out,
-                     QString indent,
-                     QString extraAttributes) const
-{
-    Model::toXml(out, indent,
-                 QString("type=\"wavefile\" file=\"%1\" %2")
-                 .arg(encodeEntities(m_path)).arg(extraAttributes));
-}
-
-    
--- a/data/model/WaveFileModel.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/WaveFileModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,120 +13,36 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _WAVE_FILE_MODEL_H_
-#define _WAVE_FILE_MODEL_H_
-
-#include "base/Thread.h"
-#include <QMutex>
-#include <QTimer>
-
-#include "data/fileio/FileSource.h"
+#ifndef WAVE_FILE_MODEL_H
+#define WAVE_FILE_MODEL_H
 
 #include "RangeSummarisableTimeValueModel.h"
-#include "PowerOfSqrtTwoZoomConstraint.h"
 
 #include <stdlib.h>
 
-class AudioFileReader;
-
 class WaveFileModel : public RangeSummarisableTimeValueModel
 {
     Q_OBJECT
 
 public:
-    WaveFileModel(FileSource source, sv_samplerate_t targetRate = 0);
-    WaveFileModel(FileSource source, AudioFileReader *reader);
-    ~WaveFileModel();
+    virtual ~WaveFileModel();
 
-    bool isOK() const;
-    bool isReady(int *) const;
+    virtual sv_frame_t getFrameCount() const = 0;
+    virtual int getChannelCount() const = 0;
+    virtual sv_samplerate_t getSampleRate() const = 0;
+    virtual sv_samplerate_t getNativeRate() const = 0;
 
-    const ZoomConstraint *getZoomConstraint() const { return &m_zoomConstraint; }
+    virtual QString getTitle() const = 0;
+    virtual QString getMaker() const = 0;
+    virtual QString getLocation() const = 0;
 
-    sv_frame_t getFrameCount() const;
-    int getChannelCount() const;
-    sv_samplerate_t getSampleRate() const;
-    sv_samplerate_t getNativeRate() const;
+    virtual sv_frame_t getStartFrame() const = 0;
+    virtual sv_frame_t getEndFrame() const = 0;
 
-    QString getTitle() const;
-    QString getMaker() const;
-    QString getLocation() const;
+    virtual void setStartFrame(sv_frame_t startFrame) = 0;
 
-    QString getLocalFilename() const;
-
-    float getValueMinimum() const { return -1.0f; }
-    float getValueMaximum() const { return  1.0f; }
-
-    virtual sv_frame_t getStartFrame() const { return m_startFrame; }
-    virtual sv_frame_t getEndFrame() const { return m_startFrame + getFrameCount(); }
-
-    void setStartFrame(sv_frame_t startFrame) { m_startFrame = startFrame; }
-
-    virtual sv_frame_t getData(int channel, sv_frame_t start, sv_frame_t count,
-                               float *buffer) const;
-
-    virtual sv_frame_t getMultiChannelData(int fromchannel, int tochannel,
-                                           sv_frame_t start, sv_frame_t count,
-                                           float **buffers) const;
-
-    virtual int getSummaryBlockSize(int desired) const;
-
-    virtual void getSummaries(int channel, sv_frame_t start, sv_frame_t count,
-                              RangeBlock &ranges,
-                              int &blockSize) const;
-
-    virtual Range getSummary(int channel, sv_frame_t start, sv_frame_t count) const;
-
-    QString getTypeName() const { return tr("Wave File"); }
-
-    virtual void toXml(QTextStream &out,
-                       QString indent = "",
-                       QString extraAttributes = "") const;
-
-protected slots:
-    void fillTimerTimedOut();
-    void cacheFilled();
-    
 protected:
-    void initialize();
-
-    class RangeCacheFillThread : public Thread
-    {
-    public:
-        RangeCacheFillThread(WaveFileModel &model) :
-	    m_model(model), m_fillExtent(0),
-            m_frameCount(model.getFrameCount()) { }
-    
-	sv_frame_t getFillExtent() const { return m_fillExtent; }
-        virtual void run();
-
-    protected:
-        WaveFileModel &m_model;
-	sv_frame_t m_fillExtent;
-        sv_frame_t m_frameCount;
-    };
-         
-    void fillCache();
-
-    FileSource m_source;
-    QString m_path;
-    AudioFileReader *m_reader;
-    bool m_myReader;
-
-    sv_frame_t m_startFrame;
-
-    RangeBlock m_cache[2]; // interleaved at two base resolutions
-    mutable QMutex m_mutex;
-    RangeCacheFillThread *m_fillThread;
-    QTimer *m_updateTimer;
-    sv_frame_t m_lastFillExtent;
-    bool m_exiting;
-    static PowerOfSqrtTwoZoomConstraint m_zoomConstraint;
-
-    mutable SampleBlock m_directRead;
-    mutable sv_frame_t m_lastDirectReadStart;
-    mutable sv_frame_t m_lastDirectReadCount;
-    mutable QMutex m_directReadMutex;
+    WaveFileModel() { } // only accessible from subclasses
 };    
 
 #endif
--- a/data/model/WritableWaveFileModel.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/WritableWaveFileModel.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -15,6 +15,8 @@
 
 #include "WritableWaveFileModel.h"
 
+#include "ReadOnlyWaveFileModel.h"
+
 #include "base/TempDirectory.h"
 #include "base/Exceptions.h"
 
@@ -28,6 +30,10 @@
 #include <iostream>
 #include <stdint.h>
 
+using namespace std;
+
+const int WritableWaveFileModel::PROPORTION_UNKNOWN = -1;
+
 //#define DEBUG_WRITABLE_WAVE_FILE_MODEL 1
 
 WritableWaveFileModel::WritableWaveFileModel(sv_samplerate_t sampleRate,
@@ -40,7 +46,7 @@
     m_channels(channels),
     m_frameCount(0),
     m_startFrame(0),
-    m_completion(0)
+    m_proportion(PROPORTION_UNKNOWN)
 {
     if (path.isEmpty()) {
         try {
@@ -74,7 +80,7 @@
         return;
     }
     
-    m_model = new WaveFileModel(source, m_reader);
+    m_model = new ReadOnlyWaveFileModel(source, m_reader);
     if (!m_model->isOK()) {
         cerr << "WritableWaveFileModel: Error in creating wave file model" << endl;
         delete m_model;
@@ -105,7 +111,7 @@
 }
 
 bool
-WritableWaveFileModel::addSamples(float **samples, sv_frame_t count)
+WritableWaveFileModel::addSamples(const float *const *samples, sv_frame_t count)
 {
     if (!m_writer) return false;
 
@@ -114,30 +120,27 @@
 #endif
 
     if (!m_writer->writeSamples(samples, count)) {
-        cerr << "ERROR: WritableWaveFileModel::addSamples: writer failed: " << m_writer->getError() << endl;
+        SVCERR << "ERROR: WritableWaveFileModel::addSamples: writer failed: " << m_writer->getError() << endl;
         return false;
     }
 
     m_frameCount += count;
 
-    static int updateCounter = 0;
-
     if (m_reader && m_reader->getChannelCount() == 0) {
-#ifdef DEBUG_WRITABLE_WAVE_FILE_MODEL
-        SVDEBUG << "WritableWaveFileModel::addSamples(" << count << "): calling updateFrameCount (initial)" << endl;
-#endif
         m_reader->updateFrameCount();
-    } else if (++updateCounter == 100) {
-#ifdef DEBUG_WRITABLE_WAVE_FILE_MODEL
-        SVDEBUG << "WritableWaveFileModel::addSamples(" << count << "): calling updateFrameCount (periodic)" << endl;
-#endif
-        if (m_reader) m_reader->updateFrameCount();
-        updateCounter = 0;
     }
 
     return true;
 }
 
+void
+WritableWaveFileModel::updateModel()
+{
+    if (m_reader) {
+        m_reader->updateFrameCount();
+    }
+}
+
 bool
 WritableWaveFileModel::isOK() const
 {
@@ -149,17 +152,31 @@
 bool
 WritableWaveFileModel::isReady(int *completion) const
 {
-    if (completion) *completion = m_completion;
-    return (m_completion == 100);
+    int c = getCompletion();
+    if (completion) *completion = c;
+    if (!isOK()) return false;
+    return (c == 100);
 }
 
 void
-WritableWaveFileModel::setCompletion(int completion)
+WritableWaveFileModel::setWriteProportion(int proportion)
 {
-    m_completion = completion;
-    if (completion == 100) {
-        if (m_reader) m_reader->updateDone();
-    }
+    m_proportion = proportion;
+}
+
+int
+WritableWaveFileModel::getWriteProportion() const
+{
+    return m_proportion;
+}
+
+void
+WritableWaveFileModel::writeComplete()
+{
+    m_writer->close();
+    if (m_reader) m_reader->updateDone();
+    m_proportion = 100;
+    emit modelChanged();
 }
 
 sv_frame_t
@@ -169,21 +186,19 @@
     return m_frameCount;
 }
 
-sv_frame_t
-WritableWaveFileModel::getData(int channel, sv_frame_t start, sv_frame_t count,
-                               float *buffer) const
+floatvec_t
+WritableWaveFileModel::getData(int channel, sv_frame_t start, sv_frame_t count) const
 {
-    if (!m_model || m_model->getChannelCount() == 0) return 0;
-    return m_model->getData(channel, start, count, buffer);
+    if (!m_model || m_model->getChannelCount() == 0) return {};
+    return m_model->getData(channel, start, count);
 }
 
-sv_frame_t
+vector<floatvec_t>
 WritableWaveFileModel::getMultiChannelData(int fromchannel, int tochannel,
-                                           sv_frame_t start, sv_frame_t count,
-                                           float **buffers) const
+                                           sv_frame_t start, sv_frame_t count) const
 {
-    if (!m_model || m_model->getChannelCount() == 0) return 0;
-    return m_model->getMultiChannelData(fromchannel, tochannel, start, count, buffers);
+    if (!m_model || m_model->getChannelCount() == 0) return {};
+    return m_model->getMultiChannelData(fromchannel, tochannel, start, count);
 }    
 
 int
@@ -215,15 +230,16 @@
                              QString indent,
                              QString extraAttributes) const
 {
-    // We don't actually write the data to XML.  We just write a brief
-    // description of the model.  Any code that uses this class is
-    // going to need to be aware that it will have to make separate
-    // arrangements for the audio file itself.
+    // The assumption here is that the underlying wave file has
+    // already been saved somewhere (its location is available through
+    // getLocation()) and that the code that uses this class is
+    // dealing with the problem of making sure it remains available.
+    // We just write this out as if it were a normal wave file.
 
     Model::toXml
         (out, indent,
-         QString("type=\"writablewavefile\" file=\"%1\" channels=\"%2\" %3")
+         QString("type=\"wavefile\" file=\"%1\" subtype=\"writable\" %2")
          .arg(encodeEntities(m_writer->getPath()))
-         .arg(m_model->getChannelCount()).arg(extraAttributes));
+         .arg(extraAttributes));
 }
 
--- a/data/model/WritableWaveFileModel.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/WritableWaveFileModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -13,15 +13,17 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _WRITABLE_WAVE_FILE_MODEL_H_
-#define _WRITABLE_WAVE_FILE_MODEL_H_
+#ifndef WRITABLE_WAVE_FILE_MODEL_H
+#define WRITABLE_WAVE_FILE_MODEL_H
 
 #include "WaveFileModel.h"
+#include "ReadOnlyWaveFileModel.h"
+#include "PowerOfSqrtTwoZoomConstraint.h"
 
 class WavFileWriter;
 class WavFileReader;
 
-class WritableWaveFileModel : public RangeSummarisableTimeValueModel
+class WritableWaveFileModel : public WaveFileModel
 {
     Q_OBJECT
 
@@ -31,17 +33,74 @@
 
     /**
      * Call addSamples to append a block of samples to the end of the
-     * file.  Caller should also call setCompletion to update the
-     * progress of this file, if it has a known end point, and should
-     * call setCompletion(100) when the file has been written.
+     * file.
+     *
+     * This function only appends the samples to the file being
+     * written; it does not update the model's view of the samples in
+     * that file. That is, it updates the file on disc but the model
+     * itself does not change its content. This is because re-reading
+     * the file to update the model may be more expensive than adding
+     * the samples in the first place. If you are writing small
+     * numbers of samples repeatedly, you probably only want the model
+     * to update periodically rather than after every write.
+     *
+     * Call updateModel() periodically to tell the model to update its
+     * own view of the samples in the file being written.
+     *
+     * Call setWriteProportion() periodically if the file being
+     * written has known duration and you want the model to be able to
+     * report the write progress as a percentage.
+     *
+     * Call writeComplete() when the file has been completely written.
      */
-    virtual bool addSamples(float **samples, sv_frame_t count);
+    virtual bool addSamples(const float *const *samples, sv_frame_t count);
+
+    /**
+     * Tell the model to update its own (read) view of the (written)
+     * file. May cause modelChanged() and modelChangedWithin() to be
+     * emitted. See the comment to addSamples above for rationale.
+     */
+    void updateModel();
+    
+    /**
+     * Set the proportion of the file which has been written so far,
+     * as a percentage. This may be used to indicate progress.
+     *
+     * Note that this differs from the "completion" percentage
+     * reported through isReady()/getCompletion(). That percentage is
+     * updated when "internal processing has advanced... but the model
+     * has not changed externally", i.e. it reports progress in
+     * calculating the initial state of a model. In contrast, an
+     * update to setWriteProportion corresponds to a change in the
+     * externally visible state of the model (i.e. it contains more
+     * data than before).
+     */
+    void setWriteProportion(int proportion);
+
+    /**
+     * Indicate that writing is complete. You should call this even if
+     * you have never called setWriteProportion() or updateModel().
+     */
+    void writeComplete();
+
+    static const int PROPORTION_UNKNOWN;
+    
+    /**
+     * Get the proportion of the file which has been written so far,
+     * as a percentage. Return PROPORTION_UNKNOWN if unknown.
+     */
+    int getWriteProportion() const;
     
     bool isOK() const;
     bool isReady(int *) const;
-
-    virtual void setCompletion(int completion); // percentage
-    virtual int getCompletion() const { return m_completion; }
+    
+    /**
+     * Return the generation completion percentage of this model. This
+     * is always 100, because the model is always in a complete state
+     * -- it just contains varying amounts of data depending on how
+     * much has been written.
+     */
+    virtual int getCompletion() const { return 100; }
 
     const ZoomConstraint *getZoomConstraint() const {
         static PowerOfSqrtTwoZoomConstraint zc;
@@ -51,6 +110,20 @@
     sv_frame_t getFrameCount() const;
     int getChannelCount() const { return m_channels; }
     sv_samplerate_t getSampleRate() const { return m_sampleRate; }
+    sv_samplerate_t getNativeRate() const { return m_sampleRate; }
+
+    QString getTitle() const {
+        if (m_model) return m_model->getTitle();
+        else return "";
+    } 
+    QString getMaker() const {
+        if (m_model) return m_model->getMaker();
+        else return "";
+    }
+    QString getLocation() const {
+        if (m_model) return m_model->getLocation();
+        else return "";
+    }
 
     float getValueMinimum() const { return -1.0f; }
     float getValueMaximum() const { return  1.0f; }
@@ -60,12 +133,9 @@
 
     void setStartFrame(sv_frame_t startFrame);
 
-    virtual sv_frame_t getData(int channel, sv_frame_t start, sv_frame_t count,
-                               float *buffer) const;
+    virtual floatvec_t getData(int channel, sv_frame_t start, sv_frame_t count) const;
 
-    virtual sv_frame_t getMultiChannelData(int fromchannel, int tochannel,
-                                           sv_frame_t start, sv_frame_t count,
-                                           float **buffer) const;
+    virtual std::vector<floatvec_t> getMultiChannelData(int fromchannel, int tochannel, sv_frame_t start, sv_frame_t count) const;
 
     virtual int getSummaryBlockSize(int desired) const;
 
@@ -81,14 +151,14 @@
                        QString extraAttributes = "") const;
 
 protected:
-    WaveFileModel *m_model;
+    ReadOnlyWaveFileModel *m_model;
     WavFileWriter *m_writer;
     WavFileReader *m_reader;
     sv_samplerate_t m_sampleRate;
     int m_channels;
     sv_frame_t m_frameCount;
     sv_frame_t m_startFrame;
-    int m_completion;
+    int m_proportion;
 };
 
 #endif
--- a/data/model/test/MockWaveModel.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/test/MockWaveModel.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -26,40 +26,39 @@
     }
 }
 
-sv_frame_t
-MockWaveModel::getData(int channel, sv_frame_t start, sv_frame_t count,
-		       float *buffer) const
+floatvec_t
+MockWaveModel::getData(int channel, sv_frame_t start, sv_frame_t count) const
 {
     sv_frame_t i = 0;
 
-    cerr << "MockWaveModel::getData(" << channel << "," << start << "," << count << "): ";
+//    cerr << "MockWaveModel::getData(" << channel << "," << start << "," << count << "): ";
 
+    floatvec_t data;
+    
     while (i < count) {
 	sv_frame_t idx = start + i;
 	if (!in_range_for(m_data[channel], idx)) break;
-	buffer[i] = m_data[channel][idx];
-	cerr << buffer[i] << " ";
+	data.push_back(m_data[channel][idx]);
+//	cerr << data[i] << " ";
 	++i;
     }
 
-    cerr << endl;
+//    cerr << endl;
     
-    return i;
+    return data;
 }
 
-sv_frame_t
+vector<floatvec_t>
 MockWaveModel::getMultiChannelData(int fromchannel, int tochannel,
-				   sv_frame_t start, sv_frame_t count,
-				   float **buffers) const
+				   sv_frame_t start, sv_frame_t count) const
 {
-    sv_frame_t min = count;
-
+    vector<floatvec_t> data(tochannel - fromchannel + 1);
+    
     for (int c = fromchannel; c <= tochannel; ++c) {
-	sv_frame_t n = getData(c, start, count, buffers[c]);
-	if (n < min) min = n;
+        data.push_back(getData(c, start, count));
     }
 
-    return min;
+    return data;
 }
 
 vector<float>
--- a/data/model/test/MockWaveModel.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/test/MockWaveModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -41,11 +41,8 @@
     virtual float getValueMaximum() const { return  1.f; }
     virtual int getChannelCount() const { return int(m_data.size()); }
     
-    virtual sv_frame_t getData(int channel, sv_frame_t start, sv_frame_t count,
-                               float *buffer) const;
-    virtual sv_frame_t getMultiChannelData(int fromchannel, int tochannel,
-					   sv_frame_t start, sv_frame_t count,
-					   float **buffers) const;
+    virtual floatvec_t getData(int channel, sv_frame_t start, sv_frame_t count) const;
+    virtual std::vector<floatvec_t> getMultiChannelData(int fromchannel, int tochannel, sv_frame_t start, sv_frame_t count) const;
 
     virtual bool canPlay() const { return true; }
     virtual QString getDefaultPlayClipId() const { return ""; }
--- a/data/model/test/TestFFTModel.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/test/TestFFTModel.h	Fri Jan 13 10:29:44 2017 +0000
@@ -40,24 +40,35 @@
               int columnNo, vector<vector<complex<float>>> expectedValues,
               int expectedWidth) {
         for (int ch = 0; in_range_for(expectedValues, ch); ++ch) {
-            for (int polar = 0; polar <= 1; ++polar) {
-                FFTModel fftm(model, ch, window, windowSize, windowIncrement,
-                              fftSize, bool(polar));
-                QCOMPARE(fftm.getWidth(), expectedWidth);
-                int hs1 = fftSize/2 + 1;
-                QCOMPARE(fftm.getHeight(), hs1);
-                vector<float> reals(hs1 + 1, 0.f);
-                vector<float> imags(hs1 + 1, 0.f);
-                reals[hs1] = 999.f; // overrun guards
-                imags[hs1] = 999.f;
+            FFTModel fftm(model, ch, window, windowSize, windowIncrement, fftSize);
+            QCOMPARE(fftm.getWidth(), expectedWidth);
+            int hs1 = fftSize/2 + 1;
+            QCOMPARE(fftm.getHeight(), hs1);
+            vector<float> reals(hs1 + 1, 0.f);
+            vector<float> imags(hs1 + 1, 0.f);
+            reals[hs1] = 999.f; // overrun guards
+            imags[hs1] = 999.f;
+            for (int stepThrough = 0; stepThrough <= 1; ++stepThrough) {
+                if (stepThrough) {
+                    // Read through the columns in order instead of
+                    // randomly accessing the one we want. This is to
+                    // exercise the case where the FFT model saves
+                    // part of each input frame and moves along by
+                    // only the non-overlapping distance
+                    for (int sc = 0; sc < columnNo; ++sc) {
+                        fftm.getValuesAt(sc, &reals[0], &imags[0]);
+                    }
+                }
                 fftm.getValuesAt(columnNo, &reals[0], &imags[0]);
                 for (int i = 0; i < hs1; ++i) {
                     float eRe = expectedValues[ch][i].real();
                     float eIm = expectedValues[ch][i].imag();
-                    if (reals[i] != eRe || imags[i] != eIm) {
-                        cerr << "NOTE: output is not as expected for column "
-                             << i << " in channel " << ch << " (polar store = "
-                             << polar << ")" << endl;
+                    float thresh = 1e-5f;
+                    if (abs(reals[i] - eRe) > thresh ||
+                        abs(imags[i] - eIm) > thresh) {
+                        cerr << "ERROR: output is not as expected for column "
+                             << i << " in channel " << ch << " (stepThrough = "
+                             << stepThrough << ")" << endl;
                         cerr << "expected : ";
                         for (int j = 0; j < hs1; ++j) {
                             cerr << expectedValues[ch][j] << " ";
@@ -76,7 +87,7 @@
             }
         }
     }
-    
+
 private slots:
 
     // NB. FFTModel columns are centred on the sample frame, and in
@@ -88,7 +99,7 @@
     // (rather than something with a step in it that is harder to
     // reason about the FFT of) and the results for subsequent columns
     // are those of our expected signal.
-
+    
     void dc_simple_rect() {
 	MockWaveModel mwm({ DC }, 16, 4);
         test(&mwm, RectangularWindow, 8, 8, 8, 0,
@@ -98,7 +109,7 @@
         test(&mwm, RectangularWindow, 8, 8, 8, 2,
              { { { 4.f, 0.f }, {}, {}, {}, {} } }, 4);
         test(&mwm, RectangularWindow, 8, 8, 8, 3,
-             { { { }, {}, {}, {}, {} } }, 4);
+             { { {}, {}, {}, {}, {} } }, 4);
     }
 
     void dc_simple_hann() {
@@ -112,7 +123,131 @@
         test(&mwm, HanningWindow, 8, 8, 8, 2,
              { { { 4.f, 0.f }, { 2.f, 0.f }, {}, {}, {} } }, 4);
         test(&mwm, HanningWindow, 8, 8, 8, 3,
-             { { { }, {}, {}, {}, {} } }, 4);
+             { { {}, {}, {}, {}, {} } }, 4);
+    }
+    
+    void dc_simple_hann_halfoverlap() {
+	MockWaveModel mwm({ DC }, 16, 4);
+        test(&mwm, HanningWindow, 8, 4, 8, 0,
+             { { {}, {}, {}, {}, {} } }, 7);
+        test(&mwm, HanningWindow, 8, 4, 8, 2,
+             { { { 4.f, 0.f }, { 2.f, 0.f }, {}, {}, {} } }, 7);
+        test(&mwm, HanningWindow, 8, 4, 8, 3,
+             { { { 4.f, 0.f }, { 2.f, 0.f }, {}, {}, {} } }, 7);
+        test(&mwm, HanningWindow, 8, 4, 8, 6,
+             { { {}, {}, {}, {}, {} } }, 7);
+    }
+    
+    void sine_simple_rect() {
+	MockWaveModel mwm({ Sine }, 16, 4);
+        // Sine: output is purely imaginary. Note the sign is flipped
+        // (normally the first half of the output would have negative
+        // sign for a sine starting at 0) because the model does an
+        // FFT shift to centre the phase
+        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+             { { {}, {}, {}, {}, {} } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+             { { {}, { 0.f, 2.f }, {}, {}, {} } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+             { { {}, { 0.f, 2.f }, {}, {}, {} } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+             { { {}, {}, {}, {}, {} } }, 4);
+    }
+    
+    void cosine_simple_rect() {
+	MockWaveModel mwm({ Cosine }, 16, 4);
+        // Cosine: output is purely real. Note the sign is flipped
+        // because the model does an FFT shift to centre the phase
+        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+             { { {}, {}, {}, {}, {} } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+             { { {}, { -2.f, 0.f }, {}, {}, {} } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+             { { {}, { -2.f, 0.f }, {}, {}, {} } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+             { { {}, {}, {}, {}, {} } }, 4);
+    }
+    
+    void twochan_simple_rect() {
+	MockWaveModel mwm({ Sine, Cosine }, 16, 4);
+        // Test that the two channels are read and converted separately
+        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+             {
+                 { {}, {}, {}, {}, {} },
+                 { {}, {}, {}, {}, {} }
+             }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+             {
+                 { {}, {  0.f, 2.f }, {}, {}, {} },
+                 { {}, { -2.f, 0.f }, {}, {}, {} }
+             }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+             {
+                 { {}, {  0.f, 2.f }, {}, {}, {} },
+                 { {}, { -2.f, 0.f }, {}, {}, {} }
+             }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+             {
+                 { {}, {}, {}, {}, {} },
+                 { {}, {}, {}, {}, {} }
+             }, 4);
+    }
+    
+    void nyquist_simple_rect() {
+	MockWaveModel mwm({ Nyquist }, 16, 4);
+        // Again, the sign is flipped. This has the same amount of
+        // energy as the DC example
+        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+             { { {}, {}, {}, {}, {} } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+             { { {}, {}, {}, {}, { -4.f, 0.f } } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+             { { {}, {}, {}, {}, { -4.f, 0.f } } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+             { { {}, {}, {}, {}, {} } }, 4);
+    }
+    
+    void dirac_simple_rect() {
+	MockWaveModel mwm({ Dirac }, 16, 4);
+        // The window scales by 0.5 and some signs are flipped. Only
+        // column 1 has any data (the single impulse).
+        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+             { { {}, {}, {}, {}, {} } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+             { { { 0.5f, 0.f }, { -0.5f, 0.f }, { 0.5f, 0.f }, { -0.5f, 0.f }, { 0.5f, 0.f } } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+             { { {}, {}, {}, {}, {} } }, 4);
+        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+             { { {}, {}, {}, {}, {} } }, 4);
+    }
+    
+    void dirac_simple_rect_2() {
+	MockWaveModel mwm({ Dirac }, 16, 8);
+        // With 8 samples padding, the FFT shift places the first
+        // Dirac impulse at the start of column 1, thus giving all
+        // positive values
+        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+             { { {}, {}, {}, {}, {} } }, 5);
+        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+             { { { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f } } }, 5);
+        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+             { { {}, {}, {}, {}, {} } }, 5);
+        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+             { { {}, {}, {}, {}, {} } }, 5);
+        test(&mwm, RectangularWindow, 8, 8, 8, 4,
+             { { {}, {}, {}, {}, {} } }, 5);
+    }
+
+    void dirac_simple_rect_halfoverlap() {
+	MockWaveModel mwm({ Dirac }, 16, 4);
+        test(&mwm, RectangularWindow, 8, 4, 8, 0,
+             { { {}, {}, {}, {}, {} } }, 7);
+        test(&mwm, RectangularWindow, 8, 4, 8, 1,
+             { { { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f } } }, 7);
+        test(&mwm, RectangularWindow, 8, 4, 8, 2,
+             { { { 0.5f, 0.f }, { -0.5f, 0.f }, { 0.5f, 0.f }, { -0.5f, 0.f }, { 0.5f, 0.f } } }, 7);
+        test(&mwm, RectangularWindow, 8, 4, 8, 3,
+             { { {}, {}, {}, {}, {} } }, 7);
     }
     
 };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/test/files.pri	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,8 @@
+TEST_HEADERS += \
+	Compares.h \
+	MockWaveModel.h \
+	TestFFTModel.h
+	
+TEST_SOURCES += \
+	MockWaveModel.cpp \
+	svcore-data-model-test.cpp
--- a/data/model/test/main.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-/* -*- 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 "TestFFTModel.h"
-
-#include <QtTest>
-
-#include <iostream>
-
-using namespace std;
-
-int main(int argc, char *argv[])
-{
-    int good = 0, bad = 0;
-
-    QCoreApplication app(argc, argv);
-    app.setOrganizationName("Sonic Visualiser");
-    app.setApplicationName("test-model");
-
-    {
-	TestFFTModel t;
-	if (QTest::qExec(&t, argc, argv) == 0) ++good;
-	else ++bad;
-    }
-
-    if (bad > 0) {
-	cerr << "\n********* " << bad << " test suite(s) failed!\n" << endl;
-	return 1;
-    } else {
-	cerr << "All tests passed" << endl;
-	return 0;
-    }
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/test/svcore-data-model-test.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,44 @@
+/* -*- 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 "TestFFTModel.h"
+
+#include <QtTest>
+
+#include <iostream>
+
+using namespace std;
+
+int main(int argc, char *argv[])
+{
+    int good = 0, bad = 0;
+
+    QCoreApplication app(argc, argv);
+    app.setOrganizationName("Sonic Visualiser");
+    app.setApplicationName("test-model");
+
+    {
+	TestFFTModel t;
+	if (QTest::qExec(&t, argc, argv) == 0) ++good;
+	else ++bad;
+    }
+
+    if (bad > 0) {
+	cerr << "\n********* " << bad << " test suite(s) failed!\n" << endl;
+	return 1;
+    } else {
+	cerr << "All tests passed" << endl;
+	return 0;
+    }
+}
+
--- a/data/model/test/test.pro	Mon Nov 21 16:32:58 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-
-TEMPLATE = app
-
-LIBS += -L../../.. -L../../../../dataquay -L../../../release -L../../../../dataquay/release -lsvcore -ldataquay
-
-win32-g++ {
-    INCLUDEPATH += ../../../../sv-dependency-builds/win32-mingw/include
-    LIBS += -L../../../../sv-dependency-builds/win32-mingw/lib
-}
-win32-msvc* {
-    INCLUDEPATH += ../../../../sv-dependency-builds/win32-msvc/include
-    LIBS += -L../../../../sv-dependency-builds/win32-msvc/lib
-}
-mac* {
-    INCLUDEPATH += ../../../../sv-dependency-builds/osx/include
-    LIBS += -L../../../../sv-dependency-builds/osx/lib
-}
-
-exists(../../../config.pri) {
-    include(../../../config.pri)
-}
-
-!exists(../../../config.pri) {
-
-    CONFIG += release
-    DEFINES += NDEBUG BUILD_RELEASE NO_TIMING
-
-    DEFINES += HAVE_BZ2 HAVE_FFTW3 HAVE_FFTW3F HAVE_SNDFILE HAVE_SAMPLERATE HAVE_VAMP HAVE_VAMPHOSTSDK HAVE_DATAQUAY HAVE_LIBLO HAVE_MAD HAVE_ID3TAG HAVE_PORTAUDIO_2_0
-
-    LIBS += -lbz2 -lvamp-hostsdk -lfftw3 -lfftw3f -lsndfile -lFLAC -logg -lvorbis -lvorbisenc -lvorbisfile -logg -lmad -lid3tag -lportaudio -lsamplerate -lz -lsord-0 -lserd-0
-
-    win* {
-        LIBS += -llo -lwinmm -lws2_32
-    }
-    macx* {
-        DEFINES += HAVE_COREAUDIO
-        LIBS += -framework CoreAudio -framework CoreMidi -framework AudioUnit -framework AudioToolbox -framework CoreFoundation -framework CoreServices -framework Accelerate
-    }
-}
-
-CONFIG += qt thread warn_on stl rtti exceptions console c++11
-QT += network xml testlib
-QT -= gui
-
-TARGET = svcore-data-model-test
-
-DEPENDPATH += ../../..
-INCLUDEPATH += ../../..
-OBJECTS_DIR = o
-MOC_DIR = o
-
-HEADERS += Compares.h MockWaveModel.h TestFFTModel.h
-SOURCES += MockWaveModel.cpp main.cpp
-
-win* {
-//PRE_TARGETDEPS += ../../../svcore.lib
-}
-!win* {
-PRE_TARGETDEPS += ../../../libsvcore.a
-}
-
-!win32 {
-    !macx* {
-        QMAKE_POST_LINK=./$${TARGET}
-    }
-    macx* {
-        QMAKE_POST_LINK=./$${TARGET}.app/Contents/MacOS/$${TARGET}
-    }
-}
-
-win32:QMAKE_POST_LINK=./release/$${TARGET}.exe
-
--- a/data/osc/OSCQueue.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/osc/OSCQueue.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -23,12 +23,13 @@
 #include "base/Profiler.h"
 
 #include <iostream>
-#include <unistd.h>
 
 #define OSC_MESSAGE_QUEUE_SIZE 1023
 
 #ifdef HAVE_LIBLO
 
+#include <unistd.h>
+
 void
 OSCQueue::oscError(int num, const char *msg, const char *path)
 {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/files.pri	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,246 @@
+SVCORE_HEADERS = \
+           base/AudioLevel.h \
+           base/AudioPlaySource.h \
+           base/AudioRecordTarget.h \
+           base/BaseTypes.h \
+           base/Clipboard.h \
+           base/ColumnOp.h \
+           base/Command.h \
+           base/Debug.h \
+           base/Exceptions.h \
+           base/HelperExecPath.h \
+           base/HitCount.h \
+           base/LogRange.h \
+           base/MagnitudeRange.h \
+           base/Pitch.h \
+           base/Playable.h \
+           base/PlayParameterRepository.h \
+           base/PlayParameters.h \
+           base/Preferences.h \
+           base/Profiler.h \
+           base/ProgressPrinter.h \
+           base/ProgressReporter.h \
+           base/PropertyContainer.h \
+           base/RangeMapper.h \
+           base/RealTime.h \
+           base/RecentFiles.h \
+           base/ResourceFinder.h \
+           base/RingBuffer.h \
+           base/Scavenger.h \
+           base/Selection.h \
+           base/Serialiser.h \
+           base/StorageAdviser.h \
+           base/StringBits.h \
+           base/Strings.h \
+           base/TempDirectory.h \
+           base/TempWriteFile.h \
+           base/TextMatcher.h \
+           base/Thread.h \
+           base/UnitDatabase.h \
+           base/ViewManagerBase.h \
+           base/Window.h \
+           base/XmlExportable.h \
+           base/ZoomConstraint.h \
+           data/fileio/AudioFileReader.h \
+           data/fileio/AudioFileReaderFactory.h \
+           data/fileio/AudioFileSizeEstimator.h \
+           data/fileio/BZipFileDevice.h \
+           data/fileio/CachedFile.h \
+           data/fileio/CodedAudioFileReader.h \
+           data/fileio/CSVFileReader.h \
+           data/fileio/CSVFileWriter.h \
+           data/fileio/CSVFormat.h \
+           data/fileio/DataFileReader.h \
+           data/fileio/DataFileReaderFactory.h \
+           data/fileio/FileFinder.h \
+           data/fileio/FileReadThread.h \
+           data/fileio/FileSource.h \
+           data/fileio/MIDIFileReader.h \
+           data/fileio/MIDIFileWriter.h \
+           data/fileio/MP3FileReader.h \
+           data/fileio/OggVorbisFileReader.h \
+           data/fileio/PlaylistFileReader.h \
+           data/fileio/CoreAudioFileReader.h \
+           data/fileio/DecodingWavFileReader.h \
+           data/fileio/WavFileReader.h \
+           data/fileio/WavFileWriter.h \
+           data/midi/MIDIEvent.h \
+           data/midi/MIDIInput.h \
+           data/midi/rtmidi/RtError.h \
+           data/midi/rtmidi/RtMidi.h \
+           data/model/AggregateWaveModel.h \
+           data/model/AlignmentModel.h \
+           data/model/Dense3DModelPeakCache.h \
+           data/model/DenseThreeDimensionalModel.h \
+           data/model/DenseTimeValueModel.h \
+           data/model/EditableDenseThreeDimensionalModel.h \
+           data/model/FFTModel.h \
+           data/model/ImageModel.h \
+           data/model/IntervalModel.h \
+           data/model/Labeller.h \
+           data/model/Model.h \
+           data/model/ModelDataTableModel.h \
+           data/model/NoteModel.h \
+           data/model/FlexiNoteModel.h \
+           data/model/PathModel.h \
+           data/model/PowerOfSqrtTwoZoomConstraint.h \
+           data/model/PowerOfTwoZoomConstraint.h \
+           data/model/RangeSummarisableTimeValueModel.h \
+           data/model/RegionModel.h \
+           data/model/SparseModel.h \
+           data/model/SparseOneDimensionalModel.h \
+           data/model/SparseTimeValueModel.h \
+           data/model/SparseValueModel.h \
+           data/model/TabularModel.h \
+           data/model/TextModel.h \
+           data/model/WaveFileModel.h \
+           data/model/ReadOnlyWaveFileModel.h \
+           data/model/WritableWaveFileModel.h \
+           data/osc/OSCMessage.h \
+           data/osc/OSCQueue.h \
+	   plugin/PluginScan.h \
+           plugin/DSSIPluginFactory.h \
+           plugin/DSSIPluginInstance.h \
+           plugin/FeatureExtractionPluginFactory.h \
+           plugin/LADSPAPluginFactory.h \
+           plugin/LADSPAPluginInstance.h \
+           plugin/NativeVampPluginFactory.h \
+           plugin/PiperVampPluginFactory.h \
+           plugin/PluginIdentifier.h \
+           plugin/PluginXml.h \
+           plugin/RealTimePluginFactory.h \
+           plugin/RealTimePluginInstance.h \
+           plugin/api/dssi.h \
+           plugin/api/ladspa.h \
+           plugin/plugins/SamplePlayer.h \
+           plugin/api/alsa/asoundef.h \
+           plugin/api/alsa/asoundlib.h \
+           plugin/api/alsa/seq.h \
+           plugin/api/alsa/seq_event.h \
+           plugin/api/alsa/seq_midi_event.h \
+           plugin/api/alsa/sound/asequencer.h \
+	   rdf/PluginRDFIndexer.h \
+           rdf/PluginRDFDescription.h \
+           rdf/RDFExporter.h \
+           rdf/RDFFeatureWriter.h \
+           rdf/RDFImporter.h \
+           rdf/RDFTransformFactory.h \
+	   system/Init.h \
+           system/System.h \
+	   transform/CSVFeatureWriter.h \
+           transform/FeatureExtractionModelTransformer.h \
+           transform/FeatureWriter.h \
+           transform/FileFeatureWriter.h \
+           transform/RealTimeEffectModelTransformer.h \
+           transform/Transform.h \
+           transform/TransformDescription.h \
+           transform/TransformFactory.h \
+           transform/ModelTransformer.h \
+           transform/ModelTransformerFactory.h
+	   
+SVCORE_SOURCES = \
+           base/AudioLevel.cpp \
+           base/Clipboard.cpp \
+           base/ColumnOp.cpp \
+           base/Command.cpp \
+           base/Debug.cpp \
+           base/Exceptions.cpp \
+           base/HelperExecPath.cpp \
+           base/LogRange.cpp \
+           base/Pitch.cpp \
+           base/PlayParameterRepository.cpp \
+           base/PlayParameters.cpp \
+           base/Preferences.cpp \
+           base/Profiler.cpp \
+           base/ProgressPrinter.cpp \
+           base/ProgressReporter.cpp \
+           base/PropertyContainer.cpp \
+           base/RangeMapper.cpp \
+           base/RealTimeSV.cpp \
+           base/RecentFiles.cpp \
+           base/ResourceFinder.cpp \
+           base/Selection.cpp \
+           base/Serialiser.cpp \
+           base/StorageAdviser.cpp \
+           base/StringBits.cpp \
+           base/Strings.cpp \
+           base/TempDirectory.cpp \
+           base/TempWriteFile.cpp \
+           base/TextMatcher.cpp \
+           base/Thread.cpp \
+           base/UnitDatabase.cpp \
+           base/ViewManagerBase.cpp \
+           base/XmlExportable.cpp \
+           data/fileio/AudioFileReader.cpp \
+           data/fileio/AudioFileReaderFactory.cpp \
+           data/fileio/AudioFileSizeEstimator.cpp \
+           data/fileio/BZipFileDevice.cpp \
+           data/fileio/CachedFile.cpp \
+           data/fileio/CodedAudioFileReader.cpp \
+           data/fileio/CSVFileReader.cpp \
+           data/fileio/CSVFileWriter.cpp \
+           data/fileio/CSVFormat.cpp \
+           data/fileio/DataFileReaderFactory.cpp \
+           data/fileio/FileReadThread.cpp \
+           data/fileio/FileSource.cpp \
+           data/fileio/MIDIFileReader.cpp \
+           data/fileio/MIDIFileWriter.cpp \
+           data/fileio/MP3FileReader.cpp \
+           data/fileio/OggVorbisFileReader.cpp \
+           data/fileio/PlaylistFileReader.cpp \
+           data/fileio/CoreAudioFileReader.cpp \
+           data/fileio/DecodingWavFileReader.cpp \
+           data/fileio/WavFileReader.cpp \
+           data/fileio/WavFileWriter.cpp \
+           data/midi/MIDIInput.cpp \
+           data/midi/rtmidi/RtMidi.cpp \
+           data/model/AggregateWaveModel.cpp \
+           data/model/AlignmentModel.cpp \
+           data/model/Dense3DModelPeakCache.cpp \
+           data/model/DenseTimeValueModel.cpp \
+           data/model/EditableDenseThreeDimensionalModel.cpp \
+           data/model/FFTModel.cpp \
+           data/model/Model.cpp \
+           data/model/ModelDataTableModel.cpp \
+           data/model/PowerOfSqrtTwoZoomConstraint.cpp \
+           data/model/PowerOfTwoZoomConstraint.cpp \
+           data/model/RangeSummarisableTimeValueModel.cpp \
+           data/model/WaveFileModel.cpp \
+           data/model/ReadOnlyWaveFileModel.cpp \
+           data/model/WritableWaveFileModel.cpp \
+           data/osc/OSCMessage.cpp \
+           data/osc/OSCQueue.cpp \
+	   plugin/PluginScan.cpp \
+           plugin/DSSIPluginFactory.cpp \
+           plugin/DSSIPluginInstance.cpp \
+           plugin/FeatureExtractionPluginFactory.cpp \
+           plugin/LADSPAPluginFactory.cpp \
+           plugin/LADSPAPluginInstance.cpp \
+           plugin/NativeVampPluginFactory.cpp \
+           plugin/PiperVampPluginFactory.cpp \
+           plugin/PluginIdentifier.cpp \
+           plugin/PluginXml.cpp \
+           plugin/RealTimePluginFactory.cpp \
+           plugin/RealTimePluginInstance.cpp \
+           plugin/plugins/SamplePlayer.cpp \
+	   rdf/PluginRDFIndexer.cpp \
+           rdf/PluginRDFDescription.cpp \
+           rdf/RDFExporter.cpp \
+           rdf/RDFFeatureWriter.cpp \
+           rdf/RDFImporter.cpp \
+           rdf/RDFTransformFactory.cpp \
+	   system/Init.cpp \
+           system/System.cpp \
+	   transform/CSVFeatureWriter.cpp \
+           transform/FeatureExtractionModelTransformer.cpp \
+           transform/FileFeatureWriter.cpp \
+           transform/RealTimeEffectModelTransformer.cpp \
+           transform/Transform.cpp \
+           transform/TransformFactory.cpp \
+           transform/ModelTransformer.cpp \
+           transform/ModelTransformerFactory.cpp
+
+!linux* {
+    SVCORE_SOURCES += plugin/api/dssi_alsa_compat.c 
+}
+
--- a/plugin/DSSIPluginFactory.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/plugin/DSSIPluginFactory.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -225,7 +225,7 @@
         }
 
 #ifdef _WIN32
-        char *pfiles = getenv("ProgramFiles");
+        const char *pfiles = getenv("ProgramFiles");
         if (!pfiles) pfiles = "C:\\Program Files";
         {
         std::string::size_type f;
--- a/plugin/DSSIPluginFactory.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/plugin/DSSIPluginFactory.h	Fri Jan 13 10:29:44 2017 +0000
@@ -48,6 +48,10 @@
     DSSIPluginFactory();
     friend class RealTimePluginFactory;
 
+    virtual PluginScan::PluginType getPluginType() const {
+        return PluginScan::DSSIPlugin;
+    }
+
     virtual std::vector<QString> getPluginPath();
 
     virtual std::vector<QString> getLRDFPath(QString &baseUri);
--- a/plugin/FeatureExtractionPluginFactory.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/plugin/FeatureExtractionPluginFactory.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -4,7 +4,7 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006 Chris Cannam and QMUL.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -13,502 +13,38 @@
     COPYING included with this distribution for more information.
 */
 
-#include "FeatureExtractionPluginFactory.h"
-#include "PluginIdentifier.h"
+#include "PiperVampPluginFactory.h"
+#include "NativeVampPluginFactory.h"
 
-#include <vamp-hostsdk/PluginHostAdapter.h>
-#include <vamp-hostsdk/PluginWrapper.h>
+#include <QMutex>
+#include <QMutexLocker>
 
-#include "system/System.h"
-
-#include <QDir>
-#include <QFile>
-#include <QFileInfo>
-#include <QTextStream>
-
-#include <iostream>
-
-#include "base/Profiler.h"
-
-using namespace std;
-
-//#define DEBUG_PLUGIN_SCAN_AND_INSTANTIATE 1
-
-class PluginDeletionNotifyAdapter : public Vamp::HostExt::PluginWrapper {
-public:
-    PluginDeletionNotifyAdapter(Vamp::Plugin *plugin,
-                                FeatureExtractionPluginFactory *factory) :
-        PluginWrapper(plugin), m_factory(factory) { }
-    virtual ~PluginDeletionNotifyAdapter();
-protected:
-    FeatureExtractionPluginFactory *m_factory;
-};
-
-PluginDeletionNotifyAdapter::~PluginDeletionNotifyAdapter()
-{
-    // see notes in vamp-sdk/hostext/PluginLoader.cpp from which this is drawn
-    Vamp::Plugin *p = m_plugin;
-    delete m_plugin;
-    m_plugin = 0;
-    // acceptable use after free here, as pluginDeleted uses p only as
-    // pointer key and does not deref it
-    if (m_factory) m_factory->pluginDeleted(p);
-}
-
-static FeatureExtractionPluginFactory *_nativeInstance = 0;
+#include "base/Preferences.h"
+#include "base/Debug.h"
 
 FeatureExtractionPluginFactory *
-FeatureExtractionPluginFactory::instance(QString pluginType)
+FeatureExtractionPluginFactory::instance()
 {
-    if (pluginType == "vamp") {
-	if (!_nativeInstance) {
-//	    SVDEBUG << "FeatureExtractionPluginFactory::instance(" << pluginType//		      << "): creating new FeatureExtractionPluginFactory" << endl;
-	    _nativeInstance = new FeatureExtractionPluginFactory();
-	}
-	return _nativeInstance;
+    static QMutex mutex;
+    static FeatureExtractionPluginFactory *instance = 0;
+
+    QMutexLocker locker(&mutex);
+    
+    if (!instance) {
+
+#ifdef HAVE_PIPER
+        if (Preferences::getInstance()->getRunPluginsInProcess()) {
+            SVDEBUG << "FeatureExtractionPluginFactory: in-process preference set, using native factory" << endl;
+            instance = new NativeVampPluginFactory();
+        } else {
+            SVDEBUG << "FeatureExtractionPluginFactory: in-process preference not set, using Piper factory" << endl;
+            instance = new PiperVampPluginFactory();
+        }
+#else
+        SVDEBUG << "FeatureExtractionPluginFactory: no Piper support compiled in, using native factory" << endl;
+        instance = new NativeVampPluginFactory();
+#endif
     }
 
-    else return 0;
+    return instance;
 }
-
-FeatureExtractionPluginFactory *
-FeatureExtractionPluginFactory::instanceFor(QString identifier)
-{
-    QString type, soName, label;
-    PluginIdentifier::parseIdentifier(identifier, type, soName, label);
-    return instance(type);
-}
-
-vector<QString>
-FeatureExtractionPluginFactory::getPluginPath()
-{
-    if (!m_pluginPath.empty()) return m_pluginPath;
-
-    vector<string> p = Vamp::PluginHostAdapter::getPluginPath();
-    for (size_t i = 0; i < p.size(); ++i) m_pluginPath.push_back(p[i].c_str());
-    return m_pluginPath;
-}
-
-vector<QString>
-FeatureExtractionPluginFactory::getAllPluginIdentifiers()
-{
-    FeatureExtractionPluginFactory *factory;
-    vector<QString> rv;
-    
-    factory = instance("vamp");
-    if (factory) {
-	vector<QString> tmp = factory->getPluginIdentifiers();
-	for (size_t i = 0; i < tmp.size(); ++i) {
-//            cerr << "identifier: " << tmp[i] << endl;
-	    rv.push_back(tmp[i]);
-	}
-    }
-
-    // Plugins can change the locale, revert it to default.
-    RestoreStartupLocale();
-
-    return rv;
-}
-
-vector<QString>
-FeatureExtractionPluginFactory::getPluginCandidateFiles()
-{
-    vector<QString> path = getPluginPath();
-    vector<QString> candidates;
-
-    for (QString dirname : path) {
-
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-        SVDEBUG << "FeatureExtractionPluginFactory::getPluginIdentifiers: scanning directory " << dirname << endl;
-#endif
-
-	QDir pluginDir(dirname, PLUGIN_GLOB,
-                       QDir::Name | QDir::IgnoreCase,
-                       QDir::Files | QDir::Readable);
-
-	for (unsigned int j = 0; j < pluginDir.count(); ++j) {
-            QString soname = pluginDir.filePath(pluginDir[j]);
-            candidates.push_back(soname);
-        }
-    }
-
-    return candidates;
-}
-
-vector<QString>
-FeatureExtractionPluginFactory::winnowPluginCandidates(vector<QString> candidates,
-                                                       QString &warningMessage)
-{
-    vector<QString> good, bad;
-    vector<PluginLoadStatus> badStatuses;
-    
-    for (QString c: candidates) {
-
-        PluginLoadStatus status =
-            TestPluginLoadability(c, "vampGetPluginDescriptor");
-
-        if (status == PluginLoadOK) {
-            good.push_back(c);
-        } else if (status == UnknownPluginLoadStatus) {
-            cerr << "WARNING: Unknown load status for plugin candidate \""
-                 << c << "\", continuing" << endl;
-            good.push_back(c);
-        } else {
-            bad.push_back(c);
-            badStatuses.push_back(status);
-        }
-    }
-    
-    if (!bad.empty()) {
-        warningMessage =
-            QObject::tr("<b>Failed to load plugins</b>"
-                        "<p>Failed to load one or more plugin libraries:</p>\n");
-        warningMessage += "<ul>";
-        for (int i = 0; in_range_for(bad, i); ++i) {
-            QString m;
-            if (badStatuses[i] == PluginLoadFailedToLoadLibrary) {
-                m = QObject::tr("Failed to load library");
-            } else if (badStatuses[i] == PluginLoadFailedToFindDescriptor) {
-                m = QObject::tr("Failed to query plugins from library after loading");
-            } else if (badStatuses[i] == PluginLoadFailedElsewhere) {
-                m = QObject::tr("Unknown failure");
-            } else {
-                m = QObject::tr("Success: internal error?");
-            }
-            warningMessage += QString("<li>%1 (%2)</li>\n")
-                .arg(bad[i])
-                .arg(m);
-        }
-        warningMessage += "</ul>";
-    }
-    return good;
-}
-
-vector<QString>
-FeatureExtractionPluginFactory::getPluginIdentifiers()
-{
-    Profiler profiler("FeatureExtractionPluginFactory::getPluginIdentifiers");
-
-    vector<QString> rv;
-    vector<QString> candidates = winnowPluginCandidates(getPluginCandidateFiles(),
-                                                        m_pluginScanError);
-    
-    for (QString soname : candidates) {
-
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-        SVDEBUG << "FeatureExtractionPluginFactory::getPluginIdentifiers: trying potential library " << soname << endl;
-#endif
-
-        void *libraryHandle = DLOPEN(soname, RTLD_LAZY | RTLD_LOCAL);
-            
-        if (!libraryHandle) {
-            cerr << "WARNING: FeatureExtractionPluginFactory::getPluginIdentifiers: Failed to load library " << soname << ": " << DLERROR() << endl;
-            continue;
-        }
-
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-        SVDEBUG << "FeatureExtractionPluginFactory::getPluginIdentifiers: It's a library all right, checking for descriptor" << endl;
-#endif
-
-        VampGetPluginDescriptorFunction fn = (VampGetPluginDescriptorFunction)
-            DLSYM(libraryHandle, "vampGetPluginDescriptor");
-
-        if (!fn) {
-            cerr << "WARNING: FeatureExtractionPluginFactory::getPluginIdentifiers: No descriptor function in " << soname << endl;
-            if (DLCLOSE(libraryHandle) != 0) {
-                cerr << "WARNING: FeatureExtractionPluginFactory::getPluginIdentifiers: Failed to unload library " << soname << endl;
-            }
-            continue;
-        }
-
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-        SVDEBUG << "FeatureExtractionPluginFactory::getPluginIdentifiers: Vamp descriptor found" << endl;
-#endif
-
-        const VampPluginDescriptor *descriptor = 0;
-        int index = 0;
-
-        map<string, int> known;
-        bool ok = true;
-
-        while ((descriptor = fn(VAMP_API_VERSION, index))) {
-
-            if (known.find(descriptor->identifier) != known.end()) {
-                cerr << "WARNING: FeatureExtractionPluginFactory::getPluginIdentifiers: Plugin library "
-                     << soname
-                     << " returns the same plugin identifier \""
-                     << descriptor->identifier << "\" at indices "
-                     << known[descriptor->identifier] << " and "
-                     << index << endl;
-                cerr << "FeatureExtractionPluginFactory::getPluginIdentifiers: Avoiding this library (obsolete API?)" << endl;
-                ok = false;
-                break;
-            } else {
-                known[descriptor->identifier] = index;
-            }
-
-            ++index;
-        }
-
-        if (ok) {
-
-            index = 0;
-
-            while ((descriptor = fn(VAMP_API_VERSION, index))) {
-
-                QString id = PluginIdentifier::createIdentifier
-                    ("vamp", soname, descriptor->identifier);
-                rv.push_back(id);
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-                SVDEBUG << "FeatureExtractionPluginFactory::getPluginIdentifiers: Found plugin id " << id << " at index " << index << endl;
-#endif
-                ++index;
-            }
-        }
-            
-        if (DLCLOSE(libraryHandle) != 0) {
-            cerr << "WARNING: FeatureExtractionPluginFactory::getPluginIdentifiers: Failed to unload library " << soname << endl;
-        }
-    }
-
-    generateTaxonomy();
-
-    return rv;
-}
-
-QString
-FeatureExtractionPluginFactory::findPluginFile(QString soname, QString inDir)
-{
-    QString file = "";
-
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-    cerr << "FeatureExtractionPluginFactory::findPluginFile(\""
-              << soname << "\", \"" << inDir << "\")"
-              << endl;
-#endif
-
-    if (inDir != "") {
-
-        QDir dir(inDir, PLUGIN_GLOB,
-                 QDir::Name | QDir::IgnoreCase,
-                 QDir::Files | QDir::Readable);
-        if (!dir.exists()) return "";
-
-        file = dir.filePath(QFileInfo(soname).fileName());
-
-        if (QFileInfo(file).exists() && QFileInfo(file).isFile()) {
-
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-            cerr << "FeatureExtractionPluginFactory::findPluginFile: "
-                      << "found trivially at " << file << endl;
-#endif
-
-            return file;
-        }
-
-	for (unsigned int j = 0; j < dir.count(); ++j) {
-            file = dir.filePath(dir[j]);
-            if (QFileInfo(file).baseName() == QFileInfo(soname).baseName()) {
-
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-                cerr << "FeatureExtractionPluginFactory::findPluginFile: "
-                          << "found \"" << soname << "\" at " << file << endl;
-#endif
-
-                return file;
-            }
-        }
-
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-        cerr << "FeatureExtractionPluginFactory::findPluginFile (with dir): "
-                  << "not found" << endl;
-#endif
-
-        return "";
-
-    } else {
-
-        QFileInfo fi(soname);
-
-        if (fi.isAbsolute() && fi.exists() && fi.isFile()) {
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-            cerr << "FeatureExtractionPluginFactory::findPluginFile: "
-                      << "found trivially at " << soname << endl;
-#endif
-            return soname;
-        }
-
-        if (fi.isAbsolute() && fi.absolutePath() != "") {
-            file = findPluginFile(soname, fi.absolutePath());
-            if (file != "") return file;
-        }
-
-        vector<QString> path = getPluginPath();
-        for (vector<QString>::iterator i = path.begin();
-             i != path.end(); ++i) {
-            if (*i != "") {
-                file = findPluginFile(soname, *i);
-                if (file != "") return file;
-            }
-        }
-
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-        cerr << "FeatureExtractionPluginFactory::findPluginFile: "
-                  << "not found" << endl;
-#endif
-
-        return "";
-    }
-}
-
-Vamp::Plugin *
-FeatureExtractionPluginFactory::instantiatePlugin(QString identifier,
-						  sv_samplerate_t inputSampleRate)
-{
-    Profiler profiler("FeatureExtractionPluginFactory::instantiatePlugin");
-
-    Vamp::Plugin *rv = 0;
-    Vamp::PluginHostAdapter *plugin = 0;
-
-    const VampPluginDescriptor *descriptor = 0;
-    int index = 0;
-
-    QString type, soname, label;
-    PluginIdentifier::parseIdentifier(identifier, type, soname, label);
-    if (type != "vamp") {
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-        cerr << "FeatureExtractionPluginFactory::instantiatePlugin: Wrong factory for plugin type " << type << endl;
-#endif
-	return 0;
-    }
-
-    QString found = findPluginFile(soname);
-
-    if (found == "") {
-        cerr << "FeatureExtractionPluginFactory::instantiatePlugin: Failed to find library file " << soname << endl;
-        return 0;
-    } else if (found != soname) {
-
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-        cerr << "FeatureExtractionPluginFactory::instantiatePlugin: Given library name was " << soname << ", found at " << found << endl;
-        cerr << soname << " -> " << found << endl;
-#endif
-
-    }        
-
-    soname = found;
-
-    void *libraryHandle = DLOPEN(soname, RTLD_LAZY | RTLD_LOCAL);
-            
-    if (!libraryHandle) {
-        cerr << "FeatureExtractionPluginFactory::instantiatePlugin: Failed to load library " << soname << ": " << DLERROR() << endl;
-        return 0;
-    }
-
-    VampGetPluginDescriptorFunction fn = (VampGetPluginDescriptorFunction)
-        DLSYM(libraryHandle, "vampGetPluginDescriptor");
-    
-    if (!fn) {
-        cerr << "FeatureExtractionPluginFactory::instantiatePlugin: No descriptor function in " << soname << endl;
-        goto done;
-    }
-
-    while ((descriptor = fn(VAMP_API_VERSION, index))) {
-        if (label == descriptor->identifier) break;
-        ++index;
-    }
-
-    if (!descriptor) {
-        cerr << "FeatureExtractionPluginFactory::instantiatePlugin: Failed to find plugin \"" << label << "\" in library " << soname << endl;
-        goto done;
-    }
-
-    plugin = new Vamp::PluginHostAdapter(descriptor, float(inputSampleRate));
-
-    if (plugin) {
-        m_handleMap[plugin] = libraryHandle;
-        rv = new PluginDeletionNotifyAdapter(plugin, this);
-    }
-
-//    SVDEBUG << "FeatureExtractionPluginFactory::instantiatePlugin: Constructed Vamp plugin, rv is " << rv << endl;
-
-    //!!! need to dlclose() when plugins from a given library are unloaded
-
-done:
-    if (!rv) {
-        if (DLCLOSE(libraryHandle) != 0) {
-            cerr << "WARNING: FeatureExtractionPluginFactory::instantiatePlugin: Failed to unload library " << soname << endl;
-        }
-    }
-
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-    cerr << "FeatureExtractionPluginFactory::instantiatePlugin: Instantiated plugin " << label << " from library " << soname << ": descriptor " << descriptor << ", rv "<< rv << ", label " << rv->getName() << ", outputs " << rv->getOutputDescriptors().size() << endl;
-#endif
-    
-    return rv;
-}
-
-void
-FeatureExtractionPluginFactory::pluginDeleted(Vamp::Plugin *plugin)
-{
-    void *handle = m_handleMap[plugin];
-    if (handle) {
-#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
-        cerr << "unloading library " << handle << " for plugin " << plugin << endl;
-#endif
-        DLCLOSE(handle);
-    }
-    m_handleMap.erase(plugin);
-}
-
-QString
-FeatureExtractionPluginFactory::getPluginCategory(QString identifier)
-{
-    return m_taxonomy[identifier];
-}
-
-void
-FeatureExtractionPluginFactory::generateTaxonomy()
-{
-    vector<QString> pluginPath = getPluginPath();
-    vector<QString> path;
-
-    for (size_t i = 0; i < pluginPath.size(); ++i) {
-	if (pluginPath[i].contains("/lib/")) {
-	    QString p(pluginPath[i]);
-            path.push_back(p);
-	    p.replace("/lib/", "/share/");
-	    path.push_back(p);
-	}
-	path.push_back(pluginPath[i]);
-    }
-
-    for (size_t i = 0; i < path.size(); ++i) {
-
-	QDir dir(path[i], "*.cat");
-
-//	SVDEBUG << "LADSPAPluginFactory::generateFallbackCategories: directory " << path[i] << " has " << dir.count() << " .cat files" << endl;
-	for (unsigned int j = 0; j < dir.count(); ++j) {
-
-	    QFile file(path[i] + "/" + dir[j]);
-
-//	    SVDEBUG << "LADSPAPluginFactory::generateFallbackCategories: about to open " << (path[i]+ "/" + dir[j]) << endl;
-
-	    if (file.open(QIODevice::ReadOnly)) {
-//		    cerr << "...opened" << endl;
-		QTextStream stream(&file);
-		QString line;
-
-		while (!stream.atEnd()) {
-		    line = stream.readLine();
-//		    cerr << "line is: \"" << line << "\"" << endl;
-		    QString id = PluginIdentifier::canonicalise
-                        (line.section("::", 0, 0));
-		    QString cat = line.section("::", 1, 1);
-		    m_taxonomy[id] = cat;
-//		    cerr << "FeatureExtractionPluginFactory: set id \"" << id << "\" to cat \"" << cat << "\"" << endl;
-		}
-	    }
-	}
-    }
-}    
--- a/plugin/FeatureExtractionPluginFactory.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/plugin/FeatureExtractionPluginFactory.h	Fri Jan 13 10:29:44 2017 +0000
@@ -4,7 +4,7 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006 Chris Cannam.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -13,66 +13,55 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _FEATURE_EXTRACTION_PLUGIN_FACTORY_H_
-#define _FEATURE_EXTRACTION_PLUGIN_FACTORY_H_
-
-#include <QString>
-#include <vector>
-#include <map>
+#ifndef SV_FEATURE_EXTRACTION_PLUGIN_FACTORY_H
+#define SV_FEATURE_EXTRACTION_PLUGIN_FACTORY_H
 
 #include <vamp-hostsdk/Plugin.h>
 
-#include "base/Debug.h"
+#include "vamp-support/PluginStaticData.h"
+
 #include "base/BaseTypes.h"
 
+#include <QString>
+
 class FeatureExtractionPluginFactory
 {
 public:
+    static FeatureExtractionPluginFactory *instance();
+    
     virtual ~FeatureExtractionPluginFactory() { }
 
-    static FeatureExtractionPluginFactory *instance(QString pluginType);
-    static FeatureExtractionPluginFactory *instanceFor(QString identifier);
-    static std::vector<QString> getAllPluginIdentifiers();
-
-    virtual std::vector<QString> getPluginPath();
-
-    virtual std::vector<QString> getPluginIdentifiers();
+    /**
+     * Return all installed plugin identifiers.
+     */
+    virtual std::vector<QString> getPluginIdentifiers(QString &errorMessage) {
+        return instance()->getPluginIdentifiers(errorMessage);
+    }
 
     /**
-     * Return any error message arising from the initial plugin
-     * scan. The return value will either be an empty string (nothing
-     * to report) or an HTML string suitable for dropping into a
-     * dialog and showing the user.
+     * Return static data for the given plugin.
      */
-    virtual QString getPluginPopulationWarning() { return m_pluginScanError; }
-    
-    virtual QString findPluginFile(QString soname, QString inDir = "");
+    virtual piper_vamp::PluginStaticData getPluginStaticData(QString identifier) {
+        return instance()->getPluginStaticData(identifier);
+    }
 
-    // We don't set blockSize or channels on this -- they're
-    // negotiated and handled via initialize() on the plugin
+    /**
+     * Instantiate (load) and return pointer to the plugin with the
+     * given identifier, at the given sample rate. We don't set
+     * blockSize or channels on this -- they're negotiated and handled
+     * via initialize() on the plugin itself after loading.
+     */
     virtual Vamp::Plugin *instantiatePlugin(QString identifier,
-                                            sv_samplerate_t inputSampleRate);
+                                            sv_samplerate_t inputSampleRate) {
+        return instance()->instantiatePlugin(identifier, inputSampleRate);
+    }
 
     /**
      * Get category metadata about a plugin (without instantiating it).
      */
-    virtual QString getPluginCategory(QString identifier);
-
-protected:
-    std::vector<QString> m_pluginPath;
-    std::map<QString, QString> m_taxonomy;
-
-    friend class PluginDeletionNotifyAdapter;
-    void pluginDeleted(Vamp::Plugin *);
-    std::map<Vamp::Plugin *, void *> m_handleMap;
-    
-    std::vector<QString> getPluginCandidateFiles();
-    std::vector<QString> winnowPluginCandidates(std::vector<QString> candidates,
-                                                QString &warningMessage);
-    
-    void generateTaxonomy();
-
-    QString m_pluginScanError;
+    virtual QString getPluginCategory(QString identifier) {
+        return instance()->getPluginCategory(identifier);
+    }
 };
 
 #endif
--- a/plugin/LADSPAPluginFactory.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/plugin/LADSPAPluginFactory.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -579,7 +579,7 @@
         }
 
 #ifdef _WIN32
-        char *pfiles = getenv("ProgramFiles");
+        const char *pfiles = getenv("ProgramFiles");
         if (!pfiles) pfiles = "C:\\Program Files";
         {
         std::string::size_type f;
@@ -668,14 +668,11 @@
 
     generateFallbackCategories();
 
-    for (std::vector<QString>::iterator i = pathList.begin();
-	 i != pathList.end(); ++i) {
+    auto candidates =
+        PluginScan::getInstance()->getCandidateLibrariesFor(getPluginType());
 
-	QDir pluginDir(*i, PLUGIN_GLOB);
-
-	for (unsigned int j = 0; j < pluginDir.count(); ++j) {
-	    discoverPluginsFrom(QString("%1/%2").arg(*i).arg(pluginDir[j]));
-	}
+    for (auto c: candidates) {
+        discoverPluginsFrom(c.libraryPath);
     }
 }
 
--- a/plugin/LADSPAPluginFactory.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/plugin/LADSPAPluginFactory.h	Fri Jan 13 10:29:44 2017 +0000
@@ -24,6 +24,8 @@
 #include "RealTimePluginFactory.h"
 #include "api/ladspa.h"
 
+#include "PluginScan.h"
+
 #include <vector>
 #include <map>
 #include <set>
@@ -63,6 +65,10 @@
     LADSPAPluginFactory();
     friend class RealTimePluginFactory;
 
+    virtual PluginScan::PluginType getPluginType() const {
+        return PluginScan::LADSPAPlugin;
+    }
+
     virtual std::vector<QString> getPluginPath();
 
     virtual std::vector<QString> getLRDFPath(QString &baseUri);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugin/NativeVampPluginFactory.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,471 @@
+/* -*- 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 file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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 "NativeVampPluginFactory.h"
+#include "PluginIdentifier.h"
+
+#include <vamp-hostsdk/PluginHostAdapter.h>
+#include <vamp-hostsdk/PluginWrapper.h>
+
+#include "system/System.h"
+
+#include "PluginScan.h"
+
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QTextStream>
+
+#include <iostream>
+
+#include "base/Profiler.h"
+
+#include <QMutex>
+#include <QMutexLocker>
+
+using namespace std;
+
+//#define DEBUG_PLUGIN_SCAN_AND_INSTANTIATE 1
+
+class PluginDeletionNotifyAdapter : public Vamp::HostExt::PluginWrapper {
+public:
+    PluginDeletionNotifyAdapter(Vamp::Plugin *plugin,
+                                NativeVampPluginFactory *factory) :
+        PluginWrapper(plugin), m_factory(factory) { }
+    virtual ~PluginDeletionNotifyAdapter();
+protected:
+    NativeVampPluginFactory *m_factory;
+};
+
+PluginDeletionNotifyAdapter::~PluginDeletionNotifyAdapter()
+{
+    // see notes in vamp-sdk/hostext/PluginLoader.cpp from which this is drawn
+    Vamp::Plugin *p = m_plugin;
+    delete m_plugin;
+    m_plugin = 0;
+    // acceptable use after free here, as pluginDeleted uses p only as
+    // pointer key and does not deref it
+    if (m_factory) m_factory->pluginDeleted(p);
+}
+
+vector<QString>
+NativeVampPluginFactory::getPluginPath()
+{
+    if (!m_pluginPath.empty()) return m_pluginPath;
+
+    vector<string> p = Vamp::PluginHostAdapter::getPluginPath();
+    for (size_t i = 0; i < p.size(); ++i) m_pluginPath.push_back(p[i].c_str());
+    return m_pluginPath;
+}
+
+static
+QList<PluginScan::Candidate>
+getCandidateLibraries()
+{
+#ifdef HAVE_PLUGIN_CHECKER_HELPER
+    return PluginScan::getInstance()->getCandidateLibrariesFor
+        (PluginScan::VampPlugin);
+#else
+    auto path = Vamp::PluginHostAdapter::getPluginPath();
+    QList<PluginScan::Candidate> candidates;
+    for (string dirname: path) {
+        SVDEBUG << "NativeVampPluginFactory: scanning directory myself: "
+                << dirname << endl;
+#if defined(_WIN32)
+#define PLUGIN_GLOB "*.dll"
+#elif defined(__APPLE__)
+#define PLUGIN_GLOB "*.dylib *.so"
+#else
+#define PLUGIN_GLOB "*.so"
+#endif
+        QDir dir(dirname.c_str(), PLUGIN_GLOB,
+                 QDir::Name | QDir::IgnoreCase,
+                 QDir::Files | QDir::Readable);
+
+        for (unsigned int i = 0; i < dir.count(); ++i) {
+            QString soname = dir.filePath(dir[i]);
+            candidates.push_back({ soname, "" });
+        }
+    }
+
+    return candidates;
+#endif
+}
+
+vector<QString>
+NativeVampPluginFactory::getPluginIdentifiers(QString &)
+{
+    Profiler profiler("NativeVampPluginFactory::getPluginIdentifiers");
+
+    QMutexLocker locker(&m_mutex);
+
+    if (!m_identifiers.empty()) {
+        return m_identifiers;
+    }
+
+    auto candidates = getCandidateLibraries();
+    
+    SVDEBUG << "INFO: Have " << candidates.size() << " candidate Vamp plugin libraries" << endl;
+        
+    for (auto candidate : candidates) {
+
+        QString soname = candidate.libraryPath;
+
+        SVDEBUG << "INFO: Considering candidate Vamp plugin library " << soname << endl;
+        
+        void *libraryHandle = DLOPEN(soname, RTLD_LAZY | RTLD_LOCAL);
+            
+        if (!libraryHandle) {
+            SVDEBUG << "WARNING: NativeVampPluginFactory::getPluginIdentifiers: Failed to load library " << soname << ": " << DLERROR() << endl;
+            continue;
+        }
+
+        VampGetPluginDescriptorFunction fn = (VampGetPluginDescriptorFunction)
+            DLSYM(libraryHandle, "vampGetPluginDescriptor");
+
+        if (!fn) {
+            SVDEBUG << "WARNING: NativeVampPluginFactory::getPluginIdentifiers: No descriptor function in " << soname << endl;
+            if (DLCLOSE(libraryHandle) != 0) {
+                SVDEBUG << "WARNING: NativeVampPluginFactory::getPluginIdentifiers: Failed to unload library " << soname << endl;
+            }
+            continue;
+        }
+
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+            cerr << "NativeVampPluginFactory::getPluginIdentifiers: Vamp descriptor found" << endl;
+#endif
+
+        const VampPluginDescriptor *descriptor = 0;
+        int index = 0;
+
+        map<string, int> known;
+        bool ok = true;
+
+        while ((descriptor = fn(VAMP_API_VERSION, index))) {
+
+            if (known.find(descriptor->identifier) != known.end()) {
+                SVDEBUG << "WARNING: NativeVampPluginFactory::getPluginIdentifiers: Plugin library "
+                     << soname
+                     << " returns the same plugin identifier \""
+                     << descriptor->identifier << "\" at indices "
+                     << known[descriptor->identifier] << " and "
+                     << index << endl;
+                    SVDEBUG << "NativeVampPluginFactory::getPluginIdentifiers: Avoiding this library (obsolete API?)" << endl;
+                ok = false;
+                break;
+            } else {
+                known[descriptor->identifier] = index;
+            }
+
+            ++index;
+        }
+
+        if (ok) {
+
+            index = 0;
+
+            while ((descriptor = fn(VAMP_API_VERSION, index))) {
+
+                QString id = PluginIdentifier::createIdentifier
+                    ("vamp", soname, descriptor->identifier);
+                m_identifiers.push_back(id);
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+                cerr << "NativeVampPluginFactory::getPluginIdentifiers: Found plugin id " << id << " at index " << index << endl;
+#endif
+                ++index;
+            }
+        }
+            
+        if (DLCLOSE(libraryHandle) != 0) {
+            SVDEBUG << "WARNING: NativeVampPluginFactory::getPluginIdentifiers: Failed to unload library " << soname << endl;
+        }
+    }
+
+    generateTaxonomy();
+
+    // Plugins can change the locale, revert it to default.
+    RestoreStartupLocale();
+
+    return m_identifiers;
+}
+
+QString
+NativeVampPluginFactory::findPluginFile(QString soname, QString inDir)
+{
+    QString file = "";
+
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+    cerr << "NativeVampPluginFactory::findPluginFile(\""
+              << soname << "\", \"" << inDir << "\")"
+              << endl;
+#endif
+
+    if (inDir != "") {
+
+        QDir dir(inDir, PLUGIN_GLOB,
+                 QDir::Name | QDir::IgnoreCase,
+                 QDir::Files | QDir::Readable);
+        if (!dir.exists()) return "";
+
+        file = dir.filePath(QFileInfo(soname).fileName());
+
+        if (QFileInfo(file).exists() && QFileInfo(file).isFile()) {
+
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+            cerr << "NativeVampPluginFactory::findPluginFile: "
+                      << "found trivially at " << file << endl;
+#endif
+
+            return file;
+        }
+
+	for (unsigned int j = 0; j < dir.count(); ++j) {
+            file = dir.filePath(dir[j]);
+            if (QFileInfo(file).baseName() == QFileInfo(soname).baseName()) {
+
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+                cerr << "NativeVampPluginFactory::findPluginFile: "
+                          << "found \"" << soname << "\" at " << file << endl;
+#endif
+
+                return file;
+            }
+        }
+
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+        cerr << "NativeVampPluginFactory::findPluginFile (with dir): "
+                  << "not found" << endl;
+#endif
+
+        return "";
+
+    } else {
+
+        QFileInfo fi(soname);
+
+        if (fi.isAbsolute() && fi.exists() && fi.isFile()) {
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+            cerr << "NativeVampPluginFactory::findPluginFile: "
+                      << "found trivially at " << soname << endl;
+#endif
+            return soname;
+        }
+
+        if (fi.isAbsolute() && fi.absolutePath() != "") {
+            file = findPluginFile(soname, fi.absolutePath());
+            if (file != "") return file;
+        }
+
+        vector<QString> path = getPluginPath();
+        for (vector<QString>::iterator i = path.begin();
+             i != path.end(); ++i) {
+            if (*i != "") {
+                file = findPluginFile(soname, *i);
+                if (file != "") return file;
+            }
+        }
+
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+        cerr << "NativeVampPluginFactory::findPluginFile: "
+                  << "not found" << endl;
+#endif
+
+        return "";
+    }
+}
+
+Vamp::Plugin *
+NativeVampPluginFactory::instantiatePlugin(QString identifier,
+                                           sv_samplerate_t inputSampleRate)
+{
+    Profiler profiler("NativeVampPluginFactory::instantiatePlugin");
+
+    Vamp::Plugin *rv = 0;
+    Vamp::PluginHostAdapter *plugin = 0;
+
+    const VampPluginDescriptor *descriptor = 0;
+    int index = 0;
+
+    QString type, soname, label;
+    PluginIdentifier::parseIdentifier(identifier, type, soname, label);
+    if (type != "vamp") {
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+        cerr << "NativeVampPluginFactory::instantiatePlugin: Wrong factory for plugin type " << type << endl;
+#endif
+	return 0;
+    }
+
+    QString found = findPluginFile(soname);
+
+    if (found == "") {
+        SVDEBUG << "NativeVampPluginFactory::instantiatePlugin: Failed to find library file " << soname << endl;
+        return 0;
+    } else if (found != soname) {
+
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+        cerr << "NativeVampPluginFactory::instantiatePlugin: Given library name was " << soname << ", found at " << found << endl;
+        cerr << soname << " -> " << found << endl;
+#endif
+
+    }        
+
+    soname = found;
+
+    void *libraryHandle = DLOPEN(soname, RTLD_LAZY | RTLD_LOCAL);
+            
+    if (!libraryHandle) {
+        SVDEBUG << "NativeVampPluginFactory::instantiatePlugin: Failed to load library " << soname << ": " << DLERROR() << endl;
+        return 0;
+    }
+
+    VampGetPluginDescriptorFunction fn = (VampGetPluginDescriptorFunction)
+        DLSYM(libraryHandle, "vampGetPluginDescriptor");
+    
+    if (!fn) {
+        SVDEBUG << "NativeVampPluginFactory::instantiatePlugin: No descriptor function in " << soname << endl;
+        goto done;
+    }
+
+    while ((descriptor = fn(VAMP_API_VERSION, index))) {
+        if (label == descriptor->identifier) break;
+        ++index;
+    }
+
+    if (!descriptor) {
+        SVDEBUG << "NativeVampPluginFactory::instantiatePlugin: Failed to find plugin \"" << label << "\" in library " << soname << endl;
+        goto done;
+    }
+
+    plugin = new Vamp::PluginHostAdapter(descriptor, float(inputSampleRate));
+
+    if (plugin) {
+        m_handleMap[plugin] = libraryHandle;
+        rv = new PluginDeletionNotifyAdapter(plugin, this);
+    }
+
+//    SVDEBUG << "NativeVampPluginFactory::instantiatePlugin: Constructed Vamp plugin, rv is " << rv << endl;
+
+    //!!! need to dlclose() when plugins from a given library are unloaded
+
+done:
+    if (!rv) {
+        if (DLCLOSE(libraryHandle) != 0) {
+            SVDEBUG << "WARNING: NativeVampPluginFactory::instantiatePlugin: Failed to unload library " << soname << endl;
+        }
+    }
+
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+    cerr << "NativeVampPluginFactory::instantiatePlugin: Instantiated plugin " << label << " from library " << soname << ": descriptor " << descriptor << ", rv "<< rv << ", label " << rv->getName() << ", outputs " << rv->getOutputDescriptors().size() << endl;
+#endif
+    
+    return rv;
+}
+
+void
+NativeVampPluginFactory::pluginDeleted(Vamp::Plugin *plugin)
+{
+    void *handle = m_handleMap[plugin];
+    if (handle) {
+#ifdef DEBUG_PLUGIN_SCAN_AND_INSTANTIATE
+        cerr << "unloading library " << handle << " for plugin " << plugin << endl;
+#endif
+        DLCLOSE(handle);
+    }
+    m_handleMap.erase(plugin);
+}
+
+QString
+NativeVampPluginFactory::getPluginCategory(QString identifier)
+{
+    return m_taxonomy[identifier];
+}
+
+void
+NativeVampPluginFactory::generateTaxonomy()
+{
+    vector<QString> pluginPath = getPluginPath();
+    vector<QString> path;
+
+    for (size_t i = 0; i < pluginPath.size(); ++i) {
+	if (pluginPath[i].contains("/lib/")) {
+	    QString p(pluginPath[i]);
+            path.push_back(p);
+	    p.replace("/lib/", "/share/");
+	    path.push_back(p);
+	}
+	path.push_back(pluginPath[i]);
+    }
+
+    for (size_t i = 0; i < path.size(); ++i) {
+
+	QDir dir(path[i], "*.cat");
+
+//	SVDEBUG << "LADSPAPluginFactory::generateFallbackCategories: directory " << path[i] << " has " << dir.count() << " .cat files" << endl;
+	for (unsigned int j = 0; j < dir.count(); ++j) {
+
+	    QFile file(path[i] + "/" + dir[j]);
+
+//	    SVDEBUG << "LADSPAPluginFactory::generateFallbackCategories: about to open " << (path[i]+ "/" + dir[j]) << endl;
+
+	    if (file.open(QIODevice::ReadOnly)) {
+//		    cerr << "...opened" << endl;
+		QTextStream stream(&file);
+		QString line;
+
+		while (!stream.atEnd()) {
+		    line = stream.readLine();
+//		    cerr << "line is: \"" << line << "\"" << endl;
+		    QString id = PluginIdentifier::canonicalise
+                        (line.section("::", 0, 0));
+		    QString cat = line.section("::", 1, 1);
+		    m_taxonomy[id] = cat;
+//		    cerr << "NativeVampPluginFactory: set id \"" << id << "\" to cat \"" << cat << "\"" << endl;
+		}
+	    }
+	}
+    }
+}    
+
+piper_vamp::PluginStaticData
+NativeVampPluginFactory::getPluginStaticData(QString identifier)
+{
+    QMutexLocker locker(&m_mutex);
+
+    if (m_pluginData.find(identifier) != m_pluginData.end()) {
+        return m_pluginData[identifier];
+    }
+    
+    QString type, soname, label;
+    PluginIdentifier::parseIdentifier(identifier, type, soname, label);
+    std::string pluginKey = (soname + ":" + label).toStdString();
+
+    std::vector<std::string> catlist;
+    for (auto s: getPluginCategory(identifier).split(" > ")) {
+        catlist.push_back(s.toStdString());
+    }
+    
+    Vamp::Plugin *p = instantiatePlugin(identifier, 44100);
+    if (!p) return {};
+
+    auto psd = piper_vamp::PluginStaticData::fromPlugin(pluginKey,
+                                                        catlist,
+                                                        p);
+
+    delete p;
+    
+    m_pluginData[identifier] = psd;
+    return psd;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugin/NativeVampPluginFactory.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,68 @@
+/* -*- 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 file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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_NATIVE_VAMP_PLUGIN_FACTORY_H
+#define SV_NATIVE_VAMP_PLUGIN_FACTORY_H
+
+#include "FeatureExtractionPluginFactory.h"
+
+#include <vector>
+#include <map>
+
+#include "base/Debug.h"
+
+#include <QMutex>
+
+/**
+ * FeatureExtractionPluginFactory type for Vamp plugins hosted
+ * in-process.
+ */
+class NativeVampPluginFactory : public FeatureExtractionPluginFactory
+{
+public:
+    virtual ~NativeVampPluginFactory() { }
+
+    virtual std::vector<QString> getPluginIdentifiers(QString &errorMessage)
+        override;
+
+    virtual piper_vamp::PluginStaticData getPluginStaticData(QString identifier)
+        override;
+
+    virtual Vamp::Plugin *instantiatePlugin(QString identifier,
+                                            sv_samplerate_t inputSampleRate)
+        override;
+
+    /**
+     * Get category metadata about a plugin (without instantiating it).
+     */
+    virtual QString getPluginCategory(QString identifier) override;
+
+protected:
+    QMutex m_mutex;
+    std::vector<QString> m_pluginPath;
+    std::vector<QString> m_identifiers;
+    std::map<QString, QString> m_taxonomy; // identifier -> category string
+    std::map<QString, piper_vamp::PluginStaticData> m_pluginData; // identifier -> data (created opportunistically)
+
+    friend class PluginDeletionNotifyAdapter;
+    void pluginDeleted(Vamp::Plugin *);
+    std::map<Vamp::Plugin *, void *> m_handleMap;
+
+    QString findPluginFile(QString soname, QString inDir = "");
+    std::vector<QString> getPluginPath();
+    void generateTaxonomy();
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugin/PiperVampPluginFactory.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,273 @@
+/* -*- 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 file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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.
+*/
+
+#ifdef HAVE_PIPER
+
+#include "PiperVampPluginFactory.h"
+#include "PluginIdentifier.h"
+
+#include "system/System.h"
+
+#include "PluginScan.h"
+
+#ifdef _WIN32
+#undef VOID
+#undef ERROR
+#define CAPNP_LITE 1
+#endif
+
+#include "vamp-client/AutoPlugin.h"
+
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QTextStream>
+#include <QCoreApplication>
+
+#include <iostream>
+
+#include "base/Profiler.h"
+#include "base/HelperExecPath.h"
+
+#include "vamp-client/ProcessQtTransport.h"
+#include "vamp-client/CapnpRRClient.h"
+
+using namespace std;
+
+//#define DEBUG_PLUGIN_SCAN_AND_INSTANTIATE 1
+
+class PiperVampPluginFactory::Logger : public piper_vamp::client::LogCallback {
+protected:
+    void log(std::string message) const override {
+        SVDEBUG << "PiperVampPluginFactory: " << message << endl;
+    }
+};
+
+PiperVampPluginFactory::PiperVampPluginFactory() :
+    m_logger(new Logger)
+{
+    QString serverName = "piper-vamp-simple-server";
+
+    HelperExecPath hep(HelperExecPath::AllInstalled);
+    m_servers = hep.getHelperExecutables(serverName);
+
+    for (auto n: m_servers) {
+        SVDEBUG << "NOTE: PiperVampPluginFactory: Found server: "
+                << n.executable << endl;
+    }
+    
+    if (m_servers.empty()) {
+        SVDEBUG << "NOTE: No Piper Vamp servers found in installation;"
+                << " found none of the following:" << endl;
+        for (auto d: hep.getHelperCandidatePaths(serverName)) {
+            SVDEBUG << "NOTE: " << d << endl;
+        }
+    }
+}
+
+PiperVampPluginFactory::~PiperVampPluginFactory()
+{
+    delete m_logger;
+}
+
+vector<QString>
+PiperVampPluginFactory::getPluginIdentifiers(QString &errorMessage)
+{
+    Profiler profiler("PiperVampPluginFactory::getPluginIdentifiers");
+
+    QMutexLocker locker(&m_mutex);
+
+    if (m_servers.empty()) {
+        errorMessage = QObject::tr("External plugin host executable does not appear to be installed");
+        return {};
+    }
+    
+    if (m_pluginData.empty()) {
+        populate(errorMessage);
+    }
+
+    vector<QString> rv;
+
+    for (const auto &d: m_pluginData) {
+        rv.push_back(QString("vamp:") + QString::fromStdString(d.second.pluginKey));
+    }
+
+    return rv;
+}
+
+Vamp::Plugin *
+PiperVampPluginFactory::instantiatePlugin(QString identifier,
+                                          sv_samplerate_t inputSampleRate)
+{
+    Profiler profiler("PiperVampPluginFactory::instantiatePlugin");
+
+    if (m_origins.find(identifier) == m_origins.end()) {
+        cerr << "ERROR: No known server for identifier " << identifier << endl;
+        SVDEBUG << "ERROR: No known server for identifier " << identifier << endl;
+        return 0;
+    }
+    
+    auto psd = getPluginStaticData(identifier);
+    if (psd.pluginKey == "") {
+        return 0;
+    }
+
+    SVDEBUG << "PiperVampPluginFactory: Creating Piper AutoPlugin for server "
+        << m_origins[identifier] << ", identifier " << identifier << endl;
+    
+    auto ap = new piper_vamp::client::AutoPlugin
+        (m_origins[identifier].toStdString(),
+         psd.pluginKey,
+         float(inputSampleRate),
+         0,
+         m_logger);
+    
+    if (!ap->isOK()) {
+        delete ap;
+        return 0;
+    }
+
+    return ap;
+}
+
+piper_vamp::PluginStaticData
+PiperVampPluginFactory::getPluginStaticData(QString identifier)
+{
+    if (m_pluginData.find(identifier) != m_pluginData.end()) {
+        return m_pluginData[identifier];
+    } else {
+        return {};
+    }
+}
+
+QString
+PiperVampPluginFactory::getPluginCategory(QString identifier)
+{
+    if (m_taxonomy.find(identifier) != m_taxonomy.end()) {
+        return m_taxonomy[identifier];
+    } else {
+        return {};
+    }
+}
+
+void
+PiperVampPluginFactory::populate(QString &errorMessage)
+{
+    QString someError;
+
+    for (auto s: m_servers) {
+
+        populateFrom(s, someError);
+
+        if (someError != "" && errorMessage == "") {
+            errorMessage = someError;
+        }
+    }
+}
+
+void
+PiperVampPluginFactory::populateFrom(const HelperExecPath::HelperExec &server,
+                                     QString &errorMessage)
+{
+    QString tag = server.tag;
+    string executable = server.executable.toStdString();
+
+    PluginScan *scan = PluginScan::getInstance();
+    auto candidateLibraries =
+        scan->getCandidateLibrariesFor(PluginScan::VampPlugin);
+
+    SVDEBUG << "PiperVampPluginFactory: Populating from " << executable << endl;
+    SVDEBUG << "INFO: Have " << candidateLibraries.size()
+            << " candidate Vamp plugin libraries from scanner" << endl;
+        
+    vector<string> from;
+    for (const auto &c: candidateLibraries) {
+        if (c.helperTag == tag) {
+            string soname = QFileInfo(c.libraryPath).baseName().toStdString();
+            SVDEBUG << "INFO: For tag \"" << tag << "\" giving library " << soname << endl;
+            from.push_back(soname);
+        }
+    }
+
+    if (from.empty()) {
+        SVDEBUG << "PiperVampPluginFactory: No candidate libraries for tag \""
+             << tag << "\"";
+        if (scan->scanSucceeded()) {
+            // we have to assume that they all failed to load (i.e. we
+            // exclude them all) rather than sending an empty list
+            // (which would mean no exclusions)
+            SVDEBUG << ", skipping" << endl;
+            return;
+        } else {
+            SVDEBUG << ", but it seems the scan failed, so bumbling on anyway" << endl;
+        }
+    }
+    
+    piper_vamp::client::ProcessQtTransport transport(executable, "capnp", m_logger);
+    if (!transport.isOK()) {
+        SVDEBUG << "PiperVampPluginFactory: Failed to start Piper process transport" << endl;
+        errorMessage = QObject::tr("Could not start external plugin host");
+        return;
+    }
+
+    piper_vamp::client::CapnpRRClient client(&transport, m_logger);
+
+    piper_vamp::ListRequest req;
+    req.from = from;
+    
+    piper_vamp::ListResponse resp;
+
+    try {
+        resp = client.listPluginData(req);
+    } catch (piper_vamp::client::ServerCrashed) {
+        SVDEBUG << "PiperVampPluginFactory: Piper server crashed" << endl;
+        errorMessage = QObject::tr
+            ("External plugin host exited unexpectedly while listing plugins");
+        return;
+    } catch (const std::exception &e) {
+        SVDEBUG << "PiperVampPluginFactory: Exception caught: " << e.what() << endl;
+        errorMessage = QObject::tr("External plugin host invocation failed: %1")
+            .arg(e.what());
+        return;
+    }
+
+    SVDEBUG << "PiperVampPluginFactory: server \"" << executable << "\" lists "
+            << resp.available.size() << " plugin(s)" << endl;
+
+    for (const auto &pd: resp.available) {
+        
+        QString identifier =
+            QString("vamp:") + QString::fromStdString(pd.pluginKey);
+
+        if (m_origins.find(identifier) != m_origins.end()) {
+            // have it already, from a higher-priority server
+            // (e.g. 64-bit instead of 32-bit)
+            continue;
+        }
+
+        m_origins[identifier] = server.executable;
+        
+        m_pluginData[identifier] = pd;
+
+        QStringList catlist;
+        for (const auto &cs: pd.category) {
+            catlist.push_back(QString::fromStdString(cs));
+        }
+
+        m_taxonomy[identifier] = catlist.join(" > ");
+    }
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugin/PiperVampPluginFactory.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,68 @@
+/* -*- 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 file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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_PIPER_VAMP_PLUGIN_FACTORY_H
+#define SV_PIPER_VAMP_PLUGIN_FACTORY_H
+
+#ifdef HAVE_PIPER
+
+#include "FeatureExtractionPluginFactory.h"
+
+#include <QMutex>
+#include <vector>
+#include <map>
+
+#include "base/Debug.h"
+#include "base/HelperExecPath.h"
+
+/**
+ * FeatureExtractionPluginFactory type for Vamp plugins hosted in a
+ * separate process using Piper protocol.
+ */
+class PiperVampPluginFactory : public FeatureExtractionPluginFactory
+{
+public:
+    PiperVampPluginFactory();
+    virtual ~PiperVampPluginFactory();
+
+    virtual std::vector<QString> getPluginIdentifiers(QString &errorMessage)
+        override;
+
+    virtual piper_vamp::PluginStaticData getPluginStaticData(QString identifier)
+        override;
+    
+    virtual Vamp::Plugin *instantiatePlugin(QString identifier,
+                                            sv_samplerate_t inputSampleRate)
+        override;
+
+    virtual QString getPluginCategory(QString identifier) override;
+
+protected:
+    QMutex m_mutex;
+    QList<HelperExecPath::HelperExec> m_servers; // executable file paths
+    std::map<QString, QString> m_origins; // plugin identifier -> server path
+    std::map<QString, piper_vamp::PluginStaticData> m_pluginData; // identifier -> data
+    std::map<QString, QString> m_taxonomy; // identifier -> category string
+
+    void populate(QString &errorMessage);
+    void populateFrom(const HelperExecPath::HelperExec &, QString &errorMessage);
+
+    class Logger;
+    Logger *m_logger;
+};
+
+#endif
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugin/PluginScan.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,222 @@
+/* -*- 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 "PluginScan.h"
+
+#include "base/Debug.h"
+#include "base/Preferences.h"
+#include "base/HelperExecPath.h"
+
+#ifdef HAVE_PLUGIN_CHECKER_HELPER
+#include "checker/knownplugins.h"
+#else
+class KnownPlugins {};
+#endif
+
+#include <QMutex>
+#include <QCoreApplication>
+
+using std::string;
+
+class PluginScan::Logger
+#ifdef HAVE_PLUGIN_CHECKER_HELPER
+    : public PluginCandidates::LogCallback
+#endif
+{
+protected:
+    void log(std::string message) {
+        SVDEBUG << "PluginScan: " << message << endl;
+    }
+};
+
+PluginScan *PluginScan::getInstance()
+{
+    static QMutex mutex;
+    static PluginScan *m_instance = 0;
+    mutex.lock();
+    if (!m_instance) m_instance = new PluginScan();
+    mutex.unlock();
+    return m_instance;
+}
+
+PluginScan::PluginScan() : m_succeeded(false), m_logger(new Logger) {
+}
+
+PluginScan::~PluginScan() {
+    QMutexLocker locker(&m_mutex);
+    clear();
+    delete m_logger;
+}
+
+void
+PluginScan::scan()
+{
+#ifdef HAVE_PLUGIN_CHECKER_HELPER
+
+    QMutexLocker locker(&m_mutex);
+
+    bool inProcess = Preferences::getInstance()->getRunPluginsInProcess();
+
+    HelperExecPath hep(inProcess ?
+                       HelperExecPath::NativeArchitectureOnly :
+                       HelperExecPath::AllInstalled);
+
+    QString helperName("vamp-plugin-load-checker");
+    auto helpers = hep.getHelperExecutables(helperName);
+
+    clear();
+
+    for (auto p: helpers) {
+        SVDEBUG << "NOTE: PluginScan: Found helper: " << p.executable << endl;
+    }
+    
+    if (helpers.empty()) {
+        SVDEBUG << "NOTE: No plugin checker helpers found in installation;"
+             << " found none of the following:" << endl;
+        for (auto d: hep.getHelperCandidatePaths(helperName)) {
+            SVDEBUG << "NOTE: " << d << endl;
+        }
+    }
+
+    for (auto p: helpers) {
+        try {
+            KnownPlugins *kp = new KnownPlugins
+                (p.executable.toStdString(), m_logger);
+            if (m_kp.find(p.tag) != m_kp.end()) {
+                SVDEBUG << "WARNING: PluginScan::scan: Duplicate tag " << p.tag
+                     << " for helpers" << endl;
+                continue;
+            }
+            m_kp[p.tag] = kp;
+            m_succeeded = true;
+        } catch (const std::exception &e) {
+            SVDEBUG << "ERROR: PluginScan::scan: " << e.what()
+                 << " (with helper path = " << p.executable << ")" << endl;
+        }
+    }
+
+#endif
+}
+
+bool
+PluginScan::scanSucceeded() const
+{
+    QMutexLocker locker(&m_mutex);
+    return m_succeeded;
+}
+
+void
+PluginScan::clear()
+{
+    for (auto &p: m_kp) {
+        delete p.second;
+    }
+    m_kp.clear();
+    m_succeeded = false;
+}
+
+QList<PluginScan::Candidate>
+PluginScan::getCandidateLibrariesFor(PluginType
+#ifdef HAVE_PLUGIN_CHECKER_HELPER
+                                     type
+#endif
+    ) const
+{
+#ifdef HAVE_PLUGIN_CHECKER_HELPER
+    
+    QMutexLocker locker(&m_mutex);
+
+    KnownPlugins::PluginType kpt;
+    switch (type) {
+    case VampPlugin: kpt = KnownPlugins::VampPlugin; break;
+    case LADSPAPlugin: kpt = KnownPlugins::LADSPAPlugin; break;
+    case DSSIPlugin: kpt = KnownPlugins::DSSIPlugin; break;
+    default: throw std::logic_error("Inconsistency in plugin type enums");
+    }
+    
+    QList<Candidate> candidates;
+
+    for (auto rec: m_kp) {
+
+        KnownPlugins *kp = rec.second;
+        
+        auto c = kp->getCandidateLibrariesFor(kpt);
+
+        SVDEBUG << "PluginScan: helper \"" << kp->getHelperExecutableName()
+                << "\" likes " << c.size() << " libraries of type "
+                << kp->getTagFor(kpt) << endl;
+
+        for (auto s: c) {
+            candidates.push_back({ s.c_str(), rec.first });
+        }
+
+        if (type != VampPlugin) {
+            // We are only interested in querying multiple helpers
+            // when dealing with Vamp plugins, for which we can use
+            // external servers and so in some cases can support
+            // additional architectures. Other plugin formats are
+            // loaded directly and so must match the host, which is
+            // what the first helper is supposed to handle -- so
+            // break after the first one if not querying Vamp
+            break;
+        }
+    }
+
+    return candidates;
+    
+#else
+    return {};
+#endif
+}
+
+QString
+PluginScan::getStartupFailureReport() const
+{
+#ifdef HAVE_PLUGIN_CHECKER_HELPER
+    
+    QMutexLocker locker(&m_mutex);
+
+    if (!m_succeeded) {
+	return QObject::tr("<b>Failed to scan for plugins</b>"
+			   "<p>Failed to scan for plugins at startup. Possibly "
+                           "the plugin checker program was not correctly "
+                           "installed alongside %1?</p>")
+            .arg(QCoreApplication::applicationName());
+    }
+    if (m_kp.empty()) {
+	return QObject::tr("<b>Did not scan for plugins</b>"
+			   "<p>Apparently no scan for plugins was attempted "
+			   "(internal error?)</p>");
+    }
+
+    QString report;
+    for (auto kp: m_kp) {
+        report += QString::fromStdString(kp.second->getFailureReport());
+    }
+    if (report == "") {
+	return report;
+    }
+
+    return QObject::tr("<b>Failed to load plugins</b>"
+		       "<p>Failed to load one or more plugin libraries:</p>")
+	+ report
+        + QObject::tr("<p>These plugins may be incompatible with the system, "
+                      "and will be ignored during this run of %1.</p>")
+        .arg(QCoreApplication::applicationName());
+
+#else
+    return "";
+#endif
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugin/PluginScan.h	Fri Jan 13 10:29:44 2017 +0000
@@ -0,0 +1,85 @@
+/* -*- 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 PLUGIN_SCAN_H
+#define PLUGIN_SCAN_H
+
+#include <QStringList>
+#include <QMutex>
+#include <vector>
+#include <map>
+
+class KnownPlugins;
+
+class PluginScan
+{
+public:
+    static PluginScan *getInstance();
+
+    /**
+     * Carry out startup scan of available plugins. Do not call
+     * getCandidateLibrariesFor() unless this has been called and
+     * scanSucceeded() is returning true.
+     */
+    void scan();
+
+    /**
+     * Return true if scan() completed successfully. If the scan
+     * failed, consider using the normal plugin path to load any
+     * available plugins (as if they had all been found to be
+     * loadable) rather than rejecting all of them -- i.e. consider
+     * falling back on the behaviour of code from before the scan
+     * logic was added.
+     */
+    bool scanSucceeded() const;
+    
+    enum PluginType {
+	VampPlugin,
+	LADSPAPlugin,
+	DSSIPlugin
+    };
+    struct Candidate {
+        QString libraryPath;    // full path, not just soname
+        QString helperTag;      // identifies the helper that found it
+                                // (see HelperExecPath) 
+    };
+
+    /**
+     * Return the candidate plugin libraries of the given type that
+     * were found by helpers during the startup scan.
+     *
+     * This could return an empty list for two reasons: the scan
+     * succeeded but no libraries were found; or the scan failed. Call
+     * scanSucceeded() to distinguish between them.
+     */
+    QList<Candidate> getCandidateLibrariesFor(PluginType) const;
+
+    QString getStartupFailureReport() const;
+
+private:
+    PluginScan();
+    ~PluginScan();
+
+    void clear();
+
+    mutable QMutex m_mutex; // while scanning; definitely can't multi-thread this
+    
+    std::map<QString, KnownPlugins *> m_kp; // tag -> KnownPlugins client
+    bool m_succeeded;
+
+    class Logger;
+    Logger *m_logger;
+};
+
+#endif
--- a/plugin/api/alsa/seq_event.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/plugin/api/alsa/seq_event.h	Fri Jan 13 10:29:44 2017 +0000
@@ -321,7 +321,11 @@
 typedef struct snd_seq_ev_ext {
 	unsigned int len;		/**< length of data */
 	void *ptr;			/**< pointer to data (note: can be 64-bit) */
-} __attribute__((packed)) snd_seq_ev_ext_t;
+}
+#ifdef __GNUC__
+__attribute__((packed))
+#endif
+snd_seq_ev_ext_t;
 
 /** Instrument cluster type */
 typedef unsigned int snd_seq_instr_cluster_t;
--- a/plugin/plugins/SamplePlayer.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/plugin/plugins/SamplePlayer.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -29,6 +29,11 @@
 #include <QDir>
 #include <QFileInfo>
 
+#ifdef Q_OS_WIN
+#include <windows.h>
+#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1
+#endif
+
 #include <sndfile.h>
 #include <samplerate.h>
 #include <iostream>
@@ -395,7 +400,11 @@
     size_t i;
 
     info.format = 0;
+#ifdef Q_OS_WIN
+    file = sf_wchar_open((LPCWSTR)path.utf16(), SFM_READ, &info);
+#else
     file = sf_open(path.toLocal8Bit().data(), SFM_READ, &info);
+#endif
     if (!file) {
 	cerr << "SamplePlayer::loadSampleData: Failed to open file "
 		  << path << ": "
--- a/rdf/RDFImporter.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/rdf/RDFImporter.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -30,7 +30,7 @@
 #include "data/model/NoteModel.h"
 #include "data/model/TextModel.h"
 #include "data/model/RegionModel.h"
-#include "data/model/WaveFileModel.h"
+#include "data/model/ReadOnlyWaveFileModel.h"
 
 #include "data/fileio/FileSource.h"
 #include "data/fileio/CachedFile.h"
@@ -270,7 +270,7 @@
             reporter->setMessage(RDFImporter::tr("Importing audio referenced in RDF..."));
         }
         fs->waitForData();
-        WaveFileModel *newModel = new WaveFileModel(*fs, m_sampleRate);
+        ReadOnlyWaveFileModel *newModel = new ReadOnlyWaveFileModel(*fs, m_sampleRate);
         if (newModel->isOK()) {
             cerr << "Successfully created wave file model from source at \"" << source << "\"" << endl;
             models.push_back(newModel);
--- a/svcore.pro	Mon Nov 21 16:32:58 2016 +0000
+++ b/svcore.pro	Fri Jan 13 10:29:44 2017 +0000
@@ -2,35 +2,10 @@
 TEMPLATE = lib
 
 INCLUDEPATH += ../vamp-plugin-sdk
-DEFINES += HAVE_VAMP HAVE_VAMPHOSTSDK
 
 exists(config.pri) {
     include(config.pri)
 }
-!exists(config.pri) {
-
-    CONFIG += release
-    DEFINES += NDEBUG BUILD_RELEASE NO_TIMING
-
-    win32-g++ {
-        INCLUDEPATH += ../sv-dependency-builds/win32-mingw/include
-        LIBS += -L../sv-dependency-builds/win32-mingw/lib
-    }
-    win32-msvc* {
-        INCLUDEPATH += ../sv-dependency-builds/win32-msvc/include
-        LIBS += -L../sv-dependency-builds/win32-msvc/lib
-    }
-    macx* {
-        INCLUDEPATH += ../sv-dependency-builds/osx/include
-        LIBS += -L../sv-dependency-builds/osx/lib
-    }
-
-    DEFINES += HAVE_BZ2 HAVE_FFTW3 HAVE_FFTW3F HAVE_SNDFILE HAVE_SAMPLERATE HAVE_LIBLO HAVE_MAD HAVE_ID3TAG 
-
-    macx* {
-        DEFINES += HAVE_COREAUDIO
-    }
-}
 
 CONFIG += staticlib qt thread warn_on stl rtti exceptions c++11
 QT += network xml
@@ -38,8 +13,8 @@
 
 TARGET = svcore
 
-DEPENDPATH += . data plugin plugin/api/alsa
-INCLUDEPATH += . data plugin plugin/api/alsa ../dataquay
+DEPENDPATH += . data plugin plugin/api/alsa ../dataquay ../checker ../piper-cpp
+INCLUDEPATH += . data plugin plugin/api/alsa ../dataquay ../checker ../piper-cpp
 OBJECTS_DIR = o
 MOC_DIR = o
 
@@ -52,253 +27,8 @@
 win*:     DEFINES += __WINDOWS_MM__
 solaris*: DEFINES += __RTMIDI_DUMMY_ONLY__
 
-HEADERS += base/AudioLevel.h \
-           base/AudioPlaySource.h \
-           base/BaseTypes.h \
-           base/Clipboard.h \
-           base/Command.h \
-           base/Debug.h \
-           base/Exceptions.h \
-           base/LogRange.h \
-           base/Pitch.h \
-           base/Playable.h \
-           base/PlayParameterRepository.h \
-           base/PlayParameters.h \
-           base/Preferences.h \
-           base/Profiler.h \
-           base/ProgressPrinter.h \
-           base/ProgressReporter.h \
-           base/PropertyContainer.h \
-           base/RangeMapper.h \
-           base/RealTime.h \
-           base/RecentFiles.h \
-           base/Resampler.h \
-           base/ResizeableBitset.h \
-           base/ResourceFinder.h \
-           base/RingBuffer.h \
-           base/Scavenger.h \
-           base/Selection.h \
-           base/Serialiser.h \
-           base/StorageAdviser.h \
-           base/StringBits.h \
-           base/TempDirectory.h \
-           base/TempWriteFile.h \
-           base/TextMatcher.h \
-           base/Thread.h \
-           base/UnitDatabase.h \
-           base/ViewManagerBase.h \
-           base/Window.h \
-           base/XmlExportable.h \
-           base/ZoomConstraint.h
-SOURCES += base/AudioLevel.cpp \
-           base/Clipboard.cpp \
-           base/Command.cpp \
-           base/Debug.cpp \
-           base/Exceptions.cpp \
-           base/LogRange.cpp \
-           base/Pitch.cpp \
-           base/PlayParameterRepository.cpp \
-           base/PlayParameters.cpp \
-           base/Preferences.cpp \
-           base/Profiler.cpp \
-           base/ProgressPrinter.cpp \
-           base/ProgressReporter.cpp \
-           base/PropertyContainer.cpp \
-           base/RangeMapper.cpp \
-           base/RealTime.cpp \
-           base/RecentFiles.cpp \
-           base/Resampler.cpp \
-           base/ResourceFinder.cpp \
-           base/Selection.cpp \
-           base/Serialiser.cpp \
-           base/StorageAdviser.cpp \
-           base/StringBits.cpp \
-           base/TempDirectory.cpp \
-           base/TempWriteFile.cpp \
-           base/TextMatcher.cpp \
-           base/Thread.cpp \
-           base/UnitDatabase.cpp \
-           base/ViewManagerBase.cpp \
-           base/XmlExportable.cpp
+include(files.pri)
 
-HEADERS += data/fft/FFTapi.h \
-           data/fft/FFTCacheReader.h \
-           data/fft/FFTCacheStorageType.h \
-           data/fft/FFTCacheWriter.h \
-           data/fft/FFTDataServer.h \
-           data/fft/FFTFileCacheReader.h \
-           data/fft/FFTFileCacheWriter.h \
-           data/fft/FFTMemoryCache.h \
-           data/fileio/AudioFileReader.h \
-           data/fileio/AudioFileReaderFactory.h \
-           data/fileio/BZipFileDevice.h \
-           data/fileio/CachedFile.h \
-           data/fileio/CodedAudioFileReader.h \
-           data/fileio/CSVFileReader.h \
-           data/fileio/CSVFileWriter.h \
-           data/fileio/CSVFormat.h \
-           data/fileio/DataFileReader.h \
-           data/fileio/DataFileReaderFactory.h \
-           data/fileio/FileFinder.h \
-           data/fileio/FileReadThread.h \
-           data/fileio/FileSource.h \
-           data/fileio/MatrixFile.h \
-           data/fileio/MIDIFileReader.h \
-           data/fileio/MIDIFileWriter.h \
-           data/fileio/MP3FileReader.h \
-           data/fileio/OggVorbisFileReader.h \
-           data/fileio/PlaylistFileReader.h \
-           data/fileio/QuickTimeFileReader.h \
-           data/fileio/CoreAudioFileReader.h \
-           data/fileio/DecodingWavFileReader.h \
-           data/fileio/WavFileReader.h \
-           data/fileio/WavFileWriter.h \
-           data/midi/MIDIEvent.h \
-           data/midi/MIDIInput.h \
-           data/midi/rtmidi/RtError.h \
-           data/midi/rtmidi/RtMidi.h \
-           data/model/AggregateWaveModel.h \
-           data/model/AlignmentModel.h \
-           data/model/Dense3DModelPeakCache.h \
-           data/model/DenseThreeDimensionalModel.h \
-           data/model/DenseTimeValueModel.h \
-           data/model/EditableDenseThreeDimensionalModel.h \
-           data/model/FFTModel.h \
-           data/model/ImageModel.h \
-           data/model/IntervalModel.h \
-           data/model/Labeller.h \
-           data/model/Model.h \
-           data/model/ModelDataTableModel.h \
-           data/model/NoteModel.h \
-           data/model/FlexiNoteModel.h \
-           data/model/PathModel.h \
-           data/model/PowerOfSqrtTwoZoomConstraint.h \
-           data/model/PowerOfTwoZoomConstraint.h \
-           data/model/RangeSummarisableTimeValueModel.h \
-           data/model/RegionModel.h \
-           data/model/SparseModel.h \
-           data/model/SparseOneDimensionalModel.h \
-           data/model/SparseTimeValueModel.h \
-           data/model/SparseValueModel.h \
-           data/model/TabularModel.h \
-           data/model/TextModel.h \
-           data/model/WaveFileModel.h \
-           data/model/WritableWaveFileModel.h \
-           data/osc/OSCMessage.h \
-           data/osc/OSCQueue.h 
-SOURCES += data/fft/FFTapi.cpp \
-           data/fft/FFTDataServer.cpp \
-           data/fft/FFTFileCacheReader.cpp \
-           data/fft/FFTFileCacheWriter.cpp \
-           data/fft/FFTMemoryCache.cpp \
-           data/fileio/AudioFileReader.cpp \
-           data/fileio/AudioFileReaderFactory.cpp \
-           data/fileio/BZipFileDevice.cpp \
-           data/fileio/CachedFile.cpp \
-           data/fileio/CodedAudioFileReader.cpp \
-           data/fileio/CSVFileReader.cpp \
-           data/fileio/CSVFileWriter.cpp \
-           data/fileio/CSVFormat.cpp \
-           data/fileio/DataFileReaderFactory.cpp \
-           data/fileio/FileReadThread.cpp \
-           data/fileio/FileSource.cpp \
-           data/fileio/MatrixFile.cpp \
-           data/fileio/MIDIFileReader.cpp \
-           data/fileio/MIDIFileWriter.cpp \
-           data/fileio/MP3FileReader.cpp \
-           data/fileio/OggVorbisFileReader.cpp \
-           data/fileio/PlaylistFileReader.cpp \
-           data/fileio/QuickTimeFileReader.cpp \
-           data/fileio/CoreAudioFileReader.cpp \
-           data/fileio/DecodingWavFileReader.cpp \
-           data/fileio/WavFileReader.cpp \
-           data/fileio/WavFileWriter.cpp \
-           data/midi/MIDIInput.cpp \
-           data/midi/rtmidi/RtMidi.cpp \
-           data/model/AggregateWaveModel.cpp \
-           data/model/AlignmentModel.cpp \
-           data/model/Dense3DModelPeakCache.cpp \
-           data/model/DenseTimeValueModel.cpp \
-           data/model/EditableDenseThreeDimensionalModel.cpp \
-           data/model/FFTModel.cpp \
-           data/model/Model.cpp \
-           data/model/ModelDataTableModel.cpp \
-           data/model/PowerOfSqrtTwoZoomConstraint.cpp \
-           data/model/PowerOfTwoZoomConstraint.cpp \
-           data/model/RangeSummarisableTimeValueModel.cpp \
-           data/model/WaveFileModel.cpp \
-           data/model/WritableWaveFileModel.cpp \
-           data/osc/OSCMessage.cpp \
-           data/osc/OSCQueue.cpp 
+HEADERS = $$SVCORE_HEADERS
+SOURCES = $$SVCORE_SOURCES
 
-HEADERS += plugin/DSSIPluginFactory.h \
-           plugin/DSSIPluginInstance.h \
-           plugin/FeatureExtractionPluginFactory.h \
-           plugin/LADSPAPluginFactory.h \
-           plugin/LADSPAPluginInstance.h \
-           plugin/PluginIdentifier.h \
-           plugin/PluginXml.h \
-           plugin/RealTimePluginFactory.h \
-           plugin/RealTimePluginInstance.h \
-           plugin/api/dssi.h \
-           plugin/api/ladspa.h \
-           plugin/plugins/SamplePlayer.h \
-           plugin/api/alsa/asoundef.h \
-           plugin/api/alsa/asoundlib.h \
-           plugin/api/alsa/seq.h \
-           plugin/api/alsa/seq_event.h \
-           plugin/api/alsa/seq_midi_event.h \
-           plugin/api/alsa/sound/asequencer.h
-
-
-SOURCES += plugin/DSSIPluginFactory.cpp \
-           plugin/DSSIPluginInstance.cpp \
-           plugin/FeatureExtractionPluginFactory.cpp \
-           plugin/LADSPAPluginFactory.cpp \
-           plugin/LADSPAPluginInstance.cpp \
-           plugin/PluginIdentifier.cpp \
-           plugin/PluginXml.cpp \
-           plugin/RealTimePluginFactory.cpp \
-           plugin/RealTimePluginInstance.cpp \
-           plugin/plugins/SamplePlayer.cpp
-
-!linux* {
-SOURCES += plugin/api/dssi_alsa_compat.c 
-}
-
-HEADERS += rdf/PluginRDFIndexer.h \
-           rdf/PluginRDFDescription.h \
-           rdf/RDFExporter.h \
-           rdf/RDFFeatureWriter.h \
-           rdf/RDFImporter.h \
-           rdf/RDFTransformFactory.h
-SOURCES += rdf/PluginRDFIndexer.cpp \
-           rdf/PluginRDFDescription.cpp \
-           rdf/RDFExporter.cpp \
-           rdf/RDFFeatureWriter.cpp \
-           rdf/RDFImporter.cpp \
-           rdf/RDFTransformFactory.cpp
-
-HEADERS += system/Init.h \
-           system/System.h
-SOURCES += system/Init.cpp \
-           system/System.cpp
-
-HEADERS += transform/CSVFeatureWriter.h \
-           transform/FeatureExtractionModelTransformer.h \
-           transform/FeatureWriter.h \
-           transform/FileFeatureWriter.h \
-           transform/RealTimeEffectModelTransformer.h \
-           transform/Transform.h \
-           transform/TransformDescription.h \
-           transform/TransformFactory.h \
-           transform/ModelTransformer.h \
-           transform/ModelTransformerFactory.h
-SOURCES += transform/CSVFeatureWriter.cpp \
-           transform/FeatureExtractionModelTransformer.cpp \
-           transform/FileFeatureWriter.cpp \
-           transform/RealTimeEffectModelTransformer.cpp \
-           transform/Transform.cpp \
-           transform/TransformFactory.cpp \
-           transform/ModelTransformer.cpp \
-           transform/ModelTransformerFactory.cpp
--- a/system/System.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/system/System.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -1,16 +1,16 @@
 /* -*- 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 file copyright 2006 Chris Cannam and QMUL.
+  Sonic Visualiser
+  An audio file viewer and annotation editor.
+  Centre for Digital Music, Queen Mary, University of London.
+  This file copyright 2006 Chris Cannam and QMUL.
     
-    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.
 */
 
 #include "System.h"
@@ -39,16 +39,16 @@
 
 #ifdef __APPLE__
 extern "C" {
-void *
-rpl_realloc (void *p, size_t n)
-{
-    p = realloc(p, n);
-    if (p == 0 && n == 0)
+    void *
+    rpl_realloc (void *p, size_t n)
     {
-    p = malloc(0);
+        p = realloc(p, n);
+        if (p == 0 && n == 0)
+        {
+            p = malloc(0);
+        }
+        return p;
     }
-    return p;
-}
 }
 #endif
 
@@ -56,25 +56,25 @@
 
 extern "C" {
 
-/* usleep is now in mingw
-void usleep(unsigned long usec)
-{
-    ::Sleep(usec / 1000);
-}
-*/
+#ifdef _MSC_VER
+    void usleep(unsigned long usec)
+    {
+        ::Sleep(usec / 1000);
+    }
+#endif
 
-int gettimeofday(struct timeval *tv, void *tz)
-{
-    union { 
-	long long ns100;  
-	FILETIME ft; 
-    } now; 
+    int gettimeofday(struct timeval *tv, void *tz)
+    {
+        union { 
+            long long ns100;  
+            FILETIME ft; 
+        } now; 
     
-    ::GetSystemTimeAsFileTime(&now.ft); 
-    tv->tv_usec = (long)((now.ns100 / 10LL) % 1000000LL); 
-    tv->tv_sec = (long)((now.ns100 - 116444736000000000LL) / 10000000LL); 
-    return 0;
-}
+        ::GetSystemTimeAsFileTime(&now.ft); 
+        tv->tv_usec = (long)((now.ns100 / 10LL) % 1000000LL); 
+        tv->tv_sec = (long)((now.ns100 - 116444736000000000LL) / 10000000LL); 
+        return 0;
+    }
 
 }
 
@@ -119,7 +119,7 @@
     DWORDLONG ullAvailVirtual;
     DWORDLONG ullAvailExtendedVirtual;
 } lMEMORYSTATUSEX;
-typedef WINBOOL (WINAPI *PFN_MS_EX) (lMEMORYSTATUSEX*);
+typedef BOOL (WINAPI *PFN_MS_EX) (lMEMORYSTATUSEX*);
 #endif
 
 void
@@ -153,10 +153,10 @@
     if (exFound) {
 
         lMEMORYSTATUSEX lms;
-	lms.dwLength = sizeof(lms);
-	if (!ex(&lms)) {
+        lms.dwLength = sizeof(lms);
+        if (!ex(&lms)) {
             cerr << "WARNING: GlobalMemoryStatusEx failed: error code "
-                      << GetLastError() << endl;
+                 << GetLastError() << endl;
             return;
         }
         wavail = lms.ullAvailPhys;
@@ -167,9 +167,9 @@
         /* Fall back to GlobalMemoryStatus which is always available.
            but returns wrong results for physical memory > 4GB  */
 
-	MEMORYSTATUS ms;
-	GlobalMemoryStatus(&ms);
-	wavail = ms.dwAvailPhys;
+        MEMORYSTATUS ms;
+        GlobalMemoryStatus(&ms);
+        wavail = ms.dwAvailPhys;
         wtotal = ms.dwTotalPhys;
     }
 
@@ -211,7 +211,9 @@
 
     char buf[256];
     while (!feof(meminfo)) {
-        fgets(buf, 256, meminfo);
+        if (!fgets(buf, 256, meminfo)) {
+            return;
+        }
         bool isMemFree = (strncmp(buf, "MemFree:", 8) == 0);
         bool isMemTotal = (!isMemFree && (strncmp(buf, "MemTotal:", 9) == 0));
         if (isMemFree || isMemTotal) {
@@ -249,13 +251,13 @@
 #ifdef _WIN32
     ULARGE_INTEGER available, total, totalFree;
     if (GetDiskFreeSpaceExA(path, &available, &total, &totalFree)) {
-	  __int64 a = available.QuadPart;
+        __int64 a = available.QuadPart;
         a /= 1048576;
         if (a > INT_MAX) a = INT_MAX;
         return ssize_t(a);
     } else {
         cerr << "WARNING: GetDiskFreeSpaceEx failed: error code "
-                  << GetLastError() << endl;
+             << GetLastError() << endl;
         return -1;
     }
 #else
@@ -277,7 +279,7 @@
 #ifdef _WIN32
 extern void SystemMemoryBarrier()
 {
-#ifdef __MSVC__
+#ifdef _MSC_VER
     MemoryBarrier();
 #else /* mingw */
     LONG Barrier = 0;
@@ -286,7 +288,7 @@
 #endif
 }
 #else /* !_WIN32 */
-#if !defined(__APPLE__) && ((__GNUC__ < 4) || (__GNUC__ == 4 && __GNUC_MINOR__ == 0))
+#if !defined(__APPLE__) && defined(__GNUC__) && ((__GNUC__ < 4) || (__GNUC__ == 4 && __GNUC_MINOR__ == 0))
 void
 SystemMemoryBarrier()
 {
@@ -325,76 +327,4 @@
 double princarg(double a) { return mod(a + M_PI, -2 * M_PI) + M_PI; }
 float princargf(float a) { return float(princarg(a)); }
 
-#ifdef _WIN32
 
-PluginLoadStatus
-TestPluginLoadability(QString soname, QString descriptorFn)
-{
-    //!!! Can't do the POSIX logic below, but we have no good
-    // alternative here yet
-    return PluginLoadOK;
-}
-
-#else
-
-#include <unistd.h>
-#include <sys/wait.h>
-
-PluginLoadStatus
-TestPluginLoadability(QString soname, QString descriptorFn)
-{
-    //!!! This is POSIX only, no equivalent on Windows, where we'll
-    //!!! have to do something completely different
-    
-    pid_t pid = fork();
-
-    if (pid < 0) {
-        return UnknownPluginLoadStatus; // fork failed
-    }
-
-    if (pid == 0) { // the child process
-
-        void *handle = DLOPEN(soname, RTLD_NOW | RTLD_LOCAL);
-        if (!handle) {
-            cerr << "isPluginLibraryLoadable: Failed to open plugin library \""
-                 << soname << "\": " << dlerror() << "\n";
-            cerr << "exiting with status 1" << endl;
-            exit(1);
-        }
-
-        void *fn = DLSYM(handle, descriptorFn.toLocal8Bit().data());
-        if (!fn) {
-            cerr << "isPluginLibraryLoadable: Failed to find plugin descriptor function \"" << descriptorFn << "\" in library \"" << soname << "\": " << dlerror() << "\n";
-            exit(2);
-        }
-
-//        cerr << "isPluginLibraryLoadable: Successfully loaded library \"" << soname << "\" and retrieved descriptor function" << endl;
-        
-        exit(0);
-
-    } else { // the parent process
-
-        int status = 0;
-
-        do {
-            waitpid(pid, &status, 0);
-        } while (WIFSTOPPED(status));
-
-        if (WIFEXITED(status)) {
-            switch (WEXITSTATUS(status)) {
-            case 0: return PluginLoadOK; // success
-            case 1: return PluginLoadFailedToLoadLibrary;
-            case 2: return PluginLoadFailedToFindDescriptor;
-            default: return PluginLoadFailedElsewhere;
-            }
-        }
-
-        if (WIFSIGNALED(status)) { 
-            return PluginLoadFailedElsewhere;
-        }
-
-        return UnknownPluginLoadStatus;
-    }
-}
-
-#endif
--- a/system/System.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/system/System.h	Fri Jan 13 10:29:44 2017 +0000
@@ -59,10 +59,20 @@
 
 #define getpid _getpid
 
+#if defined(_MSC_VER)
+#include <BaseTsd.h>
+typedef SSIZE_T ssize_t;
+#endif
+
+#ifdef _MSC_VER
 extern "C" {
-/* usleep is now in mingw
 void usleep(unsigned long usec);
-*/
+}
+#else
+#include <unistd.h>
+#endif
+
+extern "C" {
 int gettimeofday(struct timeval *p, void *tz);
 }
 
@@ -75,6 +85,7 @@
 #include <dlfcn.h>
 #include <stdio.h> // for perror
 #include <cmath>
+#include <unistd.h> // sleep + usleep primarily
 
 #define MLOCK(a,b)   ::mlock((a),(b))
 #define MUNLOCK(a,b) (::munlock((a),(b)) ? (::perror("munlock failed"), 0) : 0)
@@ -154,21 +165,6 @@
 extern void StoreStartupLocale();
 extern void RestoreStartupLocale();
 
-enum PluginLoadStatus {
-    UnknownPluginLoadStatus,
-    PluginLoadOK,
-    PluginLoadFailedToLoadLibrary,
-    PluginLoadFailedToFindDescriptor,
-    PluginLoadFailedElsewhere
-};
-
-// Check whether a plugin library is loadable without crashing (may
-// need to spawn an external process to do it). Descriptor fn is the
-// name of a LADSPA/DSSI/Vamp-style descriptor function to try
-// calling; may be an empty string if the plugin doesn't follow that
-// convention.
-PluginLoadStatus TestPluginLoadability(QString soname, QString descriptorFn);
-
 #include <cmath>
 
 #ifndef M_PI
--- a/transform/CSVFeatureWriter.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/transform/CSVFeatureWriter.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -276,7 +276,20 @@
     }
     
     for (unsigned int j = 0; j < f.values.size(); ++j) {
-        stream << m_separator << QString("%1").arg(f.values[j], 0, 'g', m_digits);
+
+        QString number = QString("%1").arg(f.values[j], 0, 'g', m_digits);
+
+        // Qt pre-5.6 zero pads single-digit exponents to two digits;
+        // Qt 5.7+ doesn't by default. But we want both to produce the
+        // same output. Getting the new behaviour from standard APIs
+        // in Qt 5.6 isn't possible I think; getting the old behaviour
+        // from Qt 5.7 is possible but fiddly, involving setting up an
+        // appropriate locale and using the %L specifier. We could
+        // doubtless do it with sprintf but Qt is a known quantity at
+        // this point. Let's just convert the old format to the new.
+        number.replace("e-0", "e-");
+        
+        stream << m_separator << number;
     }
     
     if (f.label != "") {
--- a/transform/FeatureExtractionModelTransformer.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/transform/FeatureExtractionModelTransformer.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -16,6 +16,7 @@
 #include "FeatureExtractionModelTransformer.h"
 
 #include "plugin/FeatureExtractionPluginFactory.h"
+
 #include "plugin/PluginXml.h"
 #include <vamp-hostsdk/Plugin.h>
 
@@ -42,25 +43,23 @@
 FeatureExtractionModelTransformer::FeatureExtractionModelTransformer(Input in,
                                                                      const Transform &transform) :
     ModelTransformer(in, transform),
-    m_plugin(0)
+    m_plugin(0),
+    m_haveOutputs(false)
 {
     SVDEBUG << "FeatureExtractionModelTransformer::FeatureExtractionModelTransformer: plugin " << m_transforms.begin()->getPluginIdentifier() << ", outputName " << m_transforms.begin()->getOutput() << endl;
-
-    initialise();
 }
 
 FeatureExtractionModelTransformer::FeatureExtractionModelTransformer(Input in,
                                                                      const Transforms &transforms) :
     ModelTransformer(in, transforms),
-    m_plugin(0)
+    m_plugin(0),
+    m_haveOutputs(false)
 {
     if (m_transforms.empty()) {
         SVDEBUG << "FeatureExtractionModelTransformer::FeatureExtractionModelTransformer: " << transforms.size() << " transform(s)" << endl;
     } else {
         SVDEBUG << "FeatureExtractionModelTransformer::FeatureExtractionModelTransformer: " << transforms.size() << " transform(s), first has plugin " << m_transforms.begin()->getPluginIdentifier() << ", outputName " << m_transforms.begin()->getOutput() << endl;
     }
-    
-    initialise();
 }
 
 static bool
@@ -74,6 +73,10 @@
 bool
 FeatureExtractionModelTransformer::initialise()
 {
+    // This is (now) called from the run thread. The plugin is
+    // constructed, initialised, used, and destroyed all from a single
+    // thread.
+    
     // All transforms must use the same plugin, parameters, and
     // inputs: they can differ only in choice of plugin output. So we
     // initialise based purely on the first transform in the list (but
@@ -91,7 +94,7 @@
     QString pluginId = primaryTransform.getPluginIdentifier();
 
     FeatureExtractionPluginFactory *factory =
-	FeatureExtractionPluginFactory::instanceFor(pluginId);
+        FeatureExtractionPluginFactory::instance();
 
     if (!factory) {
         m_message = tr("No factory available for feature extraction plugin id \"%1\" (unknown plugin type, or internal error?)").arg(pluginId);
@@ -104,6 +107,9 @@
         return false;
     }
 
+    SVDEBUG << "FeatureExtractionModelTransformer: Instantiating plugin for transform in thread "
+            << QThread::currentThreadId() << endl;
+    
     m_plugin = factory->instantiatePlugin(pluginId, input->getSampleRate());
     if (!m_plugin) {
         m_message = tr("Failed to instantiate plugin \"%1\"").arg(pluginId);
@@ -130,13 +136,13 @@
     }
 
     SVDEBUG << "Initialising feature extraction plugin with channels = "
-              << channelCount << ", step = " << primaryTransform.getStepSize()
-              << ", block = " << primaryTransform.getBlockSize() << endl;
+            << channelCount << ", step = " << primaryTransform.getStepSize()
+            << ", block = " << primaryTransform.getBlockSize() << endl;
 
     if (!m_plugin->initialise(channelCount,
                               primaryTransform.getStepSize(),
                               primaryTransform.getBlockSize())) {
-
+        
         int pstep = primaryTransform.getStepSize();
         int pblock = primaryTransform.getBlockSize();
 
@@ -148,16 +154,24 @@
 
         if (primaryTransform.getStepSize() != pstep ||
             primaryTransform.getBlockSize() != pblock) {
+
+            SVDEBUG << "Initialisation failed, trying again with default step = "
+                    << primaryTransform.getStepSize()
+                    << ", block = " << primaryTransform.getBlockSize() << endl;
             
             if (!m_plugin->initialise(channelCount,
                                       primaryTransform.getStepSize(),
                                       primaryTransform.getBlockSize())) {
 
+                SVDEBUG << "Initialisation failed again" << endl;
+                
                 m_message = tr("Failed to initialise feature extraction plugin \"%1\"").arg(pluginId);
                 return false;
 
             } else {
-
+                
+                SVDEBUG << "Initialisation succeeded this time" << endl;
+                
                 m_message = tr("Feature extraction plugin \"%1\" rejected the given step and block sizes (%2 and %3); using plugin defaults (%4 and %5) instead")
                     .arg(pluginId)
                     .arg(pstep)
@@ -168,9 +182,13 @@
 
         } else {
 
+            SVDEBUG << "Initialisation failed" << endl;
+                
             m_message = tr("Failed to initialise feature extraction plugin \"%1\"").arg(pluginId);
             return false;
         }
+    } else {
+        SVDEBUG << "Initialisation succeeded" << endl;
     }
 
     if (primaryTransform.getPluginVersion() != "") {
@@ -220,15 +238,30 @@
         createOutputModels(j);
     }
 
+    m_outputMutex.lock();
+    m_haveOutputs = true;
+    m_outputsCondition.wakeAll();
+    m_outputMutex.unlock();
+
     return true;
 }
 
 void
+FeatureExtractionModelTransformer::deinitialise()
+{
+    SVDEBUG << "FeatureExtractionModelTransformer: deleting plugin for transform in thread "
+            << QThread::currentThreadId() << endl;
+    
+    delete m_plugin;
+    for (int j = 0; j < (int)m_descriptors.size(); ++j) {
+        delete m_descriptors[j];
+    }
+}
+
+void
 FeatureExtractionModelTransformer::createOutputModels(int n)
 {
     DenseTimeValueModel *input = getConformingInput();
-
-//    cerr << "FeatureExtractionModelTransformer::createOutputModel: sample type " << m_descriptor->sampleType << ", rate " << m_descriptor->sampleRate << endl;
     
     PluginRDFDescription description(m_transforms[n].getPluginIdentifier());
     QString outputId = m_transforms[n].getOutput();
@@ -254,21 +287,33 @@
     }
 
     sv_samplerate_t modelRate = input->getSampleRate();
+    sv_samplerate_t outputRate = modelRate;
     int modelResolution = 1;
 
     if (m_descriptors[n]->sampleType != 
         Vamp::Plugin::OutputDescriptor::OneSamplePerStep) {
-        if (m_descriptors[n]->sampleRate > input->getSampleRate()) {
-            cerr << "WARNING: plugin reports output sample rate as "
-                      << m_descriptors[n]->sampleRate << " (can't display features with finer resolution than the input rate of " << input->getSampleRate() << ")" << endl;
+
+        outputRate = m_descriptors[n]->sampleRate;
+
+        //!!! SV doesn't actually support display of models that have
+        //!!! different underlying rates together -- so we always set
+        //!!! the model rate to be the input model's rate, and adjust
+        //!!! the resolution appropriately.  We can't properly display
+        //!!! data with a higher resolution than the base model at all
+        if (outputRate > input->getSampleRate()) {
+            SVDEBUG << "WARNING: plugin reports output sample rate as "
+                    << outputRate
+                    << " (can't display features with finer resolution than the input rate of "
+                    << modelRate << ")" << endl;
+            outputRate = modelRate;
         }
     }
 
     switch (m_descriptors[n]->sampleType) {
 
     case Vamp::Plugin::OutputDescriptor::VariableSampleRate:
-	if (m_descriptors[n]->sampleRate != 0.0) {
-	    modelResolution = int(round(modelRate / m_descriptors[n]->sampleRate));
+	if (outputRate != 0.0) {
+	    modelResolution = int(round(modelRate / outputRate));
 	}
 	break;
 
@@ -277,18 +322,12 @@
 	break;
 
     case Vamp::Plugin::OutputDescriptor::FixedSampleRate:
-        //!!! SV doesn't actually support display of models that have
-        //!!! different underlying rates together -- so we always set
-        //!!! the model rate to be the input model's rate, and adjust
-        //!!! the resolution appropriately.  We can't properly display
-        //!!! data with a higher resolution than the base model at all
-        if (m_descriptors[n]->sampleRate > input->getSampleRate()) {
-            modelResolution = 1;
-        } else if (m_descriptors[n]->sampleRate <= 0.0) {
-            cerr << "WARNING: Fixed sample-rate plugin reports invalid sample rate " << m_descriptors[n]->sampleRate << "; defaulting to input rate of " << input->getSampleRate() << endl;
+        if (outputRate <= 0.0) {
+            SVDEBUG << "WARNING: Fixed sample-rate plugin reports invalid sample rate " << m_descriptors[n]->sampleRate << "; defaulting to input rate of " << input->getSampleRate() << endl;
             modelResolution = 1;
         } else {
-            modelResolution = int(round(modelRate / m_descriptors[n]->sampleRate));
+            modelResolution = int(round(modelRate / outputRate));
+//            cerr << "modelRate = " << modelRate << ", descriptor rate = " << outputRate << ", modelResolution = " << modelResolution << endl;
         }
 	break;
     }
@@ -479,13 +518,21 @@
     }
 }
 
+void
+FeatureExtractionModelTransformer::awaitOutputModels()
+{
+    m_outputMutex.lock();
+    while (!m_haveOutputs) {
+        m_outputsCondition.wait(&m_outputMutex);
+    }
+    m_outputMutex.unlock();
+}
+
 FeatureExtractionModelTransformer::~FeatureExtractionModelTransformer()
 {
-//    SVDEBUG << "FeatureExtractionModelTransformer::~FeatureExtractionModelTransformer()" << endl;
-    delete m_plugin;
-    for (int j = 0; j < (int)m_descriptors.size(); ++j) {
-        delete m_descriptors[j];
-    }
+    // Parent class dtor set the abandoned flag and waited for the run
+    // thread to exit; the run thread owns the plugin, and should have
+    // destroyed it before exiting (via a call to deinitialise)
 }
 
 FeatureExtractionModelTransformer::Models
@@ -566,6 +613,8 @@
 void
 FeatureExtractionModelTransformer::run()
 {
+    initialise();
+    
     DenseTimeValueModel *input = getConformingInput();
     if (!input) return;
 
@@ -606,9 +655,7 @@
                                    primaryTransform.getWindowType(),
                                    blockSize,
                                    stepSize,
-                                   blockSize,
-                                   false,
-                                   StorageAdviser::PrecisionCritical);
+                                   blockSize);
             if (!model->isOK() || model->getError() != "") {
                 QString err = model->getError();
                 delete model;
@@ -618,7 +665,6 @@
                 //!!! need a better way to handle this -- previously we were using a QMessageBox but that isn't an appropriate thing to do here either
                 throw AllocationFailed("Failed to create the FFT model for this feature extraction model transformer: error is: " + err);
             }
-            model->resume();
             fftModels.push_back(model);
             cerr << "created model for channel " << ch << endl;
         }
@@ -700,7 +746,7 @@
                 }                    
                 error = fftModels[ch]->getError();
                 if (error != "") {
-                    cerr << "FeatureExtractionModelTransformer::run: Abandoning, error is " << error << endl;
+                    SVDEBUG << "FeatureExtractionModelTransformer::run: Abandoning, error is " << error << endl;
                     m_abandoned = true;
                     m_message = error;
                     break;
@@ -761,6 +807,8 @@
         delete[] buffers[ch];
     }
     delete[] buffers;
+
+    deinitialise();
 }
 
 void
@@ -790,31 +838,28 @@
 
     if (channelCount == 1) {
 
-        got = input->getData(m_input.getChannel(), startFrame, size,
-                             buffers[0] + offset);
+        auto data = input->getData(m_input.getChannel(), startFrame, size);
+        got = data.size();
+
+        copy(data.begin(), data.end(), buffers[0] + offset);
 
         if (m_input.getChannel() == -1 && input->getChannelCount() > 1) {
             // use mean instead of sum, as plugin input
             float cc = float(input->getChannelCount());
-            for (sv_frame_t i = 0; i < size; ++i) {
+            for (sv_frame_t i = 0; i < got; ++i) {
                 buffers[0][i + offset] /= cc;
             }
         }
 
     } else {
 
-        float **writebuf = buffers;
-        if (offset > 0) {
-            writebuf = new float *[channelCount];
-            for (int i = 0; i < channelCount; ++i) {
-                writebuf[i] = buffers[i] + offset;
+        auto data = input->getMultiChannelData(0, channelCount-1, startFrame, size);
+        if (!data.empty()) {
+            got = data[0].size();
+            for (int c = 0; in_range_for(data, c); ++c) {
+                copy(data[c].begin(), data[c].end(), buffers[c] + offset);
             }
         }
-
-        got = input->getMultiChannelData
-            (0, channelCount-1, startFrame, size, writebuf);
-
-        if (writebuf != buffers) delete[] writebuf;
     }
 
     while (got < size) {
@@ -844,7 +889,7 @@
 	Vamp::Plugin::OutputDescriptor::VariableSampleRate) {
 
 	if (!feature.hasTimestamp) {
-	    cerr
+	    SVDEBUG
 		<< "WARNING: FeatureExtractionModelTransformer::addFeature: "
 		<< "Feature has variable sample rate but no timestamp!"
 		<< endl;
@@ -880,7 +925,7 @@
     }
 
     if (frame < 0) {
-        cerr
+        SVDEBUG
             << "WARNING: FeatureExtractionModelTransformer::addFeature: "
             << "Negative frame counts are not supported (frame = " << frame
             << " from timestamp " << feature.timestamp
@@ -1013,8 +1058,7 @@
 	
     } else if (isOutput<EditableDenseThreeDimensionalModel>(n)) {
 	
-	DenseThreeDimensionalModel::Column values =
-            DenseThreeDimensionalModel::Column::fromStdVector(feature.values);
+	DenseThreeDimensionalModel::Column values = feature.values;
 	
 	EditableDenseThreeDimensionalModel *model =
             getConformingOutput<EditableDenseThreeDimensionalModel>(n);
--- a/transform/FeatureExtractionModelTransformer.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/transform/FeatureExtractionModelTransformer.h	Fri Jan 13 10:29:44 2017 +0000
@@ -19,6 +19,8 @@
 #include "ModelTransformer.h"
 
 #include <QString>
+#include <QMutex>
+#include <QWaitCondition>
 
 #include <vamp-hostsdk/Plugin.h>
 
@@ -28,7 +30,7 @@
 class DenseTimeValueModel;
 class SparseTimeValueModel;
 
-class FeatureExtractionModelTransformer : public ModelTransformer
+class FeatureExtractionModelTransformer : public ModelTransformer // + is a Thread
 {
     Q_OBJECT
 
@@ -50,6 +52,7 @@
 
 protected:
     bool initialise();
+    void deinitialise();
 
     virtual void run();
 
@@ -74,7 +77,12 @@
     void getFrames(int channelCount, sv_frame_t startFrame, sv_frame_t size,
                    float **buffer);
 
-    // just casts
+    bool m_haveOutputs;
+    QMutex m_outputMutex;
+    QWaitCondition m_outputsCondition;
+    void awaitOutputModels();
+    
+    // just casts:
 
     DenseTimeValueModel *getConformingInput();
 
--- a/transform/ModelTransformer.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/transform/ModelTransformer.h	Fri Jan 13 10:29:44 2017 +0000
@@ -89,16 +89,20 @@
      * be initialised; an error message may be available via
      * getMessage() in this situation.
      */
-    Models getOutputModels() { return m_outputs; }
+    Models getOutputModels() {
+        awaitOutputModels();
+        return m_outputs;
+    }
 
     /**
      * Return the set of output models, also detaching them from the
      * transformer so that they will not be deleted when the
      * transformer is.  The caller takes ownership of the models.
      */
-    Models detachOutputModels() { 
+    Models detachOutputModels() {
+        awaitOutputModels();
         m_detached = true; 
-        return getOutputModels(); 
+        return m_outputs;
     }
 
     /**
@@ -138,6 +142,8 @@
     ModelTransformer(Input input, const Transform &transform);
     ModelTransformer(Input input, const Transforms &transforms);
 
+    virtual void awaitOutputModels() = 0;
+    
     Transforms m_transforms;
     Input m_input; // I don't own the model in this
     Models m_outputs; // I own this, unless...
--- a/transform/ModelTransformerFactory.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/transform/ModelTransformerFactory.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -89,29 +89,21 @@
     bool ok = true;
     QString configurationXml = m_lastConfigurations[transform.getIdentifier()];
 
-    cerr << "last configuration: " << configurationXml << endl;
+    SVDEBUG << "ModelTransformer: last configuration for identifier " << transform.getIdentifier() << ": " << configurationXml << endl;
 
     Vamp::PluginBase *plugin = 0;
 
-    if (FeatureExtractionPluginFactory::instanceFor(id)) {
+    if (RealTimePluginFactory::instanceFor(id)) {
 
-        cerr << "getConfigurationForTransform: instantiating Vamp plugin" << endl;
-
-        Vamp::Plugin *vp =
-            FeatureExtractionPluginFactory::instanceFor(id)->instantiatePlugin
-            (id, float(inputModel->getSampleRate()));
-
-        plugin = vp;
-
-    } else if (RealTimePluginFactory::instanceFor(id)) {
-
+        SVDEBUG << "ModelTransformerFactory::getConfigurationForTransform: instantiating real-time plugin" << endl;
+        
         RealTimePluginFactory *factory = RealTimePluginFactory::instanceFor(id);
 
         sv_samplerate_t sampleRate = inputModel->getSampleRate();
         int blockSize = 1024;
         int channels = 1;
         if (source) {
-            sampleRate = source->getTargetSampleRate();
+            sampleRate = source->getSourceSampleRate();
             blockSize = source->getTargetBlockSize();
             channels = source->getTargetChannelCount();
         }
@@ -120,6 +112,16 @@
             (id, 0, 0, sampleRate, blockSize, channels);
 
         plugin = rtp;
+
+    } else {
+
+        SVDEBUG << "ModelTransformerFactory::getConfigurationForTransform: instantiating Vamp plugin" << endl;
+
+        Vamp::Plugin *vp =
+            FeatureExtractionPluginFactory::instance()->instantiatePlugin
+            (id, float(inputModel->getSampleRate()));
+
+        plugin = vp;
     }
 
     if (plugin) {
@@ -152,6 +154,8 @@
 
         configurationXml = PluginXml(plugin).toXmlString();
 
+        SVDEBUG << "ModelTransformerFactory::getConfigurationForTransform: got configuration, deleting plugin" << endl;
+        
         delete plugin;
     }
 
@@ -171,20 +175,15 @@
 
     QString id = transforms[0].getPluginIdentifier();
 
-    if (FeatureExtractionPluginFactory::instanceFor(id)) {
-
-        transformer =
-            new FeatureExtractionModelTransformer(input, transforms);
-
-    } else if (RealTimePluginFactory::instanceFor(id)) {
+    if (RealTimePluginFactory::instanceFor(id)) {
 
         transformer =
             new RealTimeEffectModelTransformer(input, transforms[0]);
 
     } else {
-        SVDEBUG << "ModelTransformerFactory::createTransformer: Unknown transform \""
-                  << transforms[0].getIdentifier() << "\"" << endl;
-        return transformer;
+
+        transformer =
+            new FeatureExtractionModelTransformer(input, transforms);
     }
 
     if (transformer) transformer->setObjectName(transforms[0].getIdentifier());
--- a/transform/RealTimeEffectModelTransformer.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/transform/RealTimeEffectModelTransformer.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -191,10 +191,14 @@
 
 	if (channelCount == 1) {
             if (inbufs && inbufs[0]) {
-                got = input->getData
-                    (m_input.getChannel(), blockFrame, blockSize, inbufs[0]);
+                auto data = input->getData
+                    (m_input.getChannel(), blockFrame, blockSize);
+                got = data.size();
+                for (sv_frame_t i = 0; i < got; ++i) {
+                    inbufs[0][i] = data[i];
+                }
                 while (got < blockSize) {
-                    inbufs[0][got++] = 0.0;
+                    inbufs[0][got++] = 0.f;
                 }          
                 for (int ch = 1; ch < (int)m_plugin->getAudioInputCount(); ++ch) {
                     for (sv_frame_t i = 0; i < blockSize; ++i) {
@@ -204,9 +208,14 @@
             }
 	} else {
             if (inbufs && inbufs[0]) {
-                got = input->getMultiChannelData(0, channelCount - 1,
-                                                 blockFrame, blockSize,
-                                                 inbufs);
+                auto data = input->getMultiChannelData
+                    (0, channelCount - 1, blockFrame, blockSize);
+                if (!data.empty()) got = data[0].size();
+                for (int ch = 0; ch < channelCount; ++ch) {
+                    for (sv_frame_t i = 0; i < got; ++i) {
+                        inbufs[ch][i] = data[ch][i];
+                    }
+                }
                 while (got < blockSize) {
                     for (int ch = 0; ch < channelCount; ++ch) {
                         inbufs[ch][got] = 0.0;
@@ -273,8 +282,10 @@
         }
 
 	if (blockFrame == contextStart || completion > prevCompletion) {
+            // This setCompletion is probably misusing the completion
+            // terminology, just as it was for WritableWaveFileModel
 	    if (stvm) stvm->setCompletion(completion);
-	    if (wwfm) wwfm->setCompletion(completion);
+	    if (wwfm) wwfm->setWriteProportion(completion);
 	    prevCompletion = completion;
 	}
         
@@ -284,6 +295,6 @@
     if (m_abandoned) return;
     
     if (stvm) stvm->setCompletion(100);
-    if (wwfm) wwfm->setCompletion(100);
+    if (wwfm) wwfm->writeComplete();
 }
 
--- a/transform/RealTimeEffectModelTransformer.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/transform/RealTimeEffectModelTransformer.h	Fri Jan 13 10:29:44 2017 +0000
@@ -31,6 +31,8 @@
 protected:
     virtual void run();
 
+    virtual void awaitOutputModels() { } // they're created synchronously
+    
     QString m_units;
     RealTimePluginInstance *m_plugin;
     int m_outputNo;
--- a/transform/Transform.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/transform/Transform.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -202,12 +202,10 @@
 Transform::Type
 Transform::getType() const
 {
-    if (FeatureExtractionPluginFactory::instanceFor(getPluginIdentifier())) {
-        return FeatureExtraction;
-    } else if (RealTimePluginFactory::instanceFor(getPluginIdentifier())) {
+    if (RealTimePluginFactory::instanceFor(getPluginIdentifier())) {
         return RealTimeEffect;
     } else {
-        return UnknownType;
+        return FeatureExtraction;
     }
 }
 
--- a/transform/TransformFactory.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/transform/TransformFactory.cpp	Fri Jan 13 10:29:44 2017 +0000
@@ -16,6 +16,7 @@
 #include "TransformFactory.h"
 
 #include "plugin/FeatureExtractionPluginFactory.h"
+
 #include "plugin/RealTimePluginFactory.h"
 #include "plugin/RealTimePluginInstance.h"
 #include "plugin/PluginXml.h"
@@ -399,95 +400,80 @@
     m_transformsPopulated = true;
 }
 
-QString
-TransformFactory::getPluginPopulationWarning()
-{
-    FeatureExtractionPluginFactory *vfactory =
-        FeatureExtractionPluginFactory::instance("vamp");
-    QString warningMessage;
-    if (vfactory) {
-        warningMessage = vfactory->getPluginPopulationWarning();
-    }
-    return warningMessage;
-}
-
 void
 TransformFactory::populateFeatureExtractionPlugins(TransformDescriptionMap &transforms)
 {
-    std::vector<QString> plugs =
-	FeatureExtractionPluginFactory::getAllPluginIdentifiers();
+    FeatureExtractionPluginFactory *factory =
+        FeatureExtractionPluginFactory::instance();
+
+    QString errorMessage;
+    std::vector<QString> plugs = factory->getPluginIdentifiers(errorMessage);
+    if (errorMessage != "") {
+        m_errorString = tr("Failed to list Vamp plugins: %1").arg(errorMessage);
+    }
+    
     if (m_exiting) return;
 
     for (int i = 0; i < (int)plugs.size(); ++i) {
 
 	QString pluginId = plugs[i];
 
-	FeatureExtractionPluginFactory *factory =
-	    FeatureExtractionPluginFactory::instanceFor(pluginId);
+        piper_vamp::PluginStaticData psd = factory->getPluginStaticData(pluginId);
 
-	if (!factory) {
-	    cerr << "WARNING: TransformFactory::populateTransforms: No feature extraction plugin factory for instance " << pluginId << endl;
-	    continue;
-	}
+        if (psd.pluginKey == "") {
+            cerr << "WARNING: TransformFactory::populateTransforms: No plugin static data available for instance " << pluginId << endl;
+            continue;
+        }
 
-	Vamp::Plugin *plugin = 
-	    factory->instantiatePlugin(pluginId, 44100);
+        QString pluginName = QString::fromStdString(psd.basic.name);
+        QString category = factory->getPluginCategory(pluginId);
+        
+        const auto &basicOutputs = psd.basicOutputInfo;
 
-	if (!plugin) {
-	    cerr << "WARNING: TransformFactory::populateTransforms: Failed to instantiate plugin " << pluginId << endl;
-	    continue;
-	}
-		
-	QString pluginName = plugin->getName().c_str();
-        QString category = factory->getPluginCategory(pluginId);
+        for (const auto &o: basicOutputs) {
 
-	Vamp::Plugin::OutputList outputs =
-	    plugin->getOutputDescriptors();
-
-	for (int j = 0; j < (int)outputs.size(); ++j) {
+            QString outputName = QString::fromStdString(o.name);
 
 	    QString transformId = QString("%1:%2")
-		    .arg(pluginId).arg(outputs[j].identifier.c_str());
+                .arg(pluginId).arg(QString::fromStdString(o.identifier));
 
 	    QString userName;
             QString friendlyName;
-            QString units = outputs[j].unit.c_str();
-            QString description = plugin->getDescription().c_str();
-            QString maker = plugin->getMaker().c_str();
+//!!! return to this            QString units = outputs[j].unit.c_str();
+            QString description = QString::fromStdString(psd.basic.description);
+            QString maker = QString::fromStdString(psd.maker);
             if (maker == "") maker = tr("<unknown maker>");
 
             QString longDescription = description;
 
             if (longDescription == "") {
-                if (outputs.size() == 1) {
+                if (basicOutputs.size() == 1) {
                     longDescription = tr("Extract features using \"%1\" plugin (from %2)")
                         .arg(pluginName).arg(maker);
                 } else {
                     longDescription = tr("Extract features using \"%1\" output of \"%2\" plugin (from %3)")
-                        .arg(outputs[j].name.c_str()).arg(pluginName).arg(maker);
+                        .arg(outputName).arg(pluginName).arg(maker);
                 }
             } else {
-                if (outputs.size() == 1) {
+                if (basicOutputs.size() == 1) {
                     longDescription = tr("%1 using \"%2\" plugin (from %3)")
                         .arg(longDescription).arg(pluginName).arg(maker);
                 } else {
                     longDescription = tr("%1 using \"%2\" output of \"%3\" plugin (from %4)")
-                        .arg(longDescription).arg(outputs[j].name.c_str()).arg(pluginName).arg(maker);
+                        .arg(longDescription).arg(outputName).arg(pluginName).arg(maker);
                 }
             }                    
 
-	    if (outputs.size() == 1) {
+	    if (basicOutputs.size() == 1) {
 		userName = pluginName;
                 friendlyName = pluginName;
 	    } else {
-		userName = QString("%1: %2")
-		    .arg(pluginName)
-		    .arg(outputs[j].name.c_str());
-                friendlyName = outputs[j].name.c_str();
+		userName = QString("%1: %2").arg(pluginName).arg(outputName);
+                friendlyName = outputName;
 	    }
 
-            bool configurable = (!plugin->getPrograms().empty() ||
-                                 !plugin->getParameterDescriptors().empty());
+            bool configurable = (!psd.programs.empty() ||
+                                 !psd.parameters.empty());
 
 #ifdef DEBUG_TRANSFORM_FACTORY
             cerr << "Feature extraction plugin transform: " << transformId << " friendly name: " << friendlyName << endl;
@@ -502,11 +488,10 @@
                                      description,
                                      longDescription,
                                      maker,
-                                     units,
+//!!!                                     units,
+                                     "",
                                      configurable);
 	}
-
-        delete plugin;
     }
 }
 
@@ -768,6 +753,9 @@
     t.setIdentifier(id);
     if (rate != 0) t.setSampleRate(rate);
 
+    SVDEBUG << "TransformFactory::getDefaultTransformFor: identifier \""
+            << id << "\"" << endl;
+    
     Vamp::PluginBase *plugin = instantiateDefaultPluginFor(id, rate);
 
     if (plugin) {
@@ -783,6 +771,9 @@
 Vamp::PluginBase *
 TransformFactory::instantiatePluginFor(const Transform &transform)
 {
+    SVDEBUG << "TransformFactory::instantiatePluginFor: identifier \""
+            << transform.getIdentifier() << "\"" << endl;
+    
     Vamp::PluginBase *plugin = instantiateDefaultPluginFor
         (transform.getIdentifier(), transform.getSampleRate());
 
@@ -806,11 +797,11 @@
 
     if (t.getType() == Transform::FeatureExtraction) {
 
-//        cerr << "TransformFactory::instantiateDefaultPluginFor: identifier \""
-//             << identifier << "\" is a feature extraction transform" << endl;
+        SVDEBUG << "TransformFactory::instantiateDefaultPluginFor: identifier \""
+                << identifier << "\" is a feature extraction transform" << endl;
         
-        FeatureExtractionPluginFactory *factory = 
-            FeatureExtractionPluginFactory::instanceFor(pluginId);
+        FeatureExtractionPluginFactory *factory =
+            FeatureExtractionPluginFactory::instance();
 
         if (factory) {
             plugin = factory->instantiatePlugin(pluginId, rate);
@@ -818,8 +809,8 @@
 
     } else if (t.getType() == Transform::RealTimeEffect) {
 
-//        cerr << "TransformFactory::instantiateDefaultPluginFor: identifier \""
-//             << identifier << "\" is a real-time transform" << endl;
+        SVDEBUG << "TransformFactory::instantiateDefaultPluginFor: identifier \""
+                << identifier << "\" is a real-time transform" << endl;
 
         RealTimePluginFactory *factory = 
             RealTimePluginFactory::instanceFor(pluginId);
@@ -829,8 +820,8 @@
         }
 
     } else {
-        cerr << "TransformFactory: ERROR: transform id \""
-             << identifier << "\" is of unknown type" << endl;
+        SVDEBUG << "TransformFactory: ERROR: transform id \""
+                << identifier << "\" is of unknown type" << endl;
     }
 
     return plugin;
@@ -899,6 +890,9 @@
     Transform transform;
     transform.setIdentifier(identifier);
 
+    SVDEBUG << "TransformFactory::getTransformInputDomain: identifier \""
+            << identifier << "\"" << endl;
+    
     if (transform.getType() != Transform::FeatureExtraction) {
         return Vamp::Plugin::TimeDomain;
     }
@@ -929,22 +923,7 @@
 {
     QString id = identifier.section(':', 0, 2);
 
-    if (FeatureExtractionPluginFactory::instanceFor(id)) {
-
-        Vamp::Plugin *plugin = 
-            FeatureExtractionPluginFactory::instanceFor(id)->
-            instantiatePlugin(id, 44100);
-        if (!plugin) return false;
-
-        min = (int)plugin->getMinChannelCount();
-        max = (int)plugin->getMaxChannelCount();
-        delete plugin;
-
-        return true;
-
-    } else if (RealTimePluginFactory::instanceFor(id)) {
-
-        // don't need to instantiate
+    if (RealTimePluginFactory::instanceFor(id)) {
 
         const RealTimePluginDescriptor *descriptor = 
             RealTimePluginFactory::instanceFor(id)->
@@ -955,6 +934,17 @@
         max = descriptor->audioInputPortCount;
 
         return true;
+
+    } else {
+
+        auto psd = FeatureExtractionPluginFactory::instance()->
+            getPluginStaticData(id);
+        if (psd.pluginKey == "") return false;
+
+        min = (int)psd.minChannelCount;
+        max = (int)psd.maxChannelCount;
+
+        return true;
     }
 
     return false;
@@ -1088,12 +1078,15 @@
 {
     QString xml;
 
+    SVDEBUG << "TransformFactory::getPluginConfigurationXml: identifier \""
+            << t.getIdentifier() << "\"" << endl;
+
     Vamp::PluginBase *plugin = instantiateDefaultPluginFor
         (t.getIdentifier(), 0);
     if (!plugin) {
-        cerr << "TransformFactory::getPluginConfigurationXml: "
-                  << "Unable to instantiate plugin for transform \""
-                  << t.getIdentifier() << "\"" << endl;
+        SVDEBUG << "TransformFactory::getPluginConfigurationXml: "
+                << "Unable to instantiate plugin for transform \""
+                << t.getIdentifier() << "\"" << endl;
         return xml;
     }
 
@@ -1110,12 +1103,15 @@
 TransformFactory::setParametersFromPluginConfigurationXml(Transform &t,
                                                           QString xml)
 {
+    SVDEBUG << "TransformFactory::setParametersFromPluginConfigurationXml: identifier \""
+            << t.getIdentifier() << "\"" << endl;
+
     Vamp::PluginBase *plugin = instantiateDefaultPluginFor
         (t.getIdentifier(), 0);
     if (!plugin) {
-        cerr << "TransformFactory::setParametersFromPluginConfigurationXml: "
-                  << "Unable to instantiate plugin for transform \""
-                  << t.getIdentifier() << "\"" << endl;
+        SVDEBUG << "TransformFactory::setParametersFromPluginConfigurationXml: "
+                << "Unable to instantiate plugin for transform \""
+                << t.getIdentifier() << "\"" << endl;
         return;
     }
 
@@ -1169,14 +1165,14 @@
     if (!m_uninstalledTransformsMutex.tryLock()) {
         // uninstalled transforms are being populated; this may take some time,
         // and they aren't critical, but we will speed them up if necessary
-        cerr << "TransformFactory::search: Uninstalled transforms mutex is held, skipping" << endl;
+        SVDEBUG << "TransformFactory::search: Uninstalled transforms mutex is held, skipping" << endl;
         m_populatingSlowly = false;
         return results;
     }
 
     if (!m_uninstalledTransformsPopulated) {
-        cerr << "WARNING: TransformFactory::search: Uninstalled transforms are not populated yet" << endl
-                  << "and are not being populated either -- was the thread not started correctly?" << endl;
+        SVDEBUG << "WARNING: TransformFactory::search: Uninstalled transforms are not populated yet" << endl
+                << "and are not being populated either -- was the thread not started correctly?" << endl;
         m_uninstalledTransformsMutex.unlock();
         return results;
     }
--- a/transform/TransformFactory.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/transform/TransformFactory.h	Fri Jan 13 10:29:44 2017 +0000
@@ -195,14 +195,10 @@
      */
     void setParametersFromPluginConfigurationXml(Transform &transform,
                                                  QString xml);
-
-    /**
-     * Return any error message arising from the initial plugin
-     * scan. The return value will either be an empty string (nothing
-     * to report) or an HTML string suitable for dropping into a
-     * dialog and showing the user.
-     */
-    QString getPluginPopulationWarning();
+    
+    QString getStartupFailureReport() const {
+        return m_errorString;
+    }
     
 protected:
     typedef std::map<TransformId, TransformDescription> TransformDescriptionMap;
@@ -213,6 +209,8 @@
     TransformDescriptionMap m_uninstalledTransforms;
     bool m_uninstalledTransformsPopulated;
 
+    QString m_errorString;
+    
     void populateTransforms();
     void populateUninstalledTransforms();
     void populateFeatureExtractionPlugins(TransformDescriptionMap &);