changeset 384:6f6ab834449d spectrogram-cache-rejig

* Merge from trunk
author Chris Cannam
date Wed, 27 Feb 2008 11:59:42 +0000
parents a6fab10ff9e6
children
files base/Clipboard.cpp base/Clipboard.h base/CommandHistory.cpp base/Pitch.cpp base/Pitch.h base/Preferences.cpp base/Preferences.h base/PropertyContainer.cpp base/PropertyContainer.h base/RangeMapper.cpp base/RangeMapper.h base/RealTime.cpp base/RealTime.h base/StorageAdviser.cpp base/Window.h base/XmlExportable.cpp data/data.pro data/fft/FFTDataServer.cpp data/fft/FFTFileCache.cpp data/fft/FFTMemoryCache.cpp data/fileio/AudioFileReader.h data/fileio/CodedAudioFileReader.cpp data/fileio/CodedAudioFileReader.h data/fileio/FileFinder.cpp data/fileio/FileSource.cpp data/fileio/FileSource.h data/fileio/MIDIFileReader.cpp data/fileio/MIDIFileWriter.cpp data/fileio/MP3FileReader.cpp data/fileio/MP3FileReader.h data/fileio/OggVorbisFileReader.cpp data/fileio/OggVorbisFileReader.h data/fileio/ProgressPrinter.cpp data/fileio/ProgressPrinter.h data/fileio/QuickTimeFileReader.cpp data/fileio/QuickTimeFileReader.h data/fileio/ResamplingWavFileReader.h data/fileio/WavFileReader.cpp data/fileio/WavFileReader.h data/model/AggregateWaveModel.cpp data/model/AggregateWaveModel.h data/model/AlignmentModel.cpp data/model/AlignmentModel.h data/model/DenseThreeDimensionalModel.h data/model/DenseTimeValueModel.h data/model/EditableDenseThreeDimensionalModel.h data/model/FFTModel.cpp data/model/FFTModel.h data/model/ImageModel.h data/model/Labeller.h data/model/Model.cpp data/model/Model.h data/model/NoteModel.h data/model/PowerOfSqrtTwoZoomConstraint.cpp data/model/RangeSummarisableTimeValueModel.h data/model/SparseModel.h data/model/SparseOneDimensionalModel.h data/model/SparseTimeValueModel.h data/model/SparseValueModel.h data/model/TextModel.h data/model/WaveFileModel.cpp data/model/WaveFileModel.h data/model/WritableWaveFileModel.cpp data/model/WritableWaveFileModel.h plugin/DSSIPluginInstance.cpp plugin/DSSIPluginInstance.h plugin/FeatureExtractionPluginFactory.cpp plugin/LADSPAPluginFactory.cpp plugin/LADSPAPluginInstance.cpp plugin/LADSPAPluginInstance.h plugin/RealTimePluginInstance.h plugin/plugin.pro plugin/transform/FeatureExtractionModelTransformer.cpp plugin/transform/FeatureExtractionModelTransformer.h plugin/transform/ModelTransformer.cpp plugin/transform/ModelTransformer.h plugin/transform/ModelTransformerFactory.cpp plugin/transform/ModelTransformerFactory.h plugin/transform/PluginTransformer.cpp plugin/transform/PluginTransformer.h plugin/transform/RealTimeEffectModelTransformer.cpp plugin/transform/RealTimeEffectModelTransformer.h plugin/transform/Transform.cpp plugin/transform/Transform.h plugin/transform/TransformDescription.h plugin/transform/TransformFactory.cpp plugin/transform/TransformFactory.h system/Init.cpp system/System.h
diffstat 89 files changed, 2539 insertions(+), 793 deletions(-) [+]
line wrap: on
line diff
--- a/base/Clipboard.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/Clipboard.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -21,7 +21,11 @@
     m_haveValue(false),
     m_haveDuration(false),
     m_haveLabel(true),
-    m_label(label)
+    m_label(label),
+    m_haveLevel(false),
+    m_level(0.f),
+    m_haveReferenceFrame(false),
+    m_referenceFrame(frame)
 {
 }
 
@@ -32,7 +36,11 @@
     m_value(value),
     m_haveDuration(false),
     m_haveLabel(true),
-    m_label(label)
+    m_label(label),
+    m_haveLevel(false),
+    m_level(0.f),
+    m_haveReferenceFrame(false),
+    m_referenceFrame(frame)
 {
 }
 
@@ -44,7 +52,27 @@
     m_haveDuration(true),
     m_duration(duration),
     m_haveLabel(true),
-    m_label(label)
+    m_label(label),
+    m_haveLevel(false),
+    m_level(0.f),
+    m_haveReferenceFrame(false),
+    m_referenceFrame(frame)
+{
+}
+
+Clipboard::Point::Point(long frame, float value, size_t duration, float level, QString label) :
+    m_haveFrame(true),
+    m_frame(frame),
+    m_haveValue(true),
+    m_value(value),
+    m_haveDuration(true),
+    m_duration(duration),
+    m_haveLabel(true),
+    m_label(label),
+    m_haveLevel(true),
+    m_level(level),
+    m_haveReferenceFrame(false),
+    m_referenceFrame(frame)
 {
 }
 
@@ -56,7 +84,11 @@
     m_haveDuration(point.m_haveDuration),
     m_duration(point.m_duration),
     m_haveLabel(point.m_haveLabel),
-    m_label(point.m_label)
+    m_label(point.m_label),
+    m_haveLevel(point.m_haveLevel),
+    m_level(point.m_level),
+    m_haveReferenceFrame(point.m_haveReferenceFrame),
+    m_referenceFrame(point.m_referenceFrame)
 {
 }
 
@@ -72,6 +104,10 @@
     m_duration = point.m_duration;
     m_haveLabel = point.m_haveLabel;
     m_label = point.m_label;
+    m_haveLevel = point.m_haveLevel;
+    m_level = point.m_level;
+    m_haveReferenceFrame = point.m_haveReferenceFrame;
+    m_referenceFrame = point.m_referenceFrame;
     return *this;
 }
 
@@ -123,6 +159,43 @@
     return m_label;
 }
 
+bool
+Clipboard::Point::haveLevel() const
+{
+    return m_haveLevel;
+}
+
+float
+Clipboard::Point::getLevel() const
+{
+    return m_level;
+}
+
+bool
+Clipboard::Point::haveReferenceFrame() const
+{
+    return m_haveReferenceFrame;
+}
+
+bool
+Clipboard::Point::referenceFrameDiffers() const
+{
+    return m_haveReferenceFrame && (m_referenceFrame != m_frame);
+}
+
+long
+Clipboard::Point::getReferenceFrame() const
+{
+    return m_referenceFrame;
+}
+
+void
+Clipboard::Point::setReferenceFrame(long f) 
+{
+    m_haveReferenceFrame = true;
+    m_referenceFrame = f;
+}
+
 Clipboard::Clipboard() { }
 Clipboard::~Clipboard() { }
 
@@ -156,3 +229,23 @@
     m_points.push_back(point);
 }
 
+bool
+Clipboard::haveReferenceFrames() const
+{
+    for (PointList::const_iterator i = m_points.begin();
+         i != m_points.end(); ++i) {
+        if (i->haveReferenceFrame()) return true;
+    } 
+    return false;
+}
+
+bool
+Clipboard::referenceFramesDiffer() const
+{
+    for (PointList::const_iterator i = m_points.begin();
+         i != m_points.end(); ++i) {
+        if (i->referenceFrameDiffers()) return true;
+    } 
+    return false;
+}
+
--- a/base/Clipboard.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/Clipboard.h	Wed Feb 27 11:59:42 2008 +0000
@@ -28,6 +28,7 @@
         Point(long frame, QString label);
         Point(long frame, float value, QString label);
         Point(long frame, float value, size_t duration, QString label);
+        Point(long frame, float value, size_t duration, float level, QString label);
         Point(const Point &point);
         Point &operator=(const Point &point);
 
@@ -43,6 +44,15 @@
         bool haveLabel() const;
         QString getLabel() const;
 
+        bool haveLevel() const;
+        float getLevel() const;
+
+        bool haveReferenceFrame() const;
+        bool referenceFrameDiffers() const; // from point frame
+
+        long getReferenceFrame() const;
+        void setReferenceFrame(long);
+
     private:
         bool m_haveFrame;
         long m_frame;
@@ -52,6 +62,10 @@
         size_t m_duration;
         bool m_haveLabel;
         QString m_label;
+        bool m_haveLevel;
+        float m_level;
+        bool m_haveReferenceFrame;
+        long m_referenceFrame;
     };
 
     Clipboard();
@@ -65,6 +79,9 @@
     void setPoints(const PointList &points);
     void addPoint(const Point &point);
 
+    bool haveReferenceFrames() const;
+    bool referenceFramesDiffer() const;
+
 protected:
     PointList m_points;
 };
--- a/base/CommandHistory.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/CommandHistory.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -216,6 +216,10 @@
 CommandHistory::addToCompound(Command *command, bool execute)
 {
 //    std::cerr << "CommandHistory::addToCompound: " << command->getName().toLocal8Bit().data() << std::endl;
+    if (!m_currentCompound) {
+	std::cerr << "CommandHistory::addToCompound: ERROR: no compound operation in progress!" << std::endl;
+        return;
+    }
 
     if (execute) command->execute();
     m_currentCompound->addCommand(command);
@@ -227,6 +231,7 @@
     if (m_currentCompound) {
 	std::cerr << "CommandHistory::startCompoundOperation: ERROR: compound operation already in progress!" << std::endl;
 	std::cerr << "(name is " << m_currentCompound->getName().toLocal8Bit().data() << ")" << std::endl;
+        return;
     }
  
     closeBundle();
@@ -240,6 +245,7 @@
 {
     if (!m_currentCompound) {
 	std::cerr << "CommandHistory::endCompoundOperation: ERROR: no compound operation in progress!" << std::endl;
+        return;
     }
 
     MacroCommand *toAdd = m_currentCompound;
--- a/base/Pitch.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/Pitch.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -52,6 +52,37 @@
     return midiPitch;
 }
 
+int
+Pitch::getPitchForFrequencyDifference(float frequencyA,
+                                      float frequencyB,
+                                      float *centsOffsetReturn,
+                                      float concertA)
+{
+    if (concertA <= 0.0) {
+        concertA = Preferences::getInstance()->getTuningFrequency();
+    }
+
+    if (frequencyA > frequencyB) {
+        std::swap(frequencyA, frequencyB);
+    }
+
+    float pA = 12.0 * (log(frequencyA / (concertA / 2.0)) / log(2.0)) + 57.0;
+    float pB = 12.0 * (log(frequencyB / (concertA / 2.0)) / log(2.0)) + 57.0;
+
+    float p = pB - pA;
+
+    int midiPitch = int(p + 0.00001);
+    float centsOffset = (p - midiPitch) * 100.0;
+
+    if (centsOffset >= 50.0) {
+	midiPitch = midiPitch + 1;
+	centsOffset = -(100.0 - centsOffset);
+    }
+    
+    if (centsOffsetReturn) *centsOffsetReturn = centsOffset;
+    return midiPitch;
+}
+
 static QString notes[] = {
     "C%1",  "C#%1", "D%1",  "D#%1",
     "E%1",  "F%1",  "F#%1", "G%1",
@@ -101,6 +132,34 @@
     return getPitchLabel(midiPitch, centsOffset, useFlats);
 }
 
+QString
+Pitch::getLabelForPitchRange(int semis, float cents)
+{
+    int ic = lrintf(cents);
+
+    if (ic == 0) {
+        if (semis >= 12) {
+            return QString("%1'%2").arg(semis/12).arg(semis - 12*(semis/12));
+        } else {
+            return QString("%1").arg(semis);
+        }
+    } else {
+        if (ic > 0) {
+            if (semis >= 12) {
+                return QString("%1'%2+%3c").arg(semis/12).arg(semis - 12*(semis/12)).arg(ic);
+            } else {
+                return QString("%1+%3c").arg(semis).arg(ic);
+            }
+        } else {
+            if (semis >= 12) {
+                return QString("%1'%2%3c").arg(semis/12).arg(semis - 12*(semis/12)).arg(ic);
+            } else {
+                return QString("%1%3c").arg(semis).arg(ic);
+            }
+        }
+    }
+}
+
 bool
 Pitch::isFrequencyInMidiRange(float frequency,
                               float concertA)
--- a/base/Pitch.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/Pitch.h	Wed Feb 27 11:59:42 2008 +0000
@@ -51,6 +51,25 @@
 				    float concertA = 0.0);
 
     /**
+     * Return the nearest MIDI pitch range to the given frequency
+     * range, that is, the difference in MIDI pitch values between the
+     * higher and lower frequencies.
+     *
+     * If centsOffsetReturn is non-NULL, return in *centsOffsetReturn
+     * the number of cents (1/100ths of a semitone) difference between
+     * the given frequency difference and the returned MIDI pitch
+     * range.  The cents offset will be in the range [-50,50).
+     * 
+     * If concertA is non-zero, use that as the reference frequency
+     * for the A at MIDI pitch 69; otherwise use the tuning frequency
+     * specified in the application preferences (default 440Hz).
+     */
+    static int getPitchForFrequencyDifference(float frequencyA,
+                                              float frequencyB,
+                                              float *centsOffsetReturn = 0,
+                                              float concertA = 0.0);
+
+    /**
      * Return a string describing the given MIDI pitch, with optional
      * cents offset.  This consists of the note name, octave number
      * (with MIDI pitch 0 having octave number -2), and optional
@@ -82,6 +101,12 @@
 					     bool useFlats = false);
 
     /**
+     * Return a string describing the given pitch range in octaves,
+     * semitones and cents.  This is in the form e.g. "1'2+4c".
+     */
+    static QString getLabelForPitchRange(int semis, float cents = 0);
+
+    /**
      * Return true if the given frequency falls within the range of
      * MIDI note pitches, plus or minus half a semitone.  This is
      * equivalent to testing whether getPitchForFrequency returns a
--- a/base/Preferences.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/Preferences.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -23,6 +23,8 @@
 #include <QFileInfo>
 #include <QMutex>
 #include <QSettings>
+#include <QApplication>
+#include <QFont>
 
 Preferences *
 Preferences::m_instance = 0;
@@ -42,7 +44,10 @@
     m_resampleQuality(1),
     m_omitRecentTemps(true),
     m_tempDirRoot(""),
-    m_resampleOnLoad(false)
+    m_resampleOnLoad(false),
+    m_viewFontSize(10),
+    m_backgroundMode(BackgroundFromTheme),
+    m_showSplash(true)
 {
     QSettings settings;
     settings.beginGroup("Preferences");
@@ -57,6 +62,10 @@
     m_resampleOnLoad = settings.value("resample-on-load", false).toBool();
     m_backgroundMode = BackgroundMode
         (settings.value("background-mode", int(BackgroundFromTheme)).toInt());
+    m_viewFontSize = settings.value
+        ("view-font-size", int(QApplication::font().pointSize() * 0.9))
+        .toInt();
+    m_showSplash = settings.value("show-splash", true).toBool();
     settings.endGroup();
 
     settings.beginGroup("TempDirectory");
@@ -81,6 +90,8 @@
     props.push_back("Resample On Load");
     props.push_back("Temporary Directory Root");
     props.push_back("Background Mode");
+    props.push_back("View Font Size");
+    props.push_back("Show Splash Screen");
     return props;
 }
 
@@ -88,7 +99,7 @@
 Preferences::getPropertyLabel(const PropertyName &name) const
 {
     if (name == "Spectrogram Smoothing") {
-        return tr("Spectrogram y-axis smoothing:");
+        return tr("Spectrogram y-axis interpolation:");
     }
     if (name == "Tuning Frequency") {
         return tr("Frequency of concert A");
@@ -114,6 +125,12 @@
     if (name == "Background Mode") {
         return tr("Background colour preference");
     }
+    if (name == "View Font Size") {
+        return tr("Font size for text overlays");
+    }
+    if (name == "Show Splash Screen") {
+        return tr("Show splash screen on startup");
+    }
     return name;
 }
 
@@ -148,6 +165,12 @@
     if (name == "Background Mode") {
         return ValueProperty;
     }
+    if (name == "View Font Size") {
+        return RangeProperty;
+    }
+    if (name == "Show Splash Screen") {
+        return ToggleProperty;
+    }
     return InvalidProperty;
 }
 
@@ -196,6 +219,17 @@
         return int(m_backgroundMode);
     }        
 
+    if (name == "View Font Size") {
+        if (min) *min = 3;
+        if (max) *max = 48;
+        if (deflt) *deflt = int(QApplication::font().pointSize() * 0.9);
+        return int(m_viewFontSize);
+    }
+
+    if (name == "Show Splash Screen") {
+        if (deflt) *deflt = 1;
+    }
+
     return 0;
 }
 
@@ -212,7 +246,7 @@
         case RectangularWindow: return tr("Rectangular");
         case BartlettWindow: return tr("Triangular");
         case HammingWindow: return tr("Hamming");
-        case HanningWindow: return tr("Hanning");
+        case HanningWindow: return tr("Hann");
         case BlackmanWindow: return tr("Blackman");
         case GaussianWindow: return tr("Gaussian");
         case ParzenWindow: return tr("Parzen");
@@ -230,8 +264,8 @@
     if (name == "Spectrogram Smoothing") {
         switch (value) {
         case NoSpectrogramSmoothing: return tr("None - blocky but accurate");
-        case SpectrogramInterpolated: return tr("Interpolate - fast but fuzzy");
-        case SpectrogramZeroPadded: return tr("Zero pad FFT - slow but clear");
+        case SpectrogramInterpolated: return tr("Linear - fast but fuzzy");
+        case SpectrogramZeroPadded: return tr("4 x Oversampled - slow but clear");
         }
     }
     if (name == "Background Mode") {
@@ -274,6 +308,10 @@
         setOmitTempsFromRecentFiles(value ? true : false);
     } else if (name == "Background Mode") {
         setBackgroundMode(BackgroundMode(value));
+    } else if (name == "View Font Size") {
+        setViewFontSize(value);
+    } else if (name == "Show Splash Screen") {
+        setShowSplash(value ? true : false);
     }
 }
 
@@ -403,4 +441,33 @@
     }
 }
 
+void
+Preferences::setViewFontSize(int size)
+{
+    if (m_viewFontSize != size) {
 
+        m_viewFontSize = size;
+
+        QSettings settings;
+        settings.beginGroup("Preferences");
+        settings.setValue("view-font-size", size);
+        settings.endGroup();
+        emit propertyChanged("View Font Size");
+    }
+}
+
+void
+Preferences::setShowSplash(bool show) 
+{
+    if (m_showSplash != show) {
+
+        m_showSplash = show;
+
+        QSettings settings;
+        settings.beginGroup("Preferences");
+        settings.setValue("show-splash", show);
+        settings.endGroup();
+        emit propertyChanged("Show Splash Screen");
+    }
+}
+        
--- a/base/Preferences.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/Preferences.h	Wed Feb 27 11:59:42 2008 +0000
@@ -54,6 +54,8 @@
     };
     PropertyBoxLayout getPropertyBoxLayout() const { return m_propertyBoxLayout; }
 
+    int getViewFontSize() const { return m_viewFontSize; }
+
     bool getOmitTempsFromRecentFiles() const { return m_omitRecentTemps; }
 
     QString getTemporaryDirectoryRoot() const { return m_tempDirRoot; }
@@ -67,6 +69,8 @@
     };
     BackgroundMode getBackgroundMode() const { return m_backgroundMode; }
 
+    bool getShowSplash() const { return m_showSplash; }
+
 public slots:
     virtual void setProperty(const PropertyName &, int);
 
@@ -79,6 +83,8 @@
     void setTemporaryDirectoryRoot(QString tempDirRoot);
     void setResampleOnLoad(bool);
     void setBackgroundMode(BackgroundMode mode);
+    void setViewFontSize(int size);
+    void setShowSplash(bool);
 
 private:
     Preferences(); // may throw DirectoryCreationFailed
@@ -94,7 +100,9 @@
     bool m_omitRecentTemps;
     QString m_tempDirRoot;
     bool m_resampleOnLoad;
+    int m_viewFontSize;
     BackgroundMode m_backgroundMode;
+    bool m_showSplash;
 };
 
 #endif
--- a/base/PropertyContainer.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/PropertyContainer.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -29,12 +29,6 @@
     return PropertyList();
 }
 
-//QString
-//PropertyContainer::getPropertyLabel(const PropertyName &) const
-//{
-//    return "";
-//}
-
 PropertyContainer::PropertyType
 PropertyContainer::getPropertyType(const PropertyName &) const
 {
@@ -42,6 +36,12 @@
 }
 
 QString
+PropertyContainer::getPropertyIconName(const PropertyName &) const
+{
+    return QString();
+}
+
+QString
 PropertyContainer::getPropertyGroupName(const PropertyName &) const
 {
     return QString();
--- a/base/PropertyContainer.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/PropertyContainer.h	Wed Feb 27 11:59:42 2008 +0000
@@ -62,6 +62,11 @@
     virtual PropertyType getPropertyType(const PropertyName &) const;
 
     /**
+     * Return an icon for the property, if any.
+     */
+    virtual QString getPropertyIconName(const PropertyName &) const;
+
+    /**
      * If this property has something in common with other properties
      * on this container, return a name that can be used to group them
      * (in order to save screen space, for example).  e.g. "Window
--- a/base/RangeMapper.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/RangeMapper.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -60,29 +60,52 @@
 }
 
 LogRangeMapper::LogRangeMapper(int minpos, int maxpos,
-                               float ratio, float minlog,
+                               float minval, float maxval,
                                QString unit) :
     m_minpos(minpos),
     m_maxpos(maxpos),
-    m_ratio(ratio),
-    m_minlog(minlog),
     m_unit(unit)
 {
+    convertMinMax(minpos, maxpos, minval, maxval, m_minlog, m_ratio);
+
+    std::cerr << "LogRangeMapper: minpos " << minpos << ", maxpos "
+              << maxpos << ", minval " << minval << ", maxval "
+              << maxval << ", minlog " << m_minlog << ", ratio " << m_ratio
+              << ", unit " << unit.toStdString() << std::endl;
+
     assert(m_maxpos != m_minpos);
 
     m_maxlog = (m_maxpos - m_minpos) / m_ratio + m_minlog;
 }
 
+void
+LogRangeMapper::convertMinMax(int minpos, int maxpos,
+                              float minval, float maxval,
+                              float &minlog, float &ratio)
+{
+    static float thresh = powf(10, -10);
+    if (minval < thresh) minval = thresh;
+    minlog = log10f(minval);
+    ratio = (maxpos - minpos) / (log10f(maxval) - minlog);
+}
+
+void
+LogRangeMapper::convertRatioMinLog(float ratio, float minlog,
+                                   int minpos, int maxpos,
+                                   float &minval, float &maxval)
+{
+    minval = powf(10, minlog);
+    maxval = powf(10, (maxpos - minpos) / ratio + minlog);
+}
+
 int
 LogRangeMapper::getPositionForValue(float value) const
 {
-    float mapped = m_ratio * log10(value);
-    int position = lrintf(((mapped - m_minlog) / (m_maxlog - m_minlog))
-                          * (m_maxpos - m_minpos));
+    int position = (log10(value) - m_minlog) * m_ratio + m_minpos;
     if (position < m_minpos) position = m_minpos;
     if (position > m_maxpos) position = m_maxpos;
-//    std::cerr << "LogRangeMapper::getPositionForValue: " << value << " -> "
-//              << position << " (minpos " << m_minpos << ", maxpos " << m_maxpos << ", ratio " << m_ratio << ", minlog " << m_minlog << ")" << std::endl;
+    std::cerr << "LogRangeMapper::getPositionForValue: " << value << " -> "
+              << position << " (minpos " << m_minpos << ", maxpos " << m_maxpos << ", ratio " << m_ratio << ", minlog " << m_minlog << ")" << std::endl;
     return position;
 }
 
@@ -90,8 +113,8 @@
 LogRangeMapper::getValueForPosition(int position) const
 {
     float value = powf(10, (position - m_minpos) / m_ratio + m_minlog);
-//    std::cerr << "LogRangeMapper::getValueForPosition: " << position << " -> "
-//              << value << " (minpos " << m_minpos << ", maxpos " << m_maxpos << ", ratio " << m_ratio << ", minlog " << m_minlog << ")" << std::endl;
+    std::cerr << "LogRangeMapper::getValueForPosition: " << position << " -> "
+              << value << " (minpos " << m_minpos << ", maxpos " << m_maxpos << ", ratio " << m_ratio << ", minlog " << m_minlog << ")" << std::endl;
     return value;
 }
 
--- a/base/RangeMapper.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/RangeMapper.h	Wed Feb 27 11:59:42 2008 +0000
@@ -54,9 +54,17 @@
 {
 public:
     LogRangeMapper(int minpos, int maxpos,
-                   float ratio, float minlog,
+                   float minval, float maxval,
                    QString m_unit = "");
 
+    static void convertRatioMinLog(float ratio, float minlog,
+                                   int minpos, int maxpos,
+                                   float &minval, float &maxval);
+
+    static void convertMinMax(int minpos, int maxpos,
+                              float minval, float maxval,
+                              float &ratio, float &minlog);
+
     virtual int getPositionForValue(float value) const;
     virtual float getValueForPosition(int position) const;
 
--- a/base/RealTime.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/RealTime.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -122,6 +122,51 @@
     return s.substr(0, s.length() - 1);
 }
 
+RealTime
+RealTime::fromString(std::string s)
+{
+    bool negative = false;
+    bool faulty = false;
+    bool section = 0;
+    std::string ssec, snsec;
+
+    for (size_t i = 0; i < s.length(); ++i) {
+
+        char c = s[i];
+        if (isspace(c)) continue;
+
+        if (section == 0) {
+
+            if (c == '-') negative = true;
+            else if (isdigit(c)) { section = 1; ssec += c; }
+            else if (c == '.') section = 2;
+            else break;
+
+        } else if (section == 1) {
+
+            if (c == '.') section = 2;
+            else if (isdigit(c)) ssec += c;
+            else break;
+
+        } else if (section == 2) {
+
+            if (isdigit(c)) snsec += c;
+            else break;
+        }
+    }
+
+    while (snsec.length() < 8) snsec += '0';
+
+    int sec = atoi(ssec.c_str());
+    int nsec = atoi(snsec.c_str());
+    if (negative) sec = -sec;
+
+    std::cerr << "RealTime::fromString: string " << s << " -> "
+              << sec << " sec, " << nsec << " nsec" << std::endl;
+
+    return RealTime(sec, nsec);
+}
+
 std::string
 RealTime::toText(bool fixedDp) const
 {
@@ -227,6 +272,22 @@
     return RealTime(secdiv, int(nsecdiv + 0.5));
 }
 
+RealTime
+RealTime::operator*(double m) const
+{
+    double t = (double(nsec) / ONE_BILLION) * m;
+    t += sec * m;
+    return fromSeconds(t);
+}
+
+RealTime
+RealTime::operator/(double d) const
+{
+    double t = (double(nsec) / ONE_BILLION) / d;
+    t += sec / d;
+    return fromSeconds(t);
+}
+
 double 
 RealTime::operator/(const RealTime &r) const
 {
--- a/base/RealTime.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/RealTime.h	Wed Feb 27 11:59:42 2008 +0000
@@ -95,30 +95,48 @@
     RealTime operator*(int m) const;
     RealTime operator/(int d) const;
 
-    // Find the fractional difference between times
-    //
+    RealTime operator*(double m) const;
+    RealTime operator/(double d) const;
+
+    /**
+     * Return the ratio of two times.
+     */
     double operator/(const RealTime &r) const;
 
-    // Return a human-readable debug-type string to full precision
-    // (probably not a format to show to a user directly).  If align
-    // is true, prepend " " to the start of positive values so that
-    // they line up with negative ones (which start with "-").
-    // 
+    /**
+     * Return a human-readable debug-type string to full precision
+     * (probably not a format to show to a user directly).  If align
+     * is true, prepend " " to the start of positive values so that
+     * they line up with negative ones (which start with "-").
+     */ 
     std::string toString(bool align = false) const;
 
-    // Return a user-readable string to the nearest millisecond
-    // in a form like HH:MM:SS.mmm
-    //
+    /**
+     * Convert a string as obtained from toString back to a RealTime
+     * object.
+     */
+    static RealTime fromString(std::string);
+
+    /**
+     * Return a user-readable string to the nearest millisecond, in a
+     * form like HH:MM:SS.mmm
+     */
     std::string toText(bool fixedDp = false) const;
 
-    // Return a user-readable string to the nearest second in a form
-    // like "6s" (for less than a minute) or "2:21" (for more).
-    //
+    /**
+     * Return a user-readable string to the nearest second, in a form
+     * like "6s" (for less than a minute) or "2:21" (for more).
+     */
     std::string toSecText() const;
 
-    // Convenience functions for handling sample frames
-    //
+    /**
+     * Convert a RealTime into a sample frame at the given sample rate.
+     */
     static long realTime2Frame(const RealTime &r, unsigned int sampleRate);
+
+    /**
+     * Convert a sample frame at the given sample rate into a RealTime.
+     */
     static RealTime frame2RealTime(long frame, unsigned int sampleRate);
 
     static const RealTime zeroTime;
--- a/base/StorageAdviser.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/StorageAdviser.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -22,6 +22,8 @@
 
 #include <iostream>
 
+//#define DEBUG_STORAGE_ADVISER 1
+
 long StorageAdviser::m_discPlanned = 0;
 long StorageAdviser::m_memoryPlanned = 0;
 
@@ -30,9 +32,11 @@
 			  int minimumSize,
 			  int maximumSize)
 {
+#ifdef DEBUG_STORAGE_ADVISER
     std::cerr << "StorageAdviser::recommend: Criteria " << criteria 
               << ", minimumSize " << minimumSize
               << ", maximumSize " << maximumSize << std::endl;
+#endif
 
     QString path = TempDirectory::getInstance()->getPath();
     int discFree = GetDiscSpaceMBAvailable(path.toLocal8Bit());
@@ -51,7 +55,9 @@
         memoryFree = 0;
     }
 
+#ifdef DEBUG_STORAGE_ADVISER
     std::cerr << "Disc space: " << discFree << ", memory free: " << memoryFree << ", memory total: " << memoryTotal << ", min " << minimumSize << ", max " << maximumSize << std::endl;
+#endif
 
     //!!! We have a potentially serious problem here if multiple
     //recommendations are made in advance of any of the resulting
@@ -86,8 +92,10 @@
     else if (minmb > (discFree / 10)) discStatus = Marginal;
     else discStatus = Sufficient;
 
+#ifdef DEBUG_STORAGE_ADVISER
     std::cerr << "Memory status: " << memoryStatus << ", disc status "
               << discStatus << std::endl;
+#endif
 
     int recommendation = NoRecommendation;
 
--- a/base/Window.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/Window.h	Wed Feb 27 11:59:42 2008 +0000
@@ -18,6 +18,7 @@
 
 #include <cmath>
 #include <iostream>
+#include <string>
 #include <map>
 
 enum WindowType {
@@ -61,6 +62,12 @@
     WindowType getType() const { return m_type; }
     size_t getSize() const { return m_size; }
 
+    // The names used by these functions are un-translated, for use in
+    // e.g. XML I/O.  Use Preferences::getPropertyValueLabel if you
+    // want translated names for use in the user interface.
+    static std::string getNameForType(WindowType type);
+    static WindowType getTypeForName(std::string name);
+
 protected:
     WindowType m_type;
     size_t m_size;
@@ -159,4 +166,46 @@
     }
 }
 
+template <typename T>
+std::string
+Window<T>::getNameForType(WindowType type)
+{
+    switch (type) {
+    case RectangularWindow:    return "rectangular";
+    case BartlettWindow:       return "bartlett";
+    case HammingWindow:        return "hamming";
+    case HanningWindow:        return "hanning";
+    case BlackmanWindow:       return "blackman";
+    case GaussianWindow:       return "gaussian";
+    case ParzenWindow:         return "parzen";
+    case NuttallWindow:        return "nuttall";
+    case BlackmanHarrisWindow: return "blackman-harris";
+    }
+
+    std::cerr << "WARNING: Window::getNameForType: unknown type "
+              << type << std::endl;
+
+    return "unknown";
+}
+
+template <typename T>
+WindowType
+Window<T>::getTypeForName(std::string name)
+{
+    if (name == "rectangular")     return RectangularWindow;
+    if (name == "bartlett")        return BartlettWindow;
+    if (name == "hamming")         return HammingWindow;
+    if (name == "hanning")         return HanningWindow;
+    if (name == "blackman")        return BlackmanWindow;
+    if (name == "gaussian")        return GaussianWindow;
+    if (name == "parzen")          return ParzenWindow;
+    if (name == "nuttall")         return NuttallWindow;
+    if (name == "blackman-harris") return BlackmanHarrisWindow;
+
+    std::cerr << "WARNING: Window::getTypeForName: unknown name \""
+              << name << "\", defaulting to \"hanning\"" << std::endl;
+
+    return HanningWindow;
+}
+
 #endif
--- a/base/XmlExportable.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/base/XmlExportable.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -25,7 +25,7 @@
 XmlExportable::toXmlString(QString indent,
                            QString extraAttributes) const
 {
-    std::cerr << "XmlExportable::toXmlString" << std::endl;
+//    std::cerr << "XmlExportable::toXmlString" << std::endl;
 
     QString s;
 
--- a/data/data.pro	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/data.pro	Wed Feb 27 11:59:42 2008 +0000
@@ -38,6 +38,7 @@
            fileio/MP3FileReader.h \
            fileio/OggVorbisFileReader.h \
            fileio/PlaylistFileReader.h \
+           fileio/ProgressPrinter.h \
            fileio/QuickTimeFileReader.h \
            fileio/ResamplingWavFileReader.h \
            fileio/WavFileReader.h \
@@ -85,6 +86,7 @@
            fileio/MP3FileReader.cpp \
            fileio/OggVorbisFileReader.cpp \
            fileio/PlaylistFileReader.cpp \
+           fileio/ProgressPrinter.cpp \
            fileio/QuickTimeFileReader.cpp \
            fileio/ResamplingWavFileReader.cpp \
            fileio/WavFileReader.cpp \
--- a/data/fft/FFTDataServer.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fft/FFTDataServer.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -453,7 +453,8 @@
 #endif
 
             if (i->second.second > 0) {
-                std::cerr << "ERROR: FFTDataServer::modelAboutToBeDeleted: Model " << model << " (\"" << model->objectName().toStdString() << "\") is about to be deleted, but is still being referred to by FFT server " << server << " with non-zero refcount " << i->second.second << std::endl;
+                std::cerr << "WARNING: FFTDataServer::modelAboutToBeDeleted: Model " << model << " (\"" << model->objectName().toStdString() << "\") is about to be deleted, but is still being referred to by FFT server " << server << " with non-zero refcount " << i->second.second << std::endl;
+                return;
             }
             for (ServerQueue::iterator j = m_releasedServers.begin();
                  j != m_releasedServers.end(); ++j) {
@@ -726,9 +727,9 @@
     compactCache = canCompact &&
         (recommendation & StorageAdviser::ConserveSpace);
 
+#ifdef DEBUG_FFT_SERVER
     std::cerr << "FFTDataServer: memory cache = " << memoryCache << ", compact cache = " << compactCache << std::endl;
     
-#ifdef DEBUG_FFT_SERVER
     std::cerr << "Width " << w << " of " << m_width << ", height " << h << ", size " << w * h << std::endl;
 #endif
 }
--- a/data/fft/FFTFileCache.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fft/FFTFileCache.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -23,6 +23,7 @@
 
 #include <QMutexLocker>
 
+
 // The underlying matrix has height (m_height * 2 + 1).  In each
 // column we store magnitude at [0], [2] etc and phase at [1], [3]
 // etc, and then store the normalization factor (maximum magnitude) at
--- a/data/fft/FFTMemoryCache.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fft/FFTMemoryCache.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -18,6 +18,8 @@
 
 #include <iostream>
 
+//#define DEBUG_FFT_MEMORY_CACHE 1
+
 FFTMemoryCache::FFTMemoryCache(StorageType storageType) :
     m_width(0),
     m_height(0),
@@ -30,13 +32,17 @@
     m_factor(0),
     m_storageType(storageType)
 {
+#ifdef DEBUG_FFT_MEMORY_CACHE
     std::cerr << "FFTMemoryCache[" << this << "]::FFTMemoryCache (type "
               << m_storageType << ")" << std::endl;
+#endif
 }
 
 FFTMemoryCache::~FFTMemoryCache()
 {
-//    std::cerr << "FFTMemoryCache[" << this << "]::~FFTMemoryCache" << std::endl;
+#ifdef DEBUG_FFT_MEMORY_CACHE
+    std::cerr << "FFTMemoryCache[" << this << "]::~FFTMemoryCache" << std::endl;
+#endif
 
     for (size_t i = 0; i < m_width; ++i) {
 	if (m_magnitude && m_magnitude[i]) free(m_magnitude[i]);
@@ -59,7 +65,9 @@
 void
 FFTMemoryCache::resize(size_t width, size_t height)
 {
+#ifdef DEBUG_FFT_MEMORY_CACHE
     std::cerr << "FFTMemoryCache[" << this << "]::resize(" << width << "x" << height << " = " << width*height << ")" << std::endl;
+#endif
     
     if (m_width == width && m_height == height) return;
 
@@ -81,7 +89,9 @@
     m_width = width;
     m_height = height;
 
-//    std::cerr << "done, width = " << m_width << " height = " << m_height << std::endl;
+#ifdef DEBUG_FFT_MEMORY_CACHE
+    std::cerr << "done, width = " << m_width << " height = " << m_height << std::endl;
+#endif
 }
 
 void
--- a/data/fileio/AudioFileReader.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/AudioFileReader.h	Wed Feb 27 11:59:42 2008 +0000
@@ -39,6 +39,12 @@
     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 location of the audio data in the reader (as passed
+     * in to the FileSource constructor, for example).
+     */
+    virtual QString getLocation() const { return ""; }
     
     /**
      * Return the title of the work in the audio file, if known.  This
--- a/data/fileio/CodedAudioFileReader.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/CodedAudioFileReader.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -294,7 +294,7 @@
     float max = 1.0;
     size_t count = sz * m_channelCount;
 
-    if (m_resampler) {
+    if (m_resampler && m_fileRate != 0) {
         
         float ratio = float(m_sampleRate) / float(m_fileRate);
 
--- a/data/fileio/CodedAudioFileReader.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/CodedAudioFileReader.h	Wed Feb 27 11:59:42 2008 +0000
@@ -27,6 +27,8 @@
 
 class CodedAudioFileReader : public AudioFileReader
 {
+    Q_OBJECT
+
 public:
     virtual ~CodedAudioFileReader();
 
@@ -40,6 +42,9 @@
 
     virtual size_t getNativeRate() const { return m_fileRate; }
 
+signals:
+    void progress(int);
+
 protected:
     CodedAudioFileReader(CacheMode cacheMode, size_t targetRate);
 
--- a/data/fileio/FileFinder.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/FileFinder.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -378,15 +378,15 @@
 QString
 FileFinder::find(FileType type, QString location, QString lastKnownLocation)
 {
-    if (QFileInfo(location).exists()) return location;
-
-    if (FileSource::isRemote(location)) {
+    if (FileSource::canHandleScheme(location)) {
         if (FileSource(location).isAvailable()) {
             std::cerr << "FileFinder::find: ok, it's available... returning" << std::endl;
             return location;
         }
     }
 
+    if (QFileInfo(location).exists()) return location;
+
     QString foundAt = "";
 
     if ((foundAt = findRelative(location, lastKnownLocation)) != "") {
@@ -415,6 +415,9 @@
         fileName = QUrl(location).path().section('/', -1, -1,
                                                  QString::SectionSkipEmpty);
     } else {
+        if (QUrl(location).scheme() == "file") {
+            location = QUrl(location).toLocalFile();
+        }
         fileName = QFileInfo(location).fileName();
     }
 
@@ -423,6 +426,9 @@
         if (!FileSource(resolved).isAvailable()) resolved = "";
         std::cerr << "resolved: " << resolved.toStdString() << std::endl;
     } else {
+        if (QUrl(relativeTo).scheme() == "file") {
+            relativeTo = QUrl(relativeTo).toLocalFile();
+        }
         resolved = QFileInfo(relativeTo).dir().filePath(fileName);
         if (!QFileInfo(resolved).exists() ||
             !QFileInfo(resolved).isFile() ||
--- a/data/fileio/FileSource.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/FileSource.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -14,6 +14,8 @@
 */
 
 #include "FileSource.h"
+#include "ProgressPrinter.h"
+
 #include "base/TempDirectory.h"
 #include "base/Exceptions.h"
 
@@ -27,7 +29,7 @@
 
 #include <iostream>
 
-//#define DEBUG_FILE_SOURCE 1
+#define DEBUG_FILE_SOURCE 1
 
 int
 FileSource::m_count = 0;
@@ -44,7 +46,7 @@
 QMutex
 FileSource::m_mapMutex;
 
-FileSource::FileSource(QString fileOrUrl, bool showProgress) :
+FileSource::FileSource(QString fileOrUrl, ShowProgressType progressType) :
     m_url(fileOrUrl),
     m_ftp(0),
     m_http(0),
@@ -54,6 +56,8 @@
     m_remote(isRemote(fileOrUrl)),
     m_done(false),
     m_leaveLocalFile(false),
+    m_progressType(progressType),
+    m_progressPrinter(0),
     m_progressDialog(0),
     m_progressShowTimer(this),
     m_refCounted(false)
@@ -68,7 +72,7 @@
         return;
     }
 
-    init(showProgress);
+    init();
 
     if (isRemote() &&
         (fileOrUrl.contains('%') ||
@@ -99,7 +103,7 @@
             m_ok = false;
             m_done = false;
             m_lastStatus = 0;
-            init(showProgress);
+            init();
         }
     }
 
@@ -109,7 +113,7 @@
     }
 }
 
-FileSource::FileSource(QUrl url, bool showProgress) :
+FileSource::FileSource(QUrl url, ShowProgressType progressType) :
     m_url(url),
     m_ftp(0),
     m_http(0),
@@ -119,6 +123,8 @@
     m_remote(isRemote(url.toString())),
     m_done(false),
     m_leaveLocalFile(false),
+    m_progressType(progressType),
+    m_progressPrinter(0),
     m_progressDialog(0),
     m_progressShowTimer(this),
     m_refCounted(false)
@@ -133,7 +139,7 @@
         return;
     }
 
-    init(showProgress);
+    init();
 }
 
 FileSource::FileSource(const FileSource &rf) :
@@ -147,6 +153,8 @@
     m_remote(rf.m_remote),
     m_done(false),
     m_leaveLocalFile(false),
+    m_progressType(rf.m_progressType),
+    m_progressPrinter(0),
     m_progressDialog(0),
     m_progressShowTimer(0),
     m_refCounted(false)
@@ -197,16 +205,40 @@
 }
 
 void
-FileSource::init(bool showProgress)
+FileSource::init()
 {
     if (!isRemote()) {
+#ifdef DEBUG_FILE_SOURCE
+        std::cerr << "FileSource::init: Not a remote URL" << std::endl;
+#endif
+        bool literal = false;
         m_localFilename = m_url.toLocalFile();
+        if (m_localFilename == "") {
+            // QUrl may have mishandled the scheme (e.g. in a DOS path)
+            m_localFilename = m_url.toString();
+            literal = true;
+        }
+#ifdef DEBUG_FILE_SOURCE
+        std::cerr << "FileSource::init: URL translates to local filename \""
+                  << m_localFilename.toStdString() << "\"" << std::endl;
+#endif
         m_ok = true;
+        m_lastStatus = 200;
+
         if (!QFileInfo(m_localFilename).exists()) {
-            m_lastStatus = 404;
-        } else {
-            m_lastStatus = 200;
+            if (literal) {
+                m_lastStatus = 404;
+            } else {
+                // Again, QUrl may have been mistreating us --
+                // e.g. dropping a part that looks like query data
+                m_localFilename = m_url.toString();
+                literal = true;
+                if (!QFileInfo(m_localFilename).exists()) {
+                    m_lastStatus = 404;
+                }
+            }
         }
+
         m_done = true;
         return;
     }
@@ -238,6 +270,7 @@
 
     if (scheme == "http") {
         initHttp();
+        std::cerr << "FileSource: initHttp succeeded" << std::endl;
     } else if (scheme == "ftp") {
         initFtp();
     } else {
@@ -271,14 +304,28 @@
         m_refCountMap[m_url]++;
         m_refCounted = true;
 
-        if (showProgress) {
-            m_progressDialog = new QProgressDialog(tr("Downloading %1...").arg(m_url.toString()), tr("Cancel"), 0, 100);
+        switch (m_progressType) {
+
+        case ProgressNone: break;
+
+        case ProgressDialog:
+            m_progressDialog = new QProgressDialog
+                (tr("Downloading %1...").arg(m_url.toString()),
+                 tr("Cancel"), 0, 100);
             m_progressDialog->hide();
             connect(&m_progressShowTimer, SIGNAL(timeout()),
                     this, SLOT(showProgressDialog()));
-            connect(m_progressDialog, SIGNAL(canceled()), this, SLOT(cancelled()));
+            connect(m_progressDialog, SIGNAL(canceled()),
+                    this, SLOT(cancelled()));
             m_progressShowTimer.setSingleShot(true);
             m_progressShowTimer.start(2000);
+            break;
+
+        case ProgressToConsole:
+            m_progressPrinter = new ProgressPrinter(tr("Downloading..."));
+            connect(this, SIGNAL(progress(int)),
+                    m_progressPrinter, SLOT(progress(int)));
+            break;
         }
     }
 }
@@ -396,6 +443,8 @@
     }
     delete m_progressDialog;
     m_progressDialog = 0;
+    delete m_progressPrinter;
+    m_progressPrinter = 0;
     delete m_localFile; // does not actually delete the file
     m_localFile = 0;
 }
@@ -403,16 +452,19 @@
 bool
 FileSource::isRemote(QString fileOrUrl)
 {
+    // Note that a "scheme" with length 1 is probably a DOS drive letter
     QString scheme = QUrl(fileOrUrl).scheme().toLower();
-    return (scheme == "http" || scheme == "ftp");
+    if (scheme == "" || scheme == "file" || scheme.length() == 1) return false;
+    return true;
 }
 
 bool
 FileSource::canHandleScheme(QUrl url)
 {
+    // Note that a "scheme" with length 1 is probably a DOS drive letter
     QString scheme = url.scheme().toLower();
     return (scheme == "http" || scheme == "ftp" ||
-            scheme == "file" || scheme == "");
+            scheme == "file" || scheme == "" || scheme.length() == 1);
 }
 
 bool
@@ -442,6 +494,7 @@
 FileSource::waitForData()
 {
     while (m_ok && !m_done) {
+//        std::cerr << "FileSource::waitForData: calling QApplication::processEvents" << std::endl;
         QApplication::processEvents();
     }
 }
@@ -795,24 +848,3 @@
     return false;
 }
 
-FileSourceProgressPrinter::FileSourceProgressPrinter() :
-    m_lastProgress(0)
-{
-}
-
-FileSourceProgressPrinter::~FileSourceProgressPrinter()
-{
-    if (m_lastProgress > 0 && m_lastProgress != 100) {
-        std::cerr << "\r\n";
-    }
-}
-
-void
-FileSourceProgressPrinter::progress(int progress)
-{
-    if (progress == m_lastProgress) return;
-    if (progress == 100) std::cerr << "\r\n";
-    else std::cerr << "\r" << progress << "%";
-    m_lastProgress = progress;
-}
-
--- a/data/fileio/FileSource.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/FileSource.h	Wed Feb 27 11:59:42 2008 +0000
@@ -28,6 +28,7 @@
 class QFile;
 class QProgressDialog;
 class QHttpResponseHeader;
+class ProgressPrinter;
 
 /**
  * FileSource is a class used to refer to the contents of a file that
@@ -62,19 +63,36 @@
 
 public:
 
+    enum ShowProgressType {
+        ProgressNone,
+        ProgressDialog,
+        ProgressToConsole
+    };
+
     /**
      * Construct a FileSource using the given local file path or URL.
-     * The URL may be raw or encoded.  If showProgress is true, a
-     * progress dialog will be shown for any network transfers.
+     * The URL may be raw or encoded.
+     *
+     * If progressType is ProgressDialog, a progress dialog will be
+     * shown for any network transfers; if it is ProgressToConsole, a
+     * progress indication will be sent to the console.
+     * Note that the progress() signal will also be emitted regularly
+     * during retrieval, even if progressType is ProgressNone.
      */
-    FileSource(QString fileOrUrl, bool showProgress = false);
+    FileSource(QString fileOrUrl,
+               ShowProgressType progressType = ProgressNone);
 
     /**
-     * Construct a FileSource using the given remote URL.  If
-     * showProgress is true, a progress dialog will be shown for any
-     * network transfers.
+     * Construct a FileSource using the given remote URL.
+     *
+     * If progressType is ProgressDialog, a progress dialog will be
+     * shown for any network transfers; if it is ProgressToConsole, a
+     * progress indication will be sent to the console.
+     * Note that the progress() signal also will be emitted regularly
+     * during retrieval, even if progressType is ProgressNone.
      */
-    FileSource(QUrl url, bool showProgress = false);
+    FileSource(QUrl url,
+               ShowProgressType progressType = ProgressNone);
 
     FileSource(const FileSource &);
 
@@ -212,6 +230,8 @@
     bool m_remote;
     bool m_done;
     bool m_leaveLocalFile;
+    ShowProgressType m_progressType;
+    ProgressPrinter *m_progressPrinter;
     QProgressDialog *m_progressDialog;
     QTimer m_progressShowTimer;
 
@@ -222,7 +242,7 @@
     static QMutex m_mapMutex;
     bool m_refCounted;
 
-    void init(bool showProgress);
+    void init();
     void initHttp();
     void initFtp();
 
@@ -237,19 +257,4 @@
     static int m_count;
 };
 
-class FileSourceProgressPrinter : public QObject
-{
-    Q_OBJECT
-
-public:
-    FileSourceProgressPrinter();
-    virtual ~FileSourceProgressPrinter();
-    
-public slots:
-    void progress(int);
-
-protected:
-    int m_lastProgress;
-};
-
 #endif
--- a/data/fileio/MIDIFileReader.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/MIDIFileReader.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -1021,8 +1021,10 @@
 		    QString noteLabel = tr("%1 - vel %2")
 			.arg(pitchLabel).arg(int((*i)->getVelocity()));
 
+                    float level = float((*i)->getVelocity()) / 128.f;
+
 		    Note note(startFrame, (*i)->getPitch(),
-			      endFrame - startFrame, noteLabel);
+			      endFrame - startFrame, level, noteLabel);
 
 //		    std::cerr << "Adding note " << startFrame << "," << (endFrame-startFrame) << " : " << int((*i)->getPitch()) << std::endl;
 
--- a/data/fileio/MIDIFileWriter.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/MIDIFileWriter.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -369,8 +369,10 @@
         double quarters = (seconds * m_tempo) / 60.0;
         unsigned long midiTime = lrint(quarters * m_timingDivision);
 
-        // We don't support velocity in note models yet
         int velocity = 100;
+        if (i->level > 0.f && i->level <= 1.f) {
+            velocity = lrintf(i->level * 127.f);
+        }
 
         // Get the sounding time for the matching NOTE_OFF
         seconds = double(frame + duration) / double(m_model->getSampleRate());
--- a/data/fileio/MP3FileReader.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/MP3FileReader.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -17,6 +17,8 @@
 #ifdef HAVE_MAD
 
 #include "MP3FileReader.h"
+#include "ProgressPrinter.h"
+
 #include "system/System.h"
 
 #include <sys/types.h>
@@ -111,6 +113,9 @@
                 (QObject::tr("Decoding %1...").arg(QFileInfo(m_path).fileName()),
                  QObject::tr("Stop"), 0, 100);
             m_progress->hide();
+        } else {
+            ProgressPrinter *pp = new ProgressPrinter(tr("Decoding..."), this);
+            connect(this, SIGNAL(progress(int)), pp, SLOT(progress(int)));
         }
 
         if (!decode(m_filebuffer, m_fileSize)) {
@@ -130,7 +135,7 @@
         m_decodeThread = new DecodeThread(this);
         m_decodeThread->start();
 
-        while (m_channelCount == 0 && !m_done) {
+        while ((m_channelCount == 0 || m_fileRate == 0) && !m_done) {
             usleep(10);
         }
     }
@@ -206,7 +211,7 @@
     unsigned int nstrings = id3_field_getnstrings(&frame->fields[1]);
     if (nstrings == 0) {
 #ifdef DEBUG_ID3TAG
-        std::cerr << "MP3FileReader::loadTags: No data for \"" << name << "\" in ID3 tag" << std::endl;
+        std::cerr << "MP3FileReader::loadTags: No strings for \"" << name << "\" in ID3 tag" << std::endl;
 #endif
         return "";
     }
@@ -291,7 +296,22 @@
     DecoderData *data = (DecoderData *)dp;
 
     if (!data->length) return MAD_FLOW_STOP;
-    mad_stream_buffer(stream, data->start, data->length);
+
+    unsigned char const *start = data->start;
+    unsigned long length = data->length;
+
+#ifdef HAVE_ID3TAG
+    if (length > ID3_TAG_QUERYSIZE) {
+        int taglen = id3_tag_query(start, ID3_TAG_QUERYSIZE);
+        if (taglen > 0) {
+//            std::cerr << "ID3 tag length to skip: " << taglen << std::endl;
+            start += taglen;
+            length -= taglen;
+        }
+    }
+#endif
+
+    mad_stream_buffer(stream, start, length);
     data->length = 0;
 
     return MAD_FLOW_CONTINUE;
@@ -328,7 +348,7 @@
         initialiseDecodeCache();
 
         if (m_cacheMode == CacheInTemporaryFile) {
-            m_completion = 1;
+//            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");
         }
@@ -338,19 +358,23 @@
         double bitrate = m_bitrateNum / m_bitrateDenom;
         double duration = double(m_fileSize * 8) / bitrate;
         double elapsed = double(m_frameCount) / m_sampleRate;
-        double percent = ((elapsed * 100.0) / duration);
-        int progress = int(percent);
-        if (progress < 1) progress = 1;
-        if (progress > 99) progress = 99;
-        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;
+        double percent = 100;
+        if (duration > 0.0) percent = ((elapsed * 100.0) / duration);
+        int p = int(percent);
+        if (p < 1) p = 1;
+        if (p > 99) p = 99;
+        if (m_completion != p || (m_progress && !m_progress->isVisible())) {
+            m_completion = p;
+            emit progress(m_completion);
+            if (m_progress) {
+                if (m_completion > m_progress->value()) {
+                    m_progress->setValue(m_completion);
+                    m_progress->show();
+                    m_progress->raise();
+                    qApp->processEvents();
+                    if (m_progress->wasCanceled()) {
+                        m_cancelled = true;
+                    }
                 }
             }
         }
--- a/data/fileio/MP3FileReader.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/MP3FileReader.h	Wed Feb 27 11:59:42 2008 +0000
@@ -43,6 +43,7 @@
 
     virtual QString getError() const { return m_error; }
 
+    virtual QString getLocation() const { return m_source.getLocation(); }
     virtual QString getTitle() const { return m_title; }
     virtual QString getMaker() const { return m_maker; }
     
--- a/data/fileio/OggVorbisFileReader.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/OggVorbisFileReader.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -17,6 +17,8 @@
 #ifdef HAVE_FISHSOUND
 
 #include "OggVorbisFileReader.h"
+#include "ProgressPrinter.h"
+
 #include "base/Profiler.h"
 #include "system/System.h"
 
@@ -75,6 +77,9 @@
                 (QObject::tr("Decoding %1...").arg(QFileInfo(m_path).fileName()),
                  QObject::tr("Stop"), 0, 100);
             m_progress->hide();
+        } else {
+            ProgressPrinter *pp = new ProgressPrinter(tr("Decoding..."), this);
+            connect(this, SIGNAL(progress(int)), pp, SLOT(progress(int)));
         }
 
         while (oggz_read(m_oggz, 1024) > 0);
@@ -145,14 +150,15 @@
 
     // The number of bytes read by this function is smaller than
     // the file size because of the packet headers
-    int progress = lrint(double(reader->m_bytesRead) * 114 /
-                         double(reader->m_fileSize));
-    if (progress > 99) progress = 99;
-    reader->m_completion = progress;
-    
+    int p = lrint(double(reader->m_bytesRead) * 114 /
+                  double(reader->m_fileSize));
+    if (p > 99) p = 99;
+    reader->m_completion = p;
+    reader->progress(p);
+
     if (reader->m_fileSize > 0 && reader->m_progress) {
-	if (progress > reader->m_progress->value()) {
-	    reader->m_progress->setValue(progress);
+	if (p > reader->m_progress->value()) {
+	    reader->m_progress->setValue(p);
 	    reader->m_progress->show();
 	    reader->m_progress->raise();
 	    qApp->processEvents();
--- a/data/fileio/OggVorbisFileReader.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/OggVorbisFileReader.h	Wed Feb 27 11:59:42 2008 +0000
@@ -45,6 +45,7 @@
 
     virtual QString getError() const { return m_error; }
 
+    virtual QString getLocation() const { return m_source.getLocation(); }
     virtual QString getTitle() const { return m_title; }
     virtual QString getMaker() const { return m_maker; }
     
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/ProgressPrinter.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -0,0 +1,48 @@
+/* -*- 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 "ProgressPrinter.h"
+
+#include <iostream>
+
+ProgressPrinter::ProgressPrinter(QString prefix, QObject *parent) :
+    QObject(parent),
+    m_prefix(prefix),
+    m_lastProgress(0)
+{
+}
+
+ProgressPrinter::~ProgressPrinter()
+{
+    if (m_lastProgress > 0 && m_lastProgress != 100) {
+        std::cerr << "\r\n";
+    }
+    std::cerr << "(progress printer dtor)" << std::endl;
+}
+
+void
+ProgressPrinter::progress(int progress)
+{
+    if (progress == m_lastProgress) return;
+    if (progress == 100) std::cerr << "\r\n";
+    else {
+        std::cerr << "\r"
+                  << m_prefix.toStdString() 
+                  << (m_prefix == "" ? "" : " ")
+                  << progress << "%";
+    }
+    m_lastProgress = progress;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/ProgressPrinter.h	Wed Feb 27 11:59:42 2008 +0000
@@ -0,0 +1,38 @@
+/* -*- 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 _PROGRESS_PRINTER_H_
+#define _PROGRESS_PRINTER_H_
+
+#include <QObject>
+#include <QString>
+
+class ProgressPrinter : public QObject
+{
+    Q_OBJECT
+
+public:
+    ProgressPrinter(QString prefix = "", QObject *parent = 0);
+    virtual ~ProgressPrinter();
+    
+public slots:
+    void progress(int);
+
+protected:
+    QString m_prefix;
+    int m_lastProgress;
+};
+
+#endif
--- a/data/fileio/QuickTimeFileReader.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/QuickTimeFileReader.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -236,6 +236,8 @@
                 break;
             }
 
+            //!!! progress?
+
 //    std::cerr << "Read " << framesRead << " frames (block size " << m_d->blockSize << ")" << std::endl;
 
             // QuickTime buffers are interleaved unless specified otherwise
--- a/data/fileio/QuickTimeFileReader.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/QuickTimeFileReader.h	Wed Feb 27 11:59:42 2008 +0000
@@ -44,6 +44,7 @@
     virtual ~QuickTimeFileReader();
 
     virtual QString getError() const { return m_error; }
+    virtual QString getLocation() const { return m_source.getLocation(); }
     virtual QString getTitle() const { return m_title; }
     
     static void getSupportedExtensions(std::set<QString> &extensions);
--- a/data/fileio/ResamplingWavFileReader.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/ResamplingWavFileReader.h	Wed Feb 27 11:59:42 2008 +0000
@@ -40,6 +40,7 @@
     virtual ~ResamplingWavFileReader();
 
     virtual QString getError() const { return m_error; }
+    virtual QString getLocation() const { return m_source.getLocation(); }
     static void getSupportedExtensions(std::set<QString> &extensions);
     static bool supportsExtension(QString ext);
     static bool supportsContentType(QString type);
--- a/data/fileio/WavFileReader.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/WavFileReader.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -111,6 +111,7 @@
 {
     if (count == 0) return;
     results.clear();
+    results.reserve(count * m_fileInfo.channels);
 
     QMutexLocker locker(&m_mutex);
 
--- a/data/fileio/WavFileReader.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/fileio/WavFileReader.h	Wed Feb 27 11:59:42 2008 +0000
@@ -29,6 +29,7 @@
     WavFileReader(FileSource source, bool fileUpdating = false);
     virtual ~WavFileReader();
 
+    virtual QString getLocation() const { return m_source.getLocation(); }
     virtual QString getError() const { return m_error; }
 
     /** 
--- a/data/model/AggregateWaveModel.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/AggregateWaveModel.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -175,6 +175,28 @@
     if (mixing) delete[] readbuf;
     return sz;
 }
+
+size_t
+AggregateWaveModel::getData(size_t fromchannel, size_t tochannel,
+                            size_t start, size_t count,
+                            float **buffer) const
+{
+    size_t min = count;
+
+    for (size_t c = fromchannel; c <= tochannel; ++c) {
+        size_t here = getData(c, start, count, buffer[c - fromchannel]);
+        if (here < min) min = here;
+    }
+    
+    return min;
+}
+
+size_t
+AggregateWaveModel::getSummaryBlockSize(size_t desired) const
+{
+    //!!! complete
+    return desired;
+}
         
 void
 AggregateWaveModel::getSummaries(size_t channel, size_t start, size_t count,
--- a/data/model/AggregateWaveModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/AggregateWaveModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -42,6 +42,8 @@
     bool isOK() const;
     bool isReady(int *) const;
 
+    QString getTypeName() const { return tr("Aggregate Wave"); }
+
     size_t getComponentCount() const;
     ModelChannelSpec getComponent(size_t c) const;
 
@@ -65,6 +67,12 @@
     virtual size_t getData(int channel, size_t start, size_t count,
                            double *buffer) const;
 
+    virtual size_t getData(size_t fromchannel, size_t tochannel,
+                           size_t start, size_t count,
+                           float **buffer) const;
+
+    virtual size_t getSummaryBlockSize(size_t desired) const;
+
     virtual void getSummaries(size_t channel, size_t start, size_t count,
                               RangeBlock &ranges,
                               size_t &blockSize) const;
--- a/data/model/AlignmentModel.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/AlignmentModel.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -17,6 +17,8 @@
 
 #include "SparseTimeValueModel.h"
 
+//#define DEBUG_ALIGNMENT_MODEL 1
+
 AlignmentModel::AlignmentModel(Model *reference,
                                Model *aligned,
                                Model *inputModel,
@@ -24,26 +26,32 @@
     m_reference(reference),
     m_aligned(aligned),
     m_inputModel(inputModel),
-    m_path(path),
+    m_rawPath(path),
+    m_path(0),
     m_reversePath(0),
     m_pathBegun(false),
     m_pathComplete(false)
 {
-    connect(m_path, SIGNAL(modelChanged()),
-            this, SLOT(pathChanged()));
+    if (m_rawPath) {
 
-    connect(m_path, SIGNAL(modelChanged(size_t, size_t)),
-            this, SLOT(pathChanged(size_t, size_t)));
+        connect(m_rawPath, SIGNAL(modelChanged()),
+                this, SLOT(pathChanged()));
 
-    connect(m_path, SIGNAL(completionChanged()),
-            this, SLOT(pathCompletionChanged()));
+        connect(m_rawPath, SIGNAL(modelChanged(size_t, size_t)),
+                this, SLOT(pathChanged(size_t, size_t)));
+        
+        connect(m_rawPath, SIGNAL(completionChanged()),
+                this, SLOT(pathCompletionChanged()));
+    }
 
+    constructPath();
     constructReversePath();
 }
 
 AlignmentModel::~AlignmentModel()
 {
     delete m_inputModel;
+    delete m_rawPath;
     delete m_path;
     delete m_reversePath;
 }
@@ -51,13 +59,13 @@
 bool
 AlignmentModel::isOK() const
 {
-    return m_path->isOK();
+    if (m_rawPath) return m_rawPath->isOK();
+    else return true;
 }
 
 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);
@@ -66,7 +74,6 @@
 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);
@@ -84,23 +91,27 @@
     return new AlignmentModel
         (m_reference, m_aligned,
          m_inputModel ? m_inputModel->clone() : 0,
-         m_path ? static_cast<SparseTimeValueModel *>(m_path->clone()) : 0);
+         m_rawPath ? static_cast<SparseTimeValueModel *>(m_rawPath->clone()) : 0);
 }
 
 bool
 AlignmentModel::isReady(int *completion) const
 {
     if (!m_pathBegun) {
-        completion = 0;
+        if (completion) *completion = 0;
         return false;
     }
-    return m_path->isReady(completion);
+    if (m_pathComplete || !m_rawPath) {
+        if (completion) *completion = 100;
+        return true;
+    }
+    return m_rawPath->isReady(completion);
 }
 
 const ZoomConstraint *
 AlignmentModel::getZoomConstraint() const
 {
-    return m_path->getZoomConstraint();
+    return 0;
 }
 
 const Model *
@@ -118,43 +129,70 @@
 size_t
 AlignmentModel::toReference(size_t frame) const
 {
-//    std::cerr << "AlignmentModel::toReference(" << frame << ")" << std::endl;
-    if (!m_reversePath) constructReversePath();
-    return align(m_reversePath, frame);
+#ifdef DEBUG_ALIGNMENT_MODEL
+    std::cerr << "AlignmentModel::toReference(" << frame << ")" << std::endl;
+#endif
+    if (!m_path) {
+        if (!m_rawPath) return frame;
+        constructPath();
+    }
+    return align(m_path, frame);
 }
 
 size_t
 AlignmentModel::fromReference(size_t frame) const
 {
-//    std::cerr << "AlignmentModel::fromReference(" << frame << ")" << std::endl;
-    return align(m_path, frame);
+#ifdef DEBUG_ALIGNMENT_MODEL
+    std::cerr << "AlignmentModel::fromReference(" << frame << ")" << std::endl;
+#endif
+    if (!m_reversePath) {
+        if (!m_rawPath) return frame;
+        constructReversePath();
+    }
+    return align(m_reversePath, frame);
 }
 
 void
 AlignmentModel::pathChanged()
 {
+    if (m_pathComplete) {
+        std::cerr << "AlignmentModel: deleting raw path model" << std::endl;
+        delete m_rawPath;
+        m_rawPath = 0;
+    }
 }
 
 void
 AlignmentModel::pathChanged(size_t, size_t)
 {
     if (!m_pathComplete) return;
+    constructPath();
     constructReversePath();
 }    
 
 void
 AlignmentModel::pathCompletionChanged()
 {
+    if (!m_rawPath) return;
     m_pathBegun = true;
 
     if (!m_pathComplete) {
+
         int completion = 0;
-        m_path->isReady(&completion);
-//        std::cerr << "AlignmentModel::pathCompletionChanged: completion = "
-//                  << completion << std::endl;
+        m_rawPath->isReady(&completion);
+
+#ifdef DEBUG_ALIGNMENT_MODEL
+        std::cerr << "AlignmentModel::pathCompletionChanged: completion = "
+                  << completion << std::endl;
+#endif
+
         m_pathComplete = (completion == 100);
+
         if (m_pathComplete) {
+
+            constructPath();
             constructReversePath();
+
             delete m_inputModel;
             m_inputModel = 0;
         }
@@ -164,78 +202,134 @@
 }
 
 void
-AlignmentModel::constructReversePath() const
+AlignmentModel::constructPath() const
 {
-    if (!m_reversePath) {
-        m_reversePath = new SparseTimeValueModel
-            (m_path->getSampleRate(), m_path->getResolution(), false);
+    if (!m_path) {
+        if (!m_rawPath) {
+            std::cerr << "ERROR: AlignmentModel::constructPath: "
+                      << "No raw path available" << std::endl;
+            return;
+        }
+        m_path = new PathModel
+            (m_rawPath->getSampleRate(), m_rawPath->getResolution(), false);
+    } else {
+        if (!m_rawPath) return;
     }
         
-    m_reversePath->clear();
+    m_path->clear();
 
-    SparseTimeValueModel::PointList points = m_path->getPoints();
+    SparseTimeValueModel::PointList points = m_rawPath->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, ""));
+        m_path->addPoint(PathPoint(frame, rframe));
     }
 
-    std::cerr << "AlignmentModel::constructReversePath: " << m_reversePath->getPointCount() << " points, at least " << (2 * m_reversePath->getPointCount() * (3 * sizeof(void *) + sizeof(int) + sizeof(SparseTimeValueModel::Point))) << " bytes" << std::endl;
+#ifdef DEBUG_ALIGNMENT_MODEL
+    std::cerr << "AlignmentModel::constructPath: " << m_path->getPointCount() << " points, at least " << (2 * m_path->getPointCount() * (3 * sizeof(void *) + sizeof(int) + sizeof(PathPoint))) << " bytes" << std::endl;
+#endif
+}
+
+void
+AlignmentModel::constructReversePath() const
+{
+    if (!m_reversePath) {
+        if (!m_rawPath) {
+            std::cerr << "ERROR: AlignmentModel::constructReversePath: "
+                      << "No raw path available" << std::endl;
+            return;
+        }
+        m_reversePath = new PathModel
+            (m_rawPath->getSampleRate(), m_rawPath->getResolution(), false);
+    } else {
+        if (!m_rawPath) return;
+    }
+        
+    m_reversePath->clear();
+
+    SparseTimeValueModel::PointList points = m_rawPath->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());
+        m_reversePath->addPoint(PathPoint(rframe, frame));
+    }
+
+#ifdef DEBUG_ALIGNMENT_MODEL
+    std::cerr << "AlignmentModel::constructReversePath: " << m_reversePath->getPointCount() << " points, at least " << (2 * m_reversePath->getPointCount() * (3 * sizeof(void *) + sizeof(int) + sizeof(PathPoint))) << " bytes" << std::endl;
+#endif
 }
 
 size_t
-AlignmentModel::align(SparseTimeValueModel *path, size_t frame) const
+AlignmentModel::align(PathModel *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.
+    if (!path) return frame;
 
-    const SparseTimeValueModel::PointList &points = path->getPoints();
+    // The path consists of a series of points, each with frame equal
+    // to the frame on the source model and mapframe equal to the
+    // frame on the target model.  Both should be monotonically
+    // increasing.
+
+    const PathModel::PointList &points = path->getPoints();
 
     if (points.empty()) {
-//        std::cerr << "AlignmentModel::align: No points" << std::endl;
+#ifdef DEBUG_ALIGNMENT_MODEL
+        std::cerr << "AlignmentModel::align: No points" << std::endl;
+#endif
         return frame;
     }        
 
-    SparseTimeValueModel::Point point(frame);
-    SparseTimeValueModel::PointList::const_iterator i = points.lower_bound(point);
-    if (i == points.end()) --i;
-    while (i != points.begin() && i->frame > frame) --i;
+#ifdef DEBUG_ALIGNMENT_MODEL
+    std::cerr << "AlignmentModel::align: frame " << frame << " requested" << std::endl;
+#endif
+
+    PathModel::Point point(frame);
+    PathModel::PointList::const_iterator i = points.lower_bound(point);
+    if (i == points.end()) {
+#ifdef DEBUG_ALIGNMENT_MODEL
+        std::cerr << "Note: i == points.end()" << std::endl;
+#endif
+        --i;
+    }
+    while (i != points.begin() && i->frame > long(frame)) --i;
 
     long foundFrame = i->frame;
-    float foundTime = i->value;
+    long foundMapFrame = i->mapframe;
 
     long followingFrame = foundFrame;
-    float followingTime = foundTime;
+    long followingMapFrame = foundMapFrame;
 
     if (++i != points.end()) {
+#ifdef DEBUG_ALIGNMENT_MODEL
+        std::cerr << "another point available" << std::endl;
+#endif
         followingFrame = i->frame;
-        followingTime = i->value;
+        followingMapFrame = i->mapframe;
+    } else {
+#ifdef DEBUG_ALIGNMENT_MODEL
+        std::cerr << "no other point available" << std::endl;
+#endif
+    }        
+
+    if (foundMapFrame < 0) return 0;
+
+    size_t resultFrame = foundMapFrame;
+
+    if (followingFrame != foundFrame && long(frame) > foundFrame) {
+        float interp =
+            float(frame - foundFrame) /
+            float(followingFrame - foundFrame);
+        resultFrame += lrintf((followingMapFrame - foundMapFrame) * interp);
     }
 
-    float resultTime = foundTime;
-
-    if (followingFrame != foundFrame && frame > foundFrame) {
-
-//        std::cerr << "AlignmentModel::align: foundFrame = " << foundFrame << ", frame = " << frame << ", followingFrame = " << followingFrame << std::endl;
-
-        float interp = float(frame - foundFrame) / float(followingFrame - foundFrame);
-//        std::cerr << "AlignmentModel::align: interp = " << interp << ", result " << resultTime << " -> ";
-
-        resultTime += (followingTime - foundTime) * interp;
-
-//        std::cerr << resultTime << std::endl;
-    }
-
-    size_t resultFrame = lrintf(resultTime * getSampleRate());
-
-//    std::cerr << "AlignmentModel::align: resultFrame = " << resultFrame << std::endl;
+#ifdef DEBUG_ALIGNMENT_MODEL
+    std::cerr << "AlignmentModel::align: resultFrame = " << resultFrame << std::endl;
+#endif
 
     return resultFrame;
 }
--- a/data/model/AlignmentModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/AlignmentModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -17,6 +17,11 @@
 #define _ALIGNMENT_MODEL_H_
 
 #include "Model.h"
+#include "SparseModel.h"
+#include "base/RealTime.h"
+
+#include <QString>
+#include <QStringList>
 
 class SparseTimeValueModel;
 
@@ -39,6 +44,8 @@
     virtual bool isReady(int *completion = 0) const;
     virtual const ZoomConstraint *getZoomConstraint() const;
 
+    QString getTypeName() const { return tr("Alignment"); }
+
     const Model *getReferenceModel() const;
     const Model *getAlignedModel() const;
 
@@ -60,14 +67,65 @@
     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
+
+    struct PathPoint
+    {
+        PathPoint(long _frame) : frame(_frame), mapframe(_frame) { }
+        PathPoint(long _frame, long _mapframe) :
+            frame(_frame), mapframe(_mapframe) { }
+
+        int getDimensions() const { return 2; }
+
+        long frame;
+        long mapframe;
+
+        QString getLabel() const { return ""; }
+
+        void toXml(QTextStream &stream, QString indent = "",
+                   QString extraAttributes = "") const {
+            stream << QString("%1<point frame=\"%2\" mapframe=\"%3\" %4/>\n")
+                .arg(indent).arg(frame).arg(mapframe).arg(extraAttributes);
+        }
+        
+        QString toDelimitedDataString(QString delimiter,
+                                      size_t sampleRate) const {
+            QStringList list;
+            list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str();
+            list << QString("%1").arg(mapframe);
+            return list.join(delimiter);
+        }
+
+        struct Comparator {
+            bool operator()(const PathPoint &p1, const PathPoint &p2) const {
+                if (p1.frame != p2.frame) return p1.frame < p2.frame;
+                return p1.mapframe < p2.mapframe;
+            }
+        };
+    
+        struct OrderComparator {
+            bool operator()(const PathPoint &p1, const PathPoint &p2) const {
+                return p1.frame < p2.frame;
+            }
+        };
+    };
+
+    class PathModel : public SparseModel<PathPoint>
+    {
+    public:
+        PathModel(size_t sampleRate, size_t resolution, bool notify = true) :
+            SparseModel<PathPoint>(sampleRate, resolution, notify) { }
+    };
+
+    SparseTimeValueModel *m_rawPath; // I own this
+    mutable PathModel *m_path; // I own this
+    mutable PathModel *m_reversePath; // I own this
     bool m_pathBegun;
     bool m_pathComplete;
 
+    void constructPath() const;
     void constructReversePath() const;
 
-    size_t align(SparseTimeValueModel *path, size_t frame) const;
+    size_t align(PathModel *path, size_t frame) const;
 };
 
 #endif
--- a/data/model/DenseThreeDimensionalModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/DenseThreeDimensionalModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -98,6 +98,8 @@
         return getValueAt(x, y) > threshold;
     }
 
+    QString getTypeName() const { return tr("Dense 3-D"); }
+
     virtual int getCompletion() const = 0;
 
 protected:
--- a/data/model/DenseTimeValueModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/DenseTimeValueModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -70,6 +70,17 @@
      */
     virtual size_t getData(int channel, size_t start, size_t count,
                            double *buffer) const = 0;
+
+    /**
+     * Get the specified set of samples from given contiguous range
+     * of channels of the model in single-precision floating-point
+     * format.  Return the number of sample frames actually retrieved.
+     */
+    virtual size_t getData(size_t fromchannel, size_t tochannel,
+                           size_t start, size_t count,
+                           float **buffers) const = 0;
+
+    QString getTypeName() const { return tr("Dense Time-Value"); }
 };
 
 #endif
--- a/data/model/EditableDenseThreeDimensionalModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/EditableDenseThreeDimensionalModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -109,6 +109,8 @@
     virtual void setCompletion(int completion, bool update = true);
     virtual int getCompletion() const { return m_completion; }
 
+    QString getTypeName() const { return tr("Editable Dense 3-D"); }
+
     virtual QString toDelimitedDataString(QString delimiter) const;
 
     virtual void toXml(QTextStream &out,
--- a/data/model/FFTModel.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/FFTModel.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -82,6 +82,19 @@
     if (m_server) FFTDataServer::releaseInstance(m_server);
 }
 
+void
+FFTModel::sourceModelAboutToBeDeleted()
+{
+    if (m_sourceModel) {
+        std::cerr << "FFTModel[" << this << "]::sourceModelAboutToBeDeleted(" << m_sourceModel << ")" << std::endl;
+        if (m_server) {
+            FFTDataServer::releaseInstance(m_server);
+            m_server = 0;
+        }
+        FFTDataServer::modelAboutToBeDeleted(m_sourceModel);
+    }
+}
+
 FFTDataServer *
 FFTModel::getServer(const DenseTimeValueModel *model,
                     int channel,
--- a/data/model/FFTModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/FFTModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -165,6 +165,11 @@
     virtual void suspendWrites() { m_server->suspendWrites(); }
     virtual void resume() { m_server->resume(); }
 
+    QString getTypeName() const { return tr("FFT"); }
+
+public slots:
+    void sourceModelAboutToBeDeleted();
+
 private:
     FFTModel(const FFTModel &); // not implemented
     FFTModel &operator=(const FFTModel &); // not implemented
--- a/data/model/ImageModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/ImageModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -38,6 +38,8 @@
     long frame;
     QString image;
     QString label;
+
+    QString getLabel() const { return label; }
     
     void toXml(QTextStream &stream,
                QString indent = "",
@@ -87,6 +89,8 @@
 	SparseModel<ImagePoint>(sampleRate, resolution, notifyOnAdd)
     { }
 
+    QString getTypeName() const { return tr("Image"); }
+
     virtual void toXml(QTextStream &out,
                        QString indent = "",
                        QString extraAttributes = "") const
--- a/data/model/Labeller.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/Labeller.h	Wed Feb 27 11:59:42 2008 +0000
@@ -36,8 +36,10 @@
         ValueFromTwoLevelCounter,
         ValueFromFrameNumber,
         ValueFromRealTime,
-        ValueFromRealTimeDifference,
-        ValueFromTempo,
+        ValueFromDurationFromPrevious,
+        ValueFromDurationToNext,
+        ValueFromTempoFromPrevious,
+        ValueFromTempoToNext,
         ValueFromExistingNeighbour,
         ValueFromLabel
     };
@@ -59,7 +61,7 @@
     // 4. re-label a set of points that have already been added to a
     // model
 
-    Labeller(ValueType type) :
+    Labeller(ValueType type = ValueNone) :
         m_type(type),
         m_counter(1),
         m_counter2(1),
@@ -70,8 +72,8 @@
     Labeller(const Labeller &l) :
         QObject(),
         m_type(l.m_type),
-        m_counter(1),
-        m_counter2(1),
+        m_counter(l.m_counter),
+        m_counter2(l.m_counter2),
         m_cycle(l.m_cycle),
         m_dp(l.m_dp),
         m_rate(l.m_rate) { }
@@ -81,16 +83,30 @@
     typedef std::map<ValueType, QString> TypeNameMap;
     TypeNameMap getTypeNames() const {
         TypeNameMap m;
-        m[ValueNone] = tr("No numbering");
-        m[ValueFromSimpleCounter] = tr("Simple counter");
-        m[ValueFromCyclicalCounter] = tr("Cyclical counter");
-        m[ValueFromTwoLevelCounter] = tr("Cyclical two-level counter (bar/beat)");
-        m[ValueFromFrameNumber] = tr("Audio sample frame number");
-        m[ValueFromRealTime] = tr("Time in seconds");
-        m[ValueFromRealTimeDifference] = tr("Duration to the following item");
-        m[ValueFromTempo] = tr("Tempo (bpm) based on duration to following item");
-        m[ValueFromExistingNeighbour] = tr("Same as the nearest previous item");
-        m[ValueFromLabel] = tr("Value extracted from the item's label (where possible)");
+        m[ValueNone]
+            = tr("No numbering");
+        m[ValueFromSimpleCounter]
+            = tr("Simple counter");
+        m[ValueFromCyclicalCounter]
+            = tr("Cyclical counter");
+        m[ValueFromTwoLevelCounter]
+            = tr("Cyclical two-level counter (bar/beat)");
+        m[ValueFromFrameNumber]
+            = tr("Audio sample frame number");
+        m[ValueFromRealTime]
+            = tr("Time in seconds");
+        m[ValueFromDurationToNext]
+            = tr("Duration to the following item");
+        m[ValueFromTempoToNext]
+            = tr("Tempo (bpm) based on duration to following item");
+        m[ValueFromDurationFromPrevious]
+            = tr("Duration since the previous item");
+        m[ValueFromTempoFromPrevious]
+            = tr("Tempo (bpm) based on duration since previous item");
+        m[ValueFromExistingNeighbour]
+            = tr("Same as the nearest previous item");
+        m[ValueFromLabel]
+            = tr("Value extracted from the item's label (where possible)");
         return m;
     }
 
@@ -212,9 +228,16 @@
         }
     }
 
+    bool requiresPrevPoint() const {
+        return (m_type == ValueFromDurationFromPrevious ||
+                m_type == ValueFromDurationToNext ||
+                m_type == ValueFromTempoFromPrevious ||
+                m_type == ValueFromDurationToNext);
+    }
+
     bool actingOnPrevPoint() const {
-        return (m_type == ValueFromRealTimeDifference ||
-                m_type == ValueFromTempo);
+        return (m_type == ValueFromDurationToNext ||
+                m_type == ValueFromTempoToNext);
     }
 
 protected:
@@ -252,15 +275,18 @@
             }
             break;
 
-        case ValueFromRealTimeDifference:
-        case ValueFromTempo:
+        case ValueFromDurationToNext:
+        case ValueFromTempoToNext:
+        case ValueFromDurationFromPrevious:
+        case ValueFromTempoFromPrevious:
             if (m_rate == 0.f) {
                 std::cerr << "ERROR: Labeller::getValueFor: Real-time conversion required, but no sample rate set" << std::endl;
             } else if (!prevPoint) {
                 std::cerr << "ERROR: Labeller::getValueFor: Time difference required, but only one point provided" << std::endl;
             } else {
                 size_t f0 = prevPoint->frame, f1 = newPoint.frame;
-                if (m_type == ValueFromRealTimeDifference) {
+                if (m_type == ValueFromDurationToNext ||
+                    m_type == ValueFromDurationFromPrevious) {
                     value = float(f1 - f0) / m_rate;
                 } else {
                     if (f1 > f0) {
--- a/data/model/Model.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/Model.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -25,7 +25,7 @@
 
 Model::~Model()
 {
-//    std::cerr << "Model::~Model(" << this << ")" << std::endl;
+    std::cerr << "Model::~Model(" << this << ")" << std::endl;
 
     if (!m_aboutToDelete) {
         std::cerr << "NOTE: Model::~Model(" << this << ", \""
@@ -66,6 +66,8 @@
 void
 Model::aboutToDelete()
 {
+    std::cerr << "Model(" << this << ")::aboutToDelete()" << std::endl;
+
     if (m_aboutToDelete) {
         std::cerr << "WARNING: Model(" << this << ", \""
                   << objectName().toStdString() << "\")::aboutToDelete: "
@@ -100,7 +102,7 @@
 {
     if (!m_alignment) {
         if (m_sourceModel) return m_sourceModel->getAlignmentReference();
-        return this;
+        return 0;
     }
     return m_alignment->getReferenceModel();
 }
@@ -113,9 +115,8 @@
         else return frame;
     }
     size_t refFrame = m_alignment->toReference(frame);
-    //!!! this should be totally wrong, but because alignToReference and
-    // alignFromReference are the wrong way around, it's right... *sigh*
-    if (refFrame > getEndFrame()) refFrame = getEndFrame();
+    const Model *m = m_alignment->getReferenceModel();
+    if (m && refFrame > m->getEndFrame()) refFrame = m->getEndFrame();
     return refFrame;
 }
 
@@ -127,6 +128,7 @@
         else return refFrame;
     }
     size_t frame = m_alignment->fromReference(refFrame);
+    if (frame > getEndFrame()) frame = getEndFrame();
     return frame;
 }
 
@@ -148,12 +150,21 @@
 Model::getTitle() const
 {
     if (m_sourceModel) return m_sourceModel->getTitle();
+    else return "";
 }
 
 QString
 Model::getMaker() const
 {
     if (m_sourceModel) return m_sourceModel->getMaker();
+    else return "";
+}
+
+QString
+Model::getLocation() const
+{
+    if (m_sourceModel) return m_sourceModel->getLocation();
+    else return "";
 }
 
 void
--- a/data/model/Model.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/Model.h	Wed Feb 27 11:59:42 2008 +0000
@@ -77,6 +77,18 @@
     virtual QString getMaker() const;
 
     /**
+     * Return the location of the data in this model (e.g. source
+     * URL).  This should not normally be returned for editable models
+     * that have been edited.
+     */
+    virtual QString getLocation() const;
+
+    /**
+     * Return the type of the model.  For display purposes only.
+     */
+    virtual QString getTypeName() const = 0;
+
+    /**
      * Return a copy of this model.
      *
      * If the model is not editable, this may be effectively a shallow
--- a/data/model/NoteModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/NoteModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -32,24 +32,27 @@
 struct Note
 {
 public:
-    Note(long _frame) : frame(_frame), value(0.0f), duration(0) { }
-    Note(long _frame, float _value, size_t _duration, QString _label) :
-	frame(_frame), value(_value), duration(_duration), label(_label) { }
+    Note(long _frame) : frame(_frame), value(0.0f), duration(0), level(1.f) { }
+    Note(long _frame, float _value, size_t _duration, float _level, QString _label) :
+	frame(_frame), value(_value), duration(_duration), level(_level), label(_label) { }
 
     int getDimensions() const { return 3; }
 
     long frame;
     float value;
     size_t duration;
+    float level;
     QString label;
 
+    QString getLabel() const { return label; }
+    
     void toXml(QTextStream &stream,
                QString indent = "",
                QString extraAttributes = "") const
     {
 	stream <<
-            QString("%1<point frame=\"%2\" value=\"%3\" duration=\"%4\" label=\"%5\" %6/>\n")
-	    .arg(indent).arg(frame).arg(value).arg(duration).arg(label).arg(extraAttributes);
+            QString("%1<point frame=\"%2\" value=\"%3\" duration=\"%4\" level=\"%5\" label=\"%6\" %7/>\n")
+	    .arg(indent).arg(frame).arg(value).arg(duration).arg(level).arg(label).arg(extraAttributes);
     }
 
     QString toDelimitedDataString(QString delimiter, size_t sampleRate) const
@@ -57,7 +60,8 @@
         QStringList list;
         list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str();
         list << QString("%1").arg(value);
-        list << QString("%1").arg(duration);
+        list << RealTime::frame2RealTime(duration, sampleRate).toString().c_str();
+        list << QString("%1").arg(level);
         if (label != "") list << label;
         return list.join(delimiter);
     }
@@ -68,6 +72,7 @@
 	    if (p1.frame != p2.frame) return p1.frame < p2.frame;
 	    if (p1.value != p2.value) return p1.value < p2.value;
 	    if (p1.duration != p2.duration) return p1.duration < p2.duration;
+            if (p1.level != p2.level) return p1.level < p2.level;
 	    return p1.label < p2.label;
 	}
     };
@@ -122,6 +127,8 @@
      */
     virtual PointList getPoints(long frame) const;
 
+    QString getTypeName() const { return tr("Note"); }
+
     virtual void toXml(QTextStream &out,
                        QString indent = "",
                        QString extraAttributes = "") const
--- a/data/model/PowerOfSqrtTwoZoomConstraint.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/PowerOfSqrtTwoZoomConstraint.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -75,7 +75,13 @@
 	}
 
 //	std::cerr << "Testing base " << base << std::endl;
-	if (base >= blockSize) {
+
+        if (base == blockSize) {
+            result = base;
+            break;
+        }
+
+	if (base > blockSize) {
 	    if (dir == RoundNearest) {
 		if (base - blockSize < blockSize - prevBase) {
 		    dir = RoundUp;
--- a/data/model/RangeSummarisableTimeValueModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/RangeSummarisableTimeValueModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -71,6 +71,10 @@
      * block size equal to the distance between start and end frames.
      */
     virtual Range getSummary(size_t channel, size_t start, size_t count) const = 0;
+
+    virtual size_t getSummaryBlockSize(size_t desired) const = 0;
+
+    QString getTypeName() const { return tr("Range-Summarisable Time-Value"); }
 };
 
 #endif
--- a/data/model/SparseModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/SparseModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -132,6 +132,8 @@
 
     virtual bool hasTextLabels() const { return m_hasTextLabels; }
 
+    QString getTypeName() const { return tr("Sparse"); }
+
     virtual void toXml(QTextStream &out,
                        QString indent = "",
                        QString extraAttributes = "") const;
@@ -462,7 +464,7 @@
 	QMutexLocker locker(&m_mutex);
 	m_points.insert(point);
         m_pointCount++;
-        if (point.label != "") m_hasTextLabels = true;
+        if (point.getLabel() != "") m_hasTextLabels = true;
     }
 
     // Even though this model is nominally sparse, there may still be
--- a/data/model/SparseOneDimensionalModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/SparseOneDimensionalModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -30,7 +30,9 @@
     
     long frame;
     QString label;
-    
+
+    QString getLabel() const { return label; }
+
     void toXml(QTextStream &stream,
                QString indent = "",
                QString extraAttributes = "") const
@@ -84,6 +86,8 @@
 	}
 	return -1;
     }
+
+    QString getTypeName() const { return tr("Sparse 1-D"); }
 };
 
 #endif
--- a/data/model/SparseTimeValueModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/SparseTimeValueModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -38,6 +38,8 @@
     long frame;
     float value;
     QString label;
+
+    QString getLabel() const { return label; }
     
     void toXml(QTextStream &stream, QString indent = "",
                QString extraAttributes = "") const
@@ -93,6 +95,8 @@
     {
 	PlayParameterRepository::getInstance()->addModel(this);
     }
+
+    QString getTypeName() const { return tr("Sparse Time-Value"); }
 };
 
 
--- a/data/model/SparseValueModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/SparseValueModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -51,6 +51,9 @@
     using SparseModel<PointType>::m_points;
     using SparseModel<PointType>::modelChanged;
     using SparseModel<PointType>::getPoints;
+    using SparseModel<PointType>::tr;
+
+    QString getTypeName() const { return tr("Sparse Value"); }
 
     virtual float getValueMinimum() const { return m_valueMinimum; }
     virtual float getValueMaximum() const { return m_valueMaximum; }
--- a/data/model/TextModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/TextModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -38,6 +38,8 @@
     long frame;
     float height;
     QString label;
+
+    QString getLabel() const { return label; }
     
     void toXml(QTextStream &stream, QString indent = "",
                QString extraAttributes = "") const
@@ -93,6 +95,8 @@
 	     QString("%1 subtype=\"text\"")
 	     .arg(extraAttributes));
     }
+
+    QString getTypeName() const { return tr("Text"); }
 };
 
 
--- a/data/model/WaveFileModel.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/WaveFileModel.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -173,6 +173,13 @@
     if (m_reader) return m_reader->getMaker();
     return "";
 }
+
+QString
+WaveFileModel::getLocation() const
+{
+    if (m_reader) return m_reader->getLocation();
+    return "";
+}
     
 size_t
 WaveFileModel::getData(int channel, size_t start, size_t count,
@@ -182,7 +189,7 @@
     // This is used for e.g. audio playback.
     // Could be much more efficient (although compiler optimisation will help)
 
-    if (start > m_startFrame) {
+    if (start >= m_startFrame) {
         start -= m_startFrame;
     } else {
         for (size_t i = 0; i < count; ++i) buffer[i] = 0.f;
@@ -204,12 +211,14 @@
 //              << start << ", " << end << "): calling reader" << std::endl;
 #endif
 
-    SampleBlock frames;
+    int channels = getChannelCount();
+
+    SampleBlock frames(count * channels);
     m_reader->getInterleavedFrames(start, count, frames);
 
     size_t i = 0;
 
-    int ch0 = channel, ch1 = channel, channels = getChannelCount();
+    int ch0 = channel, ch1 = channel;
     if (channel == -1) {
 	ch0 = 0;
 	ch1 = channels - 1;
@@ -255,12 +264,14 @@
         return 0;
     }
 
-    SampleBlock frames;
+    int channels = getChannelCount();
+
+    SampleBlock frames(count * channels);
     m_reader->getInterleavedFrames(start, count, frames);
 
     size_t i = 0;
 
-    int ch0 = channel, ch1 = channel, channels = getChannelCount();
+    int ch0 = channel, ch1 = channel;
     if (channel == -1) {
 	ch0 = 0;
 	ch1 = channels - 1;
@@ -285,12 +296,112 @@
     return i;
 }
 
+size_t
+WaveFileModel::getData(size_t fromchannel, size_t tochannel,
+                       size_t start, size_t count,
+                       float **buffer) const
+{
+    size_t channels = getChannelCount();
+
+    if (fromchannel > tochannel) {
+        std::cerr << "ERROR: WaveFileModel::getData: fromchannel ("
+                  << fromchannel << ") > tochannel (" << tochannel << ")"
+                  << std::endl;
+        return 0;
+    }
+
+    if (tochannel >= channels) {
+        std::cerr << "ERROR: WaveFileModel::getData: tochannel ("
+                  << tochannel << ") >= channel count (" << channels << ")"
+                  << std::endl;
+        return 0;
+    }
+
+    if (fromchannel == tochannel) {
+        return getData(fromchannel, start, count, buffer[0]);
+    }
+
+    size_t reqchannels = (tochannel - fromchannel) + 1;
+
+    // Always read these directly from the file. 
+    // This is used for e.g. audio playback.
+    // Could be much more efficient (although compiler optimisation will help)
+
+    if (start >= m_startFrame) {
+        start -= m_startFrame;
+    } else {
+        for (size_t c = 0; c < reqchannels; ++c) {
+            for (size_t i = 0; i < count; ++i) buffer[c][i] = 0.f;
+        }
+        if (count <= m_startFrame - start) {
+            return 0;
+        } else {
+            count -= (m_startFrame - start);
+            start = 0;
+        }
+    }
+
+    if (!m_reader || !m_reader->isOK() || count == 0) {
+        for (size_t c = 0; c < reqchannels; ++c) {
+            for (size_t i = 0; i < count; ++i) buffer[c][i] = 0.f;
+        }
+        return 0;
+    }
+
+    SampleBlock frames(count * channels);
+    m_reader->getInterleavedFrames(start, count, frames);
+
+    size_t i = 0;
+
+    int ch0 = fromchannel, ch1 = tochannel;
+    
+    size_t index = 0, available = frames.size();
+
+    while (i < count) {
+
+        if (index >= available) break;
+
+        size_t destc = 0;
+
+        for (size_t c = 0; c < channels; ++c) {
+            
+            if (c >= fromchannel && c <= tochannel) {
+                buffer[destc][i] = frames[index];
+                ++destc;
+            }
+
+            ++index;
+        }
+
+        ++i;
+    }
+
+    return i;
+}
+
+size_t
+WaveFileModel::getSummaryBlockSize(size_t desired) const
+{
+    int cacheType = 0;
+    int power = m_zoomConstraint.getMinCachePower();
+    size_t roundedBlockSize = m_zoomConstraint.getNearestBlockSize
+        (desired, cacheType, power, ZoomConstraint::RoundDown);
+    if (cacheType != 0 && cacheType != 1) {
+        // We will be reading directly from file, so can satisfy any
+        // blocksize requirement
+        return desired;
+    } else {
+        return roundedBlockSize;
+    }
+}    
+
 void
 WaveFileModel::getSummaries(size_t channel, size_t start, size_t count,
                             RangeBlock &ranges, size_t &blockSize) const
 {
     ranges.clear();
     if (!isOK()) return;
+    ranges.reserve((count / blockSize) + 1);
 
     if (start > m_startFrame) start -= m_startFrame;
     else if (count <= m_startFrame - start) return;
@@ -301,8 +412,8 @@
 
     int cacheType = 0;
     int power = m_zoomConstraint.getMinCachePower();
-    blockSize = m_zoomConstraint.getNearestBlockSize
-        (blockSize, cacheType, power, ZoomConstraint::RoundUp);
+    size_t roundedBlockSize = m_zoomConstraint.getNearestBlockSize
+        (blockSize, cacheType, power, ZoomConstraint::RoundDown);
 
     size_t channels = getChannelCount();
 
@@ -316,17 +427,26 @@
 	// matter by putting a single cache in getInterleavedFrames
 	// for short queries.
 
-	SampleBlock frames;
-	m_reader->getInterleavedFrames(start, count, frames);
+        m_directReadMutex.lock();
+
+        if (m_lastDirectReadStart != start ||
+            m_lastDirectReadCount != count ||
+            m_directRead.empty()) {
+
+            m_reader->getInterleavedFrames(start, count, m_directRead);
+            m_lastDirectReadStart = start;
+            m_lastDirectReadCount = count;
+        }
+
 	float max = 0.0, min = 0.0, total = 0.0;
 	size_t i = 0, got = 0;
 
 	while (i < count) {
 
 	    size_t index = i * channels + channel;
-	    if (index >= frames.size()) break;
+	    if (index >= m_directRead.size()) break;
             
-	    float sample = frames[index];
+	    float sample = m_directRead[index];
             if (sample > max || got == 0) max = sample;
 	    if (sample < min || got == 0) min = sample;
             total += fabsf(sample);
@@ -341,6 +461,8 @@
 	    }
 	}
 
+        m_directReadMutex.unlock();
+
 	if (got > 0) {
             ranges.push_back(Range(min, max, total / got));
 	}
@@ -353,6 +475,8 @@
     
 	const RangeBlock &cache = m_cache[cacheType];
 
+        blockSize = roundedBlockSize;
+
 	size_t cacheBlock, div;
         
 	if (cacheType == 0) {
--- a/data/model/WaveFileModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/WaveFileModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -50,6 +50,7 @@
 
     QString getTitle() const;
     QString getMaker() const;
+    QString getLocation() const;
 
     virtual Model *clone() const;
 
@@ -67,12 +68,20 @@
     virtual size_t getData(int channel, size_t start, size_t count,
                            double *buffer) const;
 
+    virtual size_t getData(size_t fromchannel, size_t tochannel,
+                           size_t start, size_t count,
+                           float **buffers) const;
+
+    virtual size_t getSummaryBlockSize(size_t desired) const;
+
     virtual void getSummaries(size_t channel, size_t start, size_t count,
                               RangeBlock &ranges,
                               size_t &blockSize) const;
 
     virtual Range getSummary(size_t channel, size_t start, size_t count) const;
 
+    QString getTypeName() const { return tr("Wave File"); }
+
     virtual void toXml(QTextStream &out,
                        QString indent = "",
                        QString extraAttributes = "") const;
@@ -121,6 +130,11 @@
     size_t m_lastFillExtent;
     bool m_exiting;
     static PowerOfSqrtTwoZoomConstraint m_zoomConstraint;
+
+    mutable SampleBlock m_directRead;
+    mutable size_t m_lastDirectReadStart;
+    mutable size_t m_lastDirectReadCount;
+    mutable QMutex m_directReadMutex;
 };    
 
 #endif
--- a/data/model/WritableWaveFileModel.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/WritableWaveFileModel.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -185,11 +185,25 @@
                                double *buffer) const
 {
     if (!m_model || m_model->getChannelCount() == 0) return 0;
-//    std::cerr << "WritableWaveFileModel::getValues(" << channel << ", "
-//              << start << ", " << end << "): calling model" << std::endl;
     return m_model->getData(channel, start, count, buffer);
 }
 
+size_t
+WritableWaveFileModel::getData(size_t fromchannel, size_t tochannel,
+                               size_t start, size_t count,
+                               float **buffers) const
+{
+    if (!m_model || m_model->getChannelCount() == 0) return 0;
+    return m_model->getData(fromchannel, tochannel, start, count, buffers);
+}    
+
+size_t
+WritableWaveFileModel::getSummaryBlockSize(size_t desired) const
+{
+    if (!m_model) return desired;
+    return m_model->getSummaryBlockSize(desired);
+}
+
 void
 WritableWaveFileModel::getSummaries(size_t channel, size_t start, size_t count,
                                     RangeBlock &ranges,
--- a/data/model/WritableWaveFileModel.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/data/model/WritableWaveFileModel.h	Wed Feb 27 11:59:42 2008 +0000
@@ -68,11 +68,19 @@
     virtual size_t getData(int channel, size_t start, size_t count,
                            double *buffer) const;
 
+    virtual size_t getData(size_t fromchannel, size_t tochannel,
+                           size_t start, size_t count,
+                           float **buffer) const;
+
+    virtual size_t getSummaryBlockSize(size_t desired) const;
+
     virtual void getSummaries(size_t channel, size_t start, size_t count,
                               RangeBlock &ranges, size_t &blockSize) const;
 
     virtual Range getSummary(size_t channel, size_t start, size_t count) const;
 
+    QString getTypeName() const { return tr("Writable Wave File"); }
+
     virtual void toXml(QTextStream &out,
                        QString indent = "",
                        QString extraAttributes = "") const;
--- a/plugin/DSSIPluginInstance.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/DSSIPluginInstance.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -831,6 +831,20 @@
     }
 }
 
+int
+DSSIPluginInstance::getParameterDisplayHint(unsigned int parameter) const
+{
+    if (parameter >= m_controlPortsIn.size()) return 0.0;
+
+    LADSPAPluginFactory *f = dynamic_cast<LADSPAPluginFactory *>(m_factory);
+    if (f) {
+	return f->getPortDisplayHint(m_descriptor->LADSPA_Plugin,
+                                     m_controlPortsIn[parameter].first);
+    } else {
+	return PortHint::NoHint;
+    }
+}
+
 std::string
 DSSIPluginInstance::configure(std::string key,
 			      std::string value)
--- a/plugin/DSSIPluginInstance.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/DSSIPluginInstance.h	Wed Feb 27 11:59:42 2008 +0000
@@ -60,6 +60,7 @@
     virtual void setParameterValue(unsigned int parameter, float value);
     virtual float getParameterValue(unsigned int parameter) const;
     virtual float getParameterDefault(unsigned int parameter) const;
+    virtual int getParameterDisplayHint(unsigned int parameter) const;
 
     virtual ParameterList getParameterDescriptors() const;
     virtual float getParameter(std::string) const;
--- a/plugin/FeatureExtractionPluginFactory.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/FeatureExtractionPluginFactory.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -95,6 +95,7 @@
     if (factory) {
 	std::vector<QString> tmp = factory->getPluginIdentifiers();
 	for (size_t i = 0; i < tmp.size(); ++i) {
+//            std::cerr << "identifier: " << tmp[i].toStdString() << std::endl;
 	    rv.push_back(tmp[i]);
 	}
     }
--- a/plugin/LADSPAPluginFactory.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/LADSPAPluginFactory.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -161,6 +161,10 @@
 	minimum *= m_sampleRate;
     }
 
+    if (LADSPA_IS_HINT_LOGARITHMIC(d)) {
+        if (minimum == 0.f) minimum = 1.f;
+    }
+
     return minimum;
 }
 
@@ -211,6 +215,17 @@
 
     bool logarithmic = LADSPA_IS_HINT_LOGARITHMIC(d);
     
+    float logmin = 0, logmax = 0;
+    if (logarithmic) {
+        float thresh = powf(10, -10);
+        if (minimum < thresh) logmin = -10;
+        else logmin = log10f(minimum);
+        if (maximum < thresh) logmax = -10;
+        else logmax = log10f(maximum);
+    }
+
+//    std::cerr << "LADSPAPluginFactory::getPortDefault: hint = " << d << std::endl;
+
     if (!LADSPA_IS_HINT_HAS_DEFAULT(d)) {
 	
 	deft = minimum;
@@ -222,8 +237,7 @@
     } else if (LADSPA_IS_HINT_DEFAULT_LOW(d)) {
 	
 	if (logarithmic) {
-	    deft = powf(10, log10(minimum) * 0.75 +
-			    log10(maximum) * 0.25);
+	    deft = powf(10, logmin * 0.75 + logmax * 0.25);
 	} else {
 	    deft = minimum * 0.75 + maximum * 0.25;
 	}
@@ -231,8 +245,7 @@
     } else if (LADSPA_IS_HINT_DEFAULT_MIDDLE(d)) {
 	
 	if (logarithmic) {
-	    deft = powf(10, log10(minimum) * 0.5 +
-		   	    log10(maximum) * 0.5);
+	    deft = powf(10, logmin * 0.5 + logmax * 0.5);
 	} else {
 	    deft = minimum * 0.5 + maximum * 0.5;
 	}
@@ -240,8 +253,7 @@
     } else if (LADSPA_IS_HINT_DEFAULT_HIGH(d)) {
 	
 	if (logarithmic) {
-	    deft = powf(10, log10(minimum) * 0.25 +
-			    log10(maximum) * 0.75);
+	    deft = powf(10, logmin * 0.25 + logmax * 0.75);
 	} else {
 	    deft = minimum * 0.25 + maximum * 0.75;
 	}
@@ -271,10 +283,14 @@
 	
 	deft = minimum;
     }
+
+//!!! No -- the min and max have already been multiplied by the rate,
+//so it would happen twice if we did it here -- and e.g. DEFAULT_440
+//doesn't want to be multiplied by the rate either
     
-    if (LADSPA_IS_HINT_SAMPLE_RATE(d)) {
-	deft *= m_sampleRate;
-    }
+//    if (LADSPA_IS_HINT_SAMPLE_RATE(d)) {
+//	deft *= m_sampleRate;
+//    }
 
     return deft;
 }
--- a/plugin/LADSPAPluginInstance.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/LADSPAPluginInstance.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -515,6 +515,19 @@
     }
 }
 
+int
+LADSPAPluginInstance::getParameterDisplayHint(unsigned int parameter) const
+{
+    if (parameter >= m_controlPortsIn.size()) return 0.0;
+
+    LADSPAPluginFactory *f = dynamic_cast<LADSPAPluginFactory *>(m_factory);
+    if (f) {
+	return f->getPortDisplayHint(m_descriptor, m_controlPortsIn[parameter].first);
+    } else {
+	return PortHint::NoHint;
+    }
+}
+
 void
 LADSPAPluginInstance::run(const Vamp::RealTime &)
 {
--- a/plugin/LADSPAPluginInstance.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/LADSPAPluginInstance.h	Wed Feb 27 11:59:42 2008 +0000
@@ -56,6 +56,7 @@
     virtual void setParameterValue(unsigned int parameter, float value);
     virtual float getParameterValue(unsigned int parameter) const;
     virtual float getParameterDefault(unsigned int parameter) const;
+    virtual int getParameterDisplayHint(unsigned int parameter) const;
     
     virtual ParameterList getParameterDescriptors() const;
     virtual float getParameter(std::string) const;
--- a/plugin/RealTimePluginInstance.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/RealTimePluginInstance.h	Wed Feb 27 11:59:42 2008 +0000
@@ -112,6 +112,7 @@
     virtual void setParameterValue(unsigned int parameter, float value) = 0;
     virtual float getParameterValue(unsigned int parameter) const = 0;
     virtual float getParameterDefault(unsigned int parameter) const = 0;
+    virtual int getParameterDisplayHint(unsigned int parameter) const = 0;
 
     virtual std::string configure(std::string /* key */, std::string /* value */) { return std::string(); }
 
--- a/plugin/plugin.pro	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/plugin.pro	Wed Feb 27 11:59:42 2008 +0000
@@ -36,7 +36,6 @@
            api/alsa/seq_midi_event.h \
            api/alsa/sound/asequencer.h \
            transform/FeatureExtractionModelTransformer.h \
-           transform/PluginTransformer.h \
            transform/RealTimeEffectModelTransformer.h \
            transform/Transform.h \
            transform/TransformDescription.h \
@@ -55,7 +54,6 @@
            api/dssi_alsa_compat.c \
            plugins/SamplePlayer.cpp \
            transform/FeatureExtractionModelTransformer.cpp \
-           transform/PluginTransformer.cpp \
            transform/RealTimeEffectModelTransformer.cpp \
            transform/Transform.cpp \
            transform/TransformFactory.cpp \
--- a/plugin/transform/FeatureExtractionModelTransformer.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/FeatureExtractionModelTransformer.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -29,81 +29,130 @@
 #include "data/model/FFTModel.h"
 #include "data/model/WaveFileModel.h"
 
+#include "TransformFactory.h"
+
 #include <QMessageBox>
 
 #include <iostream>
 
-FeatureExtractionModelTransformer::FeatureExtractionModelTransformer(Model *inputModel,
-								   QString pluginId,
-                                                                   const ExecutionContext &context,
-                                                                   QString configurationXml,
-								   QString outputName) :
-    PluginTransformer(inputModel, context),
+FeatureExtractionModelTransformer::FeatureExtractionModelTransformer(Input in,
+                                                                     const Transform &transform) :
+    ModelTransformer(in, transform),
     m_plugin(0),
     m_descriptor(0),
     m_outputFeatureNo(0)
 {
-//    std::cerr << "FeatureExtractionModelTransformer::FeatureExtractionModelTransformer: plugin " << pluginId.toStdString() << ", outputName " << outputName.toStdString() << std::endl;
+//    std::cerr << "FeatureExtractionModelTransformer::FeatureExtractionModelTransformer: plugin " << pluginId.toStdString() << ", outputName " << m_transform.getOutput().toStdString() << std::endl;
+
+    QString pluginId = transform.getPluginIdentifier();
 
     FeatureExtractionPluginFactory *factory =
 	FeatureExtractionPluginFactory::instanceFor(pluginId);
 
     if (!factory) {
-	std::cerr << "FeatureExtractionModelTransformer: No factory available for plugin id \""
-		  << pluginId.toStdString() << "\"" << std::endl;
+        m_message = tr("No factory available for feature extraction plugin id \"%1\" (unknown plugin type, or internal error?)").arg(pluginId);
 	return;
     }
 
-    m_plugin = factory->instantiatePlugin(pluginId, m_input->getSampleRate());
+    DenseTimeValueModel *input = getConformingInput();
+    if (!input) {
+        m_message = tr("Input model for feature extraction plugin \"%1\" is of wrong type (internal error?)").arg(pluginId);
+        return;
+    }
 
+    m_plugin = factory->instantiatePlugin(pluginId, input->getSampleRate());
     if (!m_plugin) {
-	std::cerr << "FeatureExtractionModelTransformer: Failed to instantiate plugin \""
-		  << pluginId.toStdString() << "\"" << std::endl;
+        m_message = tr("Failed to instantiate plugin \"%1\"").arg(pluginId);
 	return;
     }
 
-    if (configurationXml != "") {
-        PluginXml(m_plugin).setParametersFromXml(configurationXml);
-    }
+    TransformFactory::getInstance()->makeContextConsistentWithPlugin
+        (m_transform, m_plugin);
 
-    DenseTimeValueModel *input = getInput();
-    if (!input) return;
+    TransformFactory::getInstance()->setPluginParameters
+        (m_transform, m_plugin);
 
     size_t channelCount = input->getChannelCount();
     if (m_plugin->getMaxChannelCount() < channelCount) {
 	channelCount = 1;
     }
     if (m_plugin->getMinChannelCount() > channelCount) {
-	std::cerr << "FeatureExtractionModelTransformer:: "
-		  << "Can't provide enough channels to plugin (plugin min "
-		  << m_plugin->getMinChannelCount() << ", max "
-		  << m_plugin->getMaxChannelCount() << ", input model has "
-		  << input->getChannelCount() << ")" << std::endl;
+        m_message = tr("Cannot provide enough channels to feature extraction plugin \"%1\" (plugin min is %2, max %3; input model has %4)")
+            .arg(pluginId)
+            .arg(m_plugin->getMinChannelCount())
+            .arg(m_plugin->getMaxChannelCount())
+            .arg(input->getChannelCount());
 	return;
     }
 
     std::cerr << "Initialising feature extraction plugin with channels = "
-              << channelCount << ", step = " << m_context.stepSize
-              << ", block = " << m_context.blockSize << std::endl;
+              << channelCount << ", step = " << m_transform.getStepSize()
+              << ", block = " << m_transform.getBlockSize() << std::endl;
 
     if (!m_plugin->initialise(channelCount,
-                              m_context.stepSize,
-                              m_context.blockSize)) {
-        std::cerr << "FeatureExtractionModelTransformer: Plugin "
-                  << m_plugin->getIdentifier() << " failed to initialise!" << std::endl;
-        return;
+                              m_transform.getStepSize(),
+                              m_transform.getBlockSize())) {
+
+        size_t pstep = m_transform.getStepSize();
+        size_t pblock = m_transform.getBlockSize();
+
+        m_transform.setStepSize(0);
+        m_transform.setBlockSize(0);
+        TransformFactory::getInstance()->makeContextConsistentWithPlugin
+            (m_transform, m_plugin);
+
+        if (m_transform.getStepSize() != pstep ||
+            m_transform.getBlockSize() != pblock) {
+            
+            if (!m_plugin->initialise(channelCount,
+                                      m_transform.getStepSize(),
+                                      m_transform.getBlockSize())) {
+
+                m_message = tr("Failed to initialise feature extraction plugin \"%1\"").arg(pluginId);
+                return;
+
+            } else {
+
+                m_message = tr("Feature extraction plugin \"%1\" rejected the given step and block sizes (%2 and %3); using plugin defaults (%4 and %5) instead")
+                    .arg(pluginId)
+                    .arg(pstep)
+                    .arg(pblock)
+                    .arg(m_transform.getStepSize())
+                    .arg(m_transform.getBlockSize());
+            }
+
+        } else {
+
+            m_message = tr("Failed to initialise feature extraction plugin \"%1\"").arg(pluginId);
+            return;
+        }
+    }
+
+    if (m_transform.getPluginVersion() != "") {
+        QString pv = QString("%1").arg(m_plugin->getPluginVersion());
+        if (pv != m_transform.getPluginVersion()) {
+            QString vm = tr("Transform was configured for version %1 of plugin \"%2\", but the plugin being used is version %3")
+                .arg(m_transform.getPluginVersion())
+                .arg(pluginId)
+                .arg(pv);
+            if (m_message != "") {
+                m_message = QString("%1; %2").arg(vm).arg(m_message);
+            } else {
+                m_message = vm;
+            }
+        }
     }
 
     Vamp::Plugin::OutputList outputs = m_plugin->getOutputDescriptors();
 
     if (outputs.empty()) {
-	std::cerr << "FeatureExtractionModelTransformer: Plugin \""
-		  << pluginId.toStdString() << "\" has no outputs" << std::endl;
+        m_message = tr("Plugin \"%1\" has no outputs").arg(pluginId);
 	return;
     }
     
     for (size_t i = 0; i < outputs.size(); ++i) {
-	if (outputName == "" || outputs[i].identifier == outputName.toStdString()) {
+	if (m_transform.getOutput() == "" ||
+            outputs[i].identifier == m_transform.getOutput().toStdString()) {
 	    m_outputFeatureNo = i;
 	    m_descriptor = new Vamp::Plugin::OutputDescriptor
 		(outputs[i]);
@@ -112,9 +161,9 @@
     }
 
     if (!m_descriptor) {
-	std::cerr << "FeatureExtractionModelTransformer: Plugin \""
-		  << pluginId.toStdString() << "\" has no output named \""
-		  << outputName.toStdString() << "\"" << std::endl;
+        m_message = tr("Plugin \"%1\" has no output named \"%2\"")
+            .arg(pluginId)
+            .arg(m_transform.getOutput());
 	return;
     }
 
@@ -138,7 +187,7 @@
         haveExtents = true;
     }
 
-    size_t modelRate = m_input->getSampleRate();
+    size_t modelRate = input->getSampleRate();
     size_t modelResolution = 1;
     
     switch (m_descriptor->sampleType) {
@@ -150,7 +199,7 @@
 	break;
 
     case Vamp::Plugin::OutputDescriptor::OneSamplePerStep:
-	modelResolution = m_context.stepSize;
+	modelResolution = m_transform.getStepSize();
 	break;
 
     case Vamp::Plugin::OutputDescriptor::FixedSampleRate:
@@ -217,7 +266,7 @@
         m_output = model;
     }
 
-    if (m_output) m_output->setSourceModel(m_input);
+    if (m_output) m_output->setSourceModel(input);
 }
 
 FeatureExtractionModelTransformer::~FeatureExtractionModelTransformer()
@@ -228,12 +277,12 @@
 }
 
 DenseTimeValueModel *
-FeatureExtractionModelTransformer::getInput()
+FeatureExtractionModelTransformer::getConformingInput()
 {
     DenseTimeValueModel *dtvm =
 	dynamic_cast<DenseTimeValueModel *>(getInputModel());
     if (!dtvm) {
-	std::cerr << "FeatureExtractionModelTransformer::getInput: WARNING: Input model is not conformable to DenseTimeValueModel" << std::endl;
+	std::cerr << "FeatureExtractionModelTransformer::getConformingInput: WARNING: Input model is not conformable to DenseTimeValueModel" << std::endl;
     }
     return dtvm;
 }
@@ -241,7 +290,7 @@
 void
 FeatureExtractionModelTransformer::run()
 {
-    DenseTimeValueModel *input = getInput();
+    DenseTimeValueModel *input = getConformingInput();
     if (!input) return;
 
     if (!m_output) return;
@@ -258,7 +307,7 @@
         sleep(1);
     }
 
-    size_t sampleRate = m_input->getSampleRate();
+    size_t sampleRate = input->getSampleRate();
 
     size_t channelCount = input->getChannelCount();
     if (m_plugin->getMaxChannelCount() < channelCount) {
@@ -267,9 +316,12 @@
 
     float **buffers = new float*[channelCount];
     for (size_t ch = 0; ch < channelCount; ++ch) {
-	buffers[ch] = new float[m_context.blockSize + 2];
+	buffers[ch] = new float[m_transform.getBlockSize() + 2];
     }
 
+    size_t stepSize = m_transform.getStepSize();
+    size_t blockSize = m_transform.getBlockSize();
+
     bool frequencyDomain = (m_plugin->getInputDomain() ==
                             Vamp::Plugin::FrequencyDomain);
     std::vector<FFTModel *> fftModels;
@@ -277,12 +329,12 @@
     if (frequencyDomain) {
         for (size_t ch = 0; ch < channelCount; ++ch) {
             FFTModel *model = new FFTModel
-                                  (getInput(),
-                                   channelCount == 1 ? m_context.channel : ch,
-                                   m_context.windowType,
-                                   m_context.blockSize,
-                                   m_context.stepSize,
-                                   m_context.blockSize,
+                                  (getConformingInput(),
+                                   channelCount == 1 ? m_input.getChannel() : ch,
+                                   m_transform.getWindowType(),
+                                   blockSize,
+                                   stepSize,
+                                   blockSize,
                                    false,
                                    StorageAdviser::PrecisionCritical);
             if (!model->isOK()) {
@@ -299,11 +351,17 @@
         }
     }
 
-    long startFrame = m_input->getStartFrame();
-    long   endFrame = m_input->getEndFrame();
+    long startFrame = m_input.getModel()->getStartFrame();
+    long   endFrame = m_input.getModel()->getEndFrame();
 
-    long contextStart = m_context.startFrame;
-    long contextDuration = m_context.duration;
+    RealTime contextStartRT = m_transform.getStartTime();
+    RealTime contextDurationRT = m_transform.getDuration();
+
+    long contextStart =
+        RealTime::realTime2Frame(contextStartRT, sampleRate);
+
+    long contextDuration =
+        RealTime::realTime2Frame(contextDurationRT, sampleRate);
 
     if (contextStart == 0 || contextStart < startFrame) {
         contextStart = startFrame;
@@ -325,7 +383,7 @@
     while (!m_abandoned) {
 
         if (frequencyDomain) {
-            if (blockFrame - int(m_context.blockSize)/2 >
+            if (blockFrame - int(blockSize)/2 >
                 contextStart + contextDuration) break;
         } else {
             if (blockFrame >= 
@@ -334,25 +392,24 @@
 
 //	std::cerr << "FeatureExtractionModelTransformer::run: blockFrame "
 //		  << blockFrame << ", endFrame " << endFrame << ", blockSize "
-//                  << m_context.blockSize << std::endl;
+//                  << blockSize << std::endl;
 
 	long completion =
-	    (((blockFrame - contextStart) / m_context.stepSize) * 99) /
-	    (contextDuration / m_context.stepSize);
+	    (((blockFrame - contextStart) / stepSize) * 99) /
+	    (contextDuration / stepSize);
 
-	// channelCount is either m_input->channelCount or 1
+	// channelCount is either m_input.getModel()->channelCount or 1
 
-        for (size_t ch = 0; ch < channelCount; ++ch) {
-            if (frequencyDomain) {
-                int column = (blockFrame - startFrame) / m_context.stepSize;
-                for (size_t i = 0; i <= m_context.blockSize/2; ++i) {
+        if (frequencyDomain) {
+            for (size_t ch = 0; ch < channelCount; ++ch) {
+                int column = (blockFrame - startFrame) / stepSize;
+                for (size_t i = 0; i <= blockSize/2; ++i) {
                     fftModels[ch]->getValuesAt
                         (column, i, buffers[ch][i*2], buffers[ch][i*2+1]);
                 }
-            } else {
-                getFrames(ch, channelCount, 
-                          blockFrame, m_context.blockSize, buffers[ch]);
-            }                
+            }
+        } else {
+            getFrames(channelCount, blockFrame, blockSize, buffers);
         }
 
 	Vamp::Plugin::FeatureSet features = m_plugin->process
@@ -369,7 +426,7 @@
 	    prevCompletion = completion;
 	}
 
-	blockFrame += m_context.stepSize;
+	blockFrame += stepSize;
     }
 
     if (m_abandoned) return;
@@ -392,15 +449,17 @@
 }
 
 void
-FeatureExtractionModelTransformer::getFrames(int channel, int channelCount,
-                                            long startFrame, long size,
-                                            float *buffer)
+FeatureExtractionModelTransformer::getFrames(int channelCount,
+                                             long startFrame, long size,
+                                             float **buffers)
 {
     long offset = 0;
 
     if (startFrame < 0) {
-        for (int i = 0; i < size && startFrame + i < 0; ++i) {
-            buffer[i] = 0.0f;
+        for (int c = 0; c < channelCount; ++c) {
+            for (int i = 0; i < size && startFrame + i < 0; ++i) {
+                buffers[c][i] = 0.0f;
+            }
         }
         offset = -startFrame;
         size -= offset;
@@ -408,30 +467,52 @@
         startFrame = 0;
     }
 
-    long got = getInput()->getData
-        ((channelCount == 1 ? m_context.channel : channel),
-         startFrame, size, buffer + offset);
+    DenseTimeValueModel *input = getConformingInput();
+    if (!input) return;
+    
+    long got = 0;
+
+    if (channelCount == 1) {
+
+        got = input->getData(m_input.getChannel(), startFrame, size,
+                             buffers[0] + offset);
+
+        if (m_input.getChannel() == -1 && input->getChannelCount() > 1) {
+            // use mean instead of sum, as plugin input
+            float cc = float(input->getChannelCount());
+            for (long i = 0; i < size; ++i) {
+                buffers[0][i + offset] /= cc;
+            }
+        }
+
+    } else {
+
+        float **writebuf = buffers;
+        if (offset > 0) {
+            writebuf = new float *[channelCount];
+            for (int i = 0; i < channelCount; ++i) {
+                writebuf[i] = buffers[i] + offset;
+            }
+        }
+
+        got = input->getData(0, channelCount-1, startFrame, size, writebuf);
+
+        if (writebuf != buffers) delete[] writebuf;
+    }
 
     while (got < size) {
-        buffer[offset + got] = 0.0;
+        for (int c = 0; c < channelCount; ++c) {
+            buffers[c][got + offset] = 0.0;
+        }
         ++got;
     }
-
-    if (m_context.channel == -1 && channelCount == 1 &&
-        getInput()->getChannelCount() > 1) {
-        // use mean instead of sum, as plugin input
-        int cc = getInput()->getChannelCount();
-        for (long i = 0; i < size; ++i) {
-            buffer[i] /= cc;
-        }
-    }
 }
 
 void
 FeatureExtractionModelTransformer::addFeature(size_t blockFrame,
 					     const Vamp::Plugin::Feature &feature)
 {
-    size_t inputRate = m_input->getSampleRate();
+    size_t inputRate = m_input.getModel()->getSampleRate();
 
 //    std::cerr << "FeatureExtractionModelTransformer::addFeature("
 //	      << blockFrame << ")" << std::endl;
@@ -470,8 +551,10 @@
 	
     if (binCount == 0) {
 
-	SparseOneDimensionalModel *model = getOutput<SparseOneDimensionalModel>();
+	SparseOneDimensionalModel *model =
+            getConformingOutput<SparseOneDimensionalModel>();
 	if (!model) return;
+
 	model->addPoint(SparseOneDimensionalModel::Point(frame, feature.label.c_str()));
 	
     } else if (binCount == 1) {
@@ -479,8 +562,10 @@
 	float value = 0.0;
 	if (feature.values.size() > 0) value = feature.values[0];
 
-	SparseTimeValueModel *model = getOutput<SparseTimeValueModel>();
+	SparseTimeValueModel *model =
+            getConformingOutput<SparseTimeValueModel>();
 	if (!model) return;
+
 	model->addPoint(SparseTimeValueModel::Point(frame, value, feature.label.c_str()));
 //        std::cerr << "SparseTimeValueModel::addPoint(" << frame << ", " << value << "), " << feature.label.c_str() << std::endl;
 
@@ -495,12 +580,15 @@
         
         float velocity = 100;
         if (feature.values.size() > 2) velocity = feature.values[2];
+        if (velocity < 0) velocity = 127;
+        if (velocity > 127) velocity = 127;
 
-        NoteModel *model = getOutput<NoteModel>();
+        NoteModel *model = getConformingOutput<NoteModel>();
         if (!model) return;
 
         model->addPoint(NoteModel::Point(frame, pitch,
                                          lrintf(duration),
+                                         velocity / 127.f,
                                          feature.label.c_str()));
 	
     } else {
@@ -508,7 +596,7 @@
 	DenseThreeDimensionalModel::Column values = feature.values;
 	
 	EditableDenseThreeDimensionalModel *model =
-            getOutput<EditableDenseThreeDimensionalModel>();
+            getConformingOutput<EditableDenseThreeDimensionalModel>();
 	if (!model) return;
 
 	model->setColumn(frame / model->getResolution(), values);
@@ -528,29 +616,32 @@
 
     if (binCount == 0) {
 
-	SparseOneDimensionalModel *model = getOutput<SparseOneDimensionalModel>();
+	SparseOneDimensionalModel *model =
+            getConformingOutput<SparseOneDimensionalModel>();
 	if (!model) return;
-	model->setCompletion(completion, m_context.updates);
+	model->setCompletion(completion, true); //!!!m_context.updates);
 
     } else if (binCount == 1) {
 
-	SparseTimeValueModel *model = getOutput<SparseTimeValueModel>();
+	SparseTimeValueModel *model =
+            getConformingOutput<SparseTimeValueModel>();
 	if (!model) return;
-	model->setCompletion(completion, m_context.updates);
+	model->setCompletion(completion, true); //!!!m_context.updates);
 
     } else if (m_descriptor->sampleType ==
 	       Vamp::Plugin::OutputDescriptor::VariableSampleRate) {
 
-	NoteModel *model = getOutput<NoteModel>();
+	NoteModel *model =
+            getConformingOutput<NoteModel>();
 	if (!model) return;
-	model->setCompletion(completion, m_context.updates);
+	model->setCompletion(completion, true); //!!!m_context.updates);
 
     } else {
 
 	EditableDenseThreeDimensionalModel *model =
-            getOutput<EditableDenseThreeDimensionalModel>();
+            getConformingOutput<EditableDenseThreeDimensionalModel>();
 	if (!model) return;
-	model->setCompletion(completion, m_context.updates);
+	model->setCompletion(completion, true); //!!!m_context.updates);
     }
 }
 
--- a/plugin/transform/FeatureExtractionModelTransformer.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/FeatureExtractionModelTransformer.h	Wed Feb 27 11:59:42 2008 +0000
@@ -16,20 +16,23 @@
 #ifndef _FEATURE_EXTRACTION_PLUGIN_TRANSFORMER_H_
 #define _FEATURE_EXTRACTION_PLUGIN_TRANSFORMER_H_
 
-#include "PluginTransformer.h"
+#include "ModelTransformer.h"
+
+#include <QString>
+
+#include <vamp-sdk/Plugin.h>
+
+#include <iostream>
 
 class DenseTimeValueModel;
 
-class FeatureExtractionModelTransformer : public PluginTransformer
+class FeatureExtractionModelTransformer : public ModelTransformer
 {
     Q_OBJECT
 
 public:
-    FeatureExtractionModelTransformer(Model *inputModel,
-                                       QString plugin,
-                                       const ExecutionContext &context,
-                                       QString configurationXml = "",
-                                       QString outputName = "");
+    FeatureExtractionModelTransformer(Input input,
+                                      const Transform &transform);
     virtual ~FeatureExtractionModelTransformer();
 
 protected:
@@ -44,12 +47,12 @@
 
     void setCompletion(int);
 
-    void getFrames(int channel, int channelCount,
-                   long startFrame, long size, float *buffer);
+    void getFrames(int channelCount, long startFrame, long size,
+                   float **buffer);
 
     // just casts
-    DenseTimeValueModel *getInput();
-    template <typename ModelClass> ModelClass *getOutput() {
+    DenseTimeValueModel *getConformingInput();
+    template <typename ModelClass> ModelClass *getConformingOutput() {
 	ModelClass *mc = dynamic_cast<ModelClass *>(m_output);
 	if (!mc) {
 	    std::cerr << "FeatureExtractionModelTransformer::getOutput: Output model not conformable" << std::endl;
--- a/plugin/transform/ModelTransformer.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/ModelTransformer.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -15,8 +15,9 @@
 
 #include "ModelTransformer.h"
 
-ModelTransformer::ModelTransformer(Model *m) :
-    m_input(m),
+ModelTransformer::ModelTransformer(Input input, const Transform &transform) :
+    m_transform(transform),
+    m_input(input),
     m_output(0),
     m_detached(false),
     m_abandoned(false)
--- a/plugin/transform/ModelTransformer.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/ModelTransformer.h	Wed Feb 27 11:59:42 2008 +0000
@@ -20,6 +20,8 @@
 
 #include "data/model/Model.h"
 
+#include "Transform.h"
+
 /**
  * A ModelTransformer turns one data model into another.
  *
@@ -38,22 +40,72 @@
 public:
     virtual ~ModelTransformer();
 
-    // Just a hint to the processing thread that it should give up.
-    // Caller should still wait() and/or delete the transform before
-    // assuming its input and output models are no longer required.
+    class Input {
+    public:
+        Input(Model *m) : m_model(m), m_channel(-1) { }
+        Input(Model *m, int c) : m_model(m), m_channel(c) { }
+
+        Model *getModel() const { return m_model; }
+        void setModel(Model *m) { m_model = m; }
+
+        int getChannel() const { return m_channel; }
+        void setChannel(int c) { m_channel = c; }
+
+    protected:
+        Model *m_model;
+        int m_channel;
+    };
+
+    /**
+     * Hint to the processing thread that it should give up, for
+     * example because the process is going to exit or we want to get
+     * rid of the input model.  Caller should still wait() and/or
+     * delete the transform before assuming its input and output
+     * models are no longer required.
+     */
     void abandon() { m_abandoned = true; }
 
-    Model *getInputModel()  { return m_input; }
+    /**
+     * Return the input model for the transform.
+     */
+    Model *getInputModel()  { return m_input.getModel(); }
+
+    /**
+     * Return the input channel spec for the transform.
+     */
+    int getInputChannel() { return m_input.getChannel(); }
+
+    /**
+     * Return the output model created by the transform.  Returns a
+     * null model if the transform could not be initialised; an error
+     * message may be available via getMessage() in this situation.
+     */
     Model *getOutputModel() { return m_output; }
+
+    /**
+     * Return the output model, also detaching it from the transformer
+     * so that it will not be deleted when the transformer is.  The
+     * caller takes ownership of the model.
+     */
     Model *detachOutputModel() { m_detached = true; return m_output; }
 
+    /**
+     * Return a warning or error message.  If getOutputModel returned
+     * a null pointer, this should contain a fatal error message for
+     * the transformer; otherwise it may contain a warning to show to
+     * the user about e.g. suboptimal block size or whatever.  
+     */
+    QString getMessage() const { return m_message; }
+
 protected:
-    ModelTransformer(Model *m);
+    ModelTransformer(Input input, const Transform &transform);
 
-    Model *m_input; // I don't own this
+    Transform m_transform;
+    Input m_input; // I don't own the model in this
     Model *m_output; // I own this, unless...
     bool m_detached; // ... this is true.
     bool m_abandoned;
+    QString m_message;
 };
 
 #endif
--- a/plugin/transform/ModelTransformerFactory.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/ModelTransformerFactory.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -51,7 +51,8 @@
 }
 
 bool
-ModelTransformerFactory::getChannelRange(TransformId identifier, Vamp::PluginBase *plugin,
+ModelTransformerFactory::getChannelRange(TransformId identifier,
+                                         Vamp::PluginBase *plugin,
                                          int &minChannels, int &maxChannels)
 {
     Vamp::Plugin *vp = 0;
@@ -66,22 +67,24 @@
     }
 }
 
-Model *
-ModelTransformerFactory::getConfigurationForTransformer(TransformId identifier,
-                                               const std::vector<Model *> &candidateInputModels,
-                                               PluginTransformer::ExecutionContext &context,
-                                               QString &configurationXml,
-                                               AudioCallbackPlaySource *source,
-                                               size_t startFrame,
-                                               size_t duration)
+ModelTransformer::Input
+ModelTransformerFactory::getConfigurationForTransform(Transform &transform,
+                                                      const std::vector<Model *> &candidateInputModels,
+                                                      Model *defaultInputModel,
+                                                      AudioCallbackPlaySource *source,
+                                                      size_t startFrame,
+                                                      size_t duration)
 {
-    if (candidateInputModels.empty()) return 0;
+    ModelTransformer::Input input(0);
+
+    if (candidateInputModels.empty()) return input;
 
     //!!! This will need revision -- we'll have to have a callback
     //from the dialog for when the candidate input model is changed,
     //as we'll need to reinitialise the channel settings in the dialog
-    Model *inputModel = candidateInputModels[0]; //!!! for now
+    Model *inputModel = candidateInputModels[0];
     QStringList candidateModelNames;
+    QString defaultModelName;
     std::map<QString, Model *> modelMap;
     for (size_t i = 0; i < candidateInputModels.size(); ++i) {
         QString modelName = candidateInputModels[i]->objectName();
@@ -92,17 +95,20 @@
         }
         modelMap[modelName] = candidateInputModels[i];
         candidateModelNames.push_back(modelName);
+        if (candidateInputModels[i] == defaultInputModel) {
+            defaultModelName = modelName;
+        }
     }
 
-    QString id = identifier.section(':', 0, 2);
-    QString output = identifier.section(':', 3);
+    QString id = transform.getPluginIdentifier();
+    QString output = transform.getOutput();
     QString outputLabel = "";
     QString outputDescription = "";
     
     bool ok = false;
-    configurationXml = m_lastConfigurations[identifier];
+    QString configurationXml = m_lastConfigurations[transform.getIdentifier()];
 
-//    std::cerr << "last configuration: " << configurationXml.toStdString() << std::endl;
+    std::cerr << "last configuration: " << configurationXml.toStdString() << std::endl;
 
     Vamp::PluginBase *plugin = 0;
 
@@ -112,7 +118,7 @@
 
     if (FeatureExtractionPluginFactory::instanceFor(id)) {
 
-        std::cerr << "getConfigurationForTransformer: instantiating Vamp plugin" << std::endl;
+        std::cerr << "getConfigurationForTransform: instantiating Vamp plugin" << std::endl;
 
         Vamp::Plugin *vp =
             FeatureExtractionPluginFactory::instanceFor(id)->instantiatePlugin
@@ -179,11 +185,18 @@
 
     if (plugin) {
 
-        context = PluginTransformer::ExecutionContext(context.channel, plugin);
+        // Ensure block size etc are valid
+        TransformFactory::getInstance()->
+            makeContextConsistentWithPlugin(transform, plugin);
 
-        if (configurationXml != "") {
-            PluginXml(plugin).setParametersFromXml(configurationXml);
-        }
+        // Prepare the plugin with any existing parameters already
+        // found in the transform
+        TransformFactory::getInstance()->
+            setPluginParameters(transform, plugin);
+        
+        // For this interactive usage, we want to override those with
+        // whatever the user chose last time around
+        PluginXml(plugin).setParametersFromXml(configurationXml);
 
         int sourceChannels = 1;
         if (dynamic_cast<DenseTimeValueModel *>(inputModel)) {
@@ -192,7 +205,8 @@
         }
 
         int minChannels = 1, maxChannels = sourceChannels;
-        getChannelRange(identifier, plugin, minChannels, maxChannels);
+        getChannelRange(transform.getIdentifier(), plugin,
+                        minChannels, maxChannels);
 
         int targetChannels = sourceChannels;
         if (!effect) {
@@ -200,12 +214,13 @@
             if (sourceChannels > maxChannels) targetChannels = maxChannels;
         }
 
-        int defaultChannel = context.channel;
+        int defaultChannel = -1; //!!! no longer saved! [was context.channel]
 
         PluginParameterDialog *dialog = new PluginParameterDialog(plugin);
 
         if (candidateModelNames.size() > 1 && !generator) {
-            dialog->setCandidateInputModels(candidateModelNames);
+            dialog->setCandidateInputModels(candidateModelNames,
+                                            defaultModelName);
         }
 
         if (startFrame != 0 || duration != 0) {
@@ -236,22 +251,42 @@
         } else {
             std::cerr << "Selected input empty: \"" << selectedInput.toStdString() << "\"" << std::endl;
         }
+        
+        // Write parameters back to transform object
+        TransformFactory::getInstance()->
+            setParametersFromPlugin(transform, plugin);
 
-        configurationXml = PluginXml(plugin).toXmlString();
-        context.channel = dialog->getChannel();
+        input.setChannel(dialog->getChannel());
         
+        //!!! The dialog ought to be taking & returning transform
+        //objects and input objects and stuff rather than passing
+        //around all this misc stuff, but that's for tomorrow
+        //(whenever that may be)
+
         if (startFrame != 0 || duration != 0) {
             if (dialog->getSelectionOnly()) {
-                context.startFrame = startFrame;
-                context.duration = duration;
+                transform.setStartTime(RealTime::frame2RealTime
+                                       (startFrame, inputModel->getSampleRate()));
+                transform.setDuration(RealTime::frame2RealTime
+                                      (duration, inputModel->getSampleRate()));
             }
         }
 
-        dialog->getProcessingParameters(context.stepSize,
-                                        context.blockSize,
-                                        context.windowType);
+        size_t stepSize = 0, blockSize = 0;
+        WindowType windowType = HanningWindow;
 
-        context.makeConsistentWithPlugin(plugin);
+        dialog->getProcessingParameters(stepSize,
+                                        blockSize,
+                                        windowType);
+
+        transform.setStepSize(stepSize);
+        transform.setBlockSize(blockSize);
+        transform.setWindowType(windowType);
+
+        TransformFactory::getInstance()->
+            makeContextConsistentWithPlugin(transform, plugin);
+
+        configurationXml = PluginXml(plugin).toXmlString();
 
         delete dialog;
 
@@ -262,11 +297,14 @@
         }
     }
 
-    if (ok) m_lastConfigurations[identifier] = configurationXml;
+    if (ok) {
+        m_lastConfigurations[transform.getIdentifier()] = configurationXml;
+        input.setModel(inputModel);
+    }
 
-    return ok ? inputModel : 0;
+    return input;
 }
-
+/*!!!
 PluginTransformer::ExecutionContext
 ModelTransformerFactory::getDefaultContextForTransformer(TransformId identifier,
                                                 Model *inputModel)
@@ -289,43 +327,41 @@
 
     return context;
 }
-
+*/
 ModelTransformer *
-ModelTransformerFactory::createTransformer(TransformId identifier, Model *inputModel,
-                                  const PluginTransformer::ExecutionContext &context,
-                                  QString configurationXml)
+ModelTransformerFactory::createTransformer(const Transform &transform,
+                                           const ModelTransformer::Input &input)
 {
     ModelTransformer *transformer = 0;
 
-    QString id = identifier.section(':', 0, 2);
-    QString output = identifier.section(':', 3);
+    QString id = transform.getPluginIdentifier();
 
     if (FeatureExtractionPluginFactory::instanceFor(id)) {
-        transformer = new FeatureExtractionModelTransformer
-            (inputModel, id, context, configurationXml, output);
+
+        transformer =
+            new FeatureExtractionModelTransformer(input, transform);
+
     } else if (RealTimePluginFactory::instanceFor(id)) {
-        transformer = new RealTimeEffectModelTransformer
-            (inputModel, id, context, configurationXml,
-             TransformFactory::getInstance()->getTransformUnits(identifier),
-             output == "A" ? -1 : output.toInt());
+
+        transformer =
+            new RealTimeEffectModelTransformer(input, transform);
+
     } else {
         std::cerr << "ModelTransformerFactory::createTransformer: Unknown transform \""
-                  << identifier.toStdString() << "\"" << std::endl;
+                  << transform.getIdentifier().toStdString() << "\"" << std::endl;
         return transformer;
     }
 
-    if (transformer) transformer->setObjectName(identifier);
+    if (transformer) transformer->setObjectName(transform.getIdentifier());
     return transformer;
 }
 
 Model *
-ModelTransformerFactory::transform(TransformId identifier, Model *inputModel,
-                            const PluginTransformer::ExecutionContext &context,
-                            QString configurationXml)
+ModelTransformerFactory::transform(const Transform &transform,
+                                   const ModelTransformer::Input &input,
+                                   QString &message)
 {
-    ModelTransformer *t = createTransformer(identifier, inputModel, context,
-                                            configurationXml);
-
+    ModelTransformer *t = createTransformer(transform, input);
     if (!t) return 0;
 
     connect(t, SIGNAL(finished()), this, SLOT(transformerFinished()));
@@ -336,10 +372,10 @@
     Model *model = t->detachOutputModel();
 
     if (model) {
-        QString imn = inputModel->objectName();
+        QString imn = input.getModel()->objectName();
         QString trn =
             TransformFactory::getInstance()->getTransformFriendlyName
-            (identifier);
+            (transform.getIdentifier());
         if (imn != "") {
             if (trn != "") {
                 model->setObjectName(tr("%1: %2").arg(imn).arg(trn));
@@ -353,6 +389,8 @@
         t->wait();
     }
 
+    message = t->getMessage();
+
     return model;
 }
 
--- a/plugin/transform/ModelTransformerFactory.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/ModelTransformerFactory.h	Wed Feb 27 11:59:42 2008 +0000
@@ -21,8 +21,6 @@
 
 #include "ModelTransformer.h"
 
-#include "PluginTransformer.h"
-
 #include <map>
 #include <set>
 
@@ -40,27 +38,21 @@
     static ModelTransformerFactory *getInstance();
 
     /**
-     * Get a configuration XML string for the given transform (by
-     * asking the user, most likely).  Returns the selected input
-     * model if the transform is acceptable, 0 if the operation should
-     * be cancelled.  Audio callback play source may be used to
-     * audition effects plugins, if provided.
+     * Fill out the configuration for the given transform (by asking
+     * the user, most likely).  Returns the selected input model and
+     * channel if the transform is acceptable, or an input with a null
+     * model if the operation should be cancelled.  Audio callback
+     * play source may be used to audition effects plugins, if
+     * provided.
      */
-    Model *getConfigurationForTransformer(TransformId identifier,
-                                          const std::vector<Model *> &candidateInputModels,
-                                          PluginTransformer::ExecutionContext &context,
-                                          QString &configurationXml,
-                                          AudioCallbackPlaySource *source = 0,
-                                          size_t startFrame = 0,
-                                          size_t duration = 0);
-
-    /**
-     * Get the default execution context for the given transform
-     * and input model (if known).
-     */
-    PluginTransformer::ExecutionContext getDefaultContextForTransformer(TransformId identifier,
-                                                                        Model *inputModel = 0);
-
+    ModelTransformer::Input
+    getConfigurationForTransform(Transform &transform,
+                                 const std::vector<Model *> &candidateInputModels,
+                                 Model *defaultInputModel,
+                                 AudioCallbackPlaySource *source = 0,
+                                 size_t startFrame = 0,
+                                 size_t duration = 0);
+    
     /**
      * Return the output model resulting from applying the named
      * transform to the given input model.  The transform may still be
@@ -69,14 +61,15 @@
      *
      * If the transform is unknown or the input model is not an
      * appropriate type for the given transform, or if some other
-     * problem occurs, return 0.
+     * problem occurs, return 0.  Set message if there is any error or
+     * warning to report.
      * 
      * The returned model is owned by the caller and must be deleted
      * when no longer needed.
      */
-    Model *transform(TransformId identifier, Model *inputModel,
-                     const PluginTransformer::ExecutionContext &context,
-                     QString configurationXml = "");
+    Model *transform(const Transform &transform,
+                     const ModelTransformer::Input &input,
+                     QString &message);
 
 protected slots:
     void transformerFinished();
@@ -84,9 +77,8 @@
     void modelAboutToBeDeleted(Model *);
 
 protected:
-    ModelTransformer *createTransformer(TransformId identifier, Model *inputModel,
-                                        const PluginTransformer::ExecutionContext &context,
-                                        QString configurationXml);
+    ModelTransformer *createTransformer(const Transform &transform,
+                                        const ModelTransformer::Input &input);
 
     typedef std::map<TransformId, QString> TransformerConfigurationMap;
     TransformerConfigurationMap m_lastConfigurations;
--- a/plugin/transform/PluginTransformer.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,123 +0,0 @@
-/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
-
-/*
-    Sonic Visualiser
-    An audio file viewer and annotation editor.
-    Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006 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 "PluginTransformer.h"
-
-#include "vamp-sdk/PluginHostAdapter.h"
-#include "vamp-sdk/hostext/PluginWrapper.h"
-
-PluginTransformer::PluginTransformer(Model *inputModel,
-				 const ExecutionContext &context) :
-    ModelTransformer(inputModel),
-    m_context(context)
-{
-}
-
-PluginTransformer::ExecutionContext::ExecutionContext(int _c, size_t _bs) :
-    channel(_c),
-    domain(Vamp::Plugin::TimeDomain),
-    stepSize(_bs ? _bs : 1024),
-    blockSize(_bs ? _bs : 1024),
-    windowType(HanningWindow),
-    startFrame(0),
-    duration(0),
-    sampleRate(0),
-    updates(true)
-{
-}
-
-PluginTransformer::ExecutionContext::ExecutionContext(int _c, size_t _ss,
-                                                    size_t _bs, WindowType _wt) :
-    channel(_c),
-    domain(Vamp::Plugin::FrequencyDomain),
-    stepSize(_ss ? _ss : (_bs ? _bs / 2 : 512)),
-    blockSize(_bs ? _bs : 1024),
-    windowType(_wt),
-    startFrame(0),
-    duration(0),
-    sampleRate(0),
-    updates(true)
-{
-}
-
-PluginTransformer::ExecutionContext::ExecutionContext(int _c,
-                                                    const Vamp::PluginBase *_plugin) :
-    channel(_c),
-    domain(Vamp::Plugin::TimeDomain),
-    stepSize(0),
-    blockSize(0),
-    windowType(HanningWindow),
-    startFrame(0),
-    duration(0),
-    sampleRate(0)
-{
-    makeConsistentWithPlugin(_plugin);
-}
-
-bool
-PluginTransformer::ExecutionContext::operator==(const ExecutionContext &c)
-{
-    return (c.channel == channel &&
-            c.domain == domain &&
-            c.stepSize == stepSize &&
-            c.blockSize == blockSize &&
-            c.windowType == windowType &&
-            c.startFrame == startFrame &&
-            c.duration == duration &&
-            c.sampleRate == sampleRate);
-}
-
-void
-PluginTransformer::ExecutionContext::makeConsistentWithPlugin(const Vamp::PluginBase *_plugin)
-{
-    const Vamp::Plugin *vp = dynamic_cast<const Vamp::Plugin *>(_plugin);
-    if (!vp) {
-//        std::cerr << "makeConsistentWithPlugin: not a Vamp::Plugin" << std::endl;
-        vp = dynamic_cast<const Vamp::PluginHostAdapter *>(_plugin); //!!! why?
-}
-    if (!vp) {
-//        std::cerr << "makeConsistentWithPlugin: not a Vamp::PluginHostAdapter" << std::endl;
-        vp = dynamic_cast<const Vamp::HostExt::PluginWrapper *>(_plugin); //!!! no, I mean really why?
-    }
-    if (!vp) {
-//        std::cerr << "makeConsistentWithPlugin: not a Vamp::HostExt::PluginWrapper" << std::endl;
-    }
-
-    if (!vp) {
-        domain = Vamp::Plugin::TimeDomain;
-        if (!stepSize) {
-            if (!blockSize) blockSize = 1024;
-            stepSize = blockSize;
-        } else {
-            if (!blockSize) blockSize = stepSize;
-        }
-    } else {
-        domain = vp->getInputDomain();
-        if (!stepSize) stepSize = vp->getPreferredStepSize();
-        if (!blockSize) blockSize = vp->getPreferredBlockSize();
-        if (!blockSize) blockSize = 1024;
-        if (!stepSize) {
-            if (domain == Vamp::Plugin::FrequencyDomain) {
-//                std::cerr << "frequency domain, step = " << blockSize/2 << std::endl;
-                stepSize = blockSize/2;
-            } else {
-//                std::cerr << "time domain, step = " << blockSize/2 << std::endl;
-                stepSize = blockSize;
-            }
-        }
-    }
-}
-    
-
--- a/plugin/transform/PluginTransformer.h	Thu Nov 15 14:03:56 2007 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
-
-/*
-    Sonic Visualiser
-    An audio file viewer and annotation editor.
-    Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006 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 _PLUGIN_TRANSFORMER_H_
-#define _PLUGIN_TRANSFORMER_H_
-
-#include "ModelTransformer.h"
-
-#include "base/Window.h"
-
-#include "vamp-sdk/Plugin.h"
-
-//!!! should this just move back up to Transformer? It is after all used
-//directly in all sorts of generic places, like Document
-
-class PluginTransformer : public ModelTransformer
-{
-public:
-    class ExecutionContext {
-    public:
-        // Time domain:
-        ExecutionContext(int _c = -1, size_t _bs = 0);
-        
-        // Frequency domain:
-        ExecutionContext(int _c, size_t _ss, size_t _bs, WindowType _wt);
-
-        // From plugin defaults:
-        ExecutionContext(int _c, const Vamp::PluginBase *_plugin);
-
-        bool operator==(const ExecutionContext &);
-
-        void makeConsistentWithPlugin(const Vamp::PluginBase *_plugin);
-
-        int channel;
-        Vamp::Plugin::InputDomain domain;
-        size_t stepSize;
-        size_t blockSize;
-        WindowType windowType;
-        size_t startFrame;
-        size_t duration;    // 0 -> whole thing
-        float sampleRate;   // 0 -> model's rate
-        bool updates;
-    };
-
-protected:
-    PluginTransformer(Model *inputModel,
-                      const ExecutionContext &context);
-
-    ExecutionContext m_context;
-};
-
-#endif
--- a/plugin/transform/RealTimeEffectModelTransformer.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/RealTimeEffectModelTransformer.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -25,22 +25,23 @@
 #include "data/model/WritableWaveFileModel.h"
 #include "data/model/WaveFileModel.h"
 
+#include "TransformFactory.h"
+
 #include <iostream>
 
-RealTimeEffectModelTransformer::RealTimeEffectModelTransformer(Model *inputModel,
-                                                               QString pluginId,
-                                                               const ExecutionContext &context,
-                                                               QString configurationXml,
-                                                               QString units,
-                                                               int output) :
-    PluginTransformer(inputModel, context),
-    m_pluginId(pluginId),
-    m_configurationXml(configurationXml),
-    m_units(units),
-    m_plugin(0),
-    m_outputNo(output)
+RealTimeEffectModelTransformer::RealTimeEffectModelTransformer(Input in,
+                                                               const Transform &transform) :
+    ModelTransformer(in, transform),
+    m_plugin(0)
 {
-    if (!m_context.blockSize) m_context.blockSize = 1024;
+    m_units = TransformFactory::getInstance()->getTransformUnits
+        (transform.getIdentifier());
+    m_outputNo =
+        (transform.getOutput() == "A") ? -1 : transform.getOutput().toInt();
+
+    QString pluginId = transform.getPluginIdentifier();
+
+    if (!m_transform.getBlockSize()) m_transform.setBlockSize(1024);
 
 //    std::cerr << "RealTimeEffectModelTransformer::RealTimeEffectModelTransformer: plugin " << pluginId.toStdString() << ", output " << output << std::endl;
 
@@ -53,12 +54,12 @@
 	return;
     }
 
-    DenseTimeValueModel *input = getInput();
+    DenseTimeValueModel *input = getConformingInput();
     if (!input) return;
 
     m_plugin = factory->instantiatePlugin(pluginId, 0, 0,
-                                          m_input->getSampleRate(),
-                                          m_context.blockSize,
+                                          input->getSampleRate(),
+                                          m_transform.getBlockSize(),
                                           input->getChannelCount());
 
     if (!m_plugin) {
@@ -67,9 +68,7 @@
 	return;
     }
 
-    if (configurationXml != "") {
-        PluginXml(m_plugin).setParametersFromXml(configurationXml);
-    }
+    TransformFactory::getInstance()->setPluginParameters(m_transform, m_plugin);
 
     if (m_outputNo >= 0 &&
         m_outputNo >= int(m_plugin->getControlOutputCount())) {
@@ -92,9 +91,9 @@
     } else {
 	
         SparseTimeValueModel *model = new SparseTimeValueModel
-            (input->getSampleRate(), m_context.blockSize, 0.0, 0.0, false);
+            (input->getSampleRate(), m_transform.getBlockSize(), 0.0, 0.0, false);
 
-        if (units != "") model->setScaleUnits(units);
+        if (m_units != "") model->setScaleUnits(m_units);
 
         m_output = model;
     }
@@ -106,12 +105,12 @@
 }
 
 DenseTimeValueModel *
-RealTimeEffectModelTransformer::getInput()
+RealTimeEffectModelTransformer::getConformingInput()
 {
     DenseTimeValueModel *dtvm =
 	dynamic_cast<DenseTimeValueModel *>(getInputModel());
     if (!dtvm) {
-	std::cerr << "RealTimeEffectModelTransformer::getInput: WARNING: Input model is not conformable to DenseTimeValueModel" << std::endl;
+	std::cerr << "RealTimeEffectModelTransformer::getConformingInput: WARNING: Input model is not conformable to DenseTimeValueModel" << std::endl;
     }
     return dtvm;
 }
@@ -119,7 +118,7 @@
 void
 RealTimeEffectModelTransformer::run()
 {
-    DenseTimeValueModel *input = getInput();
+    DenseTimeValueModel *input = getConformingInput();
     if (!input) return;
 
     while (!input->isReady()) {
@@ -136,17 +135,23 @@
 
     size_t sampleRate = input->getSampleRate();
     size_t channelCount = input->getChannelCount();
-    if (!wwfm && m_context.channel != -1) channelCount = 1;
+    if (!wwfm && m_input.getChannel() != -1) channelCount = 1;
 
     long blockSize = m_plugin->getBufferSize();
 
     float **inbufs = m_plugin->getAudioInputBuffers();
 
-    long startFrame = m_input->getStartFrame();
-    long   endFrame = m_input->getEndFrame();
+    long startFrame = m_input.getModel()->getStartFrame();
+    long   endFrame = m_input.getModel()->getEndFrame();
     
-    long contextStart = m_context.startFrame;
-    long contextDuration = m_context.duration;
+    RealTime contextStartRT = m_transform.getStartTime();
+    RealTime contextDurationRT = m_transform.getDuration();
+
+    long contextStart =
+        RealTime::realTime2Frame(contextStartRT, sampleRate);
+
+    long contextDuration =
+        RealTime::realTime2Frame(contextDurationRT, sampleRate);
 
     if (contextStart == 0 || contextStart < startFrame) {
         contextStart = startFrame;
@@ -179,7 +184,7 @@
 	if (channelCount == 1) {
             if (inbufs && inbufs[0]) {
                 got = input->getData
-                    (m_context.channel, blockFrame, blockSize, inbufs[0]);
+                    (m_input.getChannel(), blockFrame, blockSize, inbufs[0]);
                 while (got < blockSize) {
                     inbufs[0][got++] = 0.0;
                 }          
@@ -190,14 +195,14 @@
                 }
             }
 	} else {
-	    for (size_t ch = 0; ch < channelCount; ++ch) {
-                if (inbufs && inbufs[ch]) {
-                    got = input->getData
-                        (ch, blockFrame, blockSize, inbufs[ch]);
-                    while (got < blockSize) {
-                        inbufs[ch][got++] = 0.0;
-                    }
+            got = input->getData(0, channelCount - 1,
+                                 blockFrame, blockSize,
+                                 inbufs);
+            while (got < blockSize) {
+                for (size_t ch = 0; ch < channelCount; ++ch) {
+                    inbufs[ch][got] = 0.0;
                 }
+                ++got;
 	    }
             for (size_t ch = channelCount; ch < m_plugin->getAudioInputCount(); ++ch) {
                 for (long i = 0; i < blockSize; ++i) {
--- a/plugin/transform/RealTimeEffectModelTransformer.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/RealTimeEffectModelTransformer.h	Wed Feb 27 11:59:42 2008 +0000
@@ -16,34 +16,27 @@
 #ifndef _REAL_TIME_PLUGIN_TRANSFORMER_H_
 #define _REAL_TIME_PLUGIN_TRANSFORMER_H_
 
-#include "PluginTransformer.h"
+#include "ModelTransformer.h"
 #include "plugin/RealTimePluginInstance.h"
 
 class DenseTimeValueModel;
 
-class RealTimeEffectModelTransformer : public PluginTransformer
+class RealTimeEffectModelTransformer : public ModelTransformer
 {
 public:
-    RealTimeEffectModelTransformer(Model *inputModel,
-			    QString plugin,
-                            const ExecutionContext &context,
-			    QString configurationXml = "",
-                            QString units = "",
-			    int output = -1); // -1 -> audio, 0+ -> data
+    RealTimeEffectModelTransformer(Input input,
+                                   const Transform &transform);
     virtual ~RealTimeEffectModelTransformer();
 
 protected:
     virtual void run();
 
-    QString m_pluginId;
-    QString m_configurationXml;
     QString m_units;
-
     RealTimePluginInstance *m_plugin;
     int m_outputNo;
 
     // just casts
-    DenseTimeValueModel *getInput();
+    DenseTimeValueModel *getConformingInput();
 };
 
 #endif
--- a/plugin/transform/Transform.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/Transform.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -19,6 +19,17 @@
 
 #include "plugin/FeatureExtractionPluginFactory.h"
 
+#include <QXmlAttributes>
+
+#include <QDomDocument>
+#include <QDomElement>
+#include <QDomNamedNodeMap>
+#include <QDomAttr>
+
+#include <QTextStream>
+
+#include <iostream>
+
 Transform::Transform() :
     m_stepSize(0),
     m_blockSize(0),
@@ -27,10 +38,102 @@
 {
 }
 
+Transform::Transform(QString xml) :
+    m_stepSize(0),
+    m_blockSize(0),
+    m_windowType(HanningWindow),
+    m_sampleRate(0)
+{
+    QDomDocument doc;
+    
+    QString error;
+    int errorLine;
+    int errorColumn;
+
+    if (!doc.setContent(xml, false, &error, &errorLine, &errorColumn)) {
+        std::cerr << "Transform::Transform: Error in parsing XML: "
+                  << error.toStdString() << " at line " << errorLine
+                  << ", column " << errorColumn << std::endl;
+        std::cerr << "Input follows:" << std::endl;
+        std::cerr << xml.toStdString() << std::endl;
+        std::cerr << "Input ends." << std::endl;
+        return;
+    }
+    
+    QDomElement transformElt = doc.firstChildElement("transform");
+    QDomNamedNodeMap attrNodes = transformElt.attributes();
+    QXmlAttributes attrs;
+
+    for (unsigned int i = 0; i < attrNodes.length(); ++i) {
+        QDomAttr attr = attrNodes.item(i).toAttr();
+        if (!attr.isNull()) attrs.append(attr.name(), "", "", attr.value());
+    }
+
+    setFromXmlAttributes(attrs);
+
+    for (QDomElement paramElt = transformElt.firstChildElement("parameter");
+         !paramElt.isNull();
+         paramElt = paramElt.nextSiblingElement("parameter")) {
+
+        QDomNamedNodeMap paramAttrs = paramElt.attributes();
+
+        QDomAttr nameAttr = paramAttrs.namedItem("name").toAttr();
+        if (nameAttr.isNull() || nameAttr.value() == "") continue;
+        
+        QDomAttr valueAttr = paramAttrs.namedItem("value").toAttr();
+        if (valueAttr.isNull() || valueAttr.value() == "") continue;
+
+        setParameter(nameAttr.value(), valueAttr.value().toFloat());
+    }
+
+    for (QDomElement configElt = transformElt.firstChildElement("configuration");
+         !configElt.isNull();
+         configElt = configElt.nextSiblingElement("configuration")) {
+
+        QDomNamedNodeMap configAttrs = configElt.attributes();
+
+        QDomAttr nameAttr = configAttrs.namedItem("name").toAttr();
+        if (nameAttr.isNull() || nameAttr.value() == "") continue;
+        
+        QDomAttr valueAttr = configAttrs.namedItem("value").toAttr();
+        if (valueAttr.isNull() || valueAttr.value() == "") continue;
+
+        setConfigurationValue(nameAttr.value(), valueAttr.value());
+    }
+}
+
 Transform::~Transform()
 {
 }
 
+bool
+Transform::operator==(const Transform &t)
+{
+    return 
+        m_id == t.m_id &&
+        m_parameters == t.m_parameters &&
+        m_configuration == t.m_configuration &&
+        m_program == t.m_program &&
+        m_stepSize == t.m_stepSize &&
+        m_blockSize == t.m_blockSize &&
+        m_windowType == t.m_windowType &&
+        m_startTime == t.m_startTime &&
+        m_duration == t.m_duration &&
+        m_sampleRate == t.m_sampleRate;
+}
+
+void
+Transform::setIdentifier(TransformId id)
+{
+    m_id = id;
+}
+
+TransformId
+Transform::getIdentifier() const
+{
+    return m_id;
+}
+
 QString
 Transform::createIdentifier(QString type, QString soName, QString label,
                             QString output)
@@ -74,7 +177,250 @@
 }
 
 void
-Transform::toXml(QTextStream &stream, QString indent, QString extraAttributes) const
+Transform::setPluginIdentifier(QString pluginIdentifier)
 {
+    m_id = pluginIdentifier + ':' + getOutput();
+}
+
+void
+Transform::setOutput(QString output)
+{
+    m_id = getPluginIdentifier() + ':' + output;
+}
+
+TransformId
+Transform::getIdentifierForPluginOutput(QString pluginIdentifier,
+                                        QString output)
+{
+    return pluginIdentifier + ':' + output;
+}
+
+const Transform::ParameterMap &
+Transform::getParameters() const
+{
+    return m_parameters;
+}
+
+void
+Transform::setParameters(const ParameterMap &pm)
+{
+    m_parameters = pm;
+}
+
+void
+Transform::setParameter(QString name, float value)
+{
+    std::cerr << "Transform::setParameter(" << name.toStdString()
+              << ") -> " << value << std::endl;
+    m_parameters[name] = value;
+}
+
+const Transform::ConfigurationMap &
+Transform::getConfiguration() const
+{
+    return m_configuration;
+}
+
+void
+Transform::setConfiguration(const ConfigurationMap &cm)
+{
+    m_configuration = cm;
+}
+
+void
+Transform::setConfigurationValue(QString name, QString value)
+{
+    std::cerr << "Transform::setConfigurationValue(" << name.toStdString()
+              << ") -> " << value.toStdString() << std::endl;
+    m_configuration[name] = value;
+}
+
+QString
+Transform::getPluginVersion() const
+{
+    return m_pluginVersion;
+}
+
+void
+Transform::setPluginVersion(QString version)
+{
+    m_pluginVersion = version;
+}
+
+QString
+Transform::getProgram() const
+{
+    return m_program;
+}
+
+void
+Transform::setProgram(QString program)
+{
+    m_program = program;
+}
+
     
+size_t
+Transform::getStepSize() const
+{
+    return m_stepSize;
 }
+
+void
+Transform::setStepSize(size_t s)
+{
+    m_stepSize = s;
+}
+    
+size_t
+Transform::getBlockSize() const
+{
+    return m_blockSize;
+}
+
+void
+Transform::setBlockSize(size_t s)
+{
+    m_blockSize = s;
+}
+
+WindowType
+Transform::getWindowType() const
+{
+    return m_windowType;
+}
+
+void
+Transform::setWindowType(WindowType type)
+{
+    m_windowType = type;
+}
+
+RealTime
+Transform::getStartTime() const
+{
+    return m_startTime;
+}
+
+void
+Transform::setStartTime(RealTime t)
+{
+    m_startTime = t;
+}
+
+RealTime
+Transform::getDuration() const
+{
+    return m_duration;
+}
+
+void
+Transform::setDuration(RealTime d)
+{
+    m_duration = d;
+}
+    
+float
+Transform::getSampleRate() const
+{
+    return m_sampleRate;
+}
+
+void
+Transform::setSampleRate(float rate)
+{
+    m_sampleRate = rate;
+}
+
+void
+Transform::toXml(QTextStream &out, QString indent, QString extraAttributes) const
+{
+    out << indent;
+
+    bool haveContent = true;
+    if (m_parameters.empty() && m_configuration.empty()) haveContent = false;
+
+    out << QString("<transform id=\"%1\" pluginVersion=\"%2\" program=\"%3\" stepSize=\"%4\" blockSize=\"%5\" windowType=\"%6\" startTime=\"%7\" duration=\"%8\" sampleRate=\"%9\"")
+        .arg(encodeEntities(m_id))
+        .arg(encodeEntities(m_pluginVersion))
+        .arg(encodeEntities(m_program))
+        .arg(m_stepSize)
+        .arg(m_blockSize)
+        .arg(encodeEntities(Window<float>::getNameForType(m_windowType).c_str()))
+        .arg(encodeEntities(m_startTime.toString().c_str()))
+        .arg(encodeEntities(m_duration.toString().c_str()))
+        .arg(m_sampleRate);
+
+    if (extraAttributes != "") {
+        out << " " << extraAttributes;
+    }
+
+    if (haveContent) {
+
+        out << ">\n";
+
+        for (ParameterMap::const_iterator i = m_parameters.begin();
+             i != m_parameters.end(); ++i) {
+            out << indent << "  "
+                << QString("<parameter name=\"%1\" value=\"%2\"/>\n")
+                .arg(encodeEntities(i->first))
+                .arg(i->second);
+        }
+        
+        for (ConfigurationMap::const_iterator i = m_configuration.begin();
+             i != m_configuration.end(); ++i) {
+            out << indent << "  "
+                << QString("<configuration name=\"%1\" value=\"%2\"/>\n")
+                .arg(encodeEntities(i->first))
+                .arg(encodeEntities(i->second));
+        }
+
+        out << indent << "</transform>\n";
+
+    } else {
+
+        out << "/>\n";
+    }
+}
+
+void
+Transform::setFromXmlAttributes(const QXmlAttributes &attrs)
+{
+    if (attrs.value("id") != "") {
+        setIdentifier(attrs.value("id"));
+    }
+
+    if (attrs.value("pluginVersion") != "") {
+        setPluginVersion(attrs.value("pluginVersion"));
+    }
+
+    if (attrs.value("program") != "") {
+        setProgram(attrs.value("program"));
+    }
+
+    if (attrs.value("stepSize") != "") {
+        setStepSize(attrs.value("stepSize").toInt());
+    }
+
+    if (attrs.value("blockSize") != "") {
+        setBlockSize(attrs.value("blockSize").toInt());
+    }
+
+    if (attrs.value("windowType") != "") {
+        setWindowType(Window<float>::getTypeForName
+                      (attrs.value("windowType").toStdString()));
+    }
+
+    if (attrs.value("startTime") != "") {
+        setStartTime(RealTime::fromString(attrs.value("startTime").toStdString()));
+    }
+
+    if (attrs.value("duration") != "") {
+        setStartTime(RealTime::fromString(attrs.value("duration").toStdString()));
+    }
+    
+    if (attrs.value("sampleRate") != "") {
+        setSampleRate(attrs.value("sampleRate").toFloat());
+    }
+}
+
--- a/plugin/transform/Transform.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/Transform.h	Wed Feb 27 11:59:42 2008 +0000
@@ -18,13 +18,14 @@
 
 #include "base/XmlExportable.h"
 #include "base/Window.h"
-
-#include <vamp-sdk/RealTime.h>
+#include "base/RealTime.h"
 
 #include <QString>
 
 typedef QString TransformId;
 
+class QXmlAttributes;
+
 namespace Vamp {
     class PluginBase;
 }
@@ -32,56 +33,101 @@
 class Transform : public XmlExportable
 {
 public:
+    /**
+     * Construct a new Transform with default data and no identifier.
+     * The Transform object will be meaningless until some data and an
+     * identifier have been set on it.
+     *
+     * To construct a Transform for use with a particular transform
+     * identifier, use TransformFactory::getDefaultTransformFor.
+     */
     Transform();
+
+    /**
+     * Construct a Transform by parsing the given XML data string.
+     * This is the inverse of toXml.
+     */
+    Transform(QString xml);
+
     virtual ~Transform();
 
-    void setIdentifier(TransformId id) { m_id = id; }
-    TransformId getIdentifier() const { return m_id; }
-    
-    void setPlugin(QString pluginIdentifier);
-    void setOutput(QString output);
+    /**
+     * Compare two Transforms.  They only compare equal if every data
+     * element matches.
+     */
+    bool operator==(const Transform &);
+
+    void setIdentifier(TransformId id);
+    TransformId getIdentifier() const;
 
     enum Type { FeatureExtraction, RealTimeEffect };
 
     Type getType() const;
     QString getPluginIdentifier() const;
     QString getOutput() const;
+    
+    void setPluginIdentifier(QString pluginIdentifier);
+    void setOutput(QString output);
+
+    // Turn a plugin ID and output name into a transform ID.  Note
+    // that our pluginIdentifier is the same thing as the Vamp SDK's
+    // PluginLoader::PluginKey.
+    static TransformId getIdentifierForPluginOutput(QString pluginIdentifier,
+                                                    QString output = "");
 
     typedef std::map<QString, float> ParameterMap;
     
-    ParameterMap getParameters() const { return m_parameters; }
-    void setParameters(const ParameterMap &pm) { m_parameters = pm; }
+    const ParameterMap &getParameters() const;
+    void setParameters(const ParameterMap &pm);
+    void setParameter(QString name, float value);
 
     typedef std::map<QString, QString> ConfigurationMap;
 
-    ConfigurationMap getConfiguration() const { return m_configuration; }
-    void setConfiguration(const ConfigurationMap &cm) { m_configuration = cm; }
+    const ConfigurationMap &getConfiguration() const;
+    void setConfiguration(const ConfigurationMap &cm);
+    void setConfigurationValue(QString name, QString value);
 
-    QString getProgram() const { return m_program; }
-    void setProgram(QString program) { m_program = program; }
+    QString getPluginVersion() const;
+    void setPluginVersion(QString version);
+
+    QString getProgram() const;
+    void setProgram(QString program);
     
-    size_t getStepSize() const { return m_stepSize; }
-    void setStepSize(size_t s) { m_stepSize = s; }
+    size_t getStepSize() const;
+    void setStepSize(size_t s);
     
-    size_t getBlockSize() const { return m_blockSize; }
-    void setBlockSize(size_t s) { m_blockSize = s; }
-
-    WindowType getWindowType() const { return m_windowType; }
-    void setWindowType(WindowType type) { m_windowType = type; }
-
-    Vamp::RealTime getStartTime() const { return m_startTime; }
-    void setStartTime(Vamp::RealTime t) { m_startTime = t; }
-
-    Vamp::RealTime getDuration() const { return m_duration; } // 0 -> all
-    void setDuration(Vamp::RealTime d) { m_duration = d; }
+    size_t getBlockSize() const;
+    void setBlockSize(size_t s);
     
-    float getSampleRate() const { return m_sampleRate; } // 0 -> as input
-    void setSampleRate(float rate) { m_sampleRate = rate; }
+    WindowType getWindowType() const;
+    void setWindowType(WindowType type);
+    
+    RealTime getStartTime() const;
+    void setStartTime(RealTime t);
+    
+    RealTime getDuration() const; // 0 -> all
+    void setDuration(RealTime d);
+    
+    float getSampleRate() const; // 0 -> as input
+    void setSampleRate(float rate);
 
     void toXml(QTextStream &stream, QString indent = "",
                QString extraAttributes = "") const;
 
-    static Transform fromXmlString(QString xml);
+    /**
+     * Set the main transform data from the given XML attributes.
+     * This does not set the parameters or configuration, which are
+     * exported to separate XML elements rather than attributes of the
+     * transform element.
+     * 
+     * Note that this only sets those attributes which are actually
+     * present in the argument.  Any attributes not defined in the
+     * attribute will remain unchanged in the Transform.  If your aim
+     * is to create a transform exactly matching the given attributes,
+     * ensure you start from an empty transform rather than one that
+     * has already been configured.
+     */
+    void setFromXmlAttributes(const QXmlAttributes &);
 
 protected:
     TransformId m_id; // pluginid:output, that is type:soname:label:output
@@ -95,12 +141,13 @@
 
     ParameterMap m_parameters;
     ConfigurationMap m_configuration;
+    QString m_pluginVersion;
     QString m_program;
     size_t m_stepSize;
     size_t m_blockSize;
     WindowType m_windowType;
-    Vamp::RealTime m_startTime;
-    Vamp::RealTime m_duration;
+    RealTime m_startTime;
+    RealTime m_duration;
     float m_sampleRate;
 };
 
--- a/plugin/transform/TransformDescription.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/TransformDescription.h	Wed Feb 27 11:59:42 2008 +0000
@@ -37,6 +37,10 @@
  * The friendly name is a shorter version of the name.
  *
  * The type is also intended to be user-readable, for use in menus.
+ *
+ * To obtain these objects, use
+ * TransformFactory::getAllTransformDescriptions and
+ * TransformFactory::getTransformDescription.
  */
 
 struct TransformDescription
@@ -62,7 +66,9 @@
     bool configurable;
     
     bool operator<(const TransformDescription &od) const {
-        return (name < od.name);
+        return
+            (name <  od.name) ||
+            (name == od.name && identifier < od.identifier);
     };
 };
 
--- a/plugin/transform/TransformFactory.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/TransformFactory.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -28,6 +28,7 @@
 #include <set>
 
 #include <QRegExp>
+#include <QTextStream>
 
 TransformFactory *
 TransformFactory::m_instance = new TransformFactory;
@@ -43,25 +44,39 @@
 }
 
 TransformList
-TransformFactory::getAllTransforms()
+TransformFactory::getAllTransformDescriptions()
 {
     if (m_transforms.empty()) populateTransforms();
 
     std::set<TransformDescription> dset;
     for (TransformDescriptionMap::const_iterator i = m_transforms.begin();
 	 i != m_transforms.end(); ++i) {
+//        std::cerr << "inserting transform into set: id = " << i->second.identifier.toStdString() << std::endl;
 	dset.insert(i->second);
     }
 
     TransformList list;
     for (std::set<TransformDescription>::const_iterator i = dset.begin();
 	 i != dset.end(); ++i) {
+//        std::cerr << "inserting transform into list: id = " << i->identifier.toStdString() << std::endl;
 	list.push_back(*i);
     }
 
     return list;
 }
 
+TransformDescription
+TransformFactory::getTransformDescription(TransformId id)
+{
+    if (m_transforms.empty()) populateTransforms();
+
+    if (m_transforms.find(id) == m_transforms.end()) {
+        return TransformDescription();
+    }
+
+    return m_transforms[id];
+}
+
 std::vector<QString>
 TransformFactory::getAllTransformTypes()
 {
@@ -217,7 +232,7 @@
 	}
 
 	Vamp::Plugin *plugin = 
-	    factory->instantiatePlugin(pluginId, 48000);
+	    factory->instantiatePlugin(pluginId, 44100);
 
 	if (!plugin) {
 	    std::cerr << "WARNING: TransformFactory::populateTransforms: Failed to instantiate plugin " << pluginId.toLocal8Bit().data() << std::endl;
@@ -273,7 +288,7 @@
             bool configurable = (!plugin->getPrograms().empty() ||
                                  !plugin->getParameterDescriptors().empty());
 
-//            std::cerr << "Feature extraction plugin transform: " << transformId.toStdString() << std::endl;
+//            std::cerr << "Feature extraction plugin transform: " << transformId.toStdString() << " friendly name: " << friendlyName.toStdString() << std::endl;
 
 	    transforms[transformId] = 
                 TransformDescription(tr("Analysis"),
@@ -423,6 +438,83 @@
     }
 }
 
+
+Transform
+TransformFactory::getDefaultTransformFor(TransformId id, size_t rate)
+{
+    Transform t;
+    t.setIdentifier(id);
+    if (rate != 0) t.setSampleRate(rate);
+
+    Vamp::PluginBase *plugin = instantiateDefaultPluginFor(id, rate);
+
+    if (plugin) {
+        t.setPluginVersion(QString("%1").arg(plugin->getPluginVersion()));
+        setParametersFromPlugin(t, plugin);
+        makeContextConsistentWithPlugin(t, plugin);
+        delete plugin;
+    }
+
+    return t;
+}
+
+Vamp::PluginBase *
+TransformFactory::instantiatePluginFor(const Transform &transform)
+{
+    Vamp::PluginBase *plugin = instantiateDefaultPluginFor
+        (transform.getIdentifier(), transform.getSampleRate());
+    if (plugin) {
+        setPluginParameters(transform, plugin);
+    }
+    return plugin;
+}
+
+Vamp::PluginBase *
+TransformFactory::instantiateDefaultPluginFor(TransformId identifier, size_t rate)
+{
+    Transform t;
+    t.setIdentifier(identifier);
+    if (rate == 0) rate = 44100;
+    QString pluginId = t.getPluginIdentifier();
+
+    Vamp::PluginBase *plugin = 0;
+
+    if (t.getType() == Transform::FeatureExtraction) {
+
+        FeatureExtractionPluginFactory *factory = 
+            FeatureExtractionPluginFactory::instanceFor(pluginId);
+
+        plugin = factory->instantiatePlugin(pluginId, rate);
+
+    } else {
+
+        RealTimePluginFactory *factory = 
+            RealTimePluginFactory::instanceFor(pluginId);
+            
+        plugin = factory->instantiatePlugin(pluginId, 0, 0, rate, 1024, 1);
+    }
+
+    return plugin;
+}
+
+Vamp::Plugin *
+TransformFactory::downcastVampPlugin(Vamp::PluginBase *plugin)
+{
+    Vamp::Plugin *vp = dynamic_cast<Vamp::Plugin *>(plugin);
+    if (!vp) {
+//        std::cerr << "makeConsistentWithPlugin: not a Vamp::Plugin" << std::endl;
+        vp = dynamic_cast<Vamp::PluginHostAdapter *>(plugin); //!!! why?
+}
+    if (!vp) {
+//        std::cerr << "makeConsistentWithPlugin: not a Vamp::PluginHostAdapter" << std::endl;
+        vp = dynamic_cast<Vamp::HostExt::PluginWrapper *>(plugin); //!!! no, I mean really why?
+    }
+    if (!vp) {
+//        std::cerr << "makeConsistentWithPlugin: not a Vamp::HostExt::PluginWrapper" << std::endl;
+    }
+    return vp;
+}
+
 bool
 TransformFactory::haveTransform(TransformId identifier)
 {
@@ -454,6 +546,28 @@
     } else return "";
 }
 
+Vamp::Plugin::InputDomain
+TransformFactory::getTransformInputDomain(TransformId identifier)
+{
+    Transform transform;
+    transform.setIdentifier(identifier);
+
+    if (transform.getType() != Transform::FeatureExtraction) {
+        return Vamp::Plugin::TimeDomain;
+    }
+
+    Vamp::Plugin *plugin =
+        downcastVampPlugin(instantiateDefaultPluginFor(identifier, 0));
+
+    if (plugin) {
+        Vamp::Plugin::InputDomain d = plugin->getInputDomain();
+        delete plugin;
+        return d;
+    }
+
+    return Vamp::Plugin::TimeDomain;
+}
+
 bool
 TransformFactory::isTransformConfigurable(TransformId identifier)
 {
@@ -472,7 +586,7 @@
 
         Vamp::Plugin *plugin = 
             FeatureExtractionPluginFactory::instanceFor(id)->
-            instantiatePlugin(id, 48000);
+            instantiatePlugin(id, 44100);
         if (!plugin) return false;
 
         min = plugin->getMinChannelCount();
@@ -483,6 +597,8 @@
 
     } else if (RealTimePluginFactory::instanceFor(id)) {
 
+        // don't need to instantiate
+
         const RealTimePluginDescriptor *descriptor = 
             RealTimePluginFactory::instanceFor(id)->
             getPluginDescriptor(id);
@@ -503,6 +619,10 @@
 {
     Transform::ParameterMap pmap;
 
+    //!!! record plugin & API version
+
+    //!!! check that this is the right plugin!
+
     Vamp::PluginBase::ParameterList parameters =
         plugin->getParameterDescriptors();
 
@@ -539,21 +659,48 @@
 }
 
 void
+TransformFactory::setPluginParameters(const Transform &transform,
+                                      Vamp::PluginBase *plugin)
+{
+    //!!! check plugin & API version (see e.g. PluginXml::setParameters)
+
+    //!!! check that this is the right plugin!
+
+    RealTimePluginInstance *rtpi =
+        dynamic_cast<RealTimePluginInstance *>(plugin);
+
+    if (rtpi) {
+        const Transform::ConfigurationMap &cmap = transform.getConfiguration();
+        for (Transform::ConfigurationMap::const_iterator i = cmap.begin();
+             i != cmap.end(); ++i) {
+            rtpi->configure(i->first.toStdString(), i->second.toStdString());
+        }
+    }
+
+    if (transform.getProgram() != "") {
+        plugin->selectProgram(transform.getProgram().toStdString());
+    }
+
+    const Transform::ParameterMap &pmap = transform.getParameters();
+
+    Vamp::PluginBase::ParameterList parameters =
+        plugin->getParameterDescriptors();
+
+    for (Vamp::PluginBase::ParameterList::const_iterator i = parameters.begin();
+         i != parameters.end(); ++i) {
+        QString key = i->identifier.c_str();
+        Transform::ParameterMap::const_iterator pmi = pmap.find(key);
+        if (pmi != pmap.end()) {
+            plugin->setParameter(i->identifier, pmi->second);
+        }
+    }
+}
+
+void
 TransformFactory::makeContextConsistentWithPlugin(Transform &transform,
                                                   Vamp::PluginBase *plugin)
 {
-    const Vamp::Plugin *vp = dynamic_cast<const Vamp::Plugin *>(plugin);
-    if (!vp) {
-//        std::cerr << "makeConsistentWithPlugin: not a Vamp::Plugin" << std::endl;
-        vp = dynamic_cast<const Vamp::PluginHostAdapter *>(plugin); //!!! why?
-}
-    if (!vp) {
-//        std::cerr << "makeConsistentWithPlugin: not a Vamp::PluginHostAdapter" << std::endl;
-        vp = dynamic_cast<const Vamp::HostExt::PluginWrapper *>(plugin); //!!! no, I mean really why?
-    }
-    if (!vp) {
-//        std::cerr << "makeConsistentWithPlugin: not a Vamp::HostExt::PluginWrapper" << std::endl;
-    }
+    const Vamp::Plugin *vp = downcastVampPlugin(plugin);
 
     if (!vp) {
         // time domain input for real-time effects plugin
@@ -586,49 +733,44 @@
     }
 }
 
-Transform
-TransformFactory::getDefaultTransformFor(TransformId id, size_t rate)
+QString
+TransformFactory::getPluginConfigurationXml(const Transform &t)
 {
-    Transform t;
-    t.setIdentifier(id);
-    
-    if (rate == 0) {
-        rate = 44100;
-    } else {
-        t.setSampleRate(rate);
+    QString xml;
+
+    Vamp::PluginBase *plugin = instantiateDefaultPluginFor
+        (t.getIdentifier(), 0);
+    if (!plugin) {
+        std::cerr << "TransformFactory::getPluginConfigurationXml: "
+                  << "Unable to instantiate plugin for transform \""
+                  << t.getIdentifier().toStdString() << "\"" << std::endl;
+        return xml;
     }
 
-    QString pluginId = t.getPluginIdentifier();
+    setPluginParameters(t, plugin);
 
-    if (t.getType() == Transform::FeatureExtraction) {
+    QTextStream out(&xml);
+    PluginXml(plugin).toXml(out);
+    delete plugin;
 
-        FeatureExtractionPluginFactory *factory = 
-            FeatureExtractionPluginFactory::instanceFor(pluginId);
+    return xml;
+}
 
-        Vamp::Plugin *plugin = factory->instantiatePlugin
-            (pluginId, rate);
-
-        if (plugin) {
-            setParametersFromPlugin(t, plugin);
-            makeContextConsistentWithPlugin(t, plugin);
-            delete plugin;
-        }
-
-    } else {
-
-        RealTimePluginFactory *factory = 
-            RealTimePluginFactory::instanceFor(pluginId);
-            
-        RealTimePluginInstance *plugin = factory->instantiatePlugin
-            (pluginId, 0, 0, rate, 1024, 1);
-        
-        if (plugin) {
-            setParametersFromPlugin(t, plugin);
-            makeContextConsistentWithPlugin(t, plugin);
-            delete plugin;
-        }
+void
+TransformFactory::setParametersFromPluginConfigurationXml(Transform &t,
+                                                          QString xml)
+{
+    Vamp::PluginBase *plugin = instantiateDefaultPluginFor
+        (t.getIdentifier(), 0);
+    if (!plugin) {
+        std::cerr << "TransformFactory::setParametersFromPluginConfigurationXml: "
+                  << "Unable to instantiate plugin for transform \""
+                  << t.getIdentifier().toStdString() << "\"" << std::endl;
+        return;
     }
 
-    return t;
+    PluginXml(plugin).setParametersFromXml(xml);
+    setParametersFromPlugin(t, plugin);
+    delete plugin;
 }
 
--- a/plugin/transform/TransformFactory.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/plugin/transform/TransformFactory.h	Wed Feb 27 11:59:42 2008 +0000
@@ -18,11 +18,11 @@
 
 #include "TransformDescription.h"
 
+#include <vamp-sdk/Plugin.h>
+
 #include <map>
 #include <set>
 
-namespace Vamp { class PluginBase; }
-
 class TransformFactory : public QObject
 {
     Q_OBJECT
@@ -32,10 +32,10 @@
 
     static TransformFactory *getInstance();
 
-    TransformList getAllTransforms();
+    TransformList getAllTransformDescriptions();
+    TransformDescription getTransformDescription(TransformId id);
 
     std::vector<QString> getAllTransformTypes();
-
     std::vector<QString> getTransformCategories(QString transformType);
     std::vector<QString> getTransformMakers(QString transformType);
 
@@ -45,6 +45,13 @@
     bool haveTransform(TransformId identifier);
 
     /**
+     * A single transform ID can lead to many possible Transforms,
+     * with different parameters and execution context settings.
+     * Return the default one for the given transform.
+     */
+    Transform getDefaultTransformFor(TransformId identifier, size_t rate = 0);
+
+    /**
      * Full name of a transform, suitable for putting on a menu.
      */
     QString getTransformName(TransformId identifier);
@@ -57,6 +64,8 @@
 
     QString getTransformUnits(TransformId identifier);
 
+    Vamp::Plugin::InputDomain getTransformInputDomain(TransformId identifier);
+
     /**
      * Return true if the transform has any configurable parameters,
      * i.e. if getConfigurationForTransform can ever return a non-trivial
@@ -74,6 +83,35 @@
                                   int &minChannels, int &maxChannels);
 
     /**
+     * Load an appropriate plugin for the given transform and set the
+     * parameters, program and configuration strings on that plugin
+     * from the Transform object.
+     *
+     * Note that this requires that the transform has a meaningful
+     * sample rate set, as that is used as the rate for the plugin.  A
+     * Transform can legitimately have rate set at zero (= "use the
+     * rate of the input source"), so the caller will need to test for
+     * this case.
+     *
+     * Returns the plugin thus loaded.  This will be a
+     * Vamp::PluginBase, but not necessarily a Vamp::Plugin (only if
+     * the transform was a feature-extraction type -- call
+     * downcastVampPlugin if you only want Vamp::Plugins).  Returns
+     * NULL if no suitable plugin was available.
+     *
+     * The returned plugin is owned by the caller, and should be
+     * deleted (using "delete") when no longer needed.
+     */
+    Vamp::PluginBase *instantiatePluginFor(const Transform &transform);
+
+    /**
+     * Convert a Vamp::PluginBase to a Vamp::Plugin, if it is one.
+     * Return NULL otherwise.  This ill-fitting convenience function
+     * is really just a dynamic_cast wrapper.
+     */
+    Vamp::Plugin *downcastVampPlugin(Vamp::PluginBase *);
+
+    /**
      * Set the plugin parameters, program and configuration strings on
      * the given Transform object from the given plugin instance.
      * Note that no check is made whether the plugin is actually the
@@ -82,6 +120,12 @@
     void setParametersFromPlugin(Transform &transform, Vamp::PluginBase *plugin);
 
     /**
+     * Set the parameters, program and configuration strings on the
+     * given plugin from the given Transform object.
+     */
+    void setPluginParameters(const Transform &transform, Vamp::PluginBase *plugin);
+    
+    /**
      * If the given Transform object has no processing step and block
      * sizes set, set them to appropriate defaults for the given
      * plugin.
@@ -89,11 +133,27 @@
     void makeContextConsistentWithPlugin(Transform &transform, Vamp::PluginBase *plugin); 
 
     /**
-     * A single transform ID can lead to many possible Transforms,
-     * with different parameters and execution context settings.
-     * Return the default one for the given transform.
+     * Retrieve a <plugin ... /> XML fragment that describes the
+     * plugin parameters, program and configuration data for the given
+     * transform.
+     *
+     * This function is provided for backward compatibility only.  Use
+     * Transform::toXml where compatibility with PluginXml
+     * descriptions of transforms is not required.
      */
-    Transform getDefaultTransformFor(TransformId identifier, size_t rate = 0);
+    QString getPluginConfigurationXml(const Transform &transform);
+
+    /**
+     * Set the plugin parameters, program and configuration strings on
+     * the given Transform object from the given <plugin ... /> XML
+     * fragment.
+     *
+     * This function is provided for backward compatibility only.  Use
+     * Transform(QString) where compatibility with PluginXml
+     * descriptions of transforms is not required.
+     */
+    void setParametersFromPluginConfigurationXml(Transform &transform,
+                                                 QString xml);
 
 protected:
     typedef std::map<TransformId, TransformDescription> TransformDescriptionMap;
@@ -103,6 +163,8 @@
     void populateFeatureExtractionPlugins(TransformDescriptionMap &);
     void populateRealTimePlugins(TransformDescriptionMap &);
 
+    Vamp::PluginBase *instantiateDefaultPluginFor(TransformId id, size_t rate);
+
     static TransformFactory *m_instance;
 };
 
--- a/system/Init.cpp	Thu Nov 15 14:03:56 2007 +0000
+++ b/system/Init.cpp	Wed Feb 27 11:59:42 2008 +0000
@@ -79,9 +79,9 @@
 
 #ifdef Q_WS_WIN32
     redirectStderr();
-    QFont fn = qApp->font();
-    fn.setFamily("Tahoma");
-    qApp->setFont(fn);
+//    QFont fn = qApp->font();
+//    fn.setFamily("Tahoma");
+//    qApp->setFont(fn);
 #else
 #ifdef Q_WS_X11
 //    QFont fn = qApp->font();
--- a/system/System.h	Thu Nov 15 14:03:56 2007 +0000
+++ b/system/System.h	Wed Feb 27 11:59:42 2008 +0000
@@ -25,6 +25,7 @@
 #define MLOCK(a,b)   1
 #define MUNLOCK(a,b) 1
 #define MUNLOCK_SAMPLEBLOCK(a) 1
+#define MUNLOCKALL() 1
 
 #define DLOPEN(a,b)  LoadLibrary((a).toStdWString().c_str())
 #define DLSYM(a,b)   GetProcAddress((HINSTANCE)(a),(b))
@@ -82,6 +83,8 @@
 #define DEFAULT_LADSPA_PATH "$HOME/Library/Audio/Plug-Ins/LADSPA:/Library/Audio/Plug-Ins/LADSPA"
 #define DEFAULT_DSSI_PATH   "$HOME/Library/Audio/Plug-Ins/DSSI:/Library/Audio/Plug-Ins/DSSI"
 
+#define MUNLOCKALL() 1
+
 #else 
 
 #define PLUGIN_GLOB  "*.so"
@@ -90,6 +93,8 @@
 #define DEFAULT_LADSPA_PATH "$HOME/ladspa:$HOME/.ladspa:/usr/local/lib/ladspa:/usr/lib/ladspa"
 #define DEFAULT_DSSI_PATH "$HOME/dssi:$HOME/.dssi:/usr/local/lib/dssi:/usr/lib/dssi"
 
+#define MUNLOCKALL() ::munlockall()
+
 #endif /* __APPLE__ */
 
 #endif /* ! _WIN32 */