changeset 1766:85b9b466a59f

Merge from branch by-id
author Chris Cannam
date Wed, 17 Jul 2019 14:24:51 +0100
parents 649ac57c5a2d (current diff) 2e2497cba59e (diff)
children aa0b56d72f27
files data/model/PathModel.h
diffstat 59 files changed, 2092 insertions(+), 1473 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/ById.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -0,0 +1,142 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ById.h"
+
+#include <unordered_map>
+#include <typeinfo>
+
+int IdAlloc::getNextId()
+{
+    static int nextId = 0;
+    static QMutex mutex;
+    QMutexLocker locker(&mutex);
+    int i = nextId;
+    if (nextId == INT_MAX) {
+        nextId = INT_MIN;
+    } else {
+        ++nextId;
+        if (nextId == 0 || nextId == NO_ID) {
+            throw std::runtime_error("Internal ID limit exceeded!");
+        }
+    }
+    return i;
+}
+
+class AnyById::Impl
+{
+public:    
+    ~Impl() {
+        QMutexLocker locker(&m_mutex);
+        bool empty = true;
+        for (const auto &p: m_items) {
+            if (p.second && p.second.use_count() > 0) {
+                empty = false;
+                break;
+            }
+        }
+        if (!empty) {
+            SVCERR << "WARNING: ById map is not empty at close; some items have not been released" << endl;
+            SVCERR << "         Unreleased items are:" << endl;
+            for (const auto &p: m_items) {
+                auto ptr = p.second;
+                if (ptr && ptr.use_count() > 0) {
+                    QString message = QString("id #%1: type %2")
+                        .arg(p.first).arg(typeid(*ptr.get()).name());
+                    if (auto qobj = std::dynamic_pointer_cast<QObject>(ptr)) {
+                        message += QString(", object name \"%1\"")
+                            .arg(qobj->objectName());
+                    }
+                    message += QString(", use count %1").arg(ptr.use_count());
+                    SVCERR << "         - " << message << endl; 
+                }
+            }
+        }
+    }
+        
+    int add(std::shared_ptr<WithId> item) {
+        int id = item->getUntypedId();
+        if (id == IdAlloc::NO_ID) {
+            throw std::logic_error("item id should never be NO_ID");
+        }
+        SVCERR << "ById::add(#" << id << ") of type "
+               << typeid(*item.get()).name() << endl;
+        QMutexLocker locker(&m_mutex);
+        if (m_items.find(id) != m_items.end()) {
+            SVCERR << "ById::add: item with id " << id
+                   << " is already recorded (existing item type is "
+                   << typeid(*m_items.find(id)->second.get()).name()
+                   << ", proposed is "
+                   << typeid(*item.get()).name() << ")" << endl;
+            throw std::logic_error("item id is already recorded in add");
+        }
+        m_items[id] = item;
+        return id;
+    }
+
+    void release(int id) {
+        if (id == IdAlloc::NO_ID) {
+            return;
+        }
+        SVCERR << "ById::release(#" << id << ")" << endl;
+        QMutexLocker locker(&m_mutex);
+        if (m_items.find(id) == m_items.end()) {
+            SVCERR << "ById::release: unknown item id " << id << endl;
+            throw std::logic_error("unknown item id in release");
+        }
+        m_items.erase(id);
+    }
+    
+    std::shared_ptr<WithId> get(int id) const {
+        if (id == IdAlloc::NO_ID) {
+            return {}; // this id cannot be added: avoid locking
+        }
+        QMutexLocker locker(&m_mutex);
+        const auto &itr = m_items.find(id);
+        if (itr != m_items.end()) {
+            return itr->second;
+        } else {
+            return {};
+        }
+    }
+
+private:
+    mutable QMutex m_mutex;
+    std::unordered_map<int, std::shared_ptr<WithId>> m_items;
+};
+
+int
+AnyById::add(std::shared_ptr<WithId> item)
+{
+    return impl().add(item);
+}
+
+void
+AnyById::release(int id)
+{
+    impl().release(id);
+}
+
+std::shared_ptr<WithId>
+AnyById::get(int id)
+{
+    return impl().get(id);
+}
+
+AnyById::Impl &
+AnyById::impl()
+{
+    static Impl impl;
+    return impl;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/ById.h	Wed Jul 17 14:24:51 2019 +0100
@@ -0,0 +1,269 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_BY_ID_H
+#define SV_BY_ID_H
+
+#include "Debug.h"
+
+#include <memory>
+#include <iostream>
+#include <climits>
+#include <stdexcept>
+
+#include <QMutex>
+#include <QString>
+
+#include "XmlExportable.h"
+
+/*
+ * ById - central pool of objects to be retrieved by persistent id.
+ *
+ * This is a pretty simple mechanism for obtaining safe "borrowed"
+ * references to shared objects, including across threads, based on an
+ * object ID.
+ *
+ * A class (call it C) inherits WithTypedId<C>. This produces a type
+ * C::Id containing a numerical id. Each instance of C (or subclass
+ * thereof) has an internal id of type C::Id whose value is unique
+ * among all ids ever possessed by any instances of all classes that
+ * use this id mechanism (within a single run of the program).
+ *
+ * Then we have a static store of type TypedById<C, C::Id>. This holds
+ * a set of heap-allocated C objects (or subclass thereof) and hands
+ * out shared_ptr references to them when queried by id. The
+ * application calls add() to pass an object to the store (which takes
+ * ownership of it), and the application calls release() when it
+ * thinks it has finished with an object, to request the store to
+ * delete it.
+ *
+ * Note that an object's id can't (without shenanigans) be queried
+ * directly from that object - it is returned when the object is added
+ * to a ById store. So if you have an object id, you know that the
+ * object must have been added to a store at some point.
+ *
+ * The goal is to improve code that would previously have retained a
+ * bare pointer to a heap-allocated object that it did not own. For
+ * example, in Sonic Visualiser we have a Model hierarchy of complex
+ * mutable objects, and any given model may be referred to by many
+ * different layers, transforms (as both source and target) etc. Using
+ * bare pointers for those references means that we need everything to
+ * be notified (and act properly on the notification) if a model is
+ * about to be deleted. Using a Model::Id instead gives the code a
+ * guarantee: if the model has been deleted since you last looked at
+ * it, then the ById store will return a null shared_ptr from its
+ * get() function for that id; but if it returns a non-null
+ * shared_ptr, then the object being pointed to can't be deleted while
+ * that shared_ptr is in scope.
+ *
+ * Example:
+ *
+ * class Thing : public WithTypedId<Thing> { Thing(int x) { } };
+ * typedef TypedById<Thing, Thing::Id> ThingById;
+ *
+ * // application creates a new Thing
+ * // ...
+ * auto thing = std::make_shared<Thing>(10);
+ * auto thingId = ThingById::add(thing);
+ *
+ * // application then passes thingId to something else, without
+ * // storing the shared_ptr anywhere - the ById store manages that
+ * 
+ * // code elsewhere now has the thingId, and needs to use the Thing
+ * // ...
+ * void doSomething() {
+ *     auto thing = ThingById::get(m_thingId);
+ *     if (!thing) { // the Thing has been deleted, stop acting on it
+ *         return;   // (this may be an error or it may be unexceptional)
+ *     }
+ *     // now we have a guarantee that the thing ptr will be valid
+ *     // until it goes out of scope when doSomething returns
+ * }
+ *
+ * // application wants to be rid of the Thing
+ * ThingById::release(thingId);
+ */
+
+//!!! to do: review how often we are calling getAs<...> when we could
+// just be using get
+
+struct IdAlloc {
+
+    // The value NO_ID (-1) is never allocated
+    static const int NO_ID = -1;
+    
+    static int getNextId();
+};
+
+template <typename T>
+struct TypedId {
+    
+    int untyped;
+    
+    TypedId() : untyped(IdAlloc::NO_ID) {}
+
+    TypedId(const TypedId &) =default;
+    TypedId &operator=(const TypedId &) =default;
+
+    bool operator==(const TypedId &other) const {
+        return untyped == other.untyped;
+    }
+    bool operator!=(const TypedId &other) const {
+        return untyped != other.untyped;
+    }
+    bool operator<(const TypedId &other) const {
+        return untyped < other.untyped;
+    }
+    bool isNone() const {
+        return untyped == IdAlloc::NO_ID;
+    }
+};
+
+template <typename T>
+std::ostream &
+operator<<(std::ostream &ostr, const TypedId<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.untyped);
+    }
+}
+
+class WithId
+{
+public:
+    WithId() :
+        m_id(IdAlloc::getNextId()) {
+    }
+    virtual ~WithId() {
+    }
+
+protected:
+    friend class AnyById;
+    
+    /**
+     * Return an id for this object. The id is a unique number for
+     * this object among all objects that implement WithId within this
+     * single run of the application.
+     */
+    int getUntypedId() const {
+        return m_id;
+    }
+
+private:
+    int m_id;
+};
+
+template <typename T>
+class WithTypedId : virtual public WithId
+{
+public:
+    typedef TypedId<T> Id;
+    
+    WithTypedId() : WithId() { }
+
+protected:
+    template <typename Item, typename Id>
+    friend class TypedById;
+    
+    /**
+     * Return an id for this object. The id is a unique value for this
+     * object among all objects that implement WithTypedId within this
+     * single run of the application.
+     */
+    Id getId() const {
+        Id id;
+        id.untyped = getUntypedId();
+        return id;
+    }
+};
+
+class AnyById
+{
+public:
+    static int add(std::shared_ptr<WithId>);
+    static void release(int);
+    static std::shared_ptr<WithId> get(int); 
+
+    template <typename Derived>
+    static bool isa(int id) {
+        std::shared_ptr<WithId> p = get(id);
+        return bool(std::dynamic_pointer_cast<Derived>(p));
+    }
+   
+    template <typename Derived>
+    static std::shared_ptr<Derived> getAs(int id) {
+        std::shared_ptr<WithId> p = get(id);
+        return std::dynamic_pointer_cast<Derived>(p);
+    }
+
+private:
+    class Impl;
+    static Impl &impl();
+};
+
+template <typename Item, typename Id>
+class TypedById
+{
+public:
+    static Id add(std::shared_ptr<Item> item) {
+        Id id;
+        id.untyped = AnyById::add(item);
+        return id;
+    }
+
+    static void release(Id id) {
+        AnyById::release(id.untyped);
+    }
+    static void release(std::shared_ptr<Item> item) {
+        release(item->getId());
+    }
+
+    template <typename Derived>
+    static bool isa(Id id) {
+        return AnyById::isa<Derived>(id.untyped);
+    }
+
+    template <typename Derived>
+    static std::shared_ptr<Derived> getAs(Id id) {
+        return AnyById::getAs<Derived>(id.untyped);
+    }
+
+    static std::shared_ptr<Item> get(Id id) {
+        return getAs<Item>(id);
+    }
+    
+    /**
+     * If the Item type is an XmlExportable, return the export ID of
+     * 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.
+     */
+    static int getExportId(Id id) {
+        auto exportable = getAs<XmlExportable>(id);
+        if (exportable) {
+            return exportable->getExportId();
+        } else {
+            return XmlExportable::NO_ID;
+        }
+    }
+};
+
+#endif
+
--- a/base/PlayParameterRepository.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/base/PlayParameterRepository.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -33,19 +33,15 @@
 }
 
 void
-PlayParameterRepository::addPlayable(const Playable *playable)
+PlayParameterRepository::addPlayable(int playableId, const Playable *playable)
 {
-//    cerr << "PlayParameterRepository:addPlayable playable = " << playable <<  endl;
-
-    if (!getPlayParameters(playable)) {
-
+    if (!getPlayParameters(playableId)) {
+        
         // Give all playables the same type of play parameters for the
         // moment
 
-//        cerr << "PlayParameterRepository:addPlayable: Adding play parameters for " << playable << endl;
-
-        PlayParameters *params = new PlayParameters;
-        m_playParameters[playable] = params;
+        auto params = std::make_shared<PlayParameters>();
+        m_playParameters[playableId] = params;
 
         params->setPlayClipId
             (playable->getDefaultPlayClipId());
@@ -53,64 +49,65 @@
         params->setPlayAudible
             (playable->getDefaultPlayAudible());
         
-        connect(params, SIGNAL(playParametersChanged()),
+        connect(params.get(), SIGNAL(playParametersChanged()),
                 this, SLOT(playParametersChanged()));
         
-        connect(params, SIGNAL(playClipIdChanged(QString)),
+        connect(params.get(), SIGNAL(playClipIdChanged(QString)),
                 this, SLOT(playClipIdChanged(QString)));
-
-//        cerr << "Connected play parameters " << params << " for playable "
-//                     << playable << " to this " << this << endl;
     }
 }    
 
 void
-PlayParameterRepository::removePlayable(const Playable *playable)
+PlayParameterRepository::removePlayable(int playableId)
 {
-    if (m_playParameters.find(playable) == m_playParameters.end()) {
-        cerr << "WARNING: PlayParameterRepository::removePlayable: unknown playable " << playable << endl;
+    if (m_playParameters.find(playableId) == m_playParameters.end()) {
         return;
     }
-    delete m_playParameters[playable];
-    m_playParameters.erase(playable);
+    m_playParameters.erase(playableId);
 }
 
 void
-PlayParameterRepository::copyParameters(const Playable *from, const Playable *to)
+PlayParameterRepository::copyParameters(int from, int to)
 {
     if (!getPlayParameters(from)) {
         cerr << "ERROR: PlayParameterRepository::copyParameters: source playable unknown" << endl;
         return;
     }
     if (!getPlayParameters(to)) {
-        cerr << "WARNING: PlayParameterRepository::copyParameters: target playable unknown, adding it now" << endl;
-        addPlayable(to);
+        cerr << "ERROR: PlayParameterRepository::copyParameters: target playable unknown" << endl;
+        return;
     }
-    getPlayParameters(to)->copyFrom(getPlayParameters(from));
+    getPlayParameters(to)->copyFrom(getPlayParameters(from).get());
 }
 
-PlayParameters *
-PlayParameterRepository::getPlayParameters(const Playable *playable) 
+std::shared_ptr<PlayParameters>
+PlayParameterRepository::getPlayParameters(int playableId) 
 {
-    if (m_playParameters.find(playable) == m_playParameters.end()) return nullptr;
-    return m_playParameters.find(playable)->second;
+    if (m_playParameters.find(playableId) == m_playParameters.end()) {
+        return nullptr;
+    }
+    return m_playParameters.find(playableId)->second;
 }
 
 void
 PlayParameterRepository::playParametersChanged()
 {
     PlayParameters *params = dynamic_cast<PlayParameters *>(sender());
-    emit playParametersChanged(params);
+    for (auto i: m_playParameters) {
+        if (i.second.get() == params) {
+            emit playParametersChanged(i.first);
+            return;
+        }
+    }
 }
 
 void
 PlayParameterRepository::playClipIdChanged(QString id)
 {
     PlayParameters *params = dynamic_cast<PlayParameters *>(sender());
-    for (PlayableParameterMap::iterator i = m_playParameters.begin();
-         i != m_playParameters.end(); ++i) {
-        if (i->second == params) {
-            emit playClipIdChanged(i->first, id);
+    for (auto i: m_playParameters) {
+        if (i.second.get() == params) {
+            emit playClipIdChanged(i.first, id);
             return;
         }
     }
@@ -119,18 +116,14 @@
 void
 PlayParameterRepository::clear()
 {
-//    cerr << "PlayParameterRepository: PlayParameterRepository::clear" << endl;
-    while (!m_playParameters.empty()) {
-        delete m_playParameters.begin()->second;
-        m_playParameters.erase(m_playParameters.begin());
-    }
+    m_playParameters.clear();
 }
 
-PlayParameterRepository::EditCommand::EditCommand(PlayParameters *params) :
+PlayParameterRepository::EditCommand::EditCommand(std::shared_ptr<PlayParameters> params) :
     m_params(params)
 {
-    m_from.copyFrom(m_params);
-    m_to.copyFrom(m_params);
+    m_from.copyFrom(m_params.get());
+    m_to.copyFrom(m_params.get());
 }
 
 void
--- a/base/PlayParameterRepository.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/base/PlayParameterRepository.h	Wed Jul 17 14:24:51 2019 +0100
@@ -22,6 +22,7 @@
 class Playable;
 
 #include <map>
+#include <memory>
 
 #include <QObject>
 #include <QString>
@@ -35,18 +36,34 @@
 
     virtual ~PlayParameterRepository();
 
-    void addPlayable(const Playable *playable);
-    void removePlayable(const Playable *playable);
-    void copyParameters(const Playable *from, const Playable *to);
+    /**
+     * Register a playable. The id can be anything you like, so long
+     * as it is unique among playables.
+     */
+    void addPlayable(int id, const Playable *);
 
-    PlayParameters *getPlayParameters(const Playable *playable);
+    /**
+     * Unregister a playable. This must happen before a playable is
+     * deleted.
+     */
+    void removePlayable(int id);
+
+    /**
+     * Copy the play parameters from one playable to another.
+     */
+    void copyParameters(int fromId, int toId);
+
+    /**
+     * Retrieve the play parameters for a playable.
+     */
+    std::shared_ptr<PlayParameters> getPlayParameters(int id);
 
     void clear();
 
     class EditCommand : public Command
     {
     public:
-        EditCommand(PlayParameters *params);
+        EditCommand(std::shared_ptr<PlayParameters> params);
         void setPlayMuted(bool);
         void setPlayAudible(bool);
         void setPlayPan(float);
@@ -57,21 +74,21 @@
         QString getName() const override;
 
     protected:
-        PlayParameters *m_params;
+        std::shared_ptr<PlayParameters> m_params;
         PlayParameters m_from;
         PlayParameters m_to;
     };
 
 signals:
-    void playParametersChanged(PlayParameters *);
-    void playClipIdChanged(const Playable *, QString);
+    void playParametersChanged(int playableId);
+    void playClipIdChanged(int playableId, QString);
 
 protected slots:
     void playParametersChanged();
     void playClipIdChanged(QString);
 
 protected:
-    typedef std::map<const Playable *, PlayParameters *> PlayableParameterMap;
+    typedef std::map<int, std::shared_ptr<PlayParameters>> PlayableParameterMap;
     PlayableParameterMap m_playParameters;
 
     static PlayParameterRepository *m_instance;
--- a/base/PlayParameters.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/base/PlayParameters.h	Wed Jul 17 14:24:51 2019 +0100
@@ -37,8 +37,8 @@
     virtual void copyFrom(const PlayParameters *);
 
     void toXml(QTextStream &stream,
-                       QString indent = "",
-                       QString extraAttributes = "") const override;
+               QString indent = "",
+               QString extraAttributes = "") const override;
 
 public slots:
     virtual void setPlayMuted(bool muted);
--- a/base/PropertyContainer.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/base/PropertyContainer.h	Wed Jul 17 14:24:51 2019 +0100
@@ -21,6 +21,7 @@
 #include <QString>
 #include <QObject>
 #include <vector>
+#include <memory>
 
 class PlayParameters;
 class RangeMapper;
@@ -111,7 +112,13 @@
     virtual QString getPropertyContainerName() const = 0;
     virtual QString getPropertyContainerIconName() const = 0;
 
-    virtual PlayParameters *getPlayParameters() { return 0; }
+    /**
+     * Return the play parameters for this layer, if any. The return
+     * value is a shared_ptr that, if not null, can be passed to
+     * e.g. PlayParameterRepository::EditCommand to change the
+     * parameters.
+     */
+    virtual std::shared_ptr<PlayParameters> getPlayParameters() { return {}; }
 
 signals:
     void propertyChanged(PropertyContainer::PropertyName);
--- a/base/TextMatcher.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/base/TextMatcher.h	Wed Jul 17 14:24:51 2019 +0100
@@ -38,8 +38,9 @@
         FragmentMap fragments;
 
         Match() : score(0) { }
-        Match(const Match &m) :
-            key(m.key), score(m.score), fragments(m.fragments) { }
+
+        Match(const Match &m) =default;
+        Match &operator=(const Match &m) =default;
 
         bool operator<(const Match &m) const; // sort by score first
     };
--- a/base/XmlExportable.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/base/XmlExportable.h	Wed Jul 17 14:24:51 2019 +0100
@@ -25,7 +25,14 @@
 class XmlExportable
 {
 public:
-    XmlExportable() : m_exportId(-1) { }
+    enum {
+        // The value NO_ID (-1) is never allocated as an export id
+        NO_ID = -1
+    };
+
+    typedef int ExportId;
+    
+    XmlExportable() : m_exportId(NO_ID) { }
     virtual ~XmlExportable() { }
 
     /**
@@ -33,7 +40,7 @@
      * allocated the first time this is called, so objects on which
      * this is never called do not get allocated one.
      */
-    int getExportId() const;
+    ExportId getExportId() const;
 
     /**
      * Stream this exportable object out to XML on a text stream.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/TestById.h	Wed Jul 17 14:24:51 2019 +0100
@@ -0,0 +1,210 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "../ById.h"
+
+#include <QObject>
+#include <QtTest>
+
+#include <iostream>
+
+using namespace std;
+
+struct WithoutId {};
+
+// We'll need to change access levels for getId() and getUntypedId()
+// to test the raw calls
+
+struct A : public WithTypedId<A> { public: using WithTypedId<A>::getId; };
+struct B1 : public A {};
+struct B2 : public A {};
+
+struct M {};
+
+typedef TypedById<A, A::Id> AById;
+
+struct X : virtual public WithId { public: using WithId::getUntypedId; };
+struct Y : public X, public B2, public M {};
+
+class TestById : public QObject
+{
+    Q_OBJECT
+
+private slots:
+    void ids() {
+        // Verify that ids are unique across all classes, not just
+        // within a class. These must be the first two WithId objects
+        // allocated in the first test in the suite, otherwise they
+        // could be different even if they were allocated from
+        // separate pools.
+        A a;
+        X x;
+        if (a.getId().untyped == x.getUntypedId()) {
+            std::cerr << "ERROR: a and x have the same id: " << a.getId()
+                      << std::endl;
+        }
+        QVERIFY(a.getId().untyped != x.getUntypedId());
+
+        A aa;
+        QVERIFY(aa.getId().untyped != a.getId().untyped);
+        QVERIFY(aa.getId().untyped != x.getUntypedId());
+
+        // Check the actual ids that have been allocated. This is
+        // supposed to be a hidden implementation detail, but we want
+        // to make sure the test itself hasn't become broken in terms
+        // of allocation order (see comment above)
+        QCOMPARE(a.getId().untyped, 0);
+        QCOMPARE(x.getUntypedId(), 1);
+        QCOMPARE(aa.getId().untyped, 2);
+
+        QVERIFY(!a.getId().isNone());
+        QVERIFY(A::Id().isNone());
+    }
+
+    // NB each test must release all the items it adds to the ById store
+    
+    void anyEmpty() {
+        auto p = AnyById::get(0);
+        QVERIFY(!p);
+    }
+
+    void anySimple() {
+        auto a = std::make_shared<A>();
+        int id = AnyById::add(a);
+        QCOMPARE(id, a->getId().untyped);
+
+        auto aa = AnyById::getAs<A>(id);
+        QVERIFY(!!aa);
+        QCOMPARE(aa->getId(), a->getId());
+        QCOMPARE(aa.get(), a.get()); // same object, not just same id!
+        AnyById::release(id);
+    }
+    
+    void typedEmpty() {
+        auto p = AById::get({});
+        QVERIFY(!p);
+    }
+
+    void typedSimple() {
+        auto a = std::make_shared<A>();
+        AById::add(a);
+
+        auto aa = AById::get(a->getId());
+        QVERIFY(!!aa);
+        QCOMPARE(aa->getId(), a->getId());
+        QCOMPARE(aa.get(), a.get()); // same object, not just same id!
+        AById::release(a);
+    }
+
+    void typedReleaseById() {
+        auto a = std::make_shared<A>();
+        auto aid = AById::add(a);
+
+        auto aa = AById::get(aid);
+        QVERIFY(!!aa);
+        AById::release(aid);
+
+        aa = AById::get(aid);
+        QVERIFY(!aa);
+    }
+
+    void typedReleaseByItem() {
+        auto a = std::make_shared<A>();
+        auto aid = AById::add(a);
+
+        auto aa = AById::get(aid);
+        QVERIFY(!!aa);
+        AById::release(a);
+
+        aa = AById::get(aid);
+        QVERIFY(!aa);
+    }
+
+    void typedDowncast() {
+        auto a = std::make_shared<A>();
+        auto b1 = std::make_shared<B1>();
+        AById::add(a);
+        AById::add(b1);
+
+        auto bb1 = AById::getAs<B1>(a->getId());
+        QVERIFY(!bb1);
+
+        bb1 = AById::getAs<B1>(b1->getId());
+        QVERIFY(!!bb1);
+        QCOMPARE(bb1->getId(), b1->getId());
+
+        auto bb2 = AById::getAs<B2>(b1->getId());
+        QVERIFY(!bb2);
+
+        AById::release(a);
+        AById::release(b1);
+    }
+
+    void typedCrosscast() {
+        auto y = std::make_shared<Y>();
+        AById::add(y);
+
+        auto yy = AById::getAs<Y>(y->getId());
+        QVERIFY(!!yy);
+        QCOMPARE(yy->getId(), y->getId());
+        
+        yy = AnyById::getAs<Y>(y->getId().untyped);
+        QVERIFY(!!yy);
+        QCOMPARE(yy->getId(), y->getId());
+
+        auto xx = AById::getAs<X>(y->getId());
+        QVERIFY(!!xx);
+        QCOMPARE(xx->getUntypedId(), y->getId().untyped);
+        QCOMPARE(xx.get(), yy.get());
+        
+        xx = AnyById::getAs<X>(y->getId().untyped);
+        QVERIFY(!!xx);
+        QCOMPARE(xx->getUntypedId(), y->getId().untyped);
+        QCOMPARE(xx.get(), yy.get());
+        
+        auto mm = AnyById::getAs<M>(y->getId().untyped);
+        QVERIFY(!!mm);
+        QCOMPARE(mm.get(), yy.get());
+
+        AById::release(y);
+    }
+
+    void duplicateAdd() {
+        auto a = std::make_shared<A>();
+        AById::add(a);
+        try {
+            AById::add(a);
+            std::cerr << "Failed to catch expected exception in duplicateAdd"
+                      << std::endl;
+            QVERIFY(false);
+        } catch (const std::logic_error &) {
+        }
+        AById::release(a);
+    }
+
+    void unknownRelease() {
+        auto a = std::make_shared<A>();
+        auto b1 = std::make_shared<B1>();
+        AById::add(a);
+        try {
+            AById::release(b1);
+            std::cerr << "Failed to catch expected exception in unknownRelease"
+                      << std::endl;
+            QVERIFY(false);
+        } catch (const std::logic_error &) {
+        }
+        AById::release(a);
+    }
+};
+
--- a/base/test/files.pri	Thu Jun 20 14:58:20 2019 +0100
+++ b/base/test/files.pri	Wed Jul 17 14:24:51 2019 +0100
@@ -1,4 +1,5 @@
 TEST_HEADERS = \
+	     TestById.h \
 	     TestColumnOp.h \
 	     TestLogRange.h \
 	     TestMovingMedian.h \
--- a/base/test/svcore-base-test.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/base/test/svcore-base-test.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -20,6 +20,7 @@
 #include "TestVampRealTime.h"
 #include "TestColumnOp.h"
 #include "TestMovingMedian.h"
+#include "TestById.h"
 #include "TestEventSeries.h"
 #include "StressEventSeries.h"
 
@@ -91,13 +92,19 @@
         if (QTest::qExec(&t, argc, argv) == 0) ++good;
         else ++bad;
     }
-/*
+    {
+        TestById t;
+        if (QTest::qExec(&t, argc, argv) == 0) ++good;
+        else ++bad;
+    }
+
+#ifdef NOT_DEFINED
     {
         StressEventSeries t;
         if (QTest::qExec(&t, argc, argv) == 0) ++good;
         else ++bad;
     }
-*/
+#endif
     
     if (bad > 0) {
         SVCERR << "\n********* " << bad << " test suite(s) failed!\n" << endl;
--- a/data/fileio/FileSource.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/fileio/FileSource.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -883,7 +883,8 @@
 
     QDir dir;
     try {
-        dir = TempDirectory::getInstance()->getSubDirectoryPath("download");
+        dir.setPath(TempDirectory::getInstance()->
+                    getSubDirectoryPath("download"));
     } catch (const DirectoryCreationFailed &f) {
 #ifdef DEBUG_FILE_SOURCE
         cerr << "FileSource::createCacheFile: ERROR: Failed to create temporary directory: " << f.what() << endl;
--- a/data/midi/MIDIEvent.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/midi/MIDIEvent.h	Wed Jul 17 14:24:51 2019 +0100
@@ -162,6 +162,9 @@
         m_metaMessage(sysEx)
     { }
 
+    MIDIEvent(const MIDIEvent &) =default;
+    MIDIEvent& operator=(const MIDIEvent &) =default;
+    
     ~MIDIEvent() { }
 
     void setTime(const unsigned long &time) { m_deltaTime = time; }
@@ -195,8 +198,6 @@
     friend bool operator<(const MIDIEvent &a, const MIDIEvent &b);
 
 private:
-    MIDIEvent& operator=(const MIDIEvent);
-
     unsigned long  m_deltaTime;
     unsigned long  m_duration;
     MIDIByte       m_eventCode;
--- a/data/model/AggregateWaveModel.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/AggregateWaveModel.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -27,19 +27,37 @@
 AggregateWaveModel::m_zoomConstraint;
 
 AggregateWaveModel::AggregateWaveModel(ChannelSpecList channelSpecs) :
-    m_components(channelSpecs),
-    m_invalidated(false)
+    m_components(channelSpecs)
 {
-    for (ChannelSpecList::const_iterator i = channelSpecs.begin();
-         i != channelSpecs.end(); ++i) {
+    sv_samplerate_t overallRate = 0;
 
-        connect(i->model, SIGNAL(aboutToBeDeleted()),
-                this, SLOT(componentModelAboutToBeDeleted()));
-        
-        if (i->model->getSampleRate() !=
-            channelSpecs.begin()->model->getSampleRate()) {
-            SVDEBUG << "AggregateWaveModel::AggregateWaveModel: WARNING: Component models do not all have the same sample rate" << endl;
-            break;
+    for (int channel = 0; in_range_for(m_components, channel); ++channel) {
+
+        auto model = ModelById::getAs<RangeSummarisableTimeValueModel>
+            (m_components[channel].model);
+
+        if (!model) {
+            SVCERR << "AggregateWaveModel: WARNING: component for channel "
+                   << channel << " is not found or is of wrong model type"
+                   << endl;
+            continue;
+        }
+
+        sv_samplerate_t rate = model->getSampleRate();
+
+        if (!rate) {
+            SVCERR << "AggregateWaveModel: WARNING: component for channel "
+                   << channel << " reports zero sample rate" << endl;
+
+        } else if (!overallRate) {
+
+            overallRate = rate;
+
+        } else if (rate != overallRate) {
+            SVCERR << "AggregateWaveModel: WARNING: component for channel "
+                   << channel << " has different sample rate from earlier "
+                   << "channels (has " << rate << ", expected " << overallRate
+                   << ")" << endl;
         }
     }
 }
@@ -49,25 +67,15 @@
     SVDEBUG << "AggregateWaveModel::~AggregateWaveModel" << endl;
 }
 
-void
-AggregateWaveModel::componentModelAboutToBeDeleted()
-{
-    SVDEBUG << "AggregateWaveModel::componentModelAboutToBeDeleted: invalidating"
-            << endl;
-    m_components.clear();
-    m_invalidated = true;
-    emit modelInvalidated();
-}
-
 bool
 AggregateWaveModel::isOK() const
 {
-    if (m_invalidated || m_components.empty()) {
+    if (m_components.empty()) {
         return false;
     }
-    for (ChannelSpecList::const_iterator i = m_components.begin();
-         i != m_components.end(); ++i) {
-        if (!i->model->isOK()) {
+    for (const auto &c: m_components) {
+        auto model = ModelById::get(c.model);
+        if (!model || !model->isOK()) {
             return false;
         }
     }
@@ -80,10 +88,11 @@
     if (completion) *completion = 100;
 
     bool ready = true;
-    for (ChannelSpecList::const_iterator i = m_components.begin();
-         i != m_components.end(); ++i) {
+    for (auto c: m_components) {
         int completionHere = 100;
-        if (!i->model->isReady(&completionHere)) {
+        auto model = ModelById::get(c.model);
+        if (!model) continue;
+        if (!model->isReady(&completionHere)) {
             ready = false;
         }
         if (completion && completionHere < *completion) {
@@ -103,10 +112,10 @@
 AggregateWaveModel::getFrameCount() const
 {
     sv_frame_t count = 0;
-    for (ChannelSpecList::const_iterator i = m_components.begin();
-         i != m_components.end(); ++i) {
-        sv_frame_t thisCount =
-            i->model->getEndFrame() - i->model->getStartFrame();
+    for (auto c: m_components) {
+        auto model = ModelById::get(c.model);
+        if (!model) continue;
+        sv_frame_t thisCount = model->getEndFrame() - model->getStartFrame();
         if (thisCount > count) count = thisCount;
     }
     return count;
@@ -121,19 +130,23 @@
 sv_samplerate_t
 AggregateWaveModel::getSampleRate() const
 {
-    if (m_invalidated || m_components.empty()) return 0;
-    return m_components.begin()->model->getSampleRate();
+    if (m_components.empty()) return 0;
+    auto model = ModelById::get(m_components.begin()->model);
+    if (!model) return 0;
+    return model->getSampleRate();
 }
 
 floatvec_t
 AggregateWaveModel::getData(int channel, sv_frame_t start, sv_frame_t count) const
 {
-    if (m_invalidated || m_components.empty()) return {};
-    
+    if (m_components.empty()) return {};
+
     int ch0 = channel, ch1 = channel;
     if (channel == -1) {
         ch0 = 0;
         ch1 = getChannelCount()-1;
+    } else if (!in_range_for(m_components, channel)) {
+        return {};
     }
 
     floatvec_t result(count, 0.f);
@@ -141,8 +154,11 @@
     
     for (int c = ch0; c <= ch1; ++c) {
 
-        auto here = m_components[c].model->getData(m_components[c].channel,
-                                                   start, count);
+        auto model = ModelById::getAs<RangeSummarisableTimeValueModel>
+            (m_components[c].model);
+        if (!model) continue;
+
+        auto here = model->getData(m_components[c].channel, start, count);
         if (sv_frame_t(here.size()) > longest) {
             longest = sv_frame_t(here.size());
         }
@@ -236,7 +252,9 @@
 {
     QStringList componentStrings;
     for (const auto &c: m_components) {
-        componentStrings.push_back(QString("%1").arg(c.model->getExportId()));
+        auto model = ModelById::get(c.model);
+        if (!model) continue;
+        componentStrings.push_back(QString("%1").arg(model->getExportId()));
     }
     Model::toXml(out, indent,
                  QString("type=\"aggregatewave\" components=\"%1\" %2")
--- a/data/model/AggregateWaveModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/AggregateWaveModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -28,9 +28,8 @@
 public:
     struct ModelChannelSpec
     {
-        ModelChannelSpec(RangeSummarisableTimeValueModel *m, int c) :
-            model(m), channel(c) { }
-        RangeSummarisableTimeValueModel *model;
+        ModelChannelSpec(ModelId m, int c) : model(m), channel(c) { }
+        ModelId model;
         int channel;
     };
 
@@ -84,18 +83,15 @@
     void modelChanged();
     void modelChangedWithin(sv_frame_t, sv_frame_t);
     void completionChanged();
-    void modelInvalidated();
 
 protected slots:
     void componentModelChanged();
     void componentModelChangedWithin(sv_frame_t, sv_frame_t);
     void componentModelCompletionChanged();
-    void componentModelAboutToBeDeleted();
 
 protected:
     ChannelSpecList m_components;
     static PowerOfSqrtTwoZoomConstraint m_zoomConstraint;
-    bool m_invalidated; // because one of its component models is aboutToBeDeleted
 };
 
 #endif
--- a/data/model/AlignmentModel.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/AlignmentModel.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -19,35 +19,18 @@
 
 //#define DEBUG_ALIGNMENT_MODEL 1
 
-AlignmentModel::AlignmentModel(Model *reference,
-                               Model *aligned,
-                               SparseTimeValueModel *path) :
+AlignmentModel::AlignmentModel(ModelId reference,
+                               ModelId aligned,
+                               ModelId pathSource) :
     m_reference(reference),
     m_aligned(aligned),
-    m_rawPath(path),
+    m_pathSource(pathSource),
     m_path(nullptr),
     m_reversePath(nullptr),
     m_pathBegun(false),
     m_pathComplete(false)
 {
-    if (m_rawPath) {
-
-        connect(m_rawPath, SIGNAL(modelChanged()),
-                this, SLOT(pathChanged()));
-
-        connect(m_rawPath, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
-                this, SLOT(pathChangedWithin(sv_frame_t, sv_frame_t)));
-        
-        connect(m_rawPath, SIGNAL(completionChanged()),
-                this, SLOT(pathCompletionChanged()));
-
-        constructPath();
-        constructReversePath();
-    }
-
-    if (m_rawPath && m_rawPath->isReady()) {
-        pathCompletionChanged();
-    }
+    setPathFrom(pathSource);
 
     if (m_reference == m_aligned) {
         // Trivial alignment, e.g. of main model to itself, which we
@@ -62,51 +45,66 @@
 #ifdef DEBUG_ALIGNMENT_MODEL
     SVCERR << "AlignmentModel(" << this << ")::~AlignmentModel()" << endl;
 #endif
-
-    if (m_rawPath) m_rawPath->aboutToDelete();
-    delete m_rawPath;
-
-    if (m_path) m_path->aboutToDelete();
-    delete m_path;
-
-    if (m_reversePath) m_reversePath->aboutToDelete();
-    delete m_reversePath;
 }
 
 bool
 AlignmentModel::isOK() const
 {
     if (m_error != "") return false;
-    if (m_rawPath) return m_rawPath->isOK();
+    if (m_pathSource.isNone()) return true;
+    auto pathSourceModel =
+        ModelById::getAs<SparseTimeValueModel>(m_pathSource);
+    if (pathSourceModel) {
+        return pathSourceModel->isOK();
+    }
     return true;
 }
 
 sv_frame_t
 AlignmentModel::getStartFrame() const
 {
-    sv_frame_t a = m_reference->getStartFrame();
-    sv_frame_t b = m_aligned->getStartFrame();
-    return std::min(a, b);
+    auto reference = ModelById::get(m_reference);
+    auto aligned = ModelById::get(m_aligned);
+    
+    if (reference && aligned) {
+        sv_frame_t a = reference->getStartFrame();
+        sv_frame_t b = aligned->getStartFrame();
+        return std::min(a, b);
+    } else {
+        return 0;
+    }
 }
 
 sv_frame_t
 AlignmentModel::getTrueEndFrame() const
 {
-    sv_frame_t a = m_reference->getEndFrame();
-    sv_frame_t b = m_aligned->getEndFrame();
-    return std::max(a, b);
+    auto reference = ModelById::get(m_reference);
+    auto aligned = ModelById::get(m_aligned);
+
+    if (reference && aligned) {
+        sv_frame_t a = reference->getEndFrame();
+        sv_frame_t b = aligned->getEndFrame();
+        return std::max(a, b);
+    } else {
+        return 0;
+    }
 }
 
 sv_samplerate_t
 AlignmentModel::getSampleRate() const
 {
-    return m_reference->getSampleRate();
+    auto reference = ModelById::get(m_reference);
+    if (reference) {
+        return reference->getSampleRate();
+    } else {
+        return 0;
+    }
 }
 
 bool
 AlignmentModel::isReady(int *completion) const
 {
-    if (!m_pathBegun && m_rawPath) {
+    if (!m_pathBegun && !m_pathSource.isNone()) {
         if (completion) *completion = 0;
 #ifdef DEBUG_ALIGNMENT_MODEL
         SVCERR << "AlignmentModel::isReady: path not begun" << endl;
@@ -120,9 +118,9 @@
 #endif
         return true;
     }
-    if (!m_rawPath) {
+    if (m_pathSource.isNone()) {
         // lack of raw path could mean path is complete (in which case
-        // m_pathComplete true above) or else no alignment has been
+        // m_pathComplete true above) or else no path source has been
         // set at all yet (this case)
         if (completion) *completion = 0;
 #ifdef DEBUG_ALIGNMENT_MODEL
@@ -130,7 +128,13 @@
 #endif
         return false;
     }
-    return m_rawPath->isReady(completion);
+    auto pathSourceModel =
+        ModelById::getAs<SparseTimeValueModel>(m_pathSource);
+    if (pathSourceModel) {
+        return pathSourceModel->isReady(completion);
+    } else {
+        return true; // there is no meaningful answer here
+    }
 }
 
 const ZoomConstraint *
@@ -139,13 +143,13 @@
     return nullptr;
 }
 
-const Model *
+ModelId
 AlignmentModel::getReferenceModel() const
 {
     return m_reference;
 }
 
-const Model *
+ModelId
 AlignmentModel::getAlignedModel() const
 {
     return m_aligned;
@@ -158,10 +162,16 @@
     cerr << "AlignmentModel::toReference(" << frame << ")" << endl;
 #endif
     if (!m_path) {
-        if (!m_rawPath) return frame;
+        if (m_pathSource.isNone()) {
+            return frame;
+        }
         constructPath();
     }
-    return align(m_path, frame);
+    if (!m_path) {
+        return frame;
+    }
+
+    return performAlignment(*m_path, frame);
 }
 
 sv_frame_t
@@ -171,25 +181,20 @@
     cerr << "AlignmentModel::fromReference(" << frame << ")" << endl;
 #endif
     if (!m_reversePath) {
-        if (!m_rawPath) return frame;
+        if (m_pathSource.isNone()) {
+            return frame;
+        }
         constructReversePath();
     }
-    return align(m_reversePath, frame);
+    if (!m_reversePath) {
+        return frame;
+    }
+
+    return performAlignment(*m_reversePath, frame);
 }
 
 void
-AlignmentModel::pathChanged()
-{
-    if (m_pathComplete) {
-        cerr << "AlignmentModel: deleting raw path model" << endl;
-        if (m_rawPath) m_rawPath->aboutToDelete();
-        delete m_rawPath;
-        m_rawPath = nullptr;
-    }
-}
-
-void
-AlignmentModel::pathChangedWithin(sv_frame_t, sv_frame_t)
+AlignmentModel::pathSourceChangedWithin(ModelId, sv_frame_t, sv_frame_t)
 {
     if (!m_pathComplete) return;
     constructPath();
@@ -197,15 +202,18 @@
 }    
 
 void
-AlignmentModel::pathCompletionChanged()
+AlignmentModel::pathSourceCompletionChanged(ModelId)
 {
-    if (!m_rawPath) return;
+    auto pathSourceModel =
+        ModelById::getAs<SparseTimeValueModel>(m_pathSource);
+    if (!pathSourceModel) return;
+    
     m_pathBegun = true;
 
     if (!m_pathComplete) {
 
         int completion = 0;
-        m_rawPath->isReady(&completion);
+        pathSourceModel->isReady(&completion);
 
 #ifdef DEBUG_ALIGNMENT_MODEL
         SVCERR << "AlignmentModel::pathCompletionChanged: completion = "
@@ -225,32 +233,39 @@
         }
     }
 
-    emit completionChanged();
+    emit completionChanged(getId());
 }
 
 void
 AlignmentModel::constructPath() const
 {
+    auto alignedModel = ModelById::get(m_aligned);
+    if (!alignedModel) return;
+    
+    auto pathSourceModel =
+        ModelById::getAs<SparseTimeValueModel>(m_pathSource);
     if (!m_path) {
-        if (!m_rawPath) {
+        if (!pathSourceModel) {
             cerr << "ERROR: AlignmentModel::constructPath: "
-                      << "No raw path available" << endl;
+                 << "No raw path available (id is " << m_pathSource
+                 << ")" << endl;
             return;
         }
-        m_path = new PathModel
-            (m_rawPath->getSampleRate(), m_rawPath->getResolution(), false);
+        m_path.reset(new Path
+                     (pathSourceModel->getSampleRate(),
+                      pathSourceModel->getResolution()));
     } else {
-        if (!m_rawPath) return;
+        if (!pathSourceModel) return;
     }
         
     m_path->clear();
 
-    EventVector points = m_rawPath->getAllEvents();
+    EventVector points = pathSourceModel->getAllEvents();
 
     for (const auto &p: points) {
         sv_frame_t frame = p.getFrame();
         double value = p.getValue();
-        sv_frame_t rframe = lrint(value * m_aligned->getSampleRate());
+        sv_frame_t rframe = lrint(value * alignedModel->getSampleRate());
         m_path->add(PathPoint(frame, rframe));
     }
 
@@ -268,20 +283,20 @@
                       << "No forward path available" << endl;
             return;
         }
-        m_reversePath = new PathModel
-            (m_path->getSampleRate(), m_path->getResolution(), false);
+        m_reversePath.reset(new Path
+                            (m_path->getSampleRate(),
+                             m_path->getResolution()));
     } else {
         if (!m_path) return;
     }
         
     m_reversePath->clear();
 
-    PathModel::PointList points = m_path->getPoints();
+    Path::Points points = m_path->getPoints();
         
-    for (PathModel::PointList::const_iterator i = points.begin();
-         i != points.end(); ++i) {
-        sv_frame_t frame = i->frame;
-        sv_frame_t rframe = i->mapframe;
+    for (auto p: points) {
+        sv_frame_t frame = p.frame;
+        sv_frame_t rframe = p.mapframe;
         m_reversePath->add(PathPoint(rframe, frame));
     }
 
@@ -291,16 +306,14 @@
 }
 
 sv_frame_t
-AlignmentModel::align(PathModel *path, sv_frame_t frame) const
+AlignmentModel::performAlignment(const Path &path, sv_frame_t frame) const
 {
-    if (!path) return frame;
-
     // The path consists of a series of points, each with frame equal
     // to the frame on the source model and mapframe equal to the
     // frame on the target model.  Both should be monotonically
     // increasing.
 
-    const PathModel::PointList &points = path->getPoints();
+    const Path::Points &points = path.getPoints();
 
     if (points.empty()) {
 #ifdef DEBUG_ALIGNMENT_MODEL
@@ -314,14 +327,16 @@
 #endif
 
     PathPoint point(frame);
-    PathModel::PointList::const_iterator i = points.lower_bound(point);
+    Path::Points::const_iterator i = points.lower_bound(point);
     if (i == points.end()) {
 #ifdef DEBUG_ALIGNMENT_MODEL
         cerr << "Note: i == points.end()" << endl;
 #endif
         --i;
     }
-    while (i != points.begin() && i->frame > frame) --i;
+    while (i != points.begin() && i->frame > frame) {
+        --i;
+    }
 
     sv_frame_t foundFrame = i->frame;
     sv_frame_t foundMapFrame = i->mapframe;
@@ -347,7 +362,9 @@
          << followingMapFrame << endl;
 #endif
     
-    if (foundMapFrame < 0) return 0;
+    if (foundMapFrame < 0) {
+        return 0;
+    }
 
     sv_frame_t resultFrame = foundMapFrame;
 
@@ -366,49 +383,37 @@
 }
 
 void
-AlignmentModel::setPathFrom(SparseTimeValueModel *rawpath)
+AlignmentModel::setPathFrom(ModelId pathSource)
 {
-    if (m_rawPath) m_rawPath->aboutToDelete();
-    delete m_rawPath;
+    m_pathSource = pathSource;
+    
+    auto pathSourceModel =
+        ModelById::getAs<SparseTimeValueModel>(m_pathSource);
+    
+    if (pathSourceModel) {
 
-    m_rawPath = rawpath;
+        connect(pathSourceModel.get(),
+                SIGNAL(modelChangedWithin(ModelId, sv_frame_t, sv_frame_t)),
+                this, SLOT(pathSourceChangedWithin(ModelId, sv_frame_t, sv_frame_t)));
+        
+        connect(pathSourceModel.get(), SIGNAL(completionChanged(ModelId)),
+                this, SLOT(pathSourceCompletionChanged(ModelId)));
 
-    if (!m_rawPath) {
-        return;
+        constructPath();
+        constructReversePath();
+
+        if (pathSourceModel->isReady()) {
+            pathSourceCompletionChanged(m_pathSource);
+        }
     }
-
-    connect(m_rawPath, SIGNAL(modelChanged()),
-            this, SLOT(pathChanged()));
-
-    connect(m_rawPath, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
-            this, SLOT(pathChangedWithin(sv_frame_t, sv_frame_t)));
-        
-    connect(m_rawPath, SIGNAL(completionChanged()),
-            this, SLOT(pathCompletionChanged()));
-    
-    constructPath();
-    constructReversePath();
-
-    if (m_rawPath->isReady()) {
-        pathCompletionChanged();
-    }        
 }
 
 void
-AlignmentModel::setPath(PathModel *path)
+AlignmentModel::setPath(const Path &path)
 {
-    if (m_path) m_path->aboutToDelete();
-    delete m_path;
-    m_path = path;
+    m_path.reset(new Path(path));
     m_pathComplete = true;
-#ifdef DEBUG_ALIGNMENT_MODEL
-    cerr << "AlignmentModel::setPath: path = " << m_path << endl;
-#endif
     constructReversePath();
-#ifdef DEBUG_ALIGNMENT_MODEL
-    cerr << "AlignmentModel::setPath: after construction path = "
-              << m_path << ", rpath = " << m_reversePath << endl;
-#endif
 }
     
 void
@@ -425,10 +430,8 @@
 
     Model::toXml(stream, indent,
                  QString("type=\"alignment\" reference=\"%1\" aligned=\"%2\" path=\"%3\" %4")
-                 .arg(m_reference->getExportId())
-                 .arg(m_aligned->getExportId())
+                 .arg(ModelById::getExportId(m_reference))
+                 .arg(ModelById::getExportId(m_aligned))
                  .arg(m_path->getExportId())
                  .arg(extraAttributes));
 }
-
-
--- a/data/model/AlignmentModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/AlignmentModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -17,7 +17,7 @@
 #define SV_ALIGNMENT_MODEL_H
 
 #include "Model.h"
-#include "PathModel.h"
+#include "Path.h"
 #include "base/RealTime.h"
 
 #include <QString>
@@ -30,9 +30,9 @@
     Q_OBJECT
 
 public:
-    AlignmentModel(Model *reference,
-                   Model *aligned,
-                   SparseTimeValueModel *path);
+    AlignmentModel(ModelId reference /* any model */,
+                   ModelId aligned /* any model */,
+                   ModelId path /* a SparseTimeValueModel */);
     ~AlignmentModel();
 
     bool isOK() const override;
@@ -53,18 +53,18 @@
 
     QString getTypeName() const override { return tr("Alignment"); }
 
-    const Model *getReferenceModel() const;
-    const Model *getAlignedModel() const;
+    ModelId getReferenceModel() const;
+    ModelId getAlignedModel() const;
 
     sv_frame_t toReference(sv_frame_t frame) const;
     sv_frame_t fromReference(sv_frame_t frame) const;
 
-    void setPathFrom(SparseTimeValueModel *rawpath);
-    void setPath(PathModel *path);
+    void setPathFrom(ModelId pathSource); // a SparseTimeValueModel
+    void setPath(const Path &path);
 
     void toXml(QTextStream &stream,
-                       QString indent = "",
-                       QString extraAttributes = "") const override;
+               QString indent = "",
+               QString extraAttributes = "") const override;
 
     QString toDelimitedDataString(QString, DataExportOptions,
                                   sv_frame_t, sv_frame_t) const override {
@@ -72,22 +72,21 @@
     }
 
 signals:
-    void modelChanged();
-    void modelChangedWithin(sv_frame_t startFrame, sv_frame_t endFrame);
-    void completionChanged();
+    void completionChanged(ModelId);
 
 protected slots:
-    void pathChanged();
-    void pathChangedWithin(sv_frame_t startFrame, sv_frame_t endFrame);
-    void pathCompletionChanged();
+    void pathSourceChangedWithin(ModelId, sv_frame_t startFrame, sv_frame_t endFrame);
+    void pathSourceCompletionChanged(ModelId);
 
 protected:
-    Model *m_reference; // I don't own this
-    Model *m_aligned; // I don't own this
+    ModelId m_reference;
+    ModelId m_aligned;
 
-    SparseTimeValueModel *m_rawPath; // I own this
-    mutable PathModel *m_path; // I own this
-    mutable PathModel *m_reversePath; // I own this
+    ModelId m_pathSource; // a SparseTimeValueModel, which we need a
+                          // handle on only while it's still being generated
+
+    mutable std::unique_ptr<Path> m_path;
+    mutable std::unique_ptr<Path> m_reversePath;
     bool m_pathBegun;
     bool m_pathComplete;
     QString m_error;
@@ -95,7 +94,7 @@
     void constructPath() const;
     void constructReversePath() const;
 
-    sv_frame_t align(PathModel *path, sv_frame_t frame) const;
+    sv_frame_t performAlignment(const Path &path, sv_frame_t frame) const;
 };
 
 #endif
--- a/data/model/DeferredNotifier.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/DeferredNotifier.h	Wed Jul 17 14:24:51 2019 +0100
@@ -30,7 +30,8 @@
         NOTIFY_DEFERRED
     };
     
-    DeferredNotifier(Model *m, Mode mode) : m_model(m), m_mode(mode) { }
+    DeferredNotifier(Model *m, ModelId id, Mode mode) :
+        m_model(m), m_modelId(id), m_mode(mode) { }
 
     Mode getMode() const {
         return m_mode;
@@ -41,7 +42,7 @@
     
     void update(sv_frame_t frame, sv_frame_t duration) {
         if (m_mode == NOTIFY_ALWAYS) {
-            m_model->modelChangedWithin(frame, frame + duration);
+            m_model->modelChangedWithin(m_modelId, frame, frame + duration);
         } else {
             QMutexLocker locker(&m_mutex);
             m_extents.sample(frame);
@@ -60,7 +61,7 @@
             }
         }
         if (shouldEmit) {
-            m_model->modelChangedWithin(from, to);
+            m_model->modelChangedWithin(m_modelId, from, to);
             QMutexLocker locker(&m_mutex);
             m_extents.reset();
         }
@@ -68,6 +69,7 @@
 
 private:
     Model *m_model;
+    ModelId m_modelId;
     Mode m_mode;
     QMutex m_mutex;
     Extents<sv_frame_t> m_extents;
--- a/data/model/Dense3DModelPeakCache.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/Dense3DModelPeakCache.cpp	Wed Jul 17 14:24:51 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()),
-            this, SLOT(sourceModelChanged()));
-    connect(source, SIGNAL(aboutToBeDeleted()),
-            this, SLOT(sourceModelAboutToBeDeleted()));
+    m_cache.reset(new EditableDenseThreeDimensionalModel
+                  (source->getSampleRate(),
+                   source->getResolution() * m_columnsPerPeak,
+                   source->getHeight(),
+                   EditableDenseThreeDimensionalModel::NoCompression,
+                   false));
+
+    connect(source.get(), SIGNAL(modelChanged(ModelId)),
+            this, SLOT(sourceModelChanged(ModelId)));
 }
 
 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,15 +56,13 @@
 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);
 }
 
 void
-Dense3DModelPeakCache::sourceModelChanged()
+Dense3DModelPeakCache::sourceModelChanged(ModelId)
 {
-    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	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/Dense3DModelPeakCache.h	Wed Jul 17 14:24:51 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,
@@ -103,12 +116,11 @@
     }
 
 protected slots:
-    void sourceModelChanged();
-    void sourceModelAboutToBeDeleted();
+    void sourceModelChanged(ModelId);
 
 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/DenseTimeValueModel.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/DenseTimeValueModel.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -14,19 +14,8 @@
 */
 
 #include "DenseTimeValueModel.h"
-#include "base/PlayParameterRepository.h"
 
 #include <QStringList>
-
-DenseTimeValueModel::DenseTimeValueModel()
-{
-    PlayParameterRepository::getInstance()->addPlayable(this);
-}
-
-DenseTimeValueModel::~DenseTimeValueModel()
-{
-    PlayParameterRepository::getInstance()->removePlayable(this);
-}
         
 QString
 DenseTimeValueModel::toDelimitedDataString(QString delimiter,
--- a/data/model/DenseTimeValueModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/DenseTimeValueModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -32,9 +32,9 @@
     Q_OBJECT
 
 public:
-    DenseTimeValueModel();
+    DenseTimeValueModel() { }
 
-    virtual ~DenseTimeValueModel();
+    virtual ~DenseTimeValueModel() { }
 
     /**
      * Return the minimum possible value found in this model type.
--- a/data/model/EditableDenseThreeDimensionalModel.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/EditableDenseThreeDimensionalModel.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -359,15 +359,16 @@
 
     if (m_notifyOnAdd) {
         if (allChange) {
-            emit modelChanged();
+            emit modelChanged(getId());
         } else {
-            emit modelChangedWithin(windowStart, windowStart + m_resolution);
+            emit modelChangedWithin(getId(),
+                                    windowStart, windowStart + m_resolution);
         }
     } else {
         if (allChange) {
             m_sinceLastNotifyMin = -1;
             m_sinceLastNotifyMax = -1;
-            emit modelChanged();
+            emit modelChanged(getId());
         } else {
             if (m_sinceLastNotifyMin == -1 ||
                 windowStart < m_sinceLastNotifyMin) {
@@ -393,14 +394,14 @@
 {
     while ((int)m_binNames.size() <= n) m_binNames.push_back("");
     m_binNames[n] = name;
-    emit modelChanged();
+    emit modelChanged(getId());
 }
 
 void
 EditableDenseThreeDimensionalModel::setBinNames(std::vector<QString> names)
 {
     m_binNames = names;
-    emit modelChanged();
+    emit modelChanged(getId());
 }
 
 bool
@@ -474,21 +475,22 @@
         if (completion == 100) {
 
             m_notifyOnAdd = true; // henceforth
-            emit modelChanged();
+            emit modelChanged(getId());
 
         } else if (!m_notifyOnAdd) {
 
             if (update &&
                 m_sinceLastNotifyMin >= 0 &&
                 m_sinceLastNotifyMax >= 0) {
-                emit modelChangedWithin(m_sinceLastNotifyMin,
+                emit modelChangedWithin(getId(),
+                                        m_sinceLastNotifyMin,
                                         m_sinceLastNotifyMax + m_resolution);
                 m_sinceLastNotifyMin = m_sinceLastNotifyMax = -1;
             } else {
-                emit completionChanged();
+                emit completionChanged(getId());
             }
         } else {
-            emit completionChanged();
+            emit completionChanged(getId());
         }            
     }
 }
--- a/data/model/EventCommands.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/EventCommands.h	Wed Jul 17 14:24:51 2019 +0100
@@ -18,6 +18,7 @@
 
 #include "base/Event.h"
 #include "base/Command.h"
+#include "base/ById.h"
 
 /**
  * Interface for classes that can be modified through these commands
@@ -29,45 +30,77 @@
     virtual void remove(Event e) = 0;
 };
 
+class WithEditable
+{
+protected:
+    WithEditable(int editableId) : m_editableId(editableId) { }
+
+    std::shared_ptr<EventEditable> getEditable() {
+        auto editable = AnyById::getAs<EventEditable>(m_editableId);
+        if (!editable) {
+            SVCERR << "WARNING: Id passed to EventEditable command is not that of an EventEditable" << endl;
+        }
+        return editable;
+    }
+
+private:
+    int m_editableId;
+};
+
 /**
- * Command to add an event to an editable containing events, with undo.
+ * Command to add an event to an editable containing events, with
+ * undo.  The id must be that of a type that can be retrieved from the
+ * AnyById store and dynamic_cast to EventEditable.
  */
-class AddEventCommand : public Command
+class AddEventCommand : public Command,
+                        public WithEditable
 {
 public:
-    AddEventCommand(EventEditable *editable, const Event &e, QString name) :
-        m_editable(editable), m_event(e), m_name(name) { }
+    AddEventCommand(int editableId, const Event &e, QString name) :
+        WithEditable(editableId), m_event(e), m_name(name) { }
 
     QString getName() const override { return m_name; }
     Event getEvent() const { return m_event; }
 
-    void execute() override { m_editable->add(m_event); }
-    void unexecute() override { m_editable->remove(m_event); }
+    void execute() override {
+        auto editable = getEditable();
+        if (editable) editable->add(m_event);
+    }
+    void unexecute() override {
+        auto editable = getEditable();
+        if (editable) editable->remove(m_event);
+    }
 
 private:
-    EventEditable *m_editable;
     Event m_event;
     QString m_name;
 };
 
 /**
  * Command to remove an event from an editable containing events, with
- * undo.
+ * undo.  The id must be that of a type that can be retrieved from the
+ * AnyById store and dynamic_cast to EventEditable.
  */
-class RemoveEventCommand : public Command
+class RemoveEventCommand : public Command,
+                           public WithEditable
 {
 public:
-    RemoveEventCommand(EventEditable *editable, const Event &e, QString name) :
-        m_editable(editable), m_event(e), m_name(name) { }
+    RemoveEventCommand(int editableId, const Event &e, QString name) :
+        WithEditable(editableId), m_event(e), m_name(name) { }
 
     QString getName() const override { return m_name; }
     Event getEvent() const { return m_event; }
 
-    void execute() override { m_editable->remove(m_event); }
-    void unexecute() override { m_editable->add(m_event); }
+    void execute() override {
+        auto editable = getEditable();
+        if (editable) editable->remove(m_event);
+    }
+    void unexecute() override {
+        auto editable = getEditable();
+        if (editable) editable->add(m_event);
+    }
 
 private:
-    EventEditable *m_editable;
     Event m_event;
     QString m_name;
 };
@@ -76,19 +109,20 @@
  * Command to add or remove a series of events to or from an editable,
  * with undo. Creates and immediately executes a sub-command for each
  * add/remove requested. Consecutive add/remove pairs for the same
- * point are collapsed.
+ * point are collapsed.  The id must be that of a type that can be
+ * retrieved from the AnyById store and dynamic_cast to EventEditable.
  */
 class ChangeEventsCommand : public MacroCommand
 {
 public:
-    ChangeEventsCommand(EventEditable *editable, QString name) :
-        MacroCommand(name), m_editable(editable) { }
+    ChangeEventsCommand(int editableId, QString name) :
+        MacroCommand(name), m_editableId(editableId) { }
 
     void add(Event e) {
-        addCommand(new AddEventCommand(m_editable, e, getName()), true);
+        addCommand(new AddEventCommand(m_editableId, e, getName()), true);
     }
     void remove(Event e) {
-        addCommand(new RemoveEventCommand(m_editable, e, getName()), true);
+        addCommand(new RemoveEventCommand(m_editableId, e, getName()), true);
     }
 
     /**
@@ -134,7 +168,7 @@
         MacroCommand::addCommand(command);
     }
 
-    EventEditable *m_editable;
+    int m_editableId;
 };
 
 #endif
--- a/data/model/FFTModel.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/FFTModel.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -32,13 +32,13 @@
 static HitCount inSmallCache("FFTModel: Small FFT cache");
 static HitCount inSourceCache("FFTModel: Source data cache");
 
-FFTModel::FFTModel(const DenseTimeValueModel *model,
+FFTModel::FFTModel(ModelId modelId,
                    int channel,
                    WindowType windowType,
                    int windowSize,
                    int windowIncrement,
                    int fftSize) :
-    m_model(model),
+    m_model(modelId),
     m_channel(channel),
     m_windowType(windowType),
     m_windowSize(windowSize),
@@ -61,32 +61,51 @@
 
     m_fft.initFloat();
 
-    connect(model, SIGNAL(modelChanged()),
-            this, SIGNAL(modelChanged()));
-    connect(model, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
-            this, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)));
-    connect(model, SIGNAL(aboutToBeDeleted()),
-            this, SLOT(sourceModelAboutToBeDeleted()));
+    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
+    if (model) {
+        connect(model.get(), SIGNAL(modelChanged(ModelId)),
+                this, SIGNAL(modelChanged(ModelId)));
+        connect(model.get(), SIGNAL(modelChangedWithin(ModelId, sv_frame_t, sv_frame_t)),
+                this, SIGNAL(modelChangedWithin(ModelId, sv_frame_t, sv_frame_t)));
+    }
 }
 
 FFTModel::~FFTModel()
 {
 }
 
-void
-FFTModel::sourceModelAboutToBeDeleted()
+bool
+FFTModel::isOK() const
 {
-    if (m_model) {
-        SVDEBUG << "FFTModel[" << this << "]::sourceModelAboutToBeDeleted(" << m_model << ")" << endl;
-        m_model = nullptr;
+    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
+    return (model && model->isOK());
+}
+
+int
+FFTModel::getCompletion() const
+{
+    int c = 100;
+    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
+    if (model) {
+        if (model->isReady(&c)) return 100;
     }
+    return c;
+}
+
+sv_samplerate_t
+FFTModel::getSampleRate() const
+{
+    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
+    if (model) return model->getSampleRate();
+    else return 0;
 }
 
 int
 FFTModel::getWidth() const
 {
-    if (!m_model) return 0;
-    return int((m_model->getEndFrame() - m_model->getStartFrame())
+    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
+    if (!model) return 0;
+    return int((model->getEndFrame() - model->getStartFrame())
                / m_windowIncrement) + 1;
 }
 
@@ -277,7 +296,8 @@
 {
     Profiler profiler("FFTModel::getSourceDataUncached");
 
-    if (!m_model) return {};
+    auto model = ModelById::getAs<DenseTimeValueModel>(m_model);
+    if (!model) return {};
     
     decltype(range.first) pfx = 0;
     if (range.first < 0) {
@@ -285,14 +305,14 @@
         range = { 0, range.second };
     }
 
-    auto data = m_model->getData(m_channel,
-                                 range.first,
-                                 range.second - range.first);
+    auto data = model->getData(m_channel,
+                               range.first,
+                               range.second - range.first);
 
     if (data.empty()) {
         SVDEBUG << "NOTE: empty source data for range (" << range.first << ","
                 << range.second << ") (model end frame "
-                << m_model->getEndFrame() << ")" << endl;
+                << model->getEndFrame() << ")" << endl;
     }
     
     // don't return a partial frame
@@ -304,7 +324,7 @@
     }
     
     if (m_channel == -1) {
-        int channels = m_model->getChannelCount();
+        int channels = model->getChannelCount();
         if (channels > 1) {
             int n = int(data.size());
             float factor = 1.f / float(channels);
--- a/data/model/FFTModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/FFTModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -50,7 +50,7 @@
      * unless the channel is -1 in which case merge all available
      * channels.
      */
-    FFTModel(const DenseTimeValueModel *model,
+    FFTModel(ModelId model, // a DenseTimeValueModel
              int channel,
              WindowType windowType,
              int windowSize,
@@ -63,14 +63,12 @@
     int getWidth() const override;
     int getHeight() const override;
     float getValueAt(int x, int y) const override { return getMagnitudeAt(x, y); }
-    bool isOK() const override { return m_model && m_model->isOK(); }
+    bool isOK() const override;
     sv_frame_t getStartFrame() const override { return 0; }
     sv_frame_t getTrueEndFrame() const override {
         return sv_frame_t(getWidth()) * getResolution() + getResolution();
     }
-    sv_samplerate_t getSampleRate() const override {
-        return isOK() ? m_model->getSampleRate() : 0;
-    }
+    sv_samplerate_t getSampleRate() const override;
     int getResolution() const override { return m_windowIncrement; }
     virtual int getYBinCount() const { return getHeight(); }
     float getMinimumLevel() const override { return 0.f; } // Can't provide
@@ -79,13 +77,7 @@
     virtual Column getPhases(int x) const;
     QString getBinName(int n) const override;
     bool shouldUseLogValueScale() const override { return true; }
-    int getCompletion() const override {
-        int c = 100;
-        if (m_model) {
-            if (m_model->isReady(&c)) return 100;
-        }
-        return c;
-    }
+    int getCompletion() const override;
     virtual QString getError() const { return ""; } //!!!???
     virtual sv_frame_t getFillExtent() const { return getEndFrame(); }
     QString toDelimitedDataString(QString, DataExportOptions,
@@ -143,14 +135,11 @@
 
     QString getTypeName() const override { return tr("FFT"); }
 
-public slots:
-    void sourceModelAboutToBeDeleted();
+private:
+    FFTModel(const FFTModel &) =delete;
+    FFTModel &operator=(const FFTModel &) =delete;
 
-private:
-    FFTModel(const FFTModel &); // not implemented
-    FFTModel &operator=(const FFTModel &); // not implemented
-
-    const DenseTimeValueModel *m_model;
+    const ModelId m_model; // a DenseTimeValueModel
     int m_channel;
     WindowType m_windowType;
     int m_windowSize;
--- a/data/model/ImageModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/ImageModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -48,6 +48,7 @@
         m_sampleRate(sampleRate),
         m_resolution(resolution),
         m_notifier(this,
+                   getId(),
                    notifyOnAdd ?
                    DeferredNotifier::NOTIFY_ALWAYS :
                    DeferredNotifier::NOTIFY_DEFERRED),
@@ -84,12 +85,12 @@
             m_notifier.makeDeferredNotifications();
         }
         
-        emit completionChanged();
+        emit completionChanged(getId());
 
         if (completion == 100) {
             // henceforth:
             m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS);
-            emit modelChanged();
+            emit modelChanged(getId());
         }
     }
     
@@ -152,7 +153,8 @@
         {   QMutexLocker locker(&m_mutex);
             m_events.remove(e);
         }
-        emit modelChangedWithin(e.getFrame(), e.getFrame() + m_resolution);
+        emit modelChangedWithin(getId(),
+                                e.getFrame(), e.getFrame() + m_resolution);
     }
 
     /**
@@ -231,8 +233,7 @@
         case 3: e1 = e0.withLabel(value.toString()); break;
         }
 
-        ChangeEventsCommand *command =
-            new ChangeEventsCommand(this, tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- a/data/model/Labeller.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/Labeller.h	Wed Jul 17 14:24:51 2019 +0100
@@ -225,14 +225,16 @@
      * Relabel all events in the given event vector that lie within
      * the given multi-selection, according to the labelling
      * properties of this labeller.  Return a command that has been
-     * executed but not yet added to the history.
+     * executed but not yet added to the history.  The id must be that
+     * of a type that can be retrieved from the AnyById store and
+     * dynamic_cast to EventEditable.
      */
-    Command *labelAll(EventEditable *editable,
+    Command *labelAll(int editableId,
                       MultiSelection *ms,
                       const EventVector &allEvents) {
 
-        ChangeEventsCommand *command = new ChangeEventsCommand
-            (editable, tr("Label Points"));
+        auto command = new ChangeEventsCommand
+            (editableId, tr("Label Points"));
 
         Event prev;
         bool havePrev = false;
@@ -270,15 +272,17 @@
      * that event lies within the given multi-selection, add n-1 new
      * events at equally spaced intervals between it and the following
      * event.  Return a command that has been executed but not yet
-     * added to the history.
+     * added to the history.  The id must be that of a type that can
+     * be retrieved from the AnyById store and dynamic_cast to
+     * EventEditable.
      */
-    Command *subdivide(EventEditable *editable,
+    Command *subdivide(int editableId,
                        MultiSelection *ms,
                        const EventVector &allEvents,
                        int n) {
 
-        ChangeEventsCommand *command = new ChangeEventsCommand
-            (editable, tr("Subdivide Points"));
+        auto command = new ChangeEventsCommand
+            (editableId, tr("Subdivide Points"));
 
         for (auto i = allEvents.begin(); i != allEvents.end(); ++i) {
 
@@ -319,15 +323,17 @@
      * multi-selection, and a number n, remove all but every nth event
      * from the vector within the extents of the multi-selection.
      * Return a command that has been executed but not yet added to
-     * the history.
+     * the history. The id must be that of a type that can be
+     * retrieved from the AnyById store and dynamic_cast to
+     * EventEditable.
      */
-    Command *winnow(EventEditable *editable,
+    Command *winnow(int editableId,
                     MultiSelection *ms,
                     const EventVector &allEvents,
                     int n) {
 
-        ChangeEventsCommand *command = new ChangeEventsCommand
-            (editable, tr("Winnow Points"));
+        auto command = new ChangeEventsCommand
+            (editableId, tr("Winnow Points"));
 
         int counter = 0;
         
--- a/data/model/Model.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/Model.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -24,141 +24,90 @@
 
 Model::~Model()
 {
-    SVDEBUG << "Model::~Model(" << this << ")" << endl;
-
-    if (!m_aboutToDelete) {
-        SVDEBUG << "NOTE: Model(" << this << ", \""
-                << objectName() << "\", type uri <"
-                << m_typeUri << ">)::~Model(): Model deleted "
-                << "with no aboutToDelete notification"
-                << endl;
-    }
-
-    if (m_alignment) {
-        m_alignment->aboutToDelete();
-        delete m_alignment;
-    }
-}
-
-int
-Model::getNextId()
-{
-    static int nextId = 0;
-    static QMutex mutex;
-    QMutexLocker locker(&mutex);
-    int i = nextId;
-    if (nextId == INT_MAX) {
-        nextId = INT_MIN;
-    }
-    ++nextId;
-    return i;
+    SVDEBUG << "Model::~Model: " << this << " with id " << getId() << endl;
 }
 
 void
-Model::setSourceModel(Model *model)
+Model::setSourceModel(ModelId modelId)
 {
-    if (m_sourceModel) {
-        disconnect(m_sourceModel, SIGNAL(aboutToBeDeleted()),
-                   this, SLOT(sourceModelAboutToBeDeleted()));
-    }
+    m_sourceModel = modelId;
 
-    m_sourceModel = model;
-
-    if (m_sourceModel) {
-        connect(m_sourceModel, SIGNAL(alignmentCompletionChanged()),
-                this, SIGNAL(alignmentCompletionChanged()));
-        connect(m_sourceModel, SIGNAL(aboutToBeDeleted()),
-                this, SLOT(sourceModelAboutToBeDeleted()));
+    auto model = ModelById::get(m_sourceModel);
+    if (model) {
+        connect(model.get(), SIGNAL(alignmentCompletionChanged(ModelId)),
+                this, SIGNAL(alignmentCompletionChanged(ModelId)));
     }
 }
 
 void
-Model::aboutToDelete()
-{
-    SVDEBUG << "Model(" << this << ", \""
-            << objectName() << "\", type name \""
-            << getTypeName() << "\", type uri <"
-            << m_typeUri << ">)::aboutToDelete()" << endl;
-
-    if (m_aboutToDelete) {
-        SVDEBUG << "WARNING: Model(" << this << ", \""
-                << objectName() << "\", type uri <"
-                << m_typeUri << ">)::aboutToDelete: "
-                << "aboutToDelete called more than once for the same model"
-                << endl;
-    }
-
-    emit aboutToBeDeleted();
-    m_aboutToDelete = true;
-}
-
-void
-Model::sourceModelAboutToBeDeleted()
-{
-    m_sourceModel = nullptr;
-}
-
-void
-Model::setAlignment(AlignmentModel *alignment)
+Model::setAlignment(ModelId alignmentModel)
 {
     SVDEBUG << "Model(" << this << "): accepting alignment model "
-            << alignment << endl;
-    
-    if (m_alignment) {
-        m_alignment->aboutToDelete();
-        delete m_alignment;
+            << alignmentModel << endl;
+
+    if (auto model = ModelById::get(m_alignmentModel)) {
+        disconnect(model.get(), SIGNAL(completionChanged(ModelId)),
+                   this, SIGNAL(alignmentCompletionChanged(ModelId)));
     }
     
-    m_alignment = alignment;
+    m_alignmentModel = alignmentModel;
 
-    if (m_alignment) {
-        connect(m_alignment, SIGNAL(completionChanged()),
-                this, SIGNAL(alignmentCompletionChanged()));
+    if (auto model = ModelById::get(m_alignmentModel)) {
+        connect(model.get(), SIGNAL(completionChanged(ModelId)),
+                this, SIGNAL(alignmentCompletionChanged(ModelId)));
     }
 }
 
-const AlignmentModel *
+const ModelId
 Model::getAlignment() const
 {
-    return m_alignment;
+    return m_alignmentModel;
 }
 
-const Model *
+const ModelId
 Model::getAlignmentReference() const
 {
-    if (!m_alignment) {
-        if (m_sourceModel) return m_sourceModel->getAlignmentReference();
-        return nullptr;
-    }
-    return m_alignment->getReferenceModel();
+    auto model = ModelById::getAs<AlignmentModel>(m_alignmentModel);
+    if (model) return model->getReferenceModel();
+    else return {};
 }
 
 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;
 }
 
@@ -166,17 +115,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;
@@ -185,21 +143,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/data/model/Model.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/Model.h	Wed Jul 17 14:24:51 2019 +0100
@@ -19,6 +19,7 @@
 #include <vector>
 #include <QObject>
 
+#include "base/ById.h"
 #include "base/XmlExportable.h"
 #include "base/Playable.h"
 #include "base/BaseTypes.h"
@@ -27,20 +28,20 @@
 class ZoomConstraint;
 class AlignmentModel;
 
-typedef int ModelId;
-
 /** 
  * Model is the base class for all data models that represent any sort
  * of data on a time scale based on an audio frame rate.
  */
-
 class Model : public QObject,
+              public WithTypedId<Model>,
               public XmlExportable,
               public Playable
 {
     Q_OBJECT
 
 public:
+    typedef Id ModelId;
+    
     virtual ~Model();
 
     /**
@@ -132,33 +133,6 @@
     virtual bool isSparse() const { return false; }
 
     /**
-     * Return an id for this model. The id is guaranteed to be a
-     * unique identifier for this model among all models that may ever
-     * exist within this single run of the application.
-     */
-    ModelId getId() const { return m_id; }
-    
-    /**
-     * Mark the model as abandoning. This means that the application
-     * no longer needs it, so it can stop doing any background
-     * calculations it may be involved in. Note that as far as the
-     * model API is concerned, this does nothing more than tell the
-     * model to return true from isAbandoning().  The actual response
-     * to this will depend on the model's context -- it's possible
-     * nothing at all will change.
-     */
-    virtual void abandon() {
-        m_abandoning = true;
-    }
-
-    /**
-     * Query whether the model has been marked as abandoning.
-     */
-    virtual bool isAbandoning() const { 
-        return m_abandoning;
-    }
-
-    /**
      * Return true if the model has finished loading or calculating
      * all its data, for a model that is capable of calculating in a
      * background thread.
@@ -208,28 +182,27 @@
     }
 
     /**
-     * If this model was derived from another, return the model it was
-     * derived from.  The assumption is that the source model's
-     * alignment will also apply to this model, unless some other
-     * property (such as a specific alignment model set on this model)
-     * indicates otherwise.
+     * If this model was derived from another, return the id of the
+     * model it was derived from.  The assumption is that the source
+     * model's alignment will also apply to this model, unless some
+     * other property (such as a specific alignment model set on this
+     * model) indicates otherwise.
      */
-    virtual Model *getSourceModel() const {
+    virtual ModelId getSourceModel() const {
         return m_sourceModel;
     }
 
     /**
      * Set the source model for this model.
      */
-    virtual void setSourceModel(Model *model);
+    virtual void setSourceModel(ModelId model);
 
     /**
-     * Specify an aligment between this model's timeline and that of a
-     * reference model.  The alignment model records both the
-     * reference and the alignment.  This model takes ownership of the
-     * alignment model.
+     * Specify an alignment between this model's timeline and that of
+     * a reference model. The alignment model, of type AlignmentModel,
+     * records both the reference and the alignment.
      */
-    virtual void setAlignment(AlignmentModel *alignment);
+    virtual void setAlignment(ModelId alignmentModel);
 
     /**
      * Retrieve the alignment model for this model.  This is not a
@@ -240,13 +213,13 @@
      * application for this function is in streaming out alignments to
      * the session file.
      */
-    virtual const AlignmentModel *getAlignment() const;
+    virtual const ModelId getAlignment() const;
 
     /**
      * Return the reference model for the current alignment timeline,
      * if any.
      */
-    virtual const Model *getAlignmentReference() const;
+    virtual const ModelId getAlignmentReference() const;
 
     /**
      * Return the frame number of the reference model that corresponds
@@ -290,22 +263,18 @@
                                           sv_frame_t startFrame,
                                           sv_frame_t duration) const = 0;
 
-public slots:
-    void aboutToDelete();
-    void sourceModelAboutToBeDeleted();
-
 signals:
     /**
      * Emitted when a model has been edited (or more data retrieved
      * from cache, in the case of a cached model that generates slowly)
      */
-    void modelChanged();
+    void modelChanged(ModelId myId);
 
     /**
      * Emitted when a model has been edited (or more data retrieved
      * from cache, in the case of a cached model that generates slowly)
      */
-    void modelChangedWithin(sv_frame_t startFrame, sv_frame_t endFrame);
+    void modelChangedWithin(ModelId myId, sv_frame_t startFrame, sv_frame_t endFrame);
 
     /**
      * Emitted when some internal processing has advanced a stage, but
@@ -313,52 +282,35 @@
      * updating any progress meters or other monitoring, but not
      * refreshing the actual view.
      */
-    void completionChanged();
+    void completionChanged(ModelId myId);
 
     /**
      * Emitted when internal processing is complete (i.e. when
      * isReady() would return true, with completion at 100).
      */
-    void ready();
+    void ready(ModelId myId);
 
     /**
      * Emitted when the completion percentage changes for the
      * calculation of this model's alignment model.
      */
-    void alignmentCompletionChanged();
-
-    /**
-     * Emitted when something notifies this model (through calling
-     * aboutToDelete() that it is about to delete it.  Note that this
-     * depends on an external agent such as a Document object or
-     * owning model telling the model that it is about to delete it;
-     * there is nothing in the model to guarantee that this signal
-     * will be emitted before the actual deletion.
-     */
-    void aboutToBeDeleted();
+    void alignmentCompletionChanged(ModelId myId);
 
 protected:
     Model() :
-        m_id(getNextId()),
-        m_sourceModel(0), 
-        m_alignment(0), 
-        m_abandoning(false), 
-        m_aboutToDelete(false),
         m_extendTo(0) { }
 
     // Not provided.
-    Model(const Model &);
-    Model &operator=(const Model &); 
+    Model(const Model &) =delete;
+    Model &operator=(const Model &) =delete;
 
-    const ModelId m_id;
-    Model *m_sourceModel;
-    AlignmentModel *m_alignment;
+    ModelId m_sourceModel;
+    ModelId m_alignmentModel;
     QString m_typeUri;
-    bool m_abandoning;
-    bool m_aboutToDelete;
     sv_frame_t m_extendTo;
-    
-    int getNextId();
 };
 
+typedef Model::Id ModelId;
+typedef TypedById<Model, Model::Id> ModelById;
+
 #endif
--- a/data/model/ModelDataTableModel.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/ModelDataTableModel.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -22,19 +22,18 @@
 #include <algorithm>
 #include <iostream>
 
-ModelDataTableModel::ModelDataTableModel(TabularModel *m) :
+ModelDataTableModel::ModelDataTableModel(ModelId m) :
     m_model(m),
     m_sortColumn(0),
     m_sortOrdering(Qt::AscendingOrder),
     m_currentRow(0)
 {
-    Model *baseModel = dynamic_cast<Model *>(m);
-
-    connect(baseModel, SIGNAL(modelChanged()), this, SLOT(modelChanged()));
-    connect(baseModel, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
-            this, SLOT(modelChangedWithin(sv_frame_t, sv_frame_t)));
-    connect(baseModel, SIGNAL(aboutToBeDeleted()),
-            this, SLOT(modelAboutToBeDeleted()));
+    auto model = ModelById::get(m);
+    if (model) {
+        connect(model.get(), SIGNAL(modelChanged()), this, SLOT(modelChanged()));
+        connect(model.get(), SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
+                this, SLOT(modelChangedWithin(sv_frame_t, sv_frame_t)));
+    }
 }
 
 ModelDataTableModel::~ModelDataTableModel()
@@ -44,19 +43,21 @@
 QVariant
 ModelDataTableModel::data(const QModelIndex &index, int role) const
 {
-    if (!m_model) return QVariant();
+    auto model = getTabularModel();
+    if (!model) return QVariant();
     if (role != Qt::EditRole && role != Qt::DisplayRole) return QVariant();
     if (!index.isValid()) return QVariant();
-    QVariant d = m_model->getData(getUnsorted(index.row()), index.column(), role);
+    QVariant d = model->getData(getUnsorted(index.row()), index.column(), role);
     return d;
 }
 
 bool
 ModelDataTableModel::setData(const QModelIndex &index, const QVariant &value, int role)
 {
-    if (!m_model) return false;
+    auto model = getTabularModel();
+    if (!model) return false;
     if (!index.isValid()) return false;
-    Command *command = m_model->getSetDataCommand(getUnsorted(index.row()),
+    Command *command = model->getSetDataCommand(getUnsorted(index.row()),
                                                   index.column(),
                                                   value, role);
     if (command) {
@@ -70,10 +71,11 @@
 bool
 ModelDataTableModel::insertRow(int row, const QModelIndex &parent)
 {
-    if (!m_model) return false;
+    auto model = getTabularModel();
+    if (!model) return false;
     if (parent.isValid()) return false;
 
-    Command *command = m_model->getInsertRowCommand(getUnsorted(row));
+    Command *command = model->getInsertRowCommand(getUnsorted(row));
 
     if (command) {
         emit addCommand(command);
@@ -85,10 +87,11 @@
 bool
 ModelDataTableModel::removeRow(int row, const QModelIndex &parent)
 {
-    if (!m_model) return false;
+    auto model = getTabularModel();
+    if (!model) return false;
     if (parent.isValid()) return false;
 
-    Command *command = m_model->getRemoveRowCommand(getUnsorted(row));
+    Command *command = model->getRemoveRowCommand(getUnsorted(row));
 
     if (command) {
         emit addCommand(command);
@@ -108,13 +111,14 @@
 QVariant
 ModelDataTableModel::headerData(int section, Qt::Orientation orientation, int role) const
 {
-    if (!m_model) return QVariant();
+    auto model = getTabularModel();
+    if (!model) return QVariant();
 
     if (orientation == Qt::Vertical && role == Qt::DisplayRole) {
         return section + 1;
     }
     if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
-        return m_model->getHeading(section);
+        return model->getHeading(section);
     } 
     return QVariant();
 }
@@ -134,38 +138,44 @@
 int
 ModelDataTableModel::rowCount(const QModelIndex &parent) const
 {
-    if (!m_model) return 0;
+    auto model = getTabularModel();
+    if (!model) return 0;
     if (parent.isValid()) return 0;
-    int count = m_model->getRowCount();
+    int count = model->getRowCount();
     return count;
 }
 
 int
 ModelDataTableModel::columnCount(const QModelIndex &parent) const
 {
-    if (!m_model) return 0;
+    auto model = getTabularModel();
+    if (!model) return 0;
     if (parent.isValid()) return 0;
-    return m_model->getColumnCount();
+    return model->getColumnCount();
 }
 
 QModelIndex 
 ModelDataTableModel::getModelIndexForFrame(sv_frame_t frame) const
 {
-    if (!m_model) return createIndex(0, 0);
-    int row = m_model->getRowForFrame(frame);
+    auto model = getTabularModel();
+    if (!model) return createIndex(0, 0);
+    int row = model->getRowForFrame(frame);
     return createIndex(getSorted(row), 0, (void *)nullptr);
 }
 
 sv_frame_t
 ModelDataTableModel::getFrameForModelIndex(const QModelIndex &index) const
 {
-    if (!m_model) return 0;
-    return m_model->getFrameForRow(getUnsorted(index.row()));
+    auto model = getTabularModel();
+    if (!model) return 0;
+    return model->getFrameForRow(getUnsorted(index.row()));
 }
 
 QModelIndex
 ModelDataTableModel::findText(QString text) const
 {
+    auto model = getTabularModel();
+    if (!model) return QModelIndex();
     if (text == "") return QModelIndex();
     int rows = rowCount();
     int cols = columnCount();
@@ -173,10 +183,10 @@
     for (int row = 1; row <= rows; ++row) {
         int wrapped = (row + current) % rows;
         for (int col = 0; col < cols; ++col) {
-            if (m_model->getSortType(col) != TabularModel::SortAlphabetical) {
+            if (model->getSortType(col) != TabularModel::SortAlphabetical) {
                 continue;
             }
-            QString cell = m_model->getData(getUnsorted(wrapped), col,
+            QString cell = model->getData(getUnsorted(wrapped), col,
                                             Qt::DisplayRole).toString();
             if (cell.contains(text, Qt::CaseInsensitive)) {
                 return createIndex(wrapped, col);
@@ -243,19 +253,13 @@
     emit layoutChanged();
 }
 
-void
-ModelDataTableModel::modelAboutToBeDeleted()
-{
-    m_model = nullptr;
-    emit modelRemoved();
-}
-
 int
 ModelDataTableModel::getSorted(int row) const
 {
-    if (!m_model) return row;
+    auto model = getTabularModel();
+    if (!model) return row;
 
-    if (m_model->isColumnTimeValue(m_sortColumn)) {
+    if (model->isColumnTimeValue(m_sortColumn)) {
         if (m_sortOrdering == Qt::AscendingOrder) {
             return row;
         } else {
@@ -280,9 +284,10 @@
 int
 ModelDataTableModel::getUnsorted(int row) const
 {
-    if (!m_model) return row;
+    auto model = getTabularModel();
+    if (!model) return row;
 
-    if (m_model->isColumnTimeValue(m_sortColumn)) {
+    if (model->isColumnTimeValue(m_sortColumn)) {
         if (m_sortOrdering == Qt::AscendingOrder) {
             return row;
         } else {
@@ -309,9 +314,10 @@
 void
 ModelDataTableModel::resort() const
 {
-    if (!m_model) return;
+    auto model = getTabularModel();
+    if (!model) return;
 
-    bool numeric = (m_model->getSortType(m_sortColumn) ==
+    bool numeric = (model->getSortType(m_sortColumn) ==
                     TabularModel::SortNumeric);
 
 //    cerr << "resort: numeric == " << numeric << endl;
@@ -342,15 +348,16 @@
 void
 ModelDataTableModel::resortNumeric() const
 {
-    if (!m_model) return;
+    auto model = getTabularModel();
+    if (!model) return;
 
     typedef std::multimap<double, int> MapType;
 
     MapType rowMap;
-    int rows = m_model->getRowCount();
+    int rows = model->getRowCount();
 
     for (int i = 0; i < rows; ++i) {
-        QVariant value = m_model->getData(i, m_sortColumn, TabularModel::SortRole);
+        QVariant value = model->getData(i, m_sortColumn, TabularModel::SortRole);
         rowMap.insert(MapType::value_type(value.toDouble(), i));
     }
 
@@ -365,16 +372,17 @@
 void
 ModelDataTableModel::resortAlphabetical() const
 {
-    if (!m_model) return;
+    auto model = getTabularModel();
+    if (!model) return;
 
     typedef std::multimap<QString, int> MapType;
 
     MapType rowMap;
-    int rows = m_model->getRowCount();
+    int rows = model->getRowCount();
 
     for (int i = 0; i < rows; ++i) {
         QVariant value =
-            m_model->getData(i, m_sortColumn, TabularModel::SortRole);
+            model->getData(i, m_sortColumn, TabularModel::SortRole);
         rowMap.insert(MapType::value_type(value.toString(), i));
     }
 
--- a/data/model/ModelDataTableModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/ModelDataTableModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -22,6 +22,9 @@
 
 #include "base/BaseTypes.h"
 
+#include "TabularModel.h"
+#include "Model.h"
+
 class TabularModel;
 class Command;
 
@@ -30,7 +33,7 @@
     Q_OBJECT
 
 public:
-    ModelDataTableModel(TabularModel *m);
+    ModelDataTableModel(ModelId modelId); // a TabularModel
     virtual ~ModelDataTableModel();
 
     QVariant data(const QModelIndex &index, int role) const override;
@@ -72,10 +75,13 @@
 protected slots:
     void modelChanged();
     void modelChangedWithin(sv_frame_t, sv_frame_t);
-    void modelAboutToBeDeleted();
 
 protected:
-    TabularModel *m_model;
+    std::shared_ptr<TabularModel> getTabularModel() const {
+        return ModelById::getAs<TabularModel>(m_model);
+    }
+    
+    ModelId m_model;
     int m_sortColumn;
     Qt::SortOrder m_sortOrdering;
     int m_currentRow;
--- a/data/model/NoteModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/NoteModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -58,6 +58,7 @@
         m_units(""),
         m_extendTo(0),
         m_notifier(this,
+                   getId(),
                    notifyOnAdd ?
                    DeferredNotifier::NOTIFY_ALWAYS :
                    DeferredNotifier::NOTIFY_DEFERRED),
@@ -66,7 +67,8 @@
             m_valueMinimum = 33.f;
             m_valueMaximum = 88.f;
         }
-        PlayParameterRepository::getInstance()->addPlayable(this);
+        PlayParameterRepository::getInstance()->addPlayable
+            (getId().untyped, this);
     }
 
     NoteModel(sv_samplerate_t sampleRate, int resolution,
@@ -83,15 +85,18 @@
         m_units(""),
         m_extendTo(0),
         m_notifier(this,
+                   getId(),
                    notifyOnAdd ?
                    DeferredNotifier::NOTIFY_ALWAYS :
                    DeferredNotifier::NOTIFY_DEFERRED),
         m_completion(100) {
-        PlayParameterRepository::getInstance()->addPlayable(this);
+        PlayParameterRepository::getInstance()->addPlayable
+            (getId().untyped, this);
     }
 
     virtual ~NoteModel() {
-        PlayParameterRepository::getInstance()->removePlayable(this);
+        PlayParameterRepository::getInstance()->removePlayable
+            (getId().untyped);
     }
 
     QString getTypeName() const override { return tr("Note"); }
@@ -142,12 +147,12 @@
             m_notifier.makeDeferredNotifications();
         }
         
-        emit completionChanged();
+        emit completionChanged(getId());
 
         if (completion == 100) {
             // henceforth:
             m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS);
-            emit modelChanged();
+            emit modelChanged(getId());
         }
     }
 
@@ -216,7 +221,7 @@
         m_notifier.update(e.getFrame(), e.getDuration() + m_resolution);
 
         if (allChange) {
-            emit modelChanged();
+            emit modelChanged(getId());
         }
     }
     
@@ -225,7 +230,8 @@
             QMutexLocker locker(&m_mutex);
             m_events.remove(e);
         }
-        emit modelChangedWithin(e.getFrame(),
+        emit modelChangedWithin(getId(),
+                                e.getFrame(),
                                 e.getFrame() + e.getDuration() + m_resolution);
     }
 
@@ -308,8 +314,7 @@
         case 5: e1 = e0.withLabel(value.toString()); break;
         }
 
-        ChangeEventsCommand *command =
-            new ChangeEventsCommand(this, tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/model/Path.h	Wed Jul 17 14:24:51 2019 +0100
@@ -0,0 +1,164 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_PATH_H
+#define SV_PATH_H
+
+#include "base/XmlExportable.h"
+#include "base/RealTime.h"
+#include "base/BaseTypes.h"
+
+#include <QStringList>
+#include <set>
+
+struct PathPoint
+{
+    PathPoint(sv_frame_t _frame) :
+        frame(_frame), mapframe(_frame) { }
+    PathPoint(sv_frame_t _frame, sv_frame_t _mapframe) :
+        frame(_frame), mapframe(_mapframe) { }
+
+    sv_frame_t frame;
+    sv_frame_t mapframe;
+
+    void toXml(QTextStream &stream, QString indent = "",
+               QString extraAttributes = "") const {
+        stream << QString("%1<point frame=\"%2\" mapframe=\"%3\" %4/>\n")
+            .arg(indent).arg(frame).arg(mapframe).arg(extraAttributes);
+    }
+        
+    QString toDelimitedDataString(QString delimiter, DataExportOptions,
+                                  sv_samplerate_t sampleRate) const {
+        QStringList list;
+        list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str();
+        list << QString("%1").arg(mapframe);
+        return list.join(delimiter);
+    }
+
+    bool operator<(const PathPoint &p2) const {
+        if (frame != p2.frame) return frame < p2.frame;
+        return mapframe < p2.mapframe;
+    }
+};
+
+class Path : public XmlExportable
+{
+public:
+    Path(sv_samplerate_t sampleRate, int resolution) :
+        m_sampleRate(sampleRate),
+        m_resolution(resolution) {
+    }
+    Path(const Path &) =default;
+    Path &operator=(const Path &) =default;
+
+    typedef std::set<PathPoint> Points;
+
+    sv_samplerate_t getSampleRate() const { return m_sampleRate; }
+    int getResolution() const { return m_resolution; }
+
+    int getPointCount() const {
+        return int(m_points.size());
+    }
+
+    const Points &getPoints() const {
+        return m_points;
+    }
+
+    void add(PathPoint p) {
+        m_points.insert(p);
+    }
+    
+    void remove(PathPoint p) {
+        m_points.erase(p);
+    }
+
+    void clear() {
+        m_points.clear();
+    }
+
+    /**
+     * XmlExportable methods.
+     */
+    void toXml(QTextStream &out,
+               QString indent = "",
+               QString extraAttributes = "") const override {
+
+        // For historical reasons we serialise a Path as a model,
+        // although the class itself no longer is.
+
+        // We also write start and end frames - which our API no
+        // longer exposes - just for backward compatibility
+
+        sv_frame_t start = 0;
+        sv_frame_t end = 0;
+        if (!m_points.empty()) {
+            start = m_points.begin()->frame;
+            end = m_points.rbegin()->frame + m_resolution;
+        }
+        
+        // Our dataset doesn't have its own export ID, we just use
+        // ours. Actually any model could do that, since datasets
+        // aren't in the same id-space as models (or paths) when
+        // re-read
+        
+        out << indent;
+        out << QString("<model id=\"%1\" name=\"\" sampleRate=\"%2\" "
+                       "start=\"%3\" end=\"%4\" type=\"sparse\" "
+                       "dimensions=\"2\" resolution=\"%5\" "
+                       "notifyOnAdd=\"true\" dataset=\"%6\" "
+                       "subtype=\"path\" %7/>\n")
+            .arg(getExportId())
+            .arg(m_sampleRate)
+            .arg(start)
+            .arg(end)
+            .arg(m_resolution)
+            .arg(getExportId())
+            .arg(extraAttributes);
+
+        out << indent << QString("<dataset id=\"%1\" dimensions=\"2\">\n")
+            .arg(getExportId());
+        
+        for (PathPoint p: m_points) {
+            p.toXml(out, indent + "  ", "");
+        }
+
+        out << indent << "</dataset>\n";
+    }
+
+    QString toDelimitedDataString(QString delimiter,
+                                  DataExportOptions,
+                                  sv_frame_t startFrame,
+                                  sv_frame_t duration) const {
+
+        QString s;
+        for (PathPoint p: m_points) {
+            if (p.frame < startFrame) continue;
+            if (p.frame >= startFrame + duration) break;
+            s += QString("%1%2%3\n")
+                .arg(p.frame)
+                .arg(delimiter)
+                .arg(p.mapframe);
+        }
+
+        return s;
+    }
+    
+protected:
+    sv_samplerate_t m_sampleRate;
+    int m_resolution;
+    Points m_points;
+};
+
+
+#endif
--- a/data/model/PathModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,229 +0,0 @@
-/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
-
-/*
-    Sonic Visualiser
-    An audio file viewer and annotation editor.
-    Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2007 QMUL.
-    
-    This program is free software; you can redistribute it and/or
-    modify it under the terms of the GNU General Public License as
-    published by the Free Software Foundation; either version 2 of the
-    License, or (at your option) any later version.  See the file
-    COPYING included with this distribution for more information.
-*/
-
-#ifndef SV_PATH_MODEL_H
-#define SV_PATH_MODEL_H
-
-#include "Model.h"
-#include "DeferredNotifier.h"
-#include "base/RealTime.h"
-#include "base/BaseTypes.h"
-
-#include "base/XmlExportable.h"
-#include "base/RealTime.h"
-
-#include <QStringList>
-#include <set>
-
-struct PathPoint
-{
-    PathPoint(sv_frame_t _frame) :
-        frame(_frame), mapframe(_frame) { }
-    PathPoint(sv_frame_t _frame, sv_frame_t _mapframe) :
-        frame(_frame), mapframe(_mapframe) { }
-
-    sv_frame_t frame;
-    sv_frame_t mapframe;
-
-    void toXml(QTextStream &stream, QString indent = "",
-               QString extraAttributes = "") const {
-        stream << QString("%1<point frame=\"%2\" mapframe=\"%3\" %4/>\n")
-            .arg(indent).arg(frame).arg(mapframe).arg(extraAttributes);
-    }
-        
-    QString toDelimitedDataString(QString delimiter, DataExportOptions,
-                                  sv_samplerate_t sampleRate) const {
-        QStringList list;
-        list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str();
-        list << QString("%1").arg(mapframe);
-        return list.join(delimiter);
-    }
-
-    bool operator<(const PathPoint &p2) const {
-        if (frame != p2.frame) return frame < p2.frame;
-        return mapframe < p2.mapframe;
-    }
-};
-
-class PathModel : public Model
-{
-public:
-    typedef std::set<PathPoint> PointList;
-
-    PathModel(sv_samplerate_t sampleRate,
-              int resolution,
-              bool notifyOnAdd = true) :
-        m_sampleRate(sampleRate),
-        m_resolution(resolution),
-        m_notifier(this,
-                   notifyOnAdd ?
-                   DeferredNotifier::NOTIFY_ALWAYS :
-                   DeferredNotifier::NOTIFY_DEFERRED),
-        m_completion(100),
-        m_start(0),
-        m_end(0) {
-    }
-
-    QString getTypeName() const override { return tr("Path"); }
-    bool isSparse() const override { return true; }
-    bool isOK() const override { return true; }
-
-    sv_frame_t getStartFrame() const override {
-        return m_start;
-    }
-    sv_frame_t getTrueEndFrame() const override {
-        return m_end;
-    }
-    
-    sv_samplerate_t getSampleRate() const override { return m_sampleRate; }
-    int getResolution() const { return m_resolution; }
-    
-    int getCompletion() const override { return m_completion; }
-
-    void setCompletion(int completion, bool update = true) {
-        
-        {   QMutexLocker locker(&m_mutex);
-            if (m_completion == completion) return;
-            m_completion = completion;
-        }
-
-        if (update) {
-            m_notifier.makeDeferredNotifications();
-        }
-        
-        emit completionChanged();
-
-        if (completion == 100) {
-            // henceforth:
-            m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS);
-            emit modelChanged();
-        }
-    }
-
-    /**
-     * Query methods.
-     */
-    int getPointCount() const {
-        return int(m_points.size());
-    }
-    const PointList &getPoints() const {
-        return m_points;
-    }
-
-    /**
-     * Editing methods.
-     */
-    void add(PathPoint p) {
-
-        {   QMutexLocker locker(&m_mutex);
-            m_points.insert(p);
-
-            if (m_start == m_end) {
-                m_start = p.frame;
-                m_end = m_start + m_resolution;
-            } else {
-                if (p.frame < m_start) {
-                    m_start = p.frame;
-                }
-                if (p.frame + m_resolution > m_end) {
-                    m_end = p.frame + m_resolution;
-                }
-            }
-        }
-        
-        m_notifier.update(p.frame, m_resolution);
-    }
-    
-    void remove(PathPoint p) {
-        {   QMutexLocker locker(&m_mutex);
-            m_points.erase(p);
-        }
-
-        emit modelChangedWithin(p.frame, p.frame + m_resolution);
-    }
-
-    void clear() {
-        {   QMutexLocker locker(&m_mutex);
-            m_start = m_end = 0;
-            m_points.clear();
-        }
-    }
-
-    /**
-     * XmlExportable methods.
-     */
-    void toXml(QTextStream &out,
-                       QString indent = "",
-                       QString extraAttributes = "") const override {
-
-        // Our dataset doesn't have its own export ID, we just use
-        // ours. Actually any model could do that, since datasets
-        // aren't in the same id-space as models when re-read
-        
-        Model::toXml
-            (out,
-             indent,
-             QString("type=\"sparse\" dimensions=\"2\" resolution=\"%1\" "
-                     "notifyOnAdd=\"%2\" dataset=\"%3\" subtype=\"path\" %4")
-             .arg(m_resolution)
-             .arg("true") // always true after model reaches 100% -
-                          // subsequent points are always notified
-             .arg(getExportId())
-             .arg(extraAttributes));
-
-        out << indent << QString("<dataset id=\"%1\" dimensions=\"2\">\n")
-            .arg(getExportId());
-        
-        for (PathPoint p: m_points) {
-            p.toXml(out, indent + "  ", "");
-        }
-
-        out << indent << "</dataset>\n";
-    }
-
-    QString toDelimitedDataString(QString delimiter,
-                                  DataExportOptions,
-                                  sv_frame_t startFrame,
-                                  sv_frame_t duration) const override {
-
-        QString s;
-        for (PathPoint p: m_points) {
-            if (p.frame < startFrame) continue;
-            if (p.frame >= startFrame + duration) break;
-            s += QString("%1%2%3\n")
-                .arg(p.frame)
-                .arg(delimiter)
-                .arg(p.mapframe);
-        }
-
-        return s;
-    }
-    
-protected:
-    sv_samplerate_t m_sampleRate;
-    int m_resolution;
-
-    DeferredNotifier m_notifier;
-    int m_completion;
-
-    sv_frame_t m_start;
-    sv_frame_t m_end;
-    PointList m_points;
-
-    mutable QMutex m_mutex;  
-};
-
-
-#endif
--- a/data/model/RangeSummarisableTimeValueModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/RangeSummarisableTimeValueModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -41,10 +41,11 @@
     public:
         Range() : 
             m_new(true), m_min(0.f), m_max(0.f), m_absmean(0.f) { }
-        Range(const Range &r) : 
-            m_new(true), m_min(r.m_min), m_max(r.m_max), m_absmean(r.m_absmean) { }
         Range(float min, float max, float absmean) :
-            m_new(true), m_min(min), m_max(max), m_absmean(absmean) { }
+            m_new(false), m_min(min), m_max(max), m_absmean(absmean) { }
+
+        Range(const Range &r) =default;
+        Range &operator=(const Range &) =default;
 
         float min() const { return m_min; }
         float max() const { return m_max; }
--- a/data/model/ReadOnlyWaveFileModel.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/ReadOnlyWaveFileModel.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -21,12 +21,12 @@
 #include "system/System.h"
 
 #include "base/Preferences.h"
+#include "base/PlayParameterRepository.h"
 
 #include <QFileInfo>
 #include <QTextStream>
 
 #include <iostream>
-//#include <unistd.h>
 #include <cmath>
 #include <sndfile.h>
 
@@ -85,6 +85,9 @@
     if (m_reader) setObjectName(m_reader->getTitle());
     if (objectName() == "") setObjectName(QFileInfo(m_path).fileName());
     if (isOK()) fillCache();
+    
+    PlayParameterRepository::getInstance()->addPlayable
+        (getId().untyped, this);
 }
 
 ReadOnlyWaveFileModel::ReadOnlyWaveFileModel(FileSource source, AudioFileReader *reader) :
@@ -106,10 +109,16 @@
     if (m_reader) setObjectName(m_reader->getTitle());
     if (objectName() == "") setObjectName(QFileInfo(m_path).fileName());
     fillCache();
+    
+    PlayParameterRepository::getInstance()->addPlayable
+        (getId().untyped, this);
 }
 
 ReadOnlyWaveFileModel::~ReadOnlyWaveFileModel()
 {
+    PlayParameterRepository::getInstance()->removePlayable
+        (getId().untyped);
+    
     m_exiting = true;
     if (m_fillThread) m_fillThread->wait();
     if (m_myReader) delete m_reader;
@@ -581,14 +590,14 @@
         SVDEBUG << "ReadOnlyWaveFileModel(" << objectName() << ")::fillTimerTimedOut: extent = " << fillExtent << endl;
 #endif
         if (fillExtent > m_lastFillExtent) {
-            emit modelChangedWithin(m_lastFillExtent, fillExtent);
+            emit modelChangedWithin(getId(), m_lastFillExtent, fillExtent);
             m_lastFillExtent = fillExtent;
         }
     } else {
 #ifdef DEBUG_WAVE_FILE_MODEL
         SVDEBUG << "ReadOnlyWaveFileModel(" << objectName() << ")::fillTimerTimedOut: no thread" << endl;
 #endif
-        emit modelChanged();
+        emit modelChanged(getId());
     }
 }
 
@@ -607,10 +616,10 @@
     SVDEBUG << "ReadOnlyWaveFileModel(" << objectName() << ")::cacheFilled, about to emit things" << endl;
 #endif
     if (getEndFrame() > prevFillExtent) {
-        emit modelChangedWithin(prevFillExtent, getEndFrame());
+        emit modelChangedWithin(getId(), prevFillExtent, getEndFrame());
     }
-    emit modelChanged();
-    emit ready();
+    emit modelChanged(getId());
+    emit ready(getId());
 }
 
 void
--- a/data/model/RegionModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/RegionModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -50,6 +50,7 @@
         m_valueQuantization(0),
         m_haveDistinctValues(false),
         m_notifier(this,
+                   getId(),
                    notifyOnAdd ?
                    DeferredNotifier::NOTIFY_ALWAYS :
                    DeferredNotifier::NOTIFY_DEFERRED),
@@ -67,6 +68,7 @@
         m_valueQuantization(0),
         m_haveDistinctValues(false),
         m_notifier(this,
+                   getId(),
                    notifyOnAdd ?
                    DeferredNotifier::NOTIFY_ALWAYS :
                    DeferredNotifier::NOTIFY_DEFERRED),
@@ -120,12 +122,12 @@
             m_notifier.makeDeferredNotifications();
         }
         
-        emit completionChanged();
+        emit completionChanged(getId());
 
         if (completion == 100) {
             // henceforth:
             m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS);
-            emit modelChanged();
+            emit modelChanged(getId());
         }
     }
 
@@ -197,7 +199,7 @@
         m_notifier.update(e.getFrame(), e.getDuration() + m_resolution);
 
         if (allChange) {
-            emit modelChanged();
+            emit modelChanged(getId());
         }
     }
     
@@ -206,7 +208,8 @@
             QMutexLocker locker(&m_mutex);
             m_events.remove(e);
         }
-        emit modelChangedWithin(e.getFrame(),
+        emit modelChangedWithin(getId(),
+                                e.getFrame(),
                                 e.getFrame() + e.getDuration() + m_resolution);
     }
     
@@ -291,8 +294,7 @@
         case 4: e1 = e0.withLabel(value.toString()); break;
         }
 
-        ChangeEventsCommand *command =
-            new ChangeEventsCommand(this, tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- a/data/model/SparseOneDimensionalModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/SparseOneDimensionalModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -49,15 +49,18 @@
         m_resolution(resolution),
         m_haveTextLabels(false),
         m_notifier(this,
+                   getId(),
                    notifyOnAdd ?
                    DeferredNotifier::NOTIFY_ALWAYS :
                    DeferredNotifier::NOTIFY_DEFERRED),
         m_completion(100) {
-        PlayParameterRepository::getInstance()->addPlayable(this);
+        PlayParameterRepository::getInstance()->addPlayable
+            (getId().untyped, this);
     }
 
     virtual ~SparseOneDimensionalModel() {
-        PlayParameterRepository::getInstance()->removePlayable(this);
+        PlayParameterRepository::getInstance()->removePlayable
+            (getId().untyped);
     }
 
     QString getTypeName() const override { return tr("Sparse 1-D"); }
@@ -95,12 +98,12 @@
             m_notifier.makeDeferredNotifications();
         }
         
-        emit completionChanged();
+        emit completionChanged(getId());
 
         if (completion == 100) {
             // henceforth:
             m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS);
-            emit modelChanged();
+            emit modelChanged(getId());
         }
     }
     
@@ -164,7 +167,8 @@
         {   QMutexLocker locker(&m_mutex);
             m_events.remove(e);
         }
-        emit modelChangedWithin(e.getFrame(), e.getFrame() + m_resolution);
+        emit modelChangedWithin(getId(),
+                                e.getFrame(), e.getFrame() + m_resolution);
     }
     
     /**
@@ -239,8 +243,7 @@
         case 2: e1 = e0.withLabel(value.toString()); break;
         }
 
-        ChangeEventsCommand *command =
-            new ChangeEventsCommand(this, tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- a/data/model/SparseTimeValueModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/SparseTimeValueModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -48,13 +48,15 @@
         m_haveExtents(false),
         m_haveTextLabels(false),
         m_notifier(this,
+                   getId(),
                    notifyOnAdd ?
                    DeferredNotifier::NOTIFY_ALWAYS :
                    DeferredNotifier::NOTIFY_DEFERRED),
         m_completion(100) {
         // Model is playable, but may not sound (if units not Hz or
         // range unsuitable)
-        PlayParameterRepository::getInstance()->addPlayable(this);
+        PlayParameterRepository::getInstance()->addPlayable
+            (getId().untyped, this);
     }
 
     SparseTimeValueModel(sv_samplerate_t sampleRate, int resolution,
@@ -67,17 +69,20 @@
         m_haveExtents(true),
         m_haveTextLabels(false),
         m_notifier(this,
+                   getId(),
                    notifyOnAdd ?
                    DeferredNotifier::NOTIFY_ALWAYS :
                    DeferredNotifier::NOTIFY_DEFERRED),
         m_completion(100) {
         // Model is playable, but may not sound (if units not Hz or
         // range unsuitable)
-        PlayParameterRepository::getInstance()->addPlayable(this);
+        PlayParameterRepository::getInstance()->addPlayable
+            (getId().untyped, this);
     }
 
     virtual ~SparseTimeValueModel() {
-        PlayParameterRepository::getInstance()->removePlayable(this);
+        PlayParameterRepository::getInstance()->removePlayable
+            (getId().untyped);
     }
 
     QString getTypeName() const override { return tr("Sparse Time-Value"); }
@@ -124,12 +129,12 @@
             m_notifier.makeDeferredNotifications();
         }
         
-        emit completionChanged();
+        emit completionChanged(getId());
 
         if (completion == 100) {
             // henceforth:
             m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS);
-            emit modelChanged();
+            emit modelChanged(getId());
         }
     }
     
@@ -202,7 +207,7 @@
         m_notifier.update(e.getFrame(), m_resolution);
 
         if (allChange) {
-            emit modelChanged();
+            emit modelChanged(getId());
         }
     }
     
@@ -211,7 +216,8 @@
             QMutexLocker locker(&m_mutex);
             m_events.remove(e);
         }
-        emit modelChangedWithin(e.getFrame(), e.getFrame() + m_resolution);
+        emit modelChangedWithin(getId(),
+                                e.getFrame(), e.getFrame() + m_resolution);
     }
     
     /**
@@ -289,8 +295,7 @@
         case 3: e1 = e0.withLabel(value.toString()); break;
         }
 
-        ChangeEventsCommand *command =
-            new ChangeEventsCommand(this, tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- a/data/model/TextModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/TextModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -46,6 +46,7 @@
         m_sampleRate(sampleRate),
         m_resolution(resolution),
         m_notifier(this,
+                   getId(),
                    notifyOnAdd ?
                    DeferredNotifier::NOTIFY_ALWAYS :
                    DeferredNotifier::NOTIFY_DEFERRED),
@@ -82,12 +83,12 @@
             m_notifier.makeDeferredNotifications();
         }
         
-        emit completionChanged();
+        emit completionChanged(getId());
 
         if (completion == 100) {
             // henceforth:
             m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS);
-            emit modelChanged();
+            emit modelChanged(getId());
         }
     }
     
@@ -147,7 +148,8 @@
         {   QMutexLocker locker(&m_mutex);
             m_events.remove(e);
         }
-        emit modelChangedWithin(e.getFrame(), e.getFrame() + m_resolution);
+        emit modelChangedWithin(getId(),
+                                e.getFrame(), e.getFrame() + m_resolution);
     }
 
     /**
@@ -226,8 +228,7 @@
         case 3: e1 = e0.withLabel(value.toString()); break;
         }
 
-        ChangeEventsCommand *command =
-            new ChangeEventsCommand(this, tr("Edit Data"));
+        auto command = new ChangeEventsCommand(getId().untyped, tr("Edit Data"));
         command->remove(e0);
         command->add(e1);
         return command->finish();
--- a/data/model/WaveformOversampler.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/WaveformOversampler.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -19,7 +19,7 @@
 #include "data/model/DenseTimeValueModel.h"
 
 floatvec_t
-WaveformOversampler::getOversampledData(const DenseTimeValueModel *source,
+WaveformOversampler::getOversampledData(const DenseTimeValueModel &source,
                                         int channel,
                                         sv_frame_t sourceStartFrame,
                                         sv_frame_t sourceFrameCount,
@@ -53,14 +53,14 @@
 }
 
 floatvec_t
-WaveformOversampler::getFixedRatioData(const DenseTimeValueModel *source,
+WaveformOversampler::getFixedRatioData(const DenseTimeValueModel &source,
                                        int channel,
                                        sv_frame_t sourceStartFrame,
                                        sv_frame_t sourceFrameCount)
 {
     Profiler profiler("WaveformOversampler::getFixedRatioData");
     
-    sv_frame_t sourceLength = source->getEndFrame();
+    sv_frame_t sourceLength = source.getEndFrame();
     
     if (sourceStartFrame + sourceFrameCount > sourceLength) {
         sourceFrameCount = sourceLength - sourceStartFrame;
@@ -84,7 +84,7 @@
         i1 = sourceLength;
     }
     
-    floatvec_t sourceData = source->getData(channel, i0, i1 - i0);
+    floatvec_t sourceData = source.getData(channel, i0, i1 - i0);
     
     for (sv_frame_t i = i0; i < i1; ++i) {
         float v = sourceData[i - i0];
--- a/data/model/WaveformOversampler.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/WaveformOversampler.h	Wed Jul 17 14:24:51 2019 +0100
@@ -38,14 +38,14 @@
      *  will have sourceFrameCount * oversampleBy samples, except when
      *  truncated because the end of the model was reached.
      */
-    static floatvec_t getOversampledData(const DenseTimeValueModel *source,
+    static floatvec_t getOversampledData(const DenseTimeValueModel &source,
                                          int channel,
                                          sv_frame_t sourceStartFrame,
                                          sv_frame_t sourceFrameCount,
                                          int oversampleBy);
 
 private:
-    static floatvec_t getFixedRatioData(const DenseTimeValueModel *source,
+    static floatvec_t getFixedRatioData(const DenseTimeValueModel &source,
                                         int channel,
                                         sv_frame_t sourceStartFrame,
                                         sv_frame_t sourceFrameCount);
--- a/data/model/WritableWaveFileModel.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/WritableWaveFileModel.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -19,6 +19,7 @@
 
 #include "base/TempDirectory.h"
 #include "base/Exceptions.h"
+#include "base/PlayParameterRepository.h"
 
 #include "fileio/WavFileWriter.h"
 #include "fileio/WavFileReader.h"
@@ -96,7 +97,8 @@
             // so the filename only needs to be unique within that -
             // model ID should be ok
             QDir dir(TempDirectory::getInstance()->getPath());
-            path = dir.filePath(QString("written_%1.wav").arg(getId()));
+            path = dir.filePath(QString("written_%1.wav")
+                                .arg(getId().untyped));
         } catch (const DirectoryCreationFailed &f) {
             SVCERR << "WritableWaveFileModel: Failed to create temporary directory" << endl;
             return;
@@ -126,7 +128,8 @@
         // Temp dir is exclusive to this run of the application, so
         // the filename only needs to be unique within that
         QDir dir(TempDirectory::getInstance()->getPath());
-        m_temporaryPath = dir.filePath(QString("prenorm_%1.wav").arg(getId()));
+        m_temporaryPath = dir.filePath(QString("prenorm_%1.wav")
+                                       .arg(getId().untyped));
 
         m_temporaryWriter = new WavFileWriter
             (m_temporaryPath, m_sampleRate, m_channels,
@@ -158,10 +161,16 @@
     connect(m_model, SIGNAL(modelChanged()), this, SIGNAL(modelChanged()));
     connect(m_model, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
             this, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)));
+    
+    PlayParameterRepository::getInstance()->addPlayable
+        (getId().untyped, this);
 }
 
 WritableWaveFileModel::~WritableWaveFileModel()
 {
+    PlayParameterRepository::getInstance()->removePlayable
+        (getId().untyped);
+    
     delete m_model;
     delete m_targetWriter;
     delete m_temporaryWriter;
@@ -247,8 +256,8 @@
     
     m_reader->updateDone();
     m_proportion = 100;
-    emit modelChanged();
-    emit writeCompleted();
+    emit modelChanged(getId());
+    emit writeCompleted(getId());
 }
 
 void
--- a/data/model/WritableWaveFileModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/WritableWaveFileModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -197,7 +197,7 @@
                        QString extraAttributes = "") const override;
 
 signals:
-    void writeCompleted();
+    void writeCompleted(ModelId);
     
 protected:
     ReadOnlyWaveFileModel *m_model;
--- a/data/model/test/TestFFTModel.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/test/TestFFTModel.h	Wed Jul 17 14:24:51 2019 +0100
@@ -35,7 +35,7 @@
     Q_OBJECT
 
 private:
-    void test(DenseTimeValueModel *model,
+    void test(ModelId model, // a DenseTimeValueModel
               WindowType window, int windowSize, int windowIncrement, int fftSize,
               int columnNo, vector<vector<complex<float>>> expectedValues,
               int expectedWidth) {
@@ -88,6 +88,15 @@
         }
     }
 
+    ModelId makeMock(std::vector<Sort> sorts, int length, int pad) {
+        auto mwm = std::make_shared<MockWaveModel>(sorts, length, pad);
+        return ModelById::add(mwm);
+    }
+
+    void releaseMock(ModelId id) {
+        ModelById::release(id);
+    }
+
 private slots:
 
     // NB. FFTModel columns are centred on the sample frame, and in
@@ -101,153 +110,163 @@
     // are those of our expected signal.
     
     void dc_simple_rect() {
-        MockWaveModel mwm({ DC }, 16, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+        auto mwm = makeMock({ DC }, 16, 4);
+        test(mwm, RectangularWindow, 8, 8, 8, 0,
              { { {}, {}, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+        test(mwm, RectangularWindow, 8, 8, 8, 1,
              { { { 4.f, 0.f }, {}, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+        test(mwm, RectangularWindow, 8, 8, 8, 2,
              { { { 4.f, 0.f }, {}, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+        test(mwm, RectangularWindow, 8, 8, 8, 3,
              { { {}, {}, {}, {}, {} } }, 4);
+        releaseMock(mwm);
     }
 
     void dc_simple_hann() {
         // The Hann window function is a simple sinusoid with period
         // equal to twice the window size, and it halves the DC energy
-        MockWaveModel mwm({ DC }, 16, 4);
-        test(&mwm, HanningWindow, 8, 8, 8, 0,
+        auto mwm = makeMock({ DC }, 16, 4);
+        test(mwm, HanningWindow, 8, 8, 8, 0,
              { { {}, {}, {}, {}, {} } }, 4);
-        test(&mwm, HanningWindow, 8, 8, 8, 1,
+        test(mwm, HanningWindow, 8, 8, 8, 1,
              { { { 4.f, 0.f }, { 2.f, 0.f }, {}, {}, {} } }, 4);
-        test(&mwm, HanningWindow, 8, 8, 8, 2,
+        test(mwm, HanningWindow, 8, 8, 8, 2,
              { { { 4.f, 0.f }, { 2.f, 0.f }, {}, {}, {} } }, 4);
-        test(&mwm, HanningWindow, 8, 8, 8, 3,
+        test(mwm, HanningWindow, 8, 8, 8, 3,
              { { {}, {}, {}, {}, {} } }, 4);
+        releaseMock(mwm);
     }
     
     void dc_simple_hann_halfoverlap() {
-        MockWaveModel mwm({ DC }, 16, 4);
-        test(&mwm, HanningWindow, 8, 4, 8, 0,
+        auto mwm = makeMock({ DC }, 16, 4);
+        test(mwm, HanningWindow, 8, 4, 8, 0,
              { { {}, {}, {}, {}, {} } }, 7);
-        test(&mwm, HanningWindow, 8, 4, 8, 2,
+        test(mwm, HanningWindow, 8, 4, 8, 2,
              { { { 4.f, 0.f }, { 2.f, 0.f }, {}, {}, {} } }, 7);
-        test(&mwm, HanningWindow, 8, 4, 8, 3,
+        test(mwm, HanningWindow, 8, 4, 8, 3,
              { { { 4.f, 0.f }, { 2.f, 0.f }, {}, {}, {} } }, 7);
-        test(&mwm, HanningWindow, 8, 4, 8, 6,
+        test(mwm, HanningWindow, 8, 4, 8, 6,
              { { {}, {}, {}, {}, {} } }, 7);
+        releaseMock(mwm);
     }
     
     void sine_simple_rect() {
-        MockWaveModel mwm({ Sine }, 16, 4);
+        auto mwm = makeMock({ Sine }, 16, 4);
         // Sine: output is purely imaginary. Note the sign is flipped
         // (normally the first half of the output would have negative
         // sign for a sine starting at 0) because the model does an
         // FFT shift to centre the phase
-        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+        test(mwm, RectangularWindow, 8, 8, 8, 0,
              { { {}, {}, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+        test(mwm, RectangularWindow, 8, 8, 8, 1,
              { { {}, { 0.f, 2.f }, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+        test(mwm, RectangularWindow, 8, 8, 8, 2,
              { { {}, { 0.f, 2.f }, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+        test(mwm, RectangularWindow, 8, 8, 8, 3,
              { { {}, {}, {}, {}, {} } }, 4);
+        releaseMock(mwm);
     }
     
     void cosine_simple_rect() {
-        MockWaveModel mwm({ Cosine }, 16, 4);
+        auto mwm = makeMock({ Cosine }, 16, 4);
         // Cosine: output is purely real. Note the sign is flipped
         // because the model does an FFT shift to centre the phase
-        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+        test(mwm, RectangularWindow, 8, 8, 8, 0,
              { { {}, {}, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+        test(mwm, RectangularWindow, 8, 8, 8, 1,
              { { {}, { -2.f, 0.f }, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+        test(mwm, RectangularWindow, 8, 8, 8, 2,
              { { {}, { -2.f, 0.f }, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+        test(mwm, RectangularWindow, 8, 8, 8, 3,
              { { {}, {}, {}, {}, {} } }, 4);
+        releaseMock(mwm);
     }
     
     void twochan_simple_rect() {
-        MockWaveModel mwm({ Sine, Cosine }, 16, 4);
+        auto mwm = makeMock({ Sine, Cosine }, 16, 4);
         // Test that the two channels are read and converted separately
-        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+        test(mwm, RectangularWindow, 8, 8, 8, 0,
              {
                  { {}, {}, {}, {}, {} },
                  { {}, {}, {}, {}, {} }
              }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+        test(mwm, RectangularWindow, 8, 8, 8, 1,
              {
                  { {}, {  0.f, 2.f }, {}, {}, {} },
                  { {}, { -2.f, 0.f }, {}, {}, {} }
              }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+        test(mwm, RectangularWindow, 8, 8, 8, 2,
              {
                  { {}, {  0.f, 2.f }, {}, {}, {} },
                  { {}, { -2.f, 0.f }, {}, {}, {} }
              }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+        test(mwm, RectangularWindow, 8, 8, 8, 3,
              {
                  { {}, {}, {}, {}, {} },
                  { {}, {}, {}, {}, {} }
              }, 4);
+        releaseMock(mwm);
     }
     
     void nyquist_simple_rect() {
-        MockWaveModel mwm({ Nyquist }, 16, 4);
+        auto mwm = makeMock({ Nyquist }, 16, 4);
         // Again, the sign is flipped. This has the same amount of
         // energy as the DC example
-        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+        test(mwm, RectangularWindow, 8, 8, 8, 0,
              { { {}, {}, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+        test(mwm, RectangularWindow, 8, 8, 8, 1,
              { { {}, {}, {}, {}, { -4.f, 0.f } } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+        test(mwm, RectangularWindow, 8, 8, 8, 2,
              { { {}, {}, {}, {}, { -4.f, 0.f } } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+        test(mwm, RectangularWindow, 8, 8, 8, 3,
              { { {}, {}, {}, {}, {} } }, 4);
+        releaseMock(mwm);
     }
     
     void dirac_simple_rect() {
-        MockWaveModel mwm({ Dirac }, 16, 4);
+        auto mwm = makeMock({ Dirac }, 16, 4);
         // The window scales by 0.5 and some signs are flipped. Only
         // column 1 has any data (the single impulse).
-        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+        test(mwm, RectangularWindow, 8, 8, 8, 0,
              { { {}, {}, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+        test(mwm, RectangularWindow, 8, 8, 8, 1,
              { { { 0.5f, 0.f }, { -0.5f, 0.f }, { 0.5f, 0.f }, { -0.5f, 0.f }, { 0.5f, 0.f } } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+        test(mwm, RectangularWindow, 8, 8, 8, 2,
              { { {}, {}, {}, {}, {} } }, 4);
-        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+        test(mwm, RectangularWindow, 8, 8, 8, 3,
              { { {}, {}, {}, {}, {} } }, 4);
+        releaseMock(mwm);
     }
     
     void dirac_simple_rect_2() {
-        MockWaveModel mwm({ Dirac }, 16, 8);
+        auto mwm = makeMock({ Dirac }, 16, 8);
         // With 8 samples padding, the FFT shift places the first
         // Dirac impulse at the start of column 1, thus giving all
         // positive values
-        test(&mwm, RectangularWindow, 8, 8, 8, 0,
+        test(mwm, RectangularWindow, 8, 8, 8, 0,
              { { {}, {}, {}, {}, {} } }, 5);
-        test(&mwm, RectangularWindow, 8, 8, 8, 1,
+        test(mwm, RectangularWindow, 8, 8, 8, 1,
              { { { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f } } }, 5);
-        test(&mwm, RectangularWindow, 8, 8, 8, 2,
+        test(mwm, RectangularWindow, 8, 8, 8, 2,
              { { {}, {}, {}, {}, {} } }, 5);
-        test(&mwm, RectangularWindow, 8, 8, 8, 3,
+        test(mwm, RectangularWindow, 8, 8, 8, 3,
              { { {}, {}, {}, {}, {} } }, 5);
-        test(&mwm, RectangularWindow, 8, 8, 8, 4,
+        test(mwm, RectangularWindow, 8, 8, 8, 4,
              { { {}, {}, {}, {}, {} } }, 5);
+        releaseMock(mwm);
     }
 
     void dirac_simple_rect_halfoverlap() {
-        MockWaveModel mwm({ Dirac }, 16, 4);
-        test(&mwm, RectangularWindow, 8, 4, 8, 0,
+        auto mwm = makeMock({ Dirac }, 16, 4);
+        test(mwm, RectangularWindow, 8, 4, 8, 0,
              { { {}, {}, {}, {}, {} } }, 7);
-        test(&mwm, RectangularWindow, 8, 4, 8, 1,
+        test(mwm, RectangularWindow, 8, 4, 8, 1,
              { { { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f }, { 0.5f, 0.f } } }, 7);
-        test(&mwm, RectangularWindow, 8, 4, 8, 2,
+        test(mwm, RectangularWindow, 8, 4, 8, 2,
              { { { 0.5f, 0.f }, { -0.5f, 0.f }, { 0.5f, 0.f }, { -0.5f, 0.f }, { 0.5f, 0.f } } }, 7);
-        test(&mwm, RectangularWindow, 8, 4, 8, 3,
+        test(mwm, RectangularWindow, 8, 4, 8, 3,
              { { {}, {}, {}, {}, {} } }, 7);
+        releaseMock(mwm);
     }
     
 };
--- a/data/model/test/TestSparseModels.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/test/TestSparseModels.h	Wed Jul 17 14:24:51 2019 +0100
@@ -18,7 +18,7 @@
 #include "../SparseOneDimensionalModel.h"
 #include "../NoteModel.h"
 #include "../TextModel.h"
-#include "../PathModel.h"
+#include "../Path.h"
 #include "../ImageModel.h"
 
 #include <QObject>
@@ -252,7 +252,7 @@
     }
     
     void path_xml() {
-        PathModel m(100, 10, false);
+        Path m(100, 10);
         PathPoint p1(20, 30);
         PathPoint p2(40, 60);
         PathPoint p3(50, 49);
--- a/data/model/test/TestWaveformOversampler.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/data/model/test/TestWaveformOversampler.h	Wed Jul 17 14:24:51 2019 +0100
@@ -72,7 +72,7 @@
                    sv_frame_t sourceFrameCount,
                    int oversampleBy) {
         return WaveformOversampler::getOversampledData
-            (m_sourceModel, 0,
+            (*m_sourceModel, 0,
              sourceStartFrame, sourceFrameCount, oversampleBy);
     }
     
--- a/files.pri	Thu Jun 20 14:58:20 2019 +0100
+++ b/files.pri	Wed Jul 17 14:24:51 2019 +0100
@@ -3,6 +3,7 @@
            base/AudioPlaySource.h \
            base/AudioRecordTarget.h \
            base/BaseTypes.h \
+           base/ById.h \
            base/Clipboard.h \
            base/ColumnOp.h \
            base/Command.h \
@@ -89,7 +90,7 @@
            data/model/Model.h \
            data/model/ModelDataTableModel.h \
            data/model/NoteModel.h \
-           data/model/PathModel.h \
+           data/model/Path.h \
            data/model/PowerOfSqrtTwoZoomConstraint.h \
            data/model/PowerOfTwoZoomConstraint.h \
            data/model/RangeSummarisableTimeValueModel.h \
@@ -149,6 +150,7 @@
 	   
 SVCORE_SOURCES = \
            base/AudioLevel.cpp \
+           base/ById.cpp \
            base/Clipboard.cpp \
            base/ColumnOp.cpp \
            base/Command.cpp \
--- a/rdf/RDFImporter.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/rdf/RDFImporter.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -58,7 +58,7 @@
     bool isOK();
     QString getErrorString() const;
 
-    std::vector<Model *> getDataModels(ProgressReporter *);
+    std::vector<ModelId> getDataModels(ProgressReporter *);
 
 protected:
     BasicStore *m_store;
@@ -66,22 +66,22 @@
 
     QString m_uristring;
     QString m_errorString;
-    std::map<QString, Model *> m_audioModelMap;
+    std::map<QString, ModelId> m_audioModelMap;
     sv_samplerate_t m_sampleRate;
 
-    std::map<Model *, std::map<QString, float> > m_labelValueMap;
+    std::map<ModelId, std::map<QString, float> > m_labelValueMap;
 
-    void getDataModelsAudio(std::vector<Model *> &, ProgressReporter *);
-    void getDataModelsSparse(std::vector<Model *> &, ProgressReporter *);
-    void getDataModelsDense(std::vector<Model *> &, ProgressReporter *);
+    void getDataModelsAudio(std::vector<ModelId> &, ProgressReporter *);
+    void getDataModelsSparse(std::vector<ModelId> &, ProgressReporter *);
+    void getDataModelsDense(std::vector<ModelId> &, ProgressReporter *);
 
-    void getDenseModelTitle(Model *, QString, QString);
+    QString getDenseModelTitle(QString featureUri, QString featureTypeUri);
 
     void getDenseFeatureProperties(QString featureUri,
                                    sv_samplerate_t &sampleRate, int &windowLength,
                                    int &hopSize, int &width, int &height);
 
-    void fillModel(Model *, sv_frame_t, sv_frame_t,
+    void fillModel(ModelId, sv_frame_t, sv_frame_t,
                    bool, std::vector<float> &, QString);
 };
 
@@ -119,7 +119,7 @@
     return m_d->getErrorString();
 }
 
-std::vector<Model *>
+std::vector<ModelId>
 RDFImporter::getDataModels(ProgressReporter *r)
 {
     return m_d->getDataModels(r);
@@ -169,10 +169,10 @@
     return m_errorString;
 }
 
-std::vector<Model *>
+std::vector<ModelId>
 RDFImporterImpl::getDataModels(ProgressReporter *reporter)
 {
-    std::vector<Model *> models;
+    std::vector<ModelId> models;
 
     getDataModelsAudio(models, reporter);
 
@@ -206,7 +206,7 @@
 }
 
 void
-RDFImporterImpl::getDataModelsAudio(std::vector<Model *> &models,
+RDFImporterImpl::getDataModelsAudio(std::vector<ModelId> &models,
                                     ProgressReporter *reporter)
 {
     Nodes sigs = m_store->match
@@ -270,24 +270,25 @@
             reporter->setMessage(RDFImporter::tr("Importing audio referenced in RDF..."));
         }
         fs->waitForData();
-        ReadOnlyWaveFileModel *newModel = new ReadOnlyWaveFileModel(*fs, m_sampleRate);
+        auto newModel = std::make_shared<ReadOnlyWaveFileModel>
+            (*fs, m_sampleRate);
         if (newModel->isOK()) {
             cerr << "Successfully created wave file model from source at \"" << source << "\"" << endl;
-            models.push_back(newModel);
-            m_audioModelMap[signal] = newModel;
+            auto modelId = ModelById::add(newModel);
+            models.push_back(modelId);
+            m_audioModelMap[signal] = modelId;
             if (m_sampleRate == 0) {
                 m_sampleRate = newModel->getSampleRate();
             }
         } else {
             m_errorString = QString("Failed to create wave file model from source at \"%1\"").arg(source);
-            delete newModel;
         }
         delete fs;
     }
 }
 
 void
-RDFImporterImpl::getDataModelsDense(std::vector<Model *> &models,
+RDFImporterImpl::getDataModelsDense(std::vector<ModelId> &models,
                                     ProgressReporter *reporter)
 {
     if (reporter) {
@@ -342,7 +343,7 @@
 
         if (height == 1) {
 
-            SparseTimeValueModel *m = new SparseTimeValueModel
+            auto m = std::make_shared<SparseTimeValueModel>
                 (sampleRate, hopSize, false);
 
             for (int j = 0; j < values.size(); ++j) {
@@ -351,16 +352,13 @@
                 m->add(e);
             }
 
-            getDenseModelTitle(m, feature, type);
-        
+            m->setObjectName(getDenseModelTitle(feature, type));
             m->setRDFTypeURI(type);
-
-            models.push_back(m);
+            models.push_back(ModelById::add(m));
 
         } else {
 
-            EditableDenseThreeDimensionalModel *m =
-                new EditableDenseThreeDimensionalModel
+            auto m = std::make_shared<EditableDenseThreeDimensionalModel>
                 (sampleRate, hopSize, height, 
                  EditableDenseThreeDimensionalModel::NoCompression, false);
             
@@ -380,18 +378,15 @@
                 m->setColumn(x++, column);
             }
 
-            getDenseModelTitle(m, feature, type);
-        
+            m->setObjectName(getDenseModelTitle(feature, type));
             m->setRDFTypeURI(type);
-
-            models.push_back(m);
+            models.push_back(ModelById::add(m));
         }
     }
 }
 
-void
-RDFImporterImpl::getDenseModelTitle(Model *m,
-                                    QString featureUri,
+QString
+RDFImporterImpl::getDenseModelTitle(QString featureUri,
                                     QString featureTypeUri)
 {
     Node n = m_store->complete
@@ -399,8 +394,7 @@
 
     if (n.type == Node::Literal && n.value != "") {
         SVDEBUG << "RDFImporterImpl::getDenseModelTitle: Title (from signal) \"" << n.value << "\"" << endl;
-        m->setObjectName(n.value);
-        return;
+        return n.value;
     }
 
     n = m_store->complete
@@ -408,11 +402,11 @@
 
     if (n.type == Node::Literal && n.value != "") {
         SVDEBUG << "RDFImporterImpl::getDenseModelTitle: Title (from signal type) \"" << n.value << "\"" << endl;
-        m->setObjectName(n.value);
-        return;
+        return n.value;
     }
 
     SVDEBUG << "RDFImporterImpl::getDenseModelTitle: No title available for feature <" << featureUri << ">" << endl;
+    return {};
 }
 
 void
@@ -481,7 +475,7 @@
 }
 
 void
-RDFImporterImpl::getDataModelsSparse(std::vector<Model *> &models,
+RDFImporterImpl::getDataModelsSparse(std::vector<ModelId> &models,
                                      ProgressReporter *reporter)
 {
     if (reporter) {
@@ -510,8 +504,8 @@
         (Triple(Node(), expand("a"), expand("mo:Signal"))).subjects();
 
     // Map from timeline uri to event type to dimensionality to
-    // presence of duration to model ptr.  Whee!
-    std::map<QString, std::map<QString, std::map<int, std::map<bool, Model *> > > >
+    // presence of duration to model id.  Whee!
+    std::map<QString, std::map<QString, std::map<int, std::map<bool, ModelId> > > >
         modelMap;
 
     foreach (Node sig, sigs) {
@@ -617,7 +611,7 @@
                 if (values.size() == 1) dimensions = 2;
                 else if (values.size() > 1) dimensions = 3;
 
-                Model *model = nullptr;
+                ModelId modelId;
 
                 if (modelMap[timeline][type][dimensions].find(haveDuration) ==
                     modelMap[timeline][type][dimensions].end()) {
@@ -628,7 +622,9 @@
                       << ", time = " << time << ", duration = " << duration
                       << endl;
 */
-            
+
+                    Model *model = nullptr;
+                    
                     if (!haveDuration) {
 
                         if (dimensions == 1) {
@@ -680,16 +676,20 @@
                     }
                     model->setObjectName(title);
 
-                    modelMap[timeline][type][dimensions][haveDuration] = model;
-                    models.push_back(model);
+                    modelId = ModelById::add(std::shared_ptr<Model>(model));
+                    modelMap[timeline][type][dimensions][haveDuration] = modelId;
+                    models.push_back(modelId);
                 }
 
-                model = modelMap[timeline][type][dimensions][haveDuration];
+                modelId = modelMap[timeline][type][dimensions][haveDuration];
 
-                if (model) {
-                    sv_frame_t ftime = RealTime::realTime2Frame(time, m_sampleRate);
-                    sv_frame_t fduration = RealTime::realTime2Frame(duration, m_sampleRate);
-                    fillModel(model, ftime, fduration, haveDuration, values, label);
+                if (!modelId.isNone()) {
+                    sv_frame_t ftime =
+                        RealTime::realTime2Frame(time, m_sampleRate);
+                    sv_frame_t fduration =
+                        RealTime::realTime2Frame(duration, m_sampleRate);
+                    fillModel(modelId, ftime, fduration,
+                              haveDuration, values, label);
                 }
             }
         }
@@ -697,7 +697,7 @@
 }
 
 void
-RDFImporterImpl::fillModel(Model *model,
+RDFImporterImpl::fillModel(ModelId modelId,
                            sv_frame_t ftime,
                            sv_frame_t fduration,
                            bool haveDuration,
@@ -706,17 +706,13 @@
 {
 //    SVDEBUG << "RDFImporterImpl::fillModel: adding point at frame " << ftime << endl;
 
-    SparseOneDimensionalModel *sodm =
-        dynamic_cast<SparseOneDimensionalModel *>(model);
-    if (sodm) {
+    if (auto sodm = ModelById::getAs<SparseOneDimensionalModel>(modelId)) {
         Event point(ftime, label);
         sodm->add(point);
         return;
     }
 
-    TextModel *tm =
-        dynamic_cast<TextModel *>(model);
-    if (tm) {
+    if (auto tm = ModelById::getAs<TextModel>(modelId)) {
         Event e
             (ftime,
              values.empty() ? 0.5f : values[0] < 0.f ? 0.f : values[0] > 1.f ? 1.f : values[0], // I was young and feckless once too
@@ -725,17 +721,13 @@
         return;
     }
 
-    SparseTimeValueModel *stvm =
-        dynamic_cast<SparseTimeValueModel *>(model);
-    if (stvm) {
+    if (auto stvm = ModelById::getAs<SparseTimeValueModel>(modelId)) {
         Event e(ftime, values.empty() ? 0.f : values[0], label);
         stvm->add(e);
         return;
     }
 
-    NoteModel *nm =
-        dynamic_cast<NoteModel *>(model);
-    if (nm) {
+    if (auto nm = ModelById::getAs<NoteModel>(modelId)) {
         if (haveDuration) {
             float value = 0.f, level = 1.f;
             if (!values.empty()) {
@@ -764,16 +756,14 @@
         return;
     }
 
-    RegionModel *rm = 
-        dynamic_cast<RegionModel *>(model);
-    if (rm) {
+    if (auto rm = ModelById::getAs<RegionModel>(modelId)) {
         float value = 0.f;
         if (values.empty()) {
             // no values? map each unique label to a distinct value
-            if (m_labelValueMap[model].find(label) == m_labelValueMap[model].end()) {
-                m_labelValueMap[model][label] = rm->getValueMaximum() + 1.f;
+            if (m_labelValueMap[modelId].find(label) == m_labelValueMap[modelId].end()) {
+                m_labelValueMap[modelId][label] = rm->getValueMaximum() + 1.f;
             }
-            value = m_labelValueMap[model][label];
+            value = m_labelValueMap[modelId][label];
         } else {
             value = values[0];
         }
--- a/rdf/RDFImporter.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/rdf/RDFImporter.h	Wed Jul 17 14:24:51 2019 +0100
@@ -22,8 +22,8 @@
 #include <vector>
 
 #include "base/BaseTypes.h"
+#include "data/model/Model.h"
 
-class Model;
 class RDFImporterImpl;
 class ProgressReporter;
 
@@ -47,7 +47,12 @@
     bool isOK();
     QString getErrorString() const;
 
-    std::vector<Model *> getDataModels(ProgressReporter *reporter);
+    /**
+     * Return a list of models imported from the RDF source. The
+     * models have been newly created and registered with ById; the
+     * caller must arrange to release them.
+     */
+    std::vector<ModelId> getDataModels(ProgressReporter *reporter);
 
     enum RDFDocumentType {
         AudioRefAndAnnotations,
--- a/transform/FeatureExtractionModelTransformer.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/transform/FeatureExtractionModelTransformer.cpp	Wed Jul 17 14:24:51 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,23 @@
              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());
+        m_outputs.push_back(ModelById::add(out));
     }
 }
 
@@ -540,13 +542,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 +553,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 +603,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 = ModelById::add
+        (std::shared_ptr<SparseTimeValueModel>(additional));
+    m_additionalModels[n][binNo] = additionalId;
+    return additionalId;
 }
 
 void
@@ -624,12 +622,6 @@
         m_message = e.what();
         return;
     }
-    
-    DenseTimeValueModel *input = getConformingInput();
-    if (!input) {
-        abandon();
-        return;
-    }
 
     if (m_outputs.empty()) {
         abandon();
@@ -638,19 +630,47 @@
 
     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;
 
-    sv_samplerate_t sampleRate = input->getSampleRate();
+    ModelId inputId = getInputModel();
 
-    int channelCount = input->getChannelCount();
-    if ((int)m_plugin->getMaxChannelCount() < channelCount) {
-        channelCount = 1;
+    sv_samplerate_t sampleRate;
+    int channelCount;
+    sv_frame_t startFrame;
+    sv_frame_t endFrame;
+    
+    { // scope so as not to have this borrowed pointer retained around
+      // the edges of the process loop
+        auto input = ModelById::getAs<DenseTimeValueModel>(inputId);
+        if (!input) {
+            abandon();
+            return;
+        }
+
+        sampleRate = input->getSampleRate();
+
+        channelCount = input->getChannelCount();
+        if ((int)m_plugin->getMaxChannelCount() < channelCount) {
+            channelCount = 1;
+        }
+
+        startFrame = input->getStartFrame();
+        endFrame = input->getEndFrame();
     }
 
     float **buffers = new float*[channelCount];
@@ -669,29 +689,25 @@
     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);
+                (inputId,
+                 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();
-
     RealTime contextStartRT = primaryTransform.getStartTime();
     RealTime contextDurationRT = primaryTransform.getDuration();
 
@@ -716,7 +732,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 +750,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 +770,39 @@
                 ((((blockFrame - contextStart) / stepSize) * 99) /
                  (contextDuration / stepSize + 1));
 
-            // channelCount is either m_input.getModel()->channelCount or 1
+            bool haveAllModels = true;
+            if (!ModelById::get(inputId)) {
+#ifdef DEBUG_FEATURE_EXTRACTION_TRANSFORMER_RUN
+                SVDEBUG << "FeatureExtractionModelTransformer::run: Input model " << inputId << " no longer exists" << endl;
+#endif
+                haveAllModels = false;
+            } else {
+#ifdef DEBUG_FEATURE_EXTRACTION_TRANSFORMER_RUN
+                SVDEBUG << "Input model " << inputId << " still exists" << endl;
+#endif
+            }
+            for (auto mid: m_outputs) {
+                if (!ModelById::get(mid)) {
+#ifdef DEBUG_FEATURE_EXTRACTION_TRANSFORMER_RUN
+                    SVDEBUG << "FeatureExtractionModelTransformer::run: Output model " << mid << " no longer exists" << endl;
+#endif
+                    haveAllModels = false;
+                } else {
+#ifdef DEBUG_FEATURE_EXTRACTION_TRANSFORMER_RUN
+                    SVDEBUG << "Output model " << mid << " still exists" << endl;
+#endif
+                }
+            }
+            if (!haveAllModels) {
+                abandon();
+                break;
+            }
+            
+#ifdef DEBUG_FEATURE_EXTRACTION_TRANSFORMER_RUN
+            SVDEBUG << "FeatureExtractionModelTransformer::run: All models still exist" << endl;
+#endif
+
+            // channelCount is either input->channelCount or 1
 
             if (frequencyDomain) {
                 for (int ch = 0; ch < channelCount; ++ch) {
@@ -781,22 +833,22 @@
 
             if (m_abandoned) break;
 
-            Vamp::Plugin::FeatureSet features = m_plugin->process
+            auto features = m_plugin->process
                 (buffers,
                  RealTime::frame2RealTime(blockFrame, sampleRate)
                  .toVampRealTime());
             
             if (m_abandoned) break;
 
-            for (int j = 0; j < (int)m_outputNos.size(); ++j) {
-                for (int fi = 0; fi < (int)features[m_outputNos[j]].size(); ++fi) {
-                    Vamp::Plugin::Feature feature = features[m_outputNos[j]][fi];
+            for (int j = 0; in_range_for(m_outputNos, j); ++j) {
+                for (int fi = 0; in_range_for(features[m_outputNos[j]], fi); ++fi) {
+                    auto feature = features[m_outputNos[j]][fi];
                     addFeature(j, blockFrame, feature);
                 }
             }
 
             if (blockFrame == contextStart || completion > prevCompletion) {
-                for (int j = 0; j < (int)m_outputNos.size(); ++j) {
+                for (int j = 0; in_range_for(m_outputNos, j); ++j) {
                     setCompletion(j, completion);
                 }
                 prevCompletion = completion;
@@ -807,12 +859,15 @@
         }
 
         if (!m_abandoned) {
-            Vamp::Plugin::FeatureSet features = m_plugin->getRemainingFeatures();
+            auto features = m_plugin->getRemainingFeatures();
 
-            for (int j = 0; j < (int)m_outputNos.size(); ++j) {
-                for (int fi = 0; fi < (int)features[m_outputNos[j]].size(); ++fi) {
-                    Vamp::Plugin::Feature feature = features[m_outputNos[j]][fi];
+            for (int j = 0; in_range_for(m_outputNos, j); ++j) {
+                for (int fi = 0; in_range_for(features[m_outputNos[j]], fi); ++fi) {
+                    auto feature = features[m_outputNos[j]][fi];
                     addFeature(j, blockFrame, feature);
+                    if (m_abandoned) {
+                        break;
+                    }
                 }
             }
         }
@@ -863,8 +918,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 +964,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 +977,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 +993,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 +1009,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,21 +1031,20 @@
     // to, we instead test what sort of model the constructor decided
     // to create.
 
-    if (isOutput<SparseOneDimensionalModel>(n)) {
+    ModelId outputId = m_outputs[n];
 
-        SparseOneDimensionalModel *model =
-            getConformingOutput<SparseOneDimensionalModel>(n);
+    if (isOutputType<SparseOneDimensionalModel>(n)) {
+
+        auto model = ModelById::getAs<SparseOneDimensionalModel>(outputId);
+        if (!model) return;
+        model->add(Event(frame, feature.label.c_str()));
+        
+    } else if (isOutputType<SparseTimeValueModel>(n)) {
+
+        auto model = ModelById::getAs<SparseTimeValueModel>(outputId);
         if (!model) return;
 
-        model->add(Event(frame, feature.label.c_str()));
-        
-    } else if (isOutput<SparseTimeValueModel>(n)) {
-
-        SparseTimeValueModel *model =
-            getConformingOutput<SparseTimeValueModel>(n);
-        if (!model) return;
-
-        for (int i = 0; i < (int)feature.values.size(); ++i) {
+        for (int i = 0; in_range_for(feature.values, i); ++i) {
 
             float value = feature.values[i];
 
@@ -994,20 +1053,19 @@
                 label = QString("[%1] %2").arg(i+1).arg(label);
             }
 
-            SparseTimeValueModel *targetModel = model;
+            auto targetModel = model;
 
             if (m_needAdditionalModels[n] && i > 0) {
-                targetModel = getAdditionalModel(n, i);
+                targetModel = ModelById::getAs<SparseTimeValueModel>
+                    (getAdditionalModel(n, i));
                 if (!targetModel) targetModel = model;
-//                std::cerr << "adding point to model " << targetModel
-//                          << " for output " << n << " bin " << i << std::endl;
             }
 
             targetModel->add(Event(frame, value, label));
         }
 
-    } else if (isOutput<NoteModel>(n) || isOutput<RegionModel>(n)) {
-
+    } else if (isOutputType<NoteModel>(n) || isOutputType<RegionModel>(n)) {
+    
         int index = 0;
 
         float value = 0.0;
@@ -1024,7 +1082,8 @@
             }
         }
 
-        if (isOutput<NoteModel>(n)) {
+        auto noteModel = ModelById::getAs<NoteModel>(outputId);
+        if (noteModel) {
 
             float velocity = 100;
             if ((int)feature.values.size() > index) {
@@ -1032,62 +1091,58 @@
             }
             if (velocity < 0) velocity = 127;
             if (velocity > 127) velocity = 127;
+            
+            noteModel->add(Event(frame, value, // value is pitch
+                                 duration,
+                                 velocity / 127.f,
+                                 feature.label.c_str()));
+        }
 
-            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;
-
+        auto regionModel = ModelById::getAs<RegionModel>(outputId);
+        if (regionModel) {
+            
             if (feature.hasDuration && !feature.values.empty()) {
-
-                for (int i = 0; i < (int)feature.values.size(); ++i) {
-
+                
+                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);
                     }
-
-                    model->add(Event(frame,
-                                     value,
-                                     duration,
-                                     label));
+                    
+                    regionModel->add(Event(frame,
+                                           value,
+                                           duration,
+                                           label));
                 }
             } else {
-            
-                model->add(Event(frame,
-                                 value,
-                                 duration,
-                                 feature.label.c_str()));
+                
+                regionModel->add(Event(frame,
+                                       value,
+                                       duration,
+                                       feature.label.c_str()));
             }
         }
-        
-    } else if (isOutput<EditableDenseThreeDimensionalModel>(n)) {
+
+    } else if (isOutputType<EditableDenseThreeDimensionalModel>(n)) {
+
+        auto model = ModelById::getAs
+            <EditableDenseThreeDimensionalModel>(outputId);
+        if (!model) return;
         
         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 {
-        SVDEBUG << "FeatureExtractionModelTransformer::addFeature: Unknown output model type!" << endl;
+        
+        SVDEBUG << "FeatureExtractionModelTransformer::addFeature: Unknown output model type - possibly a deleted model" << endl;
+        abandon();
     }
 }
 
@@ -1099,43 +1154,11 @@
               << completion << ")" << endl;
 #endif
 
-    if (isOutput<SparseOneDimensionalModel>(n)) {
-
-        SparseOneDimensionalModel *model =
-            getConformingOutput<SparseOneDimensionalModel>(n);
-        if (!model) return;
-        if (model->isAbandoning()) abandon();
-        model->setCompletion(completion, true);
-
-    } else if (isOutput<SparseTimeValueModel>(n)) {
-
-        SparseTimeValueModel *model =
-            getConformingOutput<SparseTimeValueModel>(n);
-        if (!model) return;
-        if (model->isAbandoning()) abandon();
-        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);
-    }
+    (void)
+        (setOutputCompletion<SparseOneDimensionalModel>(n, completion) ||
+         setOutputCompletion<SparseTimeValueModel>(n, completion) ||
+         setOutputCompletion<NoteModel>(n, completion) ||
+         setOutputCompletion<RegionModel>(n, completion) ||
+         setOutputCompletion<EditableDenseThreeDimensionalModel>(n, completion));
 }
 
--- a/transform/FeatureExtractionModelTransformer.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/transform/FeatureExtractionModelTransformer.h	Wed Jul 17 14:24:51 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,
@@ -82,24 +95,21 @@
     QWaitCondition m_outputsCondition;
     void awaitOutputModels() override;
     
-    // just casts:
-
-    DenseTimeValueModel *getConformingInput();
-
-    template <typename ModelClass> bool isOutput(int n) {
-        return dynamic_cast<ModelClass *>(m_outputs[n]) != 0;
+    template <typename T> bool isOutputType(int n) {
+        if (!ModelById::getAs<T>(m_outputs[n])) {
+            return false;
+        } else {
+            return true;
+        }
     }
 
-    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;
+    template <typename T> bool setOutputCompletion(int n, int completion) {
+        auto model = ModelById::getAs<T>(m_outputs[n]);
+        if (!model) {
+            return false;
         } else {
-            std::cerr << "FeatureExtractionModelTransformer::getOutput: No such output number " << n << std::endl;
-            return 0;
+            model->setCompletion(completion, true);
+            return true;
         }
     }
 };
--- a/transform/ModelTransformer.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/transform/ModelTransformer.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -17,8 +17,6 @@
 
 ModelTransformer::ModelTransformer(Input input, const Transform &transform) :
     m_input(input),
-    m_detached(false),
-    m_detachedAdd(false),
     m_abandoned(false)
 {
     m_transforms.push_back(transform);
@@ -27,8 +25,6 @@
 ModelTransformer::ModelTransformer(Input input, const Transforms &transforms) :
     m_transforms(transforms),
     m_input(input),
-    m_detached(false),
-    m_detachedAdd(false),
     m_abandoned(false)
 {
 }
@@ -37,13 +33,5 @@
 {
     m_abandoned = true;
     wait();
-    if (!m_detached) {
-        Models mine = getOutputModels();
-        foreach (Model *m, mine) delete m;
-    }
-    if (!m_detachedAdd) {
-        Models mine = getAdditionalOutputModels();
-        foreach (Model *m, mine) delete m;
-    }
 }
 
--- a/transform/ModelTransformer.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/transform/ModelTransformer.h	Wed Jul 17 14:24:51 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;
 };
--- a/transform/ModelTransformerFactory.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/transform/ModelTransformerFactory.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -56,8 +56,8 @@
 
 ModelTransformer::Input
 ModelTransformerFactory::getConfigurationForTransform(Transform &transform,
-                                                      const vector<Model *> &candidateInputModels,
-                                                      Model *defaultInputModel,
+                                                      vector<ModelId> candidateInputModels,
+                                                      ModelId defaultInputModel,
                                                       AudioPlaySource *source,
                                                       sv_frame_t startFrame,
                                                       sv_frame_t duration,
@@ -65,26 +65,39 @@
 {
     QMutexLocker locker(&m_mutex);
     
-    ModelTransformer::Input input(nullptr);
+    ModelTransformer::Input input({});
 
     if (candidateInputModels.empty()) return input;
 
     //!!! This will need revision -- we'll have to have a callback
     //from the dialog for when the candidate input model is changed,
     //as we'll need to reinitialise the channel settings in the dialog
-    Model *inputModel = candidateInputModels[0];
+    ModelId inputModel = candidateInputModels[0];
     QStringList candidateModelNames;
     QString defaultModelName;
-    QMap<QString, Model *> modelMap;
-    for (int i = 0; i < (int)candidateInputModels.size(); ++i) {
-        QString modelName = candidateInputModels[i]->objectName();
+    QMap<QString, ModelId> modelMap;
+
+    sv_samplerate_t defaultSampleRate;
+    {   auto im = ModelById::get(inputModel);
+        if (!im) return input;
+        defaultSampleRate = im->getSampleRate();
+    }
+    
+    for (int i = 0; in_range_for(candidateInputModels, i); ++i) {
+
+        auto model = ModelById::get(candidateInputModels[i]);
+        if (!model) return input;
+        
+        QString modelName = model->objectName();
         QString origModelName = modelName;
         int dupcount = 1;
         while (modelMap.contains(modelName)) {
             modelName = tr("%1 <%2>").arg(origModelName).arg(++dupcount);
         }
+        
         modelMap[modelName] = candidateInputModels[i];
         candidateModelNames.push_back(modelName);
+        
         if (candidateInputModels[i] == defaultInputModel) {
             defaultModelName = modelName;
         }
@@ -105,7 +118,7 @@
         
         RealTimePluginFactory *factory = RealTimePluginFactory::instanceFor(id);
 
-        sv_samplerate_t sampleRate = inputModel->getSampleRate();
+        sv_samplerate_t sampleRate = defaultSampleRate;
         int blockSize = 1024;
         int channels = 1;
         if (source) {
@@ -125,7 +138,7 @@
 
         Vamp::Plugin *vp =
             FeatureExtractionPluginFactory::instance()->instantiatePlugin
-            (id, float(inputModel->getSampleRate()));
+            (id, float(defaultSampleRate));
 
         plugin = vp;
     }
@@ -196,7 +209,7 @@
     return transformer;
 }
 
-Model *
+ModelId
 ModelTransformerFactory::transform(const Transform &transform,
                                    const ModelTransformer::Input &input,
                                    QString &message,
@@ -206,12 +219,12 @@
 
     Transforms transforms;
     transforms.push_back(transform);
-    vector<Model *> mm = transformMultiple(transforms, input, message, handler);
-    if (mm.empty()) return nullptr;
+    vector<ModelId> mm = transformMultiple(transforms, input, message, handler);
+    if (mm.empty()) return {};
     else return mm[0];
 }
 
-vector<Model *>
+vector<ModelId>
 ModelTransformerFactory::transformMultiple(const Transforms &transforms,
                                            const ModelTransformer::Input &input,
                                            QString &message,
@@ -220,9 +233,12 @@
     SVDEBUG << "ModelTransformerFactory::transformMultiple: Constructing transformer with input model " << input.getModel() << endl;
     
     QMutexLocker locker(&m_mutex);
+
+    auto inputModel = ModelById::get(input.getModel());
+    if (!inputModel) return {};
     
     ModelTransformer *t = createTransformer(transforms, input);
-    if (!t) return vector<Model *>();
+    if (!t) return {};
 
     if (handler) {
         m_handlers[t] = handler;
@@ -233,22 +249,24 @@
     connect(t, SIGNAL(finished()), this, SLOT(transformerFinished()));
 
     t->start();
-    vector<Model *> models = t->detachOutputModels();
-
+    vector<ModelId> models = t->getOutputModels();
+    
     if (!models.empty()) {
-        QString imn = input.getModel()->objectName();
+        QString imn = inputModel->objectName();
         QString trn =
             TransformFactory::getInstance()->getTransformFriendlyName
             (transforms[0].getIdentifier());
-        for (int i = 0; i < (int)models.size(); ++i) {
+        for (int i = 0; in_range_for(models, i); ++i) {
+            auto model = ModelById::get(models[i]);
+            if (!model) continue;
             if (imn != "") {
                 if (trn != "") {
-                    models[i]->setObjectName(tr("%1: %2").arg(imn).arg(trn));
+                    model->setObjectName(tr("%1: %2").arg(imn).arg(trn));
                 } else {
-                    models[i]->setObjectName(imn);
+                    model->setObjectName(imn);
                 }
             } else if (trn != "") {
-                models[i]->setObjectName(trn);
+                model->setObjectName(trn);
             }
         }
     } else {
@@ -284,12 +302,12 @@
 
     m_runningTransformers.erase(transformer);
 
-    map<AdditionalModelHandler *, vector<Model *>> toNotifyOfMore;
+    map<AdditionalModelHandler *, vector<ModelId>> toNotifyOfMore;
     vector<AdditionalModelHandler *> toNotifyOfNoMore;
     
     if (m_handlers.find(transformer) != m_handlers.end()) {
         if (transformer->willHaveAdditionalOutputModels()) {
-            vector<Model *> mm = transformer->detachAdditionalOutputModels();
+            vector<ModelId> mm = transformer->getAdditionalOutputModels();
             toNotifyOfMore[m_handlers[transformer]] = mm;
         } else {
             toNotifyOfNoMore.push_back(m_handlers[transformer]);
@@ -299,11 +317,9 @@
 
     m_mutex.unlock();
 
-    // These calls have to be made without the mutex held, as they may
-    // ultimately call back on us (e.g. we have one baroque situation
-    // where this could trigger a command to create a layer, which
-    // triggers the command history to clip the stack, which deletes a
-    // spare old model, which calls back on our modelAboutToBeDeleted)
+    // We make these calls without the mutex held, in case they
+    // ultimately call back on us - not such a concern as in the old
+    // model lifecycle but just in case
     
     for (const auto &i: toNotifyOfMore) {
         i.first->moreModelsAvailable(i.second);
@@ -322,43 +338,6 @@
     delete transformer;
 }
 
-void
-ModelTransformerFactory::modelAboutToBeDeleted(Model *m)
-{
-    TransformerSet affected;
-
-    {
-        QMutexLocker locker(&m_mutex);
-    
-        for (TransformerSet::iterator i = m_runningTransformers.begin();
-             i != m_runningTransformers.end(); ++i) {
-
-            ModelTransformer *t = *i;
-
-            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);
-                }
-            }
-        }
-    }
-
-    for (TransformerSet::iterator i = affected.begin();
-         i != affected.end(); ++i) {
-
-        ModelTransformer *t = *i;
-
-        t->abandon();
-
-        t->wait(); // this should eventually call back on
-                   // transformerFinished, which will remove from
-                   // m_runningTransformers and delete.
-    }
-}
-
 bool
 ModelTransformerFactory::haveRunningTransformers() const
 {
--- a/transform/ModelTransformerFactory.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/transform/ModelTransformerFactory.h	Wed Jul 17 14:24:51 2019 +0100
@@ -46,11 +46,11 @@
         virtual bool configure(ModelTransformer::Input &input,
                                Transform &transform,
                                Vamp::PluginBase *plugin,
-                               Model *&inputModel,
+                               ModelId &inputModel,
                                AudioPlaySource *source,
                                sv_frame_t startFrame,
                                sv_frame_t duration,
-                               const QMap<QString, Model *> &modelMap,
+                               const QMap<QString, ModelId> &modelMap,
                                QStringList candidateModelNames,
                                QString defaultModelName) = 0;
     };
@@ -59,14 +59,14 @@
      * Fill out the configuration for the given transform (may include
      * asking the user by calling back on the UserConfigurator).
      * Returns the selected input model and channel if the transform
-     * is acceptable, or an input with a null model if the operation
+     * is acceptable, or an input with no model if the operation
      * should be cancelled.  Audio play source may be used to audition
      * effects plugins, if provided.
      */
     ModelTransformer::Input
     getConfigurationForTransform(Transform &transform,
-                                 const std::vector<Model *> &candidateInputModels,
-                                 Model *defaultInputModel,
+                                 std::vector<ModelId> candidateInputModels,
+                                 ModelId defaultInputModel,
                                  AudioPlaySource *source = 0,
                                  sv_frame_t startFrame = 0,
                                  sv_frame_t duration = 0,
@@ -77,7 +77,7 @@
         virtual ~AdditionalModelHandler() { }
 
         // Exactly one of these functions will be called
-        virtual void moreModelsAvailable(std::vector<Model *> models) = 0;
+        virtual void moreModelsAvailable(std::vector<ModelId> models) = 0;
         virtual void noMoreModelsAvailable() = 0;
     };
     
@@ -104,10 +104,10 @@
      * 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,
-                     AdditionalModelHandler *handler = 0);
+    ModelId transform(const Transform &transform,
+                      const ModelTransformer::Input &input,
+                      QString &message,
+                      AdditionalModelHandler *handler = 0);
 
     /**
      * Return the multiple output models resulting from applying the
@@ -141,7 +141,7 @@
      * The returned models are owned by the caller and must be deleted
      * when no longer needed.
      */
-    std::vector<Model *> transformMultiple(const Transforms &transform,
+    std::vector<ModelId> transformMultiple(const Transforms &transform,
                                            const ModelTransformer::Input &input,
                                            QString &message,
                                            AdditionalModelHandler *handler = 0);
@@ -154,8 +154,6 @@
 protected slots:
     void transformerFinished();
 
-    void modelAboutToBeDeleted(Model *);
-
 protected:
     ModelTransformer *createTransformer(const Transforms &transforms,
                                         const ModelTransformer::Input &input);
--- a/transform/RealTimeEffectModelTransformer.cpp	Thu Jun 20 14:58:20 2019 +0100
+++ b/transform/RealTimeEffectModelTransformer.cpp	Wed Jul 17 14:24:51 2019 +0100
@@ -58,8 +58,11 @@
         return;
     }
 
-    DenseTimeValueModel *input = getConformingInput();
-    if (!input) return;
+    auto input = ModelById::getAs<DenseTimeValueModel>(getInputModel());
+    if (!input) {
+        SVCERR << "RealTimeEffectModelTransformer: Input is absent or of wrong type" << endl;
+        return;
+    }
 
     m_plugin = factory->instantiatePlugin(pluginId, 0, 0,
                                           input->getSampleRate(),
@@ -87,19 +90,19 @@
             outputChannels = input->getChannelCount();
         }
 
-        WritableWaveFileModel *model = new WritableWaveFileModel
+        auto model = std::make_shared<WritableWaveFileModel>
             (input->getSampleRate(), outputChannels);
 
-        m_outputs.push_back(model);
+        m_outputs.push_back(ModelById::add(model));
 
     } else {
         
-        SparseTimeValueModel *model = new SparseTimeValueModel
-            (input->getSampleRate(), transform.getBlockSize(), 0.0, 0.0, false);
-
+        auto model = std::make_shared<SparseTimeValueModel>
+            (input->getSampleRate(), transform.getBlockSize(),
+             0.0, 0.0, false);
         if (m_units != "") model->setScaleUnits(m_units);
 
-        m_outputs.push_back(model);
+        m_outputs.push_back(ModelById::add(model));
     }
 }
 
@@ -108,38 +111,58 @@
     delete m_plugin;
 }
 
-DenseTimeValueModel *
-RealTimeEffectModelTransformer::getConformingInput()
-{
-    DenseTimeValueModel *dtvm =
-        dynamic_cast<DenseTimeValueModel *>(getInputModel());
-    if (!dtvm) {
-        SVDEBUG << "RealTimeEffectModelTransformer::getConformingInput: WARNING: Input model is not conformable to DenseTimeValueModel" << endl;
-    }
-    return dtvm;
-}
-
 void
 RealTimeEffectModelTransformer::run()
 {
-    DenseTimeValueModel *input = getConformingInput();
-    if (!input) return;
-
-    while (!input->isReady() && !m_abandoned) {
-        SVDEBUG << "RealTimeEffectModelTransformer::run: Waiting for input model to be ready..." << endl;
-        usleep(500000);
-    }
-    if (m_abandoned) {
+    if (m_outputs.empty()) {
+        abandon();
         return;
     }
-    if (m_outputs.empty()) {
+
+    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 << "RealTimeEffectModelTransformer::run: Waiting for input model to be ready..." << endl;
+            usleep(500000);
+        }
+    }
+    if (m_abandoned) return;
+
+    auto input = ModelById::getAs<DenseTimeValueModel>(getInputModel());
+    if (!input) {
+        abandon();
         return;
     }
-    
-    SparseTimeValueModel *stvm =
-        dynamic_cast<SparseTimeValueModel *>(m_outputs[0]);
-    WritableWaveFileModel *wwfm =
-        dynamic_cast<WritableWaveFileModel *>(m_outputs[0]);
+
+    sv_samplerate_t sampleRate;
+    int channelCount;
+    sv_frame_t startFrame;
+    sv_frame_t endFrame;
+
+    { // scope so as not to have this borrowed pointer retained around
+      // the edges of the process loop
+        auto input = ModelById::getAs<DenseTimeValueModel>(getInputModel());
+        if (!input) {
+            abandon();
+            return;
+        }
+
+        sampleRate = input->getSampleRate();
+        channelCount = input->getChannelCount();
+        startFrame = input->getStartFrame();
+        endFrame = input->getEndFrame();
+    }
+
+    auto stvm = ModelById::getAs<SparseTimeValueModel>(m_outputs[0]);
+    auto wwfm = ModelById::getAs<WritableWaveFileModel>(m_outputs[0]);
 
     if (!stvm && !wwfm) {
         return;
@@ -149,17 +172,12 @@
         return;
     }
 
-    sv_samplerate_t sampleRate = input->getSampleRate();
-    int channelCount = input->getChannelCount();
     if (!wwfm && m_input.getChannel() != -1) channelCount = 1;
 
     sv_frame_t blockSize = m_plugin->getBufferSize();
 
     float **inbufs = m_plugin->getAudioInputBuffers();
 
-    sv_frame_t startFrame = m_input.getModel()->getStartFrame();
-    sv_frame_t endFrame = m_input.getModel()->getEndFrame();
-
     Transform transform = m_transforms[0];
     
     RealTime contextStartRT = transform.getStartTime();
@@ -201,6 +219,12 @@
 
         sv_frame_t got = 0;
 
+        auto input = ModelById::getAs<DenseTimeValueModel>(getInputModel());
+        if (!input) {
+            abandon();
+            return;
+        }
+
         if (channelCount == 1) {
             if (inbufs && inbufs[0]) {
                 auto data = input->getData
@@ -242,20 +266,6 @@
             }
         }
 
-/*
-        cerr << "Input for plugin: " << m_plugin->getAudioInputCount() << " channels "<< endl;
-
-        for (int ch = 0; ch < m_plugin->getAudioInputCount(); ++ch) {
-            cerr << "Input channel " << ch << endl;
-            for (int i = 0; i < 100; ++i) {
-                cerr << inbufs[ch][i] << " ";
-                if (isnan(inbufs[ch][i])) {
-                    cerr << "\n\nWARNING: NaN in audio input" << endl;
-                }
-            }
-        }
-*/
-
         m_plugin->run(RealTime::frame2RealTime(blockFrame, sampleRate));
 
         if (stvm) {
--- a/transform/RealTimeEffectModelTransformer.h	Thu Jun 20 14:58:20 2019 +0100
+++ b/transform/RealTimeEffectModelTransformer.h	Wed Jul 17 14:24:51 2019 +0100
@@ -36,9 +36,6 @@
     QString m_units;
     RealTimePluginInstance *m_plugin;
     int m_outputNo;
-
-    // just casts
-    DenseTimeValueModel *getConformingInput();
 };
 
 #endif