changeset 904:aa2257796931 tony_integration

Merge
author Chris Cannam
date Wed, 07 May 2014 15:14:17 +0100
parents 1b05ba6af360 (diff) 16c48a3db2a7 (current diff)
children 166e3fc1e962
files
diffstat 39 files changed, 1435 insertions(+), 455 deletions(-) [+]
line wrap: on
line diff
--- a/base/Clipboard.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/Clipboard.cpp	Wed May 07 15:14:17 2014 +0100
@@ -123,6 +123,15 @@
     return m_frame;
 }
 
+Clipboard::Point
+Clipboard::Point::withFrame(long frame) const
+{
+    Point p(*this);
+    p.m_haveFrame = true;
+    p.m_frame = frame;
+    return p;
+}
+
 bool
 Clipboard::Point::haveValue() const
 {
@@ -135,6 +144,15 @@
     return m_value;
 }
 
+Clipboard::Point
+Clipboard::Point::withValue(float value) const
+{
+    Point p(*this);
+    p.m_haveValue = true;
+    p.m_value = value;
+    return p;
+}
+
 bool
 Clipboard::Point::haveDuration() const
 {
@@ -147,6 +165,15 @@
     return m_duration;
 }
 
+Clipboard::Point
+Clipboard::Point::withDuration(size_t duration) const
+{
+    Point p(*this);
+    p.m_haveDuration = true;
+    p.m_duration = duration;
+    return p;
+}
+
 bool
 Clipboard::Point::haveLabel() const
 {
@@ -159,6 +186,15 @@
     return m_label;
 }
 
+Clipboard::Point
+Clipboard::Point::withLabel(QString label) const
+{
+    Point p(*this);
+    p.m_haveLabel = true;
+    p.m_label = label;
+    return p;
+}
+
 bool
 Clipboard::Point::haveLevel() const
 {
@@ -171,6 +207,15 @@
     return m_level;
 }
 
+Clipboard::Point
+Clipboard::Point::withLevel(float level) const
+{
+    Point p(*this);
+    p.m_haveLevel = true;
+    p.m_level = level;
+    return p;
+}
+
 bool
 Clipboard::Point::haveReferenceFrame() const
 {
--- a/base/Clipboard.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/Clipboard.h	Wed May 07 15:14:17 2014 +0100
@@ -34,18 +34,23 @@
 
         bool haveFrame() const;
         long getFrame() const;
+        Point withFrame(long frame) const;
 
         bool haveValue() const;
         float getValue() const;
+        Point withValue(float value) const;
         
         bool haveDuration() const;
         size_t getDuration() const;
+        Point withDuration(size_t duration) const;
         
         bool haveLabel() const;
         QString getLabel() const;
+        Point withLabel(QString label) const;
 
         bool haveLevel() const;
         float getLevel() const;
+        Point withLevel(float level) const;
 
         bool haveReferenceFrame() const;
         bool referenceFrameDiffers() const; // from point frame
--- a/base/Debug.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/Debug.h	Wed May 07 15:14:17 2014 +0100
@@ -19,6 +19,8 @@
 #include <QDebug>
 #include <QTextStream>
 
+#include <vamp-hostsdk/RealTime.h>
+
 #include <string>
 #include <iostream>
 
@@ -39,6 +41,11 @@
 
 #define SVDEBUG getSVDebug()
 
+inline QDebug &operator<<(QDebug &d, const Vamp::RealTime &rt) {
+    d << rt.toString();
+    return d;
+}
+
 template <typename T>
 inline QDebug &operator<<(QDebug &d, const T &t) {
     QString s;
--- a/base/Pitch.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/Pitch.cpp	Wed May 07 15:14:17 2014 +0100
@@ -101,7 +101,13 @@
 		     float centsOffset,
 		     bool useFlats)
 {
-    int octave = -2;
+    int baseOctave = Preferences::getInstance()->getOctaveOfLowestMIDINote();
+    int octave = baseOctave;
+
+    // Note, this only gets the right octave number at octave
+    // boundaries because Cb is enharmonic with B (not B#) and B# is
+    // enharmonic with C (not Cb). So neither B# nor Cb will be
+    // spelled from a MIDI pitch + flats flag in isolation.
 
     if (midiPitch < 0) {
 	while (midiPitch < 0) {
@@ -109,7 +115,7 @@
 	    --octave;
 	}
     } else {
-	octave = midiPitch / 12 - 2;
+	octave = midiPitch / 12 + baseOctave;
     }
 
     QString plain = (useFlats ? flatNotes : notes)[midiPitch % 12].arg(octave);
--- a/base/Pitch.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/Pitch.h	Wed May 07 15:14:17 2014 +0100
@@ -71,9 +71,10 @@
 
     /**
      * 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
-     * cents.
+     * cents offset.  This consists of the note name, octave number,
+     * and optional cents. The octave numbering system is based on the
+     * application preferences (default is C4 = middle C, though in
+     * previous SV releases that was C3).
      *
      * For example, "A#3" (A# in octave 3) or "C2-12c" (C in octave 2,
      * minus 12 cents).
--- a/base/PlayParameterRepository.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/PlayParameterRepository.cpp	Wed May 07 15:14:17 2014 +0100
@@ -35,36 +35,29 @@
 void
 PlayParameterRepository::addPlayable(const Playable *playable)
 {
-//    cerr << "PlayParameterRepository:addPlayable " << playable <<  endl;
+    cerr << "PlayParameterRepository:addPlayable playable = " << playable <<  endl;
 
     if (!getPlayParameters(playable)) {
 
 	// Give all playables the same type of play parameters for the
 	// moment
 
-//	    cerr << "PlayParameterRepository: Adding play parameters for " << playable << endl;
+        cerr << "PlayParameterRepository:addPlayable: Adding play parameters for " << playable << endl;
 
         PlayParameters *params = new PlayParameters;
         m_playParameters[playable] = params;
 
-        params->setPlayPluginId
-            (playable->getDefaultPlayPluginId());
-        
-        params->setPlayPluginConfiguration
-            (playable->getDefaultPlayPluginConfiguration());
+        params->setPlayClipId
+            (playable->getDefaultPlayClipId());
         
         connect(params, SIGNAL(playParametersChanged()),
                 this, SLOT(playParametersChanged()));
         
-        connect(params, SIGNAL(playPluginIdChanged(QString)),
-                this, SLOT(playPluginIdChanged(QString)));
+        connect(params, SIGNAL(playClipIdChanged(QString)),
+                this, SLOT(playClipIdChanged(QString)));
 
-        connect(params, SIGNAL(playPluginConfigurationChanged(QString)),
-                this, SLOT(playPluginConfigurationChanged(QString)));
-        
-//            cerr << "Connected play parameters " << params << " for playable "
-//                      << playable << " to this " << this << endl;
-
+        cerr << "Connected play parameters " << params << " for playable "
+                     << playable << " to this " << this << endl;
     }
 }    
 
@@ -108,27 +101,13 @@
 }
 
 void
-PlayParameterRepository::playPluginIdChanged(QString id)
+PlayParameterRepository::playClipIdChanged(QString id)
 {
     PlayParameters *params = dynamic_cast<PlayParameters *>(sender());
     for (PlayableParameterMap::iterator i = m_playParameters.begin();
          i != m_playParameters.end(); ++i) {
         if (i->second == params) {
-            emit playPluginIdChanged(i->first, id);
-            return;
-        }
-    }
-}
-
-void
-PlayParameterRepository::playPluginConfigurationChanged(QString config)
-{
-    PlayParameters *params = dynamic_cast<PlayParameters *>(sender());
-//    SVDEBUG << "PlayParameterRepository::playPluginConfigurationChanged" << endl;
-    for (PlayableParameterMap::iterator i = m_playParameters.begin();
-         i != m_playParameters.end(); ++i) {
-        if (i->second == params) {
-            emit playPluginConfigurationChanged(i->first, config);
+            emit playClipIdChanged(i->first, id);
             return;
         }
     }
@@ -176,15 +155,9 @@
 }
 
 void
-PlayParameterRepository::EditCommand::setPlayPluginId(QString id)
+PlayParameterRepository::EditCommand::setPlayClipId(QString id)
 {
-    m_to.setPlayPluginId(id);
-}
-
-void
-PlayParameterRepository::EditCommand::setPlayPluginConfiguration(QString conf)
-{
-    m_to.setPlayPluginConfiguration(conf);
+    m_to.setPlayClipId(id);
 }
 
 void
@@ -222,13 +195,8 @@
         if (++changed > 1) return multiname;
     }
 
-    if (m_to.getPlayPluginId() != m_from.getPlayPluginId()) {
-        name = tr("Change Playback Plugin");
-        if (++changed > 1) return multiname;
-    }
-
-    if (m_to.getPlayPluginConfiguration() != m_from.getPlayPluginConfiguration()) {
-        name = tr("Configure Playback Plugin");
+    if (m_to.getPlayClipId() != m_from.getPlayClipId()) {
+        name = tr("Change Playback Sample");
         if (++changed > 1) return multiname;
     }
 
--- a/base/PlayParameterRepository.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/PlayParameterRepository.h	Wed May 07 15:14:17 2014 +0100
@@ -51,8 +51,7 @@
         void setPlayAudible(bool);
         void setPlayPan(float);
         void setPlayGain(float);
-        void setPlayPluginId(QString);
-        void setPlayPluginConfiguration(QString);
+        void setPlayClipId(QString);
         void execute();
         void unexecute();
         QString getName() const;
@@ -65,13 +64,11 @@
 
 signals:
     void playParametersChanged(PlayParameters *);
-    void playPluginIdChanged(const Playable *, QString);
-    void playPluginConfigurationChanged(const Playable *, QString);
+    void playClipIdChanged(const Playable *, QString);
 
 protected slots:
     void playParametersChanged();
-    void playPluginIdChanged(QString);
-    void playPluginConfigurationChanged(QString);
+    void playClipIdChanged(QString);
 
 protected:
     typedef std::map<const Playable *, PlayParameters *> PlayableParameterMap;
--- a/base/PlayParameters.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/PlayParameters.cpp	Wed May 07 15:14:17 2014 +0100
@@ -43,15 +43,9 @@
         changed = true;
     }
 
-    if (m_playPluginId != pp->getPlayPluginId()) {
-        m_playPluginId = pp->getPlayPluginId();
-        emit playPluginIdChanged(m_playPluginId);
-        changed = true;
-    }
-    
-    if (m_playPluginConfiguration != pp->getPlayPluginConfiguration()) {
-        m_playPluginConfiguration = pp->getPlayPluginConfiguration();
-        emit playPluginConfigurationChanged(m_playPluginConfiguration);
+    if (m_playClipId != pp->getPlayClipId()) {
+        m_playClipId = pp->getPlayClipId();
+        emit playClipIdChanged(m_playClipId);
         changed = true;
     }
 
@@ -64,18 +58,24 @@
                       QString extraAttributes) const
 {
     stream << indent;
-    stream << QString("<playparameters mute=\"%1\" pan=\"%2\" gain=\"%3\" pluginId=\"%4\" %6")
+    stream << QString("<playparameters mute=\"%1\" pan=\"%2\" gain=\"%3\" clipId=\"%4\" %6")
         .arg(m_playMuted ? "true" : "false")
         .arg(m_playPan)
         .arg(m_playGain)
-        .arg(m_playPluginId)
+        .arg(m_playClipId)
         .arg(extraAttributes);
-    if (m_playPluginConfiguration != "") {
-        stream << ">\n  " << indent << m_playPluginConfiguration
-               << "\n" << indent << "</playparameters>\n";
-    } else {
-        stream << "/>\n";
+
+    stream << ">\n";
+
+    if (m_playClipId != "") {
+        // for backward compatibility
+        stream << indent << "  ";
+        stream << QString("<plugin identifier=\"%1\" program=\"%2\"/>\n")
+            .arg("sample_player")
+            .arg(m_playClipId);
     }
+
+    stream << indent << "</playparameters>\n";
 }
 
 void
@@ -118,24 +118,11 @@
 }
 
 void
-PlayParameters::setPlayPluginId(QString id)
+PlayParameters::setPlayClipId(QString id)
 {
-    if (m_playPluginId != id) {
-        m_playPluginId = id;
-        emit playPluginIdChanged(id);
+    if (m_playClipId != id) {
+        m_playClipId = id;
+        emit playClipIdChanged(id);
         emit playParametersChanged();
     }
 }
-
-void
-PlayParameters::setPlayPluginConfiguration(QString configuration)
-{
-    if (m_playPluginConfiguration != configuration) {
-        m_playPluginConfiguration = configuration;
-//        cerr << "PlayParameters(" << this << "): setPlayPluginConfiguration to \"" << configuration << "\"" << endl;
-        emit playPluginConfigurationChanged(configuration);
-        emit playParametersChanged();
-    }
-}
-
-
--- a/base/PlayParameters.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/PlayParameters.h	Wed May 07 15:14:17 2014 +0100
@@ -32,8 +32,7 @@
     virtual float getPlayPan() const { return m_playPan; } // -1.0 -> 1.0
     virtual float getPlayGain() const { return m_playGain; }
 
-    virtual QString getPlayPluginId() const { return m_playPluginId; } 
-    virtual QString getPlayPluginConfiguration() const { return m_playPluginConfiguration; }
+    virtual QString getPlayClipId() const { return m_playClipId; }
 
     virtual void copyFrom(const PlayParameters *);
 
@@ -46,8 +45,7 @@
     virtual void setPlayAudible(bool nonMuted);
     virtual void setPlayPan(float pan);
     virtual void setPlayGain(float gain);
-    virtual void setPlayPluginId(QString id);
-    virtual void setPlayPluginConfiguration(QString configuration);
+    virtual void setPlayClipId(QString id);
 
 signals:
     void playParametersChanged();
@@ -55,15 +53,13 @@
     void playAudibleChanged(bool);
     void playPanChanged(float);
     void playGainChanged(float);
-    void playPluginIdChanged(QString);
-    void playPluginConfigurationChanged(QString);
+    void playClipIdChanged(QString);
 
 protected:
     bool m_playMuted;
     float m_playPan;
     float m_playGain;
-    QString m_playPluginId;
-    QString m_playPluginConfiguration;
+    QString m_playClipId;
 
 private:
     PlayParameters(const PlayParameters &);
--- a/base/Playable.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/Playable.h	Wed May 07 15:14:17 2014 +0100
@@ -24,8 +24,7 @@
     virtual ~Playable() { }
     
     virtual bool canPlay() const { return false; }
-    virtual QString getDefaultPlayPluginId() const { return ""; }
-    virtual QString getDefaultPlayPluginConfiguration() const { return ""; }
+    virtual QString getDefaultPlayClipId() const { return ""; }
 };
 
 #endif
--- a/base/Preferences.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/Preferences.cpp	Wed May 07 15:14:17 2014 +0100
@@ -47,6 +47,7 @@
     m_viewFontSize(10),
     m_backgroundMode(BackgroundFromTheme),
     m_timeToTextMode(TimeToTextMs),
+    m_octave(4),
     m_showSplash(true)
 {
     QSettings settings;
@@ -66,6 +67,7 @@
         (settings.value("background-mode", int(BackgroundFromTheme)).toInt());
     m_timeToTextMode = TimeToTextMode
         (settings.value("time-to-text-mode", int(TimeToTextMs)).toInt());
+    m_octave = (settings.value("octave-of-middle-c", 4)).toInt();
     m_viewFontSize = settings.value("view-font-size", 10).toInt();
     m_showSplash = settings.value("show-splash", true).toBool();
     settings.endGroup();
@@ -94,6 +96,7 @@
     props.push_back("Temporary Directory Root");
     props.push_back("Background Mode");
     props.push_back("Time To Text Mode");
+    props.push_back("Octave Numbering System");
     props.push_back("View Font Size");
     props.push_back("Show Splash Screen");
     return props;
@@ -135,6 +138,9 @@
     if (name == "Time To Text Mode") {
         return tr("Time display format");
     }
+    if (name == "Octave Numbering System") {
+        return tr("Label middle C as");
+    }
     if (name == "View Font Size") {
         return tr("Font size for text overlays");
     }
@@ -181,6 +187,9 @@
     if (name == "Time To Text Mode") {
         return ValueProperty;
     }
+    if (name == "Octave Numbering System") {
+        return ValueProperty;
+    }
     if (name == "View Font Size") {
         return RangeProperty;
     }
@@ -248,6 +257,16 @@
         return int(m_timeToTextMode);
     }        
 
+    if (name == "Octave Numbering System") {
+        // we don't support arbitrary octaves in the gui, because we
+        // want to be able to label what the octave system comes
+        // from. so we support 0, 3, 4 and 5.
+        if (min) *min = 0;
+        if (max) *max = 3;
+        if (deflt) *deflt = 2;
+        return int(getSystemWithMiddleCInOctave(m_octave));
+    }
+
     if (name == "View Font Size") {
         if (min) *min = 3;
         if (max) *max = 48;
@@ -322,6 +341,14 @@
         case TimeToText60Frame: return tr("60 FPS");
         }
     }
+    if (name == "Octave Numbering System") {
+        switch (value) {
+        case C0_Centre: return tr("C0 - middle of octave scale");
+        case C3_Logic: return tr("C3 - common MIDI sequencer convention");
+        case C4_ASA: return tr("C4 - ASA American standard");
+        case C5_Sonar: return tr("C5 - used in Cakewalk and others");
+        }
+    }
             
     return "";
 }
@@ -359,6 +386,9 @@
         setBackgroundMode(BackgroundMode(value));
     } else if (name == "Time To Text Mode") {
         setTimeToTextMode(TimeToTextMode(value));
+    } else if (name == "Octave Numbering System") {
+        setOctaveOfMiddleC(getOctaveOfMiddleCInSystem
+                           (OctaveNumberingSystem(value)));
     } else if (name == "View Font Size") {
         setViewFontSize(value);
     } else if (name == "Show Splash Screen") {
@@ -525,6 +555,45 @@
 }
 
 void
+Preferences::setOctaveOfMiddleC(int oct)
+{
+    if (m_octave != oct) {
+
+        m_octave = oct;
+
+        QSettings settings;
+        settings.beginGroup("Preferences");
+        settings.setValue("octave-of-middle-c", int(oct));
+        settings.endGroup();
+        emit propertyChanged("Octave Numbering System");
+    }
+}
+
+int
+Preferences::getOctaveOfMiddleCInSystem(OctaveNumberingSystem s)
+{
+    switch (s) {
+    case C0_Centre: return 0;
+    case C3_Logic: return 3;
+    case C4_ASA: return 4;
+    case C5_Sonar: return 5;
+    default: return 4;
+    }
+}
+
+Preferences::OctaveNumberingSystem
+Preferences::getSystemWithMiddleCInOctave(int o)
+{
+    switch (o) {
+    case 0: return C0_Centre;
+    case 3: return C3_Logic;
+    case 4: return C4_ASA;
+    case 5: return C5_Sonar;
+    default: return C4_ASA;
+    }
+}
+
+void
 Preferences::setViewFontSize(int size)
 {
     if (m_viewFontSize != size) {
--- a/base/Preferences.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/Preferences.h	Wed May 07 15:14:17 2014 +0100
@@ -86,6 +86,14 @@
     };
     TimeToTextMode getTimeToTextMode() const { return m_timeToTextMode; }
 
+    int getOctaveOfMiddleC() const {
+        // weed out unsupported octaves
+        return getOctaveOfMiddleCInSystem(getSystemWithMiddleCInOctave(m_octave));
+    }
+    int getOctaveOfLowestMIDINote() const {
+        return getOctaveOfMiddleC() - 5;
+    }
+    
     bool getShowSplash() const { return m_showSplash; }
 
 public slots:
@@ -102,6 +110,7 @@
     void setResampleOnLoad(bool);
     void setBackgroundMode(BackgroundMode mode);
     void setTimeToTextMode(TimeToTextMode mode);
+    void setOctaveOfMiddleC(int oct);
     void setViewFontSize(int size);
     void setShowSplash(bool);
 
@@ -111,6 +120,19 @@
 
     static Preferences *m_instance;
 
+    // We don't support arbitrary octaves in the gui, because we want
+    // to be able to label what the octave system comes from. These
+    // are the ones we support. (But we save and load as octave
+    // numbers, so as not to make the prefs format really confusing)
+    enum OctaveNumberingSystem {
+        C0_Centre,
+        C3_Logic,
+        C4_ASA,
+        C5_Sonar
+    };
+    static int getOctaveOfMiddleCInSystem(OctaveNumberingSystem s);
+    static OctaveNumberingSystem getSystemWithMiddleCInOctave(int o);
+
     SpectrogramSmoothing m_spectrogramSmoothing;
     SpectrogramXSmoothing m_spectrogramXSmoothing;
     float m_tuningFrequency;
@@ -123,6 +145,7 @@
     int m_viewFontSize;
     BackgroundMode m_backgroundMode;
     TimeToTextMode m_timeToTextMode;
+    int m_octave;
     bool m_showSplash;
 };
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/TestPitch.h	Wed May 07 15:14:17 2014 +0100
@@ -0,0 +1,97 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef TEST_PITCH_H
+#define TEST_PITCH_H
+
+#include "../Pitch.h"
+#include "../Preferences.h"
+
+#include <QObject>
+#include <QtTest>
+#include <QDir>
+
+#include <iostream>
+
+using namespace std;
+
+class TestPitch : public QObject
+{
+    Q_OBJECT
+
+private slots:
+    void init() {
+	Preferences::getInstance()->setOctaveOfMiddleC(4);
+	Preferences::getInstance()->setTuningFrequency(440);
+    }
+
+    void pitchLabel()
+    {
+	QCOMPARE(Pitch::getPitchLabel(60, 0, false), QString("C4"));
+	QCOMPARE(Pitch::getPitchLabel(69, 0, false), QString("A4"));
+	QCOMPARE(Pitch::getPitchLabel(61, 0, false), QString("C#4"));
+	QCOMPARE(Pitch::getPitchLabel(61, 0, true), QString("Db4"));
+	QCOMPARE(Pitch::getPitchLabel(59, 0, false), QString("B3"));
+	QCOMPARE(Pitch::getPitchLabel(59, 0, true), QString("B3"));
+	QCOMPARE(Pitch::getPitchLabel(0, 0, false), QString("C-1"));
+
+	QCOMPARE(Pitch::getPitchLabel(60, -40, false), QString("C4-40c"));
+	QCOMPARE(Pitch::getPitchLabel(60, 40, false), QString("C4+40c"));
+	QCOMPARE(Pitch::getPitchLabel(58, 4, false), QString("A#3+4c"));
+
+	Preferences::getInstance()->setOctaveOfMiddleC(3);
+
+	QCOMPARE(Pitch::getPitchLabel(60, 0, false), QString("C3"));
+	QCOMPARE(Pitch::getPitchLabel(69, 0, false), QString("A3"));
+	QCOMPARE(Pitch::getPitchLabel(61, 0, false), QString("C#3"));
+	QCOMPARE(Pitch::getPitchLabel(61, 0, true), QString("Db3"));
+	QCOMPARE(Pitch::getPitchLabel(59, 0, false), QString("B2"));
+	QCOMPARE(Pitch::getPitchLabel(59, 0, true), QString("B2"));
+	QCOMPARE(Pitch::getPitchLabel(0, 0, false), QString("C-2"));
+
+	QCOMPARE(Pitch::getPitchLabel(60, -40, false), QString("C3-40c"));
+	QCOMPARE(Pitch::getPitchLabel(60, 40, false), QString("C3+40c"));
+	QCOMPARE(Pitch::getPitchLabel(58, 4, false), QString("A#2+4c"));
+    }
+
+    void pitchLabelForFrequency()
+    {
+	QCOMPARE(Pitch::getPitchLabelForFrequency(440, 440, false), QString("A4"));
+	QCOMPARE(Pitch::getPitchLabelForFrequency(440, 220, false), QString("A5"));
+	QCOMPARE(Pitch::getPitchLabelForFrequency(261.63, 440, false), QString("C4"));
+    }
+
+#define MIDDLE_C 261.6255653f
+
+    void frequencyForPitch()
+    {
+	QCOMPARE(Pitch::getFrequencyForPitch(60, 0), MIDDLE_C);
+	QCOMPARE(Pitch::getFrequencyForPitch(69, 0), 440.f);
+	QCOMPARE(Pitch::getFrequencyForPitch(60, 0, 220), MIDDLE_C / 2.f);
+	QCOMPARE(Pitch::getFrequencyForPitch(69, 0, 220), 220.f);
+    }
+
+    void pitchForFrequency()
+    {
+	float centsOffset = 0.f;
+	QCOMPARE(Pitch::getPitchForFrequency(MIDDLE_C, &centsOffset), 60);
+	QCOMPARE(centsOffset, 0.f);
+	QCOMPARE(Pitch::getPitchForFrequency(261.0, &centsOffset), 60);
+	QCOMPARE(int(centsOffset), -4);
+	QCOMPARE(Pitch::getPitchForFrequency(440.0, &centsOffset), 69);
+	QCOMPARE(centsOffset, 0.f);
+    }
+};
+
+#endif
--- a/base/test/main.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/test/main.cpp	Wed May 07 15:14:17 2014 +0100
@@ -12,6 +12,7 @@
 */
 
 #include "TestRangeMapper.h"
+#include "TestPitch.h"
 
 #include <QtTest>
 
@@ -30,6 +31,11 @@
 	if (QTest::qExec(&t, argc, argv) == 0) ++good;
 	else ++bad;
     }
+    {
+	TestPitch t;
+	if (QTest::qExec(&t, argc, argv) == 0) ++good;
+	else ++bad;
+    }
 
     if (bad > 0) {
 	cerr << "\n********* " << bad << " test suite(s) failed!\n" << endl;
--- a/base/test/test.pro	Sat Apr 26 23:05:32 2014 +0100
+++ b/base/test/test.pro	Wed May 07 15:14:17 2014 +0100
@@ -30,7 +30,7 @@
 OBJECTS_DIR = o
 MOC_DIR = o
 
-HEADERS += TestRangeMapper.h
+HEADERS += TestRangeMapper.h TestPitch.h
 SOURCES += main.cpp
 
 win* {
--- a/data/fft/FFTDataServer.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/data/fft/FFTDataServer.cpp	Wed May 07 15:14:17 2014 +0100
@@ -191,7 +191,7 @@
                 if (server->getFillCompletion() < 50) distance += 100;
 
 #ifdef DEBUG_FFT_SERVER
-                SVDEBUG << "FFTDataServer::getFuzzyInstance: Distance for server " << server << " is " << distance << ", best is " << bestdist << endl;
+                std::cerr << "FFTDataServer::getFuzzyInstance: Distance for server " << server << " is " << distance << ", best is " << bestdist << std::endl;
 #endif
                 
                 if (bestdist == -1 || distance < bestdist) {
@@ -204,7 +204,7 @@
         if (bestdist >= 0) {
             FFTDataServer *server = best->second.first;
 #ifdef DEBUG_FFT_SERVER
-            SVDEBUG << "FFTDataServer::getFuzzyInstance: We like server " << server << " (with distance " << bestdist << ")" << endl;
+            std::cerr << "FFTDataServer::getFuzzyInstance: We like server " << server << " (with distance " << bestdist << ")" << std::endl;
 #endif
             claimInstance(server, false);
             return server;
@@ -228,7 +228,7 @@
 FFTDataServer::findServer(QString n)
 {    
 #ifdef DEBUG_FFT_SERVER
-    SVDEBUG << "FFTDataServer::findServer(\"" << n << "\")" << endl;
+    std::cerr << "FFTDataServer::findServer(\"" << n << "\")" << std::endl;
 #endif
 
     if (m_servers.find(n) != m_servers.end()) {
@@ -236,7 +236,7 @@
         FFTDataServer *server = m_servers[n].first;
 
 #ifdef DEBUG_FFT_SERVER
-        SVDEBUG << "FFTDataServer::findServer(\"" << n << "\"): found " << server << endl;
+        std::cerr << "FFTDataServer::findServer(\"" << n << "\"): found " << server << std::endl;
 #endif
 
         claimInstance(server, false);
@@ -245,7 +245,7 @@
     }
 
 #ifdef DEBUG_FFT_SERVER
-        SVDEBUG << "FFTDataServer::findServer(\"" << n << "\"): not found" << endl;
+        std::cerr << "FFTDataServer::findServer(\"" << n << "\"): not found" << std::endl;
 #endif
 
     return 0;
@@ -264,7 +264,7 @@
                        "FFTDataServer::claimInstance::m_serverMapMutex");
 
 #ifdef DEBUG_FFT_SERVER
-    SVDEBUG << "FFTDataServer::claimInstance(" << server << ")" << endl;
+    std::cerr << "FFTDataServer::claimInstance(" << server << ")" << std::endl;
 #endif
 
     for (ServerMap::iterator i = m_servers.begin(); i != m_servers.end(); ++i) {
@@ -275,7 +275,7 @@
 
                 if (*j == server) {
 #ifdef DEBUG_FFT_SERVER
-    SVDEBUG << "FFTDataServer::claimInstance: found in released server list, removing from it" << endl;
+    std::cerr << "FFTDataServer::claimInstance: found in released server list, removing from it" << std::endl;
 #endif
                     m_releasedServers.erase(j);
                     break;
@@ -285,7 +285,7 @@
             ++i->second.second;
 
 #ifdef DEBUG_FFT_SERVER
-            SVDEBUG << "FFTDataServer::claimInstance: new refcount is " << i->second.second << endl;
+            std::cerr << "FFTDataServer::claimInstance: new refcount is " << i->second.second << std::endl;
 #endif
 
             return;
@@ -309,7 +309,7 @@
                        "FFTDataServer::releaseInstance::m_serverMapMutex");
 
 #ifdef DEBUG_FFT_SERVER
-    SVDEBUG << "FFTDataServer::releaseInstance(" << server << ")" << endl;
+    std::cerr << "FFTDataServer::releaseInstance(" << server << ")" << std::endl;
 #endif
 
     // -- if ref count > 0, decrement and return
@@ -332,18 +332,18 @@
 /*!!!
                 if (server->m_lastUsedCache == -1) { // never used
 #ifdef DEBUG_FFT_SERVER
-                    SVDEBUG << "FFTDataServer::releaseInstance: instance "
+                    std::cerr << "FFTDataServer::releaseInstance: instance "
                               << server << " has never been used, erasing"
-                              << endl;
+                              << std::endl;
 #endif
                     delete server;
                     m_servers.erase(i);
                 } else {
 */
 #ifdef DEBUG_FFT_SERVER
-                    SVDEBUG << "FFTDataServer::releaseInstance: instance "
+                    std::cerr << "FFTDataServer::releaseInstance: instance "
                               << server << " no longer in use, marking for possible collection"
-                              << endl;
+                              << std::endl;
 #endif
                     bool found = false;
                     for (ServerQueue::iterator j = m_releasedServers.begin();
@@ -361,9 +361,9 @@
 //!!!                }
             } else {
 #ifdef DEBUG_FFT_SERVER
-                    SVDEBUG << "FFTDataServer::releaseInstance: instance "
+                    std::cerr << "FFTDataServer::releaseInstance: instance "
                               << server << " now has refcount " << i->second.second
-                              << endl;
+                              << std::endl;
 #endif
             }
             return;
@@ -378,8 +378,8 @@
 FFTDataServer::purgeLimbo(int maxSize)
 {
 #ifdef DEBUG_FFT_SERVER
-    SVDEBUG << "FFTDataServer::purgeLimbo(" << maxSize << "): "
-              << m_releasedServers.size() << " candidates" << endl;
+    std::cerr << "FFTDataServer::purgeLimbo(" << maxSize << "): "
+              << m_releasedServers.size() << " candidates" << std::endl;
 #endif
 
     while (int(m_releasedServers.size()) > maxSize) {
@@ -389,8 +389,8 @@
         bool found = false;
 
 #ifdef DEBUG_FFT_SERVER
-        SVDEBUG << "FFTDataServer::purgeLimbo: considering candidate "
-                  << server << endl;
+        std::cerr << "FFTDataServer::purgeLimbo: considering candidate "
+                  << server << std::endl;
 #endif
 
         for (ServerMap::iterator i = m_servers.begin(); i != m_servers.end(); ++i) {
@@ -405,8 +405,8 @@
                     break;
                 }
 #ifdef DEBUG_FFT_SERVER
-                SVDEBUG << "FFTDataServer::purgeLimbo: looks OK, erasing it"
-                          << endl;
+                std::cerr << "FFTDataServer::purgeLimbo: looks OK, erasing it"
+                          << std::endl;
 #endif
 
                 m_servers.erase(i);
@@ -426,8 +426,8 @@
     }
 
 #ifdef DEBUG_FFT_SERVER
-    SVDEBUG << "FFTDataServer::purgeLimbo(" << maxSize << "): "
-              << m_releasedServers.size() << " remain" << endl;
+    std::cerr << "FFTDataServer::purgeLimbo(" << maxSize << "): "
+              << m_releasedServers.size() << " remain" << std::endl;
 #endif
 
 }
@@ -439,8 +439,8 @@
                        "FFTDataServer::modelAboutToBeDeleted::m_serverMapMutex");
 
 #ifdef DEBUG_FFT_SERVER
-    SVDEBUG << "FFTDataServer::modelAboutToBeDeleted(" << model << ")"
-              << endl;
+    std::cerr << "FFTDataServer::modelAboutToBeDeleted(" << model << ")"
+              << std::endl;
 #endif
 
     for (ServerMap::iterator i = m_servers.begin(); i != m_servers.end(); ++i) {
@@ -450,8 +450,8 @@
         if (server->getModel() == model) {
 
 #ifdef DEBUG_FFT_SERVER
-            SVDEBUG << "FFTDataServer::modelAboutToBeDeleted: server is "
-                      << server << endl;
+            std::cerr << "FFTDataServer::modelAboutToBeDeleted: server is "
+                      << server << std::endl;
 #endif
 
             if (i->second.second > 0) {
@@ -463,14 +463,14 @@
                  j != m_releasedServers.end(); ++j) {
                 if (*j == server) {
 #ifdef DEBUG_FFT_SERVER
-                    SVDEBUG << "FFTDataServer::modelAboutToBeDeleted: erasing from released servers" << endl;
+                    std::cerr << "FFTDataServer::modelAboutToBeDeleted: erasing from released servers" << std::endl;
 #endif
                     m_releasedServers.erase(j);
                     break;
                 }
             }
 #ifdef DEBUG_FFT_SERVER
-            SVDEBUG << "FFTDataServer::modelAboutToBeDeleted: erasing server" << endl;
+            std::cerr << "FFTDataServer::modelAboutToBeDeleted: erasing server" << std::endl;
 #endif
             m_servers.erase(i);
             delete server;
@@ -841,7 +841,7 @@
     // preconditions: m_caches[c] exists and contains a file writer;
     // m_cacheVectorLock is not locked by this thread
 #ifdef DEBUG_FFT_SERVER
-    SVDEBUG << "FFTDataServer::makeCacheReader(" << c << ")" << endl;
+    std::cerr << "FFTDataServer::makeCacheReader(" << c << ")" << std::endl;
 #endif
 
     QThread *me = QThread::currentThread();
@@ -875,7 +875,7 @@
     cb = m_caches.at(deleteCandidate);
     if (cb && cb->fileCacheReader.find(me) != cb->fileCacheReader.end()) {
 #ifdef DEBUG_FFT_SERVER
-        SVDEBUG << "FFTDataServer::makeCacheReader: Deleting probably unpopular reader " << deleteCandidate << " for this thread (as I create reader " << c << ")" << endl;
+        std::cerr << "FFTDataServer::makeCacheReader: Deleting probably unpopular reader " << deleteCandidate << " for this thread (as I create reader " << c << ")" << std::endl;
 #endif
         delete cb->fileCacheReader[me];
         cb->fileCacheReader.erase(me);
@@ -901,8 +901,8 @@
         if (!cache->haveSetColumnAt(col)) {
             Profiler profiler("FFTDataServer::getMagnitudeAt: filling");
 #ifdef DEBUG_FFT_SERVER
-            SVDEBUG << "FFTDataServer::getMagnitudeAt: calling fillColumn(" 
-                  << x << ")" << endl;
+            std::cerr << "FFTDataServer::getMagnitudeAt: calling fillColumn("
+                  << x << ")" << std::endl;
 #endif
             fillColumn(x);
         }
@@ -1130,7 +1130,7 @@
         if (!cache->haveSetColumnAt(col)) {
             Profiler profiler("FFTDataServer::getValuesAt: filling");
 #ifdef DEBUG_FFT_SERVER
-            SVDEBUG << "FFTDataServer::getValuesAt(" << x << ", " << y << "): filling" << endl;
+            std::cerr << "FFTDataServer::getValuesAt(" << x << ", " << y << "): filling" << std::endl;
 #endif
             fillColumn(x);
         }        
@@ -1189,7 +1189,7 @@
 /*!!!
         if (m_lastUsedCache == -1) {
             if (m_suspended) {
-                SVDEBUG << "FFTDataServer::isColumnReady(" << x << "): no cache, calling resume" << endl;
+                std::cerr << "FFTDataServer::isColumnReady(" << x << "): no cache, calling resume" << std::endl;
                 resume();
             }
             m_fillThread->start();
@@ -1258,12 +1258,12 @@
     endFrame   -= winsize / 2;
 
 #ifdef DEBUG_FFT_SERVER_FILL
-    SVDEBUG << "FFTDataServer::fillColumn: requesting frames "
+    std::cerr << "FFTDataServer::fillColumn: requesting frames "
               << startFrame + pfx << " -> " << endFrame << " ( = "
               << endFrame - (startFrame + pfx) << ") at index "
               << off + pfx << " in buffer of size " << m_fftSize
               << " with window size " << m_windowSize 
-              << " from channel " << m_channel << endl;
+              << " from channel " << m_channel << std::endl;
 #endif
 
     QMutexLocker locker(&m_fftBuffersLock);
@@ -1370,7 +1370,7 @@
     }
 
     if (m_suspended) {
-//        SVDEBUG << "FFTDataServer::fillColumn(" << x << "): calling resume" << endl;
+//        std::cerr << "FFTDataServer::fillColumn(" << x << "): calling resume" << std::endl;
 //        resume();
     }
 }    
@@ -1446,7 +1446,7 @@
 FFTDataServer::FillThread::run()
 {
 #ifdef DEBUG_FFT_SERVER_FILL
-    SVDEBUG << "FFTDataServer::FillThread::run()" << endl;
+    std::cerr << "FFTDataServer::FillThread::run()" << std::endl;
 #endif
     
     m_extent = 0;
@@ -1454,7 +1454,7 @@
     
     while (!m_server.m_model->isReady() && !m_server.m_exiting) {
 #ifdef DEBUG_FFT_SERVER_FILL
-        SVDEBUG << "FFTDataServer::FillThread::run(): waiting for model " << m_server.m_model << " to be ready" << endl;
+        std::cerr << "FFTDataServer::FillThread::run(): waiting for model " << m_server.m_model << " to be ready" << std::endl;
 #endif
         sleep(1);
     }
@@ -1476,7 +1476,7 @@
             try {
                 m_server.fillColumn(int((f - start) / m_server.m_windowIncrement));
             } catch (std::exception &e) {
-                SVDEBUG << "FFTDataServer::FillThread::run: exception: " << e.what() << endl;
+                std::cerr << "FFTDataServer::FillThread::run: exception: " << e.what() << std::endl;
                 m_error = e.what();
                 m_server.fillComplete();
                 m_completion = 100;
@@ -1525,7 +1525,7 @@
         try {
             m_server.fillColumn(int((f - start) / m_server.m_windowIncrement));
         } catch (std::exception &e) {
-            SVDEBUG << "FFTDataServer::FillThread::run: exception: " << e.what() << endl;
+            std::cerr << "FFTDataServer::FillThread::run: exception: " << e.what() << std::endl;
             m_error = e.what();
             m_server.fillComplete();
             m_completion = 100;
@@ -1567,7 +1567,7 @@
     m_extent = end;
 
 #ifdef DEBUG_FFT_SERVER
-    SVDEBUG << "FFTDataServer::FillThread::run exiting" << endl;
+    std::cerr << "FFTDataServer::FillThread::run exiting" << std::endl;
 #endif
 }
 
--- a/data/fileio/CoreAudioFileReader.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/data/fileio/CoreAudioFileReader.cpp	Wed May 07 15:14:17 2014 +0100
@@ -89,16 +89,16 @@
 
     //!!! how do we find out if the file open fails because of DRM protection?
 
-#if (MACOSX_DEPLOYMENT_TARGET <= 1040 && MAC_OS_X_VERSION_MIN_REQUIRED <= 1040)
-    FSRef fsref;
-    if (!CFURLGetFSRef(url, &fsref)) { // returns Boolean, not error code
-        m_error = "CoreAudioReadStream: Error looking up FS ref (file not found?)";
-        return;
-    }
-    m_d->err = ExtAudioFileOpen(&fsref, &m_d->file);
-#else
+//#if (MACOSX_DEPLOYMENT_TARGET <= 1040 && MAC_OS_X_VERSION_MIN_REQUIRED <= 1040)
+//    FSRef fsref;
+//    if (!CFURLGetFSRef(url, &fsref)) { // returns Boolean, not error code
+//        m_error = "CoreAudioReadStream: Error looking up FS ref (file not found?)";
+//        return;
+//    }
+//    m_d->err = ExtAudioFileOpen(&fsref, &m_d->file);
+//#else
     m_d->err = ExtAudioFileOpenURL(url, &m_d->file);
-#endif
+//#endif
 
     CFRelease(url);
 
--- a/data/fileio/FileFinder.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/data/fileio/FileFinder.h	Wed May 07 15:14:17 2014 +0100
@@ -30,6 +30,8 @@
         ImageFile,
         AnyFile,
         CSVFile,
+        LayerFileNonSV,
+        LayerFileNoMidiNonSV,
     };
 
     virtual QString getOpenFileName(FileType type, QString fallbackLocation = "") = 0;
--- a/data/fileio/MIDIFileWriter.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/data/fileio/MIDIFileWriter.cpp	Wed May 07 15:14:17 2014 +0100
@@ -23,8 +23,7 @@
 #include "MIDIFileWriter.h"
 
 #include "data/midi/MIDIEvent.h"
-
-#include "model/NoteModel.h"
+#include "model/NoteData.h"
 
 #include "base/Pitch.h"
 
@@ -37,14 +36,13 @@
 
 using namespace MIDIConstants;
 
-MIDIFileWriter::MIDIFileWriter(QString path, NoteModel *model, float tempo) :
+MIDIFileWriter::MIDIFileWriter(QString path, const NoteExportable *exportable,
+                               int sampleRate, float tempo) :
     m_path(path),
-    m_model(model),
-    m_modelUsesHz(false),
+    m_exportable(exportable),
+    m_sampleRate(sampleRate),
     m_tempo(tempo)
 {
-    if (model->getScaleUnits().toLower() == "hz") m_modelUsesHz = true;
-
     if (!convert()) {
         m_error = "Conversion from model to internal MIDI format failed";
     }
@@ -342,42 +340,28 @@
 
     // Omit time signature
 
-    const NoteModel::PointList &notes =
-        static_cast<SparseModel<Note> *>(m_model)->getPoints();
+    NoteList notes = m_exportable->getNotes();
 
-    for (NoteModel::PointList::const_iterator i = notes.begin();
-         i != notes.end(); ++i) {
+    for (NoteList::const_iterator i = notes.begin(); i != notes.end(); ++i) {
 
-        long frame = i->frame;
-        float value = i->value;
+        size_t frame = i->start;
         size_t duration = i->duration;
-
-        int pitch;
-
-        if (m_modelUsesHz) {
-            pitch = Pitch::getPitchForFrequency(value);
-        } else {
-            pitch = lrintf(value);
-        }
+        int pitch = i->midiPitch;
+        int velocity = i->velocity;
 
         if (pitch < 0) pitch = 0;
         if (pitch > 127) pitch = 127;
 
         // Convert frame to MIDI time
 
-        double seconds = double(frame) / double(m_model->getSampleRate());
+        double seconds = double(frame) / double(m_sampleRate);
         double quarters = (seconds * m_tempo) / 60.0;
-        unsigned long midiTime = lrint(quarters * m_timingDivision);
-
-        int velocity = 100;
-        if (i->level > 0.f && i->level <= 1.f) {
-            velocity = lrintf(i->level * 127.f);
-        }
+        unsigned long midiTime = int(quarters * m_timingDivision + 0.5);
 
         // Get the sounding time for the matching NOTE_OFF
-        seconds = double(frame + duration) / double(m_model->getSampleRate());
+        seconds = double(frame + duration) / double(m_sampleRate);
         quarters = (seconds * m_tempo) / 60.0;
-        unsigned long endTime = lrint(quarters * m_timingDivision);
+        unsigned long endTime = int(quarters * m_timingDivision + 0.5);
 
         // At this point all the notes we insert have absolute times
         // in the delta time fields.  We resolve these into delta
--- a/data/fileio/MIDIFileWriter.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/data/fileio/MIDIFileWriter.h	Wed May 07 15:14:17 2014 +0100
@@ -32,7 +32,7 @@
 #include <fstream>
 
 class MIDIEvent;
-class NoteModel;
+class NoteExportable;
 
 /**
  * Write a MIDI file.  This includes file write code for generic
@@ -43,7 +43,10 @@
 class MIDIFileWriter 
 {
 public:
-    MIDIFileWriter(QString path, NoteModel *model, float tempo = 120.f);
+    MIDIFileWriter(QString path, 
+                   const NoteExportable *exportable, 
+                   int sampleRate, // used to convert exportable sample timings
+                   float tempo = 120.f);
     virtual ~MIDIFileWriter();
 
     virtual bool isOK() const;
@@ -74,18 +77,18 @@
     
     bool convert();
 
-    QString             m_path;
-    NoteModel          *m_model;
-    bool                m_modelUsesHz;
-    float               m_tempo;
-    int                 m_timingDivision;   // pulses per quarter note
-    MIDIFileFormatType  m_format;
-    unsigned int        m_numberOfTracks;
+    QString               m_path;
+    const NoteExportable *m_exportable;
+    int                   m_sampleRate;
+    float                 m_tempo;
+    int                   m_timingDivision;   // pulses per quarter note
+    MIDIFileFormatType    m_format;
+    unsigned int          m_numberOfTracks;
 
-    MIDIComposition     m_midiComposition;
+    MIDIComposition       m_midiComposition;
 
-    std::ofstream      *m_midiFile;
-    QString             m_error;
+    std::ofstream        *m_midiFile;
+    QString               m_error;
 };
 
 #endif
--- a/data/model/DenseTimeValueModel.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/data/model/DenseTimeValueModel.h	Wed May 07 15:14:17 2014 +0100
@@ -22,7 +22,9 @@
 
 /**
  * Base class for models containing dense two-dimensional data (value
- * against time).  For example, audio waveform data.
+ * against time).  For example, audio waveform data.  Other time-value
+ * plot data, especially if editable, will normally go into a
+ * SparseTimeValueModel instead even if regularly sampled.
  */
 
 class DenseTimeValueModel : public Model
@@ -83,8 +85,7 @@
                            float **buffers) const = 0;
 
     virtual bool canPlay() const { return true; }
-    virtual QString getDefaultPlayPluginId() const { return ""; }
-    virtual QString getDefaultPlayPluginConfiguration() const { return ""; }
+    virtual QString getDefaultPlayClipId() const { return ""; }
 
     virtual QString toDelimitedDataString(QString delimiter, size_t f0, size_t f1) const;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/FlexiNoteModel.h	Wed May 07 15:14:17 2014 +0100
@@ -0,0 +1,271 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _FLEXINOTE_MODEL_H_
+#define _FLEXINOTE_MODEL_H_
+
+#include "IntervalModel.h"
+#include "NoteData.h"
+#include "base/RealTime.h"
+#include "base/Pitch.h"
+#include "base/PlayParameterRepository.h"
+
+/**
+ * FlexiNoteModel -- a concrete IntervalModel for notes.
+ */
+
+/**
+ * Extension of the NoteModel for more flexible note interaction. 
+ * The original NoteModel rationale is given below, will need to be
+ * updated for FlexiNoteModel:
+ *
+ * Note type for use in a sparse model.  All we mean by a "note" is
+ * something that has an onset time, a single value, a duration, and a
+ * level.  Like other points, it can also have a label.  With this
+ * point type, the model can be thought of as representing a simple
+ * MIDI-type piano roll, except that the y coordinates (values) do not
+ * have to be discrete integers.
+ */
+
+struct FlexiNote
+{
+public:
+    FlexiNote(long _frame) : frame(_frame), value(0.0f), duration(0), level(1.f) { }
+    FlexiNote(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\" level=\"%5\" label=\"%6\" %7/>\n")
+	    .arg(indent).arg(frame).arg(value).arg(duration).arg(level)
+            .arg(XmlExportable::encodeEntities(label)).arg(extraAttributes);
+    }
+
+    QString toDelimitedDataString(QString delimiter, size_t sampleRate) const
+    {
+        QStringList list;
+        list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str();
+        list << QString("%1").arg(value);
+        list << RealTime::frame2RealTime(duration, sampleRate).toString().c_str();
+        list << QString("%1").arg(level);
+        if (label != "") list << label;
+        return list.join(delimiter);
+    }
+
+    struct Comparator {
+	bool operator()(const FlexiNote &p1,
+			const FlexiNote &p2) const {
+	    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;
+	}
+    };
+    
+    struct OrderComparator {
+	bool operator()(const FlexiNote &p1,
+			const FlexiNote &p2) const {
+	    return p1.frame < p2.frame;
+	}
+    };
+};
+
+
+class FlexiNoteModel : public IntervalModel<FlexiNote>, public NoteExportable
+{
+    Q_OBJECT
+    
+public:
+    FlexiNoteModel(size_t sampleRate, size_t resolution,
+	      bool notifyOnAdd = true) :
+	IntervalModel<FlexiNote>(sampleRate, resolution, notifyOnAdd),
+	m_valueQuantization(0)
+    {
+	PlayParameterRepository::getInstance()->addPlayable(this);
+    }
+
+    FlexiNoteModel(size_t sampleRate, size_t resolution,
+	      float valueMinimum, float valueMaximum,
+	      bool notifyOnAdd = true) :
+	IntervalModel<FlexiNote>(sampleRate, resolution,
+                            valueMinimum, valueMaximum,
+                            notifyOnAdd),
+	m_valueQuantization(0)
+    {
+	PlayParameterRepository::getInstance()->addPlayable(this);
+    }
+
+    virtual ~FlexiNoteModel()
+    {
+        PlayParameterRepository::getInstance()->removePlayable(this);
+    }
+
+    float getValueQuantization() const { return m_valueQuantization; }
+    void setValueQuantization(float q) { m_valueQuantization = q; }
+    float getValueMinimum() const { return 33; }
+    float getValueMaximum() const { return 88; }
+
+    QString getTypeName() const { return tr("FlexiNote"); }
+
+    virtual bool canPlay() const { return true; }
+
+    virtual QString getDefaultPlayClipId() const
+    {
+        return "elecpiano";
+    }
+
+    virtual void toXml(QTextStream &out,
+                       QString indent = "",
+                       QString extraAttributes = "") const
+    {
+        std::cerr << "FlexiNoteModel::toXml: extraAttributes = \"" 
+                  << extraAttributes.toStdString() << std::endl;
+
+        IntervalModel<FlexiNote>::toXml
+	    (out,
+             indent,
+	     QString("%1 subtype=\"flexinote\" valueQuantization=\"%2\"")
+	     .arg(extraAttributes).arg(m_valueQuantization));
+    }
+
+    /**
+     * TabularModel methods.  
+     */
+    
+    virtual int getColumnCount() const
+    {
+        return 6;
+    }
+
+    virtual QString getHeading(int column) const
+    {
+        switch (column) {
+        case 0: return tr("Time");
+        case 1: return tr("Frame");
+        case 2: return tr("Pitch");
+        case 3: return tr("Duration");
+        case 4: return tr("Level");
+        case 5: return tr("Label");
+        default: return tr("Unknown");
+        }
+    }
+
+    virtual QVariant getData(int row, int column, int role) const
+    {
+        if (column < 4) {
+            return IntervalModel<FlexiNote>::getData(row, column, role);
+        }
+
+        PointListConstIterator i = getPointListIteratorForRow(row);
+        if (i == m_points.end()) return QVariant();
+
+        switch (column) {
+        case 4: return i->level;
+        case 5: return i->label;
+        default: return QVariant();
+        }
+    }
+
+    virtual Command *getSetDataCommand(int row, int column, const QVariant &value, int role)
+    {
+        if (column < 4) {
+            return IntervalModel<FlexiNote>::getSetDataCommand
+                (row, column, value, role);
+        }
+
+        if (role != Qt::EditRole) return 0;
+        PointListConstIterator i = getPointListIteratorForRow(row);
+        if (i == m_points.end()) return 0;
+        EditCommand *command = new EditCommand(this, tr("Edit Data"));
+
+        Point point(*i);
+        command->deletePoint(point);
+
+        switch (column) {
+        case 4: point.level = value.toDouble(); break;
+        case 5: point.label = value.toString(); break;
+        }
+
+        command->addPoint(point);
+        return command->finish();
+    }
+
+    virtual SortType getSortType(int column) const
+    {
+        if (column == 5) return SortAlphabetical;
+        return SortNumeric;
+    }
+
+    /**
+     * NoteExportable methods.
+     */
+
+    NoteList getNotes() const {
+        return getNotes(getStartFrame(), getEndFrame());
+    }
+
+    NoteList getNotes(size_t startFrame, size_t endFrame) const {
+        
+	PointList points = getPoints(startFrame, endFrame);
+        NoteList notes;
+
+        for (PointList::iterator pli =
+		 points.begin(); pli != points.end(); ++pli) {
+
+	    size_t duration = pli->duration;
+            if (duration == 0 || duration == 1) {
+                duration = getSampleRate() / 20;
+            }
+
+            int pitch = lrintf(pli->value);
+            
+            int velocity = 100;
+            if (pli->level > 0.f && pli->level <= 1.f) {
+                velocity = lrintf(pli->level * 127);
+            }
+
+            NoteData note(pli->frame, duration, pitch, velocity);
+
+            if (getScaleUnits() == "Hz") {
+                note.frequency = pli->value;
+                note.midiPitch = Pitch::getPitchForFrequency(note.frequency);
+                note.isMidiPitchQuantized = false;
+            }
+        
+            notes.push_back(note);
+        }
+        
+        return notes;
+    }
+
+protected:
+    float m_valueQuantization;
+};
+
+#endif
--- a/data/model/Model.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/data/model/Model.h	Wed May 07 15:14:17 2014 +0100
@@ -216,7 +216,7 @@
     virtual QString toDelimitedDataString(QString delimiter) const {
         return toDelimitedDataString(delimiter, getStartFrame(), getEndFrame());
     }
-    virtual QString toDelimitedDataString(QString, size_t f0, size_t f1) const {
+    virtual QString toDelimitedDataString(QString, size_t /* f0 */, size_t /* f1 */) const {
         return "";
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/NoteData.h	Wed May 07 15:14:17 2014 +0100
@@ -0,0 +1,53 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This 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 NOTE_DATA_H
+#define NOTE_DATA_H
+
+#include <vector>
+
+#include "base/Pitch.h"
+
+struct NoteData
+{
+    NoteData(size_t _start, size_t _dur, int _mp, int _vel) :
+	start(_start), duration(_dur), midiPitch(_mp), frequency(0),
+	isMidiPitchQuantized(true), velocity(_vel) { };
+            
+    size_t start;     // audio sample frame
+    size_t duration;  // in audio sample frames
+    int midiPitch; // 0-127
+    float frequency; // Hz, to be used if isMidiPitchQuantized false
+    bool isMidiPitchQuantized;
+    int velocity;  // MIDI-style 0-127
+
+    float getFrequency() const {
+        if (isMidiPitchQuantized) {
+            return Pitch::getFrequencyForPitch(midiPitch);
+        } else {
+            return frequency;
+        }
+    }
+};
+
+typedef std::vector<NoteData> NoteList;
+
+class NoteExportable
+{
+public:
+    virtual NoteList getNotes() const = 0;
+    virtual NoteList getNotes(size_t startFrame, size_t endFrame) const = 0;
+};
+
+#endif
--- a/data/model/NoteModel.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/data/model/NoteModel.h	Wed May 07 15:14:17 2014 +0100
@@ -17,8 +17,10 @@
 #define _NOTE_MODEL_H_
 
 #include "IntervalModel.h"
+#include "NoteData.h"
 #include "base/RealTime.h"
 #include "base/PlayParameterRepository.h"
+#include "base/Pitch.h"
 
 /**
  * NoteModel -- a concrete IntervalModel for notes.
@@ -91,7 +93,7 @@
 };
 
 
-class NoteModel : public IntervalModel<Note>
+class NoteModel : public IntervalModel<Note>, public NoteExportable
 {
     Q_OBJECT
     
@@ -127,14 +129,9 @@
 
     virtual bool canPlay() const { return true; }
 
-    virtual QString getDefaultPlayPluginId() const
+    virtual QString getDefaultPlayClipId() const
     {
-        return "dssi:_builtin:sample_player";
-    }
-
-    virtual QString getDefaultPlayPluginConfiguration() const
-    {
-        return "<plugin program=\"piano\"/>";
+        return "piano";
     }
 
     virtual void toXml(QTextStream &out,
@@ -219,6 +216,48 @@
         return SortNumeric;
     }
 
+    /**
+     * NoteExportable methods.
+     */
+
+    NoteList getNotes() const {
+        return getNotes(getStartFrame(), getEndFrame());
+    }
+
+    NoteList getNotes(size_t startFrame, size_t endFrame) const {
+        
+	PointList points = getPoints(startFrame, endFrame);
+        NoteList notes;
+
+        for (PointList::iterator pli =
+		 points.begin(); pli != points.end(); ++pli) {
+
+	    size_t duration = pli->duration;
+            if (duration == 0 || duration == 1) {
+                duration = getSampleRate() / 20;
+            }
+
+            int pitch = lrintf(pli->value);
+            
+            int velocity = 100;
+            if (pli->level > 0.f && pli->level <= 1.f) {
+                velocity = lrintf(pli->level * 127);
+            }
+
+            NoteData note(pli->frame, duration, pitch, velocity);
+
+            if (getScaleUnits() == "Hz") {
+                note.frequency = pli->value;
+                note.midiPitch = Pitch::getPitchForFrequency(note.frequency);
+                note.isMidiPitchQuantized = false;
+            }
+        
+            notes.push_back(note);
+        }
+        
+        return notes;
+    }
+
 protected:
     float m_valueQuantization;
 };
--- a/data/model/SparseOneDimensionalModel.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/data/model/SparseOneDimensionalModel.h	Wed May 07 15:14:17 2014 +0100
@@ -17,6 +17,7 @@
 #define _SPARSE_ONE_DIMENSIONAL_MODEL_H_
 
 #include "SparseModel.h"
+#include "NoteData.h"
 #include "base/PlayParameterRepository.h"
 #include "base/RealTime.h"
 
@@ -69,7 +70,8 @@
 };
 
 
-class SparseOneDimensionalModel : public SparseModel<OneDimensionalPoint>
+class SparseOneDimensionalModel : public SparseModel<OneDimensionalPoint>,
+                                  public NoteExportable
 {
     Q_OBJECT
     
@@ -88,14 +90,9 @@
 
     virtual bool canPlay() const { return true; }
 
-    virtual QString getDefaultPlayPluginId() const
+    virtual QString getDefaultPlayClipId() const
     {
-        return "dssi:_builtin:sample_player";
-    }
-
-    virtual QString getDefaultPlayPluginConfiguration() const
-    {
-        return "<plugin program=\"tap\"/>";
+        return "tap";
     }
 
     int getIndexOf(const Point &point)
@@ -181,6 +178,32 @@
         if (column == 2) return SortAlphabetical;
         return SortNumeric;
     }
+
+    /**
+     * NoteExportable methods.
+     */
+
+    NoteList getNotes() const {
+        return getNotes(getStartFrame(), getEndFrame());
+    }
+
+    NoteList getNotes(size_t startFrame, size_t endFrame) const {
+        
+	PointList points = getPoints(startFrame, endFrame);
+        NoteList notes;
+
+	for (PointList::iterator pli =
+		 points.begin(); pli != points.end(); ++pli) {
+
+            notes.push_back
+                (NoteData(pli->frame,
+                          getSampleRate() / 6, // arbitrary short duration
+                          64,   // default pitch
+                          100)); // default velocity
+        }
+
+        return notes;
+    }
 };
 
 #endif
--- a/data/model/SparseTimeValueModel.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/data/model/SparseTimeValueModel.h	Wed May 07 15:14:17 2014 +0100
@@ -86,7 +86,9 @@
 	SparseValueModel<TimeValuePoint>(sampleRate, resolution,
 					 notifyOnAdd)
     {
-        // not yet playable
+        // Model is playable, but may not sound (if units not Hz or
+        // range unsuitable)
+	PlayParameterRepository::getInstance()->addPlayable(this);
     }
 
     SparseTimeValueModel(size_t sampleRate, size_t resolution,
@@ -96,11 +98,20 @@
 					 valueMinimum, valueMaximum,
 					 notifyOnAdd)
     {
-        // not yet playable
+        // Model is playable, but may not sound (if units not Hz or
+        // range unsuitable)
+	PlayParameterRepository::getInstance()->addPlayable(this);
+    }
+
+    virtual ~SparseTimeValueModel()
+    {
+	PlayParameterRepository::getInstance()->removePlayable(this);
     }
 
     QString getTypeName() const { return tr("Sparse Time-Value"); }
 
+    virtual bool canPlay() const { return true; }
+
     /**
      * TabularModel methods.  
      */
--- a/plugin/DSSIPluginInstance.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/plugin/DSSIPluginInstance.cpp	Wed May 07 15:14:17 2014 +0100
@@ -34,7 +34,7 @@
 #endif
 
 //#define DEBUG_DSSI 1
-//#define DEBUG_DSSI_PROCESS 1
+#define DEBUG_DSSI_PROCESS 1
 
 #define EVENT_BUFFER_SIZE 1023
 
--- a/plugin/plugins/SamplePlayer.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/plugin/plugins/SamplePlayer.cpp	Wed May 07 15:14:17 2014 +0100
@@ -157,6 +157,7 @@
     }
 
     SamplePlayer *player = new SamplePlayer(rate);
+	// std::cerr << "Instantiated sample player " << std::endl;
 
     if (hostDescriptor->request_non_rt_thread(player, workThreadCallback)) {
 	SVDEBUG << "SamplePlayer::instantiate: Host rejected request_non_rt_thread call, not instantiating" << endl;
--- a/svcore.pro	Sat Apr 26 23:05:32 2014 +0100
+++ b/svcore.pro	Wed May 07 15:14:17 2014 +0100
@@ -4,9 +4,28 @@
 exists(config.pri) {
     include(config.pri)
 }
-win* {
-    !exists(config.pri) {
-        DEFINES += HAVE_BZ2 HAVE_FFTW3 HAVE_FFTW3F HAVE_SNDFILE HAVE_SAMPLERATE HAVE_VAMP HAVE_VAMPHOSTSDK HAVE_RUBBERBAND HAVE_DATAQUAY HAVE_LIBLO HAVE_MAD HAVE_ID3TAG
+!exists(config.pri) {
+
+    CONFIG += release
+    DEFINES += NDEBUG BUILD_RELEASE NO_TIMING
+
+    win32-g++ {
+        INCLUDEPATH += ../sv-dependency-builds/win32-mingw/include
+        LIBS += -L../sv-dependency-builds/win32-mingw/lib
+    }
+    win32-msvc* {
+        INCLUDEPATH += ../sv-dependency-builds/win32-msvc/include
+        LIBS += -L../sv-dependency-builds/win32-msvc/lib
+    }
+    macx* {
+        INCLUDEPATH += ../sv-dependency-builds/osx/include
+        LIBS += -L../sv-dependency-builds/osx/lib
+    }
+
+    DEFINES += HAVE_BZ2 HAVE_FFTW3 HAVE_FFTW3F HAVE_SNDFILE HAVE_SAMPLERATE HAVE_VAMP HAVE_VAMPHOSTSDK HAVE_RUBBERBAND HAVE_LIBLO HAVE_MAD HAVE_ID3TAG 
+
+    macx* {
+        DEFINES += HAVE_COREAUDIO
     }
 }
 
@@ -21,13 +40,6 @@
 OBJECTS_DIR = o
 MOC_DIR = o
 
-win32-g++ {
-    INCLUDEPATH += ../sv-dependency-builds/win32-mingw/include
-}
-win32-msvc* {
-    INCLUDEPATH += ../sv-dependency-builds/win32-msvc/include
-}
-
 # Doesn't work with this library, which contains C99 as well as C++
 PRECOMPILED_HEADER =
 
@@ -155,6 +167,7 @@
            data/model/Model.h \
            data/model/ModelDataTableModel.h \
            data/model/NoteModel.h \
+           data/model/FlexiNoteModel.h \
            data/model/PathModel.h \
            data/model/PowerOfSqrtTwoZoomConstraint.h \
            data/model/PowerOfTwoZoomConstraint.h \
--- a/transform/FeatureExtractionModelTransformer.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/transform/FeatureExtractionModelTransformer.cpp	Wed May 07 15:14:17 2014 +0100
@@ -27,6 +27,7 @@
 #include "data/model/EditableDenseThreeDimensionalModel.h"
 #include "data/model/DenseTimeValueModel.h"
 #include "data/model/NoteModel.h"
+#include "data/model/FlexiNoteModel.h"
 #include "data/model/RegionModel.h"
 #include "data/model/FFTModel.h"
 #include "data/model/WaveFileModel.h"
@@ -36,43 +37,80 @@
 
 #include <iostream>
 
+#include <QSettings>
+
 FeatureExtractionModelTransformer::FeatureExtractionModelTransformer(Input in,
                                                                      const Transform &transform) :
     ModelTransformer(in, transform),
-    m_plugin(0),
-    m_descriptor(0),
-    m_outputNo(0),
-    m_fixedRateFeatureNo(-1) // we increment before use
+    m_plugin(0)
 {
 //    SVDEBUG << "FeatureExtractionModelTransformer::FeatureExtractionModelTransformer: plugin " << pluginId << ", outputName " << m_transform.getOutput() << endl;
 
-    QString pluginId = transform.getPluginIdentifier();
+    initialise();
+}
+
+FeatureExtractionModelTransformer::FeatureExtractionModelTransformer(Input in,
+                                                                     const Transforms &transforms) :
+    ModelTransformer(in, transforms),
+    m_plugin(0)
+{
+//    SVDEBUG << "FeatureExtractionModelTransformer::FeatureExtractionModelTransformer: plugin " << pluginId << ", outputName " << m_transform.getOutput() << endl;
+
+    initialise();
+}
+
+static bool
+areTransformsSimilar(const Transform &t1, const Transform &t2)
+{
+    Transform t2o(t2);
+    t2o.setOutput(t1.getOutput());
+    return t1 == t2o;
+}
+
+bool
+FeatureExtractionModelTransformer::initialise()
+{
+    // All transforms must use the same plugin, parameters, and
+    // inputs: they can differ only in choice of plugin output. So we
+    // initialise based purely on the first transform in the list (but
+    // first check that they are actually similar as promised)
+
+    for (int j = 1; j < (int)m_transforms.size(); ++j) {
+        if (!areTransformsSimilar(m_transforms[0], m_transforms[j])) {
+            m_message = tr("Transforms supplied to a single FeatureExtractionModelTransformer instance must be similar in every respect except plugin output");
+            return false;
+        }
+    }
+
+    Transform primaryTransform = m_transforms[0];
+
+    QString pluginId = primaryTransform.getPluginIdentifier();
 
     FeatureExtractionPluginFactory *factory =
 	FeatureExtractionPluginFactory::instanceFor(pluginId);
 
     if (!factory) {
         m_message = tr("No factory available for feature extraction plugin id \"%1\" (unknown plugin type, or internal error?)").arg(pluginId);
-	return;
+	return false;
     }
 
     DenseTimeValueModel *input = getConformingInput();
     if (!input) {
         m_message = tr("Input model for feature extraction plugin \"%1\" is of wrong type (internal error?)").arg(pluginId);
-        return;
+        return false;
     }
 
     m_plugin = factory->instantiatePlugin(pluginId, input->getSampleRate());
     if (!m_plugin) {
         m_message = tr("Failed to instantiate plugin \"%1\"").arg(pluginId);
-	return;
+	return false;
     }
 
     TransformFactory::getInstance()->makeContextConsistentWithPlugin
-        (m_transform, m_plugin);
+        (primaryTransform, m_plugin);
 
     TransformFactory::getInstance()->setPluginParameters
-        (m_transform, m_plugin);
+        (primaryTransform, m_plugin);
 
     size_t channelCount = input->getChannelCount();
     if (m_plugin->getMaxChannelCount() < channelCount) {
@@ -84,34 +122,35 @@
             .arg(m_plugin->getMinChannelCount())
             .arg(m_plugin->getMaxChannelCount())
             .arg(input->getChannelCount());
-	return;
+	return false;
     }
 
     SVDEBUG << "Initialising feature extraction plugin with channels = "
-              << channelCount << ", step = " << m_transform.getStepSize()
-              << ", block = " << m_transform.getBlockSize() << endl;
+              << channelCount << ", step = " << primaryTransform.getStepSize()
+              << ", block = " << primaryTransform.getBlockSize() << endl;
 
     if (!m_plugin->initialise(channelCount,
-                              m_transform.getStepSize(),
-                              m_transform.getBlockSize())) {
+                              primaryTransform.getStepSize(),
+                              primaryTransform.getBlockSize())) {
 
-        size_t pstep = m_transform.getStepSize();
-        size_t pblock = m_transform.getBlockSize();
+        size_t pstep = primaryTransform.getStepSize();
+        size_t pblock = primaryTransform.getBlockSize();
 
-        m_transform.setStepSize(0);
-        m_transform.setBlockSize(0);
+///!!! hang on, this isn't right -- we're modifying a copy
+        primaryTransform.setStepSize(0);
+        primaryTransform.setBlockSize(0);
         TransformFactory::getInstance()->makeContextConsistentWithPlugin
-            (m_transform, m_plugin);
+            (primaryTransform, m_plugin);
 
-        if (m_transform.getStepSize() != pstep ||
-            m_transform.getBlockSize() != pblock) {
+        if (primaryTransform.getStepSize() != pstep ||
+            primaryTransform.getBlockSize() != pblock) {
             
             if (!m_plugin->initialise(channelCount,
-                                      m_transform.getStepSize(),
-                                      m_transform.getBlockSize())) {
+                                      primaryTransform.getStepSize(),
+                                      primaryTransform.getBlockSize())) {
 
                 m_message = tr("Failed to initialise feature extraction plugin \"%1\"").arg(pluginId);
-                return;
+                return false;
 
             } else {
 
@@ -119,22 +158,22 @@
                     .arg(pluginId)
                     .arg(pstep)
                     .arg(pblock)
-                    .arg(m_transform.getStepSize())
-                    .arg(m_transform.getBlockSize());
+                    .arg(primaryTransform.getStepSize())
+                    .arg(primaryTransform.getBlockSize());
             }
 
         } else {
 
             m_message = tr("Failed to initialise feature extraction plugin \"%1\"").arg(pluginId);
-            return;
+            return false;
         }
     }
 
-    if (m_transform.getPluginVersion() != "") {
+    if (primaryTransform.getPluginVersion() != "") {
         QString pv = QString("%1").arg(m_plugin->getPluginVersion());
-        if (pv != m_transform.getPluginVersion()) {
+        if (pv != primaryTransform.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(primaryTransform.getPluginVersion())
                 .arg(pluginId)
                 .arg(pv);
             if (m_message != "") {
@@ -149,77 +188,88 @@
 
     if (outputs.empty()) {
         m_message = tr("Plugin \"%1\" has no outputs").arg(pluginId);
-	return;
-    }
-    
-    for (size_t i = 0; i < outputs.size(); ++i) {
-//        SVDEBUG << "comparing output " << i << " name \"" << outputs[i].identifier << "\" with expected \"" << m_transform.getOutput() << "\"" << endl;
-	if (m_transform.getOutput() == "" ||
-            outputs[i].identifier == m_transform.getOutput().toStdString()) {
-	    m_outputNo = i;
-	    m_descriptor = new Vamp::Plugin::OutputDescriptor(outputs[i]);
-	    break;
-	}
+	return false;
     }
 
-    if (!m_descriptor) {
-        m_message = tr("Plugin \"%1\" has no output named \"%2\"")
-            .arg(pluginId)
-            .arg(m_transform.getOutput());
-	return;
+    for (int j = 0; j < (int)m_transforms.size(); ++j) {
+
+        for (int i = 0; i < (int)outputs.size(); ++i) {
+//        SVDEBUG << "comparing output " << i << " name \"" << outputs[i].identifier << "\" with expected \"" << m_transform.getOutput() << "\"" << endl;
+            if (m_transforms[j].getOutput() == "" ||
+                outputs[i].identifier == m_transforms[j].getOutput().toStdString()) {
+                m_outputNos.push_back(i);
+                m_descriptors.push_back(new Vamp::Plugin::OutputDescriptor(outputs[i]));
+                m_fixedRateFeatureNos.push_back(-1); // we increment before use
+                break;
+            }
+        }
+
+        if (m_descriptors.size() <= j) {
+            m_message = tr("Plugin \"%1\" has no output named \"%2\"")
+                .arg(pluginId)
+                .arg(m_transforms[j].getOutput());
+            return false;
+        }
     }
 
-    createOutputModel();
+    for (int j = 0; j < (int)m_transforms.size(); ++j) {
+        createOutputModels(j);
+    }
+
+    return true;
 }
 
 void
-FeatureExtractionModelTransformer::createOutputModel()
+FeatureExtractionModelTransformer::createOutputModels(int n)
 {
     DenseTimeValueModel *input = getConformingInput();
 
 //    cerr << "FeatureExtractionModelTransformer::createOutputModel: sample type " << m_descriptor->sampleType << ", rate " << m_descriptor->sampleRate << endl;
     
-    PluginRDFDescription description(m_transform.getPluginIdentifier());
-    QString outputId = m_transform.getOutput();
+    PluginRDFDescription description(m_transforms[n].getPluginIdentifier());
+    QString outputId = m_transforms[n].getOutput();
 
     int binCount = 1;
     float minValue = 0.0, maxValue = 0.0;
     bool haveExtents = false;
-    
-    if (m_descriptor->hasFixedBinCount) {
-	binCount = m_descriptor->binCount;
+    bool haveBinCount = m_descriptors[n]->hasFixedBinCount;
+
+    if (haveBinCount) {
+	binCount = m_descriptors[n]->binCount;
     }
 
+    m_needAdditionalModels[n] = false;
+
 //    cerr << "FeatureExtractionModelTransformer: output bin count "
 //	      << binCount << endl;
 
-    if (binCount > 0 && m_descriptor->hasKnownExtents) {
-	minValue = m_descriptor->minValue;
-	maxValue = m_descriptor->maxValue;
+    if (binCount > 0 && m_descriptors[n]->hasKnownExtents) {
+	minValue = m_descriptors[n]->minValue;
+	maxValue = m_descriptors[n]->maxValue;
         haveExtents = true;
     }
 
     size_t modelRate = input->getSampleRate();
     size_t modelResolution = 1;
 
-    if (m_descriptor->sampleType != 
+    if (m_descriptors[n]->sampleType != 
         Vamp::Plugin::OutputDescriptor::OneSamplePerStep) {
-        if (m_descriptor->sampleRate > input->getSampleRate()) {
+        if (m_descriptors[n]->sampleRate > input->getSampleRate()) {
             cerr << "WARNING: plugin reports output sample rate as "
-                      << m_descriptor->sampleRate << " (can't display features with finer resolution than the input rate of " << input->getSampleRate() << ")" << endl;
+                      << m_descriptors[n]->sampleRate << " (can't display features with finer resolution than the input rate of " << input->getSampleRate() << ")" << endl;
         }
     }
 
-    switch (m_descriptor->sampleType) {
+    switch (m_descriptors[n]->sampleType) {
 
     case Vamp::Plugin::OutputDescriptor::VariableSampleRate:
-	if (m_descriptor->sampleRate != 0.0) {
-	    modelResolution = size_t(modelRate / m_descriptor->sampleRate + 0.001);
+	if (m_descriptors[n]->sampleRate != 0.0) {
+	    modelResolution = size_t(modelRate / m_descriptors[n]->sampleRate + 0.001);
 	}
 	break;
 
     case Vamp::Plugin::OutputDescriptor::OneSamplePerStep:
-	modelResolution = m_transform.getStepSize();
+	modelResolution = m_transforms[n].getStepSize();
 	break;
 
     case Vamp::Plugin::OutputDescriptor::FixedSampleRate:
@@ -228,32 +278,32 @@
         //!!! the model rate to be the input model's rate, and adjust
         //!!! the resolution appropriately.  We can't properly display
         //!!! data with a higher resolution than the base model at all
-        if (m_descriptor->sampleRate > input->getSampleRate()) {
+        if (m_descriptors[n]->sampleRate > input->getSampleRate()) {
             modelResolution = 1;
         } else {
             modelResolution = size_t(round(input->getSampleRate() /
-                                           m_descriptor->sampleRate));
+                                           m_descriptors[n]->sampleRate));
         }
 	break;
     }
 
     bool preDurationPlugin = (m_plugin->getVampApiVersion() < 2);
 
+    Model *out = 0;
+
     if (binCount == 0 &&
-        (preDurationPlugin || !m_descriptor->hasDuration)) {
+        (preDurationPlugin || !m_descriptors[n]->hasDuration)) {
 
         // Anything with no value and no duration is an instant
 
-	m_output = new SparseOneDimensionalModel(modelRate, modelResolution,
-						 false);
-
+        out = new SparseOneDimensionalModel(modelRate, modelResolution, false);
         QString outputEventTypeURI = description.getOutputEventTypeURI(outputId);
-        m_output->setRDFTypeURI(outputEventTypeURI);
+        out->setRDFTypeURI(outputEventTypeURI);
 
     } else if ((preDurationPlugin && binCount > 1 &&
-                (m_descriptor->sampleType ==
+                (m_descriptors[n]->sampleType ==
                  Vamp::Plugin::OutputDescriptor::VariableSampleRate)) ||
-               (!preDurationPlugin && m_descriptor->hasDuration)) {
+               (!preDurationPlugin && m_descriptors[n]->hasDuration)) {
 
         // For plugins using the old v1 API without explicit duration,
         // we treat anything that has multiple bins (i.e. that has the
@@ -284,9 +334,9 @@
 
         // Regions do not have units of Hz or MIDI things (a sweeping
         // assumption!)
-        if (m_descriptor->unit == "Hz" ||
-            m_descriptor->unit.find("MIDI") != std::string::npos ||
-            m_descriptor->unit.find("midi") != std::string::npos) {
+        if (m_descriptors[n]->unit == "Hz" ||
+            m_descriptors[n]->unit.find("MIDI") != std::string::npos ||
+            m_descriptors[n]->unit.find("midi") != std::string::npos) {
             isNoteModel = true;
         }
 
@@ -294,7 +344,14 @@
         // problem of determining whether to use that here (if bin
         // count > 1).  But we don't.
 
-        if (isNoteModel) {
+        QSettings settings;
+        settings.beginGroup("Transformer");
+        bool flexi = settings.value("use-flexi-note-model", false).toBool();
+        settings.endGroup();
+
+        cerr << "flexi = " << flexi << endl;
+
+        if (isNoteModel && !flexi) {
 
             NoteModel *model;
             if (haveExtents) {
@@ -304,8 +361,21 @@
                 model = new NoteModel
                     (modelRate, modelResolution, false);
             }
-            model->setScaleUnits(m_descriptor->unit.c_str());
-            m_output = model;
+            model->setScaleUnits(m_descriptors[n]->unit.c_str());
+            out = model;
+
+        } else if (isNoteModel && flexi) {
+
+            FlexiNoteModel *model;
+            if (haveExtents) {
+                model = new FlexiNoteModel
+                    (modelRate, modelResolution, minValue, maxValue, false);
+            } else {
+                model = new FlexiNoteModel
+                    (modelRate, modelResolution, false);
+            }
+            model->setScaleUnits(m_descriptors[n]->unit.c_str());
+            out = model;
 
         } else {
 
@@ -317,15 +387,15 @@
                 model = new RegionModel
                     (modelRate, modelResolution, false);
             }
-            model->setScaleUnits(m_descriptor->unit.c_str());
-            m_output = model;
+            model->setScaleUnits(m_descriptors[n]->unit.c_str());
+            out = model;
         }
 
         QString outputEventTypeURI = description.getOutputEventTypeURI(outputId);
-        m_output->setRDFTypeURI(outputEventTypeURI);
+        out->setRDFTypeURI(outputEventTypeURI);
 
     } else if (binCount == 1 ||
-               (m_descriptor->sampleType == 
+               (m_descriptors[n]->sampleType == 
                 Vamp::Plugin::OutputDescriptor::VariableSampleRate)) {
 
         // Anything that is not a 1D, note, or interval model and that
@@ -333,10 +403,27 @@
         // model.
 
         // Anything that is not a 1D, note, or interval model and that
-        // has a variable sample rate is also treated as a sparse time
-        // value model regardless of its bin count, because we lack a
+        // has a variable sample rate is treated as a set of sparse
+        // time value models, one per output bin, because we lack a
         // sparse 3D model.
 
+        // Anything that is not a 1D, note, or interval model and that
+        // has a fixed sample rate but an unknown number of values per
+        // result is also treated as a set of sparse time value models.
+
+        // For sets of sparse time value models, we create a single
+        // model first as the "standard" output and then create models
+        // for bins 1+ in the additional model map (mapping the output
+        // descriptor to a list of models indexed by bin-1). But we
+        // don't create the additional models yet, as this case has to
+        // work even if the number of bins is unknown at this point --
+        // we create an additional model (copying its parameters from
+        // the default one) each time a new bin is encountered.
+
+        if (!haveBinCount || binCount > 1) {
+            m_needAdditionalModels[n] = true;
+        }
+
         SparseTimeValueModel *model;
         if (haveExtents) {
             model = new SparseTimeValueModel
@@ -347,12 +434,12 @@
         }
 
         Vamp::Plugin::OutputList outputs = m_plugin->getOutputDescriptors();
-        model->setScaleUnits(outputs[m_outputNo].unit.c_str());
+        model->setScaleUnits(outputs[m_outputNos[n]].unit.c_str());
 
-        m_output = model;
+        out = model;
 
         QString outputEventTypeURI = description.getOutputEventTypeURI(outputId);
-        m_output->setRDFTypeURI(outputEventTypeURI);
+        out->setRDFTypeURI(outputEventTypeURI);
 
     } else {
 
@@ -366,28 +453,95 @@
              EditableDenseThreeDimensionalModel::BasicMultirateCompression,
              false);
 
-	if (!m_descriptor->binNames.empty()) {
+	if (!m_descriptors[n]->binNames.empty()) {
 	    std::vector<QString> names;
-	    for (size_t i = 0; i < m_descriptor->binNames.size(); ++i) {
-		names.push_back(m_descriptor->binNames[i].c_str());
+	    for (size_t i = 0; i < m_descriptors[n]->binNames.size(); ++i) {
+		names.push_back(m_descriptors[n]->binNames[i].c_str());
 	    }
 	    model->setBinNames(names);
 	}
         
-        m_output = model;
+        out = model;
 
         QString outputSignalTypeURI = description.getOutputSignalTypeURI(outputId);
-        m_output->setRDFTypeURI(outputSignalTypeURI);
+        out->setRDFTypeURI(outputSignalTypeURI);
     }
 
-    if (m_output) m_output->setSourceModel(input);
+    if (out) {
+        out->setSourceModel(input);
+        m_outputs.push_back(out);
+    }
 }
 
 FeatureExtractionModelTransformer::~FeatureExtractionModelTransformer()
 {
 //    SVDEBUG << "FeatureExtractionModelTransformer::~FeatureExtractionModelTransformer()" << endl;
     delete m_plugin;
-    delete m_descriptor;
+    for (int j = 0; j < m_descriptors.size(); ++j) {
+        delete m_descriptors[j];
+    }
+}
+
+FeatureExtractionModelTransformer::Models
+FeatureExtractionModelTransformer::getAdditionalOutputModels()
+{
+    Models mm;
+    for (AdditionalModelMap::iterator i = m_additionalModels.begin();
+         i != m_additionalModels.end(); ++i) {
+        for (std::map<int, SparseTimeValueModel *>::iterator j =
+                 i->second.begin();
+             j != i->second.end(); ++j) {
+            SparseTimeValueModel *m = j->second;
+            if (m) mm.push_back(m);
+        }
+    }
+    return mm;
+}
+
+bool
+FeatureExtractionModelTransformer::willHaveAdditionalOutputModels()
+{
+    for (std::map<int, bool>::const_iterator i =
+             m_needAdditionalModels.begin(); 
+         i != m_needAdditionalModels.end(); ++i) {
+        if (i->second) return true;
+    }
+    return false;
+}
+
+SparseTimeValueModel *
+FeatureExtractionModelTransformer::getAdditionalModel(int n, int binNo)
+{
+//    std::cerr << "getAdditionalModel(" << n << ", " << binNo << ")" << std::endl;
+
+    if (binNo == 0) {
+        std::cerr << "Internal error: binNo == 0 in getAdditionalModel (should be using primary model)" << std::endl;
+        return 0;
+    }
+
+    if (!m_needAdditionalModels[n]) return 0;
+    if (!isOutput<SparseTimeValueModel>(n)) return 0;
+    if (m_additionalModels[n][binNo]) return m_additionalModels[n][binNo];
+
+    std::cerr << "getAdditionalModel(" << n << ", " << binNo << "): creating" << std::endl;
+
+    SparseTimeValueModel *baseModel = getConformingOutput<SparseTimeValueModel>(n);
+    if (!baseModel) return 0;
+
+    std::cerr << "getAdditionalModel(" << n << ", " << binNo << "): (from " << baseModel << ")" << std::endl;
+
+    SparseTimeValueModel *additional =
+        new SparseTimeValueModel(baseModel->getSampleRate(),
+                                 baseModel->getResolution(),
+                                 baseModel->getValueMinimum(),
+                                 baseModel->getValueMaximum(),
+                                 false);
+
+    additional->setScaleUnits(baseModel->getScaleUnits());
+    additional->setRDFTypeURI(baseModel->getRDFTypeURI());
+
+    m_additionalModels[n][binNo] = additional;
+    return additional;
 }
 
 DenseTimeValueModel *
@@ -409,10 +563,12 @@
     DenseTimeValueModel *input = getConformingInput();
     if (!input) return;
 
-    if (!m_output) return;
+    if (m_outputs.empty()) return;
+
+    Transform primaryTransform = m_transforms[0];
 
     while (!input->isReady() && !m_abandoned) {
-        SVDEBUG << "FeatureExtractionModelTransformer::run: Waiting for input model to be ready..." << endl;
+        cerr << "FeatureExtractionModelTransformer::run: Waiting for input model to be ready..." << endl;
         usleep(500000);
     }
     if (m_abandoned) return;
@@ -426,11 +582,11 @@
 
     float **buffers = new float*[channelCount];
     for (size_t ch = 0; ch < channelCount; ++ch) {
-	buffers[ch] = new float[m_transform.getBlockSize() + 2];
+	buffers[ch] = new float[primaryTransform.getBlockSize() + 2];
     }
 
-    size_t stepSize = m_transform.getStepSize();
-    size_t blockSize = m_transform.getBlockSize();
+    size_t stepSize = primaryTransform.getStepSize();
+    size_t blockSize = primaryTransform.getBlockSize();
 
     bool frequencyDomain = (m_plugin->getInputDomain() ==
                             Vamp::Plugin::FrequencyDomain);
@@ -441,7 +597,7 @@
             FFTModel *model = new FFTModel
                                   (getConformingInput(),
                                    channelCount == 1 ? m_input.getChannel() : ch,
-                                   m_transform.getWindowType(),
+                                   primaryTransform.getWindowType(),
                                    blockSize,
                                    stepSize,
                                    blockSize,
@@ -449,7 +605,9 @@
                                    StorageAdviser::PrecisionCritical);
             if (!model->isOK()) {
                 delete model;
-                setCompletion(100);
+                for (int j = 0; j < (int)m_outputNos.size(); ++j) {
+                    setCompletion(j, 100);
+                }
                 //!!! need a better way to handle this -- previously we were using a QMessageBox but that isn't an appropriate thing to do here either
                 throw AllocationFailed("Failed to create the FFT model for this feature extraction model transformer");
             }
@@ -461,8 +619,8 @@
     long startFrame = m_input.getModel()->getStartFrame();
     long   endFrame = m_input.getModel()->getEndFrame();
 
-    RealTime contextStartRT = m_transform.getStartTime();
-    RealTime contextDurationRT = m_transform.getDuration();
+    RealTime contextStartRT = primaryTransform.getStartTime();
+    RealTime contextDurationRT = primaryTransform.getDuration();
 
     long contextStart =
         RealTime::realTime2Frame(contextStartRT, sampleRate);
@@ -485,7 +643,9 @@
 
     long prevCompletion = 0;
 
-    setCompletion(0);
+    for (int j = 0; j < (int)m_outputNos.size(); ++j) {
+        setCompletion(j, 0);
+    }
 
     float *reals = 0;
     float *imaginaries = 0;
@@ -542,13 +702,17 @@
 
         if (m_abandoned) break;
 
-	for (size_t fi = 0; fi < features[m_outputNo].size(); ++fi) {
-	    Vamp::Plugin::Feature feature = features[m_outputNo][fi];
-	    addFeature(blockFrame, feature);
-	}
+        for (int j = 0; j < (int)m_outputNos.size(); ++j) {
+            for (size_t fi = 0; fi < features[m_outputNos[j]].size(); ++fi) {
+                Vamp::Plugin::Feature feature = features[m_outputNos[j]][fi];
+                addFeature(j, blockFrame, feature);
+            }
+        }
 
 	if (blockFrame == contextStart || completion > prevCompletion) {
-	    setCompletion(completion);
+            for (int j = 0; j < (int)m_outputNos.size(); ++j) {
+                setCompletion(j, completion);
+            }
 	    prevCompletion = completion;
 	}
 
@@ -558,13 +722,17 @@
     if (!m_abandoned) {
         Vamp::Plugin::FeatureSet features = m_plugin->getRemainingFeatures();
 
-        for (size_t fi = 0; fi < features[m_outputNo].size(); ++fi) {
-            Vamp::Plugin::Feature feature = features[m_outputNo][fi];
-            addFeature(blockFrame, feature);
+        for (int j = 0; j < (int)m_outputNos.size(); ++j) {
+            for (size_t fi = 0; fi < features[m_outputNos[j]].size(); ++fi) {
+                Vamp::Plugin::Feature feature = features[m_outputNos[j]][fi];
+                addFeature(j, blockFrame, feature);
+            }
         }
     }
 
-    setCompletion(100);
+    for (int j = 0; j < (int)m_outputNos.size(); ++j) {
+        setCompletion(j, 100);
+    }
 
     if (frequencyDomain) {
         for (size_t ch = 0; ch < channelCount; ++ch) {
@@ -636,7 +804,8 @@
 }
 
 void
-FeatureExtractionModelTransformer::addFeature(size_t blockFrame,
+FeatureExtractionModelTransformer::addFeature(int n,
+                                              size_t blockFrame,
                                               const Vamp::Plugin::Feature &feature)
 {
     size_t inputRate = m_input.getModel()->getSampleRate();
@@ -648,13 +817,13 @@
 //              << endl;
 
     int binCount = 1;
-    if (m_descriptor->hasFixedBinCount) {
-	binCount = m_descriptor->binCount;
+    if (m_descriptors[n]->hasFixedBinCount) {
+	binCount = m_descriptors[n]->binCount;
     }
 
     int frame = blockFrame;
 
-    if (m_descriptor->sampleType ==
+    if (m_descriptors[n]->sampleType ==
 	Vamp::Plugin::OutputDescriptor::VariableSampleRate) {
 
 	if (!feature.hasTimestamp) {
@@ -667,26 +836,23 @@
 	    frame = Vamp::RealTime::realTime2Frame(feature.timestamp, inputRate);
 	}
 
-    } else if (m_descriptor->sampleType ==
+    } else if (m_descriptors[n]->sampleType ==
 	       Vamp::Plugin::OutputDescriptor::FixedSampleRate) {
 
         if (!feature.hasTimestamp) {
-            ++m_fixedRateFeatureNo;
+            ++m_fixedRateFeatureNos[n];
         } else {
             RealTime ts(feature.timestamp.sec, feature.timestamp.nsec);
-            m_fixedRateFeatureNo =
-                lrint(ts.toDouble() * m_descriptor->sampleRate);
+            m_fixedRateFeatureNos[n] =
+                lrint(ts.toDouble() * m_descriptors[n]->sampleRate);
         }
 
 //        cerr << "m_fixedRateFeatureNo = " << m_fixedRateFeatureNo 
 //             << ", m_descriptor->sampleRate = " << m_descriptor->sampleRate
 //             << ", inputRate = " << inputRate
 //             << " giving frame = ";
-
-        frame = lrintf((m_fixedRateFeatureNo / m_descriptor->sampleRate)
+        frame = lrintf((m_fixedRateFeatureNos[n] / m_descriptors[n]->sampleRate)
                        * int(inputRate));
-
-//        cerr << frame << endl;
     }
 
     if (frame < 0) {
@@ -704,19 +870,19 @@
     // to, we instead test what sort of model the constructor decided
     // to create.
 
-    if (isOutput<SparseOneDimensionalModel>()) {
+    if (isOutput<SparseOneDimensionalModel>(n)) {
 
         SparseOneDimensionalModel *model =
-            getConformingOutput<SparseOneDimensionalModel>();
+            getConformingOutput<SparseOneDimensionalModel>(n);
 	if (!model) return;
 
         model->addPoint(SparseOneDimensionalModel::Point
                        (frame, feature.label.c_str()));
 	
-    } else if (isOutput<SparseTimeValueModel>()) {
+    } else if (isOutput<SparseTimeValueModel>(n)) {
 
 	SparseTimeValueModel *model =
-            getConformingOutput<SparseTimeValueModel>();
+            getConformingOutput<SparseTimeValueModel>(n);
 	if (!model) return;
 
         for (int i = 0; i < feature.values.size(); ++i) {
@@ -728,10 +894,20 @@
                 label = QString("[%1] %2").arg(i+1).arg(label);
             }
 
-            model->addPoint(SparseTimeValueModel::Point(frame, value, label));
+            SparseTimeValueModel *targetModel = model;
+
+            if (m_needAdditionalModels[n] && i > 0) {
+                targetModel = getAdditionalModel(n, i);
+                if (!targetModel) targetModel = model;
+//                std::cerr << "adding point to model " << targetModel
+//                          << " for output " << n << " bin " << i << std::endl;
+            }
+
+            targetModel->addPoint
+                (SparseTimeValueModel::Point(frame, value, label));
         }
 
-    } else if (isOutput<NoteModel>() || isOutput<RegionModel>()) {
+    } else if (isOutput<FlexiNoteModel>(n) || isOutput<NoteModel>(n) || isOutput<RegionModel>(n)) { //GF: Added Note Model
 
         int index = 0;
 
@@ -748,8 +924,8 @@
                 duration = feature.values[index++];
             }
         }
-        
-        if (isOutput<NoteModel>()) {
+
+        if (isOutput<FlexiNoteModel>(n)) { // GF: added for flexi note model
 
             float velocity = 100;
             if (feature.values.size() > index) {
@@ -758,14 +934,31 @@
             if (velocity < 0) velocity = 127;
             if (velocity > 127) velocity = 127;
 
-            NoteModel *model = getConformingOutput<NoteModel>();
+            FlexiNoteModel *model = getConformingOutput<FlexiNoteModel>(n);
+            if (!model) return;
+            model->addPoint(FlexiNoteModel::Point(frame, value, // value is pitch
+                                             lrintf(duration),
+                                             velocity / 127.f,
+                                             feature.label.c_str()));
+			// GF: end -- added for flexi note model
+        } else  if (isOutput<NoteModel>(n)) {
+
+            float velocity = 100;
+            if (feature.values.size() > index) {
+                velocity = feature.values[index++];
+            }
+            if (velocity < 0) velocity = 127;
+            if (velocity > 127) velocity = 127;
+
+            NoteModel *model = getConformingOutput<NoteModel>(n);
             if (!model) return;
             model->addPoint(NoteModel::Point(frame, value, // value is pitch
                                              lrintf(duration),
                                              velocity / 127.f,
                                              feature.label.c_str()));
         } else {
-            RegionModel *model = getConformingOutput<RegionModel>();
+
+            RegionModel *model = getConformingOutput<RegionModel>(n);
             if (!model) return;
 
             if (feature.hasDuration && !feature.values.empty()) {
@@ -791,20 +984,20 @@
             }
         }
 	
-    } else if (isOutput<EditableDenseThreeDimensionalModel>()) {
+    } else if (isOutput<EditableDenseThreeDimensionalModel>(n)) {
 	
 	DenseThreeDimensionalModel::Column values =
             DenseThreeDimensionalModel::Column::fromStdVector(feature.values);
 	
 	EditableDenseThreeDimensionalModel *model =
-            getConformingOutput<EditableDenseThreeDimensionalModel>();
+            getConformingOutput<EditableDenseThreeDimensionalModel>(n);
 	if (!model) return;
 
 //        cerr << "(note: model resolution = " << model->getResolution() << ")"
 //             << endl;
 
-        if (!feature.hasTimestamp && m_fixedRateFeatureNo >= 0) {
-            model->setColumn(m_fixedRateFeatureNo, values);
+        if (!feature.hasTimestamp && m_fixedRateFeatureNos[n] >= 0) {
+            model->setColumn(m_fixedRateFeatureNos[n], values);
         } else {
             model->setColumn(frame / model->getResolution(), values);
         }
@@ -815,46 +1008,47 @@
 }
 
 void
-FeatureExtractionModelTransformer::setCompletion(int completion)
+FeatureExtractionModelTransformer::setCompletion(int n, int completion)
 {
-    int binCount = 1;
-    if (m_descriptor->hasFixedBinCount) {
-	binCount = m_descriptor->binCount;
-    }
-
 //    SVDEBUG << "FeatureExtractionModelTransformer::setCompletion("
 //              << completion << ")" << endl;
 
-    if (isOutput<SparseOneDimensionalModel>()) {
+    if (isOutput<SparseOneDimensionalModel>(n)) {
 
 	SparseOneDimensionalModel *model =
-            getConformingOutput<SparseOneDimensionalModel>();
+            getConformingOutput<SparseOneDimensionalModel>(n);
 	if (!model) return;
 	model->setCompletion(completion, true);
 
-    } else if (isOutput<SparseTimeValueModel>()) {
+    } else if (isOutput<SparseTimeValueModel>(n)) {
 
 	SparseTimeValueModel *model =
-            getConformingOutput<SparseTimeValueModel>();
+            getConformingOutput<SparseTimeValueModel>(n);
 	if (!model) return;
 	model->setCompletion(completion, true);
 
-    } else if (isOutput<NoteModel>()) {
+    } else if (isOutput<NoteModel>(n)) {
 
-	NoteModel *model = getConformingOutput<NoteModel>();
+	NoteModel *model = getConformingOutput<NoteModel>(n);
+	if (!model) return;
+	model->setCompletion(completion, true);
+	
+	} else if (isOutput<FlexiNoteModel>(n)) {
+
+	FlexiNoteModel *model = getConformingOutput<FlexiNoteModel>(n);
 	if (!model) return;
 	model->setCompletion(completion, true);
 
-    } else if (isOutput<RegionModel>()) {
+    } else if (isOutput<RegionModel>(n)) {
 
-	RegionModel *model = getConformingOutput<RegionModel>();
+	RegionModel *model = getConformingOutput<RegionModel>(n);
 	if (!model) return;
 	model->setCompletion(completion, true);
 
-    } else if (isOutput<EditableDenseThreeDimensionalModel>()) {
+    } else if (isOutput<EditableDenseThreeDimensionalModel>(n)) {
 
 	EditableDenseThreeDimensionalModel *model =
-            getConformingOutput<EditableDenseThreeDimensionalModel>();
+            getConformingOutput<EditableDenseThreeDimensionalModel>(n);
 	if (!model) return;
 	model->setCompletion(completion, true); //!!!m_context.updates);
     }
--- a/transform/FeatureExtractionModelTransformer.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/transform/FeatureExtractionModelTransformer.h	Wed May 07 15:14:17 2014 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _FEATURE_EXTRACTION_PLUGIN_TRANSFORMER_H_
-#define _FEATURE_EXTRACTION_PLUGIN_TRANSFORMER_H_
+#ifndef _FEATURE_EXTRACTION_MODEL_TRANSFORMER_H_
+#define _FEATURE_EXTRACTION_MODEL_TRANSFORMER_H_
 
 #include "ModelTransformer.h"
 
@@ -23,8 +23,10 @@
 #include <vamp-hostsdk/Plugin.h>
 
 #include <iostream>
+#include <map>
 
 class DenseTimeValueModel;
+class SparseTimeValueModel;
 
 class FeatureExtractionModelTransformer : public ModelTransformer
 {
@@ -33,22 +35,41 @@
 public:
     FeatureExtractionModelTransformer(Input input,
                                       const Transform &transform);
+
+    // Obtain outputs for a set of transforms that all use the same
+    // plugin and input (but with different outputs). i.e. run the
+    // plugin once only and collect more than one output from it.
+    FeatureExtractionModelTransformer(Input input,
+                                      const Transforms &relatedTransforms);
+
     virtual ~FeatureExtractionModelTransformer();
 
+    // ModelTransformer method, retrieve the additional models
+    Models getAdditionalOutputModels();
+    bool willHaveAdditionalOutputModels();
+
 protected:
+    bool initialise();
+
     virtual void run();
 
     Vamp::Plugin *m_plugin;
-    Vamp::Plugin::OutputDescriptor *m_descriptor;
-    int m_fixedRateFeatureNo; // to assign times to FixedSampleRate features
-    int m_outputNo;
+    std::vector<Vamp::Plugin::OutputDescriptor *> m_descriptors; // per transform
+    std::vector<int> m_fixedRateFeatureNos; // to assign times to FixedSampleRate features
+    std::vector<int> m_outputNos; // list of plugin output indexes required for this group of transforms
 
-    void createOutputModel();
+    void createOutputModels(int n);
 
-    void addFeature(size_t blockFrame,
+    std::map<int, bool> m_needAdditionalModels; // transformNo -> necessity
+    typedef std::map<int, std::map<int, SparseTimeValueModel *> > AdditionalModelMap;
+    AdditionalModelMap m_additionalModels;
+    SparseTimeValueModel *getAdditionalModel(int transformNo, int binNo);
+
+    void addFeature(int n,
+                    size_t blockFrame,
 		    const Vamp::Plugin::Feature &feature);
 
-    void setCompletion(int);
+    void setCompletion(int, int);
 
     void getFrames(int channelCount, long startFrame, long size,
                    float **buffer);
@@ -57,16 +78,21 @@
 
     DenseTimeValueModel *getConformingInput();
 
-    template <typename ModelClass> bool isOutput() {
-        return dynamic_cast<ModelClass *>(m_output) != 0;
+    template <typename ModelClass> bool isOutput(int n) {
+        return dynamic_cast<ModelClass *>(m_outputs[n]) != 0;
     }
 
-    template <typename ModelClass> ModelClass *getConformingOutput() {
-	ModelClass *mc = dynamic_cast<ModelClass *>(m_output);
-	if (!mc) {
-	    std::cerr << "FeatureExtractionModelTransformer::getOutput: Output model not conformable" << std::endl;
-	}
-	return mc;
+    template <typename ModelClass> ModelClass *getConformingOutput(int n) {
+        if ((int)m_outputs.size() > n) {
+            ModelClass *mc = dynamic_cast<ModelClass *>(m_outputs[n]);
+            if (!mc) {
+                std::cerr << "FeatureExtractionModelTransformer::getOutput: Output model not conformable" << std::endl;
+            }
+            return mc;
+        } else {
+            std::cerr << "FeatureExtractionModelTransformer::getOutput: No such output number " << n << std::endl;
+            return 0;
+        }
     }
 };
 
--- a/transform/ModelTransformer.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/transform/ModelTransformer.cpp	Wed May 07 15:14:17 2014 +0100
@@ -16,10 +16,19 @@
 #include "ModelTransformer.h"
 
 ModelTransformer::ModelTransformer(Input input, const Transform &transform) :
-    m_transform(transform),
     m_input(input),
-    m_output(0),
     m_detached(false),
+    m_detachedAdd(false),
+    m_abandoned(false)
+{
+    m_transforms.push_back(transform);
+}
+
+ModelTransformer::ModelTransformer(Input input, const Transforms &transforms) :
+    m_transforms(transforms),
+    m_input(input),
+    m_detached(false),
+    m_detachedAdd(false),
     m_abandoned(false)
 {
 }
@@ -28,6 +37,13 @@
 {
     m_abandoned = true;
     wait();
-    if (!m_detached) delete m_output;
+    if (!m_detached) {
+        Models mine = getOutputModels();
+        foreach (Model *m, mine) delete m;
+    }
+    if (!m_detachedAdd) {
+        Models mine = getAdditionalOutputModels();
+        foreach (Model *m, mine) delete m;
+    }
 }
 
--- a/transform/ModelTransformer.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/transform/ModelTransformer.h	Wed May 07 15:14:17 2014 +0100
@@ -40,6 +40,8 @@
 public:
     virtual ~ModelTransformer();
 
+    typedef std::vector<Model *> Models;
+
     class Input {
     public:
         Input(Model *m) : m_model(m), m_channel(-1) { }
@@ -76,18 +78,47 @@
     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.
+     * Return the set of output models created by the transform or
+     * transforms.  Returns an empty list if any transform could not
+     * be initialised; an error message may be available via
+     * getMessage() in this situation.
      */
-    Model *getOutputModel() { return m_output; }
+    Models getOutputModels() { return m_outputs; }
 
     /**
-     * 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.
+     * Return the set of output models, also detaching them from the
+     * transformer so that they will not be deleted when the
+     * transformer is.  The caller takes ownership of the models.
      */
-    Model *detachOutputModel() { m_detached = true; return m_output; }
+    Models detachOutputModels() { 
+        m_detached = true; 
+        return getOutputModels(); 
+    }
+
+    /**
+     * Return any additional models that were created during
+     * processing. This might happen if, for example, a transform was
+     * configured to split a multi-bin output into separate single-bin
+     * models as it processed. These should not be queried until after
+     * the transform has completed.
+     */
+    virtual Models getAdditionalOutputModels() { return Models(); }
+
+    /**
+     * Return true if the current transform is one that may produce
+     * additional models (to be retrieved through
+     * getAdditionalOutputModels above).
+     */
+    virtual bool willHaveAdditionalOutputModels() { return false; }
+
+    /**
+     * Return the set of additional models, also detaching them from
+     * the transformer.  The caller takes ownership of the models.
+     */
+    virtual Models detachAdditionalOutputModels() { 
+        m_detachedAdd = true;
+        return getAdditionalOutputModels();
+    }
 
     /**
      * Return a warning or error message.  If getOutputModel returned
@@ -99,11 +130,13 @@
 
 protected:
     ModelTransformer(Input input, const Transform &transform);
+    ModelTransformer(Input input, const Transforms &transforms);
 
-    Transform m_transform;
+    Transforms m_transforms;
     Input m_input; // I don't own the model in this
-    Model *m_output; // I own this, unless...
+    Models m_outputs; // I own this, unless...
     bool m_detached; // ... this is true.
+    bool m_detachedAdd;
     bool m_abandoned;
     QString m_message;
 };
--- a/transform/ModelTransformerFactory.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/transform/ModelTransformerFactory.cpp	Wed May 07 15:14:17 2014 +0100
@@ -35,6 +35,8 @@
 
 #include <QRegExp>
 
+using std::vector;
+
 ModelTransformerFactory *
 ModelTransformerFactory::m_instance = new ModelTransformerFactory;
 
@@ -163,63 +165,85 @@
 }
 
 ModelTransformer *
-ModelTransformerFactory::createTransformer(const Transform &transform,
+ModelTransformerFactory::createTransformer(const Transforms &transforms,
                                            const ModelTransformer::Input &input)
 {
     ModelTransformer *transformer = 0;
 
-    QString id = transform.getPluginIdentifier();
+    QString id = transforms[0].getPluginIdentifier();
 
     if (FeatureExtractionPluginFactory::instanceFor(id)) {
 
         transformer =
-            new FeatureExtractionModelTransformer(input, transform);
+            new FeatureExtractionModelTransformer(input, transforms);
 
     } else if (RealTimePluginFactory::instanceFor(id)) {
 
         transformer =
-            new RealTimeEffectModelTransformer(input, transform);
+            new RealTimeEffectModelTransformer(input, transforms[0]);
 
     } else {
         SVDEBUG << "ModelTransformerFactory::createTransformer: Unknown transform \""
-                  << transform.getIdentifier() << "\"" << endl;
+                  << transforms[0].getIdentifier() << "\"" << endl;
         return transformer;
     }
 
-    if (transformer) transformer->setObjectName(transform.getIdentifier());
+    if (transformer) transformer->setObjectName(transforms[0].getIdentifier());
     return transformer;
 }
 
 Model *
 ModelTransformerFactory::transform(const Transform &transform,
                                    const ModelTransformer::Input &input,
-                                   QString &message)
+                                   QString &message,
+                                   AdditionalModelHandler *handler) 
 {
     SVDEBUG << "ModelTransformerFactory::transform: Constructing transformer with input model " << input.getModel() << endl;
 
-    ModelTransformer *t = createTransformer(transform, input);
-    if (!t) return 0;
+    Transforms transforms;
+    transforms.push_back(transform);
+    vector<Model *> mm = transformMultiple(transforms, input, message, handler);
+    if (mm.empty()) return 0;
+    else return mm[0];
+}
+
+vector<Model *>
+ModelTransformerFactory::transformMultiple(const Transforms &transforms,
+                                           const ModelTransformer::Input &input,
+                                           QString &message,
+                                           AdditionalModelHandler *handler) 
+{
+    SVDEBUG << "ModelTransformerFactory::transformMultiple: Constructing transformer with input model " << input.getModel() << endl;
+    
+    ModelTransformer *t = createTransformer(transforms, input);
+    if (!t) return vector<Model *>();
+
+    if (handler) {
+        m_handlers[t] = handler;
+    }
+
+    m_runningTransformers.insert(t);
 
     connect(t, SIGNAL(finished()), this, SLOT(transformerFinished()));
 
-    m_runningTransformers.insert(t);
+    t->start();
+    vector<Model *> models = t->detachOutputModels();
 
-    t->start();
-    Model *model = t->detachOutputModel();
-
-    if (model) {
+    if (!models.empty()) {
         QString imn = input.getModel()->objectName();
         QString trn =
             TransformFactory::getInstance()->getTransformFriendlyName
-            (transform.getIdentifier());
-        if (imn != "") {
-            if (trn != "") {
-                model->setObjectName(tr("%1: %2").arg(imn).arg(trn));
-            } else {
-                model->setObjectName(imn);
+            (transforms[0].getIdentifier());
+        for (int i = 0; i < models.size(); ++i) {
+            if (imn != "") {
+                if (trn != "") {
+                    models[i]->setObjectName(tr("%1: %2").arg(imn).arg(trn));
+                } else {
+                    models[i]->setObjectName(imn);
+                }
+            } else if (trn != "") {
+                models[i]->setObjectName(trn);
             }
-        } else if (trn != "") {
-            model->setObjectName(trn);
         }
     } else {
         t->wait();
@@ -227,7 +251,7 @@
 
     message = t->getMessage();
 
-    return model;
+    return models;
 }
 
 void
@@ -252,6 +276,16 @@
 
     m_runningTransformers.erase(transformer);
 
+    if (m_handlers.find(transformer) != m_handlers.end()) {
+        if (transformer->willHaveAdditionalOutputModels()) {
+            vector<Model *> mm = transformer->detachAdditionalOutputModels();
+            m_handlers[transformer]->moreModelsAvailable(mm);
+        } else {
+            m_handlers[transformer]->noMoreModelsAvailable();
+        }
+        m_handlers.erase(transformer);
+    }
+
     transformer->wait(); // unnecessary but reassuring
     delete transformer;
 }
@@ -266,8 +300,13 @@
 
         ModelTransformer *t = *i;
 
-        if (t->getInputModel() == m || t->getOutputModel() == m) {
+        if (t->getInputModel() == m) {
             affected.insert(t);
+        } else {
+            vector<Model *> mm = t->getOutputModels();
+            for (int i = 0; i < (int)mm.size(); ++i) {
+                if (mm[i] == m) affected.insert(t);
+            }
         }
     }
 
--- a/transform/ModelTransformerFactory.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/transform/ModelTransformerFactory.h	Wed May 07 15:14:17 2014 +0100
@@ -18,6 +18,7 @@
 
 #include "Transform.h"
 #include "TransformDescription.h"
+#include "FeatureExtractionModelTransformer.h"
 
 #include "ModelTransformer.h"
 
@@ -26,6 +27,7 @@
 #include <QMap>
 #include <map>
 #include <set>
+#include <vector>
 
 class AudioPlaySource;
 
@@ -68,6 +70,15 @@
                                  size_t startFrame = 0,
                                  size_t duration = 0,
                                  UserConfigurator *configurator = 0);
+
+    class AdditionalModelHandler {
+    public:
+        virtual ~AdditionalModelHandler() { }
+
+        // Exactly one of these functions will be called
+        virtual void moreModelsAvailable(std::vector<Model *> models) = 0;
+        virtual void noMoreModelsAvailable() = 0;
+    };
     
     /**
      * Return the output model resulting from applying the named
@@ -80,12 +91,54 @@
      * problem occurs, return 0.  Set message if there is any error or
      * warning to report.
      * 
+     * Some transforms may return additional models at the end of
+     * processing. (For example, a transform that splits an output
+     * into multiple one-per-bin models.) If an additionalModelHandler
+     * is provided here, its moreModelsAvailable method will be called
+     * when those models become available, and ownership of those
+     * models will be transferred to the handler. Otherwise (if the
+     * handler is null) any such models will be discarded.
+     *
      * The returned model is owned by the caller and must be deleted
      * when no longer needed.
      */
     Model *transform(const Transform &transform,
                      const ModelTransformer::Input &input,
-                     QString &message);
+                     QString &message,
+                     AdditionalModelHandler *handler = 0);
+
+    /**
+     * Return the multiple output models resulting from applying the
+     * named transforms to the given input model.  The transforms may
+     * differ only in output identifier for the plugin: they must all
+     * use the same plugin, parameters, and programs. The plugin will
+     * be run once only, but more than one output will be harvested
+     * (as appropriate). Models will be returned in the same order as
+     * the transforms were given. The plugin may still be working in
+     * the background when the model is returned; check the output
+     * models' isReady completion statuses for more details.
+     *
+     * If a transform is unknown or the transforms are insufficiently
+     * closely related or the input model is not an appropriate type
+     * for the given transform, or if some other problem occurs,
+     * return 0.  Set message if there is any error or warning to
+     * report.
+     *
+     * Some transforms may return additional models at the end of
+     * processing. (For example, a transform that splits an output
+     * into multiple one-per-bin models.) If an additionalModelHandler
+     * is provided here, its moreModelsAvailable method will be called
+     * when those models become available, and ownership of those
+     * models will be transferred to the handler. Otherwise (if the
+     * handler is null) any such models will be discarded.
+     *
+     * The returned models are owned by the caller and must be deleted
+     * when no longer needed.
+     */
+    std::vector<Model *> transformMultiple(const Transforms &transform,
+                                           const ModelTransformer::Input &input,
+                                           QString &message,
+                                           AdditionalModelHandler *handler = 0);
 
 protected slots:
     void transformerFinished();
@@ -93,7 +146,7 @@
     void modelAboutToBeDeleted(Model *);
 
 protected:
-    ModelTransformer *createTransformer(const Transform &transform,
+    ModelTransformer *createTransformer(const Transforms &transforms,
                                         const ModelTransformer::Input &input);
 
     typedef std::map<TransformId, QString> TransformerConfigurationMap;
@@ -102,6 +155,9 @@
     typedef std::set<ModelTransformer *> TransformerSet;
     TransformerSet m_runningTransformers;
 
+    typedef std::map<ModelTransformer *, AdditionalModelHandler *> HandlerMap;
+    HandlerMap m_handlers;
+
     static ModelTransformerFactory *m_instance;
 };
 
--- a/transform/RealTimeEffectModelTransformer.cpp	Sat Apr 26 23:05:32 2014 +0100
+++ b/transform/RealTimeEffectModelTransformer.cpp	Wed May 07 15:14:17 2014 +0100
@@ -30,10 +30,16 @@
 #include <iostream>
 
 RealTimeEffectModelTransformer::RealTimeEffectModelTransformer(Input in,
-                                                               const Transform &transform) :
-    ModelTransformer(in, transform),
+                                                               const Transform &t) :
+    ModelTransformer(in, t),
     m_plugin(0)
 {
+    Transform transform(t);
+    if (!transform.getBlockSize()) {
+        transform.setBlockSize(1024);
+        m_transforms[0] = transform;
+    }
+
     m_units = TransformFactory::getInstance()->getTransformUnits
         (transform.getIdentifier());
     m_outputNo =
@@ -41,8 +47,6 @@
 
     QString pluginId = transform.getPluginIdentifier();
 
-    if (!m_transform.getBlockSize()) m_transform.setBlockSize(1024);
-
 //    SVDEBUG << "RealTimeEffectModelTransformer::RealTimeEffectModelTransformer: plugin " << pluginId << ", output " << output << endl;
 
     RealTimePluginFactory *factory =
@@ -59,16 +63,16 @@
 
     m_plugin = factory->instantiatePlugin(pluginId, 0, 0,
                                           input->getSampleRate(),
-                                          m_transform.getBlockSize(),
+                                          transform.getBlockSize(),
                                           input->getChannelCount());
 
     if (!m_plugin) {
 	cerr << "RealTimeEffectModelTransformer: Failed to instantiate plugin \""
-		  << pluginId << "\"" << endl;
+             << pluginId << "\"" << endl;
 	return;
     }
 
-    TransformFactory::getInstance()->setPluginParameters(m_transform, m_plugin);
+    TransformFactory::getInstance()->setPluginParameters(transform, m_plugin);
 
     if (m_outputNo >= 0 &&
         m_outputNo >= int(m_plugin->getControlOutputCount())) {
@@ -86,16 +90,16 @@
         WritableWaveFileModel *model = new WritableWaveFileModel
             (input->getSampleRate(), outputChannels);
 
-        m_output = model;
+        m_outputs.push_back(model);
 
     } else {
 	
         SparseTimeValueModel *model = new SparseTimeValueModel
-            (input->getSampleRate(), m_transform.getBlockSize(), 0.0, 0.0, false);
+            (input->getSampleRate(), transform.getBlockSize(), 0.0, 0.0, false);
 
         if (m_units != "") model->setScaleUnits(m_units);
 
-        m_output = model;
+        m_outputs.push_back(model);
     }
 }
 
@@ -127,8 +131,8 @@
     }
     if (m_abandoned) return;
 
-    SparseTimeValueModel *stvm = dynamic_cast<SparseTimeValueModel *>(m_output);
-    WritableWaveFileModel *wwfm = dynamic_cast<WritableWaveFileModel *>(m_output);
+    SparseTimeValueModel *stvm = dynamic_cast<SparseTimeValueModel *>(m_outputs[0]);
+    WritableWaveFileModel *wwfm = dynamic_cast<WritableWaveFileModel *>(m_outputs[0]);
     if (!stvm && !wwfm) return;
 
     if (stvm && (m_outputNo >= int(m_plugin->getControlOutputCount()))) return;
@@ -143,9 +147,11 @@
 
     long startFrame = m_input.getModel()->getStartFrame();
     long   endFrame = m_input.getModel()->getEndFrame();
+
+    Transform transform = m_transforms[0];
     
-    RealTime contextStartRT = m_transform.getStartTime();
-    RealTime contextDurationRT = m_transform.getDuration();
+    RealTime contextStartRT = transform.getStartTime();
+    RealTime contextDurationRT = transform.getDuration();
 
     long contextStart =
         RealTime::realTime2Frame(contextStartRT, sampleRate);
--- a/transform/RealTimeEffectModelTransformer.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/transform/RealTimeEffectModelTransformer.h	Wed May 07 15:14:17 2014 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _REAL_TIME_PLUGIN_TRANSFORMER_H_
-#define _REAL_TIME_PLUGIN_TRANSFORMER_H_
+#ifndef _REAL_TIME_EFFECT_TRANSFORMER_H_
+#define _REAL_TIME_EFFECT_TRANSFORMER_H_
 
 #include "ModelTransformer.h"
 #include "plugin/RealTimePluginInstance.h"
--- a/transform/Transform.h	Sat Apr 26 23:05:32 2014 +0100
+++ b/transform/Transform.h	Wed May 07 15:14:17 2014 +0100
@@ -25,6 +25,7 @@
 #include <QString>
 
 #include <map>
+#include <vector>
 
 typedef QString TransformId;
 
@@ -196,5 +197,7 @@
     float m_sampleRate;
 };
 
+typedef std::vector<Transform> Transforms;
+
 #endif