changeset 297:c022976d18e8

* Merge from sv-match-alignment branch (excluding alignment-specific document). - add aggregate wave model (not yet complete enough to be added as a true model in a layer, but there's potential) - add play solo mode - add alignment model -- unused in plain SV - fix two plugin leaks - add m3u playlist support (opens all files at once, potentially hazardous) - fix retrieval of pre-encoded URLs - add ability to resample audio files on import, so as to match rates with other files previously loaded; add preference for same - add preliminary support in transform code for range and rate of transform input - reorganise preferences dialog, move dark-background option to preferences, add option for temporary directory location
author Chris Cannam
date Fri, 28 Sep 2007 13:56:38 +0000
parents 2b6c99b607f1
children 57d7889f626c
files base/Preferences.cpp base/Preferences.h base/Resampler.cpp base/Resampler.h base/Serialiser.cpp base/Serialiser.h base/base.pro data/data.pro data/fileio/AudioFileReader.h data/fileio/AudioFileReaderFactory.cpp data/fileio/AudioFileReaderFactory.h data/fileio/CodedAudioFileReader.cpp data/fileio/CodedAudioFileReader.h data/fileio/MP3FileReader.cpp data/fileio/MP3FileReader.h data/fileio/MatchFileReader.cpp data/fileio/MatchFileReader.h data/fileio/OggVorbisFileReader.cpp data/fileio/OggVorbisFileReader.h data/fileio/PlaylistFileReader.cpp data/fileio/PlaylistFileReader.h data/fileio/QuickTimeFileReader.cpp data/fileio/QuickTimeFileReader.h data/fileio/ResamplingWavFileReader.cpp data/fileio/ResamplingWavFileReader.h data/model/AggregateWaveModel.cpp data/model/AggregateWaveModel.h data/model/AlignmentModel.cpp data/model/AlignmentModel.h data/model/FFTModel.cpp data/model/FFTModel.h data/model/Model.h data/model/RangeSummarisableTimeValueModel.cpp data/model/RangeSummarisableTimeValueModel.h data/model/SparseModel.h data/model/WaveFileModel.cpp data/model/WaveFileModel.h
diffstat 37 files changed, 2238 insertions(+), 140 deletions(-) [+]
line wrap: on
line diff
--- a/base/Preferences.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/base/Preferences.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -40,7 +40,9 @@
     m_propertyBoxLayout(VerticallyStacked),
     m_windowType(HanningWindow),
     m_resampleQuality(1),
-    m_omitRecentTemps(true)
+    m_omitRecentTemps(true),
+    m_tempDirRoot(""),
+    m_resampleOnLoad(true)
 {
     QSettings settings;
     settings.beginGroup("Preferences");
@@ -52,6 +54,13 @@
     m_windowType = WindowType
         (settings.value("window-type", int(HanningWindow)).toInt());
     m_resampleQuality = settings.value("resample-quality", 1).toInt();
+    m_resampleOnLoad = settings.value("resample-on-load", true).toBool();
+    m_backgroundMode = BackgroundMode
+        (settings.value("background-mode", int(BackgroundFromTheme)).toInt());
+    settings.endGroup();
+
+    settings.beginGroup("TempDirectory");
+    m_tempDirRoot = settings.value("create-in", "$HOME").toString();
     settings.endGroup();
 }
 
@@ -69,6 +78,9 @@
     props.push_back("Window Type");
     props.push_back("Resample Quality");
     props.push_back("Omit Temporaries from Recent Files");
+    props.push_back("Resample On Load");
+    props.push_back("Temporary Directory Root");
+    props.push_back("Background Mode");
     return props;
 }
 
@@ -91,7 +103,16 @@
         return tr("Playback resampler type");
     }
     if (name == "Omit Temporaries from Recent Files") {
-        return tr("Omit Temporaries from Recent Files");
+        return tr("Omit temporaries from Recent Files menu");
+    }
+    if (name == "Resample On Load") {
+        return tr("Resample mismatching files on import");
+    }
+    if (name == "Temporary Directory Root") {
+        return tr("Location for cache file directory");
+    }
+    if (name == "Background Mode") {
+        return tr("Background colour preference");
     }
     return name;
 }
@@ -117,6 +138,16 @@
     if (name == "Omit Temporaries from Recent Files") {
         return ToggleProperty;
     }
+    if (name == "Resample On Load") {
+        return ToggleProperty;
+    }
+    if (name == "Temporary Directory Root") {
+        // It's an arbitrary string, we don't have a set of values for this
+        return InvalidProperty;
+    }
+    if (name == "Background Mode") {
+        return ValueProperty;
+    }
     return InvalidProperty;
 }
 
@@ -158,6 +189,13 @@
         if (deflt) *deflt = 1;
     }
 
+    if (name == "Background Mode") {
+        if (min) *min = 0;
+        if (max) *max = 2;
+        if (deflt) *deflt = 0;
+        return int(m_backgroundMode);
+    }        
+
     return 0;
 }
 
@@ -196,6 +234,13 @@
         case SpectrogramZeroPadded: return tr("Zero pad FFT - slow but clear");
         }
     }
+    if (name == "Background Mode") {
+        switch (value) {
+        case BackgroundFromTheme: return tr("Follow desktop theme");
+        case DarkBackground: return tr("Dark background");
+        case LightBackground: return tr("Light background");
+        }
+    }
             
     return "";
 }
@@ -227,6 +272,8 @@
         setResampleQuality(value);
     } else if (name == "Omit Temporaries from Recent Files") {
         setOmitTempsFromRecentFiles(value ? true : false);
+    } else if (name == "Background Mode") {
+        setBackgroundMode(BackgroundMode(value));
     }
 }
 
@@ -311,3 +358,49 @@
         emit propertyChanged("Omit Temporaries from Recent Files");
     }
 }
+
+void
+Preferences::setTemporaryDirectoryRoot(QString root)
+{
+    if (root == QDir::home().absolutePath()) {
+        root = "$HOME";
+    }
+    if (m_tempDirRoot != root) {
+        m_tempDirRoot = root;
+        QSettings settings;
+        settings.beginGroup("TempDirectory");
+        settings.setValue("create-in", root);
+        settings.endGroup();
+        emit propertyChanged("Temporary Directory Root");
+    }
+}
+
+void
+Preferences::setResampleOnLoad(bool resample)
+{
+    if (m_resampleOnLoad != resample) {
+        m_resampleOnLoad = resample;
+        QSettings settings;
+        settings.beginGroup("Preferences");
+        settings.setValue("resample-on-load", resample);
+        settings.endGroup();
+        emit propertyChanged("Resample On Load");
+    }
+}
+
+void
+Preferences::setBackgroundMode(BackgroundMode mode)
+{
+    if (m_backgroundMode != mode) {
+
+        m_backgroundMode = mode;
+
+        QSettings settings;
+        settings.beginGroup("Preferences");
+        settings.setValue("background-mode", int(mode));
+        settings.endGroup();
+        emit propertyChanged("Background Mode");
+    }
+}
+
+
--- a/base/Preferences.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/base/Preferences.h	Fri Sep 28 13:56:38 2007 +0000
@@ -56,6 +56,17 @@
 
     bool getOmitTempsFromRecentFiles() const { return m_omitRecentTemps; }
 
+    QString getTemporaryDirectoryRoot() const { return m_tempDirRoot; }
+
+    bool getResampleOnLoad() const { return m_resampleOnLoad; }    
+    
+    enum BackgroundMode {
+        BackgroundFromTheme,
+        DarkBackground,
+        LightBackground 
+    };
+    BackgroundMode getBackgroundMode() const { return m_backgroundMode; }
+
 public slots:
     virtual void setProperty(const PropertyName &, int);
 
@@ -65,6 +76,9 @@
     void setWindowType(WindowType type);
     void setResampleQuality(int quality);
     void setOmitTempsFromRecentFiles(bool omit);
+    void setTemporaryDirectoryRoot(QString tempDirRoot);
+    void setResampleOnLoad(bool);
+    void setBackgroundMode(BackgroundMode mode);
 
 private:
     Preferences(); // may throw DirectoryCreationFailed
@@ -78,6 +92,9 @@
     WindowType m_windowType;
     int m_resampleQuality;
     bool m_omitRecentTemps;
+    QString m_tempDirRoot;
+    bool m_resampleOnLoad;
+    BackgroundMode m_backgroundMode;
 };
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/Resampler.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,186 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+/*
+   This is a modified version of a source file from the 
+   Rubber Band audio timestretcher library.
+   This file copyright 2007 Chris Cannam.
+*/
+
+#include "Resampler.h"
+
+#include <cstdlib>
+#include <cmath>
+
+#include <iostream>
+
+#include <samplerate.h>
+
+class Resampler::D
+{
+public:
+    D(Quality quality, size_t channels, size_t chunkSize);
+    ~D();
+
+    size_t resample(float **in, float **out,
+                    size_t incount, float ratio,
+                    bool final);
+
+    size_t resampleInterleaved(float *in, float *out,
+                               size_t incount, float ratio,
+                               bool final);
+
+    void reset();
+
+protected:
+    SRC_STATE *m_src;
+    float *m_iin;
+    float *m_iout;
+    size_t m_channels;
+    size_t m_iinsize;
+    size_t m_ioutsize;
+};
+
+Resampler::D::D(Quality quality, size_t channels, size_t chunkSize) :
+    m_src(0),
+    m_iin(0),
+    m_iout(0),
+    m_channels(channels),
+    m_iinsize(0),
+    m_ioutsize(0)
+{
+    int err = 0;
+    m_src = src_new(quality == Best ? SRC_SINC_BEST_QUALITY :
+                    quality == Fastest ? SRC_LINEAR :
+                    SRC_SINC_FASTEST,
+                    channels, &err);
+
+    //!!! check err, throw
+
+    if (chunkSize > 0 && m_channels > 1) {
+        //!!! alignment?
+        m_iinsize = chunkSize * m_channels;
+        m_ioutsize = chunkSize * m_channels * 2;
+        m_iin = (float *)malloc(m_iinsize * sizeof(float));
+        m_iout = (float *)malloc(m_ioutsize * sizeof(float));
+    }
+}
+
+Resampler::D::~D()
+{
+    src_delete(m_src);
+    if (m_iinsize > 0) {
+        free(m_iin);
+    }
+    if (m_ioutsize > 0) {
+        free(m_iout);
+    }
+}
+
+size_t
+Resampler::D::resample(float **in, float **out,
+                       size_t incount, float ratio,
+                       bool final)
+{
+    if (m_channels == 1) {
+        return resampleInterleaved(*in, *out, incount, ratio, final);
+    }
+
+    size_t outcount = lrintf(ceilf(incount * ratio));
+
+    if (incount * m_channels > m_iinsize) {
+        m_iinsize = incount * m_channels;
+        m_iin = (float *)realloc(m_iin, m_iinsize * sizeof(float));
+    }
+    if (outcount * m_channels > m_ioutsize) {
+        m_ioutsize = outcount * m_channels;
+        m_iout = (float *)realloc(m_iout, m_ioutsize * sizeof(float));
+    }
+    for (size_t i = 0; i < incount; ++i) {
+        for (size_t c = 0; c < m_channels; ++c) {
+            m_iin[i * m_channels + c] = in[c][i];
+        }
+    }
+    
+    size_t gen = resampleInterleaved(m_iin, m_iout, incount, ratio, final);
+
+    for (size_t i = 0; i < gen; ++i) {
+        for (size_t c = 0; c < m_channels; ++c) {
+            out[c][i] = m_iout[i * m_channels + c];
+        }
+    }
+
+    return gen;
+}
+
+size_t
+Resampler::D::resampleInterleaved(float *in, float *out,
+                                  size_t incount, float ratio,
+                                  bool final)
+{
+    SRC_DATA data;
+
+    size_t outcount = lrintf(ceilf(incount * ratio));
+
+    data.data_in = in;
+    data.data_out = out;
+    data.input_frames = incount;
+    data.output_frames = outcount;
+    data.src_ratio = ratio;
+    data.end_of_input = (final ? 1 : 0);
+
+    int err = src_process(m_src, &data);
+
+    //!!! check err, respond appropriately
+
+    return data.output_frames_gen;
+}
+
+void
+Resampler::D::reset()
+{
+    src_reset(m_src);
+}
+
+Resampler::Resampler(Quality quality, size_t channels, size_t chunkSize)
+{
+    m_d = new D(quality, channels, chunkSize);
+}
+
+Resampler::~Resampler()
+{
+    delete m_d;
+}
+
+size_t 
+Resampler::resample(float **in, float **out,
+                    size_t incount, float ratio,
+                    bool final)
+{
+    return m_d->resample(in, out, incount, ratio, final);
+}
+
+size_t 
+Resampler::resampleInterleaved(float *in, float *out,
+                    size_t incount, float ratio,
+                    bool final)
+{
+    return m_d->resampleInterleaved(in, out, incount, ratio, final);
+}
+
+void
+Resampler::reset()
+{
+    m_d->reset();
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/Resampler.h	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,49 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+/*
+   This is a modified version of a source file from the 
+   Rubber Band audio timestretcher library.
+   This file copyright 2007 Chris Cannam.
+*/
+
+#ifndef _RESAMPLER_H_
+#define _RESAMPLER_H_
+
+#include <sys/types.h>
+
+class Resampler
+{
+public:
+    enum Quality { Best, FastestTolerable, Fastest };
+
+    Resampler(Quality quality, size_t channels, size_t chunkSize = 0);
+    ~Resampler();
+
+    size_t resample(float **in, float **out,
+                    size_t incount, float ratio,
+                    bool final = false);
+
+    size_t resampleInterleaved(float *in, float *out,
+                               size_t incount, float ratio,
+                               bool final = false);
+
+    void reset();
+
+protected:
+    class D;
+    D *m_d;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/Serialiser.cpp	Fri Sep 28 13:56:38 2007 +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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "Serialiser.h"
+
+QMutex
+Serialiser::m_mapMutex;
+
+std::map<QString, QMutex *>
+Serialiser::m_mutexMap;
+
+Serialiser::Serialiser(QString id) :
+    m_id(id)
+{
+    m_mapMutex.lock();
+    
+    if (m_mutexMap.find(m_id) == m_mutexMap.end()) {
+        m_mutexMap[m_id] = new QMutex;
+    }
+
+    m_mutexMap[m_id]->lock();
+    m_mapMutex.unlock();
+}
+
+Serialiser::~Serialiser()
+{
+    m_mutexMap[m_id]->unlock();
+}
+
+
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/Serialiser.h	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,36 @@
+/* -*- 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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _SERIALISER_H_
+#define _SERIALISER_H_
+
+#include <QString>
+#include <QMutex>
+
+#include <map>
+
+class Serialiser
+{
+public:
+    Serialiser(QString id);
+    ~Serialiser();
+
+protected:
+    QString m_id;
+    static QMutex m_mapMutex;
+    static std::map<QString, QMutex *> m_mutexMap;
+};
+
+#endif
--- a/base/base.pro	Fri Sep 21 09:13:11 2007 +0000
+++ b/base/base.pro	Fri Sep 28 13:56:38 2007 +0000
@@ -31,10 +31,12 @@
            RangeMapper.h \
            RealTime.h \
            RecentFiles.h \
+           Resampler.h \
            ResizeableBitset.h \
            RingBuffer.h \
            Scavenger.h \
            Selection.h \
+           Serialiser.h \
            StorageAdviser.h \
            TempDirectory.h \
            TextAbbrev.h \
@@ -60,7 +62,9 @@
            RangeMapper.cpp \
            RealTime.cpp \
            RecentFiles.cpp \
+           Resampler.cpp \
            Selection.cpp \
+           Serialiser.cpp \
            StorageAdviser.cpp \
            TempDirectory.cpp \
            TextAbbrev.cpp \
--- a/data/data.pro	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/data.pro	Fri Sep 28 13:56:38 2007 +0000
@@ -29,14 +29,19 @@
            fileio/DataFileReaderFactory.h \
            fileio/FileFinder.h \
            fileio/FileReadThread.h \
+           fileio/MatchFileReader.h \
            fileio/MatrixFile.h \
            fileio/MIDIFileReader.h \
            fileio/MP3FileReader.h \
            fileio/OggVorbisFileReader.h \
+           fileio/PlaylistFileReader.h \
            fileio/QuickTimeFileReader.h \
            fileio/RemoteFile.h \
+           fileio/ResamplingWavFileReader.h \
            fileio/WavFileReader.h \
            fileio/WavFileWriter.h \
+           model/AggregateWaveModel.h \
+           model/AlignmentModel.h \
            model/DenseThreeDimensionalModel.h \
            model/DenseTimeValueModel.h \
            model/EditableDenseThreeDimensionalModel.h \
@@ -66,14 +71,19 @@
            fileio/DataFileReaderFactory.cpp \
            fileio/FileFinder.cpp \
            fileio/FileReadThread.cpp \
+           fileio/MatchFileReader.cpp \
            fileio/MatrixFile.cpp \
            fileio/MIDIFileReader.cpp \
            fileio/MP3FileReader.cpp \
            fileio/OggVorbisFileReader.cpp \
+           fileio/PlaylistFileReader.cpp \
            fileio/QuickTimeFileReader.cpp \
            fileio/RemoteFile.cpp \
+           fileio/ResamplingWavFileReader.cpp \
            fileio/WavFileReader.cpp \
            fileio/WavFileWriter.cpp \
+           model/AggregateWaveModel.cpp \
+           model/AlignmentModel.cpp \
            model/DenseTimeValueModel.cpp \
            model/EditableDenseThreeDimensionalModel.cpp \
            model/FFTModel.cpp \
@@ -81,5 +91,6 @@
            model/NoteModel.cpp \
            model/PowerOfSqrtTwoZoomConstraint.cpp \
            model/PowerOfTwoZoomConstraint.cpp \
+           model/RangeSummarisableTimeValueModel.cpp \
            model/WaveFileModel.cpp \
            model/WritableWaveFileModel.cpp
--- a/data/fileio/AudioFileReader.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/fileio/AudioFileReader.h	Fri Sep 28 13:56:38 2007 +0000
@@ -33,6 +33,7 @@
     size_t getFrameCount() const { return m_frameCount; }
     size_t getChannelCount() const { return m_channelCount; }
     size_t getSampleRate() const { return m_sampleRate; }
+    size_t getNativeRate() const { return m_sampleRate; } // if resampled
     
     /**
      * Return the title of the work in the audio file, if known.  This
--- a/data/fileio/AudioFileReaderFactory.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/fileio/AudioFileReaderFactory.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -16,6 +16,7 @@
 #include "AudioFileReaderFactory.h"
 
 #include "WavFileReader.h"
+#include "ResamplingWavFileReader.h"
 #include "OggVorbisFileReader.h"
 #include "MP3FileReader.h"
 #include "QuickTimeFileReader.h"
@@ -53,10 +54,12 @@
 }
 
 AudioFileReader *
-AudioFileReaderFactory::createReader(QString path)
+AudioFileReaderFactory::createReader(QString path, size_t targetRate)
 {
     QString err;
 
+    std::cerr << "AudioFileReaderFactory::createReader(\"" << path.toStdString() << "\"): Requested rate: " << targetRate << std::endl;
+
     AudioFileReader *reader = 0;
 
     // First try to construct a preferred reader based on the
@@ -69,6 +72,20 @@
     WavFileReader::getSupportedExtensions(extensions);
     if (extensions.find(ext) != extensions.end()) {
         reader = new WavFileReader(path);
+
+        if (targetRate != 0 &&
+            reader->isOK() &&
+            reader->getSampleRate() != targetRate) {
+
+            std::cerr << "AudioFileReaderFactory::createReader: WAV file rate: " << reader->getSampleRate() << ", creating resampling reader" << std::endl;
+
+            delete reader;
+            reader = new ResamplingWavFileReader
+                (path,
+                 ResamplingWavFileReader::ResampleThreaded,
+                 ResamplingWavFileReader::CacheInTemporaryFile,
+                 targetRate);
+        }
     }
     
 #ifdef HAVE_OGGZ
@@ -80,7 +97,8 @@
             reader = new OggVorbisFileReader
                 (path, 
                  OggVorbisFileReader::DecodeThreaded,
-                 OggVorbisFileReader::CacheInTemporaryFile);
+                 OggVorbisFileReader::CacheInTemporaryFile,
+                 targetRate);
         }
     }
 #endif
@@ -94,7 +112,8 @@
             reader = new MP3FileReader
                 (path,
                  MP3FileReader::DecodeThreaded,
-                 MP3FileReader::CacheInTemporaryFile);
+                 MP3FileReader::CacheInTemporaryFile,
+                 targetRate);
         }
     }
 #endif
@@ -107,7 +126,8 @@
             reader = new QuickTimeFileReader
                 (path,
                  QuickTimeFileReader::DecodeThreaded,
-                 QuickTimeFileReader::CacheInTemporaryFile);
+                 QuickTimeFileReader::CacheInTemporaryFile,
+                 targetRate);
         }
     }
 #endif
@@ -128,7 +148,21 @@
     }
 
     reader = new WavFileReader(path);
+
+    if (targetRate != 0 &&
+        reader->isOK() &&
+        reader->getSampleRate() != targetRate) {
+
+        delete reader;
+        reader = new ResamplingWavFileReader
+            (path,
+             ResamplingWavFileReader::ResampleThreaded,
+             ResamplingWavFileReader::CacheInTemporaryFile,
+             targetRate);
+    }
+
     if (reader->isOK()) return reader;
+
     if (reader->getError() != "") {
 	std::cerr << "AudioFileReaderFactory: WAV file reader error: \""
                   << reader->getError().toStdString() << "\"" << std::endl;
--- a/data/fileio/AudioFileReaderFactory.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/fileio/AudioFileReaderFactory.h	Fri Sep 28 13:56:38 2007 +0000
@@ -34,9 +34,15 @@
      * Return an audio file reader initialised to the file at the
      * given path, or NULL if no suitable reader for this path is
      * available or the file cannot be opened.
+     *
+     * If targetRate is non-zero, the file will be resampled to that
+     * rate (transparently).  You can query reader->getNativeRate()
+     * if you want to find out whether the file is being resampled
+     * or not.
+     *
      * Caller owns the returned object and must delete it after use.
      */
-    static AudioFileReader *createReader(QString path);
+    static AudioFileReader *createReader(QString path, size_t targetRate = 0);
 };
 
 #endif
--- a/data/fileio/CodedAudioFileReader.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/fileio/CodedAudioFileReader.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -4,7 +4,7 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006 Chris Cannam.
+    This file copyright 2006-2007 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -19,35 +19,70 @@
 #include "base/TempDirectory.h"
 #include "base/Exceptions.h"
 #include "base/Profiler.h"
+#include "base/Serialiser.h"
+#include "base/Resampler.h"
 
 #include <iostream>
 #include <QDir>
 #include <QMutexLocker>
 
-CodedAudioFileReader::CodedAudioFileReader(CacheMode cacheMode) :
+CodedAudioFileReader::CodedAudioFileReader(CacheMode cacheMode,
+                                           size_t targetRate) :
     m_cacheMode(cacheMode),
     m_initialised(false),
+    m_serialiser(0),
+    m_fileRate(0),
     m_cacheFileWritePtr(0),
     m_cacheFileReader(0),
     m_cacheWriteBuffer(0),
     m_cacheWriteBufferIndex(0),
-    m_cacheWriteBufferSize(16384)
+    m_cacheWriteBufferSize(16384),
+    m_resampler(0),
+    m_resampleBuffer(0)
 {
+    std::cerr << "CodedAudioFileReader::CodedAudioFileReader: rate " << targetRate << std::endl;
+
+    m_frameCount = 0;
+    m_sampleRate = targetRate;
 }
 
 CodedAudioFileReader::~CodedAudioFileReader()
 {
     QMutexLocker locker(&m_cacheMutex);
 
+    endSerialised();
+
     if (m_cacheFileWritePtr) sf_close(m_cacheFileWritePtr);
-    if (m_cacheFileReader) delete m_cacheFileReader;
-    if (m_cacheWriteBuffer) delete[] m_cacheWriteBuffer;
+
+    delete m_cacheFileReader;
+    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;
         }
     }
+
+    delete m_resampler;
+    delete[] m_resampleBuffer;
+}
+
+void
+CodedAudioFileReader::startSerialised(QString id)
+{
+//    std::cerr << "CodedAudioFileReader::startSerialised(" << id.toStdString() << ")" << std::endl;
+
+    delete m_serialiser;
+    m_serialiser = new Serialiser(id);
+}
+
+void
+CodedAudioFileReader::endSerialised()
+{
+//    std::cerr << "CodedAudioFileReader::endSerialised" << std::endl;
+
+    delete m_serialiser;
+    m_serialiser = 0;
 }
 
 void
@@ -55,11 +90,30 @@
 {
     QMutexLocker locker(&m_cacheMutex);
 
+    std::cerr << "CodedAudioFileReader::initialiseDecodeCache: file rate = " << m_fileRate << std::endl;
+
+    if (m_fileRate == 0) {
+        std::cerr << "CodedAudioFileReader::initialiseDecodeCache: ERROR: File sample rate unknown (bug in subclass implementation?)" << std::endl;
+        m_fileRate = 48000; // got to have something
+    }
+    if (m_sampleRate == 0) {
+        m_sampleRate = m_fileRate;
+    }
+    if (m_fileRate != m_sampleRate) {
+        std::cerr << "CodedAudioFileReader: resampling " << m_fileRate << " -> " <<  m_sampleRate << std::endl;
+        m_resampler = new Resampler(Resampler::FastestTolerable,
+                                    m_channelCount,
+                                    m_cacheWriteBufferSize);
+        float ratio = float(m_sampleRate) / float(m_fileRate);
+        m_resampleBuffer = new float
+            [lrintf(ceilf(m_cacheWriteBufferSize * m_channelCount * ratio))];
+    }
+
+    m_cacheWriteBuffer = new float[m_cacheWriteBufferSize * m_channelCount];
+    m_cacheWriteBufferIndex = 0;
+
     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")
@@ -68,17 +122,21 @@
             SF_INFO fileInfo;
             fileInfo.samplerate = m_sampleRate;
             fileInfo.channels = m_channelCount;
-            fileInfo.format = SF_FORMAT_WAV | SF_FORMAT_FLOAT;
+
+            // No point in writing 24-bit or float; generally this
+            // class is used for decoding files that have come from a
+            // 16 bit source or that decode to only 16 bits anyway.
+            fileInfo.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
     
             m_cacheFileWritePtr = sf_open(m_cacheFileName.toLocal8Bit(),
                                           SFM_WRITE, &fileInfo);
 
             if (m_cacheFileWritePtr) {
 
-                //!!! really want to do this now only if we're in a
-                //threaded mode -- creating the reader later if we're
-                //not threaded -- but we don't have access to that
-                //information here
+                // Ideally we would do this now only if we were in a
+                // threaded mode -- creating the reader later if we're
+                // not threaded -- but we don't have access to that
+                // information here
 
                 m_cacheFileReader = new WavFileReader(m_cacheFileName);
 
@@ -89,6 +147,7 @@
                     m_cacheMode = CacheInMemory;
                     sf_close(m_cacheFileWritePtr);
                 }
+
             } else {
                 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;
@@ -108,26 +167,82 @@
 }
 
 void
-CodedAudioFileReader::addSampleToDecodeCache(float sample)
+CodedAudioFileReader::addSamplesToDecodeCache(float **samples, size_t nframes)
 {
     QMutexLocker locker(&m_cacheMutex);
 
     if (!m_initialised) return;
 
-    switch (m_cacheMode) {
+    for (size_t i = 0; i < nframes; ++i) {
+        
+        for (size_t c = 0; c < m_channelCount; ++c) {
 
-    case CacheInTemporaryFile:
+            float sample = samples[c][i];
+        
+            m_cacheWriteBuffer[m_cacheWriteBufferIndex++] = sample;
 
+            if (m_cacheWriteBufferIndex ==
+                m_cacheWriteBufferSize * m_channelCount) {
+
+                pushBuffer(m_cacheWriteBuffer, m_cacheWriteBufferSize, false);
+                m_cacheWriteBufferIndex = 0;
+            }
+
+            if (m_cacheWriteBufferIndex % 10240 == 0 &&
+                m_cacheFileReader) {
+                m_cacheFileReader->updateFrameCount();
+            }
+        }
+    }
+}
+
+void
+CodedAudioFileReader::addSamplesToDecodeCache(float *samples, size_t nframes)
+{
+    QMutexLocker locker(&m_cacheMutex);
+
+    if (!m_initialised) return;
+
+    for (size_t i = 0; i < nframes; ++i) {
+        
+        for (size_t c = 0; c < m_channelCount; ++c) {
+
+            float sample = samples[i * m_channelCount + c];
+        
+            m_cacheWriteBuffer[m_cacheWriteBufferIndex++] = sample;
+
+            if (m_cacheWriteBufferIndex ==
+                m_cacheWriteBufferSize * m_channelCount) {
+
+                pushBuffer(m_cacheWriteBuffer, m_cacheWriteBufferSize, false);
+                m_cacheWriteBufferIndex = 0;
+            }
+
+            if (m_cacheWriteBufferIndex % 10240 == 0 &&
+                m_cacheFileReader) {
+                m_cacheFileReader->updateFrameCount();
+            }
+        }
+    }
+}
+
+void
+CodedAudioFileReader::addSamplesToDecodeCache(const SampleBlock &samples)
+{
+    QMutexLocker locker(&m_cacheMutex);
+
+    if (!m_initialised) return;
+
+    for (size_t i = 0; i < samples.size(); ++i) {
+
+        float sample = samples[i];
+        
         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);
-
+            pushBuffer(m_cacheWriteBuffer, m_cacheWriteBufferSize, false);
             m_cacheWriteBufferIndex = 0;
         }
 
@@ -135,11 +250,6 @@
             m_cacheFileReader) {
             m_cacheFileReader->updateFrameCount();
         }
-        break;
-
-    case CacheInMemory:
-        m_data.push_back(sample);
-        break;
     }
 }
 
@@ -155,41 +265,63 @@
         return;
     }
 
+    if (m_cacheWriteBufferIndex > 0) {
+        //!!! check for return value! out of disk space, etc!
+        pushBuffer(m_cacheWriteBuffer,
+                   m_cacheWriteBufferIndex / m_channelCount,
+                   true);
+    }        
+
+    delete[] m_cacheWriteBuffer;
+    m_cacheWriteBuffer = 0;
+
+    delete[] m_resampleBuffer;
+    m_resampleBuffer = 0;
+
+    delete m_resampler;
+    m_resampler = 0;
+
+    if (m_cacheMode == CacheInTemporaryFile) {
+        sf_close(m_cacheFileWritePtr);
+        m_cacheFileWritePtr = 0;
+        if (m_cacheFileReader) m_cacheFileReader->updateFrameCount();
+    }
+}
+
+void
+CodedAudioFileReader::pushBuffer(float *buffer, size_t sz, bool final)
+{
+    if (m_resampler) {
+        
+        float ratio = float(m_sampleRate) / float(m_fileRate);
+
+        if (ratio != 1.f) {
+            size_t out = m_resampler->resampleInterleaved
+                (buffer,
+                 m_resampleBuffer,
+                 sz,
+                 ratio,
+                 final);
+
+            buffer = m_resampleBuffer;
+            sz = out;
+        }
+    }
+
+    m_frameCount += sz;
+
     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->updateFrameCount();
-/*
-        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;
-        }*/
-
+        //!!! check for return value! out of disk space, etc!
+        sf_writef_float(m_cacheFileWritePtr, buffer, sz);
         break;
 
     case CacheInMemory:
-        // nothing to do 
+        for (size_t s = 0; s < sz; ++s) {
+            m_data.push_back(buffer[sz]);
+        }
+	MUNLOCK_SAMPLEBLOCK(m_data);
         break;
     }
 }
--- a/data/fileio/CodedAudioFileReader.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/fileio/CodedAudioFileReader.h	Fri Sep 28 13:56:38 2007 +0000
@@ -22,6 +22,8 @@
 #include <QMutex>
 
 class WavFileReader;
+class Serialiser;
+class Resampler;
 
 class CodedAudioFileReader : public AudioFileReader
 {
@@ -36,18 +38,31 @@
     virtual void getInterleavedFrames(size_t start, size_t count,
 				      SampleBlock &frames) const;
 
+    virtual size_t getNativeRate() const { return m_fileRate; }
+
 protected:
-    CodedAudioFileReader(CacheMode cacheMode);
+    CodedAudioFileReader(CacheMode cacheMode, size_t targetRate);
 
     void initialiseDecodeCache(); // samplerate, channels must have been set
-    void addSampleToDecodeCache(float sample);
+    void addSamplesToDecodeCache(float **samples, size_t nframes);
+    void addSamplesToDecodeCache(float *samplesInterleaved, size_t nframes);
+    void addSamplesToDecodeCache(const SampleBlock &interleaved);
     void finishDecodeCache();
     bool isDecodeCacheInitialised() const { return m_initialised; }
 
+    void startSerialised(QString id);
+    void endSerialised();
+
+private:
+    void pushBuffer(float *interleaved, size_t sz, bool final);
+
+protected:
     QMutex m_cacheMutex;
     CacheMode m_cacheMode;
     SampleBlock m_data;
     bool m_initialised;
+    Serialiser *m_serialiser;
+    size_t m_fileRate;
 
     QString m_cacheFileName;
     SNDFILE *m_cacheFileWritePtr;
@@ -55,6 +70,9 @@
     float *m_cacheWriteBuffer;
     size_t m_cacheWriteBufferIndex;
     size_t m_cacheWriteBufferSize; // frames
+
+    Resampler *m_resampler;
+    float *m_resampleBuffer;
 };
 
 #endif
--- a/data/fileio/MP3FileReader.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/fileio/MP3FileReader.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -34,18 +34,17 @@
 #include <QFileInfo>
 #include <QProgressDialog>
 
-MP3FileReader::MP3FileReader(QString path, DecodeMode decodeMode, CacheMode mode) :
-    CodedAudioFileReader(mode),
+MP3FileReader::MP3FileReader(QString path, DecodeMode decodeMode, 
+                             CacheMode mode, size_t targetRate) :
+    CodedAudioFileReader(mode, targetRate),
     m_path(path),
     m_decodeThread(0)
 {
-    m_frameCount = 0;
     m_channelCount = 0;
-    m_sampleRate = 0;
+    m_fileRate = 0;
     m_fileSize = 0;
     m_bitrateNum = 0;
     m_bitrateDenom = 0;
-    m_frameCount = 0;
     m_cancelled = false;
     m_completion = 0;
     m_done = false;
@@ -70,6 +69,8 @@
     }	
 
     m_filebuffer = 0;
+    m_samplebuffer = 0;
+    m_samplebuffersize = 0;
 
     try {
         m_filebuffer = new unsigned char[m_fileSize];
@@ -226,11 +227,21 @@
 
     delete[] m_reader->m_filebuffer;
     m_reader->m_filebuffer = 0;
-    
+
+    if (m_reader->m_samplebuffer) {
+        for (size_t c = 0; c < m_reader->m_channelCount; ++c) {
+            delete[] m_reader->m_samplebuffer[c];
+        }
+        delete[] m_reader->m_samplebuffer;
+        m_reader->m_samplebuffer = 0;
+    }
+
     if (m_reader->isDecodeCacheInitialised()) m_reader->finishDecodeCache();
 
     m_reader->m_done = true;
     m_reader->m_completion = 100;
+
+    m_reader->endSerialised();
 } 
 
 bool
@@ -287,8 +298,17 @@
     if (frames < 1) return MAD_FLOW_CONTINUE;
 
     if (m_channelCount == 0) {
+
+        m_fileRate = pcm->samplerate;
         m_channelCount = channels;
-        m_sampleRate = pcm->samplerate;
+
+        initialiseDecodeCache();
+
+        if (m_cacheMode == CacheInTemporaryFile) {
+            m_completion = 1;
+            std::cerr << "MP3FileReader::accept: channel count " << m_channelCount << ", file rate " << m_fileRate << ", about to start serialised section" << std::endl;
+            startSerialised("MP3FileReader::Decode");
+        }
     }
     
     if (m_bitrateDenom > 0) {
@@ -315,33 +335,41 @@
 
     if (m_cancelled) return MAD_FLOW_STOP;
 
-    m_frameCount += frames;
-
     if (!isDecodeCacheInitialised()) {
         initialiseDecodeCache();
     }
 
-    for (int i = 0; i < frames; ++i) {
+    if (m_samplebuffersize < frames) {
+        if (!m_samplebuffer) {
+            m_samplebuffer = new float *[channels];
+            for (int c = 0; c < channels; ++c) {
+                m_samplebuffer[c] = 0;
+            }
+        }
+        for (int c = 0; c < channels; ++c) {
+            delete[] m_samplebuffer[c];
+            m_samplebuffer[c] = new float[frames];
+        }
+        m_samplebuffersize = frames;
+    }
 
-	for (int ch = 0; ch < channels; ++ch) {
+    int activeChannels = int(sizeof(pcm->samples) / sizeof(pcm->samples[0]));
+
+    for (int ch = 0; ch < channels; ++ch) {
+
+        for (int i = 0; i < frames; ++i) {
+
 	    mad_fixed_t sample = 0;
-	    if (ch < int(sizeof(pcm->samples) / sizeof(pcm->samples[0]))) {
+	    if (ch < activeChannels) {
 		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);
+            
+            m_samplebuffer[ch][i] = fsample;
 	}
     }
 
-    if (frames > 0) {
-	MUNLOCK_SAMPLEBLOCK(m_data);
-    }
+    addSamplesToDecodeCache(m_samplebuffer, frames);
 
     return MAD_FLOW_CONTINUE;
 }
--- a/data/fileio/MP3FileReader.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/fileio/MP3FileReader.h	Fri Sep 28 13:56:38 2007 +0000
@@ -35,7 +35,10 @@
         DecodeThreaded // decode in a background thread after construction
     };
 
-    MP3FileReader(QString path, DecodeMode decodeMode, CacheMode cacheMode);
+    MP3FileReader(QString path,
+                  DecodeMode decodeMode,
+                  CacheMode cacheMode,
+                  size_t targetRate = 0);
     virtual ~MP3FileReader();
 
     virtual QString getError() const { return m_error; }
@@ -61,6 +64,8 @@
     bool m_done;
 
     unsigned char *m_filebuffer;
+    float **m_samplebuffer;
+    size_t m_samplebuffersize;
 
     QProgressDialog *m_progress;
     bool m_cancelled;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/MatchFileReader.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,183 @@
+/* -*- 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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "MatchFileReader.h"
+
+#include <QFile>
+#include <QTextStream>
+
+#include <cmath>
+#include <iostream>
+
+Alignment::Alignment() :
+    thisHopTime(0.0),
+    refHopTime(0.0)
+{
+}
+
+double
+Alignment::fromReference(double t) const
+{
+    int ri = lrint(t / refHopTime);
+    int index = search(refIndex, ri);
+    return thisIndex[index] * thisHopTime;
+}
+
+double
+Alignment::toReference(double t) const
+{
+    int ti = lrint(t / thisHopTime);
+    int index = search(thisIndex, ti);
+    return refIndex[index] * refHopTime;
+}
+
+int
+Alignment::search(const FrameArray &arr, int val) const
+{
+    int len = arr.size();
+    int max = len - 1;
+    int min = 0;
+    while (max > min) {
+        int mid = (max + min) / 2;
+        if (val > arr[mid]) {
+            min = mid + 1;
+        } else {
+            max = mid;
+        }
+    } // max = MIN_j (arr[j] >= val)   i.e. the first equal or next highest
+    while ((max + 1 < len) && (arr[max + 1] == val)) {
+        max++;
+    }
+    return (min + max) / 2;
+}
+
+MatchFileReader::MatchFileReader(QString path) :
+    m_file(0)
+{
+    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;
+    }
+}
+
+MatchFileReader::~MatchFileReader()
+{
+    if (m_file) {
+        std::cerr << "MatchFileReader::MatchFileReader: Closing file" << std::endl;
+        m_file->close();
+    }
+    delete m_file;
+}
+
+bool
+MatchFileReader::isOK() const
+{
+    return (m_file != 0);
+}
+
+QString
+MatchFileReader::getError() const
+{
+    return m_error;
+}
+
+Alignment
+MatchFileReader::load() const
+{
+    Alignment alignment;
+
+    if (!m_file) return alignment;
+
+    QTextStream in(m_file);
+
+/*
+File: /home/studio/match-test/mahler-3-boulez-5.wav
+Marks: -1
+FixedPoints: true 0
+0
+0
+0
+0
+File: /home/studio/match-test/mahler-3-haitink-5.wav
+Marks: 0
+FixedPoints: true 0
+0.02
+0.02
+12836
+*/
+
+    int fileCount = 0;
+    int state = 0;
+    int count = 0;
+
+    while (!in.atEnd()) {
+
+        QString line = in.readLine().trimmed();
+        if (line.startsWith("File: ")) {
+            ++fileCount;
+            continue;
+        }
+        if (fileCount != 2) continue;
+        if (line.startsWith("Marks:") || line.startsWith("FixedPoints:")) {
+            continue;
+        }
+
+        switch (state) {
+        case 0:
+            alignment.thisHopTime = line.toDouble();
+            break;
+        case 1:
+            alignment.refHopTime = line.toDouble();
+            break;
+        case 2: 
+            count = line.toInt();
+            break;
+        case 3:
+            alignment.thisIndex.push_back(line.toInt());
+            break;
+        case 4:
+            alignment.refIndex.push_back(line.toInt());
+            break;
+        }
+
+        if (state < 3) ++state;
+        else if (state == 3 && alignment.thisIndex.size() == count) ++state;
+    }
+
+    if (alignment.thisHopTime == 0.0) {
+        std::cerr << "ERROR in Match file: this hop time == 0, using 0.01 instead" << std::endl;
+        alignment.thisHopTime = 0.01;
+    }
+
+    if (alignment.refHopTime == 0.0) {
+        std::cerr << "ERROR in Match file: ref hop time == 0, using 0.01 instead" << std::endl;
+        alignment.refHopTime = 0.01;
+    }
+
+    std::cerr << "MatchFileReader: this hop = " << alignment.thisHopTime << ", ref hop = " << alignment.refHopTime << ", this index count = " << alignment.thisIndex.size() << ", ref index count = " << alignment.refIndex.size() << std::endl;
+
+    return alignment;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/MatchFileReader.h	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,67 @@
+/* -*- 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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _MATCH_FILE_READER_H_
+#define _MATCH_FILE_READER_H_
+
+#include <vector>
+#include <QString>
+
+class QFile;
+class Model;
+
+class Alignment
+{
+public:
+    Alignment();
+
+    typedef std::vector<int> FrameArray;
+
+    double thisHopTime;
+    double refHopTime;
+
+    FrameArray thisIndex;
+    FrameArray refIndex;
+
+    double fromReference(double) const;
+    double toReference(double) const;
+
+    //!!! blah
+    void setMainModel(Model *m) { m_mainModel = m; }
+    bool isMainModel(Model *m) const { return m == m_mainModel; }
+
+    int search(const FrameArray &arr, int val) const;
+
+protected:
+    Model *m_mainModel;
+};
+
+class MatchFileReader
+{
+public:
+    MatchFileReader(QString path);
+    virtual ~MatchFileReader();
+
+    virtual bool isOK() const;
+    virtual QString getError() const;
+    virtual Alignment load() const;
+
+protected:
+    QFile *m_file;
+    QString m_error;
+};
+
+#endif
+
--- a/data/fileio/OggVorbisFileReader.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/fileio/OggVorbisFileReader.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -34,8 +34,9 @@
 
 OggVorbisFileReader::OggVorbisFileReader(QString path,
                                          DecodeMode decodeMode,
-                                         CacheMode mode) :
-    CodedAudioFileReader(mode),
+                                         CacheMode mode,
+                                         size_t targetRate) :
+    CodedAudioFileReader(mode, targetRate),
     m_path(path),
     m_progress(0),
     m_fileSize(0),
@@ -45,9 +46,8 @@
     m_completion(0),
     m_decodeThread(0)
 {
-    m_frameCount = 0;
     m_channelCount = 0;
-    m_sampleRate = 0;
+    m_fileRate = 0;
 
     std::cerr << "OggVorbisFileReader::OggVorbisFileReader(" << path.toLocal8Bit().data() << "): now have " << (++instances) << " instances" << std::endl;
 
@@ -111,6 +111,11 @@
 void
 OggVorbisFileReader::DecodeThread::run()
 {
+    if (m_reader->m_cacheMode == CacheInTemporaryFile) {
+        m_reader->m_completion = 1;
+        m_reader->startSerialised("OggVorbisFileReader::Decode");
+    }
+
     while (oggz_read(m_reader->m_oggz, 1024) > 0);
         
     fish_sound_delete(m_reader->m_fishSound);
@@ -120,6 +125,8 @@
     
     if (m_reader->isDecodeCacheInitialised()) m_reader->finishDecodeCache();
     m_reader->m_completion = 100;
+
+    m_reader->endSerialised();
 } 
 
 int
@@ -175,22 +182,13 @@
 	FishSoundInfo fsinfo;
 	fish_sound_command(fs, FISH_SOUND_GET_INFO,
 			   &fsinfo, sizeof(FishSoundInfo));
+	reader->m_fileRate = fsinfo.samplerate;
 	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]);
-	    }
-	}
-
-	MUNLOCK_SAMPLEBLOCK(reader->m_data);
+        reader->addSamplesToDecodeCache(frames, nframes);
     }
 
     if (reader->m_cancelled) return 1;
--- a/data/fileio/OggVorbisFileReader.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/fileio/OggVorbisFileReader.h	Fri Sep 28 13:56:38 2007 +0000
@@ -37,8 +37,10 @@
         DecodeThreaded // decode in a background thread after construction
     };
 
-    OggVorbisFileReader(QString path, DecodeMode decodeMode,
-                        CacheMode cacheMode);
+    OggVorbisFileReader(QString path,
+                        DecodeMode decodeMode,
+                        CacheMode cacheMode,
+                        size_t targetRate = 0);
     virtual ~OggVorbisFileReader();
 
     virtual QString getError() const { return m_error; }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/PlaylistFileReader.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,94 @@
+/* -*- 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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "PlaylistFileReader.h"
+
+#include <QFile>
+#include <QTextStream>
+#include <QStringList>
+
+PlaylistFileReader::PlaylistFileReader(QString path)
+{
+    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;
+    }
+}
+
+PlaylistFileReader::~PlaylistFileReader()
+{
+    if (m_file) m_file->close();
+    delete m_file;
+}
+
+bool
+PlaylistFileReader::isOK() const
+{
+    return (m_file != 0);
+}
+
+QString
+PlaylistFileReader::getError() const
+{
+    return m_error;
+}
+
+PlaylistFileReader::Playlist
+PlaylistFileReader::load() const
+{
+    if (!m_file) return Playlist();
+
+    QTextStream in(m_file);
+    in.seek(0);
+
+    Playlist playlist;
+
+    while (!in.atEnd()) {
+
+        // cope with old-style Mac line endings (c.f. CSVFileReader)
+        // as well as DOS/Unix style
+
+        QString chunk = in.readLine();
+        QStringList lines = chunk.split('\r', QString::SkipEmptyParts);
+        
+        for (size_t li = 0; li < lines.size(); ++li) {
+
+            QString line = lines[li];
+
+            if (line.startsWith("#")) continue;
+
+            playlist.push_back(line);
+        }
+    }
+
+    return playlist;
+}
+
+void
+PlaylistFileReader::getSupportedExtensions(std::set<QString> &extensions)
+{
+    extensions.insert("m3u");
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/PlaylistFileReader.h	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,45 @@
+/* -*- 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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _PLAYLIST_FILE_READER_H_
+#define _PLAYLIST_FILE_READER_H_
+
+#include <QString>
+
+#include <vector>
+#include <set>
+
+class QFile;
+
+class PlaylistFileReader
+{
+public:
+    typedef std::vector<QString> Playlist;
+
+    PlaylistFileReader(QString path);
+    virtual ~PlaylistFileReader();
+
+    virtual bool isOK() const;
+    virtual QString getError() const;
+    virtual Playlist load() const;
+
+    static void getSupportedExtensions(std::set<QString> &extensions);
+
+protected:
+    QFile *m_file;
+    QString m_error;
+};
+
+#endif
--- a/data/fileio/QuickTimeFileReader.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/fileio/QuickTimeFileReader.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -50,8 +50,9 @@
 
 QuickTimeFileReader::QuickTimeFileReader(QString path,
                                          DecodeMode decodeMode,
-                                         CacheMode mode) :
-    CodedAudioFileReader(mode),
+                                         CacheMode mode,
+                                         size_t targetRate) :
+    CodedAudioFileReader(mode, targetRate),
     m_path(path),
     m_d(new D),
     m_progress(0),
@@ -59,9 +60,8 @@
     m_completion(0),
     m_decodeThread(0)
 {
-    m_frameCount = 0;
     m_channelCount = 0;
-    m_sampleRate = 0;
+    m_fileRate = 0;
 
     Profiler profiler("QuickTimeFileReader::QuickTimeFileReader", true);
 
@@ -181,9 +181,9 @@
     }
 	
     m_channelCount = m_d->asbd.mChannelsPerFrame;
-    m_sampleRate = m_d->asbd.mSampleRate;
+    m_fileRate = m_d->asbd.mSampleRate;
 
-    std::cerr << "QuickTime: " << m_channelCount << " channels, " << m_sampleRate << " kHz" << std::endl;
+    std::cerr << "QuickTime: " << m_channelCount << " channels, " << m_fileRate << " kHz" << std::endl;
 
     m_d->asbd.mFormatFlags =
         kAudioFormatFlagIsFloat |
@@ -236,12 +236,8 @@
 
 //    std::cerr << "Read " << framesRead << " frames (block size " << m_d->blockSize << ")" << std::endl;
 
-            m_frameCount += framesRead;
-
             // QuickTime buffers are interleaved unless specified otherwise
-            for (UInt32 i = 0; i < framesRead * m_channelCount; ++i) {
-                addSampleToDecodeCache(m_d->data[i]);
-            }
+            addSamplesToDecodeCache(m_d->data, framesRead);
 
             if (framesRead < m_d->blockSize) break;
         }
@@ -288,6 +284,11 @@
 void
 QuickTimeFileReader::DecodeThread::run()
 {
+    if (m_reader->m_cacheMode == CacheInTemporaryFile) {
+        m_reader->m_completion = 1;
+        m_reader->startSerialised("QuickTimeFileReader::Decode");
+    }
+
     while (1) {
             
         UInt32 framesRead = m_reader->m_d->blockSize;
@@ -301,12 +302,8 @@
             break;
         }
        
-        m_reader->m_frameCount += framesRead;
- 
         // QuickTime buffers are interleaved unless specified otherwise
-        for (UInt32 i = 0; i < framesRead * m_reader->m_channelCount; ++i) {
-            m_reader->addSampleToDecodeCache(m_reader->m_d->data[i]);
-        }
+        addSamplesToDecodeCache(m_d->data, framesRead);
         
         if (framesRead < m_reader->m_d->blockSize) break;
     }
@@ -319,6 +316,7 @@
     }
     
     m_reader->m_completion = 100;
+    m_reader->endSerialised();
 } 
 
 void
@@ -339,3 +337,4 @@
 }
 
 #endif
+
--- a/data/fileio/QuickTimeFileReader.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/fileio/QuickTimeFileReader.h	Fri Sep 28 13:56:38 2007 +0000
@@ -37,8 +37,10 @@
         DecodeThreaded // decode in a background thread after construction
     };
 
-    QuickTimeFileReader(QString path, DecodeMode decodeMode,
-                        CacheMode cacheMode);
+    QuickTimeFileReader(QString path,
+                        DecodeMode decodeMode,
+                        CacheMode cacheMode,
+                        size_t targetRate = 0);
     virtual ~QuickTimeFileReader();
 
     virtual QString getError() const { return m_error; }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/ResamplingWavFileReader.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,170 @@
+/* -*- 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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ResamplingWavFileReader.h"
+
+#include "WavFileReader.h"
+#include "base/Profiler.h"
+
+#include <QProgressDialog>
+#include <QFileInfo>
+#include <QApplication>
+
+ResamplingWavFileReader::ResamplingWavFileReader(QString path,
+						 ResampleMode resampleMode,
+						 CacheMode mode,
+						 size_t targetRate) :
+    CodedAudioFileReader(mode, targetRate),
+    m_path(path),
+    m_cancelled(false),
+    m_processed(0),
+    m_completion(0),
+    m_original(0),
+    m_progress(0),
+    m_decodeThread(0)
+{
+    m_channelCount = 0;
+    m_fileRate = 0;
+
+    std::cerr << "ResamplingWavFileReader::ResamplingWavFileReader(\""
+              << path.toStdString() << "\"): rate " << targetRate << std::endl;
+
+    Profiler profiler("ResamplingWavFileReader::ResamplingWavFileReader", true);
+
+    m_original = new WavFileReader(path);
+    if (!m_original->isOK()) {
+        m_error = m_original->getError();
+        return;
+    }
+
+    m_channelCount = m_original->getChannelCount();
+    m_fileRate = m_original->getSampleRate();
+
+    initialiseDecodeCache();
+
+    if (resampleMode == ResampleAtOnce) {
+
+	m_progress = new QProgressDialog
+	    (QObject::tr("Resampling %1...").arg(QFileInfo(path).fileName()),
+	     QObject::tr("Stop"), 0, 100);
+	m_progress->hide();
+
+        size_t blockSize = 16384;
+        size_t total = m_original->getFrameCount();
+
+        SampleBlock block;
+
+        for (size_t i = 0; i < total; i += blockSize) {
+
+            size_t count = blockSize;
+            if (i + count > total) count = total - i;
+
+            m_original->getInterleavedFrames(i, count, block);
+            addBlock(block);
+
+            if (m_cancelled) break;
+        }
+
+        if (isDecodeCacheInitialised()) finishDecodeCache();
+
+        delete m_original;
+        m_original = 0;
+
+        delete m_progress;
+        m_progress = 0;
+
+    } else {
+
+        m_decodeThread = new DecodeThread(this);
+        m_decodeThread->start();
+    }
+}
+
+ResamplingWavFileReader::~ResamplingWavFileReader()
+{
+    if (m_decodeThread) {
+        m_cancelled = true;
+        m_decodeThread->wait();
+        delete m_decodeThread;
+    }
+    
+    delete m_original;
+}
+
+void
+ResamplingWavFileReader::DecodeThread::run()
+{
+    if (m_reader->m_cacheMode == CacheInTemporaryFile) {
+        m_reader->startSerialised("ResamplingWavFileReader::Decode");
+    }
+
+    size_t blockSize = 16384;
+    size_t total = m_reader->m_original->getFrameCount();
+    
+    SampleBlock block;
+    
+    for (size_t i = 0; i < total; i += blockSize) {
+        
+        size_t count = blockSize;
+        if (i + count > total) count = total - i;
+        
+        m_reader->m_original->getInterleavedFrames(i, count, block);
+        m_reader->addBlock(block);
+
+        if (m_reader->m_cancelled) break;
+    }
+    
+    if (m_reader->isDecodeCacheInitialised()) m_reader->finishDecodeCache();
+    m_reader->m_completion = 100;
+
+    m_reader->endSerialised();
+
+    delete m_reader->m_original;
+    m_reader->m_original = 0;
+} 
+
+void
+ResamplingWavFileReader::addBlock(const SampleBlock &frames)
+{
+    addSamplesToDecodeCache(frames);
+
+    m_processed += frames.size();
+
+    int progress = lrint((float(m_processed) * 100) /
+                         float(m_original->getFrameCount()));
+
+    if (progress > 99) progress = 99;
+    m_completion = progress;
+    
+    if (m_progress) {
+	if (progress > m_progress->value()) {
+	    m_progress->setValue(progress);
+	    m_progress->show();
+	    m_progress->raise();
+	    qApp->processEvents();
+	    if (m_progress->wasCanceled()) {
+		m_cancelled = true;
+	    }
+	}
+    }
+}
+
+void
+ResamplingWavFileReader::getSupportedExtensions(std::set<QString> &extensions)
+{
+    WavFileReader::getSupportedExtensions(extensions);
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/ResamplingWavFileReader.h	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,78 @@
+/* -*- 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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _RESAMPLING_WAV_FILE_READER_H_
+#define _RESAMPLING_WAV_FILE_READER_H_
+
+#include "CodedAudioFileReader.h"
+
+#include "base/Thread.h"
+
+#include <set>
+
+class WavFileReader;
+class QProgressDialog;
+
+class ResamplingWavFileReader : public CodedAudioFileReader
+{
+public:
+    enum ResampleMode {
+        ResampleAtOnce, // resample the file on construction, with progress dialog
+        ResampleThreaded // resample in a background thread after construction
+    };
+
+    ResamplingWavFileReader(QString path,
+                            ResampleMode resampleMode,
+                            CacheMode cacheMode,
+                            size_t targetRate = 0);
+    virtual ~ResamplingWavFileReader();
+
+    virtual QString getError() const { return m_error; }
+
+    static void getSupportedExtensions(std::set<QString> &extensions);
+
+    virtual int getDecodeCompletion() const { return m_completion; }
+
+    virtual bool isUpdating() const {
+        return m_decodeThread && m_decodeThread->isRunning();
+    }
+
+protected:
+    QString m_path;
+    QString m_error;
+    bool m_cancelled;
+    size_t m_processed;
+    int m_completion;
+
+    WavFileReader *m_original;
+    QProgressDialog *m_progress;
+
+    void addBlock(const SampleBlock &frames);
+    
+    class DecodeThread : public Thread
+    {
+    public:
+        DecodeThread(ResamplingWavFileReader *reader) : m_reader(reader) { }
+        virtual void run();
+
+    protected:
+        ResamplingWavFileReader *m_reader;
+    };
+
+    DecodeThread *m_decodeThread;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/AggregateWaveModel.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,228 @@
+/* -*- 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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "AggregateWaveModel.h"
+
+#include <iostream>
+
+PowerOfSqrtTwoZoomConstraint
+AggregateWaveModel::m_zoomConstraint;
+
+AggregateWaveModel::AggregateWaveModel(ChannelSpecList channelSpecs) :
+    m_components(channelSpecs)
+{
+    for (ChannelSpecList::const_iterator i = channelSpecs.begin();
+         i != channelSpecs.end(); ++i) {
+        if (i->model->getSampleRate() !=
+            channelSpecs.begin()->model->getSampleRate()) {
+            std::cerr << "AggregateWaveModel::AggregateWaveModel: WARNING: Component models do not all have the same sample rate" << std::endl;
+            break;
+        }
+    }
+}
+
+AggregateWaveModel::~AggregateWaveModel()
+{
+}
+
+bool
+AggregateWaveModel::isOK() const
+{
+    for (ChannelSpecList::const_iterator i = m_components.begin();
+         i != m_components.end(); ++i) {
+        if (!i->model->isOK()) return false;
+    }
+    return true;
+}
+
+bool
+AggregateWaveModel::isReady(int *completion) const
+{
+    if (completion) *completion = 100;
+    bool ready = true;
+    for (ChannelSpecList::const_iterator i = m_components.begin();
+         i != m_components.end(); ++i) {
+        int completionHere = 100;
+        if (!i->model->isReady(&completionHere)) ready = false;
+        if (completion && completionHere < *completion) {
+            *completion = completionHere;
+        }
+    }
+    return ready;
+}
+
+size_t
+AggregateWaveModel::getFrameCount() const
+{
+    size_t count = 0;
+
+    for (ChannelSpecList::const_iterator i = m_components.begin();
+         i != m_components.end(); ++i) {
+        size_t thisCount = i->model->getEndFrame() - i->model->getStartFrame();
+        if (thisCount > count) count = thisCount;
+    }
+
+    return count;
+}
+
+size_t
+AggregateWaveModel::getChannelCount() const
+{
+    return m_components.size();
+}
+
+size_t
+AggregateWaveModel::getSampleRate() const
+{
+    if (m_components.empty()) return 0;
+    return m_components.begin()->model->getSampleRate();
+}
+
+Model *
+AggregateWaveModel::clone() const
+{
+    return new AggregateWaveModel(m_components);
+}
+
+size_t
+AggregateWaveModel::getValues(int channel, size_t start, size_t end,
+                              float *buffer) const
+{
+    int ch0 = channel, ch1 = channel;
+    bool mixing = false;
+    if (channel == -1) {
+        ch0 = 0;
+        ch1 = getChannelCount()-1;
+        mixing = true;
+    }
+
+    float *readbuf = buffer;
+    if (mixing) {
+        readbuf = new float[end - start];
+        for (size_t i = 0; i < end - start; ++i) {
+            buffer[i] = 0.f;
+        }
+    }
+
+    size_t sz = end - start;
+    
+    for (int c = ch0; c <= ch1; ++c) {
+        size_t szHere = 
+            m_components[c].model->getValues(m_components[c].channel,
+                                             start, end,
+                                             readbuf);
+        if (szHere < sz) sz = szHere;
+        if (mixing) {
+            for (size_t i = 0; i < end - start; ++i) {
+                buffer[i] += readbuf[i];
+            }
+        }
+    }
+
+    if (mixing) delete[] readbuf;
+    return sz;
+}
+         
+size_t
+AggregateWaveModel::getValues(int channel, size_t start, size_t end,
+                              double *buffer) const
+{
+    int ch0 = channel, ch1 = channel;
+    bool mixing = false;
+    if (channel == -1) {
+        ch0 = 0;
+        ch1 = getChannelCount()-1;
+        mixing = true;
+    }
+
+    double *readbuf = buffer;
+    if (mixing) {
+        readbuf = new double[end - start];
+        for (size_t i = 0; i < end - start; ++i) {
+            buffer[i] = 0.f;
+        }
+    }
+
+    size_t sz = end - start;
+    
+    for (int c = ch0; c <= ch1; ++c) {
+        size_t szHere = 
+            m_components[c].model->getValues(m_components[c].channel,
+                                             start, end,
+                                             readbuf);
+        if (szHere < sz) sz = szHere;
+        if (mixing) {
+            for (size_t i = 0; i < end - start; ++i) {
+                buffer[i] += readbuf[i];
+            }
+        }
+    }
+    
+    if (mixing) delete[] readbuf;
+    return sz;
+}
+        
+void
+AggregateWaveModel::getRanges(size_t channel, size_t start, size_t end,
+                              RangeBlock &ranges, size_t &blockSize) const
+{
+    //!!! complete
+}
+
+AggregateWaveModel::Range
+AggregateWaveModel::getRange(size_t channel, size_t start, size_t end) const
+{
+    //!!! complete
+    return Range();
+}
+        
+size_t
+AggregateWaveModel::getComponentCount() const
+{
+    return m_components.size();
+}
+
+AggregateWaveModel::ModelChannelSpec
+AggregateWaveModel::getComponent(size_t c) const
+{
+    return m_components[c];
+}
+
+void
+AggregateWaveModel::componentModelChanged()
+{
+    emit modelChanged();
+}
+
+void
+AggregateWaveModel::componentModelChanged(size_t start, size_t end)
+{
+    emit modelChanged(start, end);
+}
+
+void
+AggregateWaveModel::componentModelCompletionChanged()
+{
+    emit completionChanged();
+}
+
+void
+AggregateWaveModel::toXml(QTextStream &out,
+                          QString indent,
+                          QString extraAttributes) const
+{
+    //!!! complete
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/AggregateWaveModel.h	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,94 @@
+/* -*- 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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _AGGREGATE_WAVE_MODEL_H_
+#define _AGGREGATE_WAVE_MODEL_H_
+
+#include "RangeSummarisableTimeValueModel.h"
+#include "PowerOfSqrtTwoZoomConstraint.h"
+
+#include <vector>
+
+class AggregateWaveModel : public RangeSummarisableTimeValueModel
+{
+    Q_OBJECT
+
+public:
+    struct ModelChannelSpec
+    {
+        ModelChannelSpec(RangeSummarisableTimeValueModel *m, int c) :
+            model(m), channel(c) { }
+        RangeSummarisableTimeValueModel *model;
+        int channel;
+    };
+
+    typedef std::vector<ModelChannelSpec> ChannelSpecList;
+
+    AggregateWaveModel(ChannelSpecList channelSpecs);
+    ~AggregateWaveModel();
+
+    bool isOK() const;
+    bool isReady(int *) const;
+
+    size_t getComponentCount() const;
+    ModelChannelSpec getComponent(size_t c) const;
+
+    const ZoomConstraint *getZoomConstraint() const { return &m_zoomConstraint; }
+
+    size_t getFrameCount() const;
+    size_t getChannelCount() const;
+    size_t getSampleRate() const;
+
+    virtual Model *clone() const;
+
+    float getValueMinimum() const { return -1.0f; }
+    float getValueMaximum() const { return  1.0f; }
+
+    virtual size_t getStartFrame() const { return 0; }
+    virtual size_t getEndFrame() const { return getFrameCount(); }
+
+    virtual size_t getValues(int channel, size_t start, size_t end,
+			     float *buffer) const;
+
+    virtual size_t getValues(int channel, size_t start, size_t end,
+			     double *buffer) const;
+
+    virtual void getRanges(size_t channel, size_t start, size_t end,
+                           RangeBlock &ranges,
+                           size_t &blockSize) const;
+
+    virtual Range getRange(size_t channel, size_t start, size_t end) const;
+
+    virtual void toXml(QTextStream &out,
+                       QString indent = "",
+                       QString extraAttributes = "") const;
+
+signals:
+    void modelChanged();
+    void modelChanged(size_t, size_t);
+    void completionChanged();
+
+protected slots:
+    void componentModelChanged();
+    void componentModelChanged(size_t, size_t);
+    void componentModelCompletionChanged();
+
+protected:
+    ChannelSpecList m_components;
+    static PowerOfSqrtTwoZoomConstraint m_zoomConstraint;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/AlignmentModel.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -0,0 +1,210 @@
+/* -*- 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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "AlignmentModel.h"
+
+#include "SparseTimeValueModel.h"
+
+AlignmentModel::AlignmentModel(Model *reference,
+                               Model *aligned,
+                               Model *inputModel,
+			       SparseTimeValueModel *path) :
+    m_reference(reference),
+    m_aligned(aligned),
+    m_inputModel(inputModel),
+    m_path(path),
+    m_reversePath(0),
+    m_pathComplete(false)
+{
+    connect(m_path, SIGNAL(modelChanged()),
+            this, SLOT(pathChanged()));
+
+    connect(m_path, SIGNAL(modelChanged(size_t, size_t)),
+            this, SLOT(pathChanged(size_t, size_t)));
+
+    connect(m_path, SIGNAL(completionChanged()),
+            this, SLOT(pathCompletionChanged()));
+
+    constructReversePath();
+}
+
+AlignmentModel::~AlignmentModel()
+{
+    delete m_inputModel;
+    delete m_path;
+    delete m_reversePath;
+}
+
+bool
+AlignmentModel::isOK() const
+{
+    return m_path->isOK();
+}
+
+size_t
+AlignmentModel::getStartFrame() const
+{
+    //!!! do we care about distinct rates?
+    size_t a = m_reference->getStartFrame();
+    size_t b = m_aligned->getStartFrame();
+    return std::min(a, b);
+}
+
+size_t
+AlignmentModel::getEndFrame() const
+{
+    //!!! do we care about distinct rates?
+    size_t a = m_reference->getEndFrame();
+    size_t b = m_aligned->getEndFrame();
+    return std::max(a, b);
+}
+
+size_t
+AlignmentModel::getSampleRate() const
+{
+    return m_reference->getSampleRate();
+}
+
+Model *
+AlignmentModel::clone() const
+{
+    return new AlignmentModel
+        (m_reference, m_aligned,
+         m_inputModel ? m_inputModel->clone() : 0,
+         m_path ? static_cast<SparseTimeValueModel *>(m_path->clone()) : 0);
+}
+
+bool
+AlignmentModel::isReady(int *completion) const
+{
+    return m_path->isReady(completion);
+}
+
+const ZoomConstraint *
+AlignmentModel::getZoomConstraint() const
+{
+    return m_path->getZoomConstraint();
+}
+
+const Model *
+AlignmentModel::getReferenceModel() const
+{
+    return m_reference;
+}
+
+const Model *
+AlignmentModel::getAlignedModel() const
+{
+    return m_aligned;
+}
+
+size_t
+AlignmentModel::toReference(size_t frame) const
+{
+//    std::cerr << "AlignmentModel::toReference(" << frame << ")" << std::endl;
+    if (!m_reversePath) constructReversePath();
+    return align(m_reversePath, frame);
+}
+
+size_t
+AlignmentModel::fromReference(size_t frame) const
+{
+//    std::cerr << "AlignmentModel::fromReference(" << frame << ")" << std::endl;
+    return align(m_path, frame);
+}
+
+void
+AlignmentModel::pathChanged()
+{
+}
+
+void
+AlignmentModel::pathChanged(size_t, size_t)
+{
+    if (!m_pathComplete) return;
+    constructReversePath();
+}    
+
+void
+AlignmentModel::pathCompletionChanged()
+{
+    if (!m_pathComplete) {
+        int completion = 0;
+        m_path->isReady(&completion);
+        std::cerr << "AlignmentModel::pathCompletionChanged: completion = "
+                  << completion << std::endl;
+        m_pathComplete = (completion == 100); //!!! a bit of a hack
+        if (m_pathComplete) {
+            constructReversePath();
+            delete m_inputModel;
+            m_inputModel = 0;
+        }
+    }
+    emit completionChanged();
+}
+
+void
+AlignmentModel::constructReversePath() const
+{
+    if (!m_reversePath) {
+        m_reversePath = new SparseTimeValueModel
+            (m_path->getSampleRate(), m_path->getResolution(), false);
+    }
+        
+    m_reversePath->clear();
+
+    SparseTimeValueModel::PointList points = m_path->getPoints();
+        
+    for (SparseTimeValueModel::PointList::const_iterator i = points.begin();
+         i != points.end(); ++i) {
+        long frame = i->frame;
+        float value = i->value;
+        long rframe = lrintf(value * m_aligned->getSampleRate());
+        float rvalue = (float)frame / (float)m_reference->getSampleRate();
+        m_reversePath->addPoint
+            (SparseTimeValueModel::Point(rframe, rvalue, ""));
+    }
+
+    std::cerr << "AlignmentModel::constructReversePath: " << m_reversePath->getPointCount() << " points" << std::endl;
+}
+
+size_t
+AlignmentModel::align(SparseTimeValueModel *path, size_t frame) const
+{
+    // The path consists of a series of points, each with x (time)
+    // equal to the time on the source model and y (value) equal to
+    // the time on the target model.  Times and values are both
+    // monotonically increasing.
+
+    const SparseTimeValueModel::PointList &points = path->getPoints();
+
+    if (points.empty()) {
+//        std::cerr << "AlignmentModel::align: No points" << std::endl;
+        return frame;
+    }        
+
+    SparseTimeValueModel::Point point(frame);
+    SparseTimeValueModel::PointList::const_iterator i = points.lower_bound(point);
+    if (i == points.end()) --i;
+    float time = i->value;
+    size_t rv = lrintf(time * getSampleRate());
+
+    //!!! interpolate!
+
+//    std::cerr << "AlignmentModel::align: rv = " << rv << std::endl;
+
+    return rv;
+}
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/AlignmentModel.h	Fri Sep 28 13:56:38 2007 +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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _ALIGNMENT_MODEL_H_
+#define _ALIGNMENT_MODEL_H_
+
+#include "Model.h"
+
+class SparseTimeValueModel;
+
+class AlignmentModel : public Model
+{
+    Q_OBJECT
+
+public:
+    AlignmentModel(Model *reference,
+                   Model *aligned,
+                   Model *inputModel, // probably an AggregateWaveModel; I take ownership
+                   SparseTimeValueModel *path); // I take ownership
+    ~AlignmentModel();
+
+    virtual bool isOK() const;
+    virtual size_t getStartFrame() const;
+    virtual size_t getEndFrame() const;
+    virtual size_t getSampleRate() const;
+    virtual Model *clone() const;
+    virtual bool isReady(int *completion = 0) const;
+    virtual const ZoomConstraint *getZoomConstraint() const;
+
+    const Model *getReferenceModel() const;
+    const Model *getAlignedModel() const;
+
+    size_t toReference(size_t frame) const;
+    size_t fromReference(size_t frame) const;
+
+signals:
+    void modelChanged();
+    void modelChanged(size_t startFrame, size_t endFrame);
+    void completionChanged();
+
+protected slots:
+    void pathChanged();
+    void pathChanged(size_t startFrame, size_t endFrame);
+    void pathCompletionChanged();
+
+protected:
+    Model *m_reference; // I don't own this
+    Model *m_aligned; // I don't own this
+
+    Model *m_inputModel; // I own this
+    SparseTimeValueModel *m_path; // I own this
+    mutable SparseTimeValueModel *m_reversePath; // I own this
+    bool m_pathComplete;
+
+    void constructReversePath() const;
+
+    size_t align(SparseTimeValueModel *path, size_t frame) const;
+};
+
+#endif
--- a/data/model/FFTModel.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/model/FFTModel.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -15,6 +15,7 @@
 
 #include "FFTModel.h"
 #include "DenseTimeValueModel.h"
+#include "AggregateWaveModel.h"
 
 #include "base/Profiler.h"
 #include "base/Pitch.h"
@@ -34,14 +35,16 @@
     m_xshift(0),
     m_yshift(0)
 {
-    m_server = FFTDataServer::getFuzzyInstance(model,
-                                               channel,
-                                               windowType,
-                                               windowSize,
-                                               windowIncrement,
-                                               fftSize,
-                                               polar,
-                                               fillFromColumn);
+    setSourceModel(const_cast<DenseTimeValueModel *>(model)); //!!! hmm.
+
+    m_server = getServer(model,
+                         channel,
+                         windowType,
+                         windowSize,
+                         windowIncrement,
+                         fftSize,
+                         polar,
+                         fillFromColumn);
 
     if (!m_server) return; // caller should check isOK()
 
@@ -77,6 +80,61 @@
     if (m_server) FFTDataServer::releaseInstance(m_server);
 }
 
+FFTDataServer *
+FFTModel::getServer(const DenseTimeValueModel *model,
+                    int channel,
+                    WindowType windowType,
+                    size_t windowSize,
+                    size_t windowIncrement,
+                    size_t fftSize,
+                    bool polar,
+                    size_t fillFromColumn)
+{
+    // Obviously, an FFT model of channel C (where C != -1) of an
+    // aggregate model is the same as the FFT model of the appropriate
+    // channel of whichever model that aggregate channel is drawn
+    // from.  We should use that model here, in case we already have
+    // the data for it or will be wanting the same data again later.
+
+    // If the channel is -1 (i.e. mixture of all channels), then we
+    // can't do this shortcut unless the aggregate model only has one
+    // channel or contains exactly all of the channels of a single
+    // other model.  That isn't very likely -- if it were the case,
+    // why would we be using an aggregate model?
+
+    if (channel >= 0) {
+
+        const AggregateWaveModel *aggregate =
+            dynamic_cast<const AggregateWaveModel *>(model);
+
+        if (aggregate && channel < aggregate->getComponentCount()) {
+
+            AggregateWaveModel::ModelChannelSpec spec =
+                aggregate->getComponent(channel);
+
+            return getServer(spec.model,
+                             spec.channel,
+                             windowType,
+                             windowSize,
+                             windowIncrement,
+                             fftSize,
+                             polar,
+                             fillFromColumn);
+        }
+    }
+
+    // The normal case
+
+    return FFTDataServer::getFuzzyInstance(model,
+                                           channel,
+                                           windowType,
+                                           windowSize,
+                                           windowIncrement,
+                                           fftSize,
+                                           polar,
+                                           fillFromColumn);
+}
+
 size_t
 FFTModel::getSampleRate() const
 {
--- a/data/model/FFTModel.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/model/FFTModel.h	Fri Sep 28 13:56:38 2007 +0000
@@ -165,13 +165,17 @@
     virtual void resume() { m_server->resume(); }
 
 private:
-    FFTModel(const FFTModel &);
+    FFTModel(const FFTModel &); // not implemented
     FFTModel &operator=(const FFTModel &); // not implemented
 
     FFTDataServer *m_server;
     int m_xshift;
     int m_yshift;
 
+    FFTDataServer *getServer(const DenseTimeValueModel *,
+                             int, WindowType, size_t, size_t, size_t,
+                             bool, size_t);
+
     size_t getPeakPickWindowSize(PeakPickType type, size_t sampleRate,
                                  size_t bin, float &percentile) const;
 };
--- a/data/model/Model.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/model/Model.h	Fri Sep 28 13:56:38 2007 +0000
@@ -60,6 +60,12 @@
     virtual size_t getSampleRate() const = 0;
 
     /**
+     * Return the frame rate of the underlying material, if the model
+     * itself has already been resampled.
+     */
+    virtual size_t getNativeRate() const { return getSampleRate(); }
+
+    /**
      * Return a copy of this model.
      *
      * If the model is not editable, this may be effectively a shallow
@@ -104,6 +110,24 @@
         return 0;
     }
 
+    /**
+     * If this model was derived from another, return the model it was
+     * derived from.  The assumption is that the source model's
+     * alignment will also apply to this model, unless some other
+     * property indicates otherwise.
+     */
+    virtual Model *getSourceModel() const {
+        return m_sourceModel;
+    }
+
+    /**
+     * Set the source model for this model.
+     */
+     //!!! No way to handle source model deletion &c yet
+    virtual void setSourceModel(Model *model) {
+        m_sourceModel = model;
+    }
+
     virtual void toXml(QTextStream &stream,
                        QString indent = "",
                        QString extraAttributes = "") const;
@@ -135,11 +159,13 @@
     void completionChanged();
 
 protected:
-    Model() { }
+    Model() : m_sourceModel(0) { }
 
     // Not provided.
     Model(const Model &);
     Model &operator=(const Model &); 
+
+    Model *m_sourceModel;
 };
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/RangeSummarisableTimeValueModel.cpp	Fri Sep 28 13:56:38 2007 +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 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "RangeSummarisableTimeValueModel.h"
+
+#include "AlignmentModel.h"
+
+#include <iostream>
+
+void
+RangeSummarisableTimeValueModel::setAlignment(AlignmentModel *alignment)
+{
+    delete m_alignment;
+    m_alignment = alignment;
+    connect(m_alignment, SIGNAL(completionChanged()),
+            this, SIGNAL(alignmentCompletionChanged()));
+}
+
+const Model *
+RangeSummarisableTimeValueModel::getAlignmentReference() const
+{
+    if (!m_alignment) return 0;
+    return m_alignment->getReferenceModel();
+}
+
+size_t
+RangeSummarisableTimeValueModel::alignToReference(size_t frame) const
+{
+    if (!m_alignment) return frame;
+    return m_alignment->toReference(frame);
+}
+
+size_t
+RangeSummarisableTimeValueModel::alignFromReference(size_t refFrame) const
+{
+    if (!m_alignment) return refFrame;
+    return m_alignment->fromReference(refFrame);
+}
+
+int
+RangeSummarisableTimeValueModel::getAlignmentCompletion() const
+{
+    std::cerr << "RangeSummarisableTimeValueModel::getAlignmentCompletion" << std::endl;
+    if (!m_alignment) return 100;
+    int completion = 0;
+    (void)m_alignment->isReady(&completion);
+    std::cerr << " -> " << completion << std::endl;
+    return completion;
+}
+
--- a/data/model/RangeSummarisableTimeValueModel.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/model/RangeSummarisableTimeValueModel.h	Fri Sep 28 13:56:38 2007 +0000
@@ -4,7 +4,7 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006 Chris Cannam.
+    This file copyright 2006-2007 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -21,6 +21,8 @@
 #include "DenseTimeValueModel.h"
 #include "base/ZoomConstraint.h"
 
+class AlignmentModel;
+
 /**
  * Base class for models containing dense two-dimensional data (value
  * against time) that may be meaningfully represented in a zoomed view
@@ -33,6 +35,8 @@
     Q_OBJECT
 
 public:
+    RangeSummarisableTimeValueModel() : m_alignment(0) { }
+
     struct Range
     {
         float min;
@@ -68,6 +72,18 @@
      * and end frames.
      */
     virtual Range getRange(size_t channel, size_t start, size_t end) const = 0;
+
+    virtual void setAlignment(AlignmentModel *alignment); // I take ownership
+    virtual const Model *getAlignmentReference() const;
+    virtual size_t alignToReference(size_t frame) const;
+    virtual size_t alignFromReference(size_t referenceFrame) const;
+    virtual int getAlignmentCompletion() const;
+
+signals:
+    void alignmentCompletionChanged();
+
+protected:
+    AlignmentModel *m_alignment;
 };
 
 #endif
--- a/data/model/SparseModel.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/model/SparseModel.h	Fri Sep 28 13:56:38 2007 +0000
@@ -87,6 +87,11 @@
     virtual PointList getPoints(long frame) const;
 
     /**
+     * Get all points.
+     */
+    virtual const PointList &getPoints() const { return m_points; }
+
+    /**
      * Return all points that share the nearest frame number prior to
      * the given one at which there are any points.
      */
@@ -116,6 +121,12 @@
      */
     virtual void deletePoint(const PointType &point);
 
+    virtual bool isReady(int *completion = 0) const {
+        bool ready = isOK() && (m_completion == 100);
+        if (completion) *completion = m_completion;
+        return ready;
+    }
+
     virtual void setCompletion(int completion);
     virtual int getCompletion() const { return m_completion; }
 
@@ -505,13 +516,17 @@
 void
 SparseModel<PointType>::setCompletion(int completion)
 {
-//    std::cerr << "SparseModel::setCompletion(" << completion << ")" << std::endl;
+    std::cerr << "SparseModel::setCompletion(" << completion << ")" << std::endl;
 
     if (m_completion != completion) {
 	m_completion = completion;
 
 	if (completion == 100) {
 
+            if (!m_notifyOnAdd) {
+                emit completionChanged();
+            }
+
 	    m_notifyOnAdd = true; // henceforth
 	    emit modelChanged();
 
--- a/data/model/WaveFileModel.cpp	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/model/WaveFileModel.cpp	Fri Sep 28 13:56:38 2007 +0000
@@ -38,7 +38,7 @@
 PowerOfSqrtTwoZoomConstraint
 WaveFileModel::m_zoomConstraint;
 
-WaveFileModel::WaveFileModel(QString path) :
+WaveFileModel::WaveFileModel(QString path, size_t targetRate) :
     m_path(path),
     m_myReader(true),
     m_fillThread(0),
@@ -46,13 +46,14 @@
     m_lastFillExtent(0),
     m_exiting(false)
 {
-    m_reader = AudioFileReaderFactory::createReader(path);
+    m_reader = AudioFileReaderFactory::createReader(path, targetRate);
+    if (m_reader) std::cerr << "WaveFileModel::WaveFileModel: reader rate: " << m_reader->getSampleRate() << std::endl;
     if (m_reader) setObjectName(m_reader->getTitle());
     if (objectName() == "") setObjectName(QFileInfo(path).fileName());
     if (isOK()) fillCache();
 }
 
-WaveFileModel::WaveFileModel(QString path, QString originalLocation) :
+WaveFileModel::WaveFileModel(QString path, QString originalLocation, size_t targetRate) :
     m_path(originalLocation),
     m_myReader(true),
     m_fillThread(0),
@@ -60,7 +61,8 @@
     m_lastFillExtent(0),
     m_exiting(false)
 {
-    m_reader = AudioFileReaderFactory::createReader(path);
+    m_reader = AudioFileReaderFactory::createReader(path, targetRate);
+    if (m_reader) std::cerr << "WaveFileModel::WaveFileModel: reader rate: " << m_reader->getSampleRate() << std::endl;
     if (m_reader) setObjectName(m_reader->getTitle());
     if (objectName() == "") setObjectName(QFileInfo(originalLocation).fileName());
     if (isOK()) fillCache();
@@ -151,6 +153,15 @@
 }
 
 size_t
+WaveFileModel::getNativeRate() const 
+{
+    if (!m_reader) return 0;
+    size_t rate = m_reader->getNativeRate();
+    if (rate == 0) rate = getSampleRate();
+    return rate;
+}
+
+size_t
 WaveFileModel::getValues(int channel, size_t start, size_t end,
 			 float *buffer) const
 {
--- a/data/model/WaveFileModel.h	Fri Sep 21 09:13:11 2007 +0000
+++ b/data/model/WaveFileModel.h	Fri Sep 28 13:56:38 2007 +0000
@@ -32,8 +32,8 @@
     Q_OBJECT
 
 public:
-    WaveFileModel(QString path);
-    WaveFileModel(QString path, QString originalLocation);
+    WaveFileModel(QString path, size_t targetRate = 0);
+    WaveFileModel(QString path, QString originalLocation, size_t targetRate = 0);
     WaveFileModel(QString originalLocation, AudioFileReader *reader);
     ~WaveFileModel();
 
@@ -45,6 +45,7 @@
     size_t getFrameCount() const;
     size_t getChannelCount() const;
     size_t getSampleRate() const;
+    size_t getNativeRate() const;
 
     virtual Model *clone() const;