changeset 1273:0b2a2ebf59c9 3.0-integration

Merge from default branch
author Chris Cannam
date Mon, 21 Nov 2016 16:54:37 +0000
parents 604f369c247a (diff) 6a7ea3bd0e10 (current diff)
children 6974bd4efdb5
files
diffstat 137 files changed, 6685 insertions(+), 8343 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/ColumnOp.cpp	Mon Nov 21 16:54:37 2016 +0000
@@ -0,0 +1,162 @@
+/* -*- 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>
+
+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());
+
+    for (int y = 0; y < h; ++y) {
+
+        if (interpolate && h > bins) {
+
+            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);
+                
+            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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -21,7 +21,7 @@
 #include <QUrl>
 #include <QCoreApplication>
 
-#ifndef NDEBUG
+#include <stdexcept>
 
 static SVDebug *debug = 0;
 static QMutex mutex;
@@ -40,6 +40,11 @@
     m_ok(false),
     m_eol(false)
 {
+    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"));
 
@@ -76,8 +81,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	Mon Nov 21 16:54:37 2016 +0000
@@ -36,8 +36,6 @@
 using std::cerr;
 using std::endl;
 
-#ifndef NDEBUG
-
 class SVDebug {
 public:
     SVDebug();
@@ -72,23 +70,5 @@
 
 #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 */
-
 #endif /* !_DEBUG_H_ */
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/HelperExecPath.cpp	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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 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() {
+	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;
+    }
+
+    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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -41,6 +41,7 @@
     m_propertyBoxLayout(VerticallyStacked),
     m_windowType(HanningWindow),
     m_resampleQuality(1),
+    m_runPluginsInProcess(true),
     m_omitRecentTemps(true),
     m_tempDirRoot(""),
     m_fixedSampleRate(0),
@@ -65,6 +66,7 @@
     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_normaliseAudio = settings.value("normalise-audio", false).toBool();
@@ -266,6 +268,10 @@
         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;
@@ -414,6 +420,8 @@
         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") {
@@ -519,6 +527,19 @@
 }
 
 void
+Preferences::setRunPluginsInProcess(bool run)
+{
+    if (m_runPluginsInProcess != run) {
+        m_runPluginsInProcess = run;
+        QSettings settings;
+        settings.beginGroup("Preferences");
+        settings.setValue("run-vamp-plugins-in-process", run);
+        settings.endGroup();
+        emit propertyChanged("Run Vamp Plugins In Process");
+    }
+}
+
+void
 Preferences::setOmitTempsFromRecentFiles(bool omit)
 {
     if (m_omitRecentTemps != omit) {
--- a/base/Preferences.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/Preferences.h	Mon Nov 21 16:54:37 2016 +0000
@@ -53,6 +53,8 @@
     WindowType getWindowType() const { return m_windowType; }
     int getResampleQuality() const { return m_resampleQuality; }
 
+    bool getRunPluginsInProcess() const { return m_runPluginsInProcess; }
+    
     //!!! harmonise with PaneStack
     enum PropertyBoxLayout {
         VerticallyStacked,
@@ -114,6 +116,7 @@
     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);
@@ -151,6 +154,7 @@
     PropertyBoxLayout m_propertyBoxLayout;
     WindowType m_windowType;
     int m_resampleQuality;
+    bool m_runPluginsInProcess;
     bool m_omitRecentTemps;
     QString m_tempDirRoot;
     sv_samplerate_t m_fixedSampleRate;
--- a/base/Profiler.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/Profiler.cpp	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -59,6 +59,12 @@
     return QString();
 }
 
+QString
+PropertyContainer::getPropertyValueIconName(const PropertyName &, int) const
+{
+    return QString();
+}
+
 RangeMapper *
 PropertyContainer::getNewPropertyRangeMapper(const PropertyName &) const
 {
--- a/base/PropertyContainer.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/PropertyContainer.h	Mon Nov 21 16:54:37 2016 +0000
@@ -91,6 +91,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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -0,0 +1,481 @@
+/* -*- 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;
+
+    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);
+
+    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();
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/ResamplerSV.cpp	Mon Nov 21 16:54:37 2016 +0000
@@ -0,0 +1,196 @@
+/* -*- 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/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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -36,9 +36,9 @@
 			  size_t maximumSize)
 {
 #ifdef DEBUG_STORAGE_ADVISER
-    SVDEBUG << "StorageAdviser::recommend: Criteria " << criteria 
-              << ", minimumSize " << minimumSize
-              << ", maximumSize " << maximumSize << endl;
+    cerr << "StorageAdviser::recommend: Criteria " << criteria 
+         << ", minimumSize " << minimumSize
+         << ", maximumSize " << maximumSize << endl;
 #endif
 
     if (m_baseRecommendation != NoRecommendation) {
@@ -91,7 +91,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;
@@ -181,6 +181,10 @@
         }
     }
 
+#ifdef DEBUG_STORAGE_ADVISER
+    cerr << "StorageAdviser: returning recommendation " << recommendation << endl;
+#endif
+    
     return Recommendation(recommendation);
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/Strings.cpp	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -25,7 +25,7 @@
 
 #include <iostream>
 #include <cassert>
-#include <unistd.h>
+
 #include <time.h>
 
 TempDirectory *
--- a/base/Window.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/base/Window.h	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -0,0 +1,259 @@
+/* -*- 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>
+
+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);
+    }
+    
+        
+};
+    
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/TestOurRealTime.h	Mon Nov 21 16:54:37 2016 +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/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	Mon Nov 21 16:54:37 2016 +0000
@@ -0,0 +1,379 @@
+/* -*- 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) {
+        QCOMPARE(QString(s.c_str()), QString(e));
+    }
+
+    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));
+    }
+    
+    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 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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -17,15 +17,17 @@
 
 using std::vector;
 
-vector<SampleBlock>
+vector<vector<float>>
 AudioFileReader::getDeInterleavedFrames(sv_frame_t start, sv_frame_t count) const
 {
-    SampleBlock interleaved = getInterleavedFrames(start, count);
+    vector<float> 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<vector<float>> frames(channels, vector<float>(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	Mon Nov 21 16:54:37 2016 +0000
@@ -24,8 +24,6 @@
 #include <vector>
 #include <map>
 
-typedef std::vector<float> SampleBlock;
-
 class AudioFileReader : public QObject
 {
     Q_OBJECT
@@ -85,16 +83,14 @@
 
     /** 
      * 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 std::vector<float> getInterleavedFrames(sv_frame_t start, sv_frame_t count) const = 0;
 
     /**
      * Return de-interleaved samples for count frames from index
@@ -103,7 +99,7 @@
      * 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<std::vector<float> > 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	Mon Nov 21 16:54:37 2016 +0000
@@ -21,6 +21,9 @@
 #include "MP3FileReader.h"
 #include "QuickTimeFileReader.h"
 #include "CoreAudioFileReader.h"
+#include "AudioFileSizeEstimator.h"
+
+#include "base/StorageAdviser.h"
 
 #include <QString>
 #include <QFileInfo>
@@ -102,9 +105,32 @@
 
     AudioFileReader *reader = 0;
 
+    sv_frame_t estimatedSamples = 
+        AudioFileSizeEstimator::estimate(source, targetRate);
+    
+    CodedAudioFileReader::CacheMode cacheMode =
+        CodedAudioFileReader::CacheInTemporaryFile;
+
+    if (estimatedSamples > 0) {
+        size_t kb = (estimatedSamples * sizeof(float)) / 1024;
+        StorageAdviser::Recommendation rec =
+            StorageAdviser::recommend(StorageAdviser::SpeedCritical, kb, kb);
+        if (rec == StorageAdviser::UseMemory ||
+            rec == StorageAdviser::PreferMemory) {
+            cacheMode = CodedAudioFileReader::CacheInMemory;
+        }
+    }
+    
+    CodedAudioFileReader::DecodeMode decodeMode =
+        (threading ?
+         CodedAudioFileReader::DecodeThreaded :
+         CodedAudioFileReader::DecodeAtOnce);
+    
     // Try to construct a preferred reader based on the extension or
     // MIME type.
 
+#define CHECK(reader) if (!reader->isOK()) { delete reader; reader = 0; }
+
     if (WavFileReader::supports(source)) {
 
         reader = new WavFileReader(source);
@@ -114,167 +140,101 @@
         if (reader->isOK() &&
             (!reader->isQuicklySeekable() ||
              normalised ||
+             (cacheMode == CodedAudioFileReader::CacheInMemory) ||
              (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;
+            cerr << "AudioFileReaderFactory::createReader: WAV file rate: " << reader->getSampleRate() << ", normalised " << normalised << ", seekable " << reader->isQuicklySeekable() << ", in memory " << (cacheMode == CodedAudioFileReader::CacheInMemory) << ", creating decoding reader" << endl;
 #endif
             
             delete reader;
             reader = new DecodingWavFileReader
                 (source,
-                 threading ?
-                 DecodingWavFileReader::ResampleThreaded :
-                 DecodingWavFileReader::ResampleAtOnce,
-                 DecodingWavFileReader::CacheInTemporaryFile,
+                 decodeMode, cacheMode,
                  targetRate ? targetRate : fileRate,
                  normalised,
                  reporter);
-            if (!reader->isOK()) {
-                delete reader;
-                reader = 0;
-            }
+            CHECK(reader);
         }
     }
     
 #ifdef HAVE_OGGZ
 #ifdef HAVE_FISHSOUND
-    if (!reader) {
-        if (OggVorbisFileReader::supports(source)) {
-            reader = new OggVorbisFileReader
-                (source,
-                 threading ?
-                 OggVorbisFileReader::DecodeThreaded :
-                 OggVorbisFileReader::DecodeAtOnce,
-                 OggVorbisFileReader::CacheInTemporaryFile,
-                 targetRate,
-                 normalised,
-                 reporter);
-            if (!reader->isOK()) {
-                delete reader;
-                reader = 0;
-            }
-        }
+    if (!reader && OggVorbisFileReader::supports(source)) {
+        reader = new OggVorbisFileReader
+            (source, decodeMode, cacheMode, targetRate, normalised, reporter);
+        CHECK(reader);
     }
 #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()) {
-                delete reader;
-                reader = 0;
-            }
-        }
+    if (!reader && MP3FileReader::supports(source)) {
+        reader = new MP3FileReader
+            (source, decodeMode, cacheMode, targetRate, normalised, reporter);
+        CHECK(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()) {
-                delete reader;
-                reader = 0;
-            }
-        }
+    if (!reader && QuickTimeFileReader::supports(source)) {
+        reader = new QuickTimeFileReader
+            (source, decodeMode, cacheMode, targetRate, normalised, reporter);
+        CHECK(reader);
     }
 #endif
 
 #ifdef HAVE_COREAUDIO
-    if (!reader) {
-        if (CoreAudioFileReader::supports(source)) {
-            reader = new CoreAudioFileReader
-                (source,
-                 threading ?
-                 CoreAudioFileReader::DecodeThreaded :
-                 CoreAudioFileReader::DecodeAtOnce,
-                 CoreAudioFileReader::CacheInTemporaryFile,
-                 targetRate,
-                 normalised,
-                 reporter);
-            if (!reader->isOK()) {
-                delete reader;
-                reader = 0;
-            }
-        }
+    if (!reader && CoreAudioFileReader::supports(source)) {
+        reader = new CoreAudioFileReader
+            (source, decodeMode, cacheMode, targetRate, normalised, reporter);
+        CHECK(reader);
     }
 #endif
 
-
+    if (reader) {
+        // The happy case: a reader recognised the file extension &
+        // succeeded in opening the file
+        return reader;
+    }
+    
     // 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);
 
-        reader = new WavFileReader(source);
+    sv_samplerate_t fileRate = reader->getSampleRate();
 
-        sv_samplerate_t fileRate = reader->getSampleRate();
-
-        if (reader->isOK() &&
-            (!reader->isQuicklySeekable() ||
-             normalised ||
-             (targetRate != 0 && fileRate != targetRate))) {
+    if (reader->isOK() &&
+        (!reader->isQuicklySeekable() ||
+         normalised ||
+         (cacheMode == CodedAudioFileReader::CacheInMemory) ||
+         (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;
+        cerr << "AudioFileReaderFactory::createReader: WAV file rate: " << reader->getSampleRate() << ", normalised " << normalised << ", seekable " << reader->isQuicklySeekable() << ", in memory " << (cacheMode == CodedAudioFileReader::CacheInMemory) << ", creating decoding reader" << endl;
 #endif
 
-            delete reader;
-            reader = new DecodingWavFileReader
-                (source,
-                 threading ?
-                 DecodingWavFileReader::ResampleThreaded :
-                 DecodingWavFileReader::ResampleAtOnce,
-                 DecodingWavFileReader::CacheInTemporaryFile,
-                 targetRate ? targetRate : fileRate,
-                 normalised,
-                 reporter);
-        }
+        delete reader;
+        reader = new DecodingWavFileReader
+            (source,
+             decodeMode, cacheMode,
+             targetRate ? targetRate : fileRate,
+             normalised,
+             reporter);
+    }
 
-        if (!reader->isOK()) {
-            delete reader;
-            reader = 0;
-        }
-    }
+    CHECK(reader);
     
 #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;
-        }
+            (source, decodeMode, cacheMode, targetRate, normalised, reporter);
+        CHECK(reader);
     }
 #endif
 #endif
@@ -282,78 +242,35 @@
 #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;
-        }
+            (source, decodeMode, cacheMode, targetRate, normalised, reporter);
+        CHECK(reader);
     }
 #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;
-        }
+            (source, decodeMode, cacheMode, targetRate, normalised, reporter);
+        CHECK(reader);
     }
 #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;
-        }
+            (source, decodeMode, cacheMode, targetRate, normalised, reporter);
+        CHECK(reader);
     }
 #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;
+    if (!reader) {
+        cerr << "AudioFileReaderFactory::Failed to create a reader for "
+             << "url \"" << source.getLocation()
+             << "\" (content type \""
+             << source.getContentType() << "\")" << endl;
+        return nullptr;
     }
-
-    cerr << "AudioFileReaderFactory: No reader" << endl;
+    
     return reader;
 }
 
--- a/data/fileio/AudioFileReaderFactory.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/AudioFileReaderFactory.h	Mon Nov 21 16:54:37 2016 +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>
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/AudioFileSizeEstimator.cpp	Mon Nov 21 16:54:37 2016 +0000
@@ -0,0 +1,108 @@
+/* -*- 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>
+
+//#define DEBUG_AUDIO_FILE_SIZE_ESTIMATOR 1
+
+sv_frame_t
+AudioFileSizeEstimator::estimate(FileSource source,
+				 sv_samplerate_t targetRate)
+{
+    sv_frame_t estimate = 0;
+    
+    // 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;
+	estimate = samples;
+    }
+
+    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)) {
+#ifdef DEBUG_AUDIO_FILE_SIZE_ESTIMATOR
+		cerr << "opened file, size is "  << f.size() << endl;
+#endif
+		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);
+	}
+
+#ifdef DEBUG_AUDIO_FILE_SIZE_ESTIMATOR
+	cerr << "AudioFileSizeEstimator: for extension " << extension << ", estimate = " << estimate << endl;
+#endif
+    }
+
+#ifdef DEBUG_AUDIO_FILE_SIZE_ESTIMATOR
+    cerr << "estimate = " << estimate << endl;
+#endif
+    
+    return estimate;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/AudioFileSizeEstimator.h	Mon Nov 21 16:54:37 2016 +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/CodedAudioFileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/CodedAudioFileReader.cpp	Mon Nov 21 16:54:37 2016 +0000
@@ -21,12 +21,15 @@
 #include "base/Profiler.h"
 #include "base/Serialiser.h"
 #include "base/Resampler.h"
+#include "base/StorageAdviser.h"
 
 #include <stdint.h>
 #include <iostream>
 #include <QDir>
 #include <QMutexLocker>
 
+using namespace std;
+
 CodedAudioFileReader::CodedAudioFileReader(CacheMode cacheMode,
                                            sv_samplerate_t targetRate,
                                            bool normalised) :
@@ -57,7 +60,7 @@
     QMutexLocker locker(&m_cacheMutex);
 
     endSerialised();
-
+    
     if (m_cacheFileWritePtr) sf_close(m_cacheFileWritePtr);
 
     SVDEBUG << "CodedAudioFileReader::~CodedAudioFileReader: deleting cache file reader" << endl;
@@ -73,6 +76,12 @@
 
     delete m_resampler;
     delete[] m_resampleBuffer;
+
+    if (!m_data.empty()) {
+        StorageAdviser::notifyDoneAllocation
+            (StorageAdviser::MemoryAllocation,
+             (m_data.size() * sizeof(float)) / 1024);
+    }
 }
 
 void
@@ -258,7 +267,7 @@
 }
 
 void
-CodedAudioFileReader::addSamplesToDecodeCache(const SampleBlock &samples)
+CodedAudioFileReader::addSamplesToDecodeCache(const vector<float> &samples)
 {
     QMutexLocker locker(&m_cacheMutex);
 
@@ -308,9 +317,16 @@
     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);
     }
 }
 
@@ -367,10 +383,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;
     }
@@ -424,7 +438,7 @@
     }
 }
 
-SampleBlock
+vector<float>
 CodedAudioFileReader::getInterleavedFrames(sv_frame_t start, sv_frame_t count) const
 {
     // Lock is only required in CacheInMemory mode (the cache file
@@ -433,10 +447,10 @@
 
     if (!m_initialised) {
         SVDEBUG << "CodedAudioFileReader::getInterleavedFrames: not initialised" << endl;
-        return SampleBlock();
+        return {};
     }
 
-    SampleBlock frames;
+    vector<float> frames;
     
     switch (m_cacheMode) {
 
@@ -448,22 +462,22 @@
 
     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 (ix1 > n) ix1 = n;
+        frames = vector<float>(m_data.begin() + ix0, m_data.begin() + ix1);
         m_dataLock.unlock();
-
-        frames.resize(size_t(i));
     }
     }
 
--- a/data/fileio/CodedAudioFileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/CodedAudioFileReader.h	Mon Nov 21 16:54:37 2016 +0000
@@ -38,7 +38,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 std::vector<float> getInterleavedFrames(sv_frame_t start, sv_frame_t count) const;
 
     virtual sv_samplerate_t getNativeRate() const { return m_fileRate; }
 
@@ -60,7 +65,7 @@
     // 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 std::vector<float> &interleaved);
 
     // may throw InsufficientDiscSpace:
     void finishDecodeCache();
@@ -78,8 +83,8 @@
 protected:
     QMutex m_cacheMutex;
     CacheMode m_cacheMode;
-    SampleBlock m_data;
-    mutable QReadWriteLock m_dataLock;
+    std::vector<float> m_data;
+    mutable QMutex m_dataLock;
     bool m_initialised;
     Serialiser *m_serialiser;
     sv_samplerate_t m_fileRate;
--- a/data/fileio/CoreAudioFileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/CoreAudioFileReader.h	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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,
@@ -56,7 +58,7 @@
 
     initialiseDecodeCache();
 
-    if (resampleMode == ResampleAtOnce) {
+    if (decodeMode == DecodeAtOnce) {
 
         if (m_reporter) {
             connect(m_reporter, SIGNAL(cancelled()), this, SLOT(cancelled()));
@@ -67,7 +69,7 @@
         sv_frame_t blockSize = 16384;
         sv_frame_t total = m_original->getFrameCount();
 
-        SampleBlock block;
+        vector<float> block;
 
         for (sv_frame_t i = 0; i < total; i += blockSize) {
 
@@ -124,7 +126,7 @@
     sv_frame_t blockSize = 16384;
     sv_frame_t total = m_reader->m_original->getFrameCount();
     
-    SampleBlock block;
+    vector<float> block;
     
     for (sv_frame_t i = 0; i < total; i += blockSize) {
         
@@ -147,7 +149,7 @@
 } 
 
 void
-DecodingWavFileReader::addBlock(const SampleBlock &frames)
+DecodingWavFileReader::addBlock(const vector<float> &frames)
 {
     addSamplesToDecodeCache(frames);
 
@@ -167,7 +169,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	Mon Nov 21 16:54:37 2016 +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 std::vector<float> &frames);
     
     class DecodeThread : public Thread
     {
--- a/data/fileio/FileReadThread.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/FileReadThread.cpp	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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/MP3FileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/MP3FileReader.cpp	Mon Nov 21 16:54:37 2016 +0000
@@ -27,7 +27,6 @@
 #include <iostream>
 
 #include <cstdlib>
-#include <unistd.h>
 
 #ifdef HAVE_ID3TAG
 #include <id3tag.h>
@@ -37,6 +36,11 @@
 
 #include <QFileInfo>
 
+#ifdef _MSC_VER
+#include <io.h>
+#define open _open
+#endif
+
 MP3FileReader::MP3FileReader(FileSource source, DecodeMode decodeMode, 
                              CacheMode mode, sv_samplerate_t targetRate,
                              bool normalised,
--- a/data/fileio/MP3FileReader.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/MP3FileReader.h	Mon Nov 21 16:54:37 2016 +0000
@@ -32,11 +32,6 @@
     Q_OBJECT
 
 public:
-    enum DecodeMode {
-        DecodeAtOnce, // decode the file on construction, with progress
-        DecodeThreaded // decode in a background thread after construction
-    };
-
     MP3FileReader(FileSource source,
                   DecodeMode decodeMode,
                   CacheMode cacheMode,
--- 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.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/OggVorbisFileReader.h	Mon Nov 21 16:54:37 2016 +0000
@@ -34,17 +34,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; }
--- a/data/fileio/WavFileReader.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/WavFileReader.cpp	Mon Nov 21 16:54:37 2016 +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),
@@ -125,53 +130,70 @@
     m_updating = false;
 }
 
-SampleBlock
+vector<float>
 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;
     }
 
-    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 {};
+    }
+
+    vector<float> 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 +224,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	Mon Nov 21 16:54:37 2016 +0000
@@ -50,7 +50,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 std::vector<float> getInterleavedFrames(sv_frame_t start, sv_frame_t count) const;
     
     static void getSupportedExtensions(std::set<QString> &extensions);
     static bool supportsExtension(QString ext);
@@ -75,8 +75,7 @@
     bool m_seekable;
 
     mutable QMutex m_mutex;
-    mutable SampleBlock m_buffer;
-    mutable sv_frame_t m_bufsiz;
+    mutable std::vector<float> 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	Mon Nov 21 16:54:37 2016 +0000
@@ -25,6 +25,8 @@
 #include <iostream>
 #include <cmath>
 
+using namespace std;
+
 WavFileWriter::WavFileWriter(QString path,
 			     sv_samplerate_t sampleRate,
                              int channels,
@@ -129,8 +131,6 @@
     }
 
     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();
@@ -140,16 +140,17 @@
 
 	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);
+            vector<float> 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];
+                vector<float> 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")
@@ -159,8 +160,6 @@
 	}
     }
 
-    delete[] ub;
-    delete[] ib;
     if (ownSelection) delete selection;
 
     return isOK();
--- a/data/fileio/test/AudioFileReaderTest.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/fileio/test/AudioFileReaderTest.h	Mon Nov 21 16:54:37 2016 +0000
@@ -31,7 +31,7 @@
 
 using namespace std;
 
-static QString audioDir = "testfiles";
+static QString audioDir = "svcore/data/fileio/test/testfiles";
 
 class AudioFileReaderTest : public QObject
 {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/files.pri	Mon Nov 21 16:54:37 2016 +0000
@@ -0,0 +1,7 @@
+
+TEST_HEADERS += \
+	     AudioFileReaderTest.h \
+	     AudioTestData.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;
-    }
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/svcore-data-fileio-test.cpp	Mon Nov 21 16:54:37 2016 +0000
@@ -0,0 +1,43 @@
+/* -*- 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;
+    }
+}
+
--- 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
-
--- a/data/midi/MIDIInput.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/midi/MIDIInput.cpp	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -19,6 +19,8 @@
 
 #include <QTextStream>
 
+using namespace std;
+
 PowerOfSqrtTwoZoomConstraint
 AggregateWaveModel::m_zoomConstraint;
 
@@ -92,65 +94,56 @@
     return m_components.begin()->model->getSampleRate();
 }
 
-sv_frame_t
-AggregateWaveModel::getData(int channel, sv_frame_t start, sv_frame_t count,
-                            float *buffer) const
+vector<float>
+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;
-        }
-    }
+    vector<float> result(count, 0.f);
 
     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;
+
+        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());
         }
-        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];
-            }
+        for (sv_frame_t i = 0; in_range_for(here, i); ++i) {
+            result[i] += here[i];
         }
     }
 
-    if (mixing) delete[] readbuf;
-    return longest;
+    result.resize(longest);
+    return result;
 }
 
-sv_frame_t
+vector<vector<float>>
 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<vector<float>> 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	Mon Nov 21 16:54:37 2016 +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 std::vector<float> 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<std::vector<float>> 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	Mon Nov 21 16:54:37 2016 +0000
@@ -17,13 +17,11 @@
 
 #include "base/Profiler.h"
 
-Dense3DModelPeakCache::Dense3DModelPeakCache(DenseThreeDimensionalModel *source,
+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,20 +41,6 @@
     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
 {
@@ -81,9 +65,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 +79,7 @@
 bool
 Dense3DModelPeakCache::haveColumn(int column) const
 {
-    return column < (int)m_coverage.size() && m_coverage.get(column);
+    return in_range_for(m_coverage, column) && m_coverage[column];
 }
 
 void
@@ -103,26 +87,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	Mon Nov 21 16:54:37 2016 +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,8 +73,6 @@
         return m_source->getMaximumLevel();
     }
 
-    virtual bool isColumnAvailable(int column) const;
-
     virtual Column getColumn(int column) const;
 
     virtual float getValueAt(int column, int n) const;
@@ -90,10 +96,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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -57,22 +57,22 @@
 
     /**
      * 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 std::vector<float> 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<std::vector<float>> 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 ""; }
--- a/data/model/EditableDenseThreeDimensionalModel.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/EditableDenseThreeDimensionalModel.cpp	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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,246 @@
     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 move(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 move(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;
+}
+
+vector<float>
+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);
+        vector<float> 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;
+    }
+}
+
+vector<float>
+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;
+
+        vector<float> acc(m_savedData.data.begin() + discard,
+                          m_savedData.data.end());
+
+        vector<float> 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;
+    }
+}
+
+vector<float>
+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);
+
+    // 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;
+}
+
+vector<complex<float>>
+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);
+
+    vector<complex<float>> 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 move(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 +352,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 +384,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 +393,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 +411,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 +423,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 +445,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 +466,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 +498,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 +512,7 @@
 
 FFTModel::PeakSet
 FFTModel::getPeakFrequencies(PeakPickType type, int x,
-                             int ymin, int ymax)
+                             int ymin, int ymax) const
 {
     Profiler profiler("FFTModel::getPeakFrequencies");
 
@@ -404,7 +521,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 +528,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 +539,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 +551,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	Mon Nov 21 16:54:37 2016 +0000
@@ -16,24 +16,30 @@
 #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 <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 +49,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 +129,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 +146,44 @@
     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 };
     }
+
+    std::vector<std::complex<float> > getFFTColumn(int column) const;
+    std::vector<float> getSourceSamples(int column) const;
+    std::vector<float> getSourceData(std::pair<sv_frame_t, sv_frame_t>) const;
+    std::vector<float> getSourceDataUncached(std::pair<sv_frame_t, sv_frame_t>) const;
+
+    struct SavedSourceData {
+        std::pair<sv_frame_t, sv_frame_t> range;
+        std::vector<float> data;
+    };
+    mutable SavedSourceData m_savedData;
+    
+    struct SavedColumn {
+        int n;
+        std::vector<std::complex<float> > col;
+    };
+    mutable std::deque<SavedColumn> m_cached;
+    size_t m_cacheSize;
 };
 
 #endif
--- a/data/model/Model.h	Mon Nov 21 16:32:58 2016 +0000
+++ b/data/model/Model.h	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/ReadOnlyWaveFileModel.cpp	Mon Nov 21 16:54:37 2016 +0000
@@ -0,0 +1,722 @@
+/* -*- 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()) {
+        bool normalise = Preferences::getInstance()->getNormaliseAudio();
+        m_reader = AudioFileReaderFactory::createThreadingReader
+            (m_source, targetRate, normalise);
+        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 "";
+}
+    
+vector<float>
+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;
+        }
+    }
+
+    vector<float> interleaved = m_reader->getInterleavedFrames(start, count);
+    if (channels == 1) return interleaved;
+
+    sv_frame_t obtained = interleaved.size() / channels;
+    
+    vector<float> 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 c = 0; c < channels; ++c) {
+            for (int i = 0; i < obtained; ++i) {
+                result[i] += interleaved[i * channels + c];
+            }
+        }
+    }
+
+    return result;
+}
+
+vector<vector<float>>
+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;
+        }
+    }
+
+    vector<float> interleaved = m_reader->getInterleavedFrames(start, count);
+    if (channels == 1) return { interleaved };
+
+    sv_frame_t obtained = interleaved.size() / channels;
+    vector<vector<float>> result(reqchannels, vector<float>(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;
+    vector<float> 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	Mon Nov 21 16:54:37 2016 +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 std::vector<float> getData(int channel, sv_frame_t start, sv_frame_t count) const;
+
+    virtual std::vector<std::vector<float>> 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 std::vector<float> 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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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;
@@ -149,17 +155,30 @@
 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()
+{
+    if (m_reader) m_reader->updateDone();
+    m_proportion = 100;
+    emit modelChanged();
 }
 
 sv_frame_t
@@ -169,21 +188,19 @@
     return m_frameCount;
 }
 
-sv_frame_t
-WritableWaveFileModel::getData(int channel, sv_frame_t start, sv_frame_t count,
-                               float *buffer) const
+vector<float>
+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<vector<float>>
 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 +232,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	Mon Nov 21 16:54:37 2016 +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,51 @@
 
     /**
      * 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.  Caller should also call setWriteProportion() to update
+     * the progress of this file, if it has a known end point, and
+     * should call writeComplete() when the file has been written.
      */
     virtual bool addSamples(float **samples, sv_frame_t count);
+
+    /**
+     * 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().
+     */
+    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 +87,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 +110,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 std::vector<float> 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<std::vector<float>> getMultiChannelData(int fromchannel, int tochannel, sv_frame_t start, sv_frame_t count) const;
 
     virtual int getSummaryBlockSize(int desired) const;
 
@@ -81,14 +128,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	Mon Nov 21 16:54:37 2016 +0000
@@ -26,40 +26,39 @@
     }
 }
 
-sv_frame_t
-MockWaveModel::getData(int channel, sv_frame_t start, sv_frame_t count,
-		       float *buffer) const
+vector<float>
+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 << "): ";
 
+    vector<float> 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<vector<float>>
 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<vector<float>> 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	Mon Nov 21 16:54:37 2016 +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 std::vector<float> getData(int channel, sv_frame_t start, sv_frame_t count) const;
+    virtual std::vector<std::vector<float>> 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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -0,0 +1,249 @@
+SVCORE_HEADERS = \
+           base/AudioLevel.h \
+           base/AudioPlaySource.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/Resampler.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/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/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/ResamplerSV.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/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/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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -0,0 +1,218 @@
+/* -*- 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("plugin-checker-helper");
+    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 type) 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 helper 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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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/rdf/RDFImporter.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/rdf/RDFImporter.cpp	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -56,12 +56,12 @@
 
 extern "C" {
 
-/* usleep is now in mingw
+#ifdef _MSC_VER
 void usleep(unsigned long usec)
 {
     ::Sleep(usec / 1000);
 }
-*/
+#endif
 
 int gettimeofday(struct timeval *tv, void *tz)
 {
@@ -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
@@ -277,7 +277,7 @@
 #ifdef _WIN32
 extern void SystemMemoryBarrier()
 {
-#ifdef __MSVC__
+#ifdef _MSC_VER
     MemoryBarrier();
 #else /* mingw */
     LONG Barrier = 0;
@@ -325,76 +325,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	Mon Nov 21 16:54:37 2016 +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/FeatureExtractionModelTransformer.cpp	Mon Nov 21 16:32:58 2016 +0000
+++ b/transform/FeatureExtractionModelTransformer.cpp	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +0000
@@ -89,22 +89,14 @@
     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();
@@ -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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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	Mon Nov 21 16:54:37 2016 +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 &);