changeset 148:1a42221a1522

* Reorganising code base. This revision will not compile.
author Chris Cannam
date Mon, 31 Jul 2006 11:49:58 +0000
parents 3a13b0d4934e
children 3e4c384f518e
files data/fft/FFTDataServer.cpp data/fft/FFTDataServer.h data/fft/FFTFileCache.cpp data/fft/FFTFileCache.h data/fft/FFTFuzzyAdapter.cpp data/fft/FFTFuzzyAdapter.h data/fileio/AudioFileReader.h data/fileio/AudioFileReaderFactory.cpp data/fileio/AudioFileReaderFactory.h data/fileio/BZipFileDevice.cpp data/fileio/BZipFileDevice.h data/fileio/CSVFileReader.cpp data/fileio/CSVFileReader.h data/fileio/CSVFileWriter.cpp data/fileio/CSVFileWriter.h data/fileio/CodedAudioFileReader.cpp data/fileio/CodedAudioFileReader.h data/fileio/ConfigFile.cpp data/fileio/ConfigFile.h data/fileio/DataFileReader.h data/fileio/DataFileReaderFactory.cpp data/fileio/DataFileReaderFactory.h data/fileio/FFTDataServer.cpp data/fileio/FFTDataServer.h data/fileio/FFTFileCache.cpp data/fileio/FFTFileCache.h data/fileio/FFTFuzzyAdapter.cpp data/fileio/FFTFuzzyAdapter.h data/fileio/FileReadThread.cpp data/fileio/FileReadThread.h data/fileio/MIDIFileReader.cpp data/fileio/MIDIFileReader.h data/fileio/MP3FileReader.cpp data/fileio/MP3FileReader.h data/fileio/MatrixFile.cpp data/fileio/MatrixFile.h data/fileio/OggVorbisFileReader.cpp data/fileio/OggVorbisFileReader.h data/fileio/RecentFiles.cpp data/fileio/RecentFiles.h data/fileio/SVFileReader.cpp data/fileio/SVFileReader.h data/fileio/WavFileReader.cpp data/fileio/WavFileReader.h data/fileio/WavFileWriter.cpp data/fileio/WavFileWriter.h
diffstat 46 files changed, 9609 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fft/FFTDataServer.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,751 @@
+/* -*- 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 "FFTDataServer.h"
+
+#include "FFTFileCache.h"
+
+#include "model/DenseTimeValueModel.h"
+
+#include "base/System.h"
+
+//#define DEBUG_FFT_SERVER 1
+//#define DEBUG_FFT_SERVER_FILL 1
+
+#ifdef DEBUG_FFT_SERVER_FILL
+#define DEBUG_FFT_SERVER
+#endif
+
+FFTDataServer::ServerMap FFTDataServer::m_servers;
+QMutex FFTDataServer::m_serverMapMutex;
+
+FFTDataServer *
+FFTDataServer::getInstance(const DenseTimeValueModel *model,
+                           int channel,
+                           WindowType windowType,
+                           size_t windowSize,
+                           size_t windowIncrement,
+                           size_t fftSize,
+                           bool polar,
+                           size_t fillFromColumn)
+{
+    QString n = generateFileBasename(model,
+                                     channel,
+                                     windowType,
+                                     windowSize,
+                                     windowIncrement,
+                                     fftSize,
+                                     polar);
+
+    FFTDataServer *server = 0;
+    
+    QMutexLocker locker(&m_serverMapMutex);
+
+    if ((server = findServer(n))) {
+        return server;
+    }
+
+    QString npn = generateFileBasename(model,
+                                       channel,
+                                       windowType,
+                                       windowSize,
+                                       windowIncrement,
+                                       fftSize,
+                                       !polar);
+
+    if ((server = findServer(npn))) {
+        return server;
+    }
+
+    m_servers[n] = ServerCountPair
+        (new FFTDataServer(n,
+                           model,
+                           channel,
+                           windowType,
+                           windowSize,
+                           windowIncrement,
+                           fftSize,
+                           polar,
+                           fillFromColumn),
+         1);
+
+    return m_servers[n].first;
+}
+
+FFTDataServer *
+FFTDataServer::getFuzzyInstance(const DenseTimeValueModel *model,
+                                int channel,
+                                WindowType windowType,
+                                size_t windowSize,
+                                size_t windowIncrement,
+                                size_t fftSize,
+                                bool polar,
+                                size_t fillFromColumn)
+{
+    // 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 FFTFuzzyAdapter 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 fuzzy adapter doesn't support this).
+
+    {
+        QMutexLocker locker(&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 << "Distance " << distance << ", best is " << bestdist << std::endl;
+#endif
+                
+                if (bestdist == -1 || distance < bestdist) {
+                    bestdist = distance;
+                    best = i;
+                }
+            }
+        }
+
+        if (bestdist >= 0) {
+            ++best->second.second;
+            return best->second.first;
+        }
+    }
+
+    // Nothing found, make a new one
+
+    return getInstance(model,
+                       channel,
+                       windowType,
+                       windowSize,
+                       windowIncrement,
+                       fftSize,
+                       polar,
+                       fillFromColumn);
+}
+
+FFTDataServer *
+FFTDataServer::findServer(QString n)
+{    
+    if (m_servers.find(n) != m_servers.end()) {
+        ++m_servers[n].second;
+        return m_servers[n].first;
+    }
+
+    return 0;
+}
+
+void
+FFTDataServer::releaseInstance(FFTDataServer *server)
+{
+#ifdef DEBUG_FFT_SERVER
+    std::cerr << "FFTDataServer::releaseInstance(" << server << ")" << std::endl;
+#endif
+
+    QMutexLocker locker(&m_serverMapMutex);
+
+    //!!! not a good strategy.  Want something like:
+
+    // -- 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
+
+    // also:
+    //
+
+    for (ServerMap::iterator i = m_servers.begin(); i != m_servers.end(); ++i) {
+        if (i->second.first == server) {
+            if (i->second.second == 0) {
+                std::cerr << "ERROR: FFTDataServer::releaseInstance("
+                          << server << "): instance not allocated" << std::endl;
+            } else if (--i->second.second == 0) {
+                if (server->m_lastUsedCache == -1) { // never used
+                    delete server;
+                    m_servers.erase(i);
+                } else {
+                    server->suspend();
+                    purgeLimbo();
+                }
+            }
+            return;
+        }
+    }
+
+    std::cerr << "ERROR: FFTDataServer::releaseInstance(" << server << "): "
+              << "instance not found" << std::endl;
+}
+
+void
+FFTDataServer::purgeLimbo(int maxSize)
+{
+    ServerMap::iterator i = m_servers.end();
+
+    int count = 0;
+
+    while (i != m_servers.begin()) {
+        --i;
+        if (i->second.second == 0) {
+            if (++count > maxSize) {
+                delete i->second.first;
+                m_servers.erase(i);
+                return;
+            }
+        }
+    }
+}
+
+FFTDataServer::FFTDataServer(QString fileBaseName,
+                             const DenseTimeValueModel *model,
+                             int channel,
+			     WindowType windowType,
+			     size_t windowSize,
+			     size_t windowIncrement,
+			     size_t fftSize,
+                             bool polar,
+                             size_t fillFromColumn) :
+    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_lastUsedCache(-1),
+    m_fftInput(0),
+    m_exiting(false),
+    m_fillThread(0)
+{
+    size_t start = m_model->getStartFrame();
+    size_t end = m_model->getEndFrame();
+
+    m_width = (end - start) / m_windowIncrement + 1;
+    m_height = m_fftSize / 2;
+
+    size_t maxCacheSize = 20 * 1024 * 1024;
+    size_t columnSize = m_height * sizeof(fftsample) * 2 + sizeof(fftsample);
+    if (m_width * columnSize < maxCacheSize * 2) m_cacheWidth = m_width;
+    else m_cacheWidth = maxCacheSize / columnSize;
+    
+    int bits = 0;
+    while (m_cacheWidth) { m_cacheWidth >>= 1; ++bits; }
+    m_cacheWidth = 2;
+    while (bits) { m_cacheWidth <<= 1; --bits; }
+    
+#ifdef DEBUG_FFT_SERVER
+    std::cerr << "Width " << m_width << ", cache width " << m_cacheWidth << " (size " << m_cacheWidth * columnSize << ")" << std::endl;
+#endif
+
+    for (size_t i = 0; i <= m_width / m_cacheWidth; ++i) {
+        m_caches.push_back(0);
+    }
+
+    m_fftInput = (fftsample *)
+        fftwf_malloc(fftSize * sizeof(fftsample));
+
+    m_fftOutput = (fftwf_complex *)
+        fftwf_malloc(fftSize * sizeof(fftwf_complex));
+
+    m_workbuffer = (float *)
+        fftwf_malloc(fftSize * sizeof(float));
+
+    m_fftPlan = fftwf_plan_dft_r2c_1d(m_fftSize,
+                                      m_fftInput,
+                                      m_fftOutput,
+                                      FFTW_ESTIMATE);
+
+    if (!m_fftPlan) {
+        std::cerr << "ERROR: fftwf_plan_dft_r2c_1d(" << m_windowSize << ") failed!" << std::endl;
+        throw(0);
+    }
+
+    m_fillThread = new FillThread(*this, fillFromColumn);
+
+    //!!! respond appropriately when thread exits (deleteProcessingData etc)
+}
+
+FFTDataServer::~FFTDataServer()
+{
+#ifdef DEBUG_FFT_SERVER
+    std::cerr << "FFTDataServer(" << this << ")::~FFTDataServer()" << std::endl;
+#endif
+
+    m_exiting = true;
+    m_condition.wakeAll();
+    if (m_fillThread) {
+        m_fillThread->wait();
+        delete m_fillThread;
+    }
+
+    QMutexLocker locker(&m_writeMutex);
+
+    for (CacheVector::iterator i = m_caches.begin(); i != m_caches.end(); ++i) {
+        delete *i;
+    }
+
+    deleteProcessingData();
+}
+
+void
+FFTDataServer::deleteProcessingData()
+{
+    if (m_fftInput) {
+        fftwf_destroy_plan(m_fftPlan);
+        fftwf_free(m_fftInput);
+        fftwf_free(m_fftOutput);
+        fftwf_free(m_workbuffer);
+    }
+    m_fftInput = 0;
+}
+
+void
+FFTDataServer::suspend()
+{
+#ifdef DEBUG_FFT_SERVER
+    std::cerr << "FFTDataServer(" << this << "): suspend" << std::endl;
+#endif
+    QMutexLocker locker(&m_writeMutex);
+    m_suspended = true;
+    for (CacheVector::iterator i = m_caches.begin(); i != m_caches.end(); ++i) {
+        if (*i) (*i)->suspend();
+    }
+}
+
+void
+FFTDataServer::resume()
+{
+    m_suspended = false;
+    m_condition.wakeAll();
+}
+
+FFTCache *
+FFTDataServer::getCacheAux(size_t c)
+{
+    QMutexLocker locker(&m_writeMutex);
+
+    if (m_lastUsedCache == -1) {
+        m_fillThread->start();
+    }
+
+    if (int(c) != m_lastUsedCache) {
+
+//        std::cerr << "switch from " << m_lastUsedCache << " to " << c << std::endl;
+
+        for (IntQueue::iterator i = m_dormantCaches.begin();
+             i != m_dormantCaches.end(); ++i) {
+            if (*i == c) {
+                m_dormantCaches.erase(i);
+                break;
+            }
+        }
+
+        if (m_lastUsedCache >= 0) {
+            bool inDormant = false;
+            for (size_t i = 0; i < m_dormantCaches.size(); ++i) {
+                if (m_dormantCaches[i] == m_lastUsedCache) {
+                    inDormant = true;
+                    break;
+                }
+            }
+            if (!inDormant) {
+                m_dormantCaches.push_back(m_lastUsedCache);
+            }
+            while (m_dormantCaches.size() > 4) {
+                int dc = m_dormantCaches.front();
+                m_dormantCaches.pop_front();
+                m_caches[dc]->suspend();
+            }
+        }
+    }
+
+    if (m_caches[c]) {
+        m_lastUsedCache = c;
+        return m_caches[c];
+    }
+
+    QString name = QString("%1-%2").arg(m_fileBaseName).arg(c);
+
+    FFTCache *cache = new FFTFileCache(name, MatrixFile::ReadWrite,
+                                       m_polar ? FFTFileCache::Polar :
+                                                 FFTFileCache::Rectangular);
+
+    size_t width = m_cacheWidth;
+    if (c * m_cacheWidth + width > m_width) {
+        width = m_width - c * m_cacheWidth;
+    }
+
+    cache->resize(width, m_height);
+    cache->reset();
+
+    m_caches[c] = cache;
+    m_lastUsedCache = c;
+
+    return cache;
+}
+
+float
+FFTDataServer::getMagnitudeAt(size_t x, size_t y)
+{
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    if (!cache->haveSetColumnAt(col)) {
+        fillColumn(x);
+    }
+    return cache->getMagnitudeAt(col, y);
+}
+
+float
+FFTDataServer::getNormalizedMagnitudeAt(size_t x, size_t y)
+{
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    if (!cache->haveSetColumnAt(col)) {
+        fillColumn(x);
+    }
+    return cache->getNormalizedMagnitudeAt(col, y);
+}
+
+float
+FFTDataServer::getMaximumMagnitudeAt(size_t x)
+{
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    if (!cache->haveSetColumnAt(col)) {
+        fillColumn(x);
+    }
+    return cache->getMaximumMagnitudeAt(col);
+}
+
+float
+FFTDataServer::getPhaseAt(size_t x, size_t y)
+{
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    if (!cache->haveSetColumnAt(col)) {
+        fillColumn(x);
+    }
+    return cache->getPhaseAt(col, y);
+}
+
+void
+FFTDataServer::getValuesAt(size_t x, size_t y, float &real, float &imaginary)
+{
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    if (!cache->haveSetColumnAt(col)) {
+#ifdef DEBUG_FFT_SERVER
+        std::cerr << "FFTDataServer::getValuesAt(" << x << ", " << y << "): filling" << std::endl;
+#endif
+        fillColumn(x);
+    }        
+    float magnitude = cache->getMagnitudeAt(col, y);
+    float phase = cache->getPhaseAt(col, y);
+    real = magnitude * cosf(phase);
+    imaginary = magnitude * sinf(phase);
+}
+
+bool
+FFTDataServer::isColumnReady(size_t x)
+{
+    if (!haveCache(x)) {
+        if (m_lastUsedCache == -1) {
+            m_fillThread->start();
+        }
+        return false;
+    }
+
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    return cache->haveSetColumnAt(col);
+}    
+
+void
+FFTDataServer::fillColumn(size_t x)
+{
+    size_t col;
+#ifdef DEBUG_FFT_SERVER_FILL
+    std::cout << "FFTDataServer::fillColumn(" << x << ")" << std::endl;
+#endif
+    FFTCache *cache = getCache(x, col);
+
+    QMutexLocker locker(&m_writeMutex);
+
+    if (cache->haveSetColumnAt(col)) return;
+
+    int startFrame = m_windowIncrement * x;
+    int endFrame = startFrame + m_windowSize;
+
+    startFrame -= int(m_windowSize - m_windowIncrement) / 2;
+    endFrame   -= int(m_windowSize - m_windowIncrement) / 2;
+    size_t pfx = 0;
+
+    size_t off = (m_fftSize - m_windowSize) / 2;
+
+    for (size_t i = 0; i < off; ++i) {
+        m_fftInput[i] = 0.0;
+        m_fftInput[m_fftSize - i - 1] = 0.0;
+    }
+
+    if (startFrame < 0) {
+	pfx = size_t(-startFrame);
+	for (size_t i = 0; i < pfx; ++i) {
+	    m_fftInput[off + i] = 0.0;
+	}
+    }
+
+    size_t got = m_model->getValues(m_channel, startFrame + pfx,
+				    endFrame, m_fftInput + off + pfx);
+
+    while (got + pfx < m_windowSize) {
+	m_fftInput[off + got + pfx] = 0.0;
+	++got;
+    }
+
+    if (m_channel == -1) {
+	int channels = m_model->getChannelCount();
+	if (channels > 1) {
+	    for (size_t i = 0; i < m_windowSize; ++i) {
+		m_fftInput[off + i] /= channels;
+	    }
+	}
+    }
+
+    m_windower.cut(m_fftInput + off);
+
+    for (size_t i = 0; i < m_fftSize/2; ++i) {
+	fftsample temp = m_fftInput[i];
+	m_fftInput[i] = m_fftInput[i + m_fftSize/2];
+	m_fftInput[i + m_fftSize/2] = temp;
+    }
+
+    fftwf_execute(m_fftPlan);
+
+    fftsample factor = 0.0;
+
+    for (size_t i = 0; i < m_fftSize/2; ++i) {
+
+	fftsample mag = sqrtf(m_fftOutput[i][0] * m_fftOutput[i][0] +
+                              m_fftOutput[i][1] * m_fftOutput[i][1]);
+	mag /= m_windowSize / 2;
+
+	if (mag > factor) factor = mag;
+
+	fftsample phase = atan2f(m_fftOutput[i][1], m_fftOutput[i][0]);
+	phase = princargf(phase);
+
+        m_workbuffer[i] = mag;
+        m_workbuffer[i + m_fftSize/2] = phase;
+    }
+
+    cache->setColumnAt(col,
+                       m_workbuffer,
+                       m_workbuffer + m_fftSize/2,
+                       factor);
+}    
+
+size_t
+FFTDataServer::getFillCompletion() const 
+{
+    if (m_fillThread) return m_fillThread->getCompletion();
+    else return 100;
+}
+
+size_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,
+                                    size_t windowSize,
+                                    size_t windowIncrement,
+                                    size_t fftSize,
+                                    bool polar)
+{
+    char buffer[200];
+
+    sprintf(buffer, "%u-%u-%u-%u-%u-%u%s",
+            (unsigned int)XmlExportable::getObjectExportId(model),
+            (unsigned int)(channel + 1),
+            (unsigned int)windowType,
+            (unsigned int)windowSize,
+            (unsigned int)windowIncrement,
+            (unsigned int)fftSize,
+            polar ? "-p" : "-r");
+
+    return buffer;
+}
+
+void
+FFTDataServer::FillThread::run()
+{
+    m_extent = 0;
+    m_completion = 0;
+    
+    size_t start = m_server.m_model->getStartFrame();
+    size_t end = m_server.m_model->getEndFrame();
+    size_t remainingEnd = end;
+
+    int counter = 0;
+    int updateAt = (end / m_server.m_windowIncrement) / 20;
+    if (updateAt < 100) updateAt = 100;
+
+    if (m_fillFrom > start) {
+
+        for (size_t f = m_fillFrom; f < end; f += m_server.m_windowIncrement) {
+	    
+            m_server.fillColumn(int((f - start) / m_server.m_windowIncrement));
+
+            if (m_server.m_exiting) return;
+
+            while (m_server.m_suspended) {
+#ifdef DEBUG_FFT_SERVER
+                std::cerr << "FFTDataServer(" << this << "): suspended, waiting..." << std::endl;
+#endif
+                m_server.m_writeMutex.lock();
+                m_server.m_condition.wait(&m_server.m_writeMutex, 10000);
+                m_server.m_writeMutex.unlock();
+                if (m_server.m_exiting) return;
+            }
+
+            if (++counter == updateAt) {
+                m_extent = f;
+                m_completion = size_t(100 * fabsf(float(f - m_fillFrom) /
+                                                  float(end - start)));
+                counter = 0;
+            }
+        }
+
+        remainingEnd = m_fillFrom;
+        if (remainingEnd > start) --remainingEnd;
+        else remainingEnd = start;
+    }
+
+    size_t baseCompletion = m_completion;
+
+    for (size_t f = start; f < remainingEnd; f += m_server.m_windowIncrement) {
+
+        m_server.fillColumn(int((f - start) / m_server.m_windowIncrement));
+
+        if (m_server.m_exiting) return;
+
+        while (m_server.m_suspended) {
+#ifdef DEBUG_FFT_SERVER
+            std::cerr << "FFTDataServer(" << this << "): suspended, waiting..." << std::endl;
+#endif
+            m_server.m_writeMutex.lock();
+            m_server.m_condition.wait(&m_server.m_writeMutex, 10000);
+            m_server.m_writeMutex.unlock();
+            if (m_server.m_exiting) return;
+        }
+		    
+        if (++counter == updateAt) {
+            m_extent = f;
+            m_completion = baseCompletion +
+                size_t(100 * fabsf(float(f - start) /
+                                   float(end - start)));
+            counter = 0;
+        }
+    }
+
+    m_completion = 100;
+    m_extent = end;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fft/FFTDataServer.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,201 @@
+/* -*- 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_DATA_SERVER_H_
+#define _FFT_DATA_SERVER_H_
+
+#include "base/Window.h"
+#include "base/Thread.h"
+
+#include <fftw3.h>
+
+#include <QMutex>
+#include <QWaitCondition>
+#include <QString>
+
+#include <vector>
+#include <deque>
+
+class DenseTimeValueModel;
+class FFTCache;
+
+class FFTDataServer
+{
+public:
+    static FFTDataServer *getInstance(const DenseTimeValueModel *model,
+                                      int channel,
+                                      WindowType windowType,
+                                      size_t windowSize,
+                                      size_t windowIncrement,
+                                      size_t fftSize,
+                                      bool polar,
+                                      size_t fillFromColumn = 0);
+
+    static FFTDataServer *getFuzzyInstance(const DenseTimeValueModel *model,
+                                           int channel,
+                                           WindowType windowType,
+                                           size_t windowSize,
+                                           size_t windowIncrement,
+                                           size_t fftSize,
+                                           bool polar,
+                                           size_t fillFromColumn = 0);
+
+    static void releaseInstance(FFTDataServer *);
+
+    const DenseTimeValueModel *getModel() const { return m_model; }
+    int        getChannel() const { return m_channel; }
+    WindowType getWindowType() const { return m_windower.getType(); }
+    size_t     getWindowSize() const { return m_windowSize; }
+    size_t     getWindowIncrement() const { return m_windowIncrement; }
+    size_t     getFFTSize() const { return m_fftSize; }
+    bool       getPolar() const { return m_polar; }
+
+    size_t     getWidth() const  { return m_width;  }
+    size_t     getHeight() const { return m_height; }
+
+    float      getMagnitudeAt(size_t x, size_t y);
+    float      getNormalizedMagnitudeAt(size_t x, size_t y);
+    float      getMaximumMagnitudeAt(size_t x);
+    float      getPhaseAt(size_t x, size_t y);
+    void       getValuesAt(size_t x, size_t y, float &real, float &imaginary);
+    bool       isColumnReady(size_t x);
+
+    void       suspend();
+
+    // Convenience functions:
+
+    bool isLocalPeak(size_t x, size_t 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(size_t x, size_t y, float threshold) {
+        return getMagnitudeAt(x, y) > threshold;
+    }
+
+    size_t getFillCompletion() const;
+    size_t getFillExtent() const;
+
+private:
+    FFTDataServer(QString fileBaseName,
+                  const DenseTimeValueModel *model,
+                  int channel,
+                  WindowType windowType,
+                  size_t windowSize,
+                  size_t windowIncrement,
+                  size_t fftSize,
+                  bool polar,
+                  size_t fillFromColumn = 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;
+
+    size_t m_windowSize;
+    size_t m_windowIncrement;
+    size_t m_fftSize;
+    bool m_polar;
+
+    size_t m_width;
+    size_t m_height;
+    size_t m_cacheWidth;
+
+    typedef std::vector<FFTCache *> CacheVector;
+    CacheVector m_caches;
+    
+    typedef std::deque<int> IntQueue;
+    IntQueue m_dormantCaches;
+
+    int m_lastUsedCache;
+    FFTCache *getCache(size_t x, size_t &col) {
+        if (m_suspended) resume();
+        col   = x % m_cacheWidth;
+        int c = x / m_cacheWidth;
+        // The only use of m_lastUsedCache without a lock is to
+        // establish whether a cache has been created at all (they're
+        // created on demand, but not destroyed until the server is).
+        if (c == m_lastUsedCache) return m_caches[c];
+        else return getCacheAux(c);
+    }
+    bool haveCache(size_t x) {
+        int c = x / m_cacheWidth;
+        if (c == m_lastUsedCache) return true;
+        else return (m_caches[c] != 0);
+    }
+        
+    FFTCache *getCacheAux(size_t c);
+    QMutex m_writeMutex;
+    QWaitCondition m_condition;
+
+    fftsample *m_fftInput;
+    fftwf_complex *m_fftOutput;
+    float *m_workbuffer;
+    fftwf_plan m_fftPlan;
+
+    class FillThread : public Thread
+    {
+    public:
+        FillThread(FFTDataServer &server, size_t fillFromColumn) :
+            m_server(server), m_extent(0), m_completion(0),
+            m_fillFrom(fillFromColumn) { }
+
+        size_t getExtent() const { return m_extent; }
+        size_t getCompletion() const { return m_completion ? m_completion : 1; }
+        virtual void run();
+
+    protected:
+        FFTDataServer &m_server;
+        size_t m_extent;
+        size_t m_completion;
+        size_t m_fillFrom;
+    };
+
+    bool m_exiting;
+    bool m_suspended;
+    FillThread *m_fillThread;
+
+    void deleteProcessingData();
+    void fillColumn(size_t x);
+    void resume();
+
+    QString generateFileBasename() const;
+    static QString generateFileBasename(const DenseTimeValueModel *model,
+                                        int channel,
+                                        WindowType windowType,
+                                        size_t windowSize,
+                                        size_t windowIncrement,
+                                        size_t fftSize,
+                                        bool polar);
+
+    typedef std::pair<FFTDataServer *, int> ServerCountPair;
+    typedef std::map<QString, ServerCountPair> ServerMap;
+
+    static ServerMap m_servers;
+    static QMutex m_serverMapMutex;
+    static FFTDataServer *findServer(QString); // call with serverMapMutex held
+    static void purgeLimbo(int maxSize = 3); // call with serverMapMutex held
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fft/FFTFileCache.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,288 @@
+/* -*- 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 "FFTFileCache.h"
+
+#include "MatrixFile.h"
+
+#include "base/Profiler.h"
+
+#include <iostream>
+
+#include <QMutexLocker>
+
+// 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].
+
+FFTFileCache::FFTFileCache(QString fileBase, MatrixFile::Mode mode,
+                           StorageType storageType) :
+    m_writebuf(0),
+    m_readbuf(0),
+    m_readbufCol(0),
+    m_readbufWidth(0),
+    m_mfc(new MatrixFile
+          (fileBase, mode, 
+           storageType == Compact ? sizeof(uint16_t) : sizeof(float),
+           mode == MatrixFile::ReadOnly)),
+    m_storageType(storageType)
+{
+    std::cerr << "FFTFileCache: storage type is " << (storageType == Compact ? "Compact" : storageType == Polar ? "Polar" : "Rectangular") << std::endl;
+}
+
+FFTFileCache::~FFTFileCache()
+{
+    if (m_readbuf) delete[] m_readbuf;
+    if (m_writebuf) delete[] m_writebuf;
+    delete m_mfc;
+}
+
+size_t
+FFTFileCache::getWidth() const
+{
+    return m_mfc->getWidth();
+}
+
+size_t
+FFTFileCache::getHeight() const
+{
+    size_t mh = m_mfc->getHeight();
+    if (mh > 0) return (mh - 1) / 2;
+    else return 0;
+}
+
+void
+FFTFileCache::resize(size_t width, size_t height)
+{
+    QMutexLocker locker(&m_writeMutex);
+
+    m_mfc->resize(width, height * 2 + 1);
+    if (m_readbuf) {
+        delete[] m_readbuf;
+        m_readbuf = 0;
+    }
+    if (m_writebuf) {
+        delete[] m_writebuf;
+    }
+    m_writebuf = new char[(height * 2 + 1) * m_mfc->getCellSize()];
+}
+
+void
+FFTFileCache::reset()
+{
+    m_mfc->reset();
+}
+
+float
+FFTFileCache::getMagnitudeAt(size_t x, size_t y) const
+{
+    float value = 0.f;
+
+    switch (m_storageType) {
+
+    case Compact:
+        value = (getFromReadBufCompactUnsigned(x, y * 2) / 65535.0)
+            * getNormalizationFactor(x);
+        break;
+
+    case Rectangular:
+    {
+        float real, imag;
+        getValuesAt(x, y, real, imag);
+        value = sqrtf(real * real + imag * imag);
+        break;
+    }
+
+    case Polar:
+        value = getFromReadBufStandard(x, y * 2);
+        break;
+    }
+
+    return value;
+}
+
+float
+FFTFileCache::getNormalizedMagnitudeAt(size_t x, size_t y) const
+{
+    float value = 0.f;
+
+    switch (m_storageType) {
+
+    case Compact:
+        value = getFromReadBufCompactUnsigned(x, y * 2) / 65535.0;
+        break;
+
+    default:
+    {
+        float mag = getMagnitudeAt(x, y);
+        float factor = getNormalizationFactor(x);
+        if (factor != 0) value = mag / factor;
+        else value = 0.f;
+        break;
+    }
+    }
+
+    return value;
+}
+
+float
+FFTFileCache::getMaximumMagnitudeAt(size_t x) const
+{
+    return getNormalizationFactor(x);
+}
+
+float
+FFTFileCache::getPhaseAt(size_t x, size_t y) const
+{
+    float value = 0.f;
+    
+    switch (m_storageType) {
+
+    case Compact:
+        value = (getFromReadBufCompactSigned(x, y * 2 + 1) / 32767.0) * M_PI;
+        break;
+
+    case Rectangular:
+    {
+        float real, imag;
+        getValuesAt(x, y, real, imag);
+        value = princargf(atan2f(imag, real));
+        break;
+    }
+
+    case Polar:
+        value = getFromReadBufStandard(x, y * 2 + 1);
+        break;
+    }
+
+    return value;
+}
+
+void
+FFTFileCache::getValuesAt(size_t x, size_t y, float &real, float &imag) const
+{
+    switch (m_storageType) {
+
+    case Rectangular:
+        real = getFromReadBufStandard(x, y * 2);
+        imag = getFromReadBufStandard(x, y * 2 + 1);
+        return;
+
+    default:
+        float mag = getMagnitudeAt(x, y);
+        float phase = getPhaseAt(x, y);
+        real = mag * cosf(phase);
+        imag = mag * sinf(phase);
+        return;
+    }
+}
+
+bool
+FFTFileCache::haveSetColumnAt(size_t x) const
+{
+    return m_mfc->haveSetColumnAt(x);
+}
+
+void
+FFTFileCache::setColumnAt(size_t x, float *mags, float *phases, float factor)
+{
+    QMutexLocker locker(&m_writeMutex);
+
+    size_t h = getHeight();
+
+    switch (m_storageType) {
+
+    case Compact:
+        for (size_t 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 Rectangular:
+        for (size_t 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 Polar:
+        for (size_t 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;
+//    std::cerr << "Normalization factor: " << factor << ", max " << maxFactor << " (height " << getHeight() << ")" << std::endl;
+
+    if (m_storageType == Compact) {
+        ((uint16_t *)m_writebuf)[h * 2] = factor * 65535.0;
+    } else {
+        ((float *)m_writebuf)[h * 2] = factor;
+    }
+    m_mfc->setColumnAt(x, m_writebuf);
+}
+
+void
+FFTFileCache::setColumnAt(size_t x, float *real, float *imag)
+{
+    QMutexLocker locker(&m_writeMutex);
+
+    size_t h = getHeight();
+
+    float max = 0.0f;
+
+    switch (m_storageType) {
+
+    case Compact:
+        for (size_t y = 0; y < h; ++y) {
+            float mag = sqrtf(real[y] * real[y] + imag[y] * imag[y]);
+            if (mag > max) max = mag;
+        }
+        for (size_t y = 0; y < h; ++y) {
+            float mag = sqrtf(real[y] * real[y] + imag[y] * imag[y]);
+            float phase = princargf(atan2f(imag[y], real[y]));
+            ((uint16_t *)m_writebuf)[y * 2] = uint16_t((mag / max) * 65535.0);
+            ((uint16_t *)m_writebuf)[y * 2 + 1] = uint16_t(int16_t((phase * 32767) / M_PI));
+        }
+        break;
+
+    case Rectangular:
+        for (size_t 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 > max) max = mag;
+        }
+        break;
+
+    case Polar:
+        for (size_t y = 0; y < h; ++y) {
+            float mag = sqrtf(real[y] * real[y] + imag[y] * imag[y]);
+            if (mag > max) max = mag;
+            ((float *)m_writebuf)[y * 2] = mag;
+            ((float *)m_writebuf)[y * 2 + 1] = princargf(atan2f(imag[y], real[y]));
+        }
+        break;
+    }
+
+    ((float *)m_writebuf)[h * 2] = max;
+    m_mfc->setColumnAt(x, m_writebuf);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fft/FFTFileCache.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,125 @@
+/* -*- 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_FILE_CACHE_H_
+#define _FFT_FILE_CACHE_H_
+
+#include "FFTCache.h"
+#include "MatrixFile.h"
+
+#include <QMutex>
+
+class FFTFileCache : public FFTCache
+{
+public:
+    enum StorageType {
+        Compact, // 16 bits normalized polar
+        Rectangular, // floating point real+imag
+        Polar, // floating point mag+phase
+    };
+
+    FFTFileCache(QString fileBase, MatrixFile::Mode mode,
+                 StorageType storageType);
+    virtual ~FFTFileCache();
+
+    MatrixFile::Mode getMode() const { return m_mfc->getMode(); }
+
+    virtual size_t getWidth() const;
+    virtual size_t getHeight() const;
+	
+    virtual void resize(size_t width, size_t height);
+    virtual void reset(); // zero-fill or 1-fill as appropriate without changing size
+	
+    virtual float getMagnitudeAt(size_t x, size_t y) const;
+    virtual float getNormalizedMagnitudeAt(size_t x, size_t y) const;
+    virtual float getMaximumMagnitudeAt(size_t x) const;
+    virtual float getPhaseAt(size_t x, size_t y) const;
+
+    virtual void getValuesAt(size_t x, size_t y, float &real, float &imag) const;
+
+    virtual bool haveSetColumnAt(size_t x) const;
+
+    virtual void setColumnAt(size_t x, float *mags, float *phases, float factor);
+    virtual void setColumnAt(size_t x, float *reals, float *imags);
+
+    virtual void suspend() { m_mfc->suspend(); }
+
+protected:
+    char *m_writebuf;
+    mutable char *m_readbuf;
+    mutable size_t m_readbufCol;
+    mutable size_t m_readbufWidth;
+
+    float getFromReadBufStandard(size_t x, size_t y) const {
+        if (m_readbuf &&
+            (m_readbufCol == x || (m_readbufWidth > 1 && m_readbufCol+1 == x))) {
+            return ((float *)m_readbuf)[(x - m_readbufCol) * m_mfc->getHeight() + y];
+        } else {
+            populateReadBuf(x);
+            return getFromReadBufStandard(x, y);
+        }
+    }
+
+    float getFromReadBufCompactUnsigned(size_t x, size_t y) const {
+        if (m_readbuf &&
+            (m_readbufCol == x || (m_readbufWidth > 1 && m_readbufCol+1 == x))) {
+            return ((uint16_t *)m_readbuf)[(x - m_readbufCol) * m_mfc->getHeight() + y];
+        } else {
+            populateReadBuf(x);
+            return getFromReadBufCompactUnsigned(x, y);
+        }
+    }
+
+    float getFromReadBufCompactSigned(size_t x, size_t y) const {
+        if (m_readbuf &&
+            (m_readbufCol == x || (m_readbufWidth > 1 && m_readbufCol+1 == x))) {
+            return ((int16_t *)m_readbuf)[(x - m_readbufCol) * m_mfc->getHeight() + y];
+        } else {
+            populateReadBuf(x);
+            return getFromReadBufCompactSigned(x, y);
+        }
+    }
+
+    void populateReadBuf(size_t x) const {
+        if (!m_readbuf) {
+            m_readbuf = new char[m_mfc->getHeight() * 2 * m_mfc->getCellSize()];
+        }
+        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_readbufCol = x;
+    }
+
+    float getNormalizationFactor(size_t col) const {
+        if (m_storageType != Compact) {
+            return getFromReadBufStandard(col, m_mfc->getHeight() - 1);
+        } else {
+            float factor;
+            factor = getFromReadBufCompactUnsigned(col, m_mfc->getHeight() - 1);
+            return factor / 65535.0;
+        }
+    }
+
+    MatrixFile *m_mfc;
+    QMutex m_writeMutex;
+    StorageType m_storageType;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fft/FFTFuzzyAdapter.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,72 @@
+/* -*- 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 "FFTFuzzyAdapter.h"
+
+#include <cassert>
+
+FFTFuzzyAdapter::FFTFuzzyAdapter(const DenseTimeValueModel *model,
+				 int channel,
+				 WindowType windowType,
+				 size_t windowSize,
+				 size_t windowIncrement,
+				 size_t fftSize,
+				 bool polar,
+				 size_t fillFromColumn) :
+    m_server(0),
+    m_xshift(0),
+    m_yshift(0)
+{
+    m_server = FFTDataServer::getFuzzyInstance(model,
+                                               channel,
+                                               windowType,
+                                               windowSize,
+                                               windowIncrement,
+                                               fftSize,
+                                               polar,
+                                               fillFromColumn);
+
+    size_t xratio = windowIncrement / m_server->getWindowIncrement();
+    size_t yratio = m_server->getFFTSize() / fftSize;
+
+    while (xratio > 1) {
+        if (xratio & 0x1) {
+            std::cerr << "ERROR: FFTFuzzyAdapter: Window increment ratio "
+                      << windowIncrement << " / "
+                      << m_server->getWindowIncrement()
+                      << " must be a power of two" << std::endl;
+            assert(!(xratio & 0x1));
+        }
+        ++m_xshift;
+        xratio >>= 1;
+    }
+
+    while (yratio > 1) {
+        if (yratio & 0x1) {
+            std::cerr << "ERROR: FFTFuzzyAdapter: FFT size ratio "
+                      << m_server->getFFTSize() << " / " << fftSize
+                      << " must be a power of two" << std::endl;
+            assert(!(yratio & 0x1));
+        }
+        ++m_yshift;
+        yratio >>= 1;
+    }
+}
+
+FFTFuzzyAdapter::~FFTFuzzyAdapter()
+{
+    FFTDataServer::releaseInstance(m_server);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fft/FFTFuzzyAdapter.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,80 @@
+/* -*- 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_FUZZY_ADAPTER_H_
+#define _FFT_FUZZY_ADAPTER_H_
+
+#include "FFTDataServer.h"
+
+class FFTFuzzyAdapter
+{
+public:
+    FFTFuzzyAdapter(const DenseTimeValueModel *model,
+                    int channel,
+                    WindowType windowType,
+                    size_t windowSize,
+                    size_t windowIncrement,
+                    size_t fftSize,
+                    bool polar,
+                    size_t fillFromColumn = 0);
+    ~FFTFuzzyAdapter();
+
+    size_t getWidth() const {
+        return m_server->getWidth() >> m_xshift;
+    }
+    size_t getHeight() const {
+        return m_server->getHeight() >> m_yshift;
+    }
+    float getMagnitudeAt(size_t x, size_t y) {
+        return m_server->getMagnitudeAt(x << m_xshift, y << m_yshift);
+    }
+    float getNormalizedMagnitudeAt(size_t x, size_t y) {
+        return m_server->getNormalizedMagnitudeAt(x << m_xshift, y << m_yshift);
+    }
+    float getMaximumMagnitudeAt(size_t x) {
+        return m_server->getMaximumMagnitudeAt(x << m_xshift);
+    }
+    float getPhaseAt(size_t x, size_t y) {
+        return m_server->getPhaseAt(x << m_xshift, y << m_yshift);
+    }
+    void getValuesAt(size_t x, size_t y, float &real, float &imaginary) {
+        m_server->getValuesAt(x << m_xshift, y << m_yshift, real, imaginary);
+    }
+    bool isColumnReady(size_t x) {
+        return m_server->isColumnReady(x << m_xshift);
+    }
+    bool isLocalPeak(size_t x, size_t 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(size_t x, size_t y, float threshold) {
+        return getMagnitudeAt(x, y) > threshold;
+    }
+
+    size_t getFillCompletion() const { return m_server->getFillCompletion(); }
+    size_t getFillExtent() const { return m_server->getFillExtent(); }
+
+private:
+    FFTFuzzyAdapter(const FFTFuzzyAdapter &); // not implemented
+    FFTFuzzyAdapter &operator=(const FFTFuzzyAdapter &); // not implemented
+
+    FFTDataServer *m_server;
+    int m_xshift;
+    int m_yshift;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/AudioFileReader.h	Mon Jul 31 11:49:58 2006 +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 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 _AUDIO_FILE_READER_H_
+#define _AUDIO_FILE_READER_H_
+
+#include <QString>
+#include "base/Model.h" // for SampleBlock
+
+class AudioFileReader
+{
+public:
+    virtual ~AudioFileReader() { }
+
+    bool isOK() const { return (m_channelCount > 0); }
+
+    virtual QString getError() const { return ""; }
+
+    size_t getFrameCount() const { return m_frameCount; }
+    size_t getChannelCount() const { return m_channelCount; }
+    size_t getSampleRate() const { return m_sampleRate; }
+    
+    /** 
+     * 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 void getInterleavedFrames(size_t start, size_t count,
+				      SampleBlock &frames) const = 0;
+    
+protected:
+    size_t m_frameCount;
+    size_t m_channelCount;
+    size_t m_sampleRate;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/AudioFileReaderFactory.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,72 @@
+/* -*- 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 "AudioFileReaderFactory.h"
+
+#include "WavFileReader.h"
+#include "OggVorbisFileReader.h"
+#include "MP3FileReader.h"
+
+#include <QString>
+
+QString
+AudioFileReaderFactory::getKnownExtensions()
+{
+    return
+	"*.wav *.aiff *.aif"
+#ifdef HAVE_MAD
+	" *.mp3"
+#endif
+#ifdef HAVE_OGGZ
+#ifdef HAVE_FISHSOUND
+	" *.ogg"
+#endif
+#endif
+	;
+}
+
+AudioFileReader *
+AudioFileReaderFactory::createReader(QString path)
+{
+    QString err;
+
+    AudioFileReader *reader = 0;
+
+    reader = new WavFileReader(path);
+    if (reader->isOK()) return reader;
+    if (reader->getError() != "") err = reader->getError();
+    delete reader;
+
+#ifdef HAVE_OGGZ
+#ifdef HAVE_FISHSOUND
+    reader = new OggVorbisFileReader(path, true,
+                                     OggVorbisFileReader::CacheInTemporaryFile);
+    if (reader->isOK()) return reader;
+    if (reader->getError() != "") err = reader->getError();
+    delete reader;
+#endif
+#endif
+ 
+#ifdef HAVE_MAD
+    reader = new MP3FileReader(path, true,
+                               MP3FileReader::CacheInTemporaryFile);
+    if (reader->isOK()) return reader;
+    if (reader->getError() != "") err = reader->getError();
+    delete reader;
+#endif
+
+    return 0;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/AudioFileReaderFactory.h	Mon Jul 31 11:49:58 2006 +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 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 _AUDIO_FILE_READER_FACTORY_H_
+#define _AUDIO_FILE_READER_FACTORY_H_
+
+#include <QString>
+
+class AudioFileReader;
+
+class AudioFileReaderFactory
+{
+public:
+    /**
+     * Return the file extensions that we have audio file readers for,
+     * in a format suitable for use with QFileDialog.  For example,
+     * "*.wav *.aiff *.ogg".
+     */
+    static QString getKnownExtensions();
+
+    /**
+     * Return an audio file reader initialised to the file at the
+     * given path, or NULL if no suitable reader for this path is
+     * available or the file cannot be opened.
+     * Caller owns the returned object and must delete it after use.
+     */
+    static AudioFileReader *createReader(QString path);
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/BZipFileDevice.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,195 @@
+/* -*- 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 "BZipFileDevice.h"
+
+#include <bzlib.h>
+
+#include <iostream>
+
+BZipFileDevice::BZipFileDevice(QString fileName) :
+    m_fileName(fileName),
+    m_file(0),
+    m_bzFile(0),
+    m_atEnd(true)
+{
+}
+
+BZipFileDevice::~BZipFileDevice()
+{
+//    std::cerr << "BZipFileDevice::~BZipFileDevice(" << m_fileName.toStdString() << ")" << std::endl;
+    if (m_bzFile) close();
+}
+
+bool
+BZipFileDevice::open(OpenMode mode)
+{
+    if (m_bzFile) {
+        setErrorString(tr("File is already open"));
+        return false;
+    }
+
+    if (mode & Append) {
+        setErrorString(tr("Append mode not supported"));
+        return false;
+    }
+
+    if ((mode & (ReadOnly | WriteOnly)) == 0) {
+        setErrorString(tr("File access mode not specified"));
+        return false;
+    }
+
+    if ((mode & ReadOnly) && (mode & WriteOnly)) {
+        setErrorString(tr("Read and write modes both specified"));
+        return false;
+    }
+
+    if (mode & WriteOnly) {
+
+        m_file = fopen(m_fileName.toLocal8Bit().data(), "wb");
+        if (!m_file) {
+            setErrorString(tr("Failed to open file for writing"));
+            return false;
+        }
+
+        int bzError = BZ_OK;
+        m_bzFile = BZ2_bzWriteOpen(&bzError, m_file, 9, 0, 0);
+
+        if (!m_bzFile) {
+            fclose(m_file);
+            m_file = 0;
+            setErrorString(tr("Failed to open bzip2 stream for writing"));
+            return false;
+        }
+
+//        std::cerr << "BZipFileDevice: opened \"" << m_fileName.toStdString() << "\" for writing" << std::endl;
+
+        setErrorString(QString());
+        setOpenMode(mode);
+        return true;
+    }
+
+    if (mode & ReadOnly) {
+
+        m_file = fopen(m_fileName.toLocal8Bit().data(), "rb");
+        if (!m_file) {
+            setErrorString(tr("Failed to open file for reading"));
+            return false;
+        }
+
+        int bzError = BZ_OK;
+        m_bzFile = BZ2_bzReadOpen(&bzError, m_file, 0, 0, NULL, 0);
+
+        if (!m_bzFile) {
+            fclose(m_file);
+            m_file = 0;
+            setErrorString(tr("Failed to open bzip2 stream for reading"));
+            return false;
+        }
+
+//        std::cerr << "BZipFileDevice: opened \"" << m_fileName.toStdString() << "\" for reading" << std::endl;
+
+        m_atEnd = false;
+
+        setErrorString(QString());
+        setOpenMode(mode);
+        return true;
+    }
+
+    setErrorString(tr("Internal error (open for neither read nor write)"));
+    return false;
+}
+
+void
+BZipFileDevice::close()
+{
+    if (!m_bzFile) {
+        setErrorString(tr("File not open"));
+        return;
+    }
+
+    int bzError = BZ_OK;
+
+    if (openMode() & WriteOnly) {
+        unsigned int in = 0, out = 0;
+        BZ2_bzWriteClose(&bzError, m_bzFile, 0, &in, &out);
+//	std::cerr << "Wrote bzip2 stream (in=" << in << ", out=" << out << ")" << std::endl;
+	if (bzError != BZ_OK) {
+	    setErrorString(tr("bzip2 stream write close error"));
+	}
+        fclose(m_file);
+        m_bzFile = 0;
+        m_file = 0;
+        return;
+    }
+
+    if (openMode() & ReadOnly) {
+        BZ2_bzReadClose(&bzError, m_bzFile);
+        if (bzError != BZ_OK) {
+            setErrorString(tr("bzip2 stream read close error"));
+        }
+        fclose(m_file);
+        m_bzFile = 0;
+        m_file = 0;
+        return;
+    }
+
+    setErrorString(tr("Internal error (close for neither read nor write)"));
+    return;
+}
+
+qint64
+BZipFileDevice::readData(char *data, qint64 maxSize)
+{
+    if (m_atEnd) return 0;
+
+    int bzError = BZ_OK;
+    int read = BZ2_bzRead(&bzError, m_bzFile, data, maxSize);
+
+//    std::cerr << "BZipFileDevice::readData: requested " << maxSize << ", read " << read << std::endl;
+
+    if (bzError != BZ_OK) {
+        if (bzError != BZ_STREAM_END) {
+            std::cerr << "BZipFileDevice::readData: error condition" << std::endl;
+            setErrorString(tr("bzip2 stream read error"));
+            return -1;
+        } else {
+//            std::cerr << "BZipFileDevice::readData: reached end of file" << std::endl;
+            m_atEnd = true;
+        }            
+    }
+
+    return read;
+}
+
+qint64
+BZipFileDevice::writeData(const char *data, qint64 maxSize)
+{
+    int bzError = BZ_OK;
+    BZ2_bzWrite(&bzError, m_bzFile, (void *)data, maxSize);
+
+//    std::cerr << "BZipFileDevice::writeData: " << maxSize << " to write" << std::endl;
+
+    if (bzError != BZ_OK) {
+        std::cerr << "BZipFileDevice::writeData: error condition" << std::endl;
+        setErrorString("bzip2 stream write error");
+        return -1;
+    }
+
+//    std::cerr << "BZipFileDevice::writeData: wrote " << maxSize << std::endl;
+
+    return maxSize;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/BZipFileDevice.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,47 @@
+/* -*- 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 _BZIP_FILE_DEVICE_H_
+#define _BZIP_FILE_DEVICE_H_
+
+#include <QIODevice>
+
+#include <bzlib.h>
+
+class BZipFileDevice : public QIODevice
+{
+    Q_OBJECT
+
+public:
+    BZipFileDevice(QString fileName);
+    virtual ~BZipFileDevice();
+    
+    virtual bool open(OpenMode mode);
+    virtual void close();
+
+    virtual bool isSequential() const { return true; }
+
+protected:
+    virtual qint64 readData(char *data, qint64 maxSize);
+    virtual qint64 writeData(const char *data, qint64 maxSize);
+
+    QString m_fileName;
+
+    FILE *m_file;
+    BZFILE *m_bzFile;
+    bool m_atEnd;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/CSVFileReader.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,645 @@
+/* -*- 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 "CSVFileReader.h"
+
+#include "base/Model.h"
+#include "base/RealTime.h"
+#include "model/SparseOneDimensionalModel.h"
+#include "model/SparseTimeValueModel.h"
+#include "model/DenseThreeDimensionalModel.h"
+
+#include <QFile>
+#include <QString>
+#include <QRegExp>
+#include <QStringList>
+#include <QTextStream>
+#include <QFrame>
+#include <QGridLayout>
+#include <QPushButton>
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+#include <QTableWidget>
+#include <QComboBox>
+#include <QLabel>
+
+#include <iostream>
+
+CSVFileReader::CSVFileReader(QString path, size_t mainModelSampleRate) :
+    m_file(0),
+    m_mainModelSampleRate(mainModelSampleRate)
+{
+    m_file = new QFile(path);
+    bool good = false;
+    
+    if (!m_file->exists()) {
+	m_error = QFile::tr("File \"%1\" does not exist").arg(path);
+    } else if (!m_file->open(QIODevice::ReadOnly | QIODevice::Text)) {
+	m_error = QFile::tr("Failed to open file \"%1\"").arg(path);
+    } else {
+	good = true;
+    }
+
+    if (!good) {
+	delete m_file;
+	m_file = 0;
+    }
+}
+
+CSVFileReader::~CSVFileReader()
+{
+    std::cerr << "CSVFileReader::~CSVFileReader: file is " << m_file << std::endl;
+
+    if (m_file) {
+        std::cerr << "CSVFileReader::CSVFileReader: Closing file" << std::endl;
+        m_file->close();
+    }
+    delete m_file;
+}
+
+bool
+CSVFileReader::isOK() const
+{
+    return (m_file != 0);
+}
+
+QString
+CSVFileReader::getError() const
+{
+    return m_error;
+}
+
+Model *
+CSVFileReader::load() const
+{
+    if (!m_file) return 0;
+
+    CSVFormatDialog *dialog = new CSVFormatDialog
+	(0, m_file, m_mainModelSampleRate);
+
+    if (dialog->exec() == QDialog::Rejected) {
+	delete dialog;
+	return 0;
+    }
+
+    CSVFormatDialog::ModelType   modelType = dialog->getModelType();
+    CSVFormatDialog::TimingType timingType = dialog->getTimingType();
+    CSVFormatDialog::TimeUnits   timeUnits = dialog->getTimeUnits();
+    QString separator = dialog->getSeparator();
+    size_t sampleRate = dialog->getSampleRate();
+    size_t windowSize = dialog->getWindowSize();
+
+    delete dialog;
+
+    if (timingType == CSVFormatDialog::ExplicitTiming) {
+	windowSize = 1;
+	if (timeUnits == CSVFormatDialog::TimeSeconds) {
+	    sampleRate = m_mainModelSampleRate;
+	}
+    }
+
+    SparseOneDimensionalModel *model1 = 0;
+    SparseTimeValueModel *model2 = 0;
+    DenseThreeDimensionalModel *model3 = 0;
+    Model *model = 0;
+
+    QTextStream in(m_file);
+    in.seek(0);
+
+    unsigned int warnings = 0, warnLimit = 10;
+    unsigned int lineno = 0;
+
+    float min = 0.0, max = 0.0;
+
+    size_t frameNo = 0;
+
+    while (!in.atEnd()) {
+
+	QString line = in.readLine().trimmed();
+	if (line.startsWith("#")) continue;
+
+	QStringList list = line.split(separator);
+
+	if (!model) {
+
+	    switch (modelType) {
+
+	    case CSVFormatDialog::OneDimensionalModel:
+		model1 = new SparseOneDimensionalModel(sampleRate, windowSize);
+		model = model1;
+		break;
+		
+	    case CSVFormatDialog::TwoDimensionalModel:
+		model2 = new SparseTimeValueModel(sampleRate, windowSize,
+						  0.0, 0.0,
+						  false);
+		model = model2;
+		break;
+		
+	    case CSVFormatDialog::ThreeDimensionalModel:
+		model3 = new DenseThreeDimensionalModel(sampleRate, windowSize,
+							list.size());
+		model = model3;
+		break;
+	    }
+	}
+
+	QStringList tidyList;
+        QRegExp nonNumericRx("[^0-9.,+-]");
+
+	for (int i = 0; i < list.size(); ++i) {
+	    
+	    QString s(list[i].trimmed());
+
+	    if (s.length() >= 2 && s.startsWith("\"") && s.endsWith("\"")) {
+		s = s.mid(1, s.length() - 2);
+	    } else if (s.length() >= 2 && s.startsWith("'") && s.endsWith("'")) {
+		s = s.mid(1, s.length() - 2);
+	    }
+
+	    if (i == 0 && timingType == CSVFormatDialog::ExplicitTiming) {
+
+		bool ok = false;
+                QString numeric = s;
+                numeric.remove(nonNumericRx);
+
+		if (timeUnits == CSVFormatDialog::TimeSeconds) {
+
+		    double time = numeric.toDouble(&ok);
+		    frameNo = int(time * sampleRate + 0.00001);
+
+		} else {
+
+		    frameNo = numeric.toInt(&ok);
+
+		    if (timeUnits == CSVFormatDialog::TimeWindows) {
+			frameNo *= windowSize;
+		    }
+		}
+			       
+		if (!ok) {
+		    if (warnings < warnLimit) {
+			std::cerr << "WARNING: CSVFileReader::load: "
+				  << "Bad time format (\"" << s.toStdString()
+				  << "\") in data line "
+				  << lineno << ":" << std::endl;
+			std::cerr << line.toStdString() << std::endl;
+		    } else if (warnings == warnLimit) {
+			std::cerr << "WARNING: Too many warnings" << std::endl;
+		    }
+                    ++warnings;
+		}
+	    } else {
+		tidyList.push_back(s);
+	    }
+	}
+
+	if (modelType == CSVFormatDialog::OneDimensionalModel) {
+	    
+	    SparseOneDimensionalModel::Point point
+		(frameNo,
+		 tidyList.size() > 0 ? tidyList[tidyList.size()-1] :
+		 QString("%1").arg(lineno));
+
+	    model1->addPoint(point);
+
+	} else if (modelType == CSVFormatDialog::TwoDimensionalModel) {
+
+	    SparseTimeValueModel::Point point
+		(frameNo,
+		 tidyList.size() > 0 ? tidyList[0].toFloat() : 0.0,
+		 tidyList.size() > 1 ? tidyList[1] : QString("%1").arg(lineno));
+
+	    model2->addPoint(point);
+
+	} else if (modelType == CSVFormatDialog::ThreeDimensionalModel) {
+
+	    DenseThreeDimensionalModel::BinValueSet values;
+
+	    for (int i = 0; i < tidyList.size(); ++i) {
+
+		bool ok = false;
+		float value = list[i].toFloat(&ok);
+		values.push_back(value);
+	    
+		if ((lineno == 0 && i == 0) || value < min) min = value;
+		if ((lineno == 0 && i == 0) || value > max) max = value;
+
+		if (!ok) {
+		    if (warnings < warnLimit) {
+			std::cerr << "WARNING: CSVFileReader::load: "
+				  << "Non-numeric value in data line " << lineno
+				  << ":" << std::endl;
+			std::cerr << line.toStdString() << std::endl;
+			++warnings;
+		    } else if (warnings == warnLimit) {
+			std::cerr << "WARNING: Too many warnings" << std::endl;
+		    }
+		}
+	    }
+	
+	    std::cerr << "Setting bin values for count " << lineno << ", frame "
+		      << frameNo << ", time " << RealTime::frame2RealTime(frameNo, sampleRate) << std::endl;
+
+	    model3->setBinValues(frameNo, values);
+	}
+
+	++lineno;
+	if (timingType == CSVFormatDialog::ImplicitTiming ||
+	    list.size() == 0) {
+	    frameNo += windowSize;
+	}
+    }
+
+    if (modelType == CSVFormatDialog::ThreeDimensionalModel) {
+	model3->setMinimumLevel(min);
+	model3->setMaximumLevel(max);
+    }
+
+    return model;
+}
+
+
+CSVFormatDialog::CSVFormatDialog(QWidget *parent, QFile *file,
+				 size_t defaultSampleRate) :
+    QDialog(parent),
+    m_modelType(OneDimensionalModel),
+    m_timingType(ExplicitTiming),
+    m_timeUnits(TimeAudioFrames),
+    m_separator("")
+{
+    setModal(true);
+    setWindowTitle(tr("Select Data Format"));
+
+    (void)guessFormat(file);
+
+    QGridLayout *layout = new QGridLayout;
+
+    layout->addWidget(new QLabel(tr("\nPlease select the correct data format for this file.\n")),
+		      0, 0, 1, 4);
+
+    layout->addWidget(new QLabel(tr("Each row specifies:")), 1, 0);
+
+    m_modelTypeCombo = new QComboBox;
+    m_modelTypeCombo->addItem(tr("A point in time"));
+    m_modelTypeCombo->addItem(tr("A value at a time"));
+    m_modelTypeCombo->addItem(tr("A set of values"));
+    layout->addWidget(m_modelTypeCombo, 1, 1, 1, 2);
+    connect(m_modelTypeCombo, SIGNAL(activated(int)),
+	    this, SLOT(modelTypeChanged(int)));
+    m_modelTypeCombo->setCurrentIndex(int(m_modelType));
+
+    layout->addWidget(new QLabel(tr("The first column contains:")), 2, 0);
+    
+    m_timingTypeCombo = new QComboBox;
+    m_timingTypeCombo->addItem(tr("Time, in seconds"));
+    m_timingTypeCombo->addItem(tr("Time, in audio sample frames"));
+    m_timingTypeCombo->addItem(tr("Data (rows are consecutive in time)"));
+    layout->addWidget(m_timingTypeCombo, 2, 1, 1, 2);
+    connect(m_timingTypeCombo, SIGNAL(activated(int)),
+	    this, SLOT(timingTypeChanged(int)));
+    m_timingTypeCombo->setCurrentIndex(m_timingType == ExplicitTiming ?
+                                       m_timeUnits == TimeSeconds ? 0 : 1 : 2);
+
+    m_sampleRateLabel = new QLabel(tr("Audio sample rate (Hz):"));
+    layout->addWidget(m_sampleRateLabel, 3, 0);
+    
+    size_t sampleRates[] = {
+	8000, 11025, 12000, 22050, 24000, 32000,
+	44100, 48000, 88200, 96000, 176400, 192000
+    };
+
+    m_sampleRateCombo = new QComboBox;
+    m_sampleRate = defaultSampleRate;
+    for (size_t i = 0; i < sizeof(sampleRates) / sizeof(sampleRates[0]); ++i) {
+	m_sampleRateCombo->addItem(QString("%1").arg(sampleRates[i]));
+	if (sampleRates[i] == m_sampleRate) m_sampleRateCombo->setCurrentIndex(i);
+    }
+    m_sampleRateCombo->setEditable(true);
+
+    layout->addWidget(m_sampleRateCombo, 3, 1);
+    connect(m_sampleRateCombo, SIGNAL(activated(QString)),
+	    this, SLOT(sampleRateChanged(QString)));
+    connect(m_sampleRateCombo, SIGNAL(editTextChanged(QString)),
+	    this, SLOT(sampleRateChanged(QString)));
+
+    m_windowSizeLabel = new QLabel(tr("Frame increment between rows:"));
+    layout->addWidget(m_windowSizeLabel, 4, 0);
+
+    m_windowSizeCombo = new QComboBox;
+    m_windowSize = 1024;
+    for (int i = 0; i <= 16; ++i) {
+	int value = 1 << i;
+	m_windowSizeCombo->addItem(QString("%1").arg(value));
+	if (value == m_windowSize) m_windowSizeCombo->setCurrentIndex(i);
+    }
+    m_windowSizeCombo->setEditable(true);
+
+    layout->addWidget(m_windowSizeCombo, 4, 1);
+    connect(m_windowSizeCombo, SIGNAL(activated(QString)),
+	    this, SLOT(windowSizeChanged(QString)));
+    connect(m_windowSizeCombo, SIGNAL(editTextChanged(QString)),
+	    this, SLOT(windowSizeChanged(QString)));
+
+    layout->addWidget(new QLabel(tr("\nExample data from file:")), 5, 0, 1, 4);
+
+    m_exampleWidget = new QTableWidget
+	(std::min(10, m_example.size()), m_maxExampleCols);
+
+    layout->addWidget(m_exampleWidget, 6, 0, 1, 4);
+    layout->setColumnStretch(3, 10);
+    layout->setRowStretch(4, 10);
+
+    QPushButton *ok = new QPushButton(tr("OK"));
+    connect(ok, SIGNAL(clicked()), this, SLOT(accept()));
+    ok->setDefault(true);
+
+    QPushButton *cancel = new QPushButton(tr("Cancel"));
+    connect(cancel, SIGNAL(clicked()), this, SLOT(reject()));
+
+    QHBoxLayout *buttonLayout = new QHBoxLayout;
+    buttonLayout->addStretch(1);
+    buttonLayout->addWidget(ok);
+    buttonLayout->addWidget(cancel);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    mainLayout->addLayout(layout);
+    mainLayout->addLayout(buttonLayout);
+
+    setLayout(mainLayout);
+    
+    timingTypeChanged(m_timingTypeCombo->currentIndex());
+}
+
+CSVFormatDialog::~CSVFormatDialog()
+{
+}
+
+void
+CSVFormatDialog::populateExample()
+{
+    m_exampleWidget->setColumnCount
+	(m_timingType == ExplicitTiming ?
+	 m_maxExampleCols - 1 : m_maxExampleCols);
+
+    m_exampleWidget->setHorizontalHeaderLabels(QStringList());
+
+    for (int i = 0; i < m_example.size(); ++i) {
+	for (int j = 0; j < m_example[i].size(); ++j) {
+
+	    QTableWidgetItem *item = new QTableWidgetItem(m_example[i][j]);
+
+	    if (j == 0) {
+		if (m_timingType == ExplicitTiming) {
+		    m_exampleWidget->setVerticalHeaderItem(i, item);
+		    continue;
+		} else {
+		    QTableWidgetItem *header =
+			new QTableWidgetItem(QString("%1").arg(i));
+		    header->setFlags(Qt::ItemIsEnabled);
+		    m_exampleWidget->setVerticalHeaderItem(i, header);
+		}
+	    }
+	    int index = j;
+	    if (m_timingType == ExplicitTiming) --index;
+	    item->setFlags(Qt::ItemIsEnabled);
+	    m_exampleWidget->setItem(i, index, item);
+	}
+    }
+}
+
+void
+CSVFormatDialog::modelTypeChanged(int type)
+{
+    m_modelType = (ModelType)type;
+
+    if (m_modelType == ThreeDimensionalModel) {
+        // We can't load 3d models with explicit timing, because the 3d
+        // model is dense so we need a fixed sample increment
+        m_timingTypeCombo->setCurrentIndex(2);
+        timingTypeChanged(2);
+    }
+}
+
+void
+CSVFormatDialog::timingTypeChanged(int type)
+{
+    switch (type) {
+
+    case 0:
+	m_timingType = ExplicitTiming;
+	m_timeUnits = TimeSeconds;
+	m_sampleRateCombo->setEnabled(false);
+	m_sampleRateLabel->setEnabled(false);
+	m_windowSizeCombo->setEnabled(false);
+	m_windowSizeLabel->setEnabled(false);
+        if (m_modelType == ThreeDimensionalModel) {
+            m_modelTypeCombo->setCurrentIndex(1);
+            modelTypeChanged(1);
+        }
+	break;
+
+    case 1:
+	m_timingType = ExplicitTiming;
+	m_timeUnits = TimeAudioFrames;
+	m_sampleRateCombo->setEnabled(true);
+	m_sampleRateLabel->setEnabled(true);
+	m_windowSizeCombo->setEnabled(false);
+	m_windowSizeLabel->setEnabled(false);
+        if (m_modelType == ThreeDimensionalModel) {
+            m_modelTypeCombo->setCurrentIndex(1);
+            modelTypeChanged(1);
+        }
+	break;
+
+    case 2:
+	m_timingType = ImplicitTiming;
+	m_timeUnits = TimeWindows;
+	m_sampleRateCombo->setEnabled(true);
+	m_sampleRateLabel->setEnabled(true);
+	m_windowSizeCombo->setEnabled(true);
+	m_windowSizeLabel->setEnabled(true);
+	break;
+    }
+
+    populateExample();
+}
+
+void
+CSVFormatDialog::sampleRateChanged(QString rateString)
+{
+    bool ok = false;
+    int sampleRate = rateString.toInt(&ok);
+    if (ok) m_sampleRate = sampleRate;
+}
+
+void
+CSVFormatDialog::windowSizeChanged(QString sizeString)
+{
+    bool ok = false;
+    int size = sizeString.toInt(&ok);
+    if (ok) m_windowSize = size;
+}
+
+bool
+CSVFormatDialog::guessFormat(QFile *file)
+{
+    QTextStream in(file);
+    in.seek(0);
+
+    unsigned int lineno = 0;
+
+    bool nonIncreasingPrimaries = false;
+    bool nonNumericPrimaries = false;
+    bool floatPrimaries = false;
+    bool variableItemCount = false;
+    int itemCount = 1;
+    int earliestNonNumericItem = -1;
+
+    float prevPrimary = 0.0;
+
+    m_maxExampleCols = 0;
+
+    while (!in.atEnd()) {
+	
+	QString line = in.readLine().trimmed();
+	if (line.startsWith("#")) continue;
+
+	if (m_separator == "") {
+	    //!!! to do: ask the user
+	    if (line.split(",").size() >= 2) m_separator = ",";
+	    else if (line.split("\t").size() >= 2) m_separator = "\t";
+	    else if (line.split("|").size() >= 2) m_separator = "|";
+	    else if (line.split("/").size() >= 2) m_separator = "/";
+	    else if (line.split(":").size() >= 2) m_separator = ":";
+	    else m_separator = " ";
+	}
+
+	QStringList list = line.split(m_separator);
+	QStringList tidyList;
+
+	for (int i = 0; i < list.size(); ++i) {
+	    
+	    QString s(list[i]);
+	    bool numeric = false;
+
+	    if (s.length() >= 2 && s.startsWith("\"") && s.endsWith("\"")) {
+		s = s.mid(1, s.length() - 2);
+	    } else if (s.length() >= 2 && s.startsWith("'") && s.endsWith("'")) {
+		s = s.mid(1, s.length() - 2);
+	    } else {
+		(void)s.toFloat(&numeric);
+	    }
+
+	    tidyList.push_back(s);
+
+	    if (lineno == 0 || (list.size() < itemCount)) {
+		itemCount = list.size();
+	    } else {
+		if (itemCount != list.size()) {
+		    variableItemCount = true;
+		}
+	    }
+	    
+	    if (i == 0) { // primary
+
+		if (numeric) {
+
+		    float primary = s.toFloat();
+
+		    if (lineno > 0 && primary <= prevPrimary) {
+			nonIncreasingPrimaries = true;
+		    }
+
+		    if (s.contains(".") || s.contains(",")) {
+			floatPrimaries = true;
+		    }
+
+		    prevPrimary = primary;
+
+		} else {
+		    nonNumericPrimaries = true;
+		}
+	    } else { // secondary
+
+		if (!numeric) {
+		    if (earliestNonNumericItem < 0 ||
+			i < earliestNonNumericItem) {
+			earliestNonNumericItem = i;
+		    }
+		}
+	    }
+	}
+
+	if (lineno < 10) {
+	    m_example.push_back(tidyList);
+	    if (lineno == 0 || tidyList.size() > m_maxExampleCols) {
+		m_maxExampleCols = tidyList.size();
+	    }
+	}
+
+	++lineno;
+
+	if (lineno == 50) break;
+    }
+
+    if (nonNumericPrimaries || nonIncreasingPrimaries) {
+	
+	// Primaries are probably not a series of times
+
+	m_timingType = ImplicitTiming;
+	m_timeUnits = TimeWindows;
+	
+	if (nonNumericPrimaries) {
+	    m_modelType = OneDimensionalModel;
+	} else if (itemCount == 1 || variableItemCount ||
+		   (earliestNonNumericItem != -1)) {
+	    m_modelType = TwoDimensionalModel;
+	} else {
+	    m_modelType = ThreeDimensionalModel;
+	}
+
+    } else {
+
+	// Increasing numeric primaries -- likely to be time
+
+	m_timingType = ExplicitTiming;
+
+	if (floatPrimaries) {
+	    m_timeUnits = TimeSeconds;
+	} else {
+	    m_timeUnits = TimeAudioFrames;
+	}
+
+	if (itemCount == 1) {
+	    m_modelType = OneDimensionalModel;
+	} else if (variableItemCount || (earliestNonNumericItem != -1)) {
+	    if (earliestNonNumericItem != -1 && earliestNonNumericItem < 2) {
+		m_modelType = OneDimensionalModel;
+	    } else {
+		m_modelType = TwoDimensionalModel;
+	    }
+	} else {
+	    m_modelType = ThreeDimensionalModel;
+	}
+    }
+
+    std::cerr << "Estimated model type: " << m_modelType << std::endl;
+    std::cerr << "Estimated timing type: " << m_timingType << std::endl;
+    std::cerr << "Estimated units: " << m_timeUnits << std::endl;
+
+    in.seek(0);
+    return true;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/CSVFileReader.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,111 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This 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 _CSV_FILE_READER_H_
+#define _CSV_FILE_READER_H_
+
+#include "DataFileReader.h"
+
+#include <QList>
+#include <QStringList>
+#include <QDialog>
+
+class QFile;
+class QTableWidget;
+class QComboBox;
+class QLabel;
+
+
+class CSVFileReader : public DataFileReader
+{
+public:
+    CSVFileReader(QString path, size_t mainModelSampleRate);
+    virtual ~CSVFileReader();
+
+    virtual bool isOK() const;
+    virtual QString getError() const;
+    virtual Model *load() const;
+
+protected:
+    QFile *m_file;
+    QString m_error;
+    size_t m_mainModelSampleRate;
+};
+
+
+class CSVFormatDialog : public QDialog
+{
+    Q_OBJECT
+    
+public:
+    CSVFormatDialog(QWidget *parent, QFile *file, size_t defaultSampleRate);
+    
+    ~CSVFormatDialog();
+    
+    enum ModelType {
+	OneDimensionalModel,
+	TwoDimensionalModel,
+	ThreeDimensionalModel
+    };
+    
+    enum TimingType {
+	ExplicitTiming,
+	ImplicitTiming
+    };
+    
+    enum TimeUnits {
+	TimeSeconds,
+	TimeAudioFrames,
+	TimeWindows
+    };
+
+    ModelType  getModelType()   const { return m_modelType;   }
+    TimingType getTimingType()  const { return m_timingType;  }
+    TimeUnits  getTimeUnits()   const { return m_timeUnits;   }
+    QString    getSeparator()   const { return m_separator;   }
+    size_t     getSampleRate()  const { return m_sampleRate;  }
+    size_t     getWindowSize()  const { return m_windowSize;  }
+
+protected slots:
+    void modelTypeChanged(int type);
+    void timingTypeChanged(int type);
+    void sampleRateChanged(QString);
+    void windowSizeChanged(QString);
+
+protected:
+    ModelType  m_modelType;
+    TimingType m_timingType;
+    TimeUnits  m_timeUnits;
+    QString    m_separator;
+    size_t     m_sampleRate;
+    size_t     m_windowSize;
+    
+    QList<QStringList> m_example;
+    int m_maxExampleCols;
+    QTableWidget *m_exampleWidget;
+    
+    QComboBox *m_modelTypeCombo;
+    QComboBox *m_timingTypeCombo;
+    QLabel *m_sampleRateLabel;
+    QComboBox *m_sampleRateCombo;
+    QLabel *m_windowSizeLabel;
+    QComboBox *m_windowSizeCombo;
+
+    bool guessFormat(QFile *file);
+    void populateExample();
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/CSVFileWriter.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,66 @@
+/* -*- 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 "CSVFileWriter.h"
+
+#include "base/Model.h"
+#include "model/SparseOneDimensionalModel.h"
+#include "model/SparseTimeValueModel.h"
+#include "model/NoteModel.h"
+#include "model/TextModel.h"
+
+#include <QFile>
+#include <QTextStream>
+
+CSVFileWriter::CSVFileWriter(QString path, Model *model, QString delimiter) :
+    m_path(path),
+    m_model(model),
+    m_error(""),
+    m_delimiter(delimiter)
+{
+}
+
+CSVFileWriter::~CSVFileWriter()
+{
+}
+
+bool
+CSVFileWriter::isOK() const
+{
+    return m_error == "";
+}
+
+QString
+CSVFileWriter::getError() const
+{
+    return m_error;
+}
+
+void
+CSVFileWriter::write()
+{
+    QFile file(m_path);
+    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
+        m_error = tr("Failed to open file %1 for writing").arg(m_path);
+        return;
+    }
+    
+    QTextStream out(&file);
+    out << m_model->toDelimitedDataString(m_delimiter);
+
+    file.close();
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/CSVFileWriter.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,46 @@
+/* -*- 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 _CSV_FILE_WRITER_H_
+#define _CSV_FILE_WRITER_H_
+
+#include <QObject>
+#include <QString>
+
+class Model;
+
+class CSVFileWriter : public QObject
+{
+    Q_OBJECT
+
+public:
+    CSVFileWriter(QString path, Model *model, QString delimiter = ",");
+    virtual ~CSVFileWriter();
+
+    virtual bool isOK() const;
+    virtual QString getError() const;
+
+    virtual void write();
+
+protected:
+    QString m_path;
+    Model *m_model;
+    QString m_error;
+    QString m_delimiter;
+};
+
+#endif
+
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/CodedAudioFileReader.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,192 @@
+/* -*- 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 "CodedAudioFileReader.h"
+
+#include "WavFileReader.h"
+#include "base/TempDirectory.h"
+#include "base/Exceptions.h"
+
+#include <iostream>
+#include <QDir>
+
+CodedAudioFileReader::CodedAudioFileReader(CacheMode cacheMode) :
+    m_cacheMode(cacheMode),
+    m_initialised(false),
+    m_cacheFileWritePtr(0),
+    m_cacheFileReader(0),
+    m_cacheWriteBuffer(0),
+    m_cacheWriteBufferIndex(0),
+    m_cacheWriteBufferSize(16384)
+{
+}
+
+CodedAudioFileReader::~CodedAudioFileReader()
+{
+    if (m_cacheFileWritePtr) sf_close(m_cacheFileWritePtr);
+    if (m_cacheFileReader) delete m_cacheFileReader;
+    if (m_cacheWriteBuffer) delete[] m_cacheWriteBuffer;
+
+    if (m_cacheFileName != "") {
+        if (!QFile(m_cacheFileName).remove()) {
+            std::cerr << "WARNING: CodedAudioFileReader::~CodedAudioFileReader: Failed to delete cache file \"" << m_cacheFileName.toStdString() << "\"" << std::endl;
+        }
+    }
+}
+
+void
+CodedAudioFileReader::initialiseDecodeCache()
+{
+    if (m_cacheMode == CacheInTemporaryFile) {
+
+        m_cacheWriteBuffer = new float[m_cacheWriteBufferSize * m_channelCount];
+        m_cacheWriteBufferIndex = 0;
+
+        try {
+            QDir dir(TempDirectory::getInstance()->getPath());
+            m_cacheFileName = dir.filePath(QString("decoded_%1.wav")
+                                           .arg((intptr_t)this));
+
+            SF_INFO fileInfo;
+            fileInfo.samplerate = m_sampleRate;
+            fileInfo.channels = m_channelCount;
+            fileInfo.format = SF_FORMAT_WAV | SF_FORMAT_FLOAT;
+    
+            m_cacheFileWritePtr = sf_open(m_cacheFileName.toLocal8Bit(),
+                                          SFM_WRITE, &fileInfo);
+
+            if (!m_cacheFileWritePtr) {
+                std::cerr << "CodedAudioFileReader::initialiseDecodeCache: failed to open cache file \"" << m_cacheFileName.toStdString() << "\" (" << m_channelCount << " channels, sample rate " << m_sampleRate << " for writing, falling back to in-memory cache" << std::endl;
+                m_cacheMode = CacheInMemory;
+            }
+        } catch (DirectoryCreationFailed f) {
+            std::cerr << "CodedAudioFileReader::initialiseDecodeCache: failed to create temporary directory! Falling back to in-memory cache" << std::endl;
+            m_cacheMode = CacheInMemory;
+        }
+    }
+
+    if (m_cacheMode == CacheInMemory) {
+        m_data.clear();
+    }
+
+    m_initialised = true;
+}
+
+void
+CodedAudioFileReader::addSampleToDecodeCache(float sample)
+{
+    if (!m_initialised) return;
+
+    switch (m_cacheMode) {
+
+    case CacheInTemporaryFile:
+
+        m_cacheWriteBuffer[m_cacheWriteBufferIndex++] = sample;
+
+        if (m_cacheWriteBufferIndex ==
+            m_cacheWriteBufferSize * m_channelCount) {
+
+            //!!! check for return value! out of disk space, etc!
+            sf_writef_float(m_cacheFileWritePtr,
+                            m_cacheWriteBuffer,
+                            m_cacheWriteBufferSize);
+
+            m_cacheWriteBufferIndex = 0;
+        }
+        break;
+
+    case CacheInMemory:
+        m_data.push_back(sample);
+        break;
+    }
+}
+
+void
+CodedAudioFileReader::finishDecodeCache()
+{
+    if (!m_initialised) {
+        std::cerr << "WARNING: CodedAudioFileReader::finishDecodeCache: Cache was never initialised!" << std::endl;
+        return;
+    }
+
+    switch (m_cacheMode) {
+
+    case CacheInTemporaryFile:
+
+        if (m_cacheWriteBufferIndex > 0) {
+            //!!! check for return value! out of disk space, etc!
+            sf_writef_float(m_cacheFileWritePtr,
+                            m_cacheWriteBuffer,
+                            m_cacheWriteBufferIndex / m_channelCount);
+        }
+
+        if (m_cacheWriteBuffer) {
+            delete[] m_cacheWriteBuffer;
+            m_cacheWriteBuffer = 0;
+        }
+
+        m_cacheWriteBufferIndex = 0;
+
+        sf_close(m_cacheFileWritePtr);
+        m_cacheFileWritePtr = 0;
+
+        m_cacheFileReader = new WavFileReader(m_cacheFileName);
+
+        if (!m_cacheFileReader->isOK()) {
+            std::cerr << "ERROR: CodedAudioFileReader::finishDecodeCache: Failed to construct WAV file reader for temporary file: " << m_cacheFileReader->getError().toStdString() << std::endl;
+            delete m_cacheFileReader;
+            m_cacheFileReader = 0;
+        }
+        break;
+
+    case CacheInMemory:
+        // nothing to do 
+        break;
+    }
+}
+
+void
+CodedAudioFileReader::getInterleavedFrames(size_t start, size_t count,
+                                           SampleBlock &frames) const
+{
+    if (!m_initialised) return;
+
+    switch (m_cacheMode) {
+
+    case CacheInTemporaryFile:
+        if (m_cacheFileReader) {
+            m_cacheFileReader->getInterleavedFrames(start, count, frames);
+        }
+        break;
+
+    case CacheInMemory:
+    {
+        frames.clear();
+        if (!isOK()) return;
+        if (count == 0) return;
+
+        // slownessabounds
+
+        for (size_t i = start; i < start + count; ++i) {
+            for (size_t ch = 0; ch < m_channelCount; ++ch) {
+                size_t index = i * m_channelCount + ch;
+                if (index >= m_data.size()) return;
+                frames.push_back(m_data[index]);
+            }
+        }
+    }
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/CodedAudioFileReader.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,58 @@
+/* -*- 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 _CODED_AUDIO_FILE_READER_H_
+#define _CODED_AUDIO_FILE_READER_H_
+
+#include "AudioFileReader.h"
+
+#include <sndfile.h>
+
+class WavFileReader;
+
+class CodedAudioFileReader : public AudioFileReader
+{
+public:
+    virtual ~CodedAudioFileReader();
+
+    enum CacheMode {
+        CacheInTemporaryFile,
+        CacheInMemory
+    };
+
+    virtual void getInterleavedFrames(size_t start, size_t count,
+				      SampleBlock &frames) const;
+
+protected:
+    CodedAudioFileReader(CacheMode cacheMode);
+
+    void initialiseDecodeCache(); // samplerate, channels must have been set
+    void addSampleToDecodeCache(float sample);
+    void finishDecodeCache();
+    bool isDecodeCacheInitialised() const { return m_initialised; }
+
+    CacheMode m_cacheMode;
+    SampleBlock m_data;
+    bool m_initialised;
+
+    QString m_cacheFileName;
+    SNDFILE *m_cacheFileWritePtr;
+    WavFileReader *m_cacheFileReader;
+    float *m_cacheWriteBuffer;
+    size_t m_cacheWriteBufferIndex;
+    size_t m_cacheWriteBufferSize; // frames
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/ConfigFile.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,181 @@
+/* -*- 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 "ConfigFile.h"
+
+#include "base/Exceptions.h"
+
+#include <iostream>
+
+#include <QFile>
+#include <QMutexLocker>
+#include <QTextStream>
+#include <QStringList>
+
+ConfigFile::ConfigFile(QString filename) :
+    m_filename(filename),
+    m_loaded(false),
+    m_modified(false)
+{
+}
+
+ConfigFile::~ConfigFile()
+{
+    try {
+        commit();
+    } catch (FileOperationFailed f) {
+        std::cerr << "WARNING: ConfigFile::~ConfigFile: Commit failed for "
+                  << m_filename.toStdString() << std::endl;
+    }
+}
+
+QString
+ConfigFile::get(QString key, QString deft)
+{
+    if (!m_loaded) load();
+        
+    QMutexLocker locker(&m_mutex);
+
+    if (m_data.find(key) == m_data.end()) return deft;
+    return m_data[key];
+}
+
+int
+ConfigFile::getInt(QString key, int deft)
+{
+    return get(key, QString("%1").arg(deft)).toInt();
+}
+
+bool
+ConfigFile::getBool(QString key, bool deft)
+{
+    QString value = get(key, deft ? "true" : "false").trimmed().toLower();
+    return (value == "true" || value == "yes" || value == "on" || value == "1");
+}
+ 
+float
+ConfigFile::getFloat(QString key, float deft)
+{
+    return get(key, QString("%1").arg(deft)).toFloat();
+}
+
+QStringList
+ConfigFile::getStringList(QString key)
+{
+    return get(key).split('|');
+}
+
+void
+ConfigFile::set(QString key, QString value)
+{
+    if (!m_loaded) load();
+        
+    QMutexLocker locker(&m_mutex);
+
+    m_data[key] = value;
+
+    m_modified = true;
+}
+
+void
+ConfigFile::set(QString key, int value)
+{
+    set(key, QString("%1").arg(value));
+}
+
+void
+ConfigFile::set(QString key, bool value)
+{
+    set(key, value ? QString("true") : QString("false"));
+}
+
+void
+ConfigFile::set(QString key, float value)
+{
+    set(key, QString("%1").arg(value));
+}
+
+void
+ConfigFile::set(QString key, const QStringList &values)
+{
+    set(key, values.join("|"));
+}
+
+void
+ConfigFile::commit()
+{
+    QMutexLocker locker(&m_mutex);
+
+    if (!m_modified) return;
+
+    // Really we should write to another file and then move to the
+    // intended target, but I don't think we're all that particular
+    // about reliability here at the moment
+
+    QFile file(m_filename);
+
+    if (!file.open(QFile::WriteOnly | QFile::Text)) {
+        throw FileOperationFailed(m_filename, "open for writing");
+    }
+    
+    QTextStream out(&file);
+
+    for (DataMap::const_iterator i = m_data.begin(); i != m_data.end(); ++i) {
+        out << i->first << "=" << i->second << endl;
+    }
+
+    m_modified = false;
+}
+
+bool
+ConfigFile::load()
+{
+    QMutexLocker locker(&m_mutex);
+
+    if (m_loaded) return true;
+
+    QFile file(m_filename);
+
+    if (!file.open(QFile::ReadOnly | QFile::Text)) {
+        return false;
+    }
+
+    QTextStream in(&file);
+
+    m_data.clear();
+
+    while (!in.atEnd()) {
+        
+        QString line = in.readLine(2048);
+        QString key = line.section('=', 0, 0);
+        QString value = line.section('=', 1, -1);
+        if (key == "") continue;
+
+        m_data[key] = value;
+    }
+    
+    m_loaded = true;
+    m_modified = false;
+    return true;
+}
+
+void
+ConfigFile::reset()
+{
+    QMutexLocker locker(&m_mutex);
+    m_loaded = false;
+    m_modified = false;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/ConfigFile.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,89 @@
+/* -*- 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 _CONFIG_FILE_H_
+#define _CONFIG_FILE_H_
+
+#include <QString>
+#include <QMutex>
+
+#include <map>
+
+class ConfigFile
+{
+public:
+    ConfigFile(QString filename);
+    virtual ~ConfigFile();
+
+    /**
+     * Get a value, with a default if it hasn't been set.
+     */
+    QString get(QString key, QString deft = "");
+
+    bool getBool(QString key, bool deft);
+
+    int getInt(QString key, int deft);
+    
+    float getFloat(QString key, float deft);
+
+    QStringList getStringList(QString key);
+
+    /**
+     * Set a value.  Values must not contain carriage return or other
+     * non-printable characters.  Keys must contain [a-zA-Z0-9_-] only.
+     */
+    void set(QString key, QString value);
+
+    void set(QString key, bool value);
+
+    void set(QString key, int value);
+    
+    void set(QString key, float value);
+    
+    void set(QString key, const QStringList &values); // must not contain '|'
+
+    /**
+     * Write the data to file.  May throw FileOperationFailed.
+     *
+     * This is called automatically on destruction if any data has
+     * changed since it was last called.  At that time, any exception
+     * will be ignored.  If you want to ensure that exceptions are
+     * handled, call it yourself before destruction.
+     */
+    void commit();
+
+    /**
+     * Return to the stored values.  You can also call this before
+     * destruction if you want to ensure that any values modified so
+     * far are not written out to file on destruction.
+     */
+    void reset();
+
+protected:
+    bool load();
+
+    QString m_filename;
+
+    typedef std::map<QString, QString> DataMap;
+    DataMap m_data;
+
+    bool m_loaded;
+    bool m_modified;
+
+    QMutex m_mutex;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/DataFileReader.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,58 @@
+/* -*- 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 _DATA_FILE_READER_H_
+#define _DATA_FILE_READER_H_
+
+#include <QString>
+
+class Model;
+
+class DataFileReader
+{
+public:
+    /**
+     * Return true if the file appears to be of the correct type.
+     *
+     * The DataFileReader will be constructed by passing a file path
+     * to its constructor.  If the file can at that time be determined
+     * to be not of a type that this reader can read, it should return
+     * false in response to any subsequent call to isOK().
+     *
+     * If the file is apparently of the correct type, isOK() should
+     * return true; if it turns out that the file cannot after all be
+     * read (because it's corrupted or the detection misfired), then
+     * the read() function may return NULL.
+     */
+    virtual bool isOK() const = 0;
+
+    virtual QString getError() const { return ""; }
+
+    /**
+     * Read the file and return the corresponding data model.  This
+     * function is not expected to be thread-safe or reentrant.  This
+     * function may be interactive (i.e. it's permitted to pop up
+     * dialogs and windows and ask the user to specify any details
+     * that can't be automatically extracted from the file).
+     *
+     * Return NULL if the file cannot be parsed at all (although it's
+     * preferable to return a partial model and warn the user).
+     *
+     * Caller owns the returned model and must delete it after use.
+     */
+    virtual Model *load() const = 0;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/DataFileReaderFactory.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,61 @@
+/* -*- 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 "DataFileReaderFactory.h"
+#include "MIDIFileReader.h"
+#include "CSVFileReader.h"
+
+#include "base/Model.h"
+
+#include <QString>
+
+QString
+DataFileReaderFactory::getKnownExtensions()
+{
+    return "*.svl *.csv *.lab *.mid *.txt";
+}
+
+DataFileReader *
+DataFileReaderFactory::createReader(QString path, size_t mainModelSampleRate)
+{
+    QString err;
+
+    DataFileReader *reader = 0;
+
+    reader = new MIDIFileReader(path, mainModelSampleRate);
+    if (reader->isOK()) return reader;
+    if (reader->getError() != "") err = reader->getError();
+    delete reader;
+
+    reader = new CSVFileReader(path, mainModelSampleRate);
+    if (reader->isOK()) return reader;
+    if (reader->getError() != "") err = reader->getError();
+    delete reader;
+
+    return 0;
+}
+
+Model *
+DataFileReaderFactory::load(QString path, size_t mainModelSampleRate)
+{
+    DataFileReader *reader = createReader(path, mainModelSampleRate);
+    if (!reader) return NULL;
+
+    Model *model = reader->load();
+    delete reader;
+
+    return model;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/DataFileReaderFactory.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,51 @@
+/* -*- 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 _DATA_FILE_READER_FACTORY_H_
+#define _DATA_FILE_READER_FACTORY_H_
+
+#include <QString>
+
+class DataFileReader;
+class Model;
+
+class DataFileReaderFactory
+{
+public:
+    /**
+     * Return the file extensions that we have data file readers for,
+     * in a format suitable for use with QFileDialog.  For example,
+     * "*.csv *.xml".
+     */
+    static QString getKnownExtensions();
+
+    /**
+     * Return a data file reader initialised to the file at the
+     * given path, or NULL if no suitable reader for this path is
+     * available or the file cannot be opened.
+     * Caller owns the returned object and must delete it after use.
+     */
+    static DataFileReader *createReader(QString path,
+					size_t mainModelSampleRate);
+
+    /**
+     * Read the given path, if a suitable reader is available.
+     * Return NULL if no reader succeeded in reading this file.
+     */
+    static Model *load(QString path, size_t mainModelSampleRate);
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/FFTDataServer.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,751 @@
+/* -*- 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 "FFTDataServer.h"
+
+#include "FFTFileCache.h"
+
+#include "model/DenseTimeValueModel.h"
+
+#include "base/System.h"
+
+//#define DEBUG_FFT_SERVER 1
+//#define DEBUG_FFT_SERVER_FILL 1
+
+#ifdef DEBUG_FFT_SERVER_FILL
+#define DEBUG_FFT_SERVER
+#endif
+
+FFTDataServer::ServerMap FFTDataServer::m_servers;
+QMutex FFTDataServer::m_serverMapMutex;
+
+FFTDataServer *
+FFTDataServer::getInstance(const DenseTimeValueModel *model,
+                           int channel,
+                           WindowType windowType,
+                           size_t windowSize,
+                           size_t windowIncrement,
+                           size_t fftSize,
+                           bool polar,
+                           size_t fillFromColumn)
+{
+    QString n = generateFileBasename(model,
+                                     channel,
+                                     windowType,
+                                     windowSize,
+                                     windowIncrement,
+                                     fftSize,
+                                     polar);
+
+    FFTDataServer *server = 0;
+    
+    QMutexLocker locker(&m_serverMapMutex);
+
+    if ((server = findServer(n))) {
+        return server;
+    }
+
+    QString npn = generateFileBasename(model,
+                                       channel,
+                                       windowType,
+                                       windowSize,
+                                       windowIncrement,
+                                       fftSize,
+                                       !polar);
+
+    if ((server = findServer(npn))) {
+        return server;
+    }
+
+    m_servers[n] = ServerCountPair
+        (new FFTDataServer(n,
+                           model,
+                           channel,
+                           windowType,
+                           windowSize,
+                           windowIncrement,
+                           fftSize,
+                           polar,
+                           fillFromColumn),
+         1);
+
+    return m_servers[n].first;
+}
+
+FFTDataServer *
+FFTDataServer::getFuzzyInstance(const DenseTimeValueModel *model,
+                                int channel,
+                                WindowType windowType,
+                                size_t windowSize,
+                                size_t windowIncrement,
+                                size_t fftSize,
+                                bool polar,
+                                size_t fillFromColumn)
+{
+    // 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 FFTFuzzyAdapter 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 fuzzy adapter doesn't support this).
+
+    {
+        QMutexLocker locker(&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 << "Distance " << distance << ", best is " << bestdist << std::endl;
+#endif
+                
+                if (bestdist == -1 || distance < bestdist) {
+                    bestdist = distance;
+                    best = i;
+                }
+            }
+        }
+
+        if (bestdist >= 0) {
+            ++best->second.second;
+            return best->second.first;
+        }
+    }
+
+    // Nothing found, make a new one
+
+    return getInstance(model,
+                       channel,
+                       windowType,
+                       windowSize,
+                       windowIncrement,
+                       fftSize,
+                       polar,
+                       fillFromColumn);
+}
+
+FFTDataServer *
+FFTDataServer::findServer(QString n)
+{    
+    if (m_servers.find(n) != m_servers.end()) {
+        ++m_servers[n].second;
+        return m_servers[n].first;
+    }
+
+    return 0;
+}
+
+void
+FFTDataServer::releaseInstance(FFTDataServer *server)
+{
+#ifdef DEBUG_FFT_SERVER
+    std::cerr << "FFTDataServer::releaseInstance(" << server << ")" << std::endl;
+#endif
+
+    QMutexLocker locker(&m_serverMapMutex);
+
+    //!!! not a good strategy.  Want something like:
+
+    // -- 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
+
+    // also:
+    //
+
+    for (ServerMap::iterator i = m_servers.begin(); i != m_servers.end(); ++i) {
+        if (i->second.first == server) {
+            if (i->second.second == 0) {
+                std::cerr << "ERROR: FFTDataServer::releaseInstance("
+                          << server << "): instance not allocated" << std::endl;
+            } else if (--i->second.second == 0) {
+                if (server->m_lastUsedCache == -1) { // never used
+                    delete server;
+                    m_servers.erase(i);
+                } else {
+                    server->suspend();
+                    purgeLimbo();
+                }
+            }
+            return;
+        }
+    }
+
+    std::cerr << "ERROR: FFTDataServer::releaseInstance(" << server << "): "
+              << "instance not found" << std::endl;
+}
+
+void
+FFTDataServer::purgeLimbo(int maxSize)
+{
+    ServerMap::iterator i = m_servers.end();
+
+    int count = 0;
+
+    while (i != m_servers.begin()) {
+        --i;
+        if (i->second.second == 0) {
+            if (++count > maxSize) {
+                delete i->second.first;
+                m_servers.erase(i);
+                return;
+            }
+        }
+    }
+}
+
+FFTDataServer::FFTDataServer(QString fileBaseName,
+                             const DenseTimeValueModel *model,
+                             int channel,
+			     WindowType windowType,
+			     size_t windowSize,
+			     size_t windowIncrement,
+			     size_t fftSize,
+                             bool polar,
+                             size_t fillFromColumn) :
+    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_lastUsedCache(-1),
+    m_fftInput(0),
+    m_exiting(false),
+    m_fillThread(0)
+{
+    size_t start = m_model->getStartFrame();
+    size_t end = m_model->getEndFrame();
+
+    m_width = (end - start) / m_windowIncrement + 1;
+    m_height = m_fftSize / 2;
+
+    size_t maxCacheSize = 20 * 1024 * 1024;
+    size_t columnSize = m_height * sizeof(fftsample) * 2 + sizeof(fftsample);
+    if (m_width * columnSize < maxCacheSize * 2) m_cacheWidth = m_width;
+    else m_cacheWidth = maxCacheSize / columnSize;
+    
+    int bits = 0;
+    while (m_cacheWidth) { m_cacheWidth >>= 1; ++bits; }
+    m_cacheWidth = 2;
+    while (bits) { m_cacheWidth <<= 1; --bits; }
+    
+#ifdef DEBUG_FFT_SERVER
+    std::cerr << "Width " << m_width << ", cache width " << m_cacheWidth << " (size " << m_cacheWidth * columnSize << ")" << std::endl;
+#endif
+
+    for (size_t i = 0; i <= m_width / m_cacheWidth; ++i) {
+        m_caches.push_back(0);
+    }
+
+    m_fftInput = (fftsample *)
+        fftwf_malloc(fftSize * sizeof(fftsample));
+
+    m_fftOutput = (fftwf_complex *)
+        fftwf_malloc(fftSize * sizeof(fftwf_complex));
+
+    m_workbuffer = (float *)
+        fftwf_malloc(fftSize * sizeof(float));
+
+    m_fftPlan = fftwf_plan_dft_r2c_1d(m_fftSize,
+                                      m_fftInput,
+                                      m_fftOutput,
+                                      FFTW_ESTIMATE);
+
+    if (!m_fftPlan) {
+        std::cerr << "ERROR: fftwf_plan_dft_r2c_1d(" << m_windowSize << ") failed!" << std::endl;
+        throw(0);
+    }
+
+    m_fillThread = new FillThread(*this, fillFromColumn);
+
+    //!!! respond appropriately when thread exits (deleteProcessingData etc)
+}
+
+FFTDataServer::~FFTDataServer()
+{
+#ifdef DEBUG_FFT_SERVER
+    std::cerr << "FFTDataServer(" << this << ")::~FFTDataServer()" << std::endl;
+#endif
+
+    m_exiting = true;
+    m_condition.wakeAll();
+    if (m_fillThread) {
+        m_fillThread->wait();
+        delete m_fillThread;
+    }
+
+    QMutexLocker locker(&m_writeMutex);
+
+    for (CacheVector::iterator i = m_caches.begin(); i != m_caches.end(); ++i) {
+        delete *i;
+    }
+
+    deleteProcessingData();
+}
+
+void
+FFTDataServer::deleteProcessingData()
+{
+    if (m_fftInput) {
+        fftwf_destroy_plan(m_fftPlan);
+        fftwf_free(m_fftInput);
+        fftwf_free(m_fftOutput);
+        fftwf_free(m_workbuffer);
+    }
+    m_fftInput = 0;
+}
+
+void
+FFTDataServer::suspend()
+{
+#ifdef DEBUG_FFT_SERVER
+    std::cerr << "FFTDataServer(" << this << "): suspend" << std::endl;
+#endif
+    QMutexLocker locker(&m_writeMutex);
+    m_suspended = true;
+    for (CacheVector::iterator i = m_caches.begin(); i != m_caches.end(); ++i) {
+        if (*i) (*i)->suspend();
+    }
+}
+
+void
+FFTDataServer::resume()
+{
+    m_suspended = false;
+    m_condition.wakeAll();
+}
+
+FFTCache *
+FFTDataServer::getCacheAux(size_t c)
+{
+    QMutexLocker locker(&m_writeMutex);
+
+    if (m_lastUsedCache == -1) {
+        m_fillThread->start();
+    }
+
+    if (int(c) != m_lastUsedCache) {
+
+//        std::cerr << "switch from " << m_lastUsedCache << " to " << c << std::endl;
+
+        for (IntQueue::iterator i = m_dormantCaches.begin();
+             i != m_dormantCaches.end(); ++i) {
+            if (*i == c) {
+                m_dormantCaches.erase(i);
+                break;
+            }
+        }
+
+        if (m_lastUsedCache >= 0) {
+            bool inDormant = false;
+            for (size_t i = 0; i < m_dormantCaches.size(); ++i) {
+                if (m_dormantCaches[i] == m_lastUsedCache) {
+                    inDormant = true;
+                    break;
+                }
+            }
+            if (!inDormant) {
+                m_dormantCaches.push_back(m_lastUsedCache);
+            }
+            while (m_dormantCaches.size() > 4) {
+                int dc = m_dormantCaches.front();
+                m_dormantCaches.pop_front();
+                m_caches[dc]->suspend();
+            }
+        }
+    }
+
+    if (m_caches[c]) {
+        m_lastUsedCache = c;
+        return m_caches[c];
+    }
+
+    QString name = QString("%1-%2").arg(m_fileBaseName).arg(c);
+
+    FFTCache *cache = new FFTFileCache(name, MatrixFile::ReadWrite,
+                                       m_polar ? FFTFileCache::Polar :
+                                                 FFTFileCache::Rectangular);
+
+    size_t width = m_cacheWidth;
+    if (c * m_cacheWidth + width > m_width) {
+        width = m_width - c * m_cacheWidth;
+    }
+
+    cache->resize(width, m_height);
+    cache->reset();
+
+    m_caches[c] = cache;
+    m_lastUsedCache = c;
+
+    return cache;
+}
+
+float
+FFTDataServer::getMagnitudeAt(size_t x, size_t y)
+{
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    if (!cache->haveSetColumnAt(col)) {
+        fillColumn(x);
+    }
+    return cache->getMagnitudeAt(col, y);
+}
+
+float
+FFTDataServer::getNormalizedMagnitudeAt(size_t x, size_t y)
+{
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    if (!cache->haveSetColumnAt(col)) {
+        fillColumn(x);
+    }
+    return cache->getNormalizedMagnitudeAt(col, y);
+}
+
+float
+FFTDataServer::getMaximumMagnitudeAt(size_t x)
+{
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    if (!cache->haveSetColumnAt(col)) {
+        fillColumn(x);
+    }
+    return cache->getMaximumMagnitudeAt(col);
+}
+
+float
+FFTDataServer::getPhaseAt(size_t x, size_t y)
+{
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    if (!cache->haveSetColumnAt(col)) {
+        fillColumn(x);
+    }
+    return cache->getPhaseAt(col, y);
+}
+
+void
+FFTDataServer::getValuesAt(size_t x, size_t y, float &real, float &imaginary)
+{
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    if (!cache->haveSetColumnAt(col)) {
+#ifdef DEBUG_FFT_SERVER
+        std::cerr << "FFTDataServer::getValuesAt(" << x << ", " << y << "): filling" << std::endl;
+#endif
+        fillColumn(x);
+    }        
+    float magnitude = cache->getMagnitudeAt(col, y);
+    float phase = cache->getPhaseAt(col, y);
+    real = magnitude * cosf(phase);
+    imaginary = magnitude * sinf(phase);
+}
+
+bool
+FFTDataServer::isColumnReady(size_t x)
+{
+    if (!haveCache(x)) {
+        if (m_lastUsedCache == -1) {
+            m_fillThread->start();
+        }
+        return false;
+    }
+
+    size_t col;
+    FFTCache *cache = getCache(x, col);
+
+    return cache->haveSetColumnAt(col);
+}    
+
+void
+FFTDataServer::fillColumn(size_t x)
+{
+    size_t col;
+#ifdef DEBUG_FFT_SERVER_FILL
+    std::cout << "FFTDataServer::fillColumn(" << x << ")" << std::endl;
+#endif
+    FFTCache *cache = getCache(x, col);
+
+    QMutexLocker locker(&m_writeMutex);
+
+    if (cache->haveSetColumnAt(col)) return;
+
+    int startFrame = m_windowIncrement * x;
+    int endFrame = startFrame + m_windowSize;
+
+    startFrame -= int(m_windowSize - m_windowIncrement) / 2;
+    endFrame   -= int(m_windowSize - m_windowIncrement) / 2;
+    size_t pfx = 0;
+
+    size_t off = (m_fftSize - m_windowSize) / 2;
+
+    for (size_t i = 0; i < off; ++i) {
+        m_fftInput[i] = 0.0;
+        m_fftInput[m_fftSize - i - 1] = 0.0;
+    }
+
+    if (startFrame < 0) {
+	pfx = size_t(-startFrame);
+	for (size_t i = 0; i < pfx; ++i) {
+	    m_fftInput[off + i] = 0.0;
+	}
+    }
+
+    size_t got = m_model->getValues(m_channel, startFrame + pfx,
+				    endFrame, m_fftInput + off + pfx);
+
+    while (got + pfx < m_windowSize) {
+	m_fftInput[off + got + pfx] = 0.0;
+	++got;
+    }
+
+    if (m_channel == -1) {
+	int channels = m_model->getChannelCount();
+	if (channels > 1) {
+	    for (size_t i = 0; i < m_windowSize; ++i) {
+		m_fftInput[off + i] /= channels;
+	    }
+	}
+    }
+
+    m_windower.cut(m_fftInput + off);
+
+    for (size_t i = 0; i < m_fftSize/2; ++i) {
+	fftsample temp = m_fftInput[i];
+	m_fftInput[i] = m_fftInput[i + m_fftSize/2];
+	m_fftInput[i + m_fftSize/2] = temp;
+    }
+
+    fftwf_execute(m_fftPlan);
+
+    fftsample factor = 0.0;
+
+    for (size_t i = 0; i < m_fftSize/2; ++i) {
+
+	fftsample mag = sqrtf(m_fftOutput[i][0] * m_fftOutput[i][0] +
+                              m_fftOutput[i][1] * m_fftOutput[i][1]);
+	mag /= m_windowSize / 2;
+
+	if (mag > factor) factor = mag;
+
+	fftsample phase = atan2f(m_fftOutput[i][1], m_fftOutput[i][0]);
+	phase = princargf(phase);
+
+        m_workbuffer[i] = mag;
+        m_workbuffer[i + m_fftSize/2] = phase;
+    }
+
+    cache->setColumnAt(col,
+                       m_workbuffer,
+                       m_workbuffer + m_fftSize/2,
+                       factor);
+}    
+
+size_t
+FFTDataServer::getFillCompletion() const 
+{
+    if (m_fillThread) return m_fillThread->getCompletion();
+    else return 100;
+}
+
+size_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,
+                                    size_t windowSize,
+                                    size_t windowIncrement,
+                                    size_t fftSize,
+                                    bool polar)
+{
+    char buffer[200];
+
+    sprintf(buffer, "%u-%u-%u-%u-%u-%u%s",
+            (unsigned int)XmlExportable::getObjectExportId(model),
+            (unsigned int)(channel + 1),
+            (unsigned int)windowType,
+            (unsigned int)windowSize,
+            (unsigned int)windowIncrement,
+            (unsigned int)fftSize,
+            polar ? "-p" : "-r");
+
+    return buffer;
+}
+
+void
+FFTDataServer::FillThread::run()
+{
+    m_extent = 0;
+    m_completion = 0;
+    
+    size_t start = m_server.m_model->getStartFrame();
+    size_t end = m_server.m_model->getEndFrame();
+    size_t remainingEnd = end;
+
+    int counter = 0;
+    int updateAt = (end / m_server.m_windowIncrement) / 20;
+    if (updateAt < 100) updateAt = 100;
+
+    if (m_fillFrom > start) {
+
+        for (size_t f = m_fillFrom; f < end; f += m_server.m_windowIncrement) {
+	    
+            m_server.fillColumn(int((f - start) / m_server.m_windowIncrement));
+
+            if (m_server.m_exiting) return;
+
+            while (m_server.m_suspended) {
+#ifdef DEBUG_FFT_SERVER
+                std::cerr << "FFTDataServer(" << this << "): suspended, waiting..." << std::endl;
+#endif
+                m_server.m_writeMutex.lock();
+                m_server.m_condition.wait(&m_server.m_writeMutex, 10000);
+                m_server.m_writeMutex.unlock();
+                if (m_server.m_exiting) return;
+            }
+
+            if (++counter == updateAt) {
+                m_extent = f;
+                m_completion = size_t(100 * fabsf(float(f - m_fillFrom) /
+                                                  float(end - start)));
+                counter = 0;
+            }
+        }
+
+        remainingEnd = m_fillFrom;
+        if (remainingEnd > start) --remainingEnd;
+        else remainingEnd = start;
+    }
+
+    size_t baseCompletion = m_completion;
+
+    for (size_t f = start; f < remainingEnd; f += m_server.m_windowIncrement) {
+
+        m_server.fillColumn(int((f - start) / m_server.m_windowIncrement));
+
+        if (m_server.m_exiting) return;
+
+        while (m_server.m_suspended) {
+#ifdef DEBUG_FFT_SERVER
+            std::cerr << "FFTDataServer(" << this << "): suspended, waiting..." << std::endl;
+#endif
+            m_server.m_writeMutex.lock();
+            m_server.m_condition.wait(&m_server.m_writeMutex, 10000);
+            m_server.m_writeMutex.unlock();
+            if (m_server.m_exiting) return;
+        }
+		    
+        if (++counter == updateAt) {
+            m_extent = f;
+            m_completion = baseCompletion +
+                size_t(100 * fabsf(float(f - start) /
+                                   float(end - start)));
+            counter = 0;
+        }
+    }
+
+    m_completion = 100;
+    m_extent = end;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/FFTDataServer.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,201 @@
+/* -*- 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_DATA_SERVER_H_
+#define _FFT_DATA_SERVER_H_
+
+#include "base/Window.h"
+#include "base/Thread.h"
+
+#include <fftw3.h>
+
+#include <QMutex>
+#include <QWaitCondition>
+#include <QString>
+
+#include <vector>
+#include <deque>
+
+class DenseTimeValueModel;
+class FFTCache;
+
+class FFTDataServer
+{
+public:
+    static FFTDataServer *getInstance(const DenseTimeValueModel *model,
+                                      int channel,
+                                      WindowType windowType,
+                                      size_t windowSize,
+                                      size_t windowIncrement,
+                                      size_t fftSize,
+                                      bool polar,
+                                      size_t fillFromColumn = 0);
+
+    static FFTDataServer *getFuzzyInstance(const DenseTimeValueModel *model,
+                                           int channel,
+                                           WindowType windowType,
+                                           size_t windowSize,
+                                           size_t windowIncrement,
+                                           size_t fftSize,
+                                           bool polar,
+                                           size_t fillFromColumn = 0);
+
+    static void releaseInstance(FFTDataServer *);
+
+    const DenseTimeValueModel *getModel() const { return m_model; }
+    int        getChannel() const { return m_channel; }
+    WindowType getWindowType() const { return m_windower.getType(); }
+    size_t     getWindowSize() const { return m_windowSize; }
+    size_t     getWindowIncrement() const { return m_windowIncrement; }
+    size_t     getFFTSize() const { return m_fftSize; }
+    bool       getPolar() const { return m_polar; }
+
+    size_t     getWidth() const  { return m_width;  }
+    size_t     getHeight() const { return m_height; }
+
+    float      getMagnitudeAt(size_t x, size_t y);
+    float      getNormalizedMagnitudeAt(size_t x, size_t y);
+    float      getMaximumMagnitudeAt(size_t x);
+    float      getPhaseAt(size_t x, size_t y);
+    void       getValuesAt(size_t x, size_t y, float &real, float &imaginary);
+    bool       isColumnReady(size_t x);
+
+    void       suspend();
+
+    // Convenience functions:
+
+    bool isLocalPeak(size_t x, size_t 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(size_t x, size_t y, float threshold) {
+        return getMagnitudeAt(x, y) > threshold;
+    }
+
+    size_t getFillCompletion() const;
+    size_t getFillExtent() const;
+
+private:
+    FFTDataServer(QString fileBaseName,
+                  const DenseTimeValueModel *model,
+                  int channel,
+                  WindowType windowType,
+                  size_t windowSize,
+                  size_t windowIncrement,
+                  size_t fftSize,
+                  bool polar,
+                  size_t fillFromColumn = 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;
+
+    size_t m_windowSize;
+    size_t m_windowIncrement;
+    size_t m_fftSize;
+    bool m_polar;
+
+    size_t m_width;
+    size_t m_height;
+    size_t m_cacheWidth;
+
+    typedef std::vector<FFTCache *> CacheVector;
+    CacheVector m_caches;
+    
+    typedef std::deque<int> IntQueue;
+    IntQueue m_dormantCaches;
+
+    int m_lastUsedCache;
+    FFTCache *getCache(size_t x, size_t &col) {
+        if (m_suspended) resume();
+        col   = x % m_cacheWidth;
+        int c = x / m_cacheWidth;
+        // The only use of m_lastUsedCache without a lock is to
+        // establish whether a cache has been created at all (they're
+        // created on demand, but not destroyed until the server is).
+        if (c == m_lastUsedCache) return m_caches[c];
+        else return getCacheAux(c);
+    }
+    bool haveCache(size_t x) {
+        int c = x / m_cacheWidth;
+        if (c == m_lastUsedCache) return true;
+        else return (m_caches[c] != 0);
+    }
+        
+    FFTCache *getCacheAux(size_t c);
+    QMutex m_writeMutex;
+    QWaitCondition m_condition;
+
+    fftsample *m_fftInput;
+    fftwf_complex *m_fftOutput;
+    float *m_workbuffer;
+    fftwf_plan m_fftPlan;
+
+    class FillThread : public Thread
+    {
+    public:
+        FillThread(FFTDataServer &server, size_t fillFromColumn) :
+            m_server(server), m_extent(0), m_completion(0),
+            m_fillFrom(fillFromColumn) { }
+
+        size_t getExtent() const { return m_extent; }
+        size_t getCompletion() const { return m_completion ? m_completion : 1; }
+        virtual void run();
+
+    protected:
+        FFTDataServer &m_server;
+        size_t m_extent;
+        size_t m_completion;
+        size_t m_fillFrom;
+    };
+
+    bool m_exiting;
+    bool m_suspended;
+    FillThread *m_fillThread;
+
+    void deleteProcessingData();
+    void fillColumn(size_t x);
+    void resume();
+
+    QString generateFileBasename() const;
+    static QString generateFileBasename(const DenseTimeValueModel *model,
+                                        int channel,
+                                        WindowType windowType,
+                                        size_t windowSize,
+                                        size_t windowIncrement,
+                                        size_t fftSize,
+                                        bool polar);
+
+    typedef std::pair<FFTDataServer *, int> ServerCountPair;
+    typedef std::map<QString, ServerCountPair> ServerMap;
+
+    static ServerMap m_servers;
+    static QMutex m_serverMapMutex;
+    static FFTDataServer *findServer(QString); // call with serverMapMutex held
+    static void purgeLimbo(int maxSize = 3); // call with serverMapMutex held
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/FFTFileCache.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,288 @@
+/* -*- 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 "FFTFileCache.h"
+
+#include "MatrixFile.h"
+
+#include "base/Profiler.h"
+
+#include <iostream>
+
+#include <QMutexLocker>
+
+// 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].
+
+FFTFileCache::FFTFileCache(QString fileBase, MatrixFile::Mode mode,
+                           StorageType storageType) :
+    m_writebuf(0),
+    m_readbuf(0),
+    m_readbufCol(0),
+    m_readbufWidth(0),
+    m_mfc(new MatrixFile
+          (fileBase, mode, 
+           storageType == Compact ? sizeof(uint16_t) : sizeof(float),
+           mode == MatrixFile::ReadOnly)),
+    m_storageType(storageType)
+{
+    std::cerr << "FFTFileCache: storage type is " << (storageType == Compact ? "Compact" : storageType == Polar ? "Polar" : "Rectangular") << std::endl;
+}
+
+FFTFileCache::~FFTFileCache()
+{
+    if (m_readbuf) delete[] m_readbuf;
+    if (m_writebuf) delete[] m_writebuf;
+    delete m_mfc;
+}
+
+size_t
+FFTFileCache::getWidth() const
+{
+    return m_mfc->getWidth();
+}
+
+size_t
+FFTFileCache::getHeight() const
+{
+    size_t mh = m_mfc->getHeight();
+    if (mh > 0) return (mh - 1) / 2;
+    else return 0;
+}
+
+void
+FFTFileCache::resize(size_t width, size_t height)
+{
+    QMutexLocker locker(&m_writeMutex);
+
+    m_mfc->resize(width, height * 2 + 1);
+    if (m_readbuf) {
+        delete[] m_readbuf;
+        m_readbuf = 0;
+    }
+    if (m_writebuf) {
+        delete[] m_writebuf;
+    }
+    m_writebuf = new char[(height * 2 + 1) * m_mfc->getCellSize()];
+}
+
+void
+FFTFileCache::reset()
+{
+    m_mfc->reset();
+}
+
+float
+FFTFileCache::getMagnitudeAt(size_t x, size_t y) const
+{
+    float value = 0.f;
+
+    switch (m_storageType) {
+
+    case Compact:
+        value = (getFromReadBufCompactUnsigned(x, y * 2) / 65535.0)
+            * getNormalizationFactor(x);
+        break;
+
+    case Rectangular:
+    {
+        float real, imag;
+        getValuesAt(x, y, real, imag);
+        value = sqrtf(real * real + imag * imag);
+        break;
+    }
+
+    case Polar:
+        value = getFromReadBufStandard(x, y * 2);
+        break;
+    }
+
+    return value;
+}
+
+float
+FFTFileCache::getNormalizedMagnitudeAt(size_t x, size_t y) const
+{
+    float value = 0.f;
+
+    switch (m_storageType) {
+
+    case Compact:
+        value = getFromReadBufCompactUnsigned(x, y * 2) / 65535.0;
+        break;
+
+    default:
+    {
+        float mag = getMagnitudeAt(x, y);
+        float factor = getNormalizationFactor(x);
+        if (factor != 0) value = mag / factor;
+        else value = 0.f;
+        break;
+    }
+    }
+
+    return value;
+}
+
+float
+FFTFileCache::getMaximumMagnitudeAt(size_t x) const
+{
+    return getNormalizationFactor(x);
+}
+
+float
+FFTFileCache::getPhaseAt(size_t x, size_t y) const
+{
+    float value = 0.f;
+    
+    switch (m_storageType) {
+
+    case Compact:
+        value = (getFromReadBufCompactSigned(x, y * 2 + 1) / 32767.0) * M_PI;
+        break;
+
+    case Rectangular:
+    {
+        float real, imag;
+        getValuesAt(x, y, real, imag);
+        value = princargf(atan2f(imag, real));
+        break;
+    }
+
+    case Polar:
+        value = getFromReadBufStandard(x, y * 2 + 1);
+        break;
+    }
+
+    return value;
+}
+
+void
+FFTFileCache::getValuesAt(size_t x, size_t y, float &real, float &imag) const
+{
+    switch (m_storageType) {
+
+    case Rectangular:
+        real = getFromReadBufStandard(x, y * 2);
+        imag = getFromReadBufStandard(x, y * 2 + 1);
+        return;
+
+    default:
+        float mag = getMagnitudeAt(x, y);
+        float phase = getPhaseAt(x, y);
+        real = mag * cosf(phase);
+        imag = mag * sinf(phase);
+        return;
+    }
+}
+
+bool
+FFTFileCache::haveSetColumnAt(size_t x) const
+{
+    return m_mfc->haveSetColumnAt(x);
+}
+
+void
+FFTFileCache::setColumnAt(size_t x, float *mags, float *phases, float factor)
+{
+    QMutexLocker locker(&m_writeMutex);
+
+    size_t h = getHeight();
+
+    switch (m_storageType) {
+
+    case Compact:
+        for (size_t 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 Rectangular:
+        for (size_t 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 Polar:
+        for (size_t 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;
+//    std::cerr << "Normalization factor: " << factor << ", max " << maxFactor << " (height " << getHeight() << ")" << std::endl;
+
+    if (m_storageType == Compact) {
+        ((uint16_t *)m_writebuf)[h * 2] = factor * 65535.0;
+    } else {
+        ((float *)m_writebuf)[h * 2] = factor;
+    }
+    m_mfc->setColumnAt(x, m_writebuf);
+}
+
+void
+FFTFileCache::setColumnAt(size_t x, float *real, float *imag)
+{
+    QMutexLocker locker(&m_writeMutex);
+
+    size_t h = getHeight();
+
+    float max = 0.0f;
+
+    switch (m_storageType) {
+
+    case Compact:
+        for (size_t y = 0; y < h; ++y) {
+            float mag = sqrtf(real[y] * real[y] + imag[y] * imag[y]);
+            if (mag > max) max = mag;
+        }
+        for (size_t y = 0; y < h; ++y) {
+            float mag = sqrtf(real[y] * real[y] + imag[y] * imag[y]);
+            float phase = princargf(atan2f(imag[y], real[y]));
+            ((uint16_t *)m_writebuf)[y * 2] = uint16_t((mag / max) * 65535.0);
+            ((uint16_t *)m_writebuf)[y * 2 + 1] = uint16_t(int16_t((phase * 32767) / M_PI));
+        }
+        break;
+
+    case Rectangular:
+        for (size_t 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 > max) max = mag;
+        }
+        break;
+
+    case Polar:
+        for (size_t y = 0; y < h; ++y) {
+            float mag = sqrtf(real[y] * real[y] + imag[y] * imag[y]);
+            if (mag > max) max = mag;
+            ((float *)m_writebuf)[y * 2] = mag;
+            ((float *)m_writebuf)[y * 2 + 1] = princargf(atan2f(imag[y], real[y]));
+        }
+        break;
+    }
+
+    ((float *)m_writebuf)[h * 2] = max;
+    m_mfc->setColumnAt(x, m_writebuf);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/FFTFileCache.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,125 @@
+/* -*- 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_FILE_CACHE_H_
+#define _FFT_FILE_CACHE_H_
+
+#include "FFTCache.h"
+#include "MatrixFile.h"
+
+#include <QMutex>
+
+class FFTFileCache : public FFTCache
+{
+public:
+    enum StorageType {
+        Compact, // 16 bits normalized polar
+        Rectangular, // floating point real+imag
+        Polar, // floating point mag+phase
+    };
+
+    FFTFileCache(QString fileBase, MatrixFile::Mode mode,
+                 StorageType storageType);
+    virtual ~FFTFileCache();
+
+    MatrixFile::Mode getMode() const { return m_mfc->getMode(); }
+
+    virtual size_t getWidth() const;
+    virtual size_t getHeight() const;
+	
+    virtual void resize(size_t width, size_t height);
+    virtual void reset(); // zero-fill or 1-fill as appropriate without changing size
+	
+    virtual float getMagnitudeAt(size_t x, size_t y) const;
+    virtual float getNormalizedMagnitudeAt(size_t x, size_t y) const;
+    virtual float getMaximumMagnitudeAt(size_t x) const;
+    virtual float getPhaseAt(size_t x, size_t y) const;
+
+    virtual void getValuesAt(size_t x, size_t y, float &real, float &imag) const;
+
+    virtual bool haveSetColumnAt(size_t x) const;
+
+    virtual void setColumnAt(size_t x, float *mags, float *phases, float factor);
+    virtual void setColumnAt(size_t x, float *reals, float *imags);
+
+    virtual void suspend() { m_mfc->suspend(); }
+
+protected:
+    char *m_writebuf;
+    mutable char *m_readbuf;
+    mutable size_t m_readbufCol;
+    mutable size_t m_readbufWidth;
+
+    float getFromReadBufStandard(size_t x, size_t y) const {
+        if (m_readbuf &&
+            (m_readbufCol == x || (m_readbufWidth > 1 && m_readbufCol+1 == x))) {
+            return ((float *)m_readbuf)[(x - m_readbufCol) * m_mfc->getHeight() + y];
+        } else {
+            populateReadBuf(x);
+            return getFromReadBufStandard(x, y);
+        }
+    }
+
+    float getFromReadBufCompactUnsigned(size_t x, size_t y) const {
+        if (m_readbuf &&
+            (m_readbufCol == x || (m_readbufWidth > 1 && m_readbufCol+1 == x))) {
+            return ((uint16_t *)m_readbuf)[(x - m_readbufCol) * m_mfc->getHeight() + y];
+        } else {
+            populateReadBuf(x);
+            return getFromReadBufCompactUnsigned(x, y);
+        }
+    }
+
+    float getFromReadBufCompactSigned(size_t x, size_t y) const {
+        if (m_readbuf &&
+            (m_readbufCol == x || (m_readbufWidth > 1 && m_readbufCol+1 == x))) {
+            return ((int16_t *)m_readbuf)[(x - m_readbufCol) * m_mfc->getHeight() + y];
+        } else {
+            populateReadBuf(x);
+            return getFromReadBufCompactSigned(x, y);
+        }
+    }
+
+    void populateReadBuf(size_t x) const {
+        if (!m_readbuf) {
+            m_readbuf = new char[m_mfc->getHeight() * 2 * m_mfc->getCellSize()];
+        }
+        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_readbufCol = x;
+    }
+
+    float getNormalizationFactor(size_t col) const {
+        if (m_storageType != Compact) {
+            return getFromReadBufStandard(col, m_mfc->getHeight() - 1);
+        } else {
+            float factor;
+            factor = getFromReadBufCompactUnsigned(col, m_mfc->getHeight() - 1);
+            return factor / 65535.0;
+        }
+    }
+
+    MatrixFile *m_mfc;
+    QMutex m_writeMutex;
+    StorageType m_storageType;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/FFTFuzzyAdapter.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,72 @@
+/* -*- 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 "FFTFuzzyAdapter.h"
+
+#include <cassert>
+
+FFTFuzzyAdapter::FFTFuzzyAdapter(const DenseTimeValueModel *model,
+				 int channel,
+				 WindowType windowType,
+				 size_t windowSize,
+				 size_t windowIncrement,
+				 size_t fftSize,
+				 bool polar,
+				 size_t fillFromColumn) :
+    m_server(0),
+    m_xshift(0),
+    m_yshift(0)
+{
+    m_server = FFTDataServer::getFuzzyInstance(model,
+                                               channel,
+                                               windowType,
+                                               windowSize,
+                                               windowIncrement,
+                                               fftSize,
+                                               polar,
+                                               fillFromColumn);
+
+    size_t xratio = windowIncrement / m_server->getWindowIncrement();
+    size_t yratio = m_server->getFFTSize() / fftSize;
+
+    while (xratio > 1) {
+        if (xratio & 0x1) {
+            std::cerr << "ERROR: FFTFuzzyAdapter: Window increment ratio "
+                      << windowIncrement << " / "
+                      << m_server->getWindowIncrement()
+                      << " must be a power of two" << std::endl;
+            assert(!(xratio & 0x1));
+        }
+        ++m_xshift;
+        xratio >>= 1;
+    }
+
+    while (yratio > 1) {
+        if (yratio & 0x1) {
+            std::cerr << "ERROR: FFTFuzzyAdapter: FFT size ratio "
+                      << m_server->getFFTSize() << " / " << fftSize
+                      << " must be a power of two" << std::endl;
+            assert(!(yratio & 0x1));
+        }
+        ++m_yshift;
+        yratio >>= 1;
+    }
+}
+
+FFTFuzzyAdapter::~FFTFuzzyAdapter()
+{
+    FFTDataServer::releaseInstance(m_server);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/FFTFuzzyAdapter.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,80 @@
+/* -*- 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_FUZZY_ADAPTER_H_
+#define _FFT_FUZZY_ADAPTER_H_
+
+#include "FFTDataServer.h"
+
+class FFTFuzzyAdapter
+{
+public:
+    FFTFuzzyAdapter(const DenseTimeValueModel *model,
+                    int channel,
+                    WindowType windowType,
+                    size_t windowSize,
+                    size_t windowIncrement,
+                    size_t fftSize,
+                    bool polar,
+                    size_t fillFromColumn = 0);
+    ~FFTFuzzyAdapter();
+
+    size_t getWidth() const {
+        return m_server->getWidth() >> m_xshift;
+    }
+    size_t getHeight() const {
+        return m_server->getHeight() >> m_yshift;
+    }
+    float getMagnitudeAt(size_t x, size_t y) {
+        return m_server->getMagnitudeAt(x << m_xshift, y << m_yshift);
+    }
+    float getNormalizedMagnitudeAt(size_t x, size_t y) {
+        return m_server->getNormalizedMagnitudeAt(x << m_xshift, y << m_yshift);
+    }
+    float getMaximumMagnitudeAt(size_t x) {
+        return m_server->getMaximumMagnitudeAt(x << m_xshift);
+    }
+    float getPhaseAt(size_t x, size_t y) {
+        return m_server->getPhaseAt(x << m_xshift, y << m_yshift);
+    }
+    void getValuesAt(size_t x, size_t y, float &real, float &imaginary) {
+        m_server->getValuesAt(x << m_xshift, y << m_yshift, real, imaginary);
+    }
+    bool isColumnReady(size_t x) {
+        return m_server->isColumnReady(x << m_xshift);
+    }
+    bool isLocalPeak(size_t x, size_t 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(size_t x, size_t y, float threshold) {
+        return getMagnitudeAt(x, y) > threshold;
+    }
+
+    size_t getFillCompletion() const { return m_server->getFillCompletion(); }
+    size_t getFillExtent() const { return m_server->getFillExtent(); }
+
+private:
+    FFTFuzzyAdapter(const FFTFuzzyAdapter &); // not implemented
+    FFTFuzzyAdapter &operator=(const FFTFuzzyAdapter &); // not implemented
+
+    FFTDataServer *m_server;
+    int m_xshift;
+    int m_yshift;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/FileReadThread.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,299 @@
+/* -*- 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 "FileReadThread.h"
+
+#include "base/Profiler.h"
+
+#include <iostream>
+#include <unistd.h>
+
+//#define DEBUG_FILE_READ_THREAD 1
+
+FileReadThread::FileReadThread() :
+    m_nextToken(0),
+    m_exiting(false)
+{
+}
+
+void
+FileReadThread::run()
+{
+    m_mutex.lock();
+
+    while (!m_exiting) {
+        if (m_queue.empty()) {
+            m_condition.wait(&m_mutex, 1000);
+        } else {
+            process();
+        }
+        notifyCancelled();
+    }
+
+    notifyCancelled();
+    m_mutex.unlock();
+
+#ifdef DEBUG_FILE_READ_THREAD
+    std::cerr << "FileReadThread::run() exiting" << std::endl;
+#endif
+}
+
+void
+FileReadThread::finish()
+{
+#ifdef DEBUG_FILE_READ_THREAD
+    std::cerr << "FileReadThread::finish()" << std::endl;
+#endif
+
+    m_mutex.lock();
+    while (!m_queue.empty()) {
+        m_cancelledRequests[m_queue.begin()->first] = m_queue.begin()->second;
+        m_newlyCancelled.insert(m_queue.begin()->first);
+        m_queue.erase(m_queue.begin());
+    }
+
+    m_exiting = true;
+    m_mutex.unlock();
+
+    m_condition.wakeAll();
+
+#ifdef DEBUG_FILE_READ_THREAD
+    std::cerr << "FileReadThread::finish() exiting" << std::endl;
+#endif
+}
+
+int
+FileReadThread::request(const Request &request)
+{
+    m_mutex.lock();
+    
+    int token = m_nextToken++;
+    m_queue[token] = request;
+
+    m_mutex.unlock();
+    m_condition.wakeAll();
+
+    return token;
+}
+
+void
+FileReadThread::cancel(int token)
+{
+    m_mutex.lock();
+
+    if (m_queue.find(token) != m_queue.end()) {
+        m_cancelledRequests[token] = m_queue[token];
+        m_queue.erase(token);
+        m_newlyCancelled.insert(token);
+    } else if (m_readyRequests.find(token) != m_readyRequests.end()) {
+        m_cancelledRequests[token] = m_readyRequests[token];
+        m_readyRequests.erase(token);
+    } else {
+        std::cerr << "WARNING: FileReadThread::cancel: token " << token << " not found" << std::endl;
+    }
+
+    m_mutex.unlock();
+
+#ifdef DEBUG_FILE_READ_THREAD
+    std::cerr << "FileReadThread::cancel(" << token << ") waking condition" << std::endl;
+#endif
+
+    m_condition.wakeAll();
+}
+
+bool
+FileReadThread::isReady(int token)
+{
+    m_mutex.lock();
+
+    bool ready = m_readyRequests.find(token) != m_readyRequests.end();
+
+    m_mutex.unlock();
+    return ready;
+}
+
+bool
+FileReadThread::isCancelled(int token)
+{
+    m_mutex.lock();
+
+    bool cancelled = 
+        m_cancelledRequests.find(token) != m_cancelledRequests.end() &&
+        m_newlyCancelled.find(token) == m_newlyCancelled.end();
+
+    m_mutex.unlock();
+    return cancelled;
+}
+
+bool
+FileReadThread::getRequest(int token, Request &request)
+{
+    m_mutex.lock();
+
+    bool found = false;
+
+    if (m_queue.find(token) != m_queue.end()) {
+        request = m_queue[token];
+        found = true;
+    } else if (m_cancelledRequests.find(token) != m_cancelledRequests.end()) {
+        request = m_cancelledRequests[token];
+        found = true;
+    } else if (m_readyRequests.find(token) != m_readyRequests.end()) {
+        request = m_readyRequests[token];
+        found = true;
+    }
+
+    m_mutex.unlock();
+    
+    return found;
+}
+
+void
+FileReadThread::done(int token)
+{
+    m_mutex.lock();
+
+    bool found = false;
+
+    if (m_cancelledRequests.find(token) != m_cancelledRequests.end()) {
+        m_cancelledRequests.erase(token);
+        m_newlyCancelled.erase(token);
+        found = true;
+    } else if (m_readyRequests.find(token) != m_readyRequests.end()) {
+        m_readyRequests.erase(token);
+        found = true;
+    } else if (m_queue.find(token) != m_queue.end()) {
+        std::cerr << "WARNING: FileReadThread::done(" << token << "): request is still in queue (wait or cancel it)" << std::endl;
+    }
+
+    m_mutex.unlock();
+
+    if (!found) {
+        std::cerr << "WARNING: FileReadThread::done(" << token << "): request not found" << std::endl;
+    }
+}
+
+void
+FileReadThread::process()
+{
+    // entered with m_mutex locked and m_queue non-empty
+
+#ifdef DEBUG_FILE_READ_THREAD
+    Profiler profiler("FileReadThread::process()", true);
+#endif
+
+    int token = m_queue.begin()->first;
+    Request request = m_queue.begin()->second;
+
+    m_mutex.unlock();
+
+#ifdef DEBUG_FILE_READ_THREAD
+    std::cerr << "FileReadThread::process: reading " << request.start << ", " << request.size << " on " << request.fd << std::endl;
+#endif
+
+    bool successful = false;
+    bool seekFailed = false;
+    ssize_t r = 0;
+
+    if (request.mutex) request.mutex->lock();
+
+    if (::lseek(request.fd, request.start, SEEK_SET) == (off_t)-1) {
+        seekFailed = true;
+    } else {
+        
+        // if request.size is large, we want to avoid making a single
+        // system call to read it all as it may block too much
+
+        static const size_t blockSize = 256 * 1024;
+        
+        size_t size = request.size;
+        char *destination = request.data;
+
+        while (size > 0) {
+            size_t readSize = size;
+            if (readSize > blockSize) readSize = blockSize;
+            ssize_t br = ::read(request.fd, destination, readSize);
+            if (br < 0) { 
+                r = br;
+                break;
+            } else {
+                r += br;
+                if (br < ssize_t(readSize)) break;
+            }
+            destination += readSize;
+            size -= readSize;
+        }
+    }
+
+    if (request.mutex) request.mutex->unlock();
+
+    if (seekFailed) {
+        ::perror("Seek failed");
+        std::cerr << "ERROR: FileReadThread::process: seek to "
+                  << request.start << " failed" << std::endl;
+        request.size = 0;
+    } else {
+        if (r < 0) {
+            ::perror("ERROR: FileReadThread::process: Read failed");
+            request.size = 0;
+        } else if (r < ssize_t(request.size)) {
+            std::cerr << "WARNING: FileReadThread::process: read "
+                      << request.size << " returned only " << r << " bytes"
+                      << std::endl;
+            request.size = r;
+            usleep(100000);
+        } else {
+            successful = true;
+        }
+    }
+        
+    // Check that the token hasn't been cancelled and the thread
+    // hasn't been asked to finish
+    
+    m_mutex.lock();
+
+    request.successful = successful;
+        
+    if (m_queue.find(token) != m_queue.end() && !m_exiting) {
+        m_queue.erase(token);
+        m_readyRequests[token] = request;
+#ifdef DEBUG_FILE_READ_THREAD
+        std::cerr << "FileReadThread::process: done, marking as ready" << std::endl;
+#endif
+    } else {
+#ifdef DEBUG_FILE_READ_THREAD
+        std::cerr << "FileReadThread::process: request disappeared or exiting" << std::endl;
+#endif
+    }
+}
+
+void
+FileReadThread::notifyCancelled()
+{
+    // entered with m_mutex locked
+
+    while (!m_newlyCancelled.empty()) {
+
+        int token = *m_newlyCancelled.begin();
+
+#ifdef DEBUG_FILE_READ_THREAD
+        std::cerr << "FileReadThread::notifyCancelled: token " << token << std::endl;
+#endif
+
+        m_newlyCancelled.erase(token);
+    }
+}
+        
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/FileReadThread.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,73 @@
+/* -*- 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 _FILE_READ_THREAD_H_
+#define _FILE_READ_THREAD_H_
+
+#include "Thread.h"
+
+#include <QMutex>
+#include <QWaitCondition>
+
+#include <map>
+#include <set>
+
+#include <stdint.h>
+
+class FileReadThread : public Thread
+{
+    Q_OBJECT
+
+public:
+    FileReadThread();
+
+    virtual void run();
+    virtual void finish();
+
+    struct Request {
+        int fd;
+        QMutex *mutex; // used to synchronise access to fd; may be null
+        off_t start;
+        size_t size;
+        char *data; // caller is responsible for allocating and deallocating
+        bool successful; // set by FileReadThread after processing request
+    };
+    
+    virtual int request(const Request &request);
+    virtual void cancel(int token);
+
+    virtual bool isReady(int token);
+    virtual bool isCancelled(int token); // and safe to delete
+    virtual bool getRequest(int token, Request &request);
+    virtual void done(int token);
+    
+protected:
+    int m_nextToken;
+    bool m_exiting;
+    
+    typedef std::map<int, Request> RequestQueue;
+    RequestQueue m_queue;
+    RequestQueue m_cancelledRequests;
+    RequestQueue m_readyRequests;
+    std::set<int> m_newlyCancelled;
+
+    QMutex m_mutex;
+    QWaitCondition m_condition;
+
+    void process();
+    void notifyCancelled();
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/MIDIFileReader.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,1248 @@
+/* -*- 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 Richard Bown and Chris Cannam.
+*/
+
+
+#include <iostream>
+#include <fstream>
+#include <string>
+#include <cstdio>
+#include <algorithm>
+
+#include "MIDIFileReader.h"
+
+#include "base/Model.h"
+#include "base/Pitch.h"
+#include "base/RealTime.h"
+#include "model/NoteModel.h"
+
+#include <QString>
+#include <QMessageBox>
+#include <QInputDialog>
+
+#include <sstream>
+
+using std::string;
+using std::ifstream;
+using std::stringstream;
+using std::cerr;
+using std::endl;
+using std::ends;
+using std::ios;
+using std::vector;
+using std::map;
+using std::set;
+
+//#define MIDI_DEBUG 1
+
+static const char *const MIDI_FILE_HEADER         = "MThd";
+static const char *const MIDI_TRACK_HEADER        = "MTrk";
+
+static const MIDIFileReader::MIDIByte MIDI_STATUS_BYTE_MASK       = 0x80;
+static const MIDIFileReader::MIDIByte MIDI_MESSAGE_TYPE_MASK      = 0xF0;
+static const MIDIFileReader::MIDIByte MIDI_CHANNEL_NUM_MASK       = 0x0F;
+static const MIDIFileReader::MIDIByte MIDI_NOTE_OFF               = 0x80;
+static const MIDIFileReader::MIDIByte MIDI_NOTE_ON                = 0x90;
+static const MIDIFileReader::MIDIByte MIDI_POLY_AFTERTOUCH        = 0xA0;
+static const MIDIFileReader::MIDIByte MIDI_CTRL_CHANGE            = 0xB0;
+static const MIDIFileReader::MIDIByte MIDI_PROG_CHANGE            = 0xC0;
+static const MIDIFileReader::MIDIByte MIDI_CHNL_AFTERTOUCH        = 0xD0;
+static const MIDIFileReader::MIDIByte MIDI_PITCH_BEND             = 0xE0;
+static const MIDIFileReader::MIDIByte MIDI_SELECT_CHNL_MODE       = 0xB0;
+static const MIDIFileReader::MIDIByte MIDI_SYSTEM_EXCLUSIVE       = 0xF0;
+static const MIDIFileReader::MIDIByte MIDI_TC_QUARTER_FRAME       = 0xF1;
+static const MIDIFileReader::MIDIByte MIDI_SONG_POSITION_PTR      = 0xF2;
+static const MIDIFileReader::MIDIByte MIDI_SONG_SELECT            = 0xF3;
+static const MIDIFileReader::MIDIByte MIDI_TUNE_REQUEST           = 0xF6;
+static const MIDIFileReader::MIDIByte MIDI_END_OF_EXCLUSIVE       = 0xF7;
+static const MIDIFileReader::MIDIByte MIDI_TIMING_CLOCK           = 0xF8;
+static const MIDIFileReader::MIDIByte MIDI_START                  = 0xFA;
+static const MIDIFileReader::MIDIByte MIDI_CONTINUE               = 0xFB;
+static const MIDIFileReader::MIDIByte MIDI_STOP                   = 0xFC;
+static const MIDIFileReader::MIDIByte MIDI_ACTIVE_SENSING         = 0xFE;
+static const MIDIFileReader::MIDIByte MIDI_SYSTEM_RESET           = 0xFF;
+static const MIDIFileReader::MIDIByte MIDI_SYSEX_NONCOMMERCIAL    = 0x7D;
+static const MIDIFileReader::MIDIByte MIDI_SYSEX_NON_RT           = 0x7E;
+static const MIDIFileReader::MIDIByte MIDI_SYSEX_RT               = 0x7F;
+static const MIDIFileReader::MIDIByte MIDI_SYSEX_RT_COMMAND       = 0x06;
+static const MIDIFileReader::MIDIByte MIDI_SYSEX_RT_RESPONSE      = 0x07;
+static const MIDIFileReader::MIDIByte MIDI_MMC_STOP               = 0x01;
+static const MIDIFileReader::MIDIByte MIDI_MMC_PLAY               = 0x02;
+static const MIDIFileReader::MIDIByte MIDI_MMC_DEFERRED_PLAY      = 0x03;
+static const MIDIFileReader::MIDIByte MIDI_MMC_FAST_FORWARD       = 0x04;
+static const MIDIFileReader::MIDIByte MIDI_MMC_REWIND             = 0x05;
+static const MIDIFileReader::MIDIByte MIDI_MMC_RECORD_STROBE      = 0x06;
+static const MIDIFileReader::MIDIByte MIDI_MMC_RECORD_EXIT        = 0x07;
+static const MIDIFileReader::MIDIByte MIDI_MMC_RECORD_PAUSE       = 0x08;
+static const MIDIFileReader::MIDIByte MIDI_MMC_PAUSE              = 0x08;
+static const MIDIFileReader::MIDIByte MIDI_MMC_EJECT              = 0x0A;
+static const MIDIFileReader::MIDIByte MIDI_MMC_LOCATE             = 0x44;
+static const MIDIFileReader::MIDIByte MIDI_FILE_META_EVENT        = 0xFF;
+static const MIDIFileReader::MIDIByte MIDI_SEQUENCE_NUMBER        = 0x00;
+static const MIDIFileReader::MIDIByte MIDI_TEXT_EVENT             = 0x01;
+static const MIDIFileReader::MIDIByte MIDI_COPYRIGHT_NOTICE       = 0x02;
+static const MIDIFileReader::MIDIByte MIDI_TRACK_NAME             = 0x03;
+static const MIDIFileReader::MIDIByte MIDI_INSTRUMENT_NAME        = 0x04;
+static const MIDIFileReader::MIDIByte MIDI_LYRIC                  = 0x05;
+static const MIDIFileReader::MIDIByte MIDI_TEXT_MARKER            = 0x06;
+static const MIDIFileReader::MIDIByte MIDI_CUE_POINT              = 0x07;
+static const MIDIFileReader::MIDIByte MIDI_CHANNEL_PREFIX         = 0x20;
+static const MIDIFileReader::MIDIByte MIDI_CHANNEL_PREFIX_OR_PORT = 0x21;
+static const MIDIFileReader::MIDIByte MIDI_END_OF_TRACK           = 0x2F;
+static const MIDIFileReader::MIDIByte MIDI_SET_TEMPO              = 0x51;
+static const MIDIFileReader::MIDIByte MIDI_SMPTE_OFFSET           = 0x54;
+static const MIDIFileReader::MIDIByte MIDI_TIME_SIGNATURE         = 0x58;
+static const MIDIFileReader::MIDIByte MIDI_KEY_SIGNATURE          = 0x59;
+static const MIDIFileReader::MIDIByte MIDI_SEQUENCER_SPECIFIC     = 0x7F;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_BANK_MSB      = 0x00;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_VOLUME        = 0x07;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_BANK_LSB      = 0x20;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_MODULATION    = 0x01;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_PAN           = 0x0A;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_SUSTAIN       = 0x40;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_RESONANCE     = 0x47;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_RELEASE       = 0x48;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_ATTACK        = 0x49;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_FILTER        = 0x4A;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_REVERB        = 0x5B;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_CHORUS        = 0x5D;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_NRPN_1        = 0x62;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_NRPN_2        = 0x63;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_RPN_1         = 0x64;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_RPN_2         = 0x65;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_SOUNDS_OFF    = 0x78;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_RESET         = 0x79;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_LOCAL         = 0x7A;
+static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_ALL_NOTES_OFF = 0x7B;
+static const MIDIFileReader::MIDIByte MIDI_PERCUSSION_CHANNEL       = 9;
+
+class MIDIEvent
+{
+public:
+    typedef MIDIFileReader::MIDIByte MIDIByte;
+
+    MIDIEvent(unsigned long deltaTime,
+              MIDIByte eventCode,
+              MIDIByte data1 = 0,
+              MIDIByte data2 = 0) :
+	m_deltaTime(deltaTime),
+	m_duration(0),
+	m_eventCode(eventCode),
+	m_data1(data1),
+	m_data2(data2),
+	m_metaEventCode(0)
+    { }
+
+    MIDIEvent(unsigned long deltaTime,
+              MIDIByte eventCode,
+              MIDIByte metaEventCode,
+              const string &metaMessage) :
+	m_deltaTime(deltaTime),
+	m_duration(0),
+	m_eventCode(eventCode),
+	m_data1(0),
+	m_data2(0),
+	m_metaEventCode(metaEventCode),
+	m_metaMessage(metaMessage)
+    { }
+
+    MIDIEvent(unsigned long deltaTime,
+              MIDIByte eventCode,
+              const string &sysEx) :
+	m_deltaTime(deltaTime),
+	m_duration(0),
+	m_eventCode(eventCode),
+	m_data1(0),
+	m_data2(0),
+	m_metaEventCode(0),
+	m_metaMessage(sysEx)
+    { }
+
+    ~MIDIEvent() { }
+
+    void setTime(const unsigned long &time) { m_deltaTime = time; }
+    void setDuration(const unsigned long& duration) { m_duration = duration;}
+    unsigned long addTime(const unsigned long &time) {
+	m_deltaTime += time;
+	return m_deltaTime;
+    }
+
+    MIDIByte getMessageType() const
+        { return (m_eventCode & MIDI_MESSAGE_TYPE_MASK); }
+
+    MIDIByte getChannelNumber() const
+        { return (m_eventCode & MIDI_CHANNEL_NUM_MASK); }
+
+    unsigned long getTime() const { return m_deltaTime; }
+    unsigned long getDuration() const { return m_duration; }
+
+    MIDIByte getPitch() const { return m_data1; }
+    MIDIByte getVelocity() const { return m_data2; }
+    MIDIByte getData1() const { return m_data1; }
+    MIDIByte getData2() const { return m_data2; }
+    MIDIByte getEventCode() const { return m_eventCode; }
+
+    bool isMeta() const { return (m_eventCode == MIDI_FILE_META_EVENT); }
+
+    MIDIByte getMetaEventCode() const { return m_metaEventCode; }
+    string getMetaMessage() const { return m_metaMessage; }
+    void setMetaMessage(const string &meta) { m_metaMessage = meta; }
+
+    friend bool operator<(const MIDIEvent &a, const MIDIEvent &b);
+
+private:
+    MIDIEvent& operator=(const MIDIEvent);
+
+    unsigned long  m_deltaTime;
+    unsigned long  m_duration;
+    MIDIByte       m_eventCode;
+    MIDIByte       m_data1;         // or Note
+    MIDIByte       m_data2;         // or Velocity
+    MIDIByte       m_metaEventCode;
+    string         m_metaMessage;
+};
+
+// Comparator for sorting
+//
+struct MIDIEventCmp
+{
+    bool operator()(const MIDIEvent &mE1, const MIDIEvent &mE2) const
+    { return mE1.getTime() < mE2.getTime(); }
+
+    bool operator()(const MIDIEvent *mE1, const MIDIEvent *mE2) const
+    { return mE1->getTime() < mE2->getTime(); }
+};
+
+class MIDIException : virtual public std::exception
+{
+public:
+    MIDIException(QString message) throw() : m_message(message) {
+	cerr << "WARNING: MIDI exception: "
+		  << message.toLocal8Bit().data() << endl;
+    }
+    virtual ~MIDIException() throw() { }
+
+    virtual const char *what() const throw() {
+	return m_message.toLocal8Bit().data();
+    }
+
+protected:
+    QString m_message;
+};
+
+
+MIDIFileReader::MIDIFileReader(QString path,
+			       size_t mainModelSampleRate) :
+    m_timingDivision(0),
+    m_format(MIDI_FILE_BAD_FORMAT),
+    m_numberOfTracks(0),
+    m_trackByteCount(0),
+    m_decrementCount(false),
+    m_path(path),
+    m_midiFile(0),
+    m_fileSize(0),
+    m_mainModelSampleRate(mainModelSampleRate)
+{
+    if (parseFile()) {
+	m_error = "";
+    }
+}
+
+MIDIFileReader::~MIDIFileReader()
+{
+    for (MIDIComposition::iterator i = m_midiComposition.begin();
+	 i != m_midiComposition.end(); ++i) {
+	
+	for (MIDITrack::iterator j = i->second.begin();
+	     j != i->second.end(); ++j) {
+	    delete *j;
+	}
+
+	i->second.clear();
+    }
+
+    m_midiComposition.clear();
+}
+
+bool
+MIDIFileReader::isOK() const
+{
+    return (m_error == "");
+}
+
+QString
+MIDIFileReader::getError() const
+{
+    return m_error;
+}
+
+long
+MIDIFileReader::midiBytesToLong(const string& bytes)
+{
+    if (bytes.length() != 4) {
+	throw MIDIException(tr("Wrong length for long data in MIDI stream (%1, should be %2)").arg(bytes.length()).arg(4));
+    }
+
+    long longRet = ((long)(((MIDIByte)bytes[0]) << 24)) |
+                   ((long)(((MIDIByte)bytes[1]) << 16)) |
+                   ((long)(((MIDIByte)bytes[2]) << 8)) |
+                   ((long)((MIDIByte)(bytes[3])));
+
+    return longRet;
+}
+
+int
+MIDIFileReader::midiBytesToInt(const string& bytes)
+{
+    if (bytes.length() != 2) {
+	throw MIDIException(tr("Wrong length for int data in MIDI stream (%1, should be %2)").arg(bytes.length()).arg(2));
+    }
+
+    int intRet = ((int)(((MIDIByte)bytes[0]) << 8)) |
+                 ((int)(((MIDIByte)bytes[1])));
+    return(intRet);
+}
+
+
+// Gets a single byte from the MIDI byte stream.  For each track
+// section we can read only a specified number of bytes held in
+// m_trackByteCount.
+//
+MIDIFileReader::MIDIByte
+MIDIFileReader::getMIDIByte()
+{
+    if (!m_midiFile) {
+	throw MIDIException(tr("getMIDIByte called but no MIDI file open"));
+    }
+
+    if (m_midiFile->eof()) {
+        throw MIDIException(tr("End of MIDI file encountered while reading"));
+    }
+
+    if (m_decrementCount && m_trackByteCount <= 0) {
+        throw MIDIException(tr("Attempt to get more bytes than expected on Track"));
+    }
+
+    char byte;
+    if (m_midiFile->read(&byte, 1)) {
+	--m_trackByteCount;
+	return (MIDIByte)byte;
+    }
+
+    throw MIDIException(tr("Attempt to read past MIDI file end"));
+}
+
+
+// Gets a specified number of bytes from the MIDI byte stream.  For
+// each track section we can read only a specified number of bytes
+// held in m_trackByteCount.
+//
+string
+MIDIFileReader::getMIDIBytes(unsigned long numberOfBytes)
+{
+    if (!m_midiFile) {
+	throw MIDIException(tr("getMIDIBytes called but no MIDI file open"));
+    }
+
+    if (m_midiFile->eof()) {
+        throw MIDIException(tr("End of MIDI file encountered while reading"));
+    }
+
+    if (m_decrementCount && (numberOfBytes > (unsigned long)m_trackByteCount)) {
+        throw MIDIException(tr("Attempt to get more bytes than available on Track (%1, only have %2)").arg(numberOfBytes).arg(m_trackByteCount));
+    }
+
+    string stringRet;
+    char fileMIDIByte;
+
+    while (stringRet.length() < numberOfBytes &&
+           m_midiFile->read(&fileMIDIByte, 1)) {
+        stringRet += fileMIDIByte;
+    }
+
+    // if we've reached the end of file without fulfilling the
+    // quota then panic as our parsing has performed incorrectly
+    //
+    if (stringRet.length() < numberOfBytes) {
+        stringRet = "";
+        throw MIDIException(tr("Attempt to read past MIDI file end"));
+    }
+
+    // decrement the byte count
+    if (m_decrementCount)
+        m_trackByteCount -= stringRet.length();
+
+    return stringRet;
+}
+
+
+// Get a long number of variable length from the MIDI byte stream.
+//
+long
+MIDIFileReader::getNumberFromMIDIBytes(int firstByte)
+{
+    if (!m_midiFile) {
+	throw MIDIException(tr("getNumberFromMIDIBytes called but no MIDI file open"));
+    }
+
+    long longRet = 0;
+    MIDIByte midiByte;
+
+    if (firstByte >= 0) {
+	midiByte = (MIDIByte)firstByte;
+    } else if (m_midiFile->eof()) {
+	return longRet;
+    } else {
+	midiByte = getMIDIByte();
+    }
+
+    longRet = midiByte;
+    if (midiByte & 0x80) {
+	longRet &= 0x7F;
+	do {
+	    midiByte = getMIDIByte();
+	    longRet = (longRet << 7) + (midiByte & 0x7F);
+	} while (!m_midiFile->eof() && (midiByte & 0x80));
+    }
+
+    return longRet;
+}
+
+
+// Seek to the next track in the midi file and set the number
+// of bytes to be read in the counter m_trackByteCount.
+//
+bool
+MIDIFileReader::skipToNextTrack()
+{
+    if (!m_midiFile) {
+	throw MIDIException(tr("skipToNextTrack called but no MIDI file open"));
+    }
+
+    string buffer, buffer2;
+    m_trackByteCount = -1;
+    m_decrementCount = false;
+
+    while (!m_midiFile->eof() && (m_decrementCount == false)) {
+        buffer = getMIDIBytes(4); 
+	if (buffer.compare(0, 4, MIDI_TRACK_HEADER) == 0) {
+	    m_trackByteCount = midiBytesToLong(getMIDIBytes(4));
+	    m_decrementCount = true;
+	}
+    }
+
+    if (m_trackByteCount == -1) { // we haven't found a track
+        return false;
+    } else {
+        return true;
+    }
+}
+
+
+// Read in a MIDI file.  The parsing process throws exceptions back up
+// here if we run into trouble which we can then pass back out to
+// whoever called us using a nice bool.
+//
+bool
+MIDIFileReader::parseFile()
+{
+    m_error = "";
+
+#ifdef MIDI_DEBUG
+    cerr << "MIDIFileReader::open() : fileName = " << m_fileName.c_str() << endl;
+#endif
+
+    // Open the file
+    m_midiFile = new ifstream(m_path.toLocal8Bit().data(),
+			      ios::in | ios::binary);
+
+    if (!*m_midiFile) {
+	m_error = "File not found or not readable.";
+	m_format = MIDI_FILE_BAD_FORMAT;
+	delete m_midiFile;
+	return false;
+    }
+
+    bool retval = false;
+
+    try {
+
+	// Set file size so we can count it off
+	//
+	m_midiFile->seekg(0, ios::end);
+	m_fileSize = m_midiFile->tellg();
+	m_midiFile->seekg(0, ios::beg);
+
+	// Parse the MIDI header first.  The first 14 bytes of the file.
+	if (!parseHeader(getMIDIBytes(14))) {
+	    m_format = MIDI_FILE_BAD_FORMAT;
+	    m_error = "Not a MIDI file.";
+	    goto done;
+	}
+
+	unsigned int i = 0;
+
+	for (unsigned int j = 0; j < m_numberOfTracks; ++j) {
+
+#ifdef MIDI_DEBUG
+	    cerr << "Parsing Track " << j << endl;
+#endif
+
+	    if (!skipToNextTrack()) {
+#ifdef MIDI_DEBUG
+		cerr << "Couldn't find Track " << j << endl;
+#endif
+		m_error = "File corrupted or in non-standard format?";
+		m_format = MIDI_FILE_BAD_FORMAT;
+		goto done;
+	    }
+
+#ifdef MIDI_DEBUG
+	    cerr << "Track has " << m_trackByteCount << " bytes" << endl;
+#endif
+
+	    // Run through the events taking them into our internal
+	    // representation.
+	    if (!parseTrack(i)) {
+#ifdef MIDI_DEBUG
+		cerr << "Track " << j << " parsing failed" << endl;
+#endif
+		m_error = "File corrupted or in non-standard format?";
+		m_format = MIDI_FILE_BAD_FORMAT;
+		goto done;
+	    }
+
+	    ++i; // j is the source track number, i the destination
+	}
+	
+	m_numberOfTracks = i;
+	retval = true;
+
+    } catch (MIDIException e) {
+
+        cerr << "MIDIFileReader::open() - caught exception - " << e.what() << endl;
+	m_error = e.what();
+    }
+    
+done:
+    m_midiFile->close();
+    delete m_midiFile;
+
+    for (unsigned int track = 0; track < m_numberOfTracks; ++track) {
+
+        // Convert the deltaTime to an absolute time since the track
+        // start.  The addTime method returns the sum of the current
+        // MIDI Event delta time plus the argument.
+
+	unsigned long acc = 0;
+
+        for (MIDITrack::iterator i = m_midiComposition[track].begin();
+             i != m_midiComposition[track].end(); ++i) {
+            acc = (*i)->addTime(acc);
+        }
+
+        if (consolidateNoteOffEvents(track)) { // returns true if some notes exist
+	    m_loadableTracks.insert(track);
+	}
+    }
+
+    for (unsigned int track = 0; track < m_numberOfTracks; ++track) {
+        updateTempoMap(track);
+    }
+
+    calculateTempoTimestamps();
+
+    return retval;
+}
+
+// Parse and ensure the MIDI Header is legitimate
+//
+bool
+MIDIFileReader::parseHeader(const string &midiHeader)
+{
+    if (midiHeader.size() < 14) {
+#ifdef MIDI_DEBUG
+        cerr << "MIDIFileReader::parseHeader() - file header undersized" << endl;
+#endif
+        return false;
+    }
+
+    if (midiHeader.compare(0, 4, MIDI_FILE_HEADER) != 0) {
+#ifdef MIDI_DEBUG
+	cerr << "MIDIFileReader::parseHeader()"
+	     << "- file header not found or malformed"
+	     << endl;
+#endif
+	return false;
+    }
+
+    if (midiBytesToLong(midiHeader.substr(4,4)) != 6L) {
+#ifdef MIDI_DEBUG
+        cerr << "MIDIFileReader::parseHeader()"
+	     << " - header length incorrect"
+	     << endl;
+#endif
+        return false;
+    }
+
+    m_format = (MIDIFileFormatType) midiBytesToInt(midiHeader.substr(8,2));
+    m_numberOfTracks = midiBytesToInt(midiHeader.substr(10,2));
+    m_timingDivision = midiBytesToInt(midiHeader.substr(12,2));
+
+    if (m_format == MIDI_SEQUENTIAL_TRACK_FILE) {
+#ifdef MIDI_DEBUG
+        cerr << "MIDIFileReader::parseHeader()"
+                  << "- can't load sequential track file"
+                  << endl;
+#endif
+        return false;
+    }
+
+#ifdef MIDI_DEBUG
+    if (m_timingDivision < 0) {
+        cerr << "MIDIFileReader::parseHeader()"
+                  << " - file uses SMPTE timing"
+                  << endl;
+    }
+#endif
+
+    return true; 
+}
+
+// Extract the contents from a MIDI file track and places it into
+// our local map of MIDI events.
+//
+bool
+MIDIFileReader::parseTrack(unsigned int &lastTrackNum)
+{
+    MIDIByte midiByte, metaEventCode, data1, data2;
+    MIDIByte eventCode = 0x80;
+    string metaMessage;
+    unsigned int messageLength;
+    unsigned long deltaTime;
+    unsigned long accumulatedTime = 0;
+
+    // The trackNum passed in to this method is the default track for
+    // all events provided they're all on the same channel.  If we find
+    // events on more than one channel, we increment trackNum and record
+    // the mapping from channel to trackNum in this channelTrackMap.
+    // We then return the new trackNum by reference so the calling
+    // method knows we've got more tracks than expected.
+
+    // This would be a vector<unsigned int> but we need -1 to indicate
+    // "not yet used"
+    vector<int> channelTrackMap(16, -1);
+
+    // This is used to store the last absolute time found on each track,
+    // allowing us to modify delta-times correctly when separating events
+    // out from one to multiple tracks
+    //
+    map<int, unsigned long> trackTimeMap;
+
+    // Meta-events don't have a channel, so we place them in a fixed
+    // track number instead
+    unsigned int metaTrack = lastTrackNum;
+
+    // Remember the last non-meta status byte (-1 if we haven't seen one)
+    int runningStatus = -1;
+
+    bool firstTrack = true;
+
+    while (!m_midiFile->eof() && (m_trackByteCount > 0)) {
+
+	if (eventCode < 0x80) {
+#ifdef MIDI_DEBUG
+	    cerr << "WARNING: Invalid event code " << eventCode
+		 << " in MIDI file" << endl;
+#endif
+	    throw MIDIException(tr("Invalid event code %1 found").arg(int(eventCode)));
+	}
+
+        deltaTime = getNumberFromMIDIBytes();
+
+#ifdef MIDI_DEBUG
+	cerr << "read delta time " << deltaTime << endl;
+#endif
+
+        // Get a single byte
+        midiByte = getMIDIByte();
+
+        if (!(midiByte & MIDI_STATUS_BYTE_MASK)) {
+
+	    if (runningStatus < 0) {
+		throw MIDIException(tr("Running status used for first event in track"));
+	    }
+
+	    eventCode = (MIDIByte)runningStatus;
+	    data1 = midiByte;
+
+#ifdef MIDI_DEBUG
+	    cerr << "using running status (byte " << int(midiByte) << " found)" << endl;
+#endif
+        } else {
+#ifdef MIDI_DEBUG
+	    cerr << "have new event code " << int(midiByte) << endl;
+#endif
+            eventCode = midiByte;
+	    data1 = getMIDIByte();
+	}
+
+        if (eventCode == MIDI_FILE_META_EVENT) {
+
+	    metaEventCode = data1;
+            messageLength = getNumberFromMIDIBytes();
+
+//#ifdef MIDI_DEBUG
+		cerr << "Meta event of type " << int(metaEventCode) << " and " << messageLength << " bytes found, putting on track " << metaTrack << endl;
+//#endif
+            metaMessage = getMIDIBytes(messageLength);
+
+	    long gap = accumulatedTime - trackTimeMap[metaTrack];
+	    accumulatedTime += deltaTime;
+	    deltaTime += gap;
+	    trackTimeMap[metaTrack] = accumulatedTime;
+
+            MIDIEvent *e = new MIDIEvent(deltaTime,
+                                         MIDI_FILE_META_EVENT,
+                                         metaEventCode,
+                                         metaMessage);
+
+	    m_midiComposition[metaTrack].push_back(e);
+
+	    if (metaEventCode == MIDI_TRACK_NAME) {
+		m_trackNames[metaTrack] = metaMessage.c_str();
+	    }
+
+        } else { // non-meta events
+
+	    runningStatus = eventCode;
+
+            MIDIEvent *midiEvent;
+
+	    int channel = (eventCode & MIDI_CHANNEL_NUM_MASK);
+	    if (channelTrackMap[channel] == -1) {
+		if (!firstTrack) ++lastTrackNum;
+		else firstTrack = false;
+		channelTrackMap[channel] = lastTrackNum;
+	    }
+
+	    unsigned int trackNum = channelTrackMap[channel];
+	    
+	    // accumulatedTime is abs time of last event on any track;
+	    // trackTimeMap[trackNum] is that of last event on this track
+	    
+	    long gap = accumulatedTime - trackTimeMap[trackNum];
+	    accumulatedTime += deltaTime;
+	    deltaTime += gap;
+	    trackTimeMap[trackNum] = accumulatedTime;
+
+            switch (eventCode & MIDI_MESSAGE_TYPE_MASK) {
+
+            case MIDI_NOTE_ON:
+            case MIDI_NOTE_OFF:
+            case MIDI_POLY_AFTERTOUCH:
+            case MIDI_CTRL_CHANGE:
+                data2 = getMIDIByte();
+
+                // create and store our event
+                midiEvent = new MIDIEvent(deltaTime, eventCode, data1, data2);
+
+                /*
+		cerr << "MIDI event for channel " << channel << " (track "
+			  << trackNum << ")" << endl;
+		midiEvent->print();
+                          */
+
+
+                m_midiComposition[trackNum].push_back(midiEvent);
+
+		if (midiEvent->getChannelNumber() == MIDI_PERCUSSION_CHANNEL) {
+		    m_percussionTracks.insert(trackNum);
+		}
+
+                break;
+
+            case MIDI_PITCH_BEND:
+                data2 = getMIDIByte();
+
+                // create and store our event
+                midiEvent = new MIDIEvent(deltaTime, eventCode, data1, data2);
+                m_midiComposition[trackNum].push_back(midiEvent);
+                break;
+
+            case MIDI_PROG_CHANGE:
+            case MIDI_CHNL_AFTERTOUCH:
+                // create and store our event
+                midiEvent = new MIDIEvent(deltaTime, eventCode, data1);
+                m_midiComposition[trackNum].push_back(midiEvent);
+                break;
+
+            case MIDI_SYSTEM_EXCLUSIVE:
+                messageLength = getNumberFromMIDIBytes(data1);
+
+#ifdef MIDI_DEBUG
+		cerr << "SysEx of " << messageLength << " bytes found" << endl;
+#endif
+
+                metaMessage= getMIDIBytes(messageLength);
+
+                if (MIDIByte(metaMessage[metaMessage.length() - 1]) !=
+                        MIDI_END_OF_EXCLUSIVE)
+                {
+#ifdef MIDI_DEBUG
+                    cerr << "MIDIFileReader::parseTrack() - "
+                              << "malformed or unsupported SysEx type"
+                              << endl;
+#endif
+                    continue;
+                }
+
+                // chop off the EOX 
+                // length fixed by Pedro Lopez-Cabanillas (20030523)
+                //
+                metaMessage = metaMessage.substr(0, metaMessage.length()-1);
+
+                midiEvent = new MIDIEvent(deltaTime,
+                                          MIDI_SYSTEM_EXCLUSIVE,
+                                          metaMessage);
+                m_midiComposition[trackNum].push_back(midiEvent);
+                break;
+
+            case MIDI_END_OF_EXCLUSIVE:
+#ifdef MIDI_DEBUG
+                cerr << "MIDIFileReader::parseTrack() - "
+                          << "Found a stray MIDI_END_OF_EXCLUSIVE" << endl;
+#endif
+                break;
+
+            default:
+#ifdef MIDI_DEBUG
+                cerr << "MIDIFileReader::parseTrack()" 
+                          << " - Unsupported MIDI Event Code:  "
+                          << (int)eventCode << endl;
+#endif
+                break;
+            } 
+        }
+    }
+
+    if (lastTrackNum > metaTrack) {
+	for (unsigned int track = metaTrack + 1; track <= lastTrackNum; ++track) {
+	    m_trackNames[track] = QString("%1 <%2>")
+		.arg(m_trackNames[metaTrack]).arg(track - metaTrack + 1);
+	}
+    }
+
+    return true;
+}
+
+// Delete dead NOTE OFF and NOTE ON/Zero Velocity Events after
+// reading them and modifying their relevant NOTE ONs.  Return true
+// if there are some notes in this track.
+//
+bool
+MIDIFileReader::consolidateNoteOffEvents(unsigned int track)
+{
+    bool notesOnTrack = false;
+    bool noteOffFound;
+
+    for (MIDITrack::iterator i = m_midiComposition[track].begin();
+	 i != m_midiComposition[track].end(); i++) {
+
+        if ((*i)->getMessageType() == MIDI_NOTE_ON && (*i)->getVelocity() > 0) {
+
+	    notesOnTrack = true;
+            noteOffFound = false;
+
+            for (MIDITrack::iterator j = i;
+		 j != m_midiComposition[track].end(); j++) {
+
+                if (((*j)->getChannelNumber() == (*i)->getChannelNumber()) &&
+		    ((*j)->getPitch() == (*i)->getPitch()) &&
+                    ((*j)->getMessageType() == MIDI_NOTE_OFF ||
+                    ((*j)->getMessageType() == MIDI_NOTE_ON &&
+                     (*j)->getVelocity() == 0x00))) {
+
+                    (*i)->setDuration((*j)->getTime() - (*i)->getTime());
+
+                    delete *j;
+                    m_midiComposition[track].erase(j);
+
+                    noteOffFound = true;
+                    break;
+                }
+            }
+
+            // If no matching NOTE OFF has been found then set
+            // Event duration to length of track
+            //
+            if (!noteOffFound) {
+		MIDITrack::iterator j = m_midiComposition[track].end();
+		--j;
+                (*i)->setDuration((*j)->getTime()  - (*i)->getTime());
+	    }
+        }
+    }
+
+    return notesOnTrack;
+}
+
+// Add any tempo events found in the given track to the global tempo map.
+//
+void
+MIDIFileReader::updateTempoMap(unsigned int track)
+{
+    std::cerr << "updateTempoMap for track " << track << " (" << m_midiComposition[track].size() << " events)" << std::endl;
+
+    for (MIDITrack::iterator i = m_midiComposition[track].begin();
+	 i != m_midiComposition[track].end(); ++i) {
+
+        if ((*i)->isMeta() &&
+	    (*i)->getMetaEventCode() == MIDI_SET_TEMPO) {
+
+	    MIDIByte m0 = (*i)->getMetaMessage()[0];
+	    MIDIByte m1 = (*i)->getMetaMessage()[1];
+	    MIDIByte m2 = (*i)->getMetaMessage()[2];
+	    
+	    long tempo = (((m0 << 8) + m1) << 8) + m2;
+
+	    std::cerr << "updateTempoMap: have tempo, it's " << tempo << " at " << (*i)->getTime() << std::endl;
+
+	    if (tempo != 0) {
+		double qpm = 60000000.0 / double(tempo);
+		m_tempoMap[(*i)->getTime()] =
+		    TempoChange(RealTime::zeroTime, qpm);
+	    }
+        }
+    }
+}
+
+void
+MIDIFileReader::calculateTempoTimestamps()
+{
+    unsigned long lastMIDITime = 0;
+    RealTime lastRealTime = RealTime::zeroTime;
+    double tempo = 120.0;
+    int td = m_timingDivision;
+    if (td == 0) td = 96;
+
+    for (TempoMap::iterator i = m_tempoMap.begin(); i != m_tempoMap.end(); ++i) {
+	
+	unsigned long mtime = i->first;
+	unsigned long melapsed = mtime - lastMIDITime;
+	double quarters = double(melapsed) / double(td);
+	double seconds = (60.0 * quarters) / tempo;
+
+	RealTime t = lastRealTime + RealTime::fromSeconds(seconds);
+
+	i->second.first = t;
+
+	lastRealTime = t;
+	lastMIDITime = mtime;
+	tempo = i->second.second;
+    }
+}
+
+RealTime
+MIDIFileReader::getTimeForMIDITime(unsigned long midiTime) const
+{
+    unsigned long tempoMIDITime = 0;
+    RealTime tempoRealTime = RealTime::zeroTime;
+    double tempo = 120.0;
+
+    TempoMap::const_iterator i = m_tempoMap.lower_bound(midiTime);
+    if (i != m_tempoMap.begin()) {
+	--i;
+	tempoMIDITime = i->first;
+	tempoRealTime = i->second.first;
+	tempo = i->second.second;
+    }
+
+    int td = m_timingDivision;
+    if (td == 0) td = 96;
+
+    unsigned long melapsed = midiTime - tempoMIDITime;
+    double quarters = double(melapsed) / double(td);
+    double seconds = (60.0 * quarters) / tempo;
+
+/*
+    std::cerr << "MIDIFileReader::getTimeForMIDITime(" << midiTime << ")"
+	      << std::endl;
+    std::cerr << "timing division = " << td << std::endl;
+    std::cerr << "nearest tempo event (of " << m_tempoMap.size() << ") is at " << tempoMIDITime << " ("
+	      << tempoRealTime << ")" << std::endl;
+    std::cerr << "quarters since then = " << quarters << std::endl;
+    std::cerr << "tempo = " << tempo << " quarters per minute" << std::endl;
+    std::cerr << "seconds since then = " << seconds << std::endl;
+    std::cerr << "resulting time = " << (tempoRealTime + RealTime::fromSeconds(seconds)) << std::endl;
+*/
+
+    return tempoRealTime + RealTime::fromSeconds(seconds);
+}
+
+Model *
+MIDIFileReader::load() const
+{
+    if (!isOK()) return 0;
+
+    if (m_loadableTracks.empty()) {
+	QMessageBox::critical(0, tr("No notes in MIDI file"),
+			      tr("MIDI file \"%1\" has no notes in any track")
+			      .arg(m_path));
+	return 0;
+    }
+
+    std::set<unsigned int> tracksToLoad;
+
+    if (m_loadableTracks.size() == 1) {
+
+	tracksToLoad.insert(*m_loadableTracks.begin());
+
+    } else {
+
+	QStringList available;
+	QString allTracks = tr("Merge all tracks");
+	QString allNonPercussion = tr("Merge all non-percussion tracks");
+
+	int nonTrackItems = 1;
+
+	available << allTracks;
+
+	if (!m_percussionTracks.empty() &&
+	    (m_percussionTracks.size() < m_loadableTracks.size())) {
+	    available << allNonPercussion;
+	    ++nonTrackItems;
+	}
+
+	for (set<unsigned int>::iterator i = m_loadableTracks.begin();
+	     i != m_loadableTracks.end(); ++i) {
+
+	    unsigned int trackNo = *i;
+	    QString label;
+
+	    QString perc;
+	    if (m_percussionTracks.find(trackNo) != m_percussionTracks.end()) {
+		perc = tr(" - uses GM percussion channel");
+	    }
+
+	    if (m_trackNames.find(trackNo) != m_trackNames.end()) {
+		label = tr("Track %1 (%2)%3")
+		    .arg(trackNo).arg(m_trackNames.find(trackNo)->second)
+		    .arg(perc);
+	    } else {
+		label = tr("Track %1 (untitled)%3").arg(trackNo).arg(perc);
+	    }
+	    available << label;
+	}
+
+	bool ok = false;
+	QString selected = QInputDialog::getItem
+	    (0, tr("Select track or tracks to import"),
+	     tr("You can only import this file as a single annotation layer,\nbut the file contains more than one track,\nor notes on more than one channel.\n\nPlease select the track or merged tracks you wish to import:"),
+	     available, 0, false, &ok);
+
+	if (!ok || selected.isEmpty()) return 0;
+	
+	if (selected == allTracks || selected == allNonPercussion) {
+
+	    for (set<unsigned int>::iterator i = m_loadableTracks.begin();
+		 i != m_loadableTracks.end(); ++i) {
+		
+		if (selected == allTracks ||
+		    m_percussionTracks.find(*i) == m_percussionTracks.end()) {
+
+		    tracksToLoad.insert(*i);
+		}
+	    }
+
+	} else {
+	    
+	    int j = nonTrackItems;
+
+	    for (set<unsigned int>::iterator i = m_loadableTracks.begin();
+		 i != m_loadableTracks.end(); ++i) {
+		
+		if (selected == available[j]) {
+		    tracksToLoad.insert(*i);
+		    break;
+		}
+		
+		++j;
+	    }
+	}
+    }
+
+    if (tracksToLoad.empty()) return 0;
+
+    size_t n = tracksToLoad.size(), count = 0;
+    Model *model = 0;
+
+    for (std::set<unsigned int>::iterator i = tracksToLoad.begin();
+	 i != tracksToLoad.end(); ++i) {
+
+	int minProgress = (100 * count) / n;
+	int progressAmount = 100 / n;
+
+	model = loadTrack(*i, model, minProgress, progressAmount);
+
+	++count;
+    }
+
+    if (dynamic_cast<NoteModel *>(model)) {
+	dynamic_cast<NoteModel *>(model)->setCompletion(100);
+    }
+
+    return model;
+}
+
+Model *
+MIDIFileReader::loadTrack(unsigned int trackToLoad,
+			  Model *existingModel,
+			  int minProgress,
+			  int progressAmount) const
+{
+    if (m_midiComposition.find(trackToLoad) == m_midiComposition.end()) {
+	return 0;
+    }
+
+    NoteModel *model = 0;
+
+    if (existingModel) {
+	model = dynamic_cast<NoteModel *>(existingModel);
+	if (!model) {
+	    std::cerr << "WARNING: MIDIFileReader::loadTrack: Existing model given, but it isn't a NoteModel -- ignoring it" << std::endl;
+	}
+    }
+
+    if (!model) {
+	model = new NoteModel(m_mainModelSampleRate, 1, 0.0, 0.0, false);
+	model->setValueQuantization(1.0);
+    }
+
+    const MIDITrack &track = m_midiComposition.find(trackToLoad)->second;
+
+    size_t totalEvents = track.size();
+    size_t count = 0;
+
+    bool minorKey = false;
+    bool sharpKey = true;
+
+    for (MIDITrack::const_iterator i = track.begin(); i != track.end(); ++i) {
+
+	RealTime rt = getTimeForMIDITime((*i)->getTime());
+
+	// We ignore most of these event types for now, though in
+	// theory some of the text ones could usefully be incorporated
+
+	if ((*i)->isMeta()) {
+
+	    switch((*i)->getMetaEventCode()) {
+
+	    case MIDI_KEY_SIGNATURE:
+		minorKey = (int((*i)->getMetaMessage()[1]) != 0);
+		sharpKey = (int((*i)->getMetaMessage()[0]) >= 0);
+		break;
+
+	    case MIDI_TEXT_EVENT:
+	    case MIDI_LYRIC:
+	    case MIDI_TEXT_MARKER:
+	    case MIDI_COPYRIGHT_NOTICE:
+	    case MIDI_TRACK_NAME:
+		// The text events that we could potentially use
+		break;
+
+	    case MIDI_SET_TEMPO:
+		// Already dealt with in a separate pass previously
+		break;
+
+	    case MIDI_TIME_SIGNATURE:
+		// Not yet!
+		break;
+
+	    case MIDI_SEQUENCE_NUMBER:
+	    case MIDI_CHANNEL_PREFIX_OR_PORT:
+	    case MIDI_INSTRUMENT_NAME:
+	    case MIDI_CUE_POINT:
+	    case MIDI_CHANNEL_PREFIX:
+	    case MIDI_SEQUENCER_SPECIFIC:
+	    case MIDI_SMPTE_OFFSET:
+	    default:
+		break;
+	    }
+
+	} else {
+
+	    switch ((*i)->getMessageType()) {
+
+	    case MIDI_NOTE_ON:
+
+                if ((*i)->getVelocity() == 0) break; // effective note-off
+		else {
+		    RealTime endRT = getTimeForMIDITime((*i)->getTime() +
+							(*i)->getDuration());
+
+		    long startFrame = RealTime::realTime2Frame
+			(rt, model->getSampleRate());
+
+		    long endFrame = RealTime::realTime2Frame
+			(endRT, model->getSampleRate());
+
+		    QString pitchLabel = Pitch::getPitchLabel((*i)->getPitch(),
+							      0, 
+							      !sharpKey);
+
+		    QString noteLabel = tr("%1 - vel %2")
+			.arg(pitchLabel).arg(int((*i)->getVelocity()));
+
+		    Note note(startFrame, (*i)->getPitch(),
+			      endFrame - startFrame, noteLabel);
+
+//		    std::cerr << "Adding note " << startFrame << "," << (endFrame-startFrame) << " : " << int((*i)->getPitch()) << std::endl;
+
+		    model->addPoint(note);
+		    break;
+		}
+
+            case MIDI_PITCH_BEND:
+		// I guess we could make some use of this...
+                break;
+
+            case MIDI_NOTE_OFF:
+            case MIDI_PROG_CHANGE:
+            case MIDI_CTRL_CHANGE:
+            case MIDI_SYSTEM_EXCLUSIVE:
+            case MIDI_POLY_AFTERTOUCH:
+            case MIDI_CHNL_AFTERTOUCH:
+                break;
+
+            default:
+                break;
+            }
+	}
+
+	model->setCompletion(minProgress +
+			     (count * progressAmount) / totalEvents);
+	++count;
+    }
+
+    return model;
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/MIDIFileReader.h	Mon Jul 31 11:49:58 2006 +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.
+*/
+
+
+/*
+   This is a modified version of a source file from the 
+   Rosegarden MIDI and audio sequencer and notation editor.
+   This file copyright 2000-2006 Richard Bown and Chris Cannam.
+*/
+
+#ifndef _MIDI_FILE_READER_H_
+#define _MIDI_FILE_READER_H_
+
+#include "DataFileReader.h"
+#include "base/RealTime.h"
+
+#include <map>
+#include <set>
+#include <vector>
+
+#include <QObject>
+
+class MIDIEvent;
+
+class MIDIFileReader : public DataFileReader, public QObject
+{
+public:
+    MIDIFileReader(QString path, size_t mainModelSampleRate);
+    virtual ~MIDIFileReader();
+
+    virtual bool isOK() const;
+    virtual QString getError() const;
+    virtual Model *load() const;
+
+    typedef unsigned char MIDIByte;
+
+protected:
+    typedef std::vector<MIDIEvent *> MIDITrack;
+    typedef std::map<unsigned int, MIDITrack> MIDIComposition;
+    typedef std::pair<RealTime, double> TempoChange; // time, qpm
+    typedef std::map<unsigned long, TempoChange> TempoMap; // key is MIDI time
+
+    typedef enum {
+	MIDI_SINGLE_TRACK_FILE          = 0x00,
+	MIDI_SIMULTANEOUS_TRACK_FILE    = 0x01,
+	MIDI_SEQUENTIAL_TRACK_FILE      = 0x02,
+	MIDI_FILE_BAD_FORMAT            = 0xFF
+    } MIDIFileFormatType;
+
+    bool parseFile();
+    bool parseHeader(const std::string &midiHeader);
+    bool parseTrack(unsigned int &trackNum);
+
+    Model *loadTrack(unsigned int trackNum,
+		     Model *existingModel = 0,
+		     int minProgress = 0,
+		     int progressAmount = 100) const;
+
+    bool consolidateNoteOffEvents(unsigned int track);
+    void updateTempoMap(unsigned int track);
+    void calculateTempoTimestamps();
+    RealTime getTimeForMIDITime(unsigned long midiTime) const;
+
+    // Internal convenience functions
+    //
+    int  midiBytesToInt(const std::string &bytes);
+    long midiBytesToLong(const std::string &bytes);
+
+    long getNumberFromMIDIBytes(int firstByte = -1);
+
+    MIDIByte getMIDIByte();
+    std::string getMIDIBytes(unsigned long bytes);
+
+    bool skipToNextTrack();
+
+    int                    m_timingDivision;   // pulses per quarter note
+    MIDIFileFormatType     m_format;
+    unsigned int           m_numberOfTracks;
+
+    long                   m_trackByteCount;
+    bool                   m_decrementCount;
+
+    std::map<int, QString> m_trackNames;
+    std::set<unsigned int> m_loadableTracks;
+    std::set<unsigned int> m_percussionTracks;
+    MIDIComposition        m_midiComposition;
+    TempoMap               m_tempoMap;
+
+    QString                m_path;
+    std::ifstream         *m_midiFile;
+    size_t                 m_fileSize;
+    QString                m_error;
+    size_t                 m_mainModelSampleRate;
+};
+
+
+#endif // _MIDI_FILE_READER_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/MP3FileReader.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,227 @@
+
+/* -*- 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.
+*/
+
+#ifdef HAVE_MAD
+
+#include "MP3FileReader.h"
+#include "base/System.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <iostream>
+
+#include <QApplication>
+#include <QProgressDialog>
+
+MP3FileReader::MP3FileReader(QString path, bool showProgress, CacheMode mode) :
+    CodedAudioFileReader(mode),
+    m_path(path)
+{
+    m_frameCount = 0;
+    m_channelCount = 0;
+    m_sampleRate = 0;
+    m_fileSize = 0;
+    m_bitrateNum = 0;
+    m_bitrateDenom = 0;
+    m_frameCount = 0;
+    m_cancelled = false;
+
+    struct stat stat;
+    if (::stat(path.toLocal8Bit().data(), &stat) == -1 || stat.st_size == 0) {
+	m_error = QString("File %1 does not exist.").arg(path);
+	return;
+    }
+
+    m_fileSize = stat.st_size;
+
+    int fd;
+    if ((fd = ::open(path.toLocal8Bit().data(), O_RDONLY, 0)) < 0) {
+	m_error = QString("Failed to open file %1 for reading.").arg(path);
+	return;
+    }	
+
+    unsigned char *filebuffer = 0;
+
+    try {
+        filebuffer = new unsigned char[stat.st_size];
+    } catch (...) {
+        m_error = QString("Out of memory");
+        ::close(fd);
+	return;
+    }
+    
+    if (::read(fd, filebuffer, stat.st_size) < stat.st_size) {
+	m_error = QString("Failed to read file %1.").arg(path);
+        delete[] filebuffer;
+        ::close(fd);
+	return;
+    }
+
+    ::close(fd);
+
+    if (showProgress) {
+	m_progress = new QProgressDialog
+	    (QObject::tr("Decoding MP3 file..."),
+	     QObject::tr("Stop"), 0, 100);
+	m_progress->hide();
+    }
+
+    if (!decode(filebuffer, stat.st_size)) {
+	m_error = QString("Failed to decode file %1.").arg(path);
+        delete[] filebuffer;
+	return;
+    }
+    
+    if (isDecodeCacheInitialised()) finishDecodeCache();
+
+    if (showProgress) {
+	delete m_progress;
+	m_progress = 0;
+    }
+
+    delete[] filebuffer;
+}
+
+MP3FileReader::~MP3FileReader()
+{
+}
+
+bool
+MP3FileReader::decode(void *mm, size_t sz)
+{
+    DecoderData data;
+    struct mad_decoder decoder;
+
+    data.start = (unsigned char const *)mm;
+    data.length = (unsigned long)sz;
+    data.reader = this;
+
+    mad_decoder_init(&decoder, &data, input, 0, 0, output, error, 0);
+    mad_decoder_run(&decoder, MAD_DECODER_MODE_SYNC);
+    mad_decoder_finish(&decoder);
+
+    return true;
+}
+
+enum mad_flow
+MP3FileReader::input(void *dp, struct mad_stream *stream)
+{
+    DecoderData *data = (DecoderData *)dp;
+
+    if (!data->length) return MAD_FLOW_STOP;
+    mad_stream_buffer(stream, data->start, data->length);
+    data->length = 0;
+
+    return MAD_FLOW_CONTINUE;
+}
+
+enum mad_flow
+MP3FileReader::output(void *dp,
+		      struct mad_header const *header,
+		      struct mad_pcm *pcm)
+{
+    DecoderData *data = (DecoderData *)dp;
+    return data->reader->accept(header, pcm);
+}
+
+enum mad_flow
+MP3FileReader::accept(struct mad_header const *header,
+		      struct mad_pcm *pcm)
+{
+    int channels = pcm->channels;
+    int frames = pcm->length;
+
+    if (header) {
+        m_bitrateNum += header->bitrate;
+        m_bitrateDenom ++;
+    }
+
+    if (frames < 1) return MAD_FLOW_CONTINUE;
+
+    if (m_channelCount == 0) {
+        m_channelCount = channels;
+        m_sampleRate = pcm->samplerate;
+    }
+    
+    if (m_bitrateDenom > 0) {
+        double bitrate = m_bitrateNum / m_bitrateDenom;
+        double duration = double(m_fileSize * 8) / bitrate;
+        double elapsed = double(m_frameCount) / m_sampleRate;
+        double percent = ((elapsed * 100.0) / duration);
+        int progress = int(percent);
+        if (progress < 1) progress = 1;
+        if (progress > 99) progress = 99;
+        if (progress > m_progress->value()) {
+            m_progress->setValue(progress);
+            m_progress->show();
+            m_progress->raise();
+            qApp->processEvents();
+            if (m_progress->wasCanceled()) {
+                m_cancelled = true;
+            }
+        }
+    }
+
+    if (m_cancelled) return MAD_FLOW_STOP;
+
+    m_frameCount += frames;
+
+    if (!isDecodeCacheInitialised()) {
+        initialiseDecodeCache();
+    }
+
+    for (int i = 0; i < frames; ++i) {
+
+	for (int ch = 0; ch < channels; ++ch) {
+	    mad_fixed_t sample = 0;
+	    if (ch < int(sizeof(pcm->samples) / sizeof(pcm->samples[0]))) {
+		sample = pcm->samples[ch][i];
+	    }
+	    float fsample = float(sample) / float(MAD_F_ONE);
+            addSampleToDecodeCache(fsample);
+	}
+
+	if (! (i & 0xffff)) {
+	    // periodically munlock to ensure we don't exhaust real memory
+	    // if running with memory locked down
+	    MUNLOCK_SAMPLEBLOCK(m_data);
+	}
+    }
+
+    if (frames > 0) {
+	MUNLOCK_SAMPLEBLOCK(m_data);
+    }
+
+    return MAD_FLOW_CONTINUE;
+}
+
+enum mad_flow
+MP3FileReader::error(void *dp,
+		     struct mad_stream *stream,
+		     struct mad_frame *)
+{
+    DecoderData *data = (DecoderData *)dp;
+
+    fprintf(stderr, "decoding error 0x%04x (%s) at byte offset %u\n",
+	    stream->error, mad_stream_errorstr(stream),
+	    stream->this_frame - data->start);
+
+    return MAD_FLOW_CONTINUE;
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/MP3FileReader.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,62 @@
+/* -*- 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 _MP3_FILE_READER_H_
+#define _MP3_FILE_READER_H_
+
+#ifdef HAVE_MAD
+
+#include "CodedAudioFileReader.h"
+
+#include <mad.h>
+
+class QProgressDialog;
+
+class MP3FileReader : public CodedAudioFileReader
+{
+public:
+    MP3FileReader(QString path, bool showProgress, CacheMode cacheMode);
+    virtual ~MP3FileReader();
+
+    virtual QString getError() const { return m_error; }
+    
+protected:
+    QString m_path;
+    QString m_error;
+    size_t m_fileSize;
+    double m_bitrateNum;
+    size_t m_bitrateDenom;
+
+    QProgressDialog *m_progress;
+    bool m_cancelled;
+
+    struct DecoderData
+    {
+	unsigned char const *start;
+	unsigned long length;
+	MP3FileReader *reader;
+    };
+
+    bool decode(void *mm, size_t sz);
+    enum mad_flow accept(struct mad_header const *, struct mad_pcm *);
+
+    static enum mad_flow input(void *, struct mad_stream *);
+    static enum mad_flow output(void *, struct mad_header const *, struct mad_pcm *);
+    static enum mad_flow error(void *, struct mad_stream *, struct mad_frame *);
+};
+
+#endif
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/MatrixFile.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,633 @@
+/* -*- 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 "MatrixFile.h"
+#include "base/TempDirectory.h"
+#include "base/System.h"
+#include "base/Profiler.h"
+#include "base/Exceptions.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <iostream>
+
+#include <cstdio>
+#include <cassert>
+
+#include <QFileInfo>
+#include <QDir>
+
+//#define DEBUG_MATRIX_FILE 1
+//#define DEBUG_MATRIX_FILE_READ_SET 1
+
+#ifdef DEBUG_MATRIX_FILE_READ_SET
+#define DEBUG_MATRIX_FILE 1
+#endif
+
+std::map<QString, int> MatrixFile::m_refcount;
+QMutex MatrixFile::m_refcountMutex;
+
+MatrixFile::ResizeableBitsetMap MatrixFile::m_columnBitsets;
+QMutex MatrixFile::m_columnBitsetWriteMutex;
+
+FileReadThread *MatrixFile::m_readThread = 0;
+
+static size_t totalStorage = 0;
+static size_t totalMemory = 0;
+static size_t totalCount = 0;
+
+MatrixFile::MatrixFile(QString fileBase, Mode mode,
+                       size_t cellSize, bool eagerCache) :
+    m_fd(-1),
+    m_mode(mode),
+    m_flags(0),
+    m_fmode(0),
+    m_cellSize(cellSize),
+    m_width(0),
+    m_height(0),
+    m_headerSize(2 * sizeof(size_t)),
+    m_defaultCacheWidth(1024),
+    m_prevX(0),
+    m_eagerCache(eagerCache),
+    m_requestToken(-1),
+    m_spareData(0),
+    m_columnBitset(0)
+{
+    Profiler profiler("MatrixFile::MatrixFile", true);
+
+    if (!m_readThread) {
+        m_readThread = new FileReadThread;
+        m_readThread->start();
+    }
+
+    m_cache.data = 0;
+
+    QDir tempDir(TempDirectory::getInstance()->getPath());
+    QString fileName(tempDir.filePath(QString("%1.mfc").arg(fileBase)));
+    bool newFile = !QFileInfo(fileName).exists();
+
+    if (newFile && m_mode == ReadOnly) {
+        std::cerr << "ERROR: MatrixFile::MatrixFile: Read-only mode "
+                  << "specified, but cache file does not exist" << std::endl;
+        throw FileNotFound(fileName);
+    }
+
+    if (!newFile && m_mode == ReadWrite) {
+        std::cerr << "Note: MatrixFile::MatrixFile: Read/write mode "
+                  << "specified, but file already exists; falling back to "
+                  << "read-only mode" << std::endl;
+        m_mode = ReadOnly;
+    }
+
+    if (!eagerCache && m_mode == ReadOnly) {
+        std::cerr << "WARNING: MatrixFile::MatrixFile: Eager cacheing not "
+                  << "specified, but file is open in read-only mode -- cache "
+                  << "will not be used" << std::endl;
+    }
+
+    m_flags = 0;
+    m_fmode = S_IRUSR | S_IWUSR;
+
+    if (m_mode == ReadWrite) {
+        m_flags = O_RDWR | O_CREAT;
+    } else {
+        m_flags = O_RDONLY;
+    }
+
+#ifdef DEBUG_MATRIX_FILE
+    std::cerr << "MatrixFile::MatrixFile: opening " << fileName.toStdString() << "..." << std::endl;
+#endif
+
+    if ((m_fd = ::open(fileName.toLocal8Bit(), m_flags, m_fmode)) < 0) {
+        ::perror("Open failed");
+        std::cerr << "ERROR: MatrixFile::MatrixFile: "
+                  << "Failed to open cache file \""
+                  << fileName.toStdString() << "\"";
+        if (m_mode == ReadWrite) std::cerr << " for writing";
+        std::cerr << std::endl;
+        throw FailedToOpenFile(fileName);
+    }
+
+    if (newFile) {
+        resize(0, 0); // write header
+    } else {
+        size_t header[2];
+        if (::read(m_fd, header, 2 * sizeof(size_t)) < 0) {
+            perror("Read failed");
+            std::cerr << "ERROR: MatrixFile::MatrixFile: "
+                      << "Failed to read header (fd " << m_fd << ", file \""
+                      << fileName.toStdString() << "\")" << std::endl;
+            throw FileReadFailed(fileName);
+        }
+        m_width = header[0];
+        m_height = header[1];
+        seekTo(0, 0);
+    }
+
+    m_fileName = fileName;
+
+    m_columnBitsetWriteMutex.lock();
+
+    if (m_columnBitsets.find(m_fileName) == m_columnBitsets.end()) {
+        m_columnBitsets[m_fileName] = new ResizeableBitset;
+    }
+    m_columnBitset = m_columnBitsets[m_fileName];
+
+    m_columnBitsetWriteMutex.unlock();
+
+    QMutexLocker locker(&m_refcountMutex);
+    ++m_refcount[fileName];
+
+    std::cerr << "MatrixFile(" << this << "): fd " << m_fd << ", file " << fileName.toStdString() << ", ref " << m_refcount[fileName] << std::endl;
+
+//    std::cerr << "MatrixFile::MatrixFile: Done, size is " << "(" << m_width << ", " << m_height << ")" << std::endl;
+
+    ++totalCount;
+
+}
+
+MatrixFile::~MatrixFile()
+{
+    char *requestData = 0;
+
+    if (m_requestToken >= 0) {
+        FileReadThread::Request request;
+        if (m_readThread->getRequest(m_requestToken, request)) {
+            requestData = request.data;
+        }
+        m_readThread->cancel(m_requestToken);
+    }
+
+    if (requestData) free(requestData);
+    if (m_cache.data) free(m_cache.data);
+    if (m_spareData) free(m_spareData);
+
+    if (m_fd >= 0) {
+        if (::close(m_fd) < 0) {
+            ::perror("MatrixFile::~MatrixFile: close failed");
+        }
+    }
+
+    if (m_fileName != "") {
+
+        QMutexLocker locker(&m_refcountMutex);
+
+        if (--m_refcount[m_fileName] == 0) {
+
+            if (::unlink(m_fileName.toLocal8Bit())) {
+                ::perror("Unlink failed");
+                std::cerr << "WARNING: MatrixFile::~MatrixFile: reference count reached 0, but failed to unlink file \"" << m_fileName.toStdString() << "\"" << std::endl;
+            } else {
+                std::cerr << "deleted " << m_fileName.toStdString() << std::endl;
+            }
+
+            QMutexLocker locker2(&m_columnBitsetWriteMutex);
+            m_columnBitsets.erase(m_fileName);
+            delete m_columnBitset;
+        }
+    }
+    
+    totalStorage -= (m_headerSize + (m_width * m_height * m_cellSize));
+    totalMemory -= (2 * m_defaultCacheWidth * m_height * m_cellSize);
+    totalCount --;
+
+    std::cerr << "MatrixFile::~MatrixFile: " << std::endl;
+    std::cerr << "Total storage now " << totalStorage/1024 << "K, theoretical max memory "
+              << totalMemory/1024 << "K in " << totalCount << " instances" << std::endl;
+
+}
+
+void
+MatrixFile::resize(size_t w, size_t h)
+{
+    Profiler profiler("MatrixFile::resize", true);
+
+    assert(m_mode == ReadWrite);
+
+    QMutexLocker locker(&m_fdMutex);
+    
+    totalStorage -= (m_headerSize + (m_width * m_height * m_cellSize));
+    totalMemory -= (2 * m_defaultCacheWidth * m_height * m_cellSize);
+
+    off_t off = m_headerSize + (w * h * m_cellSize);
+
+#ifdef DEBUG_MATRIX_FILE
+    std::cerr << "MatrixFile::resize(" << w << ", " << h << "): resizing file" << std::endl;
+#endif
+
+    if (w * h < m_width * m_height) {
+        if (::ftruncate(m_fd, off) < 0) {
+            ::perror("WARNING: MatrixFile::resize: ftruncate failed");
+            throw FileOperationFailed(m_fileName, "ftruncate");
+        }
+    }
+
+    m_width = 0;
+    m_height = 0;
+
+    if (::lseek(m_fd, 0, SEEK_SET) == (off_t)-1) {
+        ::perror("ERROR: MatrixFile::resize: Seek to write header failed");
+        throw FileOperationFailed(m_fileName, "lseek");
+    }
+
+    size_t header[2];
+    header[0] = w;
+    header[1] = h;
+    if (::write(m_fd, header, 2 * sizeof(size_t)) != 2 * sizeof(size_t)) {
+        ::perror("ERROR: MatrixFile::resize: Failed to write header");
+        throw FileOperationFailed(m_fileName, "write");
+    }
+
+    if (w > 0 && m_defaultCacheWidth > w) {
+        m_defaultCacheWidth = w;
+    }
+
+    static size_t maxCacheMB = 16;
+    if (2 * m_defaultCacheWidth * h * m_cellSize > maxCacheMB * 1024 * 1024) { //!!!
+        m_defaultCacheWidth = (maxCacheMB * 1024 * 1024) / (2 * h * m_cellSize);
+        if (m_defaultCacheWidth < 16) m_defaultCacheWidth = 16;
+    }
+
+    if (m_columnBitset) {
+        QMutexLocker locker(&m_columnBitsetWriteMutex);
+        m_columnBitset->resize(w);
+    }
+
+    if (m_cache.data) {
+        free(m_cache.data);
+        m_cache.data = 0;
+    }
+
+    if (m_spareData) {
+        free(m_spareData);
+        m_spareData = 0;
+    }
+    
+    m_width = w;
+    m_height = h;
+
+    totalStorage += (m_headerSize + (m_width * m_height * m_cellSize));
+    totalMemory += (2 * m_defaultCacheWidth * m_height * m_cellSize);
+
+#ifdef DEBUG_MATRIX_FILE
+    std::cerr << "MatrixFile::resize(" << w << ", " << h << "): cache width "
+              << m_defaultCacheWidth << ", storage "
+              << (m_headerSize + w * h * m_cellSize) << ", mem "
+              << (2 * h * m_defaultCacheWidth * m_cellSize) << std::endl;
+
+    std::cerr << "Total storage " << totalStorage/1024 << "K, theoretical max memory "
+              << totalMemory/1024 << "K in " << totalCount << " instances" << std::endl;
+#endif
+
+    seekTo(0, 0);
+}
+
+void
+MatrixFile::reset()
+{
+    Profiler profiler("MatrixFile::reset", true);
+
+    assert (m_mode == ReadWrite);
+    
+    if (m_eagerCache) {
+        void *emptyCol = calloc(m_height, m_cellSize);
+        for (size_t x = 0; x < m_width; ++x) setColumnAt(x, emptyCol);
+        free(emptyCol);
+    }
+    
+    if (m_columnBitset) {
+        QMutexLocker locker(&m_columnBitsetWriteMutex);
+        m_columnBitset->resize(m_width);
+    }
+}
+
+void
+MatrixFile::getColumnAt(size_t x, void *data)
+{
+//    Profiler profiler("MatrixFile::getColumnAt");
+
+//    assert(haveSetColumnAt(x));
+
+    if (getFromCache(x, 0, m_height, data)) return;
+
+//    Profiler profiler2("MatrixFile::getColumnAt (uncached)");
+
+    ssize_t r = 0;
+
+#ifdef DEBUG_MATRIX_FILE
+    std::cerr << "MatrixFile::getColumnAt(" << x << ")"
+              << ": reading the slow way";
+
+    if (m_requestToken >= 0 &&
+        x >= m_requestingX &&
+        x <  m_requestingX + m_requestingWidth) {
+        
+        std::cerr << " (awaiting " << m_requestingX << ", " << m_requestingWidth << " from disk)";
+    }
+
+    std::cerr << std::endl;
+#endif
+
+    m_fdMutex.lock();
+
+    if (seekTo(x, 0)) {
+        r = ::read(m_fd, data, m_height * m_cellSize);
+    }
+
+    m_fdMutex.unlock();
+    
+    if (r < 0) {
+        ::perror("MatrixFile::getColumnAt: read failed");
+        throw FileReadFailed(m_fileName);
+    }
+
+    return;
+}
+
+bool
+MatrixFile::getFromCache(size_t x, size_t ystart, size_t ycount, void *data)
+{
+    m_cacheMutex.lock();
+
+    if (!m_cache.data || x < m_cache.x || x >= m_cache.x + m_cache.width) {
+        bool left = (m_cache.data && x < m_cache.x);
+        m_cacheMutex.unlock();
+        primeCache(x, left); // this doesn't take effect until a later callback
+        m_prevX = x;
+        return false;
+    }
+
+    memcpy(data,
+           m_cache.data + m_cellSize * ((x - m_cache.x) * m_height + ystart),
+           ycount * m_cellSize);
+
+    m_cacheMutex.unlock();
+
+    if (m_cache.x > 0 && x < m_prevX && x < m_cache.x + m_cache.width/4) {
+        primeCache(x, true);
+    }
+
+    if (m_cache.x + m_cache.width < m_width &&
+        x > m_prevX &&
+        x > m_cache.x + (m_cache.width * 3) / 4) {
+        primeCache(x, false);
+    }
+
+    m_prevX = x;
+    return true;
+}
+
+void
+MatrixFile::setColumnAt(size_t x, const void *data)
+{
+    assert(m_mode == ReadWrite);
+
+#ifdef DEBUG_MATRIX_FILE_READ_SET
+    std::cerr << "MatrixFile::setColumnAt(" << x << ")" << std::endl;
+#endif
+
+    ssize_t w = 0;
+    bool seekFailed = false;
+
+    m_fdMutex.lock();
+
+    if (seekTo(x, 0)) {
+        w = ::write(m_fd, data, m_height * m_cellSize);
+    } else {
+        seekFailed = true;
+    }
+
+    m_fdMutex.unlock();
+
+    if (!seekFailed && w != ssize_t(m_height * m_cellSize)) {
+        ::perror("WARNING: MatrixFile::setColumnAt: write failed");
+        throw FileOperationFailed(m_fileName, "write");
+    } else if (seekFailed) {
+        throw FileOperationFailed(m_fileName, "seek");
+    } else {
+        QMutexLocker locker(&m_columnBitsetWriteMutex);
+        m_columnBitset->set(x);
+    }
+}
+
+void
+MatrixFile::suspend()
+{
+    QMutexLocker locker(&m_fdMutex);
+    QMutexLocker locker2(&m_cacheMutex);
+
+    if (m_fd < 0) return; // already suspended
+
+#ifdef DEBUG_MATRIX_FILE
+    std::cerr << "MatrixFile(" << this << ":" << m_fileName.toStdString() << ")::suspend(): fd was " << m_fd << std::endl;
+#endif
+
+    if (m_requestToken >= 0) {
+        void *data = 0;
+        FileReadThread::Request request;
+        if (m_readThread->getRequest(m_requestToken, request)) {
+            data = request.data;
+        }
+        m_readThread->cancel(m_requestToken);
+        if (data) free(data);
+        m_requestToken = -1;
+    }
+
+    if (m_cache.data) {
+        free(m_cache.data);
+        m_cache.data = 0;
+    }
+
+    if (m_spareData) {
+        free(m_spareData);
+        m_spareData = 0;
+    }
+    
+    if (::close(m_fd) < 0) {
+        ::perror("WARNING: MatrixFile::suspend: close failed");
+        throw FileOperationFailed(m_fileName, "close");
+    }
+
+    m_fd = -1;
+}
+
+void
+MatrixFile::resume()
+{
+    if (m_fd >= 0) return;
+
+#ifdef DEBUG_MATRIX_FILE    
+    std::cerr << "MatrixFile(" << this << ")::resume()" << std::endl;
+#endif
+
+    if ((m_fd = ::open(m_fileName.toLocal8Bit(), m_flags, m_fmode)) < 0) {
+        ::perror("Open failed");
+        std::cerr << "ERROR: MatrixFile::resume: "
+                  << "Failed to open cache file \""
+                  << m_fileName.toStdString() << "\"";
+        if (m_mode == ReadWrite) std::cerr << " for writing";
+        std::cerr << std::endl;
+        throw FailedToOpenFile(m_fileName);
+    }
+
+    std::cerr << "MatrixFile(" << this << ":" << m_fileName.toStdString() << ")::resume(): fd is " << m_fd << std::endl;
+}
+
+void
+MatrixFile::primeCache(size_t x, bool goingLeft)
+{
+//    Profiler profiler("MatrixFile::primeCache");
+
+#ifdef DEBUG_MATRIX_FILE_READ_SET
+    std::cerr << "MatrixFile::primeCache(" << x << ", " << goingLeft << ")" << std::endl;
+#endif
+
+    size_t rx = x;
+    size_t rw = m_defaultCacheWidth;
+
+    size_t left = rw / 3;
+    if (goingLeft) left = (rw * 2) / 3;
+
+    if (rx > left) rx -= left;
+    else rx = 0;
+
+    if (rx + rw > m_width) rw = m_width - rx;
+
+    if (!m_eagerCache) {
+
+        size_t ti = 0;
+
+        for (ti = 0; ti < rw; ++ti) {
+            if (!m_columnBitset->get(rx + ti)) break;
+        }
+        
+#ifdef DEBUG_MATRIX_FILE
+        if (ti < rw) {
+            std::cerr << "eagerCache is false and there's a hole at "
+                      << rx + ti << ", reducing rw from " << rw << " to "
+                      << ti << std::endl;
+        }
+#endif
+
+        rw = std::min(rw, ti);
+        if (rw < 10 || rx + rw <= x) return;
+    }
+
+    QMutexLocker locker(&m_cacheMutex);
+
+    FileReadThread::Request request;
+
+    if (m_requestToken >= 0 &&
+        m_readThread->getRequest(m_requestToken, request)) {
+
+        if (x >= m_requestingX &&
+            x <  m_requestingX + m_requestingWidth) {
+
+            if (m_readThread->isReady(m_requestToken)) {
+
+                if (!request.successful) {
+                    std::cerr << "ERROR: MatrixFile::primeCache: Last request was unsuccessful" << std::endl;
+                    throw FileReadFailed(m_fileName);
+                }
+                
+#ifdef DEBUG_MATRIX_FILE_READ_SET
+                std::cerr << "last request is ready! (" << m_requestingX << ", "<< m_requestingWidth << ")"  << std::endl;
+#endif
+
+                m_cache.x = (request.start - m_headerSize) / (m_height * m_cellSize);
+                m_cache.width = request.size / (m_height * m_cellSize);
+      
+#ifdef DEBUG_MATRIX_FILE_READ_SET
+                std::cerr << "received last request: actual size is: " << m_cache.x << ", " << m_cache.width << std::endl;
+#endif
+
+                if (m_cache.data) {
+                    if (m_spareData) free(m_spareData);
+                    m_spareData = m_cache.data;
+                }
+                m_cache.data = request.data;
+
+                m_readThread->done(m_requestToken);
+                m_requestToken = -1;
+            }
+
+            // already requested something covering this area; wait for it
+            return;
+        }
+
+        // the current request is no longer of any use
+        m_readThread->cancel(m_requestToken);
+
+        // crude way to avoid leaking the data
+        while (!m_readThread->isCancelled(m_requestToken)) {
+            usleep(10000);
+        }
+
+#ifdef DEBUG_MATRIX_FILE_READ_SET
+        std::cerr << "cancelled " << m_requestToken << std::endl;
+#endif
+
+        if (m_spareData) free(m_spareData);
+        m_spareData = request.data;
+        m_readThread->done(m_requestToken);
+
+        m_requestToken = -1;
+    }
+
+    if (m_fd < 0) {
+        m_fdMutex.lock();
+        if (m_fd < 0) resume();
+        m_fdMutex.unlock();
+    }
+
+    request.fd = m_fd;
+    request.mutex = &m_fdMutex;
+    request.start = m_headerSize + rx * m_height * m_cellSize;
+    request.size = rw * m_height * m_cellSize;
+    request.data = (char *)realloc(m_spareData, rw * m_height * m_cellSize);
+    MUNLOCK(request.data, rw * m_height * m_cellSize);
+    m_spareData = 0;
+
+    m_requestingX = rx;
+    m_requestingWidth = rw;
+
+    int token = m_readThread->request(request);
+#ifdef DEBUG_MATRIX_FILE_READ_SET
+    std::cerr << "MatrixFile::primeCache: request token is "
+              << token << " (x = [" << rx << "], w = [" << rw << "], left = [" << goingLeft << "])" << std::endl;
+#endif
+    m_requestToken = token;
+}
+
+bool
+MatrixFile::seekTo(size_t x, size_t y)
+{
+    if (m_fd < 0) resume();
+
+    off_t off = m_headerSize + (x * m_height + y) * m_cellSize;
+
+    if (::lseek(m_fd, off, SEEK_SET) == (off_t)-1) {
+        ::perror("Seek failed");
+        std::cerr << "ERROR: MatrixFile::seekTo(" << x << ", " << y
+                  << ") failed" << std::endl;
+        return false;
+    }
+
+    return true;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/MatrixFile.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,124 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This 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 _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, ReadWrite };
+
+    /**
+     * 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 ReadWrite and the file does not exist, it will be
+     * created.  If mode is ReadWrite and the file does exist, the
+     * existing file will be used and the mode will be reset to
+     * ReadOnly.  Call getMode() to check whether this has occurred
+     * after construction.
+     *
+     * 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.
+     *
+     * If eagerCache is true, blocks from the file will be cached for
+     * read.  If eagerCache is false, only columns that have been set
+     * by calling setColumnAt on this MatrixFile (i.e. columns for
+     * which haveSetColumnAt returns true) will be cached.
+     */
+    MatrixFile(QString fileBase, Mode mode, size_t cellSize, bool eagerCache);
+    virtual ~MatrixFile();
+
+    Mode getMode() const { return m_mode; }
+
+    size_t getWidth() const { return m_width; }
+    size_t getHeight() const { return m_height; }
+    size_t getCellSize() const { return m_cellSize; }
+    
+    void resize(size_t width, size_t height);
+    void reset();
+
+    bool haveSetColumnAt(size_t x) const { return m_columnBitset->get(x); }
+    void getColumnAt(size_t x, void *data);
+    void setColumnAt(size_t x, const void *data);
+
+    void suspend();
+
+protected:
+    int     m_fd;
+    Mode    m_mode;
+    int     m_flags;
+    mode_t  m_fmode;
+    size_t  m_cellSize;
+    size_t  m_width;
+    size_t  m_height;
+    size_t  m_headerSize;
+    QString m_fileName;
+    size_t  m_defaultCacheWidth;
+    size_t  m_prevX;
+
+    struct Cache {
+        size_t x;
+        size_t width;
+        char *data;
+    };
+
+    Cache m_cache;
+    bool  m_eagerCache;
+
+    bool getFromCache(size_t x, size_t ystart, size_t ycount, void *data);
+    void primeCache(size_t x, bool left);
+
+    void resume();
+
+    bool seekTo(size_t x, size_t y);
+
+    static FileReadThread *m_readThread;
+    int m_requestToken;
+
+    size_t m_requestingX;
+    size_t m_requestingWidth;
+    char *m_spareData;
+
+    static std::map<QString, int> m_refcount;
+    static QMutex m_refcountMutex;
+    QMutex m_fdMutex;
+    QMutex m_cacheMutex;
+
+    typedef std::map<QString, ResizeableBitset *> ResizeableBitsetMap;
+    static ResizeableBitsetMap m_columnBitsets;
+    static QMutex m_columnBitsetWriteMutex;
+    ResizeableBitset *m_columnBitset;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/OggVorbisFileReader.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,159 @@
+/* -*- 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.
+*/
+
+#ifdef HAVE_OGGZ
+#ifdef HAVE_FISHSOUND
+
+#include "OggVorbisFileReader.h"
+#include "base/Profiler.h"
+#include "base/System.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <cmath>
+
+#include <QApplication>
+#include <QFileInfo>
+#include <QProgressDialog>
+
+static int instances = 0;
+
+OggVorbisFileReader::OggVorbisFileReader(QString path, bool showProgress,
+                                         CacheMode mode) :
+    CodedAudioFileReader(mode),
+    m_path(path),
+    m_progress(0),
+    m_fileSize(0),
+    m_bytesRead(0),
+    m_cancelled(false)
+{
+    m_frameCount = 0;
+    m_channelCount = 0;
+    m_sampleRate = 0;
+
+    std::cerr << "OggVorbisFileReader::OggVorbisFileReader(" << path.toLocal8Bit().data() << "): now have " << (++instances) << " instances" << std::endl;
+
+    Profiler profiler("OggVorbisFileReader::OggVorbisFileReader", true);
+
+    QFileInfo info(path);
+    m_fileSize = info.size();
+
+    OGGZ *oggz;
+    if (!(oggz = oggz_open(path.toLocal8Bit().data(), OGGZ_READ))) {
+	m_error = QString("File %1 is not an OGG file.").arg(path);
+	return;
+    }
+
+    FishSoundInfo fsinfo;
+    m_fishSound = fish_sound_new(FISH_SOUND_DECODE, &fsinfo);
+
+    fish_sound_set_decoded_callback(m_fishSound, acceptFrames, this);
+    oggz_set_read_callback(oggz, -1, readPacket, this);
+
+    if (showProgress) {
+	m_progress = new QProgressDialog
+	    (QObject::tr("Decoding Ogg file..."),
+	     QObject::tr("Stop"), 0, 100);
+	m_progress->hide();
+    }
+
+    while (oggz_read(oggz, 1024) > 0);
+
+    fish_sound_delete(m_fishSound);
+    m_fishSound = 0;
+    oggz_close(oggz);
+
+    if (isDecodeCacheInitialised()) finishDecodeCache();
+
+    if (showProgress) {
+	delete m_progress;
+	m_progress = 0;
+    }
+}
+
+OggVorbisFileReader::~OggVorbisFileReader()
+{
+    std::cerr << "OggVorbisFileReader::~OggVorbisFileReader(" << m_path.toLocal8Bit().data() << "): now have " << (--instances) << " instances" << std::endl;
+}
+
+int
+OggVorbisFileReader::readPacket(OGGZ *, ogg_packet *packet, long, void *data)
+{
+    OggVorbisFileReader *reader = (OggVorbisFileReader *)data;
+    FishSound *fs = reader->m_fishSound;
+
+    fish_sound_prepare_truncation(fs, packet->granulepos, packet->e_o_s);
+    fish_sound_decode(fs, packet->packet, packet->bytes);
+
+    reader->m_bytesRead += packet->bytes;
+    
+    if (reader->m_fileSize > 0 && reader->m_progress) {
+	// The number of bytes read by this function is smaller than
+	// the file size because of the packet headers
+	int progress = lrint(double(reader->m_bytesRead) * 114 /
+			     double(reader->m_fileSize));
+	if (progress > 99) progress = 99;
+	if (progress > reader->m_progress->value()) {
+	    reader->m_progress->setValue(progress);
+	    reader->m_progress->show();
+	    reader->m_progress->raise();
+	    qApp->processEvents();
+	    if (reader->m_progress->wasCanceled()) {
+		reader->m_cancelled = true;
+	    }
+	}
+    } 
+
+    if (reader->m_cancelled) return 1;
+    return 0;
+}
+
+int
+OggVorbisFileReader::acceptFrames(FishSound *fs, float **frames, long nframes,
+				  void *data)
+{
+    OggVorbisFileReader *reader = (OggVorbisFileReader *)data;
+
+    if (reader->m_channelCount == 0) {
+	FishSoundInfo fsinfo;
+	fish_sound_command(fs, FISH_SOUND_GET_INFO,
+			   &fsinfo, sizeof(FishSoundInfo));
+	reader->m_channelCount = fsinfo.channels;
+	reader->m_sampleRate = fsinfo.samplerate;
+        reader->initialiseDecodeCache();
+    }
+
+    if (nframes > 0) {
+
+	reader->m_frameCount += nframes;
+    
+	for (long i = 0; i < nframes; ++i) {
+	    for (size_t c = 0; c < reader->m_channelCount; ++c) {
+                reader->addSampleToDecodeCache(frames[c][i]);
+//		reader->m_data.push_back(frames[c][i]);
+	    }
+	}
+
+	MUNLOCK_SAMPLEBLOCK(reader->m_data);
+    }
+
+    if (reader->m_cancelled) return 1;
+    return 0;
+}
+
+#endif
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/OggVorbisFileReader.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,54 @@
+/* -*- 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 _OGG_VORBIS_FILE_READER_H_
+#define _OGG_VORBIS_FILE_READER_H_
+
+#ifdef HAVE_OGGZ
+#ifdef HAVE_FISHSOUND
+
+#include "CodedAudioFileReader.h"
+
+#include <oggz/oggz.h>
+#include <fishsound/fishsound.h>
+
+class QProgressDialog;
+
+class OggVorbisFileReader : public CodedAudioFileReader
+{
+public:
+    OggVorbisFileReader(QString path, bool showProgress, CacheMode cacheMode);
+    virtual ~OggVorbisFileReader();
+
+    virtual QString getError() const { return m_error; }
+
+protected:
+    QString m_path;
+    QString m_error;
+
+    FishSound *m_fishSound;
+    QProgressDialog *m_progress;
+    size_t m_fileSize;
+    size_t m_bytesRead;
+    bool m_cancelled;
+ 
+    static int readPacket(OGGZ *, ogg_packet *, long, void *);
+    static int acceptFrames(FishSound *, float **, long, void *);
+};
+
+#endif
+#endif
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/RecentFiles.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,123 @@
+/* -*- 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 "RecentFiles.h"
+#include "ConfigFile.h"
+
+#include "base/Preferences.h"
+
+#include <QFileInfo>
+
+RecentFiles *
+RecentFiles::m_instance = 0;
+
+RecentFiles *
+RecentFiles::getInstance(int maxFileCount)
+{
+    if (!m_instance) {
+        m_instance = new RecentFiles(maxFileCount);
+    }
+    return m_instance;
+}
+
+RecentFiles::RecentFiles(int maxFileCount) :
+    m_maxFileCount(maxFileCount)
+{
+    readFiles();
+}
+
+RecentFiles::~RecentFiles()
+{
+    // nothing
+}
+
+void
+RecentFiles::readFiles()
+{
+    m_files.clear();
+    ConfigFile *cf = Preferences::getInstance()->getConfigFile();
+    for (unsigned int i = 0; i < 100; ++i) {
+        QString key = QString("recent-file-%1").arg(i);
+        QString filename = cf->get(key);
+        if (filename == "") break;
+        if (i < m_maxFileCount) m_files.push_back(filename);
+        else cf->set(key, "");
+    }
+    cf->commit();
+}
+
+void
+RecentFiles::writeFiles()
+{
+    ConfigFile *cf = Preferences::getInstance()->getConfigFile();
+    for (unsigned int i = 0; i < m_maxFileCount; ++i) {
+        QString key = QString("recent-file-%1").arg(i);
+        QString filename = "";
+        if (i < m_files.size()) filename = m_files[i];
+        cf->set(key, filename);
+    }
+    cf->commit();
+}
+
+void
+RecentFiles::truncateAndWrite()
+{
+    while (m_files.size() > m_maxFileCount) {
+        m_files.pop_back();
+    }
+    writeFiles();
+}
+
+std::vector<QString>
+RecentFiles::getRecentFiles() const
+{
+    std::vector<QString> files;
+    for (unsigned int i = 0; i < m_maxFileCount; ++i) {
+        if (i < m_files.size()) {
+            files.push_back(m_files[i]);
+        }
+    }
+    return files;
+}
+
+void
+RecentFiles::addFile(QString filename)
+{
+    filename = QFileInfo(filename).absoluteFilePath();
+
+    bool have = false;
+    for (unsigned int i = 0; i < m_files.size(); ++i) {
+        if (m_files[i] == filename) {
+            have = true;
+            break;
+        }
+    }
+    
+    if (!have) {
+        m_files.push_front(filename);
+    } else {
+        std::deque<QString> newfiles;
+        newfiles.push_back(filename);
+        for (unsigned int i = 0; i < m_files.size(); ++i) {
+            if (m_files[i] == filename) continue;
+            newfiles.push_back(m_files[i]);
+        }
+    }
+
+    truncateAndWrite();
+    emit recentFilesChanged();
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/RecentFiles.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,56 @@
+/* -*- 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 _RECENT_FILES_H_
+#define _RECENT_FILES_H_
+
+#include <QObject>
+#include <QString>
+#include <vector>
+#include <deque>
+
+class RecentFiles : public QObject
+{
+    Q_OBJECT
+
+public:
+    // The maxFileCount argument will only be used the first time this is called
+    static RecentFiles *getInstance(int maxFileCount = 10);
+
+    virtual ~RecentFiles();
+
+    int getMaxFileCount() const { return m_maxFileCount; }
+
+    std::vector<QString> getRecentFiles() const;
+    
+    void addFile(QString filename);
+
+signals:
+    void recentFilesChanged();
+
+protected:
+    RecentFiles(int maxFileCount);
+
+    int m_maxFileCount;
+    std::deque<QString> m_files;
+
+    void readFiles();
+    void writeFiles();
+    void truncateAndWrite();
+
+    static RecentFiles *m_instance;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/SVFileReader.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,1005 @@
+/* -*- 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 "SVFileReader.h"
+
+#include "base/Layer.h"
+#include "base/View.h"
+#include "base/PlayParameters.h"
+#include "base/PlayParameterRepository.h"
+
+#include "AudioFileReaderFactory.h"
+
+#include "model/WaveFileModel.h"
+#include "model/DenseThreeDimensionalModel.h"
+#include "model/SparseOneDimensionalModel.h"
+#include "model/SparseTimeValueModel.h"
+#include "model/NoteModel.h"
+#include "model/TextModel.h"
+
+#include "widgets/Pane.h"
+
+#include "main/Document.h"
+
+#include <QString>
+#include <QMessageBox>
+#include <QFileDialog>
+
+#include <iostream>
+
+SVFileReader::SVFileReader(Document *document,
+			   SVFileReaderPaneCallback &callback) :
+    m_document(document),
+    m_paneCallback(callback),
+    m_currentPane(0),
+    m_currentDataset(0),
+    m_currentDerivedModel(0),
+    m_currentPlayParameters(0),
+    m_datasetSeparator(" "),
+    m_inRow(false),
+    m_rowNumber(0),
+    m_ok(false)
+{
+}
+
+void
+SVFileReader::parse(const QString &xmlData)
+{
+    QXmlInputSource inputSource;
+    inputSource.setData(xmlData);
+    parse(inputSource);
+}
+
+void
+SVFileReader::parse(QXmlInputSource &inputSource)
+{
+    QXmlSimpleReader reader;
+    reader.setContentHandler(this);
+    reader.setErrorHandler(this);
+    m_ok = reader.parse(inputSource);
+}    
+
+bool
+SVFileReader::isOK()
+{
+    return m_ok;
+}
+	
+SVFileReader::~SVFileReader()
+{
+    if (!m_awaitingDatasets.empty()) {
+	std::cerr << "WARNING: SV-XML: File ended with "
+		  << m_awaitingDatasets.size() << " unfilled model dataset(s)"
+		  << std::endl;
+    }
+
+    std::set<Model *> unaddedModels;
+
+    for (std::map<int, Model *>::iterator i = m_models.begin();
+	 i != m_models.end(); ++i) {
+	if (m_addedModels.find(i->second) == m_addedModels.end()) {
+	    unaddedModels.insert(i->second);
+	}
+    }
+
+    if (!unaddedModels.empty()) {
+	std::cerr << "WARNING: SV-XML: File contained "
+		  << unaddedModels.size() << " unused models"
+		  << std::endl;
+	while (!unaddedModels.empty()) {
+	    delete *unaddedModels.begin();
+	    unaddedModels.erase(unaddedModels.begin());
+	}
+    }	
+}
+
+bool
+SVFileReader::startElement(const QString &, const QString &,
+			   const QString &qName,
+			   const QXmlAttributes &attributes)
+{
+    QString name = qName.toLower();
+
+    bool ok = false;
+
+    // Valid element names:
+    //
+    // sv
+    // data
+    // dataset
+    // display
+    // derivation
+    // playparameters
+    // layer
+    // model
+    // point
+    // row
+    // view
+    // window
+
+    if (name == "sv") {
+
+	// nothing needed
+	ok = true;
+
+    } else if (name == "data") {
+
+	// nothing needed
+	m_inData = true;
+	ok = true;
+
+    } else if (name == "display") {
+
+	// nothing needed
+	ok = true;
+
+    } else if (name == "window") {
+
+	ok = readWindow(attributes);
+
+    } else if (name == "model") {
+
+	ok = readModel(attributes);
+    
+    } else if (name == "dataset") {
+	
+	ok = readDatasetStart(attributes);
+
+    } else if (name == "bin") {
+	
+	ok = addBinToDataset(attributes);
+    
+    } else if (name == "point") {
+	
+	ok = addPointToDataset(attributes);
+
+    } else if (name == "row") {
+
+	ok = addRowToDataset(attributes);
+
+    } else if (name == "layer") {
+
+        addUnaddedModels(); // all models must be specified before first layer
+	ok = readLayer(attributes);
+
+    } else if (name == "view") {
+
+	m_inView = true;
+	ok = readView(attributes);
+
+    } else if (name == "derivation") {
+
+	ok = readDerivation(attributes);
+
+    } else if (name == "playparameters") {
+        
+        ok = readPlayParameters(attributes);
+
+    } else if (name == "plugin") {
+
+	ok = readPlugin(attributes);
+
+    } else if (name == "selections") {
+
+	m_inSelections = true;
+	ok = true;
+
+    } else if (name == "selection") {
+
+	ok = readSelection(attributes);
+    }
+
+    if (!ok) {
+	std::cerr << "WARNING: SV-XML: Failed to completely process element \""
+		  << name.toLocal8Bit().data() << "\"" << std::endl;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::characters(const QString &text)
+{
+    bool ok = false;
+
+    if (m_inRow) {
+	ok = readRowData(text);
+	if (!ok) {
+	    std::cerr << "WARNING: SV-XML: Failed to read row data content for row " << m_rowNumber << std::endl;
+	}
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::endElement(const QString &, const QString &,
+			 const QString &qName)
+{
+    QString name = qName.toLower();
+
+    if (name == "dataset") {
+
+	if (m_currentDataset) {
+	    
+	    bool foundInAwaiting = false;
+
+	    for (std::map<int, int>::iterator i = m_awaitingDatasets.begin();
+		 i != m_awaitingDatasets.end(); ++i) {
+		if (m_models[i->second] == m_currentDataset) {
+		    m_awaitingDatasets.erase(i);
+		    foundInAwaiting = true;
+		    break;
+		}
+	    }
+
+	    if (!foundInAwaiting) {
+		std::cerr << "WARNING: SV-XML: Dataset precedes model, or no model uses dataset" << std::endl;
+	    }
+	}
+
+	m_currentDataset = 0;
+
+    } else if (name == "data") {
+
+        addUnaddedModels();
+	m_inData = false;
+
+    } else if (name == "derivation") {
+        
+        if (m_currentDerivedModel) {
+            m_document->addDerivedModel(m_currentTransform,
+                                        m_document->getMainModel(), //!!!
+                                        m_currentTransformChannel,
+                                        m_currentDerivedModel,
+                                        m_currentTransformConfiguration);
+            m_addedModels.insert(m_currentDerivedModel);
+            m_currentDerivedModel = 0;
+            m_currentTransform = "";
+            m_currentTransformConfiguration = "";
+        }
+
+    } else if (name == "row") {
+	m_inRow = false;
+    } else if (name == "view") {
+	m_inView = false;
+    } else if (name == "selections") {
+	m_inSelections = false;
+    } else if (name == "playparameters") {
+        m_currentPlayParameters = 0;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::error(const QXmlParseException &exception)
+{
+    m_errorString =
+	QString("ERROR: SV-XML: %1 at line %2, column %3")
+	.arg(exception.message())
+	.arg(exception.lineNumber())
+	.arg(exception.columnNumber());
+    std::cerr << m_errorString.toLocal8Bit().data() << std::endl;
+    return QXmlDefaultHandler::error(exception);
+}
+
+bool
+SVFileReader::fatalError(const QXmlParseException &exception)
+{
+    m_errorString =
+	QString("FATAL ERROR: SV-XML: %1 at line %2, column %3")
+	.arg(exception.message())
+	.arg(exception.lineNumber())
+	.arg(exception.columnNumber());
+    std::cerr << m_errorString.toLocal8Bit().data() << std::endl;
+    return QXmlDefaultHandler::fatalError(exception);
+}
+
+
+#define READ_MANDATORY(TYPE, NAME, CONVERSION)		      \
+    TYPE NAME = attributes.value(#NAME).trimmed().CONVERSION(&ok); \
+    if (!ok) { \
+	std::cerr << "WARNING: SV-XML: Missing or invalid mandatory " #TYPE " attribute \"" #NAME "\"" << std::endl; \
+	return false; \
+    }
+
+bool
+SVFileReader::readWindow(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, width, toInt);
+    READ_MANDATORY(int, height, toInt);
+
+    m_paneCallback.setWindowSize(width, height);
+    return true;
+}
+
+void
+SVFileReader::addUnaddedModels()
+{
+    std::set<Model *> unaddedModels;
+    
+    for (std::map<int, Model *>::iterator i = m_models.begin();
+         i != m_models.end(); ++i) {
+        if (m_addedModels.find(i->second) == m_addedModels.end()) {
+            unaddedModels.insert(i->second);
+        }
+    }
+    
+    for (std::set<Model *>::iterator i = unaddedModels.begin();
+         i != unaddedModels.end(); ++i) {
+        m_document->addImportedModel(*i);
+        m_addedModels.insert(*i);
+    }
+}
+
+bool
+SVFileReader::readModel(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, id, toInt);
+
+    if (m_models.find(id) != m_models.end()) {
+	std::cerr << "WARNING: SV-XML: Ignoring duplicate model id " << id
+		  << std::endl;
+	return false;
+    }
+
+    QString name = attributes.value("name");
+
+    READ_MANDATORY(int, sampleRate, toInt);
+
+    QString type = attributes.value("type").trimmed();
+    bool mainModel = (attributes.value("mainModel").trimmed() == "true");
+    
+    if (type == "wavefile") {
+	
+	QString file = attributes.value("file");
+	WaveFileModel *model = new WaveFileModel(file);
+
+	while (!model->isOK()) {
+
+	    delete model;
+	    model = 0;
+
+	    if (QMessageBox::question(0,
+				      QMessageBox::tr("Failed to open file"),
+				      QMessageBox::tr("Audio file \"%1\" could not be opened.\nLocate it?").arg(file),
+				      QMessageBox::Ok,
+				      QMessageBox::Cancel) == QMessageBox::Ok) {
+
+		QString path = QFileDialog::getOpenFileName
+		    (0, QFileDialog::tr("Locate file \"%1\"").arg(QFileInfo(file).fileName()), file,
+		     QFileDialog::tr("Audio files (%1)\nAll files (*.*)")
+		     .arg(AudioFileReaderFactory::getKnownExtensions()));
+
+		if (path != "") {
+		    model = new WaveFileModel(path);
+		} else {
+		    return false;
+		}
+	    } else {
+		return false;
+	    }
+	}
+
+	m_models[id] = model;
+	if (mainModel) {
+	    m_document->setMainModel(model);
+	    m_addedModels.insert(model);
+	}
+	// Derived models will be added when their derivation
+	// is found.
+
+	return true;
+
+    } else if (type == "dense") {
+	
+	READ_MANDATORY(int, dimensions, toInt);
+		    
+	// Currently the only dense model we support here
+	// is the dense 3d model.  Dense time-value models
+	// are always file-backed waveform data, at this
+	// point, and they come in as the wavefile model
+	// type above.
+	
+	if (dimensions == 3) {
+	    
+	    READ_MANDATORY(int, windowSize, toInt);
+	    READ_MANDATORY(int, yBinCount, toInt);
+	    
+	    DenseThreeDimensionalModel *model =
+		new DenseThreeDimensionalModel(sampleRate, windowSize, yBinCount);
+	    
+	    float minimum = attributes.value("minimum").trimmed().toFloat(&ok);
+	    if (ok) model->setMinimumLevel(minimum);
+	    
+	    float maximum = attributes.value("maximum").trimmed().toFloat(&ok);
+	    if (ok) model->setMaximumLevel(maximum);
+
+	    int dataset = attributes.value("dataset").trimmed().toInt(&ok);
+	    if (ok) m_awaitingDatasets[dataset] = id;
+
+	    m_models[id] = model;
+	    return true;
+
+	} else {
+
+	    std::cerr << "WARNING: SV-XML: Unexpected dense model dimension ("
+		      << dimensions << ")" << std::endl;
+	}
+    } else if (type == "sparse") {
+
+	READ_MANDATORY(int, dimensions, toInt);
+		  
+	if (dimensions == 1) {
+	    
+	    READ_MANDATORY(int, resolution, toInt);
+
+	    SparseOneDimensionalModel *model = new SparseOneDimensionalModel
+		(sampleRate, resolution);
+	    m_models[id] = model;
+
+	    int dataset = attributes.value("dataset").trimmed().toInt(&ok);
+	    if (ok) m_awaitingDatasets[dataset] = id;
+
+	    return true;
+
+	} else if (dimensions == 2 || dimensions == 3) {
+	    
+	    READ_MANDATORY(int, resolution, toInt);
+
+	    float minimum = attributes.value("minimum").trimmed().toFloat(&ok);
+	    float maximum = attributes.value("maximum").trimmed().toFloat(&ok);
+	    float valueQuantization =
+		attributes.value("valueQuantization").trimmed().toFloat(&ok);
+
+	    bool notifyOnAdd = (attributes.value("notifyOnAdd") == "true");
+
+            QString units = attributes.value("units");
+
+	    if (dimensions == 2) {
+		if (attributes.value("subtype") == "text") {
+		    TextModel *model = new TextModel
+			(sampleRate, resolution, notifyOnAdd);
+		    m_models[id] = model;
+		} else {
+		    SparseTimeValueModel *model = new SparseTimeValueModel
+			(sampleRate, resolution, minimum, maximum, notifyOnAdd);
+                    model->setScaleUnits(units);
+		    m_models[id] = model;
+		}
+	    } else {
+		NoteModel *model = new NoteModel
+		    (sampleRate, resolution, minimum, maximum, notifyOnAdd);
+		model->setValueQuantization(valueQuantization);
+                model->setScaleUnits(units);
+		m_models[id] = model;
+	    }
+
+	    int dataset = attributes.value("dataset").trimmed().toInt(&ok);
+	    if (ok) m_awaitingDatasets[dataset] = id;
+
+	    return true;
+
+	} else {
+
+	    std::cerr << "WARNING: SV-XML: Unexpected sparse model dimension ("
+		      << dimensions << ")" << std::endl;
+	}
+    } else {
+
+	std::cerr << "WARNING: SV-XML: Unexpected model type \""
+		  << type.toLocal8Bit().data() << "\" for model id" << id << std::endl;
+    }
+
+    return false;
+}
+
+bool
+SVFileReader::readView(const QXmlAttributes &attributes)
+{
+    QString type = attributes.value("type");
+    m_currentPane = 0;
+    
+    if (type != "pane") {
+	std::cerr << "WARNING: SV-XML: Unexpected view type \""
+		  << type.toLocal8Bit().data() << "\"" << std::endl;
+	return false;
+    }
+
+    m_currentPane = m_paneCallback.addPane();
+
+    if (!m_currentPane) {
+	std::cerr << "WARNING: SV-XML: Internal error: Failed to add pane!"
+		  << std::endl;
+	return false;
+    }
+
+    bool ok = false;
+
+    View *view = m_currentPane;
+
+    // The view properties first
+
+    READ_MANDATORY(size_t, centre, toUInt);
+    READ_MANDATORY(size_t, zoom, toUInt);
+    READ_MANDATORY(int, followPan, toInt);
+    READ_MANDATORY(int, followZoom, toInt);
+    READ_MANDATORY(int, light, toInt);
+    QString tracking = attributes.value("tracking");
+
+    // Specify the follow modes before we set the actual values
+    view->setFollowGlobalPan(followPan);
+    view->setFollowGlobalZoom(followZoom);
+    view->setPlaybackFollow(tracking == "scroll" ? View::PlaybackScrollContinuous :
+			    tracking == "page" ? View::PlaybackScrollPage
+			    : View::PlaybackIgnore);
+
+    // Then set these values
+    view->setCentreFrame(centre);
+    view->setZoomLevel(zoom);
+    view->setLightBackground(light);
+
+    // And pane properties
+    READ_MANDATORY(int, centreLineVisible, toInt);
+    m_currentPane->setCentreLineVisible(centreLineVisible);
+
+    int height = attributes.value("height").toInt(&ok);
+    if (ok) {
+	m_currentPane->resize(m_currentPane->width(), height);
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readLayer(const QXmlAttributes &attributes)
+{
+    QString type = attributes.value("type");
+
+    int id;
+    bool ok = false;
+    id = attributes.value("id").trimmed().toInt(&ok);
+
+    if (!ok) {
+	std::cerr << "WARNING: SV-XML: No layer id for layer of type \""
+		  << type.toLocal8Bit().data()
+		  << "\"" << std::endl;
+	return false;
+    }
+
+    Layer *layer = 0;
+    bool isNewLayer = false;
+
+    // Layers are expected to be defined in layer elements in the data
+    // section, and referred to in layer elements in the view
+    // sections.  So if we're in the data section, we expect this
+    // layer not to exist already; if we're in the view section, we
+    // expect it to exist.
+
+    if (m_inData) {
+
+	if (m_layers.find(id) != m_layers.end()) {
+	    std::cerr << "WARNING: SV-XML: Ignoring duplicate layer id " << id
+		      << " in data section" << std::endl;
+	    return false;
+	}
+
+	layer = m_layers[id] = m_document->createLayer
+	    (LayerFactory::getInstance()->getLayerTypeForName(type));
+
+	if (layer) {
+	    m_layers[id] = layer;
+	    isNewLayer = true;
+	}
+
+    } else {
+
+	if (!m_currentPane) {
+	    std::cerr << "WARNING: SV-XML: No current pane for layer " << id
+		      << " in view section" << std::endl;
+	    return false;
+	}
+
+	if (m_layers.find(id) != m_layers.end()) {
+	    
+	    layer = m_layers[id];
+	
+	} else {
+	    std::cerr << "WARNING: SV-XML: Layer id " << id 
+		      << " in view section has not been defined -- defining it here"
+		      << std::endl;
+
+	    layer = m_document->createLayer
+		(LayerFactory::getInstance()->getLayerTypeForName(type));
+
+	    if (layer) {
+		m_layers[id] = layer;
+		isNewLayer = true;
+	    }
+	}
+    }
+	    
+    if (!layer) {
+	std::cerr << "WARNING: SV-XML: Failed to add layer of type \""
+		  << type.toLocal8Bit().data()
+		  << "\"" << std::endl;
+	return false;
+    }
+
+    if (isNewLayer) {
+
+	QString name = attributes.value("name");
+	layer->setObjectName(name);
+
+	int modelId;
+	bool modelOk = false;
+	modelId = attributes.value("model").trimmed().toInt(&modelOk);
+
+	if (modelOk) {
+	    if (m_models.find(modelId) != m_models.end()) {
+		Model *model = m_models[modelId];
+		m_document->setModel(layer, model);
+	    } else {
+		std::cerr << "WARNING: SV-XML: Unknown model id " << modelId
+			  << " in layer definition" << std::endl;
+	    }
+	}
+
+	layer->setProperties(attributes);
+    }
+
+    if (!m_inData && m_currentPane) {
+	m_document->addLayerToView(m_currentPane, layer);
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readDatasetStart(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, id, toInt);
+    READ_MANDATORY(int, dimensions, toInt);
+    
+    if (m_awaitingDatasets.find(id) == m_awaitingDatasets.end()) {
+	std::cerr << "WARNING: SV-XML: Unwanted dataset " << id << std::endl;
+	return false;
+    }
+    
+    int modelId = m_awaitingDatasets[id];
+    
+    Model *model = 0;
+    if (m_models.find(modelId) != m_models.end()) {
+	model = m_models[modelId];
+    } else {
+	std::cerr << "WARNING: SV-XML: Internal error: Unknown model " << modelId
+		  << " expecting dataset " << id << std::endl;
+	return false;
+    }
+
+    bool good = false;
+
+    switch (dimensions) {
+    case 1:
+	if (dynamic_cast<SparseOneDimensionalModel *>(model)) good = true;
+	break;
+
+    case 2:
+	if (dynamic_cast<SparseTimeValueModel *>(model)) good = true;
+	else if (dynamic_cast<TextModel *>(model)) good = true;
+	break;
+
+    case 3:
+	if (dynamic_cast<NoteModel *>(model)) good = true;
+	else if (dynamic_cast<DenseThreeDimensionalModel *>(model)) {
+	    m_datasetSeparator = attributes.value("separator");
+	    good = true;
+	}
+	break;
+    }
+
+    if (!good) {
+	std::cerr << "WARNING: SV-XML: Model id " << modelId << " has wrong number of dimensions for " << dimensions << "-D dataset " << id << std::endl;
+	m_currentDataset = 0;
+	return false;
+    }
+
+    m_currentDataset = model;
+    return true;
+}
+
+bool
+SVFileReader::addPointToDataset(const QXmlAttributes &attributes)
+{
+    bool ok = false;
+
+    READ_MANDATORY(int, frame, toInt);
+
+    SparseOneDimensionalModel *sodm = dynamic_cast<SparseOneDimensionalModel *>
+	(m_currentDataset);
+
+    if (sodm) {
+	QString label = attributes.value("label");
+	sodm->addPoint(SparseOneDimensionalModel::Point(frame, label));
+	return true;
+    }
+
+    SparseTimeValueModel *stvm = dynamic_cast<SparseTimeValueModel *>
+	(m_currentDataset);
+
+    if (stvm) {
+	float value = 0.0;
+	value = attributes.value("value").trimmed().toFloat(&ok);
+	QString label = attributes.value("label");
+	stvm->addPoint(SparseTimeValueModel::Point(frame, value, label));
+	return ok;
+    }
+	
+    NoteModel *nm = dynamic_cast<NoteModel *>(m_currentDataset);
+
+    if (nm) {
+	float value = 0.0;
+	value = attributes.value("value").trimmed().toFloat(&ok);
+	float duration = 0.0;
+	duration = attributes.value("duration").trimmed().toFloat(&ok);
+	QString label = attributes.value("label");
+	nm->addPoint(NoteModel::Point(frame, value, duration, label));
+	return ok;
+    }
+
+    TextModel *tm = dynamic_cast<TextModel *>(m_currentDataset);
+
+    if (tm) {
+	float height = 0.0;
+	height = attributes.value("height").trimmed().toFloat(&ok);
+	QString label = attributes.value("label");
+	tm->addPoint(TextModel::Point(frame, height, label));
+	return ok;
+    }
+
+    std::cerr << "WARNING: SV-XML: Point element found in non-point dataset" << std::endl;
+
+    return false;
+}
+
+bool
+SVFileReader::addBinToDataset(const QXmlAttributes &attributes)
+{
+    DenseThreeDimensionalModel *dtdm = dynamic_cast<DenseThreeDimensionalModel *>
+	(m_currentDataset);
+
+    if (dtdm) {
+
+	bool ok = false;
+	int n = attributes.value("number").trimmed().toInt(&ok);
+	if (!ok) {
+	    std::cerr << "WARNING: SV-XML: Missing or invalid bin number"
+		      << std::endl;
+	    return false;
+	}
+
+	QString name = attributes.value("name");
+
+	dtdm->setBinName(n, name);
+	return true;
+    }
+
+    std::cerr << "WARNING: SV-XML: Bin definition found in incompatible dataset" << std::endl;
+
+    return false;
+}
+
+
+bool
+SVFileReader::addRowToDataset(const QXmlAttributes &attributes)
+{
+    m_inRow = false;
+
+    bool ok = false;
+    m_rowNumber = attributes.value("n").trimmed().toInt(&ok);
+    if (!ok) {
+	std::cerr << "WARNING: SV-XML: Missing or invalid row number"
+		  << std::endl;
+	return false;
+    }
+    
+    m_inRow = true;
+
+//    std::cerr << "SV-XML: In row " << m_rowNumber << std::endl;
+    
+    return true;
+}
+
+bool
+SVFileReader::readRowData(const QString &text)
+{
+    DenseThreeDimensionalModel *dtdm = dynamic_cast<DenseThreeDimensionalModel *>
+	(m_currentDataset);
+
+    bool warned = false;
+
+    if (dtdm) {
+	QStringList data = text.split(m_datasetSeparator);
+
+	DenseThreeDimensionalModel::BinValueSet values;
+
+	for (QStringList::iterator i = data.begin(); i != data.end(); ++i) {
+
+	    if (values.size() == dtdm->getYBinCount()) {
+		if (!warned) {
+		    std::cerr << "WARNING: SV-XML: Too many y-bins in 3-D dataset row "
+			      << m_rowNumber << std::endl;
+		    warned = true;
+		}
+	    }
+
+	    bool ok;
+	    float value = i->toFloat(&ok);
+	    if (!ok) {
+		std::cerr << "WARNING: SV-XML: Bad floating-point value "
+			  << i->toLocal8Bit().data()
+			  << " in row data" << std::endl;
+	    } else {
+		values.push_back(value);
+	    }
+	}
+
+	size_t windowStartFrame = m_rowNumber * dtdm->getWindowSize();
+
+	dtdm->setBinValues(windowStartFrame, values);
+	return true;
+    }
+
+    std::cerr << "WARNING: SV-XML: Row data found in non-row dataset" << std::endl;
+
+    return false;
+}
+
+bool
+SVFileReader::readDerivation(const QXmlAttributes &attributes)
+{
+    int modelId = 0;
+    bool modelOk = false;
+    modelId = attributes.value("model").trimmed().toInt(&modelOk);
+
+    if (!modelOk) {
+	std::cerr << "WARNING: SV-XML: No model id specified for derivation" << std::endl;
+	return false;
+    }
+    
+    QString transform = attributes.value("transform");
+    
+    if (m_models.find(modelId) != m_models.end()) {
+
+        m_currentDerivedModel = m_models[modelId];
+        m_currentTransform = transform;
+        m_currentTransformConfiguration = "";
+
+        bool ok = false;
+        int channel = attributes.value("channel").trimmed().toInt(&ok);
+        if (ok) m_currentTransformChannel = channel;
+        else m_currentTransformChannel = -1;
+
+    } else {
+	std::cerr << "WARNING: SV-XML: Unknown derived model " << modelId
+		  << " for transform \"" << transform.toLocal8Bit().data() << "\""
+		  << std::endl;
+        return false;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readPlayParameters(const QXmlAttributes &attributes)
+{
+    m_currentPlayParameters = 0;
+
+    int modelId = 0;
+    bool modelOk = false;
+    modelId = attributes.value("model").trimmed().toInt(&modelOk);
+
+    if (!modelOk) {
+	std::cerr << "WARNING: SV-XML: No model id specified for play parameters" << std::endl;
+	return false;
+    }
+
+    if (m_models.find(modelId) != m_models.end()) {
+
+        bool ok = false;
+
+        PlayParameters *parameters = PlayParameterRepository::getInstance()->
+            getPlayParameters(m_models[modelId]);
+
+        if (!parameters) {
+            std::cerr << "WARNING: SV-XML: Play parameters for model "
+                      << modelId
+                      << " not found - has model been added to document?"
+                      << std::endl;
+            return false;
+        }
+        
+        bool muted = (attributes.value("mute").trimmed() == "true");
+        if (ok) parameters->setPlayMuted(muted);
+        
+        float pan = attributes.value("pan").toFloat(&ok);
+        if (ok) parameters->setPlayPan(pan);
+        
+        float gain = attributes.value("gain").toFloat(&ok);
+        if (ok) parameters->setPlayGain(gain);
+        
+        QString pluginId = attributes.value("pluginId");
+        if (pluginId != "") parameters->setPlayPluginId(pluginId);
+        
+        m_currentPlayParameters = parameters;
+
+//        std::cerr << "Current play parameters for model: " << m_models[modelId] << ": " << m_currentPlayParameters << std::endl;
+
+    } else {
+
+	std::cerr << "WARNING: SV-XML: Unknown model " << modelId
+		  << " for play parameters" << std::endl;
+        return false;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readPlugin(const QXmlAttributes &attributes)
+{
+    if (!m_currentDerivedModel && !m_currentPlayParameters) {
+        std::cerr << "WARNING: SV-XML: Plugin found outside derivation or play parameters" << std::endl;
+        return false;
+    }
+
+    QString configurationXml = "<plugin";
+    
+    for (int i = 0; i < attributes.length(); ++i) {
+        configurationXml += QString(" %1=\"%2\"")
+            .arg(attributes.qName(i)).arg(attributes.value(i));
+    }
+
+    configurationXml += "/>";
+
+    if (m_currentPlayParameters) {
+        m_currentPlayParameters->setPlayPluginConfiguration(configurationXml);
+    } else {
+        m_currentTransformConfiguration += configurationXml;
+    }
+
+    return true;
+}
+
+bool
+SVFileReader::readSelection(const QXmlAttributes &attributes)
+{
+    bool ok;
+
+    READ_MANDATORY(int, start, toInt);
+    READ_MANDATORY(int, end, toInt);
+
+    m_paneCallback.addSelection(start, end);
+
+    return true;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/SVFileReader.h	Mon Jul 31 11:49:58 2006 +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 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 _SV_FILE_READER_H_
+#define _SV_FILE_READER_H_
+
+#include "layer/LayerFactory.h"
+#include "transform/Transform.h"
+
+#include <QXmlDefaultHandler>
+
+#include <map>
+
+class Pane;
+class Model;
+class Document;
+class PlayParameters;
+
+class SVFileReaderPaneCallback
+{
+public:
+    virtual Pane *addPane() = 0;
+    virtual void setWindowSize(int width, int height) = 0;
+    virtual void addSelection(int start, int end) = 0;
+};
+
+class SVFileReader : public QXmlDefaultHandler
+{
+public:
+    SVFileReader(Document *document,
+		 SVFileReaderPaneCallback &callback);
+    virtual ~SVFileReader();
+
+    void parse(const QString &xmlData);
+    void parse(QXmlInputSource &source);
+
+    bool isOK();
+    QString getErrorString() const { return m_errorString; }
+
+    // For loading a single layer onto an existing pane
+    void setCurrentPane(Pane *pane) { m_currentPane = pane; }
+    
+    virtual bool startElement(const QString &namespaceURI,
+			      const QString &localName,
+			      const QString &qName,
+			      const QXmlAttributes& atts);
+
+    virtual bool characters(const QString &);
+
+    virtual bool endElement(const QString &namespaceURI,
+			    const QString &localName,
+			    const QString &qName);
+
+    bool error(const QXmlParseException &exception);
+    bool fatalError(const QXmlParseException &exception);
+
+protected:
+    bool readWindow(const QXmlAttributes &);
+    bool readModel(const QXmlAttributes &);
+    bool readView(const QXmlAttributes &);
+    bool readLayer(const QXmlAttributes &);
+    bool readDatasetStart(const QXmlAttributes &);
+    bool addBinToDataset(const QXmlAttributes &);
+    bool addPointToDataset(const QXmlAttributes &);
+    bool addRowToDataset(const QXmlAttributes &);
+    bool readRowData(const QString &);
+    bool readDerivation(const QXmlAttributes &);
+    bool readPlayParameters(const QXmlAttributes &);
+    bool readPlugin(const QXmlAttributes &);
+    bool readSelection(const QXmlAttributes &);
+    void addUnaddedModels();
+
+    Document *m_document;
+    SVFileReaderPaneCallback &m_paneCallback;
+    Pane *m_currentPane;
+    std::map<int, Layer *> m_layers;
+    std::map<int, Model *> m_models;
+    std::set<Model *> m_addedModels;
+    std::map<int, int> m_awaitingDatasets; // map dataset id -> model id
+    Model *m_currentDataset;
+    Model *m_currentDerivedModel;
+    PlayParameters *m_currentPlayParameters;
+    QString m_currentTransform;
+    int m_currentTransformChannel;
+    QString m_currentTransformConfiguration;
+    QString m_datasetSeparator;
+    bool m_inRow;
+    bool m_inView;
+    bool m_inData;
+    bool m_inSelections;
+    int m_rowNumber;
+    QString m_errorString;
+    bool m_ok;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/WavFileReader.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,112 @@
+/* -*- 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 "WavFileReader.h"
+
+#include <iostream>
+
+WavFileReader::WavFileReader(QString path) :
+    m_file(0),
+    m_path(path),
+    m_buffer(0),
+    m_bufsiz(0),
+    m_lastStart(0),
+    m_lastCount(0)
+{
+    m_frameCount = 0;
+    m_channelCount = 0;
+    m_sampleRate = 0;
+
+    m_fileInfo.format = 0;
+    m_fileInfo.frames = 0;
+    m_file = sf_open(m_path.toLocal8Bit(), SFM_READ, &m_fileInfo);
+
+    if (!m_file || m_fileInfo.frames <= 0 || m_fileInfo.channels <= 0) {
+	std::cerr << "WavFileReader::initialize: Failed to open file ("
+		  << sf_strerror(m_file) << ")" << std::endl;
+
+	if (m_file) {
+	    m_error = QString("Couldn't load audio file '%1':\n%2")
+		.arg(m_path).arg(sf_strerror(m_file));
+	} else {
+	    m_error = QString("Failed to open audio file '%1'")
+		.arg(m_path);
+	}
+	return;
+    }
+
+    m_frameCount = m_fileInfo.frames;
+    m_channelCount = m_fileInfo.channels;
+    m_sampleRate = m_fileInfo.samplerate;
+}
+
+WavFileReader::~WavFileReader()
+{
+    if (m_file) sf_close(m_file);
+}
+
+void
+WavFileReader::getInterleavedFrames(size_t start, size_t count,
+				    SampleBlock &results) const
+{
+    results.clear();
+    if (!m_file || !m_channelCount) return;
+    if (count == 0) return;
+
+    if ((long)start >= m_fileInfo.frames) {
+	return;
+    }
+
+    if (long(start + count) > m_fileInfo.frames) {
+	count = m_fileInfo.frames - start;
+    }
+
+    sf_count_t readCount = 0;
+
+    m_mutex.lock();
+
+    if (start != m_lastStart || count != m_lastCount) {
+
+	if (sf_seek(m_file, start, SEEK_SET) < 0) {
+	    m_mutex.unlock();
+	    return;
+	}
+	
+	if (count * m_fileInfo.channels > m_bufsiz) {
+//	    std::cerr << "WavFileReader: Reallocating buffer for " << count
+//		      << " frames, " << m_fileInfo.channels << " channels: "
+//		      << m_bufsiz << " floats" << std::endl;
+	    m_bufsiz = count * m_fileInfo.channels;
+	    delete[] m_buffer;
+	    m_buffer = new float[m_bufsiz];
+	}
+	
+	if ((readCount = sf_readf_float(m_file, m_buffer, count)) < 0) {
+	    m_mutex.unlock();
+	    return;
+	}
+
+	m_lastStart = start;
+	m_lastCount = readCount;
+    }
+
+    for (size_t i = 0; i < count * m_fileInfo.channels; ++i) {
+	results.push_back(m_buffer[i]);
+    }
+
+    m_mutex.unlock();
+    return;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/WavFileReader.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,53 @@
+/* -*- 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 _WAV_FILE_READER_H_
+#define _WAV_FILE_READER_H_
+
+#include "AudioFileReader.h"
+
+#include <sndfile.h>
+#include <QMutex>
+
+class WavFileReader : public AudioFileReader
+{
+public:
+    WavFileReader(QString path);
+    virtual ~WavFileReader();
+
+    virtual QString getError() const { return m_error; }
+
+    /** 
+     * Must be safe to call from multiple threads with different
+     * arguments on the same object at the same time.
+     */
+    virtual void getInterleavedFrames(size_t start, size_t count,
+				      SampleBlock &frames) const;
+    
+protected:
+    SF_INFO m_fileInfo;
+    SNDFILE *m_file;
+
+    QString m_path;
+    QString m_error;
+
+    mutable QMutex m_mutex;
+    mutable float *m_buffer;
+    mutable size_t m_bufsiz;
+    mutable size_t m_lastStart;
+    mutable size_t m_lastCount;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/WavFileWriter.cpp	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,120 @@
+/* -*- 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 "WavFileWriter.h"
+
+#include "model/DenseTimeValueModel.h"
+#include "base/Selection.h"
+
+#include <QFileInfo>
+#include <sndfile.h>
+
+#include <iostream>
+
+WavFileWriter::WavFileWriter(QString path,
+			     size_t sampleRate,
+			     DenseTimeValueModel *source,
+			     MultiSelection *selection) :
+    m_path(path),
+    m_sampleRate(sampleRate),
+    m_model(source),
+    m_selection(selection)
+{
+}
+
+WavFileWriter::~WavFileWriter()
+{
+}
+
+bool
+WavFileWriter::isOK() const
+{
+    return (m_error.isEmpty());
+}
+
+QString
+WavFileWriter::getError() const
+{
+    return m_error;
+}
+
+void
+WavFileWriter::write()
+{
+    int channels = m_model->getChannelCount();
+
+    SF_INFO fileInfo;
+    fileInfo.samplerate = m_sampleRate;
+    fileInfo.channels = channels;
+    fileInfo.format = SF_FORMAT_WAV | SF_FORMAT_FLOAT;
+    
+    SNDFILE *file = sf_open(m_path.toLocal8Bit(), SFM_WRITE, &fileInfo);
+    if (!file) {
+	std::cerr << "WavFileWriter::write: Failed to open file ("
+		  << sf_strerror(file) << ")" << std::endl;
+	m_error = QString("Failed to open audio file '%1' for writing")
+	    .arg(m_path);
+	return;
+    }
+
+    MultiSelection *selection = m_selection;
+
+    if (!m_selection) {
+	selection = new MultiSelection;
+	selection->setSelection(Selection(m_model->getStartFrame(),
+					  m_model->getEndFrame()));
+    }
+
+    size_t bs = 2048;
+    float *ub = new float[bs]; // uninterleaved buffer (one channel)
+    float *ib = new float[bs * channels]; // interleaved buffer
+
+    for (MultiSelection::SelectionList::iterator i =
+	     selection->getSelections().begin();
+	 i != selection->getSelections().end(); ++i) {
+	
+	size_t f0(i->getStartFrame()), f1(i->getEndFrame());
+
+	for (size_t f = f0; f < f1; f += bs) {
+	    
+	    size_t n = std::min(bs, f1 - f);
+
+	    for (int c = 0; c < channels; ++c) {
+		m_model->getValues(c, f, f + n, ub);
+		for (size_t i = 0; i < n; ++i) {
+		    ib[i * channels + c] = ub[i];
+		}
+	    }	    
+
+	    sf_count_t written = sf_writef_float(file, ib, n);
+
+	    if (written < n) {
+		m_error = QString("Only wrote %1 of %2 frames at file frame %3")
+		    .arg(written).arg(n).arg(f);
+		break;
+	    }
+	}
+    }
+
+    sf_close(file);
+
+    delete[] ub;
+    delete[] ib;
+    if (!m_selection) delete selection;
+}
+
+
+	    
+	    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/WavFileWriter.h	Mon Jul 31 11:49:58 2006 +0000
@@ -0,0 +1,47 @@
+/* -*- 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 _WAV_FILE_WRITER_H_
+#define _WAV_FILE_WRITER_H_
+
+#include <QString>
+
+class DenseTimeValueModel;
+class MultiSelection;
+
+class WavFileWriter
+{
+public:
+    WavFileWriter(QString path, size_t sampleRate,
+		  DenseTimeValueModel *source,
+		  MultiSelection *selection);
+    virtual ~WavFileWriter();
+
+    bool isOK() const;
+
+    virtual QString getError() const;
+
+    void write();
+
+protected:
+    QString m_path;
+    size_t m_sampleRate;
+    DenseTimeValueModel *m_model;
+    MultiSelection *m_selection;
+    QString m_error;
+};
+
+
+#endif