changeset 857:cc3f78e89df0 tonioni

Merge from default branch
author Chris Cannam
date Wed, 04 Dec 2013 18:30:49 +0000
parents 8593a3fe50ac (diff) c2a399f93843 (current diff)
children 6b77fcc39723
files
diffstat 21 files changed, 902 insertions(+), 319 deletions(-) [+]
line wrap: on
line diff
--- a/base/PlayParameterRepository.cpp	Wed Dec 04 16:47:53 2013 +0000
+++ b/base/PlayParameterRepository.cpp	Wed Dec 04 18:30:49 2013 +0000
@@ -35,14 +35,14 @@
 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;
@@ -62,9 +62,8 @@
         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;
     }
 }    
 
--- a/data/fft/FFTDataServer.cpp	Wed Dec 04 16:47:53 2013 +0000
+++ b/data/fft/FFTDataServer.cpp	Wed Dec 04 18:30:49 2013 +0000
@@ -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	Wed Dec 04 16:47:53 2013 +0000
+++ b/data/fileio/CoreAudioFileReader.cpp	Wed Dec 04 18:30:49 2013 +0000
@@ -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/MIDIFileWriter.cpp	Wed Dec 04 16:47:53 2013 +0000
+++ b/data/fileio/MIDIFileWriter.cpp	Wed Dec 04 18:30:49 2013 +0000
@@ -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	Wed Dec 04 16:47:53 2013 +0000
+++ b/data/fileio/MIDIFileWriter.h	Wed Dec 04 18:30:49 2013 +0000
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/FlexiNoteModel.h	Wed Dec 04 18:30:49 2013 +0000
@@ -0,0 +1,276 @@
+/* -*- 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 getDefaultPlayPluginId() const
+    {
+        return "dssi:_builtin:sample_player";
+    }
+
+    virtual QString getDefaultPlayPluginConfiguration() const
+    {
+        return "<plugin program=\"click\"/>";
+    }
+
+    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=\"note\" 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/NoteData.h	Wed Dec 04 18:30:49 2013 +0000
@@ -0,0 +1,43 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This 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>
+
+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
+    int frequency; // Hz, to be used if isMidiPitchQuantized false
+    bool isMidiPitchQuantized;
+    int velocity;  // MIDI-style 0-127
+};
+
+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	Wed Dec 04 16:47:53 2013 +0000
+++ b/data/model/NoteModel.h	Wed Dec 04 18:30:49 2013 +0000
@@ -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
     
@@ -219,6 +221,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	Wed Dec 04 16:47:53 2013 +0000
+++ b/data/model/SparseOneDimensionalModel.h	Wed Dec 04 18:30:49 2013 +0000
@@ -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
     
@@ -181,6 +183,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/plugin/DSSIPluginInstance.cpp	Wed Dec 04 16:47:53 2013 +0000
+++ b/plugin/DSSIPluginInstance.cpp	Wed Dec 04 18:30:49 2013 +0000
@@ -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	Wed Dec 04 16:47:53 2013 +0000
+++ b/plugin/plugins/SamplePlayer.cpp	Wed Dec 04 18:30:49 2013 +0000
@@ -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	Wed Dec 04 16:47:53 2013 +0000
+++ b/svcore.pro	Wed Dec 04 18:30:49 2013 +0000
@@ -155,6 +155,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	Wed Dec 04 16:47:53 2013 +0000
+++ b/transform/FeatureExtractionModelTransformer.cpp	Wed Dec 04 18:30:49 2013 +0000
@@ -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"
@@ -37,42 +38,81 @@
 #include <iostream>
 
 FeatureExtractionModelTransformer::FeatureExtractionModelTransformer(Input in,
-                                                                     const Transform &transform) :
+                                                                     const Transform &transform,
+                                                                     const PreferredOutputModel outputmodel) :
     ModelTransformer(in, transform),
     m_plugin(0),
-    m_descriptor(0),
-    m_outputNo(0),
-    m_fixedRateFeatureNo(-1) // we increment before use
+    m_preferredOutputModel(outputmodel)
 {
 //    SVDEBUG << "FeatureExtractionModelTransformer::FeatureExtractionModelTransformer: plugin " << pluginId << ", outputName " << m_transform.getOutput() << endl;
 
-    QString pluginId = transform.getPluginIdentifier();
+    initialise();
+}
+
+FeatureExtractionModelTransformer::FeatureExtractionModelTransformer(Input in,
+                                                                     const Transforms &transforms,
+                                                                     const PreferredOutputModel outputmodel) :
+    ModelTransformer(in, transforms),
+    m_plugin(0),
+    m_preferredOutputModel(outputmodel)
+{
+//    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 +124,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 +160,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 +190,85 @@
 
     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) {
+        createOutputModel(j);
+    }
+
+    return true;
 }
 
 void
-FeatureExtractionModelTransformer::createOutputModel()
+FeatureExtractionModelTransformer::createOutputModel(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;
+    if (m_descriptors[n]->hasFixedBinCount) {
+	binCount = m_descriptors[n]->binCount;
     }
 
 //    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,33 +277,33 @@
         //!!! 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
-//	modelRate = size_t(m_descriptor->sampleRate + 0.001);
-        if (m_descriptor->sampleRate > input->getSampleRate()) {
+//	modelRate = size_t(m_descriptors[n]->sampleRate + 0.001);
+        if (m_descriptors[n]->sampleRate > input->getSampleRate()) {
             modelResolution = 1;
         } else {
             modelResolution = size_t(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
@@ -285,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;
         }
 
@@ -295,18 +344,28 @@
         // problem of determining whether to use that here (if bin
         // count > 1).  But we don't.
 
-        if (isNoteModel) {
+		if (isNoteModel && m_preferredOutputModel == NoteOutputModel) {
 
             NoteModel *model;
             if (haveExtents) {
-                model = new NoteModel
-                    (modelRate, modelResolution, minValue, maxValue, false);
+	            model = new NoteModel (modelRate, modelResolution, minValue, maxValue, false);
             } else {
-                model = new NoteModel
-                    (modelRate, modelResolution, false);
+	            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;
+
+		// GF: FlexiNoteModel is selected if the m_preferredOutputModel is set
+        } else if (isNoteModel && m_preferredOutputModel == FlexiNoteOutputModel) {
+
+            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 {
 
@@ -318,15 +377,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
@@ -348,12 +407,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 {
 
@@ -367,28 +426,33 @@
              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];
+    }
 }
 
 DenseTimeValueModel *
@@ -410,7 +474,9 @@
     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;
@@ -427,11 +493,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);
@@ -442,7 +508,7 @@
             FFTModel *model = new FFTModel
                                   (getConformingInput(),
                                    channelCount == 1 ? m_input.getChannel() : ch,
-                                   m_transform.getWindowType(),
+                                   primaryTransform.getWindowType(),
                                    blockSize,
                                    stepSize,
                                    blockSize,
@@ -450,7 +516,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");
             }
@@ -462,8 +530,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);
@@ -486,7 +554,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;
@@ -543,13 +613,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;
 	}
 
@@ -559,13 +633,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) {
@@ -637,8 +715,9 @@
 }
 
 void
-FeatureExtractionModelTransformer::addFeature(size_t blockFrame,
-					     const Vamp::Plugin::Feature &feature)
+FeatureExtractionModelTransformer::addFeature(int n,
+                                              size_t blockFrame,
+                                              const Vamp::Plugin::Feature &feature)
 {
     size_t inputRate = m_input.getModel()->getSampleRate();
 
@@ -649,13 +728,13 @@
 //              << endl;
 
     int binCount = 1;
-    if (m_descriptor->hasFixedBinCount) {
-	binCount = m_descriptor->binCount;
+    if (m_descriptors[n]->hasFixedBinCount) {
+	binCount = m_descriptors[n]->binCount;
     }
 
     size_t frame = blockFrame;
 
-    if (m_descriptor->sampleType ==
+    if (m_descriptors[n]->sampleType ==
 	Vamp::Plugin::OutputDescriptor::VariableSampleRate) {
 
 	if (!feature.hasTimestamp) {
@@ -668,18 +747,18 @@
 	    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);
         }
  
-        frame = lrintf((m_fixedRateFeatureNo / m_descriptor->sampleRate)
+        frame = lrintf((m_fixedRateFeatureNos[n] / m_descriptors[n]->sampleRate)
                        * inputRate);
     }
 	
@@ -688,19 +767,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) {
@@ -715,7 +794,7 @@
             model->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;
 
@@ -732,8 +811,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) {
@@ -742,14 +821,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()) {
@@ -775,13 +871,13 @@
             }
         }
 	
-    } 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;
 
 	model->setColumn(frame / model->getResolution(), values);
@@ -792,46 +888,52 @@
 }
 
 void
-FeatureExtractionModelTransformer::setCompletion(int completion)
+FeatureExtractionModelTransformer::setCompletion(int n, int completion)
 {
     int binCount = 1;
-    if (m_descriptor->hasFixedBinCount) {
-	binCount = m_descriptor->binCount;
+    if (m_descriptors[n]->hasFixedBinCount) {
+	binCount = m_descriptors[n]->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	Wed Dec 04 16:47:53 2013 +0000
+++ b/transform/FeatureExtractionModelTransformer.h	Wed Dec 04 18:30:49 2013 +0000
@@ -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"
 
@@ -31,24 +31,43 @@
     Q_OBJECT
 
 public:
+    enum PreferredOutputModel {
+        NoteOutputModel,
+        FlexiNoteOutputModel,
+        UndefinedOutputModel = 255
+    };
+	    
     FeatureExtractionModelTransformer(Input input,
-                                      const Transform &transform);
+                                      const Transform &transform,
+                                      const PreferredOutputModel outputmodel);
+
+    // 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,
+                                      const PreferredOutputModel outputmodel);
+
     virtual ~FeatureExtractionModelTransformer();
 
 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;
+    PreferredOutputModel m_preferredOutputModel;
 
-    void createOutputModel();
+    void createOutputModel(int n);
 
-    void addFeature(size_t blockFrame,
+    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 +76,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	Wed Dec 04 16:47:53 2013 +0000
+++ b/transform/ModelTransformer.cpp	Wed Dec 04 18:30:49 2013 +0000
@@ -16,9 +16,16 @@
 #include "ModelTransformer.h"
 
 ModelTransformer::ModelTransformer(Input input, const Transform &transform) :
-    m_transform(transform),
     m_input(input),
-    m_output(0),
+    m_detached(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_abandoned(false)
 {
@@ -28,6 +35,10 @@
 {
     m_abandoned = true;
     wait();
-    if (!m_detached) delete m_output;
+    if (!m_detached) {
+        foreach (Model *m, m_outputs) {
+            delete m;
+        }
+    }
 }
 
--- a/transform/ModelTransformer.h	Wed Dec 04 16:47:53 2013 +0000
+++ b/transform/ModelTransformer.h	Wed Dec 04 18:30:49 2013 +0000
@@ -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,19 @@
     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 m_outputs; }
 
     /**
      * Return a warning or error message.  If getOutputModel returned
@@ -99,10 +102,11 @@
 
 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_abandoned;
     QString m_message;
--- a/transform/ModelTransformerFactory.cpp	Wed Dec 04 16:47:53 2013 +0000
+++ b/transform/ModelTransformerFactory.cpp	Wed Dec 04 18:30:49 2013 +0000
@@ -35,6 +35,8 @@
 
 #include <QRegExp>
 
+using std::vector;
+
 ModelTransformerFactory *
 ModelTransformerFactory::m_instance = new ModelTransformerFactory;
 
@@ -163,63 +165,79 @@
 }
 
 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, FeatureExtractionModelTransformer::FlexiNoteOutputModel); //!!! gross
 
     } 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) 
 {
     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);
+    if (mm.empty()) return 0;
+    else return mm[0];
+}
+
+vector<Model *>
+ModelTransformerFactory::transformMultiple(const Transforms &transforms,
+                                           const ModelTransformer::Input &input,
+                                           QString &message) 
+{
+    SVDEBUG << "ModelTransformerFactory::transformMultiple: Constructing transformer with input model " << input.getModel() << endl;
+    
+    ModelTransformer *t = createTransformer(transforms, input);
+    if (!t) return vector<Model *>();
 
     connect(t, SIGNAL(finished()), this, SLOT(transformerFinished()));
 
     m_runningTransformers.insert(t);
 
     t->start();
-    Model *model = t->detachOutputModel();
+    vector<Model *> models = t->detachOutputModels();
 
-    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 +245,7 @@
 
     message = t->getMessage();
 
-    return model;
+    return models;
 }
 
 void
@@ -266,8 +284,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	Wed Dec 04 16:47:53 2013 +0000
+++ b/transform/ModelTransformerFactory.h	Wed Dec 04 18:30:49 2013 +0000
@@ -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;
 
@@ -87,13 +89,37 @@
                      const ModelTransformer::Input &input,
                      QString &message);
 
+    /**
+     * 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.
+     * 
+     * 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);
+
 protected slots:
     void transformerFinished();
 
     void modelAboutToBeDeleted(Model *);
 
 protected:
-    ModelTransformer *createTransformer(const Transform &transform,
+    ModelTransformer *createTransformer(const Transforms &transforms,
                                         const ModelTransformer::Input &input);
 
     typedef std::map<TransformId, QString> TransformerConfigurationMap;
@@ -103,6 +129,11 @@
     TransformerSet m_runningTransformers;
 
     static ModelTransformerFactory *m_instance;
+	/** 
+	* allows the  FeatureExtractionModelTransformer output model to be selected externally, 
+	* but only in case of the need for NoteModel or FlexiNoteModel 
+	*/
+	FeatureExtractionModelTransformer::PreferredOutputModel m_preferredOutputModel ;
 };
 
 
--- a/transform/RealTimeEffectModelTransformer.cpp	Wed Dec 04 16:47:53 2013 +0000
+++ b/transform/RealTimeEffectModelTransformer.cpp	Wed Dec 04 18:30:49 2013 +0000
@@ -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	Wed Dec 04 16:47:53 2013 +0000
+++ b/transform/RealTimeEffectModelTransformer.h	Wed Dec 04 18:30:49 2013 +0000
@@ -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	Wed Dec 04 16:47:53 2013 +0000
+++ b/transform/Transform.h	Wed Dec 04 18:30:49 2013 +0000
@@ -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