changeset 851:c9846844ac11 tonioni

Merge branch tonioni_multi_transform
author Chris Cannam
date Mon, 02 Dec 2013 15:47:06 +0000 (2013-12-02)
parents 2d53205f70cd (current diff) dba8a02b0413 (diff)
children 23ecd10c2eb6
files
diffstat 9 files changed, 378 insertions(+), 229 deletions(-) [+]
line wrap: on
line diff
--- a/transform/FeatureExtractionModelTransformer.cpp	Tue Nov 26 14:37:01 2013 +0000
+++ b/transform/FeatureExtractionModelTransformer.cpp	Mon Dec 02 15:47:06 2013 +0000
@@ -39,43 +39,80 @@
 
 FeatureExtractionModelTransformer::FeatureExtractionModelTransformer(Input in,
                                                                      const Transform &transform,
-																	 const PreferredOutputModel outputmodel) :
+                                                                     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) {
@@ -87,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 {
 
@@ -122,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 != "") {
@@ -152,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:
@@ -231,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
@@ -288,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;
         }
 
@@ -306,8 +352,8 @@
             } else {
 	            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) {
@@ -318,8 +364,8 @@
             } else {
                 model = new FlexiNoteModel (modelRate, modelResolution, false);
             }
-            model->setScaleUnits(m_descriptor->unit.c_str());
-            m_output = model;
+            model->setScaleUnits(m_descriptors[n]->unit.c_str());
+            out = model;
 
         } else {
 
@@ -331,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
@@ -361,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 {
 
@@ -380,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 *
@@ -423,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;
@@ -440,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);
@@ -455,7 +508,7 @@
             FFTModel *model = new FFTModel
                                   (getConformingInput(),
                                    channelCount == 1 ? m_input.getChannel() : ch,
-                                   m_transform.getWindowType(),
+                                   primaryTransform.getWindowType(),
                                    blockSize,
                                    stepSize,
                                    blockSize,
@@ -463,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");
             }
@@ -475,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);
@@ -499,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;
@@ -556,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;
 	}
 
@@ -572,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) {
@@ -650,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();
 
@@ -662,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) {
@@ -681,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);
     }
 	
@@ -701,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) {
@@ -728,7 +794,7 @@
             model->addPoint(SparseTimeValueModel::Point(frame, value, label));
         }
 
-    } else if (isOutput<FlexiNoteModel>() || isOutput<NoteModel>() || isOutput<RegionModel>()) { //GF: Added Note Model
+    } else if (isOutput<FlexiNoteModel>(n) || isOutput<NoteModel>(n) || isOutput<RegionModel>(n)) { //GF: Added Note Model
 
         int index = 0;
 
@@ -746,7 +812,7 @@
             }
         }
 
-		if (isOutput<FlexiNoteModel>()) { // GF: added for flexi note model
+		if (isOutput<FlexiNoteModel>(n)) { // GF: added for flexi note model
 
             float velocity = 100;
             if (feature.values.size() > index) {
@@ -755,14 +821,14 @@
             if (velocity < 0) velocity = 127;
             if (velocity > 127) velocity = 127;
 
-            FlexiNoteModel *model = getConformingOutput<FlexiNoteModel>();
+            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>()) {
+        } else  if (isOutput<NoteModel>(n)) {
 
             float velocity = 100;
             if (feature.values.size() > index) {
@@ -771,7 +837,7 @@
             if (velocity < 0) velocity = 127;
             if (velocity > 127) velocity = 127;
 
-            NoteModel *model = getConformingOutput<NoteModel>();
+            NoteModel *model = getConformingOutput<NoteModel>(n);
             if (!model) return;
             model->addPoint(NoteModel::Point(frame, value, // value is pitch
                                              lrintf(duration),
@@ -779,7 +845,7 @@
                                              feature.label.c_str()));
         } else {
 
-            RegionModel *model = getConformingOutput<RegionModel>();
+            RegionModel *model = getConformingOutput<RegionModel>(n);
             if (!model) return;
 
             if (feature.hasDuration && !feature.values.empty()) {
@@ -805,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);
@@ -822,52 +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>()) {
+	} else if (isOutput<FlexiNoteModel>(n)) {
 
-	FlexiNoteModel *model = getConformingOutput<FlexiNoteModel>();
+	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	Tue Nov 26 14:37:01 2013 +0000
+++ b/transform/FeatureExtractionModelTransformer.h	Mon Dec 02 15:47:06 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,33 +31,43 @@
     Q_OBJECT
 
 public:
-	enum PreferredOutputModel {
-		NoteOutputModel,
-		FlexiNoteOutputModel,
-		UndefinedOutputModel = 255
-	    };
+    enum PreferredOutputModel {
+        NoteOutputModel,
+        FlexiNoteOutputModel,
+        UndefinedOutputModel = 255
+    };
 	    
     FeatureExtractionModelTransformer(Input input,
                                       const Transform &transform,
-									  const PreferredOutputModel outputmodel);
+                                      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);
@@ -66,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	Tue Nov 26 14:37:01 2013 +0000
+++ b/transform/ModelTransformer.cpp	Mon Dec 02 15:47:06 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	Tue Nov 26 14:37:01 2013 +0000
+++ b/transform/ModelTransformer.h	Mon Dec 02 15:47:06 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	Tue Nov 26 14:37:01 2013 +0000
+++ b/transform/ModelTransformerFactory.cpp	Mon Dec 02 15:47:06 2013 +0000
@@ -35,6 +35,8 @@
 
 #include <QRegExp>
 
+using std::vector;
+
 ModelTransformerFactory *
 ModelTransformerFactory::m_instance = new ModelTransformerFactory;
 
@@ -163,66 +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, m_preferredOutputModel);
+            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,
-								   /* outputmodel default value = FeatureExtractionModelTransformer::NoteOutputModel */
-								   FeatureExtractionModelTransformer::PreferredOutputModel outputmodel) 
+                                   QString &message) 
 {
     SVDEBUG << "ModelTransformerFactory::transform: Constructing transformer with input model " << input.getModel() << endl;
 
-	m_preferredOutputModel = outputmodel;
-    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();
@@ -230,7 +245,7 @@
 
     message = t->getMessage();
 
-    return model;
+    return models;
 }
 
 void
@@ -269,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	Tue Nov 26 14:37:01 2013 +0000
+++ b/transform/ModelTransformerFactory.h	Mon Dec 02 15:47:06 2013 +0000
@@ -27,6 +27,7 @@
 #include <QMap>
 #include <map>
 #include <set>
+#include <vector>
 
 class AudioPlaySource;
 
@@ -84,10 +85,33 @@
      * 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,
-					 const FeatureExtractionModelTransformer::PreferredOutputModel outputmodel = FeatureExtractionModelTransformer::NoteOutputModel);
+    Model *transform(const Transform &transform,
+                     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();
@@ -95,7 +119,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;
--- a/transform/RealTimeEffectModelTransformer.cpp	Tue Nov 26 14:37:01 2013 +0000
+++ b/transform/RealTimeEffectModelTransformer.cpp	Mon Dec 02 15:47:06 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	Tue Nov 26 14:37:01 2013 +0000
+++ b/transform/RealTimeEffectModelTransformer.h	Mon Dec 02 15:47:06 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	Tue Nov 26 14:37:01 2013 +0000
+++ b/transform/Transform.h	Mon Dec 02 15:47:06 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