changeset 1739:565575463752 by-id

Some work on models and transformers
author Chris Cannam
date Wed, 26 Jun 2019 14:59:09 +0100
parents 4abc0f08adf9
children fe3f7f8df3a3
files base/ById.h data/model/Dense3DModelPeakCache.cpp data/model/Dense3DModelPeakCache.h data/model/Model.cpp rdf/RDFImporter.cpp rdf/RDFImporter.h transform/FeatureExtractionModelTransformer.cpp transform/FeatureExtractionModelTransformer.h transform/ModelTransformer.h
diffstat 9 files changed, 447 insertions(+), 367 deletions(-) [+]
line wrap: on
line diff
--- a/base/ById.h	Wed Jun 26 10:21:15 2019 +0100
+++ b/base/ById.h	Wed Jun 26 14:59:09 2019 +0100
@@ -54,6 +54,19 @@
 };
 
 template <typename T>
+std::ostream &
+operator<<(std::ostream &ostr, const SvId<T> &id)
+{
+    // For diagnostic purposes only. Do not use these IDs for
+    // serialisation - see XmlExportable instead.
+    if (id.isNone()) {
+        return (ostr << "<none>");
+    } else {
+        return (ostr << "#" << id.id);
+    }
+}
+
+template <typename T>
 class WithId
 {
 public:
@@ -113,6 +126,9 @@
     void add(std::shared_ptr<Item> item) {
         QMutexLocker locker(&m_mutex);
         auto id = item->getId();
+        if (id.isNone()) {
+            throw std::logic_error("item id should never be None");
+        }
         if (m_items.find(id) != m_items.end()) {
             SVCERR << "WARNING: ById::add: item with id " << id
                    << " is already recorded, replacing it (item type is "
@@ -128,12 +144,13 @@
     }
 
     std::shared_ptr<Item> get(Id id) const {
+        if (id.isNone()) return {}; // this id is never issued: avoid locking
         QMutexLocker locker(&m_mutex);
         const auto &itr = m_items.find(id);
         if (itr != m_items.end()) {
             return itr->second;
         } else {
-            return std::shared_ptr<Item>();
+            return {};
         }
     }
 
@@ -144,9 +161,11 @@
 
     /**
      * If the Item type is an XmlExportable, return the export ID of
-     * the given item ID. The export ID is a simple int, and is only
-     * allocated when first requested, so objects that are never
-     * exported don't get one.
+     * the given item ID. A call to this function will fail to compile
+     * if the Item is not an XmlExportable.
+     *
+     * The export ID is a simple int, and is only allocated when first
+     * requested, so objects that are never exported don't get one.
      */
     int getExportId(Id id) const {
         auto exportable = getAs<XmlExportable>(id);
--- a/data/model/Dense3DModelPeakCache.cpp	Wed Jun 26 10:21:15 2019 +0100
+++ b/data/model/Dense3DModelPeakCache.cpp	Wed Jun 26 14:59:09 2019 +0100
@@ -19,34 +19,36 @@
 
 #include "base/HitCount.h"
 
-Dense3DModelPeakCache::Dense3DModelPeakCache(const DenseThreeDimensionalModel *source,
+Dense3DModelPeakCache::Dense3DModelPeakCache(ModelId sourceId,
                                              int columnsPerPeak) :
-    m_source(source),
+    m_source(sourceId),
     m_columnsPerPeak(columnsPerPeak)
 {
-    m_cache = new EditableDenseThreeDimensionalModel
-        (source->getSampleRate(),
-         getResolution(),
-         source->getHeight(),
-         EditableDenseThreeDimensionalModel::NoCompression,
-         false);
+    auto source = ModelById::getAs<DenseThreeDimensionalModel>(m_source);
+    if (!source) {
+        SVCERR << "WARNING: Dense3DModelPeakCache constructed for unknown or wrong-type source model id " << m_source << endl;
+        m_source = {};
+        return;
+    }
 
-    connect(source, SIGNAL(modelChanged()),
+    m_cache.reset(new EditableDenseThreeDimensionalModel
+                  (source->getSampleRate(),
+                   source->getResolution() * m_columnsPerPeak,
+                   source->getHeight(),
+                   EditableDenseThreeDimensionalModel::NoCompression,
+                   false));
+
+    connect(source.get(), SIGNAL(modelChanged()),
             this, SLOT(sourceModelChanged()));
-    connect(source, SIGNAL(aboutToBeDeleted()),
-            this, SLOT(sourceModelAboutToBeDeleted()));
 }
 
 Dense3DModelPeakCache::~Dense3DModelPeakCache()
 {
-    if (m_cache) m_cache->aboutToDelete();
-    delete m_cache;
 }
 
 Dense3DModelPeakCache::Column
 Dense3DModelPeakCache::getColumn(int column) const
 {
-    if (!m_source) return Column();
     if (!haveColumn(column)) fillColumn(column);
     return m_cache->getColumn(column);
 }
@@ -54,7 +56,6 @@
 float
 Dense3DModelPeakCache::getValueAt(int column, int n) const
 {
-    if (!m_source) return 0.f;
     if (!haveColumn(column)) fillColumn(column);
     return m_cache->getValueAt(column, n);
 }
@@ -62,7 +63,6 @@
 void
 Dense3DModelPeakCache::sourceModelChanged()
 {
-    if (!m_source) return;
     if (m_coverage.size() > 0) {
         // The last peak may have come from an incomplete read, which
         // may since have been filled, so reset it
@@ -71,12 +71,6 @@
     m_coverage.resize(getWidth(), false); // retaining data
 }
 
-void
-Dense3DModelPeakCache::sourceModelAboutToBeDeleted()
-{
-    m_source = nullptr;
-}
-
 bool
 Dense3DModelPeakCache::haveColumn(int column) const
 {
@@ -104,7 +98,10 @@
         m_coverage.resize(column + 1, false);
     }
 
-    int sourceWidth = m_source->getWidth();
+    auto source = ModelById::getAs<DenseThreeDimensionalModel>(m_source);
+    if (!source) return;
+    
+    int sourceWidth = source->getWidth();
     
     Column peak;
     int n = 0;
@@ -113,7 +110,7 @@
         int sourceColumn = column * m_columnsPerPeak + i;
         if (sourceColumn >= sourceWidth) break;
         
-        Column here = m_source->getColumn(sourceColumn);
+        Column here = source->getColumn(sourceColumn);
 
 //        cerr << "Dense3DModelPeakCache::fillColumn(" << column << "): source col "
 //             << sourceColumn << " of " << sourceWidth
--- a/data/model/Dense3DModelPeakCache.h	Wed Jun 26 10:21:15 2019 +0100
+++ b/data/model/Dense3DModelPeakCache.h	Wed Jun 26 14:59:09 2019 +0100
@@ -24,28 +24,33 @@
     Q_OBJECT
 
 public:
-    Dense3DModelPeakCache(const DenseThreeDimensionalModel *source,
+    Dense3DModelPeakCache(ModelId source, // a DenseThreeDimensionalModel
                           int columnsPerPeak);
     ~Dense3DModelPeakCache();
 
     bool isOK() const override {
-        return m_source && m_source->isOK(); 
+        auto source = ModelById::get(m_source);
+        return source && source->isOK(); 
     }
 
     sv_samplerate_t getSampleRate() const override {
-        return m_source->getSampleRate();
+        auto source = ModelById::get(m_source);
+        return source ? source->getSampleRate() : 0;
     }
 
     sv_frame_t getStartFrame() const override {
-        return m_source->getStartFrame();
+        auto source = ModelById::get(m_source);
+        return source ? source->getStartFrame() : 0;
     }
 
     sv_frame_t getTrueEndFrame() const override {
-        return m_source->getTrueEndFrame();
+        auto source = ModelById::get(m_source);
+        return source ? source->getTrueEndFrame() : 0;
     }
 
     int getResolution() const override {
-        return m_source->getResolution() * m_columnsPerPeak;
+        auto source = ModelById::getAs<DenseThreeDimensionalModel>(m_source);
+        return source ? source->getResolution() * m_columnsPerPeak : 1;
     }
 
     virtual int getColumnsPerPeak() const {
@@ -53,7 +58,9 @@
     }
     
     int getWidth() const override {
-        int sourceWidth = m_source->getWidth();
+        auto source = ModelById::getAs<DenseThreeDimensionalModel>(m_source);
+        if (!source) return 0;
+        int sourceWidth = source->getWidth();
         if ((sourceWidth % m_columnsPerPeak) == 0) {
             return sourceWidth / m_columnsPerPeak;
         } else {
@@ -62,15 +69,18 @@
     }
 
     int getHeight() const override {
-        return m_source->getHeight();
+        auto source = ModelById::getAs<DenseThreeDimensionalModel>(m_source);
+        return source ? source->getHeight() : 0;
     }
 
     float getMinimumLevel() const override {
-        return m_source->getMinimumLevel();
+        auto source = ModelById::getAs<DenseThreeDimensionalModel>(m_source);
+        return source ? source->getMinimumLevel() : 0.f;
     }
 
     float getMaximumLevel() const override {
-        return m_source->getMaximumLevel();
+        auto source = ModelById::getAs<DenseThreeDimensionalModel>(m_source);
+        return source ? source->getMaximumLevel() : 1.f;
     }
 
     /**
@@ -84,17 +94,20 @@
     float getValueAt(int col, int n) const override;
 
     QString getBinName(int n) const override {
-        return m_source->getBinName(n);
+        auto source = ModelById::getAs<DenseThreeDimensionalModel>(m_source);
+        return source ? source->getBinName(n) : "";
     }
 
     bool shouldUseLogValueScale() const override {
-        return m_source->shouldUseLogValueScale();
+        auto source = ModelById::getAs<DenseThreeDimensionalModel>(m_source);
+        return source ? source->shouldUseLogValueScale() : false;
     }
 
     QString getTypeName() const override { return tr("Dense 3-D Peak Cache"); }
 
     int getCompletion() const override {
-        return m_source->getCompletion();
+        auto source = ModelById::get(m_source);
+        return source ? source->getCompletion() : 100;
     }
 
     QString toDelimitedDataString(QString, DataExportOptions,
@@ -104,11 +117,10 @@
 
 protected slots:
     void sourceModelChanged();
-    void sourceModelAboutToBeDeleted();
 
 private:
-    const DenseThreeDimensionalModel *m_source;
-    mutable EditableDenseThreeDimensionalModel *m_cache;
+    ModelId m_source;
+    mutable std::unique_ptr<EditableDenseThreeDimensionalModel> m_cache;
     mutable std::vector<bool> m_coverage; // must be bool, for space efficiency
                                           // (vector of bool uses 1-bit elements)
     int m_columnsPerPeak;
--- a/data/model/Model.cpp	Wed Jun 26 10:21:15 2019 +0100
+++ b/data/model/Model.cpp	Wed Jun 26 14:59:09 2019 +0100
@@ -24,8 +24,8 @@
 
 Model::~Model()
 {
-    SVDEBUG << "Model::~Model(" << this << ")" << endl;
-
+    SVDEBUG << "Model::~Model: " << this << " with id " << getId() << endl;
+/*!!!
     if (!m_aboutToDelete) {
         SVDEBUG << "NOTE: Model(" << this << ", \""
                 << objectName() << "\", type uri <"
@@ -33,10 +33,13 @@
                 << "with no aboutToDelete notification"
                 << endl;
     }
-
+*/
+    //!!! see notes in header - sort this out
+    /*
     if (!m_alignmentModel.isNone()) {
         ModelById::release(m_alignmentModel);
     }
+    */
 }
 
 void
@@ -130,29 +133,39 @@
 sv_frame_t
 Model::alignToReference(sv_frame_t frame) const
 {
-//    cerr << "Model(" << this << ")::alignToReference(" << frame << ")" << endl;
-    if (!m_alignment) {
-        if (m_sourceModel) return m_sourceModel->alignToReference(frame);
-        else return frame;
+    auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
+    
+    if (!alignmentModel) {
+        auto sourceModel = ModelById::get(m_sourceModel);
+        if (sourceModel) {
+            return sourceModel->alignToReference(frame);
+        }
+        return frame;
     }
-    sv_frame_t refFrame = m_alignment->toReference(frame);
-    const Model *m = m_alignment->getReferenceModel();
-    if (m && refFrame > m->getEndFrame()) refFrame = m->getEndFrame();
-//    cerr << "have alignment, aligned is " << refFrame << endl;
+    
+    sv_frame_t refFrame = alignmentModel->toReference(frame);
+    auto refModel = ModelById::get(alignmentModel->getReferenceModel());
+    if (refModel && refFrame > refModel->getEndFrame()) {
+        refFrame = refModel->getEndFrame();
+    }
     return refFrame;
 }
 
 sv_frame_t
 Model::alignFromReference(sv_frame_t refFrame) const
 {
-//    cerr << "Model(" << this << ")::alignFromReference(" << refFrame << ")" << endl;
-    if (!m_alignment) {
-        if (m_sourceModel) return m_sourceModel->alignFromReference(refFrame);
-        else return refFrame;
+    auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
+    
+    if (!alignmentModel) {
+        auto sourceModel = ModelById::get(m_sourceModel);
+        if (sourceModel) {
+            return sourceModel->alignFromReference(refFrame);
+        }
+        return refFrame;
     }
-    sv_frame_t frame = m_alignment->fromReference(refFrame);
+    
+    sv_frame_t frame = alignmentModel->fromReference(refFrame);
     if (frame > getEndFrame()) frame = getEndFrame();
-//    cerr << "have alignment, aligned is " << frame << endl;
     return frame;
 }
 
@@ -160,17 +173,26 @@
 Model::getAlignmentCompletion() const
 {
 #ifdef DEBUG_COMPLETION
-    SVCERR << "Model(" << this << ")::getAlignmentCompletion: m_alignment = "
-           << m_alignment << endl;
+    SVCERR << "Model(" << this
+           << ")::getAlignmentCompletion: m_alignmentModel = "
+           << m_alignmentModel << endl;
 #endif
-    if (!m_alignment) {
-        if (m_sourceModel) return m_sourceModel->getAlignmentCompletion();
-        else return 100;
+
+    auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
+    
+    if (!alignmentModel) {
+        auto sourceModel = ModelById::get(m_sourceModel);
+        if (sourceModel) {
+            return sourceModel->getAlignmentCompletion();
+        }
+        return 100;
     }
+    
     int completion = 0;
-    (void)m_alignment->isReady(&completion);
+    (void)alignmentModel->isReady(&completion);
 #ifdef DEBUG_COMPLETION
-    SVCERR << "Model(" << this << ")::getAlignmentCompletion: completion = " << completion
+    SVCERR << "Model(" << this
+           << ")::getAlignmentCompletion: completion = " << completion
            << endl;
 #endif
     return completion;
@@ -179,21 +201,24 @@
 QString
 Model::getTitle() const
 {
-    if (m_sourceModel) return m_sourceModel->getTitle();
+    auto source = ModelById::get(m_sourceModel);
+    if (source) return source->getTitle();
     else return "";
 }
 
 QString
 Model::getMaker() const
 {
-    if (m_sourceModel) return m_sourceModel->getMaker();
+    auto source = ModelById::get(m_sourceModel);
+    if (source) return source->getMaker();
     else return "";
 }
 
 QString
 Model::getLocation() const
 {
-    if (m_sourceModel) return m_sourceModel->getLocation();
+    auto source = ModelById::get(m_sourceModel);
+    if (source) return source->getLocation();
     else return "";
 }
 
--- a/rdf/RDFImporter.cpp	Wed Jun 26 10:21:15 2019 +0100
+++ b/rdf/RDFImporter.cpp	Wed Jun 26 14:59:09 2019 +0100
@@ -668,7 +668,7 @@
 
                     if (m_audioModelMap.find(source) != m_audioModelMap.end()) {
                         cerr << "source model for " << model << " is " << m_audioModelMap[source] << endl;
-                        model->setSourceModel(m_audioModelMap[source]);
+                        model->setSourceModel(m_audioModelMap[source]->getId());
                     }
 
                     QString title = m_store->complete
--- a/rdf/RDFImporter.h	Wed Jun 26 10:21:15 2019 +0100
+++ b/rdf/RDFImporter.h	Wed Jun 26 14:59:09 2019 +0100
@@ -47,6 +47,14 @@
     bool isOK();
     QString getErrorString() const;
 
+    /**
+     * Return a list of models imported from the RDF source.  The
+     * models were heap-allocated by this class and are not registered
+     * with any other owner; the caller takes ownership and must
+     * arrange for them to be deleted manually or managed by a smart
+     * pointer.
+     */
+    //!!! todo: ModelId-ise
     std::vector<Model *> getDataModels(ProgressReporter *reporter);
 
     enum RDFDocumentType {
--- a/transform/FeatureExtractionModelTransformer.cpp	Wed Jun 26 10:21:15 2019 +0100
+++ b/transform/FeatureExtractionModelTransformer.cpp	Wed Jun 26 14:59:09 2019 +0100
@@ -83,7 +83,7 @@
     // 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) {
+    for (int j = 1; in_range_for(m_transforms, j); ++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");
             SVCERR << m_message << endl;
@@ -104,7 +104,7 @@
         return false;
     }
 
-    DenseTimeValueModel *input = getConformingInput();
+    auto input = ModelById::getAs<DenseTimeValueModel>(getInputModel());
     if (!input) {
         m_message = tr("Input model for feature extraction plugin \"%1\" is of wrong type (internal error?)").arg(pluginId);
         SVCERR << m_message << endl;
@@ -158,7 +158,9 @@
             SVDEBUG << "Initialisation failed, trying again with preferred step = "
                     << preferredStep << ", block = " << preferredBlock << endl;
             
-            if (!m_plugin->initialise(channelCount, preferredStep, preferredBlock)) {
+            if (!m_plugin->initialise(channelCount,
+                                      preferredStep,
+                                      preferredBlock)) {
 
                 SVDEBUG << "Initialisation failed again" << endl;
                 
@@ -221,20 +223,22 @@
         return false;
     }
 
-    for (int j = 0; j < (int)m_transforms.size(); ++j) {
+    for (int j = 0; in_range_for(m_transforms, j); ++j) {
 
-        for (int i = 0; i < (int)outputs.size(); ++i) {
-//        SVDEBUG << "comparing output " << i << " name \"" << outputs[i].identifier << "\" with expected \"" << m_transform.getOutput() << "\"" << endl;
+        for (int i = 0; in_range_for(outputs, i); ++i) {
+
             if (m_transforms[j].getOutput() == "" ||
-                outputs[i].identifier == m_transforms[j].getOutput().toStdString()) {
+                outputs[i].identifier ==
+                m_transforms[j].getOutput().toStdString()) {
+                
                 m_outputNos.push_back(i);
-                m_descriptors.push_back(new Vamp::Plugin::OutputDescriptor(outputs[i]));
+                m_descriptors.push_back(outputs[i]);
                 m_fixedRateFeatureNos.push_back(-1); // we increment before use
                 break;
             }
         }
 
-        if ((int)m_descriptors.size() <= j) {
+        if (!in_range_for(m_descriptors, j)) {
             m_message = tr("Plugin \"%1\" has no output named \"%2\"")
                 .arg(pluginId)
                 .arg(m_transforms[j].getOutput());
@@ -243,7 +247,7 @@
         }
     }
 
-    for (int j = 0; j < (int)m_transforms.size(); ++j) {
+    for (int j = 0; in_range_for(m_transforms, j); ++j) {
         createOutputModels(j);
     }
 
@@ -271,16 +275,15 @@
         m_message = e.what();
     }
     m_plugin = nullptr;
-        
-    for (int j = 0; j < (int)m_descriptors.size(); ++j) {
-        delete m_descriptors[j];
-    }
+
+    m_descriptors.clear();
 }
 
 void
 FeatureExtractionModelTransformer::createOutputModels(int n)
 {
-    DenseTimeValueModel *input = getConformingInput();
+    auto input = ModelById::getAs<DenseTimeValueModel>(getInputModel());
+    if (!input) return;
     
     PluginRDFDescription description(m_transforms[n].getPluginIdentifier());
     QString outputId = m_transforms[n].getOutput();
@@ -288,20 +291,17 @@
     int binCount = 1;
     float minValue = 0.0, maxValue = 0.0;
     bool haveExtents = false;
-    bool haveBinCount = m_descriptors[n]->hasFixedBinCount;
+    bool haveBinCount = m_descriptors[n].hasFixedBinCount;
 
     if (haveBinCount) {
-        binCount = (int)m_descriptors[n]->binCount;
+        binCount = (int)m_descriptors[n].binCount;
     }
 
     m_needAdditionalModels[n] = false;
 
-//    cerr << "FeatureExtractionModelTransformer: output bin count "
-//              << binCount << endl;
-
-    if (binCount > 0 && m_descriptors[n]->hasKnownExtents) {
-        minValue = m_descriptors[n]->minValue;
-        maxValue = m_descriptors[n]->maxValue;
+    if (binCount > 0 && m_descriptors[n].hasKnownExtents) {
+        minValue = m_descriptors[n].minValue;
+        maxValue = m_descriptors[n].maxValue;
         haveExtents = true;
     }
 
@@ -309,10 +309,10 @@
     sv_samplerate_t outputRate = modelRate;
     int modelResolution = 1;
 
-    if (m_descriptors[n]->sampleType != 
+    if (m_descriptors[n].sampleType != 
         Vamp::Plugin::OutputDescriptor::OneSamplePerStep) {
 
-        outputRate = m_descriptors[n]->sampleRate;
+        outputRate = m_descriptors[n].sampleRate;
 
         //!!! SV doesn't actually support display of models that have
         //!!! different underlying rates together -- so we always set
@@ -328,7 +328,7 @@
         }
     }
 
-    switch (m_descriptors[n]->sampleType) {
+    switch (m_descriptors[n].sampleType) {
 
     case Vamp::Plugin::OutputDescriptor::VariableSampleRate:
         if (outputRate != 0.0) {
@@ -342,7 +342,7 @@
 
     case Vamp::Plugin::OutputDescriptor::FixedSampleRate:
         if (outputRate <= 0.0) {
-            SVDEBUG << "WARNING: Fixed sample-rate plugin reports invalid sample rate " << m_descriptors[n]->sampleRate << "; defaulting to input rate of " << input->getSampleRate() << endl;
+            SVDEBUG << "WARNING: Fixed sample-rate plugin reports invalid sample rate " << m_descriptors[n].sampleRate << "; defaulting to input rate of " << input->getSampleRate() << endl;
             modelResolution = 1;
         } else {
             modelResolution = int(round(modelRate / outputRate));
@@ -353,21 +353,23 @@
 
     bool preDurationPlugin = (m_plugin->getVampApiVersion() < 2);
 
-    Model *out = nullptr;
+    std::shared_ptr<Model> out;
 
     if (binCount == 0 &&
-        (preDurationPlugin || !m_descriptors[n]->hasDuration)) {
+        (preDurationPlugin || !m_descriptors[n].hasDuration)) {
 
         // Anything with no value and no duration is an instant
 
-        out = new SparseOneDimensionalModel(modelRate, modelResolution, false);
+        out = std::make_shared<SparseOneDimensionalModel>
+            (modelRate, modelResolution, false);
+
         QString outputEventTypeURI = description.getOutputEventTypeURI(outputId);
         out->setRDFTypeURI(outputEventTypeURI);
 
     } else if ((preDurationPlugin && binCount > 1 &&
-                (m_descriptors[n]->sampleType ==
+                (m_descriptors[n].sampleType ==
                  Vamp::Plugin::OutputDescriptor::VariableSampleRate)) ||
-               (!preDurationPlugin && m_descriptors[n]->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
@@ -398,9 +400,9 @@
 
         // Regions do not have units of Hz or MIDI things (a sweeping
         // assumption!)
-        if (m_descriptors[n]->unit == "Hz" ||
-            m_descriptors[n]->unit.find("MIDI") != std::string::npos ||
-            m_descriptors[n]->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;
         }
 
@@ -420,8 +422,8 @@
                 model = new NoteModel
                     (modelRate, modelResolution, false);
             }
-            model->setScaleUnits(m_descriptors[n]->unit.c_str());
-            out = model;
+            model->setScaleUnits(m_descriptors[n].unit.c_str());
+            out.reset(model);
 
         } else {
 
@@ -433,15 +435,15 @@
                 model = new RegionModel
                     (modelRate, modelResolution, false);
             }
-            model->setScaleUnits(m_descriptors[n]->unit.c_str());
-            out = model;
+            model->setScaleUnits(m_descriptors[n].unit.c_str());
+            out.reset(model);
         }
 
         QString outputEventTypeURI = description.getOutputEventTypeURI(outputId);
         out->setRDFTypeURI(outputEventTypeURI);
 
     } else if (binCount == 1 ||
-               (m_descriptors[n]->sampleType == 
+               (m_descriptors[n].sampleType == 
                 Vamp::Plugin::OutputDescriptor::VariableSampleRate)) {
 
         // Anything that is not a 1D, note, or interval model and that
@@ -482,7 +484,7 @@
         Vamp::Plugin::OutputList outputs = m_plugin->getOutputDescriptors();
         model->setScaleUnits(outputs[m_outputNos[n]].unit.c_str());
 
-        out = model;
+        out.reset(model);
 
         QString outputEventTypeURI = description.getOutputEventTypeURI(outputId);
         out->setRDFTypeURI(outputEventTypeURI);
@@ -499,23 +501,24 @@
              EditableDenseThreeDimensionalModel::BasicMultirateCompression,
              false);
 
-        if (!m_descriptors[n]->binNames.empty()) {
+        if (!m_descriptors[n].binNames.empty()) {
             std::vector<QString> names;
-            for (int i = 0; i < (int)m_descriptors[n]->binNames.size(); ++i) {
-                names.push_back(m_descriptors[n]->binNames[i].c_str());
+            for (int i = 0; i < (int)m_descriptors[n].binNames.size(); ++i) {
+                names.push_back(m_descriptors[n].binNames[i].c_str());
             }
             model->setBinNames(names);
         }
         
-        out = model;
+        out.reset(model);
 
         QString outputSignalTypeURI = description.getOutputSignalTypeURI(outputId);
         out->setRDFTypeURI(outputSignalTypeURI);
     }
 
     if (out) {
-        out->setSourceModel(input);
-        m_outputs.push_back(out);
+        out->setSourceModel(getInputModel());
+        ModelById::add(out);
+        m_outputs.push_back(out->getId());
     }
 }
 
@@ -540,13 +543,9 @@
 FeatureExtractionModelTransformer::getAdditionalOutputModels()
 {
     Models mm;
-    for (AdditionalModelMap::iterator i = m_additionalModels.begin();
-         i != m_additionalModels.end(); ++i) {
-        for (std::map<int, SparseTimeValueModel *>::iterator j =
-                 i->second.begin();
-             j != i->second.end(); ++j) {
-            SparseTimeValueModel *m = j->second;
-            if (m) mm.push_back(m);
+    for (auto mp : m_additionalModels) {
+        for (auto m: mp.second) {
+            mm.push_back(m.second);
         }
     }
     return mm;
@@ -555,34 +554,45 @@
 bool
 FeatureExtractionModelTransformer::willHaveAdditionalOutputModels()
 {
-    for (std::map<int, bool>::const_iterator i =
-             m_needAdditionalModels.begin(); 
-         i != m_needAdditionalModels.end(); ++i) {
-        if (i->second) return true;
+    for (auto p : m_needAdditionalModels) {
+        if (p.second) return true;
     }
     return false;
 }
 
-SparseTimeValueModel *
+ModelId
 FeatureExtractionModelTransformer::getAdditionalModel(int n, int binNo)
 {
-//    std::cerr << "getAdditionalModel(" << n << ", " << binNo << ")" << std::endl;
-
     if (binNo == 0) {
-        std::cerr << "Internal error: binNo == 0 in getAdditionalModel (should be using primary model)" << std::endl;
-        return nullptr;
+        SVCERR << "Internal error: binNo == 0 in getAdditionalModel (should be using primary model, not calling getAdditionalModel)" << endl;
+        return {};
     }
 
-    if (!m_needAdditionalModels[n]) return nullptr;
-    if (!isOutput<SparseTimeValueModel>(n)) return nullptr;
-    if (m_additionalModels[n][binNo]) return m_additionalModels[n][binNo];
+    if (!in_range_for(m_outputs, n)) {
+        SVCERR << "getAdditionalModel: Output " << n << " out of range" << endl;
+        return {};
+    }
 
-    std::cerr << "getAdditionalModel(" << n << ", " << binNo << "): creating" << std::endl;
+    if (!in_range_for(m_needAdditionalModels, n) ||
+        !m_needAdditionalModels[n]) {
+        return {};
+    }
+    
+    if (!m_additionalModels[n][binNo].isNone()) {
+        return m_additionalModels[n][binNo];
+    }
 
-    SparseTimeValueModel *baseModel = getConformingOutput<SparseTimeValueModel>(n);
-    if (!baseModel) return nullptr;
+    SVDEBUG << "getAdditionalModel(" << n << ", " << binNo
+            << "): creating" << endl;
 
-    std::cerr << "getAdditionalModel(" << n << ", " << binNo << "): (from " << baseModel << ")" << std::endl;
+    auto baseModel = ModelById::getAs<SparseTimeValueModel>(m_outputs[n]);
+    if (!baseModel) {
+        SVCERR << "getAdditionalModel: Output model not conformable, or has vanished" << endl;
+        return {};
+    }
+    
+    SVDEBUG << "getAdditionalModel(" << n << ", " << binNo
+            << "): (from " << baseModel << ")" << endl;
 
     SparseTimeValueModel *additional =
         new SparseTimeValueModel(baseModel->getSampleRate(),
@@ -594,21 +604,10 @@
     additional->setScaleUnits(baseModel->getScaleUnits());
     additional->setRDFTypeURI(baseModel->getRDFTypeURI());
 
-    m_additionalModels[n][binNo] = additional;
-    return additional;
-}
-
-DenseTimeValueModel *
-FeatureExtractionModelTransformer::getConformingInput()
-{
-//    SVDEBUG << "FeatureExtractionModelTransformer::getConformingInput: input model is " << getInputModel() << endl;
-
-    DenseTimeValueModel *dtvm =
-        dynamic_cast<DenseTimeValueModel *>(getInputModel());
-    if (!dtvm) {
-        SVDEBUG << "FeatureExtractionModelTransformer::getConformingInput: WARNING: Input model is not conformable to DenseTimeValueModel" << endl;
-    }
-    return dtvm;
+    ModelId additionalId = additional->getId();
+    ModelById::add(std::shared_ptr<SparseTimeValueModel>(additional));
+    m_additionalModels[n][binNo] = additionalId;
+    return additionalId;
 }
 
 void
@@ -624,12 +623,6 @@
         m_message = e.what();
         return;
     }
-    
-    DenseTimeValueModel *input = getConformingInput();
-    if (!input) {
-        abandon();
-        return;
-    }
 
     if (m_outputs.empty()) {
         abandon();
@@ -638,14 +631,30 @@
 
     Transform primaryTransform = m_transforms[0];
 
-    while (!input->isReady() && !m_abandoned) {
-        SVDEBUG << "FeatureExtractionModelTransformer::run: Waiting for input model to be ready..." << endl;
-        usleep(500000);
+    bool ready = false;
+    while (!ready && !m_abandoned) {
+        { // scope so as to release input shared_ptr before sleeping
+            auto input = ModelById::getAs<DenseTimeValueModel>(getInputModel());
+            if (!input) {
+                abandon();
+                return;
+            }
+            ready = input->isReady();
+        }
+        if (!ready) {
+            SVDEBUG << "FeatureExtractionModelTransformer::run: Waiting for input model to be ready..." << endl;
+            usleep(500000);
+        }
     }
-    SVDEBUG << "FeatureExtractionModelTransformer::run: Waited, ready = "
-            << input->isReady() << ", m_abandoned = " << m_abandoned << endl;
     if (m_abandoned) return;
 
+    auto input = ModelById::getAs<DenseTimeValueModel>(getInputModel());
+    if (!input) {
+        SVCERR << "FeatureExtractionModelTransformer::run: Input model not (no longer?) available, abandoning" << endl;
+        abandon();
+        return;
+    }
+
     sv_samplerate_t sampleRate = input->getSampleRate();
 
     int channelCount = input->getChannelCount();
@@ -669,28 +678,28 @@
     if (frequencyDomain) {
         for (int ch = 0; ch < channelCount; ++ch) {
             FFTModel *model = new FFTModel
-                                  (getConformingInput(),
-                                   channelCount == 1 ? m_input.getChannel() : ch,
-                                   primaryTransform.getWindowType(),
-                                   blockSize,
-                                   stepSize,
-                                   blockSize);
+//!!!                (input->getId(),
+                (nullptr,
+                 channelCount == 1 ? m_input.getChannel() : ch,
+                 primaryTransform.getWindowType(),
+                 blockSize,
+                 stepSize,
+                 blockSize);
             if (!model->isOK() || model->getError() != "") {
                 QString err = model->getError();
                 delete model;
-                for (int j = 0; j < (int)m_outputNos.size(); ++j) {
+                for (int j = 0; in_range_for(m_outputNos, j); ++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: error is: " + err);
             }
             fftModels.push_back(model);
-            cerr << "created model for channel " << ch << endl;
         }
     }
 
-    sv_frame_t startFrame = m_input.getModel()->getStartFrame();
-    sv_frame_t endFrame = m_input.getModel()->getEndFrame();
+    sv_frame_t startFrame = input->getStartFrame();
+    sv_frame_t endFrame = input->getEndFrame();
 
     RealTime contextStartRT = primaryTransform.getStartTime();
     RealTime contextDurationRT = primaryTransform.getDuration();
@@ -716,7 +725,7 @@
 
     long prevCompletion = 0;
 
-    for (int j = 0; j < (int)m_outputNos.size(); ++j) {
+    for (int j = 0; in_range_for(m_outputNos, j); ++j) {
         setCompletion(j, 0);
     }
 
@@ -734,10 +743,14 @@
 
             if (frequencyDomain) {
                 if (blockFrame - int(blockSize)/2 >
-                    contextStart + contextDuration) break;
+                    contextStart + contextDuration) {
+                    break;
+                }
             } else {
                 if (blockFrame >= 
-                    contextStart + contextDuration) break;
+                    contextStart + contextDuration) {
+                    break;
+                }
             }
 
 #ifdef DEBUG_FEATURE_EXTRACTION_TRANSFORMER_RUN
@@ -750,7 +763,7 @@
                 ((((blockFrame - contextStart) / stepSize) * 99) /
                  (contextDuration / stepSize + 1));
 
-            // channelCount is either m_input.getModel()->channelCount or 1
+            // channelCount is either input->channelCount or 1
 
             if (frequencyDomain) {
                 for (int ch = 0; ch < channelCount; ++ch) {
@@ -863,8 +876,10 @@
         startFrame = 0;
     }
 
-    DenseTimeValueModel *input = getConformingInput();
-    if (!input) return;
+    auto input = ModelById::getAs<DenseTimeValueModel>(getInputModel());
+    if (!input) {
+        return;
+    }
     
     sv_frame_t got = 0;
 
@@ -907,7 +922,10 @@
                                               sv_frame_t blockFrame,
                                               const Vamp::Plugin::Feature &feature)
 {
-    sv_samplerate_t inputRate = m_input.getModel()->getSampleRate();
+    auto input = ModelById::get(getInputModel());
+    if (!input) return;
+
+    sv_samplerate_t inputRate = input->getSampleRate();
 
 //    cerr << "FeatureExtractionModelTransformer::addFeature: blockFrame = "
 //              << blockFrame << ", hasTimestamp = " << feature.hasTimestamp
@@ -917,7 +935,7 @@
 
     sv_frame_t frame = blockFrame;
 
-    if (m_descriptors[n]->sampleType ==
+    if (m_descriptors[n].sampleType ==
         Vamp::Plugin::OutputDescriptor::VariableSampleRate) {
 
         if (!feature.hasTimestamp) {
@@ -933,10 +951,10 @@
 //        cerr << "variable sample rate: timestamp = " << feature.timestamp
 //             << " at input rate " << inputRate << " -> " << frame << endl;
         
-    } else if (m_descriptors[n]->sampleType ==
+    } else if (m_descriptors[n].sampleType ==
                Vamp::Plugin::OutputDescriptor::FixedSampleRate) {
 
-        sv_samplerate_t rate = m_descriptors[n]->sampleRate;
+        sv_samplerate_t rate = m_descriptors[n].sampleRate;
         if (rate <= 0.0) {
             rate = inputRate;
         }
@@ -949,7 +967,7 @@
         }
 
 //        cerr << "m_fixedRateFeatureNo = " << m_fixedRateFeatureNos[n]
-//             << ", m_descriptor->sampleRate = " << m_descriptors[n]->sampleRate
+//             << ", m_descriptor->sampleRate = " << m_descriptors[n].sampleRate
 //             << ", inputRate = " << inputRate
 //             << " giving frame = ";
         frame = lrint((double(m_fixedRateFeatureNos[n]) / rate) * inputRate);
@@ -971,122 +989,128 @@
     // to, we instead test what sort of model the constructor decided
     // to create.
 
-    if (isOutput<SparseOneDimensionalModel>(n)) {
+    ModelId outputId = m_outputs[n];
+    bool found = false;
+    
+    if (!found) {
+        auto model = ModelById::getAs<SparseOneDimensionalModel>(outputId);
+        if (model) {
+            found = true;
+            model->add(Event(frame, feature.label.c_str()));
+        }
+    }
+    
+    if (!found) {
+        auto model = ModelById::getAs<SparseTimeValueModel>(outputId);
+        if (model) {
+            found = true;
 
-        SparseOneDimensionalModel *model =
-            getConformingOutput<SparseOneDimensionalModel>(n);
-        if (!model) return;
+            for (int i = 0; in_range_for(feature.values, i); ++i) {
 
-        model->add(Event(frame, feature.label.c_str()));
-        
-    } else if (isOutput<SparseTimeValueModel>(n)) {
+                float value = feature.values[i];
 
-        SparseTimeValueModel *model =
-            getConformingOutput<SparseTimeValueModel>(n);
-        if (!model) return;
+                QString label = feature.label.c_str();
+                if (feature.values.size() > 1) {
+                    label = QString("[%1] %2").arg(i+1).arg(label);
+                }
 
-        for (int i = 0; i < (int)feature.values.size(); ++i) {
+                auto targetModel = model;
 
-            float value = feature.values[i];
+                if (m_needAdditionalModels[n] && i > 0) {
+                    targetModel = ModelById::getAs<SparseTimeValueModel>
+                        (getAdditionalModel(n, i));
+                    if (!targetModel) targetModel = model;
+                }
 
-            QString label = feature.label.c_str();
-            if (feature.values.size() > 1) {
-                label = QString("[%1] %2").arg(i+1).arg(label);
+                targetModel->add(Event(frame, value, label));
+            }
+        }
+    }
+    
+    if (!found) {
+        if (ModelById::getAs<NoteModel>(outputId) ||
+            ModelById::getAs<RegionModel>(outputId)) {
+            found = true;
+
+            int index = 0;
+
+            float value = 0.0;
+            if ((int)feature.values.size() > index) {
+                value = feature.values[index++];
             }
 
-            SparseTimeValueModel *targetModel = model;
-
-            if (m_needAdditionalModels[n] && i > 0) {
-                targetModel = getAdditionalModel(n, i);
-                if (!targetModel) targetModel = model;
-//                std::cerr << "adding point to model " << targetModel
-//                          << " for output " << n << " bin " << i << std::endl;
+            sv_frame_t duration = 1;
+            if (feature.hasDuration) {
+                duration = RealTime::realTime2Frame(feature.duration, inputRate);
+            } else {
+                if (in_range_for(feature.values, index)) {
+                    duration = lrintf(feature.values[index++]);
+                }
             }
 
-            targetModel->add(Event(frame, value, label));
-        }
+            auto noteModel = ModelById::getAs<NoteModel>(outputId);
+            if (noteModel) {
 
-    } else if (isOutput<NoteModel>(n) || isOutput<RegionModel>(n)) {
+                float velocity = 100;
+                if ((int)feature.values.size() > index) {
+                    velocity = feature.values[index++];
+                }
+                if (velocity < 0) velocity = 127;
+                if (velocity > 127) velocity = 127;
 
-        int index = 0;
+                noteModel->add(Event(frame, value, // value is pitch
+                                     duration,
+                                     velocity / 127.f,
+                                     feature.label.c_str()));
+            }
 
-        float value = 0.0;
-        if ((int)feature.values.size() > index) {
-            value = feature.values[index++];
-        }
+            auto regionModel = ModelById::getAs<RegionModel>(outputId);
+            if (regionModel) {
 
-        sv_frame_t duration = 1;
-        if (feature.hasDuration) {
-            duration = RealTime::realTime2Frame(feature.duration, inputRate);
-        } else {
-            if (in_range_for(feature.values, index)) {
-                duration = lrintf(feature.values[index++]);
+                if (feature.hasDuration && !feature.values.empty()) {
+
+                    for (int i = 0; in_range_for(feature.values, i); ++i) {
+
+                        float value = feature.values[i];
+
+                        QString label = feature.label.c_str();
+                        if (feature.values.size() > 1) {
+                            label = QString("[%1] %2").arg(i+1).arg(label);
+                        }
+
+                        regionModel->add(Event(frame,
+                                               value,
+                                               duration,
+                                               label));
+                    }
+                } else {
+            
+                    regionModel->add(Event(frame,
+                                           value,
+                                           duration,
+                                           feature.label.c_str()));
+                }
             }
         }
+    }
 
-        if (isOutput<NoteModel>(n)) {
-
-            float velocity = 100;
-            if ((int)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->add(Event(frame, value, // value is pitch
-                             duration,
-                             velocity / 127.f,
-                             feature.label.c_str()));
-        } else {
-
-            RegionModel *model = getConformingOutput<RegionModel>(n);
-            if (!model) return;
-
-            if (feature.hasDuration && !feature.values.empty()) {
-
-                for (int i = 0; i < (int)feature.values.size(); ++i) {
-
-                    float value = feature.values[i];
-
-                    QString label = feature.label.c_str();
-                    if (feature.values.size() > 1) {
-                        label = QString("[%1] %2").arg(i+1).arg(label);
-                    }
-
-                    model->add(Event(frame,
-                                     value,
-                                     duration,
-                                     label));
-                }
+    if (!found) {
+        auto model = ModelById::getAs
+            <EditableDenseThreeDimensionalModel>(outputId);
+        if (model) {
+            found = true;
+        
+            DenseThreeDimensionalModel::Column values = feature.values;
+        
+            if (!feature.hasTimestamp && m_fixedRateFeatureNos[n] >= 0) {
+                model->setColumn(m_fixedRateFeatureNos[n], values);
             } else {
-            
-                model->add(Event(frame,
-                                 value,
-                                 duration,
-                                 feature.label.c_str()));
+                model->setColumn(int(frame / model->getResolution()), values);
             }
         }
-        
-    } else if (isOutput<EditableDenseThreeDimensionalModel>(n)) {
-        
-        DenseThreeDimensionalModel::Column values = feature.values;
-        
-        EditableDenseThreeDimensionalModel *model =
-            getConformingOutput<EditableDenseThreeDimensionalModel>(n);
-        if (!model) return;
+    }
 
-//        cerr << "(note: model resolution = " << model->getResolution() << ")"
-//             << endl;
-
-        if (!feature.hasTimestamp && m_fixedRateFeatureNos[n] >= 0) {
-            model->setColumn(m_fixedRateFeatureNos[n], values);
-        } else {
-            model->setColumn(int(frame / model->getResolution()), values);
-        }
-
-    } else {
+    if (!found) {
         SVDEBUG << "FeatureExtractionModelTransformer::addFeature: Unknown output model type!" << endl;
     }
 }
@@ -1099,43 +1123,47 @@
               << completion << ")" << endl;
 #endif
 
-    if (isOutput<SparseOneDimensionalModel>(n)) {
+    ModelId outputId = m_outputs[n];
+    bool found = false;
+    
+    if (!found) {
+        auto model = ModelById::getAs<SparseOneDimensionalModel>(outputId);
+        if (model) {
+            found = true;
+            model->setCompletion(completion, true);
+        }
+    }
 
-        SparseOneDimensionalModel *model =
-            getConformingOutput<SparseOneDimensionalModel>(n);
-        if (!model) return;
-        if (model->isAbandoning()) abandon();
-        model->setCompletion(completion, true);
+    if (!found) {
+        auto model = ModelById::getAs<SparseTimeValueModel>(outputId);
+        if (model) {
+            found = true;
+            model->setCompletion(completion, true);
+        }
+    }
 
-    } else if (isOutput<SparseTimeValueModel>(n)) {
+    if (!found) {
+        auto model = ModelById::getAs<NoteModel>(outputId);
+        if (model) {
+            found = true;
+            model->setCompletion(completion, true);
+        }
+    }
 
-        SparseTimeValueModel *model =
-            getConformingOutput<SparseTimeValueModel>(n);
-        if (!model) return;
-        if (model->isAbandoning()) abandon();
-        model->setCompletion(completion, true);
+    if (!found) {
+        auto model = ModelById::getAs<RegionModel>(outputId);
+        if (model) {
+            found = true;
+            model->setCompletion(completion, true);
+        }
+    }
 
-    } else if (isOutput<NoteModel>(n)) {
-
-        NoteModel *model = getConformingOutput<NoteModel>(n);
-        if (!model) return;
-        if (model->isAbandoning()) abandon();
-        model->setCompletion(completion, true);
-        
-    } else if (isOutput<RegionModel>(n)) {
-
-        RegionModel *model = getConformingOutput<RegionModel>(n);
-        if (!model) return;
-        if (model->isAbandoning()) abandon();
-        model->setCompletion(completion, true);
-
-    } else if (isOutput<EditableDenseThreeDimensionalModel>(n)) {
-
-        EditableDenseThreeDimensionalModel *model =
-            getConformingOutput<EditableDenseThreeDimensionalModel>(n);
-        if (!model) return;
-        if (model->isAbandoning()) abandon();
-        model->setCompletion(completion, true); //!!!m_context.updates);
+    if (!found) {
+        auto model = ModelById::getAs<EditableDenseThreeDimensionalModel>(outputId);
+        if (model) {
+            found = true;
+            model->setCompletion(completion, true);
+        }
     }
 }
 
--- a/transform/FeatureExtractionModelTransformer.h	Wed Jun 26 10:21:15 2019 +0100
+++ b/transform/FeatureExtractionModelTransformer.h	Wed Jun 26 14:59:09 2019 +0100
@@ -38,9 +38,11 @@
     FeatureExtractionModelTransformer(Input input,
                                       const Transform &transform);
 
-    // Obtain outputs for a set of transforms that all use the same
-    // plugin and input (but with different outputs). i.e. run the
-    // plugin once only and collect more than one output from it.
+    /**
+     * 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);
 
@@ -57,16 +59,27 @@
     void run() override;
 
     Vamp::Plugin *m_plugin;
-    std::vector<Vamp::Plugin::OutputDescriptor *> m_descriptors; // per transform
-    std::vector<int> m_fixedRateFeatureNos; // to assign times to FixedSampleRate features
-    std::vector<int> m_outputNos; // list of plugin output indexes required for this group of transforms
+
+    // descriptors per transform
+    std::vector<Vamp::Plugin::OutputDescriptor> m_descriptors;
+
+    // to assign times to FixedSampleRate features
+    std::vector<int> m_fixedRateFeatureNos;
+
+    // list of plugin output indexes required for this group of transforms
+    std::vector<int> m_outputNos;
 
     void createOutputModels(int n);
 
-    std::map<int, bool> m_needAdditionalModels; // transformNo -> necessity
-    typedef std::map<int, std::map<int, SparseTimeValueModel *> > AdditionalModelMap;
+    // map from transformNo -> necessity
+    std::map<int, bool> m_needAdditionalModels;
+
+    // map from transformNo -> binNo -> SparseTimeValueModel id
+    typedef std::map<int, std::map<int, ModelId> > AdditionalModelMap;
+    
     AdditionalModelMap m_additionalModels;
-    SparseTimeValueModel *getAdditionalModel(int transformNo, int binNo);
+    
+    ModelId getAdditionalModel(int transformNo, int binNo);
 
     void addFeature(int n,
                     sv_frame_t blockFrame,
@@ -83,7 +96,7 @@
     void awaitOutputModels() override;
     
     // just casts:
-
+/*!!!
     DenseTimeValueModel *getConformingInput();
 
     template <typename ModelClass> bool isOutput(int n) {
@@ -102,6 +115,7 @@
             return 0;
         }
     }
+*/
 };
 
 #endif
--- a/transform/ModelTransformer.h	Wed Jun 26 10:21:15 2019 +0100
+++ b/transform/ModelTransformer.h	Wed Jun 26 14:59:09 2019 +0100
@@ -34,36 +34,34 @@
  * available to the user of the ModelTransformer immediately, but may
  * be initially empty until the background thread has populated it.
  */
-
 class ModelTransformer : public Thread
 {
 public:
     virtual ~ModelTransformer();
 
-    typedef std::vector<Model *> Models;
+    typedef std::vector<ModelId> Models;
 
     class Input {
     public:
-        Input(Model *m) : m_model(m), m_channel(-1) { }
-        Input(Model *m, int c) : m_model(m), m_channel(c) { }
+        Input(ModelId m) : m_model(m), m_channel(-1) { }
+        Input(ModelId m, int c) : m_model(m), m_channel(c) { }
 
-        Model *getModel() const { return m_model; }
-        void setModel(Model *m) { m_model = m; }
+        ModelId getModel() const { return m_model; }
+        void setModel(ModelId m) { m_model = m; }
 
         int getChannel() const { return m_channel; }
         void setChannel(int c) { m_channel = c; }
 
     protected:
-        Model *m_model;
+        ModelId m_model;
         int m_channel;
     };
 
     /**
      * Hint to the processing thread that it should give up, for
-     * example because the process is going to exit or we want to get
-     * rid of the input model.  Caller should still wait() and/or
-     * delete the transform before assuming its input and output
-     * models are no longer required.
+     * example because the process is going to exit or the
+     * model/document context is being replaced.  Caller should still
+     * wait() to be sure that processing has ended.
      */
     void abandon() { m_abandoned = true; }
 
@@ -76,7 +74,7 @@
     /**
      * Return the input model for the transform.
      */
-    Model *getInputModel()  { return m_input.getModel(); }
+    ModelId getInputModel()  { return m_input.getModel(); }
 
     /**
      * Return the input channel spec for the transform.
@@ -84,10 +82,11 @@
     int getInputChannel() { return m_input.getChannel(); }
 
     /**
-     * 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.
+     * Return the set of output model IDs 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. The returned models have been added to
+     * ModelById.
      */
     Models getOutputModels() {
         awaitOutputModels();
@@ -95,17 +94,6 @@
     }
 
     /**
-     * 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.
-     */
-    Models detachOutputModels() {
-        awaitOutputModels();
-        m_detached = true; 
-        return m_outputs;
-    }
-
-    /**
      * Return any additional models that were created during
      * processing. This might happen if, for example, a transform was
      * configured to split a multi-bin output into separate single-bin
@@ -122,15 +110,6 @@
     virtual bool willHaveAdditionalOutputModels() { return false; }
 
     /**
-     * Return the set of additional models, also detaching them from
-     * the transformer.  The caller takes ownership of the models.
-     */
-    virtual Models detachAdditionalOutputModels() { 
-        m_detachedAdd = true;
-        return getAdditionalOutputModels();
-    }
-
-    /**
      * Return a warning or error message.  If getOutputModel returned
      * a null pointer, this should contain a fatal error message for
      * the transformer; otherwise it may contain a warning to show to
@@ -145,10 +124,8 @@
     virtual void awaitOutputModels() = 0;
     
     Transforms m_transforms;
-    Input m_input; // I don't own the model in this
-    Models m_outputs; // I own this, unless...
-    bool m_detached; // ... this is true.
-    bool m_detachedAdd;
+    Input m_input;
+    Models m_outputs;
     bool m_abandoned;
     QString m_message;
 };